简介:一套基于Python的景区数据分析与可视化期末大作业源码,主要面向高等院校Python课程设计、数据分析方向的学生,以及希望学习景区数据采集与可视化的小白开发者。项目涵盖数据爬取、清洗、分析与可视化完整流程,可有效解决从数据获取到图表呈现的课程设计要求。压缩包共26个文件,包含9个Python脚本、9个HTML可视化页面、4个XML工程配置、2张PNG结果图及使用说明等,整体大小2.35MB,结构清晰便于对照学习。其中Python脚本覆盖爬虫采集、美食数据处理、词云生成、地图绘制等功能,HTML页面展示了箱线图、饼图、漏斗图等交互式图表。该项目为个人大作业,评审分达95分以上,已经严格调试可直接运行,还配套使用说明和结果图片,便于快速复现与二次开发。已有1048人学习下载,对于期末答辩或课程设计具有较高的参考价值。
1. 景区大作业项目的模块拆分与运行路径
这份基于 Python 的景区数据分析与可视化项目,文件结构里藏着一条完整的数据流链路:spider.py负责从公开旅游平台采集景区基础信息,clear.py做清洗标准化,food.py抓取景区周边美食数据,最后china.py、jiangsu.py、view.py等绘图脚本把处理好的数据落成 ECharts 模板和 Pyecharts 图表。整套项目拿来做期末大作业或者课程设计,能同时覆盖爬虫、数据处理、可视化三个考核点,评审分能到 95 分以上,说明代码的健壮性和输出成果的完整度都经过了验证。你拿到压缩包先别急着跑,建议按spider.py → clear.py → china.py → view.py的顺序读代码,这样能快速理解每个文件在整个分析流程里的位置。项目里 cpie.html、box.html、funnel.html 这些已经是生成好的可视化结果,即便暂时不想碰爬虫,也能直接打开看分析效果。
2. 爬虫与数据解析:景区基础信息的采集全流程
2.1 数据源选型与反爬策略
景区数据的采集对象一般是携程、同程、去哪儿这类 OTA 平台的公开榜单页或 POI 搜索接口。常见做法是用requests库直接 GET 页面,再用BeautifulSoup配合lxml解析 HTML 结构。这个项目里的spider.py走的就是这条路,它需要抓取的核心字段包括:景区名称、所在省份、景区等级(5A/4A/3A)、用户评分、门票价格、地理位置经纬度。注意这类平台通常有简单的反爬机制,必须设置合理的User-Agent和请求间隔。
import requests from bs4 import BeautifulSoup import json import time headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } url = "https://example.com/scenic/list" response = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(response.text, "lxml") items = soup.select("div.scenic-item") data = [] for item in items: name = item.select_one("h3.name").text.strip() score = item.select_one("span.score").text.strip() level = item.select_one("span.level").text.strip() price = item.select_one("span.price").text.strip() data.append({ "name": name, "score": score, "level": level, "price": price }) with open("scenic_raw.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) time.sleep(2)这段代码的逻辑是:先构造带浏览器的请求头,然后请求列表页,用 CSS 选择器定位每一条景区数据的 DOM 节点,提取后写入 JSON 文件。选择器里的h3.name、span.score这些需要根据实际网站的 HTML 结构调整,不同平台类名差异很大。请求间隔time.sleep(2)是必须要有的,直接决定你能否在连续翻页时不被封 IP。如果发现返回的页面验证码,可以在headers里补上Cookie字段,先手动登录一次从浏览器开发者工具里复制。
2.2 经纬度补全与地理编码
项目里view.py和地图类图表需要经纬度坐标,但平台列表页通常不直接提供。常见的做法是调用高德地图或百度地图的地理编码 API,把景区名称转成经纬度。免费版 QPS 限制是每秒 3 次,需要控制调用频率。值得注意的是,部分平台详情页会直接吐经纬度,可以优先尝试解析详情页的 JavaScript 变量,省下 API 配额。
import requests import time AMAP_KEY = "your_amap_web_service_key" def get_lng_lat(name, city=""): params = { "key": AMAP_KEY, "address": name, "city": city, "output": "JSON" } try: resp = requests.get( "https://restapi.amap.com/v3/geocode/geo", params=params, timeout=5 ) geocode = resp.json().get("geocodes") if geocode: location = geocode[0].get("location", "") lng, lat = location.split(",") return float(lng), float(lat) except Exception as e: print(f"[ERROR] {name}: {e}") return None, None这里geocodes返回的是标准地理位置数组,location字段是"经度,纬度"字符串,需要 split 后转 float。注意高德 API 返回的坐标是 GCJ-02 坐标系,如果后续地图组件用的是 WGS-84 坐标(比如一些英文地图库),会有几十到几百米的偏移,要再走一遍坐标转换函数。大作业评审阶段,展示的散点如果稍微偏离真实位置,一般不会扣分,但坐标系不一致导致的图表偏移要心里有数。
2.3 美食数据采集与关联存储
food.py和parsefood.py承担的是景区周边美食数据的获取和解析。这里的思路通常是:先从spider.py拿到的景区列表里取景点名称,再作为关键字去美食平台搜索周边餐饮,提取店名、人均消费、评分、菜系分类。这个模块的分析角度是"景区热度与周边餐饮消费的关系",能增加项目分析维度的丰富度。
parsefood.py的职责是解析嵌套 JSON 结构。美食平台接口返回的数据往往是多层嵌套,比如result.data.poi_list[].ext_info里才有完整信息,直接json.loads后靠多层下标取值容易越界。建议写一个独立的解析函数,对缺失字段用dict.get配合默认值兜底。整个爬虫部分需要注意:必要时把爬取结果存成 Excel 中间文件,方便在clear.py出了问题时不至于重跑全量采集。
3. 数据清洗与标准化:从脏数据到可分析 DataFrame
3.1 缺省值与字段格式的统一处理
爬下来的数据大概率不能直接用。评分的缺失、价格的"免费"字样、经纬度的字符串类型、等级字段的空值,这些都要在clear.py里统一处理。数据清洗是整套项目拿到高分的核心,评审老师主要看你对真实脏数据的处理能力。
import pandas as pd df = pd.read_json("scenic_raw.json") # 价格字段清洗:统一转为数值型,"免费"记为 0 def parse_price(val): if isinstance(val, str): if "免费" in val: return 0.0 val = val.replace("¥", "").replace("元", "").strip() try: return float(val) except ValueError: return None return val df["price"] = df["price"].apply(parse_price) # 评分:超过 5 分的视为异常值,置为空后做均值填充 df.loc[df["score"] < 0, "score"] = None df.loc[df["score"] > 5, "score"] = None df["score"] = df["score"].fillna(df["score"].mean()).round(2) # 省份字段:去掉"省/市"后缀便于地图匹配 df["province"] = df["province"].str.replace("省", "").replace("市", "")清洗的逻辑要把握几个原则:第一,能根据业务规则修正的不要直接删行,比如"免费"转 0,"暂无"转 None;第二,无法修正的缺失值用均值或中位数填充,避免图表出现大段空白;第三,用于地图匹配的字段要统一格式,省、市后缀会影响 Pyecharts 地图的 nameMap 匹配,导致数据无法映射到对应区域。价格字段处理这里,有些数据是"成人票 80 元",字符串里混杂非数值内容,正则表达式re.sub(r"[^\d.]", "", val)是更稳的做法。
3.2 归一化处理与透视表聚合
评分、价格、热度这三个字段量纲不同,做多指标对比前需要归一化。例如把评分从 [0, 5] 映射到 [0, 100] 区间,或者用最小-最大缩放把价格映射到 0~1。归一化之后的字段放在同一个量纲下,才适合放进雷达图或者做综合评分排名。这个小步骤经常被忽略,却直接影响可视化结果的可读性。
df["score_norm"] = df["score"] / 5 * 100 df["price_norm"] = (df["price"] - df["price"].min()) / \ (df["price"].max() - df["price"].min()) df["hot_norm"] = (df["heat"] - df["heat"].min()) / \ (df["heat"].max() - df["heat"].min()) # 综合热度:权重分配后排序 df["composite"] = df["score_norm"] * 0.4 + \ df["price_norm"] * 0.3 + \ df["hot_norm"] * 0.3这段代码里score_norm的算法是线性缩放,适合分布均匀的数据。如果发现评分分布集中在 4~5 分区间,用df["score"].rank(pct=True)做百分位排名比线性缩放更能拉开差距。权重 0.4/0.3/0.3 是按"质量优先、兼顾性价比和热度"的业务逻辑设定的,你可以根据分析主题调整成 0.5/0.2/0.3。最后生成的composite字段能直接作为柱状图排序依据,评审时讲清楚权重设计逻辑,比单纯贴图得分更高。
3.3 多表拼接与地理字段匹配
清洗后的景区数据和美食数据要拼接才能做关联分析,常见的做法是基于景区名称字段做左连接。但由于两个平台命名不一致,"西湖"可能在景区表里叫"西湖风景名胜区",在美食表里叫"西湖景区"。如果直接 merge,大部分行会匹配不上。我一般会在连接前做一步关键字段的归一化:去掉括号备注、去掉"风景名胜区""景区"这类后缀词,再合并。如果仍然存在不匹配,就需要维护一个手工映射字典,把常见别名映射到主键名。
df_scenic["match_key"] = df_scenic["name"].str.replace( "风景名胜区|景区|公园", "", regex=True ) df_food["match_key"] = df_food["poi_name"].str.replace( "风景名胜区|景区|公园", "", regex=True ) df_merged = pd.merge( df_scenic, df_food, on="match_key", how="left", suffixes=("_scenic", "_food") )regex=True是 pandas 的字符串替换开关,配合|符号可以一次匹配多个词。合并后的结果用suffixes参数区分两张表里同名字段,比如score_scenic和score_food,如果重名不设置 suffixes,pandas 会自动加_x、_y,可读性会差一点。还有一点容易被忽略:how="left"保留了左边表的全部记录,如果美食表里同一个景区对应多家餐饮,连接结果会产生多行冗余,此时如果只需要做聚合分析,可以先groupby("match_key").agg({"price": "mean", "score": "mean"})再 merge,数据量小很多。
4. Pyecharts 地图与组合图表的可视化实现
4.1 地图热力图的区域映射逻辑
china.py是整套可视化里最有冲击力的模块。它把各省的景区数量、平均评分、平均门票价格映射到中国地图上,用 Geo 或 Map 类型呈现。Pyecharts 地图的一个关键点是区域名称的匹配,省级地图要求数据里的省份名称与地图内部注册的名称完全一致,比如"广西"要写成"广西壮族自治区"才能正确映射。
from pyecharts import options as opts from pyecharts.charts import Map # province_df: 含 province 和 count 两列 province_data = [ [row["province"], int(row["count"])] for _, row in province_df.iterrows() ] map_chart = ( Map() .add( series_name="景区数量", data_pair=province_data, maptype="china", is_map_symbol_show=False, label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="全国景区数量分布"), visualmap_opts=opts.VisualMapOpts( min_=0, max_=int(province_df["count"].max()), is_piecewise=True, range_color=["#e0f3f8", "#abd9e9", "#74add1", "#4575b4"] ), ) ) map_chart.render("china_map.html")data_pair接收的是[[区域名, 数值]]结构,maptype="china"指定使用中国地图。visualmap_opts里is_piecewise设为 True 会显示图例分段,方便用户看区间;range_color是从浅到深的渐变色系,代表景区数量的递增。如果你的省份数据里包含"香港""澳门",maptype="china"默认是包含的,但要确认 Pyecharts 版本的地图包版本兼容。另外,地图文件是离线加载的,Pyecharts 从 v1.x 开始地图体积较大,运行前要确认pyecharts和pyecharts_snapshot的版本配套,常见问题是老代码用from pyecharts import Map导入报错,新版要改成from pyecharts.charts import Map,这是期末答辩时最容易翻车的地方。
4.2 箱线图与漏斗图的业务解读
box.html和funnel.html代表两种不同维度的分析视角。箱线图展示的是不同省份景区评分的分布形态,能看到中位数、四分位距和离群点,用来回答"哪个省份的景区品质更稳定"这个问题。
from pyecharts.charts import Boxplot province_list = ["安徽", "江苏", "浙江", "江西"] score_lists = [] for prov in province_list: subset = df[df["province"] == prov]["score"].tolist() score_lists.append(subset) box = Boxplot() box.add_xaxis(province_list) box.add_yaxis( "景区评分", box.prepare_data(score_lists), tooltip_opts=opts.TooltipOpts(trigger="item") ) box.render("score_box.html")prepare_data是 Boxplot 内置的静态方法,负责把原始分数列表转换成箱线图五要素。如果某个省份的样本量太少(少于 5 个),画出来的箱线图会缺失须线,这时可以适当合并相邻省份。漏斗图更适合做"旅游决策链路"的转化分析,比如从"浏览景区页 → 查看门票 → 购买门票 → 完成游玩"四个环节的人数递减,用Funnel的sort_="descending"控制数据排序方式,gap参数控制漏斗块之间的间隔,间隔太大会让图形脱节,评审时视觉效果不好。
4.3 词云与饼图的高频词提取
cloud.py和pie.html侧重文本分析和占比展示。词云的数据源是景区描述文本或评论关键词,核心处理在 jieba 分词和高频词统计那一层。注意分词前要加载自定义词典,把"飞来峰""云海"这类景区专有名词预先加入jieba.add_word()里,否则会被切成"飞来""峰"。
饼图在展示景区等级占比(5A、4A、3A)时非常直观。这里有个技巧:如果一个等级占比过低(比如 3A 景区只占 3%),饼图标签会重叠,可以设置label_opts=opts.LabelOpts(formatter="{b}: {d}%")让标签只显示名称和百分比,不显示具体数值。{b}是系列名称,{c}是数值,{d}是百分比占位符,这个 formatter 的写法会直接影响图表的可读性。
5. 期末答辩前的验证方法与扩展技巧
5.1 地图地名匹配的验证对照
答辩演示前,优先检查地图类图表是否有数据漏显示。Pyecharts 地图对地名匹配非常严格,最常见的坑是数据里写"安徽",而地图组件注册的是"安徽省"。可以跑一个快速脚本验证差异:从 Pyecharts 内部取出地图的注册名称,和你的数据做差集。
from pyecharts.datasets import register_url # 检查数据中的省份是否都能匹配到地图名称 import pyecharts.datasets as ds def check_name_consistency(data_provinces, map_name="china"): registed = ds.FILENAMES.get(map_name) # 实际使用时需要遍历地图内部的 name_map 做对照 mismatch = [p for p in data_provinces if p + "省" not in registed and p != "北京"] print(f"不匹配项:{mismatch}")更实际的办法是打开生成的 HTML 文件,用浏览器开发者工具看 console 里是否有data is not defined或对应地区的 value 为 null。如果某一省份空白,直接打开源码搜索省份名,看 JSON 数据里 key 和地图注册名是否一致。最常见的修正方式是做一个翻译字典,把"安徽"映射成"安徽省"、"内蒙古"映射成"内蒙古自治区",这个映射表写死成本很低,但能避免评审现场地图白一块的尴尬。
5.2 静态图表的本地部署与端口服务
render.html和其他 HTML 图表是纯静态页面,用浏览器直接打开file://协议就能运行,但如果涉及异步加载外部 JS 资源,部分浏览器会拦截跨域请求导致图表不显示。建议用 Python 自带模块起一个本地服务:
cd /path/to/project python -m http.server 8080然后浏览器访问http://localhost:8080/render.html。http.server会监听当前目录为根路径,注意所有 HTML 需要放在同一目录下,否则路径引用会 404。如果你要临时把图表嵌入 Jupyter Notebook,用chart.render_notebook()替代chart.render(),可以直接在单元格里输出交互图表,这个技巧在做实验记录时特别实用。render.html如果是手动拼接多个图表的综合页面,需要确认每个子图表的容器div的id与初始化脚本里的getElementById一一对应,常见的白屏问题都出在这里。
5.3 从期末大作业到简历项目的三个扩展方向
这个项目拿 95 分说明完成度已经很高,但如果你想让它在简历上更有竞争力,可以考虑三个低成本扩展。第一,加入时间维度:把爬虫改成支持定时增量采集,记录景区热度随时间的变化,用折线图展示"节假日效应",这能体现你理解数据采集的持续性而非一次性脚本。第二,增加一个简单的推荐逻辑:基于综合评分和价格归一化值,用df.nlargest(10, "composite")输出 Top10 推荐景区列表,并在页面上展示推荐理由文本。第三,把清洗后的数据导出成 CSV 并用 Pandas 做一次描述性统计输出到summary.txt文件,这能在答辩时直接展示你对数据分布的掌握。需要注意,扩展时尽量保持原有文件结构不变,新增模块单独命名,避免破坏已经调试好的主流程。
本文还有配套的精品资源,点击获取