初始的一些准备
const browser = await puppeteer.launch({
executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
headless: true,
args: ['--no-sandbox']
});
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36');
先拿到browser实例;拿到page对象,并设置userAgent请求头,不然userAgent请求头中是会带有headless字样的。
要爬取知乎的内容,首先要解决登陆问题
我们通过设置cookie来解决登陆问题:
首先我们自己手动登陆知乎
进入到开发者模式后,刷新知乎,在network栏看请求信息
直接把整个cookie复制下来
puppeteer设置cookie的方法是
page.setCookie(options)
options 里需要有key,value,domain。这是设置cookie的基本要求
这里我写了一个方法,快速设置cookie,将刚刚复制的cookie放进去
async function addCookies(cookies_str: string, page: puppeteer.Page, domain: string){
let cookies = cookies_str.split(';').map(pair=>{
let name = pair.trim().slice(0,pair.trim().indexOf('='))
let value = pair.trim().slice(pair.trim().indexOf('=')+1)
return {name,value,domain}
});
await Promise.all(cookies.map(pair=>{
return page.setCookie(pair)
}))
}
好了,现在page.goto('https://www.zhihu.com')就可以直接进去了,不会弹登陆页面;
这里登陆之后我们可以使用await page.waitFor('div.Card');
来等待页面的指定元素加载完成;
如果要模拟人的行为,我们可以await page.waitFor(2000);
等待个几秒什么的,还可以模拟滚轮滑动
for(let i = 0; i < 30; i++){
await page.evaluate((i)=>{
window.scrollTo(0,i*15)
},i);
await timeout(50);
}
这里page.evaluate方法就是在浏览器中植入javaScript代码,这些代码都是在浏览器里执行的,比如你在evaluate方法中执行console.log('puppeteer')
,你在你的执行终端是看不到输出的,你需要在浏览器的console那去看。
好的,现在不管怎么说已经进入到知乎首页
现在呢,我想把我收藏的高赞的收藏夹更新的高赞回答爬下来,怎么做呢?
首先我们要找到“我的收藏”这个元素,
const collection = await page.$('div.GlobalSideBar > div > div.Sticky > div.Card > ul.GlobalSideBar-navList > li.GlobalSideBar-starItem > a');
这里我们就通过page.$()方法,如同jquery一样通过选择器得到这个元素;
得到后我们就可以通过点击事件,进入到“我的收藏”的列表里了;
我们可以直接await collection.click()
;不过有时候会失灵,没有效果,我也不太清楚原因,而且这样的点击会打开一个新的page,也就是说和我们目前一直使用的这个page没有关系,比较麻烦,还需要通过如browser.pages等方法找到新打开的page,所以我们换一个思路
await page.evaluate((collection)=>{
collection.setAttribute('target','_self');
collection.click();
},collection)
我们直接进入浏览器js环境去执行setAttribute方法改变新页面出现方式,再click; 要注意这里传入的参数collection必须得通过作为evaluate方法的参入辗转传进去
现在我们进入到如下页面
这里呢,我们同样使用page.evaluate方法点击"不需要解释"的这个收藏夹;
await page.evaluate(()=>{
let box = document.querySelector('#zh-favlist-following-wrap');
if(box){
let as = box.querySelectorAll('div.zm-item > h2 > a');
for(let a of as){
if(a.getAttribute('href') == '/collection/38887091'){
a.click();
}
}
}
})
到此,作为一个'人',puppeteer要休息一下await page.waitFor(5000);
休息5秒缓一缓;
好的现在我们正式进入到高赞回答列表了
现在我们开始做爬取评论的准备操作
监听page的response事件
function response(Obj: any){
let _Obj = Obj;
return async function abc(response: puppeteer.Response){
if( response.url().indexOf('/comments') > 0 ){
console.log('装载一个知hu的评论s')
let comments = JSON.parse(await response.text())
let res_comments = comments.data.slice(0,10).map((data:any)=>{
//console.log({ name: data.author.name, content: data.content, like: data.likesCount })
return { name: data.author.name, content: data.content, like: data.likesCount }
})
_Obj.data = res_comments;
}
}
}
page.on('response', response(Obj) )
这里我们使用一个obj对象来装一个回答的前十条热评,我们通过监听response,在知乎调用'评论api'的时候,将其数据直接获取;待会我们模拟点击“展开评论”的时候,就能触发response事件并获取评论数据了
获取文字内容的代码比较多,我直接在代码里注释
for(let answer of answerlist){ //这个answerlist就是page.$$('选择器')获得的元素列表
let data = await page.evaluate((answer)=>{
//这个一个html转码方法 如:
//<script>alert(2);</script> =>
function HTMLDecode(text:string) {
let temp = document.createElement("div");
temp.innerHTML = text;
let output = temp.innerText || temp.textContent;
return output;
}
//以下就是通过选择器,获得各种元素,然后得到我们想要的数据
//如作者,回答内容,标题,多少个赞 等等
let comment = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-actions > div > a.toggle-comment');
//这里我们点击评论按钮,response事件发生,我们的函数执行,收集数据
comment.click();
let title = answer.querySelector('.zm-item-title > a').innerHTML;
let author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span.name');
let author_link = '';
if(!author){
author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').innerHTML;
author_link = 'https://www.zhihu.com'+answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').getAttribute('href');
}else{
author = author.innerHTML;
}
let content = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-rich-text > textarea.content').innerHTML;
content = HTMLDecode(content);
let like = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-vote > a').innerHTML;
return { title, author, content, like ,author_link}
},answer)
await timeout(2000);
if(Obj.data.length == 1){
await timeout(2000);
}
console.log(Obj.data)
await timeout(1000)
data.content = handler_content(data.content)
data.comments = Obj.data;
//这里我们就完成了一个回答的收集了
datas.push(data);
Obj.data = [];
console.log(data.title)
}
//保存到数据库
for(let data of datas){
let comments = data.comments;
delete data.comments;
let res = await db.insert( {table: 'zhihu', data} );
if(res ){
console.log(res)
for(let c of comments){
c.zhihu = res.insertId;
await db.insert({table: 'zhihu_comment', data: c})
}
}
}
这里其实我还有一个亮点,就是将知乎的内容那,是有不少的图片的,如果我们直接将内容显示在我们自己的网站上,这些图片是从知乎那请求不到的,会报403错误,所以我们将所有图片的src改变了一下,变成访问我的服务器,我的服务器去请求知乎的图片,再返回给我的网站,这样就ok了
最后爬的结果就在我的网站http://www.weidongwei.com/zhihu