0

0

使用Puppeteer抓取TripAdvisor旅游景点数据:从基础到高级实践

心靈之曲

心靈之曲

发布时间:2025-09-19 12:08:15

|

711人浏览过

|

来源于php中文网

原创

使用Puppeteer抓取TripAdvisor旅游景点数据:从基础到高级实践

本教程详细介绍了如何使用Node.js的Puppeteer库高效抓取TripAdvisor网站上的旅游景点数据。文章从解决常见的选择器错误入手,逐步演示如何提取景点标题、链接、图片、描述、价格和作者等关键信息,并提供了完整的示例代码和最佳实践,帮助开发者构建健壮的网页爬虫。

理解网页抓取挑战与Puppeteer

在进行网页抓取时,尤其面对像tripadvisor这类动态加载内容的网站,传统的http请求方式往往难以奏效。这些网站大量使用javascript来渲染页面内容,导致通过查看页面源代码无法直接获取所需数据。此时,像puppeteer这样的无头浏览器工具就显得尤为重要。

Puppeteer是一个Node.js库,它提供了一组高级API来控制Chrome或Chromium。通过Puppeteer,我们可以模拟用户在浏览器中的操作,例如打开页面、点击元素、填写表单,并等待JavaScript执行完毕,从而获取到完全渲染后的页面内容。这使得抓取动态网站变得可行。

核心问题:CSS选择器定位

在Puppeteer中,准确地定位页面元素是成功抓取数据的关键。这通常通过CSS选择器来完成。许多初学者在尝试抓取数据时,常常会遇到选择器错误,导致无法获取到预期的元素或数据。这可能是由于以下原因:

  1. 选择器不准确:未能精确匹配目标元素。
  2. 元素未加载:在选择器执行时,目标元素尚未通过JavaScript渲染到DOM中。
  3. 动态类名:网站可能使用动态生成的类名,导致选择器在页面刷新后失效。

例如,针对TripAdvisor景点列表页,要抓取列表项的标题,原始的选择器可能不够精确。以下是几种更健壮的选择器示例:

修正标题选择器的方法:

假设我们希望抓取TripAdvisor景点列表中的标题。我们可以通过检查页面结构,找到更稳定、更具描述性的CSS选择器。

  1. 使用article作为基础容器并精确定位内部链接:

    const places = await page.evaluate(() => 
      Array.from(document.querySelectorAll('article'), (e) => ({
        title: e.querySelector('.VLKGO a:not([class])').innerText
      }))
    );

    这里,我们首先找到每个article元素(通常代表一个独立的列表项),然后在每个article内部,通过.VLKGO a:not([class])来定位到标题链接。.VLKGO是一个包含标题的父容器,a:not([class])则确保我们选择的是没有额外类名的链接,这通常是标题链接的特征,避免选中其他辅助链接。

  2. 直接定位标题文本的父元素:

    let places = await page.$$eval('article .VLKGO span > div', el => 
      el.map(x => x.textContent)
    );

    这种方法利用$$eval在浏览器环境中执行JavaScript,直接选择所有匹配article .VLKGO span > div的元素,并提取它们的textContent。span > div进一步缩小了范围,确保我们直接获取到标题文本。

这些修正后的选择器,通过更具体或更稳定的元素结构来定位,大大提高了抓取的成功率。

Facetune
Facetune

一款在线照片和视频编辑工具,允许用户创建AI头像

下载

构建全面的数据提取逻辑

仅抓取标题通常不足以满足需求。在实际应用中,我们可能需要提取更多信息,例如链接、图片、描述、价格和作者等。为了实现这一点,我们可以结合使用page.$$和element.$eval。

  • page.$$('selector'):在页面上下文中选择所有匹配的元素,并返回这些元素的ElementHandle数组。
  • element.$eval('selector', callback):在特定ElementHandle的上下文中执行回调函数,并返回回调函数的返回值。这允许我们在每个列表项的内部进行更精细的元素查找。

下面是一个完整的Puppeteer脚本,演示如何从TripAdvisor页面抓取包括标题、链接、图片、描述、价格和作者在内的多项数据:

const puppeteer = require("puppeteer");

let browser; // 声明浏览器实例变量,以便在finally块中关闭

