如何从随机网站上刮掉所有产品?

时间:2017-12-28 22:25:46

标签: python python-3.x web-scraping lxml

我试图从this website获得所有产品,但不知怎的,我不认为我选择了最好的方法,因为其中一些缺失了,我无法弄清楚原因。这不是我第一次遇到困难时。

我现在这样做的方式是这样的:

  • 转到网站的index page
  • 从那里获得所有类别(A-Z 0-9)
  • 访问上述各个类别,并从那里递归查看所有子类别,直到我到达产品页面
  • 当我到达产品页面时,检查产品是否有更多SKU。如果有,请获取链接。否则,这是唯一的SKU。

现在,下面的代码可以运行,但它并没有获得所有产品,我也没有看到为什么它会跳过一些产品的原因。也许我接近一切的方式是错误的。

from lxml import html
from random import randint
from string import ascii_uppercase
from time import sleep
from requests import Session


INDEX_PAGE = 'https://www.richelieu.com/us/en/index'
session_ = Session()


def retry(link):
    wait = randint(0, 10)
    try:
        return session_.get(link).text
    except Exception as e:
        print('Retrying product page in {} seconds because: {}'.format(wait, e))
        sleep(wait)
        return retry(link)


def get_category_sections():
    au = list(ascii_uppercase)
    au.remove('Q')
    au.remove('Y')
    au.append('0-9')
    return au


def get_categories():
    html_ = retry(INDEX_PAGE)
    page = html.fromstring(html_)
    sections = get_category_sections()

    for section in sections:
        for link in page.xpath("//div[@id='index-{}']//li/a/@href".format(section)):
            yield '{}?imgMode=m&sort=&nbPerPage=200'.format(link)


def dig_up_products(url):
    html_ = retry(url)
    page = html.fromstring(html_)

    for link in page.xpath(
            '//h2[contains(., "CATEGORIES")]/following-sibling::*[@id="carouselSegment2b"]//li//a/@href'
    ):
        yield from dig_up_products(link)

    for link in page.xpath('//ul[@id="prodResult"]/li//div[@class="imgWrapper"]/a/@href'):
        yield link

    for link in page.xpath('//*[@id="ts_resultList"]/div/nav/ul/li[last()]/a/@href'):
        if link != '#':
            yield from dig_up_products(link)


def check_if_more_products(tree):
    more_prods = [
        all_prod
        for all_prod in tree.xpath("//div[@id='pm2_prodTableForm']//tbody/tr/td[1]//a/@href")
    ]
    if not more_prods:
        return False
    return more_prods


def main():
    for category_link in get_categories():
        for product_link in dig_up_products(category_link):
            product_page = retry(product_link)
            product_tree = html.fromstring(product_page)
            more_products = check_if_more_products(product_tree)
            if not more_products:
                print(product_link)
            else:
                for sku_product_link in more_products:
                    print(sku_product_link)


if __name__ == '__main__':
    main()

现在,这个问题可能过于笼统,但我想知道当有人想要从网站获取所有数据(在这种情况下是产品)时,是否遵循经验法则。有人可以带我完成发现这样一个场景的最佳方法吗?

3 个答案:

答案 0 :(得分:5)

如果您的最终目标是为每个类别划分整个产品列表,那么在索引页面上定位每个类别的完整产品列表可能是有意义的。此程序使用BeautifulSoup查找索引页面上的每个类别,然后遍历每个类别下的每个产品页面。最终输出是namedtuple个故事的列表,每个类别名称包含当前页面链接以及每个链接的完整产品标题:

url = "https://www.richelieu.com/us/en/index"
import urllib
import re
from bs4 import BeautifulSoup as soup
from collections import namedtuple
import itertools
s = soup(str(urllib.urlopen(url).read()), 'lxml')
blocks = s.find_all('div', {'id': re.compile('index\-[A-Z]')})
results_data = {[c.text for c in i.find_all('h2', {'class':'h1'})][0]:[b['href'] for b in i.find_all('a', href=True)] for i in blocks}
final_data = []
category = namedtuple('category', 'abbr, link, products')
for category1, links in results_data.items():
   for link in links:
      page_data = str(urllib.urlopen(link).read())
      print "link: ", link
      page_links = re.findall(';page\=(.*?)#results">(.*?)</a>', page_data)
      if not page_links:
         final_page_data = soup(page_data, 'lxml')
         final_titles = [i.text for i in final_page_data.find_all('h3', {'class':'itemHeading'})]
         new_category = category(category1, link, final_titles)
         final_data.append(new_category)

      else:
         page_numbers = set(itertools.chain(*list(map(list, page_links))))

         full_page_links = ["{}?imgMode=m&sort=&nbPerPage=48&page={}#results".format(link, num) for num in page_numbers]
         for page_result in full_page_links:
            new_page_data = soup(str(urllib.urlopen(page_result).read()), 'lxml')
            final_titles = [i.text for i in new_page_data.find_all('h3', {'class':'itemHeading'})]
            new_category = category(category1, link, final_titles)
            final_data.append(new_category)

