こんにちは!
Link-Uの町屋敷です。
今回はWikipediaの本文を収集する方法と特定のInfoboxを収集する方法を書いていきます。
Wikipediaから文章を取ってくる
Wikipediaの文章を取ってくる方法は主に以下の2つです。
APIを使う方法のほうが簡単ですが、Wikipediaはクロールが禁止されているので、データセットの作成には方法2を使わざるを得ません。チャットボットとかなら1で良いでしょう。
Wikipedia Extractorを使う
今回はWikipediaExtracctorを使って本文を取得します。
まず、最新のMediaWikiが提供しているXML形式のダンプファイルの中から、
jawiki-latest-pages-articles.xml.bz2
をダウンロード
次に適当なフォルダに移動して
git clone git@github.com:attardi/wikiextractor.git cd wikiextractor python python setup.py install
でwikiextractorをインストール出来ます。
自分はminicondaの仮想環境に入ってからやりました。
早速本文を抽出してみましょう。
WikiExtractor.py --json --keep_tables -s --lists -o ../whole_data/ ~/Downloads/jawiki-latest-pages-articles.xml コマンド説明 --json : 出力をjsonにする --keep_tables : 文章内の表を出力するようにする -s : セクション情報も出力するようにする --list : 文章内のリストを出力するようにする -o : 出力ファルダ名
最後のパスがさっきダウンロードしたxmlの入力ファイルです。
このフォイルはダウンロード時bz2という謎の形式で圧縮されています。
wikiextractorで使う分には圧縮されたままで良いのですが、後で中身を見る必要が出てくるので先に解凍しちゃいましょう。linuxなら
bzip2 -d jawiki-latest-pages-articles.xml.bz2
で解凍できます。
Windowsの場合はLhaplusを使うと良いでしょう。(なぜか7zipだと失敗した)
少し話がそれましたが、先程のスクリプトを実行するとわらわらとファイルが生成されて、
中を見ると文章が取得できているのでめでたしめでたし。
と、思いきやよく見るInfoboxが取得できていない。
Infoboxとはなんぞやと言う事ですが、要はWikipedia見てると右側にわりといる情報が書かれた表です。

これ機械学習だと正解ラベルになるようなかなり重要なこと書かれてるのになんで無いの?
最初–keep_tablesのオプション使っていなくて絶対コレじゃんと思ってやってみたが増えたのは文章の途中に出てくる表だけだった。残念。
XMLから特定のInfoboxの情報を取得する
Infoboxの情報がどうしてもほしいのでネットで色々検索したが、出てくるのはAPIを使ったものばかり。
WikipediaのXMLをJSONに変換するツールなども試してみたがうまく動かず、
そんなこんなで1日くらいあがいても良いライブラリは見つからなかった。
こうなったら自分で作るしか無い。
というわけで、XMLから特定のInfoboxの情報を取得してjsonで保存するスクリプトをPython3で作成した。
通常の文章はさっきほど抽出したものがあるので、必要なInfoboxが存在する項目だけタイトルとタイトルIDとセットで保存して、使うときに結合させるようにする。
どうせやるなら一緒に本文も保存したら良いじゃないかってなりそうだが、xml本文中にはいらないものが大量に含まれていて、消す作業が面倒だったので保留。
幸いあがいてる途中に参考になるサイトを見つけたので、大体はこれに沿ってやります。
プログラム全文はページの一番下。特に特別なパッケージは必要ない
大事なところだけ解説すると、
PythonでXMLからタグを取ってくる方法は、普通は
import xml.etree.ElementTree as ET tree = ET.parse('country_data.xml') root = tree.getroot()
こんな感じだが、今回はxmlファイルが馬鹿でかいのでこれを行うとメモリが死ぬ可能性がある。
なので、イテレータを用いて処理する。
for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')):
処理は上のタグから一つずつ進行していく、例えば
<a> <b>0</b> <c> <d>70334050</d> </c> </a>
このようなタグが来た時は、elem.tagにはa->b->b->c->d->d->c->aの順番でデータが入り、
開きタグなのか閉じタグなのかは,eventを見て判断する。
elem.textにそのタグに囲まれた部分の文章がひるようだ、
ここで、実際のxmlがどんな形式ななっているか確認しましょう。
less ~/Downloads/jawiki-latest-pages-articles.xml
巨大なファイルなので通常のテキストエディタでは開きません。

