Práticas recomendadas para expressões regulares no .NET

O mecanismo de expressões regulares no .NET é uma ferramenta poderosa e repleta de recursos que processa o texto com base em correspondências de padrões em vez de em comparar e corresponder o texto literal. Na maioria dos casos, ele realiza a correspondência de padrões de forma rápida e eficiente. No entanto, em alguns casos, o mecanismo de expressões regulares pode parecer ser muito lento. Em casos extremos, pode até mesmo parecer parar de responder enquanto processa uma entrada relativamente pequena em um período de horas ou até mesmo dias.

Este artigo descreve algumas das práticas recomendadas que os desenvolvedores podem adotar para garantir que as expressões regulares obtenham o máximo de desempenho.

Aviso

Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions passam um tempo limite.

Considere a fonte de entrada

Em geral, as expressões regulares podem aceitar dois tipos de entrada: restrita e não restrita. Uma entrada restrita é o texto proveniente de uma fonte conhecida ou confiável e segue um formato predefinido. Uma entrada irrestrita é um texto proveniente de uma fonte não confiável como um usuário web e que não pode seguir um formato predefinido ou esperado.

Os padrões de expressões regulares geralmente são escritos para corresponder as entradas válidas. Ou seja, os desenvolvedores examinam o texto que desejam corresponder e escrevem um padrão de expressão regular que corresponde a ele. Os desenvolvedores então determinam se esse padrão requer correção ou uma elaboração adicional testando-o com vários itens de entrada válidos. Quando o padrão corresponde a todas as entradas válidas previstas, é declarado como pronto para produção e pode ser incluído em um aplicativo final. Esse método torna um padrão de expressão regular adequado para corresponder uma entrada restrita. No entanto, não é adequado para corresponder à entrada irrestrita.

Para corresponder a uma entrada irrestrita, uma expressão regular deve conseguir manipular três tipos de texto de forma eficiente:

  • Texto que corresponda ao padrão da expressão regular.
  • Texto que não corresponda ao padrão da expressão regular.
  • Texto que quase corresponda ao padrão da expressão regular.

O último tipo de texto é particularmente problemático para uma expressão regular que foi escrita para lidar com entradas restritas. Se essa expressão regular também depender de retrocesso abrangente, o mecanismo de expressões regulares poderá gastar um período fora do normal (em alguns casos, muitas horas ou dias) processando texto aparentemente inócuo.

Aviso

O exemplo a seguir usa uma expressão regular que é sujeita a rastreamento inverso excessivo e que pode rejeitar endereços de email válidos. Você não deve usá-la em uma rotina de validação de email. Se você desejar uma expressão regular que valida endereços de email, confira Como verificar se cadeias de caracteres estão em um formato de email válido.

Por exemplo, pense em uma expressão regular usada, mas problemática, para validar o alias de um endereço de email. A expressão regular ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ é gravada para processar o que é considerado um endereço de email válido. Um endereço de email válido consiste em um caractere alfanumérico seguido por zero ou mais caracteres que podem ser alfanuméricos, pontos ou hifens. A expressão regular deve terminar com um caractere alfanumérico. No entanto, como mostra o exemplo a seguir, embora esta expressão regular identifique entradas válidas com facilidade, seu desempenho é muito ineficiente ao processar uma entrada quase válida:

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      Stopwatch sw;
      string[] addresses = { "AAAAAAAAAAA@contoso.com",
                             "AAAAAAAAAAaaaaaaaaaa!@contoso.com" };
      // The following regular expression should not actually be used to
      // validate an email address.
      string pattern = @"^[0-9A-Z]([-.\w]*[0-9A-Z])*$";
      string input;

      foreach (var address in addresses) {
         string mailBox = address.Substring(0, address.IndexOf("@"));
         int index = 0;
         for (int ctr = mailBox.Length - 1; ctr >= 0; ctr--) {
            index++;

            input = mailBox.Substring(ctr, index);
            sw = Stopwatch.StartNew();
            Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
            sw.Stop();
            if (m.Success)
               Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                 index, m.Value, sw.Elapsed);
            else
               Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                 index, input, sw.Elapsed);
         }
         Console.WriteLine();
      }
   }
}

// The example displays output similar to the following:
//     1. Matched '                        A' in 00:00:00.0007122
//     2. Matched '                       AA' in 00:00:00.0000282
//     3. Matched '                      AAA' in 00:00:00.0000042
//     4. Matched '                     AAAA' in 00:00:00.0000038
//     5. Matched '                    AAAAA' in 00:00:00.0000042
//     6. Matched '                   AAAAAA' in 00:00:00.0000042
//     7. Matched '                  AAAAAAA' in 00:00:00.0000042
//     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
//     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
//    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
//    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
//
//     1. Failed  '                        !' in 00:00:00.0000447
//     2. Failed  '                       a!' in 00:00:00.0000071
//     3. Failed  '                      aa!' in 00:00:00.0000071
//     4. Failed  '                     aaa!' in 00:00:00.0000061
//     5. Failed  '                    aaaa!' in 00:00:00.0000081
//     6. Failed  '                   aaaaa!' in 00:00:00.0000126
//     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
//     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
//     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
//    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
//    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
//    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
//    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
//    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
//    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
//    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
//    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
//    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
//    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
//    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
//    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372
Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim sw As Stopwatch
        Dim addresses() As String = {"AAAAAAAAAAA@contoso.com",
                                   "AAAAAAAAAAaaaaaaaaaa!@contoso.com"}
        ' The following regular expression should not actually be used to 
        ' validate an email address.
        Dim pattern As String = "^[0-9A-Z]([-.\w]*[0-9A-Z])*$"
        Dim input As String

        For Each address In addresses
            Dim mailBox As String = address.Substring(0, address.IndexOf("@"))
            Dim index As Integer = 0
            For ctr As Integer = mailBox.Length - 1 To 0 Step -1
                index += 1
                input = mailBox.Substring(ctr, index)
                sw = Stopwatch.StartNew()
                Dim m As Match = Regex.Match(input, pattern, RegexOptions.IgnoreCase)
                sw.Stop()
                if m.Success Then
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed)
                Else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed)
                End If
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays output similar to the following:
'     1. Matched '                        A' in 00:00:00.0007122
'     2. Matched '                       AA' in 00:00:00.0000282
'     3. Matched '                      AAA' in 00:00:00.0000042
'     4. Matched '                     AAAA' in 00:00:00.0000038
'     5. Matched '                    AAAAA' in 00:00:00.0000042
'     6. Matched '                   AAAAAA' in 00:00:00.0000042
'     7. Matched '                  AAAAAAA' in 00:00:00.0000042
'     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
'     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
'    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
'    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
'    
'     1. Failed  '                        !' in 00:00:00.0000447
'     2. Failed  '                       a!' in 00:00:00.0000071
'     3. Failed  '                      aa!' in 00:00:00.0000071
'     4. Failed  '                     aaa!' in 00:00:00.0000061
'     5. Failed  '                    aaaa!' in 00:00:00.0000081
'     6. Failed  '                   aaaaa!' in 00:00:00.0000126
'     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
'     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
'     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
'    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
'    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
'    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
'    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
'    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
'    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
'    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
'    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
'    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
'    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
'    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
'    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372

