こしあん
2023-12-20

PDFMinerを並列化して読み込みを高速化する


531{icon} {views}


PDFMinerというMITライセンスで利用できる、PythonベースのPDFパーサーを並列化する方法を紹介します。このライブラリ、パースが遅いというデメリットがあるのですが、並列化が効果的な場合もあります。

はじめに

PDFMinerというPythonベースのPDF読み取りライブラリがあります。MITライセンスで利用できて大変便利なのですが、遅いというデメリットがあります。

https://github.com/pdfminer/pdfminer.six

PyMuPDFを使うと圧倒的に早くなるのですが、AGPLライセンスというデメリットがあります。特にAGPLでも気にしなくていいという場合は、普通にPyMuPDFを使うのがいいと思います。

https://pymupdf.readthedocs.io/en/latest/

今回はPDFMinerを使いつつ、並列化の力技でゴリ押しして高速化する方法です。Pythonベースでなくていいなら(別途バックエンド建てる)なら他言語のライブラリを使うなども普通に考えられるでしょう。

並列化の考え方

PDFファイルのまま並列化して読み込むと競合してエラーになってしまうので、メモリにマッピングして並列処理させます。

PDFMinerでページ単位で読み込む方法は、公式コードにかかれています。

https://pdfminersix.readthedocs.io/en/latest/tutorial/composable.html

並列化コード

結果はこちらになります。以下のファイルで実験しました

import io
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfinterp import resolve1
import concurrent.futures
import time

def get_page_text(binary: bytes, page_num: int):
    with io.BytesIO(binary) as file_buf:
        with io.StringIO() as output_string:
            rsrcmgr = PDFResourceManager()
            device = TextConverter(rsrcmgr, output_string, laparams=LAParams())
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            page = list(PDFPage.get_pages(file_buf, pagenos=[page_num]))[0]
            interpreter.process_page(page)
            return output_string.getvalue()

def parse_pdf(filename: str, max_worker=8):
    with open(filename, 'rb') as in_file:
        binary = in_file.read()
        parser = PDFParser(in_file)
        doc = PDFDocument(parser)
        max_pages = resolve1(doc.catalog['Pages'])['Count']

        # Multi Process Mapping
        buf_iterator = [binary for i in range(max_pages)]
        page_iterator = list(range(0, max_pages))
        with concurrent.futures.ProcessPoolExecutor(max_workers=max_worker) as executor:
            result = list(executor.map(get_page_text, buf_iterator, page_iterator))

    return result

def main():
    start_time = time.time()
    parse_pdf(filename="zentai.pdf", max_worker=16)
    print(time.time() - start_time)

if __name__ == "__main__":
    main()

Corei9-9900KのCPUで実験した結果です(8コア16スレッド)。PDFMinerのバージョンは20221105です。

並列化なし 8プロセス並列 16プロセス並列
通商白書2023(333P) 68.33 39.45 30.94
トヨタ自動車有価証券報告書 2023年3月期(241P) 34.27 84.94 71.70

通商白書の場合は順当に高速化しましたが、有価証券報告書の場合は逆に遅くなりました。早くなるケースもあれば遅くなるケースもあるそうです。

これだけが方法じゃないと思いますが、PDFMiner縛りでいきたい!というときの力技としてはありではないでしょうか。



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Tags:

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です