Una herramienta de línea de órdenes completa en Python

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.

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.

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