OSDN Git Service

17ed0ae6936e7449b20499d56b3991435ffd785b
[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         
64
65 class HTMLElement(list):
66     """HTML element object to use as tree nodes."""
67     ROOT = 0
68     TAG = 100
69     TEXT = 200
70
71     def __init__(self, type, name="", attrs={}):
72         """
73         create HTMLElement object.
74
75         Arguments:
76         type -- element type. HTMLElement.(ROOT|TAG|TEXT)
77         name -- element name (default: "")
78         attrs -- dict of attributes (default:{})
79
80         Example:
81         attr = dict(href="http://example.com/", target="_blank")
82         e = HTMLElement(HTMLElement.TAG, "a", attr)
83         # 'e' means <a href="http://example.com/" target="_blank">
84         """
85
86         self.type = type
87         self.name = name
88         self.attrs = dict(attrs)
89         self._text = ""
90         self._parent = None
91         self._next_elem = None
92         self._prev_elem = None
93
94     def __repr__(self):
95         if self.type == HTMLElement.TAG:
96             return "<TAG:%s %s>" % (self.name, self._attrs2str())
97         elif self.type == HTMLElement.TEXT:
98             return "<TEXT:'%s'>" % self._text
99         else:
100             return "<UNKNOWN>"
101
102     def _attrs2str(self):
103         str = []
104         f = lambda x,y: x if y == None else "%s='%s'" % (x,y)
105
106         strs = [f(x,self.attrs[x]) for x in self.attrs]
107         return " ".join(strs)
108
109     # basic acquision functions
110     def get_attribute(self, attr, default=None):
111         """returns given attribute's value."""
112         return self.attrs.get(attr, default)
113
114     def has_attribute(self, attr):
115         """returns True if element has "attr" attribute."""
116         return attr in self.attrs
117
118     def text(self):
119         """returns content in the tag."""
120         return self._text
121
122     def inner_html(self):
123         "returns inner html"
124         rn = HTMLRenderer()
125         return rn.render_inner(self)
126
127     # navigation functions
128     def parent(self):
129         """returns tag's parent element."""
130         return self._parent
131
132     def next(self):
133         """returns tag's next element."""
134         return self._next_elem
135
136     def prev(self):
137         """returns tag's previous element."""
138         return self._prev_elem
139
140     # basic query functions
141     def get_elements_by_name(self, name):
142         buf = []
143         self._r_get_elements_by_name(name, buf)
144         return buf
145
146     def _r_get_elements_by_name(self, name, buf):
147         if self.name == name:
148             buf.append(self)
149         for i in self:
150             i._r_get_elements_by_name(name, buf)
151
152     def get_element_by_id(self, id):
153         if "id" in self.attrs and self.attrs["id"] == id:
154             return self
155         for i in self:
156             e = i.get_element_by_id(id)
157             if e != None:
158                 return e
159         #raise HTMLElementError("Element not found")
160         return None
161
162     def get_elements_by_class(self, cls):
163         buf = []
164         self._r_get_elements_by_class(cls, buf)
165         return buf
166
167     def _r_get_elements_by_class(self, cls, buf):
168         if self.get_attribute("class") == cls:
169             buf.append(self)
170         for i in self:
171             i._r_get_elements_by_class(cls, buf)
172
173     # manipulation functions
174     def append_tag(self, tag, attrs):
175         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
176         self.append(elem)
177
178     # query functions
179     # TODO: this function is under implementing...
180     def select(self, expr):
181         terms = expr.strip().split()
182         if len(terms) == 0:
183             return []
184         results = self
185         for pat in terms:
186             t = []
187             for elem in results:
188                 t.extend(self._select_pattern(pat, elem))
189             results = t
190         return results
191
192     def _select_pattern(self, pat, elem):
193         results = []
194         if pat[0] == "#":
195             results = [elem.get_element_by_id(pat[1:]),]
196         elif pat[0] == ".":
197             results = elem.get_elements_by_class(pat[1:])
198         return [x for x in results if x]
199
200     def select_1st(self, expr):
201         r = self.select(expr)
202         if len(r) == 0:
203             return None
204         else:
205             return r[0]
206
207     def select_by_name2(self, term1, term2):
208         tbl = self.get_elements_by_name(term1)
209         buf = []
210         for elem in tbl:
211             st = elem.get_elements_by_name(term2)
212             buf.extend(st)
213         return buf
214
215     # is_* functions
216     def is_text(self):
217         return self.type == HTMLElement.TEXT
218
219     def is_tag(self):
220         return self.type == HTMLElement.TAG
221
222     def is_root(self):
223         return self.type == HTMLElement.ROOT
224
225     def is_descendant(self, tagname):
226         p = self.parent()
227         while p != None:
228             if p.name == tagname:
229                 return p
230             p = p.parent()
231         return False
232
233     # mmmh....
234     def trace_back(self, tag):
235         """ regexp string => list"""
236         p = self.parent()
237         rex = re.compile(tag)
238         result = []
239         while p != None:
240             if rex.search(p.name):
241                 result.append(p.name)
242             p = p.parent()
243         return result
244
245
246 class HTMLTreeError(Exception):
247     def __init__(self, msg, lineno, offset):
248         self.msg = msg
249         self.lineno = lineno
250         self.offset = offset
251
252     def __repr__(self):
253         str = "HTML Parse Error: %s , line: %d, char: %d" % (self.msg, self.lineno, self.offset)
254         return str
255     
256
257 class HTMLTree(HTMLParser.HTMLParser):
258     "HTML Tree Builder"
259     USE_VALIDATE = 0x0001
260
261     IGNORE_BLANK = 0x0010
262     TRUNC_BLANK  = 0x0020
263     JOIN_TEXT    = 0x0040
264
265     TRUNC_BR = 0x0100
266     # TODO: check tags not need to close more strict...
267     UNCLOSABLE_TAGS = ["br", "link", "meta", "img", "input"]
268
269     def __init__(self):
270         "Constructor"
271         HTMLParser.HTMLParser.__init__(self)
272
273     def parse(self, data, charset=None, option=0):
274         """
275         Parse given HTML.
276
277         Arguments:
278         data -- HTML to parse
279         charset -- charset of HTML (default: None)
280         option -- option (default: 0, meaning none)
281         
282         """
283
284         self.charset = charset
285         self._htmlroot = HTMLElement(HTMLElement.ROOT)
286         self._cursor = self._htmlroot
287         self._option = option
288         try:
289             self.feed(data)
290         except HTMLParser.HTMLParseError, e:
291             raise HTMLTreeError("HTML parse error: " + e.msg,
292                                 e.lineno, e.offset)
293
294         # if charset is not given, detect charset
295         if self.charset == None:
296             r = self.root()
297             metas = r.get_elements_by_name("meta")
298             for meta in metas:
299                 if meta.attrs.get("http-equiv", None) == "Content-Type":
300                     ctype = meta.attrs.get("content", "")
301                     m = re.search(r"charset=([^;]+)", ctype)
302                     if m:
303                         self.charset = m.group(1)
304                     else:
305                         self.charset = None
306                         
307             if self.charset:
308                 self._htmlroot = HTMLElement(HTMLElement.ROOT)
309                 self._cursor = self._htmlroot
310                 self.feed(data)
311
312         self._finalize()
313
314     def _finalize(self):
315         r = self.root()
316         self._r_finalize(r)
317
318     def _r_finalize(self, elem):
319         if elem.is_text():
320             return
321         
322         l = len(elem)
323         if l > 1:
324             elem[0]._next_elem = elem[1]
325         for i in range(1, l-1):
326             elem[i]._prev_elem = elem[i-1]
327             elem[i]._next_elem = elem[i+1]
328         if l > 1:
329             elem[l-1]._prev_elem = elem[l-2]
330
331         for sub_elem in elem:
332             self._r_finalize(sub_elem)
333
334     def validate(self):
335         r = self.root()
336         self._r_validate(self, e)
337
338     # Handlers
339     def handle_starttag(self, tag, attrs):
340         # some tags treat as start-end tag.
341         if tag in self.UNCLOSABLE_TAGS:
342             return self.handle_startendtag(tag, attrs)
343             
344         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
345
346         if self._option & HTMLTree.USE_VALIDATE > 0:
347             # try validation (experimental)
348             if tag == "li" and self._cursor.name == "li":
349                 self.handle_endtag("li")
350             # end of validation
351
352         elem._parent = self._cursor
353         self._cursor.append(elem)
354         self._cursor = elem
355
356     def handle_endtag(self, tag):
357         # some tags treat as start-end tag.
358         if tag in self.UNCLOSABLE_TAGS:
359             return
360
361         self._cursor = self._cursor.parent()
362
363     def handle_startendtag(self, tag, attrs):
364         elem = HTMLElement(HTMLElement.TAG, tag, attrs)
365         elem._parent = self._cursor
366         self._cursor.append(elem)
367
368     def handle_data(self, data):
369         if self._option & HTMLTree.IGNORE_BLANK > 0:
370             if re.search(r"^\s*$", data):
371                 data = ""
372
373         elem = HTMLElement(HTMLElement.TEXT)
374         elem._parent = self._cursor
375
376         # text encode check and convert.
377         # if charset is given, convert text to unicode type.
378         if self.charset:
379             try:
380                 elem._text = unicode(data, self.charset)
381             except TypeError:
382                 # self.charset is utf-8.
383                 elem._text = data
384         else:
385             # treat as unicode input
386             elem._text = data
387         self._cursor.append(elem)
388
389     # Accessor
390     def root(self):
391         return self._htmlroot