かれ4

かれこれ4個目のブログ

SORACOM Airを使わずに、SORACOMを楽しむ方法

はじめに

SORACOMリリース1周年記念リレーブログ 11月1日分です。 blog.soracom.jp

SORACOMももう1周年という事で、色々とサービスが増えて来ました。 まずは各サービスをGoogle Trendで見てみます。

↑読み込み遅いけどGoogle Trendsのグラフ出ます。

↑読み込み遅いけどGoogle Trendsのグラフ出ます。

SORACOM Airが 2014/7/6 - 12の間に検索されていたりするのは、 創業者二人が飲みながら名前を考えているときに、 検索していたのかなと想像すると楽しいですね。

AirとBeamの検索量にたいして、他のサービスはGateがかろうじて 検索されている位の状況です。

ということで、今回はCanal周りを中心に遊んでみようと思います。

VPGで遊ぶ

まずはVPGで遊んでみたいと思います。 VPGを作成するところから。

VPGの作成

$ soracom vpg create --device-subnet-cidr-range=10.0.0.0/9  --primary-service-name=canal
{
    "createdTime": 1477998721142,
    "deviceSubnetCidrRange": "10.128.0.0/9",
    "lastModifiedTime": 1477998722107,
    "operatorId": "OP0012950956",
    "primaryServiceName": "Canal",
    "status": "creating",
    "tags": {},
    "type": 12,
    "ueSubnetCidrRange": "10.128.0.0/9",
    "useInternetGateway": true,
    "virtualInterfaces": null,
    "vpcPeeringConnections": null,
    "vpgId": "********-****-****-****-************"
}

コマンドラインでサクと作ります。
### statusがrunnningになればvpg作成完了
$ soracom  vpg get --vpg-id=acf68be6-83f7-44c3-9fd5-bd741a434176 |jq -r .status
running

特に難しいこともなく、作成完了です。

$ soracom vpg list-ip-address-map-entries --vpg-id=********-****-****-****-************
[
    {
        "hostId": "100.64.148.132",
        "ipAddress": "10.64.148.132",
        "key": "100.64.148.132",
        "type": "gatePeer"
    },
    {
        "hostId": "100.64.148.4",
        "ipAddress": "10.64.148.4",
        "key": "100.64.148.4",
        "type": "gatePeer"
    }
]

f:id:tottokug:20161102003139p:plain

 なにやら、アドレスが2個作成されています。  推測ですが、VPGというのはVPCと(AZにまたがって)EC2が2台という構成で作られているのでは無いかと推測できます。  今作成したものであれば、  100.64.128.0/17と100.64.0.0/17というサブネットがあるのではないかと思います。

 このVPCがアカウント毎に違うVPCなのか、同じVPCなのかは今回は検証していませんが、おそらく別れていることでしょう。

AWS側でVPCを作って遊んでみる

こちらもさくと作ります。

$ aws ec2 create-vpc --cidr-block 172.19.0.0/16
{
    "Vpc": {
        "VpcId": "vpc-467*****",[f:id:tottokug:20161102003146p:plain]
        "InstanceTenancy": "default",
        "State": "pending",
        "DhcpOptionsId": "dopt-ada4b3cf",
        "CidrBlock": "172.19.0.0/16",
        "IsDefault": false
    }
}

f:id:tottokug:20161102003141p:plain

VPGとAWS VPCをつなぐ

VPCとつなぐのはコマンド一つのお手軽さです。

$ soracom vpg create-vpc-peering-connection \
--destination-cidr-block=172.19.0.0/16 \
--peer-owner-id=01234567890123 \
--peer-vpc-id=vpc-********** \
--vpg-id=********-****-****-****-************

これで、SORACOM側のAWSアカウントから、指定したVPCに対してPeeringのRequestが届きます。

それをAWS側でAcceptすることで、Peeringが完成しました。 f:id:tottokug:20161102003146p:plain

VPGとSORACOMのVPCの関係について後日追記予定の内容があります。

Subnetの作成とRouteTableの設定とEC2の起動

