staging.inyokaproject.org

Regex funktioniert in Python nicht

Status: Gelöst | Ubuntu-Version: Ubuntu 24.04 (Noble Numbat)
Antworten |

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3351

UlfZibis schrieb:

TK87 schrieb:

1
re.sub(r'\D*([-\d]+)\s([.\d]+).*', r'\1\2', text)

Leider funktioniert das nicht für Werte < 1000 :

1
...

Ergebnis:

...

Leider funktioniert die "re.sub(..., r'\1\2', ...)"-Technik aber auch mit meinem Pattern nicht, obwohl es mit "matches[1]+matches[2]" funktioniert:

1
...

Ergebnis:

...

Was läuft denn da jetzt noch schief?

Hab' jetzt das Missverständnis gefunden ... und ein Pattern, das mit beiden Techniken funktioniert, mit letzterer auch als Einzeiler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
                    pattern = re.compile(r"([-\d.]*)\s([\d.]*).*")
                    if matches := pattern.match("2 345.67 kb/s") :
                        print("matches: '"+matches[1]+"', '"+matches[2]+"'")
                        bit_rate = float(matches[1]+matches[2])
                        print(bit_rate)
                    if matches := pattern.match("345.67 kb/s") :
                        print("matches: '"+matches[1]+"', '"+matches[2]+"'")
                        bit_rate = float(matches[1]+matches[2])
                        print(bit_rate)
                    print("matches: '"+pattern.sub(r'\1', "2 345.67 kb/s")+"', '"+pattern.sub(r'\2', "2 345.67 kb/s")+"'")
                    print("match:", pattern.sub(r'\1\2', "2 345.67 kb/s"))
                    print("matches: '"+pattern.sub(r'\1', "345.67 kb/s")+"', '"+pattern.sub(r'\2', "345.67 kb/s")+"'")
                    print("match:", pattern.sub(r'\1\2', "345.67 kb/s"))
                    print("matches: '"+pattern.sub(r'\1', "345 kb/s")+"', '"+pattern.sub(r'\2', "345.67 kb/s")+"'")
                    print("match:", pattern.sub(r'\1\2', "345 kb/s"))
                    print("matches: '"+pattern.sub(r'\1', "-2 345.67 kb/s")+"', '"+pattern.sub(r'\2', "2 345.67 kb/s")+"'")
                    print("match:", pattern.sub(r'\1\2', "-2 345.67 kb/s"))
                    print("matches: '"+pattern.sub(r'\1', "-345.67 kb/s")+"', '"+pattern.sub(r'\2', "345.67 kb/s")+"'")
                    print("match:", pattern.sub(r'\1\2', "-345.67 kb/s"))

Ergebnis:

matches: '2', '345.67'
2345.67
matches: '345.67', ''
345.67
matches: '2', '345.67'
match: 2345.67
matches: '345.67', ''
match: 345.67
matches: '345', ''
match: 345
matches: '-2', '345.67'
match: -2345.67
matches: '-345.67', ''
match: -345.67

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3351

seahawk1986 schrieb:

Wie wäre es damit?

Weiter oben schrieb ich, dass ich nach einem elegante Einzeiler suche.

PS: wie kommt man zu einem negativen Wert für die Bitrate in dem ursprünglichen Regex?

Ich brauche die Prozedur auch für andere Einheiten als "kb/s".

Marc_BlackJack_Rintsch schrieb:

Ich war übrigens erst mal verwirrt was Du da match nennst, weil das gar kein re.Match-Objekt ist, sondern eine Zeichenkette.

Verstehe 😉

Und matches ist auch eher ein Name für eine Sequenz oder einen Iterator über re.Match-Objekte und nicht für ein solches Objekt.

Von der Syntax her sieht es eher aus wie eine Liste mit einem oder mehr Elementen: matches[0], matches[1], matches[2], etc. , deshalb hab' ich dafür Plural verwendet.

Kann es überhaupt negative Bitraten geben?

