HtmlAgilityPack: xpath et regex

c# html-agility-pack regex

Question

J'utilise actuellement HtmlAgilityPack pour rechercher certains contenus via une requête xpath. Quelque chose comme ça:

var col = doc.DocumentNode.SelectNodes("//*[text()[contains(., 'foo'] or @*....

Maintenant, je veux rechercher un contenu spécifique dans tout le code source HTML (= texte, balises et attributs) en utilisant une expression régulière. Comment cela peut-il être réalisé avec HtmlAgilityPack? HtmlAgilityPack peut-il gérer xpath + regex ou quel serait le meilleur moyen d’utiliser un regex et un HtmlAgilityPack pour la recherche?

Réponse acceptée

Le pack d'agilité HTML utilise l'implémentation sous-jacente .NET XPATH pour sa prise en charge XPATH. Heureusement, XPATH dans .NET est totalement extensible (BTW: dommage que Microsoft n’investisse plus dans cette superbe technologie ...).

Supposons donc que j'ai ce code HTML:

<div>hello</div>
<div>hallo</div>

Voici un exemple de code qui sélectionnera les deux nœuds car il compare les nœuds avec l'expression regex 'h.llo':

HtmlNodeNavigator nav = new HtmlNodeNavigator("mypage.htm");
foreach (var node in SelectNodes(nav, "//div[regex-is-match(text(), 'h.llo')]"))
{
    Console.WriteLine(node.OuterHtml); // should dump both div elements
}

Cela fonctionne car j'utilise un contexte Xslt / XPath spécial dans lequel j'ai défini une nouvelle fonction XPATH appelée "regex-is-match". Voici le code de l'utilitaire SelectNodes:

public static IEnumerable<HtmlNode> SelectNodes(HtmlNodeNavigator navigator, string xpath)
{
    if (navigator == null)
        throw new ArgumentNullException("navigator");

    XPathExpression expr = navigator.Compile(xpath);
    expr.SetContext(new HtmlXsltContext());

    object eval = navigator.Evaluate(expr);
    XPathNodeIterator it = eval as XPathNodeIterator;
    if (it != null)
    {
        while (it.MoveNext())
        {
            HtmlNodeNavigator n = it.Current as HtmlNodeNavigator;
            if (n != null && n.CurrentNode != null)
            {
                yield return n.CurrentNode;
            }
        }
    }
}

Et voici le code de support:

    public class HtmlXsltContext : XsltContext
    {
        public HtmlXsltContext()
            : base(new NameTable())
        {
        }

        public override int CompareDocument(string baseUri, string nextbaseUri)
        {
            throw new NotImplementedException();
        }

        public override bool PreserveWhitespace(XPathNavigator node)
        {
            throw new NotImplementedException();
        }

        protected virtual IXsltContextFunction CreateHtmlXsltFunction(string prefix, string name, XPathResultType[] ArgTypes)
        {
            return HtmlXsltFunction.GetBuiltIn(this, prefix, name, ArgTypes);
        }

        public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] ArgTypes)
        {
            return CreateHtmlXsltFunction(prefix, name, ArgTypes);
        }

        public override IXsltContextVariable ResolveVariable(string prefix, string name)
        {
            throw new NotImplementedException();
        }

        public override bool Whitespace
        {
            get { return true; }
        }
    }

    public abstract class HtmlXsltFunction : IXsltContextFunction
    {
        protected HtmlXsltFunction(HtmlXsltContext context, string prefix, string name, XPathResultType[] argTypes)
        {
            Context = context;
            Prefix = prefix;
            Name = name;
            ArgTypes = argTypes;
        }

        public HtmlXsltContext Context { get; private set; }
        public string Prefix { get; private set; }
        public string Name { get; private set; }
        public XPathResultType[] ArgTypes { get; private set; }

        public virtual int Maxargs
        {
            get { return Minargs; }
        }

        public virtual int Minargs
        {
            get { return 1; }
        }

        public virtual XPathResultType ReturnType
        {
            get { return XPathResultType.String; }
        }

        public abstract object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext);

        public static IXsltContextFunction GetBuiltIn(HtmlXsltContext context, string prefix, string name, XPathResultType[] argTypes)
        {
            if (name == "regex-is-match")
                return new RegexIsMatch(context, name);

            // TODO: create other functions here
            return null;
        }

        public static string ConvertToString(object argument, bool outer, string separator)
        {
            if (argument == null)
                return null;

            string s = argument as string;
            if (s != null)
                return s;

            XPathNodeIterator it = argument as XPathNodeIterator;
            if (it != null)
            {
                if (!it.MoveNext())
                    return null;

                StringBuilder sb = new StringBuilder();
                do
                {
                    HtmlNodeNavigator n = it.Current as HtmlNodeNavigator;
                    if (n != null && n.CurrentNode != null)
                    {
                        if (sb.Length > 0 && separator != null)
                        {
                            sb.Append(separator);
                        }

                        sb.Append(outer ? n.CurrentNode.OuterHtml : n.CurrentNode.InnerHtml);
                    }
                }
                while (it.MoveNext());
                return sb.ToString();
            }

            IEnumerable enumerable = argument as IEnumerable;
            if (enumerable != null)
            {
                StringBuilder sb = null;
                foreach (object arg in enumerable)
                {
                    if (sb == null)
                    {
                        sb = new StringBuilder();
                    }

                    if (sb.Length > 0 && separator != null)
                    {
                        sb.Append(separator);
                    }

                    string s2 = ConvertToString(arg, outer, separator);
                    if (s2 != null)
                    {
                        sb.Append(s2);
                    }
                }
                return sb != null ? sb.ToString() : null;
            }

            return string.Format("{0}", argument);
        }

        public class RegexIsMatch : HtmlXsltFunction
        {
            public RegexIsMatch(HtmlXsltContext context, string name)
                : base(context, null, name, null)
            {
            }

            public override XPathResultType ReturnType { get { return XPathResultType.Boolean; } }
            public override int Minargs { get { return 2; } }

            public override object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext)
            {
                if (args.Length < 2)
                    return false;

                return Regex.IsMatch(ConvertToString(args[0], false, null), ConvertToString(args[1], false, null));
            }
        }
    }

La fonction regex est implémentée dans une classe appelée RegexIsMatch à la fin. Ce n'est pas super compliqué. Notez qu'il existe une fonction utilitaire ConvertToString qui tente de contraindre un "objet" xpath dans une chaîne très utile.

Bien sûr, avec cette technologie, vous pouvez définir la fonction XPATH dont vous avez besoin avec très peu de code (je l'utilise tout le temps pour effectuer des conversions en majuscules / minuscules ...).


Réponse populaire

Citant directement,

Je pense que la faille ici est que HTML est une grammaire de Chomsky Type 2 (grammaire sans contexte) et RegEx est une grammaire de Chomsky Type 3 (grammaire normale) . Etant donné qu'une grammaire de type 2 est fondamentalement plus complexe qu'une grammaire de type 3 (voir la hiérarchie de Chomsky ), vous ne pouvez réellement pas réussir. Mais beaucoup essaieront, certains revendiqueront le succès et d’autres trouveront la faute et vous gâcheront totalement.

Il peut être judicieux d’utiliser une expression régulière avec certaines parties d’un document HTML. Essayer d'utiliser HtmlAgilityPack pour exécuter une expression régulière sur les balises et la structure d'un document HTML est pervers et ne peut pas, en définitive, fournir une solution universelle à votre problème.




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi