JavaScript-正则表达式教程-全-

JavaScript 正则表达式教程(全)

原文:zh.annas-archive.org/md5/AD8C3DA0D9CFBFFA54C8E09B7C43FD93

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

正则表达式是一种模式或模板,允许您以一种自然而模糊的方式定义一组规则,从而使您能够匹配和验证文本。它们在几乎每种现代编程语言中都已经实现。

当处理任何类型的文本输入时,您并不总是知道值是什么,但通常可以假设(甚至要求)您将接收到应用程序中的格式。这些类型的情况正是您需要创建正则表达式来提取和操作此输入的情况。

在本书中,您将学习如何使用 JavaScript 中的正则表达式入门基础知识。我们将从基础知识开始,经过一些特殊模式,然后深入到两个示例中。第一个示例是验证 Web 表单,第二个是从日志文件中提取信息的非常复杂的模式。对于所有示例,我们将采用逐步方法,这将使学习和吸收本书所获得的所有知识变得更容易。

这本书涵盖了什么

第一章,“使用正则表达式入门”,介绍了 JavaScript 中正则表达式的概述。它还展示了如何开发用于测试前三章中使用的正则表达式的程序。

第二章,“基础知识”,介绍了 JavaScript 中正则表达式的主要特性,包括模糊匹配、乘法器和范围。

第三章,“特殊字符”,深入探讨了正则表达式的特殊字符模式。它涵盖了为正则表达式定义边界、定义非贪婪量词和定义带有组的正则表达式。

第四章,“实践中的正则表达式”,演示了如何开发 Web 表单并使用自第一章以来学到的正则表达式功能来验证其所有字段。

第五章,“Node.js 和正则表达式”,逐步解释了如何使用 Node.JS 创建一个简单的应用程序来读取和解析 Apache 日志文件。它还演示了如何将日志文件中的信息显示到用户友好的网页中。

附录 A,“JavaScript 正则表达式速查表”,总结了 JavaScript 中正则表达式使用的模式及其描述,以及一些有用的方法列表来测试和创建正则表达式。

您需要什么来阅读本书

要开发本书中提供的源代码,您需要任何您喜欢的文本编辑器和一个浏览器(如 Chrome 或 Firefox)。

对于第五章,“Node.js 和正则表达式”,您还需要在计算机上安装 Node.js。所有必需的步骤都在章节中描述。

这本书适合谁

这本书非常适合与任何类型的用户输入数据一起工作的 JavaScript 开发人员。本书适用于具有 JavaScript 正则表达式基础到中级技能的 JavaScript 程序员,他们想要第一次学习或加强自己的技能成为专家。

惯例

在本书中,您将找到一些区分不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“现在,让我们看一下其中一些辅助函数,从errclearResultsAndErrors开始。”

代码块设置如下:

123-123-1234
(123)-123-1234
1231231234

任何命令行输入或输出都以以下方式编写:

npm install http-server –g

新术语重要词汇以粗体显示。屏幕上看到的词语,例如菜单或对话框中的词语,会以这样的方式出现在文本中:“以下图像举例说明了在给定文本输入时正则表达式的匹配。”

注意

警告或重要说明会显示在这样的框中。

提示

提示和技巧会显示为这样。

第一章:正则表达式入门

正则表达式是用来以语法方式表示模式的特殊工具。在处理任何类型的文本输入时,你并不总是知道值是什么,但你通常可以假设(甚至要求)你将接收到的格式。当你创建一个正则表达式来提取和操作这个输入时,就会出现这种情况。

因此,要匹配一个特定的模式需要一个非常机械的语法,因为即使是一个或两个字符的变化也会大大改变正则表达式的行为,结果也会相应地改变。

正则表达式本身(或Regex,简称)并不特定于任何单一的编程语言,你绝对可以在几乎所有现代语言中直接使用它们。然而,不同的语言使用不同的功能集和选项实现了正则表达式;在本书中,我们将通过JavaScript来看正则表达式及其特定的实现和功能。

一切都是关于模式

正则表达式是用专门的字符语法描述模式的字符串,本书中我们将学习这些不同字符和代码,这些字符和代码用于以模糊的方式匹配和操作不同的数据片段。现在,在我们尝试创建正则表达式之前,我们需要能够发现和描述这些模式(用英语)。让我们看一些不同和常见的例子,稍后在本书中,当我们对语法有更牢固的掌握时,我们将看到如何在代码中表示这些模式。

分析电话号码

让我们从简单的事情开始,看一下一个单独的电话号码:

123-123-1234

我们可以描述这个模式为三个数字,一个破折号,然后另外三个数字,接着是第二个破折号,最后是四个数字。这很简单;我们看着一个字符串描述它是如何组成的,如果你的所有数字都遵循给定的模式,前面的描述就会完美地起作用。现在,假设我们将以下三个电话号码添加到这个集合中:

123-123-1234
(123)-123-1234
1231231234

这些都是有效的电话号码,在你的应用程序中,你可能希望能够匹配它们所有,让用户可以以他们感觉最舒适的方式写入。所以,让我们再试一次我们的模式。现在,我会说我们有三个数字,可选地在括号内,然后是一个可选的破折号,另外三个数字,接着是另一个可选的破折号,最后是四个数字。在这个例子中,唯一强制的部分是这十个数字:破折号和括号的放置完全取决于用户。

还要注意的是,我们并没有对实际数字做任何限制,事实上,我们甚至不知道它们将是什么,但我们知道它们必须是数字(与字母相反),所以我们只放置了这个约束:

分析电话号码

分析一个简单的日志文件

有时,我们可能有比数字或字母更具体的约束;在其他情况下,我们可能希望有一个特定的单词,或者至少是来自特定组的单词。在这些情况下(大多数情况下都是如此),你越具体,越好。让我们看下面的例子:

[info] – App Started
[warning] – Job Queue Full
[info] – Client Connected
[error] – Error Parsing Input
[info] – Application Exited Successfully

这当然是某种日志的示例,我们可以简单地说每一行都是一个单独的日志消息。然而,如果我们想更具体地操作或提取数据,这并没有帮助。另一个选择是说我们在括号中有某种单词,它指的是日志级别,然后是破折号后面的消息,它将由任意数量的单词组成。同样,这并不太具体,我们的应用程序可能只知道如何处理前面三个日志级别,所以你可能想忽略其他一切或引发错误。

为了最好地描述前面的模式,我们可以说你有一个单词,它可以是 info、warning 或 error,位于一对方括号内,然后是一个破折号,然后是一些句子,构成了日志消息。这将允许我们更准确地捕获日志中的信息,并确保我们的系统准备好在发送之前处理数据:

分析简单的日志文件

分析 XML 文件

我想讨论的最后一个例子是当你的模式依赖于自身时;XML 就是一个完美的例子。在 XML 中,你可能有以下标记:

<title>Demo</title>
<size>45MB</size>
<date>24 Dec, 2013</date>

我们可以说模式由一个标签、一些文本和一个闭合标签组成。这对于它是一个有效的 XML 来说并不够具体,因为闭合标签必须与开放标签匹配。因此,如果我们重新定义模式,我们会说它包含左侧由开放标签包裹的一些文本,右侧是匹配的闭合标签:

分析 XML 文件

最后三个例子只是用来让我们进入正则表达式的思维方式;这些只是一些常见类型的模式和约束,你可以在自己的应用程序中使用。

现在我们知道可以创建什么样的模式,让我们花一点时间讨论一下我们可以用这些模式做什么;这包括 JavaScript 提供的实际功能和函数,允许我们在创建模式后使用它们。

JavaScript 中的正则表达式

在 JavaScript 中,正则表达式被实现为它们自己的类型的对象(比如RegExp对象)。这些对象存储模式和选项,然后可以用于测试和操作字符串。

要开始使用正则表达式,最简单的方法是启用 JavaScript 控制台并尝试不同的值。获取控制台的最简单方法是打开浏览器,比如Chrome,然后在任何页面上打开 JavaScript 控制台(在 Mac 上按command + option + J,在 Windows 上按Ctrl + Shift + J)。

让我们从创建一个简单的正则表达式开始;我们还没有深入讨论涉及的不同特殊字符的具体内容,所以现在,我们将创建一个只匹配一个单词的正则表达式。例如,我们将创建一个匹配hello的正则表达式。

RegExp 构造函数

在 JavaScript 中,正则表达式可以以两种不同的方式创建,类似于字符串中使用的方式。有一个更明确的定义,你可以调用构造函数并传递你选择的模式(以及可选的任何设置),然后有一个文字定义,这是同样过程的简写。这是两种方式的例子(你可以直接在 JavaScript 控制台中输入):

var rgx1 = new RegExp("hello");
var rgx2 = /hello/;

这两个变量本质上是相同的,你可以根据个人喜好选择使用哪一个。唯一的区别是,使用构造函数方法时,你使用一个字符串来创建一个表达式:因此,你必须确保提前转义任何特殊字符,以便它传递到正则表达式。

除了模式,正则表达式的两种形式的构造函数都接受第二个参数,这是一个标志字符串。标志就像设置或属性,它们应用于整个表达式,因此可以改变模式及其方法的行为。

使用模式标志

我想要介绍的第一个标志是忽略大小写i标志。标准模式是区分大小写的,但如果你有一个可以是任何大小写的模式,这是一个很好的选项,允许你只指定一个大小写,并且让修改器为你调整,使模式简短和灵活。

接下来的标志是多行m标志,这使 JavaScript 将字符串中的每一行基本上视为新字符串的开始。例如,您可以说字符串必须以字母a开头。通常,JavaScript 会测试整个字符串是否以字母 a 开头,但使用 m 标志,它将针对每行单独测试此约束,因此任何行都可以通过以 a 开头的测试。

最后一个标志是全局g标志。没有这个标志,RegExp对象只检查字符串中是否有匹配,只返回找到的第一个匹配;然而,在某些情况下,您不只是想知道字符串是否匹配,您可能想要了解所有特定的匹配。这就是全局标志的作用,当使用时,它将修改不同RegExp方法的行为,允许您获取所有匹配,而不仅仅是第一个。

因此,继续从前面的例子,如果我们想创建相同的模式,但这次,大小写不敏感并使用全局标志,我们会写类似于这样的内容:

var rgx1 = new RegExp("hello", "gi");
var rgx2 = /hello/gi;

使用 rgx.test 方法

现在我们已经创建了我们的正则表达式对象,让我们使用它最简单的函数,即test函数。test方法只根据字符串是否与模式匹配返回truefalse。这是它的一个示例:

> var rgx = /hello/;
undefined
> rgx.test("hello");
true
> rgx.test("world");
false
> rgx.test("hello world");
true

如您所见,第一个字符串匹配并返回 true,第二个字符串不包含hello,因此返回false,最后一个字符串匹配模式。在模式中,我们没有指定字符串必须只包含hello,因此它匹配最后一个字符串并返回true

使用 rgx.exec 方法

RegExp对象的下一个方法是exec函数,它不仅仅是检查模式是否与文本匹配,exec还返回有关匹配的一些信息。例如,让我们创建另一个正则表达式,并获取模式的起始index

> var rgx = /world/;
undefined
> rgx.exec("world !!");
[ 'world' ]
> rgx.exec("hello world");
[ 'world' ]
> rgx.exec("hello");
null

如您在这里所见,函数的结果包含实际匹配作为第一个元素(rgx.exec("world !!")[0];),如果您console.dir结果,您还会看到它还包含两个属性:indexinput,分别存储起始index属性和完整的input文本。如果没有匹配,函数将返回null

使用 rgx.exec 方法

字符串对象和正则表达式

除了RegExp对象本身的这两种方法之外,字符串对象上还有一些接受RegExp对象作为参数的方法。

使用 String.replace 方法

最常用的方法是replace方法。例如,假设我们有foo foo字符串,我们想将其更改为qux qux。使用带有字符串的replace只会切换第一次出现,如下所示:

使用 String.replace 方法

为了替换所有出现的情况,我们需要提供一个带有g标志的RegExp对象,如下所示:

使用 String.replace 方法

使用 String.search 方法

