码农高天重构开源脚本 示范用字典与切片替代C风格字符串操作

【Code Review】一个函数能优化五次?这Python写的有C的味道了。 — 码农高天

码农高天

脚本处理应在输出前保持原生易算的数据结构,避免过早将其格式化为字符串,导致逻辑陷入反复数逗号与低效查重的反模式。

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)

中文简报

码农高天重构开源脚本 示范用字典与切片替代C风格字符串操作 脚本处理应在输出前保持原生易算的数据结构,避免过早将其格式化为字符串,导致逻辑陷入反复数逗号与低效查重的反模式。 高天针对一个网络流量特征提取脚本展开了全面重构,首先指出了未加入口保护、提交缓存文件以及深层嵌套条件等基础规范问题。随后,他重点演示了如何将一个用于去重的查找函数经历消除循环、消除全局变量、转为内联条件、消除取反及改用字典这五次优化,将时间复杂度从 O(N) 降至 O(1)。最后,他深入剖析了“为了控长而拼逗号再数逗号截断”的反模式,强调数据在内部处理时应保持为最易操作的原生列表结构、输出时再进行格式化,并敦促开发者将用户配置与代码行号解耦以提升专业性。 码农高天通过对开源脚本 extractor_json2csv.py 的 Code Review,展示了如何将一个充满 C 语言底层思维的线性查重函数经由 5 步重构为 O(1) 的字典映射。 (3:40) 强调核心数据处理原则:在最终写入 CSV 之前,数据应全程保持为 List[int] 等原生结构,严禁在内部流转中通过拼接和统计逗号来进行长度截断与补齐。 (10:00) 指出开源工程的规范短板,包括缺少入口保护 if __name__ == '__main__'、误将 __pycache__ 提交至仓库,以及在 README 中硬编码指定行号让用户修改配置的脆弱设计。 (0:00)

Reading summary

码农高天对一个开源流量分析脚本进行了代码审查,指出该项目存在“用写 C 的搬砖思维写 Python”的问题,并通过五次重构将核心查重逻辑从低效的线性查找跃迁为字典映射。 脚本入口与基础代码规范治理 (0:00) 在审查开源项目 `extractor_json2csv.py` 时,高天指出了数个初学者易犯的脚本规范缺陷。首先是项目版本控制缺失对 `__pycache__` 的忽略,以及在模块顶层直接写执行逻辑。高天强调必须通过 `if __name__ == '__main__':` 明确脚本入口,避免模块在被其他文件 import 时被动执行主流程。此外,针对脚本开头初始化的三个未被后续调用的僵尸变量,建议借助 PyLance 等静态分析工具及时清理。 在数据提取逻辑中,原作者写了四个并列的 `if prefix` 嵌套判断,并在三个分支里重复校验 `'tcp:tls' in curr_frame_protocols`。高天建议通过守卫语句(Guard Clause)将该共性条件提取至最外层,一旦不满足便直接 `continue` 跳过,同时将互斥分支改用 `elif` 明确表达,以此扁平化深层嵌套并去除冗余判断。 从线性遍历到字典:单函数的五次优化 (3:40) 针对原代码中使用 `find_index` 查重的函数,高天展示了循序渐进的五阶段重构。原代码通过 `for` 循环遍历全局 `streams` 列表并对比值。前四步针对局部细节进行优化:第一步利用原生 `any()` 消除显式循环;第二步将全局变量作为入参传入以解耦并提高可测试性;第三步将单行逻辑直接内联,消除多余的函数跳转;第四步将取反逻辑 `not any(...)` 逆向等价替换为 `all(...)`,降低阅读理解负担。 第五步则是架构层面的数据结构跃迁。高天指出,该逻辑本质是基于唯一键判断存在性,使用列表线性比对属于典型的结构误用。现代 Python 在语言规范层面已保证字典具有插入顺序,若有更强语义要求亦可使用 `OrderedDict`。将列表查重直接改为字典(Mapping)的键查找,不仅消除了冗余函数,更直接将时间复杂度从 $O(N)$ 降至 $O(1)$。 实体建模与“数逗号”反模式修正 (6:50) 在面向对象建模方面,原代码使用了小写类名 `class stream:`,导致类与同名局部变量 `stream` 产生严重命名冲突,使得编辑器高亮异常且难以调试。高天强调应严格遵守 PEP 8 将类名首字母大写,并推荐使用标准库自带的 `@dataclass` 声明轻量数据类。 更严重的设计缺陷出现在字符串截断与补齐上。原作者将包长度保存为逗号拼接的字符串,后续通过反复计算逗号数量来判断长度,甚至在超过 30 时先全局循环查找对象再数逗号截断。高天批评这种“自己加逗号又自己数逗号删掉”的做法极其荒谬,暴露了过早将结构化数据转为展示格式的底层错误。 数据表示与配置管理的工程解耦 (10:00) 高天总结了一项核心编程原则:“在输出之前,永远把数据保存成最好处理的方式”。对于长度受限的数字序列,内部应始终维持纯整数列表(`List[int]`)形态,追加元素使用 `.append()`,补齐时通过 `[0] * diff` 拼接,截断时使用列表切片 `lst[:MAX_PACKET_LENGTH]` 瞬间完成,仅在最终序列化输出时才调用 `','.join()` 或使用标准库 `csv` 模块。 针对工程配置,原代码存在硬编码变量名为 `Dictionary` 与 `Target` 的反常现象。同时,原作者在 README 文档中引导用户定位到“第 12 行、第 13 行”手动修改参数。高天指出硬编码行号会导致增删一行代码即全盘失效,极不专业;可变配置应当集中在独立配置文件中统一管理,彻底与业务执行代码解耦。 高天指出,虽然该脚本可能只是作者大学期间应对一次性需求编写的工具,但既然选择开源到 GitHub 并附带详尽文档,就应脱离类 C 语言的底层手工搬砖思维,遵循 Python 的规范与惯用法来组织清晰、健壮的代码。

