C# Expressões Regulares (Regex)

Uma expressão regular (em inglês regular expression, apelidada de regex) é um texto que descreve um formato: "cinco dígitos, um hífen e mais três dígitos" é o formato de um CEP. Com ela, o C# responde perguntas como "este texto tem cara de CEP?", "onde estão os preços neste recibo?" e "troque todos os espaços repetidos por um só". Tudo isso mora na classe Regex.

Você vai aprender
  • Os blocos de uma regex: \d, \w, [A-Z], +, {5}, ^, $, ( )…
  • Por que em C# se escreve a regex com @"…" (e o erro CS1009 sem ele).
  • Regex.IsMatch, e por que ele precisa de ^ e $ para validar o texto inteiro.
  • Match e Matches para achar pedaços, e grupos (numerados e com nome) para separar as partes.
  • Regex.Replace (com $1), Regex.Split, RegexOptions.IgnoreCase e o [GeneratedRegex].
  • As armadilhas: o .* guloso, o \d que aceita algarismos árabes e a RegexParseException.

Esta aula usa os métodos de string, os escapes e strings literais (@) e o foreach. Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia antes do código

Quando a Ana olha para 01310-100, ela reconhece um CEP sem pensar: "são 5 números, um tracinho, 3 números". Se alguém escrever 1310-100, ela estranha: falta um número. Ela não decorou todos os CEPs do Brasil; ela conhece o molde.

Uma regex é esse molde escrito numa linguagem que o computador entende. O molde do CEP fica assim:

^
"Aqui começa o texto." Nada pode vir antes.
\d
Um dígito (de digit): qualquer caractere de 0 a 9.
{5}
O item anterior, exatamente 5 vezes.
-
Um hífen de verdade. Letras e símbolos comuns representam eles mesmos.
\d{3}
Mais três dígitos.
$
"Aqui termina o texto." Nada pode vir depois.

Juntando: ^\d{5}-\d{3}$. A regex é uma linguagem pequena e própria, que existe também no grep do Linux, no JavaScript, no Python e no Java. O que você aprender aqui vale em quase todo lugar, com pequenas diferenças.


1. A primeira regex: Regex.IsMatch

A classe Regex fica em System.Text.RegularExpressions. O método IsMatch ("casa?") responde true ou false. Compare a regex sem e com ^ e $:

using System.Text.RegularExpressions;

string[] ceps = ["01310-100", "1310-100", "50030-23O", "Meu CEP é 50030-230, ok?"];
foreach (var cep in ceps)
{
    bool qualquerLugar = Regex.IsMatch(cep, @"\d{5}-\d{3}");
    bool inteiro = Regex.IsMatch(cep, @"^\d{5}-\d{3}$");
    Console.WriteLine($"{cep,-26} contém: {qualquerLugar,-5}  é um CEP: {inteiro}");
}
$ dotnet run cep.cs
01310-100                  contém: True   é um CEP: True
1310-100                   contém: False  é um CEP: False
50030-23O                  contém: False  é um CEP: False
Meu CEP é 50030-230, ok?   contém: True   é um CEP: False
using System.Text.RegularExpressions;
A pasta da classe Regex.
Regex.IsMatch(cep, …)
Primeiro o texto a examinar, depois a regex.
@"\d{5}-\d{3}"
A regex, numa string literal (@): a barra \ fica como está (seção 2).
{cep,-26}
Só formatação: alinha o texto em 26 colunas, para a tabela ficar bonita.

A última linha é a lição mais importante desta aula: IsMatch procura o molde em qualquer lugar do texto. "Meu CEP é 50030-230, ok?" contém um CEP, então a regex sem âncoras disse True. Para validar um campo (o texto inteiro tem de ser um CEP), use ^ no começo e $ no fim. E repare no 50030-23O: o último caractere é a letra O, não o zero, e o \d não deixou passar.

Quem vem do Java, atenção

O matches do Java confere o texto inteiro sozinho. O Regex.IsMatch do C# não: ele faz o papel do find. Sem ^ e $, a validação deixa passar lixo antes e depois.

