comparison BeautifulSoup.py @ 17:a3af29edcce2

Uploaded Miller Lab Devshed version a51c894f5bed
author miller-lab
date Fri, 28 Sep 2012 11:57:18 -0400
parents 2c498d40ecde
children
comparison
equal deleted inserted replaced
16:be0e2223c531 17:a3af29edcce2
1 """Beautiful Soup
2 Elixir and Tonic
3 "The Screen-Scraper's Friend"
4 http://www.crummy.com/software/BeautifulSoup/
5
6 Beautiful Soup parses a (possibly invalid) XML or HTML document into a
7 tree representation. It provides methods and Pythonic idioms that make
8 it easy to navigate, search, and modify the tree.
9
10 A well-formed XML/HTML document yields a well-formed data
11 structure. An ill-formed XML/HTML document yields a correspondingly
12 ill-formed data structure. If your document is only locally
13 well-formed, you can use this library to find and process the
14 well-formed part of it.
15
16 Beautiful Soup works with Python 2.2 and up. It has no external
17 dependencies, but you'll have more success at converting data to UTF-8
18 if you also install these three packages:
19
20 * chardet, for auto-detecting character encodings
21 http://chardet.feedparser.org/
22 * cjkcodecs and iconv_codec, which add more encodings to the ones supported
23 by stock Python.
24 http://cjkpython.i18n.org/
25
26 Beautiful Soup defines classes for two main parsing strategies:
27
28 * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific
29 language that kind of looks like XML.
30
31 * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid
32 or invalid. This class has web browser-like heuristics for
33 obtaining a sensible parse tree in the face of common HTML errors.
34
35 Beautiful Soup also defines a class (UnicodeDammit) for autodetecting
36 the encoding of an HTML or XML document, and converting it to
37 Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser.
38
39 For more than you ever wanted to know about Beautiful Soup, see the
40 documentation:
41 http://www.crummy.com/software/BeautifulSoup/documentation.html
42
43 Here, have some legalese:
44
45 Copyright (c) 2004-2010, Leonard Richardson
46
47 All rights reserved.
48
49 Redistribution and use in source and binary forms, with or without
50 modification, are permitted provided that the following conditions are
51 met:
52
53 * Redistributions of source code must retain the above copyright
54 notice, this list of conditions and the following disclaimer.
55
56 * Redistributions in binary form must reproduce the above
57 copyright notice, this list of conditions and the following
58 disclaimer in the documentation and/or other materials provided
59 with the distribution.
60
61 * Neither the name of the the Beautiful Soup Consortium and All
62 Night Kosher Bakery nor the names of its contributors may be
63 used to endorse or promote products derived from this software
64 without specific prior written permission.
65
66 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
67 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
68 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
69 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
70 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
71 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
72 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
73 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
74 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
75 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
76 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT.
77
78 """
79 from __future__ import generators
80
81 __author__ = "Leonard Richardson (leonardr@segfault.org)"
82 __version__ = "3.2.0"
83 __copyright__ = "Copyright (c) 2004-2010 Leonard Richardson"
84 __license__ = "New-style BSD"
85
86 from sgmllib import SGMLParser, SGMLParseError
87 import codecs
88 import markupbase
89 import types
90 import re
91 import sgmllib
92 try:
93 from htmlentitydefs import name2codepoint
94 except ImportError:
95 name2codepoint = {}
96 try:
97 set
98 except NameError:
99 from sets import Set as set
100
101 #These hacks make Beautiful Soup able to parse XML with namespaces
102 sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
103 markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match
104
105 DEFAULT_OUTPUT_ENCODING = "utf-8"
106
107 def _match_css_class(str):
108 """Build a RE to match the given CSS class."""
109 return re.compile(r"(^|.*\s)%s($|\s)" % str)
110
111 # First, the classes that represent markup elements.
112
113 class PageElement(object):
114 """Contains the navigational information for some part of the page
115 (either a tag or a piece of text)"""
116
117 def setup(self, parent=None, previous=None):
118 """Sets up the initial relations between this element and
119 other elements."""
120 self.parent = parent
121 self.previous = previous
122 self.next = None
123 self.previousSibling = None
124 self.nextSibling = None
125 if self.parent and self.parent.contents:
126 self.previousSibling = self.parent.contents[-1]
127 self.previousSibling.nextSibling = self
128
129 def replaceWith(self, replaceWith):
130 oldParent = self.parent
131 myIndex = self.parent.index(self)
132 if hasattr(replaceWith, "parent")\
133 and replaceWith.parent is self.parent:
134 # We're replacing this element with one of its siblings.
135 index = replaceWith.parent.index(replaceWith)
136 if index and index < myIndex:
137 # Furthermore, it comes before this element. That
138 # means that when we extract it, the index of this
139 # element will change.
140 myIndex = myIndex - 1
141 self.extract()
142 oldParent.insert(myIndex, replaceWith)
143
144 def replaceWithChildren(self):
145 myParent = self.parent
146 myIndex = self.parent.index(self)
147 self.extract()
148 reversedChildren = list(self.contents)
149 reversedChildren.reverse()
150 for child in reversedChildren:
151 myParent.insert(myIndex, child)
152
153 def extract(self):
154 """Destructively rips this element out of the tree."""
155 if self.parent:
156 try:
157 del self.parent.contents[self.parent.index(self)]
158 except ValueError:
159 pass
160
161 #Find the two elements that would be next to each other if
162 #this element (and any children) hadn't been parsed. Connect
163 #the two.
164 lastChild = self._lastRecursiveChild()
165 nextElement = lastChild.next
166
167 if self.previous:
168 self.previous.next = nextElement
169 if nextElement:
170 nextElement.previous = self.previous
171 self.previous = None
172 lastChild.next = None
173
174 self.parent = None
175 if self.previousSibling:
176 self.previousSibling.nextSibling = self.nextSibling
177 if self.nextSibling:
178 self.nextSibling.previousSibling = self.previousSibling
179 self.previousSibling = self.nextSibling = None
180 return self
181
182 def _lastRecursiveChild(self):
183 "Finds the last element beneath this object to be parsed."
184 lastChild = self
185 while hasattr(lastChild, 'contents') and lastChild.contents:
186 lastChild = lastChild.contents[-1]
187 return lastChild
188
189 def insert(self, position, newChild):
190 if isinstance(newChild, basestring) \
191 and not isinstance(newChild, NavigableString):
192 newChild = NavigableString(newChild)
193
194 position = min(position, len(self.contents))
195 if hasattr(newChild, 'parent') and newChild.parent is not None:
196 # We're 'inserting' an element that's already one
197 # of this object's children.
198 if newChild.parent is self:
199 index = self.index(newChild)
200 if index > position:
201 # Furthermore we're moving it further down the
202 # list of this object's children. That means that
203 # when we extract this element, our target index
204 # will jump down one.
205 position = position - 1
206 newChild.extract()
207
208 newChild.parent = self
209 previousChild = None
210 if position == 0:
211 newChild.previousSibling = None
212 newChild.previous = self
213 else:
214 previousChild = self.contents[position-1]
215 newChild.previousSibling = previousChild
216 newChild.previousSibling.nextSibling = newChild
217 newChild.previous = previousChild._lastRecursiveChild()
218 if newChild.previous:
219 newChild.previous.next = newChild
220
221 newChildsLastElement = newChild._lastRecursiveChild()
222
223 if position >= len(self.contents):
224 newChild.nextSibling = None
225
226 parent = self
227 parentsNextSibling = None
228 while not parentsNextSibling:
229 parentsNextSibling = parent.nextSibling
230 parent = parent.parent
231 if not parent: # This is the last element in the document.
232 break
233 if parentsNextSibling:
234 newChildsLastElement.next = parentsNextSibling
235 else:
236 newChildsLastElement.next = None
237 else:
238 nextChild = self.contents[position]
239 newChild.nextSibling = nextChild
240 if newChild.nextSibling:
241 newChild.nextSibling.previousSibling = newChild
242 newChildsLastElement.next = nextChild
243
244 if newChildsLastElement.next:
245 newChildsLastElement.next.previous = newChildsLastElement
246 self.contents.insert(position, newChild)
247
248 def append(self, tag):
249 """Appends the given tag to the contents of this tag."""
250 self.insert(len(self.contents), tag)
251
252 def findNext(self, name=None, attrs={}, text=None, **kwargs):
253 """Returns the first item that matches the given criteria and
254 appears after this Tag in the document."""
255 return self._findOne(self.findAllNext, name, attrs, text, **kwargs)
256
257 def findAllNext(self, name=None, attrs={}, text=None, limit=None,
258 **kwargs):
259 """Returns all items that match the given criteria and appear
260 after this Tag in the document."""
261 return self._findAll(name, attrs, text, limit, self.nextGenerator,
262 **kwargs)
263
264 def findNextSibling(self, name=None, attrs={}, text=None, **kwargs):
265 """Returns the closest sibling to this Tag that matches the
266 given criteria and appears after this Tag in the document."""
267 return self._findOne(self.findNextSiblings, name, attrs, text,
268 **kwargs)
269
270 def findNextSiblings(self, name=None, attrs={}, text=None, limit=None,
271 **kwargs):
272 """Returns the siblings of this Tag that match the given
273 criteria and appear after this Tag in the document."""
274 return self._findAll(name, attrs, text, limit,
275 self.nextSiblingGenerator, **kwargs)
276 fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x
277
278 def findPrevious(self, name=None, attrs={}, text=None, **kwargs):
279 """Returns the first item that matches the given criteria and
280 appears before this Tag in the document."""
281 return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs)
282
283 def findAllPrevious(self, name=None, attrs={}, text=None, limit=None,
284 **kwargs):
285 """Returns all items that match the given criteria and appear
286 before this Tag in the document."""
287 return self._findAll(name, attrs, text, limit, self.previousGenerator,
288 **kwargs)
289 fetchPrevious = findAllPrevious # Compatibility with pre-3.x
290
291 def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs):
292 """Returns the closest sibling to this Tag that matches the
293 given criteria and appears before this Tag in the document."""
294 return self._findOne(self.findPreviousSiblings, name, attrs, text,
295 **kwargs)
296
297 def findPreviousSiblings(self, name=None, attrs={}, text=None,
298 limit=None, **kwargs):
299 """Returns the siblings of this Tag that match the given
300 criteria and appear before this Tag in the document."""
301 return self._findAll(name, attrs, text, limit,
302 self.previousSiblingGenerator, **kwargs)
303 fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x
304
305 def findParent(self, name=None, attrs={}, **kwargs):
306 """Returns the closest parent of this Tag that matches the given
307 criteria."""
308 # NOTE: We can't use _findOne because findParents takes a different
309 # set of arguments.
310 r = None
311 l = self.findParents(name, attrs, 1)
312 if l:
313 r = l[0]
314 return r
315
316 def findParents(self, name=None, attrs={}, limit=None, **kwargs):
317 """Returns the parents of this Tag that match the given
318 criteria."""
319
320 return self._findAll(name, attrs, None, limit, self.parentGenerator,
321 **kwargs)
322 fetchParents = findParents # Compatibility with pre-3.x
323
324 #These methods do the real heavy lifting.
325
326 def _findOne(self, method, name, attrs, text, **kwargs):
327 r = None
328 l = method(name, attrs, text, 1, **kwargs)
329 if l:
330 r = l[0]
331 return r
332
333 def _findAll(self, name, attrs, text, limit, generator, **kwargs):
334 "Iterates over a generator looking for things that match."
335
336 if isinstance(name, SoupStrainer):
337 strainer = name
338 # (Possibly) special case some findAll*(...) searches
339 elif text is None and not limit and not attrs and not kwargs:
340 # findAll*(True)
341 if name is True:
342 return [element for element in generator()
343 if isinstance(element, Tag)]
344 # findAll*('tag-name')
345 elif isinstance(name, basestring):
346 return [element for element in generator()
347 if isinstance(element, Tag) and
348 element.name == name]
349 else:
350 strainer = SoupStrainer(name, attrs, text, **kwargs)
351 # Build a SoupStrainer
352 else:
353 strainer = SoupStrainer(name, attrs, text, **kwargs)
354 results = ResultSet(strainer)
355 g = generator()
356 while True:
357 try:
358 i = g.next()
359 except StopIteration:
360 break
361 if i:
362 found = strainer.search(i)
363 if found:
364 results.append(found)
365 if limit and len(results) >= limit:
366 break
367 return results
368
369 #These Generators can be used to navigate starting from both
370 #NavigableStrings and Tags.
371 def nextGenerator(self):
372 i = self
373 while i is not None:
374 i = i.next
375 yield i
376
377 def nextSiblingGenerator(self):
378 i = self
379 while i is not None:
380 i = i.nextSibling
381 yield i
382
383 def previousGenerator(self):
384 i = self
385 while i is not None:
386 i = i.previous
387 yield i
388
389 def previousSiblingGenerator(self):
390 i = self
391 while i is not None:
392 i = i.previousSibling
393 yield i
394
395 def parentGenerator(self):
396 i = self
397 while i is not None:
398 i = i.parent
399 yield i
400
401 # Utility methods
402 def substituteEncoding(self, str, encoding=None):
403 encoding = encoding or "utf-8"
404 return str.replace("%SOUP-ENCODING%", encoding)
405
406 def toEncoding(self, s, encoding=None):
407 """Encodes an object to a string in some encoding, or to Unicode.
408 ."""
409 if isinstance(s, unicode):
410 if encoding:
411 s = s.encode(encoding)
412 elif isinstance(s, str):
413 if encoding:
414 s = s.encode(encoding)
415 else:
416 s = unicode(s)
417 else:
418 if encoding:
419 s = self.toEncoding(str(s), encoding)
420 else:
421 s = unicode(s)
422 return s
423
424 class NavigableString(unicode, PageElement):
425
426 def __new__(cls, value):
427 """Create a new NavigableString.
428
429 When unpickling a NavigableString, this method is called with
430 the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
431 passed in to the superclass's __new__ or the superclass won't know
432 how to handle non-ASCII characters.
433 """
434 if isinstance(value, unicode):
435 return unicode.__new__(cls, value)
436 return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
437
438 def __getnewargs__(self):
439 return (NavigableString.__str__(self),)
440
441 def __getattr__(self, attr):
442 """text.string gives you text. This is for backwards
443 compatibility for Navigable*String, but for CData* it lets you
444 get the string without the CData wrapper."""
445 if attr == 'string':
446 return self
447 else:
448 raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)
449
450 def __unicode__(self):
451 return str(self).decode(DEFAULT_OUTPUT_ENCODING)
452
453 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
454 if encoding:
455 return self.encode(encoding)
456 else:
457 return self
458
459 class CData(NavigableString):
460
461 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
462 return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding)
463
464 class ProcessingInstruction(NavigableString):
465 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
466 output = self
467 if "%SOUP-ENCODING%" in output:
468 output = self.substituteEncoding(output, encoding)
469 return "<?%s?>" % self.toEncoding(output, encoding)
470
471 class Comment(NavigableString):
472 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
473 return "<!--%s-->" % NavigableString.__str__(self, encoding)
474
475 class Declaration(NavigableString):
476 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING):
477 return "<!%s>" % NavigableString.__str__(self, encoding)
478
479 class Tag(PageElement):
480
481 """Represents a found HTML tag with its attributes and contents."""
482
483 def _invert(h):
484 "Cheap function to invert a hash."
485 i = {}
486 for k,v in h.items():
487 i[v] = k
488 return i
489
490 XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'",
491 "quot" : '"',
492 "amp" : "&",
493 "lt" : "<",
494 "gt" : ">" }
495
496 XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS)
497
498 def _convertEntities(self, match):
499 """Used in a call to re.sub to replace HTML, XML, and numeric
500 entities with the appropriate Unicode characters. If HTML
501 entities are being converted, any unrecognized entities are
502 escaped."""
503 x = match.group(1)
504 if self.convertHTMLEntities and x in name2codepoint:
505 return unichr(name2codepoint[x])
506 elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS:
507 if self.convertXMLEntities:
508 return self.XML_ENTITIES_TO_SPECIAL_CHARS[x]
509 else:
510 return u'&%s;' % x
511 elif len(x) > 0 and x[0] == '#':
512 # Handle numeric entities
513 if len(x) > 1 and x[1] == 'x':
514 return unichr(int(x[2:], 16))
515 else:
516 return unichr(int(x[1:]))
517
518 elif self.escapeUnrecognizedEntities:
519 return u'&amp;%s;' % x
520 else:
521 return u'&%s;' % x
522
523 def __init__(self, parser, name, attrs=None, parent=None,
524 previous=None):
525 "Basic constructor."
526
527 # We don't actually store the parser object: that lets extracted
528 # chunks be garbage-collected
529 self.parserClass = parser.__class__
530 self.isSelfClosing = parser.isSelfClosingTag(name)
531 self.name = name
532 if attrs is None:
533 attrs = []
534 elif isinstance(attrs, dict):
535 attrs = attrs.items()
536 self.attrs = attrs
537 self.contents = []
538 self.setup(parent, previous)
539 self.hidden = False
540 self.containsSubstitutions = False
541 self.convertHTMLEntities = parser.convertHTMLEntities
542 self.convertXMLEntities = parser.convertXMLEntities
543 self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities
544
545 # Convert any HTML, XML, or numeric entities in the attribute values.
546 convert = lambda(k, val): (k,
547 re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);",
548 self._convertEntities,
549 val))
550 self.attrs = map(convert, self.attrs)
551
552 def getString(self):
553 if (len(self.contents) == 1
554 and isinstance(self.contents[0], NavigableString)):
555 return self.contents[0]
556
557 def setString(self, string):
558 """Replace the contents of the tag with a string"""
559 self.clear()
560 self.append(string)
561
562 string = property(getString, setString)
563
564 def getText(self, separator=u""):
565 if not len(self.contents):
566 return u""
567 stopNode = self._lastRecursiveChild().next
568 strings = []
569 current = self.contents[0]
570 while current is not stopNode:
571 if isinstance(current, NavigableString):
572 strings.append(current.strip())
573 current = current.next
574 return separator.join(strings)
575
576 text = property(getText)
577
578 def get(self, key, default=None):
579 """Returns the value of the 'key' attribute for the tag, or
580 the value given for 'default' if it doesn't have that
581 attribute."""
582 return self._getAttrMap().get(key, default)
583
584 def clear(self):
585 """Extract all children."""
586 for child in self.contents[:]:
587 child.extract()
588
589 def index(self, element):
590 for i, child in enumerate(self.contents):
591 if child is element:
592 return i
593 raise ValueError("Tag.index: element not in tag")
594
595 def has_key(self, key):
596 return self._getAttrMap().has_key(key)
597
598 def __getitem__(self, key):
599 """tag[key] returns the value of the 'key' attribute for the tag,
600 and throws an exception if it's not there."""
601 return self._getAttrMap()[key]
602
603 def __iter__(self):
604 "Iterating over a tag iterates over its contents."
605 return iter(self.contents)
606
607 def __len__(self):
608 "The length of a tag is the length of its list of contents."
609 return len(self.contents)
610
611 def __contains__(self, x):
612 return x in self.contents
613
614 def __nonzero__(self):
615 "A tag is non-None even if it has no contents."
616 return True
617
618 def __setitem__(self, key, value):
619 """Setting tag[key] sets the value of the 'key' attribute for the
620 tag."""
621 self._getAttrMap()
622 self.attrMap[key] = value
623 found = False
624 for i in range(0, len(self.attrs)):
625 if self.attrs[i][0] == key:
626 self.attrs[i] = (key, value)
627 found = True
628 if not found:
629 self.attrs.append((key, value))
630 self._getAttrMap()[key] = value
631
632 def __delitem__(self, key):
633 "Deleting tag[key] deletes all 'key' attributes for the tag."
634 for item in self.attrs:
635 if item[0] == key:
636 self.attrs.remove(item)
637 #We don't break because bad HTML can define the same
638 #attribute multiple times.
639 self._getAttrMap()
640 if self.attrMap.has_key(key):
641 del self.attrMap[key]
642
643 def __call__(self, *args, **kwargs):
644 """Calling a tag like a function is the same as calling its
645 findAll() method. Eg. tag('a') returns a list of all the A tags
646 found within this tag."""
647 return apply(self.findAll, args, kwargs)
648
649 def __getattr__(self, tag):
650 #print "Getattr %s.%s" % (self.__class__, tag)
651 if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3:
652 return self.find(tag[:-3])
653 elif tag.find('__') != 0:
654 return self.find(tag)
655 raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag)
656
657 def __eq__(self, other):
658 """Returns true iff this tag has the same name, the same attributes,
659 and the same contents (recursively) as the given tag.
660
661 NOTE: right now this will return false if two tags have the
662 same attributes in a different order. Should this be fixed?"""
663 if other is self:
664 return True
665 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):
666 return False
667 for i in range(0, len(self.contents)):
668 if self.contents[i] != other.contents[i]:
669 return False
670 return True
671
672 def __ne__(self, other):
673 """Returns true iff this tag is not identical to the other tag,
674 as defined in __eq__."""
675 return not self == other
676
677 def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
678 """Renders this tag as a string."""
679 return self.__str__(encoding)
680
681 def __unicode__(self):
682 return self.__str__(None)
683
684 BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
685 + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
686 + ")")
687
688 def _sub_entity(self, x):
689 """Used with a regular expression to substitute the
690 appropriate XML entity for an XML special character."""
691 return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";"
692
693 def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING,
694 prettyPrint=False, indentLevel=0):
695 """Returns a string or Unicode representation of this tag and
696 its contents. To get Unicode, pass None for encoding.
697
698 NOTE: since Python's HTML parser consumes whitespace, this
699 method is not certain to reproduce the whitespace present in
700 the original string."""
701
702 encodedName = self.toEncoding(self.name, encoding)
703
704 attrs = []
705 if self.attrs:
706 for key, val in self.attrs:
707 fmt = '%s="%s"'
708 if isinstance(val, basestring):
709 if self.containsSubstitutions and '%SOUP-ENCODING%' in val:
710 val = self.substituteEncoding(val, encoding)
711
712 # The attribute value either:
713 #
714 # * Contains no embedded double quotes or single quotes.
715 # No problem: we enclose it in double quotes.
716 # * Contains embedded single quotes. No problem:
717 # double quotes work here too.
718 # * Contains embedded double quotes. No problem:
719 # we enclose it in single quotes.
720 # * Embeds both single _and_ double quotes. This
721 # can't happen naturally, but it can happen if
722 # you modify an attribute value after parsing
723 # the document. Now we have a bit of a
724 # problem. We solve it by enclosing the
725 # attribute in single quotes, and escaping any
726 # embedded single quotes to XML entities.
727 if '"' in val:
728 fmt = "%s='%s'"
729 if "'" in val:
730 # TODO: replace with apos when
731 # appropriate.
732 val = val.replace("'", "&squot;")
733
734 # Now we're okay w/r/t quotes. But the attribute
735 # value might also contain angle brackets, or
736 # ampersands that aren't part of entities. We need
737 # to escape those to XML entities too.
738 val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val)
739
740 attrs.append(fmt % (self.toEncoding(key, encoding),
741 self.toEncoding(val, encoding)))
742 close = ''
743 closeTag = ''
744 if self.isSelfClosing:
745 close = ' /'
746 else:
747 closeTag = '</%s>' % encodedName
748
749 indentTag, indentContents = 0, 0
750 if prettyPrint:
751 indentTag = indentLevel
752 space = (' ' * (indentTag-1))
753 indentContents = indentTag + 1
754 contents = self.renderContents(encoding, prettyPrint, indentContents)
755 if self.hidden:
756 s = contents
757 else:
758 s = []
759 attributeString = ''
760 if attrs:
761 attributeString = ' ' + ' '.join(attrs)
762 if prettyPrint:
763 s.append(space)
764 s.append('<%s%s%s>' % (encodedName, attributeString, close))
765 if prettyPrint:
766 s.append("\n")
767 s.append(contents)
768 if prettyPrint and contents and contents[-1] != "\n":
769 s.append("\n")
770 if prettyPrint and closeTag:
771 s.append(space)
772 s.append(closeTag)
773 if prettyPrint and closeTag and self.nextSibling:
774 s.append("\n")
775 s = ''.join(s)
776 return s
777
778 def decompose(self):
779 """Recursively destroys the contents of this tree."""
780 self.extract()
781 if len(self.contents) == 0:
782 return
783 current = self.contents[0]
784 while current is not None:
785 next = current.next
786 if isinstance(current, Tag):
787 del current.contents[:]
788 current.parent = None
789 current.previous = None
790 current.previousSibling = None
791 current.next = None
792 current.nextSibling = None
793 current = next
794
795 def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING):
796 return self.__str__(encoding, True)
797
798 def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
799 prettyPrint=False, indentLevel=0):
800 """Renders the contents of this tag as a string in the given
801 encoding. If encoding is None, returns a Unicode string.."""
802 s=[]
803 for c in self:
804 text = None
805 if isinstance(c, NavigableString):
806 text = c.__str__(encoding)
807 elif isinstance(c, Tag):
808 s.append(c.__str__(encoding, prettyPrint, indentLevel))
809 if text and prettyPrint:
810 text = text.strip()
811 if text:
812 if prettyPrint:
813 s.append(" " * (indentLevel-1))
814 s.append(text)
815 if prettyPrint:
816 s.append("\n")
817 return ''.join(s)
818
819 #Soup methods
820
821 def find(self, name=None, attrs={}, recursive=True, text=None,
822 **kwargs):
823 """Return only the first child of this Tag matching the given
824 criteria."""
825 r = None
826 l = self.findAll(name, attrs, recursive, text, 1, **kwargs)
827 if l:
828 r = l[0]
829 return r
830 findChild = find
831
832 def findAll(self, name=None, attrs={}, recursive=True, text=None,
833 limit=None, **kwargs):
834 """Extracts a list of Tag objects that match the given
835 criteria. You can specify the name of the Tag and any
836 attributes you want the Tag to have.
837
838 The value of a key-value pair in the 'attrs' map can be a
839 string, a list of strings, a regular expression object, or a
840 callable that takes a string and returns whether or not the
841 string matches for some custom definition of 'matches'. The
842 same is true of the tag name."""
843 generator = self.recursiveChildGenerator
844 if not recursive:
845 generator = self.childGenerator
846 return self._findAll(name, attrs, text, limit, generator, **kwargs)
847 findChildren = findAll
848
849 # Pre-3.x compatibility methods
850 first = find
851 fetch = findAll
852
853 def fetchText(self, text=None, recursive=True, limit=None):
854 return self.findAll(text=text, recursive=recursive, limit=limit)
855
856 def firstText(self, text=None, recursive=True):
857 return self.find(text=text, recursive=recursive)
858
859 #Private methods
860
861 def _getAttrMap(self):
862 """Initializes a map representation of this tag's attributes,
863 if not already initialized."""
864 if not getattr(self, 'attrMap'):
865 self.attrMap = {}
866 for (key, value) in self.attrs:
867 self.attrMap[key] = value
868 return self.attrMap
869
870 #Generator methods
871 def childGenerator(self):
872 # Just use the iterator from the contents
873 return iter(self.contents)
874
875 def recursiveChildGenerator(self):
876 if not len(self.contents):
877 raise StopIteration
878 stopNode = self._lastRecursiveChild().next
879 current = self.contents[0]
880 while current is not stopNode:
881 yield current
882 current = current.next
883
884
885 # Next, a couple classes to represent queries and their results.
886 class SoupStrainer:
887 """Encapsulates a number of ways of matching a markup element (tag or
888 text)."""
889
890 def __init__(self, name=None, attrs={}, text=None, **kwargs):
891 self.name = name
892 if isinstance(attrs, basestring):
893 kwargs['class'] = _match_css_class(attrs)
894 attrs = None
895 if kwargs:
896 if attrs:
897 attrs = attrs.copy()
898 attrs.update(kwargs)
899 else:
900 attrs = kwargs
901 self.attrs = attrs
902 self.text = text
903
904 def __str__(self):
905 if self.text:
906 return self.text
907 else:
908 return "%s|%s" % (self.name, self.attrs)
909
910 def searchTag(self, markupName=None, markupAttrs={}):
911 found = None
912 markup = None
913 if isinstance(markupName, Tag):
914 markup = markupName
915 markupAttrs = markup
916 callFunctionWithTagData = callable(self.name) \
917 and not isinstance(markupName, Tag)
918
919 if (not self.name) \
920 or callFunctionWithTagData \
921 or (markup and self._matches(markup, self.name)) \
922 or (not markup and self._matches(markupName, self.name)):
923 if callFunctionWithTagData:
924 match = self.name(markupName, markupAttrs)
925 else:
926 match = True
927 markupAttrMap = None
928 for attr, matchAgainst in self.attrs.items():
929 if not markupAttrMap:
930 if hasattr(markupAttrs, 'get'):
931 markupAttrMap = markupAttrs
932 else:
933 markupAttrMap = {}
934 for k,v in markupAttrs:
935 markupAttrMap[k] = v
936 attrValue = markupAttrMap.get(attr)
937 if not self._matches(attrValue, matchAgainst):
938 match = False
939 break
940 if match:
941 if markup:
942 found = markup
943 else:
944 found = markupName
945 return found
946
947 def search(self, markup):
948 #print 'looking for %s in %s' % (self, markup)
949 found = None
950 # If given a list of items, scan it for a text element that
951 # matches.
952 if hasattr(markup, "__iter__") \
953 and not isinstance(markup, Tag):
954 for element in markup:
955 if isinstance(element, NavigableString) \
956 and self.search(element):
957 found = element
958 break
959 # If it's a Tag, make sure its name or attributes match.
960 # Don't bother with Tags if we're searching for text.
961 elif isinstance(markup, Tag):
962 if not self.text:
963 found = self.searchTag(markup)
964 # If it's text, make sure the text matches.
965 elif isinstance(markup, NavigableString) or \
966 isinstance(markup, basestring):
967 if self._matches(markup, self.text):
968 found = markup
969 else:
970 raise Exception, "I don't know how to match against a %s" \
971 % markup.__class__
972 return found
973
974 def _matches(self, markup, matchAgainst):
975 #print "Matching %s against %s" % (markup, matchAgainst)
976 result = False
977 if matchAgainst is True:
978 result = markup is not None
979 elif callable(matchAgainst):
980 result = matchAgainst(markup)
981 else:
982 #Custom match methods take the tag as an argument, but all
983 #other ways of matching match the tag name as a string.
984 if isinstance(markup, Tag):
985 markup = markup.name
986 if markup and not isinstance(markup, basestring):
987 markup = unicode(markup)
988 #Now we know that chunk is either a string, or None.
989 if hasattr(matchAgainst, 'match'):
990 # It's a regexp object.
991 result = markup and matchAgainst.search(markup)
992 elif hasattr(matchAgainst, '__iter__'): # list-like
993 result = markup in matchAgainst
994 elif hasattr(matchAgainst, 'items'):
995 result = markup.has_key(matchAgainst)
996 elif matchAgainst and isinstance(markup, basestring):
997 if isinstance(markup, unicode):
998 matchAgainst = unicode(matchAgainst)
999 else:
1000 matchAgainst = str(matchAgainst)
1001
1002 if not result:
1003 result = matchAgainst == markup
1004 return result
1005
1006 class ResultSet(list):
1007 """A ResultSet is just a list that keeps track of the SoupStrainer
1008 that created it."""
1009 def __init__(self, source):
1010 list.__init__([])
1011 self.source = source
1012
1013 # Now, some helper functions.
1014
1015 def buildTagMap(default, *args):
1016 """Turns a list of maps, lists, or scalars into a single map.
1017 Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and
1018 NESTING_RESET_TAGS maps out of lists and partial maps."""
1019 built = {}
1020 for portion in args:
1021 if hasattr(portion, 'items'):
1022 #It's a map. Merge it.
1023 for k,v in portion.items():
1024 built[k] = v
1025 elif hasattr(portion, '__iter__'): # is a list
1026 #It's a list. Map each item to the default.
1027 for k in portion:
1028 built[k] = default
1029 else:
1030 #It's a scalar. Map it to the default.
1031 built[portion] = default
1032 return built
1033
1034 # Now, the parser classes.
1035
1036 class BeautifulStoneSoup(Tag, SGMLParser):
1037
1038 """This class contains the basic parser and search code. It defines
1039 a parser that knows nothing about tag behavior except for the
1040 following:
1041
1042 You can't close a tag without closing all the tags it encloses.
1043 That is, "<foo><bar></foo>" actually means
1044 "<foo><bar></bar></foo>".
1045
1046 [Another possible explanation is "<foo><bar /></foo>", but since
1047 this class defines no SELF_CLOSING_TAGS, it will never use that
1048 explanation.]
1049
1050 This class is useful for parsing XML or made-up markup languages,
1051 or when BeautifulSoup makes an assumption counter to what you were
1052 expecting."""
1053
1054 SELF_CLOSING_TAGS = {}
1055 NESTABLE_TAGS = {}
1056 RESET_NESTING_TAGS = {}
1057 QUOTE_TAGS = {}
1058 PRESERVE_WHITESPACE_TAGS = []
1059
1060 MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'),
1061 lambda x: x.group(1) + ' />'),
1062 (re.compile('<!\s+([^<>]*)>'),
1063 lambda x: '<!' + x.group(1) + '>')
1064 ]
1065
1066 ROOT_TAG_NAME = u'[document]'
1067
1068 HTML_ENTITIES = "html"
1069 XML_ENTITIES = "xml"
1070 XHTML_ENTITIES = "xhtml"
1071 # TODO: This only exists for backwards-compatibility
1072 ALL_ENTITIES = XHTML_ENTITIES
1073
1074 # Used when determining whether a text node is all whitespace and
1075 # can be replaced with a single space. A text node that contains
1076 # fancy Unicode spaces (usually non-breaking) should be left
1077 # alone.
1078 STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, }
1079
1080 def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None,
1081 markupMassage=True, smartQuotesTo=XML_ENTITIES,
1082 convertEntities=None, selfClosingTags=None, isHTML=False):
1083 """The Soup object is initialized as the 'root tag', and the
1084 provided markup (which can be a string or a file-like object)
1085 is fed into the underlying parser.
1086
1087 sgmllib will process most bad HTML, and the BeautifulSoup
1088 class has some tricks for dealing with some HTML that kills
1089 sgmllib, but Beautiful Soup can nonetheless choke or lose data
1090 if your data uses self-closing tags or declarations
1091 incorrectly.
1092
1093 By default, Beautiful Soup uses regexes to sanitize input,
1094 avoiding the vast majority of these problems. If the problems
1095 don't apply to you, pass in False for markupMassage, and
1096 you'll get better performance.
1097
1098 The default parser massage techniques fix the two most common
1099 instances of invalid HTML that choke sgmllib:
1100
1101 <br/> (No space between name of closing tag and tag close)
1102 <! --Comment--> (Extraneous whitespace in declaration)
1103
1104 You can pass in a custom list of (RE object, replace method)
1105 tuples to get Beautiful Soup to scrub your input the way you
1106 want."""
1107
1108 self.parseOnlyThese = parseOnlyThese
1109 self.fromEncoding = fromEncoding
1110 self.smartQuotesTo = smartQuotesTo
1111 self.convertEntities = convertEntities
1112 # Set the rules for how we'll deal with the entities we
1113 # encounter
1114 if self.convertEntities:
1115 # It doesn't make sense to convert encoded characters to
1116 # entities even while you're converting entities to Unicode.
1117 # Just convert it all to Unicode.
1118 self.smartQuotesTo = None
1119 if convertEntities == self.HTML_ENTITIES:
1120 self.convertXMLEntities = False
1121 self.convertHTMLEntities = True
1122 self.escapeUnrecognizedEntities = True
1123 elif convertEntities == self.XHTML_ENTITIES:
1124 self.convertXMLEntities = True
1125 self.convertHTMLEntities = True
1126 self.escapeUnrecognizedEntities = False
1127 elif convertEntities == self.XML_ENTITIES:
1128 self.convertXMLEntities = True
1129 self.convertHTMLEntities = False
1130 self.escapeUnrecognizedEntities = False
1131 else:
1132 self.convertXMLEntities = False
1133 self.convertHTMLEntities = False
1134 self.escapeUnrecognizedEntities = False
1135
1136 self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags)
1137 SGMLParser.__init__(self)
1138
1139 if hasattr(markup, 'read'): # It's a file-type object.
1140 markup = markup.read()
1141 self.markup = markup
1142 self.markupMassage = markupMassage
1143 try:
1144 self._feed(isHTML=isHTML)
1145 except StopParsing:
1146 pass
1147 self.markup = None # The markup can now be GCed
1148
1149 def convert_charref(self, name):
1150 """This method fixes a bug in Python's SGMLParser."""
1151 try:
1152 n = int(name)
1153 except ValueError:
1154 return
1155 if not 0 <= n <= 127 : # ASCII ends at 127, not 255
1156 return
1157 return self.convert_codepoint(n)
1158
1159 def _feed(self, inDocumentEncoding=None, isHTML=False):
1160 # Convert the document to Unicode.
1161 markup = self.markup
1162 if isinstance(markup, unicode):
1163 if not hasattr(self, 'originalEncoding'):
1164 self.originalEncoding = None
1165 else:
1166 dammit = UnicodeDammit\
1167 (markup, [self.fromEncoding, inDocumentEncoding],
1168 smartQuotesTo=self.smartQuotesTo, isHTML=isHTML)
1169 markup = dammit.unicode
1170 self.originalEncoding = dammit.originalEncoding
1171 self.declaredHTMLEncoding = dammit.declaredHTMLEncoding
1172 if markup:
1173 if self.markupMassage:
1174 if not hasattr(self.markupMassage, "__iter__"):
1175 self.markupMassage = self.MARKUP_MASSAGE
1176 for fix, m in self.markupMassage:
1177 markup = fix.sub(m, markup)
1178 # TODO: We get rid of markupMassage so that the
1179 # soup object can be deepcopied later on. Some
1180 # Python installations can't copy regexes. If anyone
1181 # was relying on the existence of markupMassage, this
1182 # might cause problems.
1183 del(self.markupMassage)
1184 self.reset()
1185
1186 SGMLParser.feed(self, markup)
1187 # Close out any unfinished strings and close all the open tags.
1188 self.endData()
1189 while self.currentTag.name != self.ROOT_TAG_NAME:
1190 self.popTag()
1191
1192 def __getattr__(self, methodName):
1193 """This method routes method call requests to either the SGMLParser
1194 superclass or the Tag superclass, depending on the method name."""
1195 #print "__getattr__ called on %s.%s" % (self.__class__, methodName)
1196
1197 if methodName.startswith('start_') or methodName.startswith('end_') \
1198 or methodName.startswith('do_'):
1199 return SGMLParser.__getattr__(self, methodName)
1200 elif not methodName.startswith('__'):
1201 return Tag.__getattr__(self, methodName)
1202 else:
1203 raise AttributeError
1204
1205 def isSelfClosingTag(self, name):
1206 """Returns true iff the given string is the name of a
1207 self-closing tag according to this parser."""
1208 return self.SELF_CLOSING_TAGS.has_key(name) \
1209 or self.instanceSelfClosingTags.has_key(name)
1210
1211 def reset(self):
1212 Tag.__init__(self, self, self.ROOT_TAG_NAME)
1213 self.hidden = 1
1214 SGMLParser.reset(self)
1215 self.currentData = []
1216 self.currentTag = None
1217 self.tagStack = []
1218 self.quoteStack = []
1219 self.pushTag(self)
1220
1221 def popTag(self):
1222 tag = self.tagStack.pop()
1223
1224 #print "Pop", tag.name
1225 if self.tagStack:
1226 self.currentTag = self.tagStack[-1]
1227 return self.currentTag
1228
1229 def pushTag(self, tag):
1230 #print "Push", tag.name
1231 if self.currentTag:
1232 self.currentTag.contents.append(tag)
1233 self.tagStack.append(tag)
1234 self.currentTag = self.tagStack[-1]
1235
1236 def endData(self, containerClass=NavigableString):
1237 if self.currentData:
1238 currentData = u''.join(self.currentData)
1239 if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
1240 not set([tag.name for tag in self.tagStack]).intersection(
1241 self.PRESERVE_WHITESPACE_TAGS)):
1242 if '\n' in currentData:
1243 currentData = '\n'
1244 else:
1245 currentData = ' '
1246 self.currentData = []
1247 if self.parseOnlyThese and len(self.tagStack) <= 1 and \
1248 (not self.parseOnlyThese.text or \
1249 not self.parseOnlyThese.search(currentData)):
1250 return
1251 o = containerClass(currentData)
1252 o.setup(self.currentTag, self.previous)
1253 if self.previous:
1254 self.previous.next = o
1255 self.previous = o
1256 self.currentTag.contents.append(o)
1257
1258
1259 def _popToTag(self, name, inclusivePop=True):
1260 """Pops the tag stack up to and including the most recent
1261 instance of the given tag. If inclusivePop is false, pops the tag
1262 stack up to but *not* including the most recent instqance of
1263 the given tag."""
1264 #print "Popping to %s" % name
1265 if name == self.ROOT_TAG_NAME:
1266 return
1267
1268 numPops = 0
1269 mostRecentTag = None
1270 for i in range(len(self.tagStack)-1, 0, -1):
1271 if name == self.tagStack[i].name:
1272 numPops = len(self.tagStack)-i
1273 break
1274 if not inclusivePop:
1275 numPops = numPops - 1
1276
1277 for i in range(0, numPops):
1278 mostRecentTag = self.popTag()
1279 return mostRecentTag
1280
1281 def _smartPop(self, name):
1282
1283 """We need to pop up to the previous tag of this type, unless
1284 one of this tag's nesting reset triggers comes between this
1285 tag and the previous tag of this type, OR unless this tag is a
1286 generic nesting trigger and another generic nesting trigger
1287 comes between this tag and the previous tag of this type.
1288
1289 Examples:
1290 <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'.
1291 <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'.
1292 <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'.
1293
1294 <li><ul><li> *<li>* should pop to 'ul', not the first 'li'.
1295 <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr'
1296 <td><tr><td> *<td>* should pop to 'tr', not the first 'td'
1297 """
1298
1299 nestingResetTriggers = self.NESTABLE_TAGS.get(name)
1300 isNestable = nestingResetTriggers != None
1301 isResetNesting = self.RESET_NESTING_TAGS.has_key(name)
1302 popTo = None
1303 inclusive = True
1304 for i in range(len(self.tagStack)-1, 0, -1):
1305 p = self.tagStack[i]
1306 if (not p or p.name == name) and not isNestable:
1307 #Non-nestable tags get popped to the top or to their
1308 #last occurance.
1309 popTo = name
1310 break
1311 if (nestingResetTriggers is not None
1312 and p.name in nestingResetTriggers) \
1313 or (nestingResetTriggers is None and isResetNesting
1314 and self.RESET_NESTING_TAGS.has_key(p.name)):
1315
1316 #If we encounter one of the nesting reset triggers
1317 #peculiar to this tag, or we encounter another tag
1318 #that causes nesting to reset, pop up to but not
1319 #including that tag.
1320 popTo = p.name
1321 inclusive = False
1322 break
1323 p = p.parent
1324 if popTo:
1325 self._popToTag(popTo, inclusive)
1326
1327 def unknown_starttag(self, name, attrs, selfClosing=0):
1328 #print "Start tag %s: %s" % (name, attrs)
1329 if self.quoteStack:
1330 #This is not a real tag.
1331 #print "<%s> is not real!" % name
1332 attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs])
1333 self.handle_data('<%s%s>' % (name, attrs))
1334 return
1335 self.endData()
1336
1337 if not self.isSelfClosingTag(name) and not selfClosing:
1338 self._smartPop(name)
1339
1340 if self.parseOnlyThese and len(self.tagStack) <= 1 \
1341 and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)):
1342 return
1343
1344 tag = Tag(self, name, attrs, self.currentTag, self.previous)
1345 if self.previous:
1346 self.previous.next = tag
1347 self.previous = tag
1348 self.pushTag(tag)
1349 if selfClosing or self.isSelfClosingTag(name):
1350 self.popTag()
1351 if name in self.QUOTE_TAGS:
1352 #print "Beginning quote (%s)" % name
1353 self.quoteStack.append(name)
1354 self.literal = 1
1355 return tag
1356
1357 def unknown_endtag(self, name):
1358 #print "End tag %s" % name
1359 if self.quoteStack and self.quoteStack[-1] != name:
1360 #This is not a real end tag.
1361 #print "</%s> is not real!" % name
1362 self.handle_data('</%s>' % name)
1363 return
1364 self.endData()
1365 self._popToTag(name)
1366 if self.quoteStack and self.quoteStack[-1] == name:
1367 self.quoteStack.pop()
1368 self.literal = (len(self.quoteStack) > 0)
1369
1370 def handle_data(self, data):
1371 self.currentData.append(data)
1372
1373 def _toStringSubclass(self, text, subclass):
1374 """Adds a certain piece of text to the tree as a NavigableString
1375 subclass."""
1376 self.endData()
1377 self.handle_data(text)
1378 self.endData(subclass)
1379
1380 def handle_pi(self, text):
1381 """Handle a processing instruction as a ProcessingInstruction
1382 object, possibly one with a %SOUP-ENCODING% slot into which an
1383 encoding will be plugged later."""
1384 if text[:3] == "xml":
1385 text = u"xml version='1.0' encoding='%SOUP-ENCODING%'"
1386 self._toStringSubclass(text, ProcessingInstruction)
1387
1388 def handle_comment(self, text):
1389 "Handle comments as Comment objects."
1390 self._toStringSubclass(text, Comment)
1391
1392 def handle_charref(self, ref):
1393 "Handle character references as data."
1394 if self.convertEntities:
1395 data = unichr(int(ref))
1396 else:
1397 data = '&#%s;' % ref
1398 self.handle_data(data)
1399
1400 def handle_entityref(self, ref):
1401 """Handle entity references as data, possibly converting known
1402 HTML and/or XML entity references to the corresponding Unicode
1403 characters."""
1404 data = None
1405 if self.convertHTMLEntities:
1406 try:
1407 data = unichr(name2codepoint[ref])
1408 except KeyError:
1409 pass
1410
1411 if not data and self.convertXMLEntities:
1412 data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref)
1413
1414 if not data and self.convertHTMLEntities and \
1415 not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref):
1416 # TODO: We've got a problem here. We're told this is
1417 # an entity reference, but it's not an XML entity
1418 # reference or an HTML entity reference. Nonetheless,
1419 # the logical thing to do is to pass it through as an
1420 # unrecognized entity reference.
1421 #
1422 # Except: when the input is "&carol;" this function
1423 # will be called with input "carol". When the input is
1424 # "AT&T", this function will be called with input
1425 # "T". We have no way of knowing whether a semicolon
1426 # was present originally, so we don't know whether
1427 # this is an unknown entity or just a misplaced
1428 # ampersand.
1429 #
1430 # The more common case is a misplaced ampersand, so I
1431 # escape the ampersand and omit the trailing semicolon.
1432 data = "&amp;%s" % ref
1433 if not data:
1434 # This case is different from the one above, because we
1435 # haven't already gone through a supposedly comprehensive
1436 # mapping of entities to Unicode characters. We might not
1437 # have gone through any mapping at all. So the chances are
1438 # very high that this is a real entity, and not a
1439 # misplaced ampersand.
1440 data = "&%s;" % ref
1441 self.handle_data(data)
1442
1443 def handle_decl(self, data):
1444 "Handle DOCTYPEs and the like as Declaration objects."
1445 self._toStringSubclass(data, Declaration)
1446
1447 def parse_declaration(self, i):
1448 """Treat a bogus SGML declaration as raw data. Treat a CDATA
1449 declaration as a CData object."""
1450 j = None
1451 if self.rawdata[i:i+9] == '<![CDATA[':
1452 k = self.rawdata.find(']]>', i)
1453 if k == -1:
1454 k = len(self.rawdata)
1455 data = self.rawdata[i+9:k]
1456 j = k+3
1457 self._toStringSubclass(data, CData)
1458 else:
1459 try:
1460 j = SGMLParser.parse_declaration(self, i)
1461 except SGMLParseError:
1462 toHandle = self.rawdata[i:]
1463 self.handle_data(toHandle)
1464 j = i + len(toHandle)
1465 return j
1466
1467 class BeautifulSoup(BeautifulStoneSoup):
1468
1469 """This parser knows the following facts about HTML:
1470
1471 * Some tags have no closing tag and should be interpreted as being
1472 closed as soon as they are encountered.
1473
1474 * The text inside some tags (ie. 'script') may contain tags which
1475 are not really part of the document and which should be parsed
1476 as text, not tags. If you want to parse the text as tags, you can
1477 always fetch it and parse it explicitly.
1478
1479 * Tag nesting rules:
1480
1481 Most tags can't be nested at all. For instance, the occurance of
1482 a <p> tag should implicitly close the previous <p> tag.
1483
1484 <p>Para1<p>Para2
1485 should be transformed into:
1486 <p>Para1</p><p>Para2
1487
1488 Some tags can be nested arbitrarily. For instance, the occurance
1489 of a <blockquote> tag should _not_ implicitly close the previous
1490 <blockquote> tag.
1491
1492 Alice said: <blockquote>Bob said: <blockquote>Blah
1493 should NOT be transformed into:
1494 Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah
1495
1496 Some tags can be nested, but the nesting is reset by the
1497 interposition of other tags. For instance, a <tr> tag should
1498 implicitly close the previous <tr> tag within the same <table>,
1499 but not close a <tr> tag in another table.
1500
1501 <table><tr>Blah<tr>Blah
1502 should be transformed into:
1503 <table><tr>Blah</tr><tr>Blah
1504 but,
1505 <tr>Blah<table><tr>Blah
1506 should NOT be transformed into
1507 <tr>Blah<table></tr><tr>Blah
1508
1509 Differing assumptions about tag nesting rules are a major source
1510 of problems with the BeautifulSoup class. If BeautifulSoup is not
1511 treating as nestable a tag your page author treats as nestable,
1512 try ICantBelieveItsBeautifulSoup, MinimalSoup, or
1513 BeautifulStoneSoup before writing your own subclass."""
1514
1515 def __init__(self, *args, **kwargs):
1516 if not kwargs.has_key('smartQuotesTo'):
1517 kwargs['smartQuotesTo'] = self.HTML_ENTITIES
1518 kwargs['isHTML'] = True
1519 BeautifulStoneSoup.__init__(self, *args, **kwargs)
1520
1521 SELF_CLOSING_TAGS = buildTagMap(None,
1522 ('br' , 'hr', 'input', 'img', 'meta',
1523 'spacer', 'link', 'frame', 'base', 'col'))
1524
1525 PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
1526
1527 QUOTE_TAGS = {'script' : None, 'textarea' : None}
1528
1529 #According to the HTML standard, each of these inline tags can
1530 #contain another tag of the same type. Furthermore, it's common
1531 #to actually use these tags this way.
1532 NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
1533 'center')
1534
1535 #According to the HTML standard, these block tags can contain
1536 #another tag of the same type. Furthermore, it's common
1537 #to actually use these tags this way.
1538 NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')
1539
1540 #Lists can contain other lists, but there are restrictions.
1541 NESTABLE_LIST_TAGS = { 'ol' : [],
1542 'ul' : [],
1543 'li' : ['ul', 'ol'],
1544 'dl' : [],
1545 'dd' : ['dl'],
1546 'dt' : ['dl'] }
1547
1548 #Tables can contain other tables, but there are restrictions.
1549 NESTABLE_TABLE_TAGS = {'table' : [],
1550 'tr' : ['table', 'tbody', 'tfoot', 'thead'],
1551 'td' : ['tr'],
1552 'th' : ['tr'],
1553 'thead' : ['table'],
1554 'tbody' : ['table'],
1555 'tfoot' : ['table'],
1556 }
1557
1558 NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre')
1559
1560 #If one of these tags is encountered, all tags up to the next tag of
1561 #this type are popped.
1562 RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript',
1563 NON_NESTABLE_BLOCK_TAGS,
1564 NESTABLE_LIST_TAGS,
1565 NESTABLE_TABLE_TAGS)
1566
1567 NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS,
1568 NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS)
1569
1570 # Used to detect the charset in a META tag; see start_meta
1571 CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
1572
1573 def start_meta(self, attrs):
1574 """Beautiful Soup can detect a charset included in a META tag,
1575 try to convert the document to that charset, and re-parse the
1576 document from the beginning."""
1577 httpEquiv = None
1578 contentType = None
1579 contentTypeIndex = None
1580 tagNeedsEncodingSubstitution = False
1581
1582 for i in range(0, len(attrs)):
1583 key, value = attrs[i]
1584 key = key.lower()
1585 if key == 'http-equiv':
1586 httpEquiv = value
1587 elif key == 'content':
1588 contentType = value
1589 contentTypeIndex = i
1590
1591 if httpEquiv and contentType: # It's an interesting meta tag.
1592 match = self.CHARSET_RE.search(contentType)
1593 if match:
1594 if (self.declaredHTMLEncoding is not None or
1595 self.originalEncoding == self.fromEncoding):
1596 # An HTML encoding was sniffed while converting
1597 # the document to Unicode, or an HTML encoding was
1598 # sniffed during a previous pass through the
1599 # document, or an encoding was specified
1600 # explicitly and it worked. Rewrite the meta tag.
1601 def rewrite(match):
1602 return match.group(1) + "%SOUP-ENCODING%"
1603 newAttr = self.CHARSET_RE.sub(rewrite, contentType)
1604 attrs[contentTypeIndex] = (attrs[contentTypeIndex][0],
1605 newAttr)
1606 tagNeedsEncodingSubstitution = True
1607 else:
1608 # This is our first pass through the document.
1609 # Go through it again with the encoding information.
1610 newCharset = match.group(3)
1611 if newCharset and newCharset != self.originalEncoding:
1612 self.declaredHTMLEncoding = newCharset
1613 self._feed(self.declaredHTMLEncoding)
1614 raise StopParsing
1615 pass
1616 tag = self.unknown_starttag("meta", attrs)
1617 if tag and tagNeedsEncodingSubstitution:
1618 tag.containsSubstitutions = True
1619
1620 class StopParsing(Exception):
1621 pass
1622
1623 class ICantBelieveItsBeautifulSoup(BeautifulSoup):
1624
1625 """The BeautifulSoup class is oriented towards skipping over
1626 common HTML errors like unclosed tags. However, sometimes it makes
1627 errors of its own. For instance, consider this fragment:
1628
1629 <b>Foo<b>Bar</b></b>
1630
1631 This is perfectly valid (if bizarre) HTML. However, the
1632 BeautifulSoup class will implicitly close the first b tag when it
1633 encounters the second 'b'. It will think the author wrote
1634 "<b>Foo<b>Bar", and didn't close the first 'b' tag, because
1635 there's no real-world reason to bold something that's already
1636 bold. When it encounters '</b></b>' it will close two more 'b'
1637 tags, for a grand total of three tags closed instead of two. This
1638 can throw off the rest of your document structure. The same is
1639 true of a number of other tags, listed below.
1640
1641 It's much more common for someone to forget to close a 'b' tag
1642 than to actually use nested 'b' tags, and the BeautifulSoup class
1643 handles the common case. This class handles the not-co-common
1644 case: where you can't believe someone wrote what they did, but
1645 it's valid HTML and BeautifulSoup screwed up by assuming it
1646 wouldn't be."""
1647
1648 I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \
1649 ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong',
1650 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b',
1651 'big')
1652
1653 I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',)
1654
1655 NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS,
1656 I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS,
1657 I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS)
1658
1659 class MinimalSoup(BeautifulSoup):
1660 """The MinimalSoup class is for parsing HTML that contains
1661 pathologically bad markup. It makes no assumptions about tag
1662 nesting, but it does know which tags are self-closing, that
1663 <script> tags contain Javascript and should not be parsed, that
1664 META tags may contain encoding information, and so on.
1665
1666 This also makes it better for subclassing than BeautifulStoneSoup
1667 or BeautifulSoup."""
1668
1669 RESET_NESTING_TAGS = buildTagMap('noscript')
1670 NESTABLE_TAGS = {}
1671
1672 class BeautifulSOAP(BeautifulStoneSoup):
1673 """This class will push a tag with only a single string child into
1674 the tag's parent as an attribute. The attribute's name is the tag
1675 name, and the value is the string child. An example should give
1676 the flavor of the change:
1677
1678 <foo><bar>baz</bar></foo>
1679 =>
1680 <foo bar="baz"><bar>baz</bar></foo>
1681
1682 You can then access fooTag['bar'] instead of fooTag.barTag.string.
1683
1684 This is, of course, useful for scraping structures that tend to
1685 use subelements instead of attributes, such as SOAP messages. Note
1686 that it modifies its input, so don't print the modified version
1687 out.
1688
1689 I'm not sure how many people really want to use this class; let me
1690 know if you do. Mainly I like the name."""
1691
1692 def popTag(self):
1693 if len(self.tagStack) > 1:
1694 tag = self.tagStack[-1]
1695 parent = self.tagStack[-2]
1696 parent._getAttrMap()
1697 if (isinstance(tag, Tag) and len(tag.contents) == 1 and
1698 isinstance(tag.contents[0], NavigableString) and
1699 not parent.attrMap.has_key(tag.name)):
1700 parent[tag.name] = tag.contents[0]
1701 BeautifulStoneSoup.popTag(self)
1702
1703 #Enterprise class names! It has come to our attention that some people
1704 #think the names of the Beautiful Soup parser classes are too silly
1705 #and "unprofessional" for use in enterprise screen-scraping. We feel
1706 #your pain! For such-minded folk, the Beautiful Soup Consortium And
1707 #All-Night Kosher Bakery recommends renaming this file to
1708 #"RobustParser.py" (or, in cases of extreme enterprisiness,
1709 #"RobustParserBeanInterface.class") and using the following
1710 #enterprise-friendly class aliases:
1711 class RobustXMLParser(BeautifulStoneSoup):
1712 pass
1713 class RobustHTMLParser(BeautifulSoup):
1714 pass
1715 class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup):
1716 pass
1717 class RobustInsanelyWackAssHTMLParser(MinimalSoup):
1718 pass
1719 class SimplifyingSOAPParser(BeautifulSOAP):
1720 pass
1721
1722 ######################################################
1723 #
1724 # Bonus library: Unicode, Dammit
1725 #
1726 # This class forces XML data into a standard format (usually to UTF-8
1727 # or Unicode). It is heavily based on code from Mark Pilgrim's
1728 # Universal Feed Parser. It does not rewrite the XML or HTML to
1729 # reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi
1730 # (XML) and BeautifulSoup.start_meta (HTML).
1731
1732 # Autodetects character encodings.
1733 # Download from http://chardet.feedparser.org/
1734 try:
1735 import chardet
1736 # import chardet.constants
1737 # chardet.constants._debug = 1
1738 except ImportError:
1739 chardet = None
1740
1741 # cjkcodecs and iconv_codec make Python know about more character encodings.
1742 # Both are available from http://cjkpython.i18n.org/
1743 # They're built in if you use Python 2.4.
1744 try:
1745 import cjkcodecs.aliases
1746 except ImportError:
1747 pass
1748 try:
1749 import iconv_codec
1750 except ImportError:
1751 pass
1752
1753 class UnicodeDammit:
1754 """A class for detecting the encoding of a *ML document and
1755 converting it to a Unicode string. If the source encoding is
1756 windows-1252, can replace MS smart quotes with their HTML or XML
1757 equivalents."""
1758
1759 # This dictionary maps commonly seen values for "charset" in HTML
1760 # meta tags to the corresponding Python codec names. It only covers
1761 # values that aren't in Python's aliases and can't be determined
1762 # by the heuristics in find_codec.
1763 CHARSET_ALIASES = { "macintosh" : "mac-roman",
1764 "x-sjis" : "shift-jis" }
1765
1766 def __init__(self, markup, overrideEncodings=[],
1767 smartQuotesTo='xml', isHTML=False):
1768 self.declaredHTMLEncoding = None
1769 self.markup, documentEncoding, sniffedEncoding = \
1770 self._detectEncoding(markup, isHTML)
1771 self.smartQuotesTo = smartQuotesTo
1772 self.triedEncodings = []
1773 if markup == '' or isinstance(markup, unicode):
1774 self.originalEncoding = None
1775 self.unicode = unicode(markup)
1776 return
1777
1778 u = None
1779 for proposedEncoding in overrideEncodings:
1780 u = self._convertFrom(proposedEncoding)
1781 if u: break
1782 if not u:
1783 for proposedEncoding in (documentEncoding, sniffedEncoding):
1784 u = self._convertFrom(proposedEncoding)
1785 if u: break
1786
1787 # If no luck and we have auto-detection library, try that:
1788 if not u and chardet and not isinstance(self.markup, unicode):
1789 u = self._convertFrom(chardet.detect(self.markup)['encoding'])
1790
1791 # As a last resort, try utf-8 and windows-1252:
1792 if not u:
1793 for proposed_encoding in ("utf-8", "windows-1252"):
1794 u = self._convertFrom(proposed_encoding)
1795 if u: break
1796
1797 self.unicode = u
1798 if not u: self.originalEncoding = None
1799
1800 def _subMSChar(self, orig):
1801 """Changes a MS smart quote character to an XML or HTML
1802 entity."""
1803 sub = self.MS_CHARS.get(orig)
1804 if isinstance(sub, tuple):
1805 if self.smartQuotesTo == 'xml':
1806 sub = '&#x%s;' % sub[1]
1807 else:
1808 sub = '&%s;' % sub[0]
1809 return sub
1810
1811 def _convertFrom(self, proposed):
1812 proposed = self.find_codec(proposed)
1813 if not proposed or proposed in self.triedEncodings:
1814 return None
1815 self.triedEncodings.append(proposed)
1816 markup = self.markup
1817
1818 # Convert smart quotes to HTML if coming from an encoding
1819 # that might have them.
1820 if self.smartQuotesTo and proposed.lower() in("windows-1252",
1821 "iso-8859-1",
1822 "iso-8859-2"):
1823 markup = re.compile("([\x80-\x9f])").sub \
1824 (lambda(x): self._subMSChar(x.group(1)),
1825 markup)
1826
1827 try:
1828 # print "Trying to convert document to %s" % proposed
1829 u = self._toUnicode(markup, proposed)
1830 self.markup = u
1831 self.originalEncoding = proposed
1832 except Exception, e:
1833 # print "That didn't work!"
1834 # print e
1835 return None
1836 #print "Correct encoding: %s" % proposed
1837 return self.markup
1838
1839 def _toUnicode(self, data, encoding):
1840 '''Given a string and its encoding, decodes the string into Unicode.
1841 %encoding is a string recognized by encodings.aliases'''
1842
1843 # strip Byte Order Mark (if present)
1844 if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
1845 and (data[2:4] != '\x00\x00'):
1846 encoding = 'utf-16be'
1847 data = data[2:]
1848 elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
1849 and (data[2:4] != '\x00\x00'):
1850 encoding = 'utf-16le'
1851 data = data[2:]
1852 elif data[:3] == '\xef\xbb\xbf':
1853 encoding = 'utf-8'
1854 data = data[3:]
1855 elif data[:4] == '\x00\x00\xfe\xff':
1856 encoding = 'utf-32be'
1857 data = data[4:]
1858 elif data[:4] == '\xff\xfe\x00\x00':
1859 encoding = 'utf-32le'
1860 data = data[4:]
1861 newdata = unicode(data, encoding)
1862 return newdata
1863
1864 def _detectEncoding(self, xml_data, isHTML=False):
1865 """Given a document, tries to detect its XML encoding."""
1866 xml_encoding = sniffed_xml_encoding = None
1867 try:
1868 if xml_data[:4] == '\x4c\x6f\xa7\x94':
1869 # EBCDIC
1870 xml_data = self._ebcdic_to_ascii(xml_data)
1871 elif xml_data[:4] == '\x00\x3c\x00\x3f':
1872 # UTF-16BE
1873 sniffed_xml_encoding = 'utf-16be'
1874 xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
1875 elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \
1876 and (xml_data[2:4] != '\x00\x00'):
1877 # UTF-16BE with BOM
1878 sniffed_xml_encoding = 'utf-16be'
1879 xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
1880 elif xml_data[:4] == '\x3c\x00\x3f\x00':
1881 # UTF-16LE
1882 sniffed_xml_encoding = 'utf-16le'
1883 xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
1884 elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \
1885 (xml_data[2:4] != '\x00\x00'):
1886 # UTF-16LE with BOM
1887 sniffed_xml_encoding = 'utf-16le'
1888 xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
1889 elif xml_data[:4] == '\x00\x00\x00\x3c':
1890 # UTF-32BE
1891 sniffed_xml_encoding = 'utf-32be'
1892 xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
1893 elif xml_data[:4] == '\x3c\x00\x00\x00':
1894 # UTF-32LE
1895 sniffed_xml_encoding = 'utf-32le'
1896 xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
1897 elif xml_data[:4] == '\x00\x00\xfe\xff':
1898 # UTF-32BE with BOM
1899 sniffed_xml_encoding = 'utf-32be'
1900 xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
1901 elif xml_data[:4] == '\xff\xfe\x00\x00':
1902 # UTF-32LE with BOM
1903 sniffed_xml_encoding = 'utf-32le'
1904 xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
1905 elif xml_data[:3] == '\xef\xbb\xbf':
1906 # UTF-8 with BOM
1907 sniffed_xml_encoding = 'utf-8'
1908 xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
1909 else:
1910 sniffed_xml_encoding = 'ascii'
1911 pass
1912 except:
1913 xml_encoding_match = None
1914 xml_encoding_match = re.compile(
1915 '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
1916 if not xml_encoding_match and isHTML:
1917 regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I)
1918 xml_encoding_match = regexp.search(xml_data)
1919 if xml_encoding_match is not None:
1920 xml_encoding = xml_encoding_match.groups()[0].lower()
1921 if isHTML:
1922 self.declaredHTMLEncoding = xml_encoding
1923 if sniffed_xml_encoding and \
1924 (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
1925 'iso-10646-ucs-4', 'ucs-4', 'csucs4',
1926 'utf-16', 'utf-32', 'utf_16', 'utf_32',
1927 'utf16', 'u16')):
1928 xml_encoding = sniffed_xml_encoding
1929 return xml_data, xml_encoding, sniffed_xml_encoding
1930
1931
1932 def find_codec(self, charset):
1933 return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
1934 or (charset and self._codec(charset.replace("-", ""))) \
1935 or (charset and self._codec(charset.replace("-", "_"))) \
1936 or charset
1937
1938 def _codec(self, charset):
1939 if not charset: return charset
1940 codec = None
1941 try:
1942 codecs.lookup(charset)
1943 codec = charset
1944 except (LookupError, ValueError):
1945 pass
1946 return codec
1947
1948 EBCDIC_TO_ASCII_MAP = None
1949 def _ebcdic_to_ascii(self, s):
1950 c = self.__class__
1951 if not c.EBCDIC_TO_ASCII_MAP:
1952 emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
1953 16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
1954 128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
1955 144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
1956 32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
1957 38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
1958 45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
1959 186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
1960 195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,
1961 201,202,106,107,108,109,110,111,112,113,114,203,204,205,
1962 206,207,208,209,126,115,116,117,118,119,120,121,122,210,
1963 211,212,213,214,215,216,217,218,219,220,221,222,223,224,
1964 225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72,
1965 73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81,
1966 82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89,
1967 90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57,
1968 250,251,252,253,254,255)
1969 import string
1970 c.EBCDIC_TO_ASCII_MAP = string.maketrans( \
1971 ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
1972 return s.translate(c.EBCDIC_TO_ASCII_MAP)
1973
1974 MS_CHARS = { '\x80' : ('euro', '20AC'),
1975 '\x81' : ' ',
1976 '\x82' : ('sbquo', '201A'),
1977 '\x83' : ('fnof', '192'),
1978 '\x84' : ('bdquo', '201E'),
1979 '\x85' : ('hellip', '2026'),
1980 '\x86' : ('dagger', '2020'),
1981 '\x87' : ('Dagger', '2021'),
1982 '\x88' : ('circ', '2C6'),
1983 '\x89' : ('permil', '2030'),
1984 '\x8A' : ('Scaron', '160'),
1985 '\x8B' : ('lsaquo', '2039'),
1986 '\x8C' : ('OElig', '152'),
1987 '\x8D' : '?',
1988 '\x8E' : ('#x17D', '17D'),
1989 '\x8F' : '?',
1990 '\x90' : '?',
1991 '\x91' : ('lsquo', '2018'),
1992 '\x92' : ('rsquo', '2019'),
1993 '\x93' : ('ldquo', '201C'),
1994 '\x94' : ('rdquo', '201D'),
1995 '\x95' : ('bull', '2022'),
1996 '\x96' : ('ndash', '2013'),
1997 '\x97' : ('mdash', '2014'),
1998 '\x98' : ('tilde', '2DC'),
1999 '\x99' : ('trade', '2122'),
2000 '\x9a' : ('scaron', '161'),
2001 '\x9b' : ('rsaquo', '203A'),
2002 '\x9c' : ('oelig', '153'),
2003 '\x9d' : '?',
2004 '\x9e' : ('#x17E', '17E'),
2005 '\x9f' : ('Yuml', ''),}
2006
2007 #######################################################################
2008
2009
2010 #By default, act as an HTML pretty-printer.
2011 if __name__ == '__main__':
2012 import sys
2013 soup = BeautifulSoup(sys.stdin)
2014 print soup.prettify()