テキストの前処理は、自然言語処理(NLP)において最も重要なタスクの1つである。例えば、テキスト分類に使用する前に、テキスト文書からすべての句読点を削除したい場合があります。同様に、テキスト文字列から数字を抽出したい場合もあります。このような前処理を行うために手動でスクリプトを書くのは、多くの労力を必要とし、エラーも発生しやすい。このような前処理作業の重要性を考慮し、テキストの前処理作業を容易にするために、さまざまな言語で正規表現(別名Regex)が開発されてきました。
正規表現は、検索パターンを記述したテキスト文字列で、最小限のコードで文字列内のパターンにマッチしたり、置き換えたりするために使用されます。このチュートリアルでは、Python言語でさまざまなタイプの正規表現を実装します。
正規表現を実装するには、Pythonの re
パッケージを使用します。以下のコマンドでPythonの re
パッケージをインポートしてください。
import re
文字列の中のパターンを検索する
最も一般的な自然言語処理タスクの1つは、文字列が特定のパターンを含むか否かを検索することです。例えば、文字列に数字が含まれているという条件に基づいて、文字列に対してある操作を行いたいことがあります。
文字列の中のパターンを検索するには、 re
パッケージの match
と findall
関数を使用します。
マッチング機能
変数 text
を以下のようなテキスト文字列で初期化する。
text = "The film Titanic was released in 1998"
任意の長さ、任意の文字の文字列にマッチする正規表現を書いてみよう。
result = re.match(r".*", text)
match関数の最初のパラメータは、検索したい正規表現を指定します。正規表現はアルファベットの
r` で始まり、その後に検索したいパターンが続きます。パターンは、他の文字列と同様にシングルクォートまたはダブルクォートで囲む必要があります。
上記の正規表現は、任意の長さ、任意の文字の文字列にマッチさせようとしているので、テキスト文字列にマッチします。マッチした場合、 match
関数は以下に示すように _sre.SRE_Match
オブジェクトを返します。
type(result)
出力されます。
_sre.SRE_Match
さて、マッチした文字列を見つけるには、以下のコマンドを使います。
result.group(0)
出力
'The film Titanic was released in 1998'
match関数でマッチした文字列がない場合は、
null` オブジェクトを返します。
さて、先ほどの正規表現は、任意の長さ、任意の文字を持つ文字列にマッチします。また、長さ0の空の文字列にもマッチします。これを試すために、text変数の値を空の文字列で更新してみましょう。
text = ""
ここで、もう一度次の正規表現を実行すると、一致する文字列が見つかります。
result = re.match(r".*", text)
任意の長さ、任意の文字の文字列にマッチするように指定しているので、空の文字列にもマッチしています。
長さが1以上の文字列にマッチさせるには、次の正規表現を使用します。
result = re.match(r".+", text)
ここで、プラス記号は、文字列が少なくとも1文字でなければならないことを指定します。
アルファベットを検索する
match` 関数を使うと、文字列の中にある任意のアルファベットを検索することができます。text変数を次のようなテキストで初期化してみましょう。
text = "The film Titanic was released in 1998"
ここで、大文字と小文字の両方のアルファベットをすべて見つけるには、次の正規表現を使います。
result = re.match(r"[a-zA-z]+", text)
この正規表現は、小さな文字 a
から小さな文字 z
まで、または大文字 A
から大文字 Z
までのアルファベットにマッチすることを意味します。プラス記号は、文字列が少なくとも1文字以上であることを指定します。上の式で見つかったマッチを表示してみよう。
print(result.group(0))
出力してみましょう。
The
出力では、最初の単語、つまり The
が返されているのがわかるでしょう。これは、match
関数が最初に見つかったマッチを返すだけだからです。正規表現では、アルファベットの小文字と大文字の両方を含むパターン、つまり a
から z
までを検索するように指定しています。最初に見つかったマッチは The
でした。Theの後にはスペースがありますが、これはアルファベットとして扱われないため、マッチングは停止し、最初のマッチである
The` だけが返されます。
しかし、これには問題があります。文字列がアルファベットではなく数字で始まっている場合、数字の後にアルファベットがあっても match
関数は null を返します。これを実際に見てみましょう。
text = "1998 was the year when the film titanic was released"
result = re.match(r"[a-zA-z]+", text)
type(result)
出力
NoneType
上のスクリプトでは、text変数を更新して、数字から始まるようにしました。次に、match
関数を使って、文字列の中のアルファベットを検索しています。文字列にアルファベットが含まれていても、match
関数は文字列の最初の要素にしかマッチしないので、NULLが返されます。
この問題を解決するには、search
関数を使用します。
検索機能
search関数は
match関数と似ている。つまり、指定されたパターンにマッチしようとする関数である。しかし、
match関数とは異なり、最初の要素にのみマッチするのではなく、パターン全体にマッチします。したがって、
search` 関数は、以下のように、文字列の先頭にアルファベットがなく、文字列の他の場所にアルファベットがある場合でも、マッチした結果を返します。
text = "1998 was the year when the film titanic was released"
result = re.search(r"[a-zA-z]+", text)
print(result.group(0))
出力
was
これはテキスト文字列の中で最初に見つかったマッチなので、search
関数は “was “を返します。
最初から文字列をマッチングさせる
ある文字列が特定の単語で始まっているかどうかを調べるには、以下のようにキャロットキー、すなわち ^
の後にマッチさせたい単語を続けて search
関数を使用します。例えば、次のような文字列があるとします。
text = "XYZ 1998 was the year when the film titanic was released"
この文字列が “1998” で始まっているかどうかを調べるには、次のように search
関数を使用します。
result = re.search(r"^1998", text)
type(result)
出力では、文字列の先頭に直接 “1998” が含まれていないため、null
が返されます。
では、content text変数を変更して、先頭に “1998 “を追加し、”1998 “が先頭にあるかどうかをチェックしてみましょう。以下のスクリプトを実行します。
text = "1998 was the year when the film titanic was released"
if re.search(r"^1998", text):
print("Match found")
else:
print("Match not found")
出力してください。
Match found
端から文字列をマッチングさせる
ある文字列が特定の単語で終わっているかどうかを調べるには、正規表現でその単語を使い、その後にドル記号をつければよい。ドル記号は文の終わりを表します。次の例を見てみましょう。
text = "1998 was the year when the film titanic was released"
if re.search(r"1998$", text):
print("Match found")
else:
print("Match not found")
上記のスクリプトでは、テキスト文字列が「1998」で終わっているかどうかを探そうとした。
出力は
Match not found
ここで、文字列を更新し、文字列の最後に “1998 “を追加すると、上記のスクリプトは以下のように「一致が見つかりました」と返します。
text = "was the year when the film titanic was released 1998"
if re.search(r"1998$", text):
print("Match found")
else:
print("Match not found")
出力
Match found
文字列の代入
これまで、文字列の中にパターンが存在するかどうかを調べるために正規表現を使ってきました。もうひとつの高度な正規表現機能である、文字列内のテキストの置換について説明します。この目的のためには、 sub
関数を使用します。
substitute関数の簡単な例を見てみましょう。次のような文字列があるとします。
text = "The film Pulp Fiction was released in year 1994"
Pulp Fiction” という文字列を “Forrest Gump” (1994年に公開された別の映画) に置き換えるには、次のように sub
関数を使用することができます。
result = re.sub(r"Pulp Fiction", "Forrest Gump", text)
sub` 関数の最初のパラメータは、置換するパターンを見つけるための正規表現です。2番目のパラメータは古いテキストを置き換える新しいテキストで、3番目のパラメータは置換操作が実行されるテキスト文字列です。
結果変数を表示すると、新しい文字列が表示されます。
では、文字列中のすべてのアルファベットを「X」という文字に置き換えてみましょう。次のスクリプトを実行する。
text = "The film Pulp Fiction was released in year 1994"
result = re.sub(r"[a-z]", "X", text)
print(result)
出力してください。
TXX XXXX PXXX FXXXXXX XXX XXXXXXXX XX XXXX 1994
出力から、大文字を除くすべての文字が置換されていることがわかる。これは、A-Z
ではなく、a-z
のみを指定したためです。この問題を解決するには、2つの方法があります。以下のように、正規表現に a-z
と共に A-Z
を指定する方法です。
result = re.sub(r"[a-zA-Z]", "X", text)
あるいは、以下のように、サブ関数に追加のパラメータ flags
を渡して、その値を大文字小文字を区別しないことを意味する re.I
に設定することも可能です。
result = re.sub(r"[a-z]", "X", text, flags=re.I)
フラグの種類についての詳細は、 Python regex official documentation page を参照してください。
略記文字クラス
複雑なロジックを記述することなく、さまざまな文字列操作機能を実行するために使用できる、さまざまなタイプの省略記法文字クラスが存在します。このセクションでは、そのうちのいくつかについて説明します。
文字列から数字を削除する
文字列から数字を検索する正規表現は d
です。このパターンは、以下のように長さ0の空の文字列に置き換えて、文字列から数字を削除するために使用されます。
text = "The film Pulp Fiction was released in year 1994"
result = re.sub(r"d", "", text)
print(result)
出力
The film Pulp Fiction was released in year
文字列からアルファベットを削除する
text = "The film Pulp Fiction was released in year 1994"
result = re.sub(r"[a-z]", "", text, flags=re.I)
print(result)
出力
1994
ワード文字列の削除
文字列から単語文字(アルファベットと数字)をすべて取り除き、残りの文字を残したい場合は、以下のように正規表現でw
パターンを使い、長さ0の空の文字列に置き換えることができます。
text = "The film, '@Pulp Fiction' was ? released in % $ year 1994."
result = re.sub(r"w","", text, flags = re.I)
print(result)
出力
, '@ ' ? % $ .
出力は、すべての数字とアルファベットが取り除かれていることを示しています。
非英語の文字を削除する
すべての非単語文字を削除するには、以下のように W
パターンを用いることができる。
text = "The film, '@Pulp Fiction' was ? released in % $ year 1994."
result = re.sub(r"W", "", text, flags=re.I)
print(result)
出力
ThefilmPulpFictionwasreleasedinyear1994
出力から、数字とアルファベットを除くすべてが(スペースも含めて)削除されていることがわかります。
複数のパターンをグループ化する
角括弧を使うと、複数のパターンをグループ化して文字列のマッチングや代入を行うことができます。実際、大文字と小文字をマッチさせるときに、この方法を使いました。複数の句読点をグループ化し、文字列から削除してみましょう。
text = "The film, '@Pulp Fiction' was ? released _ in % $ year 1994."
result = re.sub(r"[,@'?.$%_]", "", text, flags=re.I)
print(result)
出力してみましょう。
The film Pulp Fiction was released in year 1994
text変数の文字列には複数の句読点がありますが、これらの句読点をすべて角括弧を使って正規表現でグループ化していることがわかると思います。ここで重要なのは、ドットとシングルクォートを使用する場合は、バックスラッシュというエスケープシーケンスを使用しなければならないことです。これは、デフォルトでは、ドット演算子は任意の文字に使用され、シングルクォートは文字列を示すために使用されるからです。
複数スペースの削除
単語や句読点を削除した結果、単語と単語の間に複数のスペースが現れることがあります。例えば、最後の例の出力では、in
と year
の間に複数のスペースがあります。これらのスペースは、1つのスペースを参照する s
パターンを使って削除することができる。
text = "The film Pulp Fiction was released in year 1994."
result = re.sub(r"s+"," ", text, flags = re.I)
print(result)
出力
The film Pulp Fiction was released in year 1994.
上のスクリプトでは、単一または複数のスペースを参照する s+
という表現を使用しました。
開始と終了のスペースを削除する
時々、文の最初や最後にスペースが入ることがあるが、これは好ましくないことが多い。次のスクリプトは、文頭のスペースを削除する。
text = " The film Pulp Fiction was released in year 1994"
result = re.sub(r"^s+", "", text)
print(result)
出力
The film Pulp Fiction was released in year 1994
同様に、文字列の末尾にあるスペースを取り除くには、次のスクリプトを使うことができる。
text = "The film Pulp Fiction was released in year 1994 "
result = re.sub(r"s+$", "", text)
print(result)
一文字削除
アポストロフィのような句読点を削除すると、意味のない1文字になることがあります。たとえば、Jacob's
という単語からアポストロフィを削除してスペースに置き換えると、結果として得られる文字列は Jacob s
となります。ここでは s
は意味を持ちません。このような一文字は、以下のように正規表現を使って削除することができます。
text = "The film Pulp Fiction s was b released in year 1994"
result = re.sub(r"s+[a-zA-Z]s+", " ", text)
print(result)
出力
The film Pulp Fiction was released in year 1994
このスクリプトは、1つ以上のスペースの間にある小文字や大文字を、1つのスペースに置き換えます。
文字列の分割
文字列の分割も非常に重要な機能です。文字列は re パッケージの split
関数を使って分割することができる。split` 関数は分割されたトークンのリストを返します。以下のように、1つ以上のスペース文字がある単語の文字列を分割してみましょう。
text = "The film Pulp Fiction was released in year 1994 "
result = re.split(r"s+", text)
print(result)
出力
['The', 'film', 'Pulp', 'Fiction', 'was', 'released', 'in', 'year', '1994', '']
同様に、 split
関数を使って、他の正規表現で文字列を分割することもできます。例えば、次の split
関数は、カンマが見つかったときに単語の文字列を分割します。
text = "The film, Pulp Fiction, was released in year 1994"
result = re.split(r",", text)
print(result)
出力
['The film', ' Pulp Fiction', ' was released in year 1994']
全てのインスタンスを検索する
関数 match
は最初の要素に対してマッチを行い、関数 search
は文字列に対してグローバル検索を行い、最初にマッチしたインスタンスを返します。
例えば、以下のような文字列があったとする。
text = "I want to buy a mobile between 200 and 400 euros"
この文字列からすべての数字を検索したいとします。もし search
関数を使用すると、以下のように最初に出現した数字、つまり 200 だけが返されます。
result = re.search(r"d+", text)
print(result.group(0))
出力
200
一方、findall
関数を使うと、以下のように、マッチしたすべての発話を含むリストが返されます。
text = "I want to buy a mobile between 200 and 400 euros"
result = re.findall(r"d+", text)
print(result)
出力
['200', '400']
出力から、findall
関数によって “200 “と “400 “の両方が返されることがわかります。
結論
今回は、Pythonで最もよく使われる正規表現関数をいくつか勉強しました。正規表現は、トピックモデリング、テキスト分類、センチメンタル分析、テキスト要約など、さまざまなアプリケーションでさらに使用できるテキストの前処理に非常に便利です。