≪いっしょにPython≫ プログラマーの実験ノート
このページは,プログラム言語Pythonをこれから学ぼうと考えている筆者の備忘録です.
そこそこ調べて書いていますが,仕様を精密に調べたものではありません.どちらかと言えば,実装されたPython 3を使ってみてどうなったかということを中心に書いたものです.いわば,実験ノートの記録です.(2019.5.24,Python3.7.3に基づいて作成)
Pythonの練習には,
(1) IDLEというメニューから入って,>>>というプロンプトが表示される状態で使う場合
• 画面の上端がPython 3.7.x Shellとなっていて,2行目がFile, Edit, Shell, Debug, ...となっている
• 1行入力するたびに,エラーを確かめながら処理を進める,対話型インタプリタの形になっている
(2) IDLEというメニューから入って,左上端のFileをクリック→New Fileと進む場合
• 画面の上端がファイル名(初めはUntitled)となっていて,2行目がFile, Edit, format, Run, ...となっている
(3) Python 3.7.x という真っ黒なコマンドプロンプトの画面(ターミナルウィンドウ)から入るもの
の3通りが使えるが,以下は(2)のNew Fileから,テキストエディタを使って,プログラムを書く場合を考える.
Pythonの正規表現
1. 正規表現とは
• 昔のMS-DOSや今のWindowsのコマンドラインでは,任意の1文字を表す?や,2文字以上の場合も含む任意の文字列を表す*のようなワイルドカードが使える.このように,それぞれのコンピュータシステムで,文字列のパターンを表す方法が決められている.
• このワイルドカードの機能をさらに詳細に拡張した「書き方」で,文字列の検索や置換が簡単に行えるようにしたものが「正規表現」だと考えればよい.
• 正規表現では,原則として,「1つの書き方」が「一群の文字列」に対応しており,例えば\dは1つの数字ならどれにでも対応し,\wは1つのアルファベット文字ならどれにでも対応する.
(余談) 正規という用語の語感から,「邪悪な」に対する「正しい」とか,「いいかげんな」に対する「正式な」という意味は勝手に付けない方が理解しやすい.あくまで,「一群の文字列」を「一つの表現」で検索したり置換したりするための各コンピュータシステムごとの約束事だと割り切るとよい.
2. 正規表現の体験,入門
• Pythonで正規表現を扱うには,reモジュールをインポートする.
(英語で正規表現のことをregular expressionという.)
【例 2.1】
IDLE → New File → Save as .. → Run
import re #1
str1 = 'Rome was not build in a day.'
pattern1 = 'Rome'
result1 = re.match(pattern1, str1) #2
if result1 == None: #3
print(None)
else:
print(result1.group()) #4
→
Rome
#1 inport reの形でインポートしたとき,インポートした関数を使うには,re.を付ける.
#2 match(パターン, 文字列)の書式で使い,文字列の先頭が与えられたパターンと完全に一致すれば,matchオブジェクトを返す.見つからなければNoneを返す.
#3, #4 matchオブジェクト自体は読める形ではないが,match.group()とすれば一致した部分文字列を返す.group()については後で述べる.
#3 文字列の先頭が与えられたパターンに一致しなければ,Noneが返される.
※文字列の先頭が,与えられたパターンと一致しているかどうかだけならば「見れば分かるだろう!」と突っ込みを入れたくなるかもしれないが,文字列が何ページもある書類になっている場合に,段落の先頭があるパターンで始まるものを探すというのは,相当な作業になるので,照合方法があるのはよいことだと思う.
3. 正規表現の書式(1)
match(パターン, 文字列)
例2.1のように,文字列の先頭が与えられたパターンと完全に一致すれば,matchオブジェクトを返す.見つからなければNoneを返す.
search(パターン, 文字列)
• 文字列の中でパターンと完全に一致する最初の場所のmatchオブジェクトを返す.見つからなければNoneを返す.
• 正規表現としての特殊記号を使う以前の,このレベルの照合ならば,ビルドイン関数の 文字列.find(パターン) でもできるが,search()で行う方が実行速度が3倍ほど速い.(コンピュータの利用環境にもよるが,文字列検索は専門のreモジュールの方が速いということらしい)
findall(パターン, 文字列)
文字列の中でパターンと完全に一致する箇所を文字列のリストとして返す.ただし,重複がないように調べる.見つからなければ空のリストを返す.
split(パターン, 文字列)
文字列をパターンで分割してリストを返す.
sub(置換前文字列, 置換後文字列, 文字列)
文字列の中で置換前文字列に一致する部分を置換後文字列に書き換える.
再度代入しない限り,元の文字列はそのまま残り,置換後の文字列が返される.
search(パターン, 文字列)
【例 3.1】
IDLE → New File → Save as .. → Run
import re
str1 = 'Rome was not built in a day.'
pattern1 = 'day'
result1 = re.search(pattern1, str1)
if result1 == None:
print(None)
else:
print(result1.group())
→
day
findall(パターン, 文字列)
【例 3.2】
IDLE → New File → Save as .. → Run
import re
str1 = 'ababab.'
pattern1 = 'aba'
result1 = re.findall(pattern1, str1)
if result1 == None:
print(None)
else:
print(result1)
→
['aba']
abaと読める箇所は2か所あるが,重複があるので,初めのほうだけが該当する.返されるのはリストなので,そのまま表示できる
split(パターン, 文字列)
【例 3.3】
IDLE → New File → Save as .. → Run
import re
str1 = '01245367152456'
pattern1 = '245'
result1 = re.split(pattern1, str1)
if result1 == None:
print(None)
else:
print(result1)
→
['01', '36715', '6']
パターンに一致するものを取り除いて,その前後を順次リストの要素として返す.
sub(置換前文字列, 置換後文字列, 文字列)
【例 3.4】
IDLE → New File → Save as .. → Run
import re
str1 = '01245367152456'
pattern1 = '245'
result1 = re.split(pattern1, str1)
if result1 == None:
print(None)
else:
print(result1)
→
['01', '36715', '6']
パターンに一致するものを取り除いて,その前後を順次リストの要素として返す.
4. パターンを表す特殊文字
通常文字
正規表現には、特殊文字と通常文字の両方を含められます。通常文字は,ここまでに述べた通り,そのまま対応する
\d
\dは0〜9までのどの数字でもよい,1個の数字を表す
【例】
re.search('\d', 'abc:XY,-.25C') → 最初に一致する数字(セミコロン,カンマ,ドットは含まない)だから2のmatchオブジェクトを返す
\D
\Dは1個の数字以外の文字を表す.(大文字になると意味が逆になるということらしい.以下同様)
【例】
re.findall('\D', '10:ef:12.3456') → 数字以外の文字を文字列のリストとして返すから,[':', 'e', 'f', ':', '.']
\w
\wは概ね演算記号以外の英字,数字,かな,漢字を表す.(全角,半角いずれも可)
アンダーバー(_)は入る.
:;?.,({[#^~=-+*/およびその全角文字は入らない.…参考書や出版年度,Python2,3によって書かれている内容が違うので,各自の実行環境で確かめる方がよい.
【例】
re.split('\w', 'アあ山@Uab12_:;?.,({[#^~=-+*/#*+') → ['', '', '', '', '', '', '', '', '', '', ':;?.,({[#^~=-+*/#*+']
\W
\Wは上記の\w以外のもの
:;?.,({[#^~=-+*/およびその全角文字は入る.
【例】
re.sub('\W', '□', 'アあ山@Uab12_:;?.,({[#^~=-+*/#*+') → アあ山@Uab12_□□□□□□□□□□□□□□□□□□□
\s
\sは空白文字と総称される,半角スペース,全角スペースの他にタブ(\t),改行記号(\n)なども表す.
【例】
re.findall('\s', '春 spring\n夏 summer\t') → ['\u3000', '\n', ' ', '\t']
\u3000は全角スペース,\nは改行記号,' 'は半角スペース,\tはタブ
\S
\Sは空白文字以外
【例】
re.findall('\S', '春 spring\n夏 summer\t') →['春', 's', 'p', 'r', 'i', 'n', 'g', '夏', 's', 'u', 'm', 'm', 'e', 'r']
.
ドット(.)は\n以外の1文字を表す
【例】
re.findall('.', '春\nspring') → ['春', 's', 'p', 'r', 'i', 'n', 'g']
^ $
キャレット(^)が先頭を表し,ドル($)が末尾を表す
【例】 先頭がabで始まるかどうか調べる.マッチすればその個所を示すmatchオブジェクトを返す
re.search('^ab', 'abc.txt')
通常文字に対してカレットを使う場合,次のいずれでも結果は同じになる
re.search('ab', 'abc.txt')
re.match('ab', 'abc.txt')
'abc.txt'.find('ab')
(最後のものは文字列のビルドインメソッド.ただしオフセット0を返す)
【例】 末尾がtxtになっているかどうか調べる.マッチすればその個所を示すmatchオブジェクトを返す
re.search('txt$', 'abc.txt')
→ マッチする
re.search('txt$', 'eng.jpg')
→ マッチしないのでNoneを返す
カレットの後が先頭の文字列を,ドルマークの前が末尾の文字列を表すので,カレットもドルマークもあるパターンを指定すると,検索される文字列と検索するパターンが「完全一致」する場合しかマッチしない.
【例】
re.search('^ab.txt$', 'abc.txt')
→ None
※実際上,カレットやドルマークを使って「普通文字」のパターンを検索してもあまりうれしくない.むしろ,「特殊文字」との組み合わせで使うときに威力を発揮すると考えられる.
ドット(.)は\n以外の1文字を表すから,次のプログラムは3文字から成る単語を探す
【例 4.1】
IDLE → New File → Save as .. → Run
import re
List1 = ['dog','horse','bird','cat']
for x in List1:
result1 = re.search('^...$', x)
print(result1)
→
<re.Match object; span=(0, 3), match='dog'>
None
None
<re.Match object; span=(0, 3), match='cat'>
\dは0〜9までのどの数字でもよい,1個の数字を表す.セミコロン,カンマ,ドットは含まない.
次のプログラムで,765はマッチするが45.6はマッチしない.
【例 4.2】
IDLE → New File → Save as .. → Run
import re
List1 = ['dog','45.6','765','cat']
for x in List1:
result1 = re.search('^\d\d\d$', x)
print(result1)
→
None
None
<re.Match object; span=(0, 3), match='765'>
None
≪繰り返しを表す記号≫
*
MS-DOSやWindowsのコマンドラインとは異なり,アスタリスク(*)を単独の正規表現として用いることはなく,アスタリスク(*)の左に書かれた文字が0回以上(ない場合もマッチする!)繰り返されるものを表す.
【例】
re.findall('a*', 'bc
ad
aaefg
aaa')
→ ['', '', 'a', '', 'aa', '', '', '', 'aaa', '']
b,c,d,e,f,gにもマッチしていることに注意
アスタリスク(*)の左に2文字以上書かれた場合は,アスタリスク(*)は直前の文字に対してのみ働き,それ以外の文字は通常文字として処理される.
xy* →
通常文字xが「なければマッチしない」
y*のyは「あってもなくてもマッチする」
マッチする:x, xy
マッチしない:y
xyz* →
通常文字のxyは,2つ揃っていなければマッチしない
z*のzは,あってもなくてもマッチする
マッチする:xy, xyz
マッチしない:x, y, yz
xyzw* →
通常文字のxyzは,3つ揃っていなければマッチしない
w*のwは,あってもなくてもマッチする
xyz,
xyzw
マッチしない:x, y, xy
?
クエスチョンマーク(?)はその左に書かれた文字が「ない場合」と「1回ある場合」を表す.
【例】
x? →
マッチする:xy, x
マッチしない:1, y, 2, 3
1は「xがないからマッチ」,xは「1回あるからマッチ」,2は「xがないからマッチ」,xxは「1回のマッチ」と「1回のマッチ」,その後の末尾は「何もないからマッチ」(癖あり,要注意!)
クエスチョンマーク(?)の左に2文字以上書かれた場合は,クエスチョンマーク(?)は直前の文字に対してのみ働き,それ以外の文字は通常文字として処理が行われる.
xy? →
通常文字xが「なければマッチしない」
y?のyは「ない場合」と「1回ある場合」にマッチする
マッチする:xy, x
マッチしない:1y2, 3
xyz? →
通常文字xyが「そろっていなければマッチしない」
z?のyは「ない場合」と「1回ある場合」にマッチする
マッチする:xy, xyz
マッチしない:y, yz
+
プラスマーク(+)はその左に書かれた文字が「1回以上ある場合」を表す.
【例】
x+ →
マッチする:x, xx
マッチしない:y, z, 2, +
xは「1回としてマッチ」,xxは「2回としてマッチ」,+自体にはマッチしない
プラスマーク(+)の左に2文字以上書かれた場合は,プラスマーク(+)は直前の文字に対してのみ働き,それ以外の文字は通常文字として処理が行われる.
xy+ →
通常文字xが「なければマッチしない」
y+のyは「1回以上ある場合」にマッチする
マッチする:xy, xyy
マッチしない:x, y
xyz+ →
通常文字xyが「そろっていなければマッチしない」
z+のzは「1回以上ある場合」にマッチする
マッチする:xyz, xyzz
マッチしない:y, yz
≪最短のマッチを探す≫
後ろに?を付け足すと,最も短いマッチを探す.
*?
+?
??
通常,正規表現で文字列はなるべく長く(欲張って)マッチさせるように選ばれるのに対して,?を付けると,なるべく短く(控えめに)マッチさせたものが選ばれる.
Python 3.7.3 ドキュメントに示されている次の例は,これらの違いを端的に示している.
【例】
HTML文書のタグは山括弧(<>)で囲まれた,
<html><head><title>Title</title>
のような文字列の中に埋め込まれています.
このような文字列に対して,
<.*>により,山括弧(<)で始まり,任意の文字(.)の複数個が続き(*),山括弧(>)で閉じるパターンをマッチさせると,できるだけ長いスパンでマッチ仕様とするので
<html><head><title>Title</title>
のように,複数個のタグをまたがってマッチする.
実際には,山括弧で囲まれたタグを個別にマッチさせたい場合,
<.*?>により,なるべく短くマッチさせると
<html><head><title>Title</title>
のように,個々のタグに分けることができる.
≪連続する文字の指定≫
a{n}
正の整数nを波かっこ{ }で囲った記号は,その前にある文字がちょうどn個並んでいることを表す.(後で述べるブラケット[ ]で囲んだものと混同しないように注意)
【例】
x{2} … xがちょうど2個並んでいるときマッチする
re.findall('x{2}', 'xyxyzzxxyy')
xy{2} … 通常文字xにyがちょうど2個並んでいるとき(すなわちxyyに)マッチする
re.findall('xy{2}', 'xyxyzzxxyy')
a{m,n}
その前にある文字がm個以上n個以下並んでいることを表す.
【例】
x{2,3} … xが2個以上3個以下並んでいるときマッチする
re.findall('x{2,3}', 'xxyxxxzxxxx')
※4個並んでいるときは,そのうちの左の3個がマッチする
a(?=b)
bが後ろに続くaを表す(bが続いていればよく,bは1個でもよい)
【例】
x(?=y) … xy, xyyとなっているときのxにマッチする.
re.findall('x(?=y)', 'xy1xyy2xxyyy')
a(?!b)
bが後ろに続かないaを表す
【例】
x(?!y) … xzy, x1などのxにマッチする.
re.findall('x(?!y)', 'xy1xyy2xxyyy')
(?<=a)b
aに続くbにマッチする
【例】
(?<=x)y … xy, xxyなどのyにマッチする.
re.findall('(?<=x)y', 'xy1xyy2xxyyy')
(?<!a)b
aに続かないbにマッチする
【例】
(?<!x)y … zy, xx2yなどのyにマッチする.
re.findall('(?<!x)y', 'xy1xyy2xxyyy')
≪またはを表す記号≫
a|b
絶対値記号(|)でつないだ式 a|bは,aまたはbにマッチする
【例】
x|y … xまたはyの文字にマッチする
re.findall('x|y', 'ab xxzy')
\d|x|z … 数字またはxまたはzの文字にマッチする
re.findall('\d|x|z', 'axyz')
[abc]
ブラケット([ ])で囲んだ式は,その中のいずれかの式とマッチする(a|b|cと同じ)
特に,[0123456789]はいずれかの数字を表し,
[0-9]と略することができる.([1-7]のように一部指定も可能であるが,必ず昇順でなければならない.
[9-0]はエラーになる.)
同様に,
[a-z]はアルファベット小文字のいずれかを表す.
[A-Z]とすればアルファベット大文字にマッチする.[a-n]のように一部指定も可能であるが,必ず昇順でなければならない.アスキーコード順ではアルファベット大文字が前で小文字が後にあり,その間に[]_^などの記号も含まれるので,
[A-z]とすれば,アルファベットの大文字小文字の他[]_^などの記号にもマッチする.
[A-Za-z]または
[a-zA-Z]とすればアルファベットのいずれかを表す.
[A-Za-z0-9]または
[0-9a-zA-Z](-の中で昇順であれば,組み方は自由:a-x0-9A-Zなどのカ)とすればアルファベットと数字のいずれかを表す.
【例】
[xyz] … xまたはyまたはzの文字にマッチする
re.findall('[xyz]', 'ax2zz3y')
[0-9] … 数字にマッチする
re.findall('[0-9]', '1ab8X0c7')
[A-Z] … 英大文字にマッチする
re.findall('[A-Z]', '1aB8X0')
[0-9]{2} … 繰り返し記号{2}と組み合わせて,数字2文字にマッチする
re.findall('[0-9]{2}', '1a27Ay')
[^ab]
ブラケットの中にあってカレットに続く文字a,b以外の文字とマッチする
【例】
[^xyz] … x,y,z以外の文字にマッチする
re.findall('[^xyz]', '1x2yaz')
5. パターンをコンパイルする場合
• reモジュールをインポートすると,ここまでに述べたような関数を使って,文字列とパターンを直接比較して,検索や置換を行うことができる.
• これに対して,パターンをあらかじめコンパイルしておく方法をとると
(1) ここまでに述べた関数を使った検索や置換もできる.
(2) コンパイル済みオブジェクトに対するメソッドとして,より詳しい,より高速な検索と置換が行える.
(1つの正規表現[パターン]を何度も使う場合は,コンパイルしてからマッチングさせる方が有利であるが,この例題に登場するような簡単なマッチングを一度だけ行うような場合は,コンパイルしてもしなくても,速さは変わらないとされている)
(3) ただし,コンパイルしておくためには,事前の作業が必要となる.
re.match(パターン, 文字列)
パターン.match(文字列)
【例 5.1】
IDLE → New File → Save as .. → Run
import re
str1 = 'about above abroad'
pat1 = 'ab'
result1 = re.match(pat1, str1) #1
print(result1)
pat2 = re.compile('ab')
result2 = pat2.match(str1) #2
print(result2)
result3 = re.match(pat2, str1) #3
print(result3)
→
re.Match object; span=(0, 2), match='ab'
re.Match object; span=(0, 2), match='ab'
re.Match object; span=(0, 2), match='ab'
• コンパイルせずに,直接マッチングを行うときは,#1のようにインポート・モジュールのmatch()関数にパターン,文字列の2つの引数を渡す.
• コンパイル済みパターンでマッチングを行うときは,#2のようにパターン・オブジェクトのmatch()メソッドに文字列を引数として渡す.
• 実際には,#3のようにコンパイル済みパターンを使って,#1と同様にインポート・モジュールのmatch()関数にパターン,文字列の2つの引数を渡すこともできる.
• いずれも,match()に通常文字を渡すときは,先頭文字が一致するかどうかが返される.今の場合先頭[0番]から[2番]の前までspan=(0, 2)でマッチする.
コンパイル済みオブジェクトのメソッドを使う場合は,
パターン.match(文字列, pos, endpos)
の書式で,第2,第3引数をオプションとして追加することができる.ここに,posは文字列中で検索をスタートする位置のオフセットで省略すれば0番=先頭位置,endposは文字列中で検索を終了させる位置(その1つ前まで入る)のオフセットで省略すれば文字列の末尾になる.(なお,pos>endposのときは検索は行われない.負の数のオフセットを使って末尾を表す方法は,ここでは使えない)
【例 5.2】
IDLE → New File → Save as .. → Run
import re
str1 = 'about above abroad'
pat2 = re.compile('ab')
result2 = pat2.match(str1, 6)
print(result2)
→
re.Match object; span=(6, 8), match='ab'
• 太字で示したabにマッチする
re.search(パターン, 文字列)
パターン.search(文字列)
【例 5.3】
IDLE → New File → Save as .. → Run
import re
str1 = 'about above abroad'
pat1 = 'abr'
result1 = re.search(pat1, str1) #1
print(result1)
pat2 = re.compile('abr')
result2 = pat2.search(str1) #2
print(result2)
result3 = re.search(pat2, str1) #3
print(result3)
→
re.Match object; span=(12, 15), match='abr'
re.Match object; span=(12, 15), match='abr'
re.Match object; span=(12, 15), match='abr'
• コンパイルせずに,直接マッチングを行うときは,#1のようにインポート・モジュールのsearch()関数にパターン,文字列の2つの引数を渡す.
• コンパイル済みパターンでマッチングを行うときは,#2のようにパターン・オブジェクトのsearch()メソッドに文字列を引数として渡す.
• 実際には,#3のようにコンパイル済みパターンを使って,#1と同様にインポート・モジュールのsearch()関数にパターン,文字列の2つの引数を渡すこともできる.
• いずれも,search()に通常文字を渡すときは,最初に一致するマッチオブジェクトが返される.ないときはNone.今の場合先頭から[12番]から[15番]の前までspan=(12, 15)でマッチする.
コンパイル済みオブジェクトのメソッドを使う場合は,
パターン.search(文字列, pos, endpos)
の書式で,第2,第3引数をオプションとして追加することができる.ここに,posは文字列中で検索をスタートする位置のオフセットで省略すれば0番=先頭位置,endposは文字列中で検索を終了させる位置(その1つ前まで入る)のオフセットで省略すれば文字列の末尾になる.(なお,pos>endposのときは検索は行われない.負の数のオフセットを使う方法は,ここでは使えないようだ)
【例 5.4】
IDLE → New File → Save as .. → Run
import re
str1 = 'about above abroad'
pat2 = re.compile('abr')
result2 = pat2.search(str1, 12)
print(result2)
→
re.Match object; span=(12, 15), match='abr'
• 太字で示したabにマッチする
6. raw string記法
• 文字列にバックスラッシュ[円記号(\)]が使われていると,\記号はそれに引き続く幾つかの文字と組み合わせて,1つの記号を表すようになっている.
• raw string(直訳すれば,生の書き方)記法とは,正規表現を用いてマッチングを行うときに,r'文字列'またはR"文字列"(r,Rと引用符の一重二重の組合せはいずれも可)の形で書くもので,raw stringで書けば,バックスラッシュ[円記号(\)]を用いたエスケープ文字が無効になり,\を1つの文字として扱うことができる.
(1) バックスラッシュ[円記号(\)]を用いたエスケープ文字には,次のようなものがある.
番号符号 (注1) | 文字 符号 | 意味 |
8進表記 | 16進表記 |
\012 | \x0A | \n | 改行 |
\011 | \x09 | \t | 水平タブ |
\047 | \x27 | \' | アポストロフィー |
\042 | \x22 | \" | ダブルクォート |
\134 | \x5C | \\ | \という文字 |
\000〜\377, \x00〜\xFFまでは制御文字,ラテン文字,それらの拡張に対応する
(例\101=A〜\133=Z, \141=a〜\173=z)
|
(注1)
[8進表記]
• \oooの形で\に続く3つの数字まで取り込んで,8進表記の1つの番号符号に対応させるのが原則
【例6.1.1】 \1413 → \に続く3桁まで取り込んで,\141='a'と'3'の2文字と解釈されて,a3になる.
• 8進数に対応しない文字が登場したら,その前までで打ち切って,以後は通常文字と解釈する.
【例*6.1.2】 \628 → 8進数では8は使えないから,\62='2'と'8'の2文字と解釈されて,28になる.
【例6.1.3】 \52 1 → 半角スペースで切れるから,\522='*'と' ','1'の3文字と解釈されて,* 1になる.
【例6.1.4】 12\ 34 → \の後に数字がないから,単に\という文字だと解釈して,12\ 34の6文字になる.
【例6.1.5】 str1 = '\'
→ 文字列の末尾に\があると,文字列の終端を表すつもりの引用符が\に取り込まれてしまって,\'という一重引用符を表す文字になるから,この文字列str1は末尾が閉じていないと解釈され,エラーになる.(一般に文字列の末尾に\を置くことはできない)
【例6.1.6】 str1 = '\141\
142'
→ 長い文字列は\で改行して複数の行に分けて書くことができる.この場合の\記号は文字として数えない.
まず,\141は8進数の番号符号と解釈されてaになり,これに142が続くから,a142の4文字を表す.
【例6.1.7】 \na → nは8進数にないが,\nが別途定義されているから,\がnを取り込んで,改行の意味の1文字になり,\nとaの2文字を表す.
[16進表記]
• \xhhの形で\xに続く2つの数字まで取り込んで,16進表記の1つの番号符号に対応させるのが原則
【例6.1.8】 \x5f → 16進数の5f(10進数の95,8進数の137)に対応する番号符号は,アンダーバー(_)だから,_の1文字を表す.
【例6.1.9】 \x0987 → \xの後ろに数字が続けば,2桁までは16進数の番号符号として取り込まれて1文字になる.今の場合,\x09は水平タブの意味の1文字になるから,\tと87の3文字を表す.
【例6.1.10】 \ x5f → \の後に数字がないから,単に\という文字だと解釈して,\ x5fの5文字になる.
• 16進数に対応しない文字が登場したら,
エラーになる.(8進数の例*と比較すると,厳しい)
【例*6.1.11】 \x5g → 16進数ではgは使えないから,エラーになる.
【例*6.1.12】 \x 51 → \xの後に16進数がないとエラーになる.
Unicode表記 | 意味 |
\uxxxx | 16ビットのunicodeで表した文字 |
\Uxxxxxxxx | 32ビットのUnicodeで表した文字 |
正規表現に使う特殊文字 | 意味 |
\d | (10進数の)数字 |
\D | (10進数の)数字以外 |
\w | 記号,かっこ以外の
英字,数字,かな,漢字 |
\W | 記号やかっこ |
\s | 空白文字 |
\S | 空白文字以外 |
^ | 文字列の先頭 |
$ | 文字列の末尾 |
. | \n以外の1文字 |
* | 左に書かれた文字が0回以上 |
? | 左に書かれた文字が0回,1回 |
+ | \左に書かれた文字が1回以上 |
(2) 正規表現のパターン側にエスケープ文字が含まれる場合の検索,置換
== raw string記法を使わない場合 ==
【例6.2.1】
IDLE → New File → Save as .. → Run
str1 = '12\345\d'
pat1 = '\d'
result1 = re.findall(pat1, str1)
→ ['1', '2']
• str1は12å\dの5文字と解釈され,pat1は10進数を検索するから,1と2がマッチする.(番号符号\345は,発音記号åを表す文字になる.)
• パターン側の\dは文字列側の数字とマッチするが,文字列側の\dとはマッチしない.
== raw string記法を使わない場合 ==
【例6.2.2】
IDLE → New File → Save as .. → Run
import re
str1 = 's\tu\n\1412'
pat1 = '\t'
result1 = re.findall(pat1, str1)
print(result1)
→ ['\t']
• 【例6.2.2】のように,バックスラッシュ(\)を含む文字列を検索する場合に,文字列側もパターン側もPythonとして有効なエスケープ文字が使われている場合は,想定した通りのマッチングができることもある.
• しかし,次のような場合に「raw string記法を使わない」文字列の直接比較ではマズイことが起こる.
(1) バックスラッシュ(\)自体を検索する場合
(2) TeXの文章,コンピュータ内のディレクトリを表す文字列のようにPythonのエスケープ文字として有効でない文字が含まれている場合
(3) ., *, ?, +, ^, $のようにPythonの正規表現として意味が決まっている文字が含まれている場合
• 結論から言えば,このようなバックスラッシュ(\)を含む可能性のある文字列のマッチングを行う場合,Pythonマニュアルには「ごく単純な表現以外は,全てraw string記法を使うことを強く推奨する」と述べられている.
== raw string記法を使う場合 ==
【例6.2.3】
IDLE → New File → Save as .. → Run
import re
str1 = r'apple \150, orange \120'
pat1 = r'\\'
result1 = re.findall(pat1, str1)
print(result1)
pat2 = re.compile(pat1) #1
result2 = pat2.findall(str1) #2
→ ['\\', '\\']
【要点】
• (1) バックスラッシュ(\)自体を検索を検索したい場合,検索される文字列の前にr(またはR)を付ける.
• 検索される文字列の方は,これによりバックスラッシュ(\)の働きを無効にし,\を単なる1つの文字として扱える.
• 検索するパターンの方も,検索する文字列の前にr(またはR)を付け,さらにバックスラッシュ(\)という文字を表すために,\をエスケープして\\というように2つ重ねる.
• この作業は,#1,#2のようにコンパイルしてから行う場合でも同様になる.
== raw string記法を使う場合 ==
【例6.2.4】
IDLE → New File → Save as .. → Run
import re
str1 = r'\sqrt{2}, \int_1^2dx, c:\data\game.jpg'
pat1 = r'\\game'
result1 = re.search(pat1, str1)
print(result1)
→ re.Match object; span=(29, 34), match='\\game'
• この例では,特にパターン側にrを付けないとエラーになる.
【要点】
• (3) 正規表現のドット(.)は\n以外の任意の1文字を表す.したがって,小数点の場所を検索するために(.)で検索すると,他の文字もすべてマッチしてしまう.これを避けるために,検索パターン側でドットをエスケープして\.とする.
== raw string記法を使う場合 ==
【例6.2.5】
IDLE → New File → Save as .. → Run
import re
str1 = r'$567.8'
pat1 = r'\.'
result1 = re.findall(pat1, str1)
print(result1)
→ ['.']
• *, ?, +, ^, $についても同様に,検索パターン側を\*, \?, \+, \^, \$とする.
7. 正規表現中で特殊な意味を持ちうる文字のみをエスケープする関数escape()
Python 3.7以降
import reのもとで,r'文字列'で書かれたパターンpat1を引数として
pat2 = re.escape(pat1)
とすると,Pythonの正規表現で特殊な意味を持ちうる文字のみをエスケープした文字列pat2が得られる.
すなわち,上記【例6.2.3】〜【例6.2.5】の検索パターンに使う文字列が作られる.
【例7.1】
IDLE → New File → Save as .. → Run
import re
pat1 = r'\a,\b,\d,\f,\n,\s,\S,\t,\w,\W,\\,.,
*,^,?, +,$, ,g,\g,h,\h,x,\x,y,\y'
pat2 = re.escape(pat1)
print(pat2)
→ \\a,\\b,\\d,\\f,\\n,\\s,\\S,\\t,\\w,\\W,
\\\\,\.,\*,\^,\?,\ \+,\$,\ ,
g,\\g,
h,\\h,
x,\\x,
y,\\y
• g,h,x,yのようは通常文字は何も変換されないが,バックスラッシュのついた文字や.,*,^,?, +,$のように正規表現において特殊な意味を持つ文字は変換を受ける:\がないものには\が1つ付く,\があるものは2倍の数の\になる.
※なお,htmlモジュールには,HTML文書の中における特殊記号をHTMLとしてのエスケープ文字に変換する同一名の関数escape()がある.
【例7.2】
IDLE → New File → Save as .. → Run
import html
str1 = '<,>,&,",^,+,-,\,/'
str2 = html.escape(str1)
print(str2)
→ <,>,&,",^,+,-,\,/
• 上記の例のうちで初めの4個は特殊文字で,各々HTMLとしてのエスケープ文字に変換されるが,他はそのまま出力される.
8. グループ(グルーピング)
• 正規表現の中で丸括弧 ( ) で囲んだ部分はグループとしてまとめることができる.
• グループとして抽出した文字列の部分は,matchオブジェクトのgroup()やgroups()で取り出すことができる.
【例8.1】
• 次のようなメールのヘッダ部分から,送信年月日を抽出するには?
IDLE → New File → Save as .. → Run
import re
str1 = 'Date: Wed, 30 Jan 2019 21:35:01 +0900\
From: [email protected]'
pat1 = r'(\d{2}) ([JFMASOND][a-z]{2}) (\d{4})' #1
mat1 = re.search(pat1, str1)
if mat1 != None: #2
print(mat1.groups()) #3
#1
2桁の数,半角スペース,
アルファベット大文字,
アルファベット小文字2個,半角スペース,
4桁の数字の順に並んでいる箇所を調べる.
• ( )で囲んだ部分がグループになる.
• \dが数字を表し,\d{2}で数字2桁を表す.
• 月を表す文字はJan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Decだから,[JFMASOND][a-z]{2}にマッチするはず.(運悪く,Sun,mon,Tue,Wed,...にマッチしないように,絞り込むと失敗を防げる)
• \d{4}は4桁の数字で年を表す.
#2 search()関数は,マッチするものがなければNoneを返すから,マッチしたときだけ表示するようにする.
#3 返されるmatchオブジェクトのメソッドとして,.group()を利用するとマッチした部分文字列が示される.
• .group()は上記のように,引数なしで使うこともでき,その場合は.group(0)として引数に0を指定した場合と同じことになり,すべてのサブグループとして示される.→ 30 Jan 2019
• .group(1)として引数に1を指定すると,1番目のサブグループが示される.→ 30
• .group(2)として引数に2を指定すると,2番目のサブグループが示される.→ Jan
• .group(3)として引数に3を指定すると,3番目のサブグループが示される.→ 2019
• .groups()とすれば,すべてのサブグループの文字列から成るタプルが示される.→ ('30', 'Jan', '2019')
[.group()と.groups()とはデータの形式が違う]
【例8.2】
• 次のようなメールのヘッダ部分から,赤字で示した送信時刻と日本時間(グリニッジ +0900)の部分を抽出するには?
IDLE → New File → Save as .. → Run
import re
str1 = 'Date: Wed, 30 Jan 2019 21:35:01 +0900\
From: [email protected]'
pat1 = r'(\d{2}):(\d{2}):(\d{2}) (\+\d{4})' #1
mat1 = re.search(pat1, str1)
if mat1 != None:
print(mat1.group())
#1
2桁の数,コロン,
2桁の数,コロン,
2桁の数,半角スペース,
プラス4桁の数の順に並んでいる箇所を調べる.
プラスは正規表現において特別な意味を持つ記号なので,その記号の意味を外してプラスという文字自体を表すようにするために,
\を付けてエスケープする.
【例8.3】
• 次のようなメールのヘッダ部分から,メールアドレスの部分を抽出するには?
• メールアドレスのドメイン名は,xyz.comのようにドットが1つのもの,abcd.ne.jpのようにドットが2つのもの,box1.abcd.co.jpのようにドットが3つのものなどドットの個数は決まっていませんが,1つ以上3つ以下まで対処することにします.(大学関係で,学部別,職種別にメールサーバを分けている場合にドットが4個のメールアドレスになるものを見たことがあるが,これは例外でしょう)
IDLE → New File → Save as .. → Run
import re
str1 = 'Date: Wed, 30 Jan 2019 21:35:01 +0900\
From: [email protected]'
pat1 = r'(\w+)@(\w+)\.(\w+)\.*(\w*)\.*(\w*)' #1
mat1 = re.search(pat1, str1)
if mat1 != None:
print(mat1.groups())
→
('abxy', 'box1', 'yahoo', 'co', 'jp')
• (\w+)は英数字またはアンダーバー1つ以上を表す.
• @マークの後にドットが少なくとも1つはあるから,@(\w+)\.と書ける.
@m4-cd.co.jpのようにサーバ名にハイフンがある場合は(\w+)では捉えられないので,これらも含めるためには英数字にハイフン,アンダーバーを追加した([-_0-9A-Za-z]+)にするとよい(ドットも含めるとドットで区切れないのでドットは含めない.以下同様)
• 2つ目以降のドットはある場合とない場合があるから,\.*(\w*)\.*(\w*)で捉える.
【例8.4】
• yyyy/mm/dd型の年月日,hh時mm分ss秒型の時刻を検索する場合.ただし,2019/05/12, 2019/5/9, 11時12分31秒, 2時3分4秒のように年以外は1桁,2桁のいずれの表記もあるとする.
IDLE → New File → Save as .. → Run
import re
str1 = r'ab cd 2019/5/12, 1時32分5秒'
pat1 = r'(\d{4})/(\d{1,2})/(\d{1,2})'
mat1 = re.search(pat1, str1)
if mat1 != None:
print(mat1.group())
pat2 = r'(\d{1,2})時(\d{1,2})分(\d{1,2})秒'
mat2 = re.search(pat2, str1)
if mat2 != None:
print(mat2.group())
→
2019/5/12
1時32分5秒