接下来,如果您只想在字符串中找到第一个匹配的(从零开始的)索引,您可以使用search方法:

> str = "hello world";
"hello world"
> str.search(/world/);
6

使用 String.match 方法

我现在要谈论的最后一个方法是match函数。当设置了g标志时,此函数返回与我们之前看到的exec函数相同的输出(包括indexinput属性),但返回了所有匹配的常规Array。这是一个例子:

使用 String.match 方法

我们已经快速浏览了 JavaScript 中正则表达式的最常见用法(代码方面),所以现在我们准备构建我们的RegExp测试页面,这将帮助我们探索实际的正则表达式语法,而不将其与 JavaScript 代码结合在一起。

构建我们的环境

为了测试我们的正则表达式模式,我们将构建一个HTML表单,该表单将处理提供的模式并将其与字符串匹配。

我将把所有的代码放在一个文件中,所以让我们从 HTML 文档的头部开始:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Regex Tester</title>
    <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
    <script src="img/jquery.min.js"></script>
    <style>
      body{
        margin-top: 30px;
      }
      .label {
         margin: 0px 3px;
      }
    </style>
  </head>

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

这是一个相当标准的文档头,包含了标题和一些样式。除此之外,我还包括了用于设计的 bootstrap CSS框架和 jQuery 库来帮助进行DOM操作。

接下来,让我们在页面中创建表单和结果区域:

<body>
  <div class="container">
    <div class="row">
      <div class="col-sm-12">
        <div class="alert alert-danger hide" id="alert-box"></div>
          <div class="form-group">
            <label for="input-text">Text</label>
            <input 
                    type="text" 
                    class="form-control" 
                    id="input-text" 
                    placeholder="Text"
            >
          </div>
          <label for="inputRegex">Regex</label>
          <div class="input-group">
            <input 
                   type="text" 
                   class="form-control" 
                   id="input-regex" 
                   placeholder="Regex"
            >
            <span class="input-group-btn">
              <button 
                      class="btn btn-default" 
                      id="test-button" 
                      type="button">
                             Test!
              </button>
            </span>
          </div>
        </div>
      </div>
      <div class="row">
        <h3>Results</h3>
        <div class="col-sm-12">
          <div class="well well-lg" id="results-box"></div>
        </div>
      </div>
    </div>
    <script>
      //JS code goes here
    </script>
  </body>
</html>

大部分代码是 Bootstrap 库所需的样式的样板 HTML;然而,要点是我们有两个输入:一个用于一些文本,另一个用于匹配的模式。我们有一个提交表单的按钮(Test!按钮)和一个额外的div来显示结果。

在浏览器中打开此页面应该会显示类似于这样的内容:

构建我们的环境

处理提交的表单

我们需要做的最后一件事是处理表单的提交并运行正则表达式。我将代码分成了辅助函数,以帮助我们在浏览代码时进行代码流。首先,让我们为提交(Test!)按钮编写完整的点击处理程序(应该放在脚本标签中的注释处):

var textbox = $("#input-text");
var regexbox = $("#input-regex");
var alertbox = $("#alert-box");
var resultsbox = $("#results-box");

$("#test-button").click(function(){
  //clear page from previous run
  clearResultsAndErrors()

  //get current values
  var text = textbox.val();
  var regex = regexbox.val();

  //handle empty values
  if (text == "") {
    err("Please enter some text to test.");
  } else if (regex == "") {
    err("Please enter a regular expression.");
  } else {
    regex = createRegex(regex);

    if (!regex) {
      return;
    }

    //get matches
    var results = getMatches(regex, text);

    if (results.length > 0 && results[0] !== null) {
      var html = getMatchesCountString(results);
      html += getResultsString(results, text);
      resultsbox.html(html);
    } else {
      resultsbox.text("There were no matches.");
    }
  }
});

前四行使用 jQuery 从页面中选择相应的 DOM 元素,并将它们存储以供整个应用程序使用。这是一种最佳实践,当 DOM 是静态的时候,而不是每次使用时选择元素。

其余的代码是提交(Test!)按钮的点击处理程序。在处理Test!按钮的函数中,我们首先清除上一次运行的结果和错误。接下来,我们从两个文本框中获取值,并使用一个名为err的函数处理它们为空的情况,我们稍后会看一下这个函数。如果两个值都正常,我们尝试创建一个新的RegExp对象,并使用我编写的另外两个函数createRegexgetMatches来获取它们的结果。最后,最后一个条件块检查是否有结果,并显示未找到匹配消息或页面上的一个元素,该元素将使用getMatchesCountString显示找到了多少个匹配项,并使用getResultsString显示实际的匹配项。

重置匹配和错误

现在,让我们来看一下一些辅助函数,从errclearResultsAndErrors开始:

function clearResultsAndErrors() {
  resultsbox.text("");
  alertbox.addClass("hide").text("");
}

function err(str) {
  alertbox.removeClass("hide").text(str);
}

第一个函数清除结果元素中的文本,然后隐藏先前的错误,第二个函数取消隐藏警报元素,并将传递的错误添加为参数。

创建正则表达式

我想要看的下一个函数负责从文本框中给定的值创建实际的RegExp对象。

function createRegex(regex) {
  try {
    if (regex.charAt(0) == "/") {
      regex = regex.split("/");
      regex.shift();

      var flags = regex.pop();
      regex = regex.join("/");

      regex = new RegExp(regex, flags);
    } else {
      regex = new RegExp(regex, "g");
    }
    return regex;
  } catch (e) {
    err("The Regular Expression is invalid.");
    return false;
  }
}

如果尝试使用不存在的标志或无效参数创建RegExp对象,它将抛出异常。因此,我们需要将RegExp的创建包装在try/catch块中,以便我们可以捕获错误并显示错误。

try部分内,我们将处理两种不同的RegExp输入,第一种是当您在表达式中使用斜杠时。在这种情况下,我们通过斜杠分割表达式,移除第一个元素(即空字符串,它之前的文本是第一个斜杠),然后弹出最后一个元素,它应该是标志的形式。

然后我们将剩余的部分重新组合成一个字符串,并将其与标志一起传递给RegExp构造函数。我们正在处理的另一种情况是,您输入了一个字符串,然后我们将只使用g标志将此模式传递给构造函数,以便获得多个结果。

执行 RegExp 并提取其匹配项

接下来的函数是用于实际循环遍历regex对象并从不同的匹配中获取results的函数:

function getMatches(regex, text) {
  var results = [];
  var result;

  if (regex.global) {
    while((result = regex.exec(text)) !== null) {
      results.push(result);
    }
  } else {
    results.push(regex.exec(text));
  }

  return results;
}

我们已经看到了之前的exec命令以及它如何为每个匹配返回一个results对象,但exec方法实际上根据是否设置了全局标志(g)而有所不同。如果没有设置,它将始终只返回第一个匹配,无论您调用多少次,但如果设置了,该函数将循环遍历结果,直到最后一个匹配返回null。在该函数中,如果设置了全局标志,我使用 while 循环来循环遍历results并将每个匹配推入results数组中,而如果没有设置,我只调用一次function并且只在第一个匹配时推入。

接下来,我们有一个函数,它将创建一个显示我们有多少匹配项(一个或多个)的字符串:

function getMatchesCountString(results) {
  if (results.length === 1) {
    return "<p>There was one match.</p>";
  } else {
    return "<p>There are " + results.length + " matches.</p>";
  }
}

最后,我们有一个function,它将循环遍历results数组并创建一个 HTML 字符串以在页面上显示:

function getResultsString(results, text) {
  for (var i = results.length - 1; i >= 0; i--) {
    var result = results[i];
    var match  = result.toString();
    var prefix = text.substr(0, result.index);
    var suffix = text.substr(result.index + match.length);
    text = prefix 
      + '<span class="label label-info">' 
      + match 
      + '</span>' 
      + suffix;
  }
  return "<h4>" + text + "</h4>";
}

function内部,我们循环遍历匹配列表,对于每一个匹配,我们会切割字符串并将实际匹配内容包裹在标签中以进行样式设置。我们需要按照相反的顺序循环遍历列表,因为我们正在通过添加标签来改变实际文本,同时也需要改变索引。为了与results数组中的索引保持同步,我们从末尾修改text,保持其之前的部分不变。

测试我们的应用程序

如果一切顺利,我们现在应该能够测试应用程序。例如,假设我们输入Hello World字符串作为文本,并添加l模式(如果您记得的话,这将类似于在我们的应用程序中输入/l/g),您应该会得到类似于这样的结果:

测试我们的应用程序

而如果我们指定相同的模式,但没有全局标志,我们将只得到第一个匹配:

测试我们的应用程序

当然,如果您遗漏了某个字段或指定了无效的模式,我们的错误处理将会启动并提供适当的消息:

测试我们的应用程序

现在一切都按预期工作,我们现在准备开始学习正则表达式本身,而不必担心旁边的 JavaScript 代码。

总结

在本章中,我们看了一下模式实际上是什么,以及我们能够表示的数据类型。正则表达式只是表达这些模式的字符串,结合 JavaScript 提供的函数,我们能够匹配和操作用户数据。

我们还介绍了构建一个快速的RegExp构建器,它使我们能够第一手了解如何在实际环境中使用正则表达式。在下一章中,我们将继续使用这个测试工具来开始探索RegExp语法。

第二章:基础知识

在前一章中,我们已经看到为了匹配一个子字符串,你只需要在正则表达式中写入这个字符串。例如,要匹配hello,你可以创建这个变量:

var pattern = /hello/;

我们还学到,如果我们想要匹配正则表达式的字符串或字符的所有出现,我们可以在正则表达式中使用g标志。然而,像这样具有明确模式的情况是罕见的,即使当它们出现时,是否需要正则表达式还是值得商榷的。当你有更少具体信息时,你才能真正看到正则表达式的真正力量。

正则表达式引擎实现了两个主要功能,可以正确表示你模式的 80%。我们将在本章介绍这两个主要功能:

  • 模糊匹配器

  • 乘数

在正则表达式中定义模糊匹配器

在这个主题中,我们将介绍字符类,告诉正则表达式匹配一个模糊字符。在模糊匹配中,可以是一个字符、数字或字母数字字符。

匹配通配符字符

假设我们想找到一个序列,其中我们有1,然后是任何其他字符,然后是3,这样它将包括1231b31 3133等等。对于这种情况,我们需要在我们的模式中使用一个模糊匹配器

在前面的例子中,我们希望能够使用尽可能宽泛的匹配器;如果我们希望的话,我们可以选择不对其进行任何约束,它可以包括任何字符。对于这种情况,我们有.匹配器。

在正则表达式中,句号将匹配除了换行符以外的任何字符,因此它可以包括字母、数字、符号等等。为了测试这一点,让我们在我们的 HTML 实用程序中实现上述例子。在文本字段中,让我们输入一些组合来测试模式对123 1b3 1 3 133 321的匹配,然后对于模式,我们可以指定/1.3/g。运行它应该会给你类似于这样的结果:

匹配通配符字符

匹配数字

通配符字符不是匹配模糊模式的唯一字符,也不总是正确的选择。例如,继续上一个例子,假设13之间的字符是一个数字。在这种情况下,我们可能不在乎哪个数字最终出现在那里,我们只需要确保它是一个数字。

为了实现这一点,我们可以使用\d。模糊匹配器d反斜杠或数字特殊字符将匹配 0 到 9 之间的任何字符。用反斜杠 d 字符替换句号将给我们以下结果:

匹配数字

匹配字母数字字符

前面提到的四个匹配中,只有两个符合新的约束。最后一个主要的模糊匹配器是\w,它是一个单词字符。它将匹配下划线字符、数字,或者字母表的 26 个字母(无论是小写还是大写字母)。在我们的应用程序中运行这个将给我们以下结果:

匹配字母数字字符

否定字母数字字符和数字

此外,如果你想要最后两个匹配器的否定版本,你可以使用它们的大写对应版本。我的意思是\d将匹配任何数字,但\D将匹配除数字之外的任何东西,因为它们是互补的,对于\w\W也是一样。

在正则表达式中定义范围

