抓取首页文章列表20条数据
根目录创建一个app.js文件。
实现思路步骤
引入依赖
定义一个地址
发起请求
页面数据解析
分析页面数据
生成数据
1. 引入依赖:
const superagent = require('superagent');
const cheerio = require('cheerio');
2. 定义一个地址
const reptileUrl = "http://www.jianshu.com/";
3. 发起请求
superagent.get(reptileUrl).end(function (err, res) {
// 抛错拦截
if(err){
return throw Error(err);
}
// 等待 code
});
这个时候我们会向简书首页发一个请求,只要不抛错,走if,那么就可以继续往下看了。
4. 页面数据解析
superagent.get(reptileUrl).end(function (err, res) {
// 抛错拦截
if(err){
return throw Error(err);
}
/**
* res.text 包含未解析前的响应内容
* 我们通过cheerio的load方法解析整个文档,就是html页面所有内容,可以通过console.log($.html());在控制台查看
*/
let $ = cheerio.load(res.text);
});
注释已经说明这行代码的意思,就不在说明了。就下了就比较难了。
5. 分析页面数据
你需在浏览器打开简书官网,简书是后台渲染部分可见的数据,后续数据是通过ajax请求,使用js填充。我们爬数据,一般只能爬到后台渲染的部分,js渲染的是爬不到,如果ajax,你可以直接去爬api接口,那个日后再说。
言归正传,简书首页文章列表,默认会加载20条数据,这个已经够我用了,你每次刷新,如果有更新就会更新,最新的永远在最上面。
这20条数据存在页面一个类叫.note-list的ul里面,每条数据就是一个li,ul父级有一个id叫list-container,学过html的都知道id是唯一,保证不出错,我选择id往下查找。
$('#list-container .note-list li')
上面就是cheerio帮我们获取到说有需要的文章列表的li,是不是和jq写一样。我要获取li里面内容就需要遍历 Element.each(function(i, elem) {}) 也是和jq一样
$('#list-container .note-list li').each(function(i, elem) {
// 拿到当前li标签下所有的内容,开始干活了
});
以上都比较简单,复杂的是下面的,数据结构。我们需要怎么拼装数据,我大致看了一下页面,根据经验总结了一个结构,还算靠谱。
{
id: 每条文章id
slug:每条文章访问的id (加密的id)
title: 标题
abstract: 描述
thumbnails: 缩略图 (如果文章有图,就会抓第一张,如果没有图就没有这个字段)
collection_tag:文集分类标签
reads_count: 阅读计数
comments_count: 评论计数
likes_count:喜欢计数
author: { 作者信息
id:没有找到
slug: 每个用户访问的id (加密的id)
avatar:会员头像
nickname:会员昵称(注册填的那个)
sharedTime:发布日期
}
}
基本数据结构有了,先定义一个数组data,来存放拼装的数据,留给后面使用。
随便截取一条文章数据
xxxxxxxxx...
我们就拿定义的数据结构和实际的页面dom去一一比对,去获取我们想要的数据。
id: 每条文章id
li上有一个 data-note-id="12732916"这个东西就是文章的id,
怎么获取:$(elem).attr('data-note-id'),这样就完事了
slug:每条文章访问的id (加密的id)
如果你点文章标题,或者带缩略图的位置,都会跳转一个新页面 http://www.jianshu.com/p/xxxxxx 这样的格式。标题是一个a链接,链接上有一个href属性,里面有一段 /p/xxxxxx 这样的 /p/是文章详情一个标识,xxxxxx是标识哪片文章。而我们slug就是这个xxxxxx,就需要处理一下。$(elem).find('.title').attr('href').replace(//p//, ""),这样就可以得到xxxxxx了。
title: 标题
这个简单,$(elem).find('.title').text()就好了。
abstract: 描述
这个简单,$(elem).find('.abstract').text()就好了。
thumbnails: 缩略图 (如果文章有图,就会抓第一张,如果没有图就没有这个字段)
这个存在.wrap-img这a标签里面img里,如果没有就不显示,$(elem).find('.wrap-img img').attr('src'),如果取不到就是一个undefined,那正合我意。
下面4个都在.meta的div里面 (我没有去打赏的数据,因为我不需要这个数据)
collection_tag:文集分类标签
有对应的class,$(elem).find('.collection-tag').text()
reads_count: 阅读计数
这个就比较麻烦了,它的结构是这样的
414
还要有一个字体图标的class可以使用,不然还真不好玩,那需要怎么获取了,$(elem).find('.ic-list-read').parent().text(),先去查找这个字体图标i标签,然后去找它的父级a标签,获取里面text文本,标签就不被获取了,只剩下数字。
接下来2个一样处理的。
comments_count: 评论计数
$(elem).find('.ic-list-comments').parent().text()
likes_count:喜欢计数
$(elem).find('.ic-list-like').parent().text()
接来就是会员信息,全部都在.author这个div里面
id:没有找到
slug: 每个用户访问的id (加密的id)
这个处理方式和文章slug一样,$(elem).find('.avatar').attr('href').replace(//u//, ""),唯一不同的需要吧p换成u。
avatar:会员头像
$(elem).find('.avatar img').attr('src')
nickname:会员昵称(注册填的那个)
昵称存在一个叫.blue-link标签里面,$(elem).find('.blue-link').text()
sharedTime:发布日期
这个发布日期,你看到页面是个性化时间,xx小时前啥的,如果直接取就是一个坑爹的事了,在.time的span上有一个data-shared-at="2017-05-24T08:05:12+08:00"这个才是正真的时间,你会发现它一上来是空的,是js来格式化的。$(elem).find('.time').attr('data-shared-at')
以上就是所有字段来源的。接下来要说一个坑爹的事,text()获取出来的,有回车符/n和空格符/s。所以需要写一个方法把它们去掉。
function replaceText(text){
return text.replace(/\n/g, "").replace(/\s/g, "");
}
组装起来的数据代码:
let data = [];
// 下面就是和jQuery一样获取元素,遍历,组装我们需要数据,添加到数组里面
$('#list-container .note-list li').each(function(i, elem) {
let _this = $(elem);
data.push({
id: _this.attr('data-note-id'),
slug: _this.find('.title').attr('href').replace(/\/p\//, ""),
author: {
slug: _this.find('.avatar').attr('href').replace(/\/u\//, ""),
avatar: _this.find('.avatar img').attr('src'),
nickname: replaceText(_this.find('.blue-link').text()),
sharedTime: _this.find('.time').attr('data-shared-at')
},
title: replaceText(_this.find('.title').text()),
abstract: replaceText(_this.find('.abstract').text()),
thumbnails: _this.find('.wrap-img img').attr('src'),
collection_tag: replaceText(_this.find('.collection-tag').text()),
reads_count: replaceText(_this.find('.ic-list-read').parent().text()) * 1,
comments_count: replaceText(_this.find('.ic-list-comments').parent().text()) * 1,
likes_count: replaceText(_this.find('.ic-list-like').parent().text()) * 1
});
});
let _this = $(elem); 先把$(elem);存到一个变量里面,jq写习惯了。
有几个*1是吧数字字符串转成数字,js小技巧,不解释。
6. 生成数据
数据已经可以获取了,都存在data这个数据里面,现在是20条数据,我们理想的数据,那么放在node里面,我们还是拿不到,怎么办,一个存在数据库(还没有弄到哪里,我都还没有想好怎么建数据库表设计),一个就存在本地json文件。
那就存在本地json文件。nodejs是一个服务端语言,就说可以访问本地磁盘,添加文件和访问文件。需要引入nodejs内置的包fs。
const fs = require('fs');
它的其他用法不解释了,只说一个创建一个文件,并且在里面写内容
这是写文件的方法:
fs.writeFile(filename,data,[options],callback);
/**
* filename, 必选参数,文件名
* data, 写入的数据,可以字符或一个Buffer对象
* [options],flag 默认‘2’,mode(权限) 默认‘0o666’,encoding 默认‘utf8’
* callback 回调函数,回调函数只包含错误信息参数(err),在写入失败时返回。
*/
我们需要这样来写了:
// 写入数据, 文件不存在会自动创建
fs.writeFile(__dirname + '/data/article.json', JSON.stringify({
status: 0,
data: data
}), function (err) {
if (err) throw err;
console.log('写入完成');
});
注意事项
我方便管理数据,放在data文件夹,如果你也是这样,记得一定先要在根目录建一个data文件夹不然就会报错
默认utf-8编码;
写json文件一定要JSON.stringify()处理,不然就是[object Object]这货了。
如果是文件名可以直接article.json会自动生成到当前项目根目录里,如果要放到某个文件里,例如data,一定要加上__dirname + '/data/article.json'。千万不能写成3. 如果是文件名可以直接article.json会自动生成到当前项目根目录里,如果要放到某个文件里,例如data,一定要加上__dirname + '/data/article.json'。千万不能写成'/data/article.json'不然就会抛错,找不到文件夹,因为文件夹在你所在的项目的盘符里。例如G:/data/article.json。
以上基本就完成一个列表页面的抓取。看下完整代码:
/**
* 获取依赖
* @type {*}
*/
const superagent = require('superagent');
const cheerio = require('cheerio');
const fs = require('fs');
/**
* 定义请求地址
* @type {*}
*/
const reptileUrl = "http://www.jianshu.com/";
/**
* 处理空格和回车
* @param text
* @returns {string}
*/
function replaceText(text) {
return text.replace(/\n/g, "").replace(/\s/g, "");
}
/**
* 核心业务
* 发请求,解析数据,生成数据
*/
superagent.get(reptileUrl).end(function (err, res) {
// 抛错拦截
if (err) {
return throw Error(err);
}
// 解析数据
let $ = cheerio.load(res.text);
/**
* 存放数据容器
* @type {Array}
*/
let data = [];
// 获取数据
$('#list-container .note-list li').each(function (i, elem) {
let _this = $(elem);
data.push({
id: _this.attr('data-note-id'),
slug: _this.find('.title').attr('href').replace(/\/p\//, ""),
author: {
slug: _this.find('.avatar').attr('href').replace(/\/u\//, ""),
avatar: _this.find('.avatar img').attr('src'),
nickname: replaceText(_this.find('.blue-link').text()),
sharedTime: _this.find('.time').attr('data-shared-at')
},
title: replaceText(_this.find('.title').text()),
abstract: replaceText(_this.find('.abstract').text()),
thumbnails: _this.find('.wrap-img img').attr('src'),
collection_tag: replaceText(_this.find('.collection-tag').text()),
reads_count: replaceText(_this.find('.ic-list-read').parent().text()) * 1,
comments_count: replaceText(_this.find('.ic-list-comments').parent().text()) * 1,
likes_count: replaceText(_this.find('.ic-list-like').parent().text()) * 1
});
});
// 生成数据
// 写入数据, 文件不存在会自动创建
fs.writeFile(__dirname + '/data/article.json', JSON.stringify({
status: 0,
data: data
}), function (err) {
if (err) throw err;
console.log('写入完成');
});
});
一个简书首页文章列表的爬虫就大工告成了,运行代码,打开Terminal运行node app.js或者node app都行。或者在package.json的scripts对象下添加一个"dev": "node app.js",然后用webstorm的npm面板运行。
有文章列表就有对应的详情页面,后面继续讲解怎么爬详情。