Package lxml :: Package html
[hide private]
[frames] | no frames]

Source Code for Package lxml.html

   1  # Copyright (c) 2004 Ian Bicking. All rights reserved. 
   2  # 
   3  # Redistribution and use in source and binary forms, with or without 
   4  # modification, are permitted provided that the following conditions are 
   5  # met: 
   6  # 
   7  # 1. Redistributions of source code must retain the above copyright 
   8  # notice, this list of conditions and the following disclaimer. 
   9  # 
  10  # 2. Redistributions in binary form must reproduce the above copyright 
  11  # notice, this list of conditions and the following disclaimer in 
  12  # the documentation and/or other materials provided with the 
  13  # distribution. 
  14  # 
  15  # 3. Neither the name of Ian Bicking nor the names of its contributors may 
  16  # be used to endorse or promote products derived from this software 
  17  # without specific prior written permission. 
  18  # 
  19  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
  20  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
  21  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
  22  # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL IAN BICKING OR 
  23  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  24  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  25  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
  26  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
  27  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
  28  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
  29  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
  30   
  31  """The ``lxml.html`` tool set for HTML handling. 
  32  """ 
  33   
  34  from __future__ import absolute_import 
  35   
  36  __all__ = [ 
  37      'document_fromstring', 'fragment_fromstring', 'fragments_fromstring', 'fromstring', 
  38      'tostring', 'Element', 'defs', 'open_in_browser', 'submit_form', 
  39      'find_rel_links', 'find_class', 'make_links_absolute', 
  40      'resolve_base_href', 'iterlinks', 'rewrite_links', 'open_in_browser', 'parse'] 
  41   
  42   
  43  import copy 
  44  import sys 
  45  import re 
  46  from functools import partial 
  47   
  48  try: 
  49      # while unnecessary, importing from 'collections.abc' is the right way to do it 
  50      from collections.abc import MutableMapping, MutableSet 
  51  except ImportError: 
  52      from collections import MutableMapping, MutableSet 
  53   
  54  from .. import etree 
  55  from . import defs 
  56  from ._setmixin import SetMixin 
  57   
  58  try: 
  59      from urlparse import urljoin 
  60  except ImportError: 
  61      # Python 3 
  62      from urllib.parse import urljoin 
  63   
  64  try: 
  65      unicode 
  66  except NameError: 
  67      # Python 3 
  68      unicode = str 
  69  try: 
  70      basestring 
  71  except NameError: 
  72      # Python 3 
  73      basestring = (str, bytes) 
74 75 76 -def __fix_docstring(s):
77 if not s: 78 return s 79 if sys.version_info[0] >= 3: 80 sub = re.compile(r"^(\s*)u'", re.M).sub 81 else: 82 sub = re.compile(r"^(\s*)b'", re.M).sub 83 return sub(r"\1'", s)
84 85 86 XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" 87 88 _rel_links_xpath = etree.XPath("descendant-or-self::a[@rel]|descendant-or-self::x:a[@rel]", 89 namespaces={'x':XHTML_NAMESPACE}) 90 _options_xpath = etree.XPath("descendant-or-self::option|descendant-or-self::x:option", 91 namespaces={'x':XHTML_NAMESPACE}) 92 _forms_xpath = etree.XPath("descendant-or-self::form|descendant-or-self::x:form", 93 namespaces={'x':XHTML_NAMESPACE}) 94 #_class_xpath = etree.XPath(r"descendant-or-self::*[regexp:match(@class, concat('\b', $class_name, '\b'))]", {'regexp': 'http://exslt.org/regular-expressions'}) 95 _class_xpath = etree.XPath("descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), concat(' ', $class_name, ' '))]") 96 _id_xpath = etree.XPath("descendant-or-self::*[@id=$id]") 97 _collect_string_content = etree.XPath("string()") 98 _iter_css_urls = re.compile(r'url\(('+'["][^"]*["]|'+"['][^']*[']|"+r'[^)]*)\)', re.I).finditer 99 _iter_css_imports = re.compile(r'@import "(.*?)"').finditer 100 _label_xpath = etree.XPath("//label[@for=$id]|//x:label[@for=$id]", 101 namespaces={'x':XHTML_NAMESPACE}) 102 _archive_re = re.compile(r'[^ ]+') 103 _parse_meta_refresh_url = re.compile( 104 r'[^;=]*;\s*(?:url\s*=\s*)?(?P<url>.*)$', re.I).search
105 106 107 -def _unquote_match(s, pos):
108 if s[:1] == '"' and s[-1:] == '"' or s[:1] == "'" and s[-1:] == "'": 109 return s[1:-1], pos+1 110 else: 111 return s,pos
112
113 114 -def _transform_result(typ, result):
115 """Convert the result back into the input type. 116 """ 117 if issubclass(typ, bytes): 118 return tostring(result, encoding='utf-8') 119 elif issubclass(typ, unicode): 120 return tostring(result, encoding='unicode') 121 else: 122 return result
123
124 125 -def _nons(tag):
126 if isinstance(tag, basestring): 127 if tag[0] == '{' and tag[1:len(XHTML_NAMESPACE)+1] == XHTML_NAMESPACE: 128 return tag.split('}')[-1] 129 return tag
130
131 132 -class Classes(MutableSet):
133 """Provides access to an element's class attribute as a set-like collection. 134 Usage:: 135 136 >>> el = fromstring('<p class="hidden large">Text</p>') 137 >>> classes = el.classes # or: classes = Classes(el.attrib) 138 >>> classes |= ['block', 'paragraph'] 139 >>> el.get('class') 140 'hidden large block paragraph' 141 >>> classes.toggle('hidden') 142 False 143 >>> el.get('class') 144 'large block paragraph' 145 >>> classes -= ('some', 'classes', 'block') 146 >>> el.get('class') 147 'large paragraph' 148 """
149 - def __init__(self, attributes):
150 self._attributes = attributes 151 self._get_class_value = partial(attributes.get, 'class', '')
152
153 - def add(self, value):
154 """ 155 Add a class. 156 157 This has no effect if the class is already present. 158 """ 159 if not value or re.search(r'\s', value): 160 raise ValueError("Invalid class name: %r" % value) 161 classes = self._get_class_value().split() 162 if value in classes: 163 return 164 classes.append(value) 165 self._attributes['class'] = ' '.join(classes)
166
167 - def discard(self, value):
168 """ 169 Remove a class if it is currently present. 170 171 If the class is not present, do nothing. 172 """ 173 if not value or re.search(r'\s', value): 174 raise ValueError("Invalid class name: %r" % value) 175 classes = [name for name in self._get_class_value().split() 176 if name != value] 177 if classes: 178 self._attributes['class'] = ' '.join(classes) 179 elif 'class' in self._attributes: 180 del self._attributes['class']
181
182 - def remove(self, value):
183 """ 184 Remove a class; it must currently be present. 185 186 If the class is not present, raise a KeyError. 187 """ 188 if not value or re.search(r'\s', value): 189 raise ValueError("Invalid class name: %r" % value) 190 super(Classes, self).remove(value)
191
192 - def __contains__(self, name):
193 classes = self._get_class_value() 194 return name in classes and name in classes.split()
195
196 - def __iter__(self):
197 return iter(self._get_class_value().split())
198
199 - def __len__(self):
200 return len(self._get_class_value().split())
201 202 # non-standard methods 203
204 - def update(self, values):
205 """ 206 Add all names from 'values'. 207 """ 208 classes = self._get_class_value().split() 209 extended = False 210 for value in values: 211 if value not in classes: 212 classes.append(value) 213 extended = True 214 if extended: 215 self._attributes['class'] = ' '.join(classes)
216
217 - def toggle(self, value):
218 """ 219 Add a class name if it isn't there yet, or remove it if it exists. 220 221 Returns true if the class was added (and is now enabled) and 222 false if it was removed (and is now disabled). 223 """ 224 if not value or re.search(r'\s', value): 225 raise ValueError("Invalid class name: %r" % value) 226 classes = self._get_class_value().split() 227 try: 228 classes.remove(value) 229 enabled = False 230 except ValueError: 231 classes.append(value) 232 enabled = True 233 if classes: 234 self._attributes['class'] = ' '.join(classes) 235 else: 236 del self._attributes['class'] 237 return enabled
238
239 240 -class HtmlMixin(object):
241
242 - def set(self, key, value=None):
243 """set(self, key, value=None) 244 245 Sets an element attribute. If no value is provided, or if the value is None, 246 creates a 'boolean' attribute without value, e.g. "<form novalidate></form>" 247 for ``form.set('novalidate')``. 248 """ 249 super(HtmlElement, self).set(key, value)
250 251 @property
252 - def classes(self):
253 """ 254 A set-like wrapper around the 'class' attribute. 255 """ 256 return Classes(self.attrib)
257 258 @classes.setter
259 - def classes(self, classes):
260 assert isinstance(classes, Classes) # only allow "el.classes |= ..." etc. 261 value = classes._get_class_value() 262 if value: 263 self.set('class', value) 264 elif self.get('class') is not None: 265 del self.attrib['class']
266 267 @property
268 - def base_url(self):
269 """ 270 Returns the base URL, given when the page was parsed. 271 272 Use with ``urlparse.urljoin(el.base_url, href)`` to get 273 absolute URLs. 274 """ 275 return self.getroottree().docinfo.URL
276 277 @property
278 - def forms(self):
279 """ 280 Return a list of all the forms 281 """ 282 return _forms_xpath(self)
283 284 @property
285 - def body(self):
286 """ 287 Return the <body> element. Can be called from a child element 288 to get the document's head. 289 """ 290 return self.xpath('//body|//x:body', namespaces={'x':XHTML_NAMESPACE})[0]
291 292 @property
293 - def head(self):
294 """ 295 Returns the <head> element. Can be called from a child 296 element to get the document's head. 297 """ 298 return self.xpath('//head|//x:head', namespaces={'x':XHTML_NAMESPACE})[0]
299 300 @property
301 - def label(self):
302 """ 303 Get or set any <label> element associated with this element. 304 """ 305 id = self.get('id') 306 if not id: 307 return None 308 result = _label_xpath(self, id=id) 309 if not result: 310 return None 311 else: 312 return result[0]
313 314 @label.setter
315 - def label(self, label):
316 id = self.get('id') 317 if not id: 318 raise TypeError( 319 "You cannot set a label for an element (%r) that has no id" 320 % self) 321 if _nons(label.tag) != 'label': 322 raise TypeError( 323 "You can only assign label to a label element (not %r)" 324 % label) 325 label.set('for', id)
326 327 @label.deleter
328 - def label(self):
329 label = self.label 330 if label is not None: 331 del label.attrib['for']
332
333 - def drop_tree(self):
334 """ 335 Removes this element from the tree, including its children and 336 text. The tail text is joined to the previous element or 337 parent. 338 """ 339 parent = self.getparent() 340 assert parent is not None 341 if self.tail: 342 previous = self.getprevious() 343 if previous is None: 344 parent.text = (parent.text or '') + self.tail 345 else: 346 previous.tail = (previous.tail or '') + self.tail 347 parent.remove(self)
348
349 - def drop_tag(self):
350 """ 351 Remove the tag, but not its children or text. The children and text 352 are merged into the parent. 353 354 Example:: 355 356 >>> h = fragment_fromstring('<div>Hello <b>World!</b></div>') 357 >>> h.find('.//b').drop_tag() 358 >>> print(tostring(h, encoding='unicode')) 359 <div>Hello World!</div> 360 """ 361 parent = self.getparent() 362 assert parent is not None 363 previous = self.getprevious() 364 if self.text and isinstance(self.tag, basestring): 365 # not a Comment, etc. 366 if previous is None: 367 parent.text = (parent.text or '') + self.text 368 else: 369 previous.tail = (previous.tail or '') + self.text 370 if self.tail: 371 if len(self): 372 last = self[-1] 373 last.tail = (last.tail or '') + self.tail 374 elif previous is None: 375 parent.text = (parent.text or '') + self.tail 376 else: 377 previous.tail = (previous.tail or '') + self.tail 378 index = parent.index(self) 379 parent[index:index+1] = self[:]
380 388
389 - def find_class(self, class_name):
390 """ 391 Find any elements with the given class name. 392 """ 393 return _class_xpath(self, class_name=class_name)
394
395 - def get_element_by_id(self, id, *default):
396 """ 397 Get the first element in a document with the given id. If none is 398 found, return the default argument if provided or raise KeyError 399 otherwise. 400 401 Note that there can be more than one element with the same id, 402 and this isn't uncommon in HTML documents found in the wild. 403 Browsers return only the first match, and this function does 404 the same. 405 """ 406 try: 407 # FIXME: should this check for multiple matches? 408 # browsers just return the first one 409 return _id_xpath(self, id=id)[0] 410 except IndexError: 411 if default: 412 return default[0] 413 else: 414 raise KeyError(id)
415
416 - def text_content(self):
417 """ 418 Return the text content of the tag (and the text in any children). 419 """ 420 return _collect_string_content(self)
421
422 - def cssselect(self, expr, translator='html'):
423 """ 424 Run the CSS expression on this element and its children, 425 returning a list of the results. 426 427 Equivalent to lxml.cssselect.CSSSelect(expr, translator='html')(self) 428 -- note that pre-compiling the expression can provide a substantial 429 speedup. 430 """ 431 # Do the import here to make the dependency optional. 432 from lxml.cssselect import CSSSelector 433 return CSSSelector(expr, translator=translator)(self)
434 435 ######################################## 436 ## Link functions 437 ######################################## 438 469 elif handle_failures == 'discard': 470 def link_repl(href): 471 try: 472 return urljoin(base_url, href) 473 except ValueError: 474 return None
475 elif handle_failures is None: 476 def link_repl(href): 477 return urljoin(base_url, href) 478 else: 479 raise ValueError( 480 "unexpected value for handle_failures: %r" % handle_failures) 481 482 self.rewrite_links(link_repl) 483
484 - def resolve_base_href(self, handle_failures=None):
485 """ 486 Find any ``<base href>`` tag in the document, and apply its 487 values to all links found in the document. Also remove the 488 tag once it has been applied. 489 490 If ``handle_failures`` is None (default), a failure to process 491 a URL will abort the processing. If set to 'ignore', errors 492 are ignored. If set to 'discard', failing URLs will be removed. 493 """ 494 base_href = None 495 basetags = self.xpath('//base[@href]|//x:base[@href]', 496 namespaces={'x': XHTML_NAMESPACE}) 497 for b in basetags: 498 base_href = b.get('href') 499 b.drop_tree() 500 if not base_href: 501 return 502 self.make_links_absolute(base_href, resolve_base_href=False, 503 handle_failures=handle_failures)
504 594 643
644 645 -class _MethodFunc(object):
646 """ 647 An object that represents a method on an element as a function; 648 the function takes either an element or an HTML string. It 649 returns whatever the function normally returns, or if the function 650 works in-place (and so returns None) it returns a serialized form 651 of the resulting document. 652 """
653 - def __init__(self, name, copy=False, source_class=HtmlMixin):
654 self.name = name 655 self.copy = copy 656 self.__doc__ = getattr(source_class, self.name).__doc__
657 - def __call__(self, doc, *args, **kw):
658 result_type = type(doc) 659 if isinstance(doc, basestring): 660 if 'copy' in kw: 661 raise TypeError( 662 "The keyword 'copy' can only be used with element inputs to %s, not a string input" % self.name) 663 doc = fromstring(doc, **kw) 664 else: 665 if 'copy' in kw: 666 make_a_copy = kw.pop('copy') 667 else: 668 make_a_copy = self.copy 669 if make_a_copy: 670 doc = copy.deepcopy(doc) 671 meth = getattr(doc, self.name) 672 result = meth(*args, **kw) 673 # FIXME: this None test is a bit sloppy 674 if result is None: 675 # Then return what we got in 676 return _transform_result(result_type, doc) 677 else: 678 return result
679 680 681 find_rel_links = _MethodFunc('find_rel_links', copy=False) 682 find_class = _MethodFunc('find_class', copy=False) 683 make_links_absolute = _MethodFunc('make_links_absolute', copy=True) 684 resolve_base_href = _MethodFunc('resolve_base_href', copy=True) 685 iterlinks = _MethodFunc('iterlinks', copy=False) 686 rewrite_links = _MethodFunc('rewrite_links', copy=True)
687 688 689 -class HtmlComment(etree.CommentBase, HtmlMixin):
690 pass
691
692 693 -class HtmlElement(etree.ElementBase, HtmlMixin):
694 # Override etree.ElementBase.cssselect() and set(), despite the MRO (FIXME: change base order?) 695 cssselect = HtmlMixin.cssselect 696 set = HtmlMixin.set
697
698 699 -class HtmlProcessingInstruction(etree.PIBase, HtmlMixin):
700 pass
701
702 703 -class HtmlEntity(etree.EntityBase, HtmlMixin):
704 pass
705
706 707 -class HtmlElementClassLookup(etree.CustomElementClassLookup):
708 """A lookup scheme for HTML Element classes. 709 710 To create a lookup instance with different Element classes, pass a tag 711 name mapping of Element classes in the ``classes`` keyword argument and/or 712 a tag name mapping of Mixin classes in the ``mixins`` keyword argument. 713 The special key '*' denotes a Mixin class that should be mixed into all 714 Element classes. 715 """ 716 _default_element_classes = {} 717
718 - def __init__(self, classes=None, mixins=None):
719 etree.CustomElementClassLookup.__init__(self) 720 if classes is None: 721 classes = self._default_element_classes.copy() 722 if mixins: 723 mixers = {} 724 for name, value in mixins: 725 if name == '*': 726 for n in classes.keys(): 727 mixers.setdefault(n, []).append(value) 728 else: 729 mixers.setdefault(name, []).append(value) 730 for name, mix_bases in mixers.items(): 731 cur = classes.get(name, HtmlElement) 732 bases = tuple(mix_bases + [cur]) 733 classes[name] = type(cur.__name__, bases, {}) 734 self._element_classes = classes
735
736 - def lookup(self, node_type, document, namespace, name):
737 if node_type == 'element': 738 return self._element_classes.get(name.lower(), HtmlElement) 739 elif node_type == 'comment': 740 return HtmlComment 741 elif node_type == 'PI': 742 return HtmlProcessingInstruction 743 elif node_type == 'entity': 744 return HtmlEntity 745 # Otherwise normal lookup 746 return None
747 748 749 ################################################################################ 750 # parsing 751 ################################################################################ 752 753 _looks_like_full_html_unicode = re.compile( 754 unicode(r'^\s*<(?:html|!doctype)'), re.I).match 755 _looks_like_full_html_bytes = re.compile( 756 r'^\s*<(?:html|!doctype)'.encode('ascii'), re.I).match
757 758 759 -def document_fromstring(html, parser=None, ensure_head_body=False, **kw):
760 if parser is None: 761 parser = html_parser 762 value = etree.fromstring(html, parser, **kw) 763 if value is None: 764 raise etree.ParserError( 765 "Document is empty") 766 if ensure_head_body and value.find('head') is None: 767 value.insert(0, Element('head')) 768 if ensure_head_body and value.find('body') is None: 769 value.append(Element('body')) 770 return value
771
772 773 -def fragments_fromstring(html, no_leading_text=False, base_url=None, 774 parser=None, **kw):
775 """Parses several HTML elements, returning a list of elements. 776 777 The first item in the list may be a string. 778 If no_leading_text is true, then it will be an error if there is 779 leading text, and it will always be a list of only elements. 780 781 base_url will set the document's base_url attribute 782 (and the tree's docinfo.URL). 783 """ 784 if parser is None: 785 parser = html_parser 786 # FIXME: check what happens when you give html with a body, head, etc. 787 if isinstance(html, bytes): 788 if not _looks_like_full_html_bytes(html): 789 # can't use %-formatting in early Py3 versions 790 html = ('<html><body>'.encode('ascii') + html + 791 '</body></html>'.encode('ascii')) 792 else: 793 if not _looks_like_full_html_unicode(html): 794 html = '<html><body>%s</body></html>' % html 795 doc = document_fromstring(html, parser=parser, base_url=base_url, **kw) 796 assert _nons(doc.tag) == 'html' 797 bodies = [e for e in doc if _nons(e.tag) == 'body'] 798 assert len(bodies) == 1, ("too many bodies: %r in %r" % (bodies, html)) 799 body = bodies[0] 800 elements = [] 801 if no_leading_text and body.text and body.text.strip(): 802 raise etree.ParserError( 803 "There is leading text: %r" % body.text) 804 if body.text and body.text.strip(): 805 elements.append(body.text) 806 elements.extend(body) 807 # FIXME: removing the reference to the parent artificial document 808 # would be nice 809 return elements
810
811 812 -def fragment_fromstring(html, create_parent=False, base_url=None, 813 parser=None, **kw):
814 """ 815 Parses a single HTML element; it is an error if there is more than 816 one element, or if anything but whitespace precedes or follows the 817 element. 818 819 If ``create_parent`` is true (or is a tag name) then a parent node 820 will be created to encapsulate the HTML in a single element. In this 821 case, leading or trailing text is also allowed, as are multiple elements 822 as result of the parsing. 823 824 Passing a ``base_url`` will set the document's ``base_url`` attribute 825 (and the tree's docinfo.URL). 826 """ 827 if parser is None: 828 parser = html_parser 829 830 accept_leading_text = bool(create_parent) 831 832 elements = fragments_fromstring( 833 html, parser=parser, no_leading_text=not accept_leading_text, 834 base_url=base_url, **kw) 835 836 if create_parent: 837 if not isinstance(create_parent, basestring): 838 create_parent = 'div' 839 new_root = Element(create_parent) 840 if elements: 841 if isinstance(elements[0], basestring): 842 new_root.text = elements[0] 843 del elements[0] 844 new_root.extend(elements) 845 return new_root 846 847 if not elements: 848 raise etree.ParserError('No elements found') 849 if len(elements) > 1: 850 raise etree.ParserError( 851 "Multiple elements found (%s)" 852 % ', '.join([_element_name(e) for e in elements])) 853 el = elements[0] 854 if el.tail and el.tail.strip(): 855 raise etree.ParserError( 856 "Element followed by text: %r" % el.tail) 857 el.tail = None 858 return el
859
860 861 -def fromstring(html, base_url=None, parser=None, **kw):
862 """ 863 Parse the html, returning a single element/document. 864 865 This tries to minimally parse the chunk of text, without knowing if it 866 is a fragment or a document. 867 868 base_url will set the document's base_url attribute (and the tree's docinfo.URL) 869 """ 870 if parser is None: 871 parser = html_parser 872 if isinstance(html, bytes): 873 is_full_html = _looks_like_full_html_bytes(html) 874 else: 875 is_full_html = _looks_like_full_html_unicode(html) 876 doc = document_fromstring(html, parser=parser, base_url=base_url, **kw) 877 if is_full_html: 878 return doc 879 # otherwise, lets parse it out... 880 bodies = doc.findall('body') 881 if not bodies: 882 bodies = doc.findall('{%s}body' % XHTML_NAMESPACE) 883 if bodies: 884 body = bodies[0] 885 if len(bodies) > 1: 886 # Somehow there are multiple bodies, which is bad, but just 887 # smash them into one body 888 for other_body in bodies[1:]: 889 if other_body.text: 890 if len(body): 891 body[-1].tail = (body[-1].tail or '') + other_body.text 892 else: 893 body.text = (body.text or '') + other_body.text 894 body.extend(other_body) 895 # We'll ignore tail 896 # I guess we are ignoring attributes too 897 other_body.drop_tree() 898 else: 899 body = None 900 heads = doc.findall('head') 901 if not heads: 902 heads = doc.findall('{%s}head' % XHTML_NAMESPACE) 903 if heads: 904 # Well, we have some sort of structure, so lets keep it all 905 head = heads[0] 906 if len(heads) > 1: 907 for other_head in heads[1:]: 908 head.extend(other_head) 909 # We don't care about text or tail in a head 910 other_head.drop_tree() 911 return doc 912 if body is None: 913 return doc 914 if (len(body) == 1 and (not body.text or not body.text.strip()) 915 and (not body[-1].tail or not body[-1].tail.strip())): 916 # The body has just one element, so it was probably a single 917 # element passed in 918 return body[0] 919 # Now we have a body which represents a bunch of tags which have the 920 # content that was passed in. We will create a fake container, which 921 # is the body tag, except <body> implies too much structure. 922 if _contains_block_level_tag(body): 923 body.tag = 'div' 924 else: 925 body.tag = 'span' 926 return body
927
928 929 -def parse(filename_or_url, parser=None, base_url=None, **kw):
930 """ 931 Parse a filename, URL, or file-like object into an HTML document 932 tree. Note: this returns a tree, not an element. Use 933 ``parse(...).getroot()`` to get the document root. 934 935 You can override the base URL with the ``base_url`` keyword. This 936 is most useful when parsing from a file-like object. 937 """ 938 if parser is None: 939 parser = html_parser 940 return etree.parse(filename_or_url, parser, base_url=base_url, **kw)
941
942 943 -def _contains_block_level_tag(el):
944 # FIXME: I could do this with XPath, but would that just be 945 # unnecessarily slow? 946 for el in el.iter(etree.Element): 947 if _nons(el.tag) in defs.block_tags: 948 return True 949 return False
950
951 952 -def _element_name(el):
953 if isinstance(el, etree.CommentBase): 954 return 'comment' 955 elif isinstance(el, basestring): 956 return 'string' 957 else: 958 return _nons(el.tag)
959
960 961 ################################################################################ 962 # form handling 963 ################################################################################ 964 965 -class FormElement(HtmlElement):
966 """ 967 Represents a <form> element. 968 """ 969 970 @property
971 - def inputs(self):
972 """ 973 Returns an accessor for all the input elements in the form. 974 975 See `InputGetter` for more information about the object. 976 """ 977 return InputGetter(self)
978 979 @property
980 - def fields(self):
981 """ 982 Dictionary-like object that represents all the fields in this 983 form. You can set values in this dictionary to effect the 984 form. 985 """ 986 return FieldsDict(self.inputs)
987 988 @fields.setter
989 - def fields(self, value):
990 fields = self.fields 991 prev_keys = fields.keys() 992 for key, value in value.items(): 993 if key in prev_keys: 994 prev_keys.remove(key) 995 fields[key] = value 996 for key in prev_keys: 997 if key is None: 998 # Case of an unnamed input; these aren't really 999 # expressed in form_values() anyway. 1000 continue 1001 fields[key] = None
1002
1003 - def _name(self):
1004 if self.get('name'): 1005 return self.get('name') 1006 elif self.get('id'): 1007 return '#' + self.get('id') 1008 iter_tags = self.body.iter 1009 forms = list(iter_tags('form')) 1010 if not forms: 1011 forms = list(iter_tags('{%s}form' % XHTML_NAMESPACE)) 1012 return str(forms.index(self))
1013
1014 - def form_values(self):
1015 """ 1016 Return a list of tuples of the field values for the form. 1017 This is suitable to be passed to ``urllib.urlencode()``. 1018 """ 1019 results = [] 1020 for el in self.inputs: 1021 name = el.name 1022 if not name or 'disabled' in el.attrib: 1023 continue 1024 tag = _nons(el.tag) 1025 if tag == 'textarea': 1026 results.append((name, el.value)) 1027 elif tag == 'select': 1028 value = el.value 1029 if el.multiple: 1030 for v in value: 1031 results.append((name, v)) 1032 elif value is not None: 1033 results.append((name, el.value)) 1034 else: 1035 assert tag == 'input', ( 1036 "Unexpected tag: %r" % el) 1037 if el.checkable and not el.checked: 1038 continue 1039 if el.type in ('submit', 'image', 'reset', 'file'): 1040 continue 1041 value = el.value 1042 if value is not None: 1043 results.append((name, el.value)) 1044 return results
1045 1046 @property
1047 - def action(self):
1048 """ 1049 Get/set the form's ``action`` attribute. 1050 """ 1051 base_url = self.base_url 1052 action = self.get('action') 1053 if base_url and action is not None: 1054 return urljoin(base_url, action) 1055 else: 1056 return action
1057 1058 @action.setter
1059 - def action(self, value):
1060 self.set('action', value)
1061 1062 @action.deleter
1063 - def action(self):
1064 attrib = self.attrib 1065 if 'action' in attrib: 1066 del attrib['action']
1067 1068 @property
1069 - def method(self):
1070 """ 1071 Get/set the form's method. Always returns a capitalized 1072 string, and defaults to ``'GET'`` 1073 """ 1074 return self.get('method', 'GET').upper()
1075 1076 @method.setter
1077 - def method(self, value):
1078 self.set('method', value.upper())
1079 1080 1081 HtmlElementClassLookup._default_element_classes['form'] = FormElement
1082 1083 1084 -def submit_form(form, extra_values=None, open_http=None):
1085 """ 1086 Helper function to submit a form. Returns a file-like object, as from 1087 ``urllib.urlopen()``. This object also has a ``.geturl()`` function, 1088 which shows the URL if there were any redirects. 1089 1090 You can use this like:: 1091 1092 form = doc.forms[0] 1093 form.inputs['foo'].value = 'bar' # etc 1094 response = form.submit() 1095 doc = parse(response) 1096 doc.make_links_absolute(response.geturl()) 1097 1098 To change the HTTP requester, pass a function as ``open_http`` keyword 1099 argument that opens the URL for you. The function must have the following 1100 signature:: 1101 1102 open_http(method, URL, values) 1103 1104 The action is one of 'GET' or 'POST', the URL is the target URL as a 1105 string, and the values are a sequence of ``(name, value)`` tuples with the 1106 form data. 1107 """ 1108 values = form.form_values() 1109 if extra_values: 1110 if hasattr(extra_values, 'items'): 1111 extra_values = extra_values.items() 1112 values.extend(extra_values) 1113 if open_http is None: 1114 open_http = open_http_urllib 1115 if form.action: 1116 url = form.action 1117 else: 1118 url = form.base_url 1119 return open_http(form.method, url, values)
1120
1121 1122 -def open_http_urllib(method, url, values):
1123 if not url: 1124 raise ValueError("cannot submit, no URL provided") 1125 ## FIXME: should test that it's not a relative URL or something 1126 try: 1127 from urllib import urlencode, urlopen 1128 except ImportError: # Python 3 1129 from urllib.request import urlopen 1130 from urllib.parse import urlencode 1131 if method == 'GET': 1132 if '?' in url: 1133 url += '&' 1134 else: 1135 url += '?' 1136 url += urlencode(values) 1137 data = None 1138 else: 1139 data = urlencode(values) 1140 return urlopen(url, data)
1141
1142 1143 -class FieldsDict(MutableMapping):
1144
1145 - def __init__(self, inputs):
1146 self.inputs = inputs
1147 - def __getitem__(self, item):
1148 return self.inputs[item].value
1149 - def __setitem__(self, item, value):
1150 self.inputs[item].value = value
1151 - def __delitem__(self, item):
1152 raise KeyError( 1153 "You cannot remove keys from ElementDict")
1154 - def keys(self):
1155 return self.inputs.keys()
1156 - def __contains__(self, item):
1157 return item in self.inputs
1158 - def __iter__(self):
1159 return iter(self.inputs.keys())
1160 - def __len__(self):
1161 return len(self.inputs)
1162
1163 - def __repr__(self):
1164 return '<%s for form %s>' % ( 1165 self.__class__.__name__, 1166 self.inputs.form._name())
1167
1168 1169 -class InputGetter(object):
1170 1171 """ 1172 An accessor that represents all the input fields in a form. 1173 1174 You can get fields by name from this, with 1175 ``form.inputs['field_name']``. If there are a set of checkboxes 1176 with the same name, they are returned as a list (a `CheckboxGroup` 1177 which also allows value setting). Radio inputs are handled 1178 similarly. 1179 1180 You can also iterate over this to get all input elements. This 1181 won't return the same thing as if you get all the names, as 1182 checkboxes and radio elements are returned individually. 1183 """ 1184 1185 _name_xpath = etree.XPath(".//*[@name = $name and (local-name(.) = 'select' or local-name(.) = 'input' or local-name(.) = 'textarea')]") 1186 _all_xpath = etree.XPath(".//*[local-name() = 'select' or local-name() = 'input' or local-name() = 'textarea']") 1187
1188 - def __init__(self, form):
1189 self.form = form
1190
1191 - def __repr__(self):
1192 return '<%s for form %s>' % ( 1193 self.__class__.__name__, 1194 self.form._name())
1195 1196 ## FIXME: there should be more methods, and it's unclear if this is 1197 ## a dictionary-like object or list-like object 1198
1199 - def __getitem__(self, name):
1200 results = self._name_xpath(self.form, name=name) 1201 if results: 1202 type = results[0].get('type') 1203 if type == 'radio' and len(results) > 1: 1204 group = RadioGroup(results) 1205 group.name = name 1206 return group 1207 elif type == 'checkbox' and len(results) > 1: 1208 group = CheckboxGroup(results) 1209 group.name = name 1210 return group 1211 else: 1212 # I don't like throwing away elements like this 1213 return results[0] 1214 else: 1215 raise KeyError( 1216 "No input element with the name %r" % name)
1217
1218 - def __contains__(self, name):
1219 results = self._name_xpath(self.form, name=name) 1220 return bool(results)
1221
1222 - def keys(self):
1223 names = set() 1224 for el in self: 1225 names.add(el.name) 1226 if None in names: 1227 names.remove(None) 1228 return list(names)
1229
1230 - def __iter__(self):
1231 ## FIXME: kind of dumb to turn a list into an iterator, only 1232 ## to have it likely turned back into a list again :( 1233 return iter(self._all_xpath(self.form))
1234
1235 1236 -class InputMixin(object):
1237 """ 1238 Mix-in for all input elements (input, select, and textarea) 1239 """ 1240 @property
1241 - def name(self):
1242 """ 1243 Get/set the name of the element 1244 """ 1245 return self.get('name')
1246 1247 @name.setter
1248 - def name(self, value):
1249 self.set('name', value)
1250 1251 @name.deleter
1252 - def name(self):
1253 attrib = self.attrib 1254 if 'name' in attrib: 1255 del attrib['name']
1256
1257 - def __repr__(self):
1258 type_name = getattr(self, 'type', None) 1259 if type_name: 1260 type_name = ' type=%r' % type_name 1261 else: 1262 type_name = '' 1263 return '<%s %x name=%r%s>' % ( 1264 self.__class__.__name__, id(self), self.name, type_name)
1265
1266 1267 -class TextareaElement(InputMixin, HtmlElement):
1268 """ 1269 ``<textarea>`` element. You can get the name with ``.name`` and 1270 get/set the value with ``.value`` 1271 """ 1272 @property
1273 - def value(self):
1274 """ 1275 Get/set the value (which is the contents of this element) 1276 """ 1277 content = self.text or '' 1278 if self.tag.startswith("{%s}" % XHTML_NAMESPACE): 1279 serialisation_method = 'xml' 1280 else: 1281 serialisation_method = 'html' 1282 for el in self: 1283 # it's rare that we actually get here, so let's not use ''.join() 1284 content += etree.tostring( 1285 el, method=serialisation_method, encoding='unicode') 1286 return content
1287 1288 @value.setter
1289 - def value(self, value):
1290 del self[:] 1291 self.text = value
1292 1293 @value.deleter
1294 - def value(self):
1295 self.text = '' 1296 del self[:]
1297 1298 1299 HtmlElementClassLookup._default_element_classes['textarea'] = TextareaElement
1300 1301 1302 -class SelectElement(InputMixin, HtmlElement):
1303 """ 1304 ``<select>`` element. You can get the name with ``.name``. 1305 1306 ``.value`` will be the value of the selected option, unless this 1307 is a multi-select element (``<select multiple>``), in which case 1308 it will be a set-like object. In either case ``.value_options`` 1309 gives the possible values. 1310 1311 The boolean attribute ``.multiple`` shows if this is a 1312 multi-select. 1313 """ 1314 @property
1315 - def value(self):
1316 """ 1317 Get/set the value of this select (the selected option). 1318 1319 If this is a multi-select, this is a set-like object that 1320 represents all the selected options. 1321 """ 1322 if self.multiple: 1323 return MultipleSelectOptions(self) 1324 for el in _options_xpath(self): 1325 if el.get('selected') is not None: 1326 value = el.get('value') 1327 if value is None: 1328 value = el.text or '' 1329 if value: 1330 value = value.strip() 1331 return value 1332 return None
1333 1334 @value.setter
1335 - def value(self, value):
1336 if self.multiple: 1337 if isinstance(value, basestring): 1338 raise TypeError("You must pass in a sequence") 1339 values = self.value 1340 values.clear() 1341 values.update(value) 1342 return 1343 checked_option = None 1344 if value is not None: 1345 value = value.strip() 1346 for el in _options_xpath(self): 1347 opt_value = el.get('value') 1348 if opt_value is None: 1349 opt_value = el.text or '' 1350 if opt_value: 1351 opt_value = opt_value.strip() 1352 if opt_value == value: 1353 checked_option = el 1354 break 1355 else: 1356 raise ValueError( 1357 "There is no option with the value of %r" % value) 1358 for el in _options_xpath(self): 1359 if 'selected' in el.attrib: 1360 del el.attrib['selected'] 1361 if checked_option is not None: 1362 checked_option.set('selected', '')
1363 1364 @value.deleter
1365 - def value(self):
1366 # FIXME: should del be allowed at all? 1367 if self.multiple: 1368 self.value.clear() 1369 else: 1370 self.value = None
1371 1372 @property
1373 - def value_options(self):
1374 """ 1375 All the possible values this select can have (the ``value`` 1376 attribute of all the ``<option>`` elements. 1377 """ 1378 options = [] 1379 for el in _options_xpath(self): 1380 value = el.get('value') 1381 if value is None: 1382 value = el.text or '' 1383 if value: 1384 value = value.strip() 1385 options.append(value) 1386 return options
1387 1388 @property
1389 - def multiple(self):
1390 """ 1391 Boolean attribute: is there a ``multiple`` attribute on this element. 1392 """ 1393 return 'multiple' in self.attrib
1394 1395 @multiple.setter
1396 - def multiple(self, value):
1397 if value: 1398 self.set('multiple', '') 1399 elif 'multiple' in self.attrib: 1400 del self.attrib['multiple']
1401 1402 1403 HtmlElementClassLookup._default_element_classes['select'] = SelectElement
1404 1405 1406 -class MultipleSelectOptions(SetMixin):
1407 """ 1408 Represents all the selected options in a ``<select multiple>`` element. 1409 1410 You can add to this set-like option to select an option, or remove 1411 to unselect the option. 1412 """ 1413
1414 - def __init__(self, select):
1415 self.select = select
1416 1417 @property
1418 - def options(self):
1419 """ 1420 Iterator of all the ``<option>`` elements. 1421 """ 1422 return iter(_options_xpath(self.select))
1423
1424 - def __iter__(self):
1425 for option in self.options: 1426 if 'selected' in option.attrib: 1427 opt_value = option.get('value') 1428 if opt_value is None: 1429 opt_value = option.text or '' 1430 if opt_value: 1431 opt_value = opt_value.strip() 1432 yield opt_value
1433
1434 - def add(self, item):
1435 for option in self.options: 1436 opt_value = option.get('value') 1437 if opt_value is None: 1438 opt_value = option.text or '' 1439 if opt_value: 1440 opt_value = opt_value.strip() 1441 if opt_value == item: 1442 option.set('selected', '') 1443 break 1444 else: 1445 raise ValueError( 1446 "There is no option with the value %r" % item)
1447
1448 - def remove(self, item):
1449 for option in self.options: 1450 opt_value = option.get('value') 1451 if opt_value is None: 1452 opt_value = option.text or '' 1453 if opt_value: 1454 opt_value = opt_value.strip() 1455 if opt_value == item: 1456 if 'selected' in option.attrib: 1457 del option.attrib['selected'] 1458 else: 1459 raise ValueError( 1460 "The option %r is not currently selected" % item) 1461 break 1462 else: 1463 raise ValueError( 1464 "There is not option with the value %r" % item)
1465
1466 - def __repr__(self):
1467 return '<%s {%s} for select name=%r>' % ( 1468 self.__class__.__name__, 1469 ', '.join([repr(v) for v in self]), 1470 self.select.name)
1471
1472 1473 -class RadioGroup(list):
1474 """ 1475 This object represents several ``<input type=radio>`` elements 1476 that have the same name. 1477 1478 You can use this like a list, but also use the property 1479 ``.value`` to check/uncheck inputs. Also you can use 1480 ``.value_options`` to get the possible values. 1481 """ 1482 @property
1483 - def value(self):
1484 """ 1485 Get/set the value, which checks the radio with that value (and 1486 unchecks any other value). 1487 """ 1488 for el in self: 1489 if 'checked' in el.attrib: 1490 return el.get('value') 1491 return None
1492 1493 @value.setter
1494 - def value(self, value):
1495 checked_option = None 1496 if value is not None: 1497 for el in self: 1498 if el.get('value') == value: 1499 checked_option = el 1500 break 1501 else: 1502 raise ValueError("There is no radio input with the value %r" % value) 1503 for el in self: 1504 if 'checked' in el.attrib: 1505 del el.attrib['checked'] 1506 if checked_option is not None: 1507 checked_option.set('checked', '')
1508 1509 @value.deleter
1510 - def value(self):
1511 self.value = None
1512 1513 @property
1514 - def value_options(self):
1515 """ 1516 Returns a list of all the possible values. 1517 """ 1518 return [el.get('value') for el in self]
1519
1520 - def __repr__(self):
1521 return '%s(%s)' % ( 1522 self.__class__.__name__, 1523 list.__repr__(self))
1524
1525 1526 -class CheckboxGroup(list):
1527 """ 1528 Represents a group of checkboxes (``<input type=checkbox>``) that 1529 have the same name. 1530 1531 In addition to using this like a list, the ``.value`` attribute 1532 returns a set-like object that you can add to or remove from to 1533 check and uncheck checkboxes. You can also use ``.value_options`` 1534 to get the possible values. 1535 """ 1536 @property
1537 - def value(self):
1538 """ 1539 Return a set-like object that can be modified to check or 1540 uncheck individual checkboxes according to their value. 1541 """ 1542 return CheckboxValues(self)
1543 1544 @value.setter
1545 - def value(self, value):
1546 values = self.value 1547 values.clear() 1548 if not hasattr(value, '__iter__'): 1549 raise ValueError( 1550 "A CheckboxGroup (name=%r) must be set to a sequence (not %r)" 1551 % (self[0].name, value)) 1552 values.update(value)
1553 1554 @value.deleter
1555 - def value(self):
1556 self.value.clear()
1557 1558 @property
1559 - def value_options(self):
1560 """ 1561 Returns a list of all the possible values. 1562 """ 1563 return [el.get('value') for el in self]
1564
1565 - def __repr__(self):
1566 return '%s(%s)' % ( 1567 self.__class__.__name__, list.__repr__(self))
1568
1569 1570 -class CheckboxValues(SetMixin):
1571 """ 1572 Represents the values of the checked checkboxes in a group of 1573 checkboxes with the same name. 1574 """ 1575
1576 - def __init__(self, group):
1577 self.group = group
1578
1579 - def __iter__(self):
1580 return iter([ 1581 el.get('value') 1582 for el in self.group 1583 if 'checked' in el.attrib])
1584
1585 - def add(self, value):
1586 for el in self.group: 1587 if el.get('value') == value: 1588 el.set('checked', '') 1589 break 1590 else: 1591 raise KeyError("No checkbox with value %r" % value)
1592
1593 - def remove(self, value):
1594 for el in self.group: 1595 if el.get('value') == value: 1596 if 'checked' in el.attrib: 1597 del el.attrib['checked'] 1598 else: 1599 raise KeyError( 1600 "The checkbox with value %r was already unchecked" % value) 1601 break 1602 else: 1603 raise KeyError( 1604 "No checkbox with value %r" % value)
1605
1606 - def __repr__(self):
1607 return '<%s {%s} for checkboxes name=%r>' % ( 1608 self.__class__.__name__, 1609 ', '.join([repr(v) for v in self]), 1610 self.group.name)
1611
1612 1613 -class InputElement(InputMixin, HtmlElement):
1614 """ 1615 Represents an ``<input>`` element. 1616 1617 You can get the type with ``.type`` (which is lower-cased and 1618 defaults to ``'text'``). 1619 1620 Also you can get and set the value with ``.value`` 1621 1622 Checkboxes and radios have the attribute ``input.checkable == 1623 True`` (for all others it is false) and a boolean attribute 1624 ``.checked``. 1625 1626 """ 1627 1628 ## FIXME: I'm a little uncomfortable with the use of .checked 1629 @property
1630 - def value(self):
1631 """ 1632 Get/set the value of this element, using the ``value`` attribute. 1633 1634 Also, if this is a checkbox and it has no value, this defaults 1635 to ``'on'``. If it is a checkbox or radio that is not 1636 checked, this returns None. 1637 """ 1638 if self.checkable: 1639 if self.checked: 1640 return self.get('value') or 'on' 1641 else: 1642 return None 1643 return self.get('value')
1644 1645 @value.setter
1646 - def value(self, value):
1647 if self.checkable: 1648 if not value: 1649 self.checked = False 1650 else: 1651 self.checked = True 1652 if isinstance(value, basestring): 1653 self.set('value', value) 1654 else: 1655 self.set('value', value)
1656 1657 @value.deleter
1658 - def value(self):
1659 if self.checkable: 1660 self.checked = False 1661 else: 1662 if 'value' in self.attrib: 1663 del self.attrib['value']
1664 1665 @property
1666 - def type(self):
1667 """ 1668 Return the type of this element (using the type attribute). 1669 """ 1670 return self.get('type', 'text').lower()
1671 1672 @type.setter
1673 - def type(self, value):
1674 self.set('type', value)
1675 1676 @property
1677 - def checkable(self):
1678 """ 1679 Boolean: can this element be checked? 1680 """ 1681 return self.type in ('checkbox', 'radio')
1682 1683 @property
1684 - def checked(self):
1685 """ 1686 Boolean attribute to get/set the presence of the ``checked`` 1687 attribute. 1688 1689 You can only use this on checkable input types. 1690 """ 1691 if not self.checkable: 1692 raise AttributeError('Not a checkable input type') 1693 return 'checked' in self.attrib
1694 1695 @checked.setter
1696 - def checked(self, value):
1697 if not self.checkable: 1698 raise AttributeError('Not a checkable input type') 1699 if value: 1700 self.set('checked', '') 1701 else: 1702 attrib = self.attrib 1703 if 'checked' in attrib: 1704 del attrib['checked']
1705 1706 1707 HtmlElementClassLookup._default_element_classes['input'] = InputElement
1708 1709 1710 -class LabelElement(HtmlElement):
1711 """ 1712 Represents a ``<label>`` element. 1713 1714 Label elements are linked to other elements with their ``for`` 1715 attribute. You can access this element with ``label.for_element``. 1716 """ 1717 @property
1718 - def for_element(self):
1719 """ 1720 Get/set the element this label points to. Return None if it 1721 can't be found. 1722 """ 1723 id = self.get('for') 1724 if not id: 1725 return None 1726 return self.body.get_element_by_id(id)
1727 1728 @for_element.setter
1729 - def for_element(self, other):
1730 id = other.get('id') 1731 if not id: 1732 raise TypeError( 1733 "Element %r has no id attribute" % other) 1734 self.set('for', id)
1735 1736 @for_element.deleter
1737 - def for_element(self):
1738 attrib = self.attrib 1739 if 'id' in attrib: 1740 del attrib['id']
1741 1742 1743 HtmlElementClassLookup._default_element_classes['label'] = LabelElement
1744 1745 1746 ############################################################ 1747 ## Serialization 1748 ############################################################ 1749 1750 -def html_to_xhtml(html):
1751 """Convert all tags in an HTML tree to XHTML by moving them to the 1752 XHTML namespace. 1753 """ 1754 try: 1755 html = html.getroot() 1756 except AttributeError: 1757 pass 1758 prefix = "{%s}" % XHTML_NAMESPACE 1759 for el in html.iter(etree.Element): 1760 tag = el.tag 1761 if tag[0] != '{': 1762 el.tag = prefix + tag
1763
1764 1765 -def xhtml_to_html(xhtml):
1766 """Convert all tags in an XHTML tree to HTML by removing their 1767 XHTML namespace. 1768 """ 1769 try: 1770 xhtml = xhtml.getroot() 1771 except AttributeError: 1772 pass 1773 prefix = "{%s}" % XHTML_NAMESPACE 1774 prefix_len = len(prefix) 1775 for el in xhtml.iter(prefix + "*"): 1776 el.tag = el.tag[prefix_len:]
1777 1778 1779 # This isn't a general match, but it's a match for what libxml2 1780 # specifically serialises: 1781 __str_replace_meta_content_type = re.compile( 1782 r'<meta http-equiv="Content-Type"[^>]*>').sub 1783 __bytes_replace_meta_content_type = re.compile( 1784 r'<meta http-equiv="Content-Type"[^>]*>'.encode('ASCII')).sub
1785 1786 1787 -def tostring(doc, pretty_print=False, include_meta_content_type=False, 1788 encoding=None, method="html", with_tail=True, doctype=None):
1789 """Return an HTML string representation of the document. 1790 1791 Note: if include_meta_content_type is true this will create a 1792 ``<meta http-equiv="Content-Type" ...>`` tag in the head; 1793 regardless of the value of include_meta_content_type any existing 1794 ``<meta http-equiv="Content-Type" ...>`` tag will be removed 1795 1796 The ``encoding`` argument controls the output encoding (defauts to 1797 ASCII, with &#...; character references for any characters outside 1798 of ASCII). Note that you can pass the name ``'unicode'`` as 1799 ``encoding`` argument to serialise to a Unicode string. 1800 1801 The ``method`` argument defines the output method. It defaults to 1802 'html', but can also be 'xml' for xhtml output, or 'text' to 1803 serialise to plain text without markup. 1804 1805 To leave out the tail text of the top-level element that is being 1806 serialised, pass ``with_tail=False``. 1807 1808 The ``doctype`` option allows passing in a plain string that will 1809 be serialised before the XML tree. Note that passing in non 1810 well-formed content here will make the XML output non well-formed. 1811 Also, an existing doctype in the document tree will not be removed 1812 when serialising an ElementTree instance. 1813 1814 Example:: 1815 1816 >>> from lxml import html 1817 >>> root = html.fragment_fromstring('<p>Hello<br>world!</p>') 1818 1819 >>> html.tostring(root) 1820 b'<p>Hello<br>world!</p>' 1821 >>> html.tostring(root, method='html') 1822 b'<p>Hello<br>world!</p>' 1823 1824 >>> html.tostring(root, method='xml') 1825 b'<p>Hello<br/>world!</p>' 1826 1827 >>> html.tostring(root, method='text') 1828 b'Helloworld!' 1829 1830 >>> html.tostring(root, method='text', encoding='unicode') 1831 u'Helloworld!' 1832 1833 >>> root = html.fragment_fromstring('<div><p>Hello<br>world!</p>TAIL</div>') 1834 >>> html.tostring(root[0], method='text', encoding='unicode') 1835 u'Helloworld!TAIL' 1836 1837 >>> html.tostring(root[0], method='text', encoding='unicode', with_tail=False) 1838 u'Helloworld!' 1839 1840 >>> doc = html.document_fromstring('<p>Hello<br>world!</p>') 1841 >>> html.tostring(doc, method='html', encoding='unicode') 1842 u'<html><body><p>Hello<br>world!</p></body></html>' 1843 1844 >>> print(html.tostring(doc, method='html', encoding='unicode', 1845 ... doctype='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"' 1846 ... ' "http://www.w3.org/TR/html4/strict.dtd">')) 1847 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 1848 <html><body><p>Hello<br>world!</p></body></html> 1849 """ 1850 html = etree.tostring(doc, method=method, pretty_print=pretty_print, 1851 encoding=encoding, with_tail=with_tail, 1852 doctype=doctype) 1853 if method == 'html' and not include_meta_content_type: 1854 if isinstance(html, str): 1855 html = __str_replace_meta_content_type('', html) 1856 else: 1857 html = __bytes_replace_meta_content_type(bytes(), html) 1858 return html
1859 1860 1861 tostring.__doc__ = __fix_docstring(tostring.__doc__)
1862 1863 1864 -def open_in_browser(doc, encoding=None):
1865 """ 1866 Open the HTML document in a web browser, saving it to a temporary 1867 file to open it. Note that this does not delete the file after 1868 use. This is mainly meant for debugging. 1869 """ 1870 import os 1871 import webbrowser 1872 import tempfile 1873 if not isinstance(doc, etree._ElementTree): 1874 doc = etree.ElementTree(doc) 1875 handle, fn = tempfile.mkstemp(suffix='.html') 1876 f = os.fdopen(handle, 'wb') 1877 try: 1878 doc.write(f, method="html", encoding=encoding or doc.docinfo.encoding or "UTF-8") 1879 finally: 1880 # we leak the file itself here, but we should at least close it 1881 f.close() 1882 url = 'file://' + fn.replace(os.path.sep, '/') 1883 print(url) 1884 webbrowser.open(url)
1885
1886 1887 ################################################################################ 1888 # configure Element class lookup 1889 ################################################################################ 1890 1891 -class HTMLParser(etree.HTMLParser):
1892 """An HTML parser that is configured to return lxml.html Element 1893 objects. 1894 """
1895 - def __init__(self, **kwargs):
1896 super(HTMLParser, self).__init__(**kwargs) 1897 self.set_element_class_lookup(HtmlElementClassLookup())
1898
1899 1900 -class XHTMLParser(etree.XMLParser):
1901 """An XML parser that is configured to return lxml.html Element 1902 objects. 1903 1904 Note that this parser is not really XHTML aware unless you let it 1905 load a DTD that declares the HTML entities. To do this, make sure 1906 you have the XHTML DTDs installed in your catalogs, and create the 1907 parser like this:: 1908 1909 >>> parser = XHTMLParser(load_dtd=True) 1910 1911 If you additionally want to validate the document, use this:: 1912 1913 >>> parser = XHTMLParser(dtd_validation=True) 1914 1915 For catalog support, see http://www.xmlsoft.org/catalog.html. 1916 """
1917 - def __init__(self, **kwargs):
1918 super(XHTMLParser, self).__init__(**kwargs) 1919 self.set_element_class_lookup(HtmlElementClassLookup())
1920
1921 1922 -def Element(*args, **kw):
1923 """Create a new HTML Element. 1924 1925 This can also be used for XHTML documents. 1926 """ 1927 v = html_parser.makeelement(*args, **kw) 1928 return v
1929 1930 1931 html_parser = HTMLParser() 1932 xhtml_parser = XHTMLParser() 1933