estin / pomp-craigslist-example

Extract data from Craigslist.org by python3 and pomp framework
37 stars 6 forks source link

Не работает парсер? #2

Closed makorne closed 7 years ago

makorne commented 7 years ago

в кафке query отображаются(current 2, total 60), но все остальное по нулям. в django спарщенного тоже нет.

В логе:

2017-03-29 22:37:01,057 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://chicago.craigslist.org/se$ 2017-03-29 22:37:01,059 DEBUG MainProcess craigslist.queue From queue: <ListRequest session_id:mountainbike url:https://newyork.craigslist.org/sear$ 2017-03-29 22:37:01,059 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Start fetch: https://newyork.craigslist.org/search/bia?s=360&is$ 2017-03-29 22:37:02,473 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Done https://newyork.craigslist.org/search/bia?s=360&is_paid=al$ 2017-03-29 22:37:02,476 DEBUG Process-1 pomp.contrib.concurrent Crawler worker pid=12 params={'worker_class': <class 'craigslist.crawler.CraigsList$ 2017-03-29 22:37:02,497 DEBUG MainProcess craigslist.queue From queue: <ListRequest session_id:mountainbike url:https://sfbay.craigslist.org/search$ 2017-03-29 22:37:02,497 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Start fetch: https://sfbay.craigslist.org/search/bia?s=360&is_p$ 2017-03-29 22:37:04,062 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Done https://sfbay.craigslist.org/search/bia?s=360&is_paid=all&$ 2017-03-29 22:37:04,065 DEBUG Process-2 pomp.contrib.concurrent Crawler worker pid=13 params={'worker_class': <class 'craigslist.crawler.CraigsList$ 2017-03-29 22:37:04,085 DEBUG MainProcess craigslist.queue From queue: <ListRequest session_id:mountainbike url:https://chicago.craigslist.org/sear$ 2017-03-29 22:37:04,086 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Start fetch: https://chicago.craigslist.org/search/bia?s=360&is$ 2017-03-29 22:37:05,516 DEBUG MainProcess craigslist.downloader [AiohttpDownloader] Done https://chicago.craigslist.org/search/bia?s=360&is_paid=al$ 2017-03-29 22:37:05,519 DEBUG Process-1 pomp.contrib.concurrent Crawler worker pid=12 params={'worker_class': <class 'craigslist.crawler.CraigsList$ 2017-03-29 22:48:56,910 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://newyork.craigslist.org/se$ 2017-03-29 22:48:56,911 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://sfbay.craigslist.org/sear$ 2017-03-29 22:48:56,912 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://chicago.craigslist.org/se$ 2017-03-29 22:51:37,266 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://newyork.craigslist.org/se$ 2017-03-29 22:51:37,267 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://sfbay.craigslist.org/sear$ 2017-03-29 22:51:37,268 DEBUG MainProcess craigslist.queue Put to queue: <ListRequest session_id:mountainbike url:https://chicago.craigslist.org/se$

estin commented 7 years ago

Возможно сменилась верстка и парсер не может "вытащить" итемы, но при этом работает пагинация. На днях попробую глянуть.

makorne commented 7 years ago

Может как-то выводить сообщение об этом? Наверно такие ситуации не редкость, мало ли где верстка изменится... Если б сразу знать где копать - можно исправить.

estin commented 7 years ago

Починил парсер - дело было в xpath. Ввел команду проверки xpath на актуальность

$ docker-compose run --rm crawler manage check-xpath "https://sfbay.craigslist.org/search/bia?is_paid=all&search_distance_type=mi&query=kid+bike"

Осторожно, craigslist начал банить по IP

Может как-то выводить сообщение об этом?

Сходу не могу представить где и как оповещать. Но так как есть команда проверки xpath на актуальность, то можно на её основе:

makorne commented 7 years ago

Наверняка обе вещи нужны, чтоб бесполезные баны не плодить и время зря не тратить.

estin commented 7 years ago

Согласен, что такое для реального проекта полезно, но в рамках данного проекта смысла не вижу.

Если есть желание, то попробуйте реализовать это сами, помогу сделать ревью.

makorne commented 7 years ago

Мне б понять как тут вообще тест запускать... :) Какой строкой юнит-тест запустить?

makorne commented 7 years ago

Ay, бог-созидатель шайтан-помпы! Ну подскажи плиз - какой строкой mock тест проекта запустить?

estin commented 7 years ago

Вот этой командой

$ docker-compose run --rm crawler manage check-xpath "https://sfbay.craigslist.org/search/bia?is_paid=all&search_distance_type=mi&query=kid+bike"

Она скачает html и подсунет парсеру, в ответ будет:

А вот mock тест проекта нету. Есть только проверка актуальности xpath и все. Остальные части проекта не проверяются.

makorne commented 7 years ago

А для чего тогда файлы в папочке tests? Ты же их делал, начит и запускал как-то..

