言語処理100本ノック 2015「第3章: 正規表現」の25本目「テンプレートの抽出」記録です。
今回は肯定の先読みという少しわかりにくい内容を扱います。理解してしまえば何でも無いのですが、言葉がとっつきにくいのでしょうか。また、DOTALLと順序付き辞書も学習します。
今回の内容は第3章の以降のノックにつながっていくので重要です。
参考リンク
リンク | 備考 |
---|---|
025.テンプレートの抽出.ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:25 | 多くのソース部分のコピペ元 |
ゼロから覚えるPython正規表現の基本とTips | 当ノックで学習した内容を整理しました |
正規表現 HOWTO | Python公式の正規表現How To |
re --- 正規表現操作 | Python公式のreパッケージ説明 |
Help:早見表 | Wikipediaの代表的なマークアップの早見表 |
Template:基礎情報 国 | Wikipediaの国のTemplate |
環境
種類 | バージョン | 内容 |
---|---|---|
OS | Ubuntu18.04.01 LTS | 仮想で動かしています |
pyenv | 1.2.15 | 複数Python環境を使うことがあるのでpyenv使っています |
Python | 3.6.9 | pyenv上でpython3.6.9を使っています 3.7や3.8系を使っていないことに深い理由はありません パッケージはvenvを使って管理しています |
上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。
種類 | バージョン |
---|---|
pandas | 0.25.3 |
第3章: 正規表現
学習内容
Wikipediaのページのマークアップ記述に正規表現を適用することで,様々な情報・知識を取り出します.
正規表現, JSON, Wikipedia, InfoBox, ウェブサービス
ノック内容
Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある.
- 1行に1記事の情報がJSON形式で格納される
- 各行には記事名が"title"キーに,記事本文が"text"キーの辞書オブジェクトに格納され,そのオブジェクトがJSON形式で書き出される
- ファイル全体はgzipで圧縮される
以下の処理を行うプログラムを作成せよ.
25. テンプレートの抽出
記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し,辞書オブジェクトとして格納せよ.
課題補足(「基礎情報」について)
Template:基礎情報 国に「基礎情報」のテンプレートがあり、参考にしました。
この基礎情報内のフィールド名と値を正規表現で抽出しています。
{{基礎情報 国\n
|略名 = イギリス\n
|日本語国名 = グレートブリテン及び北アイルランド連合王国\n
中略
|国際電話番号 = 44\n
|注記 = <references />\n
}}\n
回答
回答プログラム 025.テンプレートの抽出.ipynb
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を使って.
のワイルドカードに改行を含めています。
今までの正規表現ではfindall
関数を使っていましたが、1箇所だけでいいので'search'関数を使っています。
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
を使って順序を保った辞書型にしています(順序なしだと出力時に読みにくいため)。
templates = OrderedDict(re.findall(r'''
^\| # \はエスケープ処理、非キャプチャ
(.+?) # キャプチャ対象(key)、非貪欲
\s* # 空白文字0文字以上
= # 検索語句、非キャプチャ
\s* # 空白文字0文字以上
(.+?) # キャプチャ対象(Value)、非貪欲
(?: # キャプチャ対象外のグループ開始
(?=\n\|) # 改行(\n)+'|'の手前(肯定の先読み)
| (?=\n$) # または、改行(\n)+終端の手前(肯定の先読み)
) # キャプチャ対象外のグループ終了
''', basic.group(1), re.MULTILINE+re.VERBOSE+re.DOTALL))
肯定の先読み
肯定の先読みは、後続文字列を先に読んで条件にマッチするのならば、その部分もマッチするというテクニックです。書いていてわかりにくいのはわかっています。
そもそも、このシリーズは以下の4つがあります。
- 肯定の先読みアサーション(Positive Lookahead Assertions)
- 否定の先読みアサーション(Negative Lookahead Assertions)
- 肯定の後読みアサーション(Positive Lookbehind Assertions)
- 否定の後読みアサーション(Negative Lookbehind Assertions)
マトリックスにすると以下の形。
肯定 | 否定 | |
---|---|---|
先読み |
(?=...) ... 部分が次に続けばマッチ |
(?!...) ... 部分が次に続かなければマッチ |
後読み |
(?<=...) ... 部分が現在位置より前でマッチがあればマッチ |
(?<!...) ... 部分が現在位置より前でマッチがなければマッチ |
細かい説明より、具体例の方がわかりやすいです。
>>> 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']
出力結果(実行結果)
プログラム実行すると最後に以下の結果が出力されます。
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ポンド]] (£)'),
('通貨コード', 'GBP'),
('時間帯', '±0'),
('夏時間', '+1'),
('ISO 3166-1', 'GB / GBR'),
('ccTLD', '[[.uk]] / [[.gb]]<ref>使用は.ukに比べ圧倒的少数。</ref>'),
('国際電話番号', '44'),
('注記', '<references />')])