このブログを検索

第5章 正規表現

5.1. 没頭しよう

大きなテキストの塊から、短いテキストだけを取り出すのは、なかなか難しいものです。Pythonには文字列を検索したり置換するメソッドがあります。index()、find()、split()、count()、replace()などですが、これらのメソッドが使えるのは、非常に単純な場合に限られています。例えばindex()メソッドは、1つのハードコードのサブ文字列を探すことができますが、常に大文字小文字を区別して検索します。文字列sを大文字小文字の区別なく検索するためには、s.lower()またはs.upper()を呼び出して、検索にかかるようにする必要があります。replace()、split()メソッドにも同じ制限があります。

目的がstringメソッドで達成できるのであれば、それを使うとよいでしょう。動作は速く、シンプルで読みやすいコードで利点が多くあります。ですが、さまざまなstring関数を使ったり、特別なケースを処理するためにif文を使ったり、あるいはsplit()やjoin()を繰り返し呼び出して切り貼りしているのであれば、正規表現を使ったほうがよいかもしれません。

正規表現は、強力かつ(ほぼ)標準化された検索、置換の方法で、複雑なパターンの文字でテキストを解析するために使われるものです。正規表現の文法は厳密で普通のコードのように見えませんが、最終的な結果は長く連なった文字列関数を使って無理に書いた解法よりも、読みやすいものになります。正規表現の中にコメント埋め込む方法もあるので、きめ細かいドキュメントを入れることができます。

👉 他の言語(Perl、JavaScript、PHP)で正規表現を使ったことがあるなら、Pythonでの構文も親しみやすいでしょう。モジュールのサマリを読むと使用できる関数や引数の概要を知ることができます。

5.2.  ケース・スタディ: ストリート番地

これから出てくる例は、私が何年か前に仕事で取り組んだ実世界の問題にインスパイアされています。古いシステムから住所を出力して、整理して統一したあと、新しいシステムに入力するというものです。(ただ単に作ったのではなく、実際に役立つものです。)どういう風にアプローチしたのか、この例で示していきます。
>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                # ①
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                # ②
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  # ③
'100 NORTH BROAD RD.'
>>> import re                               # ④
>>> re.sub('ROAD$', 'RD.', s)               # ⑤
'100 NORTH BROAD RD.'

①目標は'ROAD'という住所名を'RD'という短縮表記に統一することです。一見したときは、文字列のreplace()メソッドを使えば簡単だと考えました。ここで、文字列はすべて大文字になっていたので、大文字小文字の不一致の問題はありません。検索する文字列'ROAD'は定数で、非常にシンプルなこの例では、s.replace()が確実に効いています。

②残念ながら、人生というものは例外だらけなので、すぐにこの例が見つかってしまいました。ここでの問題は、'ROAD'が住所に2回現れることです。ストリート名の'BROAD'で1回、'ROAD'でもう1回です。replace()メソッドでは盲目的にこれら2つを置換します。そのため、住所はおかしなことになっています。

③'ROAD'という部分が住所に2つ以上あるときの問題を解決するため、このように書き直します。'ROAD'を検索して置換するのは、住所の最後の4文字(s[-4:])だけにして、残りの(s[:-4])はそのままにするのです。ところが、すぐにこれも悪手とわかります。例えば、置き換えたい文字の長さでパターンが決まってしまうのです。('STREET'を'ST'で置き換えたい場合、s[:-6]、 s[-6:]を使うことになります。半年後、改めてデバックしますか?しないでしょうね)。

④正規表現の出番がやってきました。Pythonではreモジュールに正規表現のすべての関数が入っています。

⑤最初のパラメータ'ROAD$'を見てみましょう。これは'ROAD'が文字列の最後にあるときだけマッチする正規表現です。「$」が「文字列の最後」を意味します。(「文字列の最初」はキャレット「^」が対応します。) 

関数re.sub()を使うと、文字列sを正規表現'ROAD$'で検索して、'RD.'に置き換えます。これはROADがsの最後にある場合はマッチしますが、BROADはsの中間にあるのでROADを含みますがマッチしません。

