基于Selenium+PhantomJS的自动打卡系统
基于Selenium+PhantomJS的自动打卡系统

基于Selenium+PhantomJS的自动打卡系统

软件介绍:

Selenium

Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动化操作,不同是Selenium可以直接运行在浏览器上,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器)。

Selenium可以根据我们的指令,让浏览器自动加载页面,获取需要的页面,甚至页面截屏,或者判断网站上某些动作是否发生。

Selenium自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所有我们而已用一个叫PhantomJS的工具代替真实的浏览器。

可以从PyPI网站下载Selenium库http://pypi.python.org/simple/selenium,也可以用第三方管理器pip命令安装:pip install selenium
Selenium官方参考文档:http://selenium-python.readthedocs.io/index.html

PhantomJS

PhantomJS是一个基于Webkit的”无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器更高效。

如果我们把Selenium和PhantomJS结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理JavaScript、Cookie、headers,以及任何我们真实用户需要做的事情。

注意:PhantomJS只能从它的网站(http://phantomjs.org/download.html)下载。因为PhantomJS是一个功能完善(虽然无界面)的浏览器而非一个Python库,所以它不需要像Python的其它库一样安装,但我们可以通过Selenium调用PhantomJS来直接使用
PhantomsJS官方才考文档:http://phantomjs.org/documention

Phantom在服务器(linux)上的安装:将phantom官网上下载的上传到服务器,然后需要配置环境变量,打开/etc/profile文件,在文件末尾加上:export PATH=$PATH:/phantomjs/bin(路径填你存放Phantom的位置),然后再重启终端或者在终端执行source /etc/profile命令。

Selenium与PhantomJS的具体使用方法参见参考文章1

项目思路:

由于教务系统登录及提交表单涉及到很多js代码的运行,所以用一般的post request提交表单难度会非常大,所以用Selenium+PhantomJS模拟真实浏览器运行js来提交表单,难度会大大降低。

自动打卡需要用户提供教务系统账号及密码,这些内容可以存储在数据库中,然后在每天的固定时间从数据库中调出数据进行自动打卡。

这样下来,用户要使用自动打卡就得事先进行登记。利用一个静态网页向后端提交表单数据以登记自动打卡,后端接收到表单后需要做如下几件事:

  • 解析表单数据
  • 验证账号密码
  • 添加账号密码到数据库
  • 发送欢迎邮件

同时后端会运行一个每日自动打卡的线程,这个线程的工作流如下:

  • 每隔一分钟检查
  • 检查当前时间是否在设定时间
  • 如果在则进行打卡
  • 发送打卡成功或失败邮件

项目详解:

前后端接洽

前端静态网页负责提交表单,这个不多赘述。

重点在于后端的处理。我们都知道在静态网页中,表单提交需要一个去处,也就是form标签的action属性(虽然我选用的是ajax提交表单,但要表明的意思大致相同)。表单提交的路径,就是处理表单数据的地方。

那么问题来了,我们后端模拟打卡之类的操作都是通过python编写的程序,而前端静态网页提交表单是通过HTTP协议的post方法,如何才能让前后端互相通信?

这里就需要使用到一个叫做CGI(通用网关接口)的东西,CGI准确来说是一种规范,并不是什么软件。CGI是外部扩展应用程序与 Web 服务器交互的一个标准接口。这里的Web服务器可以理解为就是Nginx或者Apache之类的web服务器,符合CGI的各类程序(如大名鼎鼎的PHP就自带一种fastCGI协议实现,还有使用一些包的python,或者C/C++,都行)可以直接利用规范与web服务器直接进行数据互通,然后再由web服务器转发可以直接与客户端交互。

如此一来,利用CGI规范,我们就可以将前端提交的表单,经由Web服务器(我选用的是Nginx)通过CGI协议与我们写的python后端进行交互了。

那么CGI具体如何使用呢?

在Nginx中,需要配置一定的规则,使相关的请求通过CGI转发到相应程序,我们在Nginx中添加如下规则:

location = /autoReport
{
    fastcgi_pass localhost:9999;
    include fastcgi.conf;
}

解释一下上面的规则,location = /autoReport是精确匹配,当客户端请求的路径为/autoReport时,匹配这条规则。Nginx中支持的是fastcgi,用fastcgi_pass localhost:9999可以将这个请求由fastcgi转发到本机的9999端口上,让我们的python后端运行在9999端口上,就能接受到这项请求。include fastcgi.conf则是引入fastcgi的一些设置。

这样一来,前端只需要将表单提交到 /autoReport 这个位置,python就能收到对应的请求。

前端的请求路径搞定了,python当中又该如何处理这个请求呢?

我们写如下的python文件:

from flup.server.fcgi import WSGIServer

#Http请求处理和回复
def HttpReqHandler(environ, start_response):
    msg = 'Hello!'
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    start_response(status, response_headers)
    return [msg]

if __name__ == '__main__':
    WSGIServer(HttpReqHandler, bindAddress=('127.0.0.1',9999)).run()

这是一个最简单的处理cgi请求的python脚本。我们需要引入一个叫做flup的包,flup是python的fastCGI组件,这个包并非标准库里的包,需要通过命令pip install flup来安装。在本例中,我们只需要用到flup.server.fcgi.WSGIServer这个类来构建WSGI服务器(处理CGI请求的服务器)。

首先,我们要将WSGIServer实例化,其构造函数需要两个参数,第一个是用来处理请求的handler函数,第二个是要绑定到的地址和端口。实例化该类后,调用run()方法可以让其运行并监听请求。

关于handler函数, WSGIServer收到一个请求后,会将这个请求交给handler函数处理, WSGIServer会提供给handler两个参数:environ和start_response,前者包含了关于请求的一些参数,后者则用来构造返回的http消息中的一些头信息。

本例中我们让服务器收到请求后就返回200状态码,response_headers中设置Content-Type为纯文本,然后将”Hello!”作为正文返回。这时候将这个python脚本运行起来,访问/autoReport,会有以下效果:

至此,前端与python后端的通道算是打通了。

解析表单数据

那么python脚本中该如何处理前端post来的表单信息呢?

需要注意的是,当请求方法为POST时,请求字符串会放在HTTP的body(正文)中。可以使用环境变量中的wsgi.input进行读取。同时CONTENT_LENGTH变量记录了内容的长度,可以利用这两个变量将前端发送的表单读取出来。

让我们对上面的python脚本做个改进:

from flup.server.fcgi import WSGIServer
from cgi import parse_qs

#Http请求处理和回复
def HttpReqHandler(environ, start_response):
    try:
        request_body_size = int(environ.get('CONTENT_LENGTH', 0))
    except (ValueError):
        request_body_size = 0
 
    request_body = environ['wsgi.input'].read(request_body_size).decode("utf8")
    d = parse_qs(request_body)
    a = int(d['a'][0])
    b = int(d['b'][0])
    msg = str(a+b)
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    start_response(status, response_headers)
    return [msg]

if __name__ == '__main__':
    WSGIServer(HttpReqHandler, bindAddress=('127.0.0.1',9999)).run()

这段程序可以做到前端表单中提交一个a和b的值,返回a+b的值。

这段程序中有几个需要注意的点,首先就是为了将发来的表单转换成python中的字典对象,需要引入cgi中的parse_qs函数,cgi是python的标准库,可以直接引入。然后就是由于提交来的表单正文是以二进制的形式发送的,需要进行解码,也就是在其后加上decode(“utf8”)。

让我们写个简单的静态网页用于提交表单:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>测试</title>
	</head>
	<body>
		<form action="https://www.zeromes.cn/autoReport" method="post">
			<input type="text" name="a"/>
			<input type="text" name="b"/>
			<input type="submit" value="提交"/>
		</form>
	</body>
</html>

输入1和2:

点击提交后:

可以看到python成功解析了表单并返回了两数之和。

Selenium+PhantomJS模拟访问

参考文章1中有Selenium+PhantomJS的详细用法。这里主要举例如何利用这两个东西去教务系统打卡。

模拟浏览器教务系统打卡的python脚本代码如下:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def healthPunch(un,pd,email):
    try:
        #调用指定的PhantomJS浏览器创建浏览器对象
        driver = webdriver.PhantomJS(executable_path = "/phantomjs/bin/phantomjs")
        driver.set_window_size(1920, 1080)
        driver.delete_all_cookies()
        driver.get("http://yqtb.gzhu.edu.cn/infoplus/form/XNYQSB/start?back=1&x_posted=true")
        driver.find_element_by_id('un').send_keys(un)
        driver.find_element_by_id('pd').send_keys(pd)
        driver.find_element_by_id('index_login_btn').click()
        #页面一直循环,直到id="myDynamicElement"出现
        element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "preview_start_button"))
        )
        time.sleep(1)
        driver.find_element_by_id('preview_start_button').click()
        element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "command_button_content"))
        )
        time.sleep(1)
        driver.find_element_by_id('V1_CTRL46').click()
        driver.find_element_by_id('V1_CTRL262').click()
        driver.find_element_by_id('V1_CTRL37').click()
        driver.find_element_by_id('V1_CTRL82').click()
        driver.find_element_by_link_text(u'提交').click()
        time.sleep(3)
        driver.find_element_by_class_name('form_do_action_error')
    except:
        #打卡出问题
        print("Unexpected error:", sys.exc_info()[0], sys.exc_info()[1])
    else:
        #打卡没出问题
        print("打卡成功")
    finally:
        driver.quit()

