Lua中的正则表达式及源码分析
来源:互联网 发布:叮叮摩卡软件 编辑:程序博客网 时间:2024/06/06 12:52
利用lua进行正则匹配
正则表达式通常用于两种任务:1.验证,2.搜索/替换。不同语言有不同但类似的匹配机制,可能在判断首尾的方式和符号用法上有略微差异。
正则匹配不好理解,必须有大量的尝试和试验,在此我尽量提供不同类型的用法例子方便理解。
lua中支持的字符类
[set] :代表一个自定义的字符集合。你可以使用符号 - 来标识一个范围,比如 1-9,a-z 之类的。需要注意的是,上面提到的那些字符集合也可以在这个自定义的集合里用,但是你不能这么写[%a-z],这样的集合是没有意义的。
[^set] :代表字符集合[set]的补集(有时候挺好用的)。
另外,对于上面提到的所有用 % 跟一个字母组成的集合,如果把字母大写,那么就对应那个集合的补集,比如 %S 的意思就是所有非空白字符。Lua官网还强调了一下,这里个定义跟本地的字符集有关(语言字符排序可能不同),比如集合 [a-z] 就不一定跟 %l 是相等的。
lua中内置string库中用到模式的地方有四个函数
- string.find(s, pattern, start, plain)
- string.match(s, pattern, start)
- string.gmatch(s, pattern)
- string.gsub(s, pattern, rep, n)
1.string.find(s, pattern, start, plain)
s:源字符串
pattern:匹配模式
start:起始位置
plain:pattern中的字符是否使用特殊字符
这个函数的功能是查找字符串 s 中的指定的模式 pattern。
如果找到了一个模式的匹配,就返回找到的模式在 s 中的起点和终点;否则返回 nil。这里需要注意的是,它只会返回找到的第一个匹配的位置,所以找到了的返回值其实是两个数字,匹配的起点、匹配的终点。
第三个参数是个数字,它是可选的,start 指定在 s 中查找开始的位置,默认是 1,start可以是负数,-1 代表从最后一个字符开始,-2 代表倒数第二个字符开始。当然,最后都是到最后一个字符结束,所以如果你指定位置从最后一个字符开始,那么就只会查找这一个字符。
第四个参数是个 bool 值,它指明第二个参数 pattern 中是否使用特殊字符,如果第四个参数指明为 true,那么就意味着第二个参数 pattern 中的那些特殊字符(这些字符有 ^$+?.([%-* ,定义在Lua源码 lstrlib.c 中)都被当作正常字符进行处理,也就是一个简单的字符串匹配,而不过所谓的模式匹配,也就是不动用正则表达式的匹配。相反,false 就意味着 pattern 采用特殊字符处理。这样说也不太明了,举个例子就明白了,不过要涉及到一个Lua模式中特殊的字符,如果这里还是不明白,看了后面我关于Lua正则表达式的介绍应该就能明白。
比如:
local s = "a+"print(string.find(s, "a+", 1, false)) -- 1 1print(string.find(s, "a+", 1, true)) -- 1 2
如果你不传第四个参数,默认为false。
这里介绍下捕获的概念:
Lua为这个模式增加了一个新的功能,也就是所谓的捕获,在一个模式串中,我们可以用小括号()来标明一些我们想要保存的匹配,而这个小括号中的内容依然是模式串,也就是说我们只不过是把模式中一些我们想要的特殊字符保留下来供后面使用。比如上面那个例子中的模式串是 m+ ,如果我想要把跟 m+ 匹配的字符串捕获出来,也就是保存下来,我可以用一个小括号把它括起来,而 find 函数除了上面说到的行为外,也就是除了返回查找到 pattern 的起止位置外,还会返回所有要求捕获的字符串,像这样:
local s = "tests"print(string.find(s, "(s+)", 1, false)) -- 3 5 s
如果你想要捕获更多的内容,只需要用小括号把它括起来就好了,比如这样:
local s = "test"print(string.find(s, "((s+))", 1, false)) -- 3 3 s sprint(string.find(s, "(((s+)))", 1, false)) -- 3 3 s s s
关于捕获还有一点需要说明的,就是捕获只会在模式能够匹配成功的时候才会跟着 string 的函数进行返回,比如下面这个,我想捕获字母 a ,但事实上这个模式根本无法匹配到,所以肯定是无法返回的:
local s = "test"print(string.find(s, "(s+)(m)", 1, false)) -- nil
另外捕获返回的顺序,是依照左小括号的位置来定的,比如上面那个捕获了3个 m 的例子,第一个 m 其实是最外层的小括号捕获到的。为什么要提到捕获的顺序呢?因为我们可以使用 %n 来取得第n个捕获的字符串,至于取得对应的捕获有什么用处呢?这个在后面会介绍到。
一个空的捕获,也就是小括号里面什么内容也没有,它会返回当前字符串的比较操作进行到的位置,比如
local s = "test"print(string.find(s, "()(s+)()", 1, false)) -- 3 3 3 s 4
有一点也必须要提一下,就是在Lua5.1的源码当中,捕获字符串的数量是有限制的,默认是32个,也就是说你添加的小括号不能无限加,最多加32个。如果捕获超过限制,当然会报错了,比如:
local s = "test"print(string.find(s, "()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()", 1, false)) -- 捕获33个
当然你可以通过修改Lua的源码来调整你想要保存的捕获数量,这个数量定义在 luaconf.h 文件中:
#define LUA_MAXCAPTURES 32
Lua内置库的加载方式就不说了,源码在官网可以下载,我们直接来看 string.find() 这个函数,函数在 lstrlib.c 文件里:
static int str_find (lua_State *L) { return str_find_aux(L, 1);//L的第四个参数,默认为1,即true}typedef struct MatchState { const char *src_init; /* init of source string */ const char *src_end; /* end (`\0') of source string */ lua_State *L; int level; /* total number of captures (finished or unfinished) */ struct { const char *init; ptrdiff_t len; } capture[LUA_MAXCAPTURES];} MatchState;static int str_find_aux (lua_State *L, int find) { size_t l1, l2; const char *s = luaL_checklstring(L, 1, &l1);//获取源数据字符串 const char *p = luaL_checklstring(L, 2, &l2);//获取模式字符串 ptrdiff_t init = posrelat(luaL_optinteger(L, 3, 1), l1) - 1;//获取开始匹配的位置 if (init < 0) init = 0; else if ((size_t)(init) > l1) init = (ptrdiff_t)l1;//若起始位置比源字符串长度还大,则起始位置置为末尾 if (find && (lua_toboolean(L, 4) || /* explicit request? *///L的第四个参数,默认传1 strpbrk(p, SPECIALS) == NULL)) { /* or no special characters? *///SPECIALS为特殊转义字符 /* do a plain search */ const char *s2 = lmemfind(s+init, l1-init, p, l2);//源字符串查找的起始指针,查找长度,模式字符串起始指针,模式长度 if (s2) { lua_pushinteger(L, s2-s+1); lua_pushinteger(L, s2-s+l2);//输出匹配到的起始位置和结束位置,计算方法是指针的位置差 return 2; } } else { MatchState ms;//结构体MatchState用来保存当前匹配的情况,在用递归方法解决某个问题时,引入这样一个结构体,函数调用之间通过这个结构体来传递信息是非常方便的 int anchor = (*p == '^') ? (p++, 1) : 0;//用来标识是否只会匹配目标字符串的开头部分,准确的讲是指定位置算为开头位置 const char *s1=s+init;//目标字符串开始匹配的位置 ms.L = L; ms.src_init = s; ms.src_end = s+l1; do { const char *res; ms.level = 0; if ((res=match(&ms, s1, p)) != NULL) {//调用match函数 if (find) { lua_pushinteger(L, s1-s+1); /* start */ lua_pushinteger(L, res-s); /* end */ return push_captures(&ms, NULL, 0) + 2;//同时返回需要捕获到的内容 } else return push_captures(&ms, s1, res); } } while (s1++ < ms.src_end && !anchor); } lua_pushnil(L); /* not found *///匹配不到,返回空 return 1;}static const char *lmemfind (const char *s1, size_t l1, const char *s2, size_t l2) { if (l2 == 0) return s1; /* empty strings are everywhere */ else if (l2 > l1) return NULL; /* avoids a negative `l1' */ else { const char *init; /* to search for a `*s2' inside `s1' */ l2--; /* 1st char will be checked by `memchr' */ l1 = l1-l2; /* `s2' cannot be found after that */ while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) {//memchr返回值为*s2在由s1指向的内存中第一次出现的位置,查找过程中是按字节比对的,只比较l1个字节 init++; /* 1st char is already checked */ if (memcmp(init, s2+1, l2) == 0) return init-1; else { /* correct `l1' and `s1' to try again */ l1 -= init-s1; s1 = init; } } return NULL; /* not found */ }}
这个函数初步看起来还是比较长的,但是仔细分析一下就发现其实是很简单的。前面那 6 行,就是接收前 3 个参数罢了,只不过处理了一下那个查找起始点参数,防止了超出字符串长度。最关键的地方就是紧接着的 if else 逻辑,find 是传进来的参数,而它的 find 参数就是传递的 0 。难道 string.match 函数其实跟 string.find 函数是一样的?
static int str_match (lua_State *L) { return str_find_aux(L, 0);}static int str_find (lua_State *L) { return str_find_aux(L, 1);}
这个留到介绍 string.match 函数的时候再说。拉回来,继续谈这个 if else 逻辑,if 的判断条件其实就是看你调用 string.find 的第四个参数,如果第四个参数传递了 true,也就是我上面说的,不使用特殊字符模式,或者是模式中压根就没有特殊字符,那个 SPECIALS 宏同样定义在这个文件中:
#define L_ESC '%'#define SPECIALS "^$*+?.(%-"
如果没有这些字符或者是不对这些字符特殊处理,那么就是一个简单的字符串匹配,调用 lmemfind() 函数,如果找到了,就返回了匹配到的起止位置。
既然如此,那么 else 里就好理解了,它就是使用特殊字符进行匹配的处理,这里的关键函数是 match(),它处理字符串和模式进行匹配,并进行了捕获,这个留到介绍模式的时候再接着说。最后如果匹配到了,那么仍然返回匹配起止点,注意,这里多了一个操作,就是把捕获到的字符串也压入了栈。所以我们调用并捕获的时候才会有后面那些捕获的字符串。
这么看来还是挺好理解的嘛。在好奇心的趋势下,我非常感兴趣,Lua的那个 match()函数是如何进行字符串匹配的,难道是传说中的 KMP又或者是BM算法?如果不熟悉这两种算法的童鞋,可以看看阮一峰的这两篇文章:
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
我们还是看了眼里面的内容:
static const char *lmemfind (const char *s1, size_t l1, const char *s2, size_t l2) { if (l2 == 0) return s1; /* empty strings are everywhere */ else if (l2 > l1) return NULL; /* avoids a negative `l1' */ else { const char *init; /* to search for a `*s2' inside `s1' */ l2--; /* 1st char will be checked by `memchr' */ l1 = l1-l2; /* `s2' cannot be found after that */ while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) {//memchr返回值为*s2在由s1指向的内存中第一次出现的位置,查找过程中是按字节比对的,只比较l1个字节 init++; /* 1st char is already checked */ if (memcmp(init, s2+1, l2) == 0) return init-1; else { /* correct `l1' and `s1' to try again */ l1 -= init-s1; s1 = init; } } return NULL; /* not found */ }}
static const char *match (MatchState *ms, const char *s, const char *p) { init: /* using goto's to optimize tail recursion */ switch (*p) { case '(': { /* start capture */ if (*(p+1) == ')') /* position capture? */ return start_capture(ms, s, p+2, CAP_POSITION); else return start_capture(ms, s, p+1, CAP_UNFINISHED); } case ')': { /* end capture */ return end_capture(ms, s, p+1); } case L_ESC: { switch (*(p+1)) { case 'b': { /* balanced string? */ s = matchbalance(ms, s, p+2); if (s == NULL) return NULL; p+=4; goto init; /* else return match(ms, s, p+4); */ } case 'f': { /* frontier? */ const char *ep; char previous; p += 2; if (*p != '[') luaL_error(ms->L, "missing " LUA_QL("[") " after " LUA_QL("%%f") " in pattern"); ep = classend(ms, p); /* points to what is next */ previous = (s == ms->src_init) ? '\0' : *(s-1); if (matchbracketclass(uchar(previous), p, ep-1) || !matchbracketclass(uchar(*s), p, ep-1)) return NULL; p=ep; goto init; /* else return match(ms, s, ep); */ } default: { if (isdigit(uchar(*(p+1)))) { /* capture results (%0-%9)? */ s = match_capture(ms, s, uchar(*(p+1))); if (s == NULL) return NULL; p+=2; goto init; /* else return match(ms, s, p+2) */ } goto dflt; /* case default */ } } } case '\0': { /* end of pattern */ return s; /* match succeeded */ } case '$': { if (*(p+1) == '\0') /* is the `$' the last char in pattern? */ return (s == ms->src_end) ? s : NULL; /* check end of string */ else goto dflt; } default: dflt: { /* it is a pattern item */ const char *ep = classend(ms, p); /* points to what is next */ int m = s<ms->src_end && singlematch(uchar(*s), p, ep); switch (*ep) { case '?': { /* optional */ const char *res; if (m && ((res=match(ms, s+1, ep+1)) != NULL)) return res; p=ep+1; goto init; /* else return match(ms, s, ep+1); */ } case '*': { /* 0 or more repetitions */ return max_expand(ms, s, p, ep); } case '+': { /* 1 or more repetitions */ return (m ? max_expand(ms, s+1, p, ep) : NULL); } case '-': { /* 0 or more repetitions (minimum) */ return min_expand(ms, s, p, ep); } default: { if (!m) return NULL; s++; p=ep; goto init; /* else return match(ms, s+1, ep); */ } } } }}
总的来说,这个比较的方法还是中规中矩的,从头开始查找匹配串的第一个字符,只不过用的是 memchr 函数,找到了之后用 memcmp 函数来比较两个字符串是否是相同的,如果不相同就跳过检查了的字符继续。相比那些复杂的字符串匹配算法,这个既简单又可爱,赞一个:),memcmp 函数的执行自然比 str 系列的比较要快一些,因为不用一直检查 ‘\0’ 字符,关于 memcmp 函数的做法,这里有一篇文章,虽然是说他的优化,但是看他的代码也能大致了解 memcmp 的做法:http://blog.chinaunix.net/uid-25627207-id-3556923.html
2.string.match(s, pattern, start)
这个函数的功能是在字符串 s 中查找指定的模式 pattern,并返回模式 pattern 中指定的第一个捕获。
第三个参数指明查找的起始位置,默认为1。
相比 string.find 函数来说,string.match 要简单的多了,它不需要你再选择是否采用特殊字符,它必须要采用。pattern 中的内容跟 string.find 一样,都是一个Lua的模式,跟 string.find 不同的地方在于,它返回的不在是匹配到的起止位置,而是返回 pattern 中指定的第一个捕获,如果 pattern 中没有指明捕获,那么它会返回整个 pattern 的匹配结果,当然,没有匹配到依然返回 nil。
在介绍 string.find 函数的时候提到过,Lua 源码中 string.match 调用的函数其实跟 string.find 调用的函数是相同的,都是 str_find_aux(lua_State *L, int find) 函数,唯一不同的地方在于 string.match 调用时 find 参数传递的是 0,这样就不会进入 str_find_aux() 里简单匹配的分支,直接进行模式匹配。
3.string.gmatch(s, pattern)
上面介绍的两个函数,无论是 string.find 还是 string.match 函数,都是发现和模式相匹配的串后就停下来了,返回对应的内容,而经常我们会有在一个字符串中找到所有跟模式相匹配的串的需求,string.gmatch() 函数就能够满足这个需求。
string.gmatch() 函数可以被当作迭代器进行调用,然后获得所有跟模式相匹配的串,比如Lua官网给出的例子:
s = "hello world from Lua"for w in string.gmatch(s, "%a+") do print(w)end--[[helloworldfromLua]]
让我们来看一下 gmatch_aux (lua_State *L) 函数,刨去为了迭代器做处理之后,就和 string.match() 函数实现没有什么区别了,最后都调用 match() 函数进行模式匹配。不同的地方就是上面说的字符 ^ 的处理这里是没有的。
static int gmatch_aux (lua_State *L) { MatchState ms; size_t ls; const char *s = lua_tolstring(L, lua_upvalueindex(1), &ls); const char *p = lua_tostring(L, lua_upvalueindex(2)); const char *src; ms.L = L; ms.src_init = s; ms.src_end = s+ls; for (src = s + (size_t)lua_tointeger(L, lua_upvalueindex(3)); src <= ms.src_end; src++) { const char *e; ms.level = 0; if ((e = match(&ms, src, p)) != NULL) { lua_Integer newstart = e-s; if (e == src) newstart++; /* empty match? go at least one position */ lua_pushinteger(L, newstart); lua_replace(L, lua_upvalueindex(3));//下一次查找的位置 return push_captures(&ms, src, e);//返回匹配到的字符串或捕获的多个字符串 } } return 0; /* not found */}
4.string.gsub(s, pattern, rep, n)
这个函数跟 string.gmatch() 一样,也带一个 g,可以想象得到,这个函数也会获得所有的匹配字符串,而不像 string.find() 和 string.match() 一样,碰到一个就结束了。确实是这样。这个函数的作用是在字符串 s 中查找与模式 pattern 相匹配的所有串,然后用 rep 参数产生的字符串进行替换,你可能要问,为什么是 rep 产生的串,而不是 rep 自己呢?因为这里的 rep 除了可以是一个字符串外,还可以是一个函数,甚至可以是一个table。
当 rep 是一个字符串的时候,一般来说是当作一个普通的字符串来处理,也就是直接用这个字符串来替换匹配到的串,但是这里会特殊的处理符号 %,% 后面接数字 1-9 的时候,也就是用前面模式捕获的序号 1-9 对应的字符串来替换模式匹配的内容,这样说比较绕,还是看例子:
local s = "am+dmf"print(string.gsub(s, "()(m+)", "%1")) -- a2+d5f 2print(string.gsub(s, "()(m+)", "%2")) -- am+dmf 2print(string.gsub(s, "()(m+)", "%3")) -- error: invalid capture index
上面我们用的模式是 ()(m+),这个模式会有2个捕获,分别是字符串当前的位置以及 m+,string.gsub() 匹配到的第一个地方是 am+dmf,这个时候两个捕获分别是 2 ,m,那么 %1 也就是第一个捕获,也就是 2,替换后的串为 a2+dmf,接着又匹配到第二个地方 am+dmf,这里的两个捕获分别是 5,m,那么 %1 指向的第一个捕获是 5,替换后的串为 a2+d5f,这就是结果显示的内容。后面那个数字 2 的意思是替换成功了2次。根据上面的分析就不难理解,为什么用 %2 去替换的时候字符串没有变,因为本来就是用 m 去替换 m,当然不变。另外,第三个 print() 会报错,因为只有2个捕获,而你要去使用 %3 ,那么自然就没有这个捕获了。
这里可能还需要注意的地方就是 % 只会和后面紧接着的数字结合,换句话说为什么前面要说是 1-9 就是这个原因,虽然捕获可以默认达到之前说的 32 个,但是只能用前 9 个了。有一个比较特殊的是 %0,它是用匹配到的串去替换,简单来说就是重复匹配到的串,比如这样:
local s = "am+dmf"print(string.gsub(s, "()(m+)", "%0%0%0")) -- ammm+dmmmf 2
匹配到的串是 m,用 mmm 替换了原串中的 m。
你可能要问,既然 % 被单独处理了,那么我想要用 % 去替换怎么办,只需要用 %% 就可以表示 % 自身了。比如:
local s = "am+dmf"print(string.gsub(s, "()(m+)", "%%")) -- a%+d%f 2
当rep是一个table的时候,每次匹配到了之后,都会用第一个捕获作为key去查询这个table,然后用table的内容来替换匹配串,如果没有指定捕获,那么,就用整个匹配串作为key去查询,如果没有查到对应key的值,或者对应的值不是字符串和数字,那么就不做替换:
local s = "am+dmf"local t1 = { [2] = "hh", [5] = "xx",}local t2 = {}print(string.gsub(s, "()(m+)", t1)) -- ahh+dxxf 2print(string.gsub(s, "()(m+)", t2)) -- am+dmf 2local t3 = { [2] = false}print(string.gsub(s, "()(m+)", t3)) -- am+dmf 2local t4 = { [2] = { 123 }}print(string.gsub(s, "()(m+)", t4)) -- error : invalid replacement value ( a table )
当rep是一个函数的时候,每当匹配到字符串的时候,就把模式所有的捕获按照捕获顺序作为参数传递给这个函数,如果没有指定捕获,则传递整个匹配的字符串给函数,函数的返回值如果是字符串或者是数字就替换掉匹配,如果不是则不做替换:
local s = "am+dmf"function f1(...) print(...) -- 2 m -- 5 m return "hh"endfunction f2() return { 123 }endprint(string.gsub(s, "()(m+)", f1)) -- ahh+dhhf 2print(string.gsub(s, "()(m+)", f2)) -- error : invalid replacement value ( a table )
第四个参数,用来表明,需要替换到第几个匹配为止,比如:
local s = "am+dmf"print(string.gsub(s, "()(m+)", "%%", -1)) -- am+dmf 0print(string.gsub(s, "()(m+)", "%%", 0)) -- am+dmf 0print(string.gsub(s, "()(m+)", "%%", 1)) -- a%+dmf 1print(string.gsub(s, "()(m+)", "%%", 2)) -- a%+d%f 2print(string.gsub(s, "()(m+)", "%%", 3)) -- a%+d%f 2
看看源码是怎么写的:
static int str_gsub (lua_State *L) { size_t srcl; const char *src = luaL_checklstring(L, 1, &srcl); const char *p = luaL_checkstring(L, 2); int tr = lua_type(L, 3); int max_s = luaL_optint(L, 4, srcl+1); int anchor = (*p == '^') ? (p++, 1) : 0; int n = 0; MatchState ms; luaL_Buffer b; luaL_argcheck(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, "string/function/table expected"); //第三个参数,必须是指定的四种类型 luaL_buffinit(L, &b); ms.L = L; ms.src_init = src; ms.src_end = src+srcl; while (n < max_s) { const char *e; ms.level = 0; e = match(&ms, src, p); if (e) { n++; add_value(&ms, &b, src, e); } if (e && e>src) /* non empty match? */ src = e; /* skip it */ else if (src < ms.src_end) luaL_addchar(&b, *src++); else break; if (anchor) break; } luaL_addlstring(&b, src, ms.src_end-src); luaL_pushresult(&b); lua_pushinteger(L, n); /* number of substitutions */ return 2;}
可以看到它处理了符号 ^ ,循环进行匹配,如果匹配到了,就按照不同的类型把替换串添加进结果里,最后把所有字符压回栈上。
说几个最近在用lua正则匹配的小用法吧
游戏改名设置的名字过滤emoji字符:
尼玛不得不说,网上找的东西好多不靠谱,还得去emoji官网里找所有编码规律特性,绝知此事要躬行。
local newName = self.textField:getStringValue()local len = string.utf8len(newName)--utf8解码长度for i = 1, len do local str = string.utf8sub(newName, i, i) local byteLen = string.len(str)--编码占多少字节 if byteLen > 3 then--超过三个字节的必须是emoji字符啊 return end if byteLen == 3 then if string.find(str, "[\226][\132-\173]") or string.find(str, "[\227][\128\138]") then return--过滤部分三个字节表示的emoji字符,可能是早期的符号,用的还是三字节,坑。。。这里不保证完全正确,可能会过滤部分中文字。。。 end end if byteLen == 1 then local ox = string.byte(str) if (33 <= ox and 47 >= ox) or (58 <= ox and 64 >= ox) or (91 <= ox and 96 >= ox) or (123 <= ox and 126 >= ox) or (str == " ") then return--过滤ASCII字符中的部分标点,这里排除了空格,用编码来过滤有很好的扩展性,如果是标点可以直接用%p匹配。 end end end
- Lua中的正则表达式及源码分析
- LUA中的正则表达式
- LUA中的正则表达式
- lua中的正则表达式
- Lua中的正则表达式
- LUA中的正则表达式
- Lua中的正则表达式
- lua string库函数详解、实例及lua正则表达式
- Lua基础---lua字符串库函数详解,实例及正则表达式
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
- JQuery源码分析-02正则表达式-RegExp-常用正则表达式
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
- lua--lua中的表达式
- Lua正则表达式
- lua正则表达式
- 防止ISE优化信号
- C++_友元函数
- SQLmap常用语句
- 低氧血症与缺氧区别
- 安卓开发中spinner的三级联动
- Lua中的正则表达式及源码分析
- iOS开发:视图生命周期
- 【unityshader】径向模糊
- [codevs1220] 数字三角形
- 2016/6/24
- proxool连接池
- GlobalMemoryStatusEx函数
- C++ 继承派生多态
- MultiByteToWideChar函数