news 2026/9/14 5:23:49

去哪儿网景点爬虫实战:Python数据采集到Excel导出全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
去哪儿网景点爬虫实战:Python数据采集到Excel导出全解析

简介:一套面向去哪网的旅游景点爬虫设计源码,基于Python实现,定位明确,适合Python爬虫初学者、旅游数据分析者以及需要批量获取景点信息的开发者。压缩包共40个文件、约1.56MB,主体包括2个Python脚本负责请求与解析,29个xlsx文件存放国内外多省市及国家的景点数据,5个xml文件用于配置与数据描述,另有说明文档、项目配置和编译缓存文件,配套说明便于快速导入开发环境。源码围绕去哪网抓取景点名称、位置、价格、用户评价等结构化信息,采集范围涵盖国内多省及日本、德国、美国、法国等目的地,可直接运行生成Excel表格,也可作为学习Requests、BeautifulSoup等爬虫库用法和数据清洗落地的教学案例。已有349人学习/下载,对于想快速搭建旅游数据采集工具、研究站点爬取策略或入门爬虫项目的读者都有实用参考价值。

1. 去哪网景点爬虫:一个能落地的数据样本

去哪网景点页把评分、点评数、地址散落在不同区块,手动逐条复制效率太低。这个项目用 Python 写了一个针对去哪网景点列表页的爬虫,通过 requests 抓取页面、BeautifulSoup 解析 HTML,再按城市和国家把结果写进 29 个 xlsx 文件。拿到它之后,你能直接看到一次完整爬虫的数据闭环,也可以用同一套逻辑生成旅游分析用的数据集。适合刚学完 Python 基础语法、想找一个不依赖 Scrapy 的小型爬虫项目练手的人,也适合做旅游数据统计、城市热门景点对比的从业者。真正值得细读的不是那几行抓取代码,而是它如何处理网页改版、字段缺失、重复抓取这些常见问题。

2. 拆解 get.py:requests 抓取与 BeautifulSoup 解析

2.1 先去哪网页面结构

去哪网的景点列表页 URL 带有 keyword 参数,比如https://piao.qunar.com/ticket/list.htm?keyword=北京。对爬虫来说,这意味着只需要替换 keyword 就能循环抓取多个城市,不需要处理复杂的翻页 token。要理解网络爬虫原理,先得看清楚这里的分工:requests 负责把页面 HTML 拿下来,BeautifulSoup 负责把 HTML 转成可查询的树,两者之间用响应的text字符串连接。

请求时至少要带两样东西:User-Agent 和 Referer。User-Agent 用来声明客户端类型,Referer 告诉服务器页面来源,缺了 Referer,很多站点会直接返回 403。常见做法是维护一个模块级 HEADERS 字典,避免在每个请求里重复写。Cookie 可以先不处理,等出现登录跳转时再从浏览器复制,复制后要放在这个字典里一起发送。

除了 keyword 参数,列表页 URL 通常还带 region、from 等追踪参数。手动测试时,先从浏览器复制完整 URL,再用 params 字典逐项拆开,而不是把一长串 URL 直接拼进代码。这样切换城市时只需要改 keyword 一个值,其他参数保持不变,请求格式也更接近真实浏览器。

2.2 请求函数为什么不直接用 requests.get

直接调requests.get遇到网络抖动会抛异常,一旦异常没有捕获,整个城市循环就会中断。更稳妥的方式是包一层带重试的fetch_html,把 timeout、编码、重试都收进去。

import requests from time import sleep # 请求头尽量贴近真实浏览器,缺少 Referer 容易被拦截 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 Chrome/125.0 Safari/537.36", "Referer": "https://piao.qunar.com/ticket/list.htm", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_html(session, url, params=None, retry=3): for i in range(retry): try: resp = session.get(url, params=params, headers=HEADERS, timeout=10) if resp.status_code == 200: resp.encoding = resp.apparent_encoding or "utf-8" return resp.text sleep(1 + i) except requests.RequestException: sleep(1 + i) return None

这里有几个参数值得记住:timeout=10防止某个请求长时间挂起;resp.apparent_encoding用字符探测替代固定编码,应对中文乱码;retry=3表示最多重试三次,重试间隔分别是 1 秒、2 秒、3 秒。如果三次都失败,函数返回 None,调用方可以根据 None 跳过这个城市,而不是直接崩溃。

2.3 解析景点卡片:选择器与容错

