parse.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # coding: utf-8
  2. """
  3. Everything related to parsing links from bookmark services.
  4. For a list of supported services, see the README.md.
  5. For examples of supported files see examples/.
  6. Parsed link schema: {
  7. 'url': 'https://example.com/example/?abc=123&xyc=345#lmnop',
  8. 'domain': 'example.com',
  9. 'base_url': 'example.com/example/',
  10. 'timestamp': '15442123124234',
  11. 'tags': 'abc,def',
  12. 'title': 'Example.com Page Title',
  13. 'sources': ['ril_export.html', 'downloads/getpocket.com.txt'],
  14. }
  15. """
  16. import re
  17. import json
  18. import urllib
  19. import xml.etree.ElementTree as etree
  20. from datetime import datetime
  21. from util import (
  22. domain,
  23. base_url,
  24. str_between,
  25. get_link_type,
  26. fetch_page_title,
  27. URL_REGEX,
  28. )
  29. def get_parsers(file):
  30. """return all parsers that work on a given file, defaults to all of them"""
  31. return {
  32. 'pocket': parse_pocket_export,
  33. 'pinboard': parse_json_export,
  34. 'bookmarks': parse_bookmarks_export,
  35. 'rss': parse_rss_export,
  36. 'pinboard_rss': parse_pinboard_rss_feed,
  37. 'medium_rss': parse_medium_rss_feed,
  38. 'plain_text': parse_plain_text,
  39. }
  40. def parse_links(path):
  41. """parse a list of links dictionaries from a bookmark export file"""
  42. links = []
  43. with open(path, 'r', encoding='utf-8') as file:
  44. for parser_func in get_parsers(file).values():
  45. # otherwise try all parsers until one works
  46. try:
  47. links += list(parser_func(file))
  48. if links:
  49. break
  50. except (ValueError, TypeError, IndexError, AttributeError, etree.ParseError):
  51. # parser not supported on this file
  52. pass
  53. return links
  54. def parse_pocket_export(html_file):
  55. """Parse Pocket-format bookmarks export files (produced by getpocket.com/export/)"""
  56. html_file.seek(0)
  57. pattern = re.compile("^\\s*<li><a href=\"(.+)\" time_added=\"(\\d+)\" tags=\"(.*)\">(.+)</a></li>", re.UNICODE)
  58. for line in html_file:
  59. # example line
  60. # <li><a href="http://example.com/ time_added="1478739709" tags="tag1,tag2">example title</a></li>
  61. match = pattern.search(line)
  62. if match:
  63. fixed_url = match.group(1).replace('http://www.readability.com/read?url=', '') # remove old readability prefixes to get original url
  64. time = datetime.fromtimestamp(float(match.group(2)))
  65. info = {
  66. 'url': fixed_url,
  67. 'domain': domain(fixed_url),
  68. 'base_url': base_url(fixed_url),
  69. 'timestamp': str(datetime.now().timestamp()),
  70. 'tags': match.group(3),
  71. 'title': match.group(4).replace(' — Readability', '').replace('http://www.readability.com/read?url=', '') or fetch_page_title(url),
  72. 'sources': [html_file.name],
  73. }
  74. info['type'] = get_link_type(info)
  75. yield info
  76. def parse_json_export(json_file):
  77. """Parse JSON-format bookmarks export files (produced by pinboard.in/export/, or wallabag)"""
  78. json_file.seek(0)
  79. json_content = json.load(json_file)
  80. for line in json_content:
  81. # example line
  82. # {"href":"http:\/\/www.reddit.com\/r\/example","description":"title here","extended":"","meta":"18a973f09c9cc0608c116967b64e0419","hash":"910293f019c2f4bb1a749fb937ba58e3","time":"2014-06-14T15:51:42Z","shared":"no","toread":"no","tags":"reddit android"}]
  83. if line:
  84. erg = line
  85. if erg.get('timestamp'):
  86. timestamp = str(erg['timestamp']/10000000) # chrome/ff histories use a very precise timestamp
  87. elif erg.get('time'):
  88. timestamp = str(datetime.strptime(erg['time'].split(',', 1)[0], '%Y-%m-%dT%H:%M:%SZ').timestamp())
  89. elif erg.get('created_at'):
  90. timestamp = str(datetime.strptime(erg['created_at'], '%Y-%m-%dT%H:%M:%S%z').timestamp())
  91. else:
  92. timestamp = str(datetime.now().timestamp())
  93. if erg.get('href'):
  94. url = erg['href']
  95. else:
  96. url = erg['url']
  97. if erg.get('description'):
  98. title = (erg.get('description') or '').replace(' — Readability', '')
  99. else:
  100. title = erg['title'].strip()
  101. info = {
  102. 'url': url,
  103. 'domain': domain(url),
  104. 'base_url': base_url(url),
  105. 'timestamp': timestamp,
  106. 'tags': erg.get('tags') or '',
  107. 'title': title or fetch_page_title(url),
  108. 'sources': [json_file.name],
  109. }
  110. info['type'] = get_link_type(info)
  111. yield info
  112. def parse_rss_export(rss_file):
  113. """Parse RSS XML-format files into links"""
  114. rss_file.seek(0)
  115. items = rss_file.read().split('</item>\n<item>')
  116. for item in items:
  117. # example item:
  118. # <item>
  119. # <title><![CDATA[How JavaScript works: inside the V8 engine]]></title>
  120. # <category>Unread</category>
  121. # <link>https://blog.sessionstack.com/how-javascript-works-inside</link>
  122. # <guid>https://blog.sessionstack.com/how-javascript-works-inside</guid>
  123. # <pubDate>Mon, 21 Aug 2017 14:21:58 -0500</pubDate>
  124. # </item>
  125. trailing_removed = item.split('</item>', 1)[0]
  126. leading_removed = trailing_removed.split('<item>', 1)[-1]
  127. rows = leading_removed.split('\n')
  128. def get_row(key):
  129. return [r for r in rows if r.startswith('<{}>'.format(key))][0]
  130. title = str_between(get_row('title'), '<![CDATA[', ']]').strip()
  131. url = str_between(get_row('link'), '<link>', '</link>')
  132. ts_str = str_between(get_row('pubDate'), '<pubDate>', '</pubDate>')
  133. time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %z")
  134. info = {
  135. 'url': url,
  136. 'domain': domain(url),
  137. 'base_url': base_url(url),
  138. 'timestamp': str(datetime.now().timestamp()),
  139. 'tags': '',
  140. 'title': title or fetch_page_title(url),
  141. 'sources': [rss_file.name],
  142. }
  143. info['type'] = get_link_type(info)
  144. yield info
  145. def parse_bookmarks_export(html_file):
  146. """Parse netscape-format bookmarks export files (produced by all browsers)"""
  147. html_file.seek(0)
  148. pattern = re.compile("<a href=\"(.+?)\" add_date=\"(\\d+)\"[^>]*>(.+)</a>", re.UNICODE | re.IGNORECASE)
  149. for line in html_file:
  150. # example line
  151. # <DT><A HREF="https://example.com/?q=1+2" ADD_DATE="1497562974" LAST_MODIFIED="1497562974" ICON_URI="https://example.com/favicon.ico" ICON="data:image/png;base64,...">example bookmark title</A>
  152. match = pattern.search(line)
  153. if match:
  154. url = match.group(1)
  155. time = datetime.fromtimestamp(float(match.group(2)))
  156. info = {
  157. 'url': url,
  158. 'domain': domain(url),
  159. 'base_url': base_url(url),
  160. 'timestamp': str(datetime.now().timestamp()),
  161. 'tags': "",
  162. 'title': match.group(3).strip() or fetch_page_title(url),
  163. 'sources': [html_file.name],
  164. }
  165. info['type'] = get_link_type(info)
  166. yield info
  167. def parse_pinboard_rss_feed(rss_file):
  168. """Parse Pinboard RSS feed files into links"""
  169. rss_file.seek(0)
  170. root = etree.parse(rss_file).getroot()
  171. items = root.findall("{http://purl.org/rss/1.0/}item")
  172. for item in items:
  173. url = item.find("{http://purl.org/rss/1.0/}link").text
  174. tags = item.find("{http://purl.org/dc/elements/1.1/}subject").text
  175. title = item.find("{http://purl.org/rss/1.0/}title").text.strip()
  176. ts_str = item.find("{http://purl.org/dc/elements/1.1/}date").text
  177. # = 🌈🌈🌈🌈
  178. # = 🌈🌈🌈🌈
  179. # = 🏆🏆🏆🏆
  180. # Pinboard includes a colon in its date stamp timezone offsets, which
  181. # Python can't parse. Remove it:
  182. if ":" == ts_str[-3:-2]:
  183. ts_str = ts_str[:-3]+ts_str[-2:]
  184. time = datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z")
  185. info = {
  186. 'url': url,
  187. 'domain': domain(url),
  188. 'base_url': base_url(url),
  189. 'timestamp': str(datetime.now().timestamp()),
  190. 'tags': tags,
  191. 'title': title or fetch_page_title(url),
  192. 'sources': [rss_file.name],
  193. }
  194. info['type'] = get_link_type(info)
  195. yield info
  196. def parse_medium_rss_feed(rss_file):
  197. """Parse Medium RSS feed files into links"""
  198. rss_file.seek(0)
  199. root = etree.parse(rss_file).getroot()
  200. items = root.find("channel").findall("item")
  201. for item in items:
  202. # for child in item:
  203. # print(child.tag, child.text)
  204. url = item.find("link").text
  205. title = item.find("title").text.strip()
  206. ts_str = item.find("pubDate").text
  207. time = datetime.strptime(ts_str, "%a, %d %b %Y %H:%M:%S %Z")
  208. info = {
  209. 'url': url,
  210. 'domain': domain(url),
  211. 'base_url': base_url(url),
  212. 'timestamp': str(datetime.now().timestamp()),
  213. 'tags': '',
  214. 'title': title or fetch_page_title(url),
  215. 'sources': [rss_file.name],
  216. }
  217. info['type'] = get_link_type(info)
  218. yield info
  219. def parse_plain_text(text_file):
  220. """Parse raw links from each line in a text file"""
  221. text_file.seek(0)
  222. text_content = text_file.readlines()
  223. for line in text_content:
  224. if line:
  225. urls = re.findall(URL_REGEX, line)
  226. for url in urls:
  227. info = {
  228. 'url': url,
  229. 'domain': domain(url),
  230. 'base_url': base_url(url),
  231. 'timestamp': str(datetime.now().timestamp()),
  232. 'tags': '',
  233. 'title': fetch_page_title(url),
  234. 'sources': [text_file.name],
  235. }
  236. info['type'] = get_link_type(info)
  237. yield info