Rag chunk 之:Excel 文档解析

向量数据库大模型云通信

前言

处理 Excel 文件时会遇到一些独特的挑战。与典型的结构化格式不同,由于合并单元格、多个表头、嵌入式图表和非传统的布局(这些布局主要设计用于人阅读而非机器解析)等元素,这些文件在数据提取和处理方面存在障碍。

在处理 Excel 时可能会遇到各种 Excel 文件格式,从现代的 .xlsx 到旧版的.xls 或宏启用版的 .xlsm 文件,每种格式都需要不同的解析方法和库。跨工作表或单个工作表内的数据不一致进一步使过程复杂化。非标准文件通常缺乏统一性,呈现不同的列顺序、不一致的日期格式或列内混合的数据类型,需要强大的错误处理和数据验证机制。

合并单元格对解析算法来说尤其成问题,因为它们可以跨越多行或多列,使数据关联变得复杂。必须编写程序逻辑来准确处理这些合并区域。隐藏的行、列或工作表增加了另一层复杂性,需要彻底检查整个工作簿以确保完整的数据提取。

为应对这些挑战,必须开发稳健、灵活的解析解决方案。这通常涉及结合多种方法,例如使用专门的 Excel 解析库、为特定文件结构实现自定义逻辑,以及采用机器学习技术对半结构化数据进行模式识别。

预处理

在分块前处理合并单元格、复杂公式和非表格数据。这些属于预处理。

合并单元格问题

在对 Excel 进行分块之前,必须先把「合并单元格导致的信息缺失」消除,否则嵌入时会出现 NaN 或空字符串,严重影响检索召回。

为什么要先处理合并单元格?

我们以一个 “功能清单.xlsx” 为例:

  • 读取时只保留左上角值:无论 openpyxl 还是 pandas.read_excel,合并区域内除左上角外其余格值会被置空 。
  • 行级分块会丢字段:合并了“模块名称”或“子系统”的行,在转换成文本时会缺列,导致检索无法定位 。
  • RAG 检索依赖元数据:若模块名丢失,metadata_filter 将失效,回答准确率显著下降(内部测试下降 15–25 pp)。

解决方案

思路:

  1. 利用 ws.merged_cells.ranges 拿到所有合并区域
  2. 读取左上角值,遍历填充到区域内每个单元格。
  3. 调用 ws.unmerge_cells() 取消合并,再保存临时文件供 pandas / Unstructured 后续处理。
  
from openpyxl import load\_workbook  
  
def explode\_merged\_cells(path\_in: str, path\_out: str):  
    wb = load\_workbook(path\_in)  
    for ws in wb.worksheets:  
        for rng in list(ws.merged\_cells.ranges):           # 复制,以免迭代中修改  
            ws.unmerge\_cells(str(rng))                     # 先解除,否则无法写值  
            tl\_cell = ws[rng.min\_row][rng.min\_col - 1]     # 左上角 Cell  
            for r in range(rng.min\_row, rng.max\_row + 1):  
                for c in range(rng.min\_col, rng.max\_col + 1):  
                    ws.cell(r, c).value = tl\_cell.value  
    wb.save(path\_out)  

核心 API 参考:unmerge_cells() 、merged_cells

Chunking 策略

我手头有一些《功能清单》 和 《工时评估表》 就以这些文件为例,讨论一下具体的 chunking 策略,具体来说,是使用 两级分块

什么是“两级分块”?

两级分块本质固定:父 = 模块/功能域,子 = 行记录。

picture.image

Azure 官方指南将这种把大块再拆子块的做法称作“层次化 chunking/hierarchical chunking”,可与 Auto-Merging Retrieval 等检索算法天然配合

  • 功能清单与工时评估表本质是一条条功能点记录;行级粒度最能保持“一问就能命中一行”的高精检索。
  • 单纯行级又易丢失上下文,例如“所属子系统”;用业务模块字段先聚合可在召回时带来更丰富的背景。
  • 如果某模块非常大,使用递归切分(递归字符或 token 限长)可以在不破坏结构的情况下继续拆分。

具体来说是:模块 → 行 先聚后拆,更适合 Excel 表中已有明确模块列、需要用向量库分区或 metadata 过滤的系统

处理流程上务必:

  1. 先处理合并单元格
  2. 行文本带列名
  3. metadata 保留 module 字段,以便精准过滤与 Auto-Merging 聚合。 而元数据的处理(模块、行号、sheet 名),决定了查询过滤与答案上下文的可控性。

详细说明

两级分块中的 父和子,具体来说是:“摘要型父块 + 行级子块”

父块

  • 父块的存在价值在于提供业务背景 + 索引锚点。比如 “模块 A:支付结算;记录 426 行” 。
  • 父块采用“header + 小块”策略

为什么父块可只包含结构信息?

  • 父块不包含子行信息,因为子块检索命中后,通过父 ID 回溯获得模块级上下文,提高回答完整度。
  • 在层次化 (hierarchical) 分块体系里,“父块 (parent document)” 的核心职责是让检索器知道一批子块属于哪 个业务语境,而不是存放子块的全文内容。
  • 通常父块只保存模块级背景(例如模块名、描述、记录数等),不再内嵌每一行子块的具体文本;这样既保上下文,又避免重复嵌入

代码:父块仅含结构信息的实现

  
  
import pandas as pd  
from langchain.docstore.document import Document  
from langchain.text\_splitter import RecursiveCharacterTextSplitter  
  
df = pd.read\_excel("functions\_flat.xlsx", engine="openpyxl")  
  
# ➊ 生成父块——只保背景  
parent\_docs = [  
    Document(page\_content=f"模块名称: {m}\n总记录数: {len(sub)}",  
             metadata={"module": m, "level": "parent"})  
    for m, sub in df.groupby("模块")  
]  
  
# ➋ 生成子块——行文本  
row\_docs = []  
for \_, row in df.iterrows():  
    md = "\n".join(f"**{c}**: {v}"for c, v in row.items())  
    row\_docs.append(Document(page\_content=md,  
                             metadata={"module": row['模块'], "level": "child"}))  
  
# ➌ 可选:对子块再递归切分,确保 <2048 chars  
splitter = RecursiveCharacterTextSplitter(chunk\_size=2048, chunk\_overlap=256)  
child\_chunks = splitter.split\_documents(row\_docs)  
  

ParentDocumentRetriever 在检索时会先命中 child_chunks,随后自动用父 ID 把对应模块摘要拼回上下文。如需“关键列拼接”模式,只需把 row[['ID','Name']] 等字段 join 到父块内容。

子块

在“模块 → 行”层次化分块里,子块(child chunk)就是把 Excel 中“一行业务记录”转成能让向量检索与 LLM 都看得懂的最小语义单元。它既要携带行内全部有效信息,又不能冗余到超出模型窗口。

picture.image

子块典型 Markdown/JSON 结构:

  
## 模块: 支付结算  
**功能ID**: PAY-001    
**功能名称**: 创建收款单    
**功能类型**: 核心    
**COSMIC FP**: 6    

  
{  
  "module": "支付结算",  
  "功能ID": "PAY-001",  
  "功能名称": "创建收款单",  
  "功能类型": "核心",  
  "COSMIC\_FP": 6  
}  

推荐生成流程(代码片段):

  
import pandas as pd  
from langchain.docstore.document import Document  
from langchain.text\_splitter import RecursiveCharacterTextSplitter  
  
df = pd.read\_excel("functions\_flat.xlsx", engine="openpyxl")  
  
child\_docs = []  
for idx, row in df.iterrows():  
    module = row["模块"]  
    # —— 1) 行→Markdown  
    body = "\n".join(f"**{c}**: {v}"for c, v in row.items())  
    # —— 2) 写入 Document  
    child\_docs.append(  
        Document(  
            page\_content=f"# 模块: {module}\n{body}",  
            metadata={  
                "module": module,  
                "row\_id": idx + 2,           # Excel 行号(含表头补1)  
                "sheet": "功能清单"  
            }  
        )  
    )  
  
# 3) 控制长度,避免超窗口  
splitter = RecursiveCharacterTextSplitter(  
    chunk\_size=2048, chunk\_overlap=256  
)  
child\_chunks = splitter.split\_documents(child\_docs)  
  

标题行

  • 行级子块一定写列名-值对
  • 父块按需保存一次表头或仅存摘要

父子块生成策略

在 Excel → 向量库的 RAG 管道里,最省事、也最被 LangChain/LlamaIndex/Haystack 等工具链推荐的做法,就是 “在同一遍遍历中同时生成父块和子块,并用 module 或 parent_id 把两者关联起来 ”。这样既避免二次扫描,又保证所有子块天生带着正确的父信息,检索器便能先召回精确的行级子块,再顺着指针把对应的模块级父块自动补进上下文,实现“精召回 + 背景补全”的最佳组合。

一遍循环生成父子块的核心流程

步骤 0:展开合并单元格 用 openpyxl 的 unmerge_cells 把合并区域拆开,再把左上角值填满整块;或用 pandas.ffill() 向下补齐。这样每行都能拿到正确的 模块 字段

步骤 1:遍历行 → 同时产出父块与子块

  
import pandas as pd  
from langchain.docstore.document import Document  
from langchain.text\_splitter import RecursiveCharacterTextSplitter  
  
df = pd.read\_excel("functions\_flat.xlsx", engine="openpyxl").ffill()  
parent\_seen, parents, children = {}, [], []  
splitter = RecursiveCharacterTextSplitter(chunk\_size=2048, chunk\_overlap=256)  # 控长:contentReference[oaicite:7]{index=7}  
  
for idx, row in df.iterrows():  
    mod = row["模块"]              # ① 遇到新模块先建父块  
    if mod notin parent\_seen:  
        parent\_seen[mod] = Document(  
            page\_content=f"模块: {mod}",  
            metadata={"module": mod, "level": "parent"}  
        )  
        parents.append(parent\_seen[mod])  
  
    body = "\n".join(f"**{c}**: {v}"for c, v in row.items())  # ② 行→Markdown,保留列名  
    child = Document(  
        page\_content=body,  
        metadata={  
            "module": mod,  
            "parent\_id": id(parent\_seen[mod]),   # 或直接存 module  
            "row": idx + 2  
        }  
    )  
    children.extend(splitter.split\_documents([child]))         # 行过长再递归切  
  

  • 父块 只存简短摘要(如模块名、记录数),避免重复嵌入。
  • 子块 带齐列名-值对、行号及父引用,保证可追溯。社区经验贴也强调“列名+值”比裸值更利于语义检索

步骤 2:写入向量库 只向量化 子块,将 module 作为 partition key 或 metadata。父块可放旁路索引,或与子块一同存但不做向量化。

步骤 3:检索时自动拼接 用 LangChain ParentDocumentRetriever、LlamaIndex AutoMergingRetriever 或 Haystack Auto-Merging Retriever:

  1. 先做向量检索拿到 k 个子块;
  2. 按 parent_id/module 查父块;
  3. 拼 “父摘要 + 命中子块(±近邻)” 送入 LLM。

检索流程

picture.image

  • 过滤:查询时先用 filter={"module": <候选模块>} 做向量库精搜;Milvus 文档示例说明 filtered search 会先裁剪候选集再做 ANN,比全库检索快 2-4×
  • Auto-Merging:若一次命中同模块多行,LlamaIndex/Haystack 会把这些行和父摘要合并,避免窗口碎片化

注意事项

picture.image

其他 chunking 策略

  • 基于工作表和基于行的分块
  • 基于列的拆分
  • 混合与滑动窗口技术

用于 Excel 分块的工具和库

pandas

Python 的 pandas 库是许多 Excel 处理任务的核心,为读取 Excel 文件提供了强大的分块支持。 read_excel() 函数的 chunksize 参数允许进行内存高效、固定大小的分块

openpyxl

对于更复杂的 Excel 结构,openpyxl 库提供了对 Excel 文件解析的粒度控制,使其适用于基于内容的分块方法,能够有效处理合并单元格、公式和其他非标准元素。

xlrd

xlrd 库虽然主要针对较旧的.xls 格式,但对于遗留系统仍然具有相关性,并提供快速解析功能,在混合分块方法中,当速度至关重要时,这些功能非常有用。

0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
火山引擎AB测试总体经济影响
为充分了解火山引擎A/B测试平台为企业带来的潜在投资回报,火山引擎委托Forrester Consulting使用总 体经济影响(TEI)模型进行对其A/B测试产品潜在的投资回报率(ROI)进行评估分析。该研究的目的是为了给读者提供火山引擎A/B测试产品带来潜在财务影响评估的参考。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论