Conforme mostrado na saída do exemplo anterior, o mecanismo de expressões regulares processa o alias de email válido quase ao mesmo tempo, independentemente do seu comprimento. Por outro lado, quando o endereço de email quase válido tem mais de cinco caracteres, o tempo de processamento chega a dobrar para cada caractere adicional na cadeia de caracteres. Portanto, uma cadeia de 28 caracteres quase válidos levaria mais de uma hora para ser processada e uma cadeia de 33 caracteres quase válidos demoraria quase um dia.

Como essa expressão regular foi desenvolvida considerando apenas a correspondência com o formato da entrada, ela não leva em consideração entradas que não correspondem ao padrão. Isso, por sua vez, pode permitir que entradas irrestritas quase correspondentes ao padrão da expressão regular prejudiquem significativamente o desempenho.

Para resolver este problema, você pode fazer o seguinte:

  • Ao desenvolver um padrão, você deve considerar como o retrocesso pode afetar o desempenho do mecanismo de expressões regulares, especialmente se a expressão regular for criada para processar entradas sem restrição. Para obter mais informações, consulte a seção Tome conta do retrocesso.

  • Teste rigorosamente sua expressão regular usando entradas inválidas, quase válidas e válidas. Você pode usar Rex para gerar entrada aleatoriamente para uma expressão regular específica. Rex é uma ferramenta de exploração de expressão regular da Microsoft Research.

Trate a instanciação de objetos adequadamente

No meio do modelo de objeto de expressões regulares do .NET está a classe System.Text.RegularExpressions.Regex, a qual representa o mecanismo de expressões regulares. Muitas vezes, o maior fator individual que afeta o desempenho das expressões regulares é a forma como o mecanismo Regex é usado. Definir uma expressão regular envolve o acoplamento vigoroso do mecanismo de expressões regulares com um padrão de expressão regular. Esse processo de acoplamento, seja envolvendo a instanciação de um objeto Regex ao passar para seu constructo um padrão de expressão regular ou chamar um método estático ao passar o padrão de expressão regular e a cadeia de caracteres a ser analisada, é necessariamente caro.

Observação

Para uma análise detalhada das implicações de desempenho do uso de expressões regulares interpretadas e compiladas, confira Otimizando o desempenho de expressões regulares - Parte II: controle o rastreamento inverso no blog da equipe BCL.

É possível acoplar o mecanismo de expressões regulares com um padrão específico de expressão regular e usar o mecanismo para fazer a correspondência com o texto de várias maneiras:

  • Você pode chamar um método estático de correspondência de padrões, Regex.Match(String, String). Este método não requer a instanciação de um objeto de expressão regular.

  • Você pode criar uma instância de um objeto Regex e chamar um método de correspondência de padrões de instância de uma expressão regular interpretada, que é o método padrão para associar o mecanismo de expressão regular a um padrão de expressão regular. Ele ocorre quando um objeto Regex é instanciado sem um argumento options que inclua o sinalizador Compiled.

  • Você pode criar uma instância de um objeto Regex e chamar um método instanciado de correspondência de padrões de uma expressão regular compilada. Os objetos de expressão regular representam padrões compilados quando um objeto Regex é instanciado com um argumento options que inclui o sinalizador Compiled.

  • Você pode criar um objeto Regex com propósito especial, que é acoplado a um padrão de expressão regular específico, compilá-lo e salvá-lo em um assembly autônomo. Você pode chamar o método Regex.CompileToAssembly para compilá-lo e salvá-lo.

A forma específica como você chama métodos de correspondência de expressões regulares pode ter impacto significativo em seu aplicativo. As seções a seguir abordam quando usar chamadas de métodos estáticos, expressões regulares interpretadas e expressões regulares compiladas para melhorar o desempenho do seu aplicativo.

Importante

A forma da chamada de método (estático, interpretada, compilada) afeta o desempenho se a mesma expressão regular é usada repetidamente em chamadas de método ou se um aplicativo faz uso extensivo de objetos de expressão regular.

Expressões regulares estáticas

Os métodos de expressões regulares estáticas são recomendados como uma alternativa a criar repetidamente instâncias de um objeto de expressão regular com a mesma expressão regular. Diferentemente dos padrões de expressão regular usados pelos objetos de expressão regular, os códigos de operação ou a Common Intermediate Language (CIL) compilada dos padrões usados nas chamadas de método estático são armazenados em cache internamente pelo mecanismo de expressão regular.

Por exemplo, um manipulador de eventos chama frequentemente outro método para validar a entrada do usuário. Este exemplo é mostrado no código a seguir, no qual o evento Button de um controle Click é usado para chamar um método IsValidCurrency, o qual verifica se o usuário inseriu um símbolo de moeda seguido por pelo menos um dígito decimal.

public void OKButton_Click(object sender, EventArgs e)
{
   if (! String.IsNullOrEmpty(sourceCurrency.Text))
      if (RegexLib.IsValidCurrency(sourceCurrency.Text))
         PerformConversion();
      else
         status.Text = "The source currency value is invalid.";
}
Public Sub OKButton_Click(sender As Object, e As EventArgs) _
           Handles OKButton.Click

    If Not String.IsNullOrEmpty(sourceCurrency.Text) Then
        If RegexLib.IsValidCurrency(sourceCurrency.Text) Then
            PerformConversion()
        Else
            status.Text = "The source currency value is invalid."
        End If
    End If
