因为某些原因需要爬取一些数据,自己就用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个小时了,文章就到这里吧