言語処理100本ノック2020年版を解いてみた【第3章:正規表現 25〜29】

自身のQiita記事(こちら)の転載です。

東北大乾・岡崎研(現乾・鈴木研)で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『言語処理100本ノック 2020年版』をPython(3.7)で解いた記事の第6弾です。

独学でPythonを勉強してきたので、間違いやもっと効率の良い方法があるかもしれません。
改善点を見つけた際はご指摘いただけると幸いです。

第3章からは合っているかどうかな部分が多くなってきているので、改善点だけでなく、合っているかどうかという点もぜひご指摘ください。

ソースコードはGitHubにも公開しています。

第3章: 正規表現

Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある.

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

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

25. テンプレートの抽出

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

import pandas as pd
import re


def basic_info_extraction(text):
    texts = text.split("\n")
    index = texts.index("{{基礎情報 国")
    basic_info = []
    for i in texts[index + 1:]:
        if i == "}}":
            break
        if i.find("|") != 0:
            basic_info[-1] += ", " + i
            continue
        basic_info.append(i)

    pattern = r"\|(.*)\s=(.*)"
    ans = {}
    for i in basic_info:
        result = re.match(pattern, i)
        ans[result.group(1)] = result.group(2).lstrip(" ")
    return ans


file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query("title == 'イギリス'")['text'].values[0]

ans = basic_info_extraction(uk_text)
for key, value in ans.items():
    print(key, ":", value)

「基本情報」テンプレートを抽出するために、イギリスのテキストデータを引数にとるbasic_info_extraction()を定義しました。

この関数は"{{基礎情報 国"以降の行に対して}}の閉じカッコが現れるまでを基礎情報としてリストに保存します。
ただ、同じ行のデータでも複数行にまたがっている場合もありました。
その条件は行頭が|から始まっていたので、.find("|")で複数ヒットした場合はカンマで区切った上で1行にまとめる処理を行っています。

そして抽出したデータのリストに対して、|から=手前の半角スペースまでを「フィールド名」として、=以降を「値」として切り分け、「値」の左に空白があれば取り除いた上で辞書オブジェクトに保存し返却しています。

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

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

import pandas as pd
import re


def basic_info_extraction(text):
    # 「25. テンプレートの抽出」を参照
    ...


def remove_emphasis(value):
    pattern = r"(.*?)'{1,3}(.+?)'{1,3}(.*)"
    result = re.match(pattern, value)
    if result:
        return "".join(result.group(1, 2, 3))
    return value


file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query("title == 'イギリス'")['text'].values[0]

ans = basic_info_extraction(uk_text)
for key, value in ans.items():
    value = remove_emphasis(value) # 追加
    print(key, ":", value)

指定された強調マークアップを除去するための関数を用意しました。
強調マークアップは'が1〜3個連なったもので囲むことで表現されます。
そこで、正規表現のパターンとしてr"(.*?)'{1,3}(.+?)'{1,3}(.*)"を指定し、'以外を()で囲むことで、強調マークアップ以外を抽出。
マッチした部分をリスト化、joinメソッドで結合し値として返却しています。

27. 内部リンクの除去

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

import pandas as pd
import re


def basic_info_extraction(text):
    # 「25. テンプレートの抽出」を参照
    ...


def remove_emphasis(value):
    # 「26. 強調マークアップの除去」を参照
    ...


def remove_innerlink(value):
    pipe_pattern = r"(.*\[\[(.*?)\|(.+)\]\])"
    result = re.findall(pipe_pattern, value)
    if len(result) != 0:
        for i in result:
            pattern = "[[{}|{}]]".format(i[1], i[2])
            value = value.replace(pattern, i[2])
    pattern = r"(\[\[(.+?)\]\])"
    result = re.findall(pattern, value)
    if len(result) != 0:
        for i in result:
            if "[[ファイル:" not in value:
                value = value.replace(i[0], i[-1])
    return value


file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query("title == 'イギリス'")['text'].values[0]

ans = basic_info_extraction(uk_text)
for key, value in ans.items():
    value = remove_emphasis(value)
    value = remove_innerlink(value) # 追加
    print(key, ":", value)

次は内部リンクのマークアップを除去するための関数を用意しました。
内部リンクマークアップは[[〜]]の四角カッコを2個連ねて囲むことで表現されます。
パターンとしては中にパイプを入れて、表示文字と記事名を記載することもできます。
そこで、正規表現のパターンとしてr"(.*\[\[(.*?)|(.+)\]\])"を指定し、まずはパイプを含むリンクを抽出します。
該当する内部リンクを表示文字のみの内部リンクに置き換え、次の処理に渡します。

パイプを含む内部リンクがなくなり、表示される部分だけが残っているので、r"(\[\[(.+?)\]\])"でパターンを指定して該当する部分を探し、該当部分があればファイル以外をマークアップを除去して値を返却しています。

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

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

import pandas as pd
import re


def basic_info_extraction(text):
    # 「25. テンプレートの抽出」を参照
    ...


def remove_emphasis(value):
    # 「26. 強調マークアップの除去」を参照
    ...


def remove_innerlink(value):
    # 「27. 内部リンクの除去」を参照
    ...


def remove_footnote(value):
    # 後述
    ...


def remove_langage(value):
    # 後述
    ...


