Как лучше всего разобрать html на C#?

Я ищу библиотеку / метод для синтаксического анализа html файла с более специфическими функциями html, чем общие библиотеки синтаксического анализа xml.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
66
0
470 395
15

Ответы 15

Вы можете использовать HTML DTD и общие библиотеки синтаксического анализа XML.

Очень немногие реальные HTML-страницы переживут библиотеку синтаксического анализа XML.

Frank Krueger 11.09.2008 15:07

Проблема с синтаксическим анализом HTML в том, что это не точная наука. Если бы вы разбирали XHTML, все было бы намного проще (как вы упомянули, вы могли бы использовать обычный XML-анализатор). Поскольку HTML не обязательно является правильно сформированным XML, вы столкнетесь с множеством проблем, пытаясь его проанализировать. Это почти необходимо делать на индивидуальной основе.

Разве синтаксический анализ не формирует HTML, как определено W3C, как точную науку, как XHTML?

pupeno 08.12.2009 15:56

Должно быть, но люди этого не делают.

Dominic K 16.02.2010 06:54

@J. Пабло: Не так просто (отсюда и причина для библиотеки: p) ... например, теги <p> не нужно явно закрывать в HTML4 / 5. Ой!

user166390 22.12.2010 07:13

Вы можете использовать TidyNet.Tidy для преобразования HTML в XHTML, а затем использовать синтаксический анализатор XML.

Другой альтернативой было бы использование встроенного движка mshtml:

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

Это позволяет использовать функции, подобные javascript, например getElementById ().

Назовите меня сумасшедшим, но я не могу понять, как использовать mshtml. У вас есть хорошие ссылки?

Alex Baranosky 09.01.2009 08:52

@Alex, вам нужно включить Microsoft.mshtml, можно найти немного больше информации здесь: msdn.microsoft.com/en-us/library/aa290341(VS.71).aspx

Wilfred Knievel 13.01.2010 02:17

У меня есть сообщение в блоге о Tidy.Net и ManagedTidy, которые могут анализировать и проверять (x) html-файлы. Если вам не нужно проверять материал. Я бы выбрал htmlagilitypack. jphellemons.nl/post/…

JP Hellemons 25.10.2011 11:03

Я думаю, что использование HTMLDocument в @ Erlend - это лучший способ Лучший. Однако мне также повезло с использованием этой простой библиотеки:

SgmlReader

Вы можете многое сделать, не сходя с ума по сторонним продуктам и mshtml (то есть взаимодействию). используйте System.Windows.Forms.WebBrowser. Оттуда вы можете делать такие вещи, как GetElementById в HtmlDocument или GetElementsByTagName в HtmlElements. Если вы действительно хотите взаимодействовать с браузером (например, имитировать щелчки кнопок), вы можете использовать небольшое отражение (я думаю, меньшее зло, чем Interop), чтобы сделать это:

var wb = new WebBrowser()

... скажите браузеру, что нужно перемещаться (касательно этого вопроса). Затем в событии Document_Completed вы можете имитировать подобные щелчки.

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

вы можете делать аналогичные рефлексии для отправки форм и т. д.

Наслаждаться.

