2018-06-05

作者: 刘坤_a9d5 | 来源:发表于2018-06-05 17:13 被阅读0次

    谈一谈简单的js爬虫

    基本概念

    网络爬虫的两个主要任务就是:

    1. 下载页面
    2. 找页面中的链接

    使用到的第三方包

    "cheerio": "^1.0.0-rc.2" nodejs版的jquery
    "events": "^3.0.0" 监听
    "log4js": "^2.7.0" 日志
    "mongodb": "^3.1.0-beta4" 数据库插件
    "superagent": "^3.8.3" 网络访问的包
    

    第三方依赖的使用

    log4js

    var log4js = require('log4js')
    //设置日志文档
    log4js.configure({
        appenders:{cheese:{type:'file',filename:'../log/'+new Date()+'.log'},out:{type:'stdout'}},
        categories:{default:{appenders:['cheese','out'],level:'error'}}
    })
    const logger = log4js.getLogger('cheese')
    logger.debug('msg')
    logger.error('msg')
    logger.info('msg')
    logger.warn('msg')
    

    cheerio

    主要用来解析页面中的链接,非常核心的模块

    const $ = cheerio.load(html)
    let hrefs = $('[href]')
        for(let i = 0 ; i < hrefs.length;i++){
            this.href.push($(hrefs[i]).attr('href'))
        }
    
    

    这是官方推荐的写法,先用load方法载入页面,$('[href]')就是jquery选择器的写法,由于得到的是DOM
    对象,所以每次都要$(href[i])转换为jquery对象,最后使用attr()方法取出href属性。这便是本例用到的所有方法,如果还想继续深入了解,请前去npm阅读相应文档。

    events

    监听模块

    let emitter = new events.EventEmitter()
    const LISTEN_TITLE = 'one_turn_done'
    emitter.addListener('one_turn_done',function () {
        logger.debug('新队列开始sitemapLinks:',sitemapLinks.length)
        if(counter>=200) {
            counter = 0
            logger.debug('Rest')
            setTimeout(()=>{
                logger.debug('休息结束')
                excuteList().then((values) => {
                    emitter.emit('one_turn_done')
                })},600000)
    
        }else {
            logger.debug('不休息')
            excuteList().then((values) => {
                emitter.emit('one_turn_done')
            })
        }
    })
    

    首先,要建立一个监听的对象

    再使用EventEmitter的addListener方法添加监听

    最后使用emit方法触发监听

    mongodb

    非关系数据库,使用他的原因是因为数据量比较大,mongodb读写快。
    但由于数据库操作是异步的,所以我使用Promise来控制:下载-》href入库-》下载...这样的同步顺序

    function initMongo(resolve,reject) {
        let dburl = 'mongodb://localhost:27017'
        MongoClient.connect(dburl,function (err,db) {
            if(err){
                reject(err.message)
            }else {
                longTimeDBClient = db.db('crawler').collection('segmentfault')
                resolve('welcome mongoDb')
            }
        })
    }
    

    数据库链接的初始化操作,这种写法将一个Mongodb连接赋给全局变量,这样不用每次都去处理这个同步操作,缺点就是:非常的耗费内存。

    longTimeDBClient.insertOne({domain:'https://www.segmentfault.com',url:'/tags'},()=>{})
    

    插入操作

    longTimeDBClient.find({domain:currentDomain,url:currentUrl})
           .toArray(function (err,res) {
                if(res.length===0){
                     sitemapLinks.push({
                         domain: currentDomain,
                         url: currentUrl
                     })
                longTimeDBClient.insertOne({domain:currentDomain,url:currentUrl},()=>{
                //logger.debug(currentDomain+currentUrl+':入队成功')
                resolve(currentDomain+currentUrl+':入队成功')
                    })
                }else{
                        resolve(currentDomain+currentUrl+':重复文档')
                      }
                 })
    

    查找操作

    let updatestr ={ $set: {
        title: wi.title,
        body: wi.body,
        encoding: wi.encoding,
        html: wi.html,
    }}
    longTimeDBClient.updateOne(
        {
              domain: wi.domain,
              url: wi.url,
        },
        updatestr,
        function (err, _) {
        if (err) logger.record('error', err.message);
        else logger.debug('文档插入成功 domain:', wi.domain, ' url:', wi.url,'现在数组的长度:',sitemapLinks.length)
        })
    

    修改操作

    superagent

    const request = require('superagent')
    request
      .get(wi.getDURL())
      .set('user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36')
      .set('accept','text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8')
      .end(function (err,  res){})
    

    这个依赖对nodejs的http包分装的非常精美get用来设置要访问的网址,set可以设置表头信息,end是最后一个方法
    发送请求并将结果返回到回调函数的res参数上。

    流程图

    爬虫自然语言描述.jpg

    代码描述

    出队并下载页面

    function excuteList(){
        if(sitemapLinks.length===0){
            //如果执行器发现队列为0,那么结束
            //这种情况很少:可能是站点已经爬完或者发生了未知
            //console.log()
            logger.debug('3.可能爬完了,sitemapLinks: 0 currentLinks:',currentLinks.length)
            process.exit(0)
        }
        exchangeLinks()
        let promiseQueue = []
        let fivecounter = 0
        //console.log(currentLinks)
        while(currentLinks.length > 0){
            promiseQueue.push(new Promise(buildTheDownLoadEvn(currentLinks.pop(),fivecounter)))
            fivecounter++
        }
        return Promise.all(promiseQueue)
    }
    

    exchangeLinks()将预备栈中取出特定数量的链接,插入到爬取队列,使用buildTheDownLoadEvn()方法来消费爬取队列,fivecounter用来记录这是第几个链接,用来设置每五秒发送一个请求。这里使用Promise的all方法,使得在这些链接爬取结束后,我再进入下一轮‘出队下载页面’。

    exchangeLinks

    function exchangeLinks() {
        currentLinks = []
        //每次最多取300个
        for(let i = 0 ; i < 300; i++){
            if(sitemapLinks.length>0) {
                let shift = sitemapLinks.shift()
                currentLinks.push(new webInformation(shift.domain, shift.url))
            }
        }
    }
    

    下载页面并且判断链接是否合法

    let buildTheDownLoadEvn = (wi,fivecounter)=>{
        return function download(resolve,reject) {
            counter++
            setTimeout(()=>{
                request
                    .get(wi.getDURL())
                    .set('user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36')
                    .set('accept','text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8')
                    .end(function (err,res) {
                        if(err) {
                            logger.error(wi.getDURL(),err.message)
                            resolve(err)
                        }
                        else {
                            if (res.statusCode === 200&&res.text) {
                                wi.findTheInfo(res.text)
                                let tempLine = []
                                //这里限制了队列的长度,最长20000
                                if(sitemapLinks.length <= 60000&&wi.url.length<=60){
                                    wi.href.forEach(function (t) {
                                        tempLine.push(new Promise(pushAcceptableLink(t, wi.domain, wi.url)))
                                    })
                                    Promise.all(tempLine)
                                        .then(function (data) {
                                            resolve(wi.getDURL())
                                            logger.debug('检查promise:现在数组的长度:',sitemapLinks.length)
                                            //logger.debug('')
                                        })
                                }
    
                                else{
                                    //如果队列到达上限那么,也要返回
                                    resolve(wi.getDURL())
                                }
    
                                let updatestr ={ $set: {
                                    title: wi.title,
                                    body: wi.body,
                                    encoding: wi.encoding,
                                    html: wi.html,
                                }}
                                longTimeDBClient.updateOne(
                                    {
                                        domain: wi.domain,
                                        url: wi.url,
                                    },
                                    updatestr,
                                    function (err, _) {
                                    if (err) logger.record('error', err.message);
                                    else logger.debug('文档插入成功 domain:', wi.domain, ' url:', wi.url,'现在数组的长度:',sitemapLinks.length)
                                })
    
                                //成功带回成功的链接为了在日志文件中记录
    
                                //console.log(sitemapLinks)
                            } else {
                                resolve(0)
                                logger.error(wi.getDURL(),'internet error stateCode:' + res.statusCode)
                                //日志里要记录一些信息 DURL和错误代码,错误发生的时间
                            }
                        }
                    })
            },5000*fivecounter)
    
        }
    }
    

    通过外部函数构建一个新的环境,返回的download是符合Promise回调函数的接口。在request的回调函数中,用pushAcceptableLink来判断链接是否爬过和是否是我要爬的页面,这个规则可以自己定义的。最后longTimeDBClient.updateOne来将页面信息入库,这里没有使用Promise,因为页面入库和爬取的过程是两个不相干的过程。

    pushAcceptableLink

    function pushAcceptableLink(element,domain,url) {
        return (resolve,reject)=>{
            let regIsFullName = /^http(s)?:\/\/(.*?)\//
            let regIsLink = /^#/
            //logger.debug('oldLinks:',oldLinks.length)
            //oldLinks.forEach(function (element,i) {
                let currentUrl
                let currentDomain
                if(regIsLink.test(element)){
                    //do nothing
                    //resolve('illegal')
                }else {
                    //
                    if (element.match(regIsFullName) !== null) {
                        let m = element.match(regIsFullName)[0]
                        currentDomain = element.substr(0,m.length-1)
                        currentUrl = element.substr(m.length-1, element.length)
                    } else {
                        currentDomain = domain
                        currentUrl = element
                    }
                    //let whichOne = {url: currentUrl, domain: currentDomain};
                    //list.push(whichOne)
                }
                //去数据库里寻找是否有相同的队列
                if(currentDomain===domain&&currentUrl!==url&&/^\//.test(currentUrl)){
                    longTimeDBClient.find({domain:currentDomain,url:currentUrl})
                        .toArray(function (err,res) {
                            if(res.length===0){
                                sitemapLinks.push({
                                    domain: currentDomain,
                                    url: currentUrl
                                })
                                longTimeDBClient.insertOne({domain:currentDomain,url:currentUrl},()=>{
                                    //logger.debug(currentDomain+currentUrl+':入队成功')
                                    resolve(currentDomain+currentUrl+':入队成功')
                                })
                            }else{
                                resolve(currentDomain+currentUrl+':重复文档')
                            }
                        })
    
                }else{
                    resolve('illegal')
                }
           // })
        }
    
    }
    

    这个规则可以自己定义,这里就不赘述了。

    代码:github

    https://github.com/liuk5546/LinkCrawler

    当然,这只是一个类似于练习稿的代码,如有错误,欢迎各位同行批评指正。

    相关文章

      网友评论

        本文标题:2018-06-05

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