データの取得

データサイエンティストであるためには、データが不可欠です。実際のところデータサイエンティストが行う作業時間の大部分は、データの取得、整理、変換にあるといわれています。危機的な状況では、自分で入力する事もできますが、大抵の場合で有益な時間の使い方とはいえません。ここでは、Pythonにデータを読み込ませ、望ましい形に変換する方法を扱います。

stdinとstdout

Pythonスクリプトをコマンドラインから実行したのであれば、sys.stdinとsys,stdoutを通じてデータをプログラムに渡せます。例えば次のスクリプトはテキストファイルを読み込み正規表現と一致した行を出力します。

#egrep.py
import sys, re
#sys.srgvはプログラム引数のリスト
#sys.argv[0]は、プログラム名
#sys.argv[1]は、コマンドライン上に指定した正規表現
regex = sys.argv[1]
#スクリプトが処理する各行に対して
for line in sys.stdin:
# 正規表現に合致したなら、stdoutに出力する
if re.search(regex, line):
sys.stdout.write(line)

また、次のスクリプトは読み込んだテキストの行数を数え、出力します.

#line_count.py
import sys
count = 0
for line in sys.stdin:
count += 1

# sys.stdoutに出力される
pritnt(count)

このスクリプトを使い、数字を含む行が何行あるかを数えられます。

同様に、次のスクリプトは入力から単語の出現回数を数え、頻出するものを表示します。

#most_common_words.py
import sys
from collections import Counter
#第1引数として、出力する単語数を指定する
try:
num_words = int(sys.argv[1])
except:
print ("usage: most_common_words.py num_words")
sys.exit(1) #0以外のexitコードはエラーが発生したことを示す

counter = Counter(word.lower() #単語を小文字にする
for line in sys.stdin
for word in line.strip().split() #単語は空白で区切る
if word) #空白はスキップする

for word, count in counter.most_mommon(num_words):
sys.stdout.write(str(count))
sys.stdout.write("\t")
sys.stdout.write(word)
sys.stdout.write("\n")

ファイルの読み込み

プログラムから特定のファイルを直接書き込んで読み書きできます。Pythonではファイルの取扱は非常に簡単です。

テキストファイルの基礎

テキストファイルを使うにはopenを使ってfileオブジェクトを作成するのが最初のステップです。

#'r'はread-only(読み取り専門)の意味
fike_for_reading = open('reading_file.txt', 'r')
#'w'はwrite(書き込み)を表す(ファイルを壊す可能性がある)
file_for_writing = open ('writing_file.txt ', 'w')
#'a'はappend(追記)を表す(ファイルの末尾に追記する)
file_for_appending = open('appending_file.txt', 'a')
#ファイルを使い終わったら、クローズを忘れないように
file_for_writing.close()

ファイルのクローズは忘れがちであるので、withブロックをつかい、ブロックの最後で自動的にクローズされるようにすべきです。

with open(filename, 'r') as f:
data = function_that_gets_data_from(f)
#この時点ではfはすでにクローズされているので使うことはできない
process(date)

ファイル全体を読み込みたいのであれば、forを使って行ごとの読み込みを繰り返します。

starts_with_hash = 0
with open('input.txt', 'r') as f:
for line in file: #ファイル内の各行ごとに繰り返す
if re.match("^#", line): #正規表現を使って行頭の’#’の有無を調べる
starts_with_hash += 1 #行頭が'#'の行数を数える

この方法で読み込んだ行は改行文字で終端されているため、何か処理を加える前にstrip()で改行を取り除く必要があるかもしれません。
例えば1行に1つメールアドレスが記録されているファイルを処理して、ドメインのヒストグラムを作ることを考えてみましょう。ドメイン名を正確に抜き出すには微妙な点があります。最初の近似としてはメールアドレスの@から後ろを抜き出すのが良いでしょう。

def get_domain(email_address):
#@で分解して、後ろの部分を返す
return email_address.lower().split("@")[-1]
with open('email_address.txt', 'r') as f:
domain_counts = Counter(get_domain(line.strip())
for line in f
if "@" in line)

区切り文字を使ったファイル

メールアドレスのは言ったファイルは、1行に1つのアドレスだけが入っていることになっていました。普通のデータファイルには、同じ行に様々なデータも書かれています。こういったファイルは大抵、カンマ区切りやタブ区切りが使われます。各業にはいくつかのフィールドが含まれ、カンマがフィールドとフィールドの区切りをします。

カンマやタブや改行を含むフィールドが(不可逆的に)出てくると、混乱します。独自の構文解析は、誤りのもとです。代わりにPythonのcvsモジュール(また、pandasライブラリ)を使いましょう。技術的な理由からMicrosoftを避難してもかまわないのですが、csvファイルを扱う際には、rかwの後ろにaを置いて常にバイナリーモードでopenすべきです。

ファイルにヘッダがない(つまり行はすべてのデータであり、どの列のデータがどういう意味を持つのかを覚えてオカなければない)場合、すべての行をcvs.readerを使って繰り返し処理できます。各業は、適切に分割されます。

次のようにコードを書きます。

import csv
with open('tab_delimited_stock_prices.txt', 'rb') as f:
reader = csv.reader(f, delimiter='\t')
for row in reader:
data = row[0]
symbol = row[1]
closing_price = float(row[2])
process(data, symbol, closing_price)

(最初にreader.next()を実行しておくことで)ヘッダ行をスキップするか、csv.DictReaderを使ってヘッダ行各ふぃーるどをキーとするディクショナリとして読み込むことも出来ます。

with open('colon_delimited_stock_prices.txt', 'rb') as f:
reader = csv.DictReader(f, delimiter':')
for row in reader:
date = row["data"]
symbol = row["symbol"]
closing_price = float(row["closing_price"])
process(date, symbol, closing_price)

ヘッダがなくてもfieldnamesパラメータにへっだ情報を与えればDictReadを使えます。同様にcvs.writerを使って区切りデータをファイルに出力できます。

today_prices = {'AAPL' : 90.91, 'MSFT' : 41.68, 'FB' : 64.5}
with open('comma_delimited_stock_prices.txt', 'wb') as f:
writer = csv.writer(f, delimiter=',')
for stock, price in today_prices.items():
writer.writerow([stock, price])

csv.writerはフィールドの中にカンマを含むデータを含むデータも正しく扱います。自作のwriterでは、意図したとおりには動作しないかもしれません。例えば、次のデータを文字区切りのファイルに書き出したとしましょう。

results = [["test1", "success", "Monday"]
["test2", "success, kind of", "Tuesday"],
["test3", "failure, kind of", "Wednesday"],
["test3", "failure, utter", "Thursday"]]
#誤った処理方法
with open('bad_csv.txt', 'wb') as f:
for row in results:
f.write(",".join(map(str, row))) #おそらく必要以上のカンマ#区切り文字が書き込まれる
f.write("\n") #行末には改行も必要

この結果、作成されるcsvファイルは次のようになります。

test1, success, Monday
test2, success, kind of, Tuesday
test3, failure, kind of, Wednesday
test3, failure, utter, Thursday

このように誤った処理を加えると意味がわからなくなります。

Webスクレイピング

データを取得する方法の1つが、Webページ上のスクレイピングです。Webページを読み出すのには非常に簡単ですが、そこから意味のある構造化された情報を抜き出すのはそれほど簡単ではありません。

HTMLとその解析

WebページはHTMLで書かれており、(理想的には)要素と属性がメークアップされています。

すべてのWebページが意味的にマークアップされており、「idが”subject”である<p>要素を探して、その中身を返す」といったルールを使えば目的のデータが取り出せる、と言うのは理想的な世界の話です。実世界のHTMLは的確に表現されておらず、アノテーションなども付加されていないため、意味を解するには人間の手助けが必要です。

HTMLからデータを取り出すために、Beautiful Soupを使いましょう。このライブラリはWebページの様々な要素からツリー構造を作り、その中のデータへアクセスする簡単なインターフェイスを提供します。同時にPython組み込みの手段よりも優れたHTTPリクエスト生成機能であるrequestライブラリを使います。

Python組み込みのHTMLパーサは寛大ではないため、形式が完全に整っていないHTMLをうまく扱えません。そのため、別パーサを使いますが、インストールが必要です。

pip install html5lib

Beautiful Soapを使うには()にHTML文章を渡さなければなりません。この例ではrequests.getの結果を渡します。

from bs4 import BeautifulSoup
import requests
html = requests.get("http://www.twitter.com").text
soup = BeautifulSoup(html, 'html5lib')

ここまでくれば簡単なメソッドの呼び出しで様々なことが出来ます。
大抵の場合HTMLページのタグにそうとしたTagオブジェクトを操作します。
例えば、次のコードは、最初の<p>タグ(とその中身)を探し出します。

first_paragraph = soup.find('p')

textプロパティを通じて、タグの文字列部分を取り出せます。

first_paragraph_text = soup.p.text 
first_paragraph_words = soup.p.text.split()

tagオブジェクトを辞書として扱えば、属性にアクセスできます。

first_paragraph_id = soup.p['id']#idがなければKeyErrorとなる
first_paragraph_id2 = soup.p.get('id')#idがなければ、Noneが返る

複数のタグを一度に取り出せます。

all_paragraphs = soup.find_all('p') #または、単にsoup(`p`)
paragraphs_with_ids = [p for p in soup('p') if p.get(`id`)]

特定のクラスに属するタグが木手となる場面も頻繁に生じます。

important_pragraph = soup('p', {'class':'important'})
important_pragraph2 = soup('p', 'important')
important_pragraph3 = [p for p in soup('p')
if 'important' in p.get('class', [])]

これらを組み合わせて、より込み入ったロジックを作れます。例えば、<div>要素の中にある<span>要素を探すには、次のようにします。

