很多人让Codex理解PDF的时候,就是直接给它一个路径,让它自己去读。
这样看起来很省事,但最终Codex输出的结果可能不太符合预期。
尤其一碰到流程图、表格、时序图,翻车概率会明显变高。
其实大多数时候,问题不在模型,而在喂法。
PDF这个东西,本质是给人看的, 不是给模型看的。
人看PDF的时候,会忽略页眉页脚目录,会自己抓重点,也会自然地把正文和图表对应起来。
但模型不一定会。
对模型来说,页眉、页脚、页码、目录、注释,可能都会一起混进上下文。 真正关键的内容,很多时候又不在正文里,而是在架构图、流程图、状态机图、时序图和表格里。 再加上很多PDF随便就是几十页,你让它一边排噪音,一边找重点。 就很容易跑偏。
所以我现在都不直接把原始PDF喂给Codex。
而是先把PDF转成md,再把关键页单独导成图片。
说白了,就是把不太适合模型直接理解的PDF,拆成两类更清晰的材料。 md负责让它读文字,图片负责让它看图。
这样处理完之后,效果会比直接读原始 PDF 稳很多。
工具上,我比较推荐直接走Python这一套。
pymupdf 负责读PDF和渲染页面
pymupdf4llm 负责把PDF提取成更适合模型读的Markdown
pillow 负责导出图片
安装也很简单,一条命令就够了:
pip install pymupdf pymupdf4llm pillow这套方案有个好处,就是不用太依赖 pdftoppm。 因为很多人安装的 pdftoppm 根本不是同一个版本。 有的是Poppler。 有的是Xpdf。 参数支持不一样。 看起来命令差不多,实际一跑就报错。
直接用 Python 处理,反而更省事。
这个pdf_pack_for_codex.py脚本可以把PDF提取成 Markdown, 把页面导出成PNG或JPEG。 支持自定义DPI。 支持指定页范围。 最后再生成一个打包目录。
这样真正喂给Codex的,就不再是原始PDF。 而是一份整理过的md,再加几张关键页图片。
最常用的命令,就三条。
默认是PNG、200 DPI、导出全部页面,同时生成Markdown。
python .\pdf_pack_for_codex.py "F:\xx.pdf"如果你只关心关键页,比如第 3 到第 8 页:
python .\pdf_pack_for_codex.py "F:\xx.pdf" png 200 3 8如果图片太大,想缩小空间,就改成 JPEG:
python .\pdf_pack_for_codex.py "F:\xx.pdf" jpeg 200 3 8这三条,日常基本就够用了。
为什么建议默认200 DPI。
因为技术PDF里经常有小字、表格、流程图、架构图、细线条。 DPI 低了,细节容易看不清。
150 DPI,能看。
180 DPI,比较均衡。
200 DPI,更稳。
如果你本来就是要把图导出来,再给Codex 看,那200 DPI是一个挺合适的默认值。
还有一个点,很多人前面都做对了。 最后一步却还是把结果搞差了。
最典型的就是,文件都准备好了,结果发给Codex的时候只说一句:
帮我看下这个PDF。
这种问法太宽泛了。
更好的方式,是直接把范围和目标说清楚。 比如让它先读导出的md,再重点看第 8 到第 12 页图片,只回答状态切换顺序、哪一步最可能丢消息、文档里有没有前后矛盾,如果证据不足就明确写不确定。
这样出来的结果,会明显更像能直接拿去用的分析结论。
所以其实不是Codex能不能看PDF,而是没有把PDF处理成更适合它理解的样子。
直接把原始PDF丢进去,它不是完全不能看。 但不够稳。
先转md,让它把正文读明白。 再导关键页图片,让它把图看清楚。 最后带着明确问题一起问。
这套做法,会把实际效果往上提升了一大截。
这也是我现在一直在用的一套方法。
附:pdf_pack_for_codex.py 完整脚本
from pathlib import Pathimport sysimport fitz # PyMuPDFimport pymupdf4llmfrom PIL import Imagedef print_usage():print("用法:")print(" python pdf_pack_for_codex.py <input.pdf> [format] [dpi] [first_page] [last_page]")print("")print("参数说明:")print(" input.pdf : PDF 文件路径")print(" format : png / jpeg / jpg,默认 png")print(" dpi : 输出图片 DPI,默认 200")print(" first_page : 起始页码,从 1 开始,默认 1")print(" last_page : 结束页码,默认最后一页")print("")print("示例:")print(r" python .\pdf_pack_for_codex.py .\demo.pdf")print(r" python .\pdf_pack_for_codex.py .\demo.pdf png 200 3 8")print(r" python .\pdf_pack_for_codex.py .\demo.pdf jpeg 200 1 5")def normalize_format(fmt: str) -> str:fmt = fmt.lower().strip()if fmt == "jpg":fmt = "jpeg"if fmt not in ("png", "jpeg"):raise ValueError("format 只支持 png / jpeg / jpg")return fmtdef export_page_as_image(page, output_file: Path, dpi: int, image_format: str):scale = dpi / 72.0matrix = fitz.Matrix(scale, scale)pix = page.get_pixmap(matrix=matrix, alpha=False)if image_format == "png":pix.save(str(output_file))elif image_format == "jpeg":img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)img.save(output_file, format="JPEG", quality=95)else:raise ValueError(f"不支持的图片格式: {image_format}")def export_images(pdf_path: Path, img_dir: Path, image_format: str, dpi: int, first_page: int, last_page: int):doc = fitz.open(pdf_path)total_pages = len(doc)if total_pages == 0:doc.close()raise RuntimeError("PDF 没有可导出的页面")if first_page < 1:doc.close()raise ValueError("first_page 不能小于 1")if last_page > total_pages:doc.close()raise ValueError(f"last_page 超出范围,文档总页数为 {total_pages}")if first_page > last_page:doc.close()raise ValueError("first_page 不能大于 last_page")for page_num in range(first_page, last_page + 1):page = doc.load_page(page_num - 1)output_file = img_dir / f"page-{page_num}.{image_format}"export_page_as_image(page, output_file, dpi, image_format)print(f"已导出图片: {output_file}")doc.close()def main():if len(sys.argv) < 2:print_usage()sys.exit(1)pdf_path = Path(sys.argv[1]).resolve()if not pdf_path.exists():print(f"文件不存在: {pdf_path}")sys.exit(1)try:image_format = normalize_format(sys.argv[2]) if len(sys.argv) >= 3 else "png"dpi = int(sys.argv[3]) if len(sys.argv) >= 4 else 200except ValueError as e:print(f"参数错误: {e}")print_usage()sys.exit(1)try:doc = fitz.open(pdf_path)total_pages = len(doc)doc.close()except Exception as e:print(f"无法打开 PDF: {e}")sys.exit(1)try:first_page = int(sys.argv[4]) if len(sys.argv) >= 5 else 1last_page = int(sys.argv[5]) if len(sys.argv) >= 6 else total_pagesexcept ValueError:print("页码参数必须是整数")print_usage()sys.exit(1)base_dir = pdf_path.parent / f"{pdf_path.stem}_codex_pack"img_dir = base_dir / "images"base_dir.mkdir(parents=True, exist_ok=True)img_dir.mkdir(parents=True, exist_ok=True)md_path = base_dir / f"{pdf_path.stem}.md"try:print("开始导出 Markdown...")md_text =pymupdf4llm.to_markdown(str(pdf_path))md_path.write_text(md_text, encoding="utf-8")print(f"Markdown: {md_path}")print(f"开始导出图片,格式={image_format},dpi={dpi},页码范围={first_page}-{last_page} ...")export_images(pdf_path=pdf_path,img_dir=img_dir,image_format=image_format,dpi=dpi,first_page=first_page,last_page=last_page,)print("")print("导出完成")print(f"Markdown: {md_path}")print(f"Images: {img_dir}")except Exception as e:print(f"执行失败: {e}")sys.exit(1)if __name__ == "__main__":main()
夜雨聆风