MoreRSS

site iconOldPanda修改

Douban Book+ 插件作者。目前居于旧金山湾区。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

OldPanda的 RSS 预览

将博客转移到了 Astro

2025-02-09 03:20:28

大约在七年前将博客转移到了 WordPress ,在这期间使用体验良好,也攒下了一堆文章,收获了各种 RSS 平台上的若干订阅。但随着近年家庭支出飙升,曾经眼中的一点小钱也变成了沉重的负担,看着一个月将近 20 块钱的服务器费用,不得不开始“降本增效”,所以今年年初花了将近一个月的工夫把博客迁移到了 Astro 。

为什么选择 Astro ?

因为我不想花钱,并且还希望保留自己的域名,那么最流行的选择基本只剩下了 GitHub PagesCloudflare Pages ,由于之前搭建 Douban Book+ 主页小地瓜主页的经验,所以毫不犹豫地选择了 Cloudflare ,这样的学习成本最低。

那么下一步就是挑选主页搭建框架了,我听说 Astro 还是去年看到 X 站(前推特)上有很多网友讨论这个框架,并且很多网友利用它做出了非常漂亮的网站,并有一系列建站教程出现,比如说这一篇就包含了非常详细的步骤,虽然没有那么多时间精力去详细学习了解一个新东西,但我还是想尝尝鲜。

于是在一系列拍脑袋的决策下,我决定在 Cloudflare Pages 上用 Astro 运行我的博客。

在此特别感谢 Moeyua 开发的这款 Typography 主题,简洁清爽,使读者可以沉浸于文字,而且在代码技术相关的文章方面也有不俗的表现力。

如何迁移?

WordPress 提供文章导出的功能, Astro 支持 Markdown 语法,两者格式并不兼容。幸运的是, Astro 官网提供了详细的迁移教程。成功将 WordPress 文章转换成 Markdown 格式之后,迁移的准备工作就算完成了。

在直接复制粘贴之前,还有几件事要做。

社交媒体图标

Typography 自带许多社交媒体图标,但还是无法满足我的需求,因为我同时活跃在微博、 X 站、小红书、蓝天、长毛象等各种平台,所以我需要一个容量更大的图标库

文章标签和图片

我的每篇文章几乎都带有若干个标签,这样容易归类查找,并且会配上一张或贴题或随意的图片,所以我需要一个专门的页面显示所有标签及某个标签下所有文章的列表,同时打开每篇文章时最开始需要显示图片,比如说下面这样

但这个主题没有提供这个功能,那么我只好自己动手丰衣足食了。

博客配置

这个没什么可讲的,就是把自己需要的信息填进去即可。

文章迁移

这块是我花时间最多的一部分,包括所有图片都迁移和文章格式的整理,因为有些内容并没有那么完美地转换成 Markdown ,总共大几十篇吧。不敢想象假如哪天文章多了之后,再次面对迁移需求应该如何是好,估计到时候就得开发一款趁手的工具了,希望这是最后一次吧。

部署

完成了上述操作,就可以把内容发布到 Cloudflare Pages 上了,具体步骤我就不在这里重复了,感兴趣的读者可以参阅这篇文章的“如何部署”部分。

参考资料

我的 2024

2025-01-01 12:23:00

谈笑风生又一年

时间如白驹过隙,又到岁末,毕竟距离去年年底仅仅间隔了三篇文章。

在旧金山湾区我干了这一年也没有什么别的,大概三件事,

  • 第一个,家庭最小的成员快乐成长了一岁;
  • 第二个,成功当了一年打工人;
  • 第三个,就是财务方面在去年的里程碑之上又前进了微小的一步。

如果说还有一点什么成绩就是支出一律记账,这个对家庭的可持续发展有很大关系。还有玩遍了附近的小公园也是一个很大的。但这些都是次要的,我主要就是这三件事情。很惭愧,就做了一点微小的工作,谢谢大家!

新年快乐!

用 Python 试玩十亿行挑战

2024-08-26 10:54:00

最近用 Python 玩了一下十亿行挑战,并在本地环境上将用时从将近八分钟优化到半分钟出头,在这期间了解了很多以前不知道的 Python 技巧,所以记录下来方便后续参考。

什么是十亿行挑战?

