不少时候,在爬取没有登陆的状况下,咱们也能够访问一部分页面或请求一些接口,由于毕竟网站自己须要作SEO,不会对全部页面都设置登陆限制。
git
可是,不登陆直接爬取会有一些弊端,弊端主要有如下两点。github
设置了登陆限制的页面没法爬取。如某论坛设置了登陆才可查看资源,某博客设置了登陆才可查看全文等,这些页面都须要登陆帐号才能够查看和爬取。web
一些页面和接口虽然能够直接请求,可是请求一旦频繁,访问就容易被限制或者IP直接被封,可是登陆以后就不会出现这样的问题,所以登陆以后被反爬的可能性更低。redis
下面咱们就第二种状况作一个简单的实验。以微博为例,咱们先找到一个Ajax接口,例如新浪财经官方微博的信息接口https://m.weibo.cn/api/container/getIndex?uid=1638782947&luicode=20000174&type=uid&value=1638782947&containerid=1005051638782947,若是用浏览器直接访问,返回的数据是JSON格式,以下图所示,其中包含了新浪财经官方微博的一些信息,直接解析JSON便可提取信息。数据库
可是,这个接口在没有登陆的状况下会有请求频率检测。若是一段时间内访问太过频繁,好比打开这个连接,一直不断刷新,则会看到请求频率太高的提示,以下图所示。json
若是从新打开一个浏览器窗口,打开https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/,登陆微博帐号以后从新打开此连接,则页面正常显示接口的结果,而未登陆的页面仍然显示请求过于频繁,以下图所示。flask
图中左侧是登陆了帐号以后请求接口的结果,右侧是未登陆帐号请求接口的结果,两者的接口连接是彻底同样的。未登陆状态没法正常访问,而登陆状态能够正常显示。api
所以,登陆帐号能够下降被封禁的几率。浏览器
咱们能够尝试登陆以后再作爬取,被封禁的概率会小不少,可是也不能彻底排除被封禁的风险。若是一直用同一个帐号频繁请求,那就有可能遇到请求过于频繁而封号的问题。cookie
若是须要作大规模抓取,咱们就须要拥有不少帐号,每次请求随机选取一个帐号,这样就下降了单个帐号的访问频率,被封的几率又会大大下降。
那么如何维护多个帐号的登陆信息呢?这时就须要用到Cookies池了。接下来咱们看看Cookies池的构建方法。
咱们以新浪微博为例来实现一个Cookies池的搭建过程。Cookies池中保存了许多新浪微博帐号和登陆后的Cookies信息,而且Cookies池还须要定时检测每一个Cookies的有效性,若是某Cookies无效,那就删除该Cookies并模拟登陆生成新的Cookies。同时Cookies池还须要一个很是重要的接口,即获取随机Cookies的接口,Cookies运行后,咱们只需请求该接口,便可随机得到一个Cookies并用其爬取。
因而可知,Cookies池须要有自动生成Cookies、定时检测Cookies、提供随机Cookies等几大核心功能。
搭建以前确定须要一些微博的帐号。须要安装好Redis数据库并使其正常运行。须要安装Python的RedisPy、requests、Selelnium、Flask库。另外,还须要安装Chrome浏览器并配置好ChromeDriver。
Cookies的架构和代理池相似,一样是4个核心模块,以下图所示。
Cookies池架构的基本模块分为4块:存储模块、生成模块、检测模块、接口模块。每一个模块的功能以下。
存储模块负责存储每一个帐号的用户名密码以及每一个帐号对应的Cookies信息,同时还须要提供一些方法来实现方便的存取操做。
生成模块负责生成新的Cookies。此模块会从存储模块逐个拿取帐号的用户名和密码,而后模拟登陆目标页面,判断登陆成功,就将Cookies返回并交给存储模块存储。
检测模块须要定时检测数据库中的Cookies。在这里咱们须要设置一个检测连接,不一样的站点检测连接不一样,检测模块会逐个拿取帐号对应的Cookies去请求连接,若是返回的状态是有效的,那么此Cookies没有失效,不然Cookies失效并移除。接下来等待生成模块从新生成便可。
接口模块须要用API来提供对外服务的接口。因为可用的Cookies可能有多个,咱们能够随机返回Cookies的接口,这样保证每一个Cookies都有可能被取到。Cookies越多,每一个Cookies被取到的几率就会越小,从而减小被封号的风险。
以上设计Cookies池的的基本思路和前面讲的代理池有类似之处。接下来咱们设计总体的架构,而后用代码实现该Cookies池。
首先分别了解各个模块的实现过程。
其实,须要存储的内容无非就是帐号信息和Cookies信息。帐号由用户名和密码两部分组成,咱们能够存成用户名和密码的映射。Cookies能够存成JSON字符串,可是咱们后面得须要根据帐号来生成Cookies。生成的时候咱们须要知道哪些帐号已经生成了Cookies,哪些没有生成,因此须要同时保存该Cookies对应的用户名信息,其实也是用户名和Cookies的映射。这里就是两组映射,咱们天然而然想到Redis的Hash,因而就创建两个Hash,结构分别以下图所示。
Hash的Key就是帐号,Value对应着密码或者Cookies。另外须要注意,因为Cookies池须要作到可扩展,存储的帐号和Cookies不必定单单只有本例中的微博,其余站点一样能够对接此Cookies池,因此这里Hash的名称能够作二级分类,例如存帐号的Hash名称能够为accounts:weibo,Cookies的Hash名称能够为cookies:weibo。如要扩展知乎的Cookies池,咱们就可使用accounts:zhihu和cookies:zhihu,这样比较方便。
接下来咱们建立一个存储模块类,用以提供一些Hash的基本操做,代码以下:
import random
import redis
class RedisClient(object):
def __init__(self, type, website, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
"""
初始化Redis链接
:param host: 地址
:param port: 端口
:param password: 密码
"""
self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
self.type = type
self.website = website
def name(self):
"""
获取Hash的名称
:return: Hash名称
"""
return "{type}:{website}".format(type=self.type, website=self.website)
def set(self, username, value):
"""
设置键值对
:param username: 用户名
:param value: 密码或Cookies
:return:
"""
return self.db.hset(self.name(), username, value)
def get(self, username):
"""
根据键名获取键值
:param username: 用户名
:return:
"""
return self.db.hget(self.name(), username)
def delete(self, username):
"""
根据键名删除键值对
:param username: 用户名
:return: 删除结果
"""
return self.db.hdel(self.name(), username)
def count(self):
"""
获取数目
:return: 数目
"""
return self.db.hlen(self.name())
def random(self):
"""
随机获得键值,用于随机Cookies获取
:return: 随机Cookies
"""
return random.choice(self.db.hvals(self.name()))
def usernames(self):
"""
获取全部帐户信息
:return: 全部用户名
"""
return self.db.hkeys(self.name())
def all(self):
"""
获取全部键值对
:return: 用户名和密码或Cookies的映射表
"""
return self.db.hgetall(self.name())
这里咱们新建了一个RedisClien
t类,初始化__init__()
方法有两个关键参数type
和website
,分别表明类型和站点名称,它们就是用来拼接Hash名称的两个字段。若是这是存储帐户的Hash,那么此处的type
为accounts
、website
为weibo
,若是是存储Cookies的Hash,那么此处的type
为cookies
、website
为weibo
。
接下来还有几个字段表明了Redis的链接信息,初始化时得到这些信息后初始化StrictRedis
对象,创建Redis链接。
name()
方法拼接了type
和website
,组成Hash的名称。set()
、get()
、delete()
方法分别表明设置、获取、删除Hash的某一个键值对,count()
获取Hash的长度。
比较重要的方法是random()
,它主要用于从Hash里随机选取一个Cookies并返回。每调用一次random()
方法,就会得到随机的Cookies,此方法与接口模块对接便可实现请求接口获取随机Cookies。
生成模块负责获取各个帐号信息并模拟登陆,随后生成Cookies并保存。咱们首先获取两个Hash的信息,看看帐户的Hash比Cookies的Hash多了哪些尚未生成Cookies的帐号,而后将剩余的帐号遍历,再去生成Cookies便可。
这里主要逻辑就是找出那些尚未对应Cookies的帐号,而后再逐个获取Cookies,代码以下:
for username in accounts_usernames:
if not username in cookies_usernames:
password = self.accounts_db.get(username)
print('正在生成Cookies', '帐号', username, '密码', password)
result = self.new_cookies(username, password)
由于咱们对接的是新浪微博,前面咱们已经破解了新浪微博的四宫格验证码,在这里咱们直接对接过来便可,不过如今须要加一个获取Cookies的方法,并针对不一样的状况返回不一样的结果,逻辑以下所示:
def get_cookies(self):
return self.browser.get_cookies()
def main(self):
self.open()
if self.password_error():
return {
'status': 2,
'content': '用户名或密码错误'
}
# 若是不须要验证码直接登陆成功
if self.login_successfully():
cookies = self.get_cookies()
return {
'status': 1,
'content': cookies
}
# 获取验证码图片
image = self.get_image('captcha.png')
numbers = self.detect_image(image)
self.move(numbers)
if self.login_successfully():
cookies = self.get_cookies()
return {
'status': 1,
'content': cookies
}
else:
return {
'status': 3,
'content': '登陆失败'
}
这里返回结果的类型是字典,而且附有状态码status
,在生成模块里咱们能够根据不一样的状态码作不一样的处理。例如状态码为1的状况,表示成功获取Cookies,咱们只须要将Cookies保存到数据库便可。如状态码为2的状况,表明用户名或密码错误,那么咱们就应该把当前数据库中存储的帐号信息删除。如状态码为3的状况,则表明登陆失败的一些错误,此时不能判断是否用户名或密码错误,也不能成功获取Cookies,那么简单提示再进行下一个处理便可,相似代码实现以下所示:
result = self.new_cookies(username, password)
# 成功获取
if result.get('status') == 1:
cookies = self.process_cookies(result.get('content'))
print('成功获取到Cookies', cookies)
if self.cookies_db.set(username, json.dumps(cookies)):
print('成功保存Cookies')
# 密码错误,移除帐号
elif result.get('status') == 2:
print(result.get('content'))
if self.accounts_db.delete(username):
print('成功删除帐号')
else:
print(result.get('content'))
若是要扩展其余站点,只须要实现new_cookies()
方法便可,而后按此处理规则返回对应的模拟登陆结果,好比1表明获取成功,2表明用户名或密码错误。
代码运行以后就会遍历一次还没有生成Cookies的帐号,模拟登陆生成新的Cookies。
咱们如今能够用生成模块来生成Cookies,但仍是免不了Cookies失效的问题,例如时间太长致使Cookies失效,或者Cookies使用太频繁致使没法正常请求网页。若是遇到这样的Cookies,咱们确定不能让它继续保存在数据库里。
因此咱们还须要增长一个定时检测模块,它负责遍历池中的全部Cookies,同时设置好对应的检测连接,咱们用一个个Cookies去请求这个连接。若是请求成功,或者状态码合法,那么该Cookies有效;若是请求失败,或者没法获取正常的数据,好比直接跳回登陆页面或者跳到验证页面,那么此Cookies无效,咱们须要将该Cookies从数据库中移除。
此Cookies移除以后,刚才所说的生成模块就会检测到Cookies的Hash和帐号的Hash相比少了此帐号的Cookies,生成模块就会认为这个帐号还没生成Cookies,那么就会用此帐号从新登陆,此帐号的Cookies又被从新更新。
检测模块须要作的就是检测Cookies失效,而后将其从数据中移除。
为了实现通用可扩展性,咱们首先定义一个检测器的父类,声明一些通用组件,实现以下所示:
class ValidTester(object):
def __init__(self, website='default'):
self.website = website
self.cookies_db = RedisClient('cookies', self.website)
self.accounts_db = RedisClient('accounts', self.website)
def test(self, username, cookies):
raise NotImplementedError
def run(self):
cookies_groups = self.cookies_db.all()
for username, cookies in cookies_groups.items():
self.test(username, cookies)
在这里定义了一个父类叫做ValidTester
,在__init__()
方法里指定好站点的名称website
,另外创建两个存储模块链接对象cookies_db
和accounts_db
,分别负责操做Cookies和帐号的Hash,run()
方法是入口,在这里是遍历了全部的Cookies,而后调用test()
方法进行测试,在这里test()
方法是没有实现的,也就是说咱们须要写一个子类来重写这个test()
方法,每一个子类负责各自不一样网站的检测,如检测微博的就能够定义为WeiboValidTester
,实现其独有的test()
方法来检测微博的Cookies是否合法,而后作相应的处理,因此在这里咱们还须要再加一个子类来继承这个ValidTester
,重写其test()
方法,实现以下:
import json
import requests
from requests.exceptions import ConnectionError
class WeiboValidTester(ValidTester):
def __init__(self, website='weibo'):
ValidTester.__init__(self, website)
def test(self, username, cookies):
print('正在测试Cookies', '用户名', username)
try:
cookies = json.loads(cookies)
except TypeError:
print('Cookies不合法', username)
self.cookies_db.delete(username)
print('删除Cookies', username)
return
try:
test_url = TEST_URL_MAP[self.website]
response = requests.get(test_url, cookies=cookies, timeout=5, allow_redirects=False)
if response.status_code == 200:
print('Cookies有效', username)
print('部分测试结果', response.text[0:50])
else:
print(response.status_code, response.headers)
print('Cookies失效', username)
self.cookies_db.delete(username)
print('删除Cookies', username)
except ConnectionError as e:
print('发生异常', e.args)
test()
方法首先将Cookies转化为字典,检测Cookies的格式,若是格式不正确,直接将其删除,若是格式没问题,那么就拿此Cookies请求被检测的URL。test()
方法在这里检测微博,检测的URL能够是某个Ajax接口,为了实现可配置化,咱们将测试URL也定义成字典,以下所示:
TEST_URL_MAP = {
'weibo': 'https://m.weibo.cn/'
}
若是要扩展其余站点,咱们能够统一在字典里添加。对微博来讲,咱们用Cookies去请求目标站点,同时禁止重定向和设置超时时间,获得Response以后检测其返回状态码。若是直接返回200状态码,则Cookies有效,不然可能遇到了302跳转等状况,通常会跳转到登陆页面,则Cookies已失效。若是Cookies失效,咱们将其从Cookies的Hash里移除便可。
生成模块和检测模块若是定时运行就能够完成Cookies实时检测和更新。可是Cookies最终仍是须要给爬虫来用,同时一个Cookies池可供多个爬虫使用,因此咱们还须要定义一个Web接口,爬虫访问此接口即可以取到随机的Cookies。咱们采用Flask来实现接口的搭建,代码以下所示:
import json
from flask import Flask, g
app = Flask(__name__)
# 生成模块的配置字典
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator'
}
@app.route('/')
def index():
return '<h2>Welcome to Cookie Pool System</h2>'
def get_conn():
for website in GENERATOR_MAP:
if not hasattr(g, website):
setattr(g, website + '_cookies', eval('RedisClient' + '("cookies", "' + website + '")'))
return g
@app.route('/<website>/random')
def random(website):
"""
获取随机的Cookie, 访问地址如 /weibo/random
:return: 随机Cookie
"""
g = get_conn()
cookies = getattr(g, website + '_cookies').random()
return cookies
咱们一样须要实现通用的配置来对接不一样的站点,因此接口连接的第一个字段定义为站点名称,第二个字段定义为获取的方法,例如,/weibo/random是获取微博的随机Cookies,/zhihu/random是获取知乎的随机Cookies。
最后,咱们再加一个调度模块让这几个模块配合运行起来,主要的工做就是驱动几个模块定时运行,同时各个模块须要在不一样进程上运行,实现以下所示:
import time
from multiprocessing import Process
from cookiespool.api import app
from cookiespool.config import *
from cookiespool.generator import *
from cookiespool.tester import *
class Scheduler(object):
@staticmethod
def valid_cookie(cycle=CYCLE):
while True:
print('Cookies检测进程开始运行')
try:
for website, cls in TESTER_MAP.items():
tester = eval(cls + '(website="' + website + '")')
tester.run()
print('Cookies检测完成')
del tester
time.sleep(cycle)
except Exception as e:
print(e.args)
@staticmethod
def generate_cookie(cycle=CYCLE):
while True:
print('Cookies生成进程开始运行')
try:
for website, cls in GENERATOR_MAP.items():
generator = eval(cls + '(website="' + website + '")')
generator.run()
print('Cookies生成完成')
generator.close()
time.sleep(cycle)
except Exception as e:
print(e.args)
@staticmethod
def api():
print('API接口开始运行')
app.run(host=API_HOST, port=API_PORT)
def run(self):
if API_PROCESS:
api_process = Process(target=Scheduler.api)
api_process.start()
if GENERATOR_PROCESS:
generate_process = Process(target=Scheduler.generate_cookie)
generate_process.start()
if VALID_PROCESS:
valid_process = Process(target=Scheduler.valid_cookie)
valid_process.start()
这里用到了两个重要的配置,即产生模块类和测试模块类的字典配置,以下所示:
# 产生模块类,如扩展其余站点,请在此配置
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator'
}
# 测试模块类,如扩展其余站点,请在此配置
TESTER_MAP = {
'weibo': 'WeiboValidTester'
}
这样的配置是为了方便动态扩展使用的,键名为站点名称,键值为类名。如须要配置其余站点能够在字典中添加,如扩展知乎站点的产生模块,则能够配置成:
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator',
'zhihu': 'ZhihuCookiesGenerator',
}
Scheduler里将字典进行遍历,同时利用eval()
动态新建各个类的对象,调用其入口run()
方法运行各个模块。同时,各个模块的多进程使用了multiprocessing中的Process类,调用其start()
方法便可启动各个进程。
另外,各个模块还设有模块开关,咱们能够在配置文件中自由设置开关的开启和关闭,以下所示:
# 产生模块开关
GENERATOR_PROCESS = True
# 验证模块开关
VALID_PROCESS = False
# 接口模块开关
API_PROCESS = True
定义为True便可开启该模块,定义为False即关闭此模块。
至此,咱们的Cookies就所有完成了。接下来咱们将模块同时开启,启动调度器,控制台相似输出以下所示:
API接口开始运行 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) Cookies生成进程开始运行 Cookies检测进程开始运行 正在生成Cookies 帐号 14747223314 密码 asdf1129 正在测试Cookies 用户名 14747219309 Cookies有效 14747219309 正在测试Cookies 用户名 14740626332 Cookies有效 14740626332 正在测试Cookies 用户名 14740691419 Cookies有效 14740691419 正在测试Cookies 用户名 14740618009 Cookies有效 14740618009 正在测试Cookies 用户名 14740636046 Cookies有效 14740636046 正在测试Cookies 用户名 14747222472 Cookies有效 14747222472 Cookies检测完成 验证码位置 420 580 384 544 成功匹配 拖动顺序 [1, 4, 2, 3] 成功获取到Cookies {'SUHB': '08J77UIj4w5n_T', 'SCF': 'AimcUCUVvHjswSBmTswKh0g4kNj4K7_U9k57YzxbqFt4SFBhXq3Lx4YSNO9VuBV841BMHFIaH4ipnfqZnK7W6Qs.', 'SSOLoginState': '1501439488', '_T_WM': '99b7d656220aeb9207b5db97743adc02', 'M_WEIBOCN_PARAMS': 'uicode%3D20000174', 'SUB': '_2A250elZQDeRhGeBM6VAR8ifEzTuIHXVXhXoYrDV6PUJbkdBeLXTxkW17ZoYhhJ92N_RGCjmHpfv9TB8OJQ..'} 成功保存Cookies
以上所示是程序运行的控制台输出内容,咱们从中能够看到各个模块都正常启动,测试模块逐个测试Cookies,生成模块获取还没有生成Cookies的帐号的Cookies,各个模块并行运行,互不干扰。
咱们能够访问接口获取随机的Cookies,以下图所示。
爬虫只须要请求该接口就能够实现随机Cookies的获取。
本节代码地址为:https://github.com/Python3WebSpider/CookiesPool。