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

如何用R的pdftools正确拆分双栏PDF文本并提取表格?

双栏PDF表格提取与分栏优化问题

我需要从双栏格式的PDF文档中提取表格,使用pdftools提取文本时,两栏内容会被拼接在一起,且表格有时会跨两栏分布,导致需要大量清理才能提取数据。

现有代码如下:

library(pdftools)
library(stringr)
library(dplyr)

txt <- pdf_text("https://www.govinfo.gov/content/pkg/BUDGET-2025-APP/pdf/BUDGET-2025-APP-1-3.pdf")

left_rows <- list()
right_rows <- list()

for (i in seq_along(txt)) {
  lines <- str_split(txt[[i]], "\n")[[1]]
  lines <- str_trim(lines)
  locs <- str_locate_all(txt[[i]], regex("2025 est\\.", ignore_case = TRUE))[[1]]
  if (nrow(locs) == 0) {
    split_pos <- floor(max(nchar(lines)) / 2)
  } else {
    split_pos <- locs[1,2]
  }
  for (line in lines) {
    if (nchar(line) < split_pos) {
      left_col <- line
      right_col <- ""
    } else {
      left_col <- str_trim(substr(line, 1, split_pos))
      right_col <- str_trim(substr(line, split_pos + 1, nchar(line)))
    }
    left_fields <- str_split(left_col, "\\s{2,}")[[1]]
    right_fields <- str_split(right_col, "\\s{2,}")[[1]]
    if (length(left_fields) > 0 && any(left_fields != "")) {
      left_rows <- append(left_rows, list(left_fields))
    }
    if (length(right_fields) > 0 && any(right_fields != "")) {
      right_rows <- append(right_rows, list(right_fields))}}}

当前问题在于:现有代码基于“2025 est.”的位置或行长度中点确定固定拆分位置split_pos,但由于行长度不一,拆分后左右栏内容仍会错误合并,无法有效分离两栏。请问是否有更优方法可将双栏PDF文本拆分为独立的左右栏,并将两栏内容上下堆叠?


解决方案

方法1:用布局识别工具直接提取(推荐)

pdftools仅提取文本不保留布局信息,改用tabulizer包可直接识别PDF的栏位结构,甚至一键提取表格:

library(tabulizer)
# 提取指定页码的表格,自动识别双栏布局
tables <- extract_tables("BUDGET-2025-APP-1-3.pdf", pages = 1:3, method = "stream")
# 将左右栏表格上下堆叠(若跨栏则自动合并为完整表格)
combined_table <- do.call(rbind, tables)

注:stream方法适合结构化表格,若PDF是扫描件则需用ocr方法。

方法2:优化文本拆分逻辑(基于空白频率统计)

如果坚持用pdftools,不要用固定拆分点,而是统计每行空白区域的分布,找到最密集的空白位置作为分栏边界:

library(pdftools)
library(stringr)
library(dplyr)

txt <- pdf_text("BUDGET-2025-APP-1-3.pdf")

# 自定义函数:通过空白频率计算最优分栏位置
get_split_pos <- function(page_text) {
  lines <- str_split(page_text, "\n")[[1]] %>% str_trim()
  lines <- lines[lines != ""]
  if (length(lines) == 0) return(NA)
  
  # 统计每个字符位置的空白出现次数
  char_positions <- lapply(lines, function(line) str_locate_all(line, "\\s")[[1]][,1]) %>% unlist()
  pos_freq <- table(char_positions)
  
  # 过滤掉首尾10%的位置,避免误判边缘空白
  total_chars <- max(nchar(lines))
  valid_pos <- as.integer(names(pos_freq))
  valid_pos <- valid_pos[valid_pos > total_chars*0.1 & valid_pos < total_chars*0.9]
  
  if (length(valid_pos) == 0) {
    return(floor(total_chars / 2))
  } else {
    # 取出现频率最高的位置作为分栏点
    return(as.integer(names(which.max(pos_freq[as.character(valid_pos)]))))
  }
}

left_rows <- list()
right_rows <- list()

for (i in seq_along(txt)) {
  page <- txt[[i]]
  split_pos <- get_split_pos(page)
  if (is.na(split_pos)) next
  
  lines <- str_split(page, "\n")[[1]] %>% str_trim()
  for (line in lines) {
    if (nchar(line) == 0) next
    
    left_col <- str_trim(substr(line, 1, split_pos))
    right_col <- str_trim(substr(line, split_pos + 1, nchar(line)))
    
    if (left_col != "") {
      left_rows <- append(left_rows, list(str_split(left_col, "\\s{2,}")[[1]]))
    }
    if (right_col != "") {
      right_rows <- append(right_rows, list(str_split(right_col, "\\s{2,}")[[1]]))
    }
  }
}

# 合并左右栏内容(左栏在前,右栏在后)
combined_rows <- c(left_rows, right_rows)

方法3:跨栏表格的补充处理

如果表格跨栏,提取后可通过字段数量判断行结构:

  • 若左栏字段数与表格列数匹配,右栏属于下一行内容,直接堆叠即可;
  • 若左右栏字段数之和等于表格列数,说明是同一行跨栏,需要合并左右栏的字段列表。

内容的提问来源于stack exchange,提问作者osu-statistics

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 14:41:01