Source quotes

[10:00] 码农高天: 在我们输出数据之前,永远把数据保存成最好处理的方式,没有必要每一个数据都按照我们的输出格式来。 [3:40] 码农高天: 不要在一个局部函数里面没有必要地去找外面的全局变量,否则你这个函数的耦合性会非常强,同时这个函数还非常难以测试。 [3:40] 码农高天: 我们在可以消除 NOT 的时候一般都会尝试消除这个 NOT,就尽量减少取反这个步骤。

Original transcript

[0:00] 这期视频我们再做一个CodeReview这次code reveal是阿里云AI助理轻情赞助的我们选的呢是B站粉丝的一个项目这个项目名Rekognize Encrypted Traffic Based on Machine Learning大家以后自己写项的时候千万不要起这么长一个名字你从来没有见过哪个知名的项目有这么长一个名字是不是首先映入眼帘的就是PyCache文件件这个文件件显然是应该被放到Gitignore里的对吧我们在上传的时候要注意一下不要把它传上来这个项目的内容比较多我们呢只看他这个JSON2 CSV 的这个部分也就是他把拿到的一些数据从J上格式变到CSE格式我们打开这个文件啊我对他这个JSAN文件里面保存的是什么数据是不了解的所以我们只从程序的角度来读这段代码它的代码入口在76行那我们在写脚本文件的时候一个比较好的习惯是把if name等于man加上养成一个好习惯啊把你这些脚本放到这个if里面因为有一些代码可能是这个module本身被import的时候要必须执行的而有一些代码只是在这个文件被作为脚本的时候才需要执行的if name等于may可以帮助你定位文件的入口上来打开文件然后用IJSON这个库去做ParseIJ桑这个裤子我从来没有用过啊我查了一下大概是一个interactive parse这一桑的东西他在readme里说了一下是为了省内存那这里我不知道这个方法是不是需要保持这个文件打开着他有可能是一点一点从文件里面读数据的如果我们这里用的是标准库的JSON那我们就不要把后面的这个逻辑放到位子里面就读取数据然后把这个文件关上再进行下面的操作接下来有三个变量的初始化这三个变量都从头到尾没有用到过大家看文件里变量只出现了一次这个其实我们经常犯的一个错误就是我们开始写某个算法可能用到了一个变量后来我们换了一下算法这个变量就不用了如果你开着拍Lens之类的东西他应该会提示说你这个变量没有用到这种东西要记得及时清理接下来初始画两个值然后开始变利我们来看一下他这个Full Loop我并不知道它的数据是什么样的但是我们单看他的逻辑也是能看出一些问题的首先他做了4个跟 prefix 有关的判断由于我不知道它的数据具体是什么样的所以我不知道这4个东西是不是 exclusive 的换言之我不知道一个 prefix 里面有没有可能同时出现这些stream如果不可能的话这里是不是应该用LIF做处理这样会非常明确的表达这些是exclusive的或者我们不看第一个if它比较不一样后面这三个还挺明显是三个并列项的吧他们三个有可能同时成立吗然后我们再单独看if他这里用了一个嵌套式的写法嵌套显然是没有什么必要的如果在你外层的if下面你只在乎内层这个if成立的情况你就应该直接把它写成这样对吧if A and B那我们从一个更high level的角度看我们观察一下这三个 if statement我们会发现有一个条件判断的是一样的这个TCP 冒号 TLS in Curve Frame Protocols这个判断在三个case里都出现了我们是不是可以把它变成这个样子在最外面直接判断这三个if都需要的条件如果不满足咱就继续了然后我们再来看一些细节当满足了一定条件之后他这里写了一个if not find index value就把Streams里面Append一个Stream我们来看一下这个FindIndex是什么这个find index函数从stream里面一个一个找,如果它的TCP stream是value就返回true否则返回false这是一个非常经典的pattern那对于这个patternpython是由内置库来完成这件事的它实际上就是这一行代码 Any然后里面Stream.tcpStream等于等于Value forStream inStreams然而这样也并不足够好我们看在这个函数里面他完全没有必要的用了一个global variable这个find index它根本就是一个纯函数吗写这种函数的时候我们应该要尽量避免使用全局变量也就是说我们应该把这个stream作为一个参数给传进来不要在一个局部函数里面没有必要的去找外面的全局变量否则你这个函数的偶合性会非常强同时这个函数还非常难以测试就你想现在这个函数你写单测非常好写但如果他用了一个全聚变量你写测试就变得非常难写当然我们会忽然意识到这个函数只有一行啊我为什么要写一个函数呢我们完全可以把这个东西直接拿来当条件判断吧这样读代码的时候还少了一个找这个函数的步骤在原地就读懂了这时候我们又发现 not any 是一个比较复杂的逻辑我们在可以消除NOT的时候一般都会尝试消除这个NOT就尽量减少取反这个步骤 [5:00] 我们这块可以做一个逻辑等价没有任何一个相等和全都不等是逻辑等价的所以这里它相当于是if or TCP stream不等于valueStreams里所有存在的Stream的TCP Stream都不是Value现在足够好了吗从等价逻辑上看确实我们把它优化了不少但是如果从更高的角度看去想它这个代码的意义你就会意识到它实际上就是在看这个street在之前是不是出现过这个stream.tcpstream是哪来的呢?就是在它初始化的时候传进去的这个value也就是说他实际上是在用这个 value 作为 key来比较之前所有拿到的stream里面有没有出现过这个value这是什么?这不是一个 dictionary 吗?这不是一个mapping吗为什么要从一个list里面一个一个找呢我们直接把这个streams变成一个dictionary用这个value作为key不就好了吗我们是完全没有必要建立一个list来这么一个一个找的那当然有一种可能性他在意这个stream来的order现在python的dictionary是在文档级别保证了插入序的也就是你直接用dictionary保存然后你再根据key去做Iterate它是保证按照你插入的顺序输出的当然如果你觉得这种隐式的顺序还不能满足你的要求,你还可以用order的dict他在名字上就有顺序这两字当然我觉得更大的可能性是你这个Streams其实压根不需要顺序他只需要保证不重复就可以了所以这里用字典明显是个更好的选择然后我们再说一句题外话有关这个class stream他出现了若干额问题首先在这位同学的代码里出现了很多局部变量叫stream而class stream是会在全局里面定义一个全局边量stream的这种局部变量和全局变量同名的情况很有可能会让你陷入一些大坑这也是为什么我们要遵循所谓的命名规则类的名字和变量的名字它理论上不可能重复才对所以这里保持好习惯Class手字毛大写另外这个class的内容也是一个经典的pattern这个东西叫做data class这里是可以直接用data class来实现这个stream的强烈建议各位小伙伴啊在使用这样保存数据的class的时候使用一下dataclass这个库那我们简单看下下面这个insertsni它也是一个full loop然后找这个stream是不是如果用了dictionary这件事会变得非常简单他根本不需要一个额外的函数来完成下面append length稍微费劲一点首先他算了一下这个value里面有多少个冒号冒号应该是分格符啊然后加了一个1接下来他把这个树变成String然后扔到了Append Length里在Append Lens里也是找到这个Stream如果他的TCP length是none的话直接让他等于value否则让它加等于逗号加value也就是如果什么都没有把这个树放上去否则的话加上一个逗号再把这个书放进去这里的问题我们按下不表我们看后面为什么看后面因为后面他对这个Length有一个额外的处理首先它规定了一个Max Packet Length像这种魔法术如果我们要定义最好是定义到文件的前面把它当成一个常数也就是写到这儿让人很明确的知道它是一个常数其次我们看他做了什么对于每一个stream拿到它的TCP Lens还记着TCP Lens是什么吧最开始是Nen有了一个数的话就是Stream 1 2之类的然后如果有若干个就是1/3/5之类的他先算了一下TCP Lens 里面有几个数算的方法是数数里面有多少个逗号然后加1如果不够这个30就在后面补0 倒好0 倒好0 一直补到30个为止如果抄了就做一个remove length我们看这个remove length首先咱不说为什么还要再找一遍因为明明你remove length的时候就已经拿到那个stream了他remove length的方法依然是属痘号往后一直属痘号痘号数够了取痘号前面的那个部分那看到这里大家是不是觉得这个方法有点荒谬这逗号是刚才我一个一个加上去的现在怎么又要一个一个给删掉了呢首先如果我只需要30个的话我当时在家的时候判断一下好不好就我到30个了我就别往后添多号了我天了我后面还得删我何苦呢其次明明就是一个数量的限制为什么我要不停的数豆号呢?这里就要讲一个我们编程时候的好习惯 [10:00] 在我们输出数据之前永远把数据保存成最好处理的方式没有必要每一个数据都按照我们的输出格式来把数据保存成一个好处理的形式到要输出的时候再去管他的格式对于这个功能来说他TCP length实际上就是一个一个的数他最后要把这些数用豆号分隔输出出来这是一个很简单的事情啊我们只需要让这个TCP Length是一个List把它初始化成一个空List然后每一次append的时候在这个List里面把这个value append进去当然你也可以在这里直接判断我这个length是不是已经抄了如果抄了就不往你加了哪怕你在那块不判断这里你也不用再数豆号了对吧你直接去求TCP length 他有多大就可以了差多少个零你就往那个list里塞多少个0他就是这么个东西买加上一个这么多个零如果你要取前30个你只需要这样封过号冒号maxpacketlearn你不需要一个一个取煽动号啊你会发现用类似的形式一个一个去做保存是会让你在未来更好操作的同时它的性能是更高的肯定比你去来回来去折腾一个string性能要高得多直到最后要输出的时候你再把这个list用逗号给转一下他就变成了一个用dow号分格的string了然后你把它再做输出就可以了这就是我们之前聊到的数据要先保存成易于处理的方式到最后再去想输出格式不要上来就去套输出那个格式最后这里他要输出成一个CSV文件那这个部分呢他没有用Python自带的CSV库是直接自己输出的啊其实可以用那个CSV库啊但是他可能觉得自己这个格式比较简单最后ray的时候看一下他把每一个需要写的东西都打出来然后都加一个逗号这里还是应该把他们放到一个list里 join 起来然后这里如果我来处理的话我会在这个stream class里做一个输出格式也就是把所有需要输出的数据用逗号隔开比如说要stream.tocsv这样就可以在Stream class内部去做这个TCP Lens 处理把它从数据然后变成要输出的格式但最后你成功就可能大概这个样子大家发现了这个stream variable跟其他正常的local variable颜色是不一样的就是因为你前面的class名字也是用的stream他让VSCode非常的迷茫这是个非常非常不好的习惯那如果有了这个stream.tocsv你前面这个sni的处理也可以放到那个tocsv里就像我刚才说的从数据到输出的形式你只需要在一个地方处理其他所有的地方都是数据处理怎么方便怎么来只有到输出的时候把这些数据给处理成输出方面的形式那程序的最后用到了这个 dictionary target这个东西是什么呢 我翻到最上面Dictionary确实是一个Dictionary但是不要这么命名自己的变量不应该有一个常量叫做Dictionary太荒谬了他是什么你要管他叫什么同时的既然是一个常亮他就应该有常亮名字的样子target非常贴心的注释告诉你运行前请修改目标文件名这个东西应该统一在一个config file里面完成这是我们之前提到过的逻辑和数据分离程序需要的数据需要的可变的输入应该统一在一个位置完成而不是分布在程序的各个角落当然如果你去看readme的话就能发现一些非常神奇的事情他的文档呢 写的还蛮贴心的 写的很细致比如说他会告诉你在这个文件中在第12号你需要休怪什么第13行你需要修改什么19行需要修改什么30行需要修改什么这个文件但凡你前面改了一行你这些readme是不是都要改永远不要在文档里写这些写死的行数看起来也非常不专业啊就所有这些用户需要修改的configuration都应该在同一个文件里完成他不应该对任何的行数敏感当然朋友说了这个是他在学校期间写的一个小东西他写的时候也未必想到未来会维护他可能只是一个一次性使用的小工具我们拿它作为例子只想告诉大家怎么写出来更好维护的代码并不是说他这个程序本身就一定要怎么怎么样因为我写很多一次性程序的时候写的也是比较随意对吧但如果你把你的程序放到了Github上还认认真真写了readme我觉得还是有必要稍稍处理一下的好那这一面是乐意,希望大家点赞关注,不要忘了一件,下线了哦!优优独播剧场——YoYo Television Series Exclusive