这是一个由 Gunnar Morling 发起的挑战,不过仅限于 Java 圈:编写一段 Java 程序在最短的时间内处理一个有十亿行的文件,其中每一行包含了一个观测站和对应的气温,前者是字符串类型,后者为浮点数,两者之间由分号间隔,就像下面的片段所示,

» head -n 10 measurements.txt
Thessaloniki;20.4
Malé;23.3
Rostov-on-Don;10.1
Heraklion;18.4
Alice Springs;2.7
Port Sudan;32.1
Port-Gentil;33.0
Napier;15.2
Sapporo;3.5
Malé;29.0

程序的输出为每个观测站的最低温,平均温度,和最高温,按观测站的名称排序,

{Abha=5.0/18.0/27.4, Abidjan=15.7/26.0/34.1, Abéché=12.1/29.4/35.6, Accra=14.7/26.4/33.1, Addis Ababa=2.1/16.0/24.3, Adelaide=4.1/17.3/29.7, ...}

后来这项挑战火出了圈,也有了一个专门的网站欢迎人们提交各种语言的版本。最近我用 Python 比较多,所以就用 Python 来玩一玩。

环境

  • CPU — Apple M2 Max
  • 内存 — 32 GB
  • 操作系统 — macOS Sonoma
  • Python — 3.12.5

版本 1.0

这项任务不需要考虑复杂的算法,唯一的难点在于输入的数据大小,十亿行大约是 13 GB 左右,得出正确的计算结果很容易,但计算地有效率很难。我们很快就能实现一个简单的版本并给出正确的输出,

from dataclasses import dataclass
from typing import Dict


@dataclass
class Temperature:
    min_temp: float
    max_temp: float
    sum_temp: float
    count: int


def process_file():
    temperatures: Dict[str, Temperature] = dict()
    with open("measurements.txt") as f:
        for line in f:
            station, temp_str = line.split(";")
            t = temperatures.get(station)
            temperature = float(temp_str)
            if not t:
                temperatures[station] = Temperature(
                    temperature, temperature, temperature, 1
                )
            else:
                t.min_temp = min(t.min_temp, temperature)
                t.max_temp = max(t.max_temp, temperature)
                t.sum_temp += temperature
                t.count += 1

    with open("panda.txt", "w") as out_f:
        out_f.write("{")
        for station, t in sorted(temperatures.items()):
            out_f.write(
                f"{station}={t.min_temp:.1f}/{t.sum_temp / t.count:.1f}/{t.max_temp:.1f}, ",
            )
        out_f.write("\b\b} \n")


if __name__ == "__main__":
    process_file()

耗时如下

463.26s user 2.63s system 99% cpu 7:49.20 total

在一个 CPU 跑满的情况下用了将近八分钟才结束,我们将在此基础上进行优化。

优化

整个代码总共做了三件事,

  1. 读文件
  2. 按 1brc 的说明处理数据
  3. 写文件

在最终的结果中,所有观测站的数量只有几百个,因此在最后一步写文件上下工夫是没有必要的,需要优化的地方主要集中在前两步,一个是如何有效率地读文件,一个是如何在处理数据时有效地利用多核。

读文件耗时分析

我们先来看一看在版本 1.0 中按行读文件的方法究竟需要花费多久,把读文件的代码抽取出来只有三行,

with open("measurements.txt") as f:
    for _ in f:
        pass

耗时

56.77s user 2.16s system 99% cpu 58.985 total

只是简单过一遍文件就需要差不多一分钟,这个 I/O 开销实在是太大了,这一部分毫无疑问存在优化的空间。

经常与文件打交道的话可以知道一次读取多个字节的数据通常比按行遍历要快,但需要改一下代码找出最合适的数据块大小,

import time

chunk_sizes = []
for i in range(10, 31):
    chunk_sizes.append(2**i)

for chunk_size in chunk_sizes:
    start = time.time()

    def read_file():
        with open("measurements.txt", mode="r") as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                yield chunk

    for _ in read_file():
        pass

    print(f"CHUNK SIZE: {chunk_size: >10}, ELAPSED TIME: {time.time() - start}")

运行结果为

