Forum: PC-Programmierung Python 3 Stringverarbeitung Suche nach Sternchen


von Andreas G. (andreasgs)


Lesenswert?

Hallo Zusammen,

Ich lese meinen Stromzähler über dessen RS485 Schnittstelle aus. Als 
Antwort bekomme ich Zeichenketten wie diese:
1
b'1-0:1.8.0*255(002131.9294*kWh)\r\n'

In den Klammern steht der Zählerstand, den es jetzt rauszuschneiden 
gilt.

Mit meinem Code kann ich die beiden Klammern erkennen und rausschneiden:
1
002131.9294*kWh

Wie man sieht kann ich den String zwischen den Klammern rausschneiden. 
Leider scheitere ich nun dran, die Postition des *-chens zu finden. Habt 
ihr eine Idee, wie ich in Python das Sternchen erkennen, und dann den 
verliebenen String 002131.9294 zu einer float überführen kann.

Hinweis, die Anzahl der Stellen ist für die 24 Speicherzeillen ist immer 
anders. Lediglich die Klammern sowie das Sternchen kommt immer vor.

Grüße
Andreas

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Ich vermute, dass du die Zahl mit einem regulären Ausdruck bereits 
extrahierst, oder?

Dem Stern kannst du ganz einfach seine Sonderfunktion nehmen, indem du 
ihn in eckige Klammern fasst (character class).

Also ungefähr so:
1
>>> import re
2
>>> x = re.compile('.*[(](.*)[*](.*)[)].*')
3
>>> m = x.match('1-0:1.8.0*255(002131.9294*kWh)')
4
>>> m.group(1)
5
'002131.9294'
6
>>> m.group(2)
7
'kWh'

ps: Ja, ich weiß, reguläre Ausdrücke sind write-only. :-)

: Bearbeitet durch Moderator
von Andreas G. (andreasgs)


Lesenswert?

Hi Jörg,

Ich such mir in dem String die Positionen der Zeichen
"("
")"
"*"

und dann schneide ich den STring genau den den Positionen auseinander.
1
StartIndex = Speicherblock.find("(") #Finden von der öffnenden Klammer
2
EndIndexStar = Speicherblock.find("\x2A") #Finden von Einheitenmultiplikator
3
EndIndexklammer = Speicherblock.find(")") #Finden der schließenden Klammer

Deinen Code verstehe ich nicht ganz:
1
>>> x = re.compile('.*[(](.*)[*](.*)[)].*')

und wie es dort zur "Gruppenbildung" kommt.

Macht re.compile je eine Gruppe

1. mit den Zeichen vor "("
2. mit den Zeichen zwischen "(" und "."
3. mit den Zeichen zwischen "." und "*"
4. mit den Zeichen zwischen "*" und ")"
5. mit den Zeichen ab ")"


P.S.:
Bis jetzt kann ich folgendes zusammenschneiden:
1
b'\x021-0:96.1.0*255(001LOG0065453795)\r\n'
2
001LOG006545379
3
b'1-0:1.8.0*255(002131.9294*kWh)\r\n'
4
002131.9294*kWh
5
b'1-0:2.8.0*255(000000.0000*kWh)\r\n'
6
000000.0000*kWh
7
b'1-0:16.7.0*255(000605*W)\r\n'
8
000605*W
9
b'1-0:32.7.0*255(235.8*V)\r\n'
10
235.8*V
11
b'1-0:52.7.0*255(236.0*V)\r\n'
12
236.0*V
13
b'1-0:72.7.0*255(236.2*V)\r\n'
14
236.2*V
15
b'1-0:31.7.0*255(001.39*A)\r\n'
16
001.39*A
17
b'1-0:51.7.0*255(001.66*A)\r\n'
18
001.66*A
19
b'1-0:71.7.0*255(000.77*A)\r\n'
20
000.77*A
21
b'1-0:81.7.1*255(117*deg)\r\n'
22
117*deg
23
b'1-0:81.7.2*255(242*deg)\r\n'
24
242*deg
25
b'1-0:81.7.4*255(047*deg)\r\n'
26
047*deg
27
b'1-0:81.7.15*255(042*deg)\r\n'
28
042*deg
29
b'1-0:81.7.26*255(067*deg)\r\n'
30
067*deg
31
b'1-0:14.7.0*255(49.9*Hz)\r\n'
32
49.9*Hz
33
b'1-0:1.8.0*96(00018.0*kWh)\r\n'
34
00018.0*kWh
35
b'1-0:1.8.0*97(00137.7*kWh)\r\n'
36
00137.7*kWh
37
b'1-0:1.8.0*98(00584.8*kWh)\r\n'
38
00584.8*kWh
39
b'1-0:1.8.0*99(00000.0*kWh)\r\n'
40
00000.0*kWh
41
b'1-0:1.8.0*100(02131.9*kWh)\r\n'
42
02131.9*kWh
43
b'1-0:0.2.0*255(ver.03,432F,20170504)\r\n'
44
ver.03,432F,20170504
45
b'1-0:96.90.2*255(0F66)\r\n'
46
0F66
47
b'1-0:97.97.0*255(00000000)\r\n'
48
00000000
49
b'!\r\n'

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Andreas G. schrieb:
> Macht re.compile je eine Gruppe

