去哪儿网景点爬虫实战:Python数据采集到Excel导出全解析
发布时间:2026/9/14 5:23:56 锦皓数字建站

简介一套面向去哪网的旅游景点爬虫设计源码基于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.pyrequests 抓取与 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;q0.9, } def fetch_html(session, url, paramsNone, retry3): for i in range(retry): try: resp session.get(url, paramsparams, headersHEADERS, timeout10) 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这里有几个参数值得记住timeout10防止某个请求长时间挂起resp.apparent_encoding用字符探测替代固定编码应对中文乱码retry3表示最多重试三次重试间隔分别是 1 秒、2 秒、3 秒。如果三次都失败函数返回 None调用方可以根据 None 跳过这个城市而不是直接崩溃。2.3 解析景点卡片选择器与容错去哪网的景点卡片通常集中在div.sight_item里名称、评分、点评数分别在a.name、span.score strong、span.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(stripTrue) if name else , grade: grade.get_text(stripTrue) if grade else , comment_num: int(comment_num.get_text(stripTrue)) if comment_num and comment_num.get_text(stripTrue).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 格式。相比 CSVxlsx 可以直接用 Excel/WPS 打开表格样式、sheet 结构都能保存非技术人员查看起来没有编码障碍。用 pandas 写 xlsx 也很简单但要注意引擎问题写入单个文件用engineopenpyxl追加 sheet 时才不会覆盖已有内容。3.2 字段设计参考爬虫不仅要抓数据还要设计好存储字段。字段太少后续做分析时缺上下文字段太多页面一改就到处报错。以下是这个项目里常见字段的一个合理设计字段名含义示例值抓取来源name景点名称故宫列表页a.namegrade评分4.7列表页span.score strongcomment_num点评数81234列表页span.comment_count strongcity所属城市北京抓取时传入的 keywordurl景点地址或链接/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(parentsTrue, exist_okTrue) with pd.ExcelWriter(filepath, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_namescenic) return True调用时传入列表字典函数返回 True 或 False。返回 False 时调度层可以打印“城市抓取为空”而不是生成一个空文件。这里用Path(filepath).parent.mkdir创建目录即使filepath直接写beijing_info.xlsx.parent是空路径mkdir也不会报错因为exist_okTrue。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 从零跑起项目的完整步骤拿到源码后先不要急着改代码按以下步骤走确认本机 Python 版本在 3.8 以上执行python --version。安装依赖pip install requests beautifulsoup4 pandas openpyxl。用浏览器打开去哪网景点列表页复制页面里的 Cookie 到 HEADERS如果需要登录。运行python app.py观察每个城市打印的条数。检查生成的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, paramsparams, headersHEADERS, timeout10) if resp.status_code 200: return resp.text elif resp.status_code in (403, 429): # 被限流时退避更久这里做一次重试 time.sleep(uniform(3, 6)) resp session.get(url, paramsparams, headersHEADERS, timeout10) 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_workers3) as pool: for city, count in pool.map(run_city, CITY_KEYWORDS.items()): print(f{city}: {count} 条)max_workers3是保守值并发太高会放大被限流的概率。如果以后需要更高吞吐可以换用 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_indexTrue ) df_all.drop_duplicates(subset[city, name], keeplast, inplaceTrue)keeplast能保证重复数据中保留最后一行前提是文件按时间顺序生成。如果项目没有写入抓取时间建议在爬虫里增加一个crawl_date字段每次写入时带当天日期这样去重和后续分析都有依据。5.3 用数量分布定位异常城市合并后按城市统计景点数量能直观发现哪个城市抓取异常。比如北京、上海正常有上千条某个城市只有几十条那大概率是抓取中途出错。city_counts df_all.groupby(city).size().sort_values(ascendingFalse) print(city_counts) city_counts.to_csv(city_counts.csv, encodingutf-8-sig)sort_values(ascendingFalse)让数据量大的城市排在前面只打印前几行就能看出大致分布。保存 CSV 时用utf-8-sig编码Excel 打开不会乱码city_counts.csv可以直接作为后续分析的底表。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。