知乎应该很多人没事的时候都会去看, 毕竟知乎上平均年收入几十万, 日常出国. 哈哈 听朋友说, 今天闲来无事写了一个爬取知乎答案列表的爬虫. 当然知乎有营养的内容还是很多的
之前写过一次抓答案列表接口的爬虫, 感觉不太好, 还得找每个问题的请求接口, 这次使用puppeteer
来通过页面显示内容抓取
Puppeteer
是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。
我们随便打开一个知乎的问题, 可以看到下面就是答案列表, 点击查看所有回答, 然后下滑到底加载, 就是这么个逻辑;
我们的抓取方式就是这样, 访问到网址之后, 先点击页面内的查看所有回答按钮, 然后滑到底加载更多回答, 重复执行下滑到底的动作, 一直加载更多的回答, 加载到你想要的数量或者加载完回答之后, 开始处理得到的数据, 然后写到json文件或者excel文件里, 当然这次我写的只能抓取纯文字的内容, 有图片视频的的没有处理
下面是完整的代码, 注释我写的很清楚
代码一共需要安装两个依赖
第一个安装可能会有点慢, 第二个是插入excel的node库
const puppeteer = require("puppeteer");
const fs = require("fs");
const NodeXls = require('node-xls');
var TOTAL = 10; // 你想抓取的答案数量, 如果想抓取所有, 等于0
(async () => {
console.log("开始抓取数据...");
const browser = await puppeteer.launch();
var page = await browser.newPage();
await page.goto("https://www.zhihu.com/question/287642868/answer/1028962960", {
timeout: 0, // 等待时间无限长
waitUntil: "networkidle2"
});
// 提取所需元素
const title = await page.$eval("h1", e => e.innerText)
const content = await page.$eval(".QuestionRichText", e => {
if (e.querySelector(".QuestionRichText-more")) {
e.querySelector(".QuestionRichText-more").click();
}
return e.innerText
})
const tag = await page.$$eval(".QuestionHeader-topics .Popover", e => {
var a = [];
e.forEach(element => {
console.log(
'element.querySeloct(".Popover").innerText: ',
element.innerText
);
a.push(element.innerText);
});
return a;
})
// 标题
console.log("title: ", title);
// 标签
console.log("tag: ", tag);
// 内容
console.log("content:", content);
// 点击查看所有回答按钮之后等待两秒
if (await page.$('.QuestionMainAction') != null) {
await page.click(".QuestionMainAction");
await page.waitFor(2000);
}
// 获取总共回答数量
var totalAnswer = await page.$eval(".List-headerText", e => {
return e.innerText.split(" ")[0]
})
totalAnswer = Number(totalAnswer.replace(",", ''))
console.log(await page.$eval(".List-headerText", e => {
return e.innerText.split(" ")[0]
}));
// 获取所有回答dom
var dom = await page.$$(".List-item");
var count = dom.length
var count2;
// 无限循环下滑操作, 当下滑到你想要的答案数量或者请求完所有回答break跳出循环
for (let i = 1; i > 0; i++) {
if (TOTAL) {
console.log('抓取' + TOTAL + '条回答...');
if (count < TOTAL) {
try {
console.log("count: ", count);
// 下滑操作
await page.evaluate(() => {
window.scrollTo(0, document.querySelector('.ListShortcut').clientHeight);
})
// 如果未登录 出现登录框就关掉
let closeIcon = await page.$(".Zi.Zi--Close.Modal-closeIcon")
if(closeIcon) {
closeIcon.click()
}
// 每次下滑加载等待一秒
await page.waitFor(1000);
var dom = await page.$$(".List-item");
count = dom.length; // 当前回答条数
// 退出条件, 当前页面的回答条数与上一次下滑请求页面答案条数相等, 既请求完了所有回答
if (count2 === count) {
// 当前回答条数和上次刷新页面条数相同就说明, 加载完了所有回答
console.log('加载完成!开始处理!一共加载了' + count + " 条回答");
break;
} else {
count2 = dom.length // 上次上拉加载页面 回答条数
}
} catch (error) {
console.log('抛出错误');
}
} else {
console.log('加载完成!开始处理!一共加载了' + count + " 条回答");
break;
}
} else {
console.log('抓取所有回答...');
TOTAL = totalAnswer;
}
}
let result = await page.evaluate(getData);
writeFile(result, title) // 写入json文件
writeExcel(result, title) // 写入excel文件
// 所有进程运行完 退出
await browser.close();
})();
function getData() {
let items = document.querySelectorAll(".List-item");
let answer = [];
var num = 0;
items.forEach(element => {
try {
// 获取页面所需内容
answer.push({
id: num++,
anthor: element.querySelector(".AuthorInfo-head .AuthorInfo-name").innerText,
abrhorLink: element.querySelector("[itemprop='url']").getAttribute("content"),
authorInfo: element.querySelector('.AuthorInfo-detail').innerText,
content: element.querySelector(".RichContent-inner").innerText.replace(/\n/g, ''),
time: element.querySelector(".ContentItem-time span").innerText,
zan: element.querySelector(".Button.VoteButton.VoteButton--up").innerText.replace(/\n/g, '')
});
} catch (error) {
console.log('捕获到一个错误');
}
});
return answer;
}
// 写入json文件
function writeFile(info, title) {
fs.appendFile(title + ".json", JSON.stringify(info) + ",", function (err) {
if (err) {
console.error(err);
return;
}
});
}
// 写入execl
function writeExcel(info, title) {
var tool = new NodeXls();
var xls = tool.json2xls(info);
fs.writeFileSync(title + '.xlsx', xls, 'binary');
}
代码就这么多, 不明白的可以在下面问
puppeteer
的文档