精华内容
下载资源
问答
  • 日期判断正则表达式

    千次阅读 2007-05-03 15:56:00
    日期判断正则表达式 YYYY-MM-DD基本上把闰年和2月等的情况都考虑进去了^((((1[6-9]|[2-9]/d)/d{2})-(0?[13578]|1[02])-(0?[1-9]|[12]/d|3[01]))|(((1[6-9]|[2-9]/d)/d{2})-(0?[13456789]|1[012])-(0?[1-9]|[12]/d|30...
    日期判断正则表达式

    YYYY-MM-DD基本上把闰年和2月等的情况都考虑进去了
    ^((((1[6-9]|[2-9]/d)/d{2})-(0?[13578]|1[02])-(0?[1-9]|[12]/d|3[01]))|(((1[6-9]|[2-9]/d)/d{2})-(0?[13456789]|1[012])-(0?[1-9]|[12]/d|30))|(((1[6-9]|[2-9]/d)/d{2})-0?2-(0?[1-9]|1/d|2[0-8]))|(((1[6-9]|[2-9]/d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))-0?2-29-))$

     

    多少年来,许多的编程语言和工具都包含对正则表达式的支持,.NET基础类库中包含有一个名字空间和一系列可以充分发挥规则表达式威力的类,而且它们也都与未来的Perl 5中的规则表达式兼容。

      此外,regexp类还能够完成一些其他的功能,例如从右至左的结合模式和表达式的编辑等。

      在这篇文章中,我将简要地介绍System.Text.RegularExpression中的类和方法、一些字符串匹配和替换的例子以及组结构的详细情况,最后,还会介绍一些你可能会用到的常见的表达式。

    应该掌握的基础知识

      规则表达式的知识可能是不少编程人员“常学常忘”的知识之一。在这篇文章中,我们将假定你已经掌握了规则表达式的用法,尤其是Perl 5中表达式的用法。.NET的regexp类是Perl 5中表达式的一个超集,因此,从理论上说它将作为一个很好的起点。我们还假设你具有了C#的语法和.NET架构的基本知识。

      如果你没有规则表达式方面的知识,我建议你从Perl 5的语法着手开始学习。在规则表达式方面的权威书籍是由杰弗里·弗雷德尔编写的《掌握表达式》一书,对于希望深刻理解表达式的读者,我们强烈建议阅读这本书。

    RegularExpression组合体

      regexp规则类包含在System.Text.RegularExpressions.dll文件中,在对应用软件进行编译时你必须引用这个文件,例如:

    csc r:System.Text.RegularExpressions.dll foo.cs

    命令将创建foo.exe文件,它就引用了System.Text.RegularExpressions文件。

    名字空间简介

      在名字空间中仅仅包含着6个类和一个定义,它们是:

      Capture: 包含一次匹配的结果;
      CaptureCollection: Capture的序列;
      Group: 一次组记录的结果,由Capture继承而来;
      Match: 一次表达式的匹配结果,由Group继承而来;
      MatchCollection: Match的一个序列;
      MatchEvaluator: 执行替换操作时使用的代理;
      Regex: 编译后的表达式的实例。

      Regex类中还包含一些静态的方法:

      Escape: 对字符串中的regex中的转义符进行转义;
      IsMatch: 如果表达式在字符串中匹配,该方法返回一个布尔值;
      Match: 返回Match的实例;
      Matches: 返回一系列的Match的方法;
      Replace: 用替换字符串替换匹配的表达式;
      Split: 返回一系列由表达式决定的字符串;
      Unescape:不对字符串中的转义字符转义。

    简单匹配

      我们首先从使用Regex、Match类的简单表达式开始学习。

    Match m = Regex.Match("abracadabra", "(a|b|r)+");

    我们现在有了一个可以用于测试的Match类的实例,例如:if (m.Success)...
    如果想使用匹配的字符串,可以把它转换成一个字符串:

    Console.WriteLine("Match="+m.ToString());

    这个例子可以得到如下的输出: Match=abra。这就是匹配的字符串了。

    字符串的替换

      简单字符串的替换非常直观。例如下面的语句:

    string s = Regex.Replace("abracadabra", "abra", "zzzz");

    它返回字符串zzzzcadzzzz,所有匹配的字符串都被替换成了zzzzz。

      现在我们来看一个比较复杂的字符串替换的例子:

    string s = Regex.Replace(" abra ", @"^/s*(.*?)/s*$", "$1");

    这个语句返回字符串abra,其前导和后缀的空格都去掉了。

      上面的模式对于删除任意字符串中的前导和后续空格都非常有用。在C#中,我们还经常使用字母字符串,在一个字母字符串中,编译程序不把字符“ /” 作为转义字符处理。在使用字符“/”指定转义字符时,@"..."是非常有用的。另外值得一提的是$1在字符串替换方面的使用,它表明替换字符串只能包含被替换的字符串。

    匹配引擎的细节

      现在,我们通过一个组结构来理解一个稍微复杂的例子。看下面的例子:

    string text = "abracadabra1abracadabra2abracadabra3";

      string pat = @"

        ( # 第一个组的开始

         abra # 匹配字符串abra

         ( # 第二个组的开始

         cad # 匹配字符串cad

         )? # 第二个组结束(可选)

        ) # 第一个组结束

        + # 匹配一次或多次

        ";

      //利用x修饰符忽略注释

      Regex r = new Regex(pat, "x");

      //获得组号码的清单

      int[] gnums = r.GetGroupNumbers();

      //首次匹配

      Match m = r.Match(text);

      while (m.Success)

       {

      //从组1开始

       for (int i = 1; i < gnums.Length; i++)

        {

        Group g = m.Group(gnums[i]);

      //获得这次匹配的组

        Console.WriteLine("Group"+gnums[i]+"=["+g.ToString()+"]");

      //计算这个组的起始位置和长度

        CaptureCollection cc = g.Captures;

        for (int j = 0; j < cc.Count; j++)

         {

         Capture c = cc[j];

         Console.WriteLine(" Capture" + j + "=["+c.ToString()

           + "] Index=" + c.Index + " Length=" + c.Length);

         }

        }

      //下一个匹配

       m = m.NextMatch();

       }

    这个例子的输出如下所示:
         
      Group1=[abra]

          Capture0=[abracad] Index=0 Length=7

          Capture1=[abra] Index=7 Length=4

      Group2=[cad]

          Capture0=[cad] Index=4 Length=3

      Group1=[abra]

          Capture0=[abracad] Index=12 Length=7

          Capture1=[abra] Index=19 Length=4

      Group2=[cad]

          Capture0=[cad] Index=16 Length=3

      Group1=[abra]

          Capture0=[abracad] Index=24 Length=7

          Capture1=[abra] Index=31 Length=4

      Group2=[cad]

          Capture0=[cad] Index=28 Length=3

      我们首先从考查字符串pat开始,pat中包含有表达式。第一个capture是从第一个圆括号开始的,然后表达式将匹配到一个abra。第二个capture组从第二个圆括号开始,但第一个capture组还没有结束,这意味着第一个组匹配的结果是abracad ,而第二个组的匹配结果仅仅是cad。因此如果通过使用?符号而使cad成为一项可选的匹配,匹配的结果就可能是abra或abracad。然后,第一个组就会结束,通过指定+符号要求表达式进行多次匹配。

      现在我们来看看匹配过程中发生的情况。首先,通过调用Regex的constructor方法建立表达式的一个实例,并在其中指定各种选项。在这个例子中,由于在表达式中有注释,因此选用了x选项,另外还使用了一些空格。打开x选项,表达式将会忽略注释和其中没有转义的空格。

      然后,取得表达式中定义的组的编号的清单。你当然可以显性地使用这些编号,在这里使用的是编程的方法。如果使用了命名的组,作为一种建立快速索引的途径这种方法也十分有效。

      接下来是完成第一次匹配。通过一个循环测试当前的匹配是否成功,接下来是从group 1开始重复对组清单执行这一操作。在这个例子中没有使用group 0的原因是group 0是一个完全匹配的字符串,如果要通过收集全部匹配的字符串作为一个单一的字符串,就会用到group 0了。

      我们跟踪每个group中的CaptureCollection。通常情况下每次匹配、每个group中只能有一个capture,但本例中的Group1则有两个capture:Capture0和Capture1。如果你仅需要Group1的ToString,就会只得到abra,当然它也会与abracad匹配。组中ToString的值就是其CaptureCollection中最后一个Capture的值,这正是我们所需要的。如果你希望整个过程在匹配abra后结束,就应该从表达式中删除+符号,让regex引擎知道我们只需要对表达式进行匹配。

    基于过程和基于表达式方法的比较

      一般情况下,使用规则表达式的用户可以分为以下二大类:第一类用户尽量不使用规则表达式,而是使用过程来执行一些需要重复的操作;第二类用户则充分利用规则表达式处理引擎的功能和威力,而尽可能少地使用过程。

      对于我们大多数用户而言,最好的方案莫过于二者兼而用之了。我希望这篇文章能够说明.NET语言中regexp类的作用以及它在性能和复杂性之间的优、劣点。

    基于过程的模式

      我们在编程中经常需要用到的一个功能是对字符串中的一部分进行匹配或其他一些对字符串处理,下面是一个对字符串中的单词进行匹配的例子:

    string text = "the quick red fox jumped over the lazy brown dog.";

      System.Console.WriteLine("text=[" + text + "]");

      string result = "";

      string pattern = @"/w+|/W+";

      foreach (Match m in Regex.Matches(text, pattern))

       {

      // 取得匹配的字符串

       string x = m.ToString();

      // 如果第一个字符是小写

       if (char.IsLower(x[0]))

      // 变成大写

        x = char.ToUpper(x[0]) + x.Substring(1, x.Length-1);

      // 收集所有的字符

       result += x;

       }

      System.Console.WriteLine("result=[" + result + "]");

      正象上面的例子所示,我们使用了C#语言中的foreach语句处理每个匹配的字符,并完成相应的处理,在这个例子中,新创建了一个result字符串。这个例子的输出所下所示:

      text=[the quick red fox jumped over the lazy brown dog.]

      result=[The Quick Red Fox Jumped Over The Lazy Brown Dog.]

    基于表达式的模式

      完成上例中的功能的另一条途径是通过一个MatchEvaluator,新的代码如下所示:

    static string CapText(Match m)

        {

      //取得匹配的字符串

        string x = m.ToString();

      // 如果第一个字符是小写

        if (char.IsLower(x[0]))

      // 转换为大写

         return char.ToUpper(x[0]) + x.Substring(1, x.Length-1);

        return x;

        }

        

       static void Main()

        {

        string text = "the quick red fox jumped over the

         lazy brown dog.";

        System.Console.WriteLine("text=[" + text + "]");

        string pattern = @"/w+";

        string result = Regex.Replace(text, pattern,

       new MatchEvaluator(Test.CapText));

        System.Console.WriteLine("result=[" + result + "]");

        }

      同时需要注意的是,由于仅仅需要对单词进行修改而无需对非单词进行修改,这个模式显得非常简单。

    - 作者: muskteer 2005年07月30日, 星期六 14:32  回复(0) |  引用(0) 加入博采

    常用正则表达式集锦

    在使用RegularExpressionValidator验证控件时的验证功能及其验证表达式介绍如下:


    只能输入数字:“^[0-9]*$”
    只能输入n位的数字:“^/d{n}$”
    只能输入至少n位数字:“^/d{n,}$”
    只能输入m-n位的数字:“^/d{m,n}$”
    只能输入零和非零开头的数字:“^(0|[1-9][0-9]*)$”
    只能输入有两位小数的正实数:“^[0-9]+(.[0-9]{2})?$”
    只能输入有1-3位小数的正实数:“^[0-9]+(.[0-9]{1,3})?$”
    只能输入非零的正整数:“^/+?[1-9][0-9]*$”
    只能输入非零的负整数:“^/-[1-9][0-9]*$”
    只能输入长度为3的字符:“^.{3}$”
    只能输入由26个英文字母组成的字符串:“^[A-Za-z]+$”
    只能输入由26个大写英文字母组成的字符串:“^[A-Z]+$”
    只能输入由26个小写英文字母组成的字符串:“^[a-z]+$”
    只能输入由数字和26个英文字母组成的字符串:“^[A-Za-z0-9]+$”
    只能输入由数字、26个英文字母或者下划线组成的字符串:“^/w+$”
    验证用户密码:“^[a-zA-Z]/w{5,17}$”正确格式为:以字母开头,长度在6-18之间,

    只能包含字符、数字和下划线。
    验证是否含有^%&',;=?$/"等字符:“[^%&',;=?$/x22]+”
    只能输入汉字:“^[/u4e00-/u9fa5],{0,}$”
    验证Email地址:“^/w+[-+.]/w+)*@/w+([-.]/w+)*/./w+([-.]/w+)*$”
    验证InternetURL:“^http://([/w-]+/.)+[/w-]+(/[/w-./?%&=]*)?$”
    验证电话号码:“^(/(/d{3,4}/)|/d{3,4}-)?/d{7,8}$”

    正确格式为:“XXXX-XXXXXXX”,“XXXX-XXXXXXXX”,“XXX-XXXXXXX”,

    “XXX-XXXXXXXX”,“XXXXXXX”,“XXXXXXXX”。
    验证身份证号(15位或18位数字):“^/d{15}|/d{}18$”
    验证一年的12个月:“^(0?[1-9]|1[0-2])$”正确格式为:“01”-“09”和“1”“12”
    验证一个月的31天:“^((0?[1-9])|((1|2)[0-9])|30|31)$”

    正确格式为:“01”“09”和“1”“31”。

     

    - 作者: muskteer 2005年06月20日, 星期一 19:25  回复(0) |  引用(0) 加入博采

    安装vs.net与调试asp.net时遇到的常见问题
    安装vs.net与调试asp.net时遇到的常见问题

    特搜集了安装vs.net与调试asp.net时遇到的常见问题,希望能给刚接触.net的朋友再遇到此类问题时得已轻松的解决,如果您也遇到此类问题并已经解决而我这里又没帖出来的朋友希望您能将您的问题与解决方法一同帖出来,使此帖成为.Net 下安装、调试的常见问题与错误的一个很好的解决方案,在此谢谢大家的支持!!!


    Q:新建项目时出错:Visual Studio .NET 已检测到指定Web服务器运行的不是ASP.NET 1.1版。您将无法运行ASP.NET Web应用程序或服务。
    A:http://support.microsoft.com/default.aspx?scid=kb;en-us;817267
    ---------------------------------------------------------------------------------------------------------------------------------
    Q:  关于无法创建aps.web项目的解决办法
    A:http://www.csdn.net/develop/Read_Article.asp?Id=19725
    ---------------------------------------------------------------------------------------------------------------------------------
    Q: aspx项目不能调试
    A:1、已经启动了一个调试进程(同时打开了两个项目,且有一个已经在调试):同时只能启动一个;
      2、配置文件中debug="false":改成true;
      3、虚拟目录没有建立应用程序(或者名称为空):在虚拟目录属性中“应用程序”点击创建
      4、项目配置为Release:点菜单“生成”-配置,选择“debug”
      5、“你没有调试服务器权限”:修改IE的安全设置,“自动使用当前用户名和密码登录”

    如果还不行,参考MS的解决方法:

    对照你的错误信息,应该在这个文档中能找到解决办法
    http://www.gotdotnet.com/team/csharp/learn/whitepapers/howtosolvedebuggerproblems.doc
    ---------------------------------------------------------------------------------------------------------------------------------

    Q:把.NET程序部署到没有安装.NET Framwork的机器上
    A:http://www.microsoft.com/China/Community/program/originalarticles/TechDoc/deployNETApp.mspx
    ---------------------------------------------------------------------------------------------------------------------------------


    Q:安装VS.NET 2003的时候,遇到 "无法访问windows 安装程序组件"的错误
    A:根据提示,应该windows installer出了问题。
      需要重新安装windows installer。
      执行如下操作:
      一、先用msiexec /unregserver 停掉windows installer服务。
      二、下载InstMsiW.exe,用winrar解压开。进入目录。
      三、右击msi.inf ,点击安装。
      四、安装vs.net 2003
    ---------------------------------------------------------------------------------------------------------------------------------

    Q:vc软件包不可用或未注册
    A:
    症状:
    当建立一个WinForms应用程序时,收到下述错误信息:"VC软件包不可用或未注册".这个错误甚至会在看上去一个成功的安装之后出现.

    起因:
    如果Visual Studio.NET安装程序发现一些TLB文件和DLL文件已经存在的话,就不会再次在计算机中注册这些文件,因此
    就有可能发生"VC软件包不可用或未注册"的提示.
    在多数情况下,旧版本的的Visual Studio .NET容易引起这个错误,尤其是旧版本安装在另一个操作系统下.


    解决方案:
    要解决这个问题,需要运行Visual Studio .NET修复程序.由于修复程序会强制注册一些安装程序中没有被注册的项目,因此能更有效地解决这个问题.

    按照下列步骤运行修复程序:
    在开始中,指向"设置",点击控制面板,然后点击"添加/删除程序";
    在列出的已安装程序列表中,点击Visual Studio .NET,然后点击"更改/删除";
    点击Visual Studio .NET安装对话框1 2 3项目中的第二项;
    点击"修复/重装",然后按照屏幕提示操作.

    更多相关信息:
    重现这个问题的做法
    要重现这个问题,最好有装在独立分区上的两个操作系统.两个Windows XP Professional会达到这个目的.
    在一个操作系统下安装Visual Studio .NET;
    在另一个操作系统下Visual Studio .NET,安装过程中改变安装的缺省路径,以匹配第一次安装的路径.
    两次安装结果都会宣告成功.
    然后启动第二次安装的Visual Studio .NET(你就会发现这个问题的重现).

    这种安装形式是永远不推荐的,即使两次安装选项完全相同,而且你也会以此节省磁盘空间,但这种方法出现潜在问题的机会是很大的.如果一个Visual Studio .NET改变了文件和注册选项,这些改变不会记录到另一个Visual Studio .NET中,以致造成出现不可预料结果的潜在危险.

    引自: http://zhuonline.51.net/blogs/archives/000045.html
    http://support.microsoft.com/default.aspx?scid=http://support.microsoft.com:80/support/kb/articles/q320/4/27.asp&NoWebContent=1
    ---------------------------------------------------------------------------------------------------------------------------------

    Q:不能启动调试,灾难性错误(在 Visual Studio .NET 中调试 ASP.NET 应用程序时出现的常见错误)
    A:http://support.microsoft.com/?id=306172
    ---------------------------------------------------------------------------------------------------------------------------------

    Q:在生成安装和部署项目时收到“Unrecoverable Build Error”(不可恢复的生成错误)错误信息
    A:http://support.microsoft.com/?id=329214

    Automation服务器不能创建对象:
    javanow(原作)
      一直用得好好的vs.net编辑器,突然有一天在我新建一个工程时,进行到一半时,弹出"Automation 服务器不能创建对象",然后就停住了。
     
      我到csdn上以"Automation"关键字查阅了C#版的问题,发现有很多朋友都遇上过,而回答的朋友大部分以为提问者问的是IE解析xml文件时的那个错误 (见
    http://www.csdn.net/expert/topic/757/757297.xml )
      看到几个回答此类问题的,都是遇到过这个问题的朋友,他们的答案是:重装vs.net都没有作用,最后重新装操作系统才将这个问题解决。
    要我装操作系统真是要我的命,所以我一直用 Copy & Paste 工程文件来达到新建工程的目的。

      直到有一天,我无意中点了 vs_setup.msi 来添加 vs.net 的帮助文档(后来才知道应该点setup.exe来添加的)。vs_setup.msi进行到一半,也是出错了,一看调试结果,是FileSystemObject创建失败(感谢上帝,这里的错误信息终于是友好些了)。
    vs.net的错误 "Automation 服务器不能创建对象" 是不是也是这个导致的呢?

      我突然想起,前几个月在网上听说可以禁止使用FileSystemObject对象,我当时都忘了怎么操作的了,反正当时一摆弄,具体怎么操作的我也忘了,从那起我也从来没有在我的程序里用过FileSystemObject了。到google上搜索,发现禁止FileSystemObject有3种方法(http://www.sometips.com/faqs/315.htm),第一种方法:修改注册表([HKEY_LOCAL_MACHINE/SOFTWARE/Classes/CLSID/{0D43FE01-F093-11CF-8940-00A0C9054228}/ProgID]
    @="Scripting.FileSystemObject"),将FileSystemObject改成一个任意的名字,只有知道该名字的用户才可以创建该对象。我查看注册表,该处的名字还是 Scripting.FileSystemObject.

      那么我的FSO为何不能用呢?我突然想起原来当时我用的是第二种方法,"运行Regsvr32 scrrun.dll /u,所有用户无法创建FileSystemObject"。 当时真是太狠了些。赶快运行 Regsvr32 scrrun.dll。我试着用vs.net 创建一个工程,一阵进度提示,再也没有那个该死的 "Automation 服务器不能创建对象"


    其它解决方法相关帖子:
    http://yj.sxedu.com.cn/bk/downloadsoft.asp?mclass=14&sclass=29&id=125&keyword=
    http://www.csdn.net/develop/article/14/14928.shtm
    http://support.microsoft.com/default.aspx?scid=kb;en-us;Q323885


    :Unable to start debugging on the web server
    A:如果遇到这些错误,则需要考虑以下几个问题:

    要检查的内容
    远程服务器上的 Web 应用程序
    存储在 Visual SourceSafe 中并使用 FrontPage 服务器扩展的 Web 应用程序
    手动附加
    要检查的内容
    如果得到“无法在 Web 服务器上启动调试”错误,请尝试检查下列内容:

    您是否正在运行一个允许 Visual Studio 调试器自动附加到 Web 应用程序的 Windows 版本?如果不是,则需要启动应用程序而不调试,然后手动附加到它。(有关更多信息,请参阅手动附加和 ASP.NET 调试:系统要求。)
    您的 Web 应用程序是否具有 Web.config 文件?
    Web.config 文件是否通过将 debug 属性设置为 true 而启用了调试模式?有关更多信息,请参阅
    ASP.NET 应用程序中的调试模式。
    Web.config 是否包含任何语法错误?您可以通过运行 Web 应用程序而不调试来检查是否存在语法错误。(从“调试”菜单中,选择“开始执行(不调试)”。)如果在 Web.config 中存在语法错误,则会显示详细信息。
    您是否是“调试器用户”(Debugger Users) 组的成员?如果您作为管理员登录,则管理员是否在该组中?
    您是否是通过指定特定的 IP 地址(如 100.20.300.400)而创建了项目?调试 Web 服务器要求 NTLM 身份验证。默认情况下,IP 地址被假定为 Internet 的一部分,而在 Internet 上不进行 NTLM 身份验证。若要更正这一问题:
    创建项目时,指定 Intranet 上计算机的名称。
    -或-

    将 IP 地址 (http://100.20.300.400) 添加到您的计算机上的受信任站点列表中。(从 Internet Explorer 的“工具”菜单中,选择“Internet 选项”,然后选择“安全”选项卡)。
    运行 IIS 服务器的计算机是否已安装了 Visual Studio .NET 远程组件?
    IIS 是否是在安装了 Visual Studio .NET 之后才被安装在本地计算机(即运行 Visual Studio .NET 的计算机)上的?IIS 应在安装 Visual Studio .NET 之前安装。如果它是后来安装的,则可能需要修复 .NET 框架。
    修复 .NET 框架

    插入 Visual Studio .NET 光盘并运行
    <DVD Drive>:/wcu/dotNetFramework/dotnetfx.exe /t:c:/temp /c:"msiexec.exe /fvecms c:/temp/netfx.msi"
    -或-

    插入 Visual Studio .NET Windows 组件更新光盘并运行

    <CD Drive>:/dotNetFramework/ dotnetfx.exe /t:c:/temp /c:"msiexec.exe /fvecms c:/temp/netfx.msi"
    是否正确地指定了项目起始页的 URL?扩展名和项目目录是否正确?
    是否正确地设置了 IIS 安全设置?若要验证这一点,请检查“默认 Web 站点”设置。
    检查“默认 Web 站点”的 IIS 安全设置

    从“开始”菜单中,依次选择“程序”和“管理工具”,然后单击“Internet 服务管理器”(Windows 2000) 或“Internet 信息服务”(Windows XP)。
    在“Internet 服务管理器”或“Internet 信息服务”对话框中,单击您的计算机的树控件 (Tree Control)。在“Web 站点”文件夹中,找到“默认 Web 站点”。
    右击“默认 Web 站点”并选择“属性”。
    在“默认 Web 站点属性”窗口中,选择“目录安全性”选项卡并单击“编辑”。
    在“身份验证方法”对话框中,选择“匿名访问”和“集成的 Windows 身份验证”(如果尚未选择的话)。
    单击“确定”以关闭“Internet 服务管理器”或“Internet 信息服务”对话框。
    单击“确定”。
    对于 ATL Server 应用程序,请验证 DEBUG 谓词是否与您的 ISAPI 扩展相关联。
    对于
    ASP.NET 应用程序,请确保应用程序的虚拟文件夹具有在“Internet 服务管理器”或“Internet 信息服务”中设置的“应用程序名称”。
    为 Web 应用程序指定虚拟文件夹

    从“开始”菜单中,依次选择“程序”和“管理工具”,然后单击“Internet 服务管理器”(Windows 2000) 或“Internet 信息服务”(Windows XP)。
    在“Internet 服务管理器”或“Internet 信息服务”对话框中,单击您的计算机的树控件 (Tree Control)。在“Web 站点”文件夹中,找到此 Web 应用程序。
    右击“默认 Web 站点”并选择“属性”。
    在“默认 Web 站点属性”窗口中,选择“目录”选项卡。
    在“应用程序设置”下,单击“创建”。
    应用程序名称即出现在此框中。

    单击“确定”关闭“属性”对话框。
    单击“确定”以关闭“Internet 服务管理器”或“Internet 信息服务”对话框。
    远程服务器上的 Web 应用程序
    如果 Web 应用程序位于远程服务器上,请检查以下问题:

    是否运行了正确的安装程序以便在服务器上安装 ASP.NET/ATL Server 和远程调试器组件?
    您是否是服务器上的“调试器用户”(Debugger Users) 组的成员?您是否具有调试在系统帐户下运行的进程所必需的访问特权?
    根据安全设置的不同,ASP.NET 应用程序可能在 inetinfo.exe(IIS 进程)下运行,也可能在 ASP 辅助进程 aspnet_wp.exe 下运行。默认情况下,aspnet_wp.exe 进程作为 SYSTEM 运行。若要调试在 aspnet_wp.exe 下运行的应用程序,您需要具有管理员特权或为 aspnet_wp.exe 编辑 machine.config 文件,以便 aspnet_wp.exe 在用户帐户下运行。若要调试在 inetinfo.exe 下运行的应用程序,您必须是运行 inetinfo.exe 的计算机上的管理员。

    根据安全设置的不同,ATL Server 应用程序可能在 inetinfo.exe 下运行,也可能在 ATL 辅助进程 dllhost.exe 下运行。若要调试在 inetinfo.exe 下运行的应用程序,您必须是运行 inetinfo.exe 的计算机上的管理员,或者使用公共语言运行库应用程序设置将 dllhost 配置为作为特定用户运行。
    您是否正在使用“终端服务器”尝试调试远程计算机上的 Web 应用程序?在 Windows XP 下,支持使用“终端服务器”对本机 Web 应用程序进行远程调试。而在 Windows 2000 或 Windows NT 下则不支持。
    存储在 Visual SourceSafe 中并使用 FrontPage 服务器扩展的 Web 应用程序
    如果 Web 应用程序存储在 Visual SourceSafe 中并且使用 FrontPage 服务器扩展作为它的 Web 访问模式,请检查以下问题:

    Visual SourceSafe 是否与 FrontPage 服务器/Web 服务器位于同一台计算机上?如果是,则可以使用“集成身份验证”进行调试。(若要检查“集成身份验证”设置,请参阅此过程以检查前面的“默认 Web 站点”的 IIS 安全设置。)
    解决这一问题的另一种方法是将 Web 访问模式从 FrontPage 更改为文件共享 (File Share)。
    将 Web 访问模式更改为文件共享 (File Share)

    在解决方案资源管理器中,右击项目名称,然后从快捷菜单中选择“属性”。
    在“<Project> 属性页”对话框中,打开“通用属性”文件夹,然后选择“Web 设置”。
    在“Web 服务器连接”下,单击“Web 访问模式”,然后从列表框中选择“文件共享”。
    单击“确定”以关闭“<Project> 属性页”对话框。
    手动附加
    如果按照这些疑难解答步骤执行了相应操作,而在开始调试时仍然收到错误信息,则可能需要尝试通过手动附加来调试应用程序。

    手动附加

    启动应用程序而不调试。(从“调试”菜单中,选择“开始执行(不调试)”。)
    附加到适当的 IIS 进程或辅助进程。默认情况下,对于 ATL Server 应用程序为 inetinfo.exe;对于
    ASP.NET 应用程序为 aspnet_wp.exe。使用下面的过程来确定 ASP.NET 或 ATL Server 应用程序在哪个进程下运行。
    检查
    ASP.NET 应用程序在哪个进程下运行

    使用 Visual Studio .NET 或其他文本编辑器打开应用程序的 machine.config 文件。
    找到下面的进程模型属性:
    enable
    如果 enable 设置为 TRUE,则应用程序在 aspnet_wp.exe 下运行(这也是默认设置。)

    如果 enable 设置为 FALSE,则应用程序在 inetinfo.exe 下运行。

    检查 ATL Server 应用程序在哪个进程下运行

    在解决方案资源管理器中,右击项目名称,然后从快捷菜单中选择“属性”。
    在“<Project> 属性页”对话框中,打开“Web 部署”文件夹,然后选择“常规”。
    查看“应用程序保护”设置。
    如果此设置为“低(IIS 进程)”,则应用程序在 inetinfo.exe 下运行。

    如果此设置为“中等(池)”,则应用程序在 dllhost.exe 进程下运行(与其他放入池中的 ATL Server 应用程序相同)。

    如果此设置为“高(独立)”,则应用程序在 dllhost.exe 进程下运行(与其他 ATL Server 应用程序不同)。

    单击“确定”以关闭“<Project> 属性页”对话框。
    请参见
    调试脚本和 Web:错误和疑难解答
    Q:web访问失败
    此项目的默认web访问模式设置为文件共享,但无法从路径“D:/inetpub/bweb”打开“http://localhost/bweb”处的

    项目文件夹,返回的错误是:
        无法打开Web项目“bweb”。文件路径“D:/inetpub/bweb”与URL“http://localhost/bweb”不符。这两者需要映

    射到不同的服务器位置。HTTP错误404:Not Found

    A:1。打开IIS管理器。

    2。右键点击"Default Web Site"并选择Properties.

    3。 点击"Http Header".

    4。 点击"MIME Types".

    5。 点击“New”。

    6。 在Extension中,输入".tmp". (不需要引号)

    7。 在MIME Type中,输入 "Temp". (不需要引号)
    Q:VS.NET调试问题
    A:关于VisualStudio.NET里调试出现的一系列问题,例如没有权限调试Web服务器,不属于Debugger Users组、无法调试等情况,都可以在The VS7 Debugger doesn’t work. What can I do(
    http://blogs.msdn.com/mkpark/articles/86872.aspx)该文上找到答案。 我就碰到过没有权限在Web服务器上调试的情况,后来选中了IE的Internet选项->安全->Intranet->自定义级别->用户验证的“自动使用当前用户和密码登录”才解决无法调试的问题。怎么会想到VS.NET无法调试还要修改IE选项

    - 作者: muskteer 2005年06月13日, 星期一 14:47  回复(0) |  引用(0) 加入博采

    asp.net常用函数

    - 作者: muskteer 2005年06月13日, 星期一 14:28  回复(0) |  引用(0) 加入博采

    C#编码标准--编码习惯
    1. 避免将多个类放在一个文件里面。
      2. 一个文件应该只有一个命名空间,避免将多个命名空间放在同一个文件里面。
      3. 一个文件最好不要超过500行的代码(不包括机器产生的代码)。
      4. 一个方法的代码长度最好不要超过25行。
      5. 避免方法中有超过5个参数的情况。使用结构来传递多个参数。
      6. 每行代码不要超过80个字符。
      7. 不要手工的修改机器产生的代码。
      a) 如果需要编辑机器产生的代码,编辑格式和风格要符合该编码标准。
      b) Use partial classes whenever possible to factor out the maintained portions.
      8. 避免利用注释解释显而易见的代码。
       a) 代码应该可以自解释。好的代码由可读的变量和方法命名因此不需要注释。
      b). Document only operational assumptions, algorithm insights and so on.
      10. 避免使用方法级的文档。
      a) 使用扩展的API文档说明之。
      b) 只有在该方法需要被其他的开发者使用的时候才使用方法级的注释。(在C#中就是///)
      11. 不要硬编码数字的值,总是使用构造函数设定其值。
      12. 只有是自然结构才能直接使用const,比如一个星期的天数。
      13. 避免在只读的变量上使用const。如果想实现只读,可以直接使用readonly。
      public class MyClass
      {
       public readonly int Number;
       public MyClass(int someValue)
       {
       Number = someValue;
       }
       public const int DaysInWeek = 7;
      }
      14. 每个假设必须使用Assert检查
      a) 平均每15行要有一次检查(Assert)
      using System.Diagnostics;
      
      object GetObject()
      {…}
      
      object obj = GetObject();
      Debug.Assert(obj != null);
      15. 代码的每一行都应该通过白盒方式的测试。
      16. 只抛出已经显示处理的异常。
      17. 在捕获(catch)语句的抛出异常子句中(throw),总是抛出原始异常维护原始错误的堆栈分配。
      catch(Exception exception)
      {
       MessageBox.Show(exception.Message);
       throw ; //和throw exception一样。
      }
      18. 避免方法的返回值是错误代码。
      19. 尽量避免定义自定义异常类。
      20. 当需要定义自定义的异常时:
      a) 自定义异常要继承于ApplicationException。
      b) 提供自定义的序列化功能。
      21. 避免在单个程序集里使用多个Main方法。
      22. 只对外公布必要的操作,其他的则为internal。
      23. Avoid friend assemblies, as it increases inter-assembly coupling.
      24. Avoid code that relies on an assembly running from a particular location.
      25. 使应用程序集尽量为最小化代码(EXE客户程序)。使用类库来替换包含的商务逻辑。
      26. 避免给枚举变量提供显式的值。
      //正确方法
      public enum Color
      {
       Red,Green,Blue
      }
      //避免
      public enum Color
      {
       Red = 1,Green = 2,Blue = 3
      }
      27. 避免指定特殊类型的枚举变量。
      //避免
      public enum Color : long
      {
       Red,Green,Blue
      }
      28. 即使if语句只有一句,也要将if语句的内容用大括号扩起来。
      29. 避免使用trinary条件操作符。
      30. 避免在条件语句中调用返回bool值的函数。可以使用局部变量并检查这些局部变量。
      bool IsEverythingOK()
      {…}
      //避免
      if (IsEverythingOK ())
      {…}
      //替换方案
      bool ok = IsEverythingOK();
      if (ok)
      {…}
      31. 总是使用基于0开始的数组。
      32. 在循环中总是显式的初始化引用类型的数组。
      public class MyClass
      {}
      MyClass[] array = new MyClass[100];
      for(int index = 0; index < array.Length; index++)
      {
       array[index] = new MyClass();
      }
      33. 不要提供public 和 protected的成员变量,使用属性代替他们。
      34. 避免在继承中使用new而使用override替换。
      35. 在不是sealed的类中总是将public 和 protected的方法标记成virtual的。
      36. 除非使用interop(COM+ 或其他的dll)代码否则不要使用不安全的代码(unsafe code)。
      37. 避免显示的转换,使用as操作符进行兼容类型的转换。
      Dog dog = new GermanShepherd();
      GermanShepherd shepherd = dog as GermanShepherd;
      if (shepherd != null )
      {…}
      38. 当类成员包括委托的时候
      a) Copy a delegate to a local variable before publishing to avoid concurrency race
      condition.
      b) 在调用委托之前一定要检查它是否为null
      public class MySource
      {
       public event EventHandler MyEvent;
       public void FireEvent()
       {
       EventHandler temp = MyEvent;
       if(temp != null )
       {
       temp(this,EventArgs.Empty);
       }
       }
      }
      39. 不要提供公共的事件成员变量,使用事件访问器替换这些变量。
      public class MySource
      {
       MyDelegate m_SomeEvent ;
       public event MyDelegate SomeEvent
       {
       add
       {
       m_SomeEvent += value;
       }
       remove
       {
       m_SomeEvent -= value;
       }
       }
      }
      40. 使用一个事件帮助类来公布事件的定义。
      41. 总是使用接口。
      42. 类和接口中的方法和属性至少为2:1的比例。
      43. 避免一个接口中只有一个成员。
      44. 尽量使每个接口中包含3-5个成员。
      45. 接口中的成员不应该超过20个。
      a) 实际情况可能限制为12个
      46. 避免接口成员中包含事件。
      47. 避免使用抽象方法而使用接口替换。
      48. 在类层次中显示接口。
      49. 推荐使用显式的接口实现。
      50. 从不假设一个类型兼容一个接口。Defensively query for that interface.
      SomeType obj1;
      IMyInterface obj2;
      /* 假设已有代码初始化过obj1,接下来 */
      obj2 = obj1 as IMyInterface;
      if (obj2 != null)
      {
       obj2.Method1();
      }
      else
      {
       //处理错误
      }
      51. 表现给最终用户的字符串不要使用硬编码而要使用资源文件替换之。
      52. 不要硬编码可能更改的基于配置的字符串,比如连接字符串。
      53. 当需要构建长的字符串的时候,使用StringBuilder不要使用string
      54. 避免在结构里面提供方法。
      a) 建议使用参数化构造函数
      b) 可以重裁操作符
      55. 总是要给静态变量提供静态构造函数。
      56. 能使用早期绑定就不要使用后期绑定。
      57. 使用应用程序的日志和跟踪。
      58. 除非在不完全的switch语句中否则不要使用goto语句。
      59. 在switch语句中总是要有default子句来显示信息(Assert)。
      int number = SomeMethod();
      switch(number)
      {
       case 1:
       Trace.WriteLine("Case 1:");
       break;
       case 2:
       Trace.WriteLine("Case 2:");
       break;
       default :
       Debug.Assert(false);
       break;
      }
      60. 除非在构造函数中调用其他构造函数否则不要使用this指针。
      // 正确使用this的例子
      public class MyClass
      {
       public MyClass(string message )
       {}
       public MyClass() : this("hello")
       {}
      }
      61. 除非你想重写子类中存在名称冲突的成员或者调用基类的构造函数否则不要使用base来访问基类的成员。
      // 正确使用base的例子
      public class Dog
      {
       public Dog(string name)
       {}
       virtual public void Bark( int howLong)
       {}
      }
      public class GermanShepherd : Dog
      {
       public GermanShe pherd(string name): base (name)
       {}
       override public void Bark(int howLong)
       {
       base .Bark(howLong);
       }
      }
      62. 基于模板的时候要实现Dispose()和Finalize()两个方法。
      63. 通常情况下避免有从System.Object转换来和由System.Object转换去的代码,而使用强制转换或者as操作符替换。
      class SomeClass
      {}
      //避免:
      class MyClass
      {
       void SomeMethod(T t)
       {
       object temp = t;
       SomeClass obj = (SomeClass)temp;
       }
      }
      // 正确:
      class MyClass where T : SomeClass
      {
       void SomeMethod(T t)
       {
       SomeClass obj = t;
       }
      }
      64. 在一般情况下不要定影有限制符的接口。接口的限制级别通常可以用强类型来替换之。
      public class Customer
      {…}
      //避免:
      public interface IList where T : Customer
      {…}
      //正确:
      public interface ICustomerList : IList
      {…}
      65. 不确定在接口内的具体方法的限制条件。
      66. 总是选择使用C#内置(一般的generics)的数据结构

    - 作者: muskteer 2005年06月10日, 星期五 12:32  回复(0) |  引用(0) 加入博采

    用DataReader还是DataSet? (转载)
    我经常听到有人问这个问题:“在ASP.NET Web应用程序中我应该用DataReader类还是DataSet类呢?”在很多文章以及新闻组的贴子中我经常看到这样的误解,即认为DataReader(SqlDataReader或OleDbDataReader的缩写)比DataSet好。有时候我也会看到相反的说法。事实上,Microsoft创建了这两个数据存取类是因为它们都是我们所需要的。每个类都有其优点和不足,你可以根据应用环境来选择用哪一个。
      
      本文就两者的选择问题做了很清楚的讲述,可以让你在运用ASP.NET时,在选择DataReader类或DataSet类的方面得到一些指南。在基于客户端的Windows Form应用程序环境下,这些规则可能会改变。我在做这些讲述时,假设你已经用过DataReader和DataSet类了,并对它们很熟悉。
      
      运用DataReader类
      下面就是运用DataReader类的理想条件: 你读取的数据必须是新的,所以在每次需要数据的时候,你都必须从数据库读取。创建一个DataReader类不会消耗很多内存,不过随着负荷的增加,DataSet上的性能也会很快地提高(参考资源中Visual Studio Magazine中的文章)。
      
       你对每行数据的需求很简单。该情况的最好的例子就是简单地将DataReader绑定到一个Web控件,如DataGrid或DropDownList。
      
       你只需要从数据库中以只向前的(forward-only) 、只读的形式来存取XML数据。在这种情况下,你可以用SQLCommand对象的ExcecuteXmlReader()方法来得到一个XmlReader类(相当于XML版的DataReader)。这就需要一个运用FOR XML子句的SQL Server查询,或者一个包含有效XML的ntext字段。
       你计划对数据库进行几个重复的调用,来读取一小块信息。在这种情况下,我们前面提到过的性能数据会有更大的提高。
      
       的确,使DataSet类更强大的许多功能只适用于基于客户端的Windows Form应用程序,比如在多个表之间建立关系的功能。在很多情况下,DataSet类都比DataReader类更有优势,而且在有些情况下,你根本就不能用DataReader类。
      
      运用DataSet类
      在下面的情况,你应该考虑运用DataSet类: 你构建了一个Web service,它运用的数据是你作为返回值读取的数据。因为DataReader类必须保持到数据库的连接,所以它们不能被序列化到XML中,也不能被发送给一个Web service的调用者。
      
       你需要排序或筛选数据。在运用一个DataView对象(呈现为DataTable类的DefaultView属性,它包含一个DataSet类)来排序或筛选数据前,我们先试着用SQL查询(如WHERE和ORDER BY语句)来实现这些功能,并运用更轻量级、更快的DataReader类。然而,有时侯用这种方法是不行的,或者当你需要多次地对数据进行排序或筛选时就不能用DataReader。
      
       针对同一请求,你需要多次遍历数据。你只能在DataReader中循环一次。如果你想将多个ServerControl类绑定到同一个数据集,那么选择DataSet就更好。DataReader类不能被绑定到多个ServerControl类,因为它是只向前读取的。在这种情况下,如果要使用DataReader,必须从数据库读取两次数据。
      
       你需要存储数据,而后续的页面请求可能会用到的这些数据。如果数据只被请求它的专门的人使用,你可以将DataSet类保存在一个Session变量中。如果数据可以被任何人访问,那么你可以将它保存在一个Application变量中,或保存在Cache中(我建议使用后一种方法,因为它支持时间期限和回调(callback))。因为DataReader类必须一直打开对数据库的连接,而且它一次只能保存一行数据,所以它们不能在跨页面请求中被保存。
      
       你需要对一个结果集的每个元素实现特殊的、耗时的功能。例如,如果你从一个数据库读取一列邮政编码,并想通过调用一个Web service来得到每个地区的详细的天气状况信息,那么选择DataSet就会更好。这是因为,当你在用DataReader类时,在关闭DataReader类前,与数据库的连接不会被释放回连接池。在数千页面请求之间潜在的一个很小的延时都会造成Web应用程序的很高的访问量,从而就会消耗完可用的连接。相反,DataSet可以在前端读取所有的数据,并可以马上关闭与数据库的连接,将它返回到连接池,因此其它的页面请求就可以用这个连接了。
      
       你需要在一个两维范例中加载并处理XML数据。DataSet类对于XML很有用,因为你可以将DataView用于XML,对根本的数据进行排序和筛选,就同处理一个数据库结果集一样。然而,需要注意的是在System.Xml名字空间中有很多类,你可以将它们用于更复杂的XML操作。
      
       你的数据源不是一个数据库。虽然OleDbDataReader可以用于任何OLEDB数据提供者(可能指向一个数据库,也可能不指向一个数据库),但DataSet对象可以从一个XML文件直接加载数据,并动态地解释它的schema。DataSet类也可以将XML数据写回一个数据流或一个文件。
      
      从上面的讲述我们就可以看到,DataSet类比DataReader类有更多的功能,这就可以让你在更多的情况下运用它们。但这并不意味着你总是在用DataSet类。你需要在ASP.NET中完成的相当大一部分的任务都属于DataReader的范畴。
      
      尽管如此,毫无疑问,从重要程度或复杂程度的角度来说,DataSet类在很多ASP.NET Web应用程序中都起着很重要的作用。你可以通过明智的缓存来最小化数据库往返,从而降低DataSet类的“性能损害”。DataReader和DataSet都是一个成功的ASP.NET Web应用程序的重要的部件。重要的是,我们需要了解何时、在哪里可以最好的使用它们。

    - 作者: muskteer 2005年06月10日, 星期五 12:31  回复(0) |  引用(0) 加入博采

    C#中一些字符串操作的常用用法
    //获得汉字的区位码
      byte[] array = new byte[2];
      array = System.Text.Encoding.Default.GetBytes("啊");
      
      int i1 = (short)(array[0] - '/0');
      int i2 = (short)(array[1] - '/0');
      
      //unicode解码方式下的汉字码
      array = System.Text.Encoding.Unicode.GetBytes("啊");
      i1 = (short)(array[0] - '/0');
      i2 = (short)(array[1] - '/0');
      
      //unicode反解码为汉字
      string str = "4a55";
      string s1 = str.Substring(0,2);
      string s2 = str.Substring(2,2);
      
      int t1 = Convert.ToInt32(s1,16);
      int t2 = Convert.ToInt32(s2,16);
      
      array[0] = (byte)t1;
      array[1] = (byte)t2;
      
      string s = System.Text.Encoding.Unicode.GetString(array);
      
      //default方式反解码为汉字
      array[0] = (byte)196;
      array[1] = (byte)207;
      s = System.Text.Encoding.Default.GetString(array);
      
      //取字符串长度
      s = "iam方枪枪";
      int len = s.Length;//will output as 6
      byte[] sarr = System.Text.Encoding.Default.GetBytes(s);
      len = sarr.Length;//will output as 3+3*2=9
      
      //字符串相加
      System.Text.StringBuilder sb = new System.Text.StringBuilder("");
      sb.Append("i ");
      sb.Append("am ");
      sb.Append("Firekinger");

     
    展开全文
  • JS正则表达式完整版

    万次阅读 多人点赞 2018-07-17 13:14:13
    第一章 正则表达式字符匹配攻略 1 两种模糊匹配 2. 字符组 3. 量词 4. 多选分支 5. 案例分析 第1章 小结 第二章 正则表达式位置匹配攻略 1. 什么是位置呢? 2. 如何匹配位置呢? 3. 位置的特性 4. 相关...

    目录

    引言

    第一章 正则表达式字符匹配攻略

    1 两种模糊匹配

    2. 字符组

    3. 量词

    4. 多选分支

    5. 案例分析

    第1章 小结

    第二章 正则表达式位置匹配攻略

    1. 什么是位置呢?

    2. 如何匹配位置呢?

    3. 位置的特性

    4. 相关案例

    第二章小结

    第三章 正则表达式括号的作用

    1. 分组和分支结构

    2. 引用分组

    3. 反向引用

    4. 非捕获分组

    5. 相关案例

    第三章小结

    第4章 正则表达式回溯法原理

    1. 没有回溯的匹配

    2. 有回溯的匹配

    3. 常见的回溯形式

    第四章小结

    第5章 正则表达式的拆分

    1. 结构和操作符

    2. 注意要点

    3. 案例分析

    第五章小结

    第6章 正则表达式的构建

    1. 平衡法则

    2. 构建正则前提

    3. 准确性

    4. 效率

    第六章小结

    第七章 正则表达式编程

    1. 正则表达式的四种操作

    2. 相关API注意要点

    3. 真实案例

    第七章小结

    后记

    1. 需要注意的地方

    2. 参考资料

    3. 个人感悟


    引言

    无意中从网上查找到一篇关于正则表达式的好文章,就进行了分享给大家,希望对大家有帮助。

    亲爱的读者朋友,如果你点开了这篇文章,说明你对正则很感兴趣。

    想必你也了解正则的重要性,在我看来正则表达式是衡量程序员水平的一个侧面标准。

    关于正则表达式的教程,网上也有很多,相信你也看了一些。

    与之不同的是,本文的目的是希望所有人认真读完,都有实质性的提高。

    本文内容共有七章,用JavaScript语言完整地讨论了正则表达式的方方面面。

    具体章节如下:

    • 引言
    • 第一章 正则表达式字符匹配攻略
    • 第二章 正则表达式位置匹配攻略
    • 第三章 正则表达式括号的作用
    • 第四章 正则表达式回溯法原理
    • 第五章 正则表达式的拆分
    • 第六章 正则表达式的构建
    • 第七章 正则表达式编程
    • 后记

    下面简单地说说每一章都讨论了什么?

    正则是匹配模式,要么匹配字符,要么匹配位置。

    第1章和第2章以这个角度去讲解了正则的基础。

    在正则中可以使用括号捕获数据,要么在API中进行分组引用,要么在正则里进行反向引用。

    这是第3章的主题,讲解了正则中括号的作用。

    学习正则表达式,是需要了解其匹配原理的。

    第4章,讲解了正则了正则表达式的回溯法原理。另外在第6章里,也讲解了正则的表达式的整体工作原理。

    不仅能看懂别人的正则,还要自己会写正则。

    第5章,是从读的角度,去拆分一个正则表达式,而第6章是从写的角度,去构建一个正则表达式。

    学习正则,是为了在真实世界里应用的。

    第7章讲解了正则的用法,和相关API需要注意的地方。

    如何阅读本文?

    我的建议是阅读两遍。第一遍,不求甚解地快速阅读一遍。阅读过程中遇到的问题不妨记录下来,也许阅读完毕后就能解决很多。然后有时间的话,再带着问题去精读第二遍。

    深呼吸,开始我们的正则表达式旅程吧。我在终点等你。

    第一章 正则表达式字符匹配攻略

    正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

    然而关于正则如何匹配字符的学习,大部分人都觉得这块比较杂乱。

    毕竟元字符太多了,看起来没有系统性,不好记。本章就解决这个问题。

    内容包括:

    1. 两种模糊匹配
    2. 字符组
    3. 量词
    4. 分支结构
    5.  案例分析

    1 两种模糊匹配

    如果正则只有精确匹配是没多大意义的,比如/hello/,也只能匹配字符串中的"hello"这个子串。

    var regex = /hello/;
    
    console.log( regex.test("hello") );
    
    // => true
    
    

    正则表达式之所以强大,是因为其能实现模糊匹配。

    而模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。

    1.1 横向模糊匹配

    横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。

    其实现的方式是使用量词。譬如{m,n},表示连续出现最少m次,最多n次。

    比如/ab{2,5}c/表示匹配这样一个字符串:第一个字符是“a”,接下来是2到5个字符“b”,最后是字符“c”。测试如下:

    var regex = /ab{2,5}c/g;
    
    var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
    
    console.log( string.match(regex) );
    
    // => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
    
    

    注意:案例中用的正则是/ab{2,5}c/g,后面多了g,它是正则的一个修饰符。表示全局匹配,即在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是“所有”,而不只是“第一个”。g是单词global的首字母。

    1.2 纵向模糊匹配

    纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。

    其实现的方式是使用字符组。譬如[abc],表示该字符是可以字符“a”、“b”、“c”中的任何一个。

    比如/a[123]b/可以匹配如下三种字符串:"a1b"、"a2b"、"a3b"。测试如下:

    var regex = /a[123]b/g;
    
    var string = "a0b a1b a2b a3b a4b";
    
    console.log( string.match(regex) );
    
    // => ["a1b", "a2b", "a3b"]

    以上就是本章讲的主体内容,只要掌握横向和纵向模糊匹配,就能解决很大部分正则匹配问题。

    接下来的内容就是展开说了,如果对此都比较熟悉的话,可以跳过,直接看本章案例那节。

    2. 字符组

    需要强调的是,虽叫字符组(字符类),但只是其中一个字符。例如[abc],表示匹配一个字符,它可以是“a”、“b”、“c”之一。

    2.1 范围表示法

    如果字符组里的字符特别多的话,怎么办?可以使用范围表示法。

    比如[123456abcdefGHIJKLM],可以写成[1-6a-fG-M]。用连字符-来省略和简写。

    因为连字符有特殊用途,那么要匹配“a”、“-”、“z”这三者中任意一个字符,该怎么做呢?

    不能写成[a-z],因为其表示小写字符中的任何一个字符。

    可以写成如下的方式:[-az][az-][a\-z]。即要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。

    2.2 排除字符组

    纵向模糊匹配,还有一种情形就是,某位字符可以是任何东西,但就不能是"a"、"b"、"c"。

    此时就是排除字符组(反义字符组)的概念。例如[^abc],表示是一个除"a"、"b"、"c"之外的任意一个字符。字符组的第一位放^(脱字符),表示求反的概念。

    当然,也有相应的范围表示法。

    2.3 常见的简写形式

    有了字符组的概念后,一些常见的符号我们也就理解了。因为它们都是系统自带的简写形式。

    \d就是[0-9]。表示是一位数字。记忆方式:其英文是digit(数字)。

    \D就是[^0-9]。表示除数字外的任意字符。

    \w就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w是word的简写,也称单词字符。

    \W[^0-9a-zA-Z_]。非单词字符。

    \s[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是space character的首字母。

    \S[^ \t\v\n\r\f]。 非空白符。

    .就是[^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号...中的每个点,都可以理解成占位符,表示任何类似的东西。

    如果要匹配任意字符怎么办?可以使用[\d\D][\w\W][\s\S][^]中任何的一个。

    3. 量词

    量词也称重复。掌握{m,n}的准确含义后,只需要记住一些简写形式。

    3.1 简写形式

    {m,} 表示至少出现m次。

    {m} 等价于{m,m},表示出现m次。

    ? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?

    + 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。

    * 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。

    3.2 贪婪匹配和惰性匹配

    看如下的例子:

    var regex = /\d{2,5}/g;
    
    var string = "123 1234 12345 123456";
    
    console.log( string.match(regex) );
    
    // => ["123", "1234", "12345", "12345"]
    
    

    其中正则/\d{2,5}/,表示数字连续出现2到5次。会匹配2位、3位、4位、5位连续数字。

    但是其是贪婪的,它会尽可能多的匹配。你能给我6个,我就要5个。你能给我3个,我就3要个。反正只要在能力范围内,越多越好。

    我们知道有时贪婪不是一件好事(请看文章最后一个例子)。而惰性匹配,就是尽可能少的匹配:

    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"]
    
    

    其中/\d{2,5}?/表示,虽然2到5次都行,当2个就够的时候,就不在往下尝试了。

    通过在量词后面加个问号就能实现惰性匹配,因此所有惰性匹配情形如下:

    {m,n}?
    {m,}?
    ??
    +?
    *?

    对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?

    4. 多选分支

    一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。

    具体形式如下:(p1|p2|p3),其中p1p2p3是子模式,用|(管道符)分隔,表示其中任何之一。

    例如要匹配"good"和"nice"可以使用/good|nice/。测试如下:

    var regex = /good|nice/g;
    
    var string = "good idea, nice try.";
    
    console.log( string.match(regex) );
    
    // => ["good", "nice"]
    
    

    但有个事实我们应该注意,比如我用/good|goodbye/,去匹配"goodbye"字符串时,结果是"good":

    var regex = /good|goodbye/g;
    
    var string = "goodbye";
    
    console.log( string.match(regex) );
    
    // => ["good"]
    
    

    而把正则改成/goodbye|good/,结果是:

    var regex = /goodbye|good/g;
    
    var string = "goodbye";
    
    console.log( string.match(regex) );
    
    // => ["goodbye"]
    
    

    也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

    5. 案例分析

    匹配字符,无非就是字符组、量词和分支结构的组合使用罢了。

    下面找几个例子演练一下(其中,每个正则并不是只有唯一写法):

    5.1 匹配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"]
    
    

    5.2 匹配时间

    以24小时制为例。

    要求匹配:

    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

    如果也要求匹配7:9,也就是说时分前面的0可以省略。

    此时正则变成:

    var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
    
    console.log( regex.test("23:59") );
    
    console.log( regex.test("02:07") );
    
    console.log( regex.test("7:9") );
    
    // => true
    
    // => true
    
    // => true

    5.3 匹配日期

    比如yyyy-mm-dd格式为例。

    要求匹配:

    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

    5.4 window操作系统文件路径

    要求匹配:

    F:\study\javascript\regex\regular expression.pdf

    F:\study\javascript\regex\

    F:\study\javascript

    F:\

    分析:

    整体模式是: 盘符:\文件夹\文件夹\文件夹\

    其中匹配F:\,需要使用[a-zA-Z]:\\,其中盘符不区分大小写,注意\字符需要转义。

    文件名或者文件夹名,不能包含一些特殊字符,此时我们需要排除字符组[^\\:*<>|"?\r\n/]来表示合法字符。另外不能为空名,至少有一个字符,也就是要使用量词+。因此匹配“文件夹\”,可用[^\\:*<>|"?\r\n/]+\\

    另外“文件夹\”,可以出现任意次。也就是([^\\:*<>|"?\r\n/]+\\)*。其中括号提供子表达式。

    路径的最后一部分可以是“文件夹”,没有\,因此需要添加([^\\:*<>|"?\r\n/]+)?

    最后拼接成了一个看起来比较复杂的正则:

    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

    其中,JS中字符串表示\时,也要转义。

    5.5 匹配id

    要求从

    <div id="container" class="main"></div>

    提取出id="container"。

    可能最开始想到的正则是:

    var regex = /id=".*"/
    
    var string = '<div id="container" class="main"></div>';
    
    console.log(string.match(regex)[0]);
    
    // => id="container" class="main"

    因为.是通配符,本身就匹配双引号的,而量词*又是贪婪的,当遇到container后面双引号时,不会停下来,会继续匹配,直到遇到最后一个双引号为止。

    解决之道,可以使用惰性匹配:

    var regex = /id=".*?"/
    
    var string = '<div id="container" class="main"></div>';
    
    console.log(string.match(regex)[0]);
    
    // => id="container"

    当然,这样也会有个问题。效率比较低,因为其匹配原理会涉及到“回溯”这个概念(这里也只是顺便提一下,第四章会详细说明)。可以优化如下:

    var regex = /id="[^"]*"/
    
    var string = '<div id="container" class="main"></div>';
    
    console.log(string.match(regex)[0]);
    
    // => id="container"

    第1章 小结

    字符匹配相关的案例,挺多的,不一而足。

    掌握字符组和量词就能解决大部分常见的情形,也就是说,当你会了这二者,JS正则算是入门了。

    第二章 正则表达式位置匹配攻略

    正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

    然而大部分人学习正则时,对于匹配位置的重视程度没有那么高。

    本章讲讲正则匹配位置的总总。

    内容包括:

    1. 什么是位置?
    2. 如何匹配位置?
    3. 位置的特性
    4. 几个应用实例分析

    1. 什么是位置呢?

    位置是相邻字符之间的位置。比如,下图中箭头所指的地方:

    2. 如何匹配位置呢?

    在ES5中,共有6个锚字符:

    ^ $ \b \B (?=p) (?!p)

    2.1 ^和$

    ^(脱字符)匹配开头,在多行匹配中匹配行开头。

    $(美元符号)匹配结尾,在多行匹配中匹配行结尾。

    比如我们把字符串的开头和结尾用"#"替换(位置可以替换成字符的!):

    var result = "hello".replace(/^|$/g, '#');
    
    console.log(result);
    
    // => "#hello#"

    多行匹配模式时,二者是行的概念,这个需要我们的注意:

    var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
    
    console.log(result);
    
    /*
    
    #I#
    
    #love#
    
    #javascript#
    
    */

    2.2 \b和\B

    \b是单词边界,具体就是\w\W之间的位置,也包括\w^之间的位置,也包括\w$之间的位置。

    比如一个文件名是"[JS] Lesson_01.mp4"中的\b,如下:

    var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
    
    console.log(result);
    
    // => "[#JS#] #Lesson_01#.#mp4#"

    为什么是这样呢?这需要仔细看看。

    首先,我们知道,\w是字符组[0-9a-zA-Z_]的简写形式,即\w是字母数字或者下划线的中任何一个字符。而\W是排除字符组[^0-9a-zA-Z_]的简写形式,即\W\w以外的任何一个字符。

    此时我们可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一个"#",是怎么来的。

    • 第一个"#",两边是"["与"J",是\W\w之间的位置。
    • 第二个"#",两边是"S"与"]",也就是\w\W之间的位置。
    • 第三个"#",两边是空格与"L",也就是\W\w之间的位置。
    • 第四个"#",两边是"1"与".",也就是\w\W之间的位置。
    • 第五个"#",两边是"."与"m",也就是\W\w之间的位置。
    • 第六个"#",其对应的位置是结尾,但其前面的字符"4"是\w,即\w$之间的位置。

    知道了\b的概念后,那么\B也就相对好理解了。

    \B就是\b的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉\b,剩下的都是\B的。

    具体说来就是\w\w\W\W^\W\W$之间的位置。

    比如上面的例子,把所有\B替换成"#":

    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"

    2.3 (?=p)和(?!p)

    (?=p),其中p是一个子模式,即p前面的位置。

    比如(?=l),表示'l'字符前面的位置,例如:

    var result = "hello".replace(/(?=l)/g, '#');
    
    console.log(result);
    
    // => "he#l#lo"

    (?!p)就是(?=p)的反面意思,比如:

    var result = "hello".replace(/(?!l)/g, '#');
    
    
    console.log(result);
    
    // => "#h#ell#o#"

    二者的学名分别是positive lookahead和negative lookahead。

    中文翻译分别是正向先行断言和负向先行断言。

    ES6中,还支持positive lookbehind和negative lookbehind。

    具体是(?<=p)(?<!p)

    也有书上把这四个东西,翻译成环视,即看看右边或看看左边。

    但一般书上,没有很好强调这四者是个位置。

    比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。

    而在本人看来(?=p)就与^一样好理解,就是p前面的那个位置。

    3. 位置的特性

    对于位置的理解,我们可以理解成空字符""。

    比如"hello"字符串等价于如下的形式:

    "hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";

    也等价于:

    "hello" == "" + "" + "hello"

    因此,把/^hello$/写成/^^hello$$$/,是没有任何问题的:

    var result = /^^hello$$$/.test("hello");
    
    console.log(result);
    
    // => true

    甚至可以写成更复杂的:

    var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
    
    console.log(result);
    
    // => true

    也就是说字符之间的位置,可以写成多个。

    把位置理解空字符,是对位置非常有效的理解方式。

    4. 相关案例

    4.1 不匹配任何东西的正则

    让你写个正则不匹配任何东西

    easy,/.^/

    因为此正则要求只有一个字符,但该字符后面是开头。

    4.2 数字的千位分隔符表示法

    比如把"12345678",变成"12,345,678"。

    可见是需要把相应的位置替换成","。

    思路是什么呢?

    4.2.1 弄出最后一个逗号

    使用(?=\d{3}$)就可以做到:

    var result = "12345678".replace(/(?=\d{3}$)/g, ',')
    
    console.log(result);
    
    // => "12345,678"

    4.2.2 弄出所有的逗号

    因为逗号出现的位置,要求后面3个数字一组,也就是\d{3}至少出现一次。

    此时可以使用量词+

    var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
    
    console.log(result);
    
    // => "12,345,678"

    4.2.3 匹配其余案例

    写完正则后,要多验证几个案例,此时我们会发现问题:

    var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')
    
    console.log(result);
    
    // => ",123,456,789"

    因为上面的正则,仅仅表示把从结尾向前数,一但是3的倍数,就把其前面的位置替换成逗号。因此才会出现这个问题。

    怎么解决呢?我们要求匹配的到这个位置不能是开头。

    我们知道匹配开头可以使用^,但要求这个位置不是开头怎么办?

    easy,(?!^),你想到了吗?测试如下:

    var string1 = "12345678",
    
    string2 = "123456789";
    
    reg = /(?!^)(?=(\d{3})+$)/g;
    
    
    var result = string1.replace(reg, ',')
    
    console.log(result);
    
    // => "12,345,678"
    
    
    result = string2.replace(reg, ',');
    
    console.log(result);
    
    // => "123,456,789"

    4.2.4 支持其他形式

    如果要把"12345678 123456789"替换成"12,345,678 123,456,789"。

    此时我们需要修改正则,把里面的开头^和结尾$,替换成\b

    var string = "12345678 123456789",
    
    reg = /(?!\b)(?=(\d{3})+\b)/g;
    
    
    var result = string.replace(reg, ',')
    
    console.log(result);
    
    // => "12,345,678 123,456,789"

    其中(?!\b)怎么理解呢?

    要求当前是一个位置,但不是\b前面的位置,其实(?!\b)说的就是\B

    因此最终正则变成了:/\B(?=(\d{3})+\b)/g

    4.3 验证密码问题

    密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

    此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。

    那么,我们就来挑战一下。看看我们对位置的理解是否深刻。

    4.3.1 简化

    不考虑“但必须至少包括2种字符”这一条件。我们可以容易写出:

    var reg = /^[0-9A-Za-z]{6,12}$/;

    4.3.2 判断是否包含有某一种字符

    假设,要求的必须包含数字,怎么办?此时我们可以使用(?=.*[0-9])来做。

    因此正则变成:

    var reg = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
    
    

    4.3.3 同时包含具体两种字符

    比如同时包含数字和小写字母,可以用(?=.*[0-9])(?=.*[a-z])来做。

    因此正则变成:

    var reg = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;

    4.3.4 解答

    我们可以把原题变成下列几种情况之一:

    1. 同时包含数字和小写字母
    2. 同时包含数字和大写字母
    3. 同时包含小写字母和大写字母
    4. 同时包含数字、小写字母和大写字母

    以上的4种情况是或的关系(实际上,可以不用第4条)。

    最终答案是:

    var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;
    
    console.log( reg.test("1234567") ); // false 全是数字
    
    console.log( reg.test("abcdef") ); // false 全是小写字母
    
    console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
    
    console.log( reg.test("ab23C") ); // false 不足6位
    
    console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
    
    console.log( reg.test("abcdEF234") ); // true 三者都有

    4.3.5 解惑

    上面的正则看起来比较复杂,只要理解了第二步,其余就全部理解了。

    /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/

    对于这个正则,我们只需要弄明白(?=.*[0-9])^即可。

    分开来看就是(?=.*[0-9])^

    表示开头前面还有个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。

    (?=.*[0-9])表示该位置后面的字符匹配.*[0-9],即,有任何多个任意字符,后面再跟个数字。

    翻译成大白话,就是接下来的字符,必须包含个数字。

    4.3.6 另外一种解法

    “至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。

    那么要求“不能全部都是数字”,怎么做呢?(?!p)出马!

    对应的正则是:

    var reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;

    三种“都不能”呢?

    最终答案是:

    var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
    
    console.log( reg.test("1234567") ); // false 全是数字
    
    console.log( reg.test("abcdef") ); // false 全是小写字母
    
    console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
    
    console.log( reg.test("ab23C") ); // false 不足6位
    
    console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
    
    console.log( reg.test("abcdEF234") ); // true 三者都有

    第二章小结

    位置匹配相关的案例,挺多的,不一而足。

    掌握匹配位置的这6个锚字符,给我们解决正则问题一个新工具。

    第三章 正则表达式括号的作用

    不管哪门语言中都有括号。正则表达式也是一门语言,而括号的存在使这门语言更为强大。

    对括号的使用是否得心应手,是衡量对正则的掌握水平的一个侧面标准。

    括号的作用,其实三言两语就能说明白,括号提供了分组,便于我们引用它。

    引用某个分组,会有两种情形:在JavaScript里引用它,在正则表达式里引用它。

    本章内容虽相对简单,但我也要写长点。

    内容包括:

    1. 分组和分支结构
    2. 捕获分组
    3. 反向引用
    4. 非捕获分组
    5. 相关案例

    1. 分组和分支结构

    这二者是括号最直觉的作用,也是最原始的功能。

    1.1 分组

    我们知道/a+/匹配连续出现的“a”,而要匹配连续出现的“ab”时,需要使用/(ab)+/

    其中括号是提供分组功能,使量词+作用于“ab”这个整体,测试如下:

    var regex = /(ab)+/g;
    
    var string = "ababa abbb ababab";
    
    console.log( string.match(regex) );
    
    // => ["abab", "ab", "ababab"]

    1.2 分支结构

    而在多选分支结构(p1|p2)中,此处括号的作用也是不言而喻的,提供了子表达式的所有可能。

    比如,要匹配如下的字符串:

    I love JavaScript

    I love Regular Expression

    可以使用正则:

    var regex = /^I love (JavaScript|Regular Expression)$/;
    
    console.log( regex.test("I love JavaScript") );
    
    console.log( regex.test("I love Regular Expression") );
    
    // => true
    
    // => true

    如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。

    2. 引用分组

    这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。

    而要使用它带来的好处,必须配合使用实现环境的API。

    以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:

    var regex = /\d{4}-\d{2}-\d{2}/;

    然后再修改成括号版的:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;

    为什么要使用这个正则呢?

    2.1 提取数据

    比如提取出年、月、日,可以这么做:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    
    var string = "2017-06-12";
    
    console.log( string.match(regex) );
    
    // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

    match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符gmatch返回的数组格式是不一样的)。

    另外也可以使用正则对象的exec方法:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    
    var string = "2017-06-12";
    
    console.log( regex.exec(string) );
    
    // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

    同时,也可以使用构造函数的全局属性$1$9来获取:

    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"

    2.2 替换

    比如,想把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"

    其中replace中的,第二个参数里用$1$2$3指代相应的分组。等价于如下的形式:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    
    var string = "2017-06-12";
    
    var result = string.replace(regex, function() {
    
    return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
    
    });
    
    console.log(result);
    
    // => "06/12/2017"

    也等价于:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    
    var string = "2017-06-12";
    
    var result = string.replace(regex, function(match, year, month, day) {
    
    return month + "/" + day + "/" + year;
    
    });
    
    console.log(result);
    
    // => "06/12/2017"

    3. 反向引用

    除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

    还是以日期为例。

    比如要写一个正则支持匹配如下三种格式:

    2016-06-12

    2016/06/12

    2016.06.12

    最先可能想到的正则是:

    var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\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) ); // true

    其中/.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。

    假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:

    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,表示的引用之前的那个分组(-|\/|\.)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。

    我们知道了\1的含义后,那么\2\3的概念也就理解了,即分别指代第二个和第三个分组。

    看到这里,此时,恐怕你会有三个问题。

    3.1 括号嵌套怎么办?

    以左括号(开括号)为准。比如:

    var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
    
    var string = "1231231233";
    
    console.log( regex.test(string) ); // true
    
    console.log( RegExp.$1 ); // 123
    
    console.log( RegExp.$2 ); // 1
    
    console.log( RegExp.$3 ); // 23
    
    console.log( RegExp.$4 ); // 3

    我们可以看看这个正则匹配模式:

    • 第一个字符是数字,比如说1,
    • 第二个字符是数字,比如说2,
    • 第三个字符是数字,比如说3,
    • 接下来的是\1,是第一个分组内容,那么看第一个开括号对应的分组是什么,是123,
    • 接下来的是\2,找到第2个开括号,对应的分组,匹配的内容是1,
    • 接下来的是\3,找到第3个开括号,对应的分组,匹配的内容是23,
    • 最后的是\4,找到第3个开括号,对应的分组,匹配的内容是3。

    这个问题,估计仔细看一下,就该明白了。

    3.2 \10表示什么呢?

    另外一个疑问可能是,即\10是表示第10个分组,还是\10呢?

    答案是前者,虽然一个正则里出现\10比较罕见。测试如下:

    var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
    
    var string = "123456789# ######"
    
    console.log( regex.test(string) );
    
    // => true

    3.3 引用不存在的分组会怎样?

    因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如\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\7\8\9") );
    
    console.log( "\1\2\3\4\5\6\7\8\9".split("") );

    chrome浏览器打印的结果:

    4. 非捕获分组

    之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。

    如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p),例如本文第一个例子可以修改为:

    var regex = /(?:ab)+/g;
    
    var string = "ababa abbb ababab";
    
    console.log( string.match(regex) );
    
    // => ["abab", "ab", "ababab"]

    5. 相关案例

    至此括号的作用已经讲完了,总结一句话,就是提供了可供我们使用的分组,如何用就看我们的了。

    5.1 字符串trim方法模拟

    trim方法是去掉字符串的开头和结尾的空白符。有两种思路去做。

    第一种,匹配到开头和结尾的空白符,然后替换成空字符。如:

    function trim(str) {
    
    return str.replace(/^\s+|\s+$/g, '');
    
    }
    
    console.log( trim(" foobar ") );
    
    // => "foobar"

    第二种,匹配整个字符串,然后用引用来提取出相应的数据:

    function trim(str) {
    
    return str.replace(/^\s*(.*?)\s*$/g, "$1");
    
    }
    
    console.log( trim(" foobar ") );
    
    // => "foobar"

    这里使用了惰性匹配*?,不然也会匹配最后一个空格之前的所有空格的。

    当然,前者效率高。

    5.2 将每个单词的首字母转换为大写

    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"

    思路是找到每个单词的首字母,当然这里不使用非捕获匹配也是可以的。

    5.3 驼峰化

    function camelize(str) {
    
    return str.replace(/[-_\s]+(.)?/g, function(match, c) {
    
    return c ? c.toUpperCase() : '';
    
    });
    
    }
    
    console.log( camelize('-moz-transform') );
    
    // => "MozTransform"

    其中分组(.)表示首字母。单词的界定是,前面的字符可以是多个连字符、下划线以及空白符。正则后面的?的目的,是为了应对str尾部的字符可能不是单词字符,比如str是'-moz-transform    '。

    5.4 中划线化

    function dasherize(str) {
    
    return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
    
    }
    
    console.log( dasherize('MozTransform') );
    
    // => "-moz-transform"

    驼峰化的逆过程。

    5.5 html转义和反转义

    // 将HTML特殊字符转换成等值的实体
    
    function escapeHTML(str) {
    
    var escapeChars = {
    
    '¢' : 'cent',
    
    '£' : 'pound',
    
    '¥' : 'yen',
    
    '€': 'euro',
    
    '©' :'copy',
    
    '®' : 'reg',
    
    '<' : '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>') );
    
    // => "&lt;div&gt;Blah blah blah&lt;/div&gt";

    其中使用了用构造函数生成的正则,然后替换相应的格式就行了,这个跟本章没多大关系。

    倒是它的逆过程,使用了括号,以便提供引用,也很简单,如下:

    // 实体字符转换为等值的HTML。
    
    function unescapeHTML(str) {
    
    var htmlEntities = {
    
    nbsp: ' ',
    
    cent: '¢',
    
    pound: '£',
    
    yen: '¥',
    
    euro: '€',
    
    copy: '©',
    
    reg: '®',
    
    lt: '<',
    
    gt: '>',
    
    quot: '"',
    
    amp: '&',
    
    apos: '\''
    
    };
    
    return str.replace(/\&([^;]+);/g, function(match, key) {
    
    if (key in htmlEntities) {
    
    return htmlEntities[key];
    
    }
    
    return match;
    
    });
    
    }
    
    console.log( unescapeHTML('&lt;div&gt;Blah blah blah&lt;/div&gt;') );
    
    // => "<div>Blah blah blah</div>"

    通过key获取相应的分组引用,然后作为对象的键。

    5.6 匹配成对标签

    要求匹配:

    <title>regular expression</title>

    <p>laoyao bye bye</p>

    不匹配:

    <title>wrong!</p>

    匹配一个开标签,可以使用正则<[^>]+>

    匹配一个闭标签,可以使用<\/[^>]+>

    但是要求匹配成对标签,那就需要使用反向引用,如:

    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>

    另外[\d\D]的意思是,这个字符是数字或者不是数字,因此,也就是匹配任意字符的意思。

    第三章小结

    正则中使用括号的例子那可是太多了,不一而足。

    重点理解括号可以提供分组,我们可以提取数据,应该就可以了。

    例子中的代码,基本没做多少分析,相信你都能看懂的。

    第4章 正则表达式回溯法原理

    学习正则表达式,是需要懂点儿匹配原理的。

    而研究匹配原理时,有两个字出现的频率比较高:“回溯”。

    听起来挺高大上,确实还有很多人对此不明不白的。

    因此,本章就简单扼要地说清楚回溯到底是什么东西。

    内容包括:

    1. 没有回溯的匹配
    2. 有回溯的匹配
    3. 常见的回溯形式

    1. 没有回溯的匹配

    假设我们的正则是/ab{1,3}c/,其可视化形式是:

    而当目标字符串是"abbbc"时,就没有所谓的“回溯”。其匹配过程是:

    其中子表达式b{1,3}表示“b”字符连续出现1到3次。

    2. 有回溯的匹配

    如果目标字符串是"abbc",中间就有回溯。

    图中第5步有红颜色,表示匹配不成功。此时b{1,3}已经匹配到了2个字符“b”,准备尝试第三个时,结果发现接下来的字符是“c”。那么就认为b{1,3}就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式c,去匹配字符“c”。当然,此时整个表达式匹配成功了。

    图中的第6步,就是“回溯”。

    你可能对此没有感觉,这里我们再举一个例子。正则是:

    目标字符串是"abbbc",匹配过程是:

    其中第7步和第10步是回溯。第7步与第4步一样,此时b{1,3}匹配了两个"b",而第10步与第3步一样,此时b{1,3}只匹配了一个"b",这也是b{1,3}的最终匹配结果。

    这里再看一个清晰的回溯,正则是:

    目标字符串是:"acd"ef,匹配过程是:

    图中省略了尝试匹配双引号失败的过程。可以看出.*是非常影响效率的。

    为了减少一些不必要的回溯,可以把正则修改为/"[^"]*"/

    3. 常见的回溯形式

    正则表达式匹配字符串的这种方式,有个学名,叫回溯法。

    回 溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再 前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法, 就称作“回溯法”。(copy于百度百科)。

    本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。

    道理,我们是懂了。那么JS中正则表达式会产生回溯的地方都有哪些呢?

    3.1 贪婪量词

    之前的例子都是贪婪量词相关的。比如b{1,3},因为其是贪婪的,尝试可能的顺序是从多往少的方向去尝试。首先会尝试"bbb",然后再看整个正则是否能匹配。不能匹配时,吐出一个"b",即在"bb"的基础上,再继续尝试。如果还不行,再吐出一个,再试。如果还不行呢?只能说明匹配失败了。

    虽然局部匹配是贪婪的,但也要满足整体能正确匹配。否则,皮之不存,毛将焉附?

    此时我们不禁会问,如果当多个贪婪量词挨着存在,并相互有冲突时,此时会是怎样?

    答案是,先下手为强!因为深度优先搜索。测试如下:

    var string = "12345";
    
    var regex = /(\d{1,3})(\d{1,3})/;
    
    console.log( string.match(regex) );
    
    // => ["12345", "123", "45", index: 0, input: "12345"]

    其中,前面的\d{1,3}匹配的是"123",后面的\d{1,3}匹配的是"45"。

    3.2 惰性量词

    惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配,比如:

    var string = "12345";
    
    var regex = /(\d{1,3}?)(\d{1,3})/;
    
    console.log( string.match(regex) );
    
    // => ["1234", "1", "234", index: 0, input: "12345"]

    其中\d{1,3}?只匹配到一个字符"1",而后面的\d{1,3}匹配了"234"。

    虽然惰性量词不贪,但也会有回溯的现象。比如正则是:

    目标字符串是"12345",匹配过程是:

    知道你不贪、很知足,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后\d{1,3}?匹配的字符是"12",是两个数字,而不是一个。

    3.3 分支结构

    我们知道分支也是惰性的,比如/can|candy/,去匹配字符串"candy",得到的结果是"can",因为分支会一个一个尝试,如果前面的满足了,后面就不会再试验了。

    分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分支。这种尝试也可以看成一种回溯。

    比如正则:

    目标字符串是"candy",匹配过程:

    上面第5步,虽然没有回到之前的状态,但仍然回到了分支结构,尝试下一种可能。所以,可以认为它是一种回溯的。

    第四章小结

    其实回溯法,很容易掌握的。

    简单总结就是,正因为有多种可能,所以要一个一个试。直到,要么到某一步时,整体匹配成功了;要么最后都试完后,发现整体匹配不成功。

    1. 贪婪量词“试”的策略是:买衣服砍价。价钱太高了,便宜点,不行,再便宜点。
    2. 惰性量词“试”的策略是:卖东西加价。给少了,再多给点行不,还有点少啊,再给点。
    3. 分支结构“试”的策略是:货比三家。这家不行,换一家吧,还不行,再换。

    既然有回溯的过程,那么匹配效率肯定低一些。相对谁呢?相对那些DFA引擎。

    而JS的正则引擎是NFA,NFA是“非确定型有限自动机”的简写。

    大部分语言中的正则都是NFA,为啥它这么流行呢?

    答:你别看我匹配慢,但是我编译快啊,而且我还有趣哦。

    第5章 正则表达式的拆分

    对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。

    不仅要求自己能解决问题,还要看懂别人的解决方案。代码是这样,正则表达式也是这样。

    正则这门语言跟其他语言有一点不同,它通常就是一大堆字符,而没有所谓“语句”的概念。

    如何能正确地把一大串正则拆分成一块一块的,成为了破解“天书”的关键。

    本章就解决这一问题,内容包括:

    1. 结构和操作符
    2. 注意要点
    3. 案例分析

    1. 结构和操作符

    编程语言一般都有操作符。只要有操作符,就会出现一个问题。当一大堆操作在一起时,先操作谁,又后操作谁呢?为了不产生歧义,就需要语言本身定义好操作顺序,即所谓的优先级。

    而在正则表达式中,操作符都体现在结构中,即由特殊字符和普通字符所代表的一个个特殊整体。

    JS正则表达式中,都有哪些结构呢?

    字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用。

    具体含义简要回顾如下(如懂,可以略去不看):

    字面量,匹配一个具体字符,包括不用转义的和需要转义的。比如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.括号和方括号 (...)(?:...)(?=...)(?!...)[...]
    3.量词限定符 {m}{m,n}{m,}?*+
    4.位置和序列 ^$\元字符一般字符
    5. 管道符(竖杠) |

    上面操作符的优先级从上至下,由高到低。

    这里,我们来分析一个正则:

    /ab?(c|de*)+|fg/

    1. 由于括号的存在,所以,(c|de*)是一个整体结构。
    2. (c|de*)中,注意其中的量词*,因此e*是一个整体结构。
    3. 又因为分支结构“|”优先级最低,因此c是一个整体、而de*是另一个整体。
    4. 同理,整个正则分成了 ab?(...)+fg。而由于分支的原因,又可以分成ab?(c|de*)+fg这两部分。

    希望你没被我绕晕,上面的分析可用其可视化形式描述如下:

    2. 注意要点

    关于结构和操作符,还是有几点需要强调:

    2.1 匹配字符串整体问题

    因为是要匹配整个字符串,我们经常会在正则前后中加上锚字符^$

    比如要匹配目标字符串"abc"或者"bcd"时,如果一不小心,就会写成/^abc|bcd$/

    而位置字符和字符序列优先级要比竖杠高,故其匹配的结构是:

    应该修改成:

    2.2 量词连缀问题

    假设,要匹配这样的字符串:

    1. 每个字符为a、b、c任选其一

    2. 字符串的长度是3的倍数

    此时正则不能想当然地写成/^[abc]{3}+$/,这样会报错,说+前面没什么可重复的:

    此时要修改成:

    2.3 元字符转义问题

    所谓元字符,就是正则中有特殊含义的字符。

    所有结构里,用到的元字符总结如下:

    ^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,

    当匹配上面的字符本身时,可以一律转义:

    var string = "^$.*+?|\\/[]{}=!:-,";
    
    var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
    
    console.log( regex.test(string) );
    
    // => true

    其中string中的\字符也要转义的。

    另外,在string中,也可以把每个字符转义,当然,转义后的结果仍是本身:

    var string = "^$.*+?|\\/[]{}=!:-,";
    
    var string2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";
    
    console.log( string == string2 );
    
    // => true

    现在的问题是,是不是每个字符都需要转义呢?否,看情况。

    2.3.1 字符组中的元字符

    跟字符组相关的元字符有[]^-。因此在会引起歧义的地方进行转义。例如开头的^必须转义,不然会把整个字符组,看成反义字符组。

    var string = "^$.*+?|\\/[]{}=!:-,";
    
    var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;
    
    console.log( string.match(regex) );
    
    // => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]

    2.3.2 匹配“[abc]”和“{3,5}”

    我们知道[abc],是个字符组。如果要匹配字符串"[abc]"时,该怎么办?

    可以写成/\[abc\]/,也可以写成/\[abc]/,测试如下:

    var string = "[abc]";
    
    var regex = /\[abc]/g;
    
    console.log( string.match(regex)[0] );
    
    // => "[abc]"

    只需要在第一个方括号转义即可,因为后面的方括号构不成字符组,正则不会引发歧义,自然不需要转义。

    同理,要匹配字符串"{3,5}",只需要把正则写成/\{3,5}/即可。

    另外,我们知道量词有简写形式{m,},却没有{,n}的情况。虽然后者不构成量词的形式,但此时并不会报错。当然,匹配的字符串也是"{,n}",测试如下:

    var string = "{,3}";
    
    var regex = /{,3}/g;
    
    console.log( string.match(regex)[0] );
    
    // => "{,3}"

    2.3.3 其余情况

    比如= ! : - ,等符号,只要不在特殊结构中,也不需要转义。

    但是,括号需要前后都转义的,如/\(123\)/

    至于剩下的^ $ . * + ? | \ /等字符,只要不在字符组内,都需要转义的。

    3. 案例分析

    接下来分析两个例子,一个简单的,一个复杂的。

    3.1 身份证

    正则表达式是:

    /^(\d{15}|\d{17}[\dxX])$/

    因为竖杠“|”,的优先级最低,所以正则分成了两部分\d{15}\d{17}[\dxX]

    • \d{15}表示15位连续数字。
    • \d{17}[\dxX]表示17位连续数字,最后一位可以是数字可以大小写字母"x"。

    可视化如下:

    3.2 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])(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。

    最后来看一下其可视化形式:

    第五章小结

    掌握正则表达式中的优先级后,再看任何正则应该都有信心分析下去了。

    至于例子,不一而足,没有写太多。

    这里稍微总结一下,竖杠的优先级最低,即最后运算。

    只要知道这一点,就能读懂大部分正则。

    另外关于元字符转义问题,当自己不确定与否时,尽管去转义,总之是不会错的。

    第6章 正则表达式的构建

    对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。

    不仅要看懂别人的解决方案,也要能独立地解决问题。代码是这样,正则表达式也是这样。

    与“读”相比,“写”往往更为重要,这个道理是不言而喻的。

    对正则的运用,首重就是:如何针对问题,构建一个合适的正则表达式?

    本章就解决该问题,内容包括:

    1. 平衡法则
    2. 构建正则前提
    3. 准确性
    4.  效率

    1. 平衡法则

    构建正则有一点非常重要,需要做到下面几点的平衡:

    1. 匹配预期的字符串
    2. 不匹配非预期的字符串
    3. 可读性和可维护性
    4. 效率

    2. 构建正则前提

    2.1 是否能使用正则

    正则太强大了,以至于我们随便遇到一个操作字符串问题时,都会下意识地去想,用正则该怎么做。但我们始终要提醒自己,正则虽然强大,但不是万能的,很多看似很简单的事情,还是做不到的。

    比如匹配这样的字符串:1010010001....

    虽然很有规律,但是只靠正则就是无能为力。

    2.2 是否有必要使用正则

    要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能用字符串API解决的简单问题,就不该正则出马。

    • 比如,从日期中提取出年月日,虽然可以使用正则:
    var string = "2017-07-01"; 
    var regex = /^(\d{4})-(\d{2})-(\d{2})/;
     console.log( string.match(regex) ); 
    // => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]

    其实,可以使用字符串的split方法来做,即可:

    var string = "2017-07-01";
    
    var result = string.split("-");
    
    console.log( result );
    
    // => ["2017", "07", "01"]
    • 比如,判断是否有问号,虽然可以使用:
    var string = "?id=xx&act=search";
    
    console.log( string.search(/\?/) );
    
    // => 0

    其实,可以使用字符串的indexOf方法:

    var string = "?id=xx&act=search";
    
    console.log( string.indexOf("?") );
    
    // => 0
    • 比如获取子串,虽然可以使用正则:
    var string = "JavaScript";
    
    console.log( string.match(/.{4}(.+)/)[1] );
    
    // => Script

    其实,可以直接使用字符串的substringsubstr方法来做:

    var string = "JavaScript";
    
    console.log( string.substring(4) );
    
    // => Script

    2.3 是否有必要构建一个复杂的正则

    比如密码匹配问题,要求密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

    在第2章里,我们写出了正则是:

    /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

    其实可以使用多个小正则来做:

    var regex1 = /^[0-9A-Za-z]{6,12}$/;
    
    var regex2 = /^[0-9]{6,12}$/;
    
    var regex3 = /^[A-Z]{6,12}$/;
    
    var regex4 = /^[a-z]{6,12}$/;
    
    function checkPassword(string) {
    
    if (!regex1.test(string)) return false;
    
    if (regex2.test(string)) return false;
    
    if (regex3.test(string)) return false;
    
    if (regex4.test(string)) return false;
    
    return true;
    
    }

    3. 准确性

    所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。

    这里提到了“预期”二字,那么我们就需要知道目标的组成规则。

    不然没法界定什么样的目标字符串是符合预期的,什么样的又不是符合预期的。

    下面将举例说明,当目标字符串构成比较复杂时,该如何构建正则,并考虑到哪些平衡。

    3.1 匹配固定电话

    比如要匹配如下格式的固定电话号码:

    055188888888

    0551-88888888

    (0551)88888888

    第一步,了解各部分的模式规则。

    上面的电话,总体上分为区号和号码两部分(不考虑分机号和+86的情形)。

    区号是0开头的3到4位数字,对应的正则是:0\d{2,3}

    号码是非0开头的7到8位数字,对应的正则是:[1-9]\d{6,7}

    因此,匹配055188888888的正则是:/^0\d{2,3}[1-9]\d{6,7}$/

    匹配0551-88888888的正则是:/^0\d{2,3}-[1-9]\d{6,7}$/

    匹配(0551)88888888的正则是:/^\(0\d{2,3}\)[1-9]\d{6,7}$/

    第二步,明确形式关系。

    这三者情形是或的关系,可以构建分支:

    /^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

    提取公共部分:

    /^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

    进一步简写:

    /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

    其可视化形式:

    上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。

    上述三种情形是或的关系,这一点很重要,不然很容易按字符是否出现的情形把正则写成:

    /^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/

    虽然也能匹配上述目标字符串,但也会匹配(0551-88888888这样的字符串。当然,这不是我们想要的。

    其实这个正则也不是完美的,因为现实中,并不是每个3位数和4位数都是一个真实的区号。

    这就是一个平衡取舍问题,一般够用就行。

    3.2 匹配浮点数

    要求匹配如下的格式:

    1.23、+1.23、-1.23

    10、+10、-10

    .2、+.2、-.2

    可以看出正则分为三部分。

    符号部分:[+-]

    整数部分:\d+

    小数部分:\.\d+

    上述三个部分,并不是全部都出现。如果此时很容易写出如下的正则:

    /^[+-]?(\d+)?(\.\d+)?$/

    此正则看似没问题,但这个正则也会匹配空字符""。

    因为目标字符串的形式关系不是要求每部分都是可选的。

    要匹配1.23、+1.23、-1.23,可以用/^[+-]?\d+\.\d+$/

    要匹配10、+10、-10,可以用/^[+-]?\d+$/

    要匹配.2、+.2、-.2,可以用/^[+-]?\.\d+$/

    因此整个正则是这三者的或的关系,提取公众部分后是:

    /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

    其可视化形式是:

    如果要求不匹配+.2和-.2,此时正则变成:

    当然,/^[+-]?(\d+\.\d+|\d+|\.\d+)$/也不是完美的,我们也是做了些取舍,比如:

    • 它也会匹配012这样以0开头的整数。如果要求不匹配的话,需要修改整数部分的正则。
    • 一般进行验证操作之前,都要经过trim和判空。那样的话,也许那个错误正则也就够用了。
    • 也可以进一步改写成:/^[+-]?(\d+)?(\.)?\d+$/,这样我们就需要考虑可读性和可维护性了。

    4. 效率

    保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。

    正则表达式的运行分为如下的阶段:

    1. 编译
    2. 设定起始位置
    3. 尝试匹配
    4. 匹配失败的话,从下一位开始继续第3步
    5. 最终结果:匹配成功或失败

    下面以代码为例,来看看这几个阶段都做了什么:

    var regex = /\d+/g;
    
    console.log( regex.lastIndex, regex.exec("123abc34def") );
    
    console.log( regex.lastIndex, regex.exec("123abc34def") );
    
    console.log( regex.lastIndex, regex.exec("123abc34def") );
    
    console.log( regex.lastIndex, regex.exec("123abc34def") );
    
    // => 0 ["123", index: 0, input: "123abc34def"]
    
    // => 3 ["34", index: 6, input: "123abc34def"]
    
    // => 8 null
    
    // => 0 ["123", index: 0, input: "123abc34def"]

    具体分析如下:

    var regex = /\d+/g;

    当生成一个正则时,引擎会对其进行编译。报错与否出现这这个阶段。

    regex.exec("123abc34def")

    当尝试匹配时,需要确定从哪一位置开始匹配。一般情形都是字符串的开头,即第0位。

    但当使用testexec方法,且正则有g时,起始位置是从正则对象的lastIndex属性开始。

    因此第一次exec是从第0位开始,而第二次是从3开始的。

    设定好起始位置后,就开始尝试匹配了。

    比如第一次exec,从0开始,去尝试匹配,并且成功地匹配到3个数字。此时结束时的下标是2,因此下一次的起始位置是3。

    而 第二次,起始下标是3,但第3个字符是“a”,并不是数字。但此时并不会直接报匹配失败,而是移动到下一位置,即从第4位开始继续尝试匹配,但该字符是 b,也不是数字。再移动到下一位,是c仍不行,再移动一位是数字3,此时匹配到了两位数字34。此时,下一次匹配的位置是d的位置,即第8位。

    第三次,是从第8位开始匹配,直到试到最后一位,也没发现匹配的,因此匹配失败,返回null。同时设置lastIndex为0,即,如要再尝试匹配的话,需从头开始。

    从上面可以看出,匹配会出现效率问题,主要出现在上面的第3阶段和第4阶段。

    因此,主要优化手法也是针对这两阶段的。

    4.1 使用具体型字符组来代替通配符,来消除回溯

    而在第三阶段,最大的问题就是回溯。

    例如,匹配双引用号之间的字符。如,匹配字符串123"abc"456中的"abc"。

    如果正则用的是:/".*"/,,会在第3阶段产生4次回溯(粉色表示.*匹配的内容):

    如果正则用的是:/".*?"/,会产生2次回溯(粉色表示.*?匹配的内容):

    因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存。

    此时要使用具体化的字符组,来代替通配符.,以便消除不必要的字符,此时使用正则/"[^"]*"/,即可。

    4.2 使用非捕获型分组

    因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。

    当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。例如:

    /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

    可以修改成:

    /^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/

    4.3 独立出确定字符

    例如/a+/,可以修改成/aa*/

    因为后者能比前者多确定了字符a。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。

    4.4 提取分支公共部分

    比如/^abc|^def/,修改成/^(?:abc|def)/

    又比如/this|that/,修改成/th(?:is|at)/

    这样做,可以减少匹配过程中可消除的重复。

    4.5 减少分支的数量,缩小它们的范围

    /red|read/,可以修改成/rea?d/。此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。

    第六章小结

    本章涉及的内容并不多。

    一般情况下,针对某问题能写出一个满足需求的正则,基本上就可以了。

    至于准确性和效率方面的追求,纯属看个人要求了。我觉得够用就行了。

    关于准确性,本章关心的是最常用的解决思路:

    针对每种情形,分别写出正则,然用分支把他们合并在一起,再提取分支公共部分,就能得到准确的正则。

    至于优化,本章没有为了凑数,去写一大堆。了解了匹配原理,常见的优化手法也就这么几种。

    第七章 正则表达式编程

    什么叫知识,能指导我们实践的东西才叫知识。

    学习一样东西,如果不能使用,最多只能算作纸上谈兵。正则表达式的学习,也不例外。

    掌握了正则表达式的语法后,下一步,也是关键的一步,就是在真实世界中使用它。

    那么如何使用正则表达式呢?有哪些关键的点呢?本章就解决这个问题。

    内容包括:

    1. 正则表达式的四种操作
    2. 相关API注意要点
    3. 真实案例

    1. 正则表达式的四种操作

    正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。

    有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。

    进行任何相关操作,也需要宿主引擎相关API的配合使用。当然,在JS中,相关API也不多。

    1.1 验证

    验证是正则表达式最直接的应用,比如表单验证。

    在说验证之前,先要说清楚匹配是什么概念。

    所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,“匹配”的本质就是“查找”。

    有没有匹配,是不是匹配上,判断是否的操作,即称为“验证”。

    这里举一个例子,来看看如何使用相关API进行验证操作的。

    比如,判断一个字符串中是否有数字。

    • 使用search
    var regex = /\d/;
    
    var string = "abc123";
    
    console.log( !!~string.search(regex) );
    
    // => true
    • 使用test
    var regex = /\d/;
    
    var string = "abc123";
    
    console.log( regex.test(string) );
    
    // => true
    • 使用match
    var regex = /\d/;
    
    var string = "abc123";
    
    console.log( !!string.match(regex) );
    
    // => true
    • 使用exec
    var regex = /\d/;
    
    var string = "abc123";
    
    console.log( !!regex.exec(string) );
    
    // => true
    
    

    其中,最常用的是test

    1.2 切分

    匹配上了,我们就可以进行一些操作,比如切分。

    所谓“切分”,就是把目标字符串,切成一段一段的。在JS中使用的是split

    比如,目标字符串是"html,css,javascript",按逗号来切分:

    var regex = /,/;
    
    var string = "html,css,javascript";
    
    console.log( string.split(regex) );
    
    // => ["html", "css", "javascript"]

    又比如,如下的日期格式:

    2017/06/26

    2017.06.26

    2017-06-26

    可以使用split“切出”年月日:

    var regex = /\D/;
    
    console.log( "2017/06/26".split(regex) );
    
    console.log( "2017.06.26".split(regex) );
    
    console.log( "2017-06-26".split(regex) );
    
    // => ["2017", "06", "26"]
    
    // => ["2017", "06", "26"]
    
    // => ["2017", "06", "26"]

    1.3 提取

    虽然整体匹配上了,但有时需要提取部分匹配的数据。

    此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关API。

    这里,还是以日期为例,提取出年月日。注意下面正则中的括号:

    • match
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    
    var string = "2017-06-26";
    
    console.log( string.match(regex) );
    
    // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
    • exec
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    
    var string = "2017-06-26";
    
    console.log( regex.exec(string) );
    
    // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
    • test
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    
    var string = "2017-06-26";
    
    regex.test(string);
    
    console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
    
    // => "2017" "06" "26"
    • search
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    
    var string = "2017-06-26";
    
    string.search(regex);
    
    console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
    
    // => "2017" "06" "26"
    • replace
    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
    
    var string = "2017-06-26";
    
    var date = [];
    
    string.replace(regex, function(match, year, month, day) {
    
    date.push(year, month, day);
    
    });
    
    console.log(date);
    
    // => ["2017", "06", "26"]

    其中,最常用的是match

    1.4 替换

    找,往往不是目的,通常下一步是为了替换。在JS中,使用replace进行替换。

    比如把日期格式,从yyyy-mm-dd替换成yyyy/mm/dd:

    var string = "2017-06-26";
    
    var today = new Date( string.replace(/-/g, "/") );
    
    console.log( today );
    
    // => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)

    这里只是简单地应用了一下replace。但,replace方法是强大的,是需要重点掌握的。

    2. 相关API注意要点

    从上面可以看出用于正则操作的方法,共有6个,字符串实例4个,正则实例2个:

    String#search

    String#split

    String#match

    String#replace

    RegExp#test

    RegExp#exec

    本文不打算详细地讲解它们的方方面面细节,具体可以参考《JavaScript权威指南》的第三部分。本文重点列出一些容易忽视的地方,以飨读者。

    2.1 search和match的参数问题

    我们知道字符串实例的那4个方法参数都支持正则和字符串。

    searchmatch,会把字符串转换为正则的。

    var string = "2017.06.27";
    
    
    console.log( string.search(".") );
    
    // => 0
    
    //需要修改成下列形式之一
    
    console.log( string.search("\\.") );
    
    console.log( string.search(/\./) );
    
    // => 4
    
    // => 4
    
    
    console.log( string.match(".") );
    
    // => ["2", index: 0, input: "2017.06.27"]
    
    //需要修改成下列形式之一
    
    console.log( string.match("\\.") );
    
    console.log( string.match(/\./) );
    
    // => [".", index: 4, input: "2017.06.27"]
    
    // => [".", index: 4, input: "2017.06.27"]
    
    
    console.log( string.split(".") );
    
    // => ["2017", "06", "27"]
    
    
    console.log( string.replace(".", "/") );
    
    // => "2017/06.27"

    2.2 match返回结果的格式问题

    match返回结果的格式,与正则对象是否有修饰符g有关。

    var string = "2017.06.27";
    
    var regex1 = /\b(\d+)\b/;
    
    var regex2 = /\b(\d+)\b/g;
    
    console.log( string.match(regex1) );
    
    console.log( string.match(regex2) );
    
    // => ["2017", "2017", index: 0, input: "2017.06.27"]
    
    // => ["2017", "06", "27"]

    没有g,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。

    g,返回的是所有匹配的内容。

    当没有匹配时,不管有无g,都返回null

    2.3 exec比match更强大

    当正则没有g时,使用match返回的信息比较多。但是有g后,就没有关键的信息index了。

    exec方法就能解决这个问题,它能接着上一次匹配后继续匹配:

    var string = "2017.06.27";
    
    var regex2 = /\b(\d+)\b/g;
    
    console.log( regex2.exec(string) );
    
    console.log( regex2.lastIndex);
    
    console.log( regex2.exec(string) );
    
    console.log( regex2.lastIndex);
    
    console.log( regex2.exec(string) );
    
    console.log( regex2.lastIndex);
    
    console.log( regex2.exec(string) );
    
    console.log( regex2.lastIndex);
    
    // => ["2017", "2017", index: 0, input: "2017.06.27"]
    
    // => 4
    
    // => ["06", "06", index: 5, input: "2017.06.27"]
    
    // => 7
    
    // => ["27", "27", index: 8, input: "2017.06.27"]
    
    // => 10
    
    // => null
    
    // => 0

    其中正则实例lastIndex属性,表示下一次匹配开始的位置。

    比如第一次匹配了“2017”,开始下标是0,共4个字符,因此这次匹配结束的位置是3,下一次开始匹配的位置是4。

    从上述代码看出,在使用exec时,经常需要配合使用while循环:

    var string = "2017.06.27";
    
    var regex2 = /\b(\d+)\b/g;
    
    var result;
    
    while ( result = regex2.exec(string) ) {
    
    console.log( result, regex2.lastIndex );
    
    }
    
    // => ["2017", "2017", index: 0, input: "2017.06.27"] 4
    
    // => ["06", "06", index: 5, input: "2017.06.27"] 7
    
    // => ["27", "27", index: 8, input: "2017.06.27"] 10

    2.4 修饰符g,对exex和test的影响

    上面提到了正则实例的lastIndex属性,表示尝试匹配时,从字符串的lastIndex位开始去匹配。

    字符串的四个方法,每次匹配时,都是从0开始的,即lastIndex属性始终不变。

    而正则实例的两个方法exectest,当正则是全局匹配时,每一次匹配完成后,都会修改lastIndex。下面让我们以test为例,看看你是否会迷糊:

    var regex = /a/g;
    
    console.log( regex.test("a"), regex.lastIndex );
    
    console.log( regex.test("aba"), regex.lastIndex );
    
    console.log( regex.test("ababc"), regex.lastIndex );
    
    // => true 1
    
    // => true 3
    
    // => false 0

    注意上面代码中的第三次调用test,因为这一次尝试匹配,开始从下标lastIndex即3位置处开始查找,自然就找不到了。

    如果没有g,自然都是从字符串第0个字符处开始尝试匹配:

    var regex = /a/;
    
    console.log( regex.test("a"), regex.lastIndex );
    
    console.log( regex.test("aba"), regex.lastIndex );
    
    console.log( regex.test("ababc"), regex.lastIndex );
    
    // => true 0
    
    // => true 0
    
    // => true 0

    2.5 test整体匹配时需要使用^和$

    这个相对容易理解,因为test是看目标字符串中是否有子串匹配正则,即有部分匹配即可。

    如果,要整体匹配,正则前后需要添加开头和结尾:

    console.log( /123/.test("a123b") );
    
    // => true
    
    console.log( /^123$/.test("a123b") );
    
    // => false
    
    console.log( /^123$/.test("123") );
    
    // => true
    
    

    2.6 split相关注意事项

    split方法看起来不起眼,但要注意的地方有两个的。

    第一,它可以有第二个参数,表示结果数组的最大长度:

    var string = "html,css,javascript";
    
    console.log( string.split(/,/, 2) );
    
    // =>["html", "css"]

    第二,正则使用分组时,结果数组中是包含分隔符的:

    var string = "html,css,javascript";
    
    console.log( string.split(/(,)/) );
    
    // =>["html", ",", "css", ",", "javascript"]

    2.7 replace是很强大的

    《JavaScript权威指南》认为exec是这6个API中最强大的,而我始终认为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,5",变成"222,333,555":

    var result = "2,3,5".replace(/(\d+)/g, "$&$&$&");
    
    console.log(result);
    
    // => "222,333,555"

    再例如,把"2+3=5",变成"2+3=2+3=5=5":

    var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
    
    console.log(result);
    
    // => "2+3=2+3=5=5"

    当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:

    "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"]

    此时我们可以看到replace拿到的信息,并不比exec少。

    2.8 使用构造函数需要注意的问题

    一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多\

    var string = "2017-06-27 2017.06.27 2017/06/27";
    
    var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
    
    console.log( string.match(regex) );
    
    // => ["2017-06-27", "2017.06.27", "2017/06/27"]
    
    
    regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
    
    console.log( string.match(regex) );
    
    // => ["2017-06-27", "2017.06.27", "2017/06/27"]

    2.9 修饰符

    ES5中修饰符,共3个:

    g 全局匹配,即找到所有匹配的,单词是global

    i 忽略字母大小写,单词ingoreCase

    m 多行匹配,只影响^$,二者变成行的概念,即行开头和行结尾。单词是multiline

    当然正则对象也有相应的只读属性:

    var regex = /\w/img;
    
    console.log( regex.global );
    
    console.log( regex.ignoreCase );
    
    console.log( regex.multiline );
    
    // => true
    
    // => true
    
    // => true

    2.10 source属性

    正则实例对象属性,除了globalingnoreCasemultilinelastIndex属性之外,还有一个source属性。

    它什么时候有用呢?

    比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:

    var className = "high";
    
    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
    
    console.log( regex.source )
    
    // => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

    2.11 构造函数属性

    构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是$1,...,$9之外,还有几个不太常用的属性(有兼容性问题):

    RegExp.input 最近一次目标字符串,简写成 RegExp["$_"]
    RegExp.lastMatch 最近一次匹配的文本,简写成 RegExp["$&"]
    RegExp.lastParen 最近一次捕获的文本,简写成 RegExp["$+"]
    RegExp.leftContext 目标字符串中 lastMatch之前的文本,简写成 RegExp["$`"]
    RegExp.rightContext 目标字符串中 lastMatch之后的文本,简写成 RegExp["$'"]

    测试代码如下:

    var regex = /([abc])(\d)/g;
    
    var string = "a1b2c3d4e5";
    
    string.match(regex);
    
    
    console.log( RegExp.input );
    
    console.log( RegExp["$_"]);
    
    // => "a1b2c3d4e5"
    
    
    console.log( RegExp.lastMatch );
    
    console.log( RegExp["$&"] );
    
    // => "c3"
    
    
    console.log( RegExp.lastParen );
    
    console.log( RegExp["$+"] );
    
    // => "3"
    
    
    console.log( RegExp.leftContext );
    
    console.log( RegExp["$`"] );
    
    // => "a1b2"
    
    
    console.log( RegExp.rightContext );
    
    console.log( RegExp["$'"] );
    
    // => "d4e5"

    3. 真实案例

    3.1 使用构造函数生成正则表达式

    我们知道要优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。模拟getElementsByClassName方法,就是很能说明该问题的一个例子。

    这里getElementsByClassName函数的实现思路是:

    • 比如要获取className为"high"的dom元素;
    • 首先生成一个正则:/(^|\s)high(\s|$)/
    • 然后再用其逐一验证页面上的所有dom元素的类名,拿到满足匹配的元素即可。

    代码如下(可以直接复制到本地查看运行效果):

    <p class="high">1111</p>
    
    <p class="high">2222</p>
    
    <p>3333</p>
    
    <script>
    
    function getElementsByClassName(className) {
    
    var elements = document.getElementsByTagName("*");
    
    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
    
    var result = [];
    
    for (var i = 0; i < elements.length; i++) {
    
    var element = elements[i];
    
    if (regex.test(element.className)) {
    
    result.push(element)
    
    }
    
    }
    
    return result;
    
    }
    
    var highs = getElementsByClassName('high');
    
    highs.forEach(function(item) {
    
    item.style.color = 'red';
    
    });
    
    </script>

    3.2 使用字符串保存数据

    一般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串。

    使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:

    var utils = {};
    
    "Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(function(item) {
    
    utils["is" + item] = function(obj) {
    
    return {}.toString.call(obj) == "[object " + item + "]";
    
    };
    
    });
    
    console.log( utils.isArray([1, 2, 3]) );
    
    // => true

    3.3 if语句中使用正则替代&&

    比如,模拟ready函数,即加载完毕后再执行回调(不兼容ie的):

    var readyRE = /complete|loaded|interactive/;
    
    
    function ready(callback) {
    
    if (readyRE.test(document.readyState) && document.body) {
    
    callback()
    
    }
    
    else {
    
    document.addEventListener(
    
    'DOMContentLoaded',
    
    function () {
    
    callback()
    
    },
    
    false
    
    );
    
    }
    
    };
    
    ready(function() {
    
    alert("加载完毕!")
    
    });

    3.4 使用强大的replace

    因为replace方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息来做文章。

    这里以查询字符串(querystring)压缩技术为例,注意下面replace方法中,回调函数根本没有返回任何东西。

    function compress(source) {
    
    var keys = {};
    
    source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) {
    
    keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
    
    });
    
    var result = [];
    
    for (var key in keys) {
    
    result.push(key + '=' + keys[key]);
    
    }
    
    return result.join('&');
    
    }
    
    
    console.log( compress("a=1&b=2&a=3&b=4") );
    
    // => "a=1,3&b=2,4"

    3.5 综合运用

    最后这里再做个简单实用的正则测试器。

    具体效果如下:

    代码,直接贴了,相信你能看得懂:

    <section>
    
    <div id="err"></div>
    
    <input id="regex" placeholder="请输入正则表达式">
    
    <input id="text" placeholder="请输入测试文本">
    
    <button id="run">测试一下</button>
    
    <div id="result"></div>
    
    </section>
    
    <style>
    
    section{
    
    display:flex;
    
    flex-direction:column;
    
    justify-content:space-around;
    
    height:300px;
    
    padding:0 200px;
    
    }
    
    section *{
    
    min-height:30px;
    
    }
    
    #err {
    
    color:red;
    
    }
    
    #result{
    
    line-height:30px;
    
    }
    
    .info {
    
    background:#00c5ff;
    
    padding:2px;
    
    margin:2px;
    
    display:inline-block;
    
    }
    
    </style>
    
    <script>
    
    (function() {
    
    // 获取相应dom元素
    
    var regexInput = document.getElementById("regex");
    
    var textInput = document.getElementById("text");
    
    var runBtn = document.getElementById("run");
    
    var errBox = document.getElementById("err");
    
    var resultBox = document.getElementById("result");
    
    
    // 绑定点击事件
    
    runBtn.onclick = function() {
    
    // 清除错误和结果
    
    errBox.innerHTML = "";
    
    resultBox.innerHTML = "";
    
    
    // 获取正则和文本
    
    var text = textInput.value;
    
    var regex = regexInput.value;
    
    
    if (regex == "") {
    
    errBox.innerHTML = "请输入正则表达式";
    
    } else if (text == "") {
    
    errBox.innerHTML = "请输入测试文本";
    
    } else {
    
    regex = createRegex(regex);
    
    if (!regex) return;
    
    var result, results = [];
    
    
    // 没有修饰符g的话,会死循环
    
    if (regex.global) {
    
    while(result = regex.exec(text)) {
    
    results.push(result);
    
    }
    
    } else {
    
    results.push(regex.exec(text));
    
    }
    
    
    if (results[0] == null) {
    
    resultBox.innerHTML = "匹配到0个结果";
    
    return;
    
    }
    
    
    // 倒序是有必要的
    
    for (var i = results.length - 1; i >= 0; i--) {
    
    var result = results[i];
    
    var match = result[0];
    
    var prefix = text.substr(0, result.index);
    
    var suffix = text.substr(result.index + match.length);
    
    text = prefix
    
    + '<span class="info">'
    
    + match
    
    + '</span>'
    
    + suffix;
    
    }
    
    resultBox.innerHTML = "匹配到" + results.length + "个结果:<br>" + text;
    
    }
    
    };
    
    
    // 生成正则表达式,核心函数
    
    function createRegex(regex) {
    
    try {
    
    if (regex[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) {
    
    errBox.innerHTML = "无效的正则表达式";
    
    return false;
    
    }
    
    }
    
    })();
    
    </script>

    第七章小结

    相关API的注意点,本章基本上算是一网打尽了。

    至于文中的例子,都是点睛之笔,没有详细解析。如有理解不透的,建议自己敲一敲。

    后记

    其实本文首发于:正则表达式系列总结 - 知乎专栏

    原文是一个系列。一直等到老姚成为掘金的专栏作者,经过仔细考虑,在掘金平台没有采用系列形式,而是合成为了一篇文章。这样既便于读者阅读,最起码能一气呵成地阅读。同时也便于作者统一回复留言。

    文章要结束了,最后还要有几点说明。

    1. 需要注意的地方

    本文主要讨论的是JavaScript的正则表达式,更精确地说是ES5的正则表达式。

    JavaScript 的正则表达式引擎是传统型NFA的,因此本系列的讨论是适合任何一门正则引擎是传统型NFA的编程语言。当然,市面上大部分语言的正则引擎都是这种的。而 JS里正则涉及到的所有语法要点,是这种引擎支持的核心子集。也就是说,要学正则表达式,不妨以JS正则为出发点。

    2. 参考资料

    当然本文不是无本之末。主要参考的是几本书籍。

    以下书籍中核心章节都认真阅读过,甚至阅读多遍。

    《JavaScript权威指南》,看完本系列,再去看书中的第10章,你就知道了什么叫字字珠玑。

    《精通正则表达式》,权威且比较杂乱,我阅读的第一本正则表达式书籍。

    《正则表达式必知必会》,这是我看的第二本正则,看完后,确定自己算是入门了。

    《正则指引》,《精通正则表达式》的译者写的,相对清晰。

    《正则表达式入门》,我看的是英文版的,对于已经入门的我,基本没多少收获了。

    《正则表达式经典实例》,除了第3章,比较杂外,也有收获,以实例为主导的一本书。

    《JavaScript Regular Expressions》,为数不多转讲JS正则的。页数不多,也有收获。

    《高性能JavaScript 》第5章,我看的是英文版的。第5章,讲了回溯和优化。

    《JavaScript忍者秘籍》第7章,大概讲了一下正则的用法,几个例子还不错。

    《JavaScript高级程序设计》第5.4节,比较简短的介绍。

    使用的工具:

    Regulex,一款可视化工具
    ProcessOn - 免费在线作图,实时协作
    LICEcap – 灵活好用,GIF 屏幕录制工具

    3. 个人感悟

    要多写文章的

    首 先,我十分感谢读者。读者能在信息泛滥的网络里,点击我的文章进来瞧两眼,这都是对其注意力的消费。更何况,还有很多童鞋都认真读了,甚至给我挑毛病,这 都是对我的帮助。不知有多少童鞋是从头读到这里的,不妨留言打卡,让我知道你是用心的读者,而并非简简单单地收藏一下,然后就再也不曾看过了。

    说到要写文章,其目的是以教为学。看似为了教,其实是为了学能教会别人才算自己真正学会了,最起码形成了文字,通过了自己的语言逻辑这一关。如果还能有人指出你的错误认知,那样收获就更大了,何乐而不为呢?

    很多书中都提到类似的观点,例如《知道做到》《好好学习》《与时间做朋友》《暗时间》等。

    以教为学的其他手段

    当然,以教为学的手段还有很多,比如翻译一本书。我私下已经翻译了好几本(窃喜^_^)。

    可 以从薄点的书籍开始,比如100页左右的。基本上使用有道就可以了,也不用要求自己一词一句的翻译,能用自己的话说明白就行了。说到这里,不得不提起,我 们的阮一峰大神,在我看来,他就是成功地应用这种模式的。看完外文的文章,理解明白了,用自己的话说一说,再形成自己的简练风格。

    恐怕你可能说自己的英文水平不够,没信心尝试。相信我,熟悉了常用词汇(比如literal翻译成字面量)后,配合有道翻译,薄点的书,一天翻译一章是没问题的。当然前提是你懂相关领域,不然是没办法意译的。

    最后一种以教为学的手段是,写一本书。写文章是基础,文章多了,自然而言就可以写成一本书。当然,写书强调的是整体架构,所以文章最好成体系。

    你看看那些国内专业书籍的作者,一般都事先翻译过几本书的。最起码在前端领域,我就看到了好几位是这么干的。翻译明白了,学会了,用自己的角度去弄出一本书还是相对很容易的。

    虽然,本人并未曾写过书,但上述方法,我始终相信是可行的。

    最后,我们该想到,陆游诗人对前端界做出的最大贡献是:

    纸上得来终觉浅,绝知此事要躬行。

    本文完。

    展开全文
  • 正则表达式

    2014-12-03 14:51:39
    在JavaScript中,正则表达式是由一个RegExp对象表示的.当然,可以使用一个RegExp()构造函数来创建RegExp对象, 也可以用JavaScript 1.2中的新添加的一个特殊语法来创建RegExp对象.就像字符串直接量被定义为包含在引号...
  • JavaScript正则表达式详解

    千次阅读 多人点赞 2019-06-13 18:49:46
    第一章 正则表达式字符匹配攻略 第二章正则表达式位置匹配攻略 第三章 正则表达式括号的作用 第四章 正则表达式回溯法原理 第五章 正则表达式的拆分 第六章 正则表达式的构建 第七章 正则表达式编程 后记 ...
    • 第一章 正则表达式字符匹配攻略
    • 第二章 正则表达式位置匹配攻略
    • 第三章 正则表达式括号的作用
    • 第四章 正则表达式回溯法原理
    • 第五章 正则表达式的拆分
    • 第六章 正则表达式的构建
    • 第七章 正则表达式编程
    • 后记

    下面简单地说说每一章都讨论了什么?

    正则是匹配模式,要么匹配字符,要么匹配位置。

    第1章和第2章以这个角度去讲解了正则的基础。

    在正则中可以使用括号捕获数据,要么在API中进行分组引用,要么在正则里进行反向引用。

    这是第3章的主题,讲解了正则中括号的作用。

    学习正则表达式,是需要了解其匹配原理的。

    第4章,讲解了正则了正则表达式的回溯法原理。另外在第6章里,也讲解了正则的表达式的整体工作原理。

    不仅能看懂别人的正则,还要自己会写正则。

    第5章,是从读的角度,去拆分一个正则表达式,而第6章是从写的角度,去构建一个正则表达式。

    学习正则,是为了在真实世界里应用的。

    第7章讲解了正则的用法,和相关API需要注意的地方。

    如何阅读本文?

    我的建议是阅读两遍。第一遍,不求甚解地快速阅读一遍。阅读过程中遇到的问题不妨记录下来,也许阅读完毕后就能解决很多。然后有时间的话,再带着问题去精读第二遍。

    深呼吸,开始我们的正则表达式旅程吧。我在终点等你。

     

    第一章 正则表达式字符匹配攻略

    正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

    然而关于正则如何匹配字符的学习,大部分人都觉得这块比较杂乱。

    毕竟元字符太多了,看起来没有系统性,不好记。本章就解决这个问题。

    内容包括:

    1. 两种模糊匹配
    2. 字符组
    3. 量词
    4. 分支结构
    5.  案例分析

    1 两种模糊匹配

    如果正则只有精确匹配是没多大意义的,比如/hello/,也只能匹配字符串中的"hello"这个子串。

    var regex = /hello/;console.log( regex.test("hello") ); // => true复制代码

    正则表达式之所以强大,是因为其能实现模糊匹配。

    而模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。

    1.1 横向模糊匹配

    横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。

    其实现的方式是使用量词。譬如{m,n},表示连续出现最少m次,最多n次。

    比如/ab{2,5}c/表示匹配这样一个字符串:第一个字符是“a”,接下来是2到5个字符“b”,最后是字符“c”。测试如下:

    var regex = /ab{2,5}c/g;var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";console.log( string.match(regex) ); // => ["abbc", "abbbc", "abbbbc", "abbbbbc"]复制代码

    注意:案例中用的正则是/ab{2,5}c/g,后面多了g,它是正则的一个修饰符。表示全局匹配,即在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是“所有”,而不只是“第一个”。g是单词global的首字母。

    1.2 纵向模糊匹配

    纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。

    其实现的方式是使用字符组。譬如[abc],表示该字符是可以字符“a”、“b”、“c”中的任何一个。

    比如/a[123]b/可以匹配如下三种字符串:"a1b"、"a2b"、"a3b"。测试如下:

    var regex = /a[123]b/g;var string = "a0b a1b a2b a3b a4b";console.log( string.match(regex) ); // => ["a1b", "a2b", "a3b"]复制代码

    以上就是本章讲的主体内容,只要掌握横向和纵向模糊匹配,就能解决很大部分正则匹配问题。

    接下来的内容就是展开说了,如果对此都比较熟悉的话,可以跳过,直接看本章案例那节。

    2. 字符组

    需要强调的是,虽叫字符组(字符类),但只是其中一个字符。例如[abc],表示匹配一个字符,它可以是“a”、“b”、“c”之一。

    2.1 范围表示法

    如果字符组里的字符特别多的话,怎么办?可以使用范围表示法。

    比如[123456abcdefGHIJKLM],可以写成[1-6a-fG-M]。用连字符-来省略和简写。

    因为连字符有特殊用途,那么要匹配“a”、“-”、“z”这三者中任意一个字符,该怎么做呢?

    不能写成[a-z],因为其表示小写字符中的任何一个字符。

    可以写成如下的方式:[-az]或[az-]或[a\-z]。即要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。

    2.2 排除字符组

    纵向模糊匹配,还有一种情形就是,某位字符可以是任何东西,但就不能是"a"、"b"、"c"。

    此时就是排除字符组(反义字符组)的概念。例如[^abc],表示是一个除"a"、"b"、"c"之外的任意一个字符。字符组的第一位放^(脱字符),表示求反的概念。

    当然,也有相应的范围表示法。

    2.3 常见的简写形式

    有了字符组的概念后,一些常见的符号我们也就理解了。因为它们都是系统自带的简写形式。

    \d就是[0-9]。表示是一位数字。记忆方式:其英文是digit(数字)。

    \D就是[^0-9]。表示除数字外的任意字符。

    \w就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w是word的简写,也称单词字符。

    \W是[^0-9a-zA-Z_]。非单词字符。

    \s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是space character的首字母。

    \S是[^ \t\v\n\r\f]。 非空白符。

    .就是[^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号...中的每个点,都可以理解成占位符,表示任何类似的东西。

    如果要匹配任意字符怎么办?可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一个。

    3. 量词

    量词也称重复。掌握{m,n}的准确含义后,只需要记住一些简写形式。

    3.1 简写形式

    {m,} 表示至少出现m次。

    {m} 等价于{m,m},表示出现m次。

    ? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?

    + 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。

    * 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。

    3.2 贪婪匹配和惰性匹配

    看如下的例子:

    var regex = /\d{2,5}/g;var string = "123 1234 12345 123456";console.log( string.match(regex) ); // => ["123", "1234", "12345", "12345"]复制代码

    其中正则/\d{2,5}/,表示数字连续出现2到5次。会匹配2位、3位、4位、5位连续数字。

    但是其是贪婪的,它会尽可能多的匹配。你能给我6个,我就要5个。你能给我3个,我就3要个。反正只要在能力范围内,越多越好。

    我们知道有时贪婪不是一件好事(请看文章最后一个例子)。而惰性匹配,就是尽可能少的匹配:

    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"]复制代码

    其中/\d{2,5}?/表示,虽然2到5次都行,当2个就够的时候,就不在往下尝试了。

    通过在量词后面加个问号就能实现惰性匹配,因此所有惰性匹配情形如下:

    {m,n}? 
    {m,}?
    ??
    +?
    *?

    对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?

    4. 多选分支

    一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。

    具体形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一。

    例如要匹配"good"和"nice"可以使用/good|nice/。测试如下:

    var regex = /good|nice/g;var string = "good idea, nice try.";console.log( string.match(regex) ); // => ["good", "nice"]复制代码

    但有个事实我们应该注意,比如我用/good|goodbye/,去匹配"goodbye"字符串时,结果是"good":

    var regex = /good|goodbye/g;var string = "goodbye";console.log( string.match(regex) ); // => ["good"]复制代码

    而把正则改成/goodbye|good/,结果是:

    var regex = /goodbye|good/g;var string = "goodbye";console.log( string.match(regex) ); // => ["goodbye"]复制代码

    也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

    5. 案例分析

    匹配字符,无非就是字符组、量词和分支结构的组合使用罢了。

    下面找几个例子演练一下(其中,每个正则并不是只有唯一写法):

    5.1 匹配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"]复制代码

    5.2 匹配时间

    以24小时制为例。

    要求匹配:

    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复制代码

    如果也要求匹配7:9,也就是说时分前面的0可以省略。

    此时正则变成:

    var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;console.log( regex.test("23:59") ); console.log( regex.test("02:07") ); console.log( regex.test("7:9") ); // => true// => true// => true复制代码

    5.3 匹配日期

    比如yyyy-mm-dd格式为例。

    要求匹配:

    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复制代码

    5.4 window操作系统文件路径

    要求匹配:

    F:\study\javascript\regex\regular expression.pdf

    F:\study\javascript\regex\

    F:\study\javascript

    F:\

    分析:

    整体模式是: 盘符:\文件夹\文件夹\文件夹\

    其中匹配F:\,需要使用[a-zA-Z]:\\,其中盘符不区分大小写,注意\字符需要转义。

    文件名或者文件夹名,不能包含一些特殊字符,此时我们需要排除字符组[^\\:*<>|"?\r\n/]来表示合法字符。另外不能为空名,至少有一个字符,也就是要使用量词+。因此匹配“文件夹\”,可用[^\\:*<>|"?\r\n/]+\\。

    另外“文件夹\”,可以出现任意次。也就是([^\\:*<>|"?\r\n/]+\\)*。其中括号提供子表达式。

    路径的最后一部分可以是“文件夹”,没有\,因此需要添加([^\\:*<>|"?\r\n/]+)?。

    最后拼接成了一个看起来比较复杂的正则:

    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复制代码

    其中,JS中字符串表示\时,也要转义。

    5.5 匹配id

    要求从

    <div id="container" class="main"></div>

    提取出id="container"。

    可能最开始想到的正则是:

    var regex = /id=".*"/var string = '<div id="container" class="main"></div>';console.log(string.match(regex)[0]); // => id="container" class="main"复制代码

    因为.是通配符,本身就匹配双引号的,而量词*又是贪婪的,当遇到container后面双引号时,不会停下来,会继续匹配,直到遇到最后一个双引号为止。

    解决之道,可以使用惰性匹配:

    var regex = /id=".*?"/var string = '<div id="container" class="main"></div>';console.log(string.match(regex)[0]); // => id="container"复制代码

    当然,这样也会有个问题。效率比较低,因为其匹配原理会涉及到“回溯”这个概念(这里也只是顺便提一下,第四章会详细说明)。可以优化如下:

    var regex = /id="[^"]*"/var string = '<div id="container" class="main"></div>';console.log(string.match(regex)[0]); // => id="container"复制代码

    第1章 小结

    字符匹配相关的案例,挺多的,不一而足。

    掌握字符组和量词就能解决大部分常见的情形,也就是说,当你会了这二者,JS正则算是入门了。

     

    第二章 正则表达式位置匹配攻略

    正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

    然而大部分人学习正则时,对于匹配位置的重视程度没有那么高。

    本章讲讲正则匹配位置的总总。

    内容包括:

    1. 什么是位置?
    2. 如何匹配位置?
    3. 位置的特性
    4. 几个应用实例分析

    1. 什么是位置呢?

    位置是相邻字符之间的位置。比如,下图中箭头所指的地方:

     

    2. 如何匹配位置呢?

    在ES5中,共有6个锚字符:

    ^ $ \b \B (?=p) (?!p)

    2.1 ^和$

    ^(脱字符)匹配开头,在多行匹配中匹配行开头。

    $(美元符号)匹配结尾,在多行匹配中匹配行结尾。

    比如我们把字符串的开头和结尾用"#"替换(位置可以替换成字符的!):

    var result = "hello".replace(/^|$/g, '#');console.log(result); // => "#hello#"复制代码

    多行匹配模式时,二者是行的概念,这个需要我们的注意:

    var result = "I\nlove\njavascript".replace(/^|$/gm, '#');console.log(result);/*

    #I#

    #love#

    #javascript#

    */复制代码

    2.2 \b和\B

    \b是单词边界,具体就是\w和\W之间的位置,也包括\w和^之间的位置,也包括\w和$之间的位置。

    比如一个文件名是"[JS] Lesson_01.mp4"中的\b,如下:

    var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');console.log(result); // => "[#JS#] #Lesson_01#.#mp4#"复制代码

    为什么是这样呢?这需要仔细看看。

    首先,我们知道,\w是字符组[0-9a-zA-Z_]的简写形式,即\w是字母数字或者下划线的中任何一个字符。而\W是排除字符组[^0-9a-zA-Z_]的简写形式,即\W是\w以外的任何一个字符。

    此时我们可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一个"#",是怎么来的。

    • 第一个"#",两边是"["与"J",是\W和\w之间的位置。
    • 第二个"#",两边是"S"与"]",也就是\w和\W之间的位置。
    • 第三个"#",两边是空格与"L",也就是\W和\w之间的位置。
    • 第四个"#",两边是"1"与".",也就是\w和\W之间的位置。
    • 第五个"#",两边是"."与"m",也就是\W和\w之间的位置。
    • 第六个"#",其对应的位置是结尾,但其前面的字符"4"是\w,即\w和$之间的位置。

    知道了\b的概念后,那么\B也就相对好理解了。

    \B就是\b的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉\b,剩下的都是\B的。

    具体说来就是\w与\w、\W与\W、^与\W,\W与$之间的位置。

    比如上面的例子,把所有\B替换成"#":

    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"复制代码

    2.3 (?=p)和(?!p)

    (?=p),其中p是一个子模式,即p前面的位置。

    比如(?=l),表示'l'字符前面的位置,例如:

    var result = "hello".replace(/(?=l)/g, '#');console.log(result); // => "he#l#lo"复制代码

    而(?!p)就是(?=p)的反面意思,比如:

    var result = "hello".replace(/(?!l)/g, '#');

    console.log(result); // => "#h#ell#o#"复制代码

    二者的学名分别是positive lookahead和negative lookahead。

    中文翻译分别是正向先行断言和负向先行断言。

    ES6中,还支持positive lookbehind和negative lookbehind。

    具体是(?<=p)和(?<!p)。

    也有书上把这四个东西,翻译成环视,即看看右边或看看左边。

    但一般书上,没有很好强调这四者是个位置。

    比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。

    而在本人看来(?=p)就与^一样好理解,就是p前面的那个位置。

    3. 位置的特性

    对于位置的理解,我们可以理解成空字符""。

    比如"hello"字符串等价于如下的形式:

    "hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";复制代码

    也等价于:

    "hello" == "" + "" + "hello"复制代码

    因此,把/^hello$/写成/^^hello$$$/,是没有任何问题的:

    var result = /^^hello$$$/.test("hello");console.log(result); // => true复制代码

    甚至可以写成更复杂的:

    var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");console.log(result); // => true复制代码

    也就是说字符之间的位置,可以写成多个。

    把位置理解空字符,是对位置非常有效的理解方式。

    4. 相关案例

    4.1 不匹配任何东西的正则

    让你写个正则不匹配任何东西

    easy,/.^/

    因为此正则要求只有一个字符,但该字符后面是开头。

    4.2 数字的千位分隔符表示法

    比如把"12345678",变成"12,345,678"。

    可见是需要把相应的位置替换成","。

    思路是什么呢?

    4.2.1 弄出最后一个逗号

    使用(?=\d{3}$)就可以做到:

    var result = "12345678".replace(/(?=\d{3}$)/g, ',')console.log(result); // => "12345,678"复制代码

    4.2.2 弄出所有的逗号

    因为逗号出现的位置,要求后面3个数字一组,也就是\d{3}至少出现一次。

    此时可以使用量词+:

    var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')console.log(result); // => "12,345,678"复制代码

    4.2.3 匹配其余案例

    写完正则后,要多验证几个案例,此时我们会发现问题:

    var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')console.log(result); // => ",123,456,789"复制代码

    因为上面的正则,仅仅表示把从结尾向前数,一但是3的倍数,就把其前面的位置替换成逗号。因此才会出现这个问题。

    怎么解决呢?我们要求匹配的到这个位置不能是开头。

    我们知道匹配开头可以使用^,但要求这个位置不是开头怎么办?

    easy,(?!^),你想到了吗?测试如下:

    var string1 = "12345678",

    string2 = "123456789";

    reg = /(?!^)(?=(\d{3})+$)/g;

    var result = string1.replace(reg, ',')console.log(result); // => "12,345,678"

     

    result = string2.replace(reg, ',');console.log(result); // => "123,456,789"复制代码

    4.2.4 支持其他形式

    如果要把"12345678 123456789"替换成"12,345,678 123,456,789"。

    此时我们需要修改正则,把里面的开头^和结尾$,替换成\b:

    var string = "12345678 123456789",

    reg = /(?!\b)(?=(\d{3})+\b)/g;

    var result = string.replace(reg, ',')console.log(result); // => "12,345,678 123,456,789"复制代码

    其中(?!\b)怎么理解呢?

    要求当前是一个位置,但不是\b前面的位置,其实(?!\b)说的就是\B。

    因此最终正则变成了:/\B(?=(\d{3})+\b)/g。

    4.3 验证密码问题

    密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

    此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。

    那么,我们就来挑战一下。看看我们对位置的理解是否深刻。

    4.3.1 简化

    不考虑“但必须至少包括2种字符”这一条件。我们可以容易写出:

    var reg = /^[0-9A-Za-z]{6,12}$/;复制代码

    4.3.2 判断是否包含有某一种字符

    假设,要求的必须包含数字,怎么办?此时我们可以使用(?=.*[0-9])来做。

    因此正则变成:

    var reg = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;复制代码

    4.3.3 同时包含具体两种字符

    比如同时包含数字和小写字母,可以用(?=.*[0-9])(?=.*[a-z])来做。

    因此正则变成:

    var reg = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;复制代码

    4.3.4 解答

    我们可以把原题变成下列几种情况之一:

    1. 同时包含数字和小写字母
    2. 同时包含数字和大写字母
    3. 同时包含小写字母和大写字母
    4. 同时包含数字、小写字母和大写字母

    以上的4种情况是或的关系(实际上,可以不用第4条)。

    最终答案是:

    var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;console.log( reg.test("1234567") ); // false 全是数字console.log( reg.test("abcdef") ); // false 全是小写字母console.log( reg.test("ABCDEFGH") ); // false 全是大写字母console.log( reg.test("ab23C") ); // false 不足6位console.log( reg.test("ABCDEF234") ); // true 大写字母和数字console.log( reg.test("abcdEF234") ); // true 三者都有复制代码

    4.3.5 解惑

    上面的正则看起来比较复杂,只要理解了第二步,其余就全部理解了。

    /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/

    对于这个正则,我们只需要弄明白(?=.*[0-9])^即可。

    分开来看就是(?=.*[0-9])和^。

    表示开头前面还有个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。

    (?=.*[0-9])表示该位置后面的字符匹配.*[0-9],即,有任何多个任意字符,后面再跟个数字。

    翻译成大白话,就是接下来的字符,必须包含个数字。

    4.3.6 另外一种解法

    “至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。

    那么要求“不能全部都是数字”,怎么做呢?(?!p)出马!

    对应的正则是:

    var reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;复制代码

    三种“都不能”呢?

    最终答案是:

    var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;console.log( reg.test("1234567") ); // false 全是数字console.log( reg.test("abcdef") ); // false 全是小写字母console.log( reg.test("ABCDEFGH") ); // false 全是大写字母console.log( reg.test("ab23C") ); // false 不足6位console.log( reg.test("ABCDEF234") ); // true 大写字母和数字console.log( reg.test("abcdEF234") ); // true 三者都有复制代码

    第二章小结

    位置匹配相关的案例,挺多的,不一而足。

    掌握匹配位置的这6个锚字符,给我们解决正则问题一个新工具。

     

    第三章 正则表达式括号的作用

    不管哪门语言中都有括号。正则表达式也是一门语言,而括号的存在使这门语言更为强大。

    对括号的使用是否得心应手,是衡量对正则的掌握水平的一个侧面标准。

    括号的作用,其实三言两语就能说明白,括号提供了分组,便于我们引用它。

    引用某个分组,会有两种情形:在JavaScript里引用它,在正则表达式里引用它。

    本章内容虽相对简单,但我也要写长点。

    内容包括:

    1. 分组和分支结构
    2. 捕获分组
    3. 反向引用
    4. 非捕获分组
    5. 相关案例

    1. 分组和分支结构

    这二者是括号最直觉的作用,也是最原始的功能。

    1.1 分组

    我们知道/a+/匹配连续出现的“a”,而要匹配连续出现的“ab”时,需要使用/(ab)+/。

    其中括号是提供分组功能,使量词+作用于“ab”这个整体,测试如下:

    var regex = /(ab)+/g;var string = "ababa abbb ababab";console.log( string.match(regex) ); // => ["abab", "ab", "ababab"]复制代码

    1.2 分支结构

    而在多选分支结构(p1|p2)中,此处括号的作用也是不言而喻的,提供了子表达式的所有可能。

    比如,要匹配如下的字符串:

    I love JavaScript

    I love Regular Expression

    可以使用正则:

    var regex = /^I love (JavaScript|Regular Expression)$/;console.log( regex.test("I love JavaScript") );console.log( regex.test("I love Regular Expression") );// => true// => true复制代码

    如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。

    2. 引用分组

    这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。

    而要使用它带来的好处,必须配合使用实现环境的API。

    以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:

    var regex = /\d{4}-\d{2}-\d{2}/;复制代码

    然后再修改成括号版的:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;复制代码

    为什么要使用这个正则呢?

    2.1 提取数据

    比如提取出年、月、日,可以这么做:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;var string = "2017-06-12";console.log( string.match(regex) ); // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]复制代码

    match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符g,match返回的数组格式是不一样的)。

    另外也可以使用正则对象的exec方法:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;var string = "2017-06-12";console.log( regex.exec(string) ); // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]复制代码

    同时,也可以使用构造函数的全局属性$1至$9来获取:

    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"复制代码

    2.2 替换

    比如,想把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"复制代码

    其中replace中的,第二个参数里用$1、$2、$3指代相应的分组。等价于如下的形式:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;var string = "2017-06-12";var result = string.replace(regex, function() {

    return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;

    });console.log(result); // => "06/12/2017"复制代码

    也等价于:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;var string = "2017-06-12";var result = string.replace(regex, function(match, year, month, day) {

    return month + "/" + day + "/" + year;

    });console.log(result); // => "06/12/2017"复制代码

    3. 反向引用

    除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

    还是以日期为例。

    比如要写一个正则支持匹配如下三种格式:

    2016-06-12

    2016/06/12

    2016.06.12

    最先可能想到的正则是:

    var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\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) ); // trueconsole.log( regex.test(string2) ); // trueconsole.log( regex.test(string3) ); // trueconsole.log( regex.test(string4) ); // true复制代码

    其中/和.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。

    假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:

    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) ); // trueconsole.log( regex.test(string2) ); // trueconsole.log( regex.test(string3) ); // trueconsole.log( regex.test(string4) ); // false复制代码

    注意里面的\1,表示的引用之前的那个分组(-|\/|\.)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。

    我们知道了\1的含义后,那么\2和\3的概念也就理解了,即分别指代第二个和第三个分组。

    看到这里,此时,恐怕你会有三个问题。

    3.1 括号嵌套怎么办?

    以左括号(开括号)为准。比如:

    var regex = /^((\d)(\d(\d)))\1\2\3\4$/;var string = "1231231233";console.log( regex.test(string) ); // trueconsole.log( RegExp.$1 ); // 123console.log( RegExp.$2 ); // 1console.log( RegExp.$3 ); // 23console.log( RegExp.$4 ); // 3复制代码

    我们可以看看这个正则匹配模式:

    • 第一个字符是数字,比如说1,
    • 第二个字符是数字,比如说2,
    • 第三个字符是数字,比如说3,
    • 接下来的是\1,是第一个分组内容,那么看第一个开括号对应的分组是什么,是123,
    • 接下来的是\2,找到第2个开括号,对应的分组,匹配的内容是1,
    • 接下来的是\3,找到第3个开括号,对应的分组,匹配的内容是23,
    • 最后的是\4,找到第3个开括号,对应的分组,匹配的内容是3。

    这个问题,估计仔细看一下,就该明白了。

    3.2 \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复制代码

    3.3 引用不存在的分组会怎样?

    因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如\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\7\8\9") ); console.log( "\1\2\3\4\5\6\7\8\9".split("") );复制代码

    chrome浏览器打印的结果:

     

    4. 非捕获分组

    之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。

    如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p),例如本文第一个例子可以修改为:

    var regex = /(?:ab)+/g;var string = "ababa abbb ababab";console.log( string.match(regex) ); // => ["abab", "ab", "ababab"]复制代码

    5. 相关案例

    至此括号的作用已经讲完了,总结一句话,就是提供了可供我们使用的分组,如何用就看我们的了。

    5.1 字符串trim方法模拟

    trim方法是去掉字符串的开头和结尾的空白符。有两种思路去做。

    第一种,匹配到开头和结尾的空白符,然后替换成空字符。如:

    function trim(str) {

    return str.replace(/^\s+|\s+$/g, '');

    }console.log( trim("  foobar   ") ); // => "foobar"复制代码

    第二种,匹配整个字符串,然后用引用来提取出相应的数据:

    function trim(str) {

    return str.replace(/^\s*(.*?)\s*$/g, "$1");

    }console.log( trim("  foobar   ") ); // => "foobar"复制代码

    这里使用了惰性匹配*?,不然也会匹配最后一个空格之前的所有空格的。

    当然,前者效率高。

    5.2 将每个单词的首字母转换为大写

    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"复制代码

    思路是找到每个单词的首字母,当然这里不使用非捕获匹配也是可以的。

    5.3 驼峰化

    function camelize(str) {

    return str.replace(/[-_\s]+(.)?/g, function(match, c) {

    return c ? c.toUpperCase() : '';

    });

    }console.log( camelize('-moz-transform') ); // => "MozTransform"复制代码

    其中分组(.)表示首字母。单词的界定是,前面的字符可以是多个连字符、下划线以及空白符。正则后面的?的目的,是为了应对str尾部的字符可能不是单词字符,比如str是'-moz-transform    '。

    5.4 中划线化

    function dasherize(str) {

    return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();

    }console.log( dasherize('MozTransform') ); // => "-moz-transform"复制代码

    驼峰化的逆过程。

    5.5 html转义和反转义

    // 将HTML特殊字符转换成等值的实体function escapeHTML(str) {

    var escapeChars = {

     '¢' : 'cent',

     '£' : 'pound',

     '¥' : 'yen',

     '€': 'euro',

     '©' :'copy',

     '®' : 'reg',

     '<' : '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>";复制代码

    其中使用了用构造函数生成的正则,然后替换相应的格式就行了,这个跟本章没多大关系。

    倒是它的逆过程,使用了括号,以便提供引用,也很简单,如下:

    // 实体字符转换为等值的HTML。function unescapeHTML(str) {

    var htmlEntities = {

     nbsp: ' ',

     cent: '¢',

     pound: '£',

     yen: '¥',

     euro: '€',

     copy: '©',

     reg: '®',

     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>"复制代码

    通过key获取相应的分组引用,然后作为对象的键。

    5.6 匹配成对标签

    要求匹配:

    <title>regular expression</title>

    <p>laoyao bye bye</p>

    不匹配:

    <title>wrong!</p>

    匹配一个开标签,可以使用正则<[^>]+>,

    匹配一个闭标签,可以使用<\/[^>]+>,

    但是要求匹配成对标签,那就需要使用反向引用,如:

    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) ); // trueconsole.log( regex.test(string2) ); // trueconsole.log( regex.test(string3) ); // false复制代码

    其中开标签<[^>]+>改成<([^>]+)>,使用括号的目的是为了后面使用反向引用,而提供分组。闭标签使用了反向引用,<\/\1>。

    另外[\d\D]的意思是,这个字符是数字或者不是数字,因此,也就是匹配任意字符的意思。

    第三章小结

    正则中使用括号的例子那可是太多了,不一而足。

    重点理解括号可以提供分组,我们可以提取数据,应该就可以了。

    例子中的代码,基本没做多少分析,相信你都能看懂的。

     

    第4章 正则表达式回溯法原理

    学习正则表达式,是需要懂点儿匹配原理的。

    而研究匹配原理时,有两个字出现的频率比较高:“回溯”。

    听起来挺高大上,确实还有很多人对此不明不白的。

    因此,本章就简单扼要地说清楚回溯到底是什么东西。

    内容包括:

    1. 没有回溯的匹配
    2. 有回溯的匹配
    3. 常见的回溯形式

    1. 没有回溯的匹配

    假设我们的正则是/ab{1,3}c/,其可视化形式是:

     

    而当目标字符串是"abbbc"时,就没有所谓的“回溯”。其匹配过程是:

     

    其中子表达式b{1,3}表示“b”字符连续出现1到3次。

    2. 有回溯的匹配

    如果目标字符串是"abbc",中间就有回溯。

     

    图中第5步有红颜色,表示匹配不成功。此时b{1,3}已经匹配到了2个字符“b”,准备尝试第三个时,结果发现接下来的字符是“c”。那么就认为b{1,3}就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式c,去匹配字符“c”。当然,此时整个表达式匹配成功了。

    图中的第6步,就是“回溯”。

    你可能对此没有感觉,这里我们再举一个例子。正则是:

     

    目标字符串是"abbbc",匹配过程是:

     

    其中第7步和第10步是回溯。第7步与第4步一样,此时b{1,3}匹配了两个"b",而第10步与第3步一样,此时b{1,3}只匹配了一个"b",这也是b{1,3}的最终匹配结果。

    这里再看一个清晰的回溯,正则是:

     

    目标字符串是:"acd"ef,匹配过程是:

     

    图中省略了尝试匹配双引号失败的过程。可以看出.*是非常影响效率的。

    为了减少一些不必要的回溯,可以把正则修改为/"[^"]*"/。

    3. 常见的回溯形式

    正则表达式匹配字符串的这种方式,有个学名,叫回溯法。

    回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。(copy于百度百科)。

    本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。

    道理,我们是懂了。那么JS中正则表达式会产生回溯的地方都有哪些呢?

    3.1 贪婪量词

    之前的例子都是贪婪量词相关的。比如b{1,3},因为其是贪婪的,尝试可能的顺序是从多往少的方向去尝试。首先会尝试"bbb",然后再看整个正则是否能匹配。不能匹配时,吐出一个"b",即在"bb"的基础上,再继续尝试。如果还不行,再吐出一个,再试。如果还不行呢?只能说明匹配失败了。

    虽然局部匹配是贪婪的,但也要满足整体能正确匹配。否则,皮之不存,毛将焉附?

    此时我们不禁会问,如果当多个贪婪量词挨着存在,并相互有冲突时,此时会是怎样?

    答案是,先下手为强!因为深度优先搜索。测试如下:

    var string = "12345";var regex = /(\d{1,3})(\d{1,3})/;console.log( string.match(regex) );// => ["12345", "123", "45", index: 0, input: "12345"]复制代码

    其中,前面的\d{1,3}匹配的是"123",后面的\d{1,3}匹配的是"45"。

    3.2 惰性量词

    惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配,比如:

    var string = "12345";var regex = /(\d{1,3}?)(\d{1,3})/;console.log( string.match(regex) );// => ["1234", "1", "234", index: 0, input: "12345"]复制代码

    其中\d{1,3}?只匹配到一个字符"1",而后面的\d{1,3}匹配了"234"。

    虽然惰性量词不贪,但也会有回溯的现象。比如正则是:

     

    目标字符串是"12345",匹配过程是:

     

    知道你不贪、很知足,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后\d{1,3}?匹配的字符是"12",是两个数字,而不是一个。

    3.3 分支结构

    我们知道分支也是惰性的,比如/can|candy/,去匹配字符串"candy",得到的结果是"can",因为分支会一个一个尝试,如果前面的满足了,后面就不会再试验了。

    分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分支。这种尝试也可以看成一种回溯。

    比如正则:

     

    目标字符串是"candy",匹配过程:

     

    上面第5步,虽然没有回到之前的状态,但仍然回到了分支结构,尝试下一种可能。所以,可以认为它是一种回溯的。

    第四章小结

    其实回溯法,很容易掌握的。

    简单总结就是,正因为有多种可能,所以要一个一个试。直到,要么到某一步时,整体匹配成功了;要么最后都试完后,发现整体匹配不成功。

    1. 贪婪量词“试”的策略是:买衣服砍价。价钱太高了,便宜点,不行,再便宜点。
    2. 惰性量词“试”的策略是:卖东西加价。给少了,再多给点行不,还有点少啊,再给点。
    3. 分支结构“试”的策略是:货比三家。这家不行,换一家吧,还不行,再换。

    既然有回溯的过程,那么匹配效率肯定低一些。相对谁呢?相对那些DFA引擎。

    而JS的正则引擎是NFA,NFA是“非确定型有限自动机”的简写。

    大部分语言中的正则都是NFA,为啥它这么流行呢?

    答:你别看我匹配慢,但是我编译快啊,而且我还有趣哦。

     

    第5章 正则表达式的拆分

    对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。

    不仅要求自己能解决问题,还要看懂别人的解决方案。代码是这样,正则表达式也是这样。

    正则这门语言跟其他语言有一点不同,它通常就是一大堆字符,而没有所谓“语句”的概念。

    如何能正确地把一大串正则拆分成一块一块的,成为了破解“天书”的关键。

    本章就解决这一问题,内容包括:

    1. 结构和操作符
    2. 注意要点
    3. 案例分析

    1. 结构和操作符

    编程语言一般都有操作符。只要有操作符,就会出现一个问题。当一大堆操作在一起时,先操作谁,又后操作谁呢?为了不产生歧义,就需要语言本身定义好操作顺序,即所谓的优先级。

    而在正则表达式中,操作符都体现在结构中,即由特殊字符和普通字符所代表的一个个特殊整体。

    JS正则表达式中,都有哪些结构呢?

    字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用。

    具体含义简要回顾如下(如懂,可以略去不看):

    字面量,匹配一个具体字符,包括不用转义的和需要转义的。比如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.括号和方括号 (...)、(?:...)、(?=...)、(?!...)、[...]
    3.量词限定符 {m}、{m,n}、{m,}、?、*、+
    4.位置和序列 ^ 、$、 \元字符、 一般字符
    5. 管道符(竖杠)|

    上面操作符的优先级从上至下,由高到低。

    这里,我们来分析一个正则:

    /ab?(c|de*)+|fg/

    1. 由于括号的存在,所以,(c|de*)是一个整体结构。
    2. 在(c|de*)中,注意其中的量词*,因此e*是一个整体结构。
    3. 又因为分支结构“|”优先级最低,因此c是一个整体、而de*是另一个整体。
    4. 同理,整个正则分成了 a、b?、(...)+、f、g。而由于分支的原因,又可以分成ab?(c|de*)+和fg这两部分。

    希望你没被我绕晕,上面的分析可用其可视化形式描述如下:

     

    2. 注意要点

    关于结构和操作符,还是有几点需要强调:

    2.1 匹配字符串整体问题

    因为是要匹配整个字符串,我们经常会在正则前后中加上锚字符^和$。

    比如要匹配目标字符串"abc"或者"bcd"时,如果一不小心,就会写成/^abc|bcd$/。

    而位置字符和字符序列优先级要比竖杠高,故其匹配的结构是:

     

    应该修改成:

     

    2.2 量词连缀问题

    假设,要匹配这样的字符串:

    1. 每个字符为a、b、c任选其一

    2. 字符串的长度是3的倍数

    此时正则不能想当然地写成/^[abc]{3}+$/,这样会报错,说+前面没什么可重复的:

     

    此时要修改成:

     

    2.3 元字符转义问题

    所谓元字符,就是正则中有特殊含义的字符。

    所有结构里,用到的元字符总结如下:

    ^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,

    当匹配上面的字符本身时,可以一律转义:

    var string = "^$.*+?|\\/[]{}=!:-,";var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;console.log( regex.test(string) ); // => true复制代码

    其中string中的\字符也要转义的。

    另外,在string中,也可以把每个字符转义,当然,转义后的结果仍是本身:

    var string = "^$.*+?|\\/[]{}=!:-,";var string2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";console.log( string == string2 ); // => true复制代码

    现在的问题是,是不是每个字符都需要转义呢?否,看情况。

    2.3.1 字符组中的元字符

    跟字符组相关的元字符有[]、^、-。因此在会引起歧义的地方进行转义。例如开头的^必须转义,不然会把整个字符组,看成反义字符组。

    var string = "^$.*+?|\\/[]{}=!:-,";var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;console.log( string.match(regex) );// => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]复制代码

    2.3.2 匹配“[abc]”和“{3,5}”

    我们知道[abc],是个字符组。如果要匹配字符串"[abc]"时,该怎么办?

    可以写成/\[abc\]/,也可以写成/\[abc]/,测试如下:

    var string = "[abc]";var regex = /\[abc]/g;console.log( string.match(regex)[0] ); // => "[abc]"复制代码

    只需要在第一个方括号转义即可,因为后面的方括号构不成字符组,正则不会引发歧义,自然不需要转义。

    同理,要匹配字符串"{3,5}",只需要把正则写成/\{3,5}/即可。

    另外,我们知道量词有简写形式{m,},却没有{,n}的情况。虽然后者不构成量词的形式,但此时并不会报错。当然,匹配的字符串也是"{,n}",测试如下:

    var string = "{,3}";var regex = /{,3}/g;console.log( string.match(regex)[0] ); // => "{,3}"复制代码

    2.3.3 其余情况

    比如= ! : - ,等符号,只要不在特殊结构中,也不需要转义。

    但是,括号需要前后都转义的,如/\(123\)/。

    至于剩下的^ $ . * + ? | \ /等字符,只要不在字符组内,都需要转义的。

    3. 案例分析

    接下来分析两个例子,一个简单的,一个复杂的。

    3.1 身份证

    正则表达式是:

    /^(\d{15}|\d{17}[\dxX])$/

    因为竖杠“|”,的优先级最低,所以正则分成了两部分\d{15}和\d{17}[\dxX]。

    • \d{15}表示15位连续数字。
    • \d{17}[\dxX]表示17位连续数字,最后一位可以是数字可以大小写字母"x"。

    可视化如下:

     

    3.2 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])(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。

    最后来看一下其可视化形式:

     

    第五章小结

    掌握正则表达式中的优先级后,再看任何正则应该都有信心分析下去了。

    至于例子,不一而足,没有写太多。

    这里稍微总结一下,竖杠的优先级最低,即最后运算。

    只要知道这一点,就能读懂大部分正则。

    另外关于元字符转义问题,当自己不确定与否时,尽管去转义,总之是不会错的。

     

    第6章 正则表达式的构建

    对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。

    不仅要看懂别人的解决方案,也要能独立地解决问题。代码是这样,正则表达式也是这样。

    与“读”相比,“写”往往更为重要,这个道理是不言而喻的。

    对正则的运用,首重就是:如何针对问题,构建一个合适的正则表达式?

    本章就解决该问题,内容包括:

    1. 平衡法则
    2. 构建正则前提
    3. 准确性
    4.  效率

    1. 平衡法则

    构建正则有一点非常重要,需要做到下面几点的平衡:

    1. 匹配预期的字符串
    2. 不匹配非预期的字符串
    3. 可读性和可维护性
    4. 效率

    2. 构建正则前提

    2.1 是否能使用正则

    正则太强大了,以至于我们随便遇到一个操作字符串问题时,都会下意识地去想,用正则该怎么做。但我们始终要提醒自己,正则虽然强大,但不是万能的,很多看似很简单的事情,还是做不到的。

    比如匹配这样的字符串:1010010001....

    虽然很有规律,但是只靠正则就是无能为力。

    2.2 是否有必要使用正则

    要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能用字符串API解决的简单问题,就不该正则出马。

    • 比如,从日期中提取出年月日,虽然可以使用正则:

    var string = "2017-07-01";var regex = /^(\d{4})-(\d{2})-(\d{2})/;console.log( string.match(regex) );// => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]复制代码

    其实,可以使用字符串的split方法来做,即可:

    var string = "2017-07-01";var result = string.split("-");console.log( result );// => ["2017", "07", "01"]复制代码

    • 比如,判断是否有问号,虽然可以使用:

    var string = "?id=xx&act=search";console.log( string.search(/\?/) );// => 0复制代码

    其实,可以使用字符串的indexOf方法:

    var string = "?id=xx&act=search";console.log( string.indexOf("?") );// => 0复制代码

    • 比如获取子串,虽然可以使用正则:

    var string = "JavaScript";console.log( string.match(/.{4}(.+)/)[1] );// => Script复制代码

    其实,可以直接使用字符串的substring或substr方法来做:

    var string = "JavaScript";console.log( string.substring(4) );// => Script复制代码

    2.3 是否有必要构建一个复杂的正则

    比如密码匹配问题,要求密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

    在第2章里,我们写出了正则是:

    /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

    其实可以使用多个小正则来做:

    var regex1 = /^[0-9A-Za-z]{6,12}$/;var regex2 = /^[0-9]{6,12}$/;var regex3 = /^[A-Z]{6,12}$/;var regex4 = /^[a-z]{6,12}$/;function checkPassword(string) {

    if (!regex1.test(string)) return false;

    if (regex2.test(string)) return false;

    if (regex3.test(string)) return false;

    if (regex4.test(string)) return false;

    return true;

    }复制代码

    3. 准确性

    所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。

    这里提到了“预期”二字,那么我们就需要知道目标的组成规则。

    不然没法界定什么样的目标字符串是符合预期的,什么样的又不是符合预期的。

    下面将举例说明,当目标字符串构成比较复杂时,该如何构建正则,并考虑到哪些平衡。

    3.1 匹配固定电话

    比如要匹配如下格式的固定电话号码:

    055188888888

    0551-88888888

    (0551)88888888

    第一步,了解各部分的模式规则。

    上面的电话,总体上分为区号和号码两部分(不考虑分机号和+86的情形)。

    区号是0开头的3到4位数字,对应的正则是:0\d{2,3}

    号码是非0开头的7到8位数字,对应的正则是:[1-9]\d{6,7}

    因此,匹配055188888888的正则是:/^0\d{2,3}[1-9]\d{6,7}$/

    匹配0551-88888888的正则是:/^0\d{2,3}-[1-9]\d{6,7}$/

    匹配(0551)88888888的正则是:/^\(0\d{2,3}\)[1-9]\d{6,7}$/

    第二步,明确形式关系。

    这三者情形是或的关系,可以构建分支:

    /^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

    提取公共部分:

    /^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

    进一步简写:

    /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

    其可视化形式:

     

    上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。

    上述三种情形是或的关系,这一点很重要,不然很容易按字符是否出现的情形把正则写成:

    /^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/

    虽然也能匹配上述目标字符串,但也会匹配(0551-88888888这样的字符串。当然,这不是我们想要的。

    其实这个正则也不是完美的,因为现实中,并不是每个3位数和4位数都是一个真实的区号。

    这就是一个平衡取舍问题,一般够用就行。

    3.2 匹配浮点数

    要求匹配如下的格式:

    1.23、+1.23、-1.23

    10、+10、-10

    .2、+.2、-.2

    可以看出正则分为三部分。

    符号部分:[+-]

    整数部分:\d+

    小数部分:\.\d+

    上述三个部分,并不是全部都出现。如果此时很容易写出如下的正则:

    /^[+-]?(\d+)?(\.\d+)?$/

    此正则看似没问题,但这个正则也会匹配空字符""。

    因为目标字符串的形式关系不是要求每部分都是可选的。

    要匹配1.23、+1.23、-1.23,可以用/^[+-]?\d+\.\d+$/

    要匹配10、+10、-10,可以用/^[+-]?\d+$/

    要匹配.2、+.2、-.2,可以用/^[+-]?\.\d+$/

    因此整个正则是这三者的或的关系,提取公众部分后是:

    /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

    其可视化形式是:

     

    如果要求不匹配+.2和-.2,此时正则变成:

     

    当然,/^[+-]?(\d+\.\d+|\d+|\.\d+)$/也不是完美的,我们也是做了些取舍,比如:

    • 它也会匹配012这样以0开头的整数。如果要求不匹配的话,需要修改整数部分的正则。
    • 一般进行验证操作之前,都要经过trim和判空。那样的话,也许那个错误正则也就够用了。
    • 也可以进一步改写成:/^[+-]?(\d+)?(\.)?\d+$/,这样我们就需要考虑可读性和可维护性了。

    4. 效率

    保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。

    正则表达式的运行分为如下的阶段:

    1. 编译
    2. 设定起始位置
    3. 尝试匹配
    4. 匹配失败的话,从下一位开始继续第3步
    5. 最终结果:匹配成功或失败

    下面以代码为例,来看看这几个阶段都做了什么:

    var regex = /\d+/g;console.log( regex.lastIndex, regex.exec("123abc34def") );console.log( regex.lastIndex, regex.exec("123abc34def") );console.log( regex.lastIndex, regex.exec("123abc34def") );console.log( regex.lastIndex, regex.exec("123abc34def") );// => 0 ["123", index: 0, input: "123abc34def"]// => 3 ["34", index: 6, input: "123abc34def"]// => 8 null// => 0 ["123", index: 0, input: "123abc34def"]复制代码

    具体分析如下:

    var regex = /\d+/g;复制代码

    当生成一个正则时,引擎会对其进行编译。报错与否出现这这个阶段。

    regex.exec("123abc34def")复制代码

    当尝试匹配时,需要确定从哪一位置开始匹配。一般情形都是字符串的开头,即第0位。

    但当使用test和exec方法,且正则有g时,起始位置是从正则对象的lastIndex属性开始。

    因此第一次exec是从第0位开始,而第二次是从3开始的。

    设定好起始位置后,就开始尝试匹配了。

    比如第一次exec,从0开始,去尝试匹配,并且成功地匹配到3个数字。此时结束时的下标是2,因此下一次的起始位置是3。

    而第二次,起始下标是3,但第3个字符是“a”,并不是数字。但此时并不会直接报匹配失败,而是移动到下一位置,即从第4位开始继续尝试匹配,但该字符是b,也不是数字。再移动到下一位,是c仍不行,再移动一位是数字3,此时匹配到了两位数字34。此时,下一次匹配的位置是d的位置,即第8位。

    第三次,是从第8位开始匹配,直到试到最后一位,也没发现匹配的,因此匹配失败,返回null。同时设置lastIndex为0,即,如要再尝试匹配的话,需从头开始。

    从上面可以看出,匹配会出现效率问题,主要出现在上面的第3阶段和第4阶段。

    因此,主要优化手法也是针对这两阶段的。

    4.1 使用具体型字符组来代替通配符,来消除回溯

    而在第三阶段,最大的问题就是回溯。

    例如,匹配双引用号之间的字符。如,匹配字符串123"abc"456中的"abc"。

    如果正则用的是:/".*"/,,会在第3阶段产生4次回溯(粉色表示.*匹配的内容):

     

    如果正则用的是:/".*?"/,会产生2次回溯(粉色表示.*?匹配的内容):

     

    因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存。

    此时要使用具体化的字符组,来代替通配符.,以便消除不必要的字符,此时使用正则/"[^"]*"/,即可。

    4.2 使用非捕获型分组

    因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。

    当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。例如:

    /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

    可以修改成:

    /^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/

    4.3 独立出确定字符

    例如/a+/,可以修改成/aa*/。

    因为后者能比前者多确定了字符a。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。

    4.4 提取分支公共部分

    比如/^abc|^def/,修改成/^(?:abc|def)/。

    又比如/this|that/,修改成/th(?:is|at)/。

    这样做,可以减少匹配过程中可消除的重复。

    4.5 减少分支的数量,缩小它们的范围

    /red|read/,可以修改成/rea?d/。此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。

    第六章小结

    本章涉及的内容并不多。

    一般情况下,针对某问题能写出一个满足需求的正则,基本上就可以了。

    至于准确性和效率方面的追求,纯属看个人要求了。我觉得够用就行了。

    关于准确性,本章关心的是最常用的解决思路:

    针对每种情形,分别写出正则,然用分支把他们合并在一起,再提取分支公共部分,就能得到准确的正则。

    至于优化,本章没有为了凑数,去写一大堆。了解了匹配原理,常见的优化手法也就这么几种。

     

    第七章 正则表达式编程

    什么叫知识,能指导我们实践的东西才叫知识。

    学习一样东西,如果不能使用,最多只能算作纸上谈兵。正则表达式的学习,也不例外。

    掌握了正则表达式的语法后,下一步,也是关键的一步,就是在真实世界中使用它。

    那么如何使用正则表达式呢?有哪些关键的点呢?本章就解决这个问题。

    内容包括:

    1. 正则表达式的四种操作
    2. 相关API注意要点
    3. 真实案例

    1. 正则表达式的四种操作

    正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。

    有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。

    进行任何相关操作,也需要宿主引擎相关API的配合使用。当然,在JS中,相关API也不多。

    1.1 验证

    验证是正则表达式最直接的应用,比如表单验证。

    在说验证之前,先要说清楚匹配是什么概念。

    所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,“匹配”的本质就是“查找”。

    有没有匹配,是不是匹配上,判断是否的操作,即称为“验证”。

    这里举一个例子,来看看如何使用相关API进行验证操作的。

    比如,判断一个字符串中是否有数字。

    • 使用search

    var regex = /\d/;var string = "abc123";console.log( !!~string.search(regex) );// => true复制代码

    • 使用test

    var regex = /\d/;var string = "abc123";console.log( regex.test(string) );// => true复制代码

    • 使用match

    var regex = /\d/;var string = "abc123";console.log( !!string.match(regex) );// => true复制代码

    • 使用exec

    var regex = /\d/;var string = "abc123";console.log( !!regex.exec(string) );// => true复制代码

    其中,最常用的是test。

    1.2 切分

    匹配上了,我们就可以进行一些操作,比如切分。

    所谓“切分”,就是把目标字符串,切成一段一段的。在JS中使用的是split。

    比如,目标字符串是"html,css,javascript",按逗号来切分:

    var regex = /,/;var string = "html,css,javascript";console.log( string.split(regex) );// => ["html", "css", "javascript"]复制代码

    又比如,如下的日期格式:

    2017/06/26

    2017.06.26

    2017-06-26

    可以使用split“切出”年月日:

    var regex = /\D/;console.log( "2017/06/26".split(regex) );console.log( "2017.06.26".split(regex) );console.log( "2017-06-26".split(regex) );// => ["2017", "06", "26"]// => ["2017", "06", "26"]// => ["2017", "06", "26"]复制代码

    1.3 提取

    虽然整体匹配上了,但有时需要提取部分匹配的数据。

    此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关API。

    这里,还是以日期为例,提取出年月日。注意下面正则中的括号:

    • match

    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;var string = "2017-06-26";console.log( string.match(regex) );// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]复制代码

    • exec

    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;var string = "2017-06-26";console.log( regex.exec(string) );// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]复制代码

    • test

    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;var string = "2017-06-26";

    regex.test(string);console.log( RegExp.$1, RegExp.$2, RegExp.$3 );// => "2017" "06" "26"复制代码

    • search

    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;var string = "2017-06-26";

    string.search(regex);console.log( RegExp.$1, RegExp.$2, RegExp.$3 );// => "2017" "06" "26"复制代码

    • replace

    var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;var string = "2017-06-26";var date = [];

    string.replace(regex, function(match, year, month, day) {

    date.push(year, month, day);

    });console.log(date);// => ["2017", "06", "26"]复制代码

    其中,最常用的是match。

    1.4 替换

    找,往往不是目的,通常下一步是为了替换。在JS中,使用replace进行替换。

    比如把日期格式,从yyyy-mm-dd替换成yyyy/mm/dd:

    var string = "2017-06-26";var today = new Date( string.replace(/-/g, "/") );console.log( today );// => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)复制代码

    这里只是简单地应用了一下replace。但,replace方法是强大的,是需要重点掌握的。

    2. 相关API注意要点

    从上面可以看出用于正则操作的方法,共有6个,字符串实例4个,正则实例2个:

    String#search

    String#split

    String#match

    String#replace

    RegExp#test

    RegExp#exec

    本文不打算详细地讲解它们的方方面面细节,具体可以参考《JavaScript权威指南》的第三部分。本文重点列出一些容易忽视的地方,以飨读者。

    2.1 search和match的参数问题

    我们知道字符串实例的那4个方法参数都支持正则和字符串。

    但search和match,会把字符串转换为正则的。

    var string = "2017.06.27";

    console.log( string.search(".") );// => 0//需要修改成下列形式之一console.log( string.search("\\.") );console.log( string.search(/\./) );// => 4// => 4

    console.log( string.match(".") );// => ["2", index: 0, input: "2017.06.27"]//需要修改成下列形式之一console.log( string.match("\\.") );console.log( string.match(/\./) );// => [".", index: 4, input: "2017.06.27"]// => [".", index: 4, input: "2017.06.27"]

    console.log( string.split(".") );// => ["2017", "06", "27"]

    console.log( string.replace(".", "/") );// => "2017/06.27"复制代码

    2.2 match返回结果的格式问题

    match返回结果的格式,与正则对象是否有修饰符g有关。

    var string = "2017.06.27";var regex1 = /\b(\d+)\b/;var regex2 = /\b(\d+)\b/g;console.log( string.match(regex1) );console.log( string.match(regex2) );// => ["2017", "2017", index: 0, input: "2017.06.27"]// => ["2017", "06", "27"]复制代码

    没有g,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。

    有g,返回的是所有匹配的内容。

    当没有匹配时,不管有无g,都返回null。

    2.3 exec比match更强大

    当正则没有g时,使用match返回的信息比较多。但是有g后,就没有关键的信息index了。

    而exec方法就能解决这个问题,它能接着上一次匹配后继续匹配:

    var string = "2017.06.27";var regex2 = /\b(\d+)\b/g;console.log( regex2.exec(string) );console.log( regex2.lastIndex);console.log( regex2.exec(string) );console.log( regex2.lastIndex);console.log( regex2.exec(string) );console.log( regex2.lastIndex);console.log( regex2.exec(string) );console.log( regex2.lastIndex);// => ["2017", "2017", index: 0, input: "2017.06.27"]// => 4// => ["06", "06", index: 5, input: "2017.06.27"]// => 7// => ["27", "27", index: 8, input: "2017.06.27"]// => 10// => null// => 0复制代码

    其中正则实例lastIndex属性,表示下一次匹配开始的位置。

    比如第一次匹配了“2017”,开始下标是0,共4个字符,因此这次匹配结束的位置是3,下一次开始匹配的位置是4。

    从上述代码看出,在使用exec时,经常需要配合使用while循环:

    var string = "2017.06.27";var regex2 = /\b(\d+)\b/g;var result;while ( result = regex2.exec(string) ) {

    console.log( result, regex2.lastIndex );

    }// => ["2017", "2017", index: 0, input: "2017.06.27"] 4// => ["06", "06", index: 5, input: "2017.06.27"] 7// => ["27", "27", index: 8, input: "2017.06.27"] 10复制代码

    2.4 修饰符g,对exex和test的影响

    上面提到了正则实例的lastIndex属性,表示尝试匹配时,从字符串的lastIndex位开始去匹配。

    字符串的四个方法,每次匹配时,都是从0开始的,即lastIndex属性始终不变。

    而正则实例的两个方法exec、test,当正则是全局匹配时,每一次匹配完成后,都会修改lastIndex。下面让我们以test为例,看看你是否会迷糊:

    var regex = /a/g;console.log( regex.test("a"), regex.lastIndex );console.log( regex.test("aba"), regex.lastIndex );console.log( regex.test("ababc"), regex.lastIndex );// => true 1// => true 3// => false 0复制代码

    注意上面代码中的第三次调用test,因为这一次尝试匹配,开始从下标lastIndex即3位置处开始查找,自然就找不到了。

    如果没有g,自然都是从字符串第0个字符处开始尝试匹配:

    var regex = /a/;console.log( regex.test("a"), regex.lastIndex );console.log( regex.test("aba"), regex.lastIndex );console.log( regex.test("ababc"), regex.lastIndex );// => true 0// => true 0// => true 0复制代码

    2.5 test整体匹配时需要使用^和$

    这个相对容易理解,因为test是看目标字符串中是否有子串匹配正则,即有部分匹配即可。

    如果,要整体匹配,正则前后需要添加开头和结尾:

    console.log( /123/.test("a123b") );// => trueconsole.log( /^123$/.test("a123b") );// => falseconsole.log( /^123$/.test("123") );// => true复制代码

    2.6 split相关注意事项

    split方法看起来不起眼,但要注意的地方有两个的。

    第一,它可以有第二个参数,表示结果数组的最大长度:

    var string = "html,css,javascript";console.log( string.split(/,/, 2) );// =>["html", "css"]复制代码

    第二,正则使用分组时,结果数组中是包含分隔符的:

    var string = "html,css,javascript";console.log( string.split(/(,)/) );// =>["html", ",", "css", ",", "javascript"]复制代码

    2.7 replace是很强大的

    《JavaScript权威指南》认为exec是这6个API中最强大的,而我始终认为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,5",变成"222,333,555":

    var result = "2,3,5".replace(/(\d+)/g, "$&$&$&");console.log(result);// => "222,333,555"复制代码

    再例如,把"2+3=5",变成"2+3=2+3=5=5":

    var result = "2+3=5".replace(/=/, "$&$`$&$'$&");console.log(result);// => "2+3=2+3=5=5"复制代码

    当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:

    "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"]复制代码

    此时我们可以看到replace拿到的信息,并不比exec少。

    2.8 使用构造函数需要注意的问题

    一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多\。

    var string = "2017-06-27 2017.06.27 2017/06/27";var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;console.log( string.match(regex) );// => ["2017-06-27", "2017.06.27", "2017/06/27"]

     

    regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");console.log( string.match(regex) );// => ["2017-06-27", "2017.06.27", "2017/06/27"]复制代码

    2.9 修饰符

    ES5中修饰符,共3个:

    g 全局匹配,即找到所有匹配的,单词是global

    i 忽略字母大小写,单词ingoreCase

    m 多行匹配,只影响^和$,二者变成行的概念,即行开头和行结尾。单词是multiline

    当然正则对象也有相应的只读属性:

    var regex = /\w/img;console.log( regex.global );console.log( regex.ignoreCase );console.log( regex.multiline );// => true// => true// => true复制代码

    2.10 source属性

    正则实例对象属性,除了global、ingnoreCase、multiline、lastIndex属性之外,还有一个source属性。

    它什么时候有用呢?

    比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:

    var className = "high";var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");console.log( regex.source )// => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"复制代码

    2.11 构造函数属性

    构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是$1,...,$9之外,还有几个不太常用的属性(有兼容性问题):

    RegExp.input 最近一次目标字符串,简写成RegExp["$_"]
    RegExp.lastMatch 最近一次匹配的文本,简写成RegExp["$&"]
    RegExp.lastParen 最近一次捕获的文本,简写成RegExp["$+"]
    RegExp.leftContext 目标字符串中lastMatch之前的文本,简写成RegExp["$`"]
    RegExp.rightContext 目标字符串中lastMatch之后的文本,简写成RegExp["$'"]

    测试代码如下:

    var regex = /([abc])(\d)/g;var string = "a1b2c3d4e5";

    string.match(regex);

    console.log( RegExp.input );console.log( RegExp["$_"]);// => "a1b2c3d4e5"

    console.log( RegExp.lastMatch );console.log( RegExp["$&"] );// => "c3"

    console.log( RegExp.lastParen );console.log( RegExp["$+"] );// => "3"

    console.log( RegExp.leftContext );console.log( RegExp["$`"] );// => "a1b2"

    console.log( RegExp.rightContext );console.log( RegExp["$'"] );// => "d4e5"复制代码

    3. 真实案例

    3.1 使用构造函数生成正则表达式

    我们知道要优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。模拟getElementsByClassName方法,就是很能说明该问题的一个例子。

    这里getElementsByClassName函数的实现思路是:

    • 比如要获取className为"high"的dom元素;
    • 首先生成一个正则:/(^|\s)high(\s|$)/;
    • 然后再用其逐一验证页面上的所有dom元素的类名,拿到满足匹配的元素即可。

    代码如下(可以直接复制到本地查看运行效果):

    <p class="high">1111</p>

    <p class="high">2222</p>

    <p>3333</p>

    <script>

    function getElementsByClassName(className) {

    var elements = document.getElementsByTagName("*");

    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");

    var result = [];

    for (var i = 0; i < elements.length; i++) {

    var element = elements[i];

    if (regex.test(element.className)) {

    result.push(element)

    }

    }

    return result;

    }

    var highs = getElementsByClassName('high');

    highs.forEach(function(item) {

    item.style.color = 'red';

    });

    </script>复制代码

    3.2 使用字符串保存数据

    一般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串。

    使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:

    var utils = {};"Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(function(item) {

    utils["is" + item] = function(obj) {

    return {}.toString.call(obj) == "[object " + item + "]";

    };

    });console.log( utils.isArray([1, 2, 3]) );// => true复制代码

    3.3 if语句中使用正则替代&&

    比如,模拟ready函数,即加载完毕后再执行回调(不兼容ie的):

    var readyRE = /complete|loaded|interactive/;

    function ready(callback) {

    if (readyRE.test(document.readyState) && document.body) {

    callback()

    }

    else {

    document.addEventListener(

    'DOMContentLoaded',

    function () {

    callback()

    },

    false

    );

    }

    };

    ready(function() {

    alert("加载完毕!")

    });复制代码

    3.4 使用强大的replace

    因为replace方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息来做文章。

    这里以查询字符串(querystring)压缩技术为例,注意下面replace方法中,回调函数根本没有返回任何东西。

    function compress(source) {

    var keys = {};

    source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) {

    keys[key] = (keys[key] ? keys[key] + ',' : '') + value;

    });

    var result = [];

    for (var key in keys) {

    result.push(key + '=' + keys[key]);

    }

    return result.join('&');

    }

    console.log( compress("a=1&b=2&a=3&b=4") );// => "a=1,3&b=2,4"复制代码

    3.5 综合运用

    最后这里再做个简单实用的正则测试器。

    具体效果如下:

     

    代码,直接贴了,相信你能看得懂:

    <section>

    <div id="err"></div>

    <input id="regex" placeholder="请输入正则表达式">

    <input id="text" placeholder="请输入测试文本">

    <button id="run">测试一下</button>

    <div id="result"></div>

    </section>

    <style>

    section{

    display:flex;

    flex-direction:column;

    justify-content:space-around;

    height:300px;

    padding:0 200px;

    }

    section *{

    min-height:30px;

    }

    #err {

    color:red;

    }

    #result{

    line-height:30px;

    }

    .info {

    background:#00c5ff;

    padding:2px;

    margin:2px;

    display:inline-block;

    }

    </style>

    <script>

    (function() {

    // 获取相应dom元素

    var regexInput = document.getElementById("regex");

    var textInput = document.getElementById("text");

    var runBtn = document.getElementById("run");

    var errBox = document.getElementById("err");

    var resultBox = document.getElementById("result");

     

    // 绑定点击事件

    runBtn.onclick = function() {

    // 清除错误和结果

    errBox.innerHTML = "";

    resultBox.innerHTML = "";

     

    // 获取正则和文本

    var text = textInput.value;

    var regex = regexInput.value;

     

    if (regex == "") {

    errBox.innerHTML = "请输入正则表达式";

    } else if (text == "") {

    errBox.innerHTML = "请输入测试文本";

    } else {

    regex = createRegex(regex);

    if (!regex) return;

    var result, results = [];

     

    // 没有修饰符g的话,会死循环

    if (regex.global) {

    while(result = regex.exec(text)) {

    results.push(result);

    }

    } else {

    results.push(regex.exec(text));

    }

     

    if (results[0] == null) {

    resultBox.innerHTML = "匹配到0个结果";

    return;

    }

     

    // 倒序是有必要的

    for (var i = results.length - 1; i >= 0; i--) {

    var result = results[i];

    var match = result[0];

    var prefix = text.substr(0, result.index);

    var suffix = text.substr(result.index + match.length);

    text = prefix

    + '<span class="info">'

    + match

    + '</span>'

    + suffix;

    }

    resultBox.innerHTML = "匹配到" + results.length + "个结果:<br>" + text;

    }

    };

     

    // 生成正则表达式,核心函数

    function createRegex(regex) {

    try {

    if (regex[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) {

    errBox.innerHTML = "无效的正则表达式";

    return false;

    }

    }

    })();

    </script>复制代码

    第七章小结

    相关API的注意点,本章基本上算是一网打尽了。

    至于文中的例子,都是点睛之笔,没有详细解析。如有理解不透的,建议自己敲一敲。

     

    后记

    其实本文首发于:正则表达式系列总结 - 知乎专栏

    原文是一个系列。一直等到老姚成为掘金的专栏作者,经过仔细考虑,在掘金平台没有采用系列形式,而是合成为了一篇文章。这样既便于读者阅读,最起码能一气呵成地阅读。同时也便于作者统一回复留言。

    文章要结束了,最后还要有几点说明。

    1. 需要注意的地方

    本文主要讨论的是JavaScript的正则表达式,更精确地说是ES5的正则表达式。

    JavaScript的正则表达式引擎是传统型NFA的,因此本系列的讨论是适合任何一门正则引擎是传统型NFA的编程语言。当然,市面上大部分语言的正则引擎都是这种的。而JS里正则涉及到的所有语法要点,是这种引擎支持的核心子集。也就是说,要学正则表达式,不妨以JS正则为出发点。


    作者:老姚
    链接:https://juejin.im/post/5965943ff265da6c30653879
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • 《计算机数据、表达式与简单程序设计》由会员分享,可在线阅读,更多相关《计算机数据、表达式与简单程序设计(34页珍藏版)》请在人人文库网上搜索。1、1 第四章第四章 数据、表达式与简单程序设计数据、表达式与简单...

    《计算机数据、表达式与简单程序设计》由会员分享,可在线阅读,更多相关《计算机数据、表达式与简单程序设计(34页珍藏版)》请在人人文库网上搜索。

    1、1 第四章第四章 数据、表达式与简单程序设计数据、表达式与简单程序设计 2 本章内容 n4.1 VB程序代码的组织方式程序代码的组织方式 n4.2 代码行的书写规则代码行的书写规则 n4.3 VB的数据的数据 n4.4 运算符与表达式运算符与表达式 n4.5 赋值语句赋值语句 n4.6 VB的公共函数的公共函数 n4.7 Inputbox函数与函数与Msgbox函数函数 3 4.1 VB程序代码的组织方式 n一个一个VB程序由两部分组成:程序由两部分组成: q窗体界面设计窗体界面设计 q程序代码设计程序代码设计 n这两部分的关系:这两部分的关系: q 程序代码的设计为了将窗体界面上的控件联系在。

    2、程序代码的设计为了将窗体界面上的控件联系在 一起一起. 4 4.1.1 过程过程 根据执行的方式根据执行的方式,过程可分为过程可分为事件过程事件过程 和和通用过程通用过程. 5 简单的说就是:通用过程是公有的简单的说就是:通用过程是公有的(Public), 可被一个应用程序中所有的窗体或者一个窗体可被一个应用程序中所有的窗体或者一个窗体 内不同的事件过程共享的一些代码。内不同的事件过程共享的一些代码。 6 模块:窗体模块,标准模块,类模块。 n窗体模块:每个窗体都对应一个窗体模块。窗体模块:每个窗体都对应一个窗体模块。 q窗体的事件过程窗体的事件过程 q窗体和控件的属性及说明窗体和控件的属性及。

    3、说明 q通用过程仅供本窗体中的过程共享通用过程仅供本窗体中的过程共享 q存储在窗体文件存储在窗体文件frm中中 7 n标准模块:标准模块: q可被多个窗体共享的代码可被多个窗体共享的代码 q保存的过程都是通用过程保存的过程都是通用过程 q存储在存储在bas文件中文件中 q不限于一个应用程序,还可供其它应用程序重复使不限于一个应用程序,还可供其它应用程序重复使 用。用。 n类模块类模块 8 4.2 代码行的书写规则 n语句是构成语句是构成VB程序的最基本成分程序的最基本成分 n一句一行一句一行 n一行多句,行间加一行多句,行间加“:”, n一句多行在行尾加续行标志一句多行在行尾加续行标志“ _”。

    4、(空格加下(空格加下 划线)划线)。 n不区分大小写,不区分大小写,保留字保留字第一个字母自动变为大第一个字母自动变为大 写写 n注释的方法:注释的方法:Rem 或或 (半角,英文状态下)(半角,英文状态下) 语句定义符语句定义符 语句体语句体 9 复习上次课内容 n计时器控件、菜单 n过程 n模块 n代码行书写规则 10 数值数据类型数值数据类型:包括整型、长整型、单精度:包括整型、长整型、单精度 浮点型、双精度浮点型和货币型浮点型、双精度浮点型和货币型 字符数据类型字符数据类型:包括定长字符串和不定长字:包括定长字符串和不定长字 符串符串 广泛其它数据类型广泛其它数据类型:包括字节型、包括。

    5、字节型、布尔型布尔型、 日期型和对象类型日期型和对象类型 变体数据类型变体数据类型 4.3.1 数据类型 4.3 Visual Basic的数据类型的数据类型 11 要合理定义数据类型 n1)VB中对没有声明的变量其缺省的数据类型中对没有声明的变量其缺省的数据类型 是变体型,可以用来存储各种数据,但所占用是变体型,可以用来存储各种数据,但所占用 的的内存内存比其它类型都比其它类型都多多(=16byte)。)。 n2)为为提高运行效率提高运行效率(整型效率较高整型效率较高),或达到一定,或达到一定 的运算精确度(浮点型精度较高,但运行较的运算精确度(浮点型精度较高,但运行较 慢),应合理的定义数。

    6、据类型。慢),应合理的定义数据类型。 12 理解数值的范围 n任何一个量,都有一个大的上限,和小的下限,任何一个量,都有一个大的上限,和小的下限, 出了这个范围(比上限还大,比下限还小),出了这个范围(比上限还大,比下限还小), 就会称为就会称为数据溢出数据溢出。 n比如:整型比如:整型 2 2个字节个字节 32768327683276732767 13 4.3.2常量常量 在程序运行过程中,其值不能被改变的量称为常量。在在程序运行过程中,其值不能被改变的量称为常量。在VB 中有三类常量:中有三类常量: 普通常量普通常量 符号常量符号常量 系统常量系统常量。 一、普通常量一、普通常量 1 整型。

    7、常量(不带小数点)整型常量(不带小数点) (1)整型()整型(Integer):表示):表示-32768至至32767之间的整数之间的整数 例如:例如:10 110 20 (2)长整型()长整型(Long):): 表示表示-2,147,483,648至至2,147,483,647之间的整数之间的整数 例如:长整型常数的书写:例如:长整型常数的书写: 123456 23 ; 2 2、只能由字母、数字和下划线组成并且首、只能由字母、数字和下划线组成并且首 字符必须是字母的字符串字符必须是字母的字符串; ; 3 3、常量中字母不区分大小写。、常量中字母不区分大小写。 21 Text1.ForeCol。

    8、or=vbRed 这里的这里的vbRed就是系统常量。就是系统常量。 系统常量可在系统常量可在视图视图-对象浏览器对象浏览器中看到中看到 三、系统常量三、系统常量 22 4.3.3 变量 n 变量是指在程序运行过程中其值可以改变变量是指在程序运行过程中其值可以改变 的量。的量。 n 在应用程序的运行过程中,变量用来存储在应用程序的运行过程中,变量用来存储 程序运行中的程序运行中的临时临时数据。数据。 n 一个变量必须有一个唯一的一个变量必须有一个唯一的变量名变量名和相应和相应 的的数据类型数据类型。 n 通过通过变量名变量名来引用一个变量,数据类型则来引用一个变量,数据类型则 决定了该变量的存。

    9、储方式和在内存中占据存储决定了该变量的存储方式和在内存中占据存储 单元的大小。单元的大小。 23 n1.变量名的命名规则变量名的命名规则 q只能由字母、数字和下划线组成,首字符必须是字母只能由字母、数字和下划线组成,首字符必须是字母 q长度不超过长度不超过255个字符个字符 q在作用域内必须唯一在作用域内必须唯一 q不得包括点号和用于类型说明的字符:不得包括点号和用于类型说明的字符: % & ! q不得使用系统保留字不得使用系统保留字 24 复习上次课内容复习上次课内容 nVB中的数据类型中的数据类型 n三种常量三种常量 n变量变量 25 变量的声名语句格式为:变量的声名语句格式为: Dim 。

    10、Dim 1 As As ,变量名变量名2 As 2 As 2 PublicPublic PrivatePrivate StaticStatic 26 区别区别 作用范围作用范围 作用范围作用范围定义方法定义方法定义位置定义位置 局部变量局部变量当前过程当前过程Dim|Static在过程内在过程内 窗体窗体/模块变量模块变量 当前窗体或模块中当前窗体或模块中 的所有过程的所有过程 Private|Di m 在窗体或模块在窗体或模块 的通用部分的通用部分 全局变量全局变量工程内的所有过程工程内的所有过程Public 在模块的通用在模块的通用 部分部分 三种不同作用域的变量对比 27 例如例如: D。

    11、im ab As integer , sum As single 或或 Dim ab%, sum! 注意:定义一个变量后,如果用户不给赋值, 系统会自动为该变量赋予一个初始值 数值型:0 变长字符串:空串 定长字符串:n个空格 逻辑型:False 28 n如果在如果在VB程序开头出现程序开头出现 EXPLICIT(显式声(显式声 明),则所有变量必须先定义后使用明),则所有变量必须先定义后使用 n n 如果没有如果没有EXPLICIT,则变量可以不经过定义,则变量可以不经过定义 就使用,此时,该变量为变体型就使用,此时,该变量为变体型 29 隐式声明 直接直接使用使用一个变量,并在该变量名的一。

    12、个变量,并在该变量名的尾尾 部部加上加上类型说明符类型说明符来标识该变量的类型。来标识该变量的类型。 % & % & ! # # 货币型货币型 $ $字符串型字符串型 30 字符串问题 n字符串的类型为字符串的类型为string n缺省缺省为为变长变长 n也可以定义为也可以定义为定长定长 dim str as string*3 31 n1、若、若X=30274757,则变量声明时不能将其,则变量声明时不能将其 声明为(声明为( ) nA:Integer nB:Variant nC:Long nD:Single A 32 n2、下列类型的变量中占用的字节数最小的是、下列类型的变量中占用的字节数最。

    13、小的是 ( ) A: Integer B: Single C: Date D: Byte D 33 n3、下列声明语句中存在变体变量的是(、下列声明语句中存在变体变量的是( ) nA:Dim a,b as Integer nB:Dim a as String nC:Static a as Integer nD:Public a as Currency A 34 n4、可以用( )语句来定义符号常量。 n5、可以在常量的后面加上类型说明符以显示 常量的类型,可以用( )表示字符串型常量。 n6、下列是合法的VB变量名的是( ) nA:a2 B:2a C:int sum D:2*a Const $ A n7、下列各项不是VB基本数据类型的是( ) A:Char B:String C:Integer D:Double A。

    展开全文
  • 正则表达式简易讲义

    2016-10-12 11:22:43
    正则表达式是烦琐的,但它是强大的,除非您以前使用过正则表达式,否则您可能不熟悉此术语。本文提供了正则表达式简易的学习讲义并且把开发中常用的一些正则表达式整理了一下,包括校验数字、字符、一些特殊的需求...
  • 《JavaScript 闯关记》之正则表达式

    千次阅读 2016-10-17 22:34:30
    由于本课程的核心是 JavaScript,所以本文着重讲解了「正则表达式」在 JavaScript 中的用法,并未深入「正则表达式」的具体细节。如果您尚不了解「正则表达式」,强烈推荐您先学习 正则表达式30分钟入门教程 ...
  • JS实用的正则表达式大全
  • JS正则表达式

    2012-10-09 13:27:15
    JS正则表达式大全 正则表达式中的特殊字符 字符 含意 \ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后/\b/,转意为匹配一个单词的...
  • 正则表达式学习

    2012-08-27 10:35:31
    随着对UNIX和LINUX熟悉程度的不断加深,需要经常接触到正则表达式这个领域。使用shell时,从一个文件中抽取多于一个字符串将会很麻烦。例如,在一个文本中抽取一个词,它的头两个字符是大写的,后面紧跟四个数字。...
  • 正则表达式大全

    2013-05-27 15:16:47
    正则表达式中的特殊字符 字符 含意 \ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后/\b/,转意为匹配一个单词的边界。  -或-  ...
  • js正则表达式

    2011-08-14 20:24:21
    正则表达式中的特殊字符 字符 含意 \ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后/\b/,转意为匹配一个单词的边界。 -或- 对正则...
  • Js正则表达式

    2011-05-20 19:03:00
    正则表达式中的特殊字符 字符 含意 / 做为转意,即通常在"/"后面的字符不按原来意义解释,如/b/匹配字符"b",当b前面加了反斜杆后//b/,转意为匹配一个单词的边界。 -或- 对正则表达式功能字符的...
  • JavaScript正则表达式

    千次阅读 2012-06-07 20:39:22
    字符 含意 ...做为转意,即通常在"\...对正则表达式功能字符的还原,如"*"匹配它前面元字符0次或多次,/a*/将匹配a,aa,aaa,加了"\"后,/a\*/将只匹配"a*"。 ^ 匹配一个输入或一行的开头,/^a/匹配
  • 想必你也了解正则的重要性,在我看来正则表达式是衡量程序员水平的一个侧面标准。 关于正则表达式的教程,网上也有很多,相信你也看了一些。 与之不同的是,本文的目的是希望所有认真读完的童鞋们,都有实质性的...
  • JS正则表达式大全

    2017-10-25 15:36:33
    正则表达式中的特殊字符 字符 含意 ...\ 做为转意,即通常在"\"后面的字符不按原来意义解释,如/b/匹配字符"b",当...对正则表达式功能字符的还原,如"*"匹配它前面元字符0次或多次,/a*/将匹配a,aa,aaa,加了"\
  • 想必你也了解正则的重要性,在我看来正则表达式是衡量程序员水平的一个侧面标准。 关于正则表达式的教程,网上也有很多,相信你也看了一些。 与之不同的是,本文的目的是希望所有认真读完的童鞋们,都有实质性的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,604
精华内容 5,041
关键字:

下列正确的日期型表达式是