住所名を整える話の続きですが、私は先ほどの例をすぐに発見し、'ROAD'を文字列の最後でマッチするようにしました。しかし、それでは不十分でした。なぜならすべての住所がストリート名があるわけではなかったからです。また、少数ですが、住所がストリート名で終わっているところもありました。その場合、ストリート名が'BROAD'であれば、思惑から外れて'BROAD'の部分文字列が正規表現'ROAD'にマッチしてしまうのです。

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   # ①
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   # ②
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   # ③
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  # ④
'100 BROAD RD. APT 3'

①本来、抽出したかったのは、'ROAD'が文字列の最後にある場合で、さらに言うと(長い単語の最後の部分ではなく)'ROAD'そのものでした。

これを正規表現で表すと、「文字の区切りがここにある」という意味の「\b」を使います。Pythonでは、「\b」が文字列にあるとエスケープするので複雑です。「バックスラッシュ病」と揶揄されたりしますが、PythonよりもPerlの方が正規表現を使いやすいと言われる理由の1つになっています。逆にPerlでは正規表現を他の構文と組み合わせることができるため、もしバグがあった場合、バグが構文上にあるのか正規表現にあるのかの判断が難しいかもしれません。

②バックスラッシュ病とうまくやっていくために、ロー・ストリングと呼ばれるプリフィックス「r」を使います。これを使って、Pythonに「文字列の中にエスケープはない」と教えるのです。'\t' はタブ文字ですが、 r'\t' はバックスラッシュ文字 \ と文字 t です。正規表現では常にロー・ストリングを使うことをお勧めします。そうでないと、(そもそも正規表現は混乱するのに)すぐに混乱してしまいます。

③(*ため息*)残念ながら、もう自分のロジックに反する例を見つけてしまいました。この場合は、ストリート住所は'ROAD'をそのまま含んでいますが、末尾ではありません。この住所では、ストリート名のあとに、アパートの番号があります。'ROAD'は文字列の最後にないので、マッチしません。re.sub()を呼び出しても何も置き換えず、元の文字列がそのまま返って来ますが、これは狙った結果ではありません。

④これを解決するため、$ を外して、その場所に \b を加えました。これで正規表現は「文字列の中でどこでもよいので'ROAD'がこの形で出てくるものを抽出する」という意味になりました。

5. 3. ケーススタディ: ローマ数字

認識していないかもしれませんが、ほとんどの人はローマ数字を見たことがあることでしょう。古い映画やTV番組の中や(“Copyright MCMXLVI” は “Copyright 1946”という意味 )、図書館や大学の壁面かもしれません(“established MDCCCLXXXVIII” は “established 1888”という意味  )。あるいは書誌の目次で見たことがあるかもしれません。

ローマ数字では、7つの文字をさまざまに繰り返し組み合わせて数字を表現します。
  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000
ローマ数字を作るための一般的なルールはこのようになっています。
  • 文字を追加していきます。I は1、 II は 2、 III は 3。 VI は 6 (文字通り 5 + 1)、VII は 7、VIII は 8となります。
  • 10の倍数(I、X、C、M)を繰り返すのは3回までです。4回目は次の5を表す文字から1を引きます。つまり4はIIIIではなく、IVとなります(5より1小さい)。40はXLと書きます(50より10小さい)。41はXLI、42はXLII、43はXLIII、44はXLIVです(50より10小さいものに、5より1小さいものを加える)。
  • 文字が負数の追加を意味になることもあります。文字を他の文字の前につけることで、後ろの値からの引き算をします。例えば、9は次の10の位の文字である10から1を引きます。8はVIIIですが、9はIX(10より1小さい)であって、VIIIIではありません(文字を4回繰り返すことはできない)。90はXC、900はCMです。
  • 5を表す文字は繰り返すことができません。10は常にXであってVVではありません。100は常にCであってLLではありません。
  • ローマ数字は左から右に読むので、文字の順が重要です。DCは600ですが、CDは全く別の数字400になります(500より100小さい)。CIは101ですが、ICはローマ数字として成立しません(100から1を直接引くことはできない)。「100より10小さいものに、10より1小さいものを加えて」XCIXとなります。

5.3.1.  1000の位をチェックする

ある文字が、ローマ数字として成り立つか確認するにはどうすればよいでしょうか?1桁ずつ確認しましょう。ローマ数字は常に大きい桁から小さい桁へと書かれるので、最大の桁の1000の位の確認から始めます。1000以上の数は、Mの文字を使って表わされます。
>>> import re
>>> pattern = '^M?M?M?$'        # ①
>>> re.search(pattern, 'M')     # ②
<_sre .sre_match="" 0106fb58="" at="" object="">
>>> re.search(pattern, 'MM')    # ③
<_sre .sre_match="" 0106c290="" at="" object="">
>>> re.search(pattern, 'MMM')   # ④
<_sre .sre_match="" 0106aa38="" at="" object="">
>>> re.search(pattern, 'MMMM')  # ⑤
>>> re.search(pattern, '')      # ⑥
<_sre .sre_match="" 0106f4a8="" at="" object="">

①このpatternには3つの部分があります。^は文字列の最初にマッチします。^がない場合は、patternはMがどこにあっても関係なくマッチするので、想定しているものではありません。Mの文字列の最初にあるかどうか確認したいのです。M?は0文字または1文字のMにマッチします。これが3回繰り返されているので、Mが0〜3回連続して現れているときにマッチすることになります。$は文字列の最後にマッチします。最初の^と組み合わせると、patternは文字列全体にマッチし、Mの前にも後ろにも文字がないものにマッチすることになります。

②reモジュールの真髄はsearch()関数にあります。正規表現(pattern)と文字列('M')を受け取り、正規表現にマッチするかをテストするのです。マッチしない場合、search()はNull値であるNoneを返します。

③'MM'はマッチします。1、2番目のMがマッチして、3番目はマッチせず無視されます。

④'MMM'はマッチします。すべてのMがマッチしています。

⑤'MMMM'はマッチしません。3つのMはマッチしますが、$があるので正規表現では文字が終了しています。文字列には4番目のMがあるため、search()はNoneを返します。

⑥興味深いことに、空の文字列はこの正規表現にマッチします。全てのMはオプションだからです。

5.3.2. 100の位をチェックする

100の位の判定は、1000の位よりも難しいです。値によって相互に排他的な表現方法がいくつか存在するからです。表現は、
  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM
となるので、下記の4パターンがあることがわかります。
  • CM
  • CD
  • 0-3つのCの文字(100の位が0であればCは0)
  • DのあとにCの文字が0-3つ続く
後ろの2つのパターンは、このように合体させることができます。
  • Dがオプションで、0-3つのCの文字
このコード例は、ローマ数字の100の位を確定する方法を表しています。
>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'  # ①
>>> re.search(pattern, 'MCM')             # ②
<_sre .sre_match="" 01070390="" at="" object="">
>>> re.search(pattern, 'MD')              # ③
<_sre .sre_match="" 01073a50="" at="" object="">
>>> re.search(pattern, 'MMMCCC')          # ④
<_sre .sre_match="" 010748a8="" at="" object="">
>>> re.search(pattern, 'MCMC')            # ⑤
>>> re.search(pattern, '')                # ⑥
<_sre .sre_match="" 01071d98="" at="" object="">
①先ほどと同じようにpatternは「^」で始まり、文字列の開始場所を調べてから1000の位を「M?M?M?」で確認します。次の括弧には新しい部分が入っています。「|」で区切られてお互い独立した3つのパターン CM、 CD、 D?C?C?C?(オプションのDと0-3つのC) を定義しています。正規表現のパーサーは各パターンを左から右の確認し、最初にマッチしたものを採用して残りは無視します。

②'MCM'はマッチします。最初にMがあるのでマッチし、2、3番目は無視されます。次にCMがマッチします(CMがマッチしたので、CD、D?C?C?C?は無視される)。ローマ数字MCMは1900です。

③'MD'はマッチします。最初のMがマッチし、2、3番目は無視されます。D?C?C?C?パターンがDにマッチします(Cはオプションなので無視されます。つまり、"0つ"マッチしています)。ローマ数字MDは1500です。

④'MMMCCC'はマッチします。すべてのMがマッチして、D?C?C?C?がCCCにマッチします(Dはオプションなので今回無視されます。0つ"マッチです)。ローマ数字MMMCCCは3300です。

⑤'MCMC'はマッチしません。最初のMはマッチし、2、3番目のMは無視されます。次にCMがマッチすますが、そこが文字列の最後ではありません(マッチしていないCがある)。そのため、$がマッチしません。なお、最後のCがD?C?C?C?パターンの一部としてマッチすることはありません。すでに相互に排他的なCMパターンがマッチしているからです。

⑥興味深いことに、空の文字列もこのパターンにマッチします。Mの文字、D?C?C?C?パターンはオプションなので無視されるからです。

ふぅ! 正規表現がすぐに意味不明になってしまう理由がわかりますか?今はまだ、ローマ数字の1000、100の位を変換しただけなのです。しかし、ここまでできれば、10、1の位は簡単です。全く同じやり方で対応できるからです。もう1つの方法でpatternを表現してみましょう。

5.4. {n, m}構文を使う

前節では、同じ文字が最大3回繰り返されるパターンを扱いました。正規表現には、もう1つ記法があり、こちらの方が読みやすいという人もいます。まず、前の例で使った手法を見てみましょう。
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')     # ①
<_sre .sre_match="" 0x008ee090="" at="" object="">
>>> re.search(pattern, 'MM')    # ②
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MMM')   # ③
<_sre .sre_match="" 0x008ee090="" at="" object="">
>>> re.search(pattern, 'MMMM')  # ④
>>>

①文字列の最初にマッチし、オプションのMの最初にマッチします。2、3番目のMにはマッチしませんが、オプションなので問題ありません。最後に文字列の末尾"$"にマッチします。

②文字列の最初にマッチし、1、2番目のMにマッチします。3番目はマッチしませんが、オプションなので問題ありません。最後に文字列の末尾"$"にマッチします。

③文字列の最初にマッチし、オプションのMの3つすべてにマッチします。さらに文字列の末尾"$"にマッチします。

④文字列の最初にマッチし、オプションのM3つすべてにマッチしますが、もう1つMが残っているので文字列の末尾にマッチしません。そのためNoneを返します。

>>> pattern = '^M{0,3}$'        # ①
>>> re.search(pattern, 'M')     # ②
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MM')    # ③
<_sre .sre_match="" 0x008ee090="" at="" object="">
>>> re.search(pattern, 'MMM')   # ④
<_sre .sre_match="" 0x008eeda8="" at="" object="">
>>> re.search(pattern, 'MMMM')  # ⑤
>>>
①このpatternは「文字列の最初、文字列のどこかに0-3つのM、文字列の末尾ににマッチする」という意味です。0、3はどんな数でもかまいません。マッチするMが最低1で、最大3であるならばM{1, 3}と書けます。

②文字列の最初にマッチし、最大3までのMが1つマッチし、文の末尾がマッチします。

③文字列の最初にマッチし、最大3までのMが2つマッチし、文の末尾がマッチします。

④文字列の最初にマッチし、最大3までのMが3つマッチし、文の末尾がマッチします。

⑤文字列の最初にマッチし、Mが3つマッチしますが、文字列の末尾がマッチしません。この正規表現patternでは、文字列の末尾までにMを3つまでしか許容できないのですが、4つあるのでマッチせず、Noneを返します。

5.4.1. 10の位と1の位を確認する

ローマ数字の10の位と1の位もカバーできるように正規表現を拡張しましょう。この例は10の位を確認しています。

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')     # ①
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MCML')      # ②
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MCMLX')     # ③
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MCMLXXX')   # ④
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MCMLXXXX')  # ⑤
>>>

①文字列の最初にマッチしたあと、オプションのMの1番目、CM、XL、文字の末尾という順でマッチしています。(A|B|C)という構文は「A、B、Cのいずれか1つにマッチする」という意味でした。XLにマッチするため、今回はXC、L?X?X?X?X?を選択肢から外して文字列の末尾へと移ります。ローマ数字MCMXLは1940です。

②文字列の最初にマッチしたあと、オプションのMの1番目、CM、L?X?X?X?という順でマッチします。L?X?X?X?ではLにマッチしてオプションであるX3つはスキップし、文字列の末尾に移ります。ローマ数字MCMLは1950です。

③文字列の最初にマッチしたあと、オプションのMの1番目、CM、オプションのL、Xにマッチして2、3番目のXはスキップし、文字列の末尾にマッチします。ローマ数字MCMLXは1960です。

④文字列の最初にマッチしたあと、オプションのMの1番目にマッチし、CM、Lと3つすべてのXにマッチ、文字列の末尾という順でマッチします。

⑤文字列の最初にマッチしたあと、M、CM、LXXXにマッチしますが、マッチしないXが1つ残っているため、文字列の末尾にマッチできません。全体としてマッチしないので、Noneを返します。MCMLXXXXはローマ数字として成り立たちません。


1の位の正規表現は、同じような形になります。詳細は省いて、結果だけをお見せします。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

{n,m}の記法を使うとどうなるでしょうか?こちらが新しい記法の例です。
>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')              # ①
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MMDCLXVI')          # ②
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MMMDCCCLXXXVIII')   # ③
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'I')                 # ④
<_sre .sre_match="" 0x008eeb48="" at="" object="">

①文字列の最初にマッチしたあと、最大3つまでのMに1つマッチ、D?C{0, 3}においてオプションのDにマッチ、最大3であるCとのマッチはゼロです。さらに、L?X{0, 3}においてオプションのLとマッチし、最大3のXとのマッチはゼロです。V?I{0, 3}ではオプションのVにマッチして最大3のIとのマッチはゼロです。最後に文字列の末尾とマッチします。ローマ数字MDLVは1555です。

②文字列の最初にマッチしたあと、最大3つまでのMに2つマッチ、D?C{0, 3}においてDとC1つにマッチ、L?X{0, 3}においてLXとマッチ、V?I{0, 3}においてVIとマッチ、最後に文字列の末尾がマッチします。ローマ数字MMDCLXVIは2666です。

③文字列の最初にマッチしたあと、最大3つまでのMに3つマッチ、D?C{0, 3}においてDとC3つにマッチ、L?X{0, 3}においてLとX3つにマッチ、V?I{0, 3}ではVとI3つにマッチします。最後に文字列の末尾にマッチします。ローマ数字MMMDCCCLXXXVIIIは3888で、これは文法の拡張なしに表現できる最大のローマ数字です。

④よく見てみましょう(まるでマジシャンのような気分です・・・「君たち、よく見てくれ!帽子からウサギを出してみせよう!」)。これは文字列の最初にマッチし、Mはマッチなし、D?C{0, 3}はマッチなし、L?X{0, 3}はマッチなしで、V?I{0, 3}ではオプションのVはスキップしてIが1つマッチします。最後に文字列の末尾がマッチします。おやおや。

もし、1回だけ構文を追って理解することができたなら、私のときより上手くやれています。想像してみましょう。大規模プログラムの心臓部にあたる関数で、他人が書いた正規表現を解読することを・・・。あるいは、自分が書いた正規表現を数カ月後に解読することを・・・。私はやったことがありますが、全然楽しくありませんでした。

では、メンテンスしやすい他の構文を探してみましょう。

5.5. 冗長な正規表現

ここまで見てきたものは、言わば「コンパクトな」正規表現でした。見てわかったように、読みづらく、たとえその場で理解できても半年後に再び読めるか疑わしいものです。本当に必要なのは、行間のドキュメントなのです。

Pythonには冗長な正規表現と呼ばれる記法があります。冗長な正規表現は、コンパクトな正規表現とは2点の違いがあります。
  • 空白スペースは無視されます。スペース、タブ、改行はスペース、タブ、改行としてはマッチしません。全くマッチしないのです(スペースを冗長正規表現でマッチしたいときは、直前にバックスラッシュを入れてエスケープする必要があります)。
  • コメントは無視されます。コメントは冗長な正規表現の中ではPythonコードのコメントのように扱われます。# で始まり、行の最後まで続きます。この場合、ソースコードの中のコメントにあるような複数行のコメントになりますが、同じように動作します。
例を見ると理解しやすいでしょう。先ほどのコンパクトな正規表現をもう1度見てみましょう。次にそれを冗長な正規表現に書き換えてみましょう。

以下が例になります。

