🏔️ 重庆市旅游景点数据可视化分析系统 - 从数据爬取到智能推荐的完整实现
作为一名热爱旅游的Python开发者,我一直在思考如何用技术手段更好地了解家乡重庆的旅游资源。经过历时2天的开发,我完成了一个集数据爬取、可视化分析、智能推荐于一体的重庆旅游景点数据分析系统。今天,我将详细分享这个项目的实现过程和核心技术点。
📋 项目概述
这是一个基于PythonWeb(Django)框架开发的旅游景点数据可视化分析平台,主要功能包括:
- 🕷️ 数据爬取:从携程网实时爬取重庆旅游景点数据
- 📊 数据可视化:使用ECharts实现多维度数据展示
- 👤 用户系统:注册登录、个人中心、收藏管理、浏览历史
- 📈 数据分析:景点分布、等级分析、词云汇总、价格分析、评分分析
- 🎯 智能推荐:基于用户行为的个性化景点推荐
技术栈:Python + Django + MySQL + Requests + BeautifulSoup + ECharts + Bootstrap
🏗️ 项目架构设计
整体架构图
重庆市旅游景点数据可视化分析系统
├── 数据层
│ ├── MySQL数据库 (存储景点、用户、收藏、浏览历史)
│ └── CSV/JSON文件 (爬取数据临时存储)
├── 爬虫层
│ ├── main.py (携程网数据爬取)
│ └── import_data.py (数据导入数据库)
├── 业务逻辑层
│ ├── models.py (数据模型定义)
│ └── views.py (业务逻辑处理)
├── 表现层
│ ├── templates/ (HTML模板)
│ └── static/ (静态资源)
└── 路由层
└── urls.py (URL路由配置)
核心功能模块
- 数据爬取模块:爬取携程网重庆景点数据
- 数据存储模块:使用MySQL存储结构化数据
- 数据可视化模块:多维度图表展示
- 用户管理模块:用户认证和权限管理
- 智能推荐模块:基于用户行为的推荐算法
🕷️ 数据爬取模块实现
爬虫设计思路
爬虫模块的核心是从携程网获取重庆旅游景点的详细信息,包括景点名称、等级、评分、评论数、门票价格、开放时间等20多个字段。
核心代码解析
1. HTTP客户端封装
class HttpClient:
def __init__(self, timeout: int = 20):
self.timeout = timeout
self.session = requests.Session()
def request_json(self, method: str, url: str, headers: Optional[Dict[str, str]] = None,
json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
response_text = self.request_text(method, url, headers=headers, json_data=json_data, params=params)
return json.loads(response_text)
设计亮点:
- 封装了requests.Session,支持连接池复用
- 实现了自动重试机制,当requests失败时自动切换到curl
- 支持JSON和文本两种响应格式
2. 列表页数据获取
def crawl(list_url: str, page_size: int, max_pages: int, delay: float,
sort_type: int, timeout: int, max_items: int, csv_out_path: str) -> List[Dict[str, Any]]:
district_id = parse_district_id(list_url)
client = HttpClient(timeout=timeout)
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)
技术要点:
- 解析携程网API接口,提取district_id参数
- 构造POST请求payload,支持分页和排序
- 实现了增量爬取,避免重复数据
3. 详情页数据解析
def extract_detail_info(detail_html: str) -> Dict[str, Any]:
soup = BeautifulSoup(detail_html, "html.parser")
rank_text = ""
rank_node = soup.select_one(".rankText")
if rank_node:
rank_text = normalize_text(rank_node.get_text(" ", strip=True))
address = extract_base_info_by_title(soup, "地址")
open_time = extract_base_info_by_title(soup, "开放时间")
comment_count_map = {"好评数": 0, "消费后评价数": 0, "差评数": 0}
for tag in soup.select(".hotTags .hotTag"):
tag_text = normalize_text(tag.get_text(" ", strip=True))
match = re.search(r"(好评|消费后评价|差评)\s*\(?\s*(\d+)\s*\)?", tag_text)
if match:
label = match.group(1)
value = int(match.group(2))
comment_count_map[f"{label}数"] = value
解析技巧:
- 使用BeautifulSoup解析HTML结构
- 通过CSS选择器精确定位数据节点
- 使用正则表达式提取结构化数据
- 实现了文本清洗和标准化处理
4. 数据持久化
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)
writer.writerow(_format_csv_row(row))
f.flush()
存储策略:
- 采用增量写入方式,实时保存数据
- 使用UTF-8-BOM编码,确保Excel兼容性
- 支持JSON和CSV双格式导出
爬虫配置参数
LIST_URL = "https://you.ctrip.com/sight/chongqing158.html"
PAGE_SIZE = 10
MAX_PAGES = 0 # 0表示抓到最后一页
MAX_ITEMS = 0 # 0表示不限制
SORT_TYPE = 1
DELAY_SECONDS = 0.2
TIMEOUT_SECONDS = 20
配置说明:
- 支持自定义分页大小和最大页数
- 可设置爬取延迟,避免被封IP
- 支持多种排序方式(热度、评分、距离等)
数据字段说明
爬取的数据包含以下20个字段:
| 字段名 | 说明 | 示例 |
|---|---|---|
| 标题 | 景点名称 | 洪崖洞民俗风貌区 |
| 等级 | 景点等级 | 4A、5A |
| 碑榜 | 排行榜信息 | 2026全球100夜游必打卡景点 |
| 标签 | 景点标签 | 遛娃宝藏地|城市漫步 |
| 热度 | 热度分数 | 8.8 |
| 评分 | 用户评分 | 4.6 |
| 评论数 | 总评论数 | 8583 |
| 好评数 | 好评数量 | 4104 |
| 消费后评价数 | 消费后评价 | 1355 |
| 差评数 | 差评数量 | 160 |
| 地址 | 所在区域 | 解放碑/洪崖洞 |
| 详细地址 | 完整地址 | 重庆市渝中区朝天门街道嘉陵江滨江路88号 |
| 距离 | 距离市中心 | 距市中心2.7km |
| 门票 | 门票价格 | 免费 |
| 开放时间 | 营业时间 | 全天开放 |
| 官方电话 | 联系电话 | 023-63039995 |
| 封面图片链接 | 主图URL | dimg04.c-ctrip.com/images/... |
| 详情链接 | 详情页URL | you.ctrip.com/sight/... |
| 相关图片链接 | 轮播图URL | 多个图片URL用|分隔 |
| 更多内容 | 景点介绍 | 详细文字描述 |
爬虫运行效果
[截图位置1:爬虫运行终端输出]
🗄️ 数据存储模块实现
数据库设计
1. 景点数据模型
class ScenicSpot(models.Model):
title = models.CharField("景点名称", max_length=200)
level = models.CharField("等级", max_length=10, default='')
ranking = models.CharField("碑榜", max_length=100, default='')
tags = models.CharField("标签", max_length=200, default='')
popularity = models.FloatField("热度", default=0)
rating = models.FloatField("评分", default=0)
comment_count = models.IntegerField("评论数", default=0)
positive_count = models.IntegerField("好评数", default=0)
post_purchase_count = models.IntegerField("消费后评价数", default=0)
negative_count = models.IntegerField("差评数", default=0)
address = models.CharField("地址", max_length=50, default='')
detail_address = models.CharField("详细地址", max_length=200, default='')
distance = models.CharField("距离", max_length=50, default='')
ticket = models.CharField("门票", max_length=100, default='')
opening_time = models.CharField("开放时间", max_length=500, default='')
phone = models.CharField("官方电话", max_length=50, default='')
cover_image = models.URLField("封面图片链接", max_length=500, default='')
detail_url = models.URLField("详情链接", max_length=500, default='')
carousel_images = models.TextField("相关图片链接", default='')
description = models.TextField("更多内容", default='')
设计特点:
- 完整映射爬取的20个数据字段
- 使用合适的数据类型(CharField、FloatField、IntegerField等)
- 设置合理的字段长度限制
- 添加了verbose_name参数,便于后台管理
2. 用户数据模型
class User(models.Model):
id = models.AutoField('id', primary_key=True)
username = models.CharField(verbose_name="姓名", max_length=22, default='')
password = models.CharField(verbose_name="密码", max_length=32, default='')
phone = models.CharField(verbose_name="手机号", max_length=11, default='')
email = models.CharField(verbose_name="邮箱", max_length=22, default='')
time = models.DateField(verbose_name="创建时间", auto_now_add=True)
avatar = models.FileField(verbose_name="头像", default='user/default.gif', upload_to="user/")
3. 收藏和浏览历史模型
class Favorite(models.Model):
id = models.AutoField(primary_key=True, verbose_name='ID')
scenic_spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE, verbose_name='景点')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户')
count = models.IntegerField("点击次数", default=1)
created_time = models.DateTimeField("收藏时间", auto_now_add=True)
class BrowseHistory(models.Model):
id = models.AutoField(primary_key=True, verbose_name='ID')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户')
scenic_spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE, verbose_name='景点')
browse_time = models.DateTimeField('浏览时间', auto_now=True)
view_count = models.IntegerField('浏览次数', default=1)
关系设计:
- 使用外键关联User和ScenicSpot
- 实现了多对多关系(一个用户可以收藏多个景点,一个景点可以被多个用户收藏)
- 添加了时间戳字段,便于数据分析
数据导入实现
def import_scenic_spots(csv_file_path):
csv_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'result.csv')
with open(csv_file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
rows = list(reader)
for index, row in enumerate(rows, 1):
obj, created = ScenicSpot.objects.update_or_create(
title=row.get('标题', '').strip(),
defaults={
'level': row.get('等级', '').strip() or '无',
'ranking': row.get('碑榜', '').strip() or '无',
'tags': row.get('标签', '').strip(),
'popularity': parse_float(row.get('热度', '')),
'rating': parse_float(row.get('评分', '')),
'comment_count': parse_int(row.get('评论数', 0)),
# ... 其他字段
}
)
导入策略:
- 使用Django ORM的update_or_create方法
- 以景点名称为唯一标识,避免重复导入
- 实现了数据类型转换和异常处理
- 支持增量更新,保留用户收藏和浏览历史
[截图位置3:数据库表结构图]
展示MySQL数据库中的表结构,包括scenic_spot、User、favorite、browse_history等表。
📊 数据可视化模块实现
可视化功能概览
系统提供了5个可视化分析页面:
- 景点分布:按行政区、等级、热度、门票价格分布
- 等级分析:不同等级景点的评分、价格、热度对比
- 词云汇总:景点标签词云展示
- 价格分析:门票价格区间分布和趋势
- 评分分析:用户评分分布和影响因素
景点分布可视化
1. 数据处理逻辑
def touristDistribute(request):
# 从详细地址中提取行政区
def extract_district(detail_address):
if detail_address and '重庆市' in detail_address:
parts = detail_address.split('重庆市')[1].strip()
if parts:
import re
match = re.search(r'([^区市县]+[区市县])', parts)
if match:
return match.group(1)
return '其他'
# 统计各区景点数量
district_counts = {}
for spot in filtered_spots:
district = extract_district(spot.detail_address)
if district not in district_counts:
district_counts[district] = 0
district_counts[district] += 1
# 统计各等级景点数量
level_counts = {'5A': 0, '4A': 0, '其他': 0}
for spot in filtered_spots:
level = spot.level
if level == '5A':
level_counts['5A'] += 1
elif level == '4A':
level_counts['4A'] += 1
else:
level_counts['其他'] += 1
数据处理技巧:
- 使用正则表达式从地址中提取行政区
- 实现了多维度数据统计
- 支持按行政区筛选
2. ECharts图表配置
// 行政区分布饼图
var districtChart = echarts.init(document.getElementById('districtChart'));
var districtOption = {
title: {
text: '重庆市各区景点分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '景点数量',
type: 'pie',
radius: '50%',
data: {{ result1|safe }},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
districtChart.setOption(districtOption);
图表特点:
- 使用ECharts 5.x版本
- 支持交互式数据展示
- 实现了响应式布局
- 添加了动画效果和悬停提示
[截图位置4:景点分布可视化页面]
展示4个饼图:行政区分布、等级分布、评分分布、价格分布。
等级分析可视化
def touristLevel(request):
# 统计各等级的平均评分、平均门票价格、平均热度
level_stats = {}
for spot in filtered_spots:
level = spot.level if spot.level else '其他'
if level not in level_stats:
level_stats[level] = {
'count': 0,
'paid_count': 0,
'total_rating': 0,
'total_price': 0,
'total_popularity': 0
}
level_stats[level]['count'] += 1
level_stats[level]['total_rating'] += spot.rating
level_stats[level]['total_popularity'] += spot.popularity
# 处理门票价格
ticket = spot.ticket
if ticket and '免费' not in ticket and '0元' not in ticket:
match = re.search(r'\d+(\.\d+)?', ticket)
if match:
price = float(match.group(0))
level_stats[level]['total_price'] += price
level_stats[level]['paid_count'] += 1
分析维度:
- 不同等级景点的数量对比
- 各等级景点的平均评分
- 各等级景点的平均门票价格
- 各等级景点的平均热度
[截图位置5:等级分析可视化页面]
展示柱状图和折线图,对比不同等级景点的各项指标。
词云汇总可视化
def touristWordcloud(request):
# 提取所有标签
all_tags = []
for spot in all_spots:
if spot.tags:
tags = [tag.strip() for tag in spot.tags.split('|') if tag.strip()]
all_tags.extend(tags)
# 统计标签频率
tag_counts = {}
for tag in all_tags:
if tag not in tag_counts:
tag_counts[tag] = 0
tag_counts[tag] += 1
# 转换为词云数据格式
wordcloud_data = []
for tag, count in tag_counts.items():
wordcloud_data.append({'name': tag, 'value': count})
词云实现:
- 统计所有景点的标签频率
- 使用ECharts词云插件
- 支持自定义颜色和字体大小
- 实现了交互式点击事件
[截图位置6:词云汇总可视化页面]
展示景点标签词云,字体大小表示标签出现频率。
价格分析可视化
def priceAnalysis(request):
# 统计门票价格分布
ticket_ranges = {
'免费': 0,
'0-50元': 0,
'50-100元': 0,
'100-150元': 0,
'150-200元': 0,
'200-300元': 0,
'300元以上': 0
}
for spot in ScenicSpot.objects.all():
ticket_now = spot.ticket
price = None
if ticket_now and '免费' not in ticket_now and '无' not in ticket_now:
if '门票' in ticket_now:
try:
price = float(ticket_now.replace('门票', '').replace('元起', '').strip())
except:
price = None
if price:
if price < 50:
ticket_ranges['0-50元'] += 1
elif price < 100:
ticket_ranges['50-100元'] += 1
# ... 其他区间
价格分析功能:
- 按价格区间统计景点数量
- 分析不同等级景点的价格分布
- 计算平均门票价格
- 识别免费景点
[截图位置7:价格分析可视化页面]
展示价格分布柱状图和价格趋势折线图。
评分分析可视化
def commentAnalysis(request):
# 统计评分分布
rating_ranges = {
'9分以上': 0,
'8-9分': 0,
'7-8分': 0,
'6-7分': 0,
'5-6分': 0,
'3-5分': 0,
'0-3分': 0
}
for spot in all_spots:
rating = spot.rating
if rating >= 9:
rating_ranges['9分以上'] += 1
elif rating >= 8:
rating_ranges['8-9分'] += 1
# ... 其他区间
# 统计评论情感分析
sentiment_data = {
'好评': total_positive,
'中评': total_neutral,
'差评': total_negative
}
评分分析维度:
- 用户评分分布
- 评论情感分析
- 评分与热度的相关性
- 评分与价格的关系
[截图位置8:评分分析可视化页面]
展示评分分布直方图和情感分析饼图。
👤 用户系统实现
用户认证模块
def login(request):
if request.method == 'POST':
name = request.POST.get('name')
password = request.POST.get('password')
if User.objects.filter(username=name, password=password):
user = User.objects.get(username=name, password=password)
request.session['username'] = {
'username': user.username,
'avatar': user.avatar.name,
'id': user.id
}
return redirect('index')
else:
msg = '信息错误!'
return render(request, 'login.html', {"msg": msg})
认证特点:
- 使用Session管理用户登录状态
- 实现了用户名密码验证
- 支持记住登录状态
- 添加了错误提示功能
个人中心功能
def selfInfo(request):
username = request.session['username'].get('username')
useravatar = request.session['username'].get('avatar')
if request.method == 'POST':
phone = request.POST.get("phone")
email = request.POST.get("email")
password = request.POST.get("password")
avatar = request.FILES.get("avatar")
selfmes = User.objects.get(username=username)
selfmes.phone = phone
selfmes.email = email
selfmes.password = password
if avatar:
selfmes.avatar = avatar
useravatar = selfmes.avatar.name
request.session['username'] = {
'username': username,
'avatar': useravatar,
'id': request.session['username'].get('id')
}
selfmes.save()
功能特点:
- 支持修改个人信息
- 支持上传头像
- 实时更新Session信息
- 数据验证和错误处理
收藏功能实现
def addFavorite(request, scenic_id):
user_id = request.session.get('username', {}).get('id')
if user_id:
user = User.objects.get(id=user_id)
scenic = get_object_or_404(ScenicSpot, id=scenic_id)
favorite, created = Favorite.objects.get_or_create(
user=user,
scenic_spot=scenic,
defaults={'user': user, 'scenic_spot': scenic}
)
return redirect('scenicDetail', scenic_id=scenic_id)
def deleteFavorite(request, favorite_id):
Favorite.objects.filter(id=favorite_id).delete()
return redirect('historyTableData')
收藏功能:
- 使用get_or_create避免重复收藏
- 支持取消收藏
- 记录收藏时间
- 统计收藏次数
浏览历史功能
def scenicDetail(request, scenic_id):
scenic = get_object_or_404(ScenicSpot, id=scenic_id)
user_id = request.session.get('username', {}).get('id')
if user_id:
user = User.objects.get(id=user_id)
history, created = BrowseHistory.objects.get_or_create(
user=user,
scenic_spot=scenic,
defaults={'view_count': 1}
)
if not created:
history.view_count += 1
history.save()
浏览历史:
- 自动记录用户浏览行为
- 统计浏览次数
- 按时间倒序排列
- 支持删除历史记录
[截图位置9:用户个人中心页面]
[截图位置10:收藏列表页面]
展示用户收藏的景点列表,支持删除操作。
[截图位置11:浏览历史页面]
展示用户浏览历史,按时间排序。
🎯 智能推荐模块实现
推荐算法设计
def personalizedRecommendation(request):
user_id = request.session.get('username', {}).get('id')
if not user_id:
return redirect('login')
# 获取用户浏览历史
browse_history = BrowseHistory.objects.filter(user_id=user_id).select_related('scenic_spot')
# 获取用户收藏
favorites = Favorite.objects.filter(user_id=user_id).select_related('scenic_spot')
# 基于协同过滤的推荐
# 1. 找到相似用户
# 2. 推荐相似用户喜欢但当前用户未浏览的景点
# 基于内容的推荐
# 1. 分析用户偏好的景点标签
# 2. 推荐具有相似标签的景点
# 混合推荐策略
recommended_spots = []
# 获取用户浏览过的景点ID
viewed_spot_ids = set([bh.scenic_spot.id for bh in browse_history])
# 获取用户收藏的景点ID
favorite_spot_ids = set([f.scenic_spot.id for f in favorites])
# 排除已浏览和已收藏的景点
excluded_ids = viewed_spot_ids | favorite_spot_ids
# 基于热门度和评分的推荐
hot_spots = ScenicSpot.objects.exclude(id__in=excluded_ids).order_by('-popularity', '-rating')[:20]
# 基于标签的推荐
if browse_history.exists():
# 提取用户浏览过的景点的标签
user_tags = set()
for bh in browse_history:
if bh.scenic_spot.tags:
tags = [tag.strip() for tag in bh.scenic_spot.tags.split('|')]
user_tags.update(tags)
# 推荐具有相似标签的景点
for tag in user_tags:
tag_spots = ScenicSpot.objects.filter(
tags__contains=tag
).exclude(id__in=excluded_ids).order_by('-rating')[:5]
recommended_spots.extend(tag_spots)
# 去重并排序
recommended_spots = list(set(recommended_spots))
recommended_spots = sorted(recommended_spots, key=lambda x: (-x.rating, -x.popularity))[:30]
推荐策略:
- 基于热门度的推荐:推荐高热度、高评分的景点
- 基于内容的推荐:根据用户浏览历史,推荐具有相似标签的景点
- 协同过滤推荐:基于用户行为模式推荐相似用户喜欢的景点
- 混合推荐:结合多种策略,提高推荐准确性
[截图位置12:个性化推荐页面]
展示为用户推荐的景点列表,包含推荐理由。
🎨 前端界面设计
页面布局
系统采用了响应式设计,主要特点:
- 侧边栏导航:固定左侧,包含所有功能模块
- 顶部导航栏:显示用户信息和快捷操作
- 主内容区:动态加载不同页面内容
- 卡片式设计:景点信息以卡片形式展示
首页设计
def index(request):
scenic_spots = ScenicSpot.objects.all()
total_count = scenic_spots.count()
user_count = User.objects.count()
level_5a_count = scenic_spots.filter(level='5A').count()
level_4a_count = scenic_spots.filter(level='4A').count()
avg_rating = scenic_spots.aggregate(avg=Avg('rating'))['avg'] or 0
avg_popularity = scenic_spots.aggregate(avg=Avg('popularity'))['avg'] or 0
total_comments = scenic_spots.aggregate(total=Sum('comment_count'))['total'] or 0
max_rating_spot = scenic_spots.order_by('-rating').first()
max_popularity_spot = scenic_spots.order_by('-popularity').first()
# 搜索和筛选
search_query = request.GET.get('search', '')
selected_level = request.GET.get('level', '')
selected_address = request.GET.get('address', '')
filtered_spots = scenic_spots
if search_query:
filtered_spots = filtered_spots.filter(title__icontains=search_query)
if selected_level:
filtered_spots = filtered_spots.filter(level=selected_level)
if selected_address:
filtered_spots = filtered_spots.filter(address=selected_address)
# 分页
paginator = Paginator(filtered_spots, 12)
page = request.GET.get('page', 1)
c_page = paginator.get_page(page)
首页功能:
- 统计数据概览(景点总数、用户数、5A/4A数量等)
- 景点搜索和筛选
- 景点列表展示(分页)
- 热门景点推荐
[截图位置13:系统首页]
展示统计数据概览、搜索筛选栏、景点卡片列表。
景点详情页
def scenicDetail(request, scenic_id):
scenic = get_object_or_404(ScenicSpot, id=scenic_id)
# 处理轮播图
carousel_images = []
if scenic.carousel_images:
try:
image_data = json.loads(scenic.carousel_images)
if isinstance(image_data, list):
carousel_images = [url for url in image_data if url][:5]
elif isinstance(image_data, str):
image_urls = [url.strip() for url in image_data.split('|') if url.strip()]
carousel_images = image_urls[:5]
except (json.JSONDecodeError, TypeError):
image_urls = [url.strip() for url in scenic.carousel_images.split('|') if url.strip()]
carousel_images = image_urls[:4]
# 检查是否已收藏
is_favorited = False
if user_id:
is_favorited = Favorite.objects.filter(user_id=user_id, scenic_spot=scenic).exists()
详情页功能:
- 景点基本信息展示
- 图片轮播
- 收藏功能
- 浏览历史记录
- 相关推荐
[截图位置14:景点详情页]
展示景点详细信息、图片轮播、收藏按钮等。
🔧 技术难点与解决方案
1. 反爬虫应对策略
问题:携程网有较强的反爬虫机制,直接请求容易被封IP。
解决方案:
def _request_with_curl(self, method: str, url: str, headers: Optional[Dict[str, str]] = None,
json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> str:
cmd = [
"curl",
"--noproxy",
"*",
"-sS",
"--max-time",
str(self.timeout),
"-X",
method.upper(),
]
for key, value in (headers or {}).items():
cmd.extend(["-H", f"{key}: {value}"])
if json_data is not None:
cmd.extend(["--data", json.dumps(json_data, ensure_ascii=False)])
cmd.append(url)
completed = subprocess.run(cmd, capture_output=True, text=True, check=True)
return completed.stdout
- 实现了requests和curl双通道请求
- 设置合理的请求延迟
- 模拟真实浏览器请求头
- 使用Session保持连接
2. 数据清洗与标准化
问题:爬取的数据格式不统一,需要进行清洗。
解决方案:
def normalize_text(text: str) -> str:
return re.sub(r"\s+", " ", text or "").strip()
def parse_float(value, default=0):
try:
return float(value) if value and value.strip() else default
except (ValueError, TypeError):
return default
def parse_int(value, default=0):
try:
return int(float(value)) if value and str(value).strip() else default
except (ValueError, TypeError):
return default
- 统一文本格式(去除多余空格)
- 实现了安全的类型转换
- 提供默认值处理
- 异常捕获和容错处理
3. 大数据量查询优化
问题:景点数据较多,查询速度较慢。
解决方案:
# 使用select_related减少查询次数
browse_history = BrowseHistory.objects.filter(user_id=user_id).select_related('scenic_spot')
# 使用聚合函数减少数据传输
avg_rating = scenic_spots.aggregate(avg=Avg('rating'))['avg'] or 0
# 使用分页减少单次查询数据量
paginator = Paginator(filtered_spots, 12)
- 使用select_related和prefetch_related优化关联查询
- 使用聚合函数在数据库层面计算
- 实现分页功能,减少单次查询数据量
- 添加数据库索引
4. 可视化图表性能优化
问题:大量数据渲染图表时性能较差。
解决方案:
// 使用数据采样
var sampledData = data.filter(function(item, index) {
return index % 5 === 0; // 每5个数据点取1个
});
// 使用渐进式渲染
chart.setOption({
series: [{
data: sampledData,
animationDuration: 1000,
animationEasing: 'cubicOut'
}]
});
// 使用Web Worker进行数据处理
- 数据采样减少渲染数据量
- 渐进式渲染提升用户体验
- 使用Web Worker进行后台数据处理
- 懒加载和按需加载图表
5. 用户行为追踪与隐私保护
问题:需要追踪用户行为进行推荐,但要保护用户隐私。
解决方案:
# 匿名化处理
def anonymize_user_data(user):
return {
'user_id': hash(user.id) % 1000000, # 使用哈希值代替真实ID
'behavior_type': 'browse', # 只记录行为类型
'timestamp': timezone.now(), # 记录时间戳
}
# 数据脱敏
def mask_sensitive_info(text):
# 隐藏手机号中间4位
text = re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', text)
# 隐藏邮箱
text = re.sub(r'(\w{2})\w+(@\w+)', r'\1***\2', text)
return text
- 使用哈希值代替真实用户ID
- 只记录必要的行为数据
- 实现数据脱敏功能
- 提供用户数据删除功能
🎯 项目亮点总结
1. 完整的数据采集流程
从爬虫设计、数据清洗、数据存储到数据导入,形成了完整的数据采集链路。
2. 多维度数据可视化
提供了5个可视化分析页面,覆盖景点分布、等级分析、词云、价格、评分等多个维度。
3. 智能推荐算法
实现了基于内容推荐和协同过滤的混合推荐策略,提升用户体验。
4. 用户行为追踪
完整记录用户浏览历史和收藏行为,为个性化推荐提供数据支持。
5. 响应式界面设计
采用Bootstrap框架,实现了美观、易用的用户界面。
6. 高性能优化
通过数据库查询优化、数据采样、懒加载等技术,提升了系统性能。
🔮 未来优化方向
1. 数据扩展
- 增加更多城市的景点数据
- 集成多个旅游平台的数据源
- 添加实时数据更新功能
2. 功能增强
- 增加路线规划功能
- 添加景点对比功能
- 实现用户评论和评分功能
3. 算法优化
- 引入机器学习算法提升推荐准确性
- 实现基于地理位置的推荐
- 添加季节性推荐(如春季赏花、冬季滑雪)
4. 性能优化
- 引入Redis缓存热点数据
- 使用CDN加速静态资源加载
- 实现前后端分离架构
5. 移动端适配
- 开发微信小程序版本
- 优化移动端用户体验
- 添加离线浏览功能
📚 技术学习心得
通过这个项目,我深入学习了以下技术:
- Python爬虫技术:掌握了requests、BeautifulSoup、反爬虫应对策略
- Django框架:熟练使用Django ORM、模板引擎、中间件等
- 数据可视化:学会了使用ECharts进行数据可视化
- 数据库设计:掌握了MySQL数据库设计和优化技巧
- 推荐算法:了解了协同过滤、基于内容的推荐等算法
- 前端开发:提升了HTML、CSS、JavaScript开发能力
🤝 项目开源与交流
本项目已开源,欢迎大家一起交流学习:
如果你对这个项目有任何问题或建议,欢迎在评论区留言,或者通过以下方式联系我:
- 邮箱:2216115746@qq.com
- 微信:下方名片
📝 结语
这个项目从零开始,经历了需求分析、技术选型、系统设计、编码实现、测试优化等多个阶段。通过这个项目,我不仅提升了技术能力,更重要的是学会了如何用技术解决实际问题。
重庆是一座美丽的城市,希望这个系统能帮助更多人了解重庆的旅游资源,发现重庆的美。如果你也对重庆旅游感兴趣,欢迎使用这个系统,也欢迎大家一起完善它。
感谢大家的阅读,如果你觉得这个项目对你有帮助,欢迎点赞、收藏、转发!
版权声明
本文为原创文章,转载请注明出处。项目代码遵循MIT开源协议。
希望这份博客文章对你有帮助!如果你需要调整任何内容或添加更多技术细节,请告诉我。