# python_course3
## 本章の目的
1. Pythonでテキストファイルの読み書きができるようになること
2. 正規表現とは何かを理解し、使えるようになること
## ファイルの読み書き
### テキストファイルの読み書き
データの平均値や標準偏差などを求めるために、集計処理を行いたい場合があります。このとき、典型的な処理の流れは次の3ステップとなるでしょう。
1. 集計前のデータ(ファイル)を読み込む
1. データを集計する
1. 集計結果をファイルに書き込む
このようなファイルの読み書きを行うために、Pythonでは様々なファイル操作用の関数が用意されています。基本的なファイル操作の流れを以下に示します。open関数でファイルを開き、読み書きなどのファイル操作が終わったら、close関数でファイルを閉じます。
```
f = open('ファイルの指定','モード')
# ここでファイルの読み書きを行う。
f.close()
```
open関数は、開いたファイルを操作するためのファイルオブジェクトを返します。ファイルオブジェクトは、ファイルにデータを書き込んだり、閉じたりするのに必要です。上記のコードでは変数「f」にファイルオブジェクトを格納しています。
### モードとメソッド
前述のopen関数は、引数としてモードを指定する必要があります。モードとは、ファイルをどのように使うかを示すものです。モードの指定には、最大2文字の英字で指定します。1文字目は読み書きの種類、2文字目はファイルの種類を表します。以下に主要なモードを示します。
| モード(1文字目) | 機能・意味 |
| --------------- | :----------------------------------------------------------- |
| r | 読み込み用にファイルをオープン(デフォルトにつき省略可) |
| w | 書き込み用にファイルをオープン(ファイルが存在しない場合は新規作成され、存在する場合、その内容は上書きされる) |
| x | 新規作成用にファイルをオープン |
| a | 追記用にファイルをオープン(現在の末尾に書き込み) |
| モード(2文字目) | 機能・意味 |
| --------------- | :--------------------------------------- |
| t | テキストモード(デフォルトにつき省略可) |
| b | バイナリモード |
テキストとバイナリは、データを文字列として扱うか数値として扱うかの違いです(内部的には、tを指定すると数値が文字列に変換されます)。「r(読み込み)」や「w(書き込み)」などと組み合わせて使用します(例:rb = 読み込み+バイナリモード)。本コースではテキストモード(t)を対象とします。「t」は省略可能であるため、本コースで出てくるモードは全て「1文字」となります。頭の片隅に置いておいてください。
さて、open関数で開いたファイルは、メソッドを用いて読み書きを行うことができます。以下に主要なメソッドを示します。
| メソッド | 機能 |
| ---------- | :--------------------------------------------- |
| read | ファイル全体を読み込む |
| readline | ファイルを1行ずつ読み込む |
| readlines | ファイル全体を読み込み、各行をリストとして返す |
| write | ファイルに文字列を書き込む |
| writelines | ファイルに複数行を書き込む |
| seek | 読み書きする位置を移動する |
| tell | 現在の読み書き位置を取得する |
### テキストファイルのパスの指定
開くファイルを指定するには、open関数にファイルの絶対パスか相対パスを渡します。絶対パスとは、最上位に位置するディレクトリ(Windowsの「C:」など)から対象ファイルまでのルート全てです。フルパスとも呼ばれています。相対パスとは、現在作業中の場所から対象ファイルまでのルートのことです。JupyterLabでは、自身で作成した「○○(任意の名前).ipynb」のあるところが基準になります。以下、本コースでは、相対パスを利用してファイルの指定を行います。
### テキストファイルに書き込む
それでは、実際にファイルの読み書きを実行するコードを作成しましょう。まず、「○○(任意の名前).ipynb」ファイルと同じ場所に「test」フォルダを作成してください。その後、以下のサンプルコードを実行します。
###### 実行コード
```
f = open('test/sample.txt', 'w')
f.write('Hello Tokyo!\n')
f.write('Hello New York!\n')
f.close()
```
「w」が書き込み用のオープンモード、「write」がファイルに文字列を書き込むためのメソッドです。指定したファイルが存在しない場合は、記述したファイル名で新規作成されます。このコードを実行すると、「test」フォルダ内に「sample.txt」が作成されます。このテキストファイルをテキストエディタで開いて、実行結果を確認しましょう。
###### 実行結果
```
Hello Tokyo!
Hello New York!
```
### テキストファイルを読み込む
次に、先ほど作成したテキストファイルを読み出し、書き込みが無事できているか確認してみましょう。
###### 実行コード
```
f = open('test/sample.txt', 'r')
greetings = f.read()
print(greetings)
f.close()
```
「r」で読み込みモードになり、「read」メソッドでテキスト全文を読み出します。
実行結果を確認しましょう。
###### 実行結果
```
Hello Tokyo!
Hello New York!
```
改行を含め、意図した通りに書き込みができていることがわかります。ここではサンプルとして大変小さなファイルを扱っていますが、今後大きなファイルを読み込むときにはその分メモリが消費されるので注意しましょう。
### テキストファイルを閉じる
上記の実行コードで既に登場していますが、ファイルを閉じる際には、close関数を使います。
```
変数.close()
```
上記の実行コードの最終行がこの形になっていることを確認しておいてください。
### with文を用いた読み書き
さて、ファイルの読み書きの基本的な流れを復習すると、open関数でファイルを開き、close関数で閉じるというものでした。実はここで、クローズをし忘れてしまうと問題が発生することがあります。それを防ぐため、close関数が不要なwith文が用意されています。早速、上述の実行コードをwith文で書き直してみましょう。
###### 実行コード
```
with open('test/sample.txt') as f:
print(f.read())
```
モードの指定がなければ、読み込みモードはデフォルトの「r(読み込み)」である点に注意してください。
###### 実行結果
```
Hello Tokyo!
Hello New York!
```
先ほどと同じ実行結果になっていることが確認できます。「print()」の前にインデント(tabを1クリック)が入っていることに留意しましょう。「open関数でファイルを開いてclose関数で閉じる」という実行コードに比べ、with文の方がコードが簡素化されていることも確認しておきましょう。ここまででファイルの読み書きについて基本的な流れを押さえ、with文でよりシンプルかつミスのない記述方法を学習しました。ここからは、いくつかのモードやメソッドを使ってファイルの読み書きを行うことで、より理解を深めていきましょう。
### テキストファイルの末尾に追記する
sample.txtの末尾にテキストデータを書き足してみましょう。モードは「a(追記)」、メソッドは「write」を使います。
###### 実行コード
```
with open('test/sample.txt', 'a') as f:
f.write('Hello London!\n')
```
もしモードに「w(書き込み)」を指定すると、既に存在しているsample.txtのテキストデータ(「Hello Tokyo!」など)が削除され、ここで新しく記述した「Hello London!」のみのテキストデータとなってしまいます。テキストエディタでsample.txtを開くと以下の実行結果が確認できます。
###### 実行結果
```
Hello Tokyo!
Hello New York!
Hello London!
```
### テキストファイルを1行ずつ読み込む
今追記をしたテキストファイルを1行ずつ読み込んでみましょう。2つの方法を紹介します。
#### リストとして読み込む
1つ目はreadlinesメソッドを使う方法です。readlinesメソッドは、それぞれの行をリストとして返します。以下のコードを見てみましょう。
###### 実行コード
```
with open('test/sample.txt') as f:
i = f.readlines()
print(i[0])
print(i[1])
print(i[2])
```
###### 実行結果
```
Hello Tokyo!
Hello New York!
Hello London!
```
i[0]に1行目、i[1]に2行目、i[2]に3行目が格納されています。特定の行データを取得したいときに便利です。
#### ループで1行ずつ読み込む
2つ目はreadlineメソッドを使う方法です。readlineメソッドは、テキストデータを1行ずつ読み込み、末尾に達したらFalseで返します。while文と組み合わせて、各行データになんらかのループ処理をしたいときに便利です。以下に記述例を示します。
###### 実行コード
```
with open('test/sample.txt') as f:
while True:
i = f.readline()
print(i)
if not i:
break
```
###### 実行結果
```
Hello Tokyo!
Hello New York!
Hello London!
```
readlineメソッドは呼び出されるたびに次の行データを返していることに注意してください(そうでなければ、上記コードは無限に「Hello Tokyo!」を出力し続けます)。また、末尾はFalseで返すので、上記コードではbreakによってループから外れます。
### CSVファイルの読み書き
テキストファイルの読み書きの方法について、イメージは膨らんできたでしょうか。単純なテキストファイルでは、構造は「行」しかありません。そもそも、文字列が並んでいるだけでは「構造」と言われても、改行をして段落を作るくらいしか思い浮かばないかもしれません。実は、一口にテキストファイルと言っても、「構造化された」様々な形式があります。参考として以下の表で例を紹介します。
| 構造化の要素名 | 要素の例・記号 | ファイル形式の例 |
| -------------- | :------------------------------------------------ | ---------------- |
| セパレータ | タブ(\t), カンマ(,)など | CSV |
| タグ | <, > | HTML, XML |
| インデント | スペース(タブは使えない。スペース2個単位が一般的) | YAML |
このうち、CSV(Comma Separated Value)は、データをカンマ(,)で区切って記述される形式で、Excelなどの表計算ソフトで広く利用されています。ここではCSV形式を取り上げて、テキストファイルの読み書きを学んでいきましょう。jupyterlabのtestフォルダの配下に以下のファイルを作成しましょう。データはコピー&ペーストしてください。
profile.csv
```
Sato, 165.5cm, 67.1kg
Takeda, 178.2cm, 71.3kg
Yamamoto, 161.9cm, 53.2kg
```
### CSVファイルを読み込む
それでは早速、以下のコードを実行してみましょう。
###### 実行コード
```
import csv
f = open('test/profile.csv', 'r')
rd = csv.reader(f)
for row in rd:
for col in row:
print(col)
print()
f.close()
```
###### 実行結果
```
Sato
165.5cm
67.1kg
Takeda
178.2cm
71.3kg
Yamamoto
161.9cm
53.2kg
```
まず、標準ライブラリのcsvモジュールを取り込み、csv形式のデータを扱えるようにします(1行目)。rでcsvファイルの読み込みを行うモードにし(2行目)、次に「リーダ」と呼ばれるインスタンスを取得します(3行目)。reader()はcsvモジュールの関数です。for文によってデータを1行ずつ取得し(4行目)、さらにfor文で列を取得し(5行目)これらを表示します(6行目)。
with文で書き換えると以下のようになります。
###### 実行コード
```
import csv
with open('test/profile.csv', 'r') as f:
rd = csv.reader(f)
for row in rd:
for col in row:
print(col)
print()
```
### CSVファイルに書き込む
続いて、CSVファイルへの書き込みを行ってみましょう。こちらも実行コードを元に説明していきます。
###### 実行コード
```
import csv
f = open('test/profile.csv', 'a')
w = csv.writer(f)
w.writerow(['Ishikawa','188.7cm','81.1kg'])
f = open('test/profile.csv', 'r')
rd = csv.reader(f)
for row in rd:
for col in row:
print(col)
print()
f.close()
```
###### 実行結果
```
Sato
165.5cm
67.1kg
Takeda
178.2cm
71.3kg
Yamamoto
161.9cm
53.2kg
Ishikawa
188.7cm
81.1kg
```
CSVファイルへの書き込みでは、csvモジュールのwriter関数を用いて「ライタ」というインスタンスを取得します(3行目)。次に、writerowなどのメソッドでリストなどを書き込むことができます(4行目)。
### 主な関数とメソッド
CSVファイルの読み書きに使う、主な関数とメソッドは以下のとおりです。
| 関数 | 機能 |
| ------ | :------------------------------- |
| reader | リーダ(インスタンス)を取得する |
| writer | ライタ(インスタンス)を取得する |
| メソッド | 機能 |
| ----------- | :------------------------------ |
| writerow() | CSVファイルに書き込む(1行) |
| writerows() | CSVファイルに書き込む(複数行) |
ここでは単純なテキストファイルに加え、構造化されたテキストファイルとしてCSVファイルの読み書きについて学習しました。本コースでは他の形式については取り上げませんが、ぜひ色んな形式のファイルを扱えるように、学びの幅を広げていってください。
### 文字コードと文字エンコードの指定
Pythonでファイルの読み書きを行う際、ファイルの形式によってはエラーが発生したり、意図したものと異なる文字が表示されたりします(このことを「文字化け」と言います)。また、文字コードは「文字に割り当てられたコード(それ自体)」のことで、文字エンコードとは「どの文字にはどのコードが割り当てられているか」という対応表のことです。ファイルの読み書きを行う際には、この文字エンコードの指定を明示的に行うことで、プログラムの文字エンコード不一致による文字化けを予防することに繋がります。以下に、主な文字エンコードの記法をまとめます。
| 名前 | 使用環境の例 | Pythonでの記法例 |
| ----------- | :------------ | ---------------- |
| UTF-8 | Web、多くのOS | utf-8 |
| Shift_JIS | Windows | shift_jis |
| ISO-2022-JP | メール | iso2022_jp |
| EUC-JP | UNIX系OS | euc_jp |
このうち「UTF-8」は、世界中の文字を共通に示せる「Unicode」という表現形式がベースになっており、世界で最もポピュラーな文字エンコードです。PythonやJupyterLabでは、「UTF-8」が標準形式となっており、それ以外の形式では正しく表示されません。そこで、文字エンコードを指定する方法を見ておきましょう。
```
変数 = open('ファイル名', 'w', encoding='文字エンコード')
```
open関数のキーワード引数encodingを使います。例えば、「UTF-8」を指定したい場合は、上記で '文字エンコード' となっている部分に「utf_8」と入力します。その他の文字エンコードを指定したい場合には、「Pythonでの記法例」に則って記述しましょう。例えば、上記で登場したsample.txtを、UTF-8を指定して開く場合には、以下のようなコードを実行します。
```
file = open('ttest/sample.txt', 'w', encoding='utf_8')
```
## 正規表現
### 正規表現とは
プログラムを組む際、文字列が「ある特定のパターン」に該当するかを調べることができると便利です(この「該当する」ことを、「マッチする」と呼びます。以降では、「マッチする」という表現で統一していきますので、頭に入れておくようにしましょう)。例えば、郵便番号や電話番号のように、ユーザーに一定数の数字を記入してもらう場合、「数字」が「○桁」並んでいる、というパターンになっているか調べ、当てはまらない場合にはエラー表示することができます。このような文字列があるパターンにマッチするかを調べるための機能が、「正規表現 (regular expression)」です。
### 正規表現の書き方
正規表現には多くの記述方法があるため、本コースで全てを扱うことはできませんが、そのうち代表的な書き方を紹介します。例えば、携帯電話番号のように「0〜9までの数字が11文字並んでいる」というパターンは次のように書くことができます。
```
[0-9]{11}
```
0〜9までの数字1文字が([0-9])、11文字続く({11})という意味です。
正規表現は、色々なプログラミング言語やアプリケーションで、文字列のパターンマッチに利用されています。Pythonで正規表現を書く場合、通常の文字列ではなく「raw文字列」を用いることが一般的です。raw文字列では、バックスラッシュ(\)や円記号(¥)を、エスケープシーケンス(画面上に文字を出力する際に、文字それ自体ではなく、改行や文字色の変更、文字の消去など、文字出力の制御を行う、特殊な意味や機能を持つ文字列のこと)ではなく、通常の文字として扱います。例えば、改行を表す「¥n」は頻出のエスケープシーケンスですが、raw文字列ではそういった特別な意味は持たず、単に「¥n」という文字列として扱われます。また、raw文字列の書き方は簡単で、文字列の先頭にrを付けるだけです。例えば、先ほどのパターンをraw文字列で書き換えると次のようになります。
```
r'[0-9]{11}'
```
電話番号はハイフン(-)で区切って書くこともあるかと思います。参考までに、もう少し携帯電話番号らしく記述する方法も見てみましょう。
```
r'[0-9]{3}-[0-9]{4}-[0-9]{4}'
```
3桁(090など)、4桁、4桁の数字をハイフン(-)で繋ぐ、という非常にシンプルな書き方であることがわかります。
### パターンマッチを調べる
次は、文字列がパターンにマッチするかを調べる方法を学んでいきましょう。正規表現の機能は、標準ライブラリのreモジュールをインポートすることで活用できます。ここで、パターンと文字列を比較する関数を紹介します。
| 関数 | 機能 |
| --------- | :------------------------------------------------- |
| match | 文字列の先頭部分にマッチ |
| search | 文字列の任意の部分にマッチ |
| fullmatch | 文字列の全体にマッチ |
| findall | 全てのマッチを検索 |
| split | パターンにマッチしたところで文字列を分割 |
| sub | 文字列のうち、パターンにマッチする全ての部分を置換 |
例えば、match関数で文字列の先頭部分がマッチしているかを調べる場合、以下のように記述します。
```
re.match(パターン, 文字列)
```
早速、それぞれの関数を使いながら、パターンマッチを調べてみましょう。
### match関数で文字列の先頭を調べる
「Python Programming」という文字列をパターンと比較してみましょう。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.match('Python', source)
print(result)
```
###### 実行結果
```
<re.Match object; span=(0, 6), match='Python'>
```
パターンが文字列にマッチすると、戻り値のマッチオブジェクトが表示されます。マッチしない場合には、戻り値は何も表示されません。span=に書かれているのは、マッチした箇所の開始と終了を表すインデックスです。上記の実行コードでは、0文字目の「P」から5文字目の「n」までがマッチしています。「終了インデックス(この例では6)の1文字手前まで」がマッチしている、という点に注意しましょう。match=の後には、正規表現にマッチした文字列が続きます。
### search関数で文字列の任意部分を調べる
仮に、match関数で「Programming」とのマッチングを調べても、マッチしないことは容易に想像できます。一方で、search関数であれば、パターンがどこにあってもマッチします。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.search('Programming', source)
print(result)
```
###### 実行結果
```
<re.Match object; span=(7, 18), match='Programming'>
```
この通り、7文字目から17文字目でマッチが検出されました。なお、search関数では、マッチする部分が複数あった場合に、「最初のマッチ部分のみ」が返されることに注意しましょう (マッチする全ての部分を抽出するには、後述のfindall関数を用います)。
### fullmatch関数で文字列全体のマッチを調べる
fullmatch関数は、文字列全体が指定したパターンにマッチしているかを調べる関数です。文字列全体が「数字が10個並んでいる」というパターンにマッチしているか、以下で調べてみましょう。
###### 実行コード
```
import re
source = '0123456789'
result = re.fullmatch(r'[0-9]{10}', source)
print(result)
```
指定したパターン r'[0-9]{10}'は「0から9までの数字が」「10個並ぶ」を表しています。
###### 実行結果
```
<re.Match object; span=(0, 10), match='0123456789'>
```
この通り、マッチしていることが確かめられました。
### findall関数による全てのマッチの検索
これまで見てきた関数では、1つマッチが見つかった時点で処理が終了していました。例えば、「Python Programming」の中に「n」がいくつ含まれているか確認するためにはどうすればよいでしょうか。そのような場合は、findall関数を用いるのが最適です。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.findall('n', source)
print(result)
```
###### 実行結果
```
['n', 'n']
```
findall関数は、マッチしたパターンをリストにして返します。この例のように、文字列が短く、マッチした個数が少ない場合には数えれば済みますが、もう少し工夫ができそうです。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.findall('n', source)
print('nは', len(result), '個含まれています。')
```
###### 実行結果
```
nは2個含まれています。
```
リストの要素数を数えるlen関数を用いることで、文字列に含まれる「n」の数を出力することができました。
### split関数によるマッチを利用した分割
単純にマッチを検索するだけでなく、パターンで文字列を分割し、リストを作ることもできます。文字列から「n」を検索し、分割してみましょう。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.split('n', source)
print(result)
```
###### 実行結果
```
['Pytho', ' Programmi', 'g']
```
このように、要素が3つのリストが作成されました。
### sub, subn関数によるマッチした部分の置換
sub関数では、次のように引数を渡すことで、パターンにマッチした部分を置き換えることができます。
```
re.sub('置換対象の文字列', '置換後の文字列', '文字列')
```
では実行コードを見ていきましょう。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.sub('n', '!', source)
print(result)
```
###### 実行結果
```
Pytho! Programmi!g
```
変数sourceが持つ 「Python Programming」という調査対象の文字列の中にある「n」(= 置換対象の文字列) を 「!」(= 置換後の文字列) に置き換えられています。また、引数countで、最大置換回数 (個数) を指定することもできます。
###### 実行コード
```
# 上記コードの続き
result = re.sub('n', '!', source, count=1)
print(result)
```
###### 実行結果
```
Pytho! Programming
```
先ほどと違い、「Programming」部分の「n」は置換されていないことが見て取れます。sub関数は、replace関数と似ているように感じられますが、下記2点で異なっています。
1. replace関数では引数にリテラル文字列を取るが、sub関数ではパターンを置く
1. replace関数では、完全一致した文字列が置換されるが、sub関数では正規表現に合致した (= パターンにマッチした) 文字列が置換される
また、subn関数では、置換された文字列と置換された部分の個数とのタプルを返します。
###### 実行コード
```
import re
source = 'Python Programming'
result = re.subn('n', '!', source)
print(result)
```
###### 実行結果
```
('Pytho! Programmi!g', 2)
```
### finditer関数でマッチする部分をイテレータで取得
finditer関数を使うと、マッチする全ての部分をイテレータ(要素を順番に取り出すことのできるインターフェースのこと。リストやタプル、辞書などのコレクション型の要素に対して繰り返し処理を行う場合に用いられる)で取得することができます。戻り値はマッチオブジェクトであるため、マッチした部分の位置 (インデックス) なども取得することができます。
###### 実行コード
```
import re
source = 'aaa@bbb.com, ccc@eee.jp, ddd@eee.net'
result = re.finditer(r'[a-z]+@[a-z]+\.[a-z]+', source)
print(result)
```
###### 実行結果
```
<callable_iterator object at 0x7fdc9f3c2520>
```
イテレータそれ自体は、print()しても中身は出力されません。この実行コードではどんな文字列に対して、どんなパターンを比較しているでしょうか?答えは、与えられた文字列 (aaa@bbb.com など) が、Eメールアドレス (= パターン) かどうか、を調べています。[a-z]は英小文字aからzまでにいずれかの文字を意味し、+は直前のパターンを1回以上繰り返す (この場合は[a-z])ことを意味します。そのため、[a-z]+は英小文字が1文字以上繰り返される文字列にマッチします。「.(ドット)」は特別な意味を持つメタキャラクタなので、「\ (バックスラッシュ)」でエスケープをする必要があることに注意しましょう (メタキャラクタなどについては後述します)。
さて、イテレータを出力してみましょう。方法はいくつかありますが、ここではfor文を使っていきます。
###### 実行コード
```
# 上記コードの続き
for m in result:
print(m)
```
###### 実行結果
```
<re.Match object; span=(0, 11), match='aaa@bbb.com'>
<re.Match object; span=(13, 23), match='ccc@eee.jp'>
<re.Match object; span=(25, 36), match='ddd@eee.net'>
```
### 正規表現のメタキャラクタと特殊シーケンス
パターンマッチの主な調べ方を学んだところで、Python3のreモジュールで使えるメタキャラクタ、特殊シーケンスの一部を紹介します ([Pythonの公式ドキュメント](https://docs.python.org/ja/3/)に、全てのメタキャラクタ、特殊シーケンスが掲載されています。ぜひ一度参照してみましょう)。まずはメタキャラクタです。
| メタキャラクタ | 意味 |
| ---------------- | :----------------------------------- |
| . (ピリオド) | 任意の1文字 |
| ^ (ハット) | 文字列の先頭 |
| $ (ドル) | 文字列の末尾 |
| * (アスタリスク) | 直前のパターンを0回以上繰り返し |
| + (プラス) | 直前のパターンを1回以上繰り返し |
| ? (クエスチョン) | 直前のパターンを0回または1回繰り返し |
| {m} | 直前のパターンをm回繰り返し |
| {m, n} | 直前のパターンをm〜n回繰り返し |
| [ ... ] | [と]で囲まれた内の任意の1文字 |
| [ - ] | 範囲の中の任意の1文字 |
| \| | or (または) |
既に、携帯電話番号(r'[0-9]{3}-[0-9]{4}-[0-9]{4}')とEメールアドレス(r'[a-z]+@[a-z]+\.[a-z]+')が登場していましたが、これらがメタキャラクタを使って表現されていたことがわかると思います。改めて解説します。
```
[0-9]{3}
→ 0〜9までの数字が3つ並ぶ (3回繰り返す)
携帯電話番号は桁数が決まっているため、{}を用いています。
[a-z]+
→ a〜zまでの英小文字が1つ以上並ぶ (1回以上繰り返す)
Eメールアドレスでは、ユーザーによってドメインの長さが異なるため、
'+' を用いて「(英小文字が) 1つ以上並んでいるもの」にマッチするようにしています。
\.
'.'(ドット)は正規表現において「ピリオド (任意の1文字)」と認識されてしまいます。
ここでは、「ドット」として使用したいため、'\'(バックスラッシュ) でエスケープしています。
```
続いて、特殊シーケンスです。
| 特殊シーケンス | 意味 |
| -------------- | :---------------------------------------------- |
| \d | 数字 |
| \D | 数字以外の文字 |
| \s | 空白文字 |
| \w | 英小文字、英大文字、数字、_(アンダースコア) |
| \W | 英小文字、英大文字、数字、_(アンダースコア)以外 |
表だけ見ていてもイメージが湧かないと思いますので、少し例を見ていきましょう。以下の、stringモジュールがテスト用に提供している英数字、記号を含む100文字を題材にします。
```
import string
printable = string.printable
print(printable)
# 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
```
###### 実行コードと結果
```
# 上記コードの続き
re.findall('\d', printable)
# ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
re.findall('\w', printable)
# ['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', '_']
```
printableが持つ100文字と「\d」で比較すると0〜9までの数字が、「\w」で比較すると英小文字、英大文字、数字、_(アンダースコア)が抽出されていることがわかります。本章の最後に正規表現のより実際に近い例を用意しているのですが、その前に、より柔軟な正規表現を書くために欠かせない「先読み/後読み」を解説します。
### 先読みと後読み
先読みと後読みは、それぞれ肯定、否定と組み合わせて4通りあります。
#### 肯定先読み (?=pattern)
直後にpatternがあればマッチする
例)foo(?=bar)は、直後にbarがあるfooにマッチする
#### 否定先読み (?!pattern)
直後にpatternがなければマッチする
例)foo(?!bar)は、直後にbarがないfooにマッチする
#### 肯定後読み (?<=pattern)
直前にpatternがあればマッチする
例)(?<=bar)fooは、直前にbarがあるfooにマッチする
#### 否定後読み (?<!pattern)
直前にpatternがなければマッチする
例)(?<!bar)fooは、直前にbarがないfooにマッチする
例えば、47の都道府県名に対して「京」を引数にfindall関数を使うと、「東京都」と「京都府」の2つがマッチします。ここで、「(?<=東)京都」(直前に「東」がある「京都」という意味になり「東京都」がマッチ)や「京都(?=府)」(直後に「府」がある「京都」という意味になり「京都府」がマッチ)というように、より正確にパターンマッチを調べることができます。
### 文字列の形式がマッチしているか調べる
正規表現を使って文字列のパターンマッチを調べる典型例として、パスワードの入力があります。読者の皆様も、何かのwebサービスで「半角英数字8文字以上。英大文字、英小文字、数字をそれぞれ含む」パスワードを登録したことがあるのではないでしょうか。実行コードを見ていきましょう。
###### 実行コード
```
password = r'(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]{8,}'
result = re.match(password, 'Python123')
print(result)
```
###### 実行結果
```
<re.Match object; span=(0, 9), match='Python123'>
```
パーツに分けて解説していきます。
```
(?=.*[a-z]) : 英小文字が含まれているか
(?=.*[A-Z]) : 英大文字が含まれているか
(?=.*[0-9]) : 数字が含まれているか
[a-zA-Z0-9] : 英小文字、英大文字、数字のいずれか1文字
{8,} : 直前の文字が8文字以上続く
```
実行コードでは、まず変数passwordに、上記のパターンを代入しています。そして、match関数にて「Python123」というパスワードがこのパターンにマッチするか調べています。パターンの記述がやや複雑に思えるかもしれませんが、調べている内容についてはイメージしやすいのではないでしょうか。
## まとめ
本章ではまず、Pythonでのテキストファイルの読み書きについて学びました。これにより、プログラムの実行結果を画面に映し出すだけでなく、ファイルに書き込み、保存することができるようになりました。正規表現は、メールアドレスやパスワードなど、webサービスに置いてユーザーに入力を促す際に利用される、大変身近な機能です。引き続き、具体的な活用シーンを思い浮かべながら、Pythonを学んでいきましょう。