犀牛鸟 2026 研究笔记 estelledc.github.io

案例 #1785:Excel IMAGE 函数过滤

PR 合入日期:2026-06-24 | 作者:estelledc (Jason) | 改动:2 文件 +111/-1

结论先行

这个案例教会一件事:RAG 管线中的噪声过滤必须在最早的解析层做,而不是期望下游检索或 Rerank 来兜底

适合谁读:关注 Python docreader 层的贡献者。改动集中在 docreader/parser/excel_parser.py,不涉及 Go 后端或前端 Vue 代码。

与 #1248 的关系:间接关联,方法论层面相通。#1248(知识库问答-点赞点踩)需要将用户反馈归因到具体的知识库片段——本质上也是一种”边界识别”问题:哪些片段对回答有贡献、哪些是噪声?#1785 解决的是更上游的边界识别:哪些 cell 内容是有效文本、哪些是图片函数噪声?两者的共同模式是——在管线的某个节点做归因/过滤决策,需要精确的正则或规则来划定边界。


1. 问题现象(What)

用户在 WeKnora 知识库中上传了包含嵌入图片的 Excel 文件(由 WPS 生成),RAG 问答时 AI 的回复中出现了 DISPIMG("ID_xxx",1) 这样的函数字符串,而不是图片内容或正常文本。

复现步骤:

  1. 用 WPS 创建一个 Excel 文件,在单元格中插入图片
  2. 将该文件上传到 WeKnora 知识库
  3. 对知识库发起问答,观察返回结果
  4. 发现回答中包含类似 DISPIMG("ID_E2A33949-...",1)=_xlfn.DISPIMG("ID_xxx",1) 的无意义字符串

问题的直观感受:用户期望 AI 基于文档的实际内容回答问题,但 AI 把图片占位符当成了”知识”来引用——就像你问图书管理员一个问题,他翻开书回答你的不是书的内容,而是书页空白处的印刷标记。

修复前 / 修复后

观测 修复前 修复后
cell 解析 DISPIMG("ID_xxx",1) 原样写入文本 正则过滤,噪声不入 chunk
AI 回答 引用无意义函数串 基于有效文本或跳过空 cell
测试 无 IMAGE 变体覆盖 4 种 WPS/Excel 变体单测

定位步骤

rg "DISPIMG|IMAGE|_IMAGE_FUNC" docreader/parser/excel_parser.py
rg "ExcelImageFilter|test_excel" docreader/tests/
python -m pytest docreader/tests/test_excel_parser.py -v -k -i image
  1. Issue #1779 截图中的字符串 → grep DISPIMG
  2. excel_parser.pyparse_into_text() 单元格循环
  3. 用 openpyxl 小脚本验证 data_only=True 对 WPS cell 的行为

2. 根因分析(Why)

根因可以用一句话概括:WPS 把嵌入图片存储为函数字符串,而 openpyxl 的 data_only=True 对这类伪函数不生效,导致噪声泄入 RAG chunk

要理解这个问题,需要追踪数据从 Excel 文件到 RAG 回答的完整流向。

flowchart TD
    A["用户上传 Excel 文件<br/>(含 WPS 嵌入图片)"] --> B["docreader 服务接收文件"]
    B --> C["ExcelParser.parse_into_text()"]
    C --> D["openpyxl 打开工作簿<br/>data_only=True"]
    D --> E{"遍历每个 cell"}
    E -->|"普通文本 cell"| F["cell.value = '正常内容'<br/>✅ 写入解析结果"]
    E -->|"真实公式 cell"| G["cell.value = None<br/>(data_only 生效)<br/>✅ 被跳过"]
    E -->|"WPS 图片 cell"| H["cell.value = 'DISPIMG(\"ID_xxx\",1)'<br/>(data_only 不生效)<br/>❌ 被当成正常文本写入"]
    F --> I["解析结果传入 chunker"]
    H --> I
    I --> J["向量化写入知识库"]
    J --> K["用户提问时检索召回<br/>噪声字符串出现在回答中"]

    style H fill:#f96,stroke:#333
    style K fill:#f96,stroke:#333

关键文件表:

文件 角色 关键函数/组件
docreader/parser/excel_parser.py Excel 文件解析器 ExcelParser.parse_into_text() — 遍历单元格提取文本
docreader/tests/test_excel_parser.py 单元测试 ExcelImageFilterTest — 覆盖所有已知 IMAGE 函数变体

