Remove a tag using BeautifulSoup but keep its contents
Asked Answered
A

12

64

Currently I have code that does something like this:

soup = BeautifulSoup(value)

for tag in soup.findAll(True):
    if tag.name not in VALID_TAGS:
        tag.extract()
soup.renderContents()

Except I don't want to throw away the contents inside the invalid tag. How do I get rid of the tag but keep the contents inside when calling soup.renderContents()?

Amador answered 19/11, 2009 at 19:19 Comment(0)
T
64

The strategy I used is to replace a tag with its contents if they are of type NavigableString and if they aren't, then recurse into them and replace their contents with NavigableString, etc. Try this:

from BeautifulSoup import BeautifulSoup, NavigableString

def strip_tags(html, invalid_tags):
    soup = BeautifulSoup(html)

    for tag in soup.findAll(True):
        if tag.name in invalid_tags:
            s = ""

            for c in tag.contents:
                if not isinstance(c, NavigableString):
                    c = strip_tags(unicode(c), invalid_tags)
                s += unicode(c)

            tag.replaceWith(s)

    return soup

html = "<p>Good, <b>bad</b>, and <i>ug<b>l</b><u>y</u></i></p>"
invalid_tags = ['b', 'i', 'u']
print strip_tags(html, invalid_tags)

The result is:

<p>Good, bad, and ugly</p>

I gave this same answer on another question. It seems to come up a lot.

Tees answered 12/7, 2010 at 3:25 Comment(2)
There was a bug here, introduced by an edit made by another user. You have to pass unicode strings on each call.Tees
maximum recursion depth exceeded :/Vincent
F
86

Current versions of the BeautifulSoup library have an undocumented method on Tag objects called replaceWithChildren(). So, you could do something like this:

html = "<p>Good, <b>bad</b>, and <i>ug<b>l</b><u>y</u></i></p>"
invalid_tags = ['b', 'i', 'u']
soup = BeautifulSoup(html)
for tag in invalid_tags: 
    for match in soup.findAll(tag):
        match.replaceWithChildren()
print soup

Looks like it behaves like you want it to and is fairly straightforward code (although it does make a few passes through the DOM, but this could easily be optimized.)

Flamenco answered 9/12, 2011 at 0:47 Comment(3)
This is awesome! Any idea on how I'd be able to add a space? I tried concatenating a ' ' after match before .replaceWithChildren(), but I can't figure it out. Thanks!Allometry
I like the simplicity. Just a note, the replaceWithChildren() method has been replaced with unwrap() in BS4Cautious
Is there a way to do this by specifying only valid tags?Centerpiece
T
64

The strategy I used is to replace a tag with its contents if they are of type NavigableString and if they aren't, then recurse into them and replace their contents with NavigableString, etc. Try this:

from BeautifulSoup import BeautifulSoup, NavigableString

def strip_tags(html, invalid_tags):
    soup = BeautifulSoup(html)

    for tag in soup.findAll(True):
        if tag.name in invalid_tags:
            s = ""

            for c in tag.contents:
                if not isinstance(c, NavigableString):
                    c = strip_tags(unicode(c), invalid_tags)
                s += unicode(c)

            tag.replaceWith(s)

    return soup

html = "<p>Good, <b>bad</b>, and <i>ug<b>l</b><u>y</u></i></p>"
invalid_tags = ['b', 'i', 'u']
print strip_tags(html, invalid_tags)

The result is:

<p>Good, bad, and ugly</p>

I gave this same answer on another question. It seems to come up a lot.

Tees answered 12/7, 2010 at 3:25 Comment(2)
There was a bug here, introduced by an edit made by another user. You have to pass unicode strings on each call.Tees
maximum recursion depth exceeded :/Vincent
P
21

Although this has already been mentoned by other people in the comments, I thought I'd post a full answer showing how to do it with Mozilla's Bleach. Personally, I think this is a lot nicer than using BeautifulSoup for this.

