Wat is de beste manier om html in C# te ontleden?

Ik ben op zoek naar een bibliotheek/methode om een html-bestand te parseren met meer html-specifieke functies dan generieke xml-parseerbibliotheken.


Antwoord 1, autoriteit 100%

Html Agility-pakket

Dit is een behendige HTML-parser die een lees-/schrijf-DOM bouwt en gewone XPATH of XSLT ondersteunt (je hoeft XPATH of XSLT niet te begrijpen om het te gebruiken, maak je geen zorgen…). Het is een .NET-codebibliotheek waarmee u HTML-bestanden uit het web kunt ontleden. De parser is zeer tolerant met “echte wereld” misvormde HTML. Het objectmodel lijkt erg op wat System.Xml voorstelt, maar dan voor HTML-documenten (of streams).


Antwoord 2, autoriteit 20%

Je zou TidyNet.Tidy kunnen gebruiken om de HTML naar XHTML te converteren, en dan een XML-parser gebruiken.

Een ander alternatief zou zijn om de ingebouwde engine mshtml te gebruiken:

using mshtml;
...
object[] oPageText = { html };
HTMLDocument doc = new HTMLDocumentClass();
IHTMLDocument2 doc2 = (IHTMLDocument2)doc;
doc2.write(oPageText);

Hiermee kunt u javascript-achtige functies gebruiken, zoals getElementById()


Antwoord 3, autoriteit 12%

Ik heb een project gevonden met de naam Fizzler dat een jQuery/Sizzler-benadering hanteert voor het selecteren van HTML-elementen. Het is gebaseerd op HTML Agility Pack. Het is momenteel in bèta en ondersteunt slechts een subset van CSS-kiezers, maar het is verdomd cool en verfrissend om CSS-kiezers te gebruiken in plaats van vervelende XPath.

http://code.google.com/p/fizzler/


Antwoord 4, autoriteit 7%

U kunt veel doen zonder moeren op te gaan op producten van derden en MSHTML (d.w.z. interop). Gebruik het systeem. Windows.form.Webbrowser. Vanaf daar kunt u zulke dingen doen als “Getelementsbyid” op een HTMLDocument of “Getelementsbytagname” op HTMLelements. Als u daadwerkelijk indeface met de browser (bijvoorbeeld simuleer knop klikken), kunt u een kleine reflectie gebruiken (imo a lager kwaad dan interop) om het te doen:

var wb = new WebBrowser()

… vertel de browser om navigeren (tangentieel naar deze vraag). Vervolgens kunt u op het gebied van Document_Completed, klikken als deze simuleren.

var doc = wb.Browser.Document
var elem = doc.GetElementById(elementId);
object obj = elem.DomElement;
System.Reflection.MethodInfo mi = obj.GetType().GetMethod("click");
mi.Invoke(obj, new object[0]);

U kunt soortgelijke reflectie-dingen doen om formulieren in te dienen, enz.

Geniet.


Antwoord 5, Autoriteit 7%

Ik heb een code geschreven die “LINQ aan HTML” -functionaliteit biedt. Ik dacht dat ik het hier zou delen. Het is gebaseerd op Majestic 12. Het vergt de majestueuze-12-resultaten en produceert LINQ XML-elementen. Op dat moment kunt u al uw LINQ gebruiken aan XML-tools tegen de HTML. Als een voorbeeld:

       IEnumerable<XNode> auctionNodes = Majestic12ToXml.Majestic12ToXml.ConvertNodesToXml(byteArrayOfAuctionHtml);
        foreach (XElement anchorTag in auctionNodes.OfType<XElement>().DescendantsAndSelf("a")) {
            if (anchorTag.Attribute("href") == null)
                continue;
            Console.WriteLine(anchorTag.Attribute("href").Value);
        }

Ik wilde Majestic-12 gebruiken omdat ik weet dat het veel ingebouwde kennis heeft met betrekking tot HTML die in het wild wordt gevonden. Wat ik echter heb gevonden, is dat het extra werk vereist om de Majestic-12-resultaten toe te wijzen aan iets dat LINQ als XML accepteert. De code die ik bijvoeg, doet veel van deze opschoning, maar als je deze gebruikt, zul je pagina’s vinden die worden afgewezen. U moet de code aanpassen om dat aan te pakken. Wanneer een uitzondering wordt gegenereerd, controleert u exception.Data[“bron”] omdat deze waarschijnlijk is ingesteld op de HTML-tag die de uitzondering heeft veroorzaakt. Op een leuke manier omgaan met de HTML is soms niet triviaal…

