Pythonで文字列を抽出(位置・文字数、正規表現)
Pythonで文字列str
から部分文字列を抽出する方法について説明する。任意の位置・文字数を指定して抽出したり、正規表現のパターンで抽出したりできる。
- 位置(文字数)を指定して抽出: インデックス、スライス
- インデックスで文字を抽出
- スライスで文字列を抽出
- 文字数を利用
- 日本語(全角文字)の場合
- 正規表現で抽出:
re.search()
,re.findall()
- 正規表現パターンの活用例
- ワイルドカード的なパターン
- 貪欲マッチと非貪欲マッチ
- 括弧でパターンの一部を抽出
- 任意の1文字にマッチ
- 文字種(数字、英字、ひらがななど)で抽出
- 先頭・末尾から抽出
- 複数のパターンで抽出
- 大文字と小文字を区別せずに抽出
文字列を検索して一致する部分の位置を取得したり、個数をカウントしたりしたい場合や、文字列中の部分文字列を別の文字列に置換したい場合は以下の記事を参照。
テキストファイルから抽出したい場合は、ファイルを文字列として読み込めばよい。
位置(文字数)を指定して抽出: インデックス、スライス
インデックスで文字を抽出
[]
にインデックスを指定するとその位置の文字を取得できる。
s = 'abcde'
print(s[0])
# a
print(s[4])
# e
例からわかるように、インデックスは0
始まり(1文字目が0
)。例の文字列のインデックスは以下のようになる。
abcde
01234
負の値を使うと後ろからの位置を指定できる。一番最後の文字が-1
となる。
print(s[-1])
# e
print(s[-5])
# a
存在しないインデックスを指定するとエラーとなる。
# print(s[5])
# IndexError: string index out of range
# print(s[-6])
# IndexError: string index out of range
スライスで文字列を抽出
[start:step]
とすると、start <= x < stop
の範囲の文字列が抽出できる。上述のインデックスと同じく0
始まり。start
を省略すると先頭から、end
を省略すると末尾までの範囲となる。
s = 'abcde'
print(s[1:3])
# bc
print(s[:3])
# abc
print(s[1:])
# bcde
インデックスと同様、負の値も使える。
print(s[-4:-2])
# bc
print(s[:-2])
# abc
print(s[-4:])
# bcde
start > end
となってもエラーにはならず、空文字''
が抽出される。
print(s[3:1])
#
print(s[3:1] == '')
# True
また、存在しない範囲を指定してもエラーにならない。範囲外は無視される。
print(s[-100:100])
# abcde
開始位置start
と終了位置stop
に加えて、増分step
も指定可能。[start:stop:step]
のように書く。飛び飛びの文字を抽出できる。step
に負の値を指定すると後ろから順番に抽出される。
print(s[1:4:2])
# bd
print(s[::2])
# ace
print(s[::3])
# ad
print(s[::-1])
# edcba
print(s[::-2])
# eca
スライスについての詳細は以下の記事を参照。
文字数を利用
文字列の文字数(サイズ)は組み込み関数len()
で取得可能。これを利用して、文字列の中央の文字を取得したり、スライスと組み合わせて前半または後半の文字列を抽出したりできる。
インデックス[]
やスライス[:]
には整数int
の値しか指定できないので注意。/
による割り算は結果が浮動小数点数float
になるのでエラーとなる(割り切れる場合も同じ)。
以下の例では整数除算//
を使っている。小数点以下は切り捨てられる。
s = 'abcdefghi'
print(len(s))
# 9
# print(s[len(s) / 2])
# TypeError: string indices must be integers
print(s[len(s) // 2])
# e
print(s[:len(s) // 2])
# abcd
print(s[len(s) // 2:])
# efghi
日本語(全角文字)の場合
半角文字も全角文字も同じ1文字としてカウントされる。
s = 'abcあいう'
print(len(s))
# 6
print(s[4])
# い
print(s[1:5])
# bcあい
全角文字を2文字分としてカウントしたい場合は以下の記事を参照。
正規表現で抽出: re.search(), re.findall()
正規表現を利用するとより柔軟な処理が可能。
標準ライブラリのreモジュールを使う。詳細については以下の記事を参照。
正規表現パターンにマッチする文字列を抽出するにはre.search()
を使う。第一引数に正規表現パターンの文字列、第二引数に対象の文字列を指定する。
import re
s = '012-3456-7890'
print(re.search(r'\d+', s))
# <re.Match object; span=(0, 3), match='012'>
\d
は数字、+
は直前のパターンを1回以上繰り返すことを表している。したがって、\d+
は連続した1文字以上の数字にマッチする。
reモジュールで使える正規表現のメタ文字や特殊シーケンスおよびその注意点については前掲のreモジュールの記事を参照。覚えておくと便利なものはこの記事でも次に述べる。
\d
のように正規表現の特殊シーケンスではバックスラッシュ\
が使われるので、''
や""
の前にr
を付けてエスケープシーケンスを無効化するraw文字列を使うと便利。
マッチする部分があるとre.search()
はマッチオブジェクトを返す。マッチオブジェクトのgroup()
メソッドでマッチした部分を文字列として取得できる。
m = re.search(r'\d+', s)
print(m.group())
# 012
print(type(m.group()))
# <class 'str'>
マッチオブジェクトについての詳細は以下の記事を参照。span()
メソッドで位置を取得したりすることもできる。
上の例のように、re.search()
はマッチする部分が複数あっても最初の部分のマッチオブジェクトしか返さない。
re.findall()
はマッチするすべての部分を文字列のリストとして返す。
print(re.findall(r'\d+', s))
# ['012', '3456', '7890']
正規表現パターンの活用例
ここからは覚えておくと便利なメタ文字・特殊シーケンスを使った正規表現パターンをいくつか紹介する。基本的かつシンプルなもののみで、これがすべてではない。
ワイルドカード的なパターン
.
は改行以外の任意の1文字、*
は直前のパターンの0回以上の繰り返し。
例えば、a.*b
はa
で始まってb
で終わる文字列にマッチする。*
は0回以上の繰り返しなのでab
にもマッチする。
print(re.findall('a.*b', 'axyzb'))
# ['axyzb']
print(re.findall('a.*b', 'a---b'))
# ['a---b']
print(re.findall('a.*b', 'aあいうえおb'))
# ['aあいうえおb']
print(re.findall('a.*b', 'ab'))
# ['ab']
一方、+
は直前のパターンの1回以上の繰り返し。a.+b
の場合、ab
にはマッチしない。
print(re.findall('a.+b', 'ab'))
# []
print(re.findall('a.+b', 'axb'))
# ['axb']
print(re.findall('a.+b', 'axxxxxxb'))
# ['axxxxxxb']
さらに、?
は直前のパターンが0回か1回。a.+b
の場合、ab
およびa
とb
の間に1文字だけが存在している場合にのみマッチする。
print(re.findall('a.?b', 'ab'))
# ['ab']
print(re.findall('a.?b', 'axb'))
# ['axb']
print(re.findall('a.?b', 'axxb'))
# []
貪欲マッチと非貪欲マッチ
*
, +
, ?
で注意すべきなのが貪欲(greedy)マッチ。
*
, +
, ?
は貪欲(greedy)マッチで、できるだけ長いテキストにマッチする。*?
, +?
, ??
とすると、非貪欲(non-greedy)、最小(minimal)マッチとなり、できるだけ短い文字列にマッチする。
以下の例のように、貪欲マッチの場合、思わぬ部分でマッチしてしまうことがあるので注意。
s = 'axb-axxxxxxb'
print(re.findall('a.*b', s))
# ['axb-axxxxxxb']
print(re.findall('a.*?b', s))
# ['axb', 'axxxxxxb']
print(re.findall('a.+b', s))
# ['axb-axxxxxxb']
print(re.findall('a.+?b', s))
# ['axb', 'axxxxxxb']
括弧でパターンの一部を抽出
正規表現パターンの文字列の一部を括弧()
で囲むと、その部分の文字列が抽出できる。
print(re.findall('a(.*)b', 'axyzb'))
# ['xyz']
re.search()
の場合はマッチオブジェクトのメソッドの引数指定で()
部分の文字列や位置を抽出可能。詳細は以下の記事を参照。
文字として括弧()
にマッチさせたい場合はバックスラッシュ\
でエスケープする。対象文字列の括弧に囲まれた部分を抽出したい場合は、パターン文字列のエスケープありの括弧内をエスケープなしの括弧で囲めばよい。
print(re.findall(r'\(.+\)', 'abc(def)ghi'))
# ['(def)']
print(re.findall(r'\((.+)\)', 'abc(def)ghi'))
# ['def']
任意の1文字にマッチ
[]
で文字列を囲むと、その中の文字のいずれか1文字にマッチする。*
, +
, ?
などと組み合わせることも可能。
また、[a-z]
のようにUnicodeコードポイント(文字コード)が連続した文字を-
でつなぐと、その間の文字すべてが対象となる。[a-z]
は小文字のアルファベットのいずれか1文字を表す。
print(re.findall('[abc]x', 'ax-bx-cx'))
# ['ax', 'bx', 'cx']
print(re.findall('[abc]+', 'abc-aaa-cba'))
# ['abc', 'aaa', 'cba']
print(re.findall('[a-z]+', 'abc-xyz'))
# ['abc', 'xyz']
文字種(数字、英字、ひらがななど)で抽出
シンプルな方法としては、上述の[a-z]
のように範囲を指定することで数字や英字(アルファベット)などにマッチできる。
s = 'abc-012-あいうえお'
print(re.findall('[0-9]+', s))
# ['012']
print(re.findall('[a-z]+', s))
# ['abc']
print(re.findall('[ぁ-ゟ]+', s))
# ['あいうえお']
ひらがなの場合にゟ
というよくわからない文字を使っている理由や、Unicodeプロパティを使用した文字種の抽出など、詳細は以下の記事を参照。
また、数字は\d
でもマッチ可能だが、Python3のreモジュールではデフォルトで半角にも全角にもマッチするという注意点がある。詳細は以下の記事を参照。
先頭・末尾から抽出
先頭から始まる文字列、または、末尾で終わる文字列のみを抽出したい場合はメタ文字^
(先頭にマッチ), $
(末尾にマッチ)を使う。
s = 'abc-def-ghi'
print(re.findall('[a-z]+', s))
# ['abc', 'def', 'ghi']
print(re.findall('^[a-z]+', s))
# ['abc']
print(re.findall('[a-z]+$', s))
# ['ghi']
複数のパターンで抽出
複数のパターンのいずれかにマッチする部分を抽出したい場合は|
を使う。正規表現パターンA
、パターンB
をA|B
のように記述する。
s = 'axxxb-012'
print(re.findall('a.*b', s))
# ['axxxb']
print(re.findall(r'\d+', s))
# ['012']
print(re.findall(r'a.*b|\d+', s))
# ['axxxb', '012']
大文字と小文字を区別せずに抽出
reモジュールではデフォルトでは大文字と小文字を区別して処理される。引数flags
にre.IGNORECASE
を指定すると、大文字と小文字が区別されなくなる。
s = 'abc-Abc-ABC'
print(re.findall('[a-z]+', s))
# ['abc', 'bc']
print(re.findall('[A-Z]+', s))
# ['A', 'ABC']
print(re.findall('[a-z]+', s, flags=re.IGNORECASE))
# ['abc', 'Abc', 'ABC']