贪婪的正则

avatar

正则表达式

我们使用正则表达式,通常都是去搜索引擎上去寻找我们要用的正则表达式,很少自己去书写一个正则,完成某个功能。其实正则表达式没有我们想的那么复杂,只要了解他的工作原理,我们自己也能写出漂亮的正则。

image.png

创建一个正则对象

  1. 采用“ / 模式 / 修饰符 ”直接创建,regexp = /pattern/gmi,简单方便,但是无法插入变量。
  2. new一个出来,regexp = new RegExp("pattern", "flags"),采用new模式,这样就可以使用变量了。
let search = prompt("What you want to search?", "love");
let regexp = new RegExp(search);
// 找到用户想要的任何东西
alert( "I love JavaScript".search(regexp));

修饰符

我们创建正则时,可以在模式部分输入我们的匹配规则,并且使用一些字符类,简化我们的搜索条件,再使用修饰符去帮助我们进行搜索,常用的修饰符有g、i、m。

  • g:表示全局进行匹配,会找到所有的匹配项。
  • i:表示匹配时不区分字母的大小写。
  • m:表示可以采取多行匹配模式。 多个修饰符可以一起使用,也可以单独使用,当然也可以不使用。

字符类

通过使用字符类,可以简化匹配模式的书写,常用的字符类有/d、/w、/s

  • \d(“d” 来自 “digit”)数字:从 0 到 9 的字符。
  • \s(“s” 来自 “space”)空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v,\f 和 \r。
  • \w(“w” 来自 “word”)“单字”字符:拉丁字母或数字或下划线 _。非拉丁字母(如西里尔字母或印地文)不属于 \w。

当然还有,(.)点是一种特殊字符类,它与 “除换行符之外的任何字符” 匹配。

反向类

对于每个字符类,都有一个与之对应的反向类,以大写的字母表示

  • \D 非数字:除 \d 以外的任何字符,例如字母。
  • \S 非空格符号:除 \s 以外的任何字符,例如字母。
  • \W 非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。

锚点类

"^"匹配字符串开始,"$"匹配字符串结尾,可以用来测试是否完全匹配,例如测试用户输入的类型是否正确,如下

let goodInput = "12:34";
let badInput = "12:345";

let regexp = /^\d\d:\d\d$/;
alert( regexp.test(goodInput) ); // true
alert( regexp.test(badInput) ); // false

集合和范围

