spaCy(+GiNZA)でPDFテキスト抽出の改行位置をいい感じにする

R&D チームの徳田(@dakuton)です。
過去何回か、Tech Blog記事にてPDFやOCR、自然言語処理に関する手法を紹介してきましたが、今回もそちらに関連する内容です。

過去記事 tech-blog.optim.co.jp tech-blog.optim.co.jp tech-blog.optim.co.jp

やりたいこと

PDFからテキストを抽出する際に含まれる、中途半端な位置にある改行を除去することが目的です。
シンプルな方法としては、句点()の位置をもとに改行する方法ですが、今回はspaCy(とGiNZA)を併用した場合にどうなるかを試してみることにします。

テストデータ

今回は、下記記事のPDFを使用しました。

Poppler(pdftotext)を用いた場合のテキスト抽出結果は以下のとおりです。(冒頭部分のみ記載)

$ wget https://www.mof.go.jp/public_relations/finance/202102/202102b.pdf
$ pdftotext 202102b.pdf -
巻頭言

ポストコロナ時代を形作る、
コロナ禍で生まれる DX(デジ
タルトランスフォーメーション)
株式会社オプティム 代表取締役社長

菅谷 俊二

現

在、私が代表を勤めております株式会社オ
プティムは、私が佐賀大学農学部在学時に
創業した企業で、2015 年には東証一部に上場し
ております。実は数多くの上場企業の中で唯一国
立大学(佐賀大学)内に本店を置いた企業でもあ

・
・
・

フォント埋め込みのPDFでは、1文よりも細かい単位(1文字ごとではありませんが)文字レイアウト情報を保持しています。
この情報をもとにテキストを抽出した場合、上記のように1文ごとの末尾ではなく文の途中に改行が含まれることになります。

当然ながら、フォント埋め込みのPDFだけでなく、OCRを用いたテキスト抽出を行う場合も改行位置補正が必要となります。
あまり読みやすい改行位置とはいえないため、後述のコードで補正をかけることにします。

実験コード(parse_pdf.py)

import re
import subprocess
import sys
from pathlib import Path

import plac
import spacy


def parse_lines(lines, spacy_model):
    lines = lines.strip().replace("\n", "")
    lines = re.split(r"。", lines)

    if spacy_model is None:
        return lines

    return [sent for line in lines for sent in spacy_model(line).sents]


@plac.annotations(
    src=("pdf or txt", "positional"),
    model=("spaCy japanese models or ja_ginza", "option", None, str)
)
def main(src, model):
    file_path = Path(src)
    if file_path.suffix[1:] == "pdf":
        res = subprocess.run(["pdftotext", file_path, "-"], stdout=subprocess.PIPE)
        lines = "".join(res.stdout.decode("utf-8"))
    else:
        lines = open(file_path, "r").read()

    spacy_model = spacy.load(model) if model is not None else None
    for line in parse_lines(lines, spacy_model):
        print(line)


if __name__ == "__main__":
    plac.call(main)

実行結果

句点のみで分割した場合

$ python parse_pdf.py 202102b.pdf
巻頭言ポストコロナ時代を形作る、コロナ禍で生まれるDX(デジタルトランスフォーメーション)株式会社オプティム代表取締役社長菅谷俊二現在、私が代表を勤めております株式会社オプティムは、私が佐賀大学農学部在学時に創業した企業で、2015年には東証一部に上場しております
実は数多くの上場企業の中で唯一国立大学(佐賀大学)内に本店を置いた企業でもあります
そんな私達が夢中になって取り組んでいる事業とは「日本の多くの課題をDX(AI・IoT)によって解決すること」です
我々はこの取組を“〇〇×IT:(あらゆる産業)×(AI・IoT・Cloud・Robotics)”と呼んでいます
・
・
・

句点による分割とGiNZAによる分割を併用した場合

$ python parse_pdf.py -model ja_ginza 202102b.pdf
巻頭言ポスト
コロナ時代を形作る、コロナ禍で生まれるDX(デジタルトランスフォーメーション)株式会社オプティム代表取締役社長菅谷俊二現在、私が代表を勤めております
株式会社オプティムは、私が佐賀大学農学部在学時に創業した企業で、2015年には東証一部に上場しております
実は数多くの上場企業の中で唯一国立大学(佐賀大学)内に本店を置いた企業でもあります
そんな私達が夢中になって取り組んでいる事業とは「日本の多くの課題をDX(AI・IoT)によって解決すること」です
我々はこの取組を“〇〇×IT:(あらゆる産業)×(AI・IoT・Cloud・Robotics)”と呼んでいます
・
・
・

上記について、句点のみで分割した場合との差がそこまで見られないため、句点が含まれないケースでどのように分割されるかテストしてみます。
を意図的に除去した場合でも、下記のように「です」「ます」などを手がかりに分割できることがわかります。

$ pdftotext 202102b.pdf - | tr -d "" > 202102b_without_period.txt
$ python parse_pdf.py -model ja_ginza 202102b_without_period.txt
巻頭言ポスト
コロナ時代を形作る、コロナ禍で生まれるDX(デジタルトランスフォーメーション)株式会社オプティム代表取締役社長菅谷俊二現在、私が代表を勤めております
株式会社オプティムは、私が佐賀大学農学部在学時に創業した企業で、2015年には東証一部に上場しております
実は数多くの上場企業の中で唯一国立大学(佐賀大学)内に本店を置いた企業でもあります
そんな私達が夢中になって取り組んでいる事業とは「日本の多くの課題をDX(AI・IoT)によって解決すること」です
我々はこの取組を“〇〇×IT:(あらゆる産業)×(AI・IoT・Cloud・Robotics)”と呼んでいます
・
・
・

おわりに

今回紹介した方法は、下記のようにGiNZAのExampleレベルのコードを5行ほど追加すれば使えてお手軽です。
簡単なのでぜひともチャレンジしてみてください。

from bunkai import Bunkai
bunkai = Bunkai()
sents = bunkai('オプティムでは自然言語処理に限らずさまざまなエンジニアをお待ちしています興味があるかたはこちらまでhttps://www.optim.co.jp/recruit/')
for sent in sents:
    print(sent)

実行結果

$ python recruit.py
オプティムでは自然言語処理に限らずさまざまなエンジニアをお待ちしています
興味があるかたはこちらまで
https://www.optim.co.jp/recruit/

www.optim.co.jp