最近在编写一些bat工具时,发现自己对lua的io和正则表达式使用没什么了解,网络上转载解决问题后的文章。

函数原型 string.gsub(s, pat, repl [, n])

就是 global 全局替换子字符串的意思
s: 源字符串
pat: 即 pattern, 匹配模式
repl: replacement, 将 pat 匹配到的字串替换为 repl
[, n]: 可选, 表示只看源字符串的前 n 个字符

比如写一个 trim 函数:

[plain]  view plain  copy
  1. function trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1"))end  
  2. ----然后调用:  
  3. s='\t a bc d '  
  4. print(trim(s))  -----输出: a bc d, 开头的 \t, 结尾的空格 都被 trim 掉了  

这里有几点要解释:
1. return (string.gsub(...)), 注意 string.gsub 外面还有一层括号。 事实上, gsub 调用之后返回 两个值, 一个是替换后的字符串, 第二个是替换的次数。 而一旦外面加了括号, 就只返回第一个值, 即替换后的字符串。你可以尝试去掉外层括号, 看看输出了什么

2. 匹配模式字串 "^...$", 表示匹配的是整个字符串。 ^ 表开头, $ 表示结尾。在这里, (.-) 的效果跟 (.*) 的效果应该是一样的, 因为反正是从字串开头匹配到结尾。
如果去掉 ^跟$, 那么就不是匹配整个字串, 在 (.-) 的作用下, 输出就成了: abcd 连在一起


Lua ---- LFS库的使用

   lfs.attributes(filepath [, aname]) 获取路径指定属性
    lfs.chdir(path) 改变当前工作目录,成功返回true,失败返回nil加上错误信息
    lfs.currentdir 获取当前工作目录,成功返回路径,失败为nil加上错误信息
    lfs.dir(path) 返回一个迭代器(function)和一个目录(userdata),每次迭代器都会返回一个路径,直到不是文件目录为止,则迭代器返回nil
    lfs.lock(filehandle, mode[, start[, length]])
    lfs.mkdir(dirname)  创建一个新目录
    lfs.rmdir(dirname) 删除一个已存在的目录,成功返回true,失败返回nil加上错误信息



Lua 标准库 - 输入输出处理(input and output facilities)

责任编辑:cynthia作者: 来自ITPUB论坛    2008-02-18   
文本Tag:  Lua

  【IT168 技术文档】I/O库提供两种不同的方式进行文件处理

  1、io表调用方式:使用io表,io.open将返回指定文件的描述,并且所有的操作将围绕这个文件描述

  io表同样提供三种预定义的文件描述io.stdin,io.stdout,io.stderr

  2、文件句柄直接调用方式,即使用file:XXX()函数方式进行操作,其中file为io.open()返回的文件句柄

  多数I/O函数调用失败时返回nil加错误信息,有些函数成功时返回nil

  1、io.close ([file])

  功能:相当于file:close(),关闭默认的输出文件

  2、io.flush ()

  功能:相当于file:flush(),输出所有缓冲中的内容到默认输出文件

  3、io.lines ([filename])

  功能:打开指定的文件filename为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回nil,并自动关闭文件

  若不带参数时io.lines() <=> io.input():lines(); 读取默认输入设备的内容,但结束时不关闭文件

  如:for line in io.lines("main.lua") do

  print(line)

  end

  4、io.open (filename [, mode])

  功能:按指定的模式打开一个文件,成功则返回文件句柄,失败则返回nil+错误信息

  mode:

  "r": 读模式 (默认);

  "w": 写模式;

  "a": 添加模式; 

  "r+": 更新模式,所有之前的数据将被保存

  "w+": 更新模式,所有之前的数据将被清除

  "a+": 添加更新模式,所有之前的数据将被保存,只允许在文件尾进行添加

  "b": 某些系统支持二进制方式

  5、io.output ([file])

  功能:相当于io.input,但操作在默认输出文件上

  6、io.popen ([prog [, mode]])

  功能:开始程序prog于额外的进程,并返回用于prog的文件句柄(并不支持所有的系统平台)

  7、io.read (...)

  功能:相当于io.input():read

  8、io.tmpfile ()

  功能:返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除

  9、io.type (obj)

  功能:检测obj是否一个可用的文件句柄

  返回:

  "file":为一个打开的文件句柄

  "closed file":为一个已关闭的文件句柄

  nil:表示obj不是一个文件句柄

  10、io.write (...)

  功能:相当于io.output():write

  11、file:close()

  功能:关闭文件

  注:当文件句柄被垃圾收集后,文件将自动关闭。句柄将变为一个不可预知的值

  12、file:flush()

  功能:向文件写入缓冲中的所有数据

  13、file:lines()

  功能:返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回nil,但不关闭文件

  如:for line in file:lines() do body end

  14、file:read(...)

  功能:按指定的格式读取一个文件,按每个格式函数将返回一个字串或数字,如果不能正确读取将返回nil,若没有指定格式将指默认按行方式进行读取

  格式:

  "*n": 读取一个数字   (*number)

  "*a": 从当前位置读取整个文件,若为文件尾,则返回空字串  (可写  *all)

  "*l": [默认]读取下一行的内容,若为文件尾,则返回nil (*line)

  number: 读取指定字节数的字符,若为文件尾,则返回nil;如果number为0则返回空字串,若为文件尾,则返回nil; 

read函数从当前输入文件读取串,由它的参数控制读取的内容:

"*all"

读取整个文件

"*line"

读取下一行

"*number"

从串中转换出一个数值

num

