如何在没有多余换行符的情况下从 BeautifulSoup 输出 XML?
Posted
技术标签:
【中文标题】如何在没有多余换行符的情况下从 BeautifulSoup 输出 XML?【英文标题】:How to output XML from BeautifulSoup without extraneous newlines? 【发布时间】:2020-02-09 04:41:31 【问题描述】:我正在使用 Python 和 BeautifulSoup 来解析和访问 XML 文档中的元素。我修改了几个元素的值,然后将 XML 写回到文件中。问题是更新后的 XML 文件在每个 XML 元素的文本值的开头和结尾都包含换行符,导致文件看起来像这样:
<annotation>
<folder>
Definitiva
</folder>
<filename>
armas_229.jpg
</filename>
<path>
/tmp/tmpygedczp5/handgun/images/armas_229.jpg
</path>
<size>
<width>
1800
</width>
<height>
1426
</height>
<depth>
3
</depth>
</size>
<segmented>
0
</segmented>
<object>
<name>
handgun
</name>
<pose>
Unspecified
</pose>
<truncated>
0
</truncated>
<difficult>
0
</difficult>
<bndbox>
<xmin>
1001
</xmin>
<ymin>
549
</ymin>
<xmax>
1453
</xmax>
<ymax>
1147
</ymax>
</bndbox>
</object>
</annotation>
我宁愿让输出文件看起来像这样:
<annotation>
<folder>Definitiva</folder>
<filename>armas_229.jpg</filename>
<path>/tmp/tmpygedczp5/handgun/images/armas_229.jpg</path>
<size>
<width>1800</width>
<height>1426</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>handgun</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>1001</xmin>
<ymin>549</ymin>
<xmax>1453</xmax>
<ymax>1147</ymax>
</bndbox>
</object>
</annotation>
我打开文件并像这样得到“汤”:
with open(pascal_xml_file_path) as pascal_file:
pascal_contents = pascal_file.read()
soup = BeautifulSoup(pascal_contents, "xml")
完成对文档的几个值的修改后,我使用BeautifulSoup.prettify
将文档重写回文件,如下所示:
with open(pascal_xml_file_path, "w") as pascal_file:
pascal_file.write(soup.prettify())
我的假设是BeautifulSoup.prettify
默认情况下会添加这些多余/无用的换行符,并且似乎没有修改这种行为的好方法。我是否错过了 BeautifulSoup 文档中的某些内容,或者我真的无法修改此行为并且需要使用另一种方法将 XML 输出到文件?也许我最好改用xml.etree.ElementTree
重写它?
【问题讨论】:
【参考方案1】:我的假设是 BeautifulSoup.prettify 正在添加这些 默认情况下多余/无偿的换行符,并且没有出现 是修改这种行为的好方法。
是
它在bs4.Tag
类decode
和decode_contents
的两个方法中这样做。
参考:Source file on github
如果你只是需要一个临时修复,你可以猴子补丁这两种方法
这是我的实现
from bs4 import Tag, NavigableString, BeautifulSoup
from bs4.element import AttributeValueWithCharsetSubstitution, EntitySubstitution
def decode(
self, indent_level=None,
eventual_encoding="utf-8", formatter="minimal"
):
if not callable(formatter):
formatter = self._formatter_for_name(formatter)
attrs = []
if self.attrs:
for key, val in sorted(self.attrs.items()):
if val is None:
decoded = key
else:
if isinstance(val, list) or isinstance(val, tuple):
val = ' '.join(val)
elif not isinstance(val, str):
val = str(val)
elif (
isinstance(val, AttributeValueWithCharsetSubstitution)
and eventual_encoding is not None
):
val = val.encode(eventual_encoding)
text = self.format_string(val, formatter)
decoded = (
str(key) + '='
+ EntitySubstitution.quoted_attribute_value(text))
attrs.append(decoded)
close = ''
closeTag = ''
prefix = ''
if self.prefix:
prefix = self.prefix + ":"
if self.is_empty_element:
close = '/'
else:
closeTag = '</%s%s>' % (prefix, self.name)
pretty_print = self._should_pretty_print(indent_level)
space = ''
indent_space = ''
if indent_level is not None:
indent_space = (' ' * (indent_level - 1))
if pretty_print:
space = indent_space
indent_contents = indent_level + 1
else:
indent_contents = None
contents = self.decode_contents(
indent_contents, eventual_encoding, formatter)
if self.hidden:
# This is the 'document root' object.
s = contents
else:
s = []
attribute_string = ''
if attrs:
attribute_string = ' ' + ' '.join(attrs)
if indent_level is not None:
# Even if this particular tag is not pretty-printed,
# we should indent up to the start of the tag.
s.append(indent_space)
s.append('<%s%s%s%s>' % (
prefix, self.name, attribute_string, close))
has_tag_child = False
if pretty_print:
for item in self.children:
if isinstance(item, Tag):
has_tag_child = True
break
if has_tag_child:
s.append("\n")
s.append(contents)
if not has_tag_child:
s[-1] = s[-1].strip()
if pretty_print and contents and contents[-1] != "\n":
s.append("")
if pretty_print and closeTag:
if has_tag_child:
s.append(space)
s.append(closeTag)
if indent_level is not None and closeTag and self.next_sibling:
# Even if this particular tag is not pretty-printed,
# we're now done with the tag, and we should add a
# newline if appropriate.
s.append("\n")
s = ''.join(s)
return s
def decode_contents(
self,
indent_level=None,
eventual_encoding="utf-8",
formatter="minimal"
):
# First off, turn a string formatter into a function. This
# will stop the lookup from happening over and over again.
if not callable(formatter):
formatter = self._formatter_for_name(formatter)
pretty_print = (indent_level is not None)
s = []
for c in self:
text = None
if isinstance(c, NavigableString):
text = c.output_ready(formatter)
elif isinstance(c, Tag):
s.append(
c.decode(indent_level, eventual_encoding, formatter)
)
if text and indent_level and not self.name == 'pre':
text = text.strip()
if text:
if pretty_print and not self.name == 'pre':
s.append(" " * (indent_level - 1))
s.append(text)
if pretty_print and not self.name == 'pre':
s.append("")
return ''.join(s)
Tag.decode = decode
Tag.decode_contents= decode_contents
在这之后,当我做print(soup.prettify)
时,输出是
<annotation>
<folder>Definitiva</folder>
<filename>armas_229.jpg</filename>
<path>/tmp/tmpygedczp5/handgun/images/armas_229.jpg</path>
<size>
<width>1800</width>
<height>1426</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>handgun</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>1001</xmin>
<ymin>549</ymin>
<xmax>1453</xmax>
<ymax>1147</ymax>
</bndbox>
</object>
</annotation>
我在做这个的时候做了很多假设。只是想证明这是可能的。
【讨论】:
非常好的工作,感谢@Bitto Bennichan。我可能会使用 ElementTree 重写我的代码,因为我希望不会像这样“脱离预订”,而且我认为 BeautifulSoup 并没有真正给我带来 ElementTree 不提供的东西(我使用 BS,因为它被用于我为这个任务而关注的教程)。不错的技巧,赞!【参考方案2】:考虑XSLT 与Python 的第三方模块lxml
(您可能已经拥有BeautifulSoup
集成)。具体来说,调用identity transform照原样复制XML,然后在所有文本节点上运行normalize-space()
模板。
XSLT (另存为 .xsl、特殊的 .xml 文件或嵌入字符串)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- IDENTITY TRANSFORM -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- RUN normalize-space() ON ALL TEXT NODES -->
<xsl:template match="text()">
<xsl:copy-of select="normalize-space()"/>
</xsl:template>
</xsl:stylesheet>
Python
import lxml.etree as et
# LOAD FROM STRING OR PARSE FROM FILE
str_xml = '''...'''
str_xsl = '''...'''
doc = et.fromstring(str_xml)
style = et.fromstring(str_xsl)
# INITIALIZE TRANSFORMER AND RUN
transformer = et.XSLT(style)
result = transformer(doc)
# PRINT TO SCREEN
print(result)
# SAVE TO DISK
with open('Output.xml', 'wb') as f:
f.write(result)
Rextester demo
【讨论】:
谢谢@Parfait,这很有帮助。我希望有一些开箱即用的东西,而不是采用这样的方法,因为这似乎与重写我的代码以使用 ElementTree 而不是 BeautifulSoup 一样多。这假设 ElementTree 的输出将被合理简洁地格式化,不同于默认的 BeautifulSoup 输出,我想只有一种方法可以找出答案(即停止懒惰并使用 ElementTree 进行重写)。无论如何,再次感谢您的帮助。 嗯...在 .xml 旁边保存一个单独的 .xsl(或作为演示显示的字符串嵌入)工作量太大?在 Python 中没有任何循环或重写树?不过明白了。我不知道您的整个过程只是为了回答标题问题。我只使用lxml
来满足我所有的 XML 需求(甚至是 html),它是一个完全兼容的 XPath 1.0 和 XSLT 1.0 库。祝你好运!也许这可以帮助未来的读者。
事实证明,使用 ElementTree 重写我的代码很简单,并且当我使用 ElementTree.write(file_path)
时,XML 格式看起来符合预期/期望。无论如何,这对我来说似乎是最简单/最简单的......
很高兴听到!很高兴它成功了。随意回答自己的问题。希望它不涉及循环。
经过多一点测试,结果发现我错了,事情并没有这么简单。在我重写 XML 之前,我需要使用 strip() 使用文本值更新所有元素,以删除无关的空格和换行符。尽管如此 xml.etree.ElementTree 似乎更容易使用,所以我开始使用它而不是 BeautifulSoup 来做这类事情。【参考方案3】:
如果我改用 xml.etree.ElementTree
而不是 BeautifulSoup,那么得到我想要的缩进是很直接的。例如,下面是一些读取 XML 文件的代码,清除文本元素中的任何换行符/空格,然后将树写入 XML 文件。
import argparse
from xml.etree import ElementTree
# ------------------------------------------------------------------------------
def reformat(
input_xml: str,
output_xml: str,
):
tree = ElementTree.parse(input_xml)
# remove extraneous newlines and whitespace from text elements
for element in tree.getiterator():
if element.text:
element.text = element.text.strip()
# write the updated XML into the annotations output directory
tree.write(output_xml)
# ------------------------------------------------------------------------------
if __name__ == "__main__":
# parse the command line arguments
args_parser = argparse.ArgumentParser()
args_parser.add_argument(
"--in",
required=True,
type=str,
help="file path of original XML",
)
args_parser.add_argument(
"--out",
required=True,
type=str,
help="file path of reformatted XML",
)
args = vars(args_parser.parse_args())
reformat(
args["in"],
args["out"],
)
【讨论】:
【参考方案4】:我写了一个代码来做美化,没有任何额外的库。
美化逻辑
# Recursive function (do not call this method)
def _get_prettified(tag, curr_indent, indent):
out = ''
for x in tag.find_all(recursive=False):
if len(x.find_all()) == 0:
content = x.string.strip(' \n')
else:
content = '\n' + _get_prettified(x, curr_indent + ' ' * indent, indent) + curr_indent
attrs = ' '.join([f'k="v"' for k,v in x.attrs.items()])
out += curr_indent + ('<%s %s>' % (x.name, attrs) if len(attrs) > 0 else '<%s>' % x.name) + content + '</%s>\n' % x.name
return out
# Call this method
def get_prettified(tag, indent):
return _get_prettified(tag, '', indent);
您的意见
source = """<annotation>
<folder>
Definitiva
</folder>
<filename>
armas_229.jpg
</filename>
<path>
/tmp/tmpygedczp5/handgun/images/armas_229.jpg
</path>
<size>
<width>
1800
</width>
<height>
1426
</height>
<depth>
3
</depth>
</size>
<segmented>
0
</segmented>
<object>
<name>
handgun
</name>
<pose>
Unspecified
</pose>
<truncated>
0
</truncated>
<difficult>
0
</difficult>
<bndbox>
<xmin>
1001
</xmin>
<ymin>
549
</ymin>
<xmax>
1453
</xmax>
<ymax>
1147
</ymax>
</bndbox>
</object>
</annotation>"""
输出
bs = BeautifulSoup(source, 'html.parser')
output = get_prettified(bs, indent=2)
print(output)
# Prints following
<annotation>
<folder>Definitiva</folder>
<filename>armas_229.jpg</filename>
<path>/tmp/tmpygedczp5/handgun/images/armas_229.jpg</path>
<size>
<width>1800</width>
<height>1426</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>handgun</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>1001</xmin>
<ymin>549</ymin>
<xmax>1453</xmax>
<ymax>1147</ymax>
</bndbox>
</object>
</annotation>
在此处运行您的代码:https://replit.com/@bikcrum/BeautifulSoup-Prettifier
【讨论】:
我尝试了您的代码,它在 html 解析器bs = BeautifulSoup(source, 'html.parser')
上运行良好,但将所有标签都转换为小写。当我使用bs = BeautifulSoup(source, 'xml')
时,它给出了正确的大小写标签,但在行之间产生了间隙。以上是关于如何在没有多余换行符的情况下从 BeautifulSoup 输出 XML?的主要内容,如果未能解决你的问题,请参考以下文章
如何在没有开发人员帐户的情况下从 .app 文件构建 .ipa 文件?
如何在没有 \n 的情况下从 txt 中获取特定行(Python)