diff options
| author | eltrac <hi@eltr.ac> | 2026-03-08 20:25:59 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-03-08 20:25:59 +0800 |
| commit | f278f39e121983946de0bed3c9d408e53d23f9a3 (patch) | |
| tree | 264a444bd558ce7fd091139cc05ddbbea7a681dd | |
| parent | 209f56436ec6b96a063ca7726d43a3b6440413a5 (diff) | |
| parent | 5cb5cd6156f262977967622a37482b368316e148 (diff) | |
Merge branch 'draft'
| -rw-r--r-- | content/article/building-websites.md | 31 | ||||
| -rw-r--r-- | content/article/site-report-5.md | 136 | ||||
| -rw-r--r-- | content/categories/稻草人周刊/_index.md | 5 | ||||
| -rw-r--r-- | content/essays/_index.md | 4 | ||||
| -rw-r--r-- | content/faq.md | 18 | ||||
| -rw-r--r-- | content/posts/_index.md | 4 | ||||
| -rw-r--r-- | content/weekly/71.md | 151 | ||||
| -rw-r--r-- | tag-wrangler.py | 330 |
8 files changed, 336 insertions, 343 deletions
diff --git a/content/article/building-websites.md b/content/article/building-websites.md new file mode 100644 index 00000000..9aab25fe --- /dev/null +++ b/content/article/building-websites.md @@ -0,0 +1,31 @@ ++++ +title = '如何搭建一个网站?' +subtitle = '' +slug = '' +tags = [] +categories = '' +date = 2026-03-04T18:17:55+08:00 +draft = true +toc = true ++++ + +毫无新意的话题,但我就是想写,因为这是我的网站。本文旨在从零开始,为没有计算机网络基础的读者建立起对网站运作方式的了解,文本并不刻意区分静态网站和动态网站,而是从最底层、用尽可能通俗的方式回答这些问题:什么是网站?什么不是网站?访客怎样访问网站?服务器怎样提供网站?搭建网站的技术有哪些?他们的工作原理是怎么样的?<!--more--> + +## 基本概念 + +### 什么是网站? + +现在每天还会访问网站的人估计很少了,各个科技公司几年前就开始推进移动端应用的普及,对于大部分人而言,网站其实是很老旧的产物了,他们更关心想看的内容能不能在手机 App 上访问到。联网的 App 和网站一样,是互联网的一部分,但有很大不同。首先,网站是万维网的一部分,而手机 App 不是;其次,手机 App 是客户端,数据通过服务器发送到手机上,再由客户端展示,网站虽然也有前后端的区分,但很难把网站这一个概念归类为客户端或服务端。 + +网站的客户端,严格意义上来说,应该是浏览器。常见的浏览器有 Chrome、Safari、Firefox 和 Edge 等。访客在浏览器中输入网址来访问网站。传统意义上来讲,网站不是用来用的,而是用来「看」的,或者是用来「发」东西的,网站是「内容」的载体;不过现在也有了很多 Web 应用,用户能在网站上做很多事情,比如转换文件格式、操作远程桌面、管理服务器等等。所谓 Web 应用,就是建立在万维网上的应用,我们一般用 Web 来指代万维网。 + +### 什么是万维网? + +万维网是互联网的一部分,英文全称是 World Wide Web,简称 WWW。常见于网址中的 `www` 就是万维网的缩写。一般来说,只有 `www` 开头的域名才代表一个万维网网站,比如 `www.geedea.pro` 是「极客死亡计划」这个网站,而 `geedea.pro` 作为顶级域名,原则上不应该提供万维网内容。 + +为什么?因为一个域名可能完全不属于万维网,比如:你打开 Minecraft 这个游戏,点击「多人游戏」,点击「添加服务器」,输入 `mc.hypixel.net`,再点击按钮,你就加入了 Hypixel 这个 Minecraft 服务器。在这个例子里,`mc.hypixel.net` 提供的是游戏服务器的入口,可能还有 FTP 文件服务器、返回数据的 API 端口等等。 + +当然,这并不是什么标准,使用非 `www` 子域名提供万维网站点的例子非常多,所有人都在这样做。比如 `www.a-cool-guy.me` 可能是某人的个人主页,而 `blog.a-cool-guy.me` 就是他的博客。 + +### 什么是域名 + diff --git a/content/article/site-report-5.md b/content/article/site-report-5.md new file mode 100644 index 00000000..1eb1613d --- /dev/null +++ b/content/article/site-report-5.md @@ -0,0 +1,136 @@ ++++ +title = '极客死亡计划书 V' +subtitle = 'Stirring up some chaos.' +slug = 'site-report-5' +tags = ['写作','设计'] +categories = '极客死亡计划书' +date = 2026-03-08T14:43:32+08:00 +draft = true +toc = true ++++ + +在周末打开终端,`cd` 进极客死亡计划的项目目录,然后打开 Neovim 开始在自己的代码里到处晃悠,删删改改,对我而言是很放松的体验。我时常在浏览器里注意到某个网页元素,回忆起源代码的位置,立马打开编辑器看看有没有需要去毛球(lint)的地方。这种感觉,其实很像是园丁在自己的花园里闲逛,做些简单的修枝剪叶吧? + +总之,某个寻常的周末,我冲了一杯非常好喝的瑰夏,在书桌前坐下,开始在代码之间漫游,发觉修枝剪叶的工作都做完了,此时,一个积压在脑后已久的想法突然涌现。我拿起铲子,盯着其中两个花圃,兴致勃勃地说道:“好,那就开始铲土,重新想象园林设计吧!”——这两个花圃就是[前不久](/weekly/57/#博客的内容分区变动)才被移植过的「议论」和「散文」。是的,我把他们又合并了。<!--more--> + +--- + +## 用词说明 + +为了避免误解,我们先理清一些术语。 + +下文的「分区」指 Hugo 的 [section](https://gohugo.io/quick-reference/glossary/#section),也就是顶层的内容目录,比如 `/posts` 下有 `/posts/xxx` 和 `/post/yyy` 等页面,这个 `/posts` 就是一个分区。 + +「分类」是指 Hugo 的 [taxonomy](https://gohugo.io/quick-reference/glossary/#taxonomy),本站有两种分类,一是「[文章系列](/categories/)」,二是「[标签](/tags/)」。不过,出于一些历史原因(其实就是我懒得改),「文章系列」这个分类的代码写作 `categories`,本意是分类。 + +--- + +## 命途多舛的极客死亡大地 + +最初,极客死亡计划上只有一个内容分区,即 `/posts` 分区,所有的文章都放在这下面,其他的页面是如 `/about` 和 `/linkroll` 这样的顶层页面。那是段纯真简单的时光,直到某个名叫 Eltrac 的疯子突然决定要把自己写的~~垃圾~~小说都搬过来,于是网站上就多了一个 `/fictions` 分区。 + +骇人听闻的事件没有停止,这个疯子决定要给自己读过的每一本书在网站上留一个位置。他先对[稻草人](/weekly/)下了手,心狠手辣地把「书评」分区从周刊手里夺走了,把那些残缺的短评放进了他引以为傲的、单独的 Markdown 文件里,大喊着「原子性!」「双向链接!」的口号,使用名为短代码的邪恶武器将书评嵌入了稻草人的身体里,随后又在他飘忽不定的思绪影响下,把那短代码从整个~~奥兹国~~代码库里剥去。由于操作习惯拙劣,Git 历史的可读性极低,他的罪行也就没有留下证据,那些被他摧残的文件都改了名,或者进了垃圾桶,连 `git blame` 的机会都没有! + +简而言之,极客死亡计划又多了一个 `/library` 分区,纯真简单的小骷髅就这么被知识玷污了,眼神里失去了光! + +这便是「议叙文大分裂事变」的历史背景,在那之后,Eltrac 短暂地休息了一会儿,可没过多久,他便又舞起键盘,对网站历史最悠久的古迹下了狠手。一向忠心耿耿 `/posts` 分区被他无情地斩成两半,有超过半数的页面作为 `/essays` 分区的新臣民被分离了出去,就这样背井离乡。大魔头对此的辩解是: + +> 你也可以理解为,我把文章分成了 T 和 F 两部分(仔细想想,其实更像是 J 和 P 的两部分,判断和感知的区分)。做这个拆分是因为,我发现自己的 T 和 F 可能会在某个时间段宕机其中一个,把这个分开能让我更安心地在情绪崩溃时发疯、在好奇心超负荷运转时写一写没人关心的新话题。 + +{{< right "——《[稻草人周刊 Vol.57](/weekly/57/)》" >}} + +何其可悲!何其可悲!仅仅是因为他自己沉迷于*愚蠢的*大众心理学理论(MBTI),就祸害了全体网站公民的身心健康!更不用说,此举损人不利己,如今大魔头每次在写作时,都会纠结要把他拙劣的文字放在哪边,`/essays` 和 `/posts` 说起来有别,但实际上本是同源,都是他那扭曲头脑不能停止转动的产物。不过,我们也不要太苛责这个敌人,毕竟他自己也尝到了苦头,那便是永远不能停止分类和修整的痛苦,如西西弗斯一般永世不能停下!别听那个叫加缪的老头儿乱说,天天推石头可不幸福! + +终于,感谢上天的宽恕,误入迷途的魔头 Eltrac 得到了分类之神 cartlE 的指点。cartlE 刚莅临网站,就立刻发觉稻草人不应与 `/posts` 的子民待在一起。看着 Eltrac 在代码里给稻草人周刊写下的各种特例,神明 cartlE 皱起了眉头,发出疑问:“你是在解鸡兔同笼问题吗?把鸡放鸡笼里,兔放兔笼里不行吗?” + +于是,`/posts/weekly` 从极客死亡计划的土地上消失了,取而代之的,是新成立的 `/weekly`。天下苦稻草人周刊久矣!为了恕罪,也向 cartlE 神明展示自己的悔意,Eltrac 立马给稻草人修好了[黄砖路](/weekly/),供他自由通行。 + +很快,高明的 cartlE 发现了 `/posts` 和 `/essays` 之间的裂谷,便命信息与沟通之神墨丘利去唤来双子座,让它用蟒蛇架起桥梁,把两块大陆合二为一了。终于,失散已久的家人又重聚了,现在 `/posts` 和 `/essays` 不再分裂,他们有了共同的新名字——`/article`。墨丘利还提议,尽管两个大陆合二为一,但旧的居民仍然保留旧的地址,避免书信不能送达,尤其是来自联邦宇宙和万维网提及的消息。[^1]新的居民以 `/article/xxx` 为地址,而旧的居民仍然保留 `/essays/xxx` 和 `/posts/xxx` 的地址。 + +为了展示自己真的回心转意,大魔王 Eltrac 主动提出,应该把 `/fictions` 分区更名为 `/fiction`,因为目前的其他分区分别是 `/article` `/weekly` 和 `/library`,只留一个复数形式的名字有些眨眼。神明 cartlE 听完,满意地离去了,并留下秘籍供 Eltrac 学习良好 Git commit 习惯的艺术。 + +就这样,暮光闪闪和他的小伙伴们终于让混沌之王无序痛改前非,懂得了友谊的魔力,小马国又恢复到了祥和之中。 + +*等等,你串台到哪儿去了?* + +## ESC to Reality + +*我的天哪,究竟是什么样的读者才能读懂上面那一大坨自我意识过剩的产物……* + +让我按下 Escape 键(指切换为 Neovim 的 `NORMAL MODE`,正常模式),来认真解释一下极客死亡计划目前的内容分类设计。 + +首先,博客目前只有四个分区: + +1. [文章](/article/):一般来说,我突然想写点什么东西发出来,都会放在这。 +2. [周刊](/weekly/):每周一更新的刊物,包含这周读到的有趣的文章、听到的播客和发现的不错的项目等等,是我整理和收集信息的地方。 +3. [书目](/library/):我读完一本书都会写书评放在这里。 +4. [虚构](/fiction/):我写的各类小说。 + +其他分区几乎都是不言自明的,只有「文章」需要特别说明。正如《命途多舛的极客死亡大地》一节中所述,这个分区原本是 `/posts`,一开始被称作「议叙」页面,因为它既包含议论文,又包含叙事文和散文。后来,我觉得这两种文体或许需要分开,于是新增了一个 `/essays` 分区,把议论文都放到这里来。 + +其实在一开始创建 `/essays` 分区的时候,我就发现了问题,我很难判断一篇文章的归属,我记得当时我花了一个下午的时间给文章重新分区。这种分区在后来又造就了更多的问题,比如:同一个「文章系列」应不应包含来自两个分区的内容?我有一个文章系列叫《代码炼金术》,这里面可能包含我尝试新技术的经历和感受,重点在于「我做了什么」,写得比较散,应该归为 `/posts`;而有的时候,我可能发现了一种新的构建软件的方式,比如[用 Lisp 而不是 HTML 写网页](/posts/webpage-as-lists/),这种有深思熟虑且形成可复用的体系的文章,似乎应该归为 `/essays`。可是,它们都是和写代码相关的,不应该都放到《代码炼金术》这个文章系列里吗?如果一个文章系列里既有议论文,又有记叙文,那不是乱套了吗? + +就算不考虑文章系列,确定一篇文章属于 `/posts` 还是 `/essays` 也不容易。前者是写「我做了什么」,后者是写「我思考并的出了什么」,实际上有相当一部分文章的写作逻辑是「我做了一件事情,并且借此有了新的思考,产生了新的想法」,那这还要怎么分区? + +从盒子里跳出来想问题:给议论文和记叙文分区真的是有必要的吗? + +最后我接受了自己的风格就是叙中有议、议中有叙的,于是把两个分区合并到了一起。不过周刊里的东西实在是太杂了,给它单独设立分区会更合理。 + +## Tag, You're It + +你可能会以为,这个前不久还重新装修过[标签](/tags)和[文章系列](/categories)页面的 Eltrac,又要阴晴不定地把标签和文章系列的其中一个分类给扬掉了,毕竟已经有分区了,再算上这两个分类,网站实际上有三个维度的分类,这太复杂了! + +当然,看到「你可能会以为」几个字就知道,我不会这么干。 + +保留文章系列其实很好理解,保留标签是为了什么? + +之前在 [Jim Nielsen](https://blog.jim-nielsen.com/2022/visualizing-my-blogs-links/) 的博客上读到过这样的观点:你不需要创建标签,链接就是标签。这里的链接是指在文章中链接到的其他文章,文章之间相互引用所形成的关联,就足够让文章物以类聚了,不需要手动打标签,做额外的分类。这的确是很诱人的想法,所以我给博客做了[视觉化的双向链接图谱](https://www.geedea.pro/graph.html),然后就把「用链接替代标签」这事抛之脑后了。 + +不移除标签,首当其中的原因就是,双向链接图谱虽然很酷,但是不方便查询。如果真的要找我写的有关某一主题的文章,到像海一样的双链图谱里去找是低效的,这些文章之间也不一定有超链接关联。 + +再者,我的博客也不是维基百科。据说从维基百科的某个页面开始,一直点击页面中的第一个超链接前往不同的页面,最终会到达「哲学」这个词条。我添加超链接的习惯不如维基百科编辑者那样专业、审慎和克制,没法做到这么严谨的关联,说实话,也不需要这样严谨。 + +不过,要说最重要的原因,那还是因为我想要按照主题划分文章。我是个兴趣泛滥者,如果你关注我的博客足够长的时间,就会发现我既谈游戏开发,又谈前端开发,还做命令行工具;我不仅写计算机,还常常观察人类,并写下思考;我不仅对社会学、心理学和经济学这些科学感兴趣,还对文学、哲学和语言学感兴趣,我甚至还探索神秘学,会研究占星和塔罗牌;即便是文学这一个类别,我也会涉猎法国文学、拉美文学、日本推理文学和各种杂七杂八的门类。 + +五花八门的内容要是不按照主题分类,恐怕读者难以把握脉络,也难以找到自己真正感兴趣的内容。假设一个读者只是因为神秘学而关注我,他就可以去阅读[#神秘学](/tags/神秘学/)标签下的内容。标签实际上提供了内容的切片。 + +此外,并不止是文章,在我的设想里,网站的所有内容都通过标签来归类,而文章系列只是给文章的。如果你打开网站的标签页来看过,就可能发现 [#心理学](/tags/心理学/) 这个标签下不仅有我写的文章,还有《[被讨厌的勇气](/library/2024/被讨厌的勇气/)》《[天生不同](/library/2025/天生不同/)》《[如何在黑暗的房间里找到一只猫](/library/2025/如何在黑暗的房间里找到一只黑猫/)》等书籍的书评,实际上小说和周刊也按照标签归类。 + +标签页的截图,截于 2026 年 3 月 8 日") + +不过,现实有些残酷,从 [GoatCounter](https://stats.geedea.pro/) 的统计数据来看,几乎就没有访客会点开任何一个标签页面,我设想中的「读者可以按照自己感兴趣的话题查看内容」这个需求用例实际上根本不存在。对我自己而已,我也很少在某天突发奇想「要不要看看自己写过什么和占星有关的文章呢?」,然后点开标签页面查看,对我自己的用处也不大。 + +所以,为什么要留着标签这个东西呢?管理标签费事费力,为了打标签,每次都要先查看已有的标签,用人眼浏览一遍,看看有没有能对得上的;还要克制住自己新增标签的欲望,避免出现太多只有一两篇文章的孤立标签;更麻烦的是,还要记住各个标签的语义,不能混用。标签也很难体现出层级关系,却在很多场景下不能避免进行细分,比如博客就同时存在「神秘学」「占星」和「塔罗」三个标签,而第一个标签实际上是后两个标签的大类——能不能合并成一个呢?它们真的经常出现。然而,有时候我只是广泛地谈一谈神秘学,比如《[好为人师如何帮我反思现代科学的局限性?](/essays/好为人师如何帮我反思现代科学的局限性/)》这篇文章,仅仅是在论述「适当的玄学和迷信为什么是有用的,甚至能达到理性的科学知识达不到的效果」这个观点;有时候,我又是在具体地谈[塔罗牌里某张牌的含义](/categories/塔罗牌漫谈/),这就没必要打上更宽泛的「神秘学」标签了。[^2] + +无论以上这些问题的答案如何,都不能发现,标签的存在本身就引发了许多管理难题,而它带来的实际价值却非常少。很明显,我应该把它删掉。 + +可我偏不。 + +为什么?答案其实相当不理性,但也相当具有说服力:我不想把它删掉。我想我可能是喜欢标签体系带来的秩序感,也需要建立和维护秩序的感觉。另外,我总觉得,网站的分类就和图书馆一样,大部分找书的人其实都是找到具体书籍的编号之后,直奔书本所在地,但也不能忽视,有很少的一部分人愿意在某个主题的书架附近漫游,发现从未见过的书本。所以,标签可能大多数时候派不上用场,但对我而言,是不能没有的。 + +## 最后 + +所以,这大概就是新的秩序了。 + +《极客死亡计划书》系列文章的最后,还是要给下一期留个引子。我正在思考博客的交互设计。一两个月前就有关注我的读者可能有印象,以往博客文章的底部有一个「心脏」按钮,将鼠标移上去,显示的文字是「为这篇文章献上心脏」,点击之后,心脏图标会变红,文字会变为「你已经献上心脏了」。这其实是点赞按钮,任何人都可以点击它,让点赞数量增加 1。并且,读者点赞之后,页面的最下方会出现一整排「已经收集到的心脏」,有多少点赞数就有多少心脏。我印象最深的是,有一篇文章的点赞数量超过了 20,当时页面下面非常壮观。 + + + +可惜,「献上心脏」随着[评论系统的移除](/posts/site-report-4/)一同消失了,一直没有加回来的原因是,我在纠结如何把本身就有些复杂的新交互区设计得更简洁一些,增加元素不太明智。再者,现在博客本身也会同步来自联邦宇宙的点赞,功能上有些重叠。 + +不过,我想我大概还是会想办法把「献上心脏」加回来的,因为我的确需要一些实在的反馈,不只是访问统计数据。说实话,现在会在联邦宇宙上点赞和留言的读者不算多,至少我很久没看到点赞的数量超过 10 了。尽管,最健康的态度应该是完全不在乎这些数据,只关注和真实的人建立起的链接,但…… 我想我还需要再思考一下。 + +以前,网站的标签页面还有一个操作按钮,是一只乌鸦,按下之后会隐藏当前页面下的所有「稻草人周刊」。当时这么设计,是因为稻草人周刊混杂在文章分区内,本身的内容又很杂,容易让人抓不住重点,所以提供了一个筛选功能。当时我还费心设计了动画,把鼠标移动到乌鸦头上,页面里的稻草人周刊链接就会闪烁,点击之后这些链接就会收缩起来,直到消失;再次点击按钮,这些链接又会在入场动画之后出现。由于现在周刊是单独的分区了,没有混杂在文章里,这个按钮也就不再需要了。 + +这些好的改动,似乎磨灭了一些个性,有些不符合我在《[「极客死亡计划」的设计哲学](/essays/极客死亡计划的设计哲学/#8-适当地表达个性)》里提到的第八项原则。当时举的例子里,还留下的就只有 404 页面和 Batrick[^3] 了。 + +这些设计的去留和增改的确需要好好思考一下,不过这就是下期的内容了。 + +回见! + +[^1]: OK,我知道我有点放飞自我了,所以我~~暂时脱离精神分裂状态~~来解释一下,这里说的是:我用 Gemini(双子座)生成了一个 Python(蟒蛇)脚本,让它把两个分区合成一个了,之所以提到墨丘利(Mercury),是因为双子座的守护行星是水星(Mercury)。由于 Webmention 依赖 URL 标识源地址和目标地址,如果改了地址,仅仅作重定向是不够的,会导致旧的 Webmention 失效,除非重新发送一遍,所以就用 Python 脚本给每篇文章都添加了 `url` 属性,这样就算文件放在 `/article/xxx.md` 下,URL 也还是 `/posts/xxx` 或者 `/essays/xxx`。 + +[^2]: 不要建议我用大语言模型自动打标签,如果我不能清楚明白每个标签的具体语义,那做分类还有什么意义?为什么要为了支撑一个已经丧失了意义的事物的存在,而浪费大量的算力? + +[^3]: 如果你把页面往上滚动,就会看到一直蝙蝠从页面顶部飞下来,它是「返回顶部按钮」。点击它,Batrick 就会带你上去。 + diff --git a/content/categories/稻草人周刊/_index.md b/content/categories/稻草人周刊/_index.md deleted file mode 100644 index de4699c8..00000000 --- a/content/categories/稻草人周刊/_index.md +++ /dev/null @@ -1,5 +0,0 @@ -+++ -title = "稻草人周刊" -description = "一个读者的自述,**每周一更新**。" -pinned = true -+++ diff --git a/content/essays/_index.md b/content/essays/_index.md deleted file mode 100644 index e41c3b3d..00000000 --- a/content/essays/_index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -title = "议论集" -+++ -博客上相对严谨的部分,有关好奇心、知识、人类,以及如何思考。 diff --git a/content/faq.md b/content/faq.md new file mode 100644 index 00000000..b3551d03 --- /dev/null +++ b/content/faq.md @@ -0,0 +1,18 @@ ++++ +title = '解惑' +subtitle = 'Foreseeably Asked Questions' +date = 2026-03-03T23:57:57+08:00 +draft = true +toc = true ++++ + +众所周知,FAQ 含义并不是「Frequently Asked Questions」,而是「Foreseeably Asked Questions」,即可预见的会被问到的问题。不过,说到底,这个页面的大部分内容只是我一个人在自问自答而已。 + +## 能不能制作一个「繁简转换」功能? + +我了解到博客的读者中的确有一些来自台湾,偏好使用繁体中文,我之前也考虑过添加「繁简转换」功能,在 Telegram 上询问过意见,收到了一些回复和反馈,最终决定放弃。具体的原因如下: + +除非是自己校对过的译文,不建议添加繁简转换,因为繁体中文和简体中文并不只是字符上的区别,还有用词和用语习惯的区别,比如「博客」在台湾就被称作「部落格」,计算机的「内存」在台湾叫作「记忆体」(memory)。如果直接替换,就有可能出现把「海内存知己」转换为「海记忆体知己」的情况,会产生误解。据我了解,不同地方的繁体中文也有不少区别,马来西亚的中文和台湾的中文并不是一样的。 + +我对机器翻译的态度同理,博客不会包含任何用机器翻译的译文,博客的[英文部分](/en)也不是中文部分的译文,都是单独用英文写就的文章。一般来说,我不会在博客上发布自己文章的译文。 + diff --git a/content/posts/_index.md b/content/posts/_index.md deleted file mode 100644 index f2714fc5..00000000 --- a/content/posts/_index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -title = "散文集" -+++ -赤裸的情感、散乱的思绪和真实的生活在这里倾泻而下。 diff --git a/content/weekly/71.md b/content/weekly/71.md new file mode 100644 index 00000000..698b2409 --- /dev/null +++ b/content/weekly/71.md @@ -0,0 +1,151 @@ ++++ +title = '稻草人周刊 Vol.71' +subtitle = '' +tags = [] +date = 2026-03-04T09:24:29+08:00 +draft = true +toc = true ++++ + +{{< music title="Voicenotes" artist="Charlie Puth" cover="https://r2.eltr.ac/geedeapro/posts/weekly/71/Charlie_Puth_Voicenotes.png" apple="https://music.apple.com/cn/album/voicenotes/1293229669" spotify="https://open.spotify.com/album/0mZIUXje90JtHxPNzWsJNR" >}} + +《Voicenotes》是 Charlie Puth 的第二张录音室专辑,2018 年的热单《Attention》就出自这张专辑。最近在找几年前的专辑来听,走路和跑步时常听这一张。我很喜欢 Charlie Puth 的风格,不过我缺乏基本的乐理知识,不知道从何说起了。这张专辑里我最喜欢的歌曲其实是第一首《The Way I Am》。 + +> I'ma tell 'em all +> +> I'ma tell 'em all that you could either hate me or love me +> +> But that's just the way I am +<!--more--> + +顺带一提,Charlie Puth 的下一张专辑《Whatever's Clever》在本月底就要发行了。我想在这里引用 Taylor Swift 歌曲里的一句歌词: + +> Charlie Puth should be a bigger artist。[^3] + +就这样,继续看周刊吧。如果不麻烦的话,还请留意这一期的「当下」栏目。 + +--- + +## 连接 + +### 程序员如何沟通? + +> [!note] 📜 +> [What I Do](https://inessential.com/what-i-do.html) + +[NetNewsWire](https://netnewswire.com/)(macOS 和 iOS 上的自由且开源的 RSS 阅读器)的开发者 Brent Simmons 尝试向亲友解释他的工作,我很喜欢文中的某些句子: + +> I know I’m a big nerd and what I do must seem all super nerdy and vague. <br/> +> 我知道我是个大书呆子,我做的事情一定看起来超级书呆子,让人看不明白。 + +> My blog inessential.com (this site) has been going since 1999. I mostly write about Apple nerd stuff. Other Apple nerds read it. <br/> +> 我的博客 inessential.com(这个网站)从 1999 年就开始写了。我主要写 Apple 书呆子相关的东西。其他的 Apple 书呆子会读。 + +> In the Apple nerd world I’m the NetNewsWire guy. <br/> +> 在 Apple 书呆子们的世界里我就是 NetNewsWire 哥。 + +> So I’m a nerd and a computer programmer. But what I am not is a math nerd... I’m usually the worst person at math in any room I’m in. Shock, I know! <br/> +> 对,我是个书呆子,是个程序员。但我绝对不是数学书呆子…… 我在任何地方通常都是那个数学最差的人。令人震惊,对吧!? + +好了,对 nerd 这个词的滥用我们看够了。读完之后我突然觉得 Brent 变得很亲切,因为我和他一样,数学非常差劲!虽然有些难以启齿,但仔细想想也没什么好隐瞒的:我的高考数学成绩只有 68 分!Brent 说他没有大学毕业,但就算毕业了,也会得到英语学位,而不是数学学位。 + +似乎全世界都有这样的误解:程序员一定数学很好,要学编程一定要先学好数学。我记得我的初中老师就问过我:“你学这个编程,难道不需要把数学学好吗?” + +他关于软件开发的观点我很喜欢,他认为==编程的重点不是数学,而是沟通==。首先是程序员和机器的沟通,更重要地,如果你在开发一个 App,也要考虑如何设计软件,让软件更好地和用户沟通。我在学软件工程相关的课程,例如需求工程、软件项目管理和架构设计的时候,也逐渐意识到,软件开发中最重要的过程从来不是写代码,而是沟通。架构设计是一种沟通、需求获取是一种沟通,更别提开发团队内部的沟通和与用户的沟通了——这也是我不担心程序员会被 LLM 取代的原因。 + +> Like any other popular art form, it’s about connecting with humans. <br/> +> 就和其他流行的艺术形式一样,重点在于和人的链接。 + +我想我比起后端更喜欢前端开发的原因也是如此,前端与用户的链接感会更强。不过从技术上,我还是不太喜欢 JavaScript 生态。记得去年还有过学 Swift 做苹果开发的想法,兴许最近也可以捡起来了,先等水逆过去吧。 + +### 擦马桶如何帮我维持学者风范? + +> [!note] 📻 +> [《独树不成林》315 - 擦马桶如何帮我维持学者风范](https://www.xiaoyuzhoufm.com/episode/69ab81365b2d0ed069b63f7d) + +听这期博客的前一天刚刷过马桶,亮到能反光。于是我一边把洗手间的各个角落刷了一遍,一边把这期播客听了两遍。我最喜欢的句子是: + +> 在网上喷粪不如在现实中擦粪。 + +树老师说他不焦虑、不想死,是因为每天都要做家务,没空想东想西。整天坐在电脑前从事思考工作的人容易产生这样一种假象,认为一切都在控制当中,然而当生活中的小事出了差错,这种差错很容易降他们击垮,陷入崩溃当中。我也赞同,在网络世界、精神世界、知识世界中待久了,需要关注现实,而且是与自己有着直接关联的现实,而非别人的生活、海另一边的战争、某个无聊的家伙对自己的谩骂。 + +刚打开这期播客没多久,我就做了一个决定。我找到我的室友,告诉他以后的家务都由我来做。这里要交代一个背景:先前我们是按照时间来划分家务职责的,一个人负责一天,但他老是忘记,我也常因为各种小事跳过自己做家务的那一天;最近改成了按照事务划分,每个人负责一部分,当时厕所划分给了我,与是我立马把马桶擦得锃亮给他打了个样,可惜激励效果欠佳。某天我看不下去,自己动手吸尘拖地(这本该是他负责的工作)。于是我想,比起每天阴湿地暗暗谩骂对方不好好做家务,不如自己把活儿全干了。我告诉他接下来的家务我全包了之后,他还有些尴尬地回应说自己最近太忙了。 + +我最近也没闲着,野心勃勃地同时推进着两个个人项目,学校那边也有各种琐事,辅导员和某些老师也在推着我做职业规划,焦虑是难免的。正是因此,我才开始比以往更加频繁、细致地做家务,把自己的大脑从想东想西的状态拉出来,开始关注身体、生活环境和秩序。 + +*所以,读者有没有什么瓷砖和木地板的清洁剂推荐呢?* + +### 亚当·斯密如何颠覆古典美德? + +> [!note] 📻 +> [《政治哲学圆桌》15 - 亚当·斯密如何颠覆古典美德?](https://www.xiaoyuzhoufm.com/episode/69a8f5ebc8cdeb38c2e5a3ed) + +~~赵老师的讲话方式指定跟树老师学了不少。~~ + +播客前半部分阐述了亚当·斯密(《国富论》的作者,现代经济学之父)的生平和轶闻,后半部分进入正题,讲述亚当·斯密眼中现代社会应有的美德,与古典美德有何不同,以及他对于古典美德的观点。 + +美德在拉丁语中写作 virtus,也就是如今英语里的 virtue,这个词的词根 *vir* 意思是「男人」,拉丁语中美德的本意是 manliness,可以译作「男子气概」。古希腊语中的美德 ἀρετή 也是类似的意思。谈到古典美德,一般会说到柏拉图的四个枢德,即节制、审慎、勇气和正义[^2],其中勇气是最重要的。相对的,温柔等特质在古典语境下通常与女性联系起来。简单来说,在古希腊人和古罗马人看来,最勇猛的战士就是最具美德的人。 + +亚当·斯密认为,古典美德所强调的男子气概是野蛮的、原始的、落后的,现代社会所需要的美德应该是具有同理心(sympathy)的美德,即设身处地地为他人着想。在古典美德中,美德有高低贵贱之分,而且往往是生来就具有或者没有的,比如贵族的美德就要比奴隶的美德更高;而亚当·斯密认为的美德是人人都能够获得的,是在社会交往中通过反思获得的。简单来说,亚当·斯密的美德是一种更温顺的美德,如果说古典美德实际上是男人的美德,那么亚当·斯密的美德就更接近女人的美德。不过,亚当·斯密所言的同理心其实和大多数人理解的意思也有差异,比如,他认为和商贩交易也是同理心的体现,因为顾客设身处地地为对方着想,提供了对方所需要的东西;再比如,他认为富人和快乐的人更容易引发同理心,而不是穷人和悲伤的人。 + +对于这两种美德,也可以联系到第 42 期周刊引用的[古典审美与现代审丑](/posts/weekly/42/#古典审美与现代审丑)。在我看来,古典美德在如今的视角下更接近于自我要求,而强调同理心的现代美德更接近于社会要求。个体可以要求自身节制,比如节制饮食,但在社会层面,应该对肥胖者有同理心,不去责备他们缺乏节制的美德。[^1] + +### 锻炼是为了什么? + +> [!note] 📻 +> [《独树不成林》313 - 如何学习快乐老女人运动法](https://www.xiaoyuzhoufm.com/episode/69a84afae46749707ffdff3c) + +树老师在这期运动闲聊播客里分享了她母亲的锻炼心态,她认为她母亲的心态要比她自己和许多年轻人都健康不少。她的母亲在过去的几十年人生里都没有任何运动习惯,直到年过五十,发现身体正在明显地衰老、恶化,才开始为了维持生命而运动。她锻炼的目的是为了对抗衰老和掌控身体,是为了锻炼神经系统调动身体部位的能力,每天能动起来她就很快乐了。 + +对比另一种锻炼心态,追求进步、对比和竞争的锻炼,「快乐老女人」的锻炼心态要更健康。前者容易陷入这样的窘境:一旦做不到进步,或者进步不够明显,锻炼的乐趣和动力就会大大减少;一旦有了对比,锻炼的目的就不再是满足自己身体的需求,而是为了迎合别人的审视;一旦有了竞争,人就会感到焦虑。这种锻炼心态甚至成为了门槛,让没有锻炼习惯的人认为,自己要是没有运动天赋,就不该开始运动。实际上,每天都让自己动起来这个事实本身就值得骄傲,就像自己又活了一天一样值得庆祝。 + +关于锻炼的目的,我的观点可以引用 Steven Pressfield 的《[一生之敌](/library/2025/一生之敌/)》中的一句话来阐释:*除了爱,出于任何其他原因而从事艺术创作,都是卖淫。* 把这句话的「从事艺术创作」换成「锻炼」,就是我的观点,而且我觉得这句话用在「身体」相关的话题上,显得更有说服力。 + +锻炼是为了保证身体的机能,提升神经适应性,保证自己的身体能做到它应该做到的事情。简单来说,是为了健康——*我那个经常跑健身房,买五花八门运动补剂,没事就对着镜子拗造型的室友,还对我这个目的颇有微词。* 当然我不是说健身房撸铁增肌就不好,我向来很欣赏热爱力量训练并关注身体变化的人,不过我欣赏的是能从训练和身体变化本身得到满足的人。 + +更准确地说,我欣赏所有好好为自己和自己热爱的事情活着的人,在我看来,这才是生命力的体现,而生命本身就是意义。 + +## 星群 + +### GRAM + +[Zed](https://zed.dev/) 的分支,托管在 Codeberg 上,移除了服务条款和 AI 功能,是纯粹的自由软件,也是纯粹的编辑器。Zed 和 GRAM 是用 Rust 语言编写的 VS Code 替代品,换用 Neovim 之前我一直在使用 Zed,至少在 macOS 上启动速度很快,界面也很干净,是我很喜欢的用户界面风格。 + +不过 Zed 并不是纯粹的自由软件,因为它有[服务条款](https://zed.dev/terms),还有付费的 AI 功能,是开源的商业软件。最近 Hacker News 上还有有关 Zed 的小[讨论](https://news.ycombinator.com/item?id=47240158),因为 Zed 在服务条款里增加了年龄要求,不过并不是威胁隐私的年龄「验证」,相关内容见[我的帖文](https://akk.eltr.ac/objects/bdd43b12-41ed-41b8-9bf5-14d492082d3b)。尽管本体是 GPL 协议,但项目的 README 下赫然写着: + +> Zed is developed by Zed Industries, Inc., a for-profit company. + +那就不能指望挣钱的企业抛弃讨厌的 AI 功能了。GPL 协议的好处就是自由,可以随意更改和分发软件,于是一个移除了服务条款和 AI 功能的 Zed 编辑器就诞生了!*也不知道我这个 Neovim 用户在激动什么。* + + + +如果你在找 VS Code 替代品[^4],GRAM 值得一试。 + +访问:[GRAM](https://gram.liten.app/) 或 [Codeberg 仓库](https://codeberg.org/GramEditor/gram) + +## 当下 + +不出意外的话,这应该是《稻草人周刊》里最后一次出现「当下」这个栏目,原因是我正在计划一个每月更新的新闻报,也就是「Newsletter」,名字暂时还没有定下来。新闻报中计划包含这些内容: + +1. 过去一个月里,自认为写得不错的博客文章(其实《稻草人周刊》也有「回拨」这个栏目,原本打算每个月写一次,但总是忘记,干脆放到月刊里) +2. 正在进行或已经完成的项目,情况以及进度回报(我对「项目」的定义很广泛,一般来说是软件项目,也可以是目标明确且耗时较长的大任务,比如「把家里的所有塑料制品都替换掉」) +3. 书影音游回顾,包含这个月看的剧、写过的书评合集和完整听过的专辑等(也希望能借此治好我的电子阳痿吧) + +这意味着《稻草人周刊》会更关注于分享我每周读到的有趣文章、听过的觉得有启发的播客、发现的觉得不错的项目。如果你关心我最近做了什么事情,可以等待新的月刊,预计会在四月初发布第一期;或者,你可以在[联邦宇宙](https://akk.eltr.ac/@eltrac)上关注我,可以用 Mastodon、Misskey、Pleroma 等联邦宇宙软件与我社交。 + +## 切片 + +- 状态低迷、注意力涣散和效率低下的状态持续挺久了,春节结束回到家也不见好转,遂求助玄学,盘了半天月运势和周运势也没看出个名堂,直到突然被提醒天象:从二月底开始水星逆行了,直到三月 20 日结束。 + + 水逆啊,那不稀奇了,放心躺吧。 + + *双子座加月升处女已经被水逆折磨习惯了* + +[^1]: 相关博客文章:《[关于肥胖的杂谈](/essays/关于肥胖的杂谈/)》 + +[^2]: 参见:[美德 - Wikipedia](https://zh.wikipedia.org/zh-hans/%E7%BE%8E%E5%BE%B7#:~:text=%E6%BA%90%E8%87%AA%E4%BA%8E%E6%9F%8F%E6%8B%89%E5%9B%BE%E7%9A%84%E5%9B%9B%E4%B8%AA%E7%BB%8F%E5%85%B8%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%BE%8E%E5%BE%B7%EF%BC%88%E6%9E%A2%E5%BE%B7%EF%BC%89%E6%98%AF%EF%BC%9A%E8%8A%82%E5%88%B6%EF%BC%8C%E5%AE%A1%E6%85%8E%EF%BC%8C%E5%8B%87%E6%B0%94%E5%92%8C%E6%AD%A3%E4%B9%89%E3%80%82) + +[^3]: 出自《The Tortured Poets' Department》 + + +[^4]: 是时候抛弃这个基于 Electron 的软件,~~停止用浏览器编辑代码了~~ diff --git a/tag-wrangler.py b/tag-wrangler.py deleted file mode 100644 index 1ac896d1..00000000 --- a/tag-wrangler.py +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python3 -""" -Hugo 分类管理脚本 -支持删除或重命名 categories -""" - -import re -import sys -from pathlib import Path -from typing import Tuple, List, Optional - -import yaml -import tomli -import tomli_w - - -class FrontmatterParser: - """解析和修改 frontmatter""" - - YAML_PATTERN = re.compile(r'^---\s*\n(.*?)\n---\s*\n', re.DOTALL) - TOML_PATTERN = re.compile(r'^\+\+\+\s*\n(.*?)\n\+\+\+\s*\n', re.DOTALL) - - @staticmethod - def parse(content: str) -> Tuple[Optional[dict], str, str, str]: - """ - 解析 frontmatter - 返回: (data, body, format, raw_frontmatter) - format: 'yaml', 'toml', or 'none' - """ - # 检测 YAML frontmatter (---) - if content.startswith('---'): - match = FrontmatterParser.YAML_PATTERN.match(content) - if match: - yaml_content = match.group(1) - body = content[match.end():] - try: - data = yaml.safe_load(yaml_content) or {} - return data, body, 'yaml', yaml_content - except yaml.YAMLError as e: - raise ValueError(f"YAML 解析错误: {e}") - - # 检测 TOML frontmatter (+++) - if content.startswith('+++'): - match = FrontmatterParser.TOML_PATTERN.match(content) - if match: - toml_content = match.group(1) - body = content[match.end():] - try: - data = tomli.loads(toml_content.encode('utf-8')) - return data, body, 'toml', toml_content - except Exception as e: - raise ValueError(f"TOML 解析错误: {e}") - - # 没有 frontmatter - return None, content, 'none', '' - - @staticmethod - def serialize(data: dict, body: str, format_type: str) -> str: - """将 data 和 body 序列化为字符串""" - if format_type == 'yaml': - # 清理 None 值 - clean_data = {k: v for k, v in data.items() if v is not None} - if not clean_data: - return body - yaml_content = yaml.dump(clean_data, allow_unicode=True, sort_keys=False, default_flow_style=False) - return f"---\n{yaml_content}---\n{body}" - - elif format_type == 'toml': - # 清理 None 值,TOML 不支持 null - def clean_for_toml(obj): - if isinstance(obj, dict): - return {k: clean_for_toml(v) for k, v in obj.items() if v is not None} - elif isinstance(obj, list): - return [clean_for_toml(item) for item in obj if item is not None] - elif obj is None: - return "" - return obj - - clean_data = clean_for_toml(data) - if not clean_data: - return body - try: - toml_bytes = tomli_w.dumps(clean_data) - toml_str = toml_bytes.decode('utf-8') if isinstance(toml_bytes, bytes) else toml_bytes - return f"+++\n{toml_str}+++\n{body}" - except Exception as e: - raise ValueError(f"TOML 序列化错误: {e}") - - else: - return body - - -class CategoryManager: - def __init__(self, content_dir: Path, dry_run: bool = True): - self.content_dir = Path(content_dir) - self.dry_run = dry_run - self.parser = FrontmatterParser() - self.stats = { - 'processed': 0, - 'modified': 0, - 'skipped': 0, - 'errors': 0 - } - - def get_categories(self, data: dict) -> List[str]: - """ - 获取 categories,支持多种格式: - - categories: "single" - - categories: ["array", "of", "cats"] - - category: "single" (别名) - """ - if not data: - return [] - - # 优先检查 categories - cats = data.get('categories') - if cats is None: - # 尝试 category(单数形式) - cats = data.get('category') - - if cats is None: - return [] - - # 统一转换为列表 - if isinstance(cats, str): - return [cats] - elif isinstance(cats, list): - return cats - else: - return [str(cats)] - - def set_categories(self, data: dict, categories: List[str]): - """设置 categories,如果为空则删除该字段""" - if 'category' in data: - del data['category'] - - if not categories: - if 'categories' in data: - del data['categories'] - else: - data['categories'] = categories - - def delete_category(self, file_path: Path, target_cat: str) -> bool: - """从文件中删除指定的 category""" - try: - content = file_path.read_text(encoding='utf-8') - data, body, fmt, _ = self.parser.parse(content) - - if data is None: - return False - - categories = self.get_categories(data) - if target_cat not in categories: - return False - - # 删除目标 category - new_categories = [c for c in categories if c != target_cat] - self.set_categories(data, new_categories) - - # 生成新内容 - new_content = self.parser.serialize(data, body, fmt) - - if not self.dry_run: - file_path.write_text(new_content, encoding='utf-8') - print(f"✅ 删除 '{target_cat}' from {file_path.relative_to(self.content_dir)}") - else: - print(f"[DRY-RUN] 将删除 '{target_cat}' from {file_path.relative_to(self.content_dir)}") - if not new_categories: - print(f" categories 字段将被删除") - else: - print(f" 新 categories: {new_categories}") - - return True - - except Exception as e: - print(f"❌ 错误处理 {file_path}: {e}") - self.stats['errors'] += 1 - return False - - def rename_category(self, file_path: Path, old_cat: str, new_cat: str) -> bool: - """重命名文件中的 category""" - try: - content = file_path.read_text(encoding='utf-8') - data, body, fmt, _ = self.parser.parse(content) - - if data is None: - return False - - categories = self.get_categories(data) - if old_cat not in categories: - return False - - # 替换 category - new_categories = [new_cat if c == old_cat else c for c in categories] - self.set_categories(data, new_categories) - - # 生成新内容 - new_content = self.parser.serialize(data, body, fmt) - - if not self.dry_run: - file_path.write_text(new_content, encoding='utf-8') - print(f"✅ 重命名 '{old_cat}' -> '{new_cat}' in {file_path.relative_to(self.content_dir)}") - else: - print(f"[DRY-RUN] 将重命名 '{old_cat}' -> '{new_cat}' in {file_path.relative_to(self.content_dir)}") - print(f" 新 categories: {new_categories}") - - return True - - except Exception as e: - print(f"❌ 错误处理 {file_path}: {e}") - self.stats['errors'] += 1 - return False - - def scan_files(self) -> List[Path]: - """扫描所有 markdown 文件""" - md_files = [] - for ext in ['*.md', '*.markdown', '*.mdown']: - md_files.extend(self.content_dir.rglob(ext)) - return md_files - - def run_delete(self, target_cat: str): - """执行删除操作""" - print(f"\n{'='*60}") - print(f"🗑️ 删除分类: '{target_cat}'") - print(f"{'='*60}") - print(f"模式: {'预览 (dry-run)' if self.dry_run else '实际执行'}") - print() - - files = self.scan_files() - found_count = 0 - - for file_path in files: - self.stats['processed'] += 1 - if self.delete_category(file_path, target_cat): - found_count += 1 - self.stats['modified'] += 1 - - print(f"\n{'='*60}") - print(f"处理完成: 扫描 {self.stats['processed']} 个文件") - print(f"找到并处理: {found_count} 个文件包含 '{target_cat}'") - print(f"错误: {self.stats['errors']}") - - if self.dry_run: - print(f"\n💡 预览模式,未实际修改。使用 --execute 执行实际操作") - - def run_rename(self, old_cat: str, new_cat: str): - """执行重命名操作""" - print(f"\n{'='*60}") - print(f"✏️ 重命名分类: '{old_cat}' -> '{new_cat}'") - print(f"{'='*60}") - print(f"模式: {'预览 (dry-run)' if self.dry_run else '实际执行'}") - print() - - files = self.scan_files() - found_count = 0 - - for file_path in files: - self.stats['processed'] += 1 - if self.rename_category(file_path, old_cat, new_cat): - found_count += 1 - self.stats['modified'] += 1 - - print(f"\n{'='*60}") - print(f"处理完成: 扫描 {self.stats['processed']} 个文件") - print(f"找到并处理: {found_count} 个文件包含 '{old_cat}'") - print(f"错误: {self.stats['errors']}") - - if self.dry_run: - print(f"\n💡 预览模式,未实际修改。使用 --execute 执行实际操作") - - -def print_usage(): - print("""Hugo 分类管理脚本 - -用法: - python category_manager.py --delete <分类名> [--execute] - python category_manager.py --rename <旧分类名> <新分类名> [--execute] - -选项: - --execute, -e 实际执行修改(默认是预览模式) - -示例: - # 预览删除 "旧文章" 分类 - python category_manager.py --delete "旧文章" - - # 实际删除 "旧文章" 分类 - python category_manager.py --delete "旧文章" --execute - - # 预览重命名 "日记" 为 "生活" - python category_manager.py --rename "日记" "生活" - - # 实际重命名 - python category_manager.py --rename "日记" "生活" --execute -""") - - -def main(): - args = sys.argv[1:] - - if not args or '--help' in args or '-h' in args: - print_usage() - sys.exit(0) - - # 检查是否有 --execute - dry_run = '--execute' not in args and '-e' not in args - - # 移除 --execute 参数 - args = [a for a in args if a not in ['--execute', '-e']] - - content_dir = Path("content") - - if len(args) >= 2 and args[0] == '--delete': - target_cat = args[1] - manager = CategoryManager(content_dir, dry_run) - manager.run_delete(target_cat) - - elif len(args) >= 3 and args[0] == '--rename': - old_cat = args[1] - new_cat = args[2] - manager = CategoryManager(content_dir, dry_run) - manager.run_rename(old_cat, new_cat) - - else: - print("❌ 参数错误") - print_usage() - sys.exit(1) - - -if __name__ == "__main__": - main() |