问题的技术细节在于 Excel 文件格式和 WPS 行为的差异。正常的 Excel 公式(如 =SUM(A1:A10))存储在 cell 的公式属性中,openpyxl 用 data_only=True 打开时会读取缓存的计算结果而非公式本身,对于没有缓存结果的公式返回 None。但 WPS 嵌入图片时的做法不同——它把 DISPIMG("ID_xxx",1) 作为纯文本值(不是公式)写入 cell 的 value 属性。从 openpyxl 的视角看,这就是一个普通的文本字符串,data_only=True 对它完全没有影响。


3. 解决方案(How)

改动摘要:在 ExcelParser.parse_into_text() 的单元格遍历逻辑中,增加一个正则过滤步骤——检测到 IMAGE 函数模式的字符串直接跳过,不写入解析结果。

核心代码片段(docreader/parser/excel_parser.py):

import re

# 匹配 WPS/Excel 嵌入图片的函数字符串
# 覆盖变体:DISPIMG(...)、=DISPIMG(...)、_xlfn.DISPIMG(...)、=_xlfn.IMAGE(...)
_IMAGE_FUNC_RE = re.compile(r'^=?(_xlfn\.)?(DISPIMG|IMAGE)\(', re.IGNORECASE)

# 在 parse_into_text() 的 cell 遍历循环中:
for cell in row:
    value = cell.value
    if value is None:
        continue
    text = str(value).strip()
    if not text:
        continue
    # 新增:过滤图片函数字符串
    if _IMAGE_FUNC_RE.match(text):
        continue
    # ... 正常处理 text ...

