財務省の公開した交渉記録PDFをいじる その2(本文データのOCR etc.)
スポンサーリンク
過去記事の続き。やはり実際のデータでデータ処理をやるのは勉強になります。
……お金になるかは別にして、Pythonという言語の習熟度は向上しているはず。
方針
過去記事の方針を踏襲。
- 目次のPDFから交渉記録(応接記録)を機械可読(Computer Readable)な形式に変換
- マスク無しのPDFを画像化、再度PDFに変換して過去記事で紹介したAPIでOCR
- 目次のページ番号から必要なページを割り出して、OCR結果を分割、どうにして添付資料のページを除去
- どうにかしてMarkdown化
- 静的ページジェネレーターでWebページ化
この記事の対象は上記の「2. 」と「3. 」です。
OCR処理
といってもデータさえ用意すれば過去記事のスクリプトを実行するだけです。
画像の抽出
せっかくなので黒塗りなしの方で行ってみましょう。
一部のページで画像が複数含まれているので注意する。
- 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
過去記事参照。
もう一度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結果がいまいちだったり、そもそも書式が微妙という問題の関係でスマートには行きません。とりあえずスクリプトは以下のように。
- 前回作成したタブ区切りテキストを
csv
モジュールで開く - リストとしてデータを読み込む(1行スキップ)
- 読み込んだデータを対象に
for
ループを実行(217日分) - それぞれの行についてページ番号を取得し、そのページ番号から最終ページまで繰り返す
- 対応するページ番号のファイルを開いて「以上」という文字列*3を探す
- 見つかったらそのページを終了ページとみなす
- 見つからなければ次。もし次のページ番号が「2. 」で読み込んだリストにあればそこでループ終了
- すべてのエントリについて繰り返す(「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
反省点
続きます。
頑張ってJSONからMarkdownかHTMLにしていきます。

退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング
- 作者: Al Sweigart,相川愛三
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/06/03
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (5件) を見る

- 作者: Mark Summerfield,斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/12/01
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る