Impressão bonita de XML em Python

424

Qual é a melhor maneira (ou são as várias) de imprimir bastante XML em Python?

Hortitude
fonte

Respostas:

379
import xml.dom.minidom

dom = xml.dom.minidom.parse(xml_fname) # or xml.dom.minidom.parseString(xml_string)
pretty_xml_as_string = dom.toprettyxml()
Ben Noland
fonte
35
Isso fará com que você fique bastante xml, mas observe que o que sai no nó de texto é realmente diferente do que entrou - existem novos espaços em branco nos nós de texto. Isso pode causar problemas se você estiver esperando EXATAMENTE o que foi alimentado.
Todd Hopkinson
49
@icnivad: embora seja importante salientar esse fato, parece-me estranho que alguém queira prettify seu XML se os espaços forem de alguma importância para eles!
vaab
18
Agradável! Pode recolher isso para um único liner: python -c 'import sys; import xml.dom.minidom; s = sys.stdin.read (); print xml.dom.minidom.parseString (s) .toprettyxml ()'
Anton I. Sipos
11
O minidom é amplamente considerado uma implementação xml muito ruim. Se você permitir adicionar dependências externas, o lxml é muito superior.
bukzor
26
Não é um fã de redefinir o xml de ser um módulo para o objeto de saída, mas o método funciona de outra maneira. Eu adoraria encontrar uma maneira mais agradável de passar do estudo básico à impressão bonita. Enquanto o lxml é legal, há momentos em que eu preferiria manter o núcleo, se eu puder.
Danny Staple
162

O lxml é recente, atualizado e inclui uma bonita função de impressão

import lxml.etree as etree

x = etree.parse("filename")
print etree.tostring(x, pretty_print=True)

Confira o tutorial do lxml: http://lxml.de/tutorial.html