どうやら<page>~</page>でwikipediaの各ページが表現されていて、
<title>にタイトル
<id>にページ番号
<text>にメインの文章が書かれているようだ。
ただ、Infoboxは少々面倒で、特定のタグに囲われているわけでもなく、<text></text>の中に
{{東京都の特別区
|画像 = [[File:Sensoji 2012.JPG|200px]]
|画像の説明 = [[浅草寺]]境内
|区旗 = [[File:Flag of Taito, Tokyo.svg|100px]]
|区旗の説明 = 台東[[市町村旗|区旗]]<div style="font-size:smaller">[[1965年]][[6月4日]]制定
|区章 = [[File:東京都台東区区章.svg|75px]]
|区章の説明 = 台東[[市町村章|区章]]<div style="font-size:smaller">[[1951年]][[4月18日]]制定<ref>「東京都台東区紋章制定について」昭和26年4月18日台東区告示第47号</ref>
|自治体名 = 台東区
|コード = 13106-7
|隣接自治体 = [[千代田区]]、[[中央区 (東京都)|中央区]]、[[文京区]]、[[墨田区]]、[[荒川区]]
|木 = [[サクラ]]
|花 = [[アサガオ]]
|郵便番号 = 110-8615
|所在地 = 台東区[[東上野]]四丁目5番6号<br /><small>{{ウィキ座標度分秒|35|42|45.4|N|139|46|47.9|E|region:JP-13_type:adm3rd|display=inline,title}}</small><br />[[File:Taito Ward Office.JPG|250px|台東区役所庁舎(東上野四丁目)]]
|外部リンク = [http://www.city.taito.lg.jp/ 台東区]
|位置画像 = {{基礎自治体位置図|13|106}}
|特記事項 =}}
突然現れるだけで文章からの抽出が必要。
どうやら<text>中で{{}}で表されているものは複数あるらしく、単純に{{}}に囲われた部分を抽出してもうまく行かない。
そこでInfoboxの名前を使う。名前は{{のすぐ後に書いているやつで、lessでそのInfoboxが使われている項目を検索して直接確認するか、Wikipediaの基本情報テンプレートに書いてあるTemplate:Infobox ~ の項目を確認する。
elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text) infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL)
ここがその抽出部分で、INFOBOX_SEARCH_WORDにInfoboxの名前を入れるとその名前に対応するInfoboxを抽出できる。ただし、{{}}が入れ子になっているとバグるので、re.subで事前にいらないものを消している。
reの代わりにregexを使うと入れ子でも処理できるようだが、今回選んだInfoboxではこれで大丈夫だったので保留。
試しに上のWikipedia画面のキャプチャで囲んでいた漫画のInfobox(Infobox名 animanga/Manga)で試してみた結果がこちら。

ちゃんと取得できていることが確認できた。
参考サイト
https://www.heatonresearch.com/2017/03/03/python-basic-wikipedia-parsing.html
プログラム全文
import xml.etree.ElementTree as etree import time import os import json import re import collections as cl PATH_WIKI_XML = '/home/machiyahiki/Downloads/' FILENAME_WIKI = 'jawiki-latest-pages-articles.xml' JSON_SAVE_DIR = '.' INFOBOX_SEARCH_WORD = 'Infobox animanga/Manga' ENCODING = "utf-8" def strip_tag_name(t): idx = t.rfind("}") if idx != -1: t = t[idx + 1:] return t pathWikiXML = os.path.join(PATH_WIKI_XML, FILENAME_WIKI) totalCount = 0 articleCount = 0 redirectCount = 0 templateCount = 0 title = None start_time = time.time() dict_array = [] with open('{0}//wiki_infobox_{1}.json'.format(JSON_SAVE_DIR, re.sub('[ |//]', '_', INFOBOX_SEARCH_WORD)),'w', encoding='utf_8') as jf: for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')): tname = strip_tag_name(elem.tag) if event == 'start': if tname == 'page': inrevision = False findinfobox = False data_dict = cl.OrderedDict() elif tname == 'revision': # Do not pick up on revision id's inrevision = True if tname == 'title': data_dict['title'] = elem.text elif tname == 'id' and not inrevision: data_dict['id'] = elem.text else: if tname == 'text': if elem.text: elem.text = re.sub('{{[U|u]nicode.*?}}', '', elem.text) elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text) infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL) if infobox: findinfobox = True data_dict['infobox'] = infobox if tname == 'page': if findinfobox: if data_dict['title']: #タイトル名に{{Unicode}}があるとnullが入る、後で修可 if 'プロジェクト:' in data_dict['title']: #プロジェクト:から始まる項目は無視 continue if 'Template:Infobox' in data_dict['title']: #Template:Infoboxから始まる項目も無視 continue dict_array.append(data_dict) if len(dict_array) > 1 and (len(dict_array) % 10000) == 0: print("{:,}".format(len(dict_array))) elem.clear() json.dump(dict_array, jf, ensure_ascii=False) elapsed_time = time.time() - start_time print("Total pages: {:,}".format(len(dict_array))) print("Elapsed Time: {:,}".format(elapsed_time))