End Sub

Uma implementação ineficiente do método IsValidCurrency é mostrada no exemplo a seguir:

Observação

Observe que cada chamada de método cria uma nova instância de um objeto Regex com o mesmo padrão. Isso, por sua vez, significa que o padrão de expressão regular deve ser recompilado toda vez que o método é chamado.

using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      Regex currencyRegex = new Regex(pattern);
      return currencyRegex.IsMatch(currencyValue);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Dim currencyRegex As New Regex(pattern)
        Return currencyRegex.IsMatch(currencyValue)
    End Function
End Module

Você deve substituir esse código ineficiente por uma chamada ao método estático Regex.IsMatch(String, String). Esse método elimina a necessidade de criar uma instância de um objeto Regex toda vez que você deseja chamar um método de correspondência de padrões e permite que o mecanismo de expressões regulares recupere uma versão compilada da expressão regular do cache.

using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      return Regex.IsMatch(currencyValue, pattern);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Return Regex.IsMatch(currencyValue, pattern)
    End Function
End Module

Por padrão, os 15 padrões de expressões regulares estáticas usados mais recentemente são armazenados no cache. Para aplicativos que requerem um número maior de expressões regulares estáticas armazenadas no cache, o tamanho do cache pode ser ajustado com a definição da propriedade Regex.CacheSize.

A expressão regular \p{Sc}+\s*\d+ que é usada neste exemplo verifica que a cadeia de caracteres de entrada tem um símbolo de moeda e pelo menos um dígito decimal. O padrão é definido conforme mostrado na tabela a seguir:

Padrão Descrição
\p{Sc}+ Corresponde a um ou mais caracteres no símbolo Unicode, categoria de moeda.
\s* Corresponde a zero ou mais caracteres de espaço em branco.
\d+ Corresponde a um ou mais dígitos decimais.

Expressões regulares compiladas vs. interpretadas

Os padrões de expressões regulares que não são associados ao mecanismo de expressões regulares com a especificação da opção Compiled são interpretados. Quando um objeto de expressão regular é instanciado, o mecanismo de expressões regulares converte a expressão regular em um conjunto de códigos de operação. Quando um método de instância é chamado, os códigos de operação são convertidos em CIL e executados pelo compilador JIT. Da mesma forma, quando um método estático de expressão regular é chamado e a expressão regular não pode ser encontrada no cache, o mecanismo de expressão regular converte a expressão regular em um conjunto de códigos operacionais e os armazena no cache. Em seguida, ele converte esses códigos de operação em CIL para que o compilador JIT possa executá-los. As expressões regulares interpretadas reduzem o tempo de inicialização ao custo de um tempo de execução mais lento. Por conta desse processo, elas são melhor utilizadas quando a expressão regular é usada em um pequeno número de chamadas de método ou se o número exato de chamadas para métodos de expressão regular é desconhecido, mas com a expectativa de ser pequeno. À medida que número de chamadas de método aumenta, o ganho de desempenho do tempo de inicialização reduzido é superado pela velocidade de execução mais lenta.

Os padrões de expressões regulares associados ao mecanismo de expressões regulares com a especificação da opção Compiled são compilados. Portanto, quando um objeto de expressão regular criar uma instância ou quando um método de expressão regular estática é chamado e a expressão regular não pode ser encontrada no cache, o mecanismo de expressões regulares converte a expressão regular para um conjunto intermediário de códigos de operação. Esses códigos são então convertidos em CIL. Quando um método é chamado, o compilador JIT executa a CIL. Em contraste com as expressões regulares interpretadas, as expressões regulares compiladas aumentam o tempo de inicialização, mas executam os métodos individuais de correspondência padrão de forma mais rápida. Como resultado, o benefício de desempenho que resulta da compilação da expressão regular aumenta em proporção ao número de métodos de expressões regulares chamados.

Para resumir, recomendamos que você use expressões regulares interpretadas ao chamar métodos de expressões regulares com uma expressão regular específica com relativa pouca frequência. Você deve usar expressões regulares compiladas ao chamar os métodos de expressões regulares com uma expressão regular específica com uma relativa frequência. É difícil determinar o limite exato em que as velocidades de execução mais lentas das expressões regulares interpretadas ultrapassam os ganhos proporcionados pelo tempo de inicialização reduzido ou o limite em que os tempos de inicialização mais lentos das expressões regulares compiladas ultrapassam os ganhos gerados por suas velocidades de execução mais rápida. O limite depende de vários fatores, incluindo a complexidade da expressão regular e dos dados específicos que são processados. Para determinar se expressões regulares interpretadas ou compiladas oferecem o melhor desempenho para seu cenário de aplicativo específico, você pode usar a classe Stopwatch para comparar seu tempo de execução.

O exemplo a seguir compara o desempenho de expressões regulares compiladas e interpretadas ao ler as 10 primeiras frases e ao ler todas as frases no texto The Financier de Theodore Dreiser. Conforme mostrado pela saída do exemplo, quando apenas dez chamadas são feitas para os métodos correspondentes de expressão regular, uma expressão regular interpretada oferece um desempenho melhor do que uma expressão regular compilada. No entanto, uma expressão regular compilada oferece melhor desempenho quando um grande número de chamadas (neste caso, mais 13.000) é feito.

