文字列の比較は、プログラムで最もよく使われる操作のひとつです。
文字列と文字列の以下のような比較はPythonでも頻繁に用いられます。
- 完全に一致する
- 部分的に一致/含まれる
- 前方一致、後方一致
- パターンの一致
- 類似度を計算する
このようなPythonでの文字列の比較・一致の検証手法を、現役のエンジニアが解説しています。
一致した部分を置換(置き換え)する方法は以下のページでまとめています。
結論
完全に一致するか検証
一致するとき==
がTrue, 不一致のとき!=
がTrue
print('bar' == 'bar')
# True
print('bar' != 'foo')
# True
部分的に一致するか検証
text = 'abcd'
print('bc' in text)
# True
前方一致・後方一致を検証
text = 'ABCDEFGH'
print(text.startswith('AB'))
# True
text = 'ABCDEFGH'
print(text.endswith('GH'))
# True
パターンとの一致を検証
・全体とパターンが一致するか検討する re.fullmatch()
m = re.fullmatch('abcd', text)
if m:
print('matched!')
# matched!
・一部とパターンが一致するか検証
text = 'foobar-123456-buzbuz'
m = re.search(r'\d+', text)
if m:
print(m.group(0))
# 123456
・類似度を計算
import difflib
text_a = "This is a sample"
text_b = "This is the sample"
r = difflib.SequenceMatcher(None, text_a, text_b).ratio()
print(r)
# 0.8823529411764706
実行環境
Python 3.9.2
完全に一致するか検証
文字列が完全に一致(同一の内容)か判定するには ==
比較演算子を用います。
==
は文字列がすべて一致するときの他、数値や論理式が等価のときにTrue
を返します。
print('bar' == 'bar')
# True
print('bar' == 'foo')
# False
!=
は ==
の否定形であり、非等価(不一致)のときにTrue
を返します。
print('bar' != 'foo')
# True
==
による比較では、大文字・小文字が区別されます。
print('bar' == 'baR')
# False
同様に、全角・半角文字は異なる文字として区別されます。
print('abc' == 'abc')
# False
==
はスペースを含む文字列、長い文章にも適用できます。
text_a = "This is the same sentence"
text_b = "This is the same sentence"
print(text_a == text_b)
# True
部分的に一致/含むを検証
文字列が含まれているか(ある文字列が、他の文字列の部分文字列かどうか)はin
演算で判定できます。
文字列 x が s に含まれているか検討するには x in s
とし、含まれていればTrue
となります。
text = 'abcdefg'
print('bc' in text)
# True
print('xy' in text)
# False
in
演算の否定形はnot in
です。
よく使われるのは、not in
、if
文を使って、文字列に含まれているか否かで条件分岐をする形です。
text = 'abcdefg'
if 'x' not in text:
print("No 'x' in text")
# No 'x' in text
前方一致・後方一致を検証
文字列の先頭に特定の文字列が含まれるか(前方一致)を検討するには、str.startswith()
を用います。
text = 'ABCDEFGH'
print(text.startswith('AB'))
# True
print(text.startswith('GH'))
# False
文字列の末尾に特定の文字列が含まれるか(後方一致)を検討するには、str.endswith()
を用います。
text = 'ABCDEFGH'
print(text.endswith('GH'))
# True
print(text.endswith('AB'))
# False
なお、str.startswith()
、str.endswith()
共に、文字列の代わりにタプルを渡すことで、複数条件を実現できます。
text = 'ABCDEFGH'
cand = ('XX', 'AB', 'IJ')
print(text.startswith(cand))
# True
パターンの一致を検証
あるパターンで文字列が構成されているかどうかを調べるには、正規表現の標準ライブリ re を使用します。
使用するにはreライブラリのインストールは不要ですが、importは必要です。
import re
全体がパターンに一致するか検証
全体がパターンに一致しているか検討するには、re.fullmatch()
を用います。一致する場合はMatchオブジェクト、不一致ならNone
が返されます。
引数の構造は以下のとおり。
match = re.fullmatch(pattern, string)
pattern | 正規表現で表したパターン |
string | 検証対象の文字列 |
patternには、通常の文字列をそのまま書いてもOKです。
一致する場合はMatchオブジェクトが返され、Matchオブジェクトはif
ではTrueとなります。
import re
text = 'abcd'
m = re.fullmatch('abcd', text)
if m:
print('matched!')
# matched!
.
は任意の1文字を表現します。
m = re.fullmatch(r'a..d', text)
if m:
print('matched!')
# matched!
+
は直前の文字の1文字以上の繰り返しを表現します。.+
なら、任意の文字の1文字以上の組み合わせとなります。
以下の例では textは "c" で終わるパターンではないので、不一致となり、None
が返されます。
m = re.fullmatch(r'.+c', text)
print(m)
# None
部分的にパターンが一致するか検証
文字列の全体ではなく、部分的にあるパターンに一致しているか検討するには、re.search()
を使います。文字列の先頭にあるパターンを検討するときは re.match()
でも問題ありませんが、ここでは簡潔にre.search()
のサンプルを示しています。
re.search()
の使い方や戻り値は re.fullmatch()
と同じです。
正規表現で任意の数字を表すには[0-9]
としますが、\d
でも同じ意味です。\d+
で任意の数字の繰り返しとなります。
Matchオブジェクトから、一致した部分を取り出すには group()
を用います。
import re
text = 'foobar-123456-buzbuz'
m = re.search(r'\d+', text)
if m:
print('matched!')
print(m.group(0))
# matched!
# 123456
re.search()
で不一致の場合はNoneが返されます。
特定の文字のいずれかにマッチさせるには、それらを [ ]
で囲んで表します。
以下の例では text には "x", "y"のいずれも含まれないのでマッチせず、Noneが返ってきています。
m = re.search(r'[xy]', text)
print(m)
# None
任意の小文字アルファベットが[a-z]
と表現できるので、任意の英小文字の繰り返し部分が含まれるパターンは以下のようになります。^
は文字列の先頭を表します。
m = re.search(r'^[a-z]+', text)
if m:
print('matched!')
print(m.group(0))
# matched!
# foobar
反対に、文字列の末尾を表すには $
を用います。
m = re.search(r'[a-z]+$', text)
if m:
print('matched!')
print(m.group(0))
# matched!
# buzbuz
大文字・小文字・全角・半角の違いを吸収
大文字・小文字
==
比較演算では、大文字・小文字を区別するので次の2変数は同値ではありません。
s_large = "Hoge Fuga"
s_small = "hoge fuga"
print(s_large == s_small)
# False
大文字と小文字の違いを吸収し、差を考慮せずに比較するには、どちらかに統一すればよいでしょう。
小文字に統一するときは str.lower()
、大文字ならstr.upper()
を使用します。
print(s_large.lower())
# hoge fuga
print(s_large.upper())
# HOGE FUGA
print(s_large.lower() == s_small.lower())
# True
全角・半角
全角文字と半角文字も、異なる文字として評価されます。
full = "カタカナモジ"
half = "カタカナモジ"
print(full == half)
# False
半角文字が含まれる文字列を全角に統一するには、標準ライブラリの unicodedata
パッケージを利用します。
unicodedata.normalize()
はユニコード文字列を一定の規則に従って正規化します。
str = unicodedata.normalize(form, unistr)
正規化の形式 form
は'NFC'、'NFKC'、'NFD'、'NFKD'の4つの中から指定でき、それぞれ同値とみなす文字が異なってきます。詳しくはドキュメント参照。
半角カタカナを全角カタカナに置き換えるだけなら、'NFKC'でよいでしょう。
import unicodedata
print(half)
# カタカナモジ
half_normal = unicodedata.normalize('NFKC', half)
print(half_normal)
# カタカナモジ
print(full == half_normal)
# True
類似度を計算する
文字列を比較するテクニックとして、文字列同士の類似度を計測することが考えられます。
Pythonではさまざまな実装があり、それぞれ使用しているアルゴリズムは多様なものです。実行時間や適する応用ジャンルが異なりますが、ここでは使用方法を軽く解説するのに留めます。
difflib(Ratcliff-Obershelp)
最も手軽に実装できるのは、Pythonの標準ライブラリである difflib を使用する方法。
Ratcliff-Obershelpというアルゴリズムにより類似度が計算されます。
使用には difflib
のimportが必要です(インストールは不要)。
r = difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)
isjunk 類似度算出で無視する文字を指定(デフォルトはNone)
a 文字列1
b 文字列2
autojunk 自動 junk ヒューリスティックの有効化フラグ(デフォルトはTrue)
difflib --- 差分の計算を助ける — Python 3.9.4 ドキュメント
SequenceMatcher.ratio()
[0, 1] の範囲の浮動小数点数で、シーケンスの類似度を測る値を返します。
類似度が高いほど(完全一致に近づくほど)1.0
に近くなり、反対に類似度が低い文字列は0
に近づきます。
import difflib
# 同じテキスト
text_a = "Same Text"
text_b = "Same Text"
r = difflib.SequenceMatcher(None, text_a, text_b).ratio()
print(r)
# 1.0
# 全く異なるテキスト
text_a = "ABC"
text_b = "XYZ"
r = difflib.SequenceMatcher(None, text_a, text_b).ratio()
print(r)
# 0.0
# 類似するテキスト
text_a = "This is a sample"
text_b = "This is the sample"
r = difflib.SequenceMatcher(None, text_a, text_b).ratio()
print(r)
# 0.8823529411764706
Levenshtein (レーベンシュタイン距離法)-
パッケージのインストールが必要ですが、文字列の類似度比較で比較的手軽に使えるのがLevenshteinです。
文字列間の編集距離を求めるアルゴリズム Levenshtein は直感的にも理解しやすいもので、よく使われます。情報の授業や雑誌などで見かけたことのある人も多いのでは。
pipなら以下のようにインストールします。
$ pip install python-Levenshtein
Levenshtein.distance(string1, string2)
string1, string2 対象の文字列
戻り値はレーベンシュタイン距離です。difflib.SequenceMatcherのように0~1の数値に正規化するには、distance()
のレーベンシュタイン距離を文字列の長さで除します。(最も長い文字列長に合わせます)
このようにすると、結果の数値は、difflibの ratio()
とは反対になり、類似度が高いほど(完全一致に近づくほど)0
に近くなり、反対に類似度が低い文字列は1.0
に近づきます。
text_a = "Same Text"
text_b = "Same Text"
d = Levenshtein.distance(text_a, text_b)
max_len = len(text_a) if len(text_a) > len(text_b) else len(text_b)
r = d / max_len
print(r)
# 0
いちいち上記のように書くわけにはいかないので、関数化します。
def levens_normalize(text1:str, text2:str) -> float:
d = Levenshtein.distance(text1, text2)
max_len = len(text1) if len(text1) > len(text2) else len(text2)
r = d / max_len
return r
# 全く異なるテキスト
text_a = "ABC"
text_b = "XYZ"
r = levens_normalize(text_a, text_b)
print(r)
# 1.0
# 類似するテキスト
text_a = "This is a sample"
text_b = "This is the sample"
r = levens_normalize(text_a, text_b)
print(r)
# 0.16666666666666666
Pythonの学習法について
Python の勉強が辛くなっていませんか?
Pythonは比較的取り組みやすい言語と言われていますが、プログラミング初心者にとっては分からないことだらけ。
ゼロから独学で勉強するのは厳しい道のりです。
今回、様々な現場、システム、言語を経験してきた現役エンジニアの立場から、初心者でも挫折しない学習方法を解説する記事を書きました。もちろん、お金をかけずに習得できる方法も解説しています。
できるだけストレスがかからない勉強法を解説しているので、ぜひ参考にしてみてくださいね。
今回参考にしたページ・資料
比較 組み込み型 — Python 3.9.4 ドキュメント
共通のシーケンス演算 組み込み型 — Python 3.9.4 ドキュメント
unicodedata.normalize unicodedata --- Unicode データベース — Python 3.9.4 ドキュメント