Serienbriefe mit Python

  Ralf Hersel   Lesezeit: 8 Minuten  🗪 3 Kommentare

Einen Mustertext mit Platzhaltern durch eine Liste von Inhalten ersetzten. Mit Python geht das ganz einfach.

serienbriefe mit python

Das ist ein typischer Linux-Artikel. Wenn dich etwas juckt, dann ändere es! Zurzeit bin ich damit beschäftigt, für GNU/Linux.ch einige Serien-E-Mails zu schreiben. Dabei gibt es einen Grundtext, der für viele Adressaten personalisiert werden muss. Als Erstes fällt einem ein Serienbrief in LibreOffice Writer als Mittel der Wahl ein. Nach fünf Minuten, mit dem Serienbrief-Assistenten, hatte ich die Nase voll, und beschloss ein eigenes Programm für Serienbriefe zu schreiben.

Vermutlich gibt es schon hunderte Shell-Skripte, die diese Aufgabe lösen. Da ich nach kurzer Suche keine gefunden hatte, entschloss ich mich, diesen Anwendungsfall durch eine eigene Lösung abzudecken. Die Bestandteile sind ein Mustertext, der mit Platzhaltern ergänzt ist. Diese Platzhalter sollen durch individuelle Inhalte durch das Programm ersetzt werden. Diese Inhalte stehen in einer CSV-Datei, wobei die Namen der Platzhalter in der ersten Zeile der Datei stehen.

Damit ihr euch das besser vorstellen könnt, folgt nun eine ausführliche Erklärung:

Den Musterbrief zeigte ich zuerst ohne Platzhalter:

Freiesache GmbH
Stallmannweg 42
CH-8000 Zürich

Sehr geehrte Frau Müller,

Uns gefällt Ihre Firma Freiesache Gmbh sehr gut.

Und nun der Brief mit Platzhaltern:

[Firma]
[Adresse]

[Anrede],

Uns gefällt Ihre Firma [Firma] sehr gut.

Die Separatoren für die Platzhalter "[ und ]" können frei gewählt werden. Ebenso sind Zeilenumbrüche in den Daten für die Platzhalter möglich. Auch dieser Separator ist frei wählbar; als Standard ist das Zeichen "¬" vorgesehen. Der Musterbrief muss als Text-Datei und die Daten in Form einer CSV-Datei vorliegen. Hier nun ein Beispiel für die Daten-Datei:

Adresse|Anrede|Firma
Hauptstrasse 2¬CH-8000 Zürich|Sehr geehrter Herr Müller|Datasoft GmbH
Dummweg 3¬D-53000 Bonn|Sehr geehrter Frau Meier|Bullshit AG
Alte Strasse 4¬CH-5000 Bern|Sehr geehrte Frau Schneider|Verein Huibuh

Die erste Zeile enthält die Namen der Platzhalter. Danach folgen pro Zeile die Daten für die Serienbriefe. Als Separator wird das Zeichen "|" verwendet; auch das ist einstellbar. Ein Zeilenumbruch erfolgt durch das "¬" Zeichen. Das ist insbesondere bei der Adresse sinnvoll.

Der Python-Code sieht so aus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Name:           bulky.py
Description:    replace tags in master file with fields from data file
Usage:          bulky.py master.txt data.csv
Author:         Ralf Hersel - ralf.hersel@rum3ber.ch
License:        GPL3
Date:           2022-12-02
Version:        0.01
'''

# === Constants ================================================================

SEPARATOR = '|'
TAG_START = '['
TAG_END = ']'
LINE_BREAK = '¬'

# === Features =================================================================

def read_master(master_file):                                                   # read letter master file
    with open(master_file) as f:
        master = f.read()
    return master

def read_data(data_file):                                                       # read replacement data
    data = []
    with open(data_file) as f:
        for line in f:
            rec = line.strip().split(SEPARATOR)                                 # remove trailing line break and convert to list
            data.append(rec)
    tags = data[0]
    data = data[1:]                                                             # date without tag header
    return tags, data

def replace_tags_with_data(master, tags, data):                                 # replace tags in master with individual content
    letter = []
    count = 0                                                                   # counter for individual data records
    for d in data:                                                              # loop individual data records
        count += 1                                                              # increment letter counter
        i = 0                                                                   # data record index
        letter_instance = master                                                # new letter instance
        for h in tags:                                                          # loop tags
            tag = TAG_START + h + TAG_END                                       # e.g.: [address]
            replacement = d[i].replace(LINE_BREAK, '\n')                        # enable line break in tags
            letter_instance = letter_instance.replace(tag, replacement)         # replace tag with data field
            i += 1                                                              # next value in data record
        write_letter_instance(letter_instance, count)                           # save letter instance
    return

def write_letter_instance(letter_instance, count):                              # save letter istances, e.g.: letter_1.txt
    count = str(count)
    filename = 'letter_' + count + '.txt'                                       # construct filename
    with open(filename, 'w') as f:
        f.write(letter_instance)
    print('Wrote', filename)
    return

# === Main =====================================================================

def main(args):
    if len(args) != 3:
        print('Missing params: bulky.py master.txt data.csv')
        exit(1)
    master_file = args[1]
    data_file = args[2]
    master = read_master(master_file)
    tags, data = read_data(data_file)
    replace_tags_with_data(master, tags, data)
    return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

Das Programm ist sehr einfach gestaltet und eignet sich auch gut für Einsteiger:innen in die Programmierung.

Ganz oben steht ein wenig Dokumentations-Blabla. Dann folgen die Konstanten für die Separatoren, welche man nach eigenem Gusto verändern kann. Diese müssen natürlich mit den Quelldateien (master, data) übereinstimmen.

Unten in der main-Function werden zwei notwendige Parameter (master.txt, data.csv) überprüft und - falls nicht übergeben - beklagt. Dann werden die beiden Parameter extrahiert und an die zuständigen Funktionen übergeben. read_master liest den Mustertext ein und stellt ihn als String für die weitere Verwendung bereit. read_data liest die CSV-Datei ein und stellt sie als Liste (data) zur Verfügung. Die erste Zeile dieser Datei wird in die Liste tags übernommen; sie enthält die Namen der Platzhalter. Damit ihr euch das besser vorstellen könnt:

Die Tags:
['Adresse', 'Anrede', 'Firma']

Die Daten:
[
['Hauptstrasse 2¬CH-8000 Zürich', 'Sehr geehrter Herr Müller', 'Datasoft GmbH'],
['Dummweg 3¬D-53000 Bonn', 'Sehr geehrter Frau Meier', 'Bullshit AG'],
['Alte Strasse 4¬CH-5000 Bern', 'Sehr geehrte Frau Schneider', 'Verein Huibuh']
]

Beachtet die Zeilenumbrüche "¬" in den Adressen.

Die Musik spielt in der Funktion replace_tags_with_data. Dort wird zuerst über die Daten und darin über die Tags iteriert. Für jeden Datensatz aus der CSV-Datei werden nun die Platzhalter (tags) durch die Datenfelder ersetzt. Für jedes Datenfeld wird der Platzhalter für den Zeilenumbruch ("¬") durch einen richtigen Zeilenumbruch ("\n") ersetzt. Ausserdem gibt es zwei Zähler: count und i. Count ist der Zähler für jeden Datensatz, der für die Namen der einzelnen Briefe benötigt wird. I brauchen wir, um den passenden Tag-Inhalt aus den Daten fischen zu können. Zum Schluss schreibt die Funktion write_letter_instance die einzelnen Briefe auf die Platte. Diese Dateien werden letter_1.txt usw. benannt.

Falls ihr das Skript bei euch ausprobieren möchtet, gilt es Folgendes zu beachten:

  1. Die Datei bulky.py muss ausführbar sein, fall ihr sie mit ./bulky.py master.txt data.csv aufrufen möchtet. Ansonsten ruft ihr sie so auf: python bulky.py master.txt data.csv
  2. Die Dateien master.txt und data.csv müssen im selben Verzeichnis existieren und syntaktisch korrekte Inhalte aufweisen, insbesondere was die Separatoren angeht.

So sieht ein Beispielaufruf aus:

./bulky.py master.txt data.csv

Wrote letter_1.txt
Wrote letter_2.txt
Wrote letter_3.txt

Ob eure Ergebnisse stimmen, könnt ihr überprüfen, indem ihr die Letter anschaut, zum Beispiel:

cat letter_2.txt 

Bullshit AG
Dummweg 3
D-53000 Bonn

Sehr geehrte Frau Meier,

Uns gefällt Ihre Firma Bullshit AG sehr gut.

Ich hoffe, dass dieses kleine Python Skript für die eine oder den anderen nützlich ist. Eventuell dient es Einsteigern auch als einfaches und gut verständliches Code-Beispiel für Python. Ich habe bewusst auf fortgeschrittene Ausdrücke oder Bibliotheken im Code verzichtet, damit es für alle nachvollziehbar ist.

Tags

Serienbrief, Python, Tags

Heidi
Geschrieben von Heidi am 5. Dezember 2022 um 14:50

DANKE -So etwas hatte ich gesucht.

Unter Debian hatte ich es mit 'python3' aufrufen müssen, es muss alles in UTF8 vorliegen, was bei mir zunächst nicht der Fall gewesen war, und im Data File musste bei mir vor der Adresse ebenfalls noch der '|' Separator stehen, der oben fehlt.

Fernziel ist markdown und Datenbankanbindung mit Libreoffice.

Ich hatte im LO mehrere Datenbanktabellen und eine View, die Serienbrieffunktion im LO hat das an sich auch einwandfrei geschluckt, nur leider die 'Mehrdimensionalität' unterschlagen - also etwa bei MEHREREN(!) Bestell- oder Rechnunspositionen pro Brief (o.ä.).

Markdown ins Writer Format zu wandeln, ist kein Problem.

Viele Grüße

Ralf Hersel
Geschrieben von Ralf Hersel am 5. Dezember 2022 um 18:38

Freut mich! Es kommt auf die Distro an, ob man mit 'python3' oder 'python' aufrufen kann. UTF8 ist immer gut. "Adresse|Anrede|Firma" ist schon richtig. Wenn du vor Adresse noch einen Separator machst, bekommst du vorne ein leeres Feld. Guck mal:

a = "a|b|c"

a.split('|')

['a', 'b', 'c']

a = "|a|b|c"

a.split('|') ['', 'a', 'b', 'c']

Heidi
Geschrieben von Heidi am 5. Dezember 2022 um 22:26

Nochmals Danke. Auch das Beispiel im Kommentar leuchtet mir ein, und ich kann es sogar im Terminal nachvollziehen.

Den einleitenden von mir hinzugefügten Separator (|) kann ich auch tatsächlich wieder aus dem 'Datafile' nehmen.

Wenn ich das allerdings in der ERSTEN Zeile mache, sieht 'letter_2.txt' folgendermassen aus:

Bullshit AG [Adresse]

Sehr geehrter Frau Meier,

Uns gefällt Ihre Firma Bullshit AG sehr gut.

Gehen tut es hingegen EINWANDFREI(!) mit:

|Adresse|Anrede|Firma Hauptstrasse 2¬CH-8000 Zürich|Sehr geehrter Herr Müller|Datasoft GmbH Dummweg 3¬D-53000 Bonn|Sehr geehrter Frau Meier|Bullshit AG Alte Strasse 4¬CH-5000 Bern|Sehr geehrte Frau Schneider|Verein Huibuh

Offenbar mache ich was verkehrt...