Pythonを使ってWebページにアクセスしてデータを抽出し、CSVやJSON形式として出力する方法を解説します。
WebからHTMLを取得する
PythonでWebへのアクセスをする時に最も手軽な方法は requests
を使う方法です。
requests
はサードパーティパッケージなので pip
を使ってインストールする必要があります。
$ pip install requests
requestsがインストールされて import requests
できるようになります。実際にPythonのインタラクティブシェル上でrequestsを使ってWebページを取得してみましょう。例としてPyconJP2015のチュートリアルの一覧ページを取得します。
>>> import requests
>>> res = requests.get('https://pycon.jp/2015/ja/schedule/tutorials/list/')
>>> res.status_code
200
>>> res.reason
'OK'
>>> res.text
〜省略〜
requests.get()
の第一引数に取得したいURLを指定します。
戻り値は requests.models.Response
のインスタンスで、リクエストを送った結果を保持しています。res.status_code は返されたHTTPのステータスコードです。200 は 'OK' で正常にページが返されたことを表しています。
HTTPのステータスコード のさらに詳しい内容についてはこちらを参照してください。
http://requests-docs-ja.readthedocs.org/en/latest/
https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89
res.reason にはステータスコードの理由(Reason)が入ります。res.text には実際の返されたデータが入っています。
requests のさらに詳しい内容についてはこちらを参照してください。
http://requests-docs-ja.readthedocs.org/en/latest/
HTMLを解析する
HTMLを解析する方法もいくつかありますが、その中でもよく使われて比較的簡単に使用できるものとして BeautifulSoup
があります。今回はBeautifulSoupを使ってHTMLを解析します。
まず BeautifulSoup
をインストールします。BeautifulSoupの最新は4.4.0 (2015/08/13現在) で beautifulsoup4
というパッケージ名でPyPIに登録されています。そのためインストールの際には beautifulsoup4
というパッケージ名で指定する必要があります。
$ pip install beautifulsoup4
インストールしたBeautifulSoupを使ってHTMLを解析してみましょう。
>>> import bs4
>>> soup = bs4.BeautifulSoup(u'<html><body><h1>TEST</h1></body></html>')
>>> tag = soup.find('h1')
>>> tag.text
u'TEST'
>>> tag.name
'h1'
bs4
がbeautifulsoup4のパッケージ名です。bs4.BeautifulSoup()
に解析したいHTMLのUnicode文字列を渡します。bs4.BeautifulSoup.find()
は指定したタグのDOM要素を探すメソッドです。例では 'h1'
を指定することでh1タグを取得しています。タグ名は .name
で、テキスト要素は .textでそれぞれ取得できます。
Webから取得したデータを標準出力に表示する
ここまでで Webへのアクセス方法とHTMLの解析方法を学びました。すでに簡単な構造のWebページであればスクレイピングできるでしょう。早速WebからHTMLを取得してデータを標準出力に表示するスクリプトを書いてみましょう。表示する内容はPyConJP2015のチュートリアルのタイトル一覧です。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import bs4
import requests
url = 'https://pycon.jp/2015/ja/schedule/tutorials/list/'
res = requests.get(url)
soup = bs4.BeautifulSoup(res.text)
for elm in soup.select('.presentation h3 a'):
print(elm.text)
実行すると、https://pycon.jp/2015/ja/schedule/tutorials/list/ にアクセスして取得したHTMLからチュートリアルのタイトルを解析して標準出力に出力しています。
select()
を使っていますが、これは CSSセレクタ 1 を用いてタグを検索するための関数です。
beautifulsoup4 のさらに詳しい内容についてはこちらを参照してください。
http://kondou.com/BS4/
データの永続化
Webからデータを取得して抽出するスクリプトを書けるようになりました。
このデータをファイルに残したいことがあります。そのままのデータでよければリダイレクトを用いてファイルに書き出すということもできますが、多くの場合は決まった形式に変換して出力したいということが多いでしょう。ここではよく使用される形式でデータをファイルに書き出します。
CSV形式
CSVはカンマで区切られた形式のファイルです。Excelなどのソフトウェアでも読み込みができるためとてもよく使われる形式です。PythonでCSV形式出力をするには csv
モジュールを使うのがよいでしょう。 csv
モジュールは標準モジュールなのでpipでインストールすることなく使用できます。
csv
モジュールは読み出し用のreaderと書き出し用のwriterがあります。今回はファイルに書き出したいのでwriterを使います。 csv.writer()
にファイルオブジェクトを渡すことで、CSV形式でのファイルの書き込みを行うためのオブジェクトを生成し、そのオブジェクトを使用して書き込みを行います。以下のコードをインタラクティブシェルとして実行してみてください。
>>> import csv
>>> fp = open('test.csv', 'w+t')
>>> writer = csv.writer(fp)
>>> writer.writerow(['aho1', 'aho2', 'aho3'])
>>> writer.writerow(['aho1', 'aho2', 'aho3'])
>>> writer.writerow(['aho1', 'aho2', 'aho3'])
>>> writer.writerow(['aho1', 'aho2', 'aho3'])
>>> fp.close()
csv モジュールのさらに詳しい内容についてはこちらを参照してください。
http://docs.python.jp/3.4/library/csv.html
スクレイピング!!
これらを用いて先ほど取得したタイトルとURLをCSV形式で保存してみましょう。
import csv
import bs4
import requests
url = 'https://pycon.jp/2015/ja/schedule/tutorials/list/'
res = requests.get(url)
soup = bs4.BeautifulSoup(res.text)
titles = [(elm.text, elm.get('href')) for elm in soup.select('.presentation h3 a')]
fp = open('test.csv', 'w+t')
writer = csv.writer(fp)
writer.writerows(titles)
fp.close()
test.csv
というファイルが作成されてcsvが出力されます。
スクレイピングしたデータを使う!!
先ほど出力した test.csv
を読み込んでデータを標準出力に出力してみましょう。
CSVファイルの読み込みもcsvモジュールで行います。
csv.readerにファイルオブジェクトを渡してcsvreaderを作成しましょう。
import csv
fp = open('test.csv', 'rt', encoding='utf8')
reader = csv.reader(fp)
for row in reader:
print('{}: {}'.format(row[0], row[1]))
fp.close()
御作法としてファイルオブジェクトはちゃんとクローズしてくださいね;)
JSON形式
JSON形式もよく使われる形式です。JSONとはJavaScript Object Notationの略で、その名の通り、JavaScriptのオブジェクトの形式が由来となっています。そのためJavaScriptと相性がとてもよく非常に簡単にオブジェクト化できるので、WebAPIなどはリクエストやレスポンスをJSON形式でやりとりするものが多いです。
PythonでJSON形式を扱うためには標準モジュールの json
モジュールを使用します。それでは使用例を見ていきましょう。
>>> import json
>>> json.dumps([1, 2, 3, 4])
'[1, 2, 3, 4]'
>>> json.loads('[1, 2, 3, 4]')
[1, 2, 3, 4]
>>> json.dumps({'aho': 1, 'ajo': 2})
'{"aho": 1, "aro": 2}'
>>> json.loads('{"aho": 1, "ajo": 2}')
{u'aho': 1, u'aro': 2}
json.dumps()
はPythonのオブジェクトをJSON形式の文字列に変換するための関数です。
json.loads()
はJSON形式の文字列をPythonのオブジェクトに変換するための関数です。
文字列 <-> オブジェクト の変換はこのようにして行うことができます。
ファイルにJSON形式の文字列を書き込んだり、ファイルに記述されているJSON形式の文字列からオブジェクトにするためには json.dump()
と json.load()
を使います。
json モジュールのさらに詳しい内容についてはこちらを参照してください。
http://docs.python.jp/3.4/library/json.html
スクレイピング!!
これらを用いてタイトルとURLをJSON形式で保存するスクリプトを書いてみましょう。
import json
import bs4
import requests
url = 'https://pycon.jp/2015/ja/schedule/tutorials/list/'
res = requests.get(url)
soup = bs4.BeautifulSoup(res.text)
titles = [[elm.text, elm.get('href')] for elm in soup.select('.presentation h3 a')]
data = json.dumps(titles, ensure_ascii=False, indent=2, sort_keys=True)
with open('test.json', 'w+b') as fp:
fp.write(data.encode('utf8'))
実行後に test.json
というファイルが作成されます。その中に採取したデータが出力されています。
スクレイピングしたデータを使う!!
では次は先ほどとは逆にこのデータを読み込んで標準出力に書き出してみましょう。
import json
with open('test.json', 'rt', encoding='utf8') as fp:
title_url = json.load(fp)
for title, url in title_url:
print('{}: {}'.format(title, url))
出力できましたか?
コマンドラインとしてちゃんとさせよう
ここまでいくつかのスクリプトを作成して実行してきました。その場限りで実行して、後はステてしまってもいいのであれば、このような実装でも問題ないでしょう。
しかし業務で作成したものや、他人に使ってもらうものはそういうわけにはいきません。
引数やオプションを用意して、コマンドラインに渡すことで、スクリプトの挙動を制御したくなります。
今まで作成したスクリプトを整理して一つのコマンドにします。コマンドの仕様は以下になります。
- コマンドの第一引数で出力するファイルを指定する。
- コマンドの-fオプションもしくは--formatオプションでフォマートを指定する。
- -fオプションもしくは--formatオプションで指定できるフォーマットはjsonとcsvのみ。
- -fオプションもしくは--formatオプションのデフォルトはjsonとする。
コマンドの引数を取得するためには sys.argv
を使います。 sys.argv[0]
にはPythonに渡されたスクリプト名が入っています。それ以降が実際に解析しなければならいオプションパラーメータや引数になります。sys.argv[1:]
とするとindexが1以降の要素がリストとして取得できます。このような書き方をスライスと言います。
オプションの解析は argparse
を使うことが推奨されます。optparse
というものもありますが、こちらは古いモジュールで廃止されることが予定されています。未来のある argparse
を使いましょう。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import csv
import json
import argparse
import bs4
import requests
TARGET_URL = 'https://pycon.jp/2015/ja/schedule/tutorials/list/'
def main(argv=sys.argv[1:]):
parser = argparse.ArgumentParser()
parser.add_argument('output', type=argparse.FileType('w'))
parser.add_argument('-f', '--format', default='json', choices=['json', 'csv'])
args = parser.parse_args(argv)
fmt = args.format
output = args.output
res = requests.get(TARGET_URL)
soup = bs4.BeautifulSoup(res.text)
titles = [(elm.text, elm.get('href')) for elm in soup.select('.presentation h3 a')]
if fmt == 'json':
data = json.dumps(titles, ensure_ascii=False, indent=2, sort_keys=True)
output.write(data)
elif fmt == 'csv':
writer = csv.writer(output)
writer.writerows(titles)
if __name__ == '__main__':
sys.exit(main())
解説
シェバング
#! /usr/bin/env python
これはスクリプト言語でよく用いられる書き方です。
シェル上から実行ファイルを指定せずスクリプトを実行した時に、どの実行ファイルをするかをシェルに伝えるための機構です。この場合は python
を用いて実行するということを宣言しています。シェル用の宣言のため、この宣言がなくても以下のようにすれは動作します。
$ python test.py
エンコード宣言
# -*- coding: utf-8 -*-
このファイルがどの文字コードで記載されているかということを宣言します。Pythonはこの宣言を読み込んで実行時に利用します。この宣言がなくても概ね動作しますが、ファイル内にマルチバイト文字がある場合、Python2では文字を解析できずに実行できないです。記載しておいたほうが無難でしょう。
import文
import sys
import csv
import json
import argparse
import bs4
import requests
利用するpackageをimportしています。順序は前後しても動作はしますが、標準モジュールは先に記載されることが多いです。
(これは私の個人的な好みですが、文字数の少ないパッケージから先に並べるのが好みです)
URLはグローバル変数に
TARGET_URL = 'https://pycon.jp/2015/ja/schedule/tutorials/list/'
今回の場合、URLは固定であって変化しないものでしたのでグローバル変数 (定数) にしました。
グローバルに定義された定数として扱われることを期待する変数は大文字のスネークケースを使うことが好まれます。
main()関数宣言
def main(argv=sys.argv[1:]):
Pythonは main
という名前の関数を特別扱いして実行することはありません。
しかし、他の言語がこのスタイルのため、エントリーポイントであることを表すために、 main()
という名前がよく使われます。
argv
は mai()
の第一引数で、そのデフォルト値が sys.argv[1:]
です。これもこうしなければならないわけではありませんが、このような宣言をするメリットがあります。
例えば、main()
を引数なしで呼び出した時には、 sys.argv[1:]
が argv
としてして使用されるため、引数を省略できます。 argparse.ArgumentParser().parse_args()
は第一引数に解析するコマンドラインパラメータのリストを渡すのですが、省略もできます。省略した場合は sys.argv[1:]
が使われます。では main()
には引数を渡さないようにして argparse.ArgumentParser().parse_args()
も第一引数を省略してやればいいと思うかもしれません。しかし、スクリプト内でコマンドラインパラメータのリストを作成してmain()関数をPythonのコードから呼び出したい時に対応できません。このケースはあまりないと思うかもしれませんが、例えば main()
関数に対するテストコードを記述したいときは、スクリプト内でコマンドラインパラメータのリストを作成してmain()関数を呼び出したいでしょう。
上記のコードでは引数の省略もできて、テストコードも記載しやすい形にするために、このような記述をしています。
オプション解析
parser = argparse.ArgumentParser()
parser.add_argument('output', type=argparse.FileType('w'))
parser.add_argument('-f', '--format', default='json', choices=['json', 'csv'])
args = parser.parse_args(argv)
fmt = args.format
output = args.output
argparse.ArgumentParser()
を用いてオプション解析をしています。 add_argument()
でオプションや引数を設定できます。 output
の type
には argparse.FileType('w')
を指定しています。これは output
で指定されたパスにあるファイルをオープンします。
-f
, --format
では default
を使ってデフォルト値('json')を設定し、choicesを使って指定可能な値を制限しています。
スクレイピング部分
res = requests.get(TARGET_URL)
soup = bs4.BeautifulSoup(res.text)
titles = [(elm.text, elm.get('href')) for elm in soup.select('.presentation h3 a')]
ここはこれまでと同じですね ;)
永続化部分
if fmt == 'json':
data = json.dumps(titles, ensure_ascii=False, indent=2, sort_keys=True)
output.write(data)
elif fmt == 'csv':
writer = csv.writer(output)
writer.writerows(titles)
fmt
は -f
で指定した値を先ほど代入しました。その値を使ってif文で分岐しています。
シリアライズしてファイルに書き出すところは、これまでとほとんど同じですね。
エントリーポイント
if __name__ == '__main__':
sys.exit(main())
これはPythonでスクリプトを作成する場合によく記述されるパターンです。*.pyファイルの利用方法は大きく分けて2種類あります。モジュールとしてimportする場合とスクリプトとして実行する場合です。モジュールとしてimportする場合は __name__
はそのモジュール名になっています。スクリプトとして実行する場合は __main__
という文字列になります。
__name__ == '__main__'
だったら sys.exit(main())
実行する形になるので、スクリプトとして実行された場合にはここのコードは実行され、モジュールとしてimportされた場合は実行されません。
実行しよう
$ python pyconjp.py -o test.json -f json
$ python pyconjp.py -o test.csv -f csv
最後に...
ここまでの内容で次の内容を学びました。
- Webスクレイピングをするための最低限の知識
- データの永続化手法
- Pythonスクリプトの良い書き方
Webスクレイピングのみならず、作業の自動化や、データの整形など、さまざまなことを応用できるはずです。あとはガンガン書いちゃいましょう。
なおサンプルコードはこちらに用意しました。
https://github.com/TakesxiSximada/happy-scraping
注釈
-
CSSセレクタとはDOM要素を指定するための記法の一つでCSSで使われています。 ↩