Saltar al contenido
🐍 Curso de Python
/

Módulo 2 · Lección 15 de 28

Tuberías y entrada estándar

Cómo leer datos que llegan por una tubería para que tu programa encaje con el resto de herramientas de la terminal.

⏱ 18 min de lectura

Cuando Unix se diseñó, en los años setenta, sus autores tomaron una decisión que sigue mandando hoy: en vez de hacer programas enormes que lo hicieran todo, harían programas diminutos que hicieran una sola cosa bien, y un mecanismo para encadenarlos. Ese mecanismo es la tubería.

Por eso en la terminal no existe un comando "busca las líneas que contengan Asturias, ordénalas y enséñame las tres primeras". Existen grep, sort y head, y se encadenan. Tu herramienta puede entrar en ese juego, y para eso solo tiene que cumplir una condición: aceptar datos por la entrada estándar y escribir su resultado por la salida estándar.

Todos los ejemplos de la lección usan estos dos ficheros, así que créalos antes de seguir:

ciudades.txt
Gijón
Alcalá de Henares
Badajoz
Gijón
Teruel
Badajoz
provincias.txt
Gijón, Asturias
Avilés, Asturias
Alcalá de Henares, Madrid
Badajoz, Badajoz
Teruel, Teruel
Oviedo, Asturias

Qué es una tubería#

La barra vertical | conecta dos programas: lo que el de la izquierda escribe por stdout le llega al de la derecha por stdin. Ni fichero temporal, ni disco, ni nada; el sistema pasa los datos de uno a otro directamente.

bash
sort ciudades.txt | uniq
Salida en pantalla
Alcalá de Henares
Badajoz
Gijón
Teruel

sort no sabe nada de uniq y uniq no sabe nada de sort. El único contrato entre ellos es texto por líneas. Y como el contrato es tan simple, se pueden encadenar tantos como quieras.

Esto explica por qué en la lección anterior insistíamos tanto en no mezclar stdout y stderr: en una tubería, todo lo que escribas por stdout se convierte en la entrada del siguiente programa. Un "Procesando..." colado ahí dentro no es un mensaje, es basura en los datos de otro.

Leer la entrada estándar en Python#

sys.stdin es un objeto fichero, igual que el que te devuelve open(). Lo que sabes de ficheros vale tal cual aquí.

Iterando línea a línea#

Es la forma habitual y la única razonable cuando la entrada puede ser grande, porque procesa una línea, la suelta y coge la siguiente sin cargarlo todo en memoria.

mayusculas.py
import sys

for linea in sys.stdin:
        print(linea.rstrip("\n").upper())
bash
cat provincias.txt | python3 mayusculas.py
Salida en pantalla
GIJÓN, ASTURIAS
AVILÉS, ASTURIAS
ALCALÁ DE HENARES, MADRID
BADAJOZ, BADAJOZ
TERUEL, TERUEL
OVIEDO, ASTURIAS

El rstrip("\n") no es opcional: cada línea llega con su salto de línea al final, y print añade otro. Sin él saldría todo con una línea en blanco de por medio.

De golpe, con read()#

sys.stdin.read() devuelve todo lo que entre, como una única cadena, y no vuelve hasta que la entrada se acaba. Sirve cuando necesitas el texto entero para responder, como hace wc:

contar.py
import sys

texto = sys.stdin.read()
print(f"Caracteres: {len(texto)}")
print(f"Palabras: {len(texto.split())}")
print(f"Líneas: {len(texto.splitlines())}")
bash
cat ciudades.txt | python3 contar.py
Salida en pantalla
Caracteres: 53
Palabras: 8
Líneas: 6

Como lista, con readlines()#

sys.stdin.readlines() también lo lee todo, pero te lo da como una lista de cadenas con el salto de línea incluido. Es cómodo cuando necesitas contarlas o acceder a ellas por posición:

primeras.py
import sys

lineas = sys.stdin.readlines()
print(f"He recibido {len(lineas)} líneas")
print(f"La última es: {lineas[-1].strip()}")
bash
cat ciudades.txt | python3 primeras.py
Salida en pantalla
He recibido 6 líneas
La última es: Badajoz
FormaQué devuelveCuándo usarla
for linea in sys.stdinUna cadena por vueltaCasi siempre. Funciona con entradas infinitas
sys.stdin.read()Una cadena con todoCuando necesitas el texto completo
sys.stdin.readlines()Lista de cadenasCuando necesitas contar o indexar

