记录用nodejs爬虫爬取汽车之家遇到的一些坑

   因为某些原因需要爬取一些数据,自己就用nodejs来试试爬取数据,当然我在这方面也是一个小白,因为也是刚用nodejs来爬取数据,走了不少弯路,先说说我写爬虫的过程把。

  我用的是express框架,先安装cheerio与https以及request,因为爬取数据的地址协议是https,request是用来请求网址的。

  首先我主要是爬取经销商的信息,请求网址是https://dealer.autohome.com.cn/hefei#pvareaid=2113612,这里要分三个点,1.一个是要爬取所有城市里面的经销商。2.第二个是每个城市经销商都是有分页的,所有要求分页的处理。3.第三个是每个经销商里面还有一个页面,要在里面获取到经销商其他的信息(比如 营业执照等)。这里就截取一小段图。

需要获取这些信息,然后就开始寻找网页的规律吧。

     每一个城市的地址如下:。其中每一个城市改变的只有红色部分的地方,然后再来看看分页。分页也只是改变红色的部分,这里能看出来爬取的数据量也挺大的,到这里的时候,我就在思考这里是用同步爬取还是用异步的方式爬取,但整个nodejs是异步进行的,我也就暂时没考虑同步的,先暂时用着异步请求的方式试试,这也是之后我在写爬虫在此耽误两天的原因。这之后在详细说吧。

    那么就先贴上代码:

       

let express = require('express');
let cheerio = require('cheerio');
let iconv = require('iconv-lite');//防止乱码
let router = express.Router();
let https = require("https");
let originRequest = require('request');

https.get('https://dealer.autohome.com.cn/DealerList/GetAreasAjax?provinceId=0&cityId=0&brandid=0&manufactoryid=0&seriesid=0&isSales=0', (res) => {
    let chunk = '';
    res.on('data', (d) => {
  	    let html = (iconv.decode(d , 'gb2312'));
  	    chunk+= html;
    });

    res.on('end' , () => {

    })
})

module.exports = router;

这里的https://dealer.autohome.com.cn/DealerList/GetAreasAjax?provinceId=0&cityId=0&brandid=0&manufactoryid=0&seriesid=0&isSales=0地址是什么地址呢? 我在前面的时候,发现请求获取不了所有的城市,然后在xhr中发现这里面的城市是请求后台拿到的,那么我这里其实也是获取到所有的信息的。这里很清晰的能看出,请求的数据,那么我们也只用请求这个数据就好了。

上面代码部分获取到了这里请求的数据,我就遇到第一个问题,这里面拿到的是text格式的数据,然后我再怎么转化成json也不行,网上查了很多资料,比如:json.uncode、转化数组等我都试过,无法用xx.xx的格式获取到信息。后面就在想php是以echo输出到页面,而前台是获取到页面的数据,那么这些数据一定也是string格式的,既然是string格式的情况下,那么我将数据先存入json文件中,再取出json文件中的数据,应该就可以获取到json格式的信息了。而事实上也是如此,废话不多说,先贴上代码。

fs.writeFile('./routes/data.json',chunk,(err) => {  //写入同目录下的Data.txt文件
	 if(err) 
	     throw err;  
	 console.log('write info into json');  
});

fs.readFile('./routes/data.json',(err,data) => {
    console.log(data);
})

  到这里我们就能获取到所有城市的地址了,然后我对数据进行了整理,只要想要的数据,这里代码就不贴上来了,整理数据的格式如下:[[['北京','北京'],['beijing','beijing'],[352,352]],[['安徽','合肥'],['anhui','hefei'],[443,211]],[['xx'],['xx'],[231]]]这样的,前面是用来筛选直辖市,中间是为了处理不重复处理省级,最后是筛选出市级,做好这些准备之后我们就可以开始请求地址了。贴出代码

for(let num in filterData){
	    		if(filterData[num][0].length == 2){
	    			locateName = filterData[num][0][0]; //获得省份
	    		}
	    		for(let filterDataNum in filterData[num][1]){
    			    if(filterData[num][2][filterDataNum] % 15 != 0){
				    	dataNum = Math.floor(filterData[num][2][filterDataNum] / 15) + 1;
				    }else{
				    	dataNum = filterData[num][2][filterDataNum] / 15;
				    }
				    if(dataNum != 0){
				    	let url = "https://dealer.autohome.com.cn/"+ filterData[num][1][filterDataNum] +"#pleteaid=2113612";
				    	let filterDataNumF;
				    	if(filterData[num][0].length == 2){
				    		filterDataNumF = 1;
				    	}else{
				    		filterDataNumF = 0;
				    	}
				    	request(url,(err , res , body) => { 
                            	var html = iconv.decode(body, 'gb2312');
				 		    	var $ = cheerio.load(html, {decodeEntities: false});
                                $(".list-box").find(".list-item").each((index , obj) => {
				 		    		dealerName = $(obj).find(".link span").text();
				 		    		address = $(obj).find(".info-addr").text();
				 		    		brand = $(obj).find("em").text();
				 		    		tel = $(obj).find(".tel").text();
				 		    		shopUrl = $(obj).find(".shop").attr("href");
				 		    		shopUrlArg.push(shopUrl);
				 		    	})
                        })
				    }
				}
	    	}