在正则表达式中的范围允许你创建自己的自定义约束,就像我们刚刚讨论的那样。在一个范围内,你可以精确指定可以使用的字符,或者如果更快的话,你可以指定反向,也就是不匹配的字符。

为了举例说明,假设我们只想匹配abc。在这种情况下,我们可以创建类似于[abc]的范围,它将匹配一个单个字符,要么是ab,要么是c。让我们用bicycle文本和/[abc]/g模式来测试一下:

在正则表达式中定义范围

定义范围

现在,这将起作用,但是,如果您需要匹配很多字符,您的范围将很快变得很长。幸运的是,正则表达式允许您使用(-)破折号字符指定一组字符,而无需将它们列出。例如,假设我们要检查三个字母的名称是否格式正确,并且我们希望第一个字母是大写字母,后面跟着两个小写字母。我们可以将每个范围中的所有 26 个字母都指定为[a-z][A-Z]。因此,要实现三个字母名称验证器,我们可以创建类似于/[A-Z][a-z][a-z]/g的模式:

定义范围

匹配破折号字符

如果您要匹配破折号字符本身,并且不希望 JavaScript 将其解释为指定集合,则可以使用破折号字符开始/结束范围,或者用反斜杠对其进行转义。例如,要匹配"hello world""hello-world",我们可以编写类似于/hello[- ]world//hello[\- ]world/的模式。

我们还可以在范围内使用一个通配符,即简单的点。例如,当我们想匹配数字字符并且不介意有一个句点时(暂时忘记数字只能有一个句点时),就会出现这种情况。因此,要匹配123以及2.4.45,我们可以指定/[\d.][\d.]\d/模式,然后第一个和第二个数字都可以是句点。请注意,JavaScript 不认为我们是在范围内引用通配符句点,因为这将破坏范围的目的,因此 JavaScript 将其视为标准句点。

定义否定范围

范围中最后要涵盖的是否定范围。否定范围正是其听起来的样子。与其指定要匹配的内容,我们指定不要匹配的内容。这与在 JavaScript 中为布尔值添加否定(!)字符非常相似,因为它只是翻转了您之前得到的返回值。

要创建否定范围,可以使用(^)插入符号字符开始范围以匹配任何字符;但是,对于字母表的前五个字母,您可以使用类似于/[^a-e]/的内容。

这本身可能看起来并不那么有用,但是您可能,例如,希望为文件名删除所有非字母字符。在这种情况下,您可以键入/[^a-z]/gi,并与 JavaScript 的replace函数结合使用,可以删除所有这些字符。

在正则表达式中定义乘数

匹配器很棒,但它们只能在一个方向上“扩展”您的模式。我喜欢将匹配器看作是垂直扩展模式的东西,允许您匹配更多适合相同模式的字符串,但它们仍然受长度限制,或者扩展模式的水平。乘数允许您匹配可能作为输入接收的任意大小的字符串,从而为您提供更大范围的自由度。

在正则表达式中有三种基本的乘数:

  • +:这匹配一个或多个出现

  • ?:这匹配零次或一次出现

  • *:这匹配零个或多个出现

我们将在本节中介绍这三种乘数,并向您展示如何创建自定义乘数。

匹配一个或多个出现

最基本的乘数必须是(+)加号运算符。它告诉 JavaScript 正则表达式中使用的模式必须出现一次或多次。例如,我们可以在之前使用的格式化名称模式上构建,并且不仅匹配三个字母的名称,还可以使用/[A-Z][a-z]+/g匹配任何长度的名称:

匹配一个或多个出现

此模式表示任何以大写字母开头并至少有一个小写字母的内容。加号将继续重复该模式,直到不再匹配(在我们的情况下,当它达到空格字符时)。

匹配零次或一次出现

下一个乘数,我猜可以称为更多的量词,是(?)问号。恰当地,这个乘数允许前面的字符要么出现要么不出现,几乎就像我们在说它的存在是可疑的。我认为最好的解释方法是通过一个例子来说明。假设我们想要接收Apple的单数形式或复数形式,为此,我们可以使用这个模式:

/apples?/gi

匹配零次或一次出现

现在这可能看起来像问号更像是一个条件运算符而不是一个乘数,但它真正做的是说前面的字符可以出现一次或零次。

匹配零次或多次出现

我们工具链中的下一个乘数是(*)星号。这个星号是前两个乘数的组合,允许前面的字符出现零次到无限次。因此,如果您的输入包含一个单词或字符多次,模式将匹配。如果您的输入不包含单词或字符,模式仍然匹配。例如,如果您正在解析某种update的日志,这可能会很有用。在这种情况下,您可能会得到update或者update!!!,根据一天的时间,甚至可能得到update!!!!!!!!!!!!!!!!。为了匹配所有这些字符串,您可以简单地创建模式/update!*/g

匹配零次或多次出现

这些是三种标准的乘数,类似于内置字符集(\d)范围的乘数。同样,正则表达式允许您指定和创建自己的乘数。

定义自定义量词

只有一种语法来指定自己的乘数,但由于可用的不同参数选项,您可以获得三种不同的功能选项。

如果你想匹配给定字符的具体次数,你可以在花括号内指定允许的重复次数。这不会使您的模式更灵活,但会使它们更易于阅读。例如,如果我们要实现一个电话号码,我们可以输入/\d\d\d-\d\d\d\d/。然而,这有点长,相反,我们可以使用自定义乘数,输入/\d{3}-\d{4}/,这样可以使它更简洁,更易读。

匹配 n 次或更多出现

接下来,如果你只想设置模式可以出现的最小次数,但实际长度并不重要,你可以在数字后面加上逗号。例如,假设我们想创建一个模式,以确保用户的密码至少有六个字符长;在这种情况下,您可能不想强制最大字符限制,因此可以输入类似/.{6,}/的内容:

匹配 n 次或更多出现

匹配 n 到 m 次出现

我们自定义乘数的第三种变化是当您想要设置一组完整的选项,匹配最小和最大出现次数时。您可以在逗号后面简单地添加另一个数字。例如,如果我们有某种评论系统,并且我们希望限制评论在 15 到 140 个字符之间,我们可以创建一个正则表达式字符串来匹配这个设置,例如/.{15,140}/

现在,我并不是说前面提到的两个例子是这种正则表达式的最佳用法,因为显然,检查文本长度有更简单的方法。然而,在更大模式的上下文中,这可能非常有用。

匹配交替选项

在这个阶段,我们知道如何使用模糊匹配器匹配任何一组字符,并且我们有能力使用乘法器重复模式以匹配任何类型的序列,这为你匹配几乎任何东西提供了一个相当不错的基础。然而,即使有了这一切,还是有一种情况经常出现并且可能会成为问题。当处理两种不同且完全独立的可接受输入形式时就会出现这种情况。

假设我们正在解析某种表单数据,对于每个问题,我们想要提取一个存储在某处的 yes 或 no。凭借我们目前的专业知识,我们可以创建一个类似于/[yn][eo]s?/g的模式,它将匹配yesno。真正的问题在于它也会匹配这些字母的其他六种组合,而我们的应用程序可能不知道如何处理:

匹配交替选项

幸运的是,正则表达式有一个完全不同的系统来处理这种情况,它就是(|)管道字符。它类似于你在if语句中使用的OR运算符,只不过这里只使用一个。它的工作原理是,你用管道分隔你想要匹配的不同模式,然后任何一个模式都可以返回匹配。将我们之前的正则表达式模式改为/yes|no/g将会显示正确的结果:

匹配交替选项

好吧,至少几乎可以,尽管它仍然会匹配nos中的no。然而,这是因为我们一直在使用开放模式,而没有真正强制完整的单词(这是下一章的主题)。

管道字符不仅限于两个选项,我们可以通过用管道字符分隔它们来轻松匹配大量的值。此外,我们不仅限于使用纯文本,我们的正则表达式分割中的每个部分都可以使用范围和乘法器来定义自己的模式。

为电话号码创建一个正则表达式

为了总结这一章,让我们将刚刚学到的一些特性结合起来,构建我们在上一章中使用的电话号码模式。总之,我们希望能够匹配以下所有的数字模式:

123-123-1234
(123)-123-1234
1231231234

首先,我们可以看到前三个数字(区号)周围有可选的括号,数字之间也有可选的破折号。这是一个问号字符派上用场的情况。对于数字本身,我们可以使用内置的匹配器来指定它们必须是数字,并使用强大的乘法器来指定我们需要多少个。我们需要知道的唯一特殊之处是括号包含特殊字符,所以我们需要对它们进行转义(添加反斜杠):

/\(?\d{3}\)?-?\d{3}-?\d{4}/g

括号在正则表达式中用于定义组,这就是它们为什么是特殊字符的原因。我们将在第三章中学习如何定义组,特殊字符

使用我们在第一章中开发的测试应用程序以及本主题开头提到的示例来测试这个正则表达式,将会显示出这个正则表达式匹配所有的示例:

为电话号码创建一个正则表达式

总结

在本章中,我们学习了如何使用字符类来定义通配符匹配、数字匹配和字母数字匹配。我们还学习了如何定义量词,它们指定了字符或组在输入中可以出现的次数。

在下一章中,我们将学习边界(可用于匹配正则表达式的位置)和定义组。

第三章:特殊字符

在本章中,我们将看一些特殊字符和一些更高级的技术,这些将帮助我们创建更详细的正则表达式模式。我们还将慢慢过渡到使用标准 JavaScript 来构建更完整的真实世界示例,而不是使用我们的正则表达式测试环境。

在我们超前之前,我们还有一些事情可以使用我们当前的设置来学习,首先是一些约束。

在本章中,我们将涵盖以下主题:

  • 为正则表达式定义边界

  • 定义非贪婪量词

  • 使用分组定义正则表达式

非可视约束

到目前为止,我们对模式施加的所有约束都与可以显示或不显示的字符有关,但是正则表达式提供了许多位置约束,允许您过滤掉一些误报。

匹配输入的开头和结尾

第一个这样的集合是开始结束的字符串匹配器。使用(^) 插入符号来匹配字符串的开头和($)美元符号来匹配结尾,我们可以强制模式定位在这些位置,例如,您可以在单词的末尾添加美元符号,以确保它是提供的字符串中的最后一件事。在下一个例子中,我使用了/^word|word$/g模式来匹配word的出现,它可以是字符串的开头或结尾。下面的图片展示了给定文本输入时正则表达式的匹配:

匹配输入的开头和结尾

同时使用开始和结束字符可以确保您的模式是字符串中唯一的内容。例如,如果您有一个/world/模式,它将匹配world字符串以及任何其他仅包含world的字符串,例如hello world。但是,如果您想确保字符串只包含world,您可以修改模式为/^world$/。这意味着正则表达式将尝试找到既开始字符串又结束字符串的模式。当然,这只会发生在它是字符串中的唯一内容时。

这是默认行为,但值得一提的是,情况并非总是如此。在前一章中,我们看到了m或多行标志,这个标志的作用是使插入符号不仅匹配字符串的开头,还匹配任何行的开头。美元符号也是如此:它将匹配每行的结尾,而不是整个字符串的结尾。因此,这实际上取决于您在特定情况下的需求。

匹配单词边界

单词边界与我们刚刚看到的字符串边界非常相似,只是它们在单词的上下文中起作用。例如,我们想匹配can,但这指的是单独的can,而不是candy中的can。我们在前面的例子中看到,如果您只输入一个模式,比如/can/g,即使它是另一个单词的一部分,也会匹配can,例如,在用户输入candy的情况下。使用反斜杠(\b)字符,我们可以表示一个单词边界(在开头或结尾),这样我们就可以使用类似/\bcan\b/g的模式来解决这个问题,如下所示:

匹配单词边界

匹配非单词边界

\b字符配对的是\B符号,它是其相反的。与我们在多个场合看到的类似,大写符号通常指相反的功能,并且也不例外。大写版本将对限制模式的约束放在单词边缘。现在,我们将运行相同的示例文本,只是用/can\B/g,这将交换匹配;这是因为can中的n处于其边界:

匹配非单词边界

匹配空白字符