首先是引入相关的包。然后用webdriver.PhantomJS()函数来创建PhantomJS的webdriver对象,这里有一个参数是写PhantomJS的安装路径,但如果之前配置了环境变量的话,这里可以不填。

然后用driver.set_window_size(1920, 1080)将PhantomJS浏览器的尺寸设定为1920*1080,目的是为了get到和电脑端一样的网页而非手机端的网页(手机端有一些标签的名字不一样)

driver.delete_all_cookies()函数清除所有的cookies,这一步的目的是为了避开教务系统的验证码和多次错误登录锁账号,因为教务系统执行这两个功能靠的是cookies和sessions的回话计数。

driver.get()函数请求网页,理论上会被重定向到教务系统的登录页面。

driver.find_element_by_id()函数根据标签的id找到对应的标签(这个可以自己在浏览器里按F12查找),然后用send_keys()函数向输入框中填入对应的内容,用click()函数来点击登录按钮。

随后,因为登录后网页有个加载表单的过程,这个时候利用显示等待来等待表单加载完成,即用WebDriverWait()实例化后的until()函数,等待expected_conditions(EC)中提供的定位标签的函数presence_of_element_located()找到对应的标签。 presence_of_element_located() 的参数中,第一个是查找方式(By.ID:根据ID,By.CLASS_NAME:根据类名),第二个是对应的属性值。