(async () => {
    browser = await puppeteer.launch({ headless: true }); // 建议生产环境设置为true
    const page = await browser.newPage();

    let url = 'https://www.tripadvisor.com/Attractions-g297476-Activities-c42-Cartagena_Cartagena_District_Bolivar_Department.html';

    // 导航到目标URL,并等待页面加载完成和关键元素出现
    await page.goto(url, { waitUntil: 'load', timeout: 30000 }); // 增加超时时间
    await page.waitForSelector('main', { timeout: 10000 }); // 等待主要内容区域加载

    // 获取所有代表一个景点的article元素
    let places = await page.$$('article');

    let data = [];
    for (let place of places) {
        try {
            // 提取标题和链接
            let 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 };
            });

            // 提取图片URL
            let image = await place.$eval('picture > img[srcset]', el => el.getAttribute('srcset'));
            // 从srcset中获取最大尺寸的图片URL
            image = image.split(',').pop().replace(/2x/gi, '').trim();

            // 提取描述
            let desc = await place.$eval('a:not([class]) > div > span', el => el.textContent.trim());

            // 提取作者信息
            let 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 };
            });

            // 提取价格(如果存在)
            let price = null;
            let priceTxt = null;
            try {
                const priceEl = await place.$('[data-automation=cardPrice]');
                if (priceEl) {
                    price = await priceEl.evaluate(el => el.textContent);
                }
            } catch (error) {
                // 价格元素可能不存在,忽略错误
            }

            try {
                const priceTxtEl = await place.$('div:nth-child(1) > div:nth-child(3):not([class])');
                if (priceTxtEl) {
                    priceTxt = await priceTxtEl.evaluate(el => el.textContent);
                }
            } catch (error) {
                // 价格文本元素可能不存在,忽略错误
            }

            data.push({
                name: header.name,
                link: header.link,
                desc: desc,
                image: image,
                price: price,
                priceTxt: priceTxt,
                by: by
            });
        } catch (error) {
            console.error("在处理某个景点时发生错误:", error.message);
            // 可以选择跳过当前景点或记录错误信息
        }
    }

    console.log(data);
    await browser.close();

})().catch(err => console.error("抓取过程中发生未捕获错误:", err))
    .finally(() => {
        if (browser) {
            browser.close(); // 确保在任何情况下都关闭浏览器
        }
    });

代码解析与最佳实践

  1. puppeteer.launch({ headless: true }):

    • headless: true 表示在无头模式下运行浏览器,即不显示浏览器UI,这在生产环境中是推荐的,可以节省资源。
    • 调试时可以设置为false,以便观察浏览器行为。
  2. page.goto(url, { waitUntil: 'load', timeout: 30000 }):

    • waitUntil: 'load':等待页面的load事件触发。对于动态页面,'domcontentloaded'可能不够。更稳妥的选项是'networkidle0'(网络空闲,没有超过0个网络连接)或'networkidle2'(网络空闲,没有超过2个网络连接),但它们可能导致更长的等待时间。
    • timeout:设置页面加载的超时时间,防止页面卡住。
  3. page.waitForSelector('main', { timeout: 10000 }):

    • 在尝试抓取元素之前,等待页面上某个关键元素(如main内容区)出现。这可以确保页面内容已经渲染,避免因元素未加载而导致的抓取失败。
  4. page.$$('article') 与 place.$eval():

    • page.$$('article') 获取页面上所有符合条件的根级元素(这里是每个景点列表项的article标签)。
    • 通过for...of循环遍历这些ElementHandle。
    • 在循环内部,使用place.$eval(selector, callback)来在当前article元素的上下文中查找子元素并提取数据。这种方法比多次使用page.evaluate效率更高,因为它避免了频繁地在Node.js环境和浏览器环境之间切换。
  5. 数据清洗与处理:

    • 标题: el.textContent.split('.').pop().trim() 用于去除标题前的序号(如 "1. "),并清除首尾空格。
    • 图片: image.split(',').pop().replace(/2x/gi, '').trim() 用于从srcset属性中提取最大尺寸的图片URL。srcset通常包含多个尺寸的图片链接,我们取最后一个(通常是最大的),然后移除2x标识并清除空格。
    • 作者: el.textContent.replace('By ', '').trim() 用于去除作者名前的"By "前缀。
    • 价格: 价格和价格文本元素可能不是每个列表项都存在,因此使用try...catch块来处理可能出现的ElementHandle.evaluate错误,并检查元素是否存在 (if (priceEl))。
  6. 错误处理:

    • 在for...of循环内部使用try...catch可以确保即使某个景点的数据提取失败,整个抓取过程也能继续进行,避免因单个错误导致程序中断。
    • 外部的.catch().finally()块用于捕获整个异步操作链中的未处理错误,并确保浏览器实例在任何情况下都能被正确关闭,防止资源泄露。

注意事项

  • 网站结构变化: 网站的HTML结构可能会随时更新。如果您的选择器突然失效,请重新检查目标网站的DOM结构并更新选择器。
  • 反爬机制: TripAdvisor等网站可能有反爬虫机制,例如IP限制、验证码、用户行为分析等。频繁或高速的抓取可能会触发这些机制。本教程的代码仅是基础实现,并未包含高级反爬策略。
  • 合法合规性: 在进行网页抓取前,请务必阅读网站的robots.txt文件和用户协议,确保您的行为合法合规。尊重网站数据所有权,避免对网站服务器造成不必要的负担。

总结

通过本教程,您应该已经掌握了使用Puppeteer抓取TripAdvisor旅游景点数据的基本方法和进阶技巧。从选择器的精确定位到复杂数据的结构化提取,再到错误处理和最佳实践,这些都是构建健壮、高效网页爬虫的关键要素。记住,深入理解目标网站的DOM结构,并灵活运用Puppeteer提供的API,是成功进行网页抓取的基石。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

538

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

373

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

727

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

471

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

390

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

989

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

653

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

540

2023.09.20

ip地址修改教程大全
ip地址修改教程大全

本专题整合了ip地址修改教程大全,阅读下面的文章自行寻找合适的解决教程。

81

2025.12.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Sass 教程
Sass 教程

共14课时 | 0.7万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.7万人学习

CSS教程
CSS教程

共754课时 | 17万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号