Scrapy 爬取一个需要通过表单提交数据的 ASP.Net 页面
处理 ASP.Net 页面,PostBack 和视图状态
使用 ASP.Net 技术构建的网站对于 web 爬虫开发者来说通常是一场噩梦,这主要是由于它们处理表单的方式。
这类网站通常在请求和响应中发送状态,以便跟踪客户端的 UI 状态。想想那些你浏览许多页面,在 HTML 表单中填写你的数据来注册的网站吧。一个 ASP.Net 网站通常存储那些在前一个页面填写的数据到一个名为 __VIEWSTATE 的隐藏字段中,这个字段包含了像下面显示的一个巨大的字符 串:
我不是在开玩笑,它真的很大! (有时是数十 kB)
这是一个 Base64 编码字符串,它表示客户端 UI 状态,包括来自表单的值。这在表单中的用户动作触发 POST 请求返回给服务器以获取其他字段的数据的 web 应用中,这种设置尤为常见。
每次浏览器向服务器发起 POST 请求时,就会带着这个__VIEWSTATE 字段。然后,服务器根据该数据解码并加载客户端的 UI 状态,执行一些处理,基于新值为新的视图状态计算值,然后将这个新的视图状态作为隐藏字段渲染结果页面。
如果__VIEWSTATE 没有发回给服务器,那么你可能会看到一个空白表单,因为服务器完全失去了客户端 UI 状态。所以,为了爬取像这样的根据表单生成的页面,你必须确保你的爬虫在它发送的请求中带有这个状态,否则,页面将不会加载它应该加载的内容。
这里有一个具体的例子,你可以亲眼看到如何处理这类情况。
抓取一个基于视图状态的网站
今天抓取的小白鼠是 spidyquotes.herokuapp.com/search.aspx 。SpidyQuotes 列出了来自名人的引言,而它的搜索页面允许你根据作者和标签过滤引言:
Author 字段的改变触发了一个到服务器的 POST 请求,以使用与所选的用户相关的标签来填充 Tag 选择框。点击 Search ,显示与所选作者的标签相对应的引言:
为了爬取这些引言,我们的爬虫必须模拟用户选择一个作者,一个标签并提交表单。通过使用 Network Panel (你可以通过浏览器的开发者工具访问)来仔细看看这个流程的每一步。首先,访问 spidyquotes.herokuapp.com/search.aspx ,然后按下 F12 或 Ctrl+Shift+I (如果你使用的是 Chrome) 来加载工具,接着点击 Network 选项卡。
从列表中选择一个作者,然后你将看到生成了一个发往“/filter.aspx”的请求。点击资源名 (filter.aspx) ,你就可以看到请求细节,其中包括你选择的作者,以及在来自于服务器的原始响应中的__VIEWSTATE 数据。
选择一个标签并点击 Search。你会看到你的浏览器发送了在表单中选择的值,以及一个与前面不同的__VIEWSTATE 值。这是因为,当你选择作者时,服务器包含了一些新的信息在视图状态中。
现在,你只需要构建一个爬虫,这个爬虫完成与你的浏览器做的事情。
构建爬虫
这里是你的爬虫应该遵循的步骤:
- 抽取 spidyquotes.herokuapp.com/filter.aspx
- 对于每一个在表单作者列表中找到的 Author :
- 创建一个到/filter.aspx 的 POST 请求,同时传递选择的 Author 和__VIEWSTATE 值
- 对于在结果页面中找到的每一个 Tag :
- 发送一个到/filter.aspx 的 POST 请求,同时传递选择的 Author ,选择的 Tag 和视图状态
- 抓取结果页面
爬虫编码
这里是我开发的从该网站抓取引言的爬虫,遵循了刚刚描述的步骤:
import scrapy
class SpidyQuotesViewStateSpider(scrapy.Spider):
name = 'spidyquotes-viewstate'
start_urls = ['http://spidyquotes.herokuapp.com/search.aspx']
download_delay = 1.5
def parse(self, response):
for author in response.css('select#author > option ::attr(value)').extract():
yield scrapy.FormRequest(
'http://spidyquotes.herokuapp.com/filter.aspx',
formdata={
'author': author,
'__VIEWSTATE': response.css('input#__VIEWSTATE::attr(value)').extract_first()
},
callback=self.parse_tags
)
def parse_tags(self, response):
for tag in response.css('select#tag > option ::attr(value)').extract():
yield scrapy.FormRequest(
'http://spidyquotes.herokuapp.com/filter.aspx',
formdata={
'author': response.css(
'select#author > option[selected] ::attr(value)'
).extract_first(),
'tag': tag,
'__VIEWSTATE': response.css('input#__VIEWSTATE::attr(value)').extract_first()
},
callback=self.parse_results,
)
def parse_results(self, response):
for quote in response.css("div.quote"):
yield {
'quote': response.css('span.content ::text').extract_first(),
'author': response.css('span.author ::text').extract_first(),
'tag': response.css('span.tag ::text').extract_first(),
}
步骤 1 由 Scrapy 完成,它读取 start_urls,然后生成一个到/search.aspx 的 GET 请求。
parse() 方法负责 步骤 2 。它遍历了在第一个选择框中找到的 Authors ,然后为每一个 Author 创建一个到/filter.aspx 的 FormRequest ,模拟用户点击了列表中的每一个元素。值得注意的是,parse() 方法从它所收到的表单中读取__VIEWSTATE 字段,然后将其传回给服务器,所以服务器可以跟踪我们位于哪个页面流。
步骤 3 由 parse_tags() 方法来处理。它与 parse() 方法非常类似,因为它提取了所列的 Tags ,然后创建 POST 请求来传递每一个 Tag ,在前一个步骤中选择的 Author 以及从服务器收到的__VIEWSTATE。
最后,在 步骤 4 中,parse_results() 方法解析页面展示的引言列表,然后从中生成项。
使用 FormRequest.from_response() 简化你的爬虫
你也许注意到,在发送 POST 请求到服务器之前,我们的爬虫抽取了那些它从服务器收到的表单中的预填值,并在它将创建的请求中包含了这些值。
我们不需要对其手工编码,因为 Scrapy 提供了 FormRequest.from_response() 方法。该方法读取 response 对象,创建一个 FormRequest
,它自动包含表单所有的预填值以及隐藏值。这是我们的爬虫的 parse_tags() 方法:
def parse_tags(self, response):
for tag in response.css('select#tag > option ::attr(value)').extract():
yield scrapy.FormRequest.from_response(
response,
formdata={'tag': tag},
callback=self.parse_results,
)
所以,无论何时你处理包含隐藏值和预填值的表单,使用 from_response
方法,因为这样你的代码会看起来干净得多。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论