SPL:未结构化文本的读写和解析处理

文本文件可能是结构化的,也可能是无结构的,比如是一篇文章,一则日志,也可以是一份工资清单。未结构化的文本不能直接应用类似 SQL 式的运算,而要用更基础的文字处理运算。

下面我们根据文本文件的不同内容,来分类分析下不同内容时的相关计算。

1    读入

SPL 用 file 函数打开文本文件,可以读成序列或游标,然后就可以进一步计算了。当文件数据不大,能够全部装入内存时,用 read 函数将数据读成串或序列;当文件数据很大,不能全部装入内存时,用 cursor 函数将数据读成游标。

 

下面是一些读取方式介绍。

1.1.      整文件读入

现有一个名为 novel.txt 的文件,下述表达式将整个文件读为一个文本串:

=file(“novel.txt”).read()

file 函数用于打开指定文件,这个文件可以采用绝对路径或者相对路径,相对路径时,它是相对于集算器环境中的主目录。再使用 read 函数将文件内容读成一个大串返回。

file 函数缺省采用的是操作系统的默认字符集,如果文本需要指定字符集,则可以使用:

=file(“novel.txt”:”UTF-8”).read()

也就是在文件名后面用冒号隔开,加上指定字符集的名称。

1.2.      按行读入

..

这类按行分开的内容,将每一行读成一个序列成员时,方法如下:

=file(“urls.txt”).read@n()

read 函数使用选项 @n 将每一行数据置入序列成员,返回值跟 1.1 节不同,返回的是序列对象。变量查看面板中的红框为 Member(成员) 则表示当前对象是一个序列,如下图:

 

..

如果每个成员是一些串描述的日期,或者数值,还可以配合选项 @v,将对应的成员解析为相应类型的数据。

如下的示例数据:

..

使用 =file("data.txt").read@n() 直接读取时,每一项数据缺省都是串类型:

..

使用带选项 @v 的表达式 =file("data.txt").read@nv() 读取后,可以看到可以解析为数据的值都变成了相应类型的数据:

..

数值查看面板中,为了区分类型,字串类型会加上下划线。不过要注意的是,日期的格式需要在应用环境中设置,只有跟环境设置一致的日期写法才能正确解析,比如上图中 2020/05/06 因为跟环境格式不一致所以没法解析为日期类型。

1.3.      大文件按行读入

大文件时,使用游标函数 cursor 读入,将 1.2 节中的数据用游标读入的方法如下:

=file(“urls.txt”).cursor@s().fetch(10)

游标缺省是处理多列的序表数据,上例的每一行对应一个成员,无需拆分,则使用选项 @s 来指定不按字段拆分,直接返回包含各成员的序表。图中红框是产生序表的缺省字段名,如下图:

..

如果不想返回序表,而是想得到跟 1.2 中一样的返回序列,则需要配合选项 @i,方法如下:

=file(“urls.txt”).cursor@si().fetch(10)

 

再看如下的一些股票指数数据:

..

数据是用 Tab 值分开的多列数据,而 cursor 函数的默认处理方式便是返回多列的序表,所以用缺省选项的表达式:

=file("d:/stock.txt").cursor().fetch(10)

读取的结果就已经是多行多列的序表:

..

对于这种包含多列的数据,如果不想拆分为序表,则用 @s 选项:

=file("d:/stock.txt").cursor@s().fetch(10)

此时的结果为只含一列的序表,其中红圈为缺省字段名:

..

如果当前序表仅有一列数据,则可以用 @i 选项将结果返回成序列。当读入的序表已经是多列时,光用 @i 选项是不起作用的,没法转为序列,@i 选项通常要配合 @s 选项使用。

 

2    写出

将 SPL 中计算处理后的串、序列写出到文本文件时,先用 file 函数打开写出文件,然后用 write 函数将值写出。对于游标中的大数据,采用循环分批取数并写出时,要用追加写出方式。

下面是一些写出例子。

2.1.      整串写出

使用write 函数可以直接将文本串写出到文件,如下示例:


A

B

1

William Shakespeare (baptised 26 April   1564; died 23 April 1616) was an English poet and playwright, widely regarded   as the greatest writer in the English language and the world's pre-eminent   dramatist.

一段文章

2

=file("d:/paragraph.txt").write(A1)

将 A1 中的文章内容整段写出到 paragraph.txt

写出文件的效果:

..

2.2.      按行写出

当要写出的数据是序列对象时,write 函数会将每个成员写成一行,将 2.1 示例稍微改动一下:


A

B

1

William Shakespeare (baptised 26 April   1564; died 23 April 1616) was an English poet and playwright, widely regarded   as the greatest writer in the English language and the world's pre-eminent   dramatist.

一段文章

2

=A1.words()

将 A1 中的文章内容拆分为单词序列

3

=file("d:/words.txt").write(A2)

将单词序列 A2 写出到文件 words.txt

