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如何帮助节省时间和建立更可维护的抓取器。