diff options
Diffstat (limited to 'bs4')
-rw-r--r-- | bs4/__init__.py | 11 | ||||
-rw-r--r-- | bs4/dammit.py | 91 | ||||
-rw-r--r-- | bs4/element.py | 435 | ||||
-rw-r--r-- | bs4/testing.py | 6 | ||||
-rw-r--r-- | bs4/util.py | 23 |
5 files changed, 339 insertions, 227 deletions
diff --git a/bs4/__init__.py b/bs4/__init__.py index e0eba75..66a1c02 100644 --- a/bs4/__init__.py +++ b/bs4/__init__.py @@ -65,7 +65,7 @@ class BeautifulSoup(Tag): # can be replaced with a single space. A text node that contains # fancy Unicode spaces (usually non-breaking) should be left # alone. - STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } + STRIP_ASCII_SPACES = {9: None, 10: None, 12: None, 13: None, 32: None, } def __init__(self, markup="", features=None, builder=None, parse_only=None, from_encoding=None): @@ -167,7 +167,6 @@ class BeautifulSoup(Tag): self.previous = o self.currentTag.contents.append(o) - def _popToTag(self, name, inclusivePop=True): """Pops the tag stack up to and including the most recent instance of the given tag. If inclusivePop is false, pops the tag @@ -179,9 +178,10 @@ class BeautifulSoup(Tag): numPops = 0 mostRecentTag = None - for i in range(len(self.tagStack)-1, 0, -1): + + for i in range(len(self.tagStack) - 1, 0, -1): if name == self.tagStack[i].name: - numPops = len(self.tagStack)-i + numPops = len(self.tagStack) - i break if not inclusivePop: numPops = numPops - 1 @@ -204,7 +204,7 @@ class BeautifulSoup(Tag): if (self.parse_only and len(self.tagStack) <= 1 and (self.parse_only.text - or not self.parse_only.searchTag(name, attrs))): + or not self.parse_only.search_tag(name, attrs))): return None tag = Tag(self, self.builder, name, attrs, self.currentTag, @@ -217,7 +217,6 @@ class BeautifulSoup(Tag): self.pushTag(tag) return tag - def handle_endtag(self, name): #print "End tag: " + name self.endData() diff --git a/bs4/dammit.py b/bs4/dammit.py index 75d445e..f3e770e 100644 --- a/bs4/dammit.py +++ b/bs4/dammit.py @@ -42,7 +42,7 @@ class EntitySubstitution(object): # There's no point in turning the quotation mark into # ", unless it happens within an attribute value, which # is handled elsewhere. - continue; + continue character = unichr(codepoint) characters.append(character) lookup[character] = name @@ -52,13 +52,12 @@ class EntitySubstitution(object): (CHARACTER_TO_HTML_ENTITY, HTML_ENTITY_TO_CHARACTER, CHARACTER_TO_HTML_ENTITY_RE) = _populate_class_variables() - CHARACTER_TO_XML_ENTITY = { - "'" : "apos", - '"' : "quot", - "&" : "amp", - "<" : "lt", - ">" : "gt", + "'": "apos", + '"': "quot", + "&": "amp", + "<": "lt", + ">": "gt", } BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" @@ -157,8 +156,8 @@ class UnicodeDammit: # meta tags to the corresponding Python codec names. It only covers # values that aren't in Python's aliases and can't be determined # by the heuristics in find_codec. - CHARSET_ALIASES = { "macintosh" : "mac-roman", - "x-sjis" : "shift-jis" } + CHARSET_ALIASES = {"macintosh": "mac-roman", + "x-sjis": "shift-jis"} ENCODINGS_WITH_SMART_QUOTES = [ "windows-1252", @@ -198,7 +197,8 @@ class UnicodeDammit: break self.unicode = u - if not u: self.original_encoding = None + if not u: + self.original_encoding = None def _sub_ms_char(self, match): """Changes a MS smart quote character to an XML or HTML @@ -335,7 +335,6 @@ class UnicodeDammit: xml_encoding = sniffed_xml_encoding return xml_data, xml_encoding, sniffed_xml_encoding - def find_codec(self, charset): return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \ or (charset and self._codec(charset.replace("-", ""))) \ @@ -343,7 +342,8 @@ class UnicodeDammit: or charset def _codec(self, charset): - if not charset: return charset + if not charset: + return charset codec = None try: codecs.lookup(charset) @@ -353,6 +353,7 @@ class UnicodeDammit: return codec EBCDIC_TO_ASCII_MAP = None + def _ebcdic_to_ascii(self, s): c = self.__class__ if not c.EBCDIC_TO_ASCII_MAP: @@ -374,39 +375,39 @@ class UnicodeDammit: 90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57, 250,251,252,253,254,255) import string - c.EBCDIC_TO_ASCII_MAP = string.maketrans( \ + c.EBCDIC_TO_ASCII_MAP = string.maketrans( ''.join(map(chr, range(256))), ''.join(map(chr, emap))) return s.translate(c.EBCDIC_TO_ASCII_MAP) - MS_CHARS = { '\x80' : ('euro', '20AC'), - '\x81' : ' ', - '\x82' : ('sbquo', '201A'), - '\x83' : ('fnof', '192'), - '\x84' : ('bdquo', '201E'), - '\x85' : ('hellip', '2026'), - '\x86' : ('dagger', '2020'), - '\x87' : ('Dagger', '2021'), - '\x88' : ('circ', '2C6'), - '\x89' : ('permil', '2030'), - '\x8A' : ('Scaron', '160'), - '\x8B' : ('lsaquo', '2039'), - '\x8C' : ('OElig', '152'), - '\x8D' : '?', - '\x8E' : ('#x17D', '17D'), - '\x8F' : '?', - '\x90' : '?', - '\x91' : ('lsquo', '2018'), - '\x92' : ('rsquo', '2019'), - '\x93' : ('ldquo', '201C'), - '\x94' : ('rdquo', '201D'), - '\x95' : ('bull', '2022'), - '\x96' : ('ndash', '2013'), - '\x97' : ('mdash', '2014'), - '\x98' : ('tilde', '2DC'), - '\x99' : ('trade', '2122'), - '\x9a' : ('scaron', '161'), - '\x9b' : ('rsaquo', '203A'), - '\x9c' : ('oelig', '153'), - '\x9d' : '?', - '\x9e' : ('#x17E', '17E'), - '\x9f' : ('Yuml', ''),} + MS_CHARS = {'\x80': ('euro', '20AC'), + '\x81': ' ', + '\x82': ('sbquo', '201A'), + '\x83': ('fnof', '192'), + '\x84': ('bdquo', '201E'), + '\x85': ('hellip', '2026'), + '\x86': ('dagger', '2020'), + '\x87': ('Dagger', '2021'), + '\x88': ('circ', '2C6'), + '\x89': ('permil', '2030'), + '\x8A': ('Scaron', '160'), + '\x8B': ('lsaquo', '2039'), + '\x8C': ('OElig', '152'), + '\x8D': '?', + '\x8E': ('#x17D', '17D'), + '\x8F': '?', + '\x90': '?', + '\x91': ('lsquo', '2018'), + '\x92': ('rsquo', '2019'), + '\x93': ('ldquo', '201C'), + '\x94': ('rdquo', '201D'), + '\x95': ('bull', '2022'), + '\x96': ('ndash', '2013'), + '\x97': ('mdash', '2014'), + '\x98': ('tilde', '2DC'), + '\x99': ('trade', '2122'), + '\x9a': ('scaron', '161'), + '\x9b': ('rsaquo', '203A'), + '\x9c': ('oelig', '153'), + '\x9d': '?', + '\x9e': ('#x17E', '17E'), + '\x9f': ('Yuml', ''),} diff --git a/bs4/element.py b/bs4/element.py index 6fb6210..e141aa8 100644 --- a/bs4/element.py +++ b/bs4/element.py @@ -11,6 +11,23 @@ from util import isList DEFAULT_OUTPUT_ENCODING = "utf-8" +def _match_css_class(str): + """Build a RE to match the given CSS class.""" + return re.compile(r"(^|.*\s)%s($|\s)" % str) + + +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 + + class PageElement(object): """Contains the navigational information for some part of the page (either a tag or a piece of text)""" @@ -21,122 +38,132 @@ class PageElement(object): self.parent = parent self.previous = previous self.next = None - self.previousSibling = None - self.nextSibling = None + self.previous_sibling = None + self.next_sibling = None if self.parent and self.parent.contents: - self.previousSibling = self.parent.contents[-1] - self.previousSibling.nextSibling = self + self.previous_sibling = self.parent.contents[-1] + self.previous_sibling.next_sibling = self + + nextSibling = _alias("next_sibling") # BS3 + previousSibling = _alias("previous_sibling") # BS3 def replace_with(self, replace_with): - oldParent = self.parent - myIndex = self.parent.contents.index(self) - if hasattr(replace_with, 'parent') and replace_with.parent == self.parent: + if replace_with is self: + return + old_parent = self.parent + my_index = self.parent.index(self) + if (hasattr(replace_with, 'parent') + and replace_with.parent is self.parent): # We're replacing this element with one of its siblings. - index = self.parent.contents.index(replace_with) - if index and index < myIndex: + if self.parent.index(replace_with) < my_index: # Furthermore, it comes before this element. That # means that when we extract it, the index of this # element will change. - myIndex = myIndex - 1 + my_index -= 1 self.extract() - oldParent.insert(myIndex, replace_with) - replaceWith = replace_with # BS4 + old_parent.insert(my_index, replace_with) + replaceWith = replace_with # BS3 + + def replace_with_children(self): + my_parent = self.parent + my_index = self.parent.index(self) + self.extract() + for child in reversed(self.contents[:]): + my_parent.insert(my_index, child) + replaceWithChildren = replace_with_children # BS3 def extract(self): """Destructively rips this element out of the tree.""" if self.parent: - try: - self.parent.contents.remove(self) - except ValueError: - pass + del self.parent.contents[self.parent.index(self)] #Find the two elements that would be next to each other if #this element (and any children) hadn't been parsed. Connect #the two. - lastChild = self._last_recursive_child() - nextElement = lastChild.next + last_child = self._last_recursive_child() + next_element = last_child.next if self.previous: - self.previous.next = nextElement - if nextElement: - nextElement.previous = self.previous + self.previous.next = next_element + if next_element: + next_element.previous = self.previous self.previous = None - lastChild.next = None + last_child.next = None self.parent = None - if self.previousSibling: - self.previousSibling.nextSibling = self.nextSibling - if self.nextSibling: - self.nextSibling.previousSibling = self.previousSibling - self.previousSibling = self.nextSibling = None + if self.previous_sibling: + self.previous_sibling.next_sibling = self.next_sibling + if self.next_sibling: + self.next_sibling.previous_sibling = self.previous_sibling + self.previous_sibling = self.next_sibling = None return self def _last_recursive_child(self): "Finds the last element beneath this object to be parsed." - lastChild = self - while hasattr(lastChild, 'contents') and lastChild.contents: - lastChild = lastChild.contents[-1] - return lastChild - - def insert(self, position, newChild): - if (isinstance(newChild, basestring) - or isinstance(newChild, unicode)) \ - and not isinstance(newChild, NavigableString): - newChild = NavigableString(newChild) - - position = min(position, len(self.contents)) - if hasattr(newChild, 'parent') and newChild.parent != None: + last_child = self + while hasattr(last_child, 'contents') and last_child.contents: + last_child = last_child.contents[-1] + return last_child + # BS3: Not part of the API! + _lastRecursiveChild = _last_recursive_child + + def insert(self, position, new_child): + if (isinstance(new_child, basestring) + and not isinstance(new_child, NavigableString)): + new_child = NavigableString(new_child) + + 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 newChild.parent == self: - index = self.find(newChild) - if index and index < position: + if new_child.parent is self: + if self.index(new_child) > position: # Furthermore we're moving it 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 = position - 1 - newChild.extract() + position -= 1 + new_child.extract() - newChild.parent = self - previousChild = None + new_child.parent = self + previous_child = None if position == 0: - newChild.previousSibling = None - newChild.previous = self + new_child.previous_sibling = None + new_child.previous = self else: - previousChild = self.contents[position-1] - newChild.previousSibling = previousChild - newChild.previousSibling.nextSibling = newChild - newChild.previous = previousChild._last_recursive_child() - if newChild.previous: - newChild.previous.next = newChild + previous_child = self.contents[position - 1] + new_child.previous_sibling = previous_child + new_child.previous_sibling.next_sibling = new_child + new_child.previous = previous_child._last_recursive_child() + if new_child.previous: + new_child.previous.next = new_child - newChildsLastElement = newChild._last_recursive_child() + new_childs_last_element = new_child._last_recursive_child() if position >= len(self.contents): - newChild.nextSibling = None + new_child.next_sibling = None parent = self - parentsNextSibling = None - while not parentsNextSibling: - parentsNextSibling = parent.nextSibling + parents_next_sibling = None + while not parents_next_sibling: + parents_next_sibling = parent.next_sibling parent = parent.parent - if not parent: # This is the last element in the document. + if not parent: # This is the last element in the document. break - if parentsNextSibling: - newChildsLastElement.next = parentsNextSibling + if parents_next_sibling: + new_childs_last_element.next = parents_next_sibling else: - newChildsLastElement.next = None + new_childs_last_element.next = None else: - nextChild = self.contents[position] - newChild.nextSibling = nextChild - if newChild.nextSibling: - newChild.nextSibling.previousSibling = newChild - newChildsLastElement.next = nextChild + next_child = self.contents[position] + new_child.next_sibling = next_child + if new_child.next_sibling: + new_child.next_sibling.previous_sibling = new_child + new_childs_last_element.next = next_child - if newChildsLastElement.next: - newChildsLastElement.next.previous = newChildsLastElement - self.contents.insert(position, newChild) + if new_childs_last_element.next: + new_childs_last_element.next.previous = new_childs_last_element + self.contents.insert(position, new_child) def append(self, tag): """Appends the given tag to the contents of this tag.""" @@ -146,7 +173,7 @@ class PageElement(object): """Returns the first item that matches the given criteria and appears after this Tag in the document.""" return self._find_one(self.find_all_next, name, attrs, text, **kwargs) - findNext = find_next # BS3 + findNext = find_next # BS3 def find_all_next(self, name=None, attrs={}, text=None, limit=None, **kwargs): @@ -154,14 +181,14 @@ class PageElement(object): after this Tag in the document.""" return self._find_all(name, attrs, text, limit, self.next_elements, **kwargs) - findAllNext = find_all_next # BS3 + findAllNext = find_all_next # BS3 def find_next_sibling(self, name=None, attrs={}, text=None, **kwargs): """Returns the closest sibling to this Tag that matches the given criteria and appears after this Tag in the document.""" return self._find_one(self.find_next_siblings, name, attrs, text, **kwargs) - findNextSibling = find_next_sibling # BS3 + findNextSibling = find_next_sibling # BS3 def find_next_siblings(self, name=None, attrs={}, text=None, limit=None, **kwargs): @@ -169,15 +196,15 @@ class PageElement(object): criteria and appear after this Tag in the document.""" return self._find_all(name, attrs, text, limit, self.next_siblings, **kwargs) - findNextSiblings = find_next_siblings # BS3 - fetchNextSiblings = find_next_siblings # BS2 + findNextSiblings = find_next_siblings # BS3 + fetchNextSiblings = find_next_siblings # BS2 def find_previous(self, name=None, attrs={}, text=None, **kwargs): """Returns the first item that matches the given criteria and appears before this Tag in the document.""" return self._find_one( self.find_all_previous, name, attrs, text, **kwargs) - findPrevious = find_previous # BS3 + findPrevious = find_previous # BS3 def find_all_previous(self, name=None, attrs={}, text=None, limit=None, **kwargs): @@ -185,15 +212,15 @@ class PageElement(object): before this Tag in the document.""" return self._find_all(name, attrs, text, limit, self.previous_elements, **kwargs) - findAllPrevious = find_all_previous # BS3 - fetchPrevious = find_all_previous # BS2 + findAllPrevious = find_all_previous # BS3 + fetchPrevious = find_all_previous # BS2 def find_previous_sibling(self, name=None, attrs={}, text=None, **kwargs): """Returns the closest sibling to this Tag that matches the given criteria and appears before this Tag in the document.""" return self._find_one(self.find_previous_siblings, name, attrs, text, **kwargs) - findPreviousSibling = find_previous_sibling # BS3 + findPreviousSibling = find_previous_sibling # BS3 def find_previous_siblings(self, name=None, attrs={}, text=None, limit=None, **kwargs): @@ -201,8 +228,8 @@ class PageElement(object): criteria and appear before this Tag in the document.""" return self._find_all(name, attrs, text, limit, self.previous_siblings, **kwargs) - findPreviousSiblings = find_previous_siblings # BS3 - fetchPreviousSiblings = find_previous_siblings # BS2 + findPreviousSiblings = find_previous_siblings # BS3 + fetchPreviousSiblings = find_previous_siblings # BS2 def find_parent(self, name=None, attrs={}, **kwargs): """Returns the closest parent of this Tag that matches the given @@ -214,7 +241,7 @@ class PageElement(object): if l: r = l[0] return r - findParent = find_parent # BS3 + findParent = find_parent # BS3 def find_parents(self, name=None, attrs={}, limit=None, **kwargs): """Returns the parents of this Tag that match the given @@ -222,8 +249,8 @@ class PageElement(object): return self._find_all(name, attrs, None, limit, self.parents, **kwargs) - findParents = find_parents # BS3 - fetchParents = find_parents # BS2 + findParents = find_parents # BS3 + fetchParents = find_parents # BS2 #These methods do the real heavy lifting. @@ -239,6 +266,17 @@ class PageElement(object): if isinstance(name, SoupStrainer): strainer = name + elif text is None and not limit and not attrs and not kwargs: + # findAll*(True) + if name is True or name is None: + return [element for element in generator + if isinstance(element, Tag)] + # findAll*('tag-name') + elif isinstance(name, basestring): + return [element for element in generator + if isinstance(element, Tag) and element.name == name] + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) else: # Build a SoupStrainer strainer = SoupStrainer(name, attrs, text, **kwargs) @@ -261,35 +299,35 @@ class PageElement(object): @property def next_elements(self): i = self - while i: + while i is not None: i = i.next yield i @property def next_siblings(self): i = self - while i: - i = i.nextSibling + while i is not None: + i = i.next_sibling yield i @property def previous_elements(self): i = self - while i: + while i is not None: i = i.previous yield i @property def previous_siblings(self): i = self - while i: - i = i.previousSibling + while i is not None: + i = i.previous_sibling yield i @property def parents(self): i = self - while i: + while i is not None: i = i.parent yield i @@ -343,7 +381,8 @@ class NavigableString(unicode, PageElement): if attr == 'string': return self else: - raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + raise AttributeError("'%s' object has no attribute '%s'" % + (self.__class__.__name__, attr)) def output_ready(self, substitute_html_entities=False): if substitute_html_entities: @@ -402,9 +441,9 @@ class Tag(PageElement): # We don't actually store the parser object: that lets extracted # chunks be garbage-collected. - self.parserClass = parser.__class__ + self.parser_class = parser.__class__ self.name = name - if attrs == None: + if attrs is None: attrs = {} else: attrs = dict(attrs) @@ -418,6 +457,8 @@ class Tag(PageElement): self.can_be_empty_element = builder.can_be_empty_element(name) + parserClass = _alias("parser_class") # BS3 + @property def is_empty_element(self): """Is this tag an empty-element tag? (aka a self-closing tag) @@ -434,8 +475,7 @@ class Tag(PageElement): 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 - + isSelfClosing = is_empty_element # BS3 @property def string(self): @@ -454,6 +494,60 @@ class Tag(PageElement): return child return child.string + @string.setter + def string(self, string): + self.clear() + self.append(string) + + def get_text(self, separator=u"", strip=False): + """ + Get all child strings, concatenated using the given separator + """ + if strip: + return separator.join(string.strip() + for string in self.recursive_children + if isinstance(string, NavigableString) and string.strip()) + else: + return separator.join(string + for string in self.recursive_children + if isinstance(string, NavigableString)) + getText = get_text + + text = property(get_text) + + def decompose(self): + """Recursively destroys the contents of this tree.""" + self.extract() + i = self + while i is not None: + next = i.next + i.__dict__.clear() + i = next + + def clear(self, decompose=False): + """ + Extract all children. If decompose is True, decompose instead. + """ + 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 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. + """ + 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 @@ -461,7 +555,7 @@ class Tag(PageElement): return self.attrs.get(key, default) def has_key(self, key): - return self.attrs.has_key(key) + return key in self.attrs def __getitem__(self, key): """tag[key] returns the value of the 'key' attribute for the tag, @@ -490,8 +584,7 @@ class Tag(PageElement): def __delitem__(self, key): "Deleting tag[key] deletes all 'key' attributes for the tag." - if self.attrs.has_key(key): - del self.attrs[key] + self.attrs.pop(key, None) def __call__(self, *args, **kwargs): """Calling a tag like a function is the same as calling its @@ -501,19 +594,27 @@ class Tag(PageElement): def __getattr__(self, tag): #print "Getattr %s.%s" % (self.__class__, tag) - if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + if len(tag) > 3 and tag.endswith('Tag'): return self.find(tag[:-3]) - elif tag.find('__') != 0: + elif not tag.startswith("__"): return self.find(tag) - raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, 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 the given tag.""" - 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): + 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 in range(0, len(self.contents)): - if self.contents[i] != other.contents[i]: + for i, my_child in enumerate(self.contents): + if my_child != other.contents[i]: return False return True @@ -574,7 +675,7 @@ class Tag(PageElement): pretty_print = (indent_level is not None) if pretty_print: - space = (' ' * (indent_level-1)) + space = (' ' * (indent_level - 1)) indent_contents = indent_level + 1 else: space = '' @@ -587,12 +688,12 @@ class Tag(PageElement): s = contents else: s = [] - attributeString = '' + attribute_string = '' if attrs: - attributeString = ' ' + ' '.join(attrs) + attribute_string = ' ' + ' '.join(attrs) if pretty_print: s.append(space) - s.append('<%s%s%s>' % (self.name, attributeString, close)) + s.append('<%s%s%s>' % (self.name, attribute_string, close)) if pretty_print: s.append("\n") s.append(contents) @@ -601,21 +702,11 @@ class Tag(PageElement): if pretty_print and closeTag: s.append(space) s.append(closeTag) - if pretty_print and closeTag and self.nextSibling: + if pretty_print and closeTag and self.next_sibling: s.append("\n") s = ''.join(s) return s - def decompose(self): - """Recursively destroys the contents of this tree.""" - contents = [i for i in self.contents] - for i in contents: - if isinstance(i, Tag): - i.decompose() - else: - i.extract() - self.extract() - def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): return self.encode(encoding, True) @@ -632,7 +723,7 @@ class Tag(PageElement): encoding. """ pretty_print = (indent_level is not None) - s=[] + s = [] for c in self: text = None if isinstance(c, NavigableString): @@ -644,7 +735,7 @@ class Tag(PageElement): text = text.strip() if text: if pretty_print: - s.append(" " * (indent_level-1)) + s.append(" " * (indent_level - 1)) s.append(text) if pretty_print: s.append("\n") @@ -678,18 +769,19 @@ class Tag(PageElement): if not recursive: generator = self.children return self._find_all(name, attrs, text, limit, generator, **kwargs) - findAll = find_all # BS3 - findChildren = find_all # BS2 + findAll = find_all # BS3 + findChildren = find_all # BS2 #Generator methods @property def children(self): - return iter(self.contents) # XXX This seems to be untested. + # return iter() to make the purpose of the method clear + return iter(self.contents) # XXX This seems to be untested. @property def recursive_children(self): if not len(self.contents): - raise StopIteration # XXX return instead? + return stopNode = self._last_recursive_child().next current = self.contents[0] while current is not stopNode: @@ -712,7 +804,7 @@ class SoupStrainer(object): def __init__(self, name=None, attrs={}, text=None, **kwargs): self.name = name if isinstance(attrs, basestring): - kwargs['class'] = attrs + kwargs['class'] = _match_css_class(attrs) attrs = None if kwargs: if attrs: @@ -729,42 +821,43 @@ class SoupStrainer(object): else: return "%s|%s" % (self.name, self.attrs) - def searchTag(self, markupName=None, markupAttrs={}): + def search_tag(self, markup_name=None, markup_attrs={}): found = None markup = None - if isinstance(markupName, Tag): - markup = markupName - markupAttrs = markup - callFunctionWithTagData = callable(self.name) \ - and not isinstance(markupName, Tag) + if isinstance(markup_name, Tag): + markup = markup_name + markup_attrs = markup + call_function_with_tag_data = callable(self.name) \ + and not isinstance(markup_name, Tag) if (not self.name) \ - or callFunctionWithTagData \ + or call_function_with_tag_data \ or (markup and self._matches(markup, self.name)) \ - or (not markup and self._matches(markupName, self.name)): - if callFunctionWithTagData: - match = self.name(markupName, markupAttrs) + 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 - markupAttrMap = None - for attr, matchAgainst in self.attrs.items(): - if not markupAttrMap: - if hasattr(markupAttrs, 'get'): - markupAttrMap = markupAttrs - else: - markupAttrMap = {} - for k,v in markupAttrs: - markupAttrMap[k] = v - attrValue = markupAttrMap.get(attr) - if not self._matches(attrValue, matchAgainst): + markup_attr_map = None + for attr, match_against in 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 = markupName + found = markup_name return found + searchTag = search_tag def search(self, markup): #print 'looking for %s in %s' % (self, markup) @@ -781,24 +874,24 @@ class SoupStrainer(object): # Don't bother with Tags if we're searching for text. elif isinstance(markup, Tag): if not self.text: - found = self.searchTag(markup) + found = self.search_tag(markup) # If it's text, make sure the text matches. elif isinstance(markup, NavigableString) or \ isinstance(markup, basestring): if self._matches(markup, self.text): found = markup else: - raise Exception, "I don't know how to match against a %s" \ - % markup.__class__ + raise Exception("I don't know how to match against a %s" + % markup.__class__) return found - def _matches(self, markup, matchAgainst): - #print "Matching %s against %s" % (markup, matchAgainst) + def _matches(self, markup, match_against): + #print "Matching %s against %s" % (markup, match_against) result = False - if matchAgainst == True and type(matchAgainst) == types.BooleanType: - result = markup != None - elif callable(matchAgainst): - result = matchAgainst(markup) + if match_against is True: + result = markup is not None + elif callable(match_against): + result = match_against(markup) else: #Custom match methods take the tag as an argument, but all #other ways of matching match the tag name as a string. @@ -807,23 +900,23 @@ class SoupStrainer(object): if markup is not None and not isinstance(markup, basestring): markup = unicode(markup) #Now we know that chunk is either a string, or None. - if hasattr(matchAgainst, 'match'): + if hasattr(match_against, 'match'): # It's a regexp object. - result = markup and matchAgainst.search(markup) - elif (isList(matchAgainst) + result = markup and match_against.search(markup) + elif (isList(match_against) and (markup is not None - or not isinstance(matchAgainst, basestring))): - result = markup in matchAgainst - elif hasattr(matchAgainst, 'items'): - result = markup.has_key(matchAgainst) - elif matchAgainst and isinstance(markup, basestring): + or not isinstance(match_against, basestring))): + result = markup in match_against + elif hasattr(match_against, 'items'): + result = match_against in markup + elif match_against and isinstance(markup, basestring): if isinstance(markup, unicode): - matchAgainst = unicode(matchAgainst) + match_against = unicode(match_against) else: - matchAgainst = str(matchAgainst) + match_against = str(match_against) if not result: - result = matchAgainst == markup + result = match_against == markup return result diff --git a/bs4/testing.py b/bs4/testing.py index 9d9c26a..91c623a 100644 --- a/bs4/testing.py +++ b/bs4/testing.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup from bs4.element import Comment, SoupStrainer from bs4.builder import LXMLTreeBuilder + class SoupTest(unittest.TestCase): @property @@ -30,8 +31,3 @@ class SoupTest(unittest.TestCase): compare_parsed_to = to_parse self.assertEquals(obj.decode(), self.document_for(compare_parsed_to)) - - - - - diff --git a/bs4/util.py b/bs4/util.py new file mode 100644 index 0000000..8e33273 --- /dev/null +++ b/bs4/util.py @@ -0,0 +1,23 @@ +# Helper functions and mixin classes for Beautiful Soup + +import types +try: + set +except NameError: + from sets import Set as set + + +def isList(l): + """Convenience method that works with all 2.x versions of Python + to determine whether or not something is listlike.""" + return ((hasattr(l, '__iter__') and not isinstance(l, basestring)) + or (type(l) in (types.ListType, types.TupleType))) + + +def buildSet(args=None): + """Turns a list or a string into a set.""" + if isinstance(args, str): + return set([args]) + if args is None: + return set() + return set(args) |