1. 项目背景与核心目标
在旅游行业数据分析和竞品研究中,景点数据是最基础也最关键的信息源之一。作为国内领先的OTA平台,携程网的景点数据覆盖全面、更新及时,包含了景点名称、评分、评论数、门票价格等丰富字段。这些数据对于旅游行业从业者、数据分析师和学术研究者都具有重要价值。
本项目将实现一个完整的Python爬虫解决方案,从携程景点列表页开始,通过多级抓取最终构建结构化的景点数据集。与简单的单页抓取不同,这个爬虫需要处理三个关键挑战:
- 多页列表的遍历抓取(通常需要处理分页参数和反爬机制)
- 从列表页到详情页的二级跳转(保持请求会话和参数传递)
- 数据清洗与结构化存储(将半结构化HTML转为规整的表格数据)
最终我们会将采集到的数据持久化到SQLite数据库,形成一个可直接用于分析的结构化数据集。这个方案可以轻松扩展到其他OTA平台的数据采集场景。
提示:在实际操作中,请严格遵守robots.txt协议,控制请求频率(建议每次请求间隔2-3秒),避免对目标服务器造成过大压力。商业用途的数据采集需要获得平台授权。
2. 技术栈选型与环境准备
2.1 核心工具链选择
对于这类中等复杂度的网页抓取任务,经过多个项目的实践对比,我推荐以下技术组合:
python复制# 请求库:相比urllib有更好的API设计和连接池管理
import requests
# 解析库:比BeautifulSoup性能更好,支持XPath
from lxml import html
# 数据存储
import sqlite3
# 其他辅助
import time
import random
from fake_useragent import UserAgent
选择这些库的主要考虑因素:
requests比标准库urllib更简洁,自动处理连接复用lxml的解析速度是BeautifulSoup的5-10倍,特别适合批量处理- SQLite无需单独安装服务,适合中小规模数据存储
2.2 开发环境配置
建议使用Python 3.8+环境,主要依赖库安装命令:
bash复制pip install requests lxml fake-useragent
对于IDE的选择:
- VSCode:轻量级,适合快速开发调试
- PyCharm:专业版提供更好的HTTP请求调试工具
关键配置点:
- 在VSCode中安装Python扩展
- 设置合理的请求超时(建议connect=5s, read=30s)
- 准备User-Agent轮换池(使用fake-useragent自动生成)
3. 网页结构与数据定位分析
3.1 携程景点列表页解剖
通过浏览器开发者工具分析,携程景点列表页(以北京为例)的关键结构如下:
html复制<div class="list-item">
<h2 class="title">
<a href="/sight/beijing1/123.html">景点名称</a>
</h2>
<div class="comment">
<span>1234条点评</span>
<span class="score">4.7</span>
</div>
<div class="price">
<em>¥</em>120
</div>
</div>
需要提取的字段与对应XPath:
- 景点名称:
//div[@class="list-item"]/h2/a/text() - 详情链接:
//div[@class="list-item"]/h2/a/@href - 点评数量:
//div[@class="comment"]/span[1]/text() - 评分:
//span[@class="score"]/text() - 价格:
//div[@class="price"]/em/following-sibling::text()
3.2 详情页关键数据定位
进入详情页后,通常需要采集这些补充信息:
- 详细地址
- 开放时间
- 景点介绍文本
- 经纬度坐标(如果有)
这些字段的定位方式需要根据具体页面结构调整,常见策略包括:
- 通过class或id直接定位
- 通过相邻元素关系定位(如"地址:"后的
<span>) - 通过正则表达式提取JavaScript中的JSON数据
4. 爬虫核心实现步骤
4.1 基础请求模块封装
python复制def get_page(url, retry=3):
headers = {'User-Agent': UserAgent().random}
for i in range(retry):
try:
resp = requests.get(url, headers=headers,
timeout=(5, 30))
if resp.status_code == 200:
return resp.text
elif resp.status_code == 403:
raise Exception("触发反爬机制")
else:
time.sleep(random.uniform(2, 5))
except Exception as e:
print(f"请求失败: {e}, 重试 {i+1}/{retry}")
time.sleep(5 * (i + 1))
return None
关键设计点:
- 自动重试机制应对网络波动
- 随机User-Agent降低被封风险
- 指数退避的重试间隔(1s, 4s, 9s...)
4.2 分页抓取逻辑实现
携程的分页通常有两种形式:
- 传统页码分页:
?page=2 - 滚动加载:通过AJAX请求获取
通过实际测试,我们发现北京景点列表采用第一种方式:
python复制base_url = "https://you.ctrip.com/sight/beijing1/s0-p{page}.html"
max_page = 50 # 实际应先获取总页数
for page in range(1, max_page + 1):
url = base_url.format(page=page)
html_content = get_page(url)
if not html_content:
continue
tree = html.fromstring(html_content)
items = tree.xpath('//div[@class="list-item"]')
for item in items:
# 解析单个景点数据
parse_item(item)
time.sleep(random.uniform(1, 3)) # 关键限速
4.3 数据解析与清洗
从原始HTML到结构化数据的转换示例:
python复制def parse_item(item):
data = {
'name': item.xpath('.//h2/a/text()')[0].strip(),
'url': 'https://you.ctrip.com' + item.xpath('.//h2/a/@href')[0],
'comment_count': int(re.search(r'\d+',
item.xpath('.//div[@class="comment"]/span[1]/text()')[0]).group()),
'score': float(item.xpath('.//span[@class="score"]/text()')[0]),
'price': float(item.xpath('.//div[@class="price"]/em/following-sibling::text()')[0].strip())
}
# 数据清洗逻辑
if data['comment_count'] > 10000: # 处理"1万+"这类显示
data['comment_count'] = 10000
return data
常见数据清洗需求:
- 去除字符串首尾空白
- 处理中文数字(如"1.2万"→12000)
- 统一单位(如价格统一为元)
- 处理缺失值(评分可能不存在)
5. 数据存储与持久化
5.1 SQLite数据库设计
考虑到景点数据的特性,我们设计以下表结构:
sql复制CREATE TABLE IF NOT EXISTS attractions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT UNIQUE,
comment_count INTEGER DEFAULT 0,
score REAL CHECK (score BETWEEN 0 AND 5),
price REAL,
address TEXT,
open_time TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
设计要点:
- 设置url为UNIQUE避免重复存储
- 对score字段添加CHECK约束
- 自动记录创建时间
5.2 Python存储实现
python复制class DataStorage:
def __init__(self, db_path='ctrip.db'):
self.conn = sqlite3.connect(db_path)
self._create_table()
def _create_table(self):
sql = """CREATE TABLE IF NOT EXISTS attractions (...)""" # 接上面的SQL
self.conn.execute(sql)
def save_item(self, item):
sql = """INSERT OR IGNORE INTO attractions
(name, url, comment_count, score, price)
VALUES (?, ?, ?, ?, ?)"""
params = (item['name'], item['url'],
item['comment_count'], item['score'], item['price'])
self.conn.execute(sql, params)
self.conn.commit()
高级功能扩展:
- 批量插入(使用executemany)
- 断点续爬(记录已爬取的URL)
- 数据去重(通过UNIQUE约束)
6. 反爬策略应对方案
6.1 常见反爬手段识别
在携程爬取过程中可能遇到:
- 请求频率限制(HTTP 429)
- User-Agent检测
- IP封禁
- 验证码挑战(特别是Cloudflare)
6.2 实战应对措施
- 请求限速:
python复制# 在关键请求之间添加随机延迟
time.sleep(random.uniform(1, 3))
- 请求头伪装:
python复制headers = {
'User-Agent': UserAgent().random,
'Referer': 'https://you.ctrip.com/',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
- 代理IP池(进阶):
python复制proxies = {
'http': 'http://user:pass@proxy_ip:port',
'https': 'http://user:pass@proxy_ip:port'
}
response = requests.get(url, proxies=proxies)
- 自动化验证码处理:
- 使用第三方打码平台
- 人工介入的断点设计
7. 项目优化与扩展方向
7.1 性能优化建议
- 异步请求(aiohttp + asyncio):
python复制import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
- 分布式爬虫(Scrapy + Redis):
- 使用Scrapy框架重构
- Redis作为任务队列
- 增量抓取策略:
- 记录最后更新时间
- 只抓取新增或变更的内容
7.2 数据分析扩展
采集到的数据可用于:
- 景点热度分析(评论数随时间变化)
- 价格区间分布
- 评分与价格的相关性
- 区域景点密度地图
示例分析代码:
python复制import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_sql("SELECT * FROM attractions", con=conn)
df.plot.scatter(x='price', y='score')
plt.show()
8. 法律合规与道德考量
在开发和使用网络爬虫时,必须注意:
- 严格遵守
robots.txt规定(携程的规则通常位于https://www.ctrip.com/robots.txt) - 控制请求频率,避免对目标服务器造成负担
- 不抓取个人隐私数据(如用户评论中的个人信息)
- 商业用途需获得平台授权
- 数据使用需遵守平台用户协议
在实际项目中,我通常会:
- 设置明显的爬虫标识(在User-Agent中注明)
- 提供合规的使用声明
- 实现自动遵守robots.txt的逻辑
- 为网站管理员提供联系和退订方式
这个爬虫项目展示了从数据采集到存储的完整流程,其中的技术方案可以灵活调整以适应不同的目标网站。最重要的是在获取数据的同时,保持对数据源和服务器的尊重,这样才能实现可持续的数据采集。
