見出し画像

正規表現と文字列操作(Using Python to Access Web Data: Week 2)

引き続き、ミシガン大学がCoursera上で開講しているPython for Everybody Specializationの第3コースを受講した記録です。実際の講義はWeek 2から始まります。

コースの受講で習得できることは次の4つです。前回のテキストデータの単語カウントに引き続き、いよいよ本格的にPythonプログラミングを実践するフェーズに入りました。

・Retrieve data from websites and APIs using Python
・Understand the protocols web browsers use to retrieve documents and web apps
・Use regular expressions to extract data from strings
・Work with XML (eXtensible Markup Language) data

まずは、Webにある情報をプログラムで自動的に取得するようになるところからです。仕事でも様々なサイトにアクセスして情報を取得しているので、これが自動化できるようになれば業務効率化にも役に立つかもしれません。

Week 1はPythonのインストールやスクリーンショットの方法など、このコースを受けるための復習回であるので割愛します。


1.正規表現(Regular Expression)とは

<テキストの範囲>
Chapter 11: Regular Expression

Dr. Chuckはこの講義は必ずしも必要ではないと説明していますが、いわゆるPythonとは別のプログラミング言語では必要とされていた概念や表現方法を学んでいきます。

文章からある単語を検索するためには、検索する文字列は必ず一致していなければなりませんでした。例えば、単語をsplit()によってリスト化して以下のような単語に分解したとします。

All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives.
(The Constitution for the United States: Section 1)

ここで、Representativesという単語を検索しようとした時に、正しいスペルが分からず"~~tative"といった形で検索することができませんでした。これは、プログラムが最初に"R"、次に"e"、その次に"p"・・・といったように検索していくため、"Representatives"という単語を抜き出すために最初の文字から正しく入力する必要があるためです。また、この例では、"Representative"と検索しても一致する単語はありません。必ず複数形の"Representatives"と正確に表現しなければ、検索できなくなってしまいます。これはとても不便です。

そこで、曖昧な検索を可能とするために、ワイルドカードという表現があります。暗号のような感覚ですが、非常に有用です。

^ 行の先頭
$ 行の最後
. どのような文字でもよい1文字
\s 空白
\S 空白でない文字
* ゼロ文字または1文字以上の文字
*? ゼロ文字または1文字以上の文字(greedy matchingなし)
+ 1文字以上の文字
+? 1文字以上の文字(greedy matchingなし)
[aeiou] カッコ[ ]の中に含まれるいずれかの1文字
[^XYZ] カッコ[ ]の中に含まれないいずれかの1文字
[a-z0-9] aからz、0から9のいずれかの文字・数字
(詳しくは、https://www.py4e.com/lectures3/Pythonlearn-11-Regex-Handout.txtをご参照ください)

Greedy matchingとは何か?については後で説明します。

これらの機能はPythonに備わっている標準装備ではないので、ライブラリという別の機能セット(Regular Expression)をインポートする必要があります。別途インストールの必要ありません。命令文としてimportを用います。Regular Expressionを用いると、search()やfindall()など便利な機能も使えるようになります。

import re #...[1]
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
#   if line.startswith('From:'):
    if re.search('^From:', line): #...[2]
        print(line)

このプログラムでは、[1]でregular expressionのライブラリを読み込みます。そして、ファイルを読み込み、1行ずつfor文で処理していきます。そして、一番右端にある空白や\nを取り除きます。ここまでは前コースの復習ですね。次に、[2]で登場するsearch()は、regular expressionライブラリに備わっている検索機能です。コメントアウトしているようなstartswith()関数の代わりに使っています。

関数startswith()は行の先頭でしか使えないものです。一方で、re.search()は行にあるどの文字も検索可能です。そのため、From:から始まる行を抜き出すためには、regular expressionの文字^を使って^From:と表現してあげます。これで行の先頭にあるFrom:という文字列を指定できます。


2.ワイルドカードを用いて文字の検索をする

次に、以下のように少し特殊な文字列を持つテキストを考えてみましょう。

X-Sieve: CMU Sieve 2.3
X-DSPAM-Result: Innocent
X-DSPAM-Confidence: 0.8475
X-Content-Type-Message-Body: text/plain

このテキストの特徴として、各項目は行頭からX-(なんとか): というように、決まったフォーマットになっています。これをワイルドカードで拾うためには、

^X.*:

と指定します。^は行頭、.は何でもよい1文字、*は何文字でもよいワイルドカードを意味します。したがって、この謎の文字列は、

Xから始まり、何か1文字を持ち、その後は(ゼロでもよい)何字かあって、:で締めくくられている1綴りの単語

を意味します。暗号みたいですね。

さて、文章から数字だけを抜き出すプログラムを見ていきましょう。

import re
txt_msg = 'Hi I am Karaage I have 7 keys and 15 boxes raight now'
a = re.findall('[0-9]+', txt_msg) #...[1]
print(a)

文字列txt_msgの中から[1]で数値を抜き出しています。findall()では、検索したい文字列をワイルドカード可で指定し、検索対象とする文字列を後に持ってきます。ここで[0-9]+という表現は、0から9までのいずれかの1文字を含むと読みます。検索した結果は、リスト形式で出力されます。

['7', '15']

ワイルドカードは曖昧さを含む検索にて便利な機能ですが、一方でワイルドカードで検索することによってきちんと検索されないケースも出てきます。この不都合を解消するために、greedy matchingでない検索方法を取ることが有効です。
例として、次のプログラムを見てみましょう。

import re
data = 'From: koreha_sample_desuyo@kyoko-u.ac.jp Sat Jun 6 02:03:34 2015'
y = re.findall('^F.+:', data) #...[1]
print(y)

このプログラムの実行結果は以下のようになります。

['From: koreha_sample_desuyo@kyoko-u.ac.jp Sat Jun 6 02:03: ']

Oh, NOOO!!
From: だけを抜き出すことにはならず、何か後ろの文字まで抜き出されてしまいました。これは、[1]のワイルドカードの指定が行頭がFから始まり、何らかの1文字以上を含み、:で締めくくられる文字列となっているためです。プログラムは途中の:では止まらず、最後の:までを読み込んでしまっているわけです。これがgreedy(貪欲な)マッチングと呼ばれる理由で、一つ目の:では止まらずにプログラムが最後の:を見つけるまで走り続けてしまうことが原因です。

これを防ぐため、greedy matchingを行わない形でワイルドカードを指定する必要があります。具体的には、以下のプログラムの[1a]のように、+の代わりに+?を用いることで解決できます。

import re
data = 'From: koreha_sample_desuyo@kyoko-u.ac.jp Sat Jun 6 02:03:34 2015'
y = re.findall('^F.+?:', data) #...[1a]
print(y)


3.検索した内容から特定の文字列を抜き出す

さらに応用編として、ある特定の条件を満たす文字列を検索したうえで、その一部分だけを抜き出す方法を学んでいきましょう。

import re
data = 'From: koreha_sample_desuyo@kyoko-u.ac.jp Sat Jun 6 02:03:34 2015'
y = re.findall('^From: (\S+@\S+)', data) #...[1]
print(y)

[1]のfindall()で指定されている検索したい文字列のうち、カッコは意味を持ちません。したがって、カッコはひとまず無視して、行頭がFromから始まり、空白でない1文字以上を持ち、@があり、空白でない1文字以上を持つ文字列です。\S+の一続きで、空白でない1文字以上と読みます。
では、カッコは何を表すか?ですが、検索された文字列のうち、カッコで括られた文字列を抜き出す、という意味を持ちます。したがって、表示される文字列は

['koreha_sample_desuyo@kyoko-u.ac.jp']

だけとなります。

さらに、ホスト名だけを取り出したい場合、findall()でワイルドカードを指定してあげると、リスト形式で取り出した場合の複数行のコードから1行のコードに圧縮できます。ただし、このようなコードを使うかどうかは他人がコードを読んだ時に理解できるかどうか、自分が後でコードを見返してみたときに直感的に理解しやすいかどうかに気をつけましょう。

import re
data = 'From: koreha_sample_desuyo@kyoko-u.ac.jp Sat Jun 6 02:03:34 2015'
y = re.findall('@([^ ]*)', data) #...[1]
print(y)

[1]では、@から始まり、[^ ]で空白を含まない1文字、*で0文字以上の文字列、を検索しています。その上で、@以外の文字を抜き出すコードとなっています。

['kyoko-u.ac.jp']

第3コースからは学ぶべき事項が多くなってきているため、講義メモを1週ごとにページを割いて残していきます。それでは、次回のエントリもお楽しみに!

この記事が気に入ったらサポートをしてみませんか?