Раньше я использовал ZetaHtmlTidy для загрузки случайных веб-сайтов, а затем ударял по различным частям контента с помощью xpath (например, / html / body // p [@ class = 'textblock']). Он работал хорошо, но были некоторые исключительные сайты, с которыми у него были проблемы, поэтому я не знаю, является ли это лучшим решением.

Пакет гибкости HTML

This is an agile HTML parser that builds a read/write DOM and supports plain XPATH or XSLT (you actually don't HAVE to understand XPATH nor XSLT to use it, don't worry...). It is a .NET code library that allows you to parse "out of the web" HTML files. The parser is very tolerant with "real world" malformed HTML. The object model is very similar to what proposes System.Xml, but for HTML documents (or streams).

Стоит отметить, что он плохо справляется с самозакрывающимися тегами, такими как <p> (которые он интерпретирует как пустые), и очень плохо с необязательными конечными тегами, такими как <li> (которые он интерпретирует как отсутствие конечного тега, и т. размещает последовательные теги li).

Eamon Nerbonne 14.05.2011 20:48

Пакет Html Agility Pack уже упоминался ранее - если вы стремитесь к скорости, вы также можете проверить парсер Majestic-12 HTML. Его обработка довольно неуклюжая, но он обеспечивает действительно быстрый синтаксический анализ.

Я написал код, обеспечивающий функциональность «LINQ to HTML». Я думал, что поделюсь этим здесь. Он основан на Majestic 12. Он использует результаты Majestic-12 и создает элементы LINQ XML. На этом этапе вы можете использовать все свои инструменты LINQ to XML против HTML. В качестве примера:

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

Я хотел использовать Majestic-12, потому что знаю, что он имеет много встроенных знаний в отношении HTML, который встречается в дикой природе. Однако я обнаружил, что для сопоставления результатов Majestic-12 с чем-то, что LINQ будет принимать как XML, требуется дополнительная работа. Код, который я включаю, выполняет большую часть этой очистки, но по мере его использования вы обнаружите, что страницы отклоняются. Вам нужно исправить код, чтобы решить эту проблему. При возникновении исключения проверьте exception.Data ["source"], поскольку он, скорее всего, установлен на тег HTML, вызвавший исключение. Хорошая обработка HTML иногда бывает нетривиальной ...

Итак, теперь, когда ожидания реально низкие, вот код :)

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

Кстати, в прошлом у меня хорошо работал HtmlAgilityPack, я просто предпочитаю LINQ.

Frank Schwieterman 09.03.2009 01:21

Какова производительность при добавлении преобразования LINQ? Есть идеи, как это сравнить с HtmlAgilityPack?

user29439 04.08.2011 02:42

Я никогда не сравнивал производительность. В наши дни я использую HtmlAgilityPack, гораздо меньше хлопот. К сожалению, в приведенном выше коде есть много особых случаев, для которых я не стал писать тесты, поэтому я не могу его поддерживать.

Frank Schwieterman 04.08.2011 04:40

Используйте WatiN, если вам нужно увидеть влияние JS на страницу [и вы готовы запустить браузер]

Я нашел проект под названием Fizzler, который использует подход jQuery / Sizzler для выбора элементов HTML. Он основан на HTML Agility Pack. В настоящее время он находится в стадии бета-тестирования и поддерживает только подмножество селекторов CSS, но чертовски круто и освежающе использовать селекторы CSS вместо неприятного XPath.

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

спасибо, выглядит интересно! Я был удивлен, учитывая популярность jQuery, что было так сложно найти проект C#, вдохновленный им. Если бы я только мог найти что-то, где манипуляции с документами и более продвинутый обход также были частью пакета ... :)

Funka 14.05.2010 05:33

Я использовал это только сегодня, и я должен сказать, что им очень легко пользоваться, если вы знаете jQuery.

Chi Chan 15.10.2010 00:56

В зависимости от ваших потребностей вы можете выбрать более многофункциональные библиотеки. Я пробовал большинство / все из предложенных решений, но что бросалось в глаза, так это Html Agility Pack. Это очень щадящий и гибкий парсер.

Попробуйте этот сценарий.

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

Когда я использую его с этим URL,

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

Он показывает мне все ссылки на странице этой темы.

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

Вы можете изменить этот сценарий, чтобы проверять изображения, переменные и т. д.

Я написал несколько классов для разбора HTML-тегов на C#. Они хороши и просты, если соответствуют вашим конкретным потребностям.

Вы можете прочитать о них статью и скачать исходный код на http://www.blackbeltcoder.com/Articles/strings/parsing-html-tags-in-c.

Также есть статья об общем вспомогательном классе синтаксического анализа в http://www.blackbeltcoder.com/Articles/strings/a-text-parsing-helper-class.

Нет сторонней библиотеки, решения класса WebBrowser, которое может работать на консоли, и 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;
    }
}

использование:

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

Другие вопросы по теме