昨日、puppeteerでファイルをダウンロードする という記事を書いたのですが、puppeteerのpythonに移植した pyppeteer で同じようにファイルダウンロードをやってみました。
pyppeteer自体の使い方は以前記事を書いてるので、そちらをご覧ください。
pyppeteerの使い方
実装
puppeteerで試した中で一番筋が良さそうだった「ダウンロードのrequestをキャプチャして、同じ内容で別途requestを送信する」という方法を実装してみました。
from typing import Union, List, Dict
import asyncio
from uuid import uuid4
from mimetypes import guess_extension
from pyppeteer.launcher import launch
from pyppeteer.page import Page, Request
from pyppeteer.element_handle import ElementHandle
from pyppeteer.browser import Browser
import requests
from requests import Response
def main():
asyncio.get_event_loop().run_until_complete(
download('https://www.soumu.go.jp/toukei_toukatsu/index/seido/9-5.htm', 'a[href*=".csv"]'))
asyncio.get_event_loop().run_until_complete(
download('https://www.soumu.go.jp/toukei_toukatsu/index/seido/9-5.htm', 'a[href*=".pdf"]'))
asyncio.get_event_loop().run_until_complete(
download('https://pdf-xml-download-test.vercel.app/', '#link-pdf'))
asyncio.get_event_loop().run_until_complete(
download('https://pdf-xml-download-test.vercel.app/', '#link-xml'))
async def download(target_url: str, selector: str):
browser: Browser = await launch(headless=True)
try:
page: Page = await browser.newPage()
await asyncio.gather(page.goto(target_url), page.waitForNavigation()) # 対象ページに移動
cookies: List[Dict[str, Union[str, int, bool]]] = await page.cookies() # cookieを取得
await page.setRequestInterception(True) # リクエストをインターセプト
page.on('request',
lambda request: asyncio.create_task(send_request(request, cookies))) # キャプチャした内容で別途リクエストを送信
link: ElementHandle = await page.querySelector(selector)
await link.click() # 対象リンクをクリック
finally:
await browser.close()
async def send_request(request: Request, cookies: List[Dict[str, Union[str, int, bool]]]):
response: Response = requests.request(
url=request.url,
method=request.method,
headers=request.headers,
cookies=dict(map(lambda cookie: (cookie['name'], cookie['value']), cookies)),
data=request.postData,
)
extension: str = guess_extension(response.headers.get("content-type"))
output_path: str = f'downloads/{uuid4()}{extension}'
with open(output_path, mode='wb') as file:
file.write(response.content)
if __name__ == "__main__":
main()
実際に4つのページでpdfを二つ、xml、csvファイルをダウンロードしてみたのですが、あっさり動いてくれました。
細かい部分で多少の工夫はありますが、主要なポイントは以下です。
-
await page.setRequestInterception(True)
でリクエストをインターセプト可能にする -
page.on('request', callback)
でリクエストをキャプチャし、その中で、requestsモジュールを使って、キャプチャしたのと同じ内容で別途リクエストを送信する
さいごに
昔試してみた時はやり方が分かりませんでしたが、今回はpuppeteerでうまくいく方法を試してみた後にうまくいった方法をpyppeteerに移植するという形をとったので、比較的簡単に実現できたと思います。
ちなみに、puppeteerではうまく動いた「強制的にブラウザのファイルダウンロード処理を実行する」という方も途中まで実装したのですが、Chrome Devtoolsの Fetch.enable のrequestStageがどうしてもResponseステージでイベントキャプチャできなくて諦めました。。
参考までですが、上記のソースコードは以下で公開しています。
https://github.com/rinoguchi/python_scraping_sample/blob/master/src/pyppeteer/download_with_another_request.py