in

使用Scrapy轻松进行网页爬取

Scrapy是一个很棒的开源Python网页爬取框架。它可以处理在大规模进行网络爬行时最常见的使用情况。

  • 多线程
  • 爬取(从一个链接到另一个链接)
  • 提取数据
  • 验证
  • 保存到不同的格式/数据库
  • 更多

Scrapy与其他常用的librairies(如Requests/BeautifulSoup)的主要区别在于它是有意见的。它允许你以一种优雅的方式解决通常的网络爬取问题。

Scrapy的缺点是学起来不那么容易上手,有很多东西需要学习,但这正是我们的目的。

在本教程中,我们将创建两个不同的网络爬取器,一个是简单的,从电子商务产品页面提取数据,另一个是更 “复杂 “的,将爬取整个电子商务目录


基本概述

你可以用pip安装Scrapy。不过要注意,Scrapy的文档强烈建议将其安装在一个专门的虚拟环境中,以避免与你的系统包发生冲突。

我正在使用Virtualenv和Virtualenvwrapper:

现在你可以用这个命令创建一个新的Scrapy项目。

这将为该项目创建所有必要的模板文件。

下面是这些文件和文件夹的简要概述。

  • items.py是一个提取数据的模型。你可以定义自定义模型(比如产品),它将继承scrapy的Item类。
  • middlewares.py用来改变请求/响应生命周期的中间件。例如,你可以创建一个中间件来轮换用户代理,或者使用像ScrapingBee这样的API而不是自己做请求。
  • pipelines.py在Scrapy中,管道被用来处理提取的数据,清理HTML,验证数据,并将其导出为自定义格式或保存到数据库。
  • /spiders是一个包含Spider类的文件夹。在Scrapy中,Spider是定义如何爬取网站的类,包括跟踪什么链接以及如何提取这些链接的数据。
  • scrapy.cfg是一个配置文件,用于改变一些设置。

爬取单一产品

在这个例子中,我们将从一个假的电子商务网站上刮取一个产品。这里是我们要爬取的第一个产品。

https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/我们将提取产品的名称、图片、价格和描述。


Scrapy外壳

Scrapy有一个内置的shell,可以帮助你实时地尝试和调试你的爬取代码。你可以用它快速测试你的XPath表达式/CSS选择器。这是一个非常酷的工具,可以用来编写你的网络爬取程序,我一直在使用它

你可以配置Scrapy Shell来使用另一个控制台,而不是像IPython那样的默认Python控制台。你会得到自动完成和其他不错的好处,比如彩色输出。

为了在你的Scrapy Shell中使用它,你需要在你的scrapy.cfg文件中添加这一行。

shell = ipython

一旦配置好了,你就可以开始使用scrapy shell了。

$ scrapy shell --nolog
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    
[s]   item       {}
[s]   settings   
[s] Useful shortcuts:
[s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser
In [1]:

我们可以通过简单地开始获取一个URL。

fetch('https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/')

这将从获取/robot.txt文件开始。

[scrapy.core.engine] DEBUG: Crawled (404)  (referer: None)

在这种情况下,没有任何robot.txt,这就是为什么我们可以看到404 HTTP代码。如果有robot.txt,默认情况下Scrapy会遵循这个规则。

你可以通过改变settings.py中的这个设置来禁用这种行为。

然后你应该有一个像这样的日志。

[scrapy.core.engine] DEBUG: Crawled (200)  (referer: None)

现在你可以看到你的响应对象、响应标题,并尝试不同的XPath表达式/CSS选择器来提取你想要的数据。

你可以在你的浏览器中直接看到响应:

view(response)

请注意,由于许多不同的原因,该页面在你的浏览器中会呈现得很糟糕。这可能是 CORS 问题,Javascript 代码没有执行,或者是资产的相对 URL 在本地无法使用。

scrapy shell就像一个普通的Python shell,所以不要犹豫,在里面加载你喜欢的脚本/函数。


提取数据

Scrapy默认不执行任何Javascript,所以如果你要爬取的网站使用的是Angular/React.js这样的前端框架,你在访问你想要的数据时可能会遇到麻烦。

现在让我们试试用XPath表达式来提取产品的标题和价格。

为了提取价格,我们将使用一个XPath表达式,我们将选择div后的第一个span,其类别为my-4

In [16]: response.xpath("//div[@class='my-4']/span/text()").get()
Out[16]: '20.00$'

我也可以使用一个CSS选择器。

In [21]: response.css('.my-4 span::text').get()
Out[21]: '20.00$'

创建一个Scrapy蜘蛛

在Scrapy中,Spider是一个类,在这里你可以定义你的爬行(需要抓取哪些链接/URL)和抓取(提取什么)行为。

下面是一个蜘蛛用来抓取网站的不同步骤。

它从查看类属性start_urls开始,用start_requests()方法调用这些URLs。如果你需要改变HTTP动词,在请求中添加一些参数(例如,发送一个POST请求而不是GET),你可以覆盖这个方法。
然后它将为每个URL生成一个Request对象,并将响应发送到回调函数parse()
parse()方法将提取数据(在我们的例子中,产品价格、图片、描述、标题)并返回一个字典、一个Item对象、一个Request或一个iterable。
你可能想知道为什么parse方法可以返回这么多不同的对象。这是为了灵活性。比方说,你想爬取一个没有网站地图的电子商务网站。你可以从爬取产品类别开始,所以这将是第一个解析方法。

这个方法将产生一个Request对象给每个产品类别,然后给一个新的回调方法parse2(),对于每个类别,你需要处理分页,然后对于每个产品的实际爬取,产生一个Item,所以是第三个parse函数。

使用Scrapy,你可以将爬取到的数据作为一个简单的Python字典返回,但使用内置的ScrapyItem类是个好主意。 它是我们爬取到的数据的一个简单容器,Scrapy将查看这个项目的字段,以便将数据导出为不同的格式(JSON / CSV…),项目管道等等。

所以这里有一个基本的产品类。

import scrapy

class Product(scrapy.Item):
    product_url = scrapy.Field()
    price = scrapy.Field()
    title = scrapy.Field()
    img_url = scrapy.Field()

现在我们可以生成一个spider,可以用命令行帮助器。

scrapy genspider myspider mydomain.com

或者你可以手动操作,把你的蜘蛛代码放在/spiders目录下。

Scrapy中有不同类型的Spider,以解决最常见的网络刮擦用例。

我们将使用的Spider 。它需要一个 start_urls 列表,并通过一个解析方法对每个列表进行抓取。
CrawlSpider跟踪由一组规则定义的链接。
SitemapSpider提取网站地图中定义的URLs
还有很多

# -*- coding: utf-8 -*-
import scrapy

from product_scraper.items import Product

class EcomSpider(scrapy.Spider):
    name = 'ecom_spider'
    allowed_domains = ['clever-lichterman-044f16.netlify.com']
    start_urls = ['https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/']

    def parse(self, response):
        item = Product()
        item['product_url'] = response.url
        item['price'] = response.xpath("//div[@class='my-4']/span/text()").get()
        item['title'] = response.xpath('//section[1]//h2/text()').get()
        item['img_url'] = response.xpath("//div[@class='product-slider']//img/@src").get(0)
        return item

在这个EcomSpider类中,有两个必要的属性。

name,这是我们的蜘蛛的名字(你可以用scrapy runspider spider_name来运行)。
start_urls,这是开始的URL
allowed_domains是可选的,但当你使用的CrawlSpider可能会跟踪不同域名上的链接时,这个属性很重要。

然后我通过使用XPath表达式提取我想要的数据来填充产品字段,正如我们之前看到的那样,我们返回项目。

你可以按以下方式运行这段代码,将结果导出为JSON(你也可以导出为CSV)。

scrapy runspider ecom_spider.py -o product.json

然后你应该得到一个漂亮的JSON文件。

[
  {
    "product_url": "https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/",
    "price": "20.00$",
    "title": "Taba Cream",
    "img_url": "https://clever-lichterman-044f16.netlify.com/images/products/product-2.png"
  }
]

项目加载器

在从网络上提取数据时,有两个常见的问题。

对于同一个网站,页面布局和底层HTML可能是不同的。如果你爬取一个电子商务网站,往往会有一个正常价格和一个折扣价格,有不同的XPath / CSS选择器。
这些数据可能很脏,需要进行某种后期处理,对于电子商务网站来说,可能是价格的显示方式,例如(1.00美元、1美元、1,00美元)。
Scrapy为此提供了一个内置的解决方案,即ItemLoaders。 这是一个有趣的方法来填充我们的产品对象。

你可以向同一个Item字段添加几个XPath表达式,它将依次测试。默认情况下,如果发现几个XPath,它将把它们全部加载到一个列表中。

你可以在Scrapy文档中找到许多输入和输出处理程序的例子。

当你需要对你提取的数据进行转换/清理时,它真的很有用。 例如,从一个价格中提取货币,将一个单位转换为另一个单位(米中的厘米,华氏的摄氏度)……

在我们的网页中,我们可以通过不同的XPath表达式找到产品的标题。//title和//section[1]//h2/text()

下面是你在这种情况下如何使用和Itemloader。

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath('price', "//div[@class='my-4']/span/text()")
    l.add_xpath('title', '//section[1]//h2/text()')
    l.add_xpath('title', '//title')
    l.add_value('product_url', response.url)
    return l.load_item()

一般来说,你只想要第一个匹配的XPath,所以你需要把这个output_processor=TakeFirst()添加到项目的字段构造器中。

在我们的例子中,我们只想要每个字段的第一个匹配的XPath,所以更好的方法是创建我们自己的ItemLoader并声明一个默认的output_processor来获取第一个匹配的XPath。

from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join

def remove_dollar_sign(value):
    return value.replace('$', '')

class ProductLoader(ItemLoader):
    default_output_processor = TakeFirst()
    price_in = MapCompose(remove_dollar_sign)

我还添加了一个price_in,这是一个输入处理器,用来删除价格中的美元符号。我使用的是MapCompose,它是一个内置的处理器,可以取一个或几个函数来依次执行。你可以为.NET添加任意多的函数。惯例是在你的Item字段的名称中添加_in或_out来为它添加一个输入或输出处理器。

还有很多处理器,你可以在文档中了解更多的信息


爬取多个页面

现在我们知道了如何爬取单个页面,现在是时候学习如何爬取多个页面了,比如整个产品目录。 正如我们之前看到的,有不同种类的蜘蛛。

当你想爬取整个产品目录时,你首先要看的是一个网站地图。网站地图正是为此而建的,用来向网络爬虫展示网站的结构。

大多数情况下,你可以在base_url/sitemap.xml中找到一个。解析一个网站地图可能很麻烦,同样,Scrapy可以帮助你解决这个问题。

在我们的例子中,你可以在这里找到网站地图:https://clever-lichterman-044f16.netlify.com/sitemap.xml

如果我们看一下网站地图里面,有很多我们不感兴趣的URL,比如主页、博客文章等等。

  
  https://clever-lichterman-044f16.netlify.com/blog/post-1/
  
  2019-10-17T11:22:16+06:00


  
  https://clever-lichterman-044f16.netlify.com/products/
  
  2019-10-17T11:22:16+06:00


  
  https://clever-lichterman-044f16.netlify.com/products/taba-cream.1/
  
  2019-10-17T11:22:16+06:00

幸运的是,我们可以过滤URLs,只解析那些符合某种模式的URLs,这真的很简单,在这里我们只需要在URLs中包含/products/。

class SitemapSpider(SitemapSpider):
    name = "sitemap_spider"
    sitemap_urls = ['https://clever-lichterman-044f16.netlify.com/sitemap.xml']
    sitemap_rules = [
        ('/products/', 'parse_product')
    ]

    def parse_product(self, response):
        # ... scrape product ...

你可以按照下面的方法运行这个spider来抓取所有的产品并将结果导出到CSV文件中:scrapy runspider sitemap_spider.py -o output.csv

现在,如果网站没有任何网站地图怎么办?Scrapy又一次为这个问题提供了解决方案!

让我向你介绍…CrawlSpider。

CrawlSpider将从一个start_urls列表开始抓取目标网站。在我们的例子中,这很简单,产品有相同的URL模式/products/product_title,所以我们只需要过滤这些URL。

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from product_scraper.productloader import ProductLoader
from product_scraper.items import Product

class MySpider(CrawlSpider):
    name = 'crawl_spider'
    allowed_domains = ['clever-lichterman-044f16.netlify.com']
    start_urls = ['https://clever-lichterman-044f16.netlify.com/products/']

    rules = (
        
        Rule(LinkExtractor(allow=('products', )), callback='parse_product'),
    )

    def parse_product(self, response):
      # .. parse product
[代码源自Scrapingbee]

正如你所看到的,所有这些内置的Spiders都非常容易使用。如果从头开始做的话,就会复杂得多。

有了Scrapy,你就不必考虑爬行逻辑,比如把新的URL加入队列,跟踪已经解析过的URL,多线程等。


总    结

在这篇文章中,我们大致了解了如何用Scrapy来抓取网络,以及它如何解决你最常见的网络抓取难题。当然,我们只是触及到了表面,还有很多有趣的东西需要探索,比如中间件、导出器、扩展、管道等

如果你一直在用BeautifulSoup/Requests等工具 “手动 “进行网页抓取,就很容易理解Scrapy如何帮助节省时间和建立更可维护的抓取器。

What do you think?

68

Written by 砖家

68web团队是一支专注于跨境业务和数据获取的专业团队。我们致力于帮助企业成功出海,通过高效的数据爬取服务,为客户提供精准的数据支持;

凭借丰富的经验和专业的技术,我们不仅提供多语言网站建设,还包括国际市场推广和定制化的跨境电商解决方案;

我们的数据爬取平台利用强大的服务器和代理IP,确保获取高质量的数据,以满足客户在AI和大数据时代的需求。我们专注于提供全面的解决方案,助力企业在全球市场上取得成功。

使用Java的Chrome 无头模式简介

如何用Scrapy爬取JavaScript生成的页面?