Html Agility Pack通过类名获取所有元素

76
我正在尝试使用HTML Agility Pack,但是在找到正确的方法方面遇到了麻烦。
例如:
var findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));

不过,显然你可以给更多的元素添加类,所以我尝试了这个...

var allLinksWithDivAndClass = _doc.DocumentNode.SelectNodes("//*[@class=\"float\"]");

但是这并不能处理那些你添加了多个类,而“float”只是其中之一的情况,就像这样。

class="className float anotherclassName"

有没有一种方法可以处理所有这些?基本上我想选择所有具有类=并包含浮点数的节点。

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

6个回答

97

更新时间 2018-03-17

问题:

问题是,正如你所发现的那样,String.Contains没有执行单词边界检查,因此Contains("float")会对"foo float bar"(正确)和"unfloating"(不正确)都返回true

解决方案是确保"float"(或您想要的类名)在两端都出现在单词边界上。单词边界可以是字符串(或行)的开头(或结尾),空格,某些标点符号等。在大多数正则表达式中,这是\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对象的数组,并确保它们都匹配,或者使用lookarounds将它们合并成一个单一的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", "") )
            )
        );
}

方法二:使用非正则表达式字符串匹配:

使用自定义的 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;
}

方法三:使用CSS选择器库:

HtmlAgilityPack有些停滞不前,不支持 .querySelector.querySelectorAll,但是有第三方库可以扩展HtmlAgilityPack:即 FizzlerCssSelectors。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  );
}

1
然后删除“div”谓词。 - Dai
14
Contains()方法不能用于该属性,因此需要将d.Attributes["class"].Contains("float")替换为d.Attributes["class"].Value.Split(' ').Any(b => b.Equals("float"))。新代码可以检查属性值中是否包含"class"属性中的"float"值。 - maxp
哦,Contains 对我来说可行,因为我编写了自己的扩展方法。 - Dai
2
如果有一个名为“floating”的类,那么Value.Contains("float")也会匹配该类。 - tic
1
@RobertOschler CheapClassListContains 可能比正则表达式更便宜,并且实现了相同的逻辑 - 但是,是的,那也是一个选项。 - Dai
显示剩余5条评论

93

您可以通过在Xpath查询中使用“contains”函数来解决您的问题,如下所示:

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

要在函数中重复使用此内容,请执行以下类似操作:

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

allElementsWithClassFloat 的对象类型是什么? - Adro
allElementsWithClassFloat 是一个 HtmlNodeCollection。 - feztheforeigner
你可以使用 $"//*[contains(@class,'{classToFind}')]" 替代 string.Format。 - feztheforeigner
5
如果你创建了一个名为"float-xs"的类会发生什么? - Sameera Kumarasingha
@SameeraKumarasingha 类 'float-xs' 和 'unfloating' 都将包含在 allElementsWithClassFloat 列表中。请查看 @Dai 的答案:https://dev59.com/rmYr5IYBdhLWcg3wVYog#13774240 - webStuff
从Html Agility Pack的v1.6.5版本开始,它包含.HasClass("class-name")扩展方法。IEnumerable<HtmlNode> nodes = htmlDoc.DocumentNode.Descendants(0) .Where(n => n.HasClass("class-name")); - Tohid

6

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

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));
    }

3
在进行不区分大小写的比较时,不要使用 ToLower() 方法。而是应该传递 StringComparison.CultureIgnoreCase,这样做更加清晰明了,显示出明确的意图。 - Pauli Østerø

0
如果你正在寻找某个标签中的类(例如或任何其他标签),可以尝试这个。
 var spans = doc.DocumentNode.SelectNodes("//span"); //or other tag or all nodes

 var span_with_class = spans.Where(_ => _.Attributes["class"].Value.Split(' ').Any(b => b.Equals("someClass")));

1
仅提供代码的答案是不鼓励的。如果答案附带有关代码如何解决问题的说明,那么这些答案将具有更长期的价值。 - tdy

0
public static List<HtmlNode> GetTagsWithClass(string html,List<string> @class)
    {
        // LoadHtml(html);           
        var result = htmlDocument.DocumentNode.Descendants()
            .Where(x =>x.Attributes.Contains("class") && @class.Contains(x.Attributes["class"].Value)).ToList();          
        return result;
    }      

-6
您可以使用以下脚本:
var findclasses = _doc.DocumentNode.Descendants("div").Where(d => 
    d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("float")
);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接