estin commented 7 years ago

И точно есть же тесты, забыл про них совсем.

Запустить их можно вот так.

$ docker-compose run --rm crawler py.test
Starting pompcraigslistexample_grafana_1
Starting pompcraigslistexample_zookeeper_1
Starting pompcraigslistexample_redis_1
Starting pompcraigslistexample_kafka_1

=========================================================== test session starts ============================================================
platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /root, inifile:
collected 6 items 

tests/test_crawler.py ..
tests/test_downloader.py .
tests/test_pipeline.py ..
tests/test_queue.py .

======================================================== 6 passed in 25.35 seconds =========================================================
makorne commented 7 years ago

Ни как не пойму что делать... И гугл не знает. Может как-то не так для utf8 парсится в движке? Куда это "obj, end = self.scan_once(s, idx) " вставить?

item.photos = [
            i['url'] for i in json.loads(
                self.ITEM_PHOTOS_RE.search(response.body).groups()[0]
            )
        ]
`_____________________________ test_crawler_item_parse ______________________________

self = <json.decoder.JSONDecoder object at 0x7fbd8dad3ba8>
s = 'https://www.testing.com/v1/AUTH_svc.ui/PIX33/319c2613-d5bf-449b-98fb-744.JPG'
idx = 0

    def raw_decode(self, s, idx=0):
        """Decode a JSON document from ``s`` (a ``str`` beginning with
            a JSON document) and return a 2-tuple of the Python
            representation and the index in ``s`` where the document ended.

            This can be used to decode a JSON document from a string that may
            have extraneous data at the end.

            """
        try:
>           obj, end = self.scan_once(s, idx)
E           StopIteration: 0

/usr/local/lib/python3.6/json/decoder.py:355: StopIteration`

estin commented 7 years ago

Понял что какая-то проблема получением список фоток - ошибка декодирования json.

Вот в исходной HMTL странице есть кусок JS кода

var imgList = [{"shortid":"kN64FieYYgT","url":"http://images.craigslist.org/01717_kN64FieYYgT_600x450.jpg","thumb":"http://images.craigslist.org/01717_kN64FieYYgT_50x50c.jpg","imgid":"0:01717_kN64FieYYgT"}];

Наш парсер делает следующее

>>> import re                                                                                                                               
>>> import json                                                                                                                             
>>> source = """                   
... var imgList = [{"shortid":"kN64FieYYgT","url":"http://images.craigslist.org/01717_kN64FieYYgT_600x450.jpg","thumb":"http://images.craigslist.org/01717_kN64FieYYgT_50x50c.jpg","imgid":"0:01717_kN64FieYYgT"}];
... """                            
>>> ITEM_PHOTOS_RE = re.compile('var imgList\ =\ (.*)\;')             
>>> images = ITEM_PHOTOS_RE.search(source).groups()[0]                
>>> images                         
'[{"shortid":"kN64FieYYgT","url":"http://images.craigslist.org/01717_kN64FieYYgT_600x450.jpg","thumb":"http://images.craigslist.org/01717_kN64FieYYgT_50x50c.jpg","imgid":"0:01717_kN64FieYYgT"}]'
>>> [i['url'] for i in json.loads(images)]                            
['http://images.craigslist.org/01717_kN64FieYYgT_600x450.jpg']   

Проверти что передается в self.ITEM_PHOTOS_RE.search(response.body).groups()[0] в параметре response.body есть ли там нужный imgList?

makorne commented 7 years ago

Не знаю как правильно это сделать. Если так: log.debug( self.ITEM_PHOTOS_RE.search(response.body).groups()[0]) Ругается матом

===================================== FAILURES _ test_crawler_item_parse __

def test_crawler_item_parse():

    request = ItemRequest(
        session_id='somesession',
        url='mocked',
        city_code='',
    )

    items = crawler.extract_items(
        mock_response(
            'data/item.html',
            request_instance=request,
        ),
    )
  for item in items:

tests/test_crawler.py:56:


craigslist/crawler.py:90: in extract_items yield from self._parse_item(response, tree) craigslist/crawler.py:167: in _parse_item log.debug( self.ITEM_PHOTOS_RE.search(response.body).groups()[0])


s = None, encoding = None, cls = None, object_hook = None, parse_float = None parse_int = None, parse_constant = None, object_pairs_hook = None, kw = {}