Dus nu de verwachtingen realistisch laag zijn, hier is de code 🙂

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Majestic12;
using System.IO;
using System.Xml.Linq;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Majestic12ToXml {
public class Majestic12ToXml {
    static public IEnumerable<XNode> ConvertNodesToXml(byte[] htmlAsBytes) {
        HTMLparser parser = OpenParser();
        parser.Init(htmlAsBytes);
        XElement currentNode = new XElement("document");
        HTMLchunk m12chunk = null;
        int xmlnsAttributeIndex = 0;
        string originalHtml = "";
        while ((m12chunk = parser.ParseNext()) != null) {
            try {
                Debug.Assert(!m12chunk.bHashMode);  // popular default for Majestic-12 setting
                XNode newNode = null;
                XElement newNodesParent = null;
                switch (m12chunk.oType) {
                    case HTMLchunkType.OpenTag:
                        // Tags are added as a child to the current tag, 
                        // except when the new tag implies the closure of 
                        // some number of ancestor tags.
                        newNode = ParseTagNode(m12chunk, originalHtml, ref xmlnsAttributeIndex);
                        if (newNode != null) {
                            currentNode = FindParentOfNewNode(m12chunk, originalHtml, currentNode);
                            newNodesParent = currentNode;
                            newNodesParent.Add(newNode);
                            currentNode = newNode as XElement;
                        }
                        break;
                    case HTMLchunkType.CloseTag:
                        if (m12chunk.bEndClosure) {
                            newNode = ParseTagNode(m12chunk, originalHtml, ref xmlnsAttributeIndex);
                            if (newNode != null) {
                                currentNode = FindParentOfNewNode(m12chunk, originalHtml, currentNode);
                                newNodesParent = currentNode;
                                newNodesParent.Add(newNode);
                            }
                        }
                        else {
                            XElement nodeToClose = currentNode;
                            string m12chunkCleanedTag = CleanupTagName(m12chunk.sTag, originalHtml);
                            while (nodeToClose != null && nodeToClose.Name.LocalName != m12chunkCleanedTag)
                                nodeToClose = nodeToClose.Parent;
                            if (nodeToClose != null)
                                currentNode = nodeToClose.Parent;
                            Debug.Assert(currentNode != null);
                        }
                        break;
                    case HTMLchunkType.Script:
                        newNode = new XElement("script", "REMOVED");
                        newNodesParent = currentNode;
                        newNodesParent.Add(newNode);
                        break;
                    case HTMLchunkType.Comment:
                        newNodesParent = currentNode;
                        if (m12chunk.sTag == "!--")
                            newNode = new XComment(m12chunk.oHTML);
                        else if (m12chunk.sTag == "![CDATA[")
                            newNode = new XCData(m12chunk.oHTML);
                        else
                            throw new Exception("Unrecognized comment sTag");
                        newNodesParent.Add(newNode);
                        break;
                    case HTMLchunkType.Text:
                        currentNode.Add(m12chunk.oHTML);
                        break;
                    default:
                        break;
                }
            }
            catch (Exception e) {
                var wrappedE = new Exception("Error using Majestic12.HTMLChunk, reason: " + e.Message, e);
                // the original html is copied for tracing/debugging purposes
                originalHtml = new string(htmlAsBytes.Skip(m12chunk.iChunkOffset)
                    .Take(m12chunk.iChunkLength)
                    .Select(B => (char)B).ToArray()); 
                wrappedE.Data.Add("source", originalHtml);
                throw wrappedE;
            }
        }
        while (currentNode.Parent != null)
            currentNode = currentNode.Parent;
        return currentNode.Nodes();
    }
    static XElement FindParentOfNewNode(Majestic12.HTMLchunk m12chunk, string originalHtml, XElement nextPotentialParent) {
        string m12chunkCleanedTag = CleanupTagName(m12chunk.sTag, originalHtml);
        XElement discoveredParent = null;
        // Get a list of all ancestors
        List<XElement> ancestors = new List<XElement>();
        XElement ancestor = nextPotentialParent;
        while (ancestor != null) {
            ancestors.Add(ancestor);
            ancestor = ancestor.Parent;
        }
        // Check if the new tag implies a previous tag was closed.
        if ("form" == m12chunkCleanedTag) {
            discoveredParent = ancestors
                .Where(XE => m12chunkCleanedTag == XE.Name)
                .Take(1)
                .Select(XE => XE.Parent)
                .FirstOrDefault();
        }
        else if ("td" == m12chunkCleanedTag) {
            discoveredParent = ancestors
                .TakeWhile(XE => "tr" != XE.Name)
                .Where(XE => m12chunkCleanedTag == XE.Name)
                .Take(1)
                .Select(XE => XE.Parent)
                .FirstOrDefault();
        }
        else if ("tr" == m12chunkCleanedTag) {
            discoveredParent = ancestors
                .TakeWhile(XE => !("table" == XE.Name
                                    || "thead" == XE.Name
                                    || "tbody" == XE.Name
                                    || "tfoot" == XE.Name))
                .Where(XE => m12chunkCleanedTag == XE.Name)
                .Take(1)
                .Select(XE => XE.Parent)
                .FirstOrDefault();
        }
        else if ("thead" == m12chunkCleanedTag
                  || "tbody" == m12chunkCleanedTag
                  || "tfoot" == m12chunkCleanedTag) {
            discoveredParent = ancestors
                .TakeWhile(XE => "table" != XE.Name)
                .Where(XE => m12chunkCleanedTag == XE.Name)
                .Take(1)
                .Select(XE => XE.Parent)
                .FirstOrDefault();
        }
        return discoveredParent ?? nextPotentialParent;
    }
    static string CleanupTagName(string originalName, string originalHtml) {
        string tagName = originalName;
        tagName = tagName.TrimStart(new char[] { '?' });  // for nodes <?xml >
        if (tagName.Contains(':'))
            tagName = tagName.Substring(tagName.LastIndexOf(':') + 1);
        return tagName;
    }
    static readonly Regex _startsAsNumeric = new Regex(@"^[0-9]", RegexOptions.Compiled);
    static bool TryCleanupAttributeName(string originalName, ref int xmlnsIndex, out string result) {
        result = null;
        string attributeName = originalName;
        if (string.IsNullOrEmpty(originalName))
            return false;
        if (_startsAsNumeric.IsMatch(originalName))
            return false;
        //
        // transform xmlns attributes so they don't actually create any XML namespaces
        //
        if (attributeName.ToLower().Equals("xmlns")) {
            attributeName = "xmlns_" + xmlnsIndex.ToString(); ;
            xmlnsIndex++;
        }
        else {
            if (attributeName.ToLower().StartsWith("xmlns:")) {
                attributeName = "xmlns_" + attributeName.Substring("xmlns:".Length);
            }   
            //
            // trim trailing \"
            //
            attributeName = attributeName.TrimEnd(new char[] { '\"' });
            attributeName = attributeName.Replace(":", "_");
        }
        result = attributeName;
        return true;
    }
    static Regex _weirdTag = new Regex(@"^<!\[.*\]>$");       // matches "<![if !supportEmptyParas]>"
    static Regex _aspnetPrecompiled = new Regex(@"^<%.*%>$"); // matches "<%@ ... %>"
    static Regex _shortHtmlComment = new Regex(@"^<!-.*->$"); // matches "<!-Extra_Images->"
    static XElement ParseTagNode(Majestic12.HTMLchunk m12chunk, string originalHtml, ref int xmlnsIndex) {
        if (string.IsNullOrEmpty(m12chunk.sTag)) {
            if (m12chunk.sParams.Length > 0 && m12chunk.sParams[0].ToLower().Equals("doctype"))
                return new XElement("doctype");
            if (_weirdTag.IsMatch(originalHtml))
                return new XElement("REMOVED_weirdBlockParenthesisTag");
            if (_aspnetPrecompiled.IsMatch(originalHtml))
                return new XElement("REMOVED_ASPNET_PrecompiledDirective");
            if (_shortHtmlComment.IsMatch(originalHtml))
                return new XElement("REMOVED_ShortHtmlComment");
            // Nodes like "<br <br>" will end up with a m12chunk.sTag==""...  We discard these nodes.
            return null;
        }
        string tagName = CleanupTagName(m12chunk.sTag, originalHtml);
        XElement result = new XElement(tagName);
        List<XAttribute> attributes = new List<XAttribute>();
        for (int i = 0; i < m12chunk.iParams; i++) {
            if (m12chunk.sParams[i] == "<!--") {
                // an HTML comment was embedded within a tag.  This comment and its contents
                // will be interpreted as attributes by Majestic-12... skip this attributes
                for (; i < m12chunk.iParams; i++) {
                    if (m12chunk.sTag == "--" || m12chunk.sTag == "-->")
                        break;
                }
                continue;
            }
            if (m12chunk.sParams[i] == "?" && string.IsNullOrEmpty(m12chunk.sValues[i]))
                continue;
            string attributeName = m12chunk.sParams[i];
            if (!TryCleanupAttributeName(attributeName, ref xmlnsIndex, out attributeName))
                continue;
            attributes.Add(new XAttribute(attributeName, m12chunk.sValues[i]));
        }
        // If attributes are duplicated with different values, we complain.
        // If attributes are duplicated with the same value, we remove all but 1.
        var duplicatedAttributes = attributes.GroupBy(A => A.Name).Where(G => G.Count() > 1);
        foreach (var duplicatedAttribute in duplicatedAttributes) {
            if (duplicatedAttribute.GroupBy(DA => DA.Value).Count() > 1)
                throw new Exception("Attribute value was given different values");
            attributes.RemoveAll(A => A.Name == duplicatedAttribute.Key);
            attributes.Add(duplicatedAttribute.First());
        }
        result.Add(attributes);
        return result;
    }
    static HTMLparser OpenParser() {
        HTMLparser oP = new HTMLparser();
        // The code+comments in this function are from the Majestic-12 sample documentation.
        // ...
        // This is optional, but if you want high performance then you may
        // want to set chunk hash mode to FALSE. This would result in tag params
        // being added to string arrays in HTMLchunk object called sParams and sValues, with number
        // of actual params being in iParams. See code below for details.
        //
        // When TRUE (and its default) tag params will be added to hashtable HTMLchunk (object).oParams
        oP.SetChunkHashMode(false);
        // if you set this to true then original parsed HTML for given chunk will be kept - 
        // this will reduce performance somewhat, but may be desireable in some cases where
        // reconstruction of HTML may be necessary
        oP.bKeepRawHTML = false;
        // if set to true (it is false by default), then entities will be decoded: this is essential
        // if you want to get strings that contain final representation of the data in HTML, however
        // you should be aware that if you want to use such strings into output HTML string then you will
        // need to do Entity encoding or same string may fail later
        oP.bDecodeEntities = true;
        // we have option to keep most entities as is - only replace stuff like &nbsp; 
        // this is called Mini Entities mode - it is handy when HTML will need
        // to be re-created after it was parsed, though in this case really
        // entities should not be parsed at all
        oP.bDecodeMiniEntities = true;
        if (!oP.bDecodeEntities && oP.bDecodeMiniEntities)
            oP.InitMiniEntities();
        // if set to true, then in case of Comments and SCRIPT tags the data set to oHTML will be
        // extracted BETWEEN those tags, rather than include complete RAW HTML that includes tags too
        // this only works if auto extraction is enabled
        oP.bAutoExtractBetweenTagsOnly = true;
        // if true then comments will be extracted automatically
        oP.bAutoKeepComments = true;
        // if true then scripts will be extracted automatically: 
        oP.bAutoKeepScripts = true;
        // if this option is true then whitespace before start of tag will be compressed to single
        // space character in string: " ", if false then full whitespace before tag will be returned (slower)
        // you may only want to set it to false if you want exact whitespace between tags, otherwise it is just
        // a waste of CPU cycles
        oP.bCompressWhiteSpaceBeforeTag = true;
        // if true (default) then tags with attributes marked as CLOSED (/ at the end) will be automatically
        // forced to be considered as open tags - this is no good for XML parsing, but I keep it for backwards
        // compatibility for my stuff as it makes it easier to avoid checking for same tag which is both closed
        // or open
        oP.bAutoMarkClosedTagsWithParamsAsOpen = false;
        return oP;
    }
}
}  

Antwoord 6, autoriteit 5%

Het Html Agility Pack is al eerder genoemd – als je voor snelheid gaat, wil je misschien ook eens kijken naar de Majestic-12 HTML-parser. De bediening is nogal onhandig, maar het levert een echt snelle parseerervaring op.


Antwoord 7, autoriteit 2%

Ik denk dat @Erlends gebruik van HTMLDocumentde bestemanier is. Ik heb echter ook veel geluk gehad met het gebruik van deze eenvoudige bibliotheek:

SgmlReader


Antwoord 8

Geen lib van derden, WebBrowser-klasse-oplossing die kan worden uitgevoerd op Console en Asp.net

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Threading;
class ParseHTML
{
    public ParseHTML() { }
    private string ReturnString;
    public string doParsing(string html)
    {
        Thread t = new Thread(TParseMain);
        t.ApartmentState = ApartmentState.STA;
        t.Start((object)html);
        t.Join();
        return ReturnString;
    }
    private void TParseMain(object html)
    {
        WebBrowser wbc = new WebBrowser();
        wbc.DocumentText = "feces of a dummy";        //;magic words        
        HtmlDocument doc = wbc.Document.OpenNew(true);
        doc.Write((string)html);
        this.ReturnString = doc.Body.InnerHtml + " do here something";
        return;
    }
}

