使用Puppeteer高效抓取TripAdvisor景点数据:完整指南

使用Puppeteer高效抓取TripAdvisor景点数据:完整指南

本教程旨在指导读者如何使用Node.js的Puppeteer库从TripAdvisor网站抓取景点信息,包括标题、链接、图片和描述。文章将详细阐述如何识别和构建稳定的CSS选择器,避免常见的抓取错误,并提供一个完整的代码示例,帮助开发者构建高效且可靠的网页爬虫。

1. 理解Puppeteer与网页抓取基础

puppeteer是一个node库,它提供了一个高级api来通过devtools协议控制chrome或chromium。这意味着你可以像人类用户一样,通过代码模拟浏览器行为,例如导航页面、点击按钮、填写表单,以及最重要的——从动态加载的网页中提取数据。对于像tripadvisor这样大量使用javascript渲染内容的网站,puppeteer是进行网页抓取的理想工具

在开始之前,请确保你已安装Node.js,并通过npm安装了Puppeteer:

npm init -ynpm install puppeteer

2. 识别目标元素与构建CSS选择器

网页抓取的核心挑战在于准确地识别和定位目标数据所在的HTML元素。许多网站,特别是动态内容网站,会频繁更新其HTML结构或使用动态生成的类名,这使得选择器容易失效。因此,构建稳定且具有韧性的CSS选择器至关重要。

以TripAdvisor的景点列表页为例,我们需要抓取每个景点卡片的标题、链接、图片和描述。

初始错误与修正:

初学者常犯的错误是使用过于泛化或依赖动态类名的选择器。例如,尝试使用#lithium-root .jemSU或.VLKGO等类名作为主选择器,但这些类名可能随时改变,或者在DOM中并非唯一标识。

正确的策略是寻找更稳定的HTML结构。观察TripAdvisor页面,每个景点通常被包裹在一个标签内。这是一个语义化的标签,通常用于表示独立的、可分发的内容单元,因此它是一个相对稳定的容器。

获取标题和链接:

在内部,标题通常位于一个链接(标签)内,并且这个链接可能没有额外的类名,或者其父元素有一个稳定的类名,例如.VLKGO。

不推荐的简单选择器: ‘.VLKGO’ (可能选中多个不相关的元素)推荐的稳定选择器: ‘.VLKGO a:not([class])’ (选中.VLKGO内部的,且没有class属性的标签,这通常是标题链接)另一种获取标题文本的方式: ‘.VLKGO span > div’ (选中.VLKGO内部span的直接子div,通常包含标题文本)

代码示例:获取标题

以下代码片段演示了如何使用page.$$eval或page.evaluate来获取页面上所有景点卡片的标题。

const puppeteer = require("puppeteer");(async () => {    const browser = await puppeteer.launch({ headless: true }); // 生产环境建议设置为true    const page = await browser.newPage();    const url = 'https://www.tripadvisor.com/Attractions-g297476-Activities-c42-Cartagena_Cartagena_District_Bolivar_Department.html';    await page.goto(url, { waitUntil: 'load', timeout: 30000 });    await page.waitForSelector('main', { timeout: 10000 }); // 等待主内容区加载    // 方法一:使用page.evaluate和querySelectorAll    const titles1 = await page.evaluate(() =>         Array.from(document.querySelectorAll('article .VLKGO a:not([class])'), (e) => ({            title: e.innerText.split('.').pop().trim() // 移除序号,清理文本        }))    );    console.log('Titles (Method 1):', titles1);    // 方法二:使用page.$$eval    const titles2 = await page.$$eval('article .VLKGO span > div', el =>         el.map(x => x.textContent.split('.').pop().trim())    );    console.log('Titles (Method 2):', titles2);    await browser.close();})().catch(err => console.error(err));

3. 抓取多字段信息:标题、链接、图片、描述及更多

为了获取每个景点卡片的完整信息,我们将采用更细致的策略:首先定位所有景点卡片的容器(),然后遍历每个容器,在其内部提取所需的各个字段。

完整抓取流程:

启动浏览器并导航: 初始化Puppeteer,打开新页面,并导航到目标URL。等待页面加载: 使用waitUntil: ‘load’和waitForSelector确保页面内容完全加载。定位所有卡片: 使用page.$$(‘article’)获取所有景点卡片的元素句柄。遍历并提取数据: 循环遍历每个卡片句柄,使用elementHandle.$eval在其内部提取特定字段。$eval在元素句柄的上下文中执行,效率更高。数据清洗与结构化: 对提取到的文本进行清洗(如移除序号、多余空格),并将数据存储为结构化的对象数组。

示例代码:抓取标题、链接、图片、描述、价格等

const puppeteer = require("puppeteer");let browser; // 声明浏览器实例以便在finally块中关闭(async () => {    browser = await puppeteer.launch({ headless: true }); // 建议生产环境设为true    const page = await browser.newPage();    const url = 'https://www.tripadvisor.com/Attractions-g297476-Activities-c42-Cartagena_Cartagena_District_Bolivar_Department.html';    await page.goto(url, { waitUntil: 'load', timeout: 30000 });    await page.waitForSelector('main', { timeout: 10000 }); // 等待主要内容区加载    // 获取所有景点卡片的元素句柄    let places = await page.$$('article');    let data = [];    for (let place of places) {        let header = {};        let image = '';        let desc = '';        let by = {};        let price = '';        let priceTxt = '';        try {            // 提取标题和链接            header = await place.$eval('.VLKGO a:not([class])', el => {                // 移除标题前的序号(如 "1.")                const name = el.textContent.split('.').pop().trim();                const link = el.getAttribute('href');                return { name, link: `https://www.tripadvisor.com${link}` }; // 拼接完整链接            });        } catch (e) {            console.warn("Title/Link not found for a card:", e.message);            // 可以选择跳过或赋予默认值        }        try {            // 提取图片URL,通常在srcset中包含多尺寸图片,取最大的            image = await place.$eval('picture > img[srcset]', el => {                const srcset = el.getAttribute('srcset');                if (srcset) {                    // 获取最后一个(通常是最大尺寸)的图片URL,并移除' 2x'等描述                    return srcset.split(',').pop().replace(/2x/gi, '').trim();                }                return '';            });        } catch (e) {            console.warn("Image not found for a card:", e.message);        }        try {            // 提取描述            desc = await place.$eval('a:not([class]) > div > span', el => el.textContent.trim());        } catch (e) {            console.warn("Description not found for a card:", e.message);        }        try {            // 提取提供者/作者信息            by = await place.$eval('.VLKGO div > div > div > a', el => {                const name = el.textContent.replace('By ', '').trim();                const link = el.getAttribute('href');                return { name, link: `https://www.tripadvisor.com${link}` };            });        } catch (e) {            // 提供者信息可能不是所有卡片都有            // console.warn("Provider info not found for a card:", e.message);        }        try {            // 提取价格(如果存在)            price = await place.$eval('[data-automation=cardPrice]', el => el.textContent.trim());        } catch (e) {            // 价格信息可能不是所有卡片都有        }        try {            // 提取价格文本(如 "起" 或 "每人")            priceTxt = await place.$eval('div:nth-child(1) > div:nth-child(3):not([class])', el => el.textContent.trim());        } catch (e) {            // 价格文本可能不是所有卡片都有        }        // 将提取到的数据添加到结果数组        data.push({            name: header.name || 'N/A',            link: header.link || 'N/A',            desc: desc || 'N/A',            image: image || 'N/A',            price: price || 'N/A',            priceTxt: priceTxt || 'N/A',            by: by.name ? by : { name: 'N/A', link: 'N/A' }        });    }    console.log(JSON.stringify(data, null, 2)); // 打印结构化的JSON数据})().catch(err => console.error("An error occurred:", err)).finally(() => {    // 确保浏览器在任何情况下都被关闭    if (browser) {        browser.close();    }});

4. 注意事项与最佳实践

选择器稳定性: 网页结构随时可能更新,导致选择器失效。定期检查并更新选择器是维护爬虫的必要工作。错误处理: 使用try…catch块包裹可能失败的选择器,以防止因某个元素缺失而导致整个爬虫崩溃。等待机制: 对于动态加载的页面,使用page.waitForSelector()或page.waitForNavigation()等方法确保元素在尝试抓取之前已经加载完成。无头模式(Headless Mode): 在开发和调试阶段,可以将headless设置为false,以便观察浏览器行为。在生产环境中,应将其设置为true以提高性能和效率。负责任的抓取: 遵守网站的robots.txt协议,不要对网站造成过大负载。考虑添加延迟(page.waitForTimeout())来模拟人类行为,避免被封禁IP。IP限制与代理: 频繁的请求可能会导致IP被网站封禁。在生产环境中,可能需要结合代理IP池来规避此问题。API优先: 尽管本教程专注于网页抓取,但如果目标网站提供官方API,始终优先考虑使用API,因为这通常更稳定、合法且高效。

总结

通过本教程,我们学习了如何使用Puppeteer从TripAdvisor这样的动态网站抓取结构化数据。关键在于理解Puppeteer的工作原理,构建稳定的CSS选择器,并采用分步遍历的方式提取多字段信息。掌握这些技术,你将能够应对各种复杂的网页抓取挑战,为数据分析、市场研究等提供有力支持。记住,在进行任何网页抓取活动时,始终要遵守道德规范和法律法规。

以上就是使用Puppeteer高效抓取TripAdvisor景点数据:完整指南的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1520749.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫

关于作者

上一篇 2025年12月20日 13:36:07
下一篇 2025年12月20日 13:36:23

相关推荐

发表回复

登录后才能评论
关注微信