正規表現は「文字列のパターン」が、ある特定の文字列にあるかどうかを調べたり、その一部を抽出したりするための機能です。初心者にはかなり難しい部類のテクニックですが、Pythonだけではなくほぼすべてのメジャー言語でサポートされている機能ですので紹介させていただきます。なお、正規表現の「文字列のパターンの指定の仕方」はどの言語でもほぼ同じですが、関数などには若干の違いがあります。

正規表現の概要

PC上の画像ファイルを検索する際に“*.jpg”などという形で指定したことはありませんか。これは“.jpg”という拡張子がつくファイルすべてということなので、“*”は「なんでも」という意味になります。

正規表現は“*”を「なんでも」と解釈したように、「文字列のパターン」を定義する特別な書式だといえます。書式を覚えるのはなかなか大変で、初心者のうちは使いこなすのが難しいと思いますが、基本を覚えるだけでもそこそこパワフルな使い方ができます。

正規表現の書式について扱う前に、実際にその利用例を見てみましょう。文字列が数字からのみ構成されているかどうかをチェックする関数を書くとします。いろいろな実装方法があると思いますが、今回は文字列の一文字一文字が0~9の数字であるか否かをチェックし、すべて数字なら文字列は数字と判断するというアルゴリズムで書いてみます。

def is_integer_text(text):
    numbers = ['1','2','3','4','5','6','7','8','9','0']
    if len(text) == 0:
        return false
    for c in text:
        if c not in numbers:
            return False
    return True

print(is_integer_text('31'))   # True
print(is_integer_text('3x1'))  # False

ちょっと長いですが、プログラム自体はそれほど複雑ではないですね。これを正規表現で書き直すと以下のようになります。

import re
def is_integer_text(text):
    return re.match('\d+$', text) != None  

print(is_integer_text('31'))   # True
print(is_integer_text('3x1'))  # False

どうです。意味はさっぱりわからないかもしれませんが、実質一行で実装されています。かなり短くなりましたね。今回のような数字の判定程度であれば、自力でアルゴリズムを書けるかもしれませんが、より複雑な条件になってくると、そのアルゴリズムを考えるのも一苦労です。でも、正規表現を使いこなせれば、簡単に解決できるかもしれません。

正規表現の使い方

正規表現の使い方は簡単です。大まかにいうと、その場限りの簡単な利用方法と、何度も利用する場合に使う高速な利用方法の2つがあります。まずは前者についてお話しましょう。

正規表現を使うためには「re」というパッケージをimportする必要があります。reは正規表現「regex」の略です。そして次にreの関数を使った検索やマッチング抽出を行います。

文字列の検索(有無の確認)

例として、文字列の頭が数字から始まるかどうかの判定を、正規表現を使って実行したいと思います。実現方法は、主に2つあります。

import re

# match  先頭からのみ対応
print(re.match('\d+', '123abc') != None)   # True
print(re.match('\d+', 'abc123') != None)   # False

# search
print(re.search('^\d+', '123abc') != None) # True
print(re.search('^\d+', 'abc123') != None) # False
print(re.search('\d+', 'abc123') != None)  # True

# 補足
# ^ は先頭から
# \d は数字
# + は一回以上の繰り返し

まずはじめにreのimportを行い、その次にさまざまな文字列に対して正規表現によるパターンマッチングを実施しています。具体的にはmatch関数とsearch関数です。その判定の仕方は、

re.match(正規表現の文字列, 対象の文字列)
re.search(正規表現の文字列, 対象の文字列)

としています。matchとsearchの違いは、正規表現“\d+”を利用しているサンプルを見てもらうとわかると思いますが、matchは文字列の先頭からしかマッチを確認できないのに対して、searchは途中からもマッチできるということです。両者ともマッチするテキストがなければNoneを返します。つまり、Noneでなければマッチしているということです。

上記の「正規表現の文字列」にあたるところの文字列を調整することで、どのような「パターン」に合致するかの判定を変えられます。今回ですと“^\d+” は「^」が先頭、「\d」が数字、そして「+」は前の記号のn-1個の繰り返しという意味です。つまり先頭が数字、そのあとが何が何文字続いてもいいという意味になります。数字から始まる文字列ということですね。この条件に合致する文字列はマッチオブジェクトが返され、合致しない場合はNoneが返されます。そのため返り値を比較して、Noneでなければマッチしていると判断することが可能です。なお、matchは必ず先頭から確認するので「^」は書いても書かなくても同じです。

