前置き
前回の記事「Nightmareで郵便局から住所をスクレイピング」にて、スクレイピングの世界に入門してみました。せっかくなので流行りのAmazon API Gateway+AWS LambdaでAPI化してみようと思ったのですが、これはうまくいきませんでした。。。
Lambdaは、コードの依存先もあわせてzipやjarに固めたのちアップロードする必要があります。ここで問題になるのが、依存ライブラリにネイティブ拡張が含まれている場合です。Nightmare(v2)はブラウザとしてElectronを使っていて、たとえばWindowsでnpm install
するとelectron.exeが入ってくるのですが、LambdaはAmazon Linux上のサービスなのでこれは動かないのです……。
検索してみると、Amazon EC2やVagrantでAmazon Linux環境作って依存ライブラリをビルドなり解決なりしてねとの回答が出てくるのですが、お手軽にやりたいからLambda使うのにそこまで手をかけるのは……という感じです。
- Using Packages and Native nodejs Modules in AWS Lambda | AWS Compute Blog
- Cross-compile node module with native bindings with node-gyp - Stack Overflow
ということでNightmareはばっさり諦めて、SeleniumのJava版を使ってみることにしました。
言語もJavaScriptからScalaに切り替えです……!
コード
与えられた文字列を、Googleの検索フォームに入力して、検索結果のタイトルリストを取得するだけのコードです。見たまんまですね。
ブラウザにはHtmlUnitというJava実装のものを使っています。なので、ブラウザごとjarに固めることができます><b
import com.gargoylesoftware.htmlunit.BrowserVersion
//import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.htmlunit.HtmlUnitDriver
import scala.collection.JavaConversions._
object Main {
def main(args: Array[String]) {
println(awsLambda(args.head))
}
def awsLambda(str: String): java.util.List[String] = {
//System.setProperty("webdriver.chrome.driver","C:\\path\\to\\chromedriver.exe")
//val driver = new ChromeDriver()
val driver = new HtmlUnitDriver(BrowserVersion.CHROME)
driver.get("http://www.google.com")
val query = driver.findElementByName("q")
query.sendKeys(str)
query.submit
val results = driver.findElementsByClassName("r").map(_.getText)
driver.quit()
results
}
}
ローカルでブラウザのGUIの様子を見たい時は、使いたいブラウザに応じたドライバをダウンロードして、コメントアウトしているように指定してください。
AWSの設定
sbt assembly
してjarさえ作ってしまえば、あとは簡単です。
AWS Lambdaの「Code」からjarをアップロードして、「Configuration」にてMain::awsLambda
をHandler指定して、「API endpoints > Add API endpoint」から「API Gateway」を選んで、ぽちぽちPOSTメソッドのAPI設定をすれば完成。これが期待していたお手軽さ!
ただし注意点として、上記のコードでjarのサイズが25MBくらいになるので、ときどきアップロードが失敗します。「For .ZIP files larger than 10 MB, consider uploading via S3.」の注意書きどおりAmazon S3経由を検討しましょう。
あとタイムアウトした場合は、「Configuration > Advanced settings」で長めに設定しなおしてください。
実行結果
まずはAWS Lambdaのみでテストしてみます。
「Actions > Configure Test Event」にテスト用のリクエストボディ"Qiita"
を入力して、「Test」すると、
[
"Qiita - A technical knowledge sharing platform for programmers.",
"タグ一覧",
"Qiita:Team",
"About Qiita",
"Advent Calendar",
"ログイン",
"Organization一覧",
"キータ (@Qiita) | Twitter",
"Qiitaで何があったのか - はてな匿名ダイアリー - はてラボ",
"qrank | Qiitaの人気記事ランキング",
"Qiitaってなに?よく分かってなかった方のためのQiita基礎知識|ferret ...",
"Qiita のニュース検索結果",
"週刊 Qiita"
]
よしよしうまくいきました。消費リソースは20秒/126MBでした。
まぁスクレイピングなのでそのくらいは食いますよね。さてお待ちかねのAPI実行をば
> curl -X POST https://XXXXXXX.ap-northeast-1.amazonaws.com/prod/hello_api -H "x-api-key: XXXXXXX" -d "Qiita"
{"message": "Endpoint request timed out"}
おっとっと、API Gatewayのタイムアウト設定も長くしなきゃですね。えーと設定はどこで……
以下に示しているのは、API Gateway における現在の制限です。
...
AWS Lambda と HTTP バックエンド統合の両方で 10 秒のタイムアウト。
_人人 人人_
> 突然の死 <
 ̄Y^Y^Y^Y ̄
結論
冷静に考えると、今回の用途では自分の管理下からのみ呼びだせればいいので、API Gateway使わずにAWS SDKで直接Lambda呼びだせばいいじゃんということに。それでもタイムアウト上限は60秒なので注意。
Q: AWS Lambda 関数はどれくらいの時間実行できますか?
AWS Lambda に対する同期呼び出しは、60 秒以内に完了する必要があります。デフォルトのタイムアウトは 3 秒ですが、1 秒から 60 秒までの任意のタイムアウト時間を設定できます。非同期リクエストも現在のところ、実行時間が 60 秒に制限されています。
動作環境
name := "hello_api"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
"org.seleniumhq.selenium" % "selenium-java" % "2.48.2"
)
sbt.version=0.13.8
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")