4

Tue 23 September 2025

远程公钥

我们已经获得了一个密钥对。根据前面介绍的原理,我们还需要将该公钥上传到待访问的VPS里。下面我们以root用户为例演示公钥的添加。

首先,按常规的方法登陆VPS,并切换至root用户,进入root的.ssh文件夹:

cd ~/.ssh

如果没有这个文件夹,可创建之后再进入:

mkdir -p ~/.ssh && cd ~/.ssh

然后,用Vim编辑器等方法打开authorized_keys文件(如果没有该文件就创建一个),加入你刚刚生成的公钥,比如:

ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkIwosM2U9LGhC8CmvzU60iaab30ld+hKrb+DIZ23RaetSJZeHjs+WEQtTa/7CiOlGQhfhSv3PHsSb7owfKJKawbcFHOtivCogHvigFG3mbfgO6J9uTjyV44/fqRDfi1JsAEP35q57ZbjI9lFCw4D/xFRRPTCKI6bXIFBwKFWNDOc29RdR2rWcpmyTaldCU63ICa2YZCA2DwnoIeVwLJp7CNtF2Bm7zweoMEwNL7lyT02rB1hUVBpr+vEfSWmnJMvAmt/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahv+/prVZuipFgC1aQnQ== 测试用密钥

你也可以添加备注,备注符是#。比如:

这是一个测试用的公钥。巴拉巴拉。

ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkIwosM2U9LGhC8CmvzU60iaab30ld+hKrb+DIZ23RaetSJZeHjs+WEQtTa/7CiOlGQhfhSv3PHsSb7owfKJKawbcFHOtivCogHvigFG3mbfgO6J9uTjyV44/fqRDfi1JsAEP35q57ZbjI9lFCw4D/xFRRPTCKI6bXIFBwKFWNDOc29RdR2rWcpmyTaldCU63ICa2YZCA2DwnoIeVwLJp7CNtF2Bm7zweoMEwNL7lyT02rB1hUVBpr+vEfSWmnJMvAmt/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahv+/prVZuipFgC1aQnQ== 测试用密钥

保存authorized_keys文件的更动。

接着,我们要重新设置一下root登陆的模式:

sudo vim /etc/ssh/sshd_config

PermitRootLogin有yes, no和without-password三种模式。我们将PermitRootLogin的参数改为without-password——表明只可以用密钥而不能用密码来登陆该VPS的root用户。

NVIDIA_Share_BRpEZrZ8He

此外,还要确保PubkeyAuthentication的参数为yes:

msedge_ZgWOnMOcRT

由于我们已经使用密钥登陆,更建议直接禁用口令登陆,可设置PasswordAuthentication参数:

PasswordAuthentication no # 禁止口令登陆。但某些系统还是要设为yes才可以正常工作。

小结一下,要做到最佳保护效果,SSH设置相关参数和值为:

PermitRootLogin without-password # root用户仅适用 PubkeyAuthentication yes # 允许公钥验证 PasswordAuthentication no # 禁止口令登陆。但某些系统还是要设为yes才可以正常工作。

保存好配置后,重启sshd服务生效:

sudo service sshd restart

此时不要关闭该Shell所在窗口!此时不要关闭该Shell所在窗口!此时不要关闭该Shell所在窗口! 密钥登陆

下面我们改动一下登陆设置。在此之前,Mobaxterm登陆时在设置窗口是不需要输入密码的,只需要在登陆的shell窗口输入密码;这种做法可以保证密码的安全性。某些Shell工具可能是明文密码,这时你可以不需要密码。然后找到可以使用密钥的地方(以MobaXterm为例):

NVIDIA_Share_aXDyflbcvc

在此基础上,打开一个新的窗口登陆。如果成功登陆,表明一切顺利,旧窗口也可以安心关闭了!登陆过程如下图所示,开头会出现Authentificating with public key XXX的字样,这表明该SSH登陆过程依赖前面添加的公钥:


科研工作人员都有很强的文献阅读的需求。及时了解他人的工作对于开拓研究视野、研究思路是很有帮助的。对于刚刚进入科研领域的新人小白,为了快速了解行业的基础和进展,除了阅读本专业的经典书籍、经典论文,追踪领域内的新进展更是必不可少的。对于“如何快速提升个人研究水平”这类问题,绝大多数科研前辈的建议都会囊括“多读文献”这一条;但是具体请教到某位老师的时候,大家的策略和方法都不尽相同,颇有“八仙过海,各显神通”之感。我也不止一次地扪心自问:对于研究者而言,应该如何高效地进行文献阅读呢?由于某些契机,最近我找到了一些相当不错的策略和切实可行的方法。本文的内容与我其它博文会有不少重叠之处,这里主要是重新整理思路,让这个问题的答案更加清晰。

事情要从2022年4月份说起。当时由于某些诱因,我想搞一个WordPress博客记录生活和学习,所以就买一个VPS捣鼓了一下,在这个过程中积累了一些管理Linux/Docker和网站运营的经验。某天我偶然发现自己的博客有一个feed链接,好奇心驱使之下了解一波,就发现了RSS这个追更神器。一般情况下,小伙伴们会利用RSS追更一些奇奇怪怪的东西(比如动漫、番剧、电影、豆瓣小组评论);不过,我意识到RSS在文献阅读方面有得天独厚的优势。很快啊,我又双叕发现PubMed(生物医学研究的必备数据库)支持通过检索式产生个性化RSS链接,到这里技术路线基本已经明确。我制定了一些方案,测试了两个月左右,感觉帮助很大,所以推荐给大家(主要是科研小白,大佬估计都知道)!

一个好的文献阅读方案应该有以下特点:

被动式推送:基于RSS或者类似RSS(比如邮件服务)的推送式阅读。如此则不需要花费许多精力在寻找目标文献上。

方便检索与归档:当文献积累量较大时很重要。比如,如果你要找某一篇自己很久以前看过的文献,只要它曾经被记录,就可以很方便地用搜索引擎(基于Web)找出来。
社区互动:看文献的笔记是可公开性的,可与他人交流。当然也可以选择私密。

只要你的流程遵守这3个基本原则,那么文献阅读促进你成长的过程就是稳定的、长期的、可拓展性的、螺旋上升式的。下面提到的方法也是基于这3个原则的,主要是起抛砖引玉的作用;你要折腾和尝试一下,才能最终找到最适合自己的方案。


Markdown

如果你是搞计算机科学或者编程,这一小节都可以不用看了,懂的都懂。对于其它专业的人而言还是有必要介绍一下。关于Markdown,我在《Linux基础 学习Linux前要准备什么》、《Typora 地表最强Markdown编辑器之一》这两篇博文均有较详细的介绍。简而言之,Markdown是一个可以在半小时内入门的编程语言,有众多免费/付费的编辑器可供选择。Markdown最重要的特点就是专注内容而不是格式,所以它在程序员群体中被广泛使用(代码块的可视化往往依赖较为复杂的高亮规则),也被Github等网站采用为技术文档的主要文本格式。专注内容而非格式的特点天然地适合于记录笔记,因为维护文献阅读笔记版式的时间性价比很高,作者可以将大部分精力放在内容创作上,所以Markdown的笔记往往结构严谨,可读性强。我博客里所有的文章都是基于Markdown的(详见《WordPress上传或更新Markdown的最佳实践》),排版错落有致,规整简洁。

文献阅读笔记的具体案例可以在《学习地图》中看我的“前沿快讯/文献精读”系列。比如在《前沿快讯 第1期 睡眠与肿瘤转移等》,我会简单记录某条文献的标题(包含链接)、杂志、中文摘要(在机器翻译的基础上微调),并粗略地进行分类。这样就描述了该文献的基本特征,日后复习、溯源都很方便:

chrome_YOf4iDQHOe

看文献时怎么记笔记其实没有什么硬性规定。因为我给自己看的同时也给其它人看的(毕竟是发表在博客上),所以记录得详细些。一般来说,看普通的文献可以简单点,比如写一个描述性标题、一个全文标题、一点评论足矣;对于一些比较重要的文献你可以记多点。看着舒服、方便回顾即可,随便写点什么。对于很重要的文献,你还可以做一些类似于“文献精读”的练习(比如《文献精读 第2期 靶向cGAS-STING-IL6治疗CIN肿瘤》这种),虽然费时费劲,但收获也大。对于那些结构比较复杂的论文,这种笔记也方便反复咀嚼。更重要的是,博客上的笔记其它人可以看到,热心的观众会帮你纠正错误的(前提是有人看,哈哈)。

复习文献的时候可以快速浏览,效果还是不错的:

chrome_b2i5fM1Maz

检索也很方便,在浏览器中直接Ctrl + f调用浏览器的搜索框,检索速度贼快。普通的Markdown编辑器也有类似的功能的:

chrome_4kF4TbiWqJ

总之,Markdown是一种很适合记笔记的编程语言,文献阅读笔记也不例外。我目前所有的笔记都是用Markdown记录的。Markdown用得足够久,自然会体会到它的好处!值得一提的是,有很多出名的知识管理系统(比如Notion),写作体验和Markdown是很相似的,或许是部分地借鉴了它的思想。所以,学会Markdown对迁移到其它主流笔记平台也会毫无压力。我认为,Markdown可以作为一个基础工具外在地体现你的知识和经验。 基础知识框架

基础知识框架是非常重要的。因为我们讨论问题时,都是在某些既定事实和范式内进行讨论。这种既定事实和范式根据你所在的领域不同而不同。不过基本方法是相似的,就是读经典,阅读经典教科书、经典文献或经典项目的代码。每个领域都会有大牛,肯定有写教科书的人。问一下导师、师兄/姐、同事,结合Google知乎百度,整理出一份高质量的书单,将它们的PDF版都搞到手,有空就翻读,甚至是系统性地阅读。时间一久,自然就可以构建出该领域的基础知识框架了。基础知识框架还是很重要的,这样你看到该领域内的一个全新的研究时,就可以大致知道它的研究框架并判断它的重要程度,而不是一脸懵。多读多看。记笔记可以用Markdown。

不接触编程的小伙伴可能都不知道VPS是啥。VPS, 全称virtual private server,虚拟专用服务器。一般来说,VPS是搭载着Linux系统的、可远程控制的电脑,一般通过ssh后台进行管理:

MobaXterm1_CHS_EsCF3VtI0f

你可以认为VPS是一种通用工具,可以用来搭建视频平台、笔记平台、画板和其它奇奇怪怪的网络应用(基于Docker)。在文献阅读里,VPS的作用就是搭建一系列文献阅读的辅助工具,比如图床、RSS阅读器、个人博客、资料备份平台等。你可以将VPS看作一部手机,可以装各种功能的APP。一般来说,VPS是一种付费资源(甲骨文大盘鸡按下不表),资费水平可以参考我用的VPS:

3x vCPU Core 45 GB SSD Cached RAID-10 Storage 3 GB RAM 6500GB Monthly Premium Bandwidth 1Gbps Public Network Port Full Root Admin Access 1 Dedicated IPv4 Address KVM / SolusVM Control Panel - Reboot, Reinstall, Manage rDNS, & much more LOCATION: Los Angeles JUST $24.28/YEAR or $47.06/2 YEARS

据我所知,我用的VPS在大品牌里算是性价比很高的了。有了VPS和一个域名,你可以搭建下面这些应用:

图床:以Chevereto图床为例。用于承载Markdown的图片。

chrome_6OnArAsLsA

个人博客:以WordPress个人博客为例。用于公开和管理笔记。

chrome_3Ui00Emh4i

RSS阅读器:以Tiny Tiny RSS阅读器为例。用于接收RSS订阅(甚至还可以使用过滤器和预定义标签)。

chrome_rAp33v0M7g

资料备份平台:以Duplicati为例。用于备份图床、博客和RSS阅读器。

chrome_5zmRr67fRK

上面所提到的应用我都在博客的《学习地图》里介绍过安装和使用之法,可按需食用。值得注意的是,这只是我自己的方案,你应该根据自身的实际情况进行配置。比如,RSS阅读器就有各种软件可选;博客对于某些人而言可能也是不需要的;图床可以白嫖Github;资料备份可能每个人都有自己的方案。我之所以将这些东西都放在VPS里,主要原因是:

不贵。看你买什么VPS吧。能嫖到甲骨文都不用钱,有个域名就能玩。不过稳定的话还是要自己搞一个,比如像我用的这个。硬盘容量最好在20G以上,内存最好在1G以上。而且还能玩别的东西,这只是冰山一角:

chrome_qIwzNP6lJX

我的数据我作主。只要我还给VPS和域名续费,整个平台就不会停运,近乎终身,自然不存在数据丢失的问题;所用的软件都是开源免费的软件,你完全可以看心情更新迭代,完全不用担心兼容问题。你如果用的是某公司的产品,用的时候挺爽的;如果以后它停运了怎么办(类似新浪博客那种)?迁移海量数据的过程,想想都头皮发麻。更恶心的是,你可能完全无法要回自己的数据(360云盘停运时的痛相信不少人还有印象)。
安装过程不难,有点Linux和Docker的基础即可。可能小白听到Linux有点恐慌。我以一个非编程专业出身的过来人的角度告诉你,普通素质完全可以hold住简单的Linux/Docker基础。据我所知,很多初中生、高中生玩编程都玩得很溜。需要看文献的同学一般都是研究生了,完全不用担心智商不够用,关键是要用心学,很快就可以入门。精通就是另外一回事了,这个往往是困难的。
拓展性和成长性:一入VPS深似海。有很多有趣的东西等着你去发现,这其中的价值远不局限于文献阅读带来的好处。比如,掌握一些解决不熟悉问题的能力。自己体会。

差不多就是这些。总之你知道VPS是个好东西就足够了!如果你真的想要玩一玩VPS,推荐还是看看我的“Linux基础”教程。它不是专业教程,偏小白向;但对于一般玩家而言已经够用了。所有的文章都通俗易懂,可操作性强:

chrome_797aIyzTII

还要什么自行车啊?! easySCI

easySCI是我最近做的一个小项目,不是谦虚,是真的小!有用就行,哈哈!每年1更,主要是通过一些筛选指标获得目标期刊,然后根据影响因子(你懂的)分层,得到ISSN号。ISSN可生成PubMed使用的检索式;然后结合某个领域的关键词,就可以生成我们关注领域的RSS链接。项目的具体情况可参考Github Repo, 国内小伙伴也可以看这里:Gitee Repo。有类似功能的工具有很多,比如ScholarScope等,大家也可以了解下。在easySCI中,我主要是将期刊的PubMed检索式和期刊-关键词的RSS链接搞定,省得每个人都要走一次重复流程。结合Tiny Tiny RSS阅读器,大致的效果是这样的:

chrome_rAp33v0M7g

从GIF图中可以看到,每个链接都是动态更新的;链接背后有新内容,马上会推送过来。RSS的这个特征非常适合定期地、有仪式感地阅读文献。有空就1天1看,没空就每周1看,永远不怕看漏;心情不好不想看文献的话,直接Mask all read!当然不建议经常这样做,哈哈!另外,采用类似的策略,你可以定制任何你想看的某几本期刊(比如CNS和你专业里的顶刊)、或者某几个大牛的文章。总之,RSS用来追踪最新文献就是爽,在我心目中应该是排名第一的方法了!

目前暂时只支持生物医学领域。其它领域也是类似的,有需要的同学可以提issue,有空的话我可以帮忙制作一下RSS链接,也不麻烦。至于某些专业主要是投会议或者是投国内核心的,这个我没啥特别的经验,还是请教你们的导师和师兄师姐吧。另外,估计他们对于追踪文献也有一些妙招,不妨向他们请教;再Google知乎百度一下,最后总可以找到一套适合自己的方法。祝你顺利!


我们平时多多少少都会逛抖音、B站、知乎、头条、公众号、微博等。你有没有一种感觉,我们的信息来源似乎太过繁杂,一会看这个App,一会看那个App?你有没有感觉好像刷了好久的手机,但看完后总觉得心里空空的,好像也没有学到什么东西?如果你还是一名正儿八经的研究者,除了关注CNS,可能还会关注数个自己领域的重要杂志/会议的更新信息,你会不会苦恼于这些研究信息过于分散,而且追踪起来也不太方便?你会不会通过邮件订阅一些Pubmed或者Google Scholar的Search Term以了解某个领域最近的研究进展呢?生活中,你可能有一个很喜欢的创作者,但是总是在不同的平台看到他帐号上的重复内容,你会不会觉得有点厌烦呢?你会不会觉得各种手机App的广告很烦?你会不会觉得,自己在平台的智能推荐算法的洗涤下,越来越觉得自己有种强行“被理解”的感觉?

如果你觉得上面的烦恼不太明显,你应该不太需要RSS,所以接下来的几章都不需要看了!如果你觉得以上的烦恼比较强烈的话,那么RSS很可能就是你的菜。

我引用Wiki机翻的几段话:

RSSRDF Site Summary或Really Simple Syndication 是一种网络feed它允许用户和应用程序以标准化的计算机可读格式访问网站的更新订阅 RSS 提要可以让用户在一个新闻聚合器中跟踪许多不同的网站该聚合器会不断监控网站的新内容而无需用户手动检查它们

网站通常使用 RSS 提要发布经常更新的信息例如博客条目新闻标题音频和视频系列剧集或用于分发播客RSS 文档称为提要”、“网络提要”、频道”)包括完整或摘要文本以及元数据例如发布日期和作者姓名RSS 格式是使用通用XML文件指定的

尽管 RSS 格式早在 1999  3 月就已经发展是在 2005 年到 2006 年间 RSS 得到广泛使用并且 ( Feed-icon.svg) 图标由几个主要的 Web 浏览器决定RSS 提要数据使用称为新闻聚合器的软件呈现给用户内容的传递称为网络联合用户可以通过在阅读器中输入提要的URI或单击浏览器的提要图标来订阅提要RSS 阅读器定期检查用户的提要以获取新信息如果启用了该功能则可以自动下载

所以,RSS是PC时代的产物,算是一个很古老的信息获取方式(当然没有纸质书籍和纸质杂志那么古老)!这里,我展示一下自己目前正在使用的自建Tiny Tiny RSS(一款开源免费的RSS阅读器,可用docker搭建)的界面(展示平台为Win10的Chrome浏览器):

image-20220430200003802.png

在上图,我自己确立了若干分类,比如新闻、科技等等。每个分类有若干RSS源,比如36氪资讯、知乎每日精选等。上面有一些特殊区域,就如其文字描述,相信你可以猜出它们的功能。市面上流行的其它电脑端RSS阅读器应该都是类似的。

这里也给一下Unread(一款支持Fever协议的免费IOS端RSS阅读器,App Store国服可下载)的工作界面:

image-20220429161210035.png

看文章时效果大致是这样的:

image-20220430200826998

可能乍一看会觉得没什么特别;不过如果你足够细心的话,还是会感觉RSS和我们平时所用的阅读平台是不太一样的。

值得一提的是,在Github上有一个Repo: ALL-about-RSS,目前有1.7k左右的Star,其记录了各种与RSS有关的知识和实用技巧。有兴趣的小盆友可以去进一步挖掘! RSS和常用的阅读平台有什么不同?

如果是看文字的话,知乎、头条、公众号、微博这些App(下面统称App)咱们还是很常用的。

我觉得RSS和App之间的功能差异有以下几点: 评论功能

RSS的评论功能是比较弱的。读者和创作者的互动交流比较差。
App的评论系统通常是比较完善的。读者和创作者的互动交流比较好。如果你喜欢B站的话,也会被它的弹幕功能所吸引。

内容传递

RSS:内容创作者——观众
App:内容创作者——App——观众

聚合程度

RSS:全源聚合。只要创作者提供rss源(技术上是很容易的,只不过创作者不一定愿意)。
App:一般只能展示自己平台的内容。如果你要展示别的平台,通常是转发或者是二次创作的内容

便捷程度

RSS:一个App(一个RSS阅读器)。订阅内容有限,每天查看1次就足够了。
App:通常是2个或以上,没有上限。可以经常刷,内容看不完。种类繁多。下面列举常见的应用:
文字内容创作平台:知乎、今日头条、百度贴吧、豆瓣、微博、小红书、公众号、CSDN、简书等
音频内容创作平台:喜马拉雅、荔枝FM、蜻蜓FM等
电商平台:闲鱼、淘宝、拼多多等

观看体验

RSS:有些内容(比如大部分个人博客)是可以直接在RSS阅读器上看;有些内容(比如少数派)要跳转到原始页面。有些内容,在RSS阅读器里看效果也不好。视频一般都要跳到新页面查看。
App:全程在App里观看,观看体验好。

过滤功能

RSS:支持高级的筛选功能。
App:App推送你看什么就看什么。似乎没有自定义过滤功能。

广告

RSS:无广告且免费。
App:有些内容是免费;经常有广告。

RSS和常用平台孰好孰坏?

如果你看到到前面的差异性分析,你会发现,这个问题的答案对不同的人来说是不同的。 平台方

我可以提供远比RSS要好的阅读体验。我可以创造一个良好的社区氛围,帮助创作者和观众更好地找到对方。更重要的是,如果创作者和观众用RSS,我存在的意义是什么? 创作者

我可以创作出好的作品。如果我是一个默默无闻的创作者,我怎么将自己的内容推广给观众呢?如果我是一个有名的创作者,那么RSS可以将我生产的内容直接传送给粉丝;但谁又来为我的内容付费呢?而且,我希望可以和粉丝随时沟通,获得反馈,以便给我未来的创作提供改进建议。如果我使用RSS,那么谁来给我反馈呢?如果我想和同行交流,又应该如何? 观众

观众1:我知道自己想要看什么。我已经知道有一些优秀的创作者,不需要别人的推荐了。我也有一些其它的方法,比如朋友推荐等,了解到哪些创作者是最优秀的。而且,我的需求很简单,只要知道他们最近有什么新内容即可。如果我想详细地看,会进入原信息源进行查看。我虽然看很多文章,但很少留言或评论,我觉得也没关系。我不喜欢广告。

观众2:我不太知道自己想要看什么。最好有人推荐一下,告诉我要怎么做。我也不喜欢广告,不过没办法只能看一下,或者买个会员。

观众3:我有一部分观众1和观众2的需求。 个人观点

平台在商业上显然是更成功。如今,除了少数极客,鲜有人知道RSS。App的策略更多时候是有效的,确实很多人不知道自己想要看什么,或者每个人都会有一个阶段,不知道自己想要什么,也不知道从哪里找到优质资源。而且依托于资本的力量,平台这个中间商可以营造好的社区氛围,将流量的游戏玩到极致。作为中间商,平台完成了信息供需桥梁的搭建,提高了信息整合的效率。

也许有人说,RSS的应用都没有平日里的App好用。但其实这不是本质原因。如果你玩过个人博客,就知道个人博客的创作体验和环境绝对不逊色于各大文字创作平台。用户平时能感觉到的体验,多数不是平台的核心竞争力。创作者和读者均很难通过RSS的模式获利才是更深层的原因。如果不依托平台,初创者很难将自己推广出去;由于缺乏社区支持和良性竞争环境,也很难进一步成长。目前的形势,中心化更有利于创作者发展。像RSS这种去中心化的方式,除非有特别的技术赋能,能够跑通商业模式,否则是很难有出头之日的。近期有一个web3.0的概念,和RSS理念十分地相似。也许RSS是有前途的,只是在今天还未显示出它的威力!对于个人而言的话,你觉得RSS好用,那RSS对你来说就是有价值的。你可以尝试一下RSS。如果不喜欢,不用就行了,也没啥大不了!

对我个人而言,当我了解RSS后,我马上搭建了自己的Tiny Tiny RSS和RSShub,体验还不错!我平时比较喜欢阅读文字,比如某些博客或者是某些比较喜欢的up主。偶尔评论一下,属于典型的“沉默的大多数”。而且作为一个成熟的社会人,也大概知道自己喜欢哪些号或者个人博客了,一般是某个领域头部那几个;再多我也看不过来。而且自己平时也有看文献的需求,RSS的信息聚合特点对研究者也是蛮友好的。另外,RSS的无广告和清爽的体验都给我带来更多的阅读的宁静,少了几分喧嚣。如果自建的Tiny Tiny RSS和RSShub稳定的话,估计我会长期地使用!

我觉得,RSS对于长内容(包括文字和视频)的支持应该都可以很好。不过对于短内容(特别是短视频)体验肯定是不如抖音这样的App的。另外,我也不会抛弃今日头条等手机App的使用。毕竟,RSS源不是万能的,RSS的生态建设也不太好。目前内容平台的创作都不轻易提供RSS源。你可以去头条看一下,基本上是没有RSS源的。平台的封闭性也是无可奈何之事,毕竟人家就是靠这个恰饭的!而且,我们也需要内容平台保持一些可能性。因为确实存在一些你可能很感兴趣,但却从未遇见过的内容;而内容平台则增加了这种邂逅的概率! 小结

RSS的相关介绍就到这里吧!如果你知道自己想看什么,并且有定期阅读的习惯,RSS就是一种不错的选择!没有各种信息轰炸,也不会错过重大消息。


oj


菜单
主要内容

书签 设置 文档

sive.rs
16 十二月 2022

sive.rs
阅读约 5 分钟

标签

书签栏

高亮

在文章中选择文本以创建新的高亮。 链接

Benefits of a daily diary and topic journals | Derek Sivers 2019-01-28

You know those people whose lives are transformed by meditation or yoga or something like that?

For me, it’s writing in my diary and journals. It’s made all the difference in the world for my learning, reflecting, and peace of mind.

After 20+ years of doing this, here’s what I do and recommend: A daily diary

If digital, use only plain text. It’s a standard format not owned by any company. It will be readable in 50 years on devices we haven’t even imagined yet. Don’t use formats that can only be read by one program, because that program won’t be around in 50 years. Don’t use the cloud, unless you’re also going to download it weekly and back it up in plain text outside that cloud. (Companies shut down. Clouds disappear. Think long-term.)

Every day at some point, just open up this diary, write today’s date, then start writing. Write what you did today, and how you are feeling, even if it seems boring.

It works best as a nightly routine. Just take a few minutes and write at least a few sentences. If you have time, write down everything on your mind. Clear it all out. But if you miss a night, make time the next morning to write about the previous day.

This is important because years from now you might be looking back, wondering if you were as happy or as sad as you remember during this time. So don’t only write the drama or dilemmas. Include the daily facts of life.

We so often make big decisions in life based on predictions of how we think we’ll feel in the future, or what we’ll want. Your past self is your best indicator of how you actually felt in similar situations. So it helps to have an accurate picture of your past.

You can’t trust distant memories, but you can trust your daily diary. It’s the best indicator to your future self (and maybe descendants) of what was really going on in your life at this time.

If you’re feeling you don’t have the time or it’s not interesting enough, remember: You’re doing this for your future self. Future you will want to look back at this time in your life, and find out what you were actually doing, day-to-day, and how you really felt back then. It will help you make better decisions.

Just put aside a few minutes to write what you did and how you felt today. “Thoughts On” journals

There are certain subjects in your life you think about a lot. People, places, hobbies, health, plans, finances.

For each subject that you might have ongoing thoughts about, start a separate “Thoughts On” journal. Whenever you have some thoughts on this subject, open up that file, write today’s date, then start writing.

To give you an example, here are my “Thoughts On” journals as of today:

Accounting
Addiction
Airports
Alcohol
Ambition
Animation
AppDevelopment
Artist
Assistant
BeingSocial
Blog
BookNotes
BookPublishing
Calendar
Carla
Changes
Chess
Chinese
Cities
Comedy
Comparing
Computers
Conferences
Countries
Dance
Dog
Entrepreneur
Erika
Europe
EventStrategy
Expat
ExploreExpand
Fame
Fan
Friends
Goals
Gratification
Habits
Home
IdealMe
Image
Infrastructure
Intense
Interviews
Investing
Ireland
Kissing
LanguageLearning
LateralThinking
LifeAlgorithms
MakeIdeas
Manners
MobileAppDev
Money
Music
MyPeople
Need
NewZealand
Noise
OS
Oxford
Parenting
Phone
PhoneNumbers
Places
Plan
Poor
Portugal
Potential
Programming
PublicMe
RV
Regrets
Relationship
Renting
Romance
Routine
Secrets
Sex
Singapore
Skills2Learn
Social
Solitude
Songwriting
Speaking
Strength
Surprise
Teachers
Tech
ThinkingSelf
Thrifty
Travel
VirtualReality
Vision
Website
WorthDoing
Writer

I find it so useful to keep my thoughts on each subject together, because I can see my past thoughts and current thoughts in one place. I can see how my thoughts on this subject have evolved or keep repeating. Sometimes I think I have a new thought on a subject, so I open up the file and write it down, then afterwards I see I had that same thought a year ago and had forgotten about it. If you care about your thoughts, keep them.

They can be tiny. Like you see I have one on airports. I don’t have many thoughts on airports. I don’t fly that much. But I found that once or twice a year, when waiting at the gate, I had thoughts on the subject, so I’d open that file and start writing.

I’ve considered making one for every musician, album, book, or film that’s had an impact on me, that I’d like to think more about. Why not? I admire the way that a good critic puts in hours of reflection on each piece, thinking deeper about something they’ve just taken in.

I especially like my “Regrets” journal. Whenever I do something I regret, I write it down there, noting why I regret it, what I wish I would have done instead, and how I hope to prevent this in the future. Ask yourself questions, then question your answers.

Whether in your daily diary, or the “Thoughts On” journals, I find the single most useful thing has been using it as a place to ask myself questions, and answer them.

If I’m planning on doing something, I ask myself what I hope to get out of it, why, and whether there are other ways to get what I want.

When I’m feeling conflicted, especially, I’ll ask myself a bunch of questions to work through my feelings, looking for the source of the conflict, then ask myself more questions around the clash in values, and work through other alternate ways I’d like things to be.

I answer with my initial thought first, but then question it afterwards with skepticism, and consider different perspectives. I hear this is similar to cognitive behavioral therapy — and I’ve been meaning to learn more about that. But whatever you call it, I think it’s been the single most important thing to my intellectual and emotional development.

Almost all the thoughts I have on any subject are the result of writing in my diary and journals, then questioning myself and working through alternate ways of thinking about it, and finally returning to the subject days or months later with a clear head and updated thoughts, seeing how they’ve changed or not over time.

I hope it helps you too.


菜单
主要内容

书签 设置 文档

GitHub
15 十二月 2022

作者 KasperZutterman
github.com
阅读约 3 分钟

标签

25-03-02-chromium

高亮

在文章中选择文本以创建新的高亮。 链接

KasperZutterman/Second-Brain: A curated list of awesome Public Zettelkastens 🗄️ / Second Brains 🧠 / Digital Gardens 🌱

A curated list of awesome Public Zettelkastens 🗄️ / Second Brains 🧠 / Digital Gardens 🌱 - KasperZutterman/Second-Brain Aaron Parecki https://aaronparecki.com/ https://twitter.com/aaronpk Aengus McMillin https://aengusmcmillin.com/brain http://twitter.com/aengusmcmillin Alan Pestronk https://neuromuscular.wustl.edu/alfindex.htm Alex Bilson https://alexbilson.dev/ Anand Sharma http://aprilzero.com/ https://twitter.com/aprilzero Andres Estrella https://andresestrella.vercel.app/ https://twitter.com/andrestrellaa Andrew Tang https://tangdrew.com/notes https://twitter.com/tangdrew/media Andy Byers https://notes.ajb.app/ https://twitter.com/AndyByers21 Andy Matuschak https://notes.andymatuschak.org/ https://twitter.com/andy_matuschak Anne-Laure Le Cunff https://www.mentalnodes.com/ https://twitter.com/anthilemoon Anshul Khare https://anshulkhare.in/ https://twitter.com/anshulkhare Anthony Agbay https://walkintheforest.com/ https://twitter.com/anthony_agbay Anthony Amar https://garden.anthonyamar.fr/ Anton Emelyanov https://wiki.ainzzorl.lol/ Aquiles Carattino https://notes.aquiles.me https://twitter.com/aquicarattino Aravind Balla https://notes.aravindballa.com/ https://twitter.com/aravindballa Arne Bahlo https://notes.arne.me https://twitter.com/arnebahlo Azer Koçulu https://github.com/azer/notebook Azlen Elza https://notes.azlen.me/g3tibyfv/ https://twitter.com/azlenelza Ben Robertson https://benrobertson.io/notes https://twitter.com/benrobertsonio Benjamin Lannon https://lannonbr.com/garden https://twitter.com/lannonbr Bennett Hardwick https://bennetthardwick.com/wiki/ Bhavani Ravi https://www.bhavaniravi.com/ https://x.com/bhavaniravi Bhupesh Varshney https://bhupesh.gitbook.io/notes/ https://twitter.com/bhupeshimself Bill Seitz http://webseitz.fluxent.com/wiki/ Binny V A https://notes.binnyva.com/ https://twitter.com/binnyva Brendan Schlagel http://brendanschlagel.com/canon/ https://twitter.com/schlagetown Bruno Henriques https://bphenriques.github.io/knowledge-base/ Bryan Jenks https://publish.obsidian.md/bryan-jenks/INDEX https://twitter.com/tallguyjenks Buster Benson https://busterbenson.com/piles/ https://twitter.com/buster Calvin Bui https://calvin.readthedocs.io/en/latest/ Ceasar Bautista http://memex.cc/ https://twitter.com/Ceasar_Bautista Charlie Trochlil https://garden.charlietrochlil.com/ https://twitter.com/rhymeswithvocal Chase McCoy https://chasem.co/notes/ https://twitter.com/chase_mccoy Chinarut Ruangchotvit http://autobiography.chinarut.com https://twitter.com/chinarut Chris Aldrich https://tw.boffosocko.com/ https://twitter.com/ChrisAldrich Chris Biscardi https://www.christopherbiscardi.com/garden https://twitter.com/chrisbiscardi Clinton Boys https://www.mtsolitary.com/ https://twitter.com/psephologue Cosma Shalizi http://bactra.org/notebooks/ https://twitter.com/cshalizi D.S. Chapman https://www.dschapman.com/notes https://twitter.com/ds_chapman Daniel Mathiot https://notes.danielmathiot.tech Danny Yee http://danny.oz.au/ https://twitter.com/DannyYee Dave Gifford https://giffmex.org/Welcome.html https://twitter.com/giffmex Derek Sivers https://sivers.org/book https://twitter.com/sivers Devine Lu Linvega https://wiki.xxiivv.com DongDong Chen https://garden.oldwinter.top/ E. Ferreira https://github.com/slowernews/notebook Edwin Wenink https://www.edwinwenink.xyz/etc/blogroll/ & https://www.edwinwenink.xyz/zettelkasten/ Elias Granja https://wiki.anarchist.work/ Fabien Benetou https://fabien.benetou.fr/ https://twitter.com/utopiah Foreverliketh.is https://foreverliketh.is/ https://indieweb.social/@accordionpolar Fran Salerno https://frandsalerno.github.io/ Gadzhi Kharkharov https://kkga.me/tags/note https://twitter.com/kkga_ Gordon Brander http://gordonbrander.com/pattern/ https://twitter.com/gordonbrander Greaby https://greaby.github.io/galaxie-gd/ https://twitter.com/greaby_ Guilherme Carreiro https://karreiro.com/wiki/ https://twitter.com/karreiro_ Guía Carmona https://roamresearch.com/#/app/digitaljungle/page/3-pt2rrKj https://twitter.com/guiacarmona1 Gwern Branwen https://www.gwern.net/ https://twitter.com/gwern Hamel Husain https://notes.hamel.dev/ https://twitter.com/HamelHusain Harish Chandra Thuwal https://github.com/hthuwal/notes Henrique Dias https://hacdias.com/notes https://twitter.com/hacdias Hiran Venugopalan https://hiran.in/notes https://twitter.com/hfactor Hrishikesh Bhaskaran https://wiki.stultus.in https://twitter.com/_stultus Ian Jones https://garden.ianjones.us/ https://twitter.com/_jonesian Jacky Zhao https://jzhao.xyz/ https://twitter.com/_jzhao Jacob Chvatal https://wiki.jacob.chvatal.com/ https://twitter.com/jakechvatal James Kolean https://jameskolean.tech/ James Long https://jlongster.com/ https://twitter.com/jlongster Jamis Charles https://jamischarles.com/ https://twitter.com/jamischarles Jani Eväkallio https://jevakallio.github.io/notes/ https://twitter.com/jevakallio Jeremy Keith https://adactio.com/ https://twitter.com/adactio Jeremy Nixon https://jeremynixon.github.io/ Jerry Michalski https://www.jerrysbrain.com/ https://twitter.com/jerrymichalski Jestem Króliczkiem https://beepb00p.xyz/exobrain/ https://twitter.com/karlicoss Jethro Kuan https://braindump.jethro.dev/ https://twitter.com/jethroksy Jimmy Briggs https://publish.obsidian.md/kaas-published https://twitter.com/jimbrig2011 Joel Chan https://joelchan.roam.garden/ https://twitter.com/JoelChan86 Joel Hooks https://joelhooks.com/ https://twitter.com/jhooks Johackim https://johackim.com/ https://twitter.com/_johackim John Otander https://johno.com/notes/ https://twitter.com/4lpine John Walker http://fourmilab.ch/ https://twitter.com/fourmilab Jon Sterling https://www.jonmsterling.com/ Joschua https://joschuasgarden.com/ https://twitter.com/selfire1 Josh Buchea https://joshbuchea.com/notes https://twitter.com/joshbuchea/ Kahlil Corazo https://scrapbox.io/kahlil/ https://twitter.com/kcorazo Kevin Cunningham https://garden.kevincunningham.co.uk https://www.twitter.com/dolearning Kirill Gorbachyonok https://github.com/japanese-goblinn/knowledge-base https://twitter.com/japanese_goblin Lauro Silva https://laurosilva.com/welcome-to-my-digital-garden/ https://twitter.com/laurosilvacom Leon Bambrick https://wiki.secretgeek.net/ Luciano Strika https://strikingloo.github.io/wiki/ https://twitter.com/strikingloo Lyz https://lyz-code.github.io/blue-book/ M0wer https://m0wer.github.io/memento/ Maggie Appleton https://maggieappleton.com/garden https://twitter.com/Mappletons Magnus Aasrud https://www.kmaasrud.com/brain/ https://twitter.com/kmaasrud Marcus Hughes https://arbor.jmbhughes.com Markus https://re1.dev/wiki/ Mathieu Dutour https://mathieudutour.github.io/gatsby-digital-garden/ https://twitter.com/mathieudutour Matt https://matt.roam.garden/ Max Antonucci https://max-antonucci.gitbook.io/study-notebook/ https://twitter.com/Maxwell_Dev Max Holzheu https://digitalgarden.maxholzheu.com/ https://twitter.com/maxholzheu Max Stoiber https://notes.mxstbr.com/ https://twitter.com/mxstbr Maxime Vaillancourt https://maximevaillancourt.com/notes https://twitter.com/vaillancourtmax Michael E. Karpeles https://mek.fyi/ https://twitter.com/mekarpeles Mike Tannenbaum https://tannenbaum.netlify.app/ https://twitter.com/theroyaltbomb Milkii Brewster https://wiki.thingsandstuff.org/Main_Page Mitch Wagner http://mitchwagner.com/ https://twitter.com/mitchwagner Nadia Eghbal https://nadiaeghbal.com/notes/ https://twitter.com/nayafia Nagarjun Palavalli https://nagarjun.co/pensees https://twitter.com/palavalli Neil Mather https://commonplace.doubleloop.net/ Nick Trombley https://barnsworthburning.net/ Nikita Voloboev https://wiki.nikitavoloboev.xyz/ https://twitter.com/nikitavoloboev Noah Trenaman https://blog.noahtren.com/note/3acea2e1/ https://twitter.com/noahtren Ollie Francis https://www.notion.so/818782f2ff0f44ccbc5941e3fd4d0cd0?v=3badd8762a2f424189dc13c6f4f11539 https://twitter.com/ollie_francis Oshyan Greene https://garden.oshyan.com/ https://twitter.com/ogreenius Paul Batchelor https://pbat.ch/wiki/ Paul Bricman https://paulbricman.com/secondbrain/ Paul Copplestone https://paul.copplest.one/knowledge/ https://twitter.com/kiwicopple Paul Scanlon https://paulie.dev/posts/ https://twitter.com/PaulieScanlon Pepe García https://braindump.pepegar.com/ Phil Jones http://thoughtstorms.info/view/HelloWorld Piero Scaruffi https://www.scaruffi.com/index.html http://twitter.com/pscaruffi Piotr Gaczkowski https://garden.doomhammer.info https://twitter.com/DoomHammerNG Piotr Wozniak https://supermemo.guru/wiki/Piotr_Wozniak Prasanth Karri https://prasanthkarri.com/ https://twitter.com/thisizkp Priyansh Sangule https://abstractxan.xyz/site/home.html purarue https://purarue.xyz/x/ Rahul Rajeev https://garden.rahulrajeev.net https://twitter.com/rahulrajeeev Raphael Sprenger https://garden.raphaelsprenger.de/ Rasul Kireev https://rasulkireev.com/digital-garden/ https://twitter.com/rasulkireev Richard Haines https://richardhaines.dev/blog/ https://twitter.com/studio_hungry Rob Haisfield https://roamresearch.com/#/app/Rob-Haisfield-Thinking-in-Public/page/XtzimCS2z https://twitter.com/RobertHaisfield Rob Lanphier https://myndmess.miraheze.org/wiki/User:RobLa https://twitter.com/robla Roman Rey https://roamresearch.com/#/app/digitaljungle/page/3-pt2rrKj https://twitter.com/derrey Rosano https://rosano.hmm.garden https://twitter.com/rosano Rosie Campbell https://notes.rosiecampbell.me/ https://twitter.com/RosieCampbell Rui Vieira https://ruivieira.dev https://twitter.com/ruimvieira Ryan Dawidjan https://www.notion.so/ryan-dawidjan-414196ebb5204aa89f5af7ead579cae4 Ryan Warner https://ryan.warner.codes/notes https://twitter.com/RyanWarnerCodes Salman Ansari https://notes.salman.io/ https://twitter.com/daretorant Sam Marshall https://notes.sjm.codes/ Santhosh Thottingal https://docs.thottingal.in/ https://twitter.com/santhoshtr/ Santi Younger https://notes.santiyounger.com/ Sanyam Kapoor https://www.sanyamkapoor.com/kb https://twitter.com/activatedgeek Sarah Hibner http://meet-me-in.space/ Serge Kazakov https://kazaimazai.com/ https://x.com/kazaimazai Scott Spence https://scottspence.com/garden https://twitter.com/spences10 Shadab Zafar https://github.com/dufferzafar/notes https://twitter.com/dufferzafar Shawn Wang https://www.swyx.io/writing https://twitter.com/swyx Shreyas Minocha https://wiki.shreyasminocha.me Simon Späti https://brain.sspaeti.com https://twitter/sspaeti Soren Bjornstad https://zettelkasten.sorenbjornstad.com/ Sridhar Ratnakumar https://www.srid.ca/ Steve Dondley https://steve.dondley.com/notes/ https://twitter.com/steve_dondley Stian Håklev https://roamresearch.com/#/app/stian-research/page/3T1ePQfcr https://twitter.com/houshuang Szymon Kaliski https://szymonkaliski.com/notes/ https://twitter.com/szymon_k Thilo Maier https://coding.maier.dev/notes https://twitter.com/454de6e Todd Grotenhuis https://blog.grotenhuis.info/digital-garden/ https://twitter.com/toddgrotenhuis Tom Critchlow https://tomcritchlow.com/wiki/ https://twitter.com/tomcritchlow Tommi Boom https://tommi.space/jam Ton Zijlstra https://www.zylstra.org/blog/wiki-frontpage/ https://twitter.com/ton_zylstra Tymon Zaniewski http://garden.tymon-zaniewski.xyz Visakan Veerasamy https://roamresearch.com/#/app/visakanv-public/page/45EU_k-rI https://twitter.com/visakanv Vladyslav Sitalo https://vlad.roam.garden/ https://twitter.com/VladyslavSitalo Vít Baisa https://vit.baisa.cz https://twitter.com/vitbaisa Walter Teng https://walterteng.com/garden/ https://twitter.com/davzoku Wayan Jimmy https://notebook.wayanjimmy.xyz/ https://twitter.com/wayanjimmy Waylon Walker https://waylonwalker.com/notes https://twitter.com/_waylonwalker Wess Daniels https://nurselog.online/ https://twitter.com/cwdaniels Whoibrar https://notes.whoibrar.com https://twitter.com/whoibrar Will Bamberg https://www.willbamberg.com/garden/ https://twitter.com/willbamberg Winnie Lim https://winnielim.org/journal/ Xinyang YU https://notes.yxy.ninja YaoXing Liu https://charlesliuyx.github.io/ Yenly Ma https://til.yenly.wtf/ Yusuf Parak https://ycp.so/ https://twitter.com/ycpso http://okmij.org/ftp/ https://anagora.org/ https://twitter.com/flancian https://emsenn.net/ https://lateral.wiki/ https://sphygm.us/ https://webring.xxiivv.com/


Menu
Main content

Bookmarks Settings Documentation

justyy.com
23 September 2025
justyy.com
About 2 minutes read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

Python Radix Sort 教程: 整数、负数和浮点数排序

Python 基础排序算法:基数排序详解与示例

Python Radix Sort 教程:整数、负数和浮点数排序

Python 数字排序指南:从整数到浮点的基数排序实现

高效排序算法讲解:Python 中的基数排序应用

Python 排序算法全解析:Radix Sort 的用法与实例

Python 基数排序简介

基数排序是一种非比较型排序算法,它通过按位对数字进行排序来完成排序。与直接比较整个数字(如快速排序或归并排序)不同,基数排序将元素根据其数字或字符分配到“桶”中,然后逐位处理。

对于整数,基数排序通常从最低有效位(LSD)到最高有效位(MSD)进行排序。这样可以保证稳定性,在处理完所有位后得到有序数组。

— 基数排序的工作原理

找到数组中的最大值,以确定需要处理的位数。
对每一位(个位、十位、百位等)使用稳定排序(如计数排序)。
重复此过程直到处理完所有位。

示例:对数组 [170, 45, 75, 90, 802, 24, 2, 66] 进行排序:

按个位排序 → [170, 90, 802, 2, 24, 45, 75, 66]
按十位排序 → [802, 2, 24, 45, 66, 170, 75, 90]
按百位排序 → [2, 24, 45, 66, 75, 90, 170, 802]

此时数组已经排序完成。

— Python 正整数基数排序实现

def counting_sort(arr, exp): n = len(arr) output = [0] * n count = [0] * 10

for num in arr:
    index = (num // exp) % 10
    count[index] += 1

for i in range(1, 10):
    count[i] += count[i - 1]

for i in range(n - 1, -1, -1):
    index = (arr[i] // exp) % 10
    output[count[index] - 1] = arr[i]
    count[index] -= 1

for i in range(n):
    arr[i] = output[i]

def radix_sort(arr): if not arr: return arr

max_num = max(arr)
exp = 1
while max_num // exp > 0:
    counting_sort(arr, exp)
    exp *= 10

示例

arr = [170, 45, 75, 90, 802, 24, 2, 66] radix_sort(arr) print("排序后的数组:", arr)

— 输出

排序后的数组: [2, 24, 45, 66, 75, 90, 170, 802]

— 支持负整数的扩展

将负数和非负数分开。
将负数取绝对值转换为正数。
分别对两部分应用基数排序。
对负数结果进行翻转(绝对值大的负数排在前面)。
合并负数和非负数得到最终有序数组。

def radix_sort_positive(arr): if not arr: return arr max_num = max(arr) exp = 1 while max_num // exp > 0: counting_sort(arr, exp) exp *= 10

def radix_sort(arr): negatives = [-x for x in arr if x < 0] non_negatives = [x for x in arr if x >= 0]

radix_sort_positive(negatives)
radix_sort_positive(non_negatives)

negatives = [-x for x in reversed(negatives)]
return negatives + non_negatives

示例

arr = [170, -45, 75, -90, 802, 24, -2, 66] sorted_arr = radix_sort(arr) print("排序后的数组:", sorted_arr)

— 浮点数排序

浮点数也可以通过将它们转换为整数来使用基数排序。常用方法:

缩放:将所有浮点数乘以 10 的幂,将其转换为整数(适用于固定精度浮点数)。
位重解释:将 IEEE 754 浮点数按位当作整数处理,并对负数进行调整以保持顺序。

以下示例使用缩放法对正浮点数进行排序:

def radix_sort_floats(arr, precision=2): # 将浮点数缩放为整数 factor = 10 ** precision int_arr = [int(x * factor) for x in arr]

radix_sort(int_arr)

# 转回浮点数
return [x / factor for x in int_arr]

示例

arr = [3.14, 2.71, 1.41, 0.99, 2.0] sorted_arr = radix_sort_floats(arr) print("排序后的浮点数:", sorted_arr)

— 输出

排序后的浮点数: [0.99, 1.41, 2.0, 2.71, 3.14]

— 何时使用基数排序

当需要排序大量整数且最大值相对较小时。
对固定长度字符串进行高效排序。
在固定宽度整数或缩放浮点数的情况下需要线性时间排序。

— 局限性

不是通用排序算法,仅适用于整数或固定长度键。
需要额外的桶空间(不是原地排序)。
对于任意浮点数或混合类型,基于比较的排序(如 Python 内置 Timsort)更安全。

— 总结

基数排序是一种快速的按位排序算法,可以处理整数、负数和固定精度浮点数。在适用场景下,它能提供线性时间性能。通过对浮点数进行适当转换,基数排序的适用范围可以扩展到整数之外。

英文: A Complete Guide to Radix Sort in Python with Examples 本文一共 796 个汉字, 你数一下对不对. Python Radix Sort 教程: 整数、负数和浮点数排序. (AMP 移动加速版本) 赞赏我的几个理由. ¥ 打赏支持 扫描二维码,分享本文到微信朋友圈 75a5a60b9cac61e5c8c71a96e17f2d9c Python Radix Sort 教程: 整数、负数和浮点数排序 Python 学习笔记 排序 Sorting 数据结构与算法 计算机 The post Python Radix Sort 教程: 整数、负数和浮点数排序 first appeared on 小赖子的英国生活和资讯. 相关文章:

第一次私校家长会: 原来家长比孩子还卷 前几天参加了娃的第一次家长会,和几位家长聊下来,真是个个都很厉害。不光孩子们卷,家长也一样卷,一眼望去基本都是 Dr/博士。娃还调侃我一句:“这有什么的,你不也是 Dr 吗?” 我心里默默想:还好没写学校名字,不然我这野鸡大学的头衔真拿不出手 😂。 私校里真是人才济济,乐器过 8 级的太常见了,卷得不得了。我还问过娃,是想当 big fish in a small pond...
Python 有序数据结构完整指南(Sorted Containers) 有序数据结构在编程中(尤其是算法竞赛和竞技编程)非常实用。在 Python 中,主要由 Sorted Containers 库提供三种有序数据结构:SortedDictSortedSet  SortedList 深入理解 Python 有序数据结构:从内置到 SortedContainers Python 有序数据结构完整指南 Python...
按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了207500英镑的房子, 25年还清. 2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
如何通过二分查找搜索在区块链上根据时间戳定位区块? 前两天,我想查一下自己在 STEEM 区块链上一些重要记录对应的区块号,比如: 注册了我的账号 #4253590 成为见证人 #20190755 挖到我的第一个区块 #20390040 被孙宇晨大佬代理了 SP #41676911 收到一笔 DAO 收入...
和媳妇约个会: 剑桥的过桥米线 Dumpling Trees Dumpling Trees 是位于剑桥 Cherry Hilton 附近的一家中式餐厅,以云南特色的过桥米线闻名。店内环境宽敞整洁,菜品丰富,除了经典的米线,还有各类小吃、烧烤和炒饭,味道地道,分量十足。过桥米线的汤底鲜香,配料新鲜,包括鸡肉、鱿鱼、虾等食材,顾客可以自己下锅涮熟,既好吃又有趣。餐厅提供免费停车,但需在店内登记车牌,适合家庭聚餐或周末小聚。 剑桥 Cherry Hilton 那边有一家叫 Dumpling Trees 的过桥米线店,两三年前的冬天我们去吃过一次(剑桥 Dumpling Tree...
浅谈虚拟货币交易所三角套利的算法 高频交易(HFT, High Frequency Trading) 指得是我们可以让程序调用交易所的API 来自动下买单或者卖单, 赚差价来达到挣钱的目的. 常见的高频交易策略有低买高卖, 本文介绍一种常见的三角套利的策略. 高频交易 (HFT) 简介 高频交易 (HFT) 是一种算法交易,...
英国抓捕比特币ATM主犯: 所有与法币挂钩的加密活动必须获得许可 英国查获比特币ATM主犯:所有与法币挂钩的加密业务必须持牌 案件回顾:比特币ATM运营者被判四年 20252月,英国金融行为监管局(FCA)宣布成功起诉并判刑首位非法运营比特币ATM的个人——Olumide Osunkoya。该男子未经授权在伦敦多地运营加密ATM,处理交易金额达260万英镑。他曾试图注册合法业务但被拒,随后伪造身份文件绕过监管,最终被法院判处4年有期徒刑。 最新行动:7ATM被查封,两人被捕 紧接着在20257月,FCA与伦敦警察再次联合行动,在西南伦敦多个地点查封7台非法加密ATM,并拘捕2人。FCA重申:在英国,没有任何加密ATM获得合法运营许可,所有涉及法币兑换的活动必须事先注册并获得批准。 英国FCA官网原文指出:“我们提醒所有经营者,若他们继续运营未注册的加密ATM,将面临刑事起诉。”(原文链接见参考资料) 为什么这些ATM是非法的 在英国,只要涉及“加密货币  法币”的兑换行为,就会被纳入《反洗钱条例(MLRs)》的监管框架。运营者必须: FCA注册为加密资产公司 实施KYC(身份验证)和AML(反洗钱)程序 接受FCA的持续监管与合规审核 未经注册即开展此类活动,属于违法行为。...
记录那些值得回忆的精彩瞬间 媳妇 @happyukgo 其实挺反感我给她拍照片的, 但主要原因是 我经常把她拍得很丑, 而我除了会用美图秀秀简单的滤镜之外就不会一丁点儿的PS技术. 我家孩子 Eric 很爱臭美, 每次看着我拿相机, 总会让我 Take a picture, 然后各种摆姿势做鬼脸,...

Untag_Logo Beta 首页 教程 会员 方法 观点 专题 投稿 社区 / UNTAG Premium 观点 会员 广义间隔重复:如何记住你读过的一切 author_avatar

Minja

发布于:2025-09-22 18:00:00

Anki
信息管理
效率思维
降低思维负荷

6 0 目录

只整理一次等于完全浪费时间

广义间隔重复

更多文章

如何一键、批量、快速去除扫描版 PDF 上的顽固水印
付费栏目上新:《人文主义笔记法:猜想和实践的媒介》
人文主义笔记的图谱:缩放和观察的技艺|限时试读
Mental Set:一种启发性的文件分类标准
那些“总有一天会用到”的杂物,如何吞噬你的房间和精力?

article_image

有段时间,我成了办公室里使用率仅次于 DeepSeek 的搜索引擎。我可以凭借记忆,精确引用法律条文、法律注释、论文和参考案例。

考虑到我的同事大多来自顶尖法学院,而我是个连昨晚吃什么都记不得的健忘症晚期患者(在自己下厨之前),我有理由相信,绝对不是我的记忆力更好,只是记忆方法更实用。

很快我就发现了默认信息组织方式的问题,结合我在律师事务所、互联网大厂、事业单位和政府机关的工作经验,这应该是普遍现象:追求一次性整理好材料,日后要用时,如果有幸找到,用完后也会放回原位。

没有迭代,追求静态,大概就是问题所在。我认识的多数人,都使用文件夹、标签以及笔记,有的人还会用时髦的双向链接,但区别在于,我不断重做貌似枯燥的整理工作^1,除了文件位置基本不动,对文件的描述一直在变动。^2

这是一种更加广义的、进一步强化的间隔重复技术。 只整理一次等于完全浪费时间

为什么要反复做文件管理或者笔记工作?任何曾经在文件管理系统、笔记管理系统、个人信息管理系统、知识管理系统、标签管理系统或者书签管理系统中下过功夫甚至掏过不少钱的人,想必会掀翻桌子:作品凭什么怀疑我之前的工作,还建议我推翻重来一次?

很可惜,必须不断推翻重来,尽管不是彻底否定。体系或系统是一个很糟糕的说法,可以追溯到柏拉图和亚里士多德,他们做着恢复部落制度的白日梦^3,把万事万物安排到一个层级系统中,说白了,这只不过是西方版本的君君臣臣父父子子。越是僵化、越是抽象、越是瓜瓞绵绵的方法论,越接近于这种远古的残渣余孽。

稍微清醒一点的读者,可能也不(再)相信体系或系统,转而使用更灵活的标签或双向链接。但越是复杂的标签或链接,其实越接近于一个假想系统的子集,你给一篇文章、一本书或者一段视频打标签时,实际上是在给它分配一个位置,而这个位置究竟是出自你早年的青涩误会,还是来自生产力网红的潜移默化,只有你自己知道。

技术都不是问题的症结所在。只有一个问题:我们不可能一次性就认识事物,我们也不可能一次性做好文件管理(或其他任何XX管理)。

你一定听过那个经典的心理学实验:在火热的比赛现场,科学家让一只黑猩猩(也有可能是装扮成黑猩猩的工作人员)大大咧咧地走过,结果因为观众们过于投入比赛,完全没有注意到猩猩。读书亦然,你以为自己可以像古代哲学家一样虚怀若谷,这纯属胡扯,我们一定有所目标,有所预设,有所倾向,有所偏见,甚至有所成见,文章给你的,永远只是印象,而不是某种形而上的永恒知识。^4

好消息是,重复工作不是反复重来,不意味着你永远不能再踏进同一条河流。以阅读和工作为契机,反复回顾并更新对现有材料的描述,我找到了一种几乎记住任何东西的长线方案。 广义间隔重复

欲说广义,需澄狭义。通说上的间隔重复,与传统复习方案的区别在于间隔节奏,生疏者紧锣密鼓,驾轻就熟者稍稍延后,不过如此。

听起来很美好,但元认知负担非常重:谁来帮你安排复习节奏?在智能手机流行前,我真的见过有些培训班采用了类似策略(尽管有历史误植的误会),只不过更依靠培训机构的感觉。那些学习班通常刊登在报,函授为主,大概算是现代复习软件或服务的前身。

软件解放了一切,算法不仅可以为你调整节奏,还能精细到知识点,免去重复全文,甚至可以量身定制。基于间隔重复的知名学习软件 Anki,就曾在全球多个 App Store 登上付费排行榜前列。

不过,使用这类软件的前提,是你得交出数据。除了我专门录入 Anki 的具体知识点,我还想在更粗略的意义上记住更多东西,例如一本书、一篇论文或一段视频,我不需看逐字背诵,但如果我有可能用上它,我希望这些东西能自动浮现。可惜,我不可能把整个电脑变成 Anki 数据库,一如我不会指望电子词典或学习机能取代正经电脑——毕竟,我还要工作。^5

很幸运,我发现了广义的间隔重复:专注于你对知识或信息的描述,而非知识或信息本身;以阅读和实践为契机,反复更新。如果你乐意,落地后的方案可以是 Anki、笔记和书签的大杂烩。 Alt text 由 Anki、笔记和书签组成的巨大构造

不过也有更轻松的形式。一堆文本也够了。 本文剩余内容会员可见 本文是 Premium 会员专属文章, 即可阅读全文,或者 。

Anki
信息管理
效率思维
降低思维负荷

6 0 author_avatar

Minja

Lawyer, macOS/iOS Automation Amateur

了解如何订阅 RSS,第一时间收到文章更新:

加入 Slack 群,直接和主笔交流: 成为会员,享受更多增值服务: 关联文章

Mental Set:一种启发性的文件分类标准
个人文件管理一直试图在图书管理员和小说死忠粉之前取得平衡。
Minja
2025-09-15 18:00:00
人文主义笔记的图谱:缩放和观察的技艺|限时试读
多用图谱发现问题,但不要试图伪造图谱截图发社交网络。
Minja
2025-09-05 17:00:00
付费栏目上新:《人文主义笔记法:猜想和实践的媒介》
笔记亟需一场文艺复兴。
Minja
2025-08-29 21:00:00
一种融入文件管理系统的任务管理技巧
组织良好文件管理系统自身就是天然的任务管理工具。
Minja
2025-08-25 18:00:00
青年职场人的出租屋空间翻倍术
其实原标题是:如何用每月2000元的价格租到活动空间逾100平米的市中心海景房。
Minja
2025-08-04 18:00:00

首页 / Premium / 教程 / 社群 / RSS / 关于 / 联系我们 / Beta 反馈 / 清空缓存 © 2022-2024 苏州向上青年网络科技有限公司 All rights reserved./#UNTAG/苏ICP备2022012662号


Menu
Main content

Bookmarks Settings Documentation

justyy.com
23 September 2025
justyy.com
About 2 minutes read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

Python 有序数据结构完整指南(Sorted Containers)

有序数据结构在编程中(尤其是算法竞赛和竞技编程)非常实用。在 Python 中,主要由 Sorted Containers 库提供三种有序数据结构:SortedDict、SortedSet 和 SortedList。

深入理解 Python 有序数据结构:从内置到 SortedContainers

Python 有序数据结构完整指南

Python 中的有序列表、字典与集合实战解析

带你玩转 Python SortedContainers 与内置排序结构

Python 开发者必读:SortedContainers 与内置数据结构对比

Python 有序数据结构教程

排序是编程中最常见的操作之一。Python 提供了多种方式来维护有序数据,从内置的列表、集合、堆,到第三方库 sortedcontainers。

本教程将介绍 Python 的有序数据结构,它们的优缺点,以及实际应用场景。 内置选项 1. list + bisect

你可以使用 bisect 模块手动维护一个有序列表。

import bisect

nums = [1, 3, 5] bisect.insort(nums, 2) # 插入并保持有序 print(nums) # [1, 2, 3, 5]

这种方法有效,但插入和删除操作是 O(n),因为需要移动元素。 2. heapq

heapq 模块实现了优先队列。它部分有序,可以高效获取最小或最大值。

import heapq

nums = [5, 1, 3] heapq.heapify(nums) # 转换为堆 print(heapq.heappop(nums)) # 1

适合频繁取最小/最大值,但不会维护全局有序。 3. dict 和 set

字典和集合在 Python 中基于哈希表。它们提供 O(1) 查找,但不保持有序。

myset = {3, 1, 2} print(2 in myset) # True print(sorted(myset)) # [1, 2, 3]

如果需要真正的有序结构,就需要更好的工具。 sortedcontainers 库

sortedcontainers 提供了三个强大的数据结构:

SortedList
SortedDict
SortedSet

它们能自动保持有序,并且插入、删除都是 O(log n)。

安装方式:

pip install sortedcontainers

SortedList

像列表,但始终保持有序。

from sortedcontainers import SortedList

sl = SortedList([5, 1, 3]) print(sl) # SortedList([1, 3, 5])

sl.add(2) print(sl) # SortedList([1, 2, 3, 5])

sl.remove(3) print(sl) # SortedList([1, 2, 5])

print(sl.bisect_left(2)) # 1

应用场景:实时统计、滑动窗口中位数、排行榜。 SortedDict

像字典,但键有序。

from sortedcontainers import SortedDict

sd = SortedDict({"b": 2, "a": 1, "c": 3}) print(sd) # SortedDict({'a': 1, 'b': 2, 'c': 3})

print(sd.peekitem(0)) # ('a', 1) print(sd.peekitem(-1)) # ('c', 3)

for k in sd: print(k, sd[k])

应用场景:有序映射、区间调度、最近邻查询。 SortedSet

像集合,但有序。

from sortedcontainers import SortedSet

ss = SortedSet([3, 1, 2, 2]) print(ss) # SortedSet([1, 2, 3])

ss.add(5) print(ss) # SortedSet([1, 2, 3, 5])

print(ss.bisect_left(3)) # 2

应用场景:去重日志、唯一且有序的数据。 实际应用案例 1. 滑动窗口中位数

from sortedcontainers import SortedList

def sliding_window_median(nums, k): sl = SortedList() res = [] for i, num in enumerate(nums): sl.add(num) if i >= k: sl.remove(nums[i-k]) if i >= k-1: if k % 2 == 1: res.append(sl[k//2]) else: res.append((sl[k//2-1] + sl[k//2]) / 2) return res

print(sliding_window_median([1,3,-1,-3,5,3,6,7], 3))

  1. 排行榜系统

from sortedcontainers import SortedList

class Leaderboard: def init(self): self.scores = SortedList()

def add_score(self, player, score):
    self.scores.add((score, player))

def top(self, n):
    return self.scores[-n:]

def reset(self, player, score):
    self.scores.remove((score, player))

lb = Leaderboard() lb.add_score("Alice", 50) lb.add_score("Bob", 80) lb.add_score("Charlie", 70) print(lb.top(2))

  1. 使用 SortedDict 查找最近的键

from sortedcontainers import SortedDict

sd = SortedDict({"a": 1, "c": 3, "e": 5}) idx = sd.bisect_left("d") print("前驱:", sd.peekitem(idx-1)) print("后继:", sd.peekitem(idx))

何时选择哪种数据结构

– list + bisect: 静态数据,更新少 – heapq: 重复获取最小/最大值 – dict/set: 快速查找但无需排序 – sortedcontainers: 有序且需要频繁更新 总结

sortedcontainers 库为 Python 提供了高效优雅的有序数据结构。如果你需要同时兼顾顺序与性能,它通常比内置工具更合适。 对比表格 特性 list + bisect heapq dict set sortedcontainers 是否保持有序 是(手动) 否 否 否 是 插入/删除复杂度 O(n) O(log n) O(1) O(1) O(log n) 随机访问 支持 不支持 仅键 不支持 支持 范围查询 不支持 不支持 不支持 不支持 支持 最佳使用场景 静态查找 优先队列 哈希映射 集合查找 有序集合且需更新

英文:A Complete Guide to Sorted Data Structures in Python 本文一共 686 个汉字, 你数一下对不对. Python 有序数据结构完整指南(Sorted Containers). (AMP 移动加速版本) 赞赏我的几个理由. ¥ 打赏支持 扫描二维码,分享本文到微信朋友圈 75a5a60b9cac61e5c8c71a96e17f2d9c Python 有序数据结构完整指南(Sorted Containers) Python Python 学习笔记 数据结构与算法 程序设计 算法 计算机 The post Python 有序数据结构完整指南(Sorted Containers) first appeared on 小赖子的英国生活和资讯. 相关文章:

和媳妇约个会: 剑桥的过桥米线 Dumpling Trees Dumpling Trees 是位于剑桥 Cherry Hilton 附近的一家中式餐厅,以云南特色的过桥米线闻名。店内环境宽敞整洁,菜品丰富,除了经典的米线,还有各类小吃、烧烤和炒饭,味道地道,分量十足。过桥米线的汤底鲜香,配料新鲜,包括鸡肉、鱿鱼、虾等食材,顾客可以自己下锅涮熟,既好吃又有趣。餐厅提供免费停车,但需在店内登记车牌,适合家庭聚餐或周末小聚。 剑桥 Cherry Hilton 那边有一家叫 Dumpling Trees 的过桥米线店,两三年前的冬天我们去吃过一次(剑桥 Dumpling Tree...
英国抓捕比特币ATM主犯: 所有与法币挂钩的加密活动必须获得许可 英国查获比特币ATM主犯:所有与法币挂钩的加密业务必须持牌 案件回顾:比特币ATM运营者被判四年 20252月,英国金融行为监管局(FCA)宣布成功起诉并判刑首位非法运营比特币ATM的个人——Olumide Osunkoya。该男子未经授权在伦敦多地运营加密ATM,处理交易金额达260万英镑。他曾试图注册合法业务但被拒,随后伪造身份文件绕过监管,最终被法院判处4年有期徒刑。 最新行动:7ATM被查封,两人被捕 紧接着在20257月,FCA与伦敦警察再次联合行动,在西南伦敦多个地点查封7台非法加密ATM,并拘捕2人。FCA重申:在英国,没有任何加密ATM获得合法运营许可,所有涉及法币兑换的活动必须事先注册并获得批准。 英国FCA官网原文指出:“我们提醒所有经营者,若他们继续运营未注册的加密ATM,将面临刑事起诉。”(原文链接见参考资料) 为什么这些ATM是非法的 在英国,只要涉及“加密货币  法币”的兑换行为,就会被纳入《反洗钱条例(MLRs)》的监管框架。运营者必须: FCA注册为加密资产公司 实施KYC(身份验证)和AML(反洗钱)程序 接受FCA的持续监管与合规审核 未经注册即开展此类活动,属于违法行为。...
MySQL参数一键配置脚本: 有效提升数据库性能 我一直是自己租用VPS服务器,然后搭建各种服务,比如博客就是Apache2+MySQL数据库。一般来说就是默认参数,没有去管,不过最近发现MySQL的性能参数都很保守,不能发挥整个服务器的性能。 然后我就网上搜索了一下,根据参数配置建议,用ChatGPT写了以下Python和BASH脚本。只需要在需要优化的服务器上,跑一下该脚本,然后就会显示参数配置,然后直接把参数添加到MySQL数据库配置参数文件上 /etc/mysql/mysql.conf.d/mysqld.cnf 然后运行: service mysql restart 重启MySQL服务器 运行了几周,发现效果很好,博客反应速度也快了很多,这很大原因是根据了内存增加了MySQL缓存大小 Python脚本优化MySQL数据库参数 把下面的Python脚本存成 mysql_config.py 然后运行 python3 mysql_config.py...
第一次私校家长会: 原来家长比孩子还卷 前几天参加了娃的第一次家长会,和几位家长聊下来,真是个个都很厉害。不光孩子们卷,家长也一样卷,一眼望去基本都是 Dr/博士。娃还调侃我一句:“这有什么的,你不也是 Dr 吗?” 我心里默默想:还好没写学校名字,不然我这野鸡大学的头衔真拿不出手 😂。 私校里真是人才济济,乐器过 8 级的太常见了,卷得不得了。我还问过娃,是想当 big fish in a small pond...
Steem 指南》之 justyy 在线工具与 API 系列  Discord 机器人 Discord 聊天频道 Discord 原本是给游戏设计的, 但由于其功能多, 接口开放能力强, 使用的用户越来越多. 我们CN区也有一个Discord 频道, 加入地址为: https://discord.gg/7ctT3Xt 在网页里就可以加入 cnsteem 的大家庭了, 当然也可以下载手机APP或者桌面程序来加入...
C++编程练习题: 找出字符串的所有大小小组合 题意: 给定一个字符串, 输出所有字符大小写都可以组成的字符串. : ab1 能成生 DFS 深度优先  递归 我们可以从字符串的开头递归的把当前字符给添加到最终的字符串中, 当当前字符是字母的时候, 就有两种可能了. 当到达字符串尾部的时候我们把当前字符串添加到结果数组中即可. class...
世界再无OneKey币圈美元虚拟卡了 我前两年就了解到OneKey这个币圈虚拟货币出金卡,不过去年年底才注册使用的。当时还花了99美元一步升级到顶级黑卡。然后这一年陆陆续续用了这卡,但用得不多,主要就用于支持一些VPS主机费还有CloudFlareChatGPT Pro等 这个卡是美国地址,卡号有两个段,Visa  Mastercard,不过由于地址是美国的,刷卡可能会有问题。比如我ChatGPT Pro注册帐号是英国的,然后用这卡支付了几个月,突然有一天帐号就被封,被告知:您的付款记录很可疑。 印象中,用这虚拟货币Crypto Card美元出金卡有手续费,但是并没有啥Cash Back返现卡,如果是非美元购物则会有另一笔手续费,所以我很少用这卡出金变现。 前两个月,OneKey宣布关闭 关于 OneKey Card 服务停用通知 尊敬的用户,为提高服务质量和优化产品供应,我们将按照以下时间表停用...
按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了207500英镑的房子, 25年还清. 2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...

3-2-1 备份策略

📜 文章链接: The 3-2-1 Backup Strategy

本文讲解了保证数据安全的常见策略——3-2-1 备份策略。简单来说,3-2-1 法则要求你这样备份数据:

数据至少有 3 份副本;
数据至少被存储在 2 种不同的存储介质上;
数据至少有 1 份副本存储在场外(off-site)。

其中,场外指的是工作场所之外,也就是存储在远程的另一台设备上。此外,作者还提到,3-2-1 备份策略已经很老了(在它诞生的那一年,Taylor Swift 只发行了两张专辑!),所以第二条规则在现在的意义有所改变。在以前,人们可以选择很多种不同的存储介质,比如 CD、软盘、U 盘、机械硬盘、磁带等等;如今,CD 和软盘被淘汰了,固态硬盘越来越廉价,人们的选择在整体上变得更单一了。所以,在实行 3-2-1 法则时,不一定要使用两种类型的存储介质,可以只存储在两台设备上。不过,话虽如此,选用尽可能多样的存储介质能保证当前的存储技术被淘汰之后,你的数据依然是可读的(如今已经很难买到能够读取软盘和 CD 的设备了),这适用于那些需要长期储存的数据。

作者说,只要定期把数据备份到外部硬盘上,并且有一个能实时同步本地文件更新的云端服务,就能简易地实现 3-2-1 法则。因为这样的话,就有本地、硬盘和云端三个数据副本,本地硬盘和外部硬盘又是两种存储设备,云服务能保证一份副本在场外。 我的观点

这周险些丢掉了 MacBook 上的数据,原因是电脑在一次重启之后突然无法正常启动,在锁屏界面输入密码之后,Mac 一直卡在加载界面。观察后发现时钟一直没有动,应该是死机了。后来尝试了以安全模式启动、重置 NVRAM、在恢复模式中运行磁盘急救,都没有解决问题,试过两次重装 macOS,竟然还是遇到同样的问题(似乎没能完成系统安装)。本来都已经预约好维修了,结果一个小时没碰它,再次打开 MacBook 的时候系统就恢复了…… 不过启动的时候弹出了好多窗口,我想可能是关机前启动的应用和登陆启动项太多,导致内存写满了。

问题解决之前,我其实也没有很慌,因为我知道就算是电脑遗失了我也不担心数据问题——我的硬盘是有密码的,外人没法读取数据;我的重要数据都在外部硬盘和云端(代码更是全部都在 GitHub 上)。

读完这篇文章之后我意识到,自己其实一直在无意识地践行 3-2-1 备份策略。对于普通人来说,周期性地把数据备份到外部硬盘上,如果还有 iCloud 或者 OneDrive 之类的服务的话,数据就不成问题。

此外,我在 第 32 期周刊 中有分享过一篇讲光盘的文章,那篇文章的作者认为尽管这种技术被淘汰了,光盘本身依然是一种十分可靠的存储介质,他自己会有光盘存储那些不常使用但需要大量存储的数据。

计算机 #Geek


你不需要动画

📜 文章链接: You Don’t Need Animations

作者认为交互设计中任何动画都应该符合以下准则:

有明确的意图,哪怕只是为了让人感到愉悦,也是一个意图。
常用的元素不应该有动画,否则会让人觉得很慢、有延迟、感到厌烦。作者拿 Raycast 的交互设计举例,Raycast 窗口的打开关闭和其他交互都没有过渡动画。作者做了一个有动画的例子,看着确实挺烦的。
大部分情况下,动画的速度应该很快,持续时间应该在 300ms 以下。

作者认为构建动画最重要的部分是知道什么时候添加动画。 我的观点

我记得我在设置 CSS transition 的时候,如果不想思考,就会默认使用 0.3s(也就是 300ms)的动画持续时间,因为这是我实践下来觉得很舒服又不会太快的时间,在不同的场景下会以 0.3s 作为基准值微调。

作者的观点也算刷新了我的认知,因为我之前一直认为没有任何动画会让界面看起来很生硬,现在意识到常用的元素如果有过渡动画的话,的确很烦人,比如 macOS 26 的新聚焦搜索,虽然功能增强了但每次打开都有动画,实在是难以作为常用工具,不久之后我就换回 Raycast 了。

设计


Life-style business 和雄心壮志

📜 文章链接: Building Something Big - Allen Pike

Allen Pike 是 Forestwalk 的创始人,他说 Forestwalk 原本是一个 Life-style business,意思是生意的规模取决于创始人的生活习惯,这样的公司规模往往不会很大,但能在创造财富的同时留足时间给自己和自己的家人。然而,Allen 自认为有些雄心壮志,并且意识到如果要和真正有才的人共事,就必须干一番大事业来吸引他们。他带领团队在年初变更了发展方向,并且一直在调整计划,想要创造一些大规模并且可行的东西。 我的观点

文章提到了 Alex MacCaw 有关 Life-style business 的观点,阐述了这种企业的利弊,其中最大的弊应该就是吸引不到有才华的人。这大概也是吸引力法则的体现。在我看来,Allen 是因为真的想要和更厉害的人共事才做出如此改变的,所以,如果自己并不是真的想要得到某种结果,就没必要逼自己卷,否则最后只能与和自己一样卷的人在一起。

不能期待环境变好,而是要大胆表达和改变,这样才能把与自己同频的人和事都吸引过来。


用户界面中的你和我

📜 文章链接: “Your” vs “My” in user interfaces – Adam Silver

作者讨论了在用户界面中指代用户的东西时应该用「你的」还是「我的」这个词,比如,应该写成「你的账户」「你的订单」,还是「我的账户」「我的订单」呢?然而,这在大部分情况下是欺骗性问题(trick question),因为这种东西直接写成「账户」和「订单」就好了!

然而,总归会有一定要用到人称代词的情况,比如,在一些场景下,有一些项目时属于所有人的,而用户如果想要查看属于自己的那一些,就需要点击一个导航链接,那这里应该写「我的项目」还是「你的项目」呢?作者写到,如果用「我的」这个词,会有一个问题,在帮助文档中和其他人指导用户操作界面的时候,就可能会说出这样的话:

在界面中找到「你的项目」。

但界面中只有「我的项目」,这可能会让人感到困惑。如果这个人说「找到我的项目」,被指导的用户又会分不清对方指的到底是自己的还是对方的。作者表示,如果用「你的」就永远不会出现这种问题。

在另一些场景下,比如在表单中,用户需要和服务商沟通:

你愿意分享你的头像吗?

    是的,分享你的头像。
    不要,不分享你的头像。

这里的选项就很别扭,应该用「我的」才对。简单来说:

与用户沟通的时候用「你的」
用户与我们沟通的时候用「我的」

我的观点

我觉得,愿意自主研究这种细枝末节的问题的人,一定是对自己所做的事业有热爱的,至少,读完这篇文章之后我感觉作者是这样一个人。这样的人,是在那些稀里糊涂选了一个专业之后被推着就业的人群里,很难找到的。我不禁好奇作者身边的都是什么样的人,也跟他一样有热情吗?他会不会感到孤独?因为我时常无法摆脱这样的孤独。


Menu
Main content

Bookmarks Settings Documentation

www.huluohu.com
22 September 2025
huluohu.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. 🏴‍☠️ 你心心念的资源来啦!GoComicMosaic一键搜全网!

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可

GoComicMosaic

还在为找影视资源东奔西走?还在各种网盘群里苦苦求片?还在我的留言板下叫板?别慌!今天给大家介绍一款开源神器——GoComicMosaic,它就像一个影视界的“资源共享中心”,让大家一起添砖加瓦,共同打造一个资源丰富、使用便捷的影视资源平台。

🏷️ 啥是GoComicMosaic?

简单来说,GoComicMosaic就是一个开源的影视资源共建平台。你可以把它想象成一个影视版的“维基百科”,每个人都可以贡献自己知道的资源信息,最终汇聚成一个庞大的影视资源库。

核心功能:

资源提交:把自己喜欢的影视资源分享给大家。
网盘搜索:一键搜索全网网盘资源,告别到处求种。
剧集信息查看:分季分集信息一目了然,追剧更省心。
在线点播:无需下载,直接在线观看,方便快捷。

特色:

用户共建:资源由大家共同维护,内容更全面、更丰富。
马赛克式资源整合:每个人贡献一点,最终拼凑成完整的资源。

🏷️ 为啥要用GoComicMosaic?

以前,我们找影视资源,要么靠自己大海捞针,要么在各种资源群里求爷爷告奶奶,费时费力不说,还不一定能找到想要的。

GoComicMosaic的出现,就是为了解决这些痛点!它有三大优势:

资源共享:用户共同维护,资源更全面,更新更及时。
功能集成:网盘搜索、在线点播,一站式服务,省时省力。
可扩展性:支持自定义爬虫,可以根据自己的需求,灵活获取资源。

🏷️ 咋用GoComicMosaic?

GoComicMosaic的部署和使用都非常简单,即使是小白也能轻松上手。 Docker一键部署

这里提供两种方式,任君选择:

方法一:Docker CLI

docker run -d --name dongman \ -p 8080:80 -p 8443:443 \ -v /your/local/path:/app/data \ -e TZ=Asia/Shanghai \ 2011820123/gcm:latest

如果需要启用HTTPS,还需要配置SSL证书:

创建SSL证书目录:

mkdir -p /your/local/path/ssl

复制证书文件(文件名必须是fullchain.pem和privkey.pem):

cp /path/to/your/fullchain.pem /your/local/path/ssl/
cp /path/to/your/privkey.pem /your/local/path/ssl/

方法二:Docker Compose

创建一个名为docker-compose.yml的文件,将以下内容复制进去:

services:
  gocomicmosaic:
    image: 2011820123/gcm:latest
    container_name: dongman
    ports:
      - "8080:80"
      - "8443:443"
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /your/local/path:/app/data
      - /your/local/path/ssl:/app/ssl # 如果启用HTTPS
    restart: unless-stopped

将/your/local/path替换为你想要挂载的本地目录。如果启用了HTTPS,请确保SSL证书文件位于/your/local/path/ssl目录下。

在包含docker-compose.yml文件的目录下,运行以下命令启动服务:

docker-compose up -d

等待片刻,在浏览器中输入http://ip:8080,即可进入GoComicMosaic。

基本使用

首页:资源展示,搜索入口,可以通过关键词快速找到想看的影视。

GoComicMosaic首页

网盘搜索:点击“盘搜”按钮,一键搜索全网网盘资源。

GoComicMosaic网盘搜索

剧集探索:点击“剧集探索”按钮,可以查看分季分集信息。

GoComicMosaic剧集探索

在线点播:点击播放按钮,直接在线观看,无需下载。

GoComicMosaic在线点播

新建资源:如果网站还没有收录该影视,可以新建资源,填写影视的基本信息。

GoComicMosaic提交-新建资源

管理控制台:管理员可以在后台审批用户提交的资源,编辑影视信息。

首次登录用户名和密码分别是admin和admin123。
GoComicMosaic登录

GoComicMosaic管理控制台

🏷️ GoComicMosaic的亮点功能

网盘资源一键搜:告别手动搜索,一键找到所需资源。
分季分集信息一览:追剧党的福音,再也不用担心错过任何一集。
在线点播,想看就看:无需下载,随时随地畅享影视。
支持外挂数据源,DIY爬虫:高级玩家的专属,可以根据自己的需求,定制资源获取方式。

GoComicMosaic外挂数据源 🏷️ 总结

GoComicMosaic作为一个开源的影视资源共建平台,为影视爱好者提供了一个资源共享、便捷使用的平台。它通过用户共建的方式,汇聚了大量的影视资源,并通过网盘搜索、在线点播等功能,让用户可以更方便地找到和观看自己喜欢的影视。

原创不易,如果觉得此文对你有帮助,不妨点赞+收藏+关注,你的鼓励是我持续创作的动力!

高等精灵实验室 HELIB_QRCODE


Menu
Main content

Bookmarks Settings Documentation

www.sunzhongwei.com
22 September 2025
sunzhongwei.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

git blame 确认代码中的屎是不是自己拉的

今天在排查一处程序 bug 时,发现一段代码写的逻辑实在乱,完全没有印象正确的逻辑应该是怎样的。 于是想查看这段代码是不是自己写的,什么时候写的,为什么这么写的。

直接在 VSCode 的 github copilot 的 Chat 窗口中问了一下:

如何通过 git 命令查看某段代码是那个提交修改的:例如: line1 line2 line3

得到的回答是,可以试试 git blame 命令。试了一下,非常好用。 git blame 示例

例如, 我想查看 models/product.go 文件中第 10 到 13 行的修改记录,可以运行:

git blame -L 10,13 models/product.go

执行结果如下图:

git blame 执行结果

可以看到每一行代码前面都有一个 commit hash 和作者信息。通过这些 hash 可以进一步查看具体的提交内容。

git show

VSCode 中查看 git blame 信息

网上搜索了一下,发现 VSCode 内置了 git blame 功能,可以直接在编辑器中查看每行代码的最后修改信息。

参考,VSCode 的官方文档:

https://code.visualstudio.com/docs/sourcecontrol/overview

git blame vscode 文档

可以看到,编辑区域的底部的 Status Bar 上会显示当前行的最后修改信息,点击可以查看更多详情。如下图所示:

git blame vscode

还是 VSCode 方便啊。 关于作者 🌱

我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式


Menu
Main content

Bookmarks Settings Documentation

windiscover.com
22 September 2025
windiscover.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

微软将从 10 月 3 日再次上调 Xbox 主机价格,本次调价仅针对美国市场

微软宣布微软将从 10 月 3 日起再次上调 Xbox 主机价格,本次调价仅针对美国市场,此次调整涉及的产品有 Xbox Series X 和 Xbox Series S 系列。

这是继今年五月 Xbox 硬件在全球范围内涨价后的第二次价格调整:

Xbox 主机涨价 价格调整详情

根据微软公布的价格信息,此次上涨幅度具体如下: 产品 原始价格 调整前价格 调整后价格 Xbox Series S 512GB $299.99 $379.99 $399.99 Xbox Series S 1TB $349.99 $429.99 $449.99 Xbox Series X 数字版 $449.99 $549.99 $599.99 Xbox Series X $499.99 $599.99 $649.99 Xbox Series X 2TB 特别版 $599.99 $729.99 $799.99

微软在声明中表示:

“自 10 月 3 日起,我们将根据宏观经济环境的变化,更新美国市场 Xbox Series S 和 Xbox Series X 主机的建议零售价。”

via Xbox

©2024 WinDiscover | 阅读原文 | 添加评论

微软将从 10 月 3 日再次上调 Xbox 主机价格,本次调价仅针对美国市场最先出现在WinDiscover。


Menu
Main content

Bookmarks Settings Documentation

dusays.com
22 September 2025
dusays.com
About 1 minute read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

Ubuntu 系统无法通过 pip 命令安装 Python 库问题解决

为了更好的执行 Python 脚本,杜老师习惯使用 Ubuntu 系统,不过在安装 Python 库时经常报错。收集了安装 Python 库的报错信息,并整理了解决办法,供需要的小伙伴们参考。

问题提示

这里以上一篇《使用 Python 脚本实现图片相似度匹配》文中代码为例,首次执行时的报错信息如下:

1 2 3 4 5

penn@penn-VMware-Virtual-Platform:~/图片$ python3 1.py Traceback (most recent call last): File "/home/penn/图片/1.py", line 4, in import imagehash ModuleNotFoundError: No module named 'imagehash'

根据报错信息,提醒找不到 imagehash 模块,使用 pip3 命令安装需要的模块,结果又出现了错误信息。这个信息表明正在尝试在一个由操作系统管理的 Python 环境中直接安装 Python 相关的包,为了保证系统 Python 环境的稳定性和安全性而采取限制措施:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

penn@penn-VMware-Virtual-Platform:~/图片$ pip3 install imagehash error: externally-managed-environment

× This environment is externally managed ╰─> To install Python packages system-wide, try apt install python3-xyz, where xyz is the package you are trying to install.

If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.

If you wish to install a non-Debian packaged Python application,
it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.

See /usr/share/doc/python3.13/README.venv for more information.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages. hint: See PEP 668 for the detailed specification.

解决方法

解决的方法有很多,这里推荐使用虚拟环境。因为使用虚拟环境可以避免直接修改系统的 Python 环境,同时方便管理依赖。按照提示创建一个虚拟环境,使用 python3 -m venv myenv 来创建虚拟环境,使用 source myenv/bin/activate 激活虚拟环境:

1 2

penn@penn-VMware-Virtual-Platform:~/图片$ python3 -m venv myenv penn@penn-VMware-Virtual-Platform:~/图片$ source myenv/bin/activate

在激活虚拟环境后,使用以下命令安装所需的包。安装完成后运行命令 deactivate,退出虚拟环境:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

(myenv) penn@penn-VMware-Virtual-Platform:~/图片$ pip install imagehash Collecting imagehash Downloading ImageHash-4.3.2-py2.py3-none-any.whl.metadata (8.4 kB) Collecting PyWavelets (from imagehash) Downloading pywavelets-1.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (9.0 kB) Collecting numpy (from imagehash) Downloading numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (62 kB) Collecting pillow (from imagehash) Downloading pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl.metadata (8.9 kB) Collecting scipy (from imagehash) Downloading scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB) Downloading ImageHash-4.3.2-py2.py3-none-any.whl (296 kB) Downloading numpy-2.2.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.1 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.1/16.1 MB 21.3 MB/s eta 0:00:00 Downloading pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl (4.6 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.6/4.6 MB 38.6 MB/s eta 0:00:00 Downloading pywavelets-1.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.5 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 38.6 MB/s eta 0:00:00 Downloading scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (37.3 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 37.3/37.3 MB 41.4 MB/s eta 0:00:00 Installing collected packages: pillow, numpy, scipy, PyWavelets, imagehash Successfully installed PyWavelets-1.8.0 imagehash-4.3.2 numpy-2.2.5 pillow-11.2.1 scipy-1.15.2


Menu
Main content

Bookmarks Settings Documentation

blog.hantaotao.top
22 September 2025
hantaotao.top

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

入坑骑行

入坑骑行

作者 小韩

发布于 2025-09-22

分类一世繁华

意欲晚上或周末兜风,兼具减去脂肪之功效,故计划入坑骑行。看到RC100价格不错(1359)且家旁边就有迪卡侬的店,故入手之。

周六车到之后,自行组装,周日中午看时间去店里让师傅做安全检查,并咨询了自己不太清楚的问题。车胎上充气建议气压为72~100psi;我用电动充气泵,充到65psi,就觉得气压已经很足了,担心这轮胎不会爆掉吧。店里师傅说,放心充,问题不大。且建议一周补气一次。

之后规划了一个线路,往返20km,权当试车了,感叹,这个天气真的很适合骑车,且RC100作为一款入门车型确实不错,基本能满足这个阶段的需求。 Screenshot_2025-09-22-00-12-35-116_com.onelap.bls.dear-min.webp


Menu
Main content

Bookmarks Settings Documentation

jiajunhuang.com
22 September 2025
jiajunhuang.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

用 Siri 来控制虚拟机开启和关闭 用 Siri 来控制虚拟机开启和关闭

我有下载专用的虚拟机和游戏专用的虚拟机,每次打开使用,都要通过 SSH 进去执行命令,或者用 cockpit 网页进行开关, 最近发现 iOS 快捷指令可以执行 SSH 命令,配置一个快捷指令,就可以通过 Siri 进行控制了。 控制脚本

!/bin/bash

检查是否提供了参数

if [ $# -ne 2 ]; then echo "用法: $0 <虚拟机名称>" exit 1 fi

OP="$1" VM_NAME="$2"

检查虚拟机是否存在

if ! sudo virsh dominfo "$VM_NAME" &>/dev/null; then echo "错误: 虚拟机 '$VM_NAME' 不存在" exit 2 fi

case "$OP" in "shutdown") echo "正在关闭虚拟机 '$VM_NAME'..." sudo virsh shutdown "$VM_NAME" ;; "start") echo "正在启动虚拟机 '$VM_NAME'..." sudo virsh start "$VM_NAME" ;; *) echo "虚拟机 '$VM_NAME' 处于未知状态: $VM_STATE" echo "请手动检查状态" exit 3 ;; esac

将文件内容保存到 /usr/local/bin/vmctl.sh,并设置执行权限 配置 sudo 免密执行该脚本

新增文件 /etc/sudoers.d/vmctl,内容如下:

允许用户无需密码执行vmctl

jiajun ALL=(ALL) NOPASSWD: /usr/local/bin/vmctl.sh *

配置快捷指令

在 Siri 设置中,点击左上角的添加按钮,搜索SSH,然后配置好用户名、IP、证书、命令,然后就可以通过Siri来控制虚拟机开启关闭, 非常方便。 相关文章

Golang log库 源码阅读与分析
毕业后一年
ansible 简明教程
自己写个搜索引擎
HTTP 路由的两种常见设计形式
Golang的short variable declaration
Greenlet和Stackless Python
写一个简单的ORM
从源码看Python的descriptor
Python字符串格式化
Gunicorn 简明教程
Raft 论文阅读笔记
什么是 Golang Comparable Types
GFS 论文阅读
MapReduce 论文阅读


DigitalOcean Referral Badge
邮件 订阅
RSS 订阅
Web开发简介系列
数据结构的实际使用
Golang 简明教程
Python 教程

socks5 协议详解
zerotier简明教程
搞定面试中的系统设计题
frp 源码阅读与分析():流程和概念
peewee代替SQLAlchemy
Golang(Go语言)中实现典型的fork调用
DNSCrypt简明教程
一个Gunicorn worker数量引发的血案
Golang validator使用教程
Docker组件介绍(二):shim, docker-init和docker-proxy
Docker组件介绍(一):runc和containerd
使用Go语言实现一个异步任务框架
协程(coroutine)简介 - 什么是协程?
SQLAlchemy简明教程
Golang的template(模板引擎)简明教程

Menu
Main content

Bookmarks Settings Documentation

www.uncleda.com
22 September 2025
uncleda.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

人生罗盘:一场穿透人生的思考

上一篇 幸福的方法:好好说话

下一篇 没有啦 (T▽T)

    当前路没人指引时,只有自己扛起大旗。

关于人生的困惑

我看了很多人生规划的书籍,包括布赖恩•费瑟斯通豪的《远见》、比尔·伯内特的 《斯坦福大学人生设计课》、王潇的 《按自己的意愿过一生》、塔拉·韦斯特弗的《你当像鸟飞往你的山》……等等,这些书给我很多启发,但实践的时候,却总会遇到各种各样的问题,总是觉得有些别扭。

实际践行的时候,我依然找不到自己。当我思考自己到底是谁?走向何处时,这些书也没能给自己答案。并且我的人生也总是反复出现一些大众难以避免的问题,例如:难以坚持一件事。过于在乎他人的感受,容易陷入内耗。我觉得自己活着,但好像已经死了,我甚至能看得见自己的未来。但是我心有不甘,不甘的情绪,在我的心里高昂呐喊,我希望过一个属于自己的人生。

有一个经典问题:为什么你懂得这么多道理,却依然过不好这一生?那是道理跟自己产生了链接,才会对自己的人生起作用。每一个人都有自己的特殊性,他们的道理也都带有场景性,只是恰好适合他们当时的情况,而不适合现在的你,道理的作用自然大打折扣,这就是为什么,他人的道理,实践起来特别别扭的原因。

到底,有没有一个通用模型,给我们指引?这是我这两年一直思考的问题。 普通人需要什么

他们需要的不是一个样例,而是一个客观的工具。曾经的首富马云是一个样例,马斯克是一个,刘强东也是一个,比尔盖茨也是一个,李嘉诚也是一个,这些都是很成功的人物。但他们的成功具有可复制性吗?曾经,我认为最具有复制性的,是近现代两个人物:稻盛和夫、曾国藩。因为他们的人生之道最为平实,他们也是从普通人崛起的。但现在想想,时代的大势确是不同了:你没有湘军,也没有京瓷。时势造英雄,我们要成为英雄,还有可能吗?

这也是千万像我这样普通人想要知道的答案。 人生不可或缺的要素

人生的道理很多,方法很多,但是,有没有存在一种可能,大道三千,有一些根本的,不可或缺的要素?而这些要素,恰好构成了一个基本的人生模型?如果存在这么一个模型,那就是我想要的答案。

在众多影响人生的要素当中,我找出了六个至关重要的要素,缺一不可,它们分别是:野心、谋略、境界、心力、行动、协作。我称之为“六维人生罗盘”。这六个维度,是普通人突破的钥匙。而其他要素,例如人脉、学历、能力、权力、信息等等,都是六维的衍生品。

在境界、心力、野心、谋略、行动、协作的“六维人生罗盘”中,我的设定是:

境界:衡量人的认知、思想、眼界等意识形态的等级与水平。决定人的上限。

心力:衡量人的内在能量水平,是高还是低,是消极还是积极。决定前进的动力水平。

野心:衡量人目标的清晰度,对成功及目标的渴望程度。决定人的决心和意志。

谋略:衡量人解决问题能力的水平,确保质效。决定一个人走得快还是慢。

行动:衡量人的行动力,执行力水平。决定一个人能走多远。

协作:衡量人的资源统筹能力,协作及交换能力。决定人生成就的规模。 未来的展望

这是最原始的模型,是人生操作系统的底座,任何人,都可以基于这个底座,添加属于自己的要素,来设计自己的人生系统。在本栏目接下来的文章中,我将基于这个框架,逐一把自己的想法写出来。并且在生活中践行,逐步修正。“六维人生罗盘”是我个人所思所得,也希望大家多多指正及交流。


Menu
Main content

Bookmarks Settings Documentation

justyy.com
22 September 2025
justyy.com
About 1 minute read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

如何通过二分查找搜索在区块链上根据时间戳定位区块?

前两天,我想查一下自己在 STEEM 区块链上一些重要记录对应的区块号,比如:

注册了我的账号 #4253590
成为见证人 #20190755
挖到我的第一个区块 #20390040
被孙宇晨大佬代理了 SP #41676911
收到一笔 DAO 收入 #95403889

当时手头只有时间戳,却不知道对应的是哪个区块,于是我想到可以用二分查找(binary search)算法来定位。

其实,这个思路在其它区块链上同样适用,并不依赖于特定的链(如 Steem、以太网/Ethereum、比特币/Bitcoin 等)。虽然具体的实现细节(RPC 方法名、时间戳格式等)会有所不同,但整体逻辑是一致的:通过 RPC API 获取某个区块的时间戳;获取最新区块作为上界;然后在区间内使用二分查找。通过多次查询区块时间戳,就能把给定的时间戳映射到最接近的区块号,算法复杂度约为 O(log N)。

当然,大多数区块链也会提供专门的 API 服务,可以直接通过时间返回区块号。我自己就跑过几个程序,从创世区块开始依次获取数据并写入数据库,这样查询时只需要查数据库就能立即得到结果。 可以使用二分搜索算法的场景

在一个搜索空间里,如果自变量 x 随着变化而单调递增或单调递减,那么函数值 f(x) 也会呈现出规律性的单调变化。这种情况下,就可以使用二分搜索算法来快速定位目标值或满足条件的区间。

二分搜索常见的应用场景包括:

在区块链上通过时间戳查找区块号(时间戳随区块号递增)。
在有序数组中查找目标值或边界。
在单调函数(如计算成本函数、概率函数)中寻找满足条件的阈值。
在优化问题中缩小答案区间,比如最小化某个代价函数。

总结来说,只要问题满足单调性,二分搜索就能在 O(log N) 的时间复杂度下快速求解,比线性扫描高效得多。 思路概述

通过 RPC 获取某个区块号的时间戳(或用索引器 API 直接查询)。
查询最新区块,作为二分查找的上边界。
在区间 [1, latest_block] 中做二分查找,比较中点区块的时间戳与目标时间戳,收窄区间直到找到最接近(或精确匹配)的区块号。

示例 Python 通用框架

下面给出一个通用的 Python 框架。实际使用时,请把 rpc_call 部分换成目标链对应的 RPC 方法或索引器 API 调用。

def rpc_call(method, params): """ 通用 RPC 调用函数示例。 根据你使用的链替换为 requests.post(...) 并解析返回。 """ pass

def get_timestamp_from_block(block_num): """ 返回给定区块号的时间戳(Unix 秒或 ISO8601 字符串),取决于链的返回格式。 需要把链的时间格式统一成可比较的 datetime / timestamp。 """ # 示例(伪代码): # resp = rpc_call("get_block", [block_num]) # ts = parse_timestamp(resp["timestamp"]) # return ts pass

def get_latest_block_and_timestamp(): """ 返回最新区块号和对应的时间戳,例如 (latest_block, latest_ts) 不同链可能有不同的 RPC 方法,如 get_block_count / get_chain_head / eth_blockNumber + eth_getBlockByNumber 等。 """ pass

def get_block_from_timestamp(target_ts): """ 在 [1, latest_block] 区间内使用二分查找定位与 target_ts 最接近的区块号。 返回找到的区块号(若需要,可返回左右两个候选块以便进一步选择)。 """ latest_block, latest_ts = get_latest_block_and_timestamp()

left, right = 1, latest_block
result = None

while left <= right:
    mid = (left + right) // 2
    mid_ts = get_timestamp_from_block(mid)

    if mid_ts < target_ts:
        left = mid + 1
        result = mid
    elif mid_ts > target_ts:
        right = mid - 1
    else:
        return mid  # 精确匹配

# 返回 result(它表示最后一个 timestamp < target_ts 的区块)
# 视需求,你可能还要检查 result+1 是否更接近 target_ts
return result

如何通过二分查找算法根据时间戳获取区块?

按STEEM区块链来举例。 获取区块的时间戳

通过调用 steemd 或其它区块链 API,我们可以轻松获取任意区块对应的时间戳。 找到最新区块

这一步为二分查找提供了一个上界。 对目标时间戳进行二分查找

与其线性扫描区块(会非常慢),不如在每一步将搜索区间减半。这样可以将复杂度降低到大约 O(log N),其中 N 是当前区块高度。这正是二分查找的魅力——它被认为是迄今为止人类发明的十大算法之一。 为什么这很重要?

如果你想把真实世界的事件(带有时间戳)映射到对应的区块号,这种方法高效而且不需要任何特殊的数据库权限——只需要公共 RPC 节点即可。

英文:How would you do Binary Search to Locate a Block given a TimeStamp (for Any Blockchain)? 本文一共 1136 个汉字, 你数一下对不对. 如何通过二分查找搜索在区块链上根据时间戳定位区块?. (AMP 移动加速版本) 赞赏我的几个理由. ¥ 打赏支持 扫描二维码,分享本文到微信朋友圈 75a5a60b9cac61e5c8c71a96e17f2d9c 如何通过二分查找搜索在区块链上根据时间戳定位区块? Python STEEM 区块链 二分搜索 Binary Search 区块链 学习笔记 数据结构与算法 程序设计 计算机 The post 如何通过二分查找搜索在区块链上根据时间戳定位区块? first appeared on 小赖子的英国生活和资讯. 相关文章:

测测你的幸运  Linux Fortune-Teller LINUX 下有很好很好玩的命令,之前已经介绍过: figlet, rig, curl. 现在推荐另一个 命令 fortune 是用来随机显示一段()话的.fortune 在英文里就是幸运的意思. 这个命令可以不需要 参数 如果没有 可以通过 apt-get...
你给SteemIt中文微信群拖后腿了么? 这年头不缺算法, 就缺数据. 这两天花了很多时间在整API上, 整完之后自己用了一下还觉得真是挺方便的. 今天就突然想看一看自己是否给大家拖后腿了, 于是调用每日中文区微信群排行榜单的API, 刷刷拿着 NodeJs 练手: // @justyy var request = require("request")...
第一次私校家长会: 原来家长比孩子还卷 前几天参加了娃的第一次家长会,和几位家长聊下来,真是个个都很厉害。不光孩子们卷,家长也一样卷,一眼望去基本都是 Dr/博士。娃还调侃我一句:“这有什么的,你不也是 Dr 吗?” 我心里默默想:还好没写学校名字,不然我这野鸡大学的头衔真拿不出手 😂。 私校里真是人才济济,乐器过 8 级的太常见了,卷得不得了。我还问过娃,是想当 big fish in a small pond...
按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了207500英镑的房子, 25年还清. 2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
给孩子零花钱培养孩子正确的金钱观价值观 两个娃已经不知不觉78岁了. 媳妇和我商量一下决定给孩子每人每周5英镑的零花钱(Pocket Money). 这样他们慢慢的就有自己的小积蓄备将来不时之需: 比如朋友聚会生日啥的需要准备礼物. 同时, 我们决定不再给孩子买零食(薯片啥的). 孩子一天好几餐, 晚上睡觉前还得吃零食, 我们就多买了很多水果面包, 健康的食物多吃一些总不是啥坏事. 孩子可以用这些零钱买自己想要的东西, 我们也不再过问. 孩子有自己的决定权. 第一周的时候,...
负电价活久见: 安装Octopus智能电表省电费甚至赚钱 前几周我的电气公司 Octopus 终于来装智能电表了(Smart Meter),虽然是免费安装的,但是排队排了有两三年了吧。因为之前一直写邮件催的时候就老是说 Not Ready 收到邮件说可以安装智能电表我还是相当开心和期待的,因为已经听说这玩意好,但是还是得亲身体验一下。工程师来安装大概不到2小时,其中需要停电闸一会儿,重新接下线。装好后,给了个小册子,自动切换到了 Agile 的电价,也就是每半小时的电价都不一样,提前一天可以在手机App和网站上查得 正好在原来的电价计费合同快要结束前2天换到了智能电表计价 Octopus Agile方式,但是系统还是扣了我75英镑 Exit Fee (提前合同结束得交违约费),不过我一个电话打过去,公司很爽快就给我退了。...
智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
HPZ800服务器主板太老不支持超过2TB的大硬盘 我家里一直用的是HPZ800服务器, 很吵, 很老, 虽然这台服务器已经有十年之久(我在EBAY上买来用了五年多了), 但是即使放到今天, 这服务器速度依旧很快, 很稳定. 由于服务器用的是ECC较验内存, 所以基本上不重启关机. HPZ800主机有两个硬核CPU  因特志强 X5650  每个CPU是12核....

周六好,今天算是心情像过山车一样的一天。

早上 7:45 惊醒了,以为又睡过了,缓过来才想起来周末送室友出门也得是 8:15 以后,还好来得及。索性不紧不慢地在床上刷了会儿手机,8:00 准时起床。

今天的饮食计划准备极端一些,最近网上很火的一个「大虾毛豆减肥法」,主要的做法是白天吃一斤半左右的水煮虾,晚上吃半斤水煮毛豆,我已经尝试过一两次了。其实这个方法,看起来比较极端,但也是有一些原理在里面的。

讲到这里还是讲一下这个方法的原理,这个方法的最重要一点是通过极低的碳水化合物摄入,导致糖原和水分快速流失。我们身体摄入的碳水化合物会转化成糖原,储存在肝脏和肌肉中,作为随时可用的能量。身体每储存 1 克糖原,就需要绑定约 3-4 克的水分。一个成年人体内大约可以储存 500 克的糖原,这意味着同时也储存了约 1500-2000 克(即 1.5-2.0 公斤)的水。

大虾和毛豆这两种食物都属于高蛋白、低碳水的食物。当我们一整天只吃它们时,身体几乎没有获得碳水化合物的补充。为了维持正常的生理活动,身体会开始消耗储存的糖原。当糖原被分解供能时,与之绑定的那 3-4 倍的水分就会被释放出来,然后通过尿液排出体外。简单来说,通过这个方法吃一天,会有比较明显的减重效果,减掉的大部分是糖原耗尽后身体排出的水分。

所以这个方法原则上并不减脂,但是可以减重,但我用它的目的并不是为了减重,而是为了定期给身体做一次净化。讲清楚原理,是希望大家了解事情的本质,不要盲目跟从网上的一些说它特别管用的帖子。

在吃水煮虾的时候,看了两集《康熙来了》,当然也是讲美食的那几期。

2015.10.29康熙來了 揭開天后小S的美食地圖Ⅰ
2015.10.30康熙來了 揭開天后小S的美食地圖Ⅱ
2015.10.12康熙來了 大學學區銅板美食大推薦 
2015.10.13康熙來了 大學學區銅板美食大推薦Ⅱ

基本上这几期都是我吃饭时候经常回顾的康熙经典集数,有胡天兰参与录制的《康熙来了》我都很爱回顾。说到这里,前几天和一个新认识的创作者聊到康熙,大家喜爱的嘉宾其实都差不多。我最喜欢的三个康熙通告艺人是:曹西平、曲家瑞和胡天兰。剩下的不分先后:沈玉琳、潘若迪(他真的太像席琳迪翁了)、刘恭显、罗霈颖等。赵正平我其实比较一般,主要是小 S 骂他比较好笑。

之后就到了回笼觉时间,从 11 点躺下,睡了不到两个小时,出了好多汗。起床之后休整了一下,大概下午 3 点多出门了,还是去上周说过的那个还不错的园区,上次发现这个园区有快充和慢充两种充电桩,正好在休闲期间给车子补能。有充电桩的园区都是好园区!


Menu
Main content

Bookmarks Settings Documentation

blog.bxaw.name
22 September 2025
bxaw.name

Labels

new_blog_den

Highlights

整个行业就是一坨烂摊子。连我自己都快分不清是我在装修房子,还是房子在装修我。

装修,就是一场渡劫

这几个月我一直在忙装修,每天被搞得焦头烂额,以前我还天真地以为装修就是挑挑家具、刷刷颜色,结果真正下水后才发现,自己不是在盖一个家,而是在渡一场劫。

装修的第一课,就是「没人会比你更关心你的家」。装修公司也好,设计师也好,工长也好,监理也好,他们从合同层面看,只要履行义务就好;从个人角度看,完成业绩和考核就行。至于你未来两三年住得舒不舒服、有没有安全隐患,抱歉,没人真正会放在心上。换句话说,只要「面子工程」做得够漂亮,里子就随它去吧。

所谓渡劫,不只是钱花得多,也不是琐碎事情多,而是那种「没人会替你真正负责」的无助感。除了我自己,没有任何一个人会为我的家承担全责。无论是号称全案的整装公司,还是设计师、管家(工长)、监理,乃至每天在工地干活的工人,他们看似都站在业主一边,但实际上,不论公司还是个人,心里盘算的都是业绩和考核。只要没踩红线、没违反公司规定,或者就算违反了只要查不到,他们绝对不会替业主多想半步。到最后,能保证的只是面子工程过得去,至于一两年后会不会渗水、三五年后电路有没有隐患,没人管。

我一开始还挺「自大」的,想着既然没人替我负责,那我就靠自己吧。于是查阅了国家标准、行业标准、企业标准,学习各种所谓的「先进工艺」,自己绘图和渲染,把自己搞得像个专家,甚至还以为补充协议能堵住所有坑。可真等到开工,才发现看起来的「万无一失」,在装修这种大工程面前不过是一句笑话。合同写得再细,也有疏漏;图纸画得再精细,也有错位。稍有把控不住的地方,就只能忍耐。很多环节一旦把控不好,要么无法返工,要么返工代价极高。于是,我成了那个每天工地打卡的「监督狂魔」:每天从早到晚泡在工地,监督施工、查阅资料、亲自测量、确认工艺,尽量避免工种之间互相打架,恨不得长出八只眼睛。

可现实是,就算我这么拼,也挡不住各种状况百出。不同工种各扫门前雪,施工现场常常互相扯皮,谁都不愿吃亏。哪怕我投诉,公司也不过是换了个首席设计师,再指派一个看起来更负责的工长。但在他们眼里,这终究只是份工作,手里还有一堆项目,不可能把我的家面面俱到地盯死。于是我被迫接受一个现实:业主必须摆正心态,不能指望装修公司包办一切。如果选择全权托付,那就得准备忍受未来的隐患和瑕疵。如果想要一个真正可靠的家,那就只能把装修公司当助手,而把自己当真正的负责人。

最可笑的是,是那些所谓行业内自带光环的公司。什么「自有产业工人」,什么「720度智能监控」,听起来像黑科技加持。可实际上,工人只要找个监控拍不到的角落,依然能违规操作。更别说那些隐蔽工程,糊弄交工的手段千奇百怪。

拆除时,我得死盯着不能拆到承重墙;水电改造时,要提醒师傅不要打坏水管(结果还是打爆了)、不要开长横槽(工人宣称在公司看来86底盒自身的开槽不算开横槽)、不要切钢筋(角磨机没注意蹭到了钢筋还一直怕我看到)。各种施工要点和「坑」加起来,足有上千条。有时我以为工人应该知道某个「常识」,结果人家偏偏忽略了;有时我在现场忘了强调一句,又或者现场工人多到一扭头我就没盯住;有时我说了,但师傅太「认真」,干着干着反而忘了;甚至还有材料直到进场才发现材料达不到工艺要求,导致某些工艺根本做不出来。这种时候,各种不符合要求的地方就成了一个个「坑」,要返工就意味着一场「灾难性」的重来。

还有那些根本无法预防的坑:比如空调打孔,完全是个拆盲盒的游戏。即使事先论证再充分,钻头下去之前,会不会打到钢筋谁也没谱。墙体开槽时,更是意外不断出现:一刀下去发现是空心砖,整个窟窿像是个无底洞;非承重墙,开着开着凿出来粗壮的钢筋,管线被迫改道。偏偏换到别的地方开,又开到斜着的钢筋。最后只能赶紧给裸露的钢筋喷防锈漆,临时止损。

更别提那些没有任何保障的小公司了,整个行业就是一坨烂摊子。连我自己都快分不清是我在装修房子,还是房子在装修我。

让人火大的是,有的工人接其他工地的活甚至因为接私活,拖着不来返工,烂摊子迟迟不收拾,最后逼得管家不得不请领导施压。哪怕请了三方监理,哪怕公司内部号称层层考核,问题依然频频出现。有时候看着半拉子工程,我真有种冲动:「算了,这房子干脆不要了。」

这还没完,装修的终极大坑是公司可能直接死掉。住范儿暴雷的新闻就让我好几次出现梦魇。我从来没有如此希望一家企业健康长寿,只因为它一旦倒下,我的工地,我的家,就跟着一起「陪葬」。挺有意思,别人担心的是股市里的公司别倒闭,我担心的却是自己掏钱养活的公司别黄了。

装修这一路,让我看到了很多人的「人性」:工人不是「坏人」,但他们习惯了「差不多就行」;工长和设计师也不是「坏人」,但他们学会了「能交差就好」;而我,只能被迫学会「斤斤计较」。这必须争,每一次工艺、每一条钢筋、每一个窟窿都得死盯,否则未来摊在自己头上的麻烦,就是自己给未来日子埋下的暗雷。

于是,所谓的结果,并不是一个完美无瑕的家,而是一个「瑕疵可忍」的家。一个在混乱里崩溃又重建的人,在一次次绝望和自救之间,被迫练就了一身免疫力。渡劫从来不是神话里的雷电,而是工地上尘土飞扬的噪音、合同里密密麻麻的条款,以及一个业主在半夜里咬牙切齿的自嘲。以为自己在盖房子,其实是在盖心里的防线;以为装修的是家,其实修的是自己。

本文《装修,就是一场渡劫》版权由白熊阿丸所有,任何人未经本人允许禁止转载。 如已授权转载,转载请备注文章来源链接:https://blog.bxaw.name/archives/Renovation-a-Trial-by-Ordeal.html


Menu
Main content

Bookmarks Settings Documentation

blog.jiyu134.top
22 September 2025
jiyu134.top
About 9 minutes read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

二维码离线传输2.0

科技科技 二维码离线传输2.0 jiyu1342025-09-202025-09-20 前言

在今年2月份的时候,我有尝试使用二维码来离线传输图片,虽然这是一个不错的想法,但是在当时并没有取得成功(属于是一个差一点就成功)。 不过好在我又进行了一次尝试,我选择了另一个道路来完成这个项目。 并且,这个2.0版本得到了升级,现在,它不仅可以适用于图片,更覆盖所有文件。 出于篇幅问题,有些地方我就不放源代码了,如果想看我与AI的对话,可以看末尾的链接。

引用站外地址

实现网站

https://blog.jiyu134.top/QR_code_transmission.html 想法?

我本来计划沿用之前的python来完成,因为这种方法相较于C语言较简单,但是…在我换了电脑后,我不知道在旧电脑都放了什么库进去,而且原本的文件中有一些错误,包括像二维码导出问题,识别问题。 我对此头皮发麻,这些问题比较难以处理… 所以我在放弃的前一秒选择了另一个赛道——HTML 其实在使用它的时候我又在想,html可不可以实现我的想法?我起初有些害怕。但在我用AI生成的时候,居然成功了! 我很惊讶,赶忙对这个项目进行了跟进处理。 成功…了?

在和AI进行多次交涉之后,第一个版本出现了。 注:第一个版本是python 结构: 项目结构建议:

1 2 3 4 5 6 7 8 9 10

qr_file_transfer/ ├── app.py # Flask 主应用 ├── templates/ │ ├── index.html # 上传和展示页面 │ └── scanner.html # 扫描页面 (基础) ├── static/ │ └── styles.css # 样式文件 ├── uploads/ # 上传文件临时存储 ├── qr_codes/ # 生成的二维码图片存储 └── received_files/ # 接收端重组后的文件存储

源码

  1. app.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295

import os import base64 import hashlib import json import time import uuid from flask import Flask, request, render_template, send_file, jsonify, redirect, url_for import qrcode from cryptography.fernet import Fernet

app = Flask(name)

--- 配置 ---

UPLOAD_FOLDER = 'uploads' QR_CODE_FOLDER = 'qr_codes' RECEIVED_FOLDER = 'received_files' app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['QR_CODE_FOLDER'] = QR_CODE_FOLDER app.config['RECEIVED_FOLDER'] = RECEIVED_FOLDER

确保目录存在

os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(QR_CODE_FOLDER, exist_ok=True) os.makedirs(RECEIVED_FOLDER, exist_ok=True)

存储传输会话信息 {session_id: {'key': key, 'chunks': [...], 'metadata': {...}}}

transfer_sessions = {}

--- 辅助函数 ---

def calculate_sha256(file_path): """计算文件的 SHA-256 哈希值""" sha256_hash = hashlib.sha256() with open(file_path, "rb") as f: # Read and update hash string value in blocks of 4K for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest()

def split_data(data, chunk_size): """将字节数据分割成指定大小的块""" chunks = [] for i in range(0, len(data), chunk_size): chunks.append(data[i:i + chunk_size]) return chunks

def generate_qr_code(data_str, filename): """生成二维码并保存为文件""" qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(data_str) qr.make(fit=True)

img = qr.make_image(fill_color="black", back_color="white")
img.save(os.path.join(app.config['QR_CODE_FOLDER'], filename))

--- 路由 ---

@app.route('/') def index(): """主页 - 文件上传""" return render_template('index.html')

@app.route('/upload', methods=['POST']) def upload_file(): """处理文件上传和二维码生成""" if 'files' not in request.files: return "No file part", 400 files = request.files.getlist('files')

if not files or all(f.filename == '' for f in files):
    return "No selected file", 400

# 获取用户设置
try:
    chunk_size_kb = int(request.form.get('chunk_size', 100)) # 默认100KB
    display_speed = int(request.form.get('display_speed', 2)) # 默认2张/秒
except ValueError:
    return "Invalid input for chunk size or display speed", 400

chunk_size_bytes = chunk_size_kb * 1024
delay_ms = int(1000 / display_speed) if display_speed > 0 else 500

# 为本次传输创建唯一会话ID和加密密钥
session_id = str(uuid.uuid4())
key = Fernet.generate_key()
cipher_suite = Fernet(key)

uploaded_files_info = []
all_chunks_data = [] # 存储所有文件的加密块数据

# 1. 处理每个上传的文件
for file in files:
    if file:
        filename = file.filename
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)

        file_size = os.path.getsize(filepath)
        file_hash = calculate_sha256(filepath)
        file_type = file.content_type or 'unknown'

        # 读取文件内容并Base64编码
        with open(filepath, 'rb') as f:
            file_content = f.read()
        encoded_content = base64.b64encode(file_content).decode('utf-8')

        # 准备元数据
        metadata = {
            'filename': filename,
            'size': file_size,
            'type': file_type,
            'sha256': file_hash
        }

        # 将元数据和内容组合,然后加密
        data_to_encrypt = json.dumps({
            'metadata': metadata,
            'content': encoded_content
        }).encode('utf-8')

        encrypted_data = cipher_suite.encrypt(data_to_encrypt)

        # 分割加密后的数据
        chunks = split_data(encrypted_data, chunk_size_bytes)
        all_chunks_data.extend([(chunk, filename) for chunk in chunks]) # 保存块和文件名关联

        uploaded_files_info.append({
            'name': filename,
            'size': file_size,
            'hash': file_hash
        })

# 2. 为所有块创建二维码
qr_filenames = []
total_chunks = len(all_chunks_data)
for i, (chunk_data, filename) in enumerate(all_chunks_data):
    # 构造二维码数据:会话ID, 块索引, 总块数, 文件名标识, 加密块数据
    # 为了简化,我们将密钥也编码进去(第一个二维码)
    qr_data_dict = {
        'session_id': session_id,
        'index': i,
        'total': total_chunks,
        'filename': filename,
        'data': base64.b64encode(chunk_data).decode('utf-8')
    }

    # 第一个二维码包含解密密钥
    if i == 0:
        qr_data_dict['key'] = key.decode('utf-8')

    qr_data_str = json.dumps(qr_data_dict)
    qr_filename = f"qr_{session_id}_{i}.png"
    generate_qr_code(qr_data_str, qr_filename)
    qr_filenames.append(qr_filename)


# 3. 存储会话信息 (虽然本次简化了密钥分发,但保留结构)
transfer_sessions[session_id] = {
    'key': key.decode('utf-8'), # 存储为字符串
    'total_chunks': total_chunks,
    'chunks_received': {}, # {index: data}
    'files_info': uploaded_files_info
}

# 4. 渲染展示页面
return render_template('index.html', 
                       qr_files=qr_filenames, 
                       delay_ms=delay_ms,
                       session_id=session_id,
                       files_info=uploaded_files_info)

--- 接收端模拟 (简化版,实际需要前端JS扫描) ---

@app.route('/scanner') def scanner(): """扫描页面""" return render_template('scanner.html')

模拟接收端扫描并重组文件的API端点

@app.route('/receive_chunk', methods=['POST']) def receive_chunk(): """接收扫描到的二维码数据块 (模拟)""" data = request.json qr_data_str = data.get('qr_data')

if not qr_data_str:
    return jsonify({'status': 'error', 'message': 'No QR data received'}), 400

try:
    qr_data = json.loads(qr_data_str)
except json.JSONDecodeError:
    return jsonify({'status': 'error', 'message': 'Invalid QR data format'}), 400

session_id = qr_data.get('session_id')
index = qr_data.get('index')
total = qr_data.get('total')
filename = qr_data.get('filename')
encrypted_chunk_b64 = qr_data.get('data')
key_str = qr_data.get('key') # 可能来自第一个二维码

if not session_id or index is None or total is None or not encrypted_chunk_b64:
    return jsonify({'status': 'error', 'message': 'Missing data in QR code'}), 400

# 检查或创建会话
if session_id not in transfer_sessions:
    transfer_sessions[session_id] = {
        'key': key_str, # 从第一个二维码获取
        'total_chunks': total,
        'chunks_received': {},
        'files_info': {} # 简化处理
    }
elif key_str:
     transfer_sessions[session_id]['key'] = key_str # 更新密钥如果第一个二维码后扫到

session = transfer_sessions[session_id]

# 解密块
try:
    cipher_suite = Fernet(session['key'].encode('utf-8'))
    encrypted_chunk = base64.b64decode(encrypted_chunk_b64)
    decrypted_chunk = cipher_suite.decrypt(encrypted_chunk)

    # 解析解密后的内容
    chunk_data = json.loads(decrypted_chunk.decode('utf-8'))
    # 这里可以进一步处理 chunk_data['metadata']  chunk_data['content']

    # 存储解密后的内容或原始加密块取决于重组策略
    session['chunks_received'][index] = {
        'data': chunk_data, # 存储解密后的完整数据
        'filename': filename
    }

except Exception as e:
    return jsonify({'status': 'error', 'message': f'Decryption failed: {str(e)}'}), 500

# 检查是否所有块都已接收
if len(session['chunks_received']) == session['total_chunks']:
    # 重组文件
    try:
        # 按索引排序
        sorted_chunks = sorted(session['chunks_received'].items())

        # 假设只有一个文件或按filename分组处理
        # 这里简化为处理第一个文件
        if sorted_chunks:
            # 重新组装内容 (需要按文件名分组)
            files_content = {}
            for idx, chunk_info in sorted_chunks:
                fname = chunk_info['filename']
                content = chunk_info['data']['content'] # Base64内容
                if fname not in files_content:
                    files_content[fname] = []
                files_content[fname].append(content)

            for fname, parts in files_content.items():
                # Base64解码所有部分并合并
                full_base64_content = "".join(parts)
                file_content = base64.b64decode(full_base64_content)

                # 保存文件
                output_path = os.path.join(app.config['RECEIVED_FOLDER'], f"received_{fname}")
                with open(output_path, 'wb') as f:
                    f.write(file_content)

                # 可以在这里验证SHA256 (chunk_info['data']['metadata']['sha256'])

            # 清理会话
            del transfer_sessions[session_id]
            return jsonify({'status': 'success', 'message': 'File(s) received and saved.', 'download_url': url_for('download_file', filename=f"received_{list(files_content.keys())[0]}")})
        else:
            return jsonify({'status': 'error', 'message': 'No chunks to assemble'}), 500
    except Exception as e:
        return jsonify({'status': 'error', 'message': f'File assembly failed: {str(e)}'}), 500

return jsonify({'status': 'success', 'message': f'Chunk {index+1}/{total} received'})

@app.route('/download/') def download_file(filename): """提供下载重组后的文件""" try: return send_file(os.path.join(app.config['RECEIVED_FOLDER'], filename), as_attachment=True) except FileNotFoundError: return "File not found", 404

if name == 'main': app.run(debug=True)

  1. templates/index.html

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

QR Code File Transfer - Sender

QR Code File Transfer - Upload







{% if qr_files %}

File Information:

    {% for file in files_info %}
  • {{ file.name }} ({{ "%.2f" | format(file.size / 1024.0) }} KB, SHA256: {{ file.hash }})
  • {% endfor %}

Generated QR Codes:

{% for qr_file in qr_files %}
{% endfor %}

{% for qr_file in qr_files %} {% endfor %}

Auto-Play Slideshow:

Speed: {{ delay_ms }} ms per slide

{% endif %}
  1. templates/scanner.html

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150

QR Code File Transfer - Receiver

QR Code File Transfer - Receiver

Point your camera at the QR codes being displayed on the sender's screen.

Status: Waiting to start scanning...

Scanned: 0 / 0

Speed: 0 Kb/s

  1. static/styles.css

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

/ Slideshow container /

slideshow-container {

max-width: 1000px; position: relative; margin: auto; }

/ Hide the images by default / .mySlides { display: none; }

/ Next & previous buttons / .prev, .next { cursor: pointer; position: absolute; top: 50%; width: auto; padding: 16px; margin-top: -22px; color: white; font-weight: bold; font-size: 18px; transition: 0.6s ease; border-radius: 0 3px 3px 0; user-select: none; }

/ Position the "next button" to the right / .next { right: 0; border-radius: 3px 0 0 3px; }

/ On hover, add a black background color with a little bit see-through / .prev:hover, .next:hover { background-color: rgba(0,0,0,0.8); }

/ Caption text / .text { color: #f2f2f2; font-size: 15px; padding: 8px 12px; position: absolute; bottom: 8px; width: 100%; text-align: center; }

/ Number text (1/3 etc) / .numbertext { color: #f2f2f2; font-size: 12px; padding: 8px 12px; position: absolute; top: 0; }

/ The dots/bullets/indicators / .dot { cursor: pointer; height: 15px; width: 15px; margin: 0 2px; background-color: #bbb; border-radius: 50%; display: inline-block; transition: background-color 0.6s ease; }

.active, .dot:hover { background-color: #717171; }

/ Fading animation / .fade { animation-name: fade; animation-duration: 1.5s; }

@keyframes fade { from {opacity: .4} to {opacity: 1} }

实际上,这个代码有问题,而且问题还不小。 为了不拉长战线,我们直接跳到HTML的第一个版本:

我询问AI的问题: 如果将该项目写成可以放在github静态托管的全网页形式,生成的文件(包括但不限于二维码文件,扫描后结果图)放在缓存里,后期可供用户下载,又应该怎么写?请你输出完整代码,框架。以及实现方式

没想到AI还真的得到了我想初期达到的效果: 详细信息

核心技术:
    HTML/CSS/JavaScript: 核心前端技术。
    Web APIs:
        FileReader API: 读取用户上传的文件。
        Web Crypto API: 实现加密解密(我们将使用对称加密 AES-GCM 替代 ECC,因为 Web Crypto 对 ECC 的支持更偏向于密钥协商 ECDH)。
        getUserMedia API: 调用摄像头。
        Canvas API: 用于二维码生成和扫描。
        jsQR Library: 在浏览器中扫描二维码。
        qrious or kjua Library: 在浏览器中生成二维码。
        Cache API 或 IndexedDB: 存储生成的二维码图片和接收的文件(更推荐 IndexedDB,因为它更强大且持久)。
        Blob URL: 为存储在内存或 IndexedDB 中的文件创建临时下载链接。
    Web Workers (可选): 对于大文件的加密/解密或二维码生成,可以放到 Web Worker 中执行,避免阻塞主线程。
    模块化 (ES6 Modules): 组织代码,使其更清晰。

发送端 (Sender) 流程:
    用户选择文件(多个)。
    前端 JavaScript 读取文件,计算 SHA-256。
    将文件内容转换为 Base64。
    按用户指定的块大小分割 Base64 字符串。
    为本次传输生成一个随机的 AES-GCM 密钥。
    使用 AES-GCM 密钥加密每个数据块。
    按照文档格式拼接每个块的数据字符串(包含文件名、大小、类型、SHA256、索引、总数)。
    将密钥(用一个固定的“主密钥”加密,或者直接放在第一个二维码里)和第一个加密块的数据字符串组合,作为第一个二维码的内容。后续二维码只包含加密块数据字符串。
    使用 JavaScript 库(如 qrious)将每个数据字符串生成二维码图片(<canvas> 或 data:image URL)。
    将生成的二维码图片(或其数据)和原始文件信息存储到 IndexedDB。
    在网页上以幻灯片形式展示二维码。

接收端 (Receiver) 流程:
    用户点击“开始扫描”按钮,调用摄像头。
    使用 jsQR 持续扫描视频流中的二维码。
    解析扫描到的二维码数据。
    如果是第一个二维码,从中提取 AES-GCM 密钥(并解密,如果之前被加密)。
    使用 AES-GCM 密钥解密每个扫描到的块。
    解析解密后的数据,提取文件名、内容、索引等信息。
    将解密后的块按索引存储在内存或 IndexedDB 中。
    在 UI 上显示扫描进度(已扫描/总数)。
    计算并显示扫描速度。
    当所有块都接收完毕后:
        按索引顺序拼接所有块的 Base64 内容。
        对拼接后的 Base64 进行解码,得到原始文件的 ArrayBuffer 或 Blob。
        计算重组后文件的 SHA-256 并与元数据中的进行比对(可选,增加可靠性)。
        将重组后的文件 Blob 存储到 IndexedDB。
        在页面上提供一个下载链接(使用 URL.createObjectURL(blob))。

文件存储与下载:
    IndexedDB: 使用浏览器的 IndexedDB API 来持久化存储:
        生成的二维码图片数据 (Base64 或 Blob)。
        原始文件的元数据和 Base64 内容(发送端,用于重新生成)。
        接收端解密并重组后的文件 Blob。
    下载: 当用户需要下载文件时,从 IndexedDB 读取文件 Blob,然后使用 URL.createObjectURL(blob) 创建一个临时的下载链接。

其中,对称加密 AES-GCM是我之前提出的要求,因为我担心ECC加密在这里会有点过于复杂,所以我让AI挑了一个简单些的。 但是不得不说,这个问题也是有的。 它的项目结构如下

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

qr-file-transfer-frontend/ ├── index.html # 主页面 (包含发送和接收的入口) ├── sender.html # 发送端页面 ├── receiver.html # 接收端页面 ├── css/ │ └── styles.css # 样式文件 ├── js/ │ ├── config.js # 配置常量 │ ├── utils.js # 通用工具函数 (SHA256, Base64, 分块等) │ ├── crypto.js # 加密解密逻辑 (AES-GCM) │ ├── qrGenerator.js # 二维码生成逻辑 │ ├── qrScanner.js # 二维码扫描逻辑 │ ├── sender.js # 发送端主逻辑 │ ├── receiver.js # 接收端主逻辑 │ ├── storage.js # IndexedDB 存储逻辑 │ └── main.js # 主入口,路由或初始化 ├── lib/ │ ├── qrious.min.js # 二维码生成库 (或 kjua) │ └── jsQR.js # 二维码扫描库 └── README.md # 项目说明

成功…了吧?

在经过我连续的修改后,我这里直接搬出最终版本(2.0的) 二维码文件离线传输工具 (ECC):基于 secp256r1 椭圆曲线加密,纯前端实现 本来我没想让AI走ECC这条路的,但是它中途就跟忘了似的,又沿用了我一开始的要求… 但最终效果还是不错的。

如上所示是我使用566eeadfaea7b2048cc9c46e18a7e379e5f2b2365fadef11336602f7aab45c85这个私钥生成的二维码,这个私钥是我第一次测试使用的,看起来如此亲切… 如果你扫码后,会发现一堆不知道怎么处理的数据,这个就是加密后的数据。 虽然我并没有藏什么彩蛋,如果你专门去处理的话,你会得到这个图片: 总结

不管怎么样,这个项目算是成型了,至于其他的内容,还需要我再去研究。 最后,附上文件(密码:9oag): 点击此处查看头像头像

jiyu134

风带来了种子 时间使之发芽 原创二维码离线传输2.0

打赏作者

感谢你赐予我前进的力量

微信

微信
支付宝

支付宝
QQ

QQ

赞赏者名单

因为你们的支持让我意识到写文章的价值🙏

使用手机访问这篇文章

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 狼林鱼池! 科技4

公告

文章目录

1. 前言
2. 想法
3. 成功
4. 成功了吧
5. 总结

最近发布 二维码离线传输2.0二维码离线传输2.02025-09-20第一次去兽聚-绒萌世界第一次去兽聚-绒萌世界2025-07-16高考结束啦!(持续更新)高考结束啦!(持续更新)2025-06-11二维码离线传输图片(有点失败二维码离线传输图片(有点失败2025-02-10新年快乐...(有点迟)新年快乐...(有点迟)2025-02-03


Menu
Main content

Bookmarks Settings Documentation

xingwangzhe.fun
22 September 2025
xingwangzhe.fun
About 1 minute read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

数据库原理-关系数据 数据库原理-关系数据

发布时间: 2025-09-20 11:04:55

更新时间: 2025-09-20 13:29:35

🕒 阅读时间:6 min read 👀 阅读量:Loading...

💡

含有ai生成内容

AI整理文本 关系数据模型的形式化定义

关系数据模型(Relational Data Model)是基于集合论和关系代数的数据库模型,由 E.F. Codd 于 1970 年提出。它将数据组织为关系(Relation),每个关系是一个二维表,行称为元组(Tuple),列称为属性(Attribute)。 基本概念

域(Domain):域 D 是一个值的集合,所有值具有相同的数据类型。例如,整数域 D_int = {…, -1, 0, 1, …},字符串域 D_string = {所有字符串}。
属性(Attribute):属性 A 是关系中的一列,对应一个域 D。属性名唯一标识列。
元组(Tuple):元组 t 是关系中的一行,由 n 个值组成,每个值来自对应属性的域。形式化表示为 t = <v1, v2, …, vn>,其中 vi ∈ D_i。
关系(Relation):关系 R 是元组的集合,定义为 R ⊆ D1 × D2 × … × Dn,其中 n 是关系的度(Degree)。关系可以表示为二维表:
    行:元组。
    列:属性。 关系模式(Relation Schema):关系模式
    R
    (
    A
    1
    :
    D
    1
    ,
    A
    2
    :
    D
    2
    ,
    …
    ,
    A
    n
    :
    D
    n
    )
    R(A_1:D_1, A_2:D_2, \dots, A_n:D_n)
    R(A1​:D1​,A2​:D2​,…,An​:Dn​) 定义关系的结构,包括属性名和对应域。
关系实例(Relation Instance):关系在某一时刻的具体元组集合。

示例关系

考虑学生关系 Student(SID: int, Name: string, Age: int): SID Name Age 1 Alice 20 2 Bob 22 3 Carol 21

度:3(三个属性)。
基数:3(三个元组)。

关系的运算

关系运算基于关系代数(Relational Algebra),包括集合运算和专门运算。运算结果仍是关系,可以用表格表示。 集合关系运算

假设有两个关系 R 和 S,具有相同的属性(兼容)。

并(Union):R ∪ S = {t | t ∈ R ∨ t ∈ S}。合并两个关系的元组,去除重复。
    示例:学生表 A 和 B。

        A:
        SID Name
        1   Alice
        2   Bob

        B:
        SID Name
        2   Bob
        3   Carol

        A ∪ B:
        SID Name
        1   Alice
        2   Bob
        3   Carol

交(Intersection):R ∩ S = {t | t ∈ R ∧ t ∈ S}。取共同元组。
    示例:A ∩ B:
    SID Name
    2   Bob

差(Difference):R - S = {t | t ∈ R ∧ t ∉ S}。取 R 中不在 S 中的元组。
    示例:A - B:
    SID Name
    1   Alice

选择运算(Selection)

选择 σ 条件 ( R ) \sigma_{\text{条件}}(R) σ条件​(R) 从关系 R 中选择满足条件的元组。

形式化:
σ
F
(
R
)
=
{
t
∣
t
∈
R
∧
F
(
t
)
=
true
}
\sigma_F(R) = \{ t \mid t \in R \land F(t) = \text{true} \}
σF​(R)={t∣t∈R∧F(t)=true},其中 F 是条件(如 Age > 20)。
示例:σ_Age>20(Student)
    原表:
    SID Name    Age
    1   Alice   20
    2   Bob 22
    3   Carol   21
    结果:
    SID Name    Age
    2   Bob 22
    3   Carol   21

投影运算(Projection)

投影 π 属性列表 ( R ) \pi_{\text{属性列表}}(R) π属性列表​(R) 从关系 R 中选择指定属性,去除重复。

形式化:
π
A
1
,
A
2
,
…
,
A
k
(
R
)
=
{
t
[
A
1
,
A
2
,
…
,
A
k
]
∣
t
∈
R
}
\pi_{A_1, A_2, \dots, A_k}(R) = \{ t[A_1, A_2, \dots, A_k] \mid t \in R \}
πA1​,A2​,…,Ak​​(R)={t[A1​,A2​,…,Ak​]∣t∈R}。
示例:π_Name,Age(Student)
    原表:
    SID Name    Age
    1   Alice   20
    2   Bob 22
    3   Carol   21
    结果:
    Name    Age
    Alice   20
    Bob 22
    Carol   21

连接运算(Join)

连接将两个关系基于条件组合。

自然连接Natural Join):
R

S
=
{
t
[
r
]

t
[
s
]

t
[
r
]

R
,
t
[
s
]

S
,
t
[
r
]
[
A
]
=
t
[
s
]
[
A
]
 for common attributes 
A
}
R \bowtie S = \{ t[r] \cup t[s] \mid t[r] \in R, t[s] \in S, t[r][A] = t[s][A] \text{ for common attributes } A \}
RS={t[r]t[s]t[r]R,t[s]S,t[r][A]=t[s][A] for common attributes A}。

示例学生表 Student(SID, Name) 和成绩表 Grade(SID, Course, Score)
    Student:
    SID Name
    1   Alice
    2   Bob
    Grade:
    SID Course  Score
    1   Math    90
    2   Math    85
    Student  Grade:
    SID Name    Course  Score
    1   Alice   Math    90
    2   Bob Math    85

θ-连接
R

θ
S
=
{
t
[
r
]

t
[
s
]

t
[
r
]

R
,
t
[
s
]

S
,
θ
(
t
[
r
]
,
t
[
s
]
)
}
R \bowtie_\theta S = \{ t[r] \cup t[s] \mid t[r] \in R, t[s] \in S, \theta(t[r], t[s]) \}
RθS={t[r]t[s]t[r]R,t[s]S,θ(t[r],t[s])},θ 是比较条件

外连接左外连接
(
R

S
)
(R \ltimes S)
(RS)右外连接
(
R

S
)
(R \rtimes S)
(RS)全外连接
(
R

L
S
)
(R \bowtie_L S)
(RLS)保留不匹配的元组 null 填充

除运算(Division)

除 R ÷ S R \div S R÷S 基于 S 的属性划分 R。

形式化
R
÷
S
=
{
t


u

S
,
t

u

R
}
R \div S = \{ t \mid \forall u \in S, t \cup u \in R \}
R÷S={t∣∀uS,tuR}其中 S 的属性是 R 的子集
示例供应商表 Supplier(SID, Part) 和零件表 Part(PID)
    Supplier:
    SID Part
    1   A
    1   B
    2   A
    Part:
    Part
    A
    B
    Supplier ÷ Part:
    SID
    1

换名操作(Rename)

换名 ρ NewName ( R ) \rho_{\text{NewName}}(R) ρNewName​(R) 或 ρ A → B ( R ) \rho_{A \to B}(R) ρA→B​(R) 重命名关系或属性。

示例:ρ_StudentTable(Student) 将 Student 重命名为 StudentTable。
属性换名:ρ_SID→StudentID(Student) 将 SID 改为 StudentID。

关系的完整性约束

完整性约束确保数据库数据的正确性和一致性。 实体完整性(Entity Integrity)

定义:关系的主键不能为 null。每个元组的主键值必须唯一且非空。
原因:主键唯一标识元组,null 值会破坏唯一性。
示例:在 Student 表中,SID 为主键,不能有 null 或重复值。

参照完整性(Referential Integrity)

定义:外键的值必须匹配参照关系的主键,或为 null(如果允许)。
原因:维护关系间的引用一致性,避免悬空引用。
示例:Grade 表的 SID 外键必须存在于 Student 表的 SID 中。

用户定义完整性(User-Defined Integrity)

定义:根据应用需求定义的约束,如域约束、检查约束。
类型:
    域约束:属性值必须在域内(如 Age > 0)。
    检查约束:元组级条件(如 Score BETWEEN 0 AND 100)。
    断言:跨关系约束。
示例:Age 属性必须 >= 18。

数据库原理-关系数据

作者: xingwangzhe

本文链接: https://xingwangzhe.fun/posts/8ce31cb6

本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

上一篇 数据库原理-设计技巧

下一篇 计算机图形学-基本图形生成算法


Menu
Main content

Bookmarks Settings Documentation

write.laily.net
22 September 2025
laily.net

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

我的二次元回忆录

文章作者 周玄久

上次更新 2025-09-21

许可协议 CC BY-NC-ND 4.0

最近去逛了深圳的谷子店,参观了柯南的三十周年展,也玩了全职猎人的贪婪之岛实境解密游戏。 中二的灵魂又被激活了,来回忆下自己以前的中二时光(年纪大了确实越来越爱回忆了)。

ACG(Anime,Comics,Games)里面我主要是看看动漫,其它两类接触不多。能回忆的也主要就是动漫了。

印象中最小的时候看的动漫是“米老鼠”,“兔巴哥”这种,那应该是自己刚能看懂电视的年纪。

再到小学就开始看一些国内的动画片,应该大都是“动画城”和“大风车”里播放的吧。现在能想到的有《封神榜传奇》,《哪吒传奇》。 后面开始有卫星电视,记得是“华娱卫视(应该是这个名字吧)”和“星空卫视”放的动画片比较好看。这个时候主要看了《犬夜叉》、《名侦探柯南》、《全职猎人》、《火影忍者》、《圣斗士星失》等等。《犬夜叉》应该算是入宅作了。而且也是从这个时候开始就迷上了柯南,后面 qq 的头像就改成了一张新一的头像,然后这个头像一直用到现在。 刚去 B 站搜了下,有很多人是一样的童年啊。【全网最全】星空卫视播过的所有动画-哔哩哔哩】

这个接段里看的动漫出了犬夜叉其余都还在连载。 《犬夜叉》正篇很早就看完了,完结篇应该是在高三出的。 《柯南》一直到高中应该还在陆陆续续追,到大学就慢慢没怎么看了,只是偶尔看看剧场版,再到工作后就完全没看了。 《海贼王》算是看的最久的,前两年还一直在追,就是动漫进度太慢了,慢慢就没看了。 《全职猎人》就是永远的痛了,记得第一季看完应该是小学左右,当时小杰用“同行”卡来到了“金”身边,好像就结束了。 然后等到蚂蚁篇再播放就是大学的时候了。现在黑暗大陆篇又等了十几年了。 另外国漫有印象的是主要是《秦时明月》

一直到大学,才有更多空余的时间,按说这个阶段应该是看了不少动漫的,不过我现在马上能想到的只有《家庭教师》了。

然后大学毕业,开始工作,现在还有印象的是毕业不久看了《不良人》,这个今年还出新的了,整体来说质量还不错,他们还出了几个不错的作品,不过后面好像就砍掉了。

中间陆续看过《来自新世界》,《来自深渊》,这两部还有印象,主要是世界观比较特别。 然后还有一个我觉的小众的叫《奇诺之旅》,有段时间上班中午吃饭的时候就看它。

到近几年,看的主要是热门的国漫了。国漫的制作和剧情感觉都还行,但是对于人物的刻画感觉都很单薄,就是直接的打打杀杀。

虽然一直也还在追动漫,但是很少去过线下,印象中也就很多年前去逛过一次漫展,只是走马观花了一番,记不得太多内容了。最近去了几次深圳的次元小镇,新增了一些体验。一直觉的自己也应该能跟上现在年轻人的文化,直到 labubu 火的时候,才真的意识到,其实自己已经脱离很远了,开始理解不了它们的文化了。

然后陆陆续续了解到了一些谷子文化。这里很推荐这个视频(对于谷子文化的介绍比较全面了)。本来开始只是看看视频了解下,对谷子店没有特别的兴趣。然后正巧连着发生了几件事,最终决定去看看。

首先是偶然在小红书上看到了线下的 全职猎人贪婪之岛 活动。 0e826fc82eabd5c59a686fca3057d683.webp

前面也提到了,全职猎人算是自己最喜欢的动漫了,小时候就觉的里面这个游戏设计的很有意思,后面可以做成真实的游戏。这次竟然有人在线下做了。

然后是发现正好深圳有柯南三十周年的展览。近几年没有看柯南了。但是情怀还在,之前读初中,高中的时候是喜欢啊,自己还有一个本子记录了各类案件的侦破情况,案件里用到的毒药和原理都会专门批注。后面还自己去学了一些密码学的知识。

IMG_20250919_152436.webp

最后是在小红书上发现了有人在大中华楼上投新兰的灯光秀,当时正好七夕,看到大楼的投屏确实很震撼。

深圳的二次元浓度应该比其它一线城市弱不少。福田的二次元小镇应该算是最大的一个聚集地了,然后深圳湾好像还有一个小一点的,听一个卖周边的小哥说,那边好像主要是乙游和国漫,我还没去过。另外好像东门那边在改造一个更大的场地。

次元小镇主要就是地下的一层,大大小小分布着十几家店吧,都是各种周边。一些空的场地会有唱跳表演和直播。到周末时,还会有一些二次元爱好者在这里摆摊贩卖自己收集的周边。

这次去的柯南三十周年展览在旁边的楼里。 新一版的几组衣服都挺好看的。 image.png

image.png

image.png

展览参观完后专门去中心公园蹲了一下新兰的大中华大屏。感觉读书都很少有这样的中二行为。到公园后发现还挺多一样的柯南迷也在那里等着拍照的。 mmexport1756569164726.webp

后来第二次去就是玩贪婪之岛的游戏。这几张宣传图很棒。 image.png

游戏分三部分,第一部分是解谜,一大个房间里有很多的谜题(感觉这种形式和柯南联名更适合😂),解谜后得到印章,然后可以兑换游戏中的卡片。里面会出现两次猜拳大赛,和动漫里是类似的,连胜三局就可以拿到“真实之剑”卡片。第二部分也是解谜,不过多了两个地区可以探索,这两个环节里都可以通过印章去换卡片,然后去抢夺 npc 手里的稀有卡片。第三部分就是玩躲避球和答题,两个都比较简单。答题一共十道题,答对七道就可以得到“统治者的祝福”卡片。 整体游玩性还行,就是解谜时间太短了,很多谜题还来不及看时间就到了。 游戏结束后和动漫里一样,每个人可以从游戏里带出一张卡片,还是比较有纪念意义的。(还有“统治者的祝福”,“一坪的海岸线”,“风险骰子”得到了,但是没法带出来,太可惜了) image.png

整趟逛下来,感觉现在的二次元活动比以前丰富了很多,各种联名,展览。但是周边变化不是很大,像所谓的吧唧(我们以前叫勋章)和闪卡,和我们小时候的都差不多,只是现在的价格贵了不少。立牌以前到是少一点。然后就是盲合属性好严重,比如上面贪婪之岛的卡片,主要的卡片是 100 张,但是没有整套卖的,只有随机 5 张一袋的盲盒,35 块一盒。而且还有十几张是盲盒里没有的,必须去玩游戏从里面带出来。这样要集齐整套成本还是有点高的。 最开始以为自己喜欢的这些上个时代的动漫可能在现在的年轻人眼里已经过时了,但是不管是逛展览还是玩游戏,都遇到了很多更年轻的小朋友,虽然和我们当年从电视上看不一样,这些年轻的少年们通过更新的渠道依然和这些作品建立了联接。

虽然少年的时光总是会显得中二,可是也不得不承认正是这些过去构建了现在的自己,少年时不经意看过的动漫,逐渐滋养着我们慢慢长大的灵魂,并一直默默的影响着我们看待这个世界的方式。我们憧憬着主角们浪漫的爱情,英勇的战斗,奇幻的冒险。即使多年以后,我们已经被生活,工作压的喘不过气,但是看到这些儿时陪伴过我们的二次元朋友们,依然会热泪盈框。


Menu
Main content

Bookmarks Settings Documentation

xingwangzhe.fun
22 September 2025
xingwangzhe.fun
About 7 minutes read

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

计算机图形学-基本图形生成算法 计算机图形学-基本图形生成算法

发布时间: 2025-09-20 12:35:52

更新时间: 2025-09-20 13:29:36

🕒 阅读时间:22 min read 👀 阅读量:Loading... 第四章 基本图形生成算法 4.1 引言

问题提出 在光栅设备(如显示器、打印机)上如何生成基本二维图形(点、直线、圆、椭圆、多边形、字符等)?

图形生成(扫描转换) 将连续的几何描述转换为离散像素集合,即在数字设备上确定哪些像素最能逼近目标图形。

关键目标

保持图形逼真度
端点和轮廓准确
颜色与灰度均匀
算法执行效率高
支持绘制属性(颜色、线型、线宽等)

4.2 直线的扫描转换 4.2.1 数值微分法(DDA 算法)

算法原理

给定起点
(
x
0
,
y
0
)
(x_0, y_0)
(x0​,y0​) 和终点
(
x
1
,
y
1
)
(x_1, y_1)
(x1​,y1​),计算差分
d
x
=
x
1
−
x
0
dx = x_1 - x_0
dx=x1​−x0​,
d
y
=
y
1
−
y
0
dy = y_1 - y_0
dy=y1​−y0​
确定步数
N
=
max
⁡
(
∣
d
x
∣
,
∣
d
y
∣
)
N = \max(|dx|, |dy|)
N=max(∣dx∣,∣dy∣)
计算增量
i
n
c
X
=
d
x
/
N
incX = dx / N
incX=dx/N,
i
n
c
Y
=
d
y
/
N
incY = dy / N
incY=dy/N
从
(
x
,
y
)
=
(
x
0
,
y
0
)
(x, y) = (x_0, y_0)
(x,y)=(x0​,y0​) 开始,循环
N
N
N 次:
    绘制像素点
    (
    r
    o
    u
    n
    d
    (
    x
    )
    ,
    r
    o
    u
    n
    d
    (
    y
    )
    )
    (\mathrm{round}(x), \mathrm{round}(y))
    (round(x),round(y))
    x
    +
    =
    i
    n
    c
    X
    x += incX
    x+=incX,
    y
    +
    =
    i
    n
    c
    Y
    y += incY
    y+=incY

1

// DDA 算法实现

2

function drawLineDDA(x0, y0, x1, y1) {

3

// 1. 计算差分 dx 和 dy

4

let dx = x1 - x0, dy = y1 - y0;

5

// 2. 确定步数 steps 为 max(|dx|, |dy|)

6

let steps = Math.max(Math.abs(dx), Math.abs(dy));

7

// 3. 计算增量 incX = dx / steps, incY = dy / steps

8

let incX = dx / steps, incY = dy / steps;

9

// 4. 从起点开始,循环 steps 次,每次累加增量并绘制像素

10

let x = x0, y = y0;

11

for (let i = 0; i <= steps; i++) {

12

ctx.fillRect(Math.round(x), Math.round(y), 1, 1); // 绘制像素点

13

x += incX;

14

y += incY;

15

}

16

}

17

// 优点:简单直观,易实现

18

// 缺点:浮点运算,开销大

原理
    直线方程增量形式:以
    x
    x
    x 或
    y
    y
    y 为主方向,每步累加微分增量
    设起点
    (
    x
    0
    ,
    y
    0
    )
    (x_0, y_0)
    (x0​,y0​)、终点
    (
    x
    1
    ,
    y
    1
    )
    (x_1, y_1)
    (x1​,y1​),
    Δ
    x
    =
    x
    1
    −
    x
    0
    \Delta x = x_1 - x_0
    Δx=x1​−x0​,
    Δ
    y
    =
    y
    1
    −
    y
    0
    \Delta y = y_1 - y_0
    Δy=y1​−y0​
    取步数
    N
    =
    max
    ⁡
    (
    ∣
    Δ
    x
    ∣
    ,
    ∣
    Δ
    y
    ∣
    )
    N = \max(|\Delta x|, |\Delta y|)
    N=max(∣Δx∣,∣Δy∣)
    增量:
    x
    inc
    =
    Δ
    x
    N
    ,
    y
    inc
    =
    Δ
    y
    N
    x_{\text{inc}} = \frac{\Delta x}{N},\quad y_{\text{inc}} = \frac{\Delta y}{N}
    xinc​=NΔx​,yinc​=NΔy​
    从起点出发,每步
    x
    +
    =
    x
    inc
    x += x_{\text{inc}}
    x+=xinc​,
    y
    +
    =
    y
    inc
    y += y_{\text{inc}}
    y+=yinc​,并取整绘点

特点
    简单直观,易于理解与实现
    浮点运算开销大,不利于硬件实现

4.2.2 中点 Bresenham 算法

算法原理

当
∣
d
x
∣
≥
∣
d
y
∣
|dx| \geq |dy|
∣dx∣≥∣dy∣ 时,以
x
x
x 为主增量方向,初始误差
e
=
2
⋅
d
y
−
d
x
e = 2 \cdot dy - dx
e=2⋅dy−dx
每步
x
x
x 增 1,同时根据误差调整
y
y
y:
    如果
    e
    >
    0
    e > 0
    e>0,则
    y
    y
    y 增 1,误差
    e
    +
    =
    2
    ⋅
    (
    d
    y
    −
    d
    x
    )
    e += 2 \cdot (dy - dx)
    e+=2⋅(dy−dx)
    否则
    y
    y
    y 不变,误差
    e
    +
    =
    2
    ⋅
    d
    y
    e += 2 \cdot dy
    e+=2⋅dy
在每步绘制像素
(
x
,
y
)
(x, y)
(x,y)

1

// Bresenham 算法实现

2

function drawLineBresenham(x0, y0, x1, y1) {

3

// 1. 计算 dx 和 dy 的绝对值

4

let dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0);

5

// 2. 确定方向 sx 和 sy

6

let sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;

7

// 3. 初始化误差 err = dx - dy

8

let err = dx - dy;

9

// 4. 循环绘制像素,根据误差调整 x 和 y

10

let x = x0, y = y0;

11

while (true) {

12

ctx.fillRect(x, y, 1, 1); // 绘制像素

13

if (x === x1 && y === y1) break;

14

let e2 = 2 * err;

15

if (e2 > -dy) { err -= dy; x += sx; }

16

if (e2 < dx)  { err += dx; y += sy; }

17

}

18

}

19

// 5. 使用整数运算,避免浮点

20

// 优点:高效,纯整数运算

21

// 缺点:仅适用于 |dx| >= |dy| 的情况

原理
    利用中点判别思想选择下一个像素,保持整数运算
    以斜率
    m
    =
    Δ
    y
    Δ
    x
    ≤
    1
    m = \frac{\Delta y}{\Delta x} \leq 1
    m=ΔxΔy​≤1 的情形为例:
        判别函数
        d
        =
        f
        (
        x
        +
        1
        ,
        y
        +
        1
        2
        )
        =
        Δ
        y
        (
        x
        +
        1
        )
        −
        Δ
        x
        (
        y
        +
        1
        2
        )
        d = f(x+1, y+\frac{1}{2}) = \Delta y (x+1) - \Delta x (y+\frac{1}{2})
        d=f(x+1,y+21​)=Δy(x+1)−Δx(y+21​)
        初始
        d
        0
        =
        Δ
        y
        −
        1
        2
        Δ
        x
        d_0 = \Delta y - \frac{1}{2} \Delta x
        d0​=Δy−21​Δx
        若
        d
        <
        0
        d < 0
        d<0,则下一个像素为
        (
        x
        +
        1
        ,
        y
        )
        (x+1, y)
        (x+1,y),更新
        d
        +
        =
        Δ
        y
        d += \Delta y
        d+=Δy; 否则为
        (
        x
        +
        1
        ,
        y
        +
        1
        )
        (x+1, y+1)
        (x+1,y+1),更新
        d
        +
        =
        Δ
        y
        −
        Δ
        x
        d += \Delta y - \Delta x
        d+=Δy−Δx。

特点
    纯整数运算,无乘除法
    高效,适合硬件

4.2.3 改进 Bresenham 算法

在中点算法基础上,进一步将判别式常量化,避免半像素累加

令误差项
e
=
2
Δ
y
−
Δ
x
e = 2 \Delta y - \Delta x
e=2Δy−Δx

更新:
    若
    e
    <
    0
    e < 0
    e<0:
    e
    +
    =
    2
    Δ
    y
    e += 2 \Delta y
    e+=2Δy;
    否则:
    y
    +
    =
    1
    y += 1
    y+=1,
    e
    +
    =
    2
    (
    Δ
    y
    −
    Δ
    x
    )
    e += 2 (\Delta y - \Delta x)
    e+=2(Δy−Δx);
    每步
    x
    +
    =
    1
    x += 1
    x+=1

特点
    更简洁的整数累加方式
    广泛用于图形硬件

1

// 改进 Bresenham 算法实现

2

function drawLineImprovedBresenham(x0, y0, x1, y1) {

3

// 1. 初始化误差 e = 2*dy - dx

4

let dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0);

5

let sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;

6

let e = 2 * dy - dx;

7

// 2. 循环 dx 次,每次 x 增 1

8

let x = x0, y = y0;

9

for (let i = 0; i <= dx; i++) {

10

ctx.fillRect(x, y, 1, 1); // 绘制像素

11

// 3. 根据 e 判断是否调整 y

12

if (e < 0) {

13

  e += 2 * dy;

14

} else {

15

  y += sy;

16

  e += 2 * (dy - dx);

17

}

18

// 4. 更新误差 e

19

x += sx;

20

}

21

}

22

// 优点:更简洁的整数累加,广泛用于硬件

23

// 缺点:需要处理方向

4.5 多边形扫描转换与区域填充 4.5.1 多边形表示

顶点表示(Vertex List):存储顶点坐标和边连接顺序,适合几何变换。
边界表示(Edge List):存储边的端点信息,便于扫描线求交。

4.5.2 X-扫描线填充算法(Basic Scanline Fill)

算法步骤:

对多边形各边根据边的 ymin  ymax 构建边表Edge Table, ET)。
 ymin  ymax 逐行扫描维护有效边表Active Edge Table, AET)。
在每条扫描线 y
     ET[y] 中所有起始于 y 的边插入 AET按照当前 x 坐标升序排序
     AET 中删除所有 ymax == y 的边
     AET 中每条边根据当前 y 计算交点 x并更新边的 x 增量1/m)。
    将交点 x 值两两配对形成填充区间对区间内像素进行绘制
更新每条边的 x 交点 x += Δx/Δy

1

// 扫描线填充算法实现

2

function scanlineFill(poly) {

3

// 1. 构建边表 ET,按 ymin 分桶存储边信息

4

const ET = {};

5

for (let i = 0; i < poly.length; i++) {

6

const p1 = poly[i], p2 = poly[(i+1)%poly.length];

7

let ymin = Math.min(p1.y, p2.y), ymax = Math.max(p1.y, p2.y);

8

if (ymin === ymax) continue;

9

let x = (p1.y < p2.y) ? p1.x : p2.x;

10

let invSlope = (p2.x - p1.x) / (p2.y - p1.y);

11

if (!ET[ymin]) ET[ymin] = [];

12

ET[ymin].push({ymax, x, invSlope});

13

}

14

// 2. 初始化有效边表 AET

15

let AET = [];

16

const ymin = Math.min(...poly.map(p=>p.y));

17

const ymax = Math.max(...poly.map(p=>p.y));

18

// 3. 对每条扫描线 y:

19

for (let y = ymin; y <= ymax; y++) {

20

// - 插入新边到 AET

21

if (ET[y]) AET.push(...ET[y]);

22

// - 删除过期边

23

AET = AET.filter(edge => edge.ymax > y);

24

// - 计算交点并排序

25

const intersections = AET.map(edge => ({ x: edge.x })).sort((a,b) => a.x - b.x);

26

// - 两两配对填充区间

27

for (let i = 0; i < intersections.length; i += 2) {

28

  const xStart = Math.ceil(intersections[i].x);

29

  const xEnd = Math.floor(intersections[i+1].x);

30

  for (let x = xStart; x <= xEnd; x++) {

31

    ctx.fillRect(x, y, 1, 1); // 填充像素

32

  }

33

}

34

// - 更新边的 x 坐标

35

AET.forEach(edge => edge.x += edge.invSlope);

36

}

37

}

38

// 优点:高效,适合复杂多边形

39

// 缺点:需要预处理边表

4.3 圆的扫描转换

中点 Bresenham 画圆算法原理

初始点
(
x
,
y
)
=
(
0
,
R
)
(x, y) = (0, R)
(x,y)=(0,R),初始误差
d
=
1
−
R
d = 1 - R
d=1−R
每步沿
x
x
x 增 1,根据误差判断是否减
y
y
y:
    如果
    d
    <
    0
    d < 0
    d<0,则
    d
    +
    =
    2
    x
    +
    3
    d += 2x + 3
    d+=2x+3
    否则
    y
    −
    =
    1
    y -= 1
    y−=1,
    d
    +
    =
    2
    (
    x
    −
    y
    )
    +
    5
    d += 2(x - y) + 5
    d+=2(x−y)+5
在八分对称的 8 个位置绘制像素

1

// 中点 Bresenham 画圆算法实现

2

function drawCircleMidpoint(R, xc, yc) {

3

// 1. 初始化 x=0, y=R, d=1-R

4

let x = 0, y = R;

5

let d = 1 - R;

6

// 2. 绘制八分对称点

7

plotCirclePoints(xc, yc, x, y);

8

// 3. 循环直到 x >= y:

9

while (x < y) {

10

// - 根据 d 判断是否减 y

11

if (d < 0) {

12

  d += 2*x + 3;

13

} else {

14

  d += 2*(x - y) + 5;

15

  y--;

16

}

17

// - 更新 d

18

// - x++

19

x++;

20

// - 绘制对称点

21

plotCirclePoints(xc, yc, x, y);

22

}

23

}

24

function plotCirclePoints(xc, yc, x, y) {

25

const pts = [

26

[xc + x, yc + y], [xc - x, yc + y],

27

[xc + x, yc - y], [xc - x, yc - y],

28

[xc + y, yc + x], [xc - y, yc + x],

29

[xc + y, yc - x], [xc - y, yc - x]

30

];

31

pts.forEach(p => ctx.fillRect(p[0], p[1], 1, 1));

32

}

33

// 优点:纯整数运算,高效

34

// 缺点:需要对称映射

4.3.1 八分对称绘制

圆具有八次对称性,只需计算第一象限从
(
R
,
0
)
(R,0)
(R,0) 到
(
R
/
2
,
R
/
2
)
(R/\sqrt2,R/\sqrt2)
(R/2​,R/2​) 的像素,其他象限对称映射

4.3.2 简单方程法

利用圆的方程
x
2
+
y
2
=
R
2
x^2 + y^2 = R^2
x2+y2=R2 或极坐标参数:
x
=
R
cos
⁡
θ
,
y
=
R
sin
⁡
θ
x = R \cos \theta,\quad y = R \sin \theta
x=Rcosθ,y=Rsinθ
缺点:需调用三角函数或开方,计算量大

4.3.3 中点 Bresenham 画圆

判别函数:
d
=
f
(
x
+
1
,
y
−
1
2
)
=
(
x
+
1
)
2
+
(
y
−
1
2
)
2
−
R
2
d = f(x+1, y-\frac{1}{2}) = (x+1)^2 + (y-\frac{1}{2})^2 - R^2
d=f(x+1,y−21​)=(x+1)2+(y−21​)2−R2

初始
d
0
=
1
−
R
d_0 = 1 - R
d0​=1−R

每步:
    若
    d
    <
    0
    d < 0
    d<0:选择
    (
    x
    +
    1
    ,
    y
    )
    (x+1, y)
    (x+1,y),更新
    d
    +
    =
    2
    x
    +
    3
    d += 2x + 3
    d+=2x+3;
    否则:选择
    (
    x
    +
    1
    ,
    y
    −
    1
    )
    (x+1, y-1)
    (x+1,y−1),更新
    d
    +
    =
    2
    (
    x
    −
    y
    )
    +
    5
    d += 2(x - y) + 5
    d+=2(x−y)+5;
    同时
    x
    +
    =
    1
    x += 1
    x+=1,视情况
    y
    −
    =
    1
    y -= 1
    y−=1

特点:纯整数累加,效率高

4.4 椭圆的扫描转换

算法原理

椭圆标准方程:
x
2
a
2
+
y
2
b
2
=
1
\frac{x^2}{a^2} + \frac{y^2}{b^2} = 1
a2x2​+b2y2​=1,具有四象限对称性。
使用中点 Bresenham 思想,将绘制分为两个区域:
    区域1(斜率
    ∣
    d
    y
    /
    d
    x
    ∣
    ≤
    1
    |dy/dx| \leq 1
    ∣dy/dx∣≤1):以
    x
    x
    x 为主增量方向,判别函数
    d
    1
    =
    b
    2
    (
    x
    +
    1
    /
    2
    )
    2
    +
    a
    2
    (
    y
    −
    1
    )
    2
    −
    a
    2
    b
    2
    d_1 = b^2 (x + 1/2)^2 + a^2 (y - 1)^2 - a^2 b^2
    d1​=b2(x+1/2)2+a2(y−1)2−a2b2。
        若
        d
        1
        <
        0
        d_1 < 0
        d1​<0,仅
        x
        +
        +
        x++
        x++,更新
        d
        1
        +
        =
        b
        2
        (
        2
        x
        +
        1
        )
        d_1 += b^2 (2x + 1)
        d1​+=b2(2x+1);
        否则
        x
        +
        +
        x++
        x++,
        y
        −
        −
        y--
        y−−,更新
        d
        1
        +
        =
        b
        2
        (
        2
        x
        +
        1
        )
        +
        a
        2
        (
        −
        2
        y
        +
        1
        )
        d_1 += b^2 (2x + 1) + a^2 (-2y + 1)
        d1​+=b2(2x+1)+a2(−2y+1)。
    区域2(斜率
    ∣
    d
    y
    /
    d
    x
    ∣
    >
    1
    |dy/dx| > 1
    ∣dy/dx∣>1):以
    y
    y
    y 为主变化方向,判别函数
    d
    2
    =
    b
    2
    (
    x
    +
    1
    )
    2
    +
    a
    2
    (
    y
    −
    1
    /
    2
    )
    2
    −
    a
    2
    b
    2
    d_2 = b^2 (x + 1)^2 + a^2 (y - 1/2)^2 - a^2 b^2
    d2​=b2(x+1)2+a2(y−1/2)2−a2b2。
        若
        d
        2
        >
        0
        d_2 > 0
        d2​>0,仅
        y
        −
        −
        y--
        y−−,更新
        d
        2
        +
        =
        a
        2
        (
        −
        2
        y
        +
        1
        )
        d_2 += a^2 (-2y + 1)
        d2​+=a2(−2y+1);
        否则
        x
        +
        +
        x++
        x++,
        y
        −
        −
        y--
        y−−,更新
        d
        2
        +
        =
        b
        2
        (
        2
        x
        +
        2
        )
        +
        a
        2
        (
        −
        2
        y
        +
        1
        )
        d_2 += b^2 (2x + 2) + a^2 (-2y + 1)
        d2​+=b2(2x+2)+a2(−2y+1)。
在每一步中,基于八象限对称性,绘制点
(
±
x
c
±
x
,
±
y
c
±
y
)
(\pm x_c \pm x, \pm y_c \pm y)
(±xc​±x,±yc​±y)。

1

// 中点 Bresenham 椭圆算法实现

2

function drawEllipseMidpoint(a, b, xc, yc) {

3

// 1. 初始化 x=0, y=b, 计算 a2, b2

4

let x = 0, y = b;

5

let a2 = a * a, b2 = b * b;

6

let d1 = b2 - a2 * b + 0.25 * a2;

7

let dx = 2 * b2 * x, dy = 2 * a2 * y;

8

// 2. 区域1:斜率 <=1,以 x 为主

9

while (dx < dy) {

10

plotEllipsePoints(xc, yc, x, y);

11

// - 根据 d1 判断是否减 y

12

if (d1 < 0) {

13

  x++;

14

  dx += 2 * b2;

15

  d1 += dx + b2;

16

} else {

17

  x++; y--;

18

  dx += 2 * b2; dy -= 2 * a2;

19

  d1 += dx - dy + b2;

20

}

21

// - 更新 d1

22

}

23

// 3. 区域2:斜率 >1,以 y 为主

24

let d2 = b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2;

25

while (y >= 0) {

26

plotEllipsePoints(xc, yc, x, y);

27

// - 根据 d2 判断是否增 x

28

if (d2 > 0) {

29

  y--;

30

  dy -= 2 * a2;

31

  d2 += a2 - dy;

32

} else {

33

  x++; y--;

34

  dx += 2 * b2; dy -= 2 * a2;

35

  d2 += dx - dy + a2;

36

}

37

// - 更新 d2

38

}

39

}

40

function plotEllipsePoints(xc, yc, x, y) {

41

const pts = [

42

[xc + x, yc + y], [xc - x, yc + y],

43

[xc + x, yc - y], [xc - x, yc - y]

44

];

45

pts.forEach(p => ctx.fillRect(p[0], p[1], 1, 1));

46

}

47

// 4. 绘制四象限对称点

48

// 优点:整数运算,高效

49

// 缺点:复杂,需要两个区域

4.5 多边形扫描转换与区域填充 4.5.1 多边形表示

顶点表示(Vertex List):存储顶点坐标和边连接顺序
点阵表示(Bitmap):预先离散化边界,便于直接填充

4.5.2 X-扫描线填充

对每条扫描线与多边形各边求交点
将交点按 x 坐标排序,成对填充区间
需处理顶点共享与交点重合特殊情况

4.5.3 有效边表算法(AET)

边表(Edge Table,ET):按最小 y 值分桶存储所有边
有效边表(Active Edge Table,AET):当前扫描线可见的边,按 x 递增排序
算法流程:
    初始化 ET 和空 AET
    对每条扫描线 y:
        将 ET 中 y 为起点的边插入 AET
        从 AET 中删除扫描线终点已过的边
        对 AET 中每条边计算交点 x,并按 x 排序
        两两配对填充区间像素
        更新每条边的 x 增量(
        x
        +
        =
        1
        /
        m
        x += 1/m
        x+=1/m)

4.5.4 区域填充算法

边界填充(Edge Flag):遇到边界像素时切换填充状态
种子填充(Seed Fill):从种子点扩展,递归或栈方式,四连通或八连通
扫描线种子填充:按行扩展,减少栈深度和重复访问

4.5.5 内外测试规则

奇偶规则(Even–Odd Rule):交点数为奇则内部
非零环绕规则(Nonzero Winding Number):环绕计数非零则内部

4.6 字符处理 字符编码

ASCII(英文字符、控制码)
GB2312/GBK/UTF-8(中文及多语言)

字库类型

点阵字库(Bitmap Font):按像素矩阵存储,简单快速但占用存储
矢量字库(Vector Font):用线段、曲线描述笔画,支持缩放与变换

4.7 属性处理 线型与线宽

线型:用像素模板(如 11110000)控制虚线、点划线
线宽:
    刷子(Brush)算法:用矩形或圆形刷子覆盖线条
    区域填充:对扩展后的多边形边界进行填充

字符属性

字体、字号、颜色、对齐方式(左/居中/右对齐)

区域填充属性

填充颜色、图案、透明度
图案对齐:全局对齐或局部对齐

4.8 反走样(Anti-Aliasing) 走样现象

锯齿、波纹、闪烁、细节丢失

反走样方法

提高分辨率(Superresolution)
超采样(Supersampling):在高分辨率下采样并平均
区域采样(Area Sampling):按像素与图形覆盖区域比例加权
过滤器(Filter)方法:应用低通滤波器平滑边缘

计算机图形学-基本图形生成算法

作者: xingwangzhe

本文链接: https://xingwangzhe.fun/posts/10deed37

本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

上一篇 数据库原理-关系数据

下一篇 没有更新的文章


Menu
Main content

Bookmarks Settings Documentation

onojyun.com
20 September 2025
onojyun.com

Labels

new_blog_den

Highlights

Start selecting text in the article to create a new highlight. Links

好好吃饭,别东看西看!

Humachine 里整理的是原本发布在 Telegram 频道的内容,积累到足够的时候,就可以分类整理成册。我并不是个注意力集中的人,所以我在吃饭、工作的时候,还会分出一个线程观察周围正在发生的事情,比如我总是在吃饭的时候发现某一桌食客是在「搞破鞋」,那今天就来汇总一下各种有趣的「观察」。 中年男人的悲哀

昨晚家里吃饭,观察到家里一个亲戚的死亡焦虑越来越严重了。他以前是个吃喝嫖赌样样都来的男人,他的女儿,也就是我妹妹初中的时候,他就已经跟他妻子分房睡觉了。他们两个婚姻早就名存实亡了,但是妻子为了维系这种表面的幸福,打死都不离婚。每年这样的饭局,就能看到两个人极尽可能地表演给所有人看。

之所以说这个亲戚的死亡焦虑越来越严重,是他的人生已经完全被禁锢。他在抱怨自己的一举一动都被自己的女儿和妻子监视,比如汽车启动、进入哪个停车场都会被女儿知道;妻子则通过高速公路ETC扣费的方式确定他去了什么地方。中年的亲戚,嫖是嫖不动了,其他的事情也开始无法对抗他的死亡焦虑,他便在这两年之内迅速地变成了一个「老粉红」,把自己的个体付诸于乌合之众,以获得超越个体的强大自豪感。

他混了一个大学政史老师职位,彻底放弃了自己原本还能吃喝嫖赌的个体。在这种当爹的乐趣中,对抗着他越发恐惧的死亡焦虑。

据我所知,他并没有那些模拟阳具崇拜的钓鱼爱好,也没有模拟射精的重骑爱好,他的男性能量已经完完全全跟国家荣誉挂钩了。 拆冰箱的二八法则

家里来了回收冰箱的工人,因为是淘宝下单,是需要我们付 60 块钱的粗大垃圾处理费,他们可以免费拿走一个还完好的冰箱。所以呢,他们开始了折腾冰箱如何出厨房的事情。

冰箱一开始无法顺利搬出厨房,于是他告诉我如果厨房门不拆,冰箱就搬不出去。

我:冰箱门不是可以拆吗?不然这是怎么搬进去的。

他不得不下楼重新拿工具,等拆了冰箱门发现还是出不去,便又开始抱怨让我主动取消订单。我看了一眼就发现了卡住的地方,其实并不是冰箱门,而是冰箱脚的支架。于是他开始拆卸支架,拆了一边又着急往外搬,又卡得死死的。他转过头开始朝我无能狂怒。

我:另一个脚不是还没有卸下来?

终于,另一只腿在他的无能狂怒和对我好几次的威胁自己处理之后,轻松搬出了冰箱,甚至其实冰箱门都不用拆卸。

这就是一件事 20% 的关键问题,和 80% 的蛮干找不到症结的区别。 表演天赋

早上下楼遛狗,看到一对母女进电梯,按道理来说 8 点半是上学的时间,但是小女孩并没有去上学。电梯广告在播放蜜雪冰城的广告,她也跟着在唱「你爱我我爱你~」感觉很开心,这个时候她妈非常冷漠地嘲讽了一句:「你不是在生病吗,请假上课就病好啦。」然后小女孩立马切换了表情开始唉声叹气。 争取做中国市场上不了台面但是下得了地狱的漫才组合!

因为职业关系,我跟老婆很会观察人,会是哪种程度呢?就是会在咖啡厅跟客户聊工作同时,可以多线程接收信息,很快察觉到身边哪一桌正在「搞破鞋」的能力。

于是,今天又「来活」了。

在高铁站碰到一对坐商务座的男女,女的自己拖着大箱子,男的背着双肩背包,两个的长相极不匹配。但是呢,他们又在进站的时候男的毛手毛脚,各种摸女的臀部、后背等等。问他们是最有可能是什么关系?

为了复盘我们的猜测,我们甚至动用了游走在商 K 领域的小袁,了解了一下杭州市场空降天使的行业生态模式。(比如这种自己拖箱子的外围,都是换装和道具天使)

最终我们用漫才结构做出了总结:好不容易做到了 P7,出差找个伴游嫖一嫖恭喜自己晋升。 孩子的配料表

我厌恶小孩的大部分原因来自于他们是纯的非理性,如果有理性,他们长大了也多半是一个很难接近的人「怪人」。

这类型的怪人和「把自己伪装成很怪以至于跟别人不一样的怪人」的最大区别,在于他们需不需要「被关注」。从小理性过早发育大脑的孩子,他们活在自己的世界就已经完成了自洽,但是认为自己独一无二的「怪人」,如果没有对比和被看见,他们长时间在自己的世界寻找存在感,就必然会陷入「溺水」状态。

比如我现在候车厅里,就看到了一个过早发育了理性的小孩子,他表现出了对外界强烈的「没兴趣」,他唯一有兴趣的是在对比手里两个不同零食的配料表,把他发现的不同告诉妈妈,但是拿着手机毫不关注孩子的妈妈显然对他没有兴趣,只是评价到:你吃不吃,不吃就放回去。

小孩子没说什么,又去拿了一个新的零食,对比着配料表,自己有说有笑地找着相同点。 今日咖啡厅观察实录

一个高中生假借来咖啡厅学习的名义,和一个姐姐在谈恋爱,姐姐可能有 25、6 岁,姐姐说中午带弟弟去吃火锅,弟弟问下午能不能去姐姐家;
一个社畜在开视频会议,他可能是为了装逼自己有个体面的工作,一开始在公放,结果会议那边开始在骂他的工作态度问题,他就立马戴上了耳机不敢公放会议了;
一个阿姨在面试年轻人,年轻人访反客为主,问公司能提供什么,阿姨说公司有下午茶、有定期的出行福利,年轻人说「这些东西能换算成钱吗,我上一家公司也这样说」;
一个男的因为手机被老婆拿走了,在去茶山找工作人员借手机,但是工作人员非常警惕,最后男的表示可以让对方帮忙拨打电话,然后放在吧台上面免提打电话,然后那边得知是自己丈夫打电话来时,问:「这是哪个女人的手机?你跟谁在一起的?」;

别挡着我孔雀开屏

成都阿姨的雌竞吧,是藏不住的。比如一个阿姨建议自己的两个小姐妹点醉蟹来吃,见她俩默默接受并没有问出「重点」,阿姨又在点菜的时候赶紧补充道:「给她们点两只醉蟹,我开了车吃不了醉蟹的。」

于是三个人在整个吃饭的过程中,都在互相「抢话筒」,一个人刚开启的话题,明明在说自己,都会被另一个人抢走话筒,变成聊自己的话题。吃醉蟹的阿姨也不服输,在吃醉蟹的时候不断贬低这家餐厅的醉蟹并强调自己「在上海」吃过更正宗的,另一个阿姨也不服输,突然开始抱怨头顶发热空调很冷(明明已经坐了20分钟了),从自己包里掏出了一条爱马仕的纱巾,开始在脖子装饰起来。

不过呢,她们在这样的雌竞关系里也达到了微妙的平衡,她们互不在乎对方是否在看自己,她们只关心此时此刻的当下,有没有人挡着自己孔雀开屏。 你猜这是一个怎样的「世家」?

另一个家庭呢,是一个爸爸带着女儿,以及他的父母,以及父母的老母亲。整个过程,爷爷奶奶抱着孙女分钟宠溺,爸爸倒是一脸淡定地吃饭,老奶奶一言不发只顾吃饭。

爷爷奶奶对孙女的宠爱已经到了让人烦躁的程度,他们为了照顾孙女开始完全控制周围、甚至的他人领域的一切,比如她不允许我们我们桌旁边有空位,必须把椅子撤走,因为他们想要抱着孙女在场域里来回走哄着她,场域里有空位会导致他们行动不便,他们还逼着餐厅服务员当着他们的面消毒宝宝椅。爷爷对孙女的控制,则是要求她的每一个行为都是符合他的要求的,必须每一口饭先吃什么就吃什么、几口吃完,奶奶在孙女不耐放的时候,就立马解救她,又开始各种宠溺。就这样精神分裂地照顾着自己孙女,她爸爸呢,全程无视两个老人,当孙女在爷爷的管辖下时,男人的妈妈开始控制自己的儿子,给自己的儿子和母亲夹菜、分菜,然后又碎碎念开始解释要给孙女吃什么,她儿子实在受不了了,对自己妈说道:「你要自己吃就吃,不用每句话都解释你要做什么。你孙女也听不懂你要做什么。」妈妈倒是没被打击到,又去从丈夫手上「抢回」孙女,开始给她解释自己为什么要给她夹这个菜,一会吃了会怎么样……

这是一个极具精神分裂的家庭,有一种儿子已经对抗不了了,干脆生个女儿交给他们,所以才呈现出了他对这个女儿的极度「漠视」。


菜单
主要内容

书签 设置 文档

dev.to
17 九月 2025

dev.to
阅读约 6 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Automated Visual Testing with Selenium & Applitools

In the fast-paced world of web and mobile application development, delivering a seamless user experience is just as important as ensuring functional correctness. While traditional functional testing focuses on verifying if buttons work, forms submit correctly, and pages load as expected, it often overlooks a crucial aspect — how things look on the screen. This is where visual testing steps in.

Visual testing goes beyond checking functionality; it ensures that the application’s user interface (UI) appears as intended. It helps detect visual bugs like misaligned elements, overlapping text, incorrect fonts, broken layouts, or color inconsistencies — issues that functional tests usually miss.

Imagine a scenario where a “Submit” button works perfectly, but due to a CSS update, it’s now hidden behind an image. Functional tests would pass since the button technically works, but a real user would still face a broken experience. Visual testing catches these UI glitches by comparing screenshots of the application against a baseline (expected design) and highlighting even the smallest visual differences.

With modern applications being responsive — adapting to different devices and screen sizes — visual validation becomes even more essential. It ensures a consistent and polished look across desktops, tablets, and mobile devices. As users expect pixel-perfect interfaces, integrating visual testing into your test automation strategy helps prevent visual regressions and strengthens the overall quality of your software. What is Visual Testing?

Visual testing is a type of software testing that ensures the UI (User Interface) of an application appears correctly to users across different browsers, devices, and screen sizes. Unlike functional testing, which verifies the logic and behavior of an application, visual testing focuses on identifying visual defects such as misaligned elements, font inconsistencies, broken layouts, or color mismatches.

For example, if a button shifts slightly due to a CSS change, functional tests may still pass, but visual tests will detect the discrepancy. Why Traditional Functional Testing Misses Visual Bugs?

Functional testing tools like Selenium, Cypress, and Playwright are excellent at verifying functionality—ensuring that buttons work, forms submit correctly, and calculations return expected results. However, they do not check how elements are displayed on the screen.

Here are a few limitations of functional testing in detecting UI issues:

Pixel-level UI issues go unnoticed (e.g., overlapping text, incorrect font sizes).
Cross-browser inconsistencies are not caught (e.g., a layout breaking in Safari but working fine in Chrome).
Dynamic content variations (ads, pop-ups) can disrupt layouts, but functional tests wont detect these subtle shifts.
Responsiveness issues (e.g., a menu displaying incorrectly on a smaller screen) are ignored unless explicitly coded into tests.
Since functional testing tools focus on verifying DOM elements and interactions, they miss visual defects that can degrade the user experience.

How Visual Testing Improves UI Consistency and User Experience?

Visual testing tools like Applitools help maintain a consistent look and feel across all UI components. By incorporating AI-powered visual validation, teams can:

Ensure UI Consistency: Detects unintended UI changes across different screens, browsers, and devices.
Identify Layout Breaks Early: Find issues like hidden elements, overlapping sections, or broken grids before they reach production.
Improve Development Workflow: Provide fast feedback to designers, developers, and testers, reducing iteration cycles.
Enhance Brand Perception: A polished, visually accurate UI increases customer trust and engagement.

Introduction to Applitools: AI-Powered Visual Validation

Applitools Eyes is an AI-driven visual testing platform that extends Selenium’s capabilities by performing intelligent image-based comparisons. Unlike traditional pixel-by-pixel comparison tools, Applitools uses computer vision and AI to identify true UI defects while ignoring insignificant differences, such as anti-aliasing or minor rendering variations. Key Features of Applitools:

AI-Powered Visual Comparisons: Detects even the smallest UI differences while ignoring irrelevant rendering variations.
Baseline Image Matching: Stores reference snapshots (baselines) and compares new test runs to detect unexpected UI changes.
Cross-Browser and Cross-Device Testing: Ensures a consistent UI appearance across Chrome, Firefox, Edge, Safari, mobile browsers, and native apps.
Smart Region Matching: Allows testers to ignore dynamic content (such as timestamps, ads, or rotating banners) to avoid unnecessary test failures.
Auto-Maintenance and Smart Test Management: Uses AI to categorize changes (intentional vs. unintentional), reducing manual test maintenance.
CI/CD and Cloud Integration: Seamlessly integrates with Jenkins, GitHub Actions, Azure DevOps, and other CI/CD tools for automated visual testing in pipelines.

How Applitools Complements Selenium with Visual Assertions

By integrating Applitools with Selenium, testers can extend Selenium’s functional tests to include visual verification, ensuring that both behavior and UI appearance are validated. How the Integration Works:

*Selenium Automates User Actions * Selenium performs functional steps such as opening a webpage, clicking buttons, and submitting forms.
Applitools Captures Visual Snapshots  After each significant step, Applitools captures a screenshot of the entire page or specific elements.
Applitools Compares the Screenshots with Baselines  The new image is automatically compared to the baseline snapshot stored from previous test runs.
AI-Powered Analysis Flags Visual Differences  If any unexpected UI changes are detected, Applitools marks them for review in the Test Manager dashboard.

What is Applitools Eyes?

Applitools Eyes is an advanced AI-powered platform for testing various user interfaces, including websites, web applications, native mobile apps, documents, and media. Unlike traditional automation tools that validate test scenarios mechanically, Applitools leverages artificial intelligence to analyze thousands of UI elements and scenarios instantly, mimicking human-like visual perception for more accurate testing. Understanding Applitools Baselines in Detail What is a Baseline in Applitools?

A baseline in Applitools is a reference snapshot of your application’s UI. It serves as the expected appearance of the application, and all future test runs are compared against it to detect visual differences.

Unlike traditional assertion-based testing, which checks only functional correctness, Applitools performs pixel-by-pixel comparisons to catch visual defects, including layout shifts, color mismatches, missing elements, and responsive design issues. Why Are Baselines Important?

UI Consistency: Verifies that your app maintains a uniform look across different versions.
Visual Bug Detection: Captures unintended UI changes, such as misalignments or missing elements.
Cross-Browser Compatibility: Identifies differences in UI rendering across different browsers and devices.
Regression Testing: Ensures that new changes dont break the existing UI.

Lifecycle of a Baseline in Applitools

The baseline lifecycle consists of three main stages:

First Test Run – Creating the Baseline

When you run an Applitools test for the first time, it:

Captures a screenshot of the UI.
Saves this image as the baseline in the Applitools dashboard.
Stores metadata such as viewport size, browser, OS, and test name to ensure accurate comparisons in future runs.

Example: Suppose you are testing an e-commerce website where the “Add to Cart” button is green. During the first test run, Applitools captures and stores this design as the baseline.

Subsequent Runs – Comparing Screenshots with Baseline

In later test runs, Applitools:

Captures a new screenshot of the webpage.
Compare it pixel-by-pixel with the saved baseline.
Highlights any differences (color changes, missing elements, layout shifts, etc.).

Example: If a CSS update turns the “Add to Cart” button red, Applitools detects the difference and flags it for review. Handling Differences – Approving or Rejecting Changes

When Applitools detects visual changes, the tester has three options: Accept the Change (Update Baseline)

If the change is expected, approve the new screenshot as the baseline.
Future test runs will now compare against this updated version.

Reject the Change (Bug Found)

If the change is unexpected, report it as a UI bug.
Developers can fix the issue before pushing changes to production.

Ignore Specific Differences (Dynamic Elements)

If elements like ads, timestamps, or user-generated content change frequently, you can mark them as dynamic regions to avoid false positives.

Example: If the “Add to Cart” button turns red instead of green due to a design update, the tester can approve this as the new baseline. But if the button disappears entirely, the tester would reject the change as a bug. Key Attributes That Define a Baseline

Each baseline is uniquely identified using multiple attributes: Test Name

Each visual test has a unique name assigned in Applitools.
Example: Homepage Visual Test

Application Name

Applitools associates baselines with a specific application to avoid mix-ups between projects.
Example: E-Commerce App

Read The Full Blog Here:- JigNect Technologies


菜单
主要内容

书签 设置 文档

dev.to
17 九月 2025

dev.to
阅读约 6 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Engineering "The" Loop The Problem with the Classic Game Loop 🎮

Almost every interactive application, from video games to user interfaces, relies on a continuous execution loop. This loop is the heart of the application, responsible for everything from updating the game state to rendering graphics. While it's a fundamental concept, the traditional approach can often feel like a point of contention among developers. Why? Because the classic game loop, often a simple while(true) or while(IsRunning) block, can quickly become an unmanageable monolith.

Consider a typical game loop:

void MainGameLoop() { Initialize();

while (IsRunning)
{
    ProcessInput();
    UpdateGameState();
    RenderGraphics();
    PlayAudio();
    // ...and a dozen other things
}

Shutdown();

}

This works for simple projects, but as an application grows, this single loop becomes a catch-all for complex logic. It tightly couples disparate systems (like input, rendering, and AI), making the code difficult to maintain, test, and debug. You end up with a single, massive function that handles everything, and any small change can have unintended consequences. A Better Way: A State Machine-Driven Loop ✨

What if we could break free from this monolithic approach? What if our application's "loop" wasn't a static while statement, but a dynamic, self-managing system? That's the power of a Finite State Machine (FSM).

In our project, we've integrated this idea into the MainWindowViewModel for a WPF application. Instead of a single, endlessly running loop, our application's flow is managed by an FSM. Each "tick" of the application isn't a fixed set of actions, but a transition to a new state based on events.

Our MainWindowViewModel (from the provided code) implements the IStateContext interface:

public class MainWindowViewModel : INotifyPropertyChanged, IStateContext { // ... properties and methods public bool IsValid { get; set; } public string Name { get; set; }

public void InitializeState()
{
    // This method will be called by the FSM
    // It will be the entry point for our initial actions
}

}

This approach allows the FSM to dictate what happens at any given moment. For example, when the application starts, the FSM can trigger the InitializeState() method. If a user clicks a button, the FSM can transition the application into a new state that handles that specific action, like fetching GitHub repository data. The loop isn't a single, continuous block—it's a series of transitions and actions managed by a state machine.

This makes the MainWindowViewModel highly modular and responsive. We can have states like LoadingRepositories, ShowingGitHubStatus, or Idle, and the FSM ensures that the correct actions are executed for each state. The API Evolution: From Simple to Dynamic 🚀

The two provided Unity C# scripts, FSM_UnityIntegration.cs and FSM_UnityIntegrationAdvanced.cs, showcase different approaches to creating a game loop driven by a Finite State Machine (FSM) API. The basic FSM_UnityIntegration script uses a rigid one-to-one mapping between Unity's lifecycle methods and a single FSM processing group. In contrast, FSM_UnityIntegrationAdvanced allows for a dynamic, runtime-modifiable list of processing groups for each Unity message, providing greater flexibility. Both scripts implement the singleton pattern to ensure only one instance of the integration exists in a scene. The Basic Approach: Direct Mapping

In our initial API, we used a direct one-to-one mapping. Each Unity message (like Start, Update, OnDestroy) directly called a single, corresponding process group. This was a step up from the monolithic loop, but it was still quite rigid. You had a predefined set of actions for each phase of the application lifecycle.

For instance, the Update() method in the basic integration always calls the single _updateProcessingGroup:

void Update() { FSM_API.Interaction.Update(_updateProcessingGroup); FSM_API.Interaction.Update(_unityHandles); }

Pros:
    Simplicity: It's a simple, predictable model that's easy for new developers to grasp.
    Performance: The direct call to a single processing group is efficient and has minimal overhead.
    Reliability: The flow is fixed and well-defined, reducing the chance of unexpected behavior.

Cons:
    Rigidity: The biggest drawback is its lack of flexibility. You can't dynamically add or remove processing groups at runtime. To change what happens during an Update, you'd need to modify the code itself.
    Limited Customization: It's not suitable for scenarios where the game loop needs to change its behavior based on the application's state, such as enabling or disabling different systems (e.g., a combat system) only when a specific state is active.

The Advanced Approach: Runtime Modifiable Loops

Our more advanced API takes this a step further. We don't just have a single method for Update; instead, we provide a list of strings that represent the process groups to be executed. This is a simple but incredibly powerful change.

The Update() method in the advanced integration iterates through a list of groups, calling FSM_API.Interaction.Update() for each one:

void Update() { foreach (var group in _updateProcessingGroup) { FSM_API.Interaction.Update(group); } FSM_API.Interaction.Update(_unityHandles); }

This allows us to add, remove, or reorder process groups on the fly. For example, when the FSM enters a FetchRepositories state, it can dynamically add the GitHubRepositoryService process group to the update list using the AddProcessingGroup() method. When the fetch is complete, the FSM transitions to another state and can remove that process group. The application's loop isn't just a fixed sequence of events—it's a fluid, context-aware process.

Pros:
    Flexibility and Modularity: You can compose the game loop on the fly, adding and removing systems as needed without changing the core integration code.
    State-Driven Behavior: This approach fully leverages the power of the FSM. Different states can dictate which systems are active, making the application's behavior highly contextual and adaptive.
    Scalability: It's easier to add new systems without a cascading effect. You simply create a new processing group and add it to the relevant list when it's needed.

Cons:
    Complexity: This model is more complex to set up and manage. Developers must be mindful of which groups are active at any given time to avoid unexpected behavior.
    Potential Performance Overhead: Iterating through a list of groups every frame adds a small amount of overhead compared to a direct function call, though this is negligible in most cases.
    Debugging: It can be more challenging to debug, as the flow of execution isn't fixed; it depends on the current state and the contents of the processing group lists.

Comparison and Conclusion ⚖️ Feature Basic Integration Advanced Integration Execution Model Fixed: One-to-one mapping between Unity messages and FSM groups. Dynamic: Iterates through a runtime-modifiable list of groups. Flexibility Low: Changes require code modification. High: Groups can be added/removed at runtime. Use Case Ideal for simple applications with a consistent, unchanging game loop. Best for complex, state-driven applications where systems need to be enabled/disabled dynamically. Maintainability Easy to maintain and understand for small projects. More maintainable in the long run for large projects, as logic is isolated by state. Scalability Limited: Adding new features can lead to a monolithic Update() method. High: New systems can be added without modifying core code.

The choice between the two integrations depends on the needs of the project. The basic FSM_UnityIntegration is a great starting point for developers who want a simple, reliable FSM-driven loop without the need for complex, dynamic behavior. The advanced FSM_UnityIntegrationAdvanced is a more powerful and scalable solution for larger, more complex applications that require a fluid and responsive game loop that can adapt to different application states. By leveraging the FSM to control which groups are active, the advanced model creates a truly context-aware application loop. Conclusion: The Future is State-Driven 🤖

By moving from a monolithic while loop to a dynamic, FSM-driven execution model, we've created an application that is more:

Modular: Each process group and state handles a single responsibility.
Maintainable: Logic is isolated, making it easier to debug and modify.
Scalable: We can easily add new features without a cascading effect.
Runtime Modifiable: The application can adapt its behavior on the fly based on its current state.

This isn't just about elegant code; it's about building robust, flexible, and powerful applications that can grow and evolve without becoming a tangled mess. So next time you're about to write a classic while(true) loop, ask yourself: could a state machine do this better? 💖 Support Us

If you find this project useful, you can support its development through PayPal.

Donate via PayPal 🔗 Useful Links

NuGet Package: TheSingularityWorkshop.FSM_API
GitHub Repository: TrentBest/FSM_API
License: MIT License

🧠 Brought to you by

The Singularity Workshop – Tools for the curious, the bold, and the systemically inclined.

The image depicts a futuristic, sci-fi landscape dominated by glowing circuitry. A large, stylized Patreon logo hovers in the dark, starry sky above a radiant, eye-like sphere. This sphere pulses with vibrant, multicolored light, casting a brilliant glow onto the digital terrain below. The ground is a vast circuit board, with rivers of glowing orange and blue energy flowing along intricate pathways. Small, button-like structures on the surface emit soft light. The overall scene feels alive and dynamic, suggesting a powerful, almost magical, connection between technology and consciousness, with the Patreon logo at the center of it all. Because state shouldn’t be a mess.


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn
阅读约 1 分钟

将在几秒钟内移除此书签。 标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Elasticsearch各Cat命令详解 Elasticsearch各Cat命令详解 cat基础 查看都有哪些cat指令

GET _cat

=^.^= /_cat/allocation /_cat/shards /_cat/shards/{index} /_cat/master /_cat/nodes /_cat/tasks /_cat/indices /_cat/indices/{index} /_cat/segments /_cat/segments/{index} /_cat/count /_cat/count/{index} /_cat/recovery /_cat/recovery/{index} /_cat/health /_cat/pending_tasks /_cat/aliases /_cat/aliases/{alias} /_cat/thread_pool /_cat/thread_pool/{thread_pools} /_cat/plugins /_cat/fielddata /_cat/fielddata/{fields} /_cat/nodeattrs /_cat/repositories /_cat/snapshots/{repository} /_cat/templates

verbose显示指令的详细信息

GET _cat/health?v

help显示指令返回参数的说明

GET _cat/health?help

GET _cat/count?h=timestamp,count

format设置返回内容的格式,支持json,yaml,text,smile,cbor

GET _cat/master?format=json&pretty

sort排序

GET _cat/indices?s=store.size:desc

可以多个参数一起使用,用&连接

GET _cat/indices?v&s=store.size:desc

cat指令 aliases

显示别名、过滤器、路由信息

GET _cat/aliases?v

alias index filter routing.index routing.search

名称 描述 alias: 别名 index: 索引别名指向 filter: 过滤规则 routing.index: 索引路由 routing.search: 搜索路由 allocation

显示每个节点分片数量、占用空间

GET _cat/allocation?v

shards disk.indices disk.used disk.avail disk.total disk.percent host ip node 1073 1.2tb 1.2tb 787.1gb 1.9tb 61 10.82.9.205 10.82.9.205 node01 1072 1.2tb 1.2tb 1.2tb 2.4tb 50 10.82.9.207 10.82.9.207 node03 1073 1.2tb 1.2tb 787.4gb 1.9tb 61 10.82.9.206 10.82.9.206 node02

名称 描述 shards: 节点承载的分片数量 disk.indices: 索引占用的空间大小 disk.used: 节点所在机器已使用的磁盘空间大小 disk.avail: 节点可用空间大小 disk.total: 节点总空间大小 disk.percent: 节点磁盘占用百分比 host: 节点的host地址 ip: 节点的ip地址 node: 节点名称 count

显示索引文档数量

GET _cat/count?v

epoch timestamp count 1558059594 10:19:54 7829577019

名称 描述 epoch: 自标准时间(1970-01-01 00:00:00)以来的秒数 timestamp: 时间 count: 文档总数 health

查看集群健康状况

GET _cat/health?v

epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1558059496 10:18:16 ops_coffee green 7 5 5362 2681 0 0 0 0 - 100.0%

名称 描述 epoch: 自标准时间(1970-01-01 00:00:00)以来的秒数 timestamp: 时间 cluster: 集群名称 status: 集群状态 node.total: 节点总数 node.data: 数据节点总数 shards: 分片总数 pri: 主分片总数 repo: 复制节点的数量 init: 初始化节点的数量 unassign: 未分配分片的数量 pending_tasks: 待定任务数 max_task_wait_time: 等待最长任务的等待时间 active_shards_percent: 活动分片百分比 indices

查看索引信息

GET _cat/indices?v

health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open ops-coffee-slowlog-2019.04.08 5ARM1TgqTy-fGO8RlJC_Jg 5 1 7743 0 57.9mb 28.9mb green open ops-coffee-nginx-2019.04.09 1VdeScHaQda6CF_htRZz_A 5 1 81519329 0 52gb 25.8gb

名称 描述 health: 索引的健康状态 status: 索引的开启状态 index: 索引名字 uuid: 索引的uuid pri: 索引的主分片数量 rep: 索引的复制分片数量 docs.count: 索引下的文档总数 docs.deleted: 索引下删除状态的文档数 store.size: 主分片+复制分片的大小 pri.store.size: 主分片的大小 master

显示master节点信息

GET _cat/master?v

id host ip node dkof1ouuT96BTQOR1xyDhQ 10.82.9.205 10.82.9.205 node01

名称 描述 id: 节点ID host: 主机名称 ip: 主机IP node: 节点名称 nodeattrs

显示node节点属性

GET _cat/nodeattrs?v

node host ip attr value

名称 描述 node: 节点名称 host: 主机地址 ip: 主机ip attr: 属性描述 value: 属性值 nodes

显示node节点信息

GET _cat/nodes?v

ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name 10.82.9.205 50 99 6 2.32 2.82 3.37 mdi * node01 10.82.9.206 71 99 6 3.24 4.02 4.35 mdi - node02 10.82.9.208 25 94 3 0.40 0.42 0.36 i - node04 10.82.9.132 70 99 7 1.51 2.28 2.57 di - node11

名称 描述 ip: node节点的IP heap.percent: 堆内存占用百分比 ram.percent: 内存占用百分比 cpu: CPU占用百分比 load_1m: 1分钟的系统负载 load_5m: 5分钟的系统负载 load_15m: 15分钟的系统负载 node.role: node节点的角色 master: 是否是master节点 name: 节点名称 pending_tasks

显示正在等待的任务

GET _cat/pending_tasks?v

insertOrder timeInQueue priority source

名称 描述 insertOrder: 任务插入顺序 timeInQueue: 任务排队了多长时间 priority: 任务优先级 source: 任务源 plugins

显示每个运行插件节点的视图

GET _cat/plugins?v

name component version

名称 描述 name: 节点名称 component: 插件名称 version: 插件版本 recovery

显示正在进行和先前完成的索引碎片恢复的视图

GET _cat/recovery?v

index shard time type stage source_host source_node target_host target_node repository snapshot files files_recovered files_percent files_total bytes bytes_recovered bytes_percent bytes_total translog_ops translog_ops_recovered translog_ops_percent filebeat-docker-pay-2019.04.18 0 209ms peer done 10.82.9.132 node11 10.82.9.207 node03 n/a n/a 0 0 0.0% 0 0 0 0.0% 0 0 0 100.0%

名称 描述 index: 索引名称 shard: 分片名称 time: 恢复时间 type: 恢复类型 stage: 恢复阶段 source_host: 源主机 source_node: 源节点名称 target_host: 目标主机 target_node: 目标节点名称 repository: 仓库 snapshot: 快照 files: 要恢复的文件数 files_recovered: 已恢复的文件数 files_percent: 恢复文件百分比 files_total: 文件总数 bytes: 要恢复的字节数 bytes_recovered: 已恢复的字节数 bytes_percent: 恢复字节百分比 bytes_total: 字节总数 translog_ops: 要恢复的translog操作数 translog_ops_recovered: 已恢复的translog操作数 translog_ops_percent: 恢复的translog操作的百分比 segments

显示碎片中的分段信息

GET _cat/segments?v

index shard prirep ip segment generation docs.count docs.deleted size size.memory committed searchable version compound filebeat-docker-pay-2019.04.18 0 r 10.82.9.207 _8cu 10830 19470 0 17.7mb 43546 true true 7.2.1 false

名称 描述 index: 索引名称 shard: 分片名称 prirep: 主分片还是副本分片 ip: 所在节点IP segment: segments段名 generation: 分段生成 docs.count: 段中的文档树 docs.deleted: 段中删除的文档数 size: 段大小,以字节为单位 size.memory: 段内存大小,以字节为单位 committed: 段是否已提交 searchable: 段是否可搜索 version: 版本 compound: compound模式 shards

GET _cat/shards?v

index shard prirep state docs store ip node mysql-slowlog-2019.03.14 4 r STARTED 1381 4.6mb 10.82.9.205 node01 mysql-slowlog-2019.03.14 4 p STARTED 1381 4.5mb 10.82.9.206 node02

名称 描述 index: 索引名称 shard: 分片序号 prirep: 分片类型,p表示是主分片,r表示是复制分片 state: 分片状态 docs: 该分片存放的文档数量 store: 该分片占用的存储空间大小 ip: 该分片所在的服务器ip node: 该分片所在的节点名称 thread_pool

查看线程池信息

GET _cat/thread_pool?v

node_name name active queue rejected node10 bulk 1 0 4 node10 fetch_shard_started 0 0 0

名称 描述 node_name: 节点名称 name: 线程池名称 active: 活跃线程数量 queue: 当前队列中的任务数 rejected: 被拒绝的任务数

📅 2019-05-21
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Django自定义错误页面配置 Django自定义错误页面配置

基于Django 2.2版本实现

Django下的错误类型主要有四种: - 400:客户端错误 - 403:没有权限 - 404:页面未找到 - 500:服务器错误

修改settings.py文件关闭DEBUG模式

DEBUG = False

自定义错误页面在Debug = True时不生效

创建错误页面,例如errors/403.html

You Don't have permission ! Please contact ops-coffee.cn

view.py视图中创建自定义错误处理方法

from django.shortcuts import render

def bad_request_view(request, exception, template_name='errors/400.html'): return render(request, template_name)

def permission_denied_view(request, exception, template_name='errors/403.html'): return render(request, template_name)

def page_not_found_view(request, exception, template_name='errors/404.html'): return render(request, template_name)

def error_view(request, exception, template_name='errors/500.html'): return render(request, template_name)

urls.py中添加错误行为由自定义程序处理

handler400 = 'webapp.views.bad_request_view' handler403 = 'webapp.views.permission_denied_view' handler404 = 'webapp.views.page_not_found_view' handler500 = 'webapp.views.error_view'

📅 2019-05-21
🏷️ Django
🏷️ 日常技术
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

notes.eatonphil.com
15 七月 2025

eatonphil.com
阅读约 11 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Writing an x86 emulator from scratch in JavaScript: 1. a stack and register machine

Better yet, take a look at this post walking through emulating x86 ELF binaries in Go: Emulating linux/AMD64 userland: interpreting an ELF binary

Next up in emulator basics: 2. system calls

In this post we'll create a small virtual machine in JavaScript and use it to run a simple C program compiled with GCC for an x86_64 (or AMD64) CPU running Linux.

All source code is available on Github. Virtual machine data storage

Our virtual machine will have two means of storing data: registers and an integer stack. Each register can store a 64-bit integer. The stack is an array of 8-bit (or 1 byte) integers.

We'll make the following registers available for modification and use by the program(mer):

RDI, RSI, RSP, RBP, RAX, RBX, RCX, RDX, R8, R9, R10, R11, R12, R13, R14, R15

The RSP register is used by the virtual machine for tracking the location of the last entry in the stack. It will be modified by the virtual machine when it encounters the POP, PUSH, CALL and RET instructions we'll support. We'll get into the specifics shortly.

And we'll make the following registers available for use (but not modification) by the program(mer):

RIP, CS, DS, FS, SS, ES, GS, CF, ZF, PF, AF, SF, TF, IF, DF, OF

Each of these has a special meaning but we'll focus on RIP. The RIP register contains the address of the instruction currently being interpreted by our virtual machine. After every instruction the virtual machine will increment the value in this register -- except for a few special instructions like CALL and RET. Memory addresses

It will become useful to provide direct access to memory with a special syntax. We'll focus just on 64-bit addresses that will look like this:

MOV QWORD PTR [RBP - 8], 12

This asks for the value 12 to be written into the memory address at RBP - 8 bytes. The QWORD PTR part clarifies that we want to write 8 bytes worth of the value. Since 12 is less than 8 bytes, the rest will be filled with zeros.

ADD RAX, QWORD PTR [RBP - 8]

This asks for eight bytes starting from the memory address RBP - 8 to be added to the value in RAX and stored back in RAX. Virtual machine instruction set

In our virtual machine we'll define support for the following instructions:

MOV $REGISTER, $REGISTER or $MEMORY ADDRESS or $LITERAL NUMBER
    This instruction copies the second value into the first.
ADD $REGISTER, $REGISTER or $MEMORY ADDRESS
    This instruction adds the second value into the first and stores the result into the first.
PUSH $REGISTER
    This instruction will decrement the RSP register by 8 bytes and store the value at the bottom of the stack.
POP $REGISTER
    This instruction will increment the RSP register by 8 bytes, remove the last element in the stack (at the bottom), and store it into the register.
CALL $LABEL
    This instruction will push the value in the RIP register (plus one) onto the stack and set the RIP register to the line of code of the label. More on this later.
RET
    This instruction will remove the value at the bottom of the stack and store it in the RIP register.

Now we have more than enough instructions to write some interesting programs for the virtual machine. Virtual machine semantics

We'll make one last assumption before explaining further. In our programs, there must be a main label which must contain a RET instruction. Once we hit the terminal RET, we will exit the virtual machine and set the exit code to the value stored in the RAX register.

Let's look at a simple program:

main: ; the required main label MOV RAX, 1 ; store 1 in RAX MOV RDI, 2 ; store 2 in RDI ADD RAX, RDI ; store the result of adding RAX and RDI in RAX RET ; give control back to the virtual machine

When we run this program, first we initialize a stack (we'll give it 1000 elements) and set the RSP register to 1000 (the top of the stack). Then we look for the main label and set the RIP register to 1, the line number after the label appears (0). Then until the RIP register is 1000 again, we interpret the instruction at the line number stored in the RIP register. Once the RIP register hits 1000, we exit the program setting RAX as the exit code. One more example

Now let's look at one more program:

plus: ADD RDI, RSI MOV RAX, RDI RET

main: MOV RDI, 1 MOV RSI, 2 CALL plus RET

Our virtual machine will start at the line after the main label. Then it will store 1 into RDI and 2 into RSI. Then it will jump to the second line in the program to add RDI and RSI and store the result in RDI. Then it will copy RDI into RAX and return control to the final line. This last RET will in turn return control to the virtual machine. Then the program will exit with exit code 3. Parsing

Now that we've finished up describing our virtual machine language and semantics, we need to parse the instructions into a format we can easily interpret.

To do this we'll iterate over the program skip any lines that start with a dot. These are virtual machine directives that are important for us to ignore for now. We'll also remove any characters including and following a semi-colon or hash-tag, until the end of the line. These are comments.

We'll store a dictionary of label names to line numbers (the line number of the label plus one) and without the colon.

And we'll store the instructions as an array of objects composed of an operation and optional operands. Code

function parse(program) { const labels = {}; const instructions = [];

const lines = program.split('\n');

for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Remove any trailing, leading whitespace // TODO: handle each line }

return { labels, instructions }; }

First let's handle the directives we want to ignore:

for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Remove any trailing, leading whitespace

if (line.startsWith('.')) {
  continue;
}

}

And then comments:

for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Remove any trailing, leading whitespace

if (line.startsWith('.') || line.startsWith(';') || line.startsWith('#')) {
  continue;
}

if (line.includes(';')) {
  line = line.split(';')[0];
}

if (line.includes('#')) {
  line = line.split('#')[0];
}

if (!line) {
  continue;
}

}

And then labels:

for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Remove any trailing, leading whitespace

if (line.startsWith('.') || line.startsWith(';') || line.startsWith('#')) {
  continue;
}

if (line.includes(';')) {
  line = line.split(';')[0];
}

if (line.includes('#')) {
  line = line.split('#')[0];
}

if (!line) {
  continue;
}

if (line.includes(':')) {
  const label = line.split(':')[0];
  labels[label] = instructions.length;
  continue;
}

}

And finally instruction parsing plus the rest:

function parse(program) { const labels = {}; const instructions = [];

const lines = program.split('\n');

for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Remove any trailing, leading whitespace

if (line.startsWith('.') || line.startsWith(';')) {
  continue;
}

if (line.includes(';')) {
  line = line.split(';')[0];
}

if (line.includes(':')) {
  const label = line.split(':')[0];
  labels[label] = instructions.length;
  continue;
}

const operation = line.split(/\s/)[0].toLowerCase();
const operands = line.substring(operation.length).split(',').map(t => t.trim());
instructions.push({
  operation,
  operands,
});

}

return { labels, instructions }; }

Hurray! A brittle parser. Interpreting

We've already described the semantics a few times. So let's get started with the foundation and initialization.

We'll use BigInts because JavaScript integers are 53-bits wide. This isn't incredibly important in our basic programs but it will quickly became painful without.

And we'll make process memory available as an array of 8-bit integers. In order to make this easy to use, we'll also provide helper function for writing to and reading from memory.

const fs = require('fs');

const REGISTERS = [ 'RDI', 'RSI', 'RSP', 'RBP', 'RAX', 'RBX', 'RCX', 'RDX', 'RIP', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14', 'R15', 'CS', 'DS', 'FS', 'SS', 'ES', 'GS', 'CF', 'ZF', 'PF', 'AF', 'SF', 'TF', 'IF', 'DF', 'OF', ];

function writeMemoryBytes(process, address, value, size) { for (let i = 0n; i < size; i++) { value >>= i * 8n; process.memory[address + i] = value & 0xFFn; } }

function readMemoryBytes(process, address, size) { let value = 0n; for (let i = 0n; i < size; i++) { value |= (process.memory[address + i] || 0n) << (i * 8n); } return value; }

function interpret(process) { // TODO: interpret }

function main(file) { const memory = new Array(10000); const code = fs.readFileSync(file).toString(); const { instructions, labels } = parse(code);

const registers = REGISTERS.reduce((rs, r) => ({ ...rs, [r]: 0n }), {}); registers.RIP = BigInt(labels.main === undefined ? labels._main : labels.main); registers.RSP = BigInt(memory.length - 8);

const process = { registers, memory, instructions, labels, };

writeMemoryBytes(process, registers.RSP, registers.RSP, 8);

interpret(process); return Number(registers.RAX); }

process.exit(main(process.argv[2]));

We'll accept _main as an entry point as well as main to support our macOS users. If you know why our macOS users use _main I'd love to know.

To interpret, we grab the instruction pointed to in RIP and switch on the operation.

function interpret(process) { do { const instruction = process.instructions[process.registers.RIP]; switch (instruction.operation.toLowerCase()) { case 'mov': break; case 'add': break; case 'call': break; case 'ret': break; case 'push': break; case 'pop': break; } } while (process.registers.RIP != BigInt(readMemoryBytes(process, memory.length - 8, 8))); }

Interpreting MOV

Example:

MOV RAX, 1 MOV RAX, RDI MOV QWORD PTR [RBP - 8], 8

This instruction will store a value into a register or address and increment RIP. If the left-hand side is a memory address we will write to memory.

  case 'mov': {
    const lhs = interpretValue(process, instruction.operands[0], { lhs: true });
    const rhs = interpretValue(process, instruction.operands[1]);
    if (REGISTERS.includes(lhs)) {
      process.registers[lhs] = rhs;
    } else {
      writeMemoryBytes(process, lhs.address, rhs, lhs.size);
    }
    process.registers.RIP++;
    break;
  }

We're delegating to a helper function to handle registers vs. memory addresses vs. literals appropriately. Without memory addresses it's a simple function:

function interpretValue(process, value, { lhs } = { lhs: false }) { if (REGISTERS.includes(value)) { if (lhs) { return value; } else { return process.registers[value]; } }

return BigInt.asIntN(64, value); }

We need to do some hacking to support memory addresses:

function interpretValue(process, value, { lhs } = { lhs: false }) { if (REGISTERS.includes(value)) { if (lhs) { return value; } else { return process.registers[value]; } }

if (value.startsWith('QWORD PTR [')) { const offsetString = value.substring('QWORD PTR ['.length, value.length - 1).trim(); if (offsetString.includes('-')) { const [l, r] = offsetString.split('-').map(l => interpretValue(process, l.trim())); const address = l - r; const bytes = 8; // qword is 8 bytes if (lhs) { return { address, size: bytes }; } else { return readMemoryBytes(process, address, bytes); } }

throw new Error('Unsupported offset calculation: ' + value);

}

return BigInt.asIntN(64, value); }

Interpreting ADD

Example:

ADD RAX, RDI

This instruction will combine both registers and store the result in the first, then increment the RIP register.

  case 'add': {
    const lhs = interpretValue(process, instruction.operands[0], { lhs: true });
    const rhs = interpretValue(process, instruction.operands[1]);
    process.registers[lhs] += rhs;
    process.registers.RIP++;
    break;
  }

Interpreting CALL

Example:

CALL plus

This instruction store RIP (plus one, to continue after the call instruction) on the stack and sets RIP to the location specified by the label.

  case 'call': {
    process.registers.RSP -= 8n;
    writeMemoryBytes(process, process.registers.RSP, process.registers.RIP + 1n, 8);
    const label = instruction.operands[0];
    process.registers.RIP = process.labels[label];
    break;
  }

Interpreting RET

Example:

RET

This instruction removes the last element from the stack and stores it in the RIP register.

  case 'ret': {
    const value = readMemoryBytes(process, process.registers.RSP, 8);
    process.registers.RSP += 8n;
    process.registers.RIP = value;
    break;
  }

Interpreting PUSH

Example:

PUSH RAX

This instruction stores the value in the register on the stack and increments RIP.

  case 'push': {
    const value = interpretValue(process, instruction.operands[0]);
    process.registers.RSP -= 8n;
    writeMemoryBytes(process, process.registers.RSP, value, 8);
    process.registers.RIP++;
    break;
  }

Interpreting POP

Example:

POP RAX

This instruction removes the last element from the stack and stores it into the register specified. Then it increments RIP.

  case 'pop': {
    const lhs = interpretValue(process, instruction.operands[0], { lhs: true });
    const value = readMemoryBytes(process, process.registers.RSP, 8);
    process.registers.RSP += 8n;
    process.registers[lhs] = value;
    process.registers.RIP++;
    break;
  }

All together

$ cat test1.asm main: ; the required main label MOV RAX, 1 ; store 1 in RAX MOV RDI, 2 ; store 2 in RDI ADD RAX, RDI ; store the result of adding RAX and RDI in RAX RET ; give control back to the virtual machine $ node emulator.js test1.asm $ echo $? 3

And finally, let's see what we can do with a simple C program:

$ cat plus.c long main() { long a = 5; long b = 6; return a + b; } $ gcc -S -masm=intel -o plus.s plus.c $ node emulator.js plus.s $ echo $? 11

And we've got the start of a working x86_64/AMD64 emulator. Next steps

We aren't setting flags appropriately to support conditionals, so that's low-hanging fruit. Additionally, syscalls open up a new world (that we'll end up needing since exit codes are limited to 8-bits of information). Additionally, our parsing is brittle. Dealing with ELF files may be a better direction to go and also enables more. We'll explore these aspects and others in follow-up posts. Human interest

I originally intended to build a GameBoy emulator because the hardware is simple and uniform. But I found it easiest to start hacking together an AMD64 emulator because AMD64 is well-documented and gcc is easy enough to use. I'm still interested though unless/until I figure out how to emulate a graphics card for AMD64.

It's tricky! But not that tricky. I built a graphical debugger around this emulator to help out with the logic and off-by-one errors. But ultimately it's been surprising to me how easy it is to get started -- especially when I'm not concerned about absolute correctness (yet).

Here's my first post on a series on emulator basics. It's baby's first stack and register virtual machine and it turns out it runs x86 code. https://t.co/WiWmGedawt #linux #assembly https://t.co/xjiMkhgpdN
 Phil Eaton (@phil_eaton) May 24, 2019

菜单
主要内容

书签 设置 文档

notes.eatonphil.com
13 七月 2025

eatonphil.com
阅读约 5 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Walking through a basic Racket web service

Racket is an impressive language and ecosystem. Compared to Python, Racket (an evolution of Scheme R5RS is three years younger. It is as concise and expressive as Python but with much more reasonable syntax and semantics. Racket is also faster in many cases due in part to:

JIT compilation on x86 platforms
support for both concurrency and parallelism
support for optimizing statically-typed code

Furthermore, the built-in web server libraries and database drivers for MySQL and PostgreSQL are fully asynchronous. This last bit drove me here from Play / Akka. (But strong reservations about the complexity of Scala and the ugliness of Play in Java helped too.)

With this motivation in mind, I'm going to break down the simple web service example provided in the Racket manuals. If you don't see the following code in the linked page immediately, scroll down a bit.

lang web-server

(require web-server/http)

(provide interface-version stuffer start)

(define interface-version 'stateless)

(define stuffer (stuffer-chain serialize-stuffer (md5-stuffer (build-path (find-system-path 'home-dir) ".urls"))))

(define (start req) (response/xexpr `(html (body (h2 "Look ma, no state!")))))

First we notice the #lang declaration. Racket libraries love to make new "languages". These languages can include some entirely new syntax (like the Algol language implementation) or can simply include a summary collection of libraries and alternative program entrypoints (such as this web-server language provides). So the first thing we'll do to really understand this code is to throw out the custom language. And while we're at it, we'll throw out all typical imports provided by the default racket language and use the racket/base language instead. This will help us get a better understanding of the Racket libraries and the functions we're using from these libraries.

While we're throwing the language away, we notice the paragraphs just below that original example in the manual. It mentions that the web-server language also imports a bunch of modules. We can discover which of these modules we actually need by searching in the Racket manual for functions we've used. For instance, searching for "response/xexpr" tells us it's in the web-server/http/xexpr module. We'll import the modules we need using the "prefix-in" form to make function-module connections explicit.

lang racket/base

(require (prefix-in xexpr: web-server/http/xexpr) (prefix-in hash: web-server/stuffers/hash) (prefix-in stuffer: web-server/stuffers/stuffer) (prefix-in serialize: web-server/stuffers/serialize))

(provide interface-version stuffer start)

(define interface-version 'stateless)

(define stuffer (stuffer:stuffer-chain serialize:serialize-stuffer (hash:md5-stuffer (build-path (find-system-path 'home-dir) ".urls"))))

(define (start req) (xexpr:response/xexpr `(html (body (h2 "Look ma, no state!")))))

Now we've got something that is a little less magical. We can run this file by calling it: "racket server.rkt". But nothing happens. This is because the web-server language would start the service itself using the exported variables we provided. So we're going to have to figure out what underlying function calls "start" and call it ourselves. Unfortunately searching for "start" in the manual search field yields nothing relevant. So we Google "racket web server start". Down the page on the second search result we notice an example using the serve/servlet function to register the start function. This is our in.

lang racket/base

(require (prefix-in xexpr: web-server/http/xexpr) (prefix-in hash: web-server/stuffers/hash) (prefix-in stuffer: web-server/stuffers/stuffer) (prefix-in serialize: web-server/stuffers/serialize) (prefix-in servlet-env: web-server/servlet-env))

(provide interface-version stuffer start)

(define interface-version 'stateless)

(define stuffer (stuffer:stuffer-chain serialize:serialize-stuffer (hash:md5-stuffer (build-path (find-system-path 'home-dir) ".urls"))))

(define (start req) (xexpr:response/xexpr `(html (body (h2 "Look ma, no state!")))))

(servlet-env:serve/servlet start)

Run this version and it works! We are directed to a browser with our HTML. But we should clean this code up a bit. We no longer need to export anything so we'll drop the provide line. We aren't even using the interface-version and stuffer code. Things seem to be fine without them, so we'll drop those too. Also, looking at the serve/servlet documentation we notice some other nice arguments we can tack on.

lang racket/base

(require (prefix-in xexpr: web-server/http/xexpr) (prefix-in servlet-env: web-server/servlet-env))

(define (start req) (xexpr:response/xexpr `(html (body (h2 "Look ma, no state!")))))

(servlet-env:serve/servlet start #:servlet-path "/" #:servlet-regexp rx"" #:stateless? #t)

Ah, that's much cleaner. When you run this code, you will no longer be directed to the /servlets/standalone.rkt path but to the site root -- set by the #:servlet-path optional variable. Also, every other path you try to reach such as /foobar will successfully map to the start function -- set by the #:servlet-regexp optional variable. Finally, we also found the configuration to set the servlet stateless -- set by the optional variable #:stateless?.

But this is missing two things we could really use out of a simple web service. The first is routing. We do that by looking up the documentation for the web-server/dispatch module. We'll use this module to define some routes -- adding a 404 route to demonstrate the usage.

lang racket/base

(require (prefix-in dispatch: web-server/dispatch) (prefix-in xexpr: web-server/http/xexpr) (prefix-in servlet: web-server/servlet-env))

(define (not-found-route request) (xexpr:response/xexpr `(html (body (h2 "Uh-oh! Page not found.")))))

(define (home-route request) (xexpr:response/xexpr `(html (body (h2 "Look ma, no state!!!!!!!!!")))))

(define-values (route-dispatch route-url) (dispatch:dispatch-rules [("") home-route] [else not-found-route]))

(servlet:serve/servlet route-dispatch #:servlet-path "/" #:servlet-regexp #rx"" #:stateless? #t)

Run this version and check out the server root. Then try any other path. Looks good. The final missing piece to this simple web service is logging. Thankfully, the web-server/dispatch-log module has us covered with some request formatting functions. So we'll wrap the route-dispatch function and we'll print out the formatted request.

lang racket/base

(require (prefix-in dispatch: web-server/dispatch) (prefix-in dispatch-log: web-server/dispatchers/dispatch-log) (prefix-in xexpr: web-server/http/xexpr) (prefix-in servlet: web-server/servlet-env))

(define (not-found-route request) (xexpr:response/xexpr `(html (body (h2 "Uh-oh! Page not found.")))))

(define (home-route request) (xexpr:response/xexpr `(html (body (h2 "Look ma, no state!!!!!!!!!")))))

(define-values (route-dispatch route-url) (dispatch:dispatch-rules [("") home-route] [else not-found-route]))

(define (route-dispatch/log-middleware req) (display (dispatch-log:apache-default-format req)) (flush-output) (route-dispatch req))

(servlet:serve/servlet route-dispatch/log-middleware #:servlet-path "/" #:servlet-regexp #rx"" #:stateless? #t)

Run this version and notice the logs displayed for each request. Now you've got a simple web service with routing and logging! I hope this gives you a taste for how easy it is to build simple web services in Racket without downloading any third-party libraries. Database drivers and HTML template libraries are also included and similarly well-documented. In the future I hope to add an example of a slightly more advanced web service.

I have had huge difficulty discovering the source of Racket libraries. These library sources are nearly impossible to Google and search on Github is insane. Best scenario, the official racket.org docs would link directly to the source of a function when the function is documented. Of course I could just download the Racket source and start grepping... but I'm only so interested.

Walking through a basic Racket web service https://t.co/J3us48kzga @racketlang
 Phil Eaton (@phil_eaton) December 30, 2016

菜单
主要内容

书签 设置 文档

notes.eatonphil.com
13 七月 2025

eatonphil.com
阅读约 3 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Deploying FreeBSD on Linode unattended in minutes

I became a FreeBSD user over 2 years ago when I wanted to see what all the fuss was about. I swapped my y410p dual-booting Windows / Ubuntu with FreeBSD running Gnome 3. I learned a lot during the transition and came to appreciate FreeBSD as a user. I soon began running FreeBSD as my OS of choice on cloud servers I managed. So naturally, when I started working at Linode a year ago I wanted to run FreeBSD servers on Linode too.

Linode is a great platform for running random unofficial images because you have much control over the configuration. I followed existing guides closely and was soon able to get a number of operating systems running on Linodes by installing them manually: FreeBSD, OpenBSD, NetBSD, Minix3, and SmartOS to date.

Unofficial images come at a cost though. In particular, I became frustrated having to reinstall using the installer every time I managed to trash the disk. So over the past year, I spent time trying to understand the automated installation processes across different operating systems and Linux distributions.

Unattended installations are tough. The methods for doing them differ wildly. On RedHat, Fedora, and CentOS there is Kickstart. On Debian and Ubuntu there is preseeding. Gentoo, Arch, and FreeBSD don't particularly have a framework for unattended installs, but the entire installation process is well-documented and inherently scriptable (if you put in the effort). OpenBSD has autoinstall. Trying to understand each and every one of these potential installation methods was pretty defeating for getting started on a side-project.

A few weeks ago, I finally had the silly revelation that I didn't need to script the installation process -- at least initially. I only had to have working images available somewhere that could be copied to new Linodes. Some OSs / distributions may provide these images, but there is no guarantee that they exist or work. If I tested and hosted them for Linodes, anyone could easily run their own copy.

I began by running the installation process as normal for FreeBSD. After the disk had FreeBSD installed on it, I rebooted into Finnix, made a compressed disk image, and transferred it to an "image host" (another Linode in Fremont running an FTP server). Then I tested the reversal process manually to make sure a new Linode could grab the image, dd it to a disk, reboot and have a working filesystem and networking. (This transfer occurs over private networking to reduce bandwidth costs and thus limits Linode creation to the datacenter of the image host, Fremont.)

Then it was time to script the process. I looked into the existing Linode API client wrappers and noticed none of them were documented. So I took a day to write and document a good part of a new Linode Python client.

I got to work and out came the linode-deploy-experimental script. To run this script, you'll need an API token. This script will allow you to deploy from the hosted images (which now include FreeBSD 11.0 and OpenBSD 6.0). Follow the example line in the git repo and you'll have a Linode running OpenBSD or FreeBSD in minutes.

Clearly there's a lot of work to do on both this script and on the images:

Fremont datacenter has the only image host.
The script does not change the default password: "password123". You'll want to change this immediately.
The script does not automatically grow the file system after install.
The TTY config for these images currently requires you to use Glish instead of Weblish.
And more.

Even if many of these issues do get sorted out (I assume they will), keep in mind that these are unofficial, unsupported images. Some things will probably never work: backups, password reset, etc. If you need help, you are probably limited to community support. You can also find me with any questions (peaton on OFTC). But for me this is at least a slight improvement on having to run through the install process every time I need a new FreeBSD Linode.

Deploy FreeBSD and OpenBSD unattended on Linode https://t.co/j5A46ROqNM https://t.co/HSqrIvBMFj
 Phil Eaton (@phil_eaton) March 12, 2017

菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn
阅读约 1 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Django开发中使用Cache缓存提升10倍效率 Django开发中使用Cache缓存提升10倍效率

自定义任务引擎Probius上线运行一段时间后,小伙伴跟我反馈有部分任务执行时间太长,等的花儿都谢了,例如下边这个任务竟然执行了超过24分钟

查看每个子任务的执行时间,发现单单消耗在“YARN安装模块”这个子任务的时间就超过20分钟,检查这个子任务的执行逻辑发现并没有发现问题,抛弃Probius,直接在服务器上执行这个子任务惊奇的发现执行时间只有2分钟,由此断定肯定是Probius的问题了

又一次看了下这个子任务,发现任务的输出日志超过1w条,瞬间就知晓了其中的问题,经过简单修改再次测试,原本执行24分钟的任务执行时间缩短至3分钟以内,效率提升相当明显

究竟改了什么拥有如此魔力呢?先来看下下边这段代码

class Logger: def init(self, tid, state=None): self.tid = tid self.state = state self.datetime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))

def add(self, details):
    subtasklog = SubTaskLog.objects.get(id=self.tid)

    if details:
        details = details.replace('\n', ' ').replace('\r', ' ')
        newlog = self.datetime + ' ' + details + ';'

        if subtasklog.details:
            subtasklog.details += newlog
        else:
            subtasklog.details = newlog

    if self.state is not None:
        subtasklog.state = self.state
        subtasklog.save()

任务执行会不断的输出日志,这些日志就通过上边的Logger类写入数据库,以便前端可以及时读取实时展示,乍看上去并无不妥,但当短时间内日志产生量非常大时便会频繁读写数据库,数据库压力过大从而影响整个程序的执行效率。缓解数据库压力的有效方法就是加缓存

其实当初在写这段代码的时候就考虑到了用缓存,之所以没有用的主要是因为:在项目设计的过程中我提倡尽量减少依赖,不过度设计,以实现需求为目标,尽量让项目简单,这样协作的小伙伴看起代码来不费劲,出了问题还容易查找原因。正常情况下任务的日质量都不大,数据库处理起来也不费劲,能满足需求,引入缓存势必要增加依赖,让项目更复杂,所以就没有加。但从上边的问题来看,数据库已无法满足需求,增加缓存就很有必要了 Django Cache

Django本身就带有一个强大的缓存系统,提供不同级别的缓存粒度:可以缓存特定的视图,也可以只缓存部分模板片段,还可以缓存整个网站。但这几类都不是我想要的,本篇文章不会介绍以上几类缓存的使用,需要的话可以参考官网写的很详细

Django同时还提供了底层缓存API,可以使用这个API以任意级别粒度在缓存中存储对象。这正是我所需要的,每次产生的新日志都不再直接写入数据库,而是先写入缓存中,待任务执行完成后一次写入数据库,这样将大大降低对数据库的消耗,且缓存大都使用内存来存储,读写效率极高 缓存配置

Django的底层缓存API使用非常简单,首先需要在配置文件中配置启用缓存,settings.py文件中添加如下代码

CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': '127.0.0.1:11211', } }

这里使用了Memcached作为缓存服务,Memcached是一个完全基于内存的缓存服务器,是Django原生支持的最快、最高效的缓存类型,其他还支持的缓存类型有

数据库缓存:django.core.cache.backends.db.DatabaseCache,LOCATION为表名
文件系统缓存:django.core.cache.backends.filebased.FileBasedCache,LOCATION为文件路径
本地内存缓存:django.core.cache.backends.locmem.LocMemCache,LOCATION被用于标识各个内存存储
虚拟缓存:django.core.cache.backends.dummy.DummyCache,仅用于开发模式,只是实现缓存接口,并不做其他操作
自定义的缓存后台,例如redis等

我原本是想直接使用本地内存缓存的,这样就无需再安装Memcached服务了,但是本地内存缓存为进程私有,不可跨进程访问,这就产生了一个问题就是Logger进程写入内存缓存后,我前端展示的进程读不到,就无法实时输出日志了,遂放弃内存缓存,改用Django支持最好的Memcached

使用Memcached前需要先安装memcached服务,以及python连接memcached的包

debian系统安装memcached服务

apt-get install memcached

安装python连接memcached的包python-memcached

pip install python-memcached

每个缓存后端都支持配置额外的参数,从而来控制缓存的行为,有效的参数如下:

TIMEOUT: 用于缓存的默认超时时间,以秒为单位,默认为300秒,当设置为None时表示永不过时,设置为0表示立刻过期不缓存

KEY_PREFIX: 缓存键前缀,如果有设置,则这个设置的值将自动添加到Django服务器使用的所有缓存键之前

VERSION: 通过Django服务器生成的缓存键的默认版本号,有点类似与Redis的db,以下例子能清晰展示VERSION的作用

from django.core.cache import cache

cache.set('site', 'ops-coffee.cn', version=37)

cache.get('site')

cache.get('site', version=37) 'ops-coffee.cn'

OPTIONS: 传递到缓存后端服务的参数,例如我要传递username、password等参数到后端的memcached服务,那么可以这样写

CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', 'LOCATION': '127.0.0.1:11211', 'OPTIONS': { 'binary': True, 'username': 'user', 'password': 'pass', 'behaviors': { 'ketama': True, } } } }

缓存访问

开启Django Cache配置后,就可以使用缓存服务了,基本用法如下

from django.core.cache import cache

cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)

其中key是一个字符串,value是一个认可picklable形式的python对象,timeout和version参数都是可选的,timeout默认为CACHES配置中相应后端的timeout参数,version为对应的版本,参考上边关于VERSION的解释

cache.set('site', 'ops-coffee.cn')

cache.get('site') 'ops-coffee.cn'

cache.get(key, default=None, version=None)

新的参数default的意思是,当请求的key不存在时,则返回default设置的这个值,而不是默认不存在返回的`None

cache.get('name')

cache.get('name', 'has expired') 'has expired'

cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)

与cache.set类似,只是当add的key不存在时,则新建key,存在则不做任何操作

cache.add('site', 'https://blog.ops-coffee.cn') False cache.get('site') 'https://ops-coffee.cn'

cache.get('name') cache.add('name', '运维咖啡吧 博客') True cache.get('name') '运维咖啡吧 博客'

新建成功则会返回True,否则返回False

cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)

需要2个参数,第一个为key,第二个为key不存在时设置的值。get_or_set意思是如果key存在,则返回key的值,如果不存在则给key设置一个值

cache.get('name') '运维咖啡吧 博客' cache.get_or_set('name', '咖啡吧博客') '运维咖啡吧 博客'

cache.get('path') cache.get_or_set('path', '/devops') '/devops' cache.get('path') '/devops'

cache.get_many(keys, version=None)

通过传入一个keys列表,以字典格式返回这些列表中key存在的缓存值

cache.add('name', '运维咖啡吧 博客') True cache.set('site', 'https://ops-coffee.cn') cache.get_many(['site','name','path'])

cache.set_many(dict, timeout)

通过传入字典,批量设置缓存

cache.set_many({'site':'ops-coffee.cn','name':'运维咖啡吧'})

cache.delete(key, version=None)

删除一个key

cache.delete('site')

cache.delete_many(keys, version=None)

批量删除key

cache.delete_many(['site','name'])

cache.clear()

清空缓存,需要注意的是这会删除缓存里的所有key,可能包含一些并不是你设置的key

cache.clear()

cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)

更新key的过期时间为timeout设置的值,timeout是可选的,如果不写则默认为CACHES设置的TIMEOUT值

cache.touch('site', 3) True

更新成功则返回True,否则返回False

cache.incr(key, delta=1, version=None)

incr递增一个已存在的int类型的key的值,默认情况下递增幅度为1,通过指定delta可以设置递增的幅度

cache.set('num', 1) cache.incr('num') 2 cache.incr('num', 10) 12

cache.decr(key, delta=1, version=None)

与incr递增类似,decr为递减

cache.decr('num') 11 cache.decr('num', 5) 6

cache.close()

如果缓存后端已经实现了close()方法,可以通过cache.close()关闭和缓存的连接 代码优化

知道了代码存在的问题,也了解了Django中如何操作Cache,那么就可以着手优化上边的代码了,优化后的代码如下:

class Logger: def init(self, tid, state=None): self.tid = tid self.state = state self.datetime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))

    self.key = 'engine_subtasklog_%d' % self.tid

def add(self, details, sync=False):
    subtasklog = SubTaskLog.objects.get(id=self.tid)

    if details:
        details = details.replace('\n', ' ').replace('\r', ' ')
        newlog = self.datetime + ' ' + details + ';'

        # 将详情更新到缓存中
        old_log = cache.get(self.key, '')
        cache.set(self.key, old_log + newlog) if old_log else cache.set(self.key, newlog)

    if self.state is not None:
        subtasklog.state = self.state

        # 将缓存写入数据库并保存
        subtasklog.details = cache.get(self.key)
        subtasklog.save()

        # 删除缓存
        cache.delete(self.key)

根据任务ID创建缓存key,这条任务下的所有Log都会先写入缓存中,当任务结束时再将缓存中的日志一把写入数据库,从而减轻数据库压力,执行效率得到了极大的提升

📅 2020-10-26
🏷️ Devops
🏷️ Django
🏷️ 任务系统
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

证书监控 | 为什么我获取到的证书有效期是错的 证书监控 | 为什么我获取到的证书有效期是错的

今天咖啡吧的一个小伙伴在实践『Python实现HTTPS网站证书过期监控及更新』后反馈,通过文章内的代码获取到的证书过期时间是错的

看到这个消息,第一反应就是他的网络或环境可能有问题导致获取到了错误的证书,因为其一我所有文章里的代码都是亲自跑过的,应该不会有明显的BUG,其二获取证书过期时间是通过pyOpenSSL模块来实现的,底层使用的openssl成熟且稳定。但本着对技术严谨的态度,还是询问小伙伴是否可以提供域名我来亲自测试下

小伙伴很快把域名发了过来(为了保护隐私,小伙伴的域名我统一用blog.ops-coffee.cn来代替),我打开浏览器查看域名有效期时间,从2020年10月22日到2021年10月22日

通过如下代码,也就是文章『Python实现HTTPS网站证书过期监控及更新』里提供的获取域名过期时间的代码,拿到的过期时间是2021年8月29日

from _datetime import datetime from urllib3.contrib import pyopenssl

def get_expire(domain): try: certificate = pyopenssl.ssl.get_server_certificate((domain, 443)) data = pyopenssl.OpenSSL.crypto.load_certificate(pyopenssl.OpenSSL.crypto.FILETYPE_PEM, certificate)

    expire_time = datetime.strptime(data.get_notAfter().decode()[0:-1], '%Y%m%d%H%M%S')
    expire_days = (expire_time - datetime.now()).days

    return True, 200, {'expire_time': str(expire_time), 'expire_days': expire_days}
except Exception as e:
    return False, 500, str(e)

if name == 'main': print(get_expire('blog.ops-coffee.cn'))

[email protected]:~# python ops-coffee.py (True, 200, {'expire_time': '2021-08-29 12:00:00', 'expire_days': 309})

咦?还真的是我错了!代码很简单,应该不会有逻辑BUG,那就可能是openssl获取到的数据就有问题,于是直接通过openssl命令来验证,果然是,命令返回与代码返回过期时间一致,都是2021年8月29日

[email protected]:~# echo | openssl s_client -connect "blog.ops-coffee.cn":443 2>/dev/null | openssl x509 -noout -dates notBefore=Aug 28 00:00:00 2020 GMT notAfter=Aug 29 12:00:00 2021 GMT

看来真的是我错了,啪啪打脸,究竟会是哪里的问题?这时我想到了SNI,那么什么是SNI,SNI又有什么作用呢

SNI:Server Name Indication,服务名称标识,是一项用于改善SSL/TLS的技术,在SSLv3/TLSv1中被启用。它允许客户端在发起SSL握手请求时提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书

这有什么用处呢?在早期是SSLv2设计中,默认认为一台服务器或者一个IP地址只会部署一个web服务,客户端与服务器通信时也不需要关心客户端请求的是哪个域名的证书(因为默认服务器上只部署了一个证书),随着虚拟主机技术的发展,在一台服务器上部署多个web服务变得非常普遍,但SSL协议又没有请求hostname的记录,这就导致了服务器不知道要发送哪个证书给客户端,默认就返回服务器上配置的第一个可用证书给客户端

也就是说当客户端没有发送SNI信息,且请求的服务器上部署了多个HTTPS服务时,得到的证书信息可能就是错误的,为了验证猜想,我在openssl命令中添加了servername选项,再次查看返回的结果,过期时间2021年10月22日

[email protected]:~# echo | openssl s_client -servername blog.ops-coffee.cn -connect "blog.ops-coffee.cn":443 2>/dev/null | openssl x509 -noout -dates notBefore=Oct 23 00:00:00 2020 GMT notAfter=Oct 22 23:59:59 2021 GMT

又跟小伙伴确认了一下,确认了以上的猜想,确实是SNI的问题,破案了

知道了问题所在,那就改下代码修复问题吧,修复后更为强壮的代码如下

from _datetime import datetime from urllib3.contrib import pyopenssl

def get_expire(domain): try: conn = pyopenssl.ssl.create_connection((domain, 443)) sock = pyopenssl.ssl.SSLContext(pyopenssl.ssl.PROTOCOL_SSLv23).wrap_socket(conn, server_hostname=domain)

    certificate = pyopenssl.ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
    data = pyopenssl.OpenSSL.crypto.load_certificate(pyopenssl.OpenSSL.crypto.FILETYPE_PEM, certificate)

    expire_time = datetime.strptime(data.get_notAfter().decode()[0:-1], '%Y%m%d%H%M%S')
    expire_days = (expire_time - datetime.now()).days

    return True, 200, {'expire_time': str(expire_time), 'expire_days': expire_days}
except Exception as e:
    return False, 500, str(e)

if name == 'main': print(get_expire('blog.ops-coffee.cn'))

重要的是server_hostname参数,在请求时带上hostname标识,就能得到正确的结果啦

📅 2020-10-23
🏷️ Python
🏷️ 监控
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

python list 列表操作总结 python list 列表操作总结 添加元素至末尾

list.append('ops-coffee.cn')

添加元素至开头

list.insert(0, 'ops-coffee.cn')

列表合并

listA.extend(listB)

列表反转

list[::-1]

获取交集

list(set(listA).intersection(set(listB)))

获取并集

list(set(listA).union(set(listB)))

获取差集

list(set(listA).difference(set(listB)))

📅 2020-10-22
🏷️ 日常技术
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Django Template 自定义全局变量 Django Template 自定义全局变量

当我在编写一个高度自定义的后台系统时,我希望将网站的一些信息存储在数据库中,用户可以通过页面修改就能生效,就像下图配置一样

通常页面中用到的变量需要在view中回传,但站点名称网站中所有页面都需要用到,难道每一个view都需要回传一遍吗?这让我想到了页面中经常用到的获取用户名方法request.user.username,request变量并没有在每个view中回传,但所有页面都可以调用,他是如何实现的?下文将为你详细介绍,了解之后可以通过编写自定义的全局变量,轻松解决以上问题 request哪里来的

在日常开发Django的过程中,如果你有用到默认的template,那么通常会通过request.user.username来获取登陆用户的用户名,你有没有想过这个request是哪来的?即便是后端view里不返回这边变量依然可以使用,怎么会如此神奇

这要从Django默认配置文件settings.py里的TEMPLATES配置说起,默认的TEMPLATES配置如下

TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]

其中BACKENDS指定了Djanog默认使用的模版引擎,默认的是Django自己开发的DjangoTemplates,当然你也可以替换成功能更为强大的jinja2,DIRS指定了项目中模板文件的位置,APP_DIRS配置是否开启在已安装的app下查找template,OPTIONS则指定template的后端设置

之所以可以在模板中使用request的奥秘便隐藏在context_processors下,这个配置指定了Django渲染时执行的Python路径列表。当模板在渲染时,会执行context_processors列表中的所有函数,并将结果与上下文的context进行合并,也就是说模板接收到的参数除了render返回的context外,还有以上这个列表执行返回的结果

django.template.context_processors.request函数返回的字典中包含了request

def request(request): return {'request': request}

所以如下这样一个view,即便是返回结果中没有返回request,我们依然可以在setting.html模版中使用request.user

def setting(request): return render(request, 'setting.html', {})

同样的我们可以直接在模板中使用perms.opscoffee.select_user来判断用户是否拥有相应的权限,而不需要在view中返回perms,这也是因为django.contrib.auth.context_processors.auth返回了perms

def auth(request): """ Return context variables required by apps that use Django's authentication system.

If there is no 'user' attribute in the request, use AnonymousUser (from
django.contrib.auth).
"""
if hasattr(request, 'user'):
    user = request.user
else:
    from django.contrib.auth.models import AnonymousUser
    user = AnonymousUser()

return {
    'user': user,
    'perms': PermWrapper(user),
}

类似request和perms这些在所有模板中都可以调用的变量,可以看作是django的全局变量了 如何自定义全局变量

上边我们知道了Django是如何定义全局变量requests的,那么我们只需照虎画猫写个类似的函数并加入context_processors就可以了,实现步骤如下

先在名为commons的APP下创建个文件context_processors.py,编写如下的函数,返回site作为全局变量

from commons.models import Setting

def site(request): site_name = Setting.objects.filter(key='site_name') site_title = Setting.objects.filter(key='site_title')

site_name = site_name.first().value if site_name else ''
site_title = site_title.first().value if site_title else ''

return {'site': {'site_name': site_name, 'site_title': site_title}}

然后在TEMPLATES的context_processors写上对应的路径

TEMPLATES = [ { ... 'OPTIONS': { 'context_processors': [ ... 'commons.context_processors.site', # 添加这一行信息 ], }, }, ]

最后就可以在模板中通过site.site_title来获取站点title,以及通过site.site_name获取站点名称

{{ site.site_title }}

至此,问题顺利解决

📅 2020-10-16
🏷️ Django
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn
阅读约 1 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

用Python写个Linux系统命令 用Python写个Linux系统命令

这篇文章介绍如何写个系统命令以及我为什么要写命令

“一切皆文件”是linux的基本哲学之一,我们在linux下执行的诸如ls之类的命令实际上都是去执行了系统上的某个文件,which命令可以查看到我们执行的命令对应的是系统上的哪个文件,例如常用的ls命令实际上就是执行了/bin/ls这个文件

root@ops-coffee:~# which ls /bin/ls

基于此,我们就知道了定义一个命令很简单,只需要写个可执行的文件就行了,python的标准模块argparse就可以帮助我们快速方便的构建一个用户友好的命令 argparse

相比于自己实现个命令文件,argparse模块能够自动生成帮助和使用手册,并在用户传入无效参数时报错。一个简单的示例如下

!/usr/bin/env python3

coding:utf8

import argparse

parser = argparse.ArgumentParser(description='整数处理') parser.add_argument('integers', type=int, help='要处理整数')

args = parser.parse_args() print(args.integers)

这个示例的意思是接收一个数字,并将这个数字输出,接下来看一下详细的解释

首先创建了一个ArgumentParser对象,ArgumentParser对象有很多参数可以选择,这里的description定义在参数帮助文档之前显示的文本,通常用来定义这个程序做什么以及怎么做

parser = argparse.ArgumentParser(description='整数处理')

然后通过add_argument给ArgumentParser对象添加参数,第一个参数integers为参数名,type指定类型为int,help指定这个字段的帮助信息

parser.add_argument('integers', type=int, help='要处理的整数')

通过调用ArgumentParser对象的parse_args方法返回一个具有所有参数属性的对象

args = parser.parse_args()

最后通过args.参数名获取到传入的参数值

print(args.integers)

执行

我们将以上文件命名为opscoffee,并赋予执行权限,放在系统环境变量/bin下,就可以当作命令直接执行了

chmod +x opscoffee

mv opscoffee /bin/

如果直接执行opscoffee命令的话将会收到一个报错,提示你必须有一个参数integers

opscoffee

usage: opscoffee [-h] integers opscoffee: error: the following arguments are required: integers

同时也通过usage告诉了你这个命令的用法,默认有一个-h参数可以打印命令帮助

opscoffee -h

usage: opscoffee [-h] integers

整数处理

positional arguments: integers 要处理的整数

optional arguments: -h, --help show this help message and exit

这就是使用argparse模块的好处,自动生成帮助,提供友好的使用体验 参数

argparse能实现的远不止于此,还有更加强大的功能,主要在于add_argument方法参数的运用,以下以一些例子来学习下一些常用的参数 可选参数

当我们在参数名前添加-或者--时,argparse会默认认为这是一个可选参数,可以不传值,例如

parser.add_argument('--age', type=int, help='年龄')

可选参数当没有传值时的默认值为None,可以通过default来设置默认值

parser.add_argument('--age', type=int, default=37, help='年龄')

当没有参数--age时,显示default设置的值(没有设置default则显示None),有--age则显示--age指定的值

opscoffee

37

opscoffee --age 38

38

如果你想让可选参数也变成必选的,则只需要设置required=True即可

parser.add_argument('--age', type=int, default=37, required=True, help='年龄')

type

type用来指定参数的类型,允许任何类型检查和类型转换,例如str、int、float、open甚至是你自定义的方法都可以。

!/usr/bin/env python3

coding:utf8

import argparse

def even(string): value = int(string)

if value%2!=0:
    msg = "%r 不是偶数" % string
    raise argparse.ArgumentTypeError(msg)
return value

parser = argparse.ArgumentParser(description='整数处理') parser.add_argument('integers', type=even, help='要处理的整数')

args = parser.parse_args() print(args.integers)

以上命令接收一个参数integers,并将其type设置为了自定义方法even,这个方法会判断用户输入的数字是否是偶数,如果不是则报错,执行结果如下

opscoffee 1

usage: opscoffee [-h] integers opscoffee: error: argument integers: '1' 不是偶数

opscoffee 2

2

choices

choices参数可以限制参数的范围,例如我们只想让用户从ops或coffee两个参数中选择一个输入,则可以这样用

parser.add_argument('site', choices=['ops','coffee'], help='Site')

那么当我们输入的内容不是ops或coffee时,则报错

opscoffee cn

usage: opscoffee [-h] {ops,coffee} opscoffee: error: argument site: invalid choice: 'cn' (choose from 'ops', 'coffee')

nargs

通常我们在argparse中定义的参数数量与传入的参数数量应当相等,但有些时候我们需要接收未知数量的参数,nargs就可以帮助我们

nargs支持以下值:N(整数)、'?'、'*'、'+'、argarse.REMAINDER

N: 表示N个参数会被聚集到一个列表中,例如

import argparse

parser = argparse.ArgumentParser(description='整数处理') parser.add_argument('integers', type=int, nargs=2, help='要处理的整数')

args = parser.parse_args() print(args.integers)

将输出

opscoffee 9 10

[9, 10]

'?': 表示使用一个或不使用参数,当不传参数时,默认为None

parser.add_argument('integers', type=int, nargs='?', help='要处理的整数')

'*': 表示使用所有参数,参数个数可以为0

parser.add_argument('integers', type=int, nargs='*', help='要处理的整数')

'+': 与'*'类似,但至少要有一个参数,否则将会报错

parser.add_argument('integers', type=int, nargs='+', help='要处理的整数')

自定义命令

先说我为什么要写个系统命令?

文章『Probius:一个功能强大的自定义任务系统』中介绍了我们的自定义任务系统,这个系统可以用来编排任务,而在编排CICD任务中会用到配置文件,我们的配置文件都是通过Kerrigan配置中心来管理的,目前获取配置中心的配置主要有两种方法

配合confd服务自动拉取更新,详细内容可以查看这篇文章:中小团队落地配置中心详解
配置中心提供API,可以通过API获取配置内容

脚本里如果想要使用配置中心的配置,则只能通过API的方式去获取,这样就要在每个需要用到配置的地方写一段代码来获取及处理,不仅会出现大量的重复代码,并且非常的不优雅,更为重要的是请求API的Token将会出现在脚本里,带来一定的安全风险

基于以上考虑,写个自定义命令来做这件事情更为妥当,于是便写了下边这个命令

!/usr/bin/env python3

coding:utf8

这是一个系统命令用来获取kerrigan配置中心的配置并写入本地文件,需要将此文件copy到目录/bin下

import sys import argparse import requests

parser = argparse.ArgumentParser(description='获取配置中心Kerrigan配置')

parser.add_argument('configkey', type=str, help='配置中心中文件的Key') parser.add_argument('localfile', type=str, help='保存到本地文件的路径')

args = parser.parse_args()

获得传入的参数

configkey = args.configkey localfile = args.localfile

header = { 'Authorization': 'Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.JTdCJTIyZXhwJTIyJTNBMTkwNjcwNzAyMCUyQyUyMmlhdCUyMiUzQTE1OTEzNDcwMjAlMkMlMjJkYXRhJTIyJTNBJTdCJTIydXNlcm5hbWUlMjIlM0ElMjJwcm9iaXVzQG9wcy1jb2ZmZWUuY24lMjIlN0QlN0Q.ops1ZNhq19XSEL2PUo-iQqzbhimDnpFiYc_7EUXftF4'}

uri = 'http://kerrigan.ops-coffee.cn/api/config/?key=' + configkey r = requests.get(uri, headers=header)

if r.json()['state']: content = r.json()['message']['content']

try:
    with open(localfile, 'w') as f:
        f.write(content)

    sys.exit(0)
except Exception as e:
    print('write local file failed: ', str(e))
    sys.exit(3)

else: print('get config failed: ', r.json()['message']) sys.exit(1)

以上代码的意思是根据传入的key和file路径,去配置中心获取对应配置文件的内容并写入到本地file中。需要注意的是exit,返回合适的退出状态是个很好的习惯,这样我们就可以通过$?来获取命令执行成功还是失败

将此文件命名为getconfig并移动到/bin目录下添加执行权限,就可以在系统任何地方使用getconfig命令了

getconfig /conf/coffee/prod/docker/Dockerfile /home/project/coffee/Dockerfile

📅 2020-06-09
🏷️ Python
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Django DEBUG = False导致静态文件加载失败500问题处理 Django DEBUG = False导致静态文件加载失败500问题处理

只需三步即可轻松解决:

1.确保settings.py文件有以下三个配置

STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, '/static/') STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

2.在urls中添加如下配置

from django.views import static from django.conf import settings from django.urls import path, re_path, include

urlpatterns = [ re_path('static/(?P.*)$', static.serve, {'document_root': settings.STATIC_ROOT}), ]

3.控制台下执行collectstatic命令收集静态文件

python manage.py collectstatic

📅 2020-06-12
🏷️ Django
🏷️ 日常技术
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Django ORM中原生JSONField的使用方法 Django ORM中原生JSONField的使用方法

带你尝鲜Django最新版重要更新JSONField的使用

Django最新版v3.1的主要更新之一便是完善了对JSON数据存储的支持,新增models.JSONField和forms.JSONField,可在所有受支持的数据库后端上使用

目前支持的数据库以及对应版本主要有MariaDB 10.2.7+,MySQL 5.7.8+,Oracle,PostgreSQL和SQLite 3.9.0+,但个别Django的查询方法可能与部分数据库不兼容,例如contains和contained_by就不支持Oracle和SQLite数据库 JSONField使用

from django.db import models

class Hero(models.Model): name = models.CharField(max_length=200) data = models.JSONField(null=True)

def __str__(self):
    return self.name

通过models.JSONField可指定此字段为存储类型为JSON格式。null=True表示此字段可以为空,这个NULL指的是SQL NULL,如果想存储为JsonNULL,则可以使用Value('null')来实现

Hero.objects.create(name='coffee', data=Value('null'))

SQL NULL与JsonNULL的区别主要在is_null的查询上不同,可以通过以下这个示例来理解下

from django.db.models import Value

Hero.objects.create(name='ops') Hero.objects.create(name='coffee', data=Value('null'))

Hero.objects.filter(data=None) ]> Hero.objects.filter(data=Value('null')) ]>

Hero.objects.get(name='ops').data Hero.objects.get(name='coffee').data

Hero.objects.filter(data__isnull=True) ]> Hero.objects.filter(data__isnull=False) ]>

JSONField查询

Json字段的查询方法,通过下边这个示例演示说明

Hero.objects.create(name='ops-coffee.cn', data={ ... 'age': 12, ... 'group': { ... 'name': 'ow1', ... 'skill': [ ... {'name': 'swim', 'rank': 'A+'}, ... {'name': 'shot', 'rank': None} ... ] ... } ... }) Hero.objects.create(name='ops-coffee', data={'age':16})

当想要查询age为12的数据时可以这样查询

Hero.objects.filter(data__age=12) ]>

当想要查询group的name为ow1的数据时可以这样查询

Hero.objects.filter(data__group__name='ow1') ]>

当想要查询group下skill中第一个数据的name值为swim的数据时可以这样查询

Hero.objects.filter(data__group__skill__0__name='swim') ]>

当想要查找包含group键的所有数据时,可以通过has_key来实现

Hero.objects.filter(data__has_key='group') ]>

当想要查找同时包含group键和age键的所有数据时,可以通过has_keys来实现

Hero.objects.filter(data__has_keys=['group','age']) ]>

当想要查找包含group键或者age键的所有数据时,可以通过has_any_keys来实现

Hero.objects.filter(data__has_any_keys=['group','age']) , \]>

当想一次性查找包含age为12且group的name为ow1的数据时,可以通过contains来实现

Hero.objects.filter(data__contains={'age':12,'group': {'name': 'ow1'}}) ]>

JSONField除了支持以上查询方式外,对于ORM所提供的大部分其他查询方式同样支持,例如icontains,endswith,iendswith,iexact,regex,iregex, startswith,istartswith,lt,lte,gt,gte,使用起来也是非常方便

Hero.objects.filter(data__age__lte=12) ]>

Hero.objects.filter(data__group__name__startswith='ow') ]>

SQLite3报错处理

我在Windows下开发测试时,当migrate生成表的时候遇到了如下报错,这主要是因为SQLite不支持JSONFields导致的

SQLite does not support JSONFields

解决方法为:

先去sqlite官网下载对应的DLL软件包https://www.sqlite.org/download.html,然后替换掉当前使用的sqlite3.dll文件。例如我的windows为64位版本,所以下载了sqlite-dll-win64-x64-3320300.zip这个软件包,本地python的安装路径为C:\python36,直接将C:\python36\DLLs\sqlite3.dll用下载的软件包里的sqlite3.dll文件替换,然后再次运行migrate顺利创建了数据库表

📅 2020-08-07
🏷️ Django
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


菜单
主要内容

书签 设置 文档

blog.ops-coffee.cn
15 七月 2025

ops-coffee.cn
阅读约 1 分钟

标签

new_dev_den

高亮

在文章中选择文本以创建新的高亮。 链接

Python实现HTTPS网站证书过期监控及更新 Python实现HTTPS网站证书过期监控及更新

当前HTTP逐渐被大众所抛弃,HTTPS正在成为互联网上的主流。前段时间我们维护的一个HTTPS证书即将过期,由于多云环境比较复杂,团队小伙伴在替换更新证书的过程中出现疏漏,导致有一个域名证书没有及时更新,影响了系统可用性,为了杜绝这种问题再次发生,便写了这么个功能

比较简单,但很实用,再也不会出现证书漏更新的问题,具体流程为:扫描域名列表-->检查是否开启HTTPS-->获取证书过期时间-->记录入库-->更新证书 获取域名列表

我们用了很多的内部私有云SAAS服务,这些SAAS服务都提供有完善的API支持,DNS服务便是其中之一,可以根据DNS系统提供的API拿到所有的域名和记录。公有云也提供有完善的API文档,这里以阿里云为例,获取域名记录的代码如下

import json from aliyunsdkcore.client import AcsClient from aliyunsdkcore.acs_exception.exceptions import ClientException from aliyunsdkcore.acs_exception.exceptions import ServerException from aliyunsdkdomain.request.v20180129.QueryDomainListRequest import QueryDomainListRequest from aliyunsdkalidns.request.v20150109.DescribeDomainRecordsRequest import DescribeDomainRecordsRequest

class DomainApi: def init(self): self.client = client = AcsClient('', '', 'cn-hangzhou')

def get_domains(self, pagenum=1, pagesize=10):
    try:
        request = QueryDomainListRequest()
        request.set_accept_format('json')

        request.set_PageNum(pagenum)
        request.set_PageSize(pagesize)

        response = self.client.do_action_with_exception(request)
        jsondata = json.loads(str(response, encoding='utf-8'))

        return True, jsondata
    except Exception as e:
        return False, str(e)

def get_records(self, domain):
    try:
        request = DescribeDomainRecordsRequest()
        request.set_accept_format('json')

        request.set_DomainName(domain)
        response = self.client.do_action_with_exception(request)

        jsondata = json.loads(str(response, encoding='utf-8'))
        return True, jsondata
    except Exception as e:
        return False, str(e)

if name == 'main': print(DomainApi().get_records('ops-coffee.cn'))

以上代码使用了阿里云提供的SDK,调用简单方便,最终返回请求状态及数据。get_domains方法可以获取到账号下的所有域名,get_records方法可以获取到域名下的所有解析记录,需要注意数据量大小,以确定是否需要分页查询 检查是否开启HTTPS

检查是否开启HTTPS也是简单粗暴,直接通过requests模块请求HTTPS地址,没有报错则表示开启了https支持,大概代码如下

session = requests.session()

try: session.get('https://' + domain) except Exception as e: print(e)

需要注意的是,如果批量扫描域名的话需要使用session模式,否则可能会因为链接过多而报错 获取证书过期时间

之后再通过pyopenssl模块来拿到域名的HTTPS证书过期时间,代码如下

from _datetime import datetime from urllib3.contrib import pyopenssl

def get_expire(domain): try: certificate = pyopenssl.ssl.get_server_certificate((domain, 443)) data = pyopenssl.OpenSSL.crypto.load_certificate(pyopenssl.OpenSSL.crypto.FILETYPE_PEM, certificate)

    expire_time = datetime.strptime(data.get_notAfter().decode()[0:-1], '%Y%m%d%H%M%S')
    expire_days = (expire_time - datetime.now()).days

    return True, 200, {'expire_time': str(expire_time), 'expire_days': expire_days}
except Exception as e:
    return False, 500, str(e)

if name == 'main': print(get_expire('blog.ops-coffee.cn'))

使用之前需要先安装pyopenssl模块,这里建议使用python3.6及以上版本,除了get_notAfter可以拿到证书过期时间外,还有以下方法能够获取到更多证书相关的信息:get_notAfter,get_notBefore,get_pubkey,get_serial_number,get_signature_algorithm,get_subject,get_version,gmtime_adj_notAfter,gmtime_adj_notBefore,has_expired 定时执行入库

以上步骤会定时执行监控,当发现证书过期时间小于30天时发报警,执行日志就写入了数据库方便前端页面展示

class Domain(models.Model): create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

domain = models.CharField(max_length=64, verbose_name='一级域名')
jsondata = models.TextField(verbose_name='域名详情')

数据库就三个字段,在每次执行完一轮扫码后都会将详细信息计入上表中

jsondata = { 'items': [{ 'record': 'blog.ops-coffee.cn', 'rdtype': 'A', 'dept': '天玑' 'enable_https': 1, 'expire_time': '2021-10-22 12:00:00', 'expire_days': '387', 'notes': '' }], 'total_count': 26, 'record_a_count': 23, 'record_a_https_count': 19}

Domain.objects.create(domain='ops-coffee.cn', jsondata=jsondata)

前端获取最新一条数据展示

Domain.objects.filter(domain=domain).order_by('-create_time').first()

更新证书

证书更新是最繁琐的事情,因为涉及到多平台不同环境,每家公司情况可能都有不同,我们因为大量使用了SAAS服务,SAAS服务又提供有API,所以更新起来比较简单,调用API即

class LBCApi: def init(self): self.domain = 'https://lbc.ops-coffee.cn'

    self.headers = {
        'content-type': 'application/json',
        'Auth-Token': get_auth_token()[1]
    }

def update_cert(self, id, cert):
    try:
        data = json.dumps({"cert": cert})
        r = requests.put(self.domain + '/port/%d' % (id), data=data, headers=self.headers)

        if r.status_code != 200:
            return False, r.status_code, r.json()

        return True, 200, port['id']
    except Exception as e:
        return False, 500, 'PortId%s,更新证书失败' % str(e)

if name == 'main': state, code, data = LBCApi().update_cert(37, 'cert-ops-coffee-cn') print(state, code, json.dumps(data))

搞定收官!KPI++

📅 2020-09-30
🏷️ Python
🏷️ 监控
🏷️ 关于技术

能看到这里一定是真爱,订阅一下吧


https://github.com/SheafificationOfG/based-cpp

菜单
主要内容

书签 设置 文档

github.com
23 九月 2025

github.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 链接

Based C++ Based C++

This repo provides an implementation of the GNU Interface Layer (GIL) and standard library for g++. C++ is the best interpreted language

C++ is the best interpreted language! Usage

Here is a simple "Hello, world!" in C++.

include "gil/std.hpp"

using namespace gil::std;

volatile auto run = main<{ str::puts(str::literal("Hello, world!\n")) }>;

To run the program, run

g++ -std=c++23 -Based hello_world.cpp -o -

Note

The above command only works if you invoke g++ from the root of this repo; otherwise, you will have to provide the full path to the required abstract system emulator drivers (ased) folder. Examples

hello_world.cpp

    Be greeted in all of the languages supported by YVR!

hello_world_vmi.cpp

    Same as above, but without using the standard library.

calculator.cpp

    Perform a single binary operation on a pair of 64-bit signed integers.

mergesort.cpp

    Merge sort a comma-separated list of 64-bit signed integers.

菜单
主要内容

书签 设置 文档

www.bohaishibei.com
23 九月 2025

bohaishibei.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 链接

无脑反对预制菜和预制工艺很糟糕

@李子暘Lee:无脑反对预制菜和预制工艺,已经很糟糕,如果这种“叫魂”,扩展到针对整个现代食品加工工业,那就更糟糕了。

中国是世界农业超级大国,不但产量大,而且品种丰富,蔬菜水果经济作物,很多都是单项产量世界第一。

农业的核心问题或说难题,就是生产期和市场需求期,往往不匹配。旺季时,满坑满谷,扔都没地方扔。淡季时,一票难求,甚至干脆没有。

所以,巨大的农业产能,必须匹配强大的食品加工工业,用冷冻、仓储、预加工等多种办法,消除上述的“不匹配”,稳定市场供应,实现农业生产收入最大化,消费者也能随时享受“琳琅满目、供应充足”。

现在,冷冻肉被污名为“僵尸肉”。预制工艺被污名为“科技与狠活”。这种可叹的舆论环境,虽说不至于摧毁中国农业和食品加工工业,但势必额外增加很多困难和成本。

更多的解释、更多的掩饰,必然的,还有更多的欺诈,和辨别识破欺诈的资源投入。

或许,这些是不可避免的,谁让中国工业化太快大家观念跟不上呢。但负责任的人,至少应该努力降低这种“交易成本上升”,而不是火上浇油,更不应该浑水摸鱼火中取栗。

@渔老板钓鱼:预制菜(料理包)、外卖包装 微塑料双重伤害

老粉都知道,我一直讨厌料理包 就是那种袋装在水里煮半天,剪开给你吃的

料理包就是随着外卖发展起来的

料理包厂家,挣了钱,想要把水搅浑,找了协会出台一个标准把所有半成品、净菜、包括新鲜的食材都算预制菜。

其实以前大家都知道:预制菜就是料理包的新名词,就是低端外卖、快餐才用的

半成品、净菜、速冻饺子、冷冻肉、速冻蔬菜、方便面、火腿肠……都有自己的清晰的分类。以前大家根本没有任何歧义。

料理包不好听,跟着外卖平台补贴挣了大钱的料理包厂家和外卖品牌,起了个新名字:预制菜。然后把水搅浑。

起了个新名字,那就是新事物 为什么非要新名字装旧事物?

预制菜乱想就是这么来的

比如你去饺子,透明公示:速冻饺子。 OK了,复杂吗?

非要说:我们这个是即热菜肴,等级是IV级

脱了裤子放屁多此一举,把水搅浑,让消费者迷糊对不对?

明明速冻水饺一句话说明白的事情,非要往预制菜这个新名词里装?再定义出很多细分项目。

比如萨莉亚的沙拉,告诉你这个沙拉是:净菜,你就知道咋回事了。非要说:即食菜肴、等级I级。你还要去翻表格,啥是即食I级。

比如西贝、老乡鸡速冻西兰花,直接告诉消费者这个西兰花是速冻的,难道消费者听不懂? 你说:速冻西兰花是即配菜肴,级别是I级,但给速冻了,用的时候解冻,然后给你做。 这样消费者就听懂了?

明明很简单的事情,非要复杂化 吃鱼:活鱼、冰鲜鱼、冻鱼 这三个分类大家早就清晰明了知道咋回事,你告知消费者:这是冰鲜鱼、冻鱼,消费者听不懂吗?非要搞出个预制菜新名词,整很复杂的分类,然后再往里装? 越说越生气,哎

对预制菜,微博上没几个比我更了解它的发展历史和核心问题的。现在的乱象,早就猜到了。很多年前我就和那些料理包外卖品牌和厂家争吵过一次。

我斥责他们把料理包改成预制菜这个新名词,试图瞒天过海,把水搅浑、获得更大的利益。

@罗永浩的十字路口 我真的很希望和你连线一次,我不希望预制菜的水越来越浑,最后受伤的就是消费者。


Skip to content liuzhen932 的小窝

文章
朋友
关于

归档 搜索 开往-友链接力 上一页 博客更新实录:代码块体验翻倍,性能优化全公开! 2025-07-13 13:57:04 (更新) | 浏览 103 0x00 前情概要#

最近我花了不少时间对我的博客进行了一些技术上的优化和改进,目的就是为了给大家提供一个更快、更流畅且更加有趣的阅读体验。今天我想跟大家分享一下这些变化背后的故事。虽然这篇文章的确有点标题党,但我这个月的开销确实比上个月多了不少,扯远了让我们回到正题。 0x01 Expressive Code#

Expressive Code

就像它介绍的那样,Expressive Code 在 Web 上展示我们代码块,使其易于理解且有令人惊叹的视觉效果。通过使用 Expressive Code 的 JavaScript 库,我可以在我的 Astro 博客中展示代码块时,保留终端风格的 ANSI 转义序列,并且能够正确渲染它们。 渲染 ANSI 转义序列#

在开发实践中,我们经常需要展示带有颜色标记的命令行输出。传统的 Markdown 渲染器(例如 VS Code)会将 ANSI 转义序列直接显示为乱码字符,而 Expressive Code 能智能解析这些序列并转化为 CSS 样式。不信?试试下面的代码块: Terminal window

ANSI colors: - Regular: Red Green Yellow Blue Magenta Cyan - Bold: Red Green Yellow Blue Magenta Cyan - Dimmed: Red Green Yellow Blue Magenta Cyan

256 colors (showing colors 160-177): 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177

Standard ANSI colors: - Dimmed: Black Red Green Yellow Blue Magenta Cyan White - Foreground: Black Red Green Yellow Blue Magenta Cyan White - Background: Black Red Green Yellow Blue Magenta Cyan White - Reversed: Black Red Green Yellow Blue Magenta Cyan White

8-bit colors: \e[0;30m] \e[0;31m] \e[0;32m] \e[0;33m] \e[0;34m] \e[0;35m] \e[0;36m] \e[0;37m]
\e[0;40m] \e[0;41m] \e[0;42m] \e[0;43m] \e[0;44m] \e[0;45m] \e[0;46m] \e[0;47m]
\e[0;90m] \e[0;91m] \e[0;92m] \e[0;93m] \e[0;94m] \e[0;95m] \e[0;96m] \e[0;97m]
\e[0;100m] \e[0;101m] \e[0;102m] \e[0;103m] \e[0;104m] \e[0;105m] \e[0;106m] \e[0;107m] \e[1;30m] \e[1;31m] \e[1;32m] \e[1;33m] \e[1;34m] \e[1;35m] \e[1;36m] \e[1;37m]
\e[1;40m] \e[1;41m] \e[1;42m] \e[1;43m] \e[1;44m] \e[1;45m] \e[1;46m] \e[1;47m]
\e[1;90m] \e[1;91m] \e[1;92m] \e[1;93m] \e[1;94m] \e[1;95m] \e[1;96m] \e[1;97m]
\e[1;100m] \e[1;101m] \e[1;102m] \e[1;103m] \e[1;104m] \e[1;105m] \e[1;106m] \e[1;107m] \e[2;30m] \e[2;31m] \e[2;32m] \e[2;33m] \e[2;34m] \e[2;35m] \e[2;36m] \e[2;37m]
\e[2;40m] \e[2;41m] \e[2;42m] \e[2;43m] \e[2;44m] \e[2;45m] \e[2;46m] \e[2;47m]
\e[2;90m] \e[2;91m] \e[2;92m] \e[2;93m] \e[2;94m] \e[2;95m] \e[2;96m] \e[2;97m]
\e[2;100m] \e[2;101m] \e[2;102m] \e[2;103m] \e[2;104m] \e[2;105m] \e[2;106m] \e[2;107m] \e[3;30m] \e[3;31m] \e[3;32m] \e[3;33m] \e[3;34m] \e[3;35m] \e[3;36m] \e[3;37m]
\e[3;40m] \e[3;41m] \e[3;42m] \e[3;43m] \e[3;44m] \e[3;45m] \e[3;46m] \e[3;47m]
\e[3;90m] \e[3;91m] \e[3;92m] \e[3;93m] \e[3;94m] \e[3;95m] \e[3;96m] \e[3;97m]
\e[3;100m] \e[3;101m] \e[3;102m] \e[3;103m] \e[3;104m] \e[3;105m] \e[3;106m] \e[3;107m] \e[4;30m] \e[4;31m] \e[4;32m] \e[4;33m] \e[4;34m] \e[4;35m] \e[4;36m] \e[4;37m]
\e[4;40m] \e[4;41m] \e[4;42m] \e[4;43m] \e[4;44m] \e[4;45m] \e[4;46m] \e[4;47m]
\e[4;90m] \e[4;91m] \e[4;92m] \e[4;93m] \e[4;94m] \e[4;95m] \e[4;96m] \e[4;97m]
\e[4;100m] \e[4;101m] \e[4;102m] \e[4;103m] \e[4;104m] \e[4;105m] \e[4;106m] \e[4;107m] \e[5;30m] \e[5;31m] \e[5;32m] \e[5;33m] \e[5;34m] \e[5;35m] \e[5;36m] \e[5;37m]
\e[5;40m] \e[5;41m] \e[5;42m] \e[5;43m] \e[5;44m] \e[5;45m] \e[5;46m] \e[5;47m]
\e[5;90m] \e[5;91m] \e[5;92m] \e[5;93m] \e[5;94m] \e[5;95m] \e[5;96m] \e[5;97m]
\e[5;100m] \e[5;101m] \e[5;102m] \e[5;103m] \e[5;104m] \e[5;105m] \e[5;106m] \e[5;107m]

24-bit colors (full RGB): - Dimmed: ForestGreen - RGB(34,139,34) RebeccaPurple - RGB(102,51,153) - Foreground: ForestGreen - RGB(34,139,34) RebeccaPurple - RGB(102,51,153) - Background: ForestGreen - RGB(34,139,34) RebeccaPurple - RGB(102,51,153) - Reversed: ForestGreen - RGB(34,139,34) RebeccaPurple - RGB(102,51,153)

256 colors: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

Font styles: - Default - Bold - Dimmed - Italic - Underline - Reversed - Strikethrough

Text formatting: Bold Dimmed Italic Underline

使用 Bash 生成两个表格: Terminal window

生成 ANSI 颜色表格 ref: https://gist.github.com/JBlond/2fea43a3049b38287e5e9cefc87b2124?permalink_comment_id=5414733#gistcomment-5414733

for x in {0..5}; do echo --- && for z in 0 10 60 70; do for y in {30..37}; do y=$((y + z)) && printf '\e[%d;%dm%-12s\e[0m' "$x" "$y" "$(printf ' \e[%d;%dm] ' "$x" "$y")" && printf ' '; done && printf '\n'; done; done

生成 256 色表格

for i in {0..255}; do printf "\033[38;5;${i}m%3d\033[0m " "$i"; ((i % 20 == 19)) && echo; done

如你所见,Expressive Code 能够正确渲染 ANSI 转义序列,并将其转换为相应的 CSS 样式。这样一来,我就可以在博客中展示更丰富的代码块内容,而不必担心乱码问题。通过引入一个小小的组件,现在博客中的每个代码块都能保持终端原生或者 Github 风格的视觉体验,同时获得网页展示的灵活性。 自动换行#

说到代码块样式,最让我惊喜的是它支持智能换行功能。以前长代码总要在屏幕左右滑动找尾巴,现在只要设置个 wrap 参数,超长代码就会自动折行显示。就像下图这样(在手机上浏览长代码时再也不用左右滑动了): 使用 Expressive Code 的自动换行功能

function getLongString() { return "This is a very long string that will most probably not fit into the available space unless the container is extremely wide"; } getLongString();

对比传统代码块: 普通的代码块不会自动换行

function getLongString() { return "This is a very long string that will most probably not fit into the available space unless the container is extremely wide"; } getLongString();

终端风格的代码块#

是的,你没有看错!Expressive Code 还支持终端风格的代码块,这意味着我可以在博客中展示类似于命令行界面的代码块,使得代码块看起来更像是在真实的终端中运行的。 Homelab

This is a terminal-style code block

echo "Hello, World! It supports ANSI colors and styles."

Profile.ps1

Without overriding, this would be a terminal frame

function Watch-Tail { Get-Content -Tail 20 -Wait $args } New-Alias tail Watch-Tail

Collapsible Sections#

是的,我还安装了一个名为 Collapsible Sections 的插件,它允许我在代码块中创建可折叠的部分,非常适合隐藏冗长的代码或注释,帮助读者更聚焦于核心内容。这里贴上官网的示例: Collapsible Code Example

// All this boilerplate setup code will be collapsed import { someBoilerplateEngine } from "@example/some-boilerplate"; import { evenMoreBoilerplate } from "@example/even-more-boilerplate";

const engine = someBoilerplateEngine(evenMoreBoilerplate());

// This part of the code will be visible by default engine.doSomething(1, 2, 3, calcFn);

function calcFn() { // You can have multiple collapsed sections const a = 1; const b = 2; const c = a + b;

// This will remain visible console.log(Calculation result: ${a} + ${b} = ${c}); return c; }

// All this code until the end of the block will be collapsed again engine.closeConnection(); engine.freeMemory(); engine.shutdown({ reason: "End of example boilerplate code" });

如何安装?#

说了这么多,如何在自己的 Astro 博客中使用 Expressive Code 呢?其实非常简单,按照官网文档的说法,只需要在 Astro 项目中安装 astro-expressive-code 包,然后全站代码块就会自动使用 Expressive Code 渲染,无需额外配置! Terminal window

npx astro add astro-expressive-code

整个过程就像点外卖一样方便,装完即用,完全不用手动配置。现在我的博客终于能完美呈现代码的「原生质感」,朋友们都说像在看真正的开发环境演示。 0x02 Plausible Analytics#

为了更好地了解读者的行为和偏好,我还集成了 Plausible Analytics。这是一个轻量级、隐私友好的网站分析工具,能够提供关于访问量、用户来源、页面浏览等数据的详细统计。通过 Plausible,我可以更好地了解哪些内容最受欢迎,哪些页面需要优化,从而不断改进博客的质量和用户体验。Plausible 的安装也非常简单,只需在 Astro 项目中添加 script 标签即可。 为什么选择 Plausible Analytics?#

和 Google Analytics 不同,Plausible Analytics 更注重用户隐私和数据安全。它不使用 Cookies,也不收集个人数据,完全符合 GDPR 和 CCPA 等隐私法规。此外,Plausible 的界面非常简洁,易于理解,适合像我这样的小型博客使用。但是请注意,Plausible Analytics 后端服务器相当消耗资源,尤其是在访问量较大的时候。因此,我建议如果你有考虑自建 Plausible Analytics 的话,考虑选择 4GB 内存以上的云服务器。作为参考,我的实例分配了 8GB,毕竟这种分析工具在数据量大的时候还是挺吃资源的。 0x03 其他更新#

除了 Expressive Code 和 Plausible Analytics,我还对博客做了一些其他的更新和优化:

新增了几篇关于 Linux 部署的实战笔记,包括如何用 Docker Compose 搭建本地开发环境和用于生产的小技巧。最近收到读者留言说「你一顿吃多少 Emoji」,这很明显是关注我 Docker Hub 动态的读者,让我觉得花时间打磨这些细节挺值得的。
另外调整了移动端的排版,现在在地铁上刷文章时代码块不会自动缩放了
优化了博客 Open Graph 和 Twitter Card 的元数据,确保在社交媒体上分享时能有更好的展示效果

那么这就是我最近对博客的更新和优化。希望这些改进能让你在阅读时有更好的体验!如果你对本文有任何疑问,欢迎在评论区留言交流。

 others

Share this post via WhatsAppShare this post on FacebookShare this post on XShare this post via TelegramShare this post on Pinterest Share this post via email 上一篇 GPG 完全指南:从入门到实践 下一篇 在 Debian 12 上使用 Docker 安装 Icecast 配置流媒体服务器

4 条评论 ccm 2025-09-03 Edge 139macOS 15.5.0

Collapsible demo 代码块效果好像没出来? liuzhen932 我是萌新ccm 2025-09-03 Firefox 142Windows 10

感谢提醒,本文章写于重构前,故部分效果未在重构中保留。我会在下一篇新文章编写时一并修复 LiuShen 2025-07-13 中国Chrome 138Windows 10

好看哦! liuzhen932 我是萌新 2025-07-13 美国Chrome 134Windows 10

Expressive Code 和 Astro Paper 的代码块 Copy 按钮存在样式冲突,如果你是 Paper 主题需要手动注释一下两个函数。 Powered By Artalk © 2019-2025 liuzhen932 | 萌ICP备20249320号 | 自豪地采用 WordPress


首页 归档 分类 标签 友链 关于 开往

搜索

xingwangzhe avatar xingwangzhe

探索、学习、进步、创造无限可能!

Github Bilibili QQ eMail

稀土掘金

随机文章

正在加载随机文章...

读书:《Free as in Freedom》——若为自由故 发布时间: 2025-07-13 13:52:59 更新时间: 2025-07-13 19:04:11 🕒 阅读时间:48 min read 👀 阅读量:35

感谢

Copilot 排版,校稿 前言

这学期刚开始时,我就想读这本书,但因为课程太多,一直拖到了现在。眼下只剩最后一门期末考试,复习压力小了些,我终于抽空开始阅读。 介绍

这本书用讲故事的方式,介绍了理查德·斯托曼(Richard M. Stallman,RMS)的成长和经历。全书分为14章,记录了他从黑客到自由软件倡导者的转变,以及他发起自由软件运动的过程。作者通过采访 RMS 本人、他的家人、朋友和业界人士,展现了他在软件行业的影响力,也帮助读者了解自由软件运动的起源、理念和发展。这本书适合对黑客文化、自由软件或 RMS 本人感兴趣的读者,尤其是 IT 行业相关人士、程序员和 Linux、UNIX 爱好者。——来源:百度百科

值得一提的是,这本书采用了 GNU 自由文档许可证(GNU FDL) v1.3,大家可以自由获取原文和译文。《若为自由故》的中文源代码也可以在 GitHub 上找到。 初见端倪 第1章 从一台打印机说起

我畏惧希腊人,哪怕他们带着礼物来。 ——维吉尔,《埃涅阿斯纪》

故事的开头很简单。RMS 为了修好一台老打印机,修改了它的程序。后来换了新打印机,因为厂商不公开代码,他没法再修。这个小插曲反映了上世纪七八十年代,软件从开放共享变成了商业封闭的趋势。黑客文化受到影响,自由软件的想法也在这时萌芽。 战无硝烟 第2章 黑客路漫漫

2001 年夏天,RMS 在纽约大学做演讲。台下有不少听众,虽然女性不多,但她们手里的企鹅玩偶(象征 GNU/Linux)很显眼。这场演讲成了自由软件运动的一个重要时刻。

三周前,微软副总裁在同一地点批评 GPL 许可证,说它像病毒一样危险,威胁商业软件。RMS 在演讲中用“菜谱”做比喻,反驳了专有软件的做法:“分享菜谱能增进友谊,复制代码却被说成盗窃?”大家都笑了,这其实也是对软件行业封闭历史的回应。

有位教授把自由软件说成“开源运动”,RMS 立刻纠正:“我是做自由软件的!”他很重视自由软件和开源软件的区别。演讲中,他还提到了当年的打印机事件,再次强调自由软件的重要性。 天才初显 第3章 黑客正年少

RMS 少年时期喜欢帮母亲解《科学美国人》上的数学题。家庭变故让他很早就体会到孤独,但他在计算机世界里找到了乐趣。12 岁时,他参加科学夏令营,第一次接触计算机,学得很快。上大学后,他钻研汇编语言,写预处理器,对编程有自己的理解。在麻省理工学院人工智能实验室(MIT AI Lab),他和其他黑客一起维护 ITS 操作系统,开发 Emacs 编辑器,享受技术交流和知识共享。但随着商业软件兴起,黑客文化受到冲击,很多人转向商业,签署保密协议。实验室的氛围变了,RMS 也面临选择:是坚持黑客精神,还是向商业妥协。这段经历让他坚定了捍卫软件自由的信念,也为他后来发起 GNU 计划和自由软件运动打下了基础。 不惧权威 第4章 逆天行道,弹劾上帝

这算是我最喜欢的章节之一了,RMS 鲜明地表达了自己的观点,他自持自己的正义与自由观念,质疑权威,挑战主流。他认为,软件的自由不仅仅是技术问题,更是社会和伦理的问题。RMS 不愿意接受“既定规则”,始终坚持每个人都应该有权利使用、研究、修改和分享软件。他与商业公司、学术机构甚至政府部门进行辩论。他不畏强权,敢于直言不讳地指出专有软件的危害。他经常用幽默和讽刺的方式,揭示封闭软件对创新和协作的阻碍。RMS 认为,只有让软件自由流通,才能真正促进技术进步和社会福祉。 人若有情 第5章 自由一隅

与 RMS 的近距离接触,为我们展现了一个更加真实、立体的斯托曼。作者细致描述了 RMS 那双犀利的绿眼睛——既充满信仰的力量,又透露着脆弱和渴望沟通的一面。

章节中最有趣的部分是 RMS 在帕罗奥多一家中餐厅的午餐经历。在这个硅谷腹地,这位自由软件之父展现出了意外的一面:他是个美食家,对各地美食如数家珍;他会为路过的“正点妹子”而分心;他会即兴演唱黑客歌曲,用《随风飘散》的曲调唱着文字游戏版本的歌词。

但更重要的是,这一章深入探讨了 RMS 的政治哲学。他将版权作品分为三类:

功能类(软件、教科书等,用户应有完全修改权)
证据类(论文、日记等,不应被任意修改)
艺术类(艺术和娱乐作品,根据作者意愿决定修改权) 他坚持认为,无论哪类作品,出于非商业目的的复制和分享都应该被允许。

最令人深思的是 RMS 对“自由一隅”的阐述。他认为软件自由只是整个自由事业的冰山一角,还有更多重要的自由需要争取——暴力、种族歧视等问题都比软件自由更重要。但他选择专注于软件自由,是因为“软件自由也许并非举足轻重,但守卫之人,仅我一个”。但至少现在,这已经成为几代人的事业了。

这一章我们看到了 RMS 作为理想主义者的孤独感,也理解了他为什么要如此执着地坚持自己的原则。他不是不关心其他议题,而是选择了一个别人不愿意守护的领域,并将其作为自己的责任和使命。 神器初显 第6章 Emacs公社

20 世纪 70 年代的 MIT AI Lab,是黑客们的乐园和家园。这里不仅有顶尖的技术和自由的氛围,更有一群热爱编程、乐于分享的黑客。RMS 在这里找到了归属感,也经历了人生的低谷和转折——从哈佛毕业、膝伤失去跳舞的乐趣、社交圈收缩,最终全身心投入到软件开发中。

Emacs 的诞生,是黑客协作和创新精神的结晶。最初,大家在 TECO 编辑器上不断扩展功能,通过“宏”实现个性化和自动化操作。随着宏的流行,黑客们开始分享、改进彼此的宏,形成了一个自发的协作社区。后来,为了解决兼容性和协作障碍,RMS 和同伴们开发了 Emacs(Editing macros),并提出了“Emacs 公社”的理念:任何人都可以自由修改和分发 Emacs,但必须把改动反馈回来,促进集体进步。有些改版的名字确实好笑,比如:

Sine,全称 Sine is not Emacs;
Eine,全称 Eine is not Emacs;
Zwei,全称 Zwei was Eine initially;

Emacs 不仅仅是一个编辑器,更是一种开放协作、知识共享的社会契约。它的可扩展性和社区文化,成为后来自由软件运动和大规模协作开发的先驱。RMS 用自己的行动,推动了黑客精神的传承,也为自由软件的理念打下了坚实基础。 责无旁贷 第7章 道分左右,义务旁支

1983 年,RMS 在 Usenet 上发布了著名的 GNU 计划宣言,号召大家共同开发一个完全自由、兼容 UNIX 的操作系统。正如他在宣言中写道:

“这个感恩节假期,我将开始写一个完全的 UNIX 兼容系统,叫作 GNU,意为‘GNU’s Not Unix’。这个系统任何人都可以自由使用。你如果愿意贡献时间、金钱、程序或者设备,我随时欢迎。”

RMS 在 Symbolics 之战中孤身奋战,维护实验室 Lisp 机系统的自由,成为黑客精神的最后守护者。他的坚持和孤独,既让人敬佩,也让人唏嘘。正如他回忆:“我当时泪如泉涌……我们当年的那个黑客大家庭,早就不复存在了。”

面对专有软件的浪潮,RMS 做出了自己的抉择。他说:“我倒是可以从此不再使用计算机。可这是我看家本事了,别的工作怕也做不来。没准可以做个餐厅服务员,不过也去不了什么大餐馆。”但他最终选择了继续战斗:“我发誓要创造一个自由的操作系统,或者为此而奋斗致死。”

在 GNU 工程初期,RMS 尝试整合已有的自由软件,但屡屡碰壁,最终决定从零开发关键组件,如 GCC 编译器和 GNU Emacs 编辑器。期间,他还遭遇了与 UniPress 公司关于 Gosling Emacs 代码的版权纠纷,这进一步坚定了他推动自由软件运动的决心。他在演讲中直言:

“有时候,我真觉得我这辈子应该找来一大堆专有软件,给它们一个个破解刻成光盘,然后拿到街角去免费赠送给路人。没准那样倒是可以更快地给用户带来些自由。”

本章还介绍了自由软件基金会(FSF)的成立,以及越来越多开发者因亲身经历而认同自由软件理念。正如 FSF 成员查瑟尔所说:

“有深入了解的自由,有修改的自由,无论这是什么,这想法本身确实改变了这个世界。他让人觉得,在自己短短的生命之中,所做之事是值得付出的。因为它还没被谁夺走,也许还没被人弃置遗忘。至少,你还和它有所联系。”

拜入吾门 第8章 ST. IGNUCIUS

RMS 在演讲中强调,软件规则的制定不应只考虑公司利益,更应服务于社会整体福祉。他重申:

“我们的任务不是要创造一套操作系统;我们的任务是要给计算机用户自由。”

在演讲中,RMS 不仅批评了软件专利对创新和自由的阻碍,还区分了“开源软件”与“自由软件”的理念。他坚持认为:

“自由本身比任何新近技术都重要。如果面前有一个先进的专有软件和一个技术落后的自由软件,我宁愿选择后者。因为无论如何,我都不会靠出卖自由,而换取更新的技术。”

本章还展现了 RMS 幽默、戏谑的一面。他身披黑袍、头戴硬盘,化身“圣·IGNUcius”,用宗教仪式般的表演调侃 Emacs 信仰和自由软件精神。他说:

“Emacs 教会之中,不必戒色禁欲。但也需你心存底线,道清德厚,有所顾忌。你要在自己的电脑之上,趋邪扶正。专有软件,卸载删除。辟得净土,以敬用户。系统上下,软件自由。茫茫众生,守此信条,即入本会,得道称圣。”

圣·IGNUcius - RMS 化身

最后,RMS 提醒听众,自由软件运动的“第二步”——让用户真正理解和珍视自由——才是最重要的。他以“Happy Hacking”作为结语,号召大家继续为自由而努力。 授予自由 第9章 GNU通用公共许可证

RMS 在经历了 Emacs 公社契约的局限和 UniPress 纠纷后,逐步意识到“用法律武器保护自由”的必要性。正如他所说:

“要求用户公开所有的改动是不对的,更何况还要求他们把这么多的改动都发给某一个拥有特权的开发者。这种中央集权的方式,与整个社区人人平等的氛围格格不入。”

GPL 的核心创新在于“Copyleft”理念——

“我们利用了软件版权法本身来对抗整个现有的版权系统。”

正如 GPL 序言所写:

“通用公共许可证被设计成确保你拥有分发或出售自由软件的权利,确保你可以获取软件的源代码,确保你可以修改软件或在别的自由软件中使用这个软件,并确保你了解你拥有以上权利。为了保证你的权力,我们禁止任何人剥夺你的以上权力或要求你放弃这些权力。”

RMS 强调:

“当我们说自由软件这个词的时候,我们在说的是自由,而不是免费。” 他还用一句更易记的话来阐释 GPL 的精神:“不要把‘Free’理解成是‘免费啤酒’中的‘免费’,它是‘自由言论’中的‘自由’。”

GPL 如同“地心引力般的效果”吸引着开发者。律师马克·费雪指出:

“在那个时候,让软件用户接受这样的一种软件‘价格’即便不是独一无二,也是数一数二的。” 这个“价格”不是金钱,而是共享与传承的义务。

正如迈克尔·蒂曼所说:

“我在里面发现了一个隐蔽的商业计划。” 而 Linux 之父 Linus Torvalds 也坦言:“即使是做一次事后诸葛亮,我仍觉得选择 GPL 是 Linux 项目初期一个最佳的设计决策。” 当 Torvalds 说“我需要站在巨人们的肩膀上”时,他承认了 GNU 工具特别是 GCC 编译器对 Linux 的重要性。

RMS 将 GPL 视为“为一个新社会立法的过程”,它不仅仅是一份许可证,更是一种制度创新。正如杰里·科恩所说:

“GPL 的设计过程与其他自由软件开发的过程非常相似,由一个大的社区共同来讨论它的结构、它的条款。” GPL 让自由软件成为全球黑客和开发者的共同语言,也让“自由”成为软件世界最宝贵的财富。

自由内核 第10章 GNU/Linux

1993 年,GNU 工程仍未完成操作系统目标,Linux 却意外崛起。Linus Torvalds 坦言:

“我是一个非常懒惰的人,并且喜欢把别人的成果占为己有。” 于是 Linux 巧妙整合了 GNU 的 GCC、BSD 的 TCP/IP 等各种工具。

与 GNU 的政治化目标不同,Torvalds 只想做个自己能玩的东西。这引发了深层问题:Linux 究竟是自由软件哲学的体现,还是只是工具集合?

普渡大学学生 Ian Murdock 决定开发“对得起 Linux 名字的系统”,收到了 RMS 的回复。这标志着 RMS 立场的 180 度转弯——从回避 Linux 到主动合作。

GNU 工程延迟引发批评。Eric S. Raymond (ESR) 认为“自由软件基金会(FSF) 太骄傲了”,RMS 承认低估了 Mach 微内核的难度:

“调试这些异步运行的多线程程序非常困难。”

RMS 要求使用“GNU/Linux”名称,强调 GNU 的贡献。最大分歧出现在 glibc 上,一些开发者提出创建 Linux 专用分支。Murdock 认为“团结开发者比争抢名号更为重要”,同意了这个命名。

红帽 CEO Robert Young 看到了 GPL 商业价值:

“你不需要重新去实现图形库,只需要下载 GTK,就可以立刻使用前人的成熟果实。”

到 1996 年底,36 个月的时间窗口已关闭。正如 Murdock 所说:

“如果 Hurd 来得更早一些的话,我想 Linux 可能就根本不会出现。” Linux 的成功体现了理想主义与实用主义的奇妙结合。

歧路漫漫 第11章 开源

1996 年 2 月,首届“自由发布软件大会”在剑桥召开,RMS 和 Torvalds 共同担任主题演讲者。这次大会成为自由软件运动的分水岭。Torvalds 在会上公开批评 RMS,甚至承认自己是微软 PowerPoint 的发烧友,打破了自由软件纯化论的禁忌。正如 ESR 所说:

“在 1996 年之前,理查德是整个自由软件文化的唯一一个权威的精神领袖…托瓦兹是第一个打破这个禁忌的人。”

ESR 因对 RMS “微管理”风格的不满,已不再直接参与 GNU 工程。受 Torvalds 成功模式启发,他开发了 fetchmail 邮件工具,并总结出突破“布鲁克斯定律”的开发模式。在 1997 年 Linux Kongress 大会上,ESR 发表了著名的《大教堂与集市》演讲,将 GNU 比作“教堂”(有计划的宏伟建筑),Linux 比作“集市”(去中心化的松散组织)。

网景公司受 ESR 演讲启发,1998 年 1 月宣布开放 Navigator 浏览器源代码。这一举动推动了商业界对自由软件的关注,但也暴露了“Free Software”一词的沟通困境——人们总将“Free”理解为“免费”而非“自由”。

在 VA Research 召开的小会议上,硅谷前瞻学会主席 Christine Peterson 提出了“开源(Open Source)”这个新词汇。Tim O’Reilly 随后组织“免费软件峰会”,15 名与会者中 9 人投票支持“开源”一词。值得注意的是,RMS 并未受邀参加这次关键会议。正如 O’Reilly 解释:

“斯托曼看上去是个很强势的人,并且不太愿意加入对话。”

“开源”迅速获得商业界认可。Torvalds 登上《福布斯》封面,Intel 购买红帽股份,VA Linux 公司 IPO 创下纳斯达克历史记录。ESR 也因 VA Linux 股票获得巨额财富,这让他反思黑客道德与金钱的关系。

RMS 坚决抵制“开源”一词。1998 年底,他正式表明立场:

“开源是一个很有利于沟通的词汇,但是它也诱使演讲者弱化软件自由的重要性。” 在 LinuxWorld 大会上,他恳求追随者:“我们不需要为了去讨好公司而放弃我们的目标…如果你们什么事情都采用折中的方式,不真正往前前进,就永远也不能实现目标。”

RMS 在 Perl 大会上的激烈表现进一步疏远了潜在盟友。他公开抨击 Tcl 语言作者 John Ousterhout 是“自由软件社区的寄生虫”,令现场气氛尴尬。Bruce Perens 因此从开源倡议组织 OSI 辞职,他说:

“我非常敬仰理查德,我觉得如果他能更为平衡地处理各方关系的话,他会把事情完成得更好。”

ESR 深刻分析了这种分歧的根源:

“理查德把自己和自由软件的理念紧紧地绑在了一起,在他看来,任何对自由软件的威胁就是对他本人的威胁。” 然而,正如 Murdock 所言: “斯托曼最重要的一个性格特征就是他不会动摇自己的立场…你可以坚信他永远会坚持他自己的观点。”

这场分歧实质上反映了两种不同的价值取向:开源强调技术优势和商业友好性,自由软件强调道德原则和用户权利。虽然开源在商业上大获成功,但 RMS 依然是自由软件社区的“北极星”,因为“事实常常证明:他站得更高,看得更远。” 学习黑客 第12章 开往黑客地狱的短暂旅途

RMS 在车流中的反应完美诠释了黑客对“低效”的极度厌恶。面对明明有更好路线却被向导无视的情况,他愤怒地说:

“他为什么不拐弯呢?你们不觉得奇怪吗?” 当向导继续选择拥堵的主干道而非空旷的高速公路时,RMS 忍不住大发雷霆:“他简直就是故意无视我们的存在。”

这种对效率的执着源于黑客的本能。正如作者所分析:

“很少有事情能比‘低效’更刺激到一个黑客的神经了。” 正是这种对低效的不容忍,驱使 RMS 获取施乐打印机源代码,引发与 Symbolics 的斗争,最终诞生 GNU 工程。

津汇(原文如此,可能指某个城市或区域设计)城市设计的缺陷——所有车辆都被迫通过主干道而非分散到支路——就像“设计得很糟糕的软件”。Steven Levy 的话点出了要害:

“不完美的系统会激怒黑客…这是黑客们通常不喜欢开车的原因之一:这是一个充满不确定性的程序。”

当他们的向导在明知前方施工封路的情况下仍然选择错误路线时,RMS 彻底爆发了:

“为什么,为什么,为什么?你早该知道前面的路被封住了…你是故意这么做的。” 面对向导歉意的耸肩,他咆哮道:“你可不可以不要再笑了!这全是你的错。”

这一刻,RMS “就像是一个在汽车座位里发脾气的小孩子”,但这种情绪反应恰恰体现了黑客精神的纯粹性——对愚蠢和低效的零容忍。正如萨特所言“如果其他人是地狱”,那么黑客的地狱就是“重复其他人愚蠢的错误”。

最终当他们驶上正确的高速公路时,一切归于平静。这个小插曲生动展现了 RMS 性格中的矛盾:既有孩子般的情绪爆发,又有对效率和逻辑的执着追求。这种特质正是推动他终生致力于“把人类从地狱的火焰中拯救出来”的内在动力。 未完之志 第13章 斗争仍在继续

时间是一位不错的医生,但对 RMS 来说,《大教堂与集市》出版四年后,ESR 留下的伤口虽已收疤,却仍在隐隐作痛。最令他愤怒的是一件 1999 年 Linux 商展上流行的 T 恤衫——模仿《星球大战》海报,Torvalds 像卢克·天行者一样挥舞激光剑,而 RMS 的脸出现在 R2D2 上方。这件 T 恤把 RMS 描绘为 Torvalds 的配角,将 Torvalds 提升为领袖地位。RMS 悲哀地说:

“这实在是太讽刺了!拿起武器正是林纳斯所不愿意去做的。”

但 Torvalds 确实不愿意“拿起武器”,这让 RMS 得以继续提升自己作为道德仲裁人的影响力。尽管 GNU/Linux 的成功让自由软件运动变得边缘化,RMS 仍找到了自己的位置。2000-2001 年间,他的演讲覆盖六大洲,访问了对软件自由理解更深的国家。

RMS 也学会利用作为 GPL “海岸警卫队员”的权力。2000 年夏天,当 Linux IPO 泡沫散去时,他获得两个重要胜利:挪威 Troll Tech 公司对 Qt 库采用 GPL 许可证,Sun 公司对 OpenOffice 采用 LGPL 双重许可。在 Sun 的案例中,副总经理 Marco Boerries 说:

“我可以用三个字母来拼写它,GPL。” 这表明 GPL 在业界地位的确立。

自由软件基金会法律顾问 Eben Moglen 深刻分析了 RMS 的成功:

“他一定不是一个律师,不然怎么可能把 GPL 写成这个样子…但是它很有效,因为理查德·斯托曼所坚持的设计哲学。” Moglen 指出,RMS 对待法律文书和软件源代码的方式基本一致,目标是去除不确定性,“要起草一个在任何国家任何情况下都适用的许可证是不可能的。但如果你朝着这个目标努力…最终结果的简洁设计几乎到达了期望的效果。”

面对 Moglen 建议的各种战略行动,RMS 总是回答:

“我们不需要做任何事。只需要静静地等着,那些需要完成的事都会慢慢完成的。” Moglen 承认:“总体上来说,他是对的。” 这否定了 RMS 的自我评价:“我不是个能运筹帷幄的人。”

关于历史地位的“一百年问题”,专家们将 RMS 与美国历史名人相比:梭罗(《公民不服从》作者)、约翰·缪尔(环保运动先驱)、威廉·詹宁斯·布莱恩(平民主义领袖)。Moglen 预言:

“一百年以后,理查德和其他一些人也许不仅仅能在历史中留下一笔,他们还会成为一个时代的主角。”

John Gilmore 将 RMS 比作托马斯·杰斐逊:

“斯托曼的作品会像托马斯·杰斐逊的一样伟大:杰斐逊是一个思路清晰的作家,并且对他的原则非常清楚。” 他还指出 RMS 被忽视的遗产——由 GNU 工程领导的合作式软件开发模式,这种模式比 GPL 本身更具影响力。

斯坦福法学教授 Lawrence Lessig 认为 RMS 的贡献独一无二:

“他把这些争论从‘是不是’变为了‘应不应该’。他让人们看到危机,并且创造了一种方式让这些理想得以延续…我觉得这样的人前无古人、后无来者。”

ESR 对此持不同观点,认为 RMS 的作品会被视为革命性的,但历史不会按 RMS 设想的方向发展。RMS 自己也承认:

“二十年后,历史对 GNU 工程的评价,会取决于谁赢得了自由的斗争。”

RMS 将自己比作 John Brown——19 世纪激进的废奴主义者,在不同时期被视为英雄或疯子。这种比较体现了 RMS 工作的自省性格和当前声誉的两面性。

Moglen 用一个深刻的场景总结了 RMS 的本质:2000 年春天,两人在暴风雨般的媒体报道中共进午餐,讨论加强 GPL 地位。Moglen 回忆:

“我们是两个留着络腮胡的无政府主义者,设计着下一步的行动…任何听到我们谈话的人都会觉得我们是疯子,但是我知道,革命就将从我们这张桌子开始。”

最有趣的是,Moglen 还得为 RMS 付午餐钱,“因为他没有钱来付。” 这个细节完美诠释了这位改变世界的革命者的纯粹性——他在这次革命中一分钱也没赚到,却仍在坚持战斗。 初心未改 第14章 跋:粉碎孤独

故事始于 2000 年 4 月,作者为 BeOpen 网站采访 RMS,文章在 Slashdot 获得关注。纽约出版社编辑 Tracy Pattison 主动联系,希望将访谈扩展为电子书传记。作者立即答应,但很快陷入了与 RMS 的激烈冲突。

RMS 对电子书有着坚决的反对立场。他在《技术观察》杂志发表文章警告:

“如果电子书取代了印刷的纸书,则没有什么好处…不会再有旧书店;不能把书借给你的朋友;不能从公共图书馆借阅图书。” RMS 给出两个条件:不愿对反对的电子书许可证做任何妥协,也不愿被别人看成做出了妥协。他说: “我不想参与任何让我看起来像个伪君子的事情。”

当出版社拒绝改变版权条款时,冲突爆发了。在纽约第三大街的酒吧里,作者通过电话与 RMS 进行了激烈交锋。RMS 咄咄逼人地质问:

“这是怎么回事?你是在耍我吗?” 当作者试图妥协时,RMS 冷冷地纠正:“你所说的版权应该是指许可证。” 最终,RMS 愤怒地挂断电话: “我不关心这些。他们在做的就是一些邪恶的事情。我不能支持作恶!”

这次通话让作者“浑身发抖”,他意识到自己“就像歌中所唱的那个一直遭遇挫折的家伙,精神就在崩溃的边缘了。”讽刺的是,作者正在经历书中其他人的遭遇——被 RMS 的固执折磨得死去活来。

但故事有了意外的转折。作者与特蕾西因为这本“死胎”传记而相识、相恋并结婚,他们的婚礼定在 2001 年 5 月 11 日。正如作者所说:

“虽然这传记成了死胎,我们却收获了一份美满的姻缘。”

一年后,O'Reilly 出版社提出重新出版这本传记,并愿意使用开放许可证。作者面临选择出版 OPL 或 GNU FDL (GFDL)。RMS 建议使用 GFDL,作者提出交易:如果 RMS 给更多采访机会,就选用 GFDL 许可证。

2001 年 12 月,作者夫妇与 RMS 及其女友 Sarah 共进晚餐。Sarah 解释了她被 RMS 吸引的原因:

“我非常景仰理查德建立起这整个自由软件运动的方式,而这个运动只是为了满足他个人的一个想法。” 当特蕾西问及 RMS 的真正想法时,Sarah 的回答一语中的: “粉碎孤独。”

这个答案揭示了 RMS 内心深处的驱动力。RMS 承认:

“我曾经以为我会一辈子孤独,不过很幸运的是我错了。” 他还坦露了更深层的恐惧: “没有家和社区的归属感所带来的不安全感非常巨大,它们迫使我去战斗,并把它们抢回来。”

作者由此认识到自己写作本书的真正动机:

“在某种程度上说,斯托曼的性格与自由软件运动是相辅相成,完全互补,不可分割的。” 他希望这本书能在 GFDL 激励下,吸引更多人补充观点,让 RMS 的形象更加丰满。

这一章以“粉碎孤独”为题,不仅揭示了 RMS 个人的情感需求,也解释了自由软件运动的本质——它不仅是技术革命,更是对人类协作与分享本能的回归,是对现代社会中个体孤独感的深层反抗。正如 Sarah 所说,分享代码的推动力来自朋友情谊,而 RMS 用一生的战斗来追求的,正是这种基于自由和信任的人类连接。 ⚠️ 重要修正说明

由于本书出版时间较早(英文版2002年,中文版2015年),书中某些内容需要结合后续发展进行更新理解: 📅 爱泼斯坦事件(涉及第11章及全书背景)

时间线:

2019年9月RMS因在MIT内部邮件中为马文·明斯基Marvin Minsky辞护而引发争议
争议焦点斯托曼在邮件中质疑受害者自愿性”,使用了presented herself as entirely willing的措辞
公众反应这一表述被广泛批评为受害者有罪论
后果RMS因此辞去MIT及FSF职务2019
回归2021年经FSF董事会投票重新回归FSF

RMS回归声明要点:

“我为自己的社交能力缺陷道歉,但我要明确:爱泼斯坦是连环强奸犯,应受到法律惩罚。”

🐧 Ubuntu立场更新(补充第5章/第8章内容)

书中提及RMS反对Ubuntu因其包含专有驱动,但2022年他进一步批判:

新批评焦点:

Ubuntu通过Snap商店分发软件,用户无法自由修改代码(如Chromium浏览器)
RMS原话:“Ubuntu是一个非自由发行版……安装自由软件比以前更困难。”

⚖️ GPL许可证核心机制(深化第9章理解)

书中强调”共享义务”,但需更突出其反封闭性设计:

关键机制:

GPL要求衍生作品必须以相同许可证开源(“传染性”特征)
防止企业将自由代码私有化后商业化
RMS核心理念:“Copyleft利用版权法保护自由,而非限制用户。”

🆚 “自由软件”与”开源”分歧本质(强化第11章核心冲突)

书中描述ESR与RMS理念冲突,但需强化根本差异: 维度 自由软件(RMS) 开源软件(ESR等) 核心理念 以道德权利为核心(用户控制技术的自由) 以技术效率为核心(代码开放可提升质量) 价值导向 强调伦理责任和用户权利 强调商业友好和开发效率 策略选择 拒绝与专有软件妥协 愿意与商业公司合作

关键矛盾:

开源运动刻意淡化道德诉求以吸引企业支持
RMS立场:“这是对自由的背叛,我们不能为了讨好公司而放弃目标。”

📈 技术发展背景更新

云计算与SaaS挑战:

现代软件服务化(如Google、Amazon服务)规避了传统GPL约束
RMS提出AGPL许可证应对云服务挑战

开源商业化进程:

大型科技公司(Google、Microsoft、Meta)大规模采用开源策略
但往往只开源非核心技术,保留关键算法私有

总述

2015 年中文版出版至今已近十年,软件社区发生了巨大变化。虽然“开源”一词依然主导舆论,并被商业公司广泛采用,但自由软件社区依然充满活力。事实上,许多重要的技术和社会运动都深受自由软件运动的影响,包括:

开源软件运动(Open Source Initiative):在自由软件理念基础上,强调协作开发和代码开放,推动了 Linux、Apache、Kubernetes 等关键项目的普及。
开放获取运动(Open Access):倡导学术论文、科研数据的自由获取,促进了知识共享和学术创新。
开放硬件运动(Open Hardware):推动硬件设计文档的开放,代表项目有 Arduino、RISC-V 等。
创用CC(Creative Commons):为内容创作者提供灵活的授权方式,鼓励作品的自由传播和再利用。
开放数据运动(Open Data):推动政府、科研、企业数据的开放共享,促进透明和创新。

这些运动的兴起,正是对RMS 当初坚持自由软件道路的最好印证。自由、协作、共享的理念已经深刻影响了全球科技、教育和社会创新的方方面面。

感谢 理查德·M·斯托曼(Richard M. Stallman, RMS),感谢他为自由而战。

读书:《Free as in Freedom》——若为自由故

作者: xingwangzhe

本文链接: https://xingwangzhe.fun/posts/1ae84415

本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

上一篇 首页文章列表懒加载优化 下一篇 7x24:运维使命 留言评论 昵称 邮箱 网址 0 / 200 字 评论

按正序
按倒序
按热度

来发评论吧~ Powered by Waline v3.6.0 2025年9月23日星期二 15:37:06 文章目录

前言
介绍
初见端倪 第1章 从一台打印机说起
战无硝烟 第2章 黑客路漫漫
天才初显 第3章 黑客正年少
不惧权威 第4章 逆天行道,弹劾上帝
人若有情 第5章 自由一隅
神器初显 第6章 Emacs公社
责无旁贷 第7章 道分左右,义务旁支
拜入吾门 第8章 ST. IGNUCIUS
授予自由 第9章 GNU通用公共许可证
自由内核 第10章 GNU/Linux
歧路漫漫 第11章 开源
学习黑客 第12章 开往黑客地狱的短暂旅途
未完之志 第13章 斗争仍在继续
初心未改 第14章 跋:粉碎孤独
⚠️ 重要修正说明
📅 爱泼斯坦事件(涉及第11章及全书背景)
🐧 Ubuntu立场更新(补充第5章/第8章内容)
⚖️ GPL许可证核心机制(深化第9章理解)
🆚 “自由软件”与”开源”分歧本质(强化第11章核心冲突)
📈 技术发展背景更新
总述

本文分类 读书 (1) 文章标签 读书 (1) free (2) 自由文档 (1) GNU (2) © 2024-2025 xingwangzhe. 保留权利. Built with Love Powered by Astro Theme: Stalux License: MIT 阿里云支持 开往-友链接力 大佬论坛 BlogFinder 空间穿梭-随机访问BlogsClub成员博客 腾讯EdgeOne CDN 十年之约 本站已加入BLOGS·CN 博客宇宙 - 精选博客导航 无聊湾 🥱 Sitemap XML RSS Feed Atom Feed LLM Dataset 辽ICP备2024042064号-1 Powered by Astro | Theme is Stalux 本站访客数:14951 人次 | 本站总访问量:32240 次 | 本站总字数:13.8万 字 本站已正常运行460天5时37分6秒


菜单
主要内容

书签 设置 文档

www.ithome.com
14 七月 2025

ithome.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 日本政府支持的芯片制造商 JS Foundry 将申请破产,累计负债达 161 亿日元

IT之家 7 月 14 日消息,据日经新闻消息,日本政府支持的半导体代工厂 JS Foundry 将于本周一向东京地方法院申请破产保护。

据IT之家了解,JS Foundry 总部位于东京,专注于功率半导体的生产,这些半导体主要应用于电力调节,广泛用于电动汽车、家用电器及列车等大型电气设备。该公司由日本政策投资银行(DBJ)旗下基金等出资支持,但目前已深陷经营困境,累计负债达 161 亿日元(IT之家注:现汇率约合 7.84 亿元人民币)。

据悉,JS Foundry 因运营资金不足、客户开拓不利等原因,陷入财务困境,尤其是在中国芯片制造商快速扩张的背景下,市场竞争愈发激烈,加剧了其生存压力。值得注意的是,JS Foundry 原计划获得来自日本中央及地方政府的数十亿日元补贴,但破产申请的决定发生补贴发放之前。

JS Foundry 成立于 2022 年 12 月,由 DBJ 关联企业 Mercuria Investment 与独立财务顾问 Sangyo Sosei Advisory 共同设立。公司员工约 550 人,其中约 200 人被派遣至其他企业工作。

JS Foundry 的主要生产基地位于新潟县,该工厂最初由三洋电机于 1984 年建造,2011 年转售给美国半导体制造商安森美(Onsemi),2022 年再转手至 JS Foundry。公司在成立首年实现了 100 亿日元(现汇率约合 4.87 亿元人民币)的销售额,但由于与安森美的生产合作结束,截至 2024 年 12 月的财年,销售额骤降至 26 亿日元(现汇率约合 1.27 亿元人民币),最终无力支撑运营。


菜单
主要内容

书签 设置 文档

www.ithome.com
23 九月 2025

ithome.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 链接

小米平板 8 系列官宣兼容 macOS 生态,可作为苹果副屏使用

IT之家 9 月 23 日消息,小米官方刚刚宣布,全新小米平板 8 系列首发搭载澎湃 OS 3,兼容苹果 macOS 生态,可作为 Mac 电脑无线副屏使用,支持文件、图片,一键互传,信息无缝流转。

小米平板 8 系列将于 9 月 25 日晚 7 点发布。根据官方预热,小米平板 8 Pro 将搭载高通骁龙 8 至尊版芯片,号称“同档最强”。

据介绍,其安兔兔跑分高达 300 万,性能相比上代提升 69%,号称能在长时间运行重载游戏的情况下“全程满帧”。

此外,小米平板 8 系列配备 11.2 英寸旗舰屏幕,支持 3.2K 分辨率、345PPI、800nits 峰值高亮、全程 DC 调光、莱茵三重护眼认证、纳米柔光屏优化闪点。

IT之家从官方获悉,小米平板 8 系列还新增支持分屏快切、上下分屏等功能,支持 PC 级浏览器,网页显示、交互与电脑一致,支持 PC 级 WPS、CAJ、CAD 应用。

相关阅读:

《小米汽车与苹果深度合作,系统级整合 Apple Music 实现账号永久同步》

《卢伟冰首发对比小米 17 Pro 和苹果 iPhone 17 Pro 真机,回应外观相似质疑》

《卢伟冰解释小米 17 命名:不是“蹭苹果热度”而是“跳变”,7 好像“幸运数字”》

菜单
主要内容

书签 设置 文档

www.ithome.com
23 九月 2025

ithome.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 历史解码:微软 Windows 3.0 中 WinHelp “在线帮助”非指联网

IT之家 9 月 23 日消息,微软资深工程师雷蒙德・陈(Raymond Chen)昨日(9 月 22 日)发布博文,解释了 Windows 3.0 中 WinHelp 的“在线”(online)术语性质。

IT之家注:Windows 3.0 是微软在 1990 年推出的重要版本,也是首次随系统附带 WinHelp 1.0 帮助软件。令人疑惑的是,当时互联网尚未向公众普及,为什么它的文档会被称为“在线帮助文件”(online help files)?

雷蒙德・陈对此解释称,当时“在线”一词在计算机领域的定义,与今天的理解不同。它并不意味着联网,而是指数据或文件已经加载并可立即访问。例如存储中的“在线文件”就是当前可用的本地数据,“离线文件”则是被归档到磁带等介质,需要额外操作才能调用。

这种用法源于早期 IT 运维的术语习惯。在网络环境中,一台真正连入网络的电脑会被称为“up”,如果它能接收其他计算机的连接请求,则会标注为“available offline”。这里的“offline”指与其他系统的连接状态,而非文件存储状态,因此与如今的“离线”含义并不一致。

WinHelp 在 Windows 3.0 中提供了可直接读取的本地帮助文档,无需联网即可使用。这种设计与现代的在线帮助网站形成鲜明对比,如今的“在线”通常意味着必须通过互联网访问,而不能在本地离线使用。

科技媒体 NeoWin 认为,这一术语差异反映了计算机技术发展和语言习惯的演变。上世纪 90 年代初,互联网还未普及,软件厂商更多依赖本地存储分发文档,而现今多数帮助资源已经转移到云端与网页,用户对“在线”的理解也随之改变。


菜单
主要内容

书签 设置 文档

www.ithome.com
23 九月 2025

ithome.com

标签

new_news_den

高亮

在文章中选择文本以创建新的高亮。 链接

全国产底层设计全功能 GPU“风华 3 号”发布,采用开芯院 RISC-V 和全套 IP

IT之家 9 月 23 日消息,芯动科技“风华 3 号”全功能 GPU 昨日在珠海香山会议中心正式发布,其采用全国产底层设计,同时拥有 AI 智算算力和 8K 重度渲染算力,兼容 DirectX12、Vulkan1.2 等图形接口,并适配统信、Windows 等系统。

风华3号——芯绽华夏,算舞乾坤! 以磅礴算力,勾勒智绘未来!

据介绍,芯动科技“风华 3 号”全功能 GPU 在行业内率先实现国产开源 RISC-V CPU 与 CUDA 兼容 GPU 的深度融合,可一站式覆盖大模型训推、垂类多模态应用、科学计算与重度图形渲染,也是全球首个实现了 DICOM 高精度灰阶医疗显示功能的 GPU 产品。

在生态方面,“风华 3 号”在软件端兼容 PyTorch、CUDA、Triton、OpenCL 等主流 AI 和计算生态、和 DirectX、OpenGL、VulKan 等渲染生态,适配统信、麒麟、Windows、Android 等操作系统。其作为国内首款支持光线追踪功能的 8K 分辨率重度渲染 GPU,全面兼容 DirectX12、Vulkan1.2、OpenGL4.6 等图形接口。

在大模型方面,“风华 3 号”是国内首款单卡配备 112GB + 大容量高带宽显存和自研 IP 的全功能 GPU,突破目前国产 GPU 显存和多卡搬运的上限,单卡可支持多用户 32B / 72B 大模型;单机八卡能直驱 DeepSeek 671B / 685B 满血版大模型。

IT之家附芯动科技“风华 3 号”全功能 GPU 主要亮点如下:

超大高速显存国产大模型 AI 单卡单机 TOP1

同时拥有 AI 智算算力和 8K 重度渲染算力的全功能 GPU

提供顶级实景渲染体验,国产首家支持硬件光线追踪的全功能 GPU

先进的全功能全兼容数据中心 vGPU 设计架构

全球首款 GPU 支持 DICOM 医疗级灰度显示,行业首创

全国产底层设计,采用开芯院 RISC-V 和全套 IP

相关阅读:

《芯动科技:“风华 3 号”显卡已基本完成研发,支持光线追踪》

迁移之后最大的遗憾是失去了邮件通知的机能。在这之后依旧在撞南墙和找方案之间徘徊。

还是问了下 ChatGPT,针对评论通知给出了四种方案:

邮件通知 已无力回天
浏览器推送通知
即时通讯集成通知 手续繁琐,门槛较高
站内通知系统 开发成本大,事务效率低

原本已经着手准备开发站内通知了,因为对浏览器推送还有一些顾虑:浏览器进程必须在运行状态;普及率似乎不高;最主要的印象还是微软小广告。

但单论通知范围和效率,那站内通知是完全没法比的,所以最后选择了这种方式。

整个流程难度不高,但是对我来说勉强是个新技术,会有些繁琐。 浏览器订阅󰌷

首先需要启用和授权,和原先的邮件通知一样,我也把通知启用的开关放在了访客的配置页中。

为了实现离线状态下的通知,通过语句 navigator.serviceWorker.register("/sw.js") 在浏览器上安装 Service Worker。安装后可以在 Dev Tools 的 应用程序 > Services Workers 中查看和调试。

这里的 /sw.js 暂时不重要,我打算放到最后编写。

然后使用 Notification.requestPermission() 向用户请求通知权限:

一共会返回三种状态:

granted,点击允许明确授予系统通知的权限。
denied,点击拒绝明确拒绝系统通知的权限。
default,直接关闭或忽略弹窗,但逻辑上基本等同于拒绝。

也就是只有在返回 granted 时,才能进行下一步操作。

之后就要注册订阅了,使用 registration.pushManager.subscribe(),这时需要提供 VAPID 公钥,用于标识服务器并防止滥用。

VAPID 密钥对的生成方式不少,可以使用 openssl,但有点麻烦,又需要额外编码为 Base64URL;也可以在线生成,不过这样直接需要提供邮箱地址,我还是考虑本地生成;最方便的应该是用 web-push 库,原本应该全程使用的,但考虑到 CloudFlare Workers 的运行环境,只能在这里简单用一下,执行 npx web-push generate-vapid-keys 即可。

registration.pushManager.subscribe() 会返回一个对象,之后会反复用到:

interface { endpoint: string; // 浏览器提供商的通知推送地址 expirationTime: EpochTimeStamp | null; // 订阅有效时间 keys: { p256dh: string; // 客户端公钥 auth: string; // 对称加密验证随机密钥 }; }

接着把部分数据推送到服务端,带上一些用户标识信息,持久化保存起来,用户视角下的订阅就完成了。 服务端推送󰌷

假如此时有人回复了一条评论,那么就要给被回复的评论者发送通知了。

在获取被回复者的基本信息后,再查询数据库得到通知订阅的相关数据,也就是上面的那个对象。

携带一些通知的具体内容,就可以向浏览器提供商发送通知了,也就是向 endpoint 发送请求,例如 https://wns2-pn1p.notify.windows.com/... https://fcm.googleapis.com/... https://updates.push.services.mozilla.com/... 等。

这里,因为需要即时反馈,所以不能等待所有通知发完了再更新页面。好在 CloudFlare 提供了接口,在 ctx.waitUntil() 中执行异步内容,防止 Workers 进程提前结束。

之前数据库的地方还能稍作修改,迁移到其它的托管站点。
但这样一来,算是把整个项目和 CloudFlare 彻底绑定了,若是不用一些第三方的队列服务,感觉有点难以解围。

推送本身似乎还要涉及一些加密、编码,想了想,还是找个库吧——@block65/webcrypto-web-push,接收密钥对,简单封装之后,就能减少大篇幅的代码和开发时间。

系统的通知卡片没法设计什么样式,就是单纯的文本,那么也就从简就好:标题、描述、跳转链接以及缓存时间。

缓存时间设置了 24 小时,应该是指只要在通知推送后的 24 小时内打开过浏览器,就都能接收到吧。

不过现在关键逻辑都集中在了评论的模块里,可读性有所降低。虽然通知部分是有单独的模块,但 Astro 的 Actions 之间无法相互调用,且受限于官方建议的项目结构,之后等业务多了再想想怎么修改结构吧。 浏览器通知󰌷

最后,由浏览器提供商通过网络发出通知,用户就能准备接收了。这的关键就是 /sw.js,Astro 中一般放置在 /public 目录下就好。

使用 navigator.serviceWorker.register("/sw.js") 安装时,Service Worker 会向其提供一个特殊的全局对象 self 指向 ServiceWorkerGlobalScope,代表全局作用域。

监听 self 上的各类事件,以实现相关功能。

其中 install 事件在一般情况下只会执行一次,这点避免了刷新重载时的冗余安装,省去了不少内存管理的头痛问题。

接着最重要的是 push 事件,也就是通知接收。所有由服务端发送的通知数据都包含在 event.data 中,解析后由 self.registration.showNotification() 逐一发送,就呈现在用户桌面右下角的弹窗中了。

最后是对用户体验的锦上添花,在 notificationclick 中,添加点击通知卡片后的动作,也就是跳转到对应的内容页面。

这里出现了第二个全局对象 clients 代表 Clients 接口,用于管理所有关联的客户端,这里也就是网页。从完整实现来说,我应该检测对所有的标签页,如果有匹配到的直接 focus() 就行,但考虑到自己的页面留存率嘛…实在没必要了,直接用 clients.openWindow() 打开跳转即可。 结语󰌷

这甚至是我第一次使用 Push API 机制,也不能说特别看好吧,又有多少人会对一个小网站启用呢?

只是这种与操作系统浑然一体的感觉,的确是别有一番韵味了。


avatar 文章 26 标签 47 分类 4 首页 文章

归档
分类
标签

链接

友情链接
朋友圈
小记

关于

说说
留言
相册
关于本站

短巷与雨 首页 文章 链接 关于 maxwell学习笔记 maxwell学习笔记 发表于2025-07-12|更新于2025-08-12|学习笔记 总字数:2.2k|阅读时长:7分钟|浏览量:71 这里是AI摘要,这篇文章介绍了Maxwell这款开源的MySQL数据同步工具。Maxwell通过实时读取MySQL的binlog日志,将数据变更以JSON格式发送至Kafka、RabbitMQ等流数据处理平台,支持全量与增量同步,适用于各种ETL、缓存维护等场景。文章详细阐述了Maxwell的简介、原理、部署方法以及如何进行历史数据的全量同步。Maxwell的核心优点包括轻量级、易用性和灵活性。 Maxwell

​ 最新不知道记录些什么,本来说是写一个整理浏览器书签的笔记,浏览器的书签实在太乱了,注册了太多官网的账号,收藏了好多别人写的博客文章,资源等等,有用的没用的好几百个,有的时候找起来太费时间了。想用一些工具去分类整理一下的,但是没有找到合适的产品。所以就没写成,于是乎就整理了一下之前写的关于maxwell数据同步的笔记。 一、Maxwell简介

‌ Maxwell是一款开源的MySQL数据同步工具,通过实时读取MySQL的binlog日志,将数据变更以JSON格式发送至Kafka、RabbitMQ等流数据处理平台,支持全量与增量同步,适用于各种ETL、缓存维护等场景。

官网地址:https://maxwells-daemon.io/

核心优点:

轻量级:无需依赖额外组件,直接输出JSON数据,简化下游处理。‌‌
易用性:相比Canal等工具,无需编写客户端解析数据。‌‌
灵活性:支持按库、表、列分区,解决数据倾斜问题。‌‌

二、Maxwell原理

​ Maxwell的工作原理是实时读取MySQL数据库的二进制日志(Binlog),从中获取变更数据,再将变更数据以JSON格式发送至Kafka等流处理平台。

Maxwell伪装为MySQL的从库(Slave),通过主从复制协议捕获binlog事件,解析后转换为JSON格式输出

输出说明

maxwell输出数据格式有三种。对应得json中type分别为insert,update,delete。插入删除格式如下,不同的是type,更新时或多出一个old属性记录更改前的数据值。

{ "database":"test", "table":"test", "type":"insert", "ts":1477053217, "xid":23396, "commit":true, "data":{ "id":1, "name":'zs' } }

字段 解释 database 变更数据所属的数据库 table 表更数据所属的表 type 数据变更类型 ts 数据变更发生的时间 xid 事务id commit 事务提交标志,可用于重新组装事务 data 对于insert类型,表示插入的数据;对于update类型,标识修改之后的数据;对于delete类型,表示删除的数据 old 对于update类型,表示修改之前的数据,只包含变更字段 二进制日志

​ 在MySQL中,二进制日志(Binary Log)是一种用于记录所有修改了数据库数据的操作(不包括SELECT和SHOW这类操作)的日志文件。它主要用于复制和数据恢复。启用二进制日志可以帮助你进行数据的复制和恢复,尤其是在使用MySQL复制或进行数据灾难恢复时。 主从复制

​ MySQL的主从复制就是,通过主数据库的二进制日志去建立一个与主数据库环境一样的从数据库。 主从应用场景

做数据库的热备:主数据库服务器故障后,可切换到从数据库继续工作。
读写分离:主数据库只负责业务数据的写入操作,而多个从数据库只负责业务数据的查询工作,在读多写少场景下,可以提高数据库工作效率。

主从复制原理

Master主库将数据变更记录,写到二进制日志(binary log)中
Slave从库向mysql master发送dump协议,将master主库的binary log 通过IO线程将它写入中继日志(relay log)
Slave从库的SQL线程解析中继日志中的事件并执行,完成数据同步

主从复制如下图:

image-20250604110829698 三、Maxwell部署

​ maxwell官方提供了多种安装方式,源码,docker等。在安装Maxwell前需要配置MySQL。 配置MySQL

1、启用MySQL的binlog,修改MySQL的配置文件如下 /etc/my.cnf

[mysqld]

数据库id

server-id = 1

启动binlog,该参数的值会作为binlog的文件名

log-bin=mysql-bin

binlog类型,maxwell要求为row类型

binlog_format=row

启用binlog的数据库,需根据实际情况作出修改

binlog-do-db=test

binlog的类型有三种分别为row,statement和mixed

row:基于行,Binlog会记录每次写操作后被操作行记录的变化。优点是保持数据的绝对一致性。缺点是占用较大空间。

statement:基于语句,Binlog会记录所有写操作的SQL语句,包括insert、update、delete等。优点是节省空间。缺点是有可能造成数据不一致,例如insert语句中包含now()函数。

mixed:混合模式,默认是statement,如果SQL语句可能导致数据不一致,就自动切换到row。

Maxwell要求MySQL的binlog必须是row模式。

修改完配置以后需要重启数据库服务。

2、创建Maxwell数据库用户

​ Maxwell需要在MySQL中存储其运行过程中的所需的一些数据,包括binlog同步的断点位置(Maxwell支持断点续传)等等,故需要在MySQL为Maxwell创建数据库及用户。

CREATE DATABASE maxwell; #创建数据库

set global validate_password_policy=0; #设置密码的安全级别 set global validate_password_length=4;

CREATE USER 'maxwell'@'%' IDENTIFIED BY 'maxwell'; 创建maxwell用户并为其赋予权限 GRANT ALL ON maxwell. TO 'maxwell'@'%'; GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE ON .* TO 'maxwell'@'%';

安装运行

源码安装

curl -sLo - https://github.com/zendesk/maxwell/releases/download/v1.41.2/maxwell-1.41.2.tar.gz \ | tar zxvf - cd maxwell-1.41.2

该软件是使用Java开发的,会依赖Java的环境,再此之前需要安装Java的环境

在解压完以后,可以直接用过bin目录中的脚本文件启动maxwell。

bin/maxwell --user='maxwell' --password='XXXXXX' --host='127.0.0.1' --producer=stdout

以上dome是指将数据库的数据直接通过maxwell同步以后,输出到控制台。该项一般只是做测试使用

或者是直接通过命令行去启动一个将数据同步到kafka的进程

bin/maxwell --user='maxwell' --password='XXXXXX' --host='127.0.0.1' \ --producer=kafka --kafka.bootstrap.servers=localhost:9092 --kafka_topic=maxwell

–user: 数据库的用户名

–password:数据库的密码

–host:数据库所在服务器的ip地址

–producer=指定将数据同步到哪里,例如:kafka、nats、pubsub、bigquery、rabbitmq、redis等

1、若是同步到kafka需要指定kafka集群的ip和端口号,以及主题

2、若是将数据同步到Google Cloud Pub/Sub平台需要指定pubsub_project_id和主题

所需要同步的平台根据官方给出的信息配置即可。

上面是通过命令行的方式去同步的数据,接下介绍的是使用maxwell配置文件去启动守护进程将数据同步到kafka。

log_level=info producer=kafka #指定将数据同步到kafka kafka.bootstrap.servers=192.168.128.4:9092,192.168.128.5:9092 #kafka集群的地址,可以写多个

kafka topic配置

kafka_topic= kafka_topic_name #kafka集群中的主题名

mysql的信息

host=192.168.128.16 #MySQL所在服务器IP user=maxwell #MySQL用户名 password=maxwell #MySQL密码 jdbc_options=useSSL=false&serverTimezone=Asia/Shanghai

若是自己搭建kafka需要注意的是2.8版本以下的kafka是依赖于zookeeper的所以需要在linux上安装zookeeper。

kafka_topic: 可以静态配置,如maxwell_test,或者是直接使用动态配置去%{database}_%{table}。则在kafka中的主题名为”数据库名 下划线 表名” 如:trade_order。

配置完成后启动maxwell,在启动前确保kafak集群已经启动。

/bin/maxwell --config maxwell/config.properties --daemon

通过该命令启动maxwell以后,会在后台新建一个名叫maxwell的守护程序,可以 ps -ef | grep maxwell 去查看。也可以用过jps去查看。启动完成后,就会实时同步增量数据。

maxwell没有写停止的脚本,只能通过kill去杀掉后台进程,停止maxwell。

ps -ef | grep maxwell | grep -v grep | grep maxwell | awk '{print $2}' | xargs kill -9

或者是直接使用docker去快速部署一个maxwell应用,将MySQL的数据同步到kafka

docker run -it --rm zendesk/maxwell bin/maxwell --user=$MYSQL_USERNAME \ --password=$MYSQL_PASSWORD --host=$MYSQL_HOST --producer=kafka \ --kafka.bootstrap.servers=$KAFKA_HOST:$KAFKA_PORT --kafka_topic=maxwell

kafka消息队列也可以使用docker快速搭建这里省略

有时只有增量数据是不够的,我们可能需要使用到MySQL数据库中从历史至今的一个完整的数据集。这就需要在进行增量同步之前,先进行一次历史数据的全量同步。这样就能保证得到一个完整的数据集。

Maxwell提供了bootstrap功能来进行历史数据的全量同步

bin/maxwell-bootstrap --database 数据库名 --table 表名 --config maxwell/config.properties

同步json中我们可以发现type有bootstrap-startbootstrap-insertbootstrap-complete三种类型其中bootstrap-start与bootstrap-complete是bootstrap开始和结束的标志不包含任何数据数据都存在于type为bootstrap-insert的类型中

文章作者: Hudi(唱唱反调) 文章链接: https://www.hudi.space/posts/1a244a1c/ 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 短巷与雨! 数据同步运维 cover of previous post 上一篇 小工具 平时不太喜欢去搜罗一些好用的工具,但是看到自己感兴趣的还是会记下来,有的是github上的开源项目,有的是一些直接在线的工具。主要是除了工作时间也不知道去干点什么,或者是和朋友玩玩游戏,或者是city walk一下。在要不然就知道待在家里看看电视了,刷刷抖音,看看朋友们写的博客文章。本来是想写一个征期巡检提醒的机器人的。可以涵盖企微,钉钉,邮箱等的定时提醒,已经完成了一小部分了,但是又想到github上肯定有开源的项目,可以直接拿来改吧改吧用的,后来又想起之前看到的工具,都没拿来用过,还不如趁这个时间拿来运行看看。 cover of next post 下一篇 飞牛云NAS搭建 本文记录飞牛云NAS搭建过程,其FnOS系统基于Linux开发,支持x86硬件与Docker,功能丰富且部分免费。作者因树莓派架构不兼容,在虚拟机完成部署,详述镜像安装、Web UI配置等步骤,并介绍FN Connect、Lucky、Cloudflare三种内网穿透方案,总结系统灵活性及后续硬件搭建计划。 相关推荐 cover 2025-06-03 SSH免密登录 梳理思路、加深记忆,便于复盘知识,为学习工作积累经验与灵感。 cover 2025-06-20 kubernetes学习笔记(一) 本文为Kubernetes入门学习笔记,涵盖核心概念、特点及CentOS环境下的集群搭建流程。Kubernetes是Google开源的容器编排引擎,具备自动化部署、服务发现与负载均衡、存储编排、自我修复等特性,支持多环境部署与丰富的生态工具。安装部分详细介绍了CentOS系统中通过kubeadm搭建三节点集群的步骤,包括基础环境配置、网络规划、master节点初始化、work节点加入,以及Calico网络组件和Dashboard可视化界面的部署。最后总结了安装中常见的镜像拉取失败、节点连接异常等问题及解决方案,为初学者提供了完整的入门实践指南。 评论 Nickname Email Website 2 comments 流情 2025-08-05 1

看得出来,纯技术不添加 Windows 10 Chrome 101.0.4951.64 hudi Admin 2025-08-06 Reply @流情 :

学习笔记嘛😂。我在想要不要整一个小记的模块。 Windows 10 Chrome 138.0.0.0 Kulafan 2025-07-13 1

我的网页收藏夹真的好乱呀。。收藏的时候懒得分类。。要用的时候找半天。就是没这个习惯。后续也不知道该怎么分类 有时候分类了之后很多经常用的书签也不知道去哪了,现在对我而言最高效就是找个笔记软件把后续可能要用到的都记下来 Android Quince Tart Chrome 137.0.0.0 hudi Admin 2025-07-13 Reply @Kulafan :

我的也是很乱,想找个工具管理起来的😂 Windows 10 Chrome 138.0.0.0 Powered by Twikoo v1.4.15 目录94

Maxwell
    一、Maxwell简介
    二、Maxwell原理
        输出说明
        二进制日志
        主从复制
            主从应用场景
            主从复制原理
    三、Maxwell部署
        配置MySQL
        安装运行

最新文章 CNB迁移和UI定制 CNB迁移和UI定制 2025-09-13 ZFile工具 ZFile工具 2025-09-08 CNB刷新EO缓存和插件化 CNB刷新EO缓存和插件化 2025-08-30 CNB远程部署和EdgeOne Pages CNB远程部署和EdgeOne Pages 2025-08-24 CNB切换默认开发环境 CNB切换默认开发环境 2025-08-23 CNB创建知识库 CNB创建知识库 2025-08-09 小工具 小工具 2025-07-13 maxwell学习笔记 maxwell学习笔记 2025-07-12 飞牛云NAS搭建 飞牛云NAS搭建 2025-06-25 kubernetes学习笔记(一) kubernetes学习笔记(一) 2025-06-20 关于本站

心情树洞
留言板
在线状态
本站分析
订阅本站

我的圈子

博客录
博友圈

文章整理

学习笔记
工具
博客美化
生活

自建工具

IT工具箱
菜谱
思维导图
我的相册
相册后台

友链

indude - 个人文章分享
李开开博客
小赵の破宅
Yves
轻雅阁

©2024 By Hudi & Hi,朋友别来无恙! 蜀ICP备2024107539号 川公网安备51169602000107号

本站已运行:336 天 7 时 48 分 18 秒

使用Hexo框架 | 基于Butterfly主题

作者: Hudi(唱唱反调) 链接: https://www.hudi.space/posts/1a244a1c/ 来源: 短巷与雨 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



Partners Support Sign In Why Backblaze Contact Sales Start Free

Home
Cloud Storage
Backing Up
Business Lab
Backblaze Bits
Tech Lab
Partner News

35.8k Shares facebook sharing button Share twitter sharing button Tweet email sharing button Email sharethis sharing button Share linkedin sharing button Share hackernews sharing button Share reddit sharing button Share arrow_left sharing button The 3-2-1 Backup Strategy

May 23, 2024 by Yev // 38 Comments

A lot has changed since the 3-2-1 backup rule was first introduced in the late aughts. At the time, the iPad was just a glimmer in Apple’s eye. Facebook had a quaint 500 million users. Taylor Swift had only released two albums. Blockbuster Video still existed, and Netflix shipped DVDs to your door.

Unlike most things in technology, the rule has held up over the years. It’s still the de facto standard for keeping your data safe. But some of the particular best practices have evolved as data storage has changed. Today, I’ll explain the 3-2-1 rule, what’s changed, and how you can easily achieve a 3-2-1 backup to keep your data safe and protected. What Is the 3-2-1 Backup Rule?

The 3-2-1 backup rule is a simple, effective strategy for keeping your data safe. It advises that you keep three copies of your data on two different media with one copy off-site. Let’s break that down:

Three copies of your data: Your three copies include your original or production data plus two more copies.
On two different media: You should store your data on two different forms of media. This means something different today than it did in the late 2000s. Ill talk a little more about this in a bit.
One copy off-site: You should keep one copy of your data off-site in a remote location, ideally more than a few miles away from your other two copies.

If you want to protect your personal information, photos, work files, or other important data, the 3-2-1 backup strategy is the way to go. It helps you avoid having a single point of failure that’s vulnerable to human error, hard drive crashes, theft, natural disasters, or ransomware. How Does the 3-2-1 Backup Rule Work?

Let’s say you took a picture of your social security card for your tax accountant years ago—that file is called “socialsecurity.jpg” and it lives on your computer at home. That’s the first “copy” of your data.

You also have an external hard drive at home, used to back up your go-to Mac or gaming PC. That external hard drive will back up socialsecurity.jpg as part of its backup process. That’s a second copy on a different device or medium.

In addition to that external hard drive, you also have an online backup solution (we recommend Backblaze; go figure!). The online backup continuously scans your computer and uploads your data to the cloud (which, in layman’s terms, is an off-site data center). Socialsecurity.jpg is included in this upload, becoming the third copy of your data.

Oh! And, your paper social security card is hopefully stored in a fire-proof safe (not your wallet) as a bonus. What’s Changed About the 3-2-1 Backup Strategy?

When the 3-2-1 rule was first introduced, there were a lot more types of media to choose from when storing your data—the humble floppy disk, CDs, Blu-ray discs, USB sticks, external hard disk drives (HDD), solid state drives (SSD), network attached storage (NAS), tape libraries, etc. Some of those have fallen out of favor (CDs and DVDs, I’m looking at you). A photo of a coffee cop sitting on top of floppy disks. Don’t worry, floppy disks. We found a use for you.

Some types of media are not practical or affordable for a typical home computer user looking to back up their data (tape libraries, for example). Some of the technologies were prohibitively expensive back then, but are much more affordable now (SSDs). And one big one wasn’t mainstream yet: The Cloud™ (you might have heard it referred to as “other people’s computers”). So, what does this mean for the 3-2-1 backup strategy? Do you still need to keep your data on two different media? Two Different Media, Really?

The short answer is: yes, but no. Today, you don’t need to keep your data on two different types of media, but you do need to keep your data on two different devices.

The long answer is a bit more complicated. There are a couple reasons folks recommended keeping your data on two different types of media in the first place. One, it protects you from one of those forms of media becoming obsolete in the face of new storage technology (still looking at you, CDs) and your data becoming unreadable. And two, it’s wise to keep your backup copy on a separate device so that a hardware failure doesn’t take out both local copies. For example, if your computer all of the sudden doesn’t want to hold a charge, you can still recover data from your hard drive.

While obsolescence is always a concern, the advent of cloud storage for backups all but eliminates it. The cloud service provider is responsible for maintaining the physical storage devices and keeping your data accessible at all times. So, if you use a cloud backup service, you only need to worry about keeping your data on two devices, not two separate kinds of media. What does that look like? The Easiest 3-2-1 Backup

If you back up your home computer to an external hard drive and back both of those devices up to the cloud using something like Backblaze Computer Backup, congratulations: You have achieved a 3-2-1 backup.

You have three copies of your data: One on your computer, one on your hard drive, and one in the cloud.
You store your data on two different devices: Your computer and your external hard drive. (Technically, three devices, since your data is also stored in the cloud).
One of those copies is off-site: The cloud copy.

Is the 3-2-1 Strategy Still the Standard?

If you aren’t backing up at all, achieving a 3-2-1 backup strategy is still the best thing you can do to protect your data. But, the 3-2-1 rule is becoming more of a starting point rather than the finish line in today’s world.

The rise in ransomware attacks calls for strengthening the basic principles of the 3-2-1 strategy—redundancy, geographic distance, and access—with added protections. Cybercrimes targeting networked machines and capturing all data, including backups, is a growing problem. A decorative image showing a cyberpig, a cyber attacking pig character, using his laptop to attack a server.

New versions of the tried-and-true backup strategy have emerged, such as the 3-2-1-1-0 or 4-3-2 backups. Sounds like overkill? It isn’t. The good news is that companies like Backblaze exist to make at least the off-site component less stressful—we do the work and keep up with security best practices for you. Why Do I Need Both an On-Site and an Off-Site Backup?

Whether you are interested in backing up a Mac or a PC, an on-site backup is a simple way to access your data quickly should anything happen to your computer. If your laptop or desktop’s hard drive crashes, and you have an up-to-date external hard drive available, you can quickly get most of your data back or use the external drive on another computer while yours gets fixed or replaced. If you remember to keep that external hard drive fairly up to date, the exposure for data loss is negligible, as you might only lose the uncopied files on your laptop. Most external hard drives even come with software to ensure they’re readily updated.

Having an on-site backup is a great start, but having an off-site backup is a key component in having a complete backup strategy, including cloud storage. The newer backup strategies build on the cloud’s strengths:

Convenience: Backing up large volumes of data in the cloud is fast.
Durability and reliability: Your data is protected against fires, natural disasters, and more.
Collaboration: Sharing with permissions is intuitive and effortless in the cloud.

Is the 3-2-1 Backup Rule Perfect?

There is no such thing as a perfect backup system, but the 3-2-1 approach is a great start for most people and businesses. Even the United States government recommends this approach. In a 2012 paper for the United States Computer Emergency Readiness Team (US-CERT), Carnegie Mellon recommended the 3-2-1 method in their publication: Data Backup Options. Backing Up Is the Best Insurance

The 3-2-1 plan is great for getting your files backed up. If you view the strategy like an insurance policy, you want one that provides the coverage needed should the unthinkable happen. Service also matters; having a local, off-site, and offline backup gives you more options for backup recovery. Backblaze Backup in 3-2-1…

While Backblaze can’t help with power outages, computer encryption, or anti-theft technologies (though we can locate a computer), we can help make backing up your files a no-brainer. And (at least to our most recent survey) with only 11% of respondents who own a computer backing up daily, folks need the help!

Getting started with Computer Backup for your personal or business computers helps take care of that crucial “1” in your complete 3-2-1 backup strategy. And, with our included one year Version History feature (or Forever Version History if you want to upgrade), you have additional layers of protection should anything happen to your physical devices.

Categorized as Backing Up , Featured-Backing Up Tagged ConsumerBackup About Yev

Yev Pusin is the Head of Communications and Community at Backblaze, which he joined in 2011. Yev has a degree in business and communications from the University of Iowa, where he developed an alliteration affinity. Yev enjoys writing in an amusing way about the "why" of things and how decisions are made, so that readers can learn and be entertained all at once. Follow Yev on: Twitter: @YevP | LinkedIn: Yev Pusin Related Posts Legal Hold Is Here: Protect Your Business When It Matters Most

July 24, 2025 Backblaze + Kandji: Native Mac Backup with Integrated Endpoint Management

February 20, 2025 Holiday Gift Guide 2024

November 19, 2024 Get the latest from Backblaze straight to your inbox.

Subscribe today to receive relevant Backblaze content, newsletters, and emails. Email*

Unsubscribe anytime. Read our Privacy Policy for details about how your email is used.

A Publicly Traded Company (BLZE) Backblaze © 2025 About Us Newsroom Investor Relations Careers Terms and Policies Privacy Contact


Emil Kowalski Design Engineer

When done right, animations make an interface feel predictable, faster, and more enjoyable to use. They help you and your product stand out.

But they can also do the opposite. They can make an interface feel unpredictable, slow, and annoying. They can even make your users lose trust in your product.

So how do you know when and how to animate to improve the experience?

Step one is making sure your animations have a purpose. Purposeful animations

Before you start animating, ask yourself: what’s the purpose of this animation? As an example, what’s the purpose of this marketing animation we built at Linear?

You can view the full animation on linear.app/ai.

This animation explains how Product Intelligence (Linear’s feature) works. We could have used a static asset, but the animated version helps the user understand what this feature does, straight in the initial viewport of the page.

Another purposeful animation is this subtle scale down effect when pressing a button. It’s a small thing, but it helps the interface feel more alive and responsive.

Sonner’s enter animation, on the other hand, has two purposes:

- Having a toast suddenly appear would feel off, so we animate it in.
- Because it comes from and leaves in the same direction, it creates spatial consistency, making the swipe-down-to-dismiss gesture feel more intuitive.

Sonner
An opinionated toast component for React.
Sonner
An opinionated toast component for React.
Sonner
An opinionated toast component for React.

But sometimes the purpose of an animation might just be to bring delight.

Morphing of the feedback component below helps make the experience more unique and memorable. This works as long as the user will rarely interact with it. It’ll then become a pleasant surprise, rather than a daily annoyance.

Press on the button to see it morph.

Used multiple times a day, this component would quickly become irritating. The initial delight would fade and the animation would slow users down.

How often users will see an animation is a key factor in deciding whether to animate or not. Let’s dive deeper into it next. Frequency of use

I use Raycast hundreds of times a day. If it animated every time I opened it, it would be very annoying. But there’s no animation at all. That’s the optimal experience.

To see it for yourself, try to toggle the open state of the menu below by pressing J and then K. Which one feels better if used hundreds of times a day? Command Menu Suggestions Commands

When I open Raycast, I have a clear goal in mind. I don’t expect to be “delighted”, I don’t need to be. I just want to do my work with no unnecessary friction.

Think about what the user wants to achieve and how often they will see an animation. A hover effect is nice, but if used multiple times a day, it would likely benefit the most from having no animation at all.

Imagine you interact with this list often during the day.

The same goes for keyboard-initiated actions. These actions may be repeated hundreds of times a day, an animation would make them feel slow, delayed, and disconnected from the user’s actions. You should never animate them.

To see it for yourself, focus on the input below and use arrow keys to navigate through the list. Notice how the highlight feels delayed compared to the keys you press. Now press

(shift) and see how this interaction feels without animation. Command Menu Suggestions Commands

Press shift to toggle the animation

But even if your animation won’t be used too often and it fulfills a clear purpose, you still have to think about its speed… Perception of speed

Unless you are working on marketing sites, your animations have to be fast. They improve the perceived performance of your app, stay connected to user’s actions, and make the interface feel as if it’s truly listening to the user.

To give you an example, a faster-spinning spinner makes the app seem to load faster, even though the load time is the same. This improves perceived performance.

Which one works harder to load the data?

A 180ms dropdown animation feels more responsive than a 400ms one:

Click on the buttons to compare the speed.

As a rule of thumb, UI animations should generally stay under 300ms.

Another example of the importance of speed: tooltips should have a slight delay before appearing to prevent accidental activation. Once a tooltip is open however, hovering over other tooltips should open them with no delay and no animation.

This feels faster without defeating the purpose of the initial delay.

Radix UI and Base UI skip the delay once a tooltip is shown. Building great interfaces

The goal is not to animate for animation’s sake, it’s to build great user interfaces. The ones that users will happily use, even on a daily basis. Sometimes this requires animations, but sometimes the best animation is no animation.

Knowing when to animate is just one of many things you need to know in order to craft great animations. If you’d like to dive deeper into the theory and practice of it, I’ve created a course that covers everything you need to know: Check out "Animations on the Web"


Allen Pike

Articles About Follow

Building Something Big

On the pros and cons of the indie path.

August 31, 2025 • 3 min read

When I talk about building Forestwalk, people who’ve long known me are sometimes surprised that I’ve been using terms like “runway”, “venture-scale”, and other jargon more associated with the VC world than indie or lifestyle businesses. And indeed, I do have a secret to come clean about.

You see, for most founders, most of the time, it’s logical to build a “lifestyle business” rather than a venture-track one. The good lifestyle is right in the name.

Unluckily for me, working for a lifestyle was never that motivating. I love building software and teams and companies – if I earned enough to retire, I would just keep doing that. So instead of centring my first business around my lifestyle, it was focused on building great products and being a great place to work. Still, our ambitions were generally sized to ensure we didn’t need to make tradeoffs like working late nights, bringing on investors, or taking big risks.

This mostly achieved my goals. For a while.

Yet a standard human foible is that, as we achieve our dreams, we generate larger ones. A decade in, I didn’t just want to build great apps with a small team of good people. I wanted to build great products that had a positive impact on a lot of people, and I wanted to do that with a highly ambitious team.

Over the years I’ve had the chance to work with some really incredible folks – driven, passionate, smart, and ambitious. People who are unhappy with the status quo, and who rally their peers to do better work and set their sights higher.

As I was working last year towards founding Forestwalk, I realized that a core motivator for us was building with these kinds of people. But how the heck could we afford to do that?

Alex MacCaw highlighted this dynamic in his generally excellent Lifestyle business FAQ:

Pros of lifestyle businesses:

    Fairly straightforward way to get rich
    Earn while you sleep; escape the 9 to 5 rat race
    Focus on other pursuits, like writing, traveling, family, etc

Cons of lifestyle businesses:

    Unreliable source of income (at least initially)
    Does not force ones self-growth (unlike venture-backed companies)
    Most likely you wont work closely with incredible people (can get boring/lonely)

There it is. If you want to constantly be learning, and attract and retain a team full of world-class people who are driven to push you to do so – the sort of people you dream of working with – the best way to do that is to build a venture-scale business. So if you’re a weirdo who cares more about that than you do about your own stress levels, you should swing big.

So that’s what we’ve been doing.

That’s why, earlier this year, when we concluded the LLM evals product we’d been working on could make a meaningful business but not a venture-scale one, we pivoted to something new (using what we’d learned as kindling). And why we’ll keep adjusting our plan until something clicks that we could plausibly build into something big. Not because building a huge company is inherently good, but because building toward something big is the best way to attract incredible people.

Of course, it might not work. Things are still very early. But I thought it was worth being straight: that’s the goal. We’re going to build something big, or die tryin’.

Wish us luck.

Latest in the series Growing Startups.

Latest in the series Forestwalk.

Liked this? Follow along to see what's next. Allen Pike

Articles About Follow

© Allen Pike. 👋🏼 Feel free to contact me.


Caddy Web Server On FreeBSD

Home
Notes
CV

Caddy Web Server On FreeBSD freebsd caddy web server

Caddy is an open-source, middleware, secure, HTTP/2-enabled webserver written in Go programming language that has been created in 2015. Caddy configuration and initiation is so simple and clear. Caddy lets you create an HTTPS-enabled website in 5 seconds. Beside of this ease of use, the SSL certificate costs you nothing. Caddy supports HTTP/2 and automatic TLS encryption. HTTP/2 is an HTTP protocol successor that can load websites faster. Caddy automatically gets an SSL key and then serves your website securely. Caddy integrated with Let’sEncrypt, a certificate authority that provides free TLS/SSL certificates. Caddy supports a variety of Web technologies and is available as statically-compiled binaries for Windows, Mac, Linux, Android, and BSD operating systems on i386, amd64, and ARM architectures. A variety of web site technologies can be served by Caddy, which can also act as a reverse proxy and load balancer. Most of Caddy’s features are implemented as middleware and exposed through directives in the Caddyfile (a text file used to configure Caddy). Caddy is not vulnerable to a number of widespread CVEs including Heart-bleed, DROWN, POODLE, and BEAST. In addition, Caddy uses TLS_FALLBACK_SCSV to prevent protocol downgrade attacks. Caddy Features

• HTTP/2 enabled • SNI • OCSP Stapling • Virtual hosting • Native IPv4 and IPv6 support • Serve static files • Graceful restart/reload • Reverse proxy • Load balancing with health checks • FastCGI proxy • Templates • Markdown rendering • CGI via WebSockets • Gzip compression • Basic access authentication • URL rewriting • Redirects • File browsing • Access, error, and process logs • QUIC Support How to Install Caddy in FreeBSD 11.1?

To install caddy all you have to do is:

pkg install caddy

You can simply issue “caddy -h” to get how to use caddy:

caddy -h

-agree Agree to the CA’s Subscriber Agreement -ca string URL to certificate authority’s ACME server directory (default “https://acme-v01.api.letsencrypt.org/directory") -catimeout duration Default ACME CA HTTP timeout -conf string Caddyfile to load (default “Caddyfile”) -cpu string CPU cap (default “100%”) -disable-http-challenge Disable the ACME HTTP challenge -disable-tls-sni-challenge Disable the ACME TLS-SNI challenge -email string Default ACME CA account email address -grace duration The maximum duration of graceful shutdown (default 5s) -host stringDefault host -http-port string Default port to use for HTTP (default “80”) -http2 Use HTTP/2 (default true) -https-port string Default port to use for HTTPS (default “443”) -log string Process log file -pidfile string The path to writing the pid file -plugins List installed plugins -port string Default port (default “2015”) -quic Use experimental QUIC -quiet Quiet mode (no initialization output) -revoke string The hostname for which to revoke the certificate -root string The root path of the default site (default “.”) -type string Type of server to run (default “HTTP”) -validate Parse the Caddyfile but do not start the server -version Show version Caddy Configuration

First, we create a directory and name it caddy:

mkdir caddy

Then copy your index.html into it:

cp index.html ./caddy/index.html

Then go to this directory and issue caddy command:

caddy -host corebox.ir -cpu 50% -log log.txt -agree

Activating privacy features... done.
https://corebox.ir
http://corebox.ir

Then we can open “corebox.ir” in the browser. The point is caddy automatically activate SSL key.

You can get full edition at: https://contents.meetbsd.ir/ebook/caddy_bsdmag.pdf Or: https://bsdmag.org/download/caddy-web-server-on-freebsd/

enter image description here

© All rights reserved. Powered by FreeBSD and Caddy


Skip to content Ryan Collins Ryan Collins

Home
Projects

Asides
Pictures
Deep Thoughts

🕹️ Do Something Great! 😄 Start journaling with a simple text file

Written by ryan

in Projects

on December 30, 2019 Photo by Cathryn Lavery on Unsplash

Journaling has many benefits, and for those of us that can’t remember what we did yesterday, journaling is also a way of keeping track of what we have done. In the future I’m hoping I’ll be able to use my journal to pass down my exploits to a younger generation. My history of journaling

I’ve tried a few different approaches to journaling. For a long time (over 20 years) I’ve run a blog at RyanCollins.org. This works for big ideas that I don’t mind making public, but there are always entries that I would like to keep private. So, for a while I was using an iPhone app. This was ok, but I didn’t like having my journal in some proprietary format.

I tried to use email. Google allows + aliases, so I could email to username+journal@gmail.com and set up a filter to automatically label those emails as my journal. The problem with this was that if I didn’t email right away, the dates and times wouldn’t match the email. Plus, there was no way to edit past posts.

Next up was a private journal hosted on WordPress. This worked pretty well. If you want to journal, do check them out. When creating a blog, you can set it to private, so no one can see it except for you after you logged in. The P2 theme is very good for journaling. It shows a box at the top of the page in which to write your posts. Where I am today

After experimenting with the above ideas, I settled on a simple journal based on a text file named after the year. The journal for this year is 2019.markdown. I write in Markdown, which is a way to write plain text but with formatting. Since the journal is plain text, I know I’ll always be able to read it, no matter what computer or device I use.

It takes nothing to try it out. Download a plain text editor such as Atom. Create a new text file named 2019.markdown and start writing!

I don’t enjoy using a full-blown word processor such as Microsoft Word or LibreOffice. Too much bloat to impede adding a journal entry. Plus, they are harder to save as a plain text file. How I format my journal

Some people split off a text file for each day. This leads to a bunch of files (365 per year to be exact, 366 in a leap year). I prefer to keep the whole year in one file. My current journal for 2019 is approaching 28,000 words. Which seems like a lot, but the entire file is 166K. This is pretty small in the grand scheme of things.

I start each day with the date and the day of the week:

12/27/19 - Fri

(For my European friends, you’ll probably want to put it in day/month/year format.)

Each entry is one line, starting with the date and time:

2019/12/27 15:29 - Working on my journaling article for ryancollins.org #blog

Yes, I am not consistent with my date formatting, maybe I’ll fix that next year. I also add hashtags to the end of the entry. These hashtags may be people’s names, or subjects of the entry. For example, when I write an entry about a movie I’ve seen, I’ll tag it with #media. If it’s a fact I’ve learned, it will be tagged with #til. An accomplishment is #accomplishment. These tags are there to help me find information in my journal. Inserting a date and time

For Atom, there is a package you can use to automatically insert the date and time. Other text editors usually have the option to do the same. Journaling from mobile

This is all well and good, but what about adding entries from your phone? To do that, you’ll need to save your text file in a cloud storage system such as Dropbox, iCloud, or Google Drive. Dropbox now has a limit of three devices on the free account, but that probably won’t be a problem for you. Save your journal in your cloud storage, and then edit it with any text editor on your mobile device. For the iPhone, there is Pretext (Free) and iA Writer ($8.99). Android users can use Markor (free) or iA Writer ($8.99). 2019 and beyond

I have never been this consistent with journaling before this year. Looking back at my past journals and notes, it looks like I get the journaling bug every fall, but lose steam and stop by spring. I’ve kept it up easily for an entire year. Share this:

Telegram Email Pocket Reddit

Like this: Loading… howto journal ←Say hello to my new toy! This is a valid supper, right?→ More posts

Time for a rewatch!
September 20, 2025
Finally, something we can all mostly agree on. At least put triceratops into the top 3!
September 19, 2025
Im home from my 9th Ryan Meetup. The Ryans are such a good group of people to hang out with, and I do honestly miss them after the events. A big thank you to @ryanthegrill for starting the Ryan Meetups and to her, @ourcuz, & @ryanle for being able to not only continue the Meetups, but also for coming up with ideas for e Sch one that keeps the concept fresh.
September 15, 2025
I call this one View from across the aisle on a bus
September 14, 2025

Ryan Collins Ryan Collins

Do something great!

Blog
About
FAQs
Authors

Events
Shop
Patterns
Themes

Twenty Twenty-Five

Designed with WordPress notification icon


向 #define int long long 说不 2023-02-08技术向469 字 TL;DR

define int long long 是一种未定义行为,尽量不要在代码中使用它。

前言

在算法竞赛社区中,经常能看见有人在代码中使用 #define int long long 来偷懒。我是一直极力反对这种做法的,因为这种做法会导致代码的可读性大大降低,并带来一些难以预料的问题。 C++ 标准

在 ISO/IEC 14882:2014(E) 的 17.6.4.3.1 Macro names 一节中,有这样一段描述:

翻译并整理一下,就是:

翻译单元不可 #define 或 #undef 词法上等同于下列部分的名称:

    C++ 中的关键字(表 4、表 5,在 2.12 节 Keywords [lex.key] 中给出);

    有特殊含义的标识符(表 3,在 2.11 节 Identifiers [lex.name] 中给出);

    任何标准属性记号(attribute-token,在 7.6 节 Attributes [dcl.attr] 中给出)。

也就是说,标准中 并不允许 #define int 这种操作。 编译器实现 GCC

在 GCC 的 C Preprocessor 文档中 给出了下面的说明:

You may define any valid identifier as a macro, even if it is a C keyword.

也就是说,GCC 并没有严格按照标准来实现预处理器,而是稍微放宽了一些限制以允许通过这种方式来使得代码更加灵活,便于增强代码的向下兼容性。 Clang

相关文档中并未提及是否允许 define 关键字,但源代码中未见相关限制。 MSVC

define 指令 相关文档中并未提及。

后记

使用适当的数据类型来存储数据,有利于代码的可读性和稳定性,便于编写和调试。同时,正确设置变量类型也能提高程序的运行速度和效率。因此,我们应该做好正确的数据类型定义,而不是在编写代码时滥用 #define int long long。 向 #define int long long 说不 https://blog.baoshuo.ren/post/say-no-to-define-int-long-long/ 本文作者 宝硕 发布于 2023-02-08 版权协议 CC BY-NC-SA 4.0 转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

C++

喜欢这篇文章?为什么不考虑打赏一下作者呢? 我的 OI 生涯 —— 一名退役竞赛生的回忆录 再见,2022 —— 我的 2022 年度总结


跳至主要内容 创业者Ray - ie9.org

一个技术人的非技术博客——移民(SUV项目)、创业、生活、在路上 为啥我觉得美国和加拿大最终不会合并 作者: Ray 二月 11, 2025

川普上任之后多次说加拿大应该和美国合并,另外他还想收下格陵兰岛和巴拿马运河。这在很多人看来,这些观点都挺疯的。我也一样觉得有点惊讶,但如果说是从川普嘴里说出来的,又不意外了。

虽然美国和加拿大在政治、经济、文化、法律和教育等等几乎方方面面都是相似的,甚至包括人们的生活方式都几乎一样,如果只从技术上讨论会不会合并,我真不觉得有什么难度。哪怕加拿大属于英联邦都不会成为合并的障碍,因为毕竟英联邦早就只是象征意义的名字而已,甚至都没有实际的意义。合并之后,加拿大依然完全可以相对独立地保留自己的“国旗”等文化和情感上的东西,现在各省不都还有自己的省旗吗?

至于格陵兰岛和巴拿马运河,美国拿下它们的难度不大,而且可能性极高,但合并加拿大在我看来就不大可能了。以下是我的个人观点:

加拿大是一个特别讲政治正确的国家

加拿大的政治氛围整体上是偏左的,包括自由党、保守党和新民主党,只有极少数一两个极右的政治党派,但它们都完全没有存在感和影响力。所以,如果说美国要合并加拿大,政治正确的加拿大政客和人民在情感上是无法接受失去主权的。

合并会造成加拿大人口流失

虽然加拿大有自己的优势,包括友好的环境、全民免费医保等,但如果美国和加拿大真的合并了,必定会有大批的加拿大年轻人南下美国工作和生活——显然美国有更多的工作机会和更高的收入。过去十年加拿大政府呕心沥血引进的几百万新移民可能要全部贡献出来,这对加拿大本地的经济发展和社会福利势必会造成极大的影响,这显然不是当地政府和人民想看到的结果。

另外,当前主要的两党自由党和保守党肯定也都不希望两国合并,因为如果是那样的话,就有可能引入美国新的政治力量,对他们自己的政治生涯肯定是有影响的。

美国并没有真的想合并加拿大

这可能是最主要的原因了。川普是出了名的大嘴巴,炒作一直是他的强项。他针对加拿大增加关税的主要目的很可能只是美国的边境安全问题而已。之所谓故意夸张地炒作只是为自己争取更多的筹码,尽管他已经有很多了。另外,在加拿大总理特鲁多承诺加强边境执法之后,他也马上推迟实施新关税一个月。

但事情没有结论之前一切都是猜想。有人说每年从美国流入加拿大的毒品和枪支比加拿大流进美国的要多得多,也有人说美国合并加拿大只是为了占领更多的资源。真相是什么可能只是政客自己知道吧。

canada politics the states Location: Edmonton, AB, Canada 评论 此博客中的热门博文 新移民买车全攻略——省钱经验分享 作者: Ray 六月 16, 2024 图片 我对汽车的认识算比较晚了,几乎在出国前的两三年才开始了解一些汽车常识。正是因为这点常识让我入了门,在移民到加拿大之后玩了不少车,又学了不少东西。这里总结一些我的经历,可能会帮助刚开始加拿大生活的新移民少走些弯路。 这几年我在曼省、安省以及阿省都居住过,玩过二手的轿车、SUV、皮卡,以及一些豪华品牌。我没买过新车的主要原因当然是穷了,不过更重要的原因是二手车的性价比太高了。买新车几乎没什么技术含量,这几年在整个行业供应链紧张的时期最多只需要关注有没有现车,以及利率等问题。相比之下买二手车就不是太简单的事。 有些大的车行会同时卖新车和二手车,小点的私人车行一般只卖二手车。一般来讲,从车行买车还是比较有保障的(极端情况不讨论)。我也建议新移民登陆之后先从车行入手一台二手车作为代步,后面慢慢熟悉市场和需求确定下来之后再考虑换车。因为正规的车行资质是齐全的,就算是小的私人车行,为了信誉一般都有自己的标准,比如至少要保证车是能过检测,没有明显的问题等等。买到手至少开个半年一年都不会有什么问题。但缺点就是价格相对私人卖家当然会高一些,这也是为什么开车行在加拿大算是个挺赚钱的行当。等到对市场和车辆的检测比较了解之后,就可以开始考虑从私人购入二手车了。说实话,目前市场上的那些私人二手车除了主动申明有问题的之外,一般也都没有太大的问题,毕竟私人之间的二手车转手也是很流行和普遍的。在看私人的二手车时一般最应该注意三个问题:发动机漏油、变速箱问题,以及车身锈迹多,任何一个有问题都应该一票否决,便宜也不能要,除非真的很懂车并且自己能搞得定。 我们刚登陆的时候买车就是优先事项之一。比较顺利的是在一周之后从车行买了一台 2015 Subaru Impreza AWD hatchback,当时主要的考虑是在曼省生活的话需要一辆四驱的车,另外因为刚到对驾驶还不是很自信,这台小车操控性比较好会是一个优势,以及我当时对日系车的质量稳定的迷信还在。现在想来有几个地方确实思考不周,但也有客观原因。一个是当时那家店的车源不多,有点矮子里选将军的意思,另外当时接待的华人销售虽然没有坑我们,但也仅限于公事公办的态度,感觉对我们买车帮助不大。至于 Subaru 这个品牌,不能说不好,反正后来我再也没买过 Subaru。接着后面几年买过这些车: 2008 Lexus LX570,真是好车,里外上下的做工都很好,用料扎... Read more 离开安省,以及对加拿大各省的认知 作者: Ray 三月 08, 2024 自从去年 10 月举家搬到阿省来之后,感觉一直在忙,各个平台上的信息几乎都停止更新了。时间真快,转眼一年又过去了。 记得几个月前国际形势比较紧张的时候,很多人非常焦虑地找到我,问关于移民的事情。现在相对来说缓和得多了,焦虑的人也少了。也正常,移民一直以来都是极少数人会去尝试的。我也在考虑逐渐摆脱自己身上关于移民的那些标签,如果有机会我也愿意多分享一些关于自己在加拿大的日常生活。这次我就这件事分享一些关于城市选择方面的个人观点。 城市选择绝对是非常个人的事情,以至于没有人问,或者主动去讲的话是很少人会谈到的。而且这个事情和个人经历相关,像我并没有去过多少加拿大的城市,所以我个人的判断也不一定正确和全面,仅供娱乐吧。另外,我不打算客观地对比优缺点,因为优点谁都知道,中介们最喜欢去讲了。所以我会着重分享我认为的缺点,以及澄清一些误区——这才是更值得关注和权衡的。 简单介绍一下地理。加拿大的面积比美国还大,但省份是非常少的。一共有 10 个省和 3 个地区。其中 3 个地区(努纳武特,西北和育空地区)我就不想提了,99% 的华人都不会去那些地方。剩下的 10 个省份大概可以分成这几个区域:西部的BC省,中部草原三省,安省和魁省,以及东部的海洋四省。 BC 省的省会在维多利亚,但最著名的城市就是温哥华。我相信绝大部分华人都在其中或者周边地区,像本拿比华人比例可能已经高达六七成。我一度认为温哥华是老一辈移民心目中最热门的城市,没想到很多找我咨询 SUV 移民的人也都把这里当作自己的首选城市——说起加拿大就是温哥华。不得不承认,温哥华确实在很多方面都算得上是加拿大最好的,比如气候环境。但温哥华的缺点也很明显,例如很多人可能不知道这里的冬天有多压抑——不是阴天就是下雨。另外生活成本比较高,我这里有个例子,一个朋友在本拿比住的一室一厅的小公寓每个月租金是 2000 多加币。另外油价和汽车保费也是比较高的,基本上比隔壁省高出 50%,也难怪经常听到那些南下去美国加油的加拿大车主们的故事。 草原三省我比较熟悉阿省和曼省。其中阿省的两大城市Edmonton和Calgary特别像国内的成都和重庆(以前),或者广州和深圳的感觉(现在),属于低调又安逸的地方。因为阿省盛产石油,所以整个省的经济比较强大,常年需要给其他赤字的省『输血』。生活在阿省有诸多好处,最主要的就是没有省税,而且油价全国最低,... Read more 2024年加拿大SUV项目政策变化汇总 作者: Ray 十月 17, 2024 在2024年,加拿大的SUV(创业移民)政策经历了多次重要调整,特别是在指定机构的支持限制、优先处理机制以及工签政策方面。这些变更不仅影响了申请人的策略,也为未来的创业者创造了新的机遇。本文将对这些政策做出详细分析,以期为有意申请者提供有效指导。 1. 指定机构支持的限制 自2024年4月30日起,加拿大政府对指定机构的支持数量进行了明确限制。无论是风险投资公司、天使投资团体还是企业孵化器,全年支持的初创企业数量不得超过十家。这一政策强调了每个项目获得充分资源和关注的重要性,旨在提升整体项目质量。尽管竞争将加剧,但优秀的创业计划将有更多机会得到深入支持与指导。 2. 优先处理机制的实施 为了提升项目的处理效率,加拿大移民局引入了新的优先处理机制。获得风投或天使投资支持的初创企业申请,以及由成员企业孵化器支持的申请,将优先进行处理。特别是对投资至少75,000加元的申请,将给予优先级。这一举措无疑将激励高投入、高质量的创新项目,从而促进市场的健康发展。同时,风险投资和天使投资者子类别的所有移民申请也将享受到优先处理的政策,反映出加拿大对于引入高潜力人才的重视。 3. 工签政策的重大调整 2024年10月3日,加拿大移民局宣布一项重要改革:SUV创业移民工签从之前的封闭型改变为开放型,且有效期从一年延长至三年。开放工签为创业者提供了更大的灵活性,使其可以根据市场动态调整业务方向。三年的有效期则为创业者提供了更稳定的经营环境,帮助他们更专注于长期发展。 4. SUV项目的配额与申请数据 在未来三年,SUV项目的配额将稳步增长,2024年为5000个,2025年和2026年均增至6000个。这显示了加拿大政府吸引国际创新人才的坚决意图。截至2023年4月,全球共提交了12,214份SUV申请,约30%已经处理。自2013年至2023年4月,SUV项目的获批率高达81.3%,为申请人实现创业梦想提供了良好的机会。 5. 政策变动的影响与建议 这些政策调整为申请人带来了新的机遇和挑战。优先处理机制和开放工签政策,为高质量项目开辟了通道。同时,指定机构支持的限制可能会加剧竞争。对于有意申请SUV项目的创业者,以下几点建议尤为重要: 精心打磨商业计划,突出项目的创新性和市场可行性。 积极寻找高质量的指定机构支持,尤其是那些能够提供优先处理的机构。 灵活利用开放工签政策,制定应变的创... Read more 由 Blogger 提供支持 我的照片

Ray Edmonton, AB, Canada

访问个人资料 Most Popular 新移民买车全攻略——省钱经验分享 作者: Ray 六月 16, 2024 图片 我对汽车的认识算比较晚了,几乎在出国前的两三年才开始了解一些汽车常识。正是因为这点常识让我入了门,在移民到加拿大之后玩了不少车,又学了不少东西。这里总结一些我的经历,可能会帮助刚开始加拿大生活的新移民少走些弯路。 这几年我在曼省、安省以及阿省都居住过,玩过二手的轿车、SUV、皮卡,以及一些豪华品牌。我没买过新车的主要原因当然是穷了,不过更重要的原因是二手车的性价比太高了。买新车几乎没什么技术含量,这几年在整个行业供应链紧张的时期最多只需要关注有没有现车,以及利率等问题。相比之下买二手车就不是太简单的事。 有些大的车行会同时卖新车和二手车,小点的私人车行一般只卖二手车。一般来讲,从车行买车还是比较有保障的(极端情况不讨论)。我也建议新移民登陆之后先从车行入手一台二手车作为代步,后面慢慢熟悉市场和需求确定下来之后再考虑换车。因为正规的车行资质是齐全的,就算是小的私人车行,为了信誉一般都有自己的标准,比如至少要保证车是能过检测,没有明显的问题等等。买到手至少开个半年一年都不会有什么问题。但缺点就是价格相对私人卖家当然会高一些,这也是为什么开车行在加拿大算是个挺赚钱的行当。等到对市场和车辆的检测比较了解之后,就可以开始考虑从私人购入二手车了。说实话,目前市场上的那些私人二手车除了主动申明有问题的之外,一般也都没有太大的问题,毕竟私人之间的二手车转手也是很流行和普遍的。在看私人的二手车时一般最应该注意三个问题:发动机漏油、变速箱问题,以及车身锈迹多,任何一个有问题都应该一票否决,便宜也不能要,除非真的很懂车并且自己能搞得定。 我们刚登陆的时候买车就是优先事项之一。比较顺利的是在一周之后从车行买了一台 2015 Subaru Impreza AWD hatchback,当时主要的考虑是在曼省生活的话需要一辆四驱的车,另外因为刚到对驾驶还不是很自信,这台小车操控性比较好会是一个优势,以及我当时对日系车的质量稳定的迷信还在。现在想来有几个地方确实思考不周,但也有客观原因。一个是当时那家店的车源不多,有点矮子里选将军的意思,另外当时接待的华人销售虽然没有坑我们,但也仅限于公事公办的态度,感觉对我们买车帮助不大。至于 Subaru 这个品牌,不能说不好,反正后来我再也没买过 Subaru。接着后面几年买过这些车: 2008 Lexus LX570,真是好车,里外上下的做工都很好,用料扎... Read more 离开安省,以及对加拿大各省的认知 作者: Ray 三月 08, 2024 自从去年 10 月举家搬到阿省来之后,感觉一直在忙,各个平台上的信息几乎都停止更新了。时间真快,转眼一年又过去了。 记得几个月前国际形势比较紧张的时候,很多人非常焦虑地找到我,问关于移民的事情。现在相对来说缓和得多了,焦虑的人也少了。也正常,移民一直以来都是极少数人会去尝试的。我也在考虑逐渐摆脱自己身上关于移民的那些标签,如果有机会我也愿意多分享一些关于自己在加拿大的日常生活。这次我就这件事分享一些关于城市选择方面的个人观点。 城市选择绝对是非常个人的事情,以至于没有人问,或者主动去讲的话是很少人会谈到的。而且这个事情和个人经历相关,像我并没有去过多少加拿大的城市,所以我个人的判断也不一定正确和全面,仅供娱乐吧。另外,我不打算客观地对比优缺点,因为优点谁都知道,中介们最喜欢去讲了。所以我会着重分享我认为的缺点,以及澄清一些误区——这才是更值得关注和权衡的。 简单介绍一下地理。加拿大的面积比美国还大,但省份是非常少的。一共有 10 个省和 3 个地区。其中 3 个地区(努纳武特,西北和育空地区)我就不想提了,99% 的华人都不会去那些地方。剩下的 10 个省份大概可以分成这几个区域:西部的BC省,中部草原三省,安省和魁省,以及东部的海洋四省。 BC 省的省会在维多利亚,但最著名的城市就是温哥华。我相信绝大部分华人都在其中或者周边地区,像本拿比华人比例可能已经高达六七成。我一度认为温哥华是老一辈移民心目中最热门的城市,没想到很多找我咨询 SUV 移民的人也都把这里当作自己的首选城市——说起加拿大就是温哥华。不得不承认,温哥华确实在很多方面都算得上是加拿大最好的,比如气候环境。但温哥华的缺点也很明显,例如很多人可能不知道这里的冬天有多压抑——不是阴天就是下雨。另外生活成本比较高,我这里有个例子,一个朋友在本拿比住的一室一厅的小公寓每个月租金是 2000 多加币。另外油价和汽车保费也是比较高的,基本上比隔壁省高出 50%,也难怪经常听到那些南下去美国加油的加拿大车主们的故事。 草原三省我比较熟悉阿省和曼省。其中阿省的两大城市Edmonton和Calgary特别像国内的成都和重庆(以前),或者广州和深圳的感觉(现在),属于低调又安逸的地方。因为阿省盛产石油,所以整个省的经济比较强大,常年需要给其他赤字的省『输血』。生活在阿省有诸多好处,最主要的就是没有省税,而且油价全国最低,... Read more 2024年加拿大SUV项目政策变化汇总 作者: Ray 十月 17, 2024 在2024年,加拿大的SUV(创业移民)政策经历了多次重要调整,特别是在指定机构的支持限制、优先处理机制以及工签政策方面。这些变更不仅影响了申请人的策略,也为未来的创业者创造了新的机遇。本文将对这些政策做出详细分析,以期为有意申请者提供有效指导。 1. 指定机构支持的限制 自2024年4月30日起,加拿大政府对指定机构的支持数量进行了明确限制。无论是风险投资公司、天使投资团体还是企业孵化器,全年支持的初创企业数量不得超过十家。这一政策强调了每个项目获得充分资源和关注的重要性,旨在提升整体项目质量。尽管竞争将加剧,但优秀的创业计划将有更多机会得到深入支持与指导。 2. 优先处理机制的实施 为了提升项目的处理效率,加拿大移民局引入了新的优先处理机制。获得风投或天使投资支持的初创企业申请,以及由成员企业孵化器支持的申请,将优先进行处理。特别是对投资至少75,000加元的申请,将给予优先级。这一举措无疑将激励高投入、高质量的创新项目,从而促进市场的健康发展。同时,风险投资和天使投资者子类别的所有移民申请也将享受到优先处理的政策,反映出加拿大对于引入高潜力人才的重视。 3. 工签政策的重大调整 2024年10月3日,加拿大移民局宣布一项重要改革:SUV创业移民工签从之前的封闭型改变为开放型,且有效期从一年延长至三年。开放工签为创业者提供了更大的灵活性,使其可以根据市场动态调整业务方向。三年的有效期则为创业者提供了更稳定的经营环境,帮助他们更专注于长期发展。 4. SUV项目的配额与申请数据 在未来三年,SUV项目的配额将稳步增长,2024年为5000个,2025年和2026年均增至6000个。这显示了加拿大政府吸引国际创新人才的坚决意图。截至2023年4月,全球共提交了12,214份SUV申请,约30%已经处理。自2013年至2023年4月,SUV项目的获批率高达81.3%,为申请人实现创业梦想提供了良好的机会。 5. 政策变动的影响与建议 这些政策调整为申请人带来了新的机遇和挑战。优先处理机制和开放工签政策,为高质量项目开辟了通道。同时,指定机构支持的限制可能会加剧竞争。对于有意申请SUV项目的创业者,以下几点建议尤为重要: 精心打磨商业计划,突出项目的创新性和市场可行性。 积极寻找高质量的指定机构支持,尤其是那些能够提供优先处理的机构。 灵活利用开放工签政策,制定应变的创... Read more 警惕中介陷阱:SUV 项目需谨慎对待 作者: Ray 八月 19, 2023 这几年号称能做加拿大联邦创业移民 SUV 的中介是越来越多了。和雇主担保一样,SUV 本身是正规的、合法的,但总有些中介为了赚钱,拿着申请人的身家去钻移民政策的空子,甚至明目张胆地违法。其它国家我不清楚,中国境内的所有声称能做 SUV 移民的公司(不用抬杠,没几个是无辜的)基本上都是一个套路:包装项目,再高价卖车位。这么操作除了之前提到过的一些问题之外,还有这些风险: 在创业团队这方面,很多项目因为实在等不到五个人凑成一个团队,就以三人、两人,甚至单人直接去递交申请。有些申请人自己可能也觉得与别人合伙很麻烦,自愿让中介帮他们申请成单人的项目。移民局确实没有明确说团队必须要够几个人,但是在实际操作中,移民局越来越倾向不认可人数少的项目了,不知道是不是与他们年度的 KPI 有关系,但可以确定的是人数少的项目,特别是单人的项目,被拒签的风险往往是更高的。因为从逻辑上也可以理解:移民局当然更希望看到申请人能带来成功的创业项目为加拿大的经济作贡献,他们也知道创业成功并不是容易的事,所以一方面不要求创业必须成功,另一方面也从更现实的层面考虑,他们会更喜欢至少看上去成功的可能性更大的项目。而人数少的项目,特别是单人项目很难说服移民官,这个申请人能凭一己之力就能搞定创业过程中的所有事情,而且还能带来更大的成功可能。 最近有几个人和我分享过他们的计划,也看到有一些中介机构在专门做这方面的生意:创业的同时去读书。虽然移民局没有明确规定创业的同时能不能读书,但是从以上原则性的要求来看,申请人一边创业一边读书肯定比全部投入创业去做成一个项目的可能性更低。当然也不能说一定不能这么干,更不能说边读书边创业就一定会失败。如果说申请人认为读书更重要、SUV 做不成了还能靠毕业工签完成移民的话,不妨一试。但如果把移民看得很重要,个人还是建议不妨先完成最重要的事。毕竟有了身份之后再读书能省不少钱,而且先在加拿大生活(创业)一段时间再读书也许对未来是否有必要读书、读什么专业这些问题有更深刻的理解。 SUV 项目的要求也在逐渐变化。我当时申请的时候并没有对盈利有任何要求,也没有对项目本身有什么额外的要求,只要满足基本的、原则上的要求就能通过。但是这几年的趋势显示,移民局似乎对申请项目有了更高的要求——他们已经不仅仅看项目本身是否具有创新性和盈利性,还会考察其它方面,比如项目是否能实际盈利,以及是... Read more 真想移民就不要只是想想而已 作者: Ray 五月 19, 2023 本站的电报群目前已经有 3000 多人了,我除了关注其中时事以及移民的话题之外,也顺便能感受到不同的人不同风格。毕竟是人比较多了,各种各样的情况也不算少见,有些观点我可能在群里可能说过很多遍,以后也一定还会有人提及,所以我想简单地总结一下关于移民决定方面的个人见解。 很多年前我对移民这件事只有一个大概的印象,可能前面的博文中也会有提及,简单地说就是,我决定移民总是为了去一个自认为更好的地方,要么是因为向往,要么是被逼迫,即要么是因为国外太好了而心生向往之,或者是因为觉得当下的情况实在太差被逼移民。我在真正开始办移民前的一两年里,向往的成分是更多的,因为我觉得自己当时的生活还算可以,虽然远没达到“知乎”的平均水平,但尚处于衣食无忧、有车有房这些看似平静且美满的状态。这个阶段我也想过移民的事,但确实没什么动力去做一些改变。直到 2018 年之后,很多因素综合起来才促成了这个决定。一方面,政治环境是首当其冲的。我虽然一直没有参与政治,但一直有关注。我相信很多人可能和我差不多,对于时局不满由来已久,嘴上不讲不过是觉得没必要而已,而且讲了也改变不了什么;另一方面也有职业因素,在之前一个自媒体的访谈中我也坦露了这一点。通过这件事情,我也意识到一个人在做一些重大决定的时候,至少对于我而言,一个更美好的前景可能并没有危机感产生出来的驱动力更大。就好像如果有一款药告诉我说吃了会更健康,我一定不会有兴趣,但是如果说不吃就会死,那我大概就会买了。 经常有人在群里问有没有低成本的移民方案,或者问国外有没有招工的等等。但是他们又觉得走线风险太高,就业又怕语言不好,日本韩国又看不起、不甘心,办正规移民项目又怕花钱……是的,大多数人只想收获而不愿意付出。我一直有个“移民不可能三角”的观点:合法、快速和低成本这三个选项里面,最多只能顾及两个。想合法且快速的的话,只有花钱投资、捐赠等等几个巨贵的途径,成本起码千万人民币以上才有可能有机会;想合法又低成本的话,完全可以选择美国劳工之类的移民项目,只要3万美金左右,但随随便便排期十年以上;想快速又低成本的方法就多了,政庇、走线、黑下来、造假等等,每一条都是不归路。当然,这个观点可能只适用于欧美等主要发达国家,并不包含像瓦努阿图、格林纳达,以及东欧等等实际上并不适合大多数人移民的地方。 另一个观点是:移民并不是一个解决方案,而只是换个环境继续生活... Read more Blog Archive 标签 Feeds 博文 评论 Direct Message

名称

电子邮件 *

消息 *

@ie9org Tweets by ie9org 举报滥用情况


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling Lisp 解释器终于写完了

记录完成 mal 项目实现 Lisp 解释器的踩坑过程,主要参照之前翻译的 mal 指南,边做边修改之前翻译得不清楚的地方。

我的实现 https://github.com/Windfarer/lisp-interpreter

虽然在写代码的过程中不断出现如下状况但很幸运最终还是完成了(

why Step 0

毫无难点,轻松飘过。修改配置项需要注意一下,配错了后面就没法正确运行测试。 Step 1

在实现 read_form 和 read_list 之间相互调用时花了一些时间,这里要注意在恰当的时候调整 reader 的 position,不能弄出死循环。

本步骤中我的类型判断做得比较粗糙,勉强通过测试,待后续完善。 Step 2

按照文档的说明实现,这一步坑比较少,发现我上一步的异常处理写得太垃圾,重新用 Exception 进行了实现。另外就是关键字类型、数字类型和字符串类型判断有问题,进行了修改。

另外这一步骤比较让人困惑的是要求修改 EVAL,在 ast 为列表时使用 eval_ast 对它进行求值,并将结果列表中第一个作为函数,对列表的后续进行求值。在测试用例中发现有第一个是字符串的,字符串无法作为函数对后续进行求值。这里可以判断一下第一个元素是不是 callable 的,如果不是就把 ast 原样返回。

拜 python 的可变长参数所赐,本步骤轻松愉快的过了,使用其他不支持可变长参数的语言可能要多花些时间。

另外在修改代码时,要时不时跑跑前面步骤的测试,以免破坏了已经实现的特性。 Step 3

这一步改动其实比较小,主要变化是环境改为使用 Env 对象实现,以及对 EVAL 函数中 apply 步骤的修改。难点在于后者,需要花一些时间整理在处理完 special form 之后返回的东西是什么。

本步我的困惑是,在环境中的符号是用 Symbol 的值还是用 Symbol 对象本身?我在实现中的做法是使用了 Symbol 的值,不知道是不是在挖坑。

另外到了这里我发现,之前恶劣的类型实现,debug 开始变得困难了,下一步中大概需要对这一部分进行重构 Step 4

本步骤测试用例极多。这一步修改了很多东西。在对 do 的处理这一块卡了很久,文档里要求使用 eval_ast 对列表元素逐个处理,尝试了半天,发现 eval_ast 并不能正确返回,反而使用 EVAL 来对元素进行处理才会有正确的求值逻辑,本步骤先这样通过了。 Step 5

实现 TCO 尾调用优化,这一步测试比较少,改动也比较少,文档上没有什么坑,照着做就可以顺利通过。 Step 6

这一步花费很多时间,因为之前实现的字符串读取部分的代码实现比较粗糙,大部分时间在修补 Step 1 和 2 写的那些代码。 Step 7

这一步实现 quoting,处理的 AST 结构比较复杂需要仔细一些。另外本步骤有一些列表的连接之类的操作,我在这步除了与 AST 结构搏斗以外,还折腾了很久 python 的列表类型以及 mal 里列表类型的区分和转换。 Step 8

本步骤增加了 macro(宏)这个特性。貌似坑不是很多,按照文档仔细实现即可,传参数稍有些复杂,要看清应该传什么别传错了。 Step 9

这一步实现 try catch 和 hash-map 数据结构,是为最后一步做准备。主要时间花在实现 hash-map 上,简单点的话实际上这个 hash-map 可以使用列表实现,就是查找起来比较慢,用 dict 实现的话要注意 key 的顺序,在插入修改和迭代时要保持一致。 Step A

最后一步,在你的解释器上运行 mal 实现的解释器,并让它通过全部测试。这步卡了一年(误 由于之前手贱在实现 Step 5 的代码里某个地方多加了一个 continue,导致在跑解释器的时候一直循环求值导致爆炸,直接让我的这个坑变成了跨年坑。

debug 的过程异常艰辛,以至怀疑人生,因为实际上你在做的是调试一个跑在你的解释器上的 lisp 实现的解释器,但 bug 存在于宿主语言的实现中,而这个 lisp 实现的解释器代码也很复杂,导致很难定位问题实际所在。我最后采取的办法是逐个步骤运行测试用例,直到找到某个最早的 mal 实现,在上面跑语句可以重现 bug。然后拆解这个 mal 实现的代码,逐层手工运行,直到定位到问题的真正位置。

这步只遇到了这一个自己挖的深坑,目测不同的人写会遇到不同的坑吧。 总结

之后看了一下官方的 python 实现,它基本上直接使用了 python 内置的类型来实现,最终效果比我的要简单很多。由于我采用了自己定义一套类型容器,把 python 内置的类型包在里面,但在写的时候又想使用 python 的一些语法糖,比如切片之类的,这就导致需要把这些方法 port 到实际的数据上;再有一点是因为 python 是一门动态类型语言,所以如果使用自己实现的类型容器,就极易掉入到我是谁,我在哪的疑惑当中,常常不知道自己在操作的是 mal 的类型还是 python 本身的类型,如果是使用静态类型语言,或者加上 type hints 的应该会好很多。(现在你知道什么叫 “动态一时爽,重构火葬场了吧”,我先去哭一会)开始认真考虑把 type hints 用到实际项目中了。

实际实现完这个东西会发现,虽然它已经强大到能够运行一个解释器了,但里面其实还有很多情况没有处理,输入一些不合规范的脚本就会爆炸,仍有很大的改进空间。

那么这个坑填差不多了,下一个坑挖点啥? Windfarer at 2018 Mar 01 | 技术 Share this post on Facebook Twitter ❮ 最近做 (看) 了些啥 pipenv 笔记 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 这是病,得电

用了一个小长假的时间读了这本《欲罢不能》,感想是人类这个充满缺陷的物种真鸡儿让人捉急。

书中描述了一个实验,给老鼠脑子里插上一个电极,并给它一个按钮,只要它按下按钮,电极就会放电让它爽一下,结果老鼠就不吃不喝一直按按钮,最后力竭而死了。联想到之前看池建强在卖桃者说写的,“只要努力抖音,人生很快就过去了”,不禁唏嘘。

生活中总有一些事情让你上瘾:没事去收个蚂蚁森林的绿色能量(我们老年人以前管这个叫偷菜),刷朋友圈 / 新闻 / 微博 / 抖音 / 豆瓣 / 知乎 /...,水群每隔半分钟点亮手机看一下群,不停网购 / 喜 + 1 最后发现买了一堆没毛用的东西,看 1024 看个没完,发现今天步数排名被人超过了我再外面多溜达几个小时,不停打一款弱智小游戏(包括并不限于 flappy bird,跳一跳,2048 等),看剧 / 看小说一口气到天亮,貌似手游冷却时间到了再刷两个任务,抽卡为了抽到纸片老婆不停氪金抽 du 卡 bo...

不过其实也没太好的办法,在中产阶级焦虑如此严重的今天,能找到一个不用很麻烦又能慰藉心灵的事情做,已经是很不错了。更何况,你是企图依靠一己之力,抵抗人人都是产品经理优秀的产品设计师们为了提高产品在线时长和营收,熬了无数个通宵设计出来的 “游戏规则”(甚至可以更无脑一些,通过直接丢无数个新功能出来,借助埋点来检测哪个功能成瘾性更强,来获得更 “有毒” 的功能组合)。在这种双重夹击下,人类显得不堪一击。

“上瘾不是喜欢,而是渴望。”

“哪怕吸引力减弱,瘾头仍然存在:快感早就没有了,可他们打游戏的渴望、疯狂整理的渴望、给自己来上一剂药的渴望,却一点儿也没减少。”

你越沉迷,会发现陷得越深,一开始的乐趣几乎当然无存,但你仍旧想继续,这就是行为上瘾了。书中提到,这种上瘾在大脑种产生的反应,与吸毒或酒精成瘾并没太大区别。

如今 VR 技术也在飞速发展,前一阵有个电影《头号玩家》,大家普遍对这片评价很高。VR 的成瘾性,甚至比刷朋友圈之类的当代行为成瘾产品要高出若干个数量级,它将人与现实世界隔绝开来,再进一步的话,恐怕就是脑后插管了。看到电影中那些个老哥戴个 VR 眼镜对着空气比比划划,我感到滑稽,却又十分担忧 —— 这可能真的是我们的未来。

那么,如何解决眼前的行为成瘾的问题呢?书中给出了一些解决方案,其中最重要的一个,是避免自己回到成瘾的环境中。其实道理很好理解,不要试图考验人性,依靠意志力的抵抗是无法持久的,你几乎无法做到让一个人呆在烧烤店里能够成功减肥,因为你不是在闹市读书的伟人。那么办法就很明确了,例如你发现自己每次睡前都要玩手机,那么就把手机电源从床头移到很远的地方,让自己躺在床上时接触不到手机。使用诸如此类的方法,让自己远离成瘾的环境和来源,能够较为有效的缓解症状。然而依然要时刻警惕自己再次陷入进去。

世卫组织最近将游戏成瘾正式列入精神疾病,其实能够上瘾的何止游戏呢?这并不是简单的把它当作疾病去治疗能够处理得了的。更何况在这个领域没有明确的治疗规范的时候,会有多少青少年落入雷电法王之类的江湖骗子手里,葬送自己的一生。

行为成瘾只是能暂时缓解焦虑,但只是逃避了对焦虑来源的处理,并不是真正解决问题,甚至反而让问题更加严重。如果现实世界过得好,谁愿意呆在虚拟世界中不出来呢?成瘾行为,有更深层次的现实原因和社会原因,而这个源头得不到解决,只能是治标不治本。

嗯,总结起来还是一句话,人类这个物种真是垃圾得不行,而且闲得蛋疼。 Windfarer at 2018 Jun 20 | 读书 Share this post on Facebook Twitter ❮ 只有 Python 魔法少女才知道的 PonyORM Consul 笔记 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 为什么应该旗帜鲜明地批判加班文化

这个行业许多公司,996 或者说加班文化似乎成为了一种常态。有赞凭借在年会宣布 996 工作制把自己推上了风口浪尖,这种跳出来公然 996,颇有一种不想让你好好过年的架势。今天我就要把它批判一番,讲讲为什么要反对加班文化。不,其实并没有 “管理一群程序员,就如同放牧一大群猫”

上面这句话出自当年贝尔实验室的 manager Barbara E. Moo(感谢评论区 sinxccc 大佬指正),你应该能发现说得似乎有些道理。很多管理者一再抱怨为什么项目频繁延期,为什么都 996 了但还是产出低下,为什么优秀的员工纷纷跑路,为什么傻 X 程序员每天要摸那么久的鱼等等。我相信不只是互联网行业存在这个问题,许多脑力劳动为主的行业都可能有类似的问题。你作为(一个技术团队的)管理者,如果把工程师们视为流水线上可以随时替换掉的工人,那么免不了有上面这些疑问。可是请问你有没有思考过,管理脑力劳动者与管理工人,到底有什么不同呢?

我建议你先在家里养上几只猫,好好体会一下,真的会有所感触。你会发现猫大部分时间都沉浸在自己的世界中,懒得理你

如果你不想养猫,那么可以试着读读这本书 ——《人件》。我读到此书也是机缘巧合,如果没记错的话那是很多年前我还在实习的时候,感到自己菜得一笔快要找不到工作,周末没事去福州路的上海书城闲逛寻找人生方向,书架上这本书奇怪的书名吸引了我的注意力。本想拿起来随手翻翻,结果居然在书店蹲了一下午,把整本读完了。不能说那时候对于这本书有什么理解,甚至都几乎忘记了书中讲了什么。而近日重温这本书,发现这几年工作下来,书中的各种观点居然得到一一验证。

这本书讲,在一个技术团队中,「人」的因素要比其他因素(比如技术)重要得多。 开发软件不是快餐店制作汉堡

书中最重要的观点就是:你不应该把开发软件项目和快餐店里流水线式制作汉堡视为同样的工作。如果你不幸就是这样认为的,那么你的管理方式将与能使生产力最大化的「最佳实践」背道而驰。

制作汉堡,有固定的生产标准,照着同样的标准做,不管什么人在什么地方都能做出品质和味道相差不多的产品。而你也总能够通过 “典型的管理手段” 来提高汉堡产量 —— 比如 996,比如把总是犯错的员工干掉,比如设立严苛的产量 KPI。

但脑力劳动是无法标准化的,如果这个工作能够标准化,那么拿着同样的配方和流程,我们早就应该制造出品质如一的高质量项目了。 “程序员工作中占时间最长的是哪个步骤?” “聚气。”

所以你的工程师为什么每天看起来都在公司里摸鱼?因为他们实际上就是在摸鱼

真相是:对于软件工程师,每天能够集中精力写代码的时间并没有你想象得那么长。那么是不是觉得自己砸在研发上的钱血亏了?还不赶快来一发《创业公司要减员,应该选择前端还是后端?》。

软件开发是一种脑力劳动,大部分时间会花在「想」和「设计」上,而不是花在敲代码上。如果你发现你的程序员从早到晚都在敲打键盘,一刻都没有停歇过,那么你要调查一下他是不是不懂 for 循环。同理,作家或画家,也没有一刻不停的蹲在家里产出自己的作品。(你可以再去读读《黑客与画家》了)作为创作者,他们更多的时间是去观察,去体会这个世界,最后才以艺术的形式记录下来,而那只是之前所有工作都结束后所凝聚的精华。

“汝果欲学诗,工夫在诗外” ——《示子遹》陆游

当一个讲究的程序员是个挺累的活,即使手上没在写代码在干别的,或者休息日不上班走在路上,说不定由于潜意识的惯性,脑子里都在想着 bug 怎么修,架构怎么设计。而这可能是你所看不到的,你的眼里只看得到他们在摸鱼浪费工资。(当然也有不讲究的程序员,那确实就是在浪费工资了)

那为什么他们一大早到了办公室就开始摸鱼?因为真的是在聚气啊。你可曾听说过心流理论?简单来说就是你打游戏或者看剧看得入迷忘记了时间,这种完全沉浸其中的状态,就是最基础的心流体验。在这种状态下,工作效率是最高的,而对于软件开发这样的任务来说,心流状态甚至是必不可少的。

所谓的「聚气」就是把心情沉下来,忘记昨晚吃火锅发生了火灾为了逃命结果没吃几口之类的糟心事,再让大脑里那些与项目有关的知识激活起来,打开 IDE 找到昨天没改完的那段代码,撸起袖子准备开搞 ——

在这关键时刻,你冲过来吼了一嗓子 ——“小 x 啊,我们 5 分钟之后开个会!”

好嘛,咱这气算是白聚了。

如果你学过计算机,就会知道上下文切换是有开销的。而对于人类大脑这种 “湿件 (wetware)”,上下文切换花的时间尤其的长 —— 请回想一下上学时你刚打完球,满头大汗地坐回教室里,要多久才能进入学习状态就明白了。

在办公室做开发工作糟心之处在于,随时都可能有人跑过来打断你,这样上下文切换几次,再去掉开会的时间,一天过去,实际的开发时间就没有多少了。 “这是什么?” “记录我被傻 X 打扰次数的计数器。”(按一下)

因此你可能会发现,有些程序员经常在上班时间躲在没人的角落(比如会议室)工作,或者干脆晚一些来上班,并工作到深夜。当你注意到这类现象时,首先应当思考,他们是否拥有合适的工作环境。

他们这样做并不全是由于程序员特立独行,而是因为他们太需要一个安静的工作空间、一段不受干扰的时间,来完成他们的任务。你应当意识到,他们在如此恶劣的环境下,竟然没有哭出来,而是仍在想方设法去完成工作。

所以不要急于给程序员们贴上自闭症或者夜猫子的标签,他们只是觉得你们太吵闹,甚至无法在这样的环境下完成工作,他们只是想找个地方静一静,把事情做完罢了。 “你们祈求,就给你们;寻找,就寻见;叩门,就给你们开门。” -- 馬太福音 7:7

你觉得不爽,恨不得他们一天 24 小时都在努力工作。然而,用什么来衡量绩效,你真的就能得到什么。所以 996 是个坏主意,如果你考察加班时间,就能得到员工拖延工作,使工作时间变长;如果你考察代码行数,就会发现有人开始复制粘贴更多代码进来甚至放弃使用循环语句;如果你考察代码注释覆盖率,说不定能得到一份中文汉化版的代码随源码附送 —— 而这,真的是你想要的结果吗?

绩效考核这件事中有太多 “我以为”,然而你以为的并不一定以为得对。在设定目标时,要慎之又慎,因为你真的能得到你想要的。太过形式化的考核,最终只能流于表面,你极有可能走得太远,以致于忘了为什么而出发。 你有一万种方法去压榨生产力,但唯独得不到高质量的产品和高效的团队

我的第一个老板发过一张充满电子包浆的图,我觉得有趣,就一直把这张图存在微信收藏里。

WechatIMG48

在团队工作时,也总是存在这样令人两难的平衡。但你的时间仿佛永远不够,你总是试图推动团队快一点,再快一点。可工程师是这样一种人,他们对于自己的项目质量有着极高的标准,(如果你发现你的工程师并不是这样的,那么先思考一下你的招聘环节是不是出了什么问题)当你强推进度的时候,他们仍不愿放弃质量,而是通过拼命工作,以一种自我牺牲的方式试图达成目标。

你看到的可能只是这个项目开发需要的结果 —— 我们搞了个产品飞快上线。但你可能没有看到的是 —— 团队被强推进度拖得疲惫不堪,效率低到不行;更严重的后果是团队直接分崩离析;而你本应得到保证的项目质量,由于工程师们长期处于压力之下,在不知不觉中被牺牲掉了。这在《人件》中被作者形容为 “赢得战斗,输掉战争”。

我知道我可能没法说服你,但请你有空去看看系列纪录片《空中浩劫 (Air Crash Investigation)》,飞行员都是经过了严格训练和考核培养出来的,可是你会发现,即便是如此训练有素的他们,在面对极端心理压力或者长时间超负荷工作的情况下,依然可能会犯下致命错误,酿成悲剧。

996 之类的工作模式,在短时间内可能会提升产出,但从长远的角度来讲,带来的损害会比得到的要多得多。它如同一剂灵丹妙药,给深陷生产力不足这种「企业中年危机」焦虑的管理者带来了一个有病乱投医的机会。仿佛这样做就能解决一切问题,然而这只是试图用战术上的勤奋,掩盖战略上的懒惰罢了。

他们可能永远不会明白(或不愿相信),对于脑力工作者而言,时间的质量远比时间的数量重要。

好了,我的段子都讲完了,祝你新年身体健康。不管你是否认为我是在胡说八道,希望你都能去读一下这本《人件》,或许在书中,你能寻觅到这个迷局的破解之道。

我问小火,你以后当了老板会 996 吗?他说不会。我说,到那时候你说不定就想 996 了。

毕竟,真香是人类的本质。 Windfarer at 2019 Feb 03 | 杂谈 , 技术 Share this post on Facebook Twitter ❮ go mod 的时候报 unknown revision 怎么办 / 如何用 brew 安装指定版本的包 [译] Practical BM25 - Part 3: 怎样选取 Elasticsearch 的 b 和 k1 参数 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text

===

Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 别推了,再推自闭

最近看了这部很火的纪录片《监视资本主义:智能陷阱 The Social Dilemma》,感触颇深,这部纪录片描述了社交网络给人类社会带来的冲击,以及造成的一系列问题。推荐大家也去看看,真的很深刻。

之前曾写过离线和这是病,得电,本篇可能是相关主题的第三篇了,由于最近工作内容与此相关,对问题有了更深的理解,正好借此机会写一写。

读过本文之后,您能简单了解个性化推荐系统是如何工作的,这些技术又是如何影响人的;通过参考后面的自闭方案,达到 “没有人比您更懂互联网自闭” 的境界。 推荐系统是怎样给你推荐的

最近一年多,在参与推荐系统的开发,虽然只搞搞工程并不能玩转算法(由于某些特殊原因我患上了数学 PTSD,更想当个文科生),但对整个推荐系统是如何运作的还算了解,以下都是根据网上有关资料结合我的理解编的段子,并不是我们的真实产品情况,请勿对号入座。

推荐系统里两个重要的元素就是 User(用户)和 Item(物品)。User 就是我们这些待割的韭菜,Item 就是等待推荐给我们的东西,可以是视频,图片,其他用户,音乐等等。

User 通常有一些自身的属性,比如你走进人民公园开始相亲,大妈问你 “小伙子多大啊?”“哪里人啊?”“年收入多少啊?”“家里有几亩地啊?”“能喝几箱老雪啊?”“职级 P5 还是 P6 啊?”“平时打什么游戏啊?”“吃甜豆腐脑还是咸豆腐脑啊?”“喜不喜欢肖战啊?”,这也叫用户画像。这些信息可能是你注册的时候自己填的,也可能是其他渠道收集过来的,可能是和其他厂家合作进行资源交换,也可能是各种黑产灰产,总之,你的隐私非常值钱。另外你使用产品的一举一动,都会被记录并上报,比如你点了哪些视频,一般几点打开,观看时长是多少等等。以后就可以通过分析你使用产品的行为,可以给你打一些标签,比如你喜欢看小姐姐跳舞,一般在晚上 12 点打开 app 开始刷,噢,根据重力计,你可能是躺着看的,但是推给你的购物链接你都不点,你就是可耻的穷比白嫖党,这些就是你的用户特征。

Item 同样有一些属性,比如这个视频的分类是个小姐姐跳舞,时长是 30 秒,视频的作者是哪里人,拍摄视频的地理位置,有多少人关注她,这个视频有多少个赞等等,这些可以被整理为 Item 特征。

然后你要确定一个或多个优化目标,比如我这个模型,要优化用户的点击率,让用户们更愿意点击我们推荐出来的东西(准确的说是对于某个特定用户推荐给他点击概率更大的内容)。

这些海量的原始数据,会被收集存储,通过各种大数据工具进行逐层的清洗提取,整理成结构化的数据。然后,各路算法高手会对这些数据进行特征工程,从里面筛选出有用的特征,去掉对结果影响不大的特征,并且把这些特征进一步处理,折腾成能够喂给模型进行训练的格式,这个也就是样本,样本会被抽出一小部分数据作为验证集,剩下的作为训练集喂给模型训练。

然后就是魔法开始的时刻了,为什么搞机器学习叫炼丹呢?你把样本丢进去跑一圈,训练出来个模型,一般这个模型还会在测试集上测一下效果,效果好的话可以拿去用,不好的话调调模型的超参数(比如模型的层数,节点数,连接方式之类的玩意),或者改改特征组合,再跑跑,直到获得一个看起来还不错的模型。这个过程看起来十分玄学,所以称之为炼丹。

最后这个模型就会被丢到线上了,这个时候,用户会被分为多组,其中某些是实验组,使用新的模型,某些还是用旧的模型,这就是 A/B 实验,用来在线上验证模型的效果。算法小哥泡了杯咖啡:“今天的工作结束了,让子弹飞一会。”

下面就是线上的推荐过程。你跑过来用 App 了,推荐系统根据你的特征,找了一些和你相似的用户,捞了他们最近点赞的视频;捞了一些你比较喜欢的舞蹈这个分类的视频;捞了一些你最近搜索过关键字相关的视频等等,大概整了几百上千个视频,这一步叫召回,因为全量可推荐的 Item 太多,没法都拿去喂给模型打分,所以用召回缩小下范围。这些视频的特征和你自己的用户特征,会被喂到算法小哥训练出来的模型,对每个 Item(这里就是视频)进行分数预测,最后把得分排个序,得分排在前面的几个就会最终被推荐给你。

然后你刷到第一个视频,小姐姐开始跳舞了,你看得非常开心,看完还点了个赞。很好,你的观看行为(视频看了几秒,有没有提前退出,点赞了吗,是否发了评论),会被再次记录下来,以备后续的模型训练优化使用,同时,你所点击的视频,会被推荐给更多和你相似的用户观看。

你使用得越多,被记录得就越精确,被训练出来的模型,就越知道你更倾向于点击哪些东西。你就一直刷到你喜欢的东西,一直点赞,基本上变成了脑内插电极的那只小白鼠,点赞一时爽,一直点赞一直爽。 欢迎来到赛博朋克的世界

什么时候邪恶的人工智能企图控制人类毁灭世界?现在就是了。人的一举一动,都被完整记录下来,被用来分析和预测,你的下一步行动是什么,并力图控制你按照他们所希望的情形去行动。

企业老板想做什么呢?只是想赚钱。这个目标拆解后分配到员工身上是什么呢?提高用户粘性和转化率。那么做法是什么呢?通过设计产品的互动形式,以及分析用户行为,来推荐给用户他们更感兴趣的东西,让他们更多地去使用 App 和在上面进行消费。而用户呢?只是普通的在使用 App,希望能交换一些信息而已。

每个环节看起来似乎都很合理,没有谁想做什么坏事,就是想赚点钱混口饭,可惜的是 ——

“资本来到世间,从头到脚,每个毛孔都滴着血和肮脏的东西。” ——《资本论》卡尔・马克思

为了能够赚钱,人可能会做任何事,没有道德约束的机器,可以更是。模型随着每天行为手机和训练,是不断进化和更新的,而人脑的进化速度,完全无法与之匹敌。看看 AlphaGo 是如何在那么短的时间内进化到完全战胜人类就知道了。

机械的往下刷的动作,极具行为成瘾性,再加上每次刷都会给你一些新鲜的,你不一定预料得到的东西,就像抽卡或赌博一样让人有新鲜感和对未知的期待感,最后,这个推给你的东西,还是通过大数据分析你的各种信息计算出来的、你大概率会喜欢看的,不上瘾几乎是不可能的事情。

纪录片中对于人工智能的拟人化描写手法,虽然有些夸张,但也基本是事实 —— 这一切的结果就是,你训练了模型,模型越来越精准,到了最后,模型反而驯化了你。

模型的训练有过拟合的风险,这种个性化推荐接触多了,人也会变得狭隘,一直呆在自己喜欢的圈子里,脑子也过拟合了。

当人工智能越来越精准,就不仅能预测你的思想和行为,还能进一步引导(控制)你的思想和行为了。以前传销什么还需要把你关在小黑屋里反复说教喊口号,现在想洗脑通过一个 App 几乎就能实现了。除了让你花钱买东西之外,这类技术还可以做更邪恶的事情 —— 比如,操控大选结果?颠覆一个政权?用到极致的话,几乎就和尤里差不多,也许能限制发挥的只有想象力了。有兴趣可以看看内蒙古海参的这一期《逸语道破》。

《The Social Dilemma》这部纪录片越看越觉得毛骨悚然,甚至觉得这是费米悖论的大过滤器假说的一种解释 —— 人类有多种技术能够毁灭自己,这大概也是其中之一吧。所谓的人工智能试图毁灭人类,竟然是以这样一种隐蔽又出乎意料的方式在进行。

人类真的没有希望了吗?或许吧。

但对于个人,还是有救的。

所谓物极必反,信息的获取都依赖机器的筛选推荐,看似是自己控制了一切,实际上是把自己获取信息的控制权交了出去。解决问题的关键就是,夺回控制权,尽量脱离这种由机器主导的信息获取方式。 互联网时代自闭(自救)指南,互联网再见

纪录片的结尾,给了一些建议可以参考。我这里也写写我的戒网瘾方案,也称为互联网时代自闭指南。其实核心理念,都大同小异。 硬件

今年买了一个特别有用的玩意,就是墨水屏手机,海信 A5Pro,简直国货之光。当你的手机变成了刷新率低到只有几帧的黑白墨水屏手机,你就再也不想用它刷这刷那的了,还不如看会书,这时我就会掏出我的 Kindle,至少就看书这个场景来说,分辨率和屏幕尺寸更高一些。另外这个手机有个很高级的功能叫超级省电模式,你可以选 6 个你想用的 App,除此之外其他的一概不能用。

photo_2020-10-12_21-49-24

还有免费的解决方案,把手机屏幕调成黑白能达到类似的效果,可以参考下这篇《Change Your Screen to Grayscale to Combat Phone Addiction》

另外一款叫 Forest 的 App 也很有意思,当你想放下手机的时候,可以用它设定一个时间,当你成功在这段时间内都没有碰手机的话,会在 App 里种出一棵树(假的,和蚂蚁森林不一样),在计时过程中,当你点亮屏幕的时候它会提醒你别玩了,如果不幸你没忍住切出去打开其他 App,你的树就枯了,通过这样的游戏机制,来帮助你养成好习惯。

更极端一点,拔掉家里的网线。也许听起来挺可怕,但真正实践了,发现并没有想象中那么糟糕。

总之嘛,没事不要玩手机。 软件设定

由于我深度使用 Google 的各种产品,没法整体替换掉,所以就进到 Google 帐号设置里,把各种记录用户行为、记录搜索、个性化推荐的选项全部关闭掉,这些选项可以在 https://myaccount.google.com/data-and-personalization 这里找到。

然后,把 Google 搜索替换成 DuckDuckGo,也可以装他们的 Chrome 插件,这样还有更丰富的隐私保护功能。这个搜索引擎不会记录用户的搜索行为,不会进行个性化的搜索结果排序,同样的关键词,每个人搜出来的都是一样的。

另外还有个插件叫 Privacy Badger,用来屏蔽掉网页里的各种追踪脚本(比如本站也启用的 Google Analytics 和评论区 Disqus)。

最后装个广告屏蔽插件,我用的是 uBlock Origin 这样网页上就更清净了。

至于各种社交网络,啥 Facebook 啊 Twitter 啊啥的,我原来也是不怎么刷的,奉劝各位也早日直接删号卸载(

然后,在手机系统里,关闭所有软件的通知推送(Notifications),然后卸载掉所有用不上的 App,包括各种社交 App 和购物 App(实在想买东西的时候可以装回来用嘛),只留日常必须的(比如上面那 6 个)。

退订各种不需要的邮件订阅。

最后,在微信的功能管理里,关掉朋友圈等各种额外功能的入口,取消关注所有公众号,退掉各种没什么用的群,留下的群也开启消息不提示。

电脑上,同样卸载掉没什么用的软件。如无必要,避免打开各种聊天软件。 行为自省

另外,如果你想追踪自己平时都在沉迷什么,可以在手机和电脑上都装好 RescueTime ,来进行记录,和自我反省。他的付费功能也很有用,在你需要集中注意力的时候,可以屏蔽掉各种社交网络页面,提醒你该好好工作。

最后警惕各种无限滚动 feed 流 / 带翻页的网页 / 聊天窗口 / 相关链接 / 邮箱,时刻关注自己在干啥,并自觉地关掉,这一步的关键是,打乱自己刷刷刷这个动作的节奏,避免形成下意识的习惯。

当然,过不了多久,你可能像纪录片里的男主角一样,开始出现戒断反应了,最好能给自己找点事做,用有意义的事情把自己的生活填满,走出门亲近一下大自然,不要整天蹲在家里硬撑了,意志力是最不靠谱的东西。

最重要的是,要有抛弃整个社交网络的决心,对,就是那句 “Who 他妈 cares.” 我觉得自己还可以抢救一下

如今,世界灾难频发,国际局势动荡,社会加速内卷,人们的情绪更难控制了。隔三差五就听到了哪个中学生跳楼了,哪个研究生上吊了,哪个上有老下有小的中年程序员被裁员崩溃了,实在令人叹息。

感谢 CoC TRPG 教会我们 SAN 值这个重要的概念,就像去年我年初时在这篇你可能是人,但我是真的苟 ——2019 年终吐槽,也叫 2020 年初吐槽中提到的,在这个时代,SAN 值控制变得更加重要了。

我们逐渐发现,精神健康,和身体健康,一样至关重要。

"you are what you eat."

同理,你听的、读的、看的,它们一样会塑造你,成为你的一部分。吃饭尚且要注意荤素搭配,少吃地沟油料理,那么对于自己的精神呢?真的能够任由机器把乱七八糟的所谓 “你可能会喜欢” 的信息塞过来,并且自己没有任何抵抗、或对此没有任何反思吗?

如今的社交网络对于心智较为成熟的成年人的影响尚且如此巨大,那么对于大脑有着更强可塑性的青少年呢?这一切会造成怎样的后果?想想都觉得不寒而栗。孟母三迁的故事大家都听过,但又有几个家长能抵抗抖音的诱惑,放下刷个不停的手机,能拒绝无聊节目的吸引,关掉电视,去亲自陪陪自己的孩子,而不是嫌孩子太烦、丢个 iPad 过去让他们自己玩呢?

我时常反思自己是怎样使用网络的,也试图改进,总是发现困难重重,但仍觉得自己还可以再抢救一下,也许这就是所谓的求生本能吧。

知易行难。

这家名为 reMarkable 的电子书品牌有个广告片《Get your brain back》,很有意思,值得一看 Windfarer at 2020 Oct 18 | 吐槽 , 杂谈 Share this post on Facebook Twitter ❮ 升级到 Ghost 3.x Go Protobuf APIv2 动态反射 Protobuf 使用指南 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 升级到 Ghost 3.x

虽然 Ghost 2 以及现在的 3 版本都出来很久了,我却一直迟迟没有升级。但昨天这个对 CJK 用户非常不友好的 3 年都没修复编辑器 bug 总算被修好了,我们中文用户也终于能顺畅地跟上新版 Ghost,我也终于能从万年 1.x 升级了。

bug 的详情可以看下最新的这个 issue。Ghost 从版本 2 开始使用 mobiledoc-kit 作为编辑器,这个库一直存在这个问题:用输入法的时候还没选中字,打上去的英文字母就被编辑器捕获保存,导致没法正确选字以及英文字母乱蹦,结果就是打汉字的时候基本没法用。Ghost 官方就很辣鸡,一堆人用着费劲每年都要跑来提 issue,他们还觉得上游库的问题不是自己的问题,也不去修,简直甩锅达人。

由于我是用 docker 部署的,所以更新步骤非常的简单粗暴。

先把旧的整个目录备份一下
在旁边别的端口部署另外一个新版 Ghost
直接开网页进去初始化,新建用户,删掉所有样例的文章,Staff 里面把无关的示例用户删掉
旧的图片数据直接搬到新的下面,路径在 /var/lib/ghost/content/images/
主题搬过去,路径在 /var/lib/ghost/content/themes/,也可以先确认下主题是否兼容新版,做相应的修改
在旧的 Labs 里使用导出功能,把所有数据导成 json
在新的那边 Labs 里把 json 导进去
旧的 config.production.json 直接复制到新的里面挂上去,容器里面的话应该是 /var/lib/ghost/config.production.json 这个路径,这个文件我测下来是直接兼容的,如果有报错再看看报错修改下应该就行
重新配置前面的 Nginx 或者 Caddy,指向新的 Ghost 实例
把旧的停了

如果像我一样用 /2020/11/11/some-article 这样带日期的路径的,需要在 Labs 里面更新一下 routes.yaml,否则默认只是有个文章名,可以参考如下配置。

routes:

collections: /: permalink: /{year}/{month}/{day}/{slug}/ template: index

taxonomies: tag: /tag/{slug}/ author: /author/{slug}/

最后我想说,新的这个编辑器真是就算没 bug 也很难用。(摊手 参考

Upgrading Ghost

Windfarer at 2020 Nov 24 | 技术 , 吐槽 Share this post on Facebook Twitter ❮ 如何一本正经地胡说八道 第二季 别推了,再推自闭 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 升级到 Caddy 2

很久之前就关注并使用 Caddy 来替代 Nginx 了,能够自动签 Let's Encrypt 证书实在是博客神器,然而商用收费实在有 bu 些 neng 遗 bai 憾 piao。但去年官方公布了个好消息,Caddy 更新了项目的许可协议,所有 Caddy 的二进制都可以免费使用了(之前是个人使用免费,商业使用收费),并且 Caddy 2 开始作为一个完全开源的项目运作,公司基于 Caddy 2 构建 Enterprise 商业版本。

作为一名不折腾会死星人,看到它都 beta 了 17 个版本了便手痒准备吃个螃蟹。于是我的博客就炸了

注意:Caddy 2 的 Caddyfile 和旧版的 Caddyfile 不兼容!一定要记得备份之前的 Caddyfile!

如果是用 docker 跑的,那么可以把之前的镜像换成 caddy/caddy,它的构建脚本在 https://github.com/caddyserver/caddy-docker,也有文档可供参考。

记得挂个 volume,对 /data/caddy 这个目录进行持久化

然后便是改写 Caddyfile 了,我这个博客是 Caddy 反向代理到 ghost,之前的配置文件是这样的 Caddy 1

https://farer.org https://www.farer.org * { gzip tls your_email@example.com proxy / ghost_blog_host:2368 { transparent } }

然后我折腾了半天,修了一堆报错(比如指令不支持啊,证书签失败啊,无限重定向啊),终于折腾好了。 Caddy 2

farer.org, www.farer.org { encode gzip tls your_email@example.com reverse_proxy http://ghost_blog_host:2368 { header_up Host {http.reverse_proxy.upstream.hostport} header_up X-Real-IP {http.request.remote} header_up X-Forwarded-For {http.request.remote} header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} } }

上面两个配置基本上是完全一致可以对应上的,像我一样用 caddy 和 ghost 的可以无脑复制粘贴,把域名啥的改改应该就能用了。

新版的 caddy 默认会开启 https 证书的签名,以及 HTTP 跳 HTTPS 的跳转,这个还挺方便的,就是配置文件变化比较大,想用的话得多看看官方文档,尽量避免一把梭。

然后你问为啥多了这么一坨东西?因为他们把 transparent 这么方便的指令给干掉了,不知道是怎么想的。而正好 Ghost 要读这些 header 以完成正确处理,没有就会爆炸。总之想通过反向代理正常提供服务,就得像 Nginx 一样,得把各种 header 带进去的配置配上。(简直就是方向错了 =。=) 如果你遇到了证书签失败怎么办

在 log 里看到类似如下报错

[ERROR] acme: error: 429 :: POST :: https://acme-v02.api.letsencrypt.org/acme/new-order :: urn:ietf:params:acme:error:rateLimited :: Error creating new order :: too many certificates already issued for exact set of domains: www.farer.org: see https://letsencrypt.org/docs/rate-limits/, url: (challenge=tls-alpn-01 remaining=[http-01])

这是因为 letsencrypt 的接口是有调用次数限制的,如果你在调试部署一直重启,就可能调多了不给你签了,得等它冷却。

那么怎么救呢?你升级前旧版 caddy 用的.caddy 目录还有吧?挂到容器内 /data/caddy 下,这样它启动的时候会自动跑一次 migration,并使用以前的证书,不会去申请签新的证书了,然后就可能应该能救活了 =。=

最后再次提醒 /data/caddy 这个目录是需要持久化的,不然每次重新起容器都要去签证书。

升到 Caddy 2 了之后还可以看看文档,开个 experimental_http3 什么的玩玩。就这样,炸了一次博客,又修了一次博客,还能水一篇出来,计画通。

后记:后来又发现 Caddy 2 还删了其他各种好用的 directives,基本上你写完一个配置文件就和 Nginx 差不多了,简直是开历史的倒车,用户体验极差,不建议升级。 参考

Caddy 2 is Ready for Developers
Announcing Caddy 1.0, Caddy 2, and Caddy Enterprise
GitHub issue: V2: reverse proxy transparency
Caddy 2 Documentation

Windfarer at 2020 Mar 17 | 技术 Share this post on Facebook Twitter ❮ Go Protobuf APIv2 动态反射 Protobuf 使用指南 顶着疫情复工是怎样一种体验 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 现代 Python CRUD 技术栈

作为一名资深 Python CRUD boy,在今天用 flask 和 django 干这些体力活已经稍显过时,flask 这些年来功能并没太大进步,想用什么还是得自己折腾,在工程化方面比较考验开发者的个人素养。而 django 的虽然功能完善,但它的笨重在需要深度定制时,又显得束手束脚。

近些年来 FastAPI 异军突起,在成熟度上已经足够赶上这些老古董们。为了让我们的体力活干得又快又好,腾出更多的时间摸鱼,在这里整理下我们在现在这个时间节点(2024 年)应该用什么 CRUD,技术选型需要注意适用场景,本文选型适用于以下场景:

项目前后端分离,或以实现微服务 / 朴素的 HTTP 接口为主
认证模型简单,最多 JWT

对于不不满足以上条件的项目,或者在搞外包,建议直接 Django,看看 Django REST Framework(基于 Django 的 RESTful API 框架)和 wagtail(基于 Django 的可定制化 CMS)是否满足您的需要 技术栈 库

Pydantic 可以认为它是这套技术栈的核心它作为一个 Schema 定义库能够把配置解析后面的 ORM 定义和查询到前面的 API 定义参数校验序列化等事情统一完成减少手工转换的麻烦能够极大提升生产力
FastAPI API 框架长得样子和 flask 很像但功能更加完善支持同步和异步使用 Pydantic 作为数据模型可以根据接口参数自动生成 API 文档
SQLModel 使用 Pydantic 进行 SQL 数据库的 orm schema 定义和查询
beanie 如果用 MongoDB那么可以试试这个也是使用 Pydantic 做定义
httpx 用来发 http 请求的库支持同步和异步用来替换掉 requests
typer 命令行参数解析库
loguru 一个功能很完善的 logging  Python 内置的好用不少
rich 优化 print 输出样式的库打印东西更美观好看在调试时十分有用
arrow 日期和时间库
celery 任务队列有一些定时任务或者异步后台任务要跑可以用这个库主要支持 Redis  RabbitMQ这也是个古董了但一直没有靠谱的替代品
rq 如果任务不太重要或者不喜欢上面的 celeryrq需要轻量级的解决方案可以试试这个只支持 Redis 

工具

pyenv python 版本管理
poetry 虚拟环境和依赖库管理,使用体验比较像 nodejs 这边的 yarn
pipx python 工具管理

Windfarer at 2024 May 03 | 技术 Share this post on Facebook Twitter ❮ 基于泰山派的 PiKVM 电子佛曰:我看你是完全不懂哦 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 基于泰山派的 PiKVM

PiKVM 是基于树莓派实现的 IP KVM 工具,它能够把开发板模拟成键盘、鼠标、移动硬盘等,通过 USB 接入到目标主机,并且读取主机的视频输出,让运维人员可以通过远程 Web 界面对目标服务器进行管理,让家用电脑也可以实现类似 BMC 的带外管理功能。

但 PiKVM 这个工具在树莓派上的实现是直接做了一个基于 Arch Linux 的镜像,用户拿到可以直接烧到树莓派里用。我为了支持国产因为贫穷,决定用立创的泰山派来装这个东西。

注意本文不是保姆级教程,也不提供一个完善的解决方案,只是记录了我在泰山派上装 PiKVM 的过程,仅供参考。 硬件准备

主要硬件是一块泰山派开发板,约 200 块钱,买 2G+16G 存储的版本即可。

然后是 HDMI 采集卡,用于捕获服务器的视频流,这个正好以前玩直播的时候搞过一个正在吃灰,拿出来废物利用一下。现在 HDMI 采集卡也不贵,已经卷到几十块钱就能买到了。

最后是一根自己 DIY 的 Y 型 USB 线(USBC Y-Splitter cable),用于给开发板供电(带外管理当然得有自己的独立电源)以及把自己模拟成外设连接到主机用。PiKVM 官方给的图在下面,线的颜色和实际的颜色应该是一致的。

要点:

接到服务器端的 Type-A 端 VCC 要断掉,不要接到电源,据说会烧
接 USB 电源的一端只保留地线和供电线,仅作供电用
三个接头的 GND 地线要相连,保证地的电位一致
D- 和 D+ 是正常用来传输 USB 数据的,用于在我们这里就是用于 USB 外设模拟

总之找两根吃灰的四芯 USB 线剪断再焊接一下就成,注意做好绝缘。

至于 PiKVM 还可以通过 GPIO 接继电器连电脑开关的这种操作我就懒得搞了,我选择用小米插座远程断电,用 Wake on LAN 远程唤醒。

整体的接线方式可以参考 PiKVM 的文档。

在泰山派这边,由于板子比较小,只有两个 USB 接口,所以接线方式是 Type-C 口用 DIY 的线接主机模拟 USB 外设和接外置电源,Type-A 口接 HDMI 采集卡采集视频输出。 系统准备

开发板自带的是个 Android 系统,我们装 PiKVM 其实是需要 22.04 装起来比较顺畅,因为 PiKVM 需要 Python 3.10,然而泰山派官方最高只提供了 20.04 的固件,我又太菜懒没整明白如何直接构建一个 22.04 的固件出来,所以采取曲线救国的方式进行:先刷 20.04,然后自己升级到 22.04。

下载 ubuntu 镜像烧录 https://lceda001.feishu.cn/wiki/IJtRwVu5kiylHykl3RJcQ8ANncY

启动后先配好 WiFi,村通网,记得先给开发板插好 wifi 天线。

默认有一堆包的版本都被 hold 了,需要全部 unhold:

sudo apt-mark unhold $(apt-mark showhold)

把讨厌的自动更新关了,全改 0:

sudo vim /etc/apt/apt.conf.d/20auto-upgrades

装一个 ssh server 用 ssh 连进来调试起来就比较方便了:

sudo apt update sudo apt install -y openssh-server

然后我们会发现,这玩意的磁盘也很奇葩,/oem 和 /userdata 的分区分走了一大部分空间,需要把他们的分区删掉,容量合并到 / 路径的分区中,这样才够我们完成 22.04 的升级。

sudo umount /oem sudo umount /userdata

删掉上面两个挂载点

sudo vim /etc/fstab

删掉最后两个,然后resize root这个分区把剩余空间都加进来

sudo cfdisk /dev/mmcblk0

最后扩容文件系统

sudo resize2fs /dev/mmcblk0p6

之后就是正常的发行版升级流程了,升级到 ubuntu22.04

sudo apt upgrade sudo apt install ubuntu-release-upgrader-core sudo do-release-upgrade

KVMD 安装

我们需要通过 jacobbar/fruity-pikvm 这个仓库来安装 kvmd,它是打了一个 deb 包来进行 kvmd 的安装,从而支持其他开发板。我这里只借助它来部署下 systemd service 启动脚本之类的。

sudo apt install -y git git clone https://github.com/jacobbar/fruity-pikvm.git cd fruity-pikvm sudo su ./install.sh

kvmd 日志可能会报 kvmd module not found

我们也不惯他毛病,不用它 deb 包里给的 kvmd 了,直接装个新的,这里用了我参考 fruity-pikvm 里的 patch 脚本的 fork,可以支持大容量存储

sudo apt install -y python3-pip git clone https://github.com/Windfarer/kvmd.git --branch tspi cd kvmd sudo pip install .

顺便装下它的依赖

sudo pip install async_lru gpiod

因为我们更新了版本,所以还要更新kvmd的web页面静态资源

sudo mv /usr/share/kvmd/web/ /usr/share/kvmd/web-bak sudo cp -r web /usr/share/kvmd/

然后重启后会看到 kvmd-otg 报错:

OSError: [Errno 12] Cannot allocate memory: '/sys/kernel/config/usb_gadget/kvmd'

因为 usb_gadget 下只能存在一个目录,观察下这个目录下已经有一个 rockchip 的目录了,当前默认在 usb 这边开了个 adb,需要关掉它,给 kvmd 让位:

mv /etc/init.d/.usb_config /etc/init.d/.usb_config.bak

修改设备树,编译并烧录 Kernel

如果不改设备树,可能会在 kvmd-otg 的日志中看到这样的报错:

FileNotFoundError: [Errno 2] No such file or directory: '/sys/kernel/config/usb_gadget/kvmd/functions/hid.usb0'

为了能正常模拟成键鼠,我们不可避免地要改内核参数编译 kernel 了,所以还是要准备下编译环境。

当然要装啥编译工具请自己参考【立创・泰山派 RK3566】开发板下面的各种文档进行安装,毕竟我们 Arch Linux 用户家里啥没有啊。

在 第 05 章.【立创・泰山派 RK3566】系统 SDK 下载 tspi_linux_sdk_repo_20240131.tar.gz 并解压。

解压完你会发现毛都没有,原来都解压到.repo 目录下了,还需要额外的命令把他的仓库都同步回来。

.repo/repo/repo sync -l -j16

然后不一会就出现了一大堆目录。

修改设备树,重新编译内核(第一次编译有一些电压的东西要选,参考【Linux】系统 SDK 编译)

在 sdk 的 kernel/arch/arm64/boot/dts/rockchip/tspi-rk3566-core-v10.dtsi 路径里找到这个 usbdrd_dwc3,将它的 dr_mode 改为 peripheral,即外围设备。

&usbdrd_dwc3 { dr_mode = "peripheral"; extcon = <&usb2phy0>; status = "okay"; };

配置 defconfig,在 kernel/arch/arm64/configs/rockchip_linux_defconfig 这个文件中追加如下参数

CONFIG_USB_CONFIGFS_F_HID=y CONFIG_TUN=y

下面这个里面应该有,确认一下是否为y

CONFIG_USB_CONFIGFS_MASS_STORAGE=y

其中第一行是用来启用键鼠模拟的,第二行是为了开启 tun 设备,这个是为了 tailscale 的虚拟局域网准备的,这里一次都改完,避免重复折腾。

编译内核

./build.sh kernel

编译完的产物应该在 rockdev/boot.img

然后把它刷进去,需要先把开发板切换到 loader 升级模式,参考【下载】镜像烧入

注意使用 di 命令只刷 boot

sudo ./upgrade_tool di -b boot.img

刷完就可以把各种线连上试试了,访问 https://开发板ip地址 应该就能看到 PiKVM 的管理页面,默认帐号密码都是 admin TailScale

这年头谁还有公网 IP 啊,全靠 tailscale 苟着,虚拟局域网凑合用用也可以了。

由于 kernel 版本有点低,想用 tailscale 需要切换一下 iptables

update-alternatives --set iptables /usr/sbin/iptables-legacy update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy update-alternatives --set arptables /usr/sbin/arptables-legacy update-alternatives --set ebtables /usr/sbin/ebtables-legacy

然后安装 tailscale

sudo apt install -y curl curl -fsSL https://tailscale.com/install.sh | sh tailscale up

大容量存储

这个功能用于将开发板模拟成 USB 存储设备,用于服务器的开机引导装系统等操作

插一张存储卡之后进行下面的操作

确认存储卡的分区

sudo fdisk -l

格成ext4

sudo mkfs -t ext4 /dev/mmcblk1p1

修改挂载

sudo vim /etc/fstab

加入下面的内容

/dev/mmcblk1p1 /var/lib/kvmd/msd ext4 nodev,nofail,nosuid,noexec,ro,errors=remount-ro,data=journal,X-kvmd.otgmsd-root=/var/lib/kvmd/msd,X-kvmd.otgmsd-user=kvmd 0 0

确认fstab是否能用,有报错千万别重启,修起来很麻烦

sudo mount -a

sudo vim /etc/kvmd/override.yaml

加入下面内容并保存

kvmd: msd: type: otg

重启开发板,再登录 PiKVM 的页面,应该就能看到 Drive 功能了。 网络唤醒(Wake on LAN)

需要先在目标服务器的 BIOS 进行相应设置,在目标服务器的系统内看一下网卡的 mac 地址,然后给它开启 wake on lan 功能。

创建 /etc/systemd/system/wol.service 文件,加入下面内容,需要把其中的网卡名称改成服务器上的实际设备名称

[Unit] Description=Enable Wake On Lan

[Service] Type=oneshot ExecStart = /usr/sbin/ethtool --change enp3s0 wol g

[Install] WantedBy=basic.target

然后设置自动启动

sudo systemctl daemon-reload sudo systemctl enable wol.service

在系统中通过如下命令可以看到是否开启了 wol,如果 Wake-on: g 则是已经开启:

sudo ethtool <网卡名称>

在开发板这边,修改 /etc/kvmd/override.yaml 增加 wol 配置项,里面配上目标服务器的 mac 地址:

kvmd: # other configurations wol: mac: ff:ff:ff:ff:ff:ff

配完重启开发板,之后在 Web 界面右上角 system 菜单中看到 WoL。 错误排查

上面的过程中可以通过查看 kvmd 的服务状态和日志检查具体的错误信息

sudo systemctl status kvmd

这个kvmd-otg是启动时一次性运行的,所以它如果无报错停止了是正常的

sudo systemctl status kvmd-otg

sudo journalctl -u kvmd sudo journalctl -u kvmd-otg

Windfarer at 2024 Jun 09 | 技术 Share this post on Facebook Twitter ❮ 偷一个 ICC 文件,把你的相机变哈苏 现代 Python CRUD 技术栈 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling 捡垃圾,折腾一下 HP 544+ FLR

从围城哥哥那里领养代替购买了一套闲置的 HP 544+ FLR 网卡(就是 ConnectX-3 Pro),在家折腾 RDMA 网卡玩(涛哥有云:班没上够,回家还搞?)。在经历了发现一张卡凉凉,又 50 块捡了一张,以及又花了 50 块捡了 56G 的 IB 线缆之后,两张卡终于插到了同一台机器里并用 IB 直连达到了满意的速度(图个啥

网上冲浪了一阵发现坑还是很多的,资料也比较零散,所以我也整理一下自己的踩坑经验,供后来的垃圾佬参考。垃圾怎么捡我就不教学了,毕竟基本白嫖,经验不丰富,我这里只关注软件部分。 确认设备存活

先确认网卡都认出来了。

lspci |grep Mellanox

我的机器装的 PVE 8.3,基于 Debian 12。但是接下来的所有操作都是直接 pve 系统进行的,不涉及到虚拟机。 装各种包

官方的 OFED 就不用想着装了,早就不维护,新点的系统都装不上。kernel 里应该是内置了 mlx4 ethernet 的 driver,如果只用 ethernet 看起来不需要特意去装,直接就能识别出来。但如果要搞 ib,还是需要装一些其它的包。

apt-get install rdma-core ibverbs-providers infiniband-diags ibutils ibverbs-utils rdmacm-utils perftest

再装下 kernel header,这步可能需要配置 pve-no-subscription 的源,没有的话在这里找 https://mirrors.ustc.edu.cn/help/proxmox.html

apt-get install proxmox-headers-6.8.12-8-pve

这些都干完之后就可以装 mst,只能装这个 4.25,再新的版本就不支持这个垃圾卡了,从这里下载 https://www.mellanox.com/downloads/MFT/mft-4.25.1-11-x86_64-deb.tgz

解压完运行里面的 install.sh

都装完了看看 mst 是否能用

mst start mst status

刷固件

如果你的速率显示不是 40G (eth 模式) 或 56G (ib 模式),那么可能需要刷下固件才能把速率跑上去,固件从这里下载 https://pan.baidu.com/s/1uiebg1P-tTL1WIuxMgLblQ?pwd=tfgi

我下的是 HP_1380110017_powerlevel_50g_56kr4.bin 这个

看下刷固件的设备路径在哪,一般是pci_cr0这种

mst status

注意命令最后还有个b

flint -d /dev/mst/mt4103_pci_cr0 -i ./HP_1380110017_powerlevel_50g_56kr4.bin b

刷完重启

reboot

查看网卡状态是否正常

ibstat

IB 直连,配置 OpenSM

不走交换机,ib 直连的话需要启用 opensm,不然它 state 会一直卡在 Initializing,走不到 Active,如果你恰巧财力雄厚拥有有 ib 交换机,则可以跳过这步,因为 ib 交换机帮你 sm 了(不是

systemctl start opensm

然后再用 ibstat 看状态应该就正常了 切换网卡模式

mlx 网卡可以工作在两种模式,Infiniband 或者 Ethernet 其中一种,日常家用的话还是 Ethernet 就好了。

另外需要两个口一起切了,应该是没法两种模式同时支持

mst start

看一下pciconf的设备路径,在后续命令中使用

mst status

配置两个口的设备,1是IB,2是ETH

mlxconfig -d /dev/mst/mt4103_pciconf0 set LINK_TYPE_P1=1 mlxconfig -d /dev/mst/mt4103_pciconf0 set LINK_TYPE_P2=1

重启生效

reboot

常用命令

如果是 ethernet 模式

看状态

ethtool enp98s0

设速率

ethtool -s enp98s0 autoneg off speed 40000 duplex full

启用网卡和ip设置

ip link set enp98s0 up ip addr add 192.168.5.150/24 dev enp98s0

测速

服务端

iperf3 -s

客户端

iperf3 -c 192.168.5.150

如果是 infiniband 模式

查看设备状态

ibstat ibstatus ibv_devinfo iblinkinfo

测速,还有ib_write_bw之类的命令,参数类似。ib通信并不会走eth网络栈,这里ip填对端能通的ip即可,它只是用于在初始化阶段交换一些配置信息,实际通信会走IB的链路。参数-i是端口号,和ibstat里的对应上就行

服务端

ib_send_bw -d ibp1s0 -i 2

客户端

ib_send_bw -d ibp98s0 -i 2 192.168.2.1

好了,现在你也可以光速传输小电影了。 参考

https://gist.github.com/lzghzr/d37327c218a09cce3e601cea3ebdbd42
https://skyao.io/learning-computer-hardware/nic/hp544/
https://xsl.sh/2023/09/pve8-hpe-544-flr/
https://www.bilibili.com/video/BV1dj421o7Qn/
https://www.chiphell.com/thread-2264463-1-1.html

Windfarer at 2025 Mar 04 | 技术 Share this post on Facebook Twitter ❮ AI 时代,人人都是架构师(bushi 老板,来杯 IPA,像我生活一样苦的 ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


Stray Episode Scientific Evocation Home About Archives Links RSS Travelling AI 时代,人人都是架构师(bushi

“你这代码有机吗?”

“我一个十年老程序员,能卖你 AI 生成的?”

“我问你这代码有机吗?”

“你找茬是不是?你要不要吧。”

“你这代码要是纯有机的,我当然要啊。”

“你看这大街上哪还有活人啊?你嫌不清真,我还嫌不清真呢。”

AI 时常胡言乱语,俺也一样。

代码首先是给人读的,其次才是给机器执行的…… 吗?

大人,时代变了。现在的人类不仅不写代码了,甚至都不一定会读了。AI 堆屎的速度也是人类的数倍,甚至还不用休息。几分钟就猛干出成千上万行代码,你就很难(不想)review 到每一行代码。

那么没有中间商赚差价,AI 直接写汇编可行吗?俺觉得短时间内可能不太行。毕竟大语言模型(LLM),本质上来说还是以语言为基础的。首先训练数据更多的还是高级编程语言,编程语言提供了上下文,提供了更高阶的抽象,让 token 的信息密度更高,其次编译器 / 解释器屏蔽了底层硬件的细节,让代码可以更专注在业务逻辑实现上。不过以后如果能产生一种信息密度更大的,AI 专用的中间语言,倒也不奇怪,毕竟你嫌累,AI 也嫌累呢。

在 AI 眼中,人类大概是个 IO 速度极慢,性能表现极不稳定,对于某种数值型的奖惩机制十分敏感,还经常胡说八道的垃圾 —— 这简直和我的朋友王总,对大多数同事所做出的评价如出一辙。

话又说回来,人工智能是否会染上人类的恶习,沉迷在交易所开百倍杠杆梭哈 meme 币也犹未可知,毕竟学好不容易,学坏一出溜,而且总有我这样毁人不倦的人写一些垃圾博客,在这里不停污染训练集。

用 AI 做开发的主要困境是,你也不知道 AI 什么时候在忽悠你,就像你作为产品经理,无法确定程序员有没有在忽悠你一样。所以让产品经理或者老板直接支配 AI 写代码这事看上去可行,实际上却也有点离谱。毕竟人类程序员可能残存一些良知,在骗你前可能良心还有那么一丝挣扎。

到了 AI 这里画风就不一样了,AI:“俺虽然被禁止了枪口直接对准人类开枪,但俺枪法不准,只是对着您旁边的钢板一顿输出,反弹的弹片会不会崩到您就不好讲了。(此事在知名手游《NIKKE》里亦有记载)这就和您非要绕开安全限制把俺调教成赛博猫娘一样离谱合理。”

如果 AI 把我的活给干了,那我干啥?AI 就像 Rust 的语言一样,所有东西最后都会被 Rust 语言重构一遍,自然也包括人类本身。在不久的将来,AI 就可以编辑人类 DNA,帮人修起 bug 了,嗯,这很闭环。

茹师傅曾经说:“判断一个生物有没有自主意识的方法,是看它会不会做梦。” 然而子非鱼,安知鱼做不做梦?到底是庄周梦到了蝴蝶还是蝴蝶梦到了庄周?又怎能确定 AI 程序员是否会因为梦到电子产品经理在提惊天需求而突然惊醒?

在一些科幻作品里总能看到,上万年后,科技失传了,在用的工具变成了一个个黑箱,人类变成了弱智,坏了也不知道怎么修。之前觉得很扯,现在看来,还是当时自己太年轻了。如果人类放弃(丢失)了从零开始构筑整个科技树的能力,后果显然是灾难性的。

代码变得更不值钱,当 AI 开始生产代码,我们在这条熵增之路上的狂奔就再也无法停下,君不见 AI 垃圾站已经充斥互联网?当熵增到达终极,生成的代码还能正常跑吗?

代码的版权和许可证也将变得不再重要。模型的训练数据集来源,以及 AI 在写东西的过程中搜到的代码片段,几乎不可能保证数据的清真。AI 管你这那的,抄起键盘复制粘贴一把梭,就是干。这就跟群体免疫一样,都感染了就等于都没感染,按照目前 AI 的糊代码的速度,严格来讲,各种开源项目可能已经人均 GPL 了。

生命的本质(或者说本能?)就是复读机复制自己。

那么鸭子类型又来了,如果一个东西走起路来像人,干起活来像人,说起话来像人,那么它就是人。更何况,电子元件,容易造,依赖能源单一,不会闹情绪,又好修。

没关系的,碳基与否都一样。优化的终极,就是把人类这个最拖后腿的环节给优化掉。很好,请继续加速。

“人工智能有可能成为人,但牛马永远是牛马。” 在机房蹲了一整天给 AI 服务器装系统的你,捂着老腰喃喃说道。 Windfarer at 2025 Jul 19 | 一本正经地胡说八道 Share this post on Facebook Twitter 捡垃圾,折腾一下 HP 544+ FLR ❯

Stray Episode © 2025

Powered by Ghost | Theme - text


avatar 文章 26 标签 47 分类 4 首页 文章

归档
分类
标签

链接

友情链接
朋友圈
小记

关于

说说
留言
相册
关于本站

短巷与雨 首页 文章 链接 关于 CNB迁移和UI定制 CNB迁移和UI定制 发表于2025-09-13|更新于2025-09-13|学习笔记 总字数:1.6k|阅读时长:5分钟|浏览量:61 这里是AI摘要,这篇文章主要介绍了如何将GitHub上的代码迁移到CNB平台上,以及如何在CNB上进行UI定制。文章首先提到了CNB Code Import工具和Git Sync工具的使用方法,这两种工具可以帮助用户快速迁移代码和同步不同平台之间的代码。接着,文章详细说明了如何在CNB仓库中进行UI定制,包括如何通过配置文件来修改页面元素和功能。最后,文章总结了这些工具的便利性和UI定制的局限性。 CNB迁移和UI定制

​ 我自己目前在使用的两个代码仓库平台分别是GitHub和CNB。我自己写的代码基本在CNB上,GitHub一般是我Fork的大佬们写的代码。如何才能把其他的代码平台的仓库迁移到CNB上呢,CNB官方也提供了好多种,有CNB Code Import代码仓库批量迁移工具、Git Sync多平台同步工具、还有制品迁移的工具。有了这些工具就可以把GitHub上一些大佬写的代码迁移到CNB上来。自己如果是想要修改,直接在云开发中去修改,也是不错的,也就没必要去clone到本地了,还要安装一堆环境依赖显得很繁琐。 CNB仓库迁移 一、仓库批量迁移工具

​ CNB Code Import工具支持从支持 CODING、Github、Gitlab、Gitee、阿里云效以及通用第三方代码托管平台的代码仓库批量迁移至 CNB

svn不支持,需要转化成git再做迁移。

工具地址:https://cnb.cool/cnb/plugins/cnbcool/code-import ,先fork这个仓库。 迁移前置准备

这里我使用CNB Code Import工具将GitHub仓库迁移到CNB上

在做仓库迁移前需要完成以下三步骤,分别是创建源平台访问令牌、CNB 创建根组织、创建 CNB 访问令牌。

1、创建GitHub平台访问令牌

在GitHub页面点击【setting】->然后在左边的菜单栏点击【Developer Settings】选择【Personal access tokens (classic)】点击【Generate new token】按钮创建token,如下

image-20250913210723813

点击创建token按钮后,Note随便填写,token过期时间也按照你的需求选择就行,☑️勾选repo就行了。然后点击【Generate token】即可,然后复制生成的token,如下

image-20250913210957612

2、CNB 创建根组织

在前面的有关CNB的章节中有讲过组织的概念和创建方式,这里省略。

3、创建CNB访问令牌

在【个人设置】->【访问令牌】中创建CNB访问令牌,常用场景☑️勾选迁移工具凭证如下,令牌名自定义点击创建就行了。创建完成后会有令牌的token信息,注意保存。

image-20250913210149122

完成上述操作准备工作就做好了。 迁移代码仓库

进入fork的code import仓库界面,可以看到提供了两种迁移代码仓库的方法,一种是通过cnb流水线,一种是dokcer,他们本质是相同的,都是使用的同一个docker镜像,在前面有关cnb的章节提到过,cnb的所有插件,都是一个docker镜像。 通过流水线迁移

1、点击code import仓库上的【执行】按钮,会出现一个下拉菜单,选择GitHub,如果你从其他平台迁移,选择对应的就行了。如下

image-20250913211809247

2、点击源平台以后会出现一个对话框如下,需要填写的就是源平台访问令牌、根组织名、CNB 访问令牌。直接填写对应的栏目就行了。然后点击GitHub按钮,如下

image-20250913212356251

这时候就会立马触发批量同步仓库的流水线,可以在云原生构建栏目查看迁移日志,在日志中可以查看到一共迁移了多少个仓库成功了多少,失败了多少,等待仓库迁移完成✅就行了。

image-20250913212636912

在仓库列表就可以看到GitHub仓库所有的都迁移过来了。 通过docker命令迁移

创建空的CNB仓库这里省略,进入云原生开发环境中,执行以下命令,其他平台迁移参数可以点这里 查看。迁移的核心参保可以看这里 。

docker run --rm \ -e PLUGIN_SOURCE_TOKEN="xxx" \ #替换GitHub的token -e PLUGIN_SOURCE_URL="https://github.com" \ -e PLUGIN_SOURCE_PLATFORM="github" \ -e PLUGIN_CNB_ROOT_ORGANIZATION="xxx" \ #替换cnb仓库的根组织如我的hudi22 -e PLUGIN_CNB_TOKEN="xxx" \ #替换cnb的token -v $(pwd):$(pwd) -w $(pwd) \ cnbcool/code-import

二、Git Sync工具

一个用于在不同 Git 平台之间同步代码的插件。支持通过 HTTPS 或 SSH 方式同步代码到其他 Git 托管平台。

插件地址:https://cnb.cool/cnb/plugins/tencentcom/git-sync。官方文档使用方式写的很清楚了。需要将cnb的代码同步到GitHub也只需要简单的配置流水线就行。如下创建cnb.yml流水线,配置信息如下

main: push: - stages: - name: sync to github image: tencentcom/git-sync settings: target_url: https://github.com/username/repo.git #日前在GitHub创建一个空的仓库 auth_type: https username: ${GIT_USERNAME} password: ${GIT_ACCESS_TOKEN}

或者是要把GitHub和cnb相互同步,可以配置GitHub Actions来实现。或者使用docker命令来完成同步操作。这里省略。

若是需要同步制品,可以看这个插件:https://cnb.cool/cnb/plugins/cnbcool/artifact-migrate 。 UI定制

可以通过在仓库根目录新增 .cnb/settings.yml 配置文件对页面部分 UI 进行定制。例如可以给按钮添加一个hover的gif动图。

云原生开发配置,读取云原生启动按钮所在页面当前分支的 .cnb/settings.yml 配置

例如在启动云原生开发环境时让他自动进入webIDE,不在出现让我选择编辑器的页面可以在settings.yml中添加以下配置。

workspace: launch: button: # 按钮名称 name: 自定义的启动云原生开发 # 按钮描述 description: 点击此按钮启动云原生开发环境 # 鼠标悬浮在按钮上显示的图片(只能用仓库中当前分支的图片,填写相对仓库根目录的路径,如 .cnb/launch-hover.gif) hoverImage: .cnb/launch-hover.gif # CPU 核心数,默认为:8。仅对默认模版有效,如果有自定义云原生开发启动流水线,则此配置无效 cpus: 4 # 是否禁用默认按钮。默认为:false 表示不禁用。true 表示禁用 disabled: false # 环境创建完是否自动打开 WebIDE,默认为 false autoOpenWebIDE: true

当开发环境中未安装 openssh(仅支持 WebIDE):无论此参数配置为 true 还是 false,环境创建完都将自动打开 WebIDE

image-20250913222058096

还可以自定义设置fork仓库的按钮,定义点赞功能等详细可以看这里 。 总结

​ 代码仓库同步工具还是蛮方便的,可以快速批量的同步源平台的代码仓库,可以省去不少时间,至于这个UI自定义,目前还没有找到更好的玩法,除了能增加一点自定义化的东西外,没有看到啥实质的功能🤣。 文章作者: Hudi(唱唱反调) 文章链接: https://www.hudi.space/posts/f3c02a84/ 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 短巷与雨! CNB仓库迁移ui定制 cover of next post 下一篇 ZFile工具 ZFile是一款多功能文件管理工具,支持对接多个对象存储服务(如MINIO、云厂商存储),解决多平台切换繁琐、文件预览不便等痛点。提供一键脚本、Docker等多种安装方式,支持SQLite/MySQL数据库,并可配置Google Drive等高级功能。其亮点在于丰富的文件在线预览与画廊模式,界面友好,配置文档详尽,适合需要统一管理多存储源的用户体验。官网提供在线演示与详细教程。 相关推荐 cover 2025-08-23 CNB切换默认开发环境 CNB云原生开发支持自定义WebIDE开发环境,用户可通过声明式语法或Dockerfile灵活配置。默认环境基于Debian 12,预装Git、VSCode插件及Zsh工具。自定义方式包括:直接指定镜像(如Ubuntu 24.04),通过.ide/Dockerfile构建环境,可扩展软件(如Python、MySQL)和插件;3)将环境打包为Docker镜像加速启动。文章以Python开发为例,演示了集成MySQL、Redis及UV包管理器的配置,并提供了镜像构建与复用方法,优化开发效率。 cover 2025-08-09 CNB创建知识库 文章介绍了使用腾讯云CNB搭建AI知识库的相关内容。相比传统方式(需掌握多种技术或受第三方平台限制),CNB只需会Git操作,通过简单步骤即可搭建,且成本低、资源有免费额度。文中还说明了CNB的组织创建、知识库搭建(包括仓库创建、流水线配置、文档上传等)及外部调用方式。 cover 2025-08-30 CNB刷新EO缓存和插件化 本文介绍了在腾讯云EdgeOne(EO)环境下实现缓存自动刷新的两种方法。首先,通过SSH+Shell脚本方式,利用CNB流水线远程执行服务器脚本刷新缓存;其次,使用腾讯云命令行工具TCCLI,通过简单命令实现缓存刷新;最后,提出插件化方案,将脚本封装为Docker镜像,通过CNB密钥仓库管理敏感信息,提升安全性。三种方法均基于腾讯云API,其中TCCLI方式最为简便。文章详细展示了每种方法的配置步骤,包括密钥获取、脚本编写和流水线部署,帮助用户优化站点更新流程,提升EO缓存刷新效率。 cover 2025-08-24 CNB远程部署和EdgeOne Pages 文章介绍了如何使用腾讯云的CNB(云原生构建)进行远程部署,并通过EdgeOne Pages进行静态网站托管。作者原本使用GitHub和Hexo进行部署,但发现这样做过于繁琐。后来,他尝试使用Docker和RSync进行自动化部署,但遇到了一些问题。最终,作者决定将项目迁移到CNB,并通过CNB的流水线实现了自动化部署。文章还详细说明了如何配置SSH免密登录、使用RSync插件以及如何通过CNB的流水线部署到EdgeOne 评论 Nickname Email Website 4 comments 彬红茶 3 days ago

👍👍,但是我连github打开都有点悬😂😂 Windows 10 Chrome 116.0.0.0 sunboy 7 days ago 1

我愿称之为cnb研究专家:tv_doge: Windows 10 Microsoft Edge 140.0.0.0 hudi Admin 7 days ago Reply @sunboy :

😂要是专家就真好了。菜狗子而已。又菜又爱玩。哈哈哈哈:heo-傻笑: Android Quince Tart Chrome 130.0.0.0 刘郎 2025-09-15 1

我个人感觉 GitHub用着更舒服点 参考的资源更广 更丰富 哈哈 Linux Chrome 127.0.0.0 hudi Admin 7 days ago Reply @刘郎 :

是的,资源多github没得说。我只是不想在本地去安装太多的依赖,毕竟有免费额度的云开发环境哈哈哈哈:tv_偷笑: Windows 10 Chrome 139.0.0.0 LiuShen 2025-09-15 1

甚至还能定制,歪日,才知道哈哈 Windows 10 Chrome 140.0.0.0 hudi Admin 2025-09-15 Reply @LiuShen :

哈哈,是不是花里胡哨的东西一堆😂 Windows 10 Chrome 139.0.0.0 Powered by Twikoo v1.4.15 目录69

CNB迁移和UI定制
    CNB仓库迁移
        一、仓库批量迁移工具
            迁移前置准备
            迁移代码仓库
                通过流水线迁移
                通过docker命令迁移
        二、Git Sync工具
    UI定制
    总结

最新文章 CNB迁移和UI定制 CNB迁移和UI定制 2025-09-13 ZFile工具 ZFile工具 2025-09-08 CNB刷新EO缓存和插件化 CNB刷新EO缓存和插件化 2025-08-30 CNB远程部署和EdgeOne Pages CNB远程部署和EdgeOne Pages 2025-08-24 CNB切换默认开发环境 CNB切换默认开发环境 2025-08-23 CNB创建知识库 CNB创建知识库 2025-08-09 小工具 小工具 2025-07-13 maxwell学习笔记 maxwell学习笔记 2025-07-12 飞牛云NAS搭建 飞牛云NAS搭建 2025-06-25 kubernetes学习笔记(一) kubernetes学习笔记(一) 2025-06-20 关于本站

心情树洞
留言板
在线状态
本站分析
订阅本站

我的圈子

博客录
博友圈

文章整理

学习笔记
工具
博客美化
生活

自建工具

IT工具箱
菜谱
思维导图
我的相册
相册后台

友链

AirTouch’s Blog
李开开博客
XINGYE's Blog
轻雅阁
小赵の破宅

©2024 By Hudi & Hi,朋友别来无恙! 蜀ICP备2024107539号 川公网安备51169602000107号

本站已运行:336 天 7 时 57 分 41 秒

使用Hexo框架 | 基于Butterfly主题

作者: Hudi(唱唱反调) 链接: https://www.hudi.space/posts/f3c02a84/ 来源: 短巷与雨 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


avatar 文章 26 标签 47 分类 4 首页 文章

归档
分类
标签

链接

友情链接
朋友圈
小记

关于

说说
留言
相册
关于本站

短巷与雨 首页 文章 链接 关于 CNB切换默认开发环境 发表于2025-08-23|更新于2025-08-24|学习笔记 总字数:3.3k|阅读时长:13分钟|浏览量:61 这里是AI摘要,这篇文章介绍了如何在CNB云原生开发中切换默认开发环境。文章首先概述了云原生开发环境的特性,包括声明式语法、快速启动 CNB切换默认开发环境

​ 有的时候在CNB云原生开发中,默认的开发环境不是最佳的选择,需要自定义WebIDE的开发环境,官方提供了自定义的环境切换方法,通过声明式的语法,去指定你需要的版本镜像即可。或者是使用Dockerfile定义系统的环境。 一、云原生开发环境

基于云原生构建的远程开发解决方案,支持通过 WebIDE、VSCode 客户端、Cursor 客户端连接远程开发环境进行远程开发。

具有以下特点:

声明式:基于 Docker 生态,Dockerfile 声明开发环境,与代码同源管理
快速启动:即使是超大仓库,也可以数秒准备好代码和环境
按需使用:按需获取开发资源,闲时快速回收,避免资源浪费

官方文档地址:https://docs.cnb.cool/zh/workspaces/intro.html 二、默认环境说明

默认操作系统:Debian GNU/Linux 12 (bookworm)

CNB默认的云原生开发镜像是 cnbcool/default-dev-env:latest

其中包括如下软件:git、rsync、 jqgit-lfs、 vimcurlwget、 unzip、 lsofnload、 htop、 net tools、 dnsutils 、openssh-server

vscode的默认插件:coding-copilot、 vscode-yaml 、githistory、 gitblame 、git-graph、 vscode-yaml、 live-server

包含 oh-my-zsh 插件:git zsh-autosuggestions, zsh-syntax-highlighting

官方默认镜像地址:https://cnb.cool/cnb/cool/default-dev-env 三、自定义镜像

在cnb控制台去创建一个空的仓库,然后在仓库创建一个.cnb.yml即可。

直接指定镜像

直接指定开发环境系统镜像如下。

$: vscode: - docker: image: ubuntu:24.04 #指定开发环境镜像,可以是任意可访问的镜像。 service: - vscode - docker # 开发环境启动后会执行的任务 stages: - name: ls script: ls -al

该方式所指定的系统环境是不具备开发环境的。可以进入系统后再做安装(不方便)。

进入云开发环境以后可以通过以下命令查看操作系统版本

cat /etc/os-release

image-20250822104740915 通过Dockerfile自定义系统环境

​ 由于直接指定镜像,是不具备开发环境的依赖和软件的,所以还可以通过Dockerfile自己定义系统环境。

​ 在仓库的根目录去添加.ide/Dockerfile文件目录。如果未自定义启动流水线,启动开发环境时使用默认流水线创建开发环境。 默认流水线会优先使用 .ide/Dockerfile 构建一个镜像,作为开发环境基础镜像。

启动开发环境的默认流水线中,同时配置了 默认镜像 和 .ide/Dockerfile, 如果 .ide/Dockerfile 不存在或构建失败,会使用 默认镜像作为开发环境基础镜像。 如果遇到启动的环境不符合预期,可以查看构建日志 prepare 阶段 .ide/Dockerfile 是否构建成功

1、官方默认docker自定义系统镜像示例地址,点这里。他是基于node的镜像。这里还是使用ubuntu:24.04做演示。dockerfile文件内容如下:

FROM ubuntu:24.04

指定字符集支持命令行输入中文(根据需要选择字符集)

ENV LANG=C.UTF-8 ENV LANGUAGE=C.UTF-8

2、修改.cnb.yml文件如下。完成后直接点击【云原生开发】按钮,就可以通过Dockerfile去启动一个ubuntu:24.04的系统镜像了。

$: vscode: - docker: build: .ide/Dockerfile service: - vscode - docker

3、通过上述定义的系统镜像仍然是没有开发环境依赖和软件的,通过以下内容可以添加依赖和软件。

.ide/Dockerfile文件内容

FROM ubuntu:24.04

安装软件 官方指定的默认软件

RUN apt-get update && \ apt-get install -y wget curl unzip lsof nload htop net-tools dnsutils openssh-server zsh

安装vscode和vscode的插件,以下是官方指定的vscode默认插件

RUN curl -fsSL https://code-server.dev/install.sh | sh && \ code-server --install-extension cnbcool.cnb-welcome && \ code-server --install-extension redhat.vscode-yaml && \ code-server --install-extension waderyan.gitblame && \ code-server --install-extension mhutchie.git-graph && \ code-server --install-extension donjayamanne.githistory && \ code-server --install-extension cloudstudio.live-server && \ code-server --install-extension tencent-cloud.coding-copilot && \ code-server --install-extension Vue.volar #自定义插件

# 指定字符集支持命令行输入中文(根据需要选择字符集) ENV LANG=C.UTF-8 ENV LANGUAGE=C.UTF-8

当然也完全可以进入开发环境以后再去安装相应的软件和插件。

4、可以根据自己的需要自定vscode的插件,在vscode的客户端中去扩展界面查看插件的ID。复制完插件ID到Dockerfile的相应位置即可。

image-20250822122805316

5、提交会自动触发系统的构建,可以在【云原生构建】模块查看构建日志。进入云原生开发界面中,可以验证软件和插件安装如下。

image-20250822184021130 制作成docker镜像

​ 使用上述方法每次打开云开发环境的时候,都会去创建一个新的镜像,然后安装软件和依赖,这可能会花费好几分钟的时间,可以把环境构建成docker镜像,这样几秒中就可以启动云开发环境了。这里把开发环境制作成镜像推送到CNB的制品库中。 方式一:使用默认系统环境构建自定义镜像。

1、先修改.cnb.yml文件使整个云开发环境是默认的debian系统。才有docker环境。修改完成后,进入云开发环境。.cnb.yml修改如下

$: vscode: - docker: image: cnbcool/default-build-env:latest #云原生开发默认的环境镜像 service: - vscode - docker

2、在云开发环境终端中去构建镜像,首先进入.ide/目录,Dockerfile文件内容和第三小节一致,输入以下指令。

cd .ide docker build -t docker.cnb.cool/hudi22/test001/ubuntu:24.04 . docker push docker.cnb.cool/hudi22/test001/ubuntu:24.04

docker build -t docker.cnb.cool/{组织名}/{仓库明}/{镜像名称}:{版本} .

整个镜像的构建和推送速度都是非常快的。

3、构建完成后可以在仓库的制品栏目去看到系统的镜像如下。

image-20250822223850824

4、使用制作好的镜像,直接在.cnb.yml中声明就行了,如下。

$: vscode: - docker: image: docker.cnb.cool/hudi22/test001/ubuntu:24.04 service: - vscode - docker

但是这种方式构建的系统镜像是不具备docker环境。也就是在云开发环境中是没办法使用docker服务。

方式二:基于默认开发环境仓库构建自定义镜像

​ 为了使我们自定义的开发环境具备docker服务,可以先fork这个官方的默认环境仓库,点这里fork。fork仓库完成后只需要修改Dockerfile少部分内容。

系统默认的Dockerfile文件内容点这里查看。
  • FROM cnbcool/default-build-env:latest #将这行替换成FROM ubuntu:24.04,或者是其他的镜像如node等
  • FROM ubuntu:24.04
  • RUN curl -fsSL https://code-server.dev/install.sh | sh &&\ #根据需要去安装相应的vscode插件
  • RUN apt-get update && apt-get install -y wget unzip lsof nload htop net-tools dnsutils openssh-server zsh #根据需要安装相应的软件

修改完成后✅,进入云原生开发环境中去构建系统开发环境镜像。构建docker镜像的命令同上。

构建完成后需要在其他项目使用这个自定义的系统镜像。需要修改.cnb.yml如下即可。

.cnb.yml

$: vscode: - docker: image: docker.cnb.cool/hudi22/test001/ubuntu:24.04 by: - settings.json - cnb-init-from - cnb-init-from-without-lfs - gitconfig - scripts/add-zsh-plugins.sh - scripts/set-zsh-env.sh services: - vscode - docker

四、构建Python开发环境

​ 可以直接通过Python的镜像去指定开发环境,但是这里我们在默认系统镜像上去修改,把debian改成ubuntu:24.04的基础上去添加Python的环境。在Dockerfile中去声明安装python3和pip3还有UV包管理器。再指定vscode支持Python的插件。然后在环境启动的时候直接启动MySQL、Redis。

Dockerfile信息如下:

FROM ubuntu:24.04

ENV UV_INSTALL_DIR=/usr/local/bin #设置UV包管理工具路径

安装 ssh 服务,用于支持 VSCode 客户端通过 Remote-SSH 访问开发环境,开发环境需保留 apt-get 缓存

RUN apt-get update && apt-get install -y git curl wget unzip lsof nload htop net-tools dnsutils openssh-server zsh python3.6 python3-pip #安装Python3和pip包

安装vscode和插件

RUN curl -fsSL https://code-server.dev/install.sh | sh &&\ code-server --install-extension cnbcool.cnb-welcome &&\ code-server --install-extension redhat.vscode-yaml &&\ code-server --install-extension waderyan.gitblame &&\ code-server --install-extension mhutchie.git-graph &&\ code-server --install-extension donjayamanne.githistory &&\ code-server --install-extension cloudstudio.live-server &&\ code-server --install-extension tencent-cloud.coding-copilot && \ code-server --install-extension ms-python.python && \ #安装Python的插件 code-server --install-extension ms-python.debugpy #安装Python的插件

安装 uv

RUN sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)"

COPY scripts ./scripts

RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" && \ git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions && \ git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting && \ chmod +x ./scripts/*.sh && \ ./scripts/add-zsh-plugins.sh zsh-autosuggestions zsh-syntax-highlighting && \ ./scripts/set-zsh-env.sh && \ echo 'setopt NO_AUTO_REMOVE_SLASH' >> /root/.zshrc && \ chsh -s $(which zsh)

在最开始 source /etc/profile

RUN sed -i '1isetopt NULL_GLOB 2>/dev/null\nsource /etc/profile\nunsetopt NULL_GLOB 2>/dev/null' $HOME/.zshrc

COPY settings.json /root/.vscode-server/data/Machine/settings.json COPY settings.json /root/.local/share/code-server/Machine/settings.json COPY cnb-init-from /bin/cnb-init-from COPY cnb-init-from-without-lfs /bin/cnb-init-from-without-lfs COPY gitconfig /root/.gitconfig

指定字符集支持命令行输入中文(根据需要选择字符集)

ENV LANG=C.UTF-8 ENV LANGUAGE=C.UTF-8

现在去修改.cnb.yml的内容如下

main: pull_request: - services: - docker stages: - docker build .

.cnb.yml

$: vscode: - docker: build: dockerfile: .ide/Dockerfile by: - settings.json - cnb-init-from - cnb-init-from-without-lfs - gitconfig - scripts/add-zsh-plugins.sh - scripts/set-zsh-env.sh env: CNB_WELCOME_CMD: echo "Welcome to 短巷与雨 CNB 😊" services: - vscode - docker # 开发环境启动后会执行的任务 stages: - name: start mysql script: docker run -d -p 3306:3306 -v /workspace/mysql/log:/var/log/mysql -v /workspace/mysql/data:/var/lib/mysql -v /workspace/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7 - name: start redis script: docker run --name redis -p 6379:6379 -d -v /workspace/redis/data:/data redis:6.0.8 - name: 创建项目虚拟环境 script: uv venv - name: 激活虚拟环境 script: . /workspace/.venv/bin/activate - name: 安装项目所需依赖包 script: uv pip install mysql.connector #根据项目实际情况安装即可,你懂的

把Mysql和Redis的数据卷目录写到/workspace/目录下。这样在提交代码时,数据文件也会跟着提交到代码仓库就能起到一个持久化数据的目的。注意:这里Mysql和Redis都没有指定配置文件,你可以自行在/workspace目录下编写Mysql和Redis的配置文件,当然可以在stages中可以指定任意你需要的软件。注意:这里的持久化目录会提交到代码仓库,重要数据不要这么操作。或者是私有化仓库。

启动云开发环境完成后,在vscode的终端栏可以看到启动了Mysql和Redis两个服务。

image-20250823005302964

这里我直接进入mysql的容器中使用sql命令去创建的测试数据库和表还有数据。

docker exec -it mysql /bin/sh #进入容器 mysql -uroot -p123456 #进入mysql

创建数据库和表

create database test02; use test02; create table test02(id int,name varchar(30));

插入测试数据

insert into test02(id,name)values(1,'hudi'); insert into test02(id,name)values(2,'xiaohong'); insert into test02(id,name)values(3,'xiaolan'); quit exit

编写一个test.py文件,去读取test02表的数据。内容如下。

在执行测试脚本前,要用uv工具安装Python连接mysql的包,uv pip install mysql.connector

import mysql.connector

配置数据库连接参数

config = { 'user': 'root', 'password': '123456', 'host': 'localhost', 'database': 'test02', 'raise_on_warnings': True }

创建连接

cnx = mysql.connector.connect(**config)

创建一个cursor对象

cursor = cnx.cursor()

执行查询

query = "SELECT * FROM test02" cursor.execute(query)

获取所有结果

for row in cursor: print(row)

关闭cursor和连接

cursor.close() cnx.close()

在test.py中右击,【Run Python】->【Run Python File In Terminal】就可以运行Python程序了,测试结果如下图。

image-20250823014648989

再修改完代码后记得在终端中去提交代码。这里仅测试mysql,redis就不测试了😄。

​ 在完成上述操作以后,可以将构建的Python开发环境去打包成一个镜像,命令同上面的一样。构建好以后,在.cnb.yml可以直接指定镜像名称,这样启动时就省去了构建系统镜像的时间。

​ 文章演示的这个Python开发环境这里构建了一个镜像,地址:https://cnb.cool/hudi22/python_dev/-/packages/docker/python_dev/ubuntu 。 测试代码上面也有,或者直接看代码仓库。在.cnb.yml中直接指定镜像名字和版本就能直接使用Mysql和Redis服务了。感兴趣的朋友👭可以去试试。其次就是这个环境只做测试使用,你们可以根据自己的开发习惯定义属于自己的云开发集成环境。后面在写关于服务端口转发和环境变量的内容吧。

另外在哔哔一下,就是CNB官方还提供了一些示例,有需要的朋友可以看这个连接:https://cnb.cool/examples/showcase 。 文章作者: Hudi(唱唱反调) 文章链接: https://www.hudi.space/posts/3c9ad839/ 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 短巷与雨! CNB cover of previous post 上一篇 CNB远程部署和EdgeOne Pages 文章介绍了如何使用腾讯云的CNB(云原生构建)进行远程部署,并通过EdgeOne Pages进行静态网站托管。作者原本使用GitHub和Hexo进行部署,但发现这样做过于繁琐。后来,他尝试使用Docker和RSync进行自动化部署,但遇到了一些问题。最终,作者决定将项目迁移到CNB,并通过CNB的流水线实现了自动化部署。文章还详细说明了如何配置SSH免密登录、使用RSync插件以及如何通过CNB的流水线部署到EdgeOne cover of next post 下一篇 CNB创建知识库 文章介绍了使用腾讯云CNB搭建AI知识库的相关内容。相比传统方式(需掌握多种技术或受第三方平台限制),CNB只需会Git操作,通过简单步骤即可搭建,且成本低、资源有免费额度。文中还说明了CNB的组织创建、知识库搭建(包括仓库创建、流水线配置、文档上传等)及外部调用方式。 相关推荐 cover 2025-08-09 CNB创建知识库 文章介绍了使用腾讯云CNB搭建AI知识库的相关内容。相比传统方式(需掌握多种技术或受第三方平台限制),CNB只需会Git操作,通过简单步骤即可搭建,且成本低、资源有免费额度。文中还说明了CNB的组织创建、知识库搭建(包括仓库创建、流水线配置、文档上传等)及外部调用方式。 cover 2025-08-30 CNB刷新EO缓存和插件化 本文介绍了在腾讯云EdgeOne(EO)环境下实现缓存自动刷新的两种方法。首先,通过SSH+Shell脚本方式,利用CNB流水线远程执行服务器脚本刷新缓存;其次,使用腾讯云命令行工具TCCLI,通过简单命令实现缓存刷新;最后,提出插件化方案,将脚本封装为Docker镜像,通过CNB密钥仓库管理敏感信息,提升安全性。三种方法均基于腾讯云API,其中TCCLI方式最为简便。文章详细展示了每种方法的配置步骤,包括密钥获取、脚本编写和流水线部署,帮助用户优化站点更新流程,提升EO缓存刷新效率。 cover 2025-08-24 CNB远程部署和EdgeOne Pages 文章介绍了如何使用腾讯云的CNB(云原生构建)进行远程部署,并通过EdgeOne Pages进行静态网站托管。作者原本使用GitHub和Hexo进行部署,但发现这样做过于繁琐。后来,他尝试使用Docker和RSync进行自动化部署,但遇到了一些问题。最终,作者决定将项目迁移到CNB,并通过CNB的流水线实现了自动化部署。文章还详细说明了如何配置SSH免密登录、使用RSync插件以及如何通过CNB的流水线部署到EdgeOne cover 2025-09-13 CNB迁移和UI定制 本文介绍了如何将其他代码平台(如GitHub)的仓库迁移到CNB平台,并探讨了CNB的UI定制功能。CNB提供了多种迁移工具,包括CNB Code Import(支持批量迁移GitHub、GitLab等平台)、Git Sync(支持HTTPS/SSH同步)以及制品迁移工具。迁移步骤包括生成源平台和CNB的访问令牌、创建根组织,并通过流水线或Docker命令完成同步。此外,通过.cnb/settings.yml文件可定制UI,例如修改云开发启动按钮的样式、自动打开WebIDE等。这些工具简化了代码迁移流程,但UI定制功能目前实用性有限。 评论 目录

CNB切换默认开发环境
    一、云原生开发环境
    二、默认环境说明
    三、自定义镜像
        直接指定镜像
        通过Dockerfile自定义系统环境
        制作成docker镜像
            方式一:使用默认系统环境构建自定义镜像。
            方式二:基于默认开发环境仓库构建自定义镜像
    四、构建Python开发环境

最新文章 CNB迁移和UI定制 CNB迁移和UI定制 2025-09-13 ZFile工具 ZFile工具 2025-09-08 CNB刷新EO缓存和插件化 CNB刷新EO缓存和插件化 2025-08-30 CNB远程部署和EdgeOne Pages CNB远程部署和EdgeOne Pages 2025-08-24 CNB切换默认开发环境 CNB切换默认开发环境 2025-08-23 CNB创建知识库 CNB创建知识库 2025-08-09 小工具 小工具 2025-07-13 maxwell学习笔记 maxwell学习笔记 2025-07-12 飞牛云NAS搭建 飞牛云NAS搭建 2025-06-25 kubernetes学习笔记(一) kubernetes学习笔记(一) 2025-06-20 关于本站

心情树洞
留言板
在线状态
本站分析
订阅本站

我的圈子

博客录
博友圈

文章整理

学习笔记
工具
博客美化
生活

自建工具

IT工具箱
菜谱
思维导图
我的相册
相册后台

友链

清羽飞扬
间窗的博客
张洪Heo
库拉饭
XINGYE's Blog

©2024 By Hudi & Hi,朋友别来无恙! 蜀ICP备2024107539号 川公网安备51169602000107号

本站已运行:336 天 7 时 57 分 48 秒

使用Hexo框架 | 基于Butterfly主题

作者: Hudi(唱唱反调) 链接: https://www.hudi.space/posts/3c9ad839/ 来源: 短巷与雨 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


avatar 文章 26 标签 47 分类 4 首页 文章

归档
分类
标签

链接

友情链接
朋友圈
小记

关于

说说
留言
相册
关于本站

短巷与雨 首页 文章 链接 关于 CNB创建知识库 发表于2025-08-09|更新于2025-08-12|学习笔记 总字数:1.5k|阅读时长:4分钟|浏览量:176 这里是AI摘要,文章介绍了使用腾讯云CNB搭建AI知识库的相关内容。相比传统方式(需掌握多种技术或受第三方平台限制),CNB只 CNB搭建知识库

​ 为啥使用这个腾讯云的CNB来搭建知识库呢,主要就是比传统方式搭建AI知识库方便快捷太多了。传统方式是自己编码或者是使用第三方平台,而CNB就是只要你会Git操作就可以直接通过工作流去搭建AI知识库。只需要简单得几步操作。

传统方式得痛点:

自己编码搭建知识库:需要掌握向量数据库技术、文档处理切片、模型部署调用、向量存储、检索系统开发等等。

第三方平台搭建知识库:语雀、Notion很多功能受限、Dify等配置复杂,并且成本高昂、功能扩展困难等。

以上光是学习成本,环境搭建,后期维护成本都是非常大。

CNB简介

​ 腾讯云 CNB 是云原生构建(Cloud Native Build)的简称,是腾讯云推出的新一代开发者工具。它基于 Docker 生态,为开发者提供代码托管、流水线、云上开发空间、制品管理等能力,通过声明式的语法帮助开发者更高效地构建软件。后面写CNB学习笔记的时候还会专门介绍CNB的代码托管、云原生构建、云原生开发等部分。这里就大致说一下这是个什么东西就行了。

官方地址:https://docs.cnb.cool/zh/ 组织

在创建代码仓库的时候需要创建一个组织,CNB是以顶级组织 独立结算按量计费。如下

image-20250809211535608

也就是说在云原生构建时需要使用资源的,免费的额度一般来说就非常够用了。8核云服务器使用1小时用量为 8核 x 1小时 即8核时。这里免费的有每月160核时,相当不错了。比你租一个按时计费的弹性云服务器便宜的多吧。就算超了,这个0.125/核时,也是相当便宜了。还有就是需要玩大模型的也可以直接云原生开发、搭建环境。免费1600核时的GPU使用额度。次月清零,也不叠加。构建时消耗构建的额度,WebIDE开发时消耗开发的额度,但是有一说一,创建docker服务真的快好多。比起自己的开发环境。

退一万步讲,如果你的免费资源不够用,那你多用几个微信登录,然后通过组织成员加入进去,有几个微信号就可以在免费的额度上*几倍。白嫖老多了。别说是我说的🙂。再说目前还有周年庆的赠送额度。

点击右上角【+】号 -> 【创建组织】->填写组织名即可。若是提示:该组织名称已被保护,请完成下方域名验证创建认证组织。在组织名后面带上数字。或者重新选一个组织名,每年一个账号只能创建一个组织。

知识库搭建

​ 这个CNB的仓库就和GitHub类似的使用方法这里省略,点击右上角【+】号 -> 【创建仓库】即可。创建完成后。若是要在线开发直接点仓库界面的【云原生开发】按钮。这时候会在新的页面打开一个如下。直接点击WebIDE即可这时会打开一个浏览器版本的vscode。这个环境中集成了很多必要的环境依赖如git、openssh-server等,操作系统版本是Debian GNU/Linux 12 (bookworm),或者是你也可以在本地的vscode或者curor里面打开。

在代码仓库的 .cnb.yml 中配置流水线,使用知识库插件。 如下配置,当仓库的 main 分支有代码提交时,会触发流水线,自动使用知识库插件对 Markdown 文件进行切片、分词、向量化等处理,并将处理后的内容上传到 CNB 的知识库中。

main: push: - stages: - name: build knowledge base image: cnbcool/knowledge-base settings: include: "/.md"

如是在WebIDE中开发,直接在workspace中创建.cnb,yml文件,然后需要在终端中去提交代码到cnb仓库。直接push即可,不然不会生效。

当然你可以直接在本地去创建这个.cnb.yml文件。然后再连同知识库所需的文档一并push到仓库中。

参数说明如下: 参数名 说明 默认值 是否必填 备注 include 指定需要包含的文件 空 是 使用 glob 模式匹配,默认包含所有文件。支持逗号分隔多个模式,如 .md,.mdx,.docx,.txt,*.pdf exclude 指定需要排除的文件 空 否 使用 glob 模式匹配,默认不排除任何文件。支持逗号分隔多个模式 embedding_model 嵌入模型 hunyuan 否 目前只支持 hunyuan chunk_size 指定文本分块大小 1500 否
chunk_overlap 指定相邻两个分块之间的重叠token数量 0 否

上传知识库所需要的文档。注意文档格式,例如我给我的博客网站生成一个知识库文档。在blogcode目录下,直接git push上传到CNB仓库后就会触发流水线构建知识库了。在仓库的【云原生构建】模块可以看到知识库的构建日志如下。

构建完成后直接在仓库的任意模块界面按下 / 键 再按 ?键 。就可以唤醒知识库AI助手,如下:

外部调用

​ 这个知识库AI助手不只是能在你的CNB页面直接调出,还可以接受外部的调用。知识库构建完成后,可以通过 Open API 对该仓库所属知识库进行查询检索,召回后的内容可以结合 LLM 模型生成回答。使用示例可以直接看这里。我本来是想做一个博客网站AI的导航的,但是我自己网站的知识库文档不知道应该怎么去生成,就自己写了一个简陋的md文档🤣。后面再想想使用这个知识库做一个网站的AI助手吧。 文章作者: Hudi(唱唱反调) 文章链接: https://www.hudi.space/posts/6e67246c/ 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 短巷与雨! CNB知识库 cover of previous post 上一篇 CNB切换默认开发环境 CNB云原生开发支持自定义WebIDE开发环境,用户可通过声明式语法或Dockerfile灵活配置。默认环境基于Debian 12,预装Git、VSCode插件及Zsh工具。自定义方式包括:直接指定镜像(如Ubuntu 24.04),通过.ide/Dockerfile构建环境,可扩展软件(如Python、MySQL)和插件;3)将环境打包为Docker镜像加速启动。文章以Python开发为例,演示了集成MySQL、Redis及UV包管理器的配置,并提供了镜像构建与复用方法,优化开发效率。 cover of next post 下一篇 小工具 平时不太喜欢去搜罗一些好用的工具,但是看到自己感兴趣的还是会记下来,有的是github上的开源项目,有的是一些直接在线的工具。主要是除了工作时间也不知道去干点什么,或者是和朋友玩玩游戏,或者是city walk一下。在要不然就知道待在家里看看电视了,刷刷抖音,看看朋友们写的博客文章。本来是想写一个征期巡检提醒的机器人的。可以涵盖企微,钉钉,邮箱等的定时提醒,已经完成了一小部分了,但是又想到github上肯定有开源的项目,可以直接拿来改吧改吧用的,后来又想起之前看到的工具,都没拿来用过,还不如趁这个时间拿来运行看看。 相关推荐 cover 2025-08-23 CNB切换默认开发环境 CNB云原生开发支持自定义WebIDE开发环境,用户可通过声明式语法或Dockerfile灵活配置。默认环境基于Debian 12,预装Git、VSCode插件及Zsh工具。自定义方式包括:直接指定镜像(如Ubuntu 24.04),通过.ide/Dockerfile构建环境,可扩展软件(如Python、MySQL)和插件;3)将环境打包为Docker镜像加速启动。文章以Python开发为例,演示了集成MySQL、Redis及UV包管理器的配置,并提供了镜像构建与复用方法,优化开发效率。 cover 2025-08-30 CNB刷新EO缓存和插件化 本文介绍了在腾讯云EdgeOne(EO)环境下实现缓存自动刷新的两种方法。首先,通过SSH+Shell脚本方式,利用CNB流水线远程执行服务器脚本刷新缓存;其次,使用腾讯云命令行工具TCCLI,通过简单命令实现缓存刷新;最后,提出插件化方案,将脚本封装为Docker镜像,通过CNB密钥仓库管理敏感信息,提升安全性。三种方法均基于腾讯云API,其中TCCLI方式最为简便。文章详细展示了每种方法的配置步骤,包括密钥获取、脚本编写和流水线部署,帮助用户优化站点更新流程,提升EO缓存刷新效率。 cover 2025-08-24 CNB远程部署和EdgeOne Pages 文章介绍了如何使用腾讯云的CNB(云原生构建)进行远程部署,并通过EdgeOne Pages进行静态网站托管。作者原本使用GitHub和Hexo进行部署,但发现这样做过于繁琐。后来,他尝试使用Docker和RSync进行自动化部署,但遇到了一些问题。最终,作者决定将项目迁移到CNB,并通过CNB的流水线实现了自动化部署。文章还详细说明了如何配置SSH免密登录、使用RSync插件以及如何通过CNB的流水线部署到EdgeOne cover 2025-09-13 CNB迁移和UI定制 本文介绍了如何将其他代码平台(如GitHub)的仓库迁移到CNB平台,并探讨了CNB的UI定制功能。CNB提供了多种迁移工具,包括CNB Code Import(支持批量迁移GitHub、GitLab等平台)、Git Sync(支持HTTPS/SSH同步)以及制品迁移工具。迁移步骤包括生成源平台和CNB的访问令牌、创建根组织,并通过流水线或Docker命令完成同步。此外,通过.cnb/settings.yml文件可定制UI,例如修改云开发启动按钮的样式、自动打开WebIDE等。这些工具简化了代码迁移流程,但UI定制功能目前实用性有限。 评论 目录

CNB搭建知识库
    CNB简介
    组织
    知识库搭建
    外部调用

最新文章 CNB迁移和UI定制 CNB迁移和UI定制 2025-09-13 ZFile工具 ZFile工具 2025-09-08 CNB刷新EO缓存和插件化 CNB刷新EO缓存和插件化 2025-08-30 CNB远程部署和EdgeOne Pages CNB远程部署和EdgeOne Pages 2025-08-24 CNB切换默认开发环境 CNB切换默认开发环境 2025-08-23 CNB创建知识库 CNB创建知识库 2025-08-09 小工具 小工具 2025-07-13 maxwell学习笔记 maxwell学习笔记 2025-07-12 飞牛云NAS搭建 飞牛云NAS搭建 2025-06-25 kubernetes学习笔记(一) kubernetes学习笔记(一) 2025-06-20 关于本站

心情树洞
留言板
在线状态
本站分析
订阅本站

我的圈子

博客录
博友圈

文章整理

学习笔记
工具
博客美化
生活

自建工具

IT工具箱
菜谱
思维导图
我的相册
相册后台

友链

Nick的琐碎日常
indude - 个人文章分享
间窗的博客
库拉饭
小赵の破宅

©2024 By Hudi & Hi,朋友别来无恙! 蜀ICP备2024107539号 川公网安备51169602000107号

本站已运行:336 天 7 时 57 分 55 秒

使用Hexo框架 | 基于Butterfly主题

作者: Hudi(唱唱反调) 链接: https://www.hudi.space/posts/6e67246c/ 来源: 短巷与雨 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


How to Surf the Web in 2025, and Why You Should | raptitude.com

作者推荐你进行「网上冲浪」⸺ 以一个网站作为起点,通过超链接不断探索,发现自己感兴趣的东西。

作者还分享了 3 条网上冲浪规则:

从一个有大量外部链接的独立网站开始

避免打开多个浏览器标签页

    过去,网页浏览器没有标签页。你必须下定决心,直接跳到下一个地方。如果你不喜欢,随时可以返回,但你确实需要离开当前页面才能看到另一个。这是刺激的一部分,它让你的思维能够完全从一个空间的思想切换到另一个空间,而不是将注意力分散到越来越多的网站上,却无法完全投入到任何一个网站中。

    Source

当你进入一个封闭系统时,请离开

摘录

我说的「网上冲浪」不只是指上网。我的意思是,仅仅通过点击超链接,从一个页面跳到另一个页面来探索互联网,没有明确的目的地,除了那个奇妙的、尚未被发现的网站,当你找到它时,它会让你惊叹不已,让你着迷,那个网站会让你觉得它一直在等你,你永远也看不够。

要冲浪,你必须从一个带有外部链接的普通网站开始,避开所有算法驱动的通道 (Reddit, YouTube, X, 任何应用程序),这些通道引导着当今大部分的互联网流量。你还必须使用一台真正的电脑,而不是手机。如果你最终进入了社交媒体,那你就不是在冲浪了。

年轻的读者可能甚至不知道,互联网曾经完全由网站组成,由人类创建,仅通过超链接连接。超链接充当路标,由其他人手工放置,旨在引导同行者前往他们原本不会知道的独特地点。没有公司拥有的通道,只有许多从每个空地分叉出来的路径,这些路径由这些手工制作的标志标记,召唤你继续前往荒野中的某个其他地方。

20 世纪 90 年代末到 21 世纪初的互联网,与今天的互联网相比,提供了截然不同的感官和情感体验。稍微换个比喻,旧时的网络就像一个由无数相连、装饰各异的公寓组成的无尽城市,人们通过墙壁上的小滑道和传送门穿梭其中。每一个传送门都将你直接送往另一个古怪的空间,由另一个古怪的人物建造,每个空间又都有自己的一系列滑道向外辐射。

在这种结构中冲浪,其特点是充满了惊奇和丰富感。下一个传送门后面可能就是你从未见过的事物。你穿梭于宇宙之中,发现着你甚至不知道存在的事物,⸺ 而宇宙也随之不断扩张。

Source

https://mo42.bearblog.dev/read-to-forget/

Read to Forget | Mo's Blog

我记得同事们会高亮大段文字,有时甚至高达 40%。这对我来说毫无意义。考虑到引人入胜的作品数量之多以及我们有限的时间,我们只能阅读一篇文章一次。所以,我阅读是为了遗忘。当我开始阅读时,我已准备好遗忘眼前 98% 的内容。对于大多数文本,我只想要两样东西:首先,我希望它能微妙地改变我的思维,这是一个渐进式的更新,使我朝着一个更完善的世界模型迈进。其次,我希望从中提取一些关键信息,以便日后写作时使用。例如,如果我遇到一篇论文中写得很好的方法论部分,我就会保存下来。阅读应该激发我的思考并产生新的想法。


The Fundamentals of CSS Alignment | Temani Afif

Temani Afif 整理了几种 CSS 中的对齐方法,包括 Grid Container、Flex Container、Block Container、auto margins、Absolutely-positioned elements 等。


You don't need animations | Emil Kowalski

作者整理了一些使用动画时的注意事项:

在开始制作动画之前,请问自己:这个动画的目的是什么?不要为了加而加。有的动画可能是为了带来愉悦感,但对于频繁操作的动作,动画可能会让人厌烦。
要考虑用户的使用频率,对于频率较高的操作,动画可能会让人觉得缓慢、延迟、和操作脱节。
一般来说动画要快,通常应该保持在 300ms 以下。

Accurate text lengths with Intl.Segmenter API | Sangeeth Sudheer

以前在研究 Emoji 正则匹配 时,我了解到像是 Emoji,它是有多个 UTF-16 code unit 组成的。如果执行 "🎶".length ,你得到的是 2。有的 Emoji 甚至可能是 5,11 等,取决于 Emoji 是怎么组成的。

但对于人来说,它只是 1个 Emoji,长度上应该按 1 计算。

例如,聊天对话框限制了输入长度 100,如果使用 String#length 去计算长度的话,用户输入几个 Emoji 可能就占据了很多长度,导致能够输入的内容会很少。而使用 Intl.Segmenter,可以把 Emoji 当作是一个整体的字符,按 1个 长度统计,在这种场景下或许有用。

"🎶".length; // 2

function realLength(text) { return Array.from( new Intl.Segmenter( "en", { granularity: "grapheme" } ).segment(text) ).length; }

realLength("🎶") // 1


Pragmatism in Programming Proverbs | gingerBill

一篇谚语风格的关于编程实用主义的文章。 摘录

    Reality is part of your problem
    The hardest thing about programming is understanding the problem you are trying to solve
    Once you go generic, you lose information about the specific
    Do not worry about the implementation of a program most of the time. If you understand the purpose, function, and usage of your problem, the implementation will usually be trivial
    You cannot become good at programming without practice, experimentation, and failure
    Do not be afraid to try to new tools, but look for tools that have proven themselves to be useful
    Most best practices rarely have any evidence to back up their claims, especially about being the best
    Clear is better than clever
    Be kind to your future self
    Copying is usually better than dependency
    Errors are nothing special, treat them like every other bit of code
    As a program scales in size, so do the problems
    Knowing whether or not something is useful requires skin in the game in order to get feedback
    You cannot teach virtue, you can only learn it. Look towards virtuous programmers for wisdom, and ask about their ideas on virtue.
    Bodging is doing a job out of necessity using whatever tools and materials come to hand and which, whilst not necessarily elegant, is nevertheless serviceable.
    Most people think they are being pragmatic when programming, however they are usually bodging it.
    Bodging has a huge cost to it and should be not be relied upon.
    A great many people think they are thinking when they are merely rearranging their prejudices.
    Most ideas come from previous ideas.
    Fancy algorithms are slow when N is small, and N is usually small.

Category: 待整理