You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在R中处理OCR质量不佳的双栏长PDF文本,生成顺序正确的发言者轮次数据框

在R中处理OCR质量不佳的双栏长PDF文本,生成顺序正确的发言者轮次数据框

我完全理解你现在的困境——处理这些上世纪的WHO双栏全会PDF时,OCR经常把左右栏的文本混着读,直接导致后续的发言者识别全错,而且几百份文档手动改根本不现实。下面我会一步步给你优化方案,从核心的双栏文本拆分,到发言者识别的鲁棒性提升,再到批量处理的框架,都给你安排得明明白白。


一、核心优化:修复双栏文本拆分逻辑,解决语序混乱

你的初始拆分思路是对的,但老PDF的OCR文本常存在行对齐不齐、栏宽略有差异的问题,按固定字符数切半容易出错。下面是改进后的拆分函数,它会自动检测每页的栏位分割点,适配不同PDF的格式差异:

library(tidyverse)
library(pdftools)
library(stringr)
library(purrr)

# 改进的双栏文本读取函数
read_two_columns_optimized <- function(page_text) {
  # 拆分行并过滤空行、纯空格行
  lines <- str_split(page_text, "\n")[[1]] %>%
    str_squish() %>%
    discard(~.x == "")
  
  if (length(lines) == 0) return("")
  
  # 自动找栏位分割点:基于每行中连续3个以上空格的位置(老PDF双栏间通常有宽空格)
  split_positions <- map_int(lines, function(line) {
    space_runs <- str_locate_all(line, "\\s{3,}")[[1]]
    if (nrow(space_runs) > 0) {
      space_runs[1, "start"]
    } else {
      # 没有宽空格时, fallback到按字符长度切半
      ceiling(nchar(line)/2)
    }
  })
  
  # 取所有行分割点的中位数作为当前页的标准分割位置,适配行对齐误差
  mid_pos <- median(split_positions, na.rm = TRUE) %>% as.integer()
  
  # 拆分左右栏并拼接(先左后右,恢复正确语序)
  left_col <- map_chr(lines, ~str_squish(str_sub(.x, 1, mid_pos))) %>%
    discard(~.x == "") %>%
    paste(collapse = " ")
  
  right_col <- map_chr(lines, ~str_squish(str_sub(.x, mid_pos + 1, nchar(.x)))) %>%
    discard(~.x == "") %>%
    paste(collapse = " ")
  
  paste(left_col, right_col, sep = " ")
}

这个函数的优势:

  • 优先用PDF双栏间的宽空格作为分割依据,比固定字符数更准确
  • 过滤无效行,避免冗余空格干扰
  • 用中位数分割点适配单页内的行对齐误差

二、优化发言者轮次识别:减少OCR误差影响

解决了语序问题后,我们再优化发言者匹配的逻辑,避免因为OCR的大小写错误、名字拆分导致的匹配失败:

process_speaker_turns <- function(full_text, speaker_list) {
  # 预处理文本:合并多余空格、修复换行拆分的单词
  cleaned_text <- str_squish(full_text)
  
  # 生成鲁棒的正则匹配模式:适配名字中的连字符、空格,忽略大小写
  speaker_patterns <- speaker_list$speaker_condensed %>%
    str_replace_all("-", "\\\\-") %>%  # 转义连字符(比如VAN DER BRUGGEN)
    str_replace_all(" ", "\\\\s+")     # 匹配一个或多个空格(适配OCR的空格错误)
  
  pattern <- paste0("\\b(", paste(speaker_patterns, collapse = "|"), ")\\b")
  
  # 定位所有发言者出现的位置
  matches <- str_locate_all(cleaned_text, regex(pattern, ignore_case = TRUE))[[1]]
  
  # 构建发言者轮次数据框
  speaker_turns <- map_dfr(seq_len(nrow(matches)), function(i) {
    start_pos <- matches[i, "start"]
    # 下一个发言者的起始位置,最后一个发言者到文本结尾
    end_pos <- if (i < nrow(matches)) matches[i + 1, "start"] - 1 else nchar(cleaned_text)
    
    # 提取并标准化发言者缩写名
    speaker_match <- str_sub(cleaned_text, start_pos, matches[i, "end"]) %>%
      str_squish() %>%
      toupper()
    
    # 提取发言内容(跳过开头的发言者名字)
    speech_text <- str_sub(cleaned_text, matches[i, "end"] + 1, end_pos) %>%
      str_squish()
    
    tibble(
      speaker_condensed = speaker_match,
      speech_text = speech_text
    )
  })
  
  # 关联完整的发言者信息(国家、全名)
  speaker_turns <- speaker_turns %>%
    left_join(
      speaker_list %>% select(speaker_condensed, country, speaker),
      by = "speaker_condensed"
    )
  
  return(speaker_turns)
}

这里的关键优化:

  • 转义发言者名字中的特殊字符,避免正则匹配失效
  • 发言内容直接跳过发言者名字部分,减少冗余内容
  • 统一转换为大写后匹配,彻底规避OCR的大小写错误

三、构建批量处理框架:自动化搞定几百份PDF

既然你要处理1948-2009年的所有PDF,我们把整个流程封装成批量处理函数,用purrr自动遍历所有文件:

# 单份PDF处理函数
process_single_who_pdf <- function(pdf_path, year, start_page, end_page, speaker_list) {
  # 检查页码有效性
  total_pages <- length(pdf_text(pdf_path))
  end_page <- min(end_page, total_pages)
  
  if (start_page > end_page) {
    warning(glue::glue("⚠️ PDF {pdf_path} 起止页码无效,已跳过"))
    return(NULL)
  }
  
  # 读取目标页码并处理双栏文本
  pdf_pages <- pdf_text(pdf_path)[start_page:end_page]
  full_text <- map_chr(pdf_pages, read_two_columns_optimized) %>%
    paste(collapse = " ") %>%
    str_squish()
  
  # 识别发言者轮次并关联年份、路径信息
  speaker_turns <- process_speaker_turns(full_text, speaker_list) %>%
    mutate(
      year = year,
      pdf_path = pdf_path,
      .before = everything()
    )
  
  return(speaker_turns)
}

# 批量处理示例:假设你有一个包含所有PDF信息的数据框
all_pdfs_info <- tribble(
  ~pdf_path,                ~year, ~start_page, ~end_page,
  "webscrape/who/plenary/manual/WHA_1948.pdf", 1948, 23, 106,
  "webscrape/who/plenary/manual/WHA_1949.pdf", 1949, 79, 147,
  "webscrape/who/plenary/manual/WHA_1950.pdf", 1950, 97, 187
  # 在这里添加所有PDF的路径、年份、起止页码...
)

# 批量处理所有PDF
all_speaker_turns <- pmap_dfr(all_pdfs_info, process_single_who_pdf, speaker_list = speaker_list_unique)

# 保存结果到CSV
write_csv(all_speaker_turns, "who_plenary_speaker_turns_1948-2009.csv")

四、特殊情况的调优建议

  1. OCR质量极差的文档:如果某份PDF的OCR结果完全没法看,可以用tesseract包重新OCR,开启布局分析:
library(tesseract)
# 重新OCR指定页码
ocr_text <- ocr(pdf_path, page = 23:106, options = list(tessedit_pageseg_mode = 3))

(注:重新OCR会增加处理时间,适合质量特别差的文档)

  1. 发言者列表自动补全:对于后续年份的文档,你可以尝试从文本中自动提取发言者,减少手动整理的工作量:
# 自动提取可能的发言者(匹配Mr./Dr./Ms. + 名字格式)
potential_speakers <- str_extract_all(full_text, regex("(Mr\\.|Dr\\.|Ms\\.|Prof\\.)\\s+[A-Za-z\\-\\s]+,", ignore_case = TRUE))[[1]] %>%
  str_squish() %>%
  unique()
  1. 结果验证:处理完后随机抽几份PDF的结果,和原文档对比,调整双栏拆分的连续空格阈值(比如把\\s{3,}改成\\s{2,}\\s{4,}),适配不同PDF的格式差异。

按照这个流程,你应该能解决跨栏语序混乱的核心问题,并且自动化处理所有PDF。如果遇到某份文档的拆分效果特别差,单独调整下双栏拆分的参数就能搞定~

火山引擎 最新活动