A crate regex é a implementação padrão de expressões regulares para Rust, desenvolvida por Andrew Gallick (BurntSushi), o mesmo autor de ripgrep. Ela é conhecida por sua segurança (sem backtracking exponencial, garantia de tempo linear), performance excepcional e conformidade com a sintaxe RE2 do Google.
Expressões regulares são essenciais para validação de dados, parsing de texto, busca em logs e transformação de strings. Em Rust, a crate regex é usada extensivamente em ferramentas como ripgrep, que supera grep e ag em benchmarks reais.
Instalação
Adicione ao seu Cargo.toml:
[dependencies]
regex = "1"
Para compilar expressões regulares uma única vez e reutilizá-las (recomendado), use também:
[dependencies]
regex = "1"
once_cell = "1"
Ou, se preferir lazy_static:
[dependencies]
regex = "1"
lazy_static = "1"
Uso Básico
Verificando se um Padrão Existe
use regex::Regex;
fn main() {
let re = Regex::new(r"^\d{3}\.\d{3}\.\d{3}-\d{2}$").unwrap();
assert!(re.is_match("123.456.789-00"));
assert!(!re.is_match("12345678900"));
assert!(!re.is_match("abc.def.ghi-jk"));
println!("Validação de CPF funcionando!");
}
Note o uso de r"..." (raw string) para evitar escapar barras invertidas.
Encontrando a Primeira Correspondência
use regex::Regex;
fn main() {
let re = Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap();
let texto = "Servidor em 192.168.1.100, backup em 10.0.0.5";
if let Some(m) = re.find(texto) {
println!("IP encontrado: '{}' na posição {}..{}", m.as_str(), m.start(), m.end());
// Saída: IP encontrado: '192.168.1.100' na posição 13..26
}
}
Encontrando Todas as Correspondências
use regex::Regex;
fn main() {
let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
let texto = "Contatos: maria@exemplo.com, joao@empresa.com.br e suporte@ajuda.org";
let emails: Vec<&str> = re.find_iter(texto).map(|m| m.as_str()).collect();
println!("Emails encontrados: {:?}", emails);
// Saída: ["maria@exemplo.com", "joao@empresa.com.br", "suporte@ajuda.org"]
}
Grupos de Captura
use regex::Regex;
fn main() {
let re = Regex::new(r"(\d{2})/(\d{2})/(\d{4})").unwrap();
let texto = "Data de nascimento: 15/03/1990";
if let Some(caps) = re.captures(texto) {
let dia = &caps[1];
let mes = &caps[2];
let ano = &caps[3];
println!("Dia: {}, Mês: {}, Ano: {}", dia, mes, ano);
// Saída: Dia: 15, Mês: 03, Ano: 1990
// Grupo 0 é sempre o match completo
println!("Match completo: {}", &caps[0]);
// Saída: 15/03/1990
}
}
Capturas Nomeadas
use regex::Regex;
fn main() {
let re = Regex::new(
r"(?P<protocolo>https?)://(?P<host>[^/]+)(?P<caminho>/[^\s]*)?"
).unwrap();
let urls = vec![
"https://www.exemplo.com.br/pagina",
"http://api.servico.com/v1/usuarios",
"https://rust-lang.org",
];
for url in urls {
if let Some(caps) = re.captures(url) {
println!("URL: {}", url);
println!(" Protocolo: {}", &caps["protocolo"]);
println!(" Host: {}", &caps["host"]);
println!(" Caminho: {}", caps.name("caminho").map_or("/", |m| m.as_str()));
println!();
}
}
}
Iterando sobre Capturas
use regex::Regex;
fn main() {
let re = Regex::new(r"(?P<chave>\w+)=(?P<valor>[^&]+)").unwrap();
let query = "nome=Maria&idade=30&cidade=São+Paulo&ativo=true";
println!("Parâmetros da query string:");
for caps in re.captures_iter(query) {
println!(" {} = {}", &caps["chave"], &caps["valor"]);
}
}
Recursos Avançados
Substituição de Texto
use regex::Regex;
fn main() {
let re = Regex::new(r"\b(\w)").unwrap();
// Substituição simples
let censurado = Regex::new(r"\d{3}\.\d{3}\.\d{3}-\d{2}")
.unwrap()
.replace_all("CPF: 123.456.789-00", "***.***.***-**");
println!("{}", censurado);
// Saída: CPF: ***.***.***-**
// Substituição com referências a grupos
let re_data = Regex::new(r"(\d{2})/(\d{2})/(\d{4})").unwrap();
let iso = re_data.replace_all("Data: 15/03/2024", "$3-$2-$1");
println!("{}", iso);
// Saída: Data: 2024-03-15
// Substituição com capturas nomeadas
let re_nome = Regex::new(r"(?P<sobrenome>\w+),\s*(?P<nome>\w+)").unwrap();
let formatado = re_nome.replace_all("Silva, Maria", "$nome $sobrenome");
println!("{}", formatado);
// Saída: Maria Silva
}
Substituição com Closure
use regex::{Captures, Regex};
fn main() {
let re = Regex::new(r"\b\w+\b").unwrap();
// Capitalizar cada palavra
let resultado = re.replace_all("olá mundo rust", |caps: &Captures| {
let palavra = &caps[0];
let mut chars = palavra.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
});
println!("{}", resultado);
// Saída: Olá Mundo Rust
// Substituição condicional
let re_num = Regex::new(r"\d+").unwrap();
let resultado = re_num.replace_all("Tem 3 gatos e 15 cães", |caps: &Captures| {
let num: u32 = caps[0].parse().unwrap();
if num > 10 {
format!("MUITOS({})", num)
} else {
caps[0].to_string()
}
});
println!("{}", resultado);
// Saída: Tem 3 gatos e MUITOS(15) cães
}
Compilação Lazy com once_cell
Compilar regex é custoso. Use once_cell para compilar uma única vez:
use once_cell::sync::Lazy;
use regex::Regex;
static RE_EMAIL: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});
static RE_CPF: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\d{3}\.\d{3}\.\d{3}-\d{2}$").unwrap()
});
static RE_TELEFONE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\(?(\d{2})\)?\s?(\d{4,5})-?(\d{4})$").unwrap()
});
fn validar_email(email: &str) -> bool {
RE_EMAIL.is_match(email)
}
fn validar_cpf(cpf: &str) -> bool {
RE_CPF.is_match(cpf)
}
fn formatar_telefone(tel: &str) -> Option<String> {
RE_TELEFONE.captures(tel).map(|caps| {
format!("({}) {}-{}", &caps[1], &caps[2], &caps[3])
})
}
fn main() {
println!("Email válido: {}", validar_email("maria@exemplo.com"));
println!("CPF válido: {}", validar_cpf("123.456.789-00"));
println!("Telefone: {:?}", formatar_telefone("11987654321"));
// Saída: Telefone: Some("(11) 98765-4321")
}
RegexSet para Múltiplos Padrões
O RegexSet permite testar múltiplos padrões simultaneamente de forma eficiente:
use regex::RegexSet;
fn main() {
let tipos_log = RegexSet::new(&[
r"(?i)\berror\b", // 0
r"(?i)\bwarn(ing)?\b", // 1
r"(?i)\binfo\b", // 2
r"(?i)\bdebug\b", // 3
r"(?i)\btrace\b", // 4
]).unwrap();
let linhas = vec![
"2024-01-15 ERROR: Conexão recusada",
"2024-01-15 WARN: Memória alta",
"2024-01-15 INFO: Servidor iniciado",
"2024-01-15 DEBUG: Query executada",
"Linha sem nível de log",
];
let nomes = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
for linha in linhas {
let matches: Vec<&str> = tipos_log
.matches(linha)
.into_iter()
.map(|i| nomes[i])
.collect();
if matches.is_empty() {
println!("'{}' -> Nenhum nível detectado", linha);
} else {
println!("'{}' -> {:?}", linha, matches);
}
}
}
Flags e Modificadores
use regex::Regex;
fn main() {
// Case insensitive
let re = Regex::new(r"(?i)rust").unwrap();
assert!(re.is_match("RUST"));
assert!(re.is_match("Rust"));
assert!(re.is_match("rust"));
// Multiline: ^ e $ casam com início/fim de cada linha
let re = Regex::new(r"(?m)^\d+\.\s+(.+)$").unwrap();
let texto = "Lista:\n1. Primeiro\n2. Segundo\n3. Terceiro";
for caps in re.captures_iter(texto) {
println!("Item: {}", &caps[1]);
}
// Dot-all: . casa com \n também
let re = Regex::new(r"(?s)<div>(.+?)</div>").unwrap();
let html = "<div>\n Conteúdo\n multilinha\n</div>";
if let Some(caps) = re.captures(html) {
println!("Conteúdo: {:?}", &caps[1]);
}
// Combinando flags
let re = Regex::new(r"(?ims)^início.*fim$").unwrap();
assert!(re.is_match("INÍCIO da\nlinha FIM"));
}
Split com Regex
use regex::Regex;
fn main() {
// Split por múltiplos separadores
let re = Regex::new(r"[,;\s]+").unwrap();
let texto = "maçã, banana; laranja uva,manga";
let frutas: Vec<&str> = re.split(texto).collect();
println!("{:?}", frutas);
// Saída: ["maçã", "banana", "laranja", "uva", "manga"]
// Split com limite
let partes: Vec<&str> = re.splitn(texto, 3).collect();
println!("{:?}", partes);
// Saída: ["maçã", "banana", "laranja uva,manga"]
}
Boas Práticas
1. Sempre Compile Regex Fora de Loops
use regex::Regex;
// RUIM: compila a regex a cada chamada
fn validar_ruim(emails: &[&str]) -> Vec<bool> {
emails.iter().map(|e| {
let re = Regex::new(r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
re.is_match(e)
}).collect()
}
// BOM: compila uma vez, reutiliza
fn validar_bom(emails: &[&str]) -> Vec<bool> {
let re = Regex::new(r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
emails.iter().map(|e| re.is_match(e)).collect()
}
// MELHOR: usa Lazy para evitar recompilação entre chamadas
use once_cell::sync::Lazy;
static RE_EMAIL: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap()
});
fn validar_melhor(emails: &[&str]) -> Vec<bool> {
emails.iter().map(|e| RE_EMAIL.is_match(e)).collect()
}
2. Prefira Métodos Simples para Padrões Simples
fn main() {
let texto = "Hello, World!";
// RUIM: regex para padrão trivial
// let re = Regex::new(r"World").unwrap();
// let encontrou = re.is_match(texto);
// BOM: métodos de String são mais rápidos para buscas simples
let encontrou = texto.contains("World");
// RUIM: regex para starts_with/ends_with
// let re = Regex::new(r"^Hello").unwrap();
// BOM:
let comeca = texto.starts_with("Hello");
println!("Encontrou: {}, Começa: {}", encontrou, comeca);
}
3. Use Grupos Não-Capturantes Quando Não Precisa Capturar
use regex::Regex;
fn main() {
// Grupo capturante desnecessário — gasta memória
// let re = Regex::new(r"(https?|ftp)://(\S+)").unwrap();
// Grupo não-capturante para alternação
let re = Regex::new(r"(?:https?|ftp)://(\S+)").unwrap();
let texto = "Acesse https://exemplo.com agora";
if let Some(caps) = re.captures(texto) {
// caps[1] é o host, não o protocolo
println!("URL: {}", &caps[1]);
}
}
4. Cuidado com Padrões que Casam Strings Vazias
use regex::Regex;
fn main() {
// PERIGOSO: * permite match vazio, gerando resultados inesperados
let re = Regex::new(r"\d*").unwrap();
let matches: Vec<&str> = re.find_iter("abc").map(|m| m.as_str()).collect();
println!("{:?}", matches); // ["", "", "", ""] — provavelmente não é o que você quer
// MELHOR: use + para exigir pelo menos um dígito
let re = Regex::new(r"\d+").unwrap();
let matches: Vec<&str> = re.find_iter("abc123def456").map(|m| m.as_str()).collect();
println!("{:?}", matches); // ["123", "456"]
}
5. Valide Regex em Tempo de Compilação Quando Possível
Se sua regex é constante, considere validar em compile-time usando testes:
#[cfg(test)]
mod tests {
use regex::Regex;
#[test]
fn regex_validas() {
// Se alguma regex for inválida, o teste falha
Regex::new(r"^\d{3}\.\d{3}\.\d{3}-\d{2}$").unwrap();
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
Regex::new(r"^\(\d{2}\)\s?\d{4,5}-\d{4}$").unwrap();
}
}
Exemplos Práticos
Exemplo 1: Parser de Log
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
static RE_LOG: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?P<timestamp>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(?P<nivel>\w+)\s+\[(?P<modulo>[^\]]+)\]\s+(?P<mensagem>.+)"
).unwrap()
});
#[derive(Debug)]
struct EntradaLog {
timestamp: String,
nivel: String,
modulo: String,
mensagem: String,
}
fn parsear_log(linha: &str) -> Option<EntradaLog> {
RE_LOG.captures(linha).map(|caps| EntradaLog {
timestamp: caps["timestamp"].to_string(),
nivel: caps["nivel"].to_string(),
modulo: caps["modulo"].to_string(),
mensagem: caps["mensagem"].to_string(),
})
}
fn analisar_logs(logs: &str) -> HashMap<String, usize> {
let mut contagem: HashMap<String, usize> = HashMap::new();
for linha in logs.lines() {
if let Some(entrada) = parsear_log(linha) {
*contagem.entry(entrada.nivel).or_insert(0) += 1;
}
}
contagem
}
fn main() {
let logs = r#"
2024-01-15 10:30:00.123 INFO [servidor::http] Requisição GET /api/users - 200 OK
2024-01-15 10:30:01.456 ERROR [servidor::db] Falha na query: timeout após 30s
2024-01-15 10:30:02.789 WARN [servidor::cache] Cache miss para chave 'user:42'
2024-01-15 10:30:03.012 INFO [servidor::http] Requisição POST /api/users - 201 Created
2024-01-15 10:30:04.345 DEBUG [servidor::auth] Token validado para user_id=42
2024-01-15 10:30:05.678 ERROR [servidor::http] Requisição GET /api/admin - 403 Forbidden
2024-01-15 10:30:06.901 INFO [servidor::http] Requisição GET /health - 200 OK
"#;
println!("=== Parsing de Logs ===\n");
for linha in logs.trim().lines() {
if let Some(entrada) = parsear_log(linha) {
println!(
"[{}] {} ({}) -> {}",
entrada.nivel, entrada.timestamp, entrada.modulo, entrada.mensagem
);
}
}
println!("\n=== Estatísticas ===\n");
let stats = analisar_logs(logs);
for (nivel, contagem) in &stats {
println!(" {}: {} ocorrências", nivel, contagem);
}
}
Exemplo 2: Validador de Dados Brasileiro
use once_cell::sync::Lazy;
use regex::Regex;
static RE_CPF: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(\d{3})\.(\d{3})\.(\d{3})-(\d{2})$").unwrap()
});
static RE_CNPJ: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(\d{2})\.(\d{3})\.(\d{3})/(\d{4})-(\d{2})$").unwrap()
});
static RE_CEP: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(\d{5})-(\d{3})$").unwrap()
});
static RE_TELEFONE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\((\d{2})\)\s?(\d{4,5})-(\d{4})$").unwrap()
});
static RE_PLACA: Lazy<Regex> = Lazy::new(|| {
// Placa antiga (ABC-1234) e Mercosul (ABC1D23)
Regex::new(r"^[A-Z]{3}-?\d[A-Z0-9]\d{2}$").unwrap()
});
#[derive(Debug)]
enum TipoDocumento {
Cpf,
Cnpj,
Cep,
Telefone,
PlacaVeiculo,
}
#[derive(Debug)]
struct ResultadoValidacao {
tipo: TipoDocumento,
valido: bool,
formatado: Option<String>,
detalhes: String,
}
fn validar_cpf(input: &str) -> ResultadoValidacao {
let digitos: String = input.chars().filter(|c| c.is_ascii_digit()).collect();
if digitos.len() != 11 {
return ResultadoValidacao {
tipo: TipoDocumento::Cpf,
valido: false,
formatado: None,
detalhes: format!("CPF deve ter 11 dígitos, encontrados {}", digitos.len()),
};
}
// Verifica se todos os dígitos são iguais
if digitos.chars().all(|c| c == digitos.chars().next().unwrap()) {
return ResultadoValidacao {
tipo: TipoDocumento::Cpf,
valido: false,
formatado: None,
detalhes: "CPF com todos os dígitos iguais é inválido".to_string(),
};
}
let formatado = format!(
"{}.{}.{}-{}",
&digitos[0..3],
&digitos[3..6],
&digitos[6..9],
&digitos[9..11]
);
ResultadoValidacao {
tipo: TipoDocumento::Cpf,
valido: RE_CPF.is_match(&formatado),
formatado: Some(formatado),
detalhes: "Formato válido".to_string(),
}
}
fn validar_telefone(input: &str) -> ResultadoValidacao {
let digitos: String = input.chars().filter(|c| c.is_ascii_digit()).collect();
if digitos.len() < 10 || digitos.len() > 11 {
return ResultadoValidacao {
tipo: TipoDocumento::Telefone,
valido: false,
formatado: None,
detalhes: format!(
"Telefone deve ter 10 ou 11 dígitos, encontrados {}",
digitos.len()
),
};
}
let formatado = if digitos.len() == 11 {
format!("({}) {}-{}", &digitos[0..2], &digitos[2..7], &digitos[7..11])
} else {
format!("({}) {}-{}", &digitos[0..2], &digitos[2..6], &digitos[6..10])
};
ResultadoValidacao {
tipo: TipoDocumento::Telefone,
valido: RE_TELEFONE.is_match(&formatado),
formatado: Some(formatado),
detalhes: "Formato válido".to_string(),
}
}
fn main() {
println!("=== Validador de Dados Brasileiros ===\n");
let cpfs = vec!["123.456.789-09", "12345678909", "111.111.111-11", "abc"];
for cpf in cpfs {
let resultado = validar_cpf(cpf);
println!("CPF '{}': válido={}, formatado={:?}, detalhe={}",
cpf, resultado.valido, resultado.formatado, resultado.detalhes);
}
println!();
let telefones = vec!["(11) 98765-4321", "11987654321", "(21) 3456-7890", "123"];
for tel in telefones {
let resultado = validar_telefone(tel);
println!("Tel '{}': válido={}, formatado={:?}",
tel, resultado.valido, resultado.formatado);
}
println!();
// Validação de CEP
let ceps = vec!["01310-100", "12345-678", "1234567", "abcde-fgh"];
for cep in ceps {
let valido = RE_CEP.is_match(cep);
println!("CEP '{}': válido={}", cep, valido);
}
println!();
// Validação de placas
let placas = vec!["ABC-1234", "ABC1D23", "XYZ-9999", "AB-1234"];
for placa in placas {
let valido = RE_PLACA.is_match(placa);
println!("Placa '{}': válido={}", placa, valido);
}
}
Exemplo 3: Extrator de Dados de Texto
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
static RE_PRECO: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"R\$\s?(\d{1,3}(?:\.\d{3})*(?:,\d{2})?)").unwrap()
});
static RE_DATA: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b(\d{2})/(\d{2})/(\d{4})\b").unwrap()
});
static RE_HORA: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b(\d{2}):(\d{2})(?::(\d{2}))?\b").unwrap()
});
fn extrair_precos(texto: &str) -> Vec<f64> {
RE_PRECO
.captures_iter(texto)
.filter_map(|caps| {
let valor_str = caps[1].replace('.', "").replace(',', ".");
valor_str.parse::<f64>().ok()
})
.collect()
}
fn extrair_datas(texto: &str) -> Vec<String> {
RE_DATA
.captures_iter(texto)
.map(|caps| format!("{}-{}-{}", &caps[3], &caps[2], &caps[1]))
.collect()
}
fn substituir_dados_sensiveis(texto: &str) -> String {
let re_cpf = Regex::new(r"\d{3}\.\d{3}\.\d{3}-\d{2}").unwrap();
let re_email = Regex::new(r"[\w.+-]+@[\w-]+\.[\w.]+").unwrap();
let re_tel = Regex::new(r"\(\d{2}\)\s?\d{4,5}-\d{4}").unwrap();
let resultado = re_cpf.replace_all(texto, "***.***.***-**");
let resultado = re_email.replace_all(&resultado, "[EMAIL REDACTED]");
let resultado = re_tel.replace_all(&resultado, "[TEL REDACTED]");
resultado.to_string()
}
fn main() {
let nota_fiscal = r#"
NOTA FISCAL ELETRÔNICA
Data: 15/03/2024 Hora: 14:30:00
Cliente: Maria Silva
CPF: 123.456.789-00
Email: maria@exemplo.com
Telefone: (11) 98765-4321
Itens:
1. Notebook Dell - R$ 4.599,90
2. Mouse Logitech - R$ 189,90
3. Teclado Mecânico - R$ 459,00
4. Monitor 27" - R$ 2.199,00
Subtotal: R$ 7.447,80
Desconto: R$ 744,78
Total: R$ 6.703,02
Próxima entrega: 20/03/2024
"#;
println!("=== Preços encontrados ===");
let precos = extrair_precos(nota_fiscal);
for (i, preco) in precos.iter().enumerate() {
println!(" {}: R$ {:.2}", i + 1, preco);
}
println!(" Soma: R$ {:.2}", precos.iter().sum::<f64>());
println!("\n=== Datas encontradas (ISO 8601) ===");
let datas = extrair_datas(nota_fiscal);
for data in &datas {
println!(" {}", data);
}
println!("\n=== Dados sensíveis removidos ===");
let censurado = substituir_dados_sensiveis(nota_fiscal);
println!("{}", censurado);
}
Comparação com Alternativas
| Crate | Caso de uso | Performance | Backtracking |
|---|---|---|---|
regex | Uso geral | Excelente (tempo linear) | Não (seguro) |
fancy-regex | Lookahead/lookbehind | Boa | Sim (pode ser lento) |
pcre2 | Compatibilidade PCRE | Muito boa | Sim |
nom | Parsing complexo | Excelente | N/A (parser combinator) |
pest | Gramáticas PEG | Boa | N/A (PEG parser) |
Use regex quando precisar de expressões regulares padrão com garantia de performance. Use fancy-regex se precisar de lookahead/lookbehind. Para parsing complexo com gramáticas, considere nom ou pest.
Conclusão
A crate regex é uma ferramenta indispensável no arsenal de todo desenvolvedor Rust. Sua combinação de performance garantida (tempo linear), API ergonômica e ampla cobertura da sintaxe de expressões regulares a torna ideal para validação, parsing e transformação de texto.
Lembre-se de sempre compilar suas regex fora de loops (preferencialmente com once_cell::sync::Lazy), preferir métodos de String para padrões simples, e usar RegexSet quando precisar testar múltiplos padrões.
Próximos passos:
- Explore a crate serde para combinar regex com serialização de dados
- Veja chrono para parsear datas extraídas com regex
- Confira a documentação completa da sintaxe regex