您可以使用反斜杠s字符匹配空白字符,并且它匹配诸如空格和制表符之类的内容。它类似于单词边界,但有一些区别。首先,单词边界匹配单词的结尾,即使它是模式中的最后一个单词,而空白字符则需要额外的空格。因此,/foo\b/将匹配foo。但是,/foo\s/不会,因为字符串末尾没有后续空格字符。另一个区别是边界匹配器将计算类似句号或破折号之类的内容作为实际边界,而空白字符只有在有空格时才匹配字符串:

匹配空白字符

值得一提的是,空白字符具有一个\S反向匹配器,它将匹配除空白字符之外的任何内容。

定义非贪婪量词

在上一节中,我们看了看乘法器,您可以指定一个模式应该重复一定次数。默认情况下,JavaScript 会尝试匹配尽可能多的字符,这意味着它将是一个贪婪匹配。假设我们有一个类似/\d{1,4}/的模式,它将匹配任何文本,并且有 1 到 4 个数字。默认情况下,如果我们使用124582948,它将返回1245,因为它将采用最大数量的选项(贪婪方法)。但是,如果我们想要,我们可以添加(?)问号运算符,告诉 JavaScript 不要使用贪婪匹配,而是尽可能返回最少数量的字符:

定义非贪婪量词

贪婪匹配是使在代码中查找错误变得困难的东西。考虑以下示例文本:

<div class="container" id="main">
   Site content  
<div>

如果我们想要提取类,您可能会考虑以这种方式编写模式:

/class=".*"/

这里的问题是*字符将尝试匹配尽可能多的字符,所以我们不会得到像我们想要的container,而是会得到"container" id="main"。由于点字符将匹配任何内容,正则表达式将从class单词之前的第一个引号匹配到id单词之前的右引号。为了解决这个问题,我们可以使用非贪婪问号并将模式更改为/class=".*?"/。这将导致它在达到第一个引号时停止,这是最小要求的匹配:

定义非贪婪量词

在正则表达式中匹配组

我到目前为止留下的最后一个主题是。但是,为了使用组,我们必须回到 JavaScript 控制台,因为这将提供我们需要查看的实际结果对象。

组显示了我们如何从提供的输入中提取数据。没有组,您可以检查是否有匹配,或者给定的输入文本是否遵循特定模式。但是,您无法利用模糊的定义来提取相关内容。语法非常简单:您将要提取的模式包装在括号内,然后表达式的这部分将被提取到自己的属性中。

将字符分组以创建从句

让我们从一些基本的东西开始——标准 JavaScript 中的人名。如果您有一个包含某人姓名的字符串,您可能会按空格字符拆分它,并检查其中是否有两个或三个组件。如果有两个,第一个将包括名字,第二个将包括姓氏;但是,如果有三个组件,那么第二个组件将包括中间名,第三个将包括姓氏。

与强加条件不同,我们可以创建一个简单的模式,如下所示:

/(\S+) (\S*) ?\b(\S+)/

第一组包含一个必需的非空单词。加号将再次无限制地增加模式。接下来,我们想要一个带有第二个单词的空格;这次,我使用了星号来表示它的长度可以为零,并且在此之后,我们有另一个空格,尽管这次是可选的。

注意

如果没有中间名,就不会有第二个空格,后面跟着一个单词边界。这是因为空格是可选的,但我们仍然希望确保存在一个新单词,然后是最后一个单词。

现在,打开 JavaScript 控制台(在 Chrome 中)并为此模式创建一个变量:

var pattern = /(\S+) (\S*) ?\b(\S+)/

然后,尝试对此模式使用不同的名称运行exec命令,有中间名和没有中间名,并查看结果输出:

将字符分组在一起以创建从句

无论字符串是否有中间名,它都将具有我们可以分配给变量的三个模式,因此,我们可以使用其他东西来代替这个:

var res = name.split(" ");
first_name = res[0];

if (res.length == 2) {
   middle_name = "";
   last_name = res[1];
} else {
   middle_name = res[1];
   last_name = res[2];
}

我们可以从前面的代码中删除条件语句(if-else),并编写类似于此的代码:

var res = /(\S+) (\S*) ?\b(\S+)/.exec(name);

first_name = res[1];
middle_name = res[2];
last_name = res[3];

如果省略中间名,我们的表达式仍将具有该组,只是一个空字符串。

另一个值得一提的事情是,组的索引从1开始,因此第一组在结果1索引中,结果0索引保存整个匹配。

捕获和非捕获组

在第一章中,我们看到了一个示例,我们想要解析某种XML标记,并且我们说我们需要一个额外的约束条件,即关闭标记必须与开放标记匹配才能有效。因此,例如,这应该被解析:

<duration>5 Minutes</duration>

在这里,这不应该被解析:

<duration>5 Minutes</title>

由于闭合标签与开放标签不匹配,引用模式中先前组的方法是使用反斜杠字符,后跟组的索引号。例如,让我们编写一个小脚本,该脚本将接受一系列以XML标签分隔的行,然后将其转换为 JavaScript 对象。

首先,让我们创建一个输入字符串:

var xml = [
   "<title>File.js</title>",
   "<size>36 KB</size>",
   "<language>JavaScript</language>",
   "<modified>5 Minutes</name>"
].join("\n");

在这里,我们有四个属性,但是最后一个属性没有有效的闭合标签,所以不应该被捕获。接下来,我们将循环遍历这个模式,并设置一个data对象的属性:

var data = {};

xml.split("\n").forEach(function(line){
   match = /<(\w+)>([^<]*)<\/\1>/.exec(line);
   if (match) {
      var tag = match[1];
      data[tag] = match[2];
   }
});

如果我们在控制台中输出数据,您将看到我们实际上获得了三个有效属性:

捕获和非捕获组

但是,让我们花一点时间来检查这个模式;我们寻找一些带有名称的开放标签,然后捡起除了使用否定范围的开放三角括号之外的所有字符。之后,我们使用(\1)反向引用来查找闭合标签以确保其匹配。您可能还意识到我们需要转义斜杠,以便它不会认为我们正在关闭 Regexp 模式。

注意

当将反向引用添加到正则表达式模式的末尾时,允许您在模式内部引用子模式,以便记住子模式的值并将其用作匹配的一部分。例如,/(no)\1/nono中匹配nono\1将被替换为模式内的第一个子模式的值,或者(no),从而形成最终模式。

到目前为止,我们看到的所有组都是捕获组,它们告诉 Regexp 将模式的这一部分提取到自己的变量中。但是,还有其他组或括号的用途可以实现更多的功能,其中之一是非捕获组。

匹配非捕获组

非捕获组将模式的一部分分组,但实际上不会将这些数据提取到结果数组中,也不会在反向引用中使用它。其中一个好处是它允许您在模式中的完整部分上使用字符修饰符。例如,如果我们想要获得一个无限重复world的模式,我们可以将其写成这样:

/(?:world)*/

这将匹配world以及worldworldworld等等。非捕获组的语法类似于标准组,只是你要用问号和(?:)冒号开始。对它进行分组使我们能够将整个内容视为单个对象,并使用修饰符,这些修饰符通常只适用于单个字符。

非捕获组的另一个最常见用途(也可以在捕获组中完成)是与管道字符一起使用。管道字符允许您在模式中依次插入多个选项,例如,在我们想要匹配yesno的情况下,我们可以创建这个模式:

/yes|no/

然而,大多数情况下,这组选项只会是模式的一小部分。例如,如果我们正在解析日志消息,我们可能想要提取日志级别和消息。日志级别只能是几个选项之一(例如debuginfoerror等),但消息总是存在的。现在,您可以编写一个模式而不是这个:

/[info] - .*|[debug] - .*|[error] - .*/

我们可以将共同部分提取到自己的非捕获组中:

/[(?:info|debug|error)] - .*/

通过这样做,我们消除了大量重复的代码。

匹配前瞻组

您的代码中可以有的最后一组组是前瞻组。这些组允许我们对模式设置约束,但实际上并不包括这个约束在实际匹配中。使用非捕获组时,JavaScript 不会为某个部分创建特殊索引,尽管它会将其包含在完整结果中(结果的第一个元素)。使用前瞻组,我们希望能够确保在我们的匹配后面有或没有一些文本,但我们不希望这些文本出现在结果中。

例如,假设我们有一些输入文本,我们想要解析出所有.com 域名。我们可能并不一定希望在匹配中包含.com,只是实际的域名。在这种情况下,我们可以创建这个模式:

/\w+(?=\.com)/g

具有?=字符的组意味着我们希望它在我们的模式末尾有这个文本,但实际上我们不想包括它;我们还必须转义句号,因为它是一个特殊字符。现在,我们可以使用这个模式来提取域:

text.match(/\w+(?=\.com)/g)

我们可以假设我们有一个类似的变量文本:

匹配前瞻组

使用负向前瞻

最后,如果我们想要使用负向前瞻,就像一个前瞻组确保包含的文本不遵循某种模式,我们可以简单地使用感叹号代替等号:

var text = "Mr. Smith & Mrs. Doe";

text.match(/\w+(?!\.)\b/g);

这将匹配所有不以句号结尾的单词,也就是说,它将从这段文本中提取出名称:

使用负向前瞻

摘要

在本章中,我们学习了如何处理贪婪和非贪婪匹配。我们还学习了如何使用组来创建更复杂的正则表达式。在学习如何对 Regex 进行分组时,我们还学习了捕获组、非捕获组和前瞻组。

在下一章中,我们将实现我们在本书中学到的一切,并创建一个真实世界的示例来匹配和验证用户输入的信息。

第四章:实践中的正则表达式

在前两章中,我们深入讨论了正则表达式的语法,并且在这一点上,我们已经具备了构建真实项目所需的所有要素,这将是本章的目标。

了解正则表达式的语法允许您对文本模式进行建模,但有时提出一个好的可靠模式可能更困难,因此查看一些实际用例可以帮助您学习一些常见的设计模式。

因此,在本章中,我们将开发一个表单,并探讨以下主题:

  • 验证姓名

  • 验证电子邮件

  • 验证 Twitter 用户名

  • 验证密码

  • 验证 URL

  • 操作文本

正则表达式和表单验证

到目前为止,在前端上,正则表达式最常见的用途之一是与用户提交的表单一起使用,因此这就是我们将要构建的内容。我们将构建的表单将包含所有常见字段,如姓名、电子邮件、网站等,但除了所有验证之外,我们还将尝试一些文本处理。

在实际应用中,通常不会手动实现解析和验证代码。您可以创建一个正则表达式,并依赖于一些 JavaScript 库,例如:

注意

即使是最流行的框架也支持使用正则表达式进行本地验证引擎,例如AngularJS(参见www.ng-newsletter.com/posts/validations.html)。

设置表单

这个演示将是一个允许用户创建在线简介的网站,因此包含不同类型的字段。然而,在我们进入这个之前(因为我们不会构建一个处理表单的后端),我们将设置一些 HTML 和 JavaScript 代码来捕获表单提交并提取/验证其中输入的数据。

为了保持代码整洁,我们将创建一个包含所有验证函数的数组,以及一个数据对象,其中将保存所有最终数据。

以下是我们开始添加字段的 HTML 代码的基本大纲:

<!DOCTYPE HTML>
<html>
    <head>
        <title>Personal Bio Demo</title>
    </head>
    <body>
        <form id="main_form">
            <input type="submit" value="Process" />
        </form>

        <script>
            // js goes here
        </script>
    </body>
</html>

接下来,我们需要编写一些 JavaScript 来捕获表单并运行我们将要编写的函数列表。如果一个函数返回 false,这意味着验证未通过,我们将停止处理表单。如果我们通过整个函数列表并且没有出现问题,我们将在控制台和包含我们提取的所有字段的数据对象中注销:

<script>
    var fns = [];
    var data = {};

    var form = document.getElementById("main_form");

    form.onsubmit = function(e) {
        e.preventDefault();

        data = {};

        for (var i = 0; i < fns.length; i++) {
            if (fns[i]() == false) {
                return;
            }
        }

        console.log("Verified Data: ", data);
    }
</script>

