Html Agility Pack отримує всі елементи за класом


74

Я кидаюсь на html pack agility і маю проблеми з пошуком правильного шляху.

Наприклад:

var findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));

Однак, очевидно, ви можете додати класи набагато більше, ніж div, тому я спробував це ..

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

Але це не обробляє випадки, коли ви додаєте кілька класів, а "float" - це лише один із них, як цей ..

class="className float anotherclassName"

Чи є спосіб впоратись із усім цим? В основному я хочу вибрати всі вузли, які мають клас = і містять float.

** Відповідь задокументована в моєму блозі з повним поясненням за адресою: Html Agility Pack Отримати всі елементи за класом

Відповіді:


94

(Оновлено 17.03.2018)

Проблема:

Проблема, як ви помітили, полягає в тому, String.Containsщо не виконується перевірка межі слів, тому Contains("float")повернеться trueяк для "foo float bar" (правильний), так і "unfloating" (що неправильно).

Рішення полягає в тому, щоб на обох кінцях поряд з межею слова з’явилося слово «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об'єктів і переконатися, що всі вони збігаються, або об'єднати їх в єдиний, Regexвикористовуючи підстановки, але це призводить до жахливо складних виразів - тому використання 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 Selector:

HtmlAgilityPack в деякому стагнації не підтримує .querySelectorі .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  );
}

Невже це спричиняє пошук лише Divs? Що робити, якщо я додаю цей клас до <span class = "someClass float someClass2".
Адам

1
Потім видаліть предикат "div".
Dai

14
Contains()не існує в атрибуті, тому замініть d.Attributes["class"].Contains("float")наd.Attributes["class"].Value.Split(' ').Any(b => b.Equals("float"))
maxp

2
Якби існував клас із ім'ям , floatingто Value.Contains("float")також буде відповідати
хрестики

1
@RobertOschler CheapClassListContainsпотенційно дешевший за регулярний вираз і реалізує ту саму логіку - але так, це теж варіант.
Dai,

92

Вирішити проблему можна за допомогою функції 'містить' у запиті Xpath, як показано нижче:

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

Щоб повторно використовувати це у функції, виконайте щось подібне до наступного:

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

який би був тип об’єкта allElementsWithClassFloat?
Adromil Balais

allElementsWithClassFloatє колекцією HtmlNode
feztheforeigner

Замість string.Format ви також можете використовувати$"//*[contains(@class,'{classToFind}')]"
feztheforeigner

5
що станеться, якщо у вас є клас з ім'ям float-xs?
Самемера Кумарасінга

@SameeraKumarasingha класи "float-xs" та "unfloating" будуть включені до allElementsWithClassFloatсписку. Будь ласка , зверніть увагу на @ відповідь Дай замість цього: stackoverflow.com/a/13774240/3678079
webStuff

4

Я багато використовував цей метод розширення у своєму проекті. Сподіваюся, це допоможе комусь із вас, хлопці.

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()коли те, що ви дійсно хочете, це порівняння IgnoreCase. Проходження StringComparison.CultureIgnoreCaseє чистішим і демонструє більш явний намір.
Pauli Østerø

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

-7

Ви можете використовувати такий сценарій:

var findclasses = _doc.DocumentNode.Descendants("div").Where(d => 
    d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("float")
);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.