>>> pattern = '''
    ^                   # beginning of string
    M{0,3}              # thousands - 0 to 3 Ms
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    '''
>>> re.search(pattern, 'M', re.VERBOSE)                 # ①
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         # ②
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   # ③
<_sre .sre_match="" 0x008eeb48="" at="" object="">
>>> re.search(pattern, 'M')                             # ④

①冗長な正規表現を使うときに、忘れてはならない最も重要なことは、引数を追加することです。re.VERBOSEはreモジュールで定義された定数で、patternが冗長正規表現として扱われることを示しています。見るとわかるように、このpatternには多くのスペース(すべて無視される)があり、いくつかのコメント(すべて無視される)があります。スペースとコメントを無視すると、前節で見た正規表現と全く同じになりますが、こちらのほうがずっと読みやすいです。

②文字列の開始にマッチし、最大3つのMが1つ、CM、LXXX、IX、文の末尾がマッチします。

③文字列の開始にマッチし、最大3つのMが3つ、DCCC、LXXX、VIII、文の末尾がマッチします。

④これはマッチしませんなぜでしょうか?re.VERBOSEフラグがないからです。そのためre.search関数は非常に多くのスペースとハッシュマークを含んだコンパクト正規表現としてpatternを扱うのです。Pythonは正規表現が冗長なのかどうかを自分では判断できません。明示的に冗長であると記述しない限り、Pythonは全ての正規表現をコンパクトと解釈します。

5.6. ケース・スタディ:電話番号の解析

ここまでは、pattern全体にマッチすることに注目してきましたので、patternがマッチする・しないのどちらかでした。正規表現は、もっと強力な手法です。正規表現がマッチしないときにも、特定の部分を抽出してどこに何があるかを探すことができます。

この例も、私が以前に実際の業務で出会った課題の1つから来ています。アメリカの電話番号を解析するという課題でした。クライアントの要望は1つのフリーフォームに数字を入力できるようにすることでしたが、エリアコード、トランクプリフィックス、電話番号、さらにオプションで内線番号は、会社のデータベースに別々に保管したいというものでした。この目的のための正規表現の例をウェブ上で沢山見つけましたが、十分なものはありませんでした。

入力したい番号はこのようなものです。

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234
実にバラエティに富んでいますね! いずれもエリアコードは800で、トランクは555、残りの番号は1212です。内線がある場合は、内線番号1234です。

電話番号が解析できるコードを作っていきましょう。この例が第一歩です。

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  # ①
>>> phonePattern.search('800-555-1212').groups()             # ②
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                 # ③
>>> phonePattern.search('800-555-1212-1234').groups()        # ④
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'NoneType' object has no attribute 'groups'

①正規表現は、常に左から右に読んでいきます。この場合は文字列の開始にマッチし、次に(\d{3})にマッチします。\d{3}とは何でしょうか? \dは「数字(0から9)であれば何でもよい」という意味で、{3}は「3つの数字とマッチする」という意味です。前に{n, m}というバリエーションがありました。括弧の中を合わせると、「3つの数値にマッチし、あとで言えるように覚えておくように」という意味になります。次はハイフン記号にマッチし、さらにもう1組の3つの数値にマッチします。また1つハイフン記号があります。さらに4つの数値にマッチします。そして文字列の末尾にマッチします。

②正規表現のパーサーがプロセス上で記憶しているグループにアクセスするためには、searchメソッドが返すオブジェクトでgroups()メソッドを使います。これは正規表現内でのグループの個数にかかわらず、グループのタプルを返します。この場合、3つのグループを定義していて、3桁、3桁、4桁の数字になります。

③この正規表現では、最終解にはなりません。内線番号が末尾にある場合を扱えないからです。このため、正規表現を拡張する必要があります。

④このようなことがあるため、プロダクション・コードでsearch()とgroups()を"つなげて"はいけません。search()メソッドでマッチしなかったときはNoneが返りますが、Noneは正規表現にマッチするオブジェクトではありません。None.groups()は明白な例外が出ます。Noneはgroups()メソッドをもたないからです(もちろん、この例外がコードの深い階層で起こったら少しわかりにくいかもしれません。経験上、そうなってしまいます)。

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  # ①
>>> phonePattern.search('800-555-1212-1234').groups()              # ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                       # ③
>>>
>>> phonePattern.search('800-555-1212')                            # ④
>>>
①この正規表現はほとんど変わっていません。前と同様に、文字列の最初の3桁の数値を記憶、ハイフン、3桁の数値を記憶、ハイフン、4桁の数値を記憶します。ここからが新しい部分で、ハイフンと、新たに記憶される数値、そして文字の末尾があります。

②記憶されるグループが4つになったので、groups()メソッドは新しい4要素のタプルを返します。

③残念ながら、この正規表現も最終解ではありません。なぜなら、番号同士がハイフンで分けられることを想定しているからです。スペース、コンマ、ドットで分けられていたらどうでしょうか?より一般的な解にするために、さまざまな区切り方に対応する必要があります。

④おっと! この正規表現はしたいことができないばかりか、退化してしまいました。これでは、内線番号がないものは解析できません。これは全く的外れです。内線番号があれば、それも知りたいのですが、内線がない場合も、主番号以外の部分を知りたいのです。

次の例は、番号とは別の部分を扱うための正規表現です。
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  # ①
>>> phonePattern.search('800 555 1212 1234').groups()  # ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()  # ③
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')              # ④
>>>
>>> phonePattern.search('800-555-1212')                # ⑤
>>>
①驚かないで見てください。文字列の最初にマッチして、3桁のグループ、\D+と続きます。どうなっているかというと、\Dは数字以外のすべての文字にマッチし、+は1つ以上を意味します。つまり\D+は1つ以上の数字ではない文字にマッチします。これでハイフン記号だけでなく、他の区切り記号にもマッチするようになります。

②ハイフンの代わりに\D+と使うと、電話番号がハイフンではなくスペースで分けられていてもマッチすることができます。

③もちろん、ハイフンで分けられた番号にも有効です。

④残念ながら、これでもまだ最終解にはなりません。区切りが1つもなく、スペースもハイフンもない場合はどうしたらよいのでしょうか。

⑤おっと! 内線番号が必ず無いといけないという問題はまだ修正できていません。しかし、これも同じ方法で解決できます。
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  # ①
>>> phonePattern.search('80055512121234').groups()      # ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()  # ③
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()        # ④
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')           # ⑤
>>>
①前回からの変更点は1点だけで、+を*にしたことです。番号部分の間に、\D+ではなく\D*を配置しています。+の意味は「1以上」でしたね?一方、*の意味は「0以上」です。つまり、これで電話番号の間に区切り文字が1つも無い場合でも解析できるのです。

②いやはや、これは効果ありです。文字列の最初にマッチして、3桁の数字を記憶して(800)、数字以外の文字は無し、3桁の数字を記憶して(555)、数字以外の文字は無し、4桁の数字を記憶する(1212)、さらに任意の桁の数字を記憶して(1234)、文字列の末尾をマッチします。

③他のバリエーションでも有効です。ハイフンの代わりにドット、スペース、内線番号の前にxでも構いません。

④ついに、長く立ちはだかった問題を解くことができました。内線番号はオプションになっています。内線がない場合は、groups()メソッドは4要素のタプルを返しますが、4番目の要素は空の文字列になります。

⑤悪いニュースばかり知らせるのも嫌なのですが、まだ終わりじゃありません。ここでの問題は何でしょうか。エリアコードの前に追加の文字がありますが、正規表現では文字列の最初にエリアコードがくると想定しています。大丈夫です、「0以上の数字以外の文字」と同じテクニックを使ってエリアコードの前の部分をスキップすればよいのです。

次の例では、電話番号の前の文字を、どのように扱うかを示します。
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  # ①
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                  # ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                            # ③
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                      # ④
>>>
①冒頭に\D*を加え、0以上の数字以外をマッチし、それから最初に記憶するグループ(エリアコード)に進みます。数字でない文字は記憶しない(括弧に入っていない)ことに注意しましょう。見つけた場合はスキップして、エリアコードを探して記憶し始めるのです。

②エリアコードの前に括弧があった場合でも、電話番号の解析ができています(エリアコードのあとの括弧はすでに対処しています。最初に記憶されるグループのあとに\D*があるので、数字でない区切りとしてマッチされます)。