读取num个字符到串


  15、file:seek([whence][,offset])

  功能:设置和获取当前文件位置,成功则返回最终的文件位置(按字节),失败则返回nil加错误信息

  参数

  whence:

  "set": 从文件头开始

  "cur": 从当前位置开始[默认]

  "end": 从文件尾开始

  offset:默认为0

  不带参数file:seek()则返回当前位置,file:seek("set")则定位到文件头,file:seek("end")则定位到文件尾并返回文件大小

  16、file:setvbuf(mode,[,size])

  功能:设置输出文件的缓冲模式

  参数

  mode:

  "no": 没有缓冲,即直接输出

  "full": 全缓冲,即当缓冲满后才进行输出操作(也可调用flush马上输出)

  "line": 以行为单位,进行输出(多用于终端设备)

  最后两种模式,size可以指定缓冲的大小(按字节),忽略size将自动调整为最佳的大小

  17、file:write(...)

  功能:按指定的参数格式输出文件内容,参数必须为字符或数字,若要输出其它值,则需通过tostring或string.format进行转换



I/O库为文件操作提供两种模式。简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法。简单模式在做一些简单的文件操作时较为合适。在本书的前面部 分我们一直都在使用它。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。I/O库的所有函数都放在表(table)io中。

21.1 简单I/O模式

简单模式的所有操作都是在两个当前文件之上。I/O库将当前输入文件作为标准输入(stdin),将当前输出文件作为标准输出(stdout)。这样当我们执行io.read,就是在标准输入中读取一行。我们可以使用io.input和io.output函数来改变当前文件。例如io.input(filename)就是打开给定文件(以读模式),并将其设置为当前输入文件。接下来所有的输入都来自于该文,直到再次使用io.input。io.output函数。类似于io.input。一旦产生错误两个函数都会产生错误。如果你想直接控制错误必须使用完全模式中io.read函数。写操作较读操作简单,我们先从写操作入手。下面这个例子里函数io.write获取任意数目的字符串参数,接着将它们写到当前的输出文件。通常数字转换为字符串是按照通常的规则,如果要控制这一转换,可以使用string库中的format函数:

> io.write("sin (3) = ", math.sin(3), "/n")

--> sin (3) = 0.1411200080598672

> io.write(string.format("sin (3) = %.4f/n", math.sin(3)))

--> sin (3) = 0.1411

在编写代码时应当避免像io.write(a..b..c);这样的书写,这同io.write(a,b,c)的效果是一样的。但是后者因为避免了串联操作,而消耗较少的资源。原则上当你进行粗略(quick and dirty)编程,或者进行排错时常使用print函数。当需要完全控制输出时使用write。

> print("hello", "Lua"); print("Hi")

--> hello   Lua

--> Hi

> io.write("hello", "Lua"); io.write("Hi", "/n")

--> helloLuaHi

Write函数与print函数不同在于,write不附加任何额外的字符到输出中去,例如制表符,换行符等等。还有write函数是使用当前输出文件,而print始终使用标准输出。另外print函数会自动调用参数的tostring方法,所以可以显示出表(tables)函数(functions)和nil。

read函数从当前输入文件读取串,由它的参数控制读取的内容:

"*all"

读取整个文件

"*line"

读取下一行

"*number"

从串中转换出一个数值

num

读取num个字符到串

io.read("*all")函数从当前位置读取整个输入文件。如果当前位置在文件末尾,或者文件为空,函数将返回空串。由于Lua对长串类型值的有效管理,在Lua中使用过滤器的简单方法就是读取整个文件到串中去,处理完之后(例如使用函数gsub),接着写到输出中去:

t = io.read("*all")         -- read the whole file

t = string.gsub(t, ...)     -- do the job

io.write(t)                 -- write the file

以下代码是一个完整的处理字符串的例子。文件的内容要使用MIME(多用途的网际邮件扩充协议)中的quoted-printable码进行编码。以这种形式编码,非ASCII字符将被编码为“=XX”,其中XX是该字符值的十六进制表示,为表示一致性“=”字符同样要求被改写。在gsub函数中的“模式”参数的作用就是得到所有值在128到255之间的字符,给它们加上等号标志。

t = io.read("*all")

t = string.gsub(t, "([/128-/255=])", function (c)

    return string.format("=X", string.byte(c))

end)

io.write(t)

该程序在奔腾333MHz环境下转换200k字符需要0.2秒。

io.read("*line")函数返回当前输入文件的下一行(不包含最后的换行符)。当到达文件末尾,返回值为nil(表示没有下一行可返回)。该读取方式是read函数的默认方式,所以可以简写为io.read()。通常使用这种方式读取文件是由于对文件的操作是自然逐行进行的,否则更倾向于使用*all一次读取整个文件,或者稍后见到的逐块的读取文件。下面的程序演示了应如何使用该模式读取文件。此程序复制当前输入文件到输出文件,并记录行数。

local count = 1

while true do

    local line = io.read()

    if line == nil then break end

    io.write(string.format("m ", count), line, "/n")

    count = count + 1

end

然而为了在整个文件中逐行迭代。我们最好使用io.lines迭代器。例如对文件的行进行排序的程序如下:

local lines = {}

-- read the lines in table 'lines'

for line in io.lines() do

    table.insert(lines, line)

end

-- sort

table.sort(lines)

-- write all the lines

for i, l in ipairs(lines) do io.write(l, "/n") end

在奔腾333MHz上该程序处理处理4.5MB大小,32K行的文件耗时1.8秒,比使用高度优化的C语言系统排序程序快0.6秒。io.read("*number")函数从当前输入文件中读取出一个数值。只有在该参数下read函数才返回数值,而不是字符串。当需要从一个文件中读取大量数字时,数字间的字符串为空白可以显著的提高执行性能。*number选项会跳过两个可被识别数字之间的任意空格。这些可识别的字符串可以是-3、+5.2、1000,和 -3.4e-23。如果在当前位置找不到一个数字(由于格式不对,或者是到了文件的结尾),则返回nil 可以对每个参数设置选项,函数将返回各自的结果。假如有一个文件每行包含三个数字:

6.0        -3.23         15e12

4.3        234           1000001

...

现在要打印出每行最大的一个数,就可以使用一次read函数调用来读取出每行的全部三个数字:

while true do

    local n1, n2, n3 = io.read("*number", "*number", "*number")

    if not n1 then break end

    print(math.max(n1, n2, n3))

end

在任何情况下,都应该考虑选择使用io.read函数的 " *.all " 选项读取整个文件,然后使用gfind函数来分解:

local pat = "(%S+)%s+(%S+)%s+(%S+)%s+"

for n1, n2, n3 in string.gfind(io.read("*all"), pat) do

    print(math.max(n1, n2, n3))

end

除了基本读取方式外,还可以将数值n作为read函数的参数。在这样的情况下read函数将尝试从输入文件中读取n个字符。如果无法读取到任何字符(已经到了文件末尾),函数返回nil。否则返回一个最多包含n个字符的串。以下是关于该read函数参数的一个进行高效文件复制的例子程序(当然是指在Lua中)

local size = 2^13        -- good buffer size (8K)

while true do

    local block = io.read(size)

    if not block then break end

    io.write(block)

end

特别的,io.read(0)函数的可以用来测试是否到达了文件末尾。如果不是返回一个空串,如果已是文件末尾返回nil。

21.2 完全I/O 模式

为了对输入输出的更全面的控制,可以使用完全模式。完全模式的核心在于文件句柄(file handle)。该结构类似于C语言中的文件流(FILE*),其呈现了一个打开的文件以及当前存取位置。打开一个文件的函数是io.open。它模仿C语言中的fopen函数,同样需要打开文件的文件名参数,打开模式的字符串参数。模式字符串可以是 "r"(读模式),"w"(写模式,对数据进行覆盖),或者是 "a"(附加模式)。并且字符 "b" 可附加在后面表示以二进制形式打开文件。正常情况下open函数返回一个文件的句柄。如果发生错误,则返回nil,以及一个错误信息和错误代码。

print(io.open("non-existent file", "r"))

    --> nil    No such file or directory       2

print(io.open("/etc/passwd", "w"))

    --> nil    Permission denied               13

错误代码的定义由系统决定。

以下是一段典型的检查错误的代码:

local f = assert(io.open(filename, mode))

如果open函数失败,错误信息作为assert的参数,由assert显示出信息。文件打开后就可以用read和write方法对他们进行读写操作。它们和io表的read/write函数类似,但是调用方法上不同,必须使用冒号字符,作为文件句柄的方法来调用。例如打开一个文件并全部读取。可以使用如下代码。

local f = assert(io.open(filename, "r"))

local t = f:read("*all")

f:close()

同C语言中的流(stream)设定类似,I/O库提供三种预定义的句柄:io.stdin、io.stdout和io.stderr。因此可以用如下代码直接发送信息到错误流(error stream)。

io.stderr:write(message)

我们还可以将完全模式和简单模式混合使用。使用没有任何参数的io.input()函数得到当前的输入文件句柄;使用带有参数的io.input(handle)函数设置当前的输入文件为handle句柄代表的输入文件。(同样的用法对于io.output函数也适用)例如要实现暂时的改变当前输入文件,可以使用如下代码:

local temp = io.input()     -- save current file

io.input("newinput")        -- open a new current file

...                         -- do something with new input

io.input():close()          -- close current file

io.input(temp)              -- restore previous current file

21.2.1 I/O优化的一个小技巧

由于通常Lua中读取整个文件要比一行一行的读取一个文件快的多。尽管我们有时候针对较大的文件(几十,几百兆),不可能把一次把它们读取出来。要处理这样的文件我们仍然可以一段一段(例如8kb一段)的读取它们。同时为了避免切割文件中的行,还要在每段后加上一行:

local lines, rest = f:read(BUFSIZE, "*line")

以上代码中的rest就保存了任何可能被段划分切断的行。然后再将段(chunk)和行接起来。这样每个段就是以一个完整的行结尾的了。以下代码就较为典型的使用了这一技巧。该段程序实现对输入文件的字符,单词,行数的计数。

local BUFSIZE = 2^13        -- 8K

local f = io.input(arg[1]) -- open input file

local cc, lc, wc = 0, 0, 0 -- char, line, and word counts

while true do

    local lines, rest = f:read(BUFSIZE, "*line")

    if not lines then break end

    if rest then lines = lines .. rest .. '/n' end

    cc = cc + string.len(lines)

    -- count words in the chunk

    local _,t = string.gsub(lines, "%S+", "")

    wc = wc + t

    -- count newlines in the chunk

    _,t = string.gsub(lines, "/n", "/n")

    lc = lc + t

end

print(lc, wc, cc)

21.2.2二进制文件

默认的简单模式总是以文本模式打开。在Unix中二进制文件和文本文件并没有区别,但是在如Windows这样的系统中,二进制文件必须以显式的标记来打开文件。控制这样的二进制文件,你必须将“b”标记添加在io.open函数的格式字符串参数中。在Lua中二进制文件的控制和文本类似。一个串可以包含任何字节值,库中几乎所有的函数都可以用来处理任意字节值。(你甚至可以对二进制的“串”进行模式比较,只要串中不存在0值。如果想要进行0值字节的匹配,你可以使用%z代替)这样使用*all模式就是读取整个文件的值,使用数字n就是读取n个字节的值。以下是一个将文本文件从DOS模式转换到Unix模式的简单程序。(这样转换过程就是将“回车换行字符”替换成“换行字符”。)因为是以二进制形式(原稿是Text Mode!!??)打开这些文件的,这里无法使用标准输入输入文件(stdin/stdout)。所以使用程序中提供的参数来得到输入、输出文件名。

local inp = assert(io.open(arg[1], "rb"))

local out = assert(io.open(arg[2], "wb"))

local data = inp:read("*all")

data = string.gsub(data, "/r/n", "/n")

out:write(data)

assert(out:close())

可以使用如下的命令行来调用该程序。

> lua prog.lua file.dos file.unix

第二个例子程序:打印在二进制文件中找到的所有特定字符串。该程序定义了一种最少拥有六个“有效字符”,以零字节值结尾的特定串。(本程序中“有效字符”定义为文本数字、标点符号和空格符,由变量validchars定义。)在程序中我们使用连接和string.rep函数创建validchars,以%z结尾来匹配串的零结尾。

local f = assert(io.open(arg[1], "rb"))

local data = f:read("*all")

local validchars = "[%w%p%s]"

local pattern = string.rep(validchars, 6) .. "+%z"

for w in string.gfind(data, pattern) do

    print(w)

end

最后一个例子:该程序对二进制文件进行一次值分析[6](Dump)。程序的第一个参数是输入文件名,输出为标准输出。其按照10字节为一段读取文件,将每一段各字节的十六进制表示显示出来。接着再以文本的形式写出该段,并将控制字符转换为点号。

local f = assert(io.open(arg[1], "rb"))

local block = 10

while true do

    local bytes = f:read(block)

    if not bytes then break end

    for b in string.gfind(bytes, ".") do

       io.write(string.format("X ", string.byte(b)))

    end

    io.write(string.rep("   ", block - string.len(bytes) + 1))

    io.write(string.gsub(bytes, "%c", "."), "/n")

end

如果以vip来命名该程序脚本文件。可以使用如下命令来执行该程序处理其自身:

prompt> lua vip vip

在Unix系统中它将会会产生一个如下的输出样式:

6C 6F 63 61 6C 20 66 20 3D 20      local f =

61 73 73 65 72 74 28 69 6F 2E      assert(io.

6F 70 65 6E 28 61 72 67 5B 31      open(arg[1

5D 2C 20 22 72 62 22 29 29 0A      ], "rb")).

              ...

22 25 63 22 2C 20 22 2E 22 29      "%c", ".")

2C 20 22 5C 6E 22 29 0A 65 6E      , "/n").en

64 0A                              d.

21.3 关于文件的其它操作

函数tmpfile函数用来返回零时文件的句柄,并且其打开模式为read/write模式。该零时文件在程序执行完后会自动进行清除。函数flush用来应用针对文件的所有修改。同write函数一样,该函数的调用既可以按函数调用的方法使用io.flush()来应用当前输出文件;也可以按文件句柄方法的样式f:flush()来应用文件f。函数seek用来得到和设置一个文件的当前存取位置。它的一般形式为filehandle:seek(whence,offset)。Whence参数是一个表示偏移方式的字符串。它可以是 "set",偏移值是从文件头开始;"cur",偏移值从当前位置开始;"end",偏移值从文件尾往前计数。offset即为偏移的数值,由whence的值和offset相结合得到新的文件读取位置。该位置是实际从文件开头计数的字节数。 whence的默认值为 "cur",offset的默认值为0。这样调用file:seek()得到的返回值就是文件当前的存取位置,且保持不变。file:seek("set")就是将文件的存取位置重设到文件开头。(返回值当然就是0)。而file:seek("end")就是将位置设为文件尾,同时就可以得到文件的大小。如下的代码实现了得到文件的大小而不改变存取位置。

function fsize (file)

    local current = file:seek()     -- get current position

    local size = file:seek("end")   -- get file size

    file:seek("set", current)       -- restore position

    return size

end

以上的几个函数在出错时都将返回一个包含了错误信息的nil值。



lua查找方法 find


函数原型 string.find(s, pattern [, init [, plain]] )
 s: 源字符串
 pattern: 待搜索模式串
 init: 可选, 起始位置
 plain: 我没用过
 
 ① 子串匹配:
 

[plain]  view plain  copy
  1. print(string.find("haha", 'ah') )  ----- 输出 2 3  


 注意: lua 里面数组或者字符串的字符, 其下标索引是从 1 开始, 不是 0
  string.find 默认情况下返回两个值, 即查找到的子串的 起止下标, 如果不存在匹配返回 nil。
 如果我们只想要 string.find 返回的第二个值, 可以使用 虚变量(即 下划线)
 

[plain]  view plain  copy
  1. _, q=string.find("haha", 'ah')  
  2.  print(q)  ----- 输出 3  


 
 ② 模式匹配:
 