print final_data

输出将以以下格式获得结果:

[category(abbr=u'A', link='https://www.richelieu.com/us/en/category/tools-and-shop-supplies/workshop-accessories/tool-accessories/sander-accessories/1058847', products=[u'Replacement Plate for MKT9924DB Belt Sander', u'Non-Grip Vacuum Pads', u'Sandpaper Belt 2\xbd " x 14" for Compact Belt Sander PC371 or PC371K', u'Stick-on Non-Vacuum Pads', u'5" Non-Vacuum Disc Pad Hook-Face', u'Sanding Filter Bag', u'Grip-on Vacuum Pads', u'Plates for Non-Vacuum (Grip-On) Dynabug II Disc Pads - 7.62 cm x 10.79 cm (3" x 4-1/4")', u'4" Abrasive for Finishing Tool', u'Sander Backing Pad for RO 150 Sander', u'StickFix Sander Pad for ETS 125 Sander', u'Sub-Base Pad for Stocked Sanders', u'(5") Non-Vacuum Disc Pad Vinyl-Face', u'Replacement Sub-Base Pads for Stocked Sanders', u"5'' Multi-Hole Non-Vaccum Pad", u'Sander Backing Pad for RO 90 DX Sander', u'Converting Sanding Pad', u'Stick-On Vacuum Pads', u'Replacement "Stik It" Sub Base', u'Drum Sander/Planer Sandpaper'])....

要访问每个属性,请按以下方式调用:

categories = [i.abbr for i in final_data]
links = [i.links for i in final_data]
products = [i.products for i in final_data]

我认为使用BeautifulSoup的好处是这个例子是它提供了更高级别的控制,并且很容易修改。例如,如果OP改变了他想要抓住的产品/指数的哪些方面,只需要find_all参数的简单变化,因为上面代码的一般结构以每个产品为中心索引页面中的类别。

答案 1 :(得分:3)

首先,对于如何知道已经抓取的数据是否是所有可用数据的一般性问题,没有明确的答案。这至少是网站特定的,很少实际披露。此外,数据本身可能非常动态。在此网站上,您可能会或多或少地使用产品计数器来验证找到的结果数量:

enter image description here

这里最好的选择是调试 - 使用logging模块在​​抓取时打印出信息,然后分析日志并查找丢失产品的原因以及导致错误的原因

我目前的一些想法:

  • 可能是retry()是有问题的部分 - 可能是session_.get(link).text没有引发错误但是也不包含响应中的实际数据吗?
  • 我认为您提取类别链接的方式是正确的,我不会在索引页面上看到您缺少类别
  • dig_up_products()有问题:当你提取到子类别的链接时,你在XPath表达式中使用了这个carouselSegment2b id,但我在至少一些页面上看到了这一点(比如{{ 3}})id值为carouselSegment1b。无论如何,我可能只会//h2[contains(., "CATEGORIES")]/following-sibling::div//li//a/@href在这里
  • 我也不喜欢imgWrapper用于查找产品链接的课程(可能会丢失丢失图像的产品?)。为什么不只是://ul[@id="prodResult"]/li//a/@href - 这会带来一些重复,你可以单独解决。但是,您也可以在&#34; info&#34;中查找链接。产品容器的一部分://ul[@id="prodResult"]/li//div[contains(@class, "infoBox")]//a/@href

还可能部署了反僵尸,反网络抓取策略,可能暂时禁止您的IP或/和用户代理甚至模糊响应。检查一下。

答案 2 :(得分:2)

正如@mzjn和@alecxe所指出的,一些网站采用了反刮措施。为了掩盖他们的意图,刮刀应该试图模仿一个人类访客。

网站检测刮刀的一种特殊方法是测量后续页面请求之间的时间。这就是为什么刮刀通常在请求之间保持(随机)延迟的原因。

此外,锤击一个不属于你的网络服务器而不给它一些松懈,不被认为是好礼仪。

来自Scrapy's documentation

  

RANDOMIZE_DOWNLOAD_DELAY

     

默认值:True

     

如果启用,Scrapy会在从同一网站获取请求时等待一段随机时间(0.5 * DOWNLOAD_DELAY1.5 * DOWNLOAD_DELAY之间)。

     

此随机化降低了分析请求在其请求之间寻找统计上显着相似性的请求被检测(并随后被阻止)的机会。

     

随机化策略与wget --random-wait选项使用的相同。

     

如果DOWNLOAD_DELAY为零(默认),则此选项无效。

哦,确保HTTP请求中的User-Agent字符串类似于普通的Web浏览器。

进一步阅读: