ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

pdf转md的三种方式

pdf转md的三种方式 最近工作中需要将扫描的pdf文件转换为Markdown格式方便后续项目复用相同的合同文本信息而且Markdown格式更方便大家及大模型解读因此就做了这个转换工具主要支持三种直接提取文本、通过OCR提取文本、提取pdf中的文本和图像、去除页眉页脚后的文本提取等。其他类型的转换工具详见之前文章。主要内容如下供参考。一、主要功能主要实现将pdf格式的文本内容完整提取出来 包括直接提取文本、通过OCR提取文本、提取pdf中的文本和图像、去除页眉页脚后的文本提取等。具体支持1.通过PyMuPDF实现PDF转Markdownpdf_to_markdown_with_images() - 基于pymupdf实现PDF转Markdown包含图像提取功能按照原文顺序保存文本和图像2.通过OCR提取文本实现了文本提取的功能增强具体如下pdf_ocr_extract() - 基于Tesseract OCR提取PDF文本pdf_to_markdown_ocr() - OCR转换PDF为Markdown支持页眉页脚裁剪pdf_extract_smart() - 智能提取自动判断是否需要OCRpdf_compare_extraction() - 对比直接提取与OCR结果_remove_chinese_spaces() - 后处理去除中文字符间多余空格 __二、实现效果原始pdf部分截图转换后的Markdown文本租金、管理费、保证金、电费等各类款项 )。乙方不得委托第三方支付、代付原租赁合同及本补充协议项下款项。 9, 电费 : 乙方每月根据甲方按所租写字间之独立电表所示之耗用量而发出的收费单向甲方缴纳费用 , 缴费期限为相关收费单发出之日起 15 个工作日内。甲方保留在有关政府部门或供电企业调整各项收费时 , 相应谋整相关收费标准的权利。 — 独立电表收费标准 : 按湖北省目录电价执行缴纳。 10. 保险 : 因乙方原回或在乙方租赁范围内发生事故的而产生赔修的 , 由乙方承担责任。乙方须在租贩期内 , 购买安工险、财产险及公众责任险等相关保险 , 如乙方 决定或者放弃购买前述相关保险 , 所产生的责任和经济损失 , 由乙方自行承担。 11 本大厦空调正常使用时间由周一至周五早 8:00 至晚 19:00。周六早 9:00 至中午 13:00 ( 周日及国家法定假期停止运行 ) 。如乙方于正常时间外有需要 , 按人民币 900. 00 元 / 小时 ( 最少两小时 ) 计算。 12. 双方同意 , 免租期优惠以本合同的完全充分履行为前提 , 若乙方出现本合同项下 的违约行为、提前解除本合同或导致本各同被提前解除 , 甲方有权取消乙方享受 的装修期及免租期优惠并要求乙方按第 2 条 2. 1 约定的每月含税租金及第 2 条 2.2 约定的每月含税物业费标准予以补缴。如乙方在本协议项下已履行租赁期满 18 个月 , 则本条不适用。三、核心代码#!/root/anaconda3/envs/llm/bin/python # -*- coding: utf-8 -*- import fitz # PyMuPDF import pymupdf4llm import pathlib import pytesseract from PIL import Image import io, argparse # 基于PyMuPDF提取PDF标题 def extract_title_from_pdf(file_path): doc fitz.open(file_path) # metadata doc.metadata # title metadata.get(title, No Title Found) # return title page doc[0] blocks page.get_text(dict)[blocks] for block in blocks: if block[type] 0: # 文本块 for line in block[lines]: for span in line[spans]: # 获取格式信息 font_size span[size] # 字体大小 font_flags span[flags] # 样式标志加粗、斜体等 font_name span[font] # 字体名称 text span[text] # 文本内容 # 判断是否可能是标题 if font_size 12 and font_flags 2 ** 4: # 字号大且加粗 print(f可能的标题: {text}) return blocks # 基于Tesseract OCR提取PDF文本 def pdf_ocr_extract(pdf_path, langchi_simeng, zoom2, pagesNone): 基于Tesseract OCR提取PDF中的文本适用于扫描版PDF或直接提取文本不准确的情况 参数: pdf_path: PDF文件路径 lang: OCR语言默认中文简体英文可选 chi_sim(简体中文), chi_tra(繁体中文), eng(英文) zoom: 页面渲染缩放比例值越大图像越清晰但处理越慢默认2 pages: 要处理的页码列表(从0开始)None表示处理所有页 返回: 按页组织的文本列表每个元素为(页码, 文本内容)元组 doc fitz.open(pdf_path) results [] page_indices pages if pages is not None else range(doc.page_count) for page_num in page_indices: page doc[page_num] # 将页面渲染为图片 pix page.get_pixmap(matrixfitz.Matrix(zoom, zoom)) # 转换为PIL Image img_data pix.tobytes(png) img Image.open(io.BytesIO(img_data)) # OCR识别 text pytesseract.image_to_string(img, langlang) # 后处理去除中文字符间的多余空格 text _remove_chinese_spaces(text) results.append((page_num, text.strip())) print(fOCR处理第 {page_num 1} 页完成识别字符数: {len(text)}) doc.close() return results # 后处理去除中文字符间多余空格 def _remove_chinese_spaces(text): 去除中文字符之间的多余空格保留英文单词间的空格 import re # 匹配中文字符包括标点之间的空格 # 模式中文字符 空格 中文字符 result re.sub(r([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef])\s([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]), r\1\2, text) # 多次处理直到没有变化 while result ! text: text result result re.sub(r([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef])\s([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]), r\1\2, text) return result # 对比直接提取与OCR结果 def pdf_compare_extraction(pdf_path, langchi_simeng, zoom2): 对比直接提取和OCR提取的结果帮助选择最佳方案 参数: pdf_path: PDF文件路径 lang: OCR语言 zoom: OCR渲染缩放 返回: dict: 包含direct_text(直接提取)和ocr_text(OCR识别)的对比结果 doc fitz.open(pdf_path) print( * 60) print(PDF文本提取对比分析) print( * 60) # 直接提取 direct_texts [] for page_num in range(doc.page_count): page doc[page_num] text page.get_text(text).strip() direct_texts.append((page_num, text)) # OCR提取 ocr_texts pdf_ocr_extract(pdf_path, lang, zoom) doc.close() # 统计对比 direct_total sum(len(t) for _, t in direct_texts) ocr_total sum(len(t) for _, t in ocr_texts) print(f\n直接提取总字符数: {direct_total}) print(fOCR识别总字符数: {ocr_total}) # 显示第一页对比 if direct_texts and ocr_texts: print(\n * 60) print(第一页对比预览) print( * 60) print(\n【直接提取】:) print(direct_texts[0][1][:300] ... if len(direct_texts[0][1]) 300 else direct_texts[0][1]) print(\n【OCR识别】:) print(ocr_texts[0][1][:300] ... if len(ocr_texts[0][1]) 300 else ocr_texts[0][1]) return { direct_text: direct_texts, ocr_text: ocr_texts, direct_total: direct_total, ocr_total: ocr_total } # OCR转换PDF为Markdown支持页眉页脚裁剪 def pdf_to_markdown_ocr(pdf_path, output_md_path, langchi_simeng, zoom2, header_ratio0.08, footer_ratio0.08): 基于OCR将PDF转换为Markdown支持页眉页脚裁剪 参数: pdf_path: PDF文件路径 output_md_path: 输出的Markdown文件路径 lang: OCR语言 zoom: 渲染缩放比例 header_ratio: 页眉占比(从顶部裁剪) footer_ratio: 页脚占比(从底部裁剪) doc fitz.open(pdf_path) markdown_content [] for page_num in range(doc.page_count): page doc[page_num] page_height page.rect.height page_width page.rect.width # 计算裁剪区域 clip_rect fitz.Rect( 0, header_ratio * page_height, page_width, (1 - footer_ratio) * page_height ) # 渲染裁剪后的区域 pix page.get_pixmap(matrixfitz.Matrix(zoom, zoom), clipclip_rect) img_data pix.tobytes(png) img Image.open(io.BytesIO(img_data)) # OCR识别 text pytesseract.image_to_string(img, langlang) # 后处理去除中文字符间的多余空格 text _remove_chinese_spaces(text) # 添加页面分隔 if page_num 0: markdown_content.append(f\n\n---\n\n) markdown_content.append(f## 第 {page_num 1} 页\n\n) markdown_content.append(text.strip()) print(fOCR处理第 {page_num 1}/{doc.page_count} 页) doc.close() # 保存Markdown文件 output_path pathlib.Path(output_md_path) output_path.write_text(\n.join(markdown_content), encodingutf-8) print(f\n✅ OCR识别完成Markdown文件已保存: {output_path.absolute()}) return \n.join(markdown_content) # 智能提取自动判断是否需要OCR def pdf_extract_smart(pdf_path, output_md_pathNone, langchi_simeng, zoom2, use_ocr_fallbackTrue): 智能PDF文本提取优先直接提取文本若文本量过少则自动使用OCR 参数: pdf_path: PDF文件路径 output_md_path: 输出Markdown路径None则自动生成 lang: OCR语言 zoom: OCR渲染缩放 use_ocr_fallback: 当直接提取文本量少于阈值时是否使用OCR补充 doc fitz.open(pdf_path) total_direct_text 0 pages_need_ocr [] # 第一遍直接提取文本统计每页文本量 page_texts [] for page_num in range(doc.page_count): page doc[page_num] text page.get_text(text).strip() page_texts.append(text) total_direct_text len(text) # 如果该页文本量很少可能需要OCR if len(text) 100: # 阈值少于100字符 pages_need_ocr.append(page_num) doc.close() print(f直接提取总字符数: {total_direct_text}) print(f需要OCR补充的页面: {[p1 for p in pages_need_ocr]}) # 判断是否需要OCR avg_text_per_page total_direct_text / len(page_texts) if page_texts else 0 if avg_text_per_page 200 or len(pages_need_ocr) len(page_texts) * 0.3: # 平均每页少于200字符或超过30%的页需要OCR则全部使用OCR print(检测到文本量较少使用OCR进行完整识别...) if output_md_path is None: output_md_path pathlib.Path(pdf_path).with_suffix(.ocr.md) return pdf_to_markdown_ocr(pdf_path, output_md_path, lang, zoom) elif use_ocr_fallback and pages_need_ocr: # 只对需要OCR的页面进行补充识别 print(f对 {len(pages_need_ocr)} 页进行OCR补充识别...) ocr_results pdf_ocr_extract(pdf_path, lang, zoom, pages_need_ocr) ocr_dict {p: t for p, t in ocr_results} # 合并结果 markdown_content [] for page_num, direct_text in enumerate(page_texts): if page_num 0: markdown_content.append(f\n\n---\n\n) markdown_content.append(f## 第 {page_num 1} 页\n\n) # 优先使用直接提取的文本若太少则用OCR结果 if len(direct_text) 100: markdown_content.append(direct_text) else: markdown_content.append(ocr_dict.get(page_num, direct_text)) if output_md_path is None: output_md_path pathlib.Path(pdf_path).with_suffix(.smart.md) output_path pathlib.Path(output_md_path) output_path.write_text(\n.join(markdown_content), encodingutf-8) print(f\n✅ 智能提取完成Markdown文件已保存: {output_path.absolute()}) return \n.join(markdown_content) else: # 直接提取足够无需OCR print(直接提取文本量充足无需OCR。) if output_md_path is None: output_md_path pathlib.Path(pdf_path).with_suffix(.md) markdown_content [] for page_num, text in enumerate(page_texts): if page_num 0: markdown_content.append(f\n\n---\n\n) markdown_content.append(f## 第 {page_num 1} 页\n\n) markdown_content.append(text) output_path pathlib.Path(output_md_path) output_path.write_text(\n.join(markdown_content), encodingutf-8) print(f\n✅ 文本提取完成Markdown文件已保存: {output_path.absolute()}) return \n.join(markdown_content) # 基于pymupdf4llm实现PDF转Markdown包含图像提取功能按照原文顺序保存文本和图像 def pdf_to_markdown_with_images(pdf_path, output_md_path, image_folderimages): 将 PDF 转换为 Markdown文本和图像按原顺序保存图像以外部文件关联 参数: pdf_path: PDF 文件路径 output_md_path: 输出的 Markdown 文件路径 image_folder: 图像存储文件夹名称 # 创建图像存储文件夹 # sys.path.append(../) # 将当前目录加载道path中 image_path pathlib.Path(image_folder) image_path.mkdir(exist_okTrue) # 去掉页眉页脚方法结合PyMuPDF获取页面大小 # 打开PDF获取页面尺寸 doc fitz.open(pdf_path) page doc[0] # 获取页面尺寸 # page_width page.rect.width page_height page.rect.height header_ratio 0.08 footer_ratio 0.08 # 计算裁剪区域排除页眉页脚 topClipheader_ratio * page_height bottomClipfooter_ratio * page_height # 提取 PDF 内容包含图像 # write_imagesTrue: 将图像保存为外部文件 # image_path: 指定图像保存文件夹 # image_format: 选择图像格式png/jpg # dpi: 设置图像分辨率默认150 # margins裁剪页眉页脚,(上, 下) 或者 (左, 上, 右, 下) md_content pymupdf4llm.to_markdown( docpdf_path, margins(topClip,bottomClip), # 方法去掉页眉页脚(上, 下) write_imagesTrue, # 关键将图像写入文件而非嵌入 image_pathstr(image_path), # 图像保存路径 image_formatpng, # 图像格式 dpi200 # 分辨率越高越清晰但文件越大 ) # 保存 Markdown 文件UTF-8 编码 output_path pathlib.Path(output_md_path) output_path.write_bytes(md_content.encode(utf-8)) print(f✅ Markdown 文件已保存: {output_path.absolute()}) print(f✅ 图像文件已保存至: {image_path.absolute()}) print(f✅ 共提取 {md_content.count(![]()} 张图像) return md_content # 使用示例 if __name__ __main__: parser argparse.ArgumentParser(descriptionPDF文本提取与OCR识别工具) parser.add_argument(pdf_path, nargs?, helpPDF文件路径) parser.add_argument(-o, --output, help输出Markdown文件路径) parser.add_argument(-m, --mode, choices[smart, ocr, direct, compare], defaultsmart, help提取模式: smart(智能), ocr(强制OCR), direct(直接提取), compare(对比)) parser.add_argument(-l, --lang, defaultchi_simeng, helpOCR语言默认chi_simeng) parser.add_argument(-z, --zoom, typefloat, default2, helpOCR渲染缩放比例默认2) args parser.parse_args() # 如果没有提供参数使用测试文件 if args.pdf_path is None: test_pdf pathlib.Path(__file__).parent / data / temp.pdf output_md pathlib.Path(__file__).parent / data / temp_ocr.md print( * 60) print(PDF OCR文本识别测试) print( * 60) print(f测试文件: {test_pdf}) print(f输出文件: {output_md}) print() # 默认使用对比模式展示效果 result pdf_compare_extraction(str(test_pdf), args.lang, args.zoom) # 同时保存OCR结果 print(\n * 60) print(保存OCR识别结果到文件) print( * 60) ocr_content [] for page_num, text in result[ocr_text]: if page_num 0: ocr_content.append(\n\n---\n\n) ocr_content.append(f## 第 {page_num 1} 页\n\n) ocr_content.append(text) pathlib.Path(output_md).write_text(\n.join(ocr_content), encodingutf-8) print(f✅ 已保存: {output_md}) else: pdf_path pathlib.Path(args.pdf_path) output_md args.output if args.output else pdf_path.with_suffix(.md) if args.mode compare: pdf_compare_extraction(str(pdf_path), args.lang, args.zoom) elif args.mode ocr: pdf_to_markdown_ocr(str(pdf_path), str(output_md), args.lang, args.zoom) elif args.mode direct: # 方式1: 仅提取文本 doc fitz.open(str(pdf_path)) content [] for page_num in range(doc.page_count): if page_num 0: content.append(\n\n---\n\n) content.append(f## 第 {page_num 1} 页\n\n) content.append(doc[page_num].get_text(text)) doc.close() pathlib.Path(output_md).write_text(\n.join(content), encodingutf-8) print(f✅ 直接提取完成: {output_md}) # # 方式2: 文本和图像都提取 # pdf_to_markdown_with_images(str(pdf_path), str(output_md)) else: # smart pdf_extract_smart(str(pdf_path), str(output_md), args.lang, args.zoom)
返回列表