如何从 DataFrame 列中提取结构化编号并智能映射到新列
本文介绍使用正则表达式与上下文记忆机制,从非规范化的 section_name 中精准提取 section_id,并对无编号条目(如 “synopsis”)实现基于历史匹配的智能回填。

处理文档结构化数据时,你是否也常遇到这样的麻烦?测试用例、章节大纲或是SOP条款的 section_name 列里,文本格式五花八门:既有带着清晰编号的“1.3.1.Test Period I - Screening13”,也夹杂着只有纯标题的“Synopsis”,中间还可能点缀着空值和空白行。我们的目标很明确:生成一列语义一致的 section_id。对于带编号的条目,提取出前缀如“1.3.1”;对于纯标题,则要聪明地找到它所属的最近有效编号。这可不是简单的字符串切割,它要求算法同时具备精准的模式识别能力和理解上下文关联的能力。
核心思路:正则提取 + 历史映射表
一个直观但容易出错的思路是逐行传递状态。这里推荐更稳健的方法:构建一个动态字典,我们称之为 prev_ids。它的妙处在于,每当处理一个“编号+标题”组合时,我们就把剥离编号后的纯标题部分作为键(key),把对应的编号作为值(value)存起来。这样一来,后续遇到任何纯标题,只需要在这个“历史记忆库”里快速查找匹配项,就能回溯到它的归属编号,逻辑清晰且不易出错。
✅ 正确实现(推荐)
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, id_map):
name = row['section_name']
# 跳过 None、NaN、空字符串或纯空白
if pd.isna(name) or str(name).strip() == '':
return '' # 或返回 np.nan,按需选择
name_str = str(name).strip()
# 正则匹配:捕获连续的数字+点号组合(支持 1、1.1、1.3.1 等)
match = re.match(r'^(\d+(?:\.\d+)*)\.(.*)$', name_str)
if match:
sec_id, title_part = match.groups()
# 将标题部分(去空格)作为 key,映射到当前编号
id_map[title_part.strip()] = sec_id
return sec_id
else:
# 非编号格式:尝试在历史标题中查找是否为子串(如 "Synopsis" in "1.1.Synopsis9" → True)
for full_title, sec_id in id_map.items():
if name_str in full_title or full_title in name_str:
return sec_id
return '' # 未匹配到,返回空(或 np.nan)
# 初始化映射字典
id_mapping = {}
df['section_id'] = df.apply(lambda r: extract_section_id(r, id_mapping), axis=1)
关键细节说明:
- 正则 r'^(\d+(?:\.\d+)*)\.(.*)$':
开头的 ^ 和结尾的 $ 确保了匹配从行首开始、到行尾结束,避免误匹配中间部分。(\d+(?:\.\d+)*) 这个分组负责捕获多级编号,无论是“1”、“1.1”还是“1.3.1”都能搞定。紧接着的 \. 匹配那个作为分隔符的字面量点号,最后的 (.*) 则把剩下的所有标题内容都捕获进来。 - 智能回填逻辑:
这里没有使用严格的相等判断,而是采用了子串匹配(name_str in full_title)。这非常实用,它使得“Synopsis”能成功匹配到“1.1.Synopsis9”,“Test Period I - Screening”也能对应上“1.3.1.Test Period I - Screening13”,容错性更强。 - 空值处理统一:
对于 None、np.nan 以及空字符串,函数统一返回空字符串。当然,根据后续分析需求,你也可以选择统一返回 np.nan 以保持数据类型的整洁。
⚠️ 注意事项:
- 歧义匹配:如果存在模糊情况,比如“A”这个标题既出现在“1.A”里,也出现在“2.AB”中,当前的逻辑会返回最先注册的那个编号(sec_id)。如果需要更精确的全词匹配,可以考虑改用 re.search(rf'\b{name_str}\b', full_title) 来增强判断。
- 数据顺序假设:这个方法默认数据的顺序是有业务意义的,即带编号的条目会先于其对应的纯标题出现。如果原始数据顺序混乱,务必先按业务逻辑(比如原始索引或某个辅助序号)进行排序预处理。
- 性能提示:当面对超大规模数据集(行数超过10万)时,为了提升效率,可以预编译正则表达式对象,并优先使用 pandas 的向量化操作(如 df['section_name'].str.extract())来批量处理所有带编号的行。完成后再用 map() 方法来回填纯标题行,这样速度会快得多。
运行上述代码后,得到的 DataFrame 输出如下,可以看到智能映射的效果:
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
总的来说,这个方案鲁棒性强,逻辑清晰,能够很好地应对真实业务场景中常见的半结构化文本解析挑战。
