pdf Auswertung

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
python88
User
Beiträge: 4
Registriert: Freitag 24. Mai 2024, 08:24

Hallo zusammen,

ich hoffe ich bin hier richtig :)

Ich habe mit Hilfe von chatgpt einen python code erstellt der eine pdf einliest, auswertet und mir dann eine .ics Datei generiert.
Damit will ich einen Stundenplan ins Kalenderformat bringen.
Das klappt auch soweit ganz gut aber ich habe tatsächlich ein Problem, welches ich auch mit Hilfe der KI nicht gelöst bekomme...

Ich versuche auch hier davon ein Bild hochzuladen aber im Grunde geht es darum, dass auf dem Stundenplan 2 Doppelstunden so dargestellt werden, das dann ein Trennstrich fehlt...
Das bekomme ich aber nicht ausgewertet.

Könnt ihr mir da helfen ?
https://drive.google.com/file/d/1dFkIn3 ... sp=sharing

Mein Code:

Code: Alles auswählen

import pdfplumber
from ics import Calendar, Event
from datetime import datetime, timedelta
from pytz import timezone

# Definiere die Zeitblöcke und Pausen
time_blocks = [
    ("08:00", "09:30"),
    ("09:45", "11:15"),
    ("11:30", "13:00"),
    ("14:00", "15:30"),  # Nach der Mittagspause
]


def parse_pdf(pdf_path):
    events = []
    start_date = datetime(2024, 6, 3)  # Startdatum, das angepasst werden sollte
    weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"]

    with pdfplumber.open(pdf_path) as pdf:
        first_page = pdf.pages[0]
        tables = first_page.extract_tables()

        if tables:
            table = tables[0]

            # Durchlaufe die Tage von Montag bis Freitag
            for day_index, day in enumerate(weekdays):
                current_date = start_date + timedelta(days=day_index)

                # Durchlaufe die Zeilen jeder Spalte (entsprechend den Stundenblöcken)
                for row_index, row in enumerate(table[1:], start=1):
                    if len(row) > day_index + 1:
                        cell = row[day_index + 1]
                        if cell and 'Raum' in cell:
                            parts = cell.split("\n")
                            name = parts[0]
                            room = parts[-1]

                            # Hole die Start- und Endzeiten basierend auf dem Zeitblock
                            start_time_str, end_time_str = time_blocks[(row_index - 1) % len(time_blocks)]
                            event_start = datetime.strptime(f"{current_date.strftime('%Y-%m-%d')} {start_time_str}",
                                                            "%Y-%m-%d %H:%M")
                            event_end = datetime.strptime(f"{current_date.strftime('%Y-%m-%d')} {end_time_str}",
                                                          "%Y-%m-%d %H:%M")

                            # Wenn die Zelle mit der vorherigen übereinstimmt, ist es eine Doppelstunde
                            if events and events[-1]["name"] == name and events[-1]["location"] == room and events[-1][
                                "date"] == current_date:
                                events[-1]["end"] += timedelta(hours=1)
                            else:
                                event = {
                                    "name": name,
                                    "start": event_start,
                                    "end": event_end,
                                    "location": room,
                                    "date": current_date
                                }
                                events.append(event)

    return events


def create_ics(events, output_path):
    calendar = Calendar()

    for event in events:
        e = Event()
        e.name = event["name"]
        e.location = event["location"]

        # Konvertiere die Zeiten in naive datetime-Objekte
        start_naive = event["start"].replace(tzinfo=None)
        end_naive = event["end"].replace(tzinfo=None)

        # Setze die Zeitzone für jedes Ereignis
        tz = timezone("Europe/Berlin")  # Hier "W. Europe Standard Time" ersetzen
        e.begin = tz.localize(start_naive)
        e.end = tz.localize(end_naive)

        calendar.events.add(e)

    with open(output_path, 'w') as file:
        file.writelines(calendar)


def main(pdf_path, ics_path):
    events = parse_pdf(pdf_path)
    create_ics(events, ics_path)
    print(f"ICS-Datei wurde erfolgreich erstellt: {ics_path}")


# Beispielnutzung
pdf_path = "C:/tmp/pdf.pdf"
ics_path = "C:/tmp/output.ics"
main(pdf_path, ics_path)
Grüße & besten Dank vorab
Sirius3
User
Beiträge: 17848
Registriert: Sonntag 21. Oktober 2012, 17:20

PDF-Dateien enthalten keine Information darüber, was eine Tabelle ist. Es wird nur beschrieben, wo welcher Text gedruckt werden soll und wo welche Linie. Wenn das PDF immer vom selben Programm erstellt wird, kann es sein, dass die Reihenfolge der Linien-Befehle immer ähnlich ist, so dass man diese durchgehen könnte, um die Information "fehlende Linie" daraus zu rekonstruieren.
pdfplumber scheint wohl so etwas wie page.lines zu kennen.
Benutzeravatar
__blackjack__
User
Beiträge: 13279
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@python88: Anmerkungen zum Quelltext:

Konstanten werden per Konvention KOMPLETT_GROSS geschrieben.

`weekdays` ist auch Konstant und sollte ausserhalb der Funktion definiert werden. Wobei das überhaupt gar nicht benutzt wird. Ja, das steht in einem `enumerate()`-Aufruf, aber der Wert `day` wird dann in der Schleife gar nicht verwendet. Hier reicht also ein `range()`.

Es würde den Code vereinfachen wenn man die Tabelle auf den Datenteil reduziert und transponiert, so dass man über die Spalten, statt über die Zeilen iterieren kann.

`row_index` startet bei 1, ist damit also nur als Index zu gebrauchen wenn man beim Zugriff wieder 1 abzieht, was dann im Code auch gemacht wird. Also besser bei 0 starten.

Letztlich könnte man sich den `row_index` sparen wenn man über die Zellen in Spalten iteriert und dann `itertools.cycle()` verwendet statt sich das mit dem Index ``%`` und `len()` selber zu basteln.

``.split("\n")`` ist ein bisschen ”gefährlich” wenn man sich nicht wirklich ganz sicher ist, dass am Ende der Zeichenkette ein "\n" vorkommt, weil dann das letzte Element die leere Zeichenkette ist. Darum gibt es `splitlines()`:

Code: Alles auswählen

In [23]: "eins\nzwei\ndrei\n".split("\n")
Out[23]: ['eins', 'zwei', 'drei', '']

In [24]: "eins\nzwei\ndrei\n".splitlines()
Out[24]: ['eins', 'zwei', 'drei']
`event_start` und `event_end` werden ein bisschen sehr kompliziert ermittelt. In `TIME_BLOCKS` sollten schon `time`-Objekte vorliegen und dann kann man mit denen und dem Datum einfach die `combine()`-Methode auf `datetime`-Objekten verwenden.

In `parse_pdf()` geht es ja eigentlich nicht um Events sondern um Schulstunden. Wenn man das von der Benennung sauberer trennen würde, hätte man später auch nicht das Problem zwei verschiedene Arten von Events zu haben und das eine `event` und das andere nur `e` zu nennen.


Wenn man Wörterbücher mit einem festen Datz an Schlüsseln hat, dann ist das eigentlich ein Objekt und sollte als Klasse modelliert werden. Mindestens als `collections.namedtuple()`.

Der "date"-Eintrag wird nirgends verwendet, wäre aber auch redundant, weil man den sowohl von "start" als auch von "end" einfach abfragen könnte. Hier käme wieder eine Klasse ganz gut, wo man einfach ein entsprechendes `property()` definieren kann.

Die DateTime-Objekte sind bereits ”naiv”, zudem wäre es auch falsch bei Nicht-Naiven vor einer Zeitzonenanpassung einfach die ursprüngliche Zeitzoneninformation weg zu werfen.

Beim öffnen von Textdateien sollte man immer die Kodierung angeben. Bei *.ics-Dateien ist das UTF-8.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
from datetime import datetime as DateTime, time as Time, timedelta as TimeDelta
from itertools import cycle

import pdfplumber
from attrs import define, field
from ics import Calendar, Event
from more_itertools import transpose
from pytz import timezone

START_DATE = DateTime(2024, 6, 3)  # Startdatum, das angepasst werden muss.
TIMEZONE = timezone("Europe/Berlin")
#
# Definiere die Zeitblöcke und Pausen
#
TIME_BLOCKS = [
    (Time(8, 0), Time(9, 30)),
    (Time(9, 45), Time(11, 15)),
    (Time(11, 30), Time(13, 0)),
    (Time(14, 0), Time(15, 30)),  # Nach der Mittagspause
]


@define
class Lesson:
    subject = field()
    start = field()
    end = field()
    location = field()

    @end.validator
    def _validate_end(self, _attribute, value):
        if self.start.date() != value.date():
            raise ValueError("lessons must not cross day boundaries")

        if self.start >= value:
            raise ValueError(
                "lessons can't start before they end or at start and end at"
                " the same time"
            )

    def matches(self, subject, location, date):
        return (
            self.subject == subject
            and self.location == location
            and self.start.date() == date
        )


def parse_pdf(pdf_path):
    lessons = []
    with pdfplumber.open(pdf_path) as pdf:
        tables = pdf.pages[0].extract_tables()
        if tables:
            #
            # Erste Tabelle ohne Kopfzeile und -spalte als iterierbare Spalten.
            #
            columns = transpose(row[1:] for row in tables[0][1:])

            for day_offset, column in enumerate(columns):
                #
                # BUG Wir haben Glück das Sonntags keine Schule ist, aber das
                #     hier ist nicht so ganz robust, weil es bei der Umstellung
                #     Sommer-/Winterzeit zweimal im Jahr nicht funktioniert das
                #     ein Tag ``TimeDelta(days=1)`` lang ist. :-)
                #
                current_date = (START_DATE + TimeDelta(days=day_offset)).date()
                for cell, (start_time, end_time) in zip(
                    column, cycle(TIME_BLOCKS)
                ):
                    if cell and "Raum" in cell:
                        subject, *_, room = cell.splitlines()
                        start = DateTime.combine(current_date, start_time)
                        end = DateTime.combine(current_date, end_time)
                        #
                        # Wenn die Zelle mit der vorherigen übereinstimmt, ist
                        # es eine Doppelstunde.
                        #
                        lesson = lessons[-1] if lessons else None
                        if lesson and lesson.matches(
                            subject, room, current_date
                        ):
                            lesson.end += TimeDelta(hours=1)
                        else:
                            lessons.append(Lesson(subject, start, end, room))

    return lessons