#注意:複数の<div>の中に同じ<span>があればそれらをその都度返す その場合工夫が必要
spans_inside_divs = [span
for div in soup('div') #ページ上の<div>要素ごとに繰り返し
for span in div('span') #その中の<span>要素で繰り返し

これら少数の機能を使って、非常に様々なことが可能になります。もっと複雑な操作が必要になった(または単に興味がある)場合には、マニュアルを参照してみてください。

class=”important”とラベル付されていないデータであっても、重要なデータかもしれません。HTMLソースを注意深く調査し、抽出ロジックを樹分に吟味し、極端な場合でも正しくデータが取り出せるようにしなければなりません。

事例:本のスクレイピング

例えば、オライリーのWebサイトを調べたいとき(データに関する書籍は1ページ30点表示されるページが何ページか続いています)

捕まりたくないなら、Webサイトからデータを収集する前に、アクセスポリシーが設定されているか調べるべきです。例えば、次のページには今回のような試みを禁止するような記載はなかったのでOKです。

また、Webクローラーの動作について書かれているrobots.txtファイルもカクニンしておくべきでしょう。http://shop.oreilly.com/robots.txtの中で重要なのは2つです。

Crawl-delay: 30
Request-rate: 1/30

1行目は、リクエストごとに感覚を30秒開けるよう要求しています。2行目は30秒あたり1ページをリクエストするよう求めています。つまりこれらは同じことを別の表現で示しています(その他の行には、収集対象とすべきではないディレクトリが書かれていますが、ここでは使わないのでOKです)

データを取り出す方法を示すために、1ページをダウンロードしてBeautifuSoupに読み込ませてみましょう。

url = “http://shop.oreilly.com/category/browse-subjects/" + \ “data.do?sortby=publicationDate&page=”
soup = BeautifuSoup(requests.get(url).text, 'html5lib')

このページのHTMLソースをみれば、各書籍ごとに独立したthumbtextクラスの<td>テーブルで作られていることがわかります。

はじめにthumbtextタグ要素を探してみます。

tds = soup('td', 'thumbtext')
print(len(tds))#30

次にビデオを取り除いてみましょう。td要素には1つかそれ以上のpricelabelクラスのspan要素があり、そのテキストはEbook:,Video:またはPrint:であるとわかりました。ビデオはテキストがVideoであるpricelabelを1つだけ持つようです。これは次のコードです。

def is_video(td):
#pricelabelを1つだけもち、空白を取り除いたもじれつが’Video’であればびでおである
pricelabels = td('span', 'pricelabel')
return (len(pricelabels)) == 1 and pricelabels[0].text.strip().startswith("video"))
print (len([td for td in tds if not is_video(td)])

これでtd要素からデータを取り出す準備が整いました。著者のタイトルは<div class”thumbheader”>ブロック内の<a>タグにかかれているようです。

title = td.find("div", "thumbheader").a.text

著者はAuthorName<div>のテキストに書かれています。著者名の前にはByが全治されており(取り除きます)、著者が複数の場合にはカンマで区切られています。(分割して、不要な空白を取り除くことにします。)

author_name = td.find('div', 'AuthorName').text
authors = [x.strip() for x in re.sub("^By", "", author_name).split(",")]

ISBNはthumbheader<div>ないのリンクにあります。

isbn_link = td.find("div", "thumbheader").a.get("href")
#re.matchを使って正規表現のカッコ内にある部分を取り出す
isbn = re.match("/product/(.*)\.do, isbn_link).group(1)")

日付です

date = td.find("span", "directorudate").text.strip()

これらを1つの関数にまとめます。

def book_info(td):

title = td.find("div", "thumbheader").a.text
by_author = td.find('div', 'AuthorName').text
authors = [x.strip() for x in re.sub("^By", "", by_author).split(",")]
isbn_link = td.find("dev", "thumbheader").a.get("href")
isbn = re.match("/product/(.*)\.do", isbn_link).groups()[0]
date = td.find("span", "directorydate").text.strip()
return {
"title":title,
"authors" : authors,
"isbn" : isbn,
"date" : date
}

情報収集の準備が整いました。

base_url = "http://shop.oreilly.com/category/browse-subjects/" + \           "data.do?sortby=publicationDate&page="
books = []
NUM_PAGES = 31
for page_num in range(1, NUM_PAGES + 1):
print("souping page", "page_num",","len(books),"found so far" )
url = base_url + str(page_num)
soup = BeautifuSoup(requests.get(url).text, 'html5lib')
for td in soup('td', 'thumbtext'):
if not is_video(td):
books.append(book_info(td))
sleep(30)

APIを使う

多くのWebサイトやWebサービスでは、構造化されたデータを取り出すためのプログラムインターフェイス(API)を提供しています。APIを活用すれば、HTMLからのデータ抽出で遭遇する面倒の多くを回避できます。

JSON(XML)

HTTPはテキストをやり取りするためのプロトコルであるため、WebAPIを通してデータを要求するには、データを文字列形式にシリアライズしなければなりません。この目的のために、JavaScript Object Notation(JSON)が頻繁に使用されます。

Javasquriptおオブジェクトは。Pythonの辞書に似た形式を持っており、そのテキスト表現は解釈が容易です。

serialized = """{"title": "Data Science Book", 
"author":"Joel Grus",
"publicationYear": 2014,
"topics": ["data", "science", "data science"]}
"""
#JSONからPython辞書を作る
deserialized = json.loads(serialized)
if "data science" in deserialized["topics"]:
print(deserialized)

API提供者が意地悪しているのかもしれませんが、XMLレスポンスしか提供してないAPIもあります。

認証の必要がないAPIを使う

最近のAPIの多くは、最初に使用者の認証をします。この方法に異論があるわけではないのですが、認証のためのコードを追加する必要があるため、ここで説明するべきことに純粋に集中できなくなってしまうので、そこで、認証なしのGithubのAPIを使ってみます。

import requests, json
endpoint = "https://api.github.com/users/joelgrus/repos"
repos = json.loads(requests.get(endpoint).text)

この時点でreposはPython辞書のリストになります。この辞書はそれぞれの著者のGitHubアカウントにあるパブリックレポジトリを表します。このデータを使って著者がレポジトリを作るのは何月が多いか何曜日が多いのかがわかります。問題はレスポンスとして返されるはずのデータの形式が(Unicode)文字列である点です。Pythonは優れた日付パーサがないので何かインストールする必要があります。

pip install python-dateutil

使うのはおそらくdateutil.parser.parse関数だけです。

from dateutil.parser import parse
dates = [parse(repo["created_at"]) for repo in repos]
month_counts = Counter(date.month for date in dates)
weekday_counts = Counter(date.weekday for date in dates)

同様にして、最後の5つのリポジトリについて、プログラミング言語の種類を取り出します。

last_5_repositories = sorted(repos, 
key=lambda r : r["created_at"],
reverse=True)[:5]
last_5_languages = [repo["language"] for repo in last_5_repositories]

普通はこのリクエストを出して、結果を自分でパースすると言った低レベルのAPIは使いたくないですが、Pythonを使う利点の1つがこれから使おうとしているAPIをアクセスするためのライブラリが大抵の場合はすでに開発されている点です。ライブラリがよく出来ているなら、APIのアクセス人生じる扱いにくい問題の多くを回避してくれると思います。そうでない場合や今使われていないバージョンにしか対応してない場合は多くのエラーを起こすので、自分用にAPIアクセスライブラリを作らなければならない(または誰かが作ったライブラリをデバッグしないといけない)場合に備え、中身の知識を知っておくことは必要です。

必要なAPIの探索

とあるサイトのデータが必要である場合、サイトの開発者向けページを見ましょう。そこにPython 〇〇 APIを探しましょう。

TwitterAPI

Twitterはデータの宝庫です。最新のニュースに対する反応を調べたり、特定の話題に対して情報が得られます。APIはTwythonライブラリを使います。他にもたくさんAPIがあります。

認証の取得

APIを使うにはTwitterの認証を取得する必要があります。詳しくはこちら。

最初にSearch APIを使います。

twitter = Twython(CONSUMER_KEY, COMSUMER_SECRET)
for status in twitter.search(q = '"data science"')["statuses"]:
user = status["user"]["screen_name"].encode('utf-8')
text = status["text"].encode('utf-8')
print (user, ":", text)
print()

するとこのようにツイートが得られます

APIは最近のツイートから一部の結果を返します。データサイエンスを行うには大量のデータが必要なのでStreaming APIを使います。

Twythonを通してStreamingAPIを使うには、TwythonStreamを継承するクラスで、on_successメソッドをオーバライドします。

from twython import TwythonStreamer
#データを大域変数に格納するのは稚拙な手法ではあるが、サンプルコードを単純にできる
tweets = []
class MyStreamer(TwythonStreamer):
#streamとやり取りを行う方法を定義するTwythonStreamerのサブクラス

def on_success(self, data):
#Twitterがデータを送ってきたらどうするか。ここでdataは投稿を表すPythonのライブラリとして渡される

#えいごのTweetのみを対称とする
if data['lang'] == 'en':
tweets.append(data)
print("received tweet #" , len(tweets))

#十分なTweetが得られたら終了
if len(tweets) >= 1000:
self.disconnect()

def on_error(self, status_code, data):
print(status_code, data)
self.disconnect()

MystreamはTwitterストリームに接続して、データが送られるのを待ちますデータを受け取るとon_successメソッドが呼ばれ、英語のTweetであれがtweetsリストに追加されます。1000ツイート収集できたらStreameを切断して実行を終了します。これを行うには初期化コードが必要です。

stream = MyStreamer(COUSUMER_KEY, CONSUMER_SECRET,ACCESS_TOKEN, ACCESS_TOKEN_SERCRET)
#公開されているツイート(statuses)からキーワード'data'を持つものを収集する
stream.statuses.filter(track='data')

このコードは1000集めるかエラーが出るまで動き続けます。そこからあとにツイートの分析作業が始まります。と問えば最もよく使われているハッシュタグを調べます。

from collections import Counter
top_hashtabs = Counter(hashtag['text'].lower()
for tweet in tweets
for hashtag in tweet["entities"]["hashtags"])
print(top_hashtags.most_common(5))

それぞれの対とには多くのでーたが含まれています。自分自身でいろいろ試してみましょう。

実行結果はこのようになりました。(※17/10/2017)

[('data', 80), ('ai', 60), ('podcast', 47), ('soundcloud', 46), ('np', 46)]

補足

Scrapyはリンクされている先のページをたどるなどさらに複雑なWebスクレイパーです

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.