CHUNK SIZE:       1024, ELAPSED TIME: 4.204729080200195
CHUNK SIZE:       2048, ELAPSED TIME: 3.5845539569854736
CHUNK SIZE:       4096, ELAPSED TIME: 3.1715991497039795
CHUNK SIZE:       8192, ELAPSED TIME: 2.072834014892578
CHUNK SIZE:      16384, ELAPSED TIME: 1.478424072265625
CHUNK SIZE:      32768, ELAPSED TIME: 1.1602458953857422
CHUNK SIZE:      65536, ELAPSED TIME: 1.0105319023132324
CHUNK SIZE:     131072, ELAPSED TIME: 0.9584476947784424
CHUNK SIZE:     262144, ELAPSED TIME: 0.9280698299407959
CHUNK SIZE:     524288, ELAPSED TIME: 0.9165401458740234
CHUNK SIZE:    1048576, ELAPSED TIME: 0.8949398994445801
CHUNK SIZE:    2097152, ELAPSED TIME: 1.367276906967163
CHUNK SIZE:    4194304, ELAPSED TIME: 1.3553977012634277
CHUNK SIZE:    8388608, ELAPSED TIME: 0.9136021137237549
CHUNK SIZE:   16777216, ELAPSED TIME: 1.400062084197998
CHUNK SIZE:   33554432, ELAPSED TIME: 1.4531581401824951
CHUNK SIZE:   67108864, ELAPSED TIME: 1.6019279956817627
CHUNK SIZE:  134217728, ELAPSED TIME: 1.5939080715179443
CHUNK SIZE:  268435456, ELAPSED TIME: 1.6016528606414795
CHUNK SIZE:  536870912, ELAPSED TIME: 1.8202641010284424
CHUNK SIZE: 1073741824, ELAPSED TIME: 2.6885197162628174

直接取最小值 1048576 作为数据块的大小。但这样读出来的每一块结尾未必对应着行尾,所以简单粗暴截出来的数据块送给下游代码去处理的话会出现同一行的内容不完整的情况,因此需要稍微修改一下读文件的代码:在每次读完之后需要检查末尾是否为 \n ,若不是的话继续按字节读取,直到第一个行尾。对应的读文件代码为

def read_chunks(filename: str) -> Generator[str, None, None]:
    CHUNK_SIZE = 1048576
    with open("measurements.txt", mode="r") as f:
        while True:
            chunk = f.read(CHUNK_SIZE)
            if not chunk:
                break
            while chunk[-1] != "\n":
                chunk += f.read(1)
            yield chunk

for _ in read_file("measurements.txt"):
    pass

耗时

4.37s user 3.49s system 99% cpu 7.870 total

可以看到比按行读取大大降低了用时。

到这里还远远不是结束,还可以继续优化,据 Python 官方文档所言,以二进制模式读文件时可以省去将字节解码为字符串的开销,

As mentioned in the Overview, Python distinguishes between binary and text I/O. Files opened in binary mode (including 'b' in the mode argument) return contents as bytes objects without any decoding.

因此将文件的打开模式改为 mode="rb” ,对应的代码为

def read_chunks(filename: str) -> Generator[bytes, None, None]:
    CHUNK_SIZE = 1048576
    with open(filename, mode="rb") as f:
        while True:
            chunk = f.read(CHUNK_SIZE)
            if not chunk:
                break
            while chunk[-1] != 10:
                chunk += f.read(1)
            yield chunk


for _ in read_file("measurements.txt"):
    pass

耗时

1.91s user 0.97s system 99% cpu 2.885 total

这样读文件的优化就可以告一段落了。

多进程

下一步考虑的是如何把上面读出来的文件块分解成行,并把每一行的数据解析汇总得到最终想要的结果。

机器总共有 12 个核,那么只有一个干活,剩下的十一个围观就是极大的浪费,并且在上面的分析中整个文件已经被切成了若干块,非常适合用多进程并行处理。为什么不考虑多线程呢?请参考 Python 的 GIL 这一段,简单来说由于 Python 在最初设计时的问题,在运行 CPU 密集型任务时在同一个时刻只有一个线程可以获得全局解释锁,其他线程都被挂起,直到正在运行的线程退出。分析聚合每一行的数据明显要吃掉大量 CPU 资源,这个时候开一堆线程去抢唯一的一把锁无疑会浪费大量的资源,最终的程序性能反而不一定比最初的单核版本好。

应用多进程之前,数据块处理中的一段代码需要优化,

...
t.min_temp = min(t.min_temp, temperature)
t.max_temp = max(t.max_temp, temperature)
...