def create_ics(lessons, output_path):
    calendar = Calendar(
        events=(
            Event(
                name=lesson.subject,
                location=lesson.location,
                begin=TIMEZONE.localize(lesson.start),
                end=TIMEZONE.localize(lesson.end),
            )
            for lesson in lessons
        )
    )
    with open(output_path, "w", encoding="utf-8") as file:
        file.writelines(calendar)


def main(pdf_path, ics_path):
    create_ics(parse_pdf(pdf_path), ics_path)
    print(f"ICS-Datei wurde erfolgreich erstellt: {ics_path}")


if __name__ == "__main__":
    main("C:/tmp/pdf.pdf", "C:/tmp/output.ics")
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
python88
User
Beiträge: 4
Registriert: Freitag 24. Mai 2024, 08:24

Hallo Blackjack,

erstmal vielen vielen Dank für deine Anpassungen.
Allerdings weiß ich jetzt wo ich den Fehler habe, allerdings habe ich keine Ahnung wie ich das korrekt haben müsste...
In "meinem" Code und dadurch natürlich auch in Deiner Anpassung, gibt es diesen Bereich hier:

Code: Alles auswählen

#
                        # Wenn die Zelle mit der vorherigen übereinstimmt, ist
                        # es eine Doppelstunde.
                        #
Aber auf diese Art werden ja Doppelstunden gar nicht definiert.
Wenn es eine bzw eigentlich zwei Doppelstunden sind, ist die Zelle danach ja nicht das gleiche sondern leer und dazwischen fehlt einfach nur der Trennstrich...
Wie kann man genau das auswerten ?

VG
Thomas
Sirius3
User
Beiträge: 17848
Registriert: Sonntag 21. Oktober 2012, 17:20

@python88: hab ich Dir doch geschrieben: Du mußt herausfinden, wie in Deinen PDFs Linien gezeichnet werden und irgendwie herausfiltern, wo Linien fehlen. Dazu gibt es das lines-Property von Pages.
python88
User
Beiträge: 4
Registriert: Freitag 24. Mai 2024, 08:24

@Sirius3: hab ich auch versucht. Ich gehe sowieso alle Codes immer im Einzelschritt durch und versuche sie zu verstehen aber ich bin halt noch ganz am Anfang und verstehe die Materie nicht so...
Sirius3
User
Beiträge: 17848
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn Du etwas nicht verstehst, einfach nachfragen.
python88
User
Beiträge: 4
Registriert: Freitag 24. Mai 2024, 08:24

Naja stell Dir vor Du hast gerade angefangen mit Programmierung, hast gelernt wie man Listen erstellt, welche Datentypen es so gibt und den ganzen anderen Anfänger Kram.
Verstehe den ganzen Code nicht weil mir das viel zu fortgeschritten ist. Wollte dieses Programm aber als kleines Hilfsmittel haben um meinen Stundenplan in ein Kalenderformat zu bringen.
Zusammengefasst, ich habe keine Ahnung an welcher Stelle ich was machen muss um irgendwie Richtung Lösung zu kommen....
Benutzeravatar
__blackjack__
User
Beiträge: 13279
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@python88: Das ist vielleicht kein so gutes Anfängerprojekt. Daten aus PDFs extrahieren kann schwierig sein, weil das keine Informationen über die Struktur des Textes enthält, wie beispielsweise HTML, sondern nur die Anweisungen wo Grafik und Text ”gemalt” werden sollen. Und das in einer Reihenfolge die von der Anwendung die das Dokument erstellt hat, bestimmt wird, und die nicht zwingend etwas mit der Reihenfolge zu tun haben muss in der das auf den Seiten zu sehen ist.

In der `README.md` von `pdfplumber` gibt es einen Abschnitt Comparison to other libraries — vielleicht ist eine der Alternativen, insbesondere bei denen, die sich auf Tabellen spezialisieren, besser geeignet.

Ansonsten könnte man sich auch mal die konkreten Werte von einem `Table`-Objekt anschauen. Die `extract()`-Methode welche die 2D-Liste liefert scheint zwischen Zellen zu unterscheiden wo sie eine leere Zeichenkette liefert und solche wo sie `None` liefert. Da wäre es interessant wie das beim konkreten PDF aussieht.
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
Antworten