本节摘要:本节装配管线第②段的两半。parse 半段(semantica/parse/,22 个文件)用五类解析器把异构格式变成统一文本:文档类(PDF/DOCX/PPTX)、结构化类(CSV/JSON/Excel/XML)、代码类(ast/tree-sitter)、网页类(HTML/网页抓取)、邮件类;还有 docling_parser 这条可选的高级文档理解路线。normalize 半段(semantica/normalize/,13 个文件)做四类归一:文本(Unicode/空白/编码)、实体(别名/变体)、日期(ISO8601)、数字(单位/货币/精度),外加数据清洗与语言检测。本节最后回答一个管线设计问题:为什么归一必须放在抽取之前——垃圾进,垃圾出。
内容来源:原项目源码 semantica/parse/(document_parser.py、code_parser.py 等)、semantica/normalize/(text_normalizer.py、entity_normalizer.py、date_normalizer.py、number_normalizer.py)
⚠️ 注意:
DocumentParser.parse_document内置的格式路由只覆盖 pdf/docx/html/text 四类;CSV、JSON、代码、邮件等要走各自的解析器类(StructuredDataParser、CodeParser、EmailParser)或parse/的统一入口。另外 docling 路线依赖docling包,未安装时 import 的是占位类、构造时才报错——判断可用性要看DOCLING_AVAILABLE标志而不是类是否存在。
DocumentParser.parse_document 的探测→路由→容错三步。EntityNormalizer 的别名消解(resolve_aliases)机制。semantica/parse/ 的 22 个文件按格式家族分组,正好五类加一个编排器:
文档类: pdf_parser.py(291行) docx_parser.py(318) pptx_parser.py(247) image_parser.py(312) media_parser.py(296) docling_parser.py(729) 结构化类: csv_parser.py(206) json_parser.py(292) excel_parser.py(297) xml_parser.py(322) structured_data_parser.py(383) 代码类: code_parser.py(602) 网页类: html_parser.py(436) web_parser.py(445) 邮件类: email_parser.py(444) 编排器: document_parser.py(381) methods.py(800) registry.py(130)
主线是 DocumentParser.parse_document(document_parser.py:140),三步走:探测(file_type 缺省时 _detect_file_type 自动识别)→ 路由(按格式分发到子解析器)→ 容错(失败包成 ProcessingError 上抛,进度条标记 failed):
# document_parser.py:183 if file_type == "pdf": result = self.pdf_parser.parse(file_path, **options) elif file_type == "docx": result = self.docx_parser.parse(file_path, **options) elif file_type == "html": result = self.html_parser.parse(file_path, **options) elif file_type == "text": result = self._parse_text(file_path, **options) else: raise ValidationError(f"Unsupported document format: {file_type}")
每个子解析器都同时提供 parse(带结构:标题/段落/表格/元数据)与 extract_text(纯文本)两个粒度。以 pdf_parser 为例,parse 返回的字典里带页码、页数、元数据,甚至可以逐页抽取;extract_text 则把全部文本拼成一个字符串。选择哪个粒度取决于下游:走 split 分块的通常要带结构(块边界对齐段落),走全文嵌入的用纯文本即可。
结构化解析器(structured_data_parser.py)把表格数据变成「记录+字段」形态,为后续「表变图节点」做准备:
# 结构化数据的统一输出形态(节选自 structured_data_parser 的约定) { "records": [{"name": "Acme", "revenue": "2.4M"}, ...], # 行记录 "schema": ["name", "revenue"], # 字段列表 "metadata": {"row_count": 128, "source_format": "csv"}, }
docling_parser(729 行)是 IBM docling 路线的适配层,扫描版 PDF 的版面理解可以走它——parse/__init__.py:185 附近有一段优雅的降级:没装 docling 就创建占位类,构造时才报带安装指引的错误,模块 import 永不失败:
# parse/__init__.py:185 (简化) if DOCLING_AVAILABLE: from .docling_parser import DoclingParser, DoclingMetadata else: class DoclingParser: """Placeholder for DoclingParser when docling is not available.""" def __init__(self, **config): error_msg = ("DoclingParser requires the 'docling' package " "to be installed and working.") if import_error_msg: error_msg += f"\n\nError: {import_error_msg}" error_msg += "\n\nInstall it with: pip install docling" raise ImportError(error_msg)
代码解析器(code_parser.py)最特别:Python 走标准库 ast.parse(code_parser.py:272),抽出函数定义、类定义、import 语句与装饰器(ast.walk 遍历,code_parser.py:298 起):
tree = ast.parse(code_content) for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): ... # 函数名、参数、装饰器(ast.unparse 还原装饰器表达式) elif isinstance(node, ast.ClassDef): ... # 类名、基类([ast.unparse(b) for b in node.bases]) elif isinstance(node, (ast.Import, ast.ImportFrom)): ... # import 依赖 → 后续可建「模块依赖边」
其他语言走 tree-sitter 的语法树。为什么要这么重?因为代码里的「实体」是函数/类/模块、「关系」是 import 调用继承——按语法树抽取才能把这些结构原样带出来,按纯文本切块则全丢了。
semantica/normalize/ 共 13 个文件 6044 行,四类归一化各由一个门面类协调若干底层件:
文本归一(text_normalizer.py,606 行):协调 UnicodeNormalizer、WhitespaceNormalizer、TextCleaner。核心是 Unicode NFC 规范化(text_normalizer.py:123,unicodedata 实现)——「é」的单码位写法与「e+组合重音符」双码位写法在字节层面不同但视觉相同,不归一的话同一个词会变成两个词:
# text_normalizer.py:120 def normalize_text(self, text, unicode_form: str = "NFC", ...): normalized = self.unicode_normalizer.normalize_unicode( normalized, form=unicode_form) # NFC 规范化 normalized = self.whitespace_normalizer.normalize_whitespace( normalized, ...) # 空白/换行统一
主入口 normalize(text_normalizer.py:94)把三个底层件串成流水线:先清洗(特殊字符/控制符)、再 Unicode 规范化、再空白归一。配套的 encoding_handler.py(369 行)专治乱码——编码探测与修复;text_cleaner.py(362 行)做去噪;language_detector.py(317 行)顺带把语言标出来,供下游按语言分流。
实体归一(entity_normalizer.py,567 行):协调 AliasResolver(别名解析)、消歧器与变体处理器。API 很直观:
canonical = normalizer.resolve_aliases("J. Doe") # 别名 → 规范名 info = normalizer.disambiguate_entity("苹果", ...) # 同名不同指消歧
resolve_aliases(entity_normalizer.py:144)查别名表把「J. Doe」映到「John Doe」这类规范形式;disambiguate_entity(entity_normalizer.py:169)处理一词多义。它是第 3 章 EntityResolver 的「预处理版」:前者在抽取前把文本里的写法统一,后者在建图前把抽取结果合并。
日期归一(date_normalizer.py,678 行):normalize_date(date_input, format="ISO8601", timezone="UTC")(date_normalizer.py:102)把「March 5th, 2024」「2024/03/05」「昨天」统统变成 ISO8601。模块头注释概括了它要吃下的多样性:多格式解析、时区转换、相对日期表达式。这是第 7 章双时态图谱的地基——valid time 与 recorded time 的查询都建立在规范时间戳上,日期不规范时态查询必错。
数字归一(number_normalizer.py,793 行):四个入口各管一段——normalize_number(number_normalizer.py:91)管数值本身,normalize_quantity(164 行)管「数量+单位」复合表达式,normalize_unit(451 行)做单位规范(千米/km/kilometers 归一),normalize_currency(574 行)做货币规范(美元/USD/$)。归一后「$2.4 million」「2,400,000 美元」「2.4M USD」才有可比性。data_cleaner.py(942 行,全模块最大文件)做缺失值、重复行、离群值这类表格级清理;methods.py(808 行)提供 get_normalize_method 统一调度,registry.py 支持注册自定义归一方法。
管线把 normalize 放在 parse 之后、split/extract 之前,不是随意排序。两个硬理由:
**理由一:抽取器对「同一写法」敏感。**NER 的 pattern 路线靠正则匹配,ML 路线靠训练分布——「Apple Inc.」与「Apple Inc.」(全角)在抽取器眼里是两个东西,前者的共指消解、后者的实体合并都会因字符层面不一致而失效。第 4 章的去重会告诉你:字符层面的噪声最后都会变成「假重复实体」,让 blocking 阶段白做功。
**理由二:冲突检测对「同一值」敏感。**第 4 章的 ConflictDetector.detect_value_conflicts 用 set(str(v)) 判断同一实体的属性值是否冲突(conflict_detector.py:271)——如果来源 A 记「2024-03-05」、来源 B 记「March 5th, 2024」,没归一就会被误报为时间冲突,监管场景里每个误报都要人去调查(conflicts 模块甚至配了 investigation_guide.py 生成调查指引)。先归一、再检测,误报率天差地别。
一个实战节奏:NER 抽出 1000 个实体、EntityResolver 消解到 800 个;如果 normalize 到位,可能直接抽出 850 个规范实体、消解到 820 个——前段多花一分力,后段少做十分工。
动手把本节接进微管线只需要三行核心调用:
from semantica.parse import DocumentParser from semantica.normalize import TextNormalizer doc = DocumentParser().parse_document("report.pdf") # 解析:结构化文本 clean = TextNormalizer().normalize(doc["text"]) # 归一:NFC+空白统一 # clean 即可交给下一节的 TextSplitter 分块
💡 装配要点:本节装上管线的「洗料车间」。parse 记三件事:五类解析器、
parse_document的探测→路由→容错、代码解析走语法树不走纯文本。normalize 记四类归一(文本 NFC/实体别名/日期 ISO8601/数字单位货币)加一句口诀——归一在抽取前,是为了让「同一写法」「同一值」在后段的共指、合并、冲突检测里被认出来;字符层的噪声会逐段放大成图谱层的错误。
parse_document 内置路由只管 pdf/docx/html/text,其余走专项解析器。ast.parse 抽函数/类/import,其他语言 tree-sitter——代码的实体关系在语法树里。下一节:
03 split:13 种 GraphRAG 原生分块——固定/递归/语义之外,entity_aware/relation_aware/graph_based 这些「图原生」分块为什么是 GraphRAG 检索质量的关键。