在上面的实现中,无论比较结果如何,都需要对 t.min_tempt.max_temp 进行一次赋值,其实这是没必要的,可以改成

...
if temperature < t.min_temp:
    t.min_temp = temperature
if temperature > t.max_temp:
    t.max_temp = temperature
...

这样处理每个数据块的函数可以写作

def process_chunk(chunk: bytes) -> Dict[bytes, Temperature]:
    temp_result: Dict[bytes, Temperature] = dict()
    for line in chunk.splitlines():
        station, temp_str = line.split(b";")
        temperature = float(temp_str)
        t = temp_result.get(station)
        if not t:
            temp_result[station] = Temperature(temperature, temperature, temperature, 1)
        else:
            if temperature < t.min_temp:
                t.min_temp = temperature
            if temperature > t.max_temp:
                t.max_temp = temperature
            t.sum_temp += temperature
            t.count += 1
    return temp_result

Python 自带的进程池允许我们直接把函数映射到一个迭代器上,在这里就是 read_chunks 函数返回的生成器

def map_job(filename: str) -> Iterator[Dict[bytes, Temperature]]:
    with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
        return pool.map(process_chunk, read_chunks(filename))

最后需要用一个 reduce job 来集合 map job 产生的临时结果,整理后的完整代码如下所示

import concurrent.futures
import os

from dataclasses import dataclass
from typing import Dict, Generator, Iterator


@dataclass
class Temperature:
    min_temp: float
    max_temp: float
    sum_temp: float
    count: int


def read_chunks(filename: str) -> Generator[bytes, None, None]:
    CHUNK_SIZE = 1048576
    with open(filename, mode="rb") as f:
        while True:
            chunk = f.read(CHUNK_SIZE)
            if not chunk:
                break
            while chunk[-1] != 10:
                chunk += f.read(1)
            yield chunk


def process_chunk(chunk: bytes) -> Dict[bytes, Temperature]:
    temp_result: Dict[bytes, Temperature] = dict()
    for line in chunk.splitlines():
        station, temp_str = line.split(b";")
        temperature = float(temp_str)
        t = temp_result.get(station)
        if t is None:
            temp_result[station] = Temperature(temperature, temperature, temperature, 1)
        else:
            if temperature < t.min_temp:
                t.min_temp = temperature
            elif temperature > t.max_temp:
                t.max_temp = temperature
            t.sum_temp += temperature
            t.count += 1
    return temp_result


def map_job(filename: str) -> Iterator[Dict[bytes, Temperature]]:
    with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
        return pool.map(process_chunk, read_chunks(filename))


def reduce_job(
    temp_results: Iterator[Dict[bytes, Temperature]]
) -> Dict[bytes, Temperature]:
    result: Dict[bytes, Temperature] = dict()
    for item in temp_results:
        for station, temperature in item.items():
            t = result.get(station)
            if t is None:
                result[station] = temperature
            else:
                if temperature.min_temp < t.min_temp:
                    t.min_temp = temperature.min_temp
                elif temperature.max_temp > t.max_temp:
                    t.max_temp = temperature.max_temp
                t.sum_temp += temperature.sum_temp
                t.count += temperature.count
    return result


def write_file(result: Dict[bytes, Temperature], output_file: str):
    with open(output_file, "w") as out_f:
        out_f.write("{")
        for station, t in sorted(result.items()):
            out_f.write(f"{station.decode("utf-8")}={t.min_temp:.1f}/{t.sum_temp / t.count:.1f}/{t.max_temp:.1f}, ")
        out_f.write("\b\b} \n")

if __name__ == "__main__":
    input_file = "measurements.txt"
    temp_results = map_job(input_file)
    result = reduce_job(temp_results)

    output_file = "panda.txt"
    write_file(result, output_file)

耗时

301.05s user 21.41s system 961% cpu 33.548 total

与其他语言相比这个结果不怎么样,不过能把向来以慢著称的 Python 优化到这个程度大概也算可以了。

参考资料

《平民之宴》书评

2024-03-31 13:15:00

这本书的腰封上写着“看懂这本书,就看懂了中国中产阶级的焦虑”,这句话在我这并不成立,因为全书内容对我来说并不难懂,然而我还是没有看懂中国中产阶级的焦虑。

