甲乙小朋友的房子

甲乙小朋友很笨,但甲乙小朋友不会放弃

0%

python实现某网站自动打卡

最近学校出了个科学上网的小网站,但流量有限,可以通过每天打卡来获取几十兆流量。奈何自己记性很差,总忘记打卡。因此决定写一个自动打卡的小程序。

思路总结

  1. 用抓包工具仔细分析下登陆以及回帖时post了哪些数据,这些数据从何而来(chrome)
  2. python requests库,用requests.Session().post来登陆和回帖,用get来读取页面内容;
  3. 登陆之后,正则找到“签到”或"不能签到",来进行下一步
  4. 若能签到,就发出签到URL的请求(因为链接从原来上来说是一种URL)
  5. 记录每次签到的log
  6. 最后的最后,使用写个.sh脚本,里面运行这个python程序,配置个相应的plist,每天自动执行(MAC OS)

模拟登陆原理

浏览器访问服务器的过程

一次完整的HTTP请求过程从TCP三次握手建立连接成功后开始。

  1. 用户发起请求(点击等)
  2. 浏览器向WEB服务器发出一个请求Http Request(请求)
  3. Web服务器发相应Http Response
  4. 浏览器解析

HTTP消息

更多关于HTTP请求过程,见参考资料一次完整的HTTP请求过程

HTTP 是一种无状态的协议,协议本身不保留之前的一切请求信息和响应信息,也就是说,对于一个刚刚发送了 HTTP 请求的客户端再次发起请求时,服务端并不知道之前访问过。这样设计的理由是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计如此简单。 但是,无状态导致业务处理就变得棘手了,一个简单的例子就是网上购物的时候,当我在一个页面把商品加入购物车的时候,正当我要跳转到支付页面时,如果没有一种机制来保持我的登录状态,那么之前选的商品全部丢失了。 因此,为了在无状态的HTTP协议之上维护会话状态,使服务器可以知道当前是和哪个客户端打交道,于是Cookie技术应运而生,Cookie通俗的理解就是服务端分配给客户端的一个标识。

Cookie原理

Cookie原理其实也非常简单,总共4个步骤维护HTTP会话。 1. 浏览器第一次发起HTTP请求时,没有携带任何Cookie信息,服务器收到请求并返回给浏览器的HTTP响应,同时HTTP响应包括了一个响应头Set-Cookie字段,它的值是要设置的Cookie。 2. 浏览器收到来自服务器的HTTP响应,响应头中发现有Set-Cookie字段,就会将该字段的值保存在内存或者硬盘中。 3. 浏览器下次给该服务器发送HTTP请求时,会将Cookie信息附加在HTTP请求的头字段Cookie中。 4. 服务器收到这个HTTP请求,发现请求头中有Cookie字段,便知道之前就和这个用户打过交道了。

理解了Cookie的基本原理之后,我们就可以尝试用Python来实现模拟登录。

点击登陆后,究竟发生了什么?

为了看看浏览器究竟做了什么,我们首先进入登录界面,随便输入一个密码,点击登录。打开Chrome开发者工具条(F12)。选择Network下的Headers:

从上图我们可以发现以下关键信息: 1. 登陆的URL地址是General下面的Request URL的那个。 2. 登录需要提供的表单(Form Data)有三个,email(用户名),passwd(密码),remember_me(就是那个“记住我”的选项,我们可以从它的html中搜索得出,如下图)。

到这里,基本上摸清了浏览器登录时所需要的数据是如何获取的了,那么现在就可以开始撸代码用Python模拟浏览器来登录。

由于我今天登陆的网站实在是太简易了,也不需要分析header就可以登陆。所以此处省略header的分析。

我打算基于python的requests库来完成这一过程。

python实现模拟登陆

