再接爬虫
受同学所托,要爬一个网站
第一步很自然的,查看网页元素,看渲染的能不能直接抓下来,
但是用上request
,cherrio
,一查元素,为空?(页面上明明有),把request
的body
打印出来,才发现是ajax
,应对方法:打开chrome
,用Network
,终于查看到了请求,复制链接地址,竟然直接浏览器打开就可以获得到json
数据...
接下来就是漫长的数据整理工作了以及一直要弄明白的回调地狱的问题
第一关:字符串处理
以为返回的是json
,结果一直报错,才发现返回的内容是hxbase_json1({sum: 2660, list: [,…]})
,也就是真正的json内容在两个括号里面...
换言之,怎么获得两个指定的符号中间的内容?
第一反应:正则,但基本忘光了(以前就只是生搬硬套的用...),之后搜索中看到有人提到了直接用字符串的方法,str.substring(str.indexOf('('),str.indexOf(')'))
,这方法好是好,但是:
input: "hxbase_json1({XXXXxx(xxx)xxxx})"
output: ({XXXXxx(xxx
也就是如果中间有括号就不行(弃),如果要用字符串的方法的话,直接提取子串不就可以了!,str.slice(13, -1);
,主要发现其他请求都是返回的这个形式,所以直接写死也行,好的,第一关成功!
另,就在写总结的时候,发现找到了以前第一次爬网站的时候各种堆砌出来的较通用的正则匹配函数:
// 匹配前后缀,提取中间的内容
function getInnerString(source, prefix, postfix) {
if (prefix !== "<p>")
var regexp = new RegExp(encodeReg(prefix) + '.+' + encodeReg(postfix), 'gi');
else
var regexp = new RegExp(encodeReg(prefix) + '(?:(?!<\/p>)[\\s\\S])*' + encodeReg(postfix), 'gi');
var matches = String(source).match(regexp);
if (matches == null) {
let now = new Date();
matches = ['this is failed', now + ''];
}
var formatedMatches = matches.map(value => {
return value
.replace(prefix, '')
.replace(postfix, '');
});
return formatedMatches;
}
//转义影响正则的字符
function encodeReg(source) {
return String(source).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1');
}
第二关:字符串转换成json
以为提取出字符串之间的内容就可以了
// 提取中间内容
let str = body.slice(13, -1);
// 把字符串转化为json
let testJson = JSON.parse(str);
...
// 报错
SyntaxError: Unexpected token s in JSON at position 1
参考链接:字符串转换成JSON的三种方式,终于知道为什么,因为必须要遵守json规范,而字符串里面的属性并没有引号...
示例发现一个更神奇的现象,只有外面用单引号,而属性用双引号才可以
let jee = '{"sum":2660}'; // 正确:输出{ sum: 2660 }
let jee = "{'sum':2660}"; // SyntaxError: Unexpected token ' in JSON at position 1
console.log(JSON.parse(jee));
于是这里的解决方法只能用eval
了(第一次觉得eval这么亲切!),完美!
第三关:回调地狱
现在整个流程差不多写成这样:
// 部分为伪代码
for(let i= 11; i<=17 ;i++) {
// 先请求起始url,借此得到所有的页码数
let first = "xxx";
Request(first, (error, response, body)=> {
let str = body.slice(13, -1);
let testJson = eval("(" + str + ")");
let sum = testJson.sum;
// 获取到整个的页码
let pageNum = Math.floor(sum / 20);
for(let j=1; j<=pageNum; j++) {
let url = "xxx"+j;
Request(url, (error, response, body) => {
// 获取数据(伪代码)
list.push(data)
})
}
//写数据
fs.writeFile(i+"year.json", JSON.stringify(list),
function (err) {
if (err) throw err;
console.log('It\'s saved!');
}
);
})
}
这也就是传说中的回调金字塔(回调地狱),说说我自己遇到的情况,
第一,不一定每次请求都能正确无误,所以常常跑着跑着,突然报错了,然后这样的代码结构简直要命,实际中的我还在其中用了一个urls
数组先存下所有的url
再遍历发请求,也就是各种嵌套,各种异步,出错出在哪到找不到...,
第二,遇到了爬虫(或者异步中)很经典的问题,怎么判断回调结束了?这里的问题就是在fs.writeFile
的时候怎么确保list
已经获取到了所有的值?(11年有133条url,每条url指向20条数据)
第一种问题,以前有过一次爬博客园的经历,那里是采用Promise
化的方式解决的,request
库还有promise
的版本,promise
就保证了要么then
回调成功,要么err
/**
* 第一次爬博客园时的经历
* users 示例
{
raphael5200: {
username: 'raphael5200',
defaultPage: 'http://www.cnblogs.com/raphael5200/default.html?page=',
pages: 'http://www.cnblogs.com/raphael5200/default.html?page=2'
},
joyeecheung: {
username: 'joyeecheung',
defaultPage: 'http://www.cnblogs.com/joyeecheung/default.html?page=',
pages: 'http://www.cnblogs.com/joyeecheung/default.html?page=2'
}
...
}
*/
// 遍历每一个用户
for (let key in users) {
// 请求每一个用户的主页
rq(users[key].pages)
.then((body)=>{
let titles = [];
let reads = 0;
let replys = 0;
for (let i = 0; i < getPageNum(body); i++) {
// 请求用户发出来的每一个文章列表页
rq(users[key].defaultPage + '' + i)
.then((body) => {
// 提取当前文章列表页的所有文章标题,存到users[key].articles, 每篇文章的阅读数、评论数相加放置到readNum,replyNum中
// 写入 当时不知道怎么判断异步结束,直接用了一个比较hack的方法,不断的重写文件(最后能保存下来的就随缘了)
// fs.writeFile: Asynchronously writes data to a file, replacing the file if it already exists
fs.writeFile("users.json", JSON.stringify(users),
function (err) {
if (err) throw err;
console.log('It\'s saved!');
}
);
}).catch (function (err) {
//console.log(err);
});
}
})
.catch(function (err) {
console.log(err);
});
};
// 写入 还尝试过另一个方法,等待1分钟之后写入...(随缘)
setTimeout((users) => {
fs.writeFile("users.json",JSON.stringify(users),
function (err) {
if (err) throw err;
console.log('It\'s saved!');
}
);
}, 1000*1*60);
这次实际上是通过,把不必要的循环去掉,直接把10年17年要的数据,就分为8条url,在直接请求这8条(好在只有8条啊)
插播一条小细节: 之前的let str = body.slice(13, -1);
,常常报错,大致意思就是body
出问题了,undefined
或者什么的不能调用slice
方法,这样的话就又遇到了一个经典的问题:js怎么判断变量的类型?
参考链接:JavaScript学习总结(六)——JavaScript判断数据类型总结
// 外层加一个判断就避免了
if (typeof body === "string") {
let str = body.slice(13, -1);
第二种问题是采用的,settInterval
计数解决
参考链接:NodeJS的异步编程风格
例如:fs
去访问五个txt文件,1.txt
...5.TXT
const fs = require('fs');
const list = [];
for (let i = 1; i <= 5; i++ ) {
fs.readFile('./' + i + '.txt', 'utf-8' ,(err, data) => {
if (err) throw err;
list.push(data);
});
}
// 在后台不断的轮训,当list的长度达到指定值(也就是回调全部结束),此时就可以进行清除定时器
let intervalId = setInterval(() => {
console.log('waiting!...');
if(list.length === 5) {
console.log('SUCCESS');
// 注意setInterval与clearInterval的配合
clearInterval(intervalId);
}
}, 1000);
插话:有点疑惑的是,clearInterval(intervalId)
有种自身清除自身的感觉? 已知的,它们都是隶属于WindowOrWorkerGlobalScope的方法 (待填坑..)
但是在参考链接中的方法更佳,使用setInterval
会加入新的事件循环? Javascript 单线程模型 Event Loop 机制
//记录总的文件大小,使用一个`count`计数来判断异步结束
count = filenames.length;
for (i = 0; i < filenames.length; i++) {
fs.stat("./" + filenames[i], function (err, stats) {
totalBytes += stats.size;
count--;
if (count === 0) {
console.log(totalBytes);
}
});
}
从这个例子中,我们可以学到一点:并发运行的相同异步函数如果协作完成任务,需要添加计数代码判断执行状态,并且把所有异步函数完成后执行的代码放在判断条件的语句块里
第三关: 中文乱码
前面的关卡好不容易闯过了后,兴高采烈的抓到了数据,写到了json里,可是一打开一开,gg,股票名称的字段,全是乱码?,网上提示,直接看一下源网站的编码,浏览器打开一看头部,charset="gb2312"
,难怪...
参考链接:
request库使用时, gb2312、GBK中文乱码解决方法
也就是引用iconv-lite
包,对返回的gb2312
的body
进行一下转码,Iconv.decode(body, 'gb2312').toString()
,这样就可以了