Scripting

Python para Sysadmin — Automação e Scripting

Guia prático de Python para administração de sistemas: parsing de logs, web scraping, automação de tarefas, CLI com argparse e click, boas práticas.

Python não serve só para web apps e machine learning. No terminal, é uma navalha suíça que substitui grep, awk, sed e curl — tudo ao mesmo tempo, com uma legibilidade que o Bash nunca terá.

Quando comecei como sysadmin, o meu dia era um loop de comandos: grep | awk | sort | uniq -c | sort -rn. Funcionava, mas cada pipeline era um castelo de cartas. Um espaço a mais, uma regex mal escapada, e o output ia para o lixo.

Python mudou isso. Com re, pathlib, subprocess e argparse, consigo fazer em 30 linhas o que antes eram 5 pipes frágeis. E o código fica legível, reutilizável e com tratamento de erros a sério.

Python para Sysadmin — terminal

Script de parsing de logs Apache em Python com output colorido e argumentos CLI


Parsing de logs com re

Os logs são o pão nosso de cada dia do sysadmin. Em Bash, fazes grep | cut | sort. Em Python, fazes uma regex que captura logo os campos todos:

import re

# Log Apache combinado: IP - - [data] "METHOD URL PROTO" STATUS BYTES "REFERER" "UA"
LOG_PATTERN = re.compile(
    r'^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d+) (\d+|-) "([^"]*)" "([^"]*)"'
)

def parse_log_line(line: str) -> dict | None:
    """Parseia uma linha de log Apache. Devolve dict ou None."""
    match = LOG_PATTERN.match(line)
    if not match:
        return None

    return {
        "ip": match.group(1),
        "data": match.group(2),
        "metodo": match.group(3),
        "url": match.group(4),
        "status": int(match.group(5)),
        "bytes": int(match.group(6)) if match.group(6) != "-" else 0,
    }
Copy

Dica: Compila a regex com re.compile() uma vez, fora do loop. É muito mais rápido do que compilar de novo para cada linha. Num ficheiro com 100k linhas, a diferença é de segundos para minutos.


Web scraping com requests + BeautifulSoup

Precisas de monitorizar uma página que não tem API? Python é o teu aliado:

import logging
import requests
from bs4 import BeautifulSoup

logger = logging.getLogger(__name__)

def check_cert_expiry(url: str) -> str | None:
    """Verifica data de expiração de SSL via página de status."""
    try:
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.text, 'html.parser')
        cert_table = soup.find('table', class_='cert-details')
        if cert_table and cert_table.find('tr'):
            return cert_table.find_all('td')[3].text.strip()
    except (requests.RequestException, AttributeError) as e:
        logger.error(f"Erro ao aceder a {url}: {e}")
        return None
Copy

Automação de tarefas: shutil, pathlib, subprocess

O trio que substitui a maioria dos comandos de shell em Python. pathlib para caminhos, shutil para operações de ficheiros, subprocess para comandos externos.

from datetime import datetime
from pathlib import Path
import gzip
import shutil
import subprocess
import logging

logger = logging.getLogger(__name__)

def rotate_logs(log_dir: str, max_days: int = 30) -> list[Path]:
    """Remove logs mais velhos que max_days."""
    removed = []
    agora = datetime.now()
    for f in Path(log_dir).glob("*.log*"):
        modificado = datetime.fromtimestamp(f.stat().st_mtime)
        if (agora - modificado).days > max_days:
            f.unlink(missing_ok=True)
            logger.info(f"Removido log antigo: {f.name}")
            removed.append(f)
    return removed

def compress_logs(log_dir: str) -> bool:
    """Comprime logs não compactados com gzip."""
    for f in Path(log_dir).glob("access.log.*[0-9]"):
        if f.suffix != '.gz':
            dest = f.with_name(f.name + '.gz')
            with open(f, 'rb') as src, gzip.open(dest, 'wb') as out:
                shutil.copyfileobj(src, out)
            f.unlink()
            logger.info(f"Comprimido: {f.name}")
    return True
Copy

Atenção: Prefere subprocess.run() com check=True em vez de os.system(). O primeiro lança exceção se o comando falhar, o segundo engole erros silenciosamente. E prefere o módulo gzip a invocar o binário gzip por subprocesso: é mais simples e evita bloqueios de pipe quando ninguém lê o stdout do processo filho.


CLI com argparse e click

Um bom script de sysadmin precisa de argumentos. O argparse vem incluído no Python e é mais que suficiente para 90% dos casos:

import argparse

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Analisa logs Apache e exporta para CSV",
        epilog="Ex: python3 log_analyzer.py access.log -o relatorio.csv --top 20"
    )
    parser.add_argument('logfile', help="Caminho para o ficheiro de log")
    parser.add_argument('-o', '--output', default='relatorio.csv')
    parser.add_argument('--top', type=int, default=10)
    parser.add_argument('-v', '--verbose', action='store_true')
    return parser.parse_args()