import bleach
html = "<b>Bad</b> <strong>Ugly</strong> <script>Evil()</script>"
clean = bleach.clean(html, tags=[], strip=True)
print clean # Should print: "Bad Ugly Evil()"
Petronia answered 20/10, 2012 at 15:22 Comment(6)
Can you have it remove tags selectively?Allometry
You can pass a whitelist of tags (as a list, tuple or other iterable) that you deem acceptable and bleach will remove/escape everything else (which is a lot safer than the inverse, specifying a blacklist). See here for more info: bleach.readthedocs.org/en/latest/clean.html#tag-whitelistPetronia
Awesome! I missed this comment and have been stressing over this for a few days, hah!Allometry
Sorry to keep coming back to you on this, but how do I set a whitelist? I have the tags PRESOL, DATE, etc and tried this code: attrs = {'PRESOL':'DATE'} clean = bleach.clean(s2, attributes = attrs, strip=True) to no avail.Allometry
Hi Jared. I think you might be getting mixed up with tags and attributes.Petronia
Oops, hit return too early on my last comment. Meant to also say: Here's an example that says "strip any tags from the string that aren't <p> and only allow the "class" attribute for <p> tags: hastebin.com/meqifupelu.xml Hope that helps.Petronia
U
11

I have a simpler solution but I don't know if there's a drawback to it.

UPDATE: there's a drawback, see Jesse Dhillon's comment. Also, another solution will be to use Mozilla's Bleach instead of BeautifulSoup.

from BeautifulSoup import BeautifulSoup

VALID_TAGS = ['div', 'p']

value = '<div><p>Hello <b>there</b> my friend!</p></div>'

soup = BeautifulSoup(value)

for tag in soup.findAll(True):
    if tag.name not in VALID_TAGS:
        tag.replaceWith(tag.renderContents())

print soup.renderContents()

This will also print <div><p>Hello there my friend!</p></div> as desired.

Unreal answered 20/11, 2009 at 3:43 Comment(5)
That code needs to be enhanced yet. It leaves the <p> untouched in case VALID_TAGS = 'b'Burgett
I fixed the code, VALID_TAGS wasn't a list but it should have.Unreal
This was my first attempt. It does not work if invalid tags are nested within other tags; you are not iterating the children of the tree, so your example only works for trees where depth == 1. Try your code with the example in my answer above.Tees
@JesseDhillon Look likes you're totally right! Your answer look like the good one but, unfortunately, when I try it, with your html, I get the same error as xralf (I'm using version 3.0.8.1)? The slacy's solution works for me but the drawback is that's not possible to specify only the valid tags (and maybe the speed).Unreal
@Unreal -- I fixed it. Another user had made an edit to the code which caused a bug.Tees
D
7

You'll presumably have to move tag's children to be children of tag's parent before you remove the tag -- is that what you mean?

If so, then, while inserting the contents in the right place is tricky, something like this should work:

from BeautifulSoup import BeautifulSoup

VALID_TAGS = 'div', 'p'

value = '<div><p>Hello <b>there</b> my friend!</p></div>'

soup = BeautifulSoup(value)

for tag in soup.findAll(True):
    if tag.name not in VALID_TAGS:
        for i, x in enumerate(tag.parent.contents):
          if x == tag: break
        else:
          print "Can't find", tag, "in", tag.parent
          continue
        for r in reversed(tag.contents):
          tag.parent.insert(i, r)
        tag.extract()
print soup.renderContents()

with the example value, this prints <div><p>Hello there my friend!</p></div> as desired.

Dygall answered 19/11, 2009 at 19:42 Comment(2)
I still want value = "Hello <div>there</div> my friend!" to be valid.Amador
@Jason, apart from needing an outermost tag, the string you give is perfectly valid and comes out unchanged from the code I give, so I have absolutely no idea what your comment is about!Dygall
N
7

you can use soup.text

.text removes all tags and concatenate all text.

Nitin answered 23/12, 2013 at 6:8 Comment(0)
F
3

Use unwrap.

