Python Lernhandbuch

Kapitel 13: SFTP-Server anbinden

Musterlösung mit Erklärung

Deine Lösung hat die Prüfung noch nicht bestanden. Lies die Erklärung, dann versuche es erneut – möglichst ohne abzuschreiben.
Aufgabe noch einmal lesen

Schreibe sftp_holen.py, das:

  1. alle .csv-Dateien im Server-Ordner upload in den lokalen Ordner eingang/ herunterlädt (Ordner anlegen, falls nötig),
  2. jede erfolgreich heruntergeladene Datei auf dem Server in den Ordner upload/archiv verschiebt (Ordner anlegen, falls nötig),
  3. Dateien überspringt, die lokal schon vorhanden sind, und das mit logging meldet,
  4. Zugangsdaten aus Umgebungsvariablen liest.
python
import logging
import os
from pathlib import Path

import paramiko

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger(__name__)

REMOTE = 'upload'
ARCHIV = f'{REMOTE}/archiv'
LOKAL = Path('eingang')


def verbinde():
    ssh = paramiko.SSHClient()
    ssh.load_system_host_keys()
    ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
    ssh.connect(
        hostname=os.environ['SFTP_HOST'],
        port=int(os.environ.get('SFTP_PORT', '22')),
        username=os.environ['SFTP_USER'],
        password=os.environ['SFTP_PASSWORD'],
        timeout=15,
    )
    return ssh


def stelle_ordner_sicher(sftp, pfad):
    try:
        sftp.stat(pfad)
    except FileNotFoundError:
        sftp.mkdir(pfad)


def main():
    LOKAL.mkdir(exist_ok=True)

    with verbinde() as ssh, ssh.open_sftp() as sftp:
        stelle_ordner_sicher(sftp, ARCHIV)

        for name in sftp.listdir(REMOTE):
            if not name.lower().endswith('.csv'):
                continue

            ziel = LOKAL / name
            if ziel.exists():
                log.warning('%s existiert lokal schon, übersprungen', name)
                continue

            sftp.get(f'{REMOTE}/{name}', str(ziel))
            sftp.rename(f'{REMOTE}/{name}', f'{ARCHIV}/{name}')
            log.info('%s geholt und archiviert', name)


if __name__ == '__main__':
    main()

Was passiert:

  • sftp.listdir(REMOTE) liefert alle Namen im Ordner, auch archiv. Dank endswith('.csv') wird der Ordner übersprungen, ebenso andere Dateitypen. lower() fängt .CSV ab.
  • stelle_ordner_sicher fragt per stat, ob der Ordner da ist. Fehlt er, löst stat einen FileNotFoundError aus, der abgefangen wird, und der Ordner wird angelegt. Dieses Muster "erst versuchen, bei Fehler anlegen" ist in Python üblich.
  • Erst nach erfolgreichem get wird verschoben. Schlägt der Download fehl, bleibt die Datei auf dem Server liegen und wird beim nächsten Lauf erneut versucht.
  • Die Log-Meldungen nutzen %s-Platzhalter statt f-Strings. Das ist die empfohlene Schreibweise für logging, denn der Text wird nur zusammengebaut, wenn die Meldung tatsächlich ausgegeben wird.

Warum so: Ein Skript, das Dateien holt und archiviert, kann ohne Sorge wiederholt laufen (zum Beispiel per Cron oder als geplanter Container), weil schon verarbeitete Dateien nicht erneut geholt werden. Das macht Automatisierung robust.