资讯详情

基于Python+Flask的旅游数据爬虫与可视化分析实践

📅 2026/9/23 11:01:35 | 华诺云谱 👁 阅读
基于Python+Flask的旅游数据爬虫与可视化分析实践
1. 项目发起的真实动机与技术选型拆解1.1 这个项目到底在解决什么问题先把这个标题拆开看——PythonFlask爬虫大同旅游产业数据可视化分析后缀还挂着一长串_o4skhwx7-vue pycharm django。这其实是一个非常典型的旅游数据采集与分析展示项目在高校的毕业设计、课程实践以及个人作品集里出现频率很高。核心任务可以归纳成一句话把网络上分散的、非结构化的大同旅游相关数据通过爬虫抓下来存进数据库再做清洗和统计分析最后用一个可视化的网页界面把分析结果展示给用户。大同这个选题并非随便挑的。作为中国首批历史文化名城之一大同拥有云冈石窟、悬空寺、华严寺等一批高知名度景点旅游产业数据游客量、门票价格、景点热度、住宿情况、交通信息等在网上有着相当丰富的分布。拿它做分析对象数据源好找、业务脉络清晰、可视化维度的可挖掘空间也大。那标题里的技术栈为什么要这样搭配Python Flask Django Vue PyCharm乍一看似乎有点重复造轮子——Flask和Django都是Python Web框架为什么同时出现这背后其实有一个很实际的分工逻辑爬虫层用Python的requestsBeautifulSoup或Scrapy完成数据采集后端服务层Flask负责提供轻量的数据分析查询接口因为它灵活、起步快适合做聚合统计和API输出Web框架层Django负责整体站点框架、数据库模型和后台管理因为它的MTV模式在管理数据模型时比Flask省事得多前端展示层Vue负责页面交互与图表渲染配合ECharts做可视化开发工具PyCharm作为主力IDE调试爬虫和Flask接口确实比VS Code在某些场景下更顺手。提示如果你的项目周期比较紧只想快速出一个能跑的Demo可以不用DjangoFlask全栈Flask Jinja2模板 原生HTML就能解决。但如果你希望项目看起来很完整能展示后台管理、数据库迁移、用户权限这些能力那把Django加进来会让整个系统的层次感完全不同。1.2 每个技术组件在项目里的实际分工很多人在做类似项目时会陷入一个误区为了用某个技术而用某个技术。比如看了几篇教程觉得Django很火不管项目是否需要硬把它塞进来。我个人的建议是先把每个组件需要承担的职责画清楚再决定要不要引入。这个项目里各环节的配合是这样的组件职责定位为什么是它Python requests数据采集层语法简单生态完善爬虫入门首选BeautifulSoup / ScrapyHTML解析与爬虫框架BS4轻量适合中小型抓取Scrapy适合管理大量爬虫任务SQLite / MySQL数据存储前期用SQLite起步快数据量大后切MySQLFlask轻量API接口写聚合查询、统计接口非常快几行代码就能出数据Django完整Web站点与后台MTV模式自带Admin后台管理爬虫抓下来的数据非常方便Vue ECharts前端页面与可视化Vue组件化开发效率高ECharts图表展示效果出彩PyCharm开发调试环境对Flask/Django的Debug支持完善爬虫断点调试体验好这个组合的真正优势在于每一层都有明确边界。爬虫不用关心页面长什么样Flask不用管数据库表结构怎么迁移Django只管后台和模型Vue只管消费接口数据。出现问题时顺着调用链去排查非常清晰——这也是面试官、答辩评委最看重的工程化意识。2. 爬虫采集层设计与大同旅游数据源处理2.1 数据源调研与采集范围界定爬虫的第一步不是写代码而是先搞清楚你究竟要哪些数据、去哪拿、拿到之后能分析出什么。这一步做扎实了后面所有环节都会顺。我在做大同旅游数据采集时把数据源分成了这么几个类别景区基础信息景点名称、所在区县、门票价格、开放时间、评级5A/4A、简介。来源通常是百度百科、大同文旅局官网、各大旅游平台景点页。游客评论数据用户在旅游平台上的评分、评论内容、游玩日期。这一部分最有分析价值——可以做情感分析、热度趋势、口碑排行。交通与住宿数据大同站/大同南站的车次信息、云冈机场航班情况、主要区域酒店价格区间。搜索热度数据通过百度指数或一些公开的指数平台获取关键词如云冈石窟悬空寺的搜索趋势。我建议你不要一开始就贪多先把景区基本信息和游客评论这两块做扎实因为它们直接支撑游客量趋势景点热度排行口碑分析这些最能出图的分析维度。2.2 爬虫代码结构requests BeautifulSoup解析流程拿携程或去哪儿的大同景点评论页来说它的评论区数据部分在页面源码里部分通过异步接口返回。我从实际经验出发整理了两种场景下的处理方式场景一数据直接在HTML静态部分这种最简单requests拿到页面后用BeautifulSoup解析即可。核心代码结构如下import requests from bs4 import BeautifulSoup def fetch_static_page(url, headers): resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 if resp.status_code 200: soup BeautifulSoup(resp.text, html.parser) spot_name soup.select_one(.spot-title).get_text(stripTrue) rating soup.select_one(.rating-value).get_text(stripTrue) return { name: spot_name, rating: rating } return None这里有个容易踩的坑编码问题。很多旅游站点返回的HTML声明的是gbk或gb2312你直接用resp.text解析会满屏乱码。建议先用resp.apparent_encoding探测或者干脆在请求头里带Accept-Encoding: gzip让服务器返回压缩数据再做相应解压。场景二数据通过XHR异步接口加载旅游大平台的评论、价格数据大概率是异步加载的。你打开开发者工具切到Network标签刷新页面找XHR里的JSON请求复制它的请求URL和参数。这个用模拟请求的方式采集更高效import requests import json def fetch_comment_api(spot_id, page1, page_size20): url https://example.com/api/review/list params { poiId: spot_id, page: page, pageSize: page_size } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://example.com/ } resp requests.get(url, paramsparams, headersheaders) if resp.status_code 200: data resp.json()[data][list] return data return []这种接口式的采集重点是先手动构造一次请求确认返回的JSON字段含义再决定提取哪些字段。不需要把所有字段都存下来存你分析用得上的那些就够。2.3 反爬应对与请求伪装做旅游数据爬虫高频请求几乎必然会触发反爬机制真实工作里是绕不开的。这部分的处理原则是先君子后小人。先做好基础伪装随机User-Agent池不要一个UA打天下每次请求之间加time.sleep()频率控制在合理范围1~3秒带上Referer字段让请求看起来是从页面内触发的用requests.Session()维持会话状态。import random import time user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ] def make_request(url, sessionNone): s session or requests.Session() s.headers.update({ User-Agent: random.choice(user_agents), Referer: https://example.com/ }) time.sleep(random.uniform(1, 3)) resp s.get(url, timeout10) return resp这些基础伪装做足之后绝大多数静态页面的采集就不再是问题。如果遇到验证码、登录墙这类的强限制应当选择尊重站点规则保存好已采集的数据换数据源或者做手工采集弥补。写爬虫的前提是清楚边界在哪里哪些数据可以拿、拿到什么程度合适保持克制才能让项目走得更远。提示爬虫采集务必尊重目标网站的robots协议、服务条款和数据使用规范。本项目的数据仅用于学术研究、个人学习与旅游产业分析的展示公开演示时建议只展示统计聚合结果不批量展示原始评论数据和个人信息。2.4 数据字段设计为可视化预留维度爬虫写得好不好不只看能不能抓到数据还要看抓下来的数据能不能被下游分析直接使用。我的建议是在建表和字段设计阶段就考虑好可视化需要什么样的维度梳理出下面这样一张表字段名含义示例值可视化用途spot_name景点名称云冈石窟图表分类轴的柱子district所在区县云冈区地图/饼图区域统计rating_score评分5分制4.8评分对比雷达图comment_count评论数量8732热度排行条形图ticket_price门票价格120价格散点分析visit_date游玩日期2024-08-15游客量时间趋势线comment_content评论内容震撼值得一看文本情感分析词云travel_type出行类型亲子/情侣/朋友游客画像占比饼图这里有几个设计心得很值得分享时间字段一定要存标准格式YYYY-MM-DD不然后面做趋势分析时各种花式日期格式会把你折磨疯。价格字段用数值类型别把120元¥120120.00这种带单位的字符串存进去清洗阶段就该统一。区县字段值得单独存一列虽然可以从景点名推导但独立存储会让后续筛选和地图展示省掉大量代码。3. 数据存储与清洗SQLite起步、MySQL上线的演进路径3.1 存储方案选型小步快跑预留升级空间爬虫抓下来的数据应该放哪里对这个项目来说我推荐分阶段选择第一阶段SQLiteSQLite是一个文件型数据库一个.db文件就搞定所有表零配置文件Python标准库sqlite3直接就能操作。对爬虫开发阶段来说优势是巨大的——不需要装服务、不需要配账号、随时查看表结构。它的写入性能对几千到几万条数据完全撑得住。import sqlite3 conn sqlite3.connect(datong_travel.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS spots ( id INTEGER PRIMARY KEY AUTOINCREMENT, spot_name TEXT NOT NULL, district TEXT, rating_score REAL, comment_count INTEGER, ticket_price REAL, visit_date TEXT, comment_content TEXT, travel_type TEXT ) ) conn.commit() conn.close()第二阶段MySQL当数据量到了一定规模比如评论数据破万条或者你打算把项目部署上线给多人访问就该切到MySQL了。MySQL的优势在于并发读写稳定性、索引查询效率、以及和Django ORM的天然亲和度。从SQLite迁移到MySQL的稳妥思路是先写一个导出脚本把SQLite的表数据逐行读出再通过SQL语句批量插入MySQL。# 数据迁移示例 import sqlite3 import pymysql sqlite_conn sqlite3.connect(datong_travel.db) mysql_conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasedatong_travel, charsetutf8mb4 ) sqlite_cur sqlite_conn.cursor() mysql_cur mysql_conn.cursor() sqlite_cur.execute(SELECT spot_name, district, rating_score, comment_count, ticket_price, visit_date, comment_content, travel_type FROM spots) rows sqlite_cur.fetchall() for row in rows: mysql_cur.execute( INSERT INTO spots (spot_name, district, rating_score, comment_count, ticket_price, visit_date, comment_content, travel_type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s), row ) mysql_conn.commit()我个人在这个阶段还有一个建议迁移之前先做一次数据清洗把脏数据剔除掉再搬库不然脏数据会一路跟你到MySQL后面排错极其痛苦。3.2 清洗规则设计去重、补全、归一化爬虫抓到的原始数据用脏乱差三个字形容一点都不夸张。常见的几类问题及处理思路如下第一类重复数据同一个景点、同一条评论可能被爬虫跑了多遍后重复入库。去重逻辑建议用自然键而非自增ID来判断比如spot_name visit_date comment_content这三个字段的组合。def is_duplicate(conn, spot_name, visit_date, comment_content): cursor conn.cursor() cursor.execute( SELECT COUNT(*) FROM spots WHERE spot_name %s AND visit_date %s AND comment_content %s, (spot_name, visit_date, comment_content) ) return cursor.fetchone()[0] 0第二类缺失或异常值比如某景点没采集到门票价格字段是None或空字符串。处理方案是分而治之——有的可以按同类景点的均价填充有的可以直接标记为未知并在可视化时单独归为一类。第三类文本清洗评论内容里常会有HTML标签、表情符号、乱码。这里用正则做一次统一清洗import re def clean_text(text): if not text: return # 去除HTML标签 text re.sub(r[^], , text) # 去除多余空白 text re.sub(r\s, , text).strip() # 去除特殊符号保留中文、英文、数字、常见标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、\], , text) return text清洗环节最核心的原则是尽早清洗。每次爬完一批数据立刻清洗入库比集中到最后一次性处理要省力得多问题定位也更精准。4. 后端接口层Flask轻接口与Django MTV的协同方案4.1 Flask写数据分析接口几行代码出数据标题里Flask排在最前面说明它是这个项目的核心接口层。Flask最大的优势是轻、快、心智负担小写一个返回JSON的API只需要几行代码。这个项目里Flask的职责就是把数据库里的数据加工成前端能直接用的聚合结果。比如前端需要看大同各区县景点数量分布后端接口就可以这样写from flask import Flask, jsonify import pymysql app Flask(__name__) def get_db_conn(): return pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasedatong_travel, charsetutf8mb4 ) app.route(/api/district_summary) def district_summary(): conn get_db_conn() cursor conn.cursor() cursor.execute( SELECT district, COUNT(*) as cnt FROM spots GROUP BY district ORDER BY cnt DESC ) rows cursor.fetchall() data [{district: r[0], count: r[1]} for r in rows] return jsonify({code: 200, data: data}) if __name__ __main__: app.run(debugTrue, port5000)另一个常见需求是景点热度排行按评论数量倒序取前10app.route(/api/hot_rank) def hot_rank(): conn get_db_conn() cursor conn.cursor() cursor.execute( SELECT spot_name, comment_count, rating_score FROM spots ORDER BY comment_count DESC LIMIT 10 ) rows cursor.fetchall() data [{name: r[0], comments: r[1], rating: r[2]} for r in rows] return jsonify({code: 200, data: data})做这类接口时有一个准则值得坚持SQL能完成的聚合分析不要在Python内存里去算。数据库的GROUP BY、ORDER BY、COUNT、AVG的效率远高于你先把全部数据load到内存再循环计算。我见过太多人把大量数据集中到内存里做统计结果接口响应时间动辄好几秒——这种问题本质上不是性能问题而是设计问题。4.2 Django的MTV模式与后台管理价值Django在这个项目里扮演的是正式站点框架的角色。它的MTV模式Model-Template-View是一个在很多教程里被反复提及的经典概念但很多初学者只记住了名词没理解它到底解决了什么问题。这里我用大白话拆一遍Model模型你给Django定义一张表长什么样它负责把Python类和数据库表对应起来。你不用手写SQL建表语句写一个类就行。Template模板负责页面展示。它像是一个带占位符的HTML文件真正的数据由View传进来填进去。这个项目的核心可视化页面虽然用Vue实现但Django模板可以承载框架页面的部分。View视图处理用户请求的业务逻辑代码。用户在页面点了一下浏览器发请求给DjangoView接收到请求之后去查数据库然后决定是要渲染一个模板页面还是返回JSON。在这个项目里Django的核心价值是我认为最常被低估的一项——Admin后台管理系统。你把数据模型定义好Django自动帮你生成一个可以增删改查的后台界面几乎不用写额外代码。爬虫抓回来的景点数据、评论数据都可以在这里人工核对、修正、补充。对课程设计或毕业设计而言这个后台的存在会让整个项目的完整度上一个台阶。举个例子定义景点数据模型from django.db import models class Spot(models.Model): name models.CharField(景点名称, max_length100) district models.CharField(所在区县, max_length50) rating models.FloatField(评分, default0.0) comments models.IntegerField(评论数, default0) ticket_price models.FloatField(门票价格, default0.0) visit_date models.DateField(游玩日期) content models.TextField(评论内容, blankTrue) travel_type models.CharField(出行类型, max_length20, blankTrue) class Meta: verbose_name 景点数据 verbose_name_plural verbose_name def __str__(self): return self.name定义完模型执行python manage.py makemigrations和python manage.py migrate再创建一个超级管理员账号登录/admin就能直接对景点数据做管理了。整个过程十分钟以内搞定这个是Django给项目带来的最直接收益之一。4.3 前后端接口数据格式约定项目同时用了Flask和Django前端又是Vue独立开发的这就有个很重要的工程问题前后端对接的时候接口数据格式必须统一前端才不会疯掉。我习惯用一个统一的响应格式{ code: 200, message: success, data: { district_summary: [...], hot_rank: [...], trend_data: [...] } }code业务状态码200代表成功其它数字代表各类错误message描述信息方便前端Toast提示data实际业务数据结构根据接口而定。这样约定的好处是前端axios拦截器里只需要写一次状态判断逻辑后面所有接口都是同一套处理模式不用为每个接口单独做异常分支。5. Vue前端可视化从表格到图表的落地过程5.1 Vue项目初始化与环境配置前端部分用Vue搭建可视化页面。如果你是从零开始配置Vue环境这里有一个踩坑概率极高的点先指出来Node.js版本与Vue CLI/Vite版本不匹配会导致项目创建成功但起服务时报莫名其妙的错误。我建议的稳定组合是Node.js 16.20.x及以上版本配合Vite构建工具创建Vue 3项目。命令如下# 安装Vue脚手架 npm create vitelatest datong-visualization -- --template vue # 进入项目目录并安装依赖 cd datong-visualization npm installVite创建的项目结构简洁启动速度快对Vue 3的组合式APIComposition API支持非常到位。如果你的目标浏览器环境需要兼容老内核再考虑用Vue CLI Babel的方案否则Vite是最省心的选择。然后安装可视化图表库ECharts以及HTTP请求库axiosnpm install echarts axios5.2 ECharts图表对接后端接口数据以大同各区县景点分布柱状图为例完整走一遍从接口到渲染的流程。先在Vue组件里引入ECharts并定义图表容器template div refchartRef classchart-container/div /template script setup import { ref, onMounted } from vue; import * as echarts from echarts; import axios from axios; const chartRef ref(null); onMounted(async () { const res await axios.get(http://localhost:5000/api/district_summary); const data res.data.data; const chart echarts.init(chartRef.value); chart.setOption({ title: { text: 大同各区县景点分布 }, tooltip: {}, xAxis: { type: category, data: data.map(item item.district) }, yAxis: { type: value }, series: [{ type: bar, data: data.map(item item.count), itemStyle: { color: #3b82f6 } }] }); }); /script style scoped .chart-container { width: 100%; height: 400px; } /style这段代码虽然简洁但有一个常见问题值得注意接口请求完成之前图表容器可能还没有拿到数据或者容器宽度为0。所以建议在onMounted里先await接口返回再初始化图表。如果页面上有多个图表还可以封装一个useChart组合式函数逻辑可以集中在同一处。5.3 可视化页面布局与交互设计一个完整的可视化大屏页面通常会包含以下几个模块顶部标题栏展示大同旅游产业数据分析的标题和统计时间范围。左侧面板景点热度排行Top10条形图、游客出行类型占比饼图。中间区域游客量随时间变化的趋势折线图面积图。右侧面板各区县景点分布柱状图、评分区间分布散点图或雷达图。底部滚动区游客评论情感词云或最新评论列表。页面的交互方面可以加两个点击切换的维度按时间范围筛选近3个月/近1年/全部按区县筛选点地图或下拉框联动所有图表更新。这个联动效果在答辩或演示时讲出来非常加分因为它体现了数据驱动视图更新的前端思维。6. PyCharm调试技巧与部署踩坑记录6.1 PyCharm中对Flask和Django的调试配置用PyCharm做这个项目的开发有几个调试配置技巧值得分享关于Flask调试运行app.run(debugTrue)之后PyCharm的Run窗口可以直接点击行号左侧的断点带红色的行就是断点行。当接口被前端请求时控制台输出到* Running on http://127.0.0.1:5000这段提示之后再发起请求请求会被卡在断点处。此时可以逐行查看所有变量的当前值。关于Vue的调试Vue项目本身跑在Node环境里调试可以用Chrome的Vue Devtools插件。但后端代码的断点调试PyCharm的Python Debugger是主力。调试时有一个非常实用的技巧给接口代码加日志。不要只依赖Debug模式的断点因为爬虫触发接口时频率可能很快断点一个一个点过去效率太低。在关键位置加print输出或logging.info配合PyCharm的Console过滤功能能更快定位问题import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) app.route(/api/trend_data) def trend_data(): logging.info(收到趋势数据请求参数: %s, request.args) # 业务逻辑... logging.info(返回数据条数: %d, len(data)) return jsonify(data)6.2 前后端联调中的跨域问题Flask跑在5000端口Vue开发服务器跑在5173端口前端页面用axios请求http://localhost:5000/api/...必然会遇到跨域问题。浏览器控制台会报经典的No Access-Control-Allow-Origin header is present on the requested resource错误。解决方案有两种官方推荐的方式是直接用Flask-CORS这个扩展from flask_cors import CORS app Flask(__name__) CORS(app) # 允许所有来源跨域开发阶段够用如果想精确配置只允许自己的前端地址访问CORS(app, origins[http://localhost:5173], supports_credentialsTrue)生产部署时更合理的做法是用Nginx做反向代理。前端请求发给Nginx同一个域名下的/api路径Nginx把请求转发给内网的Flask服务这样浏览器看到的始终是同源请求不存在跨域问题。Nginx配置片段如下server { listen 80; server_name your_domain.com; location / { root /var/www/datong-visualization/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.3 从本地到服务器的部署注意点项目做到最后总要考虑怎么把它跑在服务器上。这里有几个我在实际部署中踩过、也帮别人排查过的坑按重要性排序坑一MySQL编码问题数据库建库时如果没有指定utf8mb4中文数据插入后极可能变成???问号。创建数据库时务必执行CREATE DATABASE datong_travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;坑二Vue打包后的静态资源路径默认Vue打包后JS和CSS文件的路径是绝对路径/assets/...。如果你把dist目录放在Nginx子路径下访问页面会白屏。解决办法是修改vite.config.js里的base配置import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], base: / // 部署在根路径用这个子路径则改为 ./ });坑三Flask的启动方式app.run(debugTrue)能用于开发但不要直接用来做生产服务。生产环境可以用waitress这种WSGI服务器来跑Flask应用pip install waitress waitress-serve --host0.0.0.0 --port5000 app:appDjango那边如果也需要部署生产环境建议用gunicornLinux环境 Nginx的方案或者同样用waitress在Windows服务器上顶上。6.4 爬虫数据更新频率与长期运行策略最后再聊一个容易被忽略的运营性问题爬虫不是跑一次就完了。你希望可视化页面上的数据保持新鲜就得让爬虫定期运行。这里有一个简单且稳妥的方案写一个爬虫主脚本用APScheduler做定时调度。比如每天凌晨2点自动更新前一天的新评论数据。from apscheduler.schedulers.blocking import BlockingScheduler def daily_crawl_job(): crawl_spot_comments() clean_duplicate_data() print(每日爬虫任务完成) scheduler BlockingScheduler() scheduler.add_job(daily_crawl_job, cron, hour2, minute0) scheduler.start()如果在Linux服务器上跑也可以用系统的crontab让脚本定时执行原理一样。关键是把这一步写进部署文档里不然换个环境之后数据就再也不更新了。写在实际项目之后的一点体会这个项目做完之后我最大的感受是技术难点从来不在某个具体框架怎么用而在于把数据链路完整打通。爬虫抓下来的数据要经过清洗进库后端要做聚合查询暴露接口前端要把接口数据渲染成能讲故事的可视化图表最后还要部署到公网让别人能访问。每一环单独拎出来都不难难的是衔接处的问题——编码对不对、字段名对不对、跨域解决没解决、打包路径对不对。我前前后后梳理过几次才发现大量时间都花在了这种接口对齐和环境配置的琐碎问题上。这也是我写这篇文章的初衷把这条链路上的关键节点和容易踩的坑一次性整理出来希望能给正在做同类项目的朋友省下一些时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。