如何将Html转换为纯文本?

135

我在一个表格中存储了一些Html片段。不是整页,没有标签或类似的东西,只有基本格式。

我想要能够仅作为文本显示该Html,在给定页面上没有格式(实际上只需前30-50个字符,但那很容易)。

我如何将Html中的“文本”作为纯文本放入字符串中?

因此,这段代码。

<b>Hello World.</b><br/><p><i>Is there anyone out there?</i><p>

变成:

你好,世界。有人在吗?


你可能需要使用SgmlReader。http://code.msdn.microsoft.com/SgmlReader - Leonardo Herrera
在http://www.blackbeltcoder.com/Articles/strings/convert-html-to-text上有一些非常简单和直接的代码,可以将HTML转换为纯文本。 - Jonathan Wood
这里有一些来自W3C的好建议:http://www.w3.org/Tools/html2things.html - Rich
4
如何将一个问题标记为一个在6个月后被提出的问题的副本?这似乎有些不合逻辑... - Stuart Helwig
我已经编写了一个将HTML转换为纯文本的函数(http://pastebin.com/NswerNkQ)。它有一些限制,例如不能从“a”标签中提取链接。我最好基于PHP的html2text源代码(https://github.com/soundasleep/html2text/blob/master/src/Html2Text.php)来改进我的函数。 - Uwe Keim
20个回答

126

14
我之前用过HtmlAgilityPack,但找不到ConvertToPlainText的相关信息。请问你能告诉我在哪里可以找到它吗? - horatio
9
Horatio,这在HtmlAgilityPack的示例文件中已经包含了:http://htmlagilitypack.codeplex.com/sourcecontrol/changeset/view/62772?projectName=htmlagilitypack#52179 - Judah Gabriel Himango
8
实际上,在敏捷包中没有内置的方法来实现这一点。你提供的链接是一个例子,它使用敏捷包遍历节点树,移除“script”和“style”标签,并将其他元素的内部文本写入输出字符串。我怀疑它在真实世界的输入测试中经过了充分的测试。 - Lou
4
有人能提供可行的代码,而不是需要重新修改才能正常运行的示例链接吗?请注意保持原意,使内容通俗易懂。 - Eric K
10
现在可以在这里找到示例:https://github.com/ceee/ReadSharp/blob/master/ReadSharp/HtmlUtilities.cs - StuartQ
显示剩余8条评论

82

我不能使用HtmlAgilityPack,所以我为自己写了一个次优解决方案。

private static string HtmlToPlainText(string html)
{
    const string tagWhiteSpace = @"(>|$)(\W|\n|\r)+<";//matches one or more (white space or line breaks) between '>' and '<'
    const string stripFormatting = @"<[^>]*(>|$)";//match any character between '<' and '>', even when end tag is missing
    const string lineBreak = @"<(br|BR)\s{0,1}\/{0,1}>";//matches: <br>,<br/>,<br />,<BR>,<BR/>,<BR />
    var lineBreakRegex = new Regex(lineBreak, RegexOptions.Multiline);
    var stripFormattingRegex = new Regex(stripFormatting, RegexOptions.Multiline);
    var tagWhiteSpaceRegex = new Regex(tagWhiteSpace, RegexOptions.Multiline);

    var text = html;
    //Decode html specific characters
    text = System.Net.WebUtility.HtmlDecode(text); 
    //Remove tag whitespace/line breaks
    text = tagWhiteSpaceRegex.Replace(text, "><");
    //Replace <br /> with line breaks
    text = lineBreakRegex.Replace(text, Environment.NewLine);
    //Strip formatting
    text = stripFormattingRegex.Replace(text, string.Empty);

    return text;
}

5
“<blabla>”被解析出来,因此我将文本=System.Net.WebUtility.HtmlDecode(text); 移到该方法的底部。 - Luuk
1
这太棒了,我还添加了一个多空间电容器,因为HTML可能是从CMS生成的:var spaceRegex = new Regex("[ ]{2,}", RegexOptions.None); - Enkode
有时,在HTML代码中会出现程序员的新行(新行在注释中看不到,所以我用[new line]来表示,例如:<br> I [new line] miss [new line] you <br>。因此,它应该显示为:“I miss you”,但实际上它显示为“I [new line] miss [new line] you”。这使得纯文本看起来很痛苦。你知道如何解决吗? - 123iamking
我曾经使用这个方法,但有时会在字符串开头留下“>”。另一种应用正则表达式<[^>]*>的解决方案效果很好。 - Etienne Charland
1
我是这里唯一一个认为正则表达式不应该用于解析结构化语言(如HTML)的人吗?https://dev59.com/unRB5IYBdhLWcg3wiHz7 - Mladen B.
显示剩余2条评论

32

如果您在谈论标签剥离,那么如果您不必担心像 <script> 标签这样的东西,这相对来说是相当简单的。如果你只需要显示文本而没有标签,你可以使用正则表达式来实现:

<[^>]*>

如果你需要担心<script>标签等问题,那么你需要使用比正则表达式更强大的东西来跟踪状态,例如上下文无关文法(CFG),虽然你可能可以用“从左到右”或非贪婪匹配来实现。

如果你可以使用正则表达式,那么有许多网页提供了很好的信息:

如果您需要CFG的更复杂行为,我建议使用第三方工具,不幸的是我不知道推荐一个好的工具。


3
您还需要担心XML中属性值、注释、PI/CDATA以及遗留HTML中的各种常见格式错误。总的来说,[X][HT]ML不适合使用正则表达式进行解析。 - bobince
20
这是一个糟糕的方法。正确的做法是使用一个库来解析HTML,并遍历DOM,只输出白名单中列出的内容。 - usr
3
你所提到的部分是答案中关于CFG的部分。正则表达式可以用于快速简单地去除标签,虽然它有弱点,但它很快也很容易。如果需要更复杂的解析,请使用基于CFG的工具(在你的术语中是生成DOM的库)。我没有进行测试,但我敢打赌,DOM解析比正则表达式去除慢,如果需要考虑性能的话。 - vfilby
1
@vfilby:不!标签剥离是黑名单。举个例子,你忘了什么:你的正则表达式将无法剥离缺少关闭“>”的标签。你考虑过这个吗?我不确定这是否会成为一个问题,但至少证明你错过了这种情况。谁知道你还错过了什么。再举一个例子:你错过了带有javascript src属性的图像。除非安全性不重要,否则永远不要使用黑名单。 - usr
1
正如@bobince所写,HTML不适合使用正则表达式进行解析。这在现实世界的HTML中会出现问题,因为它经常存在格式不正确的情况。 - Judah Gabriel Himango
显示剩余7条评论

21

HTTPUtility.HTMLEncode()方法的作用是将HTML标签编码为字符串。它将为您处理所有繁琐的工作。根据MSDN文档的说明:

如果在HTTP流中传递了空格和标点等字符,则可能会在接收端被错误解释。 HTML编码将不允许在HTML中的字符转换为字符实体等效项; HTML解码反转编码。例如,当嵌入在文本块中时,字符<>被编码为&lt;&gt;以进行HTTP传输。

HTTPUtility.HTMLEncode() 方法的详细信息可以在这里查看。

public static void HtmlEncode(
  string s,
  TextWriter output
)

使用方法:

String TestString = "This is a <Test String>.";
StringWriter writer = new StringWriter();
Server.HtmlEncode(TestString, writer);
String EncodedString = writer.ToString();

非常好的回答,George,谢谢。它还突显了我第一次提问时的不足之处。抱歉。 - Stuart Helwig
HTML Agility Pack已经过时,不支持HTML5。 - Manoochehr Dadashi

11

将HTML转换为纯文本的三个步骤:

第一步,您需要安装Nuget包以获取HtmlAgilityPack 第二步,创建此类:

public class HtmlToText
{
    public HtmlToText()
    {
    }

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

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

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

通过参考Judah Himango的答案,您可以使用上述类。

第三步,您需要创建上述类的对象,并使用ConvertHtml(HTMLContent)方法将HTML转换为纯文本,而不是ConvertToPlainText(string html);

HtmlToText htt=new HtmlToText();
var plainText = htt.ConvertHtml(HTMLContent);

我能跳过将HTML中的链接转换吗?在将其转换为文本时,我需要保留HTML中的链接。 - coder771

7

除了vfilby的回答,你可以在你的代码中进行正则表达式替换;不需要新的类。如果其他像我一样的新手遇到这个问题。

using System.Text.RegularExpressions;

然后...
private string StripHtml(string source)
{
        string output;

        //get rid of HTML tags
        output = Regex.Replace(source, "<[^>]*>", string.Empty);

        //get rid of multiple blank lines
        output = Regex.Replace(output, @"^\s*$\n", string.Empty, RegexOptions.Multiline);

        return output;
}

22
这不好!通过省略闭合尖括号,它可以被欺骗包含脚本。伙计们,永远不要使用黑名单。你不能通过黑名单来净化输入,这是错误的做法。 - usr

7

更新2023年的答案。答案基本上与以往相同:

  1. 安装最新的HtmlAgilityPack

  2. 创建一个名为HtmlUtilities的实用程序类,该类使用HtmlAgilityPack。

  3. 使用它:var plainText = HtmlUtilities.ConvertToPlainText(email.HtmlCode);

以下是从上面链接中复制的HtmlUtilities类:

using HtmlAgilityPack;
using System;
using System.IO;

namespace ReadSharp
{
public class HtmlUtilities
{
/// <summary>
/// Converts HTML to plain text / strips tags.
/// </summary>
/// <param name="html">The HTML.</param>
/// <returns></returns>
public static string ConvertToPlainText(string html)
{
  HtmlDocument doc = new HtmlDocument();
  doc.LoadHtml(html);

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


/// <summary>
/// Count the words.
/// The content has to be converted to plain text before (using ConvertToPlainText).
/// </summary>
/// <param name="plainText">The plain text.</param>
/// <returns></returns>
public static int CountWords(string plainText)
{
  return !String.IsNullOrEmpty(plainText) ? plainText.Split(' ', '\n').Length : 0;
}


public static string Cut(string text, int length)
{
  if (!String.IsNullOrEmpty(text) && text.Length > length)
  {
    text = text.Substring(0, length - 4) + " ...";
  }
  return text;
}


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


private static 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;
        case "br":
          outText.Write("\r\n");
          break;
      }

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

6
它的局限性在于无法折叠长行内空格,但它绝对是可移植的,并且像Web浏览器一样尊重布局。
static string HtmlToPlainText(string html) {
  string buf;
  string block = "address|article|aside|blockquote|canvas|dd|div|dl|dt|" +
    "fieldset|figcaption|figure|footer|form|h\\d|header|hr|li|main|nav|" +
    "noscript|ol|output|p|pre|section|table|tfoot|ul|video";

  string patNestedBlock = $"(\\s*?</?({block})[^>]*?>)+\\s*";
  buf = Regex.Replace(html, patNestedBlock, "\n", RegexOptions.IgnoreCase);

  // Replace br tag to newline.
  buf = Regex.Replace(buf, @"<(br)[^>]*>", "\n", RegexOptions.IgnoreCase);

  // (Optional) remove styles and scripts.
  buf = Regex.Replace(buf, @"<(script|style)[^>]*?>.*?</\1>", "", RegexOptions.Singleline);

  // Remove all tags.
  buf = Regex.Replace(buf, @"<[^>]*(>|$)", "", RegexOptions.Multiline);

  // Replace HTML entities.
  buf = WebUtility.HtmlDecode(buf);
  return buf;
}

@Prof.Falken 我承认,我认为每段代码都有优缺点。它的缺点是稳定性,而优点可能在于简单性(相对于SLOC而言)。您可以使用 XDocument 发布一段代码。 - jeiea
这是最可靠的解决方案,因为它使用HTML标签而不是任何看起来像它的东西。在邮件HTML测试期间,这是绝对完美的解决方案。我将“\n”更改为Environment.NewLine。最后,根据我的需求添加了return buf.Trim();到最终结果。非常好,这应该是最佳答案。 - Tanner Ornelas

5

我认为最简单的方法是创建一个“字符串”扩展方法(基于用户Richard提出的建议):

using System;
using System.Text.RegularExpressions;

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

在您的程序中,只需对任何“字符串”变量使用此扩展方法:

var yourHtmlString = "<div class=\"someclass\"><h2>yourHtmlText</h2></span>";
var yourTextString = yourHtmlString.StripHTML();

我使用这个扩展方法将HTML格式的评论转换为纯文本,以便在Crystal报表上正确显示,它运行得非常完美!


3
我发现最简单的方法是:
HtmlFilter.ConvertToPlainText(html);

HtmlFilter类位于Microsoft.TeamFoundation.WorkItemTracking.Controls.dll中。

该dll可以在以下文件夹中找到: %ProgramFiles%\Common Files\microsoft shared\Team Foundation Server\14.0\

在VS 2015中,该dll还需要引用位于同一文件夹中的Microsoft.TeamFoundation.WorkItemTracking.Common.dll。


它是否处理脚本标签并且格式化为粗体斜体等? - Samra
7
引入一个用于将HTML转换为纯文本的团队基础依赖项,非常值得怀疑... - ViRuSTriNiTy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接