码农高天评测MediaCrawler 敦促爬虫弃用字典改推Pydantic建模

【Code Review】传参的时候有这么多细节要考虑?冗余循环变量你也写过么? — 码农高天

码农高天

用原始字典传递抓取数据极易因拼写错误静默写入空值,应引入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)

中文简报

码农高天评测MediaCrawler 敦促爬虫弃用字典改推Pydantic建模 用原始字典传递抓取数据极易因拼写错误静默写入空值,应引入Pydantic强类型对象在网络解析层阻断异常并校验格式。 高天围绕开源爬虫库 MediaCrawler 的入口与核心模块展开代码审查,首先指出命令行参数应在前端通过 choices 严格校验与解耦传参,避免把配置动态塞进对象或传递 Namespace。接着,他分析了敏感配置切勿明文硬编码、`while` 循环中冗余计数变量的化简,以及信号量限制并发时预创建海量 Task 可能造成的内存隐患。最后,高天针对“用原始字典传递一切”容易引发拼写错误静默失败的问题,强烈建议在网络返回端引入 Pydantic 构建强类型数据模型,以获得更好的类型提示与运行时安全保障。 针对开源爬虫项目 MediaCrawler,高天指出了命令行参数校验延迟、隐式动态属性注入以及枚举类型未做强约束等入口设计缺陷。 (0:00) 配置文件中的数据库凭证、账号密码及 Cookies 等敏感数据必须剥离出明文代码库,改由环境变量动态读取。 (5:00) 使用 asyncio.Semaphore 控制并发时若直接把任务全量塞入列表,成千上万个挂起 Task 仍会消耗大量内存,大规模抓取需结合任务队列控制生成节奏。 (10:00) 用原始字典传递数据易因 dict.get() 静默返回 None 导致拼写错误被掩盖,建议引入 Pydantic 强类型模型在接口层进行解析与校验。 (15:00)

Reading summary

码农高天在评审开源爬虫项目 MediaCrawler 时指出,虽然项目整体架构与异步设计已具雏形,但在入口参数校验、状态控制、并发机制及数据流建模等工程细节上仍需重构,以提升健壮性并规避静默故障。 入口校验与解耦传参 (0:00) 原项目在 main.py 中将平台名称与登录类型作为普通字符串接收,并在业务下游深层才抛出 ValueError,甚至报错提示的中文“抖音”与代码期望的缩写 dy 不一致。高天指出,应在 argparse 中通过 choices 明确限定枚举范围,在最前端拦截非法入参;调用静态工厂方法时直接通过类名调用即可,无需额外实例化工厂对象。 在爬虫初始化配置中,原代码将 argparse.Namespace 整包传入并通过 **kwargs 配合 setattr 动态注入属性。这种做法灵活度过高,不仅掩盖了拼写错误风险,还破坏了代码的可追溯性,并使核心库产生了对命令行解析器的外部依赖。高天建议在入口处拆解 Namespace,将参数显式声明并传递给下游属性。 敏感配置隔离与循环状态精简 (5:00) 针对配置管理,高天分析了明文配置与环境变量的权衡。通用配置(如重试间隔、代理开关)写入 Python、JSON 或 YAML 文件便于导入,但数据库账号密码、Cookies 等敏感凭证严禁明文入库,应统一通过环境变量读取,防止误提交泄漏凭证。在命名规范上,从小红书语境到注释均称为“笔记”(notes),业务函数却命名为 search_posts,应统一改为 search_notes。 在搜索笔记的控制流中,代码同时维护了向前递增的 page 与从配置读取并按步长 20 递减的 max_note_len。高天指出后者不仅存在未解释的魔法数字,且变量名与实际的倒计时逻辑脱节,属于冗余控制变量。最直接的重构是彻底移除 max_note_len,将循环判断直接收敛为 while page * 20 <= config.max_page_num,以简化状态维护。 并发任务的内存隐患与流量控制 (10:00) 为限制并发协程数并降低反爬风险,原项目在子任务内部引入了 asyncio.Semaphore。高天提醒,局部变量命名无须加私有下划线,更关键的是要认清异步信号量的作用范围:信号量仅限制同一时刻执行的 worker 数量,并不能阻止外部任务的实例化。 如果在外层直接将所有任务一次性塞入 task_list 并交由 asyncio.gather 执行,数万乃至数十万个 Task 对象会瞬间全部常驻于事件循环中,导致大量内存被等待中的协程实例耗尽。针对超大规模数据抓取,应引入 asyncio.Queue 等任务队列进行按需生产与消费。 抛弃裸字典并引入强类型模型 (15:00) 原代码在各层级之间直接流转网络请求返回的原始 JSON 字典,在数据持久化时充斥着 note_item.get(...) 操作。由于字典的 get 方法在键缺失时默认静默返回 None,开发者一旦拼错键名(例如将 note_id 误写为 noteid),程序不仅无法得到 IDE 的类型提示与自动补全,也不会触发任何运行时异常,只会将空值默默存入数据库,产生极难排查的静默数据丢失。 高天建议在网络数据返回的第一时间将其转换为强类型对象,并推荐使用 Pydantic 进行建模。Pydantic 能自动完成反序列化与字段类型、结构的严谨校验;在访问属性时若发生拼写错误,Python 会立即抛出 AttributeError 阻断程序,从根本上保障数据流转的严谨度与可维护性。 高天总结认为,尽管项目在传参解耦、循环控制、凭据隔离与数据建模等方面存在细节坏味道,但整体架构与异步设计已具备相对成熟的开源水准,对于在校开发者而言具备很高的工程参考价值。

Source quotes

[5:00] 码农高天: 你不知道哪天你手一抖就把你数据库的账号密码发出去了包括像这种什么用户名密码信息还有cookies这种可能涉及隐私的信息它们都不适合明文的保存到配置文件里

Original transcript

[0:00] 好那这一视频依然是我们知识星球专属的code review这一次我都review的是一个爬虫的库可以看到已经是一个出具雏形的调研项目了已经100多个star了这一户主要实现的是小红书跟抖音的爬虫作者希望我对其中的一个文件提出一些建议我们今天主要review的是小红书这个部分的core.pi但是在我看了这个文件的代码之后呢我觉得我们还是应该先看一下入口部分的代码这个main.py就是入口文件这里他写的是一个async函数啊直接asyncl.runmay我们可以看到上面may函数的部分 首先是建立了一个arc parser这也是非常常用的一个Python内置库这里呢我们就开始有第一个意见了我们可以看一下它这两个argument,platform跟lt,也就是login type它实际上都是美举类型但是它在arc parser这块呢,taked是一个string于是他可能会运行到比较深的地方才会报错我们可以看到如果platform给的argument不对的话,他要在这块才能报错那前面这两个事情可能就没有意义的进行一起操作那对于这种每一句类型的argument我们实际上是可以在建立ArcPursor的时候直接规定的我们在做add argument的时候可以直接给他一个choices也就是说这个platform只能是小红书或者抖音如果给的不是这两个的话他就会直接报错然后我们把这个 parse args 代码给挪到前面去在做任何的其他事情之前,先 parse argument这样的话就可以避免一些无用功那其实到上面我们可以看到如果这个platform给错了它会报一个value error但是这个value error写的是我们支持的platform只有小红书或者抖音大家注意,这里的抖音是全频它这个爆错有可能会让用户很迷茫如果这个用户输入的就是抖音他会觉得我输入的不是一个正确的值吗所以这里要尽量保持一致性 这里变成地外比较好接下来这个东西叫工厂模式对吧我跟大家说过我是不相信设计模式这种东西的但是就在这个地方而言由于你这个create crawler是一个static method所以在这里你并不需要建立一个object你可以直接用这个class的CrateCrawler这个相对来说呢是更排算的写法那他create crawler返回这个object 小红书crawler就是我们这次review的核心在拿到这个对象之后 他做了两件事 第一个是initconfig 第二个是start我们看一下这个 init config他这里呢使用的是一个Keyword args方法然后用的是setattr一般情况下我们推荐避免这样的写法这个写法呢有点灵活度过高它很容易让你错过一些错误比如说这里cmd args你写成cmd args少写了一个s你可能会在很久之后程序才报错还有一个问题呢,是当别人看你这个core.py文件的时候他根本找不着你的这些 attribute 是在哪块被设置的比如说我读代码的时候读到这个CommandArts我可能在58行发现这个CMND ARX被用到了但是我根本找不到这个变量是在哪被设置的就它的输入在什么地方呢我是找不着的所以在这里呢 我强烈推荐把输入显示的传进来 在显示的复制给自己的属性上这样做还有一个好处就是当你其他函数调用这个initconfig的时候他会有更好的提示这里其实还有另外一个小问题我们看它传进来的这个CMNRX它实际上这个argument parser parse完之后直接的结果也就是在这里写的,它是一个namespace那这样呢相当于是在你这个代码的内部啊你还有一个argument parser的依赖我觉得这里如果想捷偶做得更好的话你应该把这个command arg直接拆开就是argument parse这个过程完全交给may函数传进来的就是你需要的东西那你这个initconfig需要什么呢不就是一个platform和一个lp对吧我觉得相对更好的写法应该是这样的就是把你的platform跟LT显示的传进来不要传一个ArgParse的namespace这里给 input 的时候直接把需要的数据拿出来不要传一个 args好我们把它恢复原状啊我们切换一下话题这个import的部分我实际上在知识星球里面讲过这里python是有一套推荐的方法的他这里现在的大区分是完全正确的第一部分是standard library 跟着Python一起的 第二部分是一个third party library在这里呢就是 play right最后是一些local的酷包括在每一个section里面先写import 后写fromimport 这个是对的但是最后还差一步啊每一个这种小section里面它还应该按照字典序排序排回去之后就会让你的代码显得非常专业包括这里 [5:00] 不知道他为什么会写成四行啊因为上面的typing他就写了一行对吧我觉得这里写一行 四个东西是完全可以的然后这种impro的形式应该尽可能的避免 除非东西实在是太多了你在Exception里边一共只有两个你自己的Exception我觉得这里应该是显示引入的好那config完了就start对吧开始看start的函数star这里呢先拿到proxy info然后开一个playwright接着开浏览器这个部分呢其实没有特别多可说的是一个按部就班的过程在这里我们能看到一个config对吧这也是很多人的项目都需要设计的一个东西啊,如何去配置你的项目。我们可以看到啊,它这边可选的配置还是非常多的他目前把所有的配置放到了一个package里面比如说这个database的部分然后一些基础的设置啊包括代理的一些信息啊这里我们注意一下啊Configuration这件事有非常多的实现形式我们这里可以把它分为两大种也就是明文和非明文名文的形式你可以像这个作者一样把它写成python文件优点就是非常容易引入拍档文件可读性很好同时你可以把它直接当一个常量给impoor到代码里非常的方便或者你也可以用一些其他的格式用JSON用YAMLpython也是有专门的库去处理这些东西这些统称为明文的方式明文的方式简单好用又好发布但这里涉及个问题就是有一些配置是不适合以明文的方式出现在代码库里的比如说你这个 Sequel Database URL他就不应该以明文的形式出现在大马库里你当然可以说 我这块就是举一个例子 实际上你要在本地做配置 对吧但是你不知道哪天你手一抖就把你数据库的账号密码发出去了包括像这种什么用户名密码信息还有cookies这种可能涉及隐私的信息它们都不适合明文的保存到配置文件里那明文的配置文件就适合存一些这种东西什么reply interval啊 对吧是不是开启这个proxy啊包括用户数据保存什么文件夹呀这些都可以明文保存而对于这种隐私信息最简单的还是使用环境面料这样可以最大程度的防止你误传你的隐私信息到代码库里好 那我们回过头接着看啊下面呢 也没有什么特别的加了一个GS 然后去煮液接下来开一个client如果没有登录的话 登录这里呢就涉及到其他的class对吧那不是我们review的重点我们就不看他了我们就假设它的时限都是对的这里稍微注意一下login type传进去是self.cmd.lt这里如果传的是一个self.login type是不是就会读起来更简单一些这个CMN2是不会有从天而降的感觉好 登录什么的全都搞完了 开始Search Posts这里提一下他有一个非常好的习惯就是在他的代码里面呢关键的地方都加一个logger然后logger 由于是分级的所以他平时debug开到比如说warning的时候这个info是不会被打出来的但是如果他想看这个info啊他可以调整debug的等级这种打login的方式是值得学习的啊我们看他search post在忙这里就是根据关键词 找小红书笔记这里有一个小小的建议在它整个函数里小红书笔记的翻译都是note 甚至在它的注释里 它就叫search for notes那你的函数为什么要叫search posts呢叫search notes多好啊这里还是代码的一致性这些都是增强可读性的事情好 在这里我们重点说一下这个 while 循环我们看到在while循环里面有两个所谓循环面量一个叫做Max Note Learn这个值取了一个page值啊,max page number第二个呢叫做page他让他上来等于1我们还有三个地方涉及到这个page在79行把它设成1在84行拿这个note的时候要把这个page传进去也就是爬到第几页了在96行配置加1这个配置非常好理解对吧我们再看max note length同样是三个地方78行初始化,80行作为循环的判断面量97行每一次循环减20这里你是不是会发现这个 page 跟这个 max note length他俩起到的其实是非常相似的作用这个max note learn在这个函数里的作用就是从这个max page number开始每搜一页-20-0以下循环停止这里有若干的问题 [10:00] 第一在78行这个page 里面定义的叫做max page number为什么传进来之后变成note number了呢我们从97行大概可以看出来作者认为一页一个page应该是20个note那么你在page的时候我写这个max page number 到底给的是页数呢还是Note书呢其次97行这个20是一个经典的magic number他没有任何道理没有人能直接知道他在干什么他也没有任何注视第三,作为一个变量,这个变量的名字叫Max Note Learn,对不对?我们一定要注意尽量让变量在函数的所有时刻都保持着它变量名的含义这种从最大值往下减 减到0以下停止的方式很常见但是这个变量不宜叫max note length你可以管他叫notes left对吧还剩下需要爬的笔记的数量这样你的逻辑不用变 但是你的名字明显清晰了 因为max note length应该是一个长量然而这里更好的处理方式啊应该是把这两个循环变量合成一个循环变量你说你整这么多循环变量干啥呢你只需要一个page 对不对你根本不需要这个Max Note 1哪怕你把现在的magic number也留着把现在的逻辑也留着你只要把这个while变成while page乘以20小于等于config.max page number他就OK了嘛你的程序可读性甚至比现在还好一些就这两个变量明显是表达了同一个意思啊所以这里呢 明显可以变得更好好 我们看这个循环里拿到一堆post哦 看到这我发现它确实还是有一些变量用的是post啊我的直观感觉是这里POST跟NOTE完全是同一个意思不知道这里是不是有细微的区别我的建议如果是一个意思就用一个词然后下面他加了一个semaphore他加一个semaphore的意思就是说同时爬的任务数呢不要给干的太高可能同时只有8个或者16个人在爬这样第一是能减少上下文切换带来的损失第二呢 可能是减少被封啊这个设计本身还是挺不错的但是这个Semophore完全没有必要加一个下滑线你本身是一个local variable作为一个local variable加一个下滑线没什么意义这里建立一个Task List 然后做Gather我们看这个async task啊它这块就有一个async with semaphore对吧这里小小的提一下啊在它这个程序里可能不是一个非常严重的问题但是我们大家一定要有意识这个Semaphore并没有阻止更多的task产生也就是说这些async test还是被建立了只是说有一些task被建立了之后 一直在那等着等到其他task完成了才开始工作建立task内存还是被消耗掉了那如果你这个Test List的数量本来不是很多没什么事如果你这个Task list里面本来可能有10万个Task这里你就一定得意识到这个Semaphore并没有阻止这些任务的建立那如果你想处理它的话你可能就换一个其他的方法对吧可能用对裂啊或者用一些其他的方式拿到每个note之后更新然后再去爬这个note的评论我们看拿评论的地方也是相似的那下面的一些函数上有些标准化的东西这个部分呢,可点评的空间不是很多他整体来说写的比较不错有一些内容可能涉及到一些爬虫专项的知识我们就不给大家讲了但是在我看他这个程序的数据传递的时候我发现了一个可以改进的地方我们看get node detail函数里面 它返回的是get node by id我们看getnotebyid函数,它实际上做了一个post返回来的是一个JSON他拿了JSON的一部分也就是说他最后拿到的东西是一个字典这个字典被传来传去的最后传到了这个update小红书note我们看更新数据库的部分所有的数据他都是在一个 dictionary里面尝试用get去拿也就是说他拿到的数据是一个非常大的字典字典里面有各种各样的key而在他的model里面他在尝试用这些key拿到这些数据理论上来说 这当然是没问题的但是他要拿到的数据种类很多而且在这里用的全部都是get也就是说这个字典如果没有这个key的话它直接会返回跟num我们可以想象一下 如果它这块note下滑线ID打错了 打的是比如说noteID没有下滑线那么它的程序不会出现任何的报错这个字典里面 [15:00] 本来有note下滑线id这个key但是他get是noteid没有下发现也就是说他错过了这个字典里的信息而这个错误就会像无视发生一样他只会把这个note id复制成如果这样的语句一句两句还好说我们可以认真检查但是我们可以看到他整个的model都是这样的语句那你很难保证你一直写的都是对的而IDE在这个地方也没有办法帮助你因为你的note item是个字典Ide并不知道这个字典里面应该有什么key 他甚至都没有办法提示你你只能靠自己的力量把每一个key都写对那在一个比较成熟的项目中我们遇到这种问题的时候一般来说我们就会避免用字典来传数据我们经常会做一个数据对应的class然后再拿到数据的第一时间把它转型成这个object这样我在任何使用这个object的地方都会有ide的提示如果我不小心打错了属性名,会爆attribute error。同时我们可以利用Pydentic很方便的把一个字典转型成这种class它还会帮你检查这个字典里面的数据类型和你定义的数据类型是否匹配这里建议作者可以尝试一下这个方式你如果尝试一下可能就会发现啊他比这个字典的方式确实要优秀一些拍上的字典确实非常灵活但是在遇到这种大量锁碎的数据的时候他还是容易出错啊还是容易出错OK 那我们今天review的这个代码呢 总体的完成度其实是相当不错的整个的代码风格逻辑架构都是一个相对来说比较成熟开源项目的水准了我提的很多建议其实是一些细节上的或者结构优化上面的建议并不影响这个项目成为一个有用的开源项目大家也能看到啊这个项目本身已经有100多star了还是很不错的那如果你做一个可能比如大学生啊拿出来这么一个项目在你的简历里是值钱的啊值钱好 那我们这期的co review就到这里 喜欢的话记得点下关注 别忘了一键三连哦字幕志愿者 杨茜茜