Niedawno zmagałem się z podobnym problemem, chociaż mój PDF miał nieco prostszą strukturę.
PDFMiner używa klas zwanych "urządzeniami" do analizowania stron w pliku pdf. Podstawową klasą urządzeń jest klasa PDFPageAggregator, która po prostu analizuje pola tekstowe w pliku. Klasy konwerterów, np. TextConverter, XMLConverter i HTMLConverter również wyprowadzają wynik w pliku (lub w strumieniu łańcuchów, jak w twoim przykładzie) i wykonują bardziej skomplikowane analizowanie zawartości.
Problem z TextConverter (i PDFPageAggregator) polega na tym, że nie rekompensują one wystarczająco głębokości struktury dokumentu, aby poprawnie wyodrębnić różne kolumny. Dwa pozostałe konwertery wymagają pewnych informacji o strukturze dokumentu do celów wyświetlania, aby gromadzić bardziej szczegółowe dane. W twoim przykładzie pdf oba uproszczone urządzenia analizują (w przybliżeniu) całe pole tekstowe zawierające kolumny, co uniemożliwia (lub przynajmniej bardzo trudne) prawidłowe rozdzielenie różnych wierszy. Rozwiązaniem tego, że znalazłem całkiem dobrze działa, to albo
- utworzyć nową klasę, która dziedziczy PDFPageAggregator lub
- Używaj XMLConverter i analizowania wynikowy dokument XML przy użyciu np Beautifulsoup
W obu przypadkach konieczne będzie połączenie różnych segmentów tekstu z wierszami przy użyciu współrzędnych y.
W przypadku nowej klasy urządzeń ("jest to bardziej wymowne, myślę) musiałbyś przesłonić metodę receive_layout
, która jest wywoływana dla każdej strony podczas procesu renderowania. Ta metoda następnie rekurencyjnie analizuje elementy na każdej stronie.Na przykład coś takiego może Ci zacząć:
from pdfminer.pdfdocument import PDFDocument, PDFNoOutlines
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LTPage, LTChar, LTAnno, LAParams, LTTextBox, LTTextLine
class PDFPageDetailedAggregator(PDFPageAggregator):
def __init__(self, rsrcmgr, pageno=1, laparams=None):
PDFPageAggregator.__init__(self, rsrcmgr, pageno=pageno, laparams=laparams)
self.rows = []
self.page_number = 0
def receive_layout(self, ltpage):
def render(item, page_number):
if isinstance(item, LTPage) or isinstance(item, LTTextBox):
for child in item:
render(child, page_number)
elif isinstance(item, LTTextLine):
child_str = ''
for child in item:
if isinstance(child, (LTChar, LTAnno)):
child_str += child.get_text()
child_str = ' '.join(child_str.split()).strip()
if child_str:
row = (page_number, item.bbox[0], item.bbox[1], item.bbox[2], item.bbox[3], child_str) # bbox == (x1, y1, x2, y2)
self.rows.append(row)
for child in item:
render(child, page_number)
return
render(ltpage, self.page_number)
self.page_number += 1
self.rows = sorted(self.rows, key = lambda x: (x[0], -x[2]))
self.result = ltpage
w kodzie powyżej, każdy znalazł elementem LTTextLine jest przechowywany w uporządkowaną listę krotek zawierających numer strony, współrzędne obwiedni, a tekst zawarty w tym konkretnym elemencie. Można by wtedy zrobić coś podobnego do tego:
from pprint import pprint
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.layout import LAParams
fp = open('pdf_doc.pdf', 'rb')
parser = PDFParser(fp)
doc = PDFDocument(parser)
doc.initialize('password') # leave empty for no password
rsrcmgr = PDFResourceManager()
laparams = LAParams()
device = PDFPageDetailedAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)
for page in PDFPage.create_pages(doc):
interpreter.process_page(page)
# receive the LTPage object for this page
device.get_result()
pprint(device.rows)
Zmienna device.rows zawiera uporządkowaną listę wszystkich linii tekstu umieszczone przy użyciu swojego numeru strony i Y współrzędnych. Możesz przechodzić przez linie tekstu i linie grupowe z tymi samymi współrzędnymi y, aby tworzyć wiersze, przechowywać dane kolumn itp.
Próbowałem przeanalizować plik pdf, używając powyższego kodu, a kolumny są w większości poprawnie przetwarzane. Jednak niektóre kolumny są tak blisko siebie, że domyślna heurystyka modułu PDFMiner nie może ich oddzielić na własne elementy. Prawdopodobnie można to obejść, modyfikując parametr "margin" (flaga -W w narzędziu wiersza poleceń pdf2text.py). W każdym razie możesz przeczytać (słabo udokumentowane) PDFMiner API, a także przejrzeć kod źródłowy PDFMiner, który można uzyskać z github. (Niestety, nie mogę wkleić linku, ponieważ nie mam wystarczającej liczby punktów rep: "<, ale możesz mieć nadzieję, że poprawne repo to go)
Czy ten pierwszy blok kodu powinien brzmieć' retstr = StringIO.StringIO() '? – Stedy
Wiele kolumn jest naprawdę trudnym do odczytania z pliku PDF. W zależności od tego, co chcesz [k2pdfopt] (http://www.willus.com/k2pdfopt/) tworzy obrazy z każdej strony. – bobrobbob