]> git.phdru.name Git - bookmarks_db.git/blob - parse_html/bkmk_parse_html.py
Cleanup code: use 4 spaces
[bookmarks_db.git] / parse_html / bkmk_parse_html.py
1 """HTML Parsers
2
3 This file is a part of Bookmarks database and Internet robot.
4
5 """
6
7 __author__ = "Oleg Broytman <phd@phdru.name>"
8 __copyright__ = "Copyright (C) 1997-2017 PhiloSoft Design"
9 __license__ = "GNU GPL"
10
11 __all__ = ['parse_html', 'parse_filename', 'universal_charset']
12
13
14 import codecs
15
16 universal_charset = "utf-8"
17 DEFAULT_CHARSET = "cp1251" # Stupid default for Russian Cyrillic
18
19 parsers = []
20
21 try:
22     from . import bkmk_ph_beautifulsoup
23 except ImportError:
24     pass
25 else:
26     bkmk_ph_beautifulsoup.DEFAULT_CHARSET = DEFAULT_CHARSET
27     parsers.append(bkmk_ph_beautifulsoup.parse_html)
28
29 try:
30     from . import bkmk_ph_html5
31 except ImportError:
32     pass
33 else:
34     parsers.append(bkmk_ph_html5.parse_html)
35
36 try:
37     from . import bkmk_ph_lxml
38 except ImportError:
39     pass
40 else:
41     parsers.append(bkmk_ph_lxml.parse_html)
42
43 try:
44     from . import bkmk_ph_htmlparser
45 except ImportError:
46     pass
47 else:
48     parsers.append(bkmk_ph_htmlparser.parse_html)
49
50 # ElementTidy often segfaults
51 #try:
52 #   from . import bkmk_ph_etreetidy
53 #except ImportError:
54 #   pass
55 #else:
56 #   parsers.append(bkmk_ph_etreetidy.parse_html)
57
58 import re
59 from htmlentitydefs import name2codepoint
60
61 entity_re = re.compile("(&\w+;)")
62 num_entity_re = re.compile("(&#[0-9]+;)")
63
64 def recode_entities(title, charset):
65     output = []
66     for part in entity_re.split(title):
67         if part not in ("&amp;", "&lt;", "&gt;", "&quot;") and \
68               entity_re.match(part):
69             _part = name2codepoint.get(part[1:-1], None)
70             if _part is not None:
71                 part = unichr(_part).encode(charset)
72         output.append(part)
73     title = ''.join(output)
74
75     output = []
76     for part in num_entity_re.split(title):
77         if num_entity_re.match(part):
78             try:
79                 part = unichr(int(part[2:-1])).encode(charset)
80             except UnicodeEncodeError:
81                 pass # Leave the entity as is
82         output.append(part)
83
84     return ''.join(output)
85
86
87 import os
88 BKMK_DEBUG_HTML_PARSERS = os.environ.get("BKMK_DEBUG_HTML_PARSERS")
89
90 def parse_html(html_text, charset=None, log=None):
91     if not parsers:
92         return None
93
94     if charset:
95         try:
96             codecs.lookup(charset) # In case of unknown charset...
97         except (ValueError, LookupError):
98             charset = None         # ...try charset from HTML
99
100     charsets = [universal_charset, DEFAULT_CHARSET]
101     if charset:
102         charset = charset.lower().replace("windows-", "cp")
103         if charset in charsets:
104             charsets.remove(charset)
105         charsets.insert(0, charset)
106
107     if BKMK_DEBUG_HTML_PARSERS:
108         _parsers = []
109     for p in parsers:
110         parser = None
111         for c in charsets:
112             try:
113                 parser = p(html_text, c, log)
114             except UnicodeError:
115                 pass
116             else:
117                 if parser:
118                     if BKMK_DEBUG_HTML_PARSERS:
119                         if log: log("   Parser %s: ok" % p.__module__)
120                         _parsers.append((p, parser))
121                     break
122         else:
123             if log: log("   Parser %s: fail" % p.__module__)
124         if not BKMK_DEBUG_HTML_PARSERS and parser:
125             break
126
127     if BKMK_DEBUG_HTML_PARSERS:
128         if not _parsers:
129             if log: log("   All parsers have failed")
130             return None
131     elif not parser:
132         if log: log("   All parsers have failed")
133         return None
134
135     if BKMK_DEBUG_HTML_PARSERS:
136         p, parser = _parsers[0]
137     if log: log("   Using %s" % p.__module__)
138
139     converted_title = title = parser.title
140     if title and (not parser.charset):
141         try:
142             unicode(title, "ascii")
143         except UnicodeDecodeError:
144             parser.charset = DEFAULT_CHARSET
145
146     if parser.charset:
147         parser.charset = parser.charset.lower().replace("windows-", "cp")
148
149     if title and parser.charset and (
150           (parser.charset != universal_charset) or
151           ((not charset) or (charset != parser.charset))):
152         try:
153             if parser.meta_charset:
154                 if log: log("   META charset   : %s" % parser.charset)
155             elif (not charset) or (charset != parser.charset):
156                 if log: log("   guessed charset: %s" % parser.charset)
157             #if log: log("   current charset: %s" % universal_charset)
158             if log: log("   title          : %s" % title)
159             if parser.charset != universal_charset:
160                 try:
161                     converted_title = unicode(title, parser.charset).encode(universal_charset)
162                 except UnicodeError:
163                     if log: log("   incorrect conversion from %s, converting from %s" % (parser.charset, DEFAULT_CHARSET))
164                     converted_title = unicode(title, DEFAULT_CHARSET, "replace").encode(universal_charset, "replace")
165                     parser.charset = DEFAULT_CHARSET
166             if log and (converted_title != title): log("   converted title: %s" % converted_title)
167         except LookupError:
168             if log: log("   unknown charset: '%s'" % parser.charset)
169     else:
170         if log: log("   title          : %s" % title)
171
172     if title:
173         final_title = recode_entities(converted_title, universal_charset)
174         parts = [s.strip() for s in final_title.replace('\r', '').split('\n')]
175         final_title = ' '.join([s for s in parts if s])
176         if log and (final_title != converted_title): log("   final title    : %s" % final_title)
177         parser.title = final_title
178
179     icon = parser.icon
180     if isinstance(icon, unicode):
181         try:
182             parser.icon = icon.encode('ascii')
183         except UnicodeEncodeError:
184             if parser.charset:
185                 parser.icon = icon.encode(parser.charset)
186     return parser
187
188 def parse_filename(filename, charset=None, log=None):
189     fp = open(filename, 'r')
190     try:
191         parser = parse_html(fp.read(), charset=charset, log=log)
192     finally:
193         fp.close()
194     return parser