基于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
相关产品推荐
相关产品推荐

