OSDN Git Service

append TEXTRenderer and HTMLElement.inner_text()
[htmltree-py/htmltree.git] / htmltree.py
1 # htmltree.py by hylom
2 # -*- coding: utf-8 -*-
3
4 """htmltree.py - HTML Element-Tree Builder
5 by hylom <hylomm@@single_at_mark@@gmail.com>
6 """
7
8 import HTMLParser
9 import re
10
11 class HTMLElementError(Exception):
12     def __init__(self, msg, elem):
13         self.msg = msg
14         self.elem = elem
15
16     def __repr__(self):
17         str = "HTML Element Error: %s in %s" % (self.msg, self.elem)
18         return str
19
20 class Renderer(object):
21     """HTMLElement Render base class."""
22     def attrs2str(self, elem):
23         strs = []
24         for attr in elem.attrs:
25             if elem.attrs[attr] == None:
26                 strs.append(attr)
27             elif "'" in elem.attrs[attr]:
28                 strs.append('%s="%s"' % (attr, elem.attrs[attr]))
29             else:
30                 strs.append("%s='%s'" % (attr, elem.attrs[attr]))
31         strs.insert(0, "")
32         return " ".join(strs)
33
34 class HTMLRenderer(Renderer):
35     """Render HTMLElement as HTML."""
36     # TODO: check tags not need to close more strict...
37     UNCLOSABLE_TAGS = ["br", "link", "meta", "img"]
38
39     def render_inner(self, elem):
40         texts = []
41         for child in elem:
42             self._recursive(child, texts)
43         return "".join(texts)
44
45     def render(self, elem):
46         texts = []
47         self._recursive(elem, texts)
48         return "".join(texts)
49
50     def _recursive(self, elem, texts):
51         if elem.is_tag():
52             texts.append("<" + elem.name + self.attrs2str(elem) + ">")
53             for child in elem:
54                 self._recursive(child, texts)
55             if not elem.name in self.UNCLOSABLE_TAGS:
56                 texts.append("</" + elem.name + ">")
57         elif elem.is_text():
58             if elem.text():
59                 texts.append(elem.text())
60         elif elem.is_root():
61             for child in elem:
62                 self._recursive(child, texts)
63         elif elem.is_decl():
64             texts.append("<!" + elem.name + ">")
65
66
67 class TEXTRenderer(Renderer):
68     """Render HTMLElement as TEXT."""
69     # TODO: check tags not need to close more strict...
70     UNCLOSABLE_TAGS = ["br", "link", "meta", "img"]
71
72     def render_inner(self, elem):
73         texts = []
74         for child in elem:
75             self._recursive(child, texts)
76         return "".join(texts)
77
78     def render(self, elem):
79         texts = []
80         self._recursive(elem, texts)
81         return "".join(texts)
82
83     def _recursive(self, elem, texts):
84         if elem.is_tag():
85             for child in elem:
86                 self._recursive(child, texts)
87         elif elem.is_text():
88             if elem.text():
89                 texts.append(elem.text())
90         elif elem.is_root():
91             for child in elem:
92                 self._recursive(child, texts)
93
94 class HTMLElement(list):
95     """HTML element object to use as tree nodes."""
96     ROOT = 0
97     TAG = 100
98     TEXT = 200
99     DECL = 300
100
101     def __init__(self, type, name="", attrs={}):
102         """
103         create HTMLElement object.
104
105         Arguments:
106         type -- element type. HTMLElement.(ROOT|TAG|TEXT)
107         name -- element name (default: "")
108         attrs -- dict of attributes (default:{})
109
110         Example:
111         attr = dict(href="http://example.com/", target="_blank")
112         e = HTMLElement(HTMLElement.TAG, "a", attr)
113         # 'e' means <a href="http://example.com/" target="_blank">
114         """
115
116         self.type = type
117         self.name = name
118         self.attrs = dict(attrs)
119         self._text = ""
120         self._parent = None
121         self._next_elem = None
122         self._prev_elem = None
123
124     def __repr__(self):
125         if self.type == HTMLElement.TAG:
126             return "<TAG:%s %s>" % (self.name, self._attrs2str())
127         elif self.type == HTMLElement.DECL:
128             return "<DECL:'%s'>" % self.name
129         elif self.type == HTMLElement.TEXT:
130             return "<TEXT:'%s'>" % self._text
131         else:
132             return "<UNKNOWN>"
133
134     def __eq__(self, other):
135         return id(self) == id(other)
136
137     def _attrs2str(self):
138         str = []
139         f = lambda x,y: x if y == None else "%s='%s'" % (x,y)
140
141         strs = [f(x,self.attrs[x]) for x in self.attrs]
142         return " ".join(strs)
143
144     # basic acquision functions
145     def get_attribute(self, attr, default=None):
146         """returns given attribute's value."""
147         return self.attrs.get(attr, default)
148
149     def attr(self, attr, default=None):
150         """returns given attribute's value."""
151         return self.attrs.get(attr, default)
152
153     def has_attribute(self, attr):
154         """returns True if element has "attr" attribute."""
155         return attr in self.attrs
156
157     def text(self):
158         """returns content in the tag."""
159         return self._text
160
161     def inner_html(self):
162         "returns inner html"
163         rn = HTMLRenderer()
164         return rn.render_inner(self)
165
166     def inner_text(self):
167         "returns inner text"
168         rn = TEXTRenderer()
169         return rn.render_inner(self)
170
171     # navigation functions
172     def parent(self):
173         """returns tag's parent element."""
174         return self._parent
175
176     def next(self):
177         """returns tag's next element."""
178         return self._next_elem
179
180     def prev(self):
181         """returns tag's previous element."""
182         return self._prev_elem
183
184     # basic query functions
185     def get_elements_by_name(self, name):
186         buf = []
187         self._r_get_elements_by_name(name, buf)
188         return buf
189
190     def _r_get_elements_by_name(self, name, buf):
191         if self.name == name:
192             buf.append(self)
193         for i in self:
194             i._r_get_elements_by_name(name, buf)
195
196     def get_element_by_id(self, id):
197         if "id" in self.attrs and self.attrs["id"] == id:
198             return self
199         for i in self:
200             e = i.get_element_by_id(id)
201             if e != None:
202                 return e
203         #raise HTMLElementError("Element not found")
204         return None
205
206     def get_elements_by_class(self, cls):
207         buf = []
208         self._r_get_elements_by_class(cls, buf)
209         return buf
210
211     def _r_get_elements_by_class(self, cls, buf):
212         if self.get_attribute("class") == cls:
213             buf.append(self)
214         for i in self:
215             i._r_get_elements_by_class(cls, buf)
216
217     def get_elements(self, name, attrs):
218         elems = self.get_elements_by_name(name)
219         results = []
220         for elem in elems:
221             for name in attrs:
222                 if elem.get_attribute(name, "") != attrs[name]:
223                     break
224             else:
225                 results.append(elem)
226         return results
227
228     # manipulation functions
229     def append_tag(self, tag, attrs):
230         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
231         self.append(elem)
232
233     def remove_element(self, elem):
234         parent = elem.parent()
235         parent.remove(elem)
236
237     def delete(self):
238         p = self.parent()
239         p.remove(self)
240
241     # query functions
242     # TODO: this function is under implementing...
243     def select(self, expr):
244         terms = expr.strip().split()
245         if len(terms) == 0:
246             return []
247         results = self
248         for pat in terms:
249             t = []
250             for elem in results:
251                 t.extend(self._select_pattern(pat, elem))
252             results = t
253         return results
254
255     def _select_pattern(self, pat, elem):
256         results = []
257         if pat[0] == "#":
258             results = [elem.get_element_by_id(pat[1:]),]
259         elif pat[0] == ".":
260             results = elem.get_elements_by_class(pat[1:])
261         return [x for x in results if x]
262
263     def select_1st(self, expr):
264         r = self.select(expr)
265         if len(r) == 0:
266             return None
267         else:
268             return r[0]
269
270     def select_by_name2(self, term1, term2):
271         tbl = self.get_elements_by_name(term1)
272         buf = []
273         for elem in tbl:
274             st = elem.get_elements_by_name(term2)
275             buf.extend(st)
276         return buf
277
278     # is_* functions
279     def is_text(self):
280         return self.type == HTMLElement.TEXT
281
282     def is_tag(self):
283         return self.type == HTMLElement.TAG
284
285     def is_root(self):
286         return self.type == HTMLElement.ROOT
287
288     def is_decl(self):
289         return self.type == HTMLElement.DECL
290
291     def is_descendant(self, tagname):
292         p = self.parent()
293         while p != None:
294             if p.name == tagname:
295                 return p
296             p = p.parent()
297         return False
298
299     # mmmh....
300     def trace_back(self, tag):
301         """ regexp string => list"""
302         p = self.parent()
303         rex = re.compile(tag)
304         result = []
305         while p != None:
306             if rex.search(p.name):
307                 result.append(p.name)
308             p = p.parent()
309         return result
310
311
312 class HTMLTreeError(Exception):
313     def __init__(self, msg, lineno, offset):
314         self.msg = msg
315         self.lineno = lineno
316         self.offset = offset
317
318     def __repr__(self):
319         str = "HTML Parse Error: %s , line: %d, char: %d" % (self.msg, self.lineno, self.offset)
320         return str
321
322
323 def parse(data, charset=None, option=0):
324     "parse HTML and returns HTMLTree object"
325     tree = HTMLTree()
326     tree.parse(data, charset, option)
327     return tree
328
329
330 class HTMLTree(HTMLParser.HTMLParser):
331     "HTML Tree Builder"
332     USE_VALIDATE = 0x0001
333
334     IGNORE_BLANK = 0x0010
335     TRUNC_BLANK  = 0x0020
336     JOIN_TEXT    = 0x0040
337
338     TRUNC_BR = 0x0100
339     # TODO: check tags not need to close more strict...
340     UNCLOSABLE_TAGS = ["br", "link", "meta", "img", "input"]
341
342     def __init__(self):
343         "Constructor"
344         HTMLParser.HTMLParser.__init__(self)
345
346     def parse(self, data, charset=None, option=0):
347         """
348         Parse given HTML.
349
350         Arguments:
351         data -- HTML to parse
352         charset -- charset of HTML (default: None)
353         option -- option (default: 0, meaning none)
354         
355         """
356
357         self.charset = charset
358         self._htmlroot = HTMLElement(HTMLElement.ROOT)
359         self._cursor = self._htmlroot
360         self._option = option
361         try:
362             self.feed(data)
363         except HTMLParser.HTMLParseError, e:
364             raise HTMLTreeError("HTML parse error: " + e.msg,
365                                 e.lineno, e.offset)
366
367         # if charset is not given, detect charset
368         if self.charset == None:
369             r = self.root()
370             metas = r.get_elements_by_name("meta")
371             for meta in metas:
372                 if meta.attrs.get("http-equiv", None) == "Content-Type":
373                     ctype = meta.attrs.get("content", "")
374                     m = re.search(r"charset=([^;]+)", ctype)
375                     if m:
376                         self.charset = m.group(1)
377                     else:
378                         self.charset = None
379                         
380             if self.charset:
381                 self._htmlroot = HTMLElement(HTMLElement.ROOT)
382                 self._cursor = self._htmlroot
383                 self.feed(data)
384
385         self._finalize()
386
387     def _finalize(self):
388         r = self.root()
389         self._r_finalize(r)
390
391     def _r_finalize(self, elem):
392         if elem.is_text():
393             return
394         
395         l = len(elem)
396         if l > 1:
397             elem[0]._next_elem = elem[1]
398         for i in range(1, l-1):
399             elem[i]._prev_elem = elem[i-1]
400             elem[i]._next_elem = elem[i+1]
401         if l > 1:
402             elem[l-1]._prev_elem = elem[l-2]
403
404         for sub_elem in elem:
405             self._r_finalize(sub_elem)
406
407     def validate(self):
408         r = self.root()
409         self._r_validate(self, e)
410
411     # Handlers
412     def handle_starttag(self, tag, attrs):
413         # some tags treat as start-end tag.
414         if tag in self.UNCLOSABLE_TAGS:
415             return self.handle_startendtag(tag, attrs)
416             
417         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
418
419         if self._option & HTMLTree.USE_VALIDATE > 0:
420             # try validation (experimental)
421             if tag == "li" and self._cursor.name == "li":
422                 self.handle_endtag("li")
423             # end of validation
424
425         elem._parent = self._cursor
426         self._cursor.append(elem)
427         self._cursor = elem
428
429     def handle_endtag(self, tag):
430         # some tags treat as start-end tag.
431         if tag in self.UNCLOSABLE_TAGS:
432             return
433
434         self._cursor = self._cursor.parent()
435
436     def handle_startendtag(self, tag, attrs):
437         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
438         elem._parent = self._cursor
439         self._cursor.append(elem)
440
441     def handle_data(self, data):
442         if self._option & HTMLTree.IGNORE_BLANK > 0:
443             if re.search(r"^\s*$", data):
444                 data = ""
445
446         elem = HTMLElement(HTMLElement.TEXT)
447         elem._parent = self._cursor
448
449         # text encode check and convert.
450         # if charset is given, convert text to unicode type.
451         if self.charset:
452             try:
453                 elem._text = unicode(data, self.charset)
454             except TypeError:
455                 # self.charset is utf-8.
456                 elem._text = data
457         else:
458             # treat as unicode input
459             elem._text = data
460         self._cursor.append(elem)
461
462     def handle_entityref(self, name):
463         data = "&" + name + ";"
464         self.handle_data(data)
465
466     def handle_charref(self, ref):
467         data = "&#" + ref + ";"
468         self.handle_data(data)
469
470     def handle_decl(self, decl):
471         elem = HTMLElement(HTMLElement.DECL, decl)
472         elem._parent = self._cursor
473         self._cursor.append(elem)
474
475     # Accessor
476     def root(self):
477         return self._htmlroot