Itsukaraの日記

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

NodeJS+Seleniumを使った画像ダウンロードで困ったこと(2)

前回記事「NodeJS+Seleniumを使った画像ダウンロードで困ったこと - Itsukaraの日記」の補足+追加です。

WebElementが無い場合があり、エラーとなる

某サイトでは、同じような構成のページが複数あるのですが、ページによってはWebElement(例えば<a href="#notall">*1など)が無いことがあり、findElement()でエラーとなります。ところが、Seleniumでは、WebElementがあるか無いを判定するAPIが無い模様。

調べた結果、複数のWebElementを返すfindElements()を使えば、エラーにならずに対応できる模様(WebElementが無い場合は、エラーにならず、長さが0の疑似配列が返される)。結果、次のようなコードとなります。

driver.findElements(By.css('a[href="#notall"]'))
.then(function(element_array) {
  if (element_array.length > 0) {
    // <a href="#notall">に対する処理
  }
});

ディレクトリが作れずエラーになる

fs.mkdirSync()でディレクトリを作成できるのですが、既に存在する場合はエラーになります。また、ディレクトリ名に特殊な文字(Windowsでは\/:*?"<>|の何れか)が含まれるとエラーとなります(ファイル書き込みも同様)。これらに対応できるように、次のような関数を作りました*2

// ファイル名中の特殊文字を"-"で置き換える
function f_normalize(fname) {
  return fname.replace(/\\|\/|\:|\*|\?|\"|\<|\>|\|/g, "-");
}

// ディレクトリ名中の特殊文字を"-"で置き換える
function d_normalize(dname) {
  return dname.replace(/\\|\:|\*|\?|\"|\<|\>|\|/g, "-");
}

// ディレクトリが存在しない場合は作成する
function mkdirSyncIfNotExit(dir) {
  dir = d_normalize(dir);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
  }
}

// 使い方の例
var d1 = "テスト / test", d2 = "case1";
d1 = f_normalize(d1);
d2 = f_normalize(d2);
mkdirSyncIfNotExit(d1);
mkdirSyncIfNotExit(d1 + "/" + d2);

var fname ="a/b/c/d.xml";
fname = f_normalize(fname);
fs.writeFile(fname, "ファイルに書かれるテキスト");

ブラウザ側で実行するスクリプトが時間切れエラーになる

executeScript()やexecuteAsyncScript()により、ブラウザ側でスクリプトを実行することができるのですが、スクリプトの実行時間が長いと、時間切れになりエラーとなります。これは、setScriptTimeout()で設定を変えることができます。

// スクリプトの時間切れを検出する上限値を設定(ミリ秒単位)
driver.manage().timeouts().setScriptTimeout(10000);

driver.executeAsyncScript(remote_func)
.then(function(content) {
  // ブラウザから戻ってきた値に対する処理
});

ブラウザで実行するスクリプトデバッグができない

nodeで実行されるスクリプトは、node-inspectorをインストールすることにより、node-debugでデバッグできるのですが、ブラウザで実行するスクリプトデバッグできません*3

// node-debugの使い方
F:\WORK> npm install node-inspector -g
F:\WORK> node-debug Code-5.js

これに関しては、ブラウザでスクリプトを実行する直前で長時間待ち状態にするスクリプト("driver.sleep(1000000)"など)を入れ、ブラウザが待ち状態になったところで、ブラウザのDeveloper toolsを開いて、実行するスクリプトを流し込んで試すのが良いです。ブラウザに渡す引数の設定なども含めて流し込む必要がありますが、これで、エラーの状況が正確に分かりますし、対応方法を検討できます。

driver.sleep(1000000);

// ブラウザ側でエラーが生じる部分 => ブラウザのDeveloper toolsでデバッグ
driver.executeAsyncScript(remote_func)
.then(function(content) {
  // ブラウザから戻ってきた値に対する処理
});

なお、ブラウザがchromeの場合は、Developer toolsのSourcesのSnippetで、好きなコードを書いて保存したり、実行したり、少し修正して再実行したりが簡単にできるので、便利です。

  • Snippetの実行(現在のWebページの環境で実行)

f:id:Itsukara:20160415014415j:plain

  • Snippetの保存(保管場所はChromeが勝手に確保)

f:id:Itsukara:20160415014416j:plain

WebElement.click()の後の待ち時間が分からない

WebElementをclickし、画面が変化した後で、ブラウザからデータの抽出行いたいのだが、そのための待ち時間が分かりませんでした。そこで、適当に1秒待てば良いだろうと考え、下記のようにしていました。

driver.findElements(By.css('aaa[bbb="ccc"]')).click();
driver.sleep(1000).
.then(function() {
  driver.executeAsyncScript(remote_func)
  .then(function(content) {
    // ブラウザから戻ってきた値に対する処理
  });
});

ただ、これでは、時間が適当すぎますし、このような書き方が適切か自信がありませんでした。いろいろ考えた結果、次のようにしました。つまり、click()してぺーじの内容が変化した後に出現するユニークなWebElementを確認し、これが出現したら次の処理を行うようにしました。この結果、次のようにしました*4

driver.findElements(By.css('aaa[bbb="ccc"]')).click();
// 'ddd[eee="fff"]'は、click()後に初めて出現するWebElementのセレクタ
driver.findElement(By.css('ddd[eee="fff"]'));
.then(function() {
  driver.executeAsyncScript(remote_func)
  .then(function(content) {
    // ブラウザから戻ってきた値に対する処理
  });
});

あとがき

前回も今回も、Selenium使いには当たり前のことが多かったかもしれません。寄り道しなくて済むように、Seleniumに関して体系的に勉強したいと強く思いました。Seleniumの情報ソースとしてWebばかり探していましたが、本屋に行ったらオライリーから本が出ているのですね。図書館で借りるか、購入して勉強しようと思います。

実践 Selenium WebDriver

実践 Selenium WebDriver

*1:はてな記法で"<>"を半角にするプレビューがおかしくなるため、全角にしています。

*2:元々のファイル名が"/a/b/c.d"といった名称で、a, bがディレクトリ名称を表す場合は、後で、"-a-b-c.d"を"/a/b/c.d"にリネームする必要があります。このようなファイルが多数あっても、Flexible Renamehttp://www.vector.co.jp/soft/winnt/util/se131133.htmlを使うと、一発でリネーム出来ます。なお、リネームされるファイルの一覧が表示されるので、元々"/"がディレクトリの意味以外で使われていたファイルは除外するようにしてください。

*3:ブラウザ側で実行するスクリプトのエラーが表示される点は、CasperJSよりも親切。

*4:元々このような書き方が当たり前なのかもしれませんが、体系的に勉強していないので、分かっていませんでした。