投稿者「python-tksssk」のアーカイブ

非同期IOをつかったHTTPリクエストの処理 aiohttp

非同期IOを使うことでシングルプロセス、シングルスレッドでも効率的に複数のHTTPリクエストの処理を行う。

サーバー側の準備

はじめに動作確認用に複数のリクエストを処理できるHTTPサーバプログラムを用意する。

このサーバープログラム自体も非同期IOで記述することで複数のリクエストを処理するサーバーを記述できるのだが、サーバー側とクライアント側で非同期IOの説明すると、今回の論点がぼやけてくるので、ひとまずリクエストを受け付けると3秒待ってからレスポンスを返すマルチスレッドなサーバーを用意した。このプログラムの説明は本題ではないので省略。

これを起動して、別の端末からcurlで同時に3リクエストを送ってみると、3秒後に3つのレスポンスが返る。

$ python wsgi-server.py
$ seq 1 3 | xargs -P 3 -I{} curl "localhost:8000/{}"
Hello World/1
Hello World/2
Hello World/3

非同期IOによるHTTPリクエスト

サードパーティライブラリのaiohttpを使用すると、非同期なHTTPClientをかんたんに記述できる。

import aiohttp
import asyncio
import time


async def get_request(session, no):
    response = await session.get(f'http://localhost:8000/{no}')
    content = await response.text()
    print(f"{time.strftime('%X')} - {response.status} {content}", end='')


async def main():
    tasks = []
    print(f"{time.strftime('%X')} - Start")
    async with aiohttp.ClientSession() as session:
        for n in range(3):
            tasks.append(get_request(session, n+1))
        await asyncio.gather(*tasks)
    print(f"{time.strftime('%X')} - Finish")

asyncio.run(main())

実行結果

18:03:56 - Start
18:03:59 - 200 Hello World/3
18:03:59 - 200 Hello World/2
18:03:59 - 200 Hello World/1
18:03:59 - Finish

asyncio 非同期IOの基本

Python3.7以降から使用可能なasyncioモジュールを使用することで非同期処理IOが実装できる。

しくみ

非同期IO処理とは、簡単に言えば「あるIO処理の終了を待たずに他の処理をする」こと。今までであれば時間のかかる処理を呼び出せば、その処理が終了するまで処理は止まって(ブロックされて)待たなければならなかった。これが同期処理。非同期処理は処理の終了を待たずに、その間に他の処理をできる。(ノンブロックキング)

threadingやmultiprocessでも似たように、処理の終了を待たずに他の処理をすることができるが、asyncioによる非同期IO処理はそれよりももっと軽量に動作し、実際には並列して処理が動くことはなく1スレッドで動いている処理は1つだけになる。

「同時に処理が動かない」のに「処理の終了を待たずに他の処理ができる」のは奇妙に思うかもしれない。IO処理というのはCPUを使わず処理が終わるのを待っている時間がある。例えばファイルの読み込みをしている間は、CPUを使わずにファイルのデータが読み込まれるのを待っている。この時間はCPU的には十分に長い時間なのでこの時間に他のことが処理できる。

非同期IO処理は「空き時間にこの処理をして!終わったら教えて!」という細かい処理をどんどん登録して、「空いた!」「じゃあこれやって!」という小さな処理単位をイベントループというループの中でどんどん処理する。処理自体は同時に1つしかこなせないが、空き時間を有効活用していることになる。ちょうど料理中に電子レンジをONにして、チンと完了するまでに他の作業をするのに似た感じ。

注意点

処理単位のなかで、長く時間のかかるブロッキング処理をしてしまうと、従来の同期処理と同じことになってしまうため避けなければならない。

例えばtime.sleepはスリープする処理だが、これは同期処理のsleepであり、非同期IOにはasyncio.sleepという非同期用のsleepがある。間違って非同期処理の中で、time.sleepを使うと非同期処理は他の処理をプログラムはそこでブロックして他のことを処理できなくなる。

