
AWS Lambda上でWebスクレイピング
AWS Lambdaは様々なリソースの制限(例えば900秒を超える処理は実行できない等)があるため、スクレイピング処理をうまく動作させるために一苦労しました。これらの制限をクリアできる処理を動作させることに限定するという条件ではありますが、そこそこ使いやすいスクレイピング環境ができましたのでご紹介します。
デプロイ端末の環境構築
- Amazon Linux release 2 (Karoo)
- serverless framework 1.30.3
- Python 3.6
serverless frameworkについては以下のClassmethodさんの記事が参考になりますので、載せておきます。
上記の記事にしたがってserverlessのsampleというプロジェクトを作成したという前提で、このプロジェクトにスクレイピングに必要なパッケージ等を追加していきます。この時点でsampleプロジェクト配下は以下のようになっていることを確認します。
sample/ ├ handler.py ├ serverless.yml ├ package.json ├ package-lock.json ├ requirements.txt ├ node_modules/ ├ .serverless/ ├ .requirements.zip
headless chromeとchrome driverをインストール
headlessのchromeはnpm、pipではインストールできないため、sample配下にbinディレクトリを作成して、その中で手動でダウンロードして展開します。
(venv) $ cd sample (venv) $ mkdir bin (venv) $ cd bin (venv) $ curl -SL https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-37/stable-headless-chromium-amazonlinux-2017-03.zip > headless-chromium.zip (venv) $ unzip headless-chromium.zip (venv) $ rm headless-chromium.zip (venv) $ curl -SL https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip > chromedriver.zip (venv) $ unzip chromedriver.zip (venv) $ rm chromedriver.zip
スクレイピング用のパッケージをインストール
今回はselenium、beautifulSoup4、html5libを使用するので、これらをpipでインストールします。インストールした後は必ずrequirements.txtを更新します。
(venv) $ cd sample (venv) $ pip install selenium (venv) $ pip install beautifulSoup4 (venv) $ pip install html5lib (venv) $ pip freeze > requirements.txt
Pythonのスクレイピング処理を追加
既存のhandler.pyにスクレイピング処理を追加します。selenium、beautifulSoup4はWeb上にサンプルが多数ありますので、細かい部分は省略します。ここでは会員サイトである一覧表を表示し、その一覧をCSVファイルとしてS3にアップする処理を紹介します。
try: import unzip_requirements except ImportError: pass import sys import os import time import calendar import datetime import logging import subprocess import boto3 from pathlib import Path from bs4 import BeautifulSoup from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import Select mode2bucket = { 'dev': 'sample-scraping-dev-us-east-1', 'pro': 'sample-scraping' } def main(event, context): print("main start") # log level logger = logging.getLogger() logger.setLevel(logging.INFO) STAGE = os.environ['selected_stage'] logger.info("stage[%s]" % STAGE) MODE = os.environ['scraping_mode'] logger.info("mode[%s]" % MODE) if context: logger.info("start lambda_handler() [%s]" % context.function_name) #today d = datetime.datetime.today() #yesterday d2 = d - datetime.timedelta(days=1) target_url = 'https://target-url.jp/' user_id = 'user1234@sample-email.jp' password = 'xxxxxxxx' try: options = webdriver.ChromeOptions() options.binary_location = "./bin/headless-chromium" options.add_argument("--headless") options.add_argument("--disable-gpu") options.add_argument("--window-size=1280x1696") options.add_argument("--disable-application-cache") options.add_argument("--disable-infobars") options.add_argument("--no-sandbox") options.add_argument("--hide-scrollbars") options.add_argument("--enable-logging") options.add_argument("--log-level=0") options.add_argument("--single-process") options.add_argument("--ignore-certificate-errors") options.add_argument("--homedir=/tmp") driver = webdriver.Chrome(options=options, executable_path='./bin/chromedriver') driver.implicitly_wait(30) driver.get(target_url) # login logger.info("before log in") driver.find_element_by_name("usr_id").send_keys(user_id) driver.find_element_by_name("usr_password").send_keys(password) driver.find_element_by_class_name("btnLogin").click() logger.info("after log in") logger.info(driver.current_url) # click smartphone tab element_to_hover_over = driver.find_element_by_id("navi02") hover = ActionChains(driver).click(element_to_hover_over) hover.perform() driver.find_element_by_link_text('スマートフォン').click() time.sleep(5) logger.info(driver.current_url) # check mode end_date = "" file_path = "" if MODE == "total": #前の月の全日を取得 end_date = datetime.date(int(d.strftime("%Y")), int(d.strftime("%m")), 1) - datetime.timedelta(days=1) file_path = 'sample_site/%s/%s/sample_total_%s.csv' % (d.strftime('%Y'), d.strftime('%m'), d.strftime('%Y%m%d')) else: end_date = d2 file_path = 'sample/%s/%s/sample_%s.csv' % (d2.strftime('%Y'), d2.strftime('%m'), d2.strftime('%Y%m%d')) # click 'date range' radio button driver.find_element_by_id("rep_send_date_radio").click() logger.info(driver.current_url) #input start date start_date = end_date.strftime("%Y/%m/") + str(1).zfill(2) logger.info(start_date) logger.info(end_date.strftime("%Y/%m/%d")) driver.execute_script("document.getElementById('rep_start_date').value = '%s'" % start_date) driver.execute_script("document.getElementById('rep_end_date').value = '%s'" % end_date.strftime("%Y/%m/%d")) logger.info(driver.current_url) # click 'レポート表示' button driver.find_element_by_id("rep_button_view").click() logger.info(driver.current_url) time.sleep(10) html = driver.page_source soup = BeautifulSoup(html, "html5lib") # create text in csv file s3_text = '' #add header thead_tr_list = soup.find('table', id="rep_daily_all_option").findAll('thead')[0].findAll('tr') for r1 in range(0, len(thead_tr_list)): for elm1 in thead_tr_list[r1].findAll('th'): th = elm1.text.strip() s3_text += th s3_text += '\t' s3_text += '\n' # add body tbody_tr_list = soup.find_all('table', id="rep_daily_all_option")[0].findAll('tbody')[0].findAll('tr') for r2 in range(0, len(tbody_tr_list)): if (r2 % 100) == 0: logger.info("line[%s]" % str(r2)) for elm2 in tbody_tr_list[r2].findAll('td'): td = elm2.text.strip() s3_text += td s3_text += '\t' s3_text += '\n' driver.quit() # put file to s3 s3 = boto3.resource('s3') bucket = mode2bucket[STAGE] s3Obj = s3.Object(bucket, file_path) s3Obj.put(Body = bytes(s3_text, 'UTF-8')) logger.info("finished") except Exception as e: # キャッチして例外をログに記録 logger.exception(e) post_to_chat("例外が発生しました。", e) return 1 return 0 if __name__ == "__main__": main('', '')
デプロイの準備
serverless.yml に上記で追加されたbinディレクトリの定義を追加します。(22行目を追加しました。)
service: sample-scraping provider: name: aws runtime: python3.6 stage: ${opt:stage, self:custom.defaultStage} plugins: - serverless-python-requirements custom: defaultStage: dev pythonRequirements: dockerizePip: non-linux # dockerFile: ./Dockerfile slim: true zip: true package: include: - handler.py - './bin/**' exclude: - '**' functions: sample-main: handler: sample.main timeout: 900 # Lambda の最大が 900 秒 environment: selected_stage: ${self:provider.stage} scraping_mode: normal
デプロイ・実行
下記のコマンドでデプロイと実行を行います。
(venv) $ cd sample (venv) $ sls deploy (venv) $ sls invoke -f sample-main --log
まとめ
serverlessは本当にお手軽に処理のデプロイができますし、Lambdaは使用した分だけの課金ですので、用途に合う要件であれば是非利用してみることをおすすめいたします。

優秀な技術者と一緒に、好きな場所で働きませんか
株式会社もばらぶでは、優秀で意欲に溢れる方を常に求めています。働く場所は自由、働く時間も柔軟に選択可能です。
現在、以下の職種を募集中です。ご興味のある方は、リンク先をご参照下さい。