args = parse_args()
print(f"A analisar {args.logfile}, top {args.top} IPs...")
Copy

Para algo mais declarativo, o click é fantástico:

import click

@click.command()
@click.argument('logfile')
@click.option('-o', '--output', default='relatorio.csv')
@click.option('--top', default=10, type=int)
@click.option('-v', '--verbose', is_flag=True)
def analyze(logfile, output, top, verbose):
    """Analisa logs Apache e exporta CSV com os IPs mais frequentes."""
    click.echo(f"A analisar {logfile}...")
Copy

Boas práticas para scripts de sysadmin

PráticaPorquê
if __name__ == "__main__"Permite importar funções sem executar o script. Essencial para testes e reutilização
Type hintsDocumentam o código e permitem deteção de erros com mypy/pyright
logging em vez de printNíveis (INFO, ERROR, DEBUG), output para ficheiro, formatação consistente
VirtualenvsIsolam dependências. Usa python -m venv .venv e pip install -r requirements.txt
pathlib em vez de os.pathAPI moderna, orientada a objetos, mais legível e menos propensa a erros

Exemplo prático: analisador de logs Apache

"""log_analyzer.py — Analisa logs Apache, extrai IPs e exporta CSV.

Uso:
    python3 log_analyzer.py /var/log/apache2/access.log -o top_ips.csv --top 20
"""
import argparse
import csv
import logging
import re
from collections import Counter
from pathlib import Path

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)

# Regex para log combinado Apache
LOG_RE = re.compile(
    r'^(\S+) \S+ \S+ \[([^\]]+)\] "\S+ (\S+) \S+" (\d+) \S+'
)

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('logfile', type=Path, help="Ficheiro de log Apache")
    parser.add_argument('-o', '--output', type=Path, default=Path('top_ips.csv'))
    parser.add_argument('--top', type=int, default=10)
    parser.add_argument('-v', '--verbose', action='store_true')
    return parser.parse_args()

def count_ips(logfile: Path) -> Counter[str]:
    """Lê o log e devolve um Counter de IPs."""
    counter: Counter[str] = Counter()
    with open(logfile, 'r') as f:
        for line in f:
            match = LOG_RE.match(line)
            if match:
                counter[match.group(1)] += 1
    logger.info(f"Processadas {sum(counter.values())} linhas, "
                f"{len(counter)} IPs únicos")
    return counter

def export_csv(counter: Counter[str], output: Path, top_n: int) -> None:
    """Exporta top N IPs para CSV."""
    with open(output, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['IP', 'Requests'])
        for ip, count in counter.most_common(top_n):
            writer.writerow([ip, count])
    logger.info(f"CSV exportado: {output} ({top_n} linhas)")

def main() -> None:
    args = parse_args()
    if args.verbose:
        logger.setLevel(logging.DEBUG)
    if not args.logfile.exists():
        logger.error(f"Ficheiro não encontrado: {args.logfile}")
        raise SystemExit(1)
    counter = count_ips(args.logfile)
    export_csv(counter, args.output, args.top)
    print("\nTop 5 IPs:")
    for ip, count in counter.most_common(5):
        print(f"  {ip:20s} → {count:5d} requests")

if __name__ == "__main__":
    main()
Copy
Saída do script:
$ python3 log_analyzer.py access.log --top 20
14:32:01 [INFO] Processadas 45231 linhas, 389 IPs únicos
14:32:01 [INFO] CSV exportado: top_ips.csv (20 linhas)
Top 5 IPs:
  192.168.1.100        →  4521 requests
  10.0.0.45            →  3210 requests
  203.0.113.42         →  2899 requests
  198.51.100.77        →  1543 requests
  192.0.2.88           →  1201 requests
Copy

Recapitulando

  • Logs: re.compile() + named groups para parsing rápido e legível
  • Web scraping: requests + BeautifulSoup para monitorização sem API
  • Automação: pathlib + shutil + subprocess.run(check=True)
  • CLI: argparse para o básico, click para algo mais declarativo
  • Boas práticas: type hints, logging, __name__ == "__main__", virtualenvs
  • Exemplo completo: parser de logs Apache com output CSV

Python é a ferramenta mais versátil que um sysadmin pode ter na caixa. A curva de aprendizagem é curta, o retorno é enorme.

Pega num script teu que uses grep | awk | sort e reescreve-o em Python. Vais ver que fica mais legível, mais robusto e mais fácil de manter.

Recursos adicionais

Partilhar

Comentários 0

Ainda sem comentários — sê o primeiro.