最近在忙着期末考试,等期末考完之后再对各个模块进行详细补充~
1 项目目标
目前项目目标都已经实现。
1.1 基础目标
(1)实现北邮信息门户校内通知、学术讲座、公示公告、校园新闻的查询
(2)将从北邮信息门户校内通知、学术讲座、公示公告、校园新闻的内容更新到数据库
(3)将微信小程序与数据库进行连接,使得小程序可以实时调取数据库内容
(4)在微信小程序上实现搜索功能
(5)在微信小程序上实现用户登录功能
1.2 进阶目标
(1)用户可以在微信小程序上对重要通知进行收藏和取消收藏
(2)将用户收藏的通知与用户微信号挂钩,使用户可以随时读取自己收藏的内容
2 技术栈及相关环境要求
2.1 技术栈
-
微信小程序
用户“无需下载,用完即走”,增加了用户使用的便捷性
不受手机操作系统的限制,只需要开发一套程序 -
腾讯云
支撑微信小程序云开发,可以将数据操作与用户绑定 -
JAVA 服务器
开发的服务器稳定性和安全性都在长期实践中被得以证明 -
Microsoft SQL Server 数据库
具备完全Web支持,具有易用性和良好的性价比 -
Python
具有海量的第三方库,实现功能迅速、稳定、便捷
2.2 系统运行环境
2.2.1 软件环境
分类 | 名称 | 版本 | 语种 |
---|---|---|---|
PC 操作系统 | Windows 10 | 家庭中文版 | 简体中文 |
移动端操作系统 | iOS/Android | / | 简体中文 |
应用平台 | 微信小程序 | / | 简体中文 |
数据库平台 | Microsoft SQL Server | 2019 | Sql |
服务器端 | IntelliJ IDEA | 2020.1 | Java |
Tomcat | 9.0.44 | ||
数据爬取端 | Spyder | 3.0.0 | Python |
开发环境 | Python | 3.7 及以上 | Python |
Java | 14.0.1 | Java |
2.2.2 硬件平台
设备名称 | 设备要求 |
---|---|
智能手机/平板电脑 | 安装微信(IOS:7.0.20 及以上版本,Andriod: 7.0.22 及以上版本) |
PC | 装有 python 3.7 及以上版本、Java 14.0.1 及 以上版本、 Apache Tomcat 对应版本 、 Microsoft SQL Server 数据库 2019及以上版本 |
2.2.3 开发环境
分类 | 名称 | 版本 | 语种 |
---|---|---|---|
PC 操作系统 | Windows 10 | 家庭中文版 | 简体中文 |
开发平台 | 微信小程序开发者工具 | Stable 1.03.2101150 | JavaScript/WXSS/ WXML |
开发平台 | IntelliJ IDEA | 2020.1 | Java |
开发平台 | Tomcat | 9.0.44 | Java |
开发平台 | Spyder | 3.0.0 | Python |
数据库平台 | Microsoft SQL Server | 2019 | Sql |
3 设计思想
整个系统被划分为四个部分:Python 爬虫程序、Microsoft SQL Server 数据库、Java Web的Tomcat 以及微信小程序。划分基于每一个部分对系统的贡献和使用的语言。
Python 由于其具有海量的第三方库,可以迅速、稳定、便捷地实现爬虫程序和数据库的更新,因此选用Python 作为数据的获取和处理部分的主要语言。
Microsoft SQL Server 的是一个具备完全Web 支持的数据库产品,提供了对可扩展标记语言(XML)的核心支持以及在Internet 上和防火墙外进行查询的能力,具有易用性和良好的性价比,这些都是我们选择其作为数据库的原因。
Java 用于Web 开发具有天然的优势。长期以来Java 开发者为其提供了良好的开发基础,使得其具有成熟的设计模式,而且还有成熟的框架,可以用很多表达式以及标签来展示我们需要的内容;Java 开发的稳定性和安全性都在长期实践中被得以证明,因此我们选用Java 作为连接数据库和微信小程序的程序设计语言。
微信小程序从出生以来一直受到开发者的青睐,其背靠10 亿+微信用户,用户“无需下载,用完即走”,增加了用户使用的便捷性,降低了使用门槛。此外,微信小程序的开发不受手机操作系统的限制,只需要开发一套程序即可在不同操作系统的手机进行展示。基于此,我们使用微信小程序作为我们的前端展示工具。
整体技术路线如下图所示
4 模块设计
4.1 Python爬虫及数据库更新模块
该部分包含一个portalToSQL 类。该类用于将门户中特定的消息更新到Microsoft SQL Server 数据库中,该类的设计如下图所示。
主体代码具体实现如下:
class portalToSQL:
# 基础信息
username = '' # 信息门户登录用户名
password = '' # 信息门户登录密码
server = "127.0.0.1" # 服务器地址,该处为本地地址
sqlUsername = "" # 数据库用户名
sqlPassword = "" # 数据库密码
database = "" # 数据库名
'''
Input: loginUrl: 登录网址
cookkie: 登录cookie
infoUrl: 信息所在的网址
infoHref: 信息所在的Href标识
lableName: 预存入的数据库的表名
Output: None
Function: 构造函数,初始化
'''
def __init__(self, loginUrl, cookie, infoUrl, infoHref, lableName):
self.loginUrl = loginUrl
self.cookie = cookie
self.infoUrl = infoUrl
self.infoHref = infoHref
self.lableName = lableName
'''
Input: HTTP GET请求 loginUrl 所得到的内容的字符串 str
Output: 包含本次登录的网站信息的字典 dic (包括用户名、密码、Lt等参数)
Function: 从 loginUrl 里获取本次登录所特有的参数,如 lt, execution 等
'''
def getLt(self, str):
lt=bs(str,'html.parser')
dic={}
for inp in lt.form.find_all('input'):
if(inp.get('name'))!=None:
dic[inp.get('name')]=inp.get('value')
return dic
'''
Input: 目标地址 objectUrl,实例化的 Session 对象
Output: 含有标题、日期和文章主体的字典 result
Function: 获得所需要爬取的页面的标题、日期和文章主体
'''
def getNewsDetail(self, objectUrl, session):
result = {}
res = session.get(objectUrl, headers=self.cookie)
res.encoding = 'utf-8'
soup = bs(res.text, 'html.parser')
result['Title'] = soup.select('.text-center')[0].text
date=[]
# 学术讲座和其他通知的标签不同,故做两种处理。'singlemeta text-center'为学术讲座的格式。
if soup.find_all(class_ = 'pmeta ptime'):
for j in soup.find_all(class_ = 'pmeta ptime'):
date.append(j.text)
eachDate = ''
for i in date:
eachDate = i
else:
for j in soup.find_all(class_ = 'singlemeta text-center'):
date.append(j.text)
eachDate = ''
for i in date:
eachDate = i
eachDate = (eachDate.split(' ')[-1] + " " + eachDate.split(' ')[-2])
result['date'] = eachDate
article = []
image = []
tmp = ''
allP = soup.select('.singleinfo #vsb_content .v_news_content p')
# 插入图片
allPicture = soup.select('.singleinfo #vsb_content .v_news_content p img')
try:
for pictures in allPicture:
imgsUrl = 'http://my.bupt.edu.cn' + pictures.get('src')
image.append(imgsUrl)
except:
pass # 解决不含img信息时的报错问题
for p in allP:
article.append(' ' + p.text.strip() + '\n')
for i in range(0,len(article)):
tmp += article[i]
result['article'] = tmp
tmp = ''
for unitImg in range(0, len(image)):
if unitImg == len(image) - 1:
tmp += image[unitImg]
else:
tmp += image[unitImg] + ','
result['image'] = tmp
return result
# 将数据写入数据库
# 连接数据库
'''
Input: 需要写入数据库的 DataFrame 型数据 df
Output: 写入数据库的结果
Function: 将 df 写入 database 数据库的 lablename 表中
'''
def toSqlServer(self, df):
connect = pymssql.connect(self.server, self.sqlUsername, self.sqlPassword, self.database)
# 一次插入多条数据
cols = ','.join(df.columns)
val = (tuple(i) for i in df.values) # 这里需要转成tuple类型才能写入到数据库中
sqlstr = "INSERT INTO {}({}) VALUES ({})".format(self.lableName, cols, ','.join(['%s']*len(df.columns)))
try:
with connect.cursor() as cursor:
cursor.executemany(sqlstr, val)
sqlDel = 'Delete T From (Select Row_Number() Over(Partition By title order By date) As RowNumber,* From {})T Where T.RowNumber > 1'.format(self.lableName)
cursor.execute(sqlDel)
sqlDelNull = 'delete from {} where (datalength (article) = 0 or datalength (article) is null) and (datalength (image) = 0 or datalength (image) is null)'.format(self.lableName)
cursor.execute(sqlDelNull)
connect.commit()
print('>>> 插入数据成功,表 {} 共插入 {} 行数据'.format(self.lableName, len(df)))
print('>>> 表 {} 删除重复数据、空白数据成功'.format(self.lableName))
except Exception as e:
print('>>> 插入数据失败', e)
connect.rollback()
finally:
connect.close()
'''
Input: None
Output: 含有需要爬取信息的 Excel 文件并更新到数据库
Function: 将所需要爬取的信息保存到 Excel 文件和 SQL Server 数据库
'''
def getInformation(self):
#模拟一个浏览器头
header={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0'}
#实例化session
session = requests.Session()
session.cookies = cookielib.CookieJar()
response=session.get(self.loginUrl, headers=header)
# 得到含有输入的用户名、密码、Lt的字典格式
dic = self.getLt(response.text)
# 更新post信息
postdata={
'username':self.username,
'password':self.password,
'lt':dic['lt'],
'execution':dic['execution'],
'_eventId':'submit',
'rmShown':'1'
}
#携带登陆数据,以post方式登录,
response = session.post(self.loginUrl, data=postdata, headers=header)
#用 GET 方式访问“校内通知”的页面
res = session.get(self.infoUrl, headers=self.cookie)
#用 beautifulsoup 解析 html
soup = bs(res.text,'html.parser')
# 获取各个通知的详细URL
url = []
urls = soup.find_all(href = re.compile(self.infoHref))
isPrompt = False # 让提示代码只执行一次
for j in urls:
if(('http://my.bupt.edu.cn/' + j.get('href')) not in url):
url.append('http://my.bupt.edu.cn/' + j.get('href'))
if url != [] and isPrompt == False:
print(">>> 网址爬取成功")
isPrompt = True
news_total=[]
for i in range(0, len(url)):
newsary = self.getNewsDetail(url[i], session) # 读取网址中的内容详情
news_total.append(newsary)
df = pd.DataFrame(news_total)
self.toSqlServer(df) # 将数据存入数据库
该类中包含四个功能函数,其中getLt(str) 函数用于从loginUrl 里获取本次登录所特有的参数,如lt, execution 等; getNewsDetaill(objectUrl, session) 函数用于获得所需要爬取的页面的标题、日期和文章主体; toSqlServer(df) 函数用于将df 写入database 数据库的lablename 表中; getInformation() 函数用于将所需要爬取的信息保存到SQL Server 数据库。设计图如下图所示。
该部分的接口描述如下
接口名称 | 输入信息 | 输出信息 | 异常处理 |
---|---|---|---|
toSqlServer | 需要存入数据库的DataFrame 类型数据df | None | Try: 将数据插入对应表中Except: 无法插入,则提示插入数据失败,并显示报错结果 |
getInformaion | 登录页面的URL、登录页面后获得的cookie、需要爬取消息列表对应的URL、需要爬取的消息对应的Href、对应的数据库中的表名 | 获取的URL和是否成功插入数据 | Try: 成功获取信息Except: 获取失败,则提示执行失败的模块,并显示报错结果 |
4.2 JAVA Tomcat服务器模块
该部分包含9个类,设计图如下图所示。
该模块接口描述如下
接口名称 | 输入信息 | 输出信息 | 异常处理 |
---|---|---|---|
informationDatabase | 需要连入的数据库及列表名 | 查询到的数据结果 | Try: 将数据存入数组中 Except: 无法存入,则提示失败,并显示报错结果 |
userDatabase | 需要连入的数据库及列表名 | 查询到的数据结果 | Try: 将数据存入数组中 Except: 无法存入,则提示失败,并显示报错结果 |
http://localhost:8080/userServer/userServlet | 用户名username 密码password | 成功:success 失败:error | Try:在网页上显示数据内容 Except:打印错误报告 |
http://localhost:8080/articleServer/notificationServlet | None | 返回对应JSON数据 | Try:在网页上显示数据内容 Except:打印错误报告 |
http://localhost:8080/articleServer/newsServlet | None | 返回对应JSON数据 | Try:在网页上显示数据内容 Except:打印错误报告 |
http://localhost:8080/articleServer/announcementServlet | None | 返回对应JSON数据 | Try:在网页上显示数据内容 Except:打印错误报告 |
http://localhost:8080/articleServer/lectureServlet | None | 返回对应JSON数据 | Try:在网页上显示数据内容 Except:打印错误报告 |
返回的JSON参数格式如下表所示
参数名 | 参数类型 | 说明 | 注意事项 |
---|---|---|---|
title | String | 信息标题 | None |
date | String | 信息发布日期 | 若为学术讲座,该部分包含主办学院 |
article | String | 信息主体 | None |
image | Array | 图片的 URL 地址 | None |
主体代码实现如下:
package database;
import model.Information;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.*;
public class informationDatabase {
String lableName = "";
Connection ct = null;
PreparedStatement pestmt = null;
String jsonOutput = "";
private List<Information> information = new ArrayList<Information>();
public informationDatabase(String lableName) {
try {
this.lableName = lableName;
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
ct = DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databaseName=schoolNews", "这里输入数据库用户名", "这里输入数据库密码");
if (ct != null) {
System.out.println("数据库连接成功");
} else {
System.out.println("数据库连接失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取通知信息并存储到Notification数组中
public String getInformation() throws SQLException {
try {
System.out.println("正在打包发送 校内通知");
pestmt = ct.prepareStatement("select * from " + this.lableName + " order by date desc");
ResultSet rs = pestmt.executeQuery(); // 将数据库响应的查询结果放在rs中
while(rs.next()) {
//System.out.println(rs.getString(1)+","); //标题
//System.out.println(rs.getString(2)+",");
//System.out.println(rs.getString(3)+",");
String[] imgUrls = rs.getString(4).split(","); // 将image字符串按逗号分割,成为数组
Information information = new Information(rs.getString(1),rs.getString(2),rs.getString(3),imgUrls);
this.information.add(information);
}
jsonOutput = JSON.toJSONString(information);
System.out.println(jsonOutput);
return jsonOutput;
} catch (Exception e) {
e.printStackTrace();
return "转换JSON出错";
} finally {
ct.close();
pestmt.close();
}
}
}
package database;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import model.User;
public class userDatabase {
Connection ct = null;
PreparedStatement pestmt = null;
public userDatabase(){
try{
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
ct=DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databaseName=schoolNews", "数据库用户名", "数据库密码");
if(ct != null){
System.out.println("数据库连接成功");
}
else{
System.out.println("数据库连接失败");
}
}catch(Exception e){
e.printStackTrace();
}
}
// 确认登录信息
public User checkUser(String username,String password) throws SQLException{
try{
System.out.println("正在确认登录信息");
// 该处报错原因:nvarchar和ntext的类型不匹配。解决方案参考:https://blog.csdn.net/weixin_34343000/article/details/92824471
pestmt=ct.prepareStatement("select * from [User] where convert(nvarchar(255),username)=? and convert(nvarchar(255),password)=?");
pestmt.setString(1, username);
pestmt.setString(2, password);
ResultSet rs=pestmt.executeQuery(); // 将数据库响应的查询结果放在rs中
System.out.println("数据库响应结果为:" + rs.toString());
User user = new User();
while(rs.next()){
user.setUsername(rs.getString(1));//第一个属性
user.setPassword(rs.getString(2));//第二个属性
System.out.println("用户信息为:" + user.getUsername() + " " + user.getPassword());
return user; ///查到就返回对象
}
return null;
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
ct.close();
pestmt.close();
}
}
}
4.3 前端登录页面
本模块实现登陆功能。用户输入用户名(学号)与密码,前端进行简单的表单校验后, 通过 login 接口将数据传到数据库进行账号与密码的比对,如果成功的话,将跳转到消息页 面,反之将提醒用户账号与密码错误。
- 输入:用户名(学号)与密码
- 输出:登陆成功与否的信息
- 异常处理:用户名或密码为空时,前端将告知用户名或密码为空,这时数据将不被传到后端
- 安全处理:密码传输过程采用国密SM3加密
该部分主体代码实现如下:
login: function (e) {
var that = this
let password = e.detail.value.password
let username = e.detail.value.username
var unameNULL = (username.length === 0) ? true : false // 判断用户名是否为空
var pwdVaild = (password.length < 6) ? false : true // 判断密码是否大于6位
var unameSpace = username.lastIndexOf(' ') // 判断用户名中有没有空格,值为-1时没有
var pwdSpace = password.lastIndexOf(' ') // 判断密码中有没有空格,值为-1时没有
console.log(pwdVaild,unameNULL)
console.log(sm3('nihao'))
// 用户名不为空以及密码大于6位且其中都没有空格时,才传递登录表单
if(!unameNULL && pwdVaild && unameSpace === -1 && pwdSpace === -1){
password = sm3(password) // 上传前,先基于国密算法sm3将密码加密
console.log(password)
wx.request({
url: 'http://localhost:8080/userServer/userServlet', //后端的url地址
// 传给后端的数据 —— 用户名与加密后的密码
data: {
username: username,
password: password,
},
method: 'GET',
header: {
'content-type': 'application/json' // 默认值
},
// res为从后端获取的数据
success: function (res) {
console.log(res)
console.log(res.statusCode === 200);
// 只要后端返回的状态码以2开头,即请求成功的情况
if (res.statusCode === 200) {
// 登录成功,将跳转至首页
if(res.data === 'success') {
wx.showToast({
title: '登录成功!',
duration: 1000
})
wx.switchTab({
url: '../index/index',
})
}
// 这个是登录失败的处理,界面将显示提示字段,告知用户是哪个部分出错
else that.setData({ flag: true })
}
else {
wx.showToast({
title: '服务器异常',
})
}
},
fail: function (err) {
wx.showToast({
title: '网络异常!',
})
console.log("失败!!!!!!");
}
})
}
// 设置标志位,不管什么时候都进行
this.setData({
unameNULL: unameNULL,
pwdVaild: pwdVaild,
unameSpace: unameSpace,
pwdSpace: pwdSpace
})
},
4.4 前端信息展示模块
渲染效果上,利用组件 mp-cells & mp-cell 组件为用户带来一致的视觉体验。将事先设计一个信息展示的模板(信息详情页),以确保所有的信息都可以规范地展示出来,让用户的视觉体验更加统一。
逻辑实现上,四种类型的消息每一个都对应着一个列表。运用条件渲染语句 wx:for(类 似于 C/C++中的 for 语句)来对列表数据进行循环渲染,以实现消息的展示效果。使用搜索 时,利用条件渲染语句 wx:if 将主页面隐藏,只留下搜索部分。利用 JS 的字符串方法将搜 索内容与全局消息进行匹配,将结果放入列表当中,由条件渲染语句 wx:for 来展示。当用 户点击对应信息后,由小程序自带的函数来获取这条消息对应的列表索引。获取这条消息的 详情信息,随后携带其详情信息跳转到信息详情页。
该部分主体代码实现如下:
<van-tab title="校内通知">
<mp-cells>
<mp-cell wx:for="{{noticeList}}" id="{{index}}" link hover="true" bindtap="getDetail">
<view style="font-size:14px;margin-bottom:8px">
<view style="margin:4px;font-weight:bolder">{{item.title}}</view>
<text style="margin:4px">{{item.date}}</text>
</view>
</mp-cell>
</mp-cells>
</van-tab>
<van-tab title="校内新闻">
<mp-cells>
<mp-cell wx:for="{{newList}}" id="{{index}}" link hover="true" bindtap="getDetail">
<view style="font-size:14px;margin-bottom:8px">
<view style="margin:4px;font-weight:bolder">{{item.title}}</view>
<text style="margin:4px">{{item.date}}</text>
</view>
</mp-cell>
</mp-cells>
</van-tab>
<van-tab title="公示公告">
<mp-cells>
<mp-cell wx:for="{{signiList}}" id="{{index}}" link hover="true" bindtap="getDetail">
<view style="font-size:14px;margin-bottom:8px">
<view style="margin:4px;font-weight:bolder">{{item.title}}</view>
<text style="margin:4px">{{item.date}}</text>
</view>
</mp-cell>
</mp-cells>
</van-tab>
<van-tab title="学术讲座">
<mp-cells>
<mp-cell wx:for="{{academicList}}" id="{{index}}" link hover="true" bindtap="getDetail">
<view style="font-size:14px;margin-bottom:8px">
<view style="margin:4px;font-weight:bolder">{{item.title}}</view>
<view style="margin:4px">{{item.tutor}}</view>
<text style="margin:4px">{{item.date}}</text>
</view>
</mp-cell>
</mp-cells>
</van-tab>
</van-tabs>
4.5 前端个人信息展示模块
本模块展示用户的个人信息。同时提供用户个人收藏消息的管理
该部分主体实现代码如下
<mp-cells>
<mp-cell id="avatar" title="头像:">
<image class="userinfo-avatar" src="{{avatar}}" mode="cover"></image>
</mp-cell>
<mp-cell id="nickname" title="昵称: ">
<text style="margin-left:55vw">{{nickname}}</text>
</mp-cell>
<mp-cell id="sex" title="性别:">
<text style="margin-left:65vw">{{sex}}</text>
</mp-cell>
<mp-cell title="收藏管理" hover="true" link url="../collectInfo/collectInfo"></mp-cell>
</mp-cells>
4.6 前端收藏管理模块
本模块提供收藏模块的管理功能(查看收藏的消息,或者对消息取消收藏),分为信息的展示页以及信息详情页两部分。
该部分主体实现代码如下
getDetail: function(e){
console.log(e)
var index = parseInt(e.currentTarget.id)
var tab = this.data.nowTab
var info = this.data.collectionList[index]
var title = info.title
var date = info.date
var article = encodeURIComponent(info.article)
var image = info.image
var like = true // 本部分的like肯定是true
var id = ''
var collectList = this.data.collectionList
for(var i = 0;i < collectList.length; i++){
if(title === collectList[i].title){
id = collectList[i]._id
}
}
if(tab === '学术讲座')
tutor = info.tutor
wx.navigateTo({
url: '../noticeDetail/noticeDetail?title=' + title + '&date=' + date + '&id=' + id +
'&article=' + article + '&tab=' + tab + '&image=' + image + '&like=' + like,
success(res){
console.log(res)
},
fail(err){
console.log(err)
}
})
},
request: function(){
var that = this
DB.get({
_openid: app.globalData.openid,
success(res){
that.setData({
collectionList: res.data
})
console.log(that.data.collectionList)
},
fail(err){
console.log(err)
}
})
},
5 数据库与数据结构设计
本系统内使用的数据库系统为 Microsoft SQL Server 2019 数据库,数据库中包含表 User, schoolAnnouncements, schoolArticles, schoolLectures 和 schoolNews 五张表,分别用于 用户登录鉴权、公告公示、校内通知、学术讲座和校内新闻的存储。
五张表中,User 表的数据结构设计如下表所示
列名 | 数据类型 | 允许 Null 值 |
---|---|---|
username | nchar(10) | √ |
password | ntext | √ |
schoolAnnouncements, schoolArticles, schoolLectures 和 schoolNews 表的数据结构设计如下表所示
列名 | 数据类型 | 允许 Null 值 |
---|---|---|
title | nvarchar(50) | √ |
date | nvarchar(50) | √ |
article | ntext | √ |
image | ntext | √ |
数据存储设计:
- 访问方法:程序登录数据库后,通过 SQL 语句直接取用;
- 每次取用整组数据,对整组数据进行搜索,最终将得到的数据在程序中处理;
- 所有数据存储在电脑硬盘中,由程序按期删除;
- 数据库内容的保密通过连接数据库时所使用的用户名、密码所对应的权限来进行区分。