続きです。
どのようにデータを収集していくかを解説します。
ちなみに、分析の手順は「手作業の工程が多少発生してもよいのでわかりやすさ優先」の方針をとっています。ルーティンで回すものを作るわけではないので、もっと自動化できる箇所でも、そこまで負担にならなさそうなら、ひとまずGUI上で行ってもよいというスタンスです。
データのありか
福岡市議会の議事録サイトです
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/
収集方法を選ぶ
サイトを見ればわかる通り、議事録はフレームの中にテキストで表示されます。ページのソースを確認すると、HTMLの中にべたっと記述されている状態です。分析のためには議事録データをテキストファイルやCSVファイルで得たいので、このままでは扱えません。
</div>
<!-- -->
<div class="page-text__voice" id="VoiceNo1">
<p class="page-text__text textwrap">
<!-- -->
午前10時 開議<br />
◯議長(おばた久弥) これより本日の会議を開きます。<br />
日程第1、一般質問を行います。発言通告者のうちから順次質問を許します。阿部真之助議員。<br />
<br />
</p>
</div>
<!-- -->
</div>
数が少なければ1ページずつコピペできるかもしれませんが、さすがに約20年分をそうするのは厳しそうです。よって、今回はPythonでクローラーを作成して自動でテキストを収集させます。
###クローラー作成時の注意
クローラーは相手サーバに負荷をかけます。もしAPIで済むなら、必ずそうすべきです(国会の議事録はAPIが使えるようです)。やむなくクローリングする際は、下記のようなことに注意しましょう。
https://vaaaaaanquish.hatenablog.com/entry/2017/12/01/064227
クローラーの作成
収集すべきデータを再確認する
条件として、
・1997年2月~2016年9月
・定例会および臨時会の議事録+委員会の議事録
※発言者の区別がつく形式
※会議体の区別がつく形式
があります。
今回はサンプルなので、2015年の定例会の議事録のみ収集します。
(他の年度、会議体についても、基本的な方法は同じです)
サイトの構造を確かめる
各会議の議事録は、デフォルトだと発言別に表示されるようになっており、左側で発言者を選ぶと、表示される発言内容が変わるようになっています。
なんだか複雑に見えますが、案外構造は単純で、このページ(便宜的にメインページと呼びます)の裏側には、それぞれURLを持った各発言ページが存在しています。メインページには、その各発言ページの内容を表示しています。つまり、メインページの裏側には、こんな感じの独立した各発言ページがたくさん存在しています。
したがって、データの収集は、必要な各発言ページに順番にアクセスして取得していけばよいとわかります。
URLの規則性を確かめる
各発言ページのデータを取得していくにあたって、URLに規則性があるか確認します。規則性があれば取得が容易だからです。
2015年(平成27年) 第1回定例会(第1日)本文のメインページ
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-one-frame&VoiceType=onehit&DocumentID=1444
上記のページの各発言ページ(いくつか抜粋)
◯議長(森 英鷹)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56344
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56345
◯7番(伊藤嘉人)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56348
◯市長(高島宗一郎)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56353
2015年(平成27年) 第1回定例会(第2日)本文のメインページ
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-one-frame&VoiceType=onehit&DocumentID=1447
上記のページの各発言ページ(いくつか抜粋)
◯議長(森 英鷹)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56418
◯4番(飯盛利康)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56419
2015年(平成27年) 第5回定例会(第1日)本文のメインページ
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-one-frame&VoiceType=onehit&DocumentID=1492
上記のページの各発言ページ(いくつか抜粋)
◯議長(おばた久弥)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=60615
◯8番(打越基安)
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=60619
2017年(平成28年) 第1回定例会(第1日)本文のメインページ
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-one-frame&VoiceType=onehit&DocumentID=1507
なんとなく、以下のような規則性が見えてきます。
・index.phpの後ろの7桁の数字は、年が変わっても同じ(おそらく検索方法などで動的に変わる部分。今回は深く触れません)
・メインページは一意のDocumentIDを持っている様子
・各発言ページは一意のVoiceIDを持っている様子
・各発言ページのURLにDocumentsIDは含まれない。
収集したいページのVoiceIDがわかれば、必要なURLをつくれそうです。
VoiceIDについても見てみます。
第1回
1日目 最初56344 最後 56415
2日目 最初56418 最後 56443
3日目 最初56446 最後 56449
4日目 最初56452 最後 56477
5日目 最初56479 最後 56530
6日目 最初56533 最後 56536
7日目 最初56538 最後 56618
第2回(臨時会)
1日目 最初88141 ←臨時会の場合はVoiceIDの規則が異なる
第3回
1日目 最初56622 ←第1回の最後+4
第5回
5日目 最初63010 最後63086 ←ここがこの年の最後
ここにもある程度の規則性がありそうです。
・1つの開催回・開催日でのVoiceIDは連番。例えば第1回定例会(第1日)では56344〜56415の値をとる
・2015年最後の定例会の最終日のVoiceIDは63086
・定例会と臨時会はVoideIDの持ち方が違う。なので、定例会と定例会の間に臨時会を挟んでも定例会のVoiceIDは連番になる
・開催回や開催日をまたぐとVoiceIDは連番でなくなる。例えば、第1回定例会(第2日)は56418から始まる。
・またいだ際の間隔は+2〜4くらいで、一定ではない(もしかすると何か規則性があるかも)
・またいだ際の間隔にあるVoiceIDは空(VoiceIDをURLにはめ込んでアクセスしても「発言が指定されていません」というページが出るだけ)
ここまでをまとめます。データの収集は、2015年の定例会を対象とする場合VoiceIDが56344~63086のURLについて行えばよいとわかりました。つまり、以下のようなURLの末尾の数字を56344~63086に変えて自動取得していけばOKです。
http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/5408574?Template=doc-page&VoiceID=56418
※中間部分の5408574のような数字はコロコロ変わりますが、今回は気にしないで大丈夫です
また、開催回や開催日の間隔にあたるVoiceIDのページについては、最初から除外してクローリングしてもよいのですが、今回はそこまで収集するデータが多くないので、わかりやすさ重視でいったん集めてしまってから取り除くことにしようと思います。
必要な技術を特定する
今回のように、URLにわかりやすく静的な規則性がある場合、クローリングは比較的簡単に行なえます。とっつきやすさとweb上の資料の多さからPythonのBeautiful soupを使用することにします。
https://www.crummy.com/software/BeautifulSoup/bs4/doc/
データを貯める形式を決める
報道内容を見ると、発言者や会議体といった軸でも分析しているようです。各発言ページからテキストを取得する際は、発言者と会議体をひもづけて取得するようにしないといけません(ただし今回は定例会のみなので省いて、発言者のみにします)
コードを書く
さて、次から実際にPythonのコードを書いていきます。Pythonの実行環境は既にあるものとして進めていきます。
コード
実行環境
実行環境がない方は、Googleで検索すると大量に出てくるので参考にして準備してください。Windowsの場合はAnacondaとか使うのが楽だと思います。
Beautiful soupの利用
例えばこのあたりが参考になります。
python3でwebスクレイピング(Beautiful Soup):https://qiita.com/matsu0228/items/edf7dbba9b0b0246ef8f
コード例その1
まず、単純に1つのページから発言者名と発言内容のテキストを取得してみます。
#coding: UTF-8
import requests
from bs4 import BeautifulSoup
def scraping(baseurl, startid):
#urlをつくる
url = baseurl + startid
#urlをもとにhtmlを取得する
html = requests.get(url)
#BueatifulSoupセット
bs = BeautifulSoup(html.text, "html.parser")
#テキストの取得
shimei = bs.h1 #発言者名のテキストを取得
shimei_notag = shimei.text #発言者名テキストからhtmlのタグをとる
voice = bs.find(class_="page-text__text textwrap") #発言内容のテキストを取得
voice_notag = voice.text #発言内容テキストからhtmlのタグをとる
print(shimei_notag)
print(voice_notag)
if __name__ == "__main__":
base = "http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/9562620?Template=doc-page&VoiceID="
start= "56344" #VoiceIDの56344
scraping(base, start)
#実行結果
#1 : ◯議長(森 英鷹)
#◯議長(森 英鷹) ただいまから平成27年第1回福岡市議会定例会を開会いたします。
# これより本日の会議を開きます。
# 会議録署名議員に打越基安議員、中山郁美議員を指名いたします。
# 日程に入るに先立ち、この際、報告いたします。まず、市長から別紙報告書類一覧表に記載の書類が提出されましたので、その写しを去る2月12日お手元に送付いたしておきました。
# 次に、監査委員から監査報告第2号及び第3号が提出されましたので、その写しをお手元に送付いたしておきました。
# 次に、地方自治法第100条第13項及び会議規則第125条第2項の規定により、お手元に配付いたしております議員派遣報告一覧表のとおり議長において議員の派遣を決定いたしておきました。
# 以上で報告を終わります。
# これより日程に入ります。
# 日程第1、会期決定の件を議題といたします。
# お諮りいたします。
# 今期定例会の会期は、本日から3月16日までの26日間といたしたいと思います。これに御異議ありませんか。
# 〔「異議なし」と呼ぶ者あり〕
うまくいきそうです。必要なすべてのページに対して同じ処理をかけられるよう、コードを書き加えます。
追記:後になって本文の文頭にも発言者名があることに気づきましたが、あまり分析結果に影響しなさそうなのと、後で前処理のときに取り除くこともできるので、いったん気にしないでおきます。
#coding: UTF-8
import requests
import time
from bs4 import BeautifulSoup
import pandas as pd
def scraping(baseurl, startid):
#urlをつくる
url = baseurl + startid
#urlをもとにhtmlを取得する
html = requests.get(url)
#BueatifulSoupセット
bs = BeautifulSoup(html.text, "html.parser")
#テキストの取得
speaker = bs.h1 #発言者名のテキストを取得
speaker_notag = speaker.text #発言者名テキストからhtmlのタグをとる
voice = bs.find(class_="page-text__text textwrap") #発言内容のテキストを取得
voice_notag = voice.text #発言内容テキストからhtmlのタグをとる
time.sleep(5) #サーバー負荷軽減のため間隔をあける
return speaker_notag, voice_notag #取得したテキストを戻り値にする
if __name__ == "__main__":
df = pd.DataFrame({'speaker':[], 'voice':[]}) #取得したテキストの入れ物を作る
base = "http://www.city.fukuoka.fukuoka.dbsr.jp/index.php/9562620?Template=doc-page&VoiceID=" #VoiceID抜きのURL
#VoiceIDを56344~63086までループさせてテキストを取得する
for i in range(56344, 63086):
start = str(i) #文字列に変換
result = scraping(base, start) #関数呼び出し
speaker = result[0] #戻り値のうち発言者名を代入
voice = result[1] #戻り値のうち発言内容を代入
series = pd.Series([speaker, voice],['speaker', 'voice'])
print(series) #進捗確認用として画面表示させる
df = df.append(series, ignore_index=True) #入れ物に入れる
df.to_csv('gijiroku.csv',encoding='utf_8_sig') #csvファイルに書き出す(Excelで文字化けしないよう文字コード指定)
print('完了')
ループさせて、取得したテキストをcsv形式で書き出すようにしました。本当はエラー処理とかしたほうがいい、というかすべきなんですが、面倒なので量が少ないので省きました。
実行結果
このまま分析するにはノイズが多いので、前処理で取り除いていきます。前処理については別の記事で扱います。
(続く...続きは暇なとき書きます)