github链接 :https://github.com/YYFCY/Word-Count
一、项目要求
1. 统计文本文件的字符数、单词数和行数
2. 递归处理目录下符合条件的文件
3. 返回更复杂的数据(代码行/空行/注释行)
4. 实现GUI界面
二、项目计划PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时 (分钟) |
Planning | 计划 | ||
Estimate | 估计这个任务需要多少时间 | 30 | |
Development | 开发 | ||
Analysis | 需求分析(包括学习新技术) | 200 | |
Design Spec | 生成设计文档 | 100 | |
Design Review | 设计复审(和同事审核设计文档) | 100 | |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | |
Design | 具体设计 | 100 | |
Coding | 具体编码 | 1000 | |
Code Review | 代码复审 | 100 | |
Test | 测试(自我测试、修改代码、提交修改) | 100 | |
Reporting | 报告 | ||
Test Report | 测试报告 | 150 | |
Size Measurement | 计算工作量 | 30 | |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | |
合计 | 2000 |
三、解题思路
参数 | 功能 |
-c | 返回文件file.c的字符数 |
-w | 返回文件file.c的词的数目 |
-l | 返回文件file.c的行数 |
-s | 递归处理目录下符合条件的文件 |
-a | 返回更复杂的数据 |
-x | 打开GUI界面 |
下面对参数读取部分做详细解释:
根据输入的参数检查文件是否可以存在并可以读取,返回-1表示文件不存在,返回-2表示文件存在但是没有读取权限,返回1表示文件可以正常读取。
//检查文件是否存在且可以读取
int check_file_name(char file_name[])
{
if (access(file_name, 0))//判断该文件/文件夹是否存在;
return -1;
if (access(file_name, 2) != 0)//判断该文件/文件夹是否有读权限;
return -2;
return 1;
}
在确定文件可以读取之后,还需要对输入参数的前半部分做一检测,判断其是否符合题目要求,返回为1则表明符合要求,返回为-1表明输入参数错误
//检查命令是否正确,命令正确返回1,错误返回-1.
int check_command(char command[])
{
int len = strlen(command);
if (len != 2 || command[0] != '-')
return -1;
if (command[1] == 'c' || command[1] == 'w' || command[1] == 'l' || command[1] == 'a')
return 1;
else
return -1;
}
四、设计实现
总体设计(程序流程图)
五、各功能模块
1. 统计文件字符数 count_char()函数
//统计文件的字符数
int count_char(char file[])
{
int char_num = 0;
char ch;
freopen(file, "r", stdin);//重定向输入
while ((ch = getchar()) != EOF)
{
//if (ch != ' '&&ch != '\n'&&ch != '\t')
char_num++;
}
fclose(stdin);
return char_num;
}
2.统计文件单词数 count_word()
//统计文件的单词数
int count_word(char file[])
{
int word_num = 0, flag = 0;
char ch;
freopen(file, "r", stdin);
while ((ch = getchar()) != EOF)
{
if ((ch >= 'a'&&ch <= 'z') || (ch >= 'A'&&ch <= 'Z') || ch == '_') flag = 1;
else
{
if (flag)
{
word_num++;
flag = 0;
}
}
}
fclose(stdin);
return word_num;
}
3. 统计文件的行数 count_line()
//统计文件的行数
int count_line(char file[])
{
int line_num = 0;
char ch;
freopen(file, "r", stdin);
while ((ch = getchar()) != EOF)
{
if (ch == '\n')
{
line_num++;
}
}
fclose(stdin);
return line_num;
}
4. 统计文件的空行数count_blank_line()
//统计文件的空行数
int count_blank_line(char file[])
{
int blank_line_num = 0, ch_num = 0;
char ch;
freopen(file, "r", stdin);
while ((ch = getchar()) != EOF)
{
if (ch == '\n')
{
if (ch_num <= 1)
{
blank_line_num++;
}
ch_num = 0;
}
else if (ch != ' '&&ch != '\t')
ch_num++;
}
fclose(stdin);
return blank_line_num;
}
5. 统计文件的注释的行数count_comment_line()
该部分算法比较简单,只是在编写程序时需要注意一些细节方法,可以使程序显得简单明了
例如本小组用flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志
//统计文件的注释的行数
int count_comment_line(char file[])
{
//flag1是注释开头的标志位,current_line记录当前行是单行注释,flag2是多行注释开头的标志,line记录多行注释的行数,flag3是多行注释结尾的标志
int comment_line_num = 0, ch_num=0, flag1 = 0, flag2 = 0, flag3 = 0, line = 0, current_line = 0;
char ch;
freopen(file, "r", stdin);
while ((ch = getchar()) != EOF)
{
if (ch == '\n')
{
if (line&&ch_num > 0)//若line>0且字符数大于0表示当前行是多行注释中的一行
{
line++;
}
ch_num = flag1 = current_line = 0;
}
if (current_line == 1) continue;//当前行是单行注释,直接跳过对字符的判断直到读到换行符
if (ch != ' '&&ch != '\n'&&ch != '\t')
ch_num++;
if (flag2)//当前读到多行注释
{
if (ch != ' '&&ch != '\n'&&ch != '\t')
ch_num++;
if (ch == '*')//读到多行注释的结尾'*/'的符号'*'
flag3 = 1;
else if (ch == '/'&&flag3)//读到多行注释结尾的'/',必须连续读'*'和'/',否则flag3置0
{
comment_line_num += line;
line = 0;
flag2 = flag3 = 0;
current_line = 1;
}
else
flag3 = 0;
}
else if (ch == '/')
{
if (flag1 == 0)
flag1 = 1;
else if (flag1 = 1 && ch_num <= 3)//单行注释必须连续读取两个'/',且当前行在注释之前最多只有一个可显示字符
{
comment_line_num++;
current_line = 1;
}
}
else if (ch == '*')//多行注释的开头'/*'
{
if (flag1 == 1)
{
flag2 = 1;
line = 1;//进入多行注释
}
}
else
flag1 = 0;
}
fclose(stdin);
return comment_line_num;
}
6. wchar_t转换为string
编写该部分函数主要是为了使之后的输出和数据之间的传递更为方便
//wchar_t 转换 string
void Wchar_tToString(std::string& szDst, wchar_t *wchar)
{
wchar_t * wText = wchar;
DWORD dwNum = WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, NULL, 0, NULL, FALSE);// WideCharToMultiByte的运用
char *psText;// psText为char*的临时数组,作为赋值给std::string的中间变量
psText = new char[dwNum];
WideCharToMultiByte(CP_OEMCP, NULL, wText, -1, psText, dwNum, NULL, FALSE);// WideCharToMultiByte的再次运用
szDst = psText;// std::string赋值
delete[]psText;// psText的清除
}
该部分代码参考博客:https://blog.youkuaiyun.com/chinahaerbin/article/details/7764483
7. 递归处理文件模块 search_file()
//递归处理文件模块
void search_file(string path, int idx)
{
struct _finddata_t filefind;
string cur = path + "*.*";
int done = 0, handle;
if ((handle = _findfirst(cur.c_str(), &filefind)) != -1)
{
while (!(done = _findnext(handle, &filefind)))
{
if (strcmp(filefind.name, "..") == 0)
continue;
if ((_A_SUBDIR == filefind.attrib))
{ //是目录
cur = path + filefind.name + '\\';
search_file(cur, idx); //递归处理
}
else //不是目录,是文件
{
int len = strlen(filefind.name);
for (int i = 0; i < len; i++)
{
if (filefind.name[i] == '.')
{
len = i;
break;
}
}
if (strcmp(filefind.name + len, para[idx] + 1) == 0)
{
cur = path + filefind.name;
printf("%s:\n", filefind.name);
for (int i = 1; i < idx; i++)
basic_command(para[i], &cur[0]);
}
}
}
_findclose(handle);
}
}
遇到的问题:
一开始编写该部分的函数时有点无从下手,之后会参考了某博主的写的博客,才有了较为清晰的思路。
8.GUI界面
核心代码如下:
//设置进程的重定向输出
public void ProcessOutDataReceived(object sender, DataReceivedEventArgs e)
{
if (this.Output.InvokeRequired)
{
this.Output.Invoke(new Action(() =>
{
this.Output.AppendText(e.Data + "\r\n");
}));
}
else
{
this.Output.AppendText(e.Data + "\r\n");
}
}
//C#启动EXE文件
public bool StartProcess(string filename, string[] args)
{
try
{
string s = "";
foreach (string arg in args)
{
s = s + arg + " ";
}
s = s.Trim();
Process myprocess = new Process();
myprocess.OutputDataReceived -= new DataReceivedEventHandler(ProcessOutDataReceived);
ProcessStartInfo startInfo = new ProcessStartInfo(filename, s);
startInfo.FileName = "WordCount.exe";
startInfo.UseShellExecute = false;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardInput = true;//重定向输入
startInfo.RedirectStandardOutput = true;//重定向输出
myprocess.StartInfo = startInfo;
myprocess.StartInfo.UseShellExecute = false;
myprocess.Start();
myprocess.BeginOutputReadLine();
myprocess.OutputDataReceived += new DataReceivedEventHandler(ProcessOutDataReceived);
return true;
}
catch (Exception ex)
{
MessageBox.Show("启动应用程序时出错!原因:" + ex.Message);
}
return false;
}
private void button1_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog1 = new OpenFileDialog(); //显示选择文件对话框
openFileDialog1.InitialDirectory = "c:\\";
openFileDialog1.Filter = "All files (*.*)|*.*";
openFileDialog1.FilterIndex = 2;
openFileDialog1.RestoreDirectory = true;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
this.FilePath.Text = openFileDialog1.FileName; //显示文件路径
}
}
private void button2_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(this.FilePath.Text.Trim()))
{
MessageBox.Show("请选择文件");
}
string[] arg = new string[10];
arg[0] = "-c"; arg[1] = "-w"; arg[2] = "-l"; arg[3] = "-a";
arg[4] = this.FilePath.Text.Trim();
StartProcess(@"WordCount.exe", arg);
}
遇到的问题:
这一部分的进程重定向输出参考了博客:https://www.cnblogs.com/hsiang/p/6596276.html
启动exe文件参考了博客:http://www.cnblogs.com/HappyEDay/p/5498884.html
9. 主函数
int main(int argc, char* argv[])
{
if (strcmp(argv[1], "-s") == 0) // 递归处理目录下符合条件的文件
{
TCHAR szPath[MAX_PATH] = { 0 };
if (GetModuleFileName(NULL, szPath, MAX_PATH))//通过函数GetModuleFileName获取执行程序的绝对路径。
{
(_tcsrchr(szPath, _T('\\')))[1] = 0;//这里需要用到_tcsrchr函数来将获取到的执行程序的绝对路径中的执行程序的名称去掉
}
string Path;
Wchar_tToString(Path, szPath);
for (int i = 0; i < argc; i++)
strcpy(para[i], argv[i]);
search_file(Path, argc - 1);
}
else if (strcmp(argv[1], "-x") == 0) //图形界面
{
ShellExecuteA(NULL, "open", "GUI.exe", NULL, NULL, SW_SHOW);
}
else //对单个.c文件的统计
{
int num_para;
for (num_para = argc - 1; num_para > 1; num_para--)
{
if (strcmp(argv[num_para], "-s") == 0 || strcmp(argv[num_para], "-x") == 0
|| strcmp(argv[num_para], "-c") == 0 || strcmp(argv[num_para], "-w") == 0
|| strcmp(argv[num_para], "-l") == 0 || strcmp(argv[num_para], "-a") == 0)
{
break;
}
}
char filename[500] = { 0 };
strcat(filename, argv[num_para + 1]);
for (int i = num_para + 2; i < argc; i++)
{
strcat(filename, " ");
strcat(filename, argv[i]);
}
int flag_3 = check_file_name(filename);
if (flag_3 == -1)
{
printf("Error:The file does not exist !\n");
return 0;
}
else if (flag_3 == -2)
{
printf("Error:The file has no read permissions !\n");
return 0;
}
printf("%s:\n", filename);
for (int i = 1; i <= num_para; i++)
{
if (check_command(argv[i]) == 1)
basic_command(argv[i], filename);
else
printf("Command parameter error !\n");
}
}
return 0;
}
六、PSP表格总结
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时 (分钟) |
Planning | 计划 | ||
Estimate | 估计这个任务需要多少时间 | 30 | 50 |
Development | 开发 | ||
Analysis | 需求分析(包括学习新技术) | 200 | 200 |
Design Spec | 生成设计文档 | 100 | 120 |
Design Review | 设计复审(和同事审核设计文档) | 100 | 80 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 30 |
Design | 具体设计 | 100 | 150 |
Coding | 具体编码 | 1000 | 1500 |
Code Review | 代码复审 | 100 | 200 |
Test | 测试(自我测试、修改代码、提交修改) | 100 | 100 |
Reporting | 报告 | ||
Test Report | 测试报告 | 150 | 150 |
Size Measurement | 计算工作量 | 30 | 50 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 60 |
合计 | 2000 | 2690 |
七、总结与反思
这次项目设计与编码部分主要由我来完成,测试与博客撰写主要由我的队友完成。总的来说,编码部分的完成较为顺利,难点在于递归处理文件以及界面的设计部分。经过这次项目,个人编程能力得到了提升,也让我学会了如何与他人合作开发项目。