2012-06-20 5 views
12

Cerco di ottenere l'intero contenuto tra un tag xml di apertura e la controparte di chiusura.Come ottengo l'intero contenuto tra due tag xml in Python?

Ottenere il contenuto in casi diritte come title sotto è facile, ma come posso ottenere l'intero contenuto tra i tag se misto contenuti viene utilizzato e voglio preservare i tag interni?

<?xml version="1.0" encoding="UTF-8"?> 
<review> 
    <title>Some testing stuff</title> 
    <text sometimes="attribute">Some text with <extradata>data</extradata> in it. 
    It spans <sometag>multiple lines: <tag>one</tag>, <tag>two</tag> 
    or more</sometag>.</text> 
</review> 

Quello che voglio è il contenuto tra i due text tag, comprese eventuali tag: Some text with <extradata>data</extradata> in it. It spans <sometag>multiple lines: <tag>one</tag>, <tag>two</tag> or more</sometag>.

Per ora io uso le espressioni regolari, ma ottengo kinda disordinato e non mi piace questo approccio. Mi rivolgo a una soluzione basata su parser XML. Ho dato un'occhiata a minidom, etree, lxml e BeautifulSoup ma non sono riuscito a trovare una soluzione per questo caso (tutto il contenuto, compresi i tag interni).

risposta

3
from lxml import etree 
t = etree.XML(
"""<?xml version="1.0" encoding="UTF-8"?> 
<review> 
    <title>Some testing stuff</title> 
    <text>Some text with <extradata>data</extradata> in it.</text> 
</review>""" 
) 
(t.text + ''.join(map(etree.tostring, t))).strip() 

Il trucco è che t è iterabile, e quando iterato, produce tutti i nodi figli. Poiché etree evita i nodi di testo, è inoltre necessario ripristinare il testo prima del primo tag secondario, con t.text.

In [50]: (t.text + ''.join(map(etree.tostring, t))).strip() 
Out[50]: '<title>Some testing stuff</title>\n <text>Some text with <extradata>data</extradata> in it.</text>' 

Oppure:

In [6]: e = t.xpath('//text')[0] 

In [7]: (e.text + ''.join(map(etree.tostring, e))).strip() 
Out[7]: 'Some text with <extradata>data</extradata> in it.' 
+0

L'OP vuole ottenere il contenuto di un elemento specifico. La tua soluzione non funziona in questo caso, almeno non direttamente. Ho ottenuto un elemento con 'e = t.xpath ('// text') [0]' e provato (''' .join (map (etree.tostring, e))') ma il risultato era '' dati in esso.''. – brandizzi

+0

@brandizzi Buon punto. Aggiornato per riflettere questo. – Marcin

+0

È necessario testare su alcuni altri casi, ma il tuo ultimo esempio funziona bene per me (finora). Quando si usa 'find' invece di' xpath' funziona anche con lo standard '' etree''. – Brutus

-2

appena trovato la soluzione, piuttosto facile:

In [31]: t = x.find('text') 

In [32]: t 
Out[32]: <Element text at 0xa87ed74> 

In [33]: list(t.itertext()) 
Out[33]: ['Some text with ', 'data', ' in it.'] 

In [34]: ''.join(_) 
Out[34]: 'Some text with data in it.' 

itertext è sicuramente il modo di andare qui!

Edit: // dispiace Ho pensato che si desidera solo il testo tra i figli, il mio male

+1

Posso ottenere lo stesso, penso, con 'x.find ('testo'). Get_text()'. ** Ma ** questo approccio esclude i tag interni e ne ho bisogno. – Brutus

+1

Questo non risolve il problema dell'OP in alcun modo, in realtà. È * obbligatorio * mantenere i tag interni. – brandizzi

+0

Conserva i tag interni, solo non più di un livello, vedi la mia modifica, 'itertext' ottiene tutto – dav1d

7

Ecco qualcosa che funziona per me e il campione:

from lxml import etree 
doc = etree.XML(
"""<?xml version="1.0" encoding="UTF-8"?> 
<review> 
    <title>Some testing stuff</title> 
    <text>Some text with <extradata>data</extradata> in it.</text> 
</review>""" 
) 

def flatten(seq): 
    r = [] 
    for item in seq: 
    if isinstance(item,(str,unicode)): 
     r.append(unicode(item)) 
    elif isinstance(item,(etree._Element,)): 
     r.append(etree.tostring(item,with_tail=False)) 
    return u"".join(r) 

print flatten(doc.xpath('/review/text/node()')) 

Resa:

Some text with <extradata>data</extradata> in it. 

xpath seleziona tutti i nodi figlio dell'elemento <text> e li esegue il rendering direttamente in Unicode se sono una sottoclasse stringa/unicode (<class 'lxml.etree._ElementStringResult'>) o cal ls etree.tostring su di esso se è un Element, with_tail=False evita la duplicazione della coda.

