JavaScript 正则表达式迷你书(1.1版)
# 1. 两种模糊匹配
横向模糊匹配。
- 横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。
- 其实现的方式是使用量词。譬如 {m,n},表示连续出现最少 m 次,最多 n 次。
纵向模糊匹配。
- 纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。
- 其实现的方式是使用字符组。譬如 [abc],表示该字符是可以字符 "a"、"b"、"c" 中的任何一个。
# 2. 字符组
范围表示法。
- 因为连字符有特殊用途,那么要匹配 "a"、"-"、"z" 这三者中任意一个字符,该怎么做呢?
- 可以写成如下的方式:[-az] 或 [az-] 或 [a-z]。
- 即要么放在开头,要么放在结尾,要么转义。
常见的简写形式。
如果要匹配任意字符怎么办?可以使用 [\d\D]、[\w\W]、[\s\S] 和 [^] 中任何的一个。
# 3. 量词
贪婪匹配与惰性匹配。
通过在量词后面加个问号就能实现惰性匹配。
// 贪婪匹配 var regex = /\d{2,5}/g var string = '123 1234 12345 123456' console.log(string.match(regex)) // => ["123", "1234", "12345", "12345"] // 惰性匹配 var regex = /\d{2,5}?/g var string = '123 1234 12345 123456' console.log(string.match(regex)) // => ["12", "12", "34", "12", "34", "12", "34", "56"]
1
2
3
4
5
6
7
8
9
10
11
# 4. 正则表达式字符匹配攻略 - 案例分析
匹配 16 进制颜色值。
- 要求匹配:#ffbbad、#Fc01DF、#FFF、#ffE。
- 分析:表示一个 16 进制字符,可以用字符组 [0-9a-fA-F]。其中字符可以出现 3 或 6 次,需要是用量词和分支结构。使用分支结构时,需要注意顺序。
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g var string = '#ffbbad #Fc01DF #FFF #ffE' console.log(string.match(regex)) // => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
1
2
3
4匹配时间。
- 要求匹配:23:59、02:07。
- 分析:共 4 位数字,第一位数字可以为 [0-2]。当第 1 位为 "2" 时,第 2 位可以为 [0-3],其他情况时,第 2 位为 [0-9]。第 3 位数字为 [0-5],第 4 位为 [0-9]。
var regex = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/ console.log(regex.test('23:59')) console.log(regex.test('02:07')) // => true // => true
1
2
3
4
5匹配日期。
- 要求匹配:2017-06-10。
- 分析:年,四位数字即可,可用 [0-9]{4}。月,共 12 个月,分两种情况 "01"、"02"、…、"09" 和 "10"、"11"、"12",可用 (0[1-9]|1[0-2])。日,最大 31 天,可用 (0[1-9]|[12][0-9]|3[01])。
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/ console.log(regex.test('2017-06-10')) // => true
1
2
3window 操作系统文件路径。
var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/ console.log( regex.test('F:\\study\\javascript\\regex\\regular expression.pdf') ) console.log(regex.test('F:\\study\\javascript\\regex\\')) console.log(regex.test('F:\\study\\javascript')) console.log(regex.test('F:\\')) // => true // => true // => true // => true
1
2
3
4
5
6
7
8
9
10
11匹配 id。
惰性匹配会有回溯问题,效率比较低。
var regex = /id=".*?"/ var string = '<div id="container" class="main"></div>' console.log(string.match(regex)[0]) // => id="container"
1
2
3
4优化。
var regex = /id="[^"]*"/ var string = '<div id="container" class="main"></div>' console.log(string.match(regex)[0]) // => id="container"
1
2
3
4
# 5. 如何匹配位置呢?
^(脱字符)匹配开头,在多行匹配中匹配行开头。$(美元符号)匹配结尾,在多行匹配中匹配行结尾。
var result = 'hello'.replace(/^|$/g, '#') console.log(result) // => "#hello#"
1
2
3\b 是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置。
var result = '[JS] Lesson_01.mp4'.replace(/\b/g, '#') console.log(result) // => "[#JS#] #Lesson_01#.#mp4#"
1
2
3\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。具体说来就是 \w 与 \w、 \W 与 \W、^ 与 \W,\W 与 $ 之间的位置。
var result = '[JS] Lesson_01.mp4'.replace(/\B/g, '#') console.log(result) // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
1
2
3(?=p),其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p。
var result = 'hello'.replace(/(?=l)/g, '#') console.log(result) // => "he#l#lo"
1
2
3而 (?!p) 就是 (?=p) 的反面意思。
var result = 'hello'.replace(/(?!l)/g, '#') console.log(result) // => "#h#ell#o#"
1
2
3
# 6. 正则表达式括号的作用 - 相关案例
不匹配任何东西的正则。
因为此正则要求只有一个字符,但该字符后面是开头,而这样的字符串是不存在的。
var regex = /.^/
1数字的千位分隔符表示法。
把 "12345678",变成 "12,345,678"。
var regex = /(?!^)(?=(\d{3})+$)/g var result = '12345678'.replace(regex, ',') console.log(result) // => "12,345,678" result = '123456789'.replace(regex, ',') console.log(result) // => "123,456,789"
1
2
3
4
5
6
7把 "12345678 123456789" 替换成 "12,345,678 123,456,789"。
var string = '12345678 123456789', regex = /\B(?=(\d{3})+\b)/g var result = string.replace(regex, ',') console.log(result) // => "12,345,678 123,456,789"
1
2
3
4
51888 格式化成 $ 1888.00。
function format(num) { return num .toFixed(2) .replace(/\B(?=(\d{3})+\b)/g, ',') .replace(/^/, '$$ ') } console.log(format(1888)) // => "$ 1,888.00"
1
2
3
4
5
6
7
8验证密码问题。
- 要求:密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。
- 下面的正则看起来比较复杂,只要理解了第二步,其余就全部理解了。/(?=.[0-9])[1]{6,12}$/对于这个正则,我们只需要弄明白 (?=.[0-9])^ 即可。分开来看就是 (?=.[0-9]) 和 ^。表示开头前面还有个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。(?=.[0-9]) 表示该位置后面的字符匹配 .*[0-9],即,有任何多个任意字符,后面再跟个数字。翻译成大白话,就是接下来的字符,必须包含个数字。
var regex = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A- Z]))^[0-9A-Za-z]{6,12}$/; console.log( regex.test("1234567") ); // false 全是数字 console.log( regex.test("abcdef") ); // false 全是小写字母 console.log( regex.test("ABCDEFGH") ); // false 全是大写字母 console.log( regex.test("ab23C") ); // false 不足6位 console.log( regex.test("ABCDEF234") ); // true 大写字母和数字 console.log( regex.test("abcdEF234") ); // true 三者都有
1
2
3
4
5
6
7
8“至少包含两种字符” 的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。
var regex = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/ console.log(regex.test('1234567')) // false 全是数字 console.log(regex.test('abcdef')) // false 全是小写字母 console.log(regex.test('ABCDEFGH')) // false 全是大写字母 console.log(regex.test('ab23C')) // false 不足6位 console.log(regex.test('ABCDEF234')) // true 大写字母和数字 console.log(regex.test('abcdEF234')) // true 三者都有
1
2
3
4
5
6
7
# 7. 分组引用
提取数据。
提取出年、月、日。
var regex = /(\d{4})-(\d{2})-(\d{2})/ var string = '2017-06-12' regex.test(string) // 正则操作即可,例如 //regex.exec(string); //string.match(regex); console.log(RegExp.$1) // "2017" console.log(RegExp.$2) // "06" console.log(RegExp.$3) // "12"
1
2
3
4
5
6
7
8替换。
把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy。
var regex = /(\d{4})-(\d{2})-(\d{2})/ var string = '2017-06-12' var result = string.replace(regex, '$2/$3/$1') console.log(result) // => "06/12/2017"
1
2
3
4
5
# 8. 反向引用
要写一个正则支持匹配如下三种格式。
- 要求:2016-06-12、2016/06/12、2016.06.12。
- 注意里面的 \1,表示的引用之前的那个分组 (-|/|.)。不管它匹配到什么(比如 -),\1 都匹配那个同样的具体某个字符。
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/ var string1 = '2017-06-12' var string2 = '2017/06/12' var string3 = '2017.06.12' var string4 = '2016-06/12' console.log(regex.test(string1)) // true console.log(regex.test(string2)) // true console.log(regex.test(string3)) // true console.log(regex.test(string4)) // false
1
2
3
4
5
6
7
8
9\10 表示什么呢?
- 另外一个疑问可能是,即 \10 是表示第 10 个分组,还是 \1 和 0 呢?
- 答案是前者,虽然一个正则里出现 \10 比较罕见。
var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/ var string = '123456789# ######' console.log(regex.test(string)) // => true
1
2
3
4引用不存在的分组会怎样?
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如 \2,就匹配 "\2",注意 "\2" 表示对 "2" 进行了转义。
var regex = /\1\2\3\4\5\6\7\8\9/ console.log(regex.test('\1\2\3\4\5\6\789')) console.log('\1\2\3\4\5\6\789'.split(''))
1
2
3分组后面有量词会怎样?
分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。
var regex = /(\d)+/ var string = '12345' console.log(string.match(regex)) // => ["12345", "5", index: 0, input: "12345"]
1
2
3
4非捕获括号。
- 之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分支。
- 如果只想要括号最原始的功能,但不会引用它,即,既不在 API 里引用,也不在正则里反向引用。此时可以使用非捕获括号 (?:p) 和 (?:p1|p2|p3)。
# 9. 正则表达式位置匹配攻略 - 相关案例
字符串 trim 方法模拟。
第一种,匹配到开头和结尾的空白符,然后替换成空字符。
function trim(str) { return str.replace(/^\s+|\s+$/g, '') } console.log(trim(' foobar ')) // => "foobar"
1
2
3
4
5第二种,匹配整个字符串,然后用引用来提取出相应的数据。这里使用了惰性匹配 *?,不然也会匹配最后一个空格之前的所有空格的。当然,前者效率高。
function trim(str) { return str.replace(/^\s*(.*?)\s*$/g, '$1') } console.log(trim(' foobar ')) // => "foobar"
1
2
3
4
5将每个单词的首字母转换为大写。
思路是找到每个单词的首字母,当然这里不使用非捕获匹配也是可以的。
function titleize(str) { return str.toLowerCase().replace(/(?:^|\s)\w/g, function(c) { return c.toUpperCase() }) } console.log(titleize('my name is epeli')) // => "My Name Is Epeli"
1
2
3
4
5
6
7驼峰化。
其中分组 (.) 表示首字母,单词的界定是,前面的字符可以是多个连字符、下划线以及空白符。正则后面的 ? 的目的,是为了应对 str 尾部的字符可能不是单词字符,比如 str 是 '-moz-transform '。
function camelize(str) { return str.replace(/[-_\s]+(.)?/g, function(match, c) { return c ? c.toUpperCase() : '' }) } console.log(camelize('-moz-transform')) // => "MozTransform"
1
2
3
4
5
6
7中划线化。
驼峰化的逆过程。
function dasherize(str) { return str .replace(/([A-Z])/g, '-$1') .replace(/[-_\s]+/g, '-') .toLowerCase() } console.log(dasherize('MozTransform')) // => "-moz-transform"
1
2
3
4
5
6
7
8HTML 转义和反转义。
其中使用了用构造函数生成的正则,然后替换相应的格式就行了。
// 将HTML特殊字符转换成等值的实体 function escapeHTML(str) { var escapeChars = { '<': 'lt', '>': 'gt', '"': 'quot', '&': 'amp', "'": '#39', } return str.replace( new RegExp('[' + Object.keys(escapeChars).join('') + ']', 'g'), function(match) { return '&' + escapeChars[match] + ';' } ) } console.log(escapeHTML('<div>Blah blah blah</div>')) // => "<div>Blah blah blah</div>";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18倒是它的逆过程,使用了括号,以便提供引用。通过 key 获取相应的分组引用,然后作为对象的键。
// 实体字符转换为等值的HTML。 function unescapeHTML(str) { var htmlEntities = { nbsp: ' ', lt: '<', gt: '>', quot: '"', amp: '&', apos: "'", } return str.replace(/\&([^;]+);/g, function(match, key) { if (key in htmlEntities) { return htmlEntities[key] } return match }) } console.log(unescapeHTML('<div>Blah blah blah</div>')) // => "<div>Blah blah blah</div>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19匹配成对标签。
匹配一个开标签,可以使用正则 <[^>]+>,匹配一个闭标签,可以使用 </[^>]+>,但是要求匹配成对标签,那就需要使用反向引用
var regex = /<([^>]+)>[\d\D]*<\/\1>/ var string1 = '<title>regular expression</title>' var string2 = '<p>laoyao bye bye</p>' var string3 = '<title>wrong!</p>' console.log(regex.test(string1)) // true console.log(regex.test(string2)) // true console.log(regex.test(string3)) // false
1
2
3
4
5
6
7
# 10. 正则表达式回溯法原理
其实回溯法,很容易掌握的。简单总结就是,正因为有多种可能,所以要一个一个试。直到,要么到某一步时,整体匹配成功了。要么最后都试完后,发现整体匹配不成功。
既然有回溯的过程,那么匹配效率肯定低一些。相对谁呢?相对那些 DFA 引擎, DFA 是 “确定型有限自动机” 的简写。
而 JavaScript 的正则引擎是 NFA,NFA 是 “非确定型有限自动机” 的简写。大部分语言中的正则都是 NFA,为啥它这么流行呢?
答:你别看我匹配慢,但是我编译快啊,而且我还有趣哦。
# 11. 结构和操作符
结构 | 说明 |
---|---|
字面量 | 匹配一个具体字符,包括不用转义的和需要转义的。比如 a 匹配字符 "a",又比如 \n 匹配换行符,又比如 . 匹配小数点 |
字符组 | 匹配一个字符,可以是多种可能之一,比如 [0-9],表示匹配一个数字,也有 \d 的简写形式,另外还有反义字符组,表示可以是除了特定字符之外任何一个字符,比如 [^0-9],表示一个非数字字符,也有 \D 的简写形式 |
量词 | 表示一个字符连续出现,比如 a{1,3} 表示 "a" 字符连续出现 3 次。另外还有常见的简写形式,比如 a+ 表示 "a" 字符连续出现至少一次 |
锚 | 匹配一个位置,而不是字符。比如 ^ 匹配字符串的开头,又比如 \b 匹配单词边界,又比如 (?=\d) 表示数字前面的位置 |
分组 | 用括号表示一个整体,比如 (ab)+,表示 "ab" 两个字符连续出现多次,也可以使用非捕获分组 (?:ab)+ |
分支 | 多个子表达式多选一,比如 abc|bcd,表达式匹配 "abc" 或者 "bcd" 字符子串。反向引用,比如 \2,表示引用第 2 个分组 |
操作符描述 | 操作符 | 优先级 |
---|---|---|
转义符 | \ | 1 |
括号和方括号 | (…)、(?:…)、(?=…)、(?!…)、[…] | 2 |
量词限定符 | {m}、{m,n}、{m,}、?、*、+ | 3 |
位置和序列 | ^、$、\元字符、一般字符 | 4 |
管道符(竖杠) | | | 5 |
# 12. 正则表达式的拆分 - 案例分析
身份证。
- 正则表达式是:/^(\d{15}|\d{17}[\dxX])$/。
- 因为竖杠 | 的优先级最低,所以正则分成了两部分 \d{15} 和 \d{17}[\dxX]。\d{15} 表示 15 位连续数字。\d{17}[\dxX] 表示 17 位连续数字,最后一位可以是数字,可以大小写字母 "x"。
IPV4 地址。
- 正则表达式是:/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]).){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/。
- 这个正则,看起来非常吓人。但是熟悉优先级后,会立马得出如下的结构:((…).){3}(…)其中,两个 (…) 是一样的结构。表示匹配的是 3 位数字。因此整个结构是 3 位数.3 位数.3 位数.3 位数。
- 然后再来分析 (…):(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])它是一个多选结构,分成 5 个部分:
- 0{0,2}\d,匹配一位数,包括 "0" 补齐的。比如,"9"、"09"、"009"。
- 0?\d{2},匹配两位数,包括 "0" 补齐的,也包括一位数。
- 1\d{2},匹配 "100" 到 "199";
- 2[0-4]\d,匹配 "200" 到 "249"。
- 25[0-5],匹配 "250" 到 "255"
# 13. replace 是很强大的
- replace 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。当第二个参数是字符串时,如下的字符有特殊的含义。
属性 | 描述 |
---|---|
$1,$2,…,$99 | 匹配第 1-99 个 分组里捕获的文本 |
$& | 匹配到的子串文本 |
$` | 匹配到的子串的左边文 |
$' | 匹配到的子串的右边文本 |
$$ | 美元符号 |
把 "2,3,5",变成 "5=2+3"。
var result = '2,3,5'.replace(/(\d+),(\d+),(\d+)/, '$3=$1+$2')
console.log(result)
// => "5=2+3"
2
3
把 "2,3,5",变成 "222,333,555"。
var result = '2,3,5'.replace(/(\d+)/g, '$&$&$&')
console.log(result)
// => "222,333,555"
2
3
把 "2+3=5",变成 "2+3=2+3=5=5"。要把 "2+3=5",变成 "2+3=2+3=5=5",其实就是想办法把 = 替换成=2+3=5=,其中,$& 匹配的是 =, ' 匹配的是 5。因此使用 "&`&'$&" 便达成了目的。
var result = '2+3=5'.replace(/=/, "$&$`$&$'$&")
console.log(result)
// => "2+3=2+3=5=5"
2
3
- 当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么?
'1234 2345 3456'.replace(/(\d)\d{2}(\d)/g, function(
match,
$1,
$2,
index,
input
) {
console.log([match, $1, $2, index, input])
})
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]
2
3
4
5
6
7
8
9
10
11
12
0-9A-Za-z ↩︎