这个作业属于哪个课程 | <软件工程实践2022年春-F班> |
---|---|
这个作业要求在哪里 | <软件工程实践第二次作业——个人实战> |
这个作业的目标 | <完成对冬奥会的赛事数据的爬取(仅供教学使用),并实现一个能够对国家排名及奖牌个数统计的控制台程序。> |
其他参考文献 | CSDN、博客网 |
目录
一、Gitcode项目地址
项目地址:栩xx的项目地址
二、PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 20 | 20 |
Development | • 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 30 | 30 |
• Design Spec | • 生成设计文档 | 30 | 30 |
• Design Review | • 设计复审 | 20 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 25 | 45 |
• Design | • 具体设计 | 30 | 30 |
• Coding | • 具体编码 | 180 | 240 |
• Code Review | • 代码复审 | 60 | 60 |
• Test | • 测试(自我测试,修改代码,提交修改) | 200 | 300 |
Reporting | 报告 | ||
• Test Report | • 测试报告 | 10 | 10 |
• Size Measurement | • 计算工作量 | 20 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 45 | 60 |
合计 | 670 | 835 |
三、解题思路描述
1.问题1 数据的获取((仅供教学使用))
本次项目所需要的数据分为两部分,一部分是奖牌数据(total.json),另一部分是赛程数据(xxxx.json),即每日比赛数据。赛程数据从0202到0215都是采用老师所给的数据,剩下则是通过谷歌浏览器自带的检查里的internet功能通过关键字搜索,找到对应的json数据,直接爬取。
2.问题2 json的解析
json数据的解析则是采用C++的第三方库rapidjson(一开始是采用jsoncpp,但后面可能是使用的版本过低,导致内存泄漏,电脑直接卡死),通过使用rapidjson库自带的函数来解析数据。
3.问题3 文件的读取和输出
文件的读取和输出都是采用C++的流操作,使用了ofstream和ifstream。
4.问题4 功能实现
主要功能为两个,一个是输出总奖牌数据,另一个则是输出指定日期的赛程数据,将两个功能包装成两个函数。
5.各种参考资料的获取
资料获取:CSDN、博客园、百度
三、接口设计和实现过程
代码共分为两部分,一部分为工具代码(Lib.h和Lib.cpp),另一部分为主程序(OlympicSearch.cpp)。在本次实践中,经过一系列的需求分析后,决定采用简单工厂设计模式来完成本次作业(因为本次作业就是根据各种指令的输入来决定相应的输出,符合简单工厂的设计思想),所以Lib中只有一个类,将两个功能模块包装成两个成员函数,调用时直接类对象.功能就行。
Lib.h如下
//主要程序类
class OlympicProgram {
private:
string input;
string output;
string totalout;//用来保存指令为total时的输出
string scheduleOut[19];//用来保存指令为schedulexxxx的输出
public:
OlympicProgram(string input,string output);
void resolveInput();//根据输入的指令决定输出
string outputTotal();//获取total奖牌信息
string outputSchedule(string date);//获取指定日期赛程信息
void outputWrong(string wrong);//输出错误信息
};
string solveOrder(string order);//处理指令
bool isTrueDate(string date);//判断日期的正确性
string ReadFile(string filename);//将读取的文件转为string
int trimSpace(string& order);//将指令中的空格去除
程序流程图:
程序流程解释:
1.在主程序OlympicSearch的main方法中将获取到的命令行参数作为参数用来直接声明OlympicProgram对象,并直接调用resolveInput()的方法。
2.申明对象的时候,构造函数中共要给四个成员进行赋值,其中input和output直接从传入的参数获取,而totalOut则是调用outputTotal()方法直接从文件中获取,scheduleOut成员是一个数组,则通过for循环调用outputSchedule()方法获取。
3.在resolveInput()方法中,则是通过文件读取流,使用getline+for循环一行行获取指令并调用solveOrder()方法对指令进行处理,获取其返回值,根据返回值的不同来决定输出。
4.solveOrder()方法是用来处理指令,即对指令进行反馈,调用isTrueDate()和trimSpace()方法来实现。
四、关键代码展示
//入口:处理输入的文件
//返回值:无
//作用:根据所输入的信息调用不同的函数来处理
void OlympicProgram::resolveInput()
{
ifstream myfile(this->input,ios::in);
string temp;
if (!myfile.is_open())
{
cout<< "未成功打开文件" << endl;
return;
}
ofstream file_writer(this->output, ios_base::out);//第一次运行时先将output.txt清空
ofstream outfile(this->output, ios::app);
string outBuf;//设置输出缓冲区
for (int i = 0; getline(myfile, temp); i++)
{
string order = solveOrder(temp);
if (order._Equal("space"))
{
i--;
continue;
}
if (i != 0)
{
outBuf += "\n";
}
if (order._Equal("total"))
{
outBuf += this->totalout;
}
else if(order._Equal("N/A")||order._Equal("ERROR"))
{
outBuf += outputWrong(order);
}
else
{
int datenum = atoi(order.c_str());
outBuf += this->scheduleOut[datenum - 202];
}
if (outBuf.size() > (1024 * 1024 * 5))
{
outfile << outBuf;
outBuf = "";
}
}
if (outBuf != "")
outfile << outBuf;
myfile.close();
outfile.close();
}
解释思路与注释说明: 先读取文件input.txt,然后通过getline一行一行读取指令,调用solveOrder对指令进行处理,根据其返回值控制输出。输出由outBuf来控制,将要输出的内容加到outBuf上,当outBuf大于5m时,再输出。
2.
//入口:无
//返回值:string字符串
//作用:获取指令为total时的输出
string OlympicProgram::outputTotal()
{
Document d; //文档树
if (d.Parse(ReadFile("./src/data/total.json").c_str()).HasParseError())
cout << "解析错误\n" ;
const rapidjson::Value& data = d["data"]; //data成员
const rapidjson::Value& medalsList = data["medalsList"]; //medalsList成员
string os;
string backtotal;
for (unsigned int i = 0; i < medalsList.Size(); i++)
{
//必要的临时储存变量指针
rapidjson::Value::ConstMemberIterator rank = medalsList[i].FindMember("rank");
rapidjson::Value::ConstMemberIterator countryid = medalsList[i].FindMember("countryid");
rapidjson::Value::ConstMemberIterator gold = medalsList[i].FindMember("gold");
rapidjson::Value::ConstMemberIterator silver = medalsList[i].FindMember("silver");
rapidjson::Value::ConstMemberIterator bronze = medalsList[i].FindMember("bronze");
rapidjson::Value::ConstMemberIterator count = medalsList[i].FindMember("count");
os =(string) "rank" + rank->value.GetString()+ ':' + countryid->value.GetString() + '\n'
+ "gold:" + gold->value.GetString() + '\n'
+ "silver:" + silver->value.GetString() + '\n'
+ "bronze:" + bronze->value.GetString() + '\n'
+ "total:" + count->value.GetString() + '\n'
+ "-----";
if (i != medalsList.Size() - 1)
os += '\n';
backtotal += os;
}
return backtotal;
}
解释思路与注释说明: 直接读取total.json文件,通过调用rapidjson里的方法来解析数据,通过for循环,依次获取并加到backtotal这个变量中,最后直接返回backtotal。outputSchedule()方法代码类似。
3.
//入口:错误信息
//返回值:string类型字符串
//作用:错误指令时的输出
string OlympicProgram::outputWrong(string wrong)
{
string os = wrong + '\n' + "-----";
return os;
}
解释思路与注释说明: 直接输出错误信息
4.
//入口:读取到的指令
//返回值:对指令的做出回应信息
//作用:处理输入的指令
string solveOrder(string order)
{
string t = order;
int size= trimSpace(t);
if (t._Equal("total")&&size==1)
{
return "total";
}
else if (t.substr(0, 8)._Equal("schedule"))
{
if (size == 2)
{
string date = t.substr(8, order.size() - 8);
if (!isTrueDate(date))
{
return "N/A";
}
return date;
}
else if(size==1)
{
return "ERROR";
}
else
{
return "N/A";
}
}
else if (t.empty())
{
return "space";
}
else
{
return "ERROR";
}
}
解释思路与注释说明: 调用trimspace函数对指令先进行操作(将空格都去掉,并统计原字符串个数),接着根据各种指令做出一系列返回。
5.
//入口:string类型的日期
//返回值:bool类型表示正误
//作用:判断日期是否正确
bool isTrueDate(string date)
{
if (date.size() != 4)
return false;
int length = date.length();
for (int i = 0; i < length; i++)//检测是否全为数字
{
if (!isdigit(date[i]))
return false;
}
int datenum = atoi(date.c_str());
if ( datenum > 220 || datenum < 202)
return false;
else
return true;
}
解释思路与注释说明: 先判断输入的日期是否为4位,再判断四位是否都为数字,接着判断日期是否在正确的范围内
6.
//入口:输入的文件名、输出的文件名
//返回值:无
//作用:初始化对象
OlympicProgram::OlympicProgram(string input, string output)
{
this->input = input;
this->output = output;
this->totalout=outputTotal();
string date;
for (int i = 202; i <= 220; i++)
{
date = "0" + to_string(i);
int datenum = atoi(date.c_str());
this->scheduleOut[datenum - 202] += outputSchedule(date);
}
}
解释思路与注释说明: 直接在构造函数中调用outputSchedule和outputTotal两个方法,将所有指令对应的输出放入到程序中。测试的时候也可以直接输出outTotal或outputSchedule来验证
五、性能改进
以2万条正确指令(重复total,schedule0202-0220共1000次)做测试,输入文件input.txt大小为286KB,输出文件output.txt为74233KB
改进1:将jsoncpp改为rapidjson(减少解析json时间以及减少使用内存)
改进前:电脑直接卡死,不断减少内存并且不释放
改进后:时间13分14秒,运行内存基本在2.4m以内,CPU占比最大的为IO输出
改进2:第一次读取json文件时从文件中读取后直接存入程序中,后面再遇到相同的指令就直接输出(即减少文件读取,优化IO)
改进后:时间1分08秒,运行内存基本在1.0m以内
改进3:在第二次改进的基础上,一开始就在构造函数里将所有指令对应的输出存入程序中,后面输出时就不需要判断当前程序中是否存在已有的指令输出,可以省去一部分if判断语句,如果指令较少的情况有可能会稍微慢点,但基本不影响,在大量指令的时候,则速度可以得到一部分提升。
改进后:时间42s,运行内存基本在1.2m以内
CPU,GPU
改进4:在第三次的CPU和GPU性能分析报告时,发现文件的占比较大,因此修改了代码中不必要的文件操作,原先是输出一次,就开一次文件输出流然后关闭,现在就只开一次文件输出流,等所有指令都输出完之后再关闭。即减少文件操作。
改进后:时间18s,内存基本在1.2m以内
CPU,GPU
改进5:在第四次的CPU和GPU性能分析报告时,想到操作系统里的空间换时间的想法,所以设置了一个变量用来存当前的输出,当超过一定的大小(不能太大,也不能太小,我这里是5m输出一次)再输出,就是设置一个缓冲区。
改进后:时间2s,内存基本在7.9m以内
六、单元测试
程序(exe文件)覆盖率:
单元测试:
1.程序输出文件和正确输出文件进行测试(即output.txt)
2.日期判断函数(isTrueDate())的测试
3.处理指令函数(solveOrder())的测试
样例:
对测试的评价:感觉自己的测试可能还不够完全,所有可能出现的特殊情况可能还会有些没考虑到,但大部分可能出现的错误都已测试通过,关于输出文件的测试则是写代码直接比对两个文件是否相同
七、异常处理
1.文件读取失败
2.json解析错误
3.命令行参数错误
4.容错性处理
指令中可以出现大量空格或者制表符
指令之间有空行则直接跳过空行
八、心得体会
1.复习了C++的语法(因为太久没写C++,所以这次作业才用C++)
2.git真香!!!之前都没用过git,删掉的代码有时想要撤回,却发现撤回不了,而git随时可以回档,真的好用。
3.在性能优化和单元测试方面,在我整个实践中是花时间最长的模块,性能优化让我学习到很多知识,例如如何优化IO输出输入。在优化过程中还借助了操作系统的知识,对空间换时间有了进一步的理解(虽然只在输出时用到)。单元测试是第一次接触到,之前写代码时的测试都是简单的输入输出的测试,没有现在这么严格。
4.学会了jsoncpp和rapidjson的使用,也明白了不能因为偷懒图方便,就用老版本的第三方库,jsoncpp老版本的使用非常简便,但是存在内存泄漏的问题(电脑差点挂了),后面直接换成了rapidjson。
5.对VS2019的使用更加熟练了,之前写C++和C都是DEV写的,但是DEV的功能较少,还是用VS2019更好用。