1. 项目概述
这个Python爬虫实战项目将带领大家完成从携程网站采集景点数据,并将采集到的结构化数据存储到SQLite数据库的全过程。作为一个完整的爬虫案例,它不仅涉及网页数据的抓取和解析,还包括了数据持久化存储的实现,非常适合想要学习Python爬虫和数据存储的开发者。
我在实际工作中发现,旅游景点数据对于市场分析、竞品调研和用户行为研究都非常有价值。通过自动化采集这些数据,可以节省大量人工收集的时间成本。这个项目采用的携程景点页面结构相对稳定,适合作为爬虫学习的入门案例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术准备
2.1 环境配置
首先需要确保你的Python环境已经安装好以下库:
python复制pip install requests beautifulsoup4 sqlite3
requests:用于发送HTTP请求获取网页内容beautifulsoup4:用于解析HTML文档sqlite3:Python内置库,用于操作SQLite数据库
2.2 目标网站分析
在开始编写爬虫前,我们需要先分析携程景点页面的结构:
- 打开携程景点列表页(如:https://you.ctrip.com/sight/)
- 使用浏览器开发者工具(F12)查看页面元素
- 注意观察分页机制和景点详情链接的结构
重要提示:在编写爬虫前,请务必检查目标网站的robots.txt文件,遵守网站的爬取规则。合理的爬取间隔(建议3-5秒/次)可以避免给服务器造成过大压力。
3. 爬虫实现
3.1 获取列表页数据
首先实现获取景点列表页面的函数:
python复制import requests
from bs4 import BeautifulSoup
import time
def get_list_page(page_num):
"""
获取指定页码的景点列表页
:param page_num: 页码
:return: 页面HTML内容
"""
url = f"https://you.ctrip.com/sight/beijing1/s0-p{page_num}.html"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"获取第{page_num}页失败: {e}")
return None
3.2 解析列表页获取详情链接
接下来解析列表页,提取景点详情页链接:
python复制def parse_list_page(html):
"""
解析列表页,提取景点详情链接
:param html: 列表页HTML内容
:return: 详情页URL列表
"""
soup = BeautifulSoup(html, 'html.parser')
detail_links = []
items = soup.select('.list_mod2 > .rdetailbox')
for item in items:
link = item.select_one('a')['href']
if link and link.startswith('//you.ctrip.com'):
detail_links.append('https:' + link)
return detail_links
3.3 获取并解析详情页数据
现在实现获取和解析景点详情页的函数:
python复制def get_detail_page(url):
"""
获取景点详情页
:param url: 详情页URL
:return: 页面HTML内容
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"获取详情页失败: {url}, 错误: {e}")
return None
def parse_detail_page(html):
"""
解析景点详情页,提取结构化数据
:param html: 详情页HTML内容
:return: 景点信息字典
"""
soup = BeautifulSoup(html, 'html.parser')
# 提取景点名称
name = soup.select_one('.detailtit').get_text(strip=True) if soup.select_one('.detailtit') else ''
# 提取评分
rating = soup.select_one('.comment .score').get_text(strip=True) if soup.select_one('.comment .score') else ''
# 提取评论数
comment_count = soup.select_one('.comment .count').get_text(strip=True)[3:-1] if soup.select_one('.comment .count') else ''
# 提取景点介绍
description = soup.select_one('.text_style').get_text(strip=True) if soup.select_one('.text_style') else ''
# 提取门票信息
tickets = []
ticket_items = soup.select('.ticket-list .item')
for item in ticket_items:
ticket_name = item.select_one('.name').get_text(strip=True) if item.select_one('.name') else ''
ticket_price = item.select_one('.price').get_text(strip=True) if item.select_one('.price') else ''
tickets.append(f"{ticket_name}:{ticket_price}")
return {
'name': name,
'rating': rating,
'comment_count': comment_count,
'description': description,
'tickets': '|'.join(tickets)
}
4. 数据存储
4.1 创建SQLite数据库
我们使用SQLite来存储采集到的景点数据:
python复制import sqlite3
def init_db():
"""
初始化数据库,创建表结构
"""
conn = sqlite3.connect('ctrip_sights.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
rating REAL,
comment_count INTEGER,
description TEXT,
tickets TEXT,
url TEXT,
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
4.2 数据存储函数
实现将解析后的数据存储到数据库的函数:
python复制def save_to_db(data, url):
"""
将景点数据保存到数据库
:param data: 景点数据字典
:param url: 景点详情页URL
"""
conn = sqlite3.connect('ctrip_sights.db')
cursor = conn.cursor()
try:
cursor.execute('''
INSERT INTO sights (name, rating, comment_count, description, tickets, url)
VALUES (?, ?, ?, ?, ?, ?)
''', (
data['name'],
float(data['rating']) if data['rating'] else 0,
int(data['comment_count']) if data['comment_count'] else 0,
data['description'],
data['tickets'],
url
))
conn.commit()
except sqlite3.Error as e:
print(f"保存数据到数据库失败: {e}")
finally:
conn.close()
5. 主程序实现
5.1 爬取流程控制
现在将各个功能模块组合起来,实现完整的爬取流程:
python复制def main(start_page=1, end_page=5):
"""
主函数,控制爬取流程
:param start_page: 起始页码
:param end_page: 结束页码
"""
init_db()
for page in range(start_page, end_page + 1):
print(f"正在处理第 {page} 页...")
# 获取列表页
list_html = get_list_page(page)
if not list_html:
continue
# 解析列表页获取详情链接
detail_urls = parse_list_page(list_html)
for url in detail_urls:
print(f"正在处理: {url}")
# 获取详情页
detail_html = get_detail_page(url)
if not detail_html:
continue
# 解析详情页数据
sight_data = parse_detail_page(detail_html)
# 存储到数据库
if sight_data:
save_to_db(sight_data, url)
# 礼貌爬取,添加延迟
time.sleep(3)
# 每完成一页添加稍长延迟
time.sleep(5)
print("爬取完成!")
if __name__ == '__main__':
main()
5.2 分页处理优化
对于分页处理,我们可以添加一些智能判断:
python复制def get_max_page(html):
"""
获取最大页码
:param html: 列表页HTML内容
:return: 最大页码数
"""
soup = BeautifulSoup(html, 'html.parser')
page_links = soup.select('.pages .numpage')
if page_links:
return int(page_links[-1].get_text())
return 1
# 修改后的main函数
def smart_main():
"""
智能爬取,自动检测页数
"""
init_db()
# 先获取第一页,确定总页数
first_page_html = get_list_page(1)
if not first_page_html:
return
max_page = get_max_page(first_page_html)
print(f"检测到共有 {max_page} 页数据")
# 爬取所有页面
main(1, min(max_page, 10)) # 限制最多爬取10页
6. 数据处理与导出
6.1 从数据库查询数据
实现从SQLite数据库查询数据的函数:
python复制def get_sights_from_db(limit=10):
"""
从数据库获取景点数据
:param limit: 返回结果数量限制
:return: 景点数据列表
"""
conn = sqlite3.connect('ctrip_sights.db')
conn.row_factory = sqlite3.Row # 以字典形式返回结果
cursor = conn.cursor()
cursor.execute('SELECT * FROM sights ORDER BY rating DESC LIMIT ?', (limit,))
results = cursor.fetchall()
conn.close()
return results
6.2 导出数据到CSV
将数据库中的数据导出为CSV文件:
python复制import csv
def export_to_csv(filename='sights.csv'):
"""
将景点数据导出为CSV文件
:param filename: 导出的文件名
"""
sights = get_sights_from_db(limit=0) # 获取所有数据
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# 写入表头
writer.writerow(['ID', '景点名称', '评分', '评论数', '简介', '门票信息', 'URL', '创建时间'])
# 写入数据行
for sight in sights:
writer.writerow([
sight['id'],
sight['name'],
sight['rating'],
sight['comment_count'],
sight['description'],
sight['tickets'],
sight['url'],
sight['created_time']
])
print(f"数据已导出到 {filename}")
7. 常见问题与解决方案
7.1 反爬机制应对
携程网站可能会有一些反爬措施,以下是几种常见情况及解决方法:
-
请求频率限制:
- 症状:返回403错误或验证码页面
- 解决:增加请求间隔时间,使用
time.sleep()随机延迟
-
User-Agent检测:
- 症状:请求被拒绝
- 解决:轮换使用不同的User-Agent
-
IP封禁:
- 症状:无法访问任何页面
- 解决:使用代理IP池,或暂停爬取一段时间
改进后的请求函数示例:
python复制import random
def get_random_ua():
"""获取随机User-Agent"""
ua_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15'
]
return random.choice(ua_list)
def robust_request(url, max_retry=3):
"""更健壮的请求函数"""
for i in range(max_retry):
try:
headers = {'User-Agent': get_random_ua()}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return response.text
except Exception as e:
print(f"请求失败(尝试 {i+1}/{max_retry}): {e}")
time.sleep(random.uniform(2, 5))
return None
7.2 数据解析异常处理
在解析页面时可能会遇到各种异常情况:
-
元素不存在:
- 解决:使用条件判断或try-except块
-
页面结构变化:
- 解决:定期检查选择器是否仍然有效
-
数据格式不一致:
- 解决:添加数据清洗步骤
改进后的解析函数示例:
python复制def safe_extract_text(element, default=''):
"""安全提取文本内容"""
if element:
text = element.get_text(strip=True)
return text if text else default
return default
def safe_extract_attr(element, attr, default=''):
"""安全提取属性值"""
if element and attr in element.attrs:
return element[attr]
return default
8. 项目扩展与优化
8.1 使用Scrapy框架重构
对于大规模爬取需求,可以考虑使用Scrapy框架:
python复制import scrapy
class CtripSightSpider(scrapy.Spider):
name = 'ctrip_sight'
start_urls = ['https://you.ctrip.com/sight/beijing1/s0-p1.html']
def parse(self, response):
# 解析列表页
for sight in response.css('.list_mod2 > .rdetailbox'):
detail_url = sight.css('a::attr(href)').get()
if detail_url:
yield response.follow(detail_url, self.parse_detail)
# 处理分页
next_page = response.css('.pages .nextpage::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
def parse_detail(self, response):
# 解析详情页
item = {
'name': response.css('.detailtit::text').get('').strip(),
'rating': float(response.css('.comment .score::text').get('0')),
'comment_count': int(response.css('.comment .count::text').re_first(r'(\d+)', '0')),
'description': response.css('.text_style::text').get('').strip(),
'url': response.url
}
# 处理门票信息
tickets = []
for ticket in response.css('.ticket-list .item'):
tickets.append(
f"{ticket.css('.name::text').get('')}:{ticket.css('.price::text').get('')}"
)
item['tickets'] = '|'.join(tickets)
yield item
8.2 数据库优化建议
- 添加索引:为常用查询字段添加索引
python复制cursor.execute('CREATE INDEX IF NOT EXISTS idx_rating ON sights(rating)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_comment_count ON sights(comment_count)')
- 批量插入:使用事务批量插入提高性能
python复制def batch_insert(data_list):
conn = sqlite3.connect('ctrip_sights.db')
cursor = conn.cursor()
try:
conn.execute('BEGIN TRANSACTION')
for data in data_list:
cursor.execute('INSERT INTO sights (...) VALUES (...)', data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"批量插入失败: {e}")
finally:
conn.close()
- 定期维护:执行VACUUM命令优化数据库文件
python复制conn.execute('VACUUM')
9. 项目总结
通过这个项目,我们实现了一个完整的携程景点数据采集系统,包括:
- 网页请求和HTML解析
- 多页列表处理和详情页提取
- 数据结构化存储到SQLite数据库
- 数据导出和基本分析功能
在实际开发中,我还发现以下几点经验值得分享:
- 网页结构可能会随时变化,需要定期检查爬虫是否仍然有效
- 添加足够的错误处理和日志记录,便于排查问题
- 合理设置爬取间隔,避免对目标网站造成过大压力
- 数据库设计要考虑后续的查询需求,合理添加索引
这个项目可以进一步扩展为:
- 添加定时任务自动更新数据
- 实现数据可视化分析
- 构建景点推荐系统
- 开发REST API提供数据服务