然后再模拟填写表单最后提交,并确认是否提交成功。并利用try…except来处理对应的异常。

账号验证

通过上面对Selenium和PhantomJS使用的举例,我们很容易就能想到验证用户提供的账号密码是否正确的方法,即登录后寻找登录失败(或者登录成功)会存在在网页中的标签,若没找到便是登录成功,找到了就是登录失败:

# 尝试登录
driver = webdriver.PhantomJS(executable_path = "/phantomjs/bin/phantomjs")
driver.set_window_size(1920, 1080)
driver.delete_all_cookies()
driver.get("http://yqtb.gzhu.edu.cn/infoplus/form/XNYQSB/start?back=1&x_posted=true")
driver.find_element_by_id('un').send_keys(account)
driver.find_element_by_id('pd').send_keys(password)
driver.find_element_by_id('index_login_btn').click()
time.sleep(1)
try:
    driver.find_element_by_id('index_login_btn')
except selenium.common.exceptions.NoSuchElementException:
    try:
        driver.find_element_by_id('btn_login')
    except selenium.common.exceptions.NoSuchElementException:
        #登录成功
    else:
        #账号被锁
else:
    #密码错误

上面便是验证账号密码的一段代码。当driver.find_element_by_id()未能找到一个元素时,会抛出selenium.common.exceptions.NoSuchElementException异常,利用这一点,我们可以用try…except来对账号密码进行鉴别。

数据库访问

对用户提供的账号密码鉴别确认正确后,便可将他们保存到数据库用于每天的自动打卡了,关于python如何操作mysql数据库,详见菜鸟教程的文章:Python MySQL – mysql-connector 驱动 | 菜鸟教程 (runoob.com)

每日打卡线程

由于检查时间是一个无限循环的步骤,所以推荐使用多线程技术将每日的打卡运行在一个线程上,python创建线程非常简单,详见菜鸟教程的文章:Python3 多线程 | 菜鸟教程 (runoob.com)

每日打卡的类声明如下:

class dailyPunchThread (threading.Thread):
    setTimeHour = 8
    setTimeMinute = 0
    def run(self):
        while True:
            #判断当前时间是否在规定时间
            if(datetime.datetime.now().hour==self.setTimeHour and datetime.datetime.now().minute==self.setTimeMinute):
                #从数据库读取列表
                mydb = mysql.connector.connect(
                  host="localhost",
                  user="xxxx",
                  passwd="xxxxx",
                  database="database"
                )
                mycursor = mydb.cursor()
                mycursor.execute("SELECT * FROM tableName")
                myresult = mycursor.fetchall()     # fetchall() 获取所有记录
                for x in myresult:
                    healthPunch(x[0],x[1],x[2])
            time.sleep(60)