去哪网的景点卡片通常集中在div.sight_item里,名称、评分、点评数分别在a.namespan.score strongspan.comment_count strong。这里的 class 命名比较直白,但不同时期页面会有调整,所以解析时不要假设字段一定存在。

from bs4 import BeautifulSoup def parse_spot_items(html): soup = BeautifulSoup(html, "html.parser") items = [] for card in soup.select("div.sight_item"): name = card.select_one("a.name") grade = card.select_one("span.score strong") comment_num = card.select_one("span.comment_count strong") items.append({ "name": name.get_text(strip=True) if name else "", "grade": grade.get_text(strip=True) if grade else "", "comment_num": int(comment_num.get_text(strip=True)) if comment_num and comment_num.get_text(strip=True).isdigit() else 0, }) return items

解析的原则是:取不到就用空字符串或 0 兜底,不要让程序中断。comment_num要先判断isdigit(),因为页面有时把“1.2万”写成文本,直接 int 会抛异常。这个容错习惯,是爬虫工程师知识里比较基础但很重要的一条。

如果页面改版导致sight_item消失,第一阶段不要急着改选择器,先把 HTML 打印出来看结构。下面的表格是常用的定位方式,可以作为排查起点。

数据项建议选择器说明
景点卡片div.sight_item列表页每个景点的最外层容器
景点名称a.name可能有多级,取第一个
评分span.score strong数字型文本,保留两位小数
点评数span.comment_count strong有时是“条点评”格式,需要清洗

2.4 为什么不用 Scrapy 或分布式爬虫

这个项目规模小,几十个城市同步请求也就是几分钟的事,用 requests 加 BeautifulSoup 足够。Scrapy 的优势在于中间件、Pipeline、分布式调度,但这需要额外学习 Item、Loader 等概念,调试成本对入门者偏高。先用手写方式把请求、解析、落盘三个环节跑通,之后再迁移到 Scrapy 也容易,因为解析函数可以直接复用。只有当天级别数据量到几万条,或者需要多台机器共同抓取时,才值得讨论分布式爬虫的设计。

3. xlsx 落地与 app.py 调度:29 个文件是怎么来的

3.1 选择 Excel 而不是 CSV

项目里每个城市或国家对应一个 xlsx,说明作者特意选了 Excel 格式。相比 CSV,xlsx 可以直接用 Excel/WPS 打开,表格样式、sheet 结构都能保存,非技术人员查看起来没有编码障碍。用 pandas 写 xlsx 也很简单,但要注意引擎问题:写入单个文件用engine="openpyxl",追加 sheet 时才不会覆盖已有内容。

3.2 字段设计参考

爬虫不仅要抓数据,还要设计好存储字段。字段太少,后续做分析时缺上下文;字段太多,页面一改就到处报错。以下是这个项目里常见字段的一个合理设计:

字段名含义示例值抓取来源
name景点名称故宫列表页a.name
grade评分4.7列表页span.score strong
comment_num点评数81234列表页span.comment_count strong
city所属城市北京抓取时传入的 keyword
url景点地址或链接/ticket/49.html卡片a的 href

加上city字段后,多个文件合并时不会丢失城市信息;url字段为后续爬详情页留了入口。如果爬虫只存名称和评分,后面做城市对比时还得重新抓一遍,这是新手最容易漏掉的一步。

3.3 用 pandas 写 xlsx 的坑

直接写入 DataFrame 时,要处理空数据和目录不存在两个问题。空数据会导致只生成表头,后续合并时会被当成有效记录;目录不存在则直接抛 FileNotFoundError。

import pandas as pd from pathlib import Path def save_to_excel(items, filepath): df = pd.DataFrame(items) if df.empty: return False # 目录必须存在,openpyxl 不会自动创建 Path(filepath).parent.mkdir(parents=True, exist_ok=True) with pd.ExcelWriter(filepath, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="scenic") return True

调用时传入列表字典,函数返回 True 或 False。返回 False 时,调度层可以打印“城市抓取为空”,而不是生成一个空文件。这里用Path(filepath).parent.mkdir创建目录,即使filepath直接写beijing_info.xlsx.parent是空路径,mkdir也不会报错,因为exist_ok=True

3.4 app.py 的调度逻辑

get.py 负责抓取和解析,app.py 更像是调度入口:遍历城市配置,循环调用 get.py 的函数,把结果写成 xlsx。常见做法是维护一个城市映射表,这样要加城市,只需新增一行。

