Здесь в качестве упражнения разбираем хрестоматийный пример Random proxy middleware for Scrapy и находим ошибки в устаревшем коде. Результат - рабочий код... и и десяток ссылок для его рефаеторинга. Сложный поиск на GitHub не нашел в форках ... того, что удалось найти по запросу process_exception(self, request, exception, spider) proxy
Список прокси из файла загружается в словарь, из которого случайным образом выбирается конкретный прокси, который передается в request.meta['proxy'] каждого запроса. Если запрос после 10 повторений не получает ответа, то адрес прокси-сервера удаляется из словаря. В случае, если if 'proxy' in request.meta уже заполнено (прокси задан системно или в спайдере\краулере), то он не переприсваивается, таким образом, можно комбинировать middleware (и в DOWNLOADER_MIDDLEWARES)
Random proxy middleware for Scrapy
Проксирование в Scrapy
7.2. re — Regular expression operations
Downloader Middleware
scrapy / scrapy / contrib / downloadermiddleware /
Queuelib Queuelib is a collection of persistent (disk-based) queues for Python.
Программирование на Python: Часть 8. Файловая система
Scrapyjs - Scrapy+Javascript integration
testspiders / testspiders / middleware.py
Нa GitHub можно искать прямо в коде process_exception(self, request, exception, spider) proxy
По этому запросу я получил 43 первоклассных примера, которые обязательно надо использовать для рефакторинга
scrapy-proxies fork:true
ikeikeikeike/scrapy-proxies
process_exception(self, request, exception, spider) proxy
books_spider
AnalyzingCode / scraper / scraper / middlewares.py
google_play_crawler / google_play_crawler / middleware / random_proxy.py
Файл проверен на w8 (Windows)¶
#Вот результат работы
%load "C:\\Users\\kiss\\Documents\\GitHub\\dirbot_se1\\dirbot\\randomproxy.py"
import re
import random
import base64
from scrapy import log
# I had to add this for windows
# to open proxy_list = "C:/Users/kiss/Documents/GitHub/dirbot_se1/dirbot/list.txt"
impotr os
#import pdb
class RandomProxy(object):
def __init__(self, settings):
self.proxy_list = settings.get('PROXY_LIST')
fin = open(self.proxy_list)
self.proxies = {}
for line in fin.readlines():
parts = re.match('(\w+://)(\w+:\w+@)?(.+)', line)
pdb.set_trace()
# Cut trailing @
if parts.group(2): # if there are usern:password@
user_pass = parts.group(2)[:-1]
else:
user_pass = ''
self.proxies[parts.group(1) + parts.group(3)] = user_pass
fin.close()
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
def process_request(self, request, spider):
# Don't overwrite with a random one (server-side state for IP)
if 'proxy' in request.meta:
return
proxy_address = random.choice(self.proxies.keys())
proxy_user_pass = self.proxies[proxy_address]
request.meta['proxy'] = proxy_address
if proxy_user_pass:
basic_auth = 'Basic ' + base64.encodestring(proxy_user_pass)
request.headers['Proxy-Authorization'] = basic_auth
def process_exception(self, request, exception, spider):
proxy = request.meta['proxy']
log.msg('Removing failed proxy <%s>, %d proxies left' % (
proxy, len(self.proxies)))
try:
self.proxies.pop(proxy)
except ValueError:
pass
Далее записан процесс изучения оригинала %load "C:\Users\kiss\Documents\GitHub\dirbot_se1\dirbot\randomproxy.py"¶
# Вот оригинал (то, что было)
%load "C:\\Users\\kiss\\Documents\\GitHub\\dirbot_se1\\dirbot\\randomproxy_bak.py"
import re
import random
import base64
from scrapy import log
# debugger
import pdb
class RandomProxy(object):
def __init__(self, settings):
self.proxy_list = settings.get('PROXY_LIST')
fin = open(self.proxy_list)
self.proxies = {}
for line in fin.readlines():
parts = re.match('(\w+://)(\w+:\w+@)?(.+)', line)
pdb.set_trace() # This is after my previous attemt to debug this
# Cut trailing @
if parts[1]:
parts[1] = parts[1][:-1]
self.proxies[parts[0] + parts[2]] = parts[1]
fin.close()
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
def process_request(self, request, spider):
# Don't overwrite with a random one (server-side state for IP)
if 'proxy' in request.meta:
return
proxy_address = random.choice(self.proxies.keys())
proxy_user_pass = self.proxies[proxy_address]
request.meta['proxy'] = proxy_address
if proxy_user_pass:
basic_auth = 'Basic ' + base64.encodestring(proxy_user_pass)
request.headers['Proxy-Authorization'] = basic_auth
def process_exception(self, request, exception, spider):
proxy = request.meta['proxy']
log.msg('Removing failed proxy <%s>, %d proxies left' % (
proxy, len(self.proxies)))
try:
del self.proxies[proxy]
except ValueError:
pass
Конструктор класса открывает текстовый файл, считывает строки и производит над ними непонятные манипуляции¶
Открываем файл PROXY_LIST, считываем строки, заполняем словарь self.proxies... А что там и как выбирается, сейчас посмотрим
import re
str_1 ='http://127.0.0.1:8080'
Теперь попробуем регулярное выражение из класса¶
parts_e = re.match('(\w+://)(\w+:\w+@)?(.+)', str_1)
В кавычках просмтриваются три группы
\w Matches any alphanumeric character; equivalent to [a-zA-Z0-9_]
"+" Matches 1 or more (greedy) repetitions of the preceding RE
(\w+://) - just part of string ( http:// or ftp://)
"?" Matches 0 or 1 (greedy) of the preceding RE.
*?,+?,?? Non-greedy versions of the previous three special characters.
"." Matches any character except a newline
(\w+:\w+@)? - username:passw@ or 0
(.+) - any string with length not less than 1 characters
parts_e.groups()
help(re)
В первоисточнике устаревший код, он не рабтает..., но чтобы заменить parts_e[2] на parts_e.group(2) надо понять, что собственно, этот код делал¶
parts_e[0],parts_e[1],parts_e[2]
parts_e.group(0), parts_e.group(1), parts_e.group(2), parts_e.group(3)
Вот особеености свойства group(1) - под номером (0) - исходная строка, таким образом нумерация частей начинается с (1) Вспомним про особенночти в старых индексах [] ... Не помню, но полагаю, что там все начинается с [0] Тогда, если предположить соответсвтвие [0] - group(1), [1]- group(2), [2]-group(3)
if parts[1]:
parts[1] = parts[1][:-1]
self.proxies[parts[0] + parts[2]] = parts[1]
parts_e.group(1,3)
parts_e.group(1), parts_e.group(1)[:-1]
parts_e.group(1) + parts_e.group(3)
# Cut trailing @
if parts.group(2): # если есть авторизация на прокси сервере
user_pass = parts.group(2)[:-1]
else:
user_pass = ''
self.proxies[parts.group(1) + parts.group(3)] = user_pass
%load C:/Users/kiss/Documents/GitHub/dirbot_se1/dirbot/list.txt
http://94.180.118.34:8080
http://213.141.146.146:8080
http://218.108.232.93:80
http://54.85.145.16:3128
Далее я пытался сообразить, почему не открывается файл¶
Соображал долго (читал справку..., потом искал в инете...), наконец дошло, что (когда набирал запрос windows), что надо попробовать импортировать OS. Действительно, Питону все эти абсолютные пути не шибко понятны...
import os
proxy_list = "C:/Users/kiss/Documents/GitHub/dirbot_se1/dirbot/list.txt"
fin = open(proxy_list)
proxies = {}
fin.name
fin.readlines()
help(open)
file.__doc__
for line in fin.readlines():
print line
parts = re.match('(\w+://)(\w+:\w+@)?(.+)', line)
#pdb.set_trace() # This is after my previous attemt to debug this
# Cut trailing @
if parts.group(2): # если есть авторизация на прокси сервере
user_pass = parts.group(2)[:-1]
else:
user_pass = ''
proxies[parts.group(1) + parts.group(3)] = user_pass
fin.close()
parts.group(1),parts.group(2),parts.group(3)
Вот отсюда я отправился разиратся, почему не читается файл Не должен быть словарь пустым....
.keys()
#А теперь я перезапустил код и вижу, что по умолчанию режим чтения....
fin.mode
proxies
Почему понадобился такой странный словарь? Строка : пароль ... Где юзер??? Пока не будем на это обращат внимание, просто порадуемся, что восстановили словарь...
help(proxies)
proxies.keys()
proxies.keys()[0]
del proxies[proxy]
proxies.items()[1]
Теперь попробуем стереть часть словаря, но код из randomproxy.py не работает:
del proxies.items()[1]
Как же заменить убрать запись в словаре?¶
Далее я сделал ошибку, надо было попробовать, как в первоисточнике, а я начал изобретать велосипед...
Я нашел .pop() и .popitem() методы, иллюстрация внизу:
proxies.items()
proxies.pop('http://94.180.118.34:8080')
proxies.items()
proxies.popitem()[1]
proxies.items()
Можно стереть запись задав значение по ключу, можно по номеру записи в словаре.
Зачем здесь @classmethod¶
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
Очевидно, для того, чтобы получить настройки из краулера (там, кстати, тоже можно умтановить прокси...). Но вот, как этот метод вызывается я пока не понимаю. В Лутце (стр. 894) в примере со счетчиков вызовов класса написано:
Интересно отметить, что аналогичные действия можно реализовать с помощью метода класса – следующий класс обладает тем же поведением, что и класс со статическим методом, представленный выше, но в нем используется метод класса, который в первом аргументе принимает класс экземпляра. Методы класса автоматически получают объект класса:
class Spam:
numInstances = 0 # Вместо статического метода используется метод класса
def __init__(self):
Spam.numInstances += 1
def printNumInstances(cls):
print(“Number of instances:”, cls.numInstances)
printNumInstances = classmethod(printNumInstances)
Используется этот класс точно так же, как и предыдущая версия, но его метод printNumInstances принимает объект класса, а не экземпляра, независимо от того, вызывается он через имя класса или через экземпляр
В настоящее время статические методы, к примеру, могут быть оформлены в виде декораторов, как показано ниже:
class C:
@staticmethod # Синтаксис декорирования
def meth():
...
С технической точки зрения, это объявление имеет тот же эффект, что и фрагмент ниже (передача функции декоратору и присваивание результата первоначальному имени функции):
class C:
def meth():
...
meth = staticmethod(meth) # Повторное присваивание имени
Результат, возвращаемый функцией-декоратором, повторно присваивается имени метода. В результате вызов метода по имени функции фактически будет приводить к вызову результата, полученному от декоратора staticmethod.
def process_request - это обязятельный метод Writing your own downloader middleware¶
Этот метод и вызывается в загрузчике This method is called for each request that goes through the download middleware
Проверим, работают ли функции
import random
random.choice(proxies.keys()),random.choice(proxies.keys()), random.choice(proxies.keys())
C:\Users\kiss\Documents\GitHub\dirbot_se1>scrapy crawl dmoz
2014-10-21 21:02:11+0400 [scrapy] INFO: Scrapy 0.20.1 started (bot: scrapybot)
2014-10-21 21:02:11+0400 [scrapy] DEBUG: Optional features available: ssl, http11, boto, django
2014-10-21 21:02:11+0400 [scrapy] DEBUG: Overridden settings: {'DEFAULT_ITEM_CLASS': 'dirbot.items.Website', 'NEWSPIDER_MODULE': 'di
rbot.spiders', 'SPIDER_MODULES': ['dirbot.spiders'], 'RETRY_TIMES': 10, 'RETRY_HTTP_CODES': [500, 503, 504, 400, 403, 404, 408]}
2014-10-21 21:02:13+0400 [scrapy] DEBUG: Enabled extensions: LogStats, TelnetConsole, CloseSpider, WebService, CoreStats, SpiderStat
e
2014-10-21 21:02:14+0400 [scrapy] DEBUG: Enabled downloader middlewares: RetryMiddleware, RandomProxy, HttpAuthMiddleware, DownloadT
imeoutMiddleware, UserAgentMiddleware, DefaultHeadersMiddleware, MetaRefreshMiddleware, HttpCompressionMiddleware, RedirectMiddlewar
e, CookiesMiddleware, ChunkedTransferMiddleware, DownloaderStats
2014-10-21 21:02:14+0400 [scrapy] DEBUG: Enabled spider middlewares: HttpErrorMiddleware, OffsiteMiddleware, RefererMiddleware, UrlL
engthMiddleware, DepthMiddleware
C:\Users\kiss\Anaconda\lib\site-packages\scrapy\contrib\pipeline\__init__.py:21: ScrapyDeprecationWarning: ITEM_PIPELINES defined as
a list or a set is deprecated, switch to a dict
category=ScrapyDeprecationWarning, stacklevel=1)
2014-10-21 21:02:14+0400 [scrapy] DEBUG: Enabled item pipelines: FilterWordsPipeline
2014-10-21 21:02:14+0400 [dmoz] INFO: Spider opened
2014-10-21 21:02:14+0400 [dmoz] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2014-10-21 21:02:14+0400 [scrapy] DEBUG: Telnet console listening on 0.0.0.0:6031
2014-10-21 21:02:14+0400 [scrapy] DEBUG: Web service listening on 0.0.0.0:6080
2014-10-21 21:02:35+0400 [scrapy] INFO: Removing failed proxy <http://218.108.232.93:80>, 4 proxies left
2014-10-21 21:02:35+0400 [dmoz] DEBUG: Retrying <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/> (failed 1 ti
mes): TCP connection timed out: 10060: ╧юя√Єър єёЄрэютшЄ№ ёюхфшэхэшх с√ыр схчєёях°эющ, Є.ъ. юЄ фЁєуюую ъюья№■ЄхЁр чр ЄЁхсєхьюх тЁхь
эх яюыєўхэ эєцэ√щ юЄъышъ, шыш с√ыю ЁрчюЁтрэю єцх єёЄрэютыхээюх ёюхфшэхэшх шч-чр эхтхЁэюую юЄъышър єцх яюфъы■ўхээюую ъюья№■ЄхЁр..
2014-10-21 21:02:35+0400 [scrapy] INFO: Removing failed proxy <http://218.108.232.93:80>, 3 proxies left
2014-10-21 21:02:35+0400 [dmoz] ERROR: Error downloading <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/>
Traceback (most recent call last):
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 490, in _startRunCallbacks
self._runCallbacks()
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 577, in _runCallbacks
current.result = callback(current.result, *args, **kw)
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 423, in errback
self._startRunCallbacks(fail)
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 490, in _startRunCallbacks
self._runCallbacks()
--- <exception caught here> ---
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 577, in _runCallbacks
current.result = callback(current.result, *args, **kw)
File "C:\Users\kiss\Anaconda\lib\site-packages\scrapy\core\downloader\middleware.py", line 57, in process_exception
response = method(request=request, exception=exception, spider=spider)
File "dirbot\randomproxy.py", line 51, in process_exception
self.proxies.pop(proxy)
exceptions.KeyError: 'http://218.108.232.93:80'
2014-10-21 21:02:56+0400 [scrapy] INFO: Removing failed proxy <http://218.108.232.93:80>, 3 proxies left
2014-10-21 21:02:56+0400 [dmoz] ERROR: Error downloading <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/>
Traceback (most recent call last):
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 490, in _startRunCallbacks
self._runCallbacks()
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 577, in _runCallbacks
current.result = callback(current.result, *args, **kw)
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 423, in errback
self._startRunCallbacks(fail)
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 490, in _startRunCallbacks
self._runCallbacks()
--- <exception caught here> ---
File "C:\Users\kiss\Anaconda\lib\site-packages\twisted\internet\defer.py", line 577, in _runCallbacks
current.result = callback(current.result, *args, **kw)
File "C:\Users\kiss\Anaconda\lib\site-packages\scrapy\core\downloader\middleware.py", line 57, in process_exception
response = method(request=request, exception=exception, spider=spider)
File "dirbot\randomproxy.py", line 51, in process_exception
self.proxies.pop(proxy)
exceptions.KeyError: 'http://218.108.232.93:80'
2014-10-21 21:02:56+0400 [dmoz] INFO: Closing spider (finished)
2014-10-21 21:02:56+0400 [dmoz] INFO: Dumping Scrapy stats:
{'downloader/exception_count': 3,
'downloader/exception_type_count/twisted.internet.error.TCPTimedOutError': 3,
'downloader/request_bytes': 793,
'downloader/request_count': 3,
'downloader/request_method_count/GET': 3,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2014, 10, 21, 17, 2, 56, 480000),
'log_count/DEBUG': 7,
'log_count/ERROR': 2,
'log_count/INFO': 6,
'scheduler/dequeued': 3,
'scheduler/dequeued/memory': 3,
'scheduler/enqueued': 3,
'scheduler/enqueued/memory': 3,
'start_time': datetime.datetime(2014, 10, 21, 17, 2, 14, 361000)}
2014-10-21 21:02:56+0400 [dmoz] INFO: Spider closed (finished)
C:\Users\kiss\Documents\GitHub\dirbot_se1>
Видим, что строчка Removing failed proxy http://218.108.232.93:80, 3 proxies left посторяется три раза, сразу пробуем "тупо откатить" (прежде, чем тратить время на понимание ошибки):
proxies
proxies['http://218.108.232.93:80']
Вот так выглядит первоначальный код, он выполняется..., а я выыше грубо ошибся, посколькупрбовал **del proxies.items()[1]**
del proxies['http://218.108.232.93:80']
proxies
Пробуем изменить строчку на первоначальную и опять получаем те же ошибки, пора разобраться с командой del ... Ответ находим в документации 2. Built-in Functions
delattr(object, name)
This is a relative of setattr(). The arguments are an object and a string.
The string must be the name of one of the object’s attributes.
The function deletes the named attribute, provided the object allows it.
For example, delattr(x, 'foobar') is equivalent to del x.foobar.
Посты чуть ниже также могут вас заинтересовать
Комментариев нет:
Отправить комментарий