input() lee de lo mismo#

input() no es un mecanismo aparte: lee una línea de sys.stdin, exactamente de donde lee todo lo demás. Lo que pasa es que hasta ahora stdin venía siempre del teclado. Si le enchufas una tubería, input() la lee sin enterarse:

preguntar.py
nombre = input("¿Cómo te llamas? ")
print(f"Hola, {nombre}")
bash
echo "Erwin" | python3 preguntar.py
Salida en pantalla
¿Cómo te llamas? Hola, Erwin

Las dos cosas salen en la misma línea porque el mensaje de input no lleva salto al final y nadie ha tecleado nada.

Y si la entrada se acaba y input() sigue pidiendo, salta un EOFError:

bash
python3 preguntar.py < /dev/null
Salida en pantalla
¿Cómo te llamas? Traceback (most recent call last):
  File "/Users/erwin/curso-python/preguntar.py", line 1, in <module>
    nombre = input("¿Cómo te llamas? ")
             ~~~~~^^^^^^^^^^^^^^^^^^^^^
EOFError: EOF when reading a line

Saber si hay algo entrando#

Una herramienta bien hecha admite las dos formas de uso: si le enchufas una tubería, la procesa; si la lanzas a mano, te pregunta. Para distinguirlo está sys.stdin.isatty().

Un tty es un terminal de verdad, con su teclado y su pantalla. isatty() devuelve True si la entrada viene de ahí y False si viene de una tubería o de un fichero redirigido.

flexible.py
import sys


def main() -> int:
        if sys.stdin.isatty():
        # Nadie ha conectado una tubería: se lo pedimos a la persona.
                texto = input("Escribe una frase: ")
    else:
        # Los datos vienen de una tubería o de un fichero redirigido.
                texto = sys.stdin.read()

    print(f"{len(texto.split())} palabras")
    return 0


raise SystemExit(main())

Lanzado a mano, pregunta:

bash
python3 flexible.py
Salida en pantalla
Escribe una frase: hola que tal estamos
4 palabras

Con una tubería, no pregunta nada:

bash
echo "una frase de cinco palabras justas" | python3 flexible.py
Salida en pantalla
6 palabras

Y con un fichero redirigido, tampoco:

bash
python3 flexible.py < ciudades.txt
Salida en pantalla
8 palabras

Un filtro completo#

Con esto ya se puede escribir un filtro de verdad. Este es un grep reducido: lee líneas, se queda con las que contienen un texto y devuelve un código de salida que dice si encontró algo.

buscar.py
import argparse
import sys


def main() -> int:
        analizador = argparse.ArgumentParser(
        description="Muestra las líneas de la entrada que contienen un texto."
    )
    analizador.add_argument("patron", help="Texto que hay que buscar")
    analizador.add_argument(
        "-i", "--ignorar-mayusculas",
        action="store_true",
        help="No distinguir mayúsculas de minúsculas",
    )
    analizador.add_argument(
        "-n", "--numerar",
        action="store_true",
        help="Mostrar el número de línea delante",
    )
    argumentos = analizador.parse_args()

    patron = argumentos.patron
    if argumentos.ignorar_mayusculas:
                patron = patron.lower()

    encontradas = 0
    for numero, linea in enumerate(sys.stdin, start=1):
                linea = linea.rstrip("\n")
        comparable = linea.lower() if argumentos.ignorar_mayusculas else linea
        if patron in comparable:
                        encontradas += 1
            print(f"{numero}: {linea}" if argumentos.numerar else linea)

    print(f"{encontradas} líneas encontradas", file=sys.stderr)
    return 0 if encontradas else 1


raise SystemExit(main())
bash
cat provincias.txt | python3 buscar.py Asturias
Salida en pantalla
Gijón, Asturias
Avilés, Asturias
Oviedo, Asturias
3 líneas encontradas
bash
cat provincias.txt | python3 buscar.py -i -n asturias
Salida en pantalla
1: Gijón, Asturias
2: Avilés, Asturias
6: Oviedo, Asturias
3 líneas encontradas

