Sitemap generator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

153 lines
5.7 KiB

  1. # -*- coding: utf-8 -*-
  2. import __future__
  3. import sys
  4. import urlparse
  5. import requests
  6. from lxml import html
  7. import re
  8. import time
  9. try:
  10. import sys
  11. if 'threading' in sys.modules:
  12. del sys.modules['threading']
  13. print('threading module loaded before patching!')
  14. print('threading module deleted from sys.modules!\n')
  15. from gevent import monkey, pool
  16. monkey.patch_all()
  17. gevent_installed = True
  18. except:
  19. print("Gevent does not installed. Parsing process will be slower.")
  20. gevent_installed = False
  21. class Crawler:
  22. def __init__(self, url, outputfile='sitemap.xml', logfile='error.log', oformat='xml'):
  23. self.url = url
  24. self.logfile = open(logfile, 'a')
  25. self.oformat = oformat
  26. self.outputfile = outputfile
  27. # create lists for the urls in que and visited urls
  28. self.urls = set([url])
  29. self.visited = set([url])
  30. self.exts = ['htm', 'php']
  31. self.allowed_regex = '\.((?!htm)(?!php)\w+)$'
  32. self.errors = {'404': []}
  33. def set_exts(self, exts):
  34. self.exts = exts
  35. def allow_regex(self, regex=None):
  36. if regex is not None:
  37. self.allowed_regex = regex
  38. else:
  39. allowed_regex = ''
  40. for ext in self.exts:
  41. allowed_regex += '(!{})'.format(ext)
  42. self.allowed_regex = '\.({}\w+)$'.format(allowed_regex)
  43. def crawl(self, echo=False, pool_size=1):
  44. # sys.stdout.write('echo attribute deprecated and will be removed in future')
  45. self.echo = echo
  46. self.regex = re.compile(self.allowed_regex)
  47. print('Parsing pages')
  48. if gevent_installed and pool_size >= 1:
  49. self.pool = pool.Pool(pool_size)
  50. self.pool.spawn(self.parse_gevent)
  51. self.pool.join()
  52. else:
  53. while len(self.urls) > 0:
  54. self.parse()
  55. if self.oformat == 'xml':
  56. self.write_xml()
  57. elif self.oformat == 'txt':
  58. self.write_txt()
  59. with open('errors.txt', 'w') as err_file:
  60. for key, val in self.errors.items():
  61. err_file.write(u'\n\nError {}\n\n'.format(key))
  62. err_file.write(u'\n'.join(set(val)))
  63. def parse_gevent(self):
  64. self.parse()
  65. while len(self.urls) > 0 and not self.pool.full():
  66. self.pool.spawn(self.parse_gevent)
  67. def parse(self):
  68. if self.echo:
  69. n_visited, n_urls, n_pool = len(self.visited), len(self.urls), len(self.pool)
  70. status = (
  71. '{} pages parsed :: {} pages in the queue'.format(n_visited, n_urls),
  72. '{} pages parsed :: {} parsing processes :: {} pages in the queue'.format(n_visited, n_pool, n_urls)
  73. )
  74. print(status[int(gevent_installed)])
  75. if not self.urls:
  76. return
  77. else:
  78. url = self.urls.pop()
  79. try:
  80. response = requests.get(url)
  81. # if status code is not 404, then add url in seld.errors dictionary
  82. if response.status_code != 200:
  83. if self.errors.get(str(response.status_code), False):
  84. self.errors[str(response.status_code)].extend([url])
  85. else:
  86. self.errors.update({str(response.status_code): [url]})
  87. self.errlog("Error {} at url {}".format(response.status_code, url))
  88. return
  89. tree = html.fromstring(response.text)
  90. for link_tag in tree.findall('.//a'):
  91. link = link_tag.attrib.get('href', '')
  92. newurl = urlparse.urljoin(self.url, link)
  93. # print(newurl)
  94. if self.is_valid(newurl):
  95. self.visited.update([newurl])
  96. self.urls.update([newurl])
  97. except Exception, e:
  98. self.errlog(e.message)
  99. def is_valid(self, url):
  100. if '#' in url:
  101. url = url[:url.find('#')]
  102. if url in self.visited:
  103. return False
  104. if self.url not in url:
  105. return False
  106. if re.search(self.regex, url):
  107. return False
  108. return True
  109. def errlog(self, msg):
  110. self.logfile.write(msg)
  111. self.logfile.write('\n')
  112. def write_xml(self):
  113. of = open(self.outputfile, 'w')
  114. of.write('<?xml version="1.0" encoding="utf-8"?>\n')
  115. of.write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">\n')
  116. url_str = '<url><loc>{}</loc></url>\n'
  117. while self.visited:
  118. of.write(url_str.format(self.visited.pop()))
  119. of.write('</urlset>')
  120. of.close()
  121. def write_txt(self):
  122. of = open(self.outputfile, 'w')
  123. url_str = '{}\n'
  124. while self.visited:
  125. of.write(url_str.format(self.visited.pop()))
  126. of.close()
  127. def show_progress(self, count, total, status=''):
  128. bar_len = 60
  129. filled_len = int(round(bar_len * count / float(total)))
  130. percents = round(100.0 * count / float(total), 1)
  131. bar = '=' * filled_len + '-' * (bar_len - filled_len)
  132. sys.stdout.write('[%s] %s%s ...%s\r' % (bar, percents, '%', status))
  133. sys.stdout.flush() # As suggested by Rom Ruben (see: http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console/27871113#comment50529068_27871113)
  134. time.sleep(0.5)