Update: Leider gab es in der ersten Version des Scripts noch einen Fehler, der dazu führen konnte, dass bestimmte Arten (UNTIL, COUNT) von Serienterminen nicht übernommen werden, obwohl sie erst nach dem Löschtermin enden. Sollten Sie das Script bereits installiert haben, laden Sie es bitte noch einmal herunter. Wir bedauern die Unannehmlichkeiten.
Benutzen Sie auch das ICS-Format für Ihre Kalenderdaten? Dann sind Sie vielleicht schon einmal auf folgendes Problem gestoßen: Nehmen wir an, Sie haben im Jahr 2010 begonnen, Ihren Kalender im ICS-Format zu führen.
Über die Jahre sammeln sich viele Termine an, und nach einer Zeit könnten die natürlich weg. Aber Sie möchten die alten Termine nicht löschen, weil darin auch wiederkehrende Termine enthalten sind. Bestes Beispiel sind Geburtstage. Sie haben den Geburtstag eines Freundes im Jahr 2010 erfasst und möchten jährlich daran erinnert werden. Wenn Sie nun aber alle Termine vor z.B. 01.01.2023 löschen würden, dann wäre auch die Erinnerung an den Geburtstag des Freundes weg, weil dieser Termin vor 2023 angelegt wurde.
Und deshalb löschen Sie natürlich nicht, und die ICS-Datei wächst und wächst und schwillt mit der Zeit so weit an, dass es ein Performance-Problem ergibt.
Was tun? Hier ist ein Python-Script, das nicht nur das Datum berücksichtigt, an welchem der Termin angelegt wurde, sondern auch dessen Wiederholungen. Es ermittelt, wann die letzte Wiederholung eines Termins ist, und wenn dieser nach dem Löschdatum liegt, wird der Termin übernommen.
Das Script löscht jedoch alle Termin-Einträge, die (incl. deren etwaiger Wiederholungen) vor dem Löschdatum liegen. Der Default für das Löschdatum ist der 01.01. des laufenden Jahres, oder Sie geben es als Parameter an. Hinweis: Alle sonstigen Einträge im ICS-Kalender (Todo, Zeitzonen, Journal) werden unverändert übernommen, unabhängig von ihrem Erstellungsdatum – das Script bearbeitet nur Termine.
Wichtig! Wir übernehmen keinerlei Garantie für das Ergebnis. Bitte erzeugen Sie zunächst eine neue Datei und prüfen Sie das Ergebnis sorgfältig, bevor Sie das Ergebnis dieses Scripts in Ihre Kalenderverwaltung übernehmen. Heben Sie unbedingt den Stand vor der Bearbeitung auf, damit Sie zur Not ein Backup haben. Wir haben das Tool sehr sorgfältig getestet, aber das ICS-Format ist komplex und leistungsstark, und wir können nur sagen, wie wir selbst es benutzen, aber nicht, was irgendwer irgendwo damit alles anfangen könnte.
Und noch eine Anmerkung: Wir haben im Script die Zeitzone „Europe/Berlin“ als Default fest eingestellt vorgegeben. Falls Sie in einer anderen Zeitzone sind, sollten Sie diesen Parameter im Code anpassen. Oder Sie erweitern das Script, um die Zeitzone vom System her automatisch zu ermitteln – ohnehin ist dieses Script vorwiegend als Beispiel für die Verwendung der Bibliothek icalendar in Python gedacht. Vielleicht haben Sie ja eine ganz andere Problemstellung und können von unserem Script nur den Rahmen brauchen?
Genug der Vorrede, Sie können sich das Script als ZIP von unserem Server herunterladen, oder Sie kopieren es aus der Textbox:
#!/usr/bin/env python3 # Erfordert diese beiden Nicht-Standard-Bibliotheken # pip install pytz # pip install icalendar # usage: remove-old-ics-entries.py [-h] [-d DATE] [-o OUTPUT] input_filename # # Old ICS Calendar Entries Cleaner # # positional arguments: # input_filename The ICS input filename # # options: # -h, --help show this help message and exit # -d DATE, --date DATE Start date for cleaning (format: YYYY-MM-DD). Default: # {lfd.Jahr}-01-01 # -o OUTPUT, --output OUTPUT # Output file name (default: stdout) import argparse, sys, os from icalendar import Calendar from datetime import datetime from pytz import timezone from dateutil.rrule import rrulestr def remove_old_events(ics_file_path, cutoff_date, output_file_path): # Lade den Kalender aus der ICS-Datei with open(ics_file_path, 'r', encoding='utf-8') as f: cal = Calendar.from_ical(f.read()) # Neuer Kalender ohne die alten Events new_cal = Calendar() # Kalender-Metadaten (PRODID, VERSION, X-WR-*) vom Original übernehmen for prop, val in cal.items(): new_cal.add(prop, val) # Konvertiere cutoff_date zu einer "aware" datetime mit Zeitzoneninformation tz = timezone('Europe/Berlin') cutoff_date = tz.localize(cutoff_date) for component in cal.walk(): if component.name == "VEVENT": event_date = component.get('DTSTART').dt # Prüfen, ob das Datum eine Zeitzone hat, und angleichen if isinstance(event_date, datetime) and event_date.tzinfo is not None: event_date = event_date.astimezone(tz) elif isinstance(event_date, datetime): event_date = tz.localize(event_date) else: event_date = datetime.combine(event_date, datetime.min.time()) event_date = tz.localize(event_date) # Prüfen, ob das Event wiederkehrend ist rrule_component = component.get('RRULE') if rrule_component is not None: # Extrahiere die RRULE und berechne die Wiederholungen rrule_str = str(rrule_component.to_ical(), 'utf-8') rrule = rrulestr(rrule_str, dtstart=event_date) # Überprüfe das Enddatum (UNTIL) oder berechne das letzte Vorkommen basierend auf COUNT has_until = 'UNTIL' in rrule_component has_count = 'COUNT' in rrule_component last_occurrence = None if has_until: # Berechne das letzte Vorkommen, wenn UNTIL vorhanden ist last_occurrence = rrule[-1] elif has_count: # Berechne das letzte Vorkommen basierend auf COUNT occurrences = list(rrule) last_occurrence = occurrences[-1] if occurrences else None # Bedingungen für das Nicht-Übernehmen der Serie if (has_until or has_count) and (last_occurrence < cutoff_date): continue # Wenn die Serie ein Enddatum hat und dieses vor cutoff_date liegt, überspringen wir sie else: # Einzelereignisse, die vor dem cutoff_date liegen, überspringen if event_date < cutoff_date: continue # Füge das Event zum neuen Kalender hinzu new_cal.add_component(component) elif component.name in ["VTODO", "VTIMEZONE", "VJOURNAL"]: # Füge andere Komponenten ohne Änderung hinzu new_cal.add_component(component) if output_file_path == sys.stdout: sys.stdout.buffer.write(new_cal.to_ical()) # Schreibe binär auf stdout else: with open(output_file_path, 'wb') as f: f.write(new_cal.to_ical()) def valid_date(s): try: return datetime.strptime(s, "%Y-%m-%d") except ValueError: raise argparse.ArgumentTypeError(f"Invalid date format: {s}. Use YYYY-MM-DD format.") def check_file_exists(filename): if not os.path.exists(filename): raise argparse.ArgumentTypeError(f"The file {filename} does not exist.") return filename # Eine benutzerdefinierte Fehlerklasse, die bei fehlenden Argumenten die Usage ausgibt class MyParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write(f"error: {message}\n") self.print_help() sys.exit(2) if __name__ == "__main__": cutoff_date_default = datetime(datetime.now().year, 1, 1) parser = MyParser(description='Old ICS Calendar Entries Cleaner') parser.add_argument('input_filename', type=check_file_exists, help='The ICS input filename') # Verpflichtendes Argument parser.add_argument('-d', '--date', type=valid_date, default=cutoff_date_default, help=f'Start date for cleaning (format: YYYY-MM-DD). Default: {cutoff_date_default.strftime("%Y-%m-%d")}') parser.add_argument('-o', '--output', default=sys.stdout, help='Output file name (default: stdout)') args = parser.parse_args() input_filename = args.input_filename cutoff_date = args.date output_filename = args.output remove_old_events(input_filename, cutoff_date, output_filename)