A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

267 行
10 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net>
  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 collections import defaultdict
  23. from functools import partial
  24. from gzip import GzipFile
  25. from json import loads
  26. from StringIO import StringIO
  27. from time import sleep, time
  28. from urllib import quote_plus, urlencode
  29. from urllib2 import build_opener, URLError
  30. try:
  31. import oauth2 as oauth
  32. except ImportError:
  33. oauth = None
  34. from earwigbot.wiki.exceptions import *
  35. class _CopyvioCheckResult(object):
  36. def __init__(self, violation, confidence, url, queries, article, chains):
  37. self.violation = violation
  38. self.confidence = confidence
  39. self.url = url
  40. self.queries = queries
  41. self.article_chain = article
  42. self.source_chain = chains[0]
  43. self.delta_chain = chains[1]
  44. def __repr__(self):
  45. r = ", ".join(("_CopyvioCheckResult(violation={0!r}",
  46. "confidence={1!r}", "url={2!r}", "queries={3|r}",
  47. "article={4|r}", "chains={5!r})"))
  48. return r.format(self.violation, self.confidence, self.url,
  49. self.queries, self.article_chain,
  50. (self.source_chain, self.delta_chain))
  51. class _MarkovChain(object):
  52. START = -1
  53. END = -2
  54. def __init__(self, text):
  55. self.text = text
  56. self.chain = defaultdict(lambda: defaultdict(lambda: 0))
  57. words = text.split()
  58. prev = self.START
  59. for word in words:
  60. self.chain[prev][word] += 1
  61. prev = word
  62. try: # This won't work if the source text is completely blank
  63. self.chain[word][self.END] += 1
  64. except KeyError:
  65. pass
  66. def size(self):
  67. count = 0
  68. for node in self.chain.itervalues():
  69. for hits in node.itervalues():
  70. count += hits
  71. return count
  72. class _MarkovChainIntersection(_MarkovChain):
  73. def __init__(self, mc1, mc2):
  74. self.chain = defaultdict(lambda: defaultdict(lambda: 0))
  75. c1 = mc1.chain
  76. c2 = mc2.chain
  77. for word, nodes1 in c1.iteritems():
  78. if word in c2:
  79. nodes2 = c2[word]
  80. for node, count1 in nodes1.iteritems():
  81. if node in nodes2:
  82. count2 = nodes2[node]
  83. self.chain[word][node] = min(count1, count2)
  84. class CopyrightMixin(object):
  85. """
  86. EarwigBot's Wiki Toolset: Copyright Violation Mixin
  87. This is a mixin that provides one public method, copyvio_check(), which
  88. checks the page for copyright violations using a search engine API. The
  89. API keys must be provided to the method as arguments.
  90. """
  91. def __init__(self, site):
  92. self._opener = build_opener()
  93. self._opener.addheaders = site._opener.addheaders
  94. def _open_url_ignoring_errors(self, url):
  95. """Open a URL using self._opener and return its content, or None.
  96. Will decompress the content if the headers contain "gzip" as its
  97. content encoding, and will return None if URLError is raised while
  98. opening the URL. IOErrors while gunzipping a compressed response are
  99. ignored, and the original content is returned.
  100. """
  101. try:
  102. response = self._opener.open(url)
  103. except URLError:
  104. return None
  105. result = response.read()
  106. if response.headers.get("Content-Encoding") == "gzip":
  107. stream = StringIO(result)
  108. gzipper = GzipFile(fileobj=stream)
  109. try:
  110. result = gzipper.read()
  111. except IOError:
  112. pass
  113. return result
  114. def _select_search_engine(self, engine, credentials):
  115. """Return a function that can be called to do web searches.
  116. The "function" is a functools.partial object that takes one argument, a
  117. query, and returns a list of URLs, ranked by importance. The underlying
  118. logic depends on the 'engine' argument; for example, if 'engine' is
  119. "Yahoo! BOSS", we'll use self._yahoo_boss_query for querying.
  120. Raises UnknownSearchEngineError if 'engine' is not known to us, and
  121. UnsupportedSearchEngineError if we are missing a required package or
  122. module, like oauth2 for "Yahoo! BOSS".
  123. """
  124. if engine == "Yahoo! BOSS":
  125. if not oauth:
  126. e = "The package 'oauth2' could not be imported"
  127. raise UnsupportedSearchEngineError(e)
  128. searcher = self._yahoo_boss_query
  129. else:
  130. raise UnknownSearchEngineError(engine)
  131. return partial(searcher, credentials)
  132. def _yahoo_boss_query(self, cred, query):
  133. """Do a Yahoo! BOSS web search for 'query' using 'cred' as credentials.
  134. Returns a list of URLs, no more than fifty, ranked by relevance (as
  135. determined by Yahoo). Raises SearchQueryError() on errors.
  136. """
  137. base_url = "http://yboss.yahooapis.com/ysearch/web"
  138. query = quote_plus(query.join('"', '"'))
  139. params = {"q": query, "style": "raw", "format": "json"}
  140. url = "{0}?{1}".format(base_url, urlencode(params))
  141. consumer = oauth.Consumer(key=cred["key"], secret=cred["secret"])
  142. client = oauth.Client(consumer)
  143. headers, body = client.request(url, "GET")
  144. if headers["status"] != "200":
  145. e = "Yahoo! BOSS Error: got response code '{0}':\n{1}'"
  146. raise SearchQueryError(e.format(headers["status"], body))
  147. try:
  148. res = loads(body)
  149. except ValueError:
  150. e = "Yahoo! BOSS Error: JSON could not be decoded"
  151. raise SearchQueryError(e)
  152. try:
  153. results = res["bossresponse"]["web"]["results"]
  154. except KeyError:
  155. return []
  156. return [result["url"] for result in results]
  157. def _copyvio_strip_html(self, html):
  158. return html
  159. def _copyvio_strip_article(self, content):
  160. return content
  161. def _copyvio_chunk_article(self, content, max_chunks):
  162. return [content]
  163. def _copyvio_compare_content(self, article, url):
  164. html = self._open_url_ignoring_errors(url)
  165. if not html:
  166. return 0
  167. source = _MarkovChain(self._copyvio_strip_html(html))
  168. delta = _MarkovChainIntersection(article, source)
  169. return delta.size() / article.size(), (source, delta)
  170. def copyvio_check(self, engine, credentials, min_confidence=0.5,
  171. max_queries=-1, interquery_sleep=1, force=False):
  172. """Check the page for copyright violations.
  173. Returns a _CopyvioCheckResult object with four useful attributes:
  174. "violation", "confidence", "url", and "queries". "confidence" is a
  175. number between 0 and 1; if it is less than "min_confidence", we could
  176. not find any indication of a violation (so "violation" will be False
  177. and "url" may or may not be None), otherwise it indicates the relative
  178. faith in our results, "violation" will be True, and "url" will be the
  179. place the article is suspected of being copied from. "queries" is the
  180. number of queries used to determine the results.
  181. "max_queries" is self-explanatory; we will never make more than this
  182. number of queries in a given check. If it's less than 0, we will not
  183. limit our number of queries.
  184. "interquery_sleep" is the minimum amount of time we will sleep between
  185. search engine queries, in seconds.
  186. "force" is simply passed to page.get() - it has the same behavior there
  187. as it does here.
  188. Raises CopyvioCheckError or subclasses (UnknownSearchEngineError,
  189. SearchQueryError, ...) on errors.
  190. """
  191. search = self._select_search_engine(engine, credentials)
  192. handled_urls = []
  193. best_confidence = 0
  194. best_match = None
  195. num_queries = 0
  196. empty = _MarkovChain("")
  197. best_chains = (empty, _MarkovChainIntersection(empty, empty))
  198. content = self.get(force)
  199. clean = self._copyvio_strip_article(content)
  200. chunks = self._copyvio_chunk_article(clean, max_queries)
  201. article_chain = _MarkovChain(clean)
  202. last_query = time()
  203. while (chunks and best_confidence < min_confidence and
  204. (max_queries < 0 or num_queries < max_queries)):
  205. urls = search(chunks.pop(0))
  206. urls = [url for url in urls if url not in handled_urls]
  207. for url in urls:
  208. handled_urls.append(url)
  209. conf, chains = self._copyvio_compare_content(article_chain, url)
  210. if conf > best_confidence:
  211. best_confidence = conf
  212. best_match = url
  213. best_chains = chains
  214. num_queries += 1
  215. diff = time() - last_query
  216. if diff < interquery_sleep:
  217. sleep(interquery_sleep - diff)
  218. last_query = time()
  219. if best_confidence >= min_confidence: # violation?
  220. v = True
  221. else:
  222. v = False
  223. return _CopyvioCheckResult(v, best_confidence, best_match, num_queries,
  224. article_chain, best_chains)