財務省の公開した交渉記録PDFをいじる その2(本文データのOCR etc.)


スポンサーリンク

過去記事の続き。やはり実際のデータでデータ処理をやるのは勉強になります。

……お金になるかは別にして、Pythonという言語の習熟度は向上しているはず。

a244.hateblo.jp

方針

過去記事の方針を踏襲。

  1. 目次のPDFから交渉記録(応接記録)を機械可読(Computer Readable)な形式に変換
  2. マスク無しのPDFを画像化、再度PDFに変換して過去記事で紹介したAPIOCR
  3. 目次のページ番号から必要なページを割り出して、OCR結果を分割、どうにして添付資料のページを除去
  4. どうにかしてMarkdown
  5. 静的ページジェネレーターでWebページ化

この記事の対象は上記の「2. 」と「3. 」です。

OCR処理

といってもデータさえ用意すれば過去記事のスクリプトを実行するだけです。

画像の抽出

せっかくなので黒塗りなしの方で行ってみましょう。

a244.hateblo.jp

一部のページで画像が複数含まれているので注意する。

  • 20180523p-2.pdf: p.68, p.69, p.244
  • 20180523p-4.pdf: p.82

PDFファイルがorigin_pdfというディレクトリ配下にあるとしていう前提。出力先のディレクトリはunmaskable_pdf/images_1/outupt_{1,2,3,4}

$ mkdir images_{1,2,3,4}
$ pdfimages -p -png  origin_pdf/20180523p-1.pdf unmaskable_pdf/images_1/outupt_1
$ pdfimages -p -png  origin_pdf/20180523p-2.pdf unmaskable_pdf/images_2/outupt_2
$ pdfimages -p -png  origin_pdf/20180523p-3.pdf unmaskable_pdf/images_3/outupt_3
$ pdfimages -p -png  origin_pdf/20180523p-4.pdf unmaskable_pdf/images_4/outupt_4

実際はシェルスクリプトのようにforでループ。

#! /usr/local/bin/bash

for i in 1 2 3 4 ; do
    echo $i
    pdfimages -p -png  origin_pdf/20180523p-$i.pdf unmaskable_pdf/images_$i/outupt_$i
done

画像をPDFに変換・結合

convertで各ページをPDFにしてmutoolで結合。事前に上記のダブっているファイルを退避しておくこと。

#! /usr/local/bin/bash

for i in 1 2 3 4 ; do
    TEMP_DIR=unmaskable_pdf/temp_${i}
    OUTPUT_PDF=output_${i}.pdf

    for j in `ls -d unmaskable_pdf/images_$i/*.png  | sort -V`  ; do
    #    echo $j
    filename=$(basename $j .png)
    echo ${TEMP_DIR}/$filename.pdf
    convert $j -negate -quality 100 -units PixelsPerInch -density 72x72  ${TEMP_DIR}/$filename.pdf

    done

    echo merge to ${OUTPUT_PDF}
    ls -d ${TEMP_DIR}/*.pdf | sort -V | tr '\n' '\0' | xargs -0 -J% mutool merge -o ${OUTPUT_PDF} %
done

mogrifyコマンドを使うべきだったかな。

参考:大量の印刷用画像をウェブ用に変換する方法 - クックパッド開発者ブログ

一括OCR

過去記事参照。

a244.hateblo.jp

もう一度mutoolでPDFを結合。

$ mutool merge -o gen_pdf/fullset.pdf gen_pdf/output_1.pdf gen_pdf/output_2.pdf gen_pdf/output_3.pdf gen_pdf/output_4.pdf

生成したPDFのページ数をチェックしてGoogle Cloud Storage のバケット*1にアップロードしてOCR

$ python3 ocr_and_wait.py --gcs-source-uri gs://<bucket-name>/fullset.pdf --gcs-destination-uri gs://<bucket-name>/output_unmasked/

<bucket-name>の部分は適宜修正。

JSON分割

バッチサイズの数字を"5"にしているので下記のスクリプトで扱いやすいようにJSONを分割する。

#! /usr/bin/env python3
# encoding: utf-8
# busrst:py


from google.cloud import vision_v1p2beta1 as vision
from google.protobuf import json_format
from google.protobuf.json_format import MessageToJson

import json
import pathlib
from natsort import natsorted



def burst_json(input_path, output_path="./output"):

    input_pl = pathlib.Path(input_path)

    filelist = list(input_pl.glob('*.json'))
    for file_path in filelist:
        print(file_path)

        with file_path.open(mode='rt',encoding='utf-8') as jfp:
            str = jfp.read()

            response = json_format.Parse(str, vision.types.AnnotateFileResponse())

            for i, res in enumerate(response.responses) :
                page_num = res.context.page_number

                print("{0} {1} {2}".format(file_path.name, i,page_num ) )

                filename = "output_json_{0}.json".format(page_num)

                output_pl = pathlib.Path(output_path, filename)

                with output_pl.open(mode='w',encoding='utf-8') as output:

                    serialized = MessageToJson(res)

                    temp = json.loads(serialized)
                    json.dump(temp, output, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))

if __name__ == '__main__' :

    input_path = "./output_unmasked/"
    output_path = "./ocr_unmasked/" 

    burst_json(input_path, output_path)

入力となるJSONのあるパスと出力先のパスがハードコードなのは面倒だから*2

タブ区切りテキストののパース

前回作成したTSVと、上記のOCR結果のJSONデータを用いて、交渉記録のエントリ番号(通し番号)に対応するページの範囲を割り出します。

財務省の公開した国有地の取引に関する交渉記録の目次PDFから抜き出した日付とページ番号の対応表(2018年5月) · GitHub

OCR結果がいまいちだったり、そもそも書式が微妙という問題の関係でスマートには行きません。とりあえずスクリプトは以下のように。

  1. 前回作成したタブ区切りテキストをcsvモジュールで開く
  2. リストとしてデータを読み込む(1行スキップ)
  3. 読み込んだデータを対象にforループを実行(217日分)
  4. それぞれの行についてページ番号を取得し、そのページ番号から最終ページまで繰り返す
  5. 対応するページ番号のファイルを開いて「以上」という文字列*3を探す
  6. 見つかったらそのページを終了ページとみなす
  7. 見つからなければ次。もし次のページ番号が「2. 」で読み込んだリストにあればそこでループ終了
  8. すべてのエントリについて繰り返す(「3. 」に戻る)

細かい条件判定はコードを参照。

#! /usr/bin/env python3
import csv
import pathlib
import json

import re
import collections
import sys

tsvfilename = "negotiation-history.tsv"
tfp = open(tsvfilename)

reader = csv.reader(tfp, delimiter="\t")
next(reader) # 見出し行をスキップ

start_page_index = [row for row in reader ]

black_list = { 191: 198, 318: 320, 850: 852, 945: 945}


# 分割済みのJSONファイルの格納パス
pl = pathlib.Path('./ocr_unmasked')

# Python 3.6系以降では実装仕様として辞書のキーの順序が保存されるので通常の辞書でも良いが、一応
page_range_list = collections.OrderedDict() 

p_finish_word = re.compile('以\s*上\s+') # スペースの有無に関わらずマッチする

# 最後のエントリの開始ページ番号
last_page_index = int(start_page_index[-1][1])

for node in start_page_index :
    date_st = node[0]
    idx = node[1]
    for i in range(int(idx),957+1):
    
        json_filename = "output_json_{}.json".format(i)
        
        path = pl / json_filename
        
        with path.open(mode='rt',encoding='utf-8') as fp:
            json_data = json.load(fp)
        
            text = json_data['fullTextAnnotation']['text']
                       
            if p_finish_word.search(text) :
                print("{0}: start {1}, end of article: {2}".format(path, idx, i), file=sys.stderr)
                
                if not str(idx) in page_range_list:
                    page_range_list[str(idx)] =  [idx, i , date_st]
                else:
                     page_range_list[str(idx) + "_1"] =  [idx, i , date_st]
                
                break

            if i + 1 in start_page_index or i == last_page_index :
                
                if idx in black_list :
                    
                    if not str(idx) in page_range_list:
                        page_range_list[str(idx)] = [idx, black_list[idx] , date_st]
                    else:
                         page_range_list[str(idx) + "_1"] = [idx, black_list[idx] , date_st]
                
                    
                    print("{0}: start {1}, End marker not found!, but in black list...{2}".format(path, idx, black_list[idx]), file=sys.stderr)
                else:
                    if not str(idx) in page_range_list:
                        page_range_list[str(idx)] =  [idx, i , date_st]
                    else:
                        page_range_list[str(idx) + "_1"] =  [idx, i , date_st]
                
                    print("{0}: start {1}, End marker not found!, so use current page number...{2}".format(path, idx,i), file=sys.stderr)

                break
            
#print(page_range_list)


output_tsv = pathlib.Path("./entries_page_range.tsv")

with output_tsv.open('wt', encoding='utf-8') as ot :
    tsv_writer = csv.writer(ot, delimiter="\t")

    for entry_id,key in enumerate(page_range_list, 1) :
        #print(page_range_list[key])
        start_num = page_range_list[key][0]
        end_num = page_range_list[key][1]
        entry_date = page_range_list[key][2]

        record = [ "{0:03}".format(entry_id), start_num,end_num, entry_date]
        tsv_writer.writerow(record)

成果物

OCR結果のJSONは数が多いのでともかくとして、最終的なページ番号対応表(見出し行つき)。

財務省の公開した国有地の取引に関する交渉記録の目次PDFから抜き出したエントリ番号とページ範囲、日付の対応表(2018年5月) · GitHub

反省点

  • バッチサイズを1にしておけば余計な手間がいらなかった
  • めんどうだけどコマンドライン引数でファイルのパスを指定するように(コードスニペットを用意しておくとか)
  • 遊んでないで他のことをやるべきでは?

続きます。

頑張ってJSONからMarkdownかHTMLにしていきます。

実践 Python 3

実践 Python 3

*1:適当に作成

*2:怠惰ですいません

*3:空白を含む場合なども考慮する

広告