Html Agility Pack按类获取所有元素

Ada*_*dam 73 html c# html-agility-pack

我正在攻击html敏捷包并且无法找到正确的方法来解决这个问题.

例如:

var findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));
Run Code Online (Sandbox Code Playgroud)

但是,显然你可以添加更多的类然后div,所以我尝试了..

var allLinksWithDivAndClass = _doc.DocumentNode.SelectNodes("//*[@class=\"float\"]");
Run Code Online (Sandbox Code Playgroud)

但是这并没有处理你添加多个类的情况,而"float"只是其中之一,就像这样.

class="className float anotherclassName"
Run Code Online (Sandbox Code Playgroud)

有没有办法处理所有这些?我基本上想要选择所有具有class =且包含float的节点.

**答案已记录在我的博客上,并附有完整说明:Html Agility Pack按类获取所有元素

Dai*_*Dai 93

(2018-03-17更新)

问题:

正如您所发现的那样,问题是String.Contains不执行字边界检查,因此Contains("float")将返回true"foo float bar"(正确)和"unfloating"(这是不正确的).

解决方案是确保"浮动"(或任何您想要的类名称)出现在两端的字边界旁边.字边界是字符串(或行)的开头(或结尾),空格,某些标点符号等.在大多数正则表达式中,这是\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", "") ) );
    }
}
Run Code Online (Sandbox Code Playgroud)

如果需要在运行时选择单个类名,那么可以构建一个正则表达式:

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", "") ) );
}
Run Code Online (Sandbox Code Playgroud)

如果您有多个类名并且想要匹配所有类名,则可以创建一个Regex对象数组并确保它们全部匹配,或者将它们组合成一个Regex使用外观,但这会导致极其复杂的表达式 - 所以使用一个Regex[]可能更好:

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", "") )
            )
        );
}
Run Code Online (Sandbox Code Playgroud)

方法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;
}
Run Code Online (Sandbox Code Playgroud)

方法3:使用CSS Selector库:

HtmlAgilityPack有所停滞不支持.querySelector.querySelectorAll,但也有扩展HtmlAgilityPack与它的第三方库:即FizzlerCssSelectors.Fizzler和CssSelectors都实现了QuerySelectorAll,所以你可以像这样使用它:

private static IEnumerable<HtmlNode> GetDivElementsWithFloatClass(HtmlDocument doc) {

    return doc.QuerySelectorAll( "div.float" );
}
Run Code Online (Sandbox Code Playgroud)

使用运行时定义的类:

private static IEnumerable<HtmlNode> GetDivElementsWithClasses(HtmlDocument doc, IEnumerable<String> classNames) {

    String selector = "div." + String.Join( ".", classNames );

    return doc.QuerySelectorAll( selector  );
}
Run Code Online (Sandbox Code Playgroud)

  • `Contains()`不存在于属性上,所以替换`d.Attributes ["class"].包含("float")`与`d.Attributes ["class"].Value.Split('').Any( b => b.Equals("float"))` (14认同)
  • 只需致电.Descendants() (3认同)
  • 如果有一个名为 `floating` 的类,那么 `Value.Contains("float")` 也会匹配 (2认同)

小智 88

您可以使用Xpath查询中的"包含"功能解决您的问题,如下所示:

var allElementsWithClassFloat = 
   _doc.DocumentNode.SelectNodes("//*[contains(@class,'float')]")
Run Code Online (Sandbox Code Playgroud)

要在函数中重用它,请执行以下类似操作:

string classToFind = "float";    
var allElementsWithClassFloat = 
   _doc.DocumentNode.SelectNodes(string.Format("//*[contains(@class,'{0}')]", classToFind));
Run Code Online (Sandbox Code Playgroud)

  • 如果你有一个名为float-xs的类,会发生什么? (5认同)

Hun*_*Cao 5

我在我的项目中经常使用这种扩展方法。希望它会帮助你们中的一个。

public static bool HasClass(this HtmlNode node, params string[] classValueArray)
    {
        var classValue = node.GetAttributeValue("class", "");
        var classValues = classValue.Split(' ');
        return classValueArray.All(c => classValues.Contains(c));
    }
Run Code Online (Sandbox Code Playgroud)

  • 当您真正想要的是忽略大小写比较时,不要使用 `ToLower()`。传递`StringComparison.CultureIgnoreCase` 更清晰,并显示出更明确的意图。 (3认同)