[javascript] インスタグラムのログインをAjaxで突破する方法

最近、開発以来の多いスクレイピング案件でインスタグラムクローリングをやっていた時に、Storiesのスクレイピングをやっている人が世の中にほとんどいないという事実に気がつきました。
インスタのbotシステムはpythonライブラリがgithubで公開されているので、簡単にbotを作りたい人は、下記を参考にしていただくといいでしょう。
https://github.com/instagrambot/instabot
しかし、他のシステムとの連動や、自分プロイグラムとの連携を行いたい場合には、少しばかり扱いづらいライブラリのようなので、ちょこっと参考にさせていただき、独自にjavascriptで構築してみました。
インスタのAPIは使えない
そもそもインスタグラムのデータを取得する時に、すでにあるAPIを使えばいいじゃないかと考えがちですが、2018年4月あたりから機能別に順次廃止されていっているという情報が出回っており、ベンダーや利用者には、インスタから直接メールが送信されているとの事。
こうした処理にAPIを使うのは、確実にデータ取得ができるメリットがあるのですが、
Facebookも含め、APIはとにかく使いにくい
わかりにくい
不具合の保証をしてくれない
という3つの原因があるので、スクレイピングした方が確実という意見もエンジニア内では意外と少なく無い意見のようです。
とはいえ、インスタはAPIが廃止されるので、スクレイピングできなければ色々と不便極まりないので、今回思い立ってみました。
インスタ機能のスクレイピング仕様
インスタグラムでスクレイピングしたい仕様としては、下記のような内容です。
フォロワー数
フォロー数
投稿数
投稿内容
ストーリーズ
これらを最低でも1日に1回定時に取得できると、マーケティングデータとしての利用価値があるという事なのですが、
インスタグラムは、上記の内容は「ストーリーズ」以外はログイン(認証)をしなくても対象のアカウント名さえわかれば取得できてしまいます。
問題はこの「ストーリーズ」データのスクレイピングという事なんですね。
僕も最初は「nodejsなどを使って、ログインページでIDとパスワードをvalueセットして、submitすればいいや」程度に思っていたら、どんでも無い安易な思い込みで、さすがにこれだけでかいサービスであれば、こうしたbotログイン対策もできているようでした。
まず、ログインページでIDとパスワードのinputタグに対して、value値の書き換えを行なっても、サイト側が「クッキーが正常に動作していません」というエラーページが表示されるだけで、うんともすんとも行かないわけです。
そして、前期したpythonライブラリのログイン箇所を見てみて、submitのリクエストヘッダを工夫すればうまくログインができてセッションキープ状態を取得できることが判明したので、今回はその作ったソースコードを載せておきます。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
;(function(){ var config = { username : "*username", password : "*password" }; var $$ = function(){ // ajax-auth $$AJAX({ url : "https://www.instagram.com/accounts/login/ajax/", query : { username : config.username, password : config.password }, headers : { 'Accept' : '*/*', 'Accept-Language' : 'en-US,en;q=0.5', 'Accept-Encoding' : 'gzip, deflate, br', 'Connection' : 'keep-alive', 'Content-Length' : '0', 'Host' : 'www.instagram.com', 'Origin' : 'https://www.instagram.com', 'Referer' : 'https://www.instagram.com/', 'User-Agent' : navigator.userAgent, 'X-Instagram-AJAX': '1', 'Content-Type' : 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken' : $$COOKIE.val("csrftoken") }, onSuccess : function(res){ console.log("response : " + res); } }); }; var $$AJAX = (function(){ var $$ajax = function(options){ if(!options){return} var ajax = new $$ajax; var httpoj = $$ajax.prototype.createHttpRequest(); if(!httpoj){return;} // open メソッド; var option = ajax.setOption(options); // 実行 httpoj.open( option.method , option.url , option.async ); // type if(option.headers){ httpoj = $$ajax.prototype.setHeaders(httpoj , option.headers); } else{ httpoj.setRequestHeader('Content-Type', option.type); } // onload-check httpoj.onreadystatechange = function(){ //readyState値は4で受信完了; if (this.readyState==4){ //コールバック option.onSuccess(this.responseText); } }; // responseType if(typeof option.responseType !== "undefined" && option.responseType){ httpoj.responseType = option.responseType; } //query整形 var data = ajax.setQuery(option); //send メソッド if(data.length){ httpoj.send(data.join("&")); } else{ httpoj.send(); } }; $$ajax.prototype.setHeaders = function(httpoj , headers){ for(var key in headers){ httpoj.setRequestHeader(key , headers[key]); } return httpoj; }; $$ajax.prototype.dataOption = { url:"", query:{}, // same-key Nothing querys:[], // same-key OK data:{}, // ETC-data event受渡用 async:"true", // [trye:非同期 false:同期] method:"POST", // [POST / GET] responseType:"", headers : {}, type:"application/x-www-form-urlencoded", // [text/javascript]... onSuccess:function(res){}, onError:function(res){} }; $$ajax.prototype.option = {}; $$ajax.prototype.createHttpRequest = function(){ //Win ie用 if(window.ActiveXObject){ //MSXML2以降用; try{return new ActiveXObject("Msxml2.XMLHTTP")} catch(e){ //旧MSXML用; try{return new ActiveXObject("Microsoft.XMLHTTP")} catch(e2){return null} } } //Win ie以外のXMLHttpRequestオブジェクト実装ブラウザ用; else if(window.XMLHttpRequest){return new XMLHttpRequest()} else{return null} }; $$ajax.prototype.setOption = function(options){ var option = {}; for(var i in this.dataOption){ if(typeof options[i] != "undefined"){ option[i] = options[i]; } else{ option[i] = this.dataOption[i]; } } return option; }; $$ajax.prototype.setQuery = function(option){ var data = []; if(typeof option.query != "undefined"){ for(var i in option.query){ data.push(i+"="+encodeURIComponent(option.query[i])); } } if(typeof option.querys != "undefined"){ for(var i=0;i<option.querys.length;i++){ if(typeof option.querys[i] == "Array"){ data.push(option.querys[i][0]+"="+encodeURIComponent(option.querys[i][1])); } else{ var sp = option.querys[i].split("="); data.push(sp[0]+"="+encodeURIComponent(sp[1])); } } } return data; }; return $$ajax; })(); var $$COOKIE = (function(){ var $$ = { //init-data options:{ name : 'temporary_cookie', day : 0, hour : 0, min : 1, sec : 0 }, //expires date : function() { var exp = new Date(); exp.setTime(exp.getTime() + (this.options.day * 1000 * 60 * 60 * 24) + (this.options.hour * 1000 * 60 * 60) + (this.options.min * 1000 * 60) + (this.options.sec * 1000)); return exp.toGMTString(); }, //secure-check checkSecure : function() { if (location.href.match(/^https/)) {return true} else {return false} }, set : function(name, val) { if(!name){name = this.options.name} val = this.encode(val); if (this.checkSecure()) { document.cookie = name + "=" + val + ";expires=" + this.date() + ";secure"; } else { document.cookie = name + "=" + val + ";expires=" + this.date(); } }, get : function(name){ return this.val(name); }, val : function(name) { var ck0 = document.cookie.split(" ").join(""); var ck1 = ck0.split(";"); for ( var i = 0; i < ck1.length; i++) { var ck2 = ck1[i].split("="); if (ck2[0] == name) { ck2[1] = this.encode(ck2[1]); return ck2[1]; } } return ''; }, encode:function(val){ if (!val) {return ""} val = val.split("¥r") .join(""); val = val.split("¥n") .join(""); val = val.split("<") .join("-"); val = val.split("%3c").join("-"); val = val.split("%3C").join("-"); val = val.split(">") .join("-"); val = val.split("%3e").join("-"); val = val.split("%3E").join("-"); return val; } }; return $$; })(); return new $$; })(); |
上記ソースコードの上部にある”*username”と”*password”の箇所を自分のインスタアカウントの物をセットすれば用意完了です。
使い方
基本的にGoogleChromeブラウザを使って操作してください。
まず、ブラウザでインスタグラムのログインページを開きます。
https://www.instagram.com/accounts/login/
ブラウザのjavascriptコンソールを開いて、上記ソースコードを貼り付けてください。(IDとパスワードは書き換えてください)
すると、ログイン後のページが開くと思いますが、それで成功です。
このコードを応用した利用例も書いておくので、便利に使ってみてください。
応用例
1. seleniumでjavascriptを使って自動ログイン
おそらくログインページの項目にIDとパスワードを入力する手順で失敗すると思うので、思い切って、このJSを流し込むだけでうまくいきます。
2. ブラウザ機能拡張の対応
機能拡張を使って自動ログインをさせたりするロボティクス操作(自動操作)が可能になります。
3. Nodejsを使ったbotコントロール
ログインできたセッション状態をキープできていれば、Nodejsなどでページ移動しながら、そのページの情報をスクレイピングする事ができるので、自動フォローや、自動投稿なども可能になります。
botが作れちゃうわけですね。
ちょこっと解説
今回のポイントは、ajaxを使ってヘッダ情報を書き換えて送信するという点です。
ヘッダ情報は、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
headers : { 'Accept' : '*/*', 'Accept-Language' : 'en-US,en;q=0.5', 'Accept-Encoding' : 'gzip, deflate, br', 'Connection' : 'keep-alive', 'Content-Length' : '0', 'Host' : 'www.instagram.com', 'Origin' : 'https://www.instagram.com', 'Referer' : 'https://www.instagram.com/', 'User-Agent' : navigator.userAgent, 'X-Instagram-AJAX': '1', 'Content-Type' : 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken' : $$COOKIE.val("csrftoken") }, |
この箇所なのですが、”X-CSRFToken”でサイトアクセス時に書き込まれたcookieをつけてあげるのがポイントですね。
ajaxのヘッダ書き換えを覚えたら、他のサイトでも便利に使えるので、意外といい技術を覚えちゃいましたね。
そして、ここまで記事を読んでくれた人に一つ忠告!
インスタグラムは、サイト規約により、「スクレイピングは禁止!」とのことです。
くれぐれも悪用しないようにしてくださいね。
このソースコードによって生じる問題などは、本サイトならびに僕の方では責任は追えませんので、ご自身の責任という事でご使用ください。
ちなみに、このソースコードMITライセンスにしてますんで。