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

Python替换PDF文本遇格式差异问题咨询及处理方案

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单一文本操作符**,文本是连续的完整字符串,直接包含在一对括号中,结构简单。

现有代码的缺陷

  1. 仅处理Tj操作符:代码只判断行结尾是否为tj,完全忽略了TJ操作符的存在
  2. 按行拆分逻辑不鲁棒:PDF内容流不一定按操作符换行,TJ相关内容可能和其他操作符在同一行
  3. 无法处理拆分的文本段:示例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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 17:15:37