精华 Node.js写爬虫系列
发布于 8 年前 作者 i5ting 32009 次浏览 来自 分享

使用node-crawler和jsdom完成爬虫

初衷

从天蚕土豆的《大主宰》Huge Dominate

起点上是付费的,不经常看,偶尔看看而已,付费没啥意思。于是找免费的

http://www.biquku.com/0/330/

即使说无弹窗,可是还是有很多广告。。。

作为一个有追求的程序员能忍么?

node-crawler说明

爬取《大主宰》的所有章节列表

1

var Crawler = require("crawler");
var jsdom = require('jsdom');

var c = new Crawler({
    jQuery: jsdom,
    maxConnections : 100,
    forceUTF8:true,
  // incomingEncoding: 'gb2312',
    // This will be called for each crawled page
    callback : function (error, result, $) {
      var urls = $('#list a');
      console.log(urls)
    }
});

c.queue('http://www.biquku.com/0/330/');

执行

node examples/1/hello-crawler.js

解析章节信息

代码

var Crawler = require("crawler");
var jsdom = require('jsdom');

var current_book = { }

var c = new Crawler({
    jQuery: jsdom,
    maxConnections : 100,
    forceUTF8:true,
  // incomingEncoding: 'gb2312',
    // This will be called for each crawled page
    callback : function (error, result, $) {
      var urls = $('#list a');
      // console.log(urls)
      
      
      current_book.title = $('#maininfo h1').text()
      current_book.author = $('#info p').eq(0).text()
      current_book.update_time = $('#info p').eq(2).text()
      current_book.latest_chapter = $('#info p').eq(3).html()
      current_book.intro = $('#intro').html()
      current_book.chapters = [];

      for(var i = 0; i< urls.length; i++){
        var url = urls[i]
        
        var _url = $(url).attr('href')+"";
        var num = _url.replace('.html','');
        var title = $(url).text();


        current_book.chapters.push({
          num: num,
          title: title,
          url: _url
        })
      }
      
      console.log(current_book)
    }
});

c.queue('http://www.biquku.com/0/330/');

这里主要是拿jQuery解析dom。然后赋值current_book

执行