let request = (url, callback) => {  
  let headers = {  
	  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36',
      'Connection': 'keep-alive',
      'Accept-Encoding': '',
      'Accept-Language': 'en-US,en;q=0.8'
  }
  let options = {
    url: url,
    encoding: null,
    headers: headers
  }
  originRequest(options, callback)
}

  这里就遇上的第二个问题,乱码,网上查阅的使用iconv-lite,然后事实上在使用之后,虽然中文没有变成乱码,但却变成了编码,这也不是我想要的结果,再花了两三个小时的网上查阅,总算是找到问题的关键,再填上header之后,这些问题也都迎刃而解了,这里也要提醒自己,在写请求的时候一定要加上header。然而这个问题解决了,下个问题就将是困扰我两天的问题了。

 

   使用for循环请求接口,request因为会是异步进行的,会导致线程处于等待状态,但当for循环执行完成之后,才会执行request请求,request请求因为是异步的,处理请求会随机选择几个或者十几个等待的请求同时处理,这里将会导致两个问题:

   1.这里请求出来的数据是随机的,没有任何顺序可言

   2.这里请求的太多,同一时间请求次数过多会导致网址会限制你的请求,也就会让你获取不到body的信息。(这个问题在处理分页和进入店铺的情况下尤其严重)

在暂不考虑的顺序的情况下,处理一下第二个问题吧。在出现这个问题的时候,我考虑了三种解决的方案,这里要首先排除promise的方式,因为promise是以异步的方式同步进行,所以这里也会导致第二个问题得不到解决。先说说我的三个方案吧。

   1.使用数组的map,map因为是用链表的方式来遍历数据的,这里也会使循环的时候同步执行代码循环,等待请求完成后再执行下一个循环

   2.利用nodejs的mapLimit来限制请求次数

   3.整个程序执行改为同步的方式(async、await来控制) 

先说说使用map的情况会出现什么吧。在使用urlArg.map(data => {})的时候,第一次使用request确实没有什么问题,请求的时候很完美,但当继续请求店铺中的信息之后,会出现 Last few GC这个错误,意思是内存溢出,可能是我某个地方有导致内存泄漏的地方,但确实花了几个小时没有排查出来,到这里说明这个办法已经走不通了。如果各位大神用这个能行的,可以评论告诉我,我再尝试一次。

然后我又试了试第二种方法,虽然能限制请求个数,在请求第一层网址的时候,请求个数在5个以内是没有问题的,但是在请求第二层店铺信息的时候,数据同样疯狂undefined,我表示很无奈啊。最后我也只能利用同步的方式来试试,原因是我对async和await虽然学习过,但不熟悉啊,但原理还是懂的,利用await使线程处于阻塞状态,看来只有临阵磨枪了。

前面的就不贴代码了,代码太过于丑陋,就把最后一种成功的代码放上来吧。

dealResult(urlArg);
async function dealResult(urlArg){//得到指定所有数据
	let getData = await getTolData(urlArg);
	let trunkSql = "truncate table mainData";
	query(trunkSql);
	for(let i = 0; i < getData.length ; i ++){
       let sql = 'insert into mainData Values(null,?,?,?,?,?,?,?,?,?)' ;
       query(sql , [getData[i][0][0],getData[i][0][1],getData[i][0][2],getData[i][0][3],getData[i][0][4],getData[i][0][5],getData[i][0][6],getData[i][0][7],getData[i][0][8]]);
	}
}
async function getTolData(urlArg){  //获取店面信息和分页所有信息
	let data = await getData(urlArg);
	let tolDataArg = [];
	for(let i = 0 ; i < data.length ; i ++){
		for(let k = 0 ; k < data[i].length ; k ++){
			let tolData = await getDealer(data[i][k]);
			tolDataArg.push(tolData);
		}
	}
	return tolDataArg
}
async function getData (urlArg){  //处理分页
	let getDataArg = [];
	for(let i = 0 ; i < urlArg.length ; i ++){//urlArg.length
		let url = urlArg[i][0];
		let locateName = urlArg[i][1];
		let dataNum = urlArg[i][2];
		let cityName = urlArg[i][3];
		let cityPinYinName = urlArg[i][4];
		for(let k = 1 ; k <= dataNum ; k ++){//dataNum
			let ReData = [];
			if(k == 1){
				ReData = await runAsync(url,locateName,dataNum,cityName);
				getDataArg.push(ReData);
			}else{
				let invitePage = "https://dealer.autohome.com.cn/"+ cityPinYinName +"/0/0/0/0/"+ k +"/1/0/0.html";
				ReData = await runAsync(invitePage,locateName,dataNum,cityName);
				getDataArg.push(ReData);
			}
		}
	}
	return getDataArg;
}
async function runAsync(url,locateName,dataNum,cityName){ //获取数据
	let myVal = getLocData(url,locateName,dataNum,cityName);
	return myVal;
}
function getLocData(url,locateName,dataNum,cityName){	//处理经销商信息
    return new Promise(function(resolve,reject){
        let shopUrlArg = [];
		request(url , function(err , response , body){
			if(err){
				console.log(err, "这是获取经销商信息错误");
				resolve();
			}
	    	let dealerName,address,brand,tel,shopUrl;
			var html = iconv.decode(body, 'gb2312');
	    	var $ = cheerio.load(html, {decodeEntities: false});
	    	$(".list-box").find(".list-item").each((index , obj) => {
	    		dealerName = $(obj).find(".link span").text();
	    		address = $(obj).find(".info-addr").text();
	    		brand = $(obj).find("em").text();
	    		tel = $(obj).find(".tel").text();
	    		shopUrl = $(obj).find(".shop").attr("href");
	    		shopUrlArg.push([dealerName,address,brand,tel,locateName,cityName,shopUrl]);
	    	})
	    	console.log(shopUrlArg+"这是获取经销商信息");
	    	resolve(shopUrlArg);
	    });
    });
}

代码写的不好就多指教一下,说一下这个的思想吧。这个是将每一个request里面的数据用await拿出来,不在request中继续回调,代码中也有注解,就不多说了,而事实上,利用同步的思想,会导致爬取的速度非常慢,但优点就是非常的稳定。目前已经爬了7个小时了,文章就到这里吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值