이 글에서는 PDFMiner를 사용하여 PDF 코드 예제를 구문 분석하는 Python을 주로 소개합니다. 편집자는 이것이 꽤 좋다고 생각하므로 지금 공유하고 참고용으로 제공하겠습니다. 에디터를 따라가서 살펴볼까요
최근 크롤러를 하다 보면 가끔 웹사이트에서 PDF만 제공하는 상황이 발생합니다. 이런 식으로 scrapy를 사용하여 페이지 내용을 직접 캡처할 수는 없고, 캡처만 할 수 있습니다. PDF를 구문 분석하여 처리할 수 있지만 현재 솔루션은 대략 pyPDF 및 PDFMiner뿐입니다. 텍스트 파싱에는 PDFMiner가 더 적합하다고 하고, 내가 파싱해야 할 것은 텍스트이기 때문에 결국 PDFMiner를 사용하기로 결정했습니다(즉, pyPDF에 대해 전혀 모른다는 뜻입니다).
일단 PDF를 파싱하는 것은 매우 고통스럽습니다. 심지어 PDFMiner도 불규칙한 형식의 PDF를 파싱하는 데는 그다지 능숙하지 않기 때문에 PDFMiner 개발자조차도 PDF가 나쁘다고 불평합니다. 중요하지 않습니다.
1. 설치:
1. 먼저 소스 파일 패키지 pypi.python.org/pypi/pdfminer/를 다운로드하고 압축을 푼 다음 python setup 명령줄에서 설치합니다. py install
2. 설치가 완료된 후 다음 명령줄을 사용하여 테스트하십시오. pdf2txt.py 샘플/simple1.pdf 다음 내용이 표시되면 설치가 성공한 것입니다.
Hello World Hello World H e l l o W or l d H e l l o W or l d
3. 중국어, 일본어, 한국어 문자를 사용하려면
# make cmap python tools/conv_cmap.py pdfminer/cmap Adobe-CNS1 cmaprsrc/cid2code_Adobe_CNS1.txtreading 'cmaprsrc/cid2code_Adobe_CNS1.txt'...writing 'CNS1_H.py'......(this may take several minutes) # python setup.py install
를 컴파일한 후 설치해야 합니다.2.
사용 PDF 구문 분석은 시간과 메모리를 많이 소모하는 작업이므로 PDFMiner는 시간과 메모리 사용량을 줄이기 위해 필요할 때만 구문 분석하는 지연 구문 분석 전략을 사용합니다. PDF를 구문 분석하려면 PDFParser와 PDFDocument라는 두 가지 이상의 클래스가 필요합니다. PDFParser는 파일에서 데이터를 추출하고 PDFDocument는 데이터를 저장합니다. 또한 페이지 내용을 처리하려면 PDFPageInterpreter가 필요하며 PDFDevice는 이를 필요한 내용으로 변환합니다. PDFResourceManager는 글꼴이나 이미지와 같은 공유 콘텐츠를 저장하는 데 사용됩니다.
그림 1. PDFMiner 클래스 간의 관계
더 중요한 것은 주로 다음 구성 요소를 포함하는 레이아웃입니다.
LTPage
전체 페이지를 나타냅니다. LTTextBox, LTFigure, LTImage, LTRect, LTCurve 및 LTLine과 같은 하위 개체를 포함할 수 있습니다.
LTTextBox
포함할 수 있는 텍스트 청크 그룹을 나타냅니다. 이 상자는 기하학적 분석에 의해 생성되며 반드시 텍스트의 논리적 경계를 나타내지는 않습니다. 여기에는 LTTextLine 개체 목록이 포함되어 있습니다. get_text() 메서드는 텍스트 콘텐츠를 반환합니다.
LTTextLine
단일 텍스트 줄을 나타내는 LTChar 객체 목록이 포함되어 있습니다. 문자는 텍스트의 쓰기 모드에 따라 가로 또는 세로로 정렬됩니다. get_text() 메서드는 텍스트 내용을 반환합니다.
LTChar
LTAnno
텍스트의 실제 문자를 유니코드 문자열로 나타냅니다. LTChar 객체에는 실제 경계가 있지만 LTAno 객체는 "가상" 문자이므로 그렇지 않습니다. 두 문자 사이의 관계(예: 공백)에 따라 레이아웃 분석기에 의해 삽입됩니다.
LTFigure
PDF 양식 개체에서 사용되는 영역을 나타냅니다. PDF 양식은 그림이나 그림을 표시하는 데 사용할 수 있습니다. 페이지 내에 또 다른 PDF 문서를 포함하면 LTFigure 개체가 재귀적으로 나타날 수 있습니다.
LTImage
포함된 이미지는 JPEG 또는 다른 형식일 수 있지만 현재 PDFMiner는 그렇지 않습니다. 그래픽 개체에 크게 신경 쓰지 마세요.
LTLine
단일 직선을 나타냅니다. 텍스트나 그림을 구분하는 데 사용할 수 있습니다.
LTRect
를 나타냅니다. 직사각형 . 다른 그림이나 그림의 프레임을 만드는 데 사용할 수 있습니다.
LTCurve
일반 베지어 곡선을 나타냅니다.
官方文档给了几个Demo但是都过于简略,虽然给了一个详细一些的Demo,但链接地址是旧的现在已经失效,不过最终还是找到了新的地址:denis.papathanasiou.org/posts/2010.08.04.post.html
这个Demo就比较详细了,源码如下:
#!/usr/bin/python import sys import os from binascii import b2a_hex ### ### pdf-miner requirements ### from pdfminer.pdfparser import PDFParser from pdfminer.pdfdocument import PDFDocument, PDFNoOutlines from pdfminer.pdfpage import PDFPage from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTFigure, LTImage, LTChar def with_pdf (pdf_doc, fn, pdf_pwd, *args): """Open the pdf document, and apply the function, returning the results""" result = None try: # open the pdf file fp = open(pdf_doc, 'rb') # create a parser object associated with the file object parser = PDFParser(fp) # create a PDFDocument object that stores the document structure doc = PDFDocument(parser, pdf_pwd) # connect the parser and document objects parser.set_document(doc) # supply the password for initialization if doc.is_extractable: # apply the function and return the result result = fn(doc, *args) # close the pdf file fp.close() except IOError: # the file doesn't exist or similar problem pass return result ### ### Table of Contents ### def _parse_toc (doc): """With an open PDFDocument object, get the table of contents (toc) data [this is a higher-order function to be passed to with_pdf()]""" toc = [] try: outlines = doc.get_outlines() for (level,title,dest,a,se) in outlines: toc.append( (level, title) ) except PDFNoOutlines: pass return toc def get_toc (pdf_doc, pdf_pwd=''): """Return the table of contents (toc), if any, for this pdf file""" return with_pdf(pdf_doc, _parse_toc, pdf_pwd) ### ### Extracting Images ### def write_file (folder, filename, filedata, flags='w'): """Write the file data to the folder and filename combination (flags: 'w' for write text, 'wb' for write binary, use 'a' instead of 'w' for append)""" result = False if os.path.isdir(folder): try: file_obj = open(os.path.join(folder, filename), flags) file_obj.write(filedata) file_obj.close() result = True except IOError: pass return result def determine_image_type (stream_first_4_bytes): """Find out the image file type based on the magic number comparison of the first 4 (or 2) bytes""" file_type = None bytes_as_hex = b2a_hex(stream_first_4_bytes) if bytes_as_hex.startswith('ffd8'): file_type = '.jpeg' elif bytes_as_hex == '89504e47': file_type = '.png' elif bytes_as_hex == '47494638': file_type = '.gif' elif bytes_as_hex.startswith('424d'): file_type = '.bmp' return file_type def save_image (lt_image, page_number, images_folder): """Try to save the image data from this LTImage object, and return the file name, if successful""" result = None if lt_image.stream: file_stream = lt_image.stream.get_rawdata() if file_stream: file_ext = determine_image_type(file_stream[0:4]) if file_ext: file_name = ''.join([str(page_number), '_', lt_image.name, file_ext]) if write_file(images_folder, file_name, file_stream, flags='wb'): result = file_name return result ### ### Extracting Text ### def to_bytestring (s, enc='utf-8'): """Convert the given unicode string to a bytestring, using the standard encoding, unless it's already a bytestring""" if s: if isinstance(s, str): return s else: return s.encode(enc) def update_page_text_hash (h, lt_obj, pct=0.2): """Use the bbox x0,x1 values within pct% to produce lists of associated text within the hash""" x0 = lt_obj.bbox[0] x1 = lt_obj.bbox[2] key_found = False for k, v in h.items(): hash_x0 = k[0] if x0 >= (hash_x0 * (1.0-pct)) and (hash_x0 * (1.0+pct)) >= x0: hash_x1 = k[1] if x1 >= (hash_x1 * (1.0-pct)) and (hash_x1 * (1.0+pct)) >= x1: # the text inside this LT* object was positioned at the same # width as a prior series of text, so it belongs together key_found = True v.append(to_bytestring(lt_obj.get_text())) h[k] = v if not key_found: # the text, based on width, is a new series, # so it gets its own series (entry in the hash) h[(x0,x1)] = [to_bytestring(lt_obj.get_text())] return h def parse_lt_objs (lt_objs, page_number, images_folder, text=[]): """Iterate through the list of LT* objects and capture the text or image data contained in each""" text_content = [] page_text = {} # k=(x0, x1) of the bbox, v=list of text strings within that bbox width (physical column) for lt_obj in lt_objs: if isinstance(lt_obj, LTTextBox) or isinstance(lt_obj, LTTextLine): # text, so arrange is logically based on its column width page_text = update_page_text_hash(page_text, lt_obj) elif isinstance(lt_obj, LTImage): # an image, so save it to the designated folder, and note its place in the text saved_file = save_image(lt_obj, page_number, images_folder) if saved_file: # use html style <img alt="PDFMiner를 사용하여 PDF 예제를 구문 분석하는 Python에 대한 자세한 설명" > tag to mark the position of the image within the text text_content.append('<img alt="PDFMiner를 사용하여 PDF 예제를 구문 분석하는 Python에 대한 자세한 설명" >') else: print >> sys.stderr, "error saving image on page", page_number, lt_obj.__repr__ elif isinstance(lt_obj, LTFigure): # LTFigure objects are containers for other LT* objects, so recurse through the children text_content.append(parse_lt_objs(lt_obj, page_number, images_folder, text_content)) for k, v in sorted([(key,value) for (key,value) in page_text.items()]): # sort the page_text hash by the keys (x0,x1 values of the bbox), # which produces a top-down, left-to-right sequence of related columns text_content.append(''.join(v)) return '\n'.join(text_content) ### ### Processing Pages ### def _parse_pages (doc, images_folder): """With an open PDFDocument object, get the pages and parse each one [this is a higher-order function to be passed to with_pdf()]""" rsrcmgr = PDFResourceManager() laparams = LAParams() device = PDFPageAggregator(rsrcmgr, laparams=laparams) interpreter = PDFPageInterpreter(rsrcmgr, device) text_content = [] for i, page in enumerate(PDFPage.create_pages(doc)): interpreter.process_page(page) # receive the LTPage object for this page layout = device.get_result() # layout is an LTPage object which may contain child objects like LTTextBox, LTFigure, LTImage, etc. text_content.append(parse_lt_objs(layout, (i+1), images_folder)) return text_content def get_pages (pdf_doc, pdf_pwd='', images_folder='/tmp'): """Process each of the pages in this pdf file and return a list of strings representing the text found in each page""" return with_pdf(pdf_doc, _parse_pages, pdf_pwd, *tuple([images_folder])) a = open('a.txt','a') for i in get_pages('/home/jamespei/nova.pdf'): a.write(i) a.close()
这段代码重点在于第128行,可以看到PDFMiner是一种基于坐标来解析的框架,PDF中能解析的组件全都包括上下左右边缘的坐标,如x0 = lt_obj.bbox[0]就是lt_obj元素的左边缘的坐标,同理x1则为右边缘。以上代码的意思就是把所有x0且x1的坐标相差在20%以内的元素分成一组,这样就实现了从PDF文件中定向抽取内容。
----------------补充--------------------
有一个需要注意的地方,在解析有些PDF的时候会报这样的异常:pdfminer.pdfdocument.PDFEncryptionError: Unknown algorithm: param={'CF': {'StdCF': {'Length': 16, 'CFM': /AESV2, 'AuthEvent': /DocOpen}}, 'O': '\xe4\xe74\xb86/\xa8)\xa6x\xe6\xa3/U\xdf\x0fWR\x9cPh\xac\xae\x88B\x06_\xb0\x93@\x9f\x8d', 'Filter': /Standard, 'P': -1340, 'Length': 128, 'R': 4, 'U': '|UTX#f\xc9V\x18\x87z\x10\xcb\xf5{\xa7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'V': 4, 'StmF': /StdCF, 'StrF': /StdCF}
从字面意思来看是因为这个PDF是一个加密的PDF,所以无法解析 ,但是如果直接打开PDF却是可以的并没有要求输密码什么的,原因是这个PDF虽然是加过密的,但密码是空,所以就出现了这样的问题。
解决这个的问题的办法是通过qpdf命令来解密文件(要确保已经安装了qpdf),要想在python中调用该命令只需使用call即可:
from subprocess import call call('qpdf --password=%s --decrypt %s %s' %('', file_path, new_file_path), shell=True)
其中参数file_path是要解密的PDF的路径,new_file_path是解密后的PDF文件路径,然后使用解密后的文件去做解析就OK了
위 내용은 PDFMiner를 사용하여 PDF 예제를 구문 분석하는 Python에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!