30天挑战Day8:7小时打造价格趋势数据采集与清洗管道

发布时间:2026/10/3 3:41:50
30天挑战Day8:7小时打造价格趋势数据采集与清洗管道 这个标题看起来不像一个正经项目名倒更像是我自己复盘笔记里的一行记录。实际上它就是我在一次为期30天的个人开发挑战中第八个工作日的真实写照下午两点开工晚上九点收工整整7个小时全部投入到了一个数据采集与清洗的小项目里。这段时间我没有刷手机、没有开多余的浏览器标签页就是一台笔记本、一份接口文档、一杯水从白天写到晚上。今天这篇不是教程式文章而是把Day8这一天完整拆给你看我为什么把时间卡在14:00到21:00这7小时里到底做了什么、遇到了哪些坑、最后产出了什么。如果你也在做类似的个人项目或者正打算给自己安排一个连续的开发冲刺日这篇应该能帮上忙。1. 项目背景与Day8的定位简单交代一下这是个什么项目。我给自己定的30天挑战是做一个“轻量级价格趋势观察站”从几个公开的商品数据接口抓取指定商品的日常价格和销量信息存到本地数据库里再通过一个简单的Web页面做趋势展示。这件事的难点不在于单个环节而在于把采集、清洗、存储、展示这一整条链路跑通并且保证每天新增的数据是干净、连续、可用的。Day8这个节点很关键。前面7天我完成了需求确认、数据源连通性测试、表结构设计还有一版能跑通但写得很粗糙的采集骨架。到了Day8我要做的是把之前那些零散代码整合成“能稳定运行”的第一版处理好在采集过程中真实会遇见的各种脏数据比如字段缺失、类型错乱、重复记录再把清洗结果规规矩矩地落进数据库。选择14:00到21:00这个时间段其实是有讲究的。上午的时间我用来处理别的事务比如回复消息、整理前一天的记录让大脑先热起来真正需要大块专注力的编码工作放在下午两点以后精力最稳的时段。21:00收工是因为晚上大脑不适合继续做高强度的逻辑拼杀再拖下去容易出现“写一堆Bug然后花双倍时间修”的局面。1.1 为什么Day8必须做“收口”很多人做个人项目最容易卡在“前7天一直在铺路却迟迟没有产出”的阶段。D1到D7我会反复确认数据源稳不稳定、字段含义对不对、环境版本对不对这些都是必要的前置工作但我给自己下了一条死线Day8之后项目必须进入“每天有可运行版本、看得见数据在积累”的状态。这7小时的核心任务就是收口把采集脚本从“实验性代码”收敛为“有点工程感的代码”把零散的数据修修补补变成规整的入库记录。这一步迈过去后续的Day9到Day30就全是迭代优化迈不过去整个挑战就会陷入无限改代码的泥潭。我给自己定的Day8验收标准有三条采集脚本能连续运行1小时不崩溃、不重复入库清洗逻辑能自动处理至少5种常见脏数据情况数据库里的数据能被查询且趋势图表能正常画出来这三条标准不复杂但每一条都意味着要跟真实的网络状况和数据结构打交道。接下来的7个小时我就是在为这三条结果服务。2. 技术选型与方案设计为什么用这套组合做这个小项目之前我有过几个候选方案这里说说我最后选了什么、为什么以及它们分别解决什么问题。2.1 采集层requests还是httpx现在的Python生态里请求库最常用的就是requests和httpx。我这个项目里选的是requests原因很实际虽然httpx支持HTTP/2和异步但我这里的数据源接口是普通的HTTPS JSON接口并发要求也不高没必要引入异步复杂度。requests足够稳定生态里随便一搜就有大量踩坑案例遇到问题容易排查。我的采集逻辑很简单按照固定参数构造请求拿到JSON之后先做一层简单校验如果必要字段缺失就直接标记为异常数据交给后面的清洗阶段处理。考虑到目标数据源有访问频率限制我在请求里加了一个轻量的限频控制每次请求之间随机sleep 2到5秒。这个做法不是为了装模作样而是真实出现过连续请求后被临时封IP的情况。import requests import time import random import json HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_item(item_id: int, base_url: str) - dict: url f{base_url}/item/{item_id} try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() data resp.json() if not data or price not in data: return {item_id: item_id, error: missing_price} return data except requests.Timeout: # 超时单独标记方便后面重试 return {item_id: item_id, error: timeout} except requests.RequestException as e: return {item_id: item_id, error: str(e)} def collect_with_rate_limit(item_ids, base_url): result [] for item_id in item_ids: result.append(fetch_item(item_id, base_url)) time.sleep(random.uniform(2, 5)) return result那段sleep在很多人看来浪费了不少时间但它换来了采集过程整体的可靠性。实际跑下来1小时内采集了大概800个商品的快照没有一次因为限频失败。2.2 清洗与存储pandas加SQLite为什么不用重型数据库存储这块我选了SQLite而不是MySQL或者PostgreSQL。这个决策非常直白单人使用、数据量一天几千条、没有并发写入需求SQLite的单文件特性让我备份和迁移都极其方便。我只要把这个.db文件扔到云盘或者另一台机器上整个项目的数据基础就同步了。清洗层用pandas是因为它的DataFrame操作对“表格型脏数据”的处理效率太高了。比如有百分之十几的商品价格字段是字符串格式“¥1299”还有一些是None这类问题用pandas的to_numeric和dropna组合就能一次性处理完。import pandas as pd def clean_price_column(df: pd.DataFrame) - pd.DataFrame: # 把带货币符号的字符串转成纯数字 df[price_clean] pd.to_numeric( df[price].astype(str).str.replace(r[^0-9.], , regexTrue), errorscoerce ) # 价格不可能为负数或异常低值 df.loc[df[price_clean] 0, price_clean] None return df为什么清洗必须做在入库之前而不是之后因为在写入数据库之后再修你得先查出来、再改、还要担心有没有被其他逻辑读到。前置清洗虽然多了一点运行时间但保证了数据库里面存的都是可用的“干净状态”后面做任何统计都不需要再自欺欺人地加一堆where条件去过滤异常值。2.3 为什么没用爬虫框架比如Scrapy偶尔会有人问我都抓数据了干嘛不用Scrapy我的经验是Scrapy的工程体系很完善但它的学习成本和项目结构成本对这个体量来说太重了。那个框架更适合需要跑分布式、中间件、管道高度定制的大规模采集任务。我这个项目只需要单机、定时、轻量采集用requests加一个schedule库就够了。不过我也保留了扩展空间如果未来要抓的数据源增加到十几个或者要做增量爬取我会把代码重构成真正的“管道模式”而现在这个阶段直接、能跑、容易改是最高优先级。3. 实操过程7小时的完整推进记录下面这段是Day8的重头戏我把这一天里不同时间段实际做了的事情、看到的报错、改过的代码按时间顺序原原本本列出来。你可以把它理解成一份“带有时间戳的开发者日志”。3.1 14:00-14:30 环境状态检查与任务清单梳理开工第一件事把昨天的进度重新看了一遍。我习惯在工作开始前把前一天的代码跑一遍确认“昨天看起来正常的东西今天运行起来也正常”。这样能避免带着一个隐藏问题开始新一天的工作。我打开项目目录用python run.py --dry-run跑了一遍采集模块的冒烟测试。这里有个小技巧我给采集脚本加了一个--dry-run参数它只会请求前10个商品的数据不会写库专门用来验证网络和代码路径是否正常。结果发现一切正常接口返回的JSON都能被成功解析。然后把当天7小时按30分钟粒度切成小块写在一个便签上14:30-16:00 完整采集脚本的健壮性改造超时、重试、异常标记16:00-17:20 清洗模块重构把分散的清洗逻辑汇总成统一clean_pipeline17:20-17:40 暂停起身活动17:40-18:00 代码走查重点看数据库写入与去重逻辑18:00-18:30 晚餐18:30-20:00 数据库表结构调整与历史数据回填20:00-20:40 统计脚本与看板接口联调20:40-21:00 收尾检查记录当日进度与明日计划这个清单的好处是每一项任务都有明确的时间盒不会出现“哎呀已经晚上八点了我还在调试一个小Bug”的局面。3.2 14:30-16:00 采集脚本健壮性改造上午之前写的那版采集脚本在“网络一切正常”的时候很好用但只要遇到超时、返回非JSON、字段名变了整批采集就会中断。这种代码在实验阶段没问题但作为要连续跑30天的脚本绝对不能接受。我做了三件事统一的异常捕获分支、自动重试机制、错误数据的结构化记录。异常捕获方面我把可能出现的错误分成Timeout、ConnectionError、HTTPError、JSONDecodeError四类每类错误都单独记录到错误日志里。自动重试对齐的是“临时抽风”如果一个请求超时或返回5xx我最多重试2次每次间隔递增3秒和10秒。如果重试2次仍然失败就放弃这条数据并写明原因。这个设计让我在当天晚上回看日志时能一眼看出“哪些商品是暂时失败、哪些是彻底失败的”。有些商品接口已经下线再重试100次也白搭这种我把它单独放进一个停滞清单等后面做数据源替换时统一处理。import logging import time logging.basicConfig(filenamecollector_errors.log, levellogging.INFO) MAX_RETRIES 2 def fetch_with_retry(item_id, base_url): for attempt in range(MAX_RETRIES 1): try: data fetch_item(item_id, base_url) if error in data: raise RuntimeError(data[error]) return data except RuntimeError as e: wait_time 3 if attempt 0 else 10 logging.info(item %s retry %d after error: %s, item_id, attempt 1, e) time.sleep(wait_time) return {item_id: item_id, error: permanent_failure}改造完成后这段代码里的逻辑我再也不用动了每次采集任务都是从头到尾跑一遍失败的数据单独落库不影响整条链路的运行。3.3 16:00-17:20 清洗模块重构告别按摩尔定律堆if之前那版清洗代码是需求还没完全清晰的时候写的东一块西一块有的地方判断空值有的地方清理价格符号有的地方规范时间格式。功能分散在各处改一处就会踩到另一处的雷。所以这一小时多一点的时间我把它们归并成一个统一的clean_pipeline。这个pipeline的思路非常简单就是把所有产品和商品的原始数据都当成一张“宽表”按固定顺序做六件事去重同一商品同一采集时间只保留一条类型转换把价格、销量等数值字段统一转成float缺失值处理可推导的通过上下文补齐不可推导的标为None格式统一时间字段统一成YYYY-MM-DD HH:MM:SS范围校验价格、销量不能为负数不能是超过合理阈值的离群值业务规则比如下架状态的商品价格直接置空而不是留个0这么重构完以后维护的负担一下子小了很多。因为每一件脏数据问题都有了明确的处理位置新增一种异常只需要在这六个步骤里找到对应的环改一小段就好。3.4 18:30-20:00 表结构调整与历史数据回填吃完晚饭重新坐下我本来以为可以顺利联调了结果打开数据库一看发现了一个棘手的问题之前设计表结构时把商品分类名称写成了定长字符串但实际数据里最长的一个分类名称超过了设定长度结果入库时候直接被截断了。不仔细看大部分数据都能对应上一旦分析细分类别的价格差异就会有若干商品被归到一个“残缺分类”里。解决起来不复杂但比较繁琐修改表结构定义把字段改成TEXT类型然后对已有的历史数据进行数据订正。好在Day8之前的数据量不算大整个表只有三千多条我写了个一次性脚本从原表读出原始字段重新走一遍清洗逻辑再写入新表。这里有一个值得记下来的经验给字符串字段定长度一定要用现实中见到的最长值再乘以1.5。不然仅仅以为“差不多了”早晚会被真实数据打脸。历史数据回填时我顺便把价格字段的精度统一成了两位小数还加了一个recorded_at的索引。索引这个东西虽然暂时数据量小看不出太大优势但等跑到第二十天要按日期分桶聚合统计时就会快得多现在加上只需要一行DDL以后省掉重构的麻烦。3.5 20:00-20:40 统计脚本与看板接口联调当天的表结构和历史数据都准备就绪之后我开始联调统计模块。这个模块负责按天计算每个商品的平均价、最高价、最低价、样本数然后输出JSON给Web页面画趋势图。联调过程中发现了一个之前埋下的隐患统计脚本里有一个假定认为同一天内一个商品只会有一条价格记录。但实际采集过程中如果一条采集任务被手动触发两次就可能导致同一天出现多条记录。如果不去重平均值就失真而且这个失真刚开始非常隐蔽等到累积很多天以后才暴露排查起来就得靠翻操作日志了。我在统计脚本里加了一个“显式的当日分组去重”步骤按item_id和日期分组同一组内如果有多条记录按recorded_at排序只保留最新一条。这样即使之前的历史库里有多余数据统计出的结果也不会受到污染。看板接口的联调整体比较顺利前后端对接的数据字段很快就打通了。到20:40时我已经能在浏览器里看到几款核心商品的最近一周价格趋势曲线虽然线条还很简单但看到自己的数据经过采集、清洗、入库、统计这一整套链路最后变成了可视化的图表这感觉还是很爽的。3.6 20:40-21:00 收尾检查与当日复盘最后20分钟我没有开新需求而是把全天的工作仔细过了一遍。第一步是跑一遍全量测试脚本确认采集模块、清洗模块、统计模块三个环节的测试全部通过第二步是登录到生产环境其实就是同一台电脑上的另一个Python进程手动触发了一次全量采集任务观察它完整跑完并且数据正确入库第三步是写工作日志记录今天改过的每一个关键文件、踩过的每一个坑、留下的所有TODO。顺手也把今天遇到的几个容易再犯的问题写进了项目根目录的REMINDER.md这一条我会在第四部分展开。4. 常见问题与排查技巧实录这一部分我想把这些天真实踩过的坑总结成一个速查表。如果你是第一次做这种长期采集项目这个表大概率能帮你少走好多弯路。4.1 采集阶段的坑超时是最高频的问题也是最让人头疼的。接口偶发超时随即重两次就成功这种算是运气好。真实世界里超时往往集中于某个IP段或者某个时间窗口比如目标服务器每天凌晨会有定时任务导致凌晨时访问特别慢。如果你打算让采集脚本在凌晨运行一定得在代码里增加对超时频率的统计如果连续失败超过一定阈值就要主动停止任务并向日志报警而不是傻傻地重试一整晚。还有一个非常常见但经常被归咎于“玄学”的问题同一个接口用浏览器访问能得到完整的数据但用requests访问却少了一些字段。这种通常就是User-Agent的问题有些服务端会区分普通浏览器和脚本客户端。把User-Agent伪装成一个常见的浏览器版本就能解决同时不要带过多的自定义Header避免显得太明显。4.2 清洗阶段的坑清洗环节最能暴露“对数据不尊重”的后果。我在Day8当天遇到的最典型问题是某个商品的价格偶尔会出现“缺失但前一天的同一商品有价格”的情况。如果简单粗暴地填充为前一天的价格趋势图上会出现一条虚假的平直线如果直接置空则会导致这天的平均价格骤降。这两种结果都会让图表带上人为的误导。我的处理方式是用中位数而非均值做缺失值填充同时保留一个is_imputed字段标记哪些数据是填充来的。这样在看板展示时可以明确知道哪些点是真实数据、哪些是估计值避免误导自己。4.3 数据库层的坑SQLite虽然轻量但如果你在一个连接上同时执行很多写入操作很容易遇到database is locked错误。这个错误看起来吓人其实只是某个事务迟迟没有提交阻塞了另一个写入。解决办法是给每次写入都加上短小的超时设置并且串行执行写操作不要开启多个连接同时写。这个坑我在Day8回填历史数据时撞了个正着回填脚本并发开了5个线程写SQLite结果好几个线程一起报locked错误。我最后把写入改成单线程串行速度虽然慢了一点但再也没出现过锁冲突。4.4 日常开发效率的坑还有一种坑不是来自代码而是来自工作节奏。那天我从14:00硬坐到17:00连续三个小时盯着屏幕结果到了16:45左右明显感觉注意力在快速下降改一处代码会引入另一个无关变量看一个报错两分钟后忘了自己刚才在干嘛。后面我强制自己在每个休息节点起身做几个伸展动作、看一眼窗外让眼睛彻底从屏幕上移开。别看这件事不起眼它对后半段工作效率的保证太关键了。Day8后半段还能保持清晰思路很大程度上就是靠这些短暂的“物理抽离”来把大脑缓存清空。4.5 一个通用的排查思路如果你在自己做项目时遇到某个环节的Bug不要第一时间扎进代码里逐行看而是先确认“是输入数据的问题、环境的问题、还是逻辑的问题”。用这个顺序排查通常比盯着源码发呆高效得多先看原始返回数据和浏览器对比一下是不是一致再确认依赖库版本和Python版本是否匹配最后才是代码逻辑里的变量推断和条件分支。Day8那天修的最快的一个Bug就是这样定位的统计结果里有个商品的价格突然变成了昨天的两倍我当时第一反应是统计代码出了问题结果打开原始JSON一看是数据源返回的价格字段含义“单位变了”之前是单个价格现在变成了整箱价格。这种业务规则层面的波动代码逻辑再完美也防不住只能靠对数据的敏感度去发现。5. 时间块这件事7小时持续输出的管理心得最后一部分不讲代码了说说在那7个小时里我对自己工作方式的一些观察。14:00到21:00这个时间跨度其实比多数上班族的一整天工作时间都要长但如果分区得当它并不会让人筋疲力尽。我把它分成了三轮能量波。第一轮14:30到17:20属于深度编码时段用来处理最需要动脑的任务第二轮18:30到20:00属于中强度工程推进时段做一些偏机械但需要细心的表结构修改和历史数据回填第三轮20:00到21:00收尾沟通和验证这个阶段不再开启新技术任务约束自己只做检查和记录。每一轮之间都必须有真正的休息。我说的休息不是从写代码切到刷短视频而是离开屏幕、让注意力彻底散掉。短视频和无限流信息流只会让大脑持续接受刺激结果就是下一轮开始时没办法快速进入专注状态。休息时就做点无聊的事在屋里转一圈或者只是坐在椅子上发呆几分钟接下来反而更容易进入心流。还有一条很适合长期项目冲刺的准则不要在一个时间块里同时维护多个工作分支。Day8下午我一度想趁着手感热同时把采集脚本和后续要用的定时任务也一起写了满脑子都是“反正都打开了”。结果就是两边代码都只写了一半接不上茬白白浪费了20分钟在切换状态上。后来我强制自己一个时间盒只能有一个核心目标其他想法统一记在便签上等这个任务完成后再处理。写到这里Day8这一天就算真正落下帷幕了。从下午两点的环境检查到晚上九点的复盘日志这7小时填补的不仅是一段代码通路更是让我对这个项目有了更强的掌控感数据开始有规律地流动脚本变得稳定可信链路中的每一个环节我都知道它为什么存在、出了问题该去哪里找。如果你也在做自己的长期个人项目我希望这段记录能给你一些参考。我们不缺开始时的热情真正难的是在第八天、第九天、第十天状况接二连三地冒出来时仍然愿意坐下来一个一个把它们解决掉。Day8没有奇迹它只是把前面攒下的零散积累认真做了一次收口而正是这次收口让整个项目从此进入了一个可以持续积累的轨道。