1
2
3
4
5
6
7
8
9
10
11
email='jiayi797@163.com'
password='这里加密了'
loginurl = 'https://ssr.0v0.loan/auth/login'
# 这行代码,是用来维持cookie的,你后续的操作都不用担心cookie,他会自动带上相应的cookie
s = requests.Session()
# 我们需要带表单的参数
loginparams={'email':email,'passwd':password,'remember_me':'ture'}
# post 数据实现登录
r = s.post(loginurl,data=loginparams)
# 验证是否登陆成功,抓取首页看看内容
r = s.get(loginurl)

运行完这些后,我们发现r已经有内容了。接下来我们进行自动签到。

签到

我们查看r._content的内容,是主页的html内容。很长。为了方便分析,我只拿出“签到”标签下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="col-md-6">
<div class="box box-primary">
<div class="box-header">
<i class="fa fa-pencil"></i>
<h3 class="box-title">签到获取流量</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<p> 每24小时可以签到一次。</p>
<p>上次签到时间:<code>2017-04-04 17:11:22</code></p>
<p id="checkin-btn">
<button id="checkin" class="btn btn-success btn-flat">签到</button>
</p>
<p id="checkin-msg"></p>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col (right) -->

需要注意的还有以下这个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
$(document).ready(function () {
$("#checkin").click(function () {
$.ajax({
type: "POST",
url: "/user/checkin",
dataType: "json",
success: function (data) {
$("#checkin-msg").html(data.msg);
$("#checkin-btn").hide();
},
error: function (jqXHR) {
alert("发生错误:" + jqXHR.status);
}
})
})
})
</script>

从脚本中我们读到,签到的类型是post,url是/user/checkin, 因此我们试一下post一个这样的url会有什么后果。

1
2
3
checkinUrl="https://ssr.0v0.loan/user/checkin"
r=s.post(checkinUrl)#执行签到
r = s.get(loginurl)#查看签到结果

我们发现,签到栏的HTML已经变成了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="col-md-6">
<div class="box box-primary">
<div class="box-header">
<i class="fa fa-pencil"></i>
<h3 class="box-title">签到获取流量</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<p> 每24小时可以签到一次。</p>
<p>上次签到时间:<code>2017-04-05 14:08:02</code></p>
<p><a class="btn btn-success btn-flat disabled" href="#">不能签到</a></p>
<p id="checkin-msg"></p>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col (right) -->

即:

<button id="checkin" class="btn btn-success  btn-flat">签到</button>

变成了:

<a class="btn btn-success btn-flat disabled" href="#">不能签到</a>

大题框架搭完了。接下来要做的就是一些细节。

判断是否已签到

接下来要做的就是用正则,找到“签到”或者“不能签到”的对应标签,来获得一个当前状态。

首先我想到的是最简单的方式,用python自带的re.search()

1
2
3
4
5
6
7
8
9
10
11
12
def check(str):
hasCheckIn='<button id="checkin" class="btn btn-success btn-flat">'
noChecked='<a class="btn btn-success btn-flat disabled" href="#">'
yes=re.search(hasCheckIn,str)
if yes==None:
no=re.search(noChecked,str)
if no==None:
return -1 #什么都没找到
else:
return 0#找到了“不能签到”
else:
return 1#找到了“签到”

获取当前流量

先找到流量的对应标签:

1
2
3
4
5
6
7
8
<dl class="dl-horizontal">
<dt>总流量</dt>
<dd>1.53GB</dd>
<dt>已用流量</dt>
<dd>253.36MB</dd>
<dt>剩余流量</dt>
<dd>1.29GB</dd>
</dl>

想要获取标签内的内容(而不是暴力地匹配字符串),我们就需要用到另一种匹配方式——正则表达式

举个例子,我们想要获取之间的内容,那么我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#正则表达式获取<tr></tr>之间内容  
res_tr = r'<tr>(.*?)</tr>'
m_tr = re.findall(res_tr,language,re.S|re.M)
for line in m_tr:
print line
#获取表格第一列th 属性
res_th = r'<th>(.*?)</th>'
m_th = re.findall(res_th,line,re.S|re.M)
for mm in m_th:
print unicode(mm,'utf-8'), #unicode防止乱
#获取表格第二列td 属性值
res_td = r'<td>(.*?)</td>'
m_td = re.findall(res_td,line,re.S|re.M)
for nn in m_td:
print unicode(nn,'utf-8')

其中,res是匹配模式。 r’是匹配模式的开头, <tr是先匹配字符串<tr .是匹配任意字符,除了换行符 *是匹配0个或多个的表达式 ?是匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式 依次类推

re.S使.匹配包括换行在内的所有字符 re.M:多行匹配,影响 ^ 和 $

因此,我们的思路是,匹配<dt>总流量</dt></dd></dl>之间的内容。

改写以上匹配式子为:

1
2
3
4
5
6
7
8
def match_flows(str):
res = r'<dl class="dl-horizontal">(.*?)</dl>'
mm = re.findall(
res, str, re.S | re.M)
res=r'<dd>(.*?)</dd>'
mm= re.findall(
res, mm[0], re.S | re.M)
return mm

记录当前操作

首先

import logging

然后:

# 配置日志文件和日志级别
logging.basicConfig(filename='logger.log', level=logging.INFO)

nowtime=time.strftime('%Y-%m-%d',time.localtime(time.time()))#获取当前时间
str= nowtime+',\t总流量:'+lastFlows[0]+',\t已用流量:'+lastFlows[1]+',\t剩余流量:'+lastFlows[2]
logging.info(str)#写入日志

操作系统定期执行Python脚本

见参考文献4.

全部代码

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#! /usr/bin/env python
# coding:utf-8
import sys
import re
import requests
import logging
import time
# 配置日志文件和日志级别
logging.basicConfig(filename='logger.log', level=logging.INFO)
def check(str):
hasCheckIn='<button id="checkin" class="btn btn-success btn-flat">'
noChecked='<a class="btn btn-success btn-flat disabled" href="#">'
yes=re.search(hasCheckIn,str)
if yes==None:
no=re.search(noChecked,str)
if no==None:
return -1 #什么都没找到
else:
return 0#找到了“不能签到”
else:
return 1#找到了“签到”
def match_flows(str):
res = r'<dl class="dl-horizontal">(.*?)</dl>'
mm = re.findall(
res, str, re.S | re.M)
res=r'<dd>(.*?)</dd>'
mm= re.findall(
res, mm[0], re.S | re.M)
return mm
## 这段代码是用于解决中文报错的问题
reload(sys)
sys.setdefaultencoding("utf8")
email='你的账号'
password='你的密码'
loginurl = 'https://ssr.0v0.loan/auth/login'
# 这行代码,是用来维持cookie的,你后续的操作都不用担心cookie,他会自动带上相应的cookie
s = requests.Session()
# 我们需要带表单的参数
loginparams={'email':email,'passwd':password,'remember_me':'ture'}
# post 数据实现登录
r = s.post(loginurl,data=loginparams)
# 验证是否登陆成功,抓取首页看看内容
r = s.get(loginurl)
res=check(r.content)#0=不能签到;1=可以签到;-1=什么都没找到;
if(res==1):#可以签到
checkinUrl="https://ssr.0v0.loan/user/checkin"
r=s.post(checkinUrl)
r = s.get(loginurl)
lastFlows=match_flows(r.content)
nowtime=time.strftime('%Y-%m-%d',time.localtime(time.time()))#获取当前时间
str= nowtime+',\t总流量:'+lastFlows[0]+',\t已用流量:'+lastFlows[1]+',\t剩余流量:'+lastFlows[2]
print str
logging.info(str)

参考文献

  1. 微信群分享:用Python模拟知乎自动登录
  2. Python正则表达式
  3. 常用正则表达式爬取网页信息及分析HTML标签总结
  4. 操作系统定期定时执行PYTHON脚本

签到成功后: