3、基于规则的抽取方法:正则表达式编写技巧、词典匹配(AC自动机)、规则模板设计、优缺点分析

说到实体关系抽取,很多人一上来就想着上深度学习模型。但我得说一句大实话——规则方法,永远是性价比最高的起点。我在工业项目里见过太多场景,几条精心设计的正则,就能干掉80%的抽取需求。今天咱们就聊聊这个“老派但好用”的方法。

3.1 正则表达式编写技巧

正则表达式,说白了就是“文本模式的匹配游戏”。我刚开始写正则时,也踩过不少坑。比如匹配一个电话号码,写出来的表达式要么漏掉号码,要么把身份证号也抓进来了。

核心技巧一:从具体到抽象

别一上来就写复杂的模式。我的习惯是:先写死样本,再逐步抽象。举个例子:

# 先匹配具体文本
"张三毕业于清华大学" → 匹配"毕业于"

# 再抽象成模式
(.*?)毕业于(.*?) → 匹配"人物"和"学校"

# 最后加上边界约束
(?<=^|。|;)(.*?)毕业于(.*?)(?=。|$|;)

嗯,这里要注意:边界条件比匹配本身更重要。我曾经因为没加边界,把“张三毕业于清华大学,李四毕业于北京大学”抽成了“张三毕业于清华大学,李四”——这显然不对。

核心技巧二:善用非贪婪匹配

你想想看,默认的贪婪匹配会吞掉尽可能多的字符。比如:

文本:"张三毕业于清华大学,李四毕业于北京大学"
模式:.*毕业于.*
结果:匹配了整个句子(贪婪)
模式:.*?毕业于.*?
结果:匹配了"张三毕业于清华大学"(非贪婪)

我个人习惯在抽取任务中,默认使用非贪婪匹配,除非有特殊需求。

核心技巧三:分组与命名捕获

正则的分组功能,是抽取的利器。我建议用命名捕获组,代码可读性会好很多:

import re

text = "张三毕业于清华大学"
pattern = r"(?P<person>.*?)毕业于(?P<school>.*?)$"

match = re.search(pattern, text)
if match:
    print(match.group("person"))  # 张三
    print(match.group("school"))  # 清华大学
我的小技巧:在写正则时,先用在线工具(如regex101)调试。我每次写复杂正则,都会先在那里验证,避免上线后出bug。

3.2 词典匹配:AC自动机

正则虽然强大,但遇到大规模词典匹配就力不从心了。比如你要从文本中找出10000个公司名,用正则写10000个模式?那性能会惨不忍睹。

这时候,AC自动机(Aho-Corasick)就派上用场了。它本质上是一个多模式匹配算法,能在O(n)时间内完成所有词典词的匹配。

我记得在做一个金融舆情项目时,需要从新闻中抽取2000多个金融实体。用正则跑了半小时没跑完,换成AC自动机后,3秒搞定。差距就是这么大。

AC自动机的核心逻辑:

  • 构建Trie树:把所有词典词插入到一棵树中
  • 构建失败指针:当匹配失败时,快速跳转到下一个可能匹配的位置
  • 扫描文本:沿着Trie树走,遇到失败指针就跳转

Python里用pyahocorasick库,几行代码就能搞定:

import ahocorasick

# 构建AC自动机
automaton = ahocorasick.Automaton()

# 添加词典词
words = ["清华大学", "北京大学", "浙江大学"]
for idx, word in enumerate(words):
    automaton.add_word(word, (idx, word))

# 构建失败指针
automaton.make_automaton()

# 匹配文本
text = "张三毕业于清华大学,李四毕业于北京大学"
for end_index, (idx, word) in automaton.iter(text):
    start_index = end_index - len(word) + 1
    print(f"找到实体: {word}, 位置: ({start_index}, {end_index})")
避坑指南:我曾经在构建AC自动机时,忘了处理词典中的重复词。结果同一个实体被匹配了两次,导致后续的抽取逻辑出错。所以,词典去重是第一步

3.3 规则模板设计

有了正则和词典,接下来就是设计规则模板了。我个人习惯把规则模板分为三层:

层级 名称 示例
第一层 原子规则 匹配人名、地名、机构名等基础实体
第二层 组合规则 匹配"人物 + 毕业于 + 学校"这样的关系
第三层 上下文规则 考虑前后文约束,如否定词、修饰词

设计原则一:规则要可配置

别把规则写死在代码里。我习惯用JSON或YAML来管理规则模板:

{
  "rules": [
    {
      "name": "毕业关系",
      "pattern": "{person}毕业于{school}",
      "entities": {
        "person": "人名词典",
        "school": "学校词典"
      },
      "relation": "毕业"
    },
    {
      "name": "任职关系",
      "pattern": "{person}担任{company}的{position}",
      "entities": {
        "person": "人名词典",
        "company": "公司词典",
        "position": "职位词典"
      },
      "relation": "任职"
    }
  ]
}

设计原则二:规则要有优先级

你想想看,如果多条规则都能匹配同一段文本,该用哪条?我的做法是:精确度高的规则优先。比如:

  1. 精确匹配规则(如"毕业于")优先级最高
  2. 模糊匹配规则(如"就读于"、"在...学习")次之
  3. 兜底规则(如"人物+学校"共现)优先级最低
注意:规则优先级设置不当,会导致抽取结果混乱。我曾经在一个项目中,把模糊规则放在了精确规则前面,结果"张三毕业于清华大学"被匹配成了"张三在清华大学学习"——虽然意思对,但关系类型错了。

3.4 优缺点分析

聊了这么多,咱们客观地分析一下规则方法的优缺点。

优点:

  • 可解释性强:每条规则都能说清楚为什么匹配。这在金融、医疗等强监管领域特别重要。
  • 零样本可用:不需要标注数据,直接写规则就能跑。我经常在项目初期用规则快速验证可行性。
  • 性能极高:AC自动机+正则,处理百万级文本也就几秒钟。
  • 易于调试:哪里抽错了,改规则就行,不用重新训练模型。

缺点:

  • 维护成本高:规则多了以后,互相之间可能冲突。我见过一个项目维护了3000多条规则,改一条可能影响一片。
  • 泛化能力弱:换个领域,规则基本要重写。比如从新闻领域换到医疗领域,实体类型和关系模式完全不同。
  • 无法处理歧义:"苹果"是水果还是公司?规则很难判断。
  • 长尾问题:总有规则覆盖不到的文本模式。我曾经为了覆盖一个特殊句式,写了5条规则,结果只多覆盖了0.1%的数据。
我的建议:规则方法适合作为快速原型兜底策略。在工业级系统中,我通常的做法是:先用规则做粗抽,再用模型做细抽,最后用规则做后处理纠错。这样既保证了效率,又兼顾了泛化能力。

好了,关于规则抽取的方法就聊到这儿。说白了,规则方法就像一把瑞士军刀——小巧、实用、关键时刻能救命。但别指望它解决所有问题。该上模型的时候,也别犹豫。


公众号:蓝海资料掘金营,微信deep3321