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

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 !!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,251评论 25 707
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,721评论 1 92
  • 为什么要编写可维护的javascript? 软件生命周期80%的成本消耗在了维护上。 几乎所有的软件维护者都不是它...
    其心阅读 619评论 2 6
  • 这是一篇关于如何深度学习的文章,如果不懂深度学习,看问题的能力停留在表面,那我们的事业或者人生也大多只能停留在底层...
    瑜璟阅读 1,783评论 4 56
  • 这几天加班多,回家晚,老公也学习到深夜,等到睡觉的时候几乎都过了半夜,结果就是早上起不来。 七点多,儿子已经醒来,...
    DGbean阅读 204评论 0 0