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  import sys 
  35  import re 
  36  try: 
  37      from urlparse import urljoin 
  38  except ImportError: 
  39      # Python 3 
  40      from urllib.parse import urljoin 
  41  import copy 
  42  from lxml import etree 
  43  from lxml.html import defs 
  44  from lxml.html._setmixin import SetMixin 
  45  try: 
  46      from collections import MutableMapping as DictMixin 
  47  except ImportError: 
  48      # Python < 2.6 
  49      from UserDict import DictMixin 
  50  try: 
  51      set 
  52  except NameError: 
  53      # Python 2.3 
  54      from sets import Set as set 
  55  try: 
  56      bytes 
  57  except NameError: 
  58      # Python < 2.6 
  59      bytes = str 
  60  try: 
  61      unicode 
  62  except NameError: 
  63      # Python 3 
  64      unicode = str 
  65  try: 
  66      basestring 
  67  except NameError: 
  68      # Python 3 
  69      basestring = (str, bytes) 
  70   
71 -def __fix_docstring(s):
72 if not s: 73 return s 74 import sys 75 if sys.version_info[0] >= 3: 76 sub = re.compile(r"^(\s*)u'", re.M).sub 77 else: 78 sub = re.compile(r"^(\s*)b'", re.M).sub 79 return sub(r"\1'", s)
80 81 __all__ = [ 82 'document_fromstring', 'fragment_fromstring', 'fragments_fromstring', 'fromstring', 83 'tostring', 'Element', 'defs', 'open_in_browser', 'submit_form', 84 'find_rel_links', 'find_class', 'make_links_absolute', 85 'resolve_base_href', 'iterlinks', 'rewrite_links', 'open_in_browser', 'parse'] 86 87 XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" 88 89 _rel_links_xpath = etree.XPath("descendant-or-self::a[@rel]|descendant-or-self::x:a[@rel]", 90 namespaces={'x':XHTML_NAMESPACE}) 91 _options_xpath = etree.XPath("descendant-or-self::option|descendant-or-self::x:option", 92 namespaces={'x':XHTML_NAMESPACE}) 93 _forms_xpath = etree.XPath("descendant-or-self::form|descendant-or-self::x:form", 94 namespaces={'x':XHTML_NAMESPACE}) 95 #_class_xpath = etree.XPath(r"descendant-or-self::*[regexp:match(@class, concat('\b', $class_name, '\b'))]", {'regexp': 'http://exslt.org/regular-expressions'}) 96 _class_xpath = etree.XPath("descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), concat(' ', $class_name, ' '))]") 97 _id_xpath = etree.XPath("descendant-or-self::*[@id=$id]") 98 _collect_string_content = etree.XPath("string()") 99 _iter_css_urls = re.compile(r'url\(('+'["][^"]*["]|'+"['][^']*[']|"+r'[^)]*)\)', re.I).finditer 100 _iter_css_imports = re.compile(r'@import "(.*?)"').finditer 101 _label_xpath = etree.XPath("//label[@for=$id]|//x:label[@for=$id]", 102 namespaces={'x':XHTML_NAMESPACE}) 103 _archive_re = re.compile(r'[^ ]+') 104 _parse_meta_refresh_url = re.compile( 105 r'[^;=]*;\s*(?:url\s*=\s*)?(?P<url>.*)$', re.I).search 106 107
108 -def _unquote_match(s, pos):
109 if s[:1] == '"' and s[-1:] == '"' or s[:1] == "'" and s[-1:] == "'": 110 return s[1:-1], pos+1 111 else: 112 return s,pos
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 -def _nons(tag):
125 if isinstance(tag, basestring): 126 if tag[0] == '{' and tag[1:len(XHTML_NAMESPACE)+1] == XHTML_NAMESPACE: 127 return tag.split('}')[-1] 128 return tag
129
130 -class HtmlMixin(object):
131
132 - def base_url(self):
133 """ 134 Returns the base URL, given when the page was parsed. 135 136 Use with ``urlparse.urljoin(el.base_url, href)`` to get 137 absolute URLs. 138 """ 139 return self.getroottree().docinfo.URL
140 base_url = property(base_url, doc=base_url.__doc__) 141
142 - def forms(self):
143 """ 144 Return a list of all the forms 145 """ 146 return _forms_xpath(self)
147 forms = property(forms, doc=forms.__doc__) 148
149 - def body(self):
150 """ 151 Return the <body> element. Can be called from a child element 152 to get the document's head. 153 """ 154 return self.xpath('//body|//x:body', namespaces={'x':XHTML_NAMESPACE})[0]
155 body = property(body, doc=body.__doc__) 156
157 - def head(self):
158 """ 159 Returns the <head> element. Can be called from a child 160 element to get the document's head. 161 """ 162 return self.xpath('//head|//x:head', namespaces={'x':XHTML_NAMESPACE})[0]
163 head = property(head, doc=head.__doc__) 164
165 - def _label__get(self):
166 """ 167 Get or set any <label> element associated with this element. 168 """ 169 id = self.get('id') 170 if not id: 171 return None 172 result = _label_xpath(self, id=id) 173 if not result: 174 return None 175 else: 176 return result[0]
177 - def _label__set(self, label):
178 id = self.get('id') 179 if not id: 180 raise TypeError( 181 "You cannot set a label for an element (%r) that has no id" 182 % self) 183 if _nons(label.tag) != 'label': 184 raise TypeError( 185 "You can only assign label to a label element (not %r)" 186 % label) 187 label.set('for', id)
188 - def _label__del(self):
189 label = self.label 190 if label is not None: 191 del label.attrib['for']
192 label = property(_label__get, _label__set, _label__del, doc=_label__get.__doc__) 193
194 - def drop_tree(self):
195 """ 196 Removes this element from the tree, including its children and 197 text. The tail text is joined to the previous element or 198 parent. 199 """ 200 parent = self.getparent() 201 assert parent is not None 202 if self.tail: 203 previous = self.getprevious() 204 if previous is None: 205 parent.text = (parent.text or '') + self.tail 206 else: 207 previous.tail = (previous.tail or '') + self.tail 208 parent.remove(self)
209
210 - def drop_tag(self):
211 """ 212 Remove the tag, but not its children or text. The children and text 213 are merged into the parent. 214 215 Example:: 216 217 >>> h = fragment_fromstring('<div>Hello <b>World!</b></div>') 218 >>> h.find('.//b').drop_tag() 219 >>> print(tostring(h, encoding='unicode')) 220 <div>Hello World!</div> 221 """ 222 parent = self.getparent() 223 assert parent is not None 224 previous = self.getprevious() 225 if self.text and isinstance(self.tag, basestring): 226 # not a Comment, etc. 227 if previous is None: 228 parent.text = (parent.text or '') + self.text 229 else: 230 previous.tail = (previous.tail or '') + self.text 231 if self.tail: 232 if len(self): 233 last = self[-1] 234 last.tail = (last.tail or '') + self.tail 235 elif previous is None: 236 parent.text = (parent.text or '') + self.tail 237 else: 238 previous.tail = (previous.tail or '') + self.tail 239 index = parent.index(self) 240 parent[index:index+1] = self[:]
241 249
250 - def find_class(self, class_name):
251 """ 252 Find any elements with the given class name. 253 """ 254 return _class_xpath(self, class_name=class_name)
255
256 - def get_element_by_id(self, id, *default):
257 """ 258 Get the first element in a document with the given id. If none is 259 found, return the default argument if provided or raise KeyError 260 otherwise. 261 262 Note that there can be more than one element with the same id, 263 and this isn't uncommon in HTML documents found in the wild. 264 Browsers return only the first match, and this function does 265 the same. 266 """ 267 try: 268 # FIXME: should this check for multiple matches? 269 # browsers just return the first one 270 return _id_xpath(self, id=id)[0] 271 except IndexError: 272 if default: 273 return default[0] 274 else: 275 raise KeyError(id)
276
277 - def text_content(self):
278 """ 279 Return the text content of the tag (and the text in any children). 280 """ 281 return _collect_string_content(self)
282
283 - def cssselect(self, expr, translator='html'):
284 """ 285 Run the CSS expression on this element and its children, 286 returning a list of the results. 287 288 Equivalent to lxml.cssselect.CSSSelect(expr, translator='html')(self) 289 -- note that pre-compiling the expression can provide a substantial 290 speedup. 291 """ 292 # Do the import here to make the dependency optional. 293 from lxml.cssselect import CSSSelector 294 return CSSSelector(expr, translator=translator)(self)
295 296 ######################################## 297 ## Link functions 298 ######################################## 299 330 elif handle_failures == 'discard': 331 def link_repl(href): 332 try: 333 return urljoin(base_url, href) 334 except ValueError: 335 return None
336 elif handle_failures is None: 337 def link_repl(href): 338 return urljoin(base_url, href) 339 else: 340 raise ValueError( 341 "unexpected value for handle_failures: %r" % handle_failures) 342 343 self.rewrite_links(link_repl) 344
345 - def resolve_base_href(self, handle_failures=None):
346 """ 347 Find any ``<base href>`` tag in the document, and apply its 348 values to all links found in the document. Also remove the 349 tag once it has been applied. 350 351 If ``handle_failures`` is None (default), a failure to process 352 a URL will abort the processing. If set to 'ignore', errors 353 are ignored. If set to 'discard', failing URLs will be removed. 354 """ 355 base_href = None 356 basetags = self.xpath('//base[@href]|//x:base[@href]', 357 namespaces={'x': XHTML_NAMESPACE}) 358 for b in basetags: 359 base_href = b.get('href') 360 b.drop_tree() 361 if not base_href: 362 return 363 self.make_links_absolute(base_href, resolve_base_href=False, 364 handle_failures=handle_failures)
365 455 504 505
506 -class _MethodFunc(object):
507 """ 508 An object that represents a method on an element as a function; 509 the function takes either an element or an HTML string. It 510 returns whatever the function normally returns, or if the function 511 works in-place (and so returns None) it returns a serialized form 512 of the resulting document. 513 """
514 - def __init__(self, name, copy=False, source_class=HtmlMixin):
515 self.name = name 516 self.copy = copy 517 self.__doc__ = getattr(source_class, self.name).__doc__
518 - def __call__(self, doc, *args, **kw):
519 result_type = type(doc) 520 if isinstance(doc, basestring): 521 if 'copy' in kw: 522 raise TypeError( 523 "The keyword 'copy' can only be used with element inputs to %s, not a string input" % self.name) 524 doc = fromstring(doc, **kw) 525 else: 526 if 'copy' in kw: 527 make_a_copy = kw.pop('copy') 528 else: 529 make_a_copy = self.copy 530 if make_a_copy: 531 doc = copy.deepcopy(doc) 532 meth = getattr(doc, self.name) 533 result = meth(*args, **kw) 534 # FIXME: this None test is a bit sloppy 535 if result is None: 536 # Then return what we got in 537 return _transform_result(result_type, doc) 538 else: 539 return result
540 541 find_rel_links = _MethodFunc('find_rel_links', copy=False) 542 find_class = _MethodFunc('find_class', copy=False) 543 make_links_absolute = _MethodFunc('make_links_absolute', copy=True) 544 resolve_base_href = _MethodFunc('resolve_base_href', copy=True) 545 iterlinks = _MethodFunc('iterlinks', copy=False) 546 rewrite_links = _MethodFunc('rewrite_links', copy=True) 547
548 -class HtmlComment(etree.CommentBase, HtmlMixin):
549 pass
550
551 -class HtmlElement(etree.ElementBase, HtmlMixin):
552 pass
553
554 -class HtmlProcessingInstruction(etree.PIBase, HtmlMixin):
555 pass
556
557 -class HtmlEntity(etree.EntityBase, HtmlMixin):
558 pass
559 560
561 -class HtmlElementClassLookup(etree.CustomElementClassLookup):
562 """A lookup scheme for HTML Element classes. 563 564 To create a lookup instance with different Element classes, pass a tag 565 name mapping of Element classes in the ``classes`` keyword argument and/or 566 a tag name mapping of Mixin classes in the ``mixins`` keyword argument. 567 The special key '*' denotes a Mixin class that should be mixed into all 568 Element classes. 569 """ 570 _default_element_classes = {} 571
572 - def __init__(self, classes=None, mixins=None):
573 etree.CustomElementClassLookup.__init__(self) 574 if classes is None: 575 classes = self._default_element_classes.copy() 576 if mixins: 577 mixers = {} 578 for name, value in mixins: 579 if name == '*': 580 for n in classes.keys(): 581 mixers.setdefault(n, []).append(value) 582 else: 583 mixers.setdefault(name, []).append(value) 584 for name, mix_bases in mixers.items(): 585 cur = classes.get(name, HtmlElement) 586 bases = tuple(mix_bases + [cur]) 587 classes[name] = type(cur.__name__, bases, {}) 588 self._element_classes = classes
589
590 - def lookup(self, node_type, document, namespace, name):
591 if node_type == 'element': 592 return self._element_classes.get(name.lower(), HtmlElement) 593 elif node_type == 'comment': 594 return HtmlComment 595 elif node_type == 'PI': 596 return HtmlProcessingInstruction 597 elif node_type == 'entity': 598 return HtmlEntity 599 # Otherwise normal lookup 600 return None
601 602 ################################################################################ 603 # parsing 604 ################################################################################ 605 606 _looks_like_full_html_unicode = re.compile( 607 unicode(r'^\s*<(?:html|!doctype)'), re.I).match 608 _looks_like_full_html_bytes = re.compile( 609 r'^\s*<(?:html|!doctype)'.encode('ascii'), re.I).match 610
611 -def document_fromstring(html, parser=None, ensure_head_body=False, **kw):
612 if parser is None: 613 parser = html_parser 614 value = etree.fromstring(html, parser, **kw) 615 if value is None: 616 raise etree.ParserError( 617 "Document is empty") 618 if ensure_head_body and value.find('head') is None: 619 value.insert(0, Element('head')) 620 if ensure_head_body and value.find('body') is None: 621 value.append(Element('body')) 622 return value
623
624 -def fragments_fromstring(html, no_leading_text=False, base_url=None, 625 parser=None, **kw):
626 """ 627 Parses several HTML elements, returning a list of elements. 628 629 The first item in the list may be a string (though leading 630 whitespace is removed). If no_leading_text is true, then it will 631 be an error if there is leading text, and it will always be a list 632 of only elements. 633 634 base_url will set the document's base_url attribute (and the tree's docinfo.URL) 635 """ 636 if parser is None: 637 parser = html_parser 638 # FIXME: check what happens when you give html with a body, head, etc. 639 if isinstance(html, bytes): 640 if not _looks_like_full_html_bytes(html): 641 # can't use %-formatting in early Py3 versions 642 html = ('<html><body>'.encode('ascii') + html + 643 '</body></html>'.encode('ascii')) 644 else: 645 if not _looks_like_full_html_unicode(html): 646 html = '<html><body>%s</body></html>' % html 647 doc = document_fromstring(html, parser=parser, base_url=base_url, **kw) 648 assert _nons(doc.tag) == 'html' 649 bodies = [e for e in doc if _nons(e.tag) == 'body'] 650 assert len(bodies) == 1, ("too many bodies: %r in %r" % (bodies, html)) 651 body = bodies[0] 652 elements = [] 653 if no_leading_text and body.text and body.text.strip(): 654 raise etree.ParserError( 655 "There is leading text: %r" % body.text) 656 if body.text and body.text.strip(): 657 elements.append(body.text) 658 elements.extend(body) 659 # FIXME: removing the reference to the parent artificial document 660 # would be nice 661 return elements
662
663 -def fragment_fromstring(html, create_parent=False, base_url=None, 664 parser=None, **kw):
665 """ 666 Parses a single HTML element; it is an error if there is more than 667 one element, or if anything but whitespace precedes or follows the 668 element. 669 670 If ``create_parent`` is true (or is a tag name) then a parent node 671 will be created to encapsulate the HTML in a single element. In this 672 case, leading or trailing text is also allowed, as are multiple elements 673 as result of the parsing. 674 675 Passing a ``base_url`` will set the document's ``base_url`` attribute 676 (and the tree's docinfo.URL). 677 """ 678 if parser is None: 679 parser = html_parser 680 681 accept_leading_text = bool(create_parent) 682 683 elements = fragments_fromstring( 684 html, parser=parser, no_leading_text=not accept_leading_text, 685 base_url=base_url, **kw) 686 687 if create_parent: 688 if not isinstance(create_parent, basestring): 689 create_parent = 'div' 690 new_root = Element(create_parent) 691 if elements: 692 if isinstance(elements[0], basestring): 693 new_root.text = elements[0] 694 del elements[0] 695 new_root.extend(elements) 696 return new_root 697 698 if not elements: 699 raise etree.ParserError('No elements found') 700 if len(elements) > 1: 701 raise etree.ParserError( 702 "Multiple elements found (%s)" 703 % ', '.join([_element_name(e) for e in elements])) 704 el = elements[0] 705 if el.tail and el.tail.strip(): 706 raise etree.ParserError( 707 "Element followed by text: %r" % el.tail) 708 el.tail = None 709 return el
710
711 -def fromstring(html, base_url=None, parser=None, **kw):
712 """ 713 Parse the html, returning a single element/document. 714 715 This tries to minimally parse the chunk of text, without knowing if it 716 is a fragment or a document. 717 718 base_url will set the document's base_url attribute (and the tree's docinfo.URL) 719 """ 720 if parser is None: 721 parser = html_parser 722 if isinstance(html, bytes): 723 is_full_html = _looks_like_full_html_bytes(html) 724 else: 725 is_full_html = _looks_like_full_html_unicode(html) 726 doc = document_fromstring(html, parser=parser, base_url=base_url, **kw) 727 if is_full_html: 728 return doc 729 # otherwise, lets parse it out... 730 bodies = doc.findall('body') 731 if not bodies: 732 bodies = doc.findall('{%s}body' % XHTML_NAMESPACE) 733 if bodies: 734 body = bodies[0] 735 if len(bodies) > 1: 736 # Somehow there are multiple bodies, which is bad, but just 737 # smash them into one body 738 for other_body in bodies[1:]: 739 if other_body.text: 740 if len(body): 741 body[-1].tail = (body[-1].tail or '') + other_body.text 742 else: 743 body.text = (body.text or '') + other_body.text 744 body.extend(other_body) 745 # We'll ignore tail 746 # I guess we are ignoring attributes too 747 other_body.drop_tree() 748 else: 749 body = None 750 heads = doc.findall('head') 751 if not heads: 752 heads = doc.findall('{%s}head' % XHTML_NAMESPACE) 753 if heads: 754 # Well, we have some sort of structure, so lets keep it all 755 head = heads[0] 756 if len(heads) > 1: 757 for other_head in heads[1:]: 758 head.extend(other_head) 759 # We don't care about text or tail in a head 760 other_head.drop_tree() 761 return doc 762 if body is None: 763 return doc 764 if (len(body) == 1 and (not body.text or not body.text.strip()) 765 and (not body[-1].tail or not body[-1].tail.strip())): 766 # The body has just one element, so it was probably a single 767 # element passed in 768 return body[0] 769 # Now we have a body which represents a bunch of tags which have the 770 # content that was passed in. We will create a fake container, which 771 # is the body tag, except <body> implies too much structure. 772 if _contains_block_level_tag(body): 773 body.tag = 'div' 774 else: 775 body.tag = 'span' 776 return body
777
778 -def parse(filename_or_url, parser=None, base_url=None, **kw):
779 """ 780 Parse a filename, URL, or file-like object into an HTML document 781 tree. Note: this returns a tree, not an element. Use 782 ``parse(...).getroot()`` to get the document root. 783 784 You can override the base URL with the ``base_url`` keyword. This 785 is most useful when parsing from a file-like object. 786 """ 787 if parser is None: 788 parser = html_parser 789 return etree.parse(filename_or_url, parser, base_url=base_url, **kw)
790
791 -def _contains_block_level_tag(el):
792 # FIXME: I could do this with XPath, but would that just be 793 # unnecessarily slow? 794 for el in el.iter(etree.Element): 795 if _nons(el.tag) in defs.block_tags: 796 return True 797 return False
798
799 -def _element_name(el):
800 if isinstance(el, etree.CommentBase): 801 return 'comment' 802 elif isinstance(el, basestring): 803 return 'string' 804 else: 805 return _nons(el.tag)
806 807 ################################################################################ 808 # form handling 809 ################################################################################ 810
811 -class FormElement(HtmlElement):
812 """ 813 Represents a <form> element. 814 """ 815
816 - def inputs(self):
817 """ 818 Returns an accessor for all the input elements in the form. 819 820 See `InputGetter` for more information about the object. 821 """ 822 return InputGetter(self)
823 inputs = property(inputs, doc=inputs.__doc__) 824
825 - def _fields__get(self):
826 """ 827 Dictionary-like object that represents all the fields in this 828 form. You can set values in this dictionary to effect the 829 form. 830 """ 831 return FieldsDict(self.inputs)
832 - def _fields__set(self, value):
833 prev_keys = self.fields.keys() 834 for key, value in value.items(): 835 if key in prev_keys: 836 prev_keys.remove(key) 837 self.fields[key] = value 838 for key in prev_keys: 839 if key is None: 840 # Case of an unnamed input; these aren't really 841 # expressed in form_values() anyway. 842 continue 843 self.fields[key] = None
844 845 fields = property(_fields__get, _fields__set, doc=_fields__get.__doc__) 846
847 - def _name(self):
848 if self.get('name'): 849 return self.get('name') 850 elif self.get('id'): 851 return '#' + self.get('id') 852 forms = list(self.body.iter('form')) 853 if not forms: 854 forms = list(self.body.iter('{%s}form' % XHTML_NAMESPACE)) 855 return str(forms.index(self))
856
857 - def form_values(self):
858 """ 859 Return a list of tuples of the field values for the form. 860 This is suitable to be passed to ``urllib.urlencode()``. 861 """ 862 results = [] 863 for el in self.inputs: 864 name = el.name 865 if not name: 866 continue 867 tag = _nons(el.tag) 868 if tag == 'textarea': 869 results.append((name, el.value)) 870 elif tag == 'select': 871 value = el.value 872 if el.multiple: 873 for v in value: 874 results.append((name, v)) 875 elif value is not None: 876 results.append((name, el.value)) 877 else: 878 assert tag == 'input', ( 879 "Unexpected tag: %r" % el) 880 if el.checkable and not el.checked: 881 continue 882 if el.type in ('submit', 'image', 'reset'): 883 continue 884 value = el.value 885 if value is not None: 886 results.append((name, el.value)) 887 return results
888
889 - def _action__get(self):
890 """ 891 Get/set the form's ``action`` attribute. 892 """ 893 base_url = self.base_url 894 action = self.get('action') 895 if base_url and action is not None: 896 return urljoin(base_url, action) 897 else: 898 return action
899 - def _action__set(self, value):
900 self.set('action', value)
901 - def _action__del(self):
902 if 'action' in self.attrib: 903 del self.attrib['action']
904 action = property(_action__get, _action__set, _action__del, doc=_action__get.__doc__) 905
906 - def _method__get(self):
907 """ 908 Get/set the form's method. Always returns a capitalized 909 string, and defaults to ``'GET'`` 910 """ 911 return self.get('method', 'GET').upper()
912 - def _method__set(self, value):
913 self.set('method', value.upper())
914 method = property(_method__get, _method__set, doc=_method__get.__doc__)
915 916 HtmlElementClassLookup._default_element_classes['form'] = FormElement 917
918 -def submit_form(form, extra_values=None, open_http=None):
919 """ 920 Helper function to submit a form. Returns a file-like object, as from 921 ``urllib.urlopen()``. This object also has a ``.geturl()`` function, 922 which shows the URL if there were any redirects. 923 924 You can use this like:: 925 926 form = doc.forms[0] 927 form.inputs['foo'].value = 'bar' # etc 928 response = form.submit() 929 doc = parse(response) 930 doc.make_links_absolute(response.geturl()) 931 932 To change the HTTP requester, pass a function as ``open_http`` keyword 933 argument that opens the URL for you. The function must have the following 934 signature:: 935 936 open_http(method, URL, values) 937 938 The action is one of 'GET' or 'POST', the URL is the target URL as a 939 string, and the values are a sequence of ``(name, value)`` tuples with the 940 form data. 941 """ 942 values = form.form_values() 943 if extra_values: 944 if hasattr(extra_values, 'items'): 945 extra_values = extra_values.items() 946 values.extend(extra_values) 947 if open_http is None: 948 open_http = open_http_urllib 949 if form.action: 950 url = form.action 951 else: 952 url = form.base_url 953 return open_http(form.method, url, values)
954
955 -def open_http_urllib(method, url, values):
956 if not url: 957 raise ValueError("cannot submit, no URL provided") 958 ## FIXME: should test that it's not a relative URL or something 959 try: 960 from urllib import urlencode, urlopen 961 except ImportError: # Python 3 962 from urllib.request import urlopen 963 from urllib.parse import urlencode 964 if method == 'GET': 965 if '?' in url: 966 url += '&' 967 else: 968 url += '?' 969 url += urlencode(values) 970 data = None 971 else: 972 data = urlencode(values) 973 return urlopen(url, data)
974
975 -class FieldsDict(DictMixin):
976
977 - def __init__(self, inputs):
978 self.inputs = inputs
979 - def __getitem__(self, item):
980 return self.inputs[item].value
981 - def __setitem__(self, item, value):
982 self.inputs[item].value = value
983 - def __delitem__(self, item):
984 raise KeyError( 985 "You cannot remove keys from ElementDict")
986 - def keys(self):
987 return self.inputs.keys()
988 - def __contains__(self, item):
989 return item in self.inputs
990 - def __iter__(self):
991 return iter(self.inputs.keys())
992 - def __len__(self):
993 return len(self.inputs)
994
995 - def __repr__(self):
996 return '<%s for form %s>' % ( 997 self.__class__.__name__, 998 self.inputs.form._name())
999
1000 -class InputGetter(object):
1001 1002 """ 1003 An accessor that represents all the input fields in a form. 1004 1005 You can get fields by name from this, with 1006 ``form.inputs['field_name']``. If there are a set of checkboxes 1007 with the same name, they are returned as a list (a `CheckboxGroup` 1008 which also allows value setting). Radio inputs are handled 1009 similarly. 1010 1011 You can also iterate over this to get all input elements. This 1012 won't return the same thing as if you get all the names, as 1013 checkboxes and radio elements are returned individually. 1014 """ 1015 1016 _name_xpath = etree.XPath(".//*[@name = $name and (local-name(.) = 'select' or local-name(.) = 'input' or local-name(.) = 'textarea')]") 1017 _all_xpath = etree.XPath(".//*[local-name() = 'select' or local-name() = 'input' or local-name() = 'textarea']") 1018
1019 - def __init__(self, form):
1020 self.form = form
1021
1022 - def __repr__(self):
1023 return '<%s for form %s>' % ( 1024 self.__class__.__name__, 1025 self.form._name())
1026 1027 ## FIXME: there should be more methods, and it's unclear if this is 1028 ## a dictionary-like object or list-like object 1029
1030 - def __getitem__(self, name):
1031 results = self._name_xpath(self.form, name=name) 1032 if results: 1033 type = results[0].get('type') 1034 if type == 'radio' and len(results) > 1: 1035 group = RadioGroup(results) 1036 group.name = name 1037 return group 1038 elif type == 'checkbox' and len(results) > 1: 1039 group = CheckboxGroup(results) 1040 group.name = name 1041 return group 1042 else: 1043 # I don't like throwing away elements like this 1044 return results[0] 1045 else: 1046 raise KeyError( 1047 "No input element with the name %r" % name)
1048
1049 - def __contains__(self, name):
1050 results = self._name_xpath(self.form, name=name) 1051 return bool(results)
1052
1053 - def keys(self):
1054 names = set() 1055 for el in self: 1056 names.add(el.name) 1057 if None in names: 1058 names.remove(None) 1059 return list(names)
1060
1061 - def __iter__(self):
1062 ## FIXME: kind of dumb to turn a list into an iterator, only 1063 ## to have it likely turned back into a list again :( 1064 return iter(self._all_xpath(self.form))
1065
1066 -class InputMixin(object):
1067 1068 """ 1069 Mix-in for all input elements (input, select, and textarea) 1070 """ 1071 1072
1073 - def _name__get(self):
1074 """ 1075 Get/set the name of the element 1076 """ 1077 return self.get('name')
1078 - def _name__set(self, value):
1079 self.set('name', value)
1080 - def _name__del(self):
1081 if 'name' in self.attrib: 1082 del self.attrib['name']
1083 name = property(_name__get, _name__set, _name__del, doc=_name__get.__doc__) 1084
1085 - def __repr__(self):
1086 type = getattr(self, 'type', None) 1087 if type: 1088 type = ' type=%r' % type 1089 else: 1090 type = '' 1091 return '<%s %x name=%r%s>' % ( 1092 self.__class__.__name__, id(self), self.name, type)
1093
1094 -class TextareaElement(InputMixin, HtmlElement):
1095 """ 1096 ``<textarea>`` element. You can get the name with ``.name`` and 1097 get/set the value with ``.value`` 1098 """ 1099
1100 - def _value__get(self):
1101 """ 1102 Get/set the value (which is the contents of this element) 1103 """ 1104 content = self.text or '' 1105 if self.tag.startswith("{%s}" % XHTML_NAMESPACE): 1106 serialisation_method = 'xml' 1107 else: 1108 serialisation_method = 'html' 1109 for el in self: 1110 # it's rare that we actually get here, so let's not use ''.join() 1111 content += etree.tostring( 1112 el, method=serialisation_method, encoding='unicode') 1113 return content
1114 - def _value__set(self, value):
1115 del self[:] 1116 self.text = value
1117 - def _value__del(self):
1118 self.text = '' 1119 del self[:]
1120 value = property(_value__get, _value__set, _value__del, doc=_value__get.__doc__)
1121 1122 HtmlElementClassLookup._default_element_classes['textarea'] = TextareaElement 1123
1124 -class SelectElement(InputMixin, HtmlElement):
1125 """ 1126 ``<select>`` element. You can get the name with ``.name``. 1127 1128 ``.value`` will be the value of the selected option, unless this 1129 is a multi-select element (``<select multiple>``), in which case 1130 it will be a set-like object. In either case ``.value_options`` 1131 gives the possible values. 1132 1133 The boolean attribute ``.multiple`` shows if this is a 1134 multi-select. 1135 """ 1136
1137 - def _value__get(self):
1138 """ 1139 Get/set the value of this select (the selected option). 1140 1141 If this is a multi-select, this is a set-like object that 1142 represents all the selected options. 1143 """ 1144 if self.multiple: 1145 return MultipleSelectOptions(self) 1146 for el in _options_xpath(self): 1147 if el.get('selected') is not None: 1148 value = el.get('value') 1149 if value is None: 1150 value = el.text or '' 1151 if value: 1152 value = value.strip() 1153 return value 1154 return None
1155
1156 - def _value__set(self, value):
1157 if self.multiple: 1158 if isinstance(value, basestring): 1159 raise TypeError( 1160 "You must pass in a sequence") 1161 self.value.clear() 1162 self.value.update(value) 1163 return 1164 if value is not None: 1165 value = value.strip() 1166 for el in _options_xpath(self): 1167 opt_value = el.get('value') 1168 if opt_value is None: 1169 opt_value = el.text or '' 1170 if opt_value: 1171 opt_value = opt_value.strip() 1172 if opt_value == value: 1173 checked_option = el 1174 break 1175 else: 1176 raise ValueError( 1177 "There is no option with the value of %r" % value) 1178 for el in _options_xpath(self): 1179 if 'selected' in el.attrib: 1180 del el.attrib['selected'] 1181 if value is not None: 1182 checked_option.set('selected', '')
1183
1184 - def _value__del(self):
1185 # FIXME: should del be allowed at all? 1186 if self.multiple: 1187 self.value.clear() 1188 else: 1189 self.value = None
1190 1191 value = property(_value__get, _value__set, _value__del, doc=_value__get.__doc__) 1192
1193 - def value_options(self):
1194 """ 1195 All the possible values this select can have (the ``value`` 1196 attribute of all the ``<option>`` elements. 1197 """ 1198 options = [] 1199 for el in _options_xpath(self): 1200 value = el.get('value') 1201 if value is None: 1202 value = el.text or '' 1203 if value: 1204 value = value.strip() 1205 options.append(value) 1206 return options
1207 value_options = property(value_options, doc=value_options.__doc__) 1208
1209 - def _multiple__get(self):
1210 """ 1211 Boolean attribute: is there a ``multiple`` attribute on this element. 1212 """ 1213 return 'multiple' in self.attrib
1214 - def _multiple__set(self, value):
1215 if value: 1216 self.set('multiple', '') 1217 elif 'multiple' in self.attrib: 1218 del self.attrib['multiple']
1219 multiple = property(_multiple__get, _multiple__set, doc=_multiple__get.__doc__)
1220 1221 HtmlElementClassLookup._default_element_classes['select'] = SelectElement 1222
1223 -class MultipleSelectOptions(SetMixin):
1224 """ 1225 Represents all the selected options in a ``<select multiple>`` element. 1226 1227 You can add to this set-like option to select an option, or remove 1228 to unselect the option. 1229 """ 1230
1231 - def __init__(self, select):
1232 self.select = select
1233
1234 - def options(self):
1235 """ 1236 Iterator of all the ``<option>`` elements. 1237 """ 1238 return iter(_options_xpath(self.select))
1239 options = property(options) 1240
1241 - def __iter__(self):
1242 for option in self.options: 1243 if 'selected' in option.attrib: 1244 opt_value = option.get('value') 1245 if opt_value is None: 1246 opt_value = option.text or '' 1247 if opt_value: 1248 opt_value = opt_value.strip() 1249 yield opt_value
1250
1251 - def add(self, item):
1252 for option in self.options: 1253 opt_value = option.get('value') 1254 if opt_value is None: 1255 opt_value = option.text or '' 1256 if opt_value: 1257 opt_value = opt_value.strip() 1258 if opt_value == item: 1259 option.set('selected', '') 1260 break 1261 else: 1262 raise ValueError( 1263 "There is no option with the value %r" % item)
1264
1265 - def remove(self, item):
1266 for option in self.options: 1267 opt_value = option.get('value') 1268 if opt_value is None: 1269 opt_value = option.text or '' 1270 if opt_value: 1271 opt_value = opt_value.strip() 1272 if opt_value == item: 1273 if 'selected' in option.attrib: 1274 del option.attrib['selected'] 1275 else: 1276 raise ValueError( 1277 "The option %r is not currently selected" % item) 1278 break 1279 else: 1280 raise ValueError( 1281 "There is not option with the value %r" % item)
1282
1283 - def __repr__(self):
1284 return '<%s {%s} for select name=%r>' % ( 1285 self.__class__.__name__, 1286 ', '.join([repr(v) for v in self]), 1287 self.select.name)
1288
1289 -class RadioGroup(list):
1290 """ 1291 This object represents several ``<input type=radio>`` elements 1292 that have the same name. 1293 1294 You can use this like a list, but also use the property 1295 ``.value`` to check/uncheck inputs. Also you can use 1296 ``.value_options`` to get the possible values. 1297 """ 1298
1299 - def _value__get(self):
1300 """ 1301 Get/set the value, which checks the radio with that value (and 1302 unchecks any other value). 1303 """ 1304 for el in self: 1305 if 'checked' in el.attrib: 1306 return el.get('value') 1307 return None
1308
1309 - def _value__set(self, value):
1310 if value is not None: 1311 for el in self: 1312 if el.get('value') == value: 1313 checked_option = el 1314 break 1315 else: 1316 raise ValueError( 1317 "There is no radio input with the value %r" % value) 1318 for el in self: 1319 if 'checked' in el.attrib: 1320 del el.attrib['checked'] 1321 if value is not None: 1322 checked_option.set('checked', '')
1323
1324 - def _value__del(self):
1325 self.value = None
1326 1327 value = property(_value__get, _value__set, _value__del, doc=_value__get.__doc__) 1328
1329 - def value_options(self):
1330 """ 1331 Returns a list of all the possible values. 1332 """ 1333 return [el.get('value') for el in self]
1334 value_options = property(value_options, doc=value_options.__doc__) 1335
1336 - def __repr__(self):
1337 return '%s(%s)' % ( 1338 self.__class__.__name__, 1339 list.__repr__(self))
1340
1341 -class CheckboxGroup(list):
1342 """ 1343 Represents a group of checkboxes (``<input type=checkbox>``) that 1344 have the same name. 1345 1346 In addition to using this like a list, the ``.value`` attribute 1347 returns a set-like object that you can add to or remove from to 1348 check and uncheck checkboxes. You can also use ``.value_options`` 1349 to get the possible values. 1350 """ 1351
1352 - def _value__get(self):
1353 """ 1354 Return a set-like object that can be modified to check or 1355 uncheck individual checkboxes according to their value. 1356 """ 1357 return CheckboxValues(self)
1358 - def _value__set(self, value):
1359 self.value.clear() 1360 if not hasattr(value, '__iter__'): 1361 raise ValueError( 1362 "A CheckboxGroup (name=%r) must be set to a sequence (not %r)" 1363 % (self[0].name, value)) 1364 self.value.update(value)
1365 - def _value__del(self):
1366 self.value.clear()
1367 value = property(_value__get, _value__set, _value__del, doc=_value__get.__doc__) 1368
1369 - def value_options(self):
1370 """ 1371 Returns a list of all the possible values. 1372 """ 1373 return [el.get('value') for el in self]
1374 value_options = property(value_options, doc=value_options.__doc__) 1375
1376 - def __repr__(self):
1377 return '%s(%s)' % ( 1378 self.__class__.__name__, list.__repr__(self))
1379
1380 -class CheckboxValues(SetMixin):
1381 1382 """ 1383 Represents the values of the checked checkboxes in a group of 1384 checkboxes with the same name. 1385 """ 1386
1387 - def __init__(self, group):
1388 self.group = group
1389
1390 - def __iter__(self):
1391 return iter([ 1392 el.get('value') 1393 for el in self.group 1394 if 'checked' in el.attrib])
1395
1396 - def add(self, value):
1397 for el in self.group: 1398 if el.get('value') == value: 1399 el.set('checked', '') 1400 break 1401 else: 1402 raise KeyError("No checkbox with value %r" % value)
1403
1404 - def remove(self, value):
1405 for el in self.group: 1406 if el.get('value') == value: 1407 if 'checked' in el.attrib: 1408 del el.attrib['checked'] 1409 else: 1410 raise KeyError( 1411 "The checkbox with value %r was already unchecked" % value) 1412 break 1413 else: 1414 raise KeyError( 1415 "No checkbox with value %r" % value)
1416
1417 - def __repr__(self):
1418 return '<%s {%s} for checkboxes name=%r>' % ( 1419 self.__class__.__name__, 1420 ', '.join([repr(v) for v in self]), 1421 self.group.name)
1422
1423 -class InputElement(InputMixin, HtmlElement):
1424 """ 1425 Represents an ``<input>`` element. 1426 1427 You can get the type with ``.type`` (which is lower-cased and 1428 defaults to ``'text'``). 1429 1430 Also you can get and set the value with ``.value`` 1431 1432 Checkboxes and radios have the attribute ``input.checkable == 1433 True`` (for all others it is false) and a boolean attribute 1434 ``.checked``. 1435 1436 """ 1437 1438 ## FIXME: I'm a little uncomfortable with the use of .checked
1439 - def _value__get(self):
1440 """ 1441 Get/set the value of this element, using the ``value`` attribute. 1442 1443 Also, if this is a checkbox and it has no value, this defaults 1444 to ``'on'``. If it is a checkbox or radio that is not 1445 checked, this returns None. 1446 """ 1447 if self.checkable: 1448 if self.checked: 1449 return self.get('value') or 'on' 1450 else: 1451 return None 1452 return self.get('value')
1453 - def _value__set(self, value):
1454 if self.checkable: 1455 if not value: 1456 self.checked = False 1457 else: 1458 self.checked = True 1459 if isinstance(value, basestring): 1460 self.set('value', value) 1461 else: 1462 self.set('value', value)
1463 - def _value__del(self):
1464 if self.checkable: 1465 self.checked = False 1466 else: 1467 if 'value' in self.attrib: 1468 del self.attrib['value']
1469 value = property(_value__get, _value__set, _value__del, doc=_value__get.__doc__) 1470
1471 - def _type__get(self):
1472 """ 1473 Return the type of this element (using the type attribute). 1474 """ 1475 return self.get('type', 'text').lower()
1476 - def _type__set(self, value):
1477 self.set('type', value)
1478 type = property(_type__get, _type__set, doc=_type__get.__doc__) 1479
1480 - def checkable(self):
1481 """ 1482 Boolean: can this element be checked? 1483 """ 1484 return self.type in ['checkbox', 'radio']
1485 checkable = property(checkable, doc=checkable.__doc__) 1486
1487 - def _checked__get(self):
1488 """ 1489 Boolean attribute to get/set the presence of the ``checked`` 1490 attribute. 1491 1492 You can only use this on checkable input types. 1493 """ 1494 if not self.checkable: 1495 raise AttributeError('Not a checkable input type') 1496 return 'checked' in self.attrib
1497 - def _checked__set(self, value):
1498 if not self.checkable: 1499 raise AttributeError('Not a checkable input type') 1500 if value: 1501 self.set('checked', '') 1502 else: 1503 if 'checked' in self.attrib: 1504 del self.attrib['checked']
1505 checked = property(_checked__get, _checked__set, doc=_checked__get.__doc__)
1506 1507 HtmlElementClassLookup._default_element_classes['input'] = InputElement 1508
1509 -class LabelElement(HtmlElement):
1510 """ 1511 Represents a ``<label>`` element. 1512 1513 Label elements are linked to other elements with their ``for`` 1514 attribute. You can access this element with ``label.for_element``. 1515 """ 1516
1517 - def _for_element__get(self):
1518 """ 1519 Get/set the element this label points to. Return None if it 1520 can't be found. 1521 """ 1522 id = self.get('for') 1523 if not id: 1524 return None 1525 return self.body.get_element_by_id(id)
1526 - def _for_element__set(self, other):
1527 id = other.get('id') 1528 if not id: 1529 raise TypeError( 1530 "Element %r has no id attribute" % other) 1531 self.set('for', id)
1532 - def _for_element__del(self):
1533 if 'id' in self.attrib: 1534 del self.attrib['id']
1535 for_element = property(_for_element__get, _for_element__set, _for_element__del, 1536 doc=_for_element__get.__doc__)
1537 1538 HtmlElementClassLookup._default_element_classes['label'] = LabelElement 1539 1540 ############################################################ 1541 ## Serialization 1542 ############################################################ 1543
1544 -def html_to_xhtml(html):
1545 """Convert all tags in an HTML tree to XHTML by moving them to the 1546 XHTML namespace. 1547 """ 1548 try: 1549 html = html.getroot() 1550 except AttributeError: 1551 pass 1552 prefix = "{%s}" % XHTML_NAMESPACE 1553 for el in html.iter(etree.Element): 1554 tag = el.tag 1555 if tag[0] != '{': 1556 el.tag = prefix + tag
1557
1558 -def xhtml_to_html(xhtml):
1559 """Convert all tags in an XHTML tree to HTML by removing their 1560 XHTML namespace. 1561 """ 1562 try: 1563 xhtml = xhtml.getroot() 1564 except AttributeError: 1565 pass 1566 prefix = "{%s}" % XHTML_NAMESPACE 1567 prefix_len = len(prefix) 1568 for el in xhtml.iter(prefix + "*"): 1569 el.tag = el.tag[prefix_len:]
1570 1571 # This isn't a general match, but it's a match for what libxml2 1572 # specifically serialises: 1573 __str_replace_meta_content_type = re.compile( 1574 r'<meta http-equiv="Content-Type"[^>]*>').sub 1575 __bytes_replace_meta_content_type = re.compile( 1576 r'<meta http-equiv="Content-Type"[^>]*>'.encode('ASCII')).sub 1577
1578 -def tostring(doc, pretty_print=False, include_meta_content_type=False, 1579 encoding=None, method="html", with_tail=True, doctype=None):
1580 """Return an HTML string representation of the document. 1581 1582 Note: if include_meta_content_type is true this will create a 1583 ``<meta http-equiv="Content-Type" ...>`` tag in the head; 1584 regardless of the value of include_meta_content_type any existing 1585 ``<meta http-equiv="Content-Type" ...>`` tag will be removed 1586 1587 The ``encoding`` argument controls the output encoding (defauts to 1588 ASCII, with &#...; character references for any characters outside 1589 of ASCII). Note that you can pass the name ``'unicode'`` as 1590 ``encoding`` argument to serialise to a Unicode string. 1591 1592 The ``method`` argument defines the output method. It defaults to 1593 'html', but can also be 'xml' for xhtml output, or 'text' to 1594 serialise to plain text without markup. 1595 1596 To leave out the tail text of the top-level element that is being 1597 serialised, pass ``with_tail=False``. 1598 1599 The ``doctype`` option allows passing in a plain string that will 1600 be serialised before the XML tree. Note that passing in non 1601 well-formed content here will make the XML output non well-formed. 1602 Also, an existing doctype in the document tree will not be removed 1603 when serialising an ElementTree instance. 1604 1605 Example:: 1606 1607 >>> from lxml import html 1608 >>> root = html.fragment_fromstring('<p>Hello<br>world!</p>') 1609 1610 >>> html.tostring(root) 1611 b'<p>Hello<br>world!</p>' 1612 >>> html.tostring(root, method='html') 1613 b'<p>Hello<br>world!</p>' 1614 1615 >>> html.tostring(root, method='xml') 1616 b'<p>Hello<br/>world!</p>' 1617 1618 >>> html.tostring(root, method='text') 1619 b'Helloworld!' 1620 1621 >>> html.tostring(root, method='text', encoding='unicode') 1622 u'Helloworld!' 1623 1624 >>> root = html.fragment_fromstring('<div><p>Hello<br>world!</p>TAIL</div>') 1625 >>> html.tostring(root[0], method='text', encoding='unicode') 1626 u'Helloworld!TAIL' 1627 1628 >>> html.tostring(root[0], method='text', encoding='unicode', with_tail=False) 1629 u'Helloworld!' 1630 1631 >>> doc = html.document_fromstring('<p>Hello<br>world!</p>') 1632 >>> html.tostring(doc, method='html', encoding='unicode') 1633 u'<html><body><p>Hello<br>world!</p></body></html>' 1634 1635 >>> print(html.tostring(doc, method='html', encoding='unicode', 1636 ... doctype='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"' 1637 ... ' "http://www.w3.org/TR/html4/strict.dtd">')) 1638 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 1639 <html><body><p>Hello<br>world!</p></body></html> 1640 """ 1641 html = etree.tostring(doc, method=method, pretty_print=pretty_print, 1642 encoding=encoding, with_tail=with_tail, 1643 doctype=doctype) 1644 if method == 'html' and not include_meta_content_type: 1645 if isinstance(html, str): 1646 html = __str_replace_meta_content_type('', html) 1647 else: 1648 html = __bytes_replace_meta_content_type(bytes(), html) 1649 return html
1650 1651 tostring.__doc__ = __fix_docstring(tostring.__doc__) 1652
1653 -def open_in_browser(doc, encoding=None):
1654 """ 1655 Open the HTML document in a web browser, saving it to a temporary 1656 file to open it. Note that this does not delete the file after 1657 use. This is mainly meant for debugging. 1658 """ 1659 import os 1660 import webbrowser 1661 import tempfile 1662 if not isinstance(doc, etree._ElementTree): 1663 doc = etree.ElementTree(doc) 1664 handle, fn = tempfile.mkstemp(suffix='.html') 1665 f = os.fdopen(handle, 'wb') 1666 try: 1667 doc.write(f, method="html", encoding=encoding or doc.docinfo.encoding or "UTF-8") 1668 finally: 1669 # we leak the file itself here, but we should at least close it 1670 f.close() 1671 url = 'file://' + fn.replace(os.path.sep, '/') 1672 print(url) 1673 webbrowser.open(url)
1674 1675 ################################################################################ 1676 # configure Element class lookup 1677 ################################################################################ 1678
1679 -class HTMLParser(etree.HTMLParser):
1680 """An HTML parser that is configured to return lxml.html Element 1681 objects. 1682 """
1683 - def __init__(self, **kwargs):
1684 super(HTMLParser, self).__init__(**kwargs) 1685 self.set_element_class_lookup(HtmlElementClassLookup())
1686
1687 -class XHTMLParser(etree.XMLParser):
1688 """An XML parser that is configured to return lxml.html Element 1689 objects. 1690 1691 Note that this parser is not really XHTML aware unless you let it 1692 load a DTD that declares the HTML entities. To do this, make sure 1693 you have the XHTML DTDs installed in your catalogs, and create the 1694 parser like this:: 1695 1696 >>> parser = XHTMLParser(load_dtd=True) 1697 1698 If you additionally want to validate the document, use this:: 1699 1700 >>> parser = XHTMLParser(dtd_validation=True) 1701 1702 For catalog support, see http://www.xmlsoft.org/catalog.html. 1703 """
1704 - def __init__(self, **kwargs):
1705 super(XHTMLParser, self).__init__(**kwargs) 1706 self.set_element_class_lookup(HtmlElementClassLookup())
1707
1708 -def Element(*args, **kw):
1709 """Create a new HTML Element. 1710 1711 This can also be used for XHTML documents. 1712 """ 1713 v = html_parser.makeelement(*args, **kw) 1714 return v
1715 1716 html_parser = HTMLParser() 1717 xhtml_parser = XHTMLParser() 1718