旅游数据爬取与分析系统 —— 从携程、去哪儿等平台采集景点信息、详情与用户评论,为旅行决策提供数据支撑。
免责声明
请以学习与研究为目的使用本项目。禁止将本项目用于商业用途、大规模或高频采集、绕过平台访问控制,以及任何违反法律法规或平台规则的行为。使用者应自行确认采集行为的合规性,并承担因使用本项目产生的一切责任。项目作者不对任何直接或间接损失、数据使用后果或法律纠纷负责。
已实现
- 全链路采集:携程与去哪儿景点列表、详情、评论一站式爬取
- ID 自动解析:根据景点名或城市名自动定位
poi_id/district_id,无需手动查询 - 流水线编排:
list → detail → comments一键串联,单个入口支持sight/city/poi_id/district_id四种目标 - 断点续跑:中断后重新执行自动跳过已完成景点;评论按页 checkpoint,失败页面单独记录
- 请求防护:随机延迟(默认 1-3 秒)、HTTP 非 200 状态码线性退避重试 3 次
- 集中配置:API 端点、请求头、超时、分页参数统一管理于
config.py - 离线测试:核心单测 mock 网络请求,不依赖真实 API 可用性
- 统一基础设施:
BaseSpider重试、SpiderLogger分级日志、storage_layout统一目录布局与StorageBackendJSON/CSV 存储
待实现
- 分析层:情感分析、词云、地理热图(占位模块与依赖已就位)
- 数据模型接入:
Sight/Comment数据模型替换裸 dict(模型已定义,尚未替换各模块返回值) - 代理轮换:
RequestOptimizer.get_random_proxy()已实现,但尚未接入各爬虫的实际请求调用 - 美团平台:平台目录已预留,尚未接入
- Python >= 3.9
- 核心依赖仅
requests与beautifulsoup4;测试依赖pytest,分析层依赖见下方 extras
git clone https://github.com/Hsuant/LifeLens.git
cd LifeLens
pip install -e ".[dev]" # 基础功能 + 测试依赖可选 extras:
| extra | 用途 |
|---|---|
analysis |
pandas / numpy / scikit-learn / matplotlib / seaborn / jieba |
geo |
geopandas(地理热图) |
deep |
torch / transformers(深度模型) |
两个平台使用统一的参数名:名称参数为 --sight、--city,ID 参数为
--poi-id、--district-id。
# 按景点名称
python -m lifelens.platforms.ctrip.pipeline --sight "黄鹤楼"
# 按城市名称
python -m lifelens.platforms.ctrip.pipeline --city "杭州" --list-pages 3
# 按景点 POI ID
python -m lifelens.platforms.ctrip.pipeline --poi-id 10568753
# 按地区 ID
python -m lifelens.platforms.ctrip.pipeline --district-id 9 --list-pages 3
# 去哪儿同样使用 --sight、--city、--poi-id
python -m lifelens.platforms.qunar.pipeline --city "北京" --max-sights 10参数说明(--sight / --city / --poi-id / --district-id 四选一):
| 参数 | 说明 | 默认值 |
|---|---|---|
--sight |
景点名称,自动解析 POI ID | "" |
--city |
城市名称,自动解析地区 ID | "" |
--poi-id |
景点 POI ID | None |
--district-id |
携程地区 ID | — |
--list-pages |
景点列表爬取页数 | None(不限制,自动爬取所有页) |
--max-comment-pages |
每个景点评论最大爬取页数 | None(不限制,爬取所有页) |
--output-dir |
输出根目录 | data |
注:Qunar 评论接口暂不支持地区 ID 查询,因此
--district-id仅适用于 Ctrip;Qunar 请使用--city。
from lifelens.platforms.ctrip.pipeline import CtripPipeline
pipeline = CtripPipeline(output_dir="data")
# 统一主入口:名称用 sight / city,ID 用 poi_id / district_id
result = pipeline.run(poi_id="10568753")
result = pipeline.run(district_id=9, list_pages=5, max_comment_pages=50)
result = pipeline.run(city="杭州", list_pages=5, max_comment_pages=50)
result = pipeline.run(sight="黄鹤楼", max_comment_pages=50)
print(result)返回值结构:
// 地区模式
{"total": 20, "completed": 18, "failed": 2, "skipped": 0, "elapsed_seconds": 152.3}
// 名称解析模式:在上述字段外额外附带
{"city": "杭州", "resolved_type": "district", "resolved_info": {...}}
// 景点模式
{"total": 1, "completed": 1, "failed": 0, "skipped": 0,
"poi_id": "10568753", "poi_name": "千岛湖",
"details_success": true, "comments_success": true, "elapsed_seconds": 8.4}
// 失败
{"success": false, "error": "未获取到景点列表"}入口方法对比:
| 方法 | 适用场景 | 关键参数 |
|---|---|---|
run() |
统一入口,按景点、城市或 ID 选择任务 | sight / city / poi_id / district_id 四选一 |
_run_district() |
内部地区流水线 | district_id, city, list_pages, max_comment_pages |
_run_sight() |
内部单景点流水线 | poi_id, sight, max_comment_pages |
from lifelens.platforms.ctrip.sight_list import CtripAttractionScraper
from lifelens.platforms.ctrip.sight_detail import AttractionDetailFetcher
from lifelens.platforms.ctrip.sight_comments import CtripCommentSpider
from lifelens.platforms.ctrip.sight_locator import SightLocator
from lifelens.core.storage import json_storage
# 自动解析 ID(无需手动查询)
locator = SightLocator()
# 按景点名获取 POI ID 及所属地区
info = locator.locate_sight("黄鹤楼")
# {"type": "sight", "poi_id": "77593", "sight_name": "黄鹤楼",
# "district_id": 145, "district_name": "武汉", "city_id": ..., "city_name": "..."}
# 按地区名获取地区 ID
info = locator.locate_district("杭州")
# {"type": "district", "district_id": 14, "district_name": "杭州", ...}
# 自动判断关键词类型(流水线内部使用的入口)
info = locator.locate("西湖")
# 获取景点列表
scraper = CtripAttractionScraper()
sights = scraper.get_attractions_with_pagination(district_id=9, pages=2)
json_storage.save(sights, "data/sights.json")
# 获取景点详情(get_detail 返回原始解析结果,get_formatted_detail 返回精简视图)
fetcher = AttractionDetailFetcher()
detail = fetcher.get_detail("10568753")
print(detail["poi_name"], detail["ticket_price"], detail["description"])
# 爬取评论(单景点 / 多景点)
spider = CtripCommentSpider(output_dir="data/comments")
spider.crawl_comments("10568753", "千岛湖", max_pages=10)
spider.crawl_multiple_pois([("10568753", "千岛湖"), ("77593", "黄鹤楼")], max_pages=10)所有携程 API 参数集中定义在 src/lifelens/config.py 的 CtripConfig dataclass 中,全局目录参数见 GlobalConfig:
| 配置项 | 说明 | 默认值 |
|---|---|---|
sight_search_url |
关键词搜索 API | .../soa2/26872/search |
sight_list_url |
景点列表 API | .../soa2/13342/json/getSightRecreationList |
sight_detail_url |
景点详情 API | .../soa2/18254/json/getPoiMoreDetail |
comment_url |
评论 API | .../soa2/13444/json/getCommentCollapseList |
headers |
请求头(UA / Content-Type / Referer 等) | 见 config.py |
head |
搜索 / 列表 / 详情 / 评论共用 head 结构 | cid=09031065211914680477 |
comment_cid |
评论 API 专用 cid | 09031069112760102754 |
timeout |
请求超时(秒) | 10 |
delay_range |
随机延迟范围(秒) | (1, 3) |
list_page_size |
景点列表每页条数 | 20 |
comment_page_size |
评论每页条数 | 10 |
| 全局配置 | 说明 | 默认值 |
|---|---|---|
GlobalConfig.log_dir |
日志目录 | logs |
GlobalConfig.data_dir |
数据目录 | data |
GlobalConfig.default_timeout |
默认超时 | 10 |
GlobalConfig.default_delay_range |
默认延迟范围 | (1, 3) |
注意:
.env.example目前是配置模板,代码尚未读取环境变量(未接入 python-dotenv)。需要调整cid、超时、延迟范围时,请直接编辑config.py。
统一目录结构为:
data/<platform>/<province>/<city>/<sight>_<poi-id>/
省、市优先取平台字段;缺失时从详情地址解析。无法确定的行政区使用
Unknown,不会猜测。
地区任务(_run_district()):
data/
└── ctrip/
└── 浙江省/
└── 杭州市/
├── sights.json
├── summary.json
├── pipeline_progress.json
└── 千岛湖_10568753/
├── detail.json
├── comments.csv
├── comments_progress.json
├── failed_pages.json
└── summary.json
景点任务(_run_sight()):
data/
└── qunar/
└── 北京市/
└── 北京市/
└── 故宫博物院_989946426/
├── detail.json
├── comments.csv
├── comments_progress.json
├── failed_pages.json
└── summary.json
评论 CSV 采用 utf-8-sig 编码(Excel 可直接打开),字段依次为:
序号、景区ID、景区名称、评论ID、用户昵称、总体评分、评论内容、发布时间、有用数、回复数、出行类型、用户所在地、游玩时长、图片数量、图片链接列表、景色评分、趣味评分、性价比评分、推荐项目
pipeline_progress.json:仅当某景点详情与评论都成功时才写入。只成功一项的景点不会被标记完成,下次运行会重新尝试该景点。comments_progress.json:位于景点目录,按{poi_id: {last_page, total_pages, status}}记录评论断点。failed_pages.json:位于景点目录,记录{poi_id, page, error, timestamp},便于定位与补采。- 需要强制重新采集时,删除对应进度文件即可。
日志按平台、组件和日期组织:
logs/
├── ctrip/
│ ├── pipeline_2026-10-24.log
│ └── comment_spider_2026-10-24.log
└── qunar/
├── pipeline_2026-10-24.log
└── comment_spider_2026-10-24.log
# 当前开发环境
D:\Environments\Anaconda\envs\AI\python.exe -m pytest -q
# 或任意可用的 Python >= 3.9
python -m pytest -qpyproject.toml 已配置 pythonpath = ["src"],无需手动设置 PYTHONPATH。当前离线测试按 core/、platforms/ 与 integration/ 组织,manual/ 仅存放需要手动执行的联网或调试脚本。
| 测试文件 | 覆盖内容 |
|---|---|
core/test_base_spider.py |
BaseSpider 共享重试封装、CtripBaseSpider JSON POST、继承契约 |
core/test_logger.py |
分级日志格式、URL 脱敏、日志文件分组、请求/延迟统计 |
core/test_storage_layout.py |
行政区解析、安全路径、平台/省市/景点目录布局 |
integration/test_pipeline_cli.py |
run() 四种目标分发、城市/景点解析与统一 CLI 参数 |
integration/test_keyboard_interrupt.py |
评论爬虫与流水线 Ctrl+C 中断保留断点/已完成进度 |
platforms/ctrip/test_sight_locator.py |
关键词 → 景点 / 地区 ID 解析、类型自动判断、解析边界条件 |
platforms/ctrip/test_sight_list.py |
POI 解析、请求体构建、分页取数 |
platforms/ctrip/test_comment_storage.py |
CSV 表头、字段顺序、跨页追加、断点进度 |
platforms/ctrip/test_comment_pagination.py |
总页数向上取整计算 |
platforms/qunar/test_sight_workflow.py |
去哪儿详情、列表、定位、评论与流水线工作流 |
platforms/qunar/test_comments.py |
去哪儿 window.context 评论解析与兜底逻辑 |
platforms/qunar/test_online.py |
去哪儿联网冒烟测试(离线自动跳过) |
| 手动脚本 | 用途 |
|---|---|
manual/ctrip_online_smoke.py |
携程联网冒烟测试 |
manual/ctrip_comments_api_debug.py |
携程评论 API 响应结构探测 |
manual/ctrip_sight_detail_debug.py |
携程景点详情 API 响应结构探测 |
manual/logger_smoke.py |
手动检查日志各级别输出 |
除 platforms/qunar/test_online.py 外,其余自动收集的测试均为离线测试(mock 网络),不依赖真实 API。manual/ 下的文件名不匹配 test_*.py,pytest 不会自动收集,需要时手动运行。
LifeLens/
├── pyproject.toml # 依赖管理 + 项目元数据 + pytest 配置
├── .env.example # 配置模板(代码暂未读取)
├── src/lifelens/
│ ├── __main__.py # 顶层 CLI 入口(子命令占位)
│ ├── config.py # 集中配置 (URL / headers / 超时 / 分页)
│ ├── core/ # 基础设施层 (平台无关)
│ │ ├── base_spider.py # BaseSpider 抽象基类 + 重试封装
│ │ ├── logger.py # SpiderLogger 分级日志 + 任务汇总
│ │ ├── request_optimizer.py # 随机延迟 + 代理 + 退避参数
│ │ ├── storage_layout.py # 跨平台数据目录布局 + 行政区解析
│ │ ├── storage.py # StorageBackend (JSON / CSV)
│ │ └── pipeline.py # BasePipeline + PipelineStats 结果统计
│ ├── platforms/
│ │ ├── ctrip/ # 携程平台
│ │ │ ├── sight_locator.py # 关键词 → POI ID / 地区 ID (自动解析)
│ │ │ ├── sight_list.py # 地区 → 景点列表 (分页)
│ │ │ ├── sight_detail.py # 景点详情 (电话/门票/描述/交通)
│ │ │ ├── sight_comments.py # 评论爬取 (断点续爬 + CSV)
│ │ │ ├── base.py # CtripBaseSpider 公共请求/重试基类
│ │ │ └── pipeline.py # CtripPipeline 全流程编排 (含 CLI)
│ │ ├── qunar/ # 去哪儿平台
│ │ │ ├── sight_locator.py # 景点 / 城市 → ID 解析
│ │ │ ├── sight_list.py # 城市榜单 → 景点列表
│ │ │ ├── sight_detail.py # 详情页 + 内嵌简介页解析
│ │ │ ├── sight_comments.py # 评论爬取 (window.context + CSV)
│ │ │ ├── base.py # QunarBaseSpider 公共 GET / 解析基类
│ │ │ └── pipeline.py # QunarPipeline 全流程编排 (含 CLI)
│ │ └── meituan/ # 美团平台 (待实现)
│ ├── models/
│ │ └── sight.py # Sight / SightDetail / Comment 数据模型
│ └── analysis/ # 分析层 (待实现)
│ ├── sentiment.py # 情感分析
│ ├── wordcloud.py # 词云生成
│ └── heatmap.py # 地理热图
├── tests/ # 测试套件
│ ├── core/ # 核心基础设施测试
│ ├── platforms/ # 平台测试(ctrip / qunar)
│ ├── integration/ # 跨模块与流水线测试
│ └── manual/ # 手动联网与调试脚本
├── data/ # 输出目录 (gitignored)
└── logs/ # 日志目录 (gitignored)
| 层级 | 技术 |
|---|---|
| 网络请求 | requests(会话 + 随机延迟 + 3 次退避重试) |
| HTML 解析 | beautifulsoup4 |
| 数据模型 | dataclasses |
| 存储 | 标准库 json / csv(utf-8-sig) |
| 测试框架 | pytest |
| 分析层 (计划) | pandas · scikit-learn · jieba · matplotlib · seaborn · geopandas |
- 携程景点列表爬取(分页 + 重试)
- 携程景点详情获取(结构化解析)
- 携程评论爬取(断点续爬 + CSV 持久化)
- 去哪儿景点列表 / 详情 / 定位 / 评论爬取
- 流水线编排(list → detail → comments)
- 集中配置(
config.py统一管理) - ID 自动解析(
SightLocator:关键词 →poi_id/district_id) - 流水线 CLI(携程 / 去哪儿模块入口)
- 统一平台参数(
sight/city/poi_id/district_id) - 分级日志与任务汇总(
SpiderLogger/PipelineStats) - 统一数据目录布局(
data/<platform>/<province>/<city>/<sight>_<poi-id>) -
BaseSpider继承复用(消除各模块重复的重试逻辑) -
StorageBackend接入(统一 JSON/CSV 存储) - 数据模型接入(
Sight/Comment替换裸 dict) - 代理轮换接入实际请求(
get_random_proxy()已实现) - 顶层 CLI 子命令接入(
ctrip-list/ctrip-detail/ctrip-comments) - 分析层:情感分析、词云生成、地理热图
- 美团平台爬虫
使用本项目即表示你已阅读并同意以下条款。如不同意,请停止使用并删除本项目及相关数据。
本项目仅用于个人学习、技术研究与数据分析练习,不提供任何形式的数据采集服务,也不构成对任何平台数据的授权。
- 使用前请自行确认目标平台的服务条款、robots 协议、用户协议以及所在地法律法规;
- 不得采集、存储或传播涉及国家安全、商业秘密、个人隐私或其他受法律保护的信息;
- 不得绕过登录验证、验证码、访问控制、反爬机制或其他技术保护措施;
- 不得将本项目用于大规模、高频次或影响目标平台正常运行的采集活动;
- 如需采集个人信息,应取得合法授权,并遵循最小必要原则。
- 禁止将本项目及其衍生代码用于任何商业用途,包括但不限于销售、付费服务、数据转售或商业数据产品;
- 禁止将采集结果用于骚扰、画像、精准营销、欺诈或其他侵害他人权益的行为;
- 禁止删除、隐藏或篡改本项目中的版权声明与许可信息。
- 采集数据归原始权利人或相关平台所有,本项目不授予你对第三方数据的任何权利;
- 评论、用户昵称、IP 归属地等字段可能涉及个人信息,请谨慎存储、脱敏处理,并在使用后及时删除;
- 请勿将采集数据上传至公开仓库、云盘或其他可被未授权访问的位置。
- 默认随机延迟为 1-3 秒,请根据实际情况适当调大,避免对目标平台造成压力;
- 出现限流、封禁或异常响应时应立即停止采集;
- 本项目不提供绕过限流、验证码或封禁的能力。
- 请勿将 Cookie、Token、账号密码等敏感凭证提交至版本库;
- 建议将凭证保存在本地未跟踪文件中,并定期轮换;
- 因凭证泄露造成的损失由使用者自行承担。
- 本项目按“现状”提供,不保证功能完整性、稳定性或适用性;
- 因使用或无法使用本项目导致的任何直接或间接损失、数据丢失、账号封禁或法律纠纷,作者不承担责任;
- 使用者应自行评估并承担全部使用风险。
作者保留随时修改本使用须知的权利,修改后的条款自发布之日起生效。
本项目采用 CC BY-NC 4.0(知识共享 署名-非商业性使用 4.0 国际) 许可协议,完整条款见 LICENSE。
在遵守以下条件的前提下,你可以复制、修改、分发和以非商业目的使用本项目:
- 署名:必须注明原作者及本项目链接,并说明是否作出修改;
- 非商业性使用:不得将本项目用于商业目的,包括销售、付费服务、数据转售或其他直接或间接获利行为;
- 无附加限制:不得通过法律条款或技术手段限制他人行使许可证允许的权利。
如需学术授权、 商业授权或其他用途,请联系 hsuantx@163.com。
注意:本项目许可证仅覆盖本项目代码与文档,不覆盖通过本项目采集的第三方数据。第三方数据的版权、使用权与合规责任由原始权利人或使用者自行承担。