使用BeautifulSoup爬取动态网站可预约日期,点击下月不生效如何解决?
问题背景
我需要爬取https://seeksophie.com/options/1-5hr-basic-candle-workshop站点的活动预约信息,核心目标是获取未来1年的所有可预约日期,站点的日期选择器用span元素渲染,可通过notAllowed、flatpickr-disabled两个类属性筛选不可预约日期,后续还需要获取指定日期的可预约时段,当前优先解决日期爬取问题。
我最初的实现思路是循环点击下个月按钮,逐页提取当前页的日期span元素,现有代码如下:
def find_all_span(self, soup): new_soup = soup.__copy__() all_spans = [] for i in range(12): days_container = new_soup.find_all("div", {"class": "dayContainer"}) spans = days_container[2].find_all("span") all_spans.extend(spans) next_month_clicker = self.page_loader.driver.find_element_by_id( "js-placeholder-booking-form-accommodation-date") self.page_loader.driver.execute_script("arguments[0].click();", next_month_clicker) next_month_clicker = self.page_loader.driver.find_elements_by_class_name("flatpickr-next-month") self.page_loader.driver.execute_script("arguments[0].click();", next_month_clicker[2]) page_response = self.page_loader.driver.page_source new_soup = BeautifulSoup(page_response, 'html.parser') for span in spans: print(span["aria-label"]) return list(set(all_spans))
注:传入的soup是使用HTML Parser解析页面响应得到的BeautifulSoup对象,当前代码仅能获取1个月内的span元素,点击下月按钮后页面源码没有更新,无法获取后续月份的日期。
问题原因
- 点击下月按钮后未设置等待时间,flatpickr日期组件切换月份存在前端渲染延迟,立刻获取page_source拿到的还是切换前的旧页面内容
- 循环中重复点击日期输入框唤起日历的操作冗余,可能导致组件状态重置
- 直接对BeautifulSoup元素用set去重逻辑错误,会丢失元素属性,且无法正确去重
修复方案
1. 现有逻辑优化版
调整点击逻辑,增加显式等待,修改去重规则:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def find_all_span(self, init_soup): all_dates = [] # 首次唤起日历 date_input = self.page_loader.driver.find_element_by_id("js-placeholder-booking-form-accommodation-date") self.page_loader.driver.execute_script("arguments[0].click();", date_input) # 等待日历加载完成 WebDriverWait(self.page_loader.driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "dayContainer")) ) current_soup = init_soup seen_labels = set() for i in range(12): days_container = current_soup.find_all("div", {"class": "dayContainer"}) spans = days_container[2].find_all("span") for span in spans: label = span.get("aria-label") if label and label not in seen_labels: seen_labels.add(label) # 保存需要的属性 all_dates.append({ "date": label, "is_disabled": "flatpickr-disabled" in span.get("class", []) or "notAllowed" in span.get("class", []) }) print(label) # 点击下个月 next_btn = self.page_loader.driver.find_elements_by_class_name("flatpickr-next-month")[2] self.page_loader.driver.execute_script("arguments[0].click();", next_btn) # 等待日历容器更新,判断标准:新的dayContainer加载完成 WebDriverWait(self.page_loader.driver, 10).until( lambda d: len(d.find_elements(By.CLASS_NAME, "dayContainer")) >=3 ) # 等待100ms确保渲染完成 self.page_loader.driver.implicitly_wait(0.1) current_soup = BeautifulSoup(self.page_loader.driver.page_source, 'html.parser') return all_dates
2. 更稳定的实现思路
直接调用前端组件API
该站点使用flatpickr日历组件,所有日期状态都存在组件实例中,无需逐页点击,直接执行JS就能拿到所有数据:
# 直接执行JS获取所有禁用日期和可用日期 disable_dates = self.page_loader.driver.execute_script(""" const fp = document.querySelector('#js-placeholder-booking-form-accommodation-date')._flatpickr; return fp.config.disable; """) # 或者直接遍历拿未来1年所有日期的状态 all_date_status = self.page_loader.driver.execute_script(""" const fp = document.querySelector('#js-placeholder-booking-form-accommodation-date')._flatpickr; const result = []; const now = new Date(); const oneYearLater = new Date(now.getFullYear()+1, now.getMonth(), now.getDate()); for(let d = new Date(now); d <= oneYearLater; d.setDate(d.getDate()+1)){ const isDisabled = fp.config.disable.some(dis => { if(typeof dis === 'function') return dis(d); if(dis instanceof Date) return d.toDateString() === dis.toDateString(); return false; }) || fp.isDisabled(d); result.push({date: d.toLocaleDateString('en-CA'), is_disabled: isDisabled}); } return result; """)
抓后端接口
打开浏览器开发者工具的网络面板,筛选XHR请求,选择日期时会有请求拉取对应月份的可预约状态,直接模拟请求接口即可拿到全量数据,比模拟前端操作稳定性更高。
内容的提问来源于stack exchange,提问作者Errata
相关产品推荐
相关产品推荐

