M.Hiroi's Home Page

Lightweight Language

新・お気楽 Python プログラミング入門

第 4 回 正規表現とジェネレータ

[ PrevPage | Python | NextPage ]

はじめに

前回は再帰定義を中心に、高階関数、関数のネスト、クロージャなど関数の機能を詳しく説明しました。今回は Python の強力な文字列処理の中心である正規表現 (regular expression) とジェネレータ (generator) を取り上げます。

正規表現は「文字列のパターンを示した式」のことです。一昔前は一部のエディタやツール [*1] で利用されていた正規表現ですが、今ではほとんどのスクリプト言語で正規表現を使うことができるようになりました。また、Python には正規表現だけではなく、文字列を操作するときに便利なメソッドが用意されています。まず最初に、メソッドを使った文字列操作から説明します。

-- note --------
[*1] 有名なところでは grep, sed, awk などがあります。

●文字列の検索

それでは、文字列の検索を行うメソッドから説明しましょう。表 1 を見てください。

表 1 : 文字列の検索メソッド
操作機能
S.find(sub) / S.rfind(sub)部分文字列 sub の位置を返す。見つからない場合は -1 を返す。
S.index(sub) / S.rindex(sub)部分文字列 sub の位置を返す。見つからない場合は ValueError を送出する。
S.count(sub)部分文字列 sub の出現回数を返す。

S は文字列を表します。find() と index() は文字列 S の前(左側)から部分文字列 sub を検索し、最初に見つけた位置を返します。rfind() と rindex() は文字列 S の後ろ(右側)から部分文字列を検索します。

部分文字列が見つからなかった場合、find() は -1 を返しますが、index() はエラーになります。例外はエラーのことで、ValueError は値が見つからない場合に発生するエラーです。例外処理は第 6 回で詳しく説明します。

count() は部分文字列の出現回数を数えて返します。どのメソッドも引数に start, end を指定することができます。start は検索開始位置で、end が終了位置です。指定方法はスライス操作 (図 1) と同じです。

簡単な例を示しましょう。

>>> a = 'foo bar baz foo bar baz'
>>> a.find('bar')
4
>>> a.rfind('bar')
16
>>> a.find('abc')
-1
>>> a.count('foo')
2

●文字の除去

文字列から余分な文字を取り除くには表 2 のメソッドを使います。

表 2 : 文字の除去
名前機能
S.strip() 先頭と末尾の文字を取り除く
S.rstrip() 末尾の文字を取り除く
S.lstrip() 先頭の文字を取り除く

引数に文字列 str を指定すると、str に含まれる文字を削除します。引数を省略した場合は空白文字を削除します。簡単な例を示しましょう。

>>> a = ' hello, world \n'
>>> a.strip()
'hello, world'
>>> a.lstrip()
'hello, world \n'
>>> a.rstrip()
' hello, world'
>>> a.rstrip('\n')
' hello, world '

行末の改行文字は rstrip('\n') で削除することができます。引数を省略すると、改行文字の前の空白文字も削除するので注意してください。

●文字列の分解と結合

文字列の分解はメソッド split() を、文字列の結合はメソッド join() を使います (表 3)。

表 3 : 文字列の分解と結合
名前機能
S.split() 文字列に含まれる単語をリストに格納して返す
S.join(ls) リストに格納されている文字列を連結して返す

split() は引数を省略すると、空白文字で単語を切り出してリストに格納して返します。引数に文字列を指定すると、それが単語の区切りになり、さらにその後ろに単語を切り出す回数を指定することができます。split() は文字列の先頭から単語を切り出しますが、末尾から単語を切り出していく rsplit() もあります。join() はリスト ls の要素 (文字列) の間に文字列 S を入れて連結します。

簡単な例を示しましょう。

>>> a = 'foo bar baz'
>>> b = a.split()
>>> b
['foo', 'bar', 'baz']
>>> a.split(' ', 1)
['foo', 'bar baz']

>>> ' '.join(b)
'foo bar baz'
>>> '\t'.join(b)
'foo\tbar\tbaz'
>>> ''.join(b)
'foobarbaz'

split() で単語を切り出す回数を指定すると、切り出した単語と残りの文字列をリストに格納して返します。split() を使うと、英単語の語数を数える wordcount (wc) は簡単に作ることができます。リスト 1 を見てください。

リスト 1 : 英単語の語数を数える

import sys

c = 0
for x in sys.stdin:
    c += len(x.split())
print(c)

標準入力 sys.stdin から 1 行ずつ読み込み、それを split() で単語に分解します。すると、リストの長さが単語の数になるので、関数 len() で長さを求めて c に加算します。これで英単語の語数を数えることができます。

●文字列の置換

文字列の置換はメソッド replace(old, new [,num]) を使います。replace() は文字列 old を見つけたら、それを文字列 new に置き換えます。引数 num は置換回数を指定します。省略すると、すべての old を new に置き換えます。簡単な例を示しましょう。

>>> a = 'foo bar baz foo bar baz'
>>> a.replace('foo', 'FOO')
'FOO bar baz FOO bar baz'
>>> a.replace('bar', 'BAZ', 1)
'foo BAR baz foo bar baz'

文字単位で置換を行いたい場合は、クラス str のクラスメソッド maketrans() とメソッド translate() を使います。str は文字列型を表すクラスです。クラスメソッドは クラス名.メソッド(引数, ...) の形式で呼び出します。クラスとクラスメソッドは第 5 回で詳しく説明します。

maketrans(from, to) は変換テーブルを作成します。from と to は文字列で、from[n] の文字は to[n] の文字に置換されます。たとえば from が 'xyz' で to が 'XYZ' とすると、x, y, z はそれぞれ X, Y, Z に置換するような変換テーブルを返します。translate() には maketrans() で作成した変換テーブルを渡します。

