普段、クローラー開発をメインに仕事をしている。
業務上、データを収集するために様々なサイトにアクセスする機会があり、日々アクセスブロックと格闘している。
今回は自分の備忘録も兼ねて、スクレイピングでアクセスブロックに合わないための技術や対処法をまとめておく。
扱うスクレイピング技術はRubyのopen-uri、curlコマンド、selenium。
改めて言うまでもないが、クロール先サイトの規約や関連法律の遵守はしよう。
#アクセスブロックされたときにチェックする項目
アクセスブロックには様々な種類があり、適切な手段をもって回避する
##IP
1:アクセス頻度
頻繁にサイト側にリスエストを送信すると、ロボット判定される可能性がある。
解決策:sleep時間を増やす。クロール回数*sleep=所要時間<許容所要時間に収まる範囲で、sleepは長く取る。
2:同じペースでリスエストを送信すると、ロボット判定される可能性がある。
解決策:ランダムなsleep時間を設定する。
3:同一IPから同一サイトにリクエストを頻繁に送信した場合、IP単位でブロックされる可能性がある。
解決策:1IPあたりの連続接続可能回数・秒数を調べて、必要ならSSHサーバーを踏み台にしてIPローテーションする。
別々のサイトであってもCDNがAkamai共通で、AkamaiにIPブロックされた場合はどちらのサイトもブロックされる可能性がある。
# rubyのopen-uri
socksify gem
Socksify::proxy("127.0.0.1", port) { open(url).read }
# curl
curl -x socks5h://loalhost:port url
# selenium
selenium_option = [ --proxy-server=socks5://127.0.0.1:port ]
4:特定のIPからのアクセスをブロックするサイトがある。
解決策:サーバーとしてデータセンター(IDCFやAWS)を使っている場合、SSHサーバーを踏み台にしてIPをプロバイダ等を変えてみる。
##HTTP Header
1:デフォルトHTTP Header
リクエストを受け取るサーバー側は、不自然なHTTP Headerのリクエストがきたら怪しいリクエストだと認識して必要に応じてブロックするなどの処理をする。open-uriやcurl等のデフォルトHTTP Headerは明らかにBOTだとわかる。
解決策:User-AgentやAccept-Language等をブラウザでアクセスしたときと同じものを指定する。
ブラウザでアクセスしたときのHeaderは、Chrome→Developer tool→Networkタブ→Copy→Copy as cURLからコピーして確認する。
2:同一ユーザからのアクセス頻度
HTTP Header(やIP)から、同一ユーザーが過剰なアクセスをしているとサーバー側が判断した場合、ブロックされる可能性がある。
解決策:User-Agent等をローテーションしてみる。
##Captcha
BOTの動きを判別して、人間が簡単に解決できる問題を解かせるサイトがある。
解決策1:Captchaテストをトリガーしない。
sleepを長くする、IPを変える、seleniumを使っているならwebdriverプロパティを削除する、headlessのオプションを外す等、様々な条件を試す。
解決策2:自動的にテストを解く。
Captha突破API、もしくは機械学習またはディープラーニングスキルを使用してこのチェックに合格する画像認識技術を使う。
#補足
##webdriverプロパティを削除する
seleniumを使っている場合、次のコードで簡単に自動操作していることがわかってしまう。
var isAutomated = navigator.webdriver;
if(isAutomated){
blockAccess();
}
webdriverプロパティを削除するには次のコードを実行する。
delete Object.getPrototypeOf(navigator).webdriver;
##headlessのオプションを外す
seleniumを使っている場合、headlessで実行するとサイト側にブロックされることがある。
headlessは人間でない証であり、headlessかどうかは次のコードで簡単にわかるからだ。
ditsilやgeetest系のボット防止技術を使っているサイトだと、headlessでは無理で、サーバーで実行する場合はGUI付きが必要。
navigator.permissions.query({name:'notifications'}).then(function(permissionStatus) {
if(Notification.permission === 'denied' && permissionStatus.state === 'prompt') {
console.log("Headless Chrome");
} else {
console.log("Not Headless Chrome");
}
});
##CDNがAkamaiか確認する
digコマンドで、ドメイン名からIPアドレスを調べる
dig ドメイン名
例:dig www.armaniexchange.com
CDNがAkamaiなら、Answerセクションにakamaiedgeの記載がある。
##IPアドレスからドメイン名を調べる
digコマンドで、IPアドレスから逆引きする
dig -x IPアドレス
##ポートフォワーディングができているかを確認する
digコマンドで自分のグローバルIPアドレスを確認して、digコマンドの-xオプションでドメインを逆引きする。
Answerセクションのドメイン名でポートフォワーディングができているか判断する。
IDCF:idcfcloud.net.
AWS EC2:amazonaws.com.
フレッツ:mesh.ad.jp.
##SSHサーバーを踏み台として使う
SSHサーバーを踏み台にすれば、接続先のWebサーバーにSSHサーバーのIPアドレスから接続してきたと思わせることができる。SSHサーバーの「ダイナミックフォワード」という機能を使ってSSHサーバーまでの「トンネル」を作り、「SOCKSプロキシ」として利用する。
ssh -D ポート番号 ユーザー名@ホスト名
##ランダムにSOCKSプロキシのポートを取り出す
IPローテーションする際、複数のSOCKSプロキシを立ち上げて、ランダムにポートを取り出す必要がある。
dynamic_portsメソッドを実行すると、Linuxコマンドpgrep -fal ssh
でsshサーバーを抽出し、SOCKSプロキシのポートを配列で返すため、sampleメソッドでランダムに一つだけ取り出せば良い。
def dynamic_ports
lines = `pgrep -fal ssh`.split("\n")
ports = []
lines.each do |line|
opts = line.split
d_options_index = opts.find_index("-D")
if d_options_index.present? && d_options_index > 0
next if opts[d_options_index + 1].to_i <= 0
ports << opts[d_options_index + 1].to_i
end
end
ports
end
##SOCKSプロキシの種類
ローカルでの名前解決
curl --socks5 localhost:port
サーバーでの名前解決
curl --socks5-hostname localhost:port
curl -x socks5h://localhost:post
chrome_option = [ --proxy-server="socks5://127.0.0.1:port" ]
#参考文献
10 Ways to hide your Bot Automation from Detection | How to make Selenium undetectable and stealth
どこへ行っても安心!SSHサーバーを踏み台にしてWebアクセスする方法
#さいごに
アクセスブロックはサイトによって様々であり、その対策も千差万別である。
スクレイピングできないサイトはない!と言える技術力を身に着けたい。