Combine multiple tags with lxml

1.6k Views Asked by At

I have an html file which looks like:

...
<p>  
    <strong>This is </strong>  
    <strong>a lin</strong>  
    <strong>e which I want to </strong>  
    <strong>join.</strong>  
</p>
<p>
    2.
    <strong>But do not </strong>
    <strong>touch this</strong>
    <em>Maybe some other tags as well.</em>
    bla bla blah...
</p>
...

What I need is, if all the tags in a 'p' block are 'strong', then combine them into one line, i.e.

<p>
    <strong>This is a line which I want to join.</strong>
</p>

Without touching the other block since it contains something else.

Any suggestions? I am using lxml.

UPDATE:

So far I tried:

for p in self.tree.xpath('//body/p'):
        if p.tail is None: #no text before first element
            children = p.getchildren()
            for child in children:
                if len(children)==1 or child.tag!='strong' or child.tail is not None:
                    break
            else:
                etree.strip_tags(p,'strong')

With these code I was able to strip off the strong tag in the part desired, giving:

<p>
      This is a line which I want to join.  
</p>  

So now I just need a way to put the tag back in...

3

There are 3 best solutions below

1
On BEST ANSWER

I have managed to solve my own problem.

for p in self.tree.xpath('//body/p'):
    if p.tail is None:  # some conditions specifically for my doc 
        children = p.getchildren()
        if len(children)>1:
            for child in children:
                #if other stuffs present, break
                if child.tag!='strong' or child.tail is not None: 
                    break
            else:
                # If not break, we find a p block to fix
                # Get rid of stuffs inside p, and put a SubElement in
                etree.strip_tags(p,'strong')
                tmp_text = p.text_content()
                p.clear()
                subtext = etree.SubElement(p, "strong")
                subtext.text = tmp_text

Special thanks to @Scott who helps me come down to this solution. Although I cannot mark his answer correct, I have no less appreciation to his guidance.

2
On

I was able to do this with bs4 (BeautifulSoup):

from bs4 import BeautifulSoup as bs

html = """<p>  
<strong>This is </strong>  
<strong>a lin</strong>  
<strong>e which I want to </strong>  
<strong>join.</strong>  
</p>
<p>
<strong>But do not </strong>
<strong>touch this</strong>
</p>"""

soup = bs(html)
s = ''
# note that I use the 0th <p> block ...[0],
# so make the appropriate change in your code
for t in soup.find_all('p')[0].text:
    s = s+t.strip('\n')
s = '<p><strong>'+s+'</strong></p>'
print s # prints: <p><strong>This is a line which I want to join.</strong></p>

Then use replace_with():

p_tag = soup.p
p_tag.replace_with(bs(s, 'html.parser'))
print soup

prints:

<html><body><p><strong>This is a line which I want to join.</strong></p>
<p>
<strong>But do not </strong>
<strong>touch this</strong>
</p></body></html>
1
On

Alternatively, you can use more specific xpath to get the targeted p elements directly :

p_target = """
//p[strong]
   [not(*[not(self::strong)])]
   [not(text()[normalize-space()])]
"""
for p in self.tree.xpath(p_target):
    #logic inside the loop can also be the same as your `else` block
    content = p.xpath("normalize-space()")
    p.clear()
    strong = etree.SubElement(p, "strong")
    strong.text = content

brief explanation about xpath being used :

  • //p[strong] : find p element, anywhere in the XML/HTML document, having child element strong...
  • [not(*[not(self::strong)])] : ..and not having child element other than strong...
  • [not(text()[normalize-space()])] : ..and not having non-empty text node child.
  • normalize-space() : get all text nodes from current context element, concatenated with consecutive whitespaces normalized to single space