You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Puppeteer的Twitter自动化:优化循环及线程抓取问题

使用Puppeteer和snscrape抓取Twitter推文的问题与优化方案

需求说明

  • 第一步:通过snscrape抓取推文URL及用户名,存储为如下格式的JSON数组:
[["https://twitter.com/NikkiSiapno/status/1595712124222857217", "NikkiSiapno"],  ["https://twitter.com/NikkiSiapno/status/1559472755443220481", "NikkiSiapno"]]
  • 第二步:使用Puppeteer遍历URL,按规则提取内容:
    • 若页面标题为Tweet,仅提取第一条推文内容;
    • 若页面标题为Thread,仅提取前30条作者本人的推文,排除其他用户回复。

现有问题

  • 无法提取每条推文的用户句柄并与authorName变量对比;
  • 滚动操作控制不合理,容易遗漏线程中的推文。

当前实现代码

const puppeteer = require("puppeteer");
(async () => {
    const browser = await puppeteer.launch({
        headless: false,
        defaultViewport: false,
    });
    const page = await browser.newPage();
    const tweetUrls = require("./tweets.json");
    for (let i = 0; i < tweetUrls.length; i++) {
        const tweetUrl = tweetUrls[i][0];
        const authorName = tweetUrls[i][1];
        await page.goto(tweetUrl, { waitUntil: "networkidle0" });

        // Wait for the tweets to load
        await page.waitForSelector('[data-testid="tweet"]');
        // Wait for the sheetDialog element and close it.
        await page.waitForSelector('div[data-testid="sheetDialog"]');
        await page.click('div[data-testid="app-bar-close"]');

        // extract the heading title
        const tweetHeading = await page.$('h2[role="heading"]');
        const tweetHeadingText = await page.evaluate(
            (el) => el.querySelector("span").textContent,
            tweetHeading
        );

        // if the heading title is "Thread" execute this:
        if (tweetHeadingText === "Thread") {
            // extract the user handle
            const Handle = await page.$('div[data-testid="User-Names"]');
            const userHandle = await page.evaluate((el) => el.querySelector("a > div > span").textContent.substring(1), Handle
            );

            const tweetTextSet = new Set();

            let tweetCount = 0;

            // For the first n tweets, while the userHandle === authorName grab their text content.

            while (tweetCount < 30 && userHandle === authorName) {
                const tweetEls = await page.$$('[data-testid="tweetText"]');
                for (const tweetEl of tweetEls) {
                    const tweetText = await page.evaluate(
                        (el) => el.textContent.trim(), tweetEl
                    );
                    if (!tweetTextSet.has(tweetText)) {
                        console.log(tweetText);
                        tweetTextSet.add(tweetText);
                        tweetCount++;
                        if (tweetCount >= 40) {
                            break;
                        }
                    }
                }
                await page.evaluate(() => {
                    window.scrollBy(0, window.innerHeight);
                });
                await new Promise((resolve) => setTimeout(resolve, 1000));
            }
            // if the heading title is "Tweet" then only get the first tweet:
        } else if (tweetHeadingText === "Tweet") {
            const tweetEls = await page.$$('[data-testid="tweetText"]');
            const firstTweetEl = tweetEls[0];
            const firstTweetText = await page.evaluate(
                (el) => el.textContent.trim(),
                firstTweetEl
            );
            console.log(firstTweetText);
        }
    }
})();

优化方案与问题解决

1. 解决推文用户句柄验证问题

原代码仅提取单个用户句柄,未针对每条推文做验证。优化思路:遍历每条推文元素,单独提取其用户句柄并与authorName对比,仅保留匹配的内容。

2. 优化滚动逻辑避免遗漏

原滚动方式可能导致加载不完整,改为:滚动到页面底部后等待新推文加载,记录已处理的推文ID避免重复,当无新目标推文或已抓取30条时停止滚动。

优化后的完整代码

const puppeteer = require("puppeteer");

(async () => {
    const browser = await puppeteer.launch({
        headless: false,
        defaultViewport: false,
    });
    const page = await browser.newPage();
    const tweetUrls = require("./tweets.json");

    for (const [tweetUrl, authorName] of tweetUrls) {
        await page.goto(tweetUrl, { waitUntil: "networkidle2" });

        // 处理弹窗,无弹窗时跳过
        try {
            await page.waitForSelector('div[data-testid="sheetDialog"]', { timeout: 3000 });
            await page.click('div[data-testid="app-bar-close"]');
        } catch (e) {}

        // 等待标题加载并获取文本
        await page.waitForSelector('h2[role="heading"]');
        const tweetHeadingText = await page.$eval('h2[role="heading"] span', el => el.textContent.trim());

        if (tweetHeadingText === "Thread") {
            const processedTweetIds = new Set();
            let collectedCount = 0;

            while (collectedCount < 30) {
                // 获取当前页面所有推文元素
                const tweets = await page.$$('[data-testid="tweet"]');
                let newTweetsFound = false;

                for (const tweet of tweets) {
                    // 提取推文唯一ID
                    const tweetId = await page.evaluate(el => {
                        const link = el.querySelector('a[href*="/status/"]');
                        return link ? link.href.split('/').pop() : null;
                    }, tweet);
                    if (!tweetId || processedTweetIds.has(tweetId)) continue;

                    // 提取当前推文的用户句柄
                    const handle = await page.evaluate(el => {
                        const handleEl = el.querySelector('[data-testid="User-Names"] a div span');
                        return handleEl ? handleEl.textContent.substring(1) : null;
                    }, tweet);

                    // 验证是否为目标作者
                    if (handle !== authorName) continue;

                    // 提取推文内容
                    const tweetText = await page.evaluate(el => {
                        const textEl = el.querySelector('[data-testid="tweetText"]');
                        return textEl ? textEl.textContent.trim() : '';
                    }, tweet);

                    if (tweetText) {
                        console.log(tweetText);
                        processedTweetIds.add(tweetId);
                        collectedCount++;
                        newTweetsFound = true;
                        if (collectedCount >= 30) break;
                    }
                }

                // 无新目标推文时停止滚动
                if (!newTweetsFound) break;

                // 滚动到底部并等待加载
                await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
                await page.waitForTimeout(1500);
                await page.waitForSelector('[data-testid="tweet"]', { timeout: 3000 }).catch(() => {});
            }
        } else if (tweetHeadingText === "Tweet") {
            // 提取单条推文内容
            const firstTweetText = await page.$eval('[data-testid="tweetText"]', el => el.textContent.trim());
            console.log(firstTweetText);
        }
    }

    await browser.close();
})();

优化点说明

  • 改用for...of循环遍历URL数组,代码更简洁;
  • 增加弹窗异常捕获,避免无弹窗时程序报错;
  • 提取推文唯一ID,彻底避免重复抓取;
  • 针对每条推文单独验证用户句柄,确保只提取目标作者内容;
  • 滚动后等待新元素加载,避免遗漏线程内容;
  • 无新目标推文时自动停止滚动,提升运行效率。

内容的提问来源于stack exchange,提问作者Codewell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 11:12:23