概要
目的
node.jsの勉強、特に非同期処理を学びたくてクローラーを自作してみた。
機能
HTMLを指定すると、そのHTMLのタイトルを取得すると同時に、ページ内に存在するリンク(a.href)を探して再帰的にアクセス
最初に指定したURL以外へのリンク(=外部リンク)は対象外にする(でないと無限に検索を続けるから)
一度アクセスしたURLは2度とアクセスしないように注意
GAパラメータ、ページアンカー、index.htmlなどのデフォルトページ、などをURLから取り除いて整形
- 結果はグローバル変数urlsに格納する
苦労したポイント
非同期処理がわかりづらい。クロールしている最中に処理が最後まで流れてしまうのを防止するのが大変だった。
最初はpromiseで実装を試みたけど、途中からasync/awaitに方向転換。
とにかく動作するものが構築できたからよかった
参考リンク
teratailにあげた質問
Node.js - node.jsでpromiseを使って再帰的にURL一覧を作成したい|teratail
maisumakun様にteratailでアドバイスをいただきました。本当にありがとうございます。
非同期と反復
ループと反復処理 - JavaScript | MDN
HTTPリクエストとnode-fetch
node-fetch - npm
Node.jsでAysnc/Awaitを使ってHTTPリクエストを行う5つの方法
NodeJSでAsync/AwaitしてREST API 叩いて json を処理する - Qiita
DOM操作
jsdom - npm
Node.js+https+jsdomで超簡単にHTMLの要素やテキストを調べる方法 | iwb.jp
Node.jsでウェブスクレイピングする色々な方法 - Qiita
コード
/*
* Modules
*/
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
/*
* Config
*/
const BASE_URL = "https://www.example.com/";
const IGNORE_QUERY_PARAMS = ['utm_campaign', "utm_source", "utm_keyword", "utm_content", "utm_medium"];
/*
* Global Object(s)
*/
let urls = {};
/*
* Functions
*/
// 非同期で HTMLのタイトルを取得
const getHtmlByUrl = async(target_url, source_url) => {
try {
const res = await fetch(target_url);
if (!res.ok) throw (res.statusText);
return res.text();
} catch (e) {
var error_msg = 'Fetch failed: "' + target_url + '"' + (source_url ? ' from "' + source_url + '"' : "") + ' ' + e;
console.log(error_msg);
return;
}
};
// URLの有効性をチェック
const isUrlEligible = (url) => {
if (typeof url !== 'undefined' && url.indexOf(BASE_URL) !== 0) return false;
if (url.match(/\.(css|jpg|png|gif|pdf)($|\?.*)/)) return false;
return true;
}
// URLのフォーマット
const formatUrl = (url) => {
var formatted = url;
IGNORE_QUERY_PARAMS.forEach(param => {
var regexp = new RegExp(param + "=[^&]*");
formatted = formatted.replace(regexp, "");
});
formatted = formatted
.replace(/#.*$/, '')
.replace(/[\?&]*$/, '')
.replace(/\/index\.(html?|php|asp|cgi|jsp)\??/, '')
.replace(/\/\/$/, '');
formatted += formatted.match(/(\.(php|html?|jsp|cgi)|\/)$/) ? '' : '/';
return formatted;
}
// 重複を除外したURL一覧を生成
const getLinkUrlsFromDom = (dom) => {
var url_list = {};
dom.window.document.querySelectorAll('a')
.forEach(a => {
if (!isUrlEligible(a.href)) return;
url_list[formatUrl(a.href)] = 1
});
return Object.keys(url_list);
}
// Webページのクロール
const crawlWebPage = async(target_url, source_url) => {
// グローバルオブジェクトの初期化
if (typeof urls[target_url] !== 'undefined') return;
urls[target_url] = {};
// HTMLの取得
const html = await getHtmlByUrl(target_url, source_url);
if (!html) {
urls[target_url].is_success = false;
return;
}
// HTMLのパース
const dom = new JSDOM(html, { url: BASE_URL });
// 取り出したい情報
urls[target_url].title = dom.window.document.querySelector('title').textContent;
// HTMLに含まれるリンクの一覧を生成(重複は除外)
urls[target_url].links = getLinkUrlsFromDom(dom);
for (link of urls[target_url].links) {
await crawlWebPage(link, target_url);
}
}
/*
* main
*/
(async() => {
// URLからページの情報を取得してグローバルオブジェクトを更新
await crawlWebPage(BASE_URL);
// 結果を出力
console.log(urls);
})();
↧