Etiquetas de análisis que no están cerradas desde la página web con HtmlAgilityPack

c# html-agility-pack linq

Pregunta

Estoy tratando de analizar la lista de estaciones del sitio web de NOAA (weather.noaa.gov). Si observa la fuente de una página como Belarus Stations , puede ver que la lista de estaciones disponibles se presenta como:

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

Puedes ver que las etiquetas 'OPCION' no están cerradas. Las opciones predeterminadas en HtmlAgilityPack cierran las etiquetas así:

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

Lo que hace que sea un dolor para analizar o atravesar. Se me ocurrió el siguiente método para repetir cada etiqueta, pero me pregunto si hay una forma más elegante, ¿quizás usando LINQ?

Mi método:

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

Que se llama así:

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

Siento que estoy usando un método de fuerza bruta para obtener la lista de estaciones, y me podría estar perdiendo algo del poder de la biblioteca HtmlAgilityPack. ¿Hay alguna manera mejor? ¿Hay configuraciones que podrían hacer que esto no sea un problema? ¿Puede LINQ manejar esto más fácilmente?

Estoy probando XPATH, ya que parece ser el mecanismo más simple para obtener un subconjunto de etiquetas. Sin embargo, debido a que las etiquetas no se cierran, recibo todas las etiquetas de opción en la página, mientras que solo quiero las que están dentro de la etiqueta "seleccionar". Por lo tanto, un calificador, como puede ver, es que las etiquetas 'opción' que quiero tienen un valor @ = 'XXXX' donde 'XXXX' es una identificación de estación en mayúsculas de 4 caracteres. ¿Hay alguna manera de especificar que quiero solo las etiquetas de opción en el documente que tienen un atributo llamado 'valor' con un valor de 4 caracteres en mayúscula? ¿Puedo pasar una función de comparación a una instrucción xpath?

Respuesta aceptada

Gracias por todos los punteros. Hice más búsquedas de sintaxis xpath, y encontré esto que funciona:

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

esto me da todas las etiquetas 'opción' bajo la etiqueta 'seleccionar' con un atributo @ nombre = 'cccc' donde la etiqueta 'opción tiene un atributo @valor.

Mucho menos trabajo de lo que estaba haciendo. ¡Ahora, para refactorizar todo mi otro código que recorre el DOM usando HAP y vea cómo XPATH puede hacer mi vida más fácil!


Respuesta popular

HtmlAgilityPack puede corregir automáticamente la etiqueta de cierre, pero tal vez no exactamente de la forma que espera :

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

De todos modos, en este punto aún puede seleccionar texto que se supone que está dentro de la etiqueta <option> usando XPath following-sibling::text()[1] , por ejemplo:

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué