読者です 読者をやめる 読者になる 読者になる

Itsukaraの日記

最新IT技術を勉強・実践中。最近はDeep Learningに注力。

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サブドメインにアクセスする際にエラーとなりました。

参考までに、FireFoxcookieの値を見る方法も説明します。ログイン後の画面が表示された状態で、PF12キーを押して「firefox開発ツール」の画面を開き、右上の設定ボタン(歯車の絵)を押し、表示された画面で「ストレージ」にチェックします。すると、一番上のメニューに、「ストレージ」という項目が表示されるので、これを押すと、ストレージ情報が表示されます。その一部としてCookieが表示されます。Cookieはサイト毎に異なるので、調べたいサイトのurlを選んでください。

f:id:Itsukara:20160419002350j:plain

対処方法(裏ワザで徹底対応)

上記では、本来設定したいdomainをcookieに設定できないので、別の解決策を調べました。この記事によると、SeleniumFireFoxドライバを修正すれば、エラーが出なくなるとのこと。少し裏技的な方法ですが、試したところ、うまくいきました。

なお、上記記事では、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));

あとがき

今回は、裏ワザを含めて、いろいろ書きましたが、何が正解か、判断するのが難しいですね。ただ、ログイン後の画像のダウンロードに関しては、今回書いたのが正解と確信しています。