Y cuando no encuentra nada, lo dice con el código de salida, igual que hace grep:

bash
cat provincias.txt | python3 buscar.py Sevilla
echo $?
Salida en pantalla
0 líneas encontradas
1

Tres decisiones de diseño que convierten este programa en una pieza encajable:

  • El resumen va por stderr. Si fuera por stdout, la línea "3 líneas encontradas" se colaría entre los datos que recibe el siguiente programa de la cadena.
  • Los datos van por stdout y nada más que los datos.
  • El código de salida distingue encontrar de no encontrar, así que se puede usar con &&.

Ahora encaja en cualquier cadena:

bash
cat provincias.txt | python3 buscar.py Asturias | sort
Salida en pantalla
3 líneas encontradas
Avilés, Asturias
Gijón, Asturias
Oviedo, Asturias

El resumen aparece arriba porque sort necesita leerlo todo antes de poder ordenar, así que sus resultados llegan al final. Y si el resumen te molesta, lo tiras:

bash
cat provincias.txt | python3 buscar.py Asturias 2>/dev/null | sort
Salida en pantalla
Avilés, Asturias
Gijón, Asturias
Oviedo, Asturias

Eso es lo que ganas separando los dos flujos: quien usa tu programa puede quedarse solo con la parte que le interesa.

Cuando la tubería se rompe#

Prueba a encadenar un programa que escriba mucho con head, que solo quiere las primeras líneas:

numeros.py
for numero in range(1, 1_000_001):
        print(numero)
bash
python3 numeros.py | head -3
Salida en pantalla
1
2
3
Traceback (most recent call last):
  File "/Users/erwin/curso-python/numeros.py", line 2, in <module>
    print(numero)
    ~~~~~^^^^^^^^
BrokenPipeError: [Errno 32] Broken pipe
Exception ignored while flushing sys.stdout:
BrokenPipeError: [Errno 32] Broken pipe

No has hecho nada mal. head ha leído sus tres líneas, ha decidido que ya tiene bastante y ha cerrado su extremo de la tubería. Tu programa sigue escribiendo hacia un tubo que ya no tiene salida al otro lado, y el sistema le avisa con un BrokenPipeError.

Es una situación normalísima en la terminal, así que lo correcto es tratarla como lo que es: una señal de "ya no me hace falta más", no un error del que informar.

numeros.py
import os
import sys


def main() -> int:
        for numero in range(1, 1_000_001):
                print(numero)
    return 0


try:
        codigo = main()
except BrokenPipeError:
    # El programa de la derecha ha cerrado la tubería. Mandamos lo que quede
    # de stdout a la basura para que Python no falle otra vez al vaciarlo.
        basura = os.open(os.devnull, os.O_WRONLY)
    os.dup2(basura, sys.stdout.fileno())
    codigo = 141


raise SystemExit(codigo)
bash
python3 numeros.py | head -3
Salida en pantalla
1
2
3

Encadenar de verdad#

Con todo lo anterior, tu programa ya es un ciudadano más de la terminal. Estas cadenas funcionan sin que hayas escrito una línea de código para ellas:

bash
cat provincias.txt | python3 mayusculas.py | sort
Salida en pantalla
ALCALÁ DE HENARES, MADRID
AVILÉS, ASTURIAS
BADAJOZ, BADAJOZ
GIJÓN, ASTURIAS
OVIEDO, ASTURIAS
TERUEL, TERUEL
bash
cat provincias.txt | python3 buscar.py -i a 2>/dev/null | head -2
Salida en pantalla
Gijón, Asturias
Avilés, Asturias

Y los comandos de la terminal que más vas a encadenar con los tuyos:

ComandoQué hace
cat ficheroVuelca el contenido de un fichero por stdout
sortOrdena las líneas alfabéticamente
sort -nLas ordena como números
uniqElimina líneas repetidas consecutivas, por eso suele ir tras sort
head -5Se queda con las 5 primeras líneas
tail -5Se queda con las 5 últimas
wc -lCuenta las líneas
grep textoFiltra las líneas que contienen un texto