言語処理100本ノック 第3章: 正規表現

はじめに

今回は言語処理100本ノック 第3章: 正規表現です。

これまでの

ohshige.hatenablog.com ohshige.hatenablog.com

第3章: 正規表現

Python 3.7.0 でやっていきます。
問題の解釈違い、間違い等ありましたら、教えていただけると幸いです。

github.com

20. JSONデータの読み込み

問題

Wikipedia記事のJSONファイルを読み込み,「イギリス」に関する記事本文を表示せよ.問題21-29では,ここで抽出した記事本文に対して実行せよ.

解答&出力

import json


def get_wiki_text(title):
    with open("jawiki-country.json") as f:
        for line in f:
            wiki = json.loads(line)
            if wiki["title"] == title:
                return wiki["text"]
        return ""


if __name__ == "__main__":
    print(get_wiki_text("イギリス"))
{{redirect|UK}}
{{基礎情報 国
|略名 = イギリス
|日本語国名 = グレートブリテン及び北アイルランド連合王国
|公式国名 = {{lang|en|United Kingdom of Great Britain and Northern Ireland}}<ref>英語以外での正式国名:<br/>
*{{lang|gd|An Rìoghachd Aonaichte na Breatainn Mhòr agus Eirinn mu Thuath}}([[スコットランド・ゲール語]])<br/>
*{{lang|cy|Teyrnas Gyfunol Prydain Fawr a Gogledd Iwerddon}}([[ウェールズ語]])<br/>
*{{lang|ga|Ríocht Aontaithe na Breataine Móire agus Tuaisceart na hÉireann}}([[アイルランド語]])<br/>
*{{lang|kw|An Rywvaneth Unys a Vreten Veur hag Iwerdhon Glédh}}([[コーンウォール語]])<br/>
*{{lang|sco|Unitit Kinrick o Great Breetain an Northren Ireland}}([[スコットランド語]])<br/>
**{{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|イギリスの国章]]
|国章リンク = ([[イギリスの国章|国章]])

〜

{{デフォルトソート:いきりす}}
[[Category:イギリス|*]]
[[Category:英連邦王国|*]]
[[Category:G8加盟国]]
[[Category:欧州連合加盟国]]
[[Category:海洋国家]]
[[Category:君主国]]
[[Category:島国|くれいとふりてん]]
[[Category:1801年に設立された州・地域]]

ひとこと

1行ずつ愚直にjsonライブラリを使ってパースし、指定のタイトルを探し、返しているだけです。

21. カテゴリ名を含む行を抽出

問題

記事中でカテゴリ名を宣言している行を抽出せよ.

解答&出力

from div03.sec20 import get_wiki_text

text = get_wiki_text("イギリス")

for line in text.split("\n"):
    if line.startswith("[[Category:"):
        print(line.strip())
[[Category:イギリス|*]]
[[Category:英連邦王国|*]]
[[Category:G8加盟国]]
[[Category:欧州連合加盟国]]
[[Category:海洋国家]]
[[Category:君主国]]
[[Category:島国|くれいとふりてん]]
[[Category:1801年に設立された州・地域]]

ひとこと

Wikipediaの早見表によると、カテゴリは [[Category:ヘルプ|はやみひよう]] といった形になるようで、イギリスの例だと1行に1つのカテゴリのようなので、簡単化して正規表現すら使わず先頭一致のみで判定しました。

22. カテゴリ名の抽出

問題

記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.

解答&出力

import re
from div03.sec20 import get_wiki_text

text = get_wiki_text("イギリス")

category_reg = re.compile(r"\[\[Category:(.*?)(\|.*?)?\]\]")

for line in text.split("\n"):
    category = category_reg.match(line.strip())
    if category is not None:
        print(category[1])
イギリス
英連邦王国
G8加盟国
欧州連合加盟国
海洋国家
君主国
島国
1801年に設立された州・地域

ひとこと

今度は「行単位ではなく名前で」とわざわざ注意書きされているので、さきほどと同じ方法は使えません。
先に正規表現 r"\[\[Category:(.*?)(\|.*?)?\]\]" を定義してコンパイルしておき、1行ずつ match するか確かめています。
[[Category:ヘルプ|はやみひよう]] のうちの ヘルプ の部分がカテゴリ名であると見なして、 |* などは除去されるようにしています。
pythonでは文字列の先頭に r をつけるとバックスラッシュ自体エスケープが不要になるので、扱いが楽になります。

23. セクション構造

問題

記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.

解答&出力

import re
from div03.sec20 import get_wiki_text

text = get_wiki_text("イギリス")

section_reg = [
    (4, re.compile(r"=====(.+?)=====")),
    (3, re.compile(r"====(.+?)====")),
    (2, re.compile(r"===(.+?)===")),
    (1, re.compile(r"==(.+?)==")),
]

for line in text.split("\n"):
    line = line.strip()
    if not line.startswith("=="):
        continue
    for level, regex in section_reg:
        section = regex.match(line)
        if section is not None:
            print(section[1].strip(), level)
            break
国名 1
歴史 1
地理 1
気候 2
政治 1

〜

競馬 2
モータースポーツ 2
脚注 1
関連項目 1
外部リンク 1

ひとこと

Wikipediaの早見表によると、見出しはLevel2からLevel5までしかないようなので全探索することにします。
また、問題では == セクション名 == がレベル1で、早見表では == Level 2 == がLevel2となっていて、異なるようなので、今回は問題文の表記に合わせました。

===== セクション名 ======= セクション名 == を含んでしまうので、よりレベルの高いものから順番にマッチするか探索します。
単純に全探索してしまうと、行数×4回も正規表現の判定を実行してしまうため、行頭が == であるかどうかを最初に挟んでいます。

24. ファイル参照の抽出

問題

記事から参照されているメディアファイルをすべて抜き出せ.

解答&出力

import re
from div03.sec20 import get_wiki_text

text = get_wiki_text("イギリス")

for media in re.findall(r"\[\[(ファイル|File):(.+?)(\|.*)?\]\]", text):
    print(media[1])
Royal Coat of Arms of the United Kingdom.svg
Battle of Waterloo 1815.PNG
The British Empire.png
Uk topo en.jpg
BenNevis2005.jpg
Elizabeth II greets NASA GSFC employees, May 8, 2007 edit.jpg
Palace of Westminster, London - Feb 2007.jpg
David Cameron and Barack Obama at the G20 Summit in Toronto.jpg
Soldiers Trooping the Colour, 16th June 2007.jpg
Scotland Parliament Holyrood.jpg
London.bankofengland.arp.jpg
City of London skyline from London City Hall - Oct 2008.jpg
Oil platform in the North SeaPros.jpg
Eurostar at St Pancras Jan 2008.jpg
Heathrow T5.jpg
Anglospeak.svg
CHANDOS3.jpg
The Fabs.JPG
Wembley Stadium, illuminated.jpg

ひとこと

メディアファイルが何かよくわからなかったのですが、Wikipediaの早見表[[ファイル:Wikipedia-logo-v2-ja.png|thumb|説明文]] だとアタリをつけ、これを抽出するようにしました。
完全に早見表だけを信じてしまったのですが、 [[ファイル:〜 だけでなく [[File:〜 も存在しているということに後から気が付きました。
そのため、正規表現部分はどちらも抽出できるようになっています。

25. テンプレートの抽出

問題

記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し,辞書オブジェクトとして格納せよ.

解答&出力

from div03.sec20 import get_wiki_text


def extract_basic_info(text):
    start = text.find("{{基礎情報")
    text = text[start:]

    # 入れ子の {{ }} を無視して、基礎情報テンプレート全体を取得する
    nest = 0
    for i in range(len(text)):
        t = text[i:i + 2]
        if t == "{{":
            nest += 1
        if t == "}}":
            nest -= 1
        if nest == 0:
            text = text[:i + 2]
            break

    # 入れ子の | を無視して、テンプレートのペアを取得する
    pairs = []
    start = 0
    nest1 = 0
    nest2 = 0
    for i in range(len(text)):
        if text[i] == "|":
            if start == 0:
                start = i + 1
            elif start != 0 and nest1 == 1 and nest2 == 0:
                pairs.append(text[start:i].strip())
                start = i + 1
        if text[i:i + 2] == "{{":
            nest1 += 1
        if text[i:i + 2] == "}}":
            nest1 -= 1
        if text[i:i + 1] == "[":
            nest2 += 1
        if text[i:i + 1] == "]":
            nest2 -= 1
    pairs.append(text[start:-2].strip())

    # 辞書化する
    result = {}
    for pair in pairs:
        separator = pair.find("=")
        result[pair[:separator].strip()] = pair[separator + 1:].strip()

    return result


if __name__ == "__main__":
    import pprint
    uk_text = get_wiki_text("イギリス")
    result_dict = extract_basic_info(uk_text)
    pprint.pprint(result_dict, width=1000)
{'GDP/人': '36,727<ref name="imf-statistics-gdp" />',
 'GDP値': '2兆3162億<ref name="imf-statistics-gdp" />',
 'GDP値MER': '2兆4337億<ref name="imf-statistics-gdp" />',
 '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統計年': '2012',
 'GDP統計年MER': '2012',

〜

 '確立形態1': '[[イングランド王国]]/[[スコットランド王国]]<br />(両国とも[[連合法 (1707年)|1707年連合法]]まで)',
 '確立形態2': '[[グレートブリテン王国]]建国<br />([[連合法 (1707年)|1707年連合法]])',
 '確立形態3': '[[グレートブリテン及びアイルランド連合王国]]建国<br />([[連合法 (1800年)|1800年連合法]])',
 '確立形態4': "現在の国号「'''グレートブリテン及び北アイルランド連合王国'''」に変更",
 '通貨': '[[スターリング・ポンド|UKポンド]] (&pound;)',
 '通貨コード': 'GBP',
 '面積値': '244,820',
 '面積大きさ': '1 E11',
 '面積順位': '76',
 '首相等氏名': '[[デーヴィッド・キャメロン]]',
 '首相等肩書': '[[イギリスの首相|首相]]',
 '首都': '[[ロンドン]]'}

ひとこと

Wikipediaのテンプレートについて軽く勉強しました。
ここによると、テンプレート自体は必ずしも行頭に {{}} があるわけではないようなので、そこは頑張ってみようと思いました。
そのため、これまでで一番難しかったような気がします。

方針としては、まず基礎情報テンプレートを抽出し、その後でその中からフィールド名と値をペアで抽出します。

基礎情報テンプレートの抽出は正規表現でできるかと思いきや、 {{ 〜 }}入れ子状態になっているため困難でした。
一般的な正規表現の定義だと入れ子の表現能力は無く、文脈自由文法でないと難しそうです。
Python正規表現も一般的な正規表現であるため入れ子は表現できません。*1
そのため、素直に入れ子の階層を数え上げていくという方法で実装しています。

続いて、フィールド名と値の抽出ですが、こちらもWikipediaのテンプレートに関するページによると、必ずしも |テンプレート変数 = 引数 が行頭であるわけではなさそうなので、それを考慮した実装にしました。
さらに、 |{{ 〜 }} 内や [[ 〜 ]] 内にも現れるので、同様に入れ子の階層を数え上げていくという方法をとりました。

別のパターンで試せていないのでミスがある可能性は大です。

26. 強調マークアップの除去

問題

25の処理時に,テンプレートの値からMediaWikiの強調マークアップ(弱い強調,強調,強い強調のすべて)を除去してテキストに変換せよ(参考: マークアップ早見表).

解答&出力

import re
from div03.sec20 import get_wiki_text
from div03.sec25 import extract_basic_info

bold_regex = re.compile(r"'''(.+?)'''")
italic_regex = re.compile(r"''(.+?)''")


def remove_emphasis(text):
    text = bold_regex.sub(r"\1", text)
    text = italic_regex.sub(r"\1", text)
    return text


if __name__ == "__main__":
    import pprint
    uk_text = get_wiki_text("イギリス")
    result_dict = extract_basic_info(uk_text)
    result_dict = {k: remove_emphasis(v) for k, v in result_dict.items()}
    pprint.pprint(result_dict, width=1000)
{'GDP/人': '36,727<ref name="imf-statistics-gdp" />',
 'GDP値': '2兆3162億<ref name="imf-statistics-gdp" />',
 'GDP値MER': '2兆4337億<ref name="imf-statistics-gdp" />',
 '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統計年': '2012',
 'GDP統計年MER': '2012',

〜

 '確立形態1': '[[イングランド王国]]/[[スコットランド王国]]<br />(両国とも[[連合法 (1707年)|1707年連合法]]まで)',
 '確立形態2': '[[グレートブリテン王国]]建国<br />([[連合法 (1707年)|1707年連合法]])',
 '確立形態3': '[[グレートブリテン及びアイルランド連合王国]]建国<br />([[連合法 (1800年)|1800年連合法]])',
 '確立形態4': '現在の国号「グレートブリテン及び北アイルランド連合王国」に変更',
 '通貨': '[[スターリング・ポンド|UKポンド]] (&pound;)',
 '通貨コード': 'GBP',
 '面積値': '244,820',
 '面積大きさ': '1 E11',
 '面積順位': '76',
 '首相等氏名': '[[デーヴィッド・キャメロン]]',
 '首相等肩書': '[[イギリスの首相|首相]]',
 '首都': '[[ロンドン]]'}

ひとこと

この問題でようやくマークアップ早見表を参考として出してくるのは遅いような...

Wikipediaの早見表を見ても弱い強調/強調/強い強調が何かわからなかったのですが、今回は、弱い強調は斜体、強調は太字、強い強調は斜体と太字であると見なして実装しました。
強い強調は、弱い強調と強調の組み合わせなので、弱い強調と強調の2種類を取り除くだけで実現できそうです。
後方参照による置換で '' 〜 ''''' 〜 ''' を取り除いています。

27. 内部リンクの除去

問題

26の処理に加えて,テンプレートの値からMediaWikiの内部リンクマークアップを除去し,テキストに変換せよ(参考: マークアップ早見表).

解答&出力

import re
from div03.sec20 import get_wiki_text
from div03.sec25 import extract_basic_info
from div03.sec26 import remove_emphasis

internal_link_with_text_regex = re.compile(r"\[\[[^\[\]\|\:]+?\|([^\[\]\|\:]+?)\]\]")
internal_link_without_text_regex = re.compile(r"\[\[([^\[\]\|\:]+?)\]\]")


def remove_internal_link(text):
    text = internal_link_with_text_regex.sub(r"\1", text)
    text = internal_link_without_text_regex.sub(r"\1", text)
    return text


def _remove(text):
    text = remove_emphasis(text)
    text = remove_internal_link(text)
    return text


if __name__ == "__main__":
    import pprint
    uk_text = get_wiki_text("イギリス")
    result_dict = extract_basic_info(uk_text)
    result_dict = {k: _remove(v) for k, v in result_dict.items()}
    pprint.pprint(result_dict, width=1000)
{'GDP/人': '36,727<ref name="imf-statistics-gdp" />',
 'GDP値': '2兆3162億<ref name="imf-statistics-gdp" />',
 'GDP値MER': '2兆4337億<ref name="imf-statistics-gdp" />',
 '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統計年': '2012',
 'GDP統計年MER': '2012',

〜

 '確立形態1': 'イングランド王国/スコットランド王国<br />(両国とも1707年連合法まで)',
 '確立形態2': 'グレートブリテン王国建国<br />(1707年連合法)',
 '確立形態3': 'グレートブリテン及びアイルランド連合王国建国<br />(1800年連合法)',
 '確立形態4': '現在の国号「グレートブリテン及び北アイルランド連合王国」に変更',
 '通貨': 'UKポンド (&pound;)',
 '通貨コード': 'GBP',
 '面積値': '244,820',
 '面積大きさ': '1 E11',
 '面積順位': '76',
 '首相等氏名': 'デーヴィッド・キャメロン',
 '首相等肩書': '首相',
 '首都': 'ロンドン'}

ひとこと

Wikipediaの早見表によると、内部リンクマークアップには [[記事名]] [[記事名|表示文字]] [[記事名#節名|表示文字]] があるようです。
[[記事名]] [[記事名|表示文字]] の2パターンについて、前者なら 記事名 後者なら 表示文字 だけを残すような実装にしました。

28. MediaWikiマークアップの除去

問題

27の処理に加えて,テンプレートの値からMediaWikiマークアップを可能な限り除去し,国の基本情報を整形せよ.

解答&出力

import re
from div03.sec20 import get_wiki_text
from div03.sec25 import extract_basic_info
from div03.sec26 import remove_emphasis
from div03.sec27 import remove_internal_link

external_link_with_text_regex = re.compile(r"\[https?://[^ ]+? (.*?)\]")
external_link_without_text_regex = re.compile(r"\[(https?://[^ ]+?)\]")

file_link_regex = re.compile(r"\[\[ファイル:([^\|]*?)\|[^\|]*?\|[^\|]*?\]\]")

br_tag_regex = re.compile(r"<br ?/>")
ref_tag_1_regex = re.compile(r"<ref.*?/>")
ref_tag_2_regex = re.compile(r"<ref.*?>(.|\s)*?</ref>")

lang_template_regex = re.compile(r"{{lang\|.+?\|(.+?)}}")


def remove_external_link(text):
    text = external_link_with_text_regex.sub(r"\1", text)
    text = external_link_without_text_regex.sub(r"\1", text)
    return text


def remove_file_link(text):
    text = file_link_regex.sub(r"\1", text)
    return text


def remove_tag(text):
    text = br_tag_regex.sub("", text)
    text = ref_tag_1_regex.sub("", text)
    text = ref_tag_2_regex.sub("", text)
    return text


def remove_lang_template(text):
    text = lang_template_regex.sub(r"\1", text)
    return text


def remove_all_markup(text):
    text = remove_emphasis(text)
    text = remove_internal_link(text)
    text = remove_external_link(text)
    text = remove_file_link(text)
    text = remove_tag(text)
    text = remove_lang_template(text)
    return text


if __name__ == "__main__":
    import pprint
    uk_text = get_wiki_text("イギリス")
    result_dict = extract_basic_info(uk_text)
    result_dict = {k: remove_all_markup(v) for k, v in result_dict.items()}
    pprint.pprint(result_dict, width=1000)
{'GDP/人': '36,727',
 'GDP値': '2兆3162億',
 'GDP値MER': '2兆4337億',
 'GDP値元': '1兆5478億',
 'GDP統計年': '2012',
 'GDP統計年MER': '2012',

〜

 '確立形態1': 'イングランド王国/スコットランド王国(両国とも1707年連合法まで)',
 '確立形態2': 'グレートブリテン王国建国(1707年連合法)',
 '確立形態3': 'グレートブリテン及びアイルランド連合王国建国(1800年連合法)',
 '確立形態4': '現在の国号「グレートブリテン及び北アイルランド連合王国」に変更',
 '通貨': 'UKポンド (&pound;)',
 '通貨コード': 'GBP',
 '面積値': '244,820',
 '面積大きさ': '1 E11',
 '面積順位': '76',
 '首相等氏名': 'デーヴィッド・キャメロン',
 '首相等肩書': '首相',
 '首都': 'ロンドン'}

ひとこと

実際のデータを眺めて取り除けそうなマークアップを全て取り除きました。
外部リンク、メディアファイル、タグ、言語テンプレート、それぞれについて、正規表現を用意し、置換により除去しています。

29. 国旗画像のURLを取得する

問題

テンプレートの内容を利用し,国旗画像のURLを取得せよ.(ヒント: MediaWiki APIのimageinfoを呼び出して,ファイル参照をURLに変換すればよい)

解答&出力

import requests
from div03.sec20 import get_wiki_text
from div03.sec25 import extract_basic_info
from div03.sec28 import remove_all_markup

uk_text = get_wiki_text("イギリス")
result_dict = extract_basic_info(uk_text)
result_dict = {k: remove_all_markup(v) for k, v in result_dict.items()}

flag_image = result_dict["国旗画像"]

url = "https://www.mediawiki.org/w/api.php"
params = {
    "action": "query",
    "titles": "File:" + flag_image,
    "prop": "imageinfo",
    "iiprop": "url",
    "format": "json",
}

response = requests.get(url, params=params)

if response.status_code == 200:
    result = response.json()
    try:
        urls = result["query"]["pages"]["-1"]["imageinfo"]
        for url in urls:
            print(url["url"])
    except KeyError:
        print("nothing")
else:
    print("error", response.status_code)
https://upload.wikimedia.org/wikipedia/commons/a/ae/Flag_of_the_United_Kingdom.svg

ひとこと

requestsライブラリを使っています。
pythonでhttpリクエストを簡単に実現するためには必須のライブラリです。
APIの使い方さえ分かれば簡単です。

おわりに

問題の解釈とどの程度妥協するか次第ではありますが、徐々に難易度は上がってきている気がします。
正規表現を書く練習にはなった気がします。

この調子で途中で挫折しないように続けていきます。

続き

ohshige.hatenablog.com

*1:入れ子を表現できるPythonパッケージもあるようです