Sag ich doch, die sind write-only. ;-)

Gruppen werden mit '(' und ')' gebildet. Sie werden bei 1 beginnend 
nummeriert (die Gruppe 0 ist reserviert für den "full match", also 
alles, was überhaupt gepasst hat).

Die '[' ']' umklammern eine character class. Das kann beispielsweise 
ein Folge sein wie '[A-Za-z]' für alle ASCII-Buchstaben oder '[0-3]' für 
die ersten vier Ziffern. In diesem Falle sind die character classes alle 
jeweils nur mit einem Element angelegt, der einzige Sinn dahinter ist 
es, die Sonderfunktion von ()* aufzuheben, damit man sie später selbst 
in den Vergleich einbeziehen kann. (Das geht auch mit Backslash, aber je 
nach Tool muss man dann wieder gucken, ob man einen oder zwei 
Backslashes schreiben muss, um tatsächlich im RE einen Backslash zu 
bekommen. Die character class funktioniert immer.)

.* wiederum passt auf beliebige Teil-Strings dazwischen. Bei 
Mehrdeutigkeiten würde jeweils der längstmögliche damit gewählt, aber da 
die Begrenzungen mit (…*…) hier eindeutig sind, hat man damit kein 
Problem.

ps: Mein RE passt nur auf die Strings von dir, die in den Klammern 
"Maßzahl*Maßeinheit" haben. Wenn du die anderen auch haben willst, würde 
ich zweistufig rangehen: zuerst den Inhalt des Klammernpaares ermitteln, 
danach dann das mit dem Sternchen. Da musst du beim x.match allerdings 
das Ergebnis auswerten, denn das ergibt dann keinen Match für die 
Strings, die kein Sternchen haben.

: Bearbeitet durch Moderator
von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Oder du testest auf das Sternchen vorab mit .index():
1
>>> s = 'foo*bar'
2
>>> t = 'mumble'
3
>>> s.index('*')
4
3
5
>>> t.index('*')
6
Traceback (most recent call last):
7
  File "<stdin>", line 1, in <module>
8
ValueError: substring not found

Musst du halt den ValueError abfangen um zu wissen, dass an dieser 
Stelle kein Sternchen da ist.

In dem Falle lohnt der zweite RE aber auch gar nicht, wenn es keinen 
ValueError gab, kannst du ja die beiden Teil-Strings gleich mit Slices 
extrahieren ([0:sternpos] bzw. [sternpos+1:]).

: Bearbeitet durch Moderator
von Imonbln (Gast)


Lesenswert?

Andreas G. schrieb:
> Deinen Code verstehe ich nicht ganz:>>> x =
> re.compile('.*[(](.*)[*](.*)[)].*')
>
> und wie es dort zur "Gruppenbildung" kommt.

ich finde den match unnötig Kompliziert wie wäre es mit

'\((?P<float>\d+\.\d+)\*kWh\)' das braucht nur 12 Steps laut 
https://regex101.com/ gegen die 41 von dem Orginal Ansatz, ausserdem 
vermute ich das, dass ganze .* matchen, vermutlich zum catastrophic 
backtracking neigen kann.
1
def current2float(rs485: bytes):
2
    match = re.search(r'\((?P<float>\d+\.\d+)\*kWh\)',
3
                      rs485.decode())
4
    if not match:
5
       raise ValueError(f"Pardon Me, didn't understand: {rs485}")
6
    return float(match.group('float'))

Sollte die kWh als float wiedergeben oder eine Exception Schmeißen.

von Jemand (Gast)


Lesenswert?

Seit ich mal gute PEG-Bibliotheken genutzt habe*, würde ich Regex nie 
mehr zum Parsen verwenden.
PEG ist deutlich mächtiger, wartbarer und zugleich einfacher(!) zu 
formulieren und man erhält die garantierte Erkennung syntaktisch 
kaputter Eingaben auch noch dazu.

*keine Ahnung bei Python

von Klaus (Gast)


Lesenswert?

Ein einfaches Arbeiten mit pos = text.find("*") funktioniert bei dir 
nicht?

von Andreas G. (andreasgs)


Lesenswert?

Klaus schrieb:
> Ein einfaches Arbeiten mit pos = text.find("*") funktioniert bei dir
> nicht?

Ne. Das Sternchen ist wohl ein Sonder/Steuerzeichen. Da wir nix 
gefunden.

von Gustl B. (gustl_b)


Lesenswert?

1
import re
2
string = "b'1-0:1.8.0*255(002131.9294*kWh)\r\n"
3
Liste = re.split('(|)', string)

Das zerlegt den String in Teile die dann in Liste liegen.
re.split('(|)', string) sagt, dass string geteilt werden soll und zwar 
sowohl bei ( als auch bei ). Liste hat also am Ende 3 Einträge, der 
Mittlere, Liste[1] enthält dann 002131.9294*kWh.

: Bearbeitet durch User
von Andreas G. (andreasgs)


Lesenswert?

Imonbln schrieb:
> Andreas G. schrieb:
>> Deinen Code verstehe ich nicht ganz:>>> x =
>> re.compile('.*[(](.*)[*](.*)[)].*')
>>
>> und wie es dort zur "Gruppenbildung" kommt.
>
> ich finde den match unnötig Kompliziert wie wäre es mit
>
> '\((?P<float>\d+\.\d+)\*kWh\)' das braucht nur 12 Steps laut
> https://regex101.com/ gegen die 41 von dem Orginal Ansatz, ausserdem
> vermute ich das, dass ganze .* matchen, vermutlich zum catastrophic
> backtracking neigen kann.
>
>
1
> def current2float(rs485: bytes):
2
>     match = re.search(r'\((?P<float>\d+\.\d+)\*kWh\)',
3
>                       rs485.decode())
4
>     if not match:
5
>        raise ValueError(f"Pardon Me, didn't understand: {rs485}")
6
>     return float(match.group('float'))
7
>
>
> Sollte die kWh als float wiedergeben oder eine Exception Schmeißen.

Bzg. Abfangen von exeptions: die Ausgabe ist i.d.R. Immer die selbe, da 
ich nur ein und den selben Stromzähler auslese.

Deinen vorschlag probiere ich nacha mal aus.

von Klaus (Gast)


Lesenswert?

Andreas G. schrieb:
> Ne. Das Sternchen ist wohl ein Sonder/Steuerzeichen. Da wir nix
> gefunden.

Wenn er es als string einlesen würde und nicht binär, würde es 
funktionieren:
1
text = "1-0:1.8.0*255(002131.9294*kWh)\r\n"
2
3
pos_l = text.find("(")
4
pos_r = text.rfind(")")
5
text = text[pos_l+1:pos_r]
6
text = text.strip().replace("*kWh","")
7
fl_num = float(text)
8
9
print(fl_num)

von Andreas G. (andreasgs)


Lesenswert?

> In dem Falle lohnt der zweite RE aber auch gar nicht, wenn es keinen
> ValueError gab, kannst du ja die beiden Teil-Strings gleich mit Slices
> extrahieren ([0:sternpos] bzw. [sternpos+1:]).

Das wäre wohl das Leichteste. I'll give it a try.

von Heiner (Gast)


Lesenswert?

1
import re
2
s = "b'1-0:1.8.0*255(002131.9294*kWh)\r\n'"
3
f = float(re.search('\((.+?)\*', s).group(1))

Ja, man könnte das Pattern größer wählen, damit das ganze robuster wird.
Ja, auch sonst könnte man vielleicht noch ein paar Gedanken 
verschwenden, wie man das ganze abhärten könnte.
Ja, man sollte die letzte Zeile unbedingt in ein try ... except 
verpacken.
Ja, der TE sollte sich mal in reguläre Ausdrücke einlesen, allgemein und 
in Python.

Davon abgesehen bin ich schon immer wieder überrascht, wie kompliziert 
es manchmal sein muss ...

von Fragi (Gast)


Lesenswert?

Und was ist PEG?

von Gustl B. (-gb-)


Lesenswert?

Sorry, was ich oben geschrieben hatte hat nicht funktioniert. Das hier 
ist getestet:
1
import re
2
s = "b'1-0:1.8.0*255(002131.9294*kWh)\r\n"
3
l = re.split(r'(\(|\)|\*)', s)
4
value = float(l[4])
5
unit = l[6]
6
print(value,unit)

