You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解析带层级条款、表格的PDF规范文档导出为结构化电子表格

1. 现有R代码返回空输出的核心原因
  • 数据结构转换完全错误:pdf_text() 读取PDF后返回的是每页文本为一个元素的字符向量,你对整个返回值做strsplit(split="\n")得到的是按页拆分的行列表,直接调用as.character()会把整个列表对象转成带R内部结构标记(比如c(、元素索引)的乱码字符串,根本不是可用于正则匹配的原始文档文本,匹配对象从根源上就错了。
  • 正则锚点逻辑不成立:你写的正则用了^/$行首行尾锚点,但as.character()处理后得到的是把所有页面、所有行拼接成的超长单字符串,默认模式下^只会匹配整个长字符串的最开头,$只会匹配最结尾,自然抓不到中间的条款内容。
  • 规则选型不符合场景:现有正则没有考虑编号前后可能存在的缩进空格、折行断句、编号后多空格等实际排版情况,就算数据结构正确,碰到非理想排版的内容也会匹配失效。另外你尝试用的零宽后行断言本身就不适合这个场景——大部分正则引擎要求后行断言匹配的文本长度固定,而你的条款编号长度从2位(如1.)到6位以上(如1.2.3.4.)不等,本来就没法支持可变长度的后行断言。
2. 层级编号PDF(含表格/图示)的高效解析方案

不要死磕纯文本提取+正则的路线,pdftools这类纯文本提取工具会把表格、图注拆成零散的无坐标文本行,根本没法靠正则区分表格内容和正文条款,500页的文档靠调正则碰运气效率极低。推荐按以下路径实现,整体准确率可以到95%以上,剩下少量内容手动校对的总耗时不会超过3小时,比全手动整理效率高至少10倍:

核心实现逻辑

  • 第一步:换带布局识别的PDF解析工具
    优先选支持文本块坐标识别、表格区域自动识别的工具,不要用纯文本提取类包:
    • 如果坚持用R环境,可以用tabulizer包,它基于PDFBox开发,可以提取每个文本块的坐标、自动识别表格区域,把表格和正文分开存储;
    • 更推荐用Python的pdfplumber库,它的布局识别、表格提取精度比R生态同类工具高很多,装个轻量Python环境跑脚本导出csv,再拿回R做后续处理完全不麻烦。
  • 第二步:正文条款拆分
    1. 先把识别为表格、图片、图注、页眉页脚的内容单独过滤存储,不要和正文文本混排;
    2. 把剩下的正文文本块按页面从上到下、同页从左到右的阅读顺序排序;
    3. 用正则^\\s*(\\d+(?:\\.\\d+)*\\.)\\s+匹配行首的条款编号,识别到编号的行作为新条款的起点,后续连续的、开头没有匹配到编号的文本块,全部拼接到当前条款的内容字段里,自动处理跨折行、跨页的长条款。
  • 第三步:表格/图示归属
    提取到的表格、图示,根据它在页面上的Y轴坐标,匹配到位于它上方、距离最近的条款编号,把表格转成markdown文本后拼接到对应条款的内容字段即可,和你期望的输出格式完全兼容。

可直接运行的R版基础代码(纯文本场景)

这个版本修正了你原有代码的逻辑错误,可以先跑通纯文本条款的提取,后续再补表格识别的逻辑:

library(pdftools)
library(dplyr)
library(stringr)
library(purrr)
library(tidyr)

# 逐页读取、逐行拆分,保留页码信息
specDoc <- pdf_text("Spec Doc.pdf") |> 
  strsplit(split = "\n") |> 
  imap_dfr(~ tibble(
    page = .y,
    line_raw = str_trim(.x)
  )) |> 
  # 过滤空行,可根据文档实际情况加规则过滤固定的页眉页脚
  filter(line_raw != "")

# 匹配条款编号、向下填充归属、拼接内容
clause_df <- specDoc |> 
  mutate(
    # 识别条款开头行
    is_clause_start = str_detect(line_raw, "^(\\d+(?:\\.\\d+)*\\.)\\s+"),
    # 提取条款号
    clause_no = ifelse(
      is_clause_start,
      str_extract(line_raw, "^(\\d+(?:\\.\\d+)*\\.)"),
      NA_character_
    ),
    # 去掉行首的编号,保留纯内容
    line_content = ifelse(
      is_clause_start,
      str_remove(line_raw, "^(\\d+(?:\\.\\d+)*\\.)\\s+"),
      line_raw
    )
  ) |> 
  # 向下填充条款号,把后续无编号的行归属到上一个条款
  fill(clause_no, .direction = "down") |> 
  # 按条款号分组拼接完整内容
  group_by(clause_no) |> 
  summarise(
    `Clause Para` = str_c(line_content, collapse = " "),
    .groups = "drop"
  ) |> 
  rename(`Clause No.` = clause_no)

# 导出为csv可直接用Excel打开
write.csv(clause_df, "extracted_clause.csv", row.names = FALSE, fileEncoding = "UTF-8")

注意:这个纯文本版本无法区分表格和正文,碰到表格会把表格行当成普通文本拼到条款内容里,要处理表格必须更换带布局识别的解析工具。

实用避坑提示

  • 正则测试时注意引擎差异:regex101默认用PCRE正则引擎,而R的stringr用的是ICU引擎,两者在多行模式、断言支持上有细微差异,测试规则时要选对对应引擎,避免本地运行和测试结果不一致。
  • 不要追求100%自动解析:500页的招投标文档难免有排版不规范的地方,自动提取完成后花1-2小时校对一遍编号错位、表格归属错误的条目,比花一两周调正则追求完美解析的性价比高得多,毕竟投标工作的时间优先级远高于工具完美度。

内容的提问来源于stack exchange,提问作者inarticulatus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 21:18:21