Рекурсивное сканирование с помощью Python и Scrapy
Я использую scrapy для сканирования сайта. На сайте есть 15 записей на странице, а затем есть следующая кнопка. Я столкнулся с проблемой, когда мой запрос для следующей ссылки вызывается до того, как я закончу разбор всех моих списков в конвейере. Вот код для моего паука:
class MySpider(CrawlSpider):
name = 'mysite.com'
allowed_domains = ['mysite.com']
start_url = 'http://www.mysite.com/'
def start_requests(self):
return [Request(self.start_url, callback=self.parse_listings)]
def parse_listings(self, response):
hxs = HtmlXPathSelector(response)
listings = hxs.select('...')
for listing in listings:
il = MySiteLoader(selector=listing)
il.add_xpath('Title', '...')
il.add_xpath('Link', '...')
item = il.load_item()
listing_url = listing.select('...').extract()
if listing_url:
yield Request(urlparse.urljoin(response.url, listing_url[0]),
meta={'item': item},
callback=self.parse_listing_details)
next_page_url = hxs.select('descendant::div[@id="pagination"]/'
'div[@class="next-link"]/a/@href').extract()
if next_page_url:
yield Request(urlparse.urljoin(response.url, next_page_url[0]),
callback=self.parse_listings)
def parse_listing_details(self, response):
hxs = HtmlXPathSelector(response)
item = response.request.meta['item']
details = hxs.select('...')
il = MySiteLoader(selector=details, item=item)
il.add_xpath('Posted_on_Date', '...')
il.add_xpath('Description', '...')
return il.load_item()
Эти строки являются проблемой. Как я уже сказал, они исполняются до того, как паук закончил сканирование текущей страницы. На каждой странице сайта это всего лишь 3 из 15 моих списков, которые будут отправлены в конвейер.
if next_page_url:
yield Request(urlparse.urljoin(response.url, next_page_url[0]),
callback=self.parse_listings)
Это мой первый паук и может быть ошибкой дизайна с моей стороны, есть ли лучший способ сделать это?
Ответы
Ответ 1
Скопировать вместо паука?
Поскольку ваша оригинальная проблема требует повторной навигации по последовательному и повторяемому набору контента вместо дерева содержимого неизвестного размера, используйте mechanize (http://wwwsearch.sourceforge.net/mechanize/) и beautifulsoup (http://www.crummy.com/software/BeautifulSoup/).
Вот пример создания экземпляра браузера с помощью механизации. Кроме того, использование br.follow_link (text = "foo" ) означает, что в отличие от xpath в вашем примере ссылки будут по-прежнему выполняться независимо от структуры элементов в пути предка. Смысл, если они обновят свой HTML, ваши разрывы script. Свободная муфта сохранит вам некоторое обслуживание. Вот пример:
br = mechanize.Browser()
br.set_handle_equiv(True)
br.set_handle_redirect(True)
br.set_handle_referer(True)
br.set_handle_robots(False)
br.addheaders = [('User-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:9.0.1)Gecko/20100101 Firefox/9.0.1')]
br.addheaders = [('Accept-Language','en-US')]
br.addheaders = [('Accept-Encoding','gzip, deflate')]
cj = cookielib.LWPCookieJar()
br.set_cookiejar(cj)
br.open("http://amazon.com")
br.follow_link(text="Today Deals")
print br.response().read()
Кроме того, в "следующем 15" href есть, вероятно, что-то, указывающее разбиение на страницы, например. & Амп; индекс = 15. Если общее количество элементов на всех страницах доступно на первой странице, то:
soup = BeautifulSoup(br.response().read())
totalItems = soup.findAll(id="results-count-total")[0].text
startVar = [x for x in range(int(totalItems)) if x % 15 == 0]
Затем просто перетащите поверх startVar и создайте url, добавьте значение startVar в url, br.open() и очистите данные. Таким образом, вам не нужно программно "найти" "следующую" ссылку на странице и выполнить щелчок по ней, чтобы перейти на следующую страницу - вы уже знаете все допустимые URL-адреса. Минимизация управляемой кодами манипуляции страницей только с необходимыми данными ускорит извлечение.
Ответ 2
Существует два способа сделать это последовательно:
- определяя список
listing_url
в классе.
- определяя
listing_url
внутри parse_listings()
.
Единственное различие - это слово. Кроме того, предположим, что есть пять страниц, чтобы получить listing_urls
. Поэтому поставьте page=1
под класс.
В методе parse_listings
сделайте запрос один раз. Поместите все данные в meta
, которые вам нужно отслеживать. При этом используйте parse_listings
только для анализа "главной страницы".
Как только вы достигнете конца строки, верните свои предметы. Этот процесс является последовательным.
class MySpider(CrawlSpider):
name = 'mysite.com'
allowed_domains = ['mysite.com']
start_url = 'http://www.mysite.com/'
listing_url = []
page = 1
def start_requests(self):
return [Request(self.start_url, meta={'page': page}, callback=self.parse_listings)]
def parse_listings(self, response):
hxs = HtmlXPathSelector(response)
listings = hxs.select('...')
for listing in listings:
il = MySiteLoader(selector=listing)
il.add_xpath('Title', '...')
il.add_xpath('Link', '...')
items = il.load_item()
# populate the listing_url with the scraped URLs
self.listing_url.extend(listing.select('...').extract())
next_page_url = hxs.select('descendant::div[@id="pagination"]/'
'div[@class="next-link"]/a/@href').extract()
# now that the front page is done, move on to the next listing_url.pop(0)
# add the next_page_url to the meta data
return Request(urlparse.urljoin(response.url, self.listing_url.pop(0)),
meta={'page': self.page, 'items': items, 'next_page_url': next_page_url},
callback=self.parse_listing_details)
def parse_listing_details(self, response):
hxs = HtmlXPathSelector(response)
item = response.request.meta['item']
details = hxs.select('...')
il = MySiteLoader(selector=details, item=item)
il.add_xpath('Posted_on_Date', '...')
il.add_xpath('Description', '...')
items = il.load_item()
# check to see if you have any more listing_urls to parse and last page
if self.listing_urls:
return Request(urlparse.urljoin(response.url, self.listing_urls.pop(0)),
meta={'page': self.page, 'items': items, 'next_page_url': response.meta['next_page_url']},
callback=self.parse_listings_details)
elif not self.listing_urls and response.meta['page'] != 5:
# loop back for more URLs to crawl
return Request(urlparse.urljoin(response.url, response.meta['next_page_url']),
meta={'page': self.page + 1, 'items': items},
callback=self.parse_listings)
else:
# reached the end of the pages to crawl, return data
return il.load_item()
Ответ 3
См. ниже обновленный ответ в разделе EDIT 2 (обновлено 6 октября 2017 года)
Есть ли какая-то конкретная причина, по которой вы используете доход? Выход возвращает генератор, который будет возвращать объект Request, когда на него вызывается .next()
.
Измените свои операторы yield
на выражения return
, и все должно работать должным образом.
Вот пример генератора:
In [1]: def foo(request):
...: yield 1
...:
...:
In [2]: print foo(None)
<generator object foo at 0x10151c960>
In [3]: foo(None).next()
Out[3]: 1
EDIT:
Измените функцию def start_requests(self)
, чтобы использовать параметр follow
.
return [Request(self.start_url, callback=self.parse_listings, follow=True)]
ИЗМЕНИТЬ 2:
Начиная с версии Scope v1.4.0, выпущенной в 2017-05-18, теперь рекомендуется использовать response.follow
вместо непосредственного создания объектов scrapy.Request
.
Из примечания к выпуску:
Создает новый метод response.follow для создания запросов; сейчас рекомендуемый способ создания запросов в пауках Scrapy. Этот метод облегчает запись правильных пауков; response.follow имеет несколько преимущества над созданием объектов scrapy.Request напрямую:
- обрабатывает относительные URL-адреса;
- он работает правильно с URL-адресами, отличными от ascii, на страницах, отличных от UTF8;
- в дополнение к абсолютным и относительным URL-адресам, он поддерживает Selectors; для элементов он также может извлекать свои значения href.
Итак, для ОП выше, измените код на:
next_page_url = hxs.select('descendant::div[@id="pagination"]/'
'div[@class="next-link"]/a/@href').extract()
if next_page_url:
yield Request(urlparse.urljoin(response.url, next_page_url[0]),
callback=self.parse_listings)
в
next_page_url = hxs.select('descendant::div[@id="pagination"]/'
'div[@class="next-link"]/a/@href')
if next_page_url is not None:
yield response.follow(next_page_url, self.parse_listings)
Ответ 4
Вы можете получать запросы или элементы столько раз, сколько вам нужно.
def parse_category(self, response):
# Get links to other categories
categories = hxs.select('.../@href').extract()
# First, return CategoryItem
yield l.load_item()
for url in categories:
# Than return request for parse category
yield Request(url, self.parse_category)
Я обнаружил, что здесь - https://groups.google.com/d/msg/scrapy-users/tHAAgnuIPR4/0ImtdyIoZKYJ
Ответ 5
Возможно, вы захотите изучить две вещи.
- Веб-сайт, который вы просматриваете, может блокировать определяемый вами пользовательский агент.
- Попробуйте добавить DOWNLOAD_DELAY к вашему пауку.
Ответ 6
Я просто исправил эту же проблему в своем коде. Я использовал базу данных SQLite3, которая входит в состав Python 2.7, чтобы исправить ее: каждый элемент, который вы собираете информацию, получает свою уникальную строку, помещенную в таблицу базы данных в первом проходе функции синтаксического анализа, и каждый экземпляр обратного вызова синтаксического анализа добавляет каждый данные позиции в таблицу и строку для этого элемента. Храните счетчик экземпляров, чтобы последняя процедура разбора обратного вызова знала, что она последняя, и записывает CSV файл из базы данных или что-то еще. Обратный вызов может быть рекурсивным, когда ему сообщается в мета, где схема синтаксического анализа (и, конечно, какой элемент) была отправлена для работы. Работает для меня как шарм. У вас есть SQLite3, если у вас есть Python. Здесь был мой пост, когда я впервые обнаружил ограничение скрининга в этом отношении:
Является ли асинхронность Scrapy тем, что затрудняет создание моего файла результатов CSV?
Ответ 7
http://autopython.blogspot.com/2014/04/recursive-scraping-using-different.html
В этом примере показано, как отменить несколько страниц с веб-сайта с использованием различных технологий.