Python处理PDF-通过关键词定位-截取PDF中的图表
起因:
因为个人原因, 这些天了解了一下Python处理PDF的方法.
首先是PDF转txt, 这个方法比较多, 这里就不再赘述, 主要聊一下PDF中的图片获取.
这里用我自己的例子, 不过具体情况还得具体分析.
工具: pdfminer, pillow, fitz, re
思路:
1. 使用pdfminer解析PDF, 通过当前页的LTpage对象, 获取关键词的position与当前LTpage的size.
2. 使用fitz将当前页的PDF转换为PNG
3. 使用pillow, 通过第一步得到的参数来从第二步得到的PNG中截取目标图表
关键词: “[图表]*\s\d+[::]”, “来源[::]”
代码:
1 from pdfminer.pdfparser import PDFParser, PDFDocument 2 from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter 3 from pdfminer.converter import PDFPageAggregator 4 from pdfminer.layout import LAParams 5 from pdfminer.pdfinterp import PDFTextExtractionNotAllowed 6 from PIL import Image 7 import fitz 8 import re 9 import os 10 11 12 class GetPic: 13 def __init__(self, filename, password=\'\'): 14 """ 15 初始化 16 :param filename: pdf路径 17 :param password: 密码 18 """ 19 with open(filename, \'rb\') as file: 20 # 创建文档分析器 21 self.parser = PDFParser(file) 22 # 创建文档 23 self.doc = PDFDocument() 24 # 连接文档与文档分析器 25 self.parser.set_document(self.doc) 26 self.doc.set_parser(self.parser) 27 # 初始化, 提供初始密码, 若无则为空字符串 28 self.doc.initialize(password) 29 # 检测文档是否提供txt转换, 不提供就忽略, 抛出异常 30 if not self.doc.is_extractable: 31 raise PDFTextExtractionNotAllowed 32 else: 33 # 创建PDF资源管理器, 管理共享资源 34 self.resource_manager = PDFResourceManager() 35 # 创建一个PDF设备对象 36 self.laparams = LAParams() 37 self.device = PDFPageAggregator(self.resource_manager, laparams=self.laparams) 38 # 创建一个PDF解释器对象 39 self.interpreter = PDFPageInterpreter(self.resource_manager, self.device) 40 # pdf的page对象列表 41 self.doc_pdfs = list(self.doc.get_pages()) 42 # 打开PDF文件, 生成一个包含图片doc对象的可迭代对象 43 self.doc_pics = fitz.open(filename) 44 45 def to_pic(self, doc, zoom, pg, pic_path): 46 """ 47 将单页pdf转换为pic 48 :param doc: 图片的doc对象 49 :param zoom: 图片缩放比例, type int, 数值越大分辨率越高 50 :param pg: 对象在doc_pics中的索引 51 :param pic_path: 图片保存路径 52 :return: 图片的路径 53 """ 54 rotate = int(0) 55 trans = fitz.Matrix(zoom, zoom).preRotate(rotate) 56 pm = doc.getPixmap(matrix=trans, alpha=False) 57 path = os.path.join(pic_path, str(pg)) + \'.png\' 58 pm.writePNG(path) 59 return path 60 61 def get_pic_loc(self, doc): 62 """ 63 获取单页中图片的位置 64 :param doc: pdf的doc对象 65 :return: 返回一个list, 元素为图片名称和上下y坐标元组组成的tuple. 当前页的尺寸 66 """ 67 self.interpreter.process_page(doc) 68 layout = self.device.get_result() 69 # pdf的尺寸, tuple, (width, height) 70 canvas_size = layout.bbox 71 # 图片名称坐标 72 loc_top = [] 73 # 来源坐标 74 loc_bottom = [] 75 # 图片名称与应截取的区域y1, y2坐标 76 loc_named_pic = [] 77 # 遍历单页的所有LT对象 78 for i in layout: 79 if hasattr(i, \'get_text\'): 80 text = i.get_text().strip() 81 # 匹配关键词 82 if re.search(r\'图表*\s\d+[::]\', text): 83 loc_top.append((i.bbox, text)) 84 elif re.search(r\'来源[::]\', text): 85 loc_bottom.append((i.bbox, text)) 86 zip_loc = zip(loc_top, loc_bottom) 87 for i in zip_loc: 88 y1 = i[1][0][1] 89 y2 = i[0][0][3] 90 name = i[0][1] 91 loc_named_pic.append((name, (y1, y2))) 92 return loc_named_pic, canvas_size 93 94 def get_crops(self, pic_path, canvas_size, position, cropped_pic_name, cropped_pic_path): 95 """ 96 按给定位置截取图片 97 :param pic_path: 被截取的图片的路径 98 :param canvas_size: 图片为pdf时的尺寸, tuple, (0, 0, width, height) 99 :param position: 要截取的位置, tuple, (y1, y2) 100 :param cropped_pic_name: 截取的图片名称 101 :param cropped_pic_path: 截取的图片保存路径 102 :return: 103 """ 104 img = Image.open(pic_path) 105 # 当前图片的尺寸 tuple(width, height) 106 pic_size = img.size 107 # 截图的范围扩大值 108 size_increase = 10 109 x1 = 0 110 x2 = pic_size[0] 111 y1 = pic_size[1] * (1 - (position[1] + size_increase)/canvas_size[3]) 112 y2 = pic_size[1] * (1 - (position[0] - size_increase)/canvas_size[3]) 113 cropped_img = img.crop((x1, y1, x2, y2)) 114 # 保存截图文件的路径 115 path = os.path.join(cropped_pic_path, cropped_pic_name) + \'.png\' 116 cropped_img.save(path) 117 print(\'成功截取图片:\', cropped_pic_name) 118 119 def main(self, pic_path, cropped_pic_path, pgn=None): 120 """ 121 主函数 122 :param pic_path: 被截取的图片路径 123 :param cropped_pic_path: 图片的截图的保存路径 124 :param pgn: 指定获取截图的对象的索引 125 :return: 126 """ 127 if pgn is not None: 128 # 获取当前页的doc 129 doc_pdf = self.doc_pdfs[pgn] 130 doc_pic = self.doc_pics[pgn] 131 # 将当前页转换为PNG, 返回值为图片路径 132 path = self.to_pic(doc_pic, 2, pgn, pic_path) 133 loc_name_pic, canvas_size = self.get_pic_loc(doc_pdf) 134 if loc_name_pic: 135 for i in loc_name_pic: 136 position = i[1] 137 cropped_pic_name = re.sub(\'/\', \'_\', i[0]) 138 self.get_crops(path, canvas_size, position, cropped_pic_name, cropped_pic_path) 139 140 141 if __name__ == \'__main__\': 142 pdf_path = \'要处理的PDF的路径\' 143 test = GetPic(pdf_path) 144 pic_path = \'PNG的保存路径\' 145 cropped_pic_path = \'截图的保存路径\' 146 page_count = test.doc_pics.pageCount 147 for i in range(page_count): 148 test.main(pic_path, cropped_pic_path, pgn=i)
本例局限:
1. 目标PDF需要可以用pdfminer里的LTPage对象解析出文字.
2. PDF中没有跳页的图表.
3. 截图的时候只用了y轴截图, x轴上可能出现多个图表
局限解决方案:
1. 目前没有去尝试, 或许PyPDF2可以试一试?
2. 这里的函数都是处理单页的, 所有在处理连页图片时会出现问题, 不过解决方法也很简单. 就是将 loc_top、loc_bottom设置为全局变量并且加上页码的索引, 这样loc_top和loc_bottm中的元素就能够一一对应. 再加上一个判断, top的y轴坐标比bottom小的话, 就截取两张图片, top的y轴坐标至页尾和bottom的y轴坐标至页头. 有兴趣的可以自己尝试一下.
3. 这个问题的话, 一是可以后期通过其它库再按照一定的方法截取一次; 二是可以在一次截取的时候加上x轴的左坐标来确定目标位置, 因为如果同一y轴范围内只有一个图表的话, x轴右坐标就无关紧要类, 如果同一y轴范围内有两个图标的话, 通过x轴左坐标也能化界, 如果有两个以上的图标时候就需要加上x轴的右坐标了.
结语:
这里只是提供了一种思路, 方法其实还是很不完善的, 很多小细节都没有去解决.
还有一种思路是将PDF转换为PNG之后直接识别其中的关键词左边来获取截图, 这个的话大家也可以去了解一下, 用tesserocr库应该可以解决.