码农高天
用原始字典传递抓取数据极易因拼写错误静默写入空值,应引入Pydantic强类型对象在网络解析层阻断异常并校验格式。
AI-assisted summary of 【Code Review】传参的时候有这么多细节要考虑?冗余循环变量你也写过么? — original source. Check the original for full context.
Source published:
Key ideas
- 针对开源爬虫项目 MediaCrawler,高天指出了命令行参数校验延迟、隐式动态属性注入以及枚举类型未做强约束等入口设计缺陷。 (0:00)
- 配置文件中的数据库凭证、账号密码及 Cookies 等敏感数据必须剥离出明文代码库,改由环境变量动态读取。 (5:00)
- 使用 asyncio.Semaphore 控制并发时若直接把任务全量塞入列表,成千上万个挂起 Task 仍会消耗大量内存,大规模抓取需结合任务队列控制生成节奏。 (10:00)
- 用原始字典传递数据易因 dict.get() 静默返回 None 导致拼写错误被掩盖,建议引入 Pydantic 强类型模型在接口层进行解析与校验。 (15:00)
Detailed notes
## Background 本期视频由 YouTube 频道「码农高天」于 2024 年 3 月 31 日发布。视频内容是针对其知识星球学员开源的异步爬虫项目 MediaCrawler 进行的 Code Review,旨在通过剖析实际代码细节帮助开发者提升工程质量与 Python 编写水平。
## TL;DR 高天围绕开源爬虫库 MediaCrawler 的入口与核心模块展开代码审查,首先指出命令行参数应在前端通过 choices 严格校验与解耦传参,避免把配置动态塞进对象或传递 Namespace。接着,他分析了敏感配置切勿明文硬编码、`while` 循环中冗余计数变量的化简,以及信号量限制并发时预创建海量 Task 可能造成的内存隐患。最后,高天针对“用原始字典传递一切”容易引发拼写错误静默失败的问题,强烈建议在网络返回端引入 Pydantic 构建强类型数据模型,以获得更好的类型提示与运行时安全保障。
## Atmosphere & style 视频整体呈现出一种专业严谨又亲切幽默的技术交流氛围。高天在指出“魔法数字”、冗余循环变量和字典传递隐患时直言不讳、犀利入理,但语调始终平易近人,并穿插着“手一抖就把密码发出去”等生动的工程实战段子。在层层抽丝剥茧之后,他对作者扎实的工程能力给予了由衷肯定与鼓励,让观众既能学到扎实的工业级重构技巧,又感到备受启发与鼓舞。
## Bulletin - [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 针对开源爬虫项目 MediaCrawler,高天指出了命令行参数校验延迟、隐式动态属性注入以及枚举类型未做强约束等入口设计缺陷。 - [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 配置文件中的数据库凭证、账号密码及 Cookies 等敏感数据必须剥离出明文代码库,改由环境变量动态读取。 - [10:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=600s) 使用 asyncio.Semaphore 控制并发时若直接把任务全量塞入列表,成千上万个挂起 Task 仍会消耗大量内存,大规模抓取需结合任务队列控制生成节奏。 - [15:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=900s) 用原始字典传递数据易因 dict.get() 静默返回 None 导致拼写错误被掩盖,建议引入 Pydantic 强类型模型在接口层进行解析与校验。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 命令行参数应通过 argparse 的 choices 参数限定合法枚举值,并尽早执行 parse_args(),避免非法输入执行前置初始化后才在下游报错。 - [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 静态工厂方法(@staticmethod)应直接使用类名调用,无需多余地先实例化工厂对象。 - [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 反对在初始化配置时使用 **kwargs 与 setattr 动态绑定属性,此举破坏了代码可追溯性并容易掩盖拼写错误,应采用显式命名传参。 - [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 不要将 argparse.Namespace 对象整包穿透到底层核心逻辑中,应在上层解构为独立业务变量后再传递,解除核心库对解析器的耦合。 - [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 模块导入应严格按标准库、第三方库与本地模块分层,且各层内部应按字典序排序,避免使用通配符导入(from module import *)。 - [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 数据库密码、账号凭据及 Cookies 等隐私敏感信息绝不能明文保存在代码库或模板中,最佳实践是通过环境变量读取。 - [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 业务命名应保持一致性,例如在小红书全链路语境与代码注释中均使用 note 时,对应函数名也应统一命名为 search_notes 而非 search_posts。 - [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 应精简 while 循环中的控制变量,消除无意义的递减计数器和魔法数字,通过单循环变量与终止条件合并提升代码简洁度。 - [10:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=600s) 局部变量命名无需使用私有前缀下划线(如 _semaphore),代码命名规范应符合语义场景。 - [10:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=600s) asyncio.Semaphore 只能限制协程的执行并发度,无法阻止外层任务列表瞬间全量实例化带来的内存占用,超大规模抓取应采用队列机制按需消费。 - [10:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=600s) 在层级间流转原始字典极度危险,dict.get() 遇到拼写错误的字段只会静默返回 None 并存入数据库,导致关键字段在无报错状态下隐蔽丢失。 - [15:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=900s) 建议在拿到网络响应数据的第一时间使用 Pydantic 等库进行强类型建模,利用属性访问自动触发 AttributeError 报错并享受 IDE 补全与数据校验。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=0s) 入口设计:命令行参数校验、报错一致性与配置注入
这一期 Code Review 的对象是知识星球学员开源的一个爬虫库项目(MediaCrawler),该项目支持小红书与抖音的爬取,已有 100 多个 Star,整体结构已初具雏形。作者希望针对 `media_platform/xhs/core.py` 提出改进建议。高天在阅读核心文件之前,首先检查了入口文件 `main.py`。该入口直接通过 `asyncio.run(main())` 启动一个异步函数,其顶部通过标准库 `argparse` 进行命令行参数解析。
高天指出的第一个问题是命令行参数的校验时机与报错体验。在原始代码中,`platform` 和 `lt`(即 `login_type`)本质上都是枚举类型,但参数解析器却将它们当成普通字符串直接接收。如果用户传入了无效的平台名称,程序会先完成一系列没有意义的前置初始化,直到代码逻辑深入到下游分支时才会抛出 `ValueError`。更糟糕的是,下游错误提示信息写着“我们支持的 platform 只有小红书或者抖音”,其中抖音直接写了中文全称,而参数实际期望的缩写是 `dy`;如果用户按照报错提示真的输入了“抖音”,依然会再次报错,导致用户极度困惑。因此,应直接在 `add_argument` 中通过 `choices` 明确限定允许的值,并将 `parse_args()` 移到所有业务操作之前,一旦传参非法即可在最前端拦截,避免做无用功。
紧接着,高天指出在调用工厂方法时的 Python 惯用法问题,并对随后的配置注入方式提出重构意见:
```mermaid flowchart TD accTitle: Command Line Argument Parsing and Crawler Initialization Flow accDescr: 展示命令行参数如何从 argparse 校验并传递至爬虫实例的正确与错误设计对比。
subgraph Bad["原方案:隐式依赖与弱校验"] A1["argparse.parse_args() 接收纯字符串"] --> B1["延迟到业务深层才报 ValueError"] B1 --> C1["传递 argparse.Namespace 进 init_config"] C1 --> D1["内部使用 setattr 动态赋值"] end
subgraph Good["推荐方案:前端强校验与显式解耦"] A2["argparse choices=['xhs', 'dy'] 限制入参"] --> B2["非法入参立即阻断并清晰报错"] B2 --> C2["类方法直接创建实例:CrawlerFactory.create_crawler()"] C2 --> D2["显式解耦传参:init_config(platform=..., login_type=...)"] end ```
在创建爬虫对象时,作者使用了工厂模式,但由于 `create_crawler` 本质上是一个静态方法(`@staticmethod`),完全没必要先实例化一个工厂对象再调用,直接使用 `Class.create_crawler` 才是更符合 Python 习惯的写法。而在爬虫对象初始化配置 `init_config` 中,原作者使用 `**kwargs` 配合 `setattr` 进行动态属性注入。高天明确反对这种写法,认为其灵活度过高,极易掩盖拼写错误(例如将 `cmd_args` 误打成 `cmd_arg`,可能运行很久之后才崩掉),并且导致代码在阅读时缺乏可追溯性——读者在第 58 行看到 `self.cmd_args`,却根本找不到这个属性到底是在何处定义的。此外,原代码直接将 `argparse.Namespace` 对象整包传入 `init_config`,在核心库中引入了不必要的参数解析器依赖。正确的做法是将 `Namespace` 在 `main` 函数处解开,将 `platform` 与 `login_type` 显式声明为命名参数传递并赋值给具体属性。
- “我跟大家说过我是不相信设计模式这种东西的但是就在这个地方而言由于你这个create crawler是一个static method所以在这里你并不需要建立一个object你可以直接用这个class的CrateCrawler这个相对来说呢是更排算的写法” - “我强烈推荐把输入显示的传进来 在显示的复制给自己的属性上这样做还有一个好处就是当你其他函数调用这个initconfig的时候他会有更好的提示” - “不要传一个ArgParse的namespace这里给 input 的时候直接把需要的数据拿出来不要传一个 args”
---
[05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 代码规范、配置敏感信息隔离与循环变量冗余
在进入爬虫执行流程前,高天提及了模块导入(`import`)的规范问题。该项目的分区结构大体正确(依次为 Python 内置标准库、以 Playwright 为代表的第三方库、本地模块;每个分区内部先写 `import` 后写 `from... import`),但还缺失了关键的一步:每个子分区内部未按字典序排序,导致不够规范整洁。同时,针对仅抛出两个自定义异常的模块,应当直接显式导入具体的异常类,而不是使用通配符导入(`from module import *`),更没有理由为这几个变量拆分成稀疏的四行。
随后分析的是爬虫的 `start` 函数。该函数获取代理信息后启动 Playwright 打开浏览器。项目将所有配置项集中在一个 `config` 包中,涵盖数据库、基础参数与代理设置。针对配置管理,高天分析了明文配置与非明文配置的权衡:将配置写在 Python 文件、JSON 或 YAML 中属于明文方式,其优点是直观、容易导入与发布,适合存储 `retry_interval`、是否开启代理、用户数据目录等无敏感属性的字段;然而像 MySQL Database URL、账号密码及 Cookies 等隐私敏感信息,绝不能明文提交到代码库中。哪怕只是本地配置的模板,“你不知道哪天你手一抖就把你数据库的账号密码发出去了”,最佳实践是通过环境变量读取此类敏感凭证。
进入具体的业务搜索函数 `search_posts`(搜索笔记)时,高天首先点赞了作者在关键执行节点分级打 Log 的好习惯,便于平时将日志级别调至 Warning 静默运行、排查问题时再切回 Info。不过命名上存在一致性缺陷:从小红书语境到函数内注释,内容都被称作“笔记”(`notes`),但函数名却叫做 `search_posts`,应当统称为 `search_notes`。
在此处,高天重点剖析了一个典型的控制流坏味道——`while` 循环中的冗余控制变量:
| 变量名 | 初始化位置 | 循环中的变化逻辑 | 存在的核心问题 | |:--- |:--- |:--- |:--- | | `page` | 79 行:`page = 1` | 84 行传参使用,96 行递增:`page += 1` | 语义明确,代表当前爬取到的实际页码。 | | `max_note_len` | 78 行:从 `config.max_page_num` 读取 | 80 行作为终止判断,97 行递减:`max_note_len -= 20` | 1. 变量名写的是长度(len),取的却是页数配置(page_num);<br>2. 循环内按 20 递减,20 属于没有任何解释的“魔法数字”(magic number);<br>3. 变量名暗示它是最大长度常量,实际却充当向下倒计时的计数器。 |
这种让两个变量表达完全同一种控制逻辑的设计非常臃肿。如果只是需要倒计时,至少应该改名为 `notes_left`;而最干脆优化的方案是彻底砍掉 `max_note_len`,只保留 `page` 变量,将循环终止条件直接合并为 `while page * 20 <= config.max_page_num:`,即可消除冗余状态并提升可读性。
- “你不知道哪天你手一抖就把你数据库的账号密码发出去了包括像这种什么用户名密码信息还有cookies这种可能涉及隐私的信息它们都不适合明文的保存到配置文件里” - “这种打login的方式是值得学习的啊我们看他search post在忙这里就是根据关键词 找小红书笔记这里有一个小小的建议在它整个函数里小红书笔记的翻译都是note 甚至在它的注释里 它就叫search for notes那你的函数为什么要叫search posts呢叫search notes多好啊这里还是代码的一致性” - “你说你整这么多循环变量干啥呢你只需要一个page 对不对你根本不需要这个Max Note 1哪怕你把现在的magic number也留着把现在的逻辑也留着你只要把这个while变成while page乘以20小于等于config.max page number他就OK了嘛”
---
[10:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=600s) 并发控制的陷阱与原始字典传递的数据隐患
在爬虫任务并发处理阶段,作者引入了信号量 `asyncio.Semaphore` 来限制并发爬取笔记及评论的协程数量(例如限制 8 个或 16 个 worker 并发),以降低频繁上下文切换的开销并降低被反爬封禁的概率。高天指出,变量名命名为带下划线的 `_semaphore` 毫无必要,因为对于局部变量(local variable)而言私有命名规范没有实际意义。更为核心的技术细节在于对异步信号量生命周期的认知:
```mermaid flowchart LR accTitle: asyncio.gather and Semaphore Concurrency Mechanism accDescr: 解释 TaskList 预先全量创建与协程内部信号量拦截的机制差异,指出高并发下内存消耗的潜在风险。
subgraph Memory["内存阶段(即时消耗)"] T["建立大量异步任务<br/>task_list = [async_task(n)...]"] end
subgraph Execution["调度阶段(受限并发)"] S{"async with semaphore"} W1["并发任务 1"] W2["并发任务..."] W3["并发任务 16"] end
T -->|"全部实例化进入事件循环"| S S -->|"令牌释放后唤醒"| W1 S --> W2 S --> W3 ```
“这个 Semaphore 并没有阻止更多的 task 产生”,虽然在 `async task` 内部使用了 `async with semaphore` 实现了执行时的流量控制,但是在循环外层直接把所有任务实例化塞进 `task_list` 并交付 `asyncio.gather`,意味着成千上万个 Task 会在瞬间全量建立。如果爬取规模扩大到十万级别,内存将因大量处于等待挂起状态的 Task 实例而被直接耗尽。面对超大规模任务,应当改用任务队列(如 `asyncio.Queue`)按需生产和消费。
随后高天重点指出了数据在层级间流转时的工程隐患。在 `get_note_detail` 及 `get_note_by_id` 等方法中,网络请求返回 JSON 数据后,接口仅对数据做简单截取,便将未经结构化封装的 Python 原始字典(`dict`)一路层层传递,直到落地到 `update_xhs_note` 保存数据库。在整个持久化模型中,代码充斥着从大字典里使用 `note_item.get(...)` 提取字段的逻辑。
这种“用字典传一切”在动态语言中虽然简单,但在多字段场景下极度危险。由于字典的 `dict.get('key')` 找不到键时默认静默返回 `None`,一旦开发者不小心打错属性名(例如把 `note_id` 误写成 `noteid` 或遗漏下划线),程序不会发生任何报错,IDE 无法对字典键做任何补全或静态类型推断,系统只会将 `None` 默默写入数据库,“这个错误就会像无事发生一样”,丢失关键字段却无人知晓。
- “这个Semaphore并没有阻止更多的task产生也就是说这些async test还是被建立了只是说有一些task被建立了之后 一直在那等着等到其他task完成了才开始工作建立task内存还是被消耗掉了” - “这个字典如果没有这个key的话它直接会返回跟num我们可以想象一下 如果它这块note下滑线ID打错了 打的是比如说noteID没有下滑线那么它的程序不会出现任何的报错这个字典里面本来有note下滑线id这个key但是他get是noteid没有下发现也就是说他错过了这个字典里的信息而这个错误就会像无视发生一样”
---
[15:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=900s) 基于 Pydantic 的强类型建模建议与总结
针对字典传递数据导致的静默失败和开发体验缺失,高天提出了成熟软件工程中的标准解法:抛弃原始的大字典,为数据层定义专门的模型类(Class),并在数据从网络返回的第一时间将其转换为对象。使用对象属性(如 `note_item.note_id`)访问数据时,一旦出现字段拼写错误,Python 运行时会立即抛出 `AttributeError` 阻断程序,同时现代 IDE 能够提供完美的字段自动补全与类型提示。
高天强烈推荐在项目中使用 Pydantic 等库实现强类型数据建模。Pydantic 能够轻松将网络返回的字典反序列化解析为强类型对象,同时还会自动对输入字段的数据类型、嵌套结构进行严谨的格式校验,确保流转到业务层下游的数据完全合规。尽管 Python 字典有着极高的灵活性,但是在处理抓取业务中海量、琐碎且字段复杂的结构化数据时,强类型对象在安全性和可维护性上都远胜字典。
在视频尾声,高天对整份代码给予了高度评价。他指出,该项目虽然在传参解耦、循环控制变量、敏感信息隔离与数据类型建模等细节上存在重构空间,但代码架构、异步逻辑和整体代码风格已经达到了一个相对成熟的开源项目水准,目前的 100 多 Star 充分体现了其实用价值;对于在校大学生而言,能独立做出这种规模的工程项目并在简历中展示,含金量是非常高的。
- “在拿到数据的第一时间把它转型成这个object这样我在任何使用这个object的地方都会有ide的提示如果我不小心打错了属性名,会爆attribute error。同时我们可以利用Pydantic很方便的把一个字典转型成这种class它还会帮你检查这个字典里面的数据类型和你定义的数据类型是否匹配” - “拍上的字典确实非常灵活但是在遇到这种大量锁碎的数据的时候他还是容易出错啊还是容易出错” - “整个的代码风格逻辑架构都是一个相对来说比较成熟开源项目的水准了我提的很多建议其实是一些细节上的或者结构优化上面的建议并不影响这个项目成为一个有用的开源项目”
## Notable quotes > [05:00](https://www.youtube.com/watch?v=er9MKp7foEQ&t=300s) 你不知道哪天你手一抖就把你数据库的账号密码发出去了包括像这种什么用户名密码信息还有cookies这种可能涉及隐私的信息它们都不适合明文的保存到配置文件里 — 码农高天
## Mentioned 码农高天, MediaCrawler, 小红书, 抖音, Python, argparse, Playwright, MySQL, Pydantic
## Video description 其他Code Review:https://www.youtube.com/playlist?list=PLSo-C2L8kdSNr5yUJYhyDArnM4FU9iG1S
Code Review是提升代码水平最快的方式之一,这期视频的主角是一个async爬虫库,整体的代码质量还不错哦!
本次repo:https://github.com/NanmiCoder/MediaCrawler 主要文件:https://github.com/NanmiCoder/MediaCrawler/blob/main/media_platform/xhs/core.py
[Source](https://www.youtube.com/watch?v=er9MKp7foEQ)