如何美化HTML,使标签属性保持在一行中?

10
我得到了这段小代码:
text = """<html><head></head><body>
    <h1 style="
    text-align: center;
">Main site</h1>
    <div>
        <p style="
    color: blue;
    text-align: center;
">text1
        </p>
        <p style="
    color: blueviolet;
    text-align: center;
">text2
        </p>
    </div>
    <div>
        <p style="text-align:center">
            <img src="./foo/test.jpg" alt="Testing static images" style="
">
        </p>
    </div>
</body></html>
"""

import sys
import re
import bs4


def prettify(soup, indent_width=4):
    r = re.compile(r'^(\s*)', re.MULTILINE)
    return r.sub(r'\1' * indent_width, soup.prettify())

soup = bs4.BeautifulSoup(text, "html.parser")
print(prettify(soup))

上面代码片段的输出结果是:
<html>
    <head>
    </head>
    <body>
        <h1 style="
                text-align: center;
">
            Main site
        </h1>
        <div>
            <p style="
                color: blue;
                text-align: center;
">
                text1
            </p>
            <p style="
                color: blueviolet;
                text-align: center;
">
                text2
            </p>
        </div>
        <div>
            <p style="text-align:center">
                <img alt="Testing static images" src="./foo/test.jpg" style="
"/>
            </p>
        </div>
    </body>
</html>

我想知道如何格式化输出,使其变成这样:

<html>
    <head>
    </head>
    <body>
        <h1 style="text-align: center;">
            Main site
        </h1>
        <div>
            <p style="color: blue;text-align: center;">
                text1
            </p>
            <p style="color: blueviolet;text-align: center;">
                text2
            </p>
        </div>
        <div>
            <p style="text-align:center">
                <img alt="Testing static images" src="./foo/test.jpg" style=""/>
            </p>
        </div>
    </body>
</html>

换句话说,我希望尽可能将html语句(例如<tag attrib1=value1 attrib2=value2 ... attribn=valuen>)保持在一行中。当我说“如果可能”的时候,我的意思是不破坏属性本身的值(value1,value2,...,valuen)。使用beautifulsoup4可以实现这一点吗?据我所读的文档,似乎可以使用自定义formatter,但我不知道如何编写自定义格式化程序以满足所描述的要求。
编辑:
@alecxe的解决方案非常简单,但不幸的是,在某些更复杂的情况下会失败,例如下面的情况:
test1 = """
<div id="dialer-capmaign-console" class="fill-vertically" style="flex: 1 1 auto;">
    <div id="sessionsGrid" data-columns="[
        { field: 'dialerSession.startTime', format:'{0:G}', title:'Start time', width:122 },
        { field: 'dialerSession.endTime', format:'{0:G}', title:'End time', width:122, attributes: {class:'tooltip-column'}},
        { field: 'conversationStartTime', template: cty.ui.gct.duration_dialerSession_conversationStartTime_endTime, title:'Duration', width:80},
        { field: 'dialerSession.caller.lastName',template: cty.ui.gct.person_dialerSession_caller_link, title:'Caller', width:160 },
        { field: 'noteType',template:cty.ui.gct.nameDescription_noteType, title:'Note type', width:150, attributes: {class:'tooltip-column'}},
        { field: 'note', title:'Note'}
        ]">
</div>
</div>
"""

from bs4 import BeautifulSoup
import re


def prettify(soup, indent_width=4, single_lines=True):
    if single_lines:
        for tag in soup():
            for attr in tag.attrs:
                print(tag.attrs[attr], tag.attrs[attr].__class__)
                tag.attrs[attr] = " ".join(
                    tag.attrs[attr].replace("\n", " ").split())

    r = re.compile(r'^(\s*)', re.MULTILINE)
    return r.sub(r'\1' * indent_width, soup.prettify())


def html_beautify(text):
    soup = BeautifulSoup(text, "html.parser")
    return prettify(soup)

print(html_beautify(test1))

TRACEBACK:

dialer-capmaign-console <class 'str'>
['fill-vertically'] <class 'list'>
Traceback (most recent call last):
  File "d:\mcve\x.py", line 35, in <module>
    print(html_beautify(test1))
  File "d:\mcve\x.py", line 33, in html_beautify
    return prettify(soup)
  File "d:\mcve\x.py", line 25, in prettify
    tag.attrs[attr].replace("\n", " ").split())
AttributeError: 'list' object has no attribute 'replace'
2个回答

10

BeautifulSoup试图保留输入HTML中属性值中的换行符和多个空格。

这里的一个解决方法是在美化之前迭代元素属性并清理它们 - 删除换行符并将多个连续空格替换为单个空格:

for tag in soup():
    for attr in tag.attrs:
        tag.attrs[attr] = " ".join(tag.attrs[attr].replace("\n", " ").split())

print(soup.prettify())

输出:

<html>
 <head>
 </head>
 <body>
  <h1 style="text-align: center;">
   Main site
  </h1>
  <div>
   <p style="color: blue; text-align: center;">
    text1
   </p>
   <p style="color: blueviolet; text-align: center;">
    text2
   </p>
  </div>
  <div>
   <p style="text-align:center">
    <img alt="Testing static images" src="./foo/test.jpg" style=""/>
   </p>
  </div>
 </body>
</html>

更新(针对像class这样的多值属性):

您只需要添加一个小修改,特殊处理当属性为list类型的情况:

for tag in soup():
    tag.attrs = {
        attr: [" ".join(attr_value.replace("\n", " ").split()) for attr_value in value] 
              if isinstance(value, list)
              else " ".join(value.replace("\n", " ").split())
        for attr, value in tag.attrs.items()
    }

1
在这里接受和提供赏金的原因如下:1)问题涉及bs4,而这个问题符合要求;2)来自其他人的更多赞同和第一个回答;3)@carlo chen的答案不能直接使用,即tidylib不是一个自包含的软件包,需要一些外部dll文件。 - BPL

4

虽然BeautifulSoup更常用,但如果您正在处理怪异的情况并有更具体的要求,则HTML Tidy可能是更好的选择。

安装Python库(pip install pytidylib)后,请尝试以下代码:

from tidylib import Tidy
tidy = Tidy()
# assign string to text
config = {
    "doctype": "omit",
    # "show-body-only": True
}
print tidy.tidy_document(text, options=config)[0]

tidy.tidy_document 返回一个元组,其中包含 HTML 和可能发生的任何错误。此代码将输出

<html>
  <head>
    <title></title>
  </head>
  <body>
    <h1 style="text-align: center;">
      Main site
    </h1>
    <div>
      <p style="color: blue; text-align: center;">
        text1
      </p>
      <p style="color: blueviolet; text-align: center;">
        text2
      </p>
    </div>
    <div>
      <p style="text-align:center">
        <img src="./foo/test.jpg" alt="Testing static images" style="">
      </p>
    </div>
  </body>
</html>

通过取消第二个示例中的"show-body-only": True的注释。

<div id="dialer-capmaign-console" class="fill-vertically" style="flex: 1 1 auto;">
  <div id="sessionsGrid" data-columns="[ { field: 'dialerSession.startTime', format:'{0:G}', title:'Start time', width:122 }, { field: 'dialerSession.endTime', format:'{0:G}', title:'End time', width:122, attributes: {class:'tooltip-column'}}, { field: 'conversationStartTime', template: cty.ui.gct.duration_dialerSession_conversationStartTime_endTime, title:'Duration', width:80}, { field: 'dialerSession.caller.lastName',template: cty.ui.gct.person_dialerSession_caller_link, title:'Caller', width:160 }, { field: 'noteType',template:cty.ui.gct.nameDescription_noteType, title:'Note type', width:150, attributes: {class:'tooltip-column'}}, { field: 'note', title:'Note'} ]"></div>
</div>

请参阅更多配置以获取进一步的选项和自定义。有一些特定于属性的包装选项可能会有所帮助。正如您所见,空元素只占据一行,并且html-tidy将自动尝试添加诸如DOCTYPEheadtitle标签等内容。

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