Laden...

Regular Expressions

Erstellt von CoderboyPB vor einem Jahr Letzter Beitrag vor einem Jahr 678 Views
C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr
Regular Expressions

Ich habe folgenden String (Ausschnitt)
<tr><td>1.</td><td>&#x1f1e7;&#x1f1f7; Brasilien</td><td>1840.77</td><td>&#x2194;&#xfe0f;</td></tr><tr><td>2.</td><td>&#x1f1e6;&#x1f1f7; Argentinien</td><td>1838.38</td><td>&#x1f51d;</td></tr><tr><td>3.</td><td>&#x1f1eb;&#x1f1f7; Frankreich</td><td>1823.39</td><td>&#x1f51d;</td></tr><tr><td>4.</td><td>&#x1f1e7;&#x1f1ea; Belgien</td><td>1781.3</td><td>&#x1f53b;</td></tr><tr><td>5.</td><td>&#x1f3f4;&#xe0067;&#xe0062;&#xe0065;&#xe006e;&#xe0067;&#xe007f; England</td><td>1774.19</td><td>&#x2194;&#xfe0f;</td></tr><tr><td>6.</td><td>&#x1f1f3;&#x1f1f1; Niederlande</td><td>1740.92</td><td>&#x1f51d;</td></tr><tr><td>7.</td><td>&#x1f ...

Das Ziel ist den String in <tr>...</tr> Chunks aufzuteilen, also für jede Tabellenzeile ein Match.
Mit


<tr>[\s\S]*<\/tr> 

aber wird der ganze Text als 1 Gesamtmatch gefunden.
Wie bekomme ich den Ausdruck weniger 'gierig', dass der nach dem ersten </tr> endet?

D
261 Beiträge seit 2015
vor einem Jahr

Fragezeichen hinter das Sternchen:


<tr>[\s\S]*?<\/tr>


C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Danke, das funktioniert, aber was genau macht die Kombination


\*? 

?

D
261 Beiträge seit 2015
vor einem Jahr

Greed (Gier): Standardmäßig versuchen die Quantoren *, ?, +, und {min,max}, so viele Zeichen wie möglich einzubeziehen, um eine Übereinstimmung zu finden. Um dieses Verhalten auf so wenig Zeichen wie möglich zu begrenzen, fügen Sie nach den Quantoren ein Fragezeichen an. Zum Beispiel bedeutet das Suchmuster <.+> (das kein Fragezeichen enthält): "Suche nach einem <, gefolgt von 1 oder mehr Zeichen, gefolgt von einem >". Um zu verhindern, dass das Suchmuster die komplette Zeichenkette <em>text</em> findet, fügen Sie nach dem Pluszeichen ein Fragezeichen an: <.+?>. Dies führt dazu, dass die Übereinstimmung bereits beim ersten '>' endet und dementsprechend nur der erste HTML-Tag <em> gefunden wird.

2.078 Beiträge seit 2012
vor einem Jahr

Du könntest auch einfach nach "</tr>" splitten, dürfte bedeutend einfacher sein:


var text = "<tr><td>1.</td><td>&#x1f1e7;&#x1f1f7; Brasilien</td><td>1840.77</td><td>&#x2194;&#xfe0f;</td></tr><tr><td>2.</td><td>&#x1f1e6;&#x1f1f7; Argentinien</td><td>1838.38</td><td>&#x1f51d;</td></tr><tr><td>3.</td><td>&#x1f1eb;&#x1f1f7; Frankreich</td><td>1823.39</td><td>&#x1f51d;</td></tr><tr><td>4.</td><td>&#x1f1e7;&#x1f1ea; Belgien</td><td>1781.3</td><td>&#x1f53b;</td></tr><tr><td>5.</td><td>&#x1f3f4;&#xe0067;&#xe0062;&#xe0065;&#xe006e;&#xe0067;&#xe007f; England</td><td>1774.19</td><td>&#x2194;&#xfe0f;</td></tr><tr><td>6.</td><td>&#x1f1f3;&#x1f1f1; Niederlande</td><td>1740.92</td><td>&#x1f51d;</td></tr>";

foreach (var part in text.Split("</tr>", StringSplitOptions.RemoveEmptyEntries))
    Console.WriteLine(part + "</tr>");

Oder Du parst es als XML - HTML ist ja auch nix anderes als XML.
Damit hättest Du dann mit Abstand die meisten Möglichkeiten.

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Ja, xml und xpath war eigentlich auch mein erster Ansatz, aber das html Dokument ließ sich nicht in ein XDocument laden, weshalb ich es mit einer Library versucht hatte, mit
HtmlAgilityPack
Aber anstatt dem gewünschten Knoten erhielt ich nur NULL zurück, dabei war der xpath richtig, denn der stammte aus den Browsertools zur Inspektion.

Nachdem auch andere Bibliotheken keinen Erfolg brachten, musste dann eben die Fußgänger Methode über Regex die Kartoffeln aussem Feuer holen.

@dannoe: Danke 🙂

2.078 Beiträge seit 2012
vor einem Jahr

Wäre es dann nicht besser, den Fehler mit XML zu suchen, anstatt auf Regex zurückzugreifen?
Weil mit XML oder XPath geht es ganz sicher - Du hast also irgendwas falsch gemacht.

Wenn ich das z.B. in Notepad++ werfe, ein root-Element drum (damit es ein Document ist), kann ich problemlos alle Werte abrufen:

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Diese Option hatte ich auch noch in Bedacht gezogen:

Mit Regex den Teilbaum mit den Daten extrahieren und dann versuchen mit xpath drauf zuzugreifen, da ich mich aber mit xpath nicht auskenne, hatte ich dann Abstand davon genommen, ehe ich in noch weitere Fehler geraten wäre.

2.078 Beiträge seit 2012
vor einem Jahr

Glaub mir: XPath ist einfacher.

Und du kannst auch das XML auf eine Klasse mappen oder Du liest es Manuell Element für Element.

Was nun die bessere Option ist, kann ich nicht sagen, ohne zu wissen, wo die Reise hin gehen soll.

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Im Grunde genommen, ist das hier nur ein Nebenprojekt, das eigentliche Ziel ist ne App, wo man zwei Nationalmannschaften auswählt, und dann berechnet wird, wie viel FIFA Weltranglisten Punkte es für beide Teams (oder auch Abzüge) bei jeweiligen Spielausgängen gibt.

Aber da die FIFA leider keine API bereitstellt, muss ich halt selber eine Routine schreiben, die diese Info aus der Webseite parst.
Da die Webseite der FIFA selber aber nicht geeignet ist, weil der Seitensourcecode erst im Browser gerendert werden muss, entnehme ich die Daten hier:
FIFA Weltrangliste - Die aktuelle Fußball Rangliste

Hab jetzt sogar ne funktionierende Lösung, wahrscheinlich nicht die beste, aber eine, die funktioniert:


using System.Text.RegularExpressions;

List<Team> teams = new ();

HttpClient httpClient = new();
httpClient.BaseAddress = new Uri("https://www.fussball-wm.pro/fifa-weltrangliste/");
var html = await httpClient.GetStringAsync(httpClient.BaseAddress);

var sub = Regex.Match(html, "<tbody>.*<\\/tbody>");
html = sub.Value;

var rows = Regex.Matches(html, "<tr>.*?<\\/tr>");

foreach (var row in rows)
{
    string? rowHTML = row.ToString();
    var cells = Regex.Matches(rowHTML, "<td>.*?<\\/td>");

    string name = extractName(cells[1].Value);
    double points = extractPoints(cells[2].Value);

    if (string.IsNullOrEmpty(name))
    {
        name = "USA";
    }

    teams.Add(new Team { Name = name, Points = points }); 
}

foreach(var team in teams)
{
    Console.WriteLine($"{team.Name} : {team.Points}");
}

string extractName(string value)
{
    string trash = Regex.Match(value, "<td>.*\\s").Value;
    string name = RemoveStart(value, trash);    
    return name;
}

double extractPoints(string input)
{
    input = input.Substring(4);
    input = input.Substring(0, input.Length - 5);
    return Double.Parse(input) / 100;
}

string RemoveStart(string value, string trash)
{
    int length = trash.Length;
    value = value.Substring(length);
    return value.Substring(0, value.Length-5);
}

Würde das für ne weitere Verwendung noch in eine Serviceklasse kapseln, aber hier ging es erst mal nur um die Funktionalität.

So, muss jetzt los zur Dialyse, bis später dann 🙂

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Ich Depp, hatte nach der Vorschau vergesen, auf absenden zu klicken.
Das war heute morgen um 6:20, natürlich habe ich die Dialyse jetzt auch hinter mir 🙂

2.078 Beiträge seit 2012
vor einem Jahr

Da die Webseite der FIFA selber aber nicht geeignet ist, weil der Seitensourcecode erst im Browser gerendert werden muss

Kann ich so nicht bestätigen.
Ich kann deine URL einfach als curl-command absetzen und bekomme alles zurück - inklusive der Daten.

Aber wenn Du eine vollständige Website hast, kannst Du dir auch das HTML Agility Pack anschauen.
Das kann CSS-Selektoren, dabei hilft dir dann der Browser, um den richtigen Selector zu finden.

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Wenn ich die FIFA Seite in den Browsertools anschaue bekomme ich den Dokumenten Tree, aber wenn ich einfach nur auf Rechtsklick > Quelltext klicke, erscheint ein a) komplett für Menschen schwer zu lesener Code, der die gesuchten Daten (die Weltranglistenpunkte) nicht mal enthält.

Aber das Agillity Pack werde ich auf jeden Fall noch mal testen, diesen Fehlschlag beim ersten Versuch will ich so nicht hinnehmen 🙂

2.078 Beiträge seit 2012
vor einem Jahr

komplett für Menschen schwer zu lesener Code

Der ist nur unformatiert, ist immer noch gutes altes HTML

der die gesuchten Daten (die Weltranglistenpunkte) nicht mal enthält

Doch - Zeile 220 (zumindest bei dir)
Fängt mit figure und hat als erstes Child eine table.
Da stehen (vermutlich - hab's nicht abgeglichen) alle Daten drin.
Leider keine id, damit wäre es leicht gewesen, die Daten zu finden.

4.931 Beiträge seit 2008
vor einem Jahr

Es gibt auch eine Menge von "HTML pretty print" (online) Tools, die die HTML-Struktur so darstellen, so daß man die Verschachtelung besser überblicken kann.

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Wow, das ging ja wirklich deutlich einfacher, nur bei San Marino musste ich den Spezialfall beheben.

So, jetzt muss ich aber noch mal schnell was einkaufen, ehe die Geschäfte bis Dienstag schließen.
Wünsche euch allen ein frohes Fest 🙂


using HtmlAgilityPack;

var url = "https://www.fussball-wm.pro/fifa-weltrangliste/";

HtmlWeb web = new();
var htmlDoc = web.Load(url);

var nodes = htmlDoc.DocumentNode.SelectNodes(@"//figure[@class='wp-block-table']/table/tbody/tr");

foreach (var node in nodes)
{
    var country = node.ChildNodes[1].InnerText
        .Split(' ')[1];

    country = (country == "San" ? "San Marino" : country);

    var points = node.ChildNodes[2].InnerText;

    Console.WriteLine($"{country} : {points}");
}

4.931 Beiträge seit 2008
vor einem Jahr

Du solltest statt Split(' ')[1] besser den gesamten Text nach dem 1. Leerzeichen nehmen (dann brauchst du auch keine Spezialbehandlung - trifft ja auf andere Ländernamen wie "Costa Rica" oder "Burkina Faso" auch zu und ist m.E. sogar ein Fall von Coding Styles Horror).

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Ich war selber noch am überlegen, sind das jetzt alle? Saudi Arabia, New Zealand? Aber ne, die werden im Deutschen ja als Ein Wort geschrieben.
Aber das ich nicht auf Costa Rica gekommen bin, obwohl wir vor 3 Wochen noch gegen die gespielt haben, kehr, was bin ich dämlich.

Naja dadurch jetzt heute mit dem Anzahl Parameter in der Split Funktion wieder was Neues gelernt. Ohne Witz Jahre lang die Funktion genutzt, aber dieser Parameter war mir nie aufgefallen.

Habe dadurch noch einen Fehler entdeckt: In Bosnien & Herzegowina stand das & in HTML Specialchars, die noch decodiert werden müssen.

4.931 Beiträge seit 2008
vor einem Jahr

An die überladene Methode String.Split(Char, Int32) hatte ich dabei gar nicht gedacht, sondern einfach an IndexOf und Substring, aber schön, daß du selber diese elegante Methode gefunden hast.

Schöne Weihnachten!

C
CoderboyPB Themenstarter:in
327 Beiträge seit 2008
vor einem Jahr

Noch mal der Vollständigkeit halber:


using HtmlAgilityPack;
using System.Net;

var url = "https://www.fussball-wm.pro/fifa-weltrangliste/";

HtmlWeb web = new();
var htmlDoc = web.Load(url);

var nodes = htmlDoc.DocumentNode.SelectNodes(@"//figure[@class='wp-block-table']/table/tbody/tr");

foreach (var node in nodes)
{
    var country = node.ChildNodes[1].InnerText
        .Split(' ', 2)[1];

    country = WebUtility.HtmlDecode(country);

    var points = node.ChildNodes[2].InnerText;

    Console.WriteLine($"{country} : {points}");
}