scrapy的一些容易忽视的点(模拟登陆,传递item等)
scrapy爬虫注意事项
- item数据只有最后一条
- item字段传递后错误,混乱
- 对一个页面要进行两种或多种不同的解析
- xpath中contains的使用
- 提取不在标签内的文本内容
- 使用css、xpath提取倒数第n个标签
- 提取表格信息(含合并单元格)
- 模拟登陆
一、item数据只有最后一条
这种情况一般存在于对标签进行遍历时,将item对象放置在了for循环的外部。解决方式:将item放置在for循环里面。
def parse(self,response):
#item = ExampleItem() # 存在for循环时,item不要放置在这里
for result in result_list:
item = ExampleItem() # 放置在for循环里面
item['name'] = result.css('div a::text').extract_first()
item['age'] = result.css('div #id').extract_first()
yield item
二、item字段传递后错误,混乱
有时候会遇到这样的情况,item传递几次之后,发现不同页面的数据被混乱的组合在了一起。这种情况一般存在于item的传递过程中,没有使用深拷贝。解决方式:使用深拷贝来传递item。
import copy
def parse_base(self,response):
base_url = 'https://www.base_url.com'
for result in result_list:
item = ExampleItem()
item['name'] = result.css('div a::text').extract_first()
item['age'] = result.css('div #id').extract_first()
yield scrapy.Request(url=base_url,meta=copy.deepcopy({'item':item}),callback=self.parse_detail) # 使用深拷贝将item存在meta中
def parse_detail(self,response):
item = response.meta['item'] # 取出之前传递的item
"""
do some thing
"""
yield item
三、对一个页面要进行两种或多种不同的解析
这种情况一般出现在对同一页面有不同的解析要求时,但默认情况下只能得到第一个parse的结果。产生这个结果的原因是scrapy默认对拥有相同的url,相同的body以及相同的请求方法视为一个请求。解决方式:设置参数dont_filter='True'。
def start_requests(self):
base_url = 'https://www.base_url.com'
yield scrapy.Request(url=base_url,dont_filter='True',callback=self.parse_one)
yield scrapy.Request(url=base_url,dont_filter='True',callback=self.parse_two)
四、xpath中contains的使用
这种情况一般出现在标签没有特定属性值但是文本中包含特定汉字的情况,当然也可以用来包含特定的属性值来使用(只不过有特定属性值的时候我也不会用contains了)。
作者:村上春树
书名:挪威的森林
以上面这两个标签为例(自行F12查看),两个span标签没有特定的属性值,但里面一个包含作者,一个包含书名,就可以考虑使用contains来进行提取。
def parse(self,response):
item = BookItem()
item['author'] = response.xpath('//span[contains(.//text(),"作者:")]//text()').split('作者:')[-1] # 先用contains限定好特定的span标签,然后取出文本字符串并进行字符串切片得到需要的信息。下同
item['book_name'] = response.xpath('//span[contains(.//text(),"书名:")]//text()').split('书名:')[-1]
yield item
五、提取不在标签中的文本
有时候会遇到这样的情况,文本在两个标签之间,但不属于这两个标签的任何一个。此时可以考虑使用xpath的contains和following共同协助完成任务。
示例:
作者:
"村上春树"
书名
"挪威的森林"
def parse(self,response):
item = BookItem()
item['author'] = response.xpath('//span[contains(.//text(),"作者")]/following::text()[1]') # 先用contains限定好特定的span标签,然后提取出接下来的文本,并选择第一个。下同
item['book_name'] = response.xpath('//span[contains(.//text(),"书名")]/following::text()[1]')
yield item
六、使用css、xpath提取倒数第n个标签
对于很多页面,标签的数量有时候无法保证是一致的。如果用正向的下标进行提取,很可能出现数组越界的情况。这种时候可以考虑反向提取,必要时加一些判断。
def parse(self,response)
'''
使用css和xpath分别提取信息
'''
item = ExampleItem()
item['name'] = response.css('span::text').extract()[-1] # 使用css获取最后一个
item['age'] = response.xpath('//span[last()-2]//text()').extract_first() # 使用xpath获取倒数第二个,类似的last()-3是倒数第三个
yield item
七、提取表格信息
其实对于信息抓取,很多时候我们需要对表格页面进行抓取。一般的方方正正的表格提取相对简单,这里不讨论。只说下含有合并单元格的情况。
以这个网页的表格为例,定义5个字段批次,招生代码,专业,招生数量以及费用,注意到合并单元格的标签里有个rowspan属性,可以用来辨识出有几行被合并。我的思路是有多少行数据,就将batch批次扩展到多少个,形成一个新的列表,然后进行遍历提取数据
def parse(self, response):
batch_list = response.css('tr[class="pc"] td::text').extract() # batch批次的列表,本来这里可以直接用extract_first()提取出来,这里我用extract()是为了让程序更通用化,也就是处理存在多个合并单元格的情况
rowspan_list = response.css('tr[class="pc"] td::attr(rowspan)').extract() # rowspan值形成的列表,本例中只有一个值
tr_list = response.css('tbody tr')[-2:] # 选择最后两个tr标签
info_list = [] # 用来‘盛放’code、major、number、cost组合起来的列表
for tr in tr_list:
code = tr.css('td::text').extract_first()
major = tr.css('td::text').extract()[1]
number = tr.css('td::text').extract()[2]
cost = tr.css('td::text').extract()[3]
info_list.append([code,major,number,cost])
batch_middle_list = list(map(lambda a, b: (int(a) - 1) * [b], rowspan_list, batch_list)) # python3 map要搭配list使用形成列表。将batch批次与number-1数量分别相乘得到一个新的列表(由列表组成的列表)
batch_target_list = reduce(lambda a, b: a + b, batch_middle_list) # 使用reduce将列表的各项拼接成一个新的列表(由字符组成)。python3 reduce使用需要先导入,from functools import reduce
for i in range(len(batch_target_list)): # 此时patch批次新列表的元素个数与info_list的元素个数相同,可以进行遍历提取
item = ExampleProjectItem()
item['batch'] = batch_target_list[i]
item['code'] = info_list[i][0]
item['major'] = info_list[i][1]
item['number'] = info_list[i][2]
item['cost'] = info_list[i][3]
yield item
八、模拟登陆
当页面数据需要登陆进行抓取时,就需要模拟登陆了。常见的方式有:使用登陆后的cookie来抓取数据;发送表单数据进行登陆;使用自动化测试工具登陆,比如selenium配合chrome、firefox等,不过听说selenium不再更新,也可以使用chrome的无头模式。鉴于自动化测试的抓取效率比较低,而且我确实很久没使用过这个了。本次只讨论使用cookie和发送表单两种方式来模拟登陆。
-
使用cookie
使用cookie的方式比较简单,基本思路就是登陆后用抓包工具或者类似chrome的F12调试界面查看cookie值,发送请求时带上cookie值即可
base_url = 'http://www.example.com'
cookies_str = 'xxxxxxxx' # 登陆后的cookie字符串
cookies_dict = cookies_dict = {i.split('=')[0]: i.split('=')[1] for i in cookies_str.split('; ')} # 使用字典推导式将cookies_str转化为字典形式
def parse(self,response):
yield scrapy.Request(url=base_url,cookies=self.cookies_dict,callback=self.parse_detail) # 默认使用get方式进行请求
def parse_detail(self,response):
detail_url = 'http://www.xxx.com'
yield scrapy.Request(url=detail_url,method='POST',cookies=self.cookies_dict,callback=self.parse_target) # 每次发起请求带上cookie
def parse_target(self,response)
target_url = 'http://www.ooo.com'
yield scrapy.FormRequest(url=target_url,cookies=self.cookies_dict,callback=self.parse_final) # 使用FormRequest同样也是可以的
-
发送表单方式进行登陆
cookie是有有效期的,对于大量数据的抓取,更好的方式是发送表单进行模拟登陆。scrapy有专门的函数scrapy.FormRequest()用来处理表单提交。网上有些人说cookie没法保持,可以考虑用我下面的方式。
import copy
def start_requests(self):
base_url = 'http://www.example.com'
formdata = {
'user':'1822233xxxx',
'password':'test123'
}
yield scrapy.FormRequest(url=base_url,formdata=formdata,meta=copy.deepcopy{'cookiejar':1},callback=self.parse) # 发送表单数据,注意将cookiejar放到meta中进行传送,也就是保持cookie。
def parse(self,response):
yield scrapy.Request(url=detail_url,meta=copy.deepcopy({'cookiejar':response.meta['cookiejar']}),callback=self.parse_detail) # 每次发送请求都带上cookiejar,可以保持cookie
def parse_detail(self,response)
print(response.text) # 可以获取到登陆后页面的内容