Potrebbe essere necessario gestire altri tipi di nodi, se presenti.

+0

+1 Per l'uso di 'node()' – dusan

+1

Questo potrebbe essere scritto in modo più compatto. Prendi questo one-liner: ''' .join (el if isinstance (el, str) else lxml.etree.tostring (el, with_tail = False) per el in doc.xpath ('/ review/text/node()')) ' –

+0

Probabilmente potresti semplicemente usare il' tostring' in modo indiscriminato. – Marcin

1

Questo è notevolmente facile con lxml *, utilizzando i parse() e tostring() funzioni:

from lxml.etree import parse, tostring 

In primo luogo si analizza il documento e ottenere il vostro elemento (sto usando XPath, ma è possibile utilizzare quello che vuoi):

doc = parse('test.xml') 
element = doc.xpath('//text')[0] 

La funzione tostring() restituisce una rappresentazione testuale del vostro elemento:

>>> tostring(element) 
'<text>Some <text>text</text> with <extradata>data</extradata> in it.</text>\n' 

Tuttavia, non si desidera che gli elementi esterni, in modo che possiamo rimuovere con un semplice str.replace() chiamata:

>>> tostring(element).replace('<%s>'%element.tag, '', 1) 
'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n' 

noti che str.replace() ricevuto 1 come terzo parametro, in modo che rimuoverà solo la prima occorrenza del tag di apertura. Si può fare anche con il tag di chiusura.Ora, invece di 1, si passa da -1 a sostituire:

>>> tostring(element).replace('</%s>'%element.tag, '', -1) 
'<text>Some <text>text with <extradata>data</extradata> in it.\n' 

La soluzione, naturalmente, è quello di fare tutto in una volta:

>>> tostring(element).replace('<%s>'%element.tag, '', 1).replace('</%s>'%element.tag, '', -1) 
'Some <text>text with <extradata>data</extradata> in it.\n' 

EDIT: @Charles fatto un buon punto : questo codice è fragile poiché il tag può avere attributi. Una possibile soluzione ma ancora limitata è quello di dividere la stringa al primo >:

>>> tostring(element).split('>', 1) 
['<text', 
'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n'] 

ottenere la seconda stringa risultante:

>>> tostring(element).split('>', 1)[1] 
'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n' 

poi rsplitting esso:

>>> tostring(element).split('>', 1)[1].rsplit('</', 1) 
['Some <text>text</text> with <extradata>data</extradata> in it.', 'text>\n'] 

e arrivare finalmente il primo risultato:

>>> tostring(element).split('>', 1)[1].rsplit('</', 1)[0] 
'Some <text>text</text> with <extradata>data</extradata> in it.' 

Tuttavia, questo codice è ancora fragile, dal momento che > è un carattere perfettamente valido in XML, anche all'interno degli attributi.

In ogni caso, devo riconoscere che MattH solution è la vera soluzione generale.

* In realtà questa soluzione funziona anche con ElementTree, il che è ottimo se non si vuole dipendere da lxml. L'unica differenza è che non avrai modo di usare XPath.

+1

Qui la sostituzione del testo sta aggiungendo un bel po 'di fragilità. Se il tuo file di input ha attributi su di esso? Un prefisso dello spazio dei nomi? –

+0

Ho la sensazione che non guadagnerò molto rispetto alle espressioni regolari pure con questo approccio. Dal momento che il tag di apertura ha almeno un attributo, anche questo è sfocato. – Brutus

+0

Non è necessaria alcuna modifica del testo. – Marcin

1

mi piace @ soluzione di Marcin sopra, tuttavia ho trovato che quando si usa la sua seconda opzione (conversione di un sub-node, non la radice dell'albero) non gestisce entità.

suo codice dall'alto (modificato per aggiungere un soggetto):

from lxml import etree 
t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?> 
<review> 
    <title>Some testing stuff</title> 
    <text>this &amp; that.</text> 
</review>""") 
e = t.xpath('//text')[0] 
print (e.text + ''.join(map(etree.tostring, e))).strip() 

rendimenti:

this & that. 

con un carattere nuda/escape '&' invece di un'entità adeguata (' & amp ; ').

La mia soluzione era quella di utilizzare per chiamare etree.tostring a livello di nodo (invece che su tutti i bambini), poi togliere il tag di inizio e fine utilizzando un'espressione regolare:

import re 
from lxml import etree 
t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?> 
<review> 
    <title>Some testing stuff</title> 
    <text>this &amp; that.</text> 
</review>""") 

e = t.xpath('//text')[0] 
xml = etree.tostring(e) 
inner = re.match('<[^>]*?>(.*)</[^>]*>\s*$', xml, flags=re.DOTALL).group(1) 
print inner 

produce:

this &amp; that. 

Ho usato re.DOTALL per garantire che questo funzioni per XML contenente newline.