重庆市旅游景点数据可视化分析系统

3 阅读21分钟

🏔️ 重庆市旅游景点数据可视化分析系统 - 从数据爬取到智能推荐的完整实现

作为一名热爱旅游的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路由配置)

核心功能模块

  1. 数据爬取模块:爬取携程网重庆景点数据
  2. 数据存储模块:使用MySQL存储结构化数据
  3. 数据可视化模块:多维度图表展示
  4. 用户管理模块:用户认证和权限管理
  5. 智能推荐模块:基于用户行为的推荐算法

🕷️ 数据爬取模块实现

爬虫设计思路

爬虫模块的核心是从携程网获取重庆旅游景点的详细信息,包括景点名称、等级、评分、评论数、门票价格、开放时间等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
封面图片链接主图URLdimg04.c-ctrip.com/images/...
详情链接详情页URLyou.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. 景点分布:按行政区、等级、热度、门票价格分布
  2. 等级分析:不同等级景点的评分、价格、热度对比
  3. 词云汇总:景点标签词云展示
  4. 价格分析:门票价格区间分布和趋势
  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]

推荐策略

  1. 基于热门度的推荐:推荐高热度、高评分的景点
  2. 基于内容的推荐:根据用户浏览历史,推荐具有相似标签的景点
  3. 协同过滤推荐:基于用户行为模式推荐相似用户喜欢的景点
  4. 混合推荐:结合多种策略,提高推荐准确性

[截图位置12:个性化推荐页面] 展示为用户推荐的景点列表,包含推荐理由。 在这里插入图片描述

🎨 前端界面设计

页面布局

系统采用了响应式设计,主要特点:

  1. 侧边栏导航:固定左侧,包含所有功能模块
  2. 顶部导航栏:显示用户信息和快捷操作
  3. 主内容区:动态加载不同页面内容
  4. 卡片式设计:景点信息以卡片形式展示

首页设计

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. 移动端适配

  • 开发微信小程序版本
  • 优化移动端用户体验
  • 添加离线浏览功能

📚 技术学习心得

通过这个项目,我深入学习了以下技术:

  1. Python爬虫技术:掌握了requests、BeautifulSoup、反爬虫应对策略
  2. Django框架:熟练使用Django ORM、模板引擎、中间件等
  3. 数据可视化:学会了使用ECharts进行数据可视化
  4. 数据库设计:掌握了MySQL数据库设计和优化技巧
  5. 推荐算法:了解了协同过滤、基于内容的推荐等算法
  6. 前端开发:提升了HTML、CSS、JavaScript开发能力

🤝 项目开源与交流

本项目已开源,欢迎大家一起交流学习:

如果你对这个项目有任何问题或建议,欢迎在评论区留言,或者通过以下方式联系我:

📝 结语

这个项目从零开始,经历了需求分析、技术选型、系统设计、编码实现、测试优化等多个阶段。通过这个项目,我不仅提升了技术能力,更重要的是学会了如何用技术解决实际问题。

重庆是一座美丽的城市,希望这个系统能帮助更多人了解重庆的旅游资源,发现重庆的美。如果你也对重庆旅游感兴趣,欢迎使用这个系统,也欢迎大家一起完善它。

感谢大家的阅读,如果你觉得这个项目对你有帮助,欢迎点赞、收藏、转发!


版权声明

本文为原创文章,转载请注明出处。项目代码遵循MIT开源协议。


希望这份博客文章对你有帮助!如果你需要调整任何内容或添加更多技术细节,请告诉我。