正则表达式每年都要搞一次
去年的文档写的一点都不清晰, 导致我今年要重写一遍.
12圣斗士
需要\转义的12圣斗士: [ ] \ ^ $ . | ? * + ( )
1. [这是字符集, 选择这里面的所有的单个字符]
2. \本身就是转义符, 因此得用\\
3. ^代表一行的开始, 在[代表除...以外]
4. $匹配行尾(换行符之前的位置)
5. .任意asc码, 包含, 英文, 数字, 和几个简单符号, 不包含任意的换行符.
6. |或, a|b, 就是a或者b
7. ?*+这是西方三圣, 下面有解释.
8. (捕获符号) 每个被匹配的正则可以用\1, \2这样的项目在匹配环节指代, 到了替换环节要用$1, $2, 如此神奇的语法还是因为perl.
字符集[单聊中括号]
[]字符集中的4大金刚: ] \ ^ -
1. ]负责封口
2. \转义
3. ^非, 不要里面的内容. [^x] 除了x之外的所有符号
4. -代表范围, [a-z], 匹配26个小写字母.
量词{单聊大括号}
西方三圣: ?+*
- 2到4次: {2,4}
- ? 0次或1次.{0,1}
- + 1次或多次.{1,} 在量词描述中, 如果是空, 则代表无限制.
- * 任意次. {,} 此种写法不可以, 第一个项目不能省略. {0,}这样就是对的了.
- 一般以上的量词匹配都是贪婪的
- 任意量词的后面的?, 会导致量词变为懒惰的: *?
括号
括号
1. 圆括号“()”才能用于形成引用匹配组。
2. “[]”用于定义字符集。
3. “{}”用于定义重复操作。
引用匹配(小括号最复杂)
后面要讲的问号叹号就是小括号的一部分.
1. (xxx)代表一个匹配项目,
2. \1, \2这种数字, 在匹配中可以用作逐个引用.
3. 替换部分要用$1, $2, 这样的, 别看我, 都是perl惹的祸.
举例:
var tt='barfoo把foobarfoobar'.replace( /(foo)(bar)\1\2/, '$2 $1' );
此时 tt='barfoo把bar foo';
//此处要重点解释几个事情, 这个非常重要.
//
////////////////////////////////
// (any)* 这种形式, $n代表的是最后一个匹配到的项目.
console.log('121342344124'.replace(t, '$1')) //这里输出的是4.
var re = /(.)*th/;
var str = "John Smith";
var newstr = str.replace(re, "$1");
console.log(newstr); //这里输出的是i, 因为th被尾巴匹配吃掉了, 所以最后一个字母i就出现了.
//情况如此之诡异, (任意内容){任意数量} 这种情况很难被记住被理解以及被正确使用.
//因此, 最终的建议是: (小括号)之后如果写了量词, 那么不要用这个括号作为匹配项目, 不要使用$n形式去引用这个小括号.
小括号和或
如果使用| 那么必须用小括号括起来他的作用域, 否则他的作用域就一直延展开来, 除非碰到另一个|或符号.
因此使用或|的正常格式是:
(exp1|exp2|exp3|.....)
一大堆转义
太多了, 我们挑有用的说说.
\0 代表U+0000, null字符.
\0555, 代表8进制数字555.
\x56, 代表16进制数字56.
\u3838, 代表U3838, 4位的16进制字符3838.
\1, 代表重复的第一个()的匹配内容.
定位符号
1. 行开始和结束: ^$, 纯定位符, 同样不占位, 不消耗任何字符.
2. \b词的边界, /\bm/匹配“moon”中得‘m’;注意: JavaScript的正则表达式引擎将特定的字符集定义为“字”字符。不在该集合中的任何字符都被认为是一个断词。这组字符相当有限:它只包括大写和小写的罗马字母,小数位数和下划线字符。不幸的是,重要的字符,例如“é”或“ü”,被视为断词。 边界不占位, 纯的定位符.
3. \f 匹配换页符. 一个真实的字符. 占位的.
4. \n 换行 0A 一个真实的字符. 占位的.
5. \r 换行 0D 一个真实的字符. 占位的.
7. \t 水平制表符, 一个真实的字符. 占位的.
8. \v 垂直制表符, 这货就是ctrl+k, 或者word里面ctrl+enter, 滚屏用的符号, 一般文本没有这个.
空白字符
[\s\S]
- \s 空白字符 含空格 tab(制表符) 换页 换行, 一个真实的字符. 占位的.
- \S 代表除了空白字符之外的字符
- 因此, 任意字符就是[\s\S], 这个是一个很不错的范式. 不过这个范式一般的使用要切换为懒惰模式
[\s\S]*?
- 否则这个模式[\s\S]*会一直吃到全字符串结束, 关于贪婪和懒惰, 后文有介绍.
\s之坑
- 然后, \s还是有坑的, 如果拿他当\n用, 他会顺便吃掉下一行的所有缩进字符.
- 因此使用\s要慎重, 尽量指明是空格, tab, 还是回车, 尽量避免使用\s是避免采坑的法则.
要同时匹配tab和空格咋办?
- 虽然我们可以用|方式解决比如: [ | ]
- 但是最好的方式是, 先做一个预处理, 把所有的tab都替换为2个空格, 然后再统一处理空格.
匹配模式
/xxx/ooo 这里的ooo就是匹配模式
1. g 全局模式, 能匹配多少就匹配多少, 否则匹配到第一个符合的就停止了.
2. i 忽略大小写模式
3. m 多行模式, m标志用于指定多行输入字符串应该被视为多个行。如果使用m标志,^和$匹配的开始或结束输入字符串中的每一行,而不是整个字符串的开始或结束。
4. y 继续模式, 不从头开始, 从当前位置开始匹配
例如: /xxx/gimy 也就这样了, 一共就这么四个模式标志
以js的名义
//设置一个表达式
const regex = /ab+c/gim;
一共有6个函数, 一般exec+replace就够用了.
regexp.exec() 匹配RegExp,返回一个数组(未匹配到则返回null)
string.replace() 使用字符串(第二个参数)替换掉(RegExp-第一个参数, 匹配到的子字符串)。
var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
var tt='barfoo把foobarfoobar'.replace( /(foo)(bar)\1\2/, '$2 $1' );
//处理标题, 这个也是段落的区分, 既然段落只是由于h区分, 那么段落也加在这里好了.
var rex=/^(\#{1,6})( )([^\n]*)\n/gmi;
//正则表达式, 行头标志, 1-6个#必须顶着行写, 然后是任意非换行字符, 然后是一个换行字符.
var h_result=t.replace(rex,
function(match, p1, p2, p3) {
var ln=p1.length;
return '<h'+ln+'>'+p3+'</h'+ln+'>\n<p>' //很酷啊, 直接给个p段落就ok了.
});
问号
问号可以单独作为一节
#量词 代表0次或者1次
# 零宽断言, js里面都是推的
(?:x) 匹配但是不记录在\1, \2, 或者$1, $2这种匹配项目里面, 记得用小括号包住他们,否则会悲剧. //这个其实没卵用, 用了就知道了, 根本不起作用, 这货的意思并不是不记录在匹配结果, 而只是忽略这个括号, 也就是说这个小括号并不单独形成一个匹配, 总之js没有前置零宽断言, 只有下面两种后置零宽断言
x(?=y) 匹配后面有y的x
x(?!y)匹配后面没有y的x
(?!x)b这个项目匹配b前面没有x的情况, 这么用是可以的, 但是, js不支持前推零宽断言, 也就是说, 这个不是x的东西也会被匹配到结果里面.
t=t.replace(/((?!^)>)|(>(?! ))/gm,'>')
如果js不支持前推零宽断言, 那么这里有bug, ((?!^)>) 这个会吃掉>左边的一个字符
但是真的很神奇, 实际上竟然是不吃掉左边第一个字符的,
也就是说, js的replace实际是支持前推0宽断言的.
# 懒惰
任意量词的后面的?, 会导致量词变为懒惰的: *?
###
问号 - 贪婪和懒惰, 永远也躲不开的问题.
- 用字符集反相匹配解决懒惰匹配问题, 用贪婪反向排除匹配更好
[^ ooo ]
- *?其实任意量词后面加问号就是懒惰了.
问号 - 零宽断言(js的这个就是个杯具)
/?!逆向断言/ #这里断言的是后面没有. 都是后推零宽断言, js不支持前置零宽断言.
#比如不是行首
/?!^/ #rex=/((?!^)>)|(>(?! ))/gmi; //负向零宽断言
#前瞻断言
#正向前瞻用来检查接下来的出现的是不是某个特定的字符集。
#而负向前瞻则是检查接下来的不应该出现的特定字符串集。
#零宽断言是不会被捕获的。
(?=exp) 正向前瞻 匹配exp前面的位置
(?!exp) 负向前瞻 匹配后面不是exp的位置
(?<=exp) 正向后瞻 匹配exp后面的位置, js不支持
(?<!exp) 负向后瞻 匹配前面不是exp的位置, js不支持
所以, (?!exp) 这个全称是: 负向前瞻零宽断言, 因为js没有后瞻, 所以简称负向零宽断言
#后瞻断言
#后瞻断言也叫零宽度正回顾后发断言 (?<=表达式) 表示匹配表达式后面的位置
#例如(?<=abc).* 可以匹配abcdefg中的defg
*? #任意量词后面的?会导致前面的量词懒惰.
var rex=/([>] )([\s\S]*?)\n\n/gmi; #此处有两个回车结束block, 单个回车可以忽略
//这个就是>开头的任意的字符之后两个回车结尾.
技术点
var rex=/(^[>] )([\s\S]*?)\n\n/gmi; //[\s\S]所有字符
|(或)和$1之间的关系.
SyntaxError: Invalid regular expression: nothing to repeat
这是一个很常见的bug. 例如:
var regex = new RegExp("^ {"+p1.length+"}\* ([^\n]*)", "gm")
就会遇到这个bug:
SyntaxError: Invalid regular expression: nothing to repeat
查阅Stack Overflow, 发现是特殊字符转义引起的, 去除上面的\*就没有任何问题了. 咋办呢?
var regex = new RegExp("^ {"+p1.length+"}[*] ([^\n]*)", "gm") //这句就ok了.
但是, 我不解的是, 为啥转义符失效了?
var regex = new RegExp("^ {"+p1.length+"}\\* ([^\n]*)", "gm") //这样也可以了, 奶奶的竟然要我双重转义.
结论: 不到万不得已, 不要动态构造正则, 尽量别new regexp, 一堆坑. 很多时候这货能正常运行其实只是一个巧合.
还可以得出一个结论, 中括号[里面的转义都是正确的, 真神奇]:
var rex=/^( *)([0-9]+\.|[*+\-]) [^\n]*\n(^\1([0-9]+\.|[*+\-])? [^\n]*\n)*/gm
核心内容
括号 | 量词 | 作用域 |
---|---|---|
{} | * | g |
[] | + | m |
() | ? | i |
剩下的就是些特殊符号了: 12圣斗士, 本文开头有介绍