第 10 章: 正则表达式
¶ 在前面的章节中,我们多次需要在字符串值中查找模式。在 第 4 章 中,我们通过写出日期中数字所在的确切位置来从字符串中提取日期值。后来,在 第 6 章 中,我们看到了一些非常难看的代码,用于在字符串中查找特定类型的字符,例如必须在 HTML 输出中转义的字符。
¶ 正则表达式是一种用于描述字符串模式的语言。它们形成了一种小型、独立的语言,被嵌入到 JavaScript(以及各种其他编程语言中,以某种方式)。它不是一种非常易读的语言——大型正则表达式往往完全不可读。但是,它是一个有用的工具,可以真正简化字符串处理程序。
¶ 就像字符串用引号括起来一样,正则表达式模式用斜杠 (/
) 括起来。这意味着表达式中的斜杠必须用反斜杠转义。
var slash = /\//; show("AC/DC".search(slash));
¶ search
方法类似于 indexOf
,但它搜索的是正则表达式而不是字符串。正则表达式指定的模式可以做一些字符串无法做的事情。首先,它们允许某些元素匹配多个字符。在 第 6 章 中,从文档中提取标记时,我们需要找到字符串中的第一个星号或左大括号。这可以用以下方法完成:
var asteriskOrBrace = /[\{\*]/; var story = "We noticed the *giant sloth*, hanging from a giant branch."; show(story.search(asteriskOrBrace));
¶ [
和 ]
字符在正则表达式中具有特殊含义。它们可以包含一组字符,意思是“这些字符中的任何一个”。大多数非字母数字字符在正则表达式中都有特殊含义,因此当您使用它们来表示实际字符时,最好始终用反斜杠1 转义它们。
¶ 有一些常用的字符集快捷方式。点 (.
) 可以用来表示“除换行符之外的任何字符”,转义的“d”(\d
)表示“任何数字”,转义的“w”(\w
)匹配任何字母数字字符(包括下划线,出于某种原因),转义的“s”(\s
)匹配任何空白字符(制表符、换行符、空格)。
var digitSurroundedBySpace = /\s\d\s/; show("1a 2 3d".search(digitSurroundedBySpace));
¶ 转义的“d”、“w”和“s”可以被它们的字母大写替换,表示它们的相反意思。例如,\S
匹配任何非空白字符。在使用 [
和 ]
时,可以使用 ^
字符来反转模式:
var notABC = /[^ABC]/; show("ABCBACCBBADABC".search(notABC));
¶ 如您所见,正则表达式使用字符来表达模式的方式使得它们 A)非常短,B)非常难读。
¶ 编写一个正则表达式,匹配格式为 "XX/XX/XXXX"
的日期,其中 X
是数字。针对字符串 "born 15/11/2003 (mother Spot): White Fang"
测试它。
var datePattern = /\d\d\/\d\d\/\d\d\d\d/; show("born 15/11/2003 (mother Spot): White Fang".search(datePattern));
¶ 有时您需要确保模式从字符串开头开始或从字符串结尾结束。为此,可以使用特殊字符 ^
和 $
。第一个匹配字符串的开头,第二个匹配字符串的结尾。
show(/a+/.test("blah")); show(/^a+$/.test("blah"));
¶ 第一个正则表达式匹配任何包含 a
字符的字符串,第二个只匹配完全由 a
字符组成的字符串。
¶ 注意,正则表达式是对象,并且具有方法。它们的 test
方法返回一个布尔值,表示给定字符串是否与表达式匹配。
¶ 代码 \b
匹配“词边界”,可以是标点符号、空白符或字符串的开头或结尾。
show(/cat/.test("concatenate")); show(/\bcat\b/.test("concatenate"));
¶ 模式的一部分可以重复多次。在元素后添加星号 (*
) 允许它重复任意次数,包括零次。加号 (+
) 做同样的事情,但要求模式至少出现一次。问号 (?
) 使元素“可选”——它可以出现零次或一次。
var parenthesizedText = /\(.*\)/; show("Its (the sloth's) claws were gigantic!".search(parenthesizedText));
¶ 必要时,可以使用大括号来更精确地指定元素出现的次数。大括号中的数字 ({4}
) 给出了它必须出现的次数。两个数字之间用逗号隔开 ({3,10}
) 表示模式必须至少出现与第一个数字一样多次,最多出现与第二个数字一样多次。类似地,{2,}
表示两次或更多次出现,而 {,4}
表示四次或更少次出现。
var datePattern = /\d{1,2}\/\d\d?\/\d{4}/; show("born 15/11/2003 (mother Spot): White Fang".search(datePattern));
¶ 片段 /\d{1,2}/
和 /\d\d?/
都表示“一位或两位数字”。
¶ 编写一个匹配电子邮件地址的模式。为简单起见,假设 @
符号之前和之后的部分只包含字母数字字符和 .
和 -
(点和连字符)字符,而地址的最后一部分(最后一个点之后的国家代码)可能只包含字母数字字符,并且必须是两个或三个字符长。
var mailAddress = /\b[\w\.-]+@[\w\.-]+\.\w{2,3}\b/; show(mailAddress.test("[email protected]")); show(mailAddress.test("I mailt [email protected], but it didn wrok!")); show(mailAddress.test("[email protected]"));
¶ 模式开头和结尾处的 \b
确保第二个字符串不匹配。
¶ 正则表达式的一部分可以用括号分组。这使我们能够对多个字符使用 *
等。例如
var cartoonCrying = /boo(hoo+)+/i; show("Then, he exclaimed 'Boohoooohoohooo'".search(cartoonCrying));
¶ 那个正则表达式结尾的 i
从哪里来?在结束斜杠之后,可以在正则表达式中添加“选项”。这里的 i
表示表达式不区分大小写,这允许模式中的小写 B 与字符串中的大写 B 匹配。
¶ 管道字符 (|
) 用于允许模式在两个元素之间进行选择。例如
var holyCow = /(sacred|holy) (cow|bovine|bull|taurus)/i; show(holyCow.test("Sacred bovine!"));
¶ 通常,查找模式只是从字符串中提取某些内容的第一步。在前面的章节中,这种提取是通过多次调用字符串的 indexOf
和 slice
方法来完成的。现在我们已经了解了正则表达式的存在,可以使用 match
方法代替。当字符串与正则表达式匹配时,如果匹配失败,结果将为 null
;如果匹配成功,结果将为匹配字符串的数组。
show("No".match(/Yes/)); show("... yes".match(/yes/)); show("Giant Ape".match(/giant (\w+)/i));
¶ 返回数组中的第一个元素始终是与模式匹配的字符串部分。如最后一个示例所示,当模式中存在带括号的部分时,它们匹配的部分也会添加到数组中。通常,这使得提取字符串片段变得非常容易。
var parenthesized = prompt("Tell me something", "").match(/\((.*)\)/); if (parenthesized != null) print("You parenthesized '", parenthesized[1], "'");
¶ 重写我们在 第 4 章 中编写的 extractDate
函数。当给定一个字符串时,此函数查找遵循我们之前看到的日期格式的内容。如果它能找到这样的日期,它会将这些值放入 Date
对象中。否则,它会抛出异常。使它接受日期,其中日期或月份只写一位数字。
function extractDate(string) { var found = string.match(/(\d\d?)\/(\d\d?)\/(\d{4})/); if (found == null) throw new Error("No date found in '" + string + "'."); return new Date(Number(found[3]), Number(found[2]) - 1, Number(found[1])); } show(extractDate("born 5/2/2007 (mother Noog): Long-ear Johnson"));
¶ 这个版本比前一个版本稍微长一些,但它有一个优点,它实际上检查它正在做什么,并在给定无意义的输入时大声喊出来。没有正则表达式,这会困难得多——需要多次调用 indexOf
才能确定数字是一位还是两位,以及破折号是否在正确的位置。
¶ 字符串值的 replace
方法(我们在 第 6 章 中见过)可以接受一个正则表达式作为它的第一个参数。
print("Borobudur".replace(/[ou]/g, "a"));
¶ 注意正则表达式后面的 g
字符。它代表“全局”,意思是应该替换字符串中与模式匹配的每个部分。当省略此 g
时,只会替换第一个 "o"
。
¶ 有时需要保留被替换字符串的部分。例如,我们有一个大型字符串,其中包含人员的姓名,每行一个姓名,格式为“姓氏, 名字”。我们要交换这些名字,并删除逗号,以获得简单的“名字 姓氏”格式。
var names = "Picasso, Pablo\nGauguin, Paul\nVan Gogh, Vincent"; print(names.replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
¶ 替换字符串中的 $1
和 $2
指的是模式中的带括号的部分。$1
被替换为与第一对括号匹配的文本,$2
被替换为与第二对括号匹配的文本,依此类推,一直到 $9
。
¶ 如果您在模式中有超过 9 个带括号的部分,这将不再有效。但是,还有一个方法可以替换字符串的片段,这在其他一些棘手的情况下也很有用。当给 replace
方法的第二个参数是一个函数值而不是一个字符串时,每次找到匹配项都会调用此函数,并且匹配的文本将被函数返回的任何内容替换。传递给函数的参数是匹配的元素,类似于 match
返回的数组中找到的值:第一个是整个匹配项,之后是模式中每个带括号部分的一个参数。
function eatOne(match, amount, unit) { amount = Number(amount) - 1; if (amount == 1) { unit = unit.slice(0, unit.length - 1); } else if (amount == 0) { unit = unit + "s"; amount = "no"; } return amount + " " + unit; } var stock = "1 lemon, 2 cabbages, and 101 eggs"; stock = stock.replace(/(\d+) (\w+)/g, eatOne); print(stock);
¶ 最后一个技巧可以用来使 第 6 章 中的 HTML 转义器更有效。您可能记得它看起来像这样
function escapeHTML(text) { var replacements = [["&", "&"], ["\"", """], ["<", "<"], [">", ">"]]; forEach(replacements, function(replace) { text = text.replace(replace[0], replace[1]); }); return text; }
¶ 编写一个新的函数 escapeHTML
,它做同样的事情,但只调用 replace
一次。
function escapeHTML(text) { var replacements = {"<": "<", ">": ">", "&": "&", "\"": """}; return text.replace(/[<>&"]/g, function(character) { return replacements[character]; }); } print(escapeHTML("The 'pre-formatted' tag is written \"<pre>\"."));
¶ replacements
对象是一种将每个字符与其转义版本关联起来的方法。这样使用它是安全的(即不需要 Dictionary
对象),因为唯一使用的属性是与 /[<>&"]/
表达式匹配的属性。
¶ 在某些情况下,您需要匹配的模式在编写代码时是未知的。假设我们正在为留言板编写一个(非常简单的)色情过滤器。我们只允许包含不包含淫秽词语的留言。留言板管理员可以指定他们认为不可接受的词语列表。
¶ 检查文本片段中是否包含一组词语的最有效方法是使用正则表达式。如果我们将词语列表作为数组,我们可以像这样构建正则表达式
var badWords = ["ape", "monkey", "simian", "gorilla", "evolution"]; var pattern = new RegExp(badWords.join("|"), "i"); function isAcceptable(text) { return !pattern.test(text); } show(isAcceptable("Mmmm, grapes.")); show(isAcceptable("No more of that monkeybusiness, now."));
¶ 我们可以围绕这些词语添加 \b
模式,这样关于葡萄的事情就不会被归类为不可接受。不过,这也将使第二个成为可接受的,这可能不正确。色情过滤器很难做到(而且通常过于烦人,不值得一试)。
¶ RegExp
构造函数的第一个参数是一个包含模式的字符串,第二个参数可用于添加不区分大小写或全局性。在构建用于保存模式的字符串时,您需要小心反斜杠。因为通常,反斜杠在解释字符串时会被删除,所以任何必须最终出现在正则表达式本身中的反斜杠都需要转义
var digits = new RegExp("\\d+"); show(digits.test("101"));
¶ 关于正则表达式,最重要的是它们的存在,并且可以极大地增强您字符串操作代码的功能。它们非常神秘,您可能需要在第一次使用它们的前十次查看有关它们的详细信息。坚持下去,您很快就会不经意地编写出看起来像神秘咒语的表达式
¶ (漫画来自 Randall Munroe.)