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ürichSehr 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:
- 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
- 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.
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
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']
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...