using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";
      Stopwatch sw;
      Match match;
      int ctr;

      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      // Read first ten sentences with interpreted regex.
      Console.WriteLine("10 Sentences with Interpreted Regex:");
      sw = Stopwatch.StartNew();
      Regex int10 = new Regex(pattern, RegexOptions.Singleline);
      match = int10.Match(input);
      for (ctr = 0; ctr <= 9; ctr++) {
         if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
         else
            break;
      }
      sw.Stop();
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

      // Read first ten sentences with compiled regex.
      Console.WriteLine("10 Sentences with Compiled Regex:");
      sw = Stopwatch.StartNew();
      Regex comp10 = new Regex(pattern,
                   RegexOptions.Singleline | RegexOptions.Compiled);
      match = comp10.Match(input);
      for (ctr = 0; ctr <= 9; ctr++) {
         if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
         else
            break;
      }
      sw.Stop();
      Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed);

      // Read all sentences with interpreted regex.
      Console.WriteLine("All Sentences with Interpreted Regex:");
      sw = Stopwatch.StartNew();
      Regex intAll = new Regex(pattern, RegexOptions.Singleline);
      match = intAll.Match(input);
      int matches = 0;
      while (match.Success) {
         matches++;
         // Do nothing with the match except get the next match.
         match = match.NextMatch();
      }
      sw.Stop();
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);

      // Read all sentences with compiled regex.
      Console.WriteLine("All Sentences with Compiled Regex:");
      sw = Stopwatch.StartNew();
      Regex compAll = new Regex(pattern,
                      RegexOptions.Singleline | RegexOptions.Compiled);
      match = compAll.Match(input);
      matches = 0;
      while (match.Success) {
         matches++;
         // Do nothing with the match except get the next match.
         match = match.NextMatch();
      }
      sw.Stop();
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);
   }
}
// The example displays the following output:
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0047491
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0141872
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1929928
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7635869
//
//       >compare1
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0046914
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0143727
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1514100
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7432921
Imports System.Diagnostics
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]"
        Dim sw As Stopwatch
        Dim match As Match
        Dim ctr As Integer

        Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
        Dim input As String = inFile.ReadToEnd()
        inFile.Close()

        ' Read first ten sentences with interpreted regex.
        Console.WriteLine("10 Sentences with Interpreted Regex:")
        sw = Stopwatch.StartNew()
        Dim int10 As New Regex(pattern, RegexOptions.SingleLine)
        match = int10.Match(input)
        For ctr = 0 To 9
            If match.Success Then
                ' Do nothing with the match except get the next match.
                match = match.NextMatch()
            Else
                Exit For
            End If
        Next
        sw.Stop()
        Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

        ' Read first ten sentences with compiled regex.
        Console.WriteLine("10 Sentences with Compiled Regex:")
        sw = Stopwatch.StartNew()
        Dim comp10 As New Regex(pattern,
                     RegexOptions.SingleLine Or RegexOptions.Compiled)
        match = comp10.Match(input)
        For ctr = 0 To 9
            If match.Success Then
                ' Do nothing with the match except get the next match.
                match = match.NextMatch()
            Else
                Exit For
            End If
        Next
        sw.Stop()
        Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

        ' Read all sentences with interpreted regex.
        Console.WriteLine("All Sentences with Interpreted Regex:")
        sw = Stopwatch.StartNew()
        Dim intAll As New Regex(pattern, RegexOptions.SingleLine)
        match = intAll.Match(input)
        Dim matches As Integer = 0
        Do While match.Success
            matches += 1
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
        Loop
        sw.Stop()
        Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)

        ' Read all sentences with compiled regex.
        Console.WriteLine("All Sentences with Compiled Regex:")
        sw = Stopwatch.StartNew()
        Dim compAll As New Regex(pattern,
                       RegexOptions.SingleLine Or RegexOptions.Compiled)
        match = compAll.Match(input)
        matches = 0
        Do While match.Success
            matches += 1
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
        Loop
        sw.Stop()
        Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)
    End Sub
End Module
' The example displays output like the following:
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0047491
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0141872
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1929928
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7635869
'       
'       >compare1
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0046914
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0143727
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1514100
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7432921

O padrão de expressão regular usado neste exemplo, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], é definido como mostrado na tabela a seguir:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\w+ Corresponde a um ou mais caracteres de palavra.
(\r?\n)|,?\s) Corresponde a um zero ou um retorno de carro seguido por um caractere de nova linha, ou zero ou uma vírgula seguida por um caractere de espaço em branco.
(\w+((\r?\n)|,?\s))* Corresponde a zero ou mais ocorrências de um ou mais caracteres de palavra que são seguidos por zero ou por retornos de carro e por um caractere de nova linha ou por zero ou uma vírgula seguida por um caractere de espaço em branco.
\w+ Corresponde a um ou mais caracteres de palavra.
[.?:;!] Corresponde a um ponto, um ponto de interrogação, dois-pontos, ponto e vírgula ou ponto de exclamação.

Expressões regulares: compiladas em um assembly

O .NET também permite criar um assembly que contém expressões regulares compiladas. Esse recurso move a ocorrência de desempenho da compilação de expressões regulares do tempo de execução para o tempo de design. No entanto, também envolve algum trabalho extra. Você deve definir as expressões regulares com antecedência e compilá-las para um assembly. O compilador pode então fazer referência a esse assembly ao compilar código-fonte que usa expressões regulares do assembly. Cada expressão regular compilada no assembly é representada por uma classe que deriva de Regex.

Para compilar expressões regulares para um assembly, você chama o método Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) e passa por uma matriz de objetos RegexCompilationInfo e um objeto AssemblyName. Os objetos RegexCompilationInfo representam as expressões regulares a serem compiladas, e o objeto AssemblyName que contém informações sobre a assembly a ser criada.

Recomendamos que você compile as expressões regulares em um assembly nestas situações:

  • Se você é um desenvolvedor de componentes que deseja criar uma biblioteca de expressões regulares reutilizáveis.
  • Se você quer que seus métodos de correspondência de padrões das expressões regulares sejam chamados de forma indefinida, de uma ou duas vezes a milhares ou dezenas de milhares de vezes. Ao contrário de expressões regulares compiladas ou interpretadas, as expressões regulares que são compiladas para separar assemblies oferecem desempenho consistente, independente do número de chamadas de método.

Se estiver usando expressões regulares compiladas para otimizar o desempenho, não use reflexão para criar o assembly, carregar o mecanismo de expressões regulares e executar os métodos de correspondência de padrões. Para evitar a reflexão, você não deve criar padrões de expressões regulares dinamicamente, e especificar as opções de correspondência de padrões (como correspondência de padrões sem diferenciação de maiúsculas e minúsculas) no momento em que o assembly é criado. Exige também que você separe o código que cria o assembly do código que usa a expressão regular.

O exemplo a seguir mostra como criar um assembly que contém uma expressão regular compilada. Ele cria um assembly chamado RegexLib.dll com uma única classe de expressão regular SentencePattern. Essa classe contém o padrão de expressão regular correspondente à sentença usado na seção Expressões Regulares Compiladas x Interpretadas.

using System;
using System.Reflection;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      RegexCompilationInfo SentencePattern =
                           new RegexCompilationInfo(@"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                    RegexOptions.Multiline,
                                                    "SentencePattern",
                                                    "Utilities.RegularExpressions",
                                                    true);
      RegexCompilationInfo[] regexes = { SentencePattern };
      AssemblyName assemName = new AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null");
      Regex.CompileToAssembly(regexes, assemName);
   }
}
Imports System.Reflection
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim SentencePattern As New RegexCompilationInfo("\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                        RegexOptions.Multiline,
                                                        "SentencePattern",
                                                        "Utilities.RegularExpressions",
                                                        True)
        Dim regexes() As RegexCompilationInfo = {SentencePattern}
        Dim assemName As New AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null")
        Regex.CompileToAssembly(regexes, assemName)
    End Sub
End Module

Quando o exemplo é compilado em um executável e executado, ele cria um assembly denominado RegexLib.dll. Uma classe Utilities.RegularExpressions.SentencePattern derivada de Regex representa a expressão regular. O exemplo a seguir usa a expressão regular compilada para extrair as sentenças do texto The Financier de Theodore Dreiser:

using System;
using System.IO;
using System.Text.RegularExpressions;
using Utilities.RegularExpressions;

public class Example
{
   public static void Main()
   {
      SentencePattern pattern = new SentencePattern();
      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      MatchCollection matches = pattern.Matches(input);
      Console.WriteLine("Found {0:N0} sentences.", matches.Count);
   }
}
// The example displays the following output:
//      Found 13,443 sentences.
Imports System.IO
Imports System.Text.RegularExpressions
Imports Utilities.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As New SentencePattern()
        Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
        Dim input As String = inFile.ReadToEnd()
        inFile.Close()

        Dim matches As MatchCollection = pattern.Matches(input)
        Console.WriteLine("Found {0:N0} sentences.", matches.Count)
    End Sub
End Module
' The example displays the following output:
'      Found 13,443 sentences.

Tome conta do retrocesso

Normalmente, o mecanismo de expressões regulares usa progressão linear para percorrer uma cadeia de caracteres de entrada e compará-la a uma expressão regular padrão. No entanto, quando quantificadores indefinidos, como *, + e ? são usados em um padrão de expressão regular, o mecanismo de expressões regulares pode ignorar uma parte das correspondências parciais com êxito e retornar ao estado salvo anteriormente para pesquisar uma correspondência com êxito para o padrão inteiro. Esse processo é conhecido como retrocesso.

Dica

Para obter mais informações sobre o retrocesso, consulte Detalhes do comportamento de expressões regulares e Retrocesso. Para obter discussões detalhadas sobre retrocesso, consulte as Melhorias de Expressão Regular no .NET 7 e Otimizando o Desempenho de Expressões Regulares postagens no blog.

O suporte ao retrocesso proporciona poder e flexibilidade às expressões regulares. Ele também coloca a responsabilidade por controlar o funcionamento do mecanismo de expressões regulares nas mãos dos desenvolvedores de expressões regulares. Como os desenvolvedores geralmente não estão cientes dessa responsabilidade, o uso indevido do retrocesso ou a confiança no retrocesso excessivo geralmente exerce o papel mais significativo na degradação do desempenho da expressão regular. Em um cenário de pior caso, o tempo de execução pode dobrar para cada caractere adicional na cadeia de caracteres de entrada. Na verdade, com o uso de rastreamento inverso excessivo, é fácil criar o equivalente programático de um loop infinito se a entrada quase corresponder ao padrão de expressão regular. O mecanismo de expressão regular pode levar horas ou até dias para processar uma cadeia de caracteres de entrada relativamente curta.

Normalmente, os aplicativos pagam uma multa de desempenho por usar o rastreamento inverso, mesmo não sendo essencial para uma correspondência. Por exemplo, a expressão regular \b\p{Lu}\w*\b corresponde a todas as palavras que começam com um caractere maiúsculo, como mostra a tabela a seguir:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\p{Lu} Corresponde a um caractere minúsculo.
\w* Corresponde a zero ou mais caracteres de palavra.
\b Termina a correspondência em um limite de palavra.

Como um limite de palavra não é o mesmo que ou um subconjunto de, um caractere de palavra, não há nenhuma possibilidade de o mecanismo de expressões regulares cruzar um limite de palavra ao corresponder caracteres de palavra. Portanto, para essa expressão regular, o rastreamento inverso nunca pode contribuir para o sucesso geral de qualquer correspondência. Ele só pode degradar o desempenho porque o mecanismo de expressão regular é forçado a salvar seu estado para cada correspondência preliminar bem-sucedida de um caractere de palavra.

Se você determinar que o retrocesso não é necessário, você pode desabilitá-lo de algumas maneiras:

  • Definindo a opção RegexOptions.NonBacktracking (introduzida no .NET 7). Para obter mais informações, confira Modo sem retrocesso.

  • Usando o elemento de linguagem (?>subexpression), conhecido como um grupo atômico. O exemplo a seguir analisa uma cadeia de caracteres de entrada usando duas expressões regulares. A primeira, \b\p{Lu}\w*\b, depende do retrocesso. A segunda, \b\p{Lu}(?>\w*)\b, desabilita o retrocesso. Conforme mostrado pela saída do exemplo, ambas produzem o mesmo resultado:

    using System;
    using System.Text.RegularExpressions;
    
    public class Example
    {
       public static void Main()
       {
          string input = "This this word Sentence name Capital";
          string pattern = @"\b\p{Lu}\w*\b";
          foreach (Match match in Regex.Matches(input, pattern))
             Console.WriteLine(match.Value);
    
          Console.WriteLine();
    
          pattern = @"\b\p{Lu}(?>\w*)\b";
          foreach (Match match in Regex.Matches(input, pattern))
             Console.WriteLine(match.Value);
       }
    }
    // The example displays the following output:
    //       This
    //       Sentence
    //       Capital
    //
    //       This
    //       Sentence
    //       Capital
    
    Imports System.Text.RegularExpressions
    
    Module Example
        Public Sub Main()
            Dim input As String = "This this word Sentence name Capital"
            Dim pattern As String = "\b\p{Lu}\w*\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
            Console.WriteLine()
    
            pattern = "\b\p{Lu}(?>\w*)\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
        End Sub
    End Module
    ' The example displays the following output:
    '       This
    '       Sentence
    '       Capital
    '       
    '       This
    '       Sentence
    '       Capital
    

