软件介绍:
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方法)就能定义这个线程要干的事情。
关于时间的获取,具体可以看参考文章8,datetime.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。
参考文章:
- Python爬虫(二十一)_Selenium与PhantomJS – 小破孩92 – 博客园 (cnblogs.com)
- Linux环境变量配置全攻略 – 悠悠i – 博客园 (cnblogs.com)
- CGI(通用网关接口)_百度百科 (baidu.com)
- Nginx Location 路径匹配优先级 – 运维学习记录 (qiansw.com)
- nginx FastCGI模块(FastCGI)配置_bytxl的专栏-CSDN博客
- nginx+python+fastcgi环境搭建_fly2010love的专栏-CSDN博客
- web python — WSGI接口POST请求_shanzhizi的专栏-CSDN博客
- python 获取当前时间 分解为年、月、日、小时、分钟_app测试经验累积,社科管理类杂书阅读笔记-CSDN博客_python 获取当前分钟
- Python3 SMTP发送邮件 | 菜鸟教程 (runoob.com)
- demo:自动打卡系统