簡単な例を示しましょう。

>>> a = str.maketrans('xyz', 'XYZ')
>>> b = 'a b c x y z'
>>> b.translate(a)
'a b c X Y Z'

英大文字と英小文字の変換はメソッド lower() と upper() を使います。簡単な例を示します。

>>> a = 'a b c X Y Z'
>>> a.lower()
'a b c x y z'
>>> a.upper()
'A B C X Y Z'

このほかにも文字列には便利なメソッドが用意されています。詳細は Python のマニュアル 文字列メソッド をお読みください。

●正規表現の基礎知識

次は正規表現の基本について詳しく説明します。正規表現はある文字に特別な意味を持たせます。これを「メタ文字」といいます。このメタ文字を組み合わせることで、複雑な条件を表すことができます。Python で使う基本的なメタ文字を表 4 に示します。

表 4 : 正規表現で使用する基本的なメタ文字
メタ文字意味
| この前後にある正規表現のどちらかと一致する
* 直前の正規表現の 0 回以上の繰り返しに一致する
+ 直前の正規表現の 1 回以上の繰り返しに一致する
? 直前の正規表現に 0 回もしくは1回一致する
{m,n} 直前の正規表現の m 回以上 n 回以下の繰り返し
*? 直前の正規表現の 0 回以上の繰り返しに一致する(最短一致)
+? 直前の正規表現の 1 回以上の繰り返しに一致する(最短一致)
?? 直前の正規表現に 0 回もしくは1回一致する(最短一致)
{m,n}? 直前の正規表現の m 回以上 n 回以下の繰り返し(最短一致)
[ ] [ ] 内に指定した文字のどれかと一致する
[^ ] [ ] 内に指定した文字でない場合に一致する
. 任意の1文字と一致する
^ 行頭と一致する
$ 行末と一致する
( ) 正規表現をグループにまとめる
\ メタ文字を打ち消す
\A 文字列の先頭と一致
\b 単語境界と一致 (\w と \W の間の空文字列と一致)
\B \B 以外と一致
\d 数字と一致 ([0-9] と同じ)
\D \d 以外と一致
\s 空白文字と一致 ([ \t\n\r\f] と同じ)
\S \s 以外と一致
\w 英数字とアンダースコア _ に一致 ([_a-zA-Z0-9] と同じ)
\W \w 以外と一致
\Z 文字列の末尾と一致

Python のメタ文字は、このほかに後方参照や拡張表記があります。これらのメタ文字はあとで説明します。

●文字の指定

それでは、具体的に説明していきましょう。まず大前提として、メタ文字以外の文字はそれ自身の正規表現です。つまり、abc という正規表現は文字列 abc と一致します。それから、メタ文字を通常の文字として使いたい場合は、メタ文字の前にバックスラッシュ(円記号) \ を付けます。

メタ文字 . はどんな文字にも一致します。

a.c   => aac, abc, aAc
a..c  => aaac, abcc

次は文字クラス [ ] です。[ ] 中の文字のどれかと一致します。

a[ABC]c    => aAc, aBc, aCc
a[AB][CD]c => aACc, aADc, aBCc, aBDc

文字クラスはハイフン (-) を使って文字の範囲を表すことができます。- を含めたい場合は [ ] の中で先頭か最後に - を指定します。

[a-zA-Z]    => アルファベットと一致
[a-zA-Z_]   => \w と同じ
[0-9]       => 数字と一致 (\d と同じ)
[-a], [a-]  => a, - と一致

数字の正規表現は \d を使うことができます。英字文字列の正規表現は、アンダースコア ( _ ) を含んでもよければ \w を使うことができます。

文字クラスの先頭に ^ を付けると、指定した文字以外の文字と一致します。^ は先頭に付けたときに有効で、それ以外の位置では通常の文字として扱われます。

[^a-zA-Z]  => アルファベット以外の文字と一致
[^a-zA-Z_] => \W と同じ
[^0-9]     => 数字以外の文字と一致 (\D と同じ)
[^a-]      => a, - 以外の文字と一致

アルファベットとアンダースコア以外の文字と一致する正規表現は \W を使うことができます。数字以外の文字と一致する正規表現は \D を使うことができます。

●繰り返し

メタ文字 * と + と ? は繰り返しを指定します。* は直前の正規表現の 0 回以上の繰り返しと一致します。0 回以上とは空文字列にも一致するということです。

a*b => b  (a がない場合にも一致する)
       ab aab aaaab aaaaab など

+ は直前の正規表現の 1 回以上の繰り返しと一致します。* と違って空文字列とは一致しません。

a+b => ab aab aaaab aaaaab など(b とは一致しない)

? は空文字列もしくは直前の正規表現と一致します。

a?b => b ab

文字クラスと繰り返しを組み合わせることで、いろいろな文字列を表現することができます。

[a-zA-Z]+   => 英文字列と一致
a[a-zA-Z]*  => a で始まる英文字列と一致 (a 1文字とも一致)
a[a-zA-Z]+  => a で始まる英文字列と一致 (a 1文字には一致しない)
[0-9]+      => 数字列と一致 (\d+ と同じ)

{m,n} は繰り返しの回数を指定します。

\d{3,6}  => 3 桁以上 6 桁以下の数字と一致
\d{3,}   => 3 桁以上の数字と一致
\d{3}    => 3 桁の数字と一致
\d{0,}   => \d* と同じ
\d{1,}   => \d+ と同じ
\d{0,1}  => \d? と同じ

このように正規表現を使えば 3 桁以上の数字も簡単に指定することができます。

●最長一致と最短一致

ところで、*, +, ?, {m,n} の繰り返しは「欲張りマッチ」といって、文字列のもっとも左側(先頭に近い方)からもっとも長い部分文字列と一致します。これを「最左最長一致」と呼びます。伝統的な正規表現は最左最長一致の繰り返ししかありません。ところが、これでは困る場合があるのです。

たとえば、< と > の間にある文字列とマッチングさせようとして、<.*> という正規表現を書きました。この場合、<abc> と一致しますが、<abc>012<def> にも一致してしまいます。最初に現れる < > と必ず一致させたい場合は、繰り返しの後ろに ? を付けます。これを「最左最短一致」といいます。この場合は <.*?> と指定すると、最初の <abc> に一致します。

●グループ

繰り返しは他のメタ文字よりも優先順位が高いことに注意して下さい。たとえば、ab* は ab の繰り返しではなく、b の繰り返しになります。ab の繰り返しを実現するには ( ) を使って、正規表現をひとつのグループにまとめます。

(ab)+   => ab abab ababab abababab など
(ab)*c  => c abc ababc abababc ababababc など

なお、グループのカッコは一致した部分文字列を覚えておくためにも使われます。これは後方参照のところで説明します。

●位置の指定

$ と ^ は位置を指定するメタ文字です。^ は行頭を指定し $ は行末を指定します。^ は文字クラス内とは別の意味になるので注意してください。

^abcd    => 行頭の abcd と一致する
^[a-z]+  => 行頭にある英文字列と一致する
abcd$    => 行末にある abcd と一致する
[a-z]+$  => 行末にある英文字列と一致する

複数の行を一つの文字列にまとめる場合、文字列の中に複数の行頭や行末が存在することになります。\A は文字列全体の先頭と一致し、\Z は文字列全体の行末と一致します。\b は単語の境界と一致します。

\babc\b  => abc という単語と一致
            ABabcCD という文字列中の abc とは不一致

●選択

| は選択を表すメタ文字で、前後どちらかの正規表現と一致します。

ab|cd     => ab または cd
(a|b)c    => ac または bc
(ab|cd)e  => abe または cde

選択は他のメタ文字よりも優先順位が低いことに注意して下さい。ab|cd は (ab)|(cd) であり、a(b|c)d ではありません。

●正規表現の使い方

さて、正規表現の説明だけでは退屈なので、実際に Python で試してみましょう。Python で正規表現で利用する場合はモジュール re をインポートしてください。正規表現を使って文字列とマッチングを行う関数には match() と search() があります。

match(pattern, string [, flag])
search(pattern, string [, flag])

引数 pattern に正規表現を指定し、引数 string に文字列を指定します。flag は正規表現の動作を指定します。たとえば、英大文字小文字を区別しないで検索したい場合は、re モジュールで定義されている IGNORECASE または I を指定します。flag は省略することができます。

search() は文字列の中から正規表現と一致する部分列を検索しますが、match() は文字列の先頭から正規表現とマッチングするか調べるだけです。したがって、先頭文字が正規表現と一致しなければ、match() はマッチング失敗となります。

match() と search() は、マッチングに成功した場合は「マッチオブジェクト (Match Object)」を返します。失敗した場合は None を返します。Match Obect の主なメソッドを表 5 に示します。

表 5 : Match Object の主なメソッド
名前 機能
group() 正規表現と一致した文字列を返す
start() 一致した文字列の開始位置を返す
end() 一致した文字列の終了位置を返す
span() 一致した文字列の位置をタプル (s, e) で返す

それでは、簡単な例を示しましょう。

>>> import re
>>> a = re.search(r'\d+', 'abcd0123efgh')
>>> a
<re.Match object; span=(4, 8), match='0123'>
>>> a.group()
'0123'
>>> a.start(), a.end()
(4, 8)
>>> a.span()
(4, 8)
>>> b = re.match(r'\d+', 'abcd0123efgh')
>>> print(b)
None

Python の文字列はエスケープ記号 (バックスラッシュまたは円記号) が有効なため、正規表現のメタ文字として使うにはエスケープ記号を二重に書かなければいけません。たとえば、\d は \\d と書く必要があります。これでは面倒なので、エスケープ記号を無効にする raw string で正規表現 \d+ を指定します。

search() で 'abcd0123efgh' を \d+ で検索すると、'0123' とマッチングします。変数 a には Match Object がセットされ、a.group() で一致した文字列 '0123' を取り出すことができます。また、m.start(), m.end(), m.span() で一致した文字列の位置を求めることができます。数字は文字列の先頭にないので、match() はマッチング失敗となります。

●正規表現のコンパイル

正規表現は小さなプログラミング言語と同じで、正規表現のままではマッチングに時間がかかります。re モジュールは正規表現をコンパイルすることで、高速なマッチングを実現しています。関数 match() や search() は正規表現をコンパイルしてから文字列とマッチングを行いますが、同じ正規表現を何度も使う場合はあらかじめ正規表現をコンパイルしておくと便利です。正規表現のコンパイルには関数 compile() を使います。

compile(pattern [,flag])

compile() は正規表現 pattern をコンパイルして「正規表現オブジェクト (Regex Object)」を返します。flag は正規表現の動作を指定します(省略可)。そして、Regex Object 用のメソッド match() と search() を使って文字列とのマッチングを行います。

match(string [,pos = 0] [,endpos])
search(string [,pos = 0] [,endpos])

メソッド match() と search() は引数 pos と endpos で部分文字列を指定することができます。マッチングが成功した場合は、Match Object を返します。失敗した場合は None を返します。簡単な例を示しましょう。

