Python携程网旅游景点数据抓取

0 阅读7分钟

爬取携程重庆旅游景点数据: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)

三、爬虫整体架构

整个爬虫分为以下几个模块:

  1. 列表页请求:调用携程的内部接口 getAttractionList,获取景点卡片数据(提供标题、等级、封面、门票等)。
  2. 详情页解析:访问每个景点的详情页,提取额外信息(如开放时间、轮播图、好评/差评数、更多内容等)。
  3. 数据清洗:统一处理空值、去除多余空格、合并重复字段。
  4. 数据存储:实时写入 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对象,包含 districtIdindex(页码)、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.baseInfoItemdiv.hotTags 等容器中:

  • 地址、开放时间、官方电话:通过查找包含特定关键词的 .baseInfoTitle 提取。
  • 好评/差评数:在 .hotTags 中通过正则匹配。
  • 轮播图:解析 .swiperMain .swiperItemstyle 属性中的 background-image URL。
  • 更多内容:直接获取 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-AgentReferer,让请求看起来像正常浏览器访问。

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 格式,便于程序读取。

由于字段众多,这里仅展示前两行数据(部分字段):

标题等级评分评论数好评数消费后评价数差评数门票开放时间...
重庆动物园4A4.635601820910830旺季25元08:00-17:00...
武隆天生三桥5A4.810234523029802024旺季125元08:30-16:30...

所有字段均采集到位,满足深度分析需求。


七、效果图

0068a1e6156864715215a6d95e0b5e47.png

95148135b9bde5f70203f9e7fc8de19c.png

de96382f290f91d14977a993c13c1443.png

八、常见问题与改进建议

1. 如何抓取其他城市?

只需修改 LIST_URL 中的城市拼音和ID,例如:

  • 北京:https://you.ctrip.com/sight/beijing5.html
  • 上海:https://you.ctrip.com/sight/shanghai2.html

2. 如何应对IP封禁?

  • 增大 DELAY_SECONDS(如 1 秒)。
  • 使用代理IP池(可参考 requestsproxies 参数)。

3. 数据不全怎么办?

  • 检查详情页的HTML结构是否有变化,可能需要更新选择器。
  • 部分字段可能隐藏在 JavaScript 动态渲染中,此时可使用 Selenium 或分析数据接口。

4. 如何增加更多字段?

本代码已提供扩展接口,如需增加“交通信息”、“小贴士”等,可在 extract_detail_info 中添加新的选择器。


九、总结

通过这个实战案例,我们学会了:

  • 如何分析网页接口,找到真实数据源。
  • 使用 requests + BeautifulSoup 进行静态页面解析。
  • 处理多页数据,并发控制。
  • 完整采集20个字段,覆盖景点基础、评价、图片、描述等全方位信息。
  • 数据清洗与多格式输出。
  • 应对反爬的基本策略。

完整代码已上传至 小红书获取(可附链接),欢迎 fork 和改进。

希望这篇教程对你有帮助!如果有任何问题或建议,欢迎留言讨论。


附录:代码文件结构

ctrip_spider/
├── spider.py      # 主程序
└── result.csv     # 输出(运行后)

❗❗❗注意:请尊重网站权益,合理使用爬虫,勿对目标服务器造成压力。