はじめに
前回の言語処理100ノックの続きです。
今回はNLP100ノックの第3章に挑戦です。
3章には正規表現に関する10問のお題(No20~No29)があります。
今回は着手したNo20~No22を投稿します。 残りはできるところからメンテ投稿していきたいと思います。
私は現場では正規表現が必要になると若手に依頼して自分ではずっと避けていた課題でした。
3章は不得手な正規表現に加えてWikipediaのテキスト形式についての知識不足も問題の難易度を上げていました。
取り組みに当たっては先達の方々の投稿を参考にさせていただきましたが
AI(claude,gemini,copilot)も利用しました。(claudeをメインに使っています)
環境
LIPS Schemeを使ってみました8(言語処理100本ノック 1章)を参照してください。
プログラム構成
プログラムは2本のファイルに実装します。
- nlp100_chp3.scm ・・ 課題の処理を実装します。
- nlp100_chp3_main.scm ・・ nlp100_chp3.scmで実装した課題コードを実行する関数です。
20. JSONデータの読み込み
Wikipedia記事のJSONファイルを読み込み、「イギリス」に関する記事本文を表示せよ。問題21-29では、ここで抽出した記事本文に対して実行せよ。
コア処理はすべてJSまたはnodeのライブラリを使いました。
ZIPを解凍しながらパイプストリームに流し込んで記事のテキストを抽出する処理にしました。
(オリジナル案はJSベースのコードでclaudさんからいただきました。ヽ〔゚Д゚〕丿 スゴイ)
nlp100_chp3.scmに問題20の処理コードを実装します。
;; モジュールインポート
(define zlib (require "zlib"))
(define fs (require "fs"))
(define readline (require "readline"))
;; ===== 問題20: JSONデータの読み込み =====
(define (load-uk-article file-path)
(new Promise
(lambda (resolve reject)
(let* (
(gunzip (zlib.createGunzip))
(input-stream (fs.createReadStream file-path))
(piped (input-stream.pipe gunzip))
(rl (readline.createInterface `&(:input ,piped)) ))
(rl.on "line"
(lambda (line)
(display ".") ;"."で進捗状況を表示
(let ((obj (JSON.parse line)))
(when (equal? obj.title "イギリス")
(newline)
(resolve obj.text)
(rl.close)
))))
(rl.on "close"
(lambda () (resolve #null)))
(rl.on "error"
(lambda () (console.log "rejected!") (reject)))
) ; --- end of (let* ()) ---
) ; ---- end of (lambda ()) ---
) ; ---- end of (new Promise()) ---
)
nlp100_chp3_main.scmに問題20の実行コードを実装します。
;; テスト関数実行関数
(define (test-exec test-fnc)
(let* (
(file-path "../jawiki-country/jawiki-country.json.gz")
(text (await (load-uk-article file-path)))
;; (text (load-uk-article file-path))
)
(if (or (null? text) (equal? text #null))
(console.error "イギリスの記事が見つかりません")
(test-fnc text) ; 課題処理コード呼びだし
)
)
)
;; 問題No20実行
(define (test-no20 text) (display text))
;; 実行結果 =>
;; "{{redirect|UK}}
;; {{redirect|英国|春秋時代の諸侯国|英 (春秋)}}
;; {{Otheruses|ヨーロッパの国|長崎県・熊本県の郷土料理|いぎりす}}
;; {{基礎情報 国
;; |略名 =イギリス
;; ---- 中略 ----
;; [[Category:現存する君主国]]
;; [[Category:島国]]
;; [[Category:1801年に成立した国家・領域]]"
21. カテゴリ名を含む行を抽出
- 記事本文のカテゴリの形式:
[[Category:カテゴリ名]]
[[Category:カテゴリ名|ソートキー]]
-
正規表現:
/\[\[Category:.+\]\]/パーツ 意味 [[ 文字としての [[ にマッチさせる。 Category: 文字列 Category: にそのままマッチ。 .+ 任意の1文字(.)が1回以上(+)繰り返される。 ]] 文字としての ]] にマッチさせる。 -
動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
出力は行単位で返さなければならないので行分割(split)したマッチング結果をフィルターして返します。(let* ( (sample "hoge\n[[Category:い|*]]うしろ\nまえ[[Category:ろ]]\nfuga\n[[Category:は]]") (regx #/\[\[Category:.+\]\]/) ;;出力は行単位で返さなければならないので行分割(split)したマッチング結果をフィルターして返します。 (result (filter (lambda (line) (match regx line)) (split "\n" sample))) ) (print (car result)) ;=> 1行目: [[Category:い|*]]うしろ (print (cadr result)) ;=> 2行目: まえ[[Category:ろ]] (print (caddr result)) ;=> 3行目: [[Category:は]] )
nlp100_chp3.scmに問題21の処理コードを実装します。
(define (category-lines text)
(filter
(lambda (line)
(match #/\[\[Category:.+\]\]/ line)
)
(split "\n" text)
)
)
nlp100_chp3_main.scmに問題21の実行コードを実装します。
;; 問題No21実行
(define (test-no21 text)
(for-each (lambda (line) (print line)) (category-lines text))
)
;; テスト実行
(test-exec test-no21)
;; 実行結果 =>
;; [[Category:イギリス|*]]
;; [[Category:イギリス連邦加盟国]]
;; [[Category:英連邦王国|*]]
;; [[Category:G8加盟国]]
;; [[Category:欧州連合加盟国|元]]
;; [[Category:海洋国家]]
;; [[Category:現存する君主国]]
;; [[Category:島国]]
;; [[Category:1801年に成立した国家・領域]]
22. カテゴリ名の抽出
記事のカテゴリ名を(行単位ではなく名前で)抽出せよ。
[[Category:・・カテゴリ名・・]]から・・カテゴリ名・・を正規表現で抽出する。
-
正規表現:
/\[\[Category:.+\]\]/パーツ 意味 [[ マークアップの [[ にマッチさせる。 Category: 文字列 Category: にそのままマッチ。 .+ 任意の1文字(.)が1回以上(+)繰り返される。 ([^\]\|]+) キャプチャグループ(後述) -
キャプチャーグルーブ ([^\]|]+):
パーツ 意味 (...) マッチした部分をキャプチャ(結果から取り出せる) [^\]\| ]+ 否定文字クラス (「]」、「|」以外の一つ以上の文字 ) -
動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
キャプチャーグルーブを扱うためJSのmatchAllとArray.fromを使います。(let* ( (sample "hoge\n[[Category:い|*]]うしろ\nまえ[[Category:ろ]]\nfuga\n[[Category:は]]") (regex #/\[\[Category:([^\]|]+)/g) (result (Array.from (--> sample (matchAll regex)) (lambda (m) m.1))) ) (print result) ; => #(い ろ は) )
nlp100_chp3.scmに問題22の処理コードを実装します。
結果を配列(vector)として返します。
(define (category-names text)
(let ((category-regex #/\[\[Category:([^\]|]+)/g ))
(Array.from (--> text (matchAll category-regex)) (lambda (m) m.1))
)
)
nlp100_chp3_main.scmに問題22の実行コードを実装します。
;; 問題No22実行
(define (test-no22 text)
;; イギリス記事からカテゴリ名を抽出して表示
(console.log (category-names text))
)
;; テスト実行
(test-exec test-no22)
// 実行結果 =>
// [
// 'イギリス',
// 'イギリス連邦加盟国',
// '英連邦王国',
// 'G8加盟国',
// '欧州連合加盟国',
// '海洋国家',
// '現存する君主国',
// '島国',
// '1801年に成立した国家・領域'
// ]
23. セクション構造
記事中に含まれるセクション名とそのレベル(例えば”== セクション名 ==”なら1)を表示せよ。
https://nlp100.github.io/2025/ja/ch03.html#id4
-
見出しのマークアップ
記法 Level =の数 == 見出し == 1 2 === 見出し === 2 3 ==== 見出し ==== 3 4 -
正規表現:
#/^(={2,})\s*(.+?)\s*\1\s*$/gmパーツ 意味 ^(={2,}) 行頭から始まる == 以上の = の連続をキャプチャ (グルーブ1) \s*(.+?)\s* 前後の空白を除いた見出しテキストをキャプチャ(グルーブ2) \1 開始(グルーブ1)と同じ数の = で閉じることを保証(対称チェック) \s*$ 末尾空白または末尾 gm フラグ 複数行マッチ & 行末行頭(^, $)マッチ -
動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
キャプチャーグルーブを扱うためJSのmatchAllとArray.fromを使います。
(Array.from
(--> "ほげ\n==歴史==\n{{ほげの歴史}}" (matchAll #/^(={2,})\s*(.+?)\s*\1\s*$/gm))
(lambda (m) (console.log "level:" (- (string-length m.1) 1) ",title:" m.2))
) ;=> level: 1 ,title: 歴史
nlp100_chp3.scmに問題23の処理コードを実装します。
JSのexec関数を使うこともできますがループ処理が必要なのでmatchAllにしました。
;; ===== 問題23. セクション構造 =====
;; 記事中に含まれるセクション名とそのレベル(例えば”== セクション名 ==”なら1)を表示せよ。
(define (sections text)
(let ((regex #/^(={2,})\s*(.+?)\s*\1\s*$/gm ))
(Array.from (--> text (matchAll regex))
(lambda (m)
(let (
(level (- (string-length m.1) 1)) ; レベル算出(==の文字数)
(title m.2) ; セクション名
)
`&(:level ,level :title ,title) ;object型にして返す
)
)
)
)
)
nlp100_chp3_main.scmに問題23の実行コードを実装します。
;; 問題No23実行
(define (test-no23 text)
(console.log (sections text))
)
;; テスト実行
(test-exec test-no23)
;; 実行結果 =>
;; [
;; { level: 1, title: '国名' },
;; { level: 1, title: '歴史' },
;; ~~ 中略 ~~
;; { level: 2, title: '主要都市' },
;; ~~ 中略 ~~
;; { level: 3, title: '通信' },
;; ~~ 中略 ~~
;; { level: 1, title: '外部リンク' }
;; ]
24. ファイル参照の抽出
記事から参照されているメディアファイルをすべて抜き出せ。
https://nlp100.github.io/2025/ja/ch03.html#id5
-
ファイルのマークアップ
[[ファイル:Wikipedia-logo-v2-ja.png|thumb|説明文]] -
正規表現:
#/\[\[ファイル:([^\|\]]+)/gパーツ 意味 [[ [[ のエスケープ ファイル: "ファイル:"に一致する文字列 ([^|\]]+) [^|\]]+にマッチするキャプチャ (下記参照) [^|\]]+ []内のいずれかの文字(|または]以外の文字)をマッチする1文字以上の文字(文字クラス) g 全検索マッチ -
動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
(let (
(text "|国章画像 = [[ファイル:ふが Kingdom.svg|85px|ふがの国章]]\n|国章リンク =([[ふがの国章|国章]])\n")
(regex #/\[\[ファイル:([^\|\]]+)/g)
)
(console.log (Array.from (--> text (matchAll regex)) (lambda (m) m.1)))
)
;; => [ 'ふが Kingdom.svg' ]
nlp100_chp3.scmに問題24の処理コードを実装します。
;; ===== 問題24. ファイル参照の抽出 =====
;; [[ファイル:~~~~]]の内容( ([^\|\]]+) )を抽出する。
(define (mediaFiles text)
(let ((regex #/\[\[ファイル:([^\|\]]+)/g ))
(Array.from (--> text (matchAll regex)) (lambda (m) m.1))
)
)
nlp100_chp3_main.scmに問題24の実行コードを実装します。
; 問題No24実行
(define (test-no24 text)
(console.log (mediaFiles text))
)
;; テスト実行
(test-exec test-no24)
;; 実行結果 =>
;; [
;; 'Royal Coat of Arms of the United Kingdom.svg',
;; 'United States Navy Band - God Save the Queen.ogg',
;; 'Descriptio Prime Tabulae Europae.jpg',
;; ~~ 中略 ~~
;; 'The Fabs.JPG',
;; 'Wembley Stadium, illuminated.jpg'
;; ]
25. テンプレートの抽出
記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し、辞書オブジェクトとして格納せよ。
https://nlp100.github.io/2025/ja/ch03.html#id6
- 「基礎情報」テンプレートのマークアップ
{{基礎情報 国
|略名 = イギリス
|日本語国名 = グレートブリテン及び北アイルランド連合王国
|画像旗 = Flag of the United Kingdom.svg
...
}}
問題25は2ステップの正規表現でプログラムを構成します。
①「基礎情報」テンプレートのテキストを抽出する。
②「基礎情報」テンプレートのテキストからフィールド名と値を抽出する。
-
「①「基礎情報」テンプレートのテキストを抽出する。」の正規表現:
/\{\{基礎情報[^\n]*\n(.*?)\n\}\}/sパーツ 意味 \{\{基礎情報 {{基礎情報 にマッチ({ はエスケープ必要) [^\n]* テンプレート名の残りを読み飛ばす(国、都市 など) \n 最初の改行 ([\s\S]*?) テンプレート本体をキャプチャ([\s\S] で改行も含む、さらに?で最短マッチ) \n}} 閉じタグ }} の直前の改行ごとマッチ -
「①「基礎情報」テンプレートのテキストを抽出する。」の動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。(let* ( ;; (text "{{基礎情報 国\n|略名 = いぎ\n|日本語国名 = グレいぎ王国\n|画像旗 = IgiKingdom.svg\n..ああ..\n}}") (text "{{基礎情報 国\n|略名 = いぎ\n|日本語国名 = グレいぎ王国\n|公式国名 = {{lang|en|hello world}}<ref>テスト名:<br />\n*{{lang|A Test}}([[テスト語]])\n**{{lang|sco|Unitet Test}}(アルスター・テスト語)</ref>\n}}") (regex #/\{\{基礎情報[^\n]*\n([\s\S]*?)\n\}\}/) (templ-info (vector-ref (--> text (match regex)) 1)) ;配列の1(キャプチャでマッチした部分)を取り出す ) ;; (console.log (--> text (match regex))) (print templ-info) (print "------") ;; => ;; |略名 = いぎ ;; |日本語国名 = グレいぎ王国 ;; |公式国名 = {{lang|en|hello world}}<ref>テスト名:<br /> ;; *{{lang|A Test}}([[テスト語]]) ;; **{{lang|sco|Unitet Test}}(アルスター・テスト語)</ref> ) -
「②「基礎情報」テンプレートのテキストからフィールド名と値を抽出する。」の正規表現:
/^\|([^=\n]+?)\s*=\s*([\s\S]*?)(?=\n\||\n?$)/gm
/^\|([^=\n]+?)\s*=\s*((?:(?!\n\|)[\s\S])*)/gm
正規表現はgeminiに作ってもらいましたが、パーツ構造は下のようになります。- 行頭の特定とパイプ: /
^\|([^=\n]+?)\s*=\s*((?:(?!\n|)[\s\S])*)/gm - 引数名(キャプチャグループ1): /^\|
([^=\n]+?)\s*=\s*((?:(?!\n|)[\s\S])*)/gm - 区切り文字: /^\|([^=\n]+?)
\s*=\s*((?:(?!\n|)[\s\S])*)/gm - 設定値(キャプチャグループ2): /^\|([^=\n]+?)\s*=\s*
((?:(?!\n\|)[\s\S])*)/gm - フラグ: /^\|([^=\n]+?)\s*=\s*((?:(?!\n|)[\s\S])*)
/gm
パーツ 意味 ^\| 各行頭でフィールドの区切り文字マッチング(mフラグが必要) ([^=\n]+?) フィールド名("="と改行を含まない文字列)をキャプチャ \s*=\s* "=" とその前後の空白 ~~ ([\s\S]*?) ~~ ~~ 値(改行含む)を最短でキャプチャ ~~ ~~ (?=\n\||\n?$) ~~ ~~ 先読みで「次の | 行の直前」か「末尾」まで ~~ ((?:(?!\n|)[\s\S])*) <値部分のキャプチャ>
1. (?: ... )*:
全体を繰り返しますが、このグループ自体は番号付きのキャプチャを行いません(非キャプチャグループ)。
2. (?!\n|):
先行否定先読みです(?!)。「この直後の位置が『改行+パイプ』という並びではないこと」を1文字ごとにチェックします。
3. [\s\S]:
改行を含むあらゆる1文字にマッチします。gmフラグ 各行全フィールドを繰り返し抽出 - 行頭の特定とパイプ: /
-
「②「基礎情報」テンプレートのテキストからフィールド名と値を抽出する。」の動作確認
;; (let* (
;; (text "|略名 = いぎ\n|日本語国名 = グレいぎ王国\n|画像旗 = IgiKingdom.svg\n..ああ..")
;; (regex #/^\|([^=\n]+?)\s*=\s*([\s\S]*?)(?=\n\||\n?$)/gm)
;; (matches (--> Array (from (--> text (matchAll regex)))))
;; )
;; ;; 抽出したフィールド名と値を連想ペアの配列にする
;; (print (vector-map (lambda (m)(cons m.1 m.2)) matches))
;; )
;; ;; =>
;; ;; #((略名 . いぎ) (日本語国名 . グレいぎ王国) (画像旗 . IgiKingdom.svg))
(let* (
(text "{{基礎情報 国\n|略名 = いぎ\n|日本語国名 = グレいぎ王国\n|公式国名 = {{lang|en|hello world}}<ref>テスト名:<br />\n*{{lang|A Test}}([[テスト語]])\n**{{lang|sco|Unitet Test}}(アルスター・テスト語)</ref>\n}}")
(regex #/\{\{基礎情報[^\n]*\n([\s\S]*?)\n\}\}/) ; fixed
(templ-info (vector-ref (--> text (match regex)) 1)) ;配列の1(キャプチャでマッチした部分)を取り出す
)
(print templ-info)
(print "------")
(let* (
(regex2 #/^\|([^=\n]+?)\s*=\s*((?:(?!\n\|)[\s\S])*)/gm)
(matches (--> Array (from (--> templ-info (matchAll regex2)))))
)
;; 抽出したフィールド名と値を連想ペアの配列にする。その結果をprint出力する。
(vector-for-each
(lambda (pair) (print pair))
(vector-map (lambda (m)(cons m.1 m.2)) matches)
)
;; =>
;; (略名 . いぎ)
;; (日本語国名 . グレいぎ王国)
;; (公式国名 . {{lang|en|hello world}}<ref>テスト名:<br />
;; *{{lang|A Test}}([[テスト語]])
;; **{{lang|sco|Unitet Test}}(アルスター・テスト語)</ref>)
)
)
- 問題25の実装
nlp100_chp3.scmに問題25の処理コードを実装します。
;; 25. テンプレートの抽出
;; 記事中に含まれる「基礎情報」テンプレートのフィールド名と値(ex. "|フィールド名=値")を抽出し、辞書オブジェクトとして格納せよ。
(define (dic-obj text)
(let* (
;; ① テンプレート全体の抽出
(regex1 #/\{\{基礎情報[^\n]*\n([\s\S]*?)\n\}\}/)
(templ-info (--> text (match regex1)) )
)
(if (not (null? templ-info))
(let* (
;; ② フィールド名・値の抽出
;; (regex2 #/^\|([^=\n]+?)\s*=\s*([\s\S]*?)(?=\n\||\n?$)/gm) ;バグ: 行終端改行マッチによるフィールド切れ
(regex2 #/^\|([^=\n]+?)\s*=\s*((?:(?!\n\|)[\s\S])*)/gm) ; 修正
;; (filed-value (string-append templ_info.1 "\n"))
(filed-value templ-info.1)
(matches (--> Array (from (--> filed-value (matchAll regex2)))))
;; mapでalistのペアを生成
(inf_dic (vector-map (lambda (m)(cons m.1 m.2)) matches))
)
;; 配列->リスト変換して返す
(vector->list inf_dic)
)
) ; --- end of (when ()) ---
) ; --- end of (let* ()) ---
)
nlp100_chp3_main.scmに問題25の実行コードを実装します。
;; 問題No25実行
(define (test-no25 text)
(for-each (lambda (m) (print m )) (dic-obj text))
)
;; テスト実行
(test-exec test-no25)
;; 実行結果 =>
;; (略名 . イギリス)
;; (日本語国名 . グレートブリテン及び北アイルランド連合王国)
;; ~~ 中略 ~~
;; (国歌 . [[女王陛下万歳|{{lang|en|God Save the Queen}}]]{{en icon}}<br />''神よ女王を護り賜え''<br />{
;; ~~ 中略 ~~
;; (確立形態4 . 現在の国号「'''グレートブリテン及び北アイルランド連合王国'''」に変更)
;; ~~ 中略 ~~
;; (国際電話番号 . 44)
;; (注記 . <references/>)
26. 強調マークアップの除去
25の処理時に、テンプレートの値からMediaWikiの強調マークアップ(弱い強調、強調、強い強調のすべて)を除去してテキストに変換せよ(参考: マークアップ早見表)。
https://nlp100.github.io/2025/ja/ch03.html#id7
-
強調マークアップ
種類 記法 例 弱い強調(italic) ''テキスト'' ''イギリス'' 強調(bold) '''テキスト''' '''United Kingdom''' 強い強調(bold+italic) '''''テキスト''''' '''''強調''''' -
正規表現:
'{2,5}/gパーツ 意味 ' シングルクォート1文字 {2,5} 直前の文字が 2個以上5個以下 連続する /g 文字列全体にわたってすべて置換 -
動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
(print (--> "''神よ女王を護り賜え''" (replace #/'{2,5}/g "")))
;; => 神よ女王を護り賜え
nlp100_chp3.scmに問題26の処理コードを実装します。
;; 26. 強調マークアップの除去
;; 25の処理時に、テンプレートの値からMediaWikiの強調マークアップ(弱い強調、強調、強い強調のすべて)を
;; 除去してテキストに変換せよ(参考: マークアップ早見表)。
;; alistの各ペアの値(cdr)からMediaWiki強調マークアップ(''{2,5})を除去する
(define (remove-emphasis dicobj)
(let ((regex #/'{2,5}/g))
(map (lambda (pair)
;; alistの1ペア (key . value) を受け取り、valueの強調マークアップを除去して返す
(cons (car pair) (--> (cdr pair) (replace regex ""))))
dicobj ; テンプレート
)
)
)
nlp100_chp3_main.scmに問題26の実行コードを実装します。
;; 問題26実行
(define (test-no26 article-text)
(let (
(result-no25 (dic-obj article-text)) ;問題25の結果
)
(for-each
(lambda (pair) (print pair))
(remove-emphasis result-no25) ;問題26の結果
)
)
)
;; テスト実行
(test-exec test-no26)
;; 実行結果 =>
;; (略名 . イギリス)
;; (日本語国名 . グレートブリテン及び北アイルランド連合王国)
;; ~~ 中略 ~~
;; (国章画像 . [[ファイル:Royal Coat of Arms of the United Kingdom.svg|85px|イギリスの国章]])
;; ~~ 中略 ~~
;; (国歌 . [[女王陛下万歳|{{lang|en|God Save the Queen}}]]{{en icon}}<br />神よ女王を護り賜え<br />{
;; ~~ 中略 ~~
;; (公用語 . [[英語]])
;; (首都 . [[ロンドン]](事実上))
;; ~~ 中略 ~~
;; (確立形態4 . 現在の国号「グレートブリテン及び北アイルランド連合王国」に変更)
;; ~~ 中略 ~~
;; (ccTLD . [[.uk]] / [[.gb]]<ref>使用は.ukに比べ圧倒的少数。</ref>)
;; (国際電話番号 . 44)
;; (注記 . <references/>)
27. 内部リンクの除去
26の処理に加えて、テンプレートの値からMediaWikiの内部リンクマークアップを除去し、テキストに変換せよ。
https://nlp100.github.io/2025/ja/ch03.html#id8
内部リンクのマークアップと変換
| # | 書式 | 変換ルール | 例(変換前 → 後) |
|---|---|---|---|
| 1 | [[記事名]] | 記事名をそのまま残す | [[英語]] → 英語 |
| 2 | [[記事名|表示文字]] | |の右側を残す | [[イギリスの国章|国章]] → 国章 |
| 3 | [[記事名#節名|表示文字]] | |の右側を残す | [[女王陛下万歳|神よ女王陛下を守り給え]] → 神よ女王陛下を守り給え |
正規表現:
① パイプ(区切り)あり用 - [[記事名|表示文字]] / [[記事名#節名|表示文字]] → 表示文字を残す
[[[^\[\]|]+(?:#[^\[\]|]+)?|([^\[\]|]+)]]/g
\[\[[^\[\]|]+(?:#[^\[\]|]+)?\|([^\[\]]+)\]\]/g
| パーツ | 意味 |
|---|---|
| \[\[ | マークアップの [[ にマッチします。 |
| [^\[\]|]+ | 記事名部分にマッチします。[、]、|のいずれも含まない1文字以上です。 |が現れた時点で記事名の終わりと判断します。 |
| (?:#[^\[\]|]+)? | 節名部分(省略可能)にマッチします。# に続いて [、]及び|を含まない1文字以上です。 ?: で非キャプチャグループとしてかつ末尾の ? で省略可能にしています。 |
| | | マークアップのパイプにマッチします。記事名と表示文字の区切りです。 |
| ([^\[\]]+) | 表示文字部分をキャプチャします。[ と ] を含まない1文字以上です。 |
| \]\] | マークアップの ]] にマッチします。 |
| g | 文字列中の該当箇所すべてを置換対象にします。グローバルフラグです。 |
[!NOTE]
この正規表現はclaudeの提案を受け入れたものですが、 ?:#[^\\[\\]\|]+)?が冗長な気がしたのでclaudeにお尋ねしました。
動作上は不要であるが#で区切られる要素も考慮していることを示すものであるということでした。
② パイプ(区切り)なし用 - [[記事名]] → 記事名をそのまま残す
/\[\[([^\[\]|]+)\]\]/g
| パーツ | 意味 |
|---|---|
| [[ | マークアップの [[ にマッチします。 |
| ([^\[\]\|]+) |]] | 記事名部分をキャプチャします。[、]、パイプを含まない1文字以上です。パイプを除外することで、パイプありの [[記事名|表示文字]] には誤マッチしません。 |
| g | グローバルフラグです。 |
正規表現の動作確認
LIPSのサイトまたはローカルでLipsで下記LIPSコードを実行してみます。
※正規表現は ① パイプあり → ② パイプなし の順で正規表現を適用します。
(let (
;; ① パイプあり: [[記事名|表示文字]] → 表示文字
;; (regex-pipe #/\[\[[^\[\]|]+(?:#[^\[\]|]+)?\|([^\[\]|]+)\]\]/g)
(regex-pipe #/\[\[[^\[\]|]+\|([^\[\]]+)\]\]/g) ; `(?:#[^\[\]|]+)?`を除いてみる
;; ② パイプなし: [[記事名]] → 記事名
(regex-plain #/\[\[([^\[\]|]+)\]\]/g)
(texts
'("[[英語]]" "[[イギリスの国章|国章]]" "[[記事名#節名|表示文字]]"
"[[女王陛下万歳|{{lng|en|xxx}}]]{{en icon}}<br />神よ女王を護り賜え<br />{{center|[[ファイル:xxxx - xxx.ogg]]}}")
)
)
(for-each
(lambda(text)
(let* (
(val1 (--> text (replace regex-pipe "$1")))
(val2 (--> val1 (replace regex-plain "$1")))
)
(console.log text "=> val1:" val1 "→ val2:" val2)
)
;; 下のようなスタイルもOKです。
;; (let (
;; (val (replace regex-plain "$1" (replace regex-pipe "$1" text)))
;; )
;; (console.log val)
;; )
)
texts
)
)
;; => 出力
;; [[英語]] => val1: [[英語]] → val2: 英語
;; [[イギリスの国章|国章]] => val1: 国章 → val2: 国章
;; [[記事名#節名|表示文字]] => val1: 表示文字 → val2: 表示文字
;; [[女王陛下万歳|{{lng|en|xxx}}]]{{en icon}}<br />神よ女王を護り賜え<br />{{center|[[ファイル:xxxx - xxx.ogg]]}}
;; => val1: {{lng|en|xxx}}{{en icon}}<br />神よ女王を護り賜え<br />{{center|[[ファイル:xxxx - xxx.ogg]]}}
;; → val2: {{lng|en|xxx}}{{en icon}}<br />神よ女王を護り賜え<br />{{center|ファイル:xxxx - xxx.ogg}}
問題27処理コード
nlp100_chp3.scmに問題27の処理コードを実装します。
;; ## 27. 内部リンクの除去
;; 26の処理に加えて、テンプレートの値からMediaWikiの内部リンクマークアップを除去し、テキストに変換せよ。
;; https://nlp100.github.io/2025/ja/ch03.html#id8
(define (remove-internal-links dicobj)
(let (
;; ① パイプあり: [[記事名|表示文字]] → 表示文字
;; (regex-pipe #/\[\[[^\[\]|]+(?:#[^\[\]|]+)?\|([^\[\]|]+)\]\]/g)
(regex-pipe #/\[\[[^\[\]|]+(?:#[^\[\]|]+)?\|([^\[\]]+)\]\]/g)
;; ② パイプなし: [[記事名]] → 記事名
(regex-plain #/\[\[([^\[\]|]+)\]\]/g)
)
(map (lambda (pair)
(let* (
(val (cdr pair))
;; パイプありを先に処理
(val (--> val (replace regex-pipe "$1")))
;; パイプなしを処理
(val (--> val (replace regex-plain "$1")))
)
(cons (car pair) val)
)
)
dicobj
)
)
)
nlp100_chp3_main.scmに問題27の実行コードを実装します。
;; 問題27実行
(define (test-no27 article-text)
(let* (
(result-no25 (dic-obj article-text)) ;問題25の結果
(result-no26 (remove-emphasis result-no25))
)
(for-each
(lambda (pair) (print pair))
(remove-internal-links result-no26) ;問題27の結果
)
)
)
;; テスト実行
(test-exec test-no27)
;; 実行結果 =>
;; (略名 . イギリス)
;; (日本語国名 . グレートブリテン及び北アイルランド連合王国)
;; ~~ 中略 ~~
;; (国章画像 . 85px|イギリスの国章)
;; ~~ 中略 ~~
;; (国歌 . {{lang|en|God Save the Queen}}{{en icon}}<br />神よ女王を護り賜え<br />{{center|ファイル:United States Navy Band - God Save the Queen.ogg}})
;; ~~ 中略 ~~
;; (公用語 . 英語)
;; (首都 . ロンドン(事実上))
;; ~~ 中略 ~~
;; (確立形態4 . 現在の国号「グレートブリテン及び北アイルランド連合王国」に変更)
;; ~~ 中略 ~~
;; (ccTLD . .uk / .gb<ref>使用は.ukに比べ圧倒的少数。</ref>)
;; (国際電話番号 . 44)
;; (注記 . <references/>)
28. MediaWikiマークアップの除去
27の処理に加えて、テンプレートの値からMediaWikiマークアップを可能な限り除去し、国の基本情報を整形せよ。
https://nlp100.github.io/2025/ja/ch03.html#mediawiki
除去対象のマークアップ
問題28は下記のマークアップを除去対象として扱います。(claudeと相談してこのくらいと判断しました。)
| # | マークアップ | 内容 | 備考 |
|---|---|---|---|
| 1 | HTMLコメント | <!-- ... --> | ― |
| 2 | <ref>タグ | <ref>...</ref>, <ref .../> | ― |
| 3 | <br>タグ | <br />, <br> | ― |
| 4 | テンプレート | {{ }} | ― |
| 5 | 外部リンク | [http://...], http://... [https://...], https://... |
形式は[]のブラケットあり/なしがあります。 |
| 6 | 強調 | ''...'' | #26-強調マークアップの除去 |
| 7 | 内部リンク | [[...]] | #27-内部リンクの除去 |
マークアップ別正規表現:
各マークアップのマッチングに使う正規表現です。
1. HTMLコメント除去
/<!--[\s\S]*?-->/g
| パーツ | 意味 |
|---|---|
| <!-- | HTMLコメント開始タグ |
| [\s\S]*? | 改行を含む「任意の文字」を「最小限(最短)で」マッチさせる (*?で条件に合う「最小限の文字数」でマッチングすることを指定) |
| --> | HTMLコメント終了タグ |
| g | 全検索 |
動作確認:
(let (
(regex #/<!--[\s\S]*?-->/g)
(text "コメント<!--コメント コメント-->削除")
)
;; (print (replace regex "" text)) ;=> コメント削除
(print (--> text (replace regex ""))) ;=> コメント削除
)
2. <ref> タグ除去
タグ削除は2パスで実施します。
a. <ref>〜</ref>(内容ごと削除)
#/]*>.*?<\/ref>/gi
#/<ref[^>]*>[\s\S]*?<\/ref>/gi
| パーツ | 意味 |
|---|---|
| <ref | refタグ開始文字列をマッチングする |
| [^>]*> | refタグの属性文字をマッチングする (> 以外の文字が0回以上続き、その後に > で終わる文字列) |
| [\s\S]* | 改行、空白文字を含むあらゆる文字をマッチングする |
| <\/ref> | refタグ終了文字列をマッチングする |
| gi | g : 全体検索、i : 大文字・小文字無視。 |
b. <ref ... />(自己閉じ)
#/<ref[^>]*\/>/gi
| パーツ | 意味 |
|---|---|
| <ref | refタグ開始マッチ |
| [^>]* | refタグの属性文字マッチ (> 以外の文字を0回以上繰り返す) |
| \/> | refタグ終了マッチ |
| gi | g : 全体検索、i : 大文字・小文字無視。 |
動作確認:
(let (
;; a. <ref>〜</ref>(内容ごと削除)
(regex1 #/<ref[^>]*>[\s\S]*?<\/ref>/gi)
;; b. <ref ... />(自己閉じ)
(regex2 #/<ref[^>]*\/>/gi) ;
(texts '("refタグ<ref>refref</ref>削除" "refタグ<ref name=\"自己閉じ\" />削除"))
)
(print (map
(lambda (text)
(replace regex2 "" (replace regex1 "" text))
) texts)
) ; => (refタグ削除 refタグ削除)
)
3. <br> タグ除去
#/<\s*br\s*\/?\s*>/gi
| パーツ | 意味 |
|---|---|
| < | タグ開始 |
| \s* | 0個以上の空白文字(スペース、タブ、改行) |
| br | br文字列 |
| \s* | 0個以上の空白文字 |
| \/? | / という文字が0個または1個ある(自己終了タグ に対応) |
| \s* | 0個以上の空白文字 |
| gi | g : 全体検索、i : 大文字・小文字無視。 |
動作確認:
(let (
(regex #/<\s*br\s*\/?\s*>/gi)
(text "< br />おはよう<br / >こんにちは<br>こんばんわ<br>")
)
(print (--> text (replace regex ""))) ;=>おはようこんにちはこんばんわ
)
4. テンプレート
テンプレート除去はパイプありとパイプなし(フィールドの区切あり・なし)の2パスで実施します。
- パイプあり: {{テンプレ名|フィールド1|フィールド2|…|最後のフィールド}}の最後のフィールドを抽出する。
- パイプなし: {{単一フィールド}}の中身をそのまま抽出する。
a. パイプあり ("{{flagicon|UK}}"または"{{lang|en|London}}"のパターン)
#/\{\{[^{}|]+(?:\|[^{}|]+)*\|([^{}|]+)\}\}/g;
| パーツ | 意味 |
|---|---|
| \{\{ | テンプレート開始 |
| [^{}|]+ | "{}"、|"を含まない文字列。(テンプレート) |
| (?:\|[^{}|]+)* | "{}"、|"を含まない文字列の非キャプチャーグルーブ(フィールド) |
| \|([^{}|]+) | "{}"、|"を含まない文字列(フィールド)のキャプチャーグルーブ($1の対象) |
| \}\} | テンプレート終了 |
| g | 全体検索 |
b. パイプなし ("{{nopipe}}"のパターン)
#/\{\{([^{}|]+)\}\}/g;
| パーツ | 意味 |
|---|---|
| \{\{ | テンプレート開始 |
| ([^{}|]+) | "{}"、|"を含まない文字列(フィールド)のキャプチャーグルーブ($1の対象) |
| \}\} | テンプレート終了 |
| g | 全体検索 |
動作確認:
(let (
(texts '("{{lang|en|London}}" "{{flagicon|UK}" "{{0}}"))
(regex_pipe #/\{\{[^{}|]+(?:\|[^{}|]+)*\|([^{}|]+)\}\}/g)
(regex_nopipe #/\{\{([^{}|]+)\}\}/g)
)
(for-each
(lambda (text)
(print (replace regex_nopipe "$1" (replace regex_pipe "$1" text)))
)
texts
)
)
;; =>
;; London
;; {{flagicon|UK}
;; 0
5. 外部リンク除去
#/\[https?:\/\/[^\s\]]+(?:\s+([^\]]+))?\]|https?:\/\/\S+/g
外部リンク除去は2種類(ブラケットあり/なし)の正規表現をor演算で結合します。
- ブラケットあり:
#/\[https?:\/\/[^\s\]]+(?:\s+([^\]]+))?\]|https?://\S+/g - ブラケットなし: #/[https?://[^\s\]]+(?:\s+([^\]]+))?]|
https?:\/\/\S+/g
| パーツ | 意味 |
|---|---|
| /\[https?:\/\/ | ブラケットあり外部リンクのマッチング開始 |
| [^\s\]]+ | 空白(スペース)または閉じ括弧 ] 以外の文字(URL部分)マッチング |
| (?:\s+[^\]]+)? | (?: ... )? :()中のマッチがあってもなくてもよい(非キャプチャーグルーブ) \s+ : 1つ以上の空白(URLと表示文字の間のスペース)マッチング [^\]]+ : 閉じ括弧 ] 以外の文字(=表示文字部分)マッチング |
| \] | ブラケットあり外部リンクのマッチング終了 |
| | | or演算子 |
| /\https?:\/\/ | ブラケットなし外部リンクのマッチング開始 |
| [^\s<>"'\]\[]+ | URLの終わりを示す可能性のある記号以外の文字マッチング(空白、不等号、引用符、括弧など) |
動作確認:
(let (
(texts '(
"1. 説明付き: [https://www.example.org 表示文字]"
"2. URLのみ(ブラケットあり): [https://www.example.org]"
"3. 生URL: https://www.example.org"
))
(regex #/\[https?:\/\/[^\s\]]+(?:\s+([^\]]+))?\]|https?:\/\/\S+/g)
)
(for-each
(lambda (text) (print (--> text (replace regex "$1"))) ) texts
)
)
;; =>
;; 1. 説明付き: 表示文字
;; 2. URLのみ(ブラケットあり):
;; 3. 生URL:
<<外部リンクが中にある場合は除去によって外部リンクのテキストは無くなっているので処理的意味はなくなります。>>
6. 強調マークアップ除去
7. 内部リンク除去
参照 #27-内部リンクの除去
問題28処理コード
nlp100_chp3.scmに問題28の処理コードを実装します。
;; 28. MediaWikiマークアップの除去
;; 27の処理に加えて、テンプレートの値からMediaWikiマークアップを可能な限り除去し、国の基本情報を整形せよ。
;; https://nlp100.github.io/2025/ja/ch03.html#mediawiki
(define (remove-mediawiki-from-value txt-mediawiki)
(let (
;; 1. HTMLコメント: <!-- ... -->
(re-html-comment #/<!--[\s\S]*?-->/g)
;; 2a. <ref>〜</ref>(内容ごと削除)
(re-ref-block #/<ref[^>]*>[\s\S]*?<\/ref>/gi)
;; 2b. <ref ... />(自己閉じ)
(re-ref-self #/<ref[^>]*\/>/gi)
;; 3. <br> 系
(re-br #/<\s*br\s*\/?\s*>/gi)
;; 4. {{...}} テンプレート (スペース混在NG)
;; (re-template #/\{\{(?:.*|)?\s*([^|{}\s]+)\s*\}\}/g)
;; 4a. {{...}} テンプレート(区切りあり)
(re-templ-pipe #/\{\{[^{}|]+(?:\|[^{}|]+)*\|([^{}|]+)\}\}/g)
;; 4a. {{...}} テンプレート(区切りなし)
(re-templ-nopipe #/\{\{([^{}|]+)\}\}/g)
;; 5. 外部リンク: [URL], [URL 表示テキスト], URL
(re-ext-link #/\[https?:\/\/[^\s\]]+(?:\s+([^\]]+))?\]|https?:\/\/\S+/g)
)
(--> txt-mediawiki
(replace re-html-comment "")
(replace re-ref-block "")
(replace re-ref-self "")
(replace re-br "")
;; (replace re-template "$1")
(replace re-templ-pipe "$1")
(replace re-templ-nopipe "$1")
(replace re-ext-link "$1")
)
)
)
(define (remove-mediawiki-markups dic)
(map
(lambda(pair)
(let ((val (cdr pair)))
(cons (car pair) (remove-mediawiki-from-value val))
)
)
dic
)
)
nlp100_chp3_main.scmに問題28の実行コードを実装します。
(define (test-no28 article-text)
(let* (
(result-no25 (dic-obj article-text)) ;問題25の結果
(result-no26 (remove-emphasis result-no25)) ;問題26の結果
(result-no27 (remove-internal-links result-no26)) ;;問題27の結果
)
(for-each
(lambda (pair) (print pair))
(remove-mediawiki-markups result-no27) ;問題28の結果
)
)
)
;; テスト実行
(test-exec test-no28)
;; 実行結果 =>
;; (略名 . イギリス)
;; (日本語国名 . グレートブリテン及び北アイルランド連合王国)
;; (公式国名 . United Kingdom of Great Britain and Northern Ireland)
;; ~~ 中略 ~~
;; (標語 . Dieu et mon droit(フランス語:神と我が権利))
;; (国歌 . God Save the Queenen icon神よ女王を護り賜えファイル:United States Navy Band - God Save the Queen.ogg)
;; ~~ 中略 ~~
;; (他元首等氏名2 . Lindsay Hoyle)
;; ~~ 中略 ~~
;; (人口値 . 6643万5600)
;; ~~ 中略 ~~
;; (GDP値元 . 1兆5478億)
;; ~~ 中略 ~~
;; (GDP値MER . 2兆4337億)
;; ~~ 中略 ~~
;; (GDP値 . 2兆3162億)
;; (GDP/人 . 36,727)
;; ~~ 中略 ~~
;; (確立形態1 . イングランド王国/スコットランド王国(両国とも1707年合同法まで))
;; ~~ 中略 ~~
;; (確立形態2 . グレートブリテン王国成立(1707年合同法))
;; (確立年月日2 . 1707年05月01日)
;; (確立形態3 . グレートブリテン及びアイルランド連合王国成立(1800年合同法))
;; (確立年月日3 . 1801年01月01日)
;; ~~ 中略 ~~
;; (確立年月日4 . 1927年04月12日)
;; ~~ 中略 ~~
;; (ccTLD . .uk / .gb)
;; (国際電話番号 . 44)
;; (注記 . )
29 国旗画像のURLを取得する
テンプレートの内容を利用し、国旗画像のURLを取得せよ。(ヒント: MediaWiki APIのimageinfoを呼び出して、ファイル参照をURLに変換すればよい)
https://nlp100.github.io/2025/ja/ch03.html#url
処理概要
No29はNo25~No28で処理した基礎情報を元に
- MediaWiki APIのURL作成
- MediaWiki APIエンドポイントからimageinfo取得
- imageinfoから国旗画像のURLを抽出
という処理を行います。
問題29処理コード
nlp100_chp3.scmに問題28の処理コードを実装します。
;; Wikimedia Wiki Endpoint (https://www.mediawiki.org/wiki/API:Action_API#Quick_Start)
(define wiki-api-endpoint "https://ja.wikipedia.org/w/api.php")
;; ファイル名を受け取りMediaWiki imageinfo APIのURLを組み立てる
;; (imageinfo :イメージファイルに関する各種情報)
;; 参考: https://www.mediawiki.org/wiki/API:Action_API https://www.mediawiki.org/wiki/API:Imageinfo
(define (build-api-url filename)
(let* (
;; スペースをアンダースコアに統一(MediaWiki の慣習)
(normalized (--> filename (trim) (replace #/ /g "_")))
;; "File:" プレフィックスを付加
(titled (string-append "File:" normalized))
;; クエリパラメータを組み立てる
(params (string-append
"?action=query" ;Fetch data from and about MediaWiki.
"&prop=imageinfo"
"&iiprop=url"
"&format=json"
"&origin=*" ;; CORS 対応
"&titles=" (encodeURIComponent titled)
)
)
)
(string-append wiki-api-endpoint params))
)
;; Wikipedia APIで取得したデータからimageinfoを抽出する
(define (urllist data)
(let* (
(json (JSON.parse data))
(pages json.query.pages)
;;pagesが持つ文字列キーのプロパティ値を配列で取得する
(page-list (--> Object (values pages)))
(first-page (vector-ref page-list 0))
(imageinfo first-page.imageinfo)
)
(if (and imageinfo (> imageinfo.length 0))
(vector-ref imageinfo 0)
#f ; else return false
)
)
)
;; No29 国旗画像情報取得
(define (fetch-flag-url filename)
(let* (
(api-url (build-api-url filename))
(data (--> (fetch api-url) (text))) ;(text) => JSコード: Response: text()
(urls (urllist data))
)
(if urls urls.url #f)
)
)
nlp100_chp3_main.scmに問題28の実行コードを実装します。
(define (test-no29 text)
(let* (
(dic-25 (dic-obj text)) ; 25: テンプレート抽出
(dic-26 (remove-emphasis dic-25)) ; 26: 強調除去
(dic-27 (remove-internal-links dic-26)) ; 27: 内部リンク除去
(dic-28 (remove-mediawiki-markups dic-27)) ; 28: その他マークアップ除去
;; 連想リスト形式のテンプレートテキストから国旗画像の値を取得する
(flag-file-name (cdr (assoc "国旗画像" dic-28)))
)
(if (not (null? flag-file-name))
(console.log (fetch-flag-url flag-file-name))
(console.log "no url")
)
)
)
;; テスト実行
(test-exec test-no29)
;; 実行結果 =>
;; https://upload.wikimedia.org/wikipedia/commons/8/83/Flag_of_the_United_Kingdom_%283-5%29.svg
おわりに
本稿の内容に誤り、不具合、コメント等ありましたらご指摘いただければ幸いです。![]()
参考
変更履歴
2026/04/29 No23 ~ No25追加
2026/05/03 No26 ~ No27追加
2026/05/03 No25 マークダウン崩れ修正 (コードブロック範囲、連番崩れ)
2026/05/17 No25 バグ修正 (フィールド名/値抽出の正規表現)
2026/05/28 No27 ゴミnote削除
2026/05/28 No28追加
2026/06/06 No29追加