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