先程Peeringを作ったVPCもそのままでは特に何もないネットワークなので、 もうちょっと遊ぶために、Subnet(172.19.8.0/24)を作成し、 RouteTableに100.64.0.0/16 への通信をpeeringに渡す設定とSecurity Groupを適切に設定する事で、 AirからEC2への疎通は出来るはずです。 今回はAirは使っていないので、確認はできていません。。。

f:id:tottokug:20161102003148p:plain

EC2からアクセス出来るのはどこまでなのか

そして、今日の本題。 EC2からSORACOMのどこまでアクセス出来るのか試してみたいと思います。

Doorがあればまた話しは変わってくるのかもしれませんが、 今回はSORACOM CanalとSORACOM GateだけでEC2がどこまでアクセス出来るのかを試していきたいと思います。 vxlanの設定までを https://dev.soracom.io/jp/start/gate/ ここを見ながら終わらせます。

$ soracom vpg register-gate-peer --outer-ip-address 172.19.8.253 --vpg-id ********-****-****-****-************
{
    "innerIpAddress": "10.18.216.109",
    "netmask": null,
    "outerIpAddress": "172.19.8.253",
    "ownedByCustomer": true
}

SORACOM Air

これは公式ドキュメント https://dev.soracom.io/jp/start/gate/#step5 にも書いてある事で出来るはずです。 今回はSORACOM Airを使わないで楽しむことが目的なので、割愛します。

SORACOM Beamに

SORACOM Beamには100.127.127.100というIPアドレスがついています。 このネットワークにアクセス出来ると良いですね。 とりあえず、単純にRouteTableに100.127.0.0/16へのルートをPeeringに向けてみます。

$ nmap 100.127.127.100

Starting Nmap 6.40 ( http://nmap.org ) at 2016-11-01 14:58 UTC
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.03 seconds

SORACOM Funnelに

Beamのときと同様にfunnel.soracom.io (100.127.65.43)にも

$ nmap  100.127.65.43

Starting Nmap 6.40 ( http://nmap.org ) at 2016-11-01 15:01 UTC
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.03 seconds

さいごに

結論、頑張ったけど、EC2からBeamにもFunnelにも到達することはできませんでした。

もし、EC2から直接BeamやFunnelにアクセス出来たならば、 MQTTのフルマネージドサービスを手に入れることができ、 SORACOMをIoTのサービスではなく、データフローのサービスとしても楽しむことが出来るはずです。

VPGはセットアップの時に 980 円/回 (税別)がかかるのと、 VPG 利用料金: 1時間あたり 50 円 (税別) VPC ピア接続利用料金: 1ピア接続・1時間あたり 10 円 (税別) [2016/11/1 現在] がかかるので遊び終わったら削除する事と、無駄にVPGを作成しないことで 突然の課金にびっくりすることはないでせう。

と、自己満足に浸ったところで、11/1分はおしまいです。

Logicool (Logitech) R800の各ボタンのキーコード

Logicool R800とは

プレゼンを日常的に行う人なら誰でも一本は持っているであろう、道具。 これがあれば、PCの前にいなくてもKeynotePowerpointのスライド送りをすることが出来るすぐれもの。

LOGICOOL プロフェッショナルプレゼンター タイマー機能・LCD搭載 R800

LOGICOOL プロフェッショナルプレゼンター タイマー機能・LCD搭載 R800

R800はキーボード

PCに接続すると配列を問うダイアログが出たりすることから、キーボードとして認識される事は有名ですが、 キーコードはあまり知られていません。 各ボタンに対応するキーコードがわかれば、ブラウザでのデモなんかにも応用することが出来るようになってきます。

R800のボタン

f:id:tottokug:20160522162102p:plain

上の写真の通り、このプレゼンターには前面に5個のボタン 写真ではわかりませんが、右側面に2個のぜんぶで7個のボタンがついています。

このうち、PCに認識されているボタンとしては以下の画像の4つになります。

f:id:tottokug:20160522162449p:plain

キーコード表

位置 ボタン コード キーボード
左上 f:id:tottokug:20160522163732p:plain 33 PageUp
右上 f:id:tottokug:20160522163740p:plain 34 PageDown
左下 f:id:tottokug:20160522163747p:plain 27/116 Esc/F5
右下 f:id:tottokug:20160522163751p:plain 190 .

f:id:tottokug:20160522163747p:plainのボタンはF5とEscがトグルになっていて、27->116->27->....と交互にキーコードを送ってきます。

Visual Stuidio Code の0.10.6が出来たようです。

Macの日本語環境でVisual Studio Codeが動かなくなって困ったけれど
0.10.6が出てたみたいです。
おそらくこの0.10.6はupdateChannel が insiderのやつかと思われます。

このバージョンだとMacの日本語環境でも問題なく動きました。

github.com


ビルド済みのバージョンは以下からダウンロード出来ます。
https://az764295.vo.msecnd.net/public/0.10.6/VSCode-darwin.zip

ここのバージョン番号を書き換えればVisual Studio Codeのサイトでダウンロード出来ないバージョンもダウンロード出来ます。(2015/12/23)

Azure Advent Calendar - 三倉姉妹(マナカナ)をAzureを使って見分ける

Microsoft Azure Advent Calendar 2015 の20日目の記事です。


今回はProject Oxfordを使って三倉姉妹(マナカナ)の判別を行いたいと思います。

三倉姉妹とは?

三倉姉妹とは、三倉茉奈三倉佳奈の双子の女優・タレント・歌手です。
通称マナカナと呼ばれている人たちです。

「左がマナで、右がカナ。」という言葉が出来るほどに判別は難しく、人類がこれまで幾度と無く挑戦し失敗し諦めてきた大きな課題です。



準備としては、
まずBing Search APIを利用してマナカナの画像を集め、
blog.tottokug.com
Project OxfordのFace APIを使ってかおの画像を切り出しました。
blog.tottokug.com

今日はついにマナカナを判別する所を実装していきます。

実装するは言い過ぎました。実装は特にしません。

Project Oxfordの機能を使って、

といった感じに、Face APIに全てを任せます。

マナカナ判別用教師データの作成。

今回使うのもProject OxfordのFace APIです。
Face APIを使いはじめるところまでは、前回のマナカナの画像からProejctOxfordとimagemagickで顔を切り出す。 - かれ4で書いた通りで、
今回はここからさらにマナカナをマナとカナに分類することをしていきます。

今回は顔を切り出す必要はないので、ImageMagickは使わずにただBounding Boxを一覧化するところまでをおこえば良いのかなと。
気をつけないと行けないのはfaceAPIは秒間1回くらいの利用しか認めておらず、それを超えるとエラーが帰ってくるので要注意です。
なので、sleep 1などを入れて1秒待機させる事にすると回避出来るので、スリープはぜひ入れましょう。

#!/bin/bash
IFS='
'
APIKey=***************************************
[ -d images/detected ] || mkdir -p images/detected
for f in $(find ./images/manakana -type f);
do
  curl -X POST  -H "Content-Type: application/octet-stream" --data-binary @${f} \
  "https://api.projectoxford.ai/face/v0/detections?subscription-key=${APIKey}"  \
  |jq -r ".[]  | [\"$f\",.faceId,.faceRectangle.width,.faceRectangle.height,.faceRectangle.left,.faceRectangle.top] | @csv"  >> faceindex.csv
  mv "${f}" images/detected/
 sleep 1
done

これで faceindex.csvに下記のような

画像ファイル名,faceId,幅,高,x,y

という形式でのCSVファイルが吐き出せました。

"./images/manakana/00011738_02A.jpg","a6415064-2720-4c84-8dff-7701f006f36d",81,81,123,93
"./images/manakana/00011738_02A.jpg","3d957436-a206-4d78-839a-bc0174345f77",77,77,546,144
"./images/manakana/00011738_02A.jpg","2bf5f7dd-c9c5-45e5-acab-6a58b13da0ad",75,75,415,136
"./images/manakana/000504.jpg","0913d773-c52f-45f4-8dce-16ba86b26d42",115,115,28,75
"./images/manakana/001-199x300.jpg","94f18b64-1faf-425f-a064-1a203b8c4140",42,42,87,26
"./images/manakana/001-199x300.jpg","fc8407f4-4722-4e58-ad10-f5e1d1efb733",39,39,34,54
"./images/manakana/001.jpg","87de0b21-ee19-4072-bf7d-b6d14895578c",76,76,96,104
"./images/manakana/0014d0d9.jpg","ca4d66a1-2243-44ac-b084-c1703d419ff6",133,133,145,368
"./images/manakana/0014d0d9.jpg","7a71404c-81b3-4562-8f24-3d78e455b480",122,122,419,370
"./images/manakana/001l.jpg","a967b8e8-a50d-43be-beb7-5d718e0577bd",90,90,199,89

この処理はスリープが入っているので、時間がかかります。
待っている間に、先にPersonGroupの作成とPersonの作成をしてしまいます。
どちらも、繰り返し行う動作ではないのでブラウザでさくっとやってしまいます。
API Referenceから実行出来るようになっているので、そこから実行します。
最近API Referenceから実行出来るようなのが増えていますね。


Person Groupの作成

f:id:tottokug:20151220231830p:plain

f:id:tottokug:20151220231834p:plain

Person の作成

f:id:tottokug:20151220231418p:plain
f:id:tottokug:20151220231421p:plain
f:id:tottokug:20151220231436p:plain

Personができていることの確認

f:id:tottokug:20151220231438p:plain
f:id:tottokug:20151220231442p:plain


学習させる

ここまでで準備は整ったので、ついに学習させていくことになります。
学習させるためには、Face APIのPersonにあるAdd Faceを利用してまずは顔を登録していきます。
今回は顔を検出した結果、2人で写っているものだけを学習用に使い、1人もしくは3人以上で写っている画像を検証用に使います。

2人で写っているものを使う理由として、マナカナには「左:マナ、右:カナ」というルールがあるためです。
ここが今回一番大切な所です。
他にもカナの左目の下にほくろがあるとか、鼻の穴の角度が違うとかそんな見分け方も有りますが、それは今回は使いません。

大切な事なので、もう一度。
「左がマナで、右がカナです。」

言い換えると、Bounding BoxのLeftの値が小さい方がマナ、大きい方がカナです。
というわけで、2人で写っている画像を探し出してきてその画像と一緒に、顔の位置の情報を送ります。


#!/bin/bash
IFS='
'
APIKey=**************************************
personGroupId="manakana"
mana="4314b829-559c-4350-81d5-7bb24b4041e7"
kana="6eb1744b-a7e6-4782-acb4-6a35ec74a8a1"

for file in $(cat faceindex.csv|cut -d',' -f1  |sort |uniq -c  |grep "^\s*2"|perl -pe "s|.*images/manakana(.*?)\"|images/detected\1|g ");
do
  personId=$mana
  for bb in $(cat faceindex.csv |grep  "${file##*/}" |perl -pe "s|.*,(\d+),(\d+),(\d+),(\d+)|\3,\4,\2,\1|g")
  do
    echo "========="
    target=$bb
    eretry=1
    while :
    do
      URL="https://api.projectoxford.ai/face/v1.0/persongroups/${personGroupId}/persons/${personId}/persistedFaces?targetFace=${target}"
      faceId=$(curl -X POST "$URL" \
      -H "Content-Type: application/octet-stream" \
      -H "Ocp-Apim-Subscription-Key: ${APIKey}" \
      --data-binary @./${file} \
      )
      if [ ! -z "$(echo $faceId |grep RateLimitExceeded)"  ]
      then
        eretry=$(expr "$eretry" + "$eretry" )
        echo "retry ${eretry} sec ago"
        sleep ${eretry}
        continue
      fi
      echo "$file,$faceId,$bb" >> registIndex.csv
      break;
    done
    personId=$kana
    sleep ${eretry}
  done
done

ここで指定している

mana="4314b829-559c-4350-81d5-7bb24b4041e7"
kana="6eb1744b-a7e6-4782-acb4-6a35ec74a8a1"

はpersonを作った時に付与されたPersonIdになっています。
また、このAPIは1秒のSleepを入れても、RateLimitExceededが出てしまうので、出てしまった場合は1秒、2秒、4秒と増やしながらリトライする処理が入っています。
ひとまずこれで顔の登録が完了しました。

学習の開始

学習の開始は非常に簡単です。
これも繰り返し行う動作ではないのでブラウザ上で行ってしまいます。
https://dev.projectoxford.ai/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395249/console
こちらから
personGroupIdを指定してsendで完了です。
f:id:tottokug:20151221005302p:plain
Queueに登録するだけなので、202 Acceptedが帰ってきていれば成功です。
f:id:tottokug:20151221005733p:plain

学習の結果確認

こちらも繰り返し行うものではないので、ブラウザ上で確認します。
f:id:tottokug:20151221005736p:plain

f:id:tottokug:20151221005739p:plain

無事に成功しているようです。
今回顔画像をそれぞれ250枚ほど登録していますが、
約1分程度でステータスはSucceededになっていました。

検証

それではドキドキのマナカナが見分けられるかどうかのテストになります。

テストにはファイルを指定して、そのままどっちなのか判定出来るように以下のスクリプトを使います。

#!/bin/bash
IFS='
'
APIKey=****************************************

faceId=$(curl -s -X POST "https://api.projectoxford.ai/face/v1.0/detect?returnFaceId=true" \
-H "Content-Type: application/octet-stream" \
-H "Ocp-Apim-Subscription-Key: ${APIKey}" \
--data-binary @${1} \
| jq -r  .[].faceId)

echo "FACE ID = $faceId"
curl -s -X POST "https://api.projectoxford.ai/face/v1.0/identify" \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: ${APIKey}" \
--data-ascii "{\"personGroupId\":\"manakana\",\"faceIds\":[\"${faceId}\"],\"maxNumOfCandidatesReturned\":1 }" \
| jq -r '.[].candidates[] |[.personId,.confidence] | @csv' \
|perl -pe "s/4314b829-559c-4350-81d5-7bb24b4041e7/mana/g; s/6eb1744b-a7e6-4782-acb4-6a35ec74a8a1/kana/g"

マナの判定

ではまずは、こちらの画像から
f:id:tottokug:20151221012106j:plain

$ ./identify.sh images/detected/mikuramana.jpg
FACE ID = efd9608a-b6ff-47a9-84cc-4107f9789d2d
"mana",0.6302

無事にmanaと判定されました。

カナの判定

次に,この画像から、kanaと判別されれば成功です。

f:id:tottokug:20081227164536j:plain

$ ./identify.sh images/detected/kana.jpg
FACE ID = 6ff3601d-a2f6-4ba5-81e0-0e6a39df2a16
"kana",0.73804


無事に認識されました。


これで、マナカナを見分ける事が出来るようになりました。

クローラを生まれ変わらせるにあたっての思考

Advent Calendar 2015の13日目の記事です。

今うちで動いているCrawlerは数年前に作られたもので、
仕組みとしてはAzureのService Fabricの上でAWSのLambdaを動かしているのと似たアーキテクチャになっている。

そろそろ生まれ変わりの頃かなと思う。

生まれ変わるにあたって、これからのアーキテクチャをどうしようかと悩むわけだけど、
そんな時には過去に遡っていくと次時代へのヒントが転がっていることがよくある。
特に、過去に敗北した素晴らしいアーキテクチャが今の技術であれば一般時にも実現可能になっていたりする。

昔話

昔々世界にはInktomiという会社と
AltaVistaという会社があった。
Inktomiという会社はソフトウェア開発会社で、2002年に米Yahoo!に買収されている。
AltaVista検索エンジンの会社だったが、2003年にOverture社が買収し、
2003年にOverture社を米Yahoo!に買収している。

詳しい歴史等はWikipediaを見てもらえば良いとして、この2社はざっくり言うとどちらもクローラを持ち、
検索サイトとして、もしくは検索エンジンAPI提供として一時代を築いた2つの企業。

この二つの企業では、crawlingとindexingについて大きく違う思想を持ってやっていた。

AltraVistaはDEC Alphaの高性能差をアピールするために生まれたという経緯があり、
1台で完結するように作られたCrawlerが動いていた。
それに対し、Inktomiは完全に分散された環境でスケールする事を意図して作られている。

この2つの会社の事を心に止めておきながら、本題に入っていきたい。

原子的なCrawler

Crawlerについて順を追って、どのようにしていくべきなのかを考えてみる。

その過程でCrawlingとScrapingを分けて考えていかないと行けない。
Crawlingに関して言えば、初期ロボット型検索サイトを作りたいのであればScrapingはせずに、
シンプルに正規表現なんかでタグをはずし、
テキスト部分だけで転置インデックスを作って行くだけでも大規模でなければ実現できる。

それに対してScrapingをする必要があるのは、インデックスを作りたいのではなく、
構造化されたデータとして使いたい場合。

とはいえ、とりあえずCrawlingの話からしよう。
Crawlingと言っても、シンプルな方法からまるで人間がアクセスしているかのような動きをするとか
様々なレイヤーがある。

HTTPでファイルを取得する。

まずシンプルにHTTPでリクエストを送りファイルを保存する事を考えてみる。

  +-------------+    +-----------+
  | HTTP Client | -> | HTML File |
  +-------------+    +-----------+

このようにCrawlerがHTTPRequestを送り、シンプルにHTML、もしくはJsonなどをファイルに保存する方法。
ただ、これでは、CrawlerではなくただのHTTPClientでしかない。

これを実現するためにはwgetというコマンドがある。

再帰的に取得する。

次に再帰的に取得する場合を考えてみる。

  +-------------+    +-----------+    +------------+
  | HTTP Client | -> | HTML File | -> | html parse |
  +-------------+    +-----------+    +------------+
       ^                                v
       +---------------------------------
  1. HTTP ClientがHTMLファイルをダウンロード
  2. そのHTMLをパース
  3. Aタグの文字列を絶対パス(URL)に変換
  4. URLをHTTP Clientが

というループを繰り返すことになる。

そして、これを実現するためにはwgetというコマンドがあり、

  • rというオプションをつけることで可能。

不要なアクセスをなくす。

次に考える事は、どこをたどるかという事。
不要なところにアクセスして、ファイルを取得しても仕方がないので、必要なところだけに絞って次のアクセスをする。

  +-------------+    +-----------+    +------------+
  | HTTP Client | -> | HTML File | -> | html parse |
  +-------------+    +-----------+    +------------+
       ^              +--------+                   v
       +------------- | Filter | -------------------
                      +--------+

HTMLをパースして、Aタグを抜き出すところまではさっきと同じだけれども、その後、HTTP Clientに行く前に
Filterを介すことにする。
そうすることで、不要なアクセスをなくし、効率的にクローリングすることが可能になる。


そして、これを実現するためには、wgetというコマンドがあり、これだけのフィルタオプションがある。

再帰ダウンロード時のフィルタ:
  -A,  --accept=LIST               ダウンロードする拡張子をコンマ区切りで指定する
  -R,  --reject=LIST               ダウンロードしない拡張子をコンマ区切りで指定する
       --accept-regex=REGEX        許容する URL の正規表現を指定する
       --reject-regex=REGEX        拒否する URL の正規表現を指定する
       --regex-type=TYPE           正規表現のタイプ (posix|pcre)
  -D,  --domains=LIST              ダウンロードするドメインをコンマ区切りで指定する
       --exclude-domains=LIST      ダウンロードしないドメインをコンマ区切りで指定する
       --follow-ftp                HTML 文書中の FTP リンクも取得対象にする
       --follow-tags=LIST          取得対象にするタグ名をコンマ区切りで指定する
       --ignore-tags=LIST          取得対象にしないタグ名をコンマ区切りで指定する
  -H,  --span-hosts                再帰中に別のホストもダウンロード対象にする
  -L,  --relative                  相対リンクだけ取得対象にする
  -I,  --include-directories=LIST  取得対象にするディレクトリを指定する
  --trust-server-names             ファイル名としてリダイレクト先のURLの最後の部分を使う
  -X,  --exclude-directories=LIST  取得対象にしないディレクトリを指定する
  -np, --no-parent                 親ディレクトリを取得対象にしない

スクレイピングする

ここまでで、HTMLファイルを再起的に収集することは出来た。

次は取得したHTMLから必要部分を抽出しくことで、やっとクローリングに意味が出てくる。
このHTMLから必要部分を抽出していくことが、スクレイピングなわけだけど、どのように実施していくのが効率的なのだろうか?

これは大きく2つの方法が考えられる。

後になってやっぱり、この項目も抽出したいなんて感じになった場合はCrawlingしたタイミングでのHTMLが必要になる。
もう一度クローリングし直すでも良いかもしれないが、時系列データを収集したい場合なんかはそれではいけない。
その分ストレージ容量を大量に消費するというのがトレードオフになり、それぞれのメリットとデメリットとして存在する。
ここは用途に併せて作るべきと言いたいところだけれども、ストレージをクラウドに任せる事によって
なんとかなるかもしれない。

そして後は抽出後のデータをどこに格納するかという事を考えないと行けない。
今回はCrawlerとScrapingの事を考えないと行けないので、データをどこに保存するのかは
おいておく。

Crawlerをスケールさせる事を考える

次にスケールする場合について考えてみる。
とりあえずオンメモリでやる場合は、メモリ上にしかHTMLが保持されていないので、マシンやプロセスをまたぐことは出来ないと思ったほうがよい。
RDMAなんかでやってやれない事はないが、そんな事を考えるほうがめんどくさい。
というわけでスケールの単位としては以下の図のようになる。

+-- PROCESS -----------------------------------+ +------+
|  +-------------+    +------+    +----------+ | |+----+|
|  | HTTP Client | -> | HTML | -> | Scraping | |-|| DB ||
|  +-------------+    +------+    +----------+ | |+----+|
+----------------------------------------------+ +------+

このような感じでPROCESSを増やすなり、マシンを増やすなりすれば良いので、
コンテナなんかとの相性が良さそうだ。

しかし、ここで考えないと行けないのが、HTTP Client部とScraping部を非同期にするか同期にするか。
一つのProcessがHTTPでの通信とScapingの両方をやる場合、どちらの速度も同じ場合のみ、
非同期でも問題はない。
しかし、HTTPの通信速度は安定するものではないし、HTMLのボリュームによってはScrapingの速度も安定しない。
並列、非同期にするメリットがなく、直列の同期処理にするのが良い。


次に、HTMLを一度ファイルに書き出す方法の場合はどうだろうか。
ここでの前提としては、ファイルはローカルのストレージではなく、クラウド上のObjectStorageに保存するものとする。
この場合のスケールは下の図のようになる。

+---------------+  +--------+  +-------------+
|+-------------+|  |+------+|  |+----------+ |
|| HTTP Client ||->|| HTML ||->|| Scraping | |
|+-------------+|  |+------+|  |+----------+ |
+---------------+  +--------+  +-------------+

HTTP ClientとScraping部が完全に切り離されている。
これによって、HTTP ClientとScrapingのProcessを分離することが可能になっている。
HTMLのファイルを取得するためのIOコストがあるが、HTTP ClientのHTTP通信のコストよりも
低いコストである確率は高い。(ScrapingのプロセスがObjectStrageと同一クラウドに存在する場合。)


しかし、この場合再帰的なCrawlingをする時にScrapingが終わるまで、次のページの取得が始まらない。
それではあまりスケーリングのメリットが享受出来ない可能性が高い。

なので、この構造から少し改良をして、再帰的なクローリングを効率的に行うようにする。
リンクをたどるためにHTMLのParserをHttp Client内部に持ち、Crawlingだけを先に進めていき
Scrapingを非同期に動かすという方法。 下の図のようになる。

+---------------+  +--------+  +-------------+
|+-------------+|  |+------+|  |+----------+ |
|| HTTP Client ||->|| HTML ||->|| Scraping | |
|+-------------+|  |+------+|  |+----------+ |
|    ^    V     |  +--------+  +-------------+
|+-------------+| 
|| HTML Parser ||
|+-------------+|
+---------------+

これによって、HTTPClientとScrapingを完全に分離出来てはいるが、非常に無駄の多い事はすぐに分かると思う。
HTTP Client部とScraping部で、2回HTML Parserが動くことになるからだ。

ここで言うParserと言うのは、きちゃないHTMLを綺麗なHTMLにしてDomParserがすんなり読み込める形にすることをいう。
具体的には、HTMLの整形と文字コードの変換になる。

この無駄に対してのアプローチとしては、非常にシンプルに解決できる。
ParseされたHTMLを保存しておけばいい。

+---------------+
|+-------------+|
|| HTTP Client ||
|+-------------+|
|    ^    V     |  +---------------+  +-------------+
|+-------------+|  |+-------------+|  |+----------+ | 
|| HTML Parser ||->|| Parsed HTML ||->|| Scraping | |
|+-------------+|  |+-------------+|  |+----------+ |
+---------------+  +---------------+  +-------------+

ここで、さっきのwgetでやっていた、無駄なアクセスを減らす動きも加えていきたい。
ここまでの構成だと、HTMLをParseし、そのままHTTP Client部に渡しているので、
間にFilterを挟めば良いだろう。


これによって、無駄なアクセスも省略できるようになってきた。
ここまで来ると、今度はこのParserすら非同期化したくなってくる。

+---------------+
|+-------------+|
|| HTTP Client ||
|+-------------+|
+---^-------v---+
| Queue | Queue |
+---^-------v---+
|+--^---+   v   |
||Filter|   v   |
|+--^---+   v   |
+---^-------v---+  +---------------+  +-------------+
|+--^-------v--+|  |+-------------+|  |+----------+ | 
|| HTML Parser ||->|| Parsed HTML ||->|| Scraping | |
|+-------------+|  |+-------------+|  |+----------+ |
+---------------+  +---------------+  +-------------+

HTTP ClientとParserの間をQueueでつなぎ、Filterを通したものをQueueを通してHTTP Clientに戻す形にする。

ここまで来ると勘の良い人は気づいているかも知れないけれど、
Scraperの準備をする前に、Crawlingだけ開始することができる。

Parsed HTMLはObjectStorage上に保管されているので、定点観測的に使う場合にはすぐにでも
データを収集しておくことが可能だ。

そして、この構造にするともう一つメリットがある。
Scrapingをしている人なら、わかると思うけれど、XPATHで抜くにしろ正規表現で抜くにしろ
何度も試行錯誤をすることが多いと思う。
そのたびにサイトにHTMLを取得しにいっていないだろうか?

もしインメモリでやっていたら、確実にHTMLを何度も取得しにいっているだろう。
この構造になることで、一度HTMLを収集していれば、何度でも試行錯誤し放題になる。



StorageとScrapingの関係。

最後にStorageとScrapingについてもう少し深く掘り下げていきたい。
もし、Scrapingをストリーム処理のように、Crawlingしてきたそばからやっていきたい場合は
Storageに保存されたことにHookさせる事ができるサービスもあるので、随時Scrapingを開始することも可能だ。

Scrapingをストリーム処理する必要がない場合は更に自由度がます。
必要がなくてもストリーム処理しても良いし、蓄積されたParsedHTMLに対して一気にScrapingすることも可能になる。

その場合は、未処理のScraping量に併せてProcessの数を増減させて行けばよい。
Scrapingをしなくていい時は、ScrapingのProcessすべてを停止しておけば良いので、
クラウド上のコンピューティングリソースを使う場合コストメリットも出てくる。
StorageがHDFSなんかだったりしたら、Hadoopで一気にScrapingする事も可能になったりもしてくる。
例えば、

SELECT `span.name`, `span.price` FROM `STORAGEPREFIX.div#listtable ul li `

なんて事も可能に

また、StorageにHTMLを保存し、Scrapingを分離することによってデータ活用のタイミングをずらす事もできるので、
インターネット上の統計的な分析には無駄なコンピューティングリソースを使う事なく、機をうかがう事も可能になる。
半年分の動向とか見たい時なんかには効率的だ。



まとめ

ここまで、クローリングについてどのようなアーキテクチャにしていくのが良いか考えていた事をうぁーと書いてみたけれど、結果どうしたかというと、Crawling部とScraping部を分けた。
それによって、データの使い回しがきくようになったことと、クローリングだけを先に始めることができる事によって、やりたいことを実現するにあたって、
情報の取り逃しを減らすことが可能になった。
Storageコストはかかるが、そこは圧縮するなりコールドストレージに落とすなり、最適化の方法が尽きたわけではない。
というわけで、結局Microserviceのような形に落ち着いた。

で、冒頭に出てきた2つの会社だけれども、特に意味はなく意味深な感じにしたかっただけでした。