Python替换PDF文本遇格式差异问题咨询及处理方案
问题背景
用Python脚本替换PDF文本时,部分文件可正常替换,部分无法生效。提供的两类PDF内容流中,示例2可成功替换,示例1失败,当前代码无法处理示例1的格式。
内容流格式分析
示例1格式特点
Tm 0 g [(Amount In)1( Words: )] TJ /FAAABC 8 Tf [(Three Lakh)-1( S)1(e)-1(ven Thousand)-1( S)1(e)-1(ven Hund)-1(red Ninety Two Rup)-1(ees And Two Paise Only)] TJ ET Q Q Q q 1 0 0 1 0 442.59799194 cm q 1 g 0.125 0.25 m 560.02502441 0.25 l 560.02502441 8.25 l 0.125 8.25 l h f* Q q 0.125 0.25 m 560.02502441 0.25 l 560.02502441 8.25 l 0.125 8.25 l h W n q 1 0 0 1 0.125 0.25 cm Q Q Q q 1 0 0 1 0 450.84799194 cm q 1 g 0.125 0.25 m 560.02502441 0.25 l 560.02502441 9.75 l 0.125 9.75 l h f* Q q 0.125 0.25 m 560.02502441 0.25 l 560.02502441 9.75 l 0.125 9.75 l h W n q 1 0 0 1 5.625 0.25 cm BT /FAAAAJ 8 Tf 1 0 0 -1 0 6.8499999
示例1使用**TJ复合文本操作符**,文本被拆分为多个带字符间距调整参数的子串(比如(Amount In)1( Words: )中的1是间距调整值),整个文本是多个子串拼接而成,而非连续的完整字符串。
示例2格式特点
' 8 Tf', '( :Two Thousand Eight Hundred Eighty Three Only)Tj', '1 0 0 1 34.85 237.161 Tm', '/F2'
示例2使用**Tj单一文本操作符**,文本是连续的完整字符串,直接包含在一对括号中,结构简单。
现有代码的缺陷
- 仅处理
Tj操作符:代码只判断行结尾是否为tj,完全忽略了TJ操作符的存在 - 按行拆分逻辑不鲁棒:PDF内容流不一定按操作符换行,
TJ相关内容可能和其他操作符在同一行 - 无法处理拆分的文本段:示例1中目标替换文本可能被拆分成多个子串,简单的字符串替换无法匹配分散在多个括号中的内容
修复后的代码实现
修改核心的replace_text函数,支持TJ操作符的解析与重组,同时改进内容流的处理逻辑:
import re import argparse import os from PyPDF2 import PdfFileReader, PdfFileWriter from PyPDF2.generic import DecodedStreamObject, EncodedStreamObject, NameObject def parse_tj_content(tj_part): # 解析TJ中的文本段和调整参数 pattern = re.compile(r'\((.*?)\)(-?\d+)?', re.DOTALL) matches = pattern.findall(tj_part) segments = [] full_text = "" for text, adjust in matches: full_text += text segments.append((text, adjust if adjust else "")) return full_text, segments def rebuild_tj_content(segments, original_text, new_text): # 替换文本后重新构建TJ格式,保留原间距调整参数 new_segments = [] segment_count = len(segments) if segment_count == 0: return f"[{new_text}] TJ" # 按原段数拆分新文本,适配原间距结构 avg_len = len(new_text) // segment_count parts = [] for i in range(segment_count-1): parts.append(new_text[i*avg_len:(i+1)*avg_len]) parts.append(new_text[(segment_count-1)*avg_len:]) for part, (_, adjust) in zip(parts, segments): # 转义PDF特殊字符 part_escaped = part.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") if adjust: new_segments.append(f"({part_escaped}){adjust}") else: new_segments.append(f"({part_escaped})") return f"[{''.join(new_segments)}] TJ" def replace_text(content, replacements = dict()): # 处理TJ复合文本操作符 def replace_tj(match): tj_content = match.group(1) original_full_text, segments = parse_tj_content(tj_content) replaced_text = original_full_text for k, v in replacements.items(): if k in replaced_text: replaced_text = replaced_text.replace(k, v) if replaced_text == original_full_text: return match.group(0) return rebuild_tj_content(segments, original_full_text, replaced_text) content = re.sub(r'\[(.*?)\] TJ', replace_tj, content, flags=re.DOTALL) # 处理Tj单一文本操作符 def replace_tj_single(match): text_content = match.group(1) replaced_text = text_content for k, v in replacements.items(): if k in replaced_text: replaced_text = replaced_text.replace(k, v) if replaced_text == text_content: return match.group(0) replaced_text = replaced_text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") return f"({replaced_text})Tj" content = re.sub(r'\((.*?)\)Tj', replace_tj_single, content, flags=re.DOTALL) return content def process_data(object, replacements): data = object.getData() # 兼容PDF常用编码:先尝试UTF-8,失败用Latin-1 try: decoded_data = data.decode('utf-8') except UnicodeDecodeError: decoded_data = data.decode('latin-1') replaced_data = replace_text(decoded_data, replacements) # 编码回原格式 try: encoded_data = replaced_data.encode('utf-8') except: encoded_data = replaced_data.encode('latin-1') if object.decodedSelf is not None: object.decodedSelf.setData(encoded_data) else: object.setData(encoded_data) if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", required=True, help="path to PDF document") args = vars(ap.parse_args()) in_file = args["input"] filename_base = os.path.splitext(in_file)[0] replacements = {'02-JUN-21': '03 June 2022', '16 November 2022': '03 June 2022'} pdf = PdfFileReader(in_file) writer = PdfFileWriter() for page_number in range(pdf.getNumPages()): page = pdf.getPage(page_number) contents = page.getContents() if isinstance(contents, (DecodedStreamObject, EncodedStreamObject)): process_data(contents, replacements) elif hasattr(contents, '__len__') and len(contents) > 0: for obj in contents: if isinstance(obj, (DecodedStreamObject, EncodedStreamObject)): streamObj = obj.getObject() process_data(streamObj, replacements) # 更新页面内容 if hasattr(contents, 'decodedSelf'): page[NameObject("/Contents")] = contents.decodedSelf else: page[NameObject("/Contents")] = contents writer.addPage(page) with open(f"{filename_base}_result.pdf", 'wb') as out_file: writer.write(out_file)
关键改进点
- 支持
TJ操作符:解析TJ中的文本段和间距调整参数,替换后重建符合格式的内容 - 鲁棒的编码处理:兼容UTF-8和PDF常用的Latin-1编码
- 正则匹配替代行拆分:直接匹配操作符,不受内容换行影响
- 自动转义特殊字符:处理PDF文本中的
\、(、)等特殊符号
注意事项
- 若需要局部文本替换(而非完整替换
TJ内的全部文本),需调整rebuild_tj_content函数实现更精细的拆分逻辑 - 若PDF使用字体子集化,需确保替换文本的字符在原字体中存在,否则可能显示异常
- 加密PDF、表单类PDF需额外处理解密或表单域逻辑
内容的提问来源于stack exchange,提问作者VijayaDurgaPrasad

