Analyser les balises qui ne sont pas fermées depuis une page Web avec HtmlAgilityPack

c# html-agility-pack linq

Question

J'essaie d'analyser la liste des stations du site Web de la NOAA (weather.noaa.gov). Si vous regardez la source d'une page telle que Belarus Stations , vous pouvez voir que la liste des stations disponibles est présentée comme suit:

<select name="cccc">
    <option selected>Select a location
    <OPTION VALUE="UMBB"> Brest
    <OPTION VALUE="UMGG"> Gomel'
    <OPTION VALUE="UMMG"> Grodno
    <OPTION VALUE="UMMM"> Loshitsa / Minsk International 1
    <OPTION VALUE="UMMS"> Minsk
    <OPTION VALUE="UMII"> Vitebsk
</select>

Vous pouvez voir que les balises 'OPTION' ne sont pas fermées. Les options par défaut de HtmlAgilityPack ferment les balises comme suit:

<select name="cccc">
    <option selected>Select a location
    <OPTION VALUE="UMBB"> Brest
    <OPTION VALUE="UMGG"> Gomel'
    <OPTION VALUE="UMMG"> Grodno
    <OPTION VALUE="UMMM"> Loshitsa / Minsk International 1
    <OPTION VALUE="UMMS"> Minsk
    <OPTION VALUE="UMII"> Vitebsk
    </OPTION></OPTION></OPTION></OPTION></OPTION></OPTION></OPTION>
</select>

Ce qui en fait une douleur à analyser ou à parcourir. Je suis venu avec la méthode suivante pour recurse chaque étiquette, mais je me demande s'il existe un moyen plus élégant, peut-être en utilisant LINQ?

Ma méthode:

private static void GetStations(HtmlNode node, ref Dictionary<string, string> stations)
{
    // the HTML is malformed, such that the <option> elements are
    // not properly closed, so we have to parse manually
    string name = node.GetAttributeValue("value", string.Empty).Trim();
    string value = node.InnerHtml.Substring(0, node.InnerHtml.IndexOf("\n")).Trim();

    if (!string.IsNullOrEmpty(name) &&
             name.Length == 4 &&
            char.IsUpper(name[0]))
    {
        stations.Add(name, value);
    }
    // due to not closing the <option> elements
    // we have to recurse into child nodes until
    // we get them all
    if (node.HasChildNodes)
    {
        GetStations(node.LastChild, ref stations);
    }
}

Qui s'appelle comme si:

Dictionary<string, string> sites = new Dictionary<string, string>();
...
foreach (HtmlNode option in select.ChildNodes)
{
    if ((option.Name == "option") && (option.HasAttributes))
    {
        GetStations(option, ref sites);
    }
}

J'ai l'impression d'utiliser une méthode de force brute pour obtenir la liste des stations et il se peut que je manque de la puissance de la bibliothèque HtmlAgilityPack. Y a-t-il un meilleur moyen? Y a-t-il des paramètres qui pourraient en faire un problème? Est-ce que LINQ peut gérer cela plus facilement?

J'essaie XPATH, car il semble que le mécanisme le plus simple pour obtenir un sous-ensemble de balises. Cependant, étant donné que les balises ne sont pas fermées, je reçois toutes les balises d'option de la page, alors que je ne veux que celles qui se trouvent dans la balise 'select'. Ainsi, comme vous pouvez le constater, un qualificatif est que les balises 'option' que je veux ont une @ valeur = 'XXXX' où 'XXXX' est un identifiant de station majuscule à 4 caractères. Y a-t-il un moyen de spécifier que je ne veux que les balises d'option dans le document qui ont un attribut nommé 'valeur' ​​avec une valeur de 4 caractères majuscules? Puis-je passer d'une fonction de comparaison à une instruction xpath?

Réponse acceptée

Merci pour tous les conseils. J'ai fait plus de recherches pour la syntaxe xpath, et j'ai trouvé ça qui marche:

//select[@name='cccc']/descendant::option[@value]

cela me donne toutes les balises 'option' sous la balise 'select' avec un attribut @ name = 'cccc' où la balise 'option a un attribut @value.

Beaucoup moins de travail que ce que je faisais. Maintenant, reformulons tout mon autre code qui parcourt le DOM en utilisant HAP et voyons comment XPATH peut me rendre la vie plus facile!


Réponse populaire

HtmlAgilityPack peut automatiquement corriger les balises fermantes, mais peut-être pas exactement comme vous le souhaiteriez :

HtmlNode.ElementsFlags["option"] = HtmlElementFlag.Closed;
var doc = new HtmlDocument();
doc.LoadHtml(html);

Quoi qu'il en soit, à ce stade, vous pouvez toujours sélectionner le texte supposé se trouver dans la <option> l'aide de XPath following following-sibling::text()[1] .

var optionTexts = doc.DocumentNode.SelectNodes("//select[@name='cccc']/option/following-sibling::text()[1]");
foreach (HtmlNode node in optionTexts)
{
    Console.WriteLine(node.InnerText);
}


Related

Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow