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は使用した分だけの課金ですので、用途に合う要件であれば是非利用してみることをおすすめいたします。
コメントを残す