2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

言語処理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ポンド]] (&pound;)'),
             ('通貨コード', 'GBP'),
             ('時間帯', '±0'),
             ('夏時間', '+1'),
             ('ISO 3166-1', 'GB / GBR'),
             ('ccTLD', '[[.uk]] / [[.gb]]<ref>使用は.ukに比べ圧倒的少数。</ref>'),
             ('国際電話番号', '44'),
             ('注記', '<references />')])
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?