Em muitos casos, o retrocesso é essencial para corresponder um padrão de expressão regular ao texto de entrada. No entanto, o retrocesso excessivo pode prejudicar severamente o desempenho e criar a impressão de que um aplicativo parou de responder. Em particular, esse problema ocorre quando quantificadores são aninhados e o texto que corresponde à subexpressão externa é um subconjunto do texto que corresponde à subexpressão interna.

Aviso

Além de evitar rastreamentos inversos excessivos, você deve usar o recurso de tempo limite para garantir que rastreamentos inversos excessivos não degradem severamente o desempenho da expressão regular. Para obter mais informações, confira a seção Use valores de tempo limite.

Por exemplo, o padrão de expressão regular ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ destina-se a corresponder a um número de peça que consiste em pelo menos um caractere alfanumérico. Todos os demais caracteres podem consistir em um caractere alfanumérico, um hífen, um sublinhado ou um ponto, embora o último caractere deva ser alfanumérico. Um cifrão termina o número da peça. Em alguns casos, esse padrão de expressão regular pode exibir um desempenho ruim porque os quantificadores estão aninhados e porque a subexpressão [0-9A-Z] é um subconjunto da subexpressão [-.\w]*.

Nesses casos, você pode otimizar o desempenho da expressão regular ao remover os quantificadores aninhados e substituir a subexpressão externa por uma declaração de lookahead ou lookbehind de largura zero. Asserções lookbehind e lookahead são âncoras. Elas não movem o ponteiro na cadeia de caracteres de entrada, mas fazem uma verificação para checar se uma condição especificada foi atendida. Por exemplo, a expressão regular do número de peça pode ser reescrita como ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Esse padrão de expressão regular é definido conforme mostrado na tabela a seguir:

Padrão Descrição
^ Começar a correspondência no início da cadeia de caracteres de entrada.
[0-9A-Z] Corresponde a um caractere alfanumérico. O número de peça deve consistir em pelo menos este caractere.
[-.\w]* Corresponde a zero ou mais ocorrências de qualquer caractere de palavra, hífen ou ponto.
\$ Corresponde a um cifrão.
(?<=[0-9A-Z]) Avalie atrás do sinal de cifrão final para garantir que o caractere anterior seja alfanumérico.
$ Finalizar a correspondência no final da cadeia de caracteres de entrada.

O exemplo a seguir mostra o uso dessa expressão regular para corresponder a uma matriz que contém possíveis números de blocos:

using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string pattern = @"^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$";
      string[] partNos = { "A1C$", "A4", "A4$", "A1603D$", "A1603D#" };

      foreach (var input in partNos) {
         Match match = Regex.Match(input, pattern);
         if (match.Success)
            Console.WriteLine(match.Value);
         else
            Console.WriteLine("Match not found.");
      }
   }
}
// The example displays the following output:
//       A1C$
//       Match not found.
//       A4$
//       A1603D$
//       Match not found.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$"
        Dim partNos() As String = {"A1C$", "A4", "A4$", "A1603D$",
                                    "A1603D#"}

        For Each input As String In partNos
            Dim match As Match = Regex.Match(input, pattern)
            If match.Success Then
                Console.WriteLine(match.Value)
            Else
                Console.WriteLine("Match not found.")
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       A1C$
'       Match not found.
'       A4$
'       A1603D$
'       Match not found.

A linguagem de expressões regulares no .NET inclui os seguintes elementos de linguagem que você pode usar para eliminar quantificadores aninhados. Para obter mais informações, consulte Constructos de agrupamento.

Elemento de linguagem Descrição
(?= subexpression ) Lookahead positivo de largura zero. Avalia à direita da posição atual para determinar se subexpression corresponde à cadeia de caracteres de entrada.
(?! subexpression ) Lookahead negativo de largura zero. Avalia à direita da posição atual para determinar se subexpression não corresponde à cadeia de caracteres de entrada.
(?<= subexpression ) Lookbehind positivo de largura zero. Avalia à esquerda da posição atual para determinar se subexpression corresponde à cadeia de caracteres de entrada.
(?<! subexpression ) Lookbehind negativo de largura zero. Avalia à esquerda da posição atual para determinar se subexpression não corresponde à cadeia de caracteres de entrada.

Use valores de tempo limite

Se suas expressões regulares processarem entradas quase correspondentes ao padrão da expressão regular, elas poderão frequentemente confiar no retrocesso excessivo, o que afeta significativamente o desempenho. Além de considerar cuidadosamente o uso de rastreamento inverso e testar a expressão regular contra entradas quase correspondentes, você deve sempre definir um valor de tempo limite para garantir que o impacto do rastreamento inverso excessivo, caso ocorra, seja minimizado.

O intervalo de tempo limite de expressão regular define o período que o mecanismo de expressão regular procurará por uma única correspondência antes de atingir o tempo limite. Dependendo do padrão de expressão regular e do texto de entrada, o tempo de execução pode exceder o intervalo de tempo limite especificado, mas não passa mais tempo no rastreamento inverso do que o intervalo de tempo limite especificado. O intervalo de tempo limite padrão é Regex.InfiniteMatchTimeout, o que significa que a expressão regular não terá tempo limite. Você pode substituir esse valor e definir um intervalo de tempo limite da seguinte maneira:

Se você tiver definido um intervalo de tempo limite e uma correspondência não for localizada no final do intervalo, o método de expressão regular gerará uma exceção RegexMatchTimeoutException. No manipulador de exceção, você pode optar por tentar fazer novamente a correspondência com um intervalo de tempo limite mais longo, abandonar a tentativa de correspondência e assumir que não há nenhuma correspondência ou abandonar a tentativa de correspondência e registrar as informações de exceção para análise futura.

