最近正在學習node.js,就像搞一些東西來玩玩,於是這個簡單的爬蟲就誕生了。 ...
最近正在學習node.js,就像搞一些東西來玩玩,於是這個簡單的爬蟲就誕生了。
準備工作
- node.js爬蟲肯定要先安裝node.js環境
- 創建一個文件夾
- 在該文件夾打開命令行,執行
npm init
初始化項目
正式開始
安裝依賴
- express 用來搭建一個簡單http伺服器,也可以使用node原生api
- cheerio 相當於node版的jQuery,用來解析頁面
- superagent 用來請求目標頁面
- eventproxy 解決同時處理多個頁面的問題
直接使用npm install express cheerio superagent eventproxy
來安裝依賴包,當然你也可以用別的方法。
創建建好目錄
node-spider-csdn
├─ .gitignore
├─ node_modules
├─ README.md
├─ index.js 項目入口
├─ package-lock.json
├─ package.json
└─ routes
└─ csdn.js 爬蟲主要代碼
創建一個Http伺服器
在index.js
文件中,實例化一個express
對象,啟動一個Http服務
const express = require('express');
const app = express();
app.listen(3000, function() {
console.log('running in http://127.0.0.1:3000');
});
這樣就啟動了一個簡單的Http本地服務,執行node index.js
後通過http://127.0.0.1:3000
就可以訪問到這個伺服器。有關Express的更多內容可以參考官方文檔。
編寫csdn.js
模塊
先引入csdn.js
文件並且添加路由
const express = require('express');
const csdn = require('./routes/csdn.js');
const app = express();
app.use(csdn);
app.listen(3000, function() {
console.log('running in http://127.0.0.1:3000');
});
然後開始編寫csdn.js
整體結構
// 引入需要的第三方包
const cheerio = require('cheerio');
const superagent = require('superagent');
const express = require('express');
const eventproxy = require('eventproxy');
const router = express.Router(); // 掛載路由
const ep = new eventproxy();
router.get('/csdn/:name', function(req, res) {
const name = req.params.name; // 用戶id
// 具體實現...
});
// 將router暴露出去
module.exports = router;
分析頁面
整體結構寫好後就要開始分析CSDN用戶文章頁面的HTML了。
隨便找一個人的博客,經過觀察發現:
- 原創文章的完整url:
https://blog.csdn.net/l1028386804/article/list/2?t=1
- CSDN的文章列表是40篇一頁
- 分頁控制項是動態生成的,所以無法直接通過HTML解析獲得
然後我們通過開發者工具查看文章列表結構,可以發現:
- 文章信息都在類名為
article-item-box
的盒子中 - id信息在該盒子的
data-articleid
屬性中
還有一些其他的信息都很容易能查到,比如博主原創文章總數值等,可以在以後需要的時候再過來查看。
獲取所有文章頁面
因為無法直接獲得分頁信息,所以我們通過文章總數 / 每頁文章數
來獲取所有的頁面。
首先獲取文章的總數:
/**
* 獲取總文章數目
* @param {String} url 頁面路徑
* @param {Function} callback 回調
*/
let getArticleNum = function (url, callback) {
superagent.get(url).end(function (err, html) {
if (err) {
console.log(`err = ${err}`);
}
let $ = cheerio.load(html.text);
let num = parseInt($('.data-info dl').first().attr('title'));
callback(num);
});
};
然後利用簡單的迴圈獲取所有文章頁面:
// ...
router.get('/csdn/:name', function(req, res) {
const name = req.params.name;
getArticleNum(`https://blog.csdn.net/${name}`, function (num) {
let pages = []; // 保存要抓取的頁面
let pageNum = Math.ceil(num / 40); // 計算一共有多少頁面
for (let i = 1; i <= pageNum; i++) {
pages.push(`https://blog.csdn.net/${name}/article/list/${i}?t=1`);
}
// ...
});
});
// ...
我們可以通過console.log()
或者res.send()
來查看獲取的網址是否正確
遍歷獲取所有頁面的HTML
// ...
router.get('/csdn/:name', function (req, res) {
const name = req.params.name;
getArticleNum(`https://blog.csdn.net/${name}`, function (num) {
let pages = [];
let articleData = []; // 保存所有文章數據
let pageNum = Math.ceil(num / 40); // 計算一共有多少頁面
for (let i = 1; i <= pageNum; i++) {
pages.push(`https://blog.csdn.net/${name}/article/list/${i}?t=1`);
}
// 獲取所有頁面的文章信息
pages.forEach(function (targetUrl) {
superagent.get(targetUrl).end(function (err, html) {
if (err) {
console.log(`err ${err}`);
}
let $ = cheerio.load(html.text);
// 當前頁面的文章列表
let articlesHtml = $('.article-list .article-item-box');
// 遍歷當前頁的文章列表
for (let i = 0; i < articlesHtml.length; i++) {
// 解析獲取文章信息
// push到articleData中
// ...
}
});
});
});
});
// ...
解析文章信息
因為獲取到的有些文本中空格太多,所以需要用到正則表達式來去除多餘的空格。
cheerio
對於Document的操作和jQuery
基本一樣,所以有前端基礎的可以很輕鬆上手。
/**
* 解析html字元串,獲取文章信息
* @param {String} html 包含文章信息的html
* @param {Number} index 文章索引
*/
let analysisHtml = function (html, index) {
return {
id: html.eq(index).attr('data-articleid'),
title: html.eq(index).find('h4 a').text().replace(/\s+/g, '').slice(2),
link: html.eq(index).find('a').attr('href'),
abstract: html.eq(index).find('.content a').text().replace(/\s+/g, ''),
shared_time: html.eq(index).find('.info-box .date').text().replace(/\s+/, ''),
read_count: html.eq(index).find('.info-box .read-num .num').first().text().replace(/\s+/, ''),
comment_count: html.eq(index).find('.info-box .read-num .num').last().text().replace(/\s+/, '')
};
};
// ...
// 遍歷當前頁的文章列表
for (let i = 0; i < articlesHtml.length; i++) {
let article = analysisHtml(articlesHtml, i);
articleData.push(article);
// ...
}
// ...
我們已經獲取到所有文章的信息數據,但是因為獲取各個頁面的文章時是併發非同步進行的,所以要同時利用這些數據特殊的方法。
處理併發非同步操作
這裡我使用的是“計數器”eventproxy
,還有很多其他的方法都可以解決這個問題。
// ...
pages.forEach(function (targetUrl) {
superagent.get(targetUrl).end(function (err, html) {
if (err) {
console.log(`err ${err}`);
}
let $ = cheerio.load(html.text);
let articlesHtml = $('.article-list .article-item-box');
for (let i = 0; i < articlesHtml.length; i++) {
let article = analysisHtml(articlesHtml, i);
articleData.push(article);
ep.emit('blogArtc', article); // 計數器
}
});
});
// 當所有'blogArtc'完成後,觸發回調
ep.after('blogArtc', num, function (data) {
res.json({
status_code: 0,
data: data
});
});
// ...
這樣,一個簡單的node爬蟲就寫好了,執行node index.js
啟動服務後,在瀏覽器中輸入http://127.0.0.1:3000/csdn/xxxx
就可以獲得xxxx(這是id)的全部文章了。