zoukankan      html  css  js  c++  java
  • 第十部分 模拟登录(模拟登录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

  • 相关阅读:
    PTA 7-5 有趣的最近公共祖先问题 (30分)
    平衡二叉树的旋转类型及代码实现
    Ubuntu搭建青岛大学开源OJ
    见过猪跑现在开始吃猪肉了
    工作4年的老腊肉的总结
    服务器日志的目录
    Jacoco配置的问题
    一次述职之后的反省
    Python+Webdriver+Phantomjs,设置不同的User-Agent,获得的url不一致
    Eclipse+Pydev 找不到对应的module not in Pythonpath
  • 原文地址:https://www.cnblogs.com/Micro0623/p/11112946.html
Copyright © 2011-2022 走看看