2018-06-05

谈一谈简单的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参数上。

流程图

2018-06-05_第1张图片
爬虫自然语言描述.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&¤tUrl!==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)