正则表达式果然每年都要搞一次
每年都看不懂之前的文档, 悲哀啊.
12圣斗士
需要\转义的12圣斗士: [ ] \ ^ $ . | ? * + ( )
1. [这是字符集, 选择这里面的所有的单个字符]
2. \本身就是转义符, 因此得用\\
3. ^代表一行的开始, 在[代表除...以外]
4. $匹配行尾 多行匹配:换行符之前的位置, 单行匹配输入结束位置.
5. .任意asc码, 包含, 英文, 数字, 和几个简单符号, 不包含任意的换行符.
6. |或, a|b, 就是a或者b
7. ?*+这是西方三圣, 下面有解释.
8. (捕获符号) 每个被匹配的正则可以用\1, \2这样的项目在匹配环节指代, 到了替换环节要用$1, $2, 如此神奇的语法还是因为perl.
9. {}这一套也是需要转义的
括号都有用(){}[]
括号
1. 圆括号“()”才能用于形成引用匹配组。
2. “[]”用于定义字符集。
3. “{}”用于定义重复操作。
字符集[中括号]
需要转义的4大金刚: ] \ ^ -
1. ]负责封口
2. \转义
3. ^非, 不要里面的内容. [^x] 除了x之外的所有符号
4. -代表范围, [a-z], 匹配26个小写字母.[A-Za-z0-9]匹配所有英文字母和数字.
量词{大括号}
西方三圣: ?+*
- , 是关键字符
- 2到4次: {2,4} #注意这里是逗号, 不是减号
- ? 0次或1次.{0,1}
- + 1次或多次.{1,} 在量词描述中, 如果是空, 则代表无限制.
- * 任意次. {,} 此种写法不可以, 第一个项目不能省略. {0,}这样就是对的了.
- 刚好n次{n}
- 一般以上的量词匹配都是贪婪的任意量词的后面的?, 会导致量词变为懒惰的: *?
小括号比较复杂
引用匹配(小括号)
后面要讲的问号叹号就是小括号的一部分.
1. (xxx)代表一个匹配项目,
2. \1, \2这种数字, 在匹配中可以用作逐个引用.
3. 替换部分要用$1, $2, 这样的, 别看我, 都是perl惹的祸.
举例:
var tt='barfoo把foobarfoobar'.replace( /(foo)(bar)\1\2/, '$2 $1' );
此时 tt='barfoo把bar foo';
var re = /(.)*th/;
var str = "John Smith";
var newstr = str.replace(re, "$1");
console.log(newstr); //这里输出的是i, 因为th被尾巴匹配吃掉了, 所以最后一个字母i就出现了.
//情况如此之诡异, (任意内容){任意数量} 这种情况很难被记住被理解以及被正确使用.
//(某内容){量词}, 这个时候()就是在量词范围内的最后一次匹配
//这里的理由就是, group如果被多次匹配, 那么它永远保留最后一次匹配.
小括号和或
如果使用| 那么必须用小括号括起来他的作用域, 否则他的作用域就一直延展开来, 除非碰到另一个|或符号.
因此使用或|的正常格式是:
(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个空格, 然后再统一处理空格.
匹配模式
const reg=/xxx/ooo; // 这里的ooo就是匹配模式
1. g 全局模式, 能匹配多少就匹配多少, 否则匹配到第一个符合的就停止了.
2. i 忽略大小写模式
3. m 多行模式, m标志用于指定多行输入字符串应该被视为多个行。如果使用m标志,^和$匹配的开始或结束输入字符串中的每一行,而不是整个字符串的开始或结束。
4. y 继续模式, 不从头开始, 从当前位置开始匹配
例如: /xxx/gimy 也就这样了, 一共就这么四个模式标志
常用函数
- regexObj.exec(str) 可以步进式搜索
- regexObj.test(str) 判断是否匹配
- string.search(regexp), 匹配, 并返回匹配位置
- string.match(regexp), 匹配, 但是充满模式
- string.matchAll(regexp), 匹配所有的结果
-
string.replace(regexp substr, newSubstr function) 替换 const newStr = str.replace(regexp substr, newSubstr function) - string.split([separator[, limit]]), 切割一个字符串
以js的名义
//设置一个表达式
const regex = /ab+c/gim;
/*
1. regexp.exec() 匹配RegExp,返回一个数组, 这个数组描述的是一次匹配, 之所以是数组是为了小括号, 所以为了得到所有匹配, 需要反复运行exec, 直到未匹配到则返回null.
2. string.search是为了判断模式是否存在.返回模式所处位置. 类似的还有regexp.test()返回true/false.
3. string.match的表现很分裂,
如果是g模式可以一次把所有完整匹配都配出来, 但是没有()匹配组什么事了.
如果不是g模式, 那么就是第一个匹配, 并且有()匹配组. 简单地说表现和reg.exec一致.
4. string.matchAll()这个很好, 返回一个iterator, 每个元素都包含所有的匹配组.*/
//样例代码
let regexp = /t(e)(st(\d?))/g;
let str = 'test1test2';
let array = [...str.matchAll(regexp)];//三种方式处理: for...of, array spread, Array.from() :
array[0]// Array ["test1", "e", "st1", "1"]
array[1];// Array ["test2", "e", "st2", "2"]
const newStr = str.replace(regexp|substr, newSubstr|function) //使用字符串(第二个参数)替换掉(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 regex = /abc/g;
var str = regex.source; // "abc"
var restoreRegex = new RegExp(str, "g");
var regex = /abc/g;
var str = regex.toString(); // "/abc/g"
var parts = /\/(.*)\/(.*)/.exec(str);
var restoredRegex = new RegExp(parts[1], parts[2]);
var regex = /abc/g;
var str = regex.toString(); // "/abc/g"
var lastSlash = str.lastIndexOf("/");
var restoredRegex = new RegExp(str.slice(1, lastSlash), str.slice(lastSlash + 1));
要点
- 不要动态new一个正则出来, 转义能搞死人.
- 碰到有正则的动态需求, 可以考虑正则和字符串互相转化的办法
核心内容回顾
括号 | 量词 | 作用域 |
---|---|---|
{} | * | g |
[] | + | m |
() | ? | i |