尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

城市旅游数据采集分析系统:从爬虫到决策支持的完整实践框架

城市旅游数据采集分析系统:从爬虫到决策支持的完整实践框架 简介一份基于Python的城市旅游数据采集分析系统学士学位论文聚焦大数据技术在旅游业中的实际应用。论文从研究背景与国内外现状切入完整覆盖系统需求分析、功能与架构设计并详细讲解社交媒体、在线预订平台等多源数据的采集方法以及Hadoop HDFS和NoSQL数据库的存储方案。在数据分析环节运用Pandas、Numpy进行预处理结合机器学习算法挖掘游客行为模式、预测景点热度并通过Matplotlib、Seaborn实现可视化。全文还结合实证案例与最新发展趋势可为大数据或旅游管理方向学生提供选题参考、论文结构范式和实验设计思路。资源共1个docx文档压缩包大小约31KB目前已有495人学习下载轻量便捷适合快速查阅核心内容。1. 城市旅游数据采集分析系统从爬虫到决策支持站在宽窄巷子的游客中心打开这类系统后台可能正同时抓取景点POI、酒店评分、地铁客流和社交评论。这个项目的核心不是爬虫多厉害而是把采集到的异构数据整理成能支撑分析的结构再落到Python数据分析与可视化流程里。对想入门大数据分析与挖掘的人来说它是一个典型的“数据采集-清洗-分析-展示”闭环对已经写过爬虫的人价值则在于系统模块划分和增量更新设计。论文拆出来的这套方案技术栈是requests/Scrapy Pandas Pyecharts适合一个人短时间搭起来也能够作为课程设计或毕业论文的完整实践框架。2. 系统架构与数据流先把模块边界划清楚2.1 四个核心模块与职责我拿到这类项目第一步不会急着写爬虫而是先画一张模块图。系统整体分四块数据采集、数据存储、数据分析、用户交互。采集模块只负责把网页或API变成结构化的DataFrame或JSON存储模块接收原始数据并写库分析模块从库里读数据做统计和挖掘展示模块提供查询和可视化。模块核心职责产出物推荐技术数据采集抓取景点、酒店、评论、交通数据原始DataFrame / JSONrequests、Scrapy、BeautifulSoup数据存储落地、去重、索引管理MySQL / PostgreSQL 表SQLAlchemy、pymysql数据分析统计、情感、关联分析指标结果、特征表Pandas、NumPy、SnowNLP可视化展示图表和地图输出HTML / 图片Matplotlib、Pyecharts模块边界清晰以后改造成本会低很多。比如把采集模块从requests换成Scrapy只需要保证输出仍是统一的DataFrame或JSON存储和分析代码一行都不用动。很多人把爬虫和分析写在一个文件里前期跑得通后期想加一个数据源就发现到处要改这就是模块边界没有划清导致的。2.2 架构选型为什么是Python 关系型数据库这个项目最容易被问到的点是Python处理“大数据”够不够快我的回答是这里说的城市旅游数据实际是百万行以下的结构化数据瓶颈在IO和解析不在语言。Python的优势是采集和分析同语言不需要像Java架构那样起Kafka和Flink。存储上入门阶段用SQLite就够但要考虑后续切换MySQL或PostgreSQL时最好直接用SQLAlchemy的ORM层隔离。from sqlalchemy import create_engine, Column, String, Float, Integer from sqlalchemy.orm import declarative_base Base declarative_base() class ScenicSpot(Base): __tablename__ dim_scenic scenic_id Column(Integer, primary_keyTrue, autoincrementTrue) city Column(String(50), nullableFalse) name Column(String(100), nullableFalse) score Column(Float) comment_count Column(Integer)这段模型定义了景点维表。注意city和name要组合成唯一键避免同一个景点重复入库时产生多行。SQLAlchemy在这里的价值是屏蔽底层数据库差异以后把连接串换成MySQL或PostgreSQL模型代码不用改。2.3 数据流设计与增量更新策略常见做法是采集层输出统一的中间格式我一般会设计成JSON因为它天然支持嵌套结构。比如一条景点记录{ source: qunar, type: scenic, city: 成都, scenic_name: 宽窄巷子, score: 4.6, comment_count: 23145, fetch_time: 2025-04-01 10:00:00 }每个字段都有明确含义source标识数据来源便于溯源fetch_time用于增量更新判断。爬虫不需要在回调里直接写数据库可以先把数据写到一个暂存目录比如raw/2025-04-01/清洗后再统一入库。这样做的好处是如果当天抓取的数据格式有问题可以在暂存阶段修正而不是把脏数据直接写进生产表。增量更新策略我推荐“按fetch_time做阈值扫描”每天只请求最近更新过的页面URL列表从上一轮数据里读取。第一次全量抓取后后续每次只补增量既能减少请求量也能降低被反爬系统盯上的概率。3. 数据采集模块把爬虫写成可维护的工程3.1 数据源梳理与字段映射开始写代码前先把数据源列清楚。城市旅游数据常见来源包括政府开放数据平台、OTA网站、社交媒体评论、地图API。下面这张表是我做项目时常用的梳理模板。数据源类型典型来源访问方式主要字段基础地理信息政府开放平台公开API城市名称、经纬度、行政区划景点信息携程、去哪儿页面解析景点名、评分、评论数、开放时间用户评论马蜂窝、大众点评页面解析 / API评分、评论文本、发布时间交通数据高德地图、公交API申请Key线路名称、站点、经度纬度住宿餐饮美团、携程页面解析名称、价格、评分、地址字段映射的意思是所有来源统一切成系统内部命名。比如外部页面里叫“分数”系统里就叫score“点评条数”统一为comment_count。不要在数据库里同时保留score和“评分_原始”两套列名后期写SQL会非常痛苦。3.2 基于RequestsBeautifulSoup的采集脚本抓取页面时我习惯先看目标网站是否存在公开JSON接口。如果前端调用了API直接请求API比解析HTML稳定得多。找不到公开API再用BeautifulSoup。import requests import pandas as pd from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/ } def fetch_scenic(url): resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) name_node soup.select_one(h1.scenic-name) score_node soup.select_one(.score span) comment_node soup.select_one(.comment-count) if name_node is None or score_node is None: return None return { name: name_node.get_text(stripTrue), score: float(score_node.get_text(stripTrue)), comment_count: int(comment_node.get_text(stripTrue).replace(,, )), fetch_time: pd.Timestamp.now() }这段代码有几个关键点。headers里的User-Agent是必须的很多服务器会拦截裸requeststimeout10防止某个慢节点卡住整批任务select_one只取第一个匹配节点页面结构变化时返回None主动让脚本失败而不是把错误数据写进结果里。返回的字典统一交给pandas.DataFrame处理方便批量落盘。如果评论区是异步加载的BeautifulSoup只能拿到第一页。这时候需要找XHR接口用requests直接请求JSON数据。接口返回的字段名经常是英文缩写比如“sight_comment_score”要映射成系统内部字段。3.3 反爬与请求策略限速、重试、User-Agent反爬策略我的建议是“先礼貌再对抗”。先看robots.txt控制请求频率不要一上来就上代理池。对大多数旅游网站来说单IP每秒一次请求基本安全。我一般在requests外再包一层限速和重试。import time import random import requests def polite_request(session, url, min_sleep1.0, max_sleep2.5): time.sleep(random.uniform(min_sleep, max_sleep)) resp session.get(url, timeout10) if resp.status_code 403: time.sleep(30) resp session.get(url, timeout10) resp.raise_for_status() return respmin_sleep和max_sleep控制随机休眠区间避免出现固定间隔的请求模式。403时先等30秒再重试一次如果还是403说明需要登录Cookie或加密参数而不是继续硬怼。爬虫日志里要记录每个URL的HTTP状态码和返回体前200个字符否则反爬触发时你只能看到一堆401完全不知道卡在哪。4. 数据存储与预处理给分析喂干净的数据4.1 存储选型与表结构设计数据量级在几十万行以内时SQLite足够但课程设计和论文场景下我更推荐MySQL。原因很简单MySQL的SQL语法更容易迁移而且招聘市场上MySQL几乎是必问项。表结构设计遵循维度建模思路把不随时间变化的景点信息放维表把每天抓取的评论、热度放事实表。CREATE TABLE IF NOT EXISTS dim_scenic ( scenic_id INT PRIMARY KEY AUTO_INCREMENT, city VARCHAR(50) NOT NULL, name VARCHAR(100) NOT NULL, lat DECIMAL(9,6), lng DECIMAL(9,6), category VARCHAR(50), score DECIMAL(2,1), comment_count INT, UNIQUE KEY uk_city_name (city, name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联合唯一键uk_city_name是整个表的灵魂。爬虫第二天拿到同一景点时通过INSERT ... ON DUPLICATE KEY UPDATE更新评分而不是新增一行。ENGINEInnoDB是为了支持事务批量导入过程中如果出现异常可以ROLLBACK。CHARSETutf8mb4必须加评论内容里常有emoji用默认utf8会变成问号这是很多刚接触数据采集的人最容易忽略的坑。常见清洗动作与Pandas方法对应如下。清洗动作常用方法说明去重drop_duplicates按cityname保留最新记录缺失值处理dropna / fillna缺失比例高时整行删除类型矫正to_numeric / astype字符串数字转数值类型文本拆分str.extract从“评分评论数”混合字段拆列日期标准化to_datetime统一时间格式并提取星期特征4.2 清洗与去重Pandas实操数据采集回来后第一件事不是分析是清洗。我一般写一个独立的clean.py把清洗逻辑固定下来每次新数据都走同一套流程。import pandas as pd df pd.read_csv(raw/scenic_20250401.csv) df[score] pd.to_numeric(df[score], errorscoerce) df df.drop_duplicates(subset[city, name], keeplast) df df.dropna(subset[name, lng, lat]) df[comment_count] df[comment_count].fillna(0).astype(int) df[fetch_date] pd.to_datetime(df[fetch_time]).dt.dateerrorscoerce会把无法转换的评分变成NaN随后dropna可以把坐标缺失的行滤掉。如果有一行评论数是“1.2万”astype(int)会报错所以文本数字要先做单位转换。这个流程里最容易漏的是fetch_date它用于后续增量分析如果只在JSON里保留完整时间戳按天分组时会多出时分秒噪声。清洗完的数据建议保存成Parquet格式。Parquet比CSV体积小且能保留字段类型下次读入时不用重复做类型矫正。4.3 数据标准化与特征构造分析之前我还会根据目标构造一些特征。比如把评分归一化到0-1区间把时间拆成“星期几”和“是否节假日”df[date] pd.to_datetime(df[review_date]) df[weekday] df[date].dt.dayofweek df[is_holiday] df[date].isin(holiday_list) df[score_norm] (df[score] - df[score].min()) / (df[score].max() - df[score].min())is_holiday需要本地维护一份节假日列表不要每次请求第三方接口。weekday字段在后续做游客量预测时比单纯的日期更直观地反映周期性。score_norm归一化的意义是让不同景区的评分可以横向比较。如果要进一步做KMeans聚类还需要套一次StandardScaler否则评分和评论数两个量纲差异太大聚类结果会被评论数主导。5. 数据分析与可视化从统计图表到景点热度预测5.1 统计分析与情感分析数据进入分析阶段后最常用的是groupby统计。核心指标包括每个景区的平均评分、评论总量、评分标准差。summary df.groupby(city).agg( avg_score(score, mean), total_comments(comment_count, sum), std_score(score, std) ).reset_index()这里的std_score很多人忽略。评分标准差大说明游客对该城市评价分歧严重可能是服务质量不稳定也可能是景点类型差异过大。这类指标比单纯看平均分更有业务解释力。情感分析可以用最小可用方案SnowNLP对中文短评效果尚可。我一般把它封装成一个函数from snownlp import SnowNLP def sentiment_label(text): if pd.isna(text): return -1 score SnowNLP(text).sentiments if score 0.6: return 1 if score 0.4: return 0 return 2情感标签数值范围业务含义1大于0.6正面评价0小于0.4负面评价20.4到0.6中性评价阈值0.6和0.4是常见默认值实际项目里最好人工标注100条评论计算准确率后再调整。SnowNLP对“景色很美但人多”这类转折句会失灵因为它在句子级做情感判断不会区分正负面权重。5.2 地图可视化用Pyecharts打点地理信息可视化是整个展示层最具冲击力的部分。Pyecharts的Geo组件可以快速实现景点热度分布。from pyecharts import options as opts from pyecharts.charts import Geo geo ( Geo() .add_schema(maptype成都) .add_coordinate(宽窄巷子, 104.056, 30.670) .add(热度, [(宽窄巷子, 356), (都江堰, 280)], type_effectScatter) .set_global_opts(title_optsopts.TitleOpts(title成都景点热度分布)) ) geo.render(chengdu_hotspots.html)add_coordinate是把经纬度注册到地图坐标系中数据量大时要循环注册。type_effectScatter是涟漪散点图适合表达热度大小。有一个数据精度问题需要处理公开平台返回的经纬度很多是WGS-84标准而国内地图使用GCJ-02标准如果直接打点放大地图后会看到点偏移。解决方法是引入coord_convert做一次坐标系转换转换函数网上有现成实现不要自己写。5.3 案例分析识别景区热度峰值与关联推荐基于一个季度的数据可以找出每个景区的热度峰值周。先把评论数按周聚合再取每个景区最高的那一周。peak df.groupby([scenic_name, df[date].dt.isocalendar().week])[comment_count].sum().reset_index() top peak.loc[peak.groupby(scenic_name)[comment_count].idxmax()]这个结果能看出每个景区的旺季差异。比如宽窄巷子在国庆周达到峰值而都江堰在暑期更热。如果想把“宽窄巷子经常和火锅店一起被提及”这样的关联挖掘出来可以先用jieba对每条评论做关键词抽取再构造词频矩阵最后用mlxtend的apriori跑关联规则。from mlxtend.frequent_patterns import apriori, association_rules basket df.groupby([review_id, keyword])[count].sum().unstack().fillna(0) freq apriori(basket, min_support0.02, use_colnamesTrue) rules association_rules(freq, metriclift, min_threshold1.2)min_support0.02表示关键词至少出现在2%的评论里才参与规则挖掘lift大于1.2说明前后项有实际关联而非随机共现。把关联规则结果存到数据库详情页就能展示“与该景点同频出现的美食关键词”这一步能明显提升系统的实用感。6. 增量采集与数据质量校验上线前的最后一公里6.1 增量采集与断点续采全量爬一次后每天再重复抓所有页面既浪费流量也容易触发反爬。常见做法是在采集表里维护last_crawl字段每次采集前先查出每个城市和景点最近一次抓取时间。SELECT city, name, MAX(fetch_time) AS last_time FROM dim_scenic GROUP BY city, name;后续只请求fetch_time之后有更新的页面。对于没有更新时间字段的网站维护一个URL队列已完成的URL写入done.txt重启后跳过这些URL。注意增量采集和断点续采不要同时做否则逻辑容易混乱会出现“某条记录今天已经更新过但又从断点重新抓了一版”的情况。6.2 数据质量校验规则入库后要自动校验不能等图表画出来才发现数据是空的。我常用的四类规则如下。检测项规则处理动作字段缺失name或city为空剔除并告警值域越界score不在0到5区间重新采集一次重复记录cityname重复保留最新fetch_time现实异常comment_count大于100万人工确认Pandas里可以直接用assert快速校验assert df[score].between(0, 5).all()。跑完采集后将失败的行数和失败原因写入quality_report.log不要直接中断主流程。6.3 定时调度与异常告警最后把采集和分析流程串成一个定时任务。我用APScheduler每天凌晨2点半跑一次这个时段目标网站访问量低接口稳定性更好。from apscheduler.schedulers.blocking import BlockingScheduler def run_pipeline(): fetch_all_cities() clean_and_load() generate_reports() scheduler BlockingScheduler() scheduler.add_job(run_pipeline, cron, hour2, minute30, iddaily_pipeline) scheduler.start()如果某天采集失败告警信息要包含任务名、失败阶段和最近一次成功时间否则第二天只会继续跑昨天的脏数据。日志按天滚动保留7天就够排错时近几天的完整上下文比几十天前的内容更有价值。还要把数据质量校验函数挂在run_pipeline的成功回调里这样每天采集完成即自检不需要等到人工打开图表才发现数据异常。本文还有配套的精品资源点击获取
返回列表