A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

264 строки
12 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # 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
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. from gzip import GzipFile
  23. from socket import timeout
  24. from StringIO import StringIO
  25. from time import sleep, time
  26. from urllib2 import build_opener, URLError
  27. from earwigbot import exceptions, importer
  28. from earwigbot.wiki.copyvios.markov import MarkovChain, MarkovChainIntersection
  29. from earwigbot.wiki.copyvios.parsers import ArticleTextParser, HTMLTextParser
  30. from earwigbot.wiki.copyvios.result import CopyvioCheckResult
  31. from earwigbot.wiki.copyvios.search import YahooBOSSSearchEngine
  32. oauth = importer.new("oauth2")
  33. __all__ = ["CopyvioMixIn"]
  34. class CopyvioMixIn(object):
  35. """
  36. **EarwigBot: Wiki Toolset: Copyright Violation MixIn**
  37. This is a mixin that provides two public methods, :py:meth:`copyvio_check`
  38. and :py:meth:`copyvio_compare`. The former checks the page for copyright
  39. violations using a search engine API, and the latter compares the page
  40. against a given URL. Credentials for the search engine API are stored in
  41. the :py:class:`~earwigbot.wiki.site.Site`'s config.
  42. """
  43. def __init__(self, site):
  44. self._search_config = site._search_config
  45. self._exclusions_db = self._search_config.get("exclusions_db")
  46. self._opener = build_opener()
  47. self._opener.addheaders = site._opener.addheaders
  48. def _open_url_ignoring_errors(self, url):
  49. """Open a URL and return its parsed content, or None.
  50. First, we will decompress the content if the headers contain "gzip" as
  51. its content encoding. Then, we will return the content stripped using
  52. an HTML parser if the headers indicate it is HTML, or return the
  53. content directly if it is plain text. If we don't understand the
  54. content type, we'll return None.
  55. If a URLError was raised while opening the URL or an IOError was raised
  56. while decompressing, None will be returned.
  57. """
  58. try:
  59. response = self._opener.open(url.encode("utf8"), timeout=5)
  60. result = response.read()
  61. except (URLError, timeout):
  62. return None
  63. if response.headers.get("Content-Encoding") == "gzip":
  64. stream = StringIO(result)
  65. gzipper = GzipFile(fileobj=stream)
  66. try:
  67. result = gzipper.read()
  68. except IOError:
  69. return None
  70. ctype_full = response.headers.get("Content-Type", "text/plain")
  71. ctype = ctype_full.split(" ", 1)[0]
  72. if ctype in ["text/html", "application/xhtml+xml"]:
  73. return HTMLTextParser(result).strip()
  74. elif ctype == "text/plain":
  75. return result.strip()
  76. else:
  77. return None
  78. def _select_search_engine(self):
  79. """Return a function that can be called to do web searches.
  80. The function takes one argument, a search query, and returns a list of
  81. URLs, ranked by importance. The underlying logic depends on the
  82. *engine* argument within our config; for example, if *engine* is
  83. "Yahoo! BOSS", we'll use YahooBOSSSearchEngine for querying.
  84. Raises UnknownSearchEngineError if the 'engine' listed in our config is
  85. unknown to us, and UnsupportedSearchEngineError if we are missing a
  86. required package or module, like oauth2 for "Yahoo! BOSS".
  87. """
  88. engine = self._search_config["engine"]
  89. credentials = self._search_config["credentials"]
  90. if engine == "Yahoo! BOSS":
  91. try:
  92. oauth.__version__ # Force-load the lazy module
  93. except ImportError:
  94. e = "Yahoo! BOSS requires the 'oauth2' package: https://github.com/simplegeo/python-oauth2"
  95. raise exceptions.UnsupportedSearchEngineError(e)
  96. return YahooBOSSSearchEngine(credentials, self._opener)
  97. raise exceptions.UnknownSearchEngineError(engine)
  98. def _copyvio_compare_content(self, article, url):
  99. """Return a number comparing an article and a URL.
  100. The *article* is a Markov chain, whereas the *url* is just a string
  101. that we'll try to open and read ourselves.
  102. """
  103. text = self._open_url_ignoring_errors(url)
  104. if not text:
  105. empty = MarkovChain("")
  106. return 0, (empty, MarkovChainIntersection(empty, empty))
  107. source = MarkovChain(text)
  108. delta = MarkovChainIntersection(article, source)
  109. return float(delta.size()) / article.size(), (source, delta)
  110. def copyvio_check(self, min_confidence=0.5, max_queries=-1, max_time=-1,
  111. interquery_sleep=1):
  112. """Check the page for copyright violations.
  113. Returns a
  114. :py:class:`~earwigbot.wiki.copyvios.result.CopyvioCheckResult` object
  115. with information on the results of the check.
  116. *max_queries* is self-explanatory; we will never make more than this
  117. number of queries in a given check. If it's lower than 0, we will not
  118. limit the number of queries.
  119. *max_time* can be set to prevent copyvio checks from taking longer than
  120. a set amount of time (generally around a minute), which can be useful
  121. if checks are called through a web server with timeouts. We will stop
  122. checking new URLs as soon as this limit is reached.
  123. *interquery_sleep* is the minimum amount of time we will sleep between
  124. search engine queries, in seconds.
  125. Raises :py:exc:`~earwigbot.exceptions.CopyvioCheckError` or subclasses
  126. (:py:exc:`~earwigbot.exceptions.UnknownSearchEngineError`,
  127. :py:exc:`~earwigbot.exceptions.SearchQueryError`, ...) on errors.
  128. """
  129. start_time = time()
  130. searcher = self._select_search_engine()
  131. if self._exclusions_db:
  132. self._exclusions_db.sync(self.site.name)
  133. handled_urls = []
  134. best_confidence = 0
  135. best_match = None
  136. num_queries = 0
  137. empty = MarkovChain("")
  138. best_chains = (empty, MarkovChainIntersection(empty, empty))
  139. parser = ArticleTextParser(self.get())
  140. clean = parser.strip()
  141. chunks = parser.chunk(self._search_config["nltk_dir"], max_queries)
  142. article_chain = MarkovChain(clean)
  143. last_query = time()
  144. if article_chain.size() < 20: # Auto-fail very small articles
  145. return CopyvioCheckResult(False, best_confidence, best_match,
  146. num_queries, 0, article_chain,
  147. best_chains)
  148. while (chunks and best_confidence < min_confidence and
  149. (max_queries < 0 or num_queries < max_queries)):
  150. chunk = chunks.pop(0)
  151. log = u"[[{0}]] -> querying {1} for {2!r}"
  152. self._logger.debug(log.format(self.title, searcher.name, chunk))
  153. urls = searcher.search(chunk)
  154. urls = [url for url in urls if url not in handled_urls]
  155. for url in urls:
  156. handled_urls.append(url)
  157. if self._exclusions_db:
  158. if self._exclusions_db.check(self.site.name, url):
  159. continue
  160. conf, chns = self._copyvio_compare_content(article_chain, url)
  161. if conf > best_confidence:
  162. best_confidence = conf
  163. best_match = url
  164. best_chains = chns
  165. if time() - start_time > max_time:
  166. break
  167. num_queries += 1
  168. if time() - start_time > max_time:
  169. break
  170. diff = time() - last_query
  171. if diff < interquery_sleep:
  172. sleep(interquery_sleep - diff)
  173. last_query = time()
  174. ctime = time() - start_time
  175. if best_confidence >= min_confidence:
  176. is_violation = True
  177. log = u"Violation detected for [[{0}]] (confidence: {1}; URL: {2}; using {3} queries in {4} seconds)"
  178. self._logger.debug(log.format(self.title, best_confidence,
  179. best_match, num_queries, ctime))
  180. else:
  181. is_violation = False
  182. log = u"No violation for [[{0}]] (confidence: {1}; using {2} queries in {3} seconds)"
  183. self._logger.debug(log.format(self.title, best_confidence,
  184. num_queries, ctime))
  185. return CopyvioCheckResult(is_violation, best_confidence, best_match,
  186. num_queries, ctime, article_chain,
  187. best_chains)
  188. def copyvio_compare(self, url, min_confidence=0.5):
  189. """Check the page like :py:meth:`copyvio_check` against a specific URL.
  190. This is essentially a reduced version of the above - a copyivo
  191. comparison is made using Markov chains and the result is returned in a
  192. :py:class:`~earwigbot.wiki.copyvios.result.CopyvioCheckResult` object -
  193. but without using a search engine, since the suspected "violated" URL
  194. is supplied from the start.
  195. Its primary use is to generate a result when the URL is retrieved from
  196. a cache, like the one used in EarwigBot's Toolserver site. After a
  197. search is done, the resulting URL is stored in a cache for 24 hours so
  198. future checks against that page will not require another set of
  199. time-and-money-consuming search engine queries. However, the comparison
  200. itself (which includes the article's and the source's content) cannot
  201. be stored for data retention reasons, so a fresh comparison is made
  202. using this function.
  203. Since no searching is done, neither
  204. :py:exc:`~earwigbot.exceptions.UnknownSearchEngineError` nor
  205. :py:exc:`~earwigbot.exceptions.SearchQueryError` will be raised.
  206. """
  207. start_time = time()
  208. content = self.get()
  209. clean = ArticleTextParser(content).strip()
  210. article_chain = MarkovChain(clean)
  211. if not url:
  212. empty = MarkovChain("")
  213. chns = (empty, MarkovChainIntersection(empty, empty))
  214. return CopyvioCheckResult(False, 0, url, 0, 0, article_chain, chns)
  215. confidence, chains = self._copyvio_compare_content(article_chain, url)
  216. ctime = time() - start_time
  217. if confidence >= min_confidence:
  218. is_violation = True
  219. log = u"Violation detected for [[{0}]] (confidence: {1}; URL: {2}; {3} seconds)"
  220. self._logger.debug(log.format(self.title, confidence, url, ctime))
  221. else:
  222. is_violation = False
  223. log = u"No violation for [[{0}]] (confidence: {1}; URL: {2}; {3} seconds)"
  224. self._logger.debug(log.format(self.title, confidence, url, ctime))
  225. return CopyvioCheckResult(is_violation, confidence, url, 0, ctime,
  226. article_chain, chains)