Unwrap will remove one of multiple occurrence of the tag and still keep the contents.

Example:

>> soup = BeautifulSoup('Hi. This is a <nobr> nobr </nobr>')
>> soup
<html><body><p>Hi. This is a <nobr> nobr </nobr></p></body></html>
>> soup.nobr.unwrap
<nobr></nobr>
>> soup
>> <html><body><p>Hi. This is a nobr </p></body></html>
Fredkin answered 26/12, 2016 at 9:11 Comment(0)
R
2

None of the proposed answered seemed to work with BeautifulSoup for me. Here's a version that works with BeautifulSoup 3.2.1, and also inserts a space when joining content from different tags instead of concatenating words.

def strip_tags(html, whitelist=[]):
    """
    Strip all HTML tags except for a list of whitelisted tags.
    """
    soup = BeautifulSoup(html)

    for tag in soup.findAll(True):
        if tag.name not in whitelist:
            tag.append(' ')
            tag.replaceWithChildren()

    result = unicode(soup)

    # Clean up any repeated spaces and spaces like this: '<a>test </a> '
    result = re.sub(' +', ' ', result)
    result = re.sub(r' (<[^>]*> )', r'\1', result)
    return result.strip()

Example:

strip_tags('<h2><a><span>test</span></a> testing</h2><p>again</p>', ['a'])
# result: u'<a>test</a> testing again'
Rakel answered 22/4, 2013 at 10:4 Comment(0)
A
2

Here is the better solution without any hassles and boilerplate code to filter out the tags keeping the content.Lets say you want to remove any children tags within the parent tag and just want to keep the contents/text then,you can simply do:

for p_tags in div_tags.find_all("p"):
    print(p_tags.get_text())

That's it and you can be free with all the br or i b tags within the parent tags and get the clean text.

Affer answered 25/9, 2016 at 17:13 Comment(0)
P
2

Here is a python 3 friendly version of this function:

from bs4 import BeautifulSoup, NavigableString
invalidTags = ['br','b','font']
def stripTags(html, invalid_tags):
    soup = BeautifulSoup(html, "lxml")
    for tag in soup.findAll(True):
        if tag.name in invalid_tags:
            s = ""
            for c in tag.contents:
                if not isinstance(c, NavigableString):
                    c = stripTags(str(c), invalid_tags)
                s += str(c)
            tag.replaceWith(s)
    return soup
Place answered 1/6, 2019 at 14:4 Comment(0)
P
1

This is an old question, but just to say of a better ways to do it. First of all, BeautifulSoup 3* is no longer being developed, so you should rather use BeautifulSoup 4*, so called bs4.

Also, lxml has just function that you need: Cleaner class has attribute remove_tags, which you can set to tags that will be removed while their content getting pulled up into the parent tag.

Paramnesia answered 12/3, 2015 at 1:51 Comment(0)
H
0

What Worked For Me On Python 3.10 With BS4 And Unwrap

I initially liked Jesse Dhillon's answer a lot. However, I kept running into issues with the recursive calls due to recalling of the parser in BS4. I tried to change the level of recursion, but I kept running into problems with that too.

Then I looked into applying Bishwas Mishra's answer. Due to changes in BS4, I had to modify his code a bit, and I finally was able to develop a piece of code that would remove tags and maintain content.

I hope this helps some others.

from bs4 import BeautifulSoup


html = "<p>Good, <b>bad</b>, and <i>ug<b>l</b><u>y</u></i></p>"

soup = BeautifulSoup(html, "html5lib")

for c in ["html", "head", "body", "b", "i", "u"]:
    while soup.find(c):
        exec(f"soup.{c}.unwrap()")

print(soup)

NOTE: It is necessary to add "html", "head", and "body" to the invalid tags list, because BS4 will add those into your html text if they were not originally there, and I did not want them for my specific case.

The output I got from the above code was ...

<p>Good, bad, and ugly</p>
Hurried answered 24/8, 2023 at 21:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.