前言

随着旅游业的复苏,景点数据分析需求日益增长。本文将详细介绍如何使用Python爬取携程网的景点数据,并以武汉市为例,爬取完整的景点信息,包括景点名称、等级、评分、评论数、热度、标签等关键字段。

📌 技术栈:Python 3.x + Requests + Fake-UserAgent + JSON + CSV


一、项目背景与目标

1.1 数据来源分析

携程接口 https://m.ctrip.com/restapi/soa2/18109/json/getAttractionList 提供了景点列表的JSON数据,通过POST请求即可获取结构化的景点信息,比解析HTML页面更加稳定高效。

1.2 爬取字段说明

字段名称 说明
景点名称 景点中文名称
所属区域 景点所在行政区
所属城市 景点所在城市
景点等级 如5A、4A等
用户评分 用户综合评分
评论人数 累计评论数量
热度评分 携程热度分值
景点标签 景点特色标签
距离描述 与参考点的距离
图片链接 封面图片URL
详情页链接 景点详情页地址

二、代码结构设计

采用面向对象的编程思想,封装 CtripAttractionSpider 类,主要包含以下方法:

__init__()          # 初始化配置、Cookie、CSV文件
_get_headers()      # 生成随机User-Agent请求头
_build_request_data()  # 构建POST请求体
_fetch_page()       # 请求单页数据
_save_to_csv()      # 保存数据到CSV
crawl()             # 主爬取逻辑

三、完整代码实现

3.1 导入依赖库

import requests
import json
import time
import random
import csv
import os
from fake_useragent import UserAgent

3.2 爬虫类完整代码

class CtripAttractionSpider:
    """携程景点信息爬虫类"""

    def __init__(self, cookies, district_id=145, output_file='景点数据.csv'):
        self.cookies = cookies
        self.district_id = district_id  # 武汉市district_id为145
        self.output_file = output_file
        self.base_url = 'https://m.ctrip.com/restapi/soa2/18109/json/getAttractionList'

        # 基础请求头
        self.base_headers = {
            'accept': '*/*',
            'accept-language': 'zh-CN,zh;q=0.9',
            'content-type': 'application/json',
            'origin': 'https://you.ctrip.com',
            'referer': 'https://you.ctrip.com/',
            'x-ctx-currency': 'CNY',
            'x-ctx-locale': 'zh-CN',
        }

        # CSV字段(中文表头)
        self.fieldnames = [
            '景点名称', '所属区域', '所属城市', '景点等级',
            '用户评分', '评论人数', '热度评分', '景点标签',
            '距离描述', '图片链接', '详情页链接'
        ]

        self._init_csv()

    def _init_csv(self):
        """初始化CSV文件,写入表头"""
        file_exists = os.path.isfile(self.output_file)
        with open(self.output_file, 'a', newline='', encoding='utf-8-sig') as f:
            writer = csv.DictWriter(f, fieldnames=self.fieldnames)
            if not file_exists:
                writer.writeheader()

    def _get_headers(self):
        """获取带随机UA的请求头,降低反爬风险"""
        headers = self.base_headers.copy()
        headers['user-agent'] = UserAgent().random
        return headers

    def _build_request_data(self, page_index):
        """构建POST请求的JSON数据"""
        data = {
            "head": {
                "cid": "09031109216141753464",
                "ctok": "",
                "cver": "1.0",
                "lang": "01",
                "sid": "8888",
                "syscode": "999",
                "auth": "",
                "xsid": "",
                "extension": []
            },
            "scene": "online",
            "districtId": self.district_id,
            "index": page_index,
            "sortType": 1,
            "count": 10,
            "filter": {"filterItems": []},
            "coordinate": {
                "latitude": 29.02786933333333,
                "longitude": 111.62905099999999,
                "coordinateType": "WGS84"
            },
            "returnModuleType": "all"
        }
        return json.dumps(data)

    def _save_to_csv(self, attraction_info):
        """保存单条数据到CSV"""
        with open(self.output_file, 'a', newline='', encoding='utf-8-sig') as f:
            writer = csv.DictWriter(f, fieldnames=self.fieldnames)
            writer.writerow(attraction_info)

    def fetch_page(self, page_index):
        """获取单页数据"""
        try:
            # 随机延时,避免请求过快
            time.sleep(random.uniform(2.5, 3.5))

            response = requests.post(
                self.base_url,
                headers=self._get_headers(),
                cookies=self.cookies,
                data=self._build_request_data(page_index)
            )

            if response.status_code != 200:
                print(f"请求失败,状态码:{response.status_code}")
                return None

            json_data = response.json()

            if 'attractionList' not in json_data:
                print(f"第{page_index}页没有找到景点数据")
                return None

            result = []
            for scenic in json_data['attractionList']:
                card = scenic.get('card', {})
                if not card:
                    continue

                # 处理标签列表
                tags = card.get('tagNameList', [])
                tag_str = '、'.join(tags) if tags else ''

                # 提取景点信息
                attraction_info = {
                    '景点名称': card.get('poiName', ''),
                    '所属区域': card.get('zoneName', ''),
                    '所属城市': card.get('districtName', ''),
                    '景点等级': card.get('sightLevelStr', ''),
                    '用户评分': card.get('commentScore', ''),
                    '评论人数': card.get('commentCount', ''),
                    '热度评分': card.get('heatScore', ''),
                    '景点标签': tag_str,
                    '距离描述': card.get('distanceStr', ''),
                    '图片链接': card.get('coverImageUrl', ''),
                    '详情页链接': card.get('detailUrl', '')
                }

                result.append(attraction_info)
                self._save_to_csv(attraction_info)
                print(f"已保存:{attraction_info['景点名称']} - {attraction_info['距离描述']}")

            print(f"第{page_index}页完成,获取{len(result)}条数据")
            return result

        except Exception as e:
            print(f"第{page_index}页出错:{str(e)}")
            return None

    def crawl(self, start_page=1, end_page=1):
        """执行爬取任务"""
        all_data = []
        for page in range(start_page, end_page + 1):
            data = self.fetch_page(page)
            if data:
                all_data.extend(data)
        print(f"\n爬取完成!共获取 {len(all_data)} 条数据,已保存至 {self.output_file}")
        return all_data