事实上,中产阶级的故事只是这本书的一个真子集,作者想要呈现出来的并非焦虑这种无用的情绪。而且“中产阶级的焦虑”本身是一个非常大的话题,想通过一部不算长的小说将其完全呈现出来是不可能的。

在这里我不打算谈论焦虑,而是聊一聊书中的几位女性,作为主角,她们每个人都有着鲜明的个性,每个人的生活背景不同,面对的选择和困扰也不一样,但都给人留下了深刻印象。故事中的男性则主要起到了背景板的作用。

福原家

故事从福原家开始,福原家的长子翔突然决定从高中退学,这对于通过自己努力升学过上中产阶级生活的福原一家来说自然是不可接受的。这里都不需要解释什么,只要稍加想象一下假如自己从高中退了学,自己的生活会是怎样的,父母的反应会是怎样的;或者想象自己班里有人在高中退学了,同学们是如何看待 TA 的,老师们又会如何评价 TA ,这一切矛盾冲突的来源就可以感同身受了。故事的戏剧性从一开篇就拉满了,吸引着读者继续读下去,探寻在这一冲突发生之后,剧情将会驶向何方。

由美子

作为福原家的女主人,由美子算是符合了我们心目中中国式家庭主妇的所有刻板印象,对丈夫略有微词,望子成龙,好面子,看来东亚各国还是有一定的共性的。可想而知这样的一个人在得知自己的儿子从高中退学之后会作何反应,从难以置信到勃然大怒,再到苦口婆心,再到听之任之但又怀揣他回心转意的希望,最终由于儿子始终不肯回头转而将希望寄托在了自己的小外孙身上。

在由美子的身上是看不到很强的自我意志的,她从来没想过自己要怎样,反而经常想着自己要别人怎样。她的心路历程也非常具有可预测性——从小被母亲教育要努力学习,结婚后期望丈夫出人头地,有了孩子之后望子成龙望女成凤,当儿子不愿按她的意愿继续折磨自己之后就把希望的焦点转到了外孙身上,这一切仅仅是为了不让自己的家庭跌落下所谓的“中产阶级”。尽管由美子认为自己已经穷尽了所能,但她唯独忘了自己,她的种种努力只不过是把自己的意志投射到别人身上,自己活得不像自己,也强迫着别人活得不像别人自己,最终的结果就是所有人一起痛苦。

可奈

前两天刚刚引用茨威格的一句话转发了一条微博,感觉用在可奈身上非常合适,“她那时还太年轻,不知道所有命运赠送的礼物,早已在暗中标好了价格”。可奈从一开始就知道自己想要什么,那就是依靠别人让自己过上更好的生活,并且她在这个长达数年的计划中展现了超强的行动力,结果也确实如愿以偿,但可惜高收入带来的是超高的工作压力和失业风险,最终她丈夫回自己家养病,她也不得不带着自己的孩子回了娘家。

不能说可奈犯了什么错误,因为她从小耳濡目染的都是这些东西,她确实在自己的见识认知范围内为自己争取到了最大的利益。如果她能把那些行动力和精于计划的头脑用在自己身上,肯定会是另一个故事。可惜在由美子这样的家庭教育下,是不会养出那样的孩子的。

宫城家

珠绪

珠绪可以被看作是可奈的反面,也是被大部分人喜欢的一个角色,因为能从她身上看见“光”,这是一个真正依靠自己的人。

出身于南部离岛,大概算作日本的乡下,父母离异,自己高中毕业就出来打工,后来遇到高中辍学的翔并成为他的女朋友,怎么看都不是所谓的“中产阶级”家庭,然而就是这样一个女孩子,在由美子的嘲讽下决定复习考取医科大学。天助自助者,无论是朋友,著名医生,还是培训学校的老师,都在她展现了决心和毅力之后伸出了援助之手,最后天遂人愿,她拿到了录取通知书,但一心只愿在底层混下去的翔却提出了分手。

同样为自己精心制定了人生计划,并同样有足够的行动力将之推行下去,珠绪和可奈分别走向了不同的道路,区别就在于一个人决定靠自己,另一个人决定依赖他人。

尾声

