MoreRSS

site iconEltrac | 極客死亡計劃修改

常讨论心理学和社会观察相关内容,不敢说很懂哲学;也会写笔记和生产力工具相关内容;还是学生。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Eltrac | 極客死亡計劃的 RSS 预览

Ready, Steady, Go...?

2026-03-19 18:29:28

这几天出门总是发现路面被浸湿,空气冷冷的,呼吸起来很舒服,却从未见到下雨。大概是因为雨都很方便地发生在夜晚吧,不过,也有可能是我最近没怎么从家里走出来。

不知道什么时候开始,去过一趟学校之后,我就会失去和世界抗争的所有力量,然后把自己关在房间里一晚上什么也干不了,所以就不出门了。

之前写《 迷失于图形界面 》的时候,我的语气还是太收敛了,而部分路过的读者还持有对教师群体的不理性尊敬(我尊敬很少一部分教师,不是全部),以至于有人专门发邮件教我要从别人的角度思考问题,考虑老师的教学工作是如何进行的。所以我决定向读者描述最近一次,一位大学老师是怎么浪费了半个小时的时间讲授完全无用的知识的。

任何一门编程语言里,几乎都有保留字,也叫关键字,这些字符是不能用来命名自定义符号(变量、函数)的,因为它们是构成语法的一部分,比如 forifswitch 等。这本来是很简单的知识,一遍带过就好。请记住,这位老师是在给大三的学生教授 Python 这门课,他非常清楚这些学生都有 C 语言和 Java 的编程基础,这些知识他们是了解的。

这位老师在幻灯片上展示了 Python 的所有保留字,然后提出了一个匪夷所思的问题:上面这些保留字,哪些是 Python 独有的?于是接下来的几十分钟,我们都在解决这个问题。

给这场可笑的讨论雪上加霜的是老师狭隘的认知,他的眼里只有 C、C++、Java 和 Python 这几门语言,因此,虽然他的问题是「哪些保留字是 Python 独有的?」,他实际上说的是「哪些 Python 的保留字不存在于 C、C++ 和 Java 中?」,一个同样缺乏意义的问题。

“这个 asyncawait 关键字是 Python 独有的吧?”——不,JavaScript 里经常用到。

“还有 def 是 Python 用来定义函数的,其他语言里没有。”——Clojure 用 def 定义全局变量。

elif 是 Python 独有的吧?反正我是没在其他语言里见过。”——老师显然没写过 Bash 脚本。

老师甚至忽略了 Python 中布尔值写法的不同,这门语言的 TrueFalse 开头是大写的,而其他语言里一般写作 truefalse——这显然是更容易造成误解的区别,是更值得提出的问题。然而,当老师在抽点学生回答他那个毫无意义的问题时,他却批评指出这个问题的学生「钻牛角尖」。“其他语言里难道没有 truefalse 吗?仅仅是因为大写吗?我觉得不算吧。”

“我刚刚问了下豆包啊,他给我生成的结果,我看也跟我想的差不多”,是的,这位老师在课堂上临场使用大模型回答他自己提出的问题,“同学们你们也可以搜一下啊,都不用自己搜索,问问 AI 软件就好了。”

忍受这样的课堂,真是消磨我的意志力。


最近的精神状态真的日渐萎靡,昨天破天荒地在凌晨之前上床睡觉,结果没被闹钟叫醒,卡在上课前十分钟醒来,出门已经晚了,正好是一个不点名的老师的课,索性继续躺下了,醒来的时候辅导员因为旷课给我发了个黄牌警告,因为老师不点名但辅导员叫班委点了名。

今天也是,十点半的课但是十点二十才醒(不过说实话我睡得很好,这大概是最重要的),索性也没去,这节课是辅导员上的课,反倒没点名。

一节课名字叫作创新创业基础与实践,另一节课名字叫作就业指导与生涯规划。

没去上课,就在家里做了半个小时 HIIT,洗完澡之后神清气爽,感觉世界都明亮了。我在想状态萎靡有没有可能是没洗头感到不舒服导致的。

还是要少上课,多洗澡啊!

—— Eucalyptus

刚开始写这篇文章的时候本来是雨天,结果某天早上起来,天晴了。两个月前写《 All Those Who Wander 》这篇文章的时候,我不太想把它归类到《乱章》这个散文系列里,总觉得它表达了一种更深远、更柔和也更痛苦、更具美感的负面情绪,这种情绪在平常隐身,在雨天显形,像是季节性抑郁。

于是我默默把它和先前的一篇文章收进《雨天集》这个新系列里,可惜那之后的日子里,重庆都没怎么下雨。

如你所见,我现在处于一个尴尬的境地,本想用写作排忧解难的我拖延了这次写作,拖到雨停了,而我也发现自己的睡眠和情绪随着水星逆行即将结束、新的月相周期开始,逐渐好了起来。

状态逐渐恢复的另一个原因是没上课的那天畅快地洗了一次澡并且睡得很好,不如说这才是关键原因。

不过,雨水还没干,我也觉得自己需要在清醒和阳光充足的时间里,对前段时间挥之不去的负面情绪做一些理智的反思。所以,文章还是要继续写的。


两个月前我请一位塔罗爱好者朋友帮我占卜,我想知道在大四开始之前,我能不能摆脱当下所感到的迷茫,至少有清晰的职业目标。如我所料,答案是没有,或许是我想要的太多了,或许是我不愿意妥协,比如,我绝对不想要一份每天都要写恶臭的 Java 代码的工作,我不想要在主要业务令我哈欠连连的企业上班。朋友说我或许适合去有愿景的初创公司工作,我想是的,可我自己也没想好。

独立开发呢?那我岂不就成了专有软件的创造者,成为了我讨厌的人吗?诚然,编写自由软件是养不活自己的。自然也有开源但提供付费云服务的盈利模式,可我自己都不会使用这种服务,为什么要做自己不赞同的事情呢?更何况,这一切的前提,都是我有能力开发具有商业价值的软件。

我知道我所有的忧虑都有一个非常简单的解决方案:停止思考,开始行动。可怎么行动呢?像苍蝇一样到处乱撞,还是加入我讨厌的团体,写着一成不变的代码,做着无趣的事情,和无聊的人打交道?

我知道我站在这样一个岔路口,一边是令人恐惧的不确定性,另一边是味同嚼蜡的确定性。更可怕的是,所有人都在往前走,只有我蹲在原地,不敢选择。

每次去到学校,我都会被提醒那条相对而言具有确定性的那条道路有多么令人抑郁,又是如何在步步紧逼。不称职的老师自以为是地讲授着无用的知识,时不时插入一些关于 OpenClaw 的粗浅发言,让自己感到「我有在跟上潮流」。等他感到无聊了,就会吓一吓学生,不断地提醒他们大四就没有课程要修了,要出去实习找工作了。另一边,热心的辅导员要求每个学生把简历写好发给她检查,时不时发一些岗位信息在群里,拍一些学生在双选会上和人事聊天的照片,告知有人已经被某某企业通知面试的消息。

是的,我感到只有我一个人留在原地,因为我不愿意选择这条路。

把项目经历写在简历上的时候,我感到一种十分令人不适的被挑选的感觉。我要写什么呢?GitHub 上有五百多星星的项目是我在高中的时候写的,面试官会在意这个不完整的 PHP 项目吗?用 Lisp 语言写 Webmention 接收器,还实现了 microformats 解析,我的确花了不少心思,但有谁会在意这些小众的技术呢?更别提那些零零散散的小 Web 应用了。值得被好好审视一番的,似乎就只有我作为课程设计组长用 Java 写的那个我自己都讨厌的软件系统了。


我和同室1共用一个书架,那个书架是我搬家前买的,因为当时还没放满,所以就腾了些空间给他。最近买了些文学作品和技术相关的书,发现书架已经放不下了,而我的书桌上也堆放了好几本书,于是我向同室提出再购置一个书架。

“但是我下个月就不在这了。”

“你是要搬走吗?”

“也不算吧…… 总之就是我下个月就不在重庆了。”

于是同室把他的书从书架上移走了一部分,给我的新书腾出了空间。那一层空了两三天,我才开始整理书本,我尝试给哲学类书籍、小说和工具书分类,还在想要不要按国家给外国文学分类,可我很快发现我没办法在那个书架前面正常思考,匆忙地把《帕尔马修道院》《翻译研究》和《深入浅出密码学》放在一起,然后逃回了房间。

说实话,同室要走了我应该高兴,我一直不喜欢他。可我现在感受到的是什么呢?是嫉妒吧。虽然很难承认,但我的确是会嫉妒的。


在我的观察里,洁癖有两种,一种是「爱干净」,另一种是「怕脏」。

表面看起来是相似的,但完全不同,至少我作为「爱干净」的类型,和「怕脏」的人生活在一起,会感到非常烦恼。

举个例子,家里的吸尘器尘仓满了是我清倒的,抽油烟机(没有油烟管道,是会把油汇集在一个小盒子里的)是我清洁的,灶台都是我擦的(我指的是把铁架抬起来擦拭);浴室的地漏每次都是我通的,同室好像是习惯了洗澡的时候有积水似的,从没想过想办法疏通管道。

同室的「怕脏」怎么体现呢?他冲马桶之前会把马桶盖盖上再冲,有时候一次没有办法冲下去,但盖子盖上他也不去看(或者他就是忘记了),我偶尔能在自己家里体验开盲盒开到人类排泄物的恶心感。浴室的地漏很容易卡头发,他是不会用手捡的,或者很少去捡,要么一直卡在那,要么被他用什么工具拖到一旁不管(也不知道是不是想等着我来收拾)。

其实影响最大的是他擦屁股的习惯,因为怕脏了他的手,他上完一次厕所会消耗几十张纸巾(大概是要把纸张叠得厚厚得,才敢碰自己身体最脏的部位),而厕所的纸巾向来是谁用完了谁用自己的抽纸换上。有一次我买了一袋很大的竖着挂的抽纸放在厕所,换上的一天之后我就出门了,三天之后才回来,到家的时候那一大袋抽纸只剩最后几张了。我问他怎么能用这么多,他反问我「这不是很正常吗?」

—— Eucalyptus

不久前我向同室提出分开使用厕所的卫生纸,这几天我能明显观察到,我一周只会更换一两次厕所的卫生纸,但同室前一天换上的卫生纸,第二天就过半了。

说是怕脏,但他同时对一切似乎都很淡漠,他不在乎地面是否清洁,不在乎台面是否有油污,不在乎镜子是否透亮。他只是把家务当作义务,一件因为有其他人(我)存在而不得不做的事情。他显然没有很负责任,否则我也不会因为难以忍受他的疏忽,而提出「家务都让我来做」。

其他都还能忍受,但同室有堆积垃圾的习惯。当我的精力被学校的破事消磨殆尽回到家时,低头就能看见同室肆无忌惮地留在玄关的垃圾小山(说是小山其实毫不夸张,大概有我膝盖那么高吧),其中的一些垃圾,似乎四五天前就在那里。

有一天我终于没办法忍耐,问他:

“门口的垃圾你什么时候丢?”

他一如既往地露出尴尬又令人讨厌的笑,回答:

“今天出门就丢。”

等我再次回家的时候,之前的垃圾的确不见了,但又有新的外卖包装被放在门口,作为新垃圾山的地基。毫不令人意外,第二天又有新的垃圾出现在那里。


