V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
puorg
V2EX  ›  程序员

简单高效的 nodejs 爬虫模型

  •  
  •   puorg ·
    callmelanmao · 2016-11-28 20:27:42 +08:00 · 3273 次点击
    这是一个创建于 2924 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这篇文章讲解一下yunshare项目的爬虫模型。

    使用 nodejs 开发爬虫很简单,不需要类似 python 的 scrapy 这样的爬虫框架,只需要用 request 或者 superagent 这样的 http 库就能完成大部分的爬虫工作了。

    使用 nodejs 开发爬虫半年左右了,爬虫可以很简单,也可以很复杂。简单的爬虫定向爬取一个网站,可能有个几万或者几十万的页面请求,复杂的爬虫类似 google bot 这样搜索引擎的蜘蛛爬虫,要每时每刻爬取互联网上最新的内容。

    一般的个人开发者都是用爬虫定向爬取一些网站,然后提取一些结构化的数据,使用 api 接口获取数据也可以归到这一类。如果想简单的练习爬虫技术,可以尝试爬取豆瓣电影数据和书籍数据的,使用 api 接口和爬取 html 页面都能完成这个任务。

    爬虫的说白了就是一个 http 客户端,通过 http 协议和远程 http 服务器通信,获取 html 页面内容或者其他的种子文件, pdf 文件等等。和浏览器不同的一点就是爬虫不会把抓取的内容渲染出来,而是解析页面内容然后保存到数据库里面。

    在开始学习爬虫的时候我考虑的是怎么爬取 html 页面内容,怎么解析 html 页面之间的链接规则,后来遇到了页面编码的问题。

    统一 utf8 编码

    国内网站主要是使用 html 和 gbk 这两种编码方式,解决编码有两种思路,第一个是在获取页面内容的时候根据页面的<meta charset='gbk'>编码把内容统一转码成 utf8 的,因为 nodejs 字符串默认编码就是 utf8 。

    这个方案充满了不确定性。

    问题 1 :不同网站的指定编码的方式不一样,除了前面提到的那种方式,还有<meta http-equiv="Content-Type" content="text/html; charset=gbk">这种方式指定编码,这个问题还不是很大,很多的 http 工具库都能正确的解析这两种编码,问题是还有很多网站没有指定编码,又或者指定的编码和文件的实际编码不一致(遇到过真实的案例)。

    问题 2 :如果你把 gbk 编码的 html 文件转成 utf8 编码保存到本地,用浏览器直接打开这个文件的时候会显示乱码,非常不利于开发过程中的查找问题。

    不转码 html 内容

    既然前面的方案有这么多的问题,剩下的方法就是把 html 内容直接按照原来的编码保存到本地,然后解析的时候指定编码。

    这个方法有 2 个好处: 1 、简化了爬虫模型, 2 、可以用浏览器打开 html 文件,不会乱码。唯一的缺点是不同网站文件内容解析的时候似乎需要指定编码,对于小规模爬虫这个问题其实影响不大。

    统一爬虫模型

    前面的编码方案解决了爬取不同网站 html 文件的编码问题,我们可以用一个统一的爬虫方法爬取不同网站的内容,那如果你想爬取非 html 内容呢?

    是不是又要重新写一个爬虫方法,解决这个问题的方法就是 http 协议,假设我们写的这个爬虫方法就是一个完整的 http 客户端,那理论上这个客户端是不是能根据Content-Typ获取各种格式的文件。

    那到底能不能用一个简单的方法就能实现上述的功能呢?下面的方法就是我采用 request 写的 nodejs 简单高效的爬虫模型。

    function fetch(url) {
      console.log(`down ${url} started`);
      const deferred = Q.defer();
      const file = getfile(url);
      fs.ensureDirSync(path.dirname(file));
      const stream = request
        .get(url)
        .on('error', (err) => {
          deferred.reject(`down ${url}:${err}`);
        })
        .on('response', (res) => {
          if (res.statusCode !== 200) {
            deferred.reject(`down ${url}:${res.statusCode}`);
          } else {
            console.log(`down ${url}:${res.statusCode}`);
          }
        })
        .pipe(fs.createWriteStream(`${file}`));
    
      stream.on('finish', () => {
        deferred.resolve();
      });
      return deferred.promise;
    }
    

    这段代码在yunshare/src/util/fetch.js里面,当然这个方法不能单独运行,但是关键的逻辑就是这么简单。

    不管是什么格式的 http 请求, json , html , torrent 等都统一把返回的二进制格式文件保存到以md5(url)为文件名的位置。上面的getfile就是用来获取文件路径的。

    模型扩展

    使用 MD5 散列还是有发生冲突的风险的,如果你想要爬取上亿的网页,可能还需要对上面的模型进行扩展。一个简单的思路就是把网页路径中的域名提取出来,不同网站的内容保存在对应的域文件夹下面。

    其他的类似的思路也行,需要注意的就是如果爬虫保存文件和解析文件是分开的,你需要保证在解析文件的时候能用同样的方法定位这个文件。共同的参数就是 url ,所以你生成文件名的时候不能用一些随时间变化的参数。

    最后,献上第一个使用 node 全栈开发的网站:哔哩搜索,目前索引百度网盘资源 1000w 条了。

    3 条回复    2016-11-29 10:32:00 +08:00
    ericls
        1
    ericls  
       2016-11-29 00:56:28 +08:00 via iPhone
    Kafka
    xiamx
        2
    xiamx  
       2016-11-29 07:33:37 +08:00
    楼主简单的额介绍了下如何用 nodejs 实现 `xargs -n 1 curl -O < urls.txt` 不过这跟爬虫需要的功能还相差甚远。
    puorg
        3
    puorg  
    OP
       2016-11-29 10:32:00 +08:00
    @xiamx 这个只是针对简单规模的爬虫模型,大规模分布式当然比这要复杂很多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   929 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 21:49 · PVG 05:49 · LAX 13:49 · JFK 16:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.