福大软工1816:爬取及统计(结对作业二)

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

Github项目地址

结对信息

031602148 朱文婧
031602336 肖地秀

PSP表格

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

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

思路介绍

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

  • 个人项目中已实现的功能有:
    • 命令行参数读取;
    • 统计非空白行;
    • 统计字符数;
    • 词频统计并字典序输出。
  • 本次作业需新增的功能有:
    • 自定义输入输出文件;
    • 可自定词组长度的词组词频统计;
    • 具有权重的词频统计;
    • 自定义输出的单词数量;
    • 混合命令行参数读取。

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

  • 针对第一点自定义输入输出文件和第五点混合命令行参数读取:
    这两点都需要通过处理命令行参数来实现。题目提到给出的自定义信息前都有-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类封装混合命令行参数的识别、提取、检查操作;
    它们包含的具体操作以及彼此之间的关系见下图:
    1092472-20181009223954589-2001638287.png

关键算法及流程图展示

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

  • 权重词频统计:

1092472-20181009224005438-1696136014.png

  • 词组提取:

1092472-20181009224010436-185684305.png

关键代码及说明介绍

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

/*已将将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);
              }
          }
      }
  }
}

实现成果展示

爬虫程序爬取的结果:

1092255-20181010192741258-1085345676.png

统计结果展示:

1092472-20181010221448013-1301007516.jpg

性能改进和分析

改进的思路

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

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

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

  • 消耗最大的函数:
    1092472-20181010142558780-963357402.png
    在w_phrase(in,m)中消耗最大的两个部分:
    1092472-20181010142606672-1416652427.png

单元测试

编号测试项目测试结果
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(代码部分)

问题描述

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

做过哪些尝试

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

是否解决

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

有何收获

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

3.Bug3(爬虫部分)

问题描述

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

做过哪些尝试

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

是否解决

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

有何收获

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

4.Bug4(爬虫部分)

问题描述

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

做过哪些尝试

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

是否解决

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

有何收获

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

5.结对小困难

问题描述

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

做过哪些尝试

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

是否解决

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

有何收获

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

合作情况和感想

具体分工

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

评价队友

队友是一个做什么都挺认真的人,这种态度给我做了一个榜样;这次的作业分工非常明确,队友做的那一块完成度比较好,每一次约定好的都有按时完成,守时的习惯值得点赞;为人也算比较和蔼,沟通起来没有什么障碍,非常顺利;而且基本上博客的排版都是她弄得,审美观挺好的。

个人总结

这次学会了一门新的技术,初步接触了python语言,以后的团队作业就需要用python来编写相关的程序,这次的作业算是打下了一个非常好的基础。同时,也学会要和同学之间多交流、多沟通,封闭是落后的表现,多种思想的交汇才会碰撞出美丽的火花。最后,就是要守时,约定要完成的任务一定要完成,不然就会给队友留下一个不靠谱的印象,这是特别不好的一个现象。

Github代码签入记录

1092472-20181010210516855-1944222106.png

附组织目录:

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周新增代码(行)累计代码(行)本周学习耗时(小时)累计学习耗时(小时)重要成长
24134132121学用git;接触vs性能分析、单元测试功能;
3041316.537.5阅读《构建之法》;结对配合;学习NABCD模型;接触原型开发工具
4、51061147423.561结对配合经验up;了解接触爬虫

转载于:https://www.cnblogs.com/mysoul-/p/9768604.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值