言語処理100本ノック2015 をRubyでやる【第3章】
コードは GitHub に随時上げていきます。この記事では省略した長い出力も output/ ディレクトリに置いてます。
今回は第 3 章「正規表現」です。
Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある.
20. JSONデータの読み込み
Wikipedia記事のJSONファイルを読み込み,「イギリス」に関する記事本文を表示せよ.問題21-29では,ここで抽出した記事本文に対して実行せよ.
解答
# util.rb require 'zlib' require 'json' def gzip2hash(filename: 'jawiki-country.json.gz') articles = {} Zlib::GzipReader.open(filename) do |lines| lines.each do |line| article = JSON.parse(line) articles[article['title']] = article['text'] end end articles end
共通で使うメソッドとかを util.rb にまとめています。gzip は展開せずそのまま読みましたが、予め展開してても良さそうです。また以降の問題で イギリス
の出力しか使わないので、そこだけ別ファイルに保存して読み込むのも良さそうです。
# 20.rb require './util' puts gzip2hash["イギリス"]
出力(一部省略)
{{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|イギリスの国章]] |国章リンク = ([[イギリスの国章|国章]]) |標語 = {{lang|fr|Dieu et mon droit}}<br/>([[フランス語]]:神と私の権利) |国歌 = [[女王陛下万歳|神よ女王陛下を守り給え]] |位置画像 = Location_UK_EU_Europe_001.svg |公用語 = [[英語]](事実上) |首都 = [[ロンドン]] |最大都市 = ロンドン ...
21. カテゴリ名を含む行を抽出
記事中でカテゴリ名を宣言している行を抽出せよ.
解答
require './util' article = gzip2hash["イギリス"] article.each_line do |line| puts line if line.include?("Category") end
article は 1 つの長い文字列なので each_line
で 1 行ずつに切り分けて処理します。
出力
[[Category:イギリス|*]] [[Category:英連邦王国|*]] [[Category:G8加盟国]] [[Category:欧州連合加盟国]] [[Category:海洋国家]] [[Category:君主国]] [[Category:島国|くれいとふりてん]] [[Category:1801年に設立された州・地域]]
22. カテゴリ名の抽出
記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.
解答
require './util' article = gzip2hash["イギリス"] puts article.scan(/\[{2}Category:(.+?)(?:\|.*)*\]{2}/)
String#scan(pattern) -> Array
は、pattern とマッチする部分を文字列から全て取り出し、配列の要素として返します。正規表現は /
の間に書きます。
正規表現 | やくわり |
---|---|
\[{2} |
[ が 2 回連続 |
Category: |
Category: |
(.+?) |
1 文字以上の文字列 キャプチャする |
(?:ほげほげ) |
必要のために () を使うキャプチャしない |
|.* |
|ほげほげ〜 と続くもの(21 の出力を見て) |
(?:|.*)* |
|ほげほげ〜 と続くものがあってもなくても良いキャプチャしない |
\]{2} |
] が 2 回連続 |
キャプチャされた部分だけを集めた配列が scan
の返り値となります。
Ruby の正規表現はここで試し書きしています。 rubular.com
PHP・JS・Python・Go の場合はこのサイトが見やすくて好きです。 regex101.com
出力
イギリス 英連邦王国 G8加盟国 欧州連合加盟国 海洋国家 君主国 島国 1801年に設立された州・地域
23. セクション構造
記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
※ 自分がやった時はレベルが 1 ~ 4 だったんですが、今見たら 2 ~ 4 に修正されてました。ここには修正後の解答コードと出力を載せます。
解答
require './util' article = gzip2hash["イギリス"] article.scan(/(?<level>={2,})\s?(?<name>.+?)\s?(?:\k<level>)/).each do |level, name| puts [name, level.length].join(" ") end
正規表現 | やくわり |
---|---|
(?<level>={2,}) |
= が 2 回以上連続level という名前でキャプチャ |
\s? |
空白がないか 1 つ |
(?<name>.+?) |
1 文字以上の文字列 name という名前でキャプチャ |
(?:\k<level>) |
さっきマッチした level と同じもの キャプチャしない |
これだと空白文字の見出しもマッチするんですが、仕様がよくわからないのでとりあえずこれで……。
名前付きキャプチャはこの記事がわかりやすいです。
出力
国名 2 歴史 2 地理 2 気候 3 政治 2 外交と軍事 2 地方行政区分 2 主要都市 3 科学技術 2 経済 2 鉱業 3 農業 3 貿易 3 通貨 3 企業 3 交通 2 道路 3 鉄道 3 海運 3 航空 3 通信 2 国民 2 言語 3 宗教 3 婚姻 3 教育 3 文化 2 食文化 3 文学 3 哲学 3 音楽 3 イギリスのポピュラー音楽 4 映画 3 コメディ 3 国花 3 世界遺産 3 祝祭日 3 スポーツ 2 サッカー 3 競馬 3 モータースポーツ 3 脚注 2 関連項目 2 外部リンク 2
24. ファイル参照の抽出
記事から参照されているメディアファイルをすべて抜き出せ.
解答
require './util' article = gzip2hash["イギリス"] puts article.scan(/(?:ファイル|File):(.+?)\|/)
[[ファイル:Wiki.png|thumb|説明文]]
って感じなのですが、欲しいのはファイル名だけなので |
の手前で切ります。
出力
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 PalaceOfWestminsterAtNight.jpg Westminster Abbey - West Door.jpg Edinburgh Cockburn St dsc06789.jpg Canterbury Cathedral - Portal Nave Cross-spire.jpeg Kew Gardens Palm House, London - July 2009.jpg 2005-06-27 - United Kingdom - England - London - Greenwich.jpg Stonehenge2007 07 30.jpg Yard2.jpg Durham Kathedrale Nahaufnahme.jpg Roman Baths in Bath Spa, England - July 2006.jpg Fountains Abbey view02 2005-08-27.jpg Blenheim Palace IMG 3673.JPG Liverpool Pier Head by night.jpg Hadrian's Wall view near Greenhead.jpg London Tower (1).JPG Wembley Stadium, illuminated.jpg
25. テンプレートの抽出
記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し,辞書オブジェクトとして格納せよ.
解答
# util.rb def raw_template_to_hash(article) templates = {} article.match(/^\{{2}基礎情報.*?$\n(.+?)^\}{2}$/m) do |raw_template| raw_template[1].scan(/\|(?<key>.+?)\s*=\s*(?<val>.+?)(?:(?=\n$)|(?=\n\|))/m) do |key, val| templates[key] = block_given? ? yield(val) : val end end templates end
まずは /^\{{2}基礎情報.*?$\n(.+?)^\}{2}$/m
。複数行にわたる項目も含まれていて、それ込みで 1 回の正規表現で取得するためにがんばりました。
正規表現 | やくわり |
---|---|
^\{{2} |
行頭に { が 2 回連続 |
基礎情報.*?$\n |
基礎情報ほげほげ〜\n |
(.+?) |
1 文字以上の文字列 キャプチャ |
^\}{2}$ |
行頭に } が 2 回連続して、行末がくる |
m |
. が改行にもマッチするようにするこれがないと .+? は [\s\S]+ と書くことになる(デフォルトで . は改行を含まないため) |
/\|(?<key>.+?)\s*=\s*(?<val>.+?)(?:(?=\n$)|(?=\n\|))/m
について
正規表現 | やくわり |
---|---|
| |
| |
(?<key>.+?) |
1 文字以上の文字列 key という名前でキャプチャ |
\s*=\s* |
= の前後に空白がいくつあってもなくても良い |
(? |
1 文字以上の文字列 val という名前でキャプチャ |
ほげ(?=\n$) |
直後に \n$ がくる ほげ (肯定先読み) |
`(? |
(?=\n|))| 直後に \n$か \n|` がくる val先読み部分はキャプチャしない |
「{肯定,否定}{先,後}読み」の計 4 パターンあります。便利です。
# 25.rb require './util' article = gzip2hash["イギリス"] hash = raw_template_to_hash(article) hash.sort.to_h.each do |key, val| puts [key, val].join(" ") end
他の人と出力を揃えるためソートしています。
出力(一部省略)
... 最大都市 ロンドン 標語 {{lang|fr|Dieu et mon droit}}<br/>([[フランス語]]:神と私の権利) 水面積率 1.3% 注記 <references /> 略名 イギリス 確立年月日1 [[927年]]/[[843年]] 確立年月日2 [[1707年]] 確立年月日3 [[1801年]] 確立年月日4 [[1927年]] 確立形態1 [[イングランド王国]]/[[スコットランド王国]]<br />(両国とも[[連合法 (1707年)|1707年連合法]]まで) 確立形態2 [[グレートブリテン王国]]建国<br />([[連合法 (1707年)|1707年連合法]]) 確立形態3 [[グレートブリテン及びアイルランド連合王国]]建国<br />([[連合法 (1800年)|1800年連合法]]) 確立形態4 現在の国号「'''グレートブリテン及び北アイルランド連合王国'''」に変更 ...
26. 強調マークアップの除去
25の処理時に,テンプレートの値からMediaWikiの強調マークアップ(弱い強調,強調,強い強調のすべて)を除去してテキストに変換せよ(参考: マークアップ早見表).
解答
# util.rb def raw_template_to_hash(article) templates = {} article.match(/^\{{2}基礎情報.*?$\n(.+?)^\}{2}$/m) do |raw_template| raw_template[1].scan(/\|(?<key>.+?)\s*=\s*(?<val>.+?)(?:(?=\n$)|(?=\n\|))/m) do |key, val| templates[key] = block_given? ? yield(val) : val end end templates end class String def remove_emphasis self.gsub( /(?<emphasis>'{5}|'{3}|'{2})(?<content>.*?)\k<emphasis>/, '\k<content>' ) end end
25 ~ 28 の問題は大枠が一緒で、微妙に最後の処理が違うだけです。block_given?
で val に施す処理を受け取り、なければ val をそのまま返します。これ書いてて自分天才! って思ったのでかなり良い筋いってるんじゃないかな、どうですか? Ruby だいすき〜〜
特定の文字列を除去するメソッドは String クラスから生やすことにしました。早見表より、強調は '
が 2個か3個か5個連続する部分のペアです。
# 26.rb require './util' article = gzip2hash["イギリス"] hash = raw_template_to_hash(article) do |val| val.remove_emphasis end hash.sort.to_h.each do |key, val| puts [key, val].join(" ") end
25 と比較すると、raw_template_to_hash
に block が渡っています。
出力
... 最大都市 ロンドン 標語 {{lang|fr|Dieu et mon droit}}<br/>([[フランス語]]:神と私の権利) 水面積率 1.3% 注記 <references /> 略名 イギリス 確立年月日1 [[927年]]/[[843年]] 確立年月日2 [[1707年]] 確立年月日3 [[1801年]] 確立年月日4 [[1927年]] 確立形態1 [[イングランド王国]]/[[スコットランド王国]]<br />(両国とも[[連合法 (1707年)|1707年連合法]]まで) 確立形態2 [[グレートブリテン王国]]建国<br />([[連合法 (1707年)|1707年連合法]]) 確立形態3 [[グレートブリテン及びアイルランド連合王国]]建国<br />([[連合法 (1800年)|1800年連合法]]) 確立形態4 現在の国号「グレートブリテン及び北アイルランド連合王国」に変更 ...
27. 内部リンクの除去
26の処理に加えて,テンプレートの値からMediaWikiの内部リンクマークアップを除去し,テキストに変換せよ(参考: マークアップ早見表).
解答
# util.rb class String def remove_fileinfo self.gsub( /(?:ファイル|File):(?<filename>.+?)\|/, '\k<filename>' ) end def remove_innerlink self.gsub( /\[{2}(?:[^|]*?\|?)(?<link>[^|]*?)\]{2}/, '\k<link>' ) end end
remove_fileinfo
は内部リンクを除去するのに邪魔だったので先に消しておく用です。
remove_innerlink
がちょっと複雑です。内部リンクは ① [[記事名]]
② [[記事名|表示文字]]
③ [[記事名#節名|表示文字]]
の 3 パターンあるのですが、後ろ側を取得(①なら 記事名
、②③なら 表示文字
)するようにしました。
正規表現 | やくわり |
---|---|
\[{2} |
[ が 2 回連続 |
`[^ | ]*| |` じゃないやつがいくつあってもなくても良い |
`[^ | ]*?|?| |じゃないやつがいくつあってもなくても良くて、 |がないか 1 つだけある<br>1 つ目の ?` は最短マッチ |
greedy(最長、貪欲)とかlazy(最短)とかpossessive(強欲)があるみたいです。長さの観点からか欲の観点からか、名前統一できたらいいのに感があります……。
# 27.rb require './util' article = gzip2hash["イギリス"] hash = raw_template_to_hash(article) do |val| val.remove_emphasis .remove_fileinfo .remove_innerlink end hash.sort.to_h.each do |key, val| puts [key, val].join(" ") end
ブロックの中で除去の処理をメソッドチェーンして書いてます。
出力
... 最大都市 ロンドン 標語 {{lang|fr|Dieu et mon droit}}<br/>(フランス語:神と私の権利) 水面積率 1.3% 注記 <references /> 略名 イギリス 確立年月日1 927年/843年 確立年月日2 1707年 確立年月日3 1801年 確立年月日4 1927年 確立形態1 イングランド王国/スコットランド王国<br />(両国とも1707年連合法まで) 確立形態2 グレートブリテン王国建国<br />(1707年連合法) 確立形態3 グレートブリテン及びアイルランド連合王国建国<br />(1800年連合法) 確立形態4 現在の国号「グレートブリテン及び北アイルランド連合王国」に変更 ...
28. MediaWikiマークアップの除去
解答
# util.rb class String def remove_link self.gsub( /\[\S+\s?(?<title>[^\[\]]+)\]/, '\k<title>' ) end def remove_tag self.gsub( /\<(?:[^\<\>])+\>/, '' ) end def remove_template self.gsub( /\{{2}(?:.*)\|(?<lang_text>.*)\}{2}/, '\k<lang_text>' ) end end
外部リンクは ① [http://www.example.org]
② [http://www.example.org 表示文字]
③ http://www.example.org
の 3 パターンあって、①完全削除、②表示文字だけ残す、③そのまま残す、とします。
# 28.rb require './util' article = gzip2hash["イギリス"] hash = raw_template_to_hash(article) do |val| val.remove_emphasis .remove_fileinfo .remove_innerlink .remove_link .remove_tag .remove_template end hash.sort.to_h.each do |key, val| puts [key, val].join(" ") end
出力
最大都市 ロンドン 標語 Dieu et mon droit(フランス語:神と私の権利) 水面積率 1.3% 注記 略名 イギリス 確立年月日1 927年/843年 確立年月日2 1707年 確立年月日3 1801年 確立年月日4 1927年 確立形態1 イングランド王国/スコットランド王国(両国とも1707年連合法まで) 確立形態2 グレートブリテン王国建国(1707年連合法) 確立形態3 グレートブリテン及びアイルランド連合王国建国(1800年連合法)
29. 国旗画像のURLを取得する
テンプレートの内容を利用し,国旗画像のURLを取得せよ.(ヒント: MediaWiki APIのimageinfoを呼び出して,ファイル参照をURLに変換すればよい)
解答
require 'open-uri' require 'json' require './util' article = gzip2hash["イギリス"] hash = raw_template_to_hash(article) do |val| val.remove_emphasis .remove_fileinfo .remove_innerlink .remove_link .remove_tag .remove_template end filename = hash["国旗画像"] wikipedia_api = "http://ja.wikipedia.org/w/api.php?" params = { action: "query", titles: "Image:" + filename, prop: "imageinfo", iiprop: "url", format: "json", formatversion: "2" } begin uri = URI.parse(wikipedia_api) uri.query = URI.encode_www_form(params) json_res = JSON.parse(uri.open.read) puts json_res['query']['pages'][0]['imageinfo'][0]['url'] rescue => e puts e end
URI.encode_www_form
で params を application/x-www-form-urlencoded 形式に変換してくれます。action=query&titles=...
みたいなやつです。filename に空白が含まれるので、そこも +
に変換されます。
出力
https://upload.wikimedia.org/wikipedia/commons/a/ae/Flag_of_the_United_Kingdom.svg
正規表現たのしいです。たぶんもっと良く書けると思うので、数ヶ月後書き直したいです。1 行が長すぎるのも良くない。
先読み・後読みの話が出てきました。むかし CSS で肯定先読みぽいこと(子要素に .hoge
を持つ親要素にだけ特定のスタイルを当てたい! とか。破綻気味なのは気にしない……)したくて試行錯誤してたなぁと思い出しました。
もっと良い書き方あるよ〜などあれば issue とかで教えてもらえるとうれしいです!