最后一章的题目是“渐入混乱的两家”,这个说法很有意思。根据热力学第二定律,宇宙在不可逆地朝着熵增的方向发展,具体到书中的人物身上,每个人都有自己的路要走,有人依靠自己,有人依靠别人,有人决定向上走,有人决定躺平,所有人都在有意或无意地维持着某种“秩序”。《纳瓦尔宝典》中说,“人的一生就是在力所能及的范围内减少无序状态”,每个人物确实都在做着这件事,但付出之后得到的结果是否是自己想要的,大概只有角色自己知道了。

《把时间当作朋友》书评

2024-02-20 11:58:00

江湖上一直流传着这样一个说法:读李笑来的书,但不要碰他的币。

今年开年读了他在十多年前写的这本《把时间当作朋友》,深以为然。但我却没有相见恨晚,因为我知道,如果把这样一本书交给十年前的自己,大概率随便翻个几页就会想“这都什么玩意儿?浪费时间!”,然后这本书就会被束之高阁。而今天的我早已走出校园,并在社会上混了几年,给别人打过工,自己也独立创作过产品,对这本书中的观点和案例有了更深的体会,此时阅读这本书可以说是恰逢其会。

全书用很多生动的论据讲述了“时间不可被管理,我们能管理的只有自己”这个直白但对很多人不太直白的道理,毕竟谁也无法在一天里拥有第二十五个小时,一个人能做到的只有把自己的二十四个小时分配到合适的地方。整本书虽未过分强调成功学,但的确是一本不折不扣的成功学书籍,读者也确实可以从中汲取到很多有用的知识。

书中有三个观点给我的印象比较深,

  1. 开启心智。我在其他地方也看到或听到过类似的说法,比如说“提高认知”,但这些说法对我而言都比较虚,“心智”怎么开启?“认知”怎么提高?结合自己的一点人生经验,我认为就是长者教导过的“要尽可能把我们的时间,要用的来去学习。一个人学习了以后,就有一个高尚的境界,这个人啊就不会感觉空虚了”。作者在书中也给了一种具体的方法,那就是多看闲书增加知识的广度。

  2. 人脉很重要,但没有那么重要。人脉的积累不是通过主动结交什么人物来达到的,而是在自己身上有值得别人交换的价值,说回来还是要开启自己的心智,提高自己的水平,拥有了可以拿得出手交换的东西之后,然后才谈得上建立人脉,就像做生意一样,否则结交到的只能是酒肉朋友。

  3. 欲速则不达。

当然了,最重要的一条,可以读李笑来的书,但不要碰李笑来的币。

我的 2023

2024-01-01 02:43:00

谈笑风生又一年

转眼又是一年,一年前的混乱仿佛刚刚发生在昨日,上一个冬天我们告别了很多人,逝者已矣,生者如斯。

这一年的流水账

过去一年的事情比较简单,与四个季度相对应,我的 2023 也恰好可以均分成四段,只不过对应的季节却不太一样。

Q1

在上家公司搬砖,之前提到过,由于是一家规模较小的 startup ,每个人需要承担多种职责,活多人少,甚至还要加班,在这段时间里除了搬砖就是搬砖,并没能分配太多时间在自己的事业上。我感兴趣的技术方面并没有得到太大提升,因为主要职责是在伺候客户,次要职责是被分配去做了些管理工作,一个典型的变化就是天天开会,这并不是我喜欢的。总之,算是乏善可陈的一个季度。

Q2

前东家 CEO 屡屡秀出骚操作,之前由于经营不善埋下的雷在今年的经济下行中被逐个引爆,经历了接二连三的自杀式裁员之后,终于在今年一二季度之交伤筋动骨,公司瞬间进入了运转不灵的状态,我最直接的一个观察就是我所在的一个调 bug 群第二天一觉醒来只存活了我自己。据内部流言,公司的现金流已经到了一个非常严峻的地步。

接下来一段时间,几乎我所知道的所有人都在看外边的机会,同事开始陆陆续续辞职,有高调发离职邮件的,也有默默走开的,直到在系统里偶然看见他/她的状态变成了“已离职”。

我也不例外。由于担忧自己签证状态和闷声大发财的持续性,与其在可能的下一波裁员中被干掉,不如我先下手为强,赶紧找下家然后炒掉公司。所幸在之前几年的工作中攒下了一些人品,结交到了一些关系很好的同事,在他们的帮助下,我找到下家的过程在这个经济寒冬中可以说是相当顺利,更重要的是,自己的工作重心终于可以从上家的 oncall 地狱中转移到正常的软件设计实现上。

Q3

这算是比较平静的一个季度,入职新公司之后需要快速上手工作,并熟悉新公司做事的风格。

在这期间调研了一个图数据库,并根据我们的业务需要用个人 GitHub 账户贡献了一点儿代码,最后在小范围内做了一个简单的报告。

开发了一款好用的内部命令行工具,算是补完了我们整个系统的端到端测试链,并把它交给一名年轻的同事去继续开发维护。

同时在系统的某个组件上实现了一个简单的安全防护功能,功能虽简单,却需要频繁和其他小组的同事对需求,算是初步尝试了一下与新同事的合作。

入职时间不长,做的事情却不少,好在每个活都能让我有机会接触到不少新东西,组长是熟人,对我也很是照顾,总体还是很有意思的。

Q4

这是我今年最开心的一个季度,伴随着欣喜、紧张与兴奋,我们家迎来了一位新成员,初次为人父母,难免手忙脚乱,所幸伴随着我们的耐心和强烈的学习意愿,在没有任何人帮忙的情况下已经顺利度过了前两个月,小家伙一天一个样,时不时学会一些有趣的新技能,我们虽然忙碌,但无比快乐。

难得有几个月的产假,不能浪费,趁着小家伙忙着睡觉的时候,自己的项目也没有落下,为别人的事业忙碌了快一年之后,我终于有空为 Douban Book+ 升级了一下微信读书的搜索逻辑,同时开发了一款新插件——小地瓜,这款插件可以让用户在小红书网页版上一键下载帖子里的图片视频,其中图片皆为去水印的高清大图,正式发布之后迅速得到了几百名用户的一致通过。功能演示视频已传到了 B 站油管

读书

最近几年是一年比一年读书少了,到今天的统计为止,竟然只看完了 19 本,不过依然有些书我想简单推荐一下,

财务

如果说今年我在财务方面做了什么正确的决定,主要有两条:一个是跳槽,考虑到很多人离开后的工作强度以及分配给我的工作内容,继续在前司打工已经不太划算了,同时跳槽后工资又涨了一截;另一个是在第三季度清仓了沪深 300 ETF ,并把这部分资金换成了 $IXUS ,目前看来收益率还不错,分红也比较大方。

得益于今年美股的行情,我们在财务自由的路上达到了一个重大的里程碑,从此以后,它可以让我把重心放在自己的家庭和事业上,已经没必要去花费精力追求什么跳槽涨薪、升职加薪了。当然了,这个成就很大程度上多亏了今年美股的行情,还需要继续闷声大发财巩固一阵子。

一些感想

最近在网上看到一句话,

打工的目的是早点赚钱赎身不干了,而不是让你在青楼当头牌。

除非特别热爱打工这件事本身,否则大部分人应该都非常认同这个说法。打工始终是为了别人的事业付出,与自己无关,结合近两年的裁员潮,广大打工人恐怕对这句话更是深有体会,无论干了多少活,无论主持实现了什么功能,无论谈成了多少订单,只需要老板一句话,过去的一切都与自己无关了。所以每天应付完老板的事业之后,要把多余的精力放在可以积累的事情上而不是在职场上入戏太深,比如说用上班学到的知识技能开辟副业,正如我在《松弛感》读后感一文和这条微博中提到的那样,总之,要有一些别人无法轻易拿走的东西。

对于大部分普通人来说,入行初期,给人打工可以说是性价比最高的一件事,因为不需要操心太多其他方面就可以立刻用自己的技能换取收入;但对于上班上了好多年的人来说,平时只是一味地卷职场是没有意义的,并且伴随着巨大的风险。因为总会升到那么一个级别让自己感到力不从心,总有些不愿意但不得不做的事情在不断消磨人的精气神,考虑到这个时候大部分人上有老下有小,如果这个时候还没有自己的第二赛道,自己的压力就相当大了,毕竟随着时间推移,一个是 35 岁“大限”总有一天会来,另一个是裁员的风险会越积累越多,收入来源完全依靠别人终究是不靠谱的。

未来

具体的 flag 就不立了,简单来说,希望自己能以更加松弛的态度面对生活中的风风雨雨,并随性而为地学习了解一些新东西,开阔眼界,如果能学以致用那就更好了。

最后祝大家新年快乐!