我快二十一岁了,但我依然抱着玩偶睡觉。最近它安抚我的时间开始蔓延了,从令人抓狂的课堂逃离回家,从碍眼烦心的垃圾堆走过的时候,我开始奔进卧室,把它从卧室带出来,放到书桌前,然后小声跟它说话。

“我不喜欢那个人。”

“我出去接杯水,很快回来。”

“噢,我还要去拿个东西,然后洗手…… 没事,也很快的。”

“你就待在这里等我。”

之后我的理智大概是宕机了,列出的待办清单都没有被完成,我开始不间断地看 Matt Rife 的单口喜剧专场、芦苇的《植物大战僵尸》游戏视频和宝可梦对战。陷在信息流里出不来,对我来说是能量低谷的警报,可是,就像我在岔路口不知道如何抉择一样,在警报响起时,我根本不知道怎么做。

我感到自己坐在鸣笛声不停还闪着红光的封闭空间里,听见旁边的走廊上有怪物正在靠近,而我却陷入僵直反应中无法逃脱。

显然,不健康的心理状态影响了我的睡眠和认知。我读的书变少了,我开始在写代码和写文章的时候感到没法集中注意力,我开始逃避原本能让我沉浸的事情,因为对现在的我而言,沉浸意味着沦陷,仿佛我要是现在不去写简历、不去参加比赛、不去考证,而是在可笑的电脑面前写着可笑的 Lisp 代码就是在浪费生命,马上就会流落街头。

然而,我却没有办法真的去做那些事情,因为他们让我感到厌烦。如果我把一整天的时间用在谄媚和我并不在乎的竞争上,我会打心底里开始痛恨自己。我就这样被卡在中间,无法动弹。

更令人抑郁的是孤立无援之感,我感到我讨厌我身边的所有人,没有一个人让我感到安全。我知道我需要加入一个群体,找到志同道合的人,无论是我自己的理性思考还是玄学预言都揭示了这一点,可这种解决方案,就像是在说「我需要有很多钱」一样,没有改变任何事情。

或许我应该严肃地考虑养一只猫了。


如果我要回到四年前,给还处在高中的我一些建议,我一定会说:好好学习,不是为了什么考上好学校有个好文凭,是为了遇到更好的人。

可是,光是这样想就很自私吧?逼过去的自己做讨厌的事情,和让现在的自己去做 Java 开发有什么区别?

周四的上午收到一条消息,来自之前兼职过的教培机构,那里的老师问我暑假有什么安排,可不可以回去做助教老师。印象里,他们时不时就会问问我的意向,教学总监也说要想办法让我过去当老师。为什么不去呢?教培的确很赚钱,而且我的英语不差,至少他们都很认可。

此时的我似乎忘记了,我在去年写过一篇 文章 抱怨这份工作。教培很赚钱,但也很累人,而且,我并不赞赏那里的文化,尽管我在那里遇到了不少有趣的人。当然,那篇文章的内容也有失公正,因为当时我的精神状态非常差(如今回忆起来,当时好像还真的想死来着),而精神状态差并不是那份工作直接导致的。

不过说到底,我仍然不想做出选择。

我看见我的人生像小说中那棵无花果树一样,枝繁叶茂。

在每一根树枝的末梢,一个个美妙的未来,仿佛丰腴的紫色无花果,向我招手,对我眨眼示意。一枚无花果是丈夫、孩子、幸福的家,另一枚是名诗人,又一枚是才学出众的教授,一枚是埃·格,了不起的大编辑,再一枚是欧洲、非洲、南美,另一枚是康斯坦丁、苏格拉底、阿提拉以及一堆姓名古怪、从事非凡职业的情人们,再一枚是奥林匹克女队冠军,在这些无花果的上上下下还有许许多多我不大辨认得出的无花果。

我看见自己坐在这棵无花果树的枝桠上,饥肠辘辘,就因为我下不了决心究竟摘取哪一枚果子。我哪个都想要,但是选择一枚就意味着失去其余所有的果子。我坐在那儿左右为难的时候,无花果开始萎缩、变黑,然后,扑通,扑通,一枚接着一枚坠落地上,落在我的脚下。

——西尔维娅·普拉斯《钟形罩》

我与埃斯特的区别在于,我看到的无花果一点也不丰腴,它们在一开始就是萎缩、发黑、发臭的,或者,那些肥硕多汁的果实藏在我看不到也够不着的地方,而我却没有梯子。我不愿意吃掉难吃的果实充饥,也找不到梯子,我知道果实就快要落下来,可我什么也做不了。


  1. 我不喜欢「室友」这个称呼,共处一室的人不一定是朋友,所以我使用「同室」这个词代替,就像「同学」是指一起学习的人,「同事」是指共事的人一样。 ↩︎

计算无穷

2026-03-18 00:22:11

由于计算机算法的特性之一是有穷性,一切能被计算机计算的东西都必须是能够在有限步骤内完成的,所以,计算机不能够应对无穷长的数据。

真的是这样吗?

穿针引线

在正式介绍 Clojure 这门语言如何操作无穷之前,我想先介绍两个非常好用的宏,->->>,因为我会在接下来的演示代码中非常频繁地使用它们。这两个宏的名字分别叫作 thread-first 和 thread-last。它们是用来做什么的呢?

首先,在任何编程语言中(哪怕不是 Lisp),都有可能遇到连续对数据进行计算的情况,一段计算完成之后,需要把结果传递给下一段代码继续操作,一般来说需要嵌套函数。假设我们需要解析一段 Markdown 文本,而我们采取的策略是,使用一个函数解析所有的标题,使用另一个函数解析块引用,再使用一个函数解析列表,以此类推,我们可能会这样写:

func parseMarkdown(raw string) string {
 return parseLists(parseBlockquotes(parseHeadings(raw)))
}

显然,这种代码可读性很差。大多数时候,我们可能会将某一段计算的结果存放在变量里,再把变量的值传递给下一个函数,以此提升可读性。

func parseMarkdown(raw string) string {
 result1 := parseHeadings(raw)
 result2 := parseBlockquotes(result1)
 result3 := parseLists(result2)
 return result3
}

上面这段代码可读性就高很多了。你可能觉得这样很蠢,怎么会有人这样写解析器?没关系,就算不是解析器,也可能会出现类似结构的代码,比如一开始传入一个文件路径,读取这个路径下文件的内容,再把文件内容(假设是 JSON 字符串)解析为数据对象,再提取数据中的某个值。

这种做法看起来并不少见,所以能否简化呢?有没有办法让我们不需要把数据放进变量里,再取出来,再传给下一个参数,而是能够直接把所有的操作都串起来呢?

而且,我们知道,Lisp 语言一般遵从函数式编程范式,这种范式一般要求变量是不可变的,对赋值有着极为苛刻的要求,在 Lisp 语言里用这么多变量是不现实的,尽管能够用不太优雅的方式做到。

用 thread-first 和 thread-last 这两个宏就能解决上述问题。

(defn parseMarkdown [raw]
 (-> raw
 parseHeadings
 parseBlockquotes
 prseLists))

视觉上,这很像类 Unix 系统上常用的管道(Pipes),即 | 符号。在输入命令行的时候,可以用 | 把两个命令串联起来,使 | 之前的命令的结果传递给之后的命令。假设你需要读取 foo.txt 这个文件的内容,并查找其中包含 bar 的行,就可以这样写。

cat foo.txt | grep bar

使用 -> 可以让代码变得简洁。假如你想要读取一个 JSON 文件的内容,这个文件不大,你想直接加载到内存里并直接转换为 Clojure 的数据结构,就可以这样写:(-> filename slurp json/read-string)

thread-first 和 thread-last 的区别是什么呢?刚刚我们演示的函数都只接受一个参数,而 first 或 last 指的是把上一个函数运算的结果传给下一个函数的第一个参数或最后一个参数,所以并没有体现出区别。

假设我们要操作的数据是列表,而操作这类数据的函数往往在最后一个参数的位置接收列表(比如 (map f coll) 的作用是对 coll 中的每一个元素执行 f 操作,然后返回一个惰性序列,coll 就是最后一个参数)。如果使用 thread-first,也就是 -> 宏,列表就会作为第一个参数传入,也就是被当成函数 f 而非 coll

箭头宏还有另外的版本,cond->cond->>,这里的 cond 是 condition(条件),你可以根据条件判断要不要执行这条流水线里的某一环。

(cond-> filename
 true slurp ;; 由于条件为 true,所以总是会被执行
 json? json/read-string ;; 如果是 json,才会被解析
 json? :body ;; 如果是 json,才会尝试获取 body
 true println)

箭头宏的用处很多,最常见的场景之一是用来处理列表,毕竟,Lisp 就是列表处理语言(List Processing)啊!

假设我们有一个装满了狗狗的向量,我们要从所有的狗狗中找出听话的好狗狗,并给好狗狗每只狗一块零食吃。1

(defn treat-good-dogs [dogs]
 (->> dogs
 (filter :good)
 (map give-treat)
 doall))

如果对上面的代码进行宏展开,就会得到:

(defn treat-good-dogs [dogs]
 (doall (map give-treat (filter :good dogs))))

不过,为什么在最后加上 doall 呢?这是做什么的?

惰性的魔力

假设我们有无穷只狗狗,但我们的零食数量是有限的,要怎么办呢?我们当然没办法满足所有的好狗狗,在零食发放完之后就不能继续操作了。这要怎么写呢?你可能会想,需要一个整数型的变量来表示零食的数量,每发出去一块零食就减 1,当变量值为 0,就停下,终止操作。

用不着这么麻烦,我们可以直接对无穷进行操作,直接在无穷的狗狗里筛选好狗狗,再取出前 n 个好狗狗(n 为零食的数量),给他们零食就好了。

假设 dogs 是一个无穷的序列,也就是说用无穷无尽的狗狗们在等待着发放零食(天哪,光是写下来就觉得可爱爆了)。那么,下面这段代码会陷入无限循环吗?

(->> dogs
 (filter :good)
 (map give-treat))

不会,因为 filtermap 返回的是惰性序列(Lazy sequence)。

简单来说,只要我们不主动读取惰性序列中的数据,相关的操作就永远不会被计算。比如 (filter :good dogs) 这一步,虽然写法很优雅,可以直接读作「filter good dogs」(筛选好狗狗),但 Clojure 实际上并没有真的去筛选好狗狗,而是承诺:下次,下次一定,之后我就把这件事情办了。

下一步 (map give-treat ...) 也是一样,Clojure 还没有「给每一只好狗狗一块零食」,而是做出了「我之后就会给每只好狗狗一块零食」的承诺。

这就是为什么之前我们要用到 doall,这个函数的效果是执行惰性序列中的所有效果。如果没有这个函数,我们就只是做出了承诺,而没有真的去执行。

当然,现在的情况是,我们得到的惰性序列是无穷长的(无穷的狗狗中好狗狗的数量自然也是无穷),当然不能 doall,因为这样做资源会很快被消耗光,我们没有那么多零食。在计算机系统的视角下,我们也没有那么多的内存和处理器资源可以分配。

假设我们有 n 个零食,我们可以这样写:

(->> dogs
 (filter :good)
 (map give-treat)
 (take n)
 doall)

上面这段代码的意思是:

  1. 我们有无穷的狗狗
  2. 承诺之后会筛选出其中的好狗狗
  3. 承诺之后会给筛选出的好狗狗零食吃
  4. 取出前 n 个承诺
  5. 执行这些承诺

