一个简单的爬虫习作,事件,cheerio,mongoose,coffeescript
发布于 7小时前 作者 zysam 50 次浏览 来自 分享

一个简单的爬虫习作,以事件来组织代码。

主要受该文章影响。点我 scraper 的 github 一个爬虫无非是以下三个过程 : 载入网页 -> 解析内容 -> 保存数据

再加两个外部事件,错误处理及完成通知。

以事件来说明,是酱子的:

        @loadWebPage @url

        @on 'loaded',@parsePage

        @on 'parsed',@db

        @on 'error',@handleErr

        @on 'complete',@complete

1.载入网页,直取 node 原生 http.get ; 2.解析内容,这个不能写正则造轮子吧(其实我也不会写)!借用 cheerio 这个变态杀手好了 ; 3.数据保存,写本地就用 fs ,数据库就依赖 mongoose 。

所以 ,主框架 scraper.coffee 如下: 构造器 + 原型函数

EventEmitter = require('events').EventEmitter
cheerio = require 'cheerio'
http = require 'http'

STATUS_CODES = http.STATUS_CODES
show = console.log

class Scraper extends EventEmitter
    constructor : (@url) ->

    ###
    init : ->
        @loadWebPage @url

        @on 'error',@handleErr

        @on 'loaded',@parsePage

        @on 'parsed',@db

        @on 'complete',@complete
    ###

    loadWebPage : (opts,fn) ->
        fn = fn or ->
        if typeof opts is 'string'
            show 'Loading ' + opts
        else
            show 'Loading ' + opts.host + opts.path

        req = http.get opts,(res) =>
            body = ''
            if res.statusCode isnt 200
                @emit 'error',STATUS_CODES[res.statusCode]

            res.on 'data',(chunk) ->
                body += chunk

            res.on 'end', =>
                @emit 'loaded',body
                fn()
            return
        req.on 'error',(err) =>
            @emit 'error',err
        return

    parseLoad : (html) ->
        cheerio.load html

    parsePage : (html) ->
        show 'parse...'
        $ = @parseLoad html
        docs = []

        $('#shop-all-list ul li')
            .each (i,elem) ->
                model = new Object 
                    shopName : ''
                    link : ''
                    pic : ''
                    addr : ''
                    cate : 
                        life : new Array
                        buss : new Array
                    comment : new Array
                    
                #console.log 'i:%s',i
                #console.log i + ':' + $('.txt .tag-addr span',@).text()
                model.shopName = $('.txt .tit a',@).attr('title')
                model.link = $('.txt .tit a',@).attr('href')
                model.pic = $('.pic a img',@).attr('data-src')
                
                $('.txt .tag-addr',@)
                    .each (i,elem) ->
                        model.addr = $('.addr',@).text()
                        model.cate.life.push $('a span',@).eq(0).text(),$('a',@).eq(0).attr('href')
                        model.cate.buss.push $('a span',@).eq(1).text(),$('a',@).eq(1).attr('href')

                model.comment.push $('.txt .comment span',@).attr('title')

                $('.txt .comment a',@)
                    .each (i,elem) ->
                        model.comment.push $(@).children().text()
                docs.push model

        @emit 'parsed',docs

    handleErr : (err) ->
        show 'has some error ,%s',err

    complete : ->
        show 'all have done!!'

module.exports = Scraper

个人模块 bot_test.coffee 继承主框架 , 以本地一个 html 来测试 , 拿大众点评来练手 。

fs = require 'fs'
Scraper = require './scraper'
show = console.log

class Tscraper extends Scraper
    constructor : (@path,@destpath) ->
        @init2Test()

    init2Test : ->
        show ' test...'

        @read2Test @path

        @on 'error',@handleErr

        @on 'loaded',@parsePage

        @on 'parsed',@write2Test

        @on 'complete',@complete

    read2Test : (path) ->
        fs.readFile path,{encoding:'utf8'},(err,data) =>
            show 'reading file.'
            if err then @emit 'error',err else @emit 'loaded',data

    write2Test : (data) ->
        data = JSON.stringify data
        fs.writeFile @destpath,data,(err) =>
            if err then @emit 'error',err else @emit 'complete'

    #your cheerio rule and change @on 'parsed'
    parseYour : (html) ->
        $ = @parseLoad html
        docs = []

        #your rule
        #
        #
        @emit 'parsed',docs

filepath = './test/gz_movie_p1.html'
destpath = './test/test_gz_movie.json'

scraper = new Tscraper filepath,destpath

实际上 , 身为一个合格的爬虫怎能没有并发呢! 正在 bot.coffee 的例子是酱子的:

mongoose = require 'mongoose'
Scraper = require './scraper'
Model = require './model'
show = console.log

url = 'http://www.dianping.com/search/category/2/10/g132'
COUNT = 0
PAGES_LIMITS = 50
DB_COUNT = 0

class YoScraper extends Scraper
    constructor : (@url) ->
        COUNT++
        @init()

    init : ->
        
        #回调这里好 , 载完网页立即载入 , loadWebPage 加个 callback
        @loadWebPage @url,wizard

        @on 'error',@handleErr

        @on 'loaded',@parsePage

        @on 'parsed',@db

        @on 'complete',@complete

    db : (docs) ->
        Model.create docs,(err) =>
            DB_COUNT++
            show '%s db runing.',DB_COUNT
            if err then @emit 'error',err else @emit 'complete'

    complete : ->
        show 'complete website : %s',DB_COUNT
        #全部完成后 , 断开 mongoose ,安静退出 , 深藏功与名 。
        if DB_COUNT is PAGES_LIMITS
            @exit()
    
    #有错误也不让它结束 , 爬下个。
    handleErr : (err) ->
        show 'has some error ,%s',err
        wizard()

    #your cheerio rule.
    parseYourWeb : (html) ->
        $ = @parseLoad html
        docs = []

        #your rule
        #
        #
        
        @emit 'parsed',docs
    exit : ->
        mongoose.disconnect()

geraterUrls = (limit) ->
    urls = []
    urls.push url
    urls.push url + 'p' + i for i in [2..limit]

    return urls

Urls = geraterUrls PAGES_LIMITS

wizard = ->
    if !Urls.length
        show 'Run all pages!!'
    else
        url = Urls.shift()
        scraper = new YoScraper url

numberOfParallelRequests = 20
wizard() for i in [1..numberOfParallelRequests]

如有载入网页->解析内容->载入网页 , 可以再写一个 ‘init’

另外: 前练习事件写法,无料近论坛有人争论 ‘事件的自言自语’ 是否合适 , 本人是小白 , 说得太高深 ,听不进 。 觉得事件写法,有如大白菜之美。

promise 的一点看法: promise 看来挺高大上 , 只要写出返回 promise 这种风格函数 ,再用 promise 来组织 。 我拿 mongoose 原生支持 promise 写了两句 。 普通函数有变成 promise 风格的工具 ,这就不好说了。

以上是我对爬虫 , 及异步的小小认识

回到顶部