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

Puppeteer爬取希尔顿酒店价格失败:日期选择函数失效求助

问题描述

后续问题

我的fill_stayDates函数无法选择日历日期,导致无法输出酒店价格。请问需要修改fill_stayDates辅助函数的哪些部分?该函数之前可用但现在失效了,另外我该如何整合START_YEAR和END_YEAR参数?

新代码

const puppeteer = require("puppeteer"); // ^21.4.1

const constants = {};
constants.START_MONTH = 'Jan'
constants.START_DAY = '28'
constants.START_YEAR = '2024' // 意识到需要把这个整合到代码里

constants.END_MONTH = 'Feb'
constants.END_DAY = '3'
constants.END_YEAR = '2024' // 意识到需要把这个整合到代码里

const url = "https://www.hilton.com/en/";
const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36";

let browser;
(async () => {
  browser = await puppeteer.launch({
    // * 测试时选`false`可以关闭无头模式
    headless: false
    });
  const [page] = await browser.pages();
  await page.setUserAgent(ua);
  await page.setViewport({ width: 1065, height: 10700 });
  await page.goto(url, {waitUntil: "domcontentloaded"});

  // 填写目的地
  await page.type('[class="form-input w-full"]', "Seattle, WA, USA");  

  // 点击日历UI选择日期
  await page.click('[data-testid="search-dates-button"]');
  await page.waitForSelector('[data-testid="calendar-container"]');

  // ! 这里应该失效了
  await fill_stayDates(page, constants);

  // 点击"查找酒店"按钮
  await page.click('[data-testid="search-submit-button"]');

  // 获取价格
  const sel = 'li[class^="border-border"] p[data-testid="priceInfo"]';
  await page.waitForSelector(sel);
  const prices = await page.$$eval(
    sel,
    els => els.map(el => el.textContent.trim())
  );
  console.log(prices);
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());


//* 辅助函数:点击切换到正确的起始月份
async function fill_stayDates(page, constants) {
    await page.waitForTimeout(3000); // 测试用延迟
    const findMonth = async (targetMonth) => {
      for (let i = 0; i < 12; i++) {
        const month = await page.$eval('[id^="calendar-month-"]', (el) => el.textContent);
        if (month.includes(targetMonth)) break;
  
        const arrows = await page.$$(
          '[data-testid="calendar-container"] > [class="flex justify-between"] button:not([disabled])'
        );
        await arrows[arrows.length - 1].click();
      }
    };
  
    const chooseDay = async (day) => {
      await page.waitForTimeout(3000); // 测试用延迟
      const dayResult = await page.$$eval(
        'button[id*=day-] span:not([class*=sr-only])',
        (elements) => elements.map((element) => element.textContent)
      );
      const dayIndex = dayResult.indexOf(`${day}`);
      await page.$$('button[id*=day-] span:not([class*=sr-only])').then((el) => el[dayIndex].click());
    };
  
    await findMonth(constants.START_MONTH);
    await chooseDay(constants.START_DAY);
    await findMonth(constants.END_MONTH);
    await chooseDay(constants.END_DAY);
  
    // 关闭弹窗
    await page.click('[class="stroke-text"]');
}

原始问题

我尝试爬取酒店价格,但无法定位价格元素。之前用以下代码能生效,但现在返回空数组[]:

page.$$eval('li[class^="border-border"] p[data-testid="priceInfo"]', elements => elements.map(el => el.textContent.trim()))
const [prices] = await Promise.all([
    page.$$eval('li[class^="border-border"] p[data-testid="priceInfo"]', elements => elements.map(el => el.textContent.trim()));
]);

console.log('prices:', prices);

解决方案

一、修复fill_stayDates函数的日期选择问题

1. 改进月份定位逻辑,加入年份校验

原函数只判断月份,忽略年份会导致跨年度日期选择出错。修改findMonth函数,同时校验月份缩写和年份:

const findMonth = async (targetMonth, targetYear) => {
  for (let i = 0; i < 24; i++) { // 扩大循环范围,覆盖跨年度情况
    const headerText = await page.$eval('[id^="calendar-month-"]', el => el.textContent.trim());
    const [currentMonth, currentYear] = headerText.split(' ');
    const monthAbbr = currentMonth.slice(0, 3); // 提取月份缩写(如January→Jan)
    if (monthAbbr === targetMonth && currentYear === targetYear) break;

    const nextArrow = await page.$('[data-testid="calendar-next-button"]');
    if (!nextArrow) break; // 无下一页箭头则停止循环
    await nextArrow.click();
    await page.waitForTimeout(500); // 等待月份切换完成,替换长延迟
  }
};

2. 优化日期点击逻辑,避免元素引用失效

原chooseDay函数两次调用page.$$容易导致元素引用过期,改成直接通过文本匹配定位按钮:

const chooseDay = async (day) => {
  const dayButton = await page.$(`button[id*=day-]:has(span:not([class*=sr-only]):text("${day}"))`);
  if (dayButton) {
    await dayButton.click();
    await page.waitForTimeout(500);
  } else {
    throw new Error(`找不到日期${day}`);
  }
};

3. 整合年份参数调用

修改函数调用处,传入年份参数:

await findMonth(constants.START_MONTH, constants.START_YEAR);
await chooseDay(constants.START_DAY);
await findMonth(constants.END_MONTH, constants.END_YEAR);
await chooseDay(constants.END_DAY);

4. 替换硬编码延迟为元素等待

去掉固定延迟,改用waitForSelector确保元素加载完成,提升稳定性:

async function fill_stayDates(page, constants) {
    await page.waitForSelector('[id^="calendar-month-"]'); // 等待日历标题加载

    const findMonth = async (targetMonth, targetYear) => {
      // 上面的改进代码
    };
  
    const chooseDay = async (day) => {
      // 上面的改进代码
    };
  
    await findMonth(constants.START_MONTH, constants.START_YEAR);
    await chooseDay(constants.START_DAY);
    await findMonth(constants.END_MONTH, constants.END_YEAR);
    await chooseDay(constants.END_DAY);
  
    // 关闭弹窗前先确认元素存在
    await page.waitForSelector('[class="stroke-text"]');
    await page.click('[class="stroke-text"]');
}

二、修复价格元素定位问题

原选择器失效大概率是页面结构更新,改用更稳定的选择器,并等待页面完全加载:

// 替换原价格获取代码
await page.waitForNavigation({ waitUntil: 'networkidle2' }); // 等待搜索结果页面加载完成
const priceSel = '[data-testid="property-card"] [data-testid="priceInfo"]';
await page.waitForSelector(priceSel);
const prices = await page.$$eval(priceSel, els => els.map(el => el.textContent.trim()));
console.log(prices);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 18:20:39