言語処理100本ノック-25:テンプレートの抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第3章: 正規表現」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)の[25本目「テンプレートの抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec25)記録です。
今回は**肯定の先読み**という少しわかりにくい内容を扱います。理解してしまえば何でも無いのですが、言葉がとっつきにくいのでしょうか。また、**DOTALLと順序付き辞書**も学習します。
今回の内容は第3章の以降のノックにつながっていくので重要です。

# 参考リンク

|リンク|備考|
|:--|:--|
|[025.テンプレートの抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/03.%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/025.%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:25](http://qiita.com/segavvy/items/e402ad0a5b0f52453d7f)|多くのソース部分のコピペ元|
|[ゼロから覚えるPython正規表現の基本とTips](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7)|当ノックで学習した内容を整理しました|
|[正規表現 HOWTO](https://docs.python.org/ja/3/howto/regex.html#regex-howto)|Python公式の正規表現How To|
|[re --- 正規表現操作](https://docs.python.org/ja/3/library/re.html)|Python公式のreパッケージ説明|
|[Help:早見表](https://ja.wikipedia.org/wiki/Help:%E6%97%A9%E8%A6%8B%E8%A1%A8)|Wikipediaの代表的なマークアップの早見表|
|[Template:基礎情報 国](https://ja.wikipedia.org/wiki/Template:%E5%9F%BA%E7%A4%8E%E6%83%85%E5%A0%B1_%E5%9B%BD)|Wikipediaの国のTemplate|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.15|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.9|pyenv上でpython3.6.9を使っています<br>3.7や3.8系を使っていないことに深い理由はありません<br>パッケージはvenvを使って管理しています|

上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。

|種類|バージョン|
|:--|:--|
|pandas|0.25.3|

# [第3章: 正規表現](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)
## 学習内容
> Wikipediaのページのマークアップ記述に正規表現を適用することで，様々な情報・知識を取り出します．
>> 正規表現, JSON, Wikipedia, InfoBox, ウェブサービス

## ノック内容
> Wikipediaの記事を以下のフォーマットで書き出したファイル[jawiki-country.json.gz](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/jawiki-country.json.gz)がある．

> - 1行に1記事の情報がJSON形式で格納される
> - 各行には記事名が"title"キーに，記事本文が"text"キーの辞書オブジェクトに格納され，そのオブジェクトがJSON形式で書き出される
> - ファイル全体はgzipで圧縮される

> 以下の処理を行うプログラムを作成せよ．

### [25. テンプレートの抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec25)
> 記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し，辞書オブジェクトとして格納せよ．

### 課題補足(「基礎情報」について)
[Template:基礎情報 国](https://ja.wikipedia.org/wiki/Template:%E5%9F%BA%E7%A4%8E%E6%83%85%E5%A0%B1_%E5%9B%BD)に「基礎情報」のテンプレートがあり、参考にしました。
この基礎情報内のフィールド名と値を正規表現で抽出しています。

```text:ファイル内の「基礎情報」部分抜粋
{{基礎情報 国\n
|略名 = イギリス\n
|日本語国名 = グレートブリテン及び北アイルランド連合王国\n

中略

|国際電話番号 = 44\n
|注記 = <references />\n
}}\n
```

# 回答
## 回答プログラム [025.テンプレートの抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/03.%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/025.%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)

```python
from collections import OrderedDict
from pprint import pprint
import re

import pandas as pd

def extract_by_title(title):
    df_wiki = pd.read_json('jawiki-country.json', lines=True)
    return df_wiki[(df_wiki['title'] == title)]['text'].values[0]

wiki_body = extract_by_title('イギリス')

basic = re.search(r'''
                    ^\{\{基礎情報.*?\n  #検索語句(\はエスケープ処理)、非キャプチャ、非貪欲
                    (.*?)              #任意の文字列
                    \}\}               #検索語句(\はエスケープ処理)
                    $                  #文字列の末尾
                    ''', wiki_body, re.MULTILINE+re.VERBOSE+re.DOTALL)
pprint(basic.group(1))

# rを先頭にするとraw string でエスケープシーケンス無視
# 3重クォートで途中改行無視
# re.VERBOSEオプションを使うことによって、空白とコメント無視
templates = OrderedDict(re.findall(r'''
                          ^\|         # \はエスケープ処理、非キャプチャ
                          (.+?)       # キャプチャ対象(key)、非貪欲
                          \s*         # 空白文字0文字以上
                          =           # 検索語句、非キャプチャ
                          \s*         # 空白文字0文字以上
                          (.+?)       # キャプチャ対象(Value)、非貪欲
                          (?:         # キャプチャ対象外のグループ開始
                            (?=\n\|)  # 改行(\n)+'|'の手前(肯定の先読み)
                          | (?=\n$)   # または、改行(\n)+終端の手前(肯定の先読み)
                          )           # キャプチャ対象外のグループ終了
                         ''', basic[0], re.MULTILINE+re.VERBOSE+re.DOTALL))
pprint(templates)
```

## 回答解説
### 「基礎情報」抽出
まずは「基礎情報」抽出部分です。1回の正規表現では辞書型まで持っていけなかったので、2ステップにしています。
`^\{\{基礎情報`は、行頭で「{{基礎情報」から始まり改行までを開始条件としています。改行後の文字列を`}}`まで取得です。[re.DOTALL](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#dotall)を使って`.`のワイルドカードに改行を含めています。
今までの正規表現では[`findall`関数](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#findall)を使っていましたが、1箇所だけでいいので['search'関数](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#match%E3%81%A8search)を使っています。

```python:
basic = re.search(r'''
                    ^\{\{基礎情報.*?\n  #検索語句(\はエスケープ処理)、非キャプチャ、非貪欲
                    (.*?)              #任意の文字列
                    \}\}               #検索語句(\はエスケープ処理)
                    $                  #文字列の末尾
                    ''', wiki_body, re.MULTILINE+re.VERBOSE+re.DOTALL)
pprint(basic.group(1))
```

「基礎情報」部分は以下の形で抽出されます。

```:「基礎情報」抽出結果
('|略名 = イギリス\n'
 '|日本語国名 = グレートブリテン及び北アイルランド連合王国\n'
 '|公式国名 = {{lang|en|United Kingdom of Great Britain and Northern '
 'Ireland}}<ref>英語以外での正式国名:<br/>\n'

中略

 '|ccTLD = [[.uk]] / [[.gb]]<ref>使用は.ukに比べ圧倒的少数。</ref>\n'
 '|国際電話番号 = 44\n'
 '|注記 = <references />\n')
```

### フィールド名と値抽出
こちらでフィールド名と値を抽出しています。結果を`collections.OrderDIct`を使って順序を保った辞書型にしています(順序なしだと出力時に読みにくいため)。

```python:
templates = OrderedDict(re.findall(r'''
                          ^\|         # \はエスケープ処理、非キャプチャ
                          (.+?)       # キャプチャ対象(key)、非貪欲
                          \s*         # 空白文字0文字以上
                          =           # 検索語句、非キャプチャ
                          \s*         # 空白文字0文字以上
                          (.+?)       # キャプチャ対象(Value)、非貪欲
                          (?:         # キャプチャ対象外のグループ開始
                            (?=\n\|)  # 改行(\n)+'|'の手前(肯定の先読み)
                          | (?=\n$)   # または、改行(\n)+終端の手前(肯定の先読み)
                          )           # キャプチャ対象外のグループ終了
                         ''', basic.group(1), re.MULTILINE+re.VERBOSE+re.DOTALL))
```

### [肯定の先読み](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#%E5%85%88%E8%AA%AD%E3%81%BF%E5%BE%8C%E8%AA%AD%E3%81%BF%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)
肯定の先読みは、後続文字列を先に読んで条件にマッチするのならば、その部分もマッチするというテクニックです。書いていてわかりにくいのはわかっています。
そもそも、このシリーズは以下の4つがあります。

- 肯定の先読みアサーション(Positive Lookahead Assertions)
- 否定の先読みアサーション(Negative Lookahead Assertions)
- 肯定の後読みアサーション(Positive Lookbehind Assertions)
- 否定の後読みアサーション(Negative Lookbehind Assertions)

マトリックスにすると以下の形。

||肯定|否定|
|:-:|:--|:--|
|**先読み**|`(?=...)`<br>`...`部分が次に続けばマッチ|`(?!...)`<br>`...`部分が次に続かなければマッチ|
|**後読み**|`(?<=...)`<br>`...`部分が現在位置より前でマッチがあればマッチ|`(?<!...)`<br>`...`部分が現在位置より前でマッチがなければマッチ|

細かい説明より、具体例の方がわかりやすいです。

```python
>>> string = 'A01234 B91235 C01234'

# 肯定の先読みアサーション(Positive Lookahead Assertions)
# '123'の次に'5'が続く文字列('(?=5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?=5).', string))
['B91235']

# 否定の先読みアサーション(Negative Lookahead Assertions)
# '123'の次に'5'が続かない文字列('(?!5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?!5).', string))
['A01234', 'C01234']

# 肯定の後読みアサーション(Positive Lookbehind Assertions)
# '0'が'123'の前にマッチする文字列('(?<=0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<=0)123', string))
['A0123', 'C0123']

# 否定の後読みアサーション(Negative Lookbehind Assertions)
# '0'が'123'の前にマッチしない文字列('(?<!0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<!0)123', string))
['B9123']
```

### 出力結果(実行結果)
プログラム実行すると最後に以下の結果が出力されます。

```bash:出力結果
OrderedDict([('略名', 'イギリス'),
             ('日本語国名', 'グレートブリテン及び北アイルランド連合王国'),
             ('公式国名',
              '{{lang|en|United Kingdom of Great Britain and Northern '
              'Ireland}}<ref>英語以外での正式国名:<br/>\n'
              '*{{lang|gd|An Rìoghachd Aonaichte na Breatainn Mhòr agus Eirinn '
              'mu Thuath}}（[[スコットランド・ゲール語]]）<br/>\n'
              '*{{lang|cy|Teyrnas Gyfunol Prydain Fawr a Gogledd '
              'Iwerddon}}（[[ウェールズ語]]）<br/>\n'
              '*{{lang|ga|Ríocht Aontaithe na Breataine Móire agus Tuaisceart '
              'na hÉireann}}（[[アイルランド語]]）<br/>\n'
              '*{{lang|kw|An Rywvaneth Unys a Vreten Veur hag Iwerdhon '
              'Glédh}}（[[コーンウォール語]]）<br/>\n'
              '*{{lang|sco|Unitit Kinrick o Great Breetain an Northren '
              'Ireland}}（[[スコットランド語]]）<br/>\n'
              '**{{lang|sco|Claught Kängrick o Docht Brätain an Norlin '
              'Airlann}}、{{lang|sco|Unitet Kängdom o Great Brittain an Norlin '
              'Airlann}}（アルスター・スコットランド語）</ref>'),
             ('国旗画像', 'Flag of the United Kingdom.svg'),
             ('国章画像',
              '[[ファイル:Royal Coat of Arms of the United '
              'Kingdom.svg|85px|イギリスの国章]]'),
             ('国章リンク', '（[[イギリスの国章|国章]]）'),
             ('標語', '{{lang|fr|Dieu et mon droit}}<br/>（[[フランス語]]:神と私の権利）'),
             ('国歌', '[[女王陛下万歳|神よ女王陛下を守り給え]]'),
             ('位置画像', 'Location_UK_EU_Europe_001.svg'),
             ('公用語', '[[英語]]（事実上）'),
             ('首都', '[[ロンドン]]'),
             ('最大都市', 'ロンドン'),
             ('元首等肩書', '[[イギリスの君主|女王]]'),
             ('元首等氏名', '[[エリザベス2世]]'),
             ('首相等肩書', '[[イギリスの首相|首相]]'),
             ('首相等氏名', '[[デーヴィッド・キャメロン]]'),
             ('面積順位', '76'),
             ('面積大きさ', '1 E11'),
             ('面積値', '244,820'),
             ('水面積率', '1.3%'),
             ('人口統計年', '2011'),
             ('人口順位', '22'),
             ('人口大きさ', '1 E7'),
             ('人口値',
              '63,181,775<ref>[http://esa.un.org/unpd/wpp/Excel-Data/population.htm '
              'United Nations Department of Economic and Social '
              'Affairs>Population Division>Data>Population>Total '
              'Population]</ref>'),
             ('人口密度値', '246'),
             ('GDP統計年元', '2012'),
             ('GDP値元',
              '1兆5478億<ref '
              'name="imf-statistics-gdp">[http://www.imf.org/external/pubs/ft/weo/2012/02/weodata/weorept.aspx?pr.x=70&pr.y=13&sy=2010&ey=2012&scsm=1&ssd=1&sort=country&ds=.&br=1&c=112&s=NGDP%2CNGDPD%2CPPPGDP%2CPPPPC&grp=0&a= '
              'IMF>Data and Statistics>World Economic Outlook Databases>By '
              'Countrise>United Kingdom]</ref>'),
             ('GDP統計年MER', '2012'),
             ('GDP順位MER', '5'),
             ('GDP値MER', '2兆4337億<ref name="imf-statistics-gdp" />'),
             ('GDP統計年', '2012'),
             ('GDP順位', '6'),
             ('GDP値', '2兆3162億<ref name="imf-statistics-gdp" />'),
             ('GDP/人', '36,727<ref name="imf-statistics-gdp" />'),
             ('建国形態', '建国'),
             ('確立形態1',
              '[[イングランド王国]]／[[スコットランド王国]]<br />（両国とも[[連合法 '
              '(1707年)|1707年連合法]]まで）'),
             ('確立年月日1', '[[927年]]／[[843年]]'),
             ('確立形態2', '[[グレートブリテン王国]]建国<br />（[[連合法 (1707年)|1707年連合法]]）'),
             ('確立年月日2', '[[1707年]]'),
             ('確立形態3',
              '[[グレートブリテン及びアイルランド連合王国]]建国<br />（[[連合法 (1800年)|1800年連合法]]）'),
             ('確立年月日3', '[[1801年]]'),
             ('確立形態4', "現在の国号「'''グレートブリテン及び北アイルランド連合王国'''」に変更"),
             ('確立年月日4', '[[1927年]]'),
             ('通貨', '[[スターリング・ポンド|UKポンド]] (&pound;)'),
             ('通貨コード', 'GBP'),
             ('時間帯', '±0'),
             ('夏時間', '+1'),
             ('ISO 3166-1', 'GB / GBR'),
             ('ccTLD', '[[.uk]] / [[.gb]]<ref>使用は.ukに比べ圧倒的少数。</ref>'),
             ('国際電話番号', '44'),
             ('注記', '<references />')])
```