PDF转成Word之后,文字能选中,不代表能顺畅修改。每行一个段落、左右栏串在一起、整页只剩一张图片,是三种不同的问题,不能统一靠删除换行解决。
下面用三份教学PDF检查:普通正文带表格、双栏文字,以及由第一份文件生成的扫描副本。输入内容与检查结果在下文直接列出;实际操作时可使用自己有权处理的PDF。
先判断是哪种问题
在Word中显示段落标记。行末是段落标记时,按删除键可能合并两个本来独立的段落;如果文字在不同文本框中,删换行也不会把它们变成一段。扫描页还要先生成可编辑文字。
批量替换前先复制文件,只选一小段测试。表格、脚注、标题和列表分别检查,避免把真实结构一起删掉。
根据症状选择检查方法
| 看到的现象 | 先怎么确认 | 下一步 |
|---|---|---|
| 每行末尾都有段落标记 | 在副本中显示格式标记,观察一句话是否被切成数段 | 只选一段试合并,保留标题、列表和表格的结构 |
| 文字能选中,但整段不能连续重排 | 点击文字查看是否位于独立文本框 | 先核对阅读顺序,再决定重排,避免全局删换行 |
| 整页是一张图片 | 点击后只出现图片控制点,文字无法单独选取 | 转换时启用OCR,再核对编号、金额和易混字符 |
这些现象可以同时出现。先判断对象类型,再处理换行,比一上来全局替换更容易保住表格和段落。
用同一组输入检查转换结果
通过PDF转Word接口生成DOCX,扫描样本启用OCR:
bash
curl --fail-with-body 'https://api-server.compdf.com/server/v2/process/pdf/docx' --header "x-api-key: $COMPDF_API_KEY" --form 'file=@word-scan.pdf' --form 'enableOcr=1' --output response.json
响应是JSON,先检查业务状态与下载地址,再下载文件,不能直接改名成DOCX。下面的完整脚本包含这些检查,使用Python 3.10以上标准库,无需安装额外依赖。保存为convert_pdf.py:
python
"""Python 3.10+. Set COMPDF_API_KEY; keep the key on your server."""
import argparse,io,json,os,uuid,zipfile
from pathlib import Path
from urllib.request import Request,urlopen
p=argparse.ArgumentParser()
p.add_argument('input',type=Path);p.add_argument('output',type=Path)
p.add_argument('--operation',choices=['parse','word','ocr'],default='parse')
a=p.parse_args()
endpoints={'parse':'idp/documentParsing','word':'pdf/docx','ocr':'pdf/editable'}
boundary='ComPDFSample'+uuid.uuid4().hex
parts=[]
if a.operation in ['word','ocr']:
parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="enableOcr"\r\n\r\n1\r\n'.encode())
parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="input.pdf"\r\nContent-Type: application/pdf\r\n\r\n'.encode()+a.input.read_bytes()+b'\r\n')
request=Request('https://api-server.compdf.com/server/v2/process/'+endpoints[a.operation],data=b''.join(parts)+f'--{boundary}--\r\n'.encode(),headers={'x-api-key':os.environ['COMPDF_API_KEY'],'Content-Type':'multipart/form-data; boundary='+boundary})
with urlopen(request,timeout=150)as response:payload=json.load(response)
data=payload.get('data')or{}
if isinstance(data,list):data=data[0]if data else{}
if str(payload.get('code'))!='200'or data.get('status')!='success':
raise RuntimeError('Processing did not complete successfully; inspect the business status on the server.')
with urlopen(data['downloadUrl'],timeout=60)as response:body=response.read()
if a.operation=='ocr':
if not body.startswith(b'%PDF'):raise ValueError('Expected PDF')
else:
with zipfile.ZipFile(io.BytesIO(body))as archive:
required='word/document.xml'if a.operation=='word'else 'result_json.json'
if required not in archive.namelist():raise ValueError('Unexpected result archive')
a.output.write_bytes(body)
print('Saved',a.output.name,len(body),'bytes')
把自己的PDF保存为input.pdf,运行:
bash
python convert_pdf.py input.pdf output.docx --operation word
脚本把扫描OCR参数设为1;下载后检查ZIP中存在word/document.xml。遇到业务失败或处理中状态会停止,不把响应JSON改名为DOCX,也没有实现异步轮询。不要将API密钥放进浏览器代码或截图。
密钥只放服务端环境变量。本例调用海外节点;接入时根据账号及数据处理要求选择节点。
输入里有哪些容易丢失的信息
普通正文样本包含两个独立段落,以及以下表格。第二段特意放入编号QA-271与零值0.00,便于核对;扫描样本由同一内容生成。
| 项目 | 数量 | 金额 |
|---|---|---|
| 支架 | 3 | 135.60 |
| 赠品 | 1 | 0.00 |
检查时既看文字是否存在,也看金额仍对应哪一行。双栏样本单独核对"左栏从上到下读完,再读右栏",不把可提取文字当作阅读顺序正确。
实际产物说明了什么
| 输入 | 文件结构检查结果 | 需要注意的地方 |
|---|---|---|
| 普通正文 | 两段正文分别保留;表格仍有独立单元格;没有文本框 | 仍需在目标Word版本中检查增删文字后的布局 |
| 双栏文字 | 每行将左栏和右栏放在同一段,用制表符分隔 | 没有恢复按整列阅读的顺序 |
| 扫描副本 | 正文、编号和表格内容可提取 | 页脚中的"教学示例"未完整保留 |
程序修改并重新读取之外,本次还在WPS中打开普通正文样本的DOCX副本,修改教学说明文字、增加一行表格,再保存、关闭并重开。新增的QA-271、数量1、金额0.00仍在,页面保持可读。此结果不代表已覆盖Microsoft Word的不同版本,也不改变双栏顺序仍需整理的结论。

图:双栏原文件期望顺序与实际DOCX段落顺序对照;依据实际结构重排展示,非Word界面截图
交付前再改一次、重开一次
用实际接收方的Word或WPS打开文件,修改一段长正文,增加一行表格,保存并重新打开。检查后面的段落有没有挤乱、表格金额是否仍对应正确行。
本例普通正文与表格适合继续验证,双栏样本仍需整理。批量接入前,用自己的授权样本分别记录不同版式的通过项,而不是只看转换是否成功。