• 第十部分 模拟登录(模拟登录GitHub并爬取、Cookies池的搭建)


    前言:有些页面的信息在爬虫时需要登录才能查看。打开网页登录后,在客户端生成了Cookies,在Cookies中保存了SessionID的信息,登录后的请求都会携带生成后的Cookies发送给服务器。服务器根据Cookies判断出对应的SessionID,进而找到会话。如果当前会话有效,服务器就判断用户当前已登录,返回请求的页面信息,这样就可以看到登录后的页面。

    这里主要是获取登录后Cookies。要获取Cookies可以手动在浏览器输入用户名和密码后,再把Cookies复制出来,这样做就增加了人工工作量,爬虫的目的是自动化,需要用程序来完成这个过程,也就是用程序来模拟登录。下面来了解模拟登录相关方法及如何维护一个Cookies池。

    一、 模拟登录并爬取GitHub
    模拟登录的原理在于登录后Cookies的维护。

    了解模拟登录GitHub的过程,同时爬取登录后才可以访问的页面信息,如好友动态、个人信息等内容。

    需要使用到的库有:requests和 lxml 库。

    1、 分析登录过程
    打开GitHub的登录页面https://github.com/login,输入用户名和密码,打开开发者工具,勾选Preserve Log选项,这表示显示持续日志。点击登录按钮,就会在开发者工具下方显示各个请求过程。点击第一个请求(session),进入其详情页面,如图1-1所示。
    图1-1  session请求详情面

                                                                                 图1-1     session请求详情面
    从图上可看到请求的URL是 https://github.com/session,请求方式为POST。继续往下看,可以观察到它的Request Headers和Form Data 这两部分内容。如图1-2所示。
    图1-2  Request Headers和Form Data详情页面

                                                                                     图1-2     Request Headers和Form Data详情页面
    Headers里面包含了 Cookies、Host、Origin、Referer、User-Agent等信息。Form Data包含了6个字段,commit 是固定的字符串Sign in,utf8 是一个勾选字符,authenticity_token 较长,初步判断是一个Base64加密的字符串,login是登录的用户名,password是登录的密码,webauthn-support是页面认证,默认是supported。

    由上可知,现在不能构造的内容有 Cookies和 authenticity_token。下面继续看下这两部分内容如何获取。在登录前访问的是登录页面,该页面是以GET形式访问的。输入用户名和密码,点击登录按钮,浏览器发送这两部分信息,也就是说Cookies和 authenticity_token一定是在访问登录页面时候设置的。

    再次退出登录,清空Cookies,回到登录页。重新登录,截获发生的请求,如图1-3所示。
    图1-3  截获的请求

                                                                                                   图1-3     截获的请求
    在截获的请求中,Response Headers有一个 Set-Cookie 字段。这就是设置 Cookies 的过程。另外,在Response Headers中没有和authenticity_token相关的信息,这个 authenticity_token 可能隐藏在其他地方或者计算出来的。不过在网页的源代码中,搜索 authenticity_token 相关的字段,发现了源代码里面隐藏着此信息,是由一个隐藏式表单元素。如图1-4所示。
    图1-4  表单元素之authenticity_token

                                                                                           图1-4     表单元素之authenticity_token
    到此,已经获取到了所有信息,接下来实现模拟登录。

    2、模拟登录代码实例
    先来定义一个Login 类,初始化一些变量,代码如下所示:

     1 import requests
     2 from lxml import etree
     3 class Login():
     4     """登录类,初始化一些变量"""
     5     def __init__(self):
     6         self.headers = {
     7             'Referer': 'https://github.com/login',
     8             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
     9             'Host': 'github.com',
    10         }
    11         self.login_url = 'https://github.com/login'
    12         self.post_url = 'https://github.com/session'
    13         self.logined_url = 'https://github.com/settings/profile'    # 登录成功后的页面
    14         self.session = requests.Session()

    这段代码中最重要的一个变量是requests库的 Session,它可以维持一个会话,而且可以自动处理 Cookies,不用担心 Cookies的问题。接下来,访问登录页面还要完成两件事,一是通过登录页面获取初始的 Cookies,二是提取出 authenticity_token。下面实现一个token()方法,代码如下所示:

    1 def token(self):
    2     response = self.session.get(self.login_url, headers=self.headers)
    3     selector = etree.HTML(response.text)
    4     token = selector.xpath('//div//input[2]/@value')    # 注意获取到的是一个列表类型
    5     return token

    这里用Session对象的 get() 方法访问GitHub的登录页面,接着用XPath解析出登录所需的 authenticity_token 信息并返回。现在已经获取初始的 Cookies和authenticity_token,下面开始模拟登录,实现一个 login() 方法,代码如下所示:

     1 def login(self, email, password):
     2     post_data = {
     3         'commit': 'Sign in',
     4         'utf8': '',
     5         'authenticity_token': self.token()[0],
     6         'login': email,
     7         'password': password,
     8         'webauthn-support': 'supported'
     9     }
    10     response = self.session.post(self.post_url, data=post_data, headers=self.headers)
    11     if response.status_code == 200:
    12         self.dynamics(response.text)
    13 
    14     response = self.session.get(self.logined_url, headers=self.headers)
    15     if response.status_code == 200:
    16         self.profile(response.text)

    这里先构造一个表单,复制各个字段,其中email和password是以变量的形式传递。然后再用Session对象的post()方法模拟登录即可。由于 requests 自动处理了重定向信息,登录成功后就可直接跳转到首页,首页有显示所关注人的动态信息,得到响应后调用dynamics()方法对其进行处理。接下来再用Session对象请求个人详情页,调用profile()方法处理个人详情页信息。其中,dynamics()和profile()方法的实现如下所示:

     1 def dynamics(self, html):
     2     """处理登录成功后的页面,即主页面内容"""
     3     # 页面已经发生跳转,该段代码的输出为空
     4     selector = etree.HTML(html)
     5     print(html)
     6     dynamics = selector.xpath('//div[contains(@class, "news")]//div[contains(@class, "Box")]')
     7     for item in dynamics:
     8         dynamic = ' '.join(item.xpath('.//div[@class="title"]//text()')).strip()
     9         print(dynamic)
    10 
    11 def profile(self, html):
    12     """处理登录成功后的 profile 页面"""
    13     selector = etree.HTML(html)
    14     # 下面获取到的每一项数据都是列表
    15     name = selector.xpath('//input[@id="user_profile_name"]/@value')
    16     url = selector.xpath('//input[@id="user_profile_blog"]/@value')
    17     company = selector.xpath('//input[@id="user_profile_company"]/@value')
    18     location = selector.xpath('//input[@id="user_profile_location"]/@value')
    19     email = selector.xpath('//select[@id="user_profile_email"]/option[@value!=""]/text()')
    20     print(name, email, url, company, location)
    21 
    22 if __name__ == '__main__':
    23     login = Login()
    24     login.login(email='email or  username', password='password')

    这里用XPath对信息进行提取,在dynamics()方法里,提取所有的动态信息并输出(网址已发生跳转,输出为空)。在profile()里,提取个人信息并将其输出。现在完成了整个类的编写,在最后面的if代码块中,先创建Login类对象,然后运行程序,通过调用login()方法传入用户名和密码,成功实现了模拟登录,并且成功输出用户个人信息。

    利用requests的Session实现模拟登录操作,最重要的是分析思路,只要各个参数都成功获取,模拟登录就没有问题。登录成功后,就相当于建立一个 Session会话,Session对象维护着Cookies的信息,直接请求就会得到模拟登录成功后的页面。

    二、 Cookies池的搭建

    不登录直接爬取网站内容可能有下面的限制:
    (1)、设置了登录限制的页面不能爬取。如某些论坛设置了登录可查看资源,一些博客设置了登录才可查看全文等。
    (2)、有的页面请求过于频繁,访问容易被限制或者IP被封,但是登录后不会出现这些问题。因此登录后被反爬的可能性低。

    例如新浪财经官方微博的Ajax接口 https://m.weibo.cn/api/container/getIndex?uid=1804544030&type=uid&page=1&containerid=1076031804544030,这个网站用浏览器直接访问返回JSON格式信息,直接解析JSON即可提取信息。这个接口在没有登录的情况下会有请求频率检测。一段时间内请求过于频繁,请求就会被限制并提示请求过于频繁。

    重新打开浏览器窗口,打开 https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/,登录微博账号后重新打开这API接口连接可以正常显示。但是登录后一直用同一个账号频繁请求,也会有可能被封号。所在在大规模抓取,就要拥有很多账号,每次请求随机选择一个账号,这样降低单个账号的访问频率,来降低被封的概率。要维护多个账号的登录信息,就要用到Cookies池。下面就Cookies池的搭建做一些了解。

    以新浪微博为例实现一个Cookies池的搭建过程。Cookies池中保存了许多微博账号和登录后的Cookies信息,并且Cookies池还需要定时检测每个Cookies的有效性,如果Cookies无效,就删除该Cookies并模拟登录生成的Cookies。同时Cookies池还需要一个重要的接口,即获取随机Cookies的接口,Cookies运行后,只要请求该接口,即可随机获得一个Cookies并用其爬取。由此可知,Cookies池需要自动生成Cookies、定时检测Cookies、提供随机Cookies等功能。

    基本要求:Redis数据库正常运行。Python的redis-py、requests、Selelnium和Flask库。以及Chrome浏览器的安装并配置 ChromeDriver。

    1、Cookies池架构
    Cookies池架构的基本模块分为4块:存储模块、生成模块、检测模块和接口模块。每个模块功能如下:
    (1)、存储模块负责存储每个账号的用户名密码以及每个账号对应的Cookies信息,同时还需要提供一些方法来实现方便的存取操作。
    (2)、生成模块可生成新的Cookies。从存储模块获取账号的用户名和密码,然后模拟登录目标页面,判断登录成功,就将Cookies返回并交给存储模块存储。
    (3)、检测模块定时检测数据库中的Cookies。可设置一个检测连接,不同的站点检测连接不同,检测模块会逐个获取账号对应的Cookies去请求链接,如果返回的状态是有效的,此Cookies就没有失效,否则Cookies失效并移除。接下来等待生成模块重新生成。
    (4)、接口模块用API对外提供服务接口。可用的Cookies有多个,可随机返回Cookies的接口,这样保证每个Cookies都有可能被取到。Cookies越多,每个Cookies被取到的概率越小,封号的风险也越小。

    2、Cookies 池的实现
    对各个模块的实现过程做一些了解。

    (1)、存储模块
    存储的内容有账号信息和Cookies信息。账号由用户名和密码组成,将用户名和密码在数据库中存储成映射关系。Cookies存成JSON字符串,并且要对应用户名信息,实际也是用户名和Cookies的映射。可以用Redis的Hash结构,需要建立两个Hash结构,用户名和密码Hash,用户名和Cookies的Hash。

    Hash的Key对应账号,Value对应密码或者Cookies。还要注意的是,Cookies池要做到可扩展,也就是存储的账号和Cookies不一定只有新浪微博的,其他站点同样可以对接此Cookies池,所以对Hash的名称做二级分类,如存微博账号的Hash名称可以是 accounts:weibo,Cookies的名称可以是 cookies:weibo。如果要扩展知乎的Cookies池,可使用 accounts:zhihu和 cookies:zhihu。

    下面代码创建一个存储模块类,用以提供一些Hash的基本操作,代码如下:
    首先将一些基本配置放在一个config.py文件,避免各个模块的代码杂乱,config.py 文件的代码如下:

     1 # Redis 数据库地址
     2 REDIS_HOST = '192.168.64.50'
     3 
     4 # Redis 端口
     5 REDIS_PORT = 6379
     6 
     7 # Redis密码,无密码就为 None
     8 REDIS_PASSWORD = None
     9 
    10 # 产生器使用的浏览器
    11 BROWSER_TYPE = 'Chrome'
    12 
    13 # 产生器类,如要扩展其他站点,就在这里配置
    14 GENERATOR_MAP = {
    15     'weibo': 'WeiboCookiesGenerator',
    16 }
    17 
    18 # 测试类,如要扩展其他站点,就在这里配置
    19 TESTER_MAP = {
    20     'weibo': 'WeiboValidTester',
    21 }
    22 
    23 TEST_URL_MAP = {
    24     'weibo': 'https://m.weibo.cn/api/container/getIndex?uid=1804544030&type=uid&page=1&containerid=1076031804544030',
    25 }
    26 
    27 # 产生器和验证器循环周期
    28 CYCLE = 120
    29 
    30 # API地址和端口
    31 API_HOST = '0.0.0.0'
    32 API_PORT = 5000
    33 
    34 # 产生器开关,模拟登录添加Cookies
    35 GENERATOR_PROCESS = False
    36 # 验证器开关,循环检测数据库中Cookies是否可用,不可用删除
    37 VALID_PROCESS = False
    38 # API接口服务
    39 API_PROCESS = True

    下面是存储模块的代码,代码如下所示:

     1 import random
     2 import redis
     3 from cookiespool.config import *
     4 
     5 class RedisClient():
     6     def __init__(self, type, website, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
     7         """
     8         初始化Redis连接
     9         :param type:
    10         :param website:
    11         :param host: 地址
    12         :param port: 端口
    13         :param password: 密码
    14         """
    15         self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
    16         self.type = type
    17         self.website = website
    18 
    19     def name(self):
    20         """
    21         获取Hash的名称
    22         :return: Hash名称
    23         """
    24         return "{type}:{website}".format(type=self.type, website=self.website)
    25 
    26     def set(self, username, value):
    27         """
    28         设置键值对
    29         :param username: 用户名
    30         :param value: 密码或Cookies
    31         :return:
    32         """
    33         return self.db.hset(self.name(), username, value)
    34 
    35     def get(self, username):
    36         """
    37         根据键名获取键值
    38         :param username: 用户名
    39         :return:
    40         """
    41         return self.db.hget(self.name(), username)
    42 
    43     def delete(self, username):
    44         """
    45         根据键名删除键值对
    46         :param username: 用户名
    47         :return: 删除结果
    48         """
    49         return self.db.hdel(self.name(), username)
    50 
    51     def count(self):
    52         """
    53         获取数目
    54         :return: 数目
    55         """
    56         return self.db.hlen(self.name())
    57 
    58     def random(self):
    59         """
    60         随机得到键值,用于随机Cookies获取
    61         :return: 随机Cookies
    62         """
    63         return random.choice(self.db.hvals(self.name()))
    64 
    65     def username(self):
    66         """
    67         获取所有账户信息
    68         :return: 所有用户名
    69         """
    70         return self.db.hkeys(self.name())
    71 
    72     def all(self):
    73         """
    74         获取所有键值对
    75         :return: 用户名和密码或Cookies的映射表
    76         """
    77         return self.db.hgetall(self.name())
    78 
    79 
    80 if __name__ == '__main__':
    81     conn = RedisClient('accounts', 'weibo')
    82     result = conn.set('michael', 'python')
    83     print(result)

    首先创建RedisClient类,初始化__init__()方法的两个关键参数type和website,分别代表类型和站点名称,这是用来拼接Hash名称的两个字段。例如存储账户的Hash,type是accounts、website是webo,如果是存储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。

    (2)、生成模块
    生成模块负责获取各个账号信息并模拟登录,随后生成Cookies并保存。首先获取两个Hash的信息,对比账户的Hash与Cookies的Hash,看看哪些还没有生成Cookies的账号,然后将剩余账号遍历,再去生成Cookies即可。详细代码如下:

      1 import time
      2 from io import BytesIO
      3 from PIL import Image
      4 #from selenium import webdriver
      5 from selenium.common.exceptions import TimeoutException
      6 from selenium.webdriver import ActionChains
      7 from selenium.webdriver.common.by import By
      8 from selenium.webdriver.support.ui import WebDriverWait
      9 from selenium.webdriver.support import expected_conditions as EC
     10 from os import listdir
     11 from os.path import abspath, dirname
     12 
     13 TEMPLATER_FOLDER = dirname(abspath(__file__)) + '/templates/'
     14 
     15 class WeiboCookies():
     16     def __init__(self, username, password, browser):
     17         self.url = 'https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/'
     18         self.browser = browser
     19         self.wait = WebDriverWait(self.browser, 20)
     20         self.username = username
     21         self.password = password
     22 
     23     def open(self):
     24         """
     25         打开网页输入用户名密码并点击
     26         :return: None
     27         """
     28         self.browser.delete_all_cookies()       # 首先清除浏览器缓存的Cookies
     29         self.browser.get(self.url)
     30         username = self.wait.until(EC.presence_of_element_located((By.ID, 'loginName')))
     31         password = self.wait.until(EC.presence_of_element_located((By.ID, 'loginPassword')))
     32         submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'loginAction')))
     33         username.send_keys(self.username)
     34         password.send_keys(self.password)
     35         time.sleep(1)
     36         submit.click()
     37 
     38     def password_error(self):
     39         """
     40         判断是否密码错误
     41         :return:
     42         """
     43         try:
     44             return WebDriverWait(self.browser, 5).until(
     45                 EC.text_to_be_present_in_element((By.ID, 'errorMsg'), '用户名或密码错误')
     46             )
     47         except TimeoutException:
     48             return False
     49 
     50     def login_successfully(self):
     51         """
     52         判断是否登录成功
     53         :return:
     54         """
     55         try:
     56             return bool(
     57                 WebDriverWait(self.browser, 5).until(EC.presence_of_element_located((By.CLASS_NAME, 'lite-iconf-profile'))))
     58         except TimeoutException:
     59             return False
     60 
     61     def get_position(self):
     62         """
     63         获取验证码位置
     64         :return: 验证码位置元组
     65         """
     66         try:
     67             img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'patt-shadow')))
     68         except TimeoutException:
     69             print('未出现验证码')
     70             self.open()
     71         time.sleep(2)
     72         location = img.location
     73         size = img.size
     74         top, bottom, left, right =location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
     75         return (top, bottom, left, right)
     76 
     77     def get_screenshot(self):
     78         """
     79         获取网页截图
     80         :return: 截图对象
     81         """
     82         screenshot = self.browser.get_screenshot_as_png()
     83         screenshot = Image.open(BytesIO(screenshot))
     84         return screenshot
     85 
     86     def get_image(self):
     87         """
     88         获取验证码图片
     89         :return: 图片对象
     90         """
     91         top, bottom, left, right = self.get_position()
     92         print('验证码位置', top, bottom, left, right)
     93         screenshot = self.get_screenshot()
     94         captcha = screenshot.crop((left, top, right, bottom))
     95         return captcha
     96 
     97     def is_pixel_equal(self, image1, image2, x, y):
     98         """
     99         判断两个像素是否相同
    100         :param image1: 图片1
    101         :param image2: 图片2
    102         :param x: 位置x
    103         :param y: 位置y
    104         :return: 像素是否相同
    105         """
    106         # 取两个图片的像素点
    107         pixel1 = image1.load()[x, y]
    108         pixel2 = image2.load()[x, y]
    109         threshold = 20
    110         if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
    111             pixel1[2] - pixel2[2]) < threshold:
    112             return True
    113         else:
    114             return False
    115 
    116     def same_image(self, image, template):
    117         """
    118         识别相似验证码
    119         :param image: 待识别的验证码
    120         :param template: 模板
    121         :return:
    122         """
    123         # 相似度阈值
    124         threshold = 0.99
    125         count = 0
    126         for x in range(image.width):
    127             for y in range(image.height):
    128                 # 判断像素是否相同
    129                 if self.is_pixel_equal(image, template, x, y):
    130                     count += 1
    131         result = float(count) / (image.width * image.height)
    132         if result > threshold:
    133             print('成功匹配')
    134             return True
    135         return False
    136 
    137     def detect_image(self, image):
    138         """
    139         匹配图片
    140         :param image: 图片
    141         :return: 手动顺序
    142         """
    143         for template_name in listdir(TEMPLATER_FOLDER):
    144             print('正在匹配', template_name)
    145             template = Image.open(TEMPLATER_FOLDER + template_name)
    146             if self.same_image(image, template):
    147                 # 返回顺序
    148                 numbers = [int(number) for number in list(template_name.split('.')[0])]
    149                 print('拖动顺序', numbers)
    150                 return numbers
    151 
    152     def move(self, numbers):
    153         """
    154         根据顺序拖动
    155         :param numbers:
    156         :return:
    157         """
    158         # 获得四个按点
    159         try:
    160             circles = self.browser.find_elements_by_css_selector('.patt-wrap .patt-circ')
    161             dx = dy = 0
    162             for index in range(4):
    163                 circle = circles[numbers[index] - 1]
    164                 # 如果是第一次循环
    165                 if index == 0:
    166                     # 点击第一个按点
    167                     ActionChains(self.browser) 
    168                         .move_to_element_with_offset(circle, circle.size['width'] / 2, circle.size['height'] / 2) 
    169                         .click_and_hold().perform()
    170                 else:
    171                     # 小幅移动次数
    172                     times = 30
    173                     # 拖动
    174                     for i in range(times):
    175                         ActionChains(self.browser).move_by_offset(dx / times, dy / times).perform()
    176                         time.sleep(1 / times)
    177                 # 如果是最后一次循环
    178                 if index == 3:
    179                     # 松开鼠标
    180                     ActionChains(self.browser).release().perform()
    181                 else:
    182                     # 计算下一次偏移
    183                     dx = circle[numbers[index + 1] - 1].location['x'] - circle.location['x']
    184                     dy = circle[numbers[index + 1] - 1].location['y'] - circle.location['y']
    185         except:
    186             return False
    187 
    188     def get_cookies(self):
    189         """
    190         获取Cookies
    191         :return:
    192         """
    193         return self.browser.get_cookies()
    194 
    195     def main(self):
    196         """
    197         破解入口
    198         :return:
    199         """
    200         self.open()
    201         if self.password_error():
    202             return {
    203                 'status': 2,
    204                 'content': '用户名或密码错误'
    205             }
    206         # 如果不需验证码直接登录成功
    207         if self.login_successfully():
    208             cookies = self.get_cookies()
    209             return {
    210                 'status': 1,
    211                 'content': cookies
    212             }
    213         # 获取验证码图片
    214         image = self.get_image()
    215         numbers = self.detect_image(image)
    216         self.move(numbers)
    217         if self.login_successfully():
    218             cookies = self.get_cookies()    # content键对应的值是列表,列表内是字典
    219             return {
    220                 'status': 1,
    221                 'content': cookies
    222             }
    223         else:
    224             return {
    225                 'status': 3,
    226                 'content': '登录失败'
    227             }
    228 
    229 
    230 if __name__ == '__main__':
    231     browser = webdriver.Chrome()
    232     result = WeiboCookies('qq_number@qq.com', 'password', browser).main()
    233     print(result)

    在 WeiboCookies 类中,首先对接了新浪微博的四宫格验证码。在main() 方法中,调用cookies的获取方法,并针对不同的情况返回不同的结果。返回结果类型是字典,并且附有状态码status,在生成模块中可以根据不同的状态码做不同的处理。例如状态码为1时,表示成功获取Cookies,只需将Cookies保存到数据库即可。状态码为2表示用户名和密码错误,这时就应该把当前数据库中存储的账号信息删除。如果状态码为3时,则表示登录失败,此时不能判断是否用户名或密码错误,也不能成功获取Cookies,这时可做一些提示,进行下一个处理即可,完整的实现代码如下所示:

      1 import json
      2 from selenium import webdriver
      3 from selenium.webdriver import DesiredCapabilities
      4 from cookiespool.config import *
      5 from redisdb import RedisClient
      6 from login.weibo.cookies import WeiboCookies
      7 
      8 
      9 class CookiesGenerator():
     10     def __init__(self, website='default'):
     11         """
     12         父类,初始化一些对象
     13         :param website: 名称
     14         """
     15         self.website = website
     16         self.cookies_db = RedisClient('cookies', self.website)      # 创建Redis数据库连接,参数是Redis的Hash键要用到的
     17         self.accounts_db = RedisClient('accounts', self.website)
     18         self.init_browser()
     19 
     20     def __del__(self):
     21         self.close()
     22 
     23     def init_browser(self):
     24         """
     25         通过browser参数初始化全局浏览器供模拟登录使用
     26         :return:
     27         """
     28         if BROWSER_TYPE == 'PhantomJS':
     29             caps = DesiredCapabilities.PHANTOMJS
     30             caps["phantomjs.page.settings.userAgent"] = 
     31                 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
     32             self.browser = webdriver.PhantomJS(desired_capabilities=caps)
     33             self.browser.set_window_size(1300, 500)
     34         elif BROWSER_TYPE == 'Chrome':
     35             self.browser = webdriver.Chrome()
     36 
     37     def new_cookies(self, username, password):
     38         """
     39         新生成Cookies,子类需要重写
     40         :param username: 用户名
     41         :param password: 密码
     42         :return:
     43         """
     44         raise NotImplementedError
     45 
     46     def process_cookies(self, cookies):
     47         """
     48         处理Cookies
     49         :param cookies:
     50         :return:
     51         """
     52         dict = {}
     53         for cookie in cookies:
     54             dict[cookie['name']] = cookie['value']
     55         return dict
     56 
     57     def run(self):
     58         """
     59         运行,得到所有账户名,然后顺序模拟登录
     60         :return:
     61         """
     62         accounts_usernames = self.accounts_db.usernames()
     63         cookies_usernames = self.cookies_db.usernames()
     64 
     65         for username in accounts_usernames:
     66             if not username in cookies_usernames:
     67                 password = self.accounts_db.get(username)
     68                 print('正在生成Cookies', '账号', username, '密码', password)
     69                 result = self.new_cookies(username, password)
     70                 # 获取成功
     71                 if result.get('status') == 1:
     72                     cookies = self.process_cookies(result.get('content'))
     73                     print('成功获取到Cookies', cookies)
     74                     if self.cookies_db.set(username, json.dumps(cookies)):
     75                         print('成功保存Cookies')
     76                 # 密码错误,移除账号
     77                 elif result.get('status') == 2:
     78                     print(result.get('content'))
     79                     if self.accounts_db.delete(username):
     80                         print('成功删除账号')
     81                 else:
     82                     print(result.get('content'))
     83         else:
     84             print('所有账号都已经成功获取Cookies')
     85 
     86     def close(self):
     87         """
     88         关闭
     89         :return:
     90         """
     91         try:
     92             print('Closing Browser')
     93             self.browser.close()
     94             del self.browser
     95         except TypeError:
     96             print('Browser not opened')
     97 
     98 
     99 class WeiboCookiesGenerator(CookiesGenerator):
    100     def __init__(self, website='weibo'):
    101         """
    102         初始化操作
    103         :param website:
    104         """
    105         CookiesGenerator.__init__(self, website)
    106         self.website = website
    107 
    108     def new_cookies(self, username, password):
    109         """
    110         生成Cookies
    111         :param username: 用户名
    112         :param password: 密码
    113         :return: 用户名和Cookies
    114         """
    115         # 调用了 login模块下的cookies.py文件中的 WeiboCookies,self.browser由父类提供
    116         return WeiboCookies(username, password, self.browser).main()
    117 
    118 
    119 if __name__ == '__main__':
    120     generator = WeiboCookiesGenerator(website='https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/')
    121     generator.run()

    要扩展其他站点,只要实现new_cookies() 方法即可,然后按此规则返回对应的模拟登录结果,如1代表获取成功,2代表用户名或密码错误。

    3、 检测模块
    Cookies时间太长导致失效,或者Cookies使用太频繁造成无法正常请求网页。有这样的Cookies需要及时清理或者替换。所以需要一个定时检测模块来遍历Cookies池中的所有Cookies,同时设置好对应的检测链接,用每个Cookies去请求这个链接。请求成功或者状态码合法,则该Cookies有效;请求失败,或者无法获取正常数据,如跳转到登录页面或者验证页面,则此Cookies无效,需要将该Cookies从数据库中移除。

    移除Cookies后,前面的生成模块就会检测到Cookies的Hash和账号的Hash相比少了此账号的Cookies,生成模块就会认为这个账号还没有生成Cookies,就用此账号重新登录,此账号的Cookies又被重新更新。

    检测模块主要作用是检测Cookies失效,将其从数据库中移除。要考虑通用可扩展性,首先定义一个检测器的父类,声明一些通用组件,代码如下所示:

     1 import json
     2 import requests
     3 from requests.exceptions import ConnectionError
     4 from redisdb import *
     5 
     6 class ValidTester():
     7     def __init__(self, website='default'):
     8         self.website = website
     9         self.cookies_db = RedisClient('cookies', self.website)
    10         self.accouts_db = RedisClient('account', self.website)
    11 
    12     def test(self, username, cookies):
    13         """为了便于扩展,该方法由子类来实现"""
    14         raise NotImplementedError
    15 
    16     def run(self):
    17         cookies_groups = self.cookies_db.all()
    18         for username, cookies in cookies_groups.items():
    19             self.test(username, cookies)        # 调用 test 方法测试,子类提供 test 方法
    20 
    21 class WeiboValidTester(ValidTester):
    22     """测试微博,如果要测试其他网站,可创建相应的测试类,并且继承ValidTester类"""
    23     def __init__(self, website='weibo'):
    24         ValidTester.__init__(self, website)
    25 
    26     def test(self, username, cookies):
    27         print('正在测试Cookies', '用户名', username)
    28         try:
    29             cookies = json.loads(cookies)
    30         except TypeError:
    31             print('Cookies不合法', username)
    32             self.cookies_db.delete(username)
    33             print('删除Cookies', username)
    34             return
    35         # 如果上面的try代码块没有引发异常,就执行下面的try代码块
    36         try:
    37             test_url = TEST_URL_MAP[self.website]
    38             response = requests.get(test_url, cookies=cookies, timeout=5, allow_redirects=False)
    39             if response.status_code == 200:
    40                 print('Cookies有效', username)
    41             else:
    42                 print(response.status_code, response.headers)
    43                 print('Cookies失效', username)
    44                 self.cookies_db.delete(username)
    45                 print('删除Cookies', username)
    46         except ConnectionError as e:
    47             print('发生异常', e.args)
    48 
    49 if __name__ == '__main__':
    50     WeiboValidTester().run()

    这段代码中定义了一个父类ValidTester,在其__init__()方法中指定了站点名称website,另外建立两个存储模块连接对象cookies_db 和 accounts_db,分别负责操作Cookies 和账号的hash,run()方法是入口,这里遍历了所有的Cookies,然后调用test()方法进行测试,test()方法由子类来实现,每个子类负责各自不同的网站的检测。如检测微博的可定义为WeiboValidTester,实现其独有的 test() 方法来检测微博的Cookies是否合法,然后做相应的处理。WeiboValidTester类就是继承了ValidTester类的子类。

    子类的test()方法首先将Cookies转化为字典,检测Cookies的格式,如果格式不正确,直接将其删除,如果没有格式问题,就拿此 Cookies请求被检测的URL。test()方法在这里检测的是微博,检测的URL可以是某个Ajax接口,为了实现可配置化,将测试URL也定义成字典,如下所示:
    TEST_URL_MAP = {'weibo': 'https://m.weibo.cn/'}
    要扩展(检测)其他站点,可统一在字典里添加。对微博来说,用Cookies去请求目标站点,同时禁止重定向和设置超时时间,得到响应后检测其返回状态码。返回的是200,则Cookies有效,如果遇到302跳转等情况,一般会跳转到登录页面,则 Cookies已失效,此时将失效的Cookies从Cookies的Hash里移除即可。

    4、接口模块
    生成模块和检测模块定时运行可完成Cookies实时检测和更新。但Cookies最终是给爬虫用的,同时一个Cookies池可供多个爬虫使用,所以需要定义一个Web接口,爬虫访问该接口就可获取随机的Cookies。这个接口用Flask来搭建,代码如下所示:

     1 import json
     2 from flask import Flask, g
     3 from cookiespool.config import *
     4 from redisdb import *
     5 
     6 __all__ = ['app']
     7 
     8 app = Flask(__name__)
     9 
    10 @app.route('/')
    11 def index():
    12     return '<h2>Welcome to Cookie Pool System</h2>'
    13 
    14 
    15 def get_conn():
    16     """
    17     获取
    18     :return:
    19     """
    20     for website in GENERATOR_MAP:
    21         print(website)
    22         if not hasattr(g, website):
    23             setattr(g, website + '_cookies', eval('RedisClient' + '("cookies","' + website + '")'))
    24             setattr(g, website + '_accounts', eval('RedisClient' + '("accounts", "' + website + '")'))
    25     return g
    26 
    27 
    28 @app.route('/<website>/random')
    29 def random(website):
    30     """
    31     获取随机的Cookie,访问地址如 /weibo/random
    32     :param website:
    33     :return: 随机Cookie
    34     """
    35     g = get_conn()
    36     cookies = getattr(g, website + '_cookies').random()
    37     return cookies
    38 
    39 
    40 @app.route('/<website>/add/<username>/<password>')
    41 def add(website, username, password):
    42     """
    43     添加用户,访问地址如 /weibo/add/user/password
    44     :param website: 站点
    45     :param username: 用户名
    46     :param password: 密码
    47     :return:
    48     """
    49     g = get_conn()
    50     print(username, password)
    51     getattr(g, website + '_accounts').set(username, password)
    52     return json.dumps({'status': '1'})
    53 
    54 
    55 @app.route('/<website>/count')
    56 def count(website):
    57     """
    58     获取Cookies总数
    59     """
    60     g = get_conn()
    61     count = getattr(g, website + '_cookies').count()
    62     return json.dumps({'status': '1', 'count': count})
    63 
    64 if __name__ == '__main__':
    65     app.run(host='127.0.0.1')

    这里random方法实现通用的配置来对接不同的站点,所以接口链接的第一个字段定义为站点名称,第二个字段定义为获取方法,例如 /weibo/random是获取微博的随机Cookies,/zhihu/random是获取知乎的随机Cookies。

    5、调度模块
    最后再加一个调度模块,让这几个模块配合起来运行,主要工作就是驱动几个模块定时运行,同时各个模块需要在不同的进程上运行,代码实现如下所示:

     1 import time
     2 from multiprocessing import Process
     3 
     4 from cookiesapi import app
     5 from cookiespool.config import *
     6 from cookiespool.generator import *
     7 from cookiespool.tester import *
     8 
     9 class Scheduler(object):
    10 
    11     @staticmethod
    12     def valid_cookie(cycle=CYCLE):
    13         while True:
    14             print('Cookies 检测进程开始运行')
    15             try:
    16                 for website, cls in TESTER_MAP.items():
    17                     tester = eval(cls + '(website="' + website + '"")')
    18                     tester.run()
    19                     print('Cookies 检测完成')
    20                     del tester
    21                     time.sleep(cycle)
    22             except Exception as e:
    23                 print(e.args)
    24 
    25     @ staticmethod
    26     def generate_cookie(cycle=CYCLE):
    27         while True:
    28             print("Cookies生成进程开始运行")
    29             try:
    30                 for website, cls in GENERATOR_MAP.items():
    31                     generator = eval(cls + '(website="' + website + '")')
    32                     generator.run()
    33                     print('Cookies 生成完成')
    34                     generator.close()
    35                     time.sleep(cycle)
    36             except Exception as e:
    37                 print(e.args)
    38 
    39     @staticmethod
    40     def api():
    41         print('API接口开始运行')
    42         app.run(host=API_HOST, port=API_PORT)
    43 
    44     def run(self):
    45         if API_PROCESS:
    46             api_process = Process(target=Scheduler.api)
    47             api_process.start()
    48 
    49         if GENERATOR_PROCESS:
    50             generate_process = Process(target=Scheduler.generate_cookie)
    51             generate_process.start()
    52 
    53         if VALID_PROCESS:
    54             valid_process = Process(target=Scheduler.valid_cookie)
    55             valid_process.start()

    代码中用到的两个重要配置是,产生模块类和测试模块类的字典配置,该配置信息在 config 模块中,配置信息如下所示:

    1 # 产生器类,如要扩展其他站点,就在这里配置
    2 GENERATOR_MAP = {
    3     'weibo': 'WeiboCookiesGenerator',
    4 }
    5 
    6 # 测试类,如要扩展其他站点,就在这里配置
    7 TESTER_MAP = {
    8     'weibo': 'WeiboValidTester',
    9 }

    这样配置可方便动态扩展使用,键名是站点名称,键值是类名。如有需要配置其它站点,可在字典中添加,例如要扩展知乎站点的产生模块,可以这样配置:

    1 GENERATOR_MAP = {
    2     'weibo': 'WeiboCookiesGenerator',
    3     'zhihu': 'ZhihuCookiesGenerator',
    4 }


    Scheduler类里对字典遍历,并利用 eval() 方法创建各个类的对象,调用其入口 run() 方法运行各个模块。同时,各个模块的多进程使用了 multiprocessing 中的 Process 类,调用其 start()方法即可启动各个进程。

    最后,还需要为各个模块设置一个开关,可以在配置文件中设置开关的开启和关闭状态,如下所示:

    1 # 产生器开关,模拟登录添加Cookies
    2 GENERATOR_PROCESS = False
    3 # 验证器开关,循环检测数据库中Cookies是否可用,不可用删除
    4 VALID_PROCESS = False
    5 # API接口服务
    6 API_PROCESS = True


    这几个开关的值为True则开启,为False则为关闭。要让代码能够成功运行,还需要导入账号和密码,为此再写一个导入账号和密码的模块,这个模块的代码如下所示:

     1 from redisdb import RedisClient
     2 
     3 conn = RedisClient('accounts', 'weibo')
     4 
     5 def set(account, sep='----'):
     6     username, password = account.split(sep)
     7     result = conn.set(username, password)
     8     print('账号', username, '密码', password)
     9     print('录入成功' if result else '录入失败')
    10 
    11 
    12 def scan():
    13     print('请输入账号密码组,输入exit退出读入')
    14     while True:
    15         account = input()
    16         if account == 'exit':
    17             break
    18         set(account)
    19 
    20 
    21 if __name__ == '__main__':
    22     scan()


    运行这个模块,就将录入的账号和密码存储到 Redis 数据库中。最终,还需要写一个总的运行程序入口模块,这个模块很简单,主要是调用调度模块的run()方法运行程序。

    1 from cookiespool.scheduler import Scheduler
    2 
    3 def main():
    4     s = Scheduler()
    5     s.run()
    6 
    7 if __name__ == '__main__':
    8     main()


    经测试,代码运行成功,各个模块都正常启动,测试模块逐个测试Cookies,生成模块获取还未生成Cookies的账号的Ccookies,各个模块并行运行,互不干扰。这里测试了一个账号,控制台的输出信息如下所示:

    Cookies 检测进程开始运行
    API接口开始运行
     * Serving Flask app "cookiesapi" (lazy loading)
     * Environment: production
       WARNING: Do not use the development server in a production environment.
       Use a production WSGI server instead.
     * Debug mode: off
    Cookies 检测完成
    Cookies生成进程开始运行
     * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
    正在生成Cookies 账号 1234567890 密码 abcd1234       (这里的账号和密码不是真实输出的账号和密码)
    成功获取到Cookies {'M_WEIBOCN_PARAMS': 'uicode%3D10000011%26fid%3D102803', 'MLOGIN': '1', ...(后面省略)}
    成功保存Cookies
    所有账号都已经成功获取Cookies
    Cookies 生成完成
    Closing Browser


    此时在浏览器地址栏访问接口 http://127.0.0.1:5000/weibo/random 也能正确看到随机生成的 cookies,如下图1-5所示,爬虫项目只要请求该接口就可实现随机Cookies的获取。
    图1-5  浏览器上随机获取cookies

                                                                                  图1-5     浏览器上随机获取cookies

  • 相关阅读:
    Python入门11 —— 基本数据类型的操作
    Win10安装7 —— 系统的优化
    Win10安装6 —— 系统的激活
    Win10安装5 —— 系统安装步骤
    Win10安装4 —— 通过BIOS进入PE
    Win10安装2 —— 版本的选择与下载
    Win10安装1 —— 引言与目录
    Win10安装3 —— U盘启动工具安装
    虚拟机 —— VMware Workstation15安装教程
    Python入门10 —— for循环
  • 原文地址:https://www.cnblogs.com/Micro0623/p/11112946.html
Copyright © 2020-2023  润新知