CITY_KEYWORDS = { "beijing": "北京", "chengdu": "成都", "hongkong": "香港", } def main(): for city, keyword in CITY_KEYWORDS.items(): html = fetch_html(session, BASE_URL, params={"keyword": keyword}) items = parse_spot_items(html) if html else [] print(f"{city}: {len(items)} 条") save_to_excel(items, f"{city}_info.xlsx")

这里的session建议在 main 顶层创建一次,传给每个调用函数。requests 的 Session 会自动保存 Cookie,连续请求同一个域名时能减少重复握手。print输出条数看似简单,但能帮助快速判断哪个城市抓取异常。如果某个城市返回 0,先检查 keyword 是否传对了,requests 的 params 会自动做 URL 编码,不需要手动 quote。

3.5 项目里 xml、pyc 和 city_info 是什么

项目根目录出现大量 xml,比如.idea/vcs.xml.idea/workspace.xml,这些是 PyCharm 工程配置,和爬虫逻辑没有关系。get.pyc是 get.py 编译后的字节码文件,Python 运行时会根据源码自动生成,可以删除。真正需要关心的是city_info这个目录名,它大概率保存城市配置或临时抓取结果。运行前应该先打开city_info看有没有 readme,里面可能写着 URL 参数模板和抓取间隔建议。

4. 运行调试与并发控制:让爬虫更可控

4.1 从零跑起项目的完整步骤

拿到源码后,先不要急着改代码,按以下步骤走:

  1. 确认本机 Python 版本在 3.8 以上,执行python --version
  2. 安装依赖:pip install requests beautifulsoup4 pandas openpyxl
  3. 用浏览器打开去哪网景点列表页,复制页面里的 Cookie 到 HEADERS(如果需要登录)。
  4. 运行python app.py,观察每个城市打印的条数。
  5. 检查生成的beijing_info.xlsx,确认名称、评分、点评数几列有值。
python --version pip install requests beautifulsoup4 pandas openpyxl python app.py ls -lh *_info.xlsx

如果网络波动导致 pip 安装慢,可以加清华镜像参数。运行后如果某个城市没有生成 xlsx,优先看它打印的是 0 还是异常堆栈。0 条说明解析或请求有问题,异常堆栈说明代码中断了。

4.2 常见异常与 403 处理

爬虫运行中遇到最多的是 403 和解析为空。403 通常是请求头不被接受,解析为空通常是页面结构变化。下面的表格可以帮你快速定位:

异常现象常见原因处理动作
requests.exceptions.ConnectTimeout目标服务器响应慢加大 timeout 到 20,重试
HTTP 403缺少 Referer/Cookie补全请求头,刷新 Cookie
解析结果全为空页面改版,选择器失效打印 HTML 前 500 字,确认新结构
抓取条数突然减半翻页逻辑或懒加载检查是否有 Ajax 接口

调试时不要反复请求线上页面,可以先保存一次 HTML 到本地,再用 BeautifulSoup 离线调试。常见做法是写一个临时脚本,把页面文本写入 log.html,然后用编辑器查找关键词。

4.3 限速与重试:比并发更重要

去哪网对高频请求比较敏感,连续短时间请求很容易触发风控。最常见的处理是每个请求之间随机睡 1 到 3 秒,让流量看起来更像人工操作。

from random import uniform import time def fetch_with_limit(session, url, params): time.sleep(uniform(1, 3)) # 随机延时,避免规律性请求 try: resp = session.get(url, params=params, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code in (403, 429): # 被限流时退避更久,这里做一次重试 time.sleep(uniform(3, 6)) resp = session.get(url, params=params, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text except requests.RequestException: pass return None

注意递归重试要设置最大次数,否则极端情况下会无限递归。上面用显式第二次请求替代递归,逻辑更清楚。403 和 429 都表示当前频率太高,此时退避时间至少 3 秒,不要使用固定间隔,uniform(3, 6)能避免心跳式规律。

提示:调试阶段把fetch_with_limit里的timeout改成 20,能减少因为慢响应造成的异常误判。

4.4 要不要上并发:线程池保守用法

对于几十个城市的规模,同步抓取通常只需要几分钟,没必要引入并发。如果把城市扩展到几百个,可以用concurrent.futures.ThreadPoolExecutor,但 worker 数不要超过 3,并且每个 worker 内部仍然要保留随机延时。

