【Python】reモジュールを使用して特定の文字抽出・文字の置き換え・文字の分割・リスト化


投稿日 2020年10月1日 >> 更新日 2023年3月1日

今回は、Python標準ライブラリであるreモジュールについて説明していきたいと思います。

reモジュールでは正規表現を使って、文字列中にある文字を検索(取得)したり、文字列中の文字を他の文字に置き換えたり、検索に引っかかった文字から分割などができる高機能ツールです。

正規表現の使い方を覚えてしまえば、自由自在に文字列中の欲しい情報をピンポイントで取得できるようになります。

そしてデータサイエンティストにおけるデータのクリーニングでも、文字列情報からの特定のパターンだけ抜き出したり、置き換えたりすることが可能になってきます。

まずは正規表現とは何か? と言うところから始め、様々なメソッドが用意されているreモジュールをそれぞれ実装していきたいと思います。

実行環境

実行環境
Windows Subsystem for Linux Ubuntu
Python 3.6.9
jupyter==1.0.0

正規表現とは

正規表現は特殊文字(特殊シーケンス)やメタ文字によって文字を表現することで、バックスラッシュ(「\」(Windowsの場合円マーク(¥))からなる1つの特殊文字で、文字列・整数・記号などを表すことができ、1つの特殊文字から文字列中のパターンにマッチさせます。

メタ文字は特殊文字などと併用して使う記号(+, ?など)のことで、柔軟な処理を実行できます。

例えば、「abcd1234」という文字列において、整数のみを取得したい場合は、「\d+」という正規表現で検索することによって、単語文字「abcd」を除外して、整数の「1234」のみを取得することができます。

もちろんリテラルな表現で取得することも可能ですが

import re

text = 'abcd1234'

match = re.search('1234', text)
print(match.group())
# 1234

以下のように正規表現で検索すると簡単に目当ての要素を取得することができます。

import re

text = 'abcd1234'

match = re.search(r'\d+', text)
print(match.group())
# 1234

正規表現の文字列先頭に現れる「r''」の意味は、raw 文字列記法と言って、正規表現表記をエスケープシーケンスでは無く、特殊文字として保つための記述です。

そして正規表現である「\d」が特殊文字と言われる表現で、「+」がメタ文字となります。

「\d」は10進整数1個のみ、「+」を付けることによって1個以上、となります。

この特殊文字とメタ文字を上手に組み合わせることによって、柔軟な文字列検索が可能となります。

以下が今回ここで扱う予定の各表現の表になります。

※特殊文字では、対象となる文字列中の文字1個が当てはまります。

特殊文字 対象
abc..123.. リテラル表記
\A(^) 文字列の先頭
\b 文字の境界
\B 文字の間
\d 10進整数(123..)
\D 10進整数以外
\s 空白文字
\S 空白文字以外
\w 文字列
\W 文字列以外
\Z($) 文字列の末尾

※メタ文字は、特殊文字(正規表現)と組み合わせることによって複数の文字を対象付けられる。

メタ文字 内容
. 改行以外の文字
^ 文字列の先頭
$ 文字列の末尾
* 0回以上の繰り返し
+ 1回以上の繰り返し
? 0回か1回文字の繰り返し
{m} 直前の正規表現をm回繰り返す
{m,n} 直前の正規表現をm〜n回繰り返す
\ メタ文字を文字列として認識する
[ ]([a-z]や[0-9]など) 文字集合
| どちらかが当てはまれば
( ) グループ化
(?P< 名前 > 正規表現) 文字列に名前を付ける
(?<=正規表現) 正規表現 グループの位置以降

これらの特殊文字・メタ文字は一部に過ぎないので、気になる方はPythonの公式ドキュメントをご参照ください。

では上記の表を元にreモジュールで文字を取得していきたいと思います。

reモジュールの実装

reモジュールはPython標準ライブラリなのですぐ使えるようになっています。

そしてreモジュールを使うに当たって、Python標準ライブラリのstringモジュールを活用していくと、テスト試行できるので便利です。

stringモジュール内にはASCII文字列が定義されており、アルファベット・整数・記号・エスケープシーケンスの文字列が格納されています。

import string

printable = string.printable
print('文字数:{}'.format(len(printable)))
printable
文字数:100
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

ではさっそくreモジュールをインポートして、上記の文字列に検索をかける簡単な実装をします。

import re

match = re.search('ABCD', printable)
print(match)
<_sre.SRE_Match object; span=(36, 40), match='ABCD'>

上ではreモジュールのsearch()メソッドを使用していて、引数は「re.search(pattern, string, flags=0)」となっています。

第一引数のpatternには正規表現または文字列を渡し、目的である文字を検索します。

第二引数のstringには対象である文字列を渡します。

第三引数は今回デフォルト値で実装しますが、検索条件の詳細設定を決められるとの事です。

そしてこのメソッドはMatchオブジェクトとして返され、検索された文字列が見つかる(マッチする)と取得されてきた文字列や行数が結果として残ります。

match = re.search('ABCD', printable)
print(type(match))
print(match)
# <class '_sre.SRE_Match'>
# <_sre.SRE_Match object; span=(36, 40), match='ABCD'>

もしも検索した文字列が見つからなければ、「None」が返されます。

つまりブール値としてMatchオブジェクトが返されるので、if文による条件分岐も簡単に実装することができます。

肝心の要素の取得は、Match.group()属性を定義します。

print(match.group())
# ABCD

抽出先の文字列全体を取得したい場合は、Match.string属性を定義します。

print(match.string)
match.string
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~  

'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

これまで、re.search()メソッドに「(正規表現, 文字列)」を渡してマッチングを行ってきましたが、正規表現をコンパイル(設定)してから文字の抽出も行えます。

import re

comp = re.compile('ABCD')
match = comp.search(printable)
print(match.group())
# ABCD

以下は上記と同じ処理です。

match = re.search('ABCD', printable)
print(match.group())
# ABCD

先ほども言ったようにMatchオブジェクトはTrue or Falseのブール値としてのオブジェクトにもなるので、検索したパターンが見つからなければ「None」を返します。


match = re.search('DCBA', printable)
print(match)
# None

printable内の文字列には「DCBA」という並びの単語は存在しないのでMatchオブジェクトはFalseを返します。

False(None)の場合だと、Matchオブジェクトの属性には何も渡されないのでエラーとなります。

print(match.group())
# AttributeError: 'NoneType' object has no attribute 'group'

エラーを防ぐ為にも、簡単なif文を定義すると良さそうですね。

match = re.search('DCBA', printable)
if match:
    print(match.group())
else:
    print('None')
# None

ここまでの検索パターンをリテラル文字列としてハードコードしてきましたが、その文字の位置や並び順の関係が一文字でも違えばマッチできません。

正確に、そして柔軟に検索パターンを取得する方法として、正規表現で検索を行い要素を取得します。

正規表現に慣れるために1つ1つ試してみましょう。

正規表現の特殊文字とメタ文字を一通り使用してみる

Python標準パッケージのstringモジュールからASCII文字列をテスト用に準備しておきます。

import string

printable = string.printable

print('文字数:{}'.format(len(printable)))
printable
文字数:100
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

特殊文字の「\w」を使ってアンダースコアを含む単語文字整数を1個だけ抽出

import re

match = re.search(r'\w', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 1), match='0'>
'0'

特殊文字の「\w」とメタ文字「+」を組み合わせてアンダースコアを含む1個以上の単語文字整数を抽出

match = re.search(r'\w+', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 62), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

特殊文字の「\W」とメタ文字「{m}」を組み合わせて、記号を任意の数で抽出

# 6つの記号を取得
match = re.search(r'\W{6}', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(62, 68), match='!"#$%&'>
'!"#$%&'

特殊文字の「\s」とメタ文字「+」を組み合わせて1個以上の空白文字を抽出

match = re.search(r'\s+', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(94, 100), match=' \t\n\r\x0b\x0c'>
' \t\n\r\x0b\x0c'

特殊文字の「\S」とメタ文字「+」を組み合わせて1個以上の空白文字以外を抽出

match = re.search(r'\S+', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 94), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

特殊文字の「\d」とメタ文字「+」を組み合わせて1個以上の10進整数を抽出

match = re.search(r'\d+', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 10), match='0123456789'>
'0123456789'

特殊文字「\D」とメタ文字「{m,n}」を組み合わせて、m〜n回までの数を出来るだけ多く10進整数以外の文字列を抽出

match = re.search(r'\D{10,100}', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(10, 100), match='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW>
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

「re.search(r'\D{10,100}', printable)」は、下限10、上限100とした10進整数以外の文字列を抽出しています。

上限を決めずに「{10,}」とすると直前の正規表現に従って無限に抽出します。

特殊文字の「\b」を使って境界線を抽出

match = re.search(r'\w\b', printable)
print(match)
print(match.string)
match.group()
<_sre.SRE_Match object; span=(61, 62), match='Z'>
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~  

'Z'

「\b」は「\w」が最初に当てはまる直前の文字を抽出しているので、単語文字と記号との境を起点に、手前の文字「Z」を抽出しています。

正規表現を前後入れ替えて3要素まで抽出すると、最初の境界線である先頭が検索されるので以下のようになります。

match = re.search(r'\b\w{3}', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 3), match='012'>
'012'

特殊文字の「\B」を使って文字の間を抽出

match = re.search(r'\B\w', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(1, 2), match='1'>
'1'

上記は最初に現れる文字と文字との間なので、「0間(\B)1」という並びから文字間の最初の単語文字整数「1」を抽出しました。

特殊文字の「\A」とメタ文字「^」を使って文字列の先頭を抽出

※「\A」と「^」は同じ内容です。

「\A」の場合

match = re.search(r'\A\w{5}', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 5), match='01234'>
'01234'

「^」の場合

match = re.search(r'^\w{5}', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 5), match='01234'>
'01234'

特殊文字の「\Z」とメタ文字「$」を使って末尾の抽出

※「\Z」と「$」は同じ内容です。

「\Z」の場合

match = re.search(r'\s\Z', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(99, 100), match='\x0c'>
'\x0c'

「$」の場合

match = re.search(r'\s$', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(99, 100), match='\x0c'>
'\x0c'

メタ文字「( 正規表現 )」を使って、グループ化した文字列を抽出

match = re.search(r'(\d+)(\w+)(\W+)(\w+)(\S+)(\s+)', printable)
print(match)
print(match.group(0))
print(match.group(1))
print(match.group(2))
print(match.group(3))
print(match.group(4))
print(match.group(5))
match.group(6)  # エスケープシーケンスなので見える化
<_sre.SRE_Match object; span=(0, 100), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~  

0123456789
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
!"#$%&'()*+,-./:;<=>?@[\]^
_
`{|}~
' \t\n\r\x0b\x0c'

「( )」で囲まれた正規表現もしくわリテラルな文字・整数でマッチングを図ると、グループ化されてMatchオブジェクトのgroup()属性で個々のグループをインデックスによって抽出できます。

メタ文字「(?P<名前>正規表現)」を使って、抽出された各グループに名前付けする

match = re.search(r'(?P<A>\d+)(?P<B>\w+)(?P<C>\W+)(?P<D>\w+)(?P<E>\S+)(?P<F>\s+)', printable)
print(match)
print(match.group(0))
print(match.group('A'))
print(match.group('B'))
print(match.group('C'))
print(match.group('D'))
print(match.group('E'))
match.group('F')
<_sre.SRE_Match object; span=(0, 100), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~  

0123456789
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
!"#$%&'()*+,-./:;<=>?@[\]^
_
`{|}~
' \t\n\r\x0b\x0c'

メタ文字「[ ]」を使って文字集合の抽出

match = re.search(r'a[a-z]', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(10, 12), match='ab'>
'ab'

上記では「[a-z]」としていますが、これはa〜zのどれかに当てはまると抽出されます。

「a[a-z]」のように文字「a」から始まる文字を「[ ]」内の文字集合から見つけ出し「b」を抽出しています。

「b[a-z]」だったら「c」、「c[a-z]」だったら「d」といったように文字列中の並びに従った枠内の文字を抽出することができます。

ちなみに、「[ ]」の文字集合内でのメタ文字は無効化され、リテラル文字となります。

match = re.search(r'(?P<invalib>[+,-./:;<=>?]+)', printable)
print(match)
match.group('invalib')
<_sre.SRE_Match object; span=(72, 83), match='+,-./:;<=>?'>
'+,-./:;<=>?'

ここまではreモジュールのsearch()メソッドを中心に実装してきましたが、他にも様々なメソッドが用意されているので、その他の正規表現はsearch()メソッド以外で実装していきます。

各メソッド(Patternオブジェクト)の試行

reモジュールはこれまで実装してきたsearch()メソッドの文字列中からの文字の抽出以外に、マッチした文字を他の文字に置き換えたり、マッチした文字を起点に分割しリストで取得できたりといった様々なメソッドがあります。

全てをご紹介するのは厳しいですが、その中の一部を実装してみたいと思います。

文字列はPython標準ライブラリのstringモジュールを使います。

import string

printable = string.printable
print('文字数:{}'.format(len(printable)))
printable
文字数:100
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

re.match()

match(pattern, string, flags=0)メソッドは、文字列の先頭にマッチした文字を抽出します。

正規表現の「\A」や「^」が元々備わっているようなメソッドです。

match = re.match(r'\w+', printable)
print(match)
match.group()
<_sre.SRE_Match object; span=(0, 62), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

match()メソッドに正規表現の「\Z」や「$」の末尾を抽出しようとしても、Noneが返されます。

match = re.match(r'\w+\Z', printable)
print(match)
# None

re.split()

re.split(pattern, string, maxsplit=0, flags=0)メソッドは、検索パターンにマッチした文字を分割します。

このメソッドはリストとして返されるので、group()属性などはありません。

match = re.split(r'\w\b', printable)
print(match)
['0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY', '!"#$%&\'()*+,-./:;<=>?@[\\]^', '`{|}~ \t\n\r\x0b\x0c']

上記はどこで分割されているかというと、正規表現「\w\b」としているので境界線の手前の文字「Z」「_」(アンダースコア)が分割されリストとして返されました。

引数のmaxsplitを指定することによって、分割する回数を決められます。

match = re.split(r'\w\b', printable, 1)
print(match)
['0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY', '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c']

上記では引数のmaxsplitを「1」に設定したので、分割を一回分、つまり最初に探索された「Z」を分割して残りはそのままです。

メタ文字「|」を使うことで複数の分割対象を指定できます。

match = re.split(r'0|a|A|\!|\t', printable)
print(match)
printable
['', '123456789', 'bcdefghijklmnopqrstuvwxyz', 'BCDEFGHIJKLMNOPQRSTUVWXYZ', '"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ', '\n\r\x0b\x0c']
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

re.findall()とre.finditer()

re.findall(pattern, string, flags=0)とre.finditer(pattern, string, flags=0)は抽出された結果をリストで返します。

双方の違いは、Matchオブジェクトになるかならないかです。

MatchオブジェクトではMatch.group()属性やMatch.groupdict()属性などが使用でき正規表現の「( )」で囲むことにより抽出される文字をグループ化することができました。

re.findallでは抽出結果をリストで返し、re.finditerはMatchオブジェクトを抽出される回数分停止(yield)しながらイテレーションします。

re.findall()

match = re.findall(r'\w+', printable)
print(match)
['0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', '_']

特殊文字の「\w」は、アンダースコアを含む単語文字整数を抽出するので、メタ文字「+」により一回以上続く文字列を終わりまで抽出します。

アンダースコア「_」は最初の文字列から離れた位置にあるので、別々の文字列としてリスト化されました。

match変数に渡されたのは、Matchオブジェクトでは無くリストとして返されるのでそのまま取得できます。

以下は「\w」に当てはまる文字を10個区切りで抽出しています。

match = re.findall(r'\w{10}', printable)
print(match)
['0123456789', 'abcdefghij', 'klmnopqrst', 'uvwxyzABCD', 'EFGHIJKLMN', 'OPQRSTUVWX']

リストとして返されるので、for文に渡すこともできます。

for match in re.findall(r'\w{10}', printable):
    print(match)
0123456789
abcdefghij
klmnopqrst
uvwxyzABCD
EFGHIJKLMN
OPQRSTUVWX

re.finditer()

re.finditer()は、イテレーションしてMatchオブジェクトを呼び出します。

type(re.finditer(r'\w+', printable))
# <class 'callable_iterator'>
for match in re.finditer(r'\w+', printable):
    print(match)
    print(match.group())
    print('-------')
<_sre.SRE_Match object; span=(0, 62), match='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM>
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
-------
<_sre.SRE_Match object; span=(88, 89), match='_'>
_
-------

次はメタ文字「( )」を組み合わせて2組のグループとして単語文字整数を抽出します。

for i in re.finditer(r'(\w)(\w)', printable):
    print('グループ1:{}'.format(i.group(1)))
    print('グループ2:{}'.format(i.group(2)))
    print('--------')
グループ1:0
グループ2:1
--------
グループ1:2
グループ2:3
--------
グループ1:4
グループ2:5
--------
グループ1:6
グループ2:7
--------
...
...
--------
グループ1:S
グループ2:T
--------
グループ1:U
グループ2:V
--------
グループ1:W
グループ2:X
--------
グループ1:Y
グループ2:Z
--------

グループに名前を付けて抽出します。

for i in re.finditer(r'(?P<グループ1>\w{10})(?P<グループ2>\w{10})', printable):
    print(i.group('グループ1'))
    print(i.group('グループ2'))
    print('-----------')
0123456789
abcdefghij
-----------
klmnopqrst
uvwxyzABCD
-----------
EFGHIJKLMN
OPQRSTUVWX
-----------

Match.groupdict()属性では辞書形式で取得できます。

for i in re.finditer(r'(?P<グループ1>\w{10})(?P<グループ2>\w{10})', printable):
    print(i.groupdict())
{'グループ1': '0123456789', 'グループ2': 'abcdefghij'}
{'グループ1': 'klmnopqrst', 'グループ2': 'uvwxyzABCD'}
{'グループ1': 'EFGHIJKLMN', 'グループ2': 'OPQRSTUVWX'}

Match.groupdict()属性はキーで取得することもできます。

for i in re.finditer(r'(?P<グループ1>\w{10})(?P<グループ2>\w{10})', printable):
    print(i['グループ1'])
    print(i['グループ2'])
0123456789
abcdefghij
klmnopqrst
uvwxyzABCD
EFGHIJKLMN
OPQRSTUVWX

re.sub()

re.sub(pattern, repl, string, count=0, flags=0)メソッドは検索パターンにマッチした文字を任意の文字に置き換えることができます。

メソッドの第1引数「pattern」に「検索パターン」、第2引数「repl」に「置き換える文字」、第3引数「string」に「文字列」、第4引数「count」に置き換える回数を指定できます。

第5引数の「flags」については実装しない予定なので、Python公式ドキュメントをご参照ください。

そしてre.sub()メソッドで置き換えらるオブジェクトは、文字列型として返されます。

print(type(re.sub(r'\w+', '「他の文字に置き換え', printable)))
# <class 'str'>
match = re.sub(r'\w+', '「他の文字に置き換え」', printable)
match
'「他の文字に置き換え」!"#$%&\'()*+,-./:;<=>?@[\\]^「他の文字に置き換え」`{|}~ \t\n\r\x0b\x0c'

上記の置き換えは、アンダースコアと単語文字整数が抽出され他の文字へと置き換えられました。

第4引数「count」のデフォルトは「0」で無制限となっていますが、「1」に制限をすると最初にマッチされた「\w+」の単語文字整数のみ他の文字列へと置き換えられます。

match = re.sub(r'\w+', '「他の文字に置き換え」', printable, 1)
match
'「他の文字に置き換え」!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

正規表現の特殊文字である文字との対応関係が曖昧な場合は、re.sub()メソッドで関係性をチェックできます。

特殊文字「\A」の場合

match = re.sub(r'\A', '先頭', printable)
match
'先頭0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

特殊文字「\Z」の場合

match = re.sub(r'\Z', '末尾', printable)
match
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c末尾'

特殊文字「\b」の場合

match = re.sub(r'\b', '境界線', printable)
match
'境界線0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ境界線!"#$%&\'()*+,-./:;<=>?@[\\]^境界線_境界線`{|}~ \t\n\r\x0b\x0c'

特殊文字「\B」の場合

match = re.sub(r'\B', '文字間', printable)
match
'0文字間1文字間2文字間3文字間4文字間5文字間6文字間7文字間8文字間9文字間a文字間b文字間c文字間d文字間e文字間f文字間g文字間h文字間i文字間j文字間k文字間l文字間m文字間n文字間o文字間p文字間q文字間r文字間s文字間t文字間u文字間v文字間w文字間x文字間y文字間z文字間A文字間B文字間C文字間D文字間E文字間F文字間G文字間H文字間I文字間J文字間K文字間L文字間M文字間N文字間O文字間P文字間Q文字間R文字間S文字間T文字間U文字間V文字間W文字間X文字間Y文字間Z!文字間"文字間#文字間$文字間%文字間&文字間\'文字間(文字間)文字間*文字間+文字間,文字間-文字間.文字間/文字間:文字間;文字間<文字間=文字間>文字間?文字間@文字間[文字間\\文字間]文字間^_`文字間{文字間|文字間}文字間~文字間 文字間\t文字間\n文字間\r文字間\x0b文字間\x0c文字間'

正規表現「\w+」の場合

match = re.sub(r'\w+', r'単語文字整数', printable)
match
'単語文字整数!"#$%&\'()*+,-./:;<=>?@[\\]^単語文字整数`{|}~ \t\n\r\x0b\x0c'

re.escape()

re.escape(pattern)メソッドは文字中にある特殊文字をエスケープして文字列として返します。

match = re.escape(printable)
match
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\\!\\"\\#\\$\\%\\&\\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^_\\`\\{\\|\\}\\~\\ \\\t\\\n\\\r\\\x0b\\\x0c'

以下はアドレスの特殊文字に対して

match = re.escape('admin@example.com')
print(match)
# admin\@example\.com

他にも様々なメソッドが用意されているので、興味のある方はPython公式ドキュメントをご参照ください。

Matchオブジェクトの属性

Matchオブジェクトとは、Patternオブジェクトに何かしらの実行結果が返された時のオブジェクトとなります。

print(type(re.compile(r'\w+')))
print(type(re.compile(r'\w+').search(printable)))
print(type(re.search(r'\w+', printable)))
<class '_sre.SRE_Pattern'>
<class '_sre.SRE_Match'>
<class '_sre.SRE_Match'>

Patternオブジェクトからリスト表記で返されるメソッドもありますが、Matchオブジェクトには様々な属性値が用意されています。

match = re.search(r'(?P<文字>\w+)', printable)

print('正規表現オブジェクト:{}'.format(match.re))
print('Patternオブジェクトに渡された文字列:{}'.format(match.string))
print('グループ:{}'.format(match.group()))
print('名前付きグループ:{}'.format(match.groupdict()))
print('抽出された最初のインデックス:{}'.format(match.start()))
print('抽出された最後のインデックス:{}'.format(match.end()))
print('オブジェクトに渡された最初の文字列のインデックス:{}'.format(match.pos))
print('オブジェクトに渡された最後の文字列のインデックス:{}'.format(match.endpos))
正規表現オブジェクト:re.compile('(?P<文字>\\w+)')
Patternオブジェクトに渡された文字列:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~    

グループ:0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
名前付きグループ:{'文字': '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'}
抽出された最初のインデックス:0
抽出された最後のインデックス:62
オブジェクトに渡された最初の文字列のインデックス:0
オブジェクトに渡された最後の文字列のインデックス:100

他にも様々な属性値が用意されているので、興味のある方はPyhton公式ドキュメントをご参照ください。

reモジュールを使用して文字列を整えてからCSVファイルに書き込む

これまで実装してきた各メソッドを使用して、個人情報などが乱雑に書き込まれている文字列を整えてから、CSVファイルへと書き込んでいきたいと思います。

まずは個人情報の入った文字列を変数に格納します。

※こちらの電話番号は適当なので掛けないように注意してください!

text_value = """
六分儀ゲンドウ 090-1234-5678 gendou@example.com

  碇ユイ 070-5678-1234 yui@example.com


 碇シンジ 080-1357-2468 kaminoko@xample.com
"""

print(text_value)

六分儀ゲンドウ 090-1234-5678 gendou@example.com

  碇ユイ 070-5678-1234 yui@example.com


 碇シンジ 080-1357-2468 kaminoko@xample.com

このテキストを整えてからCSVファイルに書き込んでいきます。

文字列の最初の行は改行されてしまっているので、Python組み込み関数のstrip()で除外します。

text = text_value.strip()
print(text)
六分儀ゲンドウ 090-1234-5678 gendou@example.com

  碇ユイ 070-5678-1234 yui@example.com


 碇シンジ 080-1357-2468 kaminoko@xample.com

そしてreモジュールのre.split()メソッドで、改行されている「\n」を分割してリスト化します。

entries = re.split(r'\n+', text)
print(entries)
['六分儀ゲンドウ 090-1234-5678 gendou@example.com', '  碇ユイ 070-5678-1234 yui@example.com', ' 碇シンジ 080-1357-2468 kaminoko@xample.com']

リストインデックスの1番目「碇ユイ」と2番目の「碇シンジ」の文字列頭がそれぞれ異なる空白文字となっているので、re.sub()メソッドで「文字無し」に置き換えます。

entries_new = [re.sub(r'^  |^ ', '', entry) for entry in entries]
print(entries_new)
['六分儀ゲンドウ 090-1234-5678 gendou@example.com', '碇ユイ 070-5678-1234 yui@example.com', '碇シンジ 080-1357-2468 kaminoko@xample.com']

そして正規表現で「名前」「電話番号」「Emailアドレス」に名前付けをし、リストの辞書形式にします。

pattern_obj = re.compile(r'(?P<名前>\w+) (?P<電話>0[789]0-\d{4}-\d{4}) (?P<Email>\w+\@\w+\.com)')

dict_entries = [pattern_obj.search(entry).groupdict() for entry in entries_new]

print(dict_entries)
[{'名前': '六分儀ゲンドウ', '電話': '090-1234-5678', 'Email': 'gendou@example.com'}, {'名前': '碇ユイ', '電話': '070-5678-1234', 'Email': 'yui@example.com'}, {'名前': '碇シンジ', '電話': '080-1357-2468', 'Email': 'kaminoko@xample.com'}]

ここまで出来たらCSVファイルとして格納するだけなので、モジュールをインポートして書き込みます。

import csv

with open('accunts.csv', 'wt') as f:
    csvout = csv.DictWriter(f, dict_entries[0].keys()) # キーを指定
    csvout.writeheader()
    csvout.writerows(dict_entries)

JupyterNotebookを使用している場合は「!cat accunts.csv」とすることでファイルの中身をそのまま確認できます。

!cat accunts.csv
名前,電話,Email
六分儀ゲンドウ,090-1234-5678,gendou@example.com
碇ユイ,070-5678-1234,yui@example.com
碇シンジ,080-1357-2468,kaminoko@xample.com

無理矢理実装した感じはありましたが、reモジュールを上手く使用することによって乱雑な文字列内を利用可能な形に整えることができました。

自然言語処理やデータ分析において必須とも言えるツールなので、その分野を目指す方はこの機会にマスターしてしまいましょう!

それでは以上となります。

最後までご覧いただきありがとうございました。