具体分工
- 111500206 赵畅:负责WordCount的升级,添加新的命令行参数支持(自定义输入输出文件,权重词频统计,词组统计等所有新功能设计)
- 031602215 胡展瑞:负责爬虫的设计,resutlt.txt的格式化,以及附加题的所有设计(批量下载pdf、可视化WordCount以及历年情况分析对比、作者联系图)。
PSP表格、学习记录表
- PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 540 | 930 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 | 30 | 180 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 240 |
· Coding | · 具体编码 | 120 | 120 |
· Code Review | · 代码复审 | 120 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 50 | 80 |
· Test Repor | · 测试报告 | 30 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 10 |
合计 | 600 | 1020 |
- 学习记录表
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 500 | 500 | 12 | 12 | 单元测试的写法 |
2 | 0 | 500 | 12 | 17 | Axure RP 8 原型设计工具 |
3 | 300 | 800 | 16 | 33 | C++爬虫、regex正则表达式匹配 |
解题思路描述与设计实现说明
通读作业要求之后,发现整个作业其实分为两步。第一个就是生成输入用的result.txt
,这个需要学习爬虫的知识。其余是对上一次个人项目的WordCount
进行升级改造,满足各种新需求。
爬虫使用
使用工具:C++
思路:
- 爬取CVPR2018网页内容,保存到CVPR2018.py文件中。在这个文件里,根据正则表达式,得到每份论文对应的href链接;
- 根据每一篇paper的链接,循环爬取每一份paper的内容,输出到paperinfo.txt文件中;
- 最后再根据题目要求的输入格式,利用正则表达式格式化处理paperinfo.txt的内容,最终输出到result.txt文件中。
使用:
int makeSocket(string host,int port); // 根据提供的url得到连接的socket
int send(SOCKET s,const char FAR * buf,int len,int flags); // 利用socket向服务端发送消息
int recv(SOCKET s,char FAR * buf,int len,int flags); // 利用socket接收从客户端发送来的消息
vector<string> abstract(vector<string> lj) //生成abstract链接
{
regex pattern("(<dt class="ptitle"><br><a href=")(.*)(">)(.*)(</a></dt>)"); //正则表达式,目的为匹配对应标题的连接
smatch result;
fstream file;
string line; //读取每行结果
file.open(openfile1); //打开文件
while (getline(file, line))
{
if (regex_match(line, result, pattern) == true) { //每行与正则表达式匹配
lj.push_back(result[2]); //将链接存储在lj这个vector中
}
}
cout << lj.size() << endl; //提示链接的数量,用于检验结果
return lj;
}
void get_abstract() //爬取每份论文内容
{
vector<string> lj;
lj = abstract(lj);
fstream file;
file.open(fileName, ios::out | ios::binary);
for (int i = 0; i < lj.size(); i++) //循环读取vector中的链接
{
string url = "openaccess.thecvf.com";
string name = "/" + lj[i];
int port = 80;
int client_socket = makeSocket(url, port); //获取socket
string request = "GET " + name + " HTTP/1.1
Host:" + url + "
Connection:Close
";
if (send(client_socket, request.c_str(), request.size(), 0) == SOCKET_ERROR) //建立连接
{
cout << "send error" << endl;
}
char buf[1024];
::memset(buf, 0, sizeof(buf)); //清除缓冲区内容
int n = 0;
n = recv(client_socket, buf, sizeof(buf) - sizeof(char), 0);
char* cpos = strstr(buf, "
");
file.write(cpos + strlen("
"), n - (cpos - buf) - strlen("
")); //清除头部的非消息行报文
while ((n = recv(client_socket, buf, sizeof(buf) - sizeof(char), 0)) > 0)
{
try
{
file.write(buf, n); //写入文件
}
catch (...)
{
cerr << "ERROR" << endl;
}
}
closesocket(client_socket);
cout << i << endl; //提示该份论文读取成功
}
// system("pause");
file.close();
}
void Generate_result() //处理信息生成result.txt文件
{
fstream file;
string line; //按行读取文件内容
file.open(openfile2);
fstream file1;
file1.open(fileName2, ios::out | ios::binary);
smatch result;
regex pattern1("(<div id="papertitle">)"); //匹配papertitle行
regex pattern2("(<br><br><div id="abstract" >)"); //匹配abstract行
int flag1 = 0;
int flag2 = 0;
int count = 0;
while (getline(file, line))
{
if (flag1 == 1)
{
for (int j = 6; j > 0; j--) { line.pop_back(); } //删除末尾的</div>
file1 << count++ << endl;
file1 << "Title: " << line << endl;
flag1 = 0;
}
if (regex_match(line, result, pattern1) == true) {
flag1 = 1;
}
if (flag2 == 1)
{
for (int j = 6; j > 0; j--) { line.pop_back(); }
file1 << "Abstract: " << line << endl;
file1 << endl;
file1 << endl;
flag2 = 0;
}
if (regex_match(line, result, pattern2) == true) {
flag2 = 1;
}
}
file.close();
file1.close();
}
---
代码组织与内部实现设计(类图)
和上次作业大部分相似,主要的差异是为了统计词组词频多了个PhraseFrequency.h
模块 | 描述 |
---|---|
ArgumentParser.h | 用于解析命令行参数; |
CountChar.h | 用于统计一个文件内的字符数; |
CountLines.h | 用于统计一个文件内的有效行数; |
CountWords.h | 用于统计一个文件内的有效单词数量; |
WordFrequency.h | 用于分析一个文件内的单词频率数据,并输出最高的N个; |
PhraseFrequency.h | 用于分析一个文件内的词组频率数据,并输出最高的N个; |
main.cpp | 主函数 |
说明算法的关键与关键实现部分流程图 & 关键代码解释
下面对本次作业在前一次作业的基础上增加的新功能进行一一解释,而上一次作业涵括的内容省略不写。
命令行参数解析:
对于命令行参数的新需求,设置如下环境变量,这些环境变量被命令行参数修改,并运用到对应的代码段里:
变量名 | 类型 | 初始值 | 注释 |
---|---|---|---|
inputFileName | std::string | "result.txt" | 自定义输入文件名 |
outputFileName | std::string | "output.txt" | 自定义输出文件名 |
weightFrequencyOn | int | 0 | 是否开启权重统计模式,0为关闭 1为打开,下同 |
phraseFrequencyOn | int | 0 | 是否开启词组统计模式 |
phraseLength | int | 0 | 若开启词组统计模式,词组的长度为多少 |
topNWords | int | 10 | 输出出现频次最高的N个单词 / 词组 |
做法是遍历 **argv
这个指针数组:
- 遇到
-i
,-o
则把argv[i+1]
存放到inputFileName
和outputFileName
。在读入输入和向文件输出时改为使用这两个变量即可。 - 遇到
-w
,判断argv[i+1]
是否为0或1,若为0或1则存放到weightFrequencyOn
。使用此变量控制关键函数的行为分支即可。 - 遇到
-n
,-m
,则判断argv[i+1]
是否为正整数,若是,则存放到topNWords
和phraseLength
中,并将phraseFrequencyOn
置1。原本程序中输出的固定10个最高频次的单词,改为输出topNWords
个即可。
解析命令行参数的代码如下:
int Parse_Args(int argc, char ** argv)
{
int i = 1; // skip WordCount.exe
while (argv[i] != NULL)
{
if (strcmp(argv[i], "-i") == 0)
{
if (argv[i + 1] == NULL) {
printf("error: no input file name!
");
return -1;
}
inputFileName = argv[i + 1];
i += 2;
}
else if (strcmp(argv[i], "-o") == 0)
{
if (argv[i + 1] == NULL) {
printf("error: no output file name!
");
return -1;
}
outputFileName = argv[i + 1];
i += 2;
}
else if (strcmp(argv[i], "-w") == 0)
{
if (argv[i + 1] == NULL) {
printf("error: -w must follow 0 or 1!
");
return -1;
}
bool isOneOrZero = (strcmp(argv[i + 1], "1") || strcmp(argv[i + 1], "0"));
if (!isOneOrZero) {
printf("error: -w must follow 0 or 1!
");
return -1;
}
int num = atoi(argv[i + 1]);
weightFrequencyOn = num;
i += 2;
}
else if (strcmp(argv[i], "-m") == 0)
{
if (argv[i + 1] == NULL) {
printf("error: -m must follow a positive Integer!
");
return -1;
}
int res = stringIsPositiveInteger(argv[i + 1]);
if (res == -1) {
printf("error: -m must follow a positive Integer!
");
return -1;
}
phraseFrequencyOn = 1;
phraseLength = res;
i += 2;
}
else if (strcmp(argv[i], "-n") == 0)
{
if (argv[i + 1] == NULL) {
printf("error: -n must follow a positive Integer!
");
return -1;
}
int res = stringIsPositiveInteger(argv[i + 1]);
if (res == -1) {
printf("error: -n must follow a positive Integer!
");
return -1;
}
topNWords = res;
i += 2;
}
}
return 0;
}
权重统计模式 -w <0|1>
权重统计模式是指,属于标题行的单词权重在统计时变为10,属于摘要行单词在统计时权重仍为1。
要实现这个功能,我对本次作业的输入做了一番考察,这一次的输入的数据有比较清晰和特定的格式,可以用来协助完成这个功能。看下面的例子:
0
Title: Embodied Question Answering
Abstract: We present a new AI task -- Embodied Question Answering (EmbodiedQA) -- where an agent is spawned at a random location in a 3D environment and asked a question ("What color is the car?"). In order to answer, the agent must first intelligently navigate to explore the environment, gather necessary visual information through first-person (egocentric) vision, and then answer the question ("orange"). EmbodiedQA requires a range of AI skills -- language understanding, visual recognition, active perception, goal-driven navigation, commonsense reasoning, long-term memory, and grounding language into actions. In this work, we develop a dataset of questions and answers in House3D environments, evaluation metrics, and a hierarchical model trained with imitation and reinforcement learning.
1
Title: Learning by Asking Questions
Abstract: We introduce an interactive learning framework for the development and testing of intelligent visual systems, called learning-by-asking (LBA). We explore LBA in context of the Visual Question Answering (VQA) task. LBA differs from standard VQA training in that most questions are not observed during training time, and the learner must ask questions it wants answers to. Thus, LBA more closely mimics natural learning and has the potential to be more data-efficient than the traditional VQA setting. We present a model that performs LBA on the CLEVR dataset, and show that it automatically discovers an easy-to-hard curriculum when learning interactively from an oracle. Our LBA generated data consistently matches or outperforms the CLEVR train data and is more sample efficient. We also show that our model asks questions that generalize to state-of-the-art VQA models and to novel test time distributions.
首先第一行是一个数字,表示编号,这种只有一个数字的行就是编号行。接下来一行是标题行,用"Title: "打头。再接下来是摘要行,用"Abstract: "开头。每篇论文都是这样的格式(编号行、标题行、摘要行),然后每两篇论文之间再用两个空行隔开。因此,对于文件输入的数据流,我们可以一行一行处理,对于每一行,我们进行判断,判断改行属于哪种类型,然后就可以进行权重化的处理了。简要的步骤说明如下:
- A.解析命令行参数,使用
-w 1
打开了权重统计模式。 - B.读入一行,判断其中的内容
- 是空行或者是纯数字(编号行),则返回步骤B,不进行任何统计。
- 如果该行以"Title: "起始,则说明此行是标题行。在进行词频统计时,每个单词计数改为10。
- 如果该行以"Abstract: "起始,则说明此行是摘要行。在进行词频统计时,每个单词计数仍为1。
编码细节上,采取的做法是重构之前的InsertToHashTable()
函数,增加了一个int型参数,该参数根据命令行参数解析的环境变量控制在插入单词进入哈希表时函数行为的改变。
void InsertToHashTable(string & word, int TitleorAbstract)
{
if ((hash_iter = hash_table.find(word)) == hash_table.end()) { // 一个新单词
if (TitleorAbstract == TITLE) { // 开启权重词频功能后,title行的单词frequency权重算为10
pair<string, int> newWord = pair<string, int>(word, 10);
hash_table.insert(newWord);
}
else if (TitleorAbstract == ABSTRACT){ // abstract行的单词frequency权重算为1
pair<string, int> newWord = pair<string, int>(word, 1);
hash_table.insert(newWord);
}
else if (TitleorAbstract == NONWEIGHT){ // 没有开启权重词频功能,frequency统统算为1
pair<string, int> newWord = pair<string, int>(word, 1);
hash_table.insert(newWord);
}
}
else { // 一个之前出现过的单词
if (TitleorAbstract == TITLE)
hash_iter->second += 10;
else if (TitleorAbstract == ABSTRACT)
hash_iter->second++;
else if (TitleorAbstract == NONWEIGHT)
hash_iter->second++;
}
}
词组统计模式 -m
在这个小要求中提出了词组的概念。词组的意思是多个连续的合法单词构成的串。不难发现,词组其实也是string,所以我们可以重用之前的自动机,也可以重用之前的InsertToHashTable()
接口。根据之前的自动机(不在此赘述),就不难想到如下的过程来构造一个词组(伪代码):
int counter = 0;
string word;
string phrase;
每次识别到validword且正常退出到out of word状态后之后:
counter++;
phrase+=word;
if counter == M:
insert phrase into hashtable
不过,上述过程是有一些小问题的。我们假设:每一个大写字母A、B、C等,都代表一个合法单词,它们之间由分隔符隔开。若有一个合法单词的序列:ABCDEF中,假设词组所包括的合法单词数量为m,m=3,,则满足要求的词组有ABC/BCD/CDE/DEF。而使用上述伪代码中所描述的,采取计数器的做法,不容易实现求的所有的词组。因此,我提出了一个新的过程ConstructPhrase()
,使用队列作为数据结构。这个过程的伪代码如下:
deque<string> q_phrase;
每次识别到validword且正常退出到out of word状态后之后:
将单词入队列
if(队列内容纳了m个单词,m为词组所容纳的单词个数)
将队列里的词构造成词组
队首元素抛出
返回这个词组
否则返回空串
ConstructPhrase()
这个过程的用意是这样的:所有的合法单词都会被保存在一个队列中。每次识别到合法的单词之后,都要尝试去构造词组。若可以构造(也就是队列里的单词个数达到了m个),则将队列里的所有单词连接成一个string,这个string就是我们要求的词组phrase。由于这个phrase是由多个string连接而成的,其实在概念上来说也还是一个string,就可以直接调用以往的InsertToHashTable()
接口了。也就是说,后续步骤(插入哈希表中进行统计)都可以重用。
以ABCDEF这个输入流作为例子(别忘了这里每一个大写字母都是一个合法单词)。当识别到一个单词后,我们就把它放进队列。例如识别到A,就把A放入队列,队列的内容就是A。每次放一个单词进入队列之后,我们都要尝试去构造一个词组,也就是调用ConstructPhrase这个过程。做法是判断队列里的单词个数是否已经达到了命令行参数要求的词组长度,若这里m=3,而此时队列里只有一个单词A,长度为1,还没有达到3,则尝试构造phrase失败。当识别了B之后,队列内容是AB,依然不能构成词组。当第三个单词入列之后,队列内容是ABC,可以构成phrase了!依次将他们出队,连接形成一个string传递给InsertToHashTable()
。但单词B和C是有可能与后面的合法单词形成新的词组的,所以B和C需要继续呆在队列里。这就是上述伪代码步骤中“队首元素抛出”的意思,即每次识别到合法词组之后,仅仅把队首A抛出即可,保证了后续单词B和C依然可能和D构成词组的可能性。
若没有识别连续的合法单词,例如Knowledge is power这就不是词组,因为其中is不是4个字母以上的单词,不是validword。这种情况,就是Knowledge已经在队列里了,而下一个识别到的是一个不合法的情况,这意味着Knowledge没有办法和别的单词构成词组了,此时将队列清空即可。
自动机以及构造词组的代码如下:
static deque<string> q_phrase;
extern int phraseLength;
//有穷自动机。参数意义:输入状态、输入字符、存储识别到的单词、当前过程位于标题行or摘要行
int TransitionStorePhrase(int state, char input, string & word, int TitleorAbstract)
{
switch (state)
{
case OUTWORD:
if (Separator(input)) return OUTWORD;
if (isalpha(input)) { word += input; return P1; }
if (IsNum(input)) return NotAWord;
case NotAWord:
q_phrase.clear();
if (Separator(input)) return OUTWORD;
else return NotAWord;
case P1:
if (IsNum(input)) { word.clear(); return NotAWord; }
if (isalpha(input)) { word += input; return P2; }
else { word.clear(); q_phrase.clear(); return OUTWORD; }//若出现了不合法的状况,队列需要清空
case P2:
if (IsNum(input)) { word.clear(); return NotAWord; }
if (isalpha(input)) { word += input; return P3; }
else { word.clear(); q_phrase.clear(); return OUTWORD; }
case P3:
if (IsNum(input)) { word.clear(); return NotAWord; }
if (isalpha(input)) { word += input; return VALIDWORD; }
else { word.clear(); q_phrase.clear(); return OUTWORD; }
case VALIDWORD:
if (isalnum(input)) { word += input; return VALIDWORD; }
else {
string phrase = ConstructPhrase(word); // 若识别到了合法单词,就尝试去构成词组
if (phrase != ""){ // 若可以构成词组,就插入哈希表
InsertToHashTable(phrase, TitleorAbstract);
}
word.clear();
return OUTWORD;
}
}
return ERRORSTATE;
}
string ConstructPhrase(string & word) // 尝试去构成词组
{
q_phrase.push_back(word);
string phrase = "";
if (q_phrase.size() == phraseLength) // 如果队列里的单词达到了命令行参数的设定要求,就可以构成词组
{
string top;
for (int i = 0; i < phraseLength; i++) // 循环出入队并连接到phrase里
{
top = q_phrase.front();
phrase += top;
if (i != phraseLength - 1)
phrase += " ";
q_phrase.pop_front();
q_phrase.push_back(top);
}
q_phrase.pop_front(); // 最后将队首 pop
return phrase; // 返回这个词组
}
else
return phrase; // 如果不能构成词组,会返回空串
}
算法如下图所示:
这个统计词组的算法复杂度粗略分析如下:
- 最坏情况就是输入的全是合法单词,设合法单词共有
N
个。(为什么说输入流全是合法单词是最坏情况,是因为每次都能识别到词组且每次构造词组只能剔除一个合法单词,相当于暴力遍历ABCDEF的子串了) - 设词组的长度要求是包括
M
个合法单词,则最坏情况下共有N-M+1
个词组。 - 每个词组的取出需要循环队列的出入队,复杂度与队列长度有关,也就是
O(M)
- 若
M
很小,则N-M+1
近似为N
,则总的最坏情况复杂度为O(NM)
附加题设计与展示
我们设计了三个附加内容:
- 批量下载PDF的功能,批量下载可以节省用户大量的时间以及精力。
- 可视化WordCount的热词词频图谱,以及历年paper数量变化情况分析对比。
- 作者联系图。
附加题三份代码百度云链接戳这里
批量下载PDF:
使用工具python
结果如下图所示:
部分代码如下:
···
for lj in pdf_href:
url =lj['href'] #匹配链接
count = count +1
url = "http://openaccess.thecvf.com/" +url #获取pdf下载链接
r = requests.get(url, stream=True)
name = url.split('/')[-1] ##对保存的文件名进行处理,使得其为论文的名称
pdf_name=name.split('_')[1]
for x in name.split('_')[2:-3]:
pdf_name =pdf_name+"_"+x
pdf_name += '.pdf' ##保证文件格式是.pdf
with open('%s' % pdf_name, 'wb') as f:
for chunk in r.iter_content(chunk_size=128):
f.write(chunk)
print('Saved %s' % pdf_name) ##提示下载完成
···
可视化WordCount以及历年情况分析对比:
- 使用工具python
对于历年论文数量:
GIF如下:
可以看出每年论文数量都是呈现上涨的趋势,说明计算机视觉相关领域还是处于一个热门的阶段。
对于近些年(从2013到2018)的WordCount,我们采用了三个词的词频统计,采用数量的区间范围为[20,100],显示的结果如下:
可以直观看出最近几年的热门内容在于convolutional neural network---->卷积神经网络
部分代码如下:
···
year = {} #提取历年论文数量数据
for i in range(len(a)):
if(i%11==0):
year[a[i]] = b[i]
line = Line(u"论文年份增长情况",title_top="0%")
line.add(u"数量",[key for key in year],[year[key] for key in year],line_width=5,line_curve=0.2) #生成可视化图形
line.render(path='lunwen.gif') # 存储文件
···
for i in range(6):
if(i == 0):
word_count=word_count13
elif(i ==1):
word_count=word_count14
elif(i ==2):
word_count=word_count15
elif(i ==3):
word_count=word_count16
elif(i ==4):
word_count=word_count17
elif(i ==5):
word_count=word_count18
wordcount=WordCloud(width=1200,height=620)
wordcount.add("",[key for key in word_count],[word_count[key] for key in word_count],word_size_range=[20, 100]) #生成可视化图形
wordcount.render(path="wordcount201%d.gif"%(i+3)) # 存储文件
作者联系图
- 使用工具python
结果:
GIF如图所示(由于图片太大,只能高糊……):
我们假定每一份论文的每一个作者都是有紧密的联系的,并且不存在重名的作者。我们爬取了979篇论文中2910名作者,通过作者之间的关系,方便您查看各位业界大牛的py情况。
部分代码如下:
···
a_author = []
Links = []
for item in authors:
temp = []
for x in item.split(' and '):
a_author.append(x)
temp.append(x)
for i in temp:
for j in temp:
Links.append({"source": "%s" %i, "target": "%s" %j}) #获取作者之间的联系
a_author = list(set(a_author))
nodes = []
for item in a_author:
nodes.append({"name":"%s" %item,"symbolSize": 20*random.random()}) #获取作者结点
graph = Graph("作者关系图",width=1920,height=1080,is_animation=False) #生成可视化图形
graph.use_theme("walden")
graph.add("1", nodes, Links, repulsion=80000, label_pos="right")
graph.render() #直接生成html网页文件
···
性能分析与改进
测试时使用了CVPR2018的979篇paper作为输入数据,命令行参数为-i result.txt -m 3 -n 20 -w 1 -o output.txt
,即:开启标题权重模式,统计数据中出现频次前20的词组,每个词组长度为3个合法单词,结果存放到output.txt
中。共花费了15.731秒。测试结果如下:
性能报告情况如下:
其中消耗最大的函数是phrasePrequency()
,占据了所有执行时间的57.48%,而其中的有穷自动机操作就占据了54.25%,这是整个程序的核心部分,难以更加优化。想要在此部分优化,只能说继续优化那个复杂度为O(MN)的算法吧,不过我目前没有想其他的算法,而且这个算法的简易性也是它的优势。至于其他部分的优化也实在是优无可优了。
单元测试
设计了十个单元测试样例,被说明测试的函数,构造测试数据见下表:
单元测试名称 | 解释 | 期待输出 | 测试的模块/函数 |
---|---|---|---|
WrongInputFileName | 打开错误的文件名 | 能够正确返回错误信息 | CountChar |
NumberLine | 传一个文件里,只有一个数字(编号行) | 不进行统计 | CountChar |
EmptyFileTest | 传入一个空文件 | 不进行统计 | CountChar、CountWords、CountLines |
EmptyLineTest | 传入一个文件,只包含空行 | 不进行统计 | CountChar、CountWords、CountLines |
TitleTest | 传入一个文件,一行以"Title: "开头的字符串 | 应能正确进行统计 | CountChar、CountWords、CountLines |
AbstractTest | 传入一个文件,一行以"Abstract: "开头的字符串 | 应能正确进行统计 | CountChar、CountWords、CountLines |
WeightModeTest | 传入一个文件,里有两行,分别以"Title: "和"Abstract: "开头。并开启权重词频统计模式 | 应能正确统计,权重相应改变 | WordFrequency |
PhraseModeTest1 | 传入一个文件,里面的内容以"Title: "开头,后面一系列合法单词,命令行参数 -m 2 | 应能正确统计出长度为3的词组 | PhraseFrequency |
PhraseModeTest2 | 传入一个文件,里面的内容是"Knowledge is power",命令行参数 -m 2 | 因为"is"不是合法单词,不应识别出词组 | PhraseFrequency |
TopNTest | 传入一个普通的文件,改变命令行参数-n | 测试是否输出相应的 top n 单词 | WordFrequency |
运行结果如下:
部分代码展示如下:
namespace WrongInputFileName
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
char filename[100] = "FileNotExited.txt";
int count = CountChar(filename);
Assert::IsTrue(count == 0);
// TODO: 在此输入测试代码
}
};
}
namespace NumberLine
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
char filename[100] = "NumberLine.txt";
int count = CountChar(filename);
Assert::IsTrue(count == 0);
// TODO: 在此输入测试代码
}
};
}
namespace EmptyFileTest
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
char filename[100] = "EmptyFile.txt";
int count = CountChar(filename);
int numOfLines = CountLines(filename);
int numOfWords = CountWords(filename);
Assert::IsTrue(count == 0 && numOfLines == 0 && numOfWords == 0);
// TODO: 在此输入测试代码
}
};
}
贴出Github的代码签入记录
遇到的代码模块异常或结对困难及解决方法
代码模块上,由于第一次作业时就划分好了模块,以及对于多个文件的规划和安排都比较清晰,所以在添加新功能时,没有在大的代码模块上出现问题。结对时,我们遇到的困难主要还是在词组词频的统计的算法上。
在结对项目的开展初期,(也就是国庆期间)我们是利用石墨文档在线上进行讨论的:
我们在这个文档上进行需求分析,然后搜索相对应的资料并贴上来,各自也在上面进行进度的标记,控制项目的进程。
在项目进行得差不多之后,我们就进行了线下小黄鸭调试结对编程来解决我们的问题(见:小黄鸭调试法)
所谓小黄鸭调试法就是我和同伴相约奶茶店,互相向对方讲解自己做了哪些工作。我就向队友解释我对Word Count项目的升级是怎么实现的。队友向我讲解如何实现爬虫,如何爬取文件,利用正则提取数据并格式输出。在向队友描述代码时,自然而然地就发现了一些bug,以及某一个人没有注意到的需求点等。当天回来对代码进行了修改,commit时,我就写上了"small duck pair work"。
在聊完了各自已经做完的工作之后,我俩一起探讨了当时未解决的问题,我们主要聊了下词频统计的算法,最后两人敲定了使用这个朴素的算法。最后,我们还讨论了附加题的思路。(附上当时结对编程时用的草稿纸~
评价你的队友
我的队友,帅气温柔的huzr,擅长python。学习新知识非常快(据说爬虫用C++实现有额外加分就以迅雷不及掩耳之势学了C++写法并实现了)。非常高效地搞定了这次项目的数据输入部分,为我免去后顾之忧。还有一个主要贡献就是这次项目的附加题部分,瑞瑞的头脑真的不错,idea还是不少的,而且都能实现出来,真是中国好队友了。
我对于瑞瑞的建议就是代码规范可以再注意一下(谁叫我是代码整洁强迫症患者呢)。
还有就是结对项目是非常赞的,第一次有和人一起code review,体验非常好。柯老师在上课调查有多少人能在身旁有个人在review的时候能够打代码,举手的人不多,我是其中一个。