Как проанализировать несколько дочерних страниц, объединить / добавить и перейти вверх на родительский уровень? - PullRequest
1 голос
/ 03 мая 2019

Это мой первый скрап-проект - и по общему признанию, одно из моих первых упражнений с питоном.Я ищу способ очистить несколько дочерних страниц, объединить / добавить содержимое в одно значение и передать данные BACK / UP исходной родительской странице.Число дочерних страниц на родительскую страницу также является переменным - оно может быть всего 1, но никогда не будет 0 (возможно, полезно для обработки ошибок?).Кроме того, дочерние страницы могут повторяться и появляться вновь, поскольку они НЕ являются эксклюзивными для одного из родителей.Мне удалось передать метаданные родительской страницы DOWN на соответствующие дочерние страницы, но я остался в тупике при выполнении обратного.

Вот пример структуры страницы:

Top Level Domain
     - Pagination/Index Page #1 (parse recipe links)
          - Recipe #1 (select info & parse ingredient links)
               - Ingredient #1 (select info)
               - Ingredient #2 (select info)
               - Ingredient #3 (select info)
          - Recipe #2
               - Ingredient #1
          - Recipe #3
               - Ingredient #1
               - Ingredient #2
     - Pagination/Index Page #2
          - Recipe #N
               - Ingredient #N
               - ...
     - Pagination/Index Page #3
     - ... continued

Вывод, который я ищу (по рецепту) выглядит примерно так:

{
"recipe_title": "Gin & Tonic",
"recipe_posted_date": "May 2, 2019",
"recipe_url": "www.XYZ.com/gandt.html",
"recipe_instructions": "<block of text here>",
"recipe_ingredients": ["gin", "tonic water", "lime wedge"],
"recipe_calorie_total": "135 calories",
"recipe_calorie_list": ["60 calories", "70 calories", "5 calories"]
}

Я извлекаю URL каждого ингредиента со страницы соответствующего рецепта.Мне нужно извлечь количество калорий из каждой страницы ингредиента, объединить его с количеством калорий других ингредиентов и в идеале получить один элемент.Поскольку один ингредиент не является эксклюзивным для одного рецепта, мне нужно иметь возможность повторно посетить страницу с ингредиентами позже в моем сканировании.

(примечание - это не реальный пример, поскольку количество калорий, очевидно, варьируется в зависимости отпо объему, необходимому для рецепта)

Мой опубликованный код приближает меня к тому, что я ищу, но я должен представить, что есть более изящный способ решения проблемы.Размещенный код успешно передает ВНИЗ метаданных рецепта на уровень ингредиентов, просматривает ингредиенты и добавляет количество калорий.Поскольку информация передается, я уступаю на уровне ингредиентов и создаю несколько дубликатов рецептов (по одному на ингредиент), пока не переберу последний ингредиент.На этом этапе я собираюсь добавить индекс индекса ингредиента, чтобы каким-то образом сохранить запись с наибольшим индексом ингредиента # на URL рецепта.Прежде чем я добрался до этого момента, я решил обратиться к профессионалам за советом.

Код скребка:

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from recipe_scraper.items import RecipeItem

class RecipeSpider(CrawlSpider):
    name = 'Recipe'
    allowed_domains = ['www.example.com']
    start_urls = ['https://www.example.com/recipes/']
    rules = (
        Rule(
            LinkExtractor(
                allow=()
                ,restrict_css=('.pagination')
                ,unique=True
            )
            ,callback='parse_index_page'
            ,follow=True
        ),
    )

def parse_index_page(self, response):
    print('Processing Index Page.. ' + response.url)
    index_url = response.url
    recipe_urls = response.css('.recipe > a::attr(href)').getall()
    for a in recipe_urls:
        request = scrapy.Request(a, callback=self.parse_recipe_page)
        request.meta['index_url'] = index_url
        yield request

