(2018-03-17 更新)
問題:
お気づきのように、問題はString.Contains
単語境界チェックを実行しないため、「foo float bar」(正しい) と「unfloating」(正しくない) の両方Contains("float")
が返されることです。true
解決策は、「フロート」(または目的のクラス名が何であれ)が両端の単語境界に沿って表示されるようにすることです。単語境界は、文字列 (または行) の開始 (または終了)、空白、特定の句読点などのいずれかです。ほとんどの正規表現では、これは\b
. したがって、必要な正規表現は次のとおり\bfloat\b
です。
インスタンスを使用することの欠点Regex
は、オプションを使用しないと実行.Compiled
が遅くなり、コンパイルが遅くなる可能性があることです。したがって、正規表現インスタンスをキャッシュする必要があります。探しているクラス名が実行時に変更されると、これはさらに難しくなります。
または、正規表現を C# 文字列処理関数として実装することで、正規表現を使用せずに単語の境界で文字列を検索できます。ただし、新しい文字列やその他のオブジェクトの割り当てが発生しないように注意してください (たとえば、 を使用しないでくださいString.Split
)。
アプローチ 1: 正規表現を使用する:
設計時に指定された単一のクラス名を持つ要素を探したいだけだとします。
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", "") ) );
}
}
実行時に単一のクラス名を選択する必要がある場合は、正規表現を作成できます。
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", "") ) );
}
複数のクラス名があり、それらすべてを一致させたい場合は、Regex
オブジェクトの配列を作成してそれらがすべて一致していることを確認するか、Regex
ルックアラウンドを使用してそれらを 1 つに結合することができますが、これは非常に複雑な式になります。 aRegex[]
はおそらく優れています:
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", "") )
)
);
}
アプローチ 2: 非正規表現文字列マッチングの使用:
正規表現の代わりにカスタム C# メソッドを使用して文字列マッチングを行う利点は、仮想的にはパフォーマンスが向上し、メモリ使用量が削減されることです (ただし、Regex
状況によってはより高速になる場合もあります - 常に最初にコードをプロファイリングしてください!)
以下のこのメソッドCheapClassListContains
は、 と同じ方法で使用できる高速な単語境界チェック文字列マッチング関数を提供します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
)
);
}
/// <summary>Performs optionally-whitespace-padded string search without new string allocations.</summary>
/// <remarks>A regex might also work, but constructing a new regex every time this method is called would be expensive.</remarks>
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;
// Needle must be enclosed in whitespace or be at the start/end of string
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;
}
アプローチ 3: CSS セレクター ライブラリを使用する:
.querySelector
HtmlAgilityPack はやや停滞しており、 と をサポートしていません.querySelectorAll
が、HtmlAgilityPack を拡張するサードパーティ ライブラリがあります。つまり、FizzlerとCssSelectorsです。Fizzler と CssSelectors はどちらも を実装QuerySelectorAll
しているため、次のように使用できます。
private static IEnumerable<HtmlNode> GetDivElementsWithFloatClass(HtmlDocument doc) {
return doc.QuerySelectorAll( "div.float" );
}
実行時定義クラスの場合:
private static IEnumerable<HtmlNode> GetDivElementsWithClasses(HtmlDocument doc, IEnumerable<String> classNames) {
String selector = "div." + String.Join( ".", classNames );
return doc.QuerySelectorAll( selector );
}