• 福大软工1816 · 第五次作业


    福大软工1816 · 第五次作业 - 结对作业2

    Github项目地址

    结对信息

    031602148 朱文婧
    031602336 肖地秀

    PSP表格

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划 20 20
    · Estimate · 估计这个任务需要多少时间 20 20
    Development 开发 825 1235
    · Analysis · 需求分析 (包括学习新技术) 30 30
    · Design Spec · 生成设计文档 15 15
    · Design Review · 设计复审 10 10
    · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 30
    · Design · 具体设计 60 120
    · Coding · 具体编码 500 580
    · Code Review · 代码复审 30 30
    · Test · 测试(自我测试,修改代码,提交修改) 150 420
    Reporting 报告 95 135
    · Test Repor · 测试报告 20 20
    · Size Measurement · 计算工作量 15 15
    · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 100
    合计 940 1390

    解题思路描述与设计实现过程

    思路介绍

    本次作业的基础编码要求是对个人项目的程序增加新的功能。

    • 个人项目中已实现的功能有:

      • 命令行参数读取;
      • 统计非空白行;
      • 统计字符数;
      • 词频统计并字典序输出。
    • 本次作业需新增的功能有:

      • 自定义输入输出文件;
      • 可自定词组长度的词组词频统计;
      • 具有权重的词频统计;
      • 自定义输出的单词数量;
      • 混合命令行参数读取。

    以下是针对各新增功能的思路介绍:

    • 针对第一点自定义输入输出文件和第五点混合命令行参数读取:
      这两点都需要通过处理命令行参数来实现。题目提到给出的自定义信息前都有-x作为提示,因此,只要在argc参数控制的循环中判断匹配-x信息,即可识别提取所需的命令行参数信息。个人项目中已实现了自定义输入文件,用提取出的命令行参数中的输出文件名即可实现自定义输出文件。
    • 针对第二点可自定义词组长度的词组词频统计:
      题目要求将词组的范围限定在Titl和Abstract的范围内,为了实现这一点,第一想法就是在读取文件的同时依次将每篇的Titl和Abstract的内容分别存到两个字符串titlabst内再进行处理。词组提取的实现听取了队友双重循环的建议,在外层循环检测到一个合法单词后进入内层循环,判断已这个单词为起始,是否存在合法词组并截取合法词组;在内层循环判断不存在时,用内层循环当前检测的位置更新外层循环,避免重复的判断消耗以减少循环时间。
    • 针对第三点具有权重的词频统计:
      同样是依次将Titl和Abstract的内容分别存至两个字符串中,一个titl_map,一个abst_map,分别保存从titl和abstract两个字符串中提取的单词(词组),之后再分别便利两个map,对提取的单词(词组)进行权重计算,并将结果存入word_map中输出。
    • 针对第四点自定义输出的单词数量:
      个人项目的程序中为实现字典序输出,采取的方式时先将词频全部导入一个vector中,对vector从大到小排序,最后遍历10遍map即可按词频大小输出字典序排序结果。为实现自定义单词输出数量只需要将10这一常数改为自定义变量n即可。

    爬虫使用

    使用工具:Python爬虫框架Scrapy

    具体思路:

    具体实现:

    • 编写item文件,根据需要爬取的内容定义爬取字段
    import scrapy
    
    class CvprItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        Title=scrapy.Field()
        Abstract=scrapy.Field()
        pass
    
    • 在spiders文件夹中创建一个Cvpr.py的文件,对其进行编写
    from scrapy.spiders import Spider
    from cvpr.items import CvprItem
    from scrapy import Request
    
    def parse_detail (response):
        item = CvprItem()
        item['Title'] = response.xpath('//div[@id="content"]//dd/div[@id="papertitle"]/text()').extract()
        item['Abstract'] = response.xpath('//div[@id="content"]//dd/div[@id="abstract"]/text()').extract()
    
        yield item
    
    class CvprSpider(Spider):
        name="cvpr"
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36',
        }
    
        def start_requests(self):
            url='http://openaccess.thecvf.com/CVPR2018.py'
            yield Request(url,headers=self.headers)
    
        def parse(self,response):
            links= response.xpath('//div[@id="content"]//dt//a/@href').extract()
            for link in links:
                link = 'http://openaccess.thecvf.com/'+link
                yield Request(link,headers=self.headers,callback=parse_detail)
    
    • 编写pipelines文件
    class CvprPipeline(object):
        def process_item(self, item, spider):
            fp=open('result.txt','w',encoding='utf-8')
            fp.write(item['Title'])
            fp.close()
            return item
    
    • settings文件设置(主要设置内容)
    BOT_NAME = 'cvpr'
    
    SPIDER_MODULES = ['cvpr.spiders']
    NEWSPIDER_MODULE = 'cvpr.spiders'
    
    
    # Crawl responsibly by identifying yourself (and your website) on the user-agent
    #USER_AGENT = 'cvpr (+http://www.yourdomain.com)'
    
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    
    • 执行命令,运行程序
    scrapy crwal Cvpr
    

    代码组织与内部实现设计(类图)

    • Statistics类封装各项统计操作;
    • File类封装了对文件的打开、关闭、写操作;
    • Parameter类封装混合命令行参数的识别、提取、检查操作;
      它们包含的具体操作以及彼此之间的关系见下图:

    关键算法及流程图展示

    权重词频统计和词组提取的设计和实现方法在思路介绍部分已经提到过了,这里给出实现的流程图:

    • 权重词频统计:

    • 词组提取:

    关键代码及说明介绍

    代码说明介绍见注释。
    命令行参数提取代码:

    /*已将将i、o、w、m、n的初值置为-1*/
    for (int j = 0; j <= argc - 1; j++)//提取命令行参数
    {
    	if (strcmp(argv[j], "-i") == 0)
    	{
    		i = j + 1;
    	}
    	if (strcmp(argv[j], "-o") == 0)
    	{
    		o = j + 1;
    	}
    	if (strcmp(argv[j], "-w") == 0)
    	{
    		w = atoi(argv[j + 1]);
    	}
    	if (strcmp(argv[j], "-m") == 0)
    	{
    		m = atoi(argv[j + 1]);
    	}
    	if (strcmp(argv[j], "-n") == 0)
    	{
    		n = atoi(argv[j + 1]);
    	}
    }
    if (o == -1 || i == -1 || w == -1)//检测输入的命令行参数是否正确
    {
    	cout << "输入参数错误!" << endl;
    	exit(1);
    }
    

    单词权重计算部分代码(titl_map在程序中被命名为trecordabst_map在程序中被命名为arecord):

        map<string, int>::iterator tit;
    	map<string, int>::iterator ait;
    	for (tit = trecord.begin(); tit != trecord.end(); tit++)//遍历titl_map
    	{
    		if (word.count((*tit).first))
    		{
    			word[(*tit).first] = (*tit).second * 10;//不会真正运行
    		}
    		else
    		{
    			word[(*tit).first] = (*tit).second * 10;
    		}
    	}
    	for (ait = arecord.begin(); ait != arecord.end(); ait++)//遍历abst_map
    	{
    		if (word.count((*ait).first))
    		{
    			word[(*ait).first] = word[(*ait).first]+ (*ait).second;
    		}
    		else
    		{
    			word[(*ait).first] = (*ait).second * 1;
    		}
    	}
    

    合法词组提取内层循环的部分代码,进入内层循环时,在外层循环中已经检测到一个合法单词并将该单词(已排除最后一个单词的情况)后的第一个分隔符作为内层循环的起始位置。给出的代码为无权重时,Title部分的合法词组提取:

    for (unsigned i = j; i < titl.length(); i++)
    {
    	if (i == titl.length() - 1 && ((titl[i] >= 'a'&&titl[i] <= 'z') || (titl[i] >= 'A'&&titl[i] <= 'Z')) && flag >= 3)
            /*对位于末尾的合法单词做特殊处理,满足该if条件且mark+1=m时截取词组,截取操作同下,不重复贴出*/
    	if ((titl[i] >= 'a'&&titl[i] <= 'z') || (titl[i] >= 'A'&&titl[i] <= 'Z'))
    	{
    		flag++;
    	}
    	else
    	{
    		if (flag == 0 && !(titl[i] >= 'a'&&titl[i] <= 'z') && !(titl[i] >= 'A'&&titl[i] <= 'Z') && !(titl[i] >= '0'&&titl[i] <= '9'))
    		{
    			continue;//遇单词间的分隔符跳过
    		}
    		if (flag < 4)//检测到不合法单词,退出内层循环并更新外层循环的位置至不合法单词后的分隔符上
    		{
    			for (unsigned k = i; k < titl.length(); k++)
    			{
    				if ((titl[k] >= 'a'&&titl[k] <= 'z') || (titl[k] >= 'A'&&titl[k] <= 'Z'))
    				{
    					flag = 0;
    					j = k - 1;
    					break;
    				}
    			}
    			break;
    		}
    		if (flag >= 4 && (titl[i] >= '0'&&titl[i] <= '9'))//合法单词后的数字数量无限制
    		{
    			continue;
    		}
    		mark++;//连续检测到合法单词
    		flag = 0;
    		if (mark - m == 0)//合法单词连续个数满足条件,截取
    		{
    			temp = titl.substr(star, (i - star));//截取合法词组
    			{
    				for (unsigned g = 0; g < temp.length(); g++)//将大写字母转为小写字母 
    				{
    					if (temp[g] >= 'A'&&temp[g] <= 'Z')
    					{
    						temp[g] = temp[g] + 32;
    					}
    				}
    				if (word.count(temp))//截取出的词组已记录
    				{
    					word[temp]++;//该词组频数加1
    				}
    				else
    				{
    					word[temp] = 1;//记录该词组
    				}
    			}
    			mark = 0;
    			flag = 0;
    			break;//跳回外层循环
    		}
    	}
    }
    

    附加题设计与展示

    设计的创意独到之处

    通过图表的形式直观展示的爬取信息的分析和处理结果。

    实现思路

    爬取作者信息至txt文件当中,再利用词频统计功能的代码对获取的信息进行处理统计,最后通过图表形式展示结果。

    关键代码及说明介绍:

    • 编写item文件,增加需要爬取的内容定义爬取字段:
    //作者名字
    authorName = scrapy.Field()
    
    • 在spiders文件夹已经创建的Cvpr.py的文件,增加需要的代码:
    author = re_h.sub('',str(soup.find('i'))).strip().split(', ')
    
    • 统计代码:
    void ReadText(vector< pair<string, int> >& arr)
    {
    	string s;
    	ifstream ff("authors.txt");
    	while (getline(ff, s))
    	{
    		if (s[0] == 'A')
    		{
    			vector<int> point1;
    			vector<int> point2;
    			for (int i = 0; i < s.length(); i++)
    			{
    				if (s[i] == ' ' && (s[i - 1] == ',' || s[i - 1] == ':')) point1.push_back(i);
    				if (s[i] == ',') point2.push_back(i);
    			}
    			point2.push_back(s.length() - 1);
    			for (int i = 0; i < point1.size(); i++)
    			{
    				string ss = "";
    				for (int j = point1[i] + 1; j < point2[i]; j++)
    				{
    					ss += s[j];
    				}
    
    				int flag = 0;
    				for (int j = 0; j < arr.size(); j++)
    				{
    					if (arr[j].first == ss)
    					{
    						arr[j].second++;
    						flag = 1;
    						break;
    					}
    				}
    				if (flag == 0)
    				{
    					pair<string, int> arr1;
    					arr1.first = ss;
    					arr1.second = 1;
    					arr.push_back(arr1);
    				}
    			}
    		}
    	}
    }
    

    实现成果展示

    爬虫程序爬取的结果:

    统计结果展示:

    性能改进和分析

    改进的思路

    • 之前代码的大小写转换操作是在读取文件的同时进行的,对文件的每一个字符都做了一遍检测,这次的代码将大小写转换的操作改到了截取出单词或词组之后,算是一个小改进吧;
    • 在提取合法词组部分,两个循环互相配合,外层循环检测到合法单词时才进入内层循环,内层循环在检测过程中一旦发现不存在合法词组,即将外层循环的查询位置更新,尽量减少双层循环的查询消耗;
    • 对程序中消耗最大的使用map保存单词词组这一实现,目前没有更好的实现想法,未做改进。

    性能分析图和消耗最大的函数

    • 选择了涵盖算法最多的权重词组统计功能进行了性能测试;
    • 测试时的命令行参数为WordCount.exe -i input.txt -o output.txt -w 1 -m 3 -n 10
    • 选择的测试文档为自己爬取的result.txt文件,文件大小为1.16MB,用时时8.321s。
      性能分析:

    消耗最大的函数:

    在w_phrase(in,m)中消耗最大的两个部分:

    单元测试

    编号 测试项目 测试结果
    1 测试能否实现命令行参数的识别和提取 通过
    2 测试命令行参数的错误判断 通过
    3 测试字符数、有效行数、单词数的统计 修改后通过
    4 测试Titl、Abstract范围内单词的提取 通过
    5 测试Titl、Abstract范围内词组的提取 通过
    6 测试单词权重的计算 通过
    7 测试词组权重的计算 通过
    8 测试输出单词数是否遵循自定义数n 通过
    9 测试对题目示例的统计 通过
    10 测试对复制了三遍的题目示例的统计 通过

    部分单元测试代码

    Parameter类中的get_Parameter函数(提取命令行参数函数)进行的单元测试代码:

    namespace UnitTest1//测试能否实现命令行参数的识别和提取
    {
    	TEST_CLASS(UnitTest1)
    	{
    	public:
    
    		TEST_METHOD(TestMethod1)
    		{
    			int argc=11;
    			char *argv[]{(char*)"WordCount.exe", (char*)"- i", (char*)"input.txt", (char*)"- o", (char*)"output.txt", (char*)"- w" ,(char*)"1", (char*)"- m", (char*)"3", (char*)"- n", (char*)"10" };
    			Parameter p;
    			p.set();
    			p.get_Parameter(argc,argv);
    			Assert::IsTrue(p.i == 2 &&p.o == 4 &&p.w == 1 &&p.m == 3 &&p.n == 10);
    		}
    	};
    }
    

    Statistics类中的w_phrase函数(有权重的词组统计)的单元测试代码:

    namespace UnitTest7//测试词组权重的计算
    {
    	TEST_CLASS(UnitTest1)
    	{
    	public:
    
    		TEST_METHOD(TestMethod1)
    		{
    			ifstream in;
    			in.open("test7.txt", ios::in);
    			Statistics s;
    			map<string, int>::iterator it;
    			s.set(in, 1, 2);
    			it = s.word.begin();
    			Assert::IsTrue((*it).first == "aaaa bbbb" && (*it).second == 11);
    			it++;
    			Assert::IsTrue((*it).first == "bbbb bbbb" && (*it).second == 1);
    		}
    	};
    }
    

    遇到的代码模块异常或结对困难

    在写代码、调试程序、测试结果的过程中遇到了很多大大小小的bug,结对过程中也还是存在着一些小困难,提几点让我印象比较深刻的:

    1.Bug1(代码部分)

    问题描述

    程序运行时异常中止,弹出错误提示。

    做过哪些尝试

    使用vs的调试工具,查看vs的报错原因。

    是否解决

    通过vs的调试找到了程序崩溃的原因——vector数组越界,检查了输出时运用map的代码段,下面是排错前的代码:

    for (int i = 0; i < wnum && i < n; i++)
    {
    	t = a[i];
    	for (it = word.begin(); it != word.end(); it++)
    	{
    		if ((*it).second == t)
    		{
    			outfile << "<" << (*it).first << ">: " << t << endl;
    			(*it).second = 0;
    			break;
    		}
    	}
    }
    

    其中,wnum是统计的单词数。在个人项目进行中时,我弄错了单词统计的规则,当时程序中的wnum是不同的单词的个数。在助教发布第一次测试结果之后,我更正了这个错误程序中的wnum变为了单词的总数,但是我在修改时遗漏了上段代码外循环的条件处的修改,因此在不同单词个数小于10且总的单词数大于不同单词数的这种情况下,就会出现vector越界的情况,将外层循环条件由i < wnum && i < n改为i < num && i < nnum为不同单词的个数)后问题解决。

    有何收获

    这个bug是上个项目的遗留问题,之前第二次测试能通过算得上是侥幸,问题产生的根本原因是一开始对题意理解的不正确,还有一个原因是在上次修改程序后没有再认真地进行单元测试,得到的教训是以后要更加认真读题理解题意,更加重视单元测试。

    2.Bug2(代码部分)

    问题描述

    和舍友用同一篇文档测程序,统计出的有效行数不一致。

    做过哪些尝试

    和舍友讨论,检查程序统计逻辑。

    是否解决

    在舍友的帮助下成功解决,舍友发现行数差距数恰好是文章的总篇数,之后发现我的程序统计行数更多的原因在于,我没有把仅含 的行忽略不计,修改程序后问题解决。

    有何收获

    有些错误的原因仅凭自己的思维逻辑可能是根本就想不到的,一方面是由于自己的知识局限,根本就不可能想到,另一方面是自己的思维固化,很难走到正确的路上,所以别人的帮助是很重要的,在此感谢舍友。

    3.Bug3(爬虫部分)

    问题描述

    写好爬虫之后运行,爬取的信息没有成功写入text文件里面,但是爬取的信息是正确的。

    做过哪些尝试

    上网百度,还去询问了已经出结果的同学,根据其他人给出的建议去修改代码。

    是否解决

    在反复挣扎之下,终于改出来了,喜悦。

    有何收获

    写的代码大部分情况下还是很少可能可以直接运行的,会出现非常多的bug,但是这个时候要静下心来反复纠错,有时候可以去寻求帮助,感谢这次帮助我的同学。

    4.Bug4(爬虫部分)

    问题描述

    爬取的文件已经生成了text里面,但是去统计词频的时候,和其他人的结果对不上,经过对比才知道,是写文件的时候编码方式不对,导致出现乱码。

    做过哪些尝试

    私聊其他人要结果进行对比,上网百度了解text的编码格式。

    是否解决

    在某同学的帮助下,知道爬虫的代码哪里需要改动,最后的确成功解决。

    有何收获

    格式非常重要,特别是涉及字符的格式,要特别小心,有时候格式也是非常致命的一个地方,感谢那个帮助我的同学。

    5.结对小困难

    问题描述

    还是两个人时间交错,对不上的困难。

    做过哪些尝试

    把这次的作业分块划分,各自找时间完成自己的部分,有问题一起讨论。

    是否解决

    这次作业的任务可以划分得挺散的,所以问题顺利解决,作业成功完成,合作中也可以更自由灵活地完成作业时间规划,合作的过程很愉快。

    有何收获

    一回生二回熟,有过第一次合作的经历和教训,第二次的合作明显顺利了很多。结对合作中,两人合拍,搭档靠谱也是很重要的。

    合作情况和感想

    具体分工

    • 我主要负责基于第二次作业代码功能的补充和修改以及这部分的博客内容;
    • 肖地秀在这次合作中主要负责爬虫和附加题部分以及这两部分的博客内容。

    评价队友

    需要改进的地方

    • 太忙了,交流沟通的时间少。我的队友在这段时间里
      有实验室和一些其他的工作要忙,我有一点点的羡慕,所以我们交流讨论的时间比较少,这次作业完成的还是有点仓促。不过这点好像不能改进。
    • 因为太忙有几次很迟或者漏回忘回我消息,留我苦苦等待,拿小本本记下来。

    值得学习的地方

    • 虽然很忙,但她还是能够较好地完成自己那部分的任务,并且也为我这部分任务的完成提供了很大的帮助和支持,要好好学习对友的效率。
    • 人好,心态好,很好交流相处。结对过程中我对友带给我的情绪和状态总是积极乐观的,所以虽然作业任务重又苦,但是在对友的影响下,我完成作业的心情总的来说还是不错的。要好好学习我对友的人格魅力。

    个人总结

    一个小小的总结和自我反省:在写博客计算PSP表的各项实际耗时时回顾了一下这次完成作业的整个过程,发现自己走了很多的弯路,花费了很多无用的时间,仔细想了之后原因大概以下几点:

    • 没有理解好需求,构思好算法的具体实现就匆忙动工,返工修改花的时间很多;
    • 和对友的沟通交流做得还是不够;
    • 有部分作业是国庆假期在家完成的,在家做作业的时间零散、效率低下,投入时间的回报不高。
      总之,还是需要吸取经验教训,再接再厉。

    Github代码签入记录

    附组织目录:

    031602148&031602336
     |- src
        |- WordCount.sln
        |- WordCount
            |- stdafx.cpp
            |- stdafx.h
            |- File.cpp
            |- File.h
            |- main.cpp
            |- Parameter.cpp
            |- Parameter.h
            |- Statistics.cpp
            |- Statistics.h
            |- WordCount.vcxproj
            |- WordCount.vcxproj.user
            |- WordCount.vcxproj.filters
     |- cvpr
        |- result.txt
        |- Crawler
    

    学习进度条(第五周)

    第N周 新增代码(行) 累计代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
    2 413 413 21 21 学用git;接触vs性能分析、单元测试功能;
    3 0 413 16.5 37.5 阅读《构建之法》;结对配合;学习NABCD模型;接触原型开发工具
    4、5 1061 1474 23.5 61 结对配合经验up;了解接触爬虫
  • 相关阅读:
    BeautifulSoup
    python爬虫操作excel
    requests
    urllib
    线程的基本用法
    WebView
    播放多媒体文件
    调用摄像头和相册
    03 pycharm编辑默认模板
    测试
  • 原文地址:https://www.cnblogs.com/z031602148/p/9756440.html
Copyright © 2020-2023  润新知