Nein, aber das Pattern soll auch für andere Einheiten als "kb/s" funktionieren, z.B. "dB". Es lässt sich sogar noch ein +-Zeichen reinwurschteln, falls das mal vorkommen sollte:

1
pattern = re.compile(r"([-+\d.]*)\s([\d.]*).*")

Und Punkte zwischen den Ziffern vor dem Leerzeichen?

Nein.

Ungetestet:

1
2
    match = re.search(r"((?:\d+ )?\d+\.\d+) kb/s", text)
    bit_rate = float(match[1].replace(" ", "")) if match else 0.0

Finde ich ein bisschen hässlich, funktioniert mit anderen Einheiten nicht, und ist auch kein eleganter Einzeiler.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3351

Mir ist aber jetzt ein noch viel eleganterer Trick eingefallen:

1
2
3
4
5
6
7
                    pattern = re.compile(r"[^-+\d.]")
                    print("bit rate:      ", float(pattern.sub("", "2 345.67 kb/s")),       "kb/s")
                    print("bit rate:      ", float(pattern.sub("", "345.67 kb/s")),         "kb/s")
                    print("volume:        ", float(pattern.sub("", "-2 345.67 dB")),        "dB")
                    print("volume:        ", float(pattern.sub("", "+ 345.67 dB")),         "dB")
                    print("Stadtparlament:",   int(pattern.sub("", "  66 Hornochsen")),     "Hornochsen")
                    print("Volkszählung:  ",   int(pattern.sub("", "2 345 678 Einwohner")), "Einwohner")

Ergebnis:

bit rate:       2345.67 kb/s
bit rate:       345.67 kb/s
volume:         -2345.67 dB
volume:         345.67 dB
Stadtparlament: 66 Hornochsen
Volkszählung:   2345678 Einwohner

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4735

@UlfZibis: „Eleganter Einzeiler“ habe ich als Anforderung ignoriert, weil das kein Ziel sein sollte, weil ”elegant” sich zu häufig auf die Tatsache bezieht dass es ein Einzeiler ist. Was ist denn daran so erstrebenswert? Wenn man es öfter braucht, packt man es sowieso in eine Funktion, selbst wenn es nur eine Zeile ist, und damit wird auch so ziemlich alles zum Einzeiler.

So ein re.Match-Objekt sieht tatsächlich auch aus wie eine Sequenz, aber nicht über ”matches” sondern über Gruppen. Was man da über den Indexzugriff match[n] bekommt, ist ja das gleiche was man über match.group(n) beziehungsweise als Sequenz über match.groups() bekommt.

Die Lösung würde ich übrigens nicht nehmen. Dann könnte man auch einfach so etwas wie float(text.rsplit(" ")[0].replace(" ", "")) machen. Was an beiden unschön ist, ist dass es, nun ja ein anderes Wort für ”Trick” wäre hier ja ”Hack”, ist. Ein regulärer Ausdruck der sich tatsächlich an der Struktur der Zahl orientiert und die erwartete Einheit fände ich da sinnvoller. Dann fallen einem Fehler oder Änderungen oder einfach bisher Unerwartetes in den Eingabedaten auf.

 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
35
#!/usr/bin/env python3
import re


def search_number(text, expected_unit_text):
    if match := re.search(r"[-+]? ?(\d{1,3}(?: \d{3})*(?:\.\d+)?) (\S+)", text):
        if match[2] != expected_unit_text:
            raise ValueError(
                f"expected unit {expected_unit_text!r}, got {match[2]!r}"
            )

        result = float(match[1].replace(" ", ""))
        return int(result) if result.is_integer() else result

    raise ValueError(f"no number found in {text!r}")