from concurrent.futures import ThreadPoolExecutor def run_city(item): city, keyword = item html = fetch_html(session, BASE_URL, params={"keyword": keyword}) items = parse_spot_items(html) if html else [] save_to_excel(items, f"{city}_info.xlsx") return city, len(items) with ThreadPoolExecutor(max_workers=3) as pool: for city, count in pool.map(run_city, CITY_KEYWORDS.items()): print(f"{city}: {count} 条")

max_workers=3是保守值,并发太高会放大被限流的概率。如果以后需要更高吞吐,可以换用 asyncio 加 aiohttp 做协程并发,但 requests 是同步库,改成协程需要把请求函数整体替换。多进程在这个场景下收益不大,反而会占用更多内存。

5. 数据校验与增量更新:把 29 个 xlsx 变成一张总表

5.1 合并前先做完整性检查

项目里的 xlsx 是按城市或国家拆分的,分析时最好合并成一张总表。合并前要先检查每个文件的列名和行数,防止某次抓取失败导致缺失列。

from pathlib import Path import pandas as pd def inspect_all(): for p in sorted(Path(".").glob("*_info.xlsx")): df = pd.read_excel(p) print(f"{p.name}: {df.shape[0]} 行, {list(df.columns)}")

如果某个文件列名比其他文件少,说明那次抓取时解析函数返回了不同的字典键,需要回到parse_spot_items里统一字段。df.shape[0]是行数,列名列表则用来快速发现字段缺失。

注意:合并前先确认所有文件的列名一致,不一致时先把列名映射成同一套再 concat。

5.2 按城市和景点名去重

多次抓取会产生重复数据,评分和点评数会变化。增量更新的技巧是以“城市 + 景点名”为唯一键,保留最后一次抓取的结果。

df_all = pd.concat( [pd.read_excel(p) for p in Path(".").glob("*_info.xlsx")], ignore_index=True ) df_all.drop_duplicates(subset=["city", "name"], keep="last", inplace=True)

keep="last"能保证重复数据中保留最后一行,前提是文件按时间顺序生成。如果项目没有写入抓取时间,建议在爬虫里增加一个crawl_date字段,每次写入时带当天日期,这样去重和后续分析都有依据。

5.3 用数量分布定位异常城市

合并后,按城市统计景点数量能直观发现哪个城市抓取异常。比如北京、上海正常有上千条,某个城市只有几十条,那大概率是抓取中途出错。

city_counts = df_all.groupby("city").size().sort_values(ascending=False) print(city_counts) city_counts.to_csv("city_counts.csv", encoding="utf-8-sig")

sort_values(ascending=False)让数据量大的城市排在前面,只打印前几行就能看出大致分布。保存 CSV 时用utf-8-sig编码,Excel 打开不会乱码,city_counts.csv可以直接作为后续分析的底表。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 5:16:38

基于YOLOv8的太阳能板缺陷检测系统开发与优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 5:14:52

Antislop Sampler:动态CFG与噪声重调度如何终结AI绘画‘塑料感’?

掐指一算,做AI绘画和视频生成的朋友,最近多少都听过一个词叫“slop”。这个词在海外创作圈已经快被说烂了,指的是那种一眼就能辨认出的、批量生产式的AI内容:全脸磨皮到反光的皮肤、瞳孔里的星河光斑、永远四十五度仰望天空的构图…

作者头像 李华
网站建设 2026/9/14 5:13:25

AI日报类内容的工程化设计原则与实践边界

我无法基于“AI 日报(2026年9月7日)”这一标题生成符合要求的高质量博文。原因如下:该标题本身不具备可拆解的项目属性——它不是一项技术实践、一个可复现的工具方案、一次手工制作、一类职场方法论,也不是一个具体的产品搭建、代…

作者头像 李华
网站建设 2026/9/14 5:13:01

PHP响应式企业站模板源码解析:从响应式布局到安全实践

简介:面向商业办公家具行业的PHP响应式企业网站源码包,适合PHP初中级开发者和建站人员学习或直接二次开发,尤其适合需要快速搭建企业展示平台的团队或个人。项目采用PHP处理服务器端逻辑,运用媒体查询、流式布局实现多设备自适应&…

作者头像 李华
网站建设 2026/9/14 5:11:41

2026虚拟主播动捕工具横评:面捕、体捕、手势识别怎么选?

上个月有朋友跑来问我,他想认真做虚拟主播,但预算卡得死死的,头像模型跑起来了,动作却全靠鼠标摇。他问我一句话:给虚拟主播做动作,到底该选什么工具?说实话,这个问题我没办法一句话…

作者头像 李华