隙間

こちらはいわゆる仕事用のもの.

はじめての正規表現2018 ~トリコとともに~

この記事は明治大学 Advent Calendar 2018 - Qiitaの5日目の記事です.

半ばノリと勢いで巻き込まれましたが,頑張っていきます.

まぁしゃあないか,「はじまりか、」 は平成アイドル界屈指のmasterpieceやし.

気持ちが乙女なので,西野カナの「トリセツ」を歌いながら書きます.

これからもどうぞよろしくね.



きっかけ

我らが拠点,明治大学中野キャンパスの近くには「トリコカレー」というカレー屋さんがあります.

他店に類を見ない濃厚なルーと充実のトッピングが人々を魅了し,弊学にも多くのファンがいます.

その熱狂ぶりはとどまるところを知らず,ある信者ファンに至っては「トリコ」に関するツイートを目視で逐一いいね!をしているほどです.

ある日,その彼がいつものように「トリコ」に関するツイートを検索しようとしたところ, 他ユーザーとこのようなやりとりに発展しました.

確かに,これを一般的な検索窓だけで見つけたりすることは難しいよねと思ったと同時に,ちょっと頑張ればこのくらいならできそうじゃんとも思った次第です.

なので今回はそんな彼の投げキッス運動*1を文字列処理の技術で応援しようと思います.

目的

人力で発見しなくても「トリコ」に関係しそうなワードを任意のテキストから検出する.

より一般的には

「任意の文字列パターン(法則性)を一般的・定量的に表現し,文字列検索・照合等で活用する」

になります.

このような問題を解決するために,文字列処理の世界では,正規表現(Regular Expression, regex)と呼ばれる技術が存在します.

今回はその正規表現を活用しながら「トリコ」にまつわる,あんな文字列やこんな文字列を検出していきましょう.

エゴサばかりしてた今日までの自分から生まれ変わるために.

正規表現のあれこれ

メタ文字

正規表現の構文では文字列の法則性(以下パターン)を記号的に表現するために,メタ文字と呼ばれる特殊文字を使ってパターンを表現します.

. ^ $ [ ] * + ? | ( )

これらのメタ文字を組み合わせ,「(ここに)何かの1文字」とか「何かのひらがな3文字」とか「0から9の数字」といったような任意のパターンを表現することができます.順を追って解説します.

メタ文字を含まない表記

通常,完全一致を仮定した場合,文字列「トリコ」を検出する場合には文字列「トリコ」と等価評価を行います.したがって,文字列「トリコ」との比較で検出されるパターンは「トリコ」のみとなります.

トリコ 

f:id:mblogit:20181204024442p:plain

この構文では以下のような文字列が該当します.

(例) トリコ
任意の1文字 「.」(ピリオド)

検出したいパターンの中に任意の1文字を含む場合は「.」(ピリオド)でその1文字を表現することができます.

ト.リコ 

f:id:mblogit:20181204021918p:plain

この構文では以下のような文字列が該当します.

(例) トあリコ  ト1リコ トーリコ  ト&リコ  
同一文字の繰り返し 「* + ? 」

同一文字が複数繰り返されているパターンを検出したい場合は「* + ? 」を使います.しかし,それぞれの文字で表現できる繰り返しのパターンに違いがあるので,注意して使い分ける必要があります.

直前の文字が0個もしくは1個以上繰り返されている「*」(アスタリスク)

ある箇所に任意の文字が0個もしくは1個以上繰り返されている場合には「*」(アスタリスク)を使います.

ト〜*リコ 

f:id:mblogit:20181204030248p:plain
この構文では以下のような文字列が該当します.

(例) トリコ  ト〜リコ ト〜〜リコ  ト〜〜〜〜〜〜〜〜リコ  
直前の文字が最低1個以上繰り返されている「+」(プラス)

ある箇所に任意の文字が最低1個以上繰り返されている場合には「+」(プラス)を使います.「*」(アスタリスク)の場合と異なり,直前の文字が最低1回繰り返されている場合のみを表現するため,「トリコ」自体はこのパターンに該当しないことに注意してください.

ト〜+リコ 

f:id:mblogit:20181204123436p:plain
この構文では以下のような文字列が該当します.

(例) ト〜リコ ト〜〜リコ  ト〜〜〜〜〜〜〜〜リコ  
直前の文字が1個だけ存在する場合かしない場合.「?」(クエスチョンマーク)

ある箇所に任意の文字が1個だけ存在する場合,もしくは存在しない場合を表現するときは「?」(クエスチョンマーク)を使います.前述の記号と異なり繰り返し要素はあまりありませんが,直前文字が0個の場合にセンシティブという点では,類似した性質を持ちます.

ト〜?リコ 

f:id:mblogit:20181204123438p:plain

この構文では以下のような文字列が該当します.

(例) トリコ ト〜リコ
任意の文字の繰り返し 「.*」

メタ文字は組み合わせて使うことでより,複雑なパターンを表現することができます.

前述したピリオドとアスタリスクを組み合わせて「任意の文字の繰り返し」を表現することができます.

ト.*リコ 

f:id:mblogit:20181204123421p:plain
この構文では以下のような文字列が該当します.

(例) トリコ  ト〜リコ ト1〜リコ トあぺぱぺリコ  トkfジェイrfklさdfクォイエfんqリコ  
行頭と行末 「^ $」 (ハット・ドル)

正規表現では任意の文字列が行頭/行末にくるパターンも検出することが可能です.

つまり,与えられたテキストが任意の単語(文字列)で始まる場合.もしくは任意の単語で終わる場合かといったパターンも検出することができます.

(a) ^トリコ  
(b) トリコ$ 

f:id:mblogit:20181204124210p:plain
(a)

f:id:mblogit:20181204123447p:plain
(b)

この構文では以下のような文字列が該当します.

(例) (a) トリコはカレー屋さんです トリコのエコランチは日替わりのトッピングがつきます  
(例) (b) プエルトリコ
複数候補のある文字・文字列のいずれかに該当する 「|」(バー)

表現したいパターンのうち同一箇所に複数候補があり,かつ,その候補のいずれかが該当した場合を検出したい場合は「|」(バー)を用いて表記することが可能です.数学における「または」「or」「∨」をイメージするとわかりやすいです.

トリコ|野方ホープ 

f:id:mblogit:20181204123444p:plain

この構文では以下のような文字列が該当します.

(例) トリコ 野方ホープ
指定された文字のいずれか "[ ]"

ある箇所において,いくつかある文字の候補のうち,いずれかが該当する場合を表記するのには「[ ]」を活用します.

ト[リ○△]コ 

f:id:mblogit:20181204123441p:plain

この構文では以下のような文字列が該当します.

(例) トリコ ト○コ ト△コ

また,「[ ]」の中でアルファベットいずれか1文字や数字いずれか1文字を表記する場合には,以下のように「-」を用いて包括的に表記をすることが可能です.

[ABCDEFGHIJKLMNOPQRSTUVWXYZ] → [A-Z]
[0123456789] → [0-9]

さらに同じように「-」を用いて,ひらがなや漢字も包括的に表記することが可能ですが,JIS漢字コードの水準等の知識が必要になるため,ここでは扱いません.

グループ化 「( )」

ここまでで説明したメタ文字は直前の1文字というような文字単位でのパターン生成を担ってきましたが,「( )」を用いてグループ化することで,対象の範囲を複数個の文字に拡大・限定して,各々のメタ文字を生成することができます.

以下例をみてください.野方ホープには中野店と吉祥寺店の2つ*2があります.このとき「野方ホープ中野店」「野方ホープ吉祥寺店」の両方を網羅するパターンを正規表現するときにどのように書き下すことができますか?

正規表現に慣れてない読者の中にはおそらくこのように書き下した人がいると思います.

野方ホープ中野|吉祥寺店 

f:id:mblogit:20181204123428p:plain

しかし,これでは目的としてる2店舗を検出することはできません.

この構文では以下の2パターンを検出してしまいます.

(例) 野方ホープ中野 吉祥寺店

このような場合,「( )」を用いてグループ化することで「|」(バー)の作用範囲を限定し,以下のように書き下すことが可能です.

野方ホープ(中野|吉祥寺)店 

f:id:mblogit:20181204124712p:plain

この構文では以下のような文字列が該当します.

(例) 野方ホープ中野店 野方ホープ吉祥寺店

検証結果

それでは,これらの正規表現を使ってトリコに関する文字列がパターンにマッチするかどうかを確認します.

使用言語はpython で re モジュールを使用しました. またメタ文字として割り当てられている「.」(ピリオド)をピリオドとして認識させる場合には,「\.」と表記します. 他のメタ文字に関しても同様です.

# -*- coding: utf-8 -*-

import re

targetList = ["トリコ","トルコ","ト.リコ","トリ._..~..コ","トリ_コ","ト,リコ","トリ~コ","プエルトリコ","野方ホープ"]

for index,trg in enumerate(targetList):
    isMatch = re.match('^ト[\._~,]*リ[\._~,]*コ[\._~,]*$' , trg)
    if isMatch:
        print index, trg, "matched"
    else:
        print index, trg, "not matched"

f:id:mblogit:20181204123714p:plain

出力結果

0 トリコ matched
1 トルコ not matched
2 ト.リコ matched
3 トリ._..~..コ matched
4 トリ_コ matched
5 ト,リコ matched
6 トリ~コ matched
7 プエルトリコ not matched
8 野方ホープ not matched

やったね!

引用

Regexper

サルにもわかる正規表現入門

pythonの正規表現メモ - Qiita

*1:ここではむやみやたらに他人にいいね(ハート)を送りつける行為を指します

*2: (他にもあるけど)