AWSに限らない話ですが、クラウドを基盤とするシステムに関わると、自然とAPIベースで必要な情報を取得する機会は多くなります。ただ、テキストをパースするよりGUIベースで情報を自動取得したい場合も依然とあり、そちらの方が適しているユースケースは依然と多い、と個人的には感じています。
ということで(色々な実現方法がありますが)、今回はHeadless ChromeとSeleniumをLambda関数で動かし、webページのスクリーンショットを取得する方法をご紹介します。
本記事ではHeadless ChromeとSeleniumをLambda Layerで作成し、Lambda関数から使っています。ついでに開発環境としてCloud 9を使い、デプロイツールとしてServerless Frameworkを使いました。
全体の構成としては、次の通りです。
- Headless ChromeとSelenium、2つのLambda Layerを作成
- 「ビルド済みのchromiumバイナリ+chrome用webdriver」で1つのLayer
- 「selenium」で1つのLayer
- Lambda Layerを使うLambda関数を作成
- seleniumによって所定のログインフォームにID&パスワードを入力する&スクリーンショット取得
- 今回、取得したスクリーンショットはS3にアップロードして完了
残念ですが、本記事ではServerless Frameworkの紹介は割愛するため、既に導入済みの環境であることを前提とします。
Getting Started with the Serverless Framework and AWS
Lambda Layerの作成
Headless Chrome+WebdriverとSelenium、2つのLayerを作成するために、まずは準備です。
Headless Chrome、Webdriverの準備
WebDriver is now a W3C Recommendation
それぞれ優れた技術ですが、本記事では説明を割愛します。
ヘッドレスモードがリリースされたのは2017年6月。WebDriverは2018年にW3勧告されている仕様で、Seleniumを使う上では重要なコンポーネントです。なお、本記事ではChromeを扱っていますが、Firefoxでもヘッドレスモードは提供されています。
ヘッドレスブラウザを自動化する上で必要なものは、"ブラウザの実行バイナリ"と"WebDriver"です。
Chromeバイナリは、chromiumのソースコードからあの手この手でビルドすると軽量になります。ビルド済みのものが下記Githubプロジェクトでリリースされていました。感謝しつつ、今回はこちらののリリースからchromiumバイナリをダウンロードして使用します。
serverless-chrome
ChromeのWebdriverはchromedriverという名称ですが、ダウンロードはこちら。
ChromeDriver - WebDriver for Chrome
面倒なTipsとして、Chrome本体とChromeDriverの間に互換性の規則があることです。下記ページにある通り、Chromeバージョンのピリオド区切り数字(major,minor,buildの各番号)が一致するChromeDriverでなければ動きません。
Version Selection
Each version of ChromeDriver supports Chrome with matching major, minor, and build version numbers. For example, ChromeDriver 73.0.3683.20 supports all Chrome versions that start with 73.0.3683
個人的経験としても、最初はローカルPCでchromedriverを使うスクリプトを動かしていたら、Chrome本体の自動アップデート機能によってchromedriverとの互換性エラーが勝手に発生し、沢山のエラーが出ました。。。そんなつまらないエラーを回避するためにも、サーバレス環境を一考することは重要だなーと再認識しました。
ご参考ブログ:自動バージョンアップスクリプト「chromedriver_update_tool」を公開しました
なお、chromeとchromedriverのバージョンは上記の通りに守りましたが、seleniumはバージョン気にせず導入しても動作しました。
Seleniumの準備
AWS Lambda Deployment Package in Python
Lambdaに標準外パッケージを含めたい場合、上記ドキュメント通りのディレクトリ構成にする必要があります。そのため、Seleniumをpipインストールする際は、次のようなコマンドが必要です。
pip install -t {任意ディレクトリ名}/lib/python3.x/site-packages selenium ## "python3.x"は使用するPythonバージョン
Chromeの実行バイナリ、ChromeDriver、Selenium、それぞれの準備を済ませ、次のようなディレクトリ構成ができました。
. ├── headless-chrome │ ├── chromedriver │ └── headless-chromium ├── selenium │ └── lib └── serverless.yml
Lambda Layerの作成
データ一式の準備は完了したので、あとはデプロイするのみです。serverless.ymlの記述は次の通り。 今回、sls create -t aws-python -p {プロジェクト名}でテンプレートを作成した都合上、Pythonバージョンは"3.6"の指定で統一しています。
service: selenium-layer provider: name: aws runtime: python3.6 stage: dev region: ap-northeast-1 layers: selenium: path: selenium description: layer for selenium CompatibleRuntimes: - python3.6 chromedriver: path: headless-chrome description: layer for chromedriver and headless-chromium CompatibleRuntimes: - python3.6 resources: Outputs: SeleniumLayerExport: Value: Ref: SeleniumLambdaLayer Export: Name: SeleniumLambdaLayer ChromedriverLayerExport: Value: Ref: ChromedriverLambdaLayer Export: Name: ChromedriverLambdaLayer
serverless.ymlと同じ階層でデプロイ実行すれば、Lambda Layerが2つ作成されます。
# sls deploy -v Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Excluding development dependencies... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service selenium.zip file to S3 (1.32 MB)... Serverless: Uploading service chromedriver.zip file to S3 (48.67 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - selenium-layer-dev CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - SeleniumLambdaLayer CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromedriverLambdaLayer CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - SeleniumLambdaLayer CloudFormation - UPDATE_COMPLETE - AWS::Lambda::LayerVersion - SeleniumLambdaLayer CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromedriverLambdaLayer CloudFormation - UPDATE_COMPLETE - AWS::Lambda::LayerVersion - ChromedriverLambdaLayer CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - selenium-layer-dev CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::LayerVersion - ChromedriverLambdaLayer CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::LayerVersion - SeleniumLambdaLayer CloudFormation - DELETE_COMPLETE - AWS::Lambda::LayerVersion - SeleniumLambdaLayer CloudFormation - DELETE_COMPLETE - AWS::Lambda::LayerVersion - ChromedriverLambdaLayer CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - selenium-layer-dev Serverless: Stack update finished... Service Information service: selenium-layer stage: dev region: ap-northeast-1 stack: selenium-layer-dev resources: 4 api keys: None endpoints: None functions: None layers: selenium: arn:aws:lambda:ap-northeast-1:123456789012:layer:selenium:2 chromedriver: arn:aws:lambda:ap-northeast-1:123456789012:layer:chromedriver:2 Stack Outputs SeleniumLambdaLayerQualifiedArn: arn:aws:lambda:ap-northeast-1:123456789012:layer:selenium:2 SeleniumLayerExport: arn:aws:lambda:ap-northeast-1:123456789012:layer:selenium:2 ChromedriverLayerExport: arn:aws:lambda:ap-northeast-1:123456789012:layer:chromedriver:2 ChromedriverLambdaLayerQualifiedArn: arn:aws:lambda:ap-northeast-1:123456789012:layer:chromedriver:2 ServerlessDeploymentBucketName: selenium-layer-dev-serverlessdeploymentbucket-xxxxxxxxxxxx Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
なお、上記メッセージは既にLambda Layer作成後に再実行した際です。同じ定義のserverless.ymlであれば、既存のAWSリソースをアップデートする挙動になります。
Lambda関数の準備
Lambda関数に関しては、準備はないのでコードを作成するだけです。 ...というわけには行きませんでした。日本人特有のフォント問題に対処する必要があります。
いわゆる豆腐問題です。デジタル用語辞典:豆腐
最終的に、".fonts"ディレクトリをLambdaのパッケージに含めて一緒にデプロイすることで解決しました。フォントディレクトリについては、Linux一般のはなしで、下記ページが参考になりました。
ご参考ブログ:How to Manage Fonts in Linux
フォントデータはIPAが配布していたものをダウンロードし、Cloud 9環境に配置します。
個人的にはNotoフォントを使用するつもりでしたが、一式のデータサイズがGB単位だったので断念しました。IPAに感謝です。
準備が完了すると、次のようなディレクトリ構成になりました。
. ├── .fonts │ ├── ipaexg.ttf │ └── ipaexm.ttf ├── serverless.yml └── headless-chrome.py
Lambda関数の作成
Layerの際と同じく、ymlファイルにデプロイ内容を記述します。
service: test-headless-chrome provider: name: aws runtime: python3.6 memorySize: 256 timeout: 150 stage: ${opt:stage, 'dev'} region: ap-northeast-1 iamRoleStatements: - Effect: "Allow" Action: - "s3:GetObject" - "s3:PutObject" Resource: "arn:aws:s3:::{アップロード先のS3バケット名}/*" functions: headless-chrome: handler: headless-chrome.lambda_handler description: Headless Chrome environment: TZ: Asia/Tokyo STAGE: ${self:provider.stage} S3BUCKET: headless-chrome-${self:provider.stage} package: include: - '.fonts/**' layers: - arn:aws:lambda:ap-northeast-1:123456789012:layer:chromedriver:1 - arn:aws:lambda:ap-northeast-1:123456789012:layer:selenium:1
- memorySize, timeout
メモリサイズとタイムアウトの値は上積みした方が良いです。ヘッドレスとはいえブラウザを起動するため、オーバーヘッドかかります。 - package
こちらで".fonts"ディレクトリを定義します。severless docs: Package Configuration - layers
作成したLambda LayerのARNを指定します。
Lambda関数のスクリプト本体は次の通りです。
from selenium import webdriver from selenium.webdriver.chrome.options import Options import time import boto3 import os os.environ['HOME'] = '/var/task' def lambda_handler(event, context): options = webdriver.ChromeOptions() options.binary_location = '/opt/headless-chromium' options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--window-size=1280,1024') options.add_argument("--hide-scrollbars") options.add_argument("--enable-logging") options.add_argument("--disable-application-cache") options.add_argument("--disable-infobars") options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-gpu') options.add_argument("--ignore-certificate-errors") driver = webdriver.Chrome('/opt/chromedriver', chrome_options=options) s3 = boto3.client('s3') url = 'https://xxx' bucket = 'アップロード先のS3バケット名' driver.get(url) body = f"Title: {driver.title}" user = driver.find_element_by_css_selector("ログイン名のCSSセレクターパス") user.send_keys("ログイン名文字列") word = driver.find_element_by_css_selector("パスワードのCSSセレクタ―パス") word.send_keys("パスワード文字列") time.sleep(1) driver.save_screenshot('/tmp/test.png') response = { "statusCode": 200, "body": body } s3.upload_file( Filename="/tmp/test.png", Bucket=bucket, Key="test.png") driver.close(); driver.quit(); return response
os.environ['HOME']
OS環境変数HOMEとLambdaの実行ディレクトリを一致させます。分かりやすくするため、上記コードでは"/var/task"を直指定してますが、将来的な仕様変更に備えるためにも os.getenv(LAMBDA_TASK_ROOT) で指定したほうが安全です。
LAMBDA_TASK_ROOToptions.binary_location
Lambda Layerとして作成したchromiumのパスを指定します。'/opt/'に配置されるのがLambda Layer仕様です。
webdriver.Chrome
Lambda Layerとして作成したchromedriverのパスを指定します。
url
本記事では、は弊社グループウェアのログインページを指定しています。
自分のhtml知識が皆無のため、Firefoxの開発者モードに頼り切りました。
save_screenshot
Selenium仕様により、.pngフォーマットのみサポートされます。serverlessを導入していると、次のようにLambda関数を開発環境から実行することも可能です。
# sls invoke -f headless-chrome { "statusCode": 200, "body": "Title: ログイン - サイボウズ Office" }
TOFUも解消しました。
最後に
スクリーンショットを自動取得できるようになると、用途は色々考えられます。Webシステムのリグレッションテストはもとより、あるWebページの情報を通知したいだけでも、テキストより画面をみたほうが理解が早い場合は多いはずです。
個人的に今回一番手こずったのは、日本語フォントの文字化け対処でした。普段、意識もしない分野だったので苦戦しましたが、目新しく面白かったです。
Font hinting
ちなみに、今回使用したchromiumバイナリのように軽量ビルドしたい場合、下記ブログがとても参考になります。興味のある方はどうぞ。
How to get headless Chrome running on AWS Lambda