>>> p = re.compile(r'\d+')
>>> p
re.compile('\\d+')
>>> a = p.search('abcd0123defg')
>>> a.group()
'0123'
>>> b = p.match('abcd0123defg')
>>> print(b)
None
>>> b = p.match('abcd0123defg', 4)
>>> b.group()
'0123'

compile() で正規表現 \d+ をコンパイルして Regex Object を変数 p にセットします。search() で検索すると '0123' とマッチングしますが、match() ではマッチング失敗となります。ここで、match() に部分文字列の開始位置に 4 を指定すると、マッチングは成功します。

●文字列検索ツールの作成

正規表現を使うと、指定した文字列をファイルから検索するgrep のようなツールは簡単に作成することができます。リスト 2 を見てください。

リスト 2 : 文字列の検索

import sys, re

p = re.compile(sys.argv[1])
with open(sys.argv[2]) as f:
    n = 1
    for x in f:
        if p.search(x): print('{:5d}: {}'.format(n, x), end='')
        n += 1

このプログラムは次のように起動します。

$ python grep.py pattern file

まず、モジュール sys の変数 argv から正規表現 pattern を取り出して compile() に渡してコンパイルします。次に、ファイル名 file を取り出して、with open() でファイルをオープンします。あとは、for x in f: でファイルから 1 行ずつ読み込み、search() で正規表現とマッチングするか調べます。一致した場合は行番号と文字列を表示します。

●コマンドラインオプションの解析

ところで、compile() の flag で IGNORECASE または I を指定すると、英大文字小文字を区別しないで検索することができます。grep.py を使うとき、この動作をコマンドラインからオプションで指定できると便利です。Python の標準ライブラリにはコマンドラインを解析するモジュール argparse が用意されています。

  1. argparse --- コマンドラインオプション、引数、サブコマンドのパーサー, (本家)
  2. Argparse チュートリアル, (本家)
  3. ArgumentParserの使い方を簡単にまとめた, (@kzkadc さん)

argparse の基本的な使い方は 3 の記事が簡潔にまとめられていて参考になると思います。作者の @kzkadc さんに感謝いたします。

基本的な手順は次のようになります。

  1. モジュール argpare をインポートする
  2. ArgumentParser() を実行してパーサーを生成する
  3. パーサーのメソッド add_argument() で引数とオプションを設定する
  4. パーサーのメソッド parse_args() でコマンドラインを解析する

ArgumentParser() には引数に description='...' を指定することができます。これはヘルプ (-h, --h) で表示される説明文になります。ヘルプは自動的に生成されます。

add_argument() は第 1 引数に引数やオプションの名前を指定します。名前の先頭が '-' であればオプションの指定、そうでなければ引数の指定になります。オプションは -a のようにアルファベット 1 文字で指定する方法と、--abc のように文字列で指定する方法があります。両方いっしょに指定することもできます。

引数やオプションは default=value でデフォルト値を指定することができます。引数やオプションの個数は nargs=value で指定することができます。nargs を設定しない場合、個数は 1 になります。この場合、引数を省略するとエラーになるので、nargs='?' を設定してください。すると、0 または 1 個の引数を受け取ることができます。引数が省略されたとき、デフォルト値が使用されます。

オプションの有無をチェックしたいときは、action='store_true' を指定すると便利です。オプションのあとの値は不要です。オプションを指定すると値は True になり、指定しないと False になります。'stroe_false' を指定すると値が逆になります。オプションを指定すると値は False になり、指定しないと True になります。

parse_args() は解析結果を格納したオブジェクト (obj) です。引数やオプションの名前を name とすると、解析結果は obj.name で求めることができます。オプションが指定されていない場合は None になります。

簡単な使用例を示しましょう。

リスト 3a : argparse の簡単な使用例

import argparse

parser = argparse.ArgumentParser(description='コマンドの説明')
parser.add_argument('arg1', help='引数1')
parser.add_argument('arg2', nargs='?', default="baz", help='引数2')
parser.add_argument('-a', '--aopt', help='オプションa')
parser.add_argument('-b', '--bopt', action='store_true', help='オプションb')
args = parser.parse_args()

print(args.arg1, args.arg2, args.aopt, args.bopt)

add_argument() の help='...' はヘルプを表示するときのメッセージになります。引数は arg1 と arg2 の 2 つです。arg2 は省略可能で、そのとき値は baz になります。オプションは 2 つあり、-b の値は True または False になります。

簡単な使用例を示します。

$ python3 testarg.py -h
usage: testarg.py [-h] [-a AOPT] [-b] arg1 [arg2]

コマンドの説明

positional arguments:
  arg1                  引数1
  arg2                  引数2

optional arguments:
  -h, --help            show this help message and exit
  -a AOPT, --aopt AOPT  オプションa
  -b, --bopt            オプションb
$ python3 testarg.py foo bar
foo bar None False
$ python3 testarg.py foo
foo baz None False
$ python3 testarg.py -a oops -b foo bar
foo bar oops True

それでは argparse を使って grep.py を改良してみましょう。リスト 3b を見てください。

リスト 3b : 文字列の検索 (改良版)

import sys, re, argparse

# パターンの検索
def grep(pat, fin):
    n = 1
    for x in fin:
        if pat.search(x): print('{:5d}: {}'.format(n, x), end='')
        n += 1

# コマンドラインの解析
parser = argparse.ArgumentParser(description='文字列の検索')
parser.add_argument('pattern', help='検索パターン')
parser.add_argument('filename', nargs='?', default='-', help='ファイル名')
parser.add_argument('-i', '--ignorecase', help='英大小文字を区別しない', action='store_true')
args = parser.parse_args()

# パターンのコンパイル
pat = re.compile(args.pattern, re.I if args.ignorecase else False)

