ARTICLE DETAIL

建站实战干货

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

Python数据采集与可视化实战:以携程酒店分析为例

2026/10/3 10:07:43 拓冰建站 浏览量
Python数据采集与可视化实战:以携程酒店分析为例 1. 携程项目拆解这版“hx4324”到底想做一个什么样的分析工具先说结论这个项目不是单纯写一段爬虫抓数据也不是纯粹画几张图表交差而是把“从网站拿数据”到“用图表说清楚一件事”的完整链路走通。项目代号里的 hx4324 不关键关键是它对应的整套流程——我平时带新人时也一直用类似的项目当作 Python 数据可视化的实操练习数据采集、清洗、分析、出图、写结论五步走完才算真正入门。1.1 项目的目标回答三个具体业务问题我在做这版项目时给自己定了三个必须要回答的问题这样后面所有代码都不会跑偏携程在售的酒店价格整体集中在什么区间不同城市之间有没有明显差异景点热度如何排序哪些景点是真正的“头部”哪些是长尾从数据角度能不能看出“评分”和“价格”之间的相关性有了这三个问题数据采集字段、清洗逻辑和可视化图表就都有了方向。很多人写爬虫项目容易迷茫就是因为只想“抓数据”却没想清楚“抓完数据之后要证明什么”。所以我在这个项目里第一步反而是先把分析目标写进代码注释里倒逼自己只采集和分析相关的字段。1.2 技术栈选型与理由这个项目我用的是 Python 3.8 以上的环境3.8 到 3.11 都验证过核心库是 requests、pandas、matplotlib外加 wordcloud 做景点名称词云。为什么不用 scrapy因为这种数据规模通常只有几百到上千条样本scrapy 的分布式框架和中间件配置对练习项目来说偏重调试起来也更繁琐。requests BeautifulSoup 打天下脚本控制在 300 行以内思路清楚排错方便。可视化部分我坚持用 matplotlib 作为主要出图工具pyecharts 只作为补充。原因很简单matplotlib 的结果在论文、报告、公众号文章里兼容性最好图形元素可控细粒度最高。pyecharts 做交互是好看但导出的 HTML 文件在后续排版时反而不够方便。数据分析阶段静图已经足够支撑结论。1.3 数据采集字段的设计我采集并落盘的字段如下字段名类型说明citystr城市名称hotel_namestr酒店名称pricefloat当晚参考价格ratingfloat用户评分rating_countint点评数量star_levelstr酒店星级districtstr所在行政区/商圈spot_namestr景点名称单独采集spot_hotint景点热度值为什么特意保留 district 字段因为我会用“城市 商圈”双重维度来分析酒店价格单纯看城市均值会掩盖区域差异。比如上海静安寺附近和浦东机场附近的酒店均价能差出三倍只有拆到商圈粒度结论才有业务参考价值。2. 数据采集的关键细节请求头、分页与频率控制这个环节是整个项目里最耗时的部分不是代码难写而是“能稳定地拿到想要的数据”需要反复试错。我踩过几个坑写出来供参考。2.1 请求头构建为什么不能直接裸请求携程这类商业站点对请求头是比较敏感的。裸用 requests.get(url) 发出去要么拿到一个验证页面要么直接拒绝服务。我当时的做法是先打开手机端页面或者桌面端浏览器按 F12 看一下实际网络请求里带了哪些请求头字段然后有选择地复现并构建一个 User-Agent 池在每次请求之间轮换。单纯不用代码嵌套代码段落多的效果这里给出一个简化版的复用模板import requests from bs4 import BeautifulSoup HEADERS_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36, ] def build_headers(index0): headers { User-Agent: HEADERS_POOL[index % len(HEADERS_POOL)], Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://hotels.ctrip.com/ } return headers关键点是 Accept-Language 不要漏掉。很多站点会根据语言字段返回不同结构的内容如果漏了这个字段解析出来的页码结构可能和你本地浏览器预览的不一致。2.2 页面解析思路从列表页抓摘要从详情页补信息列表页的 HTML 结构比较一致我直接解析列表页里每张酒店卡片取出酒店名、参考价、评分、点评数。然后再进入详情页补充商圈和星级。这里有一个现实问题如果每个酒店都进详情页再抓一次请求量会翻好几倍触发风控的概率也随之上升。我的取舍方案是先判断列表页已经给出哪些字段能拿到的就不进详情页只有缺少关键字段的时候才发起详情页请求。实测下来这个策略可以把请求总量压缩一半以上同时保证数据集完整。数据完整性和请求成本之间永远是先保证采样覆盖度再考虑节省请求。2.3 频率控制与异常重试机制“像人一样浏览”是爬虫教程里最空洞的话但落到代码里其实就那么几件事随机延时、失败重试、暂停冷却。我给每次请求设置的延时在 1.5 到 3.2 秒之间随机浮动间隔不是一个固定常数而是用 random.uniform 生成避免出现周期性的请求节奏。重试装饰器我写得比较简单但很实用import time import random from functools import wraps def retry_request(max_retry3): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retry): try: return func(*args, **kwargs) except Exception as e: if attempt max_retry - 1: raise e sleep_time random.uniform(2, 4) print(f请求失败{sleep_time:.1f}秒后重试{e}) time.sleep(sleep_time) return wrapper return decorator这个装饰器直接放在请求函数上面就能复用。如果连续失败超过 3 次我不会继续硬闯而是停下来打印日志检查是不是页面结构改了、登录态过期了或者请求频率太高导致被限流。我见过不少人遇到重试就狂刷次数结果越刷越糟这是必须避免的。2.4 边采边存与断点恢复采集过程中我保持“每拿到 50 条数据就追加写入 CSV”的习惯而不是全部跑完再一次写入。原因很朴素采集脚本一跑就是几十分钟期间可能遇到断网、电脑睡眠、进程被杀如果数据全存在内存里一次崩溃就前功尽弃。边采边存虽然每次打开文件有一点 IO 开销但换来的是“从哪断从哪续”。断点恢复的思路是启动时先读已有的 CSV把已经采集过的酒店名放进一个集合后续循环里判断名称是否已存在存在就跳过。这个方案实现成本低效果却很稳出差错时不用重来恢复成本几乎是零。3. 数据清洗中的隐藏功力脏数据、空值与价格字段采集完成之后数据结构一定不如你想象的干净。真正费力气的不是可视化而是让数据“可以上图”。这一步我用 pandas 完成总共只有几百行代码但每一步都有它的理由。3.1 价格字段从字符串到浮点数携程页面里价格经常写成“¥468起”“585/晚”这类格式直接塞进 DataFrame 做数学统计会报错。我的处理代码很简单import pandas as pd import re df pd.read_csv(ctrip_data.csv) def parse_price(value: str) - float: if pd.isna(value): return float(nan) text str(value) numbers re.findall(r\d\.?\d*, text) if not numbers: return float(nan) return float(numbers[0]) df[price] df[price_raw].apply(parse_price)这里的正则只抽第一个数字原因是价格字符串里通常第一个数字就是主要价格。如果页面里出现“¥468起”这种带“起”字的字段取 468 作为参考价格是合理的但要在后续分析时留意“起价”是下限不是均价。我在统计口径里单独加了一列备注标注价格类型防止自己忘掉。3.2 重复数据处理与去重逻辑列表页和详情页的数据合并之后同一家酒店可能重复出现因为同城多商圈会覆盖同一个酒店 ID。去重不能只看酒店名因为不同城市可能有重名连锁酒店。我选择用“城市 酒店名”作为唯一的判断键然后再把评分、点评数、价格做累计覆盖保留信息最全的那条记录df df.sort_values(rating_count, ascendingFalse) df df.drop_duplicates(subset[city, hotel_name], keepfirst)这里的排序逻辑很关键先按点评数量降序这样 keepfirst 保留的就是点评数最多、信息最全的记录而不是随机一条。直接 drop_duplicates 不加排序很可能留下一条没有商圈信息的空记录。3.3 星级与商圈文本的规范星级字段我统一替换成“五星/四星/三星/其他”四个分组方便后面做分组统计。商圈字段则做筛选只保留出现次数大于 5 的商圈把样本量太少的长尾商圈并进“其他”避免画图时横坐标挤了一堆几乎看不到的标签。这一步容易被人忽略但它直接影响可视化效果。假设采集的 2000 条酒店记录分布在 120 个商圈里有 90 个商圈只有一两条记录如果你不合并条形图会被 120 个标签塞满一眼看过去全是噪声。所以我清洗的底层原则是为图表可读性而清洗而不是为清洗而清洗。3.4 数据集检视与统计口径记录清洗收尾时我习惯打印一份“数据体检报告”print(总记录数, len(df)) print(价格缺失, df[price].isna().sum()) print(评分为空, df[rating].isna().sum()) print(df[city].value_counts()) print(df[price].describe())这批输出我会直接留在脚本首行方便每次重跑时一眼确认数据量级。没有这份体检报告很容易把清洗不彻底的数据直接拿去画图最后发现图形结论颠三倒四还得从头查。把统计口径写进代码注释里也是好习惯例如 “price 含起价字段”、“rating 为去重后的最新评分”六个月之后再回来看代码不至于看不懂当时的处理逻辑。4. 可视化分析让数据长在图上也要让图说话到这一步才真正进入“可视化分析”的核心。我的经验是画图不是目的而是为了验证问题和发现问题。每一张图都要对应一个具体判断或待验证的假设而不是把各字段全画一遍然后挑好看的。4.1 酒店价格分布直方图与分位数分析第一张图我先画价格直方图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df[price].plot.hist(bins40) plt.xlabel(酒店参考价元) plt.ylabel(酒店数量) plt.title(酒店价格分布直方图) plt.tight_layout() plt.savefig(price_distribution.png) plt.show()跑完之后会发现价格分布非常右偏极少数高端酒店把均值拉得很高。所以分析结论不能只报均值必须结合分位数。我一般输出 25%、50%、75% 分位值print(df[price].quantile([0.25, 0.5, 0.75]))实际项目里我经常看到中位数只有均值的 60% 左右说明高价样本对均值影响大真正代表大多数用户的价位段是中位数附近。写结论时我会明确说“价格中位数是 X 元”而不是“平均价格是 X 元”这两种说法背后的业务含义差别很大。分位数还能帮你找到“合理预算区间”比如 25% 分位到 75% 分位之间是大多数普通游客真正下单的价格范围。4.2 不同城市与商圈的价格对比第二张图做“各城市酒店价格平均值”和“中位数”的双柱对比。为什么两条柱一起画因为只看均值会忽略右偏分布。用 DataFrame 的 groupby 一次算好city_price ( df.groupby(city)[price] .agg([mean, median]) .sort_values(median, ascendingFalse) ) city_price.plot.bar(figsize(10, 6)) plt.title(不同城市酒店价格对比均值 vs 中位数) plt.ylabel(价格元) plt.tight_layout() plt.savefig(city_price_compare.png)从这张图能直接看出城市的旅游消费层级。有些城市均值高但中位数不高说明是少数高端酒店拉高氛围有些城市两个数都高说明整体消费水平偏高。这种图做出来比单纯报一个“平均价格”有说服力得多。商圈维度的分析我单独对上海、北京这类数据量大的城市做一页“商区价格排行”。做法是筛出一个 city再 groupby district 取中位数画出前 20 个商圈的水平柱状图。原因是横向条形图更适合展示排名类数据且商圈名称通常较长水平摆放不会互相遮挡。4.3 景点热度、评分与价格的关系景点热度分析我用条形图排序并把词云作为辅助展示。词云不宜作为唯一分析图因为它不能精确比较数量差异只能让观众形成整体印象。所以我的顺序是先画热度条形图给出精确排序再画词云增加视觉冲击力。条形图用热度值降序取前 15 个景点这样每个名字都能看清。评分与价格的关系我做了散点图以价格作为横轴、评分作为纵轴df_scatter df.dropna(subset[price, rating]) df_scatter.plot.scatter(xprice, yrating, alpha0.5) plt.title(酒店价格与评分散点图) plt.savefig(price_rating_scatter.png)alpha 参数很关键数据量一大散点会重叠成一团黑降低透明度才能看到密度分布。画完这组图之后我的整体判断是中低价位酒店评分分布很广既有高分也有低分高价酒店相对更容易拿到高评分但并非绝对。这个现象说明“价格不等于品质”但“价格下限”可以过滤掉一部分低质量供给。4.4 图表布局与输出规范我习惯把所有图表统一保存为 PNG分辨率 150 dpi标题、坐标轴标签、数据来源三件套齐全。有读者会问“为什么不用 Jupyter Notebook 看热力图”我的回答是临时看可以用 Notebook但项目的最终交付物必须是一份可以脱离代码运行的图表集合。保存图片之后我还会在同一目录下生成一份 markdown 报告每张图底下附两到三行解读文字这样领导或队友看报告时不需要再去翻代码。5. 调试与避坑我在这类项目里反复踩过的前五个坑最后这部分是我最想写的内容。数据可视化项目在教程里看起来很顺滑但实操时有一堆细节会突然卡住我按出现频率从高到低排个序。5.1 中文字体与负号显示问题matplotlib 默认字体不包含中文不加rcParams设置的话图里的中文字符会变成方块。这个坑几乎每个入门者都会踩。解决办法就是一进脚本就设置字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False如果你用的是 macOS写成[Arial Unicode MS]Linux 则要注意SimHei不一定存在可以用[Noto Sans CJK SC]。负号那一行经常被忽略但价格区间里出现负数概率极低评分差值为负时就会踩中显示成破折号而不是负号。5.2 数据不均衡导致的可视化误导如果 A 城市有 800 条酒店数据B 城市只有 50 条你把它们画在同一张对比图上标准差和均值置信区间完全不是一个量级结论很容易被极端样本带偏。我处理这类问题的方法是在对比图上标注样本量sample count 直接在柱子顶部列出来如果样本量差异超过 10 倍就先做抽样对齐或者只做数据量足够的城市之间的对比。还有一种常见情况是采集时段差异白天抓的数据和凌晨抓的数据酒店“当前可订”状态不同价格可能差异很大。所以我在截图报告里会注明数据抓取时间窗口例如“9 月第三周的某个工作日上午”这在做趋势分析时尤其重要。5.3 重复运行脚本导致数据重复爬虫脚本重跑一遍没有去重逻辑就会把同样数据追加进 CSV最后清洗时如果不小心统计结果就重复计算。建议在采集脚本入口处就做“启动前读一次现有文件构建已采集集合”的检查不要依赖清洗阶段才去重。清洗去重只能救急源头避免才能保证数据一致。5.4 缺失值和文本编码不一致不同来源的页面可能在价格字段里用全角数字、半角数字混排也可能出现NaN。清洗逻辑里一定要做pd.to_numeric(..., errorscoerce)把无法解析的内容统一转成 NaN再做填补或剔除。我通常选择直接剔除价格缺失的记录因为价格是这个分析项目的核心变量填充出来的价格会污染所有后续图表。5.5 合规意识数据量、公开数据与学习目的这个一定要说清楚。此类项目仅面向技术学习、个人研究和教学演示采集过程应遵循目标网站的使用规则控制请求频率不采集用户隐私、不碰会员信息不用于商业用途或二次售卖。合理的做法是使用公开数据、模拟少量页面并在项目文档里明确标注数据用途和限制。我的习惯是脚本里把每次请求延时和单批采集数量写清楚让代码本身“有分寸”分析完成后立刻删除中间缓存文件只保留脱敏后的统计结果和图表。从项目里延伸出来的个人体会这套项目流程我自己带不同基础的朋友跑过好几轮最大的感受是能最快提升数据分析能力的不是背 pandas 的 API而是亲手经历一遍“数据从网站到图表”的完整过程。你会遇到页面结构不统一、编码乱掉、价格字段七零八碎、图形标题溢出、中文字体变方块……每一个问题都在逼你去读文档、看源码、理解数据本身。如果你也想自己动手复现我建议从单城市抽样开始先把流程跑通再扩大到多城市对比先把最简单的直方图画出来再加分组和交互。等你把酒店价格的分布讲明白再把同样的流程换到景点热度上基本就具备做其他行业数据小项目的能力了。最后再提醒一句所有采集和分析都要尊重数据来源控制频率、限制用途、保留授权意识项目才能既学到东西又站得住脚。