Expresiones regulares
Vamos a trabajar con varios archivos: sequences.fasta, codigos_muestreo.csv, registro_campo_bruto.csv, orfs.fasta, uniprot_headers.fasta y anotacion.gff3
¿Por qué expresiones regulares?
Imagina un listado de muestreos donde cada fila tiene un código de 5 caracteres seguido de la especie encontrada. El código sigue un formato fijo: localidad (1 letra) + tipo de sustrato (1 letra) + época (1 letra) + altura (2 dígitos, en cientos de metros):
PRA18,Erica arborea
RAB22,Genista florida
GCB07,Betula pendula
Quieres quedarte solo con los muestreos que cumplan las tres condiciones a la vez: localidad P o R, cualquier sustrato, época A o B, y una altura mayor que 9.
Resultaría inviable buscar todas las posibles cadenas de texto con grep. Lo que define a las filas que te interesan es una forma, un patrón, no un texto concreto.
Las expresiones regulares resuelven justo este problema: en vez de escribir el texto exacto que buscas, describes un patrón que ese texto debe cumplir.
¿Qué son las expresiones regulares?
Una expresión regular (regex) es un lenguaje en miniatura para describir patrones de texto: en vez de “busca la cadena exacta X”, permite decir cosas como “busca un código que empiece por P o por R, seguido de cualquier letra, luego A o B, y termine en un número de dos cifras mayor que 9” — el patrón que resuelve el ejemplo anterior.
Un motor de expresiones regulares —integrado en herramientas como grep, sed o el módulo re de Python, que usaremos más adelante— lee ese patrón y lo compara contra el texto para decidir si hay coincidencia (y, con sed, para sustituirla).
Anclas y alternativas
El ancla ^: marca el principio de línea. Su pareja, $, marca el final.
~$ grep -E "^P" codigos_muestreo.csvEn este caso grepcon -E (expresiones regulares extendidas) busccaría las líneas que empiezan con P.
Con | podemos establecer alternativas a patrones de búsqueda:
~$ grep -E "Genista|Pinus" codigos_muestreo.csv~$ grep -Ei "sylvestris|silvestris" codigos_muestreo.csvContenedores
Un contenedor, escrito entre corchetes [...], significa “uno cualquiera de estos caracteres”. [PR] coincide con una P o una R (y con nada más) en esa posición:
~$ grep -E "^[PR]" codigos_muestreo.csv
PRA18,Erica arborea
RAB22,Genista florida
PSI15,Pinus uncinata
RCA05,Juniperus communis
PAA31,Rhododendron ferrugineum
RSB44,Vaccinium myrtillus
PRB08,Arctostaphylos uva-ursi
RAA99,Saxifraga pyrenaica
PIO61,Silene ciliataEl punto . es distinto: no es un contenedor, sino un comodín que coincide con un carácter cualquiera, sea el que sea. Nos sirve para la segunda posición del código (el sustrato), que puede ser cualquier letra:
~$ grep -E "^[PR]." codigos_muestreo.csv¿Puedes expandir el regex para incluir época (A o B) y altura (mayor de 9 metros)?
¿Cómo podría buscar un punto?
Cuantificadores
Un cuantificador indica cuántas veces puede repetirse lo que va justo antes. Los más comunes son * (cero o más veces) y + (una o más veces). Son útiles precisamente para datos sucios, donde no puedes asumir que siempre habrá exactamente un espacio:
~$ grep -E "Pinus +sylvestris" registro_campo_bruto.csvUn tercer cuantificador, ?, marca que lo anterior aparece cero o una vez — es decir, es opcional. Es perfecto para erratas de una sola letra que falta, como esta en registro_campo_bruto.csv (fíjate en la fila S109, “Qurcus” en vez de “Quercus”):
~$ grep -Ei "que?rcus robur" registro_campo_bruto.csvCuando lo que necesitas no es “cero o una vez” sino un número concreto de repeticiones, */+/? se quedan cortos. Las llaves {n}, {n,} y {n,m} cubren ese caso: {n} exige exactamente n repeticiones, {n,} exige n o más, y {n,m} exige entre n y m. Por ejemplo, A{5,} encuentra una tirada de 5 adeninas seguidas o más — útil, entre otras cosas, para detectar fragmentos de una longitud mínima dentro de una secuencia, como verás en los ejercicios.
Grupos de captura
Hasta ahora usabas sed para sustituir texto por otro texto fijo. Pero también puedes capturar trozos del patrón que coincide, envolviéndolos entre paréntesis (...), y reutilizarlos en el reemplazo con \1, \2… (el primer grupo capturado, el segundo…). Es la manera de reordenar o extraer partes de un texto sin tener que escribirlas a mano.
Por ejemplo, cada cabecera de sequences.fasta tiene el formato >seqN|Genero_especie|marcador. Para quedarte solo con el género/especie y el marcador entre paréntesis, descartando el número de secuencia:
~$ grep '^>' sequences.fasta | sed -E 's/^>seq[0-9]+\|([A-Za-z_]+)\|([A-Za-z0-9]+)/\1 (\2)/'El primer paréntesis ([A-Za-z_]+) captura el género y la especie; el segundo, ([A-Za-z0-9]+), captura el marcador. En el reemplazo, \1 (\2) los coloca en el orden que quieras, con el texto que añadas alrededor (aquí, un espacio y unos paréntesis) — el resto de la cabecera (>seq1|, |) simplemente desaparece, porque no forma parte de ningún grupo capturado.
¿Puedes modificar el comando anterior para que muestre el siguiente formato: gen + tabulador + género?
Diseñar y probar patrones
Acertar con un patrón a la primera es difícil incluso con experiencia: es fácil que capture menos —o más— de lo que querías. regex101.com es una herramienta online gratuita que muestra, mientras escribes, qué partes de un texto coinciden con tu patrón —resaltadas en color— junto con una explicación de cada fragmento.
Cómo usarla:
- Abre https://regex101.com/.
- En el panel izquierdo (“FLAVOR”), selecciona Python — es el motor más parecido al que usaremos más adelante en el curso, y comparte casi toda la sintaxis básica con
grep -E. - Pega unas cuantas líneas de tus datos en el cuadro grande (“TEST STRING”), por ejemplo las filas del ejemplo anterior.
- Escribe tu patrón en el cuadro superior (“REGULAR EXPRESSION”) y observa qué se resalta abajo.
- Ajusta el patrón hasta que capture exactamente lo que quieres —ni más ni menos— y luego cópialo a tu terminal.
regex101 no es idéntico a grep -E/sed: son “dialectos” de expresiones regulares ligeramente distintos (por ejemplo, \d funciona en Python pero no en grep -E estándar). Para los patrones de este módulo —alternativas |, anclas ^/$, cuantificadores +/*— el comportamiento es el mismo en ambos, así que puedes probar ahí con confianza y copiar el resultado a la terminal.
Ejercicios
Si te atascas con un patrón, prototípalo primero en regex101.com (flavor Python) pegando las líneas relevantes del archivo, y cuando funcione cópialo a la terminal.
- En
sequences.fasta, usandogrep -Econ alternancia, selecciona en una sola orden las cabeceras deQuercus_roburyFragaria_vesca. - En
registro_campo_bruto.csv, cuenta cuántas filas mencionan la localidad “Sierra de Gredos” en cualquier combinación de mayúsculas/minúsculas. - Generar una versión de
registro_campo_bruto.csven la que todas las apariciones desilvestrisestén corregidas asylvestrisy guárdala enregistro_campo_limpio.csv. - (Reto) Encadena
grepysedcon una tubería (|) para, en una sola línea, quedarte solo con las filas deBetula pendula(en cualquier combinación de mayúsculas/minúsculas) y normalizar esas filas a minúsculas. - En orfs.fasta hay cuatro secuencias en una sola línea cada una. Usando
grep -B 1(para que también se muestre la cabecera de cada coincidencia) y un patrón con cuantificadores, localiza las secuencias que contienen un fragmento de al menos 100 pb que empiece porATGy termine en un codón de parada (TAA,TAGoTGA). Debe darte una sola secuencia — las otras tres fallan cada una por un motivo distinto (demasiado corta, sin codón de parada, o sinATG); identifica cuál es cuál. - uniprot_headers.fasta contiene cabeceras de UniProt con el formato
>sp|CÓDIGO|PROT_ESPECIE descripción...(otr|en vez desp|). Genera una tabla con tres columnas separadas por tabulador:codigo,protysp(la especie). - anotacion.gff3 contiene, para cada gen, una línea de tipo
gene(con su nombre y función en la columna de atributos,Name=...;function=...). Genera un CSV con las columnasgen,cromosoma,posicion,funcion, dondeposiciontenga el formatoinicio-fin.
Soluciones
~$ grep -E "Quercus_robur|Fragaria_vesca" sequences.fasta
>seq2|Quercus_robur|matK
>seq4|Fragaria_vesca|trnL~$ grep -Ei "sierra de gredos" registro_campo_bruto.csv | wc -l
4~$ sed -E 's/silvestris/sylvestris/g' registro_campo_bruto.csv > registro_campo_limpio.csv~$ grep -Ei "betula pendula" registro_campo_bruto.csv | sed -E 's/.*/\L&/'
s107,2024-04-10,sierra de guadarrama,betula pendula,hoja amarillenta
s108,2024-04-10,sierra de guadarrama,betula pendula,cerca del arroyo~$ grep -E -B 1 "ATG[ACGT]{94,}(TAA|TAG|TGA)" orfs.fasta
>orf1|posible_ORF_largo
CCGGCCATGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGTAACCGGCC~$ grep '^>' uniprot_headers.fasta | sed -E 's/^>(sp|tr)\|([A-Z0-9]+)\|([A-Z0-9]+)_([A-Z]+).*/\2\t\3\t\4/'
P12345 KPCA HUMAN
P67890 FAS MOUSE
Q12345 COX1 YEAST
P23456 LDHA ARATH
P45678 P53 HUMAN
P34567 RPB1 ARATH~$ echo "gen,cromosoma,posicion,funcion"
~$ grep -P '\tgene\t' anotacion.gff3 | sed -E 's/^([^\t]+)\t[^\t]+\tgene\t([0-9]+)\t([0-9]+)\t.*Name=([^;]+);function=([^\t]+)$/\4,\1,\2-\3,\5/'
gen,cromosoma,posicion,funcion
RBCS1,chr1,1001-5000,Ribulose bisphosphate carboxylase small chain
PSBA1,chr1,8200-9100,Photosystem II protein D1
MATK1,chr2,305-1890,Maturase K
TRNL1,chr2,4400-4950,Transfer RNA-Leu