【问题标题】:How to extract text from resonably sane HTML?如何从合理的 HTML 中提取文本?
【发布时间】:2011-01-07 23:47:08
【问题描述】:

我的问题有点像this question,但我有更多限制:

  • 我知道该文档的内容相当合理
  • 它们非常有规律(它们都来自同一个来源
  • 我想要大约 99% 的可见文本
  • 大约 99% 的可行内容是文本(它们或多或少是 RTF 转换为 HTML)
  • 我不关心格式,甚至不关心分段。

是否有设置任何工具来执行此操作,或者我最好只是打破 RegexBuddy 和 C#?

我对命令行或批处理工具以及 C/C#/D 库持开放态度。

【问题讨论】:

  • 如果不是这样的限制,我什至都不会想到正则表达式:)

标签: c# html d text-extraction


【解决方案1】:

我今天用HTML Agility Pack 破解的这段代码将提取无格式的修剪文本。

public static string ExtractText(string html)
{
    if (html == null)
    {
        throw new ArgumentNullException("html");
    }

    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(html);

    var chunks = new List<string>(); 

    foreach (var item in doc.DocumentNode.DescendantNodesAndSelf())
    {
        if (item.NodeType == HtmlNodeType.Text)
        {
            if (item.InnerText.Trim() != "")
            {
                chunks.Add(item.InnerText.Trim());
            }
        }
    }
    return String.Join(" ", chunks);
}

如果您想保持某种程度的格式,您可以在源代码提供的the sample 上构建。

public string Convert(string path)
{
    HtmlDocument doc = new HtmlDocument();
    doc.Load(path);

    StringWriter sw = new StringWriter();
    ConvertTo(doc.DocumentNode, sw);
    sw.Flush();
    return sw.ToString();
}

public string ConvertHtml(string html)
{
    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(html);

    StringWriter sw = new StringWriter();
    ConvertTo(doc.DocumentNode, sw);
    sw.Flush();
    return sw.ToString();
}

public void ConvertTo(HtmlNode node, TextWriter outText)
{
    string html;
    switch (node.NodeType)
    {
        case HtmlNodeType.Comment:
            // don't output comments
            break;

        case HtmlNodeType.Document:
            ConvertContentTo(node, outText);
            break;

        case HtmlNodeType.Text:
            // script and style must not be output
            string parentName = node.ParentNode.Name;
            if ((parentName == "script") || (parentName == "style"))
                break;

            // get text
            html = ((HtmlTextNode) node).Text;

            // is it in fact a special closing node output as text?
            if (HtmlNode.IsOverlappedClosingElement(html))
                break;

            // check the text is meaningful and not a bunch of whitespaces
            if (html.Trim().Length > 0)
            {
                outText.Write(HtmlEntity.DeEntitize(html));
            }
            break;

        case HtmlNodeType.Element:
            switch (node.Name)
            {
                case "p":
                    // treat paragraphs as crlf
                    outText.Write("\r\n");
                    break;
            }

            if (node.HasChildNodes)
            {
                ConvertContentTo(node, outText);
            }
            break;
    }
}


private void ConvertContentTo(HtmlNode node, TextWriter outText)
{
    foreach (HtmlNode subnode in node.ChildNodes)
    {
        ConvertTo(subnode, outText);
    }
}

【讨论】:

    【解决方案2】:

    您可以使用支持从 HTML 提取文本的NUglify

    var result = Uglify.HtmlToText("<div>  <p>This is <em>   a text    </em></p>   </div>");
    Console.WriteLine(result.Code);   // prints: This is a text
    

    由于它使用的是 HTML5 自定义解析器,它应该非常健壮(特别是如果文档不包含任何错误)并且速度非常快(不涉及正则表达式,而是一个纯递归下降解析器)

    【讨论】:

    • FWIW,这非常好用,而且几乎没有起毛。谢谢!
    【解决方案3】:

    您需要使用HTML Agility Pack

    您可能希望使用 LINQ 找到一个元素,然后调用 Descendants,然后获取它的 InnerText

    【讨论】:

    • 你的意思是我需要学习LINQ? (令人惊讶的是,这确实是我遇到的第一件事,LINQ 听起来像是正确的方法,但话又说回来,我通常不在这个领域)
    • @BCS:您不需要学习 LINQ,但是 LINQ 使它更易于使用。我猜想有效地使用 LINQ 将使您的代码至少缩短 120%,并且也更容易理解。
    • +1 敏捷包比编写自己的 DOM 处理程序要好得多。
    • 碰巧,LINQ 并不是最简单的解决方案,而仅仅是因为有一个示例项目 html2text 完成了我想要的 90% 的工作,而最后的 1% 只需添加几行就很简单了if(...) return; 的(OTOH 文档不太好。)
    【解决方案4】:

    这是我正在使用的代码:

    using System.Web;
    public static string ExtractText(string html)
    {
        Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase);
        string s =reg.Replace(html, " ");
        s = HttpUtility.HtmlDecode(s);
        return s;
    }
    

    【讨论】:

    • 这在某些情况下是可以接受的。但是,请注意,注释或 CDATA 块中出现的任何直角括号都会破坏此正则表达式,更不用说正则表达式可能会破坏 &lt;script&gt;&lt;style&gt; 标记的内容。此外,尽管(据我所知)该标准要求对属性值中的尖括号进行编码,但现代浏览器可以容忍 &lt;div data:tree="parent&gt;child"&gt;Some text&lt;/div&gt; 之类的内容,这也会破坏您的正则表达式。
    • 这里的正则表达式使用IgnoreCase 选项的目的是什么?
    【解决方案5】:

    这是最好的方法:

      public static string StripHTML(string HTMLText)
        {
            Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase);
            return reg.Replace(HTMLText, "");
        }
    

    【讨论】:

    【解决方案6】:

    如果将 HTML 加载到 C# 中,然后在 C#/WinForms 中使用 mshtml.dll 或 WebBrowser 控件,则相对简单,然后可以将整个 HTML 文档视为一棵树,遍历树捕获 InnerText 对象。

    或者,您也可以使用 document.all,它获取树并将其展平,然后您可以遍历树,再次捕获 InnerText。

    这是一个例子:

            WebBrowser webBrowser = new WebBrowser();
            webBrowser.Url = new Uri("url_of_file"); //can be remote or local
            webBrowser.DocumentCompleted += delegate
            {
                HtmlElementCollection collection = webBrowser.Document.All;
                List<string> contents = new List<string>();
    
                /*
                 * Adds all inner-text of a tag, including inner-text of sub-tags
                 * ie. <html><body><a>test</a><b>test 2</b></body></html> would do:
                 * "test test 2" when collection[i] == <html>
                 * "test test 2" when collection[i] == <body>
                 * "test" when collection[i] == <a>
                 * "test 2" when collection[i] == <b>
                 */
                for (int i = 0; i < collection.Count; i++)
                {
                    if (!string.IsNullOrEmpty(collection[i].InnerText))
                    {
                        contents.Add(collection[i].InnerText);
                    }
                }
    
                /*
                 * <html><body><a>test</a><b>test 2</b></body></html>
                 * outputs: test test 2|test test 2|test|test 2
                 */
                string contentString = string.Join("|", contents.ToArray());
                MessageBox.Show(contentString);
            };
    

    希望有帮助!

    【讨论】:

    • 谷歌搜索 mshtml.dll 提供大部分页面或错误报告、错误修复和错误。 --- 你有一些文档的链接吗?
    • 我刚刚用一个使用 WebBrowser 控件的示例编辑了我的帖子。
    • 不幸的是,这种方法不适用于服务器核心系统,因为它们没有安装 WebBrowser 组件。
    【解决方案7】:

    这是我开发的一个类来完成同样的事情。所有可用的 HTML 解析库都太慢了,正则表达式也太慢了。代码 cmets 中解释了功能。根据我的基准测试,在亚马逊的登录页面(包括在下面)上测试时,此代码比 HTML Agility Pack 的等效代码快 10 倍多一点。

    /// <summary>
    /// The fast HTML text extractor class is designed to, as quickly and as ignorantly as possible,
    /// extract text data from a given HTML character array. The class searches for and deletes
    /// script and style tags in a first and second pass, with an optional third pass to do the same
    /// to HTML comments, and then copies remaining non-whitespace character data to an ouput array.
    /// All whitespace encountered is replaced with a single whitespace in to avoid multiple
    /// whitespace in the output.
    ///
    /// Note that the returned text content still may have named character and numbered character
    /// references within that, when decoded, may produce multiple whitespace.
    /// </summary>
    public class FastHtmlTextExtractor
    {
    
        private readonly char[] SCRIPT_OPEN_TAG = new char[7] { '<', 's', 'c', 'r', 'i', 'p', 't' };
        private readonly char[] SCRIPT_CLOSE_TAG = new char[9] { '<', '/', 's', 'c', 'r', 'i', 'p', 't', '>' };
    
        private readonly char[] STYLE_OPEN_TAG = new char[6] { '<', 's', 't', 'y', 'l', 'e' };
        private readonly char[] STYLE_CLOSE_TAG = new char[8] { '<', '/', 's', 't', 'y', 'l', 'e', '>' };
    
        private readonly char[] COMMENT_OPEN_TAG = new char[3] { '<', '!', '-' };
        private readonly char[] COMMENT_CLOSE_TAG = new char[3] { '-', '-', '>' };
    
        private int[] m_deletionDictionary;
    
        public string Extract(char[] input, bool stripComments = false)
        {
            var len = input.Length;
            int next = 0;
    
            m_deletionDictionary = new int[len];
    
            // Whipe out all text content between style and script tags.
            FindAndWipe(SCRIPT_OPEN_TAG, SCRIPT_CLOSE_TAG, input);
            FindAndWipe(STYLE_OPEN_TAG, STYLE_CLOSE_TAG, input);
    
            if(stripComments)
            {
                // Whipe out everything between HTML comments.
                FindAndWipe(COMMENT_OPEN_TAG, COMMENT_CLOSE_TAG, input);
            }
    
            // Whipe text between all other tags now.
            while(next < len)
            {
                next = SkipUntil(next, '<', input);
    
                if(next < len)
                {
                    var closeNext = SkipUntil(next, '>', input);
    
                    if(closeNext < len)
                    {
                        m_deletionDictionary[next] = (closeNext + 1) - next;
                        WipeRange(next, closeNext + 1, input);
                    }
    
                    next = closeNext + 1;
                }
            }
    
            // Collect all non-whitespace and non-null chars into a new
            // char array. All whitespace characters are skipped and replaced
            // with a single space char. Multiple whitespace is ignored.
            var lastSpace = true;
            var extractedPos = 0;
            var extracted = new char[len];
    
            for(next = 0; next < len; ++next)
            {
                if(m_deletionDictionary[next] > 0)
                {
                    next += m_deletionDictionary[next];
                    continue;
                }
    
                if(char.IsWhiteSpace(input[next]) || input[next] == '\0')
                {
                    if(lastSpace)
                    {
                        continue;
                    }
    
                    extracted[extractedPos++] = ' ';
                    lastSpace = true;
                }
                else
                {
                    lastSpace = false;
                    extracted[extractedPos++] = input[next];
                }
            }
    
            return new string(extracted, 0, extractedPos);
        }
    
        /// <summary>
        /// Does a search in the input array for the characters in the supplied open and closing tag
        /// char arrays. Each match where both tag open and tag close are discovered causes the text
        /// in between the matches to be overwritten by Array.Clear().
        /// </summary>
        /// <param name="openingTag">
        /// The opening tag to search for.
        /// </param>
        /// <param name="closingTag">
        /// The closing tag to search for.
        /// </param>
        /// <param name="input">
        /// The input to search in.
        /// </param>
        private void FindAndWipe(char[] openingTag, char[] closingTag, char[] input)
        {
            int len = input.Length;
            int pos = 0;
    
            do
            {
                pos = FindNext(pos, openingTag, input);
    
                if(pos < len)
                {
                    var closenext = FindNext(pos, closingTag, input);
    
                    if(closenext < len)
                    {
                        m_deletionDictionary[pos - openingTag.Length] = closenext - (pos - openingTag.Length);
                        WipeRange(pos - openingTag.Length, closenext, input);
                    }
    
                    if(closenext > pos)
                    {
                        pos = closenext;
                    }
                    else
                    {
                        ++pos;
                    }
                }
            }
            while(pos < len);
        }
    
        /// <summary>
        /// Skips as many characters as possible within the input array until the given char is
        /// found. The position of the first instance of the char is returned, or if not found, a
        /// position beyond the end of the input array is returned.
        /// </summary>
        /// <param name="pos">
        /// The starting position to search from within the input array.
        /// </param>
        /// <param name="c">
        /// The character to find.
        /// </param>
        /// <param name="input">
        /// The input to search within.
        /// </param>
        /// <returns>
        /// The position of the found character, or an index beyond the end of the input array.
        /// </returns>
        private int SkipUntil(int pos, char c, char[] input)
        {
            if(pos >= input.Length)
            {
                return pos;
            }
    
            do
            {
                if(input[pos] == c)
                {
                    return pos;
                }
    
                ++pos;
            }
            while(pos < input.Length);
    
            return pos;
        }
    
        /// <summary>
        /// Clears a given range in the input array.
        /// </summary>
        /// <param name="start">
        /// The start position from which the array will begin to be cleared.
        /// </param>
        /// <param name="end">
        /// The end position in the array, the position to clear up-until.
        /// </param>
        /// <param name="input">
        /// The source array wherin the supplied range will be cleared.
        /// </param>
        /// <remarks>
        /// Note that the second parameter is called end, not lenghth. This parameter is meant to be
        /// a position in the array, not the amount of entries in the array to clear.
        /// </remarks>
        private void WipeRange(int start, int end, char[] input)
        {
            Array.Clear(input, start, end - start);
        }
    
        /// <summary>
        /// Finds the next occurance of the supplied char array within the input array. This search
        /// ignores whitespace.
        /// </summary>
        /// <param name="pos">
        /// The position to start searching from.
        /// </param>
        /// <param name="what">
        /// The sequence of characters to find.
        /// </param>
        /// <param name="input">
        /// The input array to perform the search on.
        /// </param>
        /// <returns>
        /// The position of the end of the first matching occurance. That is, the returned position
        /// points to the very end of the search criteria within the input array, not the start. If
        /// no match could be found, a position beyond the end of the input array will be returned.
        /// </returns>
        public int FindNext(int pos, char[] what, char[] input)
        {
            do
            {
                if(Next(ref pos, what, input))
                {
                    return pos;
                }
                ++pos;
            }
            while(pos < input.Length);
    
            return pos;
        }
    
        /// <summary>
        /// Probes the input array at the given position to determine if the next N characters
        /// matches the supplied character sequence. This check ignores whitespace.
        /// </summary>
        /// <param name="pos">
        /// The position at which to check within the input array for a match to the supplied
        /// character sequence.
        /// </param>
        /// <param name="what">
        /// The character sequence to attempt to match. Note that whitespace between characters
        /// within the input array is accebtale.
        /// </param>
        /// <param name="input">
        /// The input array to check within.
        /// </param>
        /// <returns>
        /// True if the next N characters within the input array matches the supplied search
        /// character sequence. Returns false otherwise.
        /// </returns>
        public bool Next(ref int pos, char[] what, char[] input)
        {
            int z = 0;
    
            do
            {
                if(char.IsWhiteSpace(input[pos]) || input[pos] == '\0')
                {
                    ++pos;
                    continue;
                }
    
                if(input[pos] == what[z])
                {
                    ++z;
                    ++pos;
                    continue;
                }
    
                return false;
            }
            while(pos < input.Length && z < what.Length);
    
            return z == what.Length;
        }
    }
    

    在 HtmlAgilityPack 中等效:

    // Where m_whitespaceRegex is a Regex with [\s].
    // Where sampleHtmlText is a raw HTML string.
    
    var extractedSampleText = new StringBuilder();
    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(sampleHtmlText);
    
    if(doc != null && doc.DocumentNode != null)
    {
        foreach(var script in doc.DocumentNode.Descendants("script").ToArray())
        {
            script.Remove();
        }
    
        foreach(var style in doc.DocumentNode.Descendants("style").ToArray())
        {
            style.Remove();
        }
    
        var allTextNodes = doc.DocumentNode.SelectNodes("//text()");
        if(allTextNodes != null && allTextNodes.Count > 0)
        {
            foreach(HtmlNode node in allTextNodes)
            {
                extractedSampleText.Append(node.InnerText);
            }
        }
    
        var finalText = m_whitespaceRegex.Replace(extractedSampleText.ToString(), " ");
    }
    

    【讨论】:

      【解决方案8】:

      您可以在此处下载可在 HTML 和 XAML 之间进行转换的工具及其源代码:XAML/HTML converter

      它包含一个 HTML 解析器(这样的东西显然比你的标准 XML 解析器更宽容),你可以像 XML 一样遍历 HTML。

      【讨论】:

        【解决方案9】:

        从命令行,你可以使用Lynx文本浏览器like this

        如果您想下载格式化输出的网页(即,没有 HTML 标记,而是显示在 Lynx 中),请输入:

        lynx -dump URL > filename
        

        如果页面上有任何链接,这些链接的 URL 将包含在下载页面的末尾。

        你可以disable the list of links-nolist。例如:

        lynx -dump -nolist http://stackoverflow.com/a/10469619/724176 > filename
        

        【讨论】:

          猜你喜欢
          • 2011-08-16
          • 2016-02-07
          • 1970-01-01
          • 2010-11-12
          • 2011-04-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多