【マルチスレッド化】Webスクレイピングを高速化する方法

こんにちは、のっくんです。

今日はマルチスレッディングを使ってWebスクレイピングを高速化する方法をご紹介します。

Webスクレイピングでは、

  1. requestsでURLにアクセスしHTMLをダウンロード
  2. タグの情報をBeatifulSoupでスクレイピングする

のが一般的です。

しかし、接続先URLが多数あると1番でかなり時間がかかります。

ネットワーク遅延等で処理に時間がかかるからです。

そういう場合にはマルチスレッド化することで高速化できます。

今日はその高速化した例をコードと共にご紹介します。

Wikipediaの例

必要なパッケージのインポート

スクレイピングに必要なrequestsとマルチスレッドに必要なTreadPoolExecuterをインポートします。

partialは、引数を固定したいときに使います。今回は、ヘッダー情報を引数として固定したいので使用します。

from concurrent.futures import ThreadPoolExecutor
import requests
from functools import partial

URLのリストを作成する

スクレイピングに使用するURLの一覧をリスト化します。

URLはWikipediaの機械学習に関するページにします。

urls = [
  'https://en.wikipedia.org/wiki/Automated_machine_learning',
  'https://en.wikipedia.org/wiki/Big_data',
  'https://en.wikipedia.org/wiki/Explanation-based_learning',
  'https://en.wikipedia.org/wiki/List_of_important_publications_in_computer_science#Machine_learning',
  'https://en.wikipedia.org/wiki/List_of_datasets_for_machine_learning_research',
  'https://en.wikipedia.org/wiki/Predictive_analytics',
  'https://en.wikipedia.org/wiki/Quantum_machine_learning',
  'https://en.wikipedia.org/wiki/Machine_learning_in_bioinformatics',
  'https://en.wikipedia.org/wiki/Machine_learning'
  ]

シングルスレッドで実行する

まずはシングルスレッドで実行した場合にどの程度時間がかかるか計測してみます。

%%time

results = []
for url in urls:
    r = requests.get(url)
    results.append(r)

print(results)

実行結果は以下の通り。

[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
CPU times: user 150 ms, sys: 17.1 ms, total: 167 ms
Wall time: 1.15 s

コードの実行時間は「Wall time」です。1.15秒かかりました。

マルチスレッドで実行する

次はマルチスレッドで実行してみます。

%%time

with ThreadPoolExecutor(4) as executor:
  results = list(executor.map(requests.get, urls))

print(results)

実行結果は以下の通り。

[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
CPU times: user 181 ms, sys: 29.2 ms, total: 210 ms
Wall time: 397 ms

0.39秒で実行できました。シングルスレッドに比べて速くなりました。

海外サイトの例

マルチスレッドだと高速化できることは分かりましたが、上記の例だと普通にアクセスしても1秒程度で情報を取得できてしまうのであまりマルチスレッディングのありがたみを感じません。

そこで、普通にアクセスするとかなり時間がかかる海外のサイトで速度を比較してみましょう。

以下のURLは、プレミアリーグの移籍金が掲載されているページです。

アクセスしてみると分かりますが表示するまでにかなり時間がかかります。

さらに、その年ごとにURLが異なるので13年分の移籍金情報を取得するには13回アクセスする必要があります。

かなり重いページですが、今回のマルチスレッディングの効果を確かめるには有効です。

urls2 = []
for year in range(2005,2018):
  urls2.append("https://www.transfermarkt.com/premier-league/transfers/wettbewerb/GB1/plus/?saison_id="+ str(year) +"&s_w=&leihe=0&leihe=1&intern=0&intern=1")

シングルスレッド

このサイトですがヘッダーにUser-Agentがないとアクセスできないようになっているので、User-Agentを指定してアクセスします。

requests.getでサイトにアクセスする際に、ヘッダー情報をキーワード引数として指定する必要があります。

%%time

results = []
for url in urls2:
    r = requests.get(url,headers={'User-Agent': 'hoge'})
    results.append(r)

print(results)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
CPU times: user 313 ms, sys: 36.8 ms, total: 350 ms
Wall time: 2min 48s

なんと2分48秒もかかっています。気が遠くなるような遅さですね。

マルチスレッド

%%time

mapfunc = partial(requests.get, headers={'User-Agent': 'hoge'})
with ThreadPoolExecutor(4) as executor:
  results = list(executor.map(mapfunc, urls2))

print(results)

今回のようにmap関数に関数を渡すときにキーワード引数を固定化するには、partialを使うと便利です。

[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
CPU times: user 304 ms, sys: 47.1 ms, total: 351 ms
Wall time: 5.07 s

なんと5秒で実行できました。これぞマルチスレッドの威力!

おわり。

参考

https://medium.com/towards-artificial-intelligence/the-why-when-and-how-of-using-python-multi-threading-and-multi-processing-afd1b8a8ecca

ABOUTこの記事をかいた人

個人アプリ開発者。Python、Swift、Unityのことを発信します。月間2.5万PVブログ運営。 Twitter:@yamagablog