Expresiones regulares

¿Por qué expresiones regulares?

Imagina un listado de muestreos como codigos_muestreo.csv, 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 -P "^P" codigos_muestreo.csv

En este caso grep con -P (expresiones regulares compatibles con Perl, PCRE) buscaría las líneas que empiezan con P.

Nota

-P es una opción de GNU grep, el que viene con Linux y WSL. El grep que trae macOS no la tiene: si usas Mac, instala GNU grep (brew install grep) y usa ggrep -P.

Con | podemos establecer alternativas a patrones de búsqueda:

~$ grep -P "Genista|Pinus" codigos_muestreo.csv
~$ grep -Pi "sylvestris|silvestris" codigos_muestreo.csv

Contenedores

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 -P "^[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 ciliata

El 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 -P "^[PR]." codigos_muestreo.csv
TipCuestión

¿Puedes expandir el regex para incluir época (A o B) y altura (mayor de 9 metros)?

TipCuestión

¿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 -P "Pinus +sylvestris" registro_campo_bruto.csv

Un 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 -Pi "que?rcus robur" registro_campo_bruto.csv

Cuando 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.

TipCuestión

¿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:

  1. Abre https://regex101.com/.
  2. 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 -P.
  3. Pega unas cuantas líneas de tus datos en el cuadro grande (“TEST STRING”), por ejemplo las filas del ejemplo anterior.
  4. Escribe tu patrón en el cuadro superior (“REGULAR EXPRESSION”) y observa qué se resalta abajo.
  5. Ajusta el patrón hasta que capture exactamente lo que quieres —ni más ni menos— y luego cópialo a tu terminal.
Nota

regex101 no es idéntico a grep -P/sed -E: son “dialectos” de expresiones regulares ligeramente distintos (por ejemplo, \d funciona en Python y en grep -P, pero no en sed -E). 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.

  1. En sequences.fasta selecciona en una sola orden las cabeceras de Quercus_robur y Fragaria_vesca.
  2. En codigos_muestreo.csv, selecciona con grep los muestreos realizados a 3000 m o más (altura 30 o superior en el código). Recuerda que el código son tres letras seguidas de dos dígitos.
  3. En registro_campo_bruto.csv la especie Pinus sylvestris aparece escrita de varias formas: con i o con y, y con uno o más espacios entre género y epíteto. Usa sed para que todas queden escritas como Pinus sylvestris y guarda el resultado en registro_campo_limpio.csv.
  4. ¿Cuántas observaciones de species_observations.csv se hicieron cada mes? Las fechas tienen el formato AAAA-MM-DD: usa sed para quitar el día y quedarte solo con el año y el mes. Recuerda que las líneas de cabecera empiezan por #.
  5. En species_observations.csv las especies están escritas como Genero_epiteto. ¿Qué género tiene más observaciones y cuántas?
  6. Muestra las tres especies de codigos_muestreo.csv muestreadas a mayor altura, con su altura delante (por ejemplo 99,Saxifraga pyrenaica). Usa sed para quitar las tres letras del principio del código.
  7. El archivo tiene una serie de métricas para los diferentes contigs que se han producido tras un ensamblaje de un genoma. Esta información incluye el contig, el tamaño, el porcentaje en G+C, la profundidad de lectura, el grado de cobertura y el resultado de hacer un BLAST de unos contigs contra otros. Hemos detectado una serie de contigs que presentan profundidad y cobertura 0 y queremos filtrarlos.
  8. En orfs.fasta hay cuatro secuencias en una sola línea cada una. Localiza las secuencias que contienen un fragmento de al menos 100 pb que empiece por ATG y termine en un codón de parada (TAA, TAG o TGA). Obten su secuencia y su cabecera (busca en la ayuda del comando grep cómo mostrar la línea anterior a la que coincide).
  9. uniprot_headers.fasta contiene cabeceras de UniProt con el formato >sp|CÓDIGO|PROT_ESPECIE descripción... (o tr| en vez de sp|). Genera una tabla con tres columnas separadas por tabulador: codigo, prot y sp (la especie).
  10. 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 columnas gen,cromosoma,posicion,funcion, donde posicion tenga el formato inicio-fin.

Soluciones

~$ grep -P "Quercus_robur|Fragaria_vesca" sequences.fasta
>seq2|Quercus_robur|matK
>seq4|Fragaria_vesca|trnL
~$ grep -P "^[A-Z]{3}[3-9][0-9]," codigos_muestreo.csv
PAA31,Rhododendron ferrugineum
RSB44,Vaccinium myrtillus
RAA99,Saxifraga pyrenaica
PIO61,Silene ciliata

^[A-Z]{3} son las tres letras del principio del código; [3-9] obliga a que la primera cifra de la altura sea 3 o más, y [0-9] acepta cualquier segunda cifra. La coma final asegura que el código termina ahí.

~$ sed -E 's/Pinus +s[iy]lvestris/Pinus sylvestris/' registro_campo_bruto.csv > registro_campo_limpio.csv
~$ grep "Pinus" registro_campo_limpio.csv
S104,2024-04-05,El Escorial,Pinus sylvestris,Buen estado
S105,2024-04-05,el escorial,Pinus sylvestris,Posible plaga
S106,2024-04-05,El escorial ,Pinus sylvestris,Revisar corteza

+ acepta uno o más espacios entre Pinus y el epíteto, y [iy] acepta tanto silvestris como sylvestris.

~$ grep -v '^#' species_observations.csv | cut -d',' -f2 | sed -E 's/-[0-9]{2}$//' | sort | uniq -c
      2 2024-05
     11 2024-06
     28 2024-07
     24 2024-08
     22 2024-09
     30 2024-10

grep -v '^#' descarta las líneas que empiezan por # (la cabecera). En sed, -[0-9]{2}$ es un guion seguido de dos cifras al final de la línea, es decir, el día.

~$ grep -v '^#' species_observations.csv | cut -d',' -f6 | sed -E 's/_.*//' | sort | uniq -c | sort -nr | head -1
     23 Quercus

_.* es el guion bajo y todo lo que viene detrás (el epíteto): al borrarlo queda solo el género. sort -nr ordena los recuentos de mayor a menor y head -1 se queda con el primero.

~$ tail -n +2 codigos_muestreo.csv | sed -E 's/^[A-Z]{3}//' | sort -nr | head -3
99,Saxifraga pyrenaica
61,Silene ciliata
44,Vaccinium myrtillus

^[A-Z]{3} son las tres letras del principio del código; al borrarlas, cada línea empieza por la altura y sort -nr puede ordenarlas numéricamente de mayor a menor.

~$ grep -v -P '\s0\s+0\s' contig.stats 
~$ grep -P -B 1 "ATG[ACGT]{94,}(TAA|TAG|TGA)" orfs.fasta
>orf1|posible_ORF_largo
CCGGCCATGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGTAACCGGCC

(TAA|TAG|TGA) hace uso del capturing group para introducir alternativas.

~$ 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

Esta solución hace uso de la negación: [^\t] son todos los caracteres menos tabulador.

Una solución más sencilla podría ser:

~$ echo "gen,cromosoma,posicion,funcion"
~$ grep -P '\tgene\t' anotacion.gff3 | sed -E 's/^(.+)\t.+\tgene\t([0-9]+)\t([0-9]+)\t.+Name=([a-zA-Z0-9]+);function=(.+)$/\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