]> git.phdru.name Git - bookmarks_db.git/blob - parse_html/bkmk_parse_html.py
Fix(parse_html): encode title to utf-8 as the last resort
[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_beautifulsoup4
23 except ImportError:
24     pass
25 else:
26     bkmk_ph_beautifulsoup4.DEFAULT_CHARSET = DEFAULT_CHARSET
27     parsers.append(bkmk_ph_beautifulsoup4.parse_html)
28
29 try:
30     from . import bkmk_ph_beautifulsoup
31 except ImportError:
32     pass
33 else:
34     bkmk_ph_beautifulsoup.DEFAULT_CHARSET = DEFAULT_CHARSET
35     parsers.append(bkmk_ph_beautifulsoup.parse_html)
36
37 try:
38     from . import bkmk_ph_html5
39 except ImportError:
40     pass
41 else:
42     parsers.append(bkmk_ph_html5.parse_html)
43
44 try:
45     from . import bkmk_ph_lxml
46 except ImportError:
47     pass
48 else:
49     parsers.append(bkmk_ph_lxml.parse_html)
50
51 try:
52     from . import bkmk_ph_htmlparser
53 except ImportError:
54     pass
55 else:
56     parsers.append(bkmk_ph_htmlparser.parse_html)
57
58 # ElementTidy often segfaults
59 #try:
60 #   from . import bkmk_ph_etreetidy
61 #except ImportError:
62 #   pass
63 #else:
64 #   parsers.append(bkmk_ph_etreetidy.parse_html)
65
66 import re
67 from htmlentitydefs import name2codepoint
68
69 entity_re = re.compile("(&\w+;)")
70 num_entity_re = re.compile("(&#[0-9]+;)")
71
72 def recode_entities(title, charset):
73     output = []
74     for part in entity_re.split(title):
75         if part not in ("&amp;", "&lt;", "&gt;", "&quot;") and \
76               entity_re.match(part):
77             _part = name2codepoint.get(part[1:-1], None)
78             if _part is not None:
79                 part = unichr(_part).encode(charset)
80         output.append(part)
81     title = ''.join(output)
82
83     output = []
84     for part in num_entity_re.split(title):
85         if num_entity_re.match(part):
86             try:
87                 part = unichr(int(part[2:-1])).encode(charset)
88             except UnicodeEncodeError:
89                 pass # Leave the entity as is
90         output.append(part)
91
92     return ''.join(output)
93
94
95 import os
96 BKMK_DEBUG_HTML_PARSERS = os.environ.get("BKMK_DEBUG_HTML_PARSERS")
97
98 def parse_html(html_text, charset=None, log=None):
99     if not parsers:
100         return None
101
102     if charset:
103         try:
104             codecs.lookup(charset) # In case of unknown charset...
105         except (ValueError, LookupError):
106             charset = None         # ...try charset from HTML
107
108     charsets = [universal_charset, DEFAULT_CHARSET]
109     if charset:
110         charset = charset.lower().replace("windows-", "cp")
111         if charset in charsets:
112             charsets.remove(charset)
113         charsets.insert(0, charset)
114
115     if BKMK_DEBUG_HTML_PARSERS:
116         _parsers = []
117     for p in parsers:
118         parser = None
119         for c in charsets:
120             try:
121                 parser = p(html_text, c, log)
122             except UnicodeError:
123                 pass
124             else:
125                 if parser:
126                     if BKMK_DEBUG_HTML_PARSERS:
127                         if log: log("   Parser %s: ok" % p.__module__)
128                         _parsers.append((p, parser))
129                     break
130         else:
131             if log: log("   Parser %s: fail" % p.__module__)
132         if not BKMK_DEBUG_HTML_PARSERS and parser:
133             break
134
135     if BKMK_DEBUG_HTML_PARSERS:
136         if not _parsers:
137             if log: log("   All parsers have failed")
138             return None
139     elif not parser:
140         if log: log("   All parsers have failed")
141         return None
142
143     if BKMK_DEBUG_HTML_PARSERS:
144         p, parser = _parsers[0]
145     if log: log("   Using %s" % p.__module__)
146
147     title = parser.title
148     if isinstance(title, unicode):
149         if parser.charset:
150             parser.title = title.encode(parser.charset)
151         else:
152             try:
153                 parser.title = title.encode('ascii')
154             except UnicodeEncodeError:
155                 try:
156                     parser.title = title.encode(DEFAULT_CHARSET)
157                 except UnicodeEncodeError:
158                     parser.title = title.encode(universal_charset)
159                     parser.charset = universal_charset
160                 else:
161                     parser.charset = DEFAULT_CHARSET
162             else:
163                 parser.charset = 'ascii'
164
165     converted_title = title = parser.title
166     if title and (not parser.charset):
167         try:
168             unicode(title, "ascii")
169         except UnicodeDecodeError:
170             parser.charset = DEFAULT_CHARSET
171
172     if parser.charset:
173         parser.charset = parser.charset.lower().replace("windows-", "cp")
174
175     if title and parser.charset and (
176           (parser.charset != universal_charset) or
177           ((not charset) or (charset != parser.charset))):
178         try:
179             if parser.meta_charset:
180                 if log: log("   META charset   : %s" % parser.charset)
181             elif (not charset) or (charset != parser.charset):
182                 if log: log("   guessed charset: %s" % parser.charset)
183             #if log: log("   current charset: %s" % universal_charset)
184             if log: log("   title          : %s" % title)
185             if parser.charset != universal_charset:
186                 try:
187                     converted_title = unicode(title, parser.charset).encode(universal_charset)
188                 except UnicodeError:
189                     if log: log("   incorrect conversion from %s, converting from %s" % (parser.charset, DEFAULT_CHARSET))
190                     converted_title = unicode(title, DEFAULT_CHARSET, "replace").encode(universal_charset, "replace")
191                     parser.charset = DEFAULT_CHARSET
192             if log and (converted_title != title): log("   converted title: %s" % converted_title)
193         except LookupError:
194             if log: log("   unknown charset: '%s'" % parser.charset)
195     else:
196         if log: log("   title          : %s" % title)
197
198     if title:
199         final_title = recode_entities(converted_title, universal_charset)
200         parts = [s.strip() for s in final_title.replace('\r', '').split('\n')]
201         final_title = ' '.join([s for s in parts if s])
202         if log and (final_title != converted_title): log("   final title    : %s" % final_title)
203         parser.title = final_title
204
205     icon = parser.icon
206     if isinstance(icon, unicode):
207         try:
208             parser.icon = icon.encode('ascii')
209         except UnicodeEncodeError:
210             if parser.charset:
211                 parser.icon = icon.encode(parser.charset)
212     return parser
213
214 def parse_filename(filename, charset=None, log=None):
215     fp = open(filename, 'r')
216     try:
217         parser = parse_html(fp.read(), charset=charset, log=log)
218     finally:
219         fp.close()
220     return parser