staging.inyokaproject.org

Python Lernfrage - JSON-like Code aus HTML Datei extrahieren

Status: Gelöst | Ubuntu-Version: Lubuntu 20.04 (Focal Fossa)
Antworten |

jAIk

Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Hallo zusammen,

ich habe hier ein paar HTML Dateien, aus denen ich gerne Daten im JSON Format extrahieren möchte. Die Dateien sehen wie folgt aus:

1
2
3
4
5
<HTML>
    <!-- Ganz viel Code und Script -->
        MyModel: { JSON DATEN },    
    <!-- Ganz viel Code und Script -->
</HTML>

Den Teil "{ JSON DATEN }" würde ich gerne via json.loads() einlesen.

Aktuell gehe ich wie folgt vor:

  • Datei zeilenweise einlesen

  • Wenn die Zeile mit "MyModel:" beginnt, Zeile kopieren

  • Anfang ("MyModel: ") und Ende (",") abschneiden

Das funktioniert. Trotzdem die Lernfrage: Mir kommt die Lösung unschön vor. Schon wenn irgendwo eine newline im JSON eingebaut ist, funktioniert der Code nicht mehr. Gibt es hier eine offensichtlich bessere Lösung, die ich übersehe?

LG jaik

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Es gibt richtige Parser für HTML/XML, die dir dabei helfen können die Inhalte von Tags zu extrahieren - z.B. lxml und BeautifulSoup (Paket python3-bs4). Dann brauchst du einen Lexer für JS (den Standard gibt es hier: https://262.ecma-international.org/12.0/), damit du das sauber in Elemente trennen kannst - vermutlich reicht dazu sowas wie https://github.com/django/django/blob/main/django/utils/jslex.py, also z.B. grob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
import json
from bs4 import BeautifulSoup
from collections import deque
from jslex import JsLexer


with open('example.html', 'rb') as f:
    soup = BeautifulSoup(f.read().decode(), 'html.parser')

for script_tag in soup.find_all('script'):
    script = "\n".join(script_tag.stripped_strings)
    lexer = JsLexer()
    elements = [e for e in lexer.lex(script) if e[0] != "ws"] # ignore whitespace
    if (my_model_idx := elements.index(('id', 'MyModel'))) > 0 and elements[my_model_idx + 1] == ('punct', ':'):
        opening_curly_brace_idx = elements[my_model_idx:].index(('punct', '{'))  # this is a relative offset
        stack = deque()
        json_elements = deque()

        for e in elements[my_model_idx + opening_curly_brace_idx:]:
            kind, content = e
            json_elements.append(content)
            if not kind == 'punct':
                continue
            if content == '{':
                stack.append(content)
            elif content == '}':
                if stack.pop() != '{':
                    raise ValueError("unbalanced curly braces in JSON")
                if not stack:  # we got to the last closing curled brace
                    break
        s = "".join(json_elements)
        data = json.loads("".join(s))
        print(data)

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Danke! Ich muss mich an der einen oder anderen Stelle nochmal in den Code vertiefen, um genau zu verstehen, was passiert. Insgesamt habe ich zwei kleine Änderungen vorgenommen, damit der Code durchläuft:

  • .decode() in Zeile 9 entfernt

  • Zeile 15: elements.index hat einen Value Error ausgespuckt (ValueError: ('string', '"MyModel"') is not in list), so dass ich hier ein try-except statement genommen habe.

Rest hat gepasst. Tausend Dank!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        for script_tag in soup.find_all('script'):
            script = "\n".join(script_tag.stripped_strings)
            lexer = JsLexer()
            elements = [e for e in lexer.lex(script) if e[0] != "ws"]  # ignore whitespace
            if len(elements) == 0:
                continue

            my_index = ('string', '"MyModel"')

            try:
                if (my_model_idx := elements.index(my_index)) > 0 and elements[my_model_idx + 1] == ('punct', ':'):
                    opening_curly_brace_idx = elements[my_model_idx:].index(('punct', '{'))  # this is a relative offset
                    stack = deque()
                    json_elements = deque()

                    for e in elements[my_model_idx + opening_curly_brace_idx:]:
                        kind, content = e
                        json_elements.append(content)
                        if not kind == 'punct':
                            continue
                        if content == '{':
                            stack.append(content)
                        elif content == '}':
                            if stack.pop() != '{':
                                raise ValueError("unbalanced curly braces in JSON")
                            if not stack:  # we got to the last closing curled brace
                                break

                    s = "".join(json_elements)
                    data = json.loads("".join(s))

            except:
                None

noisefloor Team-Icon

Anmeldungsdatum:
6. Juni 2006

Beiträge: 29567

jAIk schrieb:

Aktuell gehe ich wie folgt vor:

  • Datei zeilenweise einlesen

... ... Mir kommt die Lösung unschön vor ...

Genau. Das kann funktionieren - muss aber nicht. Grund: HTML/XML kann, muss aber keine Zeilenumbrüche enthalten. Sprich: der Browser würde den HTML-Code genau so darstellen bzw. das JavaScript darin ausführen, wenn das alles in einer Zeile stünde. Deine 1. Lösung aber funktioniert nicht, weil du halt nur eine Zeile einliest (weil es mehr nicht gibt) und der Rest vom Code dann nicht funktioniert. Darum HTML/XML immer mit einem passenden Parser lesen.

Zu deinem verbesserten Code: grundsätzlich keine nackten try... except benutzen, weil du damit _alle_ Fehler abfängst / unterdrückst, also z.B. auch Programmierfehler wie Syntaxfehler. Wenn man Fehler abfangen will oder muss, dann gezielt. Bei dir also wenn except ValueError:.

Gruß, noisefloor

jAIk

(Themenstarter)
Avatar von jAIk

Anmeldungsdatum:
7. Juni 2007

Beiträge: 254

Danke! Guter Hinweis und eigentlich ein No-Brainer.

Antworten |