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