Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

javascript 正则表达式解析 #26

Open
shaozj opened this issue Aug 22, 2019 · 0 comments
Open

javascript 正则表达式解析 #26

shaozj opened this issue Aug 22, 2019 · 0 comments
Assignees

Comments

@shaozj
Copy link
Owner

shaozj commented Aug 22, 2019

javascript 正则表达式解析

在日常开发中,正则表达式常常能让人很困惑。实际上,掌握好正则表达式能让你的开发大大提效。废话不多说,直接开始学习吧。

什么是正则表达式

正则表达式是用于匹配字符串中字符组合的模式。大部分语言都实现了正则表达式,而我们要研究的是 javascript 中的正则表达式。

创建一个正则表达式

有两种方式可以创建正则表达式:

使用正则表达式字面量创建

语法:/pattern/[flags]

const re = /ab+c/;
re.test('abbbc'); // true

flags 是可选的,将在下文中解释。

使用 RegExp 对象的构造函数创建

语法:new RegExp(pattern[, flags])

const re = new RegExp('ab+c');
re.test('abc'); //true

无论使用字面量的方式还是构造函数的方式,得到的都是正则表达式对象。

正则表达式的方法

正则表达式主要有两个方法:

RegExp.prototype.test()

test 方法用于测试字符串是否匹配正则表达式,它返回 true 或者 false。例子:

const re = /hello/;
re.test('hello world'); // true
re.test('hell0 world'); // false

RegExp.prototype.exec()

exec 方法用于在一个在字符串中执行查找匹配的RegExp,它返回一个数组(未匹配到则返回 null)。
exec 方法匹配成功后将返回一个数组并且更新正则表达式的属性。
先看一个例子:

const myRe = /d(b+)d/g;
const myArray = myRe.exec("cdbbdbsbz");
// ["dbbd", "bb", index: 1, input: "cdbbdbsbz", groups: undefined]

返回的数组

属性或索引 描述 在例子中对应的值
匹配到的字符串和所有被记住的子字符串。 ["dbbd", "bb"]
index 在输入的字符串中匹配到的以0开始的索引值 1
input 初始字符串。 "cdbbdbsbz"
groups 捕获组 看下面例子
// 命名捕获组的获取
let reg1 = /(?<first>\d)(?<second>\d)/;
let str2 = '123';

console.log(reg2.exec(str2).groups);
// => { first: 1, second: 2 } 而新的语法支持对这些捕获组进行命名,
// 更方便地获取某个捕获组的数据

更新正则表达式属性

属性或索引 描述 在例子中对应的值
lastIndex 下一个匹配的索引值。(这个属性只有在使用g参数时可用在) 5
source 模式文本。在正则表达式创建时更新,不执行。 "d(b+)d"

正则表达式的模式

简单模式

简单模式是由你想直接找到的字符构成。比如,/abc/ 这个模式就能且仅能匹配 "abc" 字符按照顺序同时出现的情况。

const regex = /hello/;
console.log(regex.test('hello world')); // true

使用特殊字符

当你需要搜索一个比直接匹配需要更多条件的匹配时,比如寻找一个或多个 "b",或者寻找空格,这时可以在模式中使用特殊字符。比如,你可以使用 /ab*c/ 去匹配一个单独的 "a" 后面跟了零个或者多个 "b",同时后面跟着 "c" 的字符串:*的意思是前一项出现零次或者多次。在字符串 "cbbabbbbcdebc" 中,这个模式匹配了子字符串 "abbbbc"。

下面列出了一个正则表达式中可以利用的特殊字符的完整列表和描述。

标识 flags

两个最重要的标识:

  • g :全局搜索,在搜到第一个匹配的时候不返回
  • i :大小写不敏感
const re = /\w+\s/g;
const str = "fee fi fo fum";
console.log(str.match(re)); // ["fee ", "fi ", "fo "]
const re1 = /\w+\s/;
console.log(str.match(re1)); 
// ["fee ", index: 0, input: "fee fi fo fum", groups: undefined]

注意上面例子中的 String.prototype.match 方法,当正则表达式带有 g 这个标识的时候,它返回的是所有匹配项组成的数组。如果正则表达式没有 g 这个标识,那么返回结果和 regexp 的 exec 方法一致。

const re = /abc/i;
console.log(re.test('Abc')); // true

字符类别(Character Classes)

