Path: blob/master/venv/Lib/site-packages/bs4/builder/_lxml.py
811 views
# Use of this source code is governed by the MIT license.1__license__ = "MIT"23__all__ = [4'LXMLTreeBuilderForXML',5'LXMLTreeBuilder',6]78try:9from collections.abc import Callable # Python 3.610except ImportError as e:11from collections import Callable1213from io import BytesIO14from io import StringIO15from lxml import etree16from bs4.element import (17Comment,18Doctype,19NamespacedAttribute,20ProcessingInstruction,21XMLProcessingInstruction,22)23from bs4.builder import (24FAST,25HTML,26HTMLTreeBuilder,27PERMISSIVE,28ParserRejectedMarkup,29TreeBuilder,30XML)31from bs4.dammit import EncodingDetector3233LXML = 'lxml'3435def _invert(d):36"Invert a dictionary."37return dict((v,k) for k, v in list(d.items()))3839class LXMLTreeBuilderForXML(TreeBuilder):40DEFAULT_PARSER_CLASS = etree.XMLParser4142is_xml = True43processing_instruction_class = XMLProcessingInstruction4445NAME = "lxml-xml"46ALTERNATE_NAMES = ["xml"]4748# Well, it's permissive by XML parser standards.49features = [NAME, LXML, XML, FAST, PERMISSIVE]5051CHUNK_SIZE = 5125253# This namespace mapping is specified in the XML Namespace54# standard.55DEFAULT_NSMAPS = dict(xml='http://www.w3.org/XML/1998/namespace')5657DEFAULT_NSMAPS_INVERTED = _invert(DEFAULT_NSMAPS)5859# NOTE: If we parsed Element objects and looked at .sourceline,60# we'd be able to see the line numbers from the original document.61# But instead we build an XMLParser or HTMLParser object to serve62# as the target of parse messages, and those messages don't include63# line numbers.64# See: https://bugs.launchpad.net/lxml/+bug/18469066566def initialize_soup(self, soup):67"""Let the BeautifulSoup object know about the standard namespace68mapping.6970:param soup: A `BeautifulSoup`.71"""72super(LXMLTreeBuilderForXML, self).initialize_soup(soup)73self._register_namespaces(self.DEFAULT_NSMAPS)7475def _register_namespaces(self, mapping):76"""Let the BeautifulSoup object know about namespaces encountered77while parsing the document.7879This might be useful later on when creating CSS selectors.8081:param mapping: A dictionary mapping namespace prefixes to URIs.82"""83for key, value in list(mapping.items()):84if key and key not in self.soup._namespaces:85# Let the BeautifulSoup object know about a new namespace.86# If there are multiple namespaces defined with the same87# prefix, the first one in the document takes precedence.88self.soup._namespaces[key] = value8990def default_parser(self, encoding):91"""Find the default parser for the given encoding.9293:param encoding: A string.94:return: Either a parser object or a class, which95will be instantiated with default arguments.96"""97if self._default_parser is not None:98return self._default_parser99return etree.XMLParser(100target=self, strip_cdata=False, recover=True, encoding=encoding)101102def parser_for(self, encoding):103"""Instantiate an appropriate parser for the given encoding.104105:param encoding: A string.106:return: A parser object such as an `etree.XMLParser`.107"""108# Use the default parser.109parser = self.default_parser(encoding)110111if isinstance(parser, Callable):112# Instantiate the parser with default arguments113parser = parser(114target=self, strip_cdata=False, recover=True, encoding=encoding115)116return parser117118def __init__(self, parser=None, empty_element_tags=None, **kwargs):119# TODO: Issue a warning if parser is present but not a120# callable, since that means there's no way to create new121# parsers for different encodings.122self._default_parser = parser123if empty_element_tags is not None:124self.empty_element_tags = set(empty_element_tags)125self.soup = None126self.nsmaps = [self.DEFAULT_NSMAPS_INVERTED]127super(LXMLTreeBuilderForXML, self).__init__(**kwargs)128129def _getNsTag(self, tag):130# Split the namespace URL out of a fully-qualified lxml tag131# name. Copied from lxml's src/lxml/sax.py.132if tag[0] == '{':133return tuple(tag[1:].split('}', 1))134else:135return (None, tag)136137def prepare_markup(self, markup, user_specified_encoding=None,138exclude_encodings=None,139document_declared_encoding=None):140"""Run any preliminary steps necessary to make incoming markup141acceptable to the parser.142143lxml really wants to get a bytestring and convert it to144Unicode itself. So instead of using UnicodeDammit to convert145the bytestring to Unicode using different encodings, this146implementation uses EncodingDetector to iterate over the147encodings, and tell lxml to try to parse the document as each148one in turn.149150:param markup: Some markup -- hopefully a bytestring.151:param user_specified_encoding: The user asked to try this encoding.152:param document_declared_encoding: The markup itself claims to be153in this encoding.154:param exclude_encodings: The user asked _not_ to try any of155these encodings.156157:yield: A series of 4-tuples:158(markup, encoding, declared encoding,159has undergone character replacement)160161Each 4-tuple represents a strategy for converting the162document to Unicode and parsing it. Each strategy will be tried163in turn.164"""165is_html = not self.is_xml166if is_html:167self.processing_instruction_class = ProcessingInstruction168else:169self.processing_instruction_class = XMLProcessingInstruction170171if isinstance(markup, str):172# We were given Unicode. Maybe lxml can parse Unicode on173# this system?174yield markup, None, document_declared_encoding, False175176if isinstance(markup, str):177# No, apparently not. Convert the Unicode to UTF-8 and178# tell lxml to parse it as UTF-8.179yield (markup.encode("utf8"), "utf8",180document_declared_encoding, False)181182try_encodings = [user_specified_encoding, document_declared_encoding]183detector = EncodingDetector(184markup, try_encodings, is_html, exclude_encodings)185for encoding in detector.encodings:186yield (detector.markup, encoding, document_declared_encoding, False)187188def feed(self, markup):189if isinstance(markup, bytes):190markup = BytesIO(markup)191elif isinstance(markup, str):192markup = StringIO(markup)193194# Call feed() at least once, even if the markup is empty,195# or the parser won't be initialized.196data = markup.read(self.CHUNK_SIZE)197try:198self.parser = self.parser_for(self.soup.original_encoding)199self.parser.feed(data)200while len(data) != 0:201# Now call feed() on the rest of the data, chunk by chunk.202data = markup.read(self.CHUNK_SIZE)203if len(data) != 0:204self.parser.feed(data)205self.parser.close()206except (UnicodeDecodeError, LookupError, etree.ParserError) as e:207raise ParserRejectedMarkup(e)208209def close(self):210self.nsmaps = [self.DEFAULT_NSMAPS_INVERTED]211212def start(self, name, attrs, nsmap={}):213# Make sure attrs is a mutable dict--lxml may send an immutable dictproxy.214attrs = dict(attrs)215nsprefix = None216# Invert each namespace map as it comes in.217if len(nsmap) == 0 and len(self.nsmaps) > 1:218# There are no new namespaces for this tag, but219# non-default namespaces are in play, so we need a220# separate tag stack to know when they end.221self.nsmaps.append(None)222elif len(nsmap) > 0:223# A new namespace mapping has come into play.224225# First, Let the BeautifulSoup object know about it.226self._register_namespaces(nsmap)227228# Then, add it to our running list of inverted namespace229# mappings.230self.nsmaps.append(_invert(nsmap))231232# Also treat the namespace mapping as a set of attributes on the233# tag, so we can recreate it later.234attrs = attrs.copy()235for prefix, namespace in list(nsmap.items()):236attribute = NamespacedAttribute(237"xmlns", prefix, "http://www.w3.org/2000/xmlns/")238attrs[attribute] = namespace239240# Namespaces are in play. Find any attributes that came in241# from lxml with namespaces attached to their names, and242# turn then into NamespacedAttribute objects.243new_attrs = {}244for attr, value in list(attrs.items()):245namespace, attr = self._getNsTag(attr)246if namespace is None:247new_attrs[attr] = value248else:249nsprefix = self._prefix_for_namespace(namespace)250attr = NamespacedAttribute(nsprefix, attr, namespace)251new_attrs[attr] = value252attrs = new_attrs253254namespace, name = self._getNsTag(name)255nsprefix = self._prefix_for_namespace(namespace)256self.soup.handle_starttag(name, namespace, nsprefix, attrs)257258def _prefix_for_namespace(self, namespace):259"""Find the currently active prefix for the given namespace."""260if namespace is None:261return None262for inverted_nsmap in reversed(self.nsmaps):263if inverted_nsmap is not None and namespace in inverted_nsmap:264return inverted_nsmap[namespace]265return None266267def end(self, name):268self.soup.endData()269completed_tag = self.soup.tagStack[-1]270namespace, name = self._getNsTag(name)271nsprefix = None272if namespace is not None:273for inverted_nsmap in reversed(self.nsmaps):274if inverted_nsmap is not None and namespace in inverted_nsmap:275nsprefix = inverted_nsmap[namespace]276break277self.soup.handle_endtag(name, nsprefix)278if len(self.nsmaps) > 1:279# This tag, or one of its parents, introduced a namespace280# mapping, so pop it off the stack.281self.nsmaps.pop()282283def pi(self, target, data):284self.soup.endData()285self.soup.handle_data(target + ' ' + data)286self.soup.endData(self.processing_instruction_class)287288def data(self, content):289self.soup.handle_data(content)290291def doctype(self, name, pubid, system):292self.soup.endData()293doctype = Doctype.for_name_and_ids(name, pubid, system)294self.soup.object_was_parsed(doctype)295296def comment(self, content):297"Handle comments as Comment objects."298self.soup.endData()299self.soup.handle_data(content)300self.soup.endData(Comment)301302def test_fragment_to_document(self, fragment):303"""See `TreeBuilder`."""304return '<?xml version="1.0" encoding="utf-8"?>\n%s' % fragment305306307class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):308309NAME = LXML310ALTERNATE_NAMES = ["lxml-html"]311312features = ALTERNATE_NAMES + [NAME, HTML, FAST, PERMISSIVE]313is_xml = False314processing_instruction_class = ProcessingInstruction315316def default_parser(self, encoding):317return etree.HTMLParser318319def feed(self, markup):320encoding = self.soup.original_encoding321try:322self.parser = self.parser_for(encoding)323self.parser.feed(markup)324self.parser.close()325except (UnicodeDecodeError, LookupError, etree.ParserError) as e:326raise ParserRejectedMarkup(e)327328329def test_fragment_to_document(self, fragment):330"""See `TreeBuilder`."""331return '<html><body>%s</body></html>' % fragment332333334