Pythonで正規表現:グループ化を使ってみる

正規表現でマッチした文字列の全てに興味がある訳ではないことがしばしば起こる。例えば固定電話番号のマッチングでは市外局番・市内局番・加入者番号がマッチすることを要求するが興味があるのは加入者番号だけであるというようなばあいである。このばあいはマッチした文字列全体ではなく、加入者番号のみを表示すれば足りる。このようなばあいにグループ化が使える。

グループ化は(正規表現)とグループ化したい正規表現の部分を丸括弧()で括ってやる。グループ化は入れ子(表現1(表現2))でも直列(表現1)(表現2)でも構わない。グループには番号が振られる。正規表現全体はグループ0、正規表現を右に検索し左丸括弧に出会うと番号はインクリメントし、対応する右丸括弧までがこのグループに所属することになる。実例を示す:


#coding: utf-8
import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (\d{4}) #仙台(22)
    | \d{3}-\d{2}- (\d{4]) #松本(263)
    | \d{4}-\d-    (\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('022-345-9876')
if m :
    print(m.groups())
else:
    print(m)

この例では正規表現に六個のグループが使われている。メソッドgroups()はこの六個のグループに対応する表示をタプルの形で取り出すことができる。この実例の結果は

('22-345-9876', None, '9876', None, None, None)

グループ1とグループ3でマッチする表現があることが分る。Noneは該当するグループにマッチする文字列がなかったことを意味する。

このグループ化のPython拡張としてグループに名前を付けることができる。それには(?P<名前>…)というふうに左丸括弧の次に?P<名前>を挿入する。実例を示す:


#coding: utf-8
import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(?P<電話番号>
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (?P<固定0>\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (?P<固定1>\d{4}) #仙台(22)
    | \d{3}-\d{2}- (?P<固定2>\d{4]) #松本(263)
    | \d{4}-\d-    (?P<固定3>\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (?P<携帯>\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('070-2345-9876')
if m :
    print(m.groupdict())
else:
    print(m)

メソッドgroupdict()によってグループ名をキーとしマッチした文字列を値とする辞書で結果を取り出すことができる。結果は

{'電話番号': '70-2345-9876','固定0': None,'固定1': None,
 '固定2': None, '固定3': None, '携帯': '5-9876'}

となり、この電話番号は携帯で加入者番号が5-9876であることが表示される。

Pythonで正規表現:冗長(Verbose)な表現

正規表現はとかく呪文のような読み難い表現になり勝ちであるごが、Pythonでは少し工夫がされていて冗長な表現ができる。実例を示す:


import re
telNumber = re.compile(r"""
^0    #ゼロ発信
(
    #固定電話(市外・市内・加入者)
      [36]-\d{4}- (\d{4})  #東京(3)・大阪(6)
    | \d{2}-\d{3}- (\d{4}) #仙台(22)
    | \d{3}-\d{2}- (\d{4]) #松本(263)
    | \d{4}-\d-    (\d{4}) #伊豆大島(4992)
    #携帯電話(キャリア・管轄・加入者)
    | [7-9]0-\d{3} (\d-\d{4})    
)
""", re.VERBOSE)

m=telNumber.search('070-2345-9876')
if m :
    print(m.group(0))
else:
    print(m)

日本の電話番号のマッチングのための正規表現であるが、コメント(#)や空白・改行を使って見やすくできる。

Pythonで正規表現:キャレットの役割(続き)

前のブログの続きである。

正規表現上でキャレットは前述の二つの位置以外では特別な意味は持たないが特殊記号にはちがいない。従って単なる文字としてキャレットとマッチしたいときには

\^

とバックスラッシュ(\)を使う。

[^^]

これは先頭のキャレットが補集合キャレットで二番目のキャレットは単なる文字としてのキャレットである。和集合のなかでは補集合キャレットを例外として全ての特殊記号は単なる文字として振舞う。上の正規表現はキャレットでない任意の一文字でマッチする。

Pythonで正規表現:キャレットの役割

正規表現ではキャレット(^)は特別な役割をしている。

  • 正規表現の先頭にキャレットがあるとそれは対象になる文字列の先頭でマッチングを検証することを意味する。
^abc

の正規表現では文字列abcdはマッチするが、dabcはマッチしない。

  • 文字クラス(角括弧で括られた文字の和集合、例[0-9abc]。この位置で和集合のどれかの文字であるとマッチする)の先頭にキャレットがあるとその補集合、つまりこの和集合のどの文字でもないとマッチすることを意味する。
[^0-9abc]

この正規表現ではこの和集合以外の文字であるとマッチする。

[^a]

これも有効な表現である。

  • この二つの位置以外ではキャレットは特別な意味を持たない。因みに
^a

は先頭の文字がaである文字列でマッチする。

^[^a]

は先頭の文字がa以外である文字列でマッチする。

Pythonで正規表現:先読みアサーション(再論)

具体的な例から考察しよう。ファイル名は基幹名(basename)と拡張子(extension)をドット(.)で区切って表現する。例えば、ファイル名abc.txtはabcが基幹名で、txtが拡張子である。

例えば多くのファイル名のマッチングのタスクで、特定の拡張子を持つファイル名をマッチングから外したいとする(例えば拡張子bat)。

この逆、つまり特定の拡張子を持つファイル名のみマッチングさせたいという処理は極めて簡単である。正規表現で書くと

.*[.]bat$

区別に関心のない基幹名(ドットを含めて)はドットで終わる任意の長さの任意の文字列とのマッチだから.*[.]となる。
この処理の逆、つまり特定の拡張子を持つファイル名をマッチングから除外しそれ以外の多くの拡張子を持つファイル名をマッチさせたいというのが本来のタスクである。このようなばあいに使えるのが「先読みアサーション」である。「先読みアサーション」には否定と肯定とがあるが、ここでは否定を使う。

パターンAパターンB

パターンAまでマッチングが進んだ時点でこれ以降がパターンBであるかどうかをその時点で検証し、であるとマッチングは失敗とする。ないとマッチングは継続する。問題に即して正規表現で書くと

.*[.](?!bat$)

先読みアサーション(否定)は(?!正規表現)と書く。この時点以降の文字列がこの正規表現とマッチするか検証する。それ以降で例えばbatchではマッチしない。rebatもマッチしない。マッチするものはbatのみである。これがマッチするとマッチング作業は失敗である。それがマッチしないと、後はなんの制限もなしにマッチさせればよいので最終的な正規表現は以下のようになる:

.*[.](?!bat$).*$

後読みアサーションというものもある。これも否定と肯定がある。この否定は(?<!..)と書く。例えば「ファイル名の基幹名がabc以外であるとマッチする」というような処理で使える。ファイルの基幹名がabc以外の全てのファイル名にマッチする正規表現は以下のようになる。

(?<!^abc)[.].*$

これは左からドットの位置まで探索を進め、そこで振り返って先頭からドットまでの間の文字列がabcであるとマッチングは失敗とする。それ以外では探査を右に進め通常のマッチングを続ける。この例では.*$となっているので文字列の最後まで任意の長さの任意の文字列がマッチされる。しかし結果としてマッチされる文字列は[.].*$の部分のみである。基幹名もマッチさせるには以下のようにする:

.*(?<!^abc)[.].*$

これで基幹名がabc以外の全てのファイル名がマッチされる。拡張子がなんであれ、基幹名がabcのファイル名はマッチに失敗する。

先読み(後読み)アサーションはこのように上手く使うと大変の便利な機能である。

 

サーン・ジャイアントは千年以上前の作品

これは最近のNewScientistの記事のタイトルである。

サーン・ジャイアントの画像

記事によればこの遺構の年代測定が始めて行われて予想に反して千年以上も古いものであることがわかったという。というのはこの遺構の最古の記録は300年ほど前であり、地元のサーン・アバス(Cerne Abbas)村の教会のものである。これ以前にはこの遺構の記録はない。

この遺構(像)はサーン・アバス村を見下ろす丘の側面に見えるもので、溝を掘りそこに白チョークを敷き詰めて創ったものである。

英国にはこのような遺構が三つある。他の二つはLong Man at Wilmingtonとアフィントン・ホワイト・ホース(Uffington White Horse)である。 Uffington White Horseは以前このブログでも取り上げたものであるが確実に先史時代のものである。

Pythonで正規表現:先読みアサーション

拡張子を持ったファイル名の全てにマッチした正規表現は簡単で


.*[.].*$

となる。

次に拡張子batを持つファイル名以外のファイル名の全てにマッチした正規表現を見てみよう。


.*[.]bat$

と書くと目的に反対に拡張子batを持つファイル名のみにマッチする正規表現になる。


.*[.]([^b]..|.[^a].|..[^t])$

と書くと拡張子の部分は「先頭の文字bがない三文字か、真ん中に文字bがない3文字か、末尾に文字tがない三文字か」のマッチ要求になりよさそうだ。しかし拡張子は三文字とは限らない。

もっとすっきりと表現できる正規表現がある。それが先読みアサーションである。詳細は正規表現 HOWTOにある。


.*[.](?!bat$)[^.]*$

ここで(?!..)は否定先読みアサーションと呼ばれる記述で、今のばあいこの記述位置に文字列bat$があるとマッチは失敗となる(つまり最後尾がbatなっていると失敗)。ないと先に進む。[^.]*$は最後尾から文字.を含まない任意の長さの文字列という意味である。

正規表現 HOWTOには「このパターンで [^.]* を使うことで、ファイル名に複数のドットがあったときにも上手くいくようになります。」とあるが意味不明である。例えば、abc.cd.exeのようなファイル名を考える。このばあいabc.cd.までが.*[.]によってマッチングがされ、[^.]*$によってexeをマッチングする。

 
.*[.]      abc.cd.
[^.*]$      exe

従って複数のドットを処理しているのは.*[.]の部分のように思える。

Pythonで正規表現:氏名

最後に残ったのが氏名行である。困ったことに氏名を特定する手掛りはほとんどなにもない。ここでは安直に残った行データが氏名行であるする。

全体を纏めると:

  • 最も規則が厳しい郵便番号行を始めに検出する。
  • 次に電話番号行を特定する。
  • その次に住所行を確定する。
  • 残った行データが氏名データである。

これらをLibreOfficeのマクロに組み込む。

【実行例】

LibreOffice・Calcの実行画面

このマクロはここにある。

Pythonで正規表現:住所

住所録の住所行の検出の問題である。

手掛りは住所特有な文字だろう。


#coding: utf-8
import re
msgs = ['仙台市泉区天神澤', \
        '山梨県東八代郡大沢町', \
        '東京都千代田区霞ヶ関', \
        '北海道小樽市小牧二丁目2-3', \
        '山県有朋',\
        '布施市介']
for i, msg in enumerate(msgs):
    ms = []
    m1 = re.search(r'都|道|府|県', msg)
    ms.append(m1)
    m2 = re.search(r'郡|市', msg)
    ms.append(m2)
    m3 = re.search(r'区|町|村|大字', msg)
    ms.append(m3)
    m4 = re.search(r'丁|番|号|字', msg)
    count = 0
    for m in ms:
        if bool(m):
            count+=1
    if count >= 2:
        
        print(msg, ' OK')
    else:
        print(msg, ' NG')

規模の大きさをグループにして探す。「都・道・府・県」という文字を含んでいること。次は「郡・市」、次は「区・町・村・大字」、最後は「丁・番・号・字」。プログラムではこれらのグループ化された文字を二ヶ所以上含んでいると住所としてしている。従って「山県有朋」や「布施市介」は住所でない。

Pythonで正規表現:電話番号

郵便番号については既に議論した。次は電話番号行を特定するための正規表現である。
電話番号行は
行の先頭が’電話’、’Tel’、’TEL’で始まる文字列の行(例:電話022-222-4444)。または0から9の半角数字、記号’-‘、記号'(‘、記号’)’の文字からのみなる文字列(例:022(222)2345や022-227-2345)とする。
この条件をPythonで書くと:


#coding: utf-8
import re
msgs = ['電話022-222-2222', 'Tel022-222-2345', 'TEL022-228-5678', \
        '022(2267)5678', '234-3456-4567', '456局345(65678)','34+456+4567']
for i, msg in enumerate(msgs):
    m1 = re.search(r'^電話|Tel|TEL', msg)
    #print(i, m1)
    m2 = re.search(r'[^0-9-()]', msg)
    #print(i, m2)
    if bool(m1) or not bool(m2):
        print(msg, ' OK')
    else:
        print(msg, ' NG')

m1は文の先頭に’電話’等の文字列があることを条件にしている。m2は当該の位置にない文字を指定している。つまり、0から9まで半角数字、記号’-‘、記号'(‘、記号’)’以外のものがあるとマッチする。文字列のどこかに該当する文字があるとマッチする1文字マッチの条件である。m2にはその文字が代入される。その文字列の最後まで探して該当する文字がないとm2はNoneになる。つまり、除外した文字のみでできている文字列であるかどうかを判定できる。

結果は
電話022-222-2222 OK
Tel022-222-2345 OK
TEL022-228-5678 OK
022(2267)5678 OK
234-3456-4567 OK
456局345(65678) NG
34+456+4567 NG
となる。

Pythonによる正規表現はここが詳しい。