【自制】爬取最新的开学日期并发送短信提醒的小爬虫

疫情期间学校一直没有公布开学时间,热爱学习的我一直想要尽早返校,如果学校没有公布具体的开学日期,我就一直没法参考学校的开学时间来购买火车票,又担心学校公布开学日期后会很难买到火车票,因此设计了这样一款程序通过发送短信的方式来进行通知。

1.1 设计思路

首先程序设计分为两块,分别是requests库的爬虫模块和twilio的短信发送模块。
一共需要使用到PyCharm、PyQT、Twilio API、Threading以及爬虫三件套(requests,美味汤bs,re)。

设计思路图

如上图所示,界面程序类作为人机交互的入口,可以控制爬虫模块与用于短信发送的Twilio模块。其中requests爬虫程序从学校的官方通知页面爬取消息,随后通过对网站html代码的分析(利用bs4+re的方法)将数据进行整理,并与之前获取的官网数据表进行处理,对比;如果获取的最新数据与原数据不一致,则将新数据保存到List表中;与此同时,程序在最新信息中通过正则表达式re进行信息提取,如果有信息包含关键词Keyword数据,则打开Twilio模块进行最新信息的推送。

2.1 爬虫程序的设计

先进行学校官网页面的爬取,在此以我母校南京工程学院的官网消息推送页面作为例子,我们获得学校官网的html,利用requests的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url = 'http://www.njit.edu.cn/index/tzgg.htm'
def getHTMLText(url): #获取网页代码与信息
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64)' #反扒标识
'AppleWebKit/537.36 (KHTML, like Gecko)'
'Chrome/61.0.3163.79 Safari/537.36'}
try:
r = requests.get(url,headers=header,timeout=30)
r.raise_for_status()
r.encoding = r.apparent_encoding
print("Html is Ok")
return r.text
except:
return ""

得到通知页面的html代码后,进行网页代码的分析,首先我们需要明确我们提取的信息是以下网页代码的字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>...</head>
<body>
<div class="pagetop">...</div>
<div class="logo">...</div>
<div class="menu">...</div>
<div class="conter">
<div class=" header">...</div>
<script language="javascript" src="/system/resourse/js/news/statpagedown.js"></script>
<ul>
       <li id="line_uX_X"> $ X=(0,1,2,...,n),会有很多个这种通知条目
         <span class="text" style="float:left;">
        <a target="_blank" title="需要
         提取的表头" href="需要提取的通知地址">需要提取的字段</a>
</span>
</li>
</ul>
</div>
</body>
</html>

将以上html中存储的网页字段信息进行提取,先利用BeautifulSoup美味汤进行整段网页字段的初步定位,随后再利用re正则表达式来进行字段信息的提取,将这些字段信息整理为一个List表格,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import re
from bs4 import BeautifulSoup as bs
def beLists(html): #将获取到的页面信息转换成三维的数据列表
try:
Lists = []
soup = bs(html,"html.parser")
temp = soup.body.find_all('div',attrs={"class":"conter"})
#print(type(temp))
#print(str(temp))
temp = re.findall(r'<li id="line\S*">([\s\S]*?)</li>',str(temp))
#print(temp)
for i in range(len(temp)):
mess = re.findall(r'title="(\S*?)"',temp[i])[0]
date = re.findall(r'class="date">\[(\S*?)\]</span>',temp[i])[0]
path = re.findall(r'href="\.\.(\S*?)"',temp[i])[0]
Lists.append([mess,date,"http://www.njit.edu.cn"+path])
return Lists
except:
print("解析数据出现问题")
return ""

得到List表后,再与原来获得的数据进行对比,程序设计如下(包含了import文件):
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
storageList = []        #用于储存旧的列表
def updateMessage(inLists): #检查信息是否出现变化,如果变化则返回最新信息
global storageList
#print(inLists)
#print((storageList))
if inLists == storageList:
return ""
else:
newList = []
conf = False
for i in range(len(inLists)):
for j in range(len(storageList)):
if inLists[i] == storageList[j]:
conf = True
else:
continue
if conf == False:
newList.append(inLists[i])
else:
conf = False
storageList = inLists
if len(newList) >= 0:
return newList
else:
return ""

def robotMain(url): #程序主界面
html = getHTMLText(url)
if html == "":
print("未爬取到任何信息")
return ""
else:
feedback = updateMessage(beLists(html))
if feedback != "":
print("通知数据已更新。")
return feedback
else:
print("暂无更新。")
return ""

如果有新的信息则进入Twilio模块进行短信的发送。

2.2 Twilio的配置

进入Twilio官网,如果没有Twilio账号,则进行账号的注册。

在twilio的官网注册一个试用账号,过程中需要绑定你的手机,然后获得免费的twilio号码,从你的账户界面(Dsahboard)就可以看到ACCOUNT SID和AUTH TOKEN了,以及给你的Phone Number,如下图所示:

Twilio API信息

接下来安装Twilio API库,使用

1
pip install twilio

安装即可,配置好环境后,进入PyCharm集成开发环境,这里直接上代码,其中的X都可以进行替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
from twilio.rest import Client
import re

def SendMessage(phonenum,messages): # 此为短信发送设置,请登录twilio获取XXXX与0000部分
res = re.match(r'^1[35789]\d{9}$',phonenum)
if res:
account_sid = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 由Twilio的account sid提供
auth_token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 由Twilio的auth token提供
client = Client(account_sid, auth_token)
# from_这里要提供Twilio的TRIAL NUMBER,body则是发送的短信内容,to里是接收手机的号码,需要加国际区号
message = client.messages.create(from_='+00000000',body=messages,to='+86' + phonenum)
else:
pass

随后进行测试,免费版会受到以下的信息,到此Twilio模块的配置完成。

测试短信消息

2.3 利用PyQT开发界面

打开QtDesigner设计界面,本人太懒了直接使用的图形操作界面生成界面的前段代码,获得的界面如下图所示。

UI界面

保存设计好的UI界面后,通过PyUIC将前端代码转换为Python类代码,可得以下的UI界面的Python代码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'sysUI.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(773, 654)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.groupBox_2 = QtWidgets.QGroupBox(self.centralwidget)
self.groupBox_2.setGeometry(QtCore.QRect(20, 430, 741, 221))
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(11)
font.setBold(True)
font.setWeight(75)
self.groupBox_2.setFont(font)
self.groupBox_2.setObjectName("groupBox_2")
self.lineEdit_address = QtWidgets.QLineEdit(self.groupBox_2)
self.lineEdit_address.setGeometry(QtCore.QRect(60, 30, 341, 31))
self.lineEdit_address.setObjectName("lineEdit_address")
self.label_2 = QtWidgets.QLabel(self.groupBox_2)
self.label_2.setGeometry(QtCore.QRect(10, 40, 72, 15))
self.label_2.setObjectName("label_2")
self.gridLayoutWidget = QtWidgets.QWidget(self.groupBox_2)
self.gridLayoutWidget.setGeometry(QtCore.QRect(10, 119, 381, 41))
self.gridLayoutWidget.setObjectName("gridLayoutWidget")
self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.btn_webConfirm = QtWidgets.QPushButton(self.gridLayoutWidget)
self.btn_webConfirm.setObjectName("btn_webConfirm")
self.gridLayout.addWidget(self.btn_webConfirm, 0, 0, 1, 1)
self.label_3 = QtWidgets.QLabel(self.groupBox_2)
self.label_3.setGeometry(QtCore.QRect(10, 80, 91, 16))
self.label_3.setObjectName("label_3")
self.lineEdit_time = QtWidgets.QLineEdit(self.groupBox_2)
self.lineEdit_time.setGeometry(QtCore.QRect(100, 70, 151, 31))
self.lineEdit_time.setLayoutDirection(QtCore.Qt.LeftToRight)
self.lineEdit_time.setAlignment(QtCore.Qt.AlignCenter)
self.lineEdit_time.setObjectName("lineEdit_time")
self.label_4 = QtWidgets.QLabel(self.groupBox_2)
self.label_4.setGeometry(QtCore.QRect(260, 80, 91, 16))
self.label_4.setObjectName("label_4")
self.btn_timeSet = QtWidgets.QPushButton(self.groupBox_2)
self.btn_timeSet.setGeometry(QtCore.QRect(280, 70, 121, 30))
self.btn_timeSet.setObjectName("btn_timeSet")
self.label = QtWidgets.QLabel(self.groupBox_2)
self.label.setGeometry(QtCore.QRect(440, 40, 72, 15))
self.label.setObjectName("label")
self.lineEdit_phoneNum = QtWidgets.QLineEdit(self.groupBox_2)
self.lineEdit_phoneNum.setGeometry(QtCore.QRect(520, 30, 211, 31))
self.lineEdit_phoneNum.setEchoMode(QtWidgets.QLineEdit.Password)
self.lineEdit_phoneNum.setObjectName("lineEdit_phoneNum")
self.horizontalLayoutWidget = QtWidgets.QWidget(self.groupBox_2)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(440, 70, 291, 32))
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.btn_setphoneNum = QtWidgets.QPushButton(self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.btn_setphoneNum.sizePolicy().hasHeightForWidth())
self.btn_setphoneNum.setSizePolicy(sizePolicy)
self.btn_setphoneNum.setObjectName("btn_setphoneNum")
self.horizontalLayout.addWidget(self.btn_setphoneNum)
self.btn_phoneTest = QtWidgets.QPushButton(self.horizontalLayoutWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.btn_phoneTest.sizePolicy().hasHeightForWidth())
self.btn_phoneTest.setSizePolicy(sizePolicy)
self.btn_phoneTest.setObjectName("btn_phoneTest")
self.horizontalLayout.addWidget(self.btn_phoneTest)
self.lineEdit_Keyword = QtWidgets.QLineEdit(self.groupBox_2)
self.lineEdit_Keyword.setGeometry(QtCore.QRect(520, 120, 113, 31))
self.lineEdit_Keyword.setText("")
self.lineEdit_Keyword.setObjectName("lineEdit_Keyword")
self.label_5 = QtWidgets.QLabel(self.groupBox_2)
self.label_5.setGeometry(QtCore.QRect(440, 130, 72, 15))
self.label_5.setObjectName("label_5")
self.btn_kwedit = QtWidgets.QPushButton(self.groupBox_2)
self.btn_kwedit.setGeometry(QtCore.QRect(640, 120, 91, 30))
self.btn_kwedit.setObjectName("btn_kwedit")
self.horizontalLayoutWidget_2 = QtWidgets.QWidget(self.groupBox_2)
self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(10, 170, 721, 41))
self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.btn_start = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
self.btn_start.setObjectName("btn_start")
self.horizontalLayout_2.addWidget(self.btn_start)
self.btn_stop = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
self.btn_stop.setObjectName("btn_stop")
self.horizontalLayout_2.addWidget(self.btn_stop)
self.view_console = QtWidgets.QTextEdit(self.centralwidget)
self.view_console.setEnabled(True)
self.view_console.setGeometry(QtCore.QRect(20, 10, 741, 411))
self.view_console.setInputMethodHints(QtCore.Qt.ImhNone)
self.view_console.setReadOnly(True)
self.view_console.setObjectName("view_console")
MainWindow.setCentralWidget(self.centralwidget)

self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "官网动态通知获取程序,开学日期提醒"))
self.groupBox_2.setTitle(_translate("MainWindow", "操作面板"))
self.label_2.setText(_translate("MainWindow", "网址:"))
self.btn_webConfirm.setText(_translate("MainWindow", "确认网址"))
self.label_3.setText(_translate("MainWindow", "间隔时间:"))
self.lineEdit_time.setText(_translate("MainWindow", "2"))
self.label_4.setText(_translate("MainWindow", "S"))
self.btn_timeSet.setText(_translate("MainWindow", "设置间隔时间"))
self.label.setText(_translate("MainWindow", "手机号:"))
self.btn_setphoneNum.setText(_translate("MainWindow", "确定设置"))
self.btn_phoneTest.setText(_translate("MainWindow", "测试一下"))
self.label_5.setText(_translate("MainWindow", "关键词:"))
self.btn_kwedit.setText(_translate("MainWindow", "修改"))
self.btn_start.setText(_translate("MainWindow", "开始 Start"))
self.btn_stop.setText(_translate("MainWindow", "停止 Stop"))

3.1 程序的调试与使用

将以上三种模块进行组合,将子程序写入PyQtClass类中,可以得到如下的代码,随后使用PyInstaller进行程序的打包,若没有PyInstaller则可在python环境下的shell中输入pip install pyinstaller,生成exe可执行程序,准备程序的图标,使用以下代码进行打包。

1
pyinstaller -F -w -i YourIcon YouSoftware.py

详细的PyInstaller程序打包说明请点击这里

点击运行程序:

调试程序界面

其中:

1. 输入需要爬取信息的页面,也就是学校官网的消息页面,点击确认网址即可进行确定; 
2. 输入的信息爬取间隔时间,以秒为单位,这个操作可以减少爬取页面的服务器的内存资源(为服务器君着想),点击设置间隔时间进行确定;
3. 输入接收手机的号码,因为代码中已经默认加入中国的区号(+86),因此只需要输入中国大陆的手机号码即可,点击确定设置进行确认,也可以点击测试一下进行短信的发送验证;
4. 输入需要检索的关键词信息,这里主要检测获取的最新信息中是否存在包含关键词的内容,如果包含关键词则将这条信息进行发送,若没有包含则不进行短信的发送;
5. 此处空白主要显示操作的信息反馈,不可输入。

完成以上信息的配置之后,点击开始(Start)运行程序,运行程序时可以使用停止(Stop)停止运行。

4.1 心得

本程序可以改成API进行直接调用,算是日常里面的一个沙雕程序,在Python学习上更进一步吧,源程序地址点击这里,欢迎Star或者Fork