Node.js で簡単なクローラーを作成してWeb サイトの情報を得る その3

[その1](https://qiita.com/yamachan360/items/3ee48de81394db9d3063)、[その2](https://qiita.com/yamachan360/items/d75e0807412e5d990c5b) への反応は薄かったのですが、サイト関係者には喜ばれたので続きを書きますね。

これまでは日本語、英語版の [Code Patterns サイト](https://developer.ibm.com/jp/patterns/) を対象に、Web サイトにありがちな**ページャー機能**を利用してコンテンツ一覧を作成してきました。今回もその路線を継承して少し異なったサイトを対象にしつつ、更により多くの情報を得る方法について考えていきましょう！

# 今回の対象サイト

今回もやはり IBM 関連のサイトで、[IBM Developer サイトのチュートリアル](https://www.ibm.com/developerworks/jp/) (旧 developerWorks 日本語版) を対象にします。10年以上の歴史のある技術系サイトで、過去に何度もお世話になった覚えがあります。

この対象サイトでページャー機能のある、コンテンツ一覧を表示できるのは [技術文書一覧](https://www.ibm.com/developerworks/jp/views/global/libraryview.jsp) です。七千近いコンテンツのリストになっていて、長い歴史の重みを感じますね！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/193519/ff45fb06-2f66-cfbf-fa2b-2caa8c3c43f4.png)
ページ下部にページャー用のモジュールが表示されていることがわかります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/193519/4127a96e-bfff-ff6a-63e7-e72e00d0fbe3.png)

## とりあえず js ファイルを用意

list-ibm-patterns.js ファイルをコピーして list-ibmjp-dw.js というファイルを作成します。とりあえず先頭部分を対象サイトにあわせ書き換えておきましょう。

```javascript:list-ibmjp-dw.js
const client = require('cheerio-httpcli')
const nedb = require('nedb');
const nedb_promise = require('nedb-promise');

if (process.argv.length !== 2) {
	console.log('list-ibmjp-dw.js');
	console.log('  Simple crawler tool to list patterns from IBM developerWorks JP site.');
	console.log('  2019/01/20 by github.com/yamachan');
	return;
}

const nedb_file_name = 'list-ibmjp-dw.nedb';
```

## ページャー機能の確認

まずはページャー機能を確認すると、[その1 の英語サイトと似た](https://qiita.com/yamachan360/items/3ee48de81394db9d3063#%E5%AF%BE%E8%B1%A1%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AEurl%E5%88%86%E6%9E%90) 以下の状態であることがわかります。

* 1ページ目のURLは2ページ目以降と同じでアクセス可能
* 項目数を超えたページは 404 エラーではなく、ボディが空なページが表示される

また表示されるHTMLの構成(DOMツリー)を、ブラウザの検証ツールの Element 表示で確認してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/193519/b9ee7749-58f6-b6bc-c940-bc553e01a463.png)
青枠の部分がtableタグの表になっており、各コンテンツがtrタグの行になっているのがわかりますね。これ、少し古めのサイトによくある形式なのですが、非常にシンプルで処理し易いパターンです。

というわけで、コンテンツの各要素を求めるための query 式と、対象 URL を得るための関数をそれぞれ作成してコードを更新しておきましょう。

```javascript:list-ibmjp-dw.js
const list_item_query = '.ibm-col-1-1 > .ibm-data-table tbody tr';

function getListURL(_page) {
	let start = (_page - 1) * 100 + 1;
	return 'https://www.ibm.com/developerworks/jp/views/global/libraryview.jsp?site_id=60&contentarea_by=global&sort_by=&sort_order=2&start=' + start + '&end=' + (start + 99) + '&type_by=%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E7%A8%AE%E9%A1%9E&show_abstract=true';
}
```

## 各コンテンツ項目のデータ化

さて、1つのコンテンツに注目してみましょう。以下のようなDOMツリーが確認できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/193519/ac73f95a-1778-8833-40d6-d6c186491a3d.png)
1～5の番号を振ったそれぞれの値を、以下の `itemToObject()` 関数でそれぞれ、オブジェクトの属性値として設定していきます。番号ごとに確認してみてください。

```javascript:list-ibmjp-dw.js
function tconv(_text) {
	return (_text||'').replace('\t', '  ').replace('\n', ' ').replace('\r', '').trim();
}
function itemToObject(_ret, _item) {
	return {
		title: tconv(_ret.$('a', _item).text()), // (2)
		url: _ret.$('a', _item).url(), // (1)
		description: tconv(_ret.$('td:first-child div', _item).text()), // (3)
		type: tconv(_ret.$('td:nth-child(2)', _item).text()), // (4)
		date: tconv(_ret.$('td:nth-child(3)', _item).text()) // (5)
	};
}
```

概要に不要な空白が含まれているようでしたので、前後の空白文字を削除したり、改行コードを削除するための `tconv()` 関数を今回追加して使用しています。

上記のキャプチャしたコンテンツの場合、以下のようなデータ用オブジェクトが作成されるでしょう。

```json
{
  "title":"TensorFlow 入門",
  "url":"http://www.ibm.com/developerworks/jp/cognitive/library/cc-get-started-tensorflow/index.html",
  "description":"TensorFlow は数ある機械学習用オープンソース・ソフトウェア・ライブラリーのうちの 1 つに過ぎません。このチュートリアルで、TensorFlow の概要とこれをサポートしているプラットフォーム、そして TensorFlow をインストールする際の考慮事項を把握してください。",
  "type":"記事",
  "date":"2018/7/19"
}
```

また生成されるデータ用オブジェクトの形式にあわせ、`checkUpdateObject()` 関数のほうも更新しておきます。

```javascript:list-ibmjp-dw.js
function checkUpdateObject(_obj, _doc) {
	return JSON.stringify(_obj) !== JSON.stringify({
		title: _doc.title,
		url: _doc.url,
		description: _doc.description,
		type: _doc.type,
		date: _doc.date
	});
}
```

## ひとまず完成

と、対象サイトに対するコード修正が完了したので、これで [list-ibmjp-dw.js](https://github.com/yamachan/simple-web-crawlers/blob/master/list-ibmjp-dw.js) ツールは、とりあえず完成です。

```
node list-ibmjp-dw.js
```

とツールを実行し、list-ibmjp-dw.nedb ファイルが生成されることを確認してみてください。

### 数があわない？

2019年1月20日現在、コンテンツは6793件あると表示され、ツールでも6793個のオブジェクトが処理されます。が、実際にlist-ibmjp-dw.nedb ファイルを確認すると6791行しかなく、2つ足りません。

確認のため `processListItems()` 関数を以下のように変更し、ADDしたログを表示せず、またUpdateは必ず実施するように変更して、新たにツールを実行してみると

```javascript:list-ibmjp-dw.js
async function processListItems(_ret, _items, _page) {
	for (let loop = 0; loop < _items.length; loop++) {
		let obj = itemToObject(_ret, _items[loop]);
		let doc = await db.findOne({url: obj.url});
		if (doc) {
			if (true || checkUpdateObject(obj, doc)) {
				await db.update({url: obj.url}, {$set: obj});
				console.log('DB_UPDATE: ' + obj.url);
			}
		} else {
			await db.insert(obj);
			//console.log('DB_ADD: ' + obj.url);
		}
	}
}
```

以下のようなログ表示が得られました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/193519/adb9ce4a-2c0c-d70c-0277-0e230e9c3408.png)
これら3つのコンテンツに関して調べてみると、どうやら以下のような状況のようでした。

* コンテンツの総数は6790件で、加えて3件の重複したインデックスが存在する
* うち重複した2つは全ての値が一致するため処理をスキップされていた
* うち重複した1つは概要などが異なりUPDATE処理されていた<br/>(注：nedbは更新でも行が増えるため。後述のnedb_openを使用すれば1行減る)

基本的にツールはURLベースでオブジェクトを処理しているため、これらの重複は対応可能で、特に問題はありません。ただ、Webページに表示されている総件数より少ないコンテンツを対象とすることになりますので、そこだけ注意が必要ですね。

## 引き続き拡張していきます

今回のサイトはコンテンツ数が多く、またそれに関連して、一覧画面に表示される情報が十分とは言えません。より詳細なコンテンツは、各ページにアクセスして入手するしかなさそうです。

ちょっと長くなってきましたので、この拡張は後編として、別にまとめたいとおもいます。

# CSV形式に変換する補助ツール

以前にnedb形式のファイルを [json形式に変換](https://qiita.com/yamachan360/items/3ee48de81394db9d3063#json-%E5%BD%A2%E5%BC%8F%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B) するサンプルツールをご紹介したのですが、例の知り合いから「csv形式で欲しいんだけど」と相談があり、さくっと作成してみましたので、ついでにご紹介します。

```javascript:nedb2csv.js
const nedb = require('nedb');
const nedb_promise = require('nedb-promise');

if (process.argv.length !== 3 || process.argv[2].startsWith('-')) {
	console.log('nedb2csv.js');
	console.log('  Convert a nedb file to a csv file.');
	console.log('  2018/12/24 by github.com/yamachan\n');
	console.log('  node nedb2csv list-ibmjp-patterns.nedb > list-ibmjp-patterns.csv');
	return;
}

const db = new nedb_promise({
	filename: process.argv[2],
	autoload: true
});

// ----- Main Loop -----
(async () => {
	let docs = await db.cfind({}).exec();
	// ----- List up col labels -----
	let cols = [];
	docs.forEach(doc => {
		for (let key in doc) {
			if (key !== '_id' && !cols.includes(key)) {
				cols.push(key);
			}
		}
	});
	// ----- Output CSV data -----
	console.log('"' + cols.join('","') + '"');
	docs.forEach(doc => {
		let line = cols.map(col => (doc[col]||'').toString().replace(/"/g,'""'));
		console.log('"' + line.join('","') + '"');
	});
})();
```

[nedb2csv.js](https://github.com/yamachan/simple-web-crawlers/blob/master/nedb2csv.js) として、GitHub 上のリポジトリにも追加してあります。

文字コードが UTF-8 のままなので、EXCEL ではそのまま読むと文字化けしてしまうかもしれません。その場合は何らかのツールで SJIS に変換するか、EXCELで空のシートを開いて、データとして CSV ファイルをインポートしてみてください。

# nedb ファイルを圧縮する補助ツール

上記の nedb2csv や以前に紹介した nedb2json ツールを実施する前に、nedb ファイルをいったん読み込んで内容を圧縮する簡易ツールです。

その正体は、対象となる nedb ファイルを開いて、アイテム数をカウントするだけの非常に簡単なツール。nedb はnedb ファイルを単に読み込むだけで、重複を除去して圧縮してくれる仕組みになっています。

[nedb_open.js](https://github.com/yamachan/simple-web-crawlers/blob/master/nedb_open.js) として、GitHub 上のリポジトリにも追加してあります。

例えば今回作成したツールの結果を csv 形式で得たい場合、以下のように順に実行すると良いでしょう。

```
node list-ibmjp-dw
node nedb_open list-ibmjp-dw.nedb
node nedb2csv list-ibmjp-dw.nedb > list-ibmjp-dw.json
```

# というわけで

今回はこれまでのツールをベースに、歴史のある IBM developerWorks 日本語版サイト (今はIBM Developer サイト) を対象にして、六千を超えるインデックスを処理してみました。

次回も引き続き、同じサイトを対象に、更に詳しい情報をクロールしていく拡張を実施していく予定です。