[plain]  view plain  copy
  1. pair = " name = Anna "  
  2.  print(string.find(pair, "(%a+)%s*=%s*(%a+)") ---- 输出 2 12 name Anna  


 解释:
 如果 find 的第二个参数使用了某种匹配模式, 并且模式串里面带括号。 那么表示会“捕捉”括号括起来的模式匹配到的字符串。 捕捉, 当然会把他们作为返回值。这里捕捉了两下, 所以 find 多返回了两个值
 
 那么,
这个模式是怎么匹配的呢?
 Lua 支持的字符类有:
 
 .  任意字符
 %s 空白符
 %p 标点
 %c 控制字符
 %d 数字
 %x 十六进制数
 %z 代表0的字符
 %a 字母
 %l 小写字母
 %u 大写字母
 %w 字母数字
字符类的大写形式代表相应集合的补集, 比如 %A 表示除了字母以外的字符集
另外,* + - 三个,作为通配符分别表示:
*: 匹配前面指定的 0 或多个同类字符, 尽可能匹配更长的符合条件的字串
+: 匹配前面指定的 1 或多个同类字符, 尽可能匹配更长的符合条件的字串
-: 匹配前面指定的 0 或多个同类字符, 尽可能匹配更短的符合条件的字串

于是, "(%a+)%s*=%s*(%a+)" 表示, 先匹配一个或多个字母, 然后是零个或多个空白符(比如空格), 然后是个 '=', 然后空白符, 然后字母。这样, 满足匹配的只有 "name = Anna"。 所以输出位置为 2 12.
因为捕获了两个 (%a+), 也就是 name, Anna 这两个单词, 所以还输出了这两个单词

另外, lua 使用 %1-%9 表示拷贝捕获。举例说:

[plain]  view plain  copy
  1. s="abc \"it's a cat\""  
  2. _,_,_,q=string.find(s, "([\"'])(.-)%1")  
  3. print(q)  -----输出: it's a cat  


首先, [\"'] 表示匹配 双引号或者单引号, 因为有括号,所以引号被捕获。 然后匹配几个任意字符并且捕获他, 最后 %1 匹配与第一次捕获到的(即引号)相同的字串。所以整个模式匹配到的是 "it's a cat", 而第二次捕获的是去掉两头引号的字串, 即 it's a cat.

还有, '-' 与 '*' 到底有什么不同呢? 在上面, "([\"'])(.*)%1" 匹配到的结果与 '-' 是一样的。尽可能匹配更长, 尽可能匹配更短 究竟什么不同呢?看例子:

[plain]  view plain  copy
  1. print( ("\"hello\" \"hello\""):find('"(.+)"') )  ----输出 1 15 hello" "hello  
  2. print( ("\"hello\" \"hello\""):find('"(.-)"') )  ----输出 1 7 hello  


* 尽可能长, 所以匹配了首尾两个 引号, 捕获了中间的 (hello" "hello)
- 尽可能短, 所以碰到第二个引号就说匹配完了, 因此只捕获了第一个 (hello)



Lua正则表达式

. 任意字符
%s 空白符
%p 标点字符
%c 控制字符
%d 数字
%x 十六进制数字
%z 代表0的字符
%a 字母
%l 小写字母
%u 大写字母
%w 字母和数字
上面字符类的大写形式表示小写所代表的集合的补集。例如,'%A'非字母的字符:

模式修饰符
+ 匹配前一字符1次或多次
* 匹配前一字符0次或多次;最长匹配  -- 先尽可能长地把本次匹配模式走完,再继续下一个模式,
- 匹配前一字符0次或多次;最短匹配  -- 本次匹配模式每走一步,就考查下一个模式能否进入.
? 匹配前一字符0次或1次
^ 匹配字符串开头
$ 匹配字符串结尾


模式串中的特殊字符
( ) . % + - * ? [ ^ $
'%' 用作特殊字符的转义字符
'%.' 匹配点;
'%%' 匹配字符 '%'。
转义字符 '%'不仅可以用来转义特殊字符,还可以用于所有的非字母的字符。当对一个字符有疑问的时候,为安全起见请使用转义字符转义他。
用'[]'创建字符集
'[%w_]' 匹配字母数字和下划线
'[01]' 匹配二进制数字
'[%[%]]'匹配一对方括号
在'[]'中使用连字符'-'
'%d'    表示 '[0-9]';
'%x'    表示 '[0-9a-fA-F]'
'[0-7]' 表示 '[01234567]'
在'[]'开始处使用 '^' 表示其补集:
'[^0-7]' 匹配任何不是八进制数字的字符;
'[^\n]' 匹配任何非换行符户的字符。
'[^%s]' == '%S'


-----------------------------------------------------------------------------------------------------------------------------

捕获

捕获是这样一种机制:可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个捕获。
string.find使用捕获的时候,函数会返回捕获的值作为额外的结果。这常被用来将一个目标串拆分成多个:

pair = "name = Anna"

_, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
print(key, value)    --> name  Anna

'%a+' 表示菲空的字母序列;'%s*' 表示0个或多个空白。在上面的例子中,整个模式代表:一个字母序列,后面是任意多个空白,然后是 '=' 再后面是任意多个空白,然后是一个字母序列。两个字母序列都是使用圆括号括起来的子模式,当他们被匹配的时候,他们就会被捕获。当匹配发生的时候,find函数总是先返回匹配串的索引下标(上面例子中我们存储哑元变量 _ 中),然后返回子模式匹配的捕获部分。下面的例子情况类似:

date = "17/7/1990"
_, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
print(d, m, y)      --> 17 7 1990  
  

我们可以在模式中使用向前引用,'%d'(d代表1-9的数字)表示第d个捕获的拷贝。
看个例子,假定你想查找一个字符串中单引号或者双引号引起来的子串,你可能使用模式 '["'].-["']',但是这个模式对处理类似字符串 "it's all right" 会出问题。为了解决这个问题,可以使用向前引用,使用捕获的第一个引号来表示第二个引号:

s = [[then he said: "it's all right"!]]
a, b, c, quotedPart = string.find(s, "(["'])(.-)%1")
print(quotedPart)    --> it's all right
print(c)            --> "  


第一个捕获是引号字符本身,第二个捕获是引号中间的内容('.-' 匹配引号中间的子串)。

捕获值的第三个应用是用在函数gsub中。与其他模式一样,gsub的替换串可以包含 '%d',当替换发生时他被转换为对应的捕获值。(顺便说一下,由于存在这些情况,替换串中的字符 '%' 必须用 "%%" 表示)。下面例子中,对一个字符串中的每一个字母进行复制,并用连字符将复制的字母和原字母连接起来:

print(string.gsub("hello Lua!", "(%a)", "%1-%1"))
    --> h-he-el-ll-lo-o L-Lu-ua-a!  

下面代码互换相邻的字符:

print(string.gsub("hello Lua", "(.)(.)", "%2%1"))
    --> ehll ouLa  


让我们看一个更有用的例子,写一个格式转换器:从命令行获取LaTeX风格的字符串,形如:
\command{some text}
将它们转换为XML风格的字符串:
<command>some text</command>
对于这种情况,下面的代码可以实现这个功能:
s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")  
比如,如果字符串s为:
the \quote{task} is to \em{change} that.
调用gsub之后,转换为:
the <quote>task</quote> is to change that.

另一个有用的例子是去除字符串首尾的空格:  
function trim (s)
    return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end  


注意模式串的用法,两个定位符('^' 和 '$')保证我们获取的是整个字符串。因为,两个 '%s*' 匹配首尾的所有空格,'.-' 匹配剩余部分。还有一点需要注意的是gsub返回两个值,我们使用额外的圆括号丢弃多余的结果(替换发生的次数)。

最后一个捕获值应用之处可能是功能最强大的。我们可以使用一个函数作为string.gsub的第三个参数调用gsub。在这种情况下,string.gsub每次发现一个匹配的时候就会调用给定的作为参数的函数,捕获值可以作为被调用的这个函数的参数,而这个函数的返回值作为gsub的替换串。先看一个简单的例子,下面的代码将一个字符串中全局变量$varname出现的地方替换为变量varname的值:

function expand (s)
    s = string.gsub(s, "$(%w+)", function (n)
      return _G[n]
    end)
    return s
end

name = "Lua"; status = "great"
print(expand("$name is $status, isn't it?"))

--> Lua is great, isn't it?

如果你不能确定给定的变量是否为string类型,可以使用tostring进行转换:

function expand (s)
    return (string.gsub(s, "$(%w+)", function (n)
      return tostring(_G[n])
    end))
end

print(expand("print = $print; a = $a"))

--> print = function: 0x8050ce0; a = nil


下面是一个稍微复杂点的例子,使用loadstring来计算一段文本内$后面跟着一对方括号内表达式的值:

s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]"
print((string.gsub(s, "$(%b[])", function (x)
    x = "return " .. string.sub(x, 2, -2)
    local f = loadstring(x)
    return f()
end)))

--> sin(3) = 0.1411200080598672; 2^5 = 32

第一次匹配是 "$[math.sin(3)]",对应的捕获为 "[math.sin(3)]",调用string.sub去掉首尾的方括号,
所以被加载执行的字符串是 "return math.sin(3)","$[2^5]" 的匹配情况类似。
我们常常需要使用string.gsub遍历字符串,而对返回结果不感兴趣。比如,我们收集一个字符串中所有的单词,然后插入到一个表中:

words = {}
string.gsub(s, "(%a+)", function (w)
    table.insert(words, w)
end)

如果字符串s为 "hello hi, again!",上面代码的结果将是:

{"hello", "hi", "again"}

使用string.gfind函数可以简化上面的代码:

words = {}
for w in string.gfind(s, "(%a)") do
    table.insert(words, w)
end


gfind函数比较适合用于范性for循环。他可以遍历一个字符串内所有匹配模式的子串。我们可以进一步的简化上面的代码,调用gfind函数的时候,如果不显示的指定捕获,函数将捕获整个匹配模式。所以,上面代码可以简化为:

words = {}
for w in string.gfind(s, "%a") do
    table.insert(words, w)
end

下面的例子我们使用URL编码,URL编码是HTTP协议来用发送URL中的参数进行的编码。这种编码将一些特殊字符(比如 '='、'&'、'+')转换为 "%XX" 形式的编码,其中XX是字符的16进制表示,然后将空白转换成 '+'。比如,将字符串 "a+b = c" 编码为 "a%2Bb+%3D+c"。最后,将参数名和参数值之间加一个 '=';在name=value对之间加一个 "&"。比如字符串:
name = "al"; query = "a+b = c";  q="yes or no"
被编码为:
name=al&query=a%2Bb+%3D+c&q=yes+or+no
现在,假如我们想将这URL解码并把每个值存储到表中,下标为对应的名字。下面的函数实现了解码功能:

function unescape (s)
    s = string.gsub(s, "+", " ")
    s = string.gsub(s, "%%(%x%x)", function (h)
      return string.char(tonumber(h, 16))
    end)
    return s
end


第一个语句将 '+' 转换成空白,第二个gsub匹配所有的 '%' 后跟两个数字的16进制数,然后调用一个匿名函数,匿名函数将16进制数转换成一个数字(tonumber在16进制情况下使用的)然后再转化为对应的字符。比如:

print(unescape("a%2Bb+%3D+c"))    --> a+b = c

对于name=value对,我们使用gfind解码,因为names和values都不能包含 '&' 和 '='我们可以用模式 '[^&=]+' 匹配他们:

cgi = {}
function decode (s)
    for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do
      name = unescape(name)
      value = unescape(value)
      cgi[name] = value
    end
end


调用gfind函数匹配所有的name=value对,对于每一个name=value对,迭代子将其相对应的捕获的值返回给变量name和value。循环体内调用unescape函数解码name和value部分,并将其存储到cgi表中。
与解码对应的编码也很容易实现。首先,我们写一个escape函数,这个函数将所有的特殊字符转换成 '%' 后跟字符对应的ASCII码转换成两位的16进制数字(不足两位,前面补0),然后将空白转换为 '+':

function escape (s)
    s = string.gsub(s, "([&=+%c])", function (c)
      return string.format("%%%02X", string.byte(c))
    end)
    s = string.gsub(s, " ", "+")
    return s
end

编码函数遍历要被编码的表,构造最终的结果串:

function encode (t)
    local s = ""
    for k,v in pairs(t) do
      s = s .. "&" .. escape(k) .. "=" .. escape(v)
    end
    return string.sub(s, 2)    -- remove first `&'
end
t = {name = "al", query = "a+b = c", q="yes or no"}
print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al

转换的技巧(Tricks of the Trade)
模式匹配对于字符串操纵来说是强大的工具,你可能只需要简单的调用string.gsub和find就可以完成复杂的操作,然而,因为它功能强大你必须谨慎的使用它,否则会带来意想不到的结果。
对正常的解析器而言,模式匹配不是一个替代品。对于一个quick-and-dirty程序,你可以在源代码上进行一些有用的操作,但很难完成一个高质量的产品。前面提到的匹配C程序中注释的模式是个很好的例子:'/%*.-%*/'。如果你的程序有一个字符串包含了"/*",最终你将得到错误的结果:

test = [[char s[] = "a /* here"; /* a tricky string */]]
print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
    --> char s[] = "a <COMMENT>


虽然这样内容的字符串很罕见,如果是你自己使用的话上面的模式可能还凑活。但你不能将一个带有这种毛病的程序作为产品出售。
一般情况下,Lua中的模式匹配效率是不错的:一个奔腾333MHz机器在一个有200K字符的文本内匹配所有的单词(30K的单词)只需要1/10秒。但是你不能掉以轻心,应该一直对不同的情况特殊对待,尽可能的更明确的模式描述。一个限制宽松的模式比限制严格的模式可能慢很多。一个极端的例子是模式 '(.-)%$' 用来获取一个字符串内$符号以前所有的字符,如果目标串中存在$符号,没有什么问题;但是如果目标串中不存在$符号。上面的算法会首先从目标串的第一个字符开始进行匹配,遍历整个字符串之后没有找到$符号,然后从目标串的第二个字符开始进行匹配,……这将花费原来平方次幂的时间,导致在一个奔腾333MHz的机器中需要3个多小时来处理一个200K的文本串。可以使用下面这个模式避免上面的问题 '^(.-)%$'。定位符^告诉算法如果在第一个位置没有没找到匹配的子串就停止查找。使用这个定位符之后,同样的环境也只需要不到1/10秒的时间。
也需要小心空模式:匹配空串的模式。比如,如果你打算用模式 '%a*' 匹配名字,你会发现到处都是名字:

i, j = string.find(";$% **#$hello13", "%a*")
print(i,j)    --> 1 0


这个例子中调用string.find正确的在目标串的开始处匹配了空字符。永远不要写一个以 '-' 开头或者结尾的模式,因为它将匹配空串。这个修饰符得周围总是需要一些东西来定位他的扩展。相似的,一个包含 '.*' 的模式是一个需要注意的,因为这个结构可能会比你预算的扩展的要多。
有时候,使用Lua本身构造模式是很有用的。看一个例子,我们查找一个文本中行字符大于70个的行,也就是匹配一个非换行符之前有70个字符的行。我们使用字符类'[^\n]'表示非换行符的字符。所以,我们可以使用这样一个模式来满足我们的需要:重复匹配单个字符的模式70次,后面跟着一个匹配一个字符0次或多次的模式。我们不手工来写这个最终的模式,而使用函数string.rep:

pattern = string.rep("[^\n]", 70) .. "[^\n]*"

另一个例子,假如你想进行一个大小写无关的查找。方法之一是将任何一个字符x变为字符类 '[xX]'。我们也可以使用一个函数进行自动转换:

function nocase (s)
    s = string.gsub(s, "%a", function (c)
      return string.format("[%s%s]", string.lower(c),
                                          string.upper(c))
    end)
    return s
end

print(nocase("Hi there!"))
    --> [hH][iI] [tT][hH][eE][rR][eE]!


有时候你可能想要将字符串s1转化为s2,而不关心其中的特殊字符。如果字符串s1和s2都是字符串序列,你可以给其中的特殊字符加上转义字符来实现。但是如果这些字符串是变量呢,你可以使用gsub来完成这种转义:

s1 = string.gsub(s1, "(%W)", "%%%1")
s2 = string.gsub(s2, "%%", "%%%%")


在查找串中,我们转义了所有的非字母的字符。在替换串中,我们只转义了 '%' 。另一个对模式匹配而言有用的技术是在进行真正处理之前,对目标串先进行预处理。一个预处理的简单例子是,将一段文本内的双引号内的字符串转换为大写,但是要注意双引号之间可以包含转义的引号("""):
这是一个典型的字符串例子:
"This is "great"!".
我们处理这种情况的方法是,预处理文本把有问题的字符序列转换成其他的格式。比如,我们可以将 """ 编码为 "\1",但是如果原始的文本中包含 "\1",我们又陷入麻烦之中。一个避免这个问题的简单的方法是将所有 "\x" 类型的编码为 "\ddd",其中ddd是字符x的十进制表示:

function code (s)
    return (string.gsub(s, "\\(.)", function (x)
      return string.format("\\%03d", string.byte(x))
    end))
end


注意,原始串中的 "\ddd" 也会被编码,解码是很容易的:

function decode (s)
    return (string.gsub(s, "\\(%d%d%d)", function (d)
      return "\" .. string.char(d)
    end))
end


如果被编码的串不包含任何转义符,我们可以简单的使用 ' ".-" ' 来查找双引号字符串:

s = [[follows a typical string: "This is "great"!".]]
s = code(s)
s = string.gsub(s, '(".-")', string.upper)
s = decode(s)
print(s)
    --> follows a typical string: "THIS IS "GREAT"!".


更紧缩的形式:

print(decode(string.gsub(code(s), '(".-")', string.upper)))

我们回到前面的一个例子,转换\command{string}这种格式的命令为XML风格:
<command>string</command>
但是这一次我们原始的格式中可以包含反斜杠作为转义符,这样就可以使用"\"、"\{" 和 "\}",分别表示 '\'、'{' 和 '}'。为了避免命令和转义的字符混合在一起,我们应该首先将原始串中的这些特殊序列重新编码,然而,与上面的一个例子不同的是,我们不能转义所有的 \x,因为这样会将我们的命令(\command)也转换掉。这里,我们仅当x不是字符的时候才对 \x 进行编码:

function code (s)
    return (string.gsub(s, '\\(%A)', function (x)
      return string.format(\\%03d ", string.byte(x))
    end))
end


解码部分和上面那个例子类似,但是在最终的字符串中不包含反斜杠,所以我们可直接调用string.char:

function decode (s)
    return (string.gsub(s, '\\(%d%d%d)', string.char))
end

s = [[a \emph{command} is written as \\ command\{text\}.]]
s = code(s)
s = string.gsub(s, "\\ (%a+){(.-)}", "<%1>%2</%1>")

print(decode(s))
--> a <emph>command</emph> is written as \command{text}.


我们最后一个例子是处理CSV(逗号分割)的文件,很多程序都使用这种格式的文本,比如Microsoft Excel。CSV文件十多条记录的列表,每一条记录一行,一行内值与值之间逗号分割,如果一个值内也包含逗号这个值必须用双引号引起来,如果值内还包含双引号,需使用双引号转义双引号(就是两个双引号表示一个),看例子,下面的数组:

{'a b', 'a,b', 'a,"b"c', 'hello "world"!', }
可以看作为:
a b,"a,b"," a,""b""c", hello "world"!,
将一个字符串数组转换为CSV格式的文件是非常容易的。我们要做的只是使用逗号将所有的字符串连接起来:

function toCSV (t)
    local s = ""
    for _,p in pairs(t) do
      s = s .. "," .. escapeCSV(p)
    end
    return string.sub(s, 2)    -- remove first comma
end

如果一个字符串包含逗号活着引号在里面,我们需要使用引号将这个字符串引起来,并转义原始的引号:

function escapeCSV (s)
    if string.find(s, '[,"]') then
      s = '"' .. string.gsub(s, '"', '""') .. '"'
    end
    return s
end


将CSV文件内容存放到一个数组中稍微有点难度,因为我们必须区分出位于引号中间的逗号和分割域的逗号。我们可以设法转义位于引号中间的逗号,然而并不是所有的引号都是作为引号存在,只有在逗号之后的引号才是一对引号的开始的那一个。只有不在引号中间的逗号才是真正的逗号。这里面有太多的细节需要注意,比如,两个引号可能表示单个引号,可能表示两个引号,还有可能表示空:
"hello""hello", "",""
这个例子中,第一个域是字符串 "hello"hello",第二个域是字符串 " """(也就是一个空白加两个引号),最后一个域是一个空串。
我们可以多次调用gsub来处理这些情况,但是对于这个任务使用传统的循环(在每个域上循环)来处理更有效。循环体的主要任务是查找下一个逗号;并将域的内容存放到一个表中。对于每一个域,我们循环查找封闭的引号。循环内使用模式 ' "("?) ' 来查找一个域的封闭的引号:如果一个引号后跟着一个引号,第二个引号将被捕获并赋给一个变量c,意味着这仍然不是一个封闭的引号

function fromCSV (s)
    s = s .. ','      -- ending comma
    local t = {}      -- table to collect fields
    local fieldstart = 1
    repeat
      -- next field is quoted? (start with `"'?)
      if string.find(s, '^"', fieldstart) then
      local a, c
      local i = fieldstart
      repeat
          -- find closing quote
          a, i, c = string.find(s, '"("?)', i+1)
      until c ~= '"'    -- quote not followed by quote?
      if not i then error('unmatched "') end
          local f = string.sub(s, fieldstart+1, i-1)
          table.insert(t, (string.gsub(f, '""', '"')))
          fieldstart = string.find(s, ',', i) + 1
      else              -- unquoted; find next comma
          local nexti = string.find(s, ',', fieldstart)
          table.insert(t, string.sub(s, fieldstart,
                                              nexti-1))
          fieldstart = nexti + 1
      end
    until fieldstart > string.len(s)
    return t
end

t = fromCSV('"hello "" hello", "",""')
for i, s in ipairs(t) do print(i, s) end
    --> 1      hello " hello
    --> 2        ""
    --> 3



  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值