JavaScript 首先创建了我之前提到的两个变量,然后从 DOM 中提取表单对象并设置提交处理程序。submit处理程序首先通过阻止页面实际提交(因为在这个例子中我们没有任何后端代码),然后我们逐个运行函数列表中的函数。

验证字段

在本节中,我们将探讨如何手动验证不同类型的字段,如姓名、电子邮件、网站 URL 等。

匹配完整姓名

让我们先从一个简单的姓名字段开始。这是我们之前简要介绍过的内容,所以它应该给您一个关于我们的系统将如何工作的想法。以下代码放在脚本标签内,但是在我们迄今为止写的所有内容之后:

function process_name() {
    var field = document.getElementById("name_field");
    var name = field.value;

    var name_pattern = /^(\S+) (\S*) ?\b(\S+)$/;

    if (name_pattern.test(name) === false) {
        alert("Name field is invalid");
        return false;
    }

    var res = name_pattern.exec(name);
    data.first_name = res[1];
    data.last_name = res[3];

    if (res[2].length > 0) {
        data.middle_name = res[2];
    }

    return true;
}

fns.push(process_name);

我们以类似于获取表单的方式获取姓名字段,然后提取值并根据模式匹配完整姓名。如果姓名不匹配模式,我们只是警告用户并返回false,以让表单处理程序知道验证失败。如果姓名字段格式正确,我们在数据对象上设置相应的字段(请记住,中间名在这里是可选的)。最后一行只是将此函数添加到函数数组中,因此在提交表单时将调用它。

使其正常工作所需的最后一件事是为此表单字段添加 HTML,因此在表单标签内(提交按钮右边),您可以添加此文本输入:

Name: <input type="text" id="name_field" /><br />

在浏览器中打开此页面,您应该能够通过在名称框中输入不同的值来测试它。如果您输入一个有效的名称,您应该能够打印出具有正确参数的数据对象,否则您应该能够看到此警报消息:

匹配完整名称

理解完整名称正则表达式

让我们回到用于匹配用户输入的名称的正则表达式:

/^(\S+) (\S*) ?\b(\S+)$/

以下是对正则表达式的简要解释:

  • ^ 字符断言其位置在字符串的开头

  • 第一个捕获组(\S+)

  • \S+ 匹配一个非空格字符 [^\r\n\t\f]

  • + 量词一次或多次

  • 第二个捕获组(\S*)

  • \S* 匹配任何非空白字符 [^\r\n\t\f]

  • * 量词零次或多次

  • " ?" 匹配空格字符

  • ? 量词零次或一次

  • \b 断言其位置在(^\w|\w$|\W\w|\w\W)单词边界

  • 第三个捕获组(\S+)

  • \S+ 匹配一个非空白字符 [^\r\n\t\f]

  • + 量词一次或多次

  • $ 断言其位置在字符串的末尾

使用正则表达式匹配电子邮件

我们可能想要添加的下一个字段类型是电子邮件字段。电子邮件乍看起来可能很简单,但实际上有各种各样的电子邮件。你可能只想创建一个word@word.word的模式,但第一部分除了字母之外还可以包含许多其他字符,域可以是子域,或者后缀可以有多个部分(比如.co.uk代表英国)。

我们的模式将简单地寻找一组不是空格或在第一部分中使用了@符号的字符。然后我们希望有一个@符号,后面跟着另一组至少有一个句点的字符,然后是后缀,后缀本身可能包含另一个后缀。因此,可以以以下方式完成:

/[^\s@]+@[^\s@.]+\.[^\s@]+/

注意

我们的示例模式非常简单,不会匹配每个有效的电子邮件地址。有一个官方标准用于电子邮件地址的正则表达式,称为RFC 5322。有关更多信息,请阅读www.regular-expressions.info/email.html

因此,让我们将该字段添加到我们的页面上:

Email: <input type="text" id="email_field" /><br />

然后我们可以添加这个函数来验证它:

function process_email() {
    var field = document.getElementById("email_field");
    var email = field.value;

    var email_pattern = /^[^\s@]+@[^\s@.]+\.[^\s@]+$/;

    if (email_pattern.test(email) === false) {
        alert("Email is invalid");
        return false;
    }

    data.email = email;
    return true;
}

fns.push(process_email);

注意

有一个专门设计用于电子邮件的 HTML5 字段类型,但这里我们正在手动验证,因为这是一本正则表达式书。有关更多信息,请参阅www.w3.org/TR/html-markup/input.email.html

理解电子邮件正则表达式

让我们回到用于匹配用户输入的名称的正则表达式:

/^[^\s@]+@[^\s@.]+\.[^\s@]+$/

以下是对正则表达式的简要解释:

  • ^ 断言其位置在字符串的开头

  • [^\s@]+ 匹配不在以下列表中的单个字符:

  • + 量词一次或多次

  • \s 匹配任何空白字符 [\r\n\t\f]

  • @ 匹配 @ 字面字符

  • [^\s@.]+ 匹配不在以下列表中的单个字符:

  • + 量词一次或多次

  • \s 匹配一个 [\r\n\t\f] 空白字符

  • @.@. 列表中的单个字符

  • \. 字符匹配.字符

  • [^\s@]+ 匹配不在以下列表中的单个字符:

  • + 量词一次或多次

  • \s 匹配 [\r\n\t\f] 空白字符

  • @@ 字面字符

  • $ 断言其位置在字符串的末尾

匹配 Twitter 名称

我们要添加的下一个字段是 Twitter 用户名的字段。对于不熟悉的人来说,Twitter 用户名是以 @username 格式的,但当人们输入时,他们有时会包括前置的 @ 符号,而在其他情况下,他们只会写用户名本身。显然,内部我们希望一切都以统一的方式存储,所以我们需要提取用户名,无论 @ 符号是否存在,然后手动添加一个,所以无论它是否存在,最终结果看起来都是一样的。

因此,让我们再次为此添加一个字段:

Twitter: <input type="text" id="twitter_field" /><br />

现在,让我们编写处理它的函数:

function process_twitter() {
    var field = document.getElementById("twitter_field");
    var username = field.value;

    var twitter_pattern = /^@?(\w+)$/;

    if (twitter_pattern.test(username) === false) {
        alert("Twitter username is invalid");
        return false;
    }

    var res = twitter_pattern.exec(username);
    data.twitter = "@" + res[1];
    return true;
}

fns.push(process_twitter);

如果用户输入 @ 符号,它将被忽略,因为在检查用户名后,我们将手动添加它。

理解 Twitter 用户名的正则表达式

让我们回到用于匹配用户输入的名称的正则表达式:

/^@?(\w+)$/

这是正则表达式的简要解释:

  • ^ 断言它的位置在字符串的开头

  • @? 匹配 @ 字符,字面上

  • ? 量词表示出现零次或一次

  • 第一个捕获组 (\w+)

  • \w+ 匹配一个 [a-zA-Z0-9_] 的单词字符

  • + 量词表示出现一次或多次

  • $ 断言它的位置在字符串的末尾

匹配密码

另一个常见的字段,可能有一些独特的约束条件,是密码字段。现在,并不是每个密码字段都有趣;你可能只允许几乎任何东西作为密码,只要字段不为空就可以。然而,有些网站需要至少包含一个大写字母、一个小写字母、一个数字和至少一个其他字符。考虑到这些可以组合的方式,创建一个可以验证这一点的模式可能会非常复杂。对于这个问题,一个更好的解决方案,也可以让我们在错误消息上更加详细,是创建四个单独的模式,并确保密码与每个模式匹配。

对于输入,它几乎是相同的:

Password: <input type="password" id="password_field" /><br />

process_password 函数与前面的例子并没有太大的不同,我们可以看到它的代码如下:

function process_password() {
    var field = document.getElementById("password_field");
    var password = field.value;

    var contains_lowercase = /[a-z]/;
    var contains_uppercase = /[A-Z]/;
    var contains_number = /[0-9]/;
    var contains_other = /[^a-zA-Z0-9]/;

    if (contains_lowercase.test(password) === false) {
        alert("Password must include a lowercase letter");
        return false;
    }

    if (contains_uppercase.test(password) === false) {
        alert("Password must include an uppercase letter");
        return false;
    }

    if (contains_number.test(password) === false) {
        alert("Password must include a number");
        return false;
    }

    if (contains_other.test(password) === false) {
        alert("Password must include a non-alphanumeric character");
        return false;
    }

    data.password = password;
    return true;
}

fns.push(process_password);

总的来说,你可能会说这是一个非常基本的验证,而且我们已经涵盖过了,但我认为这是一个很好的例子,展示了工作聪明而不是努力。当然,我们可能可以创建一个长模式,一次性检查所有内容,但那样会更不清晰,也更不灵活。因此,通过将其分解为更小、更易管理的验证,我们能够创建清晰的模式,并同时通过更有帮助的警报消息来提高其可用性。

匹配 URL

接下来,让我们为用户的网站创建一个字段;这个字段的 HTML 如下:

Website: <input type="text" id="website_field" /><br />

一个 URL 可以有许多不同的协议,但在这个例子中,让我们将其限制为只有 http 或 https 链接。接下来,我们有一个带有可选子域的域名,我们需要以后缀结束。后缀本身可以是一个单词,比如 .com,也可以有多个段,比如 .co.uk。

总的来说,我们的模式看起来类似于这样:

/^(?:https?:\/\/)?\w+(?:\.\w+)?(?:\.[A-Z]{2,3})+$/i

在这里,我们使用了多个非捕获组,用于可选的部分和重复一个段的情况。你可能还注意到,我们在正则表达式的末尾使用了不区分大小写的标志 (/i),因为链接可以以小写或大写字母写入。

现在,我们将实现实际的函数:

function process_website() {
    var field = document.getElementById("website_field");
    var website = field.value;

    var pattern = /^(?:https?:\/\/)?\w+(?:\.\w+)?(?:\.[A-Z]{2,3})+$/i

    if (pattern.test(website) === false) {
        alert("Website is invalid");
        return false;
    }

    data.website = website;
    return true;
}

fns.push(process_website);

到这一点,你应该对向我们的表单添加字段和添加函数来验证它们的过程非常熟悉了。因此,对于我们剩下的例子,让我们把重点从验证输入转移到操作数据上。

理解 URL 的正则表达式

让我们回到用于匹配用户输入的名称的正则表达式:

/^(?:https?:\/\/)?\w+(?:\.\w+)?(?:\.[A-Z]{2,3})+$/i

这是正则表达式的简要解释:

  • ^ 断言它的位置在字符串的开头

  • (?:https?:\/\/)? 是一个非捕获组

  • ? 量词表示出现零次或一次

  • http 字符串匹配 http 字符(不区分大小写)

  • s? 匹配 s 字符(不区分大小写)

  • ? 量词匹配零次或一次

  • : 字符与 : 匹配

  • \/ 字符与 / 匹配

  • \/ 字符与 / 匹配

  • \w+ 匹配一个 [a-zA-Z0-9_] 单词字符

  • + 量词匹配一次或多次

  • (?:\.\w+)? 是一个非捕获组

  • ? 量词匹配零次或一次

  • \. 字符与 . 匹配

  • \w+ 匹配一个 [a-zA-Z0-9_] 单词字符

  • + 量词匹配一次或多次

  • (?:\.[A-Z]{2,3})+ 是一个非捕获组

  • + 量词匹配一次或多次

  • \. 字符与 . 匹配

  • [A-Z]{2,3} 匹配列表中的单个字符

  • {2,3} 量词匹配23

  • A-Z 是在 A 和 Z 之间的单个字符(不区分大小写)

  • $ 断言它的位置在字符串的末尾

  • i 修饰符:不区分大小写。不区分大小写的字母,意味着它将匹配 a-z 和 A-Z。

操作数据

我们将在我们的表单中再添加一个输入,用于用户的描述。在描述中,我们将解析一些东西,比如电子邮件,然后创建用户描述的纯文本和 HTML 版本。

这个表单的 HTML 非常简单;我们将使用一个标准的文本框,并给它一个适当的字段:

Description: <br />
<textarea id="description_field"></textarea><br />

