NodeJS+Seleniumを使った画像ダウンロード(解決編)
前回記事「NodeJS+Seleniumを使った画像ダウンロードで困ったこと(2)」を書いた後で、実践 Selenium WebDriverにざっと目を通していたところ、新展開がありました。特に、ログイン後にアクセスできる画像を、nodeで直接ダウンロードできるようになりました(最後の方に書かれています)。画像ダウンロードに関しては、今回が「解決編」となります。
ログイン後のcookieのファイルへの保管・利用
「実践 Selenium WebDriver」に書かれていたのは、ログイン後のcookieをファイルに保管し、保管したcookieを次回以降で利用してログインを省略する方法です。これを試しましたが、一筋縄ではいかなかったので、順を追って説明します。
まず、本に書かれていた内容をベースに、下記コードを作成しました。
// Code-6.js // url、ID、PASS等は仮の値です var url = "http://bbb.com/"; var ID = "myID"; var PASS = "myPASS"; var COOKIEFILE = "bbb.com-cookie.txt"; var webdriver = require('selenium-webdriver'), By = webdriver.By, until = webdriver.until; var driver = new webdriver.Builder().forBrowser('firefox').build(); var fs = require('fs'); driver.get(url); // "login", "login_id", "password"、"login_button"などは仮の値 driver.findElement(By.linkText("login")) .then(function(login) { if (fs.existsSync(COOKIEFILE)) { // cookieが入ったファイルがある場合:この内容を利用しcookieを設定 var cookie = JSON.parse(fs.readFileSync(COOKIEFILE)); console.log("cookie read from " + COOKIEFILE); for (var i = 0; i < cookie.length; i++) { var ck = cookie[i]; // デバッグ用に、cookieの値を出力 console.log(Object.keys(ck).map(function(k) {return k + '="' + ck[k] + '"';}).join(", ")); // cookieを設定 (expireの値を1000倍するのはdriverの仕様) driver.manage().addCookie(ck.name, ck.value, ck.path, ck.domain, ck.secure, ck.expiry * 1000); } // refresh後は、ログイン後の画面が表示される driver.navigate().refresh(); } else { // cookieが入ったファイルが無い場合:ログイン後にcookieを保管 login.click(); driver.findElement(By.id("login_id")).clear(); driver.findElement(By.id("login_id")).sendKeys(ID); driver.findElement(By.id("password")).clear(); driver.findElement(By.id("password")).sendKeys(PASS); driver.findElement(By.id("login_button")).click(); // cookieをファイルに保管 driver.manage().getCookies().then(function(v) { fs.writeFile(COOKIEFILE, JSON.stringify(v)); }); console.log("cookie saved to " + COOKIEFILE); } }); // 以下、ログイン後の処理を記載
上記でうまくいくはずなのですが、私が試したサイトでは、残念ながら、下記のメッセージが出てエラーとなりました。
throw error; ^ InvalidCookieDomainError: You may only set cookies for the current domain
対処方法(妥協案)
色々と調べてみたところ、cookieに入っているdomainの値が".bbb.com"になっており、FireFox表示中サイトのドメイン名"bbb.com"と異なることが原因でした。対応方法としては、「ck.domain = "";」としてから、addCookie()を呼べば良いとのこと。
// Code-6.jsの修正版の抜粋 for (var i = 0; i < cookie.length; i++) { var ck = cookie[i]; // 下記は、ck.domainが表示中のドメインと異なる場合への対応 ck.domain = ""; // デバッグ用に、cookieの値を出力 console.log(Object.keys(ck).map(function(k) {return k + '="' + ck[k] + '"';}).join(", ")); // cookieを設定 (expireの値を1000倍するのはdriverの仕様) driver.manage().addCookie(ck.name, ck.value, ck.path, ck.domain, ck.secure, ck.expiry * 1000); }
上記修正で、cookieのみでログイン後の画面が表示できるようになりました。念のため、ログイン後にcookieの値を確認したところ、domainは"bbb.com"になっていました。しかし、".bbb.com"(先頭に"."が付いてます)と"bbb.com"では、意味が異なります。前者は、"bbb.com"のサブドメインも含めてcookieが有効であることを示し、後者は"bbb.com"のみで有効であることを示します。そのため、後者では、FireFoxがサブドメインにアクセスする際にエラーとなりました。
参考までに、FireFoxでcookieの値を見る方法も説明します。ログイン後の画面が表示された状態で、PF12キーを押して「firefox開発ツール」の画面を開き、右上の設定ボタン(歯車の絵)を押し、表示された画面で「ストレージ」にチェックします。すると、一番上のメニューに、「ストレージ」という項目が表示されるので、これを押すと、ストレージ情報が表示されます。その一部としてCookieが表示されます。Cookieはサイト毎に異なるので、調べたいサイトのurlを選んでください。
対処方法(裏ワザで徹底対応)
上記では、本来設定したいdomainをcookieに設定できないので、別の解決策を調べました。この記事によると、SeleniumのFireFoxドライバを修正すれば、エラーが出なくなるとのこと。少し裏技的な方法ですが、試したところ、うまくいきました。
なお、上記記事では、Java版のことが書かれています。Windows上のJavascript版のSelenium WebDriver(globalインストールした場合)では、次のようになります。
1. (Javascript版では不要) 2. Goes to folder: C:\Users\USERNAME\AppData\Roaming\npm\node_modules\selenium-webdriver\lib\firefox 3. Decompress webdriver.xip 4. Modified these files: components/command_processor.js and components/driver_component.js 5. Comment lines where Exception is thrown 6. Compress with zip, and rename file to webdriver.xpi
#5に従ってコメントアウトした部分は、2つのファイル共に、下記です。
if (!c.domain) { e = a.session.getWindow().location, c.domain = e.hostname; // } else { // if (-1 == a.session.getWindow().location.host.indexOf(c.domain)) { // throw new WebDriverError(bot.ErrorCode.INVALID_COOKIE_DOMAIN, "You may only set cookies for the current domain"); // } } c.domain.match(/:\d+$/) && (c.domain = c.domain.replace(/:\d+$/, "")); // e = a.session.getDocument(); // if (!e || !e.contentType.match(/html/i)) { // throw new WebDriverError(bot.ErrorCode.UNABLE_TO_SET_COOKIE, "You may only set cookies on html documents"); // }
なお、上記の#6で躓いたので、参考までに書いておきます。修正後のファイルを含めた"webdriver"というディレクトリをzip圧縮・リネームして、元のディレクトリに置いて試したところ、下記のようなエラーが出てしまいました。
throw error; ^ AddonFormatError: Could not find install.rdf in C:\Users\Itsukara\AppData\Roaming\np m\node_modules\selenium-webdriver\lib\firefox\webdriver.xpi
zipファイル中に"install.rdf"も入っているので、原因が分からず、結構悩みました。結局、上記#5の修正を行わずに試してみたところ、それでもエラーになることが分かりました。そこで、元々のwebdriver.xipをLhaForgeで閲覧してみたところ、元々のファイルでは、"webdriver"というディレクトリは含まれておらず、この下のファイル・ディレクトリのみが入っていることが分かりました。
そこで、#5の修正後に、"webdriver"の下のファイル・ディレクトリをzip圧縮し、webdriver.xpiにリネームして、元の場所において試したところ、エラーが出ず、うまくいきました。また、ログイン後のcookieのdomainも、ちゃんと".bbb.com"になっていました。
保管したcookieを使い、nodeで画像ダウンロード
上記で、cookieが得られたので、これをnodeで直接使えないか試したところ、下記コードでうまくいきました。前々回記事で、NodeJS+Seleniumを使った画像ダウンロードのやり方を書きましたが、今回の方が単純で簡単です。お騒がせしましたが、ログインした後にのみアクセスできる画像のダウンロード方法の正解は、下記ですね。
// Code-7.js // url、fname、COOKIEFILEは仮の値です var request = require('request'); var fs = require('fs'); var url = "http://aaa.bbb.com/ddd.jpg"; var fname = "ddd.jpg"; var COOKIEFILE = "bbb.com-cookie.txt"; var cookie = JSON.parse(fs.readFileSync(COOKIEFILE)); console.log("cookie read from " + COOKIEFILE); var jar = request.jar(); for (var i = 0; i < cookie.length; i++) { ck = cookie[i]; var rck = request.cookie(ck.name + "=" + ck.value); jar.setCookie(rck, url); }; // read image and save to file // request.debug = true; // デバッグ出力用 request.get({url: url, jar: jar}).pipe(fs.createWriteStream(fname));
あとがき
今回は、裏ワザを含めて、いろいろ書きましたが、何が正解か、判断するのが難しいですね。ただ、ログイン後の画像のダウンロードに関しては、今回書いたのが正解と確信しています。