Auch hier geht der Einzeiler:
1
value = float(re.split(r'(\(|\)|\*)', s)[4])

von Andreas G. (andreasgs)


Lesenswert?

Heiner schrieb:
>
1
import re
2
> s = "b'1-0:1.8.0*255(002131.9294*kWh)\r\n'"
3
> f = float(re.search('\((.+?)\*', s).group(1))
>
> Ja, man könnte das Pattern größer wählen, damit das ganze robuster wird.
> Ja, auch sonst könnte man vielleicht noch ein paar Gedanken
> verschwenden, wie man das ganze abhärten könnte.
> Ja, man sollte die letzte Zeile unbedingt in ein try ... except
> verpacken.
> Ja, der TE sollte sich mal in reguläre Ausdrücke einlesen, allgemein und
> in Python.
>
> Davon abgesehen bin ich schon immer wieder überrascht, wie kompliziert
> es manchmal sein muss ...

Ach Heiner, ich sollte so viel, Vorallem abnehmen und Bier kaufen 😜

von Heiner (Gast)


Lesenswert?

Andreas G. schrieb:
> Ach Heiner, ich sollte so viel, Vorallem abnehmen und Bier kaufen 😜

Dito. RegEx lernen läge in meinen Prioritäten unter Bier und noch über 
Abnehmen.

Cheers.

von Jemand (Gast)


Lesenswert?

Fragi schrieb:
> Und was ist PEG?

Parsing expression grammar

von flessia blu (Gast)


Lesenswert?

Also Pythons re und davon vorallem named groups haben es mir schon 
angetan.
Damit bekommt man ein dict wo die Namen tacheles reden und muss nicht 
indirekt über Indizes sich verzählen.

In den Beispielen dazu (tutorial?) sind auch kommentierte REs 
aufgeführt, sodass diese nicht writeonly bleiben müssen.
Pro-Tipp1: mehrzeilig schreiben!
Pro-Tipp2: REs in Python immer als rawstring schreiben.
1
re.compile('.*[(](.*)[*](.*)[)].*')
...finde ich stümperhaft, jedoch bei REs führen letztlich viele Wege ins 
ROM ;-)

Der Ansatz ist doch hier:
-a: vom Anfang bis & inkl. erste Klammer '(' ignorieren;
-b: dann bis '*' als Wert nehmen (Ziffern u. Dezimalpunkt);
-c: Stern '*' ignorieren;
-d: dann bis vor ')' als Einheit nehmen;
-e: ')' und Rest bis Ende ignorieren;

Also, in erstem Anlauf:
-a: r'^[^)]+\('
-b: r'[\d\.]+'
-c: r'\*'
-d: r'[^\)]+'
-e: r'\).*$'

Im 2ten Durchläuft benennen wir die Gruppen Wert und Einheit :
-a: r'^[^)]+\('
-b: r'(?P<Wert>' r'[\d\.]+' r')'
-c: r'\*'
-d: r'(?P<Einheit>' r'[^\)]+' r')'
-e: r'\).*$'

Das verbleibende ist nur noch Fingerübung...

von Sheeva P. (sheevaplug)


Lesenswert?

Andreas G. schrieb:
> Ich lese meinen Stromzähler über dessen RS485 Schnittstelle aus. Als
> Antwort bekomme ich Zeichenketten wie diese:
>
1
> b'1-0:1.8.0*255(002131.9294*kWh)\r\n'
2
>
> [...]
> Wie man sieht kann ich den String zwischen den Klammern rausschneiden.
> Leider scheitere ich nun dran, die Postition des *-chens zu finden. Habt
> ihr eine Idee, wie ich in Python das Sternchen erkennen, und dann den
> verliebenen String 002131.9294 zu einer float überführen kann.

Äh, Regex?
1
s = b'1-0:1.8.0*255(002131.9294*kWh)\r\n'
2
float( s.split(b'(')[1].split(b'*')[0] )

von Sheeva P. (sheevaplug)


Lesenswert?

Andreas G. schrieb:
> Klaus schrieb:
>> Ein einfaches Arbeiten mit pos = text.find("*") funktioniert bei dir
>> nicht?
>
> Ne. Das Sternchen ist wohl ein Sonder/Steuerzeichen. Da wir nix
> gefunden.

Ja. Weil "*" ein str-, Dein text aber ein bytes-Objekt ist. Mit 
text.find(b"*") würde das natürlich funktionieren. Ist aber Unfug, 
str.split() und bytes.split() reichen.

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.