区分不同类型的字符,例如区分字母和数字。

  • . 小数点

    • 小数点)默认匹配除换行符之外的任何单个字符。
    • 例如,/.n/ 将会匹配 "nay, an apple is on the tree" 中的 'an' 和 'on',但是不会匹配 'nay'。
    • 如果 s ("dotAll") 标志位被设为 true,它也会匹配换行符。
  • \d

    • 匹配一个数字。等价于[0-9]。
    • 例如, /\d/ 或者 /[0-9]/ 匹配"B2 is the suite number."中的'2'。
  • \D

    • 匹配一个非数字字符。等价于[^0-9]。
    • 例如, /\D/ 或者 /[^0-9]/ 匹配"B2 is the suite number."中的'B' 。
  • \w

    • 匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]
    • 例如, /\w/ 匹配 "apple," 中的 'a',"$5.28,"中的 '5' 和 "3D." 中的 '3'。
  • \W

    • 匹配一个非单字字符。等价于 [^A-Za-z0-9_]
    • 例如, /\W/ 或者 /[^A-Za-z0-9_]/ 匹配 "50%." 中的 '%'。
  • \s

    • 匹配一个空白字符,包括空格、制表符、换页符和换行符。等价于[\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。
    • 例如, /\s\w*/ 匹配"foo bar."中的' bar'。
  • \S

    • 匹配一个非空白字符。等价于 [^ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。
    • 例如,/\S\w*/ 匹配"foo bar."中的'foo'。
  • \t

    • 匹配一个水平制表符 (U+0009)。
  • \r

    • 匹配一个回车符 (U+000D)。
  • \n

    • 匹配一个换行符 (U+000A)。
  • \v

    • 匹配一个垂直制表符 (U+000B)。
  • \f

    • 匹配一个换页符 (U+000C)。
  • [\b]

    • 匹配一个退格(U+0008)。(不要和\b混淆了。)
  • \0

    • 匹配 NULL(U+0000)字符, 不要在这后面跟其它小数,因为 \0 是一个八进制转义序列。
  • \cX

    • 当X是处于A到Z之间的字符的时候,匹配字符串中的一个控制符。
    • 例如,/\cM/ 匹配字符串中的 control-M (U+000D)。
  • \xhh

    • 与代码 hh 匹配字符(两个十六进制数字)
  • \uhhhh

    • 与代码 hhhh 匹配字符(四个十六进制数字)。
  • \u{hhhh} or \u{hhhhh}

    • 仅当设置了u标志时)使用 Unicode 值 hhhh 匹配字符(十六进制数字)。
  • \

    • 依照下列规则匹配:
    • 在非特殊字符之前的反斜杠表示下一个字符是特殊字符,不能按照字面理解。例如,前面没有 "" 的 "b" 通常匹配小写字母 "b",即字符会被作为字面理解,无论它出现在哪里。但如果前面加了 "",它将不再匹配任何字符,而是表示一个字符边界。
    • 在特殊字符之前的反斜杠表示下一个字符不是特殊字符,应该按照字面理解。详情请参阅下文中的 "转义(Escaping)" 部分。
    • 如果你想将字符串传递给 RegExp 构造函数,不要忘记在字符串字面量中反斜杠是转义字符。所以为了在模式中添加一个反斜杠,你需要在字符串字面量中转义它。/[a-z]\s/i 和 new RegExp("[a-z]\s", "i") 创建了相同的正则表达式:一个用于搜索后面紧跟着空白字符(\s 可看后文)并且在 a-z 范围内的任意字符的表达式。为了通过字符串字面量给 RegExp 构造函数创建包含反斜杠的表达式,你需要在字符串级别和表达式级别都对它进行转义。例如 /[a-z]:\/i 和 new RegExp("[a-z]:\\","i") 会创建相同的表达式,即匹配类似 "C:" 字符串。

组和范围(Groups and Ranges)

表示表达式字符的分组和范围。

  • x|y

    • 匹配‘x’或者‘y’。
    • 例如,/green|red/ 匹配“green apple”中的‘green’和“red apple”中的‘red’
  • [xyz] [a-c]

    • 一个字符集合。匹配方括号中的任意字符,包括转义序列。你可以使用破折号(-)来指定一个字符范围。对于点(.)和星号(*)这样的特殊符号在一个字符集中没有特殊的意义。他们不必进行转义,不过转义也是起作用的。
    • 例如,[abcd][a-d]是一样的。他们都匹配"brisket"中的‘b’,也都匹配“city”中的‘c’。/[a-z.]+//[\w.]+/与字符串“test.i.ng”匹配。
  • [^xyz] [^a-c]

    • 一个反向字符集。也就是说, 它匹配任何没有包含在方括号中的字符。你可以使用破折号(-)来指定一个字符范围。任何普通字符在这里都是起作用的。
    • 例如,[^abc][^a-c] 是一样的。他们匹配"brisket"中的‘r’,也匹配“chop”中的‘h’。
  • 捕获括号 (x)

    • 像下面的例子展示的那样,它会匹配 'x' 并且记住匹配项。其中括号被称为捕获括号。
    • 模式 ·/(foo) (bar) \1 \2/· 中的 '(foo)' 和 '(bar)' 匹配并记住字符串 "foo bar foo bar" 中前两个单词。模式中的 \1 和 \2 表示第一个和第二个被捕获括号匹配的子字符串,即 foo 和 bar,匹配了原字符串中的后两个单词。注意 \1、\2、...、\n 是用在正则表达式的匹配环节,详情可以参阅后文的 \n 条目。而在正则表达式的替换环节,则要使用像 $1、$2、...、$n 这样的语法,例如,'bar foo'.replace(/(...) (...)/, '$2 $1')$& 表示整个用于匹配的原字符串。
  • \n 注意:n是一个具体数字

    • 在正则表达式中,它返回最后的第n个子捕获匹配的子字符串(捕获的数目以左括号计数)。
    • 比如 ·/apple(,)\sorange\1/· 匹配"apple, orange, cherry, peach."中的'apple, orange,' 。
  • (?<Name>x)

    • (?<捕获组的名字>捕获组对应的规则),有名字的捕获组
     // 命名捕获组的获取
     let reg1 = /(?<first>\d)(?<second>\d)/
     let str2 = '123'
     console.log(reg2.exec(str2).groups) 
     // => { first: 1, second: 2 } 对这些捕获组进行命名,更方便地获取某个捕获组的数据
  • 非捕获括号 (?:x)

    • 匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。看看这个例子 /(?:foo){1,2}/。如果表达式是 /foo{1,2}/,{1,2} 将只应用于 'foo' 的最后一个字符 'o'。如果使用非捕获括号,则 {1,2} 会应用于整个 'foo' 单词。
     /(?:foo){1,2}/.exec('foo'); 
     // ["foo", index: 0, input: "foo", groups: undefined]
     /(foo){1,2}/.exec('foo');
     // ["foo", "foo", index: 0, input: "foo", groups: undefined]

量词(Quantifiers)

表示匹配的字符或表达式的数量。

  • *

    • 匹配前一个表达式 0 次或多次。等价于 {0,}
    • 例如,/bo*/ 会匹配 "A ghost boooooed" 中的 'booooo' 和 "A bird warbled" 中的 'b',但是在 "A goat grunted" 中不会匹配任何内容。
  • +

    • 匹配前面一个表达式 1 次或者多次。等价于 {1,}。
    • 例如,/a+/ 会匹配 "candy" 中的 'a' 和 "caaaaaaandy" 中所有的 'a',但是在 "cndy" 中不会匹配任何内容。
  • ?

    • 匹配前面一个表达式 0 次或者 1 次。等价于 {0,1}。
    • 例如,/e?le?/ 匹配 "angel" 中的 'el'、"angle" 中的 'le' 以及 "oslo' 中的 'l'。
    • 如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 "123abc" 使用 /\d+/ 将会匹配 "123",而使用 /\d+?/ 则只会匹配到 "1"。
  • {n}

    • n 是一个正整数,匹配了前面一个字符刚好出现了 n 次。
    • 比如, /a{2}/ 不会匹配“candy”中的'a',但是会匹配“caandy”中所有的 a,以及“caaandy”中的前两个'a'。
  • {n,}

    • n是一个正整数,匹配前一个字符至少出现了n次。
    • 例如, /a{2,}/ 匹配 "aa", "aaaa" 和 "aaaaa" 但是不匹配 "a"。
  • {n,m}

    • n 和 m 都是整数。匹配前面的字符至少n次,最多m次。如果 n 或者 m 的值是0, 这个值被忽略。
    • 例如,/a{1, 3}/ 并不匹配“cndy”中的任意字符,匹配“candy”中的a,匹配“caandy”中的前两个a,也匹配“caaaaaaandy”中的前三个a。注意,当匹配”caaaaaaandy“时,匹配的值是“aaa”,即使原始的字符串中有更多的a。
  • *? +? ?? {n}? {n,}? {n,m}?

    • ?如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 "123abc" 使用 /\d+/ 将会匹配 "123",而使用 /\d+?/ 则只会匹配到 "1"。

边界(Boundaries)

表示行和单词的开始和结尾。

  • 匹配输入的开始 ^

    • 如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置。
    • 例如,/^A/ 并不会匹配 "an A" 中的 'A',但是会匹配 "An E" 中的 'A'。
    • 当 '^' 作为第一个字符出现在一个字符集合模式时,它将会有不同的含义: [^xyz] 反向字符集,匹配任何没有包含在方括号中的字符。
  • 匹配输入的结束 $

    • 如果多行标示被设置为 true,那么也匹配换行符前的位置。
    • 例如,/t$/ 并不会匹配 "eater" 中的 't',但是会匹配 "eat" 中的 't'。
  • 匹配一个词的边界 \b

    • 匹配一个词的边界。一个词的边界就是一个词不被另外一个“字”字符跟随的位置或者没有其他“字”字符在其前面的位置。注意,一个匹配的词的边界并不包含在匹配的内容中。换句话说,一个匹配的词的边界的内容的长度是0。(不要和[\b]混淆了)
    • 使用"moon"举例:
      /\bm/匹配“moon”中的‘m’;
      /oo\b/并不匹配"moon"中的'oo',因为'oo'被一个“字”字符'n'紧跟着。
      /oon\b/匹配"moon"中的'oon',因为'oon'是这个字符串的结束部分。这样他没有被一个“字”字符紧跟着。
      /\w\b\w/将不能匹配任何字符串,因为在一个单词中间的字符永远也不可能同时满足没有“字”字符跟随和有“字”字符跟随两种情况。
    • 注意:JavaScript的正则表达式引擎将特定的字符集定义为“字”字符。不在该集合中的任何字符都被认为是一个断词。这组字符相当有限:它只包括大写和小写的罗马字母,十进制数字和下划线字符。不幸的是,重要的字符,例如“é”或“ü”,被视为断词。
  • 匹配一个非单词边界 \B

    • 匹配如下几种情况:
      • 字符串第一个字符为非“字”字符
      • 字符串最后一个字符为非“字”字符
      • 两个单词字符之间
      • 两个非单词字符之间
      • 空字符串
    • 例如,/\B../匹配"noonday"中的'oo', 而/y\B../匹配"possibly yesterday"中的’yes‘

断言(Assertions)

表示一个匹配在某些条件下发生。断言包含先行断言、后行断言和条件表达式。

  • 先行断言 x(?=y)

    • 匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。
    • 例如,/Jack(?=Sprat)/会匹配到'Jack'仅仅当它后面跟着'Sprat'。/Jack(?=Sprat|Frost)/匹配‘Jack’仅仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。'
  • 后行断言 (?<=y)x

    • 匹配'x'仅仅当'x'前面是'y'.这种叫做后行断言。
    • 例如,/(?<=Jack)Sprat/ 会匹配到'Sprat'仅仅当它前面是'Jack'。/(?<=Jack|Tom)Sprat/ 匹配'Sprat'仅仅当它前面是'Jack'或者是'Tom'。但是'Jack'和'Tom'都不是匹配结果的一部分。
  • 正向否定查找 x(?!y)

    • 仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找。
    • 例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!\.)/ 匹配一个数字。正则表达式/\d+(?!\.)/.exec("3.141")匹配'141'而不是'3.141'
  • 反向否定查找 (?<!y)x

    • 仅仅当'x'前面不是'y'时匹配'x',这被称为反向否定查找。
    • 例如, 仅仅当这个数字前面没有负号的时候,/(?<!-)\d+/ 匹配一个数字。
      /(?<!-)\d+/.exec('3') 匹配到 "3".
      /(?<!-)\d+/.exec('-3') 因为这个数字前有负号,所以没有匹配到。

Unicode 属性转义(Unicode Property Escapes)

基于 unicode 字符属性区分字符。例如大写和小写字母、数学符号和标点。

例子

  • 数字千分位
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 1,234,567

用到了非单词边界、先行断言、正向否定查找、匹配数字、量词

  • 匹配 html tag 中的内容
/<span\b[^>]*>(.*?)<\/span>/

/<span\b[^>]*>(.*?)<\/span>/.exec('test')
// null
/<span\b[^>]*>(.*?)<\/span>/.exec('<span>test</span>')
// ["<span>test</span>", "test"]
/<span\b[^>]*>(.*?)<\/span>/.exec('<span class="x">test</span>')
// ["<span class="x">test</span>", "test"]

参考文献:

@shaozj shaozj self-assigned this Aug 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant