ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Selenium实战:携程酒店数据采集与数据分析

2026/9/12 20:53:52 拓冰建站 浏览量
Selenium实战:携程酒店数据采集与数据分析 简介基于Selenium的携程酒店爬虫项目内附简单数据分析模块定位为Python网络爬虫入门级的完整练习资源面向计算机相关专业在校学生、教师及希望以项目驱动方式提升技能的开发者同样适用于课程设计、毕业设计或项目初期立项演示。资源包共6个文件涵盖Python源码、Jupyter Notebook分析脚本、Markdown详细文档、文本说明、License及.gitattributes配置压缩后仅136KB轻量但结构完整。py与ipynb均可直接运行md与txt对爬虫设计思路、字段含义和数据分析步骤作了必要记录目录划分清晰便于按模块阅读、调试与二次开发。目前已有106人学习下载项目代码经过测试运行成功可在理解Selenium页面自动化与数据提取逻辑的基础上继续扩展酒店价格监控、可视化图表或接入数据库等功能。整体而言这份资源提供一个可运行、可调试、可扩展的微型项目范例能让初学者从页面抓取到初步分析完整走通一遍是实操性较强的入门级爬虫资料。1. 为什么携程酒店数据要选 Selenium 而不是直接请求接口做酒店竞对分析时最耗时间的不是写 SQL而是怎么拿到干净的价格数据。携程酒店列表页是典型的异步渲染页面requests 直接抓回来的 HTML 里只有页面外壳真正带价格、评分、房型的节点要等浏览器执行完大量 JS 才会出现。Selenium 通过驱动真实浏览器执行页面脚本能越过前端渲染逻辑直接拿到人眼看到的 DOM 状态而且天然带着浏览器指纹比纯手工构造请求头更容易通过基础风控。这个 ctrip-hotel-spider 项目就是用 Selenium 加 pandas 串起来的先驱动 Chrome 搜索酒店再抽取列表字段最后落成 CSV 做简单分析。对正在做课程设计、刚入门爬虫、以及需要批量整理酒店运营数据的读者这套链路可以直接复用到携程以外的动态渲染站点。2. Selenium 环境配置与首次请求为什么需要 ChromeDriver2.1 动态渲染页面与 requests 的边界携程酒店列表页背后的数据接口通常是一串经过加密签名的 JSON参数里带有时间戳、地理位置、设备指纹等字段直接用 requests 重放很难稳定通过。即便费劲抓到了接口后续字段变更也要重新分析一遍网络请求维护成本很高。Selenium 的思路是换一个角度不关心接口怎么签名只管“用户能看到什么”然后从渲染完成的 DOM 里抽取目标字段。这种方式对页面改版的抗性更好因为前端展示结构比接口签名稳定得多而且 Selenium 能自动处理翻页、滚动、下拉框点击等交互动作。2.2 环境准备Selenium 与 ChromeDriver 版本匹配主力浏览器推荐 Chrome因为 Selenium 对 ChromeDriver 的兼容性资料最多。需要先确认本地 Chrome 版本号再下载对应版本的 ChromeDriver。版本不匹配时脚本会在启动阶段直接抛SessionNotCreatedException提示信息里通常会写明期望的 driver 版本范围。pip install selenium pandas matplotlib安装完成后把chromedriver.exe放在系统 PATH 目录或者把路径传给webdriver.Chrome(executable_path...)。新版 Selenium 4 建议直接用webdriver.Chrome()让 Selenium Manager 自动处理驱动下载但国内网络环境可能不稳定手动管理版本更可控。常见的版本对应关系如下表。Chrome 版本ChromeDriver 版本主要变化Chrome 115115.0.5790.170支持chrome for testingChrome 116116.0.5845.96修复部分页面加载等待问题Chrome 117117.0.5938.149增加移动端模拟选项Chrome 120120.0.6099.109稳定支持无头模式2.3 最小脚本用 Selenium 打开携程酒店搜索页用一段最简代码验证环境是否跑通。这里需要注意携程的页面结构会随着前端版本变化CSS 选择器要按实际页面调整下面的选择器只是一个常见示例。from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC opts Options() # 无头模式适合服务器但更容易被风控识别本地调试建议注释掉 # opts.add_argument(--headless) opts.add_argument(--window-size1920,1080) # 去除自动化控制标志降低被前端检测的概率 opts.add_argument(--disable-blink-featuresAutomationControlled) opts.add_experimental_option(excludeSwitches, [enable-automation]) drv webdriver.Chrome(optionsopts) wait WebDriverWait(drv, 20) url https://hotels.ctrip.com/hotels/list?countryId1city0201 drv.get(url) # 等待酒店列表项出现确保核心 DOM 渲染完成 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, div.hotel-list-item))) print(title:, drv.title) print(current_url:, drv.current_url) drv.quit()代码里的核心逻辑是先构造 Chrome 配置然后通过WebDriverWait等待一个特定节点出现。presence_of_element_located和visibility_of_element_located的区别在于前者只要求节点出现在 DOM 中后者还要求节点有尺寸和可见性对需要抓取价格和评分的场景建议用后者避免拿到仍在加载中的隐藏节点。--disable-blink-featuresAutomationControlled是常见的一项反检测配置它不会完全伪装成真用户但可以过滤掉一部分执行环境扫描脚本实际使用要注意合法合规。3. 携程酒店搜索页的字段提取与数据清洗3.1 页面结构与元素定位策略打开携程酒店列表页后核心数据都集中在.hotel-list-item节点下。每个酒店项内部有酒店名、商圈、评分、评论数、价格、标签等字段。定位时我习惯优先用 CSS 选择器比 XPath 更简洁只有在属性值包含动态 class 时才退回到 XPath 的contains()表达式避免写死变化的后缀。def parse_hotel_items(drv): 从已加载完成的列表中抽取酒店字段 rows [] items drv.find_elements(By.CSS_SELECTOR, div.hotel-list-item) for it in items: try: name it.find_element(By.CSS_SELECTOR, a.hotel-name).text.strip() except Exception: name None try: price it.find_element(By.CSS_SELECTOR, span.hotel-price).text.strip() except Exception: price None try: score it.find_element(By.CSS_SELECTOR, span.hotel-score).text.strip() except Exception: score None try: comment it.find_element(By.CSS_SELECTOR, span.comment-count).text.strip() except Exception: comment None rows.append({ name: name, price: price, score: score, comment: comment, }) return rowsfind_element找不到元素时默认抛出NoSuchElementException在列表页中有些酒店可能没有评分或价格用try-except把字段置为None比中断整个任务更合理。字段名name、price、score、comment会在后续 pandas 分析中作为列名最好保持英文避免编码问题。3.2 处理非原生下拉框div/ul/li 组合携程的城市选择和日期选择器都不是原生select标签而是div包裹ul、li的下拉框。热搜里一直有很多人问这种情况怎么处理其实关键不在于选择器而在于“先触发显示再等待选项可点击”。from selenium.webdriver.common.keys import Keys import time def select_city(drv, city_name): # 点击城市输入框让下拉面板显示 city_input drv.find_element(By.CSS_SELECTOR, input.city-input) city_input.click() time.sleep(1) # 输入关键词触发联想列表 city_input.clear() city_input.send_keys(city_name) # 等待联想项出现而不是直接 sleep WebDriverWait(drv, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, ul.city-list li)) ) # 在所有联想项中精确匹配目标城市避免误触“热门城市” matches drv.find_elements(By.CSS_SELECTOR, ul.city-list li) for li in matches: if city_name in li.text: li.click() break # 等待酒店列表刷新 WebDriverWait(drv, 20).until( EC.staleness_of(drv.find_element(By.CSS_SELECTOR, div.hotel-list-item)) )这段代码有几个细节city_input.clear()之后要再send_keys不能省略联想列表出现前点击目标li会报ElementClickInterceptedException所以必须先等待可见。staleness_of是在等待旧列表节点失效旧节点一旦被页面刷新替换说明数据已经重新加载这时候再去拿新列表才有效。很多入门级教程用time.sleep(5)代替等待那会拖慢整体采集而且网络慢时容易误判。3.3 滚动加载与动态等待酒店列表是滚到底部才加载下一页或追加新数据直接find_elements只能拿到当前已渲染的部分。常用做法是反复滚动到底部直到出现“没有更多结果”或达到指定页数。def scroll_to_load(drv, max_scrolls20): last_count 0 for _ in range(max_scrolls): items drv.find_elements(By.CSS_SELECTOR, div.hotel-list-item) now_count len(items) if now_count last_count: # 两次滚动后数量不变大概率已经到底 break last_count now_count drv.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) return last_count滚动动作执行后不能立刻统计新节点因为渲染有延迟。这里的time.sleep(2)是一个兜底策略真正稳定的做法是等待某个“第 N 项”出现比如滚动后等待div.hotel-list-item:nth-child(n20)但选择器会随列表结构变化所以我一般用数量变化加短延时的组合。max_scrolls要设上限避免页面异常时无限循环。3.4 数据清洗类型转换与缺失值处理采集后的原始字段都是字符串price可能是¥ 598、¥ 833起score可能是4.7/5或暂无评分comment可能是1.2万条点评。在做分析前必须统一清洗成数值型。import pandas as pd import re def clean_dataframe(df): # 移除空行 df df.dropna(subset[name, price]) # 价格提取纯数字兼容“¥ 833起” df[clean_price] df[price].apply( lambda x: int(re.sub(r[^\d], , x.split(起)[0].split(/)[0])) if x and re.search(r\d, x) else None ) # 评分截取小数点前一位和后一位如 4.7 df[clean_score] df[score].apply( lambda x: float(re.search(r\d\.\d, x).group()) if x and 暂无 not in x and re.search(r\d\.\d, x) else None ) # 点评数将“1.2万”转换为 12000 def parse_comment(c): if not c: return None m re.search(r([\d.])(万)?, c) if not m: return None num float(m.group(1)) return int(num * 10000) if m.group(2) else int(num) df[clean_comment] df[comment].apply(parse_comment) return df清洗逻辑里要注意“起”和“万”这两个中文字符。价格¥ 598提取后是598¥ 833起提取后要先把“起”截掉否则re.sub会把“起”也删掉整数转换没问题但语义不正确。点评数处理时暂无点评会返回None后续分析时可以直接按缺失值处理。清洗后的字段用新列保存保留原始字段方便回溯。清洗结束后可以输出一张字段说明表作为数据质量检查的辅助。原始字段清洗列类型示例priceclean_priceint598scoreclean_scorefloat4.7commentclean_commentint120004. 用 pandas 与 matplotlib 做酒店价格和评分分析4.1 从 CSV 读入数据的字段约定采集完成后把所有酒店记录追加到一个 CSV 文件然后用 pandas 读入。读入时要指定dtype保持字符串避免 ID 类字段被转成数字。下面代码是项目里demo.ipynb常见的一段数据分析开头。df pd.read_csv(hotels.csv, dtype{name: str, price: str}) df clean_dataframe(df) print(df.shape) print(df.head())df.shape能快速确认总共抓到了多少条。如果采样后clean_price有大量空值说明选择器或清洗正则没有覆盖到所有价格格式要回溯到采集阶段。这里我一般会同时输出df.info()观察每一列的非空数量找出脏数据最多的字段。4.2 价格分布与商圈对比价格分布是酒店运营分析的第一张图。直方图能看出样本集中在哪个价位段箱线图能对比不同商圈的价格中位数。下面这段代码主要用matplotlib完成。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 处理中文乱码 plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) # 价格直方图 axes[0].hist(df[clean_price].dropna(), bins30, edgecolorblack) axes[0].set_title(酒店价格分布) axes[0].set_xlabel(价格元) axes[0].set_ylabel(酒店数量) # 价格箱线图 df.boxplot(columnclean_price, bydistrict, axaxes[1]) axes[1].set_title(不同商圈价格对比) axes[1].set_xlabel(商圈) plt.tight_layout() plt.savefig(price_distribution.png, dpi150)SimHei是 Windows 常见中文字体Linux 服务器需要换成其他字体也可以直接font_manager指定字体路径。历史图上clean_price中的异常值比如 20000 元以上的酒店房间会让直方图右边拖出长尾影响观察分析前可以过滤掉后 1% 的极值。4.3 评分与评论数的关系评分和评论数能反映酒店热度与口碑的关系。通常评论数多的酒店评分会更趋近于稳定值而评论极少的高分酒店可能存在刷分风险。散点图可以把这种关系直观呈现出来。scatter_df df.dropna(subset[clean_score, clean_comment]) plt.figure(figsize(10, 6)) plt.scatter(scatter_df[clean_comment], scatter_df[clean_score], alpha0.6) plt.xscale(log) # 评论数跨度大用对数刻度 plt.xlabel(评论数对数刻度) plt.ylabel(评分) plt.title(酒店评分与评论数关系) plt.grid(alpha0.3) plt.savefig(score_comment.png, dpi150)使用对数刻度会让点分布更均匀避免几万条点评的头部酒店把其他点压到左侧。如果发现左下角聚集一批低分低评论的酒店那通常是新开业或位置偏的房源可以单独提出来作为筛选条件。4.4 保存分析结果分析完成后把清洗和聚合后的数据写回新文件方便后续做报表。to_excel需要安装 openpyxl也可以在同一个 IPython notebook 里导出图片和表格。summary df.groupby(district).agg( avg_price(clean_price, mean), max_price(clean_price, max), avg_score(clean_score, mean), hotel_count(name, count) ).reset_index() summary.to_excel(district_summary.xlsx, indexFalse)groupby(district)要求数据中确实有商圈字段。如果采集阶段没存商圈可以从酒店名或 URL 参数里想办法补充。这个汇总表直接展示了每个商圈的平均价格、最高价格和平均评分是“简单数据分析”里最有用的产出之一。5. 爬虫稳定性优化与完整性校验5.1 处理 Selenium 常见异常抓取过程中最常见的三类异常是NoSuchElementException、StaleElementReferenceException和TimeoutException。前两个的本质是页面状态变化导致旧节点失效Timeout 则是因为网络或风控拦截。一个简单的重试装饰器可以让整个流程稳定很多。import functools, time def retry_on_failure(max_retries3, delay3): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt max_retries - 1: raise time.sleep(delay * (attempt 1)) return None return wrapper return decorator重试间隔用线性退避而不是固定值第一次失败等 3 秒第二次等 6 秒给前端渲染留出缓冲。如果连续失败应该记录下当前 URL 和失败位置而不是默默跳过。5.2 多线程并发与访问频率控制一个浏览器实例逐条滚动确实慢可以用 ThreadPoolExecutor 同时跑多个浏览器实例。线程数要控制在 3 到 5 之间同时必须给每个线程单独的 ChromeOptions避免共享 profile 导致崩溃。from concurrent.futures import ThreadPoolExecutor def fetch_one(city): # 每个线程内部独立创建 driver opts Options() opts.add_argument(--disable-blink-featuresAutomationControlled) drv webdriver.Chrome(optionsopts) try: drv.get(fhttps://hotels.ctrip.com/hotels/list?city{city}) # 执行采集逻辑... return parse_hotel_items(drv) finally: drv.quit() cities [0201, 0101, 0301] with ThreadPoolExecutor(max_workers3) as pool: results pool.map(fetch_one, cities)这里要强调并发不是越快越好。页面请求频率过高会触发服务端风控轻则验证码重试重则 IP 进入黑名单。线程数 3 到 5 只是一个经验值实际要根据页面响应速度和本地带宽调整。更稳妥的做法是在每个循环内增加随机time.sleep(2, 6)把访问节奏变得接近人工操作。5.3 完整性校验总数比对和日志抓完一批数据怎么确认没有漏掉酒店一种可靠方式是比对“列表底部显示的总结果数”和“实际抓到的记录数”。携程列表页通常会在底部显示“共 X 家酒店”这个数字可以通过选择器读取。total_text drv.find_element(By.CSS_SELECTOR, span.total-count).text expected int(re.search(r\d, total_text).group()) actual len(all_rows) print(fexpected{expected}, actual{actual}) if actual expected: print(采集不完整需要增加滚动次数)滚动次数写死的情况下实际数量往往比总数少这是因为列表只加载了前几页。反过来如果实际数量大于总数可能是统计口径不同比如包含了推荐位或广告位这时候要检查选择器是否匹配到了模块外的节点。校验结果写入日志可以方便后续增量抓取时对比。本文还有配套的精品资源点击获取