好久没写博客了,近期刚刚升级完自己写的自动打卡程序,来分享一下自己解决问题的思路。
起因
自2020年寒假爆发疫情以来,我们的生活都发生了不小的变化,尤其是学生,在家上了一个学期的网课。后续回复上学和生产以后,疫情依然挥之不去,所以有不少单位要求人员每天打卡登记信息。但是问题是,我们大部分时候信息都是没有变化的,所需要的就是每天点一下而已;除此之外,有很多人可能对此不够重视,导致经常遗忘(没错,正是本人)。所以,出于以上这些考虑,我决定写一个程序进行自动打卡,将该程序加入开机启动项,这样每次开机就可以自动打卡,再也不用担心忘记了。
最初的版本
实现思路
有两种思路实现,一种是直接发数据包,一种是控制浏览器。 我选择了后面一种,一来比较方便,二来比较炫酷。第一种方法也可以实现,我的室友实现了这种方法。
驱动准备
首先,需要选取浏览器驱动,我选择的是chrome浏览器,其实还可以选择一种后台驱动,就可以后台静默打卡,不过为了效果我就选择前者了。去chrome官网下载与你安装的chrome版本匹配的驱动(是驱动,不是浏览器本身,当然浏览器本身也要下载) 这里给出驱动的链接:chrome驱动 下载驱动以后,把它放在方便的目录下,比如D:/driver/,怎么方便怎么来 然后是配置环境变量,在系统变量的Path中添加上面你保存的路径,保存设置即可。
开始code
接下来就轮到我们的主角登场了,就是selenium库,这个库是web自动化工具库,专门用来处理这些问题。细节就不介绍了,直接说我的工作。
头文件
所需的头文件如下:
from selenium import webdriver
import selenium
import time
import sys
import socket
等待连接网络
众所周知,电脑启动以后还需要一定的准备时间才会连接网络完毕,所以我们需要采用轮询的方式反复尝试连接网络,直到可以连接上目标网络 首先是测试网络的函数,我们采用socket建立连接来判断是否联网,如果建立连接发生错误说明没联网,返回False,代码如下:
def is_online():
try:
host=socket.gethostbyname("your website")
s=socket.create_connection((host,80),2)
return True
except Exception as e:
return False
测试就写一个循环测试即可:
while(not is_online()):
i=0
打开网页
打开网页分两步,先创建驱动器,再用驱动器打开网页,代码如下:
driver = webdriver.Chrome()
driver.get("your website")
定位元素与填入密码
这里需要观察网页源代码,用F12查看,查看用户名和密码框的id或者class 然后用find_element_by_id()或者find_element_by_name()找到对应元素,然后用send_keys()即可将文本填入其中:
driver.find_element_by_id("username").send_keys("your username")
driver.find_element_by_id("password").send_keys("your password")
点击按钮登录打卡
和上述方法一样找到登录按钮,然后调用click()即可实现点击:
driver.find_element_by_id("login").click()
这样就基本介绍完打卡的所有操作了,至于具体怎么打卡,就运用上面的知识操作即可,因为大家打卡网页都五花八门,就不细说了。
被验证码制裁——妥协
问题
好景不长(大概有一年),某一天,突然发现,如果不在学校内网打卡,在登录时需要输入验证码,被狠狠地制裁了一波。但是打卡还是得继续,开学前打卡缺少还会被批。必须得想办法解决。
粗糙的解决办法
当时有点懒,于是选择了手动输入验证码的方案。 程序输入用户密码后,用a=input() 等待输入验证码,人工输入验证码后,在控制台随便属于一个字符即可继续。 有的人可能会觉得这样不是更麻烦了?其实单看一个打卡,这个程序可能确实也不方便。但是我在后续又添加了很多额外功能,比如自动检查邮箱是否有未读邮件以及检查图书馆借书是否过期,这样的话,这个程序的价值还是有的。
被验证码制裁——反击
提出解决方案
之前因为懒,被制裁了也就直接妥协,孬好还是有用的。但是最近因为兴致来了,我决定反击,彻底解决这个问题,不再妥协! 首先观察验证码,这个验证码可以说是非常的low,反复刷新得到如下图片: 相信大家都可以看出其中的特性:
- 数字位置非常固定
- 数字几乎一样,没有变化
于是我想,能不能用ai去识别图片,从而实现自己填入验证码。 很nice的想法! 说干就干!
数据集准备
数据源获取
训练模型肯定需要数据集,那么数据从哪儿来呢? 验证码就是不愁缺!直接从登录网站上爬取即可。 这里我们需要另一个库requests用来获取图片:
for i in range(100):
link="link"+str(i)
img=requests.get(link)
time.sleep(1)
file_out=open("./data/img"+str(i)+".png","wb")
file_out.write(img.content)
file_out.close()
这样我们就可以获取大量图片:
数据处理
下载完图片还不够,我们的模型肯定只识别单个数字,如果直接把四个数字的图片放里面,就需要10000个分类,准备的数据也需要更多(不是改个数字的事儿?并不是,因为你需要自己打标签,自己要把所有图片看一遍并把结果写下来)。所以我们需要对图片进行切割。 除了切割,因为这里我们并不需要图片的颜色信息,所以可以转换成灰度图,这样更利于模型训练。所以我们对down下来的图片进行如上处理,其中的位置定位可以自己先用一张图片尝试:
for i in range(20):
img=Image.open("./data/img"+str(i)+".png")
data=np.asarray(img.convert('L'))
num1=data[:,25:47]
num2=data[:,45:67]
num3=data[:,67:89]
num4=data[:,89:111]
pic=Image.fromarray(num1)
pic.save("./data/train/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num2)
pic.save("./data/train/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num3)
pic.save("./data/train/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num4)
pic.save("./data/train/img"+str(count)+".png")
count+=1
count=0
for i in range(20,25):
img=Image.open("./data/img"+str(i)+".png")
data=np.asarray(img.convert('L'))
num1=data[:,25:47]
num2=data[:,45:67]
num3=data[:,67:89]
num4=data[:,89:111]
pic=Image.fromarray(num1)
pic.save("./data/test/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num2)
pic.save("./data/test/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num3)
pic.save("./data/test/img"+str(count)+".png")
count+=1
pic=Image.fromarray(num4)
pic.save("./data/test/img"+str(count)+".png")
count+=1
这里处理了80个训练集,20个测试集。 差不多就可以了,这个识别容易,不用这么多,多了自己打标签太麻烦。 处理后的效果如下:
打标签
接下来就是数据准备的最后一步了,就是打标签。 自己把所有图片识别一遍,把答案写下来,如: 至此,我们的数据就准备完毕了。
模型准备
模型的话,采取的时VGGNet,实现的模型如下:
class MyNet(nn.Module):
def __init__(self):
super(MyNet,self).__init__()
self.device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.conv1 = nn.Conv2d(1,64,3,padding=1)
self.conv2 = nn.Conv2d(64,64,3,padding=1)
self.pool1 = nn.MaxPool2d(2, 2)
self.bn1 = nn.BatchNorm2d(64)
self.relu1 = nn.ReLU()
self.conv3 = nn.Conv2d(64,128,3,padding=1)
self.conv4 = nn.Conv2d(128, 128, 3,padding=1)
self.pool2 = nn.MaxPool2d(2, 2, padding=1)
self.bn2 = nn.BatchNorm2d(128)
self.relu2 = nn.ReLU()
self.conv5 = nn.Conv2d(128,128, 3,padding=1)
self.conv6 = nn.Conv2d(128, 128, 3,padding=1)
self.conv7 = nn.Conv2d(128, 128, 1,padding=1)
self.pool3 = nn.MaxPool2d(2, 2, padding=1)
self.bn3 = nn.BatchNorm2d(128)
self.relu3 = nn.ReLU()
self.conv8 = nn.Conv2d(128, 256, 3,padding=1)
self.conv9 = nn.Conv2d(256, 256, 3, padding=1)
self.conv10 = nn.Conv2d(256, 256, 1, padding=1)
self.pool4 = nn.MaxPool2d(2, 2, padding=1)
self.bn4 = nn.BatchNorm2d(256)
self.relu4 = nn.ReLU()
self.conv11 = nn.Conv2d(256, 512, 3, padding=1)
self.conv12 = nn.Conv2d(512, 512, 3, padding=1)
self.conv13 = nn.Conv2d(512, 512, 1, padding=1)
self.pool5 = nn.MaxPool2d(2, 2, padding=1)
self.bn5 = nn.BatchNorm2d(512)
self.relu5 = nn.ReLU()
self.fc14 = nn.Linear(512*4*4,1024)
self.drop1 = nn.Dropout2d()
self.fc15 = nn.Linear(1024,1024)
self.drop2 = nn.Dropout2d()
self.fc16 = nn.Linear(1024,10)
def forward(self,x):
x = x.to(self.device)
x = self.conv1(x)
x = self.conv2(x)
x = self.pool1(x)
x = self.bn1(x)
x = self.relu1(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.pool2(x)
x = self.bn2(x)
x = self.relu2(x)
x = self.conv5(x)
x = self.conv6(x)
x = self.conv7(x)
x = self.pool3(x)
x = self.bn3(x)
x = self.relu3(x)
x = self.conv8(x)
x = self.conv9(x)
x = self.conv10(x)
x = self.pool4(x)
x = self.bn4(x)
x = self.relu4(x)
x = self.conv11(x)
x = self.conv12(x)
x = self.conv13(x)
x = self.pool5(x)
x = self.bn5(x)
x = self.relu5(x)
x = x.view(-1,512*4*4)
x = F.relu(self.fc14(x))
x = self.drop1(x)
x = F.relu(self.fc15(x))
x = self.drop2(x)
x = self.fc16(x)
return x
开始训练
下面就是训练模型了,训练的代码就不细说了,没什么技巧,就是典型的训练流程:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
EPOCH = 30
pre_epoch = 0
BATCH_SIZE = 20
LR = 0.01
mean_v=0.4914
scale_v=0.2023
label_file=open("data/train/label.txt","r")
label_data=label_file.read()
y=label_data.split(',')
yy=[]
for i in range(len(y)//BATCH_SIZE):
temp_y=[]
for j in range(BATCH_SIZE):
temp_y.append(int(y[i*BATCH_SIZE+j]))
yy.append(torch.tensor(temp_y))
x=[]
for i in range(80//BATCH_SIZE):
temp_x=[]
for j in range(BATCH_SIZE):
img=Image.open("./data/train/img"+str(i*BATCH_SIZE+j)+".png")
data=np.asarray(img)
temp_x.append(data.reshape((32,22,1)).tolist())
temp=torch.tensor(temp_x)
temp=temp/255
temp=(temp-mean_v)/scale_v
x.append(temp.permute(0,3,1,2))
train_data=list(map(list,zip(x,yy)))
label_file=open("data/test/label.txt","r")
label_data=label_file.read()
y=label_data.split(',')
for i in range(len(y)):
y[i]=torch.tensor(int(y[i]))
x=[]
for i in range(20):
img=Image.open("./data/test/img"+str(i)+".png")
data=np.asarray(img)
data=torch.from_numpy(data.reshape((1,32,22,1))).permute(0,3,1,2)
data=data/255
data=(data-mean_v)/scale_v
x.append(deepcopy(data))
test_data=list(map(list,zip(x,y)))
net = MyNet().to(device)
if pre_epoch>0:
net.load_state_dict(torch.load('model/net_%03d.pth' % (pre_epoch)))
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9, weight_decay=5e-4)
for epoch in range(EPOCH):
net.train()
sum_loss = 0.0
correct = 0.0
total = 0.0
print(len(train_data))
for x,y in train_data:
x=x.clone()
y=y.clone()
optimizer.zero_grad()
x,y=x.to(device),y.to(device)
outputs=net(x)
loss=criterion(outputs,y)
sum_loss+=loss.item()
loss.backward()
optimizer.step()
_, predicted = torch.max(outputs.data, 1)
total += y.size(0)
correct += predicted.eq(y.data).cpu().sum()
print('[epoch:%d] Loss: %.03f | Acc: %.3f%% '
% (epoch + 1, sum_loss / (i + 1), 100. * correct / total))
print("Waiting Test!")
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_data:
net.eval()
images, labels = images.clone().to(device), labels.clone().to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += 1
correct += (predicted == labels).sum()
print('测试分类准确率为:%.3f%%' % (100 * correct / total))
print('Saving model......')
torch.save(net.state_dict(), 'model/net_%03d.pth' % (epoch + 1))
print("Training Finished, TotalEPOCH=%d" % EPOCH)
训练可以发现,loss在稳步下降,测试集的正确率也上升到了100%。
实践尝试
首先把我们的模型拉出来试试效果如何。 代码如下:
mean_v=0.4914
scale_v=0.2023
link="link"
img=requests.get(link)
time.sleep(1)
file_out=open("./data/img_test.png","wb")
file_out.write(img.content)
file_out.close()
img=Image.open("./data/img_test.png")
img.show()
data=np.asarray(img.convert('L'))
num1=data[:,25:47]
num2=data[:,45:67]
num3=data[:,67:89]
num4=data[:,89:111]
num1=torch.from_numpy(num1.reshape((1,32,22,1))).permute(0,3,1,2)
num1=num1/255
num1=(num1-mean_v)/scale_v
num2=torch.from_numpy(num2.reshape((1,32,22,1))).permute(0,3,1,2)
num2=num2/255
num2=(num2-mean_v)/scale_v
num3=torch.from_numpy(num3.reshape((1,32,22,1))).permute(0,3,1,2)
num3=num3/255
num3=(num3-mean_v)/scale_v
num4=torch.from_numpy(num4.reshape((1,32,22,1))).permute(0,3,1,2)
num4=num4/255
num4=(num4-mean_v)/scale_v
net=MyNet()
net.load_state_dict(torch.load('model/net_%03d.pth' % (30)))
text=""
net.eval()
ans=net(num1)
_, predicted = torch.max(ans.data, 1)
text+=str(predicted.numpy()[0])
ans=net(num2)
_, predicted = torch.max(ans.data, 1)
text+=str(predicted.numpy()[0])
ans=net(num3)
_, predicted = torch.max(ans.data, 1)
text+=str(predicted.numpy()[0])
ans=net(num4)
_, predicted = torch.max(ans.data, 1)
text+=str(predicted.numpy()[0])
print(text)
基本就是随机down一张验证码,显示图片,然后输出识别结果 经过我自己的反复测试,效果还是很好的,都是正确的。 接下俩就是把这个代码嵌入我们的打卡程序。
最终实现
接下来就是把这部分嵌入我们的打卡程序。我们把上面实践尝试的代码打包成一个函数,该函数返回验证码的字符串,然后在打卡程序中填写验证码时调用函数即可。具体代码为:
driver.find_element_by_id("validate").send_keys(get_validatecode(cookies[1]['value']) )
其中get_validatecode()就是获取验证码的函数,函数的内容就是之前的代码,此处不再赘述。但是还涉及一个问题,就是如何down图片,我们是不能单纯的访问链接去获取验证码,因为每次访问链接都会获得不同的图片。 不过,验证码的验证并不是在本地进行的,而是在云端进行的,所以实际上比较的是我们上传的 字符和云端最后一次给的图片是否一致,那么云端需要处理很多链接,怎么区分呢?我们打开burpsuite进行抓包,可以发现,在首次打开登录界面的时候,会收到一个jsessionid的cookie,接下来每次申请验证码时都会附上这个cookie。
依据这个原理,我们只需要获取cookie以后,附上这个cookie来请求验证码,这样就可以让获取到的验证码和云端验证的保持一致,这样就可以实现识别验证码。 加上这最后一步,就完成了整个程序,整个程序就可以实现整个打卡流程。
结语
不知道以后学校还会不会升级系统,有能力就会继续做下去。 这次写出这篇博客,并不是说代码有多复杂,这个网络有多难训练,实际上这些都没什么难度,主要是喜欢这个编程的过程,自己准备数据集,自己处理数据,coding,抓包分析,一个较为综合的解决问题的过程,所以在这里和大家分享这一过程。
|