实践惰性

你可能会以为,dogs 为无穷长的序列只是个假设,如果真的有无穷长的序列,Clojure 也是处理不了的。

实际上 Clojure 真的可以生成无穷长的惰性序列。 range 函数一般接收一到两个参数,生成一段由连续整数组成的惰性序列,例如执行 (range 3) 会得到 (0 1 2),执行 (range 1 3) 会得到 (1 2)。如果不传入任何参数,也就是不限定边界,就会得到一个无穷长的惰性序列。

读《Living Clojure》的时候,读到作者写了这样一句话:

不要运算它,否则会使你的 REPL2 崩溃

于是我就这么做了。

在 REPL 里运行 (range) 的后果

Clojure 显然是陷入了每个程序员都遇到过的无限循环,这是怎么发生的?首先,我输入了 (range),构造了一个无穷的惰性序列,然后 REPL 读取了这个惰性序列,并想立刻求出这个惰性序列的值,于是惰性序列中的元素就被运算了,运算之后被打印了出来,就造就了图中的局面。

我们可以优雅地从无穷里取出有限个元素,来避免无限循环。

(take 5 (range))
;; => (0 1 2 3 4)

显然,我们并不会遇到规模为无穷的问题,否则就算有能力处理无穷长的惰性序列,在实际运算这个惰性序列时,也会因为陷入无限循环而无法继续执行程序。

不过,这不妨碍我们利用惰性序列优雅地处理大规模的问题。

假设有一个几万行的 JSONL 文件,也就是说有好几万条 JSON 数据被存储在这个文件里,每一行都是有效的 JSON 对象。我们知道 I/O 操作开销很大,把这么大的数据整个读取到内存里也极其消耗资源,更别提还要把 JSON 字符串解析为可以直接操作的数据结构,为了保证程序不会太低效,必须采取一些手段。

惰性序列就是很好的解决方案。

(with-open [rdr (io/reader "very-large-file.jsonl")]
 (let [lines (line-seq rdr)] ;; line-seq 返回包含所有行的惰性序列
 ;; 它接收的数据类型是 java.io.BufferedReader
 (count lines))) ;; 返回文件行数,但没有真的读取文件内容

你也可以用 filtermapreduce 等函数直接操作惰性序列,它们接收惰性序列,返回的也是惰性序列,只要不执行 doalldorun,或者用其他函数读取其中的值,它们就永远不会被计算。

如果你打算遍历这个文件的所有行,只有被遍历到的行才会被读取,余下的行仍然是惰性的。换句话说,如果你在惰性序列里寻找一个符合特定条件的元素,比如包含特定字符串的行,而符合条件的元素恰好就是第一个元素,那么整个遍历操作实际上只会读取文件的第一行。

如此优雅的操作,只需要三行代码!

这就是把一切都变成列表的好处。除了用于筛选的 filter、用于创建映射的 map 和用于累计计算所有元素的 reduce,还有许多可以用来操作列表的工具,比如 remove 可以移除列表中符合条件的元素,flatten 可以打平任意嵌套的列表,into 可以把一个列表的元素全部放进另一个列表,partition 可以把一个列表分割成相等长度的多个列表。

你可能还发现上面的许多函数都还涉及判断列表元素是否符合某个条件,在其他语言里你可能见过类似的做法,比如给排序函数传入一个函数作为参数,用来比较谁应该放在前、谁应该放在后,这种函数一般叫谓词函数(predicate)。

Clojure 里有很多好用的谓词函数。比如 nil? 可以判断一个值是 nil 还是不是,如果要移除列表里所有的 nil,就可以这样写 (remove nil? coll)。我也很喜欢 complement 谓词,它的意思是「补集」,比如 nil? 的补集 (complement nil?) 就是判断一个值不是 nil 的谓词,如果不是 nil 就返回 true。刚才写的 (remove nil? coll) 也可以写作 (filter (complement nil?) coll)

你也可以用匿名函数作为谓词,写法是 (fn [] ...),简便写法是 #(...),在简便写法中可以使用 % 来表示参数,如果有多个参数,就写作 %1 %2…… 上面写的用来在列表中移除 nil 值的函数,还可以这样写:(filter #(not (nil? %)) coll)

利用好这些谓词函数和操作列表的函数来操作惰性序列,在最后才读取惰性序列中的值,就能用最简短的方式写出高性能的代码,虽然跑在 JVM 上性能也高不到哪里去就是了。

最后

本文是学习 Clojure 约莫一个月时间,在一个月内连连感到大脑被击穿,在人脑代码解析器过载和好奇心被狠狠满足之间反复横跳后产生的东西。

到目前为止,对 Clojure 唯一的不满就是:我写出来的东西只能编译成 .jar 文件,用 Java 运行环境来执行,因为这是一门运行在 Java 虚拟机上的语言。虽然能偷 Java 的库来用,但也不可避免地继承了 Java 的臃肿,比如 weepinbell 编译之后竟然有 170+ MB 大小,实在是太让人不爽了。

唉,美味中拌了点垃圾也将就吃吧。

死亡很近。吃垃圾。自由生活。


  1. 假设「好狗狗」的数据结构长这样 {:good true},这是 Clojure 中映射的写法,可以用关键词作为操作取出其映射的值,比如 (:good {:good true}) 就会返回 true。 ↩︎

  2. REPL:Read Eval Print Loop(读取-求值-打印-循环),是与 Clojure 交互的最简单方式,基本上就是一个接收代码然后运算,再把结果打印出来的程序。 ↩︎

Es muss sein?

2026-03-17 09:24:31

和另一个塔罗牌爱好者朋友一起解牌的时候,能够相互交流、补足理解,但偶尔也会有明显的分歧,其中的一个分歧就是我和她对于「倒吊人」这张牌的理解。她认为倒吊人代表的是一种很痛苦、很不舒服、受到限制的心理状态,可我告诉她,每次我见到倒吊人,都觉得有人要破茧成蝶了。我在「塔」的 牌意解析 中也提到,我更喜欢「倒吊人」这类令人闻风丧胆的牌。

上次更新《塔罗牌漫谈》是三个月前了,已经有些忘记这个系列应该怎么写,印象中我会把自己有的塔罗牌如数家珍地拿出来,然后把每副牌中特定的要写的那张牌挑出来,顺便读读某些设计得比较有意思的塔罗牌的说明书,然后去搜寻各种资料。不过,既然要谈倒吊人,那还是要打破规矩,选择能让我更容易沉浸其中的写作方式才比较好。所以,这篇文章我就来谈一谈,「把人脚朝上吊起来」这个意向,究竟能延伸出多少解读。

被吊起来的人是谁?

倒吊人是二十二张大阿尔卡那牌的第十二张,它的前一张是对应天秤座的正义。很显然的一种解读是:在第十二张牌中被吊起来的人和正义牌中手持天平和宝剑的是同一人,象征着旧有秩序被推翻。

说到「旧有秩序被推翻」,就不得不谈到下一张牌。十三这个不吉利的数字对应的是死神牌,紧随倒吊人之后,而还有什么秩序的覆灭比得上死亡?

我一直倾向于把死神牌提前解读为新生,或者,至少是绚丽的死亡。记得有一次我在路上看到了满地的落叶,遮住了地面,脚踩在上面发出了脆脆的声音,我觉得那个场面很像塔罗牌里的死神。可惜我把这个观察告诉朋友时,对方并不能理解。倘若再仔细想想,落叶会成为土壤的一部分,为新的生命提供养料,这便是死亡。在死亡之前,必须要确定的是,这个生命的确应该去死,而这个具有决定性的时刻,就是倒吊人。倒吊人的存在为真正重大的改变做着必不可少的准备。

于旧的秩序之后,新的生命之前,倒吊人象征着奇妙但又有些尴尬的局面:他究竟应该何去何从?答案是,他的脚被绑起来了,哪也去不了。

倒吊人在许多人眼里,寓意很坏,因为被束缚住,什么也做不了,会陷入内耗和纠结当中。可我认为,正是因为哪也去不了、什么也干不了,才不会陷入纠结——空想有什么用呢?反正也没有办法行动。倒吊人的束缚与另一张牌, 宝剑八 所象征的束缚有明显的不同:首先,倒吊人的眼睛没有被蒙住,他还看得见,还能清醒地思考;其次,宝剑八中的人被束缚的是手臂,他仍然能行走,而倒吊人却相反。

在我看来,所谓的内耗、焦虑和纠结是属于宝剑八的(因为他能走,却不知道往哪走),对于倒吊人而言,一切都相当明了:虽然照理来说,不该在一棵树上吊死,可我已经被吊着了,还能怎么办呢?

这让我想到《不能承受的生命之轻》里,托马斯从贝多芬的音乐里借来的母题「es muss sein!」,意思是「非如此不可!」。

摘自贝多芬 F 大调第 16 号弦乐四重奏

特蕾莎独自一人回到家乡后,托马斯陷入了同情心的无尽折磨当中,最终他决定接受,跟院长辞了职,要回布拉格跟特蕾莎在一起,因为「非这样不可」。当他做出这个细细掂量的决定,并认为自己除此之外别无选择之后,他马上脱离了痛苦(不过他很快又开始质疑「真的非这样不可吗?」,但那是后话了)。倒吊人就是这样的状态,深陷麻烦的境地当中,但一想到非这样不可,命运就变得容易接受了。

不过,有一个狡猾的点在于,倒吊人的「非这样不可」,真的是他细细掂量的选择吗?换句话说,真的非这样不可吗?

他是自己把自己吊起来的吗?

是时候来讨论房间里的大象了:这个以一种异常不舒服的姿势被吊起来的人,为什么脑袋发着光,看起来这样精神焕发?

于是,另一种解读就出现了:倒吊人不是被人吊起来的正义,他是自己把自己吊起来的;他不是接受了命运,而是主动选择了命运,因为「非这样不可」;他不是受罚的西西弗斯,他是快乐地推石头的西西弗斯。

但…… 为什么?为什么要这样折磨自己?

同样的问题,减肥的人、健身爱好者、废寝忘食学习的人,也会被问到。如果把倒吊人和他们联系起来,答案就不言自明了。在旁人看来是自我折磨,在当事人自己的体验里,却是无与伦比的快乐。为什么?答案有两个。

第一个答案是真理,所有让自己陷入短期(甚至长期)的痛苦并乐此不疲的人,都是在追寻某种既抽象又具体的真理。抽象是指信仰,因为相信以特定的方式活动身体能让自己的身体机能得到锻炼,提升生命的质量,所以不在乎痛苦(兴许宗教也是相同的逻辑);具体是指可被观察到的变化,体态的矫正、学识的增长、处理问题的能力等等。

第二个答案更现代,也更容易被不喜欢神神叨叨的读者理解:心流状态。倒吊人很明显的一层含义是「沉浸感」,由于被吊起来了,什么事也干不了,就像被关在书房里,而电子设备和朋友们都在房间外面,为了消磨时间,便只能冥想、读书和工作了。高质量且不受打扰的专注时间,能给人带来如牌面中倒吊人那样的光辉。

所以,是的,倒吊人可能是自己把自己吊起来的。为什么?为了给自己创造「非这样不可」的意义、无纷扰的环境,以及新的视角