③これまで動いていたものが壊れていないか、ケアレスミスのチェックをしましょう。始めにある文字全体はオプションなのでマッチしなくても問題ありません。文字列の最初にマッチし、数字だけを3桁(800)を記憶し、数字以外にマッチ(ハイフン)、3桁(555)を記憶し、1つの数字以外の文字にマッチ(ハイフン)、4桁(1212)を記憶し、数字以外の文字列は無し、0桁の数字を記憶し、文字列の末尾にマッチします。

④正規表現で何も出てこないのを見て、目を擦りたくなってしまいますね。なぜ電話番号がマッチしないのでしょうか?それはエリアコードの前に「1」があるからで、エリアコードの前はすべて数字以外を想定して(\D*)を使っていたからです。やれやれ。

少し考えてみると、これまでの正規表現は文字列の最初からマッチしていました。しかし、今わかったように文字列の最初には不確定の長さの文字があるかもしれませんが、ある場合は無視したいのです。これらをすべてスキップするためのマッチを試みるよりも、別のアプローチをします。文字の最初には明示的にはマッチさせなければよいのです。そのアプローチ方法を次の例で示します。

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  # ①
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()         # ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        # ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()                      # ④
('800', '555', '1212', '1234')


①^を無くしたことに注意しましょう。これで今後は文字列の開始点をマッチしなくなります。正規表現では、入力の全体に対してマッチしなければならないという訳ではありません。正規表現のエンジンはどこがマッチ開始点であるかを一生懸命探して、そこから始めます。

②これで解析に成功しました。電話番号の前に文字列や数字があっても、どんな種類の区切りが番号の両隣にあっても大丈夫です。

③あっているかを確認します。

④これも大丈夫です。

正規表現が、簡単に制御不能になることがわかりましたか?前のバージョンを少し見てみましょう。2つの違いを言えるでしょうか。

最終解を把握したことでしょうが、(これこそが最終解です。もしこの正規表現が扱えない新しい事例を見つけても、私に教えないでください)、何故こうしたかを忘れる前に、冗長正規表現で書き出してみましょう。
>>> phonePattern = re.compile(r'''
                # don't match beginning of string, number can start anywhere
    (\d{3})     # area code is 3 digits (e.g. '800')
    \D*         # optional separator is any number of non-digits
    (\d{3})     # trunk is 3 digits (e.g. '555')
    \D*         # optional separator
    (\d{4})     # rest of number is 4 digits (e.g. '1212')
    \D*         # optional separator
    (\d*)       # extension is optional and can be any number of digits
    $           # end of string
    ''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()  # ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                          # ②
('800', '555', '1212', '')

①複数行で書かれている以外は、前と全く同じ正規表現です。同じ結果が出ても驚きはありません。

②最後のケアレスミスチェックをします。問題ありません。完了です。

5.7. まとめ

ここまで見てきたものは、正規表現でできることの氷山の一角でしかありません。言い換えると、今は正規表現に圧倒されていると思うかもしれませんが、大丈夫です。まだ何も見ていないようなものですから。

この章で、これらのテクニックを身につけたはずです。

  • ^は文字列の最初にマッチします。
  • $は文字列の最後にマッチします。
  • \bは単語の境界にマッチします。
  • \dは数字なら何でもマッチします。
  • \Dは数字以外なら何でもマッチします。
  • x?はオプションの文字xにマッチします(0回または1回のxにマッチする)
  • x*は0回以上のxにマッチします。
  • x+は1回以上のxにマッチします。
  • x{n, m}はn回以上m回以下のxにマッチします。
  • (a|b|c)はa、b、cのいずれか1つにのみマッチします。
  • (x)は通常、マッチしたものが記憶されるグループです。その値はre.searchで返るオブジェクトにおいてgroups()メソッドを使うことで取り出すことができます。
正規表現は非常に強力ですが、すべての問題に対して正しい解法だという訳ではありません。正規表現を使うのが適切なとき、正規表現が問題を解決してくれるとき、正規表現が解く問題よりも引き起こす問題の方が大きくなるときを、よく勉強して知っておくことが重要です。

0 件のコメント:

コメントを投稿