始める前に
今回はタイトルに正規表現とあるように、Pythonの標準機能であるreへの理解を深めていくことになります。
そして、正規表現をあまり触れない人にとって、正規表現の視覚情報の見にくさと実装の独特な難しさが壁となってくるでしょう。
そういった、方でもわかるように今回はより丁寧に解説していきたいと思います。
また、reを扱うにあたって、記事を書きましたのでよかったらご覧ください。
第三章
20. JSONデータの読み込み
import json
#ファイルを開き、fに格納
with open("jawiki-country.json","r", encoding="utf-8") as f:
#ファイルから行ごとに取り出す
for line in f:
data=json.loads(line) #文字列をjsonとしてロードする
if data["title"]=="イギリス":
text=data["text"]
print(data["text"])
break
jawiki-country.jsonを開き、fに格納します。
その後、for line in fにより行ごとに取り出しますが、この段階ではただの文字列として扱われてしまっているので、json_loads()をつかいjsonとしてロードします。
今後も、この変数textは21-29を解く際に使っていきます。
21. カテゴリ名を含む行を抽出
for line in text.split():
if re.search(r'\[\[Category:.*\]\]', line):
print(line)
.split()はデフォルトでは、空白(スペース、タブ、改行など)をすべて区切ります。これをすることで、行単位で取り出します。
re.searchでは最初にマッチするパターンのみを返します。
カテゴリ名を含む行は文字列の一番最後に、[[Category:イギリス|*]] 、[[Category:イギリス連邦加盟国]]、[[Category:英連邦王国|*]]、、、のようになっています。
カテゴリを含む行の共通項としては、[[Category:○○○○]]であり、これに当てはまるパターンをif文で条件分岐させ、その行をそのまま返します。
正規表現のパターンの作り方は、[はただの文字として扱いたいため\でエスケープし、中身は.*で貪欲マッチさせます。
22. カテゴリ名の抽出
pattern = r'\[\[Category:(.*)\]\]'
categories=re.findall(pattern,text)
for category in categories:
print(category.replace("|*",""))
21を発展させて、その中身を取り出します。
re.findallはパターンにマッチする文字列をリストにして返します。キャプチャグループがあれば、そのグループをそれぞれ取り出しリストにして返します。
正規表現のパターンは21に対して、キャプチャグループ()で中身が取り出せるようにする必要があります。
また、.replace(“|*”,””)はCategory:イギリス|*のように「|*」を置換により削除しました。
23. セクション構造
pattern = r'(==+)\s*(.*?)\s*(==+)'
for s in re.finditer(pattern,text):
level=len(s.group(1))-1
print(f"セクション名: {s.group(2)} - レベル: {level}")
finditerは正規表現パターンに当てはまった文字列をmatchオブジェクトで返すイテレータです。
セクション構造を詳しく見ていくと、
== セクション名 == 1
===セクション名 === 2
==== セクション名 ==== 3
となっています。
そして、==とセクション名の間には、空白が存在する場合もあるので、\s*で空白を吸収する必要があります。
セクション名と=の数も数えるために、これらにキャプチャグループを適用します。
これらを考えると、’(==+)\s*(.*?)\s*(==+)‘というパターンになります。
24. ファイル参照の抽出
pattern=r'\[\[ファイル:(.*?)\|'
categories=re.findall(pattern,text)
for category in categories:
print(category)
メディアファイルとは、○○.jpgや○○.png,○○.svgのようなよくあるデータファイルのことです。
このテキスト内では、
[[ファイル:Royal Coat of Arms of the United Kingdom.svg|85px|イギリスの国章]]
のように‘[[ファイル:○○○○|○○○○|○○○○]]’形式になっています。
ファイル名を抜き出すために、‘[[ファイル:○○○○|‘にある正規表現を考えます。そうすると、ファイル名をキャプチャグループにして、‘\[\[ファイル:(.*?)\|’となります。
25. テンプレートの抽出
#①基礎情報をテキストから抜き出す
pattern = r'{{基礎情報.*?^(.*?)^}}'
#re.DOTALL → \nを.マッチに含める,re.MULTILINE →文字列の先頭・末尾ではなく、行列
template_match = re.search(pattern, text, re.DOTALL | re.MULTILINE)
#②抜き出した基礎情報から、フィールド名と値を取り出す
dict_pattern=r'\|(.+?)\s*=\s*(.+)'
country_dict={}
#re.findallは複数キャプチャグループがある場合、タプルで返す
for key,value in re.findall(dict_pattern, template_match.group(1)):
country_dict[key]=value
print(country_dict)
先に全体から基礎情報を抜き出した後、フィールドと値を取り出します。
基礎情報の形式は、
{{基礎情報 国
|略名 =イギリス
|日本語国名 = グレートブリテン及び北アイルランド連合王国
...
}}
のように’ |フィールド名 = 値 ‘なっています。
①基礎情報をテキストから抜き出す
基礎情報を抜き出すには、'{{基礎情報’から始まり ‘}}’で終わります。しかし、’}}’は他にも存在するため、これが行の最初にあることを利用します。
そして、実際に利用する辞書データは’|略名’からなので、ここをキャプチャグループにします。(基礎情報から|略名の間は.*?で吸収する)
ここで注意してもらいたいのが、「.」は本来改行(\n)を指定できず、「^」は文字列の先頭のみをマッチさせるので、行の先頭ではありません。
これを解決させる方法として、re.searchのフラグにre.DOTALL | re.MULTILINEをつける必要があります。
そうすると’{{基礎情報.*?^(.*?)^}}‘となり、re.searchにフラグも渡してmatchオブジェクトを取得します。
②抜き出した基礎情報から、フィールド名と値を取り出す
②では、行ごとにフィールド名と値が用意されているので、re.DOTALLを使いません。
フィールド名は非貪欲マッチ.+?にして、値は貪欲マッチで.+にします。(フィールド名を貪欲マッチにしてしまうと、=が複数ある場合できるだけ多くの=を含む可能性があります)
間の空白を吸収すると’\|(.+?)\s*=\s*(.+)’となります。
最後にre.findallはキャプチャグループが複数個あると、タプルを持った配列でデータを保持します。これを利用して、辞書としてデータを格納します。
26. 強調マークアップの除去
pattern = r'{{基礎情報.*?^(.*?)^}}'
#re.DOTALL → \nを.マッチに含める,re.MULTILINE →文字列の先頭・後ろではなく、行列
template_match = re.search(pattern, text, re.DOTALL | re.MULTILINE)
dict_pattern=r'\|(.+?)\s*=\s*(.+)'
country_dict={}
for key,value in re.findall(dict_pattern, template_match.group(1)):
value = re.sub(r"'{2,5}", "", value)#強調リンクの除去
country_dict[key]=value
print(value)
re.sub(“○○”,””)と置換することで文字を除去することができます。
re.sub(r”‘{2,5}”, “”, value)で、強調文字を削除しています。
27. 内部リンクの除去
pattern = r'{{基礎情報.*?^(.*?)^}}'
#re.DOTALL → \nを.マッチに含める,re.MULTILINE →文字列の先頭・後ろではなく、行列
template_match = re.search(pattern, text, re.DOTALL | re.MULTILINE)
dict_pattern=r'\|(.+?)\s*=\s*(.+)'
country_dict={}
for key,value in re.findall(dict_pattern, template_match.group(1)):
#強調リンクの除去
value = re.sub(r"'{2,5}", "", value)
# 内部リンクの除去 [[リンク先|表示文字]] → 表示文字
value = re.sub(r'\[\[(?!ファイル:)(?:[^|\]]*\|)?([^\]]+)\]\]', r'\1', value)
country_dict[key]=value
# 結果表示
print(country_dict)
[リンク先|表示名]] → 表示名[[リンク先]] → リンク先
のように内部リンクをテキストに取り出します。
ここで同じ形式の[[ファイル:Wikipedia-logo-v2-ja.png|thumb|説明文]]が存在するため、否定先読み(?!pattern)を使い(?!ファイル:)で、先に除外します。
[[リンク先|表示名]] を考えると、エスケープとキャプチャグループを使い\[\[(?:リンク先\|)(表示名)\]\]となります。
リンク先は[^|\]]*、表示名は[^\]]+となっています。少し読みにくいですが、|と]以外の文字をマッチさせているだけです。
この理由は、1行に複数個のリンクがある際の対策です。
例えば、[[○○|○○]]、[[○○|○○]]が一行にあるとします。もしリンク先を.*/とすると、リンク先は○○|○○]]、[[○○ 表示名は ○○となります。
これを解決するために、リンク先には|と ] の制限を行う事で○○のみになります。同時に表示名にも ] の制限をすることで別々にマッチさせることができます。
28. MediaWikiマークアップの除去
pattern = r'{{基礎情報.*?^(.*?)^}}'
#re.DOTALL → \nを.マッチに含める,re.MULTILINE →文字列の先頭・後ろではなく、行列
template_match = re.search(pattern, text, re.DOTALL | re.MULTILINE)
dict_pattern=r'\|(.+?)\s*=\s*(.+)'
country_dict={}
for key,value in re.findall(dict_pattern, template_match.group(1)):
#強調リンクの除去
value = re.sub(r"'{2,5}", "", value)
# 内部リンクの除去 [[リンク先|表示文字]] → 表示文字
value = re.sub(r'\[\[(?!ファイル:)(?:[^\]]*\|)?([^\]]+)\]\]', r'\1', value)
# 外部リンクの除去 [URL 表示文字] → 表示文字
value = re.sub(r'\[http[^>\]]+>([^]]+)\]', r'\1', value)
# ファイルリンクの除去 [[ファイル:xxx|...]] → ファイル名
value = re.sub(r'\[\[(?:ファイル):([^\]|]+)(?:[^\]]*)\]\]', r'\1', value)
# HTMLタグの除去 <ref>...</ref>, <br />, <small>...</small>
value = re.sub(r'<.*?>', '', value)
# {{lang|...}} の簡易除去(テンプレート内言語マークアップ)
value = re.sub(r'{{.*?}}', '', value)
country_dict[key]=value
print(country_dict)
27までの処理に以下を加えます。
ファイルリンクの除去
[[ファイル:Wikipedia-logo-v2-ja.png|thumb|説明文]]という形式から、\[\[(?:ファイル):(Wikipedia-logo-v2-ja.png)(?:|thumb|説明文)]]で\1でキャプチャグループを取り出し、ファイル名を取り出します。
27と同様に、キャプチャする位置と同行にある複数のファイルリンクを考えて、
\[\[(?:ファイル):([^\]|]+)(?:[^\]]*)\]\]
となります。
外部リンクの除去
[URL 表示文字] 形式になっているため\[url (表示文字)\]で表示文字のみにします。
[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]
>Data and Statistics>World Economic Outlook Databases>By Countrise>United Kingdom部分を表示名として取り出します。
re.subを使わなくても.replaceでできますが、今回は正規表現であえて表し、\[http[^>\]]+>([^]]+)\]を使い取り出します。
HTMLタグの除去
HTMLタグである<○○>を非貪欲マッチさせて””に置換させます。
{{lang|…}}テンプレート内言語マークアップの除去
{{lang|en|United Kingdom of Great Britain and Northern Ireland}}
{{lang|gd|An Rìoghachd Aonaichte na Breatainn Mhòr agus Eirinn mu Thuath}}
{{lang|cy|Teyrnas Gyfunol Prydain Fawr a Gogledd Iwerddon}}
{{lang|ga|Ríocht Aontaithe na Breataine Móire agus Tuaisceart na hÉireann}}
{{lang|kw|An Rywvaneth Unys a Vreten Veur hag Iwerdhon Glédh}}
{{lang|sco|Unitit Kinrick o Great Breetain an Northren Ireland}}
{{lang|sco|Claught Kängrick o Docht Brätain an Norlin Airlann}}
{{lang|sco|Unitet Kängdom o Great Brittain an Norlin Airlann}}
上記であるように言語ごとに、イギリスの公式国名が書かれています。つまり、{{lang|言語|公式国名}}になっているので、これを置換して除去します。
29. 国旗画像のURLを取得する
import requests
# 国旗ファイル名(例)
flag_filename = country_dict['国旗画像'] # 例: "Flag of the United Kingdom.svg"
flag_filename = "File:" + flag_filename
# セッション作成とUser-Agent設定
S = requests.Session()
S.headers.update({"User-Agent": 'CoolBot/0.0 (https://example.org/coolbot/; coolbot@example.org)'})
URL = "https://ja.wikipedia.org/w/api.php"
PARAMS = {
"action": "query",
"format": "json",
"prop": "imageinfo",
"titles": flag_filename,
"iiprop": "url" # 画像URLを取得
}
R = S.get(url=URL, params=PARAMS)
DATA = R.json()
PAGES = DATA["query"]["pages"]
for k, v in PAGES.items():
if "imageinfo" in v:
image_url = v["imageinfo"][0].get("url")
print(image_url) # 国旗画像URLだけ表示
else:
print(f"{flag_filename} has no imageinfo available")
https://www.mediawiki.org/wiki/API:Imageinfo/jaのPythonのサンプルコードを参考に実装していきます。
サンプルコードだけでは動かないので修正がいくつか必要です。
まず、セッション作成後S.headers.update({“User-Agent”: “MyWikipediaScript/1.0 (https://example.com)”})というコードを追加しました。
User-Agent とは、HTTPリクエストを送る際にサーバーに伝える「リクエスト元の情報」です。使用しているブラウザやOS、プログラム名・バージョンなどを示し、Pythonの requests では、headers に "User-Agent" を指定して送信します。
このhttps://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policyによれば、User-Agent ヘッダーを送信しないリクエストにはエラーを返してしまうとあります。これを解決するために、このサイトの下部にあるPythonのコードを利用します。
PARAMSでは、取得するファイル情報を選択できるiipropにurlを追加します。このUser-AgentとPARAMSを追加することで、requestは正しく実行できます。
最後に、レスポンスオブジェクトが複雑になっているので確認していきます。
Request URL:
{
"batchcomplete": "",
"query": {
"normalized": [
{
"from": "File:Billy_Tipton.jpg",
"to": "File:Billy Tipton.jpg"
}
],
"pages": {
"36266497": {
"pageid": 36266497,
"ns": 6,
"title": "File:Billy Tipton.jpg",
"imagerepository": "local",
"imageinfo": [
{
"timestamp": "2012-06-27T21:16:21Z",
"user": "Gobonobo"
}
]
}
}
}
}
レスポンスオブジェクトから.json()で上記のjsonデータを取得します。iipropにurlと指定したので、“query”:{“page”:{“○○”:{“imageinfo”:[{url: “○○”}]}}}となっています。
pageは複数のオブジェクトがあるため、これをイテレーションしてvのkeyにimageinfoがあれば分岐させ、階層構造に基づいて取得していきます。
まとめ
正規表現には、様々なパターンが問題も複雑です。
自然言語処理の前処理を読み、書けるようなるためにreや正規表現の基礎などをこれからも学習していきましょう。



コメント