软件工程实践第二次作业——个人实战

这个作业属于哪个课程<软件工程实践2022年春-F班>
这个作业要求在哪里<软件工程实践第二次作业——个人实战>
这个作业的目标<完成对冬奥会的赛事数据的爬取(仅供教学使用),并实现一个能够对国家排名及奖牌个数统计的控制台程序。>
其他参考文献CSDN、博客网

一、Gitcode项目地址

项目地址:栩xx的项目地址

二、PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划
• Estimate• 估计这个任务需要多少时间2020
Development• 开发
• Analysis• 需求分析 (包括学习新技术)3030
• Design Spec• 生成设计文档3030
• Design Review• 设计复审2020
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2545
• Design• 具体设计3030
• Coding• 具体编码180240
• Code Review• 代码复审6060
• Test• 测试(自我测试,修改代码,提交修改)200300
Reporting报告
• Test Report• 测试报告1010
• Size Measurement• 计算工作量2020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划4560
合计670835

三、解题思路描述

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更好用。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值