在本章中,我们将介绍:
- 重试失败的页面下载
- 支持页面重定向
- 正在等待内容在 Selenium 中可用
- 将爬网限制为单个域
- 处理无限滚动页面
- 控制爬行的深度
- 控制爬行的长度
- 处理分页网站
- 处理表单和基于表单的授权
- 处理基本授权
- 通过代理刮擦防止禁令
- 随机化用户代理
- 缓存响应
开发一个可靠的刮板机绝非易事,有太多的假设我们需要考虑。如果网站宕机怎么办?如果响应返回意外数据怎么办?如果您的 IP 被限制或阻止怎么办?如果需要身份验证怎么办?虽然我们永远无法预测和涵盖所有的假设,但我们将讨论一些常见的陷阱、挑战和解决方法。
请注意,一些食谱需要访问我作为 Docker 容器提供的网站。与我们在前面章节中使用的简单静态站点相比,它们需要更多的逻辑。因此,您需要使用以下 Docker 命令拉入并运行 Docker 容器:
docker pull mheydt/pywebscrapecookbook
docker run -p 5001:5001 pywebscrapecookbook
通过使用重试中间件,Scrapy 可以轻松处理失败的页面请求。安装后,Scrapy 将在收到以下 HTTP 错误代码时重试:
[500, 502, 503, 504, 408]
可使用以下参数进一步配置该过程:
RETRY_ENABLED
(真/假-默认为真)RETRY_TIMES
(#对任何错误重试的次数-默认值为 2)RETRY_HTTP_CODES
(应重试的 HTTP 错误代码列表-默认为[500502503500408])
06/01_scrapy_retry.py
脚本演示如何配置 Scrapy 进行重试。脚本文件包含以下 Scrapy 配置:
process = CrawlerProcess({
'LOG_LEVEL': 'DEBUG',
'DOWNLOADER_MIDDLEWARES':
{
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 500
},
'RETRY_ENABLED': True,
'RETRY_TIMES': 3
})
process.crawl(Spider)
process.start()
当爬行器运行时,Scrapy 将按照指定选择重试配置。当遇到错误时,Scrapy 将在放弃之前重试最多三次。
Scrapy 中的页面重定向使用重定向中间件进行处理,该中间件在默认情况下处于启用状态。可使用以下参数进一步配置该过程:
REDIRECT_ENABLED
:(真/假-默认为真)REDIRECT_MAX_TIMES
:(任何单个请求可遵循的最大重定向数-默认值为 20)
06/02_scrapy_redirects.py
中的脚本演示了如何配置 Scrapy 来处理重定向。这将为任何页面配置最多两个重定向。运行脚本将读取 NASA 站点地图并爬网该内容。这包含大量重定向,其中许多是从 HTTP 重定向到 URL 的 HTTPS 版本。将有很多输出,但以下几行演示了输出:
Parsing: <200 https://www.nasa.gov/content/earth-expeditions-above/>
['http://www.nasa.gov/content/earth-expeditions-above', 'https://www.nasa.gov/content/earth-expeditions-above']
此特定 URL 是在一次重定向后处理的,从 HTTP 重定向到 URL 的 HTTPS 版本。该列表定义了重定向中涉及的所有 URL。
您还可以在输出页面中查看重定向超过指定级别(2)的位置。以下是一个示例:
2017-10-22 17:55:00 [scrapy.downloadermiddlewares.redirect] DEBUG: Discarding <GET http://www.nasa.gov/topics/journeytomars/news/index.html>: max redirections reached
卡盘的定义如下:
class Spider(scrapy.spiders.SitemapSpider):
name = 'spider'
sitemap_urls = ['https://www.nasa.gov/sitemap.xml']
def parse(self, response):
print("Parsing: ", response)
print (response.request.meta.get('redirect_urls'))
这与我们之前基于 NASA 站点地图的爬虫程序相同,增加了一行打印redirect_urls
。在对parse
的任何调用中,此元数据将包含访问此页面时发生的所有重定向。
爬网过程由以下代码配置:
process = CrawlerProcess({
'LOG_LEVEL': 'DEBUG',
'DOWNLOADER_MIDDLEWARES':
{
"scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 500
},
'REDIRECT_ENABLED': True,
'REDIRECT_MAX_TIMES': 2
})
默认情况下启用重定向,但这会将最大重定向数设置为 2,而不是默认值 20。
动态 web 页面的一个常见问题是,即使在加载了整个页面之后,因此 Selenium 中的get()
方法已经返回,但仍然可能有我们需要稍后访问的内容,因为来自页面的未完成 Ajax 请求仍在等待完成。例如,需要单击一个按钮,但在加载后所有数据都异步加载到页面之前,该按钮不会启用。
以以下页面为例:http://the-internet.herokuapp.com/dynamic_loading/2 。此页面很快完成加载,并显示一个开始按钮:
The Start button presented on screen
按下按钮时,我们会看到一个进度条,持续五秒钟:
The status bar while waiting
完成后,我们将看到 Hello World!
After the page is completely rendered
现在,假设我们想要刮取这个页面,以获得只有在按下按钮和等待之后才暴露的内容?我们如何做到这一点?
我们可以使用硒来实现这一点。我们将使用 Selenium 的两个特性。首先是能够单击页面元素。第二个功能是等待页面上具有特定 ID 的元素可用。
- 首先,我们得到按钮并点击它。按钮的 HTML 如下所示:
<div id='start'>
<button>Start</button>
</div>
- 按下按钮并完成加载后,将向文档中添加以下 HTML:
<div id='finish'>
<h4>Hello World!"</h4>
</div>
- 我们将使用 Selenium 驱动程序找到开始按钮,单击它,然后等待 ID 为
'finish'
的div
可用。然后我们得到该元素并返回所附的<h4>
标记中的文本。
您可以通过运行06/03_press_and_wait.py
来尝试,其输出如下:
clicked
Hello World!
现在让我们看看它是如何工作的。
让我们详细分析一下解释:
- 我们首先从 Selenium 导入所需的项目:
from selenium import webdriver
from selenium.webdriver.support import ui
- 现在我们加载驱动程序和页面:
driver = webdriver.PhantomJS()
driver.get("http://the-internet.herokuapp.com/dynamic_loading/2")
- 加载页面后,我们可以检索按钮:
button = driver.find_element_by_xpath("//*/div[@id='start']/button")
- 然后我们可以点击按钮:
button.click()
print("clicked")
- 接下来我们创建一个
WebDriverWait
对象:
wait = ui.WebDriverWait(driver, 10)
- 使用此对象,我们可以请求 Selenium 的 UI 等待某些事件。这还设置了 10 秒的最大等待时间。现在使用这个,我们可以等到我们达到一个标准;使用以下 XPath 可以识别元素:
wait.until(lambda driver: driver.find_element_by_xpath("//*/div[@id='finish']"))
- 完成后,我们可以检索 h4 元素并获取其封闭文本:
finish_element=driver.find_element_by_xpath("//*/div[@id='finish']/h4")
print(finish_element.text)
我们可以通知 Scrapy 将爬网限制为仅限于指定域集中的页面。这是一项重要的任务,因为链接可以指向 web 上的任何位置,我们通常希望控制爬行的终点。Scrapy 使这很容易做到。所有需要做的就是设置 scraper 类的allowed_domains
字段。
本例的代码为06/04_allowed_domains.py
,您可以使用 Python 解释器运行脚本。它将执行并产生大量的输出,但如果你关注它,你会发现它只处理 nasa.gov 上的页面
代码与之前的 NASA 站点爬虫程序相同,只是我们包括了allowed_domains=['nasa.gov']
:
class Spider(scrapy.spiders.SitemapSpider):
name = 'spider'
sitemap_urls = ['https://www.nasa.gov/sitemap.xml']
allowed_domains=['nasa.gov']
def parse(self, response):
print("Parsing: ", response)
美国宇航局的网站与它的根域是相当一致的,但偶尔也会链接到其他网站,如 boeing.com 上的内容。此代码将阻止移动到这些外部站点。
许多网站已经用无限滚动机制取代了“上一页/下一页”分页按钮。这些网站使用这种技术在用户到达页面底部时加载更多数据。正因为如此,通过跟随“下一页”链接进行爬行的策略就会崩溃。
虽然这似乎是使用浏览器自动化来模拟滚动的一种情况,但实际上很容易找出网页的 Ajax 请求,并使用这些请求来进行爬行,而不是实际的页面。让我们以spidyquotes.herokuapp.com/scroll
为例。
打开http://spidyquotes.herokuapp.com/scroll 在您的浏览器中。当您滚动到页面底部时,此页面将加载其他内容:
Screenshot of the quotes to scrape
打开页面后,进入开发者工具并选择网络面板。然后,滚动到页面底部。您将在网络面板中看到新内容:
Screenshot of the developer tools options
当我们单击其中一个链接时,可以看到以下 JSON:
{
"has_next": true,
"page": 2,
"quotes": [{
"author": {
"goodreads_link": "/author/show/82952.Marilyn_Monroe",
"name": "Marilyn Monroe",
"slug": "Marilyn-Monroe"
},
"tags": ["friends", "heartbreak", "inspirational", "life", "love", "sisters"],
"text": "\u201cThis life is what you make it...."
}, {
"author": {
"goodreads_link": "/author/show/1077326.J_K_Rowling",
"name": "J.K. Rowling",
"slug": "J-K-Rowling"
},
"tags": ["courage", "friends"],
"text": "\u201cIt takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.\u201d"
},
这很好,因为我们所需要做的就是不断生成对/api/quotes?page=x
的请求,增加x
,直到has_next
标记出现在回复文档中。如果没有更多页面,则此标记将不在文档中。
06/05_scrapy_continuous.py
文件包含一个 Scrapy 代理,它对这组页面进行抓取。使用 Python 解释器运行它,您将看到类似于以下内容的输出(以下是输出的多个摘录):
<200 http://spidyquotes.herokuapp.com/api/quotes?page=2>
2017-10-29 16:17:37 [scrapy.core.scraper] DEBUG: Scraped from <200 http://spidyquotes.herokuapp.com/api/quotes?page=2>
{'text': "“This life is what you make it. No matter what, you're going to mess up sometimes, it's a universal truth. But the good part is you get to decide how you're going to mess it up. Girls will be your friends - they'll act like it anyway. But just remember, some come, some go. The ones that stay with you through everything - they're your true best friends. Don't let go of them. Also remember, sisters make the best friends in the world. As for lovers, well, they'll come and go too. And baby, I hate to say it, most of them - actually pretty much all of them are going to break your heart, but you can't give up because if you give up, you'll never find your soulmate. You'll never find that half who makes you whole and that goes for everything. Just because you fail once, doesn't mean you're gonna fail at everything. Keep trying, hold on, and always, always, always believe in yourself, because if you don't, then who will, sweetie? So keep your head high, keep your chin up, and most importantly, keep smiling, because life's a beautiful thing and there's so much to smile about.”", 'author': 'Marilyn Monroe', 'tags': ['friends', 'heartbreak', 'inspirational', 'life', 'love', 'sisters']}
2017-10-29 16:17:37 [scrapy.core.scraper] DEBUG: Scraped from <200 http://spidyquotes.herokuapp.com/api/quotes?page=2>
{'text': '“It takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.”', 'author': 'J.K. Rowling', 'tags': ['courage', 'friends']}
2017-10-29 16:17:37 [scrapy.core.scraper] DEBUG: Scraped from <200 http://spidyquotes.herokuapp.com/api/quotes?page=2>
{'text': "“If you can't explain it to a six year old, you don't understand it yourself.”", 'author': 'Albert Einstein', 'tags': ['simplicity', 'understand']}
当到达第 10 页时,它将停止,因为它将看到内容中没有设置下一页标志。
让我们通过蜘蛛来了解它是如何工作的。爬行器从以下开始 URL 的定义开始:
class Spider(scrapy.Spider):
name = 'spidyquotes'
quotes_base_url = 'http://spidyquotes.herokuapp.com/api/quotes'
start_urls = [quotes_base_url]
download_delay = 1.5
然后,parse 方法打印响应,并将 JSON 解析为数据变量:
def parse(self, response):
print(response)
data = json.loads(response.body)
然后它循环遍历 JSON 对象的 quotes 元素中的所有项。对于每个项目,它都会将一个新的“刮屑”项目返回到“刮屑引擎”:
for item in data.get('quotes', []):
yield {
'text': item.get('text'),
'author': item.get('author', {}).get('name'),
'tags': item.get('tags'),
}
然后检查数据 JSON 变量是否有'has_next'
属性,如果有,则返回下一页并向 Scrapy 返回一个新请求,以解析下一页:
if data['has_next']:
next_page = data['page'] + 1
yield scrapy.Request(self.quotes_base_url + "?page=%s" % next_page)
还可以使用 Selenium 处理无限的滚动页面。以下代码在06/06_scrape_continuous_twitter.py
中:
from selenium import webdriver
import time
driver = webdriver.PhantomJS()
print("Starting")
driver.get("https://twitter.com")
scroll_pause_time = 1.5
# Get scroll height
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
print(last_height)
# Scroll down to bottom
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Wait to load page
time.sleep(scroll_pause_time)
# Calculate new scroll height and compare with last scroll height
new_height = driver.execute_script("return document.body.scrollHeight")
print(new_height, last_height)
if new_height == last_height:
break
last_height = new_height
输出类似于以下内容:
Starting
4882
8139 4882
8139
11630 8139
11630
15055 11630
15055
15055 15055
Process finished with exit code 0
这段代码首先从 Twitter 加载页面。当页面完全加载时,将返回对.get()
的调用。然后检索scrollHeight
,程序滚动到该高度并等待片刻以加载新内容。再次检索浏览器的scrollHeight
,如果与last_height
不同,将循环并继续处理。如果与last_height
相同,则没有加载新内容,然后您可以继续并检索已完成页面的 HTML。
爬行的深度可以使用 ScrapyDepthMiddleware
中间件进行控制。深度中间件限制了 Scrapy 从任何给定链接获取的跟踪数。此选项可用于控制进入特定爬网的深度。这还用于防止爬网持续太长时间,如果您知道要爬网的内容位于爬网开始时与页面之间一定程度的分离范围内,则此功能非常有用。
深度控制中间件默认安装在中间件管道中,06/06_limit_depth.py
脚本中包含深度限制示例。此脚本在端口 8080 上爬网源代码提供的静态站点,并允许您配置深度限制。此网站由三个级别组成:0、1 和 2,每个级别有三个页面。文件名为CrawlDepth<level><pagenumber>.html
。每个级别上的第 1 页链接到同一级别上的其他两页,以及下一级别上的第一页。到更高级别的链接在级别 2 结束。此结构非常适合于检查在 Scrapy 中如何处理深度处理。
深度限制可通过设置DEPTH_LIMIT
参数进行:
process = CrawlerProcess({
'LOG_LEVEL': 'CRITICAL',
'DEPTH_LIMIT': 2,
'DEPT_STATS': True
})
深度限制为 1 意味着我们将只抓取一个级别,这意味着它将处理start_urls
中指定的 URL,然后处理这些页面中找到的任何 URL。通过DEPTH_LIMIT
我们得到以下输出:
Parsing: <200 http://localhost:8080/CrawlDepth0-1.html>
Requesting crawl of: http://localhost:8080/CrawlDepth0-2.html
Requesting crawl of: http://localhost:8080/Depth1/CrawlDepth1-1.html
Parsing: <200 http://localhost:8080/Depth1/CrawlDepth1-1.html>
Requesting crawl of: http://localhost:8080/Depth1/CrawlDepth1-2.html
Requesting crawl of: http://localhost:8080/Depth1/depth1/CrawlDepth1-2.html
Requesting crawl of: http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html
Parsing: <200 http://localhost:8080/CrawlDepth0-2.html>
Requesting crawl of: http://localhost:8080/CrawlDepth0-3.html
<scrapy.statscollectors.MemoryStatsCollector object at 0x109f754e0>
Crawled: ['http://localhost:8080/CrawlDepth0-1.html', 'http://localhost:8080/Depth1/CrawlDepth1-1.html', 'http://localhost:8080/CrawlDepth0-2.html']
Requested: ['http://localhost:8080/CrawlDepth0-2.html', 'http://localhost:8080/Depth1/CrawlDepth1-1.html', 'http://localhost:8080/Depth1/CrawlDepth1-2.html', 'http://localhost:8080/Depth1/depth1/CrawlDepth1-2.html', 'http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html', 'http://localhost:8080/CrawlDepth0-3.html']
爬网从CrawlDepth0-1.html
开始。该页面有两行,一行到CrawlDepth0-2.html
和一行到CrawlDepth1-1.html
。然后请求解析它们。考虑到起始页的深度为 0,这些页面的深度为 1,这是我们的深度限制。因此,我们将看到这两个页面被解析。但是,请注意,这两个页面中的所有链接,尽管请求进行解析,但都会被 Scrapy 忽略,因为它们位于深度 2,超过了指定的限制。
现在将深度限制更改为 2:
process = CrawlerProcess({
'LOG_LEVEL': 'CRITICAL',
'DEPTH_LIMIT': 2,
'DEPT_STATS': True
})
然后,输出如下所示:
Parsing: <200 http://localhost:8080/CrawlDepth0-1.html>
Requesting crawl of: http://localhost:8080/CrawlDepth0-2.html
Requesting crawl of: http://localhost:8080/Depth1/CrawlDepth1-1.html
Parsing: <200 http://localhost:8080/Depth1/CrawlDepth1-1.html>
Requesting crawl of: http://localhost:8080/Depth1/CrawlDepth1-2.html
Requesting crawl of: http://localhost:8080/Depth1/depth1/CrawlDepth1-2.html
Requesting crawl of: http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html
Parsing: <200 http://localhost:8080/CrawlDepth0-2.html>
Requesting crawl of: http://localhost:8080/CrawlDepth0-3.html
Parsing: <200 http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html>
Parsing: <200 http://localhost:8080/CrawlDepth0-3.html>
Parsing: <200 http://localhost:8080/Depth1/CrawlDepth1-2.html>
Requesting crawl of: http://localhost:8080/Depth1/CrawlDepth1-3.html
<scrapy.statscollectors.MemoryStatsCollector object at 0x10d3d44e0>
Crawled: ['http://localhost:8080/CrawlDepth0-1.html', 'http://localhost:8080/Depth1/CrawlDepth1-1.html', 'http://localhost:8080/CrawlDepth0-2.html', 'http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html', 'http://localhost:8080/CrawlDepth0-3.html', 'http://localhost:8080/Depth1/CrawlDepth1-2.html']
Requested: ['http://localhost:8080/CrawlDepth0-2.html', 'http://localhost:8080/Depth1/CrawlDepth1-1.html', 'http://localhost:8080/Depth1/CrawlDepth1-2.html', 'http://localhost:8080/Depth1/depth1/CrawlDepth1-2.html', 'http://localhost:8080/Depth1/depth2/CrawlDepth2-1.html', 'http://localhost:8080/CrawlDepth0-3.html', 'http://localhost:8080/Depth1/CrawlDepth1-3.html']
请注意,DEPTH_LIMIT
设置为 1 时先前忽略的三个页面现在被解析。现在,在该深度找到的链接,例如页面CrawlDepth1-3.html
的链接,由于其深度超过 2,现在被忽略。
爬网的长度,即可以解析的页面数量,可以通过CLOSESPIDER_PAGECOUNT
设置进行控制
我们将在06/07_limit_length.py
中使用脚本。脚本和 scraper 与 NASA sitemap crawler 相同,添加了以下配置,以将解析的页面数限制为 5:
if __name__ == "__main__":
process = CrawlerProcess({
'LOG_LEVEL': 'INFO',
'CLOSESPIDER_PAGECOUNT': 5
})
process.crawl(Spider)
process.start()
运行此命令时,将生成以下输出(分布在日志输出中):
<200 https://www.nasa.gov/exploration/systems/sls/multimedia/sls-hardware-being-moved-on-kamag-transporter.html>
<200 https://www.nasa.gov/exploration/systems/sls/M17-057.html>
<200 https://www.nasa.gov/press-release/nasa-awards-contract-for-center-protective-services-for-glenn-research-center/>
<200 https://www.nasa.gov/centers/marshall/news/news/icymi1708025/>
<200 https://www.nasa.gov/content/oracles-completed-suit-case-flight-series-to-ascension-island/>
<200 https://www.nasa.gov/feature/goddard/2017/asteroid-sample-return-mission-successfully-adjusts-course/>
<200 https://www.nasa.gov/image-feature/jpl/pia21754/juling-crater/>
请注意,我们将页面限制设置为 5,但该示例实际解析了 7 个页面。CLOSESPIDER_PAGECOUNT
的值应视为 Scrapy 的最小值,但可能会超出一小部分。
分页将大量内容分成若干页。通常,这些页面具有供用户单击的上一页/下一页链接。通常可以使用 XPath 或其他方法找到这些链接,然后按照这些链接进入下一页(或上一页)。让我们看看如何使用 Scrapy 遍历页面。我们将看一个自动互联网搜索结果爬行的假设示例。这些技术直接应用于许多具有搜索功能的商业网站,并且很容易针对这些情况进行修改。
我们将通过一个示例演示如何处理分页,该示例从提供的容器中的网站中抓取一组页面。此网站为五个页面建模,每个页面上都有上一个和下一个链接,以及我们将提取的每个页面中的一些嵌入数据。
在http://localhost:5001/pagination/page1.html
可以看到集合的第一页,下图显示该页已打开,我们正在查看下一步按钮:
Inspecting the Next button
这一页有两个有趣的部分。第一个是“下一步”按钮的链接。这是一个相当普遍的做法,这个链接有一个类,它将链接标识为下一页的链接。我们可以使用这些信息来查找此链接。在本例中,我们可以使用以下 XPath 找到它:
//*/a[@class='next']
第二个感兴趣的项目实际上是从页面检索我们想要的数据。在这些页面上,它由一个带有class="data"
属性的<div>
标记标识。这些页面只有一个数据项,但在本例中,我们将抓取多个数据项对页面进行爬网,从而进行搜索。
现在,让我们实际运行这些页面的刮板。
有一个脚本名为06/08_scrapy_pagination.py
。用 Python 运行这个脚本,Scrapy 会有很多输出,其中大部分是标准的 Scrapy 调试输出。但是,在该输出中,您将看到我们提取了所有五个页面上的数据项:
Page 1 Data
Page 2 Data
Page 3 Data
Page 4 Data
Page 5 Data
代码以CrawlSpider
的定义和起始 URL 开头:
class PaginatedSearchResultsSpider(CrawlSpider):
name = "paginationscraper"
start_urls = [
"http://localhost:5001/pagination/page1.html"
]
然后定义规则字段,通知 Scrapy 如何解析每个页面以查找链接。此代码使用前面讨论的 XPath 查找页面中的下一个链接。Scrapy 将在每个页面上使用此规则来查找下一个要处理的页面,并将该请求排队等待在当前页面之后处理。对于找到的每个页面,回调参数通知 Scrapy 调用哪个方法进行处理,在本例中为parse_result_page
:
rules = (
# Extract links for next pages
Rule(LinkExtractor(allow=(),
restrict_xpaths=("//*/a[@class='next']")),
callback='parse_result_page', follow=True),
)
声明一个名为all_items
的列表变量来保存我们找到的所有项:
all_items = []
然后定义了parse_start_url
方法。Scrapy 将调用此函数来解析爬网中的初始 URL。该函数只是将该处理推迟到parse_result_page
:
def parse_start_url(self, response):
return self.parse_result_page(response)
然后,parse_result_page
方法使用 XPath 在<div class="data">
标记中查找<h1>
标记内部的文本。然后将该文本添加到all_items
列表中:
def parse_result_page(self, response):
data_items = response.xpath("//*/div[@class='data']/h1/text()")
for data_item in data_items:
self.all_items.append(data_item.root)
爬网完成后,调用closed()
方法并写出all_items
字段的内容:
def closed(self, reason):
for i in self.all_items:
print(i)
爬虫程序使用 Python 作为脚本运行,脚本使用以下内容:
if __name__ == "__main__":
process = CrawlerProcess({
'LOG_LEVEL': 'DEBUG',
'CLOSESPIDER_PAGECOUNT': 10
})
process.crawl(ImdbSearchResultsSpider)
process.start()
注意将CLOSESPIDER_PAGECOUNT
属性设置为10
的用法。这超过了该网站的页面数量,但在许多(或大多数)情况下,搜索结果中可能有数千页。在适当的页数后停止是一种很好的做法。对于爬虫来说,这是一种很好的行为,因为在几页之后,项目与搜索的相关性会急剧下降,因此在前几页之后进行爬虫会大大降低回报,通常最好在几页之后停止。
正如在菜谱的开头所提到的,对于各种内容站点上的各种自动搜索,这很容易修改。这一做法可以推动可接受用途的限制,因此在这里得到了推广。但更多的实际例子,请访问我的博客:www.smac.io
。
我们通常需要先登录一个站点,然后才能对其内容进行爬网。这通常是通过一个表单来完成的,在表单中,我们输入用户名和密码,按回车,然后授予访问先前隐藏内容的权限。这种类型的表单身份验证通常称为 cookie 授权,因为当我们进行授权时,服务器会创建一个 cookie,它可以用来验证您是否已登录。Scrapy 尊重这些 cookie,因此我们所需要做的就是在爬网过程中以某种方式使表单自动化。
我们将在容器网站中的以下 URL 处抓取一个页面:http://localhost:5001/home/secured
。在此页面上,以及该页面的链接上,有我们想要抓取的内容。但是,此页面被登录阻止。在浏览器中打开页面时,我们会看到以下登录表单,在这里我们可以输入darkhelmet
作为用户名,vespa
作为密码:
Username and password credentials are entered
按输入后,我们将通过身份验证并进入最初需要的页面。
那里没有太多的内容,但消息足以验证我们是否已登录,我们的 scraper 也知道这一点。
我们按照以下方法进行配方:
- 如果检查登录页面的 HTML,您会注意到以下表单代码:
<form action="/Account/Login" method="post"><div>
<label for="Username">Username</label>
<input type="text" id="Username" name="Username" value="" />
<span class="field-validation-valid" data-valmsg-for="Username" data-valmsg-replace="true"></span></div>
<div>
<label for="Password">Password</label>
<input type="password" id="Password" name="Password" />
<span class="field-validation-valid" data-valmsg-for="Password" data-valmsg-replace="true"></span>
</div>
<input type="hidden" name="returnUrl" />
<input name="submit" type="submit" value="Login"/>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8CqzjGWzUMJKkKCmxuBIgZf3UkeXZnVKBwRV_Wu4qUkprH8b_2jno5-1SGSNjFqlFgLie84xI2ZBkhHDzwgUXpz6bbBwER0v_-fP5iTITiZi2VfyXzLD_beXUp5cgjCS5AtkIayWThJSI36InzBqj2A" /></form>
- 为了让 Scrapy 中的表单处理器工作,我们需要该表单中用户名和密码字段的 ID。它们分别是
Username
和Password
。现在我们可以使用这些信息创建一个爬行器。此爬行器位于脚本文件06/09_forms_auth.py
中。spider 定义从以下内容开始:
class Spider(scrapy.Spider):
name = 'spider'
start_urls = ['http://localhost:5001/home/secured']
login_user = 'darkhelmet'
login_pass = 'vespa'
- 我们在类中定义了两个字段,
login_user
和login_pass
来保存我们想要使用的用户名。爬网也将从指定的 URL 开始。 - 然后更改
parse
方法以检查页面是否包含登录表单。这是通过使用 XPath 来完成的,以查看是否存在密码类型的输入表单,并且输入表单的id
为Password
:
def parse(self, response):
print("Parsing: ", response)
count_of_password_fields = int(float(response.xpath("count(//*/input[@type='password' and @id='Password'])").extract()[0]))
if count_of_password_fields > 0:
print("Got a password page")
- 如果找到该字段,我们将使用其
from_response
方法生成的FormRequest
返回给 Scrapy:
return scrapy.FormRequest.from_response(
response,
formdata={'Username': self.login_user, 'Password': self.login_pass},
callback=self.after_login)
- 此函数传递响应,然后是一个字典,指定需要与这些值一起插入数据的字段的 ID。然后,将回调定义为在 Scrapy 执行此 FormRequest 后执行,并向其传递结果表单的内容:
def after_login(self, response):
if "This page is secured" in str(response.body):
print("You have logged in ok!")
- 此回调仅查找单词
This page is secured
,只有在登录成功时才会返回。当成功运行此命令时,我们将从 scraper 的 print 语句中看到以下输出:
Parsing: <200 http://localhost:5001/account/login?ReturnUrl=%2Fhome%2Fsecured>
Got a password page
You have logged in ok!
当您创建一个FormRequest
时,您正在指示 Scrapy 代表您的流程构造一个表单 POST 请求,使用指定字典中的数据作为 POST 请求中的表单参数。它构造此请求并将其发送到服务器。在收到该帖子中的答案后,它将调用指定的回调函数。
这种技术在许多其他类型的表单条目中也很有用,而不仅仅是登录表单。这可用于自动执行任何类型的 HTML 表单请求,如下达订单或用于执行搜索操作的请求。
一些网站使用一种称为基本授权的授权形式。这在其他授权方式(如 CookieAuth 或 OAuth)之前很流行。它在公司内部网和一些 web API 上也很常见。在基本授权中,向 HTTP 请求添加一个头。此头文件Authorization
被传递基本字符串,然后是值<username>:<password>
的 base64 编码。因此在 darkhelmet 的情况下,此头文件将如下所示:
Authorization: Basic ZGFya2hlbG1ldDp2ZXNwYQ==, with ZGFya2hlbG1ldDp2ZXNwYQ== being darkhelmet:vespa base 64 encoded.
请注意,这并不比以纯文本形式发送更安全(尽管在 HTTPS 上执行时是安全的)。但是,在大多数情况下,is 已包含在更健壮的授权表单中,甚至 cookie 授权也允许更复杂的功能,例如声明:
在 Scrapy 中支持基本身份验证非常简单。要使此功能适用于爬行器和爬行器正在爬行的给定站点,只需在刮板中定义http_user
、http_pass
和name
字段。以下说明:
class SomeIntranetSiteSpider(CrawlSpider):
http_user = 'someuser'
http_pass = 'somepass'
name = 'intranet.example.com'
# .. rest of the spider code omitted ...
当爬行器在名称指定的给定站点上爬行任何页面时,它将使用http_user
和http_pass
的值来构造适当的标头。
注意,此任务由 Scrapy 的HttpAuthMiddleware
模块执行。有关基本授权的更多信息,请访问:https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication 。
有时你可能会被你正在抓取的站点阻止,因为你被识别为一个抓取者,有时发生这种情况是因为网站管理员看到来自统一 IP 的抓取请求,此时他们只是阻止对该 IP 的访问。
为了帮助防止这个问题,可以在 Scrapy 中使用代理随机化中间件。存在一个库scrapy-proxies
,它实现了代理随机化功能。
您可以在从 GitHub 获得scrapy-proxies
https://github.com/aivarsk/scrapy-proxies 或使用pip install scrapy_proxies
安装。
scrapy-proxies
的使用是通过配置完成的。首先配置DOWNLOADER_MIDDLEWARES
,确保他们安装了RetryMiddleware
、RandomProxy
和HttpProxyMiddleware
。以下为典型配置:
# Retry many times since proxies often fail
RETRY_TIMES = 10
# Retry on most error codes since proxies fail for different reasons
RETRY_HTTP_CODES = [500, 503, 504, 400, 403, 404, 408]
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 90,
'scrapy_proxies.RandomProxy': 100,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 110,
}
PROXY_LIST
设置配置为指向包含代理列表的文件:
PROXY_LIST = '/path/to/proxy/list.txt'
然后,我们需要让 Scrapy 知道PROXY_MODE
:
# Proxy mode
# 0 = Every requests have different proxy
# 1 = Take only one proxy from the list and assign it to every requests
# 2 = Put a custom proxy to use in the settings
PROXY_MODE = 0
如果PROXY_MODE
为2
,则必须指定一个CUSTOM_PROXY
:
CUSTOM_PROXY = "http://host1:port"
此配置本质上告诉 Scrapy,如果页面请求因任何RETRY_HTTP_CODES
而失败,并且每个 URL 最多为RETRY_TIMES
,则使用由PROXY_LIST
指定的文件中的代理,并使用由PROXY_MODE
定义的模式。这样,您就可以让 Scrapy 故障回复到任意数量的代理服务器,以从不同的 IP 地址和/或端口重试请求。
您使用的哪个用户代理会对您的 scraper 的成功产生影响。一些网站会断然拒绝向特定的用户代理提供内容。这可能是因为用户代理被标识为被禁止的刮板,或者用户代理用于不受支持的浏览器(即 Internet Explorer 6)。
控制 scraper 的另一个原因是,根据指定的用户代理,web 服务器可能会以不同的方式呈现内容。目前,这在移动网站上很常见,但也可以用于台式机,以便为较旧的浏览器提供更简单的内容。
因此,将用户代理设置为默认值以外的其他值可能很有用。Scrapy 默认为名为scrapybot
的用户代理。这可以通过使用BOT_NAME
参数进行配置。如果使用 Scrapy 项目,Scrapy 会将代理设置为项目的名称。
对于更复杂的方案,可以使用两种流行的扩展:scrapy-fake-agent
和scrapy-random-useragent
。
我们按照以下方法进行配方:
scrapy-fake-useragent
可在 GitHub 的上获得 https://github.com/alecxe/scrapy-fake-useragent 和scrapy-random-useragent
在上提供 https://github.com/cnu/scrapy-random-useragent 。您可以使用pip install scrapy-fake-agent
和/或pip install scrapy-random-useragent
包含它们。scrapy-random-useragent
将为文件中的每个请求选择一个随机用户代理。它在两种设置中配置:
DOWNLOADER_MIDDLEWARES = {
'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None,
'random_useragent.RandomUserAgentMiddleware': 400
}
- 这将禁用现有的
UserAgentMiddleware
,并用RandomUserAgentMiddleware
中提供的实现替换它。然后,配置对包含用户代理名称列表的文件的引用:
USER_AGENT_LIST = "/path/to/useragents.txt"
- 配置后,每个请求将使用文件中的随机用户代理。
scrapy-fake-useragent
使用不同的模型。它从跟踪最常用的用户代理的在线数据库中检索用户代理。使用以下设置配置 Scrapy 以供使用:
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware': 400,
}
-
它还能够将使用的用户代理类型设置为值(如移动或桌面),以强制选择这两个类别中的用户代理。这是使用默认为随机的
RANDOM_UA_TYPE
设置来执行的。 -
如果将
scrapy-fake-useragent
与任何代理中间件一起使用,那么您可能希望对每个代理进行随机操作。这可以通过将RANDOM_UA_PER_PROXY
设置为 True 来实现。此外,您还需要将RandomUserAgentMiddleware
的优先级设置为大于scrapy-proxies
,以便在处理代理之前设置代理。
Scrapy 具有缓存 HTTP 请求的能力。如果已经访问过页面,这可以大大减少爬行时间。通过启用缓存,Scrapy 将存储每个请求和响应
06/10_file_cache.py
脚本中有一个工作示例。在 Scrapy 中,默认情况下禁用缓存中间件。要启用此缓存,请将HTTPCACHE_ENABLED
设置为True
并将HTTPCACHE_DIR
设置为文件系统上的目录(使用相对路径将在项目的数据文件夹中创建目录)。为了演示,此脚本运行 NASA 站点的爬网,并缓存内容。它使用以下配置:
if __name__ == "__main__":
process = CrawlerProcess({
'LOG_LEVEL': 'CRITICAL',
'CLOSESPIDER_PAGECOUNT': 50,
'HTTPCACHE_ENABLED': True,
'HTTPCACHE_DIR': "."
})
process.crawl(Spider)
process.start()
我们要求 Scrapy 使用文件进行缓存,并在当前文件夹中创建子目录。我们还指示它将爬网限制在大约 500 页。运行此操作时,爬网大约需要一分钟(取决于您的互联网速度),大约有 500 行输出。
第一次执行后,您可以看到您的目录中现在有一个包含缓存数据的.scrapy
文件夹,其结构如下所示:
再次运行脚本只需几秒钟,并且将生成相同的解析页面输出/报告,只是这次内容将来自缓存而不是 HTTP 请求。
在 Scrapy 中有许多用于缓存的配置和选项。默认情况下,HTTPCACHE_EXPIRATION_SECS
指定的缓存到期时间设置为 0。0 表示缓存项永不过期,因此一旦写入,Scrapy 将不再通过 HTTP 请求该项。实际上,您需要将其设置为某个确实过期的值。
缓存的文件存储只是缓存的选项之一。通过将HTTPCACHE_STORAGE
设置分别设置为scrapy.extensions.httpcache.DbmCacheStorage
或scrapy.extensions.httpcache.LeveldbCacheStorage
,项目也可以缓存在 DMB 和 LevelDB 中。您也可以编写自己的代码,如果您愿意,将页面内容存储在另一种类型的数据库或云存储中。
最后,我们来讨论缓存策略。Scrapy 内置了两个策略:Dummy(默认)和 RFC2616。可通过将HTTPCACHE_POLICY
设置更改为scrapy.extensions.httpcache.DummyPolicy
或scrapy.extensions.httpcache.RFC2616Policy
进行设置。
RFC2616 策略通过以下操作启用 HTTP 缓存控制感知:
- 请勿尝试在未设置存储缓存控制指令的情况下存储响应/请求
- 如果未设置缓存控制指令(即使是针对新响应),则不提供来自缓存的响应
- 从最大年龄缓存控制指令计算新鲜度生存期
- 从 Expires 响应头计算新鲜度生存期
- 从上次修改的响应头计算新鲜度生存期(Firefox 使用的启发式方法)
- 从年龄响应标题计算当前年龄
- 从日期标头计算当前年龄
- 根据上次修改的响应标头重新验证过时响应
- 基于 ETag 响应头重新验证过时响应
- 为任何未收到的响应设置日期标头
- 在请求中支持 max stale cache control 指令