Esto junta en un solo archivo casi todo lo de los ejemplos anteriores: analizar argumentos, leer archivos o la entrada estándar, dar la salida en texto o en JSON, y terminar con el código que espera un guion.
También te puede interesar
Qué hace
salida$ python3 contar.py texto.txt
3 13 62 texto.txt
$ python3 contar.py texto.txt otro.txt
3 13 62 texto.txt
1 4 20 otro.txt
4 17 82 total
$ cat texto.txt | python3 contar.py
3 13 62 (entrada estándar)
salida$ python3 contar.py -n 3 texto.txt
3 13 62 texto.txt
gato 2
duerme 2
perro 1
La forma del programa
def main(argv=None) -> int:
p = argparse.ArgumentParser(prog="contar", description=__doc__)
...
a = p.parse_args(argv)
...
return 0 if not fallos else 1
if __name__ == "__main__":
sys.exit(main())
Toda la lógica en una función que devuelve el código de salida, y sys.exit una sola vez, en el arranque. Eso permite probarla con assert main(["-n","3","f.txt"]) == 0 sin lanzar un proceso, y evita que un sys.exit enterrado se salte la limpieza pendiente.
Fíjate también en description=__doc__: la ayuda sale de la cadena de documentación del módulo, así que no hay dos textos que mantener.
Archivos o entrada estándar
fuentes = [(str(f), f) for f in a.archivos] or [("(entrada estándar)", None)]
for nombre, ruta in fuentes:
try:
texto = ruta.read_text(encoding="utf-8") if ruta else sys.stdin.read()
except FileNotFoundError:
print(f"contar: no existe {nombre}", file=sys.stderr); fallos += 1; continue
except UnicodeDecodeError:
print(f"contar: {nombre} no es texto UTF-8", file=sys.stderr); fallos += 1; continue
salidacontar: no existe fantasma.txt -> código 1 contar: bin.dat no es texto UTF-8 -> código 1
Tres costumbres de Unix que conviene respetar: sin archivos se lee de la entrada estándar —eso es lo que permite encadenar con tuberías—; si uno falla se sigue con los demás y se devuelve error al final; y los mensajes de error van a sys.stderr, no a la salida normal.
UnicodeDecodeError es el que te salta al pasarle un binario por error. Capturarlo y dar un mensaje claro es la diferencia entre una herramienta y un guion: lo contrario es una traza de diez líneas que no dice qué archivo era.Dos salidas: para leer y para procesar
if a.json:
print(json.dumps(salidas, ensure_ascii=False, indent=2))
else:
for d in salidas:
print(f"{d['lineas']:8d}{d['palabras']:8d}{d['caracteres']:8d} {d['archivo']}")
contar --json -n 2 texto.txt[
{
"archivo": "texto.txt",
"lineas": 3,
"palabras": 13,
"caracteres": 62,
"frecuentes": [["gato", 2], ["duerme", 2]]
}
]
Que una herramienta tenga una bandera --json es lo que permite encadenarla con jq o llamarla desde otro guion sin analizar texto con expresiones regulares. Cuesta tres líneas y cambia para qué sirve.
Contar palabras de verdad
import re
from collections import Counter
PALABRA = re.compile(r"[^\W\d_]+", re.UNICODE)
def top(texto, n):
c = Counter(p.lower() for p in PALABRA.findall(texto) if len(p) > 3)
return c.most_common(n)
Esa expresión regular dice «uno o más caracteres que no sean ni separador, ni dígito, ni guion bajo», que con re.UNICODE incluye los acentos y la eñe. Partir por espacios dejaría «casa,» y «casa» como palabras distintas.
Counter es un diccionario que cuenta, y most_common(n) devuelve los más frecuentes ya ordenados. Hacer eso a mano son diez líneas.
Instalarla
# 1. tal cual python3 contar.py -n 5 informe.txt # 2. como orden del sistema # primera línea del archivo: #!/usr/bin/env python3 chmod +x contar.py && ./contar.py -n 5 informe.txt # 3. como paquete, con su orden propia pip install -e . # con [project.scripts] en pyproject.toml contar -n 5 informe.txt
La tercera es la de un proyecto de verdad, y está explicada en la última lección del curso. Para un archivo suelto que quieres poder copiar a cualquier máquina con Python, la primera.
| Costumbre | Por qué |
|---|---|
Lógica en main(argv=None) |
se puede probar sin lanzar un proceso |
Devolver el código, no llamar a exit |
un solo sitio decide |
Errores a sys.stderr |
orden > archivo deja el archivo limpio |
| Seguir tras un archivo que falla | y devolver 1 al final |
| Leer de la entrada estándar sin argumentos | permite usarlo en una tubería |
Una bandera --json |
lo hace encadenable con otras herramientas |
Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.