- Одного перечня неработающих ссылок недостаточно; важно знать, на каких страницах сайта находятся эти ссылки, чтобы можно было внести исправления.
- Обход сайта и проверка ссылок в первой версии -- фактически, одно и то же. Значит, нет возможности проверить работоспособность внешних ссылок и вообще всего, что отбрасывается фильтром is_valid_link.
HEAD-запрос
Сделать HEAD-запрос в Python-е проще всего средствами httplib. Библиотека urllib2 это тоже позволяет, но придется писать много лишнего кода. Функция, представленная ниже, возвращает код ответа на HTTP-запрос или 0, если соединение не состоялось. Этого достаточно, чтобы узнать, жива ли страница.from urlparse import urlparse
import httplib
def request_head(url):
parts = urlparse(url)
conn = None
try:
conn = httplib.HTTPConnection(parts.netloc)
conn.request('HEAD', parts.path, parts.params)
return conn.getresponse().status
except:
return 0
finally:
if conn:
conn.close()
import httplib
def request_head(url):
parts = urlparse(url)
conn = None
try:
conn = httplib.HTTPConnection(parts.netloc)
conn.request('HEAD', parts.path, parts.params)
return conn.getresponse().status
except:
return 0
finally:
if conn:
conn.close()
# если при попытке открыть ссылку возвращается один из этих кодов,
# ссылка считается неработающей
BAD_CODES = (301, 303, 307, 404, 410, 500, 501, 502, 503, 504)
for link in links_iterator(response, is_http_link ):
status = passed.setdefault(
link,
request_head(link)
)
if not status or status in BAD_CODES:
print '%s --> %s: %s ' % (url, hostname, status)
...
Проблема в том, что после парсинга страницы внутри callback-метода test_links из файло-подобного объекта (file-like object), который возвращает метод opener.open(), уже невозможно ничего прочесть. Первая идея, приходящая в голову: применить метод файла seek(0), чтобы вернуться в начало. Однако, вызов response.seek(0) ни к чему ни приводит, хотя Python не ругается, как непременно сделала бы Java.
Утка или не утка?
В динамических языках любят идиому: "Если нечто ходит, как утка -- значит, это утка". Именно так устроено в питоне все, что называется "file-like object", в том числе, результат urllib2.Request.open(). Однако, метод seek не работает. Если задуматься, нет ничего удивительного в том, что сокет работает иначе, чем файл. Другой вопрос: хорошо это или плохо когда нечто, что называется уткой, ходит, как положено утке, но отказывается нести яйца -- не лучше бы ее тогда назвать как-то иначе? Эта тема уже обсужалась в списке рассылки python-bugs-list. Там же был предложен рецепт: как все-таки заставить файло-подобный объект одноразового использования перематываться. Для этого достаточно прочесть данные из потока и "завернуть" их в StringIO, чтобы получить своего рода виртуальный файл.import StringIO
response = self.open(url)
data = StringIO.StringIO(response.read())
response = self.open(url)
data = StringIO.StringIO(response.read())
Большая стирка
Теперь seek работает, но вместе с response исчезли и его дополнительные методы, такие как info() и geturl(). Заголовки HTTP-ответа, которые можно было получить через response.info(), уже недоступны вне traverse, поскольку в функцию обратного вызова on_success передается другой объект -- StringIO. Придется изменить функцию обратного вызова, добавив туда новый аргумент:on_success(url, response.info(), data)
def links_iterator(base, response, link_filter=None):
"""
Итератор по ссылкам, найденным в документе.
Аргументы:
base -- URL страницы, с которой делается запрос
response -- поток ввода
filter -- функция, которая может быть использована для
отбора нужных ссылок. На входе: url, на выходе
True, если проверка прошла, иначе -- False
Если параметр 'filter' не задан, итератор возвращает
все найденные ссылки.
"""
...
"""
Итератор по ссылкам, найденным в документе.
Аргументы:
base -- URL страницы, с которой делается запрос
response -- поток ввода
filter -- функция, которая может быть использована для
отбора нужных ссылок. На входе: url, на выходе
True, если проверка прошла, иначе -- False
Если параметр 'filter' не задан, итератор возвращает
все найденные ссылки.
"""
...
def traverse(self, start_url, links_filter=None, on_success=None, on_failure=None):
"""
Обход сети.
start_url -- исходный адрес
links_filter -- функция для оценки очередной ссылки, полученной со
страницы. При результате False не включается в очередь.
on_success -- callback-функция, которая вызывается при успешном
открытии страницы с аргументами (url, response)
on_failure -- callback-функция, которая вызывается в случае неудачи
с аргументами: (url, exception)
"""
queue = [ start_url ]
passed = set()
last_url = None
while queue:
logging.debug('Queue size: %d, Passed: %d ' % \
(len(queue), len(passed)) )
url = queue.pop(0)
try:
if last_url:
r = self.open(url, {'Referer': last_url})
else:
r = self.open(url)
data = StringIO(r.read())
logging.debug('Success')
if on_success:
on_success(url, r.info(), data)
# извлекаем со страницы новые ссылки и добавляем их в очередь
data.seek(0)
new_links = [
u for u in links_iterator(
url,
data,
lambda u: False if (u in passed or u in queue) \
else links_filter(u)
)
]
queue.extend(new_links)
logging.debug('Added %d new links' % len(new_links))
except Exception, ex:
logging.warn('Failure: %s' % ex)
if on_failure:
on_failure(url, ex)
finally:
last_url = url
passed.add(url)
logging.debug('Crawling completed. %d pages passed' % len(passed))
"""
Обход сети.
start_url -- исходный адрес
links_filter -- функция для оценки очередной ссылки, полученной со
страницы. При результате False не включается в очередь.
on_success -- callback-функция, которая вызывается при успешном
открытии страницы с аргументами (url, response)
on_failure -- callback-функция, которая вызывается в случае неудачи
с аргументами: (url, exception)
"""
queue = [ start_url ]
passed = set()
last_url = None
while queue:
logging.debug('Queue size: %d, Passed: %d ' % \
(len(queue), len(passed)) )
url = queue.pop(0)
try:
if last_url:
r = self.open(url, {'Referer': last_url})
else:
r = self.open(url)
data = StringIO(r.read())
logging.debug('Success')
if on_success:
on_success(url, r.info(), data)
# извлекаем со страницы новые ссылки и добавляем их в очередь
data.seek(0)
new_links = [
u for u in links_iterator(
url,
data,
lambda u: False if (u in passed or u in queue) \
else links_filter(u)
)
]
queue.extend(new_links)
logging.debug('Added %d new links' % len(new_links))
except Exception, ex:
logging.warn('Failure: %s' % ex)
if on_failure:
on_failure(url, ex)
finally:
last_url = url
passed.add(url)
logging.debug('Crawling completed. %d pages passed' % len(passed))
- В очередной запрос, начиная со второго, добавляется заголовок 'Referer' для имитации поведения браузера. Некорые сайты проверяют его.
- При извлечении ссылок со страницы вначале проверяется, не пройден ли уже адрес и нет ли его в очереди заданий и только потом, если эти условия выполняются, вызывается функция link_filter. Раньше проверка происходила в другом порядке. Поскольку неизвестно заранее, сколько ресурсов потребуются на фильтрацию, лучше сразу отсекать лишнее и не дергать link_filter лишний раз.
- Добавился блок finally.
Комментариев нет:
Отправить комментарий