可以看到,dailyPunchThread类是继承threading.Thread的子类,只需要重写run()方法(线程运行的时候就会运行run方法)就能定义这个线程要干的事情。

关于时间的获取,具体可以看参考文章8datetime.datetime.now().hour可以获取到当前的小时datetime.datetime.now().minute可以获取到当前的分钟,判断当前的时间是否在设定的时间,即可达到每日固定时间打卡的目的。

线程类准备好后,只需要将线程类实例化后调用其start方法,即可运行线程:

if __name__ == '__main__':
    #每日打卡线程
    dpt = dailyPunchThread()
    dpt.start()

发送邮件

基本的功能到目前为止都已经实现了,那么可不可以用邮件向用户发送消息呢,诸如打卡成功或者打卡出问题之类的消息。

答案是可以的,python自带的标准库smtplib对smtp协议进行了简单的封装,可以让我们利用smtp协议来简单地发送邮件。另外 python自带的标准库email中,有很多可以帮助我们构建一封邮件的函数。

需要注意的是smtp需要一个邮件服务器,我们可以直接用QQ邮箱的smtp服务器。使用QQ邮箱的smtp服务需要先去设置,具体请参考:QQ邮箱的SMTP设置 – NoWhere (zeromes.cn)

以下是一段发送欢迎邮件的代码:

import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr

my_sender = 'xxxxxxxxx@qq.com'  # 发件人邮箱账号
my_pass = 'xxxxxxxxx'    # 发件人邮箱密码
my_user = email
try:
    Subject="欢迎订阅自动打卡"
    mail_msg = """
    <p>您好!您已经成功订阅自动打卡功能!</p>
    <p>这是一封通知邮件。</p>
    <p>如果有任何问题,可以直接回复该邮件!</p>
    """
    message = MIMEText(mail_msg, 'html', 'utf-8')
    message['From']=formataddr(["AutoPunch",my_sender])  # 括号里的对应发件人邮箱昵称、发件人邮箱账号
    message['To']=formataddr(["user",my_user])              # 括号里的对应收件人邮箱昵称、收件人邮箱账号
    
    message['Subject']=Subject                # 邮件的主题,也可以说是标题

    server=smtplib.SMTP_SSL("smtp.qq.com", 465)  # 发件人邮箱中的SMTP服务器,端口是465
    server.login(my_sender, my_pass)  # 括号中对应的是发件人邮箱账号、邮箱密码
    server.sendmail(my_sender,[my_user,],message.as_string())  # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
    server.quit()  # 关闭连接
    print("订阅邮件发送成功")
except:
    print("订阅邮件发送失败:", sys.exc_info()[0], sys.exc_info()[1])

利用MIMEText()函数可以生成对应格式的邮件对象,这个邮件对象中有一些属性可以设置,比如From和To是发送者和接受者,用formataddr()返回的对象可以对其进行设置,Subject为邮件的主题,可以直接用字符串设置。然后用smtplib.SMTP_SSL()函数获取到SMTP(SSL协议)服务器的实例,再调用其login()方法以登录,sendmail()方法来发送邮件,quit()方法来关闭链接。

关于python发送邮件的更多详情请阅读参考文章9

参考文章:

  1. Python爬虫(二十一)_Selenium与PhantomJS – 小破孩92 – 博客园 (cnblogs.com)
  2. Linux环境变量配置全攻略 – 悠悠i – 博客园 (cnblogs.com)
  3. CGI(通用网关接口)_百度百科 (baidu.com)
  4. Nginx Location 路径匹配优先级 – 运维学习记录 (qiansw.com)
  5. nginx FastCGI模块(FastCGI)配置_bytxl的专栏-CSDN博客
  6. nginx+python+fastcgi环境搭建_fly2010love的专栏-CSDN博客
  7. web python — WSGI接口POST请求_shanzhizi的专栏-CSDN博客
  8. python 获取当前时间 分解为年、月、日、小时、分钟_app测试经验累积,社科管理类杂书阅读笔记-CSDN博客_python 获取当前分钟
  9. Python3 SMTP发送邮件 | 菜鸟教程 (runoob.com)
  10. demo:自动打卡系统
0 0 投票数
打个分吧!
订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x