不过别忘了,塔罗牌还有逆位含义。逆位的倒吊人,在我看来,做的是徒劳无功的事情,就算以目光长远的角度来看,也缺乏意义。如果把倒吊人这张牌倒置,会看到倒吊人尽管处于一种不舒服的被束缚状态,他的身体仍然是正立的,和倒吊之前没有区别。用通俗的话来讲,就是「没苦硬吃」,或者说吃了苦却没产生实际效果。

减肥者群体中有另一类人,他们不关注合理膳食,吃饭只吃水煮白菜,营养极度不均衡,拿自己的健康甚至性命做交换,而这样的人却能得到不少人的赞叹,实在可笑——这些人就是逆位的倒吊人,倒吊之前,他们膳食不合理(所以肥胖);倒吊之后,他们仍然坚持不健康的生活方式,只不过换了形式。

逆位的倒吊人抓错了重点,重点从来不是受苦,而是为了让自己沉浸、接受命运、活在当下、追求某种真理和心流状态。

如果是别人把他吊起来的呢?

这个「别人」是谁?不重要。无论是谁,我们都可以浪漫地归结于命运。有的时候,人的确会遇到完全无能为力的麻烦,就像被人吊了起来,只能看着事情一点点变得更糟,自己却什么也做不了。这个时候,要是大喊着「我命由我不由天」,在绳子上挣扎,只会让身体摇来摇去,感到眩晕和恶心。

倒吊人提醒当事人:有的事情就是无法改变的。

如果不能改变,那还能怎么办呢?如果执着于行动不会让情况变好,反而会白白耗费心力,甚至让自己受伤,那更好的选择是接受现状,但并不是摆烂,重要的是:以完全不同的视角观察当下的局面。

如何让自己以完全不同的视角观察世界?答案:把自己倒过来。是的,这难道不是很有趣的隐喻吗?之所以需要用新的视角观察局面,是因为自己无法改变现状;而无法改变现状的事实,也就是新视角本身——什么都做不了这个事实会让人产生新的思考,前提是甘愿接受这个事实。

我想倒吊人这张牌也可以引伸出这样的含义:解决问题的思路就是问题本身。应该停止用锤子的视角思考,开始以钉子的方式想问题。

为什么是倒吊,而不是正立?

倒吊人在占星意义上对应的是海王星,而海王星在占星中是人类精神世界的投射,与想象力、幻想、梦想和直觉相关。我之前有一个 Telegram 频道,名字叫作「大脑充血」(未来的月刊大概也会叫这个名字),频道的头像就是倒吊人。在影视作品里会见到一些角色把自己倒吊起来,使血液流向大脑,让大脑更高效地运转——这大抵是不可信的,但的确是很好的写作素材。

当全身的血液都流向大脑,人真的会变得更聪明吗?或者,用更科学的方式假设,如果人脑的所有功能都放大和加速,人是不是会变得更有效率、更理性、更容易相处?如果所有人都这样,经济会不会快速发展?会不会促成世界和平?

现代人似乎喜欢把大脑想象成理性的机器,是用来思考并且只是用来思考的,实则不然,本能、情感、想象力和直觉,也都发生在大脑当中。当大脑的功能被增强,这些功能也会变得更强。人会无法忘记创伤经历,变得更情绪化,更容易冲动行事,与此同时,大脑的另一部分又在以更高的速度阻止个体意气用事,内耗、纠结、矛盾愈演愈烈。我想起《瑞克与莫蒂》第八集最后一集的情节,Beth 的大脑被相互矛盾的记忆折磨,让她无法分清正与误、真与假,那个时候她已经无法正常思考,想要一切停下来,甚至拿枪对准了自己的太阳穴。

倒吊人象征的兴许也是这样的有些棘手的状态,由于身体被束缚,所以大脑高度活跃。正位的倒吊人能够驾驭这种状态。首当其冲的原因是,他虽然沉浸其中,但终究会从树上下来,不会让自己过度思考、过度想象,继而陷入虚无。其次,他知道自己为什么要停下来思考,他的大脑不是完全的一团糨糊,独处时的想象、思索和直觉能够帮助他更好地生活,甚至重获新生,在死神牌到来时建立起新的秩序。

在我看来,倒吊人象征的是必要的反思,对已有秩序(正义牌)的反思;倒吊人也象征发散的想象,想象为新秩序的建立(死神牌)提供养料。这正好呼应了海王星的主题,想象力、梦想和直觉。

逆位的倒吊人则陷入了海王星的另一个面向。想象力的另一个面向是不切实际,梦想的另一个面向是逃避现实,直觉的另一个面向是对事实根据的不尊重。整体而言,逆位的倒吊人代表的是过度反思和不问世事。

倒吊的群像

这一小节,我将展示不同塔罗牌中倒吊人的设计。它们大多都强调了这张牌的某一层含义,或者试图建立新的理解。

在《缥缈愿景》塔罗中,倒吊人是一个年轻的男孩,他摆出的姿势与韦特塔罗牌中的倒吊人区别不大,但他的左手拉着捆住右脚的绳子——他完全是靠着自重把自己吊起来的。

比起束缚,这张牌更强调灵活和掌控。倒吊人完全清楚自己在做什么,并且享受着常人觉得痛苦且避而远之的状态。

这张牌还透露出「玩世不恭」的态度,与「推翻旧秩序」「打破规则」的含义形成照应。

在《吸引力法则》塔罗中,倒吊人没有被吊起来,而是在吊索上艰难地行走。尽管手脚能够活动,但他仍然是受到束缚的,这种束缚实际上更贴合现实中人的境遇——我们没有被捆住手脚,但仍然感到被困。

尽管步履艰难,牌中的男人显然有明确的目的地,这对应倒吊人有关梦想和直觉的含义。起点的窗户是旧秩序的象征,终点则是新生,而这张牌是两者之间的状态。

《暴风雨》塔罗中的倒吊人牌面上没有人,取而代之的是坠入海底的三叉戟。无论是用来捕鱼还是象征海神,三叉戟都具有征服海洋的力量,而牌上的三叉戟却卡在海床的泥土当中,无计可施。

有趣的是,阳光透过海面照在了三叉戟上,这是否象征着,即便不被使用、不被驾驭,三叉戟本身依然具有某种神圣的气质和潜能?正如即便没有即时的回报也能坚持自我的倒吊人一样。

《天使感召》塔罗中的倒吊人是卡西尔,孤独与眼泪之天使。他收起了翅膀,倒挂在空中,象征着从行动中退出,什么也不做。他做的仅仅是观察善与恶,不评判也不干预。

牌中的湖代表潜意识,镜面般的湖面象征着反思。图中还有远景,卡西尔就这样看着,观察着景色,却不是景色的一部分,不参与到世界的运动当中。这幅牌的倒吊人强调的是「无为」。

《艾伦伯格》塔罗强调的完全是倒吊人的负面含义,而且变本加厉:被捆起来的不是一只脚,而是两只。被吊起来的人显然完全无能为力,只能认命。

这张牌要人们意识到,面对当下的场合,自己的确是无能为力的,应当接受这一点,并换一个视角重新看问题。第一步是接受自己不能改变的,下一步(死神牌)就是改变自己能改变的,再下一张牌是节制,兴许就是分辨这两者的智慧。

最后

至此,我们总结出倒吊人这张牌的几个关键词:旧秩序的颠覆、从行动中退出、必要的反思、心流状态、坚持自我、用新视角看待问题、接受不能改变的、精神上的掌控感。

对于逆位的倒吊人,可以这样总结:无意义的受苦、不成功的改变、痛苦的挣扎、不切实际的幻想、与现实脱节、执拗、过度反思、内耗与纠结、身不由己。

照这个更新速度,我什么时候能写完 78 张牌的牌意解析呢?

稻草人周刊 Vol.72

2026-03-16 00:07:34

Nothing's About To Happen To Me music cover

Nothing's About To Happen To Me

Mitski

一张另类摇滚专辑,是我喜欢的风格。创作者 Mitski 的歌我之前只通过另一位翻唱歌手 Chloe Moriondo 听过,对她的《Nobody》印象很深刻。这张专辑我这周听了好几遍,最喜欢的歌是《Lightning》和《Instead of Here》,第二首《Where’s My Phone》也很有个性。很奇妙,Mitski 的歌总给我这种感觉:这是来自金星的音乐。

To feel like myself again,

I won’t be here. I’m where nobody can reach.

I’m not here. I’m where nobody can reach.

Instead of here, I’m where nobody can reach.

连接

我有 ADHD 吗?

📜

其实我从未怀疑过自己有 ADHD,也就是俗称的多动症,但这个病在网络上的讨论在最近有所增加,确诊的人也不少——可能是因为人们对心理健康的关注更多了,是好事。也正因为近来讨论的人多了,听他们描述自己的症状,我自己也有些不确定了。之前听一期播客讲一个简单的 ADHD 判断方法:如果做家务的时候能做完一件再做下一件,不会把各个角落的各种事情都展开之后迟迟不收尾,那就不是 ADHD。在那之后,我每次做家务都会刻意地不让自己同时开始多件事情——尽管我后来发现我只是在进行合理的多线程并发,避免处理器空闲,比如给床单喷了除螨剂之后要等它干掉才能叠被子,这个时候我就会走出卧室去擦厨房。

我也偶尔会有在面对焦虑和压力时失去所有动力,什么也干不了的情况。不过一年前我也自我剖析过,一方面是自己的确有些抑郁的倾向在,另一方面,没办法好好应对压力也算人之常情,而且这种情况也不算常见。可是,我也经常在写文章写到一半的时候去查看社交媒体信息,或者上网冲浪的时候发现自己不知不觉地打开了十几个标签页,在同时探索各种不同的主题,这似乎是注意力没办法集中的体现吧?

无论如何,还是自检一下比较放心,我找到了一张 成人 ADHD 自检量表 ,只有 18 题,很快做完了。结果是:我不太可能有 ADHD。

做题的过程中我就猜到了,我有好几题都选择了「从来没有」。比如「在家里或是在工作时,你经常乱放东西或是找不到东西」和「你认为记住约会或是必须要做的事情很困难」,我虽然不算非常有条理,但这这两件事情还从来没有发生过,因为我非常害怕丢东西和忘记和别人的约定,这种警觉性让我从来没有遇到过这方面的问题。

做完之后还有一个感受:如果有人真的表现出这些症状,我好像是会生气的。比如这几条:

  • 有人面对你说话时,你很难专心地听完他说的内容

  • 当与他人交谈时,你会在别人还没把话讲完前就插嘴或接话替对方把话讲完

  • 你会在别人忙碌时打断别人

以前我认为这些人是没有教养,现在看来,他们有可能并不是想要这样做,而是因为觉得不这样做很困难。我想我可以多一些包容和理解吧。

Org Mode 是最好的标记语言吗?

📜

I did not find any tool support for Markdown, AsciiDoc, Wikitext or reStructuredText anywhere that could compete with the cozy Org mode syntax support within Emacs.

Well, look harder.

作者在这篇文章中对比了 Org Mode 和其他标记语言(Markdown、AsciiDoc、Wikitext 和 reStructuredText)的区别,以及为什么他认为 Org Mode 比这些标记语言更具优势、更合理。

文章前半部分的观点我很认同。首先,Org Mode 是规范化的,而 Markdown 却有很多不同的 Flavor,在不同的平台上输入 Markdown 得到的结果不总是一样的,比如 Markdown 很常见的表格就是拓展语法而非最初的标准。我自己也依赖一些并不常见的 Markdown 拓展语法,比如 Obsidan 里常用的 ==高亮标记== 和 GitHub 上常用的 Alert ,这两个语法都被 Hugo 支持,所以我在博客里也经常用,但是在其他地方就不一定了。这的确是 Markdown 的缺陷之一。