可以看到写出函数 write 的使用跟 2.1 没有区别,此时 A2 的内容为一个包含多个串的序列,write 函数会根据要写出的对象,自动识别,如果是序列,则会将每个成员写为一行。对比下按行写出后的文件内容:

..

注意,按行写出时,换行符会缺省采用操作系统的换行符。如果想强制使用 windows 风格的换行符 (包含字符回车和换行,也即 \r\n),则需要使用选项 @w。

2.3.      追加写出成大文件

假设上述 urls.txt 文件很大 (没法一次性读入内存),现在要复制一下该文件,则需要使用游标读取源文件,以及使用追加写方式,此时需要用到 write 函数的 @a 选项,复制大文件的示例代码:


A

B

1

=file("d:/urls.txt").cursor@si()

用游标方式读入源文件,并用 si 选项将内容返回成序列

2

for A1,1000

循环游标取数,每次取 1000 行,直到全部取完

3


=file("d:/urlCopy.txt").write@a(A2)

B3 中使用 write@a 函数将每次取数结果追加写出到 urlCopy.txt。

3.  文本处理举例

文本的内容形态百千,能处理的方式也各式各样。下面列出一些最常见的文本处理方式。

3.1.        查找

查找文本内容,最常见的有 grep 命令,比如:

grep magic /usr/src 将目录 /usr/src 下所有文本文件中包含 magic 的内容查找出来。

SPL 有丰富和现成的函数可以使用。仅需两行代码便能实现类似 grep 的查找功能:


A

B

1

=directory@ps(path+"/*.txt")

列出搜索目录下的所有文本文件(包含子目录)

2