def parse_recipe_page(self, response):
    print('Processing Recipe Page.. ' + response.url)
    Recipe_url = response.url
    Recipe_title = response.css('.Recipe_title::text').extract()[0]
    Recipe_posted_date = response.css('.Recipe_posted_date::text').extract()[0]
    Recipe_instructions = response.css('.Recipe_instructions::text').extract()[0]
    Recipe_ingredients = response.xpath('//ul[@class="ingredients"]//li[@class="ingredient"]/a/text()').getall()
    Recipe_ingredient_urls = response.xpath('//ul[@class="ingredients"]//li[@class="ingredient"]/a/@href').getall()
    Recipe_calorie_list_append = []
    Recipe_calorie_list = []
    Recipe_calorie_total = []
    Recipe_item = RecipeItem()
    Recipe_item['index_url'] = response.meta["index_url"]
    Recipe_item['Recipe_url'] = Recipe_url
    Recipe_item['Recipe_title'] = Recipe_title
    Recipe_item['Recipe_posted_date'] = Recipe_posted_date
    Recipe_item['Recipe_instructions'] = Recipe_instructions
    Recipe_item['Recipe_ingredients'] = Recipe_ingredients
    Recipe_item['Recipe_ingredient_urls'] = Recipe_ingredient_urls
    Recipe_item['Recipe_ingredient_url_count'] = len(Recipe_ingredient_urls)
    Recipe_calorie_list.clear()
    Recipe_ingredient_url_index = 0
    while Recipe_ingredient_url_index < len(Recipe_ingredient_urls):
        ingredient_request = scrapy.Request(Recipe_ingredient_urls[Recipe_ingredient_url_index], callback=self.parse_ingredient_page, dont_filter=True)
        ingredient_request.meta['Recipe_item'] = Recipe_item
        ingredient_request.meta['Recipe_calorie_list'] = Recipe_calorie_list
        yield ingredient_request
        Recipe_calorie_list_append.append(Recipe_calorie_list)
        Recipe_ingredient_url_index += 1

def parse_ingredient_page(self, response):
    print('Processing Ingredient Page.. ' + response.url)
    Recipe_item = response.meta['Recipe_item']
    Recipe_calorie_list = response.meta["Recipe_calorie_list"]
    ingredient_url = response.url
    ingredient_calorie_total = response.css('div.calorie::text').getall()
    Recipe_calorie_list.append(ingredient_calorie_total)
    Recipe_item['Recipe_calorie_list'] = Recipe_calorie_list
    yield Recipe_item
    Recipe_calorie_list.clear()

Как и раньше, мой неидеальный результат выглядит следующим образом (примечаниесписок калорий):

{
"recipe_title": "Gin & Tonic",
"recipe_posted_date": "May 2, 2019",
"recipe_url": "www.XYZ.com/gandt.html",
"recipe_instructions": "<block of text here>",
"recipe_ingredients": ["gin", "tonic water", "lime wedge"],
"recipe_calorie_total": [],
"recipe_calorie_list": ["60 calories"]
},
{
"recipe_title": "Gin & Tonic",
"recipe_posted_date": "May 2, 2019",
"recipe_url": "www.XYZ.com/gandt.html",
"recipe_instructions": "<block of text here>",
"recipe_ingredients": ["gin", "tonic water", "lime wedge"],
"recipe_calorie_total": [],
"recipe_calorie_list": ["60 calories", "70 calories"]
},
{
"recipe_title": "Gin & Tonic",
"recipe_posted_date": "May 2, 2019",
"recipe_url": "www.XYZ.com/gandt.html",
"recipe_instructions": "<block of text here>",
"recipe_ingredients": ["gin", "tonic water", "lime wedge"],
"recipe_calorie_total": [],
"recipe_calorie_list": ["60 calories", "70 calories", "5 calories"]
}

1 Ответ

0 голосов
/ 03 мая 2019

Одним из решений было бы очистить рецепты и ингредиенты по отдельности, как разные элементы, а затем выполнить некоторую постобработку после завершения сканирования, например, с использованием обычного Python, который объединяет данные рецепта и ингредиента по мере необходимости. Это наиболее эффективное решение.

Кроме того, вы можете извлекать URL-адреса ингредиентов из ответов на рецепты и вместо того, чтобы выдавать запросы на все из них одновременно, вы можете выдать запрос на первый ингредиент и сохранить оставшиеся URL-адреса ингредиентов в новом запросе meta вместе с предметом рецепта. Когда ответ об ингредиенте получен, вы разбираете всю необходимую информацию в meta и выдает новый запрос на следующий URL ингредиента. Когда больше нет URL-адресов ингредиентов, вы получаете полный рецепт.

Например:

def _handle_next_ingredient(self, recipe, ingredient_urls):
    try:
        return Request(
            ingredient_urls.pop(),
            callback=self.parse_ingredient,
            meta={'recipe': recipe, 'ingredient_urls': ingredient_urls},
        )
    except IndexError:
        return recipe

def parse_recipe(self, response):
    recipe = {}, ingredient_urls = []
    # [Extract needed data into recipe and ingredient URLs into ingredient_urls]
    yield self._handle_next_ingredient(recipe, ingredient_urls)

def parse_ingredient(self, response):
    recipe = response.meta['recipe']
    # [Extend recipe with the information of this ingredient]
    yield self._handle_next_ingredient(recipe, response.meta['ingredient_urls'])

Обратите внимание, однако, что если два или более рецептов могут иметь один и тот же URL-адрес ингредиента, вам придется добавить dont_filter=True к вашим запросам, повторяя несколько запросов на одни и те же ингредиенты. Вместо этого серьезно подумайте о первом предложении, если URL-адреса ингредиентов не зависят от рецепта.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...