Не удается правильно использовать dont_filter = true в spider, чтобы избежать некоторых нежелательных действий

#python #python-3.x #web-scraping #scrapy

#python #python-3.x #очистка веб-страниц #scrapy

Вопрос:

Я создал spider для анализа ссылки из другого контейнера с целевой страницы некоторого идентичного сайта (предоставленной текстовым файлом), а затем использую ссылку для получения заголовка с ее внутренней страницы. Несколько ссылок имеют кнопку «Следующая страница», которую spider обрабатывает соответствующим образом.

Spider анализирует содержимое, но попадает в бесконечный цикл, вызванный dont_filter=True параметром. Если я не использую этот параметр, spider не будет повторно использовать некоторые ссылки, которые изначально не привели к желаемому результату.

Я использовал этот параметр dont_filter=True в трех местах.

  1. В _retry() методе промежуточных программ
  2. В последней строке parse() метода
  3. В последней строке parse_content() метода

созданный мной spider:

 import os
import scrapy
import urllib
from bs4 import BeautifulSoup
from scrapy.crawler import CrawlerProcess


class YelpSpider(scrapy.Spider):
    name = "yelpspidescript"

    with open("all_urls.txt") as f:
        start_urls = f.readlines()
   
    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url,callback=self.parse,meta={"lead_link":url})

    def parse(self,response):
        if response.meta.get("lead_link"):
            lead_link = response.meta.get("lead_link")
        elif response.meta.get("redirect_urls"):
            lead_link = response.meta.get("redirect_urls")[0]

        soup = BeautifulSoup(response.text, 'lxml')
        if soup.select("[class*='hoverable'] h4 a[href^='/biz/'][name]"):
            for item in soup.select("[class*='hoverable'] h4 a[href^='/biz/'][name]"):
                lead_link = response.urljoin(item.get("href"))
                yield scrapy.Request(lead_link,meta={"lead_link":lead_link},callback=self.parse_content)

            next_page = soup.select_one("a[class*='next-link'][href^='/search?']")
            if next_page:
                link = response.urljoin(next_page.get("href"))
                yield scrapy.Request(link,meta={"lead_link":link},callback=self.parse)

        else:
            yield scrapy.Request(lead_link,meta={"lead_link":lead_link},callback=self.parse,dont_filter=True)
            
    def parse_content(self,response):
        if response.meta.get("lead_link"):
            lead_link = response.meta.get("lead_link")
        elif response.meta.get("redirect_urls"):
            lead_link = response.meta.get("redirect_urls")[0]

        soup = BeautifulSoup(response.text, 'lxml')

        if soup.select_one("h1[class*='heading--inline__']"):
            try:
                name = soup.select_one("h1[class*='heading--inline__']").get_text(strip=True)
            except AttributeError: name = ""
            print(name)

        else:
            yield scrapy.Request(lead_link,meta={"lead_link":lead_link},callback=self.parse_content,dont_filter=True)
            

if __name__ == "__main__":
    c = CrawlerProcess({
        'USER_AGENT':'Mozilla/5.0',
        'LOG_LEVEL':'ERROR',
    })
    c.crawl(YelpSpider)
    c.start()
  

промежуточные программы:

 from fake_useragent import UserAgent


RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 403, 401, 400, 404, 408]

class yelp_custom_Middleware(object):
    ua = UserAgent() 

    def process_request(self, request, spider):
        request.headers['User-Agent'] = self.ua.random

    def process_exception(self, request, exception, spider):
        return self._retry(request, exception, spider)

    def _retry(self, request, reason, spider):
        retryreq = request.copy()
        retryreq.dont_filter = True
        return retryreq

    def process_response(self, request, response, spider):
        if request.meta.get('dont_retry', False):
            return response
        if response.status in RETRY_HTTP_CODES:
            reason = response_status_message(response.status)
            return self._retry(request, reason, spider) or response
        return response
  

Как я могу позволить spider не попадать в бесконечный цикл?

Редактировать: я думал, включать несколько URL-адресов, я изо которые находятся в all_urls.txt файле в случае, если это помогает определить проблему лучше.

Комментарии:

1. Как насчет добавления счетчика повторных попыток?

2. Если я соглашусь с вашим предложением, как будет выглядеть модифицированная версия _retry( ) метода @gangabass? Спасибо.

3. @robots.txt можете ли вы лучше объяснить, как происходит бесконечный цикл? Я спрашиваю, потому что, хотя фильтр «выключен», он не должен входить в цикл. Единственные случаи, когда я видел, как это происходит, — это когда во время синтаксического анализа страницы spider находит и выдает запрос на ту же страницу Или когда сервер держит вас в бесконечном цикле перенаправления. Другой вопрос: проблема не связана с запросами, выдаваемыми start_requests (из списка URL), или это также происходит с запросами, выдаваемыми при разборе страниц?

4. Ответ на ваш второй вопрос: происходит с любым случайным URL-адресом, независимо от того, входят ли они в число начальных URL-адресов или они заполнены разными методами @renatodvc. На ваш первый вопрос: я не заметил никакого перенаправления, но я заметил URL-адрес captcha, который скрипт обходит с помощью прокси. Однако суть в том, что я не хочу, чтобы скрипт попадал в бесконечный цикл. Спасибо.

5. Это становится значительно сложнее решить из-за незнания причины возникновения цикла, поскольку это само по себе является аномалией. Считаете ли вы систему, которая ограничивает количество запросов, выполняемых по одному URL, разумным решением?

Ответ №1:

Вы можете подсчитывать повторные попытки для каждого URL:

 class yelp_custom_Middleware(object):
    ua = UserAgent()
    max_retries = 3
    retry_urls = {}

    def process_request(self, request, spider):
        request.headers['User-Agent'] = self.ua.random

    def process_exception(self, request, exception, spider):
        return self._retry(request, exception, spider)

    def _retry(self, request, reason, spider):
        retry_url = request.url
        if retry_url not in self.retry_urls:
            self.retry_urls[retry_url] = 1
        else:
            self.retry_urls[retry_url]  = 1
        
        if self.retry_urls[retry_url] > self.max_retries:
            # Dont' retry
        else:
            # Retry
  

Комментарии:

1. Как только я запускаю скрипт, я получаю эту ошибку retry_url = request.retry AttributeError: 'Request' object has no attribute 'retry' .

2. Извините, у меня глупая опечатка: retry_url = request.url

Ответ №2:

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Это решение является обходным путем к описанной проблеме, когда вы сталкиваетесь с проблемой с бесконечными циклами в запросе, всегда желательно определить причину, а не изменять, как работает фильтр.

  • Создавайте customdupefilter.py на том же уровне, что и вы middlewares.py или settings.py .

     import os
    
    from scrapy.dupefilters import RFPDupeFilter
    
    class CustomDupeFilter(RFPDupeFilter):
        default_max_retries = 5
    
        def __init__(self, path=None, debug=False):
            super().__init__(path, debug)
            self.request_counter = {}
    
        def request_seen(self, request):
            max_retries = request.meta.get('max_retries') or self.default_max_retries
            fp = self.request_fingerprint(request)
            if fp in self.fingerprints:
                if self.request_counter.get(fp) >= max_retries:
                    self.logger.info('Requests to %s exceeded maximum retries.', request)
                    return True
                else:
                    self.request_counter[fp]  = 1
                    return None
            self.fingerprints.add(fp)
            self.request_counter.update({fp: 1})
            if self.file:
                self.file.write(fp   os.linesep)
      

Предполагается, что вы используете стандартную структуру папок проекта scrapy, если вы измените структуру папок или имя файла, просто не забудьте также изменить ссылку в настройках.

  • Добавьте следующую строку в свой settings.py

     DUPEFILTER_CLASS = 'YOUR_PROJECT_NAME.customdupefilter.CustomDupeFilter'
      
  • Если вы используете dont_filter=True в запросе , он обойдет этот фильтр точно так же, как стандартный класс.

  • Вы можете отправить в meta запроса max_retries параметр, чтобы изменить максимальное количество повторных попыток, разрешенных для этого конкретного запроса, если вы этого не сделаете, он будет использоваться default_max_retries . На всякий случай, если для конкретного запроса требуется больше запросов, чем для большинства других. Пример:

     yield Request(url=request_url, meta={'max_retries': 3})
      

Объяснение:

Это просто перезапишет стандартный класс filter RFPDupeFilter и реализует счетчик, чтобы увидеть, сколько раз разрешено повторять один и тот же запрос, как только будет передано максимальное число, он будет фильтроваться точно так же, как и раньше.

Как уже упоминалось, dont_filter этот метод фильтрации будет обходиться.

Комментарии:

1. Когда вы предлагаете создать customdupefilter.py на том же уровне middlewares.py или settings.py , разве это не то, что вы имели в виду? Спасибо.

2. Да, для стандартной структуры проекта scrapy. Вы можете поместить его куда-нибудь еще, если хотите, просто не забудьте обновить DUPEFILTER_CLASS в settings.py , чтобы scrapy мог его импортировать.

3. Поскольку scrapy по умолчанию фильтрует повторяющиеся запросы, когда dont_filter=True его нет, будет ли он выполнять повторяющиеся вызовы, если я использую, meta={'max_retry_times':5} даже когда dont_filter=True @renatodvc отсутствует? Спасибо.

4. При использовании dont_filter=True запрос полностью пропускает фильтр, если не использовать его, запрос пройдет через фильтр. Фильтр будет блокировать запросы, сделанные только после того, как он превысит количество max_retries того же запроса. Обратите внимание, что мета-параметр НЕ является max_retry_times . И да, если вы продублируете запрос, scrapy выполнит их оба ( если под max_retries ), в конце концов, в этом и был смысл. Фильтр не вызовет никаких дубликатов, он просто позволит им произойти (до предела), если вы выдадите дублированные запросы.