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.
- Os blocos de uma regex:
\d,\w,[A-Z],+,{5},^,$,( )… - Por que em C# se escreve a regex com
@"…"(e o erroCS1009sem ele). Regex.IsMatch, e por que ele precisa de^e$para validar o texto inteiro.MatcheMatchespara achar pedaços, e grupos (numerados e com nome) para separar as partes.Regex.Replace(com$1),Regex.Split,RegexOptions.IgnoreCasee o[GeneratedRegex].- As armadilhas: o
.*guloso, o\dque aceita algarismos árabes e aRegexParseException.
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
0a9. - {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.
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
| Bloco | Casa com | Exemplo |
|---|---|---|
abc | Exatamente esses caracteres | R\$ casa com R$ (o $ precisa de \) |
\d | Um dígito (inclusive de outras escritas) | \d\d casa com 42 |
\w | Uma letra (com acento), dígito ou _ | \w+ casa com feijão |
\s | Um espaço, tabulação ou quebra de linha | \s+ casa com " " |
. | Qualquer caractere, menos a quebra de linha | c.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, \S | O 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 vezes | a* 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 |
\b | Uma borda de palavra | \bsol\b não casa dentro de solta |
a|b | Uma coisa ou outra | gato|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
| Trecho | O 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.Value | O pedaço de texto encontrado. |
m.Index | Onde ele começa (contando do 0, como num array). |
Regex.Match(…) | Só o primeiro achado, um Match. |
nada.Success | Quando 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
| Linha | O 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.IgnoreCase | Ignora 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.
[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 fixo | texto.Contains("x"), sem regex |
| Validar o formato de um campo inteiro | Regex.IsMatch(texto, @"^…$") |
| Achar o primeiro pedaço | Regex.Match + .Success |
| Achar todos os pedaços | Regex.Matches |
| Separar as partes de cada achado | Grupos ( ), de preferência com nome (?<nome>…) |
| Trocar um padrão | Regex.Replace (com $1 ou uma lambda) |
| Dividir num separador variável | Regex.Split (separador fixo: texto.Split(',')) |
| A mesma regex muitas vezes, num projeto | [GeneratedRegex] |
| Validar um CPF ou um e-mail de verdade | Regex 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
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étodo | Exemplo | Devolve |
|---|---|---|
Regex.IsMatch | Regex.IsMatch(t, @"^\d+$") | bool |
Regex.Match | Regex.Match(t, @"\d+") | Match (.Success, .Value, .Index, .Groups) |
Regex.Matches | foreach (Match m in Regex.Matches(t, @"\d+")) | MatchCollection |
Regex.Replace | Regex.Replace(t, @"(\d+)", "[$1]") | string |
Regex.Replace com lambda | Regex.Replace(t, @"\w+", m => m.Value.ToUpper()) | string |
Regex.Split | Regex.Split(t, @"\s*,\s*") | string[] |
| Opções | RegexOptions.IgnoreCase, Multiline (^/$ por linha) | — |
| Grupo numerado / com nome | m.Groups[1].Value / m.Groups["ddd"].Value | string |
| Objeto reutilizável | static 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):
2. No texto de troca do Regex.Replace, como se escreve "o grupo 2"?
3. Que caractere se põe antes das aspas para a barra \ não ser um escape do C#?
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:
\ddígito,\wletra,\sespaç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 deSystem.Text.RegularExpressions. Regex.IsMatchprocura em qualquer lugar: para validar o texto inteiro, use^…$. Para só 0 a 9,[0-9](o\daceita dígitos de outras escritas).Match(primeiro, comSuccess) eMatches(todos) acham pedaços;Groups[1]ouGroups["nome"]separam as partes.Regex.Replacetroca (com$1ou uma lambda) eRegex.Splitdivide..*é guloso;.*?é preguiçoso.- Regex errada é
RegexParseExceptionao rodar.[GeneratedRegex]transforma a regex em código na compilação, e uma regex errada vira o erroSYSLIB1042(nodotnete no editor do guia).
Próxima aula: Span<T>, para olhar pedaços de arrays e textos sem copiar nada.