Missão: o validador está aceitando CEP 01310-100 e 01310-1000, que não são CEPs. Acrescente as âncoras ^ e $ à regex para que só o primeiro e o último sejam aceitos.

using System.Text.RegularExpressions;

string[] testes = ["01310-100", "CEP 01310-100", "01310-1000", "50030-230"];
foreach (var t in testes)
{
    Console.WriteLine($"{t}: {Regex.IsMatch(t, @"\d{5}-\d{3}")}");
}
using System.Text.RegularExpressions;

string[] testes = ["01310-100", "CEP 01310-100", "01310-1000", "50030-230"];
foreach (var t in testes)
{
    Console.WriteLine($"{t}: {Regex.IsMatch(t, @"^\d{5}-\d{3}$")}");
}

2. Por que @"…": a barra invertida

Numa string comum do C#, a barra invertida já tem um trabalho: "\n" é uma quebra de linha, "\t" é uma tabulação. Mas \d não é um escape que o C# conheça, e o compilador reclama:

using System.Text.RegularExpressions;
Console.WriteLine(Regex.IsMatch("123", "\d+"));
$ dotnet run barra.cs
/home/ana/cs/barra.cs(2,41): error CS1009: Sequência de escape não reconhecida

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Há duas saídas: dobrar a barra ("\\d+": o C# transforma \\ numa barra só, e a regex recebe \d+) ou usar uma string literal, com @ na frente, em que a barra é só uma barra. Com regex, use sempre @"…": fica igual ao que você vê em qualquer tutorial de regex.

using System.Text.RegularExpressions;
Console.WriteLine(Regex.IsMatch("123", "\\d+"));
Console.WriteLine(Regex.IsMatch("123", @"\d+"));
Console.WriteLine(Regex.IsMatch("١٢٣", @"^\d+$"));
Console.WriteLine(Regex.IsMatch("١٢٣", @"^[0-9]+$"));
$ dotnet run barras.cs
True
True
True
False

As duas primeiras linhas são a mesma regex escrita de dois jeitos. As duas últimas mostram uma surpresa: ١٢٣ são os algarismos 1, 2 e 3 do árabe, e o \d do .NET os aceita, porque segue o Unicode (todos os dígitos de todas as escritas). Se você vai converter o texto com int.Parse ou precisa só de 0 a 9 (CEP, CPF, telefone), escreva [0-9].


3. Os blocos de uma regex

BlocoCasa comExemplo
abcExatamente esses caracteresR\$ casa com R$ (o $ precisa de \)
\dUm dígito (inclusive de outras escritas)\d\d casa com 42
\wUma letra (com acento), dígito ou _\w+ casa com feijão
\sUm espaço, tabulação ou quebra de linha\s+ casa com " "
.Qualquer caractere, menos a quebra de linhac.sa casa com casa e cosa
[abc]Um dos caracteres da lista[aeiou] é uma vogal
[a-z], [0-9]Um caractere da faixa[A-Z] é uma maiúscula sem acento
[^abc]Qualquer um que não esteja na lista[^0-9] é um não dígito
\D, \W, \SO contrário de \d, \w, \s\D é um não dígito
+O anterior, 1 ou mais vezes\d+ casa com 7 e 2026
*O anterior, 0 ou mais vezesa* casa até com o vazio
?O anterior é opcional (0 ou 1 vez)-?\d+ aceita -5 e 5
{n}, {n,m}, {n,}Exatamente n; de n a m; n ou mais\d{4,5}: 4 ou 5 dígitos
^ e $Começo e fim do texto^\d+$: só dígitos
\bUma borda de palavra\bsol\b não casa dentro de solta
a|bUma coisa ou outragato|cão
( )Agrupa e captura (seção 5)(ab)+ casa com ababab
\., \(, \$…O símbolo de verdade\. é um ponto, não "qualquer caractere"

4. Achar pedaços: Match e Matches

Validar é só metade da história. A outra metade é procurar: achar todos os preços de um recibo, todos os e-mails de um texto. Regex.Match devolve o primeiro pedaço encontrado; Regex.Matches devolve todos.

using System.Text.RegularExpressions;

string recibo = "Caderno R$ 15,90; caneta R$ 2,50 (x3); mochila R$ 129,00";

MatchCollection precos = Regex.Matches(recibo, @"R\$ \d+,\d{2}");
Console.WriteLine($"Achei {precos.Count} preços:");
foreach (Match m in precos)
{
    Console.WriteLine($"  {m.Value} (na posição {m.Index})");
}

Match primeiro = Regex.Match(recibo, @"\d+,\d{2}");
Console.WriteLine($"Primeiro número: {primeiro.Value}");
Match nada = Regex.Match(recibo, @"US\$");
Console.WriteLine($"Achou dólar? {nada.Success}");
$ dotnet run recibo.cs
Achei 3 preços:
  R$ 15,90 (na posição 8)
  R$ 2,50 (na posição 25)
  R$ 129,00 (na posição 47)
Primeiro número: 15,90
Achou dólar? False
TrechoO que é
R\$ \d+,\d{2}"R", um cifrão de verdade (\$, porque $ sozinho é "fim do texto"), um espaço, um ou mais dígitos, uma vírgula e dois dígitos.
Regex.Matches(…)Uma MatchCollection: a coleção de todos os achados, que se percorre com foreach.
m.ValueO pedaço de texto encontrado.
m.IndexOnde ele começa (contando do 0, como num array).
Regex.Match(…)Só o primeiro achado, um Match.
nada.SuccessQuando não acha nada, o Match não é null: ele vem com Success valendo false e Value vazio.

Missão: o pedido chegou como texto. Use Regex.Matches com @"\d+" para achar todos os números, some-os com int.Parse(m.Value) e mostre Total de itens: 26.

using System.Text.RegularExpressions;

string pedido = "3 cadernos, 12 canetas, 1 mochila e 10 borrachas";
int total = 0;
// percorra os números do pedido e some
Console.WriteLine($"Total de itens: {total}");
using System.Text.RegularExpressions;

string pedido = "3 cadernos, 12 canetas, 1 mochila e 10 borrachas";
int total = 0;
foreach (Match m in Regex.Matches(pedido, @"\d+"))
{
    total += int.Parse(m.Value);
}
Console.WriteLine($"Total de itens: {total}");

5. Grupos: separar as partes

Os parênteses ( ) fazem duas coisas: agrupam (como na matemática) e capturam o pedaço que casou lá dentro, para você usar depois. Num telefone (81) 99999-0001, dá para pegar o DDD separado do número:

using System.Text.RegularExpressions;

string agenda = "Ana (81) 99999-0001, Bruno (11) 98888-0002, Carla 3333-4444";

foreach (Match m in Regex.Matches(agenda, @"\((\d{2})\) (\d{4,5})-(\d{4})"))
{
    Console.WriteLine($"Completo: {m.Value} | DDD: {m.Groups[1].Value} | número: {m.Groups[2].Value}-{m.Groups[3].Value}");
}

foreach (Match m in Regex.Matches(agenda, @"(?<nome>[A-Z][a-z]+) \((?<ddd>\d{2})\)"))
{
    Console.WriteLine($"{m.Groups["nome"].Value} mora no DDD {m.Groups["ddd"].Value}");
}
$ dotnet run telefones.cs
Completo: (81) 99999-0001 | DDD: 81 | número: 99999-0001
Completo: (11) 98888-0002 | DDD: 11 | número: 98888-0002
Ana mora no DDD 81
Bruno mora no DDD 11
\(
Um parêntese de verdade, o do telefone (com barra, porque ( sozinho abre um grupo).
(\d{2})
O grupo 1: dois dígitos, o DDD.
\)
O parêntese de verdade que fecha, e um espaço.
(\d{4,5})
O grupo 2: 4 ou 5 dígitos.
-(\d{4})
Um hífen e o grupo 3.
(?<ddd>\d{2})
Um grupo com nome: o mesmo que um grupo numerado, mas lido com m.Groups["ddd"], bem mais fácil de entender depois.

Os grupos são contados pelos parênteses que abrem, da esquerda para a direita. O m.Groups[0] é sempre o achado inteiro (o mesmo que m.Value). A Carla não apareceu: o telefone dela não tem DDD entre parênteses, então o molde não casou.


6. Trocar e dividir: Replace e Split

using System.Text.RegularExpressions;

string bagunca = "Lista   de    compras:  arroz,feijão ,  café";
Console.WriteLine(Regex.Replace(bagunca, @"\s+", " "));

string[] itens = Regex.Split("arroz,feijão ,  café;açúcar", @"\s*[,;]\s*");
Console.WriteLine(string.Join(" | ", itens));

string data = "Entrega em 25/12/2026 e troca até 10/01/2027.";
Console.WriteLine(Regex.Replace(data, @"(\d{2})/(\d{2})/(\d{4})", "$3-$2-$1"));

string cpf = "CPF 123.456.789-09";
Console.WriteLine(Regex.Replace(cpf, @"\d{3}\.\d{3}\.(\d{3})-(\d{2})", "***.***.$1-$2"));

Console.WriteLine(Regex.Replace("ana maria silva", @"\b[a-z]", m => m.Value.ToUpper()));

Console.WriteLine(Regex.IsMatch("CAFÉ", "café", RegexOptions.IgnoreCase));
$ dotnet run trocar.cs
Lista de compras: arroz,feijão , café
arroz | feijão | café | açúcar
Entrega em 2026-12-25 e troca até 2027-01-10.
CPF ***.***.789-09
Ana Maria Silva
True
LinhaO que faz
Regex.Replace(bagunca, @"\s+", " ")Troca cada sequência de espaços por um espaço. O string.Replace comum não sabe fazer isso, porque só troca um texto fixo.
Regex.Split(…, @"\s*[,;]\s*")Divide nas vírgulas ou pontos e vírgulas, engolindo os espaços em volta.
"$3-$2-$1"No texto de troca, $1, $2, $3 são os grupos. A data brasileira virou o formato ano-mês-dia. (Com grupos com nome: ${ano}.)
"***.***.$1-$2"Esconde o começo do CPF e mantém o final: só o formato, não confere se o CPF é válido.
m => m.Value.ToUpper()No lugar do texto de troca, uma lambda que recebe cada achado e devolve o que pôr no lugar. Aqui, a primeira letra de cada palavra em maiúscula.
RegexOptions.IgnoreCaseIgnora maiúsculas e minúsculas: café casa com CAFÉ.

7. Reutilizar a regex: o objeto Regex e o [GeneratedRegex]

Os métodos estáticos (Regex.IsMatch(texto, molde)) são práticos: o .NET guarda as últimas regex usadas e não as reprocessa toda vez. Quando a mesma regex é usada muitas vezes, o costume é criá-la uma vez só e guardar:

static readonly Regex Cep = new(@"^\d{5}-\d{3}$");
// depois: Cep.IsMatch(texto)

Desde o .NET 7, existe um jeito ainda melhor: o atributo [GeneratedRegex]. Um gerador de código lê a regex na hora de compilar e escreve um código C# especializado para ela (mais rápido, e um erro na regex vira erro de compilação). Desde o C# 13 ele também funciona em propriedades partial:

using System.Text.RegularExpressions;

string[] testes = ["01310-100", "abc"];
foreach (var t in testes)
    Console.WriteLine($"{t}: {Validador.Cep.IsMatch(t)} {Validador.CepAntigo().IsMatch(t)}");

static partial class Validador
{
    [GeneratedRegex(@"^\d{5}-\d{3}$")]
    public static partial Regex Cep { get; }

    [GeneratedRegex(@"^\d{5}-\d{3}$")]
    public static partial Regex CepAntigo();
}
$ dotnet run gerada.cs
01310-100: True True
abc: False False

Você só escreve a "metade de cima" (partial, sem corpo); o gerador escreve a outra metade, com a regex já transformada em código. A forma com método (CepAntigo()) é a do .NET 7 e 8, e você vai vê-la muito.

O [GeneratedRegex] também roda no editor do guia

O editor desta página roda os mesmos geradores de código do dotnet run: o gerada.cs compila e mostra a mesma saída. Experimente abaixo. Depois, estrague a regex de propósito (apague o ) de um grupo, por exemplo @"^(\d{5}-\d{3}$"): o gerador recusa na compilação, com o erro SYSLIB1042: O regex especificado é inválido., antes de o programa rodar.

Experimente: acrescente uma propriedade Placa com [GeneratedRegex(@"^[A-Z]{3}\d[A-Z]\d{2}$")] e teste "ABC1D23".

using System.Text.RegularExpressions;

string[] testes = ["01310-100", "abc"];
foreach (var t in testes)
    Console.WriteLine($"{t}: {Validador.Cep.IsMatch(t)} {Validador.CepAntigo().IsMatch(t)}");

static partial class Validador
{
    [GeneratedRegex(@"^\d{5}-\d{3}$")]
    public static partial Regex Cep { get; }

    [GeneratedRegex(@"^\d{5}-\d{3}$")]
    public static partial Regex CepAntigo();
}

Qual usar?

Você quer…Use
Saber se o texto contém um texto fixotexto.Contains("x"), sem regex
Validar o formato de um campo inteiroRegex.IsMatch(texto, @"^…$")
Achar o primeiro pedaçoRegex.Match + .Success
Achar todos os pedaçosRegex.Matches
Separar as partes de cada achadoGrupos ( ), de preferência com nome (?<nome>…)
Trocar um padrãoRegex.Replace (com $1 ou uma lambda)
Dividir num separador variávelRegex.Split (separador fixo: texto.Split(','))
A mesma regex muitas vezes, num projeto[GeneratedRegex]
Validar um CPF ou um e-mail de verdadeRegex só para o formato; os dígitos verificadores do CPF são uma conta, e o e-mail só se confirma enviando uma mensagem

Quando dá errado

error CS1009: a barra numa string comum

Escrever "\d+" sem @ dá error CS1009: Sequência de escape não reconhecida (seção 2). Como consertar: @"\d+".

Regex quebrada: RegexParseException

Este código tem um erro de propósito.
using System.Text.RegularExpressions;
Console.WriteLine(Regex.IsMatch("(81) 9999", @"(\d{2}"));
$ dotnet run quebrada.cs
Unhandled exception. System.Text.RegularExpressions.RegexParseException: Invalid pattern '(\d{2}' at offset 6. Not enough )'s.
   at System.Text.RegularExpressions.RegexParser.ScanRegex()
   at System.Text.RegularExpressions.RegexParser.Parse(String pattern, RegexOptions options, CultureInfo culture)
   at System.Text.RegularExpressions.Regex..ctor(String pattern, CultureInfo culture)
   at System.Text.RegularExpressions.RegexCache.GetOrAdd(String pattern)
   at System.Text.RegularExpressions.Regex.IsMatch(String input, String pattern)
   at Program.<Main>$(String[] args) in /home/ana/cs/quebrada.cs:line 2

O que quer dizer: "regex inválida na posição 6: faltam )". O programa compilou (para o C#, a regex é só um texto), e o erro apareceu ao rodar. O ( abriu um grupo que nunca foi fechado; a intenção era um parêntese de verdade. Como consertar: @"\(\d{2}\)". A RegexParseException é um tipo de ArgumentException; se a regex vem do usuário, use try/catch. (Com [GeneratedRegex], o erro aparece já na compilação, no dotnet e no editor do guia: error SYSLIB1042: O regex especificado é inválido.)

O .* guloso pegou demais

using System.Text.RegularExpressions;
string html = "<b>Ana</b> e <b>Bruno</b>";
Console.WriteLine(Regex.Match(html, "<b>.*</b>").Value);
Console.WriteLine(Regex.Match(html, "<b>.*?</b>").Value);
$ dotnet run guloso.cs
<b>Ana</b> e <b>Bruno</b>
<b>Ana</b>

O que quer dizer: a Ana queria só o primeiro nome em negrito, mas o .* é guloso: pega o máximo possível e só para no último </b>. Como consertar: o ? depois do * (ou do +) o torna preguiçoso: .*? pega o mínimo e para no primeiro </b>. (Para HTML de verdade, use uma biblioteca de HTML; regex serve para casos simples.)

A validação deixou passar lixo

Regex.IsMatch("CEP 01310-100", @"\d{5}-\d{3}") deu True. O que quer dizer: sem âncoras, o IsMatch procura em qualquer lugar (seção 1). Como consertar: @"^\d{5}-\d{3}$". E lembre do \d que aceita ١٢٣: para "só 0 a 9", use [0-9].


Referência

MétodoExemploDevolve
Regex.IsMatchRegex.IsMatch(t, @"^\d+$")bool
Regex.MatchRegex.Match(t, @"\d+")Match (.Success, .Value, .Index, .Groups)
Regex.Matchesforeach (Match m in Regex.Matches(t, @"\d+"))MatchCollection
Regex.ReplaceRegex.Replace(t, @"(\d+)", "[$1]")string
Regex.Replace com lambdaRegex.Replace(t, @"\w+", m => m.Value.ToUpper())string
Regex.SplitRegex.Split(t, @"\s*,\s*")string[]
OpçõesRegexOptions.IgnoreCase, Multiline (^/$ por linha)—
Grupo numerado / com nomem.Groups[1].Value / m.Groups["ddd"].Valuestring
Objeto reutilizávelstatic readonly Regex R = new(@"…");Regex
Gerada na compilação[GeneratedRegex(@"…")] static partial Regex R { get; }Regex

Exercícios

1. Complete a regex que aceita só um texto de 3 dígitos (nada antes, nada depois):

Regex.IsMatch(t, @"")

2. No texto de troca do Regex.Replace, como se escreve "o grupo 2"?

Resposta:

3. Que caractere se põe antes das aspas para a barra \ não ser um escape do C#?

Regex.IsMatch(t, "\d+")

4. Regex.IsMatch("abc123xyz", @"\d+") dá:

5. Em "<i>a</i> e <i>b</i>", qual regex pega só <i>a</i>?

6. Regex.Match(texto, "x") não achou nada. O que ele devolve?

Missão final: antes de mostrar a agenda na tela da loja, a Ana quer esconder o meio dos telefones, mantendo o DDD e os 4 últimos dígitos: (81) ****-0001. Use Regex.Replace com dois grupos, um para o DDD e outro para o final, e $1 e $2 no texto de troca.

using System.Text.RegularExpressions;

string agenda = "Ana (81) 99999-0001, Bruno (11) 98888-0002, Carla (21) 3333-4444";
string protegida = agenda; // troque por um Regex.Replace
Console.WriteLine(protegida);
using System.Text.RegularExpressions;

string agenda = "Ana (81) 99999-0001, Bruno (11) 98888-0002, Carla (21) 3333-4444";
string protegida = Regex.Replace(agenda, @"\((\d{2})\) \d{4,5}-(\d{4})", "($1) ****-$2");
Console.WriteLine(protegida);

A solução, na máquina da Ana. O \d{4,5} pegou tanto os celulares (5 dígitos) quanto o fixo da Carla (4):

$ dotnet run mascara.cs
Ana (81) ****-0001, Bruno (11) ****-0002, Carla (21) ****-4444

Resumo

  • Uma regex é um molde de texto: \d dígito, \w letra, \s espaço, [a-z] faixa, + * ? {n,m} repetições, ^ $ começo e fim, ( ) grupos, \. o símbolo de verdade.
  • Em C#, escreva a regex com @"…" (sem ele, "\d" dá CS1009) e use as classes de System.Text.RegularExpressions.
  • Regex.IsMatch procura em qualquer lugar: para validar o texto inteiro, use ^…$. Para só 0 a 9, [0-9] (o \d aceita dígitos de outras escritas).
  • Match (primeiro, com Success) e Matches (todos) acham pedaços; Groups[1] ou Groups["nome"] separam as partes.
  • Regex.Replace troca (com $1 ou uma lambda) e Regex.Split divide. .* é guloso; .*? é preguiçoso.
  • Regex errada é RegexParseException ao rodar. [GeneratedRegex] transforma a regex em código na compilação, e uma regex errada vira o erro SYSLIB1042 (no dotnet e no editor do guia).

Próxima aula: Span<T>, para olhar pedaços de arrays e textos sem copiar nada.