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.
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:
Gijón
Alcalá de Henares
Badajoz
Gijón
Teruel
BadajozGijón, Asturias
Avilés, Asturias
Alcalá de Henares, Madrid
Badajoz, Badajoz
Teruel, Teruel
Oviedo, AsturiasQué 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.
sort ciudades.txt | uniqAlcalá de Henares
Badajoz
Gijón
Teruelsort 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.
import sys
for linea in sys.stdin:
print(linea.rstrip("\n").upper())cat provincias.txt | python3 mayusculas.pyGIJÓN, ASTURIAS
AVILÉS, ASTURIAS
ALCALÁ DE HENARES, MADRID
BADAJOZ, BADAJOZ
TERUEL, TERUEL
OVIEDO, ASTURIASEl 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:
import sys
texto = sys.stdin.read()
print(f"Caracteres: {len(texto)}")
print(f"Palabras: {len(texto.split())}")
print(f"Líneas: {len(texto.splitlines())}")cat ciudades.txt | python3 contar.pyCaracteres: 53
Palabras: 8
Líneas: 6Como 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:
import sys
lineas = sys.stdin.readlines()
print(f"He recibido {len(lineas)} líneas")
print(f"La última es: {lineas[-1].strip()}")cat ciudades.txt | python3 primeras.pyHe recibido 6 líneas
La última es: Badajoz| Forma | Qué devuelve | Cuándo usarla |
|---|---|---|
for linea in sys.stdin | Una cadena por vuelta | Casi siempre. Funciona con entradas infinitas |
sys.stdin.read() | Una cadena con todo | Cuando necesitas el texto completo |
sys.stdin.readlines() | Lista de cadenas | Cuando 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:
nombre = input("¿Cómo te llamas? ")
print(f"Hola, {nombre}")echo "Erwin" | python3 preguntar.py¿Cómo te llamas? Hola, ErwinLas 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:
python3 preguntar.py < /dev/null¿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 lineSaber 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.
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:
python3 flexible.pyEscribe una frase: hola que tal estamos
4 palabrasCon una tubería, no pregunta nada:
echo "una frase de cinco palabras justas" | python3 flexible.py6 palabrasY con un fichero redirigido, tampoco:
python3 flexible.py < ciudades.txt8 palabrasUn 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.
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())cat provincias.txt | python3 buscar.py AsturiasGijón, Asturias
Avilés, Asturias
Oviedo, Asturias
3 líneas encontradascat provincias.txt | python3 buscar.py -i -n asturias1: Gijón, Asturias
2: Avilés, Asturias
6: Oviedo, Asturias
3 líneas encontradasY cuando no encuentra nada, lo dice con el código de salida, igual que hace grep:
cat provincias.txt | python3 buscar.py Sevilla
echo $?0 líneas encontradas
1Tres decisiones de diseño que convierten este programa en una pieza encajable:
- El resumen va por
stderr. Si fuera porstdout, 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
stdouty 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:
cat provincias.txt | python3 buscar.py Asturias | sort3 líneas encontradas
Avilés, Asturias
Gijón, Asturias
Oviedo, AsturiasEl 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:
cat provincias.txt | python3 buscar.py Asturias 2>/dev/null | sortAvilés, Asturias
Gijón, Asturias
Oviedo, AsturiasEso 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:
for numero in range(1, 1_000_001):
print(numero)python3 numeros.py | head -31
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 pipeNo 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.
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)python3 numeros.py | head -31
2
3Encadenar 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:
cat provincias.txt | python3 mayusculas.py | sortALCALÁ DE HENARES, MADRID
AVILÉS, ASTURIAS
BADAJOZ, BADAJOZ
GIJÓN, ASTURIAS
OVIEDO, ASTURIAS
TERUEL, TERUELcat provincias.txt | python3 buscar.py -i a 2>/dev/null | head -2Gijón, Asturias
Avilés, AsturiasY los comandos de la terminal que más vas a encadenar con los tuyos:
| Comando | Qué hace |
|---|---|
cat fichero | Vuelca el contenido de un fichero por stdout |
sort | Ordena las líneas alfabéticamente |
sort -n | Las ordena como números |
uniq | Elimina líneas repetidas consecutivas, por eso suele ir tras sort |
head -5 | Se queda con las 5 primeras líneas |
tail -5 | Se queda con las 5 últimas |
wc -l | Cuenta las líneas |
grep texto | Filtra las líneas que contienen un texto |