接下来,让我们从处理表单数据所需的基本结构开始:

function process_description() {
    var field = document.getElementById("description_field");
    var description = field.value;

    data.text_description = description;

    // More Processing Here

    data.html_description = "<p>" + description + "</p>";

    return true;
}

fns.push(process_description);

这段代码从页面上的文本框中获取文本,然后保存它的纯文本版本和 HTML 版本。在这个阶段,HTML 版本只是简单地将纯文本版本包裹在一对段落标签之间,但这是我们现在要处理的内容。我想要做的第一件事是在段落之间分割,在文本区域中,用户可能有不同的分割方式——行和段落。对于我们的例子,假设用户只输入了一个换行符,那么我们将添加一个 <br /> 标签,如果有多于一个字符,我们将使用 <p> 标签创建一个新的段落。

使用 String.replace 方法

我们将在字符串对象上使用 JavaScript 的 replace 方法。这个函数可以接受一个正则表达式模式作为它的第一个参数,以及一个函数作为它的第二个参数;每当它找到模式时,它将调用该函数,函数返回的任何东西都将被插入到匹配的文本的位置。

所以,对于我们的例子,我们将寻找换行符,并在函数中决定是否要用换行标签替换换行,或者根据它能够捕获到多少个换行符来创建一个实际的新段落:

var line_pattern = /\n+/g;
description = description.replace(line_pattern, function(match) {
    if (match == "\n") {
        return "<br />";
    } else {
        return "</p><p>";
    }
});

你可能注意到的第一件事是,我们需要在模式中使用 g 标志,这样它将寻找所有可能的匹配,而不仅仅是第一个。除此之外,其余的都很简单。考虑这个表单:

使用 String.replace 方法

如果你看一下前面代码的控制台输出,你应该会得到类似这样的东西:

使用 String.replace 方法

匹配一个描述字段

接下来我们需要做的是尝试从文本中提取电子邮件,并自动将它们包装在链接标签中。我们已经介绍了一个正则表达式模式来捕获电子邮件,但我们需要稍微修改它,因为我们先前的模式期望电子邮件是文本中唯一存在的东西。在这种情况下,我们对包含在大段文本中的所有电子邮件感兴趣。

如果您只是在寻找一个单词,您可以使用 \b 匹配器,它匹配任何边界(可以是单词的结尾/句子的结尾),所以我们不再使用之前用来表示字符串结尾的美元符号,而是将边界字符放在那里,以表示单词的结尾。然而,在我们的情况下,这还不够好,因为有一些边界字符是有效的电子邮件字符,例如句号字符是有效的。为了解决这个问题,我们可以将边界字符与一个先行断言组合使用,并且说我们希望它以一个单词边界结束,但只有在后面跟着一个空格或句子/字符串的结尾时才会结束。这将确保我们不会截断子域或域的一部分,如果地址中间有一些无效的信息。

现在,我们不再创建一个会尝试解析电子邮件的东西,无论用户如何输入;创建验证器和模式的目的是强制用户输入一些逻辑的内容。也就是说,我们假设如果用户写了一个电子邮件地址,然后是一个句号,那么他/她并没有输入一个无效的地址,而是输入了一个地址,然后结束了一个句子(句号不是地址的一部分)。

在我们的代码中,我们假设在地址的末尾,用户要么会在后面加上一个空格,比如某种标点符号,要么会结束字符串/行。我们不再需要处理行,因为我们已经将它们转换为 HTML,但我们确实需要担心我们的模式在过程中不会捕获到 HTML 标签。

在这之后,我们的模式将类似于这样:

/\b[^\s<>@]+@[^\s<>@.]+\.[^\s<>@]+\b(?=.?(?:\s|<|$))/g

我们从一个单词边界开始,然后寻找之前的模式。我将大于号 (>) 和小于号 (<) 字符都添加到不允许的字符组中,这样就不会捕获任何 HTML 标签。在模式的末尾,您可以看到我们希望在单词边界结束,但只有在后面跟着一个空格、一个 HTML 标签或字符串的结尾时才会结束。完成所有匹配的完整函数如下:

function process_description() {
    var field = document.getElementById("description_field");
    var description = field.value;

    data.text_description = description;

    var line_pattern = /\n+/g;
    description = description.replace(line_pattern, function(match) {
        if (match == "\n") {
            return "<br />";
        } else {
            return "</p><p>";
        }
    });

    var email_pattern = /\b[^\s<>@]+@[^\s<>@.]+\.[^\s<>@]+\b(?=.?(?:\s|<|$))/g;
    description = description.replace(email_pattern, function(match){
        return "<a href='mailto:" + match + "'>" + match + "</a>";
    });

    data.html_description = "<p>" + description + "</p>";

    return true;
}

我们可以继续添加字段,但我认为重点已经被理解了。您有一个匹配您想要的模式,并且使用提取的数据,您能够提取和操作数据以满足您可能需要的任何格式。

理解描述正则表达式

让我们回到用于匹配用户输入的名称的正则表达式:

/\b[^\s<>@]+@[^\s<>@.]+\.[^\s<>@]+\b(?=.?(?:\s|<|$))/g

这是正则表达式的简要解释:

  • \b 断言其位置在 (^\w|\w$|\W\w|\w\W) 单词边界处

  • [^\s<>@]+ 匹配不在列表中的单个字符:

  • + 量词,出现一次或无限次

  • \s 匹配一个 [\r\n\t\f ] 空白字符

  • <>@<>@ 列表中的一个字符(区分大小写)

  • @ 字符串匹配字符 @

  • [^\s<>@.]+ 匹配不在此列表中的单个字符:

  • + 量词,出现一次或无限次

  • \s 匹配任何 [\r\n\t\f] 空白字符

  • <>@.<>@. 列表中的一个字符(区分大小写)

  • \. 字符串匹配字符 .

  • [^\s<>@]+ 匹配不在此列表中的单个字符:

  • + 量词,出现一次或无限次

  • \s 匹配一个 [\r\n\t\f ] 空白字符

  • <>@<>@ 列表中的一个字符(区分大小写)

  • \b 断言其位置在 (^\w|\w$|\W\w|\w\W) 单词边界处

  • (?=.?(?:\s|<|$)) 正向先行断言 - 断言正则表达式可以匹配下面的内容

  • .? 匹配任何字符(除了换行符)

  • ? 量词,出现零次或一次

  • (?:\s|<|$) 是一个非捕获组:

  • 第一种选择:\s 匹配任何空白字符 [\r\n\t\f]

  • 第二种选择:< 字符串匹配字符 <

  • 第三种选择:$ 断言字符串的结束位置

  • g 修饰符:全局匹配。返回正则表达式的所有匹配项,而不仅仅是第一个

解释一个 Markdown 示例

更多正则表达式的示例可以在流行的Markdown语法中看到(参考en.wikipedia.org/wiki/Markdown)。这是一个用户被迫以自定义格式编写东西的情况,尽管它仍然是一个格式,可以节省输入并且更容易理解。例如,在 Markdown 中创建一个链接,你会输入类似于这样的内容:

