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