正则表达式使用手册

667 阅读4分钟

正则表达式(regex or regexp) 通过检索特定模式的一个或多个匹配(即特定的ASCII或者unicode字符序列 ),从任何文本中提取信息,这是非常有用的。

应用范围:解析、替换字符串,数据格式转换,以及网页抓取

有趣的是,一旦你学会了它的语法,几乎可以在所有编程语言中使用这个工具(JavaScript, Java, VB, C #, C / C++, Python, Perl, Ruby, Delphi, R, Tcl, and many others),它们之中只有微小的区别。

让我们来看看一些例子和解析。

基本语法

边界匹配 —  ^ and $

  • ^The 匹配任何以The开始的字符串 -> Try it!

  • end$ 匹配任何以End结尾的字符串

  • ^The end$ 准确地匹配字符串 (The开始,End结尾)

  • roar 匹配任何含有roar的字符串

量词 —  * + ? and {}

  • abc* 匹配ab后跟零或多次c的字符串 [0, +∞] -> Try it!

  • abc+ 匹配ab后一次或多次c的字符串 [1, +∞]

  • abc? 匹配ab后零次或一次c的字符串 [0, 1]

  • abc{2} 匹配ab后2次c的字符串

  • abc{2,} 匹配ab后2次或更多次c的字符串

  • abc{2,5} 匹配ab后2到5次c的字符串

  • a(bc)* 匹配a后零次或多次bc的字符串

  • a(bc){2,5} 匹配a后2到5次bc的字符串

或— | or []

  • a(b|c) 匹配a后跟b或c的字符串

  • a[b|c] 与上述一致

字符类 — \d \w \s and .

  • \d 匹配一个数字 -> Try it!

  • \w 匹配一个字符(字母、数字、字符、下划线) -> Try it!

  • \s 匹配一个空格(包括tabs、换行\n)

  • . 匹配任何非空字符(不包含换行\n等空字符) -> Try it!

  • [\s\S] 匹配任何字符

谨慎使用.元字符,因为字符类否定字符类处理更快、更准确。

\d, \w\s 分别使用 \D, \W\S 表示它们的否定。

比如,\D将匹配与\d相反的字符。

  • \D 匹配一个非数字的字符 -> Try it!

为了正确的理解,你必须使用反斜杠\ 来转义字符^.[$()|*+?{\, 因为它们具有特殊的含义。

  • $\d 匹配一个$后跟一个数字的字符

注意,你还可以匹配一个不可打印的字符, 如tabs \t换行 \n回车符 \r

修饰符

我们正在学习如果编写一个正则表达式,但是忘记了一个基本概念:修饰符

正则表达式通常是/abc/这样的形式,其中匹配模式由两个/分隔。我们可以在它们的最后指定一个以下这些值的标志(也可以将它们组合使用)。

  • g (global): 第一个匹配后不返回结果,在上一次匹配结果之后继续检索,最后返回所有匹配项(全局匹配)。
  • m (multi-line):启用时^和 $ 将匹配行的开头和结尾,而不是整个字符串。
  • i (insensitive): 使整个表达式不区分大小写(for instance /aBc/i would match AbC

es6新增

  • y (sticky):作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配结果的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就行,而y修饰符会确保匹配从剩余位置的第一个位置开始,这也就是“粘连”的涵义。

    var s = 'aaa_aa_a';
    
    var r1 = /a+/g;
    var r2 = /a+/y;
    
    r1.exec(s); // ['aaa']
    r2.exec(s); // ['aaa']
    
    r1.exec(s); // ['aa']
    r2.exec(s); // null
    
    var r = /a+_/y;
    
    // try again
    r.exec(s); // ['aaa_']
    r.exec(s); // ['aa_']
    
    r.sticky // true
    
  • u (Unicode):“Unicode”模式,用来正确处理大于\uFFFF的Unicode字符。也就是说,可以正确处理4个字节的UTF-16编码。

    var s = '𠮷';
    
    /^.$/.test(s); // false
    /^.$/u.test(s); // true
    
  • s:匹配意字符,我们知道点(.)特殊字符代表任意字符,但是“行终止符”除外(eg.\n,\r,行分割符,段分隔符),s修饰符可包含所有,这被称为dotAll模式,即点(dot)代表一切字符。

    var s = '𠮷';
    
    /.*/s.test(s); // true
    

中级语法

分组和捕获— ()

  • a(bc) 括号为创建一个值为bc捕获组 -> Try it!

  • a(?:bc)* 使用?:禁用捕获组(非捕获组) -> Try it!

要理解?:则需要理解捕获分组非捕获分组的概念:

()表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容);

(?:)表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来。

es6新增

  • a(?< foo >bc) 使用?<foo>给这个捕获组命名。-> Try it!

如果为捕获组命名(使用?<foo>), 我们将能够使用匹配结果groups中查找捕获组的值,键值就是组名。

当我们从字符串或者数据中提取信息的时候,此运算符非常有用。在使用多个捕获组匹配数据时,

我们将使用匹配结果的索引来访问它们的值($n),也可以具名组groups.<name>访问

var string = '1999-12-31';
const matchObj = string.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
// ["1999-12-31", "1999", "12", "31", index: 0, input: "1999-12-31", groups: {day: "31", month: "12", year: "1999"}]

const newStr = string.replace(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/, '$<day>/$<month>/$<year>')
// 31/12/1999

const newStr2 = string.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1')
// 31/12/1999

// 在 正则表达式内部 也可以使用“具名组匹配” \k<组名>
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

中括号— []

  • [abc] 匹配a或b或c, 等同于a|b|c -> Try it!

  • [a-c] 与上述一致

  • [a-fA-F0-9] 匹配一个十六进制字符,不区分大小写。-> Try it!

  • [0-9]% 匹配一个%前为0到9的字符串

  • [^a-zA-Z] 匹配一个没有从a-z或者A到Z的字母。这种情况下,^用作表示否定。-> Try it!

需要注意的是,在中括号表达式中,所有特殊字符(包括反斜杠\)都会失去它们特殊的功能,因此,不要使用“转义”功能

贪婪和懒匹配(Greedy and Lazy match)

量词( * + {})是贪婪的匹配,所以它们会尽可能地通过提供的文本扩展匹配

例如,使用<.+> 来匹配 <div>simple div</div>,它会返回整个文本<div> simple div</div>

为了仅匹配一个div标签,我们可以使用?来使它lazy match

  • <.+?> 匹配<>内包含的一个或者多个字符,根据需要进行扩展。-> Try it!

那么,更好的正则方案应该避免使用., 赞成使用更严格的模式:

  • <[^<>]+> 匹配<>内包含的任何字符。-> Try it!

高级语法

边界 — \b and \B

  • \babc\b abc前后都无字符, 即执行仅限整个单词匹配 -> Try it!

\b 表示边界(类似于^和$)的位置,其中一侧是单词字符(如\w),而另一侧不是单词字符(例如,它可能是字符串的开头或空格字符 eg: \b123)。

它同样有否定,\B。这匹配\b所有不匹配的位置,如果我们找到完全被单词字符包围的匹配模式, 则可以匹配。

  • \Babc\B 执行abc左右都被字符包围匹配 -> Try it!

返回引用— \1

  • ([abc])\1 使用\1返回与第一个捕获组相同的匹配项进行匹配 == ([abc])([abc]) -> Try it!

  • ([abc])([de])\2\1 与上述一致,使用\2,\1返回与第二个捕获组,第一个捕获组相同的匹配项进行匹配 == ([abc])([de])([de])([abc]) , 以此类推-> Try it!

  • (?< foo >[abc])\k< foo > 我们将名称foo放入捕获组中,可以用\k引用它,结果与第一个正则一致 == ([abc])([abc]) -> Try it!

前瞻(前置断言)和后瞻(后置断言)— (?=) and (?<=)

firefox 目前不兼容,遇到过一次,请注意

  • d(?=r) 仅当d后跟r时才匹配d,但r不会成为整个正则表达式的一部分 -> Try it!

  • (?<=r)d 仅当d前为r时才匹配d,但r不会成为整个正则表达式的一部分 -> Try it!

你当然也可以使用否定运算符

  • d(?!r) 仅当d后不是r时才匹配d,但r不会成为整个正则表达式的一部分 -> Try it!

  • (?<!r)d 仅当d前不是r时才匹配d,但r不会成为整个正则表达式的一部分 -> Try it!

用法介绍

注:patternRegExp的实例, strString的实例

用法说明返回值
regexp.test(str)判断str是否包含匹配结果包含返回true,不包含返回false
regexp.exec(str)根据regexpstr进行正则匹配返回匹配结果数组,如匹配不到返回null ---与match的区别在于返回匹配信息更全
str.match(regexp)根据regexpstr进行正则匹配返回匹配结果数组,如匹配不到返回null
str.replace(regexp, newSubStr \ function) 详解根据regexp / stringstr进行正则匹配,把匹配结果替换为newSubStr \ function 的返回值返回替换后的字符串【原字符串不变】
str.search(regexp)根据regexpstr进行正则匹配返回第一次匹配的位置
str.split(regexp)regexp为分隔符,对str切割为数组返回切割后的数组

test/exec 注意事项

如果正则表达式设置了全局标志 /gtest() 的执行会改变正则表达式 lastIndex 属性。连续的执行 test()方法,后续的执行将会从 lastIndex 处开始匹配字符串,(exec() 同样改变正则本身的 lastIndex 属性值).

下面的实例表现了这种行为:

const digits = /\d+/g;

digits.test("Hello world! 123"); // true
digits.test("321"); // false
digits.test("321"); // true

你可以这样 hack:

const digits = /\d+/g;

digits.test("Hello world! 123"); // true

digits.lastIndex = 0;
digits.test("321"); // true

digits.lastIndex = 0;
digits.test("321"); // true

详情参考:MDN

replace详解

  • 语法
str.replace(regexp|substr, newSubStr|function)
  • 参数

    • regexp (pattern)

      一个RegExp对象或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。

    • substr (pattern)

      一个要被 newSubStr 替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅仅是第一个匹配会被替换。

    • newSubStr (replacement)

      用于替换掉第一个参数在原字符串中的匹配部分的字符串

    • function(a, b, c, d) (replacement)

      一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。

      • a:匹配项

      • b:匹配的捕获组

        若无捕获组则无此参数,若多个捕获组则多个参数 b,c,d,e...;

        若一个捕获组重复多次,则该捕获组的参数为最后一次匹配结果。例如:(\d)+

      • c:匹配项在原字符串中的索引

      • d:原字符串

        最后两个参数永远为 匹配项索引原字符串

  • 如果你对 replace 还有困惑,可以看看下面的例题

总结

正如您所见,正则表达式应用领域很广,我确信你的开发生涯中至少见过一次以上规则,下面是它的应用列表:

  • 数据验证(例如,检查时间字符串的格式是否正确)
  • 数据抓取(尤其是网络抓取,查找包含特定单词集的所有页面,最终按特定顺序排列)
  • 数据包装(将数据从“原始”格式转换为其他格式)
  • 字符串分析(例如,捕获所有url get参数,捕获一组括号内的文本)
  • 字符串替换(例如,即使在代码会话中使用一个公共IDE来将Java或C类)转换为相应的JSON对象{--替换“;”与“,”使其为小写,避免类型声明等)。
  • 语法高亮、文件重命名、packet sniffing和许多其他涉及字符串的应用程序(其中数据不需要是文本的)

Have fun and do not forget to recommend the article if you liked it 💚

附录:replace例题

  1. 将下面两处空缺补全:
// define
(function(window) {
    function fn(str) {
        this.str = str;
    }

    fn.prototype.format = function () {
        var arg = ____;

        return this.str.replace(____, function (a, b) {
            return arg[b] || '';
        })
    };

    window.fn = fn;
})(window);

// use
(function() {
    var t = new fn('<p><a href="{0}">{1}<a><span>{2}</span></p>');
    console.log(t.format('http://www.yonyou.com', 'yonyou', 'Welcome'));
})();

// 这里就不给答案了,如果你理解了 replace 用法, 这太 easy 了。
  1. 通过正则将 87654321 整数转为 货币 $87,654,321
'87654321'.replace(/(\d)+?(?=(\d{3})+(?!\d))/g, function(a, b, c, d) {
  return d < 2 ? ("$" + a + ",") : (a + ",");
})
// $87,654,321
// 请深入理解replace和正则后再理解该题

'87654321'.replace(/(\d{1,3})(?=(\d{3})+$)/g, function(a, b, c, d) {
  return d < 2 ? ("$" + a + ",") : (a + ",");
})
// $87,654,321

'87654321'.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,') // $& 为匹配项;$1,$2...为捕获组
// 87,654,321
  1. 密码强度正则,最少6位,包括至少1个大写字母,1个小写字母,1个数字
/(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])^[0-9A-Za-z]{6,}$/.test('w44Y4S')
// 前面三个前置断言,针对开头^项的约束

/^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z])/.test('w44sYw')

参考

  1. Regex tutorial — A quick cheatsheet by examples
  2. 正则表达式中?=和?:和?!的理解
  3. [ JS 进阶 ] test, exec, match, replace
  4. ES6标准入门(第3版)—— 阮一峰
  5. A regular expression surprise in JavaScript