LangExtract实战第二弹

312 阅读7分钟

上期分享了基于Docker、无GPU、免费大模型接口使用LangExtract的例子。

本期将结合实际的业务数据进行演示,以便于更加容易理解,以及应用在业务场景。

业务需求

真实的业务需求是这样的:业务方需要给企业的涉害劳动者(工人)创建档案,劳动者的一些基本信息需要从《职业健康检查表》获取(这个“检查表”是其他检测机构提供的,不是业务方自己的),形成结构化数据并存储下来,然后建立劳动者的档案。

原始文件大约十页,其中有两页如下图所示,红框里的信息是要提取的(已经做过脱敏处理): image.png

image.png

总结一下要提取的信息如下:

  • 体检类别
  • 接触毒物及种类名称
  • 姓名
  • 身份证
  • 单位
  • 电话
  • 部门
  • 体检日期
  • 性别
  • 出生年月
  • 国籍
  • 民族
  • 婚否
  • 总工龄
  • 接害工龄
  • 接触有害因素

处理流程

实际业务中拿到的文件,可能是纯文本的PDF,也可能是PDF扫描件,还有可能是照片。

因为LangExtract不会对文件进行处理,他只负责从非结构化的文本中提取你想要的信息。

因此将文件给到LangExtract之前还要进行一步文件转化的操作,即不同类型的文件转为Markdown(相当于文档类型归一化),然后给到LangExtract进行处理。这里文档转换的工具使用了MinerU,如下图所示:

graph LR
图片 --> MinerU
PDF扫描件 --> MinerU
PDF文本 --> MinerU
Word --> MinerU
MinerU --> Markdown

有了Markdown文档之后,就可以从文档里提取关键信息了。

由于数据涉及敏感信息,原始的PDF我就不上传了。我把通过MinerU转换成的Markdown上传到这里(为了避免隐私数据泄露,已做混淆处理),大家可以下载下来,然后运行一下下面代码。

from __future__ import annotations
import textwrap
import langextract as lx
import logging

# # 配置日志:设置为 DEBUG 级别
# logging.basicConfig(level=logging.DEBUG)

# # 获取 LangExtract 的 logger
# logger = logging.getLogger("langextract")
# logger.setLevel(logging.DEBUG)

# 1. 读取本地 Markdown 文件
with open("readme.md", "r", encoding="utf-8") as f:
    sales_text = f.read()

# 这是提示词
prompt = textwrap.dedent("""\
从这份检测报告中提取以下字段:
- 体检类别
- 接触毒物及种类名称
- 姓名
- 身份证
- 单位
- 电话
- 部门
- 体检日期
- 性别
- 出生年月
- 国籍
- 民族
- 婚否
- 总工龄
- 接害工龄
- 接触有害因素
重要提示:extraction_text 必须取自原文,按照顺序提取字段,如果全部字段都提取到值了,就停止继续提取,避免被覆盖掉。另外,要注意的是,文本有可能包含HTML标签或者unicode字符,移除这些标签或者unicode字符,并且提取的内容里也不应该有HTML标签或者unicode字符。
""")

# 写一个例子告诉LangExtract
examples = [
    lx.data.ExampleData(
        text="""
        体检类别: 上岗前  接触毒物种类及名称:  噪声, 粉尘
        # 江苏省职业健康检查表
        姓名: 张三  身份证: 320111198205266313  单位: 无锡龙华锅炉压力容器有限公司  电话: 15966666626  部门: 电焊生产车间  体检日期: 2025- 03- 14
        # 有害作业人员健康检查表

        单位名称: 无锡龙华锅炉压力容器有限公司

        姓名:张三 出生年月:1972年11月26日国籍:中华人民共和国 民族:满族 婚否:已婚 总工龄:22 年3 月 接害工龄:26 年1 月 体检日期:2025年03月14日

        接触有害因素:噪声,粉尘
        """,
        extractions=[
            lx.data.Extraction(extraction_class="体检类别", extraction_text="上岗前", attributes={"体检类别": "上岗前"}),
            lx.data.Extraction(extraction_class="接触毒物种类及名称", extraction_text="噪声, 粉尘", attributes={"接触毒物种类及名称": "噪声, 粉尘"}),
            lx.data.Extraction(extraction_class="姓名", extraction_text="张三", attributes={"姓名": "张三"}),
            lx.data.Extraction(extraction_class="身份证", extraction_text="320111198205266313", attributes={"身份证": "320111198205266313"}),
            lx.data.Extraction(extraction_class="单位", extraction_text="无锡龙华锅炉压力容器有限公司", attributes={"单位": "无锡龙华锅炉压力容器有限公司"}),
            lx.data.Extraction(extraction_class="电话", extraction_text="15966666626", attributes={"电话": "15966666626"}),
            lx.data.Extraction(extraction_class="部门", extraction_text="电焊生产车间", attributes={"部门": "电焊生产车间"}),
            lx.data.Extraction(extraction_class="体检日期", extraction_text="2025-03-14", attributes={"体检日期": "2025-03-14"}),
            lx.data.Extraction(extraction_class="出生年月", extraction_text="1972年11月26日", attributes={"出生年月": "1972年11月26日"}),
            lx.data.Extraction(extraction_class="国籍", extraction_text="中华人民共和国", attributes={"国籍": "中华人民共和国"}),
            lx.data.Extraction(extraction_class="民族", extraction_text="满族", attributes={"民族": "满族"}),
            lx.data.Extraction(extraction_class="婚否", extraction_text="已婚", attributes={"婚否": "已婚"}),
            lx.data.Extraction(extraction_class="总工龄", extraction_text="22年3月", attributes={"总工龄": "22年3月"}),
            lx.data.Extraction(extraction_class="接害工龄", extraction_text="26年1月", attributes={"接害工龄": "26年1月"}),
            lx.data.Extraction(extraction_class="接触有害因素", extraction_text="噪声、粉尘", attributes={"接触有害因素": "噪声、粉尘"})
        ]
    ),
]

