资讯详情

资讯详情

Python+Flask从零搭建农产品智能推介系统:数据清洗到部署全复盘

去年接了一个农产品信息化的活儿要把陕西各地的特色农产品从一堆Excel表格里变成一个能看、能搜、能自动推荐的推介系统。整个项目用纯Python开发从数据清洗到系统上线前后一个月。这篇文章就把这套系统的设计思路、核心代码和踩过的坑完整复盘一遍给想用Python做类似行业应用的朋友一个可直接参考的范本。1. 项目立项农产品推介不是简单做个网页1.1 陕西特色农产品的“家底”到底有多少做这套系统之前我先把陕西的农产品盘了一遍。洛川苹果、周至猕猴桃、临潼石榴、清涧红枣、商洛核桃、富平柿饼、洋县黑米、泾阳茯茶、韩城花椒……光叫得上名字的地标性产品就有几十个每个品类的产地、上市季节、价格区间、历史口碑都不一样。传统推介方式的问题很明显信息散落在各个农业合作社网站、批发市场报价和展会宣传册里消费者想买的时候找不到权威汇总采购商想了解产地信息也得打很多电话。这个项目要解决的核心问题就是把这些碎片化信息统一成结构化数据再通过一个网站让用户按需浏览、搜索并获得个性化推荐。1.2 项目目标与业务闭环系统定位不是电商平台而是“推介中枢”。它不需要处理在线支付、物流下单这些重业务重点做三件事农产品信息的集中展示与检索用户行为数据的采集与分析基于用户偏好的农产品推荐业务闭环是用户访问系统浏览产品系统记录用户点击和收藏行为后台根据行为数据计算偏好标签用户再次访问时看到贴合自己口味的推荐列表。对管理员来说可以通过后台维护产品数据、查看各品类的热度统计从而知道哪些产品值得重点推介。2. 技术选型Python这套组合拳是怎么定的2.1 为什么技术栈全部押在Python上这个系统没有复杂的高并发需求核心是数据处理逻辑和业务实现速度。Python在这种场景下的优势非常明显数据处理生态完善pandas做数据清洗、SQLAlchemy做ORM都是一条龙自带丰富的文本处理能力做标签匹配和相似度计算很顺手开发效率高一个人用一个周末就能搭出可跑通的原型后续如果要叠加数据分析、可视化、爬虫采集产地信息Python的轮子最多我当时在Flask和Django之间纠结了一下。Django自带Admin后台和ORM功能全但偏重Flask轻量灵活配合扩展可以达到同样的效果还更容易让新手看懂。最终选了Flask 2.x SQLAlchemy Bootstrap 5 ECharts这套组合。2.2 数据处理与推荐模块的分层代码没有硬套复杂架构而是按照功能边界分了四层数据层SQLite数据库三张核心表存储产品、用户、行为日志业务层Flask路由处理请求、调用推荐引擎、返回JSON或渲染模板推荐引擎层独立模块纯Python实现标签匹配和热度加权计算展示层Jinja2模板 Bootstrap页面 ECharts图表提示SQLite适合单机部署和数据量在几万条以内的场景如果后续数据量上来了把SQLAlchemy的连接字符串换成MySQL的连接方式就能平滑切换。2.3 开发环境准备环境这块我踩过不少坑简单说一下最终可用的配置方案。项目使用Python 3.10版本用venv创建独立虚拟环境避免和系统Python环境冲突。依赖包只有六个flask、flask-sqlalchemy、pandas、numpy、jinja2、gunicorn生产环境用。python -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy pandas numpy gunicorn在Windows上开发、Linux上部署时注意路径分隔符和编码问题。Windows下直接用python app.py启动调试Linux上使用gunicorn多worker方式启动需要设置环境变量export FLASK_ENVproduction来关闭调试模式。3. 数据建模一张product表的自我修养3.1 核心数据表结构设计推介系统的基础是干净、完整的产品数据。我设计了四张表其中最核心的是products表字段如下CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(50) NOT NULL, region VARCHAR(50) NOT NULL, season VARCHAR(50), price DECIMAL(10,2), tags VARCHAR(200), description TEXT, image_url VARCHAR(255), sales_count INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );关键设计细节tags字段存逗号分隔的标签字符串比如“脆甜、红富士、高原种植”由人工录入时固化。虽然不符合数据库第三范式但在这个量级下查询效率最高推荐引擎读取后直接split分割即可。category和region单独建字段不要塞进tags。因为分类和产地是最常用的筛选维度建索引后查询速度能提高几个量级。sales_count是冗余字段用于推荐引擎的热度排序在管理员后台每次修改产品时维护。users表和browse_logs表相对简单。users记录用户名和偏好设置browse_logs记录用户每次浏览、收藏、分享行为的流水。CREATE TABLE browse_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product_id INTEGER NOT NULL, action_type VARCHAR(10) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );action_type限定为view、collect、share三种方便统计不同行为的权重。3.2 产品数据清洗的实战经验原始数据是各地提供的Excel表格式五花八门。有的产地写“陕西-商洛-镇安县”有的直接写“商洛核桃”价格有的是“每斤10-15元”这种文本数据清洗就得花心思。我用pandas做了三步处理产地信息标准化统一提取到市级比如“陕西-商洛-镇安县”提取为“商洛”价格字段转换从文本中提取数字区间的最小值和最大值拆分成price_min和price_max两个字段标签补全人工建立品类关键词映射表比如“苹果”自动关联“水果”、“生鲜”、“可送礼”import pandas as pd df pd.read_excel(products_raw.xlsx) df[price_min] df[price].str.extract(r(\d(\.\d)?)).astype(float) df[region_simple] df[region].apply(lambda x: x.split(-)[1] if - in str(x) else x) df.to_csv(products_clean.csv, indexFalse)3.3 管理员录入端的校验逻辑虽然推介系统面向用户端但管理员录入产品才是数据质量的源头。我限定了category必须从预设列表中选择tags必须从标签库中勾选描述不能少于20个字。这一条在项目上线后体验非常明显。实际操作中刚开始没有限制录入员把图片地址写错、标签随意填“好吃”“不错”这种无效词推荐引擎的质量就直线下降。后面加了校验和默认值兜底整个系统才稳定下来。限制不是为了让操作变麻烦而是在源头保证后续推荐算法输入数据的可用性。4. 推荐引擎用纯Python实现标签匹配4.1 推荐策略的整体思路推荐引擎是本系统的技术核心。没有用复杂算法因为农产品是低频消费品用户行为数据稀疏冷启动问题严重。我用的是“标签匹配 行为加权 热度补偿”的三层策略第一层统计用户历史行为提取偏好类别和偏好标签第二层计算候选产品与偏好模型的匹配度得到内容相似度得分第三层用产品热度做补偿保证新用户也有合理推荐反过来也做了“相似产品推荐”在某个产品详情页下方展示同类产品帮助用户发现备选。4.2 用户偏好建模的代码实现用户偏好分成两个维度类别偏好比如用户爱看苹果、猕猴桃和标签偏好比如用户喜欢“脆甜”、“耐储存”。通过浏览日志统计行为权重设计为view1、collect3、share5最近30天的行为权重乘以2用时间衰减模拟兴趣的时效性。from datetime import datetime, timedelta def build_user_profile(user_id, days30): cutoff datetime.now() - timedelta(daysdays) logs BrowseLogs.query.filter( BrowseLogs.user_id user_id, BrowseLogs.created_at cutoff ).all() category_weights {} tag_weights {} weight_map { view: 1, collect: 3, share: 5 } for log in logs: product Product.query.get(log.product_id) if not product: continue w weight_map.get(log.action_type, 1) category_weights[product.category] category_weights.get(product.category, 0) w for tag in product.tags.split(,): tag tag.strip() tag_weights[tag] tag_weights.get(tag, 0) w top_category max(category_weights, keycategory_weights.get) if category_weights else None top_tags sorted(tag_weights.items(), keylambda x: -x[1])[:5] return { top_category: top_category, top_tags: [t[0] for t in top_tags] }4.3 候选产品匹配与排序拿到用户偏好模型后对所有产品计算一个综合得分。打分规则我调了几版最终比较合理的权重是类别命中占50%标签命中占30%热度归一化后的销量占20%。def recommend_products(user_profile, top_n6): products Product.query.all() scores {} for p in products: score 0 if user_profile.get(top_category) and p.category user_profile[top_category]: score 0.5 tag_hit 0 product_tags [t.strip() for t in p.tags.split(,)] for tag in user_profile.get(top_tags, []): if tag in product_tags: tag_hit 1 score 0.3 * (tag_hit / max(len(user_profile.get(top_tags, [])), 1)) max_sales db.session.query(func.max(Product.sales_count)).scalar() or 1 score 0.2 * (p.sales_count / max_sales) scores[p.id] score sorted_products sorted(scores.items(), keylambda x: -x[1])[:top_n] return [Product.query.get(pid) for pid, _ in sorted_products]一开始我用的是纯标签相似度结果新用户因为没有行为数据推荐结果和随机无异。加了热度补偿项之后新用户至少能看到全站最受欢迎的产品产品转化率和页面停留时间都有改善。4.4 冷启动与兴趣漂移的处理冷启动新注册用户没有任何行为数据系统直接返回按sales_count降序排列的“热销榜单”同时在前端让用户主动选择感兴趣的品类选完立即触发一次推荐刷新。兴趣漂移用户在浏览了某类产品后如果30天内没有新行为偏好模型会自动重建。我在build_user_profile的参数里加了时间窗口就是为了避免历史兴趣长期压制新兴趣。我还在详情页做了“看了又看”模块基于当前产品的category和其他用户对该品类的浏览序列做一个简单的association统计def related_products(product, top_n3): same_category Product.query.filter( Product.category product.category, Product.id ! product.id ).order_by(Product.sales_count.desc()).limit(top_n * 2).all() related [] used_tags set(product.tags.split(,)) for p in sorted(same_category, keylambda x: len(set(x.tags.split(,)) used_tags), reverseTrue): if p not in related: related.append(p) if len(related) top_n: break return related这个逻辑在生产环境跑下来相关产品的点击率比随机推荐高了不少完全足够支撑这个体量的系统。5. 实操开发过程环境、接口、页面一把梭5.1 项目目录结构与Flask应用初始化搭建完数据模型后项目目录布局如下shaannong-push/ ├── app.py # Flask入口路由注册 ├── models.py # SQLAlchemy模型 ├── recommend.py # 推荐引擎 ├── data_clean.py # 数据清洗脚本 ├── templates/ │ ├── index.html # 首页 │ ├── product.html # 产品详情页 │ ├── recommend.html # 推荐结果页 │ └── admin.html # 管理后台 ├── static/ │ ├── css/ │ ├── js/ # ECharts相关 │ └── images/products/ ├── requirements.txt └── database.dbapp.py核心代码如下注意Flask 2.x的route写法变化3.0以后app.get和app.post更明确from flask import Flask, render_template, request, jsonify, session, redirect, url_for from models import db, Product, User, BrowseLog from recommend import build_user_profile, recommend_products, related_products app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///database.db app.config[SECRET_KEY] your-secret-key-here db.init_app(app) app.route(/) def index(): products Product.query.order_by(Product.sales_count.desc()).limit(8).all() if user_id in session: profile build_user_profile(session[user_id]) recommend recommend_products(profile) else: recommend products return render_template(index.html, productsproducts, recommendrecommend) app.route(/product/int:pid) def product_detail(pid): product Product.query.get_or_404(pid) if user_id in session: log BrowseLog(user_idsession[user_id], product_idpid, action_typeview) db.session.add(log) db.session.commit() related related_products(product) return render_template(product.html, productproduct, relatedrelated) if __name__ __main__: with app.app_context(): db.create_all() app.run(host0.0.0.0, port5000, debugTrue)5.2 推荐接口的JSON化输出首页是服务端渲染但为了让用户点击“换一批”时不用刷新整个页面我另外做了一个/api/recommend接口返回JSON数据前端通过fetch调用。app.route(/api/recommend) def api_recommend(): if user_id not in session: return jsonify({code: 401, msg: 未登录}) profile build_user_profile(session[user_id]) recommendations recommend_products(profile) data [{ id: p.id, name: p.name, category: p.category, region: p.region, price_min: float(p.price_min) if p.price_min else None, sales_count: p.sales_count } for p in recommendations] return jsonify({code: 0, data: data})接口上线前我用Postman测试了无session、无效product_id、空数据库三种异常情况都返回了明确的错误码避免前端拿到空数据后白屏。5.3 前端页面与可视化展示首页展示热销产品卡片每个卡片包含图片、名称、产地、价格区间和一句话简介。产品筛选区按category和region做下拉筛选前端用原生Ajax请求后端过滤接口。管理员后台用ECharts展示三个维度的统计图各品类产品数量占比、各产地热度排行、近30天用户浏览趋势。数据的后端接口如下app.route(/api/stats) def stats(): category_data db.session.query( Product.category, func.count(Product.id) ).group_by(Product.category).all() region_data db.session.query( Product.region, func.count(Product.id) ).group_by(Product.region).all() return jsonify({ category_stats: [{name: c, value: n} for c, n in category_data], region_stats: [{name: r, value: n} for r, n in region_data] })ECharts初始化代码要注意图表容器必须有明确高度否则渲染不出来。我踩过一次坑div的height设置为100%但父容器没有高度整个图表就是空白。5.4 管理员产品维护功能管理员后台我直接使用Flask蓝图分模块做了登录校验管理员账号密码从环境变量读取不硬编码在代码里。产品新增、编辑、下架三个操作下架使用status字段标记不物理删除数据避免推荐引擎拿到空引用。产品录入表单包含图片上传使用Flask-WTF的FileField上传后重命名保存到static/images/products下面文件名用时间戳加随机数避免中文文件名导致的路由问题。6. 部署上线与问题排查实录6.1 Linux服务器部署要点生产环境部署在Ubuntu 22.04服务器上用gunicorn作为WSGI容器。最关键的是不要把数据库文件放到项目根目录外的临时目录我用systemd服务启动指定WorkingDirectory为项目目录。gunicorn -w 2 -b 0.0.0.0:8000 app:app反向代理使用Nginx。在配置location /时proxy_pass指向http://127.0.0.1:8000并设置proxy_set_header Host和X-Real-IP否则用户日志里的IP全变成127.0.0.1。6.2 中文乱码与编码问题项目开发在Windows环境部署在Linux环境Excel清洗出的CSV文件保存时默认是GBK编码导入SQLite后中文全部乱码。解决方法是读取CSV时显式指定编码df.to_csv(products_clean.csv, indexFalse, encodingutf-8-sig)写代码时注意所有文件操作都显式传入encoding参数。这个坑从Excel到数据库再到前端页面一共出现了三次每次都排查半天。建议新项目从第一天就统一UTF-8。6.3 高流量与慢查询的应对系统上线初期日活不高SQLite完全扛得住。但有一次推广活动带来流量暴涨首页推荐逻辑要对全表产品计算得分在数千条数据量下每个请求耗时从几十毫秒涨到几百毫秒。排查后发现瓶颈在推荐引擎的products Product.query.all()每次请求加载全表数据。优化方式是给products表的category字段加索引并给推荐引擎增加缓存用户偏好模型5分钟内不重复计算。from functools import lru_cache lru_cache(maxsize128) def get_cached_recommend(user_key, versionv1): ...6.4 常见问题速查表问题描述排查思路解决方法首页图片全部裂图检查图片路径是否包含中文上传时重命名为ASCII文件名URL用url_for生成推荐结果总是同一个品类用户行为日志只有view没有collect前端埋点缺少收藏事件补上采集逻辑管理后台修改产品后推荐没变化SQLAlchemy会话未提交检查commit时机在session中加入after_commit hookECharts图表不显示容器高度为0给图表容器设置显式高度比如400pxgunicorn启动报端口占用上一进程未退出先kill对应进程再重新启动环境变量丢失导致数据库路径错误systemd配置里没有传递环境变量在service文件中通过EnvironmentFile指定6.5 安全与鉴权注意点后台登录使用Flask-Login来做密码存储用werkzeug.security的generate_password_hash和check_password_hash。千万不要把登录页做成明文密码校验农产品管理系统虽然重要级别不高但MySQL连接信息、管理员密码一旦泄露服务器上的数据就会被拖走。前端页面使用Jinja2模板的自动转义用户输入的产品描述中如果包含script标签默认会被转义不会执行。这个特性很关键开发时千万不要手工关闭autoescape。7. 项目复盘如果重新做一遍我会怎么优化这一版系统跑通后我在复盘过程中发现几个当初设计时忽略的点写出来给后来人参考。第一标签体系应该在项目初期就由业务方牵头制定而不是等数据录入后随意补充。现在tags字段里存在大量同义不同词的情况比如“脆甜”和“脆甜多汁”“红富士”和“富士”。推荐引擎虽然能跑但匹配精度上限被数据质量锁死了。第二用户反馈数据比浏览数据更有价值。这个系统只采集了浏览、收藏、分享行为没有让用户对产品打分。如果后续增加“不满意”“不感兴趣”这类负反馈推荐引擎的准确率还能提一个档次。第三产品数据接入爬虫定期更新价格信息会更高效。陕西各地农产品价格波动频繁人工维护价格会滞后。可以写一个爬虫抓取公开市场的批发报价自动更新价格字段但一定要控制抓取频率做好异常处理。这个项目验证了一个判断Python在农产品信息化这种垂直领域是很好用的不用很深的算法积累关键是数据模型设计和技术选型贴合业务场景。整套系统代码量不大重点放在推荐引擎和数据处理逻辑上最后的效果也确实体现在了用户停留时长和推荐点击率上。如果手上有类似的信息推介需求哪怕不是农产品领域这套思路依然可以直接复用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →