puppeteer爬取知乎答案列表爬虫

知乎应该很多人没事的时候都会去看, 毕竟知乎上平均年收入几十万, 日常出国. 哈哈 听朋友说, 今天闲来无事写了一个爬取知乎答案列表的爬虫. 当然知乎有营养的内容还是很多的

之前写过一次抓答案列表接口的爬虫, 感觉不太好, 还得找每个问题的请求接口, 这次使用puppeteer来通过页面显示内容抓取

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。

我们随便打开一个知乎的问题, 可以看到下面就是答案列表, 点击查看所有回答, 然后下滑到底加载, 就是这么个逻辑;

我们的抓取方式就是这样, 访问到网址之后, 先点击页面内的查看所有回答按钮, 然后滑到底加载更多回答, 重复执行下滑到底的动作, 一直加载更多的回答, 加载到你想要的数量或者加载完回答之后, 开始处理得到的数据, 然后写到json文件或者excel文件里, 当然这次我写的只能抓取纯文字的内容, 有图片视频的的没有处理

下面是完整的代码, 注释我写的很清楚

代码一共需要安装两个依赖

  • puppeteer
  • node-xls

第一个安装可能会有点慢, 第二个是插入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的文档

你可能感兴趣的:(JavaScript,node)