このため、今まで同期処理でやっていたことと違うコードを書くことになるので注意する必要がある。

サンプルコード

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(f"{time.strftime('%X')} - {what}")

async def main():
    print(f"{time.strftime('%X')} - start")
    c1 = say_after(1, 'hello')
    c2 = say_after(2, 'world')
    await asyncio.gather(c1, c2) #2つの処理の終了を待つ
    print(f"{time.strftime('%X')} - finish")

# 非同期IOのエントリポイント
asyncio.run(main())

実行結果

16:10:28 - start
16:10:29 - hello
16:10:30 - world
16:10:30 - finish

1秒後にhelloと出力する処理、2秒後にworldと出力する処理を実行する。say_after関数は完了を待たずにすぐに返ってくる。asyncが設定された関数はそのような処理になる。asyncが設定された関数の処理を待つ場合にはawaitを使う。待つと書いたが実際にはこの間に他のasync処理を実行して、終わった後に戻ってくるような動きをする。

この結果、2つのsay_afterの処理はそれぞれ1秒、2秒まってから処理が行われるので全体としては2秒ですべてが終了する。

awaitが使えるのはasync関数の中だけになる。これが重要でasyncで非同期処理を使いたいと思ったときにはそこがasyncでなければならない。それを呼び出す関数もasyncでなければならない。どんどん遡ったトップレベルのエントリポイントで、asyncio.runなどから起動されていなければならない。

await.sleepをtime.sleepにすると、sleepでブロックされてその他の処理ができなくなってしまう。

import asyncio
import time

async def say_after(delay, what):
    # await asyncio.sleep(delay)
    time.sleep(delay)  # 非同期ioではないsleep
    print(f"{time.strftime('%X')} - {what}")

async def main():
    print(f"{time.strftime('%X')} - start")
    c1 = say_after(1, 'hello')    #time.sleepがブロックするのでc1の取得に1秒かかる
    c2 = say_after(2, 'world')
    await asyncio.gather(c1, c2) #2つの処理の終了を待つ
    print(f"{time.strftime('%X')} - finish")

# 非同期IOのエントリポイント
asyncio.run(main())

実行結果

11:05:37 - start
11:05:38 - hello
11:05:40 - world
11:05:40 - finish

実行結果からわかるように、startからhelloまで1秒かかり、helloからworldに2秒かかっている。

フォーマット済み文字列リテラル(f-string)

3.6からフォーマット済み文字列リテラル(f-string)が使用できるようになりました。

文字列内で変数を参照する書き方です。フォーマット指定に関してはformat関数とほぼ同じです。

>>> a = 10
>>> print(f'aの値は{a}です')
aの値は10です

>>> print(f'aの値は{a:03d}です')
aの値は010です

>>> print(f'aの値は{a:.2f}です')
aの値は10.00です

日付などのフォーマットも利用可能

import datetime
>>> now = datetime.datetime.now()
>>> print(f'nowの値は{now:%Y/%m/%d %H:%M:%S}です')
nowの値は2021/02/10 12:12:43です

参考: フォーマット済み文字列リテラル

現在時刻 ⇔ ( エポック秒 / UNIX時間 ) ⇔ datetime

現在時刻を 日付時刻型(datetime)で取得

>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2020, 1, 1, 10, 25, 37, 629103)

現在時刻を エポック秒 / UNIX時間 で取得

>>> import time
>>> time.time()
1577804400.1669202
>>> int(time.time())  # 秒で切り捨てたい場合は整数化
1577804400
>>> int(time.time() * 1000)  # ミリ秒で欲しいときは1000倍
15778044001669

エポック秒 / UNIX時間 を 日付時刻型(datetime)に変換

>>> import datetime
>>> sec = 1577804400
>>> datetime.datetime.fromtimestamp(sec)
datetime.datetime(2020, 1, 1, 0, 0)

特定のdatetime からエポック秒 / UNIX時間 に変換

>>> import datetime
>>> impot time
>>> d = datetime(2020, 1, 10, 10, 20, 30)
>>> int(time.mktime(d.timetuple()))
1578619230

