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