本文介绍一种基于正则匹配与上下文记忆的稳健方法,用于从 section_name 中提取结构化章节 ID(如 1.1、1.3.1),并将其填充至 section_id 列;对无编号条目(如 Synopsis)自动关联最近匹配的编号,空值保持不变。
在处理结构化文档类数据——比如测试用例、标准规范,或者从PDF表格里解析出来的结果——时,经常会遇到一个头疼的问题:数据框里的 `section_name` 列,既包含了带编号的标题(比如 "1.2.Schema12"),也混着纯内容标题(比如 "Synopsis")。目标很明确:为每一行数据准确推断出它所属的逻辑章节ID,然后填到新列 `section_id` 里。这可不是简单的字符串切分,背后其实是上下文语义映射的活儿。
先说核心思路,其实就两步:
- 正则识别编号前缀:用 `re.match(r'(\d+(?:\.\d+)*)(.*)', s)` 这种模式,精确捕获由连续数字和点号组成的层级ID(支持 `1`、`1.1`、`1.3.1` 这种任意深度),剩下的部分就作为“语义标识符”存进一个映射字典里。
- 模糊反向匹配:对于那些没有编号的行(比如 "Synopsis"),就去遍历已经记录下来的语义标识符,检查当前名称是不是某个标识符的子串(条件就是 `if name.strip() in known_semantic_key`),从而确定它的归属。
下面是一段可以直接运行的完整代码,已经处理了空值、None和空白字符串这些边界情况:
import pandas as pd
import numpy as np
import re
# 构造示例数据(含空值与 None)
data = {
'section_name': [
'1.Test Summary9',
'1.1.Synopsis9',
'1.2.Schema12',
'1.3.1.Test Period I - Screening13',
'1.3.2.Period II - obes-Treatment 15',
'Synopsis',
'Test Period I - Screening',
None,
''
]
}
df = pd.DataFrame(data)
def extract_section_id(row, memo_dict):
name = row['section_name']
# 跳过 None 和纯空白
if pd.isna(name) or str(name).strip() == '':
return '' # 或返回 np.nan,按需选择
s = str(name).strip()
# 步骤1:尝试匹配编号前缀(如 1.3.1)
match = re.match(r'^(\d+(?:\.\d+)*)\.(.*)$', s)
if match:
sec_id, semantic_part = match.groups()
# 记录该语义片段对应的 ID(用于后续匹配)
memo_dict[semantic_part] = sec_id
return sec_id
else:
# 步骤2:对无编号行,在历史语义片段中查找包含关系
for semantic_key, sec_id in memo_dict.items():
if s in semantic_key or semantic_key in s:
return sec_id
return '' # 未匹配时返回空字符串
# 初始化记忆字典
section_map = {}
df['section_id'] = df.apply(lambda x: extract_section_id(x, section_map), axis=1)
print(df)
输出结果是这样的:
section_name section_id 0 1.Test Summary9 1 1 1.1.Synopsis9 1.1 2 1.2.Schema12 1.2 3 1.3.1.Test Period I - Screening13 1.3.1 4 1.3.2.Period II - obes-Treatment 15 1.3.2 5 Synopsis 1.1 6 Test Period I - Screening 1.3.1 7 None 8
这套方法有几处关键优势:
- 鲁棒性高:显式处理了 None、空字符串和前后空格,不会因为这些边缘情况崩掉。
- 语义感知:通过 `s in semantic_key` 这种条件,实现了“Synopsis 属于 1.1.Synopsis9”的合理映射,而不是简单粗暴的前缀匹配。
- 状态可维护:`memo_dict` 字典在 `apply` 过程中不断累积已识别的模式,天然支持多层级文档的流式处理。
- 正则精准:`^(\d+(?:\.\d+)*)\.` 这个模式,确保只匹配开头的合法编号加一个点号,避免了误把 `Schema12` 里的 `12` 当成编号截取出来。
不过,有几个细节需要注意:
- 如果存在歧义语义,比如 "Summary" 同时出现在 `1.Test Summary9` 和 `2.Summary Notes` 里,就需要增强匹配逻辑了。可以考虑改成 `semantic_key.startswith(s)`,或者引入编辑距离来辅助判断。
- 生产环境里,建议把函数封装成独立模块,并加上日志记录,把那些匹配不上的项记下来,方便人工复核。
- 对于特别大的 DataFrame,可以改用 `itertuples()` 来提升性能,但需要注意的是,`memo_dict` 字典仍然需要跨行共享,确保状态一致。
总的来说,这个方法在准确性、可读性和工程实用性之间找到了一个不错的平衡点,很适合用来处理各种嵌套编号文档的结构化解析任务。