其次,Org Mode 的行内标记是符合直觉的,*加粗* /斜体/ _下划线_ ~代码~=等宽字= 都很容易记住,是唯一的语法,而且都是一前一后的单个字符,不像 Markdown 有 _斜体_*斜体* 两种写法,而 **加粗**~~删除线~~ 居然需要一前一后的双字符进行标记,和其他行内标记不统一。说实话,我之前尝试 Org Mode 时,就挺搀它的行内标记语法的。

接下来是我不太赞同的部分。

作者说他认为 Markdown 的链接语法令人疑惑,它会忘记 [文字](链接) 哪个在前、哪个在后,并且它不理解为什么要用到两种括号。Org Mode 里的链接写作 [[链接][文字]]。我可以用相同的逻辑论述为什么 Org Mode 的链接语法令人疑惑。在 Markdown 里,[] 是用来包裹文字的,() 是用来包裹链接的,至少我永远不会搞错文字和链接各自的括号是什么;我相信大部分 Markdown 用户记住哪边是链接、哪边是文字的方式都是这样的,[]() 这样的形式看得多了,就会知道谁在前在后;如此一来,我就不会搞错链接和文字的位置了。反观 Org Mode,链接和文字都用 [] 包裹,谁在前在后就更难记住。

我认为「我容易忘记语法的顺序」并不是很有说服力的论据,因为我认为 Org Mode 用户能记住 [[链接][文字]],和 Markdown 用户能记住 [文字](链接) 的方式一样:见多了就记住了,不是因为哪个语法更符合直觉。两边都有各自的优势和缺陷,仅仅是个人偏好和习惯问题。

我尝试过 Org Mode,其中一部分我很喜欢,另一部分则很不习惯。我最不能接受的是 Org Mode 对中日韩文字的支持,它不能解析 这样的*加粗*文字,因为 Org Mode 的行内标记前后必须有空格,这显然是欧洲语言的使用逻辑,中文里就是很少用空格。其次,标准化是好事,但拓展性差也是问题,我没办法用 ==高亮标记== 这种拓展语法。再次,你们是怎么忍受 #+BEGIN_SRC #+END_SRC 这种写法的?这跟 Markdown 的 ``` 比难道不是长得多吗?和 ~行内代码~ 也没有连贯性(反而 Markdown 可以写 ~~~ 标记代码块)。

不过,读完这篇文章过后,我的确产生了很多思考,其中之一是:标记语言本身真的有好坏之分吗?我认为,就和代码风格一样,有的风格固然是不好的,比如可读性很差的风格,但有的风格很难分个高下,比如 {} 应不应该换行写、缩进应该用空格还是制表符、文件末尾应该不应有空行,标记语言也有很难说是好是坏的语法。用于写配置的标记语言 YAML 和 TOML 也是一样,有人认为 YAML 有诸多缺陷 ,而 TOML 的缺陷 也有人能列出来不少。

你猜怎么着?没有一门标记语言是完美的,包括 Org Mode 和 Markdown。

如何选择标记语言,应该由使用者自己尝试、权衡和决定,不存在「最合理且对所有人都同样合理」的语言。或许,可以开发一种自定义程度极高的标记语言,人们可以自己写配置调整语法,自己创造最适合自己的语言,就像配置编辑器一样。

「结果啊……」

📜

作者表示他很喜欢「It turns out」这个表达,他常在 Paul Graham 的文章里读到,这个表达应该可以对应中文里的「结果……」「最后发现……」。

如果把一句话直接说出来,尤其是那些稍微有些让人难以接受的观点,可能不会起到很好的效果,但如果作者自己在一开始就表达些许的质疑和不确定,然后再以「结果……」「最后发现……」来转折,语气中透露的些许惊讶就会让读者更容易接受这个没有论据支撑的观点。作者说这是一种很巧妙的偷懒技巧,或者说「黑客技巧」(Hack)。

第 43 期周刊 中我分享了 Paul Graham 的另一篇文章,题为《Good Writing》。PG 表示他发现「读着顺口」和「写得正确」这两者之间存在联系,简单来说,如果把句子写得更通顺,这个句子表达的意思就更有可能是正确的,内部逻辑可能更连贯——不过前提是修改,PG 表示他经常修改一些句子,使之更顺口,这样就无意识地修正了一些错误表达。

我想「It turns out」可能也是 PG 的「顺口表达」之一吧。

死互联网已经不再是理论了

📜

作者最近邀请了一个职位的申请者来参加第一轮面试,结果收到了这样的回复:

hey sorry - my agent got a mind of its own and started applying for jobs for me. i’m not currently looking for a job 😅
嘿不好意思 - 我的 Agent 有了自己的想法,开始帮我申请工作了,我现在没有在找工作。

please ignore and sorry for that
请忽略,抱歉。

作者意识到「互联网已死」已经成现实了:Hacker News 开始限制新用户发表 ShowHN,因为最近出现了太多 Vibe Coded 且低质量的投稿; Reddit 上有很多机器人在评论区发帖宣传 SaaS 产品;LinkdIn 上的更新也是 AI 废料占大多数(作者贴的一条 LinkdIn 帖子的截图,帖子上全是些让人抓不住重点的车轱辘话,居然有两百多条评论和五千多个点赞);GitHub 也逃不过,AI 生成的 Pull Request 下的代码审查和回复竟然也是 AI 生成的。

对此我的态度是,我就待在小互联网上好了,与大平台保持距离,和自己选择的真实的人社交。说实话,我一旦在 RSS 更新中看到了 AI 生成的内容,我就会直接把这个订阅源删掉。

不过,倒霉的是,我办法屏蔽现实中人类的声音。

因为 AI 错误,老奶奶被关进监狱半年

📜

美国北达科他州法戈的警察在调查一项银行诈骗案时使用了 AI 技术,用人脸识别锁定了位于田纳西州的 Angela Lipps 为主要嫌疑犯。在七月 14 日,警察在 Lipps 的家门口逮捕了他,那个时候她还在照看四个年轻的孩子。50 岁的 Lipps 从未去过北达科他州,甚至从未坐过飞机。她被关在田纳西州的监狱近四个月,法院为他指派的律师告诉她,如果要上诉,就要去北达科他州,于是她又被转移到了那边的监狱,直到第五个月的时候,逮捕她的警察才第一次和她谈话。

在法庭上,证据表明 Lipps 在被怀疑实施诈骗的时间在田纳西州还有银行记录,最终经过了六个月后被释放。时间已经来到了冬天,出狱的时候她穿着夏装,很冷,没有地方去,也不知道怎么回家。由于在监狱里没办法支付账单,她失去了她的房子、车,甚至她的狗。

“Why did nobody from Fargo Police ever speak with Angela Lipps for the five months she was in jail?” Zibolski was asked.
“为什么法戈的警察在 Angela Lipps 被关押的前五个月都没有人想过跟她交谈?”,Zibolski 被问到。

“Thank you, Matt (Henson), for that question but we are not here to talk about that today,” Zibolski replied.
“谢谢你,马特(汉森),对于这个问题我们今天暂时不回答。”Zibolski 回复。

Lipps 现在回到了田纳西州,但从始至终,法戈警察没有跟她道过歉。

所以教训大概是,不要把 AI 技术交给蠢蛋

星群

Hugo 社交媒体卡片

Twitter 和 Mastodon 自带的嵌入卡片可能和网页的风格不统一,而且需要从外部加载资源,在这个过程中可能会暴露访客的 IP 地址和 Cookies 等信息给第三方。作者做了一个在 Hugo 构建过程从 API 拉取数据,静态展示社交媒体内容的卡片,和自己网站的风格更匹配,而且不会追踪任何用户数据。

我其实也想添加,但我一直不太能接受在静态网站的构建过程中,从网络加载资源,我认为这会拖慢世界上最快的静态网站生成器的构建速度。不过 Hugo 有构建缓存,兴许可以试试吧。

访问: thumbsupdotme/social-cards

DNSControl

使用 Go 编写的通用 DNS 管理工具,使用简单的 JavaScript 代码管理 DNS 记录,而不是忍受 DNS 提供商难用且加载速度很慢的 Web 面板。DNSControl 还鼓励用户把 DNS 配置文件放在 Git 仓库里,这样 DNS 记录也有了 Git 历史,可以查看变更和随时回退。这个项目相当观点鲜明,以下是它自述的功能:

  1. 使用高级语言维护 DNS 配置,可以使用宏和变量,便于更新;
  2. 避免被提供商锁在平台上,可以随时切换提供商,并且非常简单;
  3. 支持超过 35 个 DNS 提供商;
  4. 可以使用插件支持更多的提供商;
  5. 对 DNS 使用 CI/CD 原则:单元测试、系统测试、自动部署;
  6. 可以开关 Cloudflare 的代理;

对我来说,这意味着我可以在本地打开我最喜欢的编辑器(Neovim)编辑 DNS 记录,然后推送到远程 Git 仓库并利用 Forgejo/GitHub Action 将变更的记录推送给 DNS 提供商,不用打开浏览器、登录,然后找到 DNS 控制面板,再等待加载。DNSControl 还有预览功能,dnscontrol preview,避免误操作。迁移也很方便,可以用 dnscontrol get-zone 获取已有的 DNS 记录。

如果你也想使用 DNSControl,可以参考 Sukka 大佬编写的《 用代码和 Git 管理 DNS 记录 —— DNSControl 和 GitHub Actions CI/CD 实践 》。如果你不介意读英文,也愿意忍受我的凌乱笔记结构的话,我有一个简练的版本: DNSControl Setup

不过,未来有没有机会用 Lisp 写 DNS 记录呢?

访问: DNSControl

LibreSprite

想要画一个 88x31 小按钮,所以第一步是从源代码编译绘图软件!1然后 CMake 成功了,Ninja 编译失败了,不熟悉 C++ 的编译系统,照着 aseprite 的安装文档操作也难免做额外的功课,索性直接放弃。

狐工智能 :所以为什么不用 LibreSprite?

啊什么,居然有 Fork 吗?

LibreSprite 是自由软件,是 aseprite 的分支,可以免费下载使用。不过最新的 1.1 版本和预发布的 1.2 版本对 macOS 的支持都有 问题 ,macOS 用户可以暂时使用 1.0 版本。

访问: LibreSprite


  1. aseprite 是开源的商业软件,买断价格是 $19.99。如果不想花钱,也可以自己从源代码编译,可以合法地免费使用。不过,尽管开源,aseprite 并不是自由软件。 ↩︎

我为什么喜欢音乐剧?

2026-03-12 00:45:15

跟一些朋友聊起音乐剧的时候,他们似乎都不太能接受演戏演到一半突然唱起来的举动。不过,音乐剧和迪士尼电影还是有些不同(尽管我也很喜欢迪士尼电影里的歌!)。在音乐剧里,音乐和舞蹈不仅起抒情作用,还是叙事载体,是推动剧情发展的重要一环。

音乐可以用来描绘人物的内心活动,比如《摇滚红与黑》里的《Ding Dong》和《汉密尔顿》里的《Satisfied》;音乐可以展现人物之间的冲突,比如《地狱客栈》里的《Hell’s Greatest Dad》和《魔法坏女巫》里的《What Is This Feeling?》;同一首曲子可以由不同的角色演唱,用相似的旋律展现不同的情感和人物形象,比如《Dear Evan Hansen》里的《Requiem》,甚至相同的旋律在不同的场景响起时,会有不同的效果,这种手法叫作 Reprise。

当然,也少不了一些纯粹的幽默,比如同样是《Dear Evan Hansen》里的《Sincerely Me》,我相当喜欢这首。还有一种歌曲形式叫作 Patter Song ,节奏非常快,歌词的每个音节几乎都是连着的,而且用词通常是押尾韵或者头韵,听感很欢快,不需要什么唱功和技巧。这种歌通常可以快速地推进情节和交代大量信息,很有趣。

音乐里有个概念叫作 motif,通常译作「动机」「乐想」,也音译做「母题」。音乐动机是一段旋律、反复出现的几个突出的音形、一小段音乐片段,可以理解为「用来辨别音乐主题的最小单元」。1在一些音乐剧和歌剧里,人物有各自的动机,出场时场上会响起同一段反复出现的旋律,与人物个性相关。不过,我并不确定这是不是常用于音乐剧的手法,我只知道《地狱客栈》这部音乐剧动漫里使用了这种手法,里面所有的主要人物都有他们出场自带的背景音乐,甚至有代表乐器,比如主角夏莉的乐器是中提琴,她的女友维姬是钟琴。

说了这么多,你可能会以为我是个音乐迷,所以会喜欢音乐剧。我确实偶尔会听点古典乐,但真的很少,我听过的也是那些非常出众,或者非常有个性的,比如贝多芬的第九交响曲(《欢乐颂》就在其中)和巴赫的《咖啡康塔塔》;说实话,我的音乐品味很「流行」,比起古典乐,我更喜欢 Lady Gaga。

我喜欢音乐剧的原因,和音乐本身的关系其实不大。

在这之前要先解释清楚什么是音乐剧。和许多人的第一印象不同,音乐剧并不是什么高雅的艺术形式,实际上相当通俗。人们常常把音乐剧(Musical Theater)和歌剧(Opera)搞混,毕竟,真的有一部名为《歌剧魅影》的音乐剧。对普通人来说,区别实在不明显。

简单来说,歌剧重点在「歌」,而音乐剧重点在「剧」。歌剧的歌唱通常是连续的,不会被情节、对话和动作打断,而音乐剧的歌唱间隙可能会插入对话和其他台词,甚至歌词本身就可能是对话。此外,音乐剧往往会有更多的舞蹈和娱乐性表演,说是歌舞剧可能更准确。由于音乐剧重点在「剧」,所以理解台词以及歌词就很重要,不然会跟不上剧情,然而,聆听外语歌剧是很常见的,就算听不懂词,也能欣赏音乐。

对话穿插在歌唱中,而歌曲本身作为叙事载体推进剧情的例子,可以听这首:

Ready For This music cover

Ready For This

《Hazbin Hotel》

由于音乐剧的表演通常很戏剧化(这个词用在这里貌似很不准确,音乐剧本来就是戏剧),人物的动作很夸张,情感很丰沛,将这种情感与音乐结合在一起就显得格外富有感染力。在我看来,这也是音乐剧可能会被认为不够高雅的另一个原因:情感的表现不够矜持和细腻。无论是快乐、痛苦还是悲伤,都相当夸张。

我能想到的例子是《魔法坏女巫》,这本来是一部小说改编的音乐剧,在最近两年被搬上了电影大荧幕。如果仔细观察 Nessa(也就是主角的妹妹,东方坏女巫)在原版音乐剧和电影版中的表现,就会发现电影版 Nessa 的表现几乎可以说是淡漠,没有音乐剧的那种歇斯底里。由于音乐剧版本的原声带里没有收录《The Wicked Witch of the East》这首歌,所以读者可以参考 这个视频 对比区别。

我向来更喜欢饱满的情感,就像喝咖啡也更喜欢日晒处理的咖啡豆(这类豆子一般风味浓烈,带有更多的水果风味),如果要剖析的话,大概和我童年没有受到太多关爱有关——但说实话,这类话题谈得多了,已经有些庸俗了。

除了情感更饱满,我还更喜欢音乐剧的粉丝群体。说起来,音乐剧是音乐和舞台剧的奇妙组合,这也导致人们对音乐的要求更低,对剧情的要求也更低。这并不是说音乐剧在这两方面都不行,而是说,我很少发现有人对音乐剧的音乐或者剧情做出极端的负面评价。

一方面,不少音乐剧都改编自本身口碑就很好的小说或电影,比如《摇滚红与黑》改编自司汤达的《 红与黑 》,《雨中曲》改编自同名电影。另一方面,当所有人都沐浴在听觉和视觉的双重洗礼下,没有人会把心思放在批评上。回想起来, 去年年底 去剧院看过《雨中曲》之后,我就相信不会有人讨厌音乐剧,也很少会对某个特定的音乐剧做出负面评价。

由于我所在的地方几乎没人看音乐剧,所以当时剧院卖不出去票,但即便人只坐满了不到一半,观众的掌声和喝彩依旧很激烈。音乐剧的舞台是很大的场面,场上能站很多人,在有这么多人的地方还能编排好舞蹈,而舞蹈又能与剧情的场景完美融合在一起,本身就很令人佩服。我印象最深的是开场舞,剧情里应该是电影的拍摄现场,我记得甚至有演员站在剧院后台会用来挂衣服的小推车上跳舞,同时还有另一个人推动着他。

音乐剧的剧情衔接也不需要很巧妙地换场和紧密的逻辑,我还记得《Make ‘Em Laugh》这首歌演唱到一半,演员为了演示「如何让观众大笑」,甚至直接在舞台上表演撞大墙。倘若是其他的叙事媒介,看到有人在毫无预兆的情况从不知道什么地方搬来一块泡沫墙壁,还真的撞破了,真的会觉得有些奇怪,除非是喜剧。

若难以理解,可以想象单独的舞蹈作品,除非编排得很烂,否则很少有人会觉得舞蹈难看,夸张的肢体动作非常吸引注意力。现在,想象这个舞蹈者一边跳舞一边唱歌,歌曲不仅节奏抓耳,歌词也朗朗上口;他的旁边还有很多伴舞,都穿着颜色鲜艳的服装,身体大幅度地摆动着;除了伴舞,可能还有对手,他们唱歌和跳舞都有来有回;而这一切都在发生的同时,故事还在发展,很快就会有冲突、不速之客、意外和转折。

说真的,这样的表演难道不能让你全心全意地投入进去吗?难道不会让你甘愿放下手机,把全部的感官和注意力都献给演员们吗?难道不会让你觉得社交媒体和短视频上碎片化的娱乐信息都食之无味吗?

我每天沉浸在对技术的钻研、对人文社科的探索和无止尽的软件开发以及运维工作当中,尽管富有热情,但总归会感到疲乏。至少我最近明显地感觉到,大量脑力活动过后,我会感到心情低落,这可能是大脑活动消耗了很多糖,血糖快速降低导致的(因为心情低落的同时,我还会感到饿)。

这种情绪波动可能是生理和心理的双重作用,但无论如何,我需要将思绪从西西弗斯的命运中暂时抽离出来,投入一项关注身体感官而非精神和直觉的活动,将我的感官完全交给一支管弦乐队、一群剧场演员和背后的剧作家与作曲家。在那里,我不会看到有人讨论某部剧的续集和后几季如何毁了这部作品,不会看到有人动不动就要给编剧寄刀子,不会有人不合时宜地对情节的合理性发出自以为是的质疑。我会把我的心灵,暂时地,全部献给在我眼前上演的艺术。

如果真的有人不喜欢音乐剧?那就……

The Guy Who Didn't Like Musicals music cover

The Guy Who Didn't Like Musicals

《The Guy Who Didn't Like Musicals》

Should we kill him?
该不该杀了他?

Should we kill him?
该不该杀了他?

Oh, he pines after a cute lil’ barista
噢,他追求那个可爱的小咖啡馆服务生

Isn’t that worth a show-stopping fiesta?
难道不值得一场震惊四座的狂欢?

But for some damn reason, he won’t join our singing seaon
但出于某些原因,他不愿意加入我们的歌唱季

What an ass, what a bitch, what a cock
真是个蠢货,一个婊子,一个傻屌

The guy who didn’t like musicals…
那个不喜欢音乐剧的人……

Webmention 简明指南

2026-03-10 23:04:43

Webmention 是一个 W3C 推荐标准, IndieWeb 很喜欢这个标准,甚至制订了名为 Salmention 的拓展,只可惜 Webmention 本身就没什么人用,太小众了,这个拓展标准更是没多少人跟进和实现。对于独立博客来说,这项技术其实相当有用,实现起来也不复杂,但中文博客中支持发送和接收 Webmention 的很少,中文资料也几乎没有。

本文意在解释什么是 Webmention、如何使用它,以及如何让自己的网站支持 Webmention。

什么是 Webmention?

在即时聊天软件和社交媒体中,用户可以使用 @用户名 的格式提及另一个用户,对方会收到通知,知道他被提及了。 这是相当有用的功能,只可惜一般的提及功能是局限于某个平台上的,没办法跨平台通知,比如,如果你使用 Telegram,就没办法提及 QQ 上的用户。Webmention 虽然不是用来解决这个问题的,但它的确提供了一种分布式社交的能力,允许某人在一个网站上提及另一个网站上的内容。

假设 Alice 发布了一篇文章,URL 是 https://alice.blog/interesting-post,而 Bob 读到之后觉得很不错,在自己的网站上写了一篇文章,可能是回应,可能是表达喜爱,也可能只是简单地提及了,Bob 的这篇文章的 URL 是 https://bob.site/cool-stuff

这便是 Webmention 的应用场景。如果 Alice 的网站支持 Webmention,那么 Bob 就可以向 Alice 的网站发送 Webmention,这样 Alice 就知道她写的内容被提及了,她也可以把 Bob 文章的链接展示在网页下,让其他人也知道 Bob 写了一篇回应。

这个过程是如何发生的呢?

Webmention 其实很简单,它只是 HTTP 请求。首先,Bob 要找到 Alice 网站用于接收 Webmention 的端点,向这个地址发送 POST 请求,请求包含两个值,sourcetarget——前者是 Webmention 的源地址,也就是 Bob 的文章,Webmention 是从他的网站发送过来的;后者是目标地址,也就是 Alice 的文章,是 Webmention 要发送到的地方。

如果你不懂什么是 HTTP 请求,可以这样理解:当你在一个博客的评论区填写名字、邮箱地址和评论内容并点击发送按钮之后,你就向这个网站接收评论的端点发送了一条 HTTP 请求,准确来说,是 POST 类型的 HTTP 请求,而 Webmention 也是相同的请求,不过请求发送到的端点不同,请求的内容也不同。

Alice 的服务器通过这个端点接收到了 HTTP 请求之后,就会检查这是不是有效的 Webmention。有效性的要求很低,只要 sourcetarget 都是有效的 URL,source 真的包含了 target 链接(即真的提及了 target),就视作有效,没有其他格式要求。接收到 Webmention 之后要怎么处理,W3C 没有做规范,完全取决于接收端怎么实现。

这里有必要说明一下,Webmention 只是技术标准,而不是具体的软件。就像你可以使用不同的邮件客户端发送电子邮件,发送出去的邮件其他人用不同的客户端也能正常查看,就是因为电子邮件是开放标准,而不是具体的软件。

没什么好讲的了,Webmention 就是这么简单:一个网页提及了另一个网页,这个网站向对方网站发送 POST 请求来通知对方,对方网站接收这个请求。

如何发送 Webmention?

你不需要对自己的网站做任何修改就能够发送 Webmention,因为它只是一个 POST 请求。接收端不会验证请求的来源,仅仅是验证 sourcetarget 两个 URL,所以你可以从任何地方发送请求。

比如,使用 curl

curl \
 -d 'source=https://bob.site/cool-stuff&target=https://alice.blog/interesting-post' \
 https://alice.blog/webmention

注意这里的 https://alice.blog/webmention,这是 Alice 博客的 Webmention 端点,也就是 Webmention 要被发送到的地方。要找到这个端点很简单,只需要检查 Alice 博客的 <head> 标签,找到 rel="webmention"<link> 标签。

<head>
 <link rel="webmention" href="https://alice.blog/webmention">
</head>

这是标准的,声明 Webmention 端点的方式,也是其他人知道 Alice 的网站支持 Webmention 的判断依据。向这个地址发送 POST 请求,Alice 的 Webmention 接收器就会收到请求。

不过,一般没有人会用命令行发送 Webmention,这太不友好了。很多支持 Webmention 的博主会在网站上放一个输入框和一个按钮,表示你可以在这里输入你的文章 URL,然后点击发送,对方就能接收到 Webmention 了。这个表单做起来非常简单,我会在下一节「如何接收 Webmention 讲到」。

只要是能够发送 POST 请求的方式,都能够用来发送 Webmention。一般来说,发送 Webmention 的过程会被自动化,每当有一篇新文章被发现,就检查这篇文章里包含的外部链接,然后逐个请求这些外部链接,检查它们有没有声明 Webmention 端口,如果有,就向这个端口发送 Webmention。

发送 Webmention 的自动化工具

webmention.app 提供了发送 Webmention 的工具,最简单的方式是使用 API。假设你在一篇 URL 为 https://my.site/post-xxx 的文章里包含了一些外链,你想向这些链接发送 Webmention,那么你可以向这个地址发送 POST 请求:

POST https://webmention.app/check/?url=https://my.site/post-xxx

这样就通知了 webmention.app,让它帮你检查你这篇文章里包含了哪些外链、哪些支持 Webmention,然后向有接收端的链接发送 Webmention。你可以在他们的首页最下方的输入框输入你的 URL 并点击「START」,来测试自动发送。

如果要做到全自动,那也很简单。最方便的情况是:你有一个 RSS 订阅源。这样,就可以用 IFTTT 创建一个工作流,在 RSS 更新时通知 webmention.app 检查你的新文章,并帮你发送 Webmention。具体见 这个教程

如果你不想依赖 IFTTT 这样的服务,那就要根据你的网站架构来决定实现方式了。

如果你的网站是动态的,使用 WordPress 或 Typecho 等动态博客软件构建,那么你可以找一找有没有实现了 Webmention 的插件可以使用。我找到了 WordPress 插件 ,其他的软件需要读者自行搜索。

如果你的网站是静态的,使用 Hugo、Astro、Hexo、11ty 等静态网站生成器构建,那么你可以参考 我给 Hugo 添加自动发送 Webmention 能力 的方法。我用到了 @remy/webmention 这个 NPM 包,是命令行工具,在网站构建完成后用这个命令行工具扫描一遍 RSS 源,即可向文中提及的外部链接发送 Webmention。不必担心多次构建会重新发送 Webmention 的问题,接收端要是多次收到了相同的 Webmention,会做查重处理,这是标准里规定了的。

具体怎么在构建完成后自动执行这个命令,要看你使用的静态网站生成器。如果是基于 Node.js 开发的,比如 Hexo,可能会有类似这样的写法:

"scripts": {
 "postbuild": "webmention public/index.xml --limit 1 --send"
},

其他可以参考的链接:

如果觉得太难了,不配置自动发送也可以。手动发送反而更方便把控什么时候发送,什么时候不发送,毕竟有的时候可能不想要打扰别人。

如何接收 Webmention?

显然,你需要一个 Webmention 接收器。如果你的博客是用 WordPress 构建的,前文提到的 插件 已经具备了接收 Webmention 的功能。如果是其他博客软件,可能要考虑使用第三方 Webmention 接收器,或者自己部署接收器了。

使用 webmention.io

最简单也是最常用的第三方 Webmention 接收服务是 webmention.io ,我也在使用。这个服务是免费且开源的,所以还算值得信赖。要使用他们的服务,你首先需要配置 IndieAuth ,也就是表明你是这个网站的主人,怎么做到呢?

在网站的 <head> 添加:

<link rel="me" href="https://github.com/alice">
<link rel="me" href="https://twitter.com/alice">
<link rel="me" href="mailto:[email protected]">

alice 改成你自己的用户名。这个标记的意思是,这个网站的主人有 GitHub、Twitter 和电子邮箱账号,用户名或地址如上。接下来,确保你的 Twitter 账号页面或 GitHub 账号资料里有链接到这个网站。这样一来,你就证明了自己拥有这些账号,IndieAuth 也就允许你用这些账号登录。你不必配置全部三个账号,只需要一个有效的登录方式。如果是邮箱的话,IndieAuth 就会给你发送一封电子邮件确认登录。

你还可以把 <link> 写成 <a> 标签,如果网页里已经有这些账号页面的链接了,只需要给 <a> 加上 rel="me" 即可。这种标记方式叫作 microformats,后文还会提到。

配置好 IndieAuth 之后,在 webmention.io 输入域名登录,接下来会进入仪表盘。现在,你需要在 <head> 里面指定 webmention.io 作为接收器,添加这段内容:

<head>
 <link rel=webmention href=https://webmention.io/你的域名/webmention>
</head>

接下来 webmention.io 就能够帮你接收 Webmention 了。这个服务是 Aaron Parecki 提供的,如果你觉得不错,可以去支持他!他还是 IndieWebCamp 的创始人和 OAuth 工作组的编辑者。

好了,现在你能接收 Webmention 了,数据存放在 webmention.io 的服务器里,要怎么查看接收到的 Webmention 呢?有这样几种方式:

  1. 仪表盘 查看最近的 Webmention。
  2. 设置 获取 RSS/Atom 订阅源,使用 RSS 阅读器查看。
  3. 配置 Webhook ,在有新 Webmention 时向指定 URL 发送 HTTP 请求,可以配合 Barkntfy 推送到你的手机上。
  4. 通过 API 获取数据。

如果你想通过 API 把 Webmention 展示在自己的网站上,最简单的方法是使用 webmention.js 。把 JavaScript 文件放在自己的网站上之后,在需要显示 Webmention 的地方添加:

<div id="webmentions"></div>
<script src="/path/to/webmention.min.js" async></script>

具体的使用方式见项目文档。

如果能力允许,可以自己编写渲染逻辑,毕竟 API 已经有了。

自托管 Webmention 接收端

webmention.io 很好用,配合 webmention.js,甚至不需要自己写一行代码,不需要部署任何服务,就能接收并展示 Webmention,这对大部分人来说已经够了。不过,如果你很在乎自己的数据,并希望尽可能少依赖第三方服务,那么你大概要自己部署接收端了。

在可以自己部署的 Webmention 接收端中,使用者比较多的是 Horst Gutmann 维护的 webmentiond ,用 Go 编写,存储基于 SQLite,简单轻量。此外还有用 Rust 编写的 WesleyAC/webmention-receiver 和用 Python 编写的 capjamesg/webmention-receiver

对于重复造轮子这件事,我们国人也不输(奇怪的比较)。 Chlorine 不久前用 Rust 写了一个 Webmention 接收端,名为 CircleAt ,而我也用一门 Lisp 方言开发了 Weepinbell ,最近才发布 v0.1.0 版本。不过我不建议你用 Weepinbell,因为还没有正式投入使用,可能存在一些问题,进入稳定版本之后我会再发一篇文章的。当然,如果你愿意贡献的话,我将感激不尽。

在开始之前,我得警告读者,Webmention 作为一门小众技术标准,使用者很少,自托管接收器的更少,如果遇到问题,可能很难找到解决方案和能够提供帮助的人(虽然我很欢迎有人来问我啦)。如果对自己的技术没什么信心,webmention.io 已经很好了,比能够自托管的选项都要成熟不少。

不过剩下的我也没什么好讲的了,如果你决定自托管,那就选择一个自己觉得不错的接收端软件部署到服务器上。至于如何查看接收到的 Webmention 和如何展示,不同的接收端都有实现细节上的不同,这里略过。

如何制作一个 Webmention 表单?

还记得我们说发送 Webmention 就是发送一个 POST 请求吗?想想还有什么是 POST 请求?一个网页上能够发送 POST 请求的最常见的元素是什么?

没错,就是表单。

<form action="https://alice.blog/webmenion" method="POST">
 <input type="url" name="source">
 <input type="hidden" name="target" value="{{ .Permalink }}">
 <button type="submit">发送</button>
</form>

其中:

  • action 是 POST 请求要发送到的地方,这里填写你的 Webmention 端点
  • <input name="source"> 是用户输入的,对方文章的 URL
  • <input name="target"> 是你这篇文章的 URL,一般由程序自动生成;这里写的 {{ .Permalink }} 是 Hugo 的页面永久链接,你应该把它替换成你的博客软件的变量。

你需要改的就只是 action 的地址和 <input name="target">value,不需要写 JavaScript。不过,用户点击提交之后会被直接传送到 action 所指向的地址,如果你的 Webmention 接收端只返回 JSON,用户可能就不清楚到底有没有提交成功,还是有些不太友好的。如果你愿意的话,可以自己用 JavaScript 处理返回的数据,制作直观的提示,比如「发送成功」之类的。

放一个这样的表单在网页上并说明清楚作用,即便是不了解 Webmention 的访客也懂得如何使用。

让 Webmention 生动起来:microformats

microformats 是一系列开放数据格式,建立在已被广泛采用的标准之上(比如 HTML)。它并不是和 Webmention 强绑定的,实际上 Webmention 标准完全没有提到过 microformats,只不过 IndieWeb 上的人很喜欢把它和 Webmention 一起用。说起来,应该是 webmention.io 的维护者 Aaron Parecki 带的头,webmention.io 默认支持解析 microformats。

由于没有 microformats 也不影响使用 Webmention,如果你不感兴趣的话,可以跳过这一节。

microformats 是什么?

回答这个问题之前,先要了解它解决了什么问题。

网页可以是任何东西,可以是 Web 应用(比如 Notion 这样的在线笔记),可以是小工具(比如计算器、格式化工具、视频下载器),可以是相册,也可以是一篇文章,可以是日历。同一张网页也可能包含各种各样的东西,一篇博客文章中有页面标题、主体内容、标签、评分、回复等等。

网页的具体结构也是不确定的,为了实现不同的排版和效果,不同网站的 HTML 结构完全不同。有的可能是这样的:

<main id="container">
 <h1 id="post-title">Title</h1>
 <article id="post-content">
 <p>Content</p>
 <!-- ... -->
 <p>Author is ..., follow me on <a href="https://mastodon.social/@author">Mastodon</a></p>
 </article>
 <div id="comment">
 <!-- ... -->
 </div>
</main>

也有可能是这样的:

<main id="container" class="flex justify-center items-center my-10 mx-auto p-4">
 <header class="max-h-lg shadow-lg">
 <div class="rd overflow-hidden">
 <img src="/posts/xxx/banner.jpg" class="block">
 </div>
 <h1 class="font-extrabold text-3xl">Title</h1>
 <p class="font-semibold text-xl">Subtitle</p>
 </header>
 <article class="text-md leading-loose prose">
 <div id="post-content">
 <!-- ... -->
 </div>
 <div id="post-endnotes">
 <ul>
 <!-- ... -->
 </ul>
 </div>
 <div id="author-info" class="bg-white px-4 py-6 my-2 shadow">
 <p>Author Name</p>
 <p>Description</p>
 <a href="https://mastodon.social/@author">Mastodon</a>
 </div>
 </article>
 <footer>
 <!-- ... -->
 </footer>
</main>

偶尔还能见到一些莫名其妙的 HTML 结构,比如我就见过这样的:

<div id="scroll-body">
 <div id="contant-wrap-wrap">
 <div id="content-wrap">
 <div id="main-content">
 <div id="page-content">
 <h1 id="page-title">...</h1>
 <p>...</p>
 </div>
 </div>
 </div>
 </div>
</div>

简直有大病。 无论如何,尽管它们的结构不同,但包含的内容是相似的,换句话说,数据结构是相似的。他们都有可能是一篇文章,包含页面标题、内容、作者信息等等,这篇内容同时还有可能是另一篇内容的回应、点赞或转发。

被浏览器渲染为图形过后,人读起来会理所应当地觉得这些就是相同的数据,但在机器看来就难了。HTML 结构有无限种可能,要怎么让机器快速且准确地判断某个网页属于什么类型的内容,它包含了哪些结构化的数据?

其实已经有一些方案被提出了,比如被广泛使用的 Open Graph 协议 。网页只要按照标准,添加一些 <meta> 标签,就可以使其成为一个数据对象。Facebook 等社交媒体,在遇到这些链接的时候,会爬取 Open Graph 的内容,生成更具视觉效果的卡片。

以下是 Open Graph 的一个例子:

<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />

microformats 是类似的东西,但他没有规定新的 <meta> 标签类型,也不像另一个标准 JSON-LD 一样要求插入一大段包含 JSON 的 <script> 标签。它就建立在最基础的 HTML 之上。

以下是用 microformats 标记的一篇博客文章示例。

<article class="h-entry">
 <h1 class="p-name">Microformats are amazing</h1>
 <p>Published by <a class="p-author h-card" href="http://example.com">W. Developer</a>
 on <time class="dt-published" datetime="2013-06-13 12:00:00">13<sup>th</sup> June 2013</time></p>

 <p class="p-summary">In which I extoll the virtues of using microformats.</p>

 <div class="e-content">
 <p>Blah blah blah</p>
 </div>
</article>

注意到 class 的值了吗?p-name 标记了这篇博客文章的名字,p-author 标记了作者,p-summary 标记了这篇文章的总结摘要,e-content 则是这篇文章的内容。最外层的 h-entry,标记这是一个条目,博客文章、社交媒体发帖和维基页面等等,都可以用这个类名标记。

要实现 microformats 标准,你不需要在其他地方添加任何别的格式的数据,只需要在原有的 HTML 上操作就好了。刚刚我们看到的是 h-entry 格式,用来标记发布在万维网上的内容,除此之外,还有用来标记人和组织的 h-card ,用来标记日历和事件的 h-event 等等。

我们刚才用来配置 IndieAuth 的 <link rel="me"> 标签,也是一个 microformats 格式 。它更常见的用法是直接标记在 <a> 标签上:

<a href="https://github.com/tantek" rel="me">@t</a>
<a href="https://tantek.com/" rel="me">https://tantek.com/</a>

rel="me" 本身不是用来配置 IndieAuth 的,只是标记网站站长的其他网页链接的一种方式。IndieAuth 只是使用了 microformats 的这个格式而已。

总而言之,microformats 是名副其实的「微格式」,不需要对网站结构做出太大改动,就可以标记结构化的数据。

和 Webmention 的关系

我猜测除了 webmention.io 之外,没有太多接收端实现了解析 microformats 的能力(不过, Weepinbell 可以!)。最初 Webmention 仅仅是「A 网页提及了 B 网页」的通知,并没有什么复杂的分类,但 microformats 的使用让一切都不简单了起来。

一般来说,相互提及的网页都是博客文章,文章是用 microformats 的 h-entry 标记的。这个格式除了标记基本的元信息之外,还有这样的属性:

  • u-in-reply-to:这个 h-entry 回复了哪篇文章
  • p-rsvp:这个 h-entry 是对什么的 RSVP,即表明自己是否要参与某次聚会、活动或事件
  • u-like-of:这个 h-entry 是对哪篇文章的喜欢(点赞)
  • u-repost-of:这个 h-entry 是对哪篇文章的转发

比如,Bob 回应了 Alice 的文章,它在文档开头提及了 Alice 的文章,它可能是这样写的:

<div class="h-entry">
 <h1 class="p-name">对 Alice 文章的回复</h1>
 <div class="e-content">
 <p>前几天我在 Alice 的博客上读到了
 <a class="u-in-reply-to" href="https://alice.blog/">
 一篇有趣的文章
 </a>,
 我很受启发。</p>
 <p>对这个问题,我也有相似的思考。</p>
 <!-- ... -->
 </div>
</div>

你注意到了吗?Bob 把文章中提及的链接标记为 u-in-reply-to。如此一来,这篇文章在能够解析 microformats 的程序看来,就是对 Alice 文章的一篇回应。如果是 u-like-of,就是对那篇文章的点赞,u-repost-of 就是转发。

至于 p-rsvp,我觉得这个设计有点大病——真的会有人写网页公开发 RSVP 吗?这种东西难道不应该私下交流吗?无论如何,它是存在的,你可以用 microformats 标记 RSVP,也意味着你可以用 Webmention 来发 RSVP 通知他人你是否决定出席某次活动。

具体而言,Webmention 和 microformats 这两个标准是这样一起使用的:支持解析 microformats 的 Webmention 接收端,在收到请求时,除了做最基本的检查之外,还会解析 source 网页中的 microformats,如果有的话,就一起保存下来。

IndieWeb 上的居民会根据 h-entry 是否包含 u-in-reply-to 等属性,决定展示 Webmention 的方式。如果有 u-in-reply-to,就显示为回复或评论;如果有 u-like-of,就显示为点赞;如果有 u-repost-of,就显示为转发。以下是 Barnaby Walters 的网站实现这一展示方式的例子。

Barnaby 的 网站

如果都没有,那就当作普通的提及,也就是最原始的 Webmention。

在 Webmention 中显示自己的名字和头像

上一小节解释了 h-entry 格式,用来标记内容。用来标记作者时,可以用到 h-card 格式。

<a class="h-card" href="https://tantek.com/">Tantek Çelik</a>,

<span class="h-card">
 <a class="p-name p-org u-url" href="https://microformats.org/">microformats.org</a>
</span>

h-card 本来是独立的,用来标记人和组织的格式,但它也可以作为 h-entry 的子属性 p-author,放在 h-entry 里,标记这个内容的作者。

<div class="h-entry">
 <div class="p-author h-card">
 <img src="https://aaronpk.com/images/aaronpk.jpg" class="u-photo" width="40">
 <a href="https://aaronpk.com/" class="u-url p-name">Aaron Parecki</a>
 </div>
</div>

这样,其他软件在解析 microformats 的时候,就能发现你的名字和头像了。一般来说也会展示在 Webmention 里。

同步来自联邦宇宙的提及

你可能怀疑会不会真的有人这样用 microformats,给链接标记 u-in-reply-to 这样的类名总归有点麻烦,大部分时候我们只是简单提及一篇文章而已,不见得就是对那篇文章的直接回应,或者说点赞和转发。这些东西更像是社交媒体上会用到的。

嗯?社交媒体?

既然我们在讨论开放的标准,那就不得不提及开放且去中心化的社交媒体了。是的,我说的就是 联邦宇宙 。有没有办法把来自联邦宇宙的点赞、回复和转发,以 Webmention 的方式同步到网站上呢?

答案是使用 Brid.gy ,这个服务允许你用 Mastodon 账号(以及其他兼容 Mastodon API 的联邦宇宙软件)登录,并将社交媒体账号与你的网站桥接起来。它做的事情很简单:

  1. 每隔一段时间扫描你的社交媒体时间线,以及其他人与你的互动。
  2. 如果发现你的某篇帖文包含你的网站上的 URL,而又有人点赞、回复或转发了这篇帖文,那么就视作这个人点赞、回复或转发了你网站上对应的这篇文章。
  3. 创建一个占位 HTML 页面,包含用 microformats 标记的 h-entry,以这个占位页面作为 source,给你网站上的页面发送 Webmention。(如果你直接访问这个占位页面,会被跳转到实际的联邦宇宙帖子,但 Webmention 接收器会把这个占位页面视作真实的 source 地址)

如此一来,你的网站就通过 Webmention 和联邦宇宙互通了。

Bridgy 的后台,列出了联邦宇宙上的互动,其中的一些已经作为 Webmention 发送

我的网站就是这样做的,拉到页面最下方,你就能看到来自联邦宇宙的点赞和回应,不过我还没想好应该如何展示「转发」这个类型的 Webmention。

自己动手写 Webmention 接收端

自己写 Webmention 接收器并不困难!你只需要把 Webmention 文档 中有关接收端的部分读一遍,知道需要注意哪些技术细节12就好了。你只需要提供一个用于接收 Webmention 的端点,至于查询 Webmention 的 API,以及要不要解析和储存 microformats,都可以自己决定。

你完全可以只写一个给自己用的 Webmention 接收端,接收到 Webmention 请求后就通过聊天机器人发送给自己,不做展示也不做过多的处理,甚至不用储存。Webmention 尽管有规定删除和更新 Webmention 的方法(如果 source 有更新或者删除了引用,重新发送一次 Webmention 就应该能够更新),但并没有说一定要储存和展示 Webmention。

你也可以把 Webmention 的功能做得很丰富,用来代替评论系统。无论如何,如果你决定自己写 Webmention 接收端的话,这都取决于你,只要保持最基础的标准就好了。

你也可以看看 Salmention ,比起 Webmention,它只是增加了多级回应的功能。比如 Chris 看到 Bob 回应 Alice 的文章之后,也写了一篇文章回应 Bob,并给 Bob 发送了 Salmention。如果 Alice 的网站也支持 Salmention 的话,Alice 就会收到 Chris 的 Salmention,尽管 Chris 只是直接提及了 Bob。不过,这个拓展标准很少有人用。

资源

相关阅读

工具

最后

希望有更多的人能用上 Webmention,一起愉快地写博客交流。

我写累了,一会儿见吧。


  1. 比如,W3C 规定,为了避免 DoS 攻击,所有的 Webmention 必须异步处理,也就是验证 sourcetarget 都是正确的 URL 之后,应该异步地向 source 发送 GET 请求,检查 source 的内容是否包含 target。 ↩︎

  2. 顺带一提,Webmention 其实不止支持 HTML,还可以提及 JSON 文档和纯文本文档。不过,虽然标准是这么写的,但应该不会有太多接收端很好地支持非 HTML 文档。 ↩︎