2023-01-12 01:04:47 +00:00
# Use of this source code is governed by the MIT license.
__license__ = " MIT "
try :
from collections . abc import Callable # Python 3.6
except ImportError as e :
from collections import Callable
import re
import sys
import warnings
try :
import soupsieve
except ImportError as e :
soupsieve = None
warnings . warn (
' The soupsieve package is not installed. CSS selectors cannot be used. '
)
from . formatter import (
Formatter ,
HTMLFormatter ,
XMLFormatter ,
)
DEFAULT_OUTPUT_ENCODING = " utf-8 "
nonwhitespace_re = re . compile ( r " \ S+ " )
# NOTE: This isn't used as of 4.7.0. I'm leaving it for a little bit on
# the off chance someone imported it for their own use.
whitespace_re = re . compile ( r " \ s+ " )
def _alias ( attr ) :
""" Alias one attribute name to another for backward compatibility """
@property
def alias ( self ) :
return getattr ( self , attr )
@alias.setter
def alias ( self ) :
return setattr ( self , attr )
return alias
# These encodings are recognized by Python (so PageElement.encode
# could theoretically support them) but XML and HTML don't recognize
# them (so they should not show up in an XML or HTML document as that
# document's encoding).
#
# If an XML document is encoded in one of these encodings, no encoding
# will be mentioned in the XML declaration. If an HTML document is
# encoded in one of these encodings, and the HTML document has a
# <meta> tag that mentions an encoding, the encoding will be given as
# the empty string.
#
# Source:
# https://docs.python.org/3/library/codecs.html#python-specific-encodings
PYTHON_SPECIFIC_ENCODINGS = set ( [
" idna " ,
" mbcs " ,
" oem " ,
" palmos " ,
" punycode " ,
" raw_unicode_escape " ,
" undefined " ,
" unicode_escape " ,
" raw-unicode-escape " ,
" unicode-escape " ,
" string-escape " ,
" string_escape " ,
] )
class NamespacedAttribute ( str ) :
""" A namespaced string (e.g. ' xml:lang ' ) that remembers the namespace
( ' xml ' ) and the name ( ' lang ' ) that were used to create it .
"""
def __new__ ( cls , prefix , name = None , namespace = None ) :
if not name :
# This is the default namespace. Its name "has no value"
# per https://www.w3.org/TR/xml-names/#defaulting
name = None
2023-01-14 01:03:30 +00:00
if not name :
2023-01-12 01:04:47 +00:00
obj = str . __new__ ( cls , prefix )
2023-01-14 01:03:30 +00:00
elif not prefix :
2023-01-12 01:04:47 +00:00
# Not really namespaced.
obj = str . __new__ ( cls , name )
else :
obj = str . __new__ ( cls , prefix + " : " + name )
obj . prefix = prefix
obj . name = name
obj . namespace = namespace
return obj
class AttributeValueWithCharsetSubstitution ( str ) :
""" A stand-in object for a character encoding specified in HTML. """
class CharsetMetaAttributeValue ( AttributeValueWithCharsetSubstitution ) :
""" A generic stand-in for the value of a meta tag ' s ' charset ' attribute.
When Beautiful Soup parses the markup ' <meta charset= " utf8 " > ' , the
value of the ' charset ' attribute will be one of these objects .
"""
def __new__ ( cls , original_value ) :
obj = str . __new__ ( cls , original_value )
obj . original_value = original_value
return obj
def encode ( self , encoding ) :
""" When an HTML document is being encoded to a given encoding, the
value of a meta tag ' s ' charset ' is the name of the encoding.
"""
if encoding in PYTHON_SPECIFIC_ENCODINGS :
return ' '
return encoding
class ContentMetaAttributeValue ( AttributeValueWithCharsetSubstitution ) :
""" A generic stand-in for the value of a meta tag ' s ' content ' attribute.
When Beautiful Soup parses the markup :
< meta http - equiv = " content-type " content = " text/html; charset=utf8 " >
The value of the ' content ' attribute will be one of these objects .
"""
CHARSET_RE = re . compile ( r " ((^|;) \ s*charset=)([^;]*) " , re . M )
def __new__ ( cls , original_value ) :
match = cls . CHARSET_RE . search ( original_value )
if match is None :
# No substitution necessary.
return str . __new__ ( str , original_value )
obj = str . __new__ ( cls , original_value )
obj . original_value = original_value
return obj
def encode ( self , encoding ) :
if encoding in PYTHON_SPECIFIC_ENCODINGS :
return ' '
def rewrite ( match ) :
return match . group ( 1 ) + encoding
return self . CHARSET_RE . sub ( rewrite , self . original_value )
class PageElement ( object ) :
""" Contains the navigational information for some part of the page:
that is , its current location in the parse tree .
NavigableString , Tag , etc . are all subclasses of PageElement .
"""
def setup ( self , parent = None , previous_element = None , next_element = None ,
previous_sibling = None , next_sibling = None ) :
""" Sets up the initial relations between this element and
other elements .
: param parent : The parent of this element .
: param previous_element : The element parsed immediately before
this one .
: param next_element : The element parsed immediately before
this one .
: param previous_sibling : The most recently encountered element
on the same level of the parse tree as this one .
: param previous_sibling : The next element to be encountered
on the same level of the parse tree as this one .
"""
self . parent = parent
self . previous_element = previous_element
if previous_element is not None :
self . previous_element . next_element = self
self . next_element = next_element
if self . next_element is not None :
self . next_element . previous_element = self
self . next_sibling = next_sibling
if self . next_sibling is not None :
self . next_sibling . previous_sibling = self
if ( previous_sibling is None
and self . parent is not None and self . parent . contents ) :
previous_sibling = self . parent . contents [ - 1 ]
self . previous_sibling = previous_sibling
if previous_sibling is not None :
self . previous_sibling . next_sibling = self
def format_string ( self , s , formatter ) :
""" Format the given string using the given formatter.
: param s : A string .
: param formatter : A Formatter object , or a string naming one of the standard formatters .
"""
if formatter is None :
return s
if not isinstance ( formatter , Formatter ) :
formatter = self . formatter_for_name ( formatter )
output = formatter . substitute ( s )
return output
def formatter_for_name ( self , formatter ) :
""" Look up or create a Formatter for the given identifier,
if necessary .
: param formatter : Can be a Formatter object ( used as - is ) , a
function ( used as the entity substitution hook for an
XMLFormatter or HTMLFormatter ) , or a string ( used to look
up an XMLFormatter or HTMLFormatter in the appropriate
registry .
"""
if isinstance ( formatter , Formatter ) :
return formatter
if self . _is_xml :
c = XMLFormatter
else :
c = HTMLFormatter
if isinstance ( formatter , Callable ) :
return c ( entity_substitution = formatter )
return c . REGISTRY [ formatter ]
@property
def _is_xml ( self ) :
""" Is this element part of an XML tree or an HTML tree?
This is used in formatter_for_name , when deciding whether an
XMLFormatter or HTMLFormatter is more appropriate . It can be
inefficient , but it should be called very rarely .
"""
if self . known_xml is not None :
# Most of the time we will have determined this when the
# document is parsed.
return self . known_xml
# Otherwise, it's likely that this element was created by
# direct invocation of the constructor from within the user's
# Python code.
if self . parent is None :
# This is the top-level object. It should have .known_xml set
# from tree creation. If not, take a guess--BS is usually
# used on HTML markup.
return getattr ( self , ' is_xml ' , False )
return self . parent . _is_xml
nextSibling = _alias ( " next_sibling " ) # BS3
previousSibling = _alias ( " previous_sibling " ) # BS3
2023-01-14 01:03:30 +00:00
default = object ( )
def _all_strings ( self , strip = False , types = default ) :
""" Yield all strings of certain classes, possibly stripping them.
This is implemented differently in Tag and NavigableString .
"""
raise NotImplementedError ( )
@property
def stripped_strings ( self ) :
""" Yield all strings in this PageElement, stripping them first.
: yield : A sequence of stripped strings .
"""
for string in self . _all_strings ( True ) :
yield string
def get_text ( self , separator = " " , strip = False ,
types = default ) :
""" Get all child strings of this PageElement, concatenated using the
given separator .
: param separator : Strings will be concatenated using this separator .
: param strip : If True , strings will be stripped before being
concatenated .
: param types : A tuple of NavigableString subclasses . Any
strings of a subclass not found in this list will be
ignored . Although there are exceptions , the default
behavior in most cases is to consider only NavigableString
and CData objects . That means no comments , processing
instructions , etc .
: return : A string .
"""
return separator . join ( [ s for s in self . _all_strings (
strip , types = types ) ] )
getText = get_text
text = property ( get_text )
def replace_with ( self , * args ) :
""" Replace this PageElement with one or more PageElements, keeping the
rest of the tree the same .
2023-01-12 01:04:47 +00:00
2023-01-14 01:03:30 +00:00
: param args : One or more PageElements .
2023-01-12 01:04:47 +00:00
: return : ` self ` , no longer part of the tree .
"""
if self . parent is None :
raise ValueError (
" Cannot replace one element with another when the "
" element to be replaced is not part of a tree. " )
2023-01-14 01:03:30 +00:00
if len ( args ) == 1 and args [ 0 ] is self :
2023-01-12 01:04:47 +00:00
return
2023-01-14 01:03:30 +00:00
if any ( x is self . parent for x in args ) :
2023-01-12 01:04:47 +00:00
raise ValueError ( " Cannot replace a Tag with its parent. " )
old_parent = self . parent
my_index = self . parent . index ( self )
self . extract ( _self_index = my_index )
2023-01-14 01:03:30 +00:00
for idx , replace_with in enumerate ( args , start = my_index ) :
old_parent . insert ( idx , replace_with )
2023-01-12 01:04:47 +00:00
return self
replaceWith = replace_with # BS3
def unwrap ( self ) :
""" Replace this PageElement with its contents.
: return : ` self ` , no longer part of the tree .
"""
my_parent = self . parent
if self . parent is None :
raise ValueError (
" Cannot replace an element with its contents when that "
" element is not part of a tree. " )
my_index = self . parent . index ( self )
self . extract ( _self_index = my_index )
for child in reversed ( self . contents [ : ] ) :
my_parent . insert ( my_index , child )
return self
replace_with_children = unwrap
replaceWithChildren = unwrap # BS3
def wrap ( self , wrap_inside ) :
""" Wrap this PageElement inside another one.
: param wrap_inside : A PageElement .
: return : ` wrap_inside ` , occupying the position in the tree that used
to be occupied by ` self ` , and with ` self ` inside it .
"""
me = self . replace_with ( wrap_inside )
wrap_inside . append ( me )
return wrap_inside
def extract ( self , _self_index = None ) :
""" Destructively rips this element out of the tree.
: param _self_index : The location of this element in its parent ' s
. contents , if known . Passing this in allows for a performance
optimization .
: return : ` self ` , no longer part of the tree .
"""
if self . parent is not None :
if _self_index is None :
_self_index = self . parent . index ( self )
del self . parent . contents [ _self_index ]
#Find the two elements that would be next to each other if
#this element (and any children) hadn't been parsed. Connect
#the two.
last_child = self . _last_descendant ( )
next_element = last_child . next_element
if ( self . previous_element is not None and
self . previous_element is not next_element ) :
self . previous_element . next_element = next_element
if next_element is not None and next_element is not self . previous_element :
next_element . previous_element = self . previous_element
self . previous_element = None
last_child . next_element = None
self . parent = None
if ( self . previous_sibling is not None
and self . previous_sibling is not self . next_sibling ) :
self . previous_sibling . next_sibling = self . next_sibling
if ( self . next_sibling is not None
and self . next_sibling is not self . previous_sibling ) :
self . next_sibling . previous_sibling = self . previous_sibling
self . previous_sibling = self . next_sibling = None
return self
def _last_descendant ( self , is_initialized = True , accept_self = True ) :
""" Finds the last element beneath this object to be parsed.
: param is_initialized : Has ` setup ` been called on this PageElement
yet ?
: param accept_self : Is ` self ` an acceptable answer to the question ?
"""
if is_initialized and self . next_sibling is not None :
last_child = self . next_sibling . previous_element
else :
last_child = self
while isinstance ( last_child , Tag ) and last_child . contents :
last_child = last_child . contents [ - 1 ]
if not accept_self and last_child is self :
last_child = None
return last_child
# BS3: Not part of the API!
_lastRecursiveChild = _last_descendant
def insert ( self , position , new_child ) :
""" Insert a new PageElement in the list of this PageElement ' s children.
This works the same way as ` list . insert ` .
: param position : The numeric position that should be occupied
in ` self . children ` by the new PageElement .
: param new_child : A PageElement .
"""
if new_child is None :
raise ValueError ( " Cannot insert None into a tag. " )
if new_child is self :
raise ValueError ( " Cannot insert a tag into itself. " )
if ( isinstance ( new_child , str )
and not isinstance ( new_child , NavigableString ) ) :
new_child = NavigableString ( new_child )
from . import BeautifulSoup
if isinstance ( new_child , BeautifulSoup ) :
# We don't want to end up with a situation where one BeautifulSoup
# object contains another. Insert the children one at a time.
for subchild in list ( new_child . contents ) :
self . insert ( position , subchild )
position + = 1
return
position = min ( position , len ( self . contents ) )
if hasattr ( new_child , ' parent ' ) and new_child . parent is not None :
# We're 'inserting' an element that's already one
# of this object's children.
if new_child . parent is self :
current_index = self . index ( new_child )
if current_index < position :
# We're moving this element further down the list
# of this object's children. That means that when
# we extract this element, our target index will
# jump down one.
position - = 1
new_child . extract ( )
new_child . parent = self
previous_child = None
if position == 0 :
new_child . previous_sibling = None
new_child . previous_element = self
else :
previous_child = self . contents [ position - 1 ]
new_child . previous_sibling = previous_child
new_child . previous_sibling . next_sibling = new_child
new_child . previous_element = previous_child . _last_descendant ( False )
if new_child . previous_element is not None :
new_child . previous_element . next_element = new_child
new_childs_last_element = new_child . _last_descendant ( False )
if position > = len ( self . contents ) :
new_child . next_sibling = None
parent = self
parents_next_sibling = None
while parents_next_sibling is None and parent is not None :
parents_next_sibling = parent . next_sibling
parent = parent . parent
if parents_next_sibling is not None :
# We found the element that comes next in the document.
break
if parents_next_sibling is not None :
new_childs_last_element . next_element = parents_next_sibling
else :
# The last element of this tag is the last element in
# the document.
new_childs_last_element . next_element = None
else :
next_child = self . contents [ position ]
new_child . next_sibling = next_child
if new_child . next_sibling is not None :
new_child . next_sibling . previous_sibling = new_child
new_childs_last_element . next_element = next_child
if new_childs_last_element . next_element is not None :
new_childs_last_element . next_element . previous_element = new_childs_last_element
self . contents . insert ( position , new_child )
def append ( self , tag ) :
""" Appends the given PageElement to the contents of this one.
: param tag : A PageElement .
"""
self . insert ( len ( self . contents ) , tag )
def extend ( self , tags ) :
""" Appends the given PageElements to this one ' s contents.
: param tags : A list of PageElements .
"""
if isinstance ( tags , Tag ) :
# Calling self.append() on another tag's contents will change
# the list we're iterating over. Make a list that won't
# change.
tags = list ( tags . contents )
for tag in tags :
self . append ( tag )
def insert_before ( self , * args ) :
""" Makes the given element(s) the immediate predecessor of this one.
All the elements will have the same parent , and the given elements
will be immediately before this one .
: param args : One or more PageElements .
"""
parent = self . parent
if parent is None :
raise ValueError (
" Element has no parent, so ' before ' has no meaning. " )
if any ( x is self for x in args ) :
raise ValueError ( " Can ' t insert an element before itself. " )
for predecessor in args :
# Extract first so that the index won't be screwed up if they
# are siblings.
if isinstance ( predecessor , PageElement ) :
predecessor . extract ( )
index = parent . index ( self )
parent . insert ( index , predecessor )
def insert_after ( self , * args ) :
""" Makes the given element(s) the immediate successor of this one.
The elements will have the same parent , and the given elements
will be immediately after this one .
: param args : One or more PageElements .
"""
# Do all error checking before modifying the tree.
parent = self . parent
if parent is None :
raise ValueError (
" Element has no parent, so ' after ' has no meaning. " )
if any ( x is self for x in args ) :
raise ValueError ( " Can ' t insert an element after itself. " )
offset = 0
for successor in args :
# Extract first so that the index won't be screwed up if they
# are siblings.
if isinstance ( successor , PageElement ) :
successor . extract ( )
index = parent . index ( self )
parent . insert ( index + 1 + offset , successor )
offset + = 1
2023-01-14 01:03:30 +00:00
def find_next ( self , name = None , attrs = { } , string = None , * * kwargs ) :
2023-01-12 01:04:47 +00:00
""" Find the first PageElement that matches the given criteria and
appears later in the document than this PageElement .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
2023-01-14 01:03:30 +00:00
return self . _find_one ( self . find_all_next , name , attrs , string , * * kwargs )
2023-01-12 01:04:47 +00:00
findNext = find_next # BS3
2023-01-14 01:03:30 +00:00
def find_all_next ( self , name = None , attrs = { } , string = None , limit = None ,
2023-01-12 01:04:47 +00:00
* * kwargs ) :
""" Find all PageElements that match the given criteria and appear
later in the document than this PageElement .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A ResultSet containing PageElements .
"""
2023-01-14 01:03:30 +00:00
return self . _find_all ( name , attrs , string , limit , self . next_elements ,
2023-01-12 01:04:47 +00:00
* * kwargs )
findAllNext = find_all_next # BS3
2023-01-14 01:03:30 +00:00
def find_next_sibling ( self , name = None , attrs = { } , string = None , * * kwargs ) :
2023-01-12 01:04:47 +00:00
""" Find the closest sibling to this PageElement that matches the
given criteria and appears later in the document .
All find_ * methods take a common set of arguments . See the
online documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
2023-01-14 01:03:30 +00:00
return self . _find_one ( self . find_next_siblings , name , attrs , string ,
2023-01-12 01:04:47 +00:00
* * kwargs )
findNextSibling = find_next_sibling # BS3
2023-01-14 01:03:30 +00:00
def find_next_siblings ( self , name = None , attrs = { } , string = None , limit = None ,
2023-01-12 01:04:47 +00:00
* * kwargs ) :
""" Find all siblings of this PageElement that match the given criteria
and appear later in the document .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A ResultSet of PageElements .
: rtype : bs4 . element . ResultSet
"""
2023-01-14 01:03:30 +00:00
return self . _find_all ( name , attrs , string , limit ,
2023-01-12 01:04:47 +00:00
self . next_siblings , * * kwargs )
findNextSiblings = find_next_siblings # BS3
fetchNextSiblings = find_next_siblings # BS2
2023-01-14 01:03:30 +00:00
def find_previous ( self , name = None , attrs = { } , string = None , * * kwargs ) :
2023-01-12 01:04:47 +00:00
""" Look backwards in the document from this PageElement and find the
first PageElement that matches the given criteria .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
return self . _find_one (
2023-01-14 01:03:30 +00:00
self . find_all_previous , name , attrs , string , * * kwargs )
2023-01-12 01:04:47 +00:00
findPrevious = find_previous # BS3
2023-01-14 01:03:30 +00:00
def find_all_previous ( self , name = None , attrs = { } , string = None , limit = None ,
2023-01-12 01:04:47 +00:00
* * kwargs ) :
""" Look backwards in the document from this PageElement and find all
PageElements that match the given criteria .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A ResultSet of PageElements .
: rtype : bs4 . element . ResultSet
"""
2023-01-14 01:03:30 +00:00
return self . _find_all ( name , attrs , string , limit , self . previous_elements ,
2023-01-12 01:04:47 +00:00
* * kwargs )
findAllPrevious = find_all_previous # BS3
fetchPrevious = find_all_previous # BS2
2023-01-14 01:03:30 +00:00
def find_previous_sibling ( self , name = None , attrs = { } , string = None , * * kwargs ) :
2023-01-12 01:04:47 +00:00
""" Returns the closest sibling to this PageElement that matches the
given criteria and appears earlier in the document .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
2023-01-14 01:03:30 +00:00
return self . _find_one ( self . find_previous_siblings , name , attrs , string ,
2023-01-12 01:04:47 +00:00
* * kwargs )
findPreviousSibling = find_previous_sibling # BS3
2023-01-14 01:03:30 +00:00
def find_previous_siblings ( self , name = None , attrs = { } , string = None ,
2023-01-12 01:04:47 +00:00
limit = None , * * kwargs ) :
""" Returns all siblings to this PageElement that match the
given criteria and appear earlier in the document .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A ResultSet of PageElements .
: rtype : bs4 . element . ResultSet
"""
2023-01-14 01:03:30 +00:00
return self . _find_all ( name , attrs , string , limit ,
2023-01-12 01:04:47 +00:00
self . previous_siblings , * * kwargs )
findPreviousSiblings = find_previous_siblings # BS3
fetchPreviousSiblings = find_previous_siblings # BS2
def find_parent ( self , name = None , attrs = { } , * * kwargs ) :
""" Find the closest parent of this PageElement that matches the given
criteria .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
# NOTE: We can't use _find_one because findParents takes a different
# set of arguments.
r = None
l = self . find_parents ( name , attrs , 1 , * * kwargs )
if l :
r = l [ 0 ]
return r
findParent = find_parent # BS3
def find_parents ( self , name = None , attrs = { } , limit = None , * * kwargs ) :
""" Find all parents of this PageElement that match the given criteria.
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
return self . _find_all ( name , attrs , None , limit , self . parents ,
* * kwargs )
findParents = find_parents # BS3
fetchParents = find_parents # BS2
@property
def next ( self ) :
""" The PageElement, if any, that was parsed just after this one.
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
return self . next_element
@property
def previous ( self ) :
""" The PageElement, if any, that was parsed just before this one.
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
return self . previous_element
#These methods do the real heavy lifting.
2023-01-14 01:03:30 +00:00
def _find_one ( self , method , name , attrs , string , * * kwargs ) :
2023-01-12 01:04:47 +00:00
r = None
2023-01-14 01:03:30 +00:00
l = method ( name , attrs , string , 1 , * * kwargs )
2023-01-12 01:04:47 +00:00
if l :
r = l [ 0 ]
return r
2023-01-14 01:03:30 +00:00
def _find_all ( self , name , attrs , string , limit , generator , * * kwargs ) :
2023-01-12 01:04:47 +00:00
" Iterates over a generator looking for things that match. "
2023-01-14 01:03:30 +00:00
if string is None and ' text ' in kwargs :
string = kwargs . pop ( ' text ' )
warnings . warn (
" The ' text ' argument to find()-type methods is deprecated. Use ' string ' instead. " ,
DeprecationWarning
)
2023-01-12 01:04:47 +00:00
if isinstance ( name , SoupStrainer ) :
strainer = name
else :
2023-01-14 01:03:30 +00:00
strainer = SoupStrainer ( name , attrs , string , * * kwargs )
2023-01-12 01:04:47 +00:00
2023-01-14 01:03:30 +00:00
if string is None and not limit and not attrs and not kwargs :
2023-01-12 01:04:47 +00:00
if name is True or name is None :
# Optimization to find all tags.
result = ( element for element in generator
if isinstance ( element , Tag ) )
return ResultSet ( strainer , result )
elif isinstance ( name , str ) :
# Optimization to find all tags with a given name.
if name . count ( ' : ' ) == 1 :
# This is a name with a prefix. If this is a namespace-aware document,
# we need to match the local name against tag.name. If not,
# we need to match the fully-qualified name against tag.name.
prefix , local_name = name . split ( ' : ' , 1 )
else :
prefix = None
local_name = name
result = ( element for element in generator
if isinstance ( element , Tag )
and (
element . name == name
) or (
element . name == local_name
and ( prefix is None or element . prefix == prefix )
)
)
return ResultSet ( strainer , result )
results = ResultSet ( strainer )
while True :
try :
i = next ( generator )
except StopIteration :
break
if i :
found = strainer . search ( i )
if found :
results . append ( found )
if limit and len ( results ) > = limit :
break
return results
#These generators can be used to navigate starting from both
#NavigableStrings and Tags.
@property
def next_elements ( self ) :
""" All PageElements that were parsed after this one.
: yield : A sequence of PageElements .
"""
i = self . next_element
while i is not None :
yield i
i = i . next_element
@property
def next_siblings ( self ) :
""" All PageElements that are siblings of this one but were parsed
later .
: yield : A sequence of PageElements .
"""
i = self . next_sibling
while i is not None :
yield i
i = i . next_sibling
@property
def previous_elements ( self ) :
""" All PageElements that were parsed before this one.
: yield : A sequence of PageElements .
"""
i = self . previous_element
while i is not None :
yield i
i = i . previous_element
@property
def previous_siblings ( self ) :
""" All PageElements that are siblings of this one but were parsed
earlier .
: yield : A sequence of PageElements .
"""
i = self . previous_sibling
while i is not None :
yield i
i = i . previous_sibling
@property
def parents ( self ) :
""" All PageElements that are parents of this PageElement.
: yield : A sequence of PageElements .
"""
i = self . parent
while i is not None :
yield i
i = i . parent
@property
def decomposed ( self ) :
""" Check whether a PageElement has been decomposed.
: rtype : bool
"""
return getattr ( self , ' _decomposed ' , False ) or False
# Old non-property versions of the generators, for backwards
# compatibility with BS3.
def nextGenerator ( self ) :
return self . next_elements
def nextSiblingGenerator ( self ) :
return self . next_siblings
def previousGenerator ( self ) :
return self . previous_elements
def previousSiblingGenerator ( self ) :
return self . previous_siblings
def parentGenerator ( self ) :
return self . parents
class NavigableString ( str , PageElement ) :
""" A Python Unicode string that is part of a parse tree.
When Beautiful Soup parses the markup < b > penguin < / b > , it will
create a NavigableString for the string " penguin " .
"""
PREFIX = ' '
SUFFIX = ' '
# We can't tell just by looking at a string whether it's contained
# in an XML document or an HTML document.
known_xml = None
def __new__ ( cls , value ) :
""" Create a new NavigableString.
When unpickling a NavigableString , this method is called with
the string in DEFAULT_OUTPUT_ENCODING . That encoding needs to be
passed in to the superclass ' s __new__ or the superclass won ' t know
how to handle non - ASCII characters .
"""
if isinstance ( value , str ) :
u = str . __new__ ( cls , value )
else :
u = str . __new__ ( cls , value , DEFAULT_OUTPUT_ENCODING )
u . setup ( )
return u
def __copy__ ( self ) :
""" A copy of a NavigableString has the same contents and class
as the original , but it is not connected to the parse tree .
"""
return type ( self ) ( self )
def __getnewargs__ ( self ) :
return ( str ( self ) , )
def __getattr__ ( self , attr ) :
""" text.string gives you text. This is for backwards
compatibility for Navigable * String , but for CData * it lets you
get the string without the CData wrapper . """
if attr == ' string ' :
return self
else :
raise AttributeError (
" ' %s ' object has no attribute ' %s ' " % (
self . __class__ . __name__ , attr ) )
def output_ready ( self , formatter = " minimal " ) :
""" Run the string through the provided formatter.
: param formatter : A Formatter object , or a string naming one of the standard formatters .
"""
output = self . format_string ( self , formatter )
return self . PREFIX + output + self . SUFFIX
@property
def name ( self ) :
""" Since a NavigableString is not a Tag, it has no .name.
This property is implemented so that code like this doesn ' t crash
when run on a mixture of Tag and NavigableString objects :
[ x . name for x in tag . children ]
"""
return None
@name.setter
def name ( self , name ) :
""" Prevent NavigableString.name from ever being set. """
raise AttributeError ( " A NavigableString cannot be given a name. " )
2023-01-14 01:03:30 +00:00
def _all_strings ( self , strip = False , types = PageElement . default ) :
""" Yield all strings of certain classes, possibly stripping them.
This makes it easy for NavigableString to implement methods
like get_text ( ) as conveniences , creating a consistent
text - extraction API across all PageElements .
: param strip : If True , all strings will be stripped before being
yielded .
: param types : A tuple of NavigableString subclasses . If this
NavigableString isn ' t one of those subclasses, the
sequence will be empty . By default , the subclasses
considered are NavigableString and CData objects . That
means no comments , processing instructions , etc .
: yield : A sequence that either contains this string , or is empty .
"""
if types is self . default :
# This is kept in Tag because it's full of subclasses of
# this class, which aren't defined until later in the file.
types = Tag . DEFAULT_INTERESTING_STRING_TYPES
# Do nothing if the caller is looking for specific types of
# string, and we're of a different type.
#
# We check specific types instead of using isinstance(self,
# types) because all of these classes subclass
# NavigableString. Anyone who's using this feature probably
# wants generic NavigableStrings but not other stuff.
my_type = type ( self )
if types is not None :
if isinstance ( types , type ) :
# Looking for a single type.
if my_type is not types :
return
elif my_type not in types :
# Looking for one of a list of types.
return
value = self
if strip :
value = value . strip ( )
if len ( value ) > 0 :
yield value
strings = property ( _all_strings )
2023-01-12 01:04:47 +00:00
class PreformattedString ( NavigableString ) :
""" A NavigableString not subject to the normal formatting rules.
This is an abstract class used for special kinds of strings such
as comments ( the Comment class ) and CDATA blocks ( the CData
class ) .
"""
PREFIX = ' '
SUFFIX = ' '
def output_ready ( self , formatter = None ) :
""" Make this string ready for output by adding any subclass-specific
prefix or suffix .
: param formatter : A Formatter object , or a string naming one
of the standard formatters . The string will be passed into the
Formatter , but only to trigger any side effects : the return
value is ignored .
: return : The string , with any subclass - specific prefix and
suffix added on .
"""
if formatter is not None :
ignore = self . format_string ( self , formatter )
return self . PREFIX + self + self . SUFFIX
class CData ( PreformattedString ) :
""" A CDATA block. """
PREFIX = ' <![CDATA[ '
SUFFIX = ' ]]> '
class ProcessingInstruction ( PreformattedString ) :
""" A SGML processing instruction. """
PREFIX = ' <? '
SUFFIX = ' > '
class XMLProcessingInstruction ( ProcessingInstruction ) :
""" An XML processing instruction. """
PREFIX = ' <? '
SUFFIX = ' ?> '
class Comment ( PreformattedString ) :
""" An HTML or XML comment. """
PREFIX = ' <!-- '
SUFFIX = ' --> '
class Declaration ( PreformattedString ) :
""" An XML declaration. """
PREFIX = ' <? '
SUFFIX = ' ?> '
class Doctype ( PreformattedString ) :
""" A document type declaration. """
@classmethod
def for_name_and_ids ( cls , name , pub_id , system_id ) :
""" Generate an appropriate document type declaration for a given
public ID and system ID .
: param name : The name of the document ' s root element, e.g. ' html ' .
: param pub_id : The Formal Public Identifier for this document type ,
e . g . ' -//W3C//DTD XHTML 1.1//EN '
: param system_id : The system identifier for this document type ,
e . g . ' http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd '
: return : A Doctype .
"""
value = name or ' '
if pub_id is not None :
value + = ' PUBLIC " %s " ' % pub_id
if system_id is not None :
value + = ' " %s " ' % system_id
elif system_id is not None :
value + = ' SYSTEM " %s " ' % system_id
return Doctype ( value )
PREFIX = ' <!DOCTYPE '
SUFFIX = ' > \n '
class Stylesheet ( NavigableString ) :
""" A NavigableString representing an stylesheet (probably
CSS ) .
Used to distinguish embedded stylesheets from textual content .
"""
pass
class Script ( NavigableString ) :
""" A NavigableString representing an executable script (probably
Javascript ) .
Used to distinguish executable code from textual content .
"""
pass
class TemplateString ( NavigableString ) :
""" A NavigableString representing a string found inside an HTML
template embedded in a larger document .
Used to distinguish such strings from the main body of the document .
"""
pass
2023-01-14 01:03:30 +00:00
class RubyTextString ( NavigableString ) :
""" A NavigableString representing the contents of the <rt> HTML
element .
https : / / dev . w3 . org / html5 / spec - LC / text - level - semantics . html #the-rt-element
Can be used to distinguish such strings from the strings they ' re
annotating .
"""
pass
class RubyParenthesisString ( NavigableString ) :
""" A NavigableString representing the contents of the <rp> HTML
element .
https : / / dev . w3 . org / html5 / spec - LC / text - level - semantics . html #the-rp-element
"""
pass
2023-01-12 01:04:47 +00:00
class Tag ( PageElement ) :
""" Represents an HTML or XML tag that is part of a parse tree, along
with its attributes and contents .
When Beautiful Soup parses the markup < b > penguin < / b > , it will
create a Tag object representing the < b > tag .
"""
def __init__ ( self , parser = None , builder = None , name = None , namespace = None ,
prefix = None , attrs = None , parent = None , previous = None ,
is_xml = None , sourceline = None , sourcepos = None ,
can_be_empty_element = None , cdata_list_attributes = None ,
2023-01-14 01:03:30 +00:00
preserve_whitespace_tags = None ,
interesting_string_types = None ,
namespaces = None
2023-01-12 01:04:47 +00:00
) :
""" Basic constructor.
: param parser : A BeautifulSoup object .
: param builder : A TreeBuilder .
: param name : The name of the tag .
: param namespace : The URI of this Tag ' s XML namespace, if any.
: param prefix : The prefix for this Tag ' s XML namespace, if any.
: param attrs : A dictionary of this Tag ' s attribute values.
: param parent : The PageElement to use as this Tag ' s parent.
: param previous : The PageElement that was parsed immediately before
this tag .
: param is_xml : If True , this is an XML tag . Otherwise , this is an
HTML tag .
: param sourceline : The line number where this tag was found in its
source document .
: param sourcepos : The character position within ` sourceline ` where this
tag was found .
: param can_be_empty_element : If True , this tag should be
represented as < tag / > . If False , this tag should be represented
as < tag > < / tag > .
: param cdata_list_attributes : A list of attributes whose values should
be treated as CDATA if they ever show up on this tag .
: param preserve_whitespace_tags : A list of tag names whose contents
should have their whitespace preserved .
2023-01-14 01:03:30 +00:00
: param interesting_string_types : This is a NavigableString
subclass or a tuple of them . When iterating over this
Tag ' s strings in methods like Tag.strings or Tag.get_text,
these are the types of strings that are interesting enough
to be considered . The default is to consider
NavigableString and CData the only interesting string
subtypes .
: param namespaces : A dictionary mapping currently active
namespace prefixes to URIs . This can be used later to
construct CSS selectors .
2023-01-12 01:04:47 +00:00
"""
if parser is None :
self . parser_class = None
else :
# We don't actually store the parser object: that lets extracted
# chunks be garbage-collected.
self . parser_class = parser . __class__
if name is None :
raise ValueError ( " No value provided for new tag ' s name. " )
self . name = name
self . namespace = namespace
2023-01-14 01:03:30 +00:00
self . _namespaces = namespaces or { }
2023-01-12 01:04:47 +00:00
self . prefix = prefix
if ( ( not builder or builder . store_line_numbers )
and ( sourceline is not None or sourcepos is not None ) ) :
self . sourceline = sourceline
self . sourcepos = sourcepos
if attrs is None :
attrs = { }
elif attrs :
if builder is not None and builder . cdata_list_attributes :
attrs = builder . _replace_cdata_list_attribute_values (
self . name , attrs )
else :
attrs = dict ( attrs )
else :
attrs = dict ( attrs )
# If possible, determine ahead of time whether this tag is an
# XML tag.
if builder :
self . known_xml = builder . is_xml
else :
self . known_xml = is_xml
self . attrs = attrs
self . contents = [ ]
self . setup ( parent , previous )
self . hidden = False
if builder is None :
# In the absence of a TreeBuilder, use whatever values were
# passed in here. They're probably None, unless this is a copy of some
# other tag.
self . can_be_empty_element = can_be_empty_element
self . cdata_list_attributes = cdata_list_attributes
self . preserve_whitespace_tags = preserve_whitespace_tags
2023-01-14 01:03:30 +00:00
self . interesting_string_types = interesting_string_types
2023-01-12 01:04:47 +00:00
else :
# Set up any substitutions for this tag, such as the charset in a META tag.
builder . set_up_substitutions ( self )
# Ask the TreeBuilder whether this tag might be an empty-element tag.
self . can_be_empty_element = builder . can_be_empty_element ( name )
# Keep track of the list of attributes of this tag that
# might need to be treated as a list.
#
# For performance reasons, we store the whole data structure
# rather than asking the question of every tag. Asking would
# require building a new data structure every time, and
# (unlike can_be_empty_element), we almost never need
# to check this.
self . cdata_list_attributes = builder . cdata_list_attributes
# Keep track of the names that might cause this tag to be treated as a
# whitespace-preserved tag.
self . preserve_whitespace_tags = builder . preserve_whitespace_tags
2023-01-14 01:03:30 +00:00
if self . name in builder . string_containers :
# This sort of tag uses a special string container
# subclass for most of its strings. When we ask the
self . interesting_string_types = builder . string_containers [ self . name ]
else :
self . interesting_string_types = self . DEFAULT_INTERESTING_STRING_TYPES
2023-01-12 01:04:47 +00:00
parserClass = _alias ( " parser_class " ) # BS3
def __copy__ ( self ) :
""" A copy of a Tag is a new Tag, unconnected to the parse tree.
Its contents are a copy of the old Tag ' s contents.
"""
clone = type ( self ) (
None , self . builder , self . name , self . namespace ,
self . prefix , self . attrs , is_xml = self . _is_xml ,
sourceline = self . sourceline , sourcepos = self . sourcepos ,
can_be_empty_element = self . can_be_empty_element ,
cdata_list_attributes = self . cdata_list_attributes ,
preserve_whitespace_tags = self . preserve_whitespace_tags
)
for attr in ( ' can_be_empty_element ' , ' hidden ' ) :
setattr ( clone , attr , getattr ( self , attr ) )
for child in self . contents :
clone . append ( child . __copy__ ( ) )
return clone
2023-01-14 01:03:30 +00:00
2023-01-12 01:04:47 +00:00
@property
def is_empty_element ( self ) :
""" Is this tag an empty-element tag? (aka a self-closing tag)
A tag that has contents is never an empty - element tag .
A tag that has no contents may or may not be an empty - element
tag . It depends on the builder used to create the tag . If the
builder has a designated list of empty - element tags , then only
a tag whose name shows up in that list is considered an
empty - element tag .
If the builder has no designated list of empty - element tags ,
then any tag with no contents is an empty - element tag .
"""
return len ( self . contents ) == 0 and self . can_be_empty_element
isSelfClosing = is_empty_element # BS3
@property
def string ( self ) :
""" Convenience property to get the single string within this
PageElement .
TODO It might make sense to have NavigableString . string return
itself .
: return : If this element has a single string child , return
value is that string . If this element has one child tag ,
return value is the ' string ' attribute of the child tag ,
recursively . If this element is itself a string , has no
children , or has more than one child , return value is None .
"""
if len ( self . contents ) != 1 :
return None
child = self . contents [ 0 ]
if isinstance ( child , NavigableString ) :
return child
return child . string
@string.setter
def string ( self , string ) :
""" Replace this PageElement ' s contents with `string`. """
self . clear ( )
self . append ( string . __class__ ( string ) )
2023-01-14 01:03:30 +00:00
DEFAULT_INTERESTING_STRING_TYPES = ( NavigableString , CData )
def _all_strings ( self , strip = False , types = PageElement . default ) :
2023-01-12 01:04:47 +00:00
""" Yield all strings of certain classes, possibly stripping them.
: param strip : If True , all strings will be stripped before being
yielded .
2023-01-14 01:03:30 +00:00
: param types : A tuple of NavigableString subclasses . Any strings of
2023-01-12 01:04:47 +00:00
a subclass not found in this list will be ignored . By
2023-01-14 01:03:30 +00:00
default , the subclasses considered are the ones found in
self . interesting_string_types . If that ' s not specified,
only NavigableString and CData objects will be
considered . That means no comments , processing
instructions , etc .
2023-01-12 01:04:47 +00:00
: yield : A sequence of strings .
2023-01-14 01:03:30 +00:00
2023-01-12 01:04:47 +00:00
"""
2023-01-14 01:03:30 +00:00
if types is self . default :
types = self . interesting_string_types
2023-01-12 01:04:47 +00:00
for descendant in self . descendants :
2023-01-14 01:03:30 +00:00
if ( types is None and not isinstance ( descendant , NavigableString ) ) :
continue
descendant_type = type ( descendant )
if isinstance ( types , type ) :
if descendant_type is not types :
# We're not interested in strings of this type.
continue
elif types is not None and descendant_type not in types :
# We're not interested in strings of this type.
2023-01-12 01:04:47 +00:00
continue
if strip :
descendant = descendant . strip ( )
if len ( descendant ) == 0 :
continue
yield descendant
strings = property ( _all_strings )
def decompose ( self ) :
""" Recursively destroys this PageElement and its children.
This element will be removed from the tree and wiped out ; so
will everything beneath it .
The behavior of a decomposed PageElement is undefined and you
should never use one for anything , but if you need to _check_
whether an element has been decomposed , you can use the
` decomposed ` property .
"""
self . extract ( )
i = self
while i is not None :
n = i . next_element
i . __dict__ . clear ( )
i . contents = [ ]
i . _decomposed = True
i = n
def clear ( self , decompose = False ) :
""" Wipe out all children of this PageElement by calling extract()
on them .
: param decompose : If this is True , decompose ( ) ( a more
destructive method ) will be called instead of extract ( ) .
"""
if decompose :
for element in self . contents [ : ] :
if isinstance ( element , Tag ) :
element . decompose ( )
else :
element . extract ( )
else :
for element in self . contents [ : ] :
element . extract ( )
def smooth ( self ) :
""" Smooth out this element ' s children by consolidating consecutive
strings .
This makes pretty - printed output look more natural following a
lot of operations that modified the tree .
"""
# Mark the first position of every pair of children that need
# to be consolidated. Do this rather than making a copy of
# self.contents, since in most cases very few strings will be
# affected.
marked = [ ]
for i , a in enumerate ( self . contents ) :
if isinstance ( a , Tag ) :
# Recursively smooth children.
a . smooth ( )
if i == len ( self . contents ) - 1 :
# This is the last item in .contents, and it's not a
# tag. There's no chance it needs any work.
continue
b = self . contents [ i + 1 ]
if ( isinstance ( a , NavigableString )
and isinstance ( b , NavigableString )
and not isinstance ( a , PreformattedString )
and not isinstance ( b , PreformattedString )
) :
marked . append ( i )
# Go over the marked positions in reverse order, so that
# removing items from .contents won't affect the remaining
# positions.
for i in reversed ( marked ) :
a = self . contents [ i ]
b = self . contents [ i + 1 ]
b . extract ( )
n = NavigableString ( a + b )
a . replace_with ( n )
def index ( self , element ) :
""" Find the index of a child by identity, not value.
Avoids issues with tag . contents . index ( element ) getting the
index of equal elements .
: param element : Look for this PageElement in ` self . contents ` .
"""
for i , child in enumerate ( self . contents ) :
if child is element :
return i
raise ValueError ( " Tag.index: element not in tag " )
def get ( self , key , default = None ) :
""" Returns the value of the ' key ' attribute for the tag, or
the value given for ' default ' if it doesn ' t have that
attribute . """
return self . attrs . get ( key , default )
def get_attribute_list ( self , key , default = None ) :
""" The same as get(), but always returns a list.
: param key : The attribute to look for .
: param default : Use this value if the attribute is not present
on this PageElement .
: return : A list of values , probably containing only a single
value .
"""
value = self . get ( key , default )
if not isinstance ( value , list ) :
value = [ value ]
return value
def has_attr ( self , key ) :
""" Does this PageElement have an attribute with the given name? """
return key in self . attrs
def __hash__ ( self ) :
return str ( self ) . __hash__ ( )
def __getitem__ ( self , key ) :
""" tag[key] returns the value of the ' key ' attribute for the Tag,
and throws an exception if it ' s not there. " " "
return self . attrs [ key ]
def __iter__ ( self ) :
" Iterating over a Tag iterates over its contents. "
return iter ( self . contents )
def __len__ ( self ) :
" The length of a Tag is the length of its list of contents. "
return len ( self . contents )
def __contains__ ( self , x ) :
return x in self . contents
def __bool__ ( self ) :
" A tag is non-None even if it has no contents. "
return True
def __setitem__ ( self , key , value ) :
""" Setting tag[key] sets the value of the ' key ' attribute for the
tag . """
self . attrs [ key ] = value
def __delitem__ ( self , key ) :
" Deleting tag[key] deletes all ' key ' attributes for the tag. "
self . attrs . pop ( key , None )
def __call__ ( self , * args , * * kwargs ) :
""" Calling a Tag like a function is the same as calling its
find_all ( ) method . Eg . tag ( ' a ' ) returns a list of all the A tags
found within this tag . """
return self . find_all ( * args , * * kwargs )
def __getattr__ ( self , tag ) :
""" Calling tag.subtag is the same as calling tag.find(name= " subtag " ) """
#print("Getattr %s.%s" % (self.__class__, tag))
if len ( tag ) > 3 and tag . endswith ( ' Tag ' ) :
# BS3: soup.aTag -> "soup.find("a")
tag_name = tag [ : - 3 ]
warnings . warn (
' . %(name)s Tag is deprecated, use .find( " %(name)s " ) instead. If you really were looking for a tag called %(name)s Tag, use .find( " %(name)s Tag " ) ' % dict (
name = tag_name
2023-01-14 01:03:30 +00:00
) ,
DeprecationWarning
2023-01-12 01:04:47 +00:00
)
return self . find ( tag_name )
# We special case contents to avoid recursion.
elif not tag . startswith ( " __ " ) and not tag == " contents " :
return self . find ( tag )
raise AttributeError (
" ' %s ' object has no attribute ' %s ' " % ( self . __class__ , tag ) )
def __eq__ ( self , other ) :
""" Returns true iff this Tag has the same name, the same attributes,
and the same contents ( recursively ) as ` other ` . """
if self is other :
return True
if ( not hasattr ( other , ' name ' ) or
not hasattr ( other , ' attrs ' ) or
not hasattr ( other , ' contents ' ) or
self . name != other . name or
self . attrs != other . attrs or
len ( self ) != len ( other ) ) :
return False
for i , my_child in enumerate ( self . contents ) :
if my_child != other . contents [ i ] :
return False
return True
def __ne__ ( self , other ) :
""" Returns true iff this Tag is not identical to `other`,
as defined in __eq__ . """
return not self == other
def __repr__ ( self , encoding = " unicode-escape " ) :
""" Renders this PageElement as a string.
2023-01-14 01:03:30 +00:00
: param encoding : The encoding to use ( Python 2 only ) .
TODO : This is now ignored and a warning should be issued
if a value is provided .
: return : A ( Unicode ) string .
2023-01-12 01:04:47 +00:00
"""
2023-01-14 01:03:30 +00:00
# "The return value must be a string object", i.e. Unicode
return self . decode ( )
2023-01-12 01:04:47 +00:00
def __unicode__ ( self ) :
""" Renders this PageElement as a Unicode string. """
return self . decode ( )
2023-01-14 01:03:30 +00:00
__str__ = __repr__ = __unicode__
2023-01-12 01:04:47 +00:00
def encode ( self , encoding = DEFAULT_OUTPUT_ENCODING ,
indent_level = None , formatter = " minimal " ,
errors = " xmlcharrefreplace " ) :
""" Render a bytestring representation of this PageElement and its
contents .
: param encoding : The destination encoding .
: param indent_level : Each line of the rendering will be
2023-01-14 01:03:30 +00:00
indented this many levels . ( The formatter decides what a
' level ' means in terms of spaces or other characters
output . ) Used internally in recursive calls while
pretty - printing .
2023-01-12 01:04:47 +00:00
: param formatter : A Formatter object , or a string naming one of
the standard formatters .
: param errors : An error handling strategy such as
' xmlcharrefreplace ' . This value is passed along into
encode ( ) and its value should be one of the constants
defined by Python .
: return : A bytestring .
"""
# Turn the data structure into Unicode, then encode the
# Unicode.
u = self . decode ( indent_level , encoding , formatter )
return u . encode ( encoding , errors )
def decode ( self , indent_level = None ,
eventual_encoding = DEFAULT_OUTPUT_ENCODING ,
formatter = " minimal " ) :
""" Render a Unicode representation of this PageElement and its
contents .
: param indent_level : Each line of the rendering will be
indented this many spaces . Used internally in
recursive calls while pretty - printing .
: param eventual_encoding : The tag is destined to be
encoded into this encoding . This method is _not_
responsible for performing that encoding . This information
is passed in so that it can be substituted in if the
document contains a < META > tag that mentions the document ' s
encoding .
: param formatter : A Formatter object , or a string naming one of
the standard formatters .
"""
# First off, turn a non-Formatter `formatter` into a Formatter
# object. This will stop the lookup from happening over and
# over again.
if not isinstance ( formatter , Formatter ) :
formatter = self . formatter_for_name ( formatter )
attributes = formatter . attributes ( self )
attrs = [ ]
for key , val in attributes :
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 = formatter . attribute_value ( val )
decoded = (
str ( key ) + ' = '
+ formatter . quoted_attribute_value ( text ) )
attrs . append ( decoded )
close = ' '
closeTag = ' '
prefix = ' '
if self . prefix :
prefix = self . prefix + " : "
if self . is_empty_element :
close = formatter . void_element_close_prefix or ' '
else :
closeTag = ' </ %s %s > ' % ( prefix , self . name )
pretty_print = self . _should_pretty_print ( indent_level )
space = ' '
indent_space = ' '
if indent_level is not None :
2023-01-14 01:03:30 +00:00
indent_space = ( formatter . indent * ( indent_level - 1 ) )
2023-01-12 01:04:47 +00:00
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 ) )
if pretty_print :
s . append ( " \n " )
s . append ( contents )
if pretty_print and contents and contents [ - 1 ] != " \n " :
s . append ( " \n " )
if pretty_print and closeTag :
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 _should_pretty_print ( self , indent_level ) :
""" Should this tag be pretty-printed?
Most of them should , but some ( such as < pre > in HTML
documents ) should not .
"""
return (
indent_level is not None
and (
not self . preserve_whitespace_tags
or self . name not in self . preserve_whitespace_tags
)
)
def prettify ( self , encoding = None , formatter = " minimal " ) :
""" Pretty-print this PageElement as a string.
: param encoding : The eventual encoding of the string . If this is None ,
a Unicode string will be returned .
: param formatter : A Formatter object , or a string naming one of
the standard formatters .
: return : A Unicode string ( if encoding == None ) or a bytestring
( otherwise ) .
"""
if encoding is None :
return self . decode ( True , formatter = formatter )
else :
return self . encode ( encoding , True , formatter = formatter )
def decode_contents ( self , indent_level = None ,
eventual_encoding = DEFAULT_OUTPUT_ENCODING ,
formatter = " minimal " ) :
""" Renders the contents of this tag as a Unicode string.
: param indent_level : Each line of the rendering will be
2023-01-14 01:03:30 +00:00
indented this many levels . ( The formatter decides what a
' level ' means in terms of spaces or other characters
output . ) Used internally in recursive calls while
pretty - printing .
2023-01-12 01:04:47 +00:00
: param eventual_encoding : The tag is destined to be
encoded into this encoding . decode_contents ( ) is _not_
responsible for performing that encoding . This information
is passed in so that it can be substituted in if the
document contains a < META > tag that mentions the document ' s
encoding .
: param formatter : A Formatter object , or a string naming one of
the standard Formatters .
2023-01-14 01:03:30 +00:00
2023-01-12 01:04:47 +00:00
"""
# First off, turn a string formatter into a Formatter object. This
# will stop the lookup from happening over and over again.
if not isinstance ( formatter , 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 ) )
preserve_whitespace = (
self . preserve_whitespace_tags and self . name in self . preserve_whitespace_tags
)
if text and indent_level and not preserve_whitespace :
text = text . strip ( )
if text :
if pretty_print and not preserve_whitespace :
2023-01-14 01:03:30 +00:00
s . append ( formatter . indent * ( indent_level - 1 ) )
2023-01-12 01:04:47 +00:00
s . append ( text )
if pretty_print and not preserve_whitespace :
s . append ( " \n " )
return ' ' . join ( s )
def encode_contents (
self , indent_level = None , encoding = DEFAULT_OUTPUT_ENCODING ,
formatter = " minimal " ) :
""" Renders the contents of this PageElement as a bytestring.
: param indent_level : Each line of the rendering will be
2023-01-14 01:03:30 +00:00
indented this many levels . ( The formatter decides what a
' level ' means in terms of spaces or other characters
output . ) Used internally in recursive calls while
pretty - printing .
2023-01-12 01:04:47 +00:00
: param eventual_encoding : The bytestring will be in this encoding .
: param formatter : A Formatter object , or a string naming one of
the standard Formatters .
: return : A bytestring .
"""
contents = self . decode_contents ( indent_level , encoding , formatter )
return contents . encode ( encoding )
# Old method for BS3 compatibility
def renderContents ( self , encoding = DEFAULT_OUTPUT_ENCODING ,
prettyPrint = False , indentLevel = 0 ) :
""" Deprecated method for BS3 compatibility. """
if not prettyPrint :
indentLevel = None
return self . encode_contents (
indent_level = indentLevel , encoding = encoding )
#Soup methods
2023-01-14 01:03:30 +00:00
def find ( self , name = None , attrs = { } , recursive = True , string = None ,
2023-01-12 01:04:47 +00:00
* * kwargs ) :
""" Look in the children of this PageElement and find the first
PageElement that matches the given criteria .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
: param recursive : If this is True , find ( ) will perform a
recursive search of this PageElement ' s children. Otherwise,
only the direct children will be considered .
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A PageElement .
: rtype : bs4 . element . Tag | bs4 . element . NavigableString
"""
r = None
2023-01-14 01:03:30 +00:00
l = self . find_all ( name , attrs , recursive , string , 1 , * * kwargs )
2023-01-12 01:04:47 +00:00
if l :
r = l [ 0 ]
return r
findChild = find #BS2
2023-01-14 01:03:30 +00:00
def find_all ( self , name = None , attrs = { } , recursive = True , string = None ,
2023-01-12 01:04:47 +00:00
limit = None , * * kwargs ) :
""" Look in the children of this PageElement and find all
PageElements that match the given criteria .
All find_ * methods take a common set of arguments . See the online
documentation for detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
: param recursive : If this is True , find_all ( ) will perform a
recursive search of this PageElement ' s children. Otherwise,
only the direct children will be considered .
: param limit : Stop looking after finding this many results .
: kwargs : A dictionary of filters on attribute values .
: return : A ResultSet of PageElements .
: rtype : bs4 . element . ResultSet
"""
generator = self . descendants
if not recursive :
generator = self . children
2023-01-14 01:03:30 +00:00
return self . _find_all ( name , attrs , string , limit , generator , * * kwargs )
2023-01-12 01:04:47 +00:00
findAll = find_all # BS3
findChildren = find_all # BS2
#Generator methods
@property
def children ( self ) :
""" Iterate over all direct children of this PageElement.
: yield : A sequence of PageElements .
"""
# return iter() to make the purpose of the method clear
return iter ( self . contents ) # XXX This seems to be untested.
@property
def descendants ( self ) :
""" Iterate over all children of this PageElement in a
breadth - first sequence .
: yield : A sequence of PageElements .
"""
if not len ( self . contents ) :
return
stopNode = self . _last_descendant ( ) . next_element
current = self . contents [ 0 ]
while current is not stopNode :
yield current
current = current . next_element
# CSS selector code
def select_one ( self , selector , namespaces = None , * * kwargs ) :
""" Perform a CSS selection operation on the current element.
: param selector : A CSS selector .
: param namespaces : A dictionary mapping namespace prefixes
used in the CSS selector to namespace URIs . By default ,
Beautiful Soup will use the prefixes it encountered while
parsing the document .
: param kwargs : Keyword arguments to be passed into SoupSieve ' s
soupsieve . select ( ) method .
: return : A Tag .
: rtype : bs4 . element . Tag
"""
value = self . select ( selector , namespaces , 1 , * * kwargs )
if value :
return value [ 0 ]
return None
def select ( self , selector , namespaces = None , limit = None , * * kwargs ) :
""" Perform a CSS selection operation on the current element.
This uses the SoupSieve library .
: param selector : A string containing a CSS selector .
: param namespaces : A dictionary mapping namespace prefixes
used in the CSS selector to namespace URIs . By default ,
Beautiful Soup will use the prefixes it encountered while
parsing the document .
: param limit : After finding this number of results , stop looking .
: param kwargs : Keyword arguments to be passed into SoupSieve ' s
soupsieve . select ( ) method .
: return : A ResultSet of Tags .
: rtype : bs4 . element . ResultSet
"""
if namespaces is None :
namespaces = self . _namespaces
if limit is None :
limit = 0
if soupsieve is None :
raise NotImplementedError (
" Cannot execute CSS selectors because the soupsieve package is not installed. "
)
results = soupsieve . select ( selector , self , namespaces , limit , * * kwargs )
# We do this because it's more consistent and because
# ResultSet.__getattr__ has a helpful error message.
return ResultSet ( None , results )
# Old names for backwards compatibility
def childGenerator ( self ) :
""" Deprecated generator. """
return self . children
def recursiveChildGenerator ( self ) :
""" Deprecated generator. """
return self . descendants
def has_key ( self , key ) :
""" Deprecated method. This was kind of misleading because has_key()
( attributes ) was different from __in__ ( contents ) .
has_key ( ) is gone in Python 3 , anyway .
"""
2023-01-14 01:03:30 +00:00
warnings . warn (
' has_key is deprecated. Use has_attr(key) instead. ' ,
DeprecationWarning
)
2023-01-12 01:04:47 +00:00
return self . has_attr ( key )
# Next, a couple classes to represent queries and their results.
class SoupStrainer ( object ) :
""" Encapsulates a number of ways of matching a markup element (tag or
string ) .
This is primarily used to underpin the find_ * methods , but you can
create one yourself and pass it in as ` parse_only ` to the
` BeautifulSoup ` constructor , to parse a subset of a large
document .
"""
2023-01-14 01:03:30 +00:00
def __init__ ( self , name = None , attrs = { } , string = None , * * kwargs ) :
2023-01-12 01:04:47 +00:00
""" Constructor.
The SoupStrainer constructor takes the same arguments passed
into the find_ * methods . See the online documentation for
detailed explanations .
: param name : A filter on tag name .
: param attrs : A dictionary of filters on attribute values .
2023-01-14 01:03:30 +00:00
: param string : A filter for a NavigableString with specific text .
2023-01-12 01:04:47 +00:00
: kwargs : A dictionary of filters on attribute values .
"""
2023-01-14 01:03:30 +00:00
if string is None and ' text ' in kwargs :
string = kwargs . pop ( ' text ' )
warnings . warn (
" The ' text ' argument to the SoupStrainer constructor is deprecated. Use ' string ' instead. " ,
DeprecationWarning
)
2023-01-12 01:04:47 +00:00
self . name = self . _normalize_search_value ( name )
if not isinstance ( attrs , dict ) :
# Treat a non-dict value for attrs as a search for the 'class'
# attribute.
kwargs [ ' class ' ] = attrs
attrs = None
if ' class_ ' in kwargs :
# Treat class_="foo" as a search for the 'class'
# attribute, overriding any non-dict value for attrs.
kwargs [ ' class ' ] = kwargs [ ' class_ ' ]
del kwargs [ ' class_ ' ]
if kwargs :
if attrs :
attrs = attrs . copy ( )
attrs . update ( kwargs )
else :
attrs = kwargs
normalized_attrs = { }
for key , value in list ( attrs . items ( ) ) :
normalized_attrs [ key ] = self . _normalize_search_value ( value )
self . attrs = normalized_attrs
2023-01-14 01:03:30 +00:00
self . string = self . _normalize_search_value ( string )
# DEPRECATED but just in case someone is checking this.
self . text = self . string
2023-01-12 01:04:47 +00:00
def _normalize_search_value ( self , value ) :
# Leave it alone if it's a Unicode string, a callable, a
# regular expression, a boolean, or None.
if ( isinstance ( value , str ) or isinstance ( value , Callable ) or hasattr ( value , ' match ' )
or isinstance ( value , bool ) or value is None ) :
return value
# If it's a bytestring, convert it to Unicode, treating it as UTF-8.
if isinstance ( value , bytes ) :
return value . decode ( " utf8 " )
# If it's listlike, convert it into a list of strings.
if hasattr ( value , ' __iter__ ' ) :
new_value = [ ]
for v in value :
if ( hasattr ( v , ' __iter__ ' ) and not isinstance ( v , bytes )
and not isinstance ( v , str ) ) :
# This is almost certainly the user's mistake. In the
# interests of avoiding infinite loops, we'll let
# it through as-is rather than doing a recursive call.
new_value . append ( v )
else :
new_value . append ( self . _normalize_search_value ( v ) )
return new_value
# Otherwise, convert it into a Unicode string.
# The unicode(str()) thing is so this will do the same thing on Python 2
# and Python 3.
return str ( str ( value ) )
def __str__ ( self ) :
""" A human-readable representation of this SoupStrainer. """
2023-01-14 01:03:30 +00:00
if self . string :
return self . string
2023-01-12 01:04:47 +00:00
else :
return " %s | %s " % ( self . name , self . attrs )
def search_tag ( self , markup_name = None , markup_attrs = { } ) :
""" Check whether a Tag with the given name and attributes would
match this SoupStrainer .
Used prospectively to decide whether to even bother creating a Tag
object .
: param markup_name : A tag name as found in some markup .
: param markup_attrs : A dictionary of attributes as found in some markup .
: return : True if the prospective tag would match this SoupStrainer ;
False otherwise .
"""
found = None
markup = None
if isinstance ( markup_name , Tag ) :
markup = markup_name
markup_attrs = markup
if isinstance ( self . name , str ) :
# Optimization for a very common case where the user is
# searching for a tag with one specific name, and we're
# looking at a tag with a different name.
if markup and not markup . prefix and self . name != markup . name :
return False
call_function_with_tag_data = (
isinstance ( self . name , Callable )
and not isinstance ( markup_name , Tag ) )
if ( ( not self . name )
or call_function_with_tag_data
or ( markup and self . _matches ( markup , self . name ) )
or ( not markup and self . _matches ( markup_name , self . name ) ) ) :
if call_function_with_tag_data :
match = self . name ( markup_name , markup_attrs )
else :
match = True
markup_attr_map = None
for attr , match_against in list ( self . attrs . items ( ) ) :
if not markup_attr_map :
if hasattr ( markup_attrs , ' get ' ) :
markup_attr_map = markup_attrs
else :
markup_attr_map = { }
for k , v in markup_attrs :
markup_attr_map [ k ] = v
attr_value = markup_attr_map . get ( attr )
if not self . _matches ( attr_value , match_against ) :
match = False
break
if match :
if markup :
found = markup
else :
found = markup_name
2023-01-14 01:03:30 +00:00
if found and self . string and not self . _matches ( found . string , self . string ) :
2023-01-12 01:04:47 +00:00
found = None
return found
# For BS3 compatibility.
searchTag = search_tag
def search ( self , markup ) :
""" Find all items in `markup` that match this SoupStrainer.
Used by the core _find_all ( ) method , which is ultimately
called by all find_ * methods .
: param markup : A PageElement or a list of them .
"""
# print('looking for %s in %s' % (self, markup))
found = None
# If given a list of items, scan it for a text element that
# matches.
if hasattr ( markup , ' __iter__ ' ) and not isinstance ( markup , ( Tag , str ) ) :
for element in markup :
if isinstance ( element , NavigableString ) \
and self . search ( element ) :
found = element
break
# If it's a Tag, make sure its name or attributes match.
# Don't bother with Tags if we're searching for text.
elif isinstance ( markup , Tag ) :
2023-01-14 01:03:30 +00:00
if not self . string or self . name or self . attrs :
2023-01-12 01:04:47 +00:00
found = self . search_tag ( markup )
# If it's text, make sure the text matches.
elif isinstance ( markup , NavigableString ) or \
isinstance ( markup , str ) :
2023-01-14 01:03:30 +00:00
if not self . name and not self . attrs and self . _matches ( markup , self . string ) :
2023-01-12 01:04:47 +00:00
found = markup
else :
raise Exception (
" I don ' t know how to match against a %s " % markup . __class__ )
return found
def _matches ( self , markup , match_against , already_tried = None ) :
# print(u"Matching %s against %s" % (markup, match_against))
result = False
if isinstance ( markup , list ) or isinstance ( markup , tuple ) :
# This should only happen when searching a multi-valued attribute
# like 'class'.
for item in markup :
if self . _matches ( item , match_against ) :
return True
# We didn't match any particular value of the multivalue
# attribute, but maybe we match the attribute value when
# considered as a string.
if self . _matches ( ' ' . join ( markup ) , match_against ) :
return True
return False
if match_against is True :
# True matches any non-None value.
return markup is not None
if isinstance ( match_against , Callable ) :
return match_against ( markup )
# Custom callables take the tag as an argument, but all
# other ways of matching match the tag name as a string.
original_markup = markup
if isinstance ( markup , Tag ) :
markup = markup . name
# Ensure that `markup` is either a Unicode string, or None.
markup = self . _normalize_search_value ( markup )
if markup is None :
# None matches None, False, an empty string, an empty list, and so on.
return not match_against
if ( hasattr ( match_against , ' __iter__ ' )
and not isinstance ( match_against , str ) ) :
# We're asked to match against an iterable of items.
# The markup must be match at least one item in the
# iterable. We'll try each one in turn.
#
# To avoid infinite recursion we need to keep track of
# items we've already seen.
if not already_tried :
already_tried = set ( )
for item in match_against :
if item . __hash__ :
key = item
else :
key = id ( item )
if key in already_tried :
continue
else :
already_tried . add ( key )
if self . _matches ( original_markup , item , already_tried ) :
return True
else :
return False
# Beyond this point we might need to run the test twice: once against
# the tag's name and once against its prefixed name.
match = False
if not match and isinstance ( match_against , str ) :
# Exact string match
match = markup == match_against
if not match and hasattr ( match_against , ' search ' ) :
# Regexp match
return match_against . search ( markup )
if ( not match
and isinstance ( original_markup , Tag )
and original_markup . prefix ) :
# Try the whole thing again with the prefixed tag name.
return self . _matches (
original_markup . prefix + ' : ' + original_markup . name , match_against
)
return match
class ResultSet ( list ) :
""" A ResultSet is just a list that keeps track of the SoupStrainer
that created it . """
def __init__ ( self , source , result = ( ) ) :
""" Constructor.
: param source : A SoupStrainer .
: param result : A list of PageElements .
"""
super ( ResultSet , self ) . __init__ ( result )
self . source = source
def __getattr__ ( self , key ) :
""" Raise a helpful exception to explain a common code fix. """
raise AttributeError (
" ResultSet object has no attribute ' %s ' . You ' re probably treating a list of elements like a single element. Did you call find_all() when you meant to call find()? " % key
)