スクレイピングをAPI化したい人生だった

# 前置き

前回の記事「[Nightmareで郵便局から住所をスクレイピング](http://qiita.com/piyo7/items/d771559c0fb1386f1ffa)」にて、スクレイピングの世界に入門してみました。せっかくなので流行りの[Amazon API Gateway](http://aws.amazon.com/jp/api-gateway/)＋[AWS Lambda](http://aws.amazon.com/jp/lambda/)でAPI化してみようと思ったのですが、これはうまくいきませんでした。。。
Lambdaは、コードの依存先もあわせてzipやjarに固めたのちアップロードする必要があります。ここで問題になるのが、依存ライブラリにネイティブ拡張が含まれている場合です。[Nightmare](http://www.nightmarejs.org/)(v2)はブラウザとして[Electron](http://electron.atom.io/)を使っていて、たとえばWindowsで```npm install```するとelectron.exeが入ってくるのですが、LambdaはAmazon Linux上のサービスなのでこれは動かないのです……。
検索してみると、[Amazon EC2](http://aws.amazon.com/jp/ec2/)やVagrantでAmazon Linux環境作って依存ライブラリをビルドなり解決なりしてねとの回答が出てくるのですが、お手軽にやりたいからLambda使うのにそこまで手をかけるのは……という感じです。

- [Using Packages and Native nodejs Modules in AWS Lambda | AWS Compute Blog](https://aws.amazon.com/jp/blogs/compute/nodejs-packages-in-lambda/)
- [Cross-compile node module with native bindings with node-gyp - Stack Overflow](http://stackoverflow.com/questions/30340443/cross-compile-node-module-with-native-bindings-with-node-gyp)

ということでNightmareはばっさり諦めて、[Selenium](http://www.seleniumhq.org/)のJava版を使ってみることにしました。
言語もJavaScriptからScalaに切り替えです……！

# コード

与えられた文字列を、[Google](https://www.google.co.jp/)の検索フォームに入力して、検索結果のタイトルリストを取得するだけのコードです。見たまんまですね。
ブラウザには[HtmlUnit](http://htmlunit.sourceforge.net/)というJava実装のものを使っています。なので、ブラウザごとjarに固めることができます＞＜ｂ

```scala:src/main/scala/main.scala
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の様子を見たい時は、使いたいブラウザに応じたドライバを[ダウンロード](http://www.seleniumhq.org/download/)して、コメントアウトしているように指定してください。

# 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](https://aws.amazon.com/jp/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 秒のタイムアウト。

[Amazon API Gateway における制限](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/limits.html)

＿人人 人人＿
＞ 突然の死 ＜
￣Y^Y^Y^Y￣

# 結論

冷静に考えると、今回の用途では自分の管理下からのみ呼びだせればいいので、API Gateway使わずに[AWS SDK](https://aws.amazon.com/jp/tools/)で直接Lambda呼びだせばいいじゃんということに。それでもタイムアウト上限は60秒なので注意。

> Q: AWS Lambda 関数はどれくらいの時間実行できますか?
> AWS Lambda に対する同期呼び出しは、60 秒以内に完了する必要があります。デフォルトのタイムアウトは 3 秒ですが、1 秒から 60 秒までの任意のタイムアウト時間を設定できます。非同期リクエストも現在のところ、実行時間が 60 秒に制限されています。

[よくある質問 - AWS Lambda](https://aws.amazon.com/jp/lambda/faqs/)

# 動作環境

```scala:build.sbt
name := "hello_api"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies ++= Seq(
  "org.seleniumhq.selenium" % "selenium-java" % "2.48.2"
)
```

```scala:project/build.properties
sbt.version=0.13.8
```

```scala:project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")
```