25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

516 satır
16 KiB

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