分类

课内:
不限
类型:
不限 毕业设计 课程设计 小学期 大作业
汇编语言 C语言 C++ JAVA C# JSP PYTHON PHP
数据结构与算法 操作系统 编译原理 数据库 计算机网络 软件工程 VC++程序设计
游戏 PC程序 APP 网站 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019

资源列表

  • 基于Android系统手机通讯录管理软件的设计与开发

    摘要谷歌在安卓领域投入了大量精力来开发,使得安卓技术得以广泛推广,现安卓移动平台设备在市场上已经得到大量推广及普及。在Android移动终端操作系统的快速发展,Android的各种手机软件也大量增长。当然,在手机终端中,手机通讯录是手机终端必不可少的基础功能,其质量直接影响着用户对手机使用的体验与感觉。手机通讯管理软件不仅仅只是能够简单添加联系人以及联系方式的功能,而今已发展成为多种形式,丰富了联系人的信息,存储了更多的内容。此课程设计研究的这个项目,主要实现添加联系人的多种联系方式的功能。
    本软件采用Android Studio+Android SDK集成环境,应用程序编程语言采用Java高级语言开发。通过对通讯录中的联系人的管理,来方便用户通讯更加便捷,联系人的数据保存更加安全。在对Android手机通讯管理软件进行详细的系统设计时,对功能进行详细的划分并对其功能做了详细的介绍,列出了一些主要功能流程图。
    关键词:通讯录 Android 数据库 SQLite
    第一章 绪论1.1 项目研究背景经过多年的发展,移动终端不再仅是通讯网络的终端,还将成为互联网的终端。因此,移动终端的应用软件和需要的服务将会有很大的发展空间。
    Android是一套真正意义上的开放性移动设备综合平台,它包括操作系统、中间件和一些关键的平台应用。Android最大特点在于它是一个开放的体系架构,具有非常好的开发和调试环境,而且还支持各种可扩展的用户体验,Android里面具有非常丰富的图形系统,对多媒体的支持功能和非常强大的浏览器。
    Android平台的开放性等特点既能促进技术的创新,又有助于降低开发成本,还可以使运营商能非常方便地制定特色化的产品。
    1.2 项目研究的目的及意义随着4G网络的使用,移动终端不再仅是通讯网络的终端,还将成为互联网的终端。在Google和Android手机联盟的共同推动下,Android在众多手机操作系统中脱颖而出,受到广大消费者的欢迎。
    手机通讯录作为手机的基本功能之一,每天我们都在频繁地使用着。根据手机功能使用调查显示,有9成以上的消费者使用手机通讯录功能。随着手机通讯录功能的不断加强与完善,手机通讯录对于人们的意义,已不仅仅像记事簿一样显示通讯地址,而是向着个性化、人性化的方向发展。通讯录从无到有,大大丰富了内容,同时结构也发生了革命性变化,而且随着手机的发展,相信更优秀的通讯录会越来越受到社会各层人士的喜爱。
    1.3 系统主要实现内容通过对Android技术的相关研究,了解Android源码实现原理以及过程,从而设计出一款能够使用的手机通讯录。
    这款手机通讯录实现的相关内容如下:

    简洁、实用的操作界面
    联系人的增删改查
    分类的增删改查
    呼叫联系人
    登录、注册、修改密码
    群组的增删改查
    导入导出联系人
    支持模糊查询手机通讯录

    第二章 系统分析2.1 系统可行性分析2.1.1 技术可行性Java 应用编程接口为Java应用提供了一个独立于操作系统的标准接口,可分为基本部分和扩展部分。在硬件或操作系统平台上安装一个Java平台之后,Java 应用程序就可运行。现在Java平台已经嵌入了几乎所有的操作系统。这样Java程序可以只编译一次,就可以在各种系统中运行。
    本软件用的是Java开发语言,在Android Studio集成开发环境下,调试容易。当前的计算机硬件配置或则现有安卓手机的硬件配置也完全能满足开发的需求,因此技术上是绝独可行的。
    2.1.2 经济可行性开发该系统所需的相关资料可以通过已存在的相关系统进行调查采集,所需的软件系统、硬件平台等都易于获得,且不需要Android平台机器,用模拟器即可实现开发研究,开发成本低,容易实现,从经济角度来看,该系统可行。
    2.1.3 操作可行性不管是安卓平台的手机,还是计算机,其成本的下降,导致计算机,安卓手机购买成本的降低.这套系统是利用自己的计算机,且使用安卓模拟器,使开发出来的系统有友好的用户界面、操作简单,因此在操作上是可行的。
    2.2 Android通讯录的使用意义该系统针对的主要用户是Android手机用户。Android手机通信管理系统包括以下主要内容:联系人增删改查、呼叫联系人、分类增删改查、多条件搜索、导入导出联系人、修改密码等功能。要设计一个良好的手机通讯录,就必须首先明确该应用环境对系统的要求。
    第三章 系统概要设计3.1 系统总体设计Android手机通讯管理软件主要功能模块包括:联系人增删改查、呼叫联系人、分类增删改查、多条件搜索、导入导出联系人、修改密码等。

    3.2 处理流程设计3.2.1 业务流程图用户首次进入手机通讯管理软件后,会进入用户注册界面,当用户注册成功之后,输入密码即可看到联系人列表界面。联系人列表界面右下方显示增加联系人按钮。上方可以进行联系人的多条件搜索。同时长按某个联系人可实现编辑删除功能。当然点击联系人也可以看到详细信息。界面中显示我的群组列表,打开之后即可进行群组的增删改查功能。点击菜单键,显示通讯录的导入导出功能以及修改密码功能。
    3.2.2 数据增加流程图添加联系人时,数据由用户输入,点击确定按钮,判断数据是否合法(及用户名是否为空),合法则插入到数据库;不合法,提示错误信息,让用户重新输入。流程如图所示:

    3.2.3 数据修改流程图编辑联系人时,点击编辑联系人菜单,输入修改后的数据,点击确定按钮,判断数据是否合法,合法,则更新数据库;不合法,则返回错误信息。 流程如图所示:

    3.2.4 数据删除流程当用户选定一个联系人时,单击删除联系人菜单,提示用户是否删除,点击确定按钮,则从数据库中删除此条记录。数据删除流程如图所示:

    3.3 数据库设计3.3.1 SQLite数据库简介SQLite,是一款轻型的数据库,是遵守ACID的关联式数据库管理系统,它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低。
    本系统采用的是Android系统自带的SQLite轻型数据库数据库。因此占用资源非常小。
    3.3.3 数据库表结构首先创建数据库,在数据库中创建表用来存储联系人数据,其中包括联系人姓名、手机号、群组ID、地址等联系方式。创建群组表用来保存ID和群组名称等信息。两个表联合配合。表结构如图所示:

    第四章 系统详细设计4.1 联系人浏览模块进入手机通讯管理软件后,看到的第一个界面是联系人列表界面。该列表是由ListView控件生成的,打开数据库(如果数据库不存在则创建数据库,并创建数据表),查找数据库中所有的联系人,并把联系人姓名和移动电话号码以及职位这填充到ListView的adapter中。每一行显示一个联系人的姓名和手机号码,联系人的显示的顺序是根据插入数据库的顺序显示的。点击某个联系人会进入查看联系人界面,可以查看联系人的详细信息,对联系人进行编辑、删除、拨打电话、导入导出通讯录等。

    点击[菜单]按钮时,就会显示主菜单项,其中包括:修改密码、导出数据、导入数据。
    点击添加按钮,会进入添加联系人界面,可以输入联系人相关信息,完成联系人添加功能。点击上方搜索栏目,会进入联系人查找界面,可以进行联系人查找,搜索想要找的联系人。
    点击菜单按钮,打开修改密码、导出通讯录、导出通讯录等功能。

    长按列表的某一行时,会弹出长按菜单,其中包括:拨号、编辑联系人、删除联系人。点击查看联系人菜单会进入查看联系人界面。点击编辑联系人菜单会进入编辑联系人编辑界面。点击删除联系人时,会弹出对话框,询问是否删除联系人,点击确定,则从数据库中删除该联系人。

    4.2 查看联系人模块在联系人浏览界面点击某个联系人,则会跳转到该界面。该界面使用TextView把从数据库中调出的联系人的详细信息显示出来,这里面包括联系人姓名、手机号、地址等详细信息。

    4.3 编辑联系人模块编辑联系人界面使用EditView控件显示并修改联系人的详细信息。联系人的所有信息,处于可编辑状态,手机号和座机号的EditView设定为只能输入数字。修改完信息后点击确定按钮,触发确定按钮点击监听事件,从而对数据库中该联系人的信息进行更新, 然后自动返回联系人浏览界面。点击取消按钮会返回联系人浏览界面。

    4.4 查找联系人模块这里采用的查找方法是SQL模糊查询,可以只输入联系人姓名中的一部分,即可查找到所有包含该部分的联系人,并在ListView中显示出来所有的联系人的姓名和手机号码。可实现查找职位、手机号码、名字等信息。

    4.5 修改密码点击菜单,可以查看该软件的修改密码、导入导出等情况。并可实现全部功能。

    4.6 分类管理点击我的群组界面,可以查看群组并且显示群组。在里面可以对群组进行增删改查操作。
    5 评论 88 下载 2019-01-30 17:26:14 下载需要12点积分
  • 基于C#的超市进存销管理系统

    第一章需求分析1.1 需求分析1.2 用例模型及分析类图的描述1.2.1 用例Use Case:账号密码登录
    参与者:用户
    主事件流:

    用户选择账号登录选项并输入账号密码
    将用户输入的EmpLoginName和EmpLoginPwd与数据库中相应的字段进行匹配
    若匹配成功,则跳转页面,转到主窗口。反之,则提示登陆失败

    1.2.2 用例图用例模型本系统以管理员对数据库的操作为主,实现用例图如下:

    根据对用例的分析,做出用例图如上,管理员主要利用本系统,实现对进货信息、库存信息、销售信息和职工信息、供应商信息的管理。系统采用VS环境开发,实现C/S结构,管理员对各个信息的修改都直接写入数据库。
    1.3 分析类1.3.1 用户登录模块用户登录用例图
    用户登录的用例图,如图1-3-1-1所示:

    用户登录时序图
    如图1-3-1-2所示,表示用户登录的时序图。

    用户登录分析类图(协作图)
    如图1-3-1-3所示,表示用户登录的协作图。

    1.3.2 进货模块进货用例图
    进货的用例图,如图1-3-2-1所示:

    用户登录时序图
    如图1-3-2-2所示,表示用户登录的时序图。

    用户登录分析类图(协作图)
    如图1-3-2-3所示,表示用户登录的协作图。

    1.3.3 销售模块进货用例图
    进货的用例图,如图1-3-3-1所示:

    用户登录时序图
    如图1-3-3-2所示,表示用户登录的时序图

    用户登录分析类图(协作图)
    如图1-3-3-3所示,表示用户登录的协作图

    1.3.4 库存模块进货用例图
    进货的用例图,如图1-3-4-1所示:

    用户登录时序图
    如图1-3-4-2所示,表示用户登录的时序图。

    用户登录分析类图(协作图)
    如图1-3-4-3所示,表示用户登录的协作图。

    1.3.5 职工管理模块进货用例图
    进货的用例图,如图1-3-5-1所示:

    用户登录时序图
    如图1-3-5-2所示,表示用户登录的时序图

    用户登录分析类图(协作图)
    如图1-3-5-3所示,表示用户登录的协作图

    1.3.6 供应商管理模块进货用例图
    进货的用例图,如图1-3-6-1所示:

    用户登录时序图
    如图1-3-6-2所示,表示用户登录的时序图

    用户登录分析类图(协作图)
    如图1-3-6-3所示,表示用户登录的协作图

    第二章 概要分析2.1 系统架构设计
    2.2 数据库设计2.2.1 数据库总体概念设计数据库总体E-R图如图2-2-1所示。

    2.2.2 数据库概念设计通过对于系统的需求分析,整套系统可以设计出六个实体,他们分别是职工实体、供应商实体、进货实体、销售实体、库存实体。
    系统的使用者涉及到多种用户,多以需要一个用户表来保存登陆账号和登陆密码。
    职工表E-R图

    职工数据库表

    供应商表E-R图

    供应商数据库表

    进货表E-R图

    进货数据库表

    销售表E-R图

    销售表

    库存数据库表E-R图

    库存数据库表

    2.3 系统类图设计分析系统,本系统主要包含数据库类和操作类。数据库类包括有进货信息数据库、销售信息数据库、库存信息数据库。操作类主要是对数据库的操作,包括有添加进货、销售、库存、职工、供应商详细信息两个操作。其中添加进货单可以对进货、销售、库存信息数据库执行添加,修改,删除、查找的操作,添加职工、供应商信息可以对物品信息数据库执行添加修改删除的操作。操作类还包含对数据的查询操作,可以根据关键字进行查询;分析以上各个类,作出类图如2-4所示。

    第三章 系统详细设计及实现3.1 系统功能描述本系统主要实现登陆注册模拟超市进货增删改查、销售增删改查、库存增删改查以及对超市内员工的增删和超市供应商的增删。
    3.2 用户登录界面用户登录界面如图3-2所示。

    3.3 职工管理功能职工管理如图3-3所示。

    3.4 供应商管理功能供应商管理如图3-4所示。

    3.5 进货管理功能进货增删改管理如图3-5-1所示。

    进货查询如图3-5-2所示。

    3.6 销售管理功能销售增删改管理如图3-6-1所示。

    销售查询如图3-6-2所示。

    3.7 库存管理功能库存增删改管理如图3-7-1所示。

    库存查询如图3-7-2所示。

    3.8 系统主页面系统主页面如图3-8所示。

    3.9 退出系统退出系统功能如图3-9所示。
    1 评论 9 下载 2019-09-02 12:26:43 下载需要9点积分
  • 基于java和Sql Server数据库的停车场管理系统

    一、实验内容:实现停车场管理系统,应用于车辆的出、入管理。
    二、功能要求:包括车辆进出管理与系统管理等功能模块,可根据车辆停放时间及收费标准自动收费。用户需要事先办理停车卡并充值,停车卡分优惠卡和普通卡两类。

    车场管理:车辆入场、车辆出场
    信息查询:某时间段的出入场信息,当前在场信息,车辆历史停车记录及收费信息
    信息维护:用户及停车卡信息维护、充值等
    系统管理:车位信息,计费标准等

    系统包含两类用户:管理员用户和普通用户。
    管理员可以使用系统所有功能,普通用户只能查询车辆历史记录、用户信息、停车卡充值,查询计费标准。
    三、实验环境:
    Windows XP
    JDK 1.6
    Eclipse
    SQL Server
    备注:

    在XP平台开发DK(JavaDevelopment Kit)是Sun Microsystems针对Java开发员的产品Eclipse进行前台和程序设计,开发图形用户界面和停车收费功能实施
    SQL建立数据库

    四、需求分析与设计:4.1 需求分析:本软件具有如下主要功能:

    本系统包括两类用户:管理员用户和普通用户。管理员可以使用系统所有功能,普通用户只能查询车辆历史记录、用户信息(只限于个人信息)、查询计费标准、查询当前在场信息、查询出入场信息、当前可用车位信息、口令修改。具体模块划分为如下模块:车场管理模块、信息查询模块、信息维护模块、系统管理模块。
    车场管理模块:(应该分为车辆入场和车辆出场两部分)

    车辆入场功能描述:车辆进入停车场时进行登记,记录入场时间并指定车位。只有具有停车卡的车辆才可进场,没有办理停车卡的车辆,应先办理车卡。如果没有相应车位,不能入场;如果卡中余额低于100元,应先充值后再入场。满足条件的车辆,为其指定车位并记录入场时间。车卡分两种类型普通型和优惠型。车辆出场功能描述:车辆开出停车场时进行登记,记录出场的时间并进行自动收费(从卡上扣除)。根据车辆进场时间,出场时间及收费标准自动计算车主应该缴纳的费用。如果停车时间包含不足一小时的时间,超过30分钟按一小时计算,不足三十分钟不计算。如果卡上余额足够则直接扣除;如果卡上余额不足,则应先充值后再扣除相应费用。
    信息查询模块功能描述:在这个模块里用户可以查询出入场信息、当前在场信息、用户个人信息、用户历史记录、收费标准以及当前可用车位信息
    查询出入场信息功能描述: 查询当前在场信息户可以在这里查询到两种车位的总量及当前可有的车位数量。
    查询用户个人信息功能描述:登录的管理员可以根据卡号和名字查询用户信息。登陆的普通用户只可以查到自己的信息。
    查询用户历史记录功能描述:用户可以输入卡号查询相应卡号的历史记录,包括车位号、开始停车时间、结束停车时间、停车总时间、相应收取的费用。
    收费标准功能描述:用户可以在这里查询不同种类的车位和不同卡的计费标准。
    当前在场信息功能描述:用户可以在这里查询到当前在场的车辆信息,包括卡号,车位号,开始停车时间。
    当前可用车位信息功能描述:在这里用户可以查询当前可用的车位的信息,包括车位号、车位类型。
    信息维护模块在这个模块里用户可以实现用户注册、用户修改及用户充值
    用户注册功能描述:在这里管理员可添加新的用户(普通用户)。
    用户修改管理员在这里可以修改用户。这里会以表的形式显示所有的用户信息,包括用户的停车卡信息维护,充值信息等。管理员点击相应的一行用户信息,这行信息会自动填充到表下的面板里,用户可以在面板里修改用户信息,面板下面有两个按钮,修改、删除,点击相应的按钮可以实现相应的功能。
    用户充值功能描述:用户可以再这里查到自己的余额,并且可以在这里完成充值。
    系统管理模块功能描述:在这个模块里可以修改相应的车位信息计费标准、注册管理员、更改用户口令以及查看系统声明信息。
    管理员注册功能描述:管理员可以在这里添加新的管理员。
    更改口令功能描述:用户可以在这里更该自己的密码。注:操作员只可以修改自己的密码。
    计费标准管理功能描述:管理员可以在这里不同车位类型、不同车卡类型的收费标准。
    关于功能描述:用户可以在这里看到系统声明。

    4.2 界面设计登陆界面

    管理员主界面

    普通用户主界面

    车辆入场界面

    车辆出场界面

    计费标准界面

    当场在场信息界面

    用户历史信息界面

    用户个人信息界面

    普通用户个人信息界面

    出入场信息界面

    当前可用车位信息界面

    用户注册界面

    用户修改界面

    用户充值界面

    管理员注册界面

    更改口令界面

    计费标准管理界面

    关于界面

    五、数据库设计5.1 数据库关系图
    5.2 数据表的结构设计


    用户表:users








    字段名称
    数据类型
    可空
    默认值
    说明


    cardid
    int
    不可

    主键,用户的停车卡号


    name
    Nvarchar(20)
    不可

    用户姓名


    password
    Nvarchar(20)


    用户密码


    cardtype
    Nvarchar(20)


    停车卡类型


    userstype
    Nvarchar(20)


    用户类型


    carid
    int


    用户车牌号


    tel
    int


    用户电话号码


    overage
    int


    用户余额






    车位信息表:sit_infor








    字段名称
    数据类型
    可空
    默认值
    说明


    stationid
    int
    不可

    主键,车位号


    stationtype
    Nvarchar(20)
    不可

    车位类型






    停车收费卡收费表:charger








    字段名称
    数据类型
    可空
    默认值
    说明


    cardtype
    Nvarchar(6)


    车卡类型


    stationtype
    Nvarchar(20)


    车位类型(车卡类型与车位类型一起作为主键)


    charge
    int


    价格






    停车表:park








    字段名称
    数据类型
    可空
    默认值
    说明


    cardid
    int


    车卡号(外键)


    stationid
    int


    车位号(外键)


    parkid
    int

    1,每次增加一
    停车号,主键


    startpark
    datetime


    停车开始时间


    endpark
    datetime


    停车结束时间


    fee
    int


    停车的收费


    sumpark
    int


    停车总时间



    六、关键技术介绍6.1 在其他类中得到当前登录用户对象 实现方法:在LoginFrame类中设置两个静态方法,在其他类中只需要引入LoginFrame类,然后调用他的静态方法即可。方法体如下:
    public static users getUser() { return user; } public static void setUser(users user) { LoginFrame.user = user; }
    6.2 实现用户类型不同,主界面不同的功能 可以定义静态方法disMenu().当用户是普通用户时,调用disMenu()方法即可。具体实现如下
    public void disMenu() { mnuPark.setEnabled(false); mnuSever.setEnabled(false); mnuManZhuCe.setEnabled(false); mnuManCharge.setEnabled(false); } if(user.getUserstype().equals("管理员")) { MdiFrame frame1 = new MdiFrame();//创建一个主窗体 frame1.setVisible(true);//设置其可见 LoginFrame.this.setVisible(false);//设置登录窗体为不显示 } else {//判断用户名是否为null MdiFrame frame = new MdiFrame();//创建一个主窗体 frame.disMenu(); frame.setVisible(true);//设置其可见 LoginFrame.this.setVisible(false);//设置登录窗体为不显示 }
    6.3 怎么得到系统时间 SimpleDateFormat myfmt=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); String a4 = myfmt.format(new java.util.Date()).toString();
    6.4 怎么计算时间差值 try { java.util.Date now = myfmt.parse(a3);//a3是系统当前时间(即出场时间) java.util.Date date=myfmt.parse(a7);//a7是入场时间 int l=(int) (now.getTime()-date.getTime());//计算毫秒差值 day=l/(24*60*60*1000);//获取天数 hour=(l/(60*60*1000)-day*24);//获得小时 min=((l/(60*1000))-day*24*60-hour*60);//获得分钟 } catch (Exception e1) { JOptionPane.showMessageDialog(null,"消费计算错误"); } if(min < 30)//如果分钟小于30分钟 a8 = day*24+hour; else //如果分钟大于30分钟 a8 = day*24+hour+1;
    6.5 怎么让布局更优美 使用布局管理器; GridBagLayout,以更改密码界面为例:
    getContentPane().setLayout(new GridBagLayout()); setBounds(234, 129, 285, 223); final JLabel label_5 = new JLabel(); label_5.setText("登 录 名:"); final GridBagConstraints gridBagConstraints_11 = new GridBagConstraints(); gridBagConstraints_11.gridy = 2; gridBagConstraints_11.gridx = 0; getContentPane().add(label_5, gridBagConstraints_11);
    七、系统实现功能结构图
    18 评论 523 下载 2018-11-19 09:31:15 下载需要10点积分
  • Python实现的基于Scrapy爬虫框架和Django框架的新闻采集和订阅系统

    摘要随着互联网的迅速发展,互联网大大提升了信息的产生和传播速度,网络上每天都会产生大量的内容,如何高效地从这些杂乱无章的内容中发现并采集所需的信息显得越来越重要。网络中的新闻内容也一样,新闻分布在不同的网站上,而且存在重复的内容,我们往往只关心其中的一部分新闻,网络中的新闻页面往往还充斥着大量许多与新闻不相关的信息,影响了我们的阅读效率和阅读体验,如何更加方便及时并高效地获取我们所关心的新闻内容,本系统能够帮我们做到这一点。本系统利用网络爬虫我们可以做到对网络上的新闻网站进行定时定向的分析和采集,然后把采集到的数据进行去重,分类等操作后存入数据库,最后提供个性化的新闻订阅服务。考虑了如何应对网站的反爬虫策略,避免被网站封锁爬虫。在具体实现上会使用Python配合scrapy等框架来编写爬虫,采用特定的内容抽取算法来提取目标数据,最后使用Django加上weui来提供新闻订阅后台和新闻内容展示页,使用微信向用户推送信息。用户可以通过本系统订阅指定关键字,当爬虫系统爬取到了含有指定关键字的内容时会把新闻推送给用户。
    [关键词]网络爬虫;新闻;个性化;订阅;Python
    AbstractWith the rapid development of the Internet, the Internet has greatly enhanced the production and dissemination of information, the networkwill produce a lot of content every day, how to find and collectthe information we needed from these disorganized contentefficiently is more and more important. The news content on the network is thesame, the news is distributed on different sites, and there are many duplicate content, we only care about part of thenews usually. The network news pages areoften filled with a lot of news and information is not related that impact ourreading efficiency and readingexperience. How to more convenient and efficient access to the news we are concerned about the content, thissystem can help us to do this. This system uses the web crawler to collect news on the network site. And then toclassify data and other operations like delete the duplication,store data byuse the database, and finally providepersonalized news subscription service. This system has considered how to deal with the sit&s anti-reptile strategy, toavoid being blocked by the site crawler. In the concrete implementation, I will use Python with Scrapy framework towrite the crawler, then use a specificcontent extraction algorithm to extract the target data, and finally use Django and WeUI to provide news subscription background and news contentdisplay page, use WeChat to push information to users. Users can subscribe tothe specified keywords through the system, system will push thenews to the user when the crawler system crawled the contents contains thespecified keyword.
    [Keywords] Web Crawler;News; Personalization; Subscription; Python
    第一章 引言1.1 项目的背景和意义如今我们所处的时代是一个信息时代,信息处处影响着人们的生活,无论是个人还是企业,都希望能够获取自己所关心的内容。人们获取信息的方式渐渐从传统的纸质阅读转移到了信息传播速度更快互联网的在线阅读上,而许多媒体和互联网企业都推出了各自的新闻门户来提供新闻内容阅读和检索等功能,但是这些新闻信息仍需要我们主动去访问这些网站才能获取到,而且我们还要在这些新闻中筛选出自己所关心的内容进行阅读,这样浪费了我们许多阅读之外的时间。网络中的新闻分布在不同的网站上,我们往往只关心其中的一部分新闻,网络中的新闻页面往往还充斥着大量许多与新闻不相关的信息,影响了我们的阅读效率和阅读体验,如何更加方便及时并高效地获取我们所关心的新闻内容,这是一个急需解决的问题,本系统就是为了解决这样的痛点而产生的。
    1.2 研究开发现状分析1.2.1 个性化新闻服务现状如今国内外存在众多提供个性化新闻服务的互联网公司,如著名的ZAKER和今日头条,这些公司的产品都能够根据你的兴趣爱好来展示和推荐你喜欢的内容,这种创新性已经颠覆了传统的新闻资讯平台的市场格局,大众纷纷表现出对这些个性化新闻平台的追捧。据最新的《中国互联网络发展状况统计报告》显示,截至2016年12月,中国网民规模达7.31亿,移动互联网用户规模达到6.95亿,其中新闻资讯领域行业用户规模达到6.14亿,年增长率为8.8%,在移动端渗透率达到82. 2%。 [1]新闻资讯信息的用户需求也更加细分,用户对内容的需求也更加精细,除了方便阅读、时效性高、趣味性强外,个性化推荐方式也越来越受到用户的关注。在猎豹全球智库发布的安卓2016年1月新闻类APP排行榜中,作为个性化新闻平台的今日头条、一点资讯皆排在移动资讯APP的前三位。而前三中采用传统编辑推荐方式的只有腾讯新闻,可见如今个性化新闻平台已经成为绝对的主流。
    1.2.2 网络爬虫研究现状网页抓取工具是一种根据特定规则来自动获取互联网信息的脚本或程序。[2] 网页或搜索引擎等网站通过爬虫软件来更新自己的网站内容或者是更新对其他网站的索引。一般来说,网络爬虫会保留被抓取的页面,然后用户可以通过搜索引擎后来生成的索引进行搜索。因为爬虫访问网页的方式与人类相似,而且一般比人类访问的速度要快,会消耗访问的网站的系统资源。因此爬虫在需要大量访问页面时,要考虑到规划和负载等情况,否则容易被网站禁封。网站站长可以使用robots.txt文件来告诉爬虫访问的规则。robots.txt文件是一个具有指定格式的文件,网站站长可以通过此文件来要求爬虫机器人不能访问某些目录或者只能访问某些目录。互联网上的页面极多,而且数量一直在增长,即使是像谷歌这样的爬虫系统也无法做出完整的索引,因此在某些地方会根据需求来做一些主题化的爬虫,这样的爬虫爬到的结果往往能够更加精确。
    1.2.3 项目的范围和预期结果本文描述了基于网络爬虫的新闻订阅系统的设计与实现的过程,主要工作如下:

    编写一个网络爬虫,使其能够对网络中指定站点的新闻进行自动收集并存入数据库;
    数据的去重和网络爬虫的反爬虫策略应对;
    提供一个新闻展示页面,把爬取到的新闻展示给用户;
    提供新闻订阅页面,用户可以在页面输入指定订阅的关键词;
    编写微信推送服务,把用户订阅的新闻通过微信推送给用户;
    1.3 论文结构简介本论文的结构安排如下:

    第一章,引言。主要介绍了论文选题项目的背景、意义和目的;以及对相关领域中已有的研究成果和国内外研究现状的简要评述;介绍本系统涉及的范围和预期结果等。
    第二章,技术与原理。主要介绍本系统中所用到的主要技术和理论。
    第三章,系统需求分析。使用用例析取和用例规约等系统分析方法对本系统进行了需求分析。
    第四章,新闻采集与订阅系统的设计。介绍了系统的架构与原理,讲述本系统的各大模块的设计以及数据库的设计详情。
    第五章,新闻采集与订阅系统的实现。介绍本系统具体的实现过程以及实现的效果。
    第六章,系统部署。介绍本系统的部署环境与部署方法。
    第七章,总结与展望。对本系统所做的工作进行总结,提出了需要讨论的问题和一些本系统中可以改进的地方。

    第二章 技术与原理2.1 技术选型2.1.1 Python语言介绍Python是一种面向对象、解释型的计算机程序编程语言。它包含了一个功能强大并且完备的标准库,能够轻松完成很多常见的任务。它的语法比较简单,与其它大多数程序设计语言使用大括号把函数体包起来不一样,它通过缩进来定义语句块。[4]使用P帅on能够高效灵活地实现开发的任务,内置库以及大量的第三方库能够在许多地方避免重复造轮子的现象,有时使用c++语言来实现的一个功能可能需要几十行,Python只需要几行就足够了。与传统的脚本语言相比,Python拥有更佳的可读性和可维护性。这门语言的强大吸引到了许多开发者,拥有比较热门的Python社区,许多开发者在维护着这种Python编写的库,影响力也在日益增强。在网络爬虫领域,Python这门语言的使用也比较广泛,留下了大量的前人的学习研究的资料。基于以上优点,我选择了使用Python来开发本系统的网络爬虫部分和展示部分的服务端。
    2.1.2 Scrapy框架介绍Scrapy是一个纯Python基于Twisted实现的爬虫框架,用户只需要定制开发几个模块就可以方便地实现一个爬虫,用来抓取网页内容、图片、视频等。它最初是为了网站页面抓取所设计的,也可以应用在获取网络应用API所返回的各类数据或者是编写通用的网络爬虫。Scrapy用途比较广泛,可以应用于数据挖掘、自动化测试和数据监控等场景。Scrapy提供了一些网络爬虫中比较通用的中间件和模块等,也可以方便地编写自己所需的中间件来对爬取结果进行处理,只要在配置里面引用这些中间件就可以了。使用Scrpay来编写爬虫可以降低很多需要重复编写的爬虫处理代码所带来的成本。
    2.1.3 Django框架介绍Django是最早由Python实现的最着名的Web框架之一,最初是由美国芝加哥的Python用户组来开发的,拥有新闻行业背景的Adrian Holovaty是Django 框架的主要开发人员之一。在Adrian的领导下,Django团队致力于为Web开发人员提供一个高效和完美的Python开发框架,并授权开发人员根据BSD开源协议许可证免费访问。Django是一个高效的Web框架,可以帮助我们减少重复的代码,并把更多重点放在Web应用程序上的关键之处。在架构上,Django 跟Scrapy类似,也提供了中间件等,配置的方式也是类似的,使用类似的技术架构可以减少学习成本。本系统中我选用Django作为新闻订阅的服务端来提供API。
    2.1.4 MongoDB数据库介绍MongoDB是一个由C++语言编写的高性能,无模型的开源文档型数据库,是当前NoSQL数据库产品中最具有代表性的一种。MongoDB是使用文档来作为对象存储的,一条记录对应一个文档,集合类似传统的关系型数据库中的表,集合中存放的是那些具有同一特征或者属性的文档。在一个集合中,不同文档拥有的属性可以是不同的,这就是与传统的关系型的数据库的重点了,传统的关系型数据库要求表里的数据所拥有的属性格式都是一致的,MongoDB这种灵活性更利于文档映射到一个对象或一个实体上。对于需要经常改动数据格式或者数据格式不定的一些需求来讲,这种数据格式更为合适。MongoDB在读写性能方面也远超传统的关系型数据库的代表之一的MySQL。在本系统中我使用MongoDB 来存储爬取到的数据以及用户数据等。像MongoDB这样的非关系型数据库更合适储存爬虫数据,因为爬虫数据量可能比较大,数据之间关系型也不强。 MongoDB的性能也比传统的关系型数据库代表MySQL之类要强。
    2.1.5 AJAX介绍AJAX(异步的JavaScript + XML)本身并不是一种技术,它是由Jesse James Garrett在2005年提出的一个术语,描述了一种需要结合使用大量已经存在的技术的方式,包括HTML, JavaScript, CSS, DOM, JSON, XML等,还有最重要 JavaScript中的的XMLHttpRequest对象。当这些技术以AJAX模型的方式聚合时,Web应用程序可以更迅速地,无需加载整个页面就能更新全部或者部分的用户界面。这使Web应用能够更快地响应用户行为,带来更友好的用户体验。尽管在AJAX中X代表XML,但现在JSON使用的更多,因为JSON具有许多XML不具备的优势,比如它更轻量并且是JavaScript的一部分,各个程序语言都能够轻松解析JSON格式的数据。在AJAX模型中,JSON和XML的作用都是承载信息。[6]本系统会在新闻订阅和展示部分的前端使用AJAX来跟服务端进行交互,以达到前后端分离的目的。
    2.2 相关原理介绍2.2.1 网络爬虫介绍网络爬虫(英语:web crawler),也叫网络蜘蛛(spider),是一种自动提取网页的程序,它为搜索引擎从万维网上下载网页。传统的爬虫的启动从一个或多个初始网页开始的,从这些初始网页上获得接下来要爬取的URL,在抓取网页内容的过程中,不断从当前页面的内容上抽取新的需要继续爬取URL放入队列,直到满足系统的一定停止条件。[7]网络爬虫的抓取策略大致可以分为以下三类:广度优先搜索策略、深度优先搜索策略、最佳优先搜索策略等。本系统的爬虫部分使用的爬虫策略是广度优先搜索策略,因为本系统的网络爬虫具有针对性,所以爬取的层数不会很多。
    2.2.2 关键词提取技术通过分析文本,利用关键词抽取技术可以抽取出文本的关键词,关键词能够简单地反映出文本的主要内容,使人们更加直观方便地了解到文本内容的主题。关键词提取的技术有许多种,最常用的应该是基于统计的方法的TF-IDF算法。 TF-IDF(term frequency-inverse document frequency)是一种常用的用于数据挖掘与信息检索的加权技术。[8]词语的重要性是在TF-IDF算法中主要是由它在文中出现频率决定的。
    Jieba是一个基于Python的中文分词库,支持三种分词模式:精确模式、全模式和搜索引擎模式,它能够基于Trie树结构来实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图,采用了动态规划查找最大概率
    路径,找出基于词频的最大切分组合。在本系统中,将考虑使用Jieba分词基于 TF-IDF算法的关键词抽取来抽取出爬取到的新闻内容的关键词。
    2.2.3 智能推送技术当我们使用如今的一些应用程序时,会经常收到来自这些应用的推送,过多的、不适当的推送会打扰到用户,互联网技术在最近几十年里已经得到了很大的发展,但是推送通知技术仍旧停留多年以前。为了实现智能化推送,我们需要搜集和分析能够帮助我们实现智能化推送通知的用户数据,这些数据的来源可以是用户的设置或者用户在应用中产生的数据等。在智能推送通知中,与时间相关的,个性化的,有帮助的,有联系性的是智能推送通知中四个基本的特征。
    在推送的时间上,我们可以做到时间智能化,时间智能化指的是推送的时间要恰当,推送发生在不恰当的时间比无用的垃圾推送消息造成的不良效果更验证。在不恰当的时候推送的通知不但打扰到用户,还很容易会被用户忽略。智能推送应该能够做到自动解决推送时间不恰当的问题。具体的实现可以通过一个对推送信息的重要性评估的引擎来决定消息推送的时间。
    在个性化上,我们可以把推送机器人设计成人类的形态,比起传统的系统消息,拟人化的方式能够让人更加容易接受,如苹果的Sin和微软的Cortana。当我们将来自机器智能的推送通知语言根据用户自己的特点进行调整后,用户在查看时看到的是更像交流式的风格,会感到更有亲切感,更加个人化。
    在推送的内容上也要慎重选择,根据不同用户来推送不同的内容。因为对于用户来说,只有用户关心的内容才是对用户有帮助的。拿新闻订阅应用来讲,就是用户只会对某些主题内容的新闻感兴趣,应用要做的就是把新闻的主题进行分析,打上不同的标签。然后找到用户兴趣中含有这些标签用户进行推送。如果不经处理对全部用户进行统一的推送,用户需要花费大量时间在这上面去过滤出自己感兴趣的内容。
    在推送内容的数量上也有讲究,如果一个服务高频次地的使用通知推送,用户可能会感觉到被冒犯然后会关掉它,后面的推送就都收不到了。因此,将推送的内容进行分组就很重要了。系统可以把一些相似的通知进行分组合并,可以减少对用户的打扰,在消息很多的时候这种优势就很明显了。在信息较少的时候可以选择把这些推送通知进行展开,因为此时用户可能会比较关心这些少见的内容,也是一个不错的选择。
    第三章 系统需求分析3. 1 新闻订阅系统用例析取基于网络爬虫的新闻采集与订阅系统要实现新闻数据抓取,数据过滤,数据筛选,数据展示,新闻订阅,推送等服务和功能,本系统用例图如图3.1所示:

    本系统主要用于以下几类人员:

    数据管理员,完成数据的抓取,过滤与筛选,新闻的推送,以及本系统管理维护等。
    用户,在网页上进行新闻订阅,通过微信接收订阅新闻的推送,点击进入对应新闻展示页面等。

    3.2 新闻订阅系统用例规约3.2.1 新闻订阅3.2.1.1 简要说明本用例允许用户增加或者删除自己订阅新闻的关键字,以及对已经订阅的关键字进行确认等操作。
    3.2.1.2 参与者用户。
    3.2.1.3 事件流基本事件流:用例开始于用户进入新闻订阅页面进行操作。

    订阅新闻关键字的状态共有两种,分别为“已订阅”、“未订阅”。顾客可以在相应的状态下进行操作,选择增加关键字或者删除关键字。
    如果关键字状态为“未订阅”,用户可以增加该关键字到自己的订阅列表中,本用例结束。
    如果关键字状态为“已订阅”,用户可以选择删除该关键字,本用例结束。
    无特殊要求。
    前置条件:本用例开始前用户必须是微信已登录状态。
    后置条件:如果用例成功,用户的订阅列表将被更新。
    活动图

    3.2.2 新闻推送3.2.2.1 简要说明本用例允许数据管理员根据新闻的关键字向已经订阅该关键字的用户进行推送等操作。
    3.2.2.2 参与者数据管理员。
    3.2.2.3 事件流基本事件流:用例开始于爬虫系统采集到新闻时。

    系统将新闻内容根据算法与用户订阅的新闻关键字作对比,对比结果分别为“匹配”、“不匹配”。如果匹配状态为“匹配”,系统将调用微信推送接口向用户推送该新闻,本用例结束。如果匹配状态为“不匹配”,用户将不会收到该新闻的推送,本用例结束。无特殊要求。前置条件:本用例开始前采集到的新闻必须有效。后置条件:如果用例成功,用户将收到一条新闻推送。活动图

    第四章 新闻采集与订阅系统的设计4.1 系统架构及原理本新闻采集与订阅系统分别由爬虫部分与新闻订阅和展示部分构成,在新闻订阅与展示部分采用基于C’s的架构,代码的组织方式为MVC三层结构,其中的三个层次分别为视图层(View)、控制器层(Controller)和模型层(Model)。代码整体采取前后端分离的方式,前端负责视图层,后端负责模型层和控制器层,客户端使用微信和网页实现,前后端通讯使用AJAX交换JSON的方式。系统的总体框架图如图4.1所示:

    爬虫部分使用了Python编写Scrapy框架,它的基本架构如图4.2所示,其中 Scrapy引擎的作用是控制数据的流向,是整个爬虫框架的核心。网络蜘蛛(spiders) 定义了如何爬取某个(或某些)网站,包括了爬取的动作以及如何从网页的内容中提取结构化数据。蜘蛛中间件(spider middleware)是在Scrapy引擎和网络蜘蛛间的一个钩子,它可以处理蜘蛛的输入与输出。调度器(scheduler)能够从Scrapy引擎接受请求并放入队列,在引擎请求调度器时返回对应的请求。下载器(downloader)负责下载网页,把爬取到的内容返回给Scrapy引擎和网络蜘蛛。下载器中间件(downloader middleware)是在Scrapy引擎和下载器间的一个钩子,它可以处理传入的请求跟传出的响应。Item Pipeline负责处理网络蜘蛛传过来的Item,可以在此做数据格式化,数据清理等操作。

    爬虫的整体上数据流向的开始是由Scrapy引擎让网络蜘蛛以一个初始的 URL来初始化一个请求,并设置回调函数,然后网络蜘蛛把该请求向调度器申请任务,把申请到的任务交给下载器,这里会经过一次下载器中间件,然后下载器把下载完后产生的响应再经过一次下载器中间件,然后传递给引擎,引擎接收到该响应后通过蜘蛛中间件传给网络蜘蛛处理,网络蜘蛛处理该响应,产生一个 item或者是新的请求给引擎,引擎会把传过来的item放入Item Pipeline,把传过来的新的请求传给调度器,Item Pipeline获取接收到的item对该item进行逐层处理,接着这个流程就重复直到爬取完成。
    4.2 系统模块设计4.2.1 爬虫采集模块设计使用Scrapy框架来编写爬虫首先要编写核心的蜘蛛(sPiders)的代码,Spider 类定义了如何爬取某个(或某些)网站,包括了爬取的动作以及如何从网页的内容中提取结构化数据。本系统主要针对网易新闻和腾讯新闻的科技频道进行主题式的爬虫,所以设计了两个网络蜘蛛,名字分别为Neteasespider和QQSpider,如果后续需要更多需要爬虫的站点,只要增加对应站点的网络蜘蛛就可以了,其余处理部分都是通用的。在这里选择主题式的爬虫的原因主要是一个通用爬虫对于新闻这样的每个站点的文章有固定格式的爬取解析的代价比较大,还不如手工去对需要爬取的站点进行分析,根据每个站点的特点来编写解析的代码。这样的主题式的爬虫能够提高爬虫的精确度,同时也提高了爬虫的效率,这样我们的爬虫就能够及时爬取到最新的新闻内容了。每个站点对应一个特定的网络蜘蛛还有一个好处就是如果后续需要完成分布式爬虫等需求时会很方便,因为这样的方式代码之间的祸合度较小,同时非常简洁。
    网络蜘蛛首先从一个Start url开始爬取,这里我选取了网易新闻和腾讯新闻的科技频道的首页,蜘蛛爬取这个起始URL上的页面后,对里面的内容进行解析。因为每篇新闻的URL都具有一定的格式,凡是该页面上有符合这种格式的 URL,蜘蛛都会对这些URL进行回调,继续爬取这些URL的页面,这些页面上就会包含所要获取的新闻的内容了。对于同一个新闻站点来说,一般页面上的内容的结构也是一样的,所以按照一定的规则来对这些页面上的内容进行解析,获得新闻内容原始数据,对这些数据进行格式化的处理,封装成一个item,传回给 Scrapy引擎处理。
    因为新闻具有一定时效性,一般来说我们只会关注那些新产生的新闻内容,所以本爬虫不需要考虑需要爬取过往产生的新闻的情况。本系统的爬虫部分只聚焦于每个站点的首页的新闻,因为新闻是滚动刷新的,所以我们需要定时对首页进行爬取,获取新的新闻内容。本系统设计了一个类似守护程序,来控制爬虫的启动与停止,在爬虫结束后等待一段时间再重新开始爬取。
    4.2.2 爬虫去重模块设计在爬虫过程中会遇到重复内容的情况,所以我们需要设计一个爬虫用到的去重的模块。考虑到每个URL对应的新闻内容是不变的,我们只要针对URL来进行去重即可,而不需要等到把内容取回来之后再判断内容是否已经爬取过,那样会消耗大量额外的资源,也对目标网站造成了额外的压力,显得不友好。我们选择去重的时机是Scrapy的调度器把请求分配给下载器之前,也就是说在下载器中间件中处理,在本系统中定义了一个下载器中间件RedisMiddleware,这个中间件的作用是在Redis的一个散列中判断是否存在该URL,如果不存在,把该请求传给下一个中间件处理。如果该URL存在于散列中,则忽视掉该请求,不进行后续操作。在本系统中定义了一个Item管道RedisPipeline,在爬取数据完成后,数据库处理完后Item会传到该管道,该管道的作用是把这个新闻所属URL 存入Redis的散列中,标记该URL已经爬取。
    4.2.3 防反爬虫模块设计防反爬虫是在大多数爬虫中需要考虑的情况,因为爬虫对网站服务器造成的压力比正常人要多,如果爬取频率足够高的话,会使网站访问变慢,甚至无法访问,所以网站可能会有一系列的反爬虫措施。首先我们的爬虫需要遵守网站的爬虫协议,然后把爬取速率控制好,例如间隔一秒才爬取一个页面。其次,我们需要伪装成一个浏览器,有些网站会通过HTTP请求头中的User-Agent中的信息来判断用户,我们不但需要在爬虫请求中的HTTP设置User-Agent请求头,还需要对该请求头进行更换,因此在本系统中定义了一个下载器中间件RotateUserAgentMiddleware,这个中间件的作用是在请求前在请求的HTTP请求头中设置一个轮换的随机的模拟用户浏览器的User-Agent请求头,这些 User-Agent与真实浏览器的User-Agent一致,数据来源是Python中一个叫 fake-useragent的库。后续如果对方服务器针对’P进行禁封了的话可以采用代理服务器的方式来应对,在做了以上措施的情况下本系统目前没有出现过被禁封的情况,因此该方法没有在本系统中实现。
    4.2.4 爬虫存储模块设计爬虫的数据存储是一个爬虫系统中很重要的一部分,因为爬虫的目的就是获得数据,在这里我们需要考虑数据的存储方式与储存时机。在本系统中储存部分使用了ORM(对象关系映射)的方式来实现,ORM的好处在于把数据访问的细节隐藏起来,在ORM上的操作出错的可能性会比手写数据库操作的可能性低。在ORM中,我们只需要关注数据的结构,这样一来,我们只需要编写数据储存对象的参数定义等属性跟方法就可以了,初始化、查询、更新等操作都可以由 ORM来实现。在本系统中,爬虫部分与订阅和展示部分都共用一个数据库,爬虫部分需要对数据库进行写操作,展示部分需要对数据库进行读操作。在蜘蛛解析完数据后,蜘蛛会把封装好的Item通过Scrapy引擎传给Item管道,本系统中定义了一个MongoDBPipeline,这个管道的作用是维持一个MongoDB的数据库链接,接收到传入的Item后先校验完数据的完整性,然后把合法的数据插入数据库对应的集合中,否则丢弃该Item。
    4.2.5 消息推送模块设计消息推送部分本系统使用微信来实现,需要用户关注指定公众号。本系统需要推送消息给用户时,先选择一个本系统预定义的模板,在模板中填入消息标题,内容和链接等数据后,通过微信提供的接口来进行推送。这里需要注意的是我们需要给推送的消息接口提供一个本系统所用的公众号的AccessToken,这个 AccessToken是向微信证明本系统的凭证,它有一定的有效期,需要定时刷新。
    4.2.6 消息订阅与展示模块设计消息订阅与展示模块是本系统中与用户交互的模块,这个模块负责用户订阅新闻的功能与向用户展示所需新闻内容的模块。在本系统蜘蛛解析完数据后,蜘蛛会把封装好的Item传给MongoDBPipeline储存后,会继续往下传递,传递到一个PushPipeline中,这个管道的作用是判断爬取到的数据是否包含用户所订阅的关键词,如果包含的话则调用消息推送模块把新闻消息推送给用户。在消息推送后,用户会在微信端本系统的公众号中接收到一条包含新闻消息简要内容的消息,点击该消息可以跳转到新闻展示页面。本系统提供了一个消息订阅页面,用户可以在该页面上管理自己的新闻关键词。
    4.3 数据库设计本系统存放数据用到的数据库分别是Redis和MongoDB,在本系统的数据库设计中,数据库的集合主要包括爬取到的新闻信息集合和用户订阅新闻关键词集合,系统的配置信息都写在配置文件中,就不需要使用数据库来存放了。这里选择MongoDB的原因是考虑到当爬虫的数据量和并发数很大时,关系型数据库的容量与读写能力会是瓶颈,另一方面,爬虫需要保存的内容之间一般不会存在关系。另外本系统会使用Redis中的散列类型来存放已经爬取过的URL和不合法的URL,因为判断URL是否合法或者是否已经爬取过是一个高频的操作,使用 Redis这样的高性能的内存键值对类型的数据库可以减少主数据库的压力,同时提高爬虫的性能。
    新闻信息集合



    属性名
    含义
    类型
    说明




    title
    新闻标题
    string



    content
    正文内容
    string
    纯文本


    source
    来源
    string
    新闻出处


    published
    发布时间
    timestamp
    精确到秒


    url
    原文链接
    string
    用于跳转



    用户订阅新闻关键词集合



    属性名
    含义
    类型
    说明




    open_id
    用户微信openid
    string
    唯一标识


    keywords
    订阅的关键词列表
    array
    字符串类型的数组


    tags
    订阅的标签列表
    array
    字符串类型的数组



    第五章 新闻采集与订阅系统的实现5.1 系统框架实现本新闻采集与订阅系统的爬虫部分框架是利用Scrapy自带的命令行工具来初始化,初始化后已经创建好了Scrapy引擎所需的几个重要的文件,如中间件,数据管道,配置文件等,这样做的好处是能够快速搭建起框架,并且能够达到官方定义的最佳实践。接下来我们可以在这个目录下定义自己的一些模块文件,再在这些文件中实现自己的处理函数就可以了,最终实现的爬虫部分的目录结构如图5.1所示,其中items.py是用于定义数据储存模型的文件,middlewares.py是用于定义中间件的文件,pipelines.py是用于定义数据管道的文件,settings.py是本系统爬虫部分的配置内容,spiders文件夹中存放了不同爬虫的网络蜘蛛代码, utils.py则是一些通用的函数存放的地方,wechat_config.py和wechatpush.py分别是微信推送部分的配置和推送代码。

    新闻订阅和展示部分的API服务器端则使用Django自带的命令行工具来初始化,使用django-admin startproject命令来新建一个项目,然后使用django-admin startapp命令来新建一个app,这样API服务器的基本框架就完成了,然后往创 建的目录中添加其余代码,最终实现的新闻订阅与展示部分的目录结构如图5.2 所示,其中frontend文件夹存放的是本系统的前端静态文件,分别是新闻订阅页面和新闻展示页面,init_db.py文件是一个用于初始化数据库用的脚本,lib文件夹中存放的是本系统中一些能够被公用的函数文件。manage.py是由Django生成的用于管理任务的命令行工具脚本,newsweb存放的是本项目的代码,run server.sh是一个用于启动服务器的脚本文件,web server中存放的是本系统新闻订阅与展示部分的服务端代码的主要文件,主要包括了用于配置路由 urls.py,存放新闻和订阅信息数据模型models.py和提供API的views.py。

    5.2 爬虫采集模块实现爬虫采集模块的核心的网络蜘蛛,下面以爬取网易科技频道新闻的蜘蛛为例讲解本系统爬虫采集模块的实现过程。图5.3为该蜘蛛的解析网页请求响应的代码,首选我通过分析网易科技频道新闻中的网页源码,分析得到网页中所需的新闻内容的数据所在的位置特征信息,例如通过分析发现标题位置是处于html标签下的head标签里的title标签里的文本。24-27行中的代码的作用是通过xpath 使用之前分析出来的格式来从抓取到的数据中提取出新闻相关的信息,包括新闻标题、新闻消息来源、新闻内容、新闻发布时间。29-32行的代码作用是把时间解析为时间戳,这样做的目的是为了方便把时间转换成不同的表现格式,时间表现会更为准确。34-40行的代码作用则是把数据封装成一个本系统中的新闻Item, 然后传给Item管道来处理。另外一个用于爬取腾讯新闻科技频道蜘蛛的分析方法和代码写法是类似的,在这就不详细介绍了。

    为了实现定时爬虫的功能,在本系统中实现了一个名为worker.py的守护进程脚本和一个start_crawl.py的用于调用爬虫的脚本,运行worker.py脚本后会每三十秒调用启动一次start_crawl.py,start_crawl.py每次启动会调用爬虫主程序,程序核心代码如下:

    5.3 防反爬虫模块实现为了防止反爬虫对本系统爬虫部分的影响,对于每次请求,本系统都会伪装成一个真实的用户,防止被爬取的网站通过User-Agent等信息来判断或者禁封 掉本系统的爬虫,导致后续爬虫无法正常进行。本系统在发送请求之前会在请求的头部加上User-Agent的请求头信息,这个请求头的信息会在本系统配置中的 User-Agent列表中随机选取一个,图5.5为部分User-Agent信息。除此之外,还可以利用代理服务器来代理请求,防止被爬取的网站通过IP信息来禁封本系统爬虫。

    5.4 爬虫存储模块实现爬虫储存模块的数据设计与格式等在上一章已经说明,在这介绍在数据库中的具体实现。爬虫爬取到的新闻数据会存放于MongoDB中,使用ORM来映射数据对象模型到数据库,使用的ORM框架是MongoEngine,下面通过讲解一个新闻内容的数据模型的定义来说明这种定义方式,在图5.6中的第9行,我们定义了一个父类为MongoEngine的Document类的类,这样定义就使这个类拥有了关系对象映射的能力,再在这个类中定义一个to_json的方法,作用是把本类的实例转化为一个Dict类型的数据,方便API调用时将对象转换成JSON格式的数据返回给前端。图5.7展示了部分爬取到的新闻数据的内容。

    5.5 消息推送模块实现消息推送模块使用了微信公众号的推送,在本系统中使用微信的接口测试号来代替公众号,微信的接口测试号是一种用于测试的,可以使用微信号扫一扫登录的账号,而且这种账号能够直接体验和测试公众平台所有高级接口。在申请完后登录系统,获得该系统的applD和appsecret,这两个字符串是使用该账号的凭据。需要注意的是,用户需要关注本账号后才能够收到本账号推送的消息。

    接下来我们在网页下方新增一个消息模板,填入推送新闻消息的模板内容,填写完成后记录对应的模板ID。

    获得以上信息后把信息写入消息推送模块的配置文件中,供消息推送模块调用。下面讲解消息推送模块核心部分的实现,核心部分如图5.10所示,是一个名为send_msg的函数,这个函数接收四个参数,分别为新闻标题、新闻内容、新闻的ID和订阅者的openid。订阅者的openid是用户微信的唯一标识,在测试号的页面可以查看已关注该账号的用户微信的openid。该函数的29-42行的作用是把数据封装成微信推送接口所需的格式,然后在45行使用requests模块来 POST一个请求到微信推送接口,微信推送接口收到请求后会在公众号中把该消息推送给用户。该函数使用了一个自定义的装饰器update_token来装饰,之前存放的applD和appsecret可以用来生成推送用的access token,而这个access token 有固定的存活期限的,这个装饰器的作用就是定时去获取这个access token并存放,直到过期之后再重新获取。
    爬虫模块爬取到含有用户订阅的关键词的新闻时会向该用户推送这则新闻,图5.11是用户在微信公众号上收到的该新闻的推送消息示例。

    5.6 消息订阅与展示模块实现消息订阅与展示模块主要由前端静态文件部分和后端API部分组成。在开发方式上本系统选择了使用前后端分离的方式,前端通过AJAX的方式来跟后端提供的API进行交互,后端API服务器收到请求后返回对应的JSON格式的数据给前端,前端根据数据来渲染出最终展示给用户的页面,这种前后端分离的方式有效地降低了代码之间的Wi合度。在前端实现方面,使用了jquery来对DOM元素进行操作以及进行异步请求等,另外使用了WeUI的样式库,WeUI是一套提供同微信原生一致的视觉体验的基础样式库,由微信官方设计团队为微信内网页和微信小程序量身设计,令用户的使用感知更加统一。
    接下来以用户端的角度来展示消息订阅与展示模块的实现。
    用户想要收到新闻推送,需要先关注本系统的公众号,然后打开新闻订阅页面:

    这时可以输入要订阅的关键词,这里填IT,点击添加订阅关键词,系统提示添加关键词IT成功,刷新已订阅关键词列表:

    点击已订阅关键词列表中的项会弹出对话框询问是否删除该关键词:

    点击确定,提示操作成功,同时刷新已订阅关键词列表:

    在订阅关键词后,系统爬虫爬取到相关内容时会把内容通过微信推送给用户,用户点击后可以看到新闻内容,在此页面可以点击查看原文按钮打开原新闻页面,还有可以点击订阅更多前往订阅新闻关键词的页面:

    点击查看原文,会跳转到新闻的原页面:

    第六章 系统部署6.1 部署机器概述为了运行本新闻采集与订阅系统,至少需要一台拥有公网’P的Li~服务器,这是为了用户在外网能够访问到。至于配置方面则不需要太高,在测试时我选用了一台腾讯云上的服务器,这台服务器的配置如下:



    项目
    内容




    操作系统
    Ubuntu Server 14.04.1 LTS 64位


    CPU
    1核


    内存
    1GB


    系统盘
    20GB


    公网带宽
    1Mbps



    6.2 配置环境
    安装Nginx作为反向代理服务器,并编辑Nginx相关配置文件,这样做是为了把不同的请求分发到后端不同的地方,例如请求前端文件就返回静态文件,请求API就把请求转发给API服务器,Ningx的配置文件部分内容如图6.1所示。这样就实现前后端分离而又不受到跨域请求限制的影响了。编辑完Nginx配置文件后,重启Nginx服务器。
    安装PIP, PIP是Python用于管理第三方库的一个软件,这里用于安装本系统所需的第三方库。
    使用PIP安装本系统所依赖的第三方库,包括pymongo, scrapy, redis, fake-useragent, django,mongoengine, jieba, lxml, gevent, gunicorn等。


    6.3 系统运行由于本新闻采集与订阅系统是由爬虫部分与展示部分组成,所以需要分别运行行爬虫的守护进程和后端API服务器,静态页面是由Nginx指定的一个目录来提供的,不需要后台服务器。
    使用python worker.py命令来运行爬虫的守护进程,得出以下输出:

    使用sh run_server.sh命令来运行后端API服务器,这个脚本的实际作用是使用gevent作为gunicorn的worker来运行4个后端API服务器进程,成功运行会得到以下输出:

    第七章 总结与展望7.1 总结本系统是一个基于网络爬虫实现的新闻采集与订阅系统,实现了对网络上新闻内容的自动化采集、用户新闻关键词订阅、新闻内容展示以及新闻推送等功能。为实现本系统的功能,查阅了大量学习资料,在实现方面使用了一些比较前沿的技术以及较多的第三方库,从中能够学习到很多新知识和新技能。在本文中较为完整地从系统的需求分析、不同模块的设计与实现几个方面来展示了一个完整的爬虫系统以及对应的新闻订阅API服务器等的实现过程,最后在云服务器上部署本系统并测试,达到了预期的效果。
    7.2 展望本新闻采集与订阅系统在设计上考虑了许多来降低代码之间的祸合度,同时提高代码的健壮性与性能,使本系统能够达到容易扩展以及高可用的需求,即便后续需要爬取另外一个新的新闻网站上的新闻,只需要编写对应网站的解析部分就可以了,大部分代码已经被模块化,能够被重用。对比了已有的类似的成熟大型新闻服务系统,发现还有以下能够改进的地方:

    本系统只对新闻的基本文字信息等进行了采集与展示,后续可以考虑实现对新闻中图片与视频等多媒体信息的采集。
    本系统缺乏一个较为完善的用户模块,目前用户是在配置文件中配置的,用户模块对于这类的订阅系统是比较重要的。
    订阅机制不够智能,也没有智能推荐等功能,后期可以采用机器学习等人工智能方法来实现智能化推送与推荐功能。

    除了以上几点,本系统仍然存在许多能够改进的地方,但由于本文作者水平有限以及时间限制,未能够将这些一一实现,还希望各位专家学者能够给予批评与建议。
    参考文献[1] 中国互联网络信息中心. 中国互联网络发展状况统计报告[EB/OL]. http://www.cnnic.cn/gywm/xwzx/rdxw/20172017/201701/t20170122_66448.htm, 2017年
    [2] 胡博,基于网络爬虫的内容资源评价研究[D];北京理工大学;2015
    [3] 李建中、李金宝、石胜飞,传感器网络及其数据管理的概念、问题与进展,软件学报,14(10):17 17-1727, 2003
    [4] 邝洪胜;基于Python的电商导购APP设计与实现[D];华南理工大学;2015
    [5] 基于Django的自动化运维管理系统的设计与实现[D].姚娜.西安电子科技大学2015
    [6] 关系与非关系数据库应用对比研究——以SQL Server与Mongo DB为例[D].吴德宝.东华理工大学2015
    [7] 基于网络爬虫的网站信息采集技术研究[D]. 孙骏雄. 大连海事大学2014
    [8] 基于网络爬虫的内容资源评价研究[D].胡博.北京理工大学2015
    [9] Wang J, Guo Y. Scrapy-based crawling anduser-behavior characteristics analysis on Taobao[C1//Cyber-EnabledDistributed Computing and Knowledge Discovery (CyberC), 2012 International Conference on. IEEE, 2012:44-52.
    [10] Castillo C. Effective web crawling[C1//Acm sigir forum. Acm, 2005, 39(1):55-56.
    [11]刘金红,陆余良.主题网络爬虫研究综述 [J].计算机应用研究,2007, 24(10) :26-29.
    [12]徐远超,刘江华,刘丽珍,等.基于Web的网络爬虫的设计与实现[J].微计算机信息,2007 (21): 119-121. MLA
    [13] 王成军.“今日头条”的技术逻辑:网络爬虫+矩阵筛选[J].传媒评论,2015 (10) : 34-37.MLA
    [14] Jaiswal S, Kumar R. Learning Django Web Development [M]. PacktPublishing Ltd, 2015.
    [15] Taneja 5, Gupta P R. Python as a Toolfor Web Server Application Development [J]. 2014.
    [16] Web数据挖掘及其在网络新闻文本数据中的应用[D].胡峰.电子科技大学2010
    [17] 乔峰.基于模板化网络爬虫技术的Web网页信息抽取[D].电子科技大学2012
    致谢从本论文的选题、资料收集、资料阅读、到论文编写完成的这段时间中,我收获到许多宝贵的知识与经验。在这段时间里我查阅了许多相关资料,也使用了一些他人开发的第三方程序库,这些都减轻了我的压力。在此要感谢前人的付出,留下了那么多的学习研究材料。
    在这我要特别感谢我的论文导师卞静老师,卞老师在我完成该论文期间给予了我悉心指导与帮助,在繁忙之中抽出时间来对我的论文进行了指导并提出许多宝贵的建议。卞老师拥有渊博的专业知识、严谨的治学态度和平易近人的处事作风,是我终生学习的楷模。在此向我的导师表示最诚挚的谢意和最衷心的祝愿。
    其次,我要感谢我的家人,他们一直以来都能够理解,支持并关心我,从而使我能够专心投入到学习和工作中,让我在求学的过程中感受到温暖的力量,并能够顺利完成学业。
    在我大学学习和生活中,得到了许多老师和同学的关心与帮助。感谢我的舍友,在我学习上遇到疑惑的时候能够悉心教导我,在论文编写、技术路线和具体实现上也得到了他们宝贵的建议,谢谢你们!
    最后,在此对所有在我做毕业设计期间帮助,关心和支持过我的老师、同学和朋友们,以及百忙之中抽出时间来审阅、评议本论文的各位专家们表示衷心的感谢。
    4 评论 55 下载 2018-09-30 23:27:17 下载需要12点积分
  • 基于Android Studio实现的论坛网站Android客户端和JAVA EE后台

    第一章 概述1.1 开发环境本安卓程序在Windows 10系统下使用Android Studio开发,后台使用MyEclipse开发,测试环境为安卓系统5.1、4.4、4.3、5.0,屏幕尺寸5.0、5.1、5.5的安卓手机。
    1.2 安装配置本安卓程序要求安卓SDK为API level 15以上,target level为23,即,android 4.0.3到android 6.0。
    1.3 需求分析本程序为安卓APP开发项目,项目内容为IT主题的论坛开发。根据需求分析,需要一下功能:

    用户的登陆与注册功能
    用户权限包括管理员、版主、普通用户和游客身份登陆
    根据不同权限登录用户可以对帖子进行不同程度的管理,管理员可以查看、删除所有帖子,版主可以查看所有帖子、删除做属板块的帖子,普通用户和游客都只能查看帖子
    除了游客外所有用户都能发帖、回帖,游客不能发帖、回帖
    用户可以查看自己发的帖子并管理自己的帖子

    第二章 程序概要设计2.1 程序功能模块本安卓程序分为登陆注册模块、查看帖子模块、删除帖子模块、回复帖子模块。

    登陆注册模块是程序的入口,如果已经登陆过了又没有退出登陆,就可以自动登陆,如果没有账号,可以游客登陆
    查看帖子模块可以根据板块选择性查看所有帖子,已注册用户可以查看自己的帖子,帖子信息包括标题、内容、时间等
    删除帖子模块包括管理员可以删除所有帖子,版主可以删除所属板块的帖子,所有用户可以删除自己的帖子,游客不能使用删除功能,程序根据登陆用户的权限自动显示是否可以使用
    回复帖子模块用于已注册用户对帖子的回复,游客不能回复帖子

    2.2 流程图
    2.3 程序文件结构分析java文件夹下MyWork包内的java文件是每个布局对应的Activity,其中Circle是自定义的一个控件;control包下是两个适配器,包括主页面的适配器MainAdapter和回复页面的适配器LookAdapter,还有联网的工具类NetWord。

    res资源文件夹下的layout布局资源文件包括各个Activity的布局和listview的自布局,其中tool_bar是ToolBar的布局。

    menu文件夹下是两个ToolBar的菜单布局,values文件夹下使用到了strings文件和colors文件,特别的是attrs文件,这个是Circle自定义控件的配置文件。

    2.4 数据库设计本程序使用javaEE后台,数据库为sql server,包括card表、section表和userTable表,分别的字段如下:

    card:id、nameId、contents、date、title、sectionId、replyId、isTop、topDate
    section:id、name、host
    userTable:id、username、password、power、pic

    第三章 程序详细设计3.1 关键代码分析联网:使用java jdk自带的HttpURLConnection进行联网,并使用json进行数据传输。图3-1包括了链接网络的方法connect()、传参的输出流ObjectOutputStream,图3-2判断了状态码是否为200然后接收后台传过来的参数并处理返回,接收参数使用InputStream输入流。


    自定义适配器:先写了继承BaseAdapter的适配器类,这个项目中使用了两个适配器,分别为MainAdapter、LookAdapter,作为主界面和回复节目的适配器。两个泪重写了getView方法,处理每个listview中每个item的布局,以显示每一个帖子。
    侧滑菜单:在布局中使用DrawerLayout如图3-3,在Activity中使用ActionBarDrawerToggle作为侧滑开关,并实现开和关的监听,如图3-4。


    界面返回刷新:使用Intent跳转时用startActivityForResult,并重写onActivityResult方法,如图3-5。

    3.2 疑难问题解决用户权限识别:在第一次登陆成功后将后台返回的用户信息存进SharedPreferences,后面根据从中取出来的权限信息进行判断,然后决定是否显示相应按钮。

    第四章 程序的发布和测试4.1 发布过程安卓端的发布,将程序运行到手机,通过android studio的Logcat查看运行信息;
    JavaEE端的发布,将程序部署到Tomcat,同样通过myeclipse的Logcat查看运行信息。
    4.2 测试过程图4-1为登陆界面,点击注册按钮到如图4-2的注册界面,注册成功后跳转回登陆界面登陆。



    登录
    注册









    图4-3为管理员登陆的界面(还有打开侧滑菜单的状态),图4-4为版主和普通用户及游客登陆后的界面。



    管理员登录
    普通用户登录









    图4-5为游客登陆的侧滑菜单界面,与其他用户是不同的,只有一个选项,当其他用户登录时,可以点击右上角的笔进入发帖节目,如图4-6。



    侧滑菜单界面
    发帖









    第五章 分析与总结5.1 优点本安卓程序的优点在于界面简洁,有与服务器后台联系,交互友好,基础功能及部分拓展功能已经实现。
    5.2 缺点本安卓程序的缺点在于界面跳转仍有部分不流畅,因为代码效率问题有待改进,还有其他计划中的拓展功能由于时间关系还没实现。
    5.3 总结通过此次安卓大作业的开发,让我对安卓开发更加熟悉,对知识点的运用更加熟练,由于运用到了javaEE后台,使我对javaEE的熟练度也增加了。因为这次大作业的制作时间比较短,所以刺激了自己的潜力,锻炼了自己赶项目的感觉。
    第六章 参考目录[1] 《疯狂安卓讲义》 电子工业出版社 李刚 2015.8
    [2] 《android应用开发学习手册》 清华大学出版社 管蕾 2015.7
    9 评论 106 下载 2018-11-06 15:32:08 下载需要10点积分
  • 基于Python Web框架和MySQL的图书借阅系统

    1.需求分析1.1 系统目标利用Python web框架和MySQL开发一个仿真模拟的图书借阅系统。分为管理员和读者两个方面的功能实现。
    1.1.1 管理员方面
    增、删、改、挂失图书信息和读者信息
    查看用户信息和图书信息
    登录管理员界面
    为读者办理借书还书

    1.1.2 读者方面
    登录读者界面
    查询借书记录和个人信息

    1.2 数据流图1.2.1 读者注册
    1.2.2 图书上架
    1.2.3 图书搜索
    1.2.4 图书借阅
    1.2.5 借书记录搜索
    1.2.6 登陆
    1.2.7 图书、读者证挂失
    1.2.8 图书、读者删除
    1.3 数据字典1.3.1 数据项


    数据项名
    别名
    数据类型
    说明




    读者编号
    reader_id
    varchar(5) PK
    读者证的编号按顺序系统分配


    姓名
    reader_name
    varchar(20)
    读者姓名


    性别
    sex
    char(2)
    读者性别


    出生日期
    birthday
    date
    读者出生日期


    电话
    phone
    varchar(20)
    读者电话


    手机
    mobile
    varchar(20)
    读者手机


    证件名称
    card_name
    varchar(8)
    读者的证件可以是身份证学生证等


    证件编号
    card_id
    varchar(18)
    读者的证件号


    会员级别
    level
    varchar(6)
    有三个级别,普通银卡金卡


    办证日期
    day
    date
    即注册日期


    读者登录密码
    Password(Readers表)
    varchar(45)
    读者登录系统中时使用的密码


    图书编号
    book_id
    varchar(5)
    图书的编号系统分配


    书名
    book_name
    varchar(50)
    书的名字


    作者
    author
    varchar(20)
    书的作者


    出版社
    publishing
    varchar(20)
    书的出版社


    类别编号
    category_id
    varchar(5)
    书的类别编号


    单价
    price
    double
    书的价格


    入库日期
    date_in
    datetime
    入库的时间即图书上架时间


    库存数量
    quantity_in
    int
    上架的书的数量


    借出数量
    quantity_out
    int
    书借出去的数量


    遗失数量
    quantity_loss
    int
    书的丢失的数量


    出借日期
    date_borrow
    date
    书借出去的日期在借阅中生成


    应还日期
    date_return
    date
    根据会员的级别确定的日期


    遗失
    loss
    char(2)
    有“否”“是”区分书是否丢失了


    类别名称
    category
    varchar(20)
    类别的名字如“计算机”


    最长出借天数
    days
    smallint
    根据会员级别相应天数


    最多借书册书
    numbers
    smallint
    根据会员级别能借最多的书


    会费
    fee
    smallint
    会员级别所要的费用(单位:元)


    管理员ID
    User_ID
    Varchar(20)
    管理员登录用ID


    管理员密码
    Password(admin表)
    Varchar(20)
    管理员登录用密码



    1.3.2 数据结构
    数据结构名:读者证
    说明:定义了读者的信息
    组成

    reader_id, reader_name,sex,birthday,phone,mobile,card_name,card_id,level,day
    数据结构名:图书信息
    说明:定义了一本书的信息
    组成:

    book_id,book_name,author,publishing,category_id,price,date_in,quantity_in,quantity_out
    数据结构名:借阅信息
    说明:用户借阅书的信息
    组成:

    reader_id,book_id,date_borrow,date_return,loss
    数据结构名:会员等级
    说明:定义了会员的等级
    组成:

    level,days,numbers,fee

    2.概念设计2.1 系统ER图
    3.详细设计3.1关系模型
    会员级别:{会员级别,最长出借天数,最多借书书册,会费}
    读者:{读者编号,姓名,电话,手机,性别,办证日期,出生日期,证件名称,证件编号,会员级别,密码}
    借阅:{图书编号,读者编号,借阅日期,归还日期}
    图书:{图书编号,书名,作者,出版社,类别编号,单价,入库日期,库存数量,出借数量,遗失数量}
    读者管理:{管理员编号,读者编号,遗失日期}
    类别:{类别编号,类别名称}
    管理员:{管理员编号,密码}

    3.2 物理结构设计3.2.1 Readers表
    3.2.2 Books表
    3.2.3 Borrow表
    3.2.4 Member_level表
    3.2.5 Lost_card表
    3.2.6 Admin表
    4.系统实现4.1 程序框图4.1.1 总框图
    4.1.2 登录操作框图
    4.1.3 管理员操作框图


    4.1.4 读者操作框图
    4.2 运行界面4.2.1 登陆界面登录界面可以输入用户名和密码登录,两种不同用户组的登陆界面可以通过menu上的标签切换,在程序里是直接读取两个html文件(根目录下的member.html 和 administer.html)然后通过浏览器提出get请求完成操作。登录是提交get请求,登录信息在超链接里,后端分析登录信息做出判断。登录成功后会跳转相关界面,然后在根目录下产生一个user.txt,里面记录了登录用户的用户组和用户名,如a:root就代表管理员“root”、m:r001就代表读者“r001”。
    会员登录界面

    管理员登陆界面

    4.2.2 管理员所有操作都是通过get请求将信息返回给后端程序的。所有管理员的操作都要通过判断根目录下的登录信息文件user.txt里面的首字母来判断当前操作的用户是否拥有权限。
    图书信息表展示界面(外观模版为admin/adminBook.html)

    添加书籍界面(外观模版为admin/adminBookNew.html)

    修改书籍界面(外观模版为admin/adminBookEdit.html)

    读者信息表显示界面(外观模版为admin/adminReader.html)


    注册新读者界面(外观模版为admin/adminReaderNew.html)

    修改读者信息界面(外观模版为admin/adminReaderEdit.html)

    借书记录管理界面(外观模版为admin/adminRecord.html)

    4.2.3读者个人资料界面(外观模版为member/memberInfo.html)

    借书纪录界面(外观模版为member/memberHistory.html)

    搜索图书界面(外观模版为member/memberBorrow.html)

    4.2.4控制台开始界面

    Get请求path详情

    出错信息

    操作

    输出信息打印

    5.用户使用说明5.1 编程语言
    服务器: Python 2.7
    前端: HTML + JavaScript + CSS

    5.2 依赖库
    服务器库:BaseHTTPServer
    连接MySQL库:MySQLdb
    控制台设置库:Sys
    URL解析库:Urllib
    时间库:Time
    时间库:Datetime
    操作系统库:os

    5.3 编码方式
    前端网页:GBK
    服务器端:GBK
    数据库:UTF-8

    5.4 运行环境
    服务器:Windows7 SP1旗舰版 + Python2.7
    前端:Opera浏览器 34.0(Chrome内核)
    数据库:MySQL Server 5.5(默认端口3306)

    6.实验总结问题与解决6.1 实验总结在这个大作业实验中,我学会了很多东西,不仅更了解了数据库连接的方式和数据库的操作,还学会了使用Python搭建一个服务器和javascript语言。我也通过这次实验,深刻的考虑到了用户体验的需求,我们开发软件不仅要注重程序的完整性,更要关注界面以及操作对于用户的体验。一个好的程序必须是良好的用户体验加上完整的系统,缺一不可。我也通过这次实验了解到了web开发中前端和后端是如何交互的,比如说get请求的处理、post请求的处理,还有表单的递交、url的解析、编码的统一等等。我还学会了使用触发器来控制数据库的操作,以防非法操作,这比在程序中来判断更具有完整性,因为在程序中很有可能没有想到,而建立了触发器就可以在数据库端就把非法信息隔绝。
    6.2 遇到的问题与解决6.2.1 服务器、前端、url、数据库的编码不统一怎么办?服务器使用的是Python 2.7,这个版本的Python使用的是全局Unicode编码,前端网页使用的是GB2312的中文编码,url使用的是UTF-8编码,数据库使用的是UTF-8编码。
    解决方法
    服务器在通过转换设置为全局GBK编码,因为GBK编码包括了GB2312编码的部分,然后对于所有接收到的信息都解码然后转换为GBK编码,在服务器端统一编码。
    6.2.2 自动分配ID的机制如何实现?我们在读者和图书的添加操作的时候,如果需要用户输入编号就不太人性化,但是自动生成有可能会在删除书籍的时候重复。
    解决方法
    计算最大的编号,在最大的编号加一即可。
    6.2.3 读者证挂失怎么实现?图书证挂失的过程中,因为设计的时候没有在readers表添加挂失标记,所以挂失操作的时候不能标记为挂失,也不能直接删除这条纪录。
    解决方法
    新建一张挂失表lost_card表,凡是挂失图书证的读者就将信息复制到这个表,然后删除readers表中的相关记录,在解除挂失的时候只要反过程操作即可实现。
    1 评论 5 下载 2019-08-29 11:06:45 下载需要12点积分
  • 基于C#实现的TPS和B样条人脸变形系统

    1 需求分析人脸变形即,在引导图的面部 68 个关键点的引导下,将源图的面容进行扭曲变形,使得得到的图片的人脸的关键点特征与导引图的关键点特征相似。
    由于该变形过程无法用显式的数学公式进行表达,在变形上存在一定的难度。而我们的需求是将源图的关键点坐标的位置映射到导引图的关键点坐标的位置,同时也要将关键点坐标附近的像素坐标也映射过去。由于映射之后得到的坐标点不是整数,所以需要进行插值。为此,可以采用两个常见的变形函数,即 B 样条变形和 TPS 变形。
    在本报告中,规定源图为待变形图,导引图为将源图的关键点特征变换成的目标的图片,生成图即变形后的图片。如将特朗普的脸的特征换成梁静茹的儿子 Anderson 的脸,源图为特朗普,导引图为 Anderson。
    2 TPS 变形2.1 方案设计首先将导引图的关键点经平移、放大、旋转等操作进行仿射变换以变换到源图的关键点的位置,进行粗对齐。之后以导引图的关键点为控制点,源图的关键点为目标点,进行 TPS 求解即得到变形之后的图像。
    2.2 方案基本原理2.2.1 仿射变换参数拟合仿射变换是最常用的空间坐标变换之一,其形式如下。

    通过仿射变换拟合参数之后,坐标点进行了尺度、旋转、平移或偏移。对参数进行最小二乘法拟合,在进行仿射变换之后,将源图和导引图的人脸进行粗对齐,便于后续 TPS 操作。
    2.2.2 TPS 变形TPS(Thin plate spline) 是一种常见的插值模型,目标是寻找一个通过所有控制点的光滑曲面f(x, y),使得能量函数 If 最小,且该问题有解析解。

    TPS 求解,给定 n 个控制点 P1 = (x1, y1), …, Pn = (xn, yn),记矩阵 K,P,L 分别为

    假定目标点为 P′1 = (x′1, y′1), …, P′n = (x′n, y′n),记矩阵 V,Y 为

    其中 U(r) 为径向基函数,即 U(r) = r2log(r2) r ̸= 0,r 为坐标点之间的距离。任意一点的坐标为

    其中,a1, ax, ay, ω 为线性方程组 L[ω1, …, ωn, a1, ax, ay]T = Y 的解。
    2.2.3 具体实现控制点为导引图的 68 个关键点,目标点为源图的 68 个关键点,即 Pi 为导引图的关键点,P′i为源图的关键点,注意导引图的关键点要带入仿射变换之后的坐标。带入方程计算求解再进行插值即得到变形后的图片。

    3 B 样条变形3.1 方案设计首先将导引图的关键点经平移、放大、旋转等操作进行仿射变换以变换到源图的关键点位置以进行粗对齐。之后通过使用三次 B 样条扭曲进行人脸变形,即得到变换之后的结果。其中,网格点的移动受控制点影响,姑且将权重全部设为 1,之后再使用梯度下降方法以求得每一个网格点的位移权重,得到使得 Loss 较小的权重,用该组参数进行变形即得到目标图片。
    3.2 方案基本原理3.2.1 B 样条变形给定 m+n+1 个平面或空间 Pi(i = 0, 1, …, m + n),称 n 次参数曲线段:

    为第 k 段 n 次 B 样条曲线段 k = 0, 1, 2, …, m,这些曲线段的全体称为 n 次 B 样条曲线,其顶点Pi(i = 0, 1, …, n + m) 所组成的多边形称为 B 样条曲线的特征多边形。其中 Gi,n(t) 称为基函数。项目主要使用的是三次 B 样条曲线的基函数。
    三次 B 样条曲线基函数:

    其中,t ∈ [0, 1]。
    三次 B 样条扭曲 B 样条扭曲由网格点的位移影响其附近图像的变形。三次 B 样条扭曲中,任一点的位移由距其最近的 16 个网格点的位移决定。设图中某一点的的坐标为 (x, y),设网格大小为N,则有

    其中,坐标 (xi, yi) 为网格点 Pxi,xj 的坐标。根据这一原则将图片建立起网格点坐标系,则可以得到点 (x, y) 的位移为

    3.2.2 梯度下降法梯度下降的主要思想为:开始时随机选取一个参数的组合,即 (θ1, θ2, …, θn),计算代价函数,然后寻找下一个能让代价函数值下降最多的参数组合,不断迭代后得到一个局部最小值。
    这里为了得到较好的网格点位移权重,故采用梯度下降法。定义 Loss 函数为

    其中,m 为脸部关键点个数 (一般取 68),n 为网格点个数。s(·) 表示的是点的位移,θ 表示的是网格点位移权重。设学习率为 α,每次迭代进行参数的同步更新,即

    当迭代到一定次数或 Loss 函数的值小于某个阈值的时候梯度下降停止。
    要注意学习率 α 的选取:如果 α 太大,则梯度下降法可能会越过最低点,甚至可能无法收敛,即移动步数很大导致每次更新都越过最低点,离最低点越来越远;如果 α 太小,梯度下降可能会很慢。这里默认学习率为 0.001,且为了避免无法收敛的情况,在具有发散的趋势时,将当前学习率减半,以保证最终代价函数能够收敛。
    3.2.3 具体实现先定义三类点,即控制点、网格点、任一点。控制点即为脸部的 68 个关键点,网格点定义见上文,任一点即变换后的图像上的每一个点。现在所需要做的,首先建立好网格坐标,之后根据控制点的位移计算每个控制点周围的 16 个点的位移偏差。即遍历 68 个关键点,周围的每个网格点以 1 的权重进行分别求和。由于权重为 1,故每个网格点的位移的权重需要进行调整,故对所有网格点设一个位移权重,采用梯度下降法进行求解。设 X,Y 方向的学习率均为 0.001,初始化权重均为 0.05。

    4 68个关键点检测使用 C# 的 Dlib 库运用机器学习进行关键点检测1。在加载已经训练好的模型后,检测即可得到 68 个关键点。通过与附件中给定的 9 张图的关键点坐标相对比,相差较小。
    5 插值原理5.1 最近邻插值将变换后的图像中的元像素点最近邻像素的灰度值赋给原像素点的方法,是最简单的灰度值插值,即将非整点的坐标四舍五入,不够精确。
    5.2 双线性插值图像中任意一点的像素值由它周围四个点的像素值确定,具体关系为

    双线性插值相对简便,速度相对较快。但是双线性灰度插值的平滑作用可能使得图像的细节产生退化,这种现象在进行图像放大时尤其明显。
    5.3 双三次插值图像中任意一点的像素由它周围 16 个点的像素确定,具体关系为

    其中,矩阵 A、B、C 分别为

    S(·) 为

    双三次插值通常能够产生较好的效果,最精确的插补图形,能够创造出比双线性插值更平滑的图像边缘。但是它的速度几乎是最慢的。
    6 实现效果6.1 运行界面用户可以根据需要对变形方式进行选择 (TPS/BSpline),对三种插值方式进行选择。同时对于关键点信息的载入,用户可以选择读入外部数据或者选择自动识别。同时,为了防止用户操作上的失误,设置只有在用户载入图片之后才能上载关键点信息,二者均完成后才能开始进行变形。

    6.2 不同变形模式下的运行效果6.2.1 TPS 变形上图即为 TPS 变形的效果。由于 TPS 的原理,故在变形之后会出现没有信息的部分,对于该部分的处理为忽略。之前考虑用最边缘的像素值进行 padding,但是由于本身就缺乏该部分信息,进行 padding 之后图像会变得很奇怪。
    6.2.2 B 样条变形在进行 20 次梯度下降之后,得到变形后的图片。由于 B 样条只对局部进行处理,所以背景上没有缺失信息的部分。如这张图,奥巴马的眼睛变大。

    6.3 不同插值方式下的变形效果以奥巴马的脸特征换成 Anderson 为例,使用 TPS 变形方式。大图见 figure 文件夹。由于看小图无法感受到插值效果的不同,故将图片放大 5 倍对比奥巴马的右眼。
    6.3.1 最近邻插值
    6.3.2 双线性插值
    6.3.3 双三次插值
    6.3.4 三种插值方式效果对比由对比图可以看到,最近邻插值的效果最差,双线性和双三次插值的效果要好于最近邻插值。除此之外,双三次插值的图像边缘要比双线性平滑,即双线性插值可能会对图像产生稍微模糊的效果。

    6.4 自行检测关键点并进行变形在实现自行检测 68 个关键点之后,可以对想要被变形的图片进行变形。下图尝试“王校长”的变形,68 个关键点等在检测结束后自行显示在图片上。在识别关键点之后,可以得到变形后的图片。
    可以看到,变形后的“王校长”更符合图片的配字。

    识别算法可以识别这种通过人脸制作的表情包。

    7 误差分析7.1 观测误差与舍入误差对于观测误差,由于图片的 RGB 值是 uint8 类型,所以观测误差最大为 0.5。
    工程中多出使用 C# 的 double 类型,精度为 15 位有效数字,故中间过程的舍入误差相对较小,可以忽略。而在对 Bitmap 的 RGB 赋值时,将 double 类型转换为 0 255 的 uint8,舍入误差最大为 0.5。而对于这三种插值,其实质为周围 4 个或 16 个点的线性组合,且线性组合的系数为 1。
    综上,观测误差的最大值为 0.5,舍入误差的最大值为 0.5。
    7.2 方法误差 (插值)7.2.1 最近邻插值由坐标的舍入误差可以得到,|∆x| ≤ 0.5, |∆y| ≤ 0.5,所以最近邻的方法误差为

    由于数字图像为离散信号,故将微分换位差分。假定,f(x, y) 在 x 方向和 y 方向上的一阶导数存在且有界为 Mx1, My1。则显然,在物体边缘等变化较大的区域,Mx1, My1 比较大,其他区域非常小。所以,x 方向和 y 方向的方法误差最大为 0.5Mx1, 0.5My1,总方法误差为 0.5(Mx1 + My1)。
    7.2.2 双线性插值假定,f(x, y) 在 x 方向和 y 方向上的二阶导数存在且有界 Mx2, My2。先考虑 x 分量的插值,即考虑 y = j 与 y = j + 1 方向的插值。每个直线方向上的插值为一维插值,故可以得到每条直线的插值误差为

    再将得到的插值后的两点进行 y 分量的插值,即考虑 x = x0 方向的插值,且同为一维插值,故可以得到插值误差为

    综上,总的方法误差为

    7.2.3 双三次插值假定,f(x, y) 在 x 方向和 y 方向上的四阶导数存在且有界 Mx4, My4。若直接按照分析双线性插值对双三次插值误差进行分析,则很容易得到总的方法误差为

    考虑双三次插值的另一种形式。具体如下

    其中,参数矩阵可以表示为

    其中,矩阵 P 和矩阵 F 分别为

    记 Px, Py 矩阵如下

    则有

    根据这个形式可以得知,双三次插值即可以等价位,先做 y 方向的三次埃尔米特插值,再做 x 方向的三次埃尔米特插值,即与双线性插值分解方法类似。则误差为

    而在求取导数的过程中同样存在着误差。由于离散情况下,需要使用差分来代替导数,考虑使用中心差分来近似一阶导数。具体形式如下,这里 h 取 1。

    在这里,有

    不妨先考虑 x 方向,则易知存在 x∗ ∈ [x − 1, x + 1] 使得 fx(x) = f′(x∗)。假定 f(x, y) 的二阶导数存在且有界 Mx2, My2。所以有

    y 方向同理。所以由于使用差分来代替导数造成的误差为 Mx2 + My2。双三次插值总的方法误差为二者相同时考虑。
    7.3 方法误差 (变形)7.3.1 最小二乘拟合最小二乘参数拟合使用了高斯消元法进行求解,具体分析见下。
    7.3.2 TPS 变形高斯消元法 线性方程组 L[ω1, …, ωn, a1, ax, ay]T = Y 求解时应用了高斯消元法。由定理,若 A 是非奇异阵,Ax = b ̸= 0,且 A(x + δx) = b + δb,则有

    高斯消元误差的上界即为 ∥A−1∥ ∥A∥, 经过 Matlab 辅助计算,所给定的 9 份数据点 (最小二乘法拟合后) 的所构成的矩阵 L 不是病态矩阵。但是由于水平有限,无法进行更详细的分析。
    除此之外,舍入误差与观测误差没有被放大,几乎无影响。
    7.3.3 B 样条变形梯度下降法 迭代次数限制在 20 次时,根据当前实现的算法,代价函数 (均方误差) 的区间为(0,40)。
    而当参数最佳时,均方误差为 E(E ≥ 0),故梯度下降法的均方误差为 (0, 40)。
    8 其他相关说明8.1 TPS 与 B 样条的对比通过视觉效果来看,TPS 是针对全局进行变形,而 B 样条是对局部进行变形,所以论全局观赏效果的话,B 样条占优势。但是显而易见 TPS 的变形效果要比 B 样条更明显。除此之外,B 样条的计算时间要比 TPS 少一些。
    8.2 问题与解决8.2.1 关于线性方程组求解的误差分析由于自身能力有限,关于高斯消元法部分的误差分析在稍微自学课本后面的内容之后,仍是无法进行更具体的分析。我感觉可能无法去分析得更具体,即若分析误差上界,可能实际值要远小于该误差上界,故或许需要一个条件更强的分析高斯消元误差的方法。
    8.2.2 关于 LOSS 函数的选取LOSS 函数的选取本着网格点与移动点的位移关系设计,但事实证明若 LOSS 函数趋于 0 所得到的变形并不是最优的,猜测设计的 LOSS 函数的条件不够强。为了解决这种情况,将迭代次数固定。
    8.2.3 关于图片中有多个人68 个关键点检测算法是先对人脸进行识别,再载入模型进行检测关键点。如果图片中有多个人,则默认人脸为所检测的最后一个人脸。

    8.2.4 关于侧脸和歪脸的处理变换最好的效果就是正脸源图和正脸导引图。如果有歪脸,即头正,则通过仿射变换最小二乘,已经进行旋转,故效果较好。但是对于侧脸,比如梁静茹和特朗普的另一张照片,变换之后的图像为了模仿导引图的特征,脸会歪,但是这是正确的输出结果。同时由于没有露出的脸部的信息未知,所以无法对未知的信息进行填补。
    8.3 程序运行所需环境程序使用 VS2017 进行开发,使用的语言为 C#,电脑分辨率为 1366×768。
    8.4 总结与反思本次大作业,让我慢慢地了解并应用了 C# 语言,并让我体会到了它的优点所在。除此之外,对插值的理解更加深刻。而对于图像的变形上,我感受到了 TPS 变形与 B 样条变形之间的差异。TPS相对来讲实现简单,但是对全局进行操作导致背景出现黑色部分;而 B 样条可以对局部进行调整,但是调整效果并没有 TPS 那么明显,各有利弊。以及由于还有其他几个大作业,关于 B 样条还有一些升级的思路,但是由于时间关系没有付诸实践。除此之外,由于时间关系,没有对界面进行美化,保持着原始的朴素。
    此外,在逐渐完成大作业的过程中,我发现我之前将换脸和脸部变形的概念混淆。本次大作业完成的只是根据导引图关键点特征对源图进行变形,生成图片的像素值采自源图,所以无法实现换脸的效果。
    2 评论 26 下载 2019-04-17 14:49:59 下载需要15点积分
  • 基于C语言实现的2048小游戏

    一、主要函数实现根据题目要求需要实现以下几个函数:

    moveLeft
    moveRight
    moveDown
    moveUp
    gameOver
    boardCo
    ntains2048
    printBoard
    readBoard

    题目中表明有些函数体是不能进行修改的,只能添加自己的函数在固定的位置,以及添加所需要的#include,可以改变某些函数体的 return 语句。
    其中 boardContains2048, printBoard, readBoard 三个函数比较简单,只是二维数组的读入,输出,以及判断二维数组中是否包含 2048;
    需要注意的是:为了保证 printBoard 函数打印出来的效果,需要提前预估各个符号所占的位数和所在的位置。
    printBoard();详见代码,具体需要注意的就是外面那个框是如何保证输出的。
    void printBoard(int board[SIZE][SIZE]) { int i,j; printf("+"); for(j=0;j<SIZE;j++) { printf("-----"); } printf("+\n"); for(i=0;i<SIZE;i++) { printf("|"); for(j=0;j<SIZE;j++) { if(board[i][j]==0) { char a='.'; printf("%5c",a); }
    二、moveLeft()函数接下来是比较重要的四个移动函数:上下左右,由于其实现逻辑想通,现在只列出向左移动函数的代码进行解释:

    可以发现,向左滑动时,同行相同数字合并,并在边界为没有数字的地方随机产生数字(这个函数即 InsertNewNumber(board)原代码中已经给出了,不需要我们实现)。
    现在我们需要考虑的就是以下问题:

    向左滑动时,从左侧起,如果有相同元素则合并
    需要注意数字 0,即图中的空背景,在滑动的时候是无视的,也就是数字不会被 0 间隔

    上面阐述的或许不是很清楚,需要你多玩玩 2048 本身,就明白我所说的意思了。
    所以在代码中我们需要考虑同一行从左到右的列中,两个相邻元素(被 0 间隔也相当于相邻元素)是否相同,相同就叠加并计入分数。
    具体分为:

    如果当前元素为 0,处理不为 0 的相邻元素,需要把它们移到左边
    如果当前元素不为 0,处理不为 0 的相邻元素,是否需要叠加

    代码如下:
    int moveLeft(int board[SIZE][SIZE]) { int i,j,score=0,flag=-1; for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { int cell=board[i][j];//cell单词用的不太恰当,表示当前元素,你可以采用更有意义的命名 if(cell!=0) { int k=j+1; while(k<SIZE) { int nextcell=board[i][k]; if(nextcell!=0) { if(cell==nextcell) { flag=0;//相邻两个元素相同,就说明能移动,所以改变flag的值 board[i][j]+=board[i][k]; score+=board[i][j]; board[i][k]=0; }
    向上、右、下移动情况类似,只需要注意出发点所在的位置即可,不具体描述。
    三、gameOver()函数起初想的比较简单,结果这个函数产生了大量的 bug,主要原因是:
    咱们只需要判断当前条件下还能否上下左右移动,如果不能即 gameOver();所以我想当然的认为只需要在 gameOver()函数中添加判断语句判断几个移动函数返回值是否都为-1 即可;
    但问题在于,当执行 if 语句的时候,它执行了判断()里面的 move 函数,也就是说表面上它只是进行了判断,但实际上它判断的同时,执行了四个移动函数,相当于把输入的 2048 整个已经上下左右移动了一遍。所以最后我依据写的move 函数,采用了全局变量用来标示状态的方法。
    int gameOver(int board[SIZE][SIZE]) { int copy_board[SIZE][SIZE],i,j; /*为了避免直接把board[][]传进move函数判断的时候改变board,所以把board复制给 另一个数组,然后判断,这样就不会改变board数组了 */ for(i=0;i<SIZE;i++) { for(j=0;j<SIZE;j++) { copy_board[i][j]=board[i][j]; } } if(moveDown(copy_board)==-1&&moveUp(copy_board)==-1&&moveLeft(copy_board)==-1&&moveRight(copy_board)==-1)//如果四个移动函数都返回-1即不能移动GameOver return 1; else return 0;}
    四、一些比较重要的说明由于每个人代码风格不一样,所以你可能看起来吃力一点,所以你最好提前多玩几遍游戏,然后在纸上面画图分析一下每个方向上移动的逻辑。然后尝试写代码,遇到想不通的可以查看代码。这样效果好点。
    进行分析的时候一定要分开各个方向进行单独分析,不要揉杂在一起想。
    从代码来看,看懂之后你可以改进以下几个地方:

    变量的命名(可以使用有意义的单词)
    关于 gameOver 函数的实现,你可以想一个更巧妙的算法。(我写的不太好,但依据我写的 move 函数,目前只能采取全局变量的方式)
    游戏逻辑基本都实现了,而且我测试了常见情况和几种极端情况,均没问题,但难免有些数据没经过测试,如果你发现了 bug 及时告诉我

    五、关于 diary.txt你可以按照每天写某一个函数,或者实现某一个移动功能,或者出现哪些 bug,修复 bug 这样的工作量来进行编写。
    六、windows 下和 Ubuntu 下运行图Windows 下的运行截图

    Ubuntu 下的运行截图

    1 评论 1 下载 2019-08-27 08:05:56 下载需要8点积分
  • 基于QT实现的简易计算器

    一、实验内容使用 Qt 设计并实现带用户界面的简易计算器。计算器支持运算的数据类型为整数和小数。基本功能和 Windows10 自带的简易计算器相似。支持基本的加、减、乘、除四则运算。还支持一些单目运算:开方,平方,取倒数和取相反数。另外,还有两个主要的控制功能:清除和退格。本次实验中,一些错误提示及单目运算的使用方法本人完全参照 Window10 自带的简易计算器。
    二、代码结构实现过程中没有添加新类,所有的功能都在 MainWindow 主类中实现。类中添加的属性和方法如表 1 所示:



    属性/方法
    功能、作用




    bool lastIsNum
    上一个输入是否为数字


    bool lastIsSinOpt
    上一个输入是否为单目操作符


    bool lastIsLBracket
    记录上一个输入是否为左括号


    bool expClean
    下一次输入是否需要清空 expression label


    bool inputClean
    下一次输入是否需要清空 input label


    int lBracketNum
    当前左括号数


    int index
    左括号和弹幕操作符的位置


    QStack operands
    操作数栈


    QStack operators
    操作符栈


    void inputNumver(int)
    输入数字,修改 input label


    void inputOperators(QString)
    输入操作符,修改 expression label


    void inputSingleOpt(QString)
    输入单目操作符,修改 expression label


    void numberPush()
    输入数字的运算操作(数字进栈)


    void operatorPush(char)
    输入操作符的运算操作


    void singalOptPush(char)
    输入弹幕操作符的运算操作


    void optPriority(char)
    运算符优先级


    double calculateExp(double,double,char)
    双目运算计算



    三、算法介绍本实验使用的算法为经典的带括号表达式求值算法,算法流程如下:

    首先初始化两个栈,一个 double 类型的操作数栈(operands),一个 char类型的操作符栈(operators)。并且为了简化之后的实现过程,将’#’压入操作符栈中
    初始化操作符的优先级




    操作符
    优先级




    #
    -1


    (
    0


    + / -
    1


    × / ÷
    2




    若 读 取 到 一 个 数 值 num , 则 直 接 将 其 压 入 操 作 数 栈 中 。operands.push(num)
    若读取到一个操作符,则分为三种情况:

    若是双目操作符(加、减、乘、除),则与操作符栈顶操作符 opt 的优先级进行比较,若 optPriority(opt) > operators.top(),则直接将该操作符压入操作符栈,否则弹出栈顶操作符,并且从操作数栈中弹出两个操作数 num2 和num1,进行计算 calculateExp(num1,num2,opt)。将计算结果重新压入操作数栈中。循环该步骤,直到 optPriority(opt) > operators.top(),将 opt 压入操作符栈中若是左括号,则直接压入操作符栈中。若是右括号,则不断弹出操作符栈中的操作符,与两个操作数栈中的操作数,进行运算,直到弹出的操作符为左括号为止。并将运算结果压入操作数栈若是单目运算,则其优先级一定是最高的,所以直接弹出操作数栈的栈顶操作数 num,进行运算 opt(num)。并将运算结果压入操作数栈中
    当所有的操作数和操作符读取完毕后,若操作符栈顶元素不为’#’,则弹出操作符栈栈顶操作符 opt,并从操作数栈中弹出两个操作数 num2 和 num1,进行计算 calculateExp(num1,num2,opt)并将结果压入操作符栈。循环该步骤直到操作符栈顶元素为’#’,则此时操作数栈中的数即为表达式运算结果

    四、UML 图如图 4-1 为计算器用例图,为用户提供的功能主要有:清除(进行下一次运算),退格(当前输入回退一格),操作数输入(输入小数,整数),双目运算(加、减、乘、除),单目运算(平方,开方,取倒,取相反数)。

    类图如图 4-2 所示,只有一个 QMainWindow 类,所有功能都在其中实现。

    五、信号和槽5.1 信号与槽函数的连接数字和小数点
    connect(ui.zeroButton, SIGNAL(clicked()), this, SLOT(numberZero()));connect(ui.oneButton, SIGNAL(clicked()), this, SLOT(numberOne()));connect(ui.twoButton, SIGNAL(clicked()), this, SLOT(numberTwo()));connect(ui.threeButton, SIGNAL(clicked()), this, SLOT(numberThree()));connect(ui.fourButton, SIGNAL(clicked()), this, SLOT(numberFour()));connect(ui.fiveButton, SIGNAL(clicked()), this, SLOT(numberFive()));connect(ui.sixButton, SIGNAL(clicked()), this, SLOT(numberSix()));connect(ui.sevenButton, SIGNAL(clicked()), this, SLOT(numberSeven()));connect(ui.eightButton, SIGNAL(clicked()), this, SLOT(numberEight()));connect(ui.nineButton, SIGNAL(clicked()), this, SLOT(numberNine()));connect(ui.dotButton, SIGNAL(clicked()), this, SLOT(dot()));
    将数字和小数点 button 的 clicked()信号与其对应的槽函数连接,若释放clicked()信号,则触发相应的槽函数。其槽函数都基本相同。
    双目操作符
    connect(ui.plusButton, SIGNAL(clicked()), this, SLOT(plus()));connect(ui.minButton, SIGNAL(clicked()), this, SLOT(min()));connect(ui.mulButton, SIGNAL(clicked()), this, SLOT(multi()));connect(ui.divideButton, SIGNAL(clicked()), this, SLOT(divide()));connect(ui.equalButton, SIGNAL(clicked()), this, SLOT(dengyu()));
    单目操作符
    connect(ui.sqareButton, SIGNAL(clicked()), this, SLOT(sqareButton()));connect(ui.sqrtButton, SIGNAL(clicked()), this, SLOT(sqrtButton()));connect(ui.reciprocalButton, SIGNAL(clicked()), this, SLOT(reciprocal()));connect(ui.negateButton, SIGNAL(clicked()), this, SLOT(negate()));
    括号
    connect(ui.leftParentheseButton, SIGNAL(clicked()), this,SLOT(lbracket()));connect(ui.rightParentheseButton, SIGNAL(clicked()), this, SLOT(rbracket()));
    控制 button
    connect(ui.CButton, SIGNAL(clicked()), this, SLOT(clean()));connect(ui.backButton, SIGNAL(clicked()), this, SLOT(back()));
    5.2 槽函数执行功能


    槽函数
    执行功能




    void numberZero()
    调用 inputNumber(0),修改 inputLabel


    void numberOne()
    调用 inputNumber(1),修改 inputLabel


    void numberTwo()
    调用 inputNumber(2),修改 inputLabel


    void numberThree()
    调用 inputNumber(3),修改 inputLabel


    void numberFour()
    调用 inputNumber(4),修改 inputLabel


    void numberFive()
    调用 inputNumber(5),修改 inputLabel


    void numberSix()
    调用 inputNumber(6),修改 inputLabel


    void numberSeven()
    调用 inputNumber(7),修改 inputLabel


    void numberEight()
    调用 inputNumber(8),修改 inputLabel


    void numberNine()
    调用 inputNumber(9),修改 inputLabel


    void dot()
    修改 inputLabel,添加小数点


    void plus()
    修改 expressLabel,并进行运算


    void min()
    修改 expressLabel,并进行运算


    void multi()
    修改 expressLabel,并进行运算


    void divide()
    修改 expressLabel,并进行运算


    void dengyu()
    逐次弹出操作符栈和操作数栈进行运算,直到栈空


    void sqareButton()
    修改 expressLabel,调用运算函数,进行平方运算


    void sqrtButton()
    修改 expressLabel,调用运算函数,进行开方运算


    void reciprocal()
    修改 expressLabel,调用运算函数,进行倒数运算


    void negate()
    修改 expressLabel,调用运算函数,相反数运算


    void lbracket()
    修改 expressLabel,添加左括号


    void rbracket()
    修改 expresslabel,并调用运算函数运算


    void clean()
    清空所有的栈,初始化变量,初始化 label


    void back()
    修改 inputLabel,删除字符串结尾元素



    六、实验亮点本次实验中,自己考虑到了几乎所有可能导致程序奔溃的情况,使得计算器对各种错误情况都能做出正确“回应”。项目有比较强的鲁棒性,具体表现如下:
    6.1 除数为 0 的情况

    除数为 0 的情况实现比较简单,只需要在每次进行除法运算时候判断一下其除数是否为 0 即可。假如除数为 0,则将 inputLabel 设置为“The number can’t be divided by 0”,将 expressionLabel 设置为空。并且清空操作符栈和操作数栈,并且恢复全局变量的初始值。
    6.2 负数开方情况负数开方情况与除数为 0 情况类似,也比较简单,只需要在进行开放运算时候判断被开方数是否为 0 即可。若被开放数位 0,则将 inputLabel 设置位“Invalid input”,同时清空操作符栈和操作数栈,并且恢复全局变量的初始值。


    6.3 没有输入左括号,无法输入右括号如表 1 所示,定义主类的一个 lbracketNum 属性,并且将其初始值设置为 0。每当输入一个左括号时候,执行 lbracketNum++操作将其的值加 1。之后,当输入右括号时,判断 lbracketNum 的值,若 lbracketNum 的值大于等于 1,则可以输入右括号,反之,禁止输入右括号。若可以输入右括号,则修改 expressionLabel 的值,将其末尾添加右括号,并且执行 lbracketNum—,将 lbracketNum 的值减 1。代码如下:
    void Calculator::rbracket() { if ((lBracketNum) && (lastIsRBracket || lastIsNum ||lastIsSinOpt)) { inputOperators(")"); operatorPush(')'); }}
    由条件不难看出,当且仅当 lBracketNum 不为 0,并且上一个操作是右括号或者数字或者但目操作符时候,可以输入右括号。反之,点击右括号无效。
    6.4 输入一个数字后,直接输入左括号,形如:3此处我的处理方式与 Windows 自带计算器的处理方式是统一的。出入数字后,紧接着输入一个左括号,不会发生错误,而是当下次再输入一个操作符时候,该数字会进栈并显示在左括号后面。


    6.5 连续输入两个双目操作符,第二个操作符将无法输入见 表 1 , 主 类 中 有 三 个 属 性 与 此 有 关 : lastIsNum,lastIsSinOpt 和 lastIsRBracket。当上一个输入是一个数字时,lastIsNum 为 true,当上一个输入是单操作符时,lastIsOpt 为 true,当上一个输入是右括号时,lastIsBracket 为 true。
    分析不难得知,双目操作符仅可能出现在数字,单目操作符或者右括号后面。所以,实现代码如下:(以加法为例)
    void Calculator::plus() { if (lastIsNum || lastIsSinOpt || lastIsRBracket) { inputOperators("+"); operatorPush('+'); }}
    6.6 在一个数字中输入大于等于 1 个小数点,第二个小数点将无法输入在输入小数时,若输入了一个小数点后,还能再次输入小数点,例如产生类似 1.234.564 这样的数,这显然是不合法的。所以在输入一个小数点后,必须禁止小数点的输入。我采用的方法是,在每次输入小数点时,都获取 inputLabel 中的 QString,并利用 split 函数将其以“.”分割成数组保存在一个 QStringList 变量中,若该 list 长度不为 1,则说明已经存在一个小数点了,那么禁止小数点的输入,反之可以输入,代码实现如下:
    void Calculator::dot() { QString nu = ui.inputLabel->text(); QStringList list = nu.split('.'); if (list.size() == 1) { nu.append("."); ui.inputLabel->setText(nu); }}


    6.7 右括号存在时,直接输入左括号无法输入,形如:()同样地,如表 1 所示,主类中的 lastIsRBracket 属性,lastIsSinOpt 属性,lastIsLBracket 属性都与左括号的输入有关。当且仅当上一个输入不是右括号且不是但操作符或者上一个输入是左括号时候,可以输入左括号。实现代码如下:
    void Calculator::lbracket() { if ((!lastIsRBracket && !lastIsSinOpt || (lastIsLBracket)) { QString exp = ui.expressionLabel->text(); index = exp.length(); exp.append("("); ui.expressionLabel->setText(exp); operatorPush('('); } lBracketNum++;}
    七、总结讨论首先感谢老师的教导。毫不夸张地说,这已经是我大学以来第三次做带有可视化界面的计算器了。但是这是第一次用 Qt 做该实验。第一次使用的是VS 的 MFC,第二次使用的是 Java 做的,第三次则是本次。因此,在完成本次实验后,除了编码能力的提升外,我觉得于我而言更重要的是对于不同的语言,不同的开发框架的深入理解与体会。当时用 Java 做的计算器具有更多的功能,包括运算的历史记录,用户自定义变量的使用等等。其界面可以说完全是“画”出来的,没有 Qt 和 MFC 这种便捷式的“拖动”控件形成界面。而 Qt 和MFC 之间,MFC 的优势在于不需要进行信号与槽函数的连接,自己设计好控件后,双击控件即可自动生成与之对应的代码函数,不需要做更多的声明。Qt 的优势在于,代码逻辑和结构更加清楚,非常符合传统的 C++项目的结构,是经典的面向对象编程结构。MFC 代码更多的是看起来“一团糟”,于我这种新入门的人而言,刚刚接触 MFC 时候是很痛苦的。
    另外,不足的是,由于自己在法国,所以耽误了一些课程,导致对于信号与槽的掌握不够熟悉,从而多定义了很多冗余的槽函数,本来可以使代码更简洁。项目完成后才发现这一点,于我而言也是一个经验和教训。
    1 评论 5 下载 2019-08-25 08:49:59 下载需要8点积分
  • 基于QT实现的图的遍历演示

    1 问题分析和任务定义1.1 问题描述很多涉及图上操作的算法都是以图的遍历操作为基础的。试写一个程序,演示在连通的无向图上访问全部结点的操作。
    1.2 基本要求以邻接多重表为存储结构,实现连通无向图的深度优先和广度优先遍历。以用户指定的结点为起点,分别输出每种遍历下的结点访问序列和相应生成树的边集。
    1.3 测试数据任选国内城市,起点为合肥,暂时忽略里程。
    1.4 问题分析及任务定义此程序需要完成以下操作:使用文件读取数据,以邻接多重表为存储结构,构成一个连通的图。用户输入一个起始点,从起始点开始对图分别进行深度优先和广度优先遍历。分别输出每种遍历下的结点访问序列和相应的生产树的边集。
    程序的执行流程如下图所示。

    1.5 实现本程序需要解决的问题
    如何利用从文件读取的数据构成无向图
    如何实现从用户指定的点开始遍历
    遍历完后如何输出遍历过得点的序列和边集
    如何利用图形化动态的演示遍历过程
    如何提高用户体验

    本程序的难点在于如何创建邻接多重表,和如何遍历所有城市。
    设图的结点20-30个,每个结点用一个编号表示(如果一个图有n个结点,则它们的编号分别为1,2,…,n)。通过输入图的全部边(存于数据文件中,从文件读写)输入一个图,每个边为一个数对,可以对边的输入顺序作出某种限制。注意,生成树的边是有向边,端点顺序不能颠倒。
    2 数据结构的选择和概要设计城市与城市之间是没有方向的,构成的无向图采用邻接多重表来实现,主要表示无向图的的各个边和结点,并通过对邻接多重表的操作实现对无向图的深度优先和广度优先遍历。创建AMLGraph类,其包含的属性和方法如下图所示。

    其中adjMulList是顶点数组指针,所用的顶点结点以顺序方式存储在一维数组中;vexnum和edgenum是无向图的结点个数和边个数;cityMap类是用来存储所有从文件中读取的城市名和坐标信息的类,points、dfs_str、bfs_str是用来存储遍历的城市的坐标和名称的变量。
    GreatAMLGraph函数用来构建邻接多重表,LocationVex函数通过城市名找到该城市在表结点的位置。AMLGraph、~AMLGraph函数分别是类的构造函数和析构函数,BFS_Traverse、DFS_Traverse分别是广度优先和深度优先遍历的函数。
    VexNode结构体是点结点(城市结点),name是城市的名称,firstEdge是EdgeNode类型的指针,指向该顶点的第一条边的结点。
    EdgeNode结构体是边结点,isVisited为标志域,用来标记该条边是否已被访问过;ivex为顶点域,指示依附于这条边的一个顶点的位置(序号);jvex指示另一个顶点的位置;ilink为链域,指向依附于第一条边的结点。
    3 详细设计和编码3.1 邻接多重表的建立
    从文件中读取所有城市名、坐标和线路信息。利用头插法建立邻接多重表。
    for (int i = 0; i < edgenum; ++i){ EdgeNode * e = new EdgeNode; e->isVisited = false; string v1, v2; inFile >> v1 >> v2; int a = LocationVex(v1); int b = LocationVex(v2); e->ivex = a; e->jvex = b; e->ilink = adjMulList[a].firstEdge; e->jlink = adjMulList[b].firstEdge; adjMulList[a].firstEdge = e; adjMulList[b].firstEdge = e;}
    3.2 深度优先遍历先调用DFS_Traverse函数,初始化visited[i]数组,再调用DFS(i),其中i为用户指定城市的索引。在DFS函数中,先访问表结点中第i个点,在判断该顶点的另一端是否被访问,如果没有访问,就在调用DFS(i->jvex),形成递归,直到访问到该顶点的另一端被访问过后,在访问与i相连的其它边,当与i相连的所有边都被访问完时,结束循环。
    while (e){ if (e->ivex == i) { if (!visited[e->jvex]) DFS(e->jvex); e = e->ilink; } else { if (!visited[e->ivex]) DFS(e->ivex); e = e->jlink; }}
    e为EdgeNode类型的指针变量。时间复杂度为O(n+edgeNum)。
    3.3 广度优先遍历先调用BFS_Traverse函数,初始化visited[i]数组,再调用BFS(i),其中i为用户指定城市的索引。BFS函数中,先访问表结点中第i个点,并将该点放入队列q中,在循环中不断取出队列的第一个元素,直到队列为空时,所以点遍历完成,结束该循环。取出队列第一个元素后,再判断该顶点的另一端是否被访问,如果没有访问,就遍历该点,并将该点放入队列,当队列第一个元素的所有相连的点都被访问过后,循环结束,再从队列中取出第一个元素,进行循环遍历。
    while (!q.empty()){ p = q.front(); q.pop(); EdgeNode * e = adjMulList[p].firstEdge; while(e) { if (e->ivex == p) { if (!visited[e->jvex]) { visited[e->jvex] = 1; //std::cout << adjMulList[e->jvex].name + " "; bfs_str.append(adjMulList[e->jvex].name.substr(0, 2) + "->"); q.push(e->jvex); } e = e->ilink; } else if (e->jvex == p) { if (!visited[e->ivex]) { visited[e->ivex] = 1; bfs_str.append(adjMulList[e->ivex].name.substr(0, 2) + "->"); q.push(e->ivex); } e = e->jlink; } }}
    分析上面算法,每个顶点至多进一次队列,遍历过程实质上仍是寻找每个顶点的邻接点的过程,因此时间复杂度和深度优先搜素一样。
    4 上机调试过程调试过程中,一方面是解决产生的bug,另一个面是调整文件中的数据,保证数据的合理性。
    点的名称及坐标

    无向图的边

    5 测试结果及其分析程序运行界面

    遍历演示


    本程序以图形化的形式演示,用户能够任意指定起始点开始遍历,遍历的结果也清晰可见。其中显示了两种遍历的城市编号的序列和遍历的边集。
    6 用户使用说明本程序是在windows7下的Qt creator5.4.1环境下编写的,因为Qt本身就是跨平台图形界面软件,所以本程序支持跨平台运行,支持的平台包括Windows、Mac OS、Linux等桌面操作系统。
    执行本程序后,窗口上回现实所有城市的名称、编号和路线,输入框中默认填写的合肥的编号,该输入框支持模糊查询,可以输入城市名称、城市编号或编号加名称。点击按钮“演示”后,会以文本形式显示遍历城市的序列,以图形化的方式,显示遍历的城市之间的连线。并且可以反复搜索遍历,具有良好的用户体验。在该可执行文件目录下的map.txt是本程序的所依赖的数据来源,文件中的数据可供用户或开发者调整修改。
    参考文献[1] 王昆仑, 李红. 数据结构与算法. 北京: 中国铁道出版社, 2006年5月
    [2] Kay_Sprint. 邻接多重表存储无向图以及有关操作[EB/OL]. http://www.2cto.com/database/201111/110964.html, 2011-11-13/2017-2-16
    2 评论 28 下载 2018-10-31 11:48:00 下载需要9点积分
  • 基于JSP和MYSQL实现的图书馆管理系统

    一、概述基于Spring + Spring MVC + MyBatis的图书馆管理系统,使用Maven进行包管理。主要功能包括:图书查询、图书管理、图书编辑、读者管理、图书的借阅与归还以及借还日志记录等。
    二、环境配置2.1 开发环境
    Windows 10
    IntelliJ IDEA 2018.3

    2.2 运行配置
    首先安装Mysql5.7,设置用户名为root,密码为123456,并保证其在运行状态,并执行library.sql文件导入数据
    然后再配置Maven到环境变量中,在源代码目录下运行
    # mvn jetty:run
    使用浏览器访问 http://localhost:8080 即可进入系统

    三、概念设计用户分为两类:读者、图书馆管理员。图书馆管理员可以修改读者信息,修改书目信息,查看所有借还日志等;读者仅可以修改个人信息、借阅或归还书籍和查看自己的借还日志。


    四、数据库E-R图
    五、逻辑设计共有6个表:
    5.1 图书书目表book_info


    字段
    类型
    长度
    小数点
    NULL
    用途





    book_id
    bigint
    20
    0

    图书号



    name
    varchar
    20
    0

    书名



    author
    varchar
    15
    0

    作者



    publish
    varchar
    20
    0

    出版社



    ISBN
    varchar
    15
    0

    标准书号



    introduction
    text
    0
    0

    简介



    language
    varchar
    4
    0

    语言



    price
    decimal
    10
    2

    价格



    pub_date
    date
    0
    0

    出版时间



    class_id
    int
    11
    0

    分类号



    number
    int
    11
    0

    剩余数量



    5.2 数据库管理员表admin


    字段
    类型
    长度
    小数点
    NULL
    用途





    admin_id
    bigint
    20
    0

    账号



    password
    varchar
    15
    0

    密码



    username
    varchar
    15
    0

    用户名



    5.3 图书分类表class_info


    字段
    类型
    长度
    小数点
    NULL
    用途





    class_id
    int
    11
    0

    类别号



    class_name
    varchar
    15
    0

    类别名



    5.4 借阅信息表lend_list


    字段
    类型
    长度
    小数点
    NULL
    用途





    ser_num
    bigint
    20
    0

    流水号



    book_id
    bigint
    20
    0

    图书号



    reader_id
    bigint
    20
    0

    读者证号



    lend_date
    date
    0
    0

    借出日期



    back_date
    date
    0
    0

    归还日期



    5.5 借阅卡信息表reader_card


    字段
    类型
    长度
    小数点
    NULL
    用途





    reader_id
    bigint
    20
    0

    读者证号



    password
    varchar
    15
    0

    密码



    username
    varchar
    15
    0

    用户名



    5.6 读者信息表reader_info


    字段
    类型
    长度
    小数点
    NULL
    用途





    reader_id
    bigint
    20
    0

    读者证号



    name
    varchar
    10
    0

    姓名



    sex
    varchar
    2
    0

    性别



    birth
    date
    0
    0

    生日



    address
    varchar
    50
    0

    地址



    phone
    varchar
    15
    0

    电话



    六、功能展示6.1 首页登陆
    管理者账号:123456/123456
    读者账号:10000/123456


    6.2 管理员系统6.2.1 图书管理
    6.2.2 图书详情
    6.2.3 读者管理
    6.2.4 借还管理
    6.3 读者系统6.3.1 查看全部图书
    6.3.2 个人信息查看,可以修个个人信息
    6.3.3 个人借阅情况查看
    3 评论 30 下载 2019-05-09 11:00:42 下载需要13点积分
  • 基于QT实现的多人在线对战的五子棋游戏

    一、程序背景计算机网络的迅速发展,对游戏领域产生了巨大影响。尤其是随着信息时代的来临,人们越来越趋向于网络游戏来进行竞技,于是,各式各样的网络游戏应运而生,例如腾讯游戏,网易游戏。在这些平台上,五湖四海的玩家可以随意地竞争与合作,适当的娱乐可以很好地纾解生活压力,为家庭、事业都有巨大的积极作用。因此,开发和架构这种轻游戏平台符合时代发展的需要。
    二、设计原理及要求2.1 设计原理本系统主要由四个子模块组成:五子棋游戏、网络通信、用户系统、积分系统。
    2.2 设计要求
    采用C/S模式架构,能够同时支持至少40对玩家
    服务器端提供游戏大厅、游戏桌等
    对战平台提供的游戏:五子棋或其它等

    三、程序介绍此程序主要分为两个部分:服务端和客户端。
    游戏服务端用于存储和转发信息,收到用户的登录、准备、落子等主动信号,进行相关的数据运算,并发送回用户。
    游戏客户端可通过服务端的IP地址发送连接请求,然后登录到游戏大厅,没有账号则需要先注册。在服务端中会维护一个session列表,即在线用户名单,还有所有游戏桌状态数组,保存所有游戏桌的人数情况以及准备情况。有人进入游戏桌、开始准备游戏、开始游戏、落子、游戏结束、离开游戏桌等,服务端都会收到相应的数据,此时服务器作为一个存储器和转发站,保存所有用户的游戏状态,并转发消息给相关的用户。用户排名也会显示在对应的客户端上,可自行查看排行榜。本游戏人数理论上无上限,可修改宏定义的数值参数来设置人数上限。
    四、程序的功能描述4.1 开始会话启动客户端时,便会尝试连接至服务器。连接成功后会在服务器的会话列表中新增一个封装过的session类,这个session类保存会话用户的信息,包括用户ID、用户账号、用户排名、用户状态等。
    处理完成后向客户端发游戏大厅中所有桌子的游戏状态,将有人的桌子改变颜色。
    所有的传输的数据格式都是XML,以便于提取数据。服务端和客户端皆有特定的函数专门用来处理传输的XML数据,同时用递归的方式解决了Socket带来的数据缓冲区问题,即多条数据会合在一起发送给客户端,导致有一定的几率分析失败,产生逻辑上的错误。
    4.2 注册服务器收到用户的注册数据,便开始提取数据中的有用信息,包括用户名、密码,可注册的话则为其生成一个唯一的用户ID,确保此用户为本游戏平台中独一无二的玩家对象。接着向存储的数据中添加新用户的记录,初始化积分。客户端收到注册成功的信号,获取用户ID,开始正常游戏。注册后可通过点击游戏大厅的用户名按钮,来查看所有用户的数量(因为新账号的排名就是所有用户数量)。
    4.3 登录在客户端,用户输入其账号密码后,服务器收到用户的登录数据,提取中数据中的用户名和密码,和用户列表中的数据进行配对。如果失败则返回错误提示。如果匹配成功,则在session列表中逐一比较,避免同一用户多设备同时登录,扰乱游戏积分系统。登录后返回用户的ID、昵称、积分等个人信息。
    登录成功后可通过点击游戏大厅的用户名按钮,来查看自己所在的位置,以及所有用户的账号与排名。
    4.4 进入游戏桌这需要登录后才能进入游戏桌。每个游戏桌分为两个座位,分别为黑子、白子,先进来的玩家默认为黑棋,后进来的为白棋。玩家加入游戏桌后,对应位置上的区域将会出现头像,表示这个位置有人,状态文字也变成“未准备好”,出现“准备完毕”的按钮。
    大厅的桌子预览图列表中白色的座位图标会变成黑色,并且上面显示玩家昵称。
    4.5 离开游戏桌玩家离开桌子回到大厅时,游戏桌上的玩家头像消失,大厅中的游戏桌预览图列表的黑色座位变回白色,表示这个位置的玩家已经离开了,其他玩家随时可上去与其他玩家开始游戏。如果在激战中退出离开游戏桌,确认后即视为放弃比赛,主动认输,另一方无条件获胜。
    4.6 准备开始游戏刚开始加入游戏桌时皆为未准备状态,需要手动点击“准备完毕”表示自己已经准备好,随时可以开始游戏。双方玩家都能看到对方的准备状态,以便于自己是否确认开始进行对战。双方都准备完毕时,收到服务器发送的对战信号,将马上开始对战。
    4.7 开始游戏准备完毕时,客户端向服务端发送准备的信号,当同一桌双方玩家都准备完毕后,立即开始比赛。客户端发送start信号给游戏桌的双方客户端,客户端进行游戏初始化,包括棋盘数据清空、棋盘控件清空棋子和更新界面、设置黑子先手、五子棋AI初始化、确定各自棋子的颜色,开始进行五子棋对战。
    4.8 落子游戏双方的每一次落子都会向服务器发送落子的信号,传输的数据中同时含有自身的信息,比如桌子ID、座位ID等,避免在数据传输的过程中丢包而导致的一些逻辑问题。服务端判断收到的用户座位是不是与保存的用户状态的列表中的数据一致,如果不一致则是有错误,需要进行纠错;如果没有错误就转发落子的数据给另一方的客户端,在客户端中模拟玩家进行落子。遇到错误后服务端会进行处理,并返回尽量减少错误损失的数据,交给客户端处理。
    4.9 放弃游戏在对战中关闭棋盘窗口,则视为放弃本次比赛,对方无条件获胜。退出的一方将会警告扣除积分,但并不会真的扣除。胜利方也不会获得奖励,避免刷分操作。离开后失败方的头像将会消失,原来的头像下方的文字变为“对方已放弃”,并且字体变灰。
    判断双方处在对战中的逻辑是同一游戏桌的两名玩家都已经准备完毕,服务端中有一个维护所有桌子座位状态的数组,0为没有玩家, 1为有玩家但没有准备好,2为有玩家并且准备完毕。如果双方都已经准备,则会立即开始比赛,所以两边都准备的情况下必定是正在比赛。
    4.10 游戏胜利客户端落子时发送数据给服务端,接着调用五子棋的AI判断自己是不是胜利。如果是胜利,则发送胜利的信号给服务端,在服务端中分析对战的双方局势,判断是不是真的胜利。最后一颗棋子也会发送给失败方,以便于两边的棋谱同步。确定数据无误后在服务端通过落子数量获得胜利者应该获得的积分,转发给胜利的玩家客户端。失败者也会获得少量的积分奖励,积分约为胜利者的三分之一。游戏结束后双方的准备状态清空,胜利方状态变为金色,失败方状态变为灰色,同时需要重新点击“继续下一局”按钮来和对方继续下一句对战。
    4.11 积分奖励每胜利一局,都会获得大量的积分,计入自己的总积分。服务端会进行累加,并保存到用户数据中。失败者获得少量的积分,也会累加到用户数据中,并保存到文件,实时保存积分以便于下次启动服务端时重新读取数据,同时也是避免了数据丢失。
    4.12 用户排名在服务端中维护了一个用户列表,是从数据文件中读取的。每一个用户列表中都有封装好的用户信息,其中就包括了用户的积分。其中每一个积分都对应了一个用户ID,解决了修改用户昵称和密码后引起的后患。通过QT的qSort函数来对用户信息按照积分进行排序,通过简单的算法添加了对积分相同的用户的排名保护。遍历所有用户的排名、昵称、积分,用一个QString来保存XML格式的数据,然后传输到对应的客户端。接着在客户端使用自己编写的字符串提取函数提取出所有用户的排名、昵称、积分,使用制表符显示在一个小型对话框里面。客户端用户也可以在排行榜中看到自己所处的位置。
    五、系统的ER图下图是简易的ER图大纲:

    下图是完整的ER图以及各个子模块之间的联系:

    六、程序详细设计及主界面呈现6.1 游戏大厅界面启动程序后就建立了与服务端的连接,开启一个session,每个客户端被赋予一个独一无二的sessionID。服务器维护一个经过封装的会话list,通过sessionID来判断是哪个客户端。封装后的sessionInfo类,包含了sessionID、用户昵称、用户ID、游戏桌位置等,并且在和客户端进行通讯时判断用户的位置是否正确,如果有误则进行纠错处理,将系统损失降低到最小。
    启动时进入了游戏大厅,此时会显示所有游戏桌的状态。在客户端与服务端连接成功后,服务端就向客户端发送了特定的数据包。这段数据中涵盖了当前所有游戏桌的人数状态,采用简短的XML格式,包括每个游戏桌座位上的用户昵称、ID、准备状态等。

    如上图,每个游戏桌可分为桌子、两边的座位。每个座位默认是白色,表示没有人在这里。一旦有人进来,服务端会发送所有游戏桌的数据给所有在线的用户,客户端SocketUtil类收到数据,通过判断里面的kind关键字来分发数据给对应的窗口对象,游戏大厅窗口收到数据进行处理,座位变成黑色,表示已经有人了,同时在座位的上方打印玩家的昵称,用来辨认。上图中的第二张游戏桌的左边位置已经变黑,座位上面还显示了玩家的昵称helloLisa。
    如果没有登录,左上角只有一个“未登录”按钮,再进行任何操作例如点击登录按钮、点击游戏桌等,都会强制要求跳转到注册/登录界面,登录成功后才能继续操作。登录后的样子如下图所示。

    原先的“未登录”按钮变成了用户的昵称,而后面多了两条信息,分别表示用户的当前积分、个人排名。积分是在游戏后才能获得,而排名则是代表当前用户在所有用户之中的排行。相同积分的用户排名相同,而不是简单的按照名称排序。点击用户名按钮,可以看到所有用户的积分排名。

    点击用户名按钮,客户端发送查看排行榜的请求给服务端,服务端收到数据后访问所有用户,进行排序,并且将客户端需要的数据发送给客户端,包括排名、昵称、积分等,同样是封装成XML数据,客户端收到后弹出MessageBox对话框,读取所有用户排名,展示给用户看。
    点击任意一张桌子,都可以进入游戏等待界面。
    6.2 注册登录界面在游戏大厅中进行任何操作,如果没有登录的话,会强制要求跳转到注册登录界面。注册登录界面略显简陋,由两个垂直排列的对话框和两个水平排列的按钮组成,分别是账号、密码、注册、登录。

    注册时,如果用户昵称已经存在,则会提示注册失败,重新换个名字。因为用户名代表了这个用户的个性化,早点注册的用户有更多的可能获取到自己喜欢的用户名。

    登录时,如果用户已经在其他设备登录,则不允许再次登录。这是为了避免多人用同一个账号刷积分,保障积分系统的公平性与稳定性。

    如上图,一个客户端用helloLisa账号登录,而第二个客户端也尝试用同样的账号登录,这时就会返回登录错误,要求用户更换账号。
    注册成功后,通过MessageBox弹出“注册成功”的话语,并且提示用户牢记自己的账号和密码。

    登录成功后,通过MessageBox弹出“欢迎回来”字样的对话框,接着自动关闭登录窗口。

    登录后,游戏大厅会显示用户的信息,用户昵称、用户积分、用户排名。

    6.3 五子棋界面该界面由三部分横向的布局组成:左边是用户一的头像、状态、操作按钮,中间是五子棋棋盘的控件,右边是用户二的头像、状态、操作按钮。

    头像分为黑白两色,黑色表示该用户使用的是黑棋,白色则相反,代表白棋。头像下方的标签显示了用户的状态,刚进入游戏桌时默认为“未准备好”,而且状态标签下方的按钮会显示“准备完毕”。点击准备完毕后,“未准备好”标签变成醒目的红色,文字也变为“等待开始”。而原先的按钮文字变成“取消准备”。在游戏开始之前随时都可以取消准备,变成原先的状态,不会有任何影响。

    同一个游戏桌对面用户,不会显示准备按钮,因为无法干预对方的准备状态。而对面如果没有人,则不会显示头像,直至对手进入游戏桌,自动显示头像。


    当对手进入游戏桌后,黑色的“未准备好”字样变成了暗黄色的“未准备好”,用以提示己方用户已经有人进来了,改准备开始游戏了。等对方准备完毕,则立即开始游戏,不再给予等待时间。

    按照五子棋的规则,黑棋先下,即代表头像为黑色的用户一方先落子。用户可以用鼠标点击棋盘的任意一个位置,如果范围超出了棋盘或者已经落子,那么该次点击不会有任何效果,相等于没有进行操作。用户每下一个子,都会发送数据给服务端,由服务端进行保存状态和转发给对面的玩家。对面玩家在收到落子信号前不能进行任何的落子操作。

    对面玩家的客户端收到了都成服务器的<KIND>moves</KIND>文本信号,客户端在数据中提取出落子位置,游戏桌ID、用户座位、棋子横坐标、棋子纵坐标,然后由程序模拟对方用户进行自动操作。落完子后进入下一回合,刚落子的用户便无法操作,需要等待对方落子,继续由服务器进行转发落子信号,在自己的棋盘上落子。这样来回循环直至游戏结束。

    游戏胜利,由胜利的一方的客户端发送数据给服务端,然后服务端根据双方的状态判断用户端是否是真的胜利,若有错则进行纠错处理,发送逻辑出错的error信号。目前五子棋稳定性极高,还没有遇见过出错的情况。

    获胜的一方可以获得大量的积分奖励,游戏胜利后弹出对话框显示结果,并且显示奖励的积分数量。同时原先的头像下方的状态标签变为暗金色的“胜利”二字,对面失败者的状态标签则变成“失败”,并且是惨淡惨淡的灰色。

    失败者也会受到安慰的少量积分,大概是胜利者的三分之一不到。
    游戏结果出来之后,双方各自的操作按钮会继续显示,按钮文字变成“准备下一局”。点击此按钮则重新进入准备状态,红色的“等待开始”,可以进行下一局。
    此时如果有一方离开了游戏桌,那么其状态标签会改成灰色的“对方已离开”,头像也一起隐藏起来。

    除此之外,如果是在游戏途中离开,会询问“当前您正在对战,如果现在退出将会默认对方胜利,并且扣除自己的积分。是否继续?”。点Yes会退出游戏桌,惨淡收场。

    玩家中途退出后,剩下的一方将会立即获得胜利,但不会奖励积分。这是为了避免刷积分引起的操作。放弃掉的一方头像消失,状态变成“对手已放弃”。胜利者恢复到比赛开始的“未准备”状态,等待继续准备。

    6.4 服务端界面服务端主要用于数据存储和数据传输,无需手动干预,故界面仅由两部分组成,一是日志控件,二是调试控件。
    服务端在启动时会读取文件中存储的用户信息:用户ID、昵称、密码、排名,依次添加进一个封装后的UserInfo类,并且使用List构成一个用户数组,包含了所有的用户信息。打印用户数量至日志控件。

    在日志控件下面,是一排水平放置的调试功能的空间布局,分别是游戏桌ID、座位号、发送的数据文本。这片布局仅仅用来开发时进行调试,根据传输的数据和客户端的反应来判断逻辑有没有出错。目前已经完成了其调试的使命,暂时不需要理睬。
    在服务端界面中,可以看到所有传输的数据,例如下图新的客户端进入与登录的XML格式的数据。

    如上图,客户端启动后设置唯一的sessionID给客户端,并且保持连接。同时获取到所有游戏桌的玩家状态,用<T>标签包裹起来,返回给客户端,最后到客户端的游戏大厅中刷新,有人的座位变黑,并且显示相应位置的玩家昵称。
    在注册/登录后获取到账号密码,与用户列表中的信息进行匹配,如果登录成功就返回1,登录失败则返回0。图中的数据是登录成功,于是在<KIND>login</KIND>XML文本中标记了RESULT为1,并且添加上了存储在服务端的用户信息返回给客户端。客户端读取后显示在自己的界面之上展示给用户。

    上图是双方玩家准备完毕后的收发的数据,所有文本皆显示在上面。一个玩家准备,发送结果给游戏桌对面的玩家。如果双方都已经准备好了,则发送<KIND>start</KIND>内容给双方,开始游戏。每一个落子的信号也是封装在一个<KIND>moves</KIND>``文本之中,同时这串文本带有落子的位置,服务端收到落子信号后转发给游戏桌对面的玩家,对面玩家客户端收到信号后游戏界面存放五子棋数据的数据设置相应的位置为对方棋子,然后进入下一轮,即对方落子。接着对方也发送信号过来,继续互相传输落子数据,直至游戏结束。
    七、代码实现此处应该略过,毕竟是 gayhub,自己找源代码吧,注释很多的。
    八、总结总的来说,这次课程设计是做得最顺利的一次了……老师布置了题目之后,就在光棍节的那一天,那一个周末,我闲得无聊,就开始着手做课程设计了。既然都要做,那么就不如直接选个最有趣的吧,于是一眼就看中了棋牌类在线游戏平台。虽然觉得是最难的……但试试又无妨。
    以前已经做了好几次的五子棋,分别是安卓、C、Java,这次恰逢好机会就打算用C++做一个图形化的。而且刚好有空,便开始新建工程。
    虽然有些小意外,比如莫名失效的信号槽机制,我就调试了6个多小时……而且没有任何进展。不过这个应该不是我的锅吧。还有Session,怎么在刚开始连接的时候就知道是哪个用户,连接时好像并不能传送数据,总不能等到传送数据时再进行维护吧,于是想了一会儿,就干脆在新连接进入时增加了一个sessionID,还封装了一个SessionInfo类来进行维护会话列表。这些都很顺利,轻车熟路了。而且QT对Socket的封装真的特别特别友好,多线程什么的完全不需要我考虑,只需要把功能实现就行了。也不会产生阻塞、溢出什么的,实在是太省心了。
    然后一不小心,就差不多完成了。
    多人在线五子棋平台最麻烦的地方就是调试了,服务端只能一直写一直写,写好后再统一运行,出了错误也得重新打开两个客户端慢慢调试。因为debug机制只能调试一个客户端,干脆就直接在服务端输出调试的日志了,显示数据传输的所有内容,顺带还打印log。
    再加上了伪造发送数据的调试控件组,就是手动写数据发送给客户端,这样就能知道客户端的状况,以及跳过了数据发送出现的更加容易出现bug的隐患。这个调试的方法应该算是比较新颖的吧,而且确实非常好用呢。
    时间比较仓促,因为还有好几个项目要赶,就花了匆匆的两天时间大概24小时吧。除去无用的找框架bug的时间,粗略估计约15个小时。没有好好地修改界面,看起来界面会比较丑,但是无伤大雅,能看就好。等以后有空了再好好地修改界面,让它能够发布出去,给其他人尝尝鲜。
    虽然除了QT的socket用法以外,我并没有学到多少其他知识,但是也增加了一些开发经历,算是有不小的收获吧。
    九、服务器服务端主要用于数据存储和数据传输,无需手动干预,故界面仅由两部分组成,一是日志控件,二是调试控件。
    服务端在启动时会读取文件中存储的用户信息:用户ID、昵称、密码、排名,依次添加进一个封装后的UserInfo类,并且使用List构成一个用户数组,包含了所有的用户信息。打印用户数量至日志控件。

    在日志控件下面,是一排水平放置的调试功能的空间布局,分别是游戏桌ID、座位号、发送的数据文本。这片布局仅仅用来开发时进行调试,根据传输的数据和客户端的反应来判断逻辑有没有出错。目前已经完成了其调试的使命,暂时不需要理睬。
    在服务端界面中,可以看到所有传输的数据,例如下图新的客户端进入与登录的XML格式的数据。

    如上图,客户端启动后设置唯一的sessionID给客户端,并且保持连接。同时获取到所有游戏桌的玩家状态,用<T>标签包裹起来,返回给客户端,最后到客户端的游戏大厅中刷新,有人的座位变黑,并且显示相应位置的玩家昵称。
    在注册/登录后获取到账号密码,与用户列表中的信息进行匹配,如果登录成功就返回1,登录失败则返回0。图中的数据是登录成功,于是在<KIND>login</KIND>XML文本中标记了RESULT为1,并且添加上了存储在服务端的用户信息返回给客户端。客户端读取后显示在自己的界面之上展示给用户。

    上图是双方玩家准备完毕后的收发的数据,所有文本皆显示在上面。一个玩家准备,发送结果给游戏桌对面的玩家。如果双方都已经准备好了,则发送<KIND>start</KIND>内容给双方,开始游戏。每一个落子的信号也是封装在一个<KIND>moves</KIND>文本之中,同时这串文本带有落子的位置,服务端收到落子信号后转发给游戏桌对面的玩家,对面玩家客户端收到信号后游戏界面存放五子棋数据的数据设置相应的位置为对方棋子,然后进入下一轮,即对方落子。接着对方也发送信号过来,继续互相传输落子数据,直至游戏结束。
    1 评论 1 下载 2019-08-22 07:13:46 下载需要13点积分
  • 基于C++的银行存储管理系统

    一、实训目的通过课程设计,学会运用数据结构知识,针对具体应用,自己设计合理数据结构,确定存储结构,并能设计具体操作算法,选择使用具体语言进行实现。掌握C++较复杂程序的组织和设计过程,调试技巧。学习解决实际问题的能力。
    二、实训环境计算机windows xp或其它版本,VC6.0或更高版本,或其它语言环境。
    三、实习题目小明是一个计算机专业top student,祝贺他毕业了。并准备到银行参加工作。上班第一天,经理叫他编制一个实现一个活期储蓄处理程序,算作考查。上班第一天,一定要给领导一个好印象,小明二话没说,就答应了。现要你是小明了,请完成如下题目功能。储户开户、销户、存入、支出活动频繁,系统设计要求:

    能比较迅速地找到储户的帐户,以实现存款、取款记账
    能比较简单,迅速地实现插入和删除,以实现开户和销户的需要

    四、问题分析
    创建文件,用数组形式存储用户数据,开户时自定义用户账户、姓名、密码、开户金额
    用户登录时,输入正确的用户姓名、用户密码,完成登陆后即可进行存款、取款、查询、修改密码
    实现输入用户的账户名和密码,将其全部信息删除,进行销户

    五、逻辑结构和存储结构设计存储结构设计:该存储结构是链式存储结构,本系统主要用线性表结构类型来存储在“活期储蓄账目管理系统”中的信息。其中,结构体由4个分量构成:用户账号名、用户姓名、用户密码、开户金额。
    六、算法设计本系统采用链式结构存储储蓄账目管理。

    用户输入想开户的储户输入其姓名账户密码,然后显示开户成功,会有一个账户生成,然后开户成功
    用户登录需要输入账号名和密码,判断密码是否正确,如果错误则返回,然后点击登录,就可以进入管理系统
    用户的存取款和查询余额,首先在登录账户的基础上,选择存或取款,然后输入相应的金额,若是取款应判断其金额是否小于账户上的金额,如果不小于,则提示储户重新输入相应的金额,或者退出。 4、储户需要销户的账户,然后程序自动判断该账户是否存在,然后输入账户密码,若密码与账户相对应,则删除该账户

    七、空间复杂度和时间复杂度分析7.1 空间复杂度是程序运行所以需要的额外消耗存储空间,一般的递归算法就要有o(n)的空间复杂度了,简单说就是递归集算时通常是反复调用同一个方法,递归n次,就需要n个空间。
    7.2 时间复杂度一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
    一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f (n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
    在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)。
    八、效果截图主界面

    开户界面

    账户信息界面


    退出界面

    九、收获和体会及不足这是第二次做课程设计了,但开始自己读题时,思路不是很清晰,也不明白这个程序就是要结合所学数据结构的思想,要首先定义了多个数组,包含了储户的各类信息,如姓名,账号,金额等等,并利用文件将用户信息加以存储。开户时将输入的信息写入文件,存款和取款时对用户的存款加以处理并更新到文件中。
    最后在分析好活期储蓄帐目管理的功能模块后,我开始编写代码,在代码调试的过成中,并不是一帆风顺的,这也是平常练的不够吧,经过几轮删删改改,终于做的差不多了,基本达到了老师的要求,但是还是有很多不足的地方。通过这次对银行管理系统的编写与调试,巩固了有关结构体的知识及其操作,锻炼了实际应用能力,同时对文件有了更进一步的了解及应用,明白文件的读写等相关知识。
    在不断地进行书面设计和上机调试的过程中,认识到掌握设计程序的思路非常重要,要正确处理算法与语法的关系,算法是程序的核心,是灵魂,语法是外壳是工具。但是光掌握语法也是不够的,应该还要把重点放在解题思路上。这个实验也让我认识到自己知识的有限,与同学讨论的益处,培养了我的团队意识。
    在这一周的课程设计中,我通过这次设计我也着实又感受了一次编程的乐趣,从中也学到了不少知识。书上和老师都说“程序=数据结构+算法”,但我在学习运用数据结构编程之前,并没能深刻体会到这一点,直到这次课程设计实践。
    我感受最深的一点是:以前用C编程,只是注重如何编写函数能够完成所需要的功能,似乎没有明确的战术,只是凭单纯的意识和简单的语句来堆砌出一段程序。现在编程感觉完全不同了。在编写一个程序之前,自己能够综合考虑各种因素,
    首先选取自己需要的数据结构,是树还是图或是别的什么?然后选定一种或几种存储结构来具体的决定后面的函数的主要风格。最后在编写每一个函数之前,可以仔细斟酌比对,挑选出最适合当前状况的算法。另外,我还体会到深刻理解数据结构的重要性。只有真正理解这样定义数据类型的好处,才能用好这样一种数据结构。了解典型数据结构的性质是非常有用的,它往往是编写程序的关键。
    通过这次课程设计逐渐提高了自己的程序设计和调试能力,通过上机实习,严整自己设计算法的正确性,学会了有效利用基本调试方法,查找出代码中的错误并且修27 改,我对参数的调用也进行了很多种尝试,已经能够相对准确的选择合适的参数形式来实现函数之间的数据传输交互了。
    这次试验也让我看到了自己的不足,许多关于C++的一些比较具体的东西还不太懂,比方说指针。这次课程设计也让我知道了自己的能力只有经过不断地锻炼才能提高。
    0 评论 2 下载 2019-08-19 10:11:48 下载需要5点积分
  • 基于QT的网络对战中国象棋游戏

    一、绪论1.1 项目开发意义看不惯现如今,无论是电脑端,还是手机端,亦或者是其他设备平台的上软件,无论是游戏、还是效率类的 App,均都充满着商业气息,至少启动页面都是带着广告,亦或是带着内置充值付费的内容,让使用者感觉不悦。个人对于中国传统的文化的爱好、编程的喜爱、开发的喜爱、逻辑思维的游戏喜好,外加因为毕业需要,和充分锻炼自己的编程开发的思维,提升自己的能力和其他方方面面。所以决定开发出这个基于 QT 的跨平台网络对战中国象棋游戏。
    在该中国象棋运行程序,充分的考虑到了缺少玩家、缺失网络和没有足够多的电子设备等情况,开发出了多个游戏策略模式,以应对不同情况的需求。且程序最大优点是免安装,操作简单,主流操作系统基本全面覆盖。也没有广告或者付费充值等内容。只做好简单的下棋游戏功能,让玩家感觉到一个开发者的无私的热情和中国象棋爱好者的那一份单纯的情怀。
    1.2 项目方案1.2.1 设计方案一尽量采用最稳定最新的开发工具进行编程。确保运行程序的稳定性和用户体验。其次是便于软件的开发和后期维护、简化流程。软件开发层次考虑:首要是尽量考虑采用最稳定的、其次再是最新的开发环境。这样充分保证软件开发出来之后的稳定性,以用户体验作为最大标准。且考虑到开发周期和变卡的便捷性。所以在编程开发的采用 Windows 10(专业工作站版本)、Ubuntu 16.01.5 (LTS)、 MacOS10.14、 IOS 11.4.1、 Flyme 7.1.5.0A(最新的稳定版、 基于 Android 7.0)、 Qt Creator 5.9.7(LTS)、Enigma+Virtual+Box+7.80。
    大概设计思路如下:
    利用一些 Qt Creator 提供的一些默认组件,来进行象棋和棋盘的绘画,对于象棋的每一个棋子的走棋规则,就利用网上的提供的一些库函数来进行调用。最后添加一些贴图来进行界面的美观美化。然后象棋的基础信息就存储在SQL Server 2012 创建的表里面。且开发一个类似于微信的聊天界面,用以玩家进行通信。且将所有的消息和注册用户的相关信息全部都存储到 SQL Server 2012 的数据库表里面。 最后将这个中国象棋界面和独立的聊天界面合二为一,当编码完成之后,进行多个系统平台的发布。
    1.2.2 设计方案二尽量采用最稳定最新的开发工具进行编程。确保运行程序的稳定性和用户体验。其次是便于软件的开发和后期维护、简化流程。
    软件开发层次考虑:
    首要是尽量考虑采用最稳定的、其次再是最新的开发环境。这样充分保证软件开发出来之后的稳定性,以用户体验作为最大标准。且考虑到开发周期和变卡的便捷性。所以在编程开发的采用 Windows 10(专业工作站版本)、Ubuntu 16.01.5 (LTS)、 MacOS10.14、 IOS 11.4.1、 Flyme 7.1.5.0A(最新的稳定版、 基于 Android 7.0)、 Qt Creator 5.9.7(LTS)、Enigma+Virtual+Box+7.80。
    大概设计思路如下:
    主要是使用 Qt Creator 的 C/C++功能,基本不使用控件拖拽,尽量使用手写代码生成、熟悉面向对象编程。提高手写代码能力。去掉数据库存储数据和为玩家相互聊天的功能。全程基本只采用 Qt Creator 独立开发。
    1.2.3 设计方案对比设计方案一重点在于开发工具的最新版本的使用, 和将数据存储在数据库里面,主要是使用 Qt Creator 和 Visual Studio 和 SQL Server 的融合使用。在不通的层次使用不同的开发工具。和加强熟悉多种工具的融合,提升整体的技术。
    设计方案二则是重点在于开发工具的稳定。但是对于数据的存储采用数据结构的方式, 棋子界面和棋子采用最基本的系统 API 进行绘画。关键熟悉面向对象的开发方式。
    综上两者比较,我就决定采用设计方案二。主要理由如下。使用最新版的工具开发,有时候会遇到许多问题,或者莫名的异常警告不通过。而又难以在网上搜寻到合适解决方案,导致项目搁浅。且尤其是采用 Visual Studio + QtCrator 开发,其中还需与多余的环境设置与修改,比较复杂;且 Qt Creator +SQL server 的开发,没有提现成的驱动,需要自己下载源码编译,也不容易编译通过。且上面这种配合方式我几个月前试过了,操作复杂不易实现。且本就是写 C/C++的项目。想着熟悉一下数据结构的知识,看着能不能自学之后达到融会贯通的地步。 且还相比前者方案,也更加便于软件的开发和项目的稳定性。赢得玩家好感。
    1.3 项目成品运行环境Windows 平台、 Linux 平台、 MacOS 平台、 Android 平台、 ios 平台等。以上平台只要是目前常用的主流系统平台,都可以运行;且之后也都是了解了一下,就连一些小众化的平台,比如 Windows CE/Mobile、嵌入式 Linux(Embedded Linux)、 Symbian、 Maemo、 MeeGo 等平台,只要设置好相关的环境之后进行编译生成,都可以基本无需要修改源码、 都是可以直接运行在其相关的平台上面。受限于硬件和软件设备和其他的环境限制, 外加时间和个人精力有限,使得无法在所有版本平台均测试运行。但是在主流的系统平台上面,均做了项目产品的演示和生成,且所有的参与测试的平台,均测试成功。其他小众环境平台,若有相关的需求,可以参考开发帮助文档和开源社区的帮助。 最后结果均运行对应程序时候,都无需安装特殊的运行库或者指定环境才能运行。且电脑端的中国象棋游戏程序均无需安装, 为绿色版, 点击即可以运行。
    1.4 开发工具介绍Qt Creator:一个跨平台集成开发环境(IDE),可为多个桌面,嵌入式和移动平台创建C ++和QML应用程序。它附带一个代码编辑器,并与用于在整个产品生命周期中设计,编码,测试,部署和维护软件的工具集成在一起。
    Enigma+Virtual+Box+7.80: 软件虚拟化工具,它可以将多个文件封装到应用程序主文件,从而制作成为单执行文件的绿色软件。它支持所有类型的文件格式,虚拟化后的软件不释放任何临时文件到您的硬盘,文件模拟过程仅在内存运行。
    VMware Workstation Pro:提供用户可在单一的桌面上同时运行不同的操作系统,和进行开发、测试 、部署新的应用程序的最佳解决方案。
    1.5 可行性分析1.5.1 技术可行性基于QT的跨平台网络对战象棋项目,采用Qt Creator作为界面设计和功能设计。且Qt提供的良好的封装机制使得Qt的模块化非常高,可重用性良好;还有着丰富的API接口提供和大量的开发文档提供支持[1],更是有全球性和中文的开源社区。数据方面采用数据结构和STL和容器来存储,无需额外使用数据库;亦是减少发布时候的复杂度。且自己自从大一开始,到现在,多次反复的学习和研究C/C++相关的知识,基本功比较扎实,加上具有良好的自学能力、沟通能力、和自己用Baidu和Google来解决开发过程遇到的出错和技术上的问题。另外还有指导老师可以依靠和寻求帮助,开源社区热心网友也有技术支撑。综上所述,所以在技术可行性是可行的。
    1.5.2 操作可行性基于QT的跨平台网络对战象棋的项目,其完全是根据不会怎么操作电脑和手机平板的象棋爱好者提供的量身定制,在运行流程上十分的简便清晰。且该运行程序的用户设计比较友好,功能分明,操作简单,且提供了相应的弹窗提示。综上所述,所以在操作可行性是可行的。
    1.5.3 法律可行性基于QT的跨平台网络对战象棋的项目,本就是自己对于毕业设计从决心开发的一款游戏,一开始就是想着只是提供象棋爱好者一份更好的选择。对于采用的Qt Creator,其中Qt虽然是商业公司的产品,但是走的却是开源路线,提供免费下载,全部都是开放源代码,非商业用途亦采用GPL的版权宣告。且本人也没有违反国家相关法律法规。综上所述,所以在法律可行性是可行的。
    1.5.4 经济可行性基于QT的跨平台网络对战象棋的项目,主要是对于一些商业化严重的游戏的市场局面看不过去,和个人对于传统文化的爱好来开发的;所以本就一开始想着就是开源、非盈利的想法而设计的一款产品。虽没钱可图,但是这样花费时间和精力开发,能够让一些爱好象棋的人在使用这款软件的时候感觉到开心,我就会感觉到很开心。综上所述,所以在经济可行性是可行的。
    二、项目思路分析2.1 需求以及实现需求一:界面美观,操作简易
    需求实现:界面整体分为三大板块;其中主界面大概以7:3分布其中主要的两大板块。第三板块“关于作者”以另外的小的界面单独显示。且只有三个点击按钮。和鼠标点击棋子不规则时候会无反应。符合下棋规则棋子才会移动,故操作简单。
    需求二:下载容易,支持多种主流的操作系统平台
    需求实现:使用QT来开发,本就支持许多的跨平台。然后当某一平台成功之后,只需要在其他平台也重新按照其平台流程发布一遍即可以。
    需求三:免安装,程序体积小
    需求实现:采用动态编译,将所需要的库函数文件已打包压缩成一个绿色的运行程序以实现上面需求。
    需求四:没有广告或者付费充值内容
    需求实现:个人兴趣开发作品,开发此款作品的一个理念是非盈利和开源。
    需求五:易于下载,且一定是正版发布
    需求实现:将其在作品和源码在github上面发布,确保用户下载一定是本人发布版本,而非是旁人可能破解之后的修改版。

    玩家和自己、电脑AI对战功能流程示意图


    多人网络游戏战功能流程示意图


    系统流程图

    三、总体设计3.1 设计思路框架对于这个基于QT的中国象棋游戏项目的项目,因QT本身提供的良好的封装机制,使得它的模块化非常高,可重用性良好;鉴于此优点和大学期间学过的“软件设计”一门课,使得在开发时候也选取模块化设计,实现充分的代码的重复利用性;且尽量使得程序代码之间的组合,达到高内聚、低耦合的效果。
    其中中国象棋的整体结构如下图所示:

    由上面的功能结构图可知,其中大致分为三个部分:①多种游戏模式部分,②对战计时部分,③作品信息部分。而这其中每一个部分都是有细分为其他的多个子部分,其中每一部分的详细框架结构如下。

    多种游戏模式部分,其本质计时下象棋对战部分,是整个项目的最核心区域地方。其中又是细分为三大子模块:

    玩家和自己对战:包含有棋子和基础的棋盘类,从而实现玩家和自己对战的详细游戏规则玩家和AI对战:继承前面的玩家和自己对战类,然后做相应的重载和扩充里面的功能哈数,修改成自己的玩家和AI对战的详细游戏规则多人网络对战:同样是继承前面的玩家和自己对战类,然后做相应的重载和扩充里面的功能哈数,修改成多人网络对战的详细游戏规则
    服务器端:还有相应的Socket套接字网络编程,给客户端发送自己下棋的步骤信息客户端:也有相应的Socket套接字网络编程,给服务器端端发送自己下棋的步骤信息

    对战计时部分,其核心本子就是一个独立运行的计时器功能部分。用于游戏时间的计时
    作品信息部分,其核心本质就是关于作者部分,即就是一个独立的Dialog作品详细介绍部分

    其中一开始,先是设计一个单纯的棋子类Class:ChessPieces,其中只包含每一个棋子的基础信息;
    然后后面新的棋子结构体Struct:POS来包含这个Class:ChessPieces,可以被后面设计的棋盘类所调用,进行初始化。
    紧接着就是结构体Struct:POS的作用是被后面的 棋盘类【Class:ChessBoard 玩家和自己对战类】;以及后来继承它的的两个子类 【Class:MachineGame 玩家和AI对战类 和 Class:NetworkGame 双人网络对战类】)来进行调用。
    其中在各自三种棋盘类【Class:ChessPieces、 Class:MachineGame、Class:NetworkGame 】里面分别详细定义和重载自己所需要的功能函数。比如将里面的玩家A和玩家A对战规则;继承之后修改为玩家A电脑AI对战的规则;又或者是继承重载之后,修改为玩家A和玩家B进行跨网络对战的规则。
    到此为止,整个游戏棋盘的核心下象棋的基础框架就出来了。接下来就是两个独立的模块,在逻辑上面没有太大关联。对于计时模块,利用QT提供的一些控件模块。来完成该相应的计时部分的功能。最后就是关于作者部分,单独的显示一个窗口出来,做一些特效出来,添加一些链接或者文字和图片等,来展示此作品的一些详细信息。
    3.2 界面UI设计整个界面是选用QT自带的Dilog界面和Widget界面背景。其中象棋绘画的部分,是调用系统底层的API函数来绘画的,通过相应的接口来处理进行一些特效,比如颜色和透明度的处理。对于计时器部分,添加好透明背景,作为相应的皮肤,且选用素材图片都是可以免费商用的素材。最后就是关于作者部分界面设计,就是采用的小界面小弹框模式的界面设计,来显示相关作者和作品信息。
    3.3 数据存储设计对于这个里面的相关数据的储存,因为所用到的数据量比较小,所以就直接去掉了采用关系型数据库的考虑,采用数据结构和容器来存储临时的数据。结构简单,且可以加深自己对于数据结构的灵活的运用。
    3.4 类的继承关系图
    3.5 具体各个类的详细设计预作用其中主要是有如上图的几个类,其中每详细类的作用如下:
    项目架构名称: ChineseChess
    设计作用:作为一个总的解决方案。其解决方案下面的项目总名称,有着对开发的项目的最简洁的称呼说明
    类视图:说明:每一个项目都首先创建的,作为一个文件夹,里面用来存放所有项目的.h、.cpp、.pro、图片等其他一系列的资源

    类名:POS
    设计作用:作为一个单独的Struct结构体,用来存放棋子的一个三要素,棋子的名称和棋盘里面横纵坐标
    类视图: 是一个Struct结构体

    类名:AboutAuthor
    设计作用:作为项目里面,展示关于作者和作品详细信息,属于一个独立的模块
    类视图: 是一个Class类。且里面主要是空白,其中主要信息是在一同创建的AboutAuthor.ui里面

    类名:ChessBoar
    设计作用:其是整个项目最核心的一个类,也是整个项目里面最为复杂的一个类。其中包含有多个结构体和类,棋子类等。其所负责主要功能是绘画棋盘和棋子,对所有棋子进行初始化,定了各个棋子的下棋规则制定。同时完成游戏模式之一的玩家与自己对战模式
    类视图: 说明:是一个Class类。作为后面的MachineGame玩家与电脑AI对战和NetworkGame多人网络对战的父类

    类名:ChessPieces
    设计作用:作为单独设计的棋子类,包含棋子的颜色、唯一ID属性、是否死亡等状态属性
    类视图: 说明:是一个Class类。和ChessBoar类是包含关系

    类名:ChessStep
    设计作用:棋子步骤类,主要是使用后面的容器而准备的。只有基本棋子击杀状态、现在棋盘行列值和目标前往的行列值
    类视图: 说明:是一个Class类

    类名:ChooseMainWindow
    设计作用:作为点击游戏开始,就会弹出来的一个选择窗口,关于选择哪一个游戏模式。然后设计最为选择游戏主要窗口
    类视图: 说明:是一个Class类

    类名:MachineGame
    设计作用:作为游戏的玩家与电脑AI对战的模式的实现
    类视图:是一个Class类。也是ChessBoar派生类

    类名:NetworkGame
    设计作用:作为游戏的多人网络对战的模式的实现
    类视图:是一个Class类。也是ChessBoar派生类。其中若是在不同电脑之间测试。记得修改网络协议的,所连接的服务器的IP;当然若是发现端口被占用,亦可以修改成一个大于数值(范围是从0 到65535),建议大于6000

    类名:SelectGameMode
    设计作用:选择游戏模式的Dialog界面
    类视图:是一个Class类。是一个手写的带.ui文件,而非使用设计师设计的

    3.6 详细功能结构模块设计3.6.1 整体的功能结构图设计
    3.6.2 选择游戏方式3.6.2.1 整体思路作为最主要、最核心的基类,也是最开始就单独保持运行的玩家和自己对战的游戏模块。要包含有棋子类,在创建和绘画棋盘的时候,要在构造函数里里面对32颗棋子进行初始化赋值。
    3.6.2.2 步骤
    绘画棋盘:使用系统自带的API函数接口,调用画家Qpainter、画笔drawLine。以及相关的设备上下文等,来绘画初步的函数图像。
    绘画棋子:对棋子进行初始化赋值,使得他们分别固定在对应的棋盘位置
    棋盘行列值和屏幕之间的像素值之间进行切换:创建调用的功能函数,为后面的点击屏幕和点击棋子和点击棋盘具体位置的,之间进行转换
    象棋轮流下:设置BOOL变量值,当属于红方或者黑方下了一步棋之后,就通过限制使得其对方下载,继续进行游戏,才能够接着下棋
    制定象棋的具体规则:要设置每一种棋子对应的走法规则。
    屏幕重绘:当每点击一次棋盘或者棋子之后,和发生鼠标键盘交互事件的时候,就会调用一次系统屏幕的刷新,来进行,屏幕的重绘
    判断谁胜谁负:专门写一个功能函数,看看红方或者黑方的所有棋子均无路可走或者己方的将被击杀,则会判定对方胜利,自己失败,从而结束游戏

    3.6.2.3 局部核心代码class ChessBoard : public QWidget{ Q_OBJECTpublic: explicit ChessBoard(QWidget *parent = 0); ~ChessBoard(); bool isDead(int id); int getStoneId(int row, int col); int getStoneCountAtLine(int row1, int col1, int row2, int col2); //车 炮 的功能辅助函数 判断两个点是否在一个直线上面,且返回直线之间的棋子个数 void whoWin(); //谁胜谁负 bool isChecked(QPoint pt, int& row, int& col); //是否选中该枚棋子。pt为输入参数; row, col为输出参数public: QPoint center(int row, int col); //象棋的棋盘的坐标转换成界面坐标 QPoint center(int id); virtual void paintEvent(QPaintEvent *); //绘画棋盘 void drawChessPieces(QPainter& painter, int id); //绘画单个具体的棋子 virtual void mousePressEvent(QMouseEvent *); //鼠标点击事件 virtual void clickPieces(int checkedID, int& row, int& col); //象棋移动的规则[将士象马车炮兵] bool canMove(int moveId, int killId, int row, int col); bool canMoveJIANG(int moveId, int killId, int row, int col); bool canMoveSHI(int moveId, int killId, int row, int col); bool canMoveXIANG(int moveId, int killId, int row, int col); bool canMoveMA(int moveId, int killId, int row, int col); bool canMoveCHE(int moveId, int killId, int row, int col); bool canMovePAO(int moveId, int killId, int row, int col); bool canMoveBING(int moveId, int killId, int row, int col); }
    3.6.2.4 运行效果
    3.6.3 玩家和AI对战模块3.6.3.1 整体思路提过上面的Class:ChessBoard类已经实现了两方进行象棋的轮流博弈。所以我们在这里的类通过继承Class:ChessBoard即可以了。接下来就是通过重载鼠标和键盘点击是事件,分别来判断是玩家还是电脑系统AI来进行选择棋子和走棋子。
    3.6.3.2 步骤
    重载鼠标点击事件:判断是玩家下棋还是电脑机器AI来进行下棋
    计电脑AI的走法

    计算出所有可能移动棋子的方法:当轮到AI选择棋子和移动棋子的时候。首先判断出AI所有可以移动的棋子,通过void getAllPossibleMoveStepAndNoKill ( Qvector <ChessStep\*>& steps )来计算出获得所有可能的移动步骤(不击杀对方棋子)、以及通过 void getAllPossibleMoveStep (Qvector < ChessStep > & steps)来计算出获得所有可能的移动步骤(击杀对方棋子)的走法,然后通过void saveStep(int moveID, int checkedID, int row, int col, QVector<ChessStep>& steps)来保存这里面的所有可能走法, 以及每一个可以移动棋子的可能走法,将其全部走法都保存在容器QVector<ChessStep\*>& steps里面 假设移动棋子:移动可走的算法里面的某具体的一个棋子的走法计算当局面分:通过你自己自定义的“有利局面”,用一个分数本来表示有利的大小。这里面具体怎么计算这个有利局面的局面分,就占到了很大一部分比例关于AI有多么的“聪明”撤回当前移动的棋子:通过悔棋步骤,撤回该棋子
    AI移动最有利一步棋子:通过遍历可走算法的保存下来的多个可能,然后计算每一个可能得局面分,选择局面分最大的一步走法作为电脑AI最佳的走法
    轮流博弈下棋:在玩家和电脑AI之间,轮流下棋,直到棋局出现胜负局面, 以终止游戏运行

    3.6.3.3 局部核心代码判断是玩家游戏还是AI游戏,并且进行轮流循环:
    void MachineGame::clickPieces(int checkedID, int &row, int &col){ if(m_bIsRed) //红方玩家时间 { chooseOrMovePieces(checkedID, row, col); if(!m_bIsRed) //黑方紧接着进行游戏 { machineChooseAndMovePieces(); //ToDo: 机器 黑方时间 } }}
    3.6.3.4 获得最好的移动步骤ChessStep* MachineGame::getBestMove(){ int maxScore = -10000; ChessStep* retStep = NULL; //------------------------ //有可击杀的红棋子就走击杀红棋子最优的一步 // 1.看看有那些步骤可以走 QVector<ChessStep*> steps; getAllPossibleMoveStep(steps); // 黑棋吃红棋的所有可能的步骤 //------------------------ //没有可击杀的红棋子就走最后的一步 QVector<ChessStep*> stepsAndNoKill; getAllPossibleMoveStepAndNoKill(stepsAndNoKill); // 黑棋移动所有可能的步骤(不吃红棋子) //2.试着走一下 for(QVector<ChessStep*>::iterator it = steps.begin(); it!=steps.end(); it++) { ChessStep* step = *it; fakeMove(step); int score = calcScore(); //3.计算最好的局面分 unFakeMove(step); if(score > maxScore) { maxScore = score; retStep = step; } } if(retStep != NULL) return retStep; //2.试着走一下 //从这种不击杀红棋子,只是单纯移动黑棋steps里面,随机抽选一种进行下棋 int nStepsCount = stepsAndNoKill.count(); qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); //随机数种子, 0~MAX int temp =qrand()% nStepsCount; QVector<ChessStep*>::iterator it = stepsAndNoKill.begin(); retStep = it[temp]; if(retStep == NULL) whoWin(); //4.取最好的结果作为参考 return retStep;}
    3.6.3.5 运行效果
    3.6.4 双人网络对战(服务器端)模块3.6.4.1 整体思路同样是继承上面的思路,首先通过继承类Class:ChessBoard,然后改写部分虚函数,和添加一些额外的扩展来实现双人对战的类Class:NetworkGame。然后将自己所选中的棋子和走法通过TCP协议,Soctck套接字来传输相关的这些信息,然后对方刷新他自己的棋盘界面。然后下完棋之后仍然通过网络Soctck套接字将棋子和下法传输过来,再然后刷新自己的界面。这样往返交替循环下棋,达到局域网里面通信的下棋要求。
    3.6.4.2 步骤
    创建服务器的QtcpServer的m_tcpServer的套接字:制定使用QhostAddress :: Any的协议族,和端口。绑定主机和选择通信协议和接接口
    监听指定网段:监听网络里面其他客户机器,是否发来连接请求
    连接请求的客户机:通过connect()连接服务器和客户机,建立通信连接

    3.6.4.3 局部核心代码服务器端创建网络连接:
    NetworkGame::NetworkGame(bool isServer){ m_tcpServer = NULL; m_tcpSocket = NULL; if(isServer) //作为服务器端 { m_bIsTcpServer = true; m_tcpServer = new QTcpServer(this); m_tcpServer->listen(QHostAddress::Any, 9999); connect(m_tcpServer, SIGNAL(newConnection()),this, SLOT(slotNewConnection())); } else //作为客户端 { ....... }}
    3.6.4.4 运行效果

    3.6.5 双人网络对战(客户端)模块3.6.5.1 整体思路整体思路和服务器无差别,只不过是将客户端将消息发送到服务器。
    3.6.5.2 步骤
    创建套接字 :创建客户端的QtcpSocket套接字的m_tcpSocket
    连接服务器:因为只有一台电脑设备,这里是固定服务器地址为127.0.0.1。连接这一台电脑
    进行网络对战:进行游戏联机对战,一直到游戏运行结束

    3.6.5.3 局部核心代码客户端进行网络连接:
    NetworkGame::NetworkGame(bool isServer){ m_tcpServer = NULL; m_tcpSocket = NULL; if(isServer) //作为服务器端 { …… } else //作为客户端 { m_bIsTcpServer = false; m_tcpSocket = new QTcpSocket(this); m_tcpSocket->connectToHost(QHostAddress("127.0.0.1"), 9999); connect(m_tcpSocket, SIGNAL(readyRead()), this, SLOT(slotRecv())); }}

    这里客户端设置的是连接局域网里面的127.0.0.1,端口为9999服务器;若在两台实体客户机(可以是运行的系统不同)里面运行该程序。则是需要修改。
    3.6.6 对战计时模块3.6.6.1 整体思路利用Qt自带控件QLCDNumber来充当计时的显示屏,QpushButton按钮来作为启动开始和结束的开关。点击开始按钮,计时器后台开始工作。然后每隔一秒钟就刷新一下显示界面。使得看起来计时器是在工作。再点击一下暂停来停止,点击一下重置就会清零计时器的显示时间为00:00,可以开始下一轮的计时。
    3.6.6.2 步骤
    拖拉控件制作:拖拽设计界面的控件,做好布局的显示准备
    定义槽函数:定义各个按钮需要的对应功能,设计成为槽函数[9]
    连接信号与槽:使用connnect()连接控件发射的信号,和实施对应的槽函数,已完成相应的功能

    3.6.6.3 局部核心代码初始化计时器
    m_timer = new QTimer; //初始化定时器m_timeRecord = new QTime(0, 0, 0); //初始化时间m_bIsStart = false; //初始为还未计时connect(m_timer,SIGNAL(timeout()),this,SLOT(updateTime()));
    刷新时间槽函数
    void ChessBoard::updateTime(){ *m_timeRecord = m_timeRecord->addSecs(1); ui->lcdNumber->display(m_timeRecord->toString("hh:mm:ss")); if(m_bIsStart == false) { ui->pushButton_start->setText("开始"); } else if(m_bIsStart == true) { ui->pushButton_start->setText("暂停"); }}
    开始、暂停计时槽函数
    void ChessBoard::on_pushButton_start_clicked(){ if(!m_bIsStart) //尚未开始 开始计时 { m_timer->start(1000); } else //已经开始,暂停 { m_timer->stop(); } m_bIsStart = !m_bIsStart;}
    3.6.6.4 运行效果
    3.6.7 关于作者模块3.6.7.1 整体思路单独的设计一个继承于Widget界面的窗口,然后在里面写上一些想要显示的汉字文字,然后使用一个Qtextbrowser来承接这些,因为可以使得内容信息显示处于多行显示,和外加添加一些连链接、图片等。最后设置对应控件和背景图的层次关系,以及透明度等,就相当于设计好了带的皮肤,给人眼前一亮的感觉。
    3.6.7.2 步骤
    拖拽相关控件
    在控件里面写好相关的内容
    设置槽函数
    连接控件发出的信号和对应的槽函数,施行相应的功能

    3.6.7.3 局部核心代码初始化作者类
    m_pAbout = new AboutAuthor(); //关于作者
    关于作者槽函数
    void ChessBoard::on_pushButton_about_clicked(){ m_pAbout->setWindowTitle("关于作者"); m_pAbout->show();}
    3.6.7.4 运行效果
    3.7 其他细节设计3.7.1 多界面之间的切换实现主要就是一开始点击运行程序。提供一个游戏选择模式窗口,当选择结束之后;在编码部分,将选中的对象设置为生存状态,至少要保证其在运行期间创建的变量对象一直都在,当结束的时候,再来释放该对象分配的许多内存。这里需要注意的是,推荐是使用New一个产生的变量,而不是设置为static的全局变量,且还要将其显示为.show()状态,也不要忘记将次之前已经选择的,不需要的窗口对象使用delete释放掉,且外加.hide()隐藏起来。这样子就可以实现在多个界面之间窗口的来回切换。
    3.7.2 添加界面透明皮肤其实这个功能,是属于看起来困难,但是实施起来比较非常容易的一个步骤,大概简单的就是:在设计师界面,右键点击属性,插入一张图片,设置为置低下一层即可。
    3.7.3 给运行程序添加一个自定义图标这一功能的实现,有比较多的简单方法,这里我挑选一个比较•简单的方法说明我的实现。首先选择自己需要的个性化图片或者Logo的图标,然后采用在线格式转换没设置成.icon的格式图片,一般推荐大小为32x32或者64x64大小的。然后将其图片文件复制命令拷贝好到项目的根目录下面,然后利用记事本新建一个名为logo.rc的文件,logo.ico为转换后的图标名称,内容如下:
    IDI_ICON1 ICON DISCARDABLE “log0.ico”;紧接着就是在工程文件夹中新建一个images目录,将logo.ico、logo.rc放入文件夹中。再打开QT工程,将logo.ico、logo.rc添加进工程。然后在工程文件(*.pro)中加入一行:
    RC_FILE=images/logo.rc最后一步重新建工程,这时QT程序就有了一个漂亮的外观了,程序快捷方式效果如下图所示:

    四、项目演示效果
    1 评论 8 下载 2019-08-19 11:07:11 下载需要13点积分
  • 基于Python实现的孤立词语音识别系统

    1 任务介绍语音识别是通往真正的人工智能的不可缺少的技术。尽管能真正听懂人类说话的智能机器任然在未来不可捉摸的迷雾之中,但我们必须先解决如何识别出人类语音中包含的自然语言信息的问题。而数字信号处理技术将为这一任务赋能。在本课程项目的任务之中,我们面对的是一个简化的语音识别场景——即孤立词识别。
    我们针对 20 个关键词,采集了所有参与课程的同学朗读每个词 20 遍的语音。我将以此为数据集来构建一个能正确识别这 20 个关键词的孤立词识别系统。
    2 项目实现基于一学期跟随老师学习到的关于信号处理与语音识别技术的知识,我额外查阅多方资料,最终呈现出了我的语音识别系统与报告。
    我实现的语音识别系统的亮点有以下几个方面:

    说话人无关的孤立词识别是语音识别技术发展中一个里程碑。从现代的观点来看,如果将语言信号视作时间序列,那么孤立词识别就是一个模式识别中的分类问题。模式识别问题的解决一般分为特征提取与模型构建两个部分。我们将这两个部分分开处理,使得代码的实现更加具有结构性和层次性。报告也将这两部分的处理分开叙述
    我在整个系统的实现中,除了利用了数值处理函数包 numpy 和自动求导工具包 pytorch之外的所有核心代码都是单纯使用 python 实现。即真正锻炼了代码实现能力,也加深了对语音识别技术的理解。在报告中我也强调了各个方法和过程的代码实现,并将关键代码添加到附录之中以方便检阅
    特别地,我基于课堂上所学的蝶形变换方法,实现了以 2 为基的快速傅里叶变换,并运用到了频域特征的分析之中。这让我更加领略到该算法的优美
    根据我自行实现的快速傅里叶变换,实现了梅尔频率域的倒谱系数的计算,并根据通过梅尔滤波器之后得到梅尔频谱特征设计了基于卷积神经网络的识别算法
    我将计算出的频谱特征视为图片,因而可以使用近年来在大规模图片分类任务上大放异彩的卷积神经网络来进行分类识别。我采用了 2014 年在 ImageNet 的比赛上获胜的VGG Net 作为我们的识别模型,并使用了批归一化和 Dropout 手段来避免过拟合,提高模型的泛化能力

    2.1 预处理首先我对数据进行了清洗。
    各个同学上交的文件结构并不一致,有的是一个压缩包下包含所有文件,有的是一个压缩包中还有以自己的学号命名的文件夹,此外还有一些同学提交的压缩包是在 MacOS 上进行打包的,因此还有一个额外的缓存文件夹。这样的结构不利于我们对数据进行批量的读入。
    因此我编写了程序先解压所有压缩包,然后进行深度优先搜索来遍历所有文件夹,根据文件的命名规则把所有文件提取出来,按照 data/学号/文件名.wav 的格式统一存储。同时因为需要大规模地进行复制提取,为了效率的考量,我使用多线程的方式完成了这一任务。
    此外有几个文件显示已损坏而无法读取,以及一个文件录音长度大于两秒。为了数据的一致性,必须去除掉异常数据,但仅仅删除数据将导致样本不均衡的问题。为此我采用随机替换的方式,用同一个同学在同一个词下的另一个语音文件进行替换。这样就可以缓解数据缺失带来的样本不均衡的问题。

    同时,考虑到最终测试时是采用集外测试的方法,理论上讲应剔除女生的数据。
    在接下来的报告中,如无特殊声明,用 y0, y1 · · · , yt, · · · 来表示离散的信号构成的序列,用 sr来表示采样率。
    2.2 特征提取2.2.1 归一化因为数据在采集的时候声音强度并不一致,为了消除影响,我对每一段音频数据进行归一化。这里只需要对 y 使用 numpy.normailize 方法即可。
    2.2.2 预加重对读入的数据,我首先通过预加重的方法来补偿由于唇和空气导致的声音在产生和传播中高频部分的损失。预加重通常采用一个一阶的线性高通滤波器

    来实现,其中 α = 0.97。在代码实现上,只需要对数组进行加权差分即可。
    2.2.3 分帧一般情况下,语音信号是一种典型的非平稳信号,但是可以认为人说话的语调变换并不是是突然的,因此可假定语音信号在短时间 (10-30ms) 内是平稳的;故而在对语音信号进行分析时,我们可以将语音信号以 10-30ms 的间隔将连续的音频信号截取成一段一段来进行分析。
    用 y[i : j] 来表示 yi, yi+1, · · · , yj−1,为一帧包含为 j − i 个采样点的语音信号,其时长为( j−i)/sr。
    因此,当音频信号采样率为 48kHz 的时候,想要得到时长为 10-30ms 的信号帧,其应该包含480-1500 个采样点。
    另外,从分帧后信号的连续性考量,一般在分帧的时候会让相邻的两帧有 30%-50% 的重叠。分帧的实现是简单的,只需要使用 python 支持的数组切片 (slice) 方法即可方便地取出一帧对应的采样点。
    2.3 加窗对于每一帧数据,我们应当强调该帧数据内中间部分的数据,并将边缘的数据进行弱化。我将使用汉明窗

    作为窗函数。

    而所谓的加窗操作也就是用窗函数与一帧内的采样数据相乘


    考虑之后要进行短时傅里叶变换,对每一帧数据进行加窗的理由将更加充分。观察图 2中的一帧余弦信号,可以看到其开始和结束的位置是断开的。而我们在做离散时间傅里叶变换的时候是将该帧数据在时域上无限延拓成周期信号,这将导致延拓之后的信号在帧的开始与结束会发生跳变。直接进行离散傅里叶变换将导致信号泄露的现象。如图 3所示,左边是取出开始和结束连续的一帧信号的离散傅里叶变换的结果,而中间则没有加窗直接进行变换,右边是加了汉明窗再进行变换的结果。可以观察到中间的结果在信号的主要频率周围有明显的泄露现象,导致在原始数据中不存在的频率成分出现在频率域。而增加了汉明窗后,频率泄露的现象有了明显的改善。

    2.3.1 端点检测为了减少非语音的信号对识别的影响,我首先通过信号的时域特征来检测出语音的端点。可以使用短时平均过零率和短时能量来进行端点的检测。
    想要检测出语音信号的端点,最简单的想法就是通过信号的强度来判断,因为包含语音的信号强度会明显高于背景噪声。而短时能量就是刻画信号强度的一个有效的指标。对信号进行分帧之后,y[i : j] 的短时能量定义为

    即帧内信号的平方和。但如果仅仅采用短时能量作为端点检测的指标,我们将很难检测出清音信号,从而导致语音不完全,譬如说单词”Speech” 中开始的清音就无法检测出来。因此我们引入短时平均过零率这个指标

    即该帧内信号通过 0 的平均次数。
    因为无法检测出来的清音一般出现在单词的开始或者结束,所以我们采用以 SE 指标为主、SC 指标为辅的检测策略。首先观察到不少数据在一开始会有类似敲击键盘的的清脆噪声,导致 SE 指标较大,因此我去掉了前五帧信号;然后我设定一个阈值 E0,对于 SE > E0 的帧,可以断定其包含语音信号;取最开始判定为有语音的帧的开始和最末尾有语音的帧的结束作为粗糙的端点检测结果,再根据 SC 指标,设定一个平均过零率阈值 C0,根据粗糙的结果向前、向后扩展 SC > C0 的帧作为包含清音信号的判定条件。

    问题的难点在于阈值的设定,在多次试验后,我选择了 E0 = 0.98, C0 = 0.002 的参数,可以对大多数数据做到较好的端点检测效果。如图 4所示,上方为用红色标出检测结果的波形,中间为短时能量,下方为短时平均过零率。可以看到,如果仅仅使用短时能量进行检测,将无法测出开始的清音,而短时平均过零率的指标则将清音也一并检测出来了。
    2.3.2 快速傅里叶变换为了得到信号的频域特征,需要对每一帧信号进行离散傅里叶变换,即计算

    假设一帧数据的采样点数 N 是 2 的整次幂,那么

    从而

    用 Ek 表示偶数下标子序列的 DFT,Ok 表示奇数下标子序列的 DFT,则 k < N/2 时

    而 k > N/2 时

    这样就可以按照下标的奇偶性将待变换的信号序列拆成两半,递归地进行计算,然后在从递归计算的两个子序列的结果中计算出整个序列的离散傅里叶变换。该算法被称为基 2 的 CooleyTukey快速傅里叶变换。显然该算法计算量为 T(n) = 2T(n/2) + Θ(n)。根据主定理,该算法的时间复杂度为 Θ(n log(n))。
    在递归的过程中,数据的组合与流向的图样像蝴蝶一般对称而美妙,因此被称作蝶形变换。

    我用 python 实现了该算法,并将其利用到后续的梅尔频率域的频谱特征的计算之中。优美的算法通常在实现上也十分简洁。
    2.3.3 梅尔频率域特征梅尔频率域的理论依据来自于人耳听觉对频率的感受。耳蜗相当于一个滤波器组,在 1000hz以下为线性尺度滤波,在 1000hz 以上为对数尺度滤波。通过 fMel = 2595 + log(1 + f/700) 可以将普通频率变换到梅尔频率尺度,观察这个变换的图形会发现在 1000hz 以下会有更高的分辨率,这和人耳对低频信号更加敏感是相符的。在变换之后的梅尔频率尺度取等间隔的带通滤波器组求倒谱即可得到梅尔倒谱。

    对前述通过预加重、分帧、加汉明窗,离散时间傅里叶变换后得到的频谱变换到梅尔频率域,然后施加等间距的三角形滤波器,对滤波后的结果进行求和和取对数操作,即可得到梅尔频率尺度下的功率谱。

    仔细观察该功率谱的结果,对于同一个词来说,在谱上的图样具有明显的图形特征。我们想到可以将其视为图片,然后利用图片的分类技术对语音进行识别。
    传统上的语音识别特征会继续用离散余弦变换求出倒谱系数。余弦变换是傅里叶变换的一个变种,其结果是实数,没有虚部。离散余弦变换还有一个特点是,对于一般的语音信号,这一步的结果的前几个系数特别大,而后面的系数比较小,就可以忽略。这样就可以对特征进行降维,只需要十几个系数即可作为特征。这对于使用传统的模式识别的方法来说特别重要。
    因为无论是支持向量机、高斯混合模型都需要数据的特征在各个维度上是无关的。而不必要的高维度必然导致特征的相关性,从而使得决策面变得扭曲。但使用深度神经网络的方法则不用担心这一点,因为它可以拟合出任意决策面。
    至此完成了对特征的提取,接下来只需要根据特征图样对进行分类即可。
    2.4 识别模型2.4.1 模型构建我使用了 128 个梅尔滤波器,并使用 30ms 作为分帧长度,以及 10ms 的重叠,因此最终得到的图样大小为 128 × 98。
    我仿照 VGGNet 的设置,使用大小为 3 × 3 的卷积核,并构建了 8 层深度的卷积神经网络。因为经过每一层卷积之前都会对图片进行 1 × 1 的补 0 操作,所以对任意大小的图片都可以进行卷积。在经过卷积网络后,我使用一个多层感知器来作为最后的分类器。
    其具体结构如下:

    为了避免过拟合,我对多层感知机的神经元进行概率为 0.5 的 Dropout,并在每一层的卷积操作和非线性函数之间对进行批归一化。
    2.4.2 数据加载考虑到对模型进行训练和测试的复杂性,需要多种数据的加载方式。首先是对数据集进行划分,我需要将一部分同学的所有语音单独拿出来作为测试集,因为最终测试的时候的说话人并不在我们的训练数据中。其次是因为数据量有限,每个词只有不到 600 个的训练数据,为此我对数据进行了交叉验证的划分,采用 10 折交叉的数据划分方式。我利用 pytorch 提供的 Dataset 和 Dataloader 模块,编写了自己的数据加载器。
    2.4.3 模型训练利用 pytorch 提供的自动反向传播,利用交叉熵损失函数作为优化目标,对模型进行了训练。在一台配置为只需要迭代 5 遍测试数据集即可将模型训练至收敛。

    绘制出训练的损失曲线如下图:

    训练结束后,在训练集上能取得 99.5% 的正确率,而对于不在训练集中的说话人,也能取得98.0% 的正确率。说明该模型的确具有非常好的泛化能力。接下来我编写了交互界面,用于进行直接录音测试。
    2.5 识别交互为了方便测试系统的语音识别效果,我基于我之前设计的录音采集系统进行了修改,使得我的系统可以在任何兼容的浏览器上进行测试,避免了为了测试而配置一系列复杂的环境。
    2.5.1 前端界面基于 Web APIs 技术,通过浏览器获取用户的麦克风进行音频录制。完成录制之后通过 JQuery框架使用 ajax 技术进行语音的上传和获取识别结果,再通过 Vue 框架实现的自动绑定,进行结果展示。这样可以实现不用刷新页面即可进行测试的效果,使得使用体验与桌面应用无异。

    2.5.2 服务器端为了能使用训练好的模型进行测试,我利用 flask 编写了服务端。当其接受到前端传回的语音信号后,就会构建模型,并载入训练好的模型参数,然后进行识别。完成识别后,再将结果传回到前端进行显示。
    3 总结在本项目中,我实现了一个识别效果较好的孤立词识别系统。在这一系列过程中,我编写了大量代码,即锻炼了自己的能力,又讲一学期学到的知识真正运用到了实践之中,加深了对知识的印象和理解。
    2 评论 22 下载 2019-04-18 11:27:58 下载需要12点积分
显示 90 到 105 ,共 15 条
eject