在方括号 […] 中的几个字符或者字符类意味着“搜索给定的字符中的任意一个”, 比如说,[eao] 意味着查找在 3 个字符 'a'、'e' 或者 `‘o’ 中的任意一个。

方括号也可以包含字符范围 ,比如说,[a-z] 会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字。

  • \d —— 和 [0-9] 相同
  • \w —— 和 [a-zA-Z0-9_] 相同
  • \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 unicode 空格字符相同 除了普通的范围匹配,还有类似 [^…] 的“排除”范围匹配。

它们通过在匹配查询的开头添加插入符号 ^ 来表示,它会匹配所有除了给定的字符之外的任意字符。

  • [^aeyo] —— 匹配任何除了 'a'、'e'、'y' 或者 'o' 之外的字符
  • [^0-9] —— 匹配任何除了数字之外的字符,也可以使用 \D 来表示
  • [^\s] —— 匹配任何非空字符,也可以使用 \S 来表示 通常当我们的确需要查询点字符时,我们需要把它转义成像 \. 这样的形式。如果我们需要查询一个反斜杠,我们需要使用 \\。

一个在方括号中的点符号 "." 表示的就是一个点字符。 查询模式 [.,] 将会寻找一个为点或者逗号的字符。

转义

对量词的简写,以及“.”字符有时候也是我们想要操作的字符,我们可以使用转义符号“\”,帮助我们拿到相应的特殊字符,例如:

  • alert( "Chapter 5.1".match(/\d\.\d/) ); // 5.1,以“\.”对"."进行转义
  • alert( "function g()".match(/g\(\)/) ); // "g()",以“\(,\)”对"()"进行转义
  • alert( "1\2".match(/\\/) ); // '\',以“\\”对"\"进行转义 搜索特殊字符时 “[ \ ^ $ . | ? * + ( )”,我们需要在它们前面加上反斜杠 “\”进行转义。

另外,传递一个字符串(参数)给 new RegExp 时,我们需要双倍反斜杠 \\,因为字符串引号会消费其中的一个。

量词

当需要匹配多个相同类型的字符时,可以采用量词,最基本的可以使用{n}例如:

  • \d{5} 表示 5 位的数字,如同 \d\d\d\d\d 当然,我们还可以用来查找某个范围内的数据例如:
  • alert( "I'm not 12, but 1234 years old".match(/\d{3,5}/) ); // "1234"
  • alert( "I'm not 12, but 345678 years old".match(/\d{3,}/) ); // "345678" 我们还可以对量词进行缩写
  • “+”代表“一个或多个”,相当于 {1,}
  • “?”代表“零个或一个”,相当于 {0,1},换句话说,它使得符号变得可选
  • ”*“代表着“零个或多个”,相当于 {0,},也就是说,这个字符可以多次出现或不出现

贪婪模式

在使用量词的时候我们就要小心注意了

let reg = /".+"/g;这里我们想要获取的是“witch”和”broom”

let str = 'a "witch" and her "broom" is one';

alert( str.match(reg) ); // "witch" and her "broom"

显然,我们使用量词不当,出现了意料之外的情况,这是为什么呢?

这其实就是正则的一个匹配模式,叫做贪婪模式,在贪婪模式下进行匹配会把所有满足匹配模式的字符进行匹配,直到不满足匹配规则,或则直到字符串结束,然后再开始回溯,去寻找满足结束符号的字符。

所以我们使用/“.+”/g时会匹配到距离最远的两个“”之间的字符,而不是每两个“之间的内容。

找到满足匹配规则的第一个引号“ image.png 按照".+"匹配规则向下接着匹配 image.png 直到匹配到字符串结束 image.png 这时开始回溯,寻找结束的引号“ image.png 一直回溯到第一个引号”,出现才结束寻找 image.png 这样才完成了贪婪模式下的一次字符串匹配,显然这不是我们想要的结果,如果我们只是想要“witch”和”broom“,就要使用惰性量词来帮助我们拿到数据。

懒惰模式

通常,一个问号 ? 就是一个它本身的量词(0 或 1),但如果添加另一个量词(甚至可以是它自己),就会有不同的意思 —— 它将匹配的模式从贪婪转为懒惰。 再懒惰模式下,正则会想马上结束匹配,会在满足匹配的开始规则之后就寻找匹配结束的标志。

let reg = /".+?"/g;

let str = 'a "witch" and her "broom" is one';

alert( str.match(reg) ); // witch, broom

首先还是先匹配到开始字符引号“ image.png 然后按照.寻找匹配项 image.png 接下来就是搜索过程出现不同的时候了。因为我们对 +? 启用了懒惰模式,引擎不会去尝试多匹配一个点,并且开始了对剩余的 '"' 的匹配,如果有一个引号,搜索就会停止,但是有一个 'i',所以没有匹配到引号 image.png 接着,正则表达式引擎增加对点的重复搜索次数,并且再次尝试,直到模式中的剩余部分找到匹配项 image.png 接下来的搜索工作从当前匹配结束的那一项开始,就会再产生一个结果 image.png 这样通过开启懒惰模式成功的拿到了我们想要的数据,到这里相信大家也知道了,为什么自己写的正则看起来没什么毛病,但是就是拿不到想要的数据,多半就是贪婪模式在作怪,我们真正的了解正则的贪婪与懒惰模式之后,相信大家可以从容的写出正则,来解决问题了。

捕获组

当我们只需要复杂字符串中一部分字符时,可以采用捕获组的方式,拿到我们想要的数据。 模式的一部分可以用括号括起来 (...),这称为“捕获组(capturing group)”。

  • 它允许将匹配的一部分作为结果数组中的单独项。
  • 如果我们将量词放在括号后,则它将括号视为一个整体。 例如:
alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo"

当然我们还可以进行嵌套使用,例如:

let str = '<span class="my">';

let regexp = /<(([a-z]+)\s*([^>]*))>/;

let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"

image.png 有了嵌套组,我们就可以轻易拿到任何我们想要字符了。

如果以后再有要用到正则的地方,我们就可以自己动手,丰衣足食啦~

常用正则与字符串方法

  1. match方法 str.match(regexp) 方法在字符串 str 中找到匹配 regexp 的字符。 如果 regexp 不带有 g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 index(匹配项的位置)、input(输入字符串,等于 str)例如:
let str = "I love JavaScript";

let result = str.match(/Java(Script)/);

alert( result[0] );     // JavaScript(完全匹配)
alert( result[1] );     // Script(第一个分组)
alert( result.length ); // 2

// 其他信息:
alert( result.index );  // 7(匹配位置)
alert( result.input );  // I love JavaScript(源字符串)

如果regexp带有g标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息。

let str = "I love JavaScript";

let result = str.match(/Java(Script)/g);

alert( result[0] ); // JavaScript
alert( result.length ); // 1

如果没有匹配项,则无论是否带有标记 g ,都将返回 null。

let str = "I love JavaScript";

let result = str.match(/HTML/);

alert(result); // null
alert(result.length); // Error: Cannot read property 'length' of null
  1. matchAll方法 主要用来搜索所有组的所有匹配项,与 match 相比有 3 个区别:
  • 它返回包含匹配项的可迭代对象,而不是数组。我们可以用 Array.from 从中得到一个常规数组。
  • 每个匹配项均以包含分组的数组形式返回(返回格式与不带 g 标记的 str.match 相同)。
  • 如果没有结果,则返回的不是 null,而是一个空的可迭代对象。
let str = '<h1>Hello, world!</h1>';
let regexp = /<(.*?)>/g;

let matchAll = str.matchAll(regexp);

alert(matchAll); // [object RegExp String Iterator],不是数组,而是一个可迭代对象

matchAll = Array.from(matchAll); // 现在返回的是数组

let firstMatch = matchAll[0];
alert( firstMatch[0] );  // <h1>
alert( firstMatch[1] );  // h1
alert( firstMatch.index );  // 0
alert( firstMatch.input );  // <h1>Hello, world!</h1>
  1. search方法 方法 str.search(regexp) 返回第一个匹配项的位置,如果未找到,则返回 -1。

重要限制:search 仅查找第一个匹配项。

let str = "A drop of ink may make a million think";

alert( str.search( /ink/i ) ); // 10(第一个匹配位置)

如果需要其他匹配项的位置,则应使用其他方法,例如用 str.matchAll(regexp) 查找所有位置。

  1. replace方法 用于搜索和替换的通用方法,是最有用的方法之一,传入两个参数,当replace 的第一个参数是字符串时,它仅替换第一个匹配项。 例如:
// 用冒号替换连字符
alert('12-34-56'.replace("-", ":")) // 12:34-56

如果想要替换全部的‘-’则需要正则的帮助

// 将连字符替换为冒号
alert( '12-34-56'.replace( /-/g, ":" ) )  // 12:34:56

我们可以看到,第二个参数就是要替换的字符串,我们还可以使用特殊字符

  • $& 表示插入整个匹配项

  • $` 表示在匹配项之前插入字符串的一部分

  • $' 表示在匹配项之后插入字符串的一部分

  • $n 表示如果 n 是一个 1 到 2 位的数字,则插入第 n 个分组的内容

  • $<name> 表示插入带有给定 name 的括号内的内容

  • $$ 表示插入字符 $ 例如:

let str = "John Smith";

// 交换名字和姓氏
alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

当我们需要一些复杂的操作的时候,第二个参数还可以时一个函数,例如:

let str = "html and css";

let result = str.replace(/html|css/gi, str => str.toUpperCase());

alert(result); // HTML and CSS
  1. test方法 方法 regexp.test(str) 查找匹配项,然后返回 true/false 表示是否存在。
let str = "I love JavaScript";

// 这两个测试相同
alert( /love/i.test(str) ); // true
alert( str.search(/love/i) != -1 ); // true

现在我们对正则已经有了一个系统的了解了,我们来利用正则去解析html代码,去获取一个DOM树来看看吧

正则获取HTML代码组成DOM标签树

当我们拿到一串html代码的时候,它可能包括很多标签,而我们所关心的只是htnl中的dom结构标签,对于像script,style,以及注释标签等等,我们并不想要,这是我们可以先对拿到的htnl代码进行处理。

html = html.replace(/<script(.|\n)+?<\/script>/g, '');//替换掉html代码字符串中的script标签
html = html.replace(/<style(.|\n)+?<\/style>/g, '');//替换掉html代码字符串中的style标签
html = html.replace(/<!--(.|\n)*?-->/g, '');//替换掉html代码字符串中的注释标签
html = html.replace(/<noscript>(.|\n)*?<\/noscript>/gi, '');替换掉html代码字符串中的noscript标签