O exemplo a seguir define um método GetWordData que instancia uma expressão regular com um intervalo de tempo limite de 350 milissegundos para calcular o número de palavras e o número médio de caracteres em uma palavra em um documento de texto. Se a operação de correspondência exceder o tempo limite, o intervalo de tempo limite aumenta em 350 milissegundos e o objeto de Regex criará uma nova instância. Se o novo intervalo de tempo limite exceder 1 segundo, o método gerará novamente a exceção no chamador.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      RegexUtilities util = new RegexUtilities();
      string title = "Doyle - The Hound of the Baskervilles.txt";
      try {
         var info = util.GetWordData(title);
         Console.WriteLine("Words:               {0:N0}", info.Item1);
         Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2);
      }
      catch (IOException e) {
         Console.WriteLine("IOException reading file '{0}'", title);
         Console.WriteLine(e.Message);
      }
      catch (RegexMatchTimeoutException e) {
         Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                           e.MatchTimeout.TotalMilliseconds);
      }
   }
}

public class RegexUtilities
{
   public Tuple<int, double> GetWordData(string filename)
   {
      const int MAX_TIMEOUT = 1000;   // Maximum timeout interval in milliseconds.
      const int INCREMENT = 350;      // Milliseconds increment of timeout.

      List<string> exclusions = new List<string>( new string[] { "a", "an", "the" });
      int[] wordLengths = new int[29];        // Allocate an array of more than ample size.
      string input = null;
      StreamReader sr = null;
      try {
         sr = new StreamReader(filename);
         input = sr.ReadToEnd();
      }
      catch (FileNotFoundException e) {
         string msg = String.Format("Unable to find the file '{0}'", filename);
         throw new IOException(msg, e);
      }
      catch (IOException e) {
         throw new IOException(e.Message, e);
      }
      finally {
         if (sr != null) sr.Close();
      }

      int timeoutInterval = INCREMENT;
      bool init = false;
      Regex rgx = null;
      Match m = null;
      int indexPos = 0;
      do {
         try {
            if (! init) {
               rgx = new Regex(@"\b\w+\b", RegexOptions.None,
                               TimeSpan.FromMilliseconds(timeoutInterval));
               m = rgx.Match(input, indexPos);
               init = true;
            }
            else {
               m = m.NextMatch();
            }
            if (m.Success) {
               if ( !exclusions.Contains(m.Value.ToLower()))
                  wordLengths[m.Value.Length]++;

               indexPos += m.Length + 1;
            }
         }
         catch (RegexMatchTimeoutException e) {
            if (e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT) {
               timeoutInterval += INCREMENT;
               init = false;
            }
            else {
               // Rethrow the exception.
               throw;
            }
         }
      } while (m.Success);

      // If regex completed successfully, calculate number of words and average length.
      int nWords = 0;
      long totalLength = 0;

      for (int ctr = wordLengths.GetLowerBound(0); ctr <= wordLengths.GetUpperBound(0); ctr++) {
         nWords += wordLengths[ctr];
         totalLength += ctr * wordLengths[ctr];
      }
      return new Tuple<int, double>(nWords, totalLength/nWords);
   }
}
Imports System.Collections.Generic
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim util As New RegexUtilities()
        Dim title As String = "Doyle - The Hound of the Baskervilles.txt"
        Try
            Dim info = util.GetWordData(title)
            Console.WriteLine("Words:               {0:N0}", info.Item1)
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2)
        Catch e As IOException
            Console.WriteLine("IOException reading file '{0}'", title)
            Console.WriteLine(e.Message)
        Catch e As RegexMatchTimeoutException
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds)
        End Try
    End Sub
End Module

