ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python旅游景点数据分析与推荐系统:爬虫清洗到协同过滤完整实战

Python旅游景点数据分析与推荐系统:爬虫清洗到协同过滤完整实战 每年到毕设季总能在各个技术社区看到大量“求一个Python项目源码”的帖子。如果你刷到“Python旅游景点数据分析与推荐系统”这个题目又正好在为选题发愁那这篇文章应该能帮你省下很多瞎折腾的时间。这个题目我实际跟过几轮从爬虫取数、数据清洗到最终的协同过滤推荐每一环都和课程里学的东西对得上但又不至于简单到被导师质疑工作量。这个项目解决的核心问题很直接把散落在各个旅游网站上的景点评分、评论、票价、地理位置这些半结构化数据用爬虫抓下来清洗成能分析的结构化表格然后用可视化和推荐算法把这些数据变成有意义的信息——比如帮用户找到“评分高但票价低”的冷门景点或者根据一个用户浏览过的景点推测他可能喜欢哪些同类目的地。整套流程覆盖了Python数据分析岗位日常会碰到的核心环节也是计算机专业、数据科学专业毕设题库里的常客。不管你是打算直接把它作为毕业设计还是单纯想通过一个完整项目把Python爬虫、Pandas、Flask、协同过滤这些知识点串起来这个题目都是一个性价比很高的练手对象。下面我从头到尾把这个项目应该怎么设计、每一步有哪些坑、源码拿到之后怎么落地拆开讲清楚。1. 项目整体设计与技术选型先想清楚再动手1.1 三个核心模块的职责边界拿到这个题目第一件事不是写代码而是拆功能。一个完整的旅游景点数据分析与推荐系统按我自己的习惯会分成三个边界清晰的模块数据采集层、数据分析层、推荐展示层。数据采集层的职责是解决“数据从哪来”的问题。常见的目标网站包括去哪儿、携程、马蜂窝的景点频道这些页面结构相对规整景点名称、星级、评分、点评数、门票价格、所在城市这些字段基本都在固定的HTML节点里适合用requests加BeautifulSoup或者XPath去解析。这一层产出的是一张原始的CSV或者Excel表字段是乱的数据是有缺失的但没关系下一层会处理。数据分析层是整个项目的工作量担当。数据拿到手之后需要做去重、缺失值填充、价格区间划分、评分数量的分布统计这些操作。用Pandas清洗完之后再用Matplotlib或者PyECharts生成几组可视化图表。图表不是装饰品它们要能回答具体问题——比如“热门城市的景点评分普遍比冷门城市高吗”或者“票价和评分之间到底有没有相关性”这些分析结论会直接写进论文的需求分析章节。推荐展示层是这个系统看起来“智能”的关键。后端用Flask写几个接口接收用户传来的一个景点ID或者一组浏览记录通过推荐算法计算出相似景点或者相似用户返回一个Top N推荐列表最后渲染到前端页面上。这一层不需要做得很重但一定要能做演示——打开页面、输入一个景点、看到推荐列表出来这个流程能完整跑通答辩就成功了一半。1.2 技术栈选型默认方案背后的理由我先给出一套稳妥的默认技术组合再解释为什么这么选编程语言Python 3.8以上爬虫requests BeautifulSoup4或者lxml配合XPath数据分析Pandas NumPy可视化Matplotlib PyEChartsWeb框架Flask 2.x数据库MySQL 5.7以上也可以直接用Pandas读写CSV看导师要求推荐算法基于物品的协同过滤Item-Based Collaborative Filtering这套组合不是随便拼的。爬虫用requests加BeautifulSoup是因为这两个库的文档最全、报错最容易搜到对新手非常友好。XPath的text()函数可以用来精准提取某个节点下的纯文本内容在清洗标签混杂的HTML时尤其管用这个后面会细说。数据分析用Pandas几乎是这个领域的标配没有悬念。Web框架选Flask而不是Django是因为这个项目只有一个“输入景点、输出推荐”的核心交互Flask的路由和模板渲染机制足够简单没有Django那种大而全的项目约束部署的时候也轻量得多。推荐算法选“基于物品的协同过滤”而不是“基于用户的协同过滤”理由很实在本项目的用户数据基本靠模拟或者本地采集量级很小。基于物品的协同过滤只需要计算景点之间的相似度矩阵不依赖大量用户行为冷启动问题也容易绕过去。更关键的是物品相似度矩阵可以离线算好存进数据库用户请求时直接查表返回响应速度比实时计算快很多演示效果自然更流畅。1.3 数据表设计与项目目录结构数据库设计不要贪多三张表足够景点基本信息表、用户行为表、推荐结果缓存表。景点基本信息表的字段建议这样设计字段名类型说明idINT景点唯一标识nameVARCHAR景点名称cityVARCHAR所在城市scoreFLOAT评分保留一位小数comment_countINT点评数量priceFLOAT门票价格0表示免费rankVARCHAR榜单排名或热度等级categoryVARCHAR景点类型如自然风光、历史古迹用户行为表用来记录用户对景点的交互字段包括user_id、spot_id、rating和timestamp。如果你不想引入MySQL直接把这张表存成CSV文件也能用但用MySQL能让答辩时被问数据库设计时有东西可说。项目的目录结构我建议这样组织travel_recommend/ ├── app.py # Flask主程序路由入口 ├── spider/ │ ├── crawler.py # 爬虫核心逻辑 │ └── parser.py # 页面解析函数 ├── analysis/ │ ├── clean.py # 数据清洗脚本 │ └── visualize.py # 可视化图表生成脚本 ├── recommend/ │ ├── similarity.py # 相似度计算模块 │ └── recommender.py # 推荐逻辑封装 ├── data/ │ ├── raw_spots.csv # 爬虫原始数据 │ └── clean_spots.csv # 清洗后的数据 ├── templates/ │ └── index.html # 前端演示页面 └── static/ └── charts/ # 生成的图表文件这个结构把每个步骤分得清楚论文里写系统设计章节时可以直接对照着画架构图。2. 爬虫模块实现景点数据装箱入门2.1 目标网站分析与爬取策略实操第一步先打开目标网站用浏览器的开发者工具F12观察页面结构。以我常用的景点频道页面为例景点列表通常在一个ul标签内每个景点对应一个li节点名称在h3或a标签的文本中评分在span标签内。看清楚了再写选择器切忌边猜边写。爬取策略上我会做两层第一层爬列表页拿到所有景点详情页的URL第二层爬详情页解析出评分、点评数、价格等字段。这样做的好处是代码解耦如果列表页结构变了只需要修改第一层的解析函数详情页逻辑完全不用动。爬虫代码的核心骨架大致是这样的import requests from bs4 import BeautifulSoup import time import random headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 } def fetch_page(url): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) return None def parse_list_page(html): soup BeautifulSoup(html, lxml) spot_items soup.select(ul.spot-list li) detail_urls [] for item in spot_items: a_tag item.select_one(a.spot-title) if a_tag and a_tag.get(href): detail_urls.append(a_tag[href]) return detail_urls def parse_detail_page(html): soup BeautifulSoup(html, lxml) name soup.select_one(h1.spot-name).get_text(stripTrue) score_elem soup.select_one(span.score) score float(score_elem.get_text(stripTrue)) if score_elem else None return {name: name, score: score}我特意在处理评分时加了一个判断因为有些景点没有评论或者评分展示方式不同select_one会返回None直接调用get_text()就会报AttributeError。这种问题在真实爬虫里极其常见养成判空的习惯能少踩很多坑。2.2 XPath的text()函数与页面解析优化很多同学在爬虫课程里听说过XPath但实际写起来总喜欢全用BeautifulSoup。事实上如果要提取的文本在一个混合了标签和文字的长节点里XPath的text()函数会省很多事。举个例子一个景点标签可能长这样div classtag-box 热度span classhot高/span 2024热门榜单第3名 /div用BeautifulSoup拿全部文本需要遍历div.contents再拼接字符串。用XPath就三行from lxml import etree tree etree.HTML(html) texts tree.xpath(//div[classtag-box]/text()) full_text .join([t.strip() for t in texts if t.strip()])这里/text()精准取到的是div下的直接文本节点不会把span里的内容混进来。如果你的页面解析需求复杂比如要忽略某些子标签只取外部文本XPath的text()几乎是唯一优雅的解法。这对应到热搜词里的“python xpath爬虫 text函数”说明这是个高频痛点提前掌握它解析页面的效率能提升一个档次。爬虫合规方面多说两句毕设项目只采集公开页面数据用于学习研究没问题但不要尝试绕过登录、验证码或者突破反爬机制也不要对目标站点造成压力。我在代码里每次都随机休眠3到5秒既是为了降低被封概率也是基本的文明访问。用户代理User-Agent要设成真实浏览器的样子这是最基础的反反爬手段。2.3 爬虫模块的高频异常与处理思路爬虫跑起来之后最常见的异常有三个请求超时、编码乱码、动态加载内容拿不到。请求超时好解决给requests.get()加一个timeout参数配合try-except捕获异常并重试。我习惯写一个简单的重试装饰器最多重试3次每次间隔递增。编码乱码的根源是网站返回的编码和requests猜测的编码不一致。多数情况下用resp.apparent_encoding能解决问题但如果网页本身声明了charsetutf-8而实际返回gbk就会出乱码。稳妥的做法是在拿到响应后先检查resp.encoding再配合resp.apparent_encoding做双重判断。动态加载内容是这个项目爬虫最大的坎。部分网站的景点评分和点评数是Ajax异步加载的列表页HTML源码里根本没有这些数字。遇到这种情况不要头铁去硬抠HTML打开开发者工具的Network面板找到返回JSON数据的接口直接爬接口。JSON解析比HTML解析稳定得多数据结构清晰字段命名规范而且接口通常不会频繁变动。3. 数据分析与可视化让数据开口说话3.1 数据清洗的关键环节爬虫跑完手上是一张可能有几百行、字段参差不齐的原始表。这时候不要急着可视化先做清洗。清洗环节有四个固定动作去重、处理缺失值、统一格式、异常值过滤。去重要小心不能只看景点名称因为同一景点在不同页面可能出现别名。我的做法是用“城市名称”拼接后去重或者用景点详情页URL作为唯一标识后者更可靠。缺失值处理要分字段讨论。评分缺失的景点我倾向于直接删除因为评分是后续推荐和可视化分析的核心字段造假的填充值没有意义。票价缺失就灵活一些如果是免费景点但字段没填可以填0如果是景区没公布票价用同城市同类型景点的平均票价填充也能解释得通。格式统一最典型的坑是价格字段。爬下来可能是字符串“¥120起”、“免费”、“暂无报价”分析之前必须统一转成数字免费转成0“暂无报价”转成NaN“¥120起”用正则去掉非数字字符。这一步处理不好后面画图时会出现各种类型报错。异常值过滤我习惯用一个简单粗暴的规则评分不在0到5之间的记录直接删价格大于1000的先标记出来人工确认。风景区门票上千的不是没有但多数是索道加门票的套票这种异常值不排除会严重拉偏均价统计。3.2 分析维度的选择与图表落地项目做分析不是为了凑图表数量每个图都应该对应论文里的一句结论。我常用的四个分析维度是城市景点数量分布、景点评分分布直方图、票价区间占比饼图、评分与点评数的关系散点图。城市景点数量分布用柱状图即可X轴是城市Y轴是景点数量。这张图配合“数据来源和采样范围”章节使用让导师一眼看出你的数据覆盖了哪些地区。评分分布直方图能反映景点的整体质量水平。实际爬下来的数据通常是偏态分布大量景点集中在4分上下3分以下和4.9分以上都较少。这个现象本身就可以作为分析结论热门景点的评分普遍虚高用户打分存在“幸存者偏差”。票价区间占比饼图按0元、0到50元、50到200元、200元以上四档划分。这张图最容易产出有价值的结论比如“免费景点的平均评分是否显著高于收费景点”这样的交叉分析写进论文会非常加分。评分与点评数的散点图可以配合相关性计算用Pandas的corr()方法算一下两者间的相关系数。通常结果会是弱正相关点评数多说明浏览量大但评分高低和量大不大没有必然关系。有数据支撑的观点答辩时怎么被追问都不虚。可视化代码用Matplotlib的面向对象接口写先创建figure和axes对象再绘图这样方便调整尺寸和保存import matplotlib.pyplot as plt import pandas as pd plt.rcParams[font.sans-serif] [SimHei] # 解决中文字体问题 plt.rcParams[axes.unicode_minus] False # 解决负号显示问题 df pd.read_csv(data/clean_spots.csv) city_counts df[city].value_counts() fig, ax plt.subplots(figsize(10, 6)) city_counts.head(15).plot(kindbar, axax, color#4C72B0) ax.set_title(景点数量Top15城市分布) ax.set_xlabel(城市) ax.set_ylabel(景点数量) plt.tight_layout() plt.savefig(static/charts/city_distribution.png, dpi150)中文字体问题几乎是必踩的坑Windows下用SimHei有效如果部署到Linux服务器改成Noto Sans CJK SC或者直接上传中文字体文件到服务器。我在演示录像里特意录过这个问题的两种处理方式建议代码里用动态检测的方式选字体而不是写死。这一层做完论文里的“数据分析结果”章节就有着落了。最好再加一张清洗前后数据量对比的表格清洗了多少条、删了多少重复、补了多少缺失值这些数字就是工作量的证明。4. 推荐系统核心让代码拥有“智能”4.1 基于物品的协同过滤实现逻辑推荐系统是整个项目中技术含量最高的部分也是答辩时老师最可能深挖的地方。项目背景决定了我们拿不到真实的用户评分矩阵所以我把“用户”概念简化成“一种浏览场景”——比如一个用户看了“故宫”却没看“颐和园”系统要判断这两个景点是否相似相似的话就把颐和园推荐出来。实现分三步走构建用户-物品评分矩阵、计算物品间相似度、生成推荐列表。评分矩阵用Pandas的pivot_table生成行列分别是用户ID和景点ID值为评分。没有真实用户数据时可以用模拟数据填充——给几十个虚拟用户随机分配几个浏览过的景点和打分这个做法的合理性要在论文里讲清楚系统重点是验证推荐算法的流程而非用户数据的真实分布。相似度计算采用余弦相似度这是协同过滤最经典的度量方式。公式是两个物品的评分向量夹角的余弦值值越接近1说明越相似。之所以用余弦而不是欧氏距离是因为评分数据存在“用户评分尺度不同”的问题——有人习惯打4分有人习惯打5分余弦相似度对绝对数值不敏感更关注向量方向天然规避了尺度偏差。核心计算代码长这样import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_similarity_matrix(ratings_df): # 构造用户-物品透视表缺失值填0 user_item_matrix ratings_df.pivot_table( indexuser_id, columnsspot_id, valuesrating ).fillna(0) # 计算物品之间的余弦相似度 item_similarity cosine_similarity(user_item_matrix.T) item_similarity_df pd.DataFrame( item_similarity, indexuser_item_matrix.columns, columnsuser_item_matrix.columns, ) return item_similarity_df similarity_df build_similarity_matrix(ratings_df)计算得到相似度矩阵后推荐逻辑就简单了用户浏览了景点A查矩阵中与A最相似的N个景点过滤掉用户已经去过的按相似度从高到低排序返回前5个。整个查询过程是纯内存级别的操作响应速度毫秒级演示的时候体验很好。4.2 冷启动问题怎么处理更聪明协同过滤推荐系统一个老生常谈的弱点是冷启动新用户没有行为数据新景点没有被任何人评价推荐系统就会失灵。毕设答辩时老师十有八九会问这个问题提前想好对策很重要。我的处理方案简单直接当用户没有历史行为或者相似度矩阵为空时系统降级为“基于规则的推荐”返回评分最高的前5个景点或者在当前城市内按热度排序返回Top5。这个策略在推荐系统领域叫“Popularity-Based Recommendation”虽然不是新东西但作为一个兜底方案逻辑自洽且实现成本低。关键是要把这个降级逻辑写在文档里明确说明“当数据不足时系统自动启用热榜推荐保证在任何输入下都有结果返回”。这个设计细节体现了你对真实系统稳定性问题的思考比只会跑通主流程的代码要成熟得多。4.3 Flask接口设计与前后端联动推荐算法做完要把它包成Web接口。用Flask写两个核心路由一个渲染首页并展示数据集概况一个接收景点ID并返回推荐列表。from flask import Flask, render_template, request, jsonify import pandas as pd app Flask(__name__) # 加载清洗后的数据 spots_df pd.read_csv(data/clean_spots.csv) similarity_df load_similarity_matrix(data/similarity_matrix.csv) app.route(/) def index(): top_spots spots_df.nlargest(10, score)[[name, city, score, price]] return render_template(index.html, spotstop_spots.to_dict(orientrecords)) app.route(/recommend, methods[POST]) def recommend(): spot_id int(request.json.get(spot_id)) similar_spots get_top_n_similar(spot_id, similarity_df, top_n5) return jsonify({recommendations: similar_spots}) if __name__ __main__: app.run(debugTrue, port5000)前端页面不用做得很复杂一个下拉框选择景点一个按钮触发请求下面展示推荐结果卡片就行。建议用Bootstrap把布局做干净一点毕竟演示录像里视觉效果也是评分的一部分。记住要提前写好静态资源路径别让CSS和图片404。5. 源码获取、环境配置与落地避坑手册5.1 免费源码应该怎么挑怎么用标题里写着“免费领源码演示录像”这类资源在CSDN、GitHub、一些毕设资源分享平台都能找到。但免费源码的质量参差不齐拿过来不能直接就用至少要检查三件事。先检查依赖清单。一篇靠谱的源码通常会附带requirements.txt里面列了所有第三方库的版本号。没有这个文件的话源码大概率是从别的项目东拼西凑的运行起来会处处报错。再检查数据文件是否完整推荐系统项目必须要有数据文件不管是CSV还是MySQL导出脚本如果没有数据文件系统跑起来就是空的。最后检查Python版本兼容性很多老代码是用Python 2或者Python 3.5写的里面的print语句、iteritems这些写法在Python 3.8以上都会直接报错。先跑一个最简单的app.py文件探路比什么都稳。5.2 环境配置的三大高频翻车现场这个项目的环境配置说难不难但翻车的姿势五花八门。我按遇到的频率排序讲三个最常见的。第一个是第三方库安装失败。pandas、numpy这些库在Windows下一般能直接pip install成功但lxml这种带C扩展的库偶尔会编译报错。解决办法是去https://pypi.org/project/lxml/下载对应Python版本的预编译whl文件再用pip install 文件名.whl安装。换个更快的方式是装Anaconda直接用conda装库conda对二进制依赖的处理比pip省心得多。第二个是MySQL连接报错。源码里数据库配置的密码是写死的换了环境密码不同就会报Access denied。这个好解决改配置文件的密码即可。更隐蔽的问题是MySQL 8.0换了默认认证插件旧代码用的mysql_native_password可能不被支持需要在MySQL里执行一条命令把用户的认证插件改回去。第三个是中文字体显示成方块。这个问题在数据可视化环节说过服务器上尤其严重。Linux服务器没有SimHei字体Matplotlib画图时所有中文都变成小方块。解决方法是下载一个开源中文黑体字体放入Matplotlib的字体目录并重建缓存。这个细节我在演示录像里专门花了几分钟讲因为九十多个学生的项目都栽在这一处。5.3 拿到源码后的正确复盘顺序一旦确认源码能跑通不要急着改代码。先把项目跑一遍用演示录像作为对照确认每个功能点的实际效果。然后按“爬虫→清洗→分析→推荐”的顺序把源码从头到尾看一遍遇到看不懂的地方打个断点跑几步。推荐系统核心代码必须看懂因为答辩时关于推荐算法的问题是最容易被追问的答辩老师可能会问“物品相似度矩阵存在哪里”“如果新景点加入系统怎么及时更新”等等。带着理解复制过来的代码和完全不懂盲交上去的代码答辩表现差距会非常明显。还有一个容易被忽视的地方看源码里的图表结论是否和数据分析结果一致。部分免费源码的分析图表是网上随便下载的数据对不上一旦答辩时被要求放大看细节或者被问到“你的数据来源是哪里图表如何生成的”回答不上来会很尴尬。源码里的图表最好自己用真实数据重新生成一遍。6. 常见问题速查与答辩前的最后准备6.1 高频报错与排查手段对照表报错信息出现环节根因分析排查思路ModuleNotFoundError: No module named bs4环境配置缺少BeautifulSoup4库执行pip install beautifulsoup4检查是否安装到正确环境AttributeError: NoneType object has no attribute get_text爬虫解析页面结构变化或字段缺失打印HTML源码定位节点判空后再取值KeyError: 评分数据分析读取的CSV文件路径错误检查文件编码为UTF-8-sig用df.head()确认列名ValueError: cannot convert float NaN to integer数据清洗票价含非数字字符串用pd.to_numeric(errorscoerce)强制转换OperationalError: (1045, Access denied)Flask连接数据库MySQL账号密码错误检查配置文件的host、port、user、password是否匹配RuntimeError: Click will abort further executionFlask启动端口被占用改app.run(port5001)或先杀掉占用进程Font family [SimHei] not found数据可视化系统缺少中文字体下载中文字体并配置Matplotlib资源路径这些不是全部问题但覆盖了绝大多数学生的翻车点。遇到没见过的报错先把英文报错复制到搜索引擎查多数情况都会有人踩过。调试的时候记得多用print或者logging输出中间结果不要猜哪里出错。6.2 答辩前的核心功能演示脚本答辩时间通常只有10到15分钟演示流程要设计得像一个产品发布而不是现场写代码。我建议按下面这个顺序走第一步展示数据库中的数据量。先打开数据库管理工具让评委看到景点表里有多少条记录、用户行为表长什么样。这一步是证明数据真实采集成为了多少量级的工作。第二步展示数据可视化图表。选两到三张最有代表性的图比如城市分布柱状图和评分分布直方图简明扼要说清楚图表的结论不要贪多。第三步进入Web页面执行一次推荐操作。选择一个景点演示推荐结果返回并随口解释推荐结果背后的逻辑——比如“因为用户看过故宫评分向量相似度最高的是颐和园所以系统把它排在第一位”。第四步如果被追问冷启动问题解释规则兜底机制。推荐逻辑链完整问不倒这个项目基本就稳了。还有一个小技巧把演示录像存一份在U盘里有些答辩教室的网络环境不稳定如果网页打不开直接播放提前录好的演示视频既不慌乱也不冷场。结尾最后一个建议这个项目从爬虫到推荐覆盖的技术链条非常完整认真做一遍相当于把数据分析方向的核心技能串联起来了。我个人觉得最有价值的环节不是推荐算法本身而是数据清洗——真实数据永远比教材里的demo数据脏十倍处理这些脏数据的经验在学校里很难学到但工作里天天都要用。如果时间有限我的建议是优先保数据分析和可视化这是工作量最容易量化的部分推荐系统可以只做基于物品的协同过滤加规则兜底不要在算法新颖性上恋战。源码拿到之后一定花时间把数据清洗脚本和推荐核心代码吃透能改动一两个小功能最好——比如把推荐结果的展示从列表改成卡片式或者增加一个“只看免费景点”的筛选条件这些改动不大但答辩时能体现出你的理解深度。最后再提醒一句爬虫程序跑起来之后要控制频率别给目标站点添麻烦。数据抓取到能支撑论文分析的量就够了多抓的边际价值很低风险反而高。做好技术也守住分寸这个项目就能稳稳落地。
返回列表