- node.js爬虫肯定要先安装node.js环境
- 创建一个文件夹
- 在该文件夹打开命令行,执行
npm init
初始化项目
安装依赖
- express 用来搭建一个简单http服务器,也可以使用node原生api
- cheerio 相当于node版的jQuery,用来解析页面
- superagent 用来请求目标页面
- eventproxy 解决同时处理多个页面的问题
创建建好目录
node-spider-csdn
├─ .gitignore
├─ node_modules
├─ README.md
├─ index.js 项目入口
├─ package-lock.json
├─ package.json
└─ routes
└─ csdn.js 爬虫主要代码
创建一个Http服务器
const express = require('express');
const app = express();
app.listen(3000,function() {
console.log('running in http://127.0.0.1:3000');
});
编写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');
});
整体结构
// 引入需要的第三方包
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;
分析页面
- 原创文章的完整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',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`);
}
// ...
});
});
// ...
遍历获取所有页面的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中
// ...
}
});
});
});
});
// ...
解析文章信息
/**
* 解析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);
// ...
}
// ...
处理并发异步操作
// ...
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
});
});
// ...