=A1.run(file(~).read@n().run(if(pos(~,key),output(A1.~/"  第"/#/"行:  "/~))))

读入每个文件内容,并逐行比较关键词,输出相应信息

上表中用到了参数 path 和 key,这个需要先在脚本文件中定义好,执行时输入相关参数。

补充说明:

A2   run 为循环执行函数,对根目录下的所有文件遍历执行。后面还有一个 run 则是对文件内容遍历搜索。

output 的逻辑很简单,找到后,打印出行内容,行号等信息,这里要注意的是 SPL 中,整数类型的行号跟字符串拼接时,要用 /,不能用 +。

       其中实现查找功能的是 pos 函数。

3.2.        替换

SPL 提供的 replace 函数可以实现对文本串的词语替换。替换后的内容需要再写出,所以分别将读入内容,替换,写出分开执行:


A

B

C

1

=directory@ps(path+"/*.txt")

列出 path 目录下的所有文本文件(包含子目录)


2

for A1

=file(A2).read@n()

循环处理目录下的所有文件

3


=B2.run(~=replace(~,source,target))

每个文件读入的内容执行替换动作

4


=file(A2).write(B3)

再将替换后的内容写出到原文件

3.3.        单词计数

对单词进行计数,需要先将句子拆分为独立的单词,SPL 提供了 words 函数用于单词拆分。代码实现:


A

B

1

=file(“novel.txt”).read()

读入给定文件的文本内容

2

=A1. words()

将内容拆分为单词序列

3

=A2.groups(lower(~):Word;count(~):Count)

将单词转为小写后,分组并计数

3.4.        字母计数

类似于单词计数,使用 split 函数可以将文本串直接拆分为独立的字母。代码实现:


A

B

1

=file(“novel.txt”).read()

读入给定文件的文本内容

2

=A1. split()

将内容拆分为单词序列

3

=A2.groups(~:Char;count(~):Count)

对字符分组并计数

3.5.        文本去重

1.2 节中文本为一些收集重复的 url 地址列表,使用 group 函数可以很方便地去除多余的 url 地址。代码实现:


A

B

1

=file("d:/urls.txt"))

打开指定文件

2

=A1.read@n()

按行读取文件内容为序列

3

=A2.group@1()

按序列成员分组

4

=A1.write(A3)

获得所有行号序列

A3 中 group 函数提供了丰富的选项,这里使用 @1 选项去除多余的 url 串。

4.  结构化解析

有些文本内容构成稍微复杂,里面既包含可以结构化的内容,也有许多非结构化信息。此时需要去除非结构化信息,解析出结构化数据,从而对这类文件(也称为半结构化文件)也可以进行 SQL 式计算。

4.         

4.1.        单行解析

如下为某软件日志文件 (QQLive.log) 的部分截图:

..

去除多余的中括号以及多余的字符后,每一行都可以解析成一条固定字段的记录。为了去除多余的字符,可以使用正则表达式和 SPL 的 regex 函数,实现代码如下:


A

B

1

\[(.*)\]\[(.*)\]-\[(.*)ms\]\[(.*)\](.*)

定义正则表达式

2

=file("D:/QQLive.log").read@n()

打开日志文件,按行将内容读成序列

3

=A2.regex(A1)

用序列的正则分析函数 regex,拆解出字段

解析后的结果如下图:

..

4.2.        多行解析

日志文件的内容本就多种多样,比如下列数据,由于调试信息的不固定,所以要解析一条记录,需要读取不固定的行数:

 

..

解析同一条记录,可以用左中括号来界定,这里用到 like 函数来作为分组条件,使得位于同一记录的多行可以分到相同组。代码实现如下:


A

B

1

=file("D:/raq.log").read@n()

打开日志文件,按行将内容读成序列

2

=A1.select(~!="")

过滤掉空行

3

=A2.group@i(like(~,"[*"))   .(~.concat())

按记录内容分组,且组内成员合并为串

4

\[(.*)\] ([A-Z]+):(.*)

定义正则表达式

5

=A3.regex(A4)

执行正则分析

分组函数 group 的选项非常丰富,这里用 @i 选项可以使得同一记录的后续行都能分到当前组。

解析后的结果如下图:

..

4.3.        固定行解析

下述这种固定行解析到记录的数据,可以直接使用 record 函数来填充数据。

..

实现代码如下:


A

B

1

=file("D:\\student.txt")

打开学生文件

2

=A1.read@n()

将数据按行读进序列

3

=A2.select(~!="")

去掉记录间的空行

4

=create(ID,Name,Age)

构造表结构

5

=A4.record(A3)

将 A3 序列填充到表结构

解析后的结果如图:

..

4.4.        非固定行解析

下列邮件信息,由于邮件内容的不固定,因此需要将不固定的数据行解析到一条邮件记录:

..


A

B

1

=file("D:\\mail.txt")

打开邮件文件

2

=A1.read@n().select(~!="")

导入序列并去掉空行

3

=A2.group@i(like(~,"Sender:*"))

每一个 Sender: 开头以及后续行为一组

4

=A3.new(~(2):Sender,~(4):Receiver,~(6):Date,~.to(8,).concat():Content)

摘出记录值,合并正文,创建新的结构表

 

类似于固定行解析,需要使用 group 的 @i 选项将邮件分组到一起。然后根据每一组中邮件正文起始于第 8 行,再用 to 函数将第 8 行后面的所有邮件正文设置到字段 Content。代码如下:

解析后的结果如图:

..

4.5.        复杂格式解析

下面为一个文本格式的客户报价单数据 item.txt,如图所示:

..

横线之前的行是复杂的表头,之后每一行是一条报价记录,记录之间有空行。图中所示只是一个表头和报价记录区,这样的区域在文本文件中会不断地重复出现。红框所示分别是 Unit Price 和 Exp. Date 字段列,中间还有 Quotation Number、Customer Code、Customer Name 字段列,各列数据之间都是空格。

       观察并发现文本中的规律,我们发现这个文本的规律为:

 (1)、少于 136 个字符的行都没有有效信息,可以跳过

 (2)、所需数据位于每行 59 列至 136 列

 (3)、把每行有效信息部分按空格为分隔符拆分,若第 1 个拆分值是数值类型,则此行是报价记录,否则可跳过。第 1 个拆分值是 Unit Price 列,第 2 个是 Quotation Number 列,第 3 个是 Customer Code 列,最后 1 个是 Contract Expiry Date 列,第 4 个至倒数第 2 个用空格连接起来是 Customer Name 列。

       理清规律后,用 SPL 实现的代码如下:


A

B

C

1

=create(Customer_Code,Customer_Name,Quotation_No,Unit_Price,Contract_Expiry_Date)



2

=file("D:/item.txt").read@n()



3

for   A2

if   len(A3)<136

next

4


=right(left(A3,136),-58)

=B4.split@tp()

5


if   !ifnumber(C4(1))

next

6


=C4.m(4:C4.len()-1).concat(" ")


7


>A1.insert(0,C4(3),B6,C4(2),C4(1),C4(C4.len()))


8

=file("D:/   item.xlsx").xlsexport@t(A1)



代码注解:

       A1   创建目标数据集

       A2   打开报价单文本文件 item.txt,读入文件内容,选项 @n 表示每一行读成一个字符串 

       A3   循环处理每一行文本,实施前面找出来的规律  B3C3   如果本行长度小于 136,则跳过此行 

       B4   提取本行数据的第 59 至 136 列 

       C4   对 B4 中提取出来的数据按空白符进行拆分,选项 t 表示拆分后去除两端的空白,选项 p 表示把拆分后的串解析成对应的数据类型 

       B5C5   如果 C4 拆分出的第一个值不是数值类型,则跳过此行 

       B6   将 C4 拆分出来的第 4 个值到倒数第二个值用空格连接成串 

       B7   将 C4 拆分出的第 3 个值、B6、C4 拆分出的第 2 个值、第 1 个值、最后一个值按顺序插入到 A1 的新记录中 

       A8   将所有提取的数据保存到 Excel 文件 item.xlsx