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

Source Code for Module lxml.html.soupparser

  1  """External interface to the BeautifulSoup HTML parser. 
  2  """ 
  3   
  4  __all__ = ["fromstring", "parse", "convert_tree"] 
  5   
  6  import re 
  7  from lxml import etree, html 
  8   
  9  try: 
 10      from bs4 import ( 
 11          BeautifulSoup, Tag, Comment, ProcessingInstruction, NavigableString, 
 12          Declaration, CData, Doctype) 
 13      _DECLARATION_OR_DOCTYPE = (Declaration, Doctype) 
 14  except ImportError: 
 15      from BeautifulSoup import ( 
 16          BeautifulSoup, Tag, Comment, ProcessingInstruction, NavigableString, 
 17          Declaration, CData) 
 18      _DECLARATION_OR_DOCTYPE = Declaration 
19 20 21 -def fromstring(data, beautifulsoup=None, makeelement=None, **bsargs):
22 """Parse a string of HTML data into an Element tree using the 23 BeautifulSoup parser. 24 25 Returns the root ``<html>`` Element of the tree. 26 27 You can pass a different BeautifulSoup parser through the 28 `beautifulsoup` keyword, and a diffent Element factory function 29 through the `makeelement` keyword. By default, the standard 30 ``BeautifulSoup`` class and the default factory of `lxml.html` are 31 used. 32 """ 33 return _parse(data, beautifulsoup, makeelement, **bsargs)
34
35 36 -def parse(file, beautifulsoup=None, makeelement=None, **bsargs):
37 """Parse a file into an ElemenTree using the BeautifulSoup parser. 38 39 You can pass a different BeautifulSoup parser through the 40 `beautifulsoup` keyword, and a diffent Element factory function 41 through the `makeelement` keyword. By default, the standard 42 ``BeautifulSoup`` class and the default factory of `lxml.html` are 43 used. 44 """ 45 if not hasattr(file, 'read'): 46 file = open(file) 47 root = _parse(file, beautifulsoup, makeelement, **bsargs) 48 return etree.ElementTree(root)
49
50 51 -def convert_tree(beautiful_soup_tree, makeelement=None):
52 """Convert a BeautifulSoup tree to a list of Element trees. 53 54 Returns a list instead of a single root Element to support 55 HTML-like soup with more than one root element. 56 57 You can pass a different Element factory through the `makeelement` 58 keyword. 59 """ 60 root = _convert_tree(beautiful_soup_tree, makeelement) 61 children = root.getchildren() 62 for child in children: 63 root.remove(child) 64 return children
65
66 67 # helpers 68 69 -def _parse(source, beautifulsoup, makeelement, **bsargs):
70 if beautifulsoup is None: 71 beautifulsoup = BeautifulSoup 72 if hasattr(beautifulsoup, "HTML_ENTITIES"): # bs3 73 if 'convertEntities' not in bsargs: 74 bsargs['convertEntities'] = 'html' 75 if hasattr(beautifulsoup, "DEFAULT_BUILDER_FEATURES"): # bs4 76 if 'features' not in bsargs: 77 bsargs['features'] = ['html.parser'] # use Python html parser 78 tree = beautifulsoup(source, **bsargs) 79 root = _convert_tree(tree, makeelement) 80 # from ET: wrap the document in a html root element, if necessary 81 if len(root) == 1 and root[0].tag == "html": 82 return root[0] 83 root.tag = "html" 84 return root
85 86 87 _parse_doctype_declaration = re.compile( 88 r'(?:\s|[<!])*DOCTYPE\s*HTML' 89 r'(?:\s+PUBLIC)?(?:\s+(\'[^\']*\'|"[^"]*"))?' 90 r'(?:\s+(\'[^\']*\'|"[^"]*"))?', 91 re.IGNORECASE).match
92 93 94 -class _PseudoTag:
95 # Minimal imitation of BeautifulSoup.Tag
96 - def __init__(self, contents):
97 self.name = 'html' 98 self.attrs = [] 99 self.contents = contents
100
101 - def __iter__(self):
102 return self.contents.__iter__()
103
104 105 -def _convert_tree(beautiful_soup_tree, makeelement):
106 if makeelement is None: 107 makeelement = html.html_parser.makeelement 108 109 # Split the tree into three parts: 110 # i) everything before the root element: document type 111 # declaration, comments, processing instructions, whitespace 112 # ii) the root(s), 113 # iii) everything after the root: comments, processing 114 # instructions, whitespace 115 first_element_idx = last_element_idx = None 116 html_root = declaration = None 117 for i, e in enumerate(beautiful_soup_tree): 118 if isinstance(e, Tag): 119 if first_element_idx is None: 120 first_element_idx = i 121 last_element_idx = i 122 if html_root is None and e.name and e.name.lower() == 'html': 123 html_root = e 124 elif declaration is None and isinstance(e, _DECLARATION_OR_DOCTYPE): 125 declaration = e 126 127 # For a nice, well-formatted document, the variable roots below is 128 # a list consisting of a single <html> element. However, the document 129 # may be a soup like '<meta><head><title>Hello</head><body>Hi 130 # all<\p>'. In this example roots is a list containing meta, head 131 # and body elements. 132 pre_root = beautiful_soup_tree.contents[:first_element_idx] 133 roots = beautiful_soup_tree.contents[first_element_idx:last_element_idx+1] 134 post_root = beautiful_soup_tree.contents[last_element_idx+1:] 135 136 # Reorganize so that there is one <html> root... 137 if html_root is not None: 138 # ... use existing one if possible, ... 139 i = roots.index(html_root) 140 html_root.contents = roots[:i] + html_root.contents + roots[i+1:] 141 else: 142 # ... otherwise create a new one. 143 html_root = _PseudoTag(roots) 144 145 convert_node = _init_node_converters(makeelement) 146 147 # Process pre_root 148 res_root = convert_node(html_root) 149 prev = res_root 150 for e in reversed(pre_root): 151 converted = convert_node(e) 152 if converted is not None: 153 prev.addprevious(converted) 154 prev = converted 155 156 # ditto for post_root 157 prev = res_root 158 for e in post_root: 159 converted = convert_node(e) 160 if converted is not None: 161 prev.addnext(converted) 162 prev = converted 163 164 if declaration is not None: 165 try: 166 # bs4 provides full Doctype string 167 doctype_string = declaration.output_ready() 168 except AttributeError: 169 doctype_string = declaration.string 170 171 match = _parse_doctype_declaration(doctype_string) 172 if not match: 173 # Something is wrong if we end up in here. Since soupparser should 174 # tolerate errors, do not raise Exception, just let it pass. 175 pass 176 else: 177 external_id, sys_uri = match.groups() 178 docinfo = res_root.getroottree().docinfo 179 # strip quotes and update DOCTYPE values (any of None, '', '...') 180 docinfo.public_id = external_id and external_id[1:-1] 181 docinfo.system_url = sys_uri and sys_uri[1:-1] 182 183 return res_root
184
185 186 -def _init_node_converters(makeelement):
187 converters = {} 188 ordered_node_types = [] 189 190 def converter(*types): 191 def add(handler): 192 for t in types: 193 converters[t] = handler 194 ordered_node_types.append(t) 195 return handler
196 return add 197 198 def find_best_converter(node): 199 for t in ordered_node_types: 200 if isinstance(node, t): 201 return converters[t] 202 return None 203 204 def convert_node(bs_node, parent=None): 205 # duplicated in convert_tag() below 206 try: 207 handler = converters[type(bs_node)] 208 except KeyError: 209 handler = converters[type(bs_node)] = find_best_converter(bs_node) 210 if handler is None: 211 return None 212 return handler(bs_node, parent) 213 214 def map_attrs(bs_attrs): 215 if isinstance(bs_attrs, dict): # bs4 216 attribs = {} 217 for k, v in bs_attrs.items(): 218 if isinstance(v, list): 219 v = " ".join(v) 220 attribs[k] = unescape(v) 221 else: 222 attribs = dict((k, unescape(v)) for k, v in bs_attrs) 223 return attribs 224 225 def append_text(parent, text): 226 if len(parent) == 0: 227 parent.text = (parent.text or '') + text 228 else: 229 parent[-1].tail = (parent[-1].tail or '') + text 230 231 # converters are tried in order of their definition 232 233 @converter(Tag, _PseudoTag) 234 def convert_tag(bs_node, parent): 235 attrs = bs_node.attrs 236 if parent is not None: 237 attribs = map_attrs(attrs) if attrs else None 238 res = etree.SubElement(parent, bs_node.name, attrib=attribs) 239 else: 240 attribs = map_attrs(attrs) if attrs else {} 241 res = makeelement(bs_node.name, attrib=attribs) 242 243 for child in bs_node: 244 # avoid double recursion by inlining convert_node(), see above 245 try: 246 handler = converters[type(child)] 247 except KeyError: 248 pass 249 else: 250 if handler is not None: 251 handler(child, res) 252 continue 253 convert_node(child, res) 254 return res 255 256 @converter(Comment) 257 def convert_comment(bs_node, parent): 258 res = etree.Comment(bs_node) 259 if parent is not None: 260 parent.append(res) 261 return res 262 263 @converter(ProcessingInstruction) 264 def convert_pi(bs_node, parent): 265 if bs_node.endswith('?'): 266 # The PI is of XML style (<?as df?>) but BeautifulSoup 267 # interpreted it as being SGML style (<?as df>). Fix. 268 bs_node = bs_node[:-1] 269 res = etree.ProcessingInstruction(*bs_node.split(' ', 1)) 270 if parent is not None: 271 parent.append(res) 272 return res 273 274 @converter(NavigableString) 275 def convert_text(bs_node, parent): 276 if parent is not None: 277 append_text(parent, unescape(bs_node)) 278 return None 279 280 return convert_node 281 282 283 # copied from ET's ElementSoup 284 285 try: 286 from html.entities import name2codepoint # Python 3 287 except ImportError: 288 from htmlentitydefs import name2codepoint 289 290 291 handle_entities = re.compile("&(\w+);").sub
292 293 294 -def unescape(string):
295 if not string: 296 return '' 297 # work around oddities in BeautifulSoup's entity handling 298 def unescape_entity(m): 299 try: 300 return unichr(name2codepoint[m.group(1)]) 301 except KeyError: 302 return m.group(0) # use as is
303 return handle_entities(unescape_entity, string) 304