这时我们可以获得一个只包含html结构代码的字符串了,那么我们如何处理去拿到对应的DOM标签构成我们的标签树呢?

采用堆栈的方式处理标签

当我们拿到html代码字符串之后,可以根据<>|</>把DOM结构标签分别拿出来,我们每拿到一个标签,就让它入栈,每次入栈之前先与栈顶元素进行一次匹配,如果本次入栈标签与栈顶标签是同一个那么取出栈顶元素,放到根标签的子元素下,如果入栈元素与栈顶元素不是同一个标签,那么让该入栈标签放入栈顶元素的子元素中,这样一趟走下来,就可以获得到一个完整的DOM结构了。那么我们要做的就是,拿到各个标签,为标签分类,加状态,看它是单标签,还是双标签,双标签可以把结束的标签</>做一个标记,表示标签闭合,与<>相对比,如果是同个双标签就从栈顶取出,放入根标签。如果是单标签,直接标记,加入到栈顶元素的子元素中。例如有如下一段HTML代码

<div>
    <h1>
    </h1>
    <div>
        <div>
            <img/>
        </div>
    </div>
</div>

我们假设现在栈顶有一个root元素,那么整体的分析最外层的div首先入栈,与root对比,加入到root下到child数组中

接着h1入栈,与栈顶元素div比较,之后h1入栈

继续/h1入栈前与栈顶元素对比,与h1为一对,那么取出h1,现在栈顶为div

将h1加入到div的child数组中

接着读取到下一个div元素,与栈顶div对比,不符合闭合标签,入栈

在往下,又读取到div与栈顶div对比,还不是闭合的,继续入栈

再继续,读到img标签,是一个单标签,那么将img加入到栈顶元素div的child数组中 遇到这种单标签我们可以预先处理一下

const TAG_REG = /<(\/?)([a-z][^\s>\/]*)((?:.|\n)*?)(>|\/>)/g;
const SELF_CLOSE_TAG = ['br', 'img', 'hr', 'input', 'link', 'source', 'meta'];
for(let tag of SELF_CLOSE_TAG){
    let reg = new RegExp('<'+tag+'((.|\\n)*?)(\\/>|>)(<\\/'+tag+'>)?', 'gi');
    html = html.replace(reg, '<'+tag+'$1'+'/>');
}
for(let r of html.matchAll(TAG_REG)) {
    let isSelfCloseTag = r[4]=='/>';
        if(SELF_CLOSE_TAG.includes(tag)){
            isSelfCloseTag = true;
        }
}

再往下走,读取到/div,与栈顶div组成双标签,那么将栈顶div取出,此时栈内还有两个div元素,将取出的放入栈顶的div的child数组中依次循环往复,最终读取到最后的/div,放入root的child中形成我们所要的HTML代码DOM标签树。

{
        "tag": "div",
        "child": [
            {
                "tag": "h1",
                 "child": []
            },
            {
                "tag": "div",
                "child": [
                    {
                        "tag": "div",
                        "child": [
                            {
                                "tag": "img",
                                "child": []
                            }
                        ]
                    }
                ]
            }
}