前書き
NightmareJSを触ってみたら面白かったので少し趣味と絡めてみました。
ver.2からElectronベースのブラウザを表示できるようになったり、ES6で書けるようになったりと扱いやすくなってます。
今回作ったものは実用性があるかと言われれば正直微妙なので、あくまでNightmareJSの紹介・入門記事と思ってください。
要件
・ロドストの日記ページから日記を検索。
・検索の際に指定する情報は以下の通り。
項目 | 入力値 |
---|---|
キーワード | 4.4 |
タグ | 攻略, 固定, 零式・絶 |
データセンター | Gaia |
・検索結果の内、最新5件の日記内容のスクリーンショットを撮り保存する。
・実行した月・日・時間を元にフォルダを作成し、そこにスクリーンショットを保存する。
準備
前提としてNode.jsが必要なので、まだPCに入っていないという方はインストールしておきましょう。
あとはnpmでNightmareJSをインストールすれば、ひとまず必要なものは揃います。
npm install nightmare
処理内容
今回はff_diary.js
という名前でJSファイルを用意しました。
中身はこんな感じになっています(詳細はこの後解説します)。
手っ取り早く動きを確認したい人は丸っとコピーして$ node ff_diary.js
って叩けば動くはず。
const fs = require('fs');
const NIGHTMARE = require('nightmare');
const SCREEN_WIDTH = 960;
const SCREEN_HEIGHT = 540;
const APP = NIGHTMARE({
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
show: true, // ブラウザを表示するか
enableLargerThanScreen: true // ブラウザを画面より大きく出来るか
});
let scrapingCount = 1;
const SCRAPING_LIMIT = 5;
// ロドストにアクセスして日記を検索
const SEARCH = () => {
APP.goto('https://jp.finalfantasyxiv.com/lodestone/blog/')
.wait('.form__horizontal')
.click('.icon-btn__search_28')
.insert("input[placeholder='キーワードを入力']", false)
.insert("input[placeholder='キーワードを入力']", '4.4')
.select('.sys_tag_presets', '攻略')
.select('.sys_tag_presets', '固定')
.select('.sys_tag_presets', '零式・絶')
.select('select[name="worldname"', '_dc_Gaia')
.click('input[value="検索"]');
CHECK();
};
// 日記詳細をチェック
const CHECK = () => {
SHOW_DETAIL().then(r => {
SCREENSHOT(r);
});
};
// 詳細を表示
const SHOW_DETAIL = () => {
return new Promise((resolve, reject) => {
APP.wait('.ldst__contents')
.click(
`.entry__block__wrapper .entry__blog_block:nth-child(${scrapingCount}) .entry__blog_block__box a`
)
.wait('.ldst__window')
.evaluate(() => {
const $BODY = document.querySelector('body');
// ページ全体の幅と高さを返す
return {
width: $BODY.scrollWidth,
height: $BODY.scrollHeight
};
})
.then(result => {
resolve(result);
});
});
};
// スクリーンショット保存
const SCREENSHOT = r => {
const TODAY = new Date();
const MONTH = TODAY.getMonth() + 1;
const DATE = TODAY.getDate();
const HOUR = TODAY.getHours();
const FOLDER_NAME = `${MONTH}月${DATE}日${HOUR}時`;
// フォルダが無ければ作成
if (!fs.existsSync(`./${FOLDER_NAME}`)) {
fs.mkdirSync(`./${FOLDER_NAME}`);
}
APP.viewport(r.width, r.height)
.wait(1000)
.screenshot(`./${FOLDER_NAME}/screenshot${scrapingCount}.jpg`)
.then(() => {
if (scrapingCount === SCRAPING_LIMIT) {
APP.end();
} else {
scrapingCount += 1;
APP.viewport(SCREEN_WIDTH, SCREEN_HEIGHT).back();
CHECK();
}
});
};
// 実行開始
SEARCH();
初期設定
まずは初期設定部分から。
const fs = require('fs');
const NIGHTMARE = require('nightmare');
const SCREEN_WIDTH = 960;
const SCREEN_HEIGHT = 540;
const APP = NIGHTMARE({
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
show: true, // ブラウザを表示するか
enableLargerThanScreen: true // ブラウザを画面より大きく出来るか
});
let scrapingCount = 1;
const SCRAPING_LIMIT = 5;
最初にfs
とnightmare
をrequire
しています。
fs
はNode.jsで使用できるファイルシステム関数です(詳細は省きます)。
今回は後述するフォルダの存在確認と作成に使用しています。
ブラウザのデフォルトサイズを変数に入れた後、NightmareJSのオプションを指定しています。
width
、height
でブラウザのサイズを指定。
show
をtrue
にすると、実行時にElectronベースのブラウザが立ち上がるので挙動を見ることができます。別に見えなくていいよって人はfalse
に。
enableLargerThanScreen
は、true
にしておけばブラウザのサイズを画面サイズより大きくできます。
今回はページ全体のスクリーンショットを撮りたいのでtrue
にします。
scrapingCount
とSCRAPING_LIMIT
は、何件の日記を表示したか判定するために用意しています。
日記の検索
お次は日記の検索処理です。
// ロドストにアクセスして日記を検索
const SEARCH = () => {
APP.goto('https://jp.finalfantasyxiv.com/lodestone/blog/')
.wait('.form__horizontal')
.click('.icon-btn__search_28')
.insert("input[placeholder='キーワードを入力']", false)
.insert("input[placeholder='キーワードを入力']", '4.4')
.select('.sys_tag_presets', '攻略')
.select('.sys_tag_presets', '固定')
.select('.sys_tag_presets', '零式・絶')
.select('select[name="worldname"', '_dc_Gaia')
.click('input[value="検索"]');
CHECK();
};
// 日記詳細をチェック
const CHECK = () => {
SHOW_DETAIL().then(r => {
SCREENSHOT(r);
});
};
goto()
で指定のURLに遷移し、wait()
で指定の要素が読み込まれるまで待機します。
要素の指定はjQueryライクな書き方ができます。
今回指定している.form__horizontal
はこの部分です。
虫眼鏡アイコンをクリックする度にフォーム部分の表示・非表示が切り替わる(デフォルト非表示)ようなので、.form__horizontal
が読み込まれたら虫眼鏡アイコンをclick()
。
これでフォーム部分が表示されました。
あとはinsert()
でキーワードの部分に文字を入れ、select()
でタグとデータセンターを選択して検索ボタンをclick()
です。
insert()
の処理が.insert("input[placeholder='キーワードを入力']", '4.4')
の一文だけだと、テキストボックスにフォーカスされるだけで値が入力されないことがあったため、第二引数にfalse
を指定した処理を先に行うことで回避しています。
select()
の引数は、(select要素のセレクタ, option要素のvalue値)
を渡してあげます。
※type()
でもテキストボックスに値を入力することができます。type()
では指定の文字が一文字ずつ入力され、insert()
ではまとめて入力されます。状況に応じて使い分けましょう。
日記の詳細を表示
日記を検索できたら次は詳細ページにアクセスします。
// 詳細を表示
const SHOW_DETAIL = () => {
return new Promise((resolve, reject) => {
APP.wait('.ldst__contents')
.click(
`.entry__block__wrapper .entry__blog_block:nth-child(${scrapingCount}) .entry__blog_block__box a`
)
.wait('.ldst__window')
.evaluate(() => {
const $BODY = document.querySelector('body');
// ページ全体の幅と高さを返す
return {
width: $BODY.scrollWidth,
height: $BODY.scrollHeight
};
})
.then(result => {
resolve(result);
});
});
};
検索結果画面に遷移中なので、ひとまず.ldst__contents
が読み込まれるまで待ちます。
.ldst__contents
より下のバナー、フッターエリアは必要ないのでそこまで読み込みを待つ必要はありません。
読み込めたらn番目の記事のリンクをclick()
し、また指定の要素が読み込まれるまで待ちます。
.ldst__window
が読み込まれていれば、その中に記事とコメントが含まれているので大丈夫そうです。
この読み込みが完了したらevaluate()
内でページ全体のサイズを取得し、後述するスクリーンショットを撮る関数に渡します。
evaluate()
では、NightmareJSで動いているブラウザの中で実行されるJSを書くことができます。
よって、scrollWidth
、scrollHeight
で今表示している日記詳細ページのサイズが取得できます。
スクリーンショットの撮影・保存
日記詳細ページが表示されたらスクリーンショットを撮影し、保存します。
// スクリーンショット保存
const SCREENSHOT = r => {
const TODAY = new Date();
const MONTH = TODAY.getMonth() + 1;
const DATE = TODAY.getDate();
const HOUR = TODAY.getHours();
const FOLDER_NAME = `${MONTH}月${DATE}日${HOUR}時`;
// フォルダが無ければ作成
if (!fs.existsSync(`./${FOLDER_NAME}`)) {
fs.mkdirSync(`./${FOLDER_NAME}`);
}
APP.viewport(r.width, r.height)
.wait(1000)
.screenshot(`./${FOLDER_NAME}/screenshot${scrapingCount}.jpg`)
.then(() => {
if (scrapingCount === SCRAPING_LIMIT) {
APP.end();
} else {
scrapingCount += 1;
APP.viewport(SCREEN_WIDTH, SCREEN_HEIGHT).back();
CHECK();
}
});
};
最初に今日の月、日、時間を取得してフォルダ名を設定し、それを元にfs.existsSync()
でフォルダの存在を確認。
もしフォルダが無ければfs.mkdirSync()
で作成します。
フォルダが出来たらviewport()
でブラウザのサイズをページ全体のサイズへ変更し、念のために1秒待ってからスクリーンショットを撮影・保存します。
カウントが上限に達していればend()
で処理を終了。
まだ上限になっていなければカウントを1プラスし、viewport()
でデフォルトのブラウザサイズに戻します。
ここでブラウザサイズを戻しておかないと、次の記事のスクリーンショットを撮る時に余分な空白が生まれてしまう可能性があります。
(例:記事1の中身の高さが5000px、記事2の中身の高さが3000pxだった時、ブラウザサイズを元に戻しておかないと高さ5000pxの状態のブラウザで記事2にアクセスしてしまうので、scrollHeight
の値が5000pxになってしまい2000pxの余白ができる。)
その後back()
で一つ前の画面(検索結果画面)に戻り、日記の詳細表示→スクリーンショットの撮影・保存を再度実行します。
おまけ
以上で日記の検索→詳細表示→スクリーンショットの撮影・保存という一通りの流れが完成しましたが、別にこれやらなくても直接日記見に行けばいいじゃんって感じなので定期的にnodeのコマンドを実行できるようにしてみましょう。
watchコマンドのインストール
定期的にコマンドを実行する方法はいくつかありますが、今回は手っ取り早くwatch
コマンドにしたいと思います。
まずはwatch
コマンドをインストールしましょう。
# Homebrewが無い場合は先にHomebrewをインストール
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# watchのインストール
$ brew install watch
watchコマンドの実行
とりあえず今回は1時間毎に実行するようにしたいと思います。
実行間隔の設定は-n
オプションで設定できます。
# 1時間毎に実行(間隔の単位は秒)
$ watch -n 3600 node ff_diary.js
他にもエラー時にビープ音を鳴らす-b
、出力結果に色を付ける-c
など色々オプションがありますが今回は特に必要ないので省略。
これで1時間毎に自動で日記を検索し、最新の5件の内容をスクリーンショットで保存しておいてくれるようになりました。
・・・やっぱり直接日記見ればよくない?