Matcher オブジェクト

re.matchやre.searchにマッチしない場合はNoneが返ってくると説明しましたが、では合致するときには何が返ってくるのでしょうか。実際に試してみます。

import re

# マッチしない
m = re.search('\d+', 'abcdef')
print(type(m)) # <type 'NoneType'>

# マッチする
m = re.search('\d+', 'abc123def')
print(type(m)) # <type '_sre.SRE_Match'>

マッチする例で返ってきた値のtype(型)を確認したところ、Matchと書かれたものが返ってきましたね。いわゆる「マッチ型」と呼ばれている型です。このマッチ型のオブジェクトを操作することで、その詳細を確認することができます。操作には以下のようなものがあります。

import re

m = re.search('\d+', 'abc123def')
print(type(m)) # <type '_sre.SRE_Match'>
print(m.group()) # 123
print(m.start()) # 3
print(m.end())   # 6
print(m.span())  # (3, 6)

print(dir(m))
# ['__class__', '__copy__', '__deepcopy__', '__delattr__', '__doc__', '__format__', 
# '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', 
# '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'end', 
# 'endpos', 'expand', 'group', 'groupdict', 'groups', 'lastgroup', 'lastindex', 
# 'pos', 're', 'regs', 'span', 'start', 'string']

group()がマッチした文字列の取得、そしてstart()が開始位置のインデックス番号、end()が終了位置のインデックス番号です。span()はstart()とend()をまとめてタプルとして取得します。ほかにもいろいろあるようですが、これと後述するgroupsあたりをよく使います。

なお、すでにおわかりのようにマッチオブジェクトはマッチしないと返ってこないため、Noneに対してgroup()などの関数を呼び出さないように注意してください。一般的には以下のように、正規表現の前にマッチオブジェクトか否かの判定をします。

import re

m = re.search('\d+', 'abcdef')
# m = re.search('\d+', 'abc123def')

if(m != None):
    print(type(m))
    print(m.group())
else:
    print(type(m))

変数mにNoneが入っていると、“if(m != None):”ではなく“else:”節の処理が実行されます。この条件判定をしないとNoneに対してgroup()を呼び出すのでエラーが発生します。注意してください。

なお、以下のような書き方のほうが一般的かもしれません。

if m:
   マッチした時の処理
else:
   マッチしなかった時の処理

MatchオブジェクトはTrueとして評価されます。

マッチする文字列の部分抽出

次に合致する文字列の「一部分の抜き出し」を実施しましょう。使いどころとしてはいろいろあると思いますが、私はURLやemailアドレスを文字列から抜き出すのに最近利用しました。そういうときはたいていGoogleで検索して、賢い正規表現の書き方をコピペしてしまいますが(笑)。

例として文字列から数値を抜き出すプログラムを書いてみます。数字の文字が続いていれば、それらをまとめて抜き出します。

import re
m = re.match('[^\d]*(\d+).*$', 'hello324 hoge321')
print(m.groups()) # ('324',)

m = re.match('[^\d]*(\d+).*$', '324 hoge321')
print(m.groups()) # ('324',)

上記の正規表現では [] と () が使われています。[] はカッコの中に示されたもののいずれかということです。そのなかで使われている ^ は否定なので“[^\d]”は数字以外という意味になります。レンジ指定のようなものも可能です。[a-c]は、[abc]と同じ意味で、a b c のいずれかという意味になります。よく使われるのはアルファベットすべての[A-z]あたりでしょうか。大文字も小文字もヒットします。

次に()ですが、これ自体はマッチには関与せず、()の中の箇所を抽出するという意味で使われます。上記だと(\d+)は数字を抜き出すという意味になります。

re.matchやre.searchで返されたオブジェクトを変数mに格納し、それに対してgroups() 関数を呼び出しています。この groups()はマッチした文字列中の()をすべてタプルとして抜き出すというものです。ほかにも group(n)という関数もあり、これは0からカウントしたn番目の()の中身を取り出します。今回は()がひとつしかないので、groups()で得たタプルには数字が入っているだけです。

なお、正規表現を少し細くすると“[^\d]”に続く“*”は0回以上の繰り返し、“.”はなんでもいいという意味、“$”は文字列の末という意味です。 つまり '[^\d]*(\d+).*$' は数字以外の文字列n個のあとにある数字を抽出するという意味になります。nは0でもよいので、いきなり数字から始まる例も抽出できています。

もう少し複雑な抽出の例を示しましょう。以下のように、時刻のあとにメッセージが続くような形式のログなどから要素を取得してくることもできます。“\s”は空白(タブなども含む)という意味です。

import re
m = re.match('(\d+)\s(\d+)\s([A-z]+)\s(\d+):(\d+):(\d+)\s([A-z\s]+)', '2015 10 Aug 23:01:14 Hello World')
print(m.groups()) # ('2015', '10', 'Aug', '23', '01', '14', 'Hello World')

複雑な形式のログなどを「1行のテキスト」として解析するよりも、上記のように分解して整理したほうが操作しやすいです。たとえば上記のタプルで「この日付からこの日付まで」といった指定は簡単ですよね。正規表現の部分抽出を使うことで、テキストよりもより使いやすいデータ構造を簡単に作ることができます。

マッチする文字列を全部取得

matchとsearch は最初にマッチした要素しか取得しませんが、すべて欲しい場合はfindall()という関数を利用します。この関数の返り値はリストで、その中身はマッチした部分文字列です。まったくマッチしない場合は空になります。

import re
l = re.findall('\d+', '123abc456def')
print(l) # ['123', '456']

l = re.findall('\d+', 'abc def')
print(l) # []

このfindallでも抽出はできます。()がひとつなら配列に文字列が直接入った状態で返され、()が2つ以上ならタプルの配列として返されます。

import re
l = re.findall('([A-z]+)=\d+', 'abc=123, def=456')
print(l) # ['abc', 'def']

l = re.findall('([A-z]+)=(\d+)', 'abc=123, def=456')
print(l) # [('abc', '123'), ('def', '456')]

正規表現のコンパイル(高速化)

最後にコンパイルについて話します。今までの正規表現はその場限りの利用で何度も利用するものではありません。ただ、正規表現の文字列から正規表現のオブジェクトの生成はコストが高い(時間がかかる)ので何度も同じ正規表現を使いまわす場合は以下のようにしたほうがよいでしょう。

import re
regex = re.compile('[^\d]*(\d+).*$')

m = regex.match('hello324 hoge321')
print(m.groups()) # ('324',)

m = regex.match('324 hoge321')
print(m.groups()) # ('324',)

第一引数に正規表現を指定せずに、対象の文字列を指定するようになったこと以外は、大きな違いはありません。正規表現は書かないと覚えられないので、利用しながら感覚をつかんでください。また高度な指定については先人の知恵を借りるのもいいかもしれません。

正規表現の詳細文法とサンプル

正規表現の詳細な文法について扱います。すべてを記載することはできないので、詳細はドキュメントなどをご参照ください。

まず初めに1文字にマッチングする特別なシーケンスです。先の数字のようなものです。

  • \d : 任意の十進数
  • \D : 数字以外の文字すべて
  • \s : 空白文字。タブや改行なども含まれる
  • \S : 空白文字以外の文字すべて
  • \w : アルファベット、数字、下線のすべて
  • \W : アルファベット、数字、下線以外のすべて

規則性がありますね。小文字と大文字で反対の意味になっています。

次に繰り返しです。これも先のサンプルにあったように、記号の前の文字を繰り返すという意味になります。

  • ? : 0回か1回の繰り返し
  • + : 1回以上の繰り返し
  • * : 0回以上の繰り返し
  • {m,n} : 最小m回で最大n回

そのほかには以下のようなものがあります。

  • ^ : 文字列の先頭
  • $ : 文字列の末尾
  • . : 任意の一文字(なんでもいい)
  • [] : 文字クラスの指定。[]のうちに記されたいずれか

注意して欲しいのは [] で、この中に書かれた特殊記号は「別の特別な意味」を持ったり、意味を何も持たなかったりします。たとえば[^\d]とした場合、^ は数字「以外」という反対の意味を持ちます。一方、$などは何も意味を持たず、$という文字として扱われます。

一気に覚えようとせずに、正規表現が必要になるごとに、使いながら覚えていってください。サンプルプログラムを作ると、早く理解できるかもしれません。


演習1

以下の文字列から、正規表現を利用して英単語(アルファベットのみから構成される)をすべて抜き出してください。

'I have 2 pens.'

演習2

URLのみを、正規表現を利用して抜き出してください。

'Urs is http://example.com/index.html'

※解答はこちらをご覧ください。


次回はPythonにおける日本語の扱いについて解説します。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら