(Aktualisiert am 17.03.2018)
Das Problem:
Das Problem, wie Sie entdeckt haben, ist , dass String.Contains
kein Wort-Grenze Prüfung nicht durchführen, so Contains("float")
wird wieder true
für beide „foo float bar“ (richtig) und „unfloating“ (was falsch ist).
Die Lösung besteht darin, sicherzustellen, dass "float" (oder was auch immer Ihr gewünschter Klassenname ist) an beiden Enden neben einer Wortgrenze erscheint . Eine Wortgrenze ist entweder der Anfang (oder das Ende) einer Zeichenfolge (oder Zeile), ein Leerzeichen, eine bestimmte Interpunktion usw. In den meisten regulären Ausdrücken ist dies der Fall \b
. Der gewünschte Regex lautet also einfach : \bfloat\b
.
Ein Nachteil bei der Verwendung einer Regex
Instanz ist, dass die Ausführung langsam sein kann, wenn Sie die .Compiled
Option nicht verwenden - und dass die Kompilierung langsam sein kann. Sie sollten also die Regex-Instanz zwischenspeichern. Dies ist schwieriger, wenn sich der gesuchte Klassenname zur Laufzeit ändert.
Alternativ können Sie eine Zeichenfolge nach Wörtern durch Wortgrenzen durchsuchen, ohne einen regulären Ausdruck zu verwenden, indem Sie den regulären Ausdruck als C # -String-Verarbeitungsfunktion implementieren, wobei Sie darauf achten, keine neue Zeichenfolge oder andere Objektzuweisung zu verursachen (z String.Split
. B. nicht zu verwenden ).
Ansatz 1: Verwenden eines regulären Ausdrucks:
Angenommen, Sie möchten nur nach Elementen mit einem einzelnen, zur Entwurfszeit angegebenen Klassennamen suchen:
class Program {
private static readonly Regex _classNameRegex = new Regex( @"\bfloat\b", RegexOptions.Compiled );
private static IEnumerable<HtmlNode> GetFloatElements(HtmlDocument doc) {
return doc
.Descendants()
.Where( n => n.NodeType == NodeType.Element )
.Where( e => e.Name == "div" && _classNameRegex.IsMatch( e.GetAttributeValue("class", "") ) );
}
}
Wenn Sie zur Laufzeit einen einzelnen Klassennamen auswählen müssen, können Sie einen regulären Ausdruck erstellen:
private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String className) {
Regex regex = new Regex( "\\b" + Regex.Escape( className ) + "\\b", RegexOptions.Compiled );
return doc
.Descendants()
.Where( n => n.NodeType == NodeType.Element )
.Where( e => e.Name == "div" && regex.IsMatch( e.GetAttributeValue("class", "") ) );
}
Wenn Sie mehrere Klassennamen haben , und Sie wollen alle von ihnen entsprechen, könnten Sie eine Reihe von erstellen Regex
Objekte und sicherzustellen , dass sie alle passenden sind, oder sie zu einem einzigen kombinieren Regex
mit lookarounds, aber diese Ergebnisse in horrend kompliziert Ausdrücke - so mit a Regex[]
ist wahrscheinlich besser:
using System.Linq;
private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String[] classNames) {
Regex[] exprs = new Regex[ classNames.Length ];
for( Int32 i = 0; i < exprs.Length; i++ ) {
exprs[i] = new Regex( "\\b" + Regex.Escape( classNames[i] ) + "\\b", RegexOptions.Compiled );
}
return doc
.Descendants()
.Where( n => n.NodeType == NodeType.Element )
.Where( e =>
e.Name == "div" &&
exprs.All( r =>
r.IsMatch( e.GetAttributeValue("class", "") )
)
);
}
Ansatz 2: Verwenden von Nicht-Regex-Zeichenfolgenabgleich:
Der Vorteil der Verwendung einer benutzerdefinierten C # -Methode für den String-Abgleich anstelle eines regulären Ausdrucks ist die hypothetisch schnellere Leistung und die geringere Speichernutzung ( Regex
kann jedoch unter bestimmten Umständen schneller sein - profilieren Sie Ihren Code immer zuerst, Kinder!).
Die folgende Methode: CheapClassListContains
Bietet eine schnelle Zeichenfolgenübereinstimmungsfunktion zur Überprüfung der Wortgrenzen, die auf die gleiche Weise verwendet werden kann wie regex.IsMatch
:
private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String className) {
return doc
.Descendants()
.Where( n => n.NodeType == NodeType.Element )
.Where( e =>
e.Name == "div" &&
CheapClassListContains(
e.GetAttributeValue("class", ""),
className,
StringComparison.Ordinal
)
);
}
private static Boolean CheapClassListContains(String haystack, String needle, StringComparison comparison)
{
if( String.Equals( haystack, needle, comparison ) ) return true;
Int32 idx = 0;
while( idx + needle.Length <= haystack.Length )
{
idx = haystack.IndexOf( needle, idx, comparison );
if( idx == -1 ) return false;
Int32 end = idx + needle.Length;
Boolean validStart = idx == 0 || Char.IsWhiteSpace( haystack[idx - 1] );
Boolean validEnd = end == haystack.Length || Char.IsWhiteSpace( haystack[end] );
if( validStart && validEnd ) return true;
idx++;
}
return false;
}
Ansatz 3: Verwenden einer CSS-Auswahlbibliothek:
HtmlAgilityPack ist etwas stagniert nicht unterstützt .querySelector
und .querySelectorAll
, aber es gibt Bibliotheken von Drittanbietern , die HtmlAgilityPack mit ihm verlängern: nämlich Fizzler und CssSelectors . Sowohl Fizzler als auch CssSelectors implementieren QuerySelectorAll
, sodass Sie es folgendermaßen verwenden können:
private static IEnumerable<HtmlNode> GetDivElementsWithFloatClass(HtmlDocument doc) {
return doc.QuerySelectorAll( "div.float" );
}
Mit zur Laufzeit definierten Klassen:
private static IEnumerable<HtmlNode> GetDivElementsWithClasses(HtmlDocument doc, IEnumerable<String> classNames) {
String selector = "div." + String.Join( ".", classNames );
return doc.QuerySelectorAll( selector );
}