Public Class RegexUtilities
    Public Function GetWordData(filename As String) As Tuple(Of Integer, Double)
        Const MAX_TIMEOUT As Integer = 1000  ' Maximum timeout interval in milliseconds.
        Const INCREMENT As Integer = 350     ' Milliseconds increment of timeout.

        Dim exclusions As New List(Of String)({"a", "an", "the"})
        Dim wordLengths(30) As Integer        ' Allocate an array of more than ample size.
        Dim input As String = Nothing
        Dim sr As StreamReader = Nothing
        Try
            sr = New StreamReader(filename)
            input = sr.ReadToEnd()
        Catch e As FileNotFoundException
            Dim msg As String = String.Format("Unable to find the file '{0}'", filename)
            Throw New IOException(msg, e)
        Catch e As IOException
            Throw New IOException(e.Message, e)
        Finally
            If sr IsNot Nothing Then sr.Close()
        End Try

        Dim timeoutInterval As Integer = INCREMENT
        Dim init As Boolean = False
        Dim rgx As Regex = Nothing
        Dim m As Match = Nothing
        Dim indexPos As Integer = 0
        Do
            Try
                If Not init Then
                    rgx = New Regex("\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval))
                    m = rgx.Match(input, indexPos)
                    init = True
                Else
                    m = m.NextMatch()
                End If
                If m.Success Then
                    If Not exclusions.Contains(m.Value.ToLower()) Then
                        wordLengths(m.Value.Length) += 1
                    End If
                    indexPos += m.Length + 1
                End If
            Catch e As RegexMatchTimeoutException
                If e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT Then
                    timeoutInterval += INCREMENT
                    init = False
                Else
                    ' Rethrow the exception.
                    Throw
                End If
            End Try
        Loop While m.Success

        ' If regex completed successfully, calculate number of words and average length.
        Dim nWords As Integer
        Dim totalLength As Long

        For ctr As Integer = wordLengths.GetLowerBound(0) To wordLengths.GetUpperBound(0)
            nWords += wordLengths(ctr)
            totalLength += ctr * wordLengths(ctr)
        Next
        Return New Tuple(Of Integer, Double)(nWords, totalLength / nWords)
    End Function
End Class

Capture somente quando necessário

As expressões regulares no .NET oferecem suporte a constructos de agrupamento, que permitem a você agrupar um padrão de expressão regular em uma ou mais subexpressões. Os constructos de agrupamento mais usados na linguagem de expressões regulares no .NET são (subexpression), que define um grupo de captura numerado, e (?<name>subexpression), que define um grupo de captura nomeado. Os construtores de agrupamento são essenciais para criar referências reversas e definir uma subexpressão à qual um quantificador é aplicado.

No entanto, o uso desses elementos de linguagem tem um custo. Eles fazem com que o objeto GroupCollection retornado pela propriedade Match.Groups seja preenchido com as capturas nomeadas ou sem nome mais recentes. Se um único constructo de agrupamento capturar várias substrings de caracteres na cadeia de caracteres de entrada, também preenchem o objeto CaptureCollection retornado pela propriedade Group.Captures de um grupo de captura específico com vários objetos Capture.

Muitas vezes, os constructos de agrupamento são usados em uma expressão regular apenas para que os quantificadores possam ser aplicados a eles. Os grupos capturados por essas subexpressões não são usados posteriormente. Por exemplo, a expressão regular \b(\w+[;,]?\s?)+[.?!] é criada para capturar uma frase inteira. A tabela a seguir descreve os elementos de linguagem nesse padrão de expressão regular e seu efeito nas coleções Match e Match.Groups do objeto Group.Captures:

Padrão Descrição
\b Começar a correspondência em um limite de palavra.
\w+ Corresponde a um ou mais caracteres de palavra.
[;,]? Corresponde a zero ou uma vírgula ou ponto e vírgula.
\s? Corresponde a zero ou a um caractere de espaço em branco.
(\w+[;,]?\s?)+ Corresponde a uma ou mais ocorrências de um ou mais caracteres de palavra seguidos por uma vírgula opcional ou por ponto e vírgula seguido por um caractere de espaço em branco opcional. Este padrão define o primeiro grupo de captura, que é necessário para que a combinação de vários caracteres de palavra (ou seja, uma palavra) seguido por um símbolo de pontuação opcional seja repetida até que o mecanismo de expressões regulares atinja o final de uma sentença.
[.?!] Corresponde a um ponto, um ponto de interrogação ou um ponto de exclamação.

Como o exemplo a seguir mostra, quando uma correspondência é encontrada, os objetos GroupCollection e de CaptureCollection são preenchidos com as capturas da correspondência. Nesse caso, o grupo de captura (\w+[;,]?\s?) existe para que o quantificador + possa ser aplicado a ele, o que permite que o padrão de expressão regular corresponda a cada palavra em uma sentença. Caso contrário, ele corresponderia à última palavra em uma sentença.

using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string input = "This is one sentence. This is another.";
      string pattern = @"\b(\w+[;,]?\s?)+[.?!]";

      foreach (Match match in Regex.Matches(input, pattern)) {
         Console.WriteLine("Match: '{0}' at index {1}.",
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index);
               capCtr++;
            }
            grpCtr++;
         }
         Console.WriteLine();
      }
   }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'          Group 1: 'sentence' at index 12.
'             Capture 0: 'This ' at 0.
'             Capture 1: 'is ' at 5.
'             Capture 2: 'one ' at 8.
'             Capture 3: 'sentence' at 12.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
'          Group 1: 'another' at index 30.
'             Capture 0: 'This ' at 22.
'             Capture 1: 'is ' at 27.
'             Capture 2: 'another' at 30.

Quando você usa subexpressões apenas para aplicar quantificadores a elas e não está interessado em texto capturado, desabilite as capturas de grupo. Por exemplo, o elemento de linguagem (?:subexpression) evita que o grupo ao qual ele se aplica capture subcadeias de caracteres correspondidas. No exemplo a seguir, o padrão de expressão regular do exemplo anterior é alterado para \b(?:\w+[;,]?\s?)+[.?!]. Conforme mostrado pela saída, ele impede que o mecanismo de expressões regulares preencha as coleções GroupCollection e CaptureCollection:

using System;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      string input = "This is one sentence. This is another.";
      string pattern = @"\b(?:\w+[;,]?\s?)+[.?!]";

      foreach (Match match in Regex.Matches(input, pattern)) {
         Console.WriteLine("Match: '{0}' at index {1}.",
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, cap.Index);
               capCtr++;
            }
            grpCtr++;
         }
         Console.WriteLine();
      }
   }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(?:\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.

É possível desabilitar as capturas de uma das seguintes formas:

  • Use o elemento de linguagem (?:subexpression). Esse elemento impede a captura de subcadeias de caracteres correspondidas no grupo ao qual se ele aplica. Ele não desabilita capturas de substring de caracteres em grupos aninhados.

  • Use a opção ExplicitCapture. Ela desabilita todas as capturas sem nome ou implícitas no padrão de expressão regular. Quando você usa essa opção, somente as subcadeias de caracteres que correspondem aos grupos nomeados definidos com o elemento de linguagem (?<name>subexpression) podem ser capturadas. O sinalizador ExplicitCapture pode ser passado para o parâmetro options de um construtor de classe Regex ou para o parâmetro options de um método de correspondência estática Regex.

  • Use a opção n no elemento de linguagem (?imnsx). Esta opção desabilita todas as capturas sem nome ou implícitas a partir do ponto no padrão de expressão regular em que o elemento aparece. As capturas são desabilitadas até o final do padrão ou até a opção (-n) permitir capturas sem nome ou implícitas. Para saber mais, confira Constructos diversos.

  • Use a opção n no elemento de linguagem (?imnsx:subexpression). Esta opção desativa todas as capturas sem nome ou implícitas em subexpression. As capturas por grupos de capturas aninhadas sem nome ou implícitas também são desabilitadas.

Título Descrição
Detalhes do comportamento de expressões regulares Examina a implementação do mecanismo de expressões regulares no .NET. O artigo trata da flexibilidade de expressões regulares e explica a responsabilidade do desenvolvedor para garantir o funcionamento eficiente e robusto do mecanismo de expressões regulares.
Retrocesso Explica o que é o retrocesso é como ele afeta o desempenho da expressão regular e examina os elementos de linguagem que fornecem alternativas ao retrocesso.
Linguagem de expressões regulares – referência rápida Descreve os elementos de linguagem de expressões regulares do .NET e fornece links para a documentação detalhada de cada elemento da linguagem.