if args.filename == '-':
    grep(pat, sys.stdin)
else:
    with open(args.filename) as f:
        grep(pat, f)

オプション -i, --ignorecase で英大文字小文字を区別しないで検索します。検索文字列は引数 pattern で指定します。引数 filename を省略する、または '-' の場合は標準入力 (sys.stdin) からデータを読み込みます。これでファイルがリダイレクトされていも対応することができます。

あとは、args.ignorecase が真ならば re.I を、そうでなければ False を compile() に渡して正規表現 pattern をコンパイルすれば、オプションの指定どおりに文字列を検索することができます。興味のある方は実際に試してみてください。

●後方参照

Python では、カッコ () で正規表現をグループにまとめることができました。このほかに、もうひとつ機能があります。カッコはその中の正規表現と一致した文字列を覚えていて、あとからそれを使うことができるのです。これを「後方参照」といいます。

正規表現の中では、\num (num: 数字) でカッコと一致した文字列を参照することができます。いちばん左側にある ( が \1 に対応し、次の ( が \2、その次の ( が \3 というように、順番に数字がつけられます。カッコの数に制限はありません。簡単な例を示しましょう。

r'(\w+)\s+\1'

これは同じ単語が続けて出現する場合、たとえば 'abc abc' のような文字列と一致します。まず (\w+) と abc が一致します。このとき、abc が記憶されます。次に、\s+ と空白文字が一致し、\1 と abc がマッチングされます。\1 は最初のカッコですから、その値は abc です。したがって、"abc abc" と一致するのです。では、次の正規表現と一致する文字列はどうでしょうか。

r'(\w+)\s+(\w+)\s+\2\s+\1'

かなり複雑になりましたが、これは "abc def def abc" のような文字列と一致します。\2 と \1 の位置に注意してください。これを逆に \1\s+\2 とすれば、"abc def abc def" と一致することになります。

カッコで記憶した文字列は、メソッド group() で取り出すことができます。group() の引数に番号を指定すると、番号に対応するグループの文字列を取り出すことができます。簡単な例を示します。

>>> p = re.compile(r'(\w+)\s+(\w+)\s+\2\s+\1')
>>> m = p.search('abc def def abc')
>>> m
<re.Match object; span=(0, 15), match='abc def def abc'>
>>> m.group()
'abc def def abc'
>>> m.group(1)
'abc'
>>> m.group(2)
'def'
>>> m.group(1, 2)
('abc', 'def')
>>> m.groups()
('abc', 'def')

group(1) は最初のカッコで一致した文字列 'abc' を取り出し、group(2) は 2 番目のカッコで一致した文字列 'def' を取り出します。group は複数のグループ番号を指定することができ、各グループで一致した文字列をタプルに格納して返します。すべてのグループで一致した文字列を求める場合は、メソッド groups() を使うと便利です。

たとえば、スラッシュで区切られた日付から年月日と取り出してみましょう。次の例を見てください。

>>> p = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> a = p.search('2022/08/27')
>>> year, month, day = a.groups()
>>> year
'2022'
>>> month
'08'
>>> day
'27'

このように、各グループで一致した文字列を取り出して、変数にセットすることができます。

●拡張記法

Python の正規表現は拡張記法をサポートしています。拡張記法は (?...) で表します。? の後ろに続く文字で機能が決まります。拡張記法の正規表現を表 6 に示します。

表 6 : 正規表現の拡張記法
表記法機能
(?:...) 正規表現をグループにまとめる(後方参照無し)
(?=...) 正規表現による位置指定(文字列を消費しない)
(?!...) 正規表現の否定による位置指定(文字列を消費しない)
(?P<name>...)グループに名前 name を付ける
(?P=name) グループ name の後方参照
(?#...) コメント

(?:...) は正規表現をグループ化しますが、一致した文字列は記憶しません。したがって、文字列を取り出したり後方参照に使うことはできません。

>>> p = re.compile(r'(\w+)\s+(?:\w+)\s+(\w+)')
>>> a = p.search('abc def ghi')
>>> a.group(1)
'abc'
>>> a.group(2)
'ghi'
>>> a.groups()
('abc', 'ghi')

最初の (\w+) と最後の (\w+) は一致した文字列を記憶しますが、中の (?:\w+) は一致した文字列を記憶しません。メソッド group() で文字列を取り出すと、1 番目が 'abc' になり 2 番目が 'ghi' になります。

(?P<name>...) はグループに名前 name を付けます。カッコ内の正規表現に一致した文字列は、(?P=name) で後方参照することができます。もちろん、メソッド group() で取り出すこともできます。

>>> p = re.compile(r'(?P<word>\w+)\s+(?P=word)')
>>> a = p.search('abc abc')
>>> a.group('word')
'abc'
>>> a.group(1)
'abc'

グループを番号で覚えるよりも名前を付けたほうがわかりやすくなります。また、グループ番号でもアクセスすることができます。

(?=...) は位置を正規表現で指定します。その位置でカッコ内の正規表現と一致すれば、(?=...) のマッチングは成功になります。逆に、(?!...) の場合は、その位置でカッコ内の正規表現と一致しないときに、(?!...) のマッチングは成功します。

>>> p = re.compile(r'foo\s+(?=bar)')
>>> a = p.search('foo bar')
>>> a.group()
'foo '
>>> q = re.compile(r'foo\s+(?!bar)')
>>> b = q.search('foo baz')
>>> b.group()
'foo '

どちらの正規表現も位置を指定するだけなので、カッコ内の正規表現と一致しても、マッチングの位置を前へ進めることはありません。この後ろに正規表現があれば、その位置からマッチングを開始します。

また、(?=...) や (?!...) とマッチングした文字列は、正規表現と一致した文字列の中には含まれません。たとえば、foo\s+(?=bar) は 'foo bar' と一致しますが、group() で一致した文字列を取り出すと 'foo ' になります。

●findall() と split()

文字列の中で正規表現と一致した部分文字列をすべて求めたい場合は、findall() を使うと便利です。findall() は重複しない一致部分文字列をリストに格納して返します。簡単な例を示しましょう。

>>> pat = re.compile(r'\w+')
>>> pat.findall('foo bar baz')
['foo', 'bar', 'baz']

>>> pat = re.compile(r'(\w+)\s+(\d+)')
>>> pat.findall('foo 10 bar 20 baz 30')
[('foo', 10), ('bar', 20), ('baz', 30)]

グループがある場合は、グループと一致した部分文字列をリストに格納して返します。グループが複数ある場合は、各グループの一致文字列をタプルに格納します。

split() は string モジュールにもありますが、re モジュールの split() は単語の区切りを正規表現で指定します。簡単な例を示します。

>>> pat = re.compile('\W+')
>>> pat.split('foo bar baz')
['foo', 'bar', 'baz']

>>> pat = re.compile('(\W+)')
>>> pat.split('foo bar baz')
['foo', ' ', 'bar', ' ', 'baz']
>>> pat.split('foo bar baz', 1)
['foo', ' ', 'bar baz']

split() は正規表現にグループがあると、そのグループに一致した部分文字列をリストに追加します。また、split() は引数に単語を切り出す回数を指定することができます。

●文字列の置換

次は文字列の置換を行う関数とメソッドを説明しましょう。

function : sub(pattern, replace, string [,count])
method   : sub(replace, string [,count])

関数 sub() は文字列 string の中から pattern を探し、見つかれば一致した部分を replace に置き換えた文字列を返します。引数 count は文字列を置換する回数を指定します。デフォルトは 0 で、pattern と一致した文字列をすべて置換します。メソッド sub() は Regex Object の正規表現に一致する文字列を replace に置き換えます。なお、引数 replace には後方参照を使うことができます。

簡単な例を示しましょう。

>>> p = re.compile('abc')
>>> p.sub('123', 'abc def abc ghi')
'123 def 123 ghi'
>>> p.sub('123', 'abc def abc ghi', 1)
'123 def abc ghi'

abc を 123 に置換します。count を指定しないとすべての abc を 123 に置換しますが、count に 1 を指定すると最初の abc だけを 123 に置換します。

後方参照を使うと、一致した文字列を置換文字列に取り込むことができます。次の例を見てください。

>>> p = re.compile(r'(b\w+)')
>>> p.sub(r'[\1]', 'foo bar foo baz')
'foo [bar] foo [baz]'

b で始まる英単語をカッコで記憶しておきます。そして、置換文字列に後方参照 \1 を使うと、検索で記憶した文字列を置換文字列の中に含めることができます。したがって、r'[\1]' は検索した文字列を角カッコ [ ] で囲むように置換されます。

●文字列置換ツールの作成

sub() を使うと文字列の置換ツールも簡単に作成することができます。リスト 4 を見てください。

リスト 4 : 文字列置換ツール (gres.py)

import re, sys, argparse

# 置換の実行
def gres(pat, rep_pat, fin):
    for x in fin:
        a = pat.sub(rep_pat, x)
        sys.stdout.write(a)

# コマンドラインの解析
parser = argparse.ArgumentParser(description='文字列の置換')
parser.add_argument('pattern', help='検索パターン')
parser.add_argument('rep_pat', help='置換パターン')
parser.add_argument('filename', nargs='?', default='-', help='ファイル名')
args = parser.parse_args()

# パターンのコンパイル
pat = re.compile(args.pattern)

if args.filename == '-':
    gres(pat, args.rep_pat, sys.stdin)
else:
    with open(args.filename) as f:
        gres(pat, args.rep_pat, f)

このプログラムは、次のように実行します。

$ python3 gres.py -h
usage: gres.py [-h] pattern rep_pat [filename]

文字列の置換

positional arguments:
  pattern     検索パターン
  rep_pat     置換パターン
  filename    ファイル名

optional arguments:
  -h, --help  show this help message and exit

args.pattern から正規表現を取り出して compile() でコンパイルします。置換文字列は args.rep_pat にセットされています。それから、args.filename をチェックしてファイル名が指定されていれば open() でファイルをオープンします。そうでなければ標準入力からデータを読み込みます。あとは、ファイルから 1 行ずつ読み込み、メソッド sub() で文字列を置換するだけです。興味のある方は試してみてください。

●関数で置換文字列を生成する

ところで、sub() の引数 replace には関数を指定することができます。そうすると、正規表現と一致した部分を関数が返した文字列に置き換えます。次の例を見てください。

>>> def foo(match):
...     n = int(match.group())
...     return str(n * 2)
...
>>> p = re.compile('\d+')
>>> p.sub(foo, 'abcd1234efgh')
'abcd2468efgh'

\d+ とマッチするのは 1234 です。ここで、関数 foo() が呼び出されます。引数 match には Match Object が渡されるので、match.group() で文字列を取り出して関数 int() で数値に変換します。そして、その値を 2 倍してから関数 str() で文字列に変換して返します。したがって、sub() の返り値は 1234 の部分を 2 倍した 'abcd2468efgh' になります。

●キーワードクロスリファレンスの作成

今度は、クロスリファレンスを作成するプログラムを作ってみましょう。クロスリファレンスとは、プログラムで使用された変数や関数の名前と、それが現れる行番号をすべて書き出した一覧表のことです。今回作成するプログラムは変数名や関数名ではなく、正規表現と一致する文字列をキーワードとし、それが現れる行番号を出力することにします。

キーワードは文字コード順に整列して出力した方が見やすいので、出現したキーワードと行番号を覚えておいて、ファイルを読み終わってから結果をまとめで出力することにします。この場合、キーワードの探索処理によってプログラムの実行時間が大きく左右されます。

コンピュータの世界では、昔からデータを高速に探索するアルゴリズムが研究されています。基本的なところでは「二分探索木」や「ハッシュ法」があります。キーワードをリストに格納して線形探索すると時間がかかるので、このプログラムではディクショナリを使うことにします。

ただし、ディクショナリのキーをメソッド keys() で取り出すとき、文字コード順に取り出されるわけではありません。したがって、データを文字コード順に表示したい場合は、データをソートしないといけません。Python にはリストの要素をソートするメソッド sort() が用意されています。

簡単な例を示しましょう。

>>> d = {'abc':10,'ghi':30, 'def':20}
>>> a = list(d.keys())
>>> a
['abc', 'ghi', 'def']
>>> a.sort()
>>> a
['abc', 'def', 'ghi']

sort() はリストを破壊的に修正することに注意してください。sort() はリストに格納されている文字列を文字コード順に並べます。このとき、英大小文字は区別されます。英大小文字を区別せずにソートすることもできますが、データを比較する関数を sort() に渡す必要があります。詳細は Python のマニュアルをお読みください。

それでは、クロスリファレンスのプログラムをリスト 5 に示します。

リスト 5 : クロスリファレンスの作成

#
# cref.py : キーワードクロスリファレンスの作成
#
import re, sys, argparse

# コマンドラインの解析
parser = argparse.ArgumentParser(description='キーワードクロスリファレンス')
parser.add_argument('pattern', help='検索パターン')
parser.add_argument('filename', nargs='?', default='-', help='ファイル名')
parser.add_argument('-i', '--ignorecase', help='英大小文字を区別しない', action='store_true')
args = parser.parse_args()

# グローバル変数
dic = {}

# クロスリファレンスの出力
def print_cref():
    key_list = list(dic.keys())
    key_list.sort()
    for key in key_list:
        count = 0
        sys.stdout.write('{}\n'.format(key))
        for n in dic[key]:
            sys.stdout.write('{:8d}'.format(n))
            count += 1
            if count == 8:
                sys.stdout.write('\n')
                count = 0
        sys.stdout.write('\n')

# 行番号のセット
def set_line(key, n):
    if key in dic:
        line = dic[key]
        if line[-1] != n:
            line.append(n)
    else:
        dic[key] = [n]

# キーワードの探索
def get_keyword(pat, fin):
    n = 1
    for x in fin:
        for key in pat.findall(x):
            if type(key) is tuple:
                for key1 in key:
                    set_line(key1, n)
            else:
                set_line(key, n)
        n += 1

# パターンのコンパイル
pat = re.compile(args.pattern, re.I if args.ignorecase else False)

# 実行
if args.filename == '-':
    get_keyword(pat, sys.stdin)
else:
    with open(args.filename) as f:
        get_keyword(pat, f)
# 表示
print_cref()

少し長いリストですが、関数 get_keyword() が探索処理で、関数 print_cref() が出力処理です。コマンドラインの解析処理は文字列の検索ツール grep.py と同じです。グローバル変数 dic には空の辞書 { } をセットします。

get_keyword() は、ファイルからキーワードを検索します。この処理は grep.py とほとんど同じです。変数 n は行番号を表します。次に、ファイルから 1 行読み込み、findall() でキーワードを切り出します。正規表現にグループが含まれている場合、返り値のリストにはタプルが格納されています。このため、関数 type() でデータ型を求めて、それがタプルかチェックします。

type(object) => type_obj
>>> type(123)
<class 'int'>
>>> int
<class 'int'>
>>> type((1,2,3))
<class 'tuple'>
>>> tuple
<class 'tuple'>

type() は object のデータ型 type_obj を返します。Python の場合、データ型を表す型オブジェクト (type 型) が定義されていて、それらは変数に格納されています。型オブジェクトを格納している変数名を表 7 に示します。

表 7 : 型オブジェクト
変数名データ型
bool 真偽値
int 整数
float 浮動小数点数
complex 複素数
str 文字列
tuple タプル
list リスト
dict ディクショナリ
set セット

type(key) の値が tuple と等しければ key はタプルです。for 文でタプルからキーを一つずつ取り出して、関数 set_line() で行番号をセットします。key がタプルでなければそのまま set_line() に渡します。

関数 set_line() はキーワード key を辞書 dic に登録し、そこに行番号をセットします。行番号はリストに格納します。このとき、同じ行番号がないことを確認します。これは、リストの最後のデータと行番号 n を比較するだけです。リストを変数 line に格納し、line[-1] と n が等しくなければ、append() で n を lines の最後尾に追加します。key が見つからない場合は、dic[key] = [n] でリスト [n] を辞書に登録します。

関数 print_cref() は辞書に登録されているキーワードと行番号を表示します。最初に dic.keys() で辞書に登録されているキーを求めて、list() でリストに変換してから sort() でソートします。そして、for 文でキーを一つずつ取り出し、辞書から行番号を取り出して表示します。count は、出力した行番号を数えるカウンタとして使います。8 個表示したら改行します。

これでプログラムは完成です。それでは実行してみましょう。図 2 に示すファイル test.dat で、\w+ をキーワードにしたクロスリファレンスを作成します。実行結果を図 3 に示します。

    abc def ghi jkl
    def ghi jkl mno
    ghi jkl mno pqr
    jkl mno pqr stu
    mno pqr stu vwx

図 2 : test.dat の内容
$ python3 cref.py "\w+" test.dat
abc
       1
def
       1       2
ghi
       1       2       3
jkl
       1       2       3       4
mno
       2       3       4       5
pqr
       3       4       5
stu
       4       5
vwx
       5

図 3 : cref.py の実行結果

正規表現で表せるパターンであれば、そのクロスリファレンスを cref.py で作成することができます。このように、正規表現を使うと文字列を処理するプログラムを簡単に作ることができます。

●イテレータとジェネレータ

次は「ジェネレータ」について説明します。Python にはイテレータ (iterator) という、コレクションから要素を順番に取り出す機能があります。実をいうと、 for 文で要素を順番に取り出せるのは背後でイテレータが働いているからです。

イテレータは関数 iter() でイテレータオブジェクトを取得し、関数 next() で要素を順番に取り出します。次の例を見てください。

>>> a = [1, 2, 3]
>>> b = iter(a)
>>> b
<list_iterator object at 0x7fd850d7dd00>
>>> next(b)
1
>>> next(b)
2
>>> next(b)
3
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

next() は要素がなくなったら例外 StopIteration を送出します。例外はエラー処理のことですが、Python の例外はエラー処理以外にも使うことができます。

イテレータはコレクションの要素を順番に取り出すだけですが、ジェネレータ (generator) を使うと値を一つずつ順番に生成することができます。リスト 6 を見てください。

リスト 6 : ジェネレータ

def generator(start, end, step):
    n = start
    while n < end:
        yield n
        n += step

ジェネレータの定義は関数とほとんど同じです。違いは yield 文でデータを返すところです。ジェネレータはイテレータオブジェクトと next() が自動的に生成され、next() を呼び出すたびに yield 文で指定したデータが返されます。関数の実行が終了すると、自動的に例外 StopIteration が送出されます。

関数 generator() は start から end 未満の数値を step きざみで生成します。簡単な実行例を示します。

>>> for x in generator(0, 10, 2):
...     print(x)
...
0
2
4
6
8

このように、ジェネレータはイテレータのかわりに使うことができます。

●ジェネレータ関数の再帰定義

ジェネレータの長所は、関数の実行を一時停止するとき、そのときの状態(ローカル変数の値など)を自動的に保存するところです。このため、再帰定義を使った関数でも、簡単にジェネレータを作成することができます。

簡単な例として、ジェネレータを使って順列を生成するプログラムを作りましょう。再帰定義でジェネレータ関数を作成する場合、関数内でジェネレータを生成し、その値を使って新しい値を yield 文で返すようにします。

たとえば、要素が n 個の順列を生成する場合、n - 1 個の順列を生成するジェネレータを生成し、そこに要素を一つ加えて n 個の順列を生成すると考えます。リスト 7 を見てください。

リスト 7 : 順列の生成

def gen_perm(nums, n = 0):
    if n == len(nums):
        yield []
    else:
        for x in gen_perm(nums, n + 1):
            for y in nums:
                if y not in x:
                    yield x + [y]

ジェネレータ gen_perm() の引数 nums は要素を格納したリストで、n は選択した要素の個数を表します。n が nums の要素数と等しい場合は、すべての要素を選択したので yield 文で空リスト [ ] を返します。そうでなければ、gen_perm() を再帰呼び出しして生成される値を for 文で受け取ります。そして、その値 x に要素 y を追加したリストを yield で返します。これで順列を生成することができます。

簡単な実行例を示します

>>> for x in gen_perm([1, 2, 3]):
...     print(x)
...
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

このように、ジェネレータを使って順列を一つずつ生成することができます。

●ジェネレータ表現

いままでは関数と yield 文を使ってジェネレータを作りましたが、「ジェネレータ表現 (generator expressions)」を使うと簡単にジェネレータを利用することができます。ジェネレータ表現は内包表記とほとんど同じですが、丸カッコ () で囲むところが異なります。図 4 に基本的な構文を示します。

(式 for 変数, ... in コレクション)

    図 4 : ジェネレータ表現

この後ろに for 文または if 分を続けることができるのもリストの内包表現と同じです。簡単な例を示します。

>>> a = (x * x for x in [1, 2, 3, 4, 5])
>>> a
<generator object <genexpr> at 0x7fd84f404580>
>>> for x in a:
...     print(x)
...
1
4
9
16
25

リストの内包表現はリストを生成しますが、ジェネレータ表現はリストを生成しません。リストを生成する必要がない場合、たとえばコレクションを生成するときはジェネレータ表現の方が効率的です。次の例を見てください。

>>> a = tuple(x * x for x in range(5))
>>> a
(0, 1, 4, 9, 16)
>>> key = ['foo', 'bar', 'baz']
>>> value = [10, 20, 30]
>>> b = dict((key[x], value[x]) for x in range(len(key)))
>>> b
{'foo': 10, 'bar': 20, 'baz': 30}
>>> c = set(x * x for x in range(5))
>>> c
{0, 1, 4, 9, 16}

このように Python では、コレクションを生成する関数の引数にジェネレータを渡すことができます。このほかにも、イテレータを引数に受け取る関数でジェネレータを使うことができます。

●おわりに

正規表現を使った文字列処理とジェネレータについて詳しく説明しました。Python の正規表現は強力な機能なので、うまく使いこなすと複雑な文字列処理でも簡単にプログラムを作ることができます。次回は Python のオブジェクト指向機能について説明します。


初版 2006 年 2 月 25 日
新版 2022 年 8 月 27 日

Copyright (C) 2006-2022 Makoto Hiroi
All rights reserved.

[ PrevPage | Python | NextPage ]