ARTICLE DETAIL

资讯详情

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

基于Django的农产品电商爬虫模块设计:数据采集与大数据链路实战

基于Django的农产品电商爬虫模块设计:数据采集与大数据链路实战 做农产品电商平台的时候数据来源一直是个绕不开的坎。平台方自己录入商品信息不仅效率低而且价格、供应量、产地行情这类数据更新不及时整个平台的参考价值会大打折扣。我当时的方案是围绕 Django 单独做了一个爬虫模块负责从公开渠道采集农产品价格、产地、品种、供应量这些基础数据再配合大数据技术做清洗、分析和展示这也就是基于 Django 的大数据技术的农产品电商平台设计与实现里爬虫部分的由来。这篇文章我会完整拆解这个爬虫模块的设计思路、核心实现、反爬处理、大数据链路上的数据流转以及我在实际开发中踩过的一些坑。适合正在做 Django 项目实战的学生、想给电商平台补数据采集能力的开发者以及对 Python 爬虫如何落地到 Web 项目里感兴趣的人。我不会只贴代码而是把为什么这么设计、每一步解决什么问题讲清楚这样你拿去改成别的垂直领域电商也能直接套思路。1. 项目定位与爬虫模块的整体设计思路1.1 电商平台里为什么单独拆一个爬虫模块农产品电商和普通标品电商差别很大。标品比如数码产品SKU 稳定、参数明确、价格相对固定人工维护商品库是可行的。但农产品不一样同一种蔬菜在不同产区、不同季节、不同批发市场的价格波动非常大再加上各地多个供货渠道如果完全靠运营人员手动维护数据滞后不说人力成本也完全扛不住。所以在设计这个平台时我一开始就把爬虫定位成数据采集基础设施而不是一个挂在某个页面后面的小工具。它要解决三件事商品信息的自动录入和定期更新减少人工维护成本。第三方公开行情数据的补充比如批发市场每日报价。为后续大数据分析提供原始数据源比如价格趋势、产地分布、供应量变化。这个定位决定了它的架构不能是一个独立脚本而是要嵌在 Django 项目里能够被定时任务调度、能被后台管理页面监控、采集结果能直接落到业务数据库同时还要把原始数据同步到大数据库做分析。1.2 选型Django 作为调度外壳、Python 爬虫作为数据采集为什么爬虫模块要基于 Django 而不是单独写一套框架这个选择我想多说几句。首先是复用现有技术栈。项目本身就是 Django 写的那爬虫调度、任务记录、数据展示直接用 Django 的 ORM、Admin 后台和缓存机制不需要额外引入一套调度系统。Django 的 ORM 在爬虫场景下虽然不算性能最强的但够用而且开发效率高尤其在数据量日均几万条这个量级完全能扛住。其次是便于异常处理和可视化。爬虫运行会有一堆状态需要跟踪比如任务是否成功、抓了多少条、失败多少条、耗时多久。这些记录如果只存在脚本日志里排查问题非常痛苦。放到 Django 里我可以直接建几张表来记录任务状态后台一查便知。采集端我用的是 requests lxml XPath 这套组合。Scrapy 确实功能更强有内置的下载中间件、爬虫规则和 Item Pipeline但在这个项目里很多采集源是定制化的 API 接口用 Scrapy 反而显得重。requests 配合 XPath 足够灵活遇到动态加载的页面再补 Selenium 或者直接模拟请求。这里没有绝对的对错核心逻辑是够用就好、扩展留口。1.3 大数据视角下的爬虫边界采集、清洗、入仓爬虫在整个大数据链路里扮演的是最上游的数据源角色。很多做大数据的人容易忽略一个问题模型分析、可视化大屏的前提是得有干净、完整、口径统一的数据而数据质量恰恰是靠爬虫这一层来保障的。我当时把爬虫模块划分为三个子阶段采集从目标站点拿到原始 HTML 或 JSON 数据。清洗将原始数据转换成结构化记录包括字段抽取、单位换算、去重、缺失值处理。入仓清洗后的数据写入业务库和分析库。这三个阶段并不是一次性做完就完事而是每次采集任务都要走一遍。业务库存的是平台展示用的最终数据分析库存的是用于趋势分析的明细历史数据两者数据粒度不同用途也不同。这个设计在后来的价格趋势分析中派上了大用场因为历史明细数据是做时间序列分析的基础如果只保留最后结果分析就无从下手。2. 爬虫模块的数据库设计与调度机制2.1 商品、产地、价格、爬取任务这几张表怎么设计爬虫模块的数据库设计我一共建了 5 张核心表每张表都有明确的职责边界商品表、产地表、价格行情表、爬虫任务表、原始数据表。商品表存的是农产品的基础信息包括品类、品种、规格、单位。这里要注意一个细节品类和品种是两级概念比如品类是叶菜类品种是菠菜。如果只做一张单层表后续扩展不同地区的叫法差异时会非常痛苦。产地表相对独立因为同一个产地会对应多个商品。产地字段包括省份、城市、区县、产地名称我加了一个唯一约束在省市县三级上防止重复写入。价格行情表是核心业务表记录某个商品在某天某产地供货商给出的价格包括最低价、最高价、均价、单位、更新时间。这张表的数据量会快速膨胀所以一定要按日期建索引查询效率差千万倍。爬虫任务表用来记录每一次采集任务的执行情况。字段包括任务名称、目标站点、状态、开始时间、结束时间、成功数、失败数、错误信息。这个表是排查问题的第一入口如果任务失败错误信息会直接记录在这里。原始数据表是另一种思路它把抓到的未解析数据先原样存一份。这个表一开始我觉得多余但后来证明它非常有用。因为解析逻辑如果写错了至少原始数据还在可以重跑解析而不需要重新抓取。对于反爬严格的目标站点来说这简直是救命设计。2.2 Django 定时任务与爬虫调度的衔接定时任务我用的是 Django-celery-beatCelery 负责异步执行爬虫任务Beat 负责定时触发。为什么不直接用 crontab原因是爬虫任务的执行状态需要写回数据库而且多个爬虫任务之间可能有依赖关系比如先抓取列表页再抓取详情页这些用 crontab 管理起来很别扭。调度流程是这样的Beat 根据配置的 crontab 定时发送任务到 Celery Broker我用的是 Redis。Celery Worker 接收到任务调用爬虫执行函数。爬虫函数执行前先创建一条任务记录状态为执行中。执行结束后更新任务记录写入成功/失败数量和错误信息。这里有个很重要的经验爬虫任务函数必须做超时控制。我用的是timeout参数配合信号机制单个请求超过 15 秒就放弃。因为农产品行情页面看起来简单但偶尔会出现服务器响应极慢的情况如果没有超时控制Celery Worker 会被一个慢请求卡死后面的任务全部排队。2.3 分布式采集思路多节点采集同一任务单机爬虫的瓶颈迟早会出现尤其是当采集的目标站点增多、采集频率提高之后。我在这个项目里虽然没有把分布式做到极致但设计了可以扩展的分布式采集结构。具体做法是把爬虫任务按目标站点采集类型拆分成独立任务单元发布到同一个 Redis Broker。多个服务器节点上各跑一个 Celery Worker同时监听队列。因为每个节点拿到的任务不同天然就实现了负载分散。分布式采集需要注意的问题有两个。一个是任务幂等性同一个商品同一天的价格如果被两个节点同时抓取后写入的应该覆盖先写入的或者直接判断已存在就跳过否则会出现重复数据。另一个是不要所有节点共享同一个出口 IP否则目标站点反爬会非常明显后面会专门讲代理池的问题。3. 核心爬虫实现与反爬处理3.1 requests XPath 的常规采集流程先讲一个最常规的采集流程以某个农产品批发市场的公开价格页面为例。正常思路是先请求列表页拿到每个商品详情页的链接再逐个请求详情页提取数据。但农产品行情站的页面结构通常比较简单很多时候列表页就能拿到主要字段不必再深入详情页。核心代码结构大致是import requests from lxml import html headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml, } resp requests.get(url, headersheaders, timeout15) resp.encoding utf-8 doc html.fromstring(resp.text) # 提取商品行 rows doc.xpath(//table[classprice-table]/tbody/tr) for row in rows: name row.xpath(./td[1]/a/text()) price_text row.xpath(./td[3]/text()) ...XPath 的text()函数提取文本时经常遇到返回空列表的情况原因多半是文本被嵌套在子标签里。稳妥的写法是用string(...)或者.//text()拼接。比如row.xpath(string(./td[1]))可以直接拿到该 td 下的所有文本内容虽然有时会带多余空白但清洗一下就好。还有一个容易翻车的地方是编码。农产品网站很多还是 GBK 或者 GB2312 编码直接resp.text会出现乱码。我的做法是先判断resp.encoding如果是 gb2312 之类的就用resp.content.decode(gbk, errorsignore)处理。注意 Python 的标准库里没有 gb2312 这个编码名要用gbk兼容它。3.2 动态页面的处理Selenium 与接口直取现在很多小型电商网站也在升级前端框架页面改成了 Ajax 动态加载。遇到这种页面直接用 requests 拿到的是空壳 HTML里面根本没有数据。应对办法有两个方向。第一个是用 Selenium 模拟浏览器这个方法最无脑但最重启动浏览器实例消耗资源很大多并发场景基本跑不起来。第二个是抓包找接口直接请求后端的 JSON 数据接口这个方法我强烈推荐优先尝试。找接口的办法是在浏览器按 F12 打开开发者工具切到 Network 面板重新刷新页面观察哪些 XHR 请求返回的是 JSON 数据。找到之后直接用 requests 模拟这个请求注意把请求头里的Referer和X-Requested-With带上很多站点校验这两个字段。接口直取的成功率高、速度快返回的 JSON 直接json.loads()就可以处理省掉了 XPath 解析的麻烦。唯一的风险是接口地址和参数可能会变这个只能靠定期巡检来应对检查发现解析失败就及时更新配置。3.3 反爬应对请求头、频率、验证码反爬这个点我单独拿出来讲因为太多人一上来就想着搞代理池、搞验证码识别忽略了最基本的请求头伪装。农产品行情类网站的反爬通常不会太强最基础的检查就是 User-Agent 和访问频率。我建议先做这几件事随机换 User-Agent不要一直用一个默认的 Python-requests 标识。请求间隔随机化用time.sleep(random.uniform(2, 5))避免固定频率被发现。尽量模拟正常浏览路径先访问首页再访问数据页不要上来直接请求深链。如果目标站点要求 Cookie先用 requests.Session() 保序访问让 Server 认为你是同一个会话。验证码这块我的态度是尽量规避而不是硬刚。如果某个目标站频繁弹验证码先降低采集频率或者改到凌晨低峰期采集。很多学生项目在验证码识别上交了大量学费其实绕开问题的成本远低于解决问题。3.4 数据清洗与入库的细节清洗阶段最耗时也最容易出问题。农产品数据有几个典型脏数据来源数字中混入全角字符、单位不统一元/斤 vs 元/公斤、价格字段被写成暂无或--、不同产地同一品种的名称不一致等。我的清洗函数大致逻辑是def clean_price(raw_text): if not raw_text: return None raw_text raw_text.replace(元, ).replace( , ) if -- in raw_text or 暂无 in raw_text: return None try: return float(raw_text) except ValueError: return None这里最重要的是失败返回 None 而不是抛异常。爬虫数据量大一条脏数据不应该中断整个任务。记录清洗日志最后统计一下有多少条被清洗掉如果比例异常高再回查解析规则。写入数据库时我建议用update_or_create而不是先查再写。update_or_create是 Django ORM 自带的方法如果记录存在就更新不存在就创建配合唯一约束使用效果很好。以价格行情表为例唯一约束可以设在商品 日期 来源站点这个组合上这样同一来源同一天的数据不会重复。4. 大数据链路上的数据流转与分析4.1 采集数据如何进入大数据分析流程爬虫采集并清洗后的数据最终落到 MySQL这是业务库。但如果要做大数据分析直接分析 MySQL 里的数据不是不行可是有隐患分析任务通常比较重比如算 30 天价格均值、月度价格波动率、产地供货量排名这些查询如果直接在业务库上跑会拖累线上接口的响应速度。我的做法是把分析链路拆开MySQL 存实时业务数据同时通过定时任务每天把明细数据同步到大数据平台比如 Hadoop Hive 或者 ClickHouse分析查询只在大数据平台执行。同步方式最简单的是先导出 CSV再用脚本加载到目标表。数据量如果达到百万级以上再考虑用 Sqoop 或者 DataX 之类的工具。分析结果如果还要展示回 Django 前台就把计算结果写回一张单独的结果表。前台只读结果表不直接跑聚合查询。这个分层方式在大数据量下几乎是必须的否则随着数据积累一次查询几十秒的体验谁也接受不了。4.2 数据库选型MySQL、MongoDB、ClickHouse 的取舍我在不同阶段用过不同的存储方案在这里整理一下各自的定位方便你选型。MySQL 适合存结构化强、关系复杂的业务数据比如商品、订单、用户。它的优势是生态成熟、事务支持好、Django ORM 无缝对接。劣势是海量明细细分数据的聚合查询性能一般几百万条数据做 group by 就会开始吃力。MongoDB 适合存原始解析前的 JSON 数据schema 灵活采集字段变了不用改表结构。但 MongoDB 不适合做复杂的关联查询事务能力也比较弱和 Django ORM 对接要多加一层。ClickHouse 是分析型数据库列式存储做聚合查询非常快。一亿条数据做 group by 也能秒级返回。缺点是它是为分析而生的不适合频繁的单条更新也不适合当业务主库用。我当时业务库用 MySQL大数据分析库用 ClickHouse原始数据暂存 MongoDB三层各司其职。如果你只是课程设计级别的项目数据量不大MySQL 一份数据就够用分析型数据库可以先不引入但你要明白这个架构演进的方向。4.3 简单的价格趋势与产地分布统计大数据分析落到具体应用上最直接的是价格趋势分析和产地分布统计。价格趋势用来给用户展示过去 30 天某种蔬菜的价格走势产地分布则展示某商品的货源分别来自哪些省份、占比多少。这两个分析在 ClickHouse 里的 SQL 非常简单SELECT toDate(price_date) AS date, avg(avg_price) AS avg_price FROM price_daily_detail WHERE product_code 01 AND price_date today() - 30 GROUP BY date ORDER BY date;产地分布则是SELECT province, count() AS supply_cnt FROM price_daily_detail WHERE product_code 01 AND price_date today() - 30 GROUP BY province ORDER BY supply_cnt DESC;分析结果每天定时写入 Django 的结果表前台页面直接读结果表渲染图表。这样用户看到的图是秒开的底层的重型查询在凌晨就完成了。很多做可视化的人容易忽略这一点前台的「快」不是靠优化查询而是靠提前算好。5. 常见问题与排障实录5.1 爬虫任务卡死、请求超时的处理我遇到最头疼的问题是爬虫任务看起来在跑实际上卡死了。表现是 Celery Worker 进程存在但任务状态一直没有更新日志也没有新输出。原因通常是 requests 没有设置 timeout对方服务器连接挂起线程一直在等响应。排查步骤是先看 Celery 日志最后一条输出是什么能判断卡在哪个请求上。检查该请求的目标 URL 在浏览器里能不能正常打开。确认代码里是否所有请求都设置了 timeout。用lsof -p worker_pid看这个进程当前打开的连接能确认是否卡在某个 socket 上。修法分两层第一层是请求层设 timeout第二层是任务层用信号超时兜底。我写了一个通用的抓取函数所有请求默认 timeout15并在函数外层包了超时保护超过 30 秒直接抛异常让任务失败失败后自动记录到任务表方便人工排查。5.2 重复数据与增量采集重复数据是爬虫项目最容易出问题的点。我刚开始做的时候也碰到过一天跑下来价格表多了 30% 重复记录后来靠update_or_create配合唯一约束解决了。增量采集的思路稍微复杂一点。农产品价格是按天更新的理论上每天只需采集一次当天的数据。但有些站点当天数据更新不及时比如上午显示的还是昨天的价格下午才更新成今天的。我的策略是每天分时段采多次具体采集时段和站点更新时间对齐但写入时始终用唯一约束去重最终保证库里只有一份当日数据。重跑历史数据的情况也要考虑。如果某天采集任务挂了第二天需要补采前一天的数据所以在写解析逻辑时要保留指定日期的入参这样一个函数既能采当天数据也能补历史数据。5.3 IP 封禁与代理池管理访问频率过快被封 IP 几乎每个人都遇到过。封 IP 的迹象是本来正常的请求突然返回 403 或者跳转到验证码页面而且用浏览器访问同一页面又是正常的。规避办法从简单到复杂排列调低频率、使用代理、构建代理池。对于这个项目我先调低了频率每天的采集任务分散到 3~4 个时段执行不要集中跑完效果已经好了很多。如果确实需要高频采集就搭建一个代理池用 Redis 维护一批代理 IP每次请求前从池里随机取一个失效就剔除。代理池虽好但不要迷信代理。农产品行情类网站的数据更新频率本身不高完全没必要把采集频率拉到秒级慢一点采集反而更稳被封的概率大幅降低。稳定压倒一切。5.4 Django 数据库连接池与并发写入当爬虫任务并发提高、入库频率上来之后Django ORM 的数据库连接管理会成为一个瓶颈。Django 默认的 ORM 连接是请求时创建、请求结束关闭在爬虫这种高并发写入场景下频繁创建连接的成本非常高容易出现连接数耗尽的问题。我用了django-db-connection-pool来改造成连接池模式DATABASES { default: { ENGINE: pool, ... POOL_OPTIONS: { POOL_SIZE: 10, MAX_OVERFLOW: 10, RECYCLE: 600, }, } }POOL_SIZE 是核心连接数MAX_OVERFLOW 是在高峰期额外创建的连接数RECYCLE 是连接回收时间。不要一上来就调很大连接数太大会反过来拖垮数据库。同时并发写入时要注意用transaction.atomic()控制事务边界避免一部分数据写入成功另一部分失败导致的数据不一致。5.5 常用排障指令速查这里整理一下我日常排查爬虫问题会用到的指令很基础但很实用# 查看 Celery Worker 日志 tail -f /var/log/celery/worker.log # 查询最近一次任务执行时间和状态 python manage.py shell -c from apps.crawler.models import CrawlTask; print(list(CrawlTask.objects.order_by(-create_time)[:5].values_list(name,status,create_time))) # 查看 Redis 队列中积压的任务数 redis-cli llen celery # 查看当前是否有进程还在抓取数据 ps -ef | grep celery6. 部署链路与后续扩展的几点经验6.1 从开发到生产的部署要点开发环境下爬虫和 Django 跑在同一台机器没问题但生产环境我建议至少拆成两台一台跑 Django Web 服务一台专门跑爬虫 Worker。原因很简单爬虫对带宽和 CPU 的占用波动很大跟 Web 服务抢资源会导致页面响应变慢。部署的大致链路是代码走 Git 仓库Web 服务用 Gunicorn Nginx爬虫 Worker 用 Supervisor 托管。定时调度由 Django-celery-beat 负责Beat 进程和 Worker 进程分开启动配置文件用.env管理不同环境下的参数。采集任务的启动要做好手动触发优先的设计。也就是说上线初期不要完全依赖定时任务每一步操作先手动跑一遍确认无误后再挂到定时任务上。我第一次部署就是因为 Beet 配置写错了时间凌晨采集任务根本没跑一觉醒来数据全是空的。6.2 日志、监控与任务重跑日志的重要性怎么强调都不过分。爬虫不像是 Web 接口出了问题用户会立刻感知爬虫很多问题是悄悄发生的任务跑了但解析失败、数据入库了但字段全是空、目标站点改版了 XPath 全部失效。如果没有完整的日志和状态监控这些问题会积累到不可收拾才被发现。我的做法是三重保障任务表记录每次任务的运行摘要、日志文件记录详细错误堆栈、简单告警通过邮件或者钉钉机器人推送到工作群。任务重跑功能则设计成可挑选任务时间和任务名称一键重跑指定时间段的失败任务不需要改代码。6.3 后续扩展爬虫管理平台化做到最后我认为爬虫模块不应该只是一个隐藏在 Django 内部的功能它可以演进为一个小型的管理平台。具体来说可以做采集源配置管理目标站点的 URL、XPath 规则、采集频率都要支持后台配置采集任务的启停、重跑也要能在管理页面点按钮完成数据监控要能从后台直接看到采集量曲线、失败率曲线。这个进化路线的核心是把规则从代码中抽离出来。当爬虫规则写死在代码里时每次目标站点改版都要发版上线当规则变成数据库配置后运维人员改配置就能完成调整。数据采集规模化之后这条路几乎是必然要走。我在实际开发中最深的体会是爬虫的真正难点从来不是你抓不抓得到数据而是抓到之后能不能稳定、干净、及时地流转到下游。前面花在数据库设计、任务调度、异常处理上的时间后面都会加倍还给你。你可能会觉得这些事不够酷但生产环境里决定一个数据采集系统能不能长期跑的恰恰就是这些不酷的细节。
返回列表