如何将HTML格式化输出到文件,并进行缩进。

107

我正在使用lxml.html生成一些HTML。我想要将最终结果漂亮地打印成带缩进的html文件。我该如何做到这一点?

这是我尝试过并已经得到的内容

import lxml.html as lh
from lxml.html import builder as E
sliderRoot=lh.Element("div", E.CLASS("scroll"), style="overflow-x: hidden; overflow-y: hidden;")
scrollContainer=lh.Element("div", E.CLASS("scrollContainer"), style="width: 4340px;")
sliderRoot.append(scrollContainer)
print lh.tostring(sliderRoot, pretty_print = True, method="html")

如您所见,我正在使用 pretty_print=True 属性。我认为这将提供有缩进的代码,但实际上并没有帮助。这是输出结果:

<div style="overflow-x: hidden; overflow-y: hidden;" class="scroll"><div style="width: 4340px;" class="scrollContainer"></div></div>

11个回答

142

我最终直接使用了BeautifulSoup。这是lxml.html.soupparser用于解析HTML的内容。

BeautifulSoup有一个prettify方法,它能够实现它所说的功能。它可以以适当的缩进和其他格式使HTML变得更加美观。

BeautifulSoup不能修复HTML错误的代码,因此无效的代码仍然无效。但在这种情况下,由于代码是由lxml生成的,HTML代码应该至少是语义上正确的。

在我的问题中给出的示例中,我必须这样做:

from bs4 import BeautifulSoup as bs
root = lh.tostring(sliderRoot) #convert the generated HTML to a string
soup = bs(root)                #make BeautifulSoup
prettyHTML = soup.prettify()   #prettify the html

3
谢谢,但值得一提的是,如果对某人很重要,嵌入到HTML中的js将不会被美化。 - Vitaly Zdanevich
16
使用版本4,将第一行更改为from bs4 import BeautifulSoup as bs - shao.lo
如果您只想从字符串中美化HTML,请参见下面的AlexG的答案。 - ErikusMaximus
10
当使用prettify()时要小心,因为它会改变文档的语义:"由于它添加了空白字符(以换行符的形式),prettify()会改变HTML文档的含义,不应该用于重新格式化文档。prettify()的目标是帮助你直观地了解所处理文档的结构。" - BallpointBen
另一个警告:使用版本4,BeautifulSoup将解码HTML实体,因此,如果您正在解码用户发布的内容(例如:论坛帖子)中的字符串,则它将很高兴地将转义的HTML反转回来,从而使您面临潜在问题。 - Ralfp

51
尽管我的答案现在可能没有帮助,但我会将它放在这里,以便未来其他人可以参考。
实际上,lxml.html.tostring()不会漂亮地打印所提供的HTML,即使使用了pretty_print=True也不行。
然而,lxml.html的“同级” - lxml.etree可以正常工作。
因此,一个人可以按以下方式使用它:
from lxml import etree, html

document_root = html.fromstring("<html><body><h1>hello world</h1></body></html>")
print(etree.tostring(document_root, encoding='unicode', pretty_print=True))

输出结果如下:

<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>

2
“pretty_print”标志仅在使用“method ='xml'”调用“etree.tostring”时起作用,这是默认设置。因此,我们正在处理XHTML。 - lenz
9
这是一个很好的答案,因为它没有使用任何外部依赖。然而,如果包含HTML的字符串有回车符,那么在Python 2.7.10上,'etree.tostring'将不会对其进行美化处理,并且返回其未更改的输入... 一旦你知道了这一点,替换回车符就是一件简单的事情,但如果你不知道这个问题,你将浪费很多时间。 - Tom Swirly
这很棒,因为它只提供选项卡的解决方案。 这不像BeautifulSoup的解决方案那样改变HTML的其他方面。 - earthmeLon
1
不行!原因是 etree.tostring 会将 "<i></i>" 缩短为 "<i/>",这是不允许的。 - shrewmouse

28

如果您将 HTML 存储为未格式化的字符串,放入变量html_string中,则可以使用 beautifulsoup4 来执行以下操作:

如果需要翻译其他内容,请提供更多的上下文和详细信息。谢谢!

from bs4 import BeautifulSoup
print(BeautifulSoup(html_string, 'html.parser').prettify())

1
我刚刚尝试了这种方法来重新格式化旧版HTML,结果在垂直间距方面视觉上有所不同。并不是说原始的HTML语法一开始就是正确的,但请注意,这并不能保证相同的视觉输出。 - Arnaud P

7
如果添加一个新的依赖不是问题的话,您可以使用html5print包。与其他解决方案相比,它的优点在于它还可以美化嵌入在HTML文档中的CSS和Javascript代码。
要安装它,请执行:
pip install html5print

然后,你可以将其作为命令使用:
html5-print ugly.html -o pretty.html

或者作为Python代码:
from html5print import HTMLBeautifier
html = '<title>Page Title</title><p>Some text here</p>'
print(HTMLBeautifier.beautify(html, 4))

2
这将安装其他几个依赖项,包括beautifulsoup4。 - byteface

5

我尝试了BeautifulSoup的prettify和html5print的HTMLBeautifier解决方案,但由于我正在使用yattag生成HTML,因此更适合使用其indent函数,该函数可以产生漂亮的缩进输出。

from yattag import indent

rawhtml = "String with some HTML code..."

result = indent(
    rawhtml,
    indentation = '    ',
    newline = '\r\n',
    indent_text = True
)

print(result)

4

在底层,lxml 使用 libxml2 将树形结构序列化为字符串。以下是相关代码片段,该片段决定是否在关闭标签后添加换行符:

    xmlOutputBufferWriteString(buf, ">");
    if ((format) && (!info->isinline) && (cur->next != NULL)) {
        if ((cur->next->type != HTML_TEXT_NODE) &&
            (cur->next->type != HTML_ENTITY_REF_NODE) &&
            (cur->parent != NULL) &&
            (cur->parent->name != NULL) &&
            (cur->parent->name[0] != 'p')) /* p, pre, param */
            xmlOutputBufferWriteString(buf, "\n");
    }
    return;

所以如果一个节点是元素,不是内联标签,并且后面跟着一个同级节点(cur->next != NULL),而且不是p, pre, param之一,则会输出一个换行符。

3

你可以使用管道将其导入HTML Tidy中,无论是从shell还是通过os.system()


我最初考虑使用HTML Tidy,但我的代码有些古怪,tidy会对其造成严重影响。决定改用BeautifulSoup,效果非常好。 - bcosynot
HTML Tidy 可以纠正你的 HTML,这可以避免破坏性问题。如果你忘记了 HTML Tidy 正在处理结果,这些错误很难发现(我知道我在说什么...)。 - mzuther
1
比这里2011年的评论更近的是,可以看看这个2018年的问题的答案:https://stackoverflow.com/questions/50380799/cant-figure-out-how-to-invoke-html5tidy-from-python-3。"那个库已经损坏和/或不适用于Python 3.5。"可能会为某些人节省一些时间... - RBV

2

这不是我写的代码,我从其他地方找到它。

def indent(elem, level=0):
    i = '\n' + level * '  '
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + '  '
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

我使用它与以下内容配合:

indent(page)
tostring(page)

如果这不是你的代码,请引用它的来源。什么是 tostring() - ggorlen

2

如果您不关心奇怪的HTML格式(比如必须支持使用Netscape 2.0的大量客户端,所以必须使用<br>而不是<br />),您可以随时将方法更改为“xml”,这似乎可以解决问题。这可能是lxml或libxml中的一个bug,但我找不到原因。


1
当您将方法设置为xml时,如果标签没有任何子元素,则不会生成闭合标签。例如,在所讨论的示例中,内部div将没有闭合标签。我真的不知道为什么。最终我使用BeautifulSoup来获得正确的输出。 - bcosynot

0

这并不是很精细和健壮的代码,但它可以平衡示例 HTML 字符串,而不使用任何非标准库。

import re
html = """
<A value="X"><B value="X">
<A value="X"><B value="X">
some random text
</B></A><C />
some random text
</B></A><C />
"""
rx_al = r"(<[\/A-Za-z]+[^>]*>)"
rx_op = r"<([A-Za-z]+)[^\/]+>"
rx_cl = r"</([A-Za-z]+)>"
# self-closing not needed
#rx_sc = r"<([A-Za-z]+).* \/>"
matches = re.findall(rx_al, html, flags=re.M)

def lookup_key(match, indent):
  return f"{match[0]}:{indent}"

def balance(nodes):
  builder = []
  indent = 0
  lookup = {}
  for node in nodes:
    is_open = re.match(rx_op, node)
    is_close = re.match(rx_cl, node)
    padl = " " * indent
    if is_open:
      k = lookup_key(is_open, indent)
      lookup[k] = node
      indent += 2
    elif is_close:
      for i in range(0, indent, -2):
        if lookup.pop(lookup_key(is_close, i), None):
          break
      indent -= 2
      padl = padl[:-2]
    builder.append(padl + node)
  return "\n".join(builder)

print(balance(matches))

将会产生:

<A value="X">
  <B value="X">
    <A value="X">
      <B value="X">
      </B>
    </A>
    <C />
  </B>
</A>
<C />


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