在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")
四、特殊情况的调优建议
- OCR质量极差的文档:如果某份PDF的OCR结果完全没法看,可以用
tesseract包重新OCR,开启布局分析:
library(tesseract) # 重新OCR指定页码 ocr_text <- ocr(pdf_path, page = 23:106, options = list(tessedit_pageseg_mode = 3))
(注:重新OCR会增加处理时间,适合质量特别差的文档)
- 发言者列表自动补全:对于后续年份的文档,你可以尝试从文本中自动提取发言者,减少手动整理的工作量:
# 自动提取可能的发言者(匹配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()
- 结果验证:处理完后随机抽几份PDF的结果,和原文档对比,调整双栏拆分的连续空格阈值(比如把
\\s{3,}改成\\s{2,}或\\s{4,}),适配不同PDF的格式差异。
按照这个流程,你应该能解决跨栏语序混乱的核心问题,并且自动化处理所有PDF。如果遇到某份文档的拆分效果特别差,单独调整下双栏拆分的参数就能搞定~