def main():
    for label_text, input_text, expected_unit_text in [
        ("bit rate", "2 345.67 kb/s", "kb/s"),
        ("bit rate", "345.67 kb/s", "kb/s"),
        ("volume", "-2 345.67 dB", "dB"),
        ("volume", "+ 345.67 dB", "dB"),
        ("Stadtparlament", "  66 Hornochsen", "Hornochsen"),
        ("Volkszählung", "2 345 678 Einwohner", "Einwohner"),
    ]:
        print(
            f"{label_text}:".ljust(15),
            search_number(input_text, expected_unit_text),
            expected_unit_text,
        )


if __name__ == "__main__":
    main()

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3351

Marc_BlackJack_Rintsch schrieb:

@UlfZibis: „Eleganter Einzeiler“ habe ich als Anforderung ignoriert, weil das kein Ziel sein sollte, weil ”elegant” sich zu häufig auf die Tatsache bezieht dass es ein Einzeiler ist. Was ist denn daran so erstrebenswert? Wenn man es öfter braucht, packt man es sowieso in eine Funktion, selbst wenn es nur eine Zeile ist, und damit wird auch so ziemlich alles zum Einzeiler.

Wenn ich ein allgemein nutzbares Programm schreiben wollte, würde ich das auch so sehen.
Ich brauche die Funktionalität bisher nur an einer einzigen Stelle. Da ist ein 1-2-Zeiler komfortabler, als sich eine extra Funktion ausdenken zu müssen. Der Input sollte von file 123.(flac|mp3) kommen. Da das bei manchen Dateien gar keinen Bitraten-Wert liefert, kann dann mediainfo aushelfen. Ersteres liefert "-2345 kbps" und letzteres "-2 345 kb/s". Ob da manchmal auch Nachkommastellen auftreten ist rein hypothetisch, schien aber schon im ersten Ansatz nicht kompliziert, dies auch noch zu berücksichtigen. Der Hintergedanke, dass Muster dann auch noch für andere Werte verwenden zu können, legte nahe, dass es doch am besten unabhängig von der Einheit sein sollte.

So ein re.Match-Objekt sieht tatsächlich auch aus wie eine Sequenz, aber nicht über ”matches” sondern über Gruppen.

Genau genommen eine Liste/Sequenz von Gruppen-Passungen → group matches → matches. In Grunde ist die Klasse in der Singularform schon falsch bezeichnet, was ich anfangs sehr irritierend fand, dass ich da ohne Angabe eines Indexes mit re.search(...) eben nicht gleich ein Ergebnis bekam wie von grep gewohnt.

Die Lösung würde ich übrigens nicht nehmen. Dann könnte man auch einfach so etwas wie float(text.rsplit(" ")[0].replace(" ", "")) machen.

Oder einfach zu Fuß mit einer Kombination aus text.index(" "), text[n:m] und '+', also ganz ohne Regenechsen.

Was an beiden unschön ist, ist dass es, nun ja ein anderes Wort für ”Trick” wäre hier ja ”Hack”, ist.

Genau genommen geht es darum, aus dem "Zeichensalat" die Zahl zu extrahieren, was – wie mir erst am Ende in den Sinn kam – dem gleich kommt, alles, was da nicht zu gehört, einfach rauszuwerfen.

1
2
3
4
5
6
#!/usr/bin/env python3
import re


def search_number(text, expected_unit_text):
[....]

Diesmal nicht "ungetestet", doch genau diesmal ein Bug drin 👿 Das '-' ohne Lücke wird nicht erkannt. Sonst aber ein schönes Beispiel.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4735

@UlfZibis: Auch wenn das kein allgemein nutzbares Programm ist, so ist die Funktion innerhalb des Programms ja schon allgemein(er) genutzt. Und gerade wenn man Ausgaben von irgendwelchen Programmen parst wo man den Code nicht kennt, also nicht genau weiss welche Sonderfälle der noch generieren kann, oder vielleicht in Zukunft generiert, denn solche ”Einmalprogramme” sind ja nicht selten jahrzehntelang dann doch immer mal wieder im Einsatz wenn man sie nach Gebrauch nicht tatsächlich löscht. Wofür mir das bisherige schon zu lang und zu schade wäre. Ich bin da aus Erfahrung lieber vorsichtig und prüfe ob die Eingabe tatsächlich so aussieht wie ich das erwarte. Durch Deine letzte Änderung in den Testdaten bin ich beispielsweise erst darauf gekommen, dass der Dezimalbruchteil auch optional sein kann.

Auf die Gruppen kann man auch zugreifen wenn sie nicht gematcht haben, also „group matches“ trifft es auch nicht. Und das match[n] überhaupt geht ist auch ”neu”. Ursprünglich musste man match.group(n) benutzen. Liste/Sequenz ist es übrigens auch nicht, denn es hat keine Länge und man kann ja nicht nur Zahlen verwenden sondern auch Zeichenketten wenn man benannte Gruppen verwendet. Es gibt halt seit den 1990ern Code bei dem das eigentlich immer match heisst.

rsplit() beziehungsweise wohl besser rpartition() und replace() auf Zeichenketten verwendet ja auch keine Regenechsen. Wie gesagt, ich finde daran praktisch, dass man das Format damit besser erfassen/prüfen kann, und leicht merkt, wenn da etwas unerwartetes kommt, statt irgendwas in eine Zahl umzuwandeln, was dann eventuell einen ungültigen Wert hat und damit weiter zu machen als wäre nix passiert.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3351

Marc_BlackJack_Rintsch schrieb:

Auf die Gruppen kann man auch zugreifen wenn sie nicht gematcht haben, also „group matches“ trifft es auch nicht.

Aber ist ein Leer-String als Ergebnis einer Gruppensuche nicht auch ein "match"? Manchmal wird auch genau die Nichterfüllung eines Musters gesucht.

Es gibt halt seit den 1990ern Code bei dem das eigentlich immer match heisst.

Da fällt mir der allgemeine Gebrauch von "usage" ein, z.B. "Find usages" (wurde vermutlich von einem Nicht-Engländer eingeführt). Das heißt übersetzt nämlich eher "Brauch" (im Sinne von Maibaum aufstellen und ähnlichem). "use" bzw. "Find uses" wäre der richtigere Begriff. Ich hatte da mal für die NetBeans IDE einen Bug gepostet, auch weil der lange Begriff die Tabs unnötig breit machte, finde ihn aber nicht mehr, war noch auf dem alten Bug-Tracker, bevor die zu Apache umgezogen sind. War eine interessante Diskussion, sprich, man gab mir recht, wollte aber den gewohnten "Brauch" 😉 (usage zu benutzen) nicht brechen.

rsplit() beziehungsweise wohl besser rpartition() und replace() auf Zeichenketten verwendet ja auch keine Regenechsen.

Das käme zu meinen erwähnten String-Funktionen gerne noch dazu.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4735

@UlfZibis: Ja eine leere Zeichenkette wäre auch ein match, aber es gibt ja auch Gruppen die tatsächlich gar nicht matchen, weil die nicht auf eine leere Zeichenkette zutreffen, aber auch nichts entsprechendes in der Zeichenkette enthalten ist:

1
2
3
>>> match = re.match(r"(foo|bar)|(x*)(.)", "a")
>>> match.groups()
(None, '', 'a')

Die erste Gruppe matcht nichts, die zweite trifft auch auf eine leere Zeichenkette zu, und die dritte findet das "a".

Älteres Thema: Noch ein anderer Blickwinkel auf die „non-capturing group“ wäre übrigens Klammerung um dem Operator-/Operationsvorrang entgegen zu wirken. Wiederholungen von Teilausdrücken binden beispielsweise stärker als Verkettung von Teilausdrücken. Wenn man also eine Verkettung wiederholen will, muss man die Klammern. Also so ähnlich wie bei a + b * 3 und (a + b) * 3 wäre das bei regulären Ausdrücken der Unterschied zwischen ab{3} und (?:ab){3}.

Antworten |