乍听之下,不无道理;仔细揣摩,胡说八道

0%

序言

理论上,开发人员是不允许操作生产环境的,更别说是像商品、订单这样的重要业务数据。不过对小公司来说,后台系统往往不是很完善,总有一些需求让运营或客服部门的同事操作起来捉襟见肘,不得不寻求开发人员的帮助。

通常这些部门的同事会给过来一批需要处理的商品或订单的ID,我会将它们粘贴到一个脚本中,并将脚本放到生产环境的机器上运行,以实现他们的ad hoc需求。ID一般用Excel文件,或在线文档的方式提供过来,将它们粘贴到脚本的源码中之后,还要为它们添加必要的引号和逗号,以满足所用语言的语法要求。比如下图就是直接粘贴后,VSCode提示错误的样子

那么,怎样才能不失逼格地给这批ID加上前后的引号及行末的逗号呢?

八仙过海,各显神通

有很多方法可以完成这个任务,比如借助VSCodemulti-cursor功能,手动添加前后缀

当要添加光标的位置处于同一列时,更适合用VSCode的另一个功能在下面添加光标(快捷键是command+option+↓)来实现,免去了一遍遍点击鼠标的烦恼。multi-cursor所敲入的每个光标还可以在各自的行上沿同方向移动不同的距离,适合处理每行长度不一致的情况。

也可以用Vim中的列编辑模式,操作体验差不多,还可以比VSCode按更少的键——起码不需要一直压着option键。

Vim列模式的效果

但列编辑模式不方便在行末追加内容——必须先在第一行的末尾敲入一个空格,往右移动依次光标,然后才能继续用列编辑模式批量添加后缀。

Emacs也有类似列编辑模式的功能,它的string-insert-rectangle命令比Vim的更便于添加后缀。但它没有默认的快捷键,需要先按下M-x,再输入命令名并回车,略为繁琐(尽管命令名可以自动补全)。

Emacs的string-insert-rectangle的效果

除了各家编辑器内置的功能,命令行工具也适合完成这种处理,比如可以用sed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  /tmp cat b
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
5FEB1AE4-239A-4276-8E37-BE913CE6D117
➜ /tmp sed -e "s/^/'/" -i '' b
➜ /tmp sed -e "s/$/',/" -i '' b
➜ /tmp cat b
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',
'5FEB1AE4-239A-4276-8E37-BE913CE6D117',

有些从在线文档上复制下来的ID会有一行空行存在于两两之间,如果是在命令行的话,只需要先用grep筛选一遍即可,可组合性比编辑器更强。

美中不足的是,用sed处理后需要手动将文件b的内容粘贴到脚本中——如果是用Emacs的话,也可以用C-x i让编辑器在光标处直接插入该文件的内容。

如果可以寸步不离Emacs,通过简单的命令或快捷键来完成这个操作,岂不美哉?

自己动手,丰衣足食

用上自定义的Elisp函数后的效果如下

其实实现思路很简单:

  1. 首先用户会选中一片要添加前后缀的区域;
  2. 使用buffer-substring-no-properties函数复制这个region中的字符串,绑定为text
  3. read-from-minibuffer提示并读取用户输入待添加的前后缀字符串;
  4. split-stringtext切割为一行行的字符串,给每一行添加前后缀,再用mapconcat拼回一个字符串;
  5. delete-region删除被选中的内容,然后用insert插入新的字符串。

