码农高天
脚本处理应在输出前保持原生易算的数据结构,避免过早将其格式化为字符串,导致逻辑陷入反复数逗号与低效查重的反模式。
AI-assisted summary of 【Code Review】一个函数能优化五次?这Python写的有C的味道了。 — original source. Check the original for full context.
Source published:
Key ideas
- 码农高天通过对开源脚本 extractor_json2csv.py 的 Code Review,展示了如何将一个充满 C 语言底层思维的线性查重函数经由 5 步重构为 O(1) 的字典映射。 (3:40)
- 强调核心数据处理原则:在最终写入 CSV 之前,数据应全程保持为 List[int] 等原生结构,严禁在内部流转中通过拼接和统计逗号来进行长度截断与补齐。 (10:00)
- 指出开源工程的规范短板,包括缺少入口保护 if __name__ == '__main__'、误将 __pycache__ 提交至仓库,以及在 README 中硬编码指定行号让用户修改配置的脆弱设计。 (0:00)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2025 年 4 月 4 日发布。视频内容是对一位 B 站粉丝开源的 JSON 转 CSV 脚本进行 Code Review,指出初学者在写 Python 时由于不熟练而写出充斥着“C 语言味道”的粗糙代码,并提供多维度的重构建议。
## TL;DR 高天针对一个网络流量特征提取脚本展开了全面重构,首先指出了未加入口保护、提交缓存文件以及深层嵌套条件等基础规范问题。随后,他重点演示了如何将一个用于去重的查找函数经历消除循环、消除全局变量、转为内联条件、消除取反及改用字典这五次优化,将时间复杂度从 O(N) 降至 O(1)。最后,他深入剖析了“为了控长而拼逗号再数逗号截断”的反模式,强调数据在内部处理时应保持为最易操作的原生列表结构、输出时再进行格式化,并敦促开发者将用户配置与代码行号解耦以提升专业性。
## Atmosphere & style 视频整体呈现出幽默犀利、抽丝剥茧且兼具建设性的教学氛围。高天在面对诸如“数逗号”截断逻辑和“按行号改配置”的 README 时,常常发出又好气又好笑的真实吐槽,让技术审查充满了生动的戏剧效果。随着代码一步步被化繁为简、层层优化,观众能真切感受到从混乱杂糅到优雅 Pythonic 架构的推导快感,既具启发性又极富观赏乐趣。
## Bulletin - [03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 码农高天通过对开源脚本 extractor_json2csv.py 的 Code Review,展示了如何将一个充满 C 语言底层思维的线性查重函数经由 5 步重构为 O(1) 的字典映射。 - [10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 强调核心数据处理原则:在最终写入 CSV 之前,数据应全程保持为 List[int] 等原生结构,严禁在内部流转中通过拼接和统计逗号来进行长度截断与补齐。 - [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 指出开源工程的规范短板,包括缺少入口保护 if __name__ == '__main__'、误将 __pycache__ 提交至仓库,以及在 README 中硬编码指定行号让用户修改配置的脆弱设计。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 脚本必须规范定义入口 if __name__ == '__main__',明确区分模块导入与直接执行时的行为,避免逻辑无保护地顶层运行。 - [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 版本控制需保持整洁,__pycache__ 等缓存目录应写入 .gitignore,避免污染 Git 仓库。 - [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 使用 with open(...) 时若无需流式交互解析,读取完数据后应立即关闭文件上下文,不应将长段处理逻辑全部深层缩进在 with 块内。 - [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 针对多重条件过滤,应使用守卫条件(Guard Clause)将共用的排除逻辑提取到最外层,并用 elif 明确表达互斥分支。 - [03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 查重逻辑重构经历了从显式 for 循环、any()、消除全局变量、内联表达式到转换为 all() 消除 NOT 取反的演进路径。 - [03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 若需求是用唯一标识(如 tcp_stream)查重与存取,数据结构应直接从 List 跃迁为 Dict,不仅语义贴合,更将时间复杂度从 O(N) 降至 O(1)。 - [06:50](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=410s) 类命名应严格遵循 PEP 8 规范首字母大写,避免局部变量名与类名重名导致的作用域混淆及语法高亮异常,并推荐使用 @dataclass 承载纯数据类。 - [06:50](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=410s) 反模式警告:避免过早格式化数据,严禁通过循环数逗号来控制序列长度,长度控制应在追加时直接阻断或利用列表切片 lst[:30] 完成。 - [10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 数据与表现必须彻底分离,内部计算始终使用原生易操作的数据结构,仅在最终序列化输出到文件时才调用 to_csv() 或 ','.join(...) 进行格式化。 - [10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 避免在全局使用 Dictionary 这种指代不明的变量命名,固定常量应当全大写并提取到文件头部。 - [10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 配置与代码必须解耦,绝不能在 README 文档中写死“请修改第 XX 行”来引导用户配置参数,任何代码变动都会导致文档失效。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=0s) 项目初识与脚本入口规范
本期 Code Review 选取的是 B 站粉丝的一个开源项目,项目名称为 `RecognizeEncryptedTrafficBasedOnMachineLearning`。码农高天首先吐槽了这个过长的命名:“大家以后自己写项目的时候千万不要起这么长一个名字,你从来没有见过哪个知名的项目有这么长一个名字,是不是?”。进入项目目录,最先映入眼帘的是本不该出现在版本控制里的 `__pycache__` 文件夹,高天提醒大家在上传 Git 时要将其写入 `.gitignore` 中,避免污染仓库。
整个项目涉及内容较多,本次评测聚焦于 `extractor_json2csv.py` 文件,其主要功能是将特定数据从 JSON 格式转换为 CSV 格式。高天在审查过程中提出了数个基础脚本规范问题:
1. **入口保护缺失**:该脚本在第 76 行直接书写顶层执行代码,高天强调写脚本一定要加上 `if __name__ == '__main__':`,把主逻辑放进判断块。“养成一个好习惯,把你这些脚本放到这个 if 里面,因为有一些代码可能是这个 module 本身被 import 的时候要必须执行的,而有一些代码只是在这个文件被作为脚本的时候才需要执行的,`if __name__ == '__main__':` 可以帮助你定位文件的入口。” 2. **IJSON 与标准库 JSON 的取舍**:原作者使用了 `ijson` 库解析 JSON,并在 README 中说明是为了节省内存。高天指出,如果不需要流式交互解析(interactive parse)或者换回标准库的 `json`,就应该在 `with open(...)` 读取数据后立即关闭文件,而不是将整个数据处理长逻辑全套在 `with` 上下文缩进内。 3. **僵尸变量未清理**:在脚本开头初始化了三个变量,但后续全流程从未被调用。高天指出这是大家重构算法时极易遗留的毛病,应配合 PyLance 等静态检查工具及时清理。
```mermaid flowchart TD accTitle: 脚本入口及前期过滤结构优化 accDescr: 展示将冗余嵌套及重复协议前缀条件提升到最外层的重构过程。
subgraph Before["原始结构 (冗余且深层嵌套)"] direction TB B1["4个并列 if prefix 判断"] --> B2["外层 if: 协议包含 'tcp:tls'"] B2 --> B3["内层 if: 单独条件判断"] B3 --> B4["调用 find_index() 线性查重"] end
subgraph After["重构后结构 (扁平高效)"] direction TB A1["最外层统一守卫判断:<br/>'tcp:tls' in curr_frame_protocols"] A1 -->|不满足| A2["continue 跳过"] A1 -->|满足| A3["使用 elif 明确互斥分支"] A3 --> A4["直接使用 dict 查重"] end ```
在遍历数据的 `for` 循环中,原作者写了 4 个针对 `prefix` 的判断。由于数据逻辑很可能是互斥的,理应使用 `elif` 明确表达独占性分支;同时原作者使用了深层嵌套 `if`,完全可以合并为 `if A and B`。更关键的是,作者在三个并列分支中都重复判断了 `'tcp:tls' in curr_frame_protocols`,完全应该提取到最外层进行守卫判断(Guard Clause)——只要不满足该条件直接 `continue` 跳过,避免内层重复判断。
---
[03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 从 O(N) 线性查找到字典查找:一个函数的五次优化
满足条件后,原作者写了 `if not find_index(value): streams.append(Stream(...))`,通过 `find_index` 判断当前数据是否已存在。高天对这一处实现进行了层层抽丝剥茧的重构演示,展现了“一个函数能优化五次”的典型过程:
```mermaid flowchart LR accTitle: find_index 函数五次优化迭代路径 accDescr: 展示从全局变量线性循环到字典映射的五阶段演进。
V1["原始状态:<br/>显式循环 + 全局变量"] --> V2["第1步:<br/>用 any() 代替显式遍历"] V2 --> V3["第2步:<br/>消除全局变量,转为纯函数参数"] V3 --> V4["第3步:<br/>内联单行表达式,消除函数定义"] V4 --> V5["第4步 (语义重构):<br/>not any 逆否转换为 all()"] V5 --> V6["第5步 (架构跃迁):<br/>抛弃 List,直接改用 Dict / Mapping"] ```
- **消除显式遍历**:原函数使用 `for` 循环比对 `stream.tcp_stream == value`,Python 原生可以直接写为 `any(s.tcp_stream == value for s in streams)`。 - **消除全局变量**:原函数在内部直接抓取外层的全局变量 `streams`。高天强调:“写这种函数的时候我们应该要尽量避免使用全局变量,也就是说我们应该把这个 stream 作为一个参数给传进来,不要在一个局部函数里面没有必要地去找外面的全局变量,否则你这个函数的耦合性会非常强,同时这个函数还非常难以测试。” - **消除冗余函数声明**:该逻辑实质只有一行,可以直接作为 `if` 的内联条件表达式,省去跳转读函数的认知负担。 - **消除逻辑取反**:“我们在可以消除 NOT 的时候一般都会尝试消除这个 NOT,就尽量减少取反这个步骤。”没有任何一个相等与全都不相等在逻辑上等价,因此 `not any(...)` 可等价为 `all(s.tcp_stream!= value for s in streams)`。 - **核心数据结构跃迁(List 改 Dict)**:跳出局部代码后,高天点出其本质是在看某个 `tcp_stream` 之前是否出现过。既然是用 `value` 作为键做去重查找,“这不是一个 dictionary 吗?这不是一个 mapping 吗?为什么要从一个 list 里面一个一个找呢?我们直接把这个 streams 变成一个 dictionary,用这个 value 作为 key 不就好了吗?”针对顺序问题,高天补充说明,现代 Python 的字典在语言规范级别已经保证了插入顺序(insertion order),若有强语义需求还可显式使用 `OrderedDict`;用字典不仅逻辑简洁,更将查找时间复杂度直接从 $O(N)$ 降低到 $O(1)$。
---
[06:50](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=410s) 命名规范、dataclass 与“数逗号”的反模式
在分析实体建模时,高天指出了代码中极其危险的同名命名问题:原作者定义了类 `class stream:`,同时在局部定义了大量名为 `stream` 的变量。因为类名首字母未大写,导致它在全局命名空间中与同名局部变量相互干扰,甚至导致编辑器(如 VSCode)语法高亮混乱,排查极为痛苦。高天强调务必遵守 PEP 8 命名规范,类名必须首字母大写,同时强烈推荐使用标准库自带的 `@dataclass` 来简化这类纯承载数据的类定义。
接下来,高天 review 到了脚本中最为奇葩的字符串处理逻辑——`append_length` 与长度截断:
| 操作步骤 | 原作者的实现方式 | 高天指出的逻辑荒谬点与正确方案 | |:--- |:--- |:--- | | **数据追加** | 计算冒号数量加 1,转为字符串;若为空则赋值,否则用逗号拼接(`+= ',' + value`) | 将数字保存为逗号字符串属于过早格式化,后续极难修改运算。 | | **魔法数字** | 在函数内直接写死 30 限制(`max_packet_length`) | 魔法数字应当统一提到文件开头定义为模块常量(全大写)。 | | **长度补齐** | 通过不断统计字符串里的逗号个数是否少于 30,不足则循环拼 `',0'` | 每次数逗号性能极低;若使用列表,直接 `len(lst)` 并用乘法追加零元素即可。 | | **超长截断 (`remove_length`)** | 再次从全局遍历找到 `stream`,数出 30 个逗号后截取前半段字符串 | 既然已经拿到对象为何二次遍历查找?既然只需要 30 个,为什么追加时不直接阻断,反而“先加逗号再数逗号删掉”? |
看到此处,高天不禁感叹其逻辑的荒谬:“这逗号是刚才我一个一个加上去的,现在怎么又要一个一个给删掉了呢?首先如果我只需要 30 个的话,我当时在加的时候判断一下好不好?我到 30 个了我就别往后添逗号了,我添了我后面还得删,我何苦呢?其次明明就是一个数量的限制,为什么我要不停地数逗号呢?”
---
[10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 数据表示原则与配置分离规范
针对上述糟糕的处理手段,高天总结了一条核心的编程架构原则:“在我们输出数据之前,永远把数据保存成最好处理的方式,没有必要每一个数据都按照我们的输出格式来。把数据保存成一个好处理的形式,到要输出的时候再去管它的格式。”
对于序列长度控制这一场景,最好的处理方式是: 1. **数据与展现分离**:在内部让 `tcp_length` 保持为一个纯整数列表(`List[int]`),每次直接 `.append(value)`。 2. **切片与补齐**:需要限制 30 个元素时,若数量不足,通过计算差值直接做列表拼接补充零(`lst += [0] * diff`);若超出长度,直接利用切片语法 `lst[:MAX_PACKET_LENGTH]` 瞬间完成截断。 3. **输出阶段再格式化**:全流程内部运算完毕、最终写文件时,再调用 `','.join(map(str, lst))` 转为逗号分隔的字符串。内部数据结构操作的性能与可读性,远胜于反复解析与切割长字符串。
针对后续的 CSV 写入逻辑,原代码手动拼装每一项并累加逗号,高天建议直接在类中封装 `to_csv()` 方法,或者使用 Python 标准库的 `csv` 模块。将数据转化逻辑闭包在类内,调用端只需执行 `','.join(...)` 即可保证纯粹性。
```mermaid flowchart LR accTitle: 数据全生命周期流转规范 accDescr: 区分数据处理阶段与最终输出阶段的数据结构形态。
subgraph DataStage["内部数据处理阶段 (追求易操作与高性能)"] D1["原生数值 / 对象"] --> D2["List[int] 列表追加"] D2 --> D3["切片截断 lst[:30] 或 [0]*N 补零"] end
subgraph OutputStage["序列化输出阶段 (按需格式化)"] D3 --> S1["Stream.to_csv() 或 csv.writer"] S1 --> S2["','.join(...) 生成输出文本"] end ```
在脚本末尾,高天还审查了配置管理问题: - **反常命名与硬编码**:原代码在全局定义了名为 `Dictionary` 的变量(“不应该有一个常量叫做 Dictionary,太荒谬了,它是什么你要管它叫什么”),以及名为 `Target` 的文件路径变量,并附带注释提示“运行前请修改目标文件名”。 - **文档脆弱性问题**:作者在 README 文档中极尽详细地引导读者“请在第 12 行修改某配置、第 13 行修改某变量、第 19 行修改某参数”。高天指出:“永远不要在文档里写这些写死的行数,看起来也非常不专业。所有这些用户需要修改的 configuration 都应该在同一个文件里完成,它不应该对任何的行数敏感。”只要代码前面增删一行,整个 README 涉及的所有行号就会全盘失效。可变配置与业务逻辑必须彻底解耦。
在视频尾声,高天表示原作者说明这是大学期间写的小工具,可能原本只为了应付一次性需求。但高天认为,既然选择将代码开源到 GitHub 并附带了详尽的 README,就应该以更高的工程标准要求自己,用“更 Pythonic”而非写 C 语言搬砖的思维来编写清晰、易读、可维护的代码。
## Notable quotes > [10:00](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=600s) 在我们输出数据之前,永远把数据保存成最好处理的方式,没有必要每一个数据都按照我们的输出格式来。 — 码农高天
> [03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 不要在一个局部函数里面没有必要地去找外面的全局变量,否则你这个函数的耦合性会非常强,同时这个函数还非常难以测试。 — 码农高天
> [03:40](https://www.youtube.com/watch?v=_FBU-jsf7rM&t=220s) 我们在可以消除 NOT 的时候一般都会尝试消除这个 NOT,就尽量减少取反这个步骤。 — 码农高天
## Mentioned 码农高天, Python, C语言, Bilibili, GitHub, ijson, json, csv, PyLance, VSCode, PEP 8, dataclass, OrderedDict, RecognizeEncryptedTrafficBasedOnMachineLearning
## Video description 这次的code reivew的是一个很简单的json到csv的脚本,但是这个脚本写的比较粗糙,里面有非常多可以改善的地方。对Python不熟练就可能会写出来很像C的代码。
本次代码:https://github.com/WaltYoung/RecognizeEncryptedTrafficBasedOnMachineLearning/blob/main/extractor_json2csv.py
[Source](https://www.youtube.com/watch?v=_FBU-jsf7rM)