前言
处理 Excel 文件时会遇到一些独特的挑战。与典型的结构化格式不同,由于合并单元格、多个表头、嵌入式图表和非传统的布局(这些布局主要设计用于人阅读而非机器解析)等元素,这些文件在数据提取和处理方面存在障碍。
在处理 Excel 时可能会遇到各种 Excel 文件格式,从现代的 .xlsx 到旧版的.xls 或宏启用版的 .xlsm 文件,每种格式都需要不同的解析方法和库。跨工作表或单个工作表内的数据不一致进一步使过程复杂化。非标准文件通常缺乏统一性,呈现不同的列顺序、不一致的日期格式或列内混合的数据类型,需要强大的错误处理和数据验证机制。
合并单元格对解析算法来说尤其成问题,因为它们可以跨越多行或多列,使数据关联变得复杂。必须编写程序逻辑来准确处理这些合并区域。隐藏的行、列或工作表增加了另一层复杂性,需要彻底检查整个工作簿以确保完整的数据提取。
为应对这些挑战,必须开发稳健、灵活的解析解决方案。这通常涉及结合多种方法,例如使用专门的 Excel 解析库、为特定文件结构实现自定义逻辑,以及采用机器学习技术对半结构化数据进行模式识别。
预处理
在分块前处理合并单元格、复杂公式和非表格数据。这些属于预处理。
合并单元格问题
在对 Excel 进行分块之前,必须先把「合并单元格导致的信息缺失」消除,否则嵌入时会出现 NaN 或空字符串,严重影响检索召回。
为什么要先处理合并单元格?
我们以一个 "功能清单.xlsx" 为例:
- 读取时只保留左上角值:无论 openpyxl 还是 pandas.read_excel,合并区域内除左上角外其余格值会被置空 。
- 行级分块会丢字段:合并了"模块名称"或"子系统"的行,在转换成文本时会缺列,导致检索无法定位 。
- RAG 检索依赖元数据:若模块名丢失,metadata_filter 将失效,回答准确率显著下降(内部测试下降 15--25 pp)。
解决方案
思路:
- 利用 ws.merged_cells.ranges 拿到所有合并区域
- 读取左上角值,遍历填充到区域内每个单元格。
- 调用 ws.unmerge_cells() 取消合并,再保存临时文件供 pandas / Unstructured 后续处理。
python
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 策略,具体来说,是使用 两级分块
什么是"两级分块"?
两级分块本质固定:父 = 模块/功能域,子 = 行记录。

Azure 官方指南将这种把大块再拆子块的做法称作"层次化 chunking/hierarchical chunking",可与 Auto-Merging Retrieval 等检索算法天然配合
- 功能清单与工时评估表本质是一条条功能点记录;行级粒度最能保持"一问就能命中一行"的高精检索。
- 单纯行级又易丢失上下文,例如"所属子系统";用业务模块字段先聚合可在召回时带来更丰富的背景。
- 如果某模块非常大,使用递归切分(递归字符或 token 限长)可以在不破坏结构的情况下继续拆分。
具体来说是:模块 → 行 先聚后拆,更适合 Excel 表中已有明确模块列、需要用向量库分区或 metadata 过滤的系统
处理流程上务必:
- 先处理合并单元格
- 行文本带列名
- metadata 保留 module 字段,以便精准过滤与 Auto-Merging 聚合。 而元数据的处理(模块、行号、sheet 名),决定了查询过滤与答案上下文的可控性。
详细说明
两级分块中的 父和子,具体来说是:"摘要型父块 + 行级子块"
父块
- 父块的存在价值在于提供业务背景 + 索引锚点。比如 "模块 A:支付结算;记录 426 行" 。
- 父块采用"header + 小块"策略
为什么父块可只包含结构信息?
- 父块不包含子行信息,因为子块检索命中后,通过父 ID 回溯获得模块级上下文,提高回答完整度。
- 在层次化 (hierarchical) 分块体系里,"父块 (parent document)" 的核心职责是让检索器知道一批子块属于哪 个业务语境,而不是存放子块的全文内容。
- 通常父块只保存模块级背景(例如模块名、描述、记录数等),不再内嵌每一行子块的具体文本;这样既保上下文,又避免重复嵌入
代码:父块仅含结构信息的实现
python
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 都看得懂的最小语义单元。它既要携带行内全部有效信息,又不能冗余到超出模型窗口。

子块典型 Markdown/JSON 结构:
md
## 模块: 支付结算
**功能ID**: PAY-001
**功能名称**: 创建收款单
**功能类型**: 核心
**COSMIC FP**: 6
json
{
"module": "支付结算",
"功能ID": "PAY-001",
"功能名称": "创建收款单",
"功能类型": "核心",
"COSMIC_FP": 6
}
推荐生成流程(代码片段):
python
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:遍历行 → 同时产出父块与子块
python
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 not in 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:
- 先做向量检索拿到 k 个子块;
- 按 parent_id/module 查父块;
- 拼 "父摘要 + 命中子块(±近邻)" 送入 LLM。
检索流程

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

其他 chunking 策略
- 基于工作表和基于行的分块
- 基于列的拆分
- 混合与滑动窗口技术
用于 Excel 分块的工具和库
pandas
Python 的 pandas 库是许多 Excel 处理任务的核心,为读取 Excel 文件提供了强大的分块支持。 read_excel() 函数的 chunksize 参数允许进行内存高效、固定大小的分块
openpyxl
对于更复杂的 Excel 结构,openpyxl 库提供了对 Excel 文件解析的粒度控制,使其适用于基于内容的分块方法,能够有效处理合并单元格、公式和其他非标准元素。
xlrd
xlrd 库虽然主要针对较旧的.xls 格式,但对于遗留系统仍然具有相关性,并提供快速解析功能,在混合分块方法中,当速度至关重要时,这些功能非常有用。