言語処理100本ノック2015年版をJSで解いてみました。
第3章の正規表現編です。
http://www.cl.ecei.tohoku.ac.jp/nlp100/
第1章の記事はこちら
今回はajaxでファイルを読む形を取ったのでjqueryを使いブラウザで実行しています。
また、問20~29まで一まとまりの実装になっています。
全体ソースを記載したあと、各問ごとに必要な箇所を抜き出して説明を加えていきました。
※各問部分での見やすさを重視して、別問の処理を削って記載している箇所があります。
間違いがあればご指摘くださると幸いです。
では。
#第3章:正規表現編
/**
記事データを取得し整形
* @class MyWikipediaAPI
* @property {has} datas 記事タイトルをキーに持つ整形済みデータ
* @property {string} url 記事データのURL
* @param {strings} url 記事データのURL
*/
var MyWikipediaAPI = function(url){
this.datas = [];
this.url = url;
/**
* ajaxでdata取得
* @method getData
* @return jsonデータ
*/
this.getData = $.ajax({
url:this.url,
type:'GET',
cache:true,
context:this,
dataType:'text'
});
/**
* データの整形
* @method shaping
* @param {string} t 行分割したテキスト
*/
this.shaping = function(t){
//カテゴリ(問21,22)
t.categoles = t.text.match(/\[\[カテゴリ:[^\]]*?\]\]/g);
if(t.categoles){
for (var j = 0; j < t.categoles.length; j++) {
t.categoles[j] = t.categoles[j].replace(/\[\[カテゴリ:([^\]]*?)\]\]/,'$1');
}
}
//セクション(問23)
t.sections = t.text.match(/={2,} [^=]*? ={2,}/g);
if(t.sections){
for (var j = 0; j < t.sections.length; j++) {
_level = t.sections[j].replace(/(={2,}).+/,'$1').length - 1
t.sections[j] = {
level:_level,
name:t.sections[j].replace(/^={2,} (.*?) ={2,}$/,'$1')
}
}
}
//ファイル(問24)
t.files = t.text.match(/\[\[ファイル:[^\]]*?\]\]/g);
if(t.files){
for (var j = 0; j < t.files.length; j++) {
_datas = t.files[j].replace(/\[\[ファイル:([^\]]*?)\]\]/,'$1').split("|");
t.files[j] = {
name:_datas[0],
size:_datas[1],
alt:_datas[2]
}
}
}
//基礎情報(問25~29)
if(t.text.match(/{{基礎情報[\s\S]*?}}\n/g)){
//基礎情報スタート位置
var _lines = t.text.replace(/[\s\S]*{{基礎情報 国([\s\S]*)/g,'$1').replace(/\|\n/g,'\n|').split("\n|");
var _setLines = []
for (var j = 0; j < _lines.length; j++) {
var targetLine = _lines[j];
//問25
targetLine = targetLine.trim().replace('|','');
//基礎情報が終わるポイント
if(targetLine.indexOf("}}\n") == 0){
j = _lines.length + 1;
}else if(targetLine.indexOf("}}\n") > 0){
targetLine = targetLine.split("}}\n")[0];
j = _lines.length + 1;
}
if(j<_lines.length && targetLine != ""){
//問26
targetLine = targetLine.replace(/'{2,}/g,'');
//問27
var _reg = /\[\[(.*?)\]\]/g;
var _rep = "__reg__link__ptn__";
var _links = targetLine.match(_reg);
if(_links){
targetLine = targetLine.replace(_reg,_rep);
for (var k = 0; k < _links.length; k++) {
var _cnp = _links[k].replace(_reg,'$1').split("|");
targetLine = targetLine.replace(_rep,_cnp[_cnp.length - 1]);
}
}
//問28
targetLine = targetLine
.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')
.replace(/{{[^\|]*?\|/g,'')
.replace(/}}/g,'')
.replace(/\[[^\[]*?\]/g,'');
//キー配列に変換
var _name = targetLine.replace(/([^=]*?)=[\s\S]*/,'$1').trim();
var _val = targetLine.replace(/.*?=([\s\S]*)/,'$1').trim().replace(/\n/,'');
_setLines[_name] = _val;
}
}
t.baseData = _setLines;
}
this.datas[t.title] = t;
}
/**
* 国旗画像のデータを取得
* @method getFlagImg
* @param {string} name 国名
* @return jsonデータ
*/
this.getFlagImg = function(name){
if(this.datas[name].baseData["国旗画像"]){
return $.ajax({
url: 'https://en.wikipedia.org/w/api.php?action=query&titles=File:' + encodeURIComponent(this.datas[name].baseData["国旗画像"]) + '&prop=imageinfo&iiprop=url&format=json',
type:'get',
context:this,
dataType:'jsonp'
});
}
}
}
$(function(){
var myWikipedia = new MyWikipediaAPI('data/jawiki-country.json');
myWikipedia.getData.done(function(_data) {
var list = _data.split("\n");
for (var i = 0; i < list.length; i++) {
if(list[i] != ""){
var t = JSON.parse(list[i]);
this.shaping(t);
}
}
console.log(this.datas["イギリス"]);
myWikipedia.getFlagImg("イギリス").done(function(_data) {
for (var id in _data.query.pages) {
console.log(_data.query.pages[id].imageinfo[0].url);
}
});
});
});
20. JSONデータの読み込み
Wikipedia記事のJSONファイルを読み込み,「イギリス」に関する記事本文を表示せよ.問題21-29では,ここで抽出した記事本文に対して実行せよ.
this.getData = $.ajax({
url:this.url,
type:'GET',
cache:true,
context:this,
dataType:'text'
});
var myWikipedia = new MyWikipediaAPI('data/jawiki-country.json');
myWikipedia.getData.done(function(_data) {
var list = _data.split("\n");
for (var i = 0; i < list.length; i++) {
if(list[i] != ""){
var t = JSON.parse(list[i]);
this.shaping(t);
}
}
}
非同期なのでクラスの外からgetData()
を呼び、戻り値を待ってデータ整形用の関数shaping()
に渡しています。
もっと上手い実装があるかもしれません。
またこれは jquery の話ですがajax()
のオプションにcontext:this
を設定しておくとコールバックの処理内で自身のクラスが参照できます。
これをしないとコールバックの中でthis.shaping()
の様な参照が出来ません。
問21以降の整形処理は、shaping()
の中で行われます。
なんですが記事データのフォーマットが全体的にグダグダで中々面倒です。
そもそもjsonデータとしてjqueryに読ませるとパースエラーが出てしまいます。
プレーンなtextとして読み込みJSON.parse()
でjsonに変換しました。
21,22. カテゴリ名を含む行を抽出,カテゴリ名の抽出
記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.
記事中でカテゴリ名を宣言している行を抽出せよ.
//カテゴリ(問21,22)
t.categoles = t.text.match(/\[\[カテゴリ:[^\]]*?\]\]/g);
if(t.categoles){
for (var j = 0; j < t.categoles.length; j++) {
t.categoles[j] = t.categoles[j].replace(/\[\[カテゴリ:([^\]]*?)\]\]/,'$1');
}
}
t.text
に1記事のテキスト情報が格納されています。
match
で結果を配列にした物を順番に置換している感じです。
後方参照($1,$2...)で個数が確定していないものを上手に置換する方法ってあるんでしょうか。
あったら教えて欲しいです。
23. セクション構造
記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
//セクション(問23)
t.sections = t.text.match(/={2,} [^=]*? ={2,}/g);
if(t.sections){
for (var j = 0; j < t.sections.length; j++) {
_level = t.sections[j].replace(/(={2,}).+/,'$1').length - 1
t.sections[j] = {
level:_level,
name:t.sections[j].replace(/^={2,} (.*?) ={2,}$/,'$1')
}
}
}
levelとnameを持ったキー配列に格納しています。
24. ファイル参照の抽出
記事から参照されているメディアファイルをすべて抜き出せ.
//ファイル(問24)
t.files = t.text.match(/\[\[ファイル:[^\]]*?\]\]/g);
if(t.files){
for (var j = 0; j < t.files.length; j++) {
_datas = t.files[j].replace(/\[\[ファイル:([^\]]*?)\]\]/,'$1').split("|");
t.files[j] = {
name:_datas[0],
size:_datas[1],
alt:_datas[2]
}
}
}
置換した後にsplit()
で分割しキー配列に格納しています。
25. テンプレートの抽出
記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し,辞書オブジェクトとして格納せよ.
//基礎情報スタート位置
var _lines = t.text.replace(/[\s\S]*{{基礎情報 国([\s\S]*)/g,'$1').replace(/\|\n/g,'\n|').split("\n|");
var _setLines = []
for (var j = 0; j < _lines.length; j++) {
var targetLine = _lines[j];
//問25
targetLine = targetLine.trim().replace('|','');
//基礎情報が終わるポイント
if(targetLine.indexOf("}}\n") == 0){
j = _lines.length + 1;
}else if(targetLine.indexOf("}}\n") > 0){
targetLine = targetLine.split("}}\n")[0];
j = _lines.length + 1;
}
if(j<_lines.length && targetLine != ""){
//キー配列に変換
var _name = targetLine.replace(/([^=]*?)=[\s\S]*/,'$1').trim();
var _val = targetLine.replace(/.*?=([\s\S]*)/,'$1').trim().replace(/\n/,'');
_setLines[_name] = _val;
}
}
基礎情報の書式がグダグダで記事毎に差があり、自分には正規表現だけで実装する事が出来ませんでした。
一行毎に精査して基礎情報が終わる行を探しています。
キー配列変換時の置換に改行コードが含まれているものがあり、[\s\S]*
の形で正規表現を書きました。
26. 強調マークアップの除去
25の処理時に,テンプレートの値からMediaWikiの強調マークアップ(弱い強調,強調,強い強調のすべて)を除去してテキストに変換せよ(参考: マークアップ早見表)
//問26
targetLine = targetLine.replace(/'{2,}/g,'');
特になんの工夫もなく素直に置換です。
27. 内部リンクの除去
26の処理に加えて,テンプレートの値からMediaWikiの内部リンクマークアップを除去し,テキストに変換せよ(参考: マークアップ早見表).
//問27
var _reg = /\[\[(.*?)\]\]/g;
var _rep = "__reg__link__ptn__";
var _links = targetLine.match(_reg);
if(_links){
targetLine = targetLine.replace(_reg,_rep);
for (var k = 0; k < _links.length; k++) {
var _cnp = _links[k].replace(_reg,'$1').split("|");
targetLine = targetLine.replace(_rep,_cnp[_cnp.length - 1]);
}
}
これもマッチで配列にした後に、順番に置換していっています。
一度何にも被りそうにない_rep
にした文字列を整形した文字列に戻します。
結構怪しい実装ですね。
28. MediaWikiマークアップの除去
27の処理に加えて,テンプレートの値からMediaWikiマークアップを可能な限り除去し,国の基本情報を整形せよ.
//問28
targetLine = targetLine
.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')
.replace(/{{[^\|]*?\|/g,'')
.replace(/}}/g,'')
.replace(/\[[^\[]*?\]/g,'');
仕様が曖昧です。見る限り基本情報に残っているフォーマットはこんなもので消えるんじゃないかと。
HTMLタグは全部消してよかったのかな。
29. 国旗画像のURLを取得する
テンプレートの内容を利用し,国旗画像のURLを取得せよ.(ヒント: MediaWiki APIのimageinfoを呼び出して,ファイル参照をURLに変換すればよい)
/**
* 国旗画像のデータを取得
* @method getFlagImg
* @param {string} name 国名
* @return jsonデータ
*/
this.getFlagImg = function(name){
if(this.datas[name].baseData["国旗画像"]){
return $.ajax({
url: 'https://en.wikipedia.org/w/api.php?action=query&titles=File:' + encodeURIComponent(this.datas[name].baseData["国旗画像"]) + '&prop=imageinfo&iiprop=url&format=json',
type:'get',
context:this,
dataType:'jsonp'
});
}
}
myWikipedia.getFlagImg("イギリス").done(function(_data) {
for (var id in _data.query.pages) {
console.log(_data.query.pages[id].imageinfo[0].url);
}
});
Ajaxでの非同期処理を含むクラスの作りについては問20と同じ方法です。
一つのURLを取得するのにfor...inを使っているんですが、戻ってくるjsonの中で、キー名に任意の数字が使われている部分がある為の苦肉の策です。
例えばイギリスの国旗だと下記のjsonが返ってきます。
{
"batchcomplete": "",
"query": {
"pages": {
"23473560": {
"pageid": 23473560,
"ns": 6,
"title": "File:Flag of the United Kingdom.svg",
"imagerepository": "local",
"imageinfo": [
{
"url": "https://upload.wikimedia.org/wikipedia/en/a/ae/Flag_of_the_United_Kingdom.svg",
"descriptionurl": "https://en.wikipedia.org/wiki/File:Flag_of_the_United_Kingdom.svg"
}
]
}
}
}
}
問題はpagesの下にある、把握出来ていない任意の数字を持ったキー名です。
これが数字じゃなければquery.pages[0]
で参照出来るんですが、数字にされるとquery.pages[23473560]
にする必要があります。
意味のある数字をキー名に使うのは止めようと思いました。
もしかしたら文字列に変換して正規表現で置換した方が問題の意図に近いのかもしれません。
ソースはこちらにも置いて有ります。
https://github.com/pppp606/nlp100_2015
次は第4章ですが、ここら辺から本格的に言語処理になってきます。
用語の意味を調べながらになりそうです。
もうJSとか気にせずMeCabで解析結果を書き出してから始めるか、MeCabを使わずjsのライブラリ(最近話題のkuromoji.jsとか)を使ってみるか悩みながらやってみます。