ミリ秒部分も欲しい場合

>>> import datetime
>>> d = datetime.datetime(2020, 1, 10, 10, 20, 30, 123)
datetime.datetime(2020, 1, 10, 10, 20, 30, 123)
>>> int(time.mktime(d.timetuple())) + d.microsecond/1000 
1578619230.123

timeitで実行時間計測

「この関数の実行時間を計測して、もっと高速化しよう」といった時に実行時間計測を便利に行うモジュール。

import timeit

# 計測対象の関数
def number_join_by_forloop():
    s = "-"
    for n in range(10000):
        s += str(n)
    return s

# 1000回実行を繰り返した秒数
t1 = timeit.timeit('number_join_by_forloop()', number=1000, globals=globals())
print("for loop: %.3f sec" % t1)

timeit関数での実行は、関数を実行するのとは別の名前空間で実行するため、そのままでは計測対象の関数を参照できない。globals引数で実行時のグローバル領域をコピーして指定すると呼び出し側で定義した関数が参照できる(globals引数はPython3.5から)。

呼び出し側の名前空間を使用しない、単純なPythonコードであればglobals引数は不要。

Python3.5以前のglobals引数が使えない環境では、以下のように自身のモジュールをインポートすることで参照できる。

t2 = timeit.timeit('from __main__ import number_join_by_forloop; number_join_by_forloop()', number=1000)
print("map join: %.3f sec" % t2)

ワイルドカードインポート(import *)は推奨されない

from hoge import *とすることで、hogeモジュールのすべてをインポートできますが、これは雑なインポートの指定方法で意図しない変数、関数までこのモジュールの名前空間にインポートしてしまうので推奨されない。

pythonの公式のスタイルガイドであるPEP8でも非推奨となっている。

http://pep8-ja.readthedocs.io/ja/latest/#import
ワイルドカードを使った import (from import *) は避けるべきです。なぜなら、どの名前が名前空間に存在しているかをわかりにくくし、コードの読み手や多くのツールを混乱させるからです。

ワイルドカードインポートによって混乱する例

あるモジュールmain.pyがfirst.pyに依存しfirst.pyはsecond.pyに依存するとする。

main --> first --> second

first.pyでは、second.pyのすべてをワイルドカードインポートした場合、secondモジュールのすべてのグローバル変数、関数がfirst.pyの名前空間に入る。

# second.py
var_x = 1
def func1():
    pass
# first.py
from second import *
var_y = 2
def func2():
    pass

first.pyからはsecond.pyのfunc1関数を使いたいだけだったが、var_xの変数もインポートされる。

同様にmain.pyはfirst.pyをワイルドカードインポートすると、first.pyの全変数、関数がインポートされる。これにはfirst.pyがインポートしていたsecond.pyの内容までもインポートされる。

# main.py
from first import *
var_z = 3
def func3():
    pass

print(dir())

dir関数で定義された名前を確認すると、func1, func2, func3, var_x, var_y, var_zすべてが参照可能になっている。

[ ...中略..., 'func1', 'func2', 'func3', 'var_x', 'var_y', 'var_z']

意図しないモジュールからのインポートも行われるため定義位置を把握するのがとても難しくなる。
(main.pyでvar_xが参照できるのは、first.pyを参照し、second.pyのインポートからたどることでやっとvar_xの存在がわかる)

これはシンプルな例だが、カジュアルにワイルドカードインポートを使うとこれが肥大化しソースコードの可読性が著しく悪くなる。

second.pyに関数を一つ追加すると、main.pyでも自動的に参照できるようになる点も危険。

インポートする順番によって意図せず上書きする可能性がある

func1という関数が定義されたhogeモジュール。

# hoge.py
def func1():
    print("func1 called")

main.pyでは、円周率πを使用したかったために、mathモジュールのpi定数をインポートしていた。
ここから、hogeモジュールの関数を使いたいために、hogeモジュールをワイルドカードインポートをした。

# main.py
from math import pi
from hoge import *

func1()
print(pi)

main.pyスクリプトの実行結果は、以下のようになり円周率πの値が出力される。

func1 called
3.141592653589793

この状態で、hoge.pyで何気なくpiというグローバル変数を定義すると、main.pyがインポートしたpi変数を上書きしてしまうことになる。

# hoge.py
def func1():
    print("func1 called")
pi = 12345
func1 called
12345

hoge.pyで記述した内容は、piというグローバル変数を作っただけであり、mathモジュールのpi定数を上書きしたわけではない。
しかしmain.pyではワイルドカードインポートしているため、main.pyの名前空間ではpi変数は、hogeモジュールのpi変数を参照することになる。

当初main.pyを記述したときには、hogeモジュールを確認しpi変数がなく上書きされなかったのかもしれない。だがあとからhogeモジュールだけを更新するだけでmainモジュールにも影響を与えてしまっている。

意図しないインポートをしないためには、main.pyからは from hoge import func1 という形で必要なものだけインポートするのがよい。

BeautifulSoup4のチートシート(セレクターなど)

BeautiflSoup4でスクレイピングして要素を抽出するときに、よく使うセレクタをチートシート的にまとめておく。

BeautifuSoup4の使い方

スクレイピングする時にBeautifulSoup4を使うことは多いと思い。よく使うAPIやセレクターの記述方法をまとめます。
ちなみに、よく忘れてしまって「どうするんだっけ?」となるんですが、BeautifulSoup4ではxpathを使ったセレクタは存在しない。urlをわたしてHTTPリクエストを投げてくれるような機能はない。

インストール

beautifulsoup4 もしくは別名の bs4でpipからインストールする。
pip install BeautifulSoupとすると古いBeautifulSoup3になるので注意。

$ pip install beautifulsoup4
or 
$ pip install bs4

BeautifuSoupオブジェクト生成のチートシート

説明 コード例
soupオブジェクト生成(HTML文字列) BeautifulSoup('html文字列', 'html.parser')
soupオブジェクト生成(ファイル) BeautifulSoup(file_handle, 'html.parser')

第1引数はHTML文字列かファイルハンドル。
第2引数はParserライブラリを指定する。よく使用するのは html.parserlxml

html.parser はPython標準ライブラリなのでインストール不要ですぐ使えるが、速度は遅い。
lxml は高速だが別途インストールの必要なC依存ライブラリ。実行環境に依存する。

from bs4 import BeautifulSoup
# HTML文字列コンテンツ引数に生成
soup = BeautifulSoup('<html><body>hoge</body></html>', 'html.parser')

# ファイルハンドルを引数に生成
with open('index.html') as html_file:
    soup = BeautifulSoup(html_file, 'html.parser')

# URLからHTTPリクエストを投げて取得するようなAPIは無い
# requestsなどの別モジュールを使って取得する
import requests
res = requests.get('http://b.hatena.ne.jp/hotentry')
soup = BeautifulSoup(res.content, 'html.parser')

BeautifulSoup APIチートシート

説明 コード例
子要素 soup.head
タグ全検索 soup.find_all('li')
1件検索 soup.find('li')
属性検索 soup.find('li', href="html://www.google.com/")
class検索 soup.find('a', class_="first")
属性取得 first_link_element['href']
テキスト要素 first_link_element.string
親要素 first_link_element.parent
from bs4 import BeautifulSoup

soup = BeautifulSoup('''
<html>
    <head><title>example</title></head>
    <body>
        <ul>
            <li><a href="http://example.com/" class="first">example.com</a></li>
            <li><a href="http://www.google.com/" data="123">google.com</a></li>
        </ul>
    </body>
</html>''', 'html.parser')

# 子要素をたぐる
print(soup.head)        # <head><title>example</title></head>
# headの下のtitle
print(soup.head.title)  # <title>example</title>

