有些网站需要用户登录之后才能爬取数据,此时scrapy爬虫需要先模拟用户登录。

了解网站登录的原理

  【请求】浏览器会向服务器发送含有登录表单数据的HTTP请求。具体来说:”表单”对应于HTML中的form节点,当用户填写完表单提交后,浏览器会根据form节点的内容发送请求。表单数据应包含的信息:账号 + 密码信息 + 3个隐藏信息(由多个键值对构成,每个键值对对应一个input元素,键→name,值→value)
   form节点的属性:①method 决定HTTP请求方法(一般是POST)
           ②action 决定http请求的url
           ③enctype 决定表单数据的编码类型
   form中的input节点属性:①email(账号) ②password(密码)
    隐藏的input属性:在div style=”display:none;”中,比如①name=”_next”(登录成功后页面跳转的地址) ②name=”_formkey”(防止CSRF跨域攻击)
  【响应】响应头部中Set-Cookie字段,就是服务器保存在浏览器的Cookie信息,其中包含标识用户身份的session信息,之后对该网站发送的其他HTTP请求都会带上这个“身份证”,服务器程序通过这个“身份证”识别出发送请求的用户,从而决定响应怎样的页面。

用FormRequest模拟登录

(1)构造FormRequest对象,即构造含有表单数据的请求

  【方法1】直接构造FormRequest对象:先把表单信息收集到一个字典中,然后使用表单数据字典构造FormRequest对象。

