网页抓取是一种强大的技术,通过寻找一个或多个域的所有URL来收集网络数据。Python有几个流行的网络抓取库和框架。
在这篇文章中,我们将首先介绍不同的爬行策略和使用案例。然后,我们将使用两个库:request和Beautiful Soup,在Python中从头开始构建一个简单的网络爬虫。接下来,我们将看到为什么使用Scrapy这样的网页抓取框架更好。最后,我们将用Scrapy建立一个爬虫实例,从IMDb收集电影元数据,并看看Scrapy如何扩展到有几百万页的网站。
什么是网络爬虫?
网页抓取和网页爬取是两个不同但相关的概念。网页抓取是网页爬取的一个组成部分,爬行者的逻辑是找到要由爬取者代码处理的URL。
网络爬虫开始时有一个要访问的URL列表,称为种子。对于每个URL,爬虫在HTML中找到链接,根据一些标准过滤这些链接,并将新的链接添加到一个队列中。所有的HTML或一些特定的信息被提取出来,由一个不同的管道进行处理。
网页抓取策略
在实践中,网络爬虫只访问网页的一个子集,这取决于爬虫预算,可以是每个域的最大网页数、深度或执行时间。
大多数受欢迎的网站都提供一个robots.txt文件,以表明网站的哪些区域不允许每个用户代理抓取。与robots文件相对应的是sitemap.xml文件,它列出了可以抓取的网页。
流行的网络爬虫使用案例包括:。
- 搜索引擎(Googlebot、Bingbot、Yandex Bot…)收集了相当一部分网络的所有HTML。这些数据被编入索引,以使其可被搜索。
- SEO分析工具在收集HTML的基础上还收集元数据,如响应时间、响应状态,以检测破损的页面,以及不同域之间的链接,以收集反向链接。
- 价格监测工具爬行电子商务网站,找到产品页面并提取元数据,特别是价格。然后定期重新访问产品页面。
- Common Crawl维护一个开放的网络抓取数据库。例如,2020年10月的档案包含27.1亿个网页。
接下来,我们将比较用Python构建网络爬虫的三种不同策略。首先,只使用标准库,然后是用于发出HTTP请求和解析HTML的第三方库,最后是一个网络爬虫框架。
从头开始用Python构建一个简单的爬虫
要在Python中建立一个简单的网络爬虫,我们至少需要一个库来从URL中下载HTML,以及一个HTML解析库来提取链接。Python 提供了用于发出 HTTP 请求的标准库urllib和用于解析 HTML 的html.parser。在Github上可以找到一个只用标准库构建的Python爬虫实例。
用于请求和HTML解析的标准Python库对开发者不是很友好。其他流行的库,如request,被称为人类的HTTP,和Beautiful Soup,提供了更好的开发者体验。
你可以在本地安装这两个库。
一个基本的爬虫可以按照前面的架构图来构建。
import logging from urllib.parse import urljoin import requests from bs4 import BeautifulSoup logging.basicConfig( format='%(asctime)s %(levelname)s:%(message)s', level=logging.INFO) class Crawler: def __init__(self, urls=[]): self.visited_urls = [] self.urls_to_visit = urls def download_url(self, url): return requests.get(url).text def get_linked_urls(self, url, html): soup = BeautifulSoup(html, 'html.parser') for link in soup.find_all('a'): path = link.get('href') if path and path.startswith('/'): path = urljoin(url, path) yield path def add_url_to_visit(self, url): if url not in self.visited_urls and url not in self.urls_to_visit: self.urls_to_visit.append(url) def crawl(self, url): html = self.download_url(url) for url in self.get_linked_urls(url, html): self.add_url_to_visit(url) def run(self): while self.urls_to_visit: url = self.urls_to_visit.pop(0) logging.info(f'Crawling: {url}') try: self.crawl(url) except Exception: logging.exception(f'Failed to crawl: {url}') finally: self.visited_urls.append(url) if __name__ == '__main__': Crawler(urls=['https://www.imdb.com/']).run()
上面的代码定义了一个Crawler类,它的辅助方法是使用request库的download_url,使用Beautiful Soup库的get_linked_urls和add_url_to_visit来过滤URL。要访问的URL和已访问的URL被存储在两个独立的列表中。你可以在你的终端上运行爬虫。
python crawler.py
爬虫对每个访问过的URL都会记录一行。
2020-12-04 18:10:10,737 INFO:Crawling: https://www.imdb.com/ 2020-12-04 18:10:11,599 INFO:Crawling: https://www.imdb.com/?ref_=nv_home 2020-12-04 18:10:12,868 INFO:Crawling: https://www.imdb.com/calendar/?ref_=nv_mv_cal 2020-12-04 18:10:13,526 INFO:Crawling: https://www.imdb.com/list/ls016522954/?ref_=nv_tvv_dvd 2020-12-04 18:10:19,174 INFO:Crawling: https://www.imdb.com/chart/top/?ref_=nv_mv_250 2020-12-04 18:10:20,624 INFO:Crawling: https://www.imdb.com/chart/moviemeter/?ref_=nv_mv_mpm 2020-12-04 18:10:21,556 INFO:Crawling: https://www.imdb.com/feature/genre/?ref_=nv_ch_gr
代码非常简单,但在成功抓取一个完整的网站之前,有许多性能和可用性问题需要解决。
用Scrapy进行网页抓取
爬虫很慢,不支持并行。从时间戳中可以看出,爬行每个URL大约需要一秒钟。每次爬虫发出请求时,它都在等待请求被解决,中间没有任何工作。
下载URL逻辑没有重试机制,URL队列不是一个真正的队列,在URL数量较多的情况下效率不高。
链接提取逻辑不支持通过删除URL查询字符串参数来规范URL,不处理以#开头的URL,不支持按域名过滤URL或过滤掉对静态文件的请求。
爬虫不识别自己,并忽略了robots.txt文件。
接下来,我们将看到Scrapy是如何提供所有这些功能的,并使它很容易为你的自定义抓取进行扩展。
Scrapy是最流行的网络刮擦和抓取Python框架,在Github上有40k颗星。Scrapy的一个优点是,请求是异步安排和处理的。这意味着Scrapy可以在前一个请求完成之前发送另一个请求,或者在这中间做一些其他工作。Scrapy可以处理许多并发请求,但也可以通过自定义设置来尊重网站,这一点我们将在后面看到。
Scrapy有一个多组件的架构。通常情况下,你至少要实现两个不同的类。Spider和Pipeline。网页抓取可以被认为是一种ETL,你从网络上提取数据并将其加载到你自己的存储中。蜘蛛提取数据,管道将其加载到存储中。转换可以在蜘蛛和管道中发生,但我建议你设置一个自定义的Scrapy管道来独立地转换每个项目。这样一来,处理一个项目的失败对其他项目没有影响。
在所有这些之上,你可以在组件之间添加蜘蛛和下载器中间件。
如果你以前使用过Scrapy,你就知道网络爬虫被定义为一个继承自基础Spider类的类,并实现一个解析方法来处理每个响应。
from scrapy.spiders import Spider class ImdbSpider(Spider): name = 'imdb' allowed_domains = ['www.imdb.com'] start_urls = ['https://www.imdb.com/'] def parse(self, response): pass
Scrapy还提供了几个通用的spider类。CrawlSpider、XMLFeedSpider、CSVFeedSpider和SitemapSpider。CrawlSpider类继承自基础Spider类,并提供一个额外的规则属性来定义如何抓取网站。每个规则都使用一个LinkExtractor来指定从每个页面提取哪些链接。接下来,我们将通过为IMDb(互联网电影数据库)建立一个爬虫来看看如何使用它们中的每一个。
为IMDb建立一个Scrapy爬虫的例子
在尝试抓取IMDb之前,我检查了IMDb robots.txt文件,看看哪些URL路径是允许的。robots文件只不允许所有用户代理的26个路径。Scrapy事先读取了robots.txt文件,并在ROBOTSTXT_OBEY设置为真时尊重它。所有用Scrapy命令startproject生成的项目都是这种情况。
scrapy startproject scrapy_crawler
该命令用默认的Scrapy项目文件夹结构创建一个新项目。
scrapy_crawler/ ├── scrapy.cfg └── scrapy_crawler ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders ├── __init__.py
然后你可以在scrapy_crawler/spiders/imdb.py中创建一个蜘蛛,用一个规则来提取所有链接。
from scrapy.spiders import CrawlSpider, Rule from scrapy.linkextractors import LinkExtractor class ImdbCrawler(CrawlSpider): name = 'imdb' allowed_domains = ['www.imdb.com'] start_urls = ['https://www.imdb.com/'] rules = (Rule(LinkExtractor()),)
你可以在终端启动爬虫程序。
scrapy crawl imdb --logfile imdb.log
你会得到很多日志,包括每个请求的一个日志。探索日志时我注意到,即使我们将allowed_domains设置为只抓取https://www.imdb.com 下的网页,也有对外部域的请求,如amazon.com。
2020-12-06 12:25:18 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://www.amazon.com/b/?&node=5160028011&ref_=ft_iba> from <GET [https://www.imdb.com/whitelist-offsite?url=https%3A%2F%2Fwww.amazon.com%2Fb%2F%3F%26node%3D5160028011%26ref_%3Dft_iba&page-action=ft-iba&ref=ft_iba](https://www.imdb.com/whitelist-offsite?url=https%3A%2F%2Fwww.amazon.com%2Fb%2F%3F%26node%3D5160028011%26ref_%3Dft_iba&page-action=ft-iba&ref=ft_iba)>
IMDb从白名单-网站和白名单下的URL路径重定向到外部域。有一个开放的ScrapyGithub问题显示,当OffsiteMiddleware在RedirectMiddleware之前应用时,外部URL不会被过滤掉。为了解决这个问题,我们可以配置链接提取器,拒绝以两个正则表达式开头的URL。
class ImdbCrawler(CrawlSpider): name = 'imdb' allowed_domains = ['www.imdb.com'] start_urls = ['https://www.imdb.com/'] rules = ( Rule(LinkExtractor( deny=[ re.escape('https://www.imdb.com/offsite'), re.escape('https://www.imdb.com/whitelist-offsite'), ], )), )
规则和LinkExtractor类支持几个参数来过滤掉URLs。例如,你可以忽略特定的URL扩展名,并通过对查询字符串进行排序来减少重复的URL数量。如果你没有找到适合你使用情况的特定参数,你可以向LinkExtractor的process_links或Rule的process_values传递一个自定义函数。
例如,IMDb有两个不同的URL,内容相同。
https://www.imdb.com/name/nm1156914/
https://www.imdb.com/name/nm1156914/?mode=desktop&ref_=m_ft_dsk
为了限制抓取的URL数量,我们可以用w3lib库中的url_query_cleaner函数删除URL中的所有查询字符串,并在process_links中使用它。
from w3lib.url import url_query_cleaner def process_links(links): for link in links: link.url = url_query_cleaner(link.url) yield link class ImdbCrawler(CrawlSpider): name = 'imdb' allowed_domains = ['www.imdb.com'] start_urls = ['https://www.imdb.com/'] rules = ( Rule(LinkExtractor( deny=[ re.escape('https://www.imdb.com/offsite'), re.escape('https://www.imdb.com/whitelist-offsite'), ], ), process_links=process_links), )
现在我们已经限制了要处理的请求的数量,我们可以添加一个parse_item方法来从每个页面中提取数据,并将其传递给一个管道来存储它。例如,我们可以提取整个response.text以在不同的管道中处理,或者选择HTML元数据。为了选择头标签中的HTML元数据,我们可以编写自己的XPATHs,但我发现使用一个库extruct更好,它可以从HTML页面中提取所有元数据。你可以用pip install extract来安装它。
import re from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from w3lib.url import url_query_cleaner import extruct def process_links(links): for link in links: link.url = url_query_cleaner(link.url) yield link class ImdbCrawler(CrawlSpider): name = 'imdb' allowed_domains = ['www.imdb.com'] start_urls = ['https://www.imdb.com/'] rules = ( Rule( LinkExtractor( deny=[ re.escape('https://www.imdb.com/offsite'), re.escape('https://www.imdb.com/whitelist-offsite'), ], ), process_links=process_links, callback='parse_item', follow=True ), ) def parse_item(self, response): return { 'url': response.url, 'metadata': extruct.extract( response.text, response.url, syntaxes=['opengraph', 'json-ld'] ), }
我把follow属性设置为True,这样即使我们提供了一个自定义的解析方法,Scrapy仍然会跟踪每个响应中的所有链接。我还对extruct进行了配置,使其只提取Open Graph元数据和JSON-LD,这是一种在网络中使用JSON对链接数据进行编码的流行方法,被IMDb使用。你可以运行爬虫并将JSON行格式的项目存储到一个文件中。
scrapy crawl imdb --logfile imdb.log -o imdb.jl -t jsonlines
输出文件imdb.jl为每个抓取的项目包含一行。例如,从HTML的标签中提取的一部电影的Open Graph元数据看起来是这样的。
{ "url": "http://www.imdb.com/title/tt2442560/", "metadata": {"opengraph": [{ "namespace": {"og": "http://ogp.me/ns#"}, "properties": [ ["og:url", "http://www.imdb.com/title/tt2442560/"], ["og:image", "https://m.media-amazon.com/images/M/MV5BMTkzNjEzMDEzMF5BMl5BanBnXkFtZTgwMDI0MjE4MjE@._V1_UY1200_CR90,0,630,1200_AL_.jpg"], ["og:type", "video.tv_show"], ["og:title", "Peaky Blinders (TV Series 2013\u2013 ) - IMDb"], ["og:site_name", "IMDb"], ["og:description", "Created by Steven Knight. With Cillian Murphy, Paul Anderson, Helen McCrory, Sophie Rundle. A gangster family epic set in 1900s England, centering on a gang who sew razor blades in the peaks of their caps, and their fierce boss Tommy Shelby."] ] }]} }
单个项目的JSON-LD太长了,不能包含在文章中,这里是Scrapy从script type=”application/ld+json”标签中提取的一个样本。
"json-ld": [ { "@context": "http://schema.org", "@type": "TVSeries", "url": "/title/tt2442560/", "name": "Peaky Blinders", "image": "https://m.media-amazon.com/images/M/MV5BMTkzNjEzMDEzMF5BMl5BanBnXkFtZTgwMDI0MjE4MjE@._V1_.jpg", "genre": ["Crime","Drama"], "contentRating": "TV-MA", "actor": [ { "@type": "Person", "url": "/name/nm0614165/", "name": "Cillian Murphy" }, ... ] ... } ]
[文中代码均来源于Scrapingbee]
在探索日志时,我注意到爬虫的另一个常见问题。通过依次点击过滤器,爬虫生成了内容相同的URL,只是过滤器的应用顺序不同。
https://www.imdb.com/name/nm2900465/videogallery/content_type-trailer/related_titles-tt0479468
https://www.imdb.com/name/nm2900465/videogallery/related_titles-tt0479468/content_type-trailer
长的过滤和搜索URL是一个困难的问题,可以通过Scrapy的设置URLLENGTH_LIMIT限制URL的长度来部分解决。
我以IMDb为例,展示了用Python构建网络爬虫的基本原理。我没有让爬虫运行很长时间,因为我对这些数据没有具体的用例。如果你需要来自IMDb的特定数据,你可以查看IMDb Datasets项目,它提供了IMDb数据的每日输出,以及IMDbPY,一个用于检索和管理数据的Python包。
规模化的网页抓取
如果您试图抓取像IMDb这样的大网站,根据谷歌的数据,该网站有超过4500万个页面,那么通过配置以下设置,负责任地进行抓取是很重要的。你可以在BOT_NAME设置中识别你的爬虫,并提供详细的联系方式。为了限制你对网站服务器的压力,你可以增加DOWNLOAD_DELAY,限制CONCURRENT_REQUESTS_PER_DOMAIN,或者设置AUTOTHROTTLE_ENABLED,它将根据服务器的响应时间动态地调整这些设置。
请注意,Scrapy的抓取默认是针对单个域进行优化的。如果您要抓取多个域,请检查这些设置以优化广泛的抓取,包括将默认的抓取顺序从深度优先改为呼吸优先。为了限制你的抓取预算,你可以用close spider扩展的CLOSESPIDER_PAGECOUNT设置来限制请求的数量。
在默认设置下,对于像IMDb这样的网站,Scrapy每分钟抓取大约600个页面。要抓取4500万个网页,一个机器人需要50多天的时间。如果你需要抓取多个网站,最好为每个大网站或网站组启动单独的爬虫。如果你对分布式网络抓取感兴趣,你可以阅读一个开发者如何使用20个亚马逊EC2机器实例在40小时内用Python抓取了2.5亿个网页。
在某些情况下,你可能会遇到需要你执行JavaScript代码来呈现所有HTML的网站。如果不这样做,你就可能无法收集到网站上的所有链接。
总 结
我们将使用第三方库下载URL和解析HTML的Python爬虫的代码与使用流行的网络爬虫框架构建的爬虫进行了比较。Scrapy是一个性能非常好的网页抓取框架,而且很容易用你的自定义代码进行扩展。但你需要知道所有可以钩住你自己代码的地方,以及每个组件的设置。
在抓取有数百万页的网站时,正确配置Scrapy变得更加重要。如果你想了解更多关于网络抓取的信息,我建议你挑选一个受欢迎的网站,并尝试抓取它。你肯定会遇到新的问题,这让这个话题变得很有吸引力!