# find_allは全検索してリストで返す
li_elements = soup.find_all("li")
print(li_elements)  # [<li><a class="first" href="http://example.com/">example.com</a></li>,
                    #  <li><a href="http://www.google.com/">google.com</a></li>]
# 見つからない場合は空のリスト
nothing_images = soup.find_all('img')
print(nothing_images)   # []

# findは検索して最初に見つかった1つ目
first_li_element = soup.find('li')
print(first_li_element) # <li><a class="first" href="http://example.com/">example.com</a></li>

# 見つからない場合はNone
nothing_image_element = soup.find('img')
print(nothing_image_element)    # None

#属性も検索条件につける
anchors_to_google = soup.find_all("a", href="http://www.google.com/")
print(anchors_to_google)    # [<a href="http://www.google.com/">google.com</a>]

# classは予約後なのでアンダースコア付加
first_link_element = soup.find("a", class_="first")
print(first_link_element)   # <a class="first" href="http://example.com/">example.com</a>

# 属性取得
print(first_link_element['href'])   # http://example.com/
# テキスト要素
print(first_link_element.string)    # example.com

# 親要素
parent = first_link_element.parent
print(parent)   # <li><a class="first" href="http://example.com/">example.com</a></li>

BeautifulSoup cssセレクタ チートシート

説明 コード例
タグ検索 soup.select('li')
1件検索 soup.select_one('li')
属性検索 soup.select('a[href="http://www.google.com"]')
属性存在 soup.select('a[data])
class検索 soup.select('a.first')
# タグ全検索してリストで返す
li_elements = soup.select("li")
print(li_elements)  # [<li><a class="first" href="http://example.com/">example.com</a></li>,
                    #  <li><a href="http://www.google.com/">google.com</a></li>]

# select_oneは検索して最初に見つかった1つ目
first_li_element = soup.select_one('li')
print(first_li_element) # <li><a class="first" href="http://example.com/">example.com</a></li>
# 見つからない場合はNone
nothing_image_element = soup.select_one('img')
print(nothing_image_element)    # None

#属性検索
anchors_to_google = soup.select('a[href="http://www.google.com/"]')
print(anchors_to_google)    # [<a href="http://www.google.com/">google.com</a>]
#data属性存在
print(soup.select('a[data]'))  # [<a data="123" href="http://www.google.com/">google.com</a>]

# class指定
first_link_element = soup.select_one("a.first")
print(first_link_element)   # <a class="first" href="http://example.com/">example.com</a>

requestsとBeautifulSoup4でシンプルなスクレイピング

はてなブックマークのホットエントリーから記事URL,タイトルを取得する例。

import requests
from bs4 import BeautifulSoup

res = requests.get('http://b.hatena.ne.jp/hotentry')
soup = BeautifulSoup(res.content, 'html.parser')

for a in soup.select('a.entry-link'):
    print(a['href'], a.text)

BeautifuSoupはインスタンス生成時にParserを指定できる。上記の例はhtml.parserを指定してあり、これはPython標準ライブラリで提供されるため、手軽に利用できるが速度的には遅い。高速なParserとしてはlxmlなどを別途インストールして指定することができる。

soup.selectではCSSセレクタを使用して走査している。

Scrapyとの比較

Scrapyでの例と比べると、単純な処理であればrequestsとBeatifulSoup4で実装したほうが簡単に書ける。

Scrapyはクローリングやスクレイピングのフレームワークで、多数のページを連鎖的にクローリングする手法などが組み込まれていて、複雑なスクレイピングが簡単に実装できる。

requestsとBeatifulSoup4で作成する際には、フレームワークの決まりごとがないぶんシンプルに記述できるが、上記のような多ページを取得することなどをやろうとすると複雑になってくる。

スクレイピングの注意事項

スクレイピングはブラウザと同じようにHTTPリクエストをサーバーに送り、データを取得するプログラムだが、プログラムの記述方法次第では、短時間に大量のリクエストをサーバーに送りつけ攻撃のようになってしまう事がある。

スクレイピングを実装する作法としては、繰り返しリクエストを送る間隔にsleep処理を入れて(最低でも1秒以上)短時間に大量のリクエストを送らないように注意する必要がある。

scrapyでシュッとスクレイピングする

Scrapyはスクレイピング用のフレームワークで、scrapyプロジェクトを作って大規模なクローリング・スクレイピングを作ることができるが、ちょこっと使い捨てなスクレイピングをするだけのコードも簡単に作ることができる。

ここでは、scrapyのプロジェクトを作らずスクレイピングのコードを作成する一連の流れを紹介する。

目的

scrapyのexampleページを対象とする。
https://doc.scrapy.org/en/latest/_static/selectors-sample1.html
上記URLがレスポンスするHTMLは以下のようになる。

<html>
 <head>
  <base href='http://example.com/' />
  <title>Example website</title>
 </head>
 <body>
  <div id='images'>
   <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
   <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
   <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
   <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
   <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
  </div>
 </body>
</html>

上記ページのaタグ内テキストの一覧を取得する。

Name: My image 1 
Name: My image 2 
Name: My image 3 
Name: My image 4 
Name: My image 5 

スクレイピングの動作確認

いきなりコードを書いてから動作確認してもよいが、スクレイピングのセレクターが正しいかどうか確認してみるには対話コンソールで確認するのが楽。まずは目的のデータがどのようにスクレイピングすれば取得できるかを手作業で確認する。

scrapy shellを起動して確認。

$ scrapy shell

>>> fetch('https://doc.scrapy.org/en/latest/_static/selectors-sample1.html')
2017-12-10 11:30:37 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://doc.scrapy.org/en/latest/_static/selectors-sample1.html> (referer: None)
>>> response.css('#images a::text').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']

fetchコマンドでURLを渡して、ページの情報を取得する。取得結果はresponseという変数に自動的に保存される。
resposen.cssで、CSSセレクタを使ってaタグ内のテキストを取得する。結果、目的のデータが取得できた。

scrapy shellでの作業はこれから書くコードの動きを確認するためのもので、コードの作成には関係しない。「CSSセレクタをこのように記述すれば目的のデータが取れるんだな」ということを試行錯誤しながら試すことができる。

セレクタの記述については、xpathやcssセレクタが使用可能。使い方のチートシートはこちらの記事を参照。
scrapyでよく使うxpath,cssのセレクタ

Spiderの作成

どのようにスクレイピングすればよいかは、scrapy shellで試行錯誤することで確認できた。あとはこれを実行するコードを書けば良い。ScrapyではWebからページをクローリングして、スクレイピングする部分はSpiderというプログラムで記述する。
scrapyのツールでSpiderの雛形が作成できる。

scrapy genspiderコマンドでSpiderの雛形を作成する。Spiderの名前とドメインを指定する。

$ scrapy genspider first_scrapy doc.scrapy.org

生成された雛形のSpiderファイル「first_scrapy.py」を編集して開始URLとレスポンスの処理を修正する。

# -*- coding: utf-8 -*-
#first_scrapy.py

import scrapy

class FirstScrapySpider(scrapy.Spider):
    name = 'first_scrapy'
    allowed_domains = ['doc.scrapy.org']
    start_urls = ['https://doc.scrapy.org/en/latest/_static/selectors-sample1.html']

    def parse(self, response):
        for name in response.css('#images a::text').extract():
            print(name)

Spiderの実行

作成したSpiderを実行するには、scrapy runspiderコマンドを実行する。

$ scrapy runspider --loglevel=WARN first_scrapy.py
Name: My image 1
Name: My image 2
Name: My image 3
Name: My image 4
Name: My image 5

コードで実行しているアンカータグの中身が出力されていることが分かる。--loglevelを指定しないとデバッグの出力が得られる。

データの返却

本来、Spiderはクローリング、スクレイピングした結果をデータとして返却する役割を担う。

先ほどのスクリプトはスクレイピングした結果をそのまま出力することで処理を終えてしまっている。簡単な処理ならばそれで十分だが「データの取得」と「取得したデータを活用する」部分は分けたほうがよいこともある。
Spiderはデータを取得して返すことを目的とした実装にできる。その場合はdict型でデータをyieldで逐次返却するようにする。

スクレイピングの処理を修正し、aタグを走査してhref属性とtext要素を取得するように修正。

# -*- coding: utf-8 -*-
#first_scrapy.py

import scrapy

class FirstScrapySpider(scrapy.Spider):
    name = 'first_scrapy'
    allowed_domains = ['doc.scrapy.org']
    start_urls = ['https://doc.scrapy.org/en/latest/_static/selectors-sample1.html']

    def parse(self, response):
        for a in response.css('#images a'):    #aタグの走査
            yield {
                "href": a.css('::attr(href)').extract_first(),
                "name": a.css('::text').extract_first(),
            }

runspiderを実行する際に、出力先としてhoge.jsonというファイルを指定する。

$ scrapy runspider --loglevel=WARN -o hoge.json first_scrapy.py

hoge.jsonの中身はこのようになる。

[
{"href": "image1.html", "name": "Name: My image 1 "},
{"href": "image2.html", "name": "Name: My image 2 "},
{"href": "image3.html", "name": "Name: My image 3 "},
{"href": "image4.html", "name": "Name: My image 4 "},
{"href": "image5.html", "name": "Name: My image 5 "}
]

jsonファイルを出力先に指定したことで、自動的にjson形式で出力されている。明示的に出力形式を指定する場合は、-t でフォーマットを指定する。(csv, json, jsonlines, xmlなどが指定可能)

また出力先の指定に-を指定すると標準出力に出力できる。

$ scrapy runspider --loglevel=WARN -t csv -o - first_scrapy.py
href,name
image1.html,Name: My image 1
image2.html,Name: My image 2
image3.html,Name: My image 3
image4.html,Name: My image 4
image5.html,Name: My image 5

スクレイピングの注意事項

スクレイピングはブラウザと同じようにHTTPリクエストをサーバーに送り、データを取得するプログラムだが、プログラムの記述方法次第では、短時間に大量のリクエストをサーバーに送りつけ攻撃のようになってしまう事がある。

スクレイピングを実装する作法としては、繰り返しリクエストを送る間隔にsleep処理を入れて(最低でも1秒以上)短時間に大量のリクエストを送らないように注意する必要がある。

pythonで並列処理(multiprocessing)

multiprocessingモジュールを使って、threadingモジュールと同じように並列処理を記述できる。

multiprocessing.Processクラスの生成時にtarget引数に指定する関数を、別プロセスで並列的に実行できる。

import multiprocessing
import time


def worker(interval):
    for n in range(3):
        time.sleep(interval)
        print("%s --> %d" % (multiprocessing.current_process().name, n))

p1 = multiprocessing.Process(name="a", target=worker, args=(1, ))
p2 = multiprocessing.Process(name="b", target=worker, args=(2, ))

p1.start()
p2.start()

threadingと同じように書けるように、作られているためほとんど同じように記述できる。生成するのがthreading.Threadか、multiprocessing.Processかの違い。

multiprocessingを使った場合は、サブプロセスが生成されて実行できる。(cpythonの場合)threadingではマルチコアを活かした並行処理ができないが、multiprocessingの場合は独立したプロセスになるのでマルチコアを活かした実行が可能になる。

では、threadingではなくいつもmultiprocessingを使えばいいのかというとそうはならない。
同じように記述はできるが、worker関数を実行するのは別プロセスになるので、メインプロセスとサブプロセスの間で変数を共有したりすることができない。サブプロセスで実行した結果をメインプロセスで受け取ったり、サブプロセスの実行中の状態を取得したりするには、プロセス間通信の仕組みが必要になって複雑になる。

threadの場合は、同一のプロセスで動くのでメモリ空間は共有してあり、ロックなどの処理は必要になるもののデータのやり取りは簡単にできる。

参考:17.2. multiprocessing — プロセスベースの並列処理