3.3 主函数调用

def main():
    # 携程Cookie(需从浏览器复制)
    cookies = {
        'GUID': '09031109216141753464',
        '_ga': 'GA1.1.2068278497.1768015686',
        'ibulocale': 'zh_cn',
        # ... 此处省略完整Cookie,请替换为自己的
    }

    # 创建爬虫实例
    spider = CtripAttractionSpider(
        cookies=cookies,
        district_id=145,              # 145代表武汉市
        output_file='携程武汉景点数据.csv'
    )

    # 爬取第1页到第101页(可根据实际需求调整)
    spider.crawl(start_page=1, end_page=101)


if __name__ == '__main__':
    main()

四、关键参数说明

4.1 district_id(城市代码)

不同城市的 district_id 不同,常用城市代码如下:

城市 district_id
北京 1
上海 2
广州 4
深圳 7
杭州 17
武汉 145
成都 104
重庆 158

4.2 Cookie获取方式

  1. 打开携程景点页面:https://you.ctrip.com/sight/wuhan145.html
  2. F12 打开开发者工具
  3. 切换到 Network 标签页
  4. 刷新页面,找到 getAttractionList 请求
  5. 在请求头中复制完整的 Cookie

4.3 爬取速度控制

代码中设置了 time.sleep(random.uniform(2.5, 3.5)),每次请求间隔2.5-3.5秒,避免因请求过快导致IP被封。


五、运行结果展示

运行成功后,控制台输出示例:
在这里插入图片描述

生成的CSV文件可用Excel直接打开,效果如下:
在这里插入图片描述

六、注意事项与优化建议

⚠️ 注意事项

  1. Cookie时效性:携程Cookie有一定有效期,如遇请求失败请及时更新
  2. 爬取频率:建议保持2秒以上的请求间隔,尊重网站服务器
  3. 仅供学习:本代码仅供Python爬虫技术学习交流,请勿用于商业用途

🚀 优化建议

  1. 代理IP池:大规模爬取时可接入代理IP池
  2. 异常重试:可增加请求失败重试机制
  3. 断点续爬:记录已爬取页码,支持中断后继续
  4. 多线程:在遵守爬取规范的前提下可考虑多线程提速

七、总结

本文详细介绍了携程景点数据的爬取方法,核心要点包括:

  • 分析移动端接口,获取结构化的JSON数据
  • 使用面向对象方式封装爬虫逻辑
  • 合理控制请求频率,降低反爬风险
  • 数据持久化保存为CSV格式

通过本教程,你可以轻松获取任意城市的携程景点数据,为后续的数据分析、可视化或旅游推荐系统提供数据支撑。


📎 参考资料


如果本文对你有帮助,欢迎点赞 👍、收藏 ⭐、关注 🔔
如有疑问,欢迎在评论区留言交流!


END

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