素人の言語処理100本ノック:29


[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の挑戦記録です。環境はUbuntu 16.04 LTS ＋ Python 3.5.2 \:\: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧は[こちら](http://qiita.com/segavvy/items/fb50ba8097d59475f760)からどうぞ。

## 第3章: 正規表現
>Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある．
>・1行に1記事の情報がJSON形式で格納される
>・各行には記事名が"title"キーに，記事本文が"text"キーの辞書オブジェクトに格納され，そのオブジェクトがJSON形式で書き出される
>・ファイル全体はgzipで圧縮される
>以下の処理を行うプログラムを作成せよ．

###29. 国旗画像のURLを取得する
>テンプレートの内容を利用し，国旗画像のURLを取得せよ．（ヒント: [MediaWiki API](https://www.mediawiki.org/wiki/API:Main_page/ja)の[imageinfo](https://www.mediawiki.org/wiki/API:Properties/ja#imageinfo_.2F_ii)を呼び出して，ファイル参照をURLに変換すればよい）

####出来上がったコード：

```python:main.py
# coding: utf-8
import gzip
import json
import re
import urllib.parse, urllib.request
fname = 'jawiki-country.json.gz'


def extract_UK():
	'''イギリスに関する記事本文を取得

	戻り値：
	イギリスの記事本文
	'''

	with gzip.open(fname, 'rt') as data_file:
		for line in data_file:
			data_json = json.loads(line)
			if data_json['title'] == 'イギリス':
				return data_json['text']

	raise ValueError('イギリスの記事が見つからない')


def remove_markup(target):
	'''マークアップの除去
	MediaWikiマークアップを可能な限り除去する

	引数：
	target -- 対象の文字列
	戻り値：
	マークアップを除去した文字列
	'''

	# 強調マークアップの除去
	pattern = re.compile(r'''
		(\'{2,5})	# 2〜5個の'（マークアップの開始）
		(.*?)		# 任意の1文字以上（対象の文字列）
		(\1)		# 1番目のキャプチャと同じ（マークアップの終了）
		''', re.MULTILINE + re.VERBOSE)
	target = pattern.sub(r'\2', target)

	# 内部リンク、ファイルの除去
	pattern = re.compile(r'''
		\[\[		# '[['（マークアップの開始）
		(?:			# キャプチャ対象外のグループ開始
			[^|]*?	# '|'以外の文字が0文字以上、非貪欲
			\|		# '|'
		)*?			# グループ終了、このグループが0以上出現、非貪欲
		([^|]*?)	# キャプチャ対象、'|'以外が0文字以上、非貪欲（表示対象の文字列）
		\]\]		# ']]'（マークアップの終了）
		''', re.MULTILINE + re.VERBOSE)
	target = pattern.sub(r'\1', target)

	# Template:Langの除去		{{lang|言語タグ|文字列}}
	pattern = re.compile(r'''
		\{\{lang	# '{{lang'（マークアップの開始）
		(?:			# キャプチャ対象外のグループ開始
			[^|]*?	# '|'以外の文字が0文字以上、非貪欲
			\|		# '|'
		)*?			# グループ終了、このグループが0以上出現、非貪欲
		([^|]*?)	# キャプチャ対象、'|'以外が0文字以上、非貪欲（表示対象の文字列）
		\}\}		# '}}'（マークアップの終了）
		''', re.MULTILINE + re.VERBOSE)
	target = pattern.sub(r'\1', target)

	# 外部リンクの除去	[http://xxxx] 、[http://xxx xxx]
	pattern = re.compile(r'''
		\[http:\/\/	# '[http://'（マークアップの開始）
		(?:			# キャプチャ対象外のグループ開始
			[^\s]*?	# 空白以外の文字が0文字以上、非貪欲
			\s		# 空白
		)?			# グループ終了、このグループが0か1出現
		([^]]*?)	# キャプチャ対象、']'以外が0文字以上、非貪欲（表示対象の文字列）
		\]			# ']'（マークアップの終了）
		''', re.MULTILINE + re.VERBOSE)
	target = pattern.sub(r'\1', target)

	# <br>、<ref>の除去
	pattern = re.compile(r'''
		<			# '<'（マークアップの開始）
		\/?			# '/'が0か1出現（終了タグの場合は/がある）
		[br|ref]	# 'br'か'ref'
		[^>]*?		# '>'以外が0文字以上、非貪欲
		>			# '>'（マークアップの終了）
		''', re.MULTILINE + re.VERBOSE)
	target = pattern.sub('', target)

	return target


# 基礎情報テンプレートの抽出条件のコンパイル
pattern = re.compile(r'''
	^\{\{基礎情報.*?$	# '{{基礎情報'で始まる行
	(.*?)		# キャプチャ対象、任意の0文字以上、非貪欲
	^\}\}$		# '}}'の行
	''', re.MULTILINE + re.VERBOSE + re.DOTALL)

# 基礎情報テンプレートの抽出
contents = pattern.findall(extract_UK())

# 抽出結果からのフィールド名と値の抽出条件コンパイル
pattern = re.compile(r'''
	^\|			# '|'で始まる行
	(.+?)		# キャプチャ対象（フィールド名）、任意の1文字以上、非貪欲
	\s*			# 空白文字0文字以上
	=
	\s*			# 空白文字0文字以上
	(.+?)		# キャプチャ対象（値）、任意の1文字以上、非貪欲
	(?:			# キャプチャ対象外のグループ開始
		(?=\n\|) 	# 改行+'|'の手前（肯定の先読み）
		| (?=\n$)	# または、改行+終端の手前（肯定の先読み）
	)			# グループ終了
	''', re.MULTILINE + re.VERBOSE + re.DOTALL)

# フィールド名と値の抽出
fields = pattern.findall(contents[0])

# 辞書にセット
result = {}
for field in fields:
	result[field[0]] = remove_markup(field[1])

# 国旗画像の値を取得
fname_flag = result['国旗画像']

# リクエスト生成
url = 'https://www.mediawiki.org/w/api.php?' \
	+ 'action=query' \
	+ '&titles=File:' + urllib.parse.quote(fname_flag) \
	+ '&format=json' \
	+ '&prop=imageinfo' \
	+ '&iiprop=url'

# MediaWikiのサービスへリクエスト送信
request = urllib.request.Request(url,
	headers={'User-Agent': 'NLP100_Python(@segavvy)'})
connection = urllib.request.urlopen(request)

# jsonとして受信
data = json.loads(connection.read().decode())

# URL取り出し
url = data['query']['pages'].popitem()[1]['imageinfo'][0]['url']
print(url)
```

####実行結果：

```console:端末
https://upload.wikimedia.org/wikipedia/commons/a/ae/Flag_of_the_United_Kingdom.svg
```

Webブラウザで得られたURLにアクセスしてみました。

![Kobito.4Q8ynS.png](https://qiita-image-store.s3.amazonaws.com/0/139624/c12cca3b-7954-dc2d-fc6b-2bd0f85a1c61.png "Kobito.4Q8ynS.png")

正しいURLが取得できているようです。

###Webサービスの利用
Webサービスを使うにはHTTPをしゃべる必要があります。今回はお手軽そうな[`urllib.request`](http://docs.python.jp/3/library/urllib.request.html#module-urllib.request)を使いました。
ただ、この解説に、

>参考:より高水準の HTTP クライアントインターフェイスとしては[Requests package](https://requests.readthedocs.io/en/master/) がお奨めです。

とあるので、そちらを使った方が良いのかもしれません。

###MediaWiki API
プログラムの前半は[前問](http://qiita.com/segavvy/items/8c4567ec1124320d3354)のままです。前問で作った辞書から国旗画像のファイル名を取り出して、MediaWiki APIを使ってURLを取得しています。MediaWiki APIの使い方は[API:メイン ページ](https://www.mediawiki.org/wiki/API:Main_page/ja)で調べられます。[単純な例](https://www.mediawiki.org/wiki/API:Main_page/ja#.E5.8D.98.E7.B4.94.E3.81.AA.E4.BE.8B)を参考に、HTTP GETで実装してみました。

####アクセスするURL（エンドポイント）
[エンドポイント](https://www.mediawiki.org/wiki/API:Main_page/ja#.E3.82.A8.E3.83.B3.E3.83.89.E3.83.9D.E3.82.A4.E3.83.B3.E3.83.88)に説明があります。とりあえずMediaWiki APIのURL`https://www.mediawiki.org/w/api.php `を使っています。

####`User-Agent`の指定
[クライアントを識別する](https://www.mediawiki.org/wiki/API:Main_page/ja#.E3.82.AF.E3.83.A9.E3.82.A4.E3.82.A2.E3.83.B3.E3.83.88.E3.82.92.E8.AD.98.E5.88.A5.E3.81.99.E3.82.8B)に説明されているように、`User-Agent`を省略することができません。正確なフォーマットは分かりませんが、クライアントを識別するための文字列を指定すれば良いようなので、`NLP100_Python(@segavvy)`としてみました。

####パラメーター
今回は次のものを指定しています。

| パラメーター | 指定値 | 詳細 |
|--------|--------|---|
|action |query        |[単純な例](https://www.mediawiki.org/wiki/API:Main_page/ja#.E5.8D.98.E7.B4.94.E3.81.AA.E4.BE.8B)をそのまま真似しました。 |
|titles |File:(国旗画像のファイル名)|[API:Imageinfo](https://www.mediawiki.org/wiki/API:Imageinfo)で取得したい対象のファイルを指定します。なお、特殊文字のエスケープが必要なため、[`urllib.parse.quote()`](http://docs.python.jp/3/library/urllib.parse.html#urllib.parse.quote)を使いました。 |
|format |json        |[単純な例](https://www.mediawiki.org/wiki/API:Main_page/ja#.E5.8D.98.E7.B4.94.E3.81.AA.E4.BE.8B)をそのまま真似して、json形式にしました。|
|prop |imageinfo |[API:Imageinfo](https://www.mediawiki.org/wiki/API:Imageinfo)を使うことを示しています。|
|iiprop |url       |[API:Imageinfo](https://www.mediawiki.org/wiki/API:Imageinfo)で取得したい項目を指定します。今回はURLが欲しいので`url`を指定しました。 |

####APIの戻り値
json形式を指定しているので、以前の問題で使用した`json.loads()`で変換させれば、簡単に扱えるようになります。なお、受信結果はバイト列のため、`json.loads()`する前に[`bytes.decode()`](http://docs.python.jp/3/library/stdtypes.html#bytes.decode)で文字列への変換が必要です。

以下、`json.loads()`した`data`を、`print(json.dumps(data, indent=4))`した結果です。

```console:端末
{
    "continue": {
        "continue": "||",
        "iistart": "2007-09-03T09:51:34Z"
    },
    "query": {
        "pages": {
            "-1": {
                "imagerepository": "shared",
                "imageinfo": [
                    {
                        "url": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Flag_of_the_United_Kingdom.svg",
                        "descriptionshorturl": "https://commons.wikimedia.org/w/index.php?curid=347935",
                        "descriptionurl": "https://commons.wikimedia.org/wiki/File:Flag_of_the_United_Kingdom.svg"
                    }
                ],
                "missing": "",
                "title": "File:Flag of the United Kingdom.svg",
                "ns": 6
            }
        }
    }
}
```

目的の`url`が結構深いところにあるので、取り出しはちょっと面倒でした。

　
30本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
<hr>

*実行結果には、[100本ノックで用いるコーパス・データ](http://www.cl.ecei.tohoku.ac.jp/nlp100/#data)で配布されているデータの一部が含まれます。この第3章で用いているデータのライセンスは[クリエイティブ・コモンズ 表示-継承 3.0 非移植](https://creativecommons.org/licenses/by-sa/3.0/legalcode)（[日本語訳](https://creativecommons.org/licenses/by-sa/3.0/deed.ja)）です。*