"""
这个例子使用qwen/qwen3-30b-a3b模型,还是比较稳定的
"""
model_config = lx.factory.ModelConfig(
    model_id="qwen/qwen3-30b-a3b",
    provider="OpenAILanguageModel",
    provider_kwargs={
        "base_url": "https://platform.aitools.cfd/api/v1",  # 这里的不能使用下面的后缀,要用base_url
        # "format_type": lx.data.FormatType.JSON,
        "temperature": 0.1,
        "api_key": "sk-b891a5********0a781", #这里做了脱敏
        "max_workers": 50,
    },
)

print(lx.providers.router.list_providers())

model = lx.factory.create_model(
    config=model_config,
)

result = lx.extract(
    text_or_documents=sales_text,    
    prompt_description=prompt,    
    examples=examples,    
    model=model,
    fence_output=True,
    use_schema_constraints=False,
    # debug=True
    extraction_passes=1
)

for e in result.extractions:
    print(e.extraction_class, e.extraction_text, e.attributes)

# 步骤4:处理结果
if result.extractions:
    extracted_metadata = {}
    extracted_metadata_attributes = {}
    for ext in result.extractions:
        # 如果 key 已存在就跳过
        if ext.extraction_class not in extracted_metadata:
            extracted_metadata[ext.extraction_class] = ext.extraction_text
            extracted_metadata_attributes[ext.extraction_class] = ext.attributes

    print("提取出的结构化元数据:")
    for key, value in extracted_metadata.items():
        print(f"  - {key}: {value}")
        print(f"  - {key} attributes: {extracted_metadata_attributes[key]}")
else:
    print("未能提取出任何信息。")

# 步骤5 (可选): 可视化调试
lx.io.save_annotated_documents([result], output_name="extraction_results.jsonl", output_dir=".")
html_content = lx.visualize("extraction_results.jsonl")
with open("visualization.html","w", encoding="utf-8")as f:
    f.write(html_content)

注意:代码中的api_key做了脱敏处理,如果需要使用免费大模型接口的,可以参考我上一篇文章:Google开源框架LangExtract实践(1)——Docker部署,免费、低碳、无需GPU、多种大模型灵活切换,绝对可用!

输出结果如下(数据做了脱敏处理,看起来跟截图不一样,其实是一致的):

提取出的结构化元数据:
  - 体检类别: 上岗前
  - 体检类别 attributes: {'体检类别': '上岗前'}
  - 接触毒物种类及名称: 噪声, 电焊烟尘
  - 接触毒物种类及名称 attributes: {'接触毒物种类及名称': '噪声, 电焊烟尘'}
  - 姓名: 张家辉
  - 姓名 attributes: {'姓名': '张家辉'}
  - 身份证: 120222197211269696
  - 身份证 attributes: {'身份证': '120222197211269696'}
  - 单位: 苏州龙华飞鸟锅炉压力容器有限公司
  - 单位 attributes: {'单位': '苏州龙华飞鸟锅炉压力容器有限公司'}
  - 电话: 15966666626
  - 电话 attributes: {'电话': '15966666626'}
  - 部门: 钻床
  - 部门 attributes: {'部门': '钻床'}
  - 体检日期: 2025-06-24
  - 体检日期 attributes: {'体检日期': '2025-06-24'}
  - 出生年月: 1972年11月26日
  - 出生年月 attributes: {'出生年月': '1972年11月26日'}
  - 国籍: 中华人民共和国
  - 国籍 attributes: {'国籍': '中华人民共和国'}
  - 民族: 汉族
  - 民族 attributes: {'民族': '汉族'}
  - 婚否: 已婚
  - 婚否 attributes: {'婚否': '已婚'}
  - 总工龄: 32年7月
  - 总工龄 attributes: {'总工龄': '32年7月'}
  - 接害工龄: 25年1月
  - 接害工龄 attributes: {'接害工龄': '25年1月'}
  - 接触有害因素: 噪声,电焊烟尘
  - 接触有害因素 attributes: {'接触有害因素': '噪声,电焊烟尘'}
  - 性别: 男
  - 性别 attributes: {'性别': '男'}
  - 肺功能: FVC
  - 肺功能 attributes: {'肺功能': 'FVC'}

接下来的实际代码中只要对attributes进行数据提取即可。

本期内容就到这里,大家可以将代码复制下来本地跑一下。也可以参照我一篇文章中使用Docker + 免费大模型接口来运行。

下期准备跟大家分享更复杂的信息提取,即从表格中提取数据(即:多行数据、列表数据提取),敬请期待。