29

最近在捣鼓一个仿简书的开源项目,从前端到后台,一战撸到底。就需要数据支持,最近mock数据,比较费劲。简书的很多数据都是后台渲染的,很难快速抓api请求数据,本人又比较懒,就想到用写个简易爬虫系统。

项目初始化

安装nodejs,官网中文网。根据自己系统安装,这里跳过,表示你已经安装了nodejs。

选择一款顺手拉风的编辑器,用来写代码。推荐webstorm最近版。

webstorm创建一个工程,起一个喜欢的名字。创建一个package.json文件,webstorm快捷创建package.json非常简单。还是用命令行创建,打开Terminal,默认当前项目根目录,npm init,一直下一步。

可以看这里npm常用你应该懂的使用技巧

主要技术栈

  • superagent 页面数据下载

  • cheerio 页面数据解析

这是2个npm包,我们先下载在接着继续,下载需要时间的。

npm install superagent cheerio --save

接下啦简单说说这2个是啥东西

superagent 页面数据下载

superagent是nodejs里一个非常方便的客户端请求代码模块,superagent是一个轻量级的,渐进式的ajax API,可读性好,学习曲线低,内部依赖nodejs原生的请求API,适用于nodejs环境下。

请求方式

  • get (默认)

  • post

  • put

  • delete

  • head

语法:request(RequestType, RequestUrl).end(callback(err, res));

写法:

request
    .get('/login')
    .end(function(err, res){
        // code
    });

设置Content-Type

  • application/json (默认)

  • form

  • json

  • png

  • xml

  • ...

设置方式:

1. 
request
    .get('/login')
    .set('Content-Type', 'application/json');
2. 
request
    .get('/login')
    .type('application/json');
3. 
request
    .get('/login')
    .accept('application/json');

以上三种方效果一样。

设置参数

  • query

  • send

query

设置请求参数,可以写json对象或者字符串形式。

json对象{key,value}

可以写多组key,value


request
    .get('/login')
    .query({
        username: 'jiayi',
        password: '123456'
    });
字符串形式key=value

可以写多组key=value,需要用&隔开


request
    .get('/login')
    .query('username=jiayi&password=123456');

sned

设置请求参数,可以写json对象或者字符串形式。

json对象{key,value}

可以写多组key,value


request
    .get('/login')
    .sned({
        username: 'jiayi',
        password: '123456'
    });
字符串形式key=value

可以写多组key=value,需要用&隔开


request
    .get('/login')
    .sned('username=jiayi&password=123456');

上面两种方式可以使用在一起


request
    .get('/login')
    .query({
        id: '100'
    })
    .sned({
          username: 'jiayi',
          password: '123456'
      });

响应属性Response

Response text

Response.text包含未解析前的响应内容,一般只在mime类型能够匹配text/json、x-www-form-urlencoding的情况下,默认为nodejs客户端提供,这是为了节省内存,因为当响应以文件或者图片大内容的情况下影响性能。

Response header fields

Response.header包含解析之后的响应头数据,键值都是node处理成小写字母形式,比如res.header('content-length')。

Response Content-Type

Content-Type响应头字段是一个特列,服务器提供res.type来访问它,默认res.charset是空的,如果有的化,则自动填充,例如Content-Type值为text/html;charset=utf8,则res.type为text/html;res.charset为utf8。

Response status

http响应规范

cheerio 页面数据解析

cheerio是一个node的库,可以理解为一个Node.js版本的jquery,用来从网页中以 css selector取数据,使用方式和jquery基本相同。

  • 相似的语法:Cheerio 包括了 jQuery 核心的子集。Cheerio 从jQuery库中去除了所有 DOM不一致性和浏览器尴尬的部分,揭示了它真正优雅的API。

  • 闪电般的块:Cheerio 工作在一个非常简单,一致的DOM模型之上。解析,操作,呈送都变得难以置信的高效。基础的端到端的基准测试显示Cheerio 大约比JSDOM快八倍(8x)。

  • 巨灵活: Cheerio 封装了兼容的htmlparser。Cheerio 几乎能够解析任何的 HTML 和 XML document。

需要先loading一个需要加载html文档,后面就可以jQuery一样使用操作页面了。

const cheerio = require('cheerio');
const $ = cheerio.load('<ul id="fruits">...</ul>');
$('#fruits').addClass('newClass');