[Click Me](http://gabrielmanricks.com)

然后会转换为:

<a href="http://gabrielmanricks.com">Click Me</a>

忽略 URL 本身的任何验证,可以很容易地通过以下模式实现:

/\[([^\]]*)\]\(([^(]*)\)/g

它看起来有点复杂,因为方括号和括号都是需要转义的特殊字符。基本上,我们想要一个开放的方括号,直到闭合的方括号,然后我们想要一个开放的括号,再次,直到闭合的括号。

提示

一个很好的网站来写 markdown 文档是dillinger.io/

由于我们将每个部分都包装到自己的捕获组中,我们可以编写这个函数:

text.replace(/\[([^\]]*)\]\(([^(]*)\)/g, function(match, text, link){
    return "<a href='" + link + "'>" + text + "</a>";
});

在我们的操作示例中,我们没有使用捕获组,但如果使用它们,那么回调的第一个参数将是整个匹配(类似于我们一直在使用的那些),然后所有单独的组将作为后续参数传递,按照它们在模式中出现的顺序。

总结

在本章中,我们涵盖了一些示例,向我们展示了如何验证用户输入以及如何操作它们。我们还看了一些常见的设计模式,并且看到有时候简化问题而不是在一个模式中使用蛮力来创建验证更好。

在下一章中,我们将继续通过使用Node.js开发一个应用程序来探索一些真实世界的问题,该应用程序可以用于读取文件并提取其信息,以更加用户友好的方式显示出来。

第五章:Node.js 和正则表达式

到目前为止,我们已经乐在其中学习如何为不同情况创建正则表达式。然而,您可能想知道在现实世界的情况下应用正则表达式会是什么样子,比如读取一个日志文件并以用户友好的格式呈现其信息?

在本章中,我们将学习如何实现一个简单的Node.js应用程序,它可以读取一个日志文件,并使用正则表达式解析它。这样,我们可以从中检索特定信息,并以不同的格式输出它。我们将测试我们从本书前几章中获得的所有知识。

在本章中,我们将涵盖以下主题:

  • 安装所需的软件来开发我们的示例

  • 使用 Node.js 读取文件

  • 分析 Apache 日志文件的解剖学

  • 使用正则表达式创建解析器来读取 Apache 日志文件

设置 Node.js

由于我们将开发一个 Node.js 应用程序,第一步是安装 Node.js。我们可以从nodejs.org/download/获取它。只需按照下载说明进行操作,我们就可以在计算机上设置好它。

注意

如果这是您第一次使用 Node.js,请阅读nodejs.org/上的教程。

为了确保我们已经安装了 Node.js,打开终端应用程序(如果您使用 Windows,则为命令提示符),然后输入node –v。安装的 Node.js 版本应该显示如下:

设置 Node.js

我们现在可以开始了!

开始我们的应用程序

让我们开始用 Node.js 开发我们的示例应用程序,它将读取一个日志文件,并使用正则表达式解析其信息。我们将在一个名为regex.js的 JavaScript 文件中创建所有所需的代码。在我们开始编码之前,我们将进行一个简单的测试。在regex.js中添加以下内容:

console.log('Hello, World!');

接下来,在终端应用程序中,从创建文件的目录中执行regex.js命令节点。Hello, World!消息应该显示如下:

开始我们的应用程序

使用 Node.js 创建了 hello world 应用程序并且它可以工作了!我们现在可以开始编写我们的应用程序了。

使用 Node.js 读取文件

由于我们的应用程序的主要目标是读取一个文件,我们需要应用程序要读取的文件!我们将使用一个示例 Apache 日志文件。互联网上有很多文件,但我们将使用可以从fossies.org/linux/source-highlight/tests/access.log下载的日志文件。将文件放在创建regex.js文件的同一目录中。

注意

这个示例 Apache 日志文件也可以在本书的源代码包中找到。

要使用 Node.js 读取文件,我们需要导入 Node.js 文件系统模块。删除我们在regex.js文件中放置的console.log消息,并添加以下一行代码:

var fs = require('fs');

注意

要了解更多关于 Node.js 文件系统模块的信息,请阅读其文档nodejs.org/api/fs.html

下一步是打开文件并读取其内容。我们将使用以下代码来实现这一点:

fs.readFile('access.log', function (err, data) {//#1

  if (err) throw err;//#2

  var text = data.toString();//#3

  var lines = text.split('\n');//#4

  lines.forEach(function(line) {//#5
    console.log(line);//#6
  });
});

根据 Node.js 文档,readFile函数(#1)可以接收三个参数:文件名(access.log),某些选项(在本例中我们没有使用),以及当文件内容加载到内存中时将执行的回调函数。

注意

要了解更多关于readLine函数的信息,请访问nodejs.org/api/fs.html#fs_fs_readfile_filename_options_callback

回调函数接收两个参数。第一个是错误。如果出现问题,将抛出异常(#2)。第二个参数是data,其中包含文件内容。我们将在名为text的变量中存储一个包含所有文件内容的字符串(#3)。

然后,日志的每条记录都放在文件的一行中。因此,我们可以继续分隔文件记录并将其存储到一个数组中(#4)。现在,我们可以迭代包含日志行的数组(#5)并在每行执行一个操作。在这种情况下,我们现在只是在console#6)中输出每行的内容。我们将在下一节中用不同的逻辑替换代码的第#6行。

如果我们执行regex.js命令节点,则所有文件内容应显示如下:

使用 Node.js 读取文件

Apache 日志文件的解剖

在创建将匹配 Apache 文件一行的正则表达式之前,我们需要了解它包含什么样的信息。

让我们看一下access.log中的一行:

127.0.0.1 - jan [30/Jun/2004:22:20:17 +0200] "GET /cgi-bin/trac.cgi/login HTTP/1.1" 302 4370 "http://saturn.solar_system/cgi-bin/trac.cgi" "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7) Gecko/20040620 Galeon/1.3.15"

我们正在阅读的 Apache 访问日志遵循%h%l%u%t“%r”%>s%b“%{Referer}i”“%{User-agent}i”格式。让我们看看每个部分:

  • %h:日志的第一部分是(127.0.0.1)IP 地址

  • %l:在第二部分中,输出中的连字符表示所请求的信息部分不可用

  • %u:第三部分是请求(jan)文档的用户 ID。

  • %t:第四部分是请求接收的时间,例如([30/Jun/2004:22:20:17 +0200])。它的格式是[day/month/year:hour:minute:second zone],其中:

  • day = 2*digit

  • month = 3*letter

  • year = 4*digit

  • hour = 2*digit

  • minute = 2*digit

  • second = 2*digit

  • zone =(``+' | `-')4*digit

  • \"%r\":第五部分是客户端的请求行,用双引号给出,例如("GET /cgi-bin/trac.cgi/login HTTP/1.1"

  • %>s:第六部分是服务器发送给(302)客户端的状态代码

  • %b:第七部分是返回给(4370)客户端的对象的大小

  • \"%{Referer}i\":第八部分是客户端报告从中引用的站点,用双引号给出,例如("http://saturn.solar_system/cgi-bin/trac.cgi"

  • \"%{User-agent}i\":第九部分也是用户代理 HTTP 请求标头,也用双引号给出,例如("Mozilla/5.0(X11;U;Linux i686;en-US;rv:1.7)Gecko/20040620 Galeon/1.3.15"

所有部分都用空格分隔。有了这些信息和之前提供的信息,我们可以开始创建我们的正则表达式。

注意

有关 Apache 日志格式的更多信息,请阅读httpd.apache.org/docs/2.2/logs.html

创建 Apache 日志正则表达式

在 Apache 访问日志文件中,我们要识别并从文件的每一行中提取九个部分。在创建正则表达式时,我们可以尝试两种方法:可以非常具体或更通用。如前所述,最强大的正则表达式是通用的。我们将在本章中尝试实现这些表达式。

例如,对于日志的第一部分,我们知道它是一个 IP 地址。我们可以具体使用正则表达式(^\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)匹配 IP,或者,正如我们所知,日志以 IP 开头,我们可以使用^(\S+),其中^表示它匹配输入的开头,\S匹配除空格之外的单个字符。^(\S+)表达式将精确匹配日志的第一部分,直到找到空格(例如 IP 地址)。此外,^(\S+)比使用^\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b更简单,但我们仍然实现了相同的结果。

让我们继续测试到目前为止创建的正则表达式:

创建 Apache 日志正则表达式

为了总结我们在第一章中学到的内容,正则表达式入门exec方法在字符串中执行匹配搜索。它返回一个信息数组,因为它是字符串第一个匹配的位置,然后是正则表达式的每个部分中的后续位置。

对于第二部分和第三部分,我们可以继续使用^(\S+)正则表达式。第二部分和第三部分可以包含特定信息(包括一组字母数字字符),或者可以包含连字符。我们对每个部分中出现的信息感兴趣,直到找到一个空格。因此,我们可以在我们的正则表达式中添加两个^(\S+)^(\S+) (\S+) (\S+)并测试它:

创建 Apache 日志正则表达式

日志行的前三部分被识别出来。

为时间部分创建正则表达式

第四部分是括号之间给定的时间。从日志中匹配时间的正则表达式是\[([^:]+):(\d+:\d+:\d+) ([^\]]+)\]

让我们看看如何实现这个结果。

首先,我们有开放和关闭括号。我们不能简单地在正则表达式中使用[],因为正则表达式中的括号代表一组字符(正如我们在第三章中学到的那样,特殊字符)。因此,我们需要在每个括号前使用(\)转义字符,以便我们可以将括号表示为正则表达式的一部分。

时间正则表达式的下一部分是"([^:]+):"。在开放括号之后,我们想匹配任何字符,直到找到(:)冒号。我们在第二章中学到了关于否定范围的知识,这正是我们要使用的。我们期望任何字符都可以出现,除了冒号,所以我们使用[ˆ:]来表示它。此外,它可以由一个或多个字符组成,比如(+)。接下来,我们期望有一个(:)冒号。有了这个正则表达式的一部分,我们就可以匹配"[30/Jun/2004:",从"[30/Jun/2004:22:20:17 +0200]"中。

同样的正则表达式可以表示为"(\d{2}\/\w{3}\/\d{4}):",因为日期以两位数字的形式给出,月份以三个字符给出,年份以四位数字给出,并用\分隔。

正则表达式的下一部分是(\d+:\d+:\d+)。它将从示例中匹配22:20:17\d字符匹配任何数字(+匹配一个或多个),后跟一个(:)冒号。我们也可以使用(\d{2}:\d{2}:\d{2}),因为小时、分钟和秒分别由两位数字表示。

最后一部分是([^\]]+)\]。我们期望任何字符,除了"]"([^\]] - 否定])。这将匹配时区(+0200)。我们也可以使用([\+|-]\d{4})作为正则表达式,因为时区格式是+-,后跟四位数字。

当我们测试正则表达式时,我们将得到这个结果:

为时间部分创建正则表达式

注意

请注意,时间的每个部分都被一个子集分割(日期、时间和时区),由括号组()分隔。如果我们想将时间作为一个单独的部分,我们可以删除子集:\[(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [\+|-]\d{4})\]

为请求信息创建正则表达式

在我们分开的部分(在此之前的几个部分中),让我们来处理日志的第五部分,即请求信息。

让我们看一下"GET /cgi-bin/trac.cgi/login HTTP/1.1"的例子,这样我们就可以从中创建一个正则表达式。

请求用双引号括起来,这样我们知道要在\" \"内创建一个正则表达式。从前面的例子中,有三个部分(GET/cgi-bin/trac.cgi/loginHTTP/1.1)。因此,GET可以用(\S+)表示。

接下来,我们有/cgi-bin/trac.cgi/login。我们将使用(.*?),意思是,它可以是任何字符,或者什么都没有。我们将使用这个,因为我们不知道这个信息的格式。

然后,我们有HTTP/1.1协议,为了匹配它,我们还将使用(\S+)

当我们尝试匹配正则表达式时,将会得到以下结果:

为请求信息创建正则表达式

提示

如果我们想要分别检索请求的每个部分(比如方法、资源和协议),我们可以像之前一样使用()

为状态码和对象大小创建正则表达式

日志的下两部分很简单。第一部分是状态,表示为2xx3xx4xx5xx,因此基本上是三位数。我们可以用两种方式表示它:(\S+),它将匹配任何字符,直到找到一个空格;或者(\d{3})。当然,我们还可以更具体一些,允许第一个数字只能是2345,不过,让我们不要把它搞得太复杂。

一个数字也可以表示对象大小。然而,如果没有返回信息,它将用连字符表示,所以(\S+)表示得最好。或者我们也可以使用([\d|-]+)

输出将会是以下内容:

为状态码和对象大小创建正则表达式

为引荐者和用户代理创建正则表达式

这两部分都用双引号括起来。我们可以使用"([^"]*)"表达式来表示这些信息,这意味着包括除了"之外的任何字符。我们可以在这两部分中应用它。

通过添加日志的最后两部分,我们将得到以下输出:

为引荐者和用户代理创建正则表达式

我们的最终正则表达式来匹配 Apache 访问日志的一行如下:

^(\S+) (\S+) (\S+) \[(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [\+|-]\d{4})\] \"(\S+ .*? \S+)\" (\d{3}) ([\d|-]+) "([^"]*)" "([^"]*)"

一次性创建一个正则表达式可能会很棘手和复杂。然而,我们已经将每个部分分开并创建了一个正则表达式。在所有这些结束时,我们所要做的就是将所有这些部分组合在一起。

我们现在已经准备好继续编写我们的应用程序。

解析每个 Apache 日志行

我们现在知道了我们想要使用的正则表达式,所以我们需要做的就是将(#1)正则表达式添加到代码中,对每一行进行正则表达式匹配(#2),并获得结果(#3)。目前我们只需要在控制台中输出结果(#4)。代码如下所示:

var fs = require('fs');

fs.readFile('access.log', function (err, logData) {

  if (err) throw err;

  var text = logData.toString(),
    lines = text.split('\n'),
    results = {},
    regex = /^(\S+) (\S+) (\S+) \[(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} [\+|-]\d{4})\] \"(\S+ .*? \S+)\" (\d{3}) ([\d|-]+) "([^"]*)" "([^"]*)"/; //#1

  lines.forEach(function(line) {

    results = regex.exec(line); //#2

    for (i=0; i<results.length; i++){ //#3
      console.log(results[i]); //#4
    }

  });  //#5
});

提示

这是唯一使正则表达式与 Node.js 配合工作的方法吗?

在这个例子中,我们使用了 JavaScript 正则表达式,这是我们在整本书中学到的。然而,Node.js 还有其他可以在使用正则表达式时让我们的生活更轻松的包。node-regexp包是提供在 Node.js 中使用正则表达式的新方法的包之一。值得一看,并花一些时间在www.npmjs.com/package/node-regexp上进行尝试。

我们将在接下来的两节中继续完成我们的代码。

为每一行创建一个 JSON 对象

让我们尝试对 Apache 日志的每一行做一些更有用的事情。我们将为每一行创建一个JavaScript 对象表示JSON)对象,并将其添加到一个数组中。为了包装我们的应用程序,我们将把 JSON 内容保存到一个文件中。

注意

要了解更多关于 JSON 的信息,请参考www.json.org/

所以在正则表达式声明之后(它在var声明内部),我们将添加一个新变量,用于保存我们将创建的JSON对象的集合:

jsonObject = [],
row;

与前一节代码中的#3#4行不同,我们将放置以下代码:

if (results){
  row = {
    ip: results[1],
    available: results[2],
    userid: results[3],
    time: results[4],
    request: results[5],
    status: results[6],
    size: results[7],
    referrer: results[8],
    userAgent: results[9],
  }

  jsonObject.push(row);
}

这段代码将验证正则表达式的执行是否产生了任何结果,并将创建一个名为row的 JSON 对象。然后,我们只需要将 JSON 对象添加到jsonObject数组中。

接下来,我们将构建 Node.js 应用程序的最后一部分。我们将创建一个 JSON 文件,其中包含我们创建的 JSON 数组。我们需要将以下代码放在代码的第#5行,就像在前一节中看到的那样:

var outputFilename = 'log.json';
fs.writeFile(outputFilename, JSON.stringify(jsonObject, null, 4), function(err) {
  if(err) {
    console.log(err);
  } else {
    console.log("JSON saved to " + outputFilename);
  }
});

注意

要了解更多关于writeFile函数的信息,请参考nodejs.org/api/fs.html#fs_fs_writefile_filename_data_options_callback

结果将是一个类似以下内容的 JSON:

[
  {
    "ip": "127.0.0.1",
    "available": "-",
    "userid": "jan",
    "time": "30/Jun/2004:22:20:17 +0200",
    "request": "GET /cgi-bin/trac.cgi/login HTTP/1.1",
    "status": "302",
    "size": "4370",
    "referrer": "http://saturn.solar_system/cgi-bin/trac.cgi",
    "userAgent": "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7) Gecko/20040620 Galeon/1.3.15"
  }
  //more content
]

在表中显示 JSON

最后一步是创建一个简单的 HTML 页面来显示 Apache 日志内容。我们将创建一个 HTML 文件,并将以下代码放入其中:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Log</title>
    <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.5.0/bootstrap-table.min.css">
    <script src="img/jquery.min.js"></script>
    <script src="img/bootstrap-table.min.js"></script>
    <style>
      body{
        margin-top: 30px;
        margin-right: 30px;
        margin-left: 30px;
      }
    </style>
  </head>

上述代码包含所需的 JavaScript 和 CSS 导入,以便我们可以显示 Apache 日志。

注意

此示例的表是使用 Bootstrap 表创建的。有关其用法和示例的更多信息,请访问wenzhixin.net.cn/p/bootstrap-table/docs/examples.html

下一个也是最后一个代码片段是 HTML 的正文:

  <body>
    <table data-toggle="table" data-url="log.json" data-cache="false" data-height="400" data-show-refresh="true" data-show-toggle="true" data-show-columns="true" data-search="true" data-select-item-name="toolbar1" >
      <thead>
        <tr>
          <th data-field="ip">IP</th>
          <th data-field="time">Time</th>
          <th data-field="request">Request Info</th>
          <th data-field="status">Status</th>
          <th data-field="size">Size</th>
          <th data-field="referrer">Referrer</th>
          <th data-field="userAgent">User Agent</th>
        </tr>
      </thead>
    </table>
  </body>
</html>

正文将包含一个表,该表将读取log.json文件的内容,解析并显示它。

为了能够在浏览器中打开 html 文件,我们需要一个服务器。这是因为我们的代码正在使用 Ajax 请求来加载 Node.js 应用程序创建的 JSON 文件。由于我们已经安装了 Node.js,我们可以使用它最简单的服务器来执行我们的代码。

在终端中,执行以下命令以安装服务器:

npm install http-server –g

然后,将目录更改为为 HTML 文件创建的目录:

cd chapter05

最后,启动服务器:

http-server

您将能够在http://localhost:8080/ URL 中看到结果。我们可以在以下图片中看到最终结果:

在表中显示 JSON

我们还可以切换表中的结果并查看完整的数据:

在表中显示 JSON

现在我们已经完成了我们的示例 Node.js 应用程序,它已经读取并解析了 Apache 日志文件,并可以以更友好的方式显示出来。

摘要

在本章中,我们学习了如何创建一个简单的 Node.js 应用程序,读取 Apache 日志文件并使用正则表达式提取日志信息。我们能够将我们在本书前几章中所学到的知识付诸实践。

我们还学到,要创建一个非常复杂的正则表达式,最好是分步进行。我们学到,创建正则表达式时可以非常具体,也可以更通用,从而达到相同的结果。

随着EcmaScript的新版本(EcmaScript 6)的创建(将为 JavaScript 添加许多新功能),熟悉与正则表达式相关的改进是很好的。有关更多信息,请访问www.ecmascript.org/dev.php

希望您喜欢这本书!玩得开心,创建正则表达式!

附录 A. JavaScript 正则表达式速查表

在本附录中,您可以找到 JavaScript 中正则表达式中使用的模式的摘要,以及它们的描述,以及一些有用的测试和创建正则表达式的方法。

本附录将涵盖以下正则表达式主题:

  • 字符类和文字

  • 字符集

  • 边界和量词

  • 分组、选择和反向引用

  • 有用的方法

字符类

在下表中,您可以找到字符类的模式,这些模式告诉正则表达式匹配单个字符:

模式 描述 示例
. 这匹配任何字符,除了换行符或其他 Unicode 行终止符,例如 (\n, \r, \u2028\u2029)。 /f.o/ 匹配 "fao","feo" 和 "foo"
\w 这匹配任何字母数字字符,包括下划线。它等同于[a-zA-Z0-9_] /\w/ 匹配 "foo" 中的 "f"
\W 这匹配任何单个非单词字符。它等同于[^a-zA-Z0-9_] /\W/ 匹配 "100%" 中的 "%"
\d 这匹配任何单个数字。它等同于[0-9] /\d/ 匹配 "100" 中的 "1"
\D 这匹配任何非数字字符。它等同于[⁰-9] /\D/ 匹配 "R2-D2" 中的 "R"
\s 这匹配任何单个空格字符。它等同于[ \t\r\n\v\f] /\s/ 匹配 "foo bar" 中的 " "
\S 这匹配任何单个非空格字符。它等同于[^ \t\r\n\v\f] /\S/ 匹配 "foo bar" 中的 "foo"

文字

在下表中,您可以找到文字字符的模式,这些模式告诉正则表达式匹配特殊字符:

模式 描述 示例
字母数字 这些字符本身匹配。 /javascript book/ 匹配 "javascript book" 中的 "javascript book"
\0 这匹配 NUL 字符。
\n 这匹配换行符。
\f 这匹配换页符。
\r 这匹配回车符。
\t 这匹配制表符。
\v 这匹配垂直制表符。
[\b] 这匹配退格字符。
\xxx 这匹配 ASCII 字符,由 xxx 八进制数表示。 /112/ 匹配 "J" 字符
\xdd 这匹配 ASCII 字符,由 dd 十六进制数表示。 /x4A/ 匹配 "J" 字符
\uxxxx 这匹配 ASCII 字符,由 xxxx UNICODE 表示。 /u0237/ 匹配 "J" 字符
\ 这表明下一个字符是否特殊,不应被字面解释。 /\^/ 匹配 "char ^" 中的 "^"

字符集

在下表中,您可以找到字符集的模式,这些模式告诉正则表达式仅匹配几个字符中的一个。

模式 描述 示例
[xyz] 这匹配字符集中的任何一个字符。您可以使用连字符表示范围。例如,/[a-z]/ 匹配字母表中的任何字母,/[0-9]/ 匹配任何单个数字。 /[ao]/ 匹配 "bar" 中的 "a"
[^xyz] 这匹配任何一个不在字符集中的字符。 /[^ao]/ 匹配 "bar" 中的 "b"

边界

在下表中,您可以找到边界的模式,这将告诉正则表达式在什么位置进行匹配。

模式 描述 示例
^ 这匹配输入的开头。如果多行标志设置为 true,则也匹配 (\n) 换行字符之后的位置。 /^ The/ 匹配 "The" 中的 "The",但不匹配 "In The stars"
` 模式 描述
--- --- ---
^ 这匹配输入的开头。如果多行标志设置为 true,则也匹配 (\n) 换行字符之后的位置。 /^ The/ 匹配 "The" 中的 "The",但不匹配 "In The stars"
| 这匹配输入的结尾。如果多行标志设置为 true,则也匹配 (\n) 换行字符之前的位置。 | /$/ 匹配 "land" 中的 "and",但不匹配 "and the bar"
\b 这匹配任何单词边界(测试字符必须存在于字符串中单词的开头或结尾)。 /va\b/ 在“this is a java script book”中匹配“va”,但不匹配“this is a javascript book”。
\B 这匹配任何非单词边界。 /va\B/ 在“this is a JavaScript book”中匹配“va”,但不匹配“this is a JavaScript book”。

分组、交替和反向引用

在下表中,您可以找到分组、交替和反向引用的模式。分组用于在正则表达式中分组一组字符。交替用于将字符组合成单个正则表达式,而反向引用用于匹配与以前捕获组匹配的相同文本:

模式 描述 示例
(x) 这将字符分组在一起以创建一个子句,即匹配x并记住匹配。这些称为捕获括号。 /(foo)/ 在“foo bar”中匹配并记住“foo”。
() 括号还用于捕获模式内所需的子模式。 /(\d\d)\/(\d\d)\/(\d\d\d\d)/ 在“12/12/2000”中匹配“12”、“12”和“2000”。
(?:x) 这匹配x但不捕获它。换句话说,括号内的项目不会创建编号引用。这些称为非捕获括号。 /(?:foo)/ 匹配但不记住“foo”在“foo bar”中。
&#124; 交替将子句组合成一个正则表达式,然后匹配任何单个子句。 x&#124;y匹配xy。它类似于“OR”语句。 /morning&#124;night/ 在“good morning”中匹配“morning”,在“good night”中匹配“night”。
()\n 在正则表达式模式的末尾添加"\n"(其中 n 是 1-9 之间的数字)允许您在模式内部反向引用一个子模式,因此,子模式的值被记住并用作匹配的一部分。 /(no)\1/ 在“nono”中匹配“nono”。 "\1"被替换为模式内的第一个子模式的值,或(no),形成最终模式。

量词

在下表中,您可以找到量词的模式,它们指定了输入中必须存在多少个字符、组或字符类的实例才能找到匹配。

模式 描述 示例
{n} 这匹配正则表达式的确切n次出现。 /\d{5}/ 在“1234567890”中匹配“12345”(五个数字)。
{n,} 这匹配正则表达式的n次或更多次出现。 /\d{5,}/ 在“1234567890”中匹配“1234567890”(至少五个数字)。
{n,m} 这匹配正则表达式的nm次出现。 /\d{5,7}/ 在“1234567890”中匹配“1234567”(至少五个数字,最多七个数字)。
* 这匹配零次或更多次出现,等同于{0,} /fo*/ 在“foo”中匹配“foo”,在“fooooooooled”中匹配“foooooooo”。
+ 这匹配一次或多次出现,等同于{1,} /o+/ 在“foo”中匹配“oo”。
? 这匹配零次或一次出现,等同于{0,1} /fo?/ 在“foo”中匹配“fo”,在“fairy”中匹配“f”。
+?``*? ?也可以用在*+?{}量词之后,使后者匹配非贪婪,或最小次数,而不是默认的最大次数。 /\d{2,4}?/ 在字符串“12345”中匹配“12”,而不是由于量词非贪婪的末尾的?而匹配“1234”。
x(?=y) 正向先行断言:仅当x后面跟着y时才匹配x。请注意,y不作为匹配的一部分,只作为必需条件。 /Java(?=Script&#124;Hut)/ 在“JavaScript”或“JavaHut”中匹配“Java”,但不匹配“JavaLand”。
x(?!y) 负向先行断言:仅当x后面不跟着y时才匹配x。请注意,y不作为匹配的一部分,只起到必要的条件。 /^\d+(?! years)/ 在"5 days"或"5 books"中匹配"5",但不匹配"5 years"。

JavaScript 正则表达式方法

在下表中,您可以找到用于匹配或测试正则表达式的方法。正则表达式中使用的主要 JavaScript 对象是StringRegExp,它们表示模式(如regular expression)。

方法 描述 示例
String.match(regular expression) 这在字符串中执行基于正则表达式的匹配搜索。 var myString = "today is 12-12-2000";``var matches = myString.match(/\d{4}/);``//返回数组["2000"]
RegExp.exec(string) 这在其字符串参数中执行匹配搜索。与String.match不同,输入的参数应该是一个字符串,而不是正则表达式模式。 var pattern = /\d{4}/;``pattern.exec("today is 12-12-2000");``//返回数组["2000"]
String.replace(regular expression, replacement text) 这搜索并用替换文本替换正则表达式部分(匹配)。 var phone = "(201) 123-4567";``var phoneFormatted = phone.replace(/[\(\)-\s]/g, "");``//返回 2011234567(删除了()-和空格)
String.split (string literal or regular expression) 这根据正则表达式或固定字符串将字符串分割成子字符串数组。 var oldstring = "1,2, 3, 4, 5";``var newstring = oldstring.split(/\s*,\s*/);``//返回数组["1","2","3","4","5"]
String.search(regular expression) 这在字符串中测试匹配。它返回匹配的索引,如果没有找到则返回-1 var myString = "today is 12-12-2000";``myString.search(/\d{4}/);``//返回 15 - 2000 的索引
RegExp.test(string) 这测试给定的字符串是否与正则表达式匹配,如果匹配则返回 true,否则返回 false。 var pattern = /\d{4}/;``pattern.test("today is 12-12-2000");``//返回 true

在本附录中,我们非常简要地介绍了本书中学到的模式,以便日常查阅。

posted @ 2024-05-22 12:07  绝不原创的飞龙  阅读(20)  评论(0编辑  收藏  举报