ARTICLE DETAIL

资讯详情

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

Python爬虫+数据分析实战:构建农产品价格监控与可视化系统

Python爬虫+数据分析实战:构建农产品价格监控与可视化系统 做这个系统的念头其实是我妈某天在菜市场跟我抱怨新闻说黄瓜涨到八块了怎么我买才三块我当时一愣意识到一个特别反直觉的现象——农产品价格数据在网上到处都是但普通消费者、小商贩、甚至部分农业从业者真正能看懂行情走势的极少。官方发布的数据散落在各个网页表格里凌乱、口径不一、更新不及时普通人根本没法用。而我那会儿正在啃Python数据分析手头正好有爬虫和可视化的底子就决定做一个能自动抓取、清洗、分析、展示农产品价格的完整系统。从爬虫采集到数据库设计从数据清洗到波动分析最后用可视化大屏把结果展示出来这个项目前后花了我大概三周业余时间。做完之后我发现技术本身没多高深真正烧脑的是数据清洗和指标设计的细节——就像做饭炒菜就几分钟前面洗菜切菜才是功夫。这篇文章我把整个实现链路拆开写清楚包括数据源怎么选、清洗规则怎么定、环比同比怎么算才不被坑、可视化怎么选型以及我实测过程中踩过的几个典型坑。适合正在学Python数据分析想找实战项目的同学也适合想给自己的数据能力加一个完整案例的从业者参考。1. 农产品价格数据的脏乱差就是这个项目最大的价值开始动手之前先得搞清楚一个问题农产品价格数据到底长什么样难在哪。1.1 看似简单的黄瓜多少钱背后藏着三类数据矛盾第一类矛盾是时间错位。同一个农贸市场早市、午市、晚市的价格都不一样产地收购价、批发价、零售价更是三个完全不同的数字。第二类矛盾是单位口径混乱有的报价按斤有的按公斤还有的按件和箱如果不统一直接比较算出来的趋势全是错的。第三类矛盾是品类命名无规范黄瓜在有些平台上叫刺黄瓜有些叫青瓜还有些叫旱黄瓜如果不做别名映射统计出来的品类根本对不上。我当时统计了一下从公开渠道能拿到的农产品价格数据里大约有三成记录存在格式或口径问题需要程序化处理才能进入分析流程。这就意味着单纯能拿到数据远远不够数据清洗能力决定了这个分析系统的质量上限。说来也简单数据越乱清洗逻辑越值钱系统也越有说服力。1.2 数据源怎么选不追全追稳定和可回溯做数据项目的第一步其实是筛选数据源而不是写爬虫。我当时列了几个候选最终选择了农业主管部门公开的批发市场行情页面和几个大型农产品信息平台。选择标准有三条更新频率稳定至少每个工作日更新一次字段结构相对固定方便解析有历史数据可回溯能追溯到过去一年以上。这里多说一句很多人写爬虫喜欢追求全量数据把所有网站都抓一遍结果光维护解析规则就累死了。我的做法是选一到两个主数据源再配一个备用数据源主源挂了备用顶上。能从公开页面抓到的数据优先级高于需要登录、验证码或者小程序接口的渠道因为后者的反爬成本很高对个人项目来说性价比太低。爬虫部分我用的requests加BeautifulSoup请求频率控制在每两秒一次避免给目标服务器造成压力。核心代码如下import requests import time from bs4 import BeautifulSoup import pandas as pd def fetch_price_page(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding return resp.text def parse_price_table(html): soup BeautifulSoup(html, html.parser) rows [] for tr in soup.select(table tr): cells [td.get_text(stripTrue) for td in tr.select(td, th)] if len(cells) 5: rows.append(cells) return rows注意resp.apparent_encoding这一步很多网页声明的是utf-8实际内容却是gbk编码不处理会直接乱码。这个细节我后面还会再讲。1.3 数据模型设计一开始就为多市场、多品类、多周期留好接口数据抓下来不能直接堆到一个大表里最好按业务语义拆成几张表。我的设计是表名字段说明marketid, name, city, level市场信息表categoryid, name, alias品类表alias存别名price_recordid, market_id, category_id, price, unit, record_date价格记录表daily_summaryrecord_date, category_id, avg_price, max_price, min_price, price_change日汇总表拆表看起来麻烦但后续做多市场对比、多品类分析会省非常多的事。price_record表只负责存明细所有计算结果落到daily_summary分析层的查询就不会拖着几百万条明细跑。比如要查过去30天西红柿全国批发均价走势只需要对daily_summary做一次group by秒出结果。如果当初不分表每次查询都对明细做聚合数据量上来之后性能会非常难看。个人项目虽然不至于有千万级数据但良好的建模习惯是能迁移到工作中的。2. 清洗环节决定了分析结果可信度这一步没有捷径很多教程把数据清洗随便讲讲就跳过去了我在这部分着墨最多因为实际项目里80%的问题都出在数据质量上。2.1 单位统一与别名映射数据标准化的第一步从不同渠道抓回来的数据单位很可能是混着的。我写过一段全量清洗逻辑核心做三件事单位统一把元/斤元/公斤元/件的记录全部换算成元/公斤别名映射用一张字典表把刺黄瓜→黄瓜青瓜→黄瓜白萝卜→萝卜这类别名归并类型矫正把价格的字符串强制转成float把日期文本统一转成YYYY-MM-DD格式。unit_map {元/斤: 2.0, 元/公斤: 1.0, 元/500g: 2.0} def normalize_price(row): price float(row[price]) if row[unit] in unit_map: price price * unit_map[row[unit]] return round(price, 2) alias_map {刺黄瓜: 黄瓜, 青瓜: 黄瓜, 旱黄瓜: 黄瓜, 白萝卜: 萝卜, 胡萝卜: 胡萝卜} def normalize_category(name): return alias_map.get(name, name)这段代码看起来简单但有一个容易踩坑的地方换算系数不能想当然。比如元/斤换算成元/公斤乘2这个没错但有些平台写的元/500g其实对应的就是斤有些平台写的元/千克其实录入的是公斤。最好在写清洗逻辑之前抽样验证几条记录别一张表换算到底。2.2 异常值处理到底是真暴涨还是脏数据清洗过程中最考验判断力的就是异常值。某天黄瓜价格突然从4块变成40块是行情异动还是数据录错我的处理办法是三层校验范围校验设定每个品类合理的价格上下限比如黄瓜的批发价在历史上很少超过10元/公斤超过这个值先标记环比突变校验计算每条记录与前一天的环比变化率涨跌幅超过50%的标为待检查同源交叉校验同一个品类、同一天如果两个独立数据源的价格偏差超过20%说明至少有一个源有问题。被标记的数据我不会直接删除而是先进入一个suspect_records表确认是录入错误才剔除。为什么这么谨慎因为农产品价格受天气、节假日影响非常大比如台风天叶菜价格翻倍是真实存在的如果算法把这些真实波动当异常删掉分析结果就失真了。2.3 缺失值的业务含义不硬填先问缺失原因pandas里处理缺失值最常用的就是fillna和dropna但在真实项目里不能机械地填平均值或者删行。比如某些市场周末不更新数据周一的记录后面跟着周二的记录中间没有周六日这时候缺失值代表休市不是真正意义上的缺失。再比如某个市场在搬迁装修连续两周没有更新这时候如果硬填历史均值等于把无数据伪装成了价格稳定。我的策略是先按市场、品类分组做连续性检查如果某个序列的缺失模式是规律的比如固定周末缺失保持原样如果是随机缺失且占比低于5%用前后两天的均值插补缺失率超过30%的品类直接不参与分析等数据补全再纳入。这个思路说穿了就是任何填充方式都是一种假设要选符合业务真实情况的假设而不是符合统计方便的假设。3. 从价格记录到价格洞察核心分析维度的落地实现数据清洗干净之后分析才有意义。我做分析的时候没有一上来就上机器学习而是先做好几类基础指标这些指标才是用户真正看得懂、用得上、觉得系统有用的东西。3.1 环比与同比短期波动和长期趋势要分开看环比和同比是价格分析里最基础也最重要的两个指标。环比的逻辑是跟前一天或前一周比反映短期变化同比是跟去年同一天或同一周比剔除季节性因素看长期趋势。def calculate_price_change(df): df df.sort_values(record_date) df[prev_close] df[avg_price].shift(1) df[week_ago] df[avg_price].shift(7) df[year_ago] df[avg_price].shift(365) df[daily_change] (df[avg_price] - df[prev_close]) / df[prev_close] * 100 df[weekly_change] (df[avg_price] - df[week_ago]) / df[week_ago] * 100 df[yoy_change] (df[avg_price] - df[year_ago]) / df[year_ago] * 100 return df这里有个细节shift(7)和shift(365)适用的是日频数据如果中间有休市空档这个位移就不准。所以我在清洗阶段把休市日期补成空值之后做环比计算之前又做了一次reindex让日期连续起来用空值隔开不连续区间这样shift才能对齐到真正的前一天。环比指标给普通用户看同比指标给分析场景用。系统里我把两类指标都算好存进汇总表页面上默认展示环比用户可以手动切到同比视角。3.2 移动平均与波动区间剥离噪声看到真趋势单日价格波动非常大直接看折线图会很乱所以我额外计算了7日和30日移动平均线。移动平均的本质是低通滤波把短期噪声滤掉让趋势线平滑下来。比如某个蔬菜价格连着三天小涨单日看涨跌幅意义不大但30日均线持续上抬就能说明这波涨势是趋势性的。df[ma7] df[avg_price].rolling(window7, min_periods3).mean() df[ma30] df[avg_price].rolling(window30, min_periods10).mean() df[price_std] df[avg_price].rolling(window30).std()这里min_periods参数很有用它的含义是窗口中至少有几个有效数据才计算。因为数据可能有零星缺失如果窗口期里有效数据太少算出来的平均值没有参考价值设一个最小有效数能过滤掉这种情况。波动区间我用了均值加减一倍标准差来定义。假设30天均价是5块标准差是0.8那么正常波动区间就是4.2到5.8。如果某天价格跳出这个区间且不是节假日因素系统就会打上关注标签。这个逻辑比单纯看涨跌幅更稳定因为它是基于这个品类自身的历史波动特征来判断的。3.3 品类相关性菜价之间的联动物价能不能量化分析过程中我发现一个有意思的现象某些蔬菜的走势高度同步比如菠菜和生菜因为它们的种植周期和消费场景很像。而黄瓜和猪肉几乎不相关。为了把这个观察量化我计算了品类价格序列的皮尔逊相关系数并做了简单的聚类。pivot_df df.pivot_table(indexrecord_date, columnscategory, valuesavg_price) corr_matrix pivot_df.corr() high_corr_pairs corr_matrix.where( np.triu(np.ones(corr_matrix.shape), k1).astype(bool) ).stack().reset_index() high_corr_pairs.columns [品类A, 品类B, 相关系数] high_corr_pairs high_corr_pairs[high_corr_pairs[相关系数] 0.7]这个分析结果在系统里做成了一张热力图用户可以直观看到哪些品类会「同涨同跌」。对采购商来说如果A和B高度相关那么在A涨价的时候可以提前预判B大概率也会跟涨这是很有实用价值的决策参考。有一点需要注意相关性不代表因果。菠菜和生菜同涨可能是共同受气温影响不是因为菠菜涨价带涨了生菜。在系统里我把这个模块定位成观察窗口而不是预测工具避免用户过度解读。4. 可视化输出怎样让一个分析系统真正看得下去分析做得再深展示不友好等于白做。可视化这部分我经历了从Matplotlib到ECharts的选型过程踩了一些坑最后整体效果和开发效率都提升了一个档次。4.1 选型逻辑大屏场景用 ECharts 而不是 Matplotlib最开始我是用Matplotlib出图的静态图片在本地看没问题但做Web展示就尴尬了不能交互、不能缩放、不能hover看具体数值而且中文字体问题让人头大。后来我换成ECharts才意识到交互才是数据可视化最重要的加分项。ECharts是前端的JavaScript图表库天然适合Web展示支持折线、柱状、热力、地图等几十种图表类型而且配置直接写在JSON里后端把数据拼成JSON返回前端渲染就行。Matplotlib在数据探索阶段依然有用但在对外展示系统这个场景里ECharts是更合理的选择。我的整体架构是Python爬虫采集数据 → pandas清洗入库 → MySQL存储 → Flask提供JSON接口 → 前端页面用ECharts渲染图表。这套架构的好处是每层职责单一后面哪一环出问题都能快速定位。4.2 Flask 数据服务接口设计Flask接口我设计得比较简单主要提供三类数据接口/api/price/trend?category黄瓜days30返回某个品类在指定时间段内的价格走势包括日均价、7日均线、30日均线/api/price/rank?date2024-06-01返回某一天所有品类的涨跌幅排行/api/price/correlation返回品类相关系数矩阵。from flask import Flask, jsonify, request import pymysql app Flask(__name__) def get_db_conn(): return pymysql.connect( hostlocalhost, userroot, password******, databaseagri_price, charsetutf8mb4 ) app.route(/api/price/trend) def price_trend(): category request.args.get(category) days int(request.args.get(days, 30)) conn get_db_conn() cursor conn.cursor() cursor.execute( SELECT record_date, avg_price, ma7, ma30 FROM daily_summary WHERE category%s AND record_date DATE_SUB(CURDATE(), INTERVAL %s DAY) ORDER BY record_date , (category, days)) rows cursor.fetchall() conn.close() data [{date: r[0].strftime(%Y-%m-%d), avg: r[1], ma7: r[2], ma30: r[3]} for r in rows] return jsonify({code: 0, data: data})注意查询里的参数化写法%s占位符由pymysql转义防止SQL注入。虽然这是个本地项目但这个习惯应该从一开始就养成别等部署到公网才来补课。前端拿到JSON之后ECharts的配置大概长这样fetch(/api/price/trend?category黄瓜days30) .then(res res.json()) .then(res { const dates res.data.map(d d.date); const avgPrices res.data.map(d d.avg); const ma7Values res.data.map(d d.ma7); const ma30Values res.data.map(d d.ma30); const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [日均价, 7日均线, 30日均线] }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: 元/公斤 }, series: [ { name: 日均价, type: line, data: avgPrices }, { name: 7日均线, type: line, data: ma7Values, smooth: true }, { name: 30日均线, type: line, data: ma30Values, smooth: true } ] }); });4.3 三类核心图表的选型与配置细节系统里我最终保留了三种图表折线趋势图、涨跌排行柱状图、热力图。折线趋势图用于品类价格走势日均价用带透明度的细线展示均线用平滑粗线展示用户能清楚地区分噪音和趋势。涨跌排行柱状图展示某天涨跌幅前10和后10名的品类上涨用红色、下跌用绿色这个配色要特别说明一下在中国大陆的金融和交易语境里红涨绿跌是约定俗成的千万别搞反。柱状图我还会把数值标签显示出来方便截图传播。品类相关性热力图用的矩阵图颜色深浅代表相关系数绝对值大小。配置上有一个细节热力的visualMap分段要设置好我分了五档分别是0.5以下/0.5-0.6/0.6-0.7/0.7-0.8/0.8以上颜色从浅蓝过渡到深红既美观又有区分度。4.4 大屏布局的取舍信息密度和可读性的平衡做可视化大屏的时候最容易犯的错就是什么图表都想往上放结果页面信息密度过高反而什么都看不清。我的取舍标准是一张大屏只回答三个问题——最近价格怎么变什么在涨什么在跌哪些品类在联动。所以我最终的布局是顶部一条KPI指标栏展示综合价格指数和今日涨跌分布左侧是折线趋势图中间是涨跌排行柱状图右侧是热力图和表格。每个区域之间留足呼吸感配色用深色背景加亮色数据这样在普通显示器上也有比较好的视觉效果。大屏的本质是给人快速获取信息的不是为了炫技。把信息层级理清楚比堆叠任何花哨的动画都重要。5. 实测过程里最值得说的几个坑系统做出来之后我连续跑了三周期间遇到不少问题挑几个典型的写出来这些是文档里查不到的经验。5.1 中文标签与字体问题一个elusive的方块Matplotlib出图时中文变方块的问题网上教程一大堆核心就是设置中文字体。但ECharts也会遇到类似问题只不过表现不太一样——前端页面本身能显示中文问题是图表里hover出来的数值提示框有时出现字体错乱。这是因为ECharts的tooltip默认字体栈在某些浏览器里没包含中文字体。解决办法很简单在ECharts的textStyle里显式指定fontFamily: Microsoft YaHei, sans-serif。这看起来是个小到不能再小的问题但真到了演示环节满屏方块会显得非常不专业。另外还有一个坑在数据库连接串上。MySQL连接参数里的charsetutf8mb4不是可选项如果漏了存入的中文可能变成???。这个坑我在本地开发时没遇到换了一台环境后突然出现排查了很久才定位到是连接串缺了字符集参数。5.2 特殊时期的价格假异常算法不能只有统计思维我前面提到异常值校验的逻辑但实测中发现这套逻辑在面对真实世界的事件时会失灵。比如某个产地遭遇连续降雨叶菜价格全线上涨30%以上环比突变校验直接把这些记录标记成了异常。实际上这是真实的波动不能删。后来我在异常校验里加了一个全场联动的判断如果多个品类同时出现类似的涨跌方向且涨跌幅度在类似量级这大概率是宏观经济因素或天气因素导致的系统性波动而非数据错误。只有孤立的、单个品类突然出现离谱价格才判定为录入异常。这个处理思路让我意识到数据清洗规则不完全是技术问题还涉及对业务场景的理解。你越了解这个行业清洗规则就越精准。5.3 历史价格修正数据更新不等于追加最后一个坑来自数据的增量更新机制。我一开始写的更新逻辑很天真新抓到的数据直接插入price_record表。结果跑了一周后发现趋势图出现了一个明显的断层跳水。查了半天才发现某个数据源会修正过去几天的历史价格之前发布的某个价格是错的网站后来改了而我抓取时只把新数据append进去没有更新已有记录。从那以后我把写入逻辑改成了按日期按市场按品类做唯一键存在就更新不存在才插入用一条SQL的ON DUPLICATE KEY UPDATE搞定。INSERT INTO price_record (market_id, category_id, price, unit, record_date) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE priceVALUES(price), unitVALUES(unit);这个改动解决了一个隐蔽的数据一致性问题。很多人做爬虫项目只关注首次采集却忽略了源站可能会回头修数据。增量更新必须考虑覆盖写不是无脑追加这个经验我写在任何报告里都值得单独标星。整个项目做下来我最深的体会是农产品价格分析在技术难度上不算高它真正的价值在于把一堆杂乱的、口径不一的数据变成普通人能看懂的信息。Python生态里的pandas、Flask、ECharts这几个工具链已经非常成熟任何一个人按照这篇文章的链路走一遍都有机会做出一个能跑起来的完整系统。如果后续想继续进阶可以尝试引入更高频的数据源做日内价格预测或者结合天气数据做联动分析方向很多每个方向都还大有可为。
返回列表