#前置き
こんにちは、私は最近コロナのおかげで学校も休みで、延々と暇な日々を送っており、そのため日々いろいろなテクノロジーで遊ぶことで暇を潰しています。
正直言って、すっごい楽しいです。
さて、そんな日々の中で今日も今日とてDiscord botを作ったり2048を実装したり難解言語で遊んだり形態素解析で遊んだりしていると、あるものを見つけました。
そう、現在Googleドライブでねとらぼ編集部によって絶賛公開中の香川県ネット・ゲーム依存症対策条例のパブリックコメントです。
これを見つけたとき、私は思ったわけです。
これで遊ぶの楽しそうだなって。
PDFな上にスキャナーで読んだ奴をデータ化してるからそのままではデータとして扱えず、そのためテキストデータに変換する必要があるわけですが、そもそもそのテキストデータにするまでの工程がすでに楽しそうです。私はこの辺りの画像処理なんかのテクノロジーはまだ触っていないので、新しい知見も広げられそうです。しかも聞いた話では、データとしてもかなり不自然な偏りが散見されるらしいじゃないですか。これを解析するの絶対楽しいですよ。
そういうわけで、遊ぶことにしました。
#環境とか
- Windows10
- Python 3.8.1
- pdf2image 1.12.1
- Pillow 7.1.1
- Poppler 0.68.0(Poppler for Windowsとpoppler-data-0.4.9使用)
- tesseract 5.0.0-alpha.20200328
- PyOCR 0.7.2
- pathlib 1.0.1
- re 3.4.1
- Matplotlib 3.2.1
#とりあえず画像に
まずはpdf2imageを使ってPDFを画像化します。ぶっちゃけほとんどこの記事に書いてあったコードのパクリです。すみません、これ以上にいいコードが書ける気がしなくてですね…
import pathlib
import pdf2image
pdf_files = pathlib.Path('PDF').glob('*.pdf')
for pdf_file in pdf_files:
base = pdf_file.stem
img_dir = pathlib.Path(f'画像/{base}')
img_dir.mkdir()
images = pdf2image.convert_from_path(pdf_file, grayscale=True, dpi=200)
for index, image in enumerate(images):
image.save(img_dir/pathlib.Path(f'{index + 1}.png'), 'png')
print(base) # 進捗確認用
実行に少し時間がかかるので気長に待ちます。
待つとこうなります。
いやぁ、こうして並べてみると、今自分の手元にはパブコメがあるんだなって自覚がふつふつと湧いてきますね。
#画像から文字列に
Tessertact_OCRを使います。いい感じに認識しないかなぁ…という期待を込めてパソコンに拝みます。なるべく深く頭を下げるのが重要です。お供え物とかもあるといいですね。
拝みが通じた気がしたら、試しに1月23日の賛成のうち14件目(適当に決めました)を認識させてみます。
C:\Users\usr\Documents\香川>tesseract .\画像\賛成0123\14.png .\文字認識\test -l jpn
Tesseract Open Source OCR Engine v5.0.0-alpha.20200328 with Leptonica
Warning: Invalid resolution 0 dpi. Using 70 instead.
Estimating resolution as 344
Detected 201 diacritics
なんか色々問題が起きてるようにも見えますがたぶん気のせいです、気のせい
では入力と出力を比較してみましょう。
入力した画像がこちら
出力されたテキストがこちらです。
desknefs NEO - 171 ページ
議会事務局 (glkeldprefr kagawa lg jp)
ーーーーー 『
―――(空白行が続くので省略)―――
出人 : "和川県 ご意見・お問い合わせページ"<hp- adm@pref. kagawa.Idg.]p> ー
宛先 : gikaiGpref.kagawa.Ig.jp
CC : ー 」
件名 : ご凍見・お問い合わせページからの投稿
白時 : 2020年01月23日(本) 15:16
―――(空白行が続くので省略)―――
【ご意見・ お問い合わはの内容】
【県議会ホームページへのご 意見箱】 - 」
県議会では、今後も議会の状況をわかりやすくお知らせしたいと思ってい
ます。 議会ホームページをご覧にかったご意見・ご感想をお聞かせくださ
い。いただきましたご意見等は、皆様方からの貴重な声として、参考にさせ
ていただきます。
をご注意$ ーー
@メールによる陳情や議員個人に関するメールはお受けできません。
【(住 所】 ーー
【E-Maill . ,
【件 名】 パブリック・コメントへの意見
【ご意見・ご感想】
年齢 軸 電話番号
ネット・ゲーム依存症対策条に賛同します。
どこへ行っても ゲームやスマホをしている子供がいて 心配こなりま
[ADDR]192. 168.7. 21
[DATE]2020/01/23 15:16: 42
[USERAGENT]Mozilla/5.0 (Windows NT 10.0: Win64: x64) AppleWeb
Kit/537.36 (KHTML, like Gecko) Chrome/70.0.3538. 102 faP/53か 3
6 TOg9. 18362
う~~~~~~~ん
非常に不安定なところはありますが、今回主に弄る予定の日付は問題なく取得できてるしとりあえずは良しとします
##一斉取得
PyOCRのターンです。
/^([^0-9\n]*\d){12}[^0-9\n]*$/
の正規表現(「ちょうど」数字が12個含まれる行)に合致する文字列を抜き出します。数字の認識はかなり正確にできているようなので、そうそう取りこぼしは起こらないでしょう。
取得した日付は、「賛成」「反対」「事業者」「提言」の4つのテキストファイルに収めます。
この記事をベースに書いたこのコードを、何か神秘的な力でもって奇跡が起きてパソコンのスペックが4倍になることを信じて実行します。
from PIL import Image
import sys
import pyocr
import pyocr.builders
from pathlib import Path
import re
count = 0
tool = pyocr.get_available_tools()[0]
folders = list(Path("画像").glob("*")) #画像フォルダのパスをすべて取得
agr, opp, bsp, rec = open("賛成.txt", "w"), open("反対.txt", "w"), open("事業者.txt", "w"), open("提言.txt", "w") # テキストファイルを一度初期化
agr,opp,bsp,rec.close()
dic = {"賛": "賛成.txt", "反": "反対.txt", "事": "事業者.txt", "提": "提言.txt"} # Switch文的な物を書くための辞書
for fol in folders:
with open(dic[str(fol)[3]],"a") as fil: #フォルダパスの「4文字目」で開くファイルを判定
for path in (Path(fol).glob("*")):
count += 1
text = tool.image_to_string(
Image.open(path),
lang="jpn",
builder=pyocr.builders.TextBuilder(tesseract_layout=6)
)
match = re.search(r'^([^0-9\n]*\d){12}[^0-9\n]*$', text, re.MULTILINE)
if match != None: # 数ページにわたる文書とかではページ内のどこにも日付がないことがあるので
match = match.group()
fil.write(match + "\n")
print(count) #進捗確認用
ちなみに私に奇跡は起こりませんでした、実行時間が長すぎる
たぶんこれもうちょっと早く終わるやり方があると思います
##取得結果
このプログラムを走らせた結果、例えば「賛成.txt」の中身はこんな感じになりました。
日時 : 2020年01月23日(木) 11:39 ーー
日時 : 2020年01月23日(木) 11:49 ーー
- 是時 : 2020年01月23日(本) 11:50 .
日時 : 2020年01月23日(木) 11:55 ーー ュー
日時 : 2020年01月23日(木) 13:49
日時 : 2020年01月23日(本) 15:16 ーー 。
. 日時 : 2020年01月23日(木) 15:31
日時 : 2020年01月23日(木) 15:51 . ーー
日時 : 2020年01月23日(木) 15:58 .
日時 : 2020年01月23日(木) 17:55 . - ーー
日時 : 2020年01月23日(木) 20:23 .
日時 : 2020年01月23日(木) 12:22
日時 : 2020年01月23日(木) 20:31 - 「・
日時 : 2020年01月23日(木) 13:10 ーー 。
日時 : 2020年01月23日(木) 16:27 ] 」
日時 : 2020年01月23日(木) 17:03
日時 : 2020年01月23日(木) 18:09 ] ーー
日時 : 2020年01月23日(木) 21:41
22812 050 還是呈 IO008 『 1 -
日時 : 2020年01月24日(金) 08:49 ーー
. 日時: 2020年01月24日(金) 12:40 .
日時 : 2020年01月24日(金) 13:28
日時 : 2020年01月24日(金) 13:31
日時 : 2020年01月24日(金) 13:34 -
日時 : 2020年01月24日(金) 13:35
.日時 : 2020年01月24日(金) 14:01 ] ー
. 日時 : 2020年01月24日(金) 15:08 ーー .
。 日時 : "2020年01月24日(金) 08:49 . ーー
日時 : 2020年01月24日(金) 15:33 ーー
日時 : 2020年01月24日(金) 15:34
日時 : 2020年01月24日(金) 15:37 ・
日時 : 2020年01月24日(金) 15:44 ・
日時 : 2020年01月24日(金) 16:03 」 - -
日時 : 2020年01月24日(金) 16:13 ーー
- 日時 : 2020年01月24日(金) 16:14
日時 : 2020年01月24日(金) 16:16 - 」 ーー
日時 : 2020年01月24日(金) 16:39 -
-. 曰時 : 2020年01月24日(金) 08:50 ーー
日時 : 2020年01月24日(金) 16:47 -
(以下略)
一部「日付ではないもの」が紛れ込んでいるようですが、おおむねうまくいっているようです。
ちなみに「日付ではないもの」は全体から見るとごく少数だったので手作業で取り除きました、一瞬でした。
##正規化
このままだとノイズが酷いので、データを正規化します。
簡単に、「日付内の数字すべてを結合したもの」で統一します。文字数は12文字固定なはずなので、これで正規化が可能なはずです。
import re
for name in ["賛成","反対","事業者","提言"]:
with open(name + ".txt") as fil:
contents = fil.read()
match = re.findall(r'([0-9]|\n)', contents, re.MULTILINE)
with open(name + "_正規化.txt","w") as fil:
fil.write("".join(match))
202001231139
202001231149
202001231150
202001231155
202001231349
202001231516
202001231531
202001231551
202001231558
202001231755
202001232023
202001231222
202001232031
202001231310
(以下略)
いい感じですね。
#散布図を描く
いよいよ散布図を描きます。
パブコメの募集期間は1/23~2/6(短くないですかこれ)なので、この期間中の賛成票の分布をとりあえずプロットしてみましょう。
teratileのこの質問のベストアンサーをパクります。
import matplotlib.pyplot as plt
from matplotlib import dates as mdates
from datetime import datetime as dt
date = []
time = []
x = []
y = []
with open("賛成_正規化.txt", "r") as fil:
for line in fil:
date.append(line[4:10])
time.append(line[10:12])
for d in date:
y.append(dt.strptime(d, "%m%d%H"))
for d in time:
x.append(dt.strptime(d, "%M"))
ax = plt.subplot()
ax.scatter(x, y, alpha=0.1,c='red',s=40)
ax.set_xlim([dt.strptime('00', '%M'),
dt.strptime('59', '%M')])
ax.set_ylim([dt.strptime('01/23', '%m/%d'), dt.strptime('02/06', '%m/%d')])
plt.xticks(rotation=90)
plt.savefig("グラフ.png")
出力されるグラフ1がこちら。
明らかに何かが起きている。
注釈でも言ってますがこのグラフ、縦線が「月日時」、横線が「分」で刻まれています。つまるところこの明らかに濃く見える2本の線は、「分」刻みでも連続して見えるほどに高速でパブコメが投稿されたことによるものと思われます。いやあ、面白いですね。
#最後に
すっごく楽しかったです。今日はもう眠いので私はやめますが、パブコメは今のところまだ公開されているので、皆さんも暇があったら遊んでみるといいんじゃないでしょうか。
#参考にしたもの色々
- https://qiita.com/kikuyan8540/items/35751c573de014df205b
- http://pdf-file.nnn2.com/?p=863
- https://qiita.com/henjiganai/items/7a5e871f652b32b41a18
- http://blog.machine-powers.net/2018/08/02/learning-tesseract-command-utility/#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB
- https://blog.14nigo.net/2018/03/tesseract-ocr.html
- https://qiita.com/nabechi6011/items/3a367ca94dbd208efcc7
- https://qiita.com/amowwee/items/e63b3610ea750f7dba1b
- https://narito.ninja/blog/detail/72/
- https://teratail.com/questions/143164
- https://qiita.com/Alice1017/items/4ce5be3f46aa34f9f900
-
もう眠いのでラベルの設定をしませんでしたが解説だけすると、x軸は「分」(0~59)を表しており、y軸は1時間刻みの「月日」(1/23/00~2/6/23)を表している感じです。 ↩