gebruik:

string myhtml = "<HTML><BODY>This is a new HTML document.</BODY></HTML>";
Console.WriteLine("before:" + myhtml);
myhtml = (new ParseHTML()).doParsing(myhtml);
Console.WriteLine("after:" + myhtml);

Antwoord 9

Het probleem met het ontleden van HTML is dat het geen exacte wetenschap is. Als het XHTML was dat je aan het ontleden was, dan zou het een stuk eenvoudiger zijn (zoals je al zei, je zou een algemene XML-parser kunnen gebruiken). Omdat HTML niet per se goed gevormde XML is, zul je veel problemen krijgen als je het probeert te ontleden. Het moet bijna per site worden gedaan.


Antwoord 10

Ik heb in het verleden ZetaHtmlTidygebruikt om willekeurige websites te laden en raak vervolgens verschillende delen van de inhoud aan met xpath (bijv. /html/body//p[@class=’textblock’]). Het werkte goed, maar er waren enkele uitzonderlijke sites waarmee het problemen had, dus ik weet niet of dit absoluut de beste oplossing is.


Antwoord 11

Je zou een HTML-DTD kunnen gebruiken, en de generieke XML-parsingbibliotheken.


Antwoord 12

Gebruik WatiN als u de impact van JS op de pagina wilt zien [en u bent bereid een browser te starten]


Antwoord 13

Afhankelijk van uw behoeften kunt u kiezen voor de bibliotheken met meer functies. Ik heb de meeste / alle voorgestelde oplossingen geprobeerd, maar wat opviel was het hoofd & schouders was Html Agility Pack. Het is een zeer vergevingsgezinde en flexibele parser.


Antwoord 14

Probeer dit script.

http://www.biterscripting.com/SS_URLs.html

Als ik het gebruik met deze url,

script SS_URLs.txt URL("https://stackoverflow.com/questions/56107/what-is-the-best-way-to-parse-html-in-c")

Het toont me alle links op de pagina voor deze thread.

http://sstatic.net/so/all.css
http://sstatic.net/so/favicon.ico
http://sstatic.net/so/apple-touch-icon.png
.
.
.

U kunt dit script wijzigen om te controleren op afbeeldingen, variabelen, wat dan ook.


Antwoord 15

Ik schreef sommige klassen voor het parseren van HTML-tags in C #. Ze zijn mooi en eenvoudig als ze aan uw specifieke behoeften voldoen.

U kunt een artikel over hen lezen en de broncode downloaden op http://www.blackbeltcoder.com/articles/Sstrings/parsing-html-tags-in-c .

Er is ook een artikel over een generieke parsing helper-klasse bij http : //www.blackbeltcoder.com/articles/Sstrings/a-text-parsing-helper-class .

Other episodes