基本所有选择器基本和jQuery一样,就不一一列举。具体怎么使用看官网

上面已经基本把我们要用到东西有了基本的了解了,我们用到比较简单,接下来就开始写代码了,爬数据了哦。

抓取首页文章列表20条数据

根目录创建一个app.js文件。

实现思路步骤

  1. 引入依赖

  2. 定义一个地址

  3. 发起请求

  4. 页面数据解析

  5. 分析页面数据

  6. 生成数据

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,来存放拼装的数据,留给后面使用。

随便截取一条文章数据

<li id="note-12732916" data-note-id="12732916" class="have-img">
    <a class="wrap-img" href="/p/b0ea2ac2d5c4" target="_blank">
      <img src="//upload-images.jianshu.io/upload_images/1996705-7e00331b8f3dbc5d.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/375/h/300" alt="300" />
    </a>
  <div class="content">
    <div class="author">
      <a class="avatar" target="_blank" href="/u/652fbdd1e7b3">
        <img src="//upload.jianshu.io/users/upload_avatars/1996705/738ba2908445?imageMogr2/auto-orient/strip|imageView2/1/w/96/h/96" alt="96" />
</a>      <div class="name">
        <a class="blue-link" target="_blank" href="/u/652fbdd1e7b3">xxx</a>
        <span class="time" data-shared-at="2017-05-24T08:05:12+08:00"></span>
      </div>
    </div>
    <a class="title" target="_blank" href="/p/b0ea2ac2d5c4">xxxxxxx</a>
    <p class="abstract">
     xxxxxxxxx...
    </p>
    <div class="meta">
        <a class="collection-tag" target="_blank" href="/c/8c92f845cd4d">xxxx</a>
      <a target="_blank" href="/p/b0ea2ac2d5c4">
        <i class="iconfont ic-list-read"></i> 414
</a>        <a target="_blank" href="/p/b0ea2ac2d5c4#comments">
          <i class="iconfont ic-list-comments"></i> 2
</a>      <span><i class="iconfont ic-list-like"></i> 16</span>
        <span><i class="iconfont ic-list-money"></i> 1</span>
    </div>
  </div>
</li>

我们就拿定义的数据结构和实际的页面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: 阅读计数

这个就比较麻烦了,它的结构是这样的

<a target="_blank" href="/p/b0ea2ac2d5c4">
        <i class="iconfont ic-list-read"></i> 414
</a>

还要有一个字体图标的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('写入完成');
});

注意事项

  1. 我方便管理数据,放在data文件夹,如果你也是这样,记得一定先要在根目录建一个data文件夹不然就会报错

  2. 默认utf-8编码;

  3. 写json文件一定要JSON.stringify()处理,不然就是[object Object]这货了。

  4. 如果是文件名可以直接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面板运行。

有文章列表就有对应的详情页面,后面继续讲解怎么爬详情。

抓取首页文章列表对应的20条详情数据

有了上面抓取文章列表的经验,接下来就好办多了,完事开头难。

实现思路步骤

  1. 引入依赖

  2. 定义一个地址

  3. 发起请求

  4. 页面数据解析

  5. 分析页面数据

  6. 生成数据

1. 引入依赖

这个就不用引入,在一个文件里面,因为比较简单的,代码不多,懒得分文件写。导入导出模块麻烦,人懒就这样的。

但我们需要写一个函数,来处理爬详情的方法。

function getArticle(item){
   // 等待code
}

2. 定义一个地址

注意这个地址,是有规律的,不是随便的地址,随便点开一篇文章就可以看到地址栏,http://www.jianshu.com/p/xxxxxx, 我们定义的reptileUrl = "http://www.jianshu.com/";那么就需要拼地址了,还记得xxxxxx我们存在哪里吗,存在slug里面。请求地址:reptileUrl + 'p/' + item.slug

3. 发起请求

superagent.get(reptileUrl + 'p/' + item.slug).end(function (err, res) {
    // 抛错拦截
     if(err){
         return throw Error(err);
     }
});

你懂的

4. 页面数据解析

