Expressions regulars per netejar dades: el 20 % que fas servir sempre
Classes, quantificadors, grups i àncores explicats amb casos reals de neteja, i les diferències entre Sheets, Python i pandas que et faran perdre una tarda.
·8 min de lectura
Una expressió regular és un patró que descriu una forma de text. [0-9]{8}[A-Z] vol dir «vuit dígits seguits d’una lletra majúscula», i això és un DNI.
Amb una desena de peces es resol la pràctica totalitat de la feina de neteja de dades. Aquest article són aquestes peces, amb els casos on les faràs servir.
Les peces
Classes de caràcters
Entre claudàtors es posa un conjunt de caràcters possibles, i el patró en consumeix un:
[0-9] un dígit
[A-Z] una majúscula (sense accents)
[A-Za-z] una lletra
[0-9A-Z] un dígit o una majúscula
[^0-9] qualsevol cosa que NO sigui un dígit
L’accent circumflex dins dels claudàtors nega el conjunt. És la peça més útil de totes per netejar: [^0-9] és «tot el que sobra d’un telèfon».
Hi ha abreviatures:
\d igual que [0-9]
\w lletra, dígit o guió baix
\s espai, tabulador o salt de línia
\D \W \S els seus contraris
. qualsevol caràcter
Compte amb \w i els accents: en Python 3 inclou à, ç i ñ per defecte; en altres eines, no. Quan treballis amb noms en català, val més ser explícit: [A-Za-zÀ-ÿ].
Quantificadors
Diuen quantes vegades es repeteix el que tenen just abans:
? zero o una vegada (opcional)
* zero o més
+ una o més
{3} exactament tres
{2,4} entre dues i quatre
{2,} dues o més
\d{8} són vuit dígits. \d+ és «un número de la llargada que sigui». -?\d+ és un número que pot portar signe negatiu.
Àncores
Sense àncores, un patró troba la seva forma en qualsevol lloc del text. Amb elles, dius on ha d’estar:
^ principi del text
$ final del text
\b frontera de paraula
La diferència importa molt:
\d{8}[A-Z]troba un DNI enmig d’un text lliure. Serveix per extreure.^\d{8}[A-Z]$només accepta un text que sigui exactament un DNI i res més. Serveix per validar.
Confondre les dues coses és la causa d’aquell camp de «validació» que dona per bo el meu dni és 12345678Z, gràcies.
\b marca el límit entre una paraula i el que no ho és: \bSL\b troba Acme SL però no SLOGAN.
Grups
Els parèntesis serveixen per a dues coses: aplicar un quantificador a un tros sencer i recuperar aquell tros després.
(\d{2})/(\d{2})/(\d{4})
Sobre 31/12/2026, el grup 1 és 31, el 2 és 12 i el 3 és 2026. En una substitució s’hi fa referència amb $1 o \1 segons l’eina.
També hi ha alternatives amb |:
(SL|SA|SLU|SCP)
I grups amb nom, que fan el codi llegible quan n’hi ha més de dos:
(?P<dia>\d{2})/(?P<mes>\d{2})/(?P<any>\d{4})
Àvid contra mandrós
Aquesta és la que fa perdre més temps a qui comença. Per defecte, els quantificadors són àvids: agafen tot el que poden.
Sobre el text <b>hola</b> <i>adeu</i>:
<.*>captura tot el text, de la primera<fins a l’última>.<.*?>captura només<b>. L’interrogant el fa mandrós: para a la primera coincidència possible.
Cada cop que un patró et retorni «massa», el que falta és una ? després del quantificador.
Els casos reals
Deixar només els dígits d’un telèfon
Patró: [^0-9]
Substituir: (res)
+34 600 12 34 56 → 34600123456. És la primera passa de qualsevol normalització de telèfons. Després decideixes què fas amb el prefix, però ja tens una cosa comparable.
Col·lapsar espais
Patró: \s+
Substituir: un espai
Converteix Acme SL i Acme\tSL en Acme SL. Combinat amb un TRIM, elimina la meitat dels «duplicats» que no ho són.
Extreure un import d’un text lliure
\d{1,3}(?:\.\d{3})*,\d{2}
Casa amb 1.234.567,89 en format català. El (?:...) és un grup que no captura: agrupa per poder-hi posar el *, però no ocupa un número de grup. Fes-lo servir sempre que agrupis sense voler recuperar el contingut.
Separar codi i descripció
Amb un text com A-1042 Manteniment anual:
^([A-Z]-\d{4})\s+(.*)$
Grup 1 el codi, grup 2 la descripció. Les àncores garanteixen que el codi és al principi.
Detectar files que no encaixen
De vegades el patró no és per extreure sinó per trobar el que està malament. Aquesta consulta —«dona’m tot el que no sigui un DNI vàlid»— sol ser el primer informe útil d’una migració.
Una advertència: hi ha coses que la regex no pot fer
Aquest patró no valida un DNI:
^\d{8}[A-Z]$
Comprova la forma, no el contingut. La lletra d’un DNI es calcula amb el residu de dividir el número entre 23, i això una expressió regular no ho sap fer. 00000000A passa el patró i no existeix.
La regla general: la regex descriu formes, no regles. Per a la lletra del DNI, la validació d’un IBAN o el dígit de control d’un codi de barres calen tres línies de codi. Fes servir la regex per aïllar el candidat i el codi per validar-lo.
El mateix val per als correus electrònics. L’expressió que valida de debò un correu segons l’estàndard té milers de caràcters i no serveix de res. A la pràctica:
^[^@\s]+@[^@\s]+\.[^@\s]+$
Filtra el 99 % dels errors de teclat, i la resta la valida un correu de confirmació, que és l’única validació real que existeix.
Les diferències entre eines que et costaran una tarda
Aquí és on tot es complica, perquè no totes les eines parlen el mateix dialecte.
Google Sheets
REGEXEXTRACT, REGEXREPLACE i REGEXMATCH fan servir el motor RE2, que no admet lookahead, lookbehind ni referències enrere. Si has copiat un patró amb (?=...) o (?<=...) i et diu error, és això. No hi ha manera d’activar-ho: has de reescriure el patró sense mirar endavant.
A canvi, RE2 té una propietat que els altres motors no tenen: garanteix temps lineal. Mai es penjarà.
Els grups es recuperen així:
=REGEXEXTRACT(A2; "^([A-Z]-\d{4})")
=REGEXREPLACE(A2; "[^0-9]"; "")
Si el patró té més d’un grup, REGEXEXTRACT torna una cel·la per grup cap a la dreta.
Python
import re
m = re.search(r'(\d{2})/(\d{2})/(\d{4})', text)
if m:
dia, mes, any_ = m.groups()
net = re.sub(r'[^0-9]', '', telefon)
tots = re.findall(r'\d{8}[A-Z]', document)
La r davant de la cometa és obligatòria a la pràctica: sense ella, \d i \b s’interpreten com a seqüències d’escapament de Python i el patró deixa de voler dir el que creus.
Python sí que admet lookarounds. I quan un patró es fa llarg, re.VERBOSE el fa mantenible:
patro = re.compile(r"""
(?P<codi>[A-Z]-\d{4}) # codi de projecte
\s+
(?P<desc>.+) # descripció
""", re.VERBOSE)
Amb re.VERBOSE, els espais i els comentaris del patró s’ignoren. Un patró comentat és un patró que algú podrà tocar d’aquí a un any.
pandas
Sobre una columna sencera, sense bucles:
df['telefon'] = df['telefon'].str.replace(r'\D', '', regex=True)
df['te_dni'] = df['text'].str.contains(r'\d{8}[A-Z]', na=False)
# extract torna un DataFrame amb una columna per grup
df[['dia', 'mes', 'any']] = df['data'].str.extract(r'(\d{2})/(\d{2})/(\d{4})')
L’regex=True de .str.replace() és obligatori: sense ell, pandas tracta el patró com a text literal i no et diu res.
Com treballar-hi sense patir
- Prova el patró abans d’enganxar-lo. A
regex101.com, amb el dialecte correcte seleccionat (Python, RE2, PCRE…), i amb dades reals teves enganxades a sota. T’explica què fa cada peça mentre l’escrius. - Construeix-lo per trossos. Primer
\d{8}, comprova; després\d{8}[A-Z], comprova. Escriure trenta caràcters de cop i preguntar-se per què no funciona és la manera lenta. - Compte amb els quantificadors niats. Patrons com
(\w+)+$poden fer que el motor provi milions de combinacions davant d’una entrada que no casa. En Python o JavaScript això penja el procés; en RE2 no pot passar. - Comenta’l. El teu jo d’aquí sis mesos no reconeixerà el patró que has escrit avui.
El resum en deu línies
[abc] un d'aquests \d dígit
[^abc] cap d'aquests \w lletra/dígit
? opcional \s espai
* zero o més . qualsevol
+ una o més ^ principi
{2,4} entre 2 i 4 $ final
*? el mateix, mandrós \b frontera
(...) grup que es recupera | o
(?:...) grup que no
Amb això es fa el 90 % de la feina. La resta, quan et calgui, la busques.