最终的Elisp函数的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defun lt--insert-at-start-end ()
"为TEXT中的每一行添加PREFIX前缀和SUFFIX后缀。"
(interactive)
(let* ((text (buffer-substring-no-properties (mark) (point)))
(prefix (read-from-minibuffer "插入的前缀:"))
(suffix (read-from-minibuffer "插入的后缀:"))
(lines (split-string text))
(decorated-lines
(mapcar (lambda (line)
(concat prefix line suffix))
lines))
(new-text (mapconcat 'identity decorated-lines "\n")))
(delete-region (mark) (point))
(insert new-text)))

欢迎读者朋友中的Emacs用户也来使用使用;-)

序言

《编码》这本书曾经在我的豆瓣“想读”列表中躺了很久,大概在今年年初才开始看。但读着读着发现书中的电路图越来越多,而我的阅读热情也随之被慢慢浇灭。五月初的时候,终究还是把它合上,并在豆瓣上羞愧难当地将其标注为“读过”。

抛开晦涩的电路图不谈,书中有一句话吸引了我的注意力

第一次读到这里时,我想作者应当会在下一段给出具体的证明过程——结果居然没有。难道作者觉得两侧的空白太小了,不足以写下他所发现的美妙证法?

受好奇心的驱使,我便试着证明书中的这个结论。

不过正式开始前,还得明确一下命题:对于任意的正整数aba不等于b),10的a次幂和2的b次幂不相等。

先证明一条引理

为了证明上面的命题,需要先证明一条引理:对于任意的正整数a,5的a次幂是一个奇数。可以用数学归纳法来证明。

首先验证a为1时命题成立。由于5的1次幂为5,并且5是一个奇数,所以命题成立;

接着,假设ak时命题成立,将5的k次幂写成2n+1的形式,当ak+1时,

因此,5的k次幂也是一个奇数。因此,该命题对于任意的正整数a都是成立的。

同理可证:对于任意的正整数a,2的a次幂是偶数。

反证法证明原命题

假设存在正整数abb大于a),使得10的a次幂与2的b次幂相等

将10分解为2和5的积,再两边同时除以2的a次幂

等式的左边和右边分别是5的正整数次幂与2的正整数次幂。由前一节的引理可知,左边是奇数,右边是偶数,两者不可能相等,与上述等式产生矛盾。因此,原假设不成立,命题得证。

后记

我最开始的想法很复杂。虽然也是采用反证法,但我将等式做了如下变换

然后试图证明以2为底的10的对数不是有理数,和等式右边不相等。不过这个方法于我而言太难了,便没有继续尝试下去。

序言

相信各位读者对秒表都不陌生,智能手机上通常都有这样一款软件

来自我的小米手机的截图

有一天心血来潮,便想要“复刻”一个命令行版本的秒表程序——主要是想尝试一下新学会的、“原地更新”的技能,而不是一行接一行地输出。程序的运行效果如下

那么这是怎么做的呢?

实现思路及代码

如何获取流逝的时间长度?

要实现一个秒表,首先要知道从开始计时至今过了多久。在*nix系统中,表示时刻的事实标准是Epoch Time,在shell脚本中要获取Epoch Time可以用date命令。再用首尾时刻相减便得到了期间流逝的秒数了,示例代码如下

1
2
3
4
begin_at=$(date '+%s')
# 睡个觉
end_at=$(date '+%s')
((interval=${end_at} - ${begin_at}))

双圆括号是一种在shell脚本中执行算术运算的语法,其它语法可以参见Math in Shell Scripts

如何换算为时分秒?

有了interval中存储的总秒数后,换算成时分秒便是轻而易举的事情,示例代码如下

1
2
3
((hours=${interval} / 3600))
((minutes=(${interval} % 3600) / 60))
((seconds=(${interval} % 3600) % 60))

如何输出形如hh:mm:ss的格式?

hh:mm:ss的意思是分别用两个十进制数字显示时分秒,并以冒号分隔它们。如果有任何一个单位的数值小于10,便用字符0填充左侧的空白。按这个格式,凌晨1点2分3秒便会显示为01:02:03

要在命令行中打印字符串,最容易想到的便是echo命令,只可惜它不能方便地实现填充字符0的需求。

强人所难也不是不行,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
hours=1
minutes=2
seconds=3

if [ "${hours}" -lt '10' ];
then
echo -n "0${hours}"
else
echo -n "${hours}"
fi
echo -n ':'
if [ "${minutes}" -lt '10' ];
then
echo -n "0${minutes}"
else
echo -n "${minutes}"
fi
echo -n ':'
if [ "${seconds}" -lt '10' ];
then
echo -n "0${seconds}"
else
echo -n "${seconds}"
fi

更优雅的方法是用printf命令来自动填充左侧的字符0

1
printf "%02d:%02d:%02d" ${hours} ${minutes} ${seconds}

printf命令类似于C语言中的printf函数——它也支持打印转义的字符,下文会提到。

如何覆盖已经打印的内容?

今年以来我在断断续续地看Build Your Own Text Editor,学习如何开发文本编辑器。在这本小册子的第三章中,作者讲述了如何使用终端的转义序列(escape sequence)来控制屏幕上显示的东西——这正是秒表程序所需要的。

例如,在终端输出转义序列\x1b[2J可以清空屏幕,效果如下

为了覆盖已经打印出来的时分秒,需要:

  1. 先将光标移动到行首;
  2. 再清除从光标开始到行末的内容。

查阅《VT100 User Guide》第三章可以知道

  1. 要把光标移动到行首可以用转义序列\x1b[8D。之所以是8,是因为按照hh:mm:ss输出时分秒后光标距离行首8个身位;
  2. 要清除光标到行末内容可以用转义序列\x1b[0K(实际上,将光标移到行首只需要使用回车(carriage return)即可,但它被解释为开启新的一行了)。

更优雅的方法甚至连转义序列也不需要,只要用tput命令即可,示例代码如下

1
2
3
4
echo -n '11:22:33'
tput cr
tput el
echo '44:55:66'

关于crel,以及更多可以传给tput命令的参数,可以参见terminfoman文档。

如何每隔一秒钟输出一次?

这大概是整个程序中最简单的需求了

1
2
3
4
5
while [ 1 -eq 1 ]
do
# 此处可以为所欲为
sleep 0.5
done

完整的秒表实现

至此,完整的秒表程序就可以实现出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
# 秒表,以hh:mm:ss的格式展示数据

begin_at=$(date '+%s')

while [ 1 -eq 1 ]
do
end_at=$(date '+%s')
# 算术运算:http://faculty.salina.k-state.edu/tim/unix_sg/bash/math.html
((interval=${end_at} - ${begin_at}))
((hours=${interval} / 3600))
((minutes=(${interval} % 3600) / 60))
((seconds=(${interval} % 3600) % 60))
tput cr
tput el
printf "%02d:%02d:%02d" ${hours} ${minutes} ${seconds}
sleep 0.5
done

运行后的效果正如本文开头的GIF所示。

全文完。

浅尝一下NOT EXISTS

最近老婆在看视频学习MySQL,然后碰到了这样一道习题:有三个表,分别记录学生、课程,以及学生选修了什么课程的信息,问如何用NOT EXISTS找出选修了所有课程的学生。

为了避免想破脑袋编造一些尴尬的学生姓名和课程名,我简化了一下习题中的表的结构,只留下它们的ID列。建表语句如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 学生表
CREATE TABLE `student` (
`id` INT NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);

-- 课程表
CREATE TABLE `course` (
`id` INT NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);

-- 选修关系
CREATE TABLE `elective` (
`student_id` INT NOT NULL,
`course_id` INT NOT NULL,
FOREIGN KEY (`student_id`) REFERENCES `student`(`id`),
FOREIGN KEY (`course_id`) REFERENCES `course`(`id`)
);

还需要给它们塞入一些示例数据

1
2
3
INSERT INTO `student` (`id`) VALUES (1), (2), (3), (4), (5);
INSERT INTO `course` (`id`) VALUES (1), (2);
INSERT INTO `elective` (`course_id`, `student_id`) VALUES (1, 1), (2, 1), (1, 2), (2, 3), (2, 5), (1, 5);

显然,只有id列的值为1和5的学生是选修了全部课程的。用NOT EXISTS写出来的SQL语句如下

1
2
3
4
5
6
7
8
9
10
SELECT * 
FROM `student`
WHERE NOT EXISTS (SELECT *
FROM `course`
WHERE NOT EXISTS (SELECT *
FROM `elective`
WHERE `student`.`id` =
`elective`.`student_id`
AND `course`.`id` =
`elective`.`course_id`));

DBEaver中运行后的结果为

在DBEaver中执行的结果

正确地找出了两个选修了所有课程的学生的id

如何理解双重NOT EXISTS

当第一次被请教这道习题的时候,我其实并不能理解NOT EXISTS的含义。直到后来去看EXISTS文档,才顿悟了上面的SQL。

我的理解方法是将双重NOT EXISTS转换为三层循环。以上面的SQL为例,转述为人话就是:找出student表中所有的、没有任何一门course表中的课程是没有选修的、的学生——双重的 没有

转换为三层循环大概长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (const student of students) {
// 是否存在学生未选修的课程
let existSuchCourse = false;
for (const course of courses) {
let existSuchElective = false;
for (const elective of electives) {
if (elective.student_id === student.id && elective.course_id === course.id) {
existSuchElective = true;
break;
}
}
// 如果遍历完elective表的记录后,existSuchElective仍然为false,说明的确有一门课程是没有选修记录的
// 那么便意味着“存在至少一门课程,使得当前被遍历的学生与该课程没有选修关系”。
if (!existSuchElective) {
existSuchCourse = true;
break;
}
}
// 如果遍历完一圈后确实没有找到“未选修”的课程,说明这名学生全都选修了
if (!existSuchCourse) {
console.log(student);
}
}

NOT EXISTS的本质

即使不强行理解,也可以让MySQL明确告知双重NOT EXISTS是怎么运作的。用EXPLAIN解释上面的SQL的结果如下图所示

MySQL的EXPLAIN命令的文档中说明了如何解读执行计划

EXPLAIN returns a row of information for each table used in the SELECT statement. It lists the tables in the output in the order that MySQL would read them while processing the statement. This means that MySQL reads a row from the first table, then finds a matching row in the second table, and then in the third table, and so on. When all tables are processed, MySQL outputs the selected columns and backtracks through the table list until a table is found for which there are more matching rows. The next row is read from this table and the process continues with the next table.

以上面的EXPLAIN为例,MySQL从student表中读出一行,再从course表中读取一行,最后从elective表中读取一行,然后看看WHERE子句是否能够被满足。如果可以,就输出从student表中读出来的这行数据。上图第2和第3行的select_type都是DEPENDENT SUBQUERY,表示它们依赖于“外层”的查询上下文——electiveWHERE子句依赖于studentcourse中读出来的行。

似乎和方才的三重循环有异曲同工之妙呢。

后记

NOT EXISTS这么“高阶”的功能我从未在业务代码中读过和使用过——别说NOT EXISTS,就算是EXISTS也是从未有之,甚至连子查询也极少。毕竟“正经的互联网公司”只是把MySQL当妹妹当一个具备复杂查询查询功能的key-value数据库来使用(笑

比起双重NOT EXISTS,我更可能凭直觉写出基于子查询的解决方法

1
2
3
4
5
6
SELECT * 
FROM `student`
WHERE `id` IN (SELECT `student_id`
FROM `elective`
GROUP BY `student_id`
HAVING( Count(0) ) = 2);

我甚至觉得会有人把数据库里的行读进内存然后用应用层代码来找出选修了全部课程的学生!

全文完。

API测试用例是什么?

在互联网大行其道的今天,身为一名电商平台的程序员,必定经常与HTTP API打交道,一个常见的情况便是做API测试。抛开可以用单元测试代替的,很多时候需要真地发出HTTP请求才行。这些负责发出HTTP请求的东西可能是一行curl命令,可能是一个.js文件,也可能是一个在postman中点击按钮的操作,但不管形态如何,它们便是API测试用例。尽管名字中带有“用例”二字,但很多时候是由人来校验结果的,用例更关注发出怎样的HTTP请求。

为什么要管理它们?

不同于每天在浏览器中发生成千上万次的、平凡的HTTP(或HTTPS)请求,API测试用例是值得一番精心管理的,因为:

  1. API测试用例通常会重复使用,因此必须将它们持久化保存。也许是保存成shell脚本,也许是保存成脚本语言源文件,也许是保存为某一款软件的数据文件;

  2. 需要为多个API编写测试用例,因此必须区分不同的API对应的测试用例。例如,负责管理业务资源的服务(比如一个管理商品数据的、提供RESTful API的服务),起码需要提供增删查改的功能,那么也就需要有增删查改对应的API测试用例;

  3. 需要为多个服务的API编写用例,因此必须区分不同的服务对应的测试用例集。例如,既然有商品服务,那么极可能还有订单服务、优惠券服务、物流服务,等等,每个服务又都有增删查改的功能,这些不同服务的API也需要各自的测试用例;

  4. 需要区分不同的运行环境。通常本地、开发、测试,以及生产环境是互相隔离的,一个用例中的参数往往不能照搬到另一个环境中。

API、服务,以及环境这三个维度上的区别,使得测试用例的数量显著增加,如不进行管理,当要用时,要么不得不从零开始再写一遍脚本,要么得翻查很久才能找到所需的用例。

用org-mode管理

org-mode是什么?

org-mode是一款Emacs编辑器的扩展,它让使用者能够用快速高效的纯文本方式来记笔记、维护待办事项、安排计划,以及编写文档。org-mode的精髓在于它的大纲组织能力,以及依托于Emacs的扩展能力,两者使其正好可以胜任管理API测试用例的工作。一个.org文件的示例如下图所示

org-mode官网的示例截图

如何用org-mode管理API测试用例?

尽管org-mode提供了丰富的功能,但只是管理API测试用例的话,并用不上太多花里胡哨的东西,只需要org-mode的大纲功能和org-babel特性即可。

首先用不同的.org文件区分不同的环境。

接着用不同层级的headline区分不同的服务、资源类型,以及API。

然后用org-mode代码块语法来编写HTTP请求。以请求https://httpbin.org/uuid为例

1
2
3
#+BEGIN_SRC restclient
GET https://httpbin.org/uuid
#+END_SRC

#+BEGIN_SRC#+END_SRC分别表示开启和结束代码块,restclient表示这个代码块内的代码可以用Emacs的restclient-mode来编辑。在代码块中,GET https://httpbin.org/uuid表示以GET方法请求https://httpbin.org/uuid

安装了restclient后,将光标定位在代码块上并按下ctrl-c ',可以进入一个单独的buffer编辑其中的源代码

最后,如果配置了org-babel,甚至可以直接在代码块上按下ctrl-c ctrl-c来发出HTTP请求。


可以看到,HTTP响应的内容会保留在这个.org文件中。

后记

以前我也用过其它的工具来管理API测试用例:

  1. 刚工作的时候用的是Postman,那时候Postman还是Chrome的一个插件;
  2. 后来出于对Firefox浏览器的喜爱,找了一个叫RESTClient的插件来代替postman,久而久之发觉两者的差距蛮大,终究无法代替;
  3. 接着遇到了Emacs中的restclient.el,于是用了好一段时间的纯restclient-mode(没有搭配org-mode);
  4. 再后来开始用Mac办公了,便开始寻找Mac下的这类工具,遇到了Insomnia。如果有人找我推荐用于HTTP API测试的GUI工具的话,我会毫无不犹豫地推荐这款。

再后来,我又回到了Emacs,并用org-mode来管理这些API测试用例。目前这是最适合我的一种方式。

全文完。