1
2
3
4
5
6
7
>>scrapy shell 登录url
>>sel = response.xpath('//div[@style]/input') #<input>中的信息(包含隐藏的)
>>fd = dict(zip(sel.xpath('./@name').extract(),sel.xpath('./@value').extract())
>>fd['email']='邮箱'
>>fd['password']='密码'
>>from scrapy.http import FormRequest
>>request=FormRequest('登录url',formdata=fd) #用formdata接受表单数据字典

  【方法2】FormRequest的from_response方法:参数是Response对象、账号和密码的表达数据字典,该方法能解析Rresponse对象的form节点,将隐藏在input中的信息自动填入表单数据。

1
2
>>fd={'email':'邮箱','password':'密码'}
>>request=FormRequest.from_response(response,formdata=fd)

(2)提交表单请求

  运行fetch(request),得到log信息,可以看到POST请求的响应状态码为303,之后scrapy自动再发送一个GET请求(携带了第一个POST请求获取的Cookie信息)下载跳转页面,可以在浏览器中查看页面或者查看是否有特殊字符串,来验证登录是否成功。

(3)设计Spider代码:爬取用户信息,实现登录

  把模拟登录和爬取内容的代码分离开,使得逻辑上更加清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import scrapy
from scrapy.http import Request,FormRequest
class LoginSpider(scrapy.Spider):
name = 'login"
allowed_domains=['example.webscraping.com']
start_urls=['http:/....']
def parse(self,response):
#解析登录后下载的页面,此例中为用户个人信息页面
keys = response.css('table label::text').re('(.+):')
values-response.css('table td.w2p_fw::text').extract()
yield dict(zip(keys,values))
login_url='登录页面的url'
#覆写基类的start_requests方法,最先请求登录页面
def start_requests(self):
yield Request(self.login_url,callback=self.login)
def login(self,response): #登录页面的解析函数,在该方法中进行模拟登录,构造表单请求并提交
fd = {'email':'邮箱','password':'密码'}
yield FormRequest.from_response(response,formdata=fd,callback=self.parse_login)
def parse_login(self,response): #登录成功后,处理响应
if 'Welcome Liu' in response.text: #页面是否有他是字符串
yield from super().start_requests() #如果成功则调用基类的start_requests方法,继续爬取start_urls中的页面

识别验证码

  很多网站为了防止爬虫爬取,登录时需要用户输入验证码。①验证码图片对应一个img ②与账号密码输入框一样,验证码输入框也对应一个input节点,所以用户输入的验证码会成为表单数据的一部分,表单提交后由服务器验证。识别验证码有很多方式,常用的有以下3种

(1)用pytesseract识别验证码(OCR识别技术)

  OCR是光学字符识别,用于在图像中提取文本信息。tesseract-ocr是用OCR技术实现的一个验证码识别库,在Python中可以通过第三方库pytesseract调用它(先安装tesseract-ocr和pytesseract)

1
2
3
4
5
6
#用pytesseract识别图片code.png中的验证码
from PIL import Image
import pytesseract
img=Image.open('code.png')
img=img.convert('L') #为提高识别率,调用Image对象的convert方法把图片转换为黑白图
pytesseract.image_to_string(img) #将黑白图传递给image_to_string方法进行识别

  以之前的LoginSpider为模板,实现一个使用pytesseract识别验证码登录的Spider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import scrapy
from scrapy import Request,FormRequest
import json
from PIL import Image
from io import BytesIO
import pytesseract
from scrapy.log import logger

class CaptchaLoginSpidr(scrapy.Spider):
name = "login_captcha"
start_urls = ['http://XXX.com/']
def parse(self,response):
……
login_url='登录页面url'
user = '邮箱'
password = 密码
def start_requests(self):
yield Request(self.login_url,callback=self.login,dont_filter=True)

#带有验证码的登录,需额外发送一个请求→获取验证码图片。login方法处理登录页面的响应、下载验证码图片的响应
#当response.meta['login_response']存在时,response是验证码图片的响应;否则是登录页面的响应

def login(self,response):
login_response = response.meta.get('login_response')
if not login_response: # 解析登录页面时
captchaUrl = response.css('label.field.prepend-icon img::attr(src)').extract_first()
captchaUrl = response.urljoin(captchaUrl) # 提取验证码图片的url
# 发送请求下载图片,并将登录页面的Response对象保存到Request对象的meat字典中
yield Request(captchaUrl,callback=self.login,meta={'login_response':response},dont_filter=True)

else: #处理下载验证码图片的响应
# 识别图片验证码的方法get_captcha_by_OCR(图片二进制数据)
formdata = { 'email':self.user,'pass':self.password, 'code':self.get_captcha_by_OCR(response.body),}
# 将之前保存的登录页面的Response对象取出,构造FormRequest对象并提交
yield FormRequest.from_response(login_response, callback=self.parse_login, formdata=formdata, dont_filter=True)

def parse_login(self,response): # 处理表单请求的响应,响应正文是一个json串,包含了用户验证结果
info = json.loads(response.text) # 用json.loads将正文转换为Python字典
if info['error']=='0': # 根据error字段的值,判断登录是否成功
logger.info('登录成功:-)')
return super().start_requests() # 登录成功,就从start_urls中的页面开始爬取
logger.info('登录失败:-(,重新登录...')
return self.start_requests()

def get_captcha_by_OCR(self,data): # OCR识别,data是图片二进制数据(bytes)
img = Image.open(BytesIO(data)) # 先把二进制转换成类文件对象,再用Image.open函数构造Image对象
img = img.convert('L') # 转换成黑白图
captcha = pytesseract.image_to_string(img) # 识别
img.close()
return captcha

(2)验证码识别平台

  对于某些复杂的验证码,方法(1)的识别率很低或者无法识别。目前有很多网站(验证码识别平台)专门提供验证码识别服务,可以识别较为复杂的验证码(有些是人工处理的),这些平台多数是付费使用的,价格大约为1元钱识别100个验证码。平台提供了HTTP服务接口,用户可以通过HTTP请求将验证码图片发送给平台,平台识别后将结果通过HTTP响应返回。

(3)人工识别

  在其他识别方式不管用时,人工识别一次验证码也是可行的。将人工识别的方式也加入CaptchaLoginSpider中,再添加一个get_captcha_by_user方法:

1
2
3
4
5
6
def get_captcha_by_user(self,data):
img = Image.open(BytesIO(data)) #scrapy下载完验证码图片后,得到Image对象
img.show() #将图片显示出来
captcha=input('输入验证码:') #用Python内置的input函数,等待用户肉眼识别后输入识别结果
img.close()
return captcha

Cookie登录

  目前网站的验证码越来越复杂,某些验证码已经复杂到人类难以识别的程度,有时提交表单登录的路子难以走通。此时我们可以换一种登录爬取的思路,在使用浏览器登录网站后,包含用户身份信息的Cookie会被浏览器保存在本地,如果scrapy爬虫能直接使用浏览器中的Cookie发生HTTP请求,就可以绕过提交表单登录的步骤。

(1)获取浏览器Cookie

  不用研究种浏览器将Cookie以哪种形式存储在哪里,可以用第三方Python库browsercookie,就能获取chrome和firefox浏览器中的Cookie

1
2
3
4
5
6
import browsercookie
chrome_cookiejar=browsercookie.chrome() #获取chrome浏览器中的cookie
firefox_cookiejar=browsercookie.firefox() #获取firefox浏览器中的cookie
type(chrome_cookiejar)
for cookie in chrome_cookiejar:
print(cookie)

  browsercookie的chrome和firefox方法分别返回chrome和firefox浏览器中的cookie,返回值是一个http.cookiejar.CookieJar对象,对CookieJar对象进行迭代,可以访问其中的每个Cookie对象

(2)CookiesMiddleware源码分析

  scrapy爬虫所使用的cookie由内置下载中间件CookiesMiddleware自动处理。下面我们来分析一下CookiesMiddleware是如何工作的,源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import os,six,logging
from collections import defaultdict
from scrapy.exceptions import NotConfigured
from scrapy.http import Response
from scrapy.http.cookies import CookieJar
from scrapy.utils.python import to_native_str

logger=logging.getLogger(__naem__)
class CookiesMiddleware(object):
def __init__(self,debug=False):
self.jars=defaultdict(CookieJar) #创建一个默认字典,每一项都是一个scrapy.http.cookies.CookieJar对象
self.debug=debug

#CookiesMiddleware能让爬虫同时使用多个不同CookieJar,比如在某网站我们注册了两个账号
#若想同时登陆两个账号爬取网站,为了避免Cookie冲突,可以让每个账号发生的HTTP请求使用不同的CookieJar
#在构造Requset对象时,参数meta={'cookiejar':'账号'},表示该账号发送的请求

@classmethod
def from_crawler(cls,crawler):
if not crawler.settings.getbool('COOKIES_ENABLED'); #从设置文件读取COOKIES_ENABLED,决定是否启用中间件
raise NotConfigured #如果不启用,则抛出NotConfigured异常,Scrapy将忽略该中间件
return cls(crawler.settings.getbool('COOKIES_DEBUG')) #如果启用,调用构造器创建对象

def process_request(self,request,spider): #处理每一个待发送的Request对象
if request.meta.get('dont_merge_cookies',False):
return
cookiejarkey=request.meta.get("cookiejar") #获取用户指定的CookieJar,未指定则用CookieJar(self.jars[None])
jar=self.jars[cookiejarkey]
cookies=self._get_request_cookies(jar,request) #获取发送请求request应携带的Cookie信息,填写到HTTP请求
for cookie in cookies:
jar.set_cookie_if_ok(cookie,request)
request.headers.pop('Cookie',None)
jar.add_cookie_header(request)
self._debug_cookie(request,spider)

def process_response(self,request,response,spider): #处理每一个Response对象
if request.meta.get('dont_merge_cookies',False):
return response
cookiejarkey=request.meta.get('cookiejar') #获取CookieJar对象
jar=self.jars[cookiejarkey]
jar.extract_cookies(resposne,request) #将HTTP响应头部中的Cookie信息保存到CookieJar对象中
self._debug_set_set_cookie(response,spider)
return response
#注意:这里的CookieJar是scrapy.http.cookies.CookieJar
#而第一节的CookieJar是标准库中的http.cookiejar.CookieJar,是不同的类
#前者对后者进行了包装,两者可以互相转化

def _debug_cookie(self,request,spider):
if self.debug:
cl=[to_native_str(c,errors='replace')
for c in request.headers.getlist('Cookie')]
if cl:
cookies="\n".join("Cookie:{ }\n".format(c) for c in cl)
msg="Sending cookies to:{ }\n{ }".format(request,cookies)
logger.debug(msg,extra={'spider':spider})

def _debug_set_cookie(self,response,spider):
if self.debug:
cl=[to_native_str(c,errors='replace')
for c in response.headers.getlist('Set-Cookie')]
if cl:
cookies="\n".join("Set-Cookie:{ }\n".format(c) for c in cl)
msg="Received cookies from:{ }\n{ }".format(response,cookies)
logger.debug(msg,extra={'spider':spider})

def _format_cookie(self,cookie):
cookie_str='%s=%s'%(cookie['name'],cookie['value'])
if cookie.get('path',None):
cookie_str += ';Path=%s'%cookie['path']
if cookie.get('domain',None):
cookie_str += ';Domain=%s'%cookie['domain']
return cookie_str

def _get_request_cookies(self,jar,request):
if isinstance(request.cookies,dict):
cookie_list=[{'name':k,'value':v} for k,v in six.iteritems(request.cookies)]
else:
cookie_list=request.cookies
cookies=[self._format_cookie(x) for x in cookie_list]
headers = {'Set-Cookie':cookies}
response=Response(request.url,headers=headers)
return jar.make_cookies(response,request)

(3)实现BrowserCookiesMiddleware

CookiesMiddleware自动处理头部Cookie的特性给用户提供了便利,但它不能使用浏览器的Cookie,我们可以利用browsercookie对CookiesMiddleware进行改良,实现一个能使用浏览器Cookie的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import browsercookiess
from scrapy.downloadermiddlewares.cookies import CookiesMiddleware

class BrowserCookiesMiddleware(CookiesMiddleware): #继承CookiesMiddleware并实现构造器方法
def __init__(self,debug=Fales):
super().__init__(debug)
self.load_browser_cookies() #加载浏览器Cookie

#浏览器中的Cookie存储在CookieJar字典中self.jars中
def load_browser_cookies(self):
jar = self.jars['chrome'] #从默认字典中获得CookieJar对象
chrome_cookiejar = browsercookie.chrome() #获得chrome浏览器中的cookie,填入CookieJar对象中
for cookie in chrome_cookiejar:
jar.set_cookie(cookie)
jar = self.jars['firefox']
forefox_cookiejar = browsercookie.firefox()
for cookie in firefox_cookiejar:
jar.set_cookie(cookie)

例子:爬取知乎个人信息

  通过例子展示BrowserCookiesMiddleware的使用:在Chrome浏览器登录知乎后,可以访问用户个人信息页面,然后我们用BrowserCookiesMiddleware爬取这个登录后才能访问的页面,提取用户的“姓名”和“个性域名” 信息
   ①创建项目
   ②将BrowserCookiesMiddleware源码赋值到该项目下的middlewares.py中,并在配置文件settings.py中添加如下配置:

1
2
3
4
5
6
7
#伪装成常规浏览器
USER_AGENT='Mozilla/5.0(X11;Linux x86_64)Chrome/42.0.23311.90 Safari/537.36'
#用BrowserCookiesMiddleware替代CookiesMiddleware启用前者,关闭后者
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware':None,
'browser_cookie.middlewares.BrowserCookiesMiddleware':701,
}

   ③由于需求非常简单,所以不再编写Spider,直接在scrapy shell环境中进行演示。注意,为了使用项目中的配置,需要在项目目录下启动scrapy shell命令:

1
2
3
4
5
>>scrpay shell
>>from scrapy import Request
>>url = '网站'
>>fetch(Request(url,meta={'cookiejar':'chrome'}))
>>view(response)

   调用了view函数后,在浏览器中看到页面。结果表明BrowserCookiesMiddleware按我们所预期的工作了,Scrapy爬虫使用浏览器的Cookie成功的获取了一个需要用户登录后才能访问的页面
   ④提取页面中的“姓名”和“个性域名”信息

1
2
resposne.css('div#rename-section span.name::text').extract_first()     #返回"硕-奶酪'
response.xpath('string(//div[@id="js-url-preview"])').extract_first() #返回'zhihu.com/people/shuo_cheese'

  小结:学习了scrapy爬虫模拟登录网站的相关内容,首先介绍了网站登录的原理,并讲解如何使用FormRequest提交登录表单模拟登录,然后讲解识别验证码的3种方法,最后介绍如何使用浏览器Cookie直接登录,并实现了一个下载中间件BrowserCookiesMiddleware