【程序设计】7.2 [字与时] 拆分合并

 

7.2 拆分合并

我们已经会用循环函数和 mid 把字符串拆成单个的字符序列,因为这种情况很常用,SPL 提供了 split 函数。s.split()就相当于 len(s).(mid(s,~,1))。

split 还有更多的拆串能力。

不过,在讲 split 之前,我们先学一个 clipboard 函数。

随便找个文本编辑程序,比如 Notepad,输入点字然后选中再按 Ctrl-C,就是我们熟悉的复制动作。现在到切换到集算器,新建网格在 A1 格里填上 =clipboard(),然后执行再看 A1 格的值。

刚才在 Notepad 里复制的文字到了这里,clipboard 函数可以把复制到系统剪贴板的文字取出来。

再在 A2 填入

>clipboard(“Hello,esProc”)

执行后,切换回 Notepad,再按 Ctrl-V 粘贴。看到了什么?

带有参数的 clipboard 函数会把作为参数的字符串复制进系统剪贴板,然后就可以在其它程序中粘贴了。

现在我们来利用这个 clipboard 函数帮助 Excel 干点事。

假定在 Excel 的某一列,比如就是 A 列吧,有一堆名字,比如前几行是这样:

imagepng

现在我们想知道这些名字中总共用了几次字母 e。

Excel 算这个并不太容易,我们用 SPL 来配合一下。

1) 在 Excel 中把这一列选中,按 Ctrl-C,复制到剪贴板里去。

2) 切换到集算器中,写这样的代码:


A

1

=clipboard()

2

=A1.split("\n")

3

=A2.(~.split())

4

=A3.conj()

5

=A4.count(lower(~)=="e" )

执行它,A5 就是我们要的结果了。

我们来看这个代码执行完后脚本中的每个格值,A1 是这样的:

imagepng

它是从剪贴板取出刚才在 Excel 复制的内容,看起来是把 Excel 那一列文字挤到一起了。再看 A2:

imagepng

这是个序列,每个成员正好是 Excel 那一列中的每一行文字,这是怎么做到的?

其实,Excel 复制出来的东西是一个由这些文字构成的大字符串,两行之间用回车符分隔。而回车符是个不可显示字符,在集算器界面中看不到,于是就看起来就像是把这些行的文字都挤到一起了。A1.split(“\n”) 的意思就是用回车符作为分隔符把这个大串拆开成序列,于是就回到每一个成员正好对应 Excel 中每一行的情形了。

这里的 "\n" 就是回车符,字符串常数中的反斜杠 \ 称为转义字符,它的作用是帮助我们写出来一些不方便写出来的字符。比如回车符就用 \n 表示,制表符用 \t 表示,这些都是不可显示字符,但也有编码,也是个字符。\n 在字符串里只是一个字符,虽然在引号中书写出来是 \ 和 n 两个字符,\t 也是类似的。

因为 \ 被当成转义字符了,如果我们想真地需要在字符串常数中有一个 \,那也就转义,写成两个,即 "a\\b" 的实际内容是 a\b,长度为 3;还有,双引号被我们用来当作字符串的分界符号了,但仍然有可能有字符串里含有双引号,这时候也要用转义字符来书写,"a\"b"的实际内容是 a"b,长度为 3。

后面几句就简单了,A3 是个循环函数,它的内部 ~.split() 会把每个成员的字符串再拆成单个字符的序列,所以 A3 是个二层序列:

[[“J”,“a”,“m”,“e”,“s”],[“A”,“n”,“d”,“r”,“e”,“w”],…]

conj()函数我们在讲递归时碰到过,它的作用是把多层序列的作为成员的序列都拼接起来,返回这些成员之间用 | 运算的结果,所以 A4 中的 A3.conj() 将得到:

[“J”,“a”,“m”,“e”,“s”,“A”,“n”,“d”,“r”,“e”,“w”,…]

lower 函数将字符变成小写,现在可以和字符串 "e" 比较了,再用 count 计算就可以了。

写成现在这样是为了把步骤拆开方便解释代码,其实这些运算都可以连着写:


A

1

=clipboard()

2

=A1.split@n().conj()

3

=A2.count(lower(~)=="e" )

s.split@n(x) 就是 s.split(“\n”).(~.split(x)),因为按回车符拆分后再拆开的情况很常见,SPL 就为 split 函数加了 @n 选项。

再看一个例子。

Excel 里某一列存了一批 Email 地址。我们知道,Email 地址都是 x@y 的格式。我们希望把这些 Email 地址重新排序,同一个企业的邮箱能排到一起,即希望按先 y 后 x 的次序排列,比如要把 abc@google.com 和 xyz@google.com 排到一起,而不是把 abc@google.com 和 abc@apple.com 排到一起,但直接用 Excel 的排序就会出后面这种情况了。

还是用剪贴板,在 Excel 中把这列数据复制出来,然后切换到集算器写代码:


A

1

=clipboard()

2

=A1.split@n("@")

3

=A2.sort([~(2),~(1)])

4

=A3.concat@n("@")

5

>clipboard(A4)

执行,然后切换回 Excel 在那一列用 Ctrl-V 粘贴,已经按我们的希望排好了。

A1 我们已经理解了,A2 里的 split@n 还是同样的意思,先按回车符拆分成字符串的序列,再把每个成员用分隔符 "@" 拆分,一个 Email 地址中有且只会有一个 @,所以每个串将被拆成两个成员的序列,分别是 @前面和后面的部分。然后 sort 函数排序时,把序列成员反过来写,这样按序列比较规则会先比 @后面部分,再比 @前面部分,也就是我们希望的次序了。

A4 中的 concat 是 split 相反的函数,split 负责拆,concat 负责合,@n 选项表示针对二层序列做 concat,每个成员恢复成原先的 Email 地址串后拼成用回车分隔的大串,但现在次序已经合理了。再用 clipboard 函数复制到剪贴板中,Excel 那边只要粘贴就 OK 了。

A4 还可以直接写成 A3.sort(~(2),~(1)),sort 函数也支持多参数形式,意思就是先比前面参数再比后面参数。对于单值构成的序列,多参数的 sort 意义不大,但对于二层序列就会方便一些,将来学习结构化数据后,多参数 sort 会更常见。

现在我们处理的 Excel 都是单列的数据,那么多列的行不行呢?

当然也没有问题。

比如这样的 Excel,我们希望将每一行的数据从小到大排序。

imagepng

Excel 的排序通常是按行进行的,在列方向排序就很困难了。借助 SPL 就很容易实现,在 Excel 中把这一片选中复制,然后切换到集算器执行这样的代码:


A

1

=clipboard()

2

=A1.split@n("\t")

3

=A2.(~.sort())

4

=A3.concat@n("\t")

5

>clipboard(A4)

然后再粘贴回去就行了。

Excel 复制出来的成片数据,被 clipboard 函数接收后是一个大字符串,每行之间用回车符即 \n 分隔,每行内的列则用制表符 \t 分隔。所以在 A2 中用 A1.split@n(“\t”) 就可以把这个大串拆成二层序列,其内层成员正好是 Excel 每个单元格的数据;然后在 A3 中对每一行排序,在 A4 中再拼回成这样的 \n 和 \t 分隔的串,粘贴进去 Excel 就完事了。

仔细看 Excel 里的结果,好象有点不对,它把 42 排到了 5 的前面,大小搞错了?

其实没错,因为 split 函数拆出来的东西还是字符串,而作为字符串,"42" 就是比 "5" 小。

那么怎么才能按数值来比较呢?需要转换一下,把 A3 改成:

=A2.(~.sort(number(~)))

与 string()对应,number() 可以把字符串参数转换成数值类型,然后就可以按数值规则来比较和排序了。现在再看,没有问题了。

以前的操作系统下有个叫 grep 的命令,能够在很多文本文件中找出含有某个字符串的文件及以所在的行号。这个挺有用的命令,后来不知道为啥被 Windows 取消了,我们现在来自己实现一下。

假定要找的字符串在 A1 中,我们来指定路径下的所有文本文件:


A

B

C

D

1

abc

=directory@p("D:/data/*.txt")

2

for B1

=file(A2).read@n()

=filename(A2)

3


for B2

if pos(B3,A1)