def remove_temporarylink(value):
    # 後述
    ....


def remove_zero(value):
    # 後述
    ...


def remove_br(value):
    # 後述
    ...


def remove_pipe(value):
    # 後述
    ...


file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query("title == 'イギリス'")['text'].values[0]

ans = basic_info_extraction(uk_text)
for key, value in ans.items():
    value = remove_footnote(value)
    value = remove_emphasis(value)
    value = remove_innerlink(value)
    value = remove_langage(value)       # 追加
    value = remove_temporarylink(value) # 追加 
    value = remove_zero(value)          # 追加
    value = remove_br(value)            # 追加
    value = remove_pipe(value)          # 追加
    print(key, ":", value)

脚注コメントの除去

def remove_footnote(value):
    pattern = r"(.*?)(<ref.*?</ref>)(.*)"
    result = re.match(pattern, value)
    if result:
        return "".join(result.group(1, 3))
    pattern = r"<ref.*/>"
    value = re.sub(pattern, "", value)
    return value

まず脚注の除去です。
引数として得た行が<ref〜</ref>を含んでいたら<ref〜</ref>の前後を結合して値を返します。
これに該当しない<ref〜/>という脚注表記も存在するので、これが残っていたら、取り除いて返却します。

言語タグの除去

def remove_langage(value):
    pattern = r"{{lang\|.*?\|(.*?)[}}|)]"
    result = re.match(pattern, value)
    if result:
        return result.group(1)
    return value

続いては言語タグです。
言語タグは{{lang〜}}で囲まれた部分です。
途中のパイプのあとに表示部分が含まれるので、正規表現中でカッコで囲んで、group()メソッドで抽出して値として返します。

仮リンクの除去

def remove_temporarylink(value):
    pattern = r"{{仮リンク\|.*\|(.*)}}"
    result = re.match(pattern, value)
    if result:
        return result.group(1)
    return value

仮リンクの除去は言語タグと若干パターンが違えど、ほぼ同じです。
{{仮リンク〜}}で囲まれた部分をパターンとし、マッチした部分があればgroup()メソッドで抽出です。

囲まれたゼロの除去

def remove_zero(value):
    pattern = r"\{\{0\}\}"
    value = re.sub(pattern, "", value)
    return value

よくわからない{{0}}というものが多々あったので、マッチした部分があれば空文字で置換します。

<br />の除去

def remove_br(value):
    pattern = r"<br />"
    value = re.sub(pattern, "", value)
    return value

末尾に改行タグ<br />が含まれていることがあったので、{{0}}同様、マッチしたら空文字で置換します。

パイプの除去

def remove_pipe(value):
    pattern = r".*\|(.*)"
    result = re.match(pattern, value)
    if result:
        return result.group(1)
    return value

パイプが残っている部分があるので、マッチしたら表記部分だけを返却します。

これらを一通り行うとマークアップを除去したことになります!

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

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

import pandas as pd
import re
import requests # 追加


def basic_info_extraction(text):
    # 「25. テンプレートの抽出」を参照
    ...


def remove_emphasis(value):
    # 「26. 強調マークアップの除去」を参照
    ...


def remove_innerlink(value):
    # 「27. 内部リンクの除去」を参照
    ...


def remove_footnote(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ...


def remove_langage(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ...


def remove_temporarylink(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ....


def remove_zero(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ...


def remove_br(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ...


def remove_pipe(value):
    # 「28. MediaWikiマークアップの除去」を参照
    ...


file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query("title == 'イギリス'")['text'].values[0]

ans = basic_info_extraction(uk_text)
for key, value in ans.items():
    value = remove_footnote(value)
    value = remove_emphasis(value)
    value = remove_innerlink(value)
    value = remove_langage(value)
    value = remove_temporarylink(value)
    value = remove_zero(value)
    value = remove_br(value)
    value = remove_pipe(value)
    uk_data[key] = value

S = requests.Session()
url = "https://en.wikipedia.org/w/api.php"
params = {
    "action": "query",
    "format": "json",
    "prop": "imageinfo",
    "titles": "File:{}".format(uk_data["国旗画像"])
}

R = S.get(url=url, params=params)
data = R.json()

pages = data["query"]["pages"]
for k, v in pages.items():
    print(v['imageinfo'][0]['url'])

28.までで整形したデータを用いて課題を解決します。
$ pip install requestsでインストールしたRequestsモジュールを使います。
使う必要はないかもしれないのですが、Sessionを用いています。
URLやパラメータを記述し、欲しいデータを取得します。
ここでは課題を解決するためのパラメータを指定しています。
あとはGetリクエストを送り、返ってきたデータを表示すれば完了です。

と言いたいところなのですが、表示したい画像のURLが取得したデータに含まれておらず、エラーになります。

間違っているのか、データが変わったのかまで調査できていないのが現状です…

まとめ

この記事では言語処理100本ノック2020年版 第3章: 正規表現の問題番号25〜29までを解いてみました。

データの整形はどこまですべきなのかとても迷いますよね…
きちんとできているかというのも確かめるのが大変なので、この辺が言語処理で一番難しいところだと個人的には感じております…

独学なので、プロの方とはコードの書き方がこのあたりで如実に差が出てくるのではと思っています。
ぜひより良い書き方をご教示いただければと思います。

よろしくお願いいたします!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です