读:SES——Emacs内置的简易电子表格
目录
在用 Emacs 做表格这件事上,大多数人的第一反应是 Org Table——在 org 文件里画表格、按 C-c = 算公式。但有时候你只是想打开一个文件,像 Excel 那样往格子里填数、写公式、看结果,不需要 org 文件那一整套标题、TODO、tags 的体系。这时候 SES(Simple Emacs Spreadsheet) 就派上用场了。
SES 是 Emacs 自带的 major mode,文件名以 .ses 结尾就会自动进入。它的定位如其名:简单,不支持图表,但足够覆盖你日常需要的电子表格操作。
场景一:记账
先从最经典的电子表格"hello world"开始:记账。新建一个 finances.ses 文件。
Emacs 会打开一个空白的电子表格,里面只有一个 A1 单元格。我们先搭好表头。
填表头
把光标移到 A1 单元格内部,按 " (双引号),minibuffer 会出现提示让你输入文本。输入 Income 回车。现在需要第二列,按 TAB 在右边创建一个新单元格 B1,在里面同样按 " 输入 Expenses 。
Income Expenses
填数据
SES 新建后只有 A1 这一个单元格,要填数据得先创建新行。把光标移到表格下方的空白区域,按 C-o 在当前行上方插入一行。连续按三次,创建 3 个空行。
现在在 A2、A3、A4 里输入数字——直接打数字键就会弹提示。在 B 列输入负数的支出。
Income Expenses 10.5 -3 45.32 -9.87 1 -0.5
用公式算余额
在 A5 写求和公式。按 ( (左括号),输入:
(apply #'ses+ (ses-range A2 A4))
SES 会帮你自动把单元格坐标(如 A2)转成真实的值。同理在 B5 求和支出。然后在 C5 用公式 (ses+ A5 B5) 算出最终余额。
Income Expenses 10.5 -3 45.32 -9.87 1 -0.5 56.82 -13.37 43.45
ses+ 是 SES 内置的安全加法函数。有两个好处:(1) 自动跳过空单元格;(2) 空范围返回 0 而不是报错。这一点很重要,后面插入新行的时候你会看到。
插入新行,公式自动调整
假设后来又翻到几张旧账单,要在中间加几行支出。把光标移到数据区最后一行,按 C-o 插入新行,填入新账目。
每次插入,已经算好的总计会立刻自动更新。按回车选中一个公式单元格, ses-range 的范围已经自动扩大到新的数据范围了。这是 SES 跟 Org Table 相比最明显的便利:不用手动调公式范围。
场景二:服务器集群资源巡检
运维工作中经常要盯一批服务器的资源占用:CPU、内存、磁盘余量。这些数据来自监控系统,但如果你想把最近一次巡检的快照拉出来做个快速聚合分析,SES 很顺手。
新建 server-inventory.ses ,先填表头和数据(方法跟场景一一样:"= 输文本,数字键输数值)。"Environment" 比较长,填完后光标移到 B 列任意单元格,按 w 输入一个合适的宽度值(比如 12),列宽就能完整显示了:
Hostname Environment OS CPU% Memory% Disk% web-01 production ubuntu 23 45 62 web-02 production ubuntu 35 52 78 db-01 production centos 12 78 34 web-stg-01 staging ubuntu 8 30 25 db-stg-01 staging centos 5 25 18
一个实用小技巧:SES 没有直接的"复制上方单元格"快捷键。相邻行内容一样时(比如 B2、B3、B4 都是 "production"),用公式 "(=B2)"= 来引用上一个格的值可以省点事,但会建立依赖关系。数据量不大的话,直接敲字反而最快。
数据多了以后,手动回答这类问题就很容易出错:每个环境的平均 CPU 是多少?Ubuntu 和 CentOS 机器的资源占用有没有差异?磁盘超过 80% 的有几台?
条件筛选:ses-select
ses-select 是 SES 内置的筛选函数:传入一个测试范围、一个比对值和结果范围,返回所有比对通过的结果。
想知道所有生产环境机器的平均 CPU:
(ses-average
(ses-select (ses-range B2 B6)
"production"
(ses-range D2 D6)))
ses-select 做的事情:逐行比较 B 列(Environment)的值是否等于 "production" ,命中就把对应行 D 列(CPU%)加入结果列表, ses-average 求平均。代入示例数据,生产环境平均 CPU 是 23.33%。
同理,按 OS 筛选也不在话下,把比对列换到 C 列就行。比如想知道 Ubuntu 机器的平均内存:
(ses-average
(ses-select (ses-range C2 C6)
"ubuntu"
(ses-range E2 E6)))
会得到 42.33%。
按环境、按 OS、按任意你填了分类标签的列,一样的套路。
处理空值:! 标志
不是每台机器都能准时回传数据。假设 db-stg-01 的网络刚好断了,Disk% 单元格是空白的。如果你直接用 Emacs 自带的 max 函数去算某个环境的最高磁盘占用:
(apply #'max
(ses-select (ses-range B2 B6 !)
"production"
(ses-range F2 F6 !)))
ses-range 末尾的 ! 标志告诉 SES:跳过空单元格,不把它们送进 max 。没有这个标志,普通 Elisp 函数遇到空值就直接报错退出了。
运行结果:生产环境磁盘占用最高的是 78%( web-02 )。
min 也是同样的用法,把 #'max 换成 #'min 就行。
统计数量
用 length 统计某类机器有多少台:
(length
(ses-select (ses-range B2 B6 !)
"staging"
(ses-range D2 D6 !)))
示例数据中 staging 环境有 2 台机器。
**非等值筛选:ses-select 做不到的
ses-select 只支持等值比较。如果想统计"磁盘占用超过 80% 的机器有多少台",就超出它的能力了。但你可以在 SES 公式里直接用 Elisp 的标准过滤函数,比如 seq-filter :
(length
(seq-filter (lambda (x) (> x 80))
(ses-range F2 F6 !)))
ses-range 返回的是普通的 Elisp 列表, seq-filter 对它做一个 lambda 过滤:大于 80 的保留,其他的丢出去。 ! 先排掉空单元格,避免 lambda 拿到 nil 报错。
示例数据中所有磁盘都低于 80%,所以返回 0。同理, cl-remove-if 、 seq-find 这些函数都可以在 SES 公式里直接用,过滤器可以写任意复杂的条件。关键就是记住: ses-range 吐出来的是标准列表,之后你想怎么处理都行。
通过函数组合就可以直接在电子表格里就能把集群概览跑出来。数据变了公式自动重算,下次巡检拷新数据进来,所有聚合结果实时更新。
SES vs Org Table
讲了 SES 能干什么之后,自然会问:什么时候用 SES,什么时候用 Org Table?
| 工具 | 最适合的场景 | 公式能力 | 劣势 |
|---|---|---|---|
| SES | 打开就填、需要交互式电子表格 | Elisp 公式、自动重算、单元格引用自动跟踪 | 纯 .ses 文件,不嵌入文档 |
| Org Table | 表格只是文档的一部分,要导出或写报告 | TBLFM 公式 + Calc 语法,手动重算 |
插入行列后引用不自动调整 |
简单说:你要写文档顺便放个表格 → Org Table。你要像管服务器清单那样频繁增改数据、调公式 → SES。
SES 的局限也不小。内置的空值安全函数不够多——目前只有 ses+ 和 ses-average ,其他聚合都得靠 ! 标志配合普通 Elisp 函数。它也不支持图表、条件格式、数据验证这些重型电子表格的功能。但你本来也不会在 Emacs 里做这些事,所以它找准的位置就是:数据量不大、不想离开 Emacs、想要公式自动重算的轻量级场景。
导出与实用技巧
SES 的导出很简单:选中区域,按普通 Emacs 操作复制( M-w ),每列用 tab 分隔。粘贴到别的 buffer 里就是纯文本。把 tab 替换成逗号就是 CSV。
导出为 Org 表格也容易:直接复制粘贴后,用 C-c | 在 org buffer 里做区域转换就行。
速查:SES 常用按键
| 按键 | 作用 |
|---|---|
M-o |
在当前列左边插入一列 |
M-k |
删除当前列 |
C-o |
在当前行上方插入一行 |
C-k |
删除当前行 |
" |
输入文本 |
| 数字键 | 输入数值 |
( |
输入 Elisp 表达式 |
TAB |
右边创建新单元格并移动光标 |
RET |
查看/编辑当前单元格的表达式 |
C-d |
清空当前单元格 |
C-c C-c |
手动强制重算当前单元格 |
C-/ |
撤销(支持行列和单元格操作的撤销) |
w |
设置当前列宽度 |
h |
显示 SES 所有按键绑定 |
p |
设置当前单元格的打印格式(如 %.2f 保留两位小数) |
M-w |
复制选中区域(tab 分隔) |