def loads(s, *, encoding=None, cls=None, object_hook=None, parse_float=None,
        parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
    """Deserialize ``s`` (a ``str``, ``bytes`` or ``bytearray`` instance
    containing a JSON document) to a Python object.

    ``object_hook`` is an optional function that will be called with the
    result of any object literal decode (a ``dict``). The return value of
    ``object_hook`` will be used instead of the ``dict``. This feature
    can be used to implement custom decoders (e.g. JSON-RPC class hinting).

    ``object_pairs_hook`` is an optional function that will be called with the
    result of any object literal decoded with an ordered list of pairs.  The
    return value of ``object_pairs_hook`` will be used instead of the ``dict``.
    This feature can be used to implement custom decoders that rely on the
    order that the key and value pairs are decoded (for example,
    collections.OrderedDict will remember the order of insertion). If
    ``object_hook`` is also defined, the ``object_pairs_hook`` takes priority.

    ``parse_float``, if specified, will be called with the string
    of every JSON float to be decoded. By default this is equivalent to
    float(num_str). This can be used to use another datatype or parser
    for JSON floats (e.g. decimal.Decimal).

    ``parse_int``, if specified, will be called with the string
    of every JSON int to be decoded. By default this is equivalent to
    int(num_str). This can be used to use another datatype or parser
    for JSON integers (e.g. float).

    ``parse_constant``, if specified, will be called with one of the
    following strings: -Infinity, Infinity, NaN.
    This can be used to raise an exception if invalid JSON numbers
    are encountered.

    To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
    kwarg; otherwise ``JSONDecoder`` is used.

    The ``encoding`` argument is ignored and deprecated.

    """
    if isinstance(s, str):
        if s.startswith('\ufeff'):
            raise JSONDecodeError("Unexpected UTF-8 BOM (decode using utf-8-sig)",
                                  s, 0)
    else:
        if not isinstance(s, (bytes, bytearray)):
            raise TypeError('the JSON object must be str, bytes or bytearray, '
                          'not {!r}'.format(s.__class__.__name__))

E TypeError: the JSON object must be str, bytes or bytearray, not 'NoneType'

/usr/local/lib/python3.6/json/init.py:348: TypeError ------------------------------- Captured stderr call ------------------------------- DEBUG:craigslist.crawler:https://www.testing.com/v1/AUTH_svc.ui/PIX33/319c2613-d5bf-449b-98fb-744.JPG `

makorne commented 7 years ago

Я парщу другую страницу, там регексп такой: ITEM_PHOTOS_RE = re.compile('(https:\/\/.+.JPG)') В онлайн тестере работает, тут тоже, но видимо изза ошибки UTF8 на странице его зацикливает, надо куда-то "obj, end = self.scan_once(s, idx) " вставить.

estin commented 7 years ago

re.compile в исходном парсинге craigslist нужен для выдерания JSON строк из JS кода на странице - костыль по сути.

У Вас же похоже "фотки" есть полноценные DOM элементы их можно и лучше "выдерать" через XPATH.

makorne commented 7 years ago

pythex.org выгрызает 10 на этой странице, ну если так проще... Спасибо!

makorne commented 7 years ago

А как лучше это сделать?

estin commented 7 years ago

photos = item.xpath('<some-xpath-to-photo-links>/img/@src')

Вот пример XPATH, который должен вытащить все ссылки на аватарки участников обсуждения с это страницы github - //div[contains(@class, 'timeline-comment-avatar')]//img/@src

см скрин

2017-10-30 22-56-21

makorne commented 7 years ago

Спасибище, я просто не знаю как на питоне лучше . вот это интересовало: photos = item.xpath('/img/@src')

makorne commented 7 years ago

Чета туплю... item.photos = response.body.xpath('/img/@src') File "/root/craigslist/crawler.py", line 163 E item.photos = response.body.xpath('/img/@src') E ^ E IndentationError: unindent does not match any outer indentation level _______________________ ERROR collecting tests/test_queue.py _______________________ lib/python3.6/site-packages/pytest-3.0.7-py3.6.egg/_pytest/python.py:418: in _importtestmodule

makorne commented 7 years ago
<div class="viewAllPhotosRelative">
            <img src="photos_files/319c2613-d8bf-44b-98fb-748294.JPG" 

item.photos = response.body.xpath('//div[contains(@class, "viewAllPhotosRelative")]/img/@src')

File "/root/craigslist/crawler.py", line 163
E       item.photos = response.body.xpath('//div[contains(@class, "viewAllPhotosRelative")]//img/@src')
E                                                                                                     ^
E   IndentationError: unindent does not match any outer indentation level

Стрелочка постоянно показывает на последнюю скобку

makorne commented 7 years ago

Class - с маленькой, тут чтото тупит автозаменяет

estin commented 7 years ago

Вам нужно редактор настроить так что бы вместо tab были 4 space. И во всех файлах tab нужно заменить на 4 space и тогда все должно завестись

makorne commented 7 years ago

Тогда:

item.photos = response.body.xpath('//div[contains(@class, "viewAllPhotosRelative")]//img/@src')
E       AttributeError: 'str' object has no attribute 'xpath'
estin commented 7 years ago

Попробуйте так html.fromstring(response.body).xpath('//div[contains(@class, "viewAllPhotosRelative")]//img/@src')

makorne commented 7 years ago

Спасибо, я во как нашел tree.xpath(...