Python 文字列中のサブストリングの出現回数を数える

部分文字列とは、Stringの中にある連続した文字の並びのことである。

例えば、"substring""Find a substring within a string" の部分文字列となります。

Pythonの文字列は、Unicode文字を表すバイトの配列で、人間が読みやすい形式でデータを表現するために最もよく使われるデータ型の1つです。

>
今回は、Pythonで文字列内の指定した部分文字列の出現回数を数える方法を学びます。

count() を使用して文字列中の部分文字列の出現回数をすべて検索する

文字列クラスの count() メソッドは、実際にこのような処理を行います。

これは、指定した値 (部分文字列) が文字列の中に何回現れるかを返します。

また、オプションのパラメータとして startend があり、これは探索空間の開始点と終了点を表します。

string.count(value, start, end)


注意: デフォルトの start0 であり、デフォルトの end は文字列の長さである。

それでは、代表的な文章で、このメソッドの使い方を見てみよう。

# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'


# Occurences of substring 'apples' in the string
result = str1.count(substr)
print("Number of substring occurrences:", result)


# Occurences of substring 'apples' from index 0 to index 40
start, end = 0, 40
result2 = str1.count(substr, start, end)
print("Number of substring occurrences:", result2)


この結果は

Number of substring occurrences: 2
Number of substring occurrences: 1


このメソッドは非常にシンプルでわかりやすく、ほとんどの場合でうまく機能します。

効率的であり、大きな入力サイズにも対応できる。

例えば、大きなテキストを読み込んで、一般的な単語や、必ず存在するストップワードを検索することができる。

また、単純に大きな検索空間を取得して、効率の良さを実感することもできる。

例えば、ウィリアム・シェイクスピアの「ロミオとジュリエット」をProject Gutenbergからダウンロードして、「ロミオ」が何回出てきたかを調べてみよう。

import time
import requests


txt = requests.get('https://www.gutenberg.org/cache/epub/1513/pg1513.txt').text
print(f"Downloaded {len(txt)} bytes of text...")


start_time = time.time()
count = txt.count('Romeo')
end_time = time.time()


print(f"Time to find all occurences of 'Romeo': {end_time - start_time}s with {count} results")


この結果、以下のようになる。

Downloaded 167333 bytes of text...
Time to find all occurences of 'Romeo': 0.0s with 153 results


あるいは、もっと一般的な単語、例えば 'a' を見つけたとしても。

start_time = time.time()
count = txt.count('a')
end_time = time.time()


print(f"Time to find all occurences of 'a': {end_time - start_time}s with {count} results")


となり、結果は同じです。

Downloaded 167333 bytes of text...
Time to find all occurences of 'Romeo': 0.0s with 8308 results


実行時間の大部分は、テキストをダウンロードするのにかかる時間である。

注意:このメソッドは、文字列の中で部分文字列が発生する位置を返しません。

もしこの知識が必要なら、出現箇所をカウントする以外の変換処理を行うか、正規表現を使って出現箇所を見つけるか、あるいは startsWith() を使って個々のケースをチェックすることになるでしょう。

以下のセクションでは、この2つのケースについて見ていきます。

Pythonで文字列中の部分文字列の出現回数と位置をすべて検索する。

startswith()メソッドは、文字列が指定したvalue(部分文字列) で始まっていればTrueを、始まっていなければFalseを返します。

count() メソッドと同様に、このメソッドにもオプションのパラメータ start と end があり、探索空間の開始位置と終了位置を指定することができます。

string.startswith(value, start, end)


デフォルトの start 値は 0 であり、デフォルトの end 値は文字列の長さである。

このメソッドの使用方法は少し複雑で、メソッド自体に加えてリスト内包を使用するか、より伝統的な for ループを使用する必要があるからです。

startswith()` メソッドは部分文字列の開始インデックスを返します。

その後、リスト内包を利用して、探索空間全体を繰り返し処理します。

# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'


# Print original string and substring
print("Original string is:", str1)
print("Substring is:", substr)


# Sse startswith() and list comprehension
# Find all occurrences of a substring within a string
result = [i for i in range(len(str1)) if str1.startswith(substr, i)]


# Print the number of substring occurrences
print("Number of substring occurrences is:", len(result))


# We can also find the starting indices of substrings
print("Starting indices of substrings are: " + str(result))


これにより、前回のように出現回数だけでなく、文字列自体の開始位置も得ることができます。

問題の文字列、つまりその長さがわかっているので、それが検索文字列の中で占める空間を簡単に推測することができます。

Original string is: John has 1 apple, Sarah has 2 apples, Mike has 5 apples.
Substring is: apples
Number of substring occurrences is: 2
Starting indices of substrings are: [30, 49]


re.finditer() を使って Python で文字列中の部分文字列の出現回数をすべて検索する

finditer()関数は Python の RegEx ライブラリであるre` に含まれています。

