Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 
 

497 рядки
15 KiB

  1. # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. #!/usr/bin/env python
  4. from __future__ import unicode_literals
  5. """html2text: Turn HTML into equivalent Markdown-structured text."""
  6. __version__ = "3.02"
  7. __author__ = "Aaron Swartz (me@aaronsw.com)"
  8. __copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3."
  9. __contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
  10. # TODO:
  11. # Support decoded entities with unifiable.
  12. try:
  13. True
  14. except NameError:
  15. setattr(__builtins__, 'True', 1)
  16. setattr(__builtins__, 'False', 0)
  17. def has_key(x, y):
  18. if hasattr(x, 'has_key'): return x.has_key(y)
  19. else: return y in x
  20. try:
  21. import htmlentitydefs
  22. import urlparse
  23. import HTMLParser
  24. except ImportError: #Python3
  25. import html.entities as htmlentitydefs
  26. import urllib.parse as urlparse
  27. import html.parser as HTMLParser
  28. try: #Python3
  29. import urllib.request as urllib
  30. except:
  31. import urllib
  32. import optparse, re, sys, codecs, types
  33. try: from textwrap import wrap
  34. except: pass
  35. # Use Unicode characters instead of their ascii psuedo-replacements
  36. UNICODE_SNOB = 0
  37. # Put the links after each paragraph instead of at the end.
  38. LINKS_EACH_PARAGRAPH = 0
  39. # Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
  40. BODY_WIDTH = 78
  41. # Don't show internal links (href="#local-anchor") -- corresponding link targets
  42. # won't be visible in the plain text file anyway.
  43. SKIP_INTERNAL_LINKS = False
  44. ### Entity Nonsense ###
  45. def name2cp(k):
  46. if k == 'apos': return ord("'")
  47. if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3
  48. return htmlentitydefs.name2codepoint[k]
  49. else:
  50. k = htmlentitydefs.entitydefs[k]
  51. if k.startswith("&#") and k.endswith(";"): return int(k[2:-1]) # not in latin-1
  52. return ord(codecs.latin_1_decode(k)[0])
  53. unifiable = {'rsquo':"'", 'lsquo':"'", 'rdquo':'"', 'ldquo':'"',
  54. 'copy':'(C)', 'mdash':'--', 'nbsp':' ', 'rarr':'->', 'larr':'<-', 'middot':'*',
  55. 'ndash':'-', 'oelig':'oe', 'aelig':'ae',
  56. 'agrave':'a', 'aacute':'a', 'acirc':'a', 'atilde':'a', 'auml':'a', 'aring':'a',
  57. 'egrave':'e', 'eacute':'e', 'ecirc':'e', 'euml':'e',
  58. 'igrave':'i', 'iacute':'i', 'icirc':'i', 'iuml':'i',
  59. 'ograve':'o', 'oacute':'o', 'ocirc':'o', 'otilde':'o', 'ouml':'o',
  60. 'ugrave':'u', 'uacute':'u', 'ucirc':'u', 'uuml':'u'}
  61. unifiable_n = {}
  62. for k in unifiable.keys():
  63. unifiable_n[name2cp(k)] = unifiable[k]
  64. def charref(name):
  65. if name[0] in ['x','X']:
  66. c = int(name[1:], 16)
  67. else:
  68. c = int(name)
  69. if not UNICODE_SNOB and c in unifiable_n.keys():
  70. return unifiable_n[c]
  71. else:
  72. try:
  73. return unichr(c)
  74. except NameError: #Python3
  75. return chr(c)
  76. def entityref(c):
  77. if not UNICODE_SNOB and c in unifiable.keys():
  78. return unifiable[c]
  79. else:
  80. try: name2cp(c)
  81. except KeyError: return "&" + c + ';'
  82. else:
  83. try:
  84. return unichr(name2cp(c))
  85. except NameError: #Python3
  86. return chr(name2cp(c))
  87. def replaceEntities(s):
  88. s = s.group(1)
  89. if s[0] == "#":
  90. return charref(s[1:])
  91. else: return entityref(s)
  92. r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
  93. def unescape(s):
  94. return r_unescape.sub(replaceEntities, s)
  95. ### End Entity Nonsense ###
  96. def onlywhite(line):
  97. """Return true if the line does only consist of whitespace characters."""
  98. for c in line:
  99. if c is not ' ' and c is not ' ':
  100. return c is ' '
  101. return line
  102. def optwrap(text):
  103. """Wrap all paragraphs in the provided text."""
  104. if not BODY_WIDTH:
  105. return text
  106. assert wrap, "Requires Python 2.3."
  107. result = ''
  108. newlines = 0
  109. for para in text.split("\n"):
  110. if len(para) > 0:
  111. if para[0] != ' ' and para[0] != '-' and para[0] != '*':
  112. for line in wrap(para, BODY_WIDTH):
  113. result += line + "\n"
  114. result += "\n"
  115. newlines = 2
  116. else:
  117. if not onlywhite(para):
  118. result += para + "\n"
  119. newlines = 1
  120. else:
  121. if newlines < 2:
  122. result += "\n"
  123. newlines += 1
  124. return result
  125. def hn(tag):
  126. if tag[0] == 'h' and len(tag) == 2:
  127. try:
  128. n = int(tag[1])
  129. if n in range(1, 10): return n
  130. except ValueError: return 0
  131. class _html2text(HTMLParser.HTMLParser):
  132. def __init__(self, out=None, baseurl=''):
  133. HTMLParser.HTMLParser.__init__(self)
  134. if out is None: self.out = self.outtextf
  135. else: self.out = out
  136. try:
  137. self.outtext = unicode()
  138. except NameError: # Python3
  139. self.outtext = str()
  140. self.quiet = 0
  141. self.p_p = 0
  142. self.outcount = 0
  143. self.start = 1
  144. self.space = 0
  145. self.a = []
  146. self.astack = []
  147. self.acount = 0
  148. self.list = []
  149. self.blockquote = 0
  150. self.pre = 0
  151. self.startpre = 0
  152. self.lastWasNL = 0
  153. self.abbr_title = None # current abbreviation definition
  154. self.abbr_data = None # last inner HTML (for abbr being defined)
  155. self.abbr_list = {} # stack of abbreviations to write later
  156. self.baseurl = baseurl
  157. def outtextf(self, s):
  158. self.outtext += s
  159. def close(self):
  160. HTMLParser.HTMLParser.close(self)
  161. self.pbr()
  162. self.o('', 0, 'end')
  163. return self.outtext
  164. def handle_charref(self, c):
  165. self.o(charref(c))
  166. def handle_entityref(self, c):
  167. self.o(entityref(c))
  168. def handle_starttag(self, tag, attrs):
  169. self.handle_tag(tag, attrs, 1)
  170. def handle_endtag(self, tag):
  171. self.handle_tag(tag, None, 0)
  172. def previousIndex(self, attrs):
  173. """ returns the index of certain set of attributes (of a link) in the
  174. self.a list
  175. If the set of attributes is not found, returns None
  176. """
  177. if not has_key(attrs, 'href'): return None
  178. i = -1
  179. for a in self.a:
  180. i += 1
  181. match = 0
  182. if has_key(a, 'href') and a['href'] == attrs['href']:
  183. if has_key(a, 'title') or has_key(attrs, 'title'):
  184. if (has_key(a, 'title') and has_key(attrs, 'title') and
  185. a['title'] == attrs['title']):
  186. match = True
  187. else:
  188. match = True
  189. if match: return i
  190. def handle_tag(self, tag, attrs, start):
  191. #attrs = fixattrs(attrs)
  192. if hn(tag):
  193. self.p()
  194. if start: self.o(hn(tag)*"#" + ' ')
  195. if tag in ['p', 'div']: self.p()
  196. if tag == "br" and start: self.o(" \n")
  197. if tag == "hr" and start:
  198. self.p()
  199. self.o("* * *")
  200. self.p()
  201. if tag in ["head", "style", 'script']:
  202. if start: self.quiet += 1
  203. else: self.quiet -= 1
  204. if tag in ["body"]:
  205. self.quiet = 0 # sites like 9rules.com never close <head>
  206. if tag == "blockquote":
  207. if start:
  208. self.p(); self.o('> ', 0, 1); self.start = 1
  209. self.blockquote += 1
  210. else:
  211. self.blockquote -= 1
  212. self.p()
  213. if tag in ['em', 'i', 'u']: self.o("_")
  214. if tag in ['strong', 'b']: self.o("**")
  215. if tag == "code" and not self.pre: self.o('`') #TODO: `` `this` ``
  216. if tag == "abbr":
  217. if start:
  218. attrsD = {}
  219. for (x, y) in attrs: attrsD[x] = y
  220. attrs = attrsD
  221. self.abbr_title = None
  222. self.abbr_data = ''
  223. if has_key(attrs, 'title'):
  224. self.abbr_title = attrs['title']
  225. else:
  226. if self.abbr_title != None:
  227. self.abbr_list[self.abbr_data] = self.abbr_title
  228. self.abbr_title = None
  229. self.abbr_data = ''
  230. if tag == "a":
  231. if start:
  232. attrsD = {}
  233. for (x, y) in attrs: attrsD[x] = y
  234. attrs = attrsD
  235. if has_key(attrs, 'href') and not (SKIP_INTERNAL_LINKS and attrs['href'].startswith('#')):
  236. self.astack.append(attrs)
  237. self.o("[")
  238. else:
  239. self.astack.append(None)
  240. else:
  241. if self.astack:
  242. a = self.astack.pop()
  243. if a:
  244. i = self.previousIndex(a)
  245. if i is not None:
  246. a = self.a[i]
  247. else:
  248. self.acount += 1
  249. a['count'] = self.acount
  250. a['outcount'] = self.outcount
  251. self.a.append(a)
  252. self.o("][" + str(a['count']) + "]")
  253. if tag == "img" and start:
  254. attrsD = {}
  255. for (x, y) in attrs: attrsD[x] = y
  256. attrs = attrsD
  257. if has_key(attrs, 'src'):
  258. attrs['href'] = attrs['src']
  259. alt = attrs.get('alt', '')
  260. i = self.previousIndex(attrs)
  261. if i is not None:
  262. attrs = self.a[i]
  263. else:
  264. self.acount += 1
  265. attrs['count'] = self.acount
  266. attrs['outcount'] = self.outcount
  267. self.a.append(attrs)
  268. self.o("![")
  269. self.o(alt)
  270. self.o("]["+ str(attrs['count']) +"]")
  271. if tag == 'dl' and start: self.p()
  272. if tag == 'dt' and not start: self.pbr()
  273. if tag == 'dd' and start: self.o(' ')
  274. if tag == 'dd' and not start: self.pbr()
  275. if tag in ["ol", "ul"]:
  276. if start:
  277. self.list.append({'name':tag, 'num':0})
  278. else:
  279. if self.list: self.list.pop()
  280. self.p()
  281. if tag == 'li':
  282. if start:
  283. self.pbr()
  284. if self.list: li = self.list[-1]
  285. else: li = {'name':'ul', 'num':0}
  286. self.o(" "*len(self.list)) #TODO: line up <ol><li>s > 9 correctly.
  287. if li['name'] == "ul": self.o("* ")
  288. elif li['name'] == "ol":
  289. li['num'] += 1
  290. self.o(str(li['num'])+". ")
  291. self.start = 1
  292. else:
  293. self.pbr()
  294. if tag in ["table", "tr"] and start: self.p()
  295. if tag == 'td': self.pbr()
  296. if tag == "pre":
  297. if start:
  298. self.startpre = 1
  299. self.pre = 1
  300. else:
  301. self.pre = 0
  302. self.p()
  303. def pbr(self):
  304. if self.p_p == 0: self.p_p = 1
  305. def p(self): self.p_p = 2
  306. def o(self, data, puredata=0, force=0):
  307. if self.abbr_data is not None: self.abbr_data += data
  308. if not self.quiet:
  309. if puredata and not self.pre:
  310. data = re.sub('\s+', ' ', data)
  311. if data and data[0] == ' ':
  312. self.space = 1
  313. data = data[1:]
  314. if not data and not force: return
  315. if self.startpre:
  316. #self.out(" :") #TODO: not output when already one there
  317. self.startpre = 0
  318. bq = (">" * self.blockquote)
  319. if not (force and data and data[0] == ">") and self.blockquote: bq += " "
  320. if self.pre:
  321. bq += " "
  322. data = data.replace("\n", "\n"+bq)
  323. if self.start:
  324. self.space = 0
  325. self.p_p = 0
  326. self.start = 0
  327. if force == 'end':
  328. # It's the end.
  329. self.p_p = 0
  330. self.out("\n")
  331. self.space = 0
  332. if self.p_p:
  333. self.out(('\n'+bq)*self.p_p)
  334. self.space = 0
  335. if self.space:
  336. if not self.lastWasNL: self.out(' ')
  337. self.space = 0
  338. if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"):
  339. if force == "end": self.out("\n")
  340. newa = []
  341. for link in self.a:
  342. if self.outcount > link['outcount']:
  343. self.out(" ["+ str(link['count']) +"]: " + urlparse.urljoin(self.baseurl, link['href']))
  344. if has_key(link, 'title'): self.out(" ("+link['title']+")")
  345. self.out("\n")
  346. else:
  347. newa.append(link)
  348. if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done.
  349. self.a = newa
  350. if self.abbr_list and force == "end":
  351. for abbr, definition in self.abbr_list.items():
  352. self.out(" *[" + abbr + "]: " + definition + "\n")
  353. self.p_p = 0
  354. self.out(data)
  355. self.lastWasNL = data and data[-1] == '\n'
  356. self.outcount += 1
  357. def handle_data(self, data):
  358. if r'\/script>' in data: self.quiet -= 1
  359. self.o(data, 1)
  360. def unknown_decl(self, data): pass
  361. def wrapwrite(text):
  362. text = text.encode('utf-8')
  363. try: #Python3
  364. sys.stdout.buffer.write(text)
  365. except AttributeError:
  366. sys.stdout.write(text)
  367. def html2text_file(html, out=wrapwrite, baseurl=''):
  368. h = _html2text(out, baseurl)
  369. h.feed(html)
  370. h.feed("")
  371. return h.close()
  372. def html2text(html, baseurl=''):
  373. txt = html2text_file(html, None, baseurl)
  374. return optwrap(txt) #.encode('utf-8'))
  375. if __name__ == "__main__":
  376. baseurl = ''
  377. p = optparse.OptionParser('%prog [(filename|url) [encoding]]',
  378. version='%prog ' + __version__)
  379. args = p.parse_args()[1]
  380. if len(args) > 0:
  381. file_ = args[0]
  382. encoding = None
  383. if len(args) == 2:
  384. encoding = args[1]
  385. if len(args) > 2:
  386. p.error('Too many arguments')
  387. if file_.startswith('http://') or file_.startswith('https://'):
  388. baseurl = file_
  389. j = urllib.urlopen(baseurl)
  390. text = j.read()
  391. if encoding is None:
  392. try:
  393. from feedparser import _getCharacterEncoding as enc
  394. except ImportError:
  395. enc = lambda x, y: ('utf-8', 1)
  396. encoding = enc(j.headers, text)[0]
  397. if encoding == 'us-ascii':
  398. encoding = 'utf-8'
  399. data = text.decode(encoding)
  400. else:
  401. data = open(file_, 'rb').read()
  402. if encoding is None:
  403. try:
  404. from chardet import detect
  405. except ImportError:
  406. detect = lambda x: {'encoding': 'utf-8'}
  407. encoding = detect(data)['encoding']
  408. data = data.decode(encoding)
  409. else:
  410. data = sys.stdin.read()
  411. wrapwrite(html2text(data, baseurl))