1729
fonte
11
A única desvantagem do lxml é a dependência de bibliotecas externas. Acho que isso não é tão ruim no Windows que as bibliotecas são empacotadas com o módulo. No Linux, eles estão aptitude installfora. No OS / X, não tenho certeza.
intuited
4
No OS X, você só precisa de um gcc e easy_install / pip em funcionamento.
pkoch 8/02
11
A impressora bonita lxml não é confiável e não imprime seu XML adequadamente em muitos casos, explicados nas Perguntas frequentes sobre lxml . Eu parei de usar lxml para imprimir bastante depois de vários casos de canto que simplesmente não funcionam (ou seja, isso não será corrigido: Bug # 910018 ). Todo esse problema está relacionado ao uso de valores XML que contêm espaços que devem ser preservados.
vaab
1
O lxml também faz parte do MacPorts, funciona sem problemas para mim.
Jens
14
Uma vez que em Python 3 você geralmente deseja trabalhar com str (= string unicode em Python 2), melhor usar este: print(etree.tostring(x, pretty_print=True, encoding="unicode")). A gravação em um arquivo de saída é possível em apenas uma linha, não sendo necessária nenhuma variável intermediária:etree.parse("filename").write("outputfile", encoding="utf-8")
Thor
109

Outra solução é pegar emprestada essa indentfunção , para uso com a biblioteca ElementTree incorporada ao Python desde a versão 2.5. Aqui está o que isso seria:

from xml.etree import ElementTree

def indent(elem, level=0):
    i = "\n" + level*"  "
    j = "\n" + (level-1)*"  "
    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 subelem in elem:
            indent(subelem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = j
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = j
    return elem        

root = ElementTree.parse('/tmp/xmlfile').getroot()
indent(root)
ElementTree.dump(root)
ade
fonte
... e então use lxml tostring!
Stefano
2
Observe que você ainda pode tree.write([filename])escrever para o arquivo ( treesendo a instância do ElementTree).
Bouke
16
Este link effbot.org/zone/element-lib.htm#prettyprint possui o código correto. O código aqui tem algo errado. Precisa ser editado.
Aylwyn Lake
Não, você não pode, pois elementtree.getroot () não possui esse método, apenas um objeto elementtree possui. @bouke
shinzou
1
Veja como você pode gravar em um arquivo:tree = ElementTree.parse('file) ; root = tree.getroot() ; indent(root); tree.write('Out.xml');
e-malito
47

Aqui está minha solução (hacky?) Para solucionar o problema feio do nó de texto.

uglyXml = doc.toprettyxml(indent='  ')

text_re = re.compile('>\n\s+([^<>\s].*?)\n\s+</', re.DOTALL)    
prettyXml = text_re.sub('>\g<1></', uglyXml)

print prettyXml

O código acima produzirá:

<?xml version="1.0" ?>
<issues>
  <issue>
    <id>1</id>
    <title>Add Visual Studio 2005 and 2008 solution files</title>
    <details>We need Visual Studio 2005/2008 project files for Windows.</details>
  </issue>
</issues>

Em vez disso:

<?xml version="1.0" ?>
<issues>
  <issue>
    <id>
      1
    </id>
    <title>
      Add Visual Studio 2005 and 2008 solution files
    </title>
    <details>
      We need Visual Studio 2005/2008 project files for Windows.
    </details>
  </issue>
</issues>

Isenção de responsabilidade: Provavelmente existem algumas limitações.

Nick Bolton
fonte
Obrigado! Esta foi a minha única reclamação com todos os métodos de impressão bonitos. Funciona bem com os poucos arquivos que tentei.
iano
Eu encontrei uma solução muito 'quase idênticos', mas o seu é mais direto, usando re.compileantes da suboperação (eu estava usando re.findall()duas vezes, zipe um forlaço com str.replace()...)
heltonbiker
3
Isso não é mais necessário no Python 2.7: o toprettyxml () do xml.dom.minidom agora produz uma saída como '<id> 1 </id>' por padrão, para nós que possuem exatamente um nó filho de texto.
Marius Gedminas
Sou obrigado a usar o Python 2.6. Portanto, esse truque de reformatação de regex é muito útil. Trabalhou como está sem problemas.
Mike Finch
@ Marius Gedminas Estou executando o 2.7.2 e o "padrão" definitivamente não é o que você diz.
Posfan12 04/07/19
23

Como outros apontaram, o lxml possui uma bonita impressora embutida.

Esteja ciente de que, por padrão, ele altera as seções CDATA para texto normal, o que pode ter resultados desagradáveis.

Aqui está uma função Python que preserva o arquivo de entrada e altera apenas o recuo (observe o strip_cdata=False). Além disso, garante que a saída use UTF-8 como codificação em vez do ASCII padrão (observe o encoding='utf-8'):

from lxml import etree

def prettyPrintXml(xmlFilePathToPrettyPrint):
    assert xmlFilePathToPrettyPrint is not None
    parser = etree.XMLParser(resolve_entities=False, strip_cdata=False)
    document = etree.parse(xmlFilePathToPrettyPrint, parser)
    document.write(xmlFilePathToPrettyPrint, pretty_print=True, encoding='utf-8')

Exemplo de uso:

prettyPrintXml('some_folder/some_file.xml')
roskakori
fonte
1
Está um pouco tarde agora. Mas acho que o lxml corrigiu o CDATA? CDATA é CDATA do meu lado.
Elwc
Obrigado, esta é a melhor resposta até agora.
George Chalhoub
20

BeautifulSoup tem um prettify()método fácil de usar .

Recua um espaço por nível de recuo. Funciona muito melhor que o pretty_print do lxml e é curto e agradável.

from bs4 import BeautifulSoup

bs = BeautifulSoup(open(xml_file), 'xml')
print bs.prettify()
ChaimG
fonte
1
Recebendo esta mensagem de erro:bs4.FeatureNotFound: Couldn't find a tree builder with the features you requested: xml. Do you need to install a parser library?
hadoop
12

Se você tiver, xmllintpode gerar um subprocesso e usá-lo. xmllint --format <file>imprime bastante seu XML de entrada na saída padrão.

Observe que esse método usa um programa externo ao python, o que o torna uma espécie de hack.

def pretty_print_xml(xml):
    proc = subprocess.Popen(
        ['xmllint', '--format', '/dev/stdin'],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
    )
    (output, error_output) = proc.communicate(xml);
    return output

print(pretty_print_xml(data))
Russell Silva
fonte
11

Tentei editar a resposta de "ade" acima, mas o Stack Overflow não me permitiu editar depois que eu havia inicialmente fornecido o feedback anonimamente. Esta é uma versão menos complicada da função para imprimir um ElementTree com bastante facilidade.

def indent(elem, level=0, more_sibs=False):
    i = "\n"
    if level:
        i += (level-1) * '  '
    num_kids = len(elem)
    if num_kids:
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
            if level:
                elem.text += '  '
        count = 0
        for kid in elem:
            indent(kid, level+1, count < num_kids - 1)
            count += 1
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
            if more_sibs:
                elem.tail += '  '
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i
            if more_sibs:
                elem.tail += '  '
Joshua Richardson
fonte
8

Se você estiver usando uma implementação DOM, cada um terá sua própria forma de impressão bonita embutida:

# minidom
#
document.toprettyxml()

# 4DOM
#
xml.dom.ext.PrettyPrint(document, stream)

# pxdom (or other DOM Level 3 LS-compliant imp)
#
serializer.domConfig.setParameter('format-pretty-print', True)
serializer.writeToString(document)

Se você estiver usando outra coisa sem a sua própria impressora bonita - ou essas impressoras bonitas não fizerem exatamente da maneira que você deseja - você provavelmente terá que escrever ou subclassificar seu próprio serializador.

bobince
fonte
6

Eu tive alguns problemas com as bonitas impressões do minidom. Eu recebia um UnicodeError sempre que tentava imprimir um documento com caracteres fora da codificação especificada, por exemplo, se eu tivesse um β em um documento e tentasse doc.toprettyxml(encoding='latin-1'). Aqui está minha solução alternativa para isso:

def toprettyxml(doc, encoding):
    """Return a pretty-printed XML document in a given encoding."""
    unistr = doc.toprettyxml().replace(u'<?xml version="1.0" ?>',
                          u'<?xml version="1.0" encoding="%s"?>' % encoding)
    return unistr.encode(encoding, 'xmlcharrefreplace')
giltay
fonte
5
from yattag import indent

pretty_string = indent(ugly_string)

Ele não adicionará espaços ou novas linhas dentro de nós de texto, a menos que você solicite com:

indent(mystring, indent_text = True)

Você pode especificar como deve ser a unidade de indentação e como deve ser a nova linha.

pretty_xml_string = indent(
    ugly_xml_string,
    indentation = '    ',
    newline = '\r\n'
)

O documento está na página inicial http://www.yattag.org .

John Smith Opcional
fonte
4

Eu escrevi uma solução para percorrer um ElementTree existente e usar texto / cauda para identá-lo como normalmente se espera.

def prettify(element, indent='  '):
    queue = [(0, element)]  # (level, element)
    while queue:
        level, element = queue.pop(0)
        children = [(level + 1, child) for child in list(element)]
        if children:
            element.text = '\n' + indent * (level+1)  # for child open
        if queue:
            element.tail = '\n' + indent * queue[0][0]  # for sibling open
        else:
            element.tail = '\n' + indent * (level-1)  # for parent close
        queue[0:0] = children  # prepend so children come before siblings
nacitar sevaht
fonte
3

A impressão bonita de XML para python parece muito boa para esta tarefa. (Também chamado apropriadamente.)

Uma alternativa é usar pyXML , que possui uma função PrettyPrint .

Dan Lew
fonte
HTTPError: 404 Client Error: Not Found for url: https://pypi.org/simple/xmlpp/Pense que o projeto está no sótão hoje em dia, que vergonha.
8bitjunkie 04/06
3

Você pode usar a biblioteca externa popular xmltodict , com unparsee pretty=Trueobterá o melhor resultado:

xmltodict.unparse(
    xmltodict.parse(my_xml), full_document=False, pretty=True)

full_document=Falsecontra <?xml version="1.0" encoding="UTF-8"?>no topo.

Vitaly Zdanevich
fonte
3

Aqui está uma solução Python3 que elimina o feio problema de nova linha (toneladas de espaço em branco) e usa apenas bibliotecas padrão, diferente da maioria das outras implementações.

import xml.etree.ElementTree as ET
import xml.dom.minidom
import os

def pretty_print_xml_given_root(root, output_xml):
    """
    Useful for when you are editing xml data on the fly
    """
    xml_string = xml.dom.minidom.parseString(ET.tostring(root)).toprettyxml()
    xml_string = os.linesep.join([s for s in xml_string.splitlines() if s.strip()]) # remove the weird newline issue
    with open(output_xml, "w") as file_out:
        file_out.write(xml_string)

def pretty_print_xml_given_file(input_xml, output_xml):
    """
    Useful for when you want to reformat an already existing xml file
    """
    tree = ET.parse(input_xml)
    root = tree.getroot()
    pretty_print_xml_given_root(root, output_xml)

Eu descobri como corrigir o problema comum de nova linha aqui .

Josh Correia
fonte
2

Dê uma olhada no módulo vkbeautify .

É uma versão python do meu popular plugin javascript / nodejs com o mesmo nome. Pode imprimir / reduzir bastante o texto XML, JSON e CSS. Entrada e saída podem ser string / arquivo em qualquer combinação. É muito compacto e não tem nenhuma dependência.

Exemplos :

import vkbeautify as vkb

vkb.xml(text)                       
vkb.xml(text, 'path/to/dest/file')  
vkb.xml('path/to/src/file')        
vkb.xml('path/to/src/file', 'path/to/dest/file') 
vadimk
fonte
Essa biblioteca específica lida com o problema do nó de texto feio.
Cameron Lowell Palmer
1

Uma alternativa, se você não quiser revisar novamente , existe a biblioteca xmlpp.py com a get_pprint()função Funcionou bem e sem problemas nos meus casos de uso, sem ter que revisar novamente para um objeto lxml ElementTree.

laborioso
fonte
1
Tentei minidom e lxml e não obteve um xml corretamente formatado e recuado. Isso funcionou como esperado
david-Hoze
1
Falha nos nomes de tags que são prefixados por um namespace e contêm um hífen (por exemplo, <ns: hyphenated-tag />; a parte que começa com o hífen é simplesmente descartada, fornecendo, por exemplo, <ns: hyphenated />.
Endre Both
@EndreBoth Boa captura, eu não testei, mas talvez fosse fácil corrigir isso no código xmlpp.py?
gaborous 12/11/19
1

Você pode tentar esta variação ...

Instale BeautifulSoupe as lxmlbibliotecas de back-end (analisador):

user$ pip3 install lxml bs4

Processe seu documento XML:

from bs4 import BeautifulSoup

with open('/path/to/file.xml', 'r') as doc: 
    for line in doc: 
        print(BeautifulSoup(line, 'lxml-xml').prettify())  
NYCeyes
fonte
1
'lxml'usa o analisador HTML do lxml - consulte os documentos do BS4 . Você precisa 'xml'ou 'lxml-xml'para o analisador XML.
user2357112 suporta Monica
1
Este comentário continua sendo excluído. Novamente, insira uma reclamação formal (além de) 4 sinalizadores) de adulteração pós-StackOverflow e não pararei até que seja investigada forense por uma equipe de segurança (logs de acesso e históricos de versões). O carimbo de data / hora acima está errado (por anos) e provavelmente o conteúdo também.
NYCeyes
1
Isso funcionou bem para mim, inseguro do voto negativo dos documentoslxml’s XML parser BeautifulSoup(markup, "lxml-xml") BeautifulSoup(markup, "xml")
Datanovice 20/01
1
@Datanovice, estou feliz por ter ajudado. :) Quanto ao voto negativo suspeito, alguém violou minha resposta original (que foi especificada corretamente originalmente lxml-xml) e, em seguida, passou a votar no mesmo dia. Enviei uma reclamação oficial à S / O, mas eles se recusaram a investigar. De qualquer forma, desde então "adulterei" minha resposta, que agora está correta novamente (e especifica lxml-xmlcomo originalmente). Obrigado.
NYCeyes 20/01
0

Eu tive esse problema e resolvi-o assim:

def write_xml_file (self, file, xml_root_element, xml_declaration=False, pretty_print=False, encoding='unicode', indent='\t'):
    pretty_printed_xml = etree.tostring(xml_root_element, xml_declaration=xml_declaration, pretty_print=pretty_print, encoding=encoding)
    if pretty_print: pretty_printed_xml = pretty_printed_xml.replace('  ', indent)
    file.write(pretty_printed_xml)

No meu código, esse método é chamado assim:

try:
    with open(file_path, 'w') as file:
        file.write('<?xml version="1.0" encoding="utf-8" ?>')

        # create some xml content using etree ...

        xml_parser = XMLParser()
        xml_parser.write_xml_file(file, xml_root, xml_declaration=False, pretty_print=True, encoding='unicode', indent='\t')

except IOError:
    print("Error while writing in log file!")

Isso funciona apenas porque etree, por padrão, usa two spacesindentação, o que não acho muito enfatizar a indentação e, portanto, não é bonito. Não consegui identificar nenhuma configuração para etree ou parâmetro para qualquer função para alterar o recuo etree padrão. Eu gosto de como é fácil usar etree, mas isso foi realmente me irritando.

Zelphir Kaltstahl
fonte
0

Para converter um documento xml inteiro em um documento xml bonito
(por exemplo: supondo que você tenha extraído [descompactado] um arquivo .odt ou .ods do LibreOffice Writer e que você deseje converter o arquivo "content.xml" feio em um arquivo bonito para controle de versão git automatizado e git difftooling de arquivos .odt / .ods , como eu estou implementando aqui )

import xml.dom.minidom

file = open("./content.xml", 'r')
xml_string = file.read()
file.close()

parsed_xml = xml.dom.minidom.parseString(xml_string)
pretty_xml_as_string = parsed_xml.toprettyxml()

file = open("./content_new.xml", 'w')
file.write(pretty_xml_as_string)
file.close()

Referências:
- Graças à resposta de Ben Noland nesta página, que me levou a maior parte do caminho até lá.

Gabriel Staples
fonte
0
from lxml import etree
import xml.dom.minidom as mmd

xml_root = etree.parse(xml_fiel_path, etree.XMLParser())

def print_xml(xml_root):
    plain_xml = etree.tostring(xml_root).decode('utf-8')
    urgly_xml = ''.join(plain_xml .split())
    good_xml = mmd.parseString(urgly_xml)
    print(good_xml.toprettyxml(indent='    ',))

Está funcionando bem para o xml com chinês!

Reed_Xia
fonte
0

Se, por algum motivo, você não conseguir colocar em mãos nenhum dos módulos Python mencionados por outros usuários, sugiro a seguinte solução para o Python 2.7:

import subprocess

def makePretty(filepath):
  cmd = "xmllint --format " + filepath
  prettyXML = subprocess.check_output(cmd, shell = True)
  with open(filepath, "w") as outfile:
    outfile.write(prettyXML)

Até onde eu sei, esta solução funcionará em sistemas baseados em Unix que possuem o xmllintpacote instalado.

Friday Sky
fonte
xmllint já foi sugerido em outra resposta: stackoverflow.com/a/10133365/407651
mzjn
@mzjn Eu vi a resposta, mas simplifiquei a minha check_outputporque você não precisa fazer a verificação de erros
Friday Sky
-1

Resolvi isso com algumas linhas de código, abrindo o arquivo, passando por ele e adicionando recuo e salvando-o novamente. Eu estava trabalhando com pequenos arquivos xml e não queria adicionar dependências ou mais bibliotecas para instalar para o usuário. Enfim, aqui está o que eu acabei com:

    f = open(file_name,'r')
    xml = f.read()
    f.close()

    #Removing old indendations
    raw_xml = ''        
    for line in xml:
        raw_xml += line

    xml = raw_xml

    new_xml = ''
    indent = '    '
    deepness = 0

    for i in range((len(xml))):

        new_xml += xml[i]   
        if(i<len(xml)-3):

            simpleSplit = xml[i:(i+2)] == '><'
            advancSplit = xml[i:(i+3)] == '></'        
            end = xml[i:(i+2)] == '/>'    
            start = xml[i] == '<'

            if(advancSplit):
                deepness += -1
                new_xml += '\n' + indent*deepness
                simpleSplit = False
                deepness += -1
            if(simpleSplit):
                new_xml += '\n' + indent*deepness
            if(start):
                deepness += 1
            if(end):
                deepness += -1

    f = open(file_name,'w')
    f.write(new_xml)
    f.close()

Funciona para mim, talvez alguém tenha alguma utilidade :)

Petter TB
fonte
Mostre uma captura de tela do antes e depois e talvez evite futuros votos negativos. Eu não tentei o seu código, e claramente outras respostas aqui são melhores, eu acho (e mais gerais / totalmente formadas, já que elas dependem de boas bibliotecas), mas não sei por que você recebeu um voto negativo aqui. As pessoas devem deixar um comentário quando votam negativamente.
Gabriel Staples