この関数は、与えられた文字列の中で特定のパターンが出現している箇所を見つけるために最もよく使われます。

このメソッドと RegEx 式を扱う他の多くのメソッドを使用できるようにするために、まず regex ライブラリをインポートする必要があります。

re.finditer(pattern, string, flags=0)


>
> もし、正規表現についてもっと学びたいのであれば、Pythonの正規表現ガイドを読んでみてください。

関数 re.finditer() は、文字列中の RegEx パターンに重複しないすべてのマッチオブジェクトを生成するイテレータを返します。

スキャンは左から右へ行われ、マッチは見つかった順に返されます。

空のマッチも含まれます。

フラグを使用すると、さまざまなユニークな機能や構文のバリエーションを有効にすることができます (例えば、 re.Ire.IGNORECASE フラグを使用すると大文字小文字を区別しないマッチングを行うことができます。

re.Are.ASCII フラグは通常の UNICODE マッチではなく ASCII マッチのみを有効にします)。

先ほどのリスト内包を正規表現に置き換えてみましょう。

import re


# Define string and substring
str1 = 'John has 1 apple, Sarah has 2 apples, Mike has 5 apples.'
substr = 'apples'


# Print original string and substring
print("Original string is:", str1)
print("Substring is:", substr)


# Use re.finditer() to find all substring occurrences
# Using list comprehension we find the start and end indices of every substring occurence
result = [(_.start(), _.end()) for _ in re.finditer(substr, str1)]


# Print number of substrings found
print("Number of substring occurrences is:", len(result))


# Print start and end indices of substring occurrences
print("The start and end indices of the substrings are: " + str(result))


この結果は

Original string is: John has 1 apple, Sarah has 2 apples, Mike has 5 apples.
Substring is: apples
Number of substring occurrences is: 2
The start and end indices of the substrings are: [(30, 36), (49, 55)]


これで、文字列の長さを開始インデックスに手動で足し算する必要がなくなりました。

ベンチマーク・パフォーマンス

性能は選択した方法によって異なることに注意する必要がある。

どのような場合でも、コードはかなり速く終わりますが、それでも本当に大きな検索空間ではパフォーマンスを考慮する価値があります。

これらの3つの方法を使って、「ロミオとジュリエット」の中の文字 'a' のすべてのインスタンスを探してみましょう。

import re
import time
import requests


txt = requests.get('https://www.gutenberg.org/cache/epub/1513/pg1513.txt').text
print(f"Downloaded {len(txt)} bytes of text...")


start_time_1 = time.time()
result_1 = txt.count('a')
end_time_1 = time.time()


print(f"String.count(): Time to find all occurences of 'a': {end_time_1 - start_time_1}s")


start_time_2 = time.time()
result_2 = [i for i in range(len(txt)) if txt.startswith('a', i)]
end_time_2 = time.time()


print(f"List Comprehensions: Time to find all occurences of 'a': {end_time_2 - start_time_2}s")


start_time_3 = time.time()
result_3 = [(_.start(), _.end()) for _ in re.finditer('a', txt)]
end_time_3 = time.time()


print(f"Regex: Time to find all occurences of 'a': {end_time_3 - start_time_3}s")


この結果は

String.count(): Time to find all occurences of 'a': 0.0s
List Comprehensions: Time to find all occurences of 'a': 0.031008481979370117s
Regex: Time to find all occurences of 'a': 0.002000093460083008s


count()` メソッドは間違いなく最も効率的なものですが、文字列がどこにあるのかを知ることはできません。

追加知識として – 正規表現はこのタスクではまだ非常に高速で、手動でリスト内包ループを行うよりも10倍以上効率的です。

結論

この問題を解くには複数の方法があり、抽出したいデータに応じてよく使われる方法もあります。

ベンチマークでは、count()`メソッドは他の2つのメソッドより優れていましたが、部分文字列がどこにあるかという情報は得られません。

一方、正規表現は遅いとはいえ、この情報を提供してくれます。

特筆すべきは、3つのアプローチとも例外なく高速であり、文学の傑作全体を解析して共通の単語を探すのに1秒の何分の一もかからないということです。

タイトルとURLをコピーしました