正则 ^=?(_xlfn\.)?(DISPIMG|IMAGE)\( 的设计逻辑:

为什么这样改而不是别的方式?至少有两个备选方案被排除:

第一个备选是在 chunker 层过滤。但 chunker 处理的是已经拼接好的文本块,此时 DISPIMG(...) 已经混在正常文本中间,正则匹配的边界更模糊,误过滤风险更高。在最上游(parser 层)过滤是”垃圾进垃圾出”原则的直接应用——不让噪声进入管线,比在管线中间捞噪声更可靠

第二个备选是改用 openpyxl 的其他模式读取。但 data_only=True 已经是读取纯值的标准方式,WPS 的行为属于”合规但非预期”的边界情况,不是 openpyxl 能解决的问题。

测试覆盖:新增 ExcelImageFilterTest 类,包含 4 个测试用例:

测试用例 输入 预期行为
test_dispimg DISPIMG("ID_xxx",1) 被过滤
test_equals_dispimg =DISPIMG("ID_xxx",1) 被过滤
test_xlfn_dispimg _xlfn.DISPIMG("ID_xxx",1) 被过滤
test_xlfn_image =_xlfn.IMAGE("https://...") 被过滤

测试用例覆盖了目前已知的所有 WPS 和 Excel 图片函数变体。


4. Review 与合入(Maintainer 视角)

这个 PR 没有收到 review 评论,直接合入。

为什么会直接合入?从 PR 本身的特征可以推断几个原因:

第一,改动范围高度收敛。只改了 1 个解析文件 + 1 个测试文件,不涉及任何跨模块交互,review 者能在 5 分钟内完全理解改动的影响面。

第二,测试覆盖充分。4 个测试用例覆盖了所有已知变体,且测试逻辑直观(输入一个字符串,断言它被过滤),reviewer 不需要猜测”这个修复是不是真的有效”。

第三,PR 描述自带根因分析。PR description 明确说明了”WPS 以纯文本值存储图片函数 → openpyxl data_only=True 不生效 → 噪声泄入”的因果链,reviewer 不需要自己去挖掘 why。

从这个案例可以提炼出几条 WeKnora 贡献规范:


5. 可复用模式(Pattern)


6. 常见误区

错误认知:openpyxl 的 data_only=True 会过滤掉所有非数据内容。 正确理解:data_only=True 只影响真正的公式 cell——它读取公式的缓存计算结果而非公式文本。但 WPS 的图片函数字符串是以纯文本值存储的,不是公式,data_only 对它无效。类比:data_only 像一个只认”公式格式”的安检员,WPS 把违禁品伪装成了普通行李,安检员放行了。

错误认知:噪声过滤应该放在 chunker 或 Rerank 层,让上游保持”忠实原文”。 正确理解:RAG 管线的设计原则是”向上游要质量”(GIGO)。DISPIMG("ID_xxx",1) 不是文档的”原始内容”,而是特定软件的实现细节——用户写在 Excel 里的是一张图片,不是这个函数字符串。在 parser 层过滤掉它,恰恰是在还原用户的真实意图。

错误认知:正则匹配用 search 和用 match 效果一样。 正确理解:re.match 只匹配字符串开头,re.search 在整个字符串中搜索。这里用 match(加上 ^ 锚定)是有意为之——一个正常的 cell 内容可能在中间包含 “DISPIMG” 这个子串(虽然概率极低),用 match 可以避免误过滤。

错误认知:WPS 与 Microsoft Excel 的 cell 存储行为完全一致。 正确理解:WPS 将图片嵌入存为 纯文本函数字符串,不走 openpyxl 的公式解析路径;修复必须 针对 WPS 变体 写正则与单测,不能假设 data_only=True 或标准 Excel 行为。


7. 思考点

  1. 如果 WPS 未来新增了另一种图片嵌入方式(比如用 XLIMAGE(...) 函数),现有正则会怎样?你会如何设计一个更具前瞻性的过滤策略?

  2. 画出一个 Excel 文件从上传到最终出现在 AI 回答中的完整数据流图(提示:至少经过 docreader → chunker → 向量化 → 检索 → LLM 生成 5 个阶段)。在每个阶段标注:这个噪声在该阶段有没有机会被拦截?代价是什么?

  3. 假设你不改 parser 层的代码,而是在 Rerank 阶段加一个”噪声检测 Rerank 信号”——对包含 DISPIMG 模式的 chunk 降权。这种方案的优缺点是什么?在什么场景下它可能比 parser 层过滤更合适?

  4. 这个 PR 的正则用了 re.IGNORECASE 标志。思考:Excel 函数名是否大小写敏感?如果去掉这个标志,会不会有遗漏的变体?

  5. 如果 WeKnora 的 docreader 将来迁移到用 LLM 做 cell 内容理解(而非规则过滤),这个正则过滤还有存在的必要吗?LLM 方案的 trade-off 是什么?


8. 延伸阅读


作者视角

这是我提交给 WeKnora 的第一个被合入的 PR。回过头看,从 issue 到 PR 的过程可以拆成几个阶段。

发现问题:在浏览 WeKnora 的 open issues 时看到 #1779,issue 描述很清晰——截图直接展示了 DISPIMG("ID_xxx",1) 出现在解析结果中。这类”有截图、能复现、根因可推断”的 issue 是新贡献者的理想起点。

定位根因:从现象倒推数据流——AI 回答中出现了这个字符串,说明它一定经过了 解析 → 分块 → 向量化 → 检索 → 生成 的完整管线。那么它最早是在哪一步进入管线的?答案显然在 parser 层。打开 excel_parser.py,找到 parse_into_text() 方法,确认它对 cell.value 没有做任何过滤——只要不是 None 就写入结果。接下来搜索 “DISPIMG” 的含义,确认这是 WPS 和 Excel 的图片嵌入函数。再用 openpyxl 写一个小脚本验证 data_only=True 对这类值的行为,确认它不会被过滤。根因链就清楚了。

设计方案:考虑过两个方向——在 parser 层过滤(改 excel_parser.py)还是在 chunker 层过滤。选择 parser 层的理由很直接:这些字符串根本不是文档内容,越早拦截越好。正则的设计花了一些时间——需要覆盖 DISPIMG=DISPIMG_xlfn.DISPIMG=_xlfn.IMAGE 四种已知变体,同时不能误伤正常文本。最终用一个带可选组的正则搞定。

写测试:测试是这个 PR 中行数最多的部分(93 行 vs 代码改动 18 行)。每个已知变体一个测试用例,确保正则不遗漏。这种”测试比代码多”的比例在过滤类修复中是正常的——你对边界的承诺就体现在测试用例里。

与 #1248 的关联思考:#1785 和 #1248 表面上看领域不同(一个是 Excel 解析,一个是问答反馈),但它们同属”边界/归因”这个问题类型。#1785 要回答”这个 cell 的内容是有效知识还是噪声?”,#1248 要回答”这个知识库片段对用户的回答有帮助还是没帮助?”两者都需要在管线的某个节点画一条清晰的线——线内的保留,线外的丢弃或降权。这种”在数据流中做精确归因”的思维模式,是 RAG 系统贡献中反复出现的核心能力。

对新贡献者的建议: