Вызов запроса на выход дает странный результат в рекурсивном методе с помощью scrapy

Я пытаюсь отказаться от всех вылетов и прибытий за один день из всех аэропортов по всей стране, используя Python и Scrapy.

База данных JSON, используемая этим знаменитым сайтом (радар), должна запрашивать страницу за страницей при выезде или прибытии > 100 в одном аэропорту. Я также вычисляю временную метку, основанную на фактическом дневном UTC для запроса.

Я пытаюсь создать базу данных с этой иерархией:

country 1
 - airport 1
    - departures
      - page 1
      - page ...
    - arrivals
      - page 1
      - page ...
- airport 2
    - departures
      - page 1
      - page ...
    - arrivals
      - page 
      - page ...
...

Я использую два метода для вычисления временных меток и URL-адресов по страницам:

def compute_timestamp(self):
    from datetime import datetime, date
    import calendar
    # +/- 24 heures
    d = date(2017, 4, 27)
    timestamp = calendar.timegm(d.timetuple())
    return timestamp

def build_api_call(self,code,page,timestamp):
    return 'https://api.flightradar24.com/common/v1/airport.json?code={code}&plugin\[\]=&plugin-setting\[schedule\]\[mode\]=&plugin-setting\[schedule\]\[timestamp\]={timestamp}&page={page}&limit=100&token='.format(
        code=code, page=page, timestamp=timestamp)

Я сохраняю результат в CountryItem, который содержит много AirportItem в аэропортах. Мой item.py:

class CountryItem(scrapy.Item):
    name = scrapy.Field()
    link = scrapy.Field()
    num_airports = scrapy.Field()
    airports = scrapy.Field()
    other_url= scrapy.Field()
    last_updated = scrapy.Field(serializer=str)

class AirportItem(scrapy.Item):
    name = scrapy.Field()
    code_little = scrapy.Field()
    code_total = scrapy.Field()
    lat = scrapy.Field()
    lon = scrapy.Field()
    link = scrapy.Field()
    departures = scrapy.Field()
    arrivals = scrapy.Field()

Мой основной синтаксис строит элемент Страны для всех стран (например, я ограничиваюсь здесь, например, Израилем). Затем я уступаю каждой стране а scrapy.Request для очистки аэропортов.

###################################
# MAIN PARSE
####################################
def parse(self, response):
    count_country = 0
    countries = []
    for country in response.xpath('//a[@data-country]'):
        item = CountryItem()
        url =  country.xpath('./@href').extract()
        name = country.xpath('./@title').extract()
        item['link'] = url[0]
        item['name'] = name[0]
        item['airports'] = []
        count_country += 1
        if name[0] == "Israel":
            countries.append(item)
            self.logger.info("Country name : %s with link %s" , item['name'] , item['link'])
            yield scrapy.Request(url[0],meta={'my_country_item':item}, callback=self.parse_airports)

Этот метод очищает информацию для каждого аэропорта, а также призывает к каждому аэропорту scrapy.Request с адресом аэропорта, чтобы очистить вылеты и прибытия:

  ###################################
# PARSE EACH AIRPORT
####################################
def parse_airports(self, response):
    item = response.meta['my_country_item']
    item['airports'] = []

    for airport in response.xpath('//a[@data-iata]'):
        url = airport.xpath('./@href').extract()
        iata = airport.xpath('./@data-iata').extract()
        iatabis = airport.xpath('./small/text()').extract()
        name = ''.join(airport.xpath('./text()').extract()).strip()
        lat = airport.xpath("./@data-lat").extract()
        lon = airport.xpath("./@data-lon").extract()
        iAirport = AirportItem()
        iAirport['name'] = self.clean_html(name)
        iAirport['link'] = url[0]
        iAirport['lat'] = lat[0]
        iAirport['lon'] = lon[0]
        iAirport['code_little'] = iata[0]
        iAirport['code_total'] = iatabis[0]

        item['airports'].append(iAirport)

    urls = []
    for airport in item['airports']:
        json_url = self.build_api_call(airport['code_little'], 1, self.compute_timestamp())
        urls.append(json_url)
    if not urls:
        return item

    # start with first url
    next_url = urls.pop()
    return scrapy.Request(next_url, self.parse_schedule, meta={'airport_item': item, 'airport_urls': urls, 'i': 0})

С помощью рекурсивного метода parse_schedule я добавляю каждый аэропорт в пункт страны. Члены SO уже помогите мне по этому вопросу.

###################################
# PARSE EACH AIRPORT OF COUNTRY
###################################
def parse_schedule(self, response):
        """we want to loop this continuously to build every departure and arrivals requests"""
        item = response.meta['airport_item']
        i = response.meta['i']
        urls = response.meta['airport_urls']

        urls_departures, urls_arrivals = self.compute_urls_by_page(response, item['airports'][i]['name'], item['airports'][i]['code_little'])

        print("urls_departures = ", len(urls_departures))
        print("urls_arrivals = ", len(urls_arrivals))

        ## YIELD NOT CALLED
        yield scrapy.Request(response.url, self.parse_departures_page, meta={'airport_item': item, 'page_urls': urls_departures, 'i':0 , 'p': 0}, dont_filter=True)

        # now do next schedule items
        if not urls:
            yield item
            return
        url = urls.pop()

        yield scrapy.Request(url, self.parse_schedule, meta={'airport_item': item, 'airport_urls': urls, 'i': i + 1})

метод self.compute_urls_by_page вычислит правильные URL-адреса, чтобы получить все отправления и прибытия в один аэропорт.

###################################
# PARSE EACH DEPARTURES / ARRIVALS
###################################
def parse_departures_page(self, response):
    item = response.meta['airport_item']
    p = response.meta['p']
    i = response.meta['i']
    page_urls = response.meta['page_urls']

    print("PAGE URL = ", page_urls)

    if not page_urls:
        yield item
        return
    page_url = page_urls.pop()

    print("GET PAGE FOR  ", item['airports'][i]['name'], ">> ", p)

    jsonload = json.loads(response.body_as_unicode())
    json_expression = jmespath.compile("result.response.airport.pluginData.schedule.departures.data")
    item['airports'][i]['departures'] = json_expression.search(jsonload)

    yield scrapy.Request(page_url, self.parse_departures_page, meta={'airport_item': item, 'page_urls': page_urls, 'i': i, 'p': p + 1})

Затем первый выход в parse_schedule, который обычно вызывает рекурсивный метод self.parse_departure_page, дает странные результаты. Scrapy вызывает этот метод, но я собираю страницу отправлений только для одного аэропорта, я не понимаю, почему... У меня, вероятно, ошибка заказа в моем запросе или исходный код, так что, возможно, вы могли бы помочь мне узнать.

Полный код находится на GitHub https://github.com/IDEES-Rouen/Flight-Scrapping/tree/master/flight/flight_project

Вы можете запустить его с помощью команд scrapy cawl airports.

Обновление 1:

Я пытаюсь ответить на один вопрос, используя yield from, без успеха, поскольку вы можете увидеть ответ внизу... так что если у вас есть идея?

Ответы

Ответ 1

Комментарий:... не совсем понятно... вы называете AirportData (ответ, 1)... также немного опечатайте здесь: self.pprint(расписание)

Я использовал class AirportData для реализации (ограничение до 2 страниц и 2 полеты).
Обновлен мой код, удален class AirportData и добавлен class Page.
Должны теперь заполнять все зависимости.

Это не опечатка, self.pprint(... - это class AirportsSpider Method, используемый для Довольно Печать объекта, например, Результат, показанный в конце. Я добавил class Schedule, чтобы показать основное использование.


Комментарий. Что такое "AirportData" в вашем ответе?

ИЗМЕНИТЬ: class AirportData удален.
Как отмечено в # ENDPOINT, a Page object данных полета разбито на page.arrivals и page.departures. (Ограничено до 2 страниц и 2 рейса)

Page = [Flight 1, Flight 1, ... Flight n] 
schedule.airport['arrivals'] == [Page 1, Page 2, ..., Page n]
schedule.airport['departures'] == [Page 1, Page 2, ..., Page n]

Комментарий:... у нас есть кратные страницы, содержащие кратность вылетов/прибытий.

Да, во время первого ответа у меня не было ответа api json, чтобы получить дальше.
Теперь я получил ответ от api json, но не отражает данный timestamp, возвращается из current date. api params выглядит необычно, есть ли у вас ссылка на описание?


Тем не менее, рассмотрим этот упрощенный подход:

# Объект страницы, содержащий одну страницу данных о полетах/вылетах

class Page(object):
    def __init__(self, title, schedule):
        # schedule includes ['arrivals'] or ['departures]
        self.current = schedule['page']['current']
        self.total = schedule['page']['total']

        self.header = '{}:page:{} item:{}'.format(title, schedule['page'], schedule['item'])
        self.flight = []
        for data in schedule['data']:
            self.flight.append(data['flight'])

    def __iter__(self):
        yield from self.flight

# Объект расписания, содержащий один аэропорт всех страниц

class Schedule(object):
    def __init__(self):
        self.country = None
        self.airport = None

    def __str__(self):
        arrivals = self.airport['arrivals'][0]
        departures = self.airport['departures'][0]
        return '{}\n\t{}\n\t\t{}\n\t\t\t{}\n\t\t{}\n\t\t\t{}'. \
            format(self.country['name'],
                   self.airport['name'],
                   arrivals.header,
                   arrivals.flight[0]['airline']['name'],
                   departures.header,
                   departures.flight[0]['airline']['name'], )

# PARSE КАЖДЫЙ АЭРОПОРТ СТРАНЫ

def parse_schedule(self, response):
    meta = response.meta

    if 'airport' in meta:
        # First call from parse_airports
        schedule = Schedule()
        schedule.country = response.meta['country']
        schedule.airport = response.meta['airport']
    else:
        schedule = response.meta['schedule']

    data = json.loads(response.body_as_unicode())
    airport = data['result']['response']['airport']

    schedule.airport['arrivals'].append(Page('Arrivals', airport['pluginData']['schedule']['arrivals']))
    schedule.airport['departures'].append(Page('Departures', airport['pluginData']['schedule']['departures']))

    page = schedule.airport['departures'][-1]
    if page.current < page.total:
        json_url = self.build_api_call(schedule.airport['code_little'], page.current + 1, self.compute_timestamp())
        yield scrapy.Request(json_url, meta={'schedule': schedule}, callback=self.parse_schedule)
    else:
        # ENDPOINT Schedule object holding one Airport.
        # schedule.airport['arrivals'] and schedule.airport['departures'] ==
        #   List of Page with List of Flight Data
        print(schedule)

# PARSE КАЖДЫЙ АЭРОПОРТ

def parse_airports(self, response):
    country = response.meta['country']

    for airport in response.xpath('//a[@data-iata]'):
        name = ''.join(airport.xpath('./text()').extract()[0]).strip()

        if 'Charles' in name:
            meta = response.meta
            meta['airport'] = AirportItem()
            meta['airport']['name'] = name
            meta['airport']['link'] = airport.xpath('./@href').extract()[0]
            meta['airport']['lat'] = airport.xpath("./@data-lat").extract()[0]
            meta['airport']['lon'] = airport.xpath("./@data-lon").extract()[0]
            meta['airport']['code_little'] = airport.xpath('./@data-iata').extract()[0]
            meta['airport']['code_total'] = airport.xpath('./small/text()').extract()[0]

            json_url = self.build_api_call(meta['airport']['code_little'], 1, self.compute_timestamp())
            yield scrapy.Request(json_url, meta=meta, callback=self.parse_schedule)

# ГЛАВНАЯ ПАРАМЕТРЫ

Примечание: response.xpath('//a[@data-country]') возвращает все Страны два раза!

def parse(self, response):
    for a_country in response.xpath('//a[@data-country]'):
            name = a_country.xpath('./@title').extract()[0]
            if name == "France":
                country = CountryItem()
                country['name'] = name
                country['link'] = a_country.xpath('./@href').extract()[0]

                yield scrapy.Request(country['link'],
                                     meta={'country': country},
                                     callback=self.parse_airports)

Qutput: Сократите до 2 Страницы и 2 Полеты на страницу

France
    Paris Charles de Gaulle Airport
        Departures:(page=(1, 1, 7)) 2017-07-02 21:28:00 page:{'current': 1, 'total': 7} item:{'current': 100, 'limit': 100, 'total': 696}
            21:30 PM    AF1558  Newcastle Airport (NCL) Air France ARJ  Estimated dep 21:30
            21:30 PM    VY8833  Seville San Pablo Airport (SVQ) Vueling 320 Estimated dep 21:30
            ... (omitted for brevity)
        Departures:(page=(2, 2, 7)) 2017-07-02 21:28:00 page:{'current': 2, 'total': 7} item:{'current': 100, 'limit': 100, 'total': 696}
            07:30 AM    AF1680  London Heathrow Airport (LHR)   Air France 789  Scheduled
            07:30 AM    SN3628  Brussels Airport (BRU)  Brussels Airlines 733   Scheduled
            ... (omitted for brevity)
        Arrivals:(page=(1, 1, 7)) 2017-07-02 21:28:00 page:{'current': 1, 'total': 7} item:{'current': 100, 'limit': 100, 'total': 693}
            16:30 PM    LY325   Tel Aviv Ben Gurion International Airport (TLV) El Al Israel Airlines B739  Estimated 21:29
            18:30 PM    AY877   Helsinki Vantaa Airport (HEL)   Finnair E190    Landed 21:21
            ... (omitted for brevity)
        Arrivals:(page=(2, 2, 7)) 2017-07-02 21:28:00 page:{'current': 2, 'total': 7} item:{'current': 100, 'limit': 100, 'total': 693}
            00:15 AM    AF982   Douala International Airport (DLA)  Air France 772  Scheduled
            23:15 PM    AA44    New York John F. Kennedy International Airport (JFK)    American Airlines B763  Scheduled
            ... (omitted for brevity)

Протестировано с помощью Python: 3.4.2 - Scrapy 1.4.0

Ответ 2

Да, я наконец нашел ответ здесь на SO...

Когда вы используете рекурсивный yield, вам нужно использовать yield from. Здесь один пример упрощен:

airport_list = ["airport1", "airport2", "airport3", "airport4"]

def parse_page_departure(airport, next_url, page_urls):

    print(airport, " / ", next_url)


    if not page_urls:
        return

    next_url = page_urls.pop()

    yield from parse_page_departure(airport, next_url, page_urls)

###################################
# PARSE EACH AIRPORT OF COUNTRY
###################################
def parse_schedule(next_airport, airport_list):

    ## GET EACH DEPARTURE PAGE

    departures_list = ["p1", "p2", "p3", "p4"]

    next_departure_url = departures_list.pop()
    yield parse_page_departure(next_airport,next_departure_url, departures_list)

    if not airport_list:
        print("no new airport")
        return

    next_airport_url = airport_list.pop()

    yield from parse_schedule(next_airport_url, airport_list)

next_airport_url = airport_list.pop()
result = parse_schedule(next_airport_url, airport_list)

for i in result:
    print(i)
    for d in i:
        print(d)

ОБНОВЛЕНИЕ, НЕ РАБОТАЙТЕ с реальной программой:

Я пытаюсь воспроизвести тот же шаблон yield from с реальной программой здесь, но у меня есть ошибка, использующая его на scrapy.Request, не понимаю, почему...

Здесь трассировка python:

Traceback (most recent call last):
  File "/home/reyman/.pyenv/versions/venv352/lib/python3.5/site-packages/scrapy/utils/defer.py", line 102, in iter_errback
    yield next(it)
  File "/home/reyman/.pyenv/versions/venv352/lib/python3.5/site-packages/scrapy/spidermiddlewares/offsite.py", line 29, in process_spider_output
    for x in result:
  File "/home/reyman/.pyenv/versions/venv352/lib/python3.5/site-packages/scrapy/spidermiddlewares/referer.py", line 339, in <genexpr>
    return (_set_referer(r) for r in result or ())
  File "/home/reyman/.pyenv/versions/venv352/lib/python3.5/site-packages/scrapy/spidermiddlewares/urllength.py", line 37, in <genexpr>
    return (r for r in result or () if _filter(r))
  File "/home/reyman/.pyenv/versions/venv352/lib/python3.5/site-packages/scrapy/spidermiddlewares/depth.py", line 58, in <genexpr>
    return (r for r in result or () if _filter(r))
  File "/home/reyman/Projets/Flight-Scrapping/flight/flight_project/spiders/AirportsSpider.py", line 209, in parse_schedule
    yield from scrapy.Request(url, self.parse_schedule, meta={'airport_item': item, 'airport_urls': urls, 'i': i + 1})
TypeError: 'Request' object is not iterable
2017-06-27 17:40:50 [scrapy.core.engine] INFO: Closing spider (finished)
2017-06-27 17:40:50 [scrapy.statscollectors] INFO: Dumping Scrapy stats:

Ответ 3

Я попытался клонировать локально и исследовать немного лучше, но когда он доходит до анализа партирования, я получил некоторую ошибку ConnectionRefused, поэтому я не уверен, что мой предложенный ответ исправит ее, во всяком случае:

###################################
# PARSE EACH AIRPORT OF COUNTRY
###################################
def parse_schedule(self, response):
    """we want to loop this continuously to build every departure and arrivals requests"""
    item = response.meta['airport_item']
    i = response.meta['i']
    urls = response.meta['airport_urls']

    urls_departures, urls_arrivals = self.compute_urls_by_page(response, item['airports'][i]['name'], item['airports'][i]['code_little'])

    if 'urls_departures' in response.meta:
        urls_departures += response.meta["urls_departures"]

    if 'urls_arrivals' in response.meta:
        urls_arrivals += response.meta["urls_arrivals"]

    print("urls_departures = ", len(urls_departures))
    print("urls_arrivals = ", len(urls_arrivals))
    item['airports'][i]['departures'] = []

    # now do next schedule items
    if not urls:
        yield scrapy.Request(urls_departures.pop(), self.parse_departures_page, meta={'airport_item': item, 'page_urls': urls_departures, 'i':i , 'p': 0}, dont_filter=True)
    else:
        url = urls.pop()

        yield scrapy.Request(url, self.parse_schedule, meta={'airport_item': item, 'airport_urls': urls, 'i': i + 1, 'urls_departures': urls_departures, 'urls_arrivals': urls_arrivals})

###################################
# PARSE EACH DEPARTURES / ARRIVALS
###################################
def parse_departures_page(self, response):
    item = response.meta['airport_item']
    p = response.meta['p']
    i = response.meta['i']
    page_urls = response.meta['page_urls']

    jsonload = json.loads(response.body_as_unicode())
    json_expression = jmespath.compile("result.response.airport.pluginData.schedule.departures.data")

    # Append a new page
    item['airports'][i]['departures'].append(json_expression.search(jsonload))

    if len(page_urls) > 0:
        page_url = page_urls.pop()

        yield scrapy.Request(page_url, self.parse_departures_page, meta={'airport_item': item, 'page_urls': page_urls, 'i': i, 'p': p + 1}, dont_filter=True)
    else:
        yield item

Но в основном это ваши ошибки:

  • В вашем parse_schedule и на странице parse_departures_page есть условия для получения окончательного элемента;

  • Вы передаете неправильный URL-адрес parse_departures_page;

  • Вам нужно dont_filter = True на parse_departures_page;

  • Вы пытаетесь сохранить много циклов для синтаксического анализа дополнительной информации на один и тот же объект

Мои предложенные изменения будут отслеживать все urls_departures в этом аэропорту, чтобы вы могли перебирать их на странице parse_departures_page и исправлять свои проблемы.

Даже если это исправить вашу проблему, я ДЕЙСТВИТЕЛЬНО рекомендую вам изменить структуру данных, чтобы вы могли иметь несколько элементов для отправлений и иметь возможность извлекать эту информацию более эффективно.