美文网首页大前端JavaScript专业专题
node.js爬虫-爬取简书特定作者的所有文章

node.js爬虫-爬取简书特定作者的所有文章

作者: lanzhiheng | 来源:发表于2017-05-07 22:04 被阅读1480次

    1. 前言

    这篇文章藏在心中已经好一段时日了,迟迟不敢动笔,主要是担心不知道该如何去组织这样一篇技术文章。

    其实个人感觉技术性的文章是最难写的,细节往往很难拿捏。有些技术细节没有解释到位,害怕读的人难以理解。反之,有些简单的东西怕讲解太多,增加了不必要的篇章。

    授人以鱼不如授人以渔

    故而,这篇文章我会尽量少贴代码,多谈思考过程。

    2. 编码的缘由

    我最近在重新搭建自己的个人网站,想把自己在简书写的所有文章数据都导入到那个网站上。

    这个时候有些朋友可能会不乐意了:“如果要获取自己所写的所有文章为什么要写代码?简书不是已经提供了下载当前用户所有文章的功能了吗?“

    兄弟说得在理,首先我得声明简书确实是一个很棒的写作平台,他能够满足绝大多数用户的需求,但是对于我这种“懒”癌晚期的程序员来说却还是有点不太够。我有以下几点考虑

    • 把几十篇文章一篇篇复制粘贴到个人网站的后台,我想我会疯掉的(我现在有68篇文章)。
    • 当简书的文章需要更新的时候,我不得不再去自己的网站后台手动更新对应文章,这个工作有点重复了。
    • 简书下载的文章并没有提供对应文章的发布时间所属文集, 字数这些元数据,我并不想手动为每一篇文章设置对应的元数据值。

    秉持着

    Don't Repeat Yourself

    这个原则,让我们开始这次爬虫之旅吧。

    3. 功能介绍

    简单概括一下爬虫的功能

    1. 向服务端发送请求获取作者的所有文章。
    2. 从文章中提取出自己需要的数据(文章内容,标题,发布日期......)
    3. 组织数据,存储到对应的数据库中。

    本文重点会放在页面的请求,解析以及数据的组织上,至于如何把数据存进数据库,不会多加讲解。有兴趣的可以直接看源码

    4. 技术栈

    我最后选择使用node.js来写这个爬虫,毕竟要重新去学习python或者ruby这些服务端语言需要耗费我不少时间。只是这样当然这样还不够。

    工欲善其事,必先利其器

    为了少写些代码我还需要一个较为成熟的爬虫框架。这里使用的是node-crawler。个人觉得这是一个比较cool的爬虫框架,而且前端人员用起来定会觉得倍感亲切-我们可以用我们最亲切的jQuery语法来解析响应返回的页面。

    5. 数据模型

    在要爬一样东西之前,首先我们肯定得确认要爬取的东西是什么。这里为了简化文章,我把需要爬取的内容定为以下三个字段(源码里面可不止这几个字段)。

    • 文章标题
    • 文章发布日期
    • 文章内容(markdown格式)

    我只需要想办法从响应返回的页面里提取出上面三种数据就可以了。至于最后把数据存到什么数据库里面,怎么存,那便看个人喜好了。

    6. 制定爬虫策略

    (1)基本信息爬取

    首先进入对应作者的简书主页,类似这个页面。我们会看到有一堆文章列表,看看能不能提取到我们需要的信息?

    好吧,一大堆问题,除了文章标题之外其他内容似乎都难以爬取。不管怎么样都得去文章详情页看看了

    看来详情页面还算能够满足我的需求。故而,我决定基本信息的提取采用如下策略

    • 通过爬虫爬取用户首页的所有文章条目,提取出每一篇文章对应详情页的链接。
    • 通过爬虫框架发送请求,分别请求每一篇文章的详情页面。
    • 解析详情页面的内容,提取出我们需要的信息。

    (2)面对滚动加载,该如何获得所有文章?

    在进入特定作者的简书主页的时候,我发现其实简书并没有加载该作者所有的文章,他只是加载了文章列表的一部分。如果需要获取更多的文章列表,需要向下滚动,向服务器端请求更多的页面。

    刚开始我是站在前端的立场上去考虑这个问题,真的很蛋疼。我最初的想法是使用一些库如phantomjs去模拟浏览器的行为。我打算模拟浏览器滚动行为,等数据加载完成之后再继续滚动,直到不再往服务端请求数据为止。我就真的这么做了,后来才发现这是个噩梦,这意味着我得做下面的事情:

    • 加载页面。
    • 模拟浏览器滚动行为加载更多文章数据。
    • 监听请求行为,请求完成之后再继续滚动。
    • 判断什么时候滚到最底部。

    先不说有没有程序库支持上面的功能,以上策略明眼人一看就觉得不靠谱,估计也就只有我傻乎乎地跑去尝试了。我多次尝试后发现,要模拟滚动行为都相当困难,而且使用phantomjs的时候开发环境经常会卡死。直觉告诉我或许还有更好的实现方式。当对一个问题百思不得其解的时候,或许可以尝试跳出原来的思维定势,从另一个角度去考虑这个问题。问题往往会简单不少。

    后来我从后端的角度上去考虑这个事情

    问: 加载更多这个行为的本质是什么?
    答: 向服务端发送请求,获取更多页面数据。

    那我只需要知道页面滚动的时候浏览器向服务端发送请求的url以及对应的参数,我不就可以用爬虫来迭代发送这个请求,进而达到获取完整的文章列表的目的了吗?

    OK,马上去Network看看,它可以看到我们发送的网络请求。我在Network下清除历史记录后滚动页面,有以下发现。

    服务端响应后返回的是渲染过的列表数据。果然跟我预料的一样。然后我再看看完整的url

    原来它是使用了分页参数page,来向服务端请求分页数据。我通过代码来封装这个过程

    var i = 1;
    var queue = []
    
    while( i < 10) {
      var uriObject = {
        uri: 'http://www.jianshu.com/u/a8522ac98584?order_by=shared_at&&page=' + i,
      queue.push(uriObject);
      i ++;
    }
    

    这只是代码片段的一部分,把10个url存进队列数组里。最终也只能获取10页的数据,不过这就有个问题了如果真实数据不到10页,那该如何处理?
    我试着请求第100页的数据,看看会发生什么。发现简书的服务端返回了一个302 的状态码,然后浏览器跳转到个人动态的页面去了。

    这个状态码很有用,我可以针对这个状态码判断我们的请求的页码参数page的值有没有超出指定的页数。
    我可以预设更多的请求,如果请求返回这个状态码,则不对请求的数据进行任何处理(因为已经超出页数的范围)。反之则对返回的数据进行解析,提取出我们需要的关键数据。

    当然这种做法相当粗暴,会发送许多不必要的请求。接下来有时间我会对这部分代码进行优化。

    7. 页面解析

    请求发送完了,等服务端响应之后我们便可获取到我们需要的页面了。接下来要做的就是对页面结果进行解析,提取出我们需要的内容。上面也说过了 node-crawler这个爬虫框架内置了jQuery,这让我们页面解析工作变得简单。

    (1)获取文章详情页面的url

    先来看看文章列表每个条目的html结构(简书的程序员也做了注释)。

    我们只需要提取出 ul.note-list里面的每一条li a.title,然后再提取出它们的href属性对应的值,便是我们需要获取的url了。这种操作对jQuery来说简直易如反掌。下面是我的代码片段,我把所有url提出来后都存储在articlesLink这个数组里面,供后面的程序使用。

    let articlesLink = [];
    $('ul.note-list').find('li').each((i, item) => {
      var $article = $(item);
      let link = $article.find('.title').attr('href');
      articlesLink.push(link);
    })
    

    (2)从详情页面中提取数据

    再look look详情页面的结构

    从页面结构看,我们可以很简单地提取出标题,还有发布日期这两个字段的内容

    let title = $article.find('.title').text();
    let date = $article.find('.publish-time').text().replace('*', '');
    

    有些更新过的文章,发布日期最后就会有个*号,避免干扰我需要把他们都处理掉。但是文章主体的提取就有一些问题了。

    我最后期望得到的是markdown格式的字符串。这点我可以通过 to-markdown这个package把html转换成markdown。但是现在问题是这个包似乎是无法解析div这个标签的。我考虑着把文章主体里面的所有div标签都删掉,然后把处理过的字符串通过to-markdown转换成对应的markdown格式的字符串,便可以得到我们期望的数据了。

    既然有了jQuery这个神器,实现起来不会很麻烦。不过我还想要删除含有类名image-caption的标签-这个是简书默认设置,有的时候它有点碍事,可以考虑删掉。

    以下是我的代码片段:

    var toMarkdown = require('to-markdown');
    
    // 删除图片的标题
    let $content = $article.find('.show-content');
    $content.find('.image-caption').remove();
    $content.find('div').each(function(i, item) {
      var children = $(this).html();
      $(this).replaceWith(children);
    })
    
    // 获取markdown格式的文章
    
    let articleBody = toMarkdown($content.html());
    

    最后,只需要把他们放到对象里面:

    let article;
    article = {
      title: title,
      date: new Date(date),
      articleBody: articleBody
    }
    

    至于如何把上面的数据存入数据库,方法有很多,考虑到文章篇幅问题,这里就不多做叙述了。个人比较推崇MongoDB。它是一款目前用得最多的非关系型数据库,灵活性很强。对于构建渐进式的应用,我觉得这是一个比较好的选择。在经常修改的表结构的情景下,起码你不用去维护一大堆的migrations文件。

    最后入库结果如下,刚好是68篇文章

    结尾

    不知不觉这篇文章已经占去了好几个小时,即便这篇文章我已经去除了不必要的代码细节,着重对自己的思考过程以及遇到的问题进行了总结,但还是写了不少字。目测总结能力还有待提高啊~~~~

    注意:本文只是在总结一些经验以及思考过程,github的源代码也只是起参考作用,并不是即插即用的程序包。您可以根据您的情况来写出满足自己需求的爬虫,相信你能做得比我更好。

    Happy Coding and Writing !!

    相关文章

      网友评论

      • IT晴天:有没有尝试过获取专题和文集,好像是异步加载的,爬虫爬不到,有直接获取json的接口,但是限制跨域访问
        lanzhiheng:@IT_晴天 我觉得可以的,你可以用开发工具看看请求,我发现这个请求http://www.jianshu.com/users/a8522ac98584/collections_and_notebooks?slug=a8522ac98584 就是获取你想要的信息,你看看是不是?注意一下http头部,这部分信息另外发请求获取应该就可以了。
        IT晴天:@lanzhiheng 我说的不是文章所属的文集,我说的是作者所有的文集是专题,即右侧边栏的站分,这样就可以做分类标签
        lanzhiheng:@IT_晴天 专题没有爬 没发现是异步的。文集倒是有爬 代码里面应该是爬了文集的。
      • IT晴天:我刚好也想把简书的文章导入自己博客,英雄所见略同,哈哈
      • 程序员小哥哥:之前看到过作者的文档,点击关注了,然后在RC看了作者发布的帖子,觉得应该经常写文档(一旦有了思路和想法、或者知识点做过了想做了记录或者总结),像作者学习,目前从事ruby工作,在看作者之前的文章。
        Happy Coding and Writing !!
        lanzhiheng:@佐为君 92年的,可能比你年轻
        程序员小哥哥:@lanzhiheng :smiley: 好的,老哥。
        lanzhiheng:@佐为君 已经互粉:smile:
      • 深海泰坦:用了分页的get可以直接获取到列表的url,再去列表页提取不是重复了吗?7(1)好像有点问题。
        lanzhiheng:@深海泰坦 我贪快这样做,通过不同的头部可以获取到不同的页面数据。 可以通过设置头部单独获取列表数据而不是整个页面。分页的get直接获取新的列表页也是要解析的 返回的不是json数据。是服务端渲染好的列表数据。
      • 等等z:终于又更新啦
        lanzhiheng:@等等z 是啊 最近有灵感

      本文标题:node.js爬虫-爬取简书特定作者的所有文章

      本文链接:https://www.haomeiwen.com/subject/keiutxtx.html