>output(D2/"\t"/#B3/"\t"/B3)

我们要再学习几个新函数。

A1 中 directory 函数将返回指定路径下(最好是用绝对路径,不然可能因为集算器的启动路径不确定而找不到文件)所有的.txt 文件名,这些文件名将返回成一个字符串序列,@p 表示返回的文件名也是全路径的。A2 循环这个序列,B2 读出每个文件,file 函数用文件名产生一个文件对象,read@n 函数将文本文件又读成字符串的序列,每行一个成员。filename 将全路径文件名中路径的部分剥离。

B3:D3 就容易看懂了,循环每一行文本,找到 A1 就输出文件名、行号及这一行的内容。

这里我们要假定文件都不大,可以一次读入。如果文件很大,就需要使用后面才讲到的游标技术了。

数一数一批文章中有多少单词,也是一个常见的练习题。学会了读文件后,我们也可以练习一下:


A

1

=directory@p("D:/book/*.txt").sum(file(~).read().words().len())

没有选项的 read 函数将把整个文本文件读一个大字符串,而 words 函数将把这个大串中的单词都拆出来构成序列,那剩下只要数一下长度再加起来就行了, 一行就够了。

我们再做一个平常可能用得着的任务:把一批 Excel 文件合并成一个大的。

经常我们可能会收集到各个时期或者各个部门的 Excel 文件,这个 Excel 的格式都一样。要把这些文件合并成一个完整的文件方便进一步统计。但手工复制粘贴很麻烦,假如有几十上百个文件,那就相当劳累了。这种事情正好让程序来做。

我们假定要合并的 Excel 都是行式的,即第一行是标题,之后每一行都是数据,比如这样的:

imagepng

这也是很常见的的 Excel 格式。

还要增加一个要求,把原来分开时的 Excel 文件名拼到合并后的 Excel 的最后一列,这样能区分出数据是从哪个文件来的。


A

B

1

=directory@p("D:/data/*.xlsx")

2

for A1

=file(A2).xlsimport@w()

3


=filename@n(A2)

4


=B2.to(2,).(~|B3)

5


=@|B4

6


=if(#A2==1,B2(1)|"File",@)

7

=file("D:/all.xlsx").xlsexport@w([B6]|B5)

B3 的 filename@n 会拆解出文件名中去除扩展名的部分,自己用串拆分也能做出来。B2 的 xlsimport@w() 将把一个 Excel 文件读成二层序列,Excel 的每一行对应其一个成员,而每一行中的每一列则对应其成员的成员。B4 中去除第一行的标题,再将文件名拼到每一行的最后。B5 把这些数据合并起来,而 B6 要再保留一个标题,同样也要多拼一列。

最后 A7 用 xlsexport@w()把汇总后的二层序列 [B6]|B5 写成一个新的 Excel 文件(写文件时最好也用绝对路径)。

无 @w 选项的 xlsimport() 其实能把 Excel 文件读成更方便的数据类型,完成这个任务的代码也会更简单,但要在讲过结构化数据之后再来介绍了。

随着学习的深入,我们将逐步进入可以实战的程度,处理文件数据是很常见的任务。这一节的例子中,我们都使用了绝对路径来定位文件。这里简单介绍一下集算器对于使用相对路径时的文件寻找规则(绝对路径就直接找到了):

如果在环境中设置了主路径(第 6 章第 3 节),那么集算器将从这个主路径开始找;如果主路径填成空的,则从当前正在使用的脚本文件所在路径去寻找,当前脚本可能刚新建还未保存的,这时还没有所在路径,就无法确定会哪里找了,很可能发生找不到的现象。所以要么设置主路径,要么当前脚本都被保存而有所在路径。

和主路径同时设置的那个寻址路径是用来寻找被调用脚本的,和数据文件无关。把数据文件放到寻址路径中不会自动被找到。

自己做点实验看看,也就容易理清楚了。

我们后面再举例时,将使用简单的相对路径,请读者根据上面的介绍自行调整合适的系统配置以及代码中的路径。

【程序设计】 前言及目录

【程序设计】7.1 [字与时] 字符串

【程序设计】7.3 [字与时] 日期与时间