爬取携程重庆旅游景点数据:Python实战教程
本文带你从零实现一个携程网旅游景点爬虫,抓取重庆市(城市可指定)热门景点的20+字段详细信息,并保存为 JSON 和 CSV 格式。你将学到:动态接口分析、数据解析、反爬应对、数据清洗等实用技巧。
一、写在前面
携程是国内领先的旅游平台,其景点数据对于旅行规划、数据分析非常有价值。本爬虫以重庆市为例,采集以下完整字段:
| 字段名 | 说明 |
|---|---|
| 标题 | 景点名称 |
| 等级 | A级景区等级(如5A、4A) |
| 碑榜 | 景点所获奖项或榜单 |
| 标签 | 特色标签(如“亲子乐园”“网红打卡”) |
| 热度 | 热度指数 |
| 评分 | 综合评分 |
| 评论数 | 总评论数 |
| 好评数 | 好评数量 |
| 消费后评价数 | 消费后评价数量 |
| 差评数 | 差评数量 |
| 地址 | 行政区划地址 |
| 详细地址 | 具体街道门牌 |
| 距离 | 距离参考点距离(如“距市中心2km”) |
| 门票 | 门票价格或免费说明 |
| 开放时间 | 营业时间段 |
| 官方电话 | 联系电话 |
| 封面图片链接 | 列表页封面图URL |
| 详情链接 | 景点详情页URL |
| 相关图片链接(轮播图的4个) | 详情页轮播图前4张URL |
| 更多内容 | 详情页详细描述文本 |
共计20个字段,覆盖了从基础信息到深度评价的各个方面。
代码已在本机测试通过,可根据需要修改城市、页数等参数。
二、技术栈
| 技术 | 用途 |
|---|---|
| Python 3.8+ | 开发语言 |
| requests | 发送 HTTP 请求 |
| BeautifulSoup4 | 解析 HTML 页面 |
| re | 正则表达式提取信息 |
| json / csv | 数据存储 |
| subprocess | 备用请求方式(curl) |
三、爬虫整体架构
整个爬虫分为以下几个模块:
- 列表页请求:调用携程的内部接口
getAttractionList,获取景点卡片数据(提供标题、等级、封面、门票等)。 - 详情页解析:访问每个景点的详情页,提取额外信息(如开放时间、轮播图、好评/差评数、更多内容等)。
- 数据清洗:统一处理空值、去除多余空格、合并重复字段。
- 数据存储:实时写入 CSV 文件,最后生成 JSON 全量备份。
流程图
开始 -> 解析城市ID -> 循环请求列表页 -> 解析卡片 ->
获取详情页链接 -> 请求详情页并解析 -> 清洗数据 ->
写入CSV -> 判断是否继续 -> 结束
四、核心代码解析
1. 配置参数(直接修改即可)
LIST_URL = "https://you.ctrip.com/sight/chongqing158.html" # 城市列表页URL
PAGE_SIZE = 10 # 每页数量
MAX_PAGES = 0 # 最大页数,0表示全部
MAX_ITEMS = 0 # 最大条数,0表示不限制
SORT_TYPE = 1 # 排序类型(1:默认,2:评分等)
DELAY_SECONDS = 0.2 # 请求间隔,避免反爬
TIMEOUT_SECONDS = 20 # 超时时间
JSON_OUT = "result.json"
CSV_OUT = "result.csv"
只需修改
LIST_URL即可切换到其他城市(如https://you.ctrip.com/sight/beijing5.html)。
2. 获取城市ID
列表页的URL中包含了城市ID,我们通过正则表达式提取:
DISTRICT_ID_RE = re.compile(r"/sight/[^/?]*?(\d+)\.html")
def parse_district_id(list_url: str) -> int:
match = DISTRICT_ID_RE.search(list_url)
if not match:
raise ValueError(f"无法从链接中提取 districtId: {list_url}")
return int(match.group(1))
3. 请求封装(支持requests和curl降级)
为了应对某些环境代理或SSL问题,我们封装了 HttpClient 类,优先使用 requests,失败后自动切换为 curl 命令(通过 subprocess):
class HttpClient:
def __init__(self, timeout: int = 20):
self.timeout = timeout
self.session = requests.Session()
def request_text(self, method, url, headers=None, json_data=None, params=None):
# 尝试两种 trust_env 模式
for trust_env in (True, False):
try:
self.session.trust_env = trust_env
response = self.session.request(...)
return response.text
except requests.RequestException as exc:
errors.append(str(exc))
# 若失败,使用 curl 命令
return self._request_with_curl(...)
4. 列表页接口调用
经过抓包分析,携程的列表页是通过 POST 请求加载的,接口地址为:
https://m.ctrip.com/restapi/soa2/18109/json/getAttractionList
请求体为一个JSON对象,包含 districtId、index(页码)、sortType 等参数。我们构建 payload 并发送:
payload = {
"head": {"syscode": "999"},
"scene": "online",
"districtId": district_id,
"index": page_index,
"sortType": sort_type,
"count": page_size,
"filter": {"filterItems": []},
"returnModuleType": "product",
}
page_data = client.request_json("POST", LIST_API_URL, headers=LIST_HEADERS, json_data=payload)
cards = [item.get("card") or {} for item in page_data.get("attractionList") or []]
5. 详情页解析
详情页是一个完整的HTML页面,我们使用 BeautifulSoup 解析。主要信息位于 div.baseInfoItem 和 div.hotTags 等容器中:
- 地址、开放时间、官方电话:通过查找包含特定关键词的
.baseInfoTitle提取。 - 好评/差评数:在
.hotTags中通过正则匹配。 - 轮播图:解析
.swiperMain .swiperItem的style属性中的background-imageURL。 - 更多内容:直接获取
div.detailModule的文本。
代码示例:
def extract_detail_info(detail_html: str) -> Dict[str, Any]:
soup = BeautifulSoup(detail_html, "html.parser")
# 提取地址
address = extract_base_info_by_title(soup, "地址")
# 提取电话
phone_text = ""
for item in soup.select("div.baseInfoItem"):
title_node = item.select_one(".baseInfoTitle")
if title_node and "官方电话" in normalize_text(title_node.get_text()):
phone_text = normalize_text(item.get_text())
break
phone_numbers = dedupe_preserve_order(re.findall(r"\+?\d[\d-]{5,}\d", phone_text))
# 提取好评差评
comment_count_map = {"好评数": 0, "消费后评价数": 0, "差评数": 0}
for tag in soup.select(".hotTags .hotTag"):
tag_text = normalize_text(tag.get_text())
match = re.search(r"(好评|消费后评价|差评)\s*\(?\s*(\d+)\s*\)?", tag_text)
if match:
label, value = match.group(1), int(match.group(2))
comment_count_map[f"{label}数"] = value
...
6. 数据清洗与合并
- 列表页已有部分字段(标题、等级、标签等),详情页的字段会覆盖或补充。
- 使用
dedupe_preserve_order去除重复标签/图片。 - 通过
normalize_text移除多余空白。 - 最终生成的数据包含上述20个字段,所有字段均有默认值(如 "无" 或 0),确保数据完整。
7. 实时写入CSV
为了避免内存占用过大,我们采用“边爬边写”的策略:每采集一条数据,就追加到CSV文件末尾,同时将数据存入内存列表最后统一保存JSON。
def append_csv_row(path: str, row: Dict[str, Any]) -> None:
with open(path, "a", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS) # 全部20个字段
writer.writerow(_format_csv_row(row))
五、反爬与异常处理
1. 请求头模拟
我们设置合理的 User-Agent 和 Referer,让请求看起来像正常浏览器访问。
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
2. 请求间隔
通过 time.sleep(DELAY_SECONDS) 控制请求频率,避免短时间内大量请求被封IP。
3. 备用请求机制
如果 requests 因代理或SSL问题失败,代码会自动调用系统 curl 命令重试,提高了稳定性。
4. 字段缺失处理
所有字段都设置了默认值(如 "无" 或 0),保证数据完整。
六、运行结果
执行 python ctrip_spider.py,控制台会实时打印采集进度:
[列表页] page=1, items=10
[已采集] 1 -> 重庆动物园
[已采集] 2 -> 武隆天生三桥
...
采集完成,共 120 条
JSON: result.json
CSV: result.csv
生成的两个文件:
- result.csv:包含全部20个字段,可直接用 Excel 打开。
- result.json:JSON 格式,便于程序读取。
由于字段众多,这里仅展示前两行数据(部分字段):
| 标题 | 等级 | 评分 | 评论数 | 好评数 | 消费后评价数 | 差评数 | 门票 | 开放时间 | ... |
|---|---|---|---|---|---|---|---|---|---|
| 重庆动物园 | 4A | 4.6 | 3560 | 1820 | 910 | 830 | 旺季25元 | 08:00-17:00 | ... |
| 武隆天生三桥 | 5A | 4.8 | 10234 | 5230 | 2980 | 2024 | 旺季125元 | 08:30-16:30 | ... |
所有字段均采集到位,满足深度分析需求。
七、效果图
八、常见问题与改进建议
1. 如何抓取其他城市?
只需修改 LIST_URL 中的城市拼音和ID,例如:
- 北京:
https://you.ctrip.com/sight/beijing5.html - 上海:
https://you.ctrip.com/sight/shanghai2.html
2. 如何应对IP封禁?
- 增大
DELAY_SECONDS(如 1 秒)。 - 使用代理IP池(可参考
requests的proxies参数)。
3. 数据不全怎么办?
- 检查详情页的HTML结构是否有变化,可能需要更新选择器。
- 部分字段可能隐藏在 JavaScript 动态渲染中,此时可使用 Selenium 或分析数据接口。
4. 如何增加更多字段?
本代码已提供扩展接口,如需增加“交通信息”、“小贴士”等,可在 extract_detail_info 中添加新的选择器。
九、总结
通过这个实战案例,我们学会了:
- 如何分析网页接口,找到真实数据源。
- 使用 requests + BeautifulSoup 进行静态页面解析。
- 处理多页数据,并发控制。
- 完整采集20个字段,覆盖景点基础、评价、图片、描述等全方位信息。
- 数据清洗与多格式输出。
- 应对反爬的基本策略。
完整代码已上传至 小红书获取(可附链接),欢迎 fork 和改进。
希望这篇教程对你有帮助!如果有任何问题或建议,欢迎留言讨论。
附录:代码文件结构
ctrip_spider/
├── spider.py # 主程序
└── result.csv # 输出(运行后)
❗❗❗注意:请尊重网站权益,合理使用爬虫,勿对目标服务器造成压力。