node examples/1/chaper-list.js
{ title: '大主宰',
  author: '作    者:天蚕土豆',
  update_time: '更新时间:2016-07-10',
  latest_chapter: '最新章节:<a href="4091426.html" target="_blank">第一千两百六十二章 大陆洗礼</a>',
  intro: '\n\t\t\t\t\t<p>    大千世界,位面交汇,万族林立,群雄荟萃,一位位来自下位面的天之至尊,在这无尽世界,演绎着令人向往的传奇,追求着那主宰之路。\n    无尽火域,炎帝执掌,万火焚苍穹。\n    武境之内,武祖之威,震慑乾坤。\n    西天之殿,百战之皇,战威无可敌。\n    北荒之丘,万墓之地,不死之主镇天地。\n    ......\n    少年自北灵境而出,骑九幽冥雀,闯向了那精彩绝伦的纷纭世界,主宰之路,谁主沉浮?\n    大千世界,万道争锋,吾为大主宰。\n    ..................\n</p>\n\t\t\t\t\t<p>各位书友要是觉得《大主宰》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!</p>\n\t\t\t\t',
  chapters: 
   [ { num: '153064', title: '第一章已发。', url: '153064.html' },
     { num: '153065', title: '第一章 北灵院', url: '153065.html' },
     { num: '153066', title: '第二章 被踢出灵路的少年', url: '153066.html' },
     { num: '153067', title: '第三章 牧域', url: '153067.html' },
     { num: '153068', title: '第四章 大浮屠诀', url: '153068.html' },

想要的信息都准备好了,下面就开始爬某一章吧

爬取某一章

看一下

第一章 北灵院
http://www.biquku.com/0/330/153065.html

和之前的地址的差别是什么呢?

再回顾一下

  chapters: 
   [ { num: '153064', title: '第一章已发。', url: '153064.html' },

也就是说 url 即是章节地址,剩下的就拼接起来就好了。

function one(chapter){
  console.log(chapter)
  c.queue([{
    uri: 'http://www.biquku.com/0/330/' + chapter.num + '.html',
    jQuery: jsdom,
    forceUTF8:true,
    // The global callback won't be called
    callback: function (error, result, $) {
        var content = $('#content').html();
        console.log(content)
    }
  }]);
}

模拟执行代码

var chapter = { num: '4063307', title: '第一千两百五十二章 现世!', url: '4063307.html' }

one(chapter);

执行

node examples/1/chaper-one.js
{ num: '4063307', title: '第一千两百五十二章 现世!', url: '4063307.html' }
&nbsp;&nbsp;&nbsp;&nbsp;第一千两百五十二章<br><br>&nbsp;&nbsp;&nbsp;&nbsp;轰轰!<br><br>&nbsp;&nbsp;&nbsp;&nbsp;灵战子的低吼声,犹如雷鸣一般在这天地之间回荡,一股股磅礴浩瀚的灵力,也是犹如洪流一般,不断的从其体内呼啸而出,引得空间震荡。◇↓,<br><br>&nbsp;&nbsp;&nbsp;&nbsp;此时的灵战子,双目精光涌动,神采飞扬,再没了先前的那种虚弱之感,显然,借助着那所谓的“战祭”,他直接是在顷刻间就将自身状态恢复到了巅峰。<br><br>&nbsp;&nbsp;&nbsp;&nbsp;之前消耗的灵力,也是再度充盈了他的身躯。

总结

爬取一本书的流程

  • 先取列表
  • 再去章节

技能

  • node-crawler 爬取,发送http请求,是基于request模块的
  • 结合jsdom,使用类似于jquery的dom操作,解析结果

node-crawler有2种用法

  • c.queue方法如无callback,走全局的callback,这是获取列表的时候的用法
  • c.queue方法如有callback,走自己的callback,这是获取章节的用法

我们的做法

  • 最小化问题,先关注爬取一本书的流程
  • 爬取一本书的流程中的列表
  • 爬取一本书的流程中的章节

在这个过程中,我们可以很好的学习node-crawler和jquery的dom操作,知识点整体来说比较少,更加容易学习。

下面,爬取的信息咋办呢?见下一章。

更多

参见 https://github.com/i5ting/githubrank

全文完

欢迎关注我的公众号【node全栈】

node全栈.png

联系我,更多交流

xiaoweiquan.jpeg

66 回复

缓存设计:使用dist目录

上节解决的是爬取问题,已经可以成功取到信息了,那么如何处理爬取到的信息呢?

初衷

电子书的目的是为了阅读,我之所以要写这个爬虫,目的就是为了简单,直观,方便,无广告的安安静静的看本书而已。。。

如果还记得这个初衷,那么一切就很简单了。

爬取的信息有2种处理办法

  • 写到文件里,核心是文字,爬取下来是html,直接静态化是比较好的
  • 那么书的简介和章节呢?这部分可能是动态的
    • 生成book.json
    • 保存到数据库

综合来看,写文件肯定是最简单的办法。为了便于大家学习,我们从简,少给大家挖坑,简单粗暴点。

创建目录

先想想,如果是多本书,多个分类存比较好呢?

  • dist
    • 0 玄幻
      • 330 大主宰

这样的结构是不是会更清楚,更加灵活?

创建多级目录,比如dist/0/330

推荐使用mkdirp模块

function mkdir(folder){
  var mkdirp = require('mkdirp');
    
  mkdirp('dist/' + folder, function (err) {
      if (err) console.error(err)
      else console.log('pow!')
  });
}

mkdir('i am mkdir folder')

执行

$ node examples/2/mkdir.js
pow!

$ ls dist 
0                 css               js
6                 i am mkdir folder reader.html

写文件

var fs = require('fs')

function write_chapter(chapter, content){
  content = content.replace('[笔趣库手机版 m.biquku.com]', '')
  
  fs.writeFile('dist/i am mkdir folder/' + chapter + '.html', content, function (err) {
    if (err) throw err;
    console.log('It\'s saved!');
  });
}

var content = "&nbsp;&nbsp;&nbsp;&nbsp;第一千两百五十二章<br><br>&nbsp;&nbsp;&nbsp;&nbsp;轰轰!<br><br>&nbsp;&nbsp;&nbsp;&nbsp;灵战子的低吼声,犹如雷鸣一般在这天地之间回荡,一股股磅礴浩瀚的灵力,也是犹如洪流一般,不断的从其体内呼啸而出,引得空间震荡。◇↓,<br><br>&nbsp;&nbsp;&nbsp;&nbsp;此时的灵战子,双目精光涌动,神采飞扬,再没了先前的那种虚弱之感,显然,借助着那所谓的“战祭”,他直接是在顷刻间就将自身状态恢复到了巅峰。<br><br>&nbsp;&nbsp;&nbsp;&nbsp;之前消耗的灵力,也是再度充盈了他的身躯"

write_chapter('1', content)

执行

$ node examples/2/create_file.js 
It's saved!

写文件就是这么简单

写json文件

var fs = require('fs')

function write_json(book){
  var content =  JSON.stringify(book, null, 4); // Indented 4 spaces
  
  fs.writeFile('dist/i am mkdir folder/book.json', content, function (err) {
    if (err) throw err;
    console.log('It\'s saved!');
  });
}


var book = { title: '大主宰',
  author: '作    者:天蚕土豆',
  update_time: '更新时间:2016-07-10',
  latest_chapter: '最新章节:<a href="4091426.html" target="_blank">第一千两百六十二章 大陆洗礼</a>',
  intro: '\n\t\t\t\t\t<p>    大千世界,位面交汇,万族林立,群雄荟萃,一位位来自下位面的天之至尊,在这无尽世界,演绎着令人向往的传奇,追求着那主宰之路。\n    无尽火域,炎帝执掌,万火焚苍穹。\n    武境之内,武祖之威,震慑乾坤。\n    西天之殿,百战之皇,战威无可敌。\n    北荒之丘,万墓之地,不死之主镇天地。\n    ......\n    少年自北灵境而出,骑九幽冥雀,闯向了那精彩绝伦的纷纭世界,主宰之路,谁主沉浮?\n    大千世界,万道争锋,吾为大主宰。\n    ..................\n</p>\n\t\t\t\t\t<p>各位书友要是觉得《大主宰》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!</p>\n\t\t\t\t',
  chapters: 
   [ { num: '153064', title: '第一章已发。', url: '153064.html' },
     { num: '153065', title: '第一章 北灵院', url: '153065.html' },
     { num: '153066', title: '第二章 被踢出灵路的少年', url: '153066.html' },
     { num: '153067', title: '第三章 牧域', url: '153067.html' },
  { num: '153068', title: '第四章 大浮屠诀', url: '153068.html' }
  ]
};

  
write_json(book)

执行

$ node examples/2/create_json.js 
It's saved!

生成的book.json如下

{
    "title": "大主宰",
    "author": "作    者:天蚕土豆",
    "update_time": "更新时间:2016-07-10",
    "latest_chapter": "最新章节:<a href=\"4091426.html\" target=\"_blank\">第一千两百六十二章 大陆洗礼</a>",
    "intro": "\n\t\t\t\t\t<p>    大千世界,位面交汇,万族林立,群雄荟萃,一位位来自下位面的天之至尊,在这无尽世界,演绎着令人向往的传奇,追求着那主宰之路。\n    无尽火域,炎帝执掌,万火焚苍穹。\n    武境之内,武祖之威,震慑乾坤。\n    西天之殿,百战之皇,战威无可敌。\n    北荒之丘,万墓之地,不死之主镇天地。\n    ......\n    少年自北灵境而出,骑九幽冥雀,闯向了那精彩绝伦的纷纭世界,主宰之路,谁主沉浮?\n    大千世界,万道争锋,吾为大主宰。\n    ..................\n</p>\n\t\t\t\t\t<p>各位书友要是觉得《大主宰》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!</p>\n\t\t\t\t",
    "chapters": [
        {
            "num": "153064",
            "title": "第一章已发。",
            "url": "153064.html"
        },
        {
            "num": "153065",
            "title": "第一章 北灵院",
            "url": "153065.html"
        },
        {
            "num": "153066",
            "title": "第二章 被踢出灵路的少年",
            "url": "153066.html"
        },
        {
            "num": "153067",
            "title": "第三章 牧域",
            "url": "153067.html"
        },
        {
            "num": "153068",
            "title": "第四章 大浮屠诀",
            "url": "153068.html"
        }
    ]
}

讲方法抽到utils.js里

var fs = require('fs')
var debug = require('debug')('crawler')

exports.mkdir = function(folder){
  var mkdirp = require('mkdirp');
    
  mkdirp('dist/' + folder, function (err) {
      if (err) console.error(err)
      else debug('pow!')
  });
}

exports.write_chapter = function(chapter, content){
  // content = content.replace('[笔趣库手机版 m.biquku.com]', '')
  
  fs.writeFile('dist/0/330/' + chapter.num + '.html', content, function (err) {
    if (err) throw err;
    debug('It\'s saved!');
  });
}

exports.write_config = function(book){
  var content =  JSON.stringify(book, null, 4); // Indented 4 spaces
  fs.writeFile('dist/0/330/book.json', content, function (err) {
    if (err) throw err;
    debug('It\'s saved!');
  });
}

注意区分exports.xxx和module.exports即可。

改写chapter-one.js

var Crawler = require("crawler");
var jsdom = require('jsdom');
var utils = require('./utils')

var current_book = { }

var c = new Crawler({
    jQuery: jsdom,
    maxConnections : 100,
    forceUTF8:true,
  // incomingEncoding: 'gb2312',
    // This will be called for each crawled page
    callback : function (error, result, $) {
      var urls = $('#list a');
      // console.log(urls)
      
      utils.mkdir('0/330');
      
      current_book.title = $('#maininfo h1').text()
      current_book.author = $('#info p').eq(0).text()
      current_book.update_time = $('#info p').eq(2).text()
      current_book.latest_chapter = $('#info p').eq(3).html()
      current_book.intro = $('#intro').html()
      current_book.chapters = [];

      for(var i = 0; i< urls.length; i++){
        var url = urls[i]

        var _url = $(url).attr('href')+"";
        var num = _url.replace('.html','');
        var title = $(url).text();


        current_book.chapters.push({
          num: num,
          title: title,
          url: _url
        })
      }

      utils.write_config(current_book);
      // console.log(current_book)
      // 为了演示,模拟一个,不如1k多条,慢死了
      var chapter = { num: '4063307', title: '第一千两百五十二章 现世!', url: '4063307.html' }
      one(chapter);
    }
});

function one(chapter){
  console.log(chapter)
  c.queue([{
    uri: 'http://www.biquku.com/0/330/' + chapter.num + '.html',
    jQuery: jsdom,
    forceUTF8:true,
    // The global callback won't be called
    callback: function (error, result, $) {
      var content = $('#content').html();
      console.log(content)
      
      utils.write_chapter(chapter, content);
      
      process.exit()
    }
  }]);
}

function start(){
  c.queue('http://www.biquku.com/0/330/');
}

start()

代码里有3处

  • utils.mkdir(‘0/330’);
  • utils.write_config(current_book);
  • utils.write_chapter(chapter, content);

此3处即缓存处理。

总结

文件操作即io操作,是Node.js的基础,初学者一般不太会用到,这里通过实例,讲文件夹创建,写文件进行举例,希望大家能够学会。

  • 再一次最小化问题:本章只处理缓存
  • 爬虫和缓存,分步处理

不忘初衷,方得善终。

为了简单,直观,方便,无广告的安安静静的看本书而已。。。

可是还不能看。。。别急,下一章就做这事儿

先顶一哈,再看 终于从公众号上面搬来了

心疼,很想知道楼主经历了什么,想用异步写爬虫。。。

我有个想法,关于更新的 针对这个小说网站,我想把爬取的东西最终展现在我的Web上面 如果用户访问一次我就爬一次显然不合理,因为爬去的数据可能很多,而且一般情况下时间比较接近的话爬去的内容是一样的,因为没更新 那么是不是可以,定期爬一次,例如针对这个小说,我10分钟爬一次,然后存在数据库或者直接文本,用户访问的时候我就把它取出来 也就是说要启动一个定时任务来专门爬取内容 想法合理吗?

@hezhongfeng 可以根据缓存,然后更新

我都是用 request+cheerio 抓网页解析

我也是用 request+cheerio抓网页解析

@liygheart @wang-weifeng cheerio确实比较jsdom要好,不过request没有队列,需要自己实现队列的,其他无大差异

@wang-weifeng superagent+superagent-promise 这个怎么样?

@i5ting 刚在你的公众号看到了这篇,看到这么实战的文章总是忍不住打赏。 我的思路和楼上2位一样, request + cheerio, 流程控制用async的map(并行)和mapSeries(串行),蛮好用的 下面是我个人的一些想法: 很多页面手动解析蛮累的,这种页面的爬取能否抽取成一个配置文件,做一个工具通过点选dom(自动获取xpath或者selector)来映射json,得到一个extractor 封装一个lib来解析schedule(流程)、pagnation(翻页)、extractor(解析),最终用一个crawler去读取配置来实现特定数据的爬取。

我的爬虫项目:岛国丽人,一个种子下载器,request+cheerio,流程控制用Promise https://github.com/zhangjh/islandBeauty

@zhangjh 种子应该用dht : }

俺也用request+cheerio+async,楼主这个值得学习一下

想问下各位,有时候需要连续request,那么各个request得到的cookie如何更好的在后续request中使用。我现在是自己解析出来,传递到下一个里面request里面去,我是想问能不能有个自动,能够保持住,自动的在下一次request中带上

简单,连我都看得懂,赞一个

实用爬虫类文章,mark!

@zhangjh 好东西啊!

@i5ting 马克一下,lz的东西深入浅出,信息量非常丰富,赞

3-编写小说阅读器

之前我们通过爬取和缓存,可以获得book.json和章节信息。

{
    "title": "大主宰",
    "author": "作    者:天蚕土豆",
    "update_time": "更新时间:2016-07-10",
    "latest_chapter": "最新章节:<a href=\"4091426.html\" target=\"_blank\">第一千两百六十二章 大陆洗礼</a>",
    "intro": "\n\t\t\t\t\t<p>    大千世界,位面交汇,万族林立,群雄荟萃,一位位来自下位面的天之至尊,在这无尽世界,演绎着令人向往的传奇,追求着那主宰之路。\n    无尽火域,炎帝执掌,万火焚苍穹。\n    武境之内,武祖之威,震慑乾坤。\n    西天之殿,百战之皇,战威无可敌。\n    北荒之丘,万墓之地,不死之主镇天地。\n    ......\n    少年自北灵境而出,骑九幽冥雀,闯向了那精彩绝伦的纷纭世界,主宰之路,谁主沉浮?\n    大千世界,万道争锋,吾为大主宰。\n    ..................\n</p>\n\t\t\t\t\t<p>各位书友要是觉得《大主宰》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!</p>\n\t\t\t\t",
    "chapters": [
        {
            "num": "153064",
            "title": "第一章已发。",
            "url": "153064.html"
        },
        {
            "num": "153065",
            "title": "第一章 北灵院",
            "url": "153065.html"
        },
        {
            "num": "153066",
            "title": "第二章 被踢出灵路的少年",
            "url": "153066.html"
        },
        {
            "num": "153067",
            "title": "第三章 牧域",
            "url": "153067.html"
        },
        {
            "num": "153068",
            "title": "第四章 大浮屠诀",
            "url": "153068.html"
        }
    ]
}

可见,只要有了book.json就可以获得整部书的信息了。

设计

实现一个阅读器,可以有很多办法,比如整部书都完全静态化,利用模板引擎编译即可,那么有没有更简单的做法呢?

  • 假定有一个书列表页面
  • 点击某一本书,进入阅读界面
  • 在阅读界面里切换上下章

分析共性的条件

reader.html?type=0&book=330&chapter=120
  • type是分类
  • book是书的编号
  • chapter是章节的编号

很明显,这种设计是最简单的,你只需要解析querystring即可,然后按需展示。

精简结构

reader.html

<html>
  <head> 
    <title>第十二章 出手_大主宰_笔趣阁</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
    <link rel="stylesheet" type="text/css" href="./css/reader.css" /> 
    <script type="text/javascript" src="./js/zepto.min.js"></script> 
    <script type="text/javascript" src="./js/main.js"></script> 
 </head>
  <body> 
  <div id="wrapper"> 
     <div class="content_read"> 
      <div class="box_con"> 
       <div class="con_top"> 
          <a href="/">小说阅读器</a> &gt; 
          <a href="/xiaoshuo1/">玄幻小说</a> &gt; 
          <a href="/0/330/">大主宰</a> &gt; 正文 <span class='chapter_title'>第十二章 出手 </span>
       </div> 
       <div class="bookname"> 
        <h1 class='chapter_title'>第十二章 出手</h1> 
          <div class="bottem1"> 
             <a href="javascript:;" class='pre_chapter_btn'>上一章</a> ← 
             <a href="javascript:;" class='chapter_btn'>章节列表</a> → 
             <a href="javascript:;" class='post_chapter_btn'>下一章</a> 
             <a href="javascript:;" class='bookmark'>加入书签</a> 
          </div> 
       </div> 
   
       <div id="content">
         正文加载中。。。
       </div>

       <div class="bottem2"> 
         <a href="javascript:;" class='pre_chapter_btn'>上一章</a> ← 
         <a href="javascript:;" class='chapter_btn'>章节列表</a> → 
         <a href="javascript:;" class='post_chapter_btn'>下一章</a> 
         <a href="javascript:;" class='bookmark'>加入书签</a>
       </div> 
    </div> 
   </div> 
  </div> 
 </body>
</html>

解析QueryString

function getQueryStringByName(name){
  var result = location.search.match(new RegExp("[\?\&]" + name+ "=([^\&]+)","i"));
  if(result == null || result.length < 1){
    return "";
  }
  return result[1];
}

用法

  type = getQueryStringByName('type')
  book = getQueryStringByName('book')
  chapter = getQueryStringByName('chapter')

很简单吧?

获取book.js

使用$.getJSON方法

  $.getJSON(type + '/' + book + '/book.json', function(data){
    current_book = data;
    
    pre_chapter_info    = current_book.chapters[parseInt(chapter) - 1]
    chapter_info        = current_book.chapters[chapter]
    post_chapter_info   = current_book.chapters[parseInt(chapter) + 1]
    
    load(chapter_info)
  })

加载章节

$( “#result” ).load( “ajax/test.html” );

This method is the simplest way to fetch data from the server. It is roughly equivalent to $.get(url, data, success) except that it is a method rather than global function and it has an implicit callback function. When a successful response is detected (i.e. when textStatus is “success” or “notmodified”), .load() sets the HTML contents of the matched element to the returned data. This means that most uses of the method can be quite simple

注意上面的留空

       <div id="content">
         正文加载中。。。
       </div>

代码就很简单了

  function load(chapter_info){
    console.log(chapter_info.title)
    
    $('title').html(chapter_info.title)
    $('.chapter_title').html(chapter_info.title)
    $('#content').load(type + '/' + book + '/' + chapter_info.url);
  }

把章节.html加载到$(’#content’)里。

工具条

  <div class="bottem1"> 
     <a href="javascript:;" class='pre_chapter_btn'>上一章</a> ← 
     <a href="javascript:;" class='chapter_btn'>章节列表</a> → 
     <a href="javascript:;" class='post_chapter_btn'>下一章</a> 
     <a href="javascript:;" class='bookmark'>加入书签</a> 
  </div> 

需要3个章节来切换状态

  • 上一章
  • 当前章节
  • 下一章

分别绑定事件

  //上一章
  $('.pre_chapter_btn').on('click', function(){
    console.log(pre_chapter_info)
    
    load(pre_chapter_info)
    
    addHistory(type, book, parseInt(chapter) - 1)
  })
  
  //章节列表
  $('.chapter_btn').on('click', function(){
    console.log(chapter_info)
    window.scrollTo(0,0)
  })
  
  //下一章
  $('.post_chapter_btn').on('click', function(){
    console.log(post_chapter_info)
    
    load(post_chapter_info)
    
    addHistory(type, book, parseInt(chapter) + 1)
  })

通过load方法来加载内容。可是点击【上一章】或【下一章】,如何页面不懂,url地址能改变呢?

pushstate

其实这是h5的一个特性

// 增加历史记录
function addHistory(type, book, chapter)
{
	history.pushState({"chapter":chapter}, '','/reader.html?type='+type+'&book='+book+'&chapter=' + chapter + '');
  reset()
  window.scrollTo(0,0)
}

history.pushState有3个参数

  • state
  • title
  • url 即跳转的下一个url地址

重置章节状态

在url变化的时候

function reset(){
  type = getQueryStringByName('type')
  book = getQueryStringByName('book')
  chapter = getQueryStringByName('chapter')
  
  pre_chapter_info    = current_book.chapters[parseInt(chapter) - 1]
  chapter_info        = current_book.chapters[chapter]
  post_chapter_info   = current_book.chapters[parseInt(chapter) + 1]
}

工具条是正文上下都有的,所以点击下面的,最好能够调回到顶部,故而当url变动的时候,需要滚动到顶部

  window.scrollTo(0,0)

收藏

HTML5 提供了两种在客户端存储数据的新方法:

  • localStorage - 没有时间限制的数据存储
  • sessionStorage - 针对一个 session 的数据存储

之前,这些都是由 cookie 完成的。但是 cookie 不适合大量数据的存储,因为它们由每个对服务器的请求来传递,这使得 cookie 速度很慢而且效率也不高。

在 HTML5 中,数据不是由每个服务器请求传递的,而是只有在请求时使用数据。它使在不影响网站性能的情况下存储大量数据成为可能。

对于不同的网站,数据存储于不同的区域,并且一个网站只能访问其自身的数据。 HTML5 使用 JavaScript 来存储和访问数据。

localStorage 方法存储的数据没有时间限制。第二天、第二周或下一年之后,数据依然可用。

  • get
  • set
  • remove
if(window.localStorage){
  localStorage.setItem("b","isaac");//设置b为"isaac"
  var b = localStorage.getItem("b");//获取b的值
  localStorage.removeItem("c");//清除c的值
}else{
 alert('This browser does NOT support localStorage');
}

在chrome打开resource查看

这种k/v的存储,设计好key是非常重要的。

设置书key=0/330, 上次读到120章,每天url变动的时候设置,当用户主动点击的时候也可以重置。

使用http-server启动

$ npm i -g http-server
$ hs dist -o

总结

只有1个reader.html就可以简化所有操作,还算是比较简单

涉及的知识点

  • ajax(getJSON和load)
  • on事件
  • h5里的pushstate和localstorage

这部分是完整的前端内容,所以单独讲解,即使是后端同学,应该也会比较容易理解。

如果网站有防爬机制,比如爬的太多封IP, 我需要设10秒一次请求,这在JS 里怎么做呢?

LZ,请教一下,我也在做一个类似novel reader的APP,现在有个问题就是,有没有办法可以自动把text按屏幕大小分页的js?比如,一章小说有3k字,在某个屏幕下一页能放下1k字,那么就把这一章分成三页,然后可以点击翻页?

@winglight 根据字体大小,行间距可以算的

@rupertqin setTimeout就可以了

@rupertqin 试试node-schedule?

@luoyjx 用了 async.eachOfSeries 解决了。。

@i5ting 如何爬取全部章节? 只能爬到95章。-.-

@rupertqin 引用一个async不划算吧

@yzzting 不会啊,看看你的book.json

@i5ting 只引用 require(‘async.eachOfSeries’) 就可以吧

@i5ting 哎,这个计算有点难啊,没有现成可以用的吗?能同时兼容ios和android各种dpi以及不同字体这难度难以估计。。。

使用koa替换hs显示优化

增加etag,gzip等

hs

https://github.com/indexzero/http-server/

http-server是基于node-ecstatic的

 before.push(ecstatic({
    root: this.root,
    cache: this.cache,
    showDir: this.showDir,
    autoIndex: this.autoIndex,
    defaultExt: this.ext,
    contentType: this.contentType,
    handleError: typeof options.proxy !== 'string'
  }));

https://github.com/jfhbrook/node-ecstatic

var opts = {
             root               : __dirname + '/public',
             port               : 8000,
             baseDir            : '/',
             cache              : 3600,
             showDir            : true,
             showDotfiles       : true,
             autoIndex          : false,
             humanReadable      : true,
             headers            : {},
             si                 : false,
             defaultExt         : 'html',
             gzip               : false,
             serverHeader       : true,
             contentType        : 'application/octet-stream',
             mimeTypes          : undefined,
             handleOptionsMethod: false
           }

etag开了,但gzip默认是不开的,所以hs还是有些麻烦

使用koa 2.x

安装模块

    "koa": "^2.0.0",
    "koa-compress": "^2.0.0",
    "koa-conditional-get": "^2.0.0",
    "koa-etag": "^3.0.0",
    "koa-favicon": "^2.0.0",
    "koa-static": "^3.0.0",

app.js

var serve = require('koa-static');
var Koa = require('koa');
var app = new Koa();

var favicon = require('koa-favicon');
var compress = require('koa-compress')
var conditional = require('koa-conditional-get');
var etag = require('koa-etag');

app.use(compress({
  filter: function (content_type) {
    return /text/i.test(content_type)
  },
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH
}))


app.use(favicon(__dirname + '/public/favicon.ico'));

// etag works together with conditional-get
app.use(conditional());
app.use(etag());


// or use absolute paths
app.use(serve(__dirname + '/dist'));

app.listen(9090);

console.log('listening on port 9090');

gzip compress

var compress = require('koa-compress')

app.use(compress({
  filter: function (content_type) {
    return /text/i.test(content_type)
  },
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH
}))
  • http使用gzip压缩传输
  • 如何压缩,即配置项

etag && conditional

var conditional = require('koa-conditional-get');
var etag = require('koa-etag');

// etag works together with conditional-get
app.use(conditional());
app.use(etag());
  • etag就是如何生产etag值
  • 而etag缓存如何用,是通过conditional-get拦截的

static

简单点说,就是类似于apache或nginx的静态http server,只能放静态资源,比如html、css、js、图片等,偶尔也可以用于下载(很少这么用)

var serve = require('koa-static');

// or use absolute paths
app.use(serve(__dirname + '/dist'));

使用pm2 部署

npm i -g pm2
pm2 start app.js -i 0 --name 'simplereader'

查看状态

pm2

查看日志

pm2 logs

其实pm2 deploy也不错,非常方便

用wkhtmltopdf爬pdf,连解析和格式都懒得管了 /手动dog

代理和rate limit这俩办得了不

其实用dom没有用正则高效,亲测用正则比用dom解析快多了

@luoyjx 办的了,简单可以直接limiter就可以搞定,复杂的有更多方案了,后面会有一小节

@i5ting 额,我看到limit了,proxy我再看看

@zhangjh 评论里我就服你……

@i5ting 请问你有遇到中文乱码的情况吗? 在写html时中文全是乱码。 环境:centos6.8+node4.47

抓取内容页面地址:https://github.com/amfe/article/issues/1

@enternoder 哈哈,还不star一发

@zhangjh Mac 下载蛋疼……

这个可以有

这技术玩的可以,做资料收集决赞

https://cnodejs.org/topic/57eb90d26ab98805449b955a

也有兄弟真去做了,但真如这兄弟去开站,估计要哭死,seo做不起来是软肋

@i5ting 想请教一下,如果用async.each+request去遍历所有的小说章节,如果章节过多会报read ECONNRESET的错误,我查了,可能是由于,大量的访问,导致大量的连接未断开,所以会报这个错误。楼主怎么遇到这个问题了吗,怎么解决的?

lz我爱你有qq吗

@zhangjh 你的项目在处理并发的时候会出错,建议用async控制并发请求

适合入门,很赞!

爬小说看。。。有点萌

之前说些https爬掘进,其实https代理就可以搞定,使用chrome 扩展也可以玩,不过没空写

@i5ting 楼主,我window系统的,只能爬到95章,还有就是book.json里面 后面还有很多,不知道是不是window系统的问题?就爬到59章的时间node进程就停止了

@i5ting fs.writeFile 一共执行了99次 node就自动停止了?

@i5ting 很奇怪,不知道楼主遇到过这样的问题没,还是只是windwos系统出的问题

@haisenbergX 不应该啊,里面有队列,不应该被卡死啊。

@haisenbergX 把日志丢出来

@i5ting 我看了fs.writeFile 的eer,在98次的时候,没有报错,eer是null,

@i5ting 是不是c.queue这个地方,造成的呢

@i5ting 我用了setInterval,的时候发现到了99次的时候 c.queue 这个任务运行不了是不是任务过多造成的

@i5ting 我知道问题了,是Crawler这个对象炸掉了,我在one函数里面 添加了 var c=new Crawler() ,就可以一直不停的跑了。可能真的是c这个对象的c.queue 有最大数吧

@i5ting windows系统里面process.exit()好像不能用,我是直接注释掉了

我也用 request+cheerio抓网页解析

以前用python 和java 做过爬虫

找时间坐下 ,很强大,感谢狼叔的分享

回到顶部