superagent.get(reptileUrl + 'p/' + item.slug).end(function (err, res) {
    // 抛错拦截
     if(err){
         return throw Error(err);
     }
   /**
   * res.text 包含未解析前的响应内容
   * 我们通过cheerio的load方法解析整个文档,就是html页面所有内容,可以通过console.log($.html());在控制台查看
   */
   let $ = cheerio.load(res.text);
});

5. 分析页面数据

你可能会按上面的方法,打开一个页面,然后就去获取标签上面的class,id。我开始也在这个上面遇到一个坑,页面上有阅读 ,评论 ,喜欢 这三个数据,我一开始以为都是直接load页面就有数据,在获取时候,并没有数据,是一个空。我就奇怪,然后我就按了几次f5刷新,发现问题了,这几个数据的是页面加载完成以后才显示出来的,那么就是说这个有可能是js渲染填充的。那就说明的我写的代码没有错。

有问题要解决呀,如果是js渲染,要么会有网络加载,刷新几次,没有这个数据,那就只能存在页面里,写的内联的script标签里面了,右键查看源码,ctrl+f搜索,把阅读 ,评论 ,喜欢的数字,随便挑一个,找到了最底部data-name="page-data"的script标签里面,有一个json对象,里面有些字段,和我文章列表定义很像,就是这个。有了这个就好办了,省的我去截取一大堆操作。

解析script数据

let note = JSON.parse($('script[data-name=page-data]').text());

script里面数据

{"user_signed_in":false,"locale":"zh-CN","os":"windows","read_mode":"day","read_font":"font2","note_show":{"is_author":false,"is_following_author":false,"is_liked_note":false,"uuid":"7219e299-034d-4051-b995-a6a4344038ef"},"note":{"id":12741121,"slug":"b746f17a8d90","user_id":6126137,"notebook_id":12749292,"commentable":true,"likes_count":59,"views_count":2092,"public_wordage":1300,"comments_count":29,"author":{"total_wordage":37289,"followers_count":221,"total_likes_count":639}}}

把script里面内容都获取出来,然后用 JSON方法,字符串转对象。

接下来依旧是要定义数据结构:

article: {   文章信息
     id:  文章id
     slug:  每条文章访问的id (加密的id)
    title: 标题
    content: 正文(记得要带html标签的)
    publishTime: 更新时间
     wordage: 字数
     views_count: 阅读计数
    comments_count: 评论计数
    likes_count: 喜欢计数
},
author: {
    id: 用户id
   slug: 每个用户访问的id (加密的id)
   avatar: 会员头像
   nickname: 会员昵称(注册填的那个)
   signature: 会员昵称签名
   total_wordage: 总字数
   followers_count: 总关注计数
   total_likes_count: 总喜欢计数
}                           

还要专题分类和评论列表我没有累出来,有兴趣可以自己去看看怎么爬出来。它们是单独api接口,数据结构就不需要了。

因为有了note 这个对象很多数据都简单了,还是一个一个说明来源

article 文章信息

id: 文章id

主要信息都存在note.note里面,文章id就是note.note.id,

slug: 每条文章访问的id (加密的id)

note.note.slug

title: 标题
所有的正文都存在.post下的.article里,那么获取title就是$('div.post').find('.article .title').text()

content: 正文(记得要带html标签的)

注意正文不是获取text文本是要获取html标签,需要用到html来获取而不是text,$('div.post').find('.article .show-content').html() 返回都是转义字符。到时候前端需要处理就会显示了。虽然我们看不懂,浏览器看得懂就行了。

publishTime: 更新时间

这时间直接显示出来了,不是个性化时间,直接取就好了$('div.post').find('.article .publish-time').text()

wordage: 字数

这个是一个标签里面<字数 1230>这样的,我们肯定不能要这样的,需要吧数字提取出来,$('div.post').find('.article .wordage').text().match(/d+/g)[0]*1 用正则获取数字字符串,然后转成数字。

views_count: 阅读计数

note.note.views_count

comments_count: 评论计数

note.note.comments_count

likes_count: 喜欢计数

note.note.likes_count

author 用户信息

id: 用户id

前面的文章列表我们并没有拿到用户id,note.note发现了一个user_id,反正不管是不是先存了再说,别空着,note.note.user_id

slug: 每个用户访问的id (加密的id)

文章列表怎么获取,这个就怎么获取$('div.post').find('.avatar').attr('href').replace(//u//, "")

avatar: 会员头像

$('div.post').find('.avatar img').attr('src')

nickname: 会员昵称(注册填的那个)

$('div.post').find('.author .name a').text()

signature: 会员昵称签名

这个签名在上面位置了,就在文章正文下面,评论和打赏上面,有个很大关注按钮那个灰色框框里面,最先一段文字。$('div.post').find('.signature').text()

total_wordage: 总字数

note.note.author.total_wordage

followers_count: 总关注计数

note.note.author.followers_count

total_likes_count: 总喜欢计数

note.note.author.total_likes_count

有些字段命名就是从note.note这个json对象里面获取的,一开始我也不知道取什么名字。

最终拼接的数据

/**
         * 存放数据容器
         * @type {Array}
         */
        let data = {
            article: {
                id: note.note.id,
                slug: note.note.slug,
                title: replaceText($post.find('.article .title').text()),
                content: replaceText($post.find('.article .show-content').html()),
                publishTime: replaceText($post.find('.article .publish-time').text()),
                wordage: $post.find('.article .wordage').text().match(/\d+/g)[0]*1,
                views_count: note.note.views_count,
                comments_count: note.note.comments_count,
                likes_count: note.note.likes_count
            },
            author: {
                id: note.note.user_id,
                slug: $post.find('.avatar').attr('href').replace(/\/u\//, ""),
                avatar: $post.find('.avatar img').attr('src'),
                nickname: replaceText($post.find('.author .name a').text()),
                signature: replaceText($post.find('.signature').text()),
                total_wordage: note.note.author.total_wordage,
                followers_count: note.note.author.followers_count,
                total_likes_count: note.note.author.total_likes_count
            }
        };

6. 生成数据

和列表生成数据基本一样,有一个区别。文件需要加一个标识,article_+ item.slug(文章访问的id)

 // 写入数据, 文件不存在会自动创建
        fs.writeFile(__dirname + '/data/article_' + item.slug + '.json', JSON.stringify({
            status: 0,
            data: data
        }), function (err) {
            if (err) throw err;
            console.log('写入完成');
        });

基本就撸完了,看获取详情的完整代码:

function getArticle(item) {
// 拼接请求地址
  let url = reptileUrl + '/p/' + item.slug;
   /**
 * 核心业务
 * 发请求,解析数据,生成数据
 */
    superagent.get(url).end(function (err, res) {
        // 抛错拦截
    if (err) {
        return throw Error(err);
    }
      // 解析数据
        let $ = cheerio.load(res.text);
    // 获取容器,存放在变量里,方便获取
        let $post = $('div.post');
    // 获取script里的json数据
        let note = JSON.parse($('script[data-name=page-data]').text());
        /**
         * 存放数据容器
         * @type {Array}
         */
        let data = {
            article: {
                id: note.note.id,
                slug: note.note.slug,
                title: replaceText($post.find('.article .title').text()),
                content: replaceText($post.find('.article .show-content').html()),
                publishTime: replaceText($post.find('.article .publish-time').text()),
                wordage: $post.find('.article .wordage').text().match(/\d+/g)[0]*1,
                views_count: note.note.views_count,
                comments_count: note.note.comments_count,
                likes_count: note.note.likes_count
            },
            author: {
                id: note.note.user_id,
                slug: $post.find('.avatar').attr('href').replace(/\/u\//, ""),
                avatar: $post.find('.avatar img').attr('src'),
                nickname: replaceText($post.find('.author .name a').text()),
                signature: replaceText($post.find('.signature').text()),
                total_wordage: note.note.author.total_wordage,
                followers_count: note.note.author.followers_count,
                total_likes_count: note.note.author.total_likes_count
            }
        };
       // 生成数据
        // 写入数据, 文件不存在会自动创建
        fs.writeFile(__dirname + '/data/article_' + item.slug + '.json', JSON.stringify({
            status: 0,
            data: data
        }), function (err) {
            if (err) throw err;
            console.log('写入完成');
        });
    });
}

你肯定要问了,在哪里调用了,
在上面获取文章列表的请求end里面底部随便找个位置加上:

data.forEach(function (item) {
        getArticle(item);
    });

运行,你就会在data文件夹里看到21个json文件。源文件,欢迎指正Bug。


jiayisheji
192 声望14 粉丝