作业基本信息
这个作业属于哪个课程 | 软工实践2022年春-F班 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
这个作业的目标 | 完成对冬奥会的赛事数据的爬取(仅供学习使用),并实现一个能够对国家排名及奖牌个数统计的控制台程序。 |
其他参考文献 | 工程师的能力评估和发展 、单元测试和回归测试 |
目录
一、GitCode项目地址
链接: 点击此处进入.
二、PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 20 | 20 |
Development | • 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 20 | 30 |
• Design Spec | • 生成设计文档 | 10 | 10 |
• Design Review | • 设计复审 | 10 | 40 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 30 |
• Design | • 具体设计 | 10 | 45 |
• Coding | • 具体编码 | 300 | 360 |
• Code Review | • 代码复审 | 60 | 120 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 300 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 20 | 20 |
• Size Measurement | • 计算工作量 | 10 | 15 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 15 | 40 |
合计 | 615 | 1010 |
三、解题思路描述
1、第一步
首先题目指明了需要拥有2月15日之后的赛程数据,由于没有要求运行时获取,因此可以利用网页的元素检查获取2月15日之后的赛程数据并保存在json文件中。
2、第二步
由于取消了使用第三方库的限制,接着就是去寻找一个可以解析json数据的库。一开始使用的为jsoncpp,大致完成程序之后发现,jsoncpp存在内存泄漏的可能。这可能是由于导入的jsoncpp版本较老的问题。后来经由同学建议,改换成rapidjson。
3、第三步
完成前置工作之后,就要开始考虑数据的存储方式以及规划代码的模块,首先需要完成最主要的两个功能——输出奖牌总榜和每日赛程信息,接着再考虑细枝末节的地方,例如流程控制等,最后进行代码的优化。
四、接口设计和实现过程
1、本程序的主要功能大致可拆分成两个模块:
①数据的存储
主要涉及三个类:JsonDataSave
、TotalStatistic
、DailyEventsStatistic
,分别用来保存从文件中读取的信息、每个国家获取的奖牌情况、每日赛程信息。其中JsonDataSave
还保存了文件中的关键信息。
②用户命令的解析
主要涉及JsonAnalysis
类和InputAnalysis
函数,前者用于存储文件名和指示输出,后者用于对用户的指令进行分析和处理并返还给JsonAnalysis
中负责处理输入的函数一个结果。
2、程序的具体实现过程如下:
这个程序是建立在基础数据之上进行分析,最开始要做的,当然是对数据进行解析。利用jsoncpp(后换为rapidjson,实现方式如出一辙),截取下重要的信息,封装在TotalStatistic
类和DailyEventsStatistic
类中,接着分别在各自数据存储的类中重载运算符,实现输出格式化。由于一开始直接输出到文件并不利于测试,于是在完成项目之前,都输出到控制台中方便观测。据此完成两个功能之后,就要开始对用户命令进行解析。
命令解析分为两步:第一步骤当然是能正确识别指令并能做出正确的反馈,第二步骤是排除错误的指令并能输出错误所在。因此我建立InputAnalysis
函数用于处理用户输入的命令并返回指令的正确含义或者错误信息,接着在JsonAnalysis
类中据此返回结果实现正确的输出。该步骤简单但不容易,需要考虑命令中参杂的各种空格或者制表符(罪大恶极 ),还需要注意输出的格式。完成此项后程序的大致功能得以实现,接着完成文件的输入输出以及改为命令行程序即可。
该程序一共有4个类、10个函数(不包括构造函数或重载),类功能在上文有提及,函数中有2个为非类成员函数,用于对用户命令进行解析和输出出错信息,余下的功能如下:统计指令条数(用于控制输出格式)、根据解析结果指示输出、显示奖牌总榜、显示指定日期的赛程、清空文件内容、纠正命令格式、解析json数据并存入关键信息(奖牌总榜信息和每日赛程信息)。
3、程序的流程图如下:
五、关键代码展示
个人认为本程序中的关键代码有如下几块:
1、数据解析
//功能:存储指令为schedule时输出文件的内容
//入口参数:无
//返回:无
void JsonData::ScheduleOutPueSave()
{
DailyEvents dailyevents;
for (int d = 0; d < DAY_SUM; d++)
{
Document document;
if (document.Parse(this->schedule[d].c_str()).HasParseError())
{
throw "解析数据文件出错!\n";
}
const rapidjson::Value& matcharray = document["data"]["matchList"];
int matchsum = matcharray.Size();
for (int i = 0; i < matchsum; i++)
{
rapidjson::Value::ConstMemberIterator time = matcharray[i].FindMember("startdatecn");
rapidjson::Value::ConstMemberIterator sportname = matcharray[i].FindMember("itemcodename");
rapidjson::Value::ConstMemberIterator title = matcharray[i].FindMember("title");
rapidjson::Value::ConstMemberIterator homename = matcharray[i].FindMember("homename");
rapidjson::Value::ConstMemberIterator awayname = matcharray[i].FindMember("awayname");
rapidjson::Value::ConstMemberIterator venuename = matcharray[i].FindMember("venuename");
dailyevents = DailyEvents(time->value.GetString(), sportname->value.GetString(), title->value.GetString(),
homename->value.GetString(), awayname->value.GetString(), venuename->value.GetString());
this->scheduleoutput[d] = this->scheduleoutput[d] + dailyevents;
if (i != matchsum - 1)
{
this->scheduleoutput[d] = this->scheduleoutput[d] + "\n";
}
}
}
}
该处代码主要是利用rapidjson来解析json数据,在程序一开始时,我便将json文件中的数据读取出并保存(如上代码是将各个赛程的数据保存在schedule[]
中),解析完之后再将关键信息保存在scheduleoutput[]
中作为schedule命令的输出。该部分的解析方式固定,难度不高。解析奖牌总榜信息的方法与之类似,此处不再赘述。
2、命令解析
//功能:对用户输入的命令进行处理
//入口参数:字符串(用户输入命令)、ofstream(输出文件)、
// 布尔型(指示是否为最后一条指令)、JsonData类对象(存储json数据相关内容)
//返回:字符串(用户指令数据)
string InputAnalysis(string input, ofstream& outfile, bool islast, JsonData& jsondata)
{
if (input._Equal(INPUT_TOTAL))
{
return INPUT_TOTAL;
}
else if (input._Equal(INPUT_SCHEDULE))
{
return CopeErrorMessage(ERROR_SPELLINGMISTAKE, outfile, islast, jsondata);
}
else if (input.substr(0, 9)._Equal(INPUT_SCHEDULE))
{
string inputdate = input.substr(9, input.length());
inputdate=inputdate.erase(inputdate.find_last_not_of(" ") + 1).erase(0, inputdate.find_first_not_of(" "));
//cout << "输出:|" << input.substr(0, 9) << inputdate << "|\n";
if (inputdate.length() == MAX_DATE)
{
for (int i = 0;i < inputdate.length();i++)
{
if (!isdigit(inputdate[i])) //输入的时间中有字母即为错误指令
{
return CopeErrorMessage(ERROR_OUTOFRANGE, outfile, islast, jsondata);
}
}
}
else
{
return CopeErrorMessage(ERROR_OUTOFRANGE, outfile, islast, jsondata);
}
int date = atoi(input.substr(9, input.length()).c_str());
if (date>=202&&date<=220) //分别为冬奥开始日期和结束日期
{
return inputdate;
}
else
{
return CopeErrorMessage(ERROR_OUTOFRANGE, outfile, islast, jsondata);
}
}
else
{
return CopeErrorMessage(ERROR_SPELLINGMISTAKE, outfile, islast, jsondata);
}
}
解析用户命令简单但不容易,需要考虑到各种情况。指令为total时只需考虑其前后存在空格或制表符的问题,但指令为schedule时,还需注意schedule与时间中间存在多个空格或者制表符的情况。于是在解析一开始,我就将命令中的所有制表符转变为空格,之后去掉命令中前后多余的空格(此步操作在全局控制的函数中),此时若指令为total且输入无误,可以正常识别。接着就是让schedule命令可以正常运作。首先截取命令前九位,若不为“schedule(此处空格)”,则抛出"ERROR"信息,如果符合,就截取第十位至末尾,去掉其前后空格,若此时指令无指令输出错误的情况,则可以得到一个时间(或者一堆乱七八糟的字符),对该字符串进行判断,不为四位、参杂其他字符或者不在冬奥范围内,抛出"N/A"信息,否则返回一个时间,以此来确定输出内容。其中CopeErrorMessage
用于输出错误信息。
3、全局控制
//功能:对用户指令进行解析反馈
//入口参数:无
//返回:无
void OlympicSearch::CommandAnalysis()
{
JsonData jsondata;
string line = "";
ifstream readfile(inputfile, ios::in);
ofstream outfile(this->outputfile, ios::app);
if (readfile && outfile)
{
int i = 1;
int sum = CountCommand();
while (getline(readfile, line))
{
line = CorrectFormat(line);
if (line.erase(line.find_last_not_of(" ") + 1)._Equal(""))
{
continue;
}
this->commanding++;
if (this->commanding == sum)
{
this->islast = true;
}
//cout << "第" << i << "条指令执行结果:\n";
string result = InputAnalysis(line, outfile, this->islast, jsondata);
//cout << result;
if (result._Equal(ERROR_SPELLINGMISTAKE)||result._Equal(ERROR_OUTOFRANGE))
{
}
else if(result._Equal(INPUT_TOTAL))
{
ShowTotalAnalysis(jsondata,outfile);
}
else
{
ShowSchedule(result,jsondata,outfile);
}
i++;
}
if (jsondata.bufferstorage != "")
{
outfile << jsondata.bufferstorage;
}
outfile.close();
}
else
{
cout << "存在未创建的文件名,请重新输入!\n" << endl;
}
readfile.close();
}
该部分代码主要控制程序的流程,并指示文件的输出内容,其运作流程如第四部分中流程图所示。其中CountCommand
、CorrectFormat
函数分别用于统计命令条数以及纠正命令格式。
六、性能改进
1、更改第三方库
一开始我使用的是jsoncpp,但在大致完成程序之后进行测试发现,跑完2000条指令居然需要整整三分半钟,而且更不可思议的是,运行过程中C盘少了20多个G。不是我有问题就是jsoncpp有问题,但我能力有限,找不出jsoncpp哪里有问题,行吧,那就默认我有问题。在同学的建议下,换成了rapidjson,运行过程之中一切正常,跑完2000条指令需1分钟15秒左右。
2、将json数据预先读入
文件的输入输出占了程序很大一部分的运行空间,发现在每一次运行过程之中,每条指令要输出都需要先加载对应的json文件,那么何不在一开始就进行加载,将json数据保存进string中,这样在需要的时候就不用再打开json文件进行那一堆繁琐的操作了。进行此操作后,跑完2000条指令只需30秒左右。
3、事先加载输出内容以及输出调整
既然一开始都加载完json数据了,那么总体20条指令的输出不是也没有变化了吗?于是可以很容易的想到,我们可以在存储json数据的时候,顺便解析对应命令应输出的内容并保存,在解析完指令时,若指令正确,直接输出相应的内容,就省去了在运行每条指令的时候还需要对json数据进行解析的过程,此时还有一个发现,便是每条命令的执行内容写入文件都需要打开一次文件,造成了大量的时间浪费,将文件改成在全局控制的函数中打开,对需要打开文件的函数传入文件输出流的参数即可。由于性能优化很多,跑完2000条指令在1秒左右,但为了进一步优化,将指令改为20000条,此时跑完需要10秒左右,不过相应的内存使用。由于string类存在存储上限,个人认为该方法在大量数据处理的时候可能会崩溃,而且并不适应数据实时变化的情况(此次作业数据固定且不强制从网络上实时获取),该程序在数据结构方面仍有很大的改进空间。
4、分批次输入文件
思路来源:
打开文件需要时间,写入文件当然也会占用时间!于是我们可以控制输入文件的次数,设定一个大小 ,当我们存储在程序中需要输入文件的内容超出此设定的大小后再进行文件的输入,大幅度提高了效率。进行此项优化后,跑完20000条指令仅需2秒左右,当然代价是内存使用进一步提升。
5、优化前后CPU使用率对比
七、单元测试
1、程序覆盖率
2、单元测试内容
JsonData jsondata;
OlympicSearch olympicsearch("input.txt","output.txt");
ofstream outfile("outputfile", ios::app);
//验证指令处理
string r1 = "ERROR";
string r2 = "N/A";
string c1 = "total";
Assert::AreEqual(r1, InputAnalysis("totall", outfile, true, jsondata)); //后两者为无关参数
Assert::AreEqual(r1, InputAnalysis("schedule", outfile, true, jsondata));
Assert::AreEqual(r1, InputAnalysis("schedule0122", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule 0122", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule asda", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule total", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule a0202", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule 0203a", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule a215", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule 215a", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule 215", outfile, true, jsondata));
Assert::AreEqual(r2, InputAnalysis("schedule aaa", outfile, true, jsondata));
//参杂制表符与空格测试
Assert::AreEqual("schedule a125", olympicsearch.CorrectFormat(" schedule a125 ").c_str());
Assert::AreEqual("schedule 0202",olympicsearch.CorrectFormat(" schedule 0202").c_str());
Assert::AreEqual("schedule 0202", olympicsearch.CorrectFormat(" schedule 0202 ").c_str());
Assert::AreEqual("0202", InputAnalysis("schedule 0202", outfile, true, jsondata).c_str());
Assert::AreEqual("schedule 0203", olympicsearch.CorrectFormat(" schedule 0203 ").c_str());
Assert::AreEqual("schedule 0204", olympicsearch.CorrectFormat(" schedule 0204 ").c_str());
Assert::AreEqual("schedule 0205", olympicsearch.CorrectFormat(" schedule 0205").c_str());
Assert::AreEqual("schedule 0206", olympicsearch.CorrectFormat("schedule 0206 ").c_str());
Assert::AreEqual("total", olympicsearch.CorrectFormat(" total").c_str());
Assert::AreEqual(c1, InputAnalysis("total", outfile, true, jsondata));
Assert::AreEqual("total", olympicsearch.CorrectFormat(" total ").c_str());
Assert::AreEqual("total", olympicsearch.CorrectFormat("total ").c_str());
outfile.close();
ifstream outputfile1("C:\\Users\\86136\\Desktop\\OlympicSearch\\UnitTest\\Debug\\output.txt", std::ios::in);
if (!outputfile1.is_open())
{
cout << "打开文件失败!\n";
return;
}
ostringstream stream1;
stream1 << outputfile1.rdbuf();
string string1 = stream1.str();
outputfile1.close();
ifstream outputfile2("C:\\Users\\86136\\Desktop\\OlympicSearch\\UnitTest\\Debug\\output1.txt", std::ios::in);
if (!outputfile2.is_open())
{
cout << "打开文件失败!\n";
return;
}
ostringstream stream2;
stream2 << outputfile2.rdbuf();
string string2 = stream2.str();
outputfile2.close();
Assert::AreEqual(string1, string2);
八、异常处理
1、文件读取异常
2、数据文件出错
3、文件不存在
4、命令行参数错误
九、心得体会
本次作业难度不高,但要求我们对细节处理方面做的完善,例如输出格式、代码优化等等,总而言之还是很有意义的一次作业。
在编码过程中,我不仅学会了gitcode的使用方法,还学会了用第三方库去解析json数据,将重要信息读取下来;在优化过程中,让我更深刻地了解了I/O操作对程序效率的影响;在代码测试中,我更加熟练地编写了测试数据,并且初步了解了单元测试该如何去进行……
这次作业同样地加深了同学之间、同学与老师之间的交流,在流程控制、代码测试、代码优化等方面与同学或老师进行了讨论,得出了最适合的程序方法和优化方式。当然印象最深的是代码测试和优化方面——一定要充分考虑各类情况的发生,永远不要相信用户的输入!
虽然这次任务的完成不是那么一帆风顺,但也是这不顺利的过程才能使我们学到更多。