一、前言

  作业具体要求见[https://edu.cnblogs.com/campus/nenu/SWE2017FALL/homework/922]。一开始用JAVA写了个词频统计,然而没想出输入格式怎么解决,于9/17日晚将JAVA程序改成用C#程序写。9/17晚上八点~9/18下午四点前做的工作,主要都是做技术原型,分析题中哪些是自己不确定或不会完成的地方。到了下午五点左右就开始真正完成满足题目要求的各项功能。代码地址[https://git.coding.net/Dawnfox/wf.git]

二、可能存在的困难

  • 困难一:C#中文件输入
 1             string fName = "";//文件路径
 2             FileStream isrr = new FileStream(fName, FileMode.Open, FileAccess.Read);
 3             StreamReader ioData = new StreamReader(isrr);
 4             string s = "";//每行数据
 5             while ((s = ioData.ReadLine()) != null)
 6             {
 7                 //处理数据
 8 
 9             }
10             ioData.Close();
View Code
  • 困难二:C#中判断目录、文件是否存在
 1         //判断是否为目录
 2         static bool IsDir(String fDir)
 3         {
 4             bool res = false;
 5             if (Directory.Exists(fDir))
 6             {
 7                 res = true;
 8             }
 9             return res;
10         }
11         //判断是否为文件
12         static bool IsFile(String fPos)
13         {
14             bool res = false;
15             if (File.Exists(fPos))
16             {
17                 res = true;
18             }
19             return res;
20         }
View Code
  • 困难三:C#中遍历指定目录中的文件
 1         //扫描目录 扫描拓展名为.txt
 2         static List<string> scanDir(String fFir)
 3         {
 4             List<string> filesPath = new List<string>();
 5             DirectoryInfo dir = new DirectoryInfo(fFir);
 6             //fFir为某个目录,如: “D:\Program Files”
 7             FileInfo[] inf = dir.GetFiles();
 8             //int fNum = 0;
 9             foreach (FileInfo finf in inf)
10             {
11                 if (finf.Extension.Equals(fExtension))
12                 {
13                     filesPath.Add(finf.FullName);
14                 }
15             }
16             return filesPath;
17         }
View Code
  • 困难四:C#字典(Dictionary)排序
1 Dictionary<String, int> mp = new Dictionary<string, int>();//key:单词,value:词频
2 mp = mp.OrderByDescending(r => r.Value).ToDictionary(r => r.Key, r => r.Value);//排序
View Code
  • 困难五:C#格式化输出
1 //格式码
2 Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
View Code
  • 困难六:C#获取控制台重定向文件内容

这块挺卡人的。一直没想到如何用C#对通过控制台进行重定向的文件进行处理。直到今天(9/22)通过MSDN,查看关于C#的I/O流与.net中的console类[点击]。才想到解决办法。

1                 if (Console.IsInputRedirected)//判断是否存在重定向输入
2                 {
3                     StreamReader ioData = null;
4                        //获取控制台重定向文件数据流  2017/9/22 14:29:03
5                     ioData = new StreamReader(Console.OpenStandardInput());
6                 }    
View Code
  • 困难七:C#多行输入,允许从控制台复制粘贴(Ctrl+c),并以F6或者Ctrl+z结束输入(9/25)
 1                     ConsoleKeyInfo consoleKeyInfo;//获得输入键盘输入 2017/9/25 19:43:47
 2                     Console.TreatControlCAsInput = true;//将复制Ctrl+C作为普通输入
 3                     String input = "";//输入字符串
 4                     consoleKeyInfo = Console.ReadKey();//获取字符串
 5                     input += consoleKeyInfo.KeyChar;
 6                     /*warning 多行输入操作 不支持单词删除后再输入单词 不允许回退操作 不支持特殊功能键操作*/
 7                     // F6 退出 Ctrl+z
 8                     while (!(consoleKeyInfo.Key == ConsoleKey.F6 || (consoleKeyInfo.Modifiers == ConsoleModifiers.Control && consoleKeyInfo.Key == ConsoleKey.Z)))
 9                     {
10                         consoleKeyInfo = Console.ReadKey();
11                         if (consoleKeyInfo.Key == ConsoleKey.Enter)
12                         {
13                             input += Environment.NewLine;//输入字符串换行,则在字符串尾部加上系统的换行符
14                             Console.SetCursorPosition(0, Console.CursorTop + 1);//换行之后,控制台光标移动到新的一行首部
15                         }
16                         input += consoleKeyInfo.KeyChar;
17                     }
View Code

三、需要注意的地方

  需要注意四个功能(实际上是五个功能,功能四分为功能4-1和功能4-2)的输入输出格式。五个功能输出单词总数和出现次数最多的TOP10的单词及频率时,英语单词左对齐,数字右对齐。(假设n为单词数)

  • 功能一的输出格式需要注意单词总数和所列出TOP10的单词频率之间有空行,单词总数格式为“total  n”。
  • 功能二的输出格式需要注意单词总数和所列出TOP10的单词频率之间有空行,单词总数格式为“total  n words"。
  • 功能三的输出格式需要注意单词总数和所列出TOP10的单词频率之间有空行,单词总数格式为“total  n words",同时需要注意还要输出被测文件的名称,不用文件数据用“----”(四个短横)分割。
  • 功能4-1的输出格式需要注意单词总数和所列出TOP10的单词频率之间无空行,单词总数格式为“total  n",输入参数和输出结果之间存在空行。
  • 功能4-2的输出格式需要注意单词总数和所列出TOP10的单词频率之间无空行,单词总数格式为“total  n",输入的待测字符串和输出结果之间存在空行。
  • 功能4-2需要注意两种情况,一是直接从控制台输入多行数据;二是通过复制粘贴输入数据(Ctrl+c)。输入字符串与输出结果有空行,需要注意。(9/25)
  • 功能4-2结束输入应该是控制台标准结束输入(F6或者Ctrl+z)。(9/25)

四、解题思路

  总的思路就是扫描待测文件,用正则表达式将数字、换行、标点符号(小写的减号不需要替换)换成空格,至于上撇号之后的内容我是用正则表达式舍去不考虑。然后我就直接暴力通过空格将字符串切割成单词,然后将单词的值和出现的频率作为词典的key和value。

完整代码如下。

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Diagnostics;
  4 using System.IO;
  5 using System.Linq;
  6 using System.Text.RegularExpressions;
  7 
  8 namespace wf
  9 {
 10     class Program
 11     {
 12         static private String fExtension = ".txt";//文件为文本类型
 13         static private Regex regex1 = new Regex("(\\d|\\r|\\n)");//空格替换数字和换行
 14         static private Regex regex2 = new Regex(@"\p{Pc}|\p{Ps}|\p{Pe}|\p{Pi}|\p{Pf}|\p{Po}|\p{N}|\p{C}");//空格替换标点 短横线不替换
 15         static private Regex regex3 = new Regex("(\\w+)'\\w+");////舍去上撇号之后内容
 16         //功能四-2对应处理
 17         static void FuncBaseT(string s)
 18         {
 19             string[] sl;//保存空格分割出的单词
 20             int Freq = 0, wordNum = 0;//Freq,单词出现频率 单词总数(重复也算)
 21             Dictionary<String, int> mp = new Dictionary<string, int>();//key:单词,value:词频
 22             s = regex1.Replace(s, " ");
 23             s = regex3.Replace(s, "$1");
 24             s = regex2.Replace(s, " ");
 25             s = s.ToLower();//小写单词 避免单词大小写造成同一个单词被记作不同单词
 26             sl = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
 27 
 28             foreach (string sElem in sl)
 29             {
 30                 wordNum++;
 31                 if (!mp.ContainsKey(sElem))
 32                 {
 33                     mp.Add(sElem, 1);
 34                 }
 35                 else
 36                 {
 37                     mp.TryGetValue(sElem, out Freq);
 38                     mp.Remove(sElem);
 39                     mp.Add(sElem, (Freq + 1));
 40 
 41                 }
 42             }
 43             //输出不重复的单词
 44             Console.WriteLine("{0,-20}{1,10}", "total", mp.Count);
 45             // ling 对字典排序 取TOP10 单词
 46             mp = mp.OrderByDescending(r => r.Value).ToDictionary(r => r.Key, r => r.Value);//排序
 47             int flag = 0;
 48             foreach (KeyValuePair<string, int> kvp in mp)
 49             {
 50                 Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
 51                 flag++;
 52                 if (flag >= 10)
 53                     break;
 54             }
 55 
 56         }
 57         //功能一、二、三 4-1对应处理
 58         static void FuncBase(string ifName, bool isShowWords)
 59         {
 60             Dictionary<String, int> mp = new Dictionary<string, int>();//key:单词,value:词频
 61             StreamReader ioData = null;
 62             Boolean isFuncF = (ifName.Count() == 0);//是否为功能4-1 重定向文件
 63             try
 64             {
 65                 if (isFuncF)
 66                 {
 67                     //获取控制台重定向文件数据流  2017/9/22 14:29:03
 68                     ioData = new StreamReader(Console.OpenStandardInput());
 69                 }
 70                 else {
 71                     //通过文件路径读取文件内容
 72                     FileStream isr = new FileStream(ifName, FileMode.Open, FileAccess.Read);
 73                     ioData = new StreamReader(isr);
 74                 }
 75                 
 76                 
 77                 try
 78                 {
 79                     string s;
 80                     string[] sl;
 81                     int Freq = 0, wordNum = 0;//Freq,单词出现频率 单词总数(重复也算)
 82                     while ((s = ioData.ReadLine()) != null)
 83                     {
 84                         s = regex1.Replace(s, " ");
 85                         s = regex3.Replace(s, "$1");
 86                         s = regex2.Replace(s, " ");
 87                         s = s.ToLower();//小写单词 避免单词大小写造成同一个单词被记作不同单词
 88                         sl = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
 89 
 90                         foreach (string sElem in sl)
 91                         {
 92                             wordNum++;
 93                             if (!mp.ContainsKey(sElem))
 94                             {
 95                                 mp.Add(sElem, 1);
 96                             }
 97                             else
 98                             {
 99                                 mp.TryGetValue(sElem, out Freq);
100                                 mp.Remove(sElem);
101                                 mp.Add(sElem, (Freq + 1));
102 
103                             }
104                         }
105                     }
106                 }
107                 finally
108                 {
109                     ioData.Close();
110 
111                     //功能4-1 输入与输出之间有换行
112                     if (isFuncF)
113                     {
114                         Console.WriteLine("\r\n");
115                     }
116 
117                     //不重复单词数
118                     if (isShowWords)
119                     {
120                         Console.WriteLine("{0,-20}{1,10}  words", "total", mp.Count);//功能二和三
121                     }
122                     else
123                     {
124                         Console.WriteLine("{0,-20}{1,10}", "total", mp.Count);//功能一 功能4-1
125                     }
126 
127                     ////功能4-1 输出总数与单次频率显示之间无换行,但是功能一、二、三有换行
128                     if (!isFuncF)
129                     {
130                         Console.WriteLine("\r\n");
131                     }
132                    
133                     // ling 对字典排序 取TOP10 单词
134                     mp = mp.OrderByDescending(r => r.Value).ToDictionary(r => r.Key, r => r.Value);//排序
135                     int flag = 0;
136                     foreach (KeyValuePair<string, int> kvp in mp)
137                     {
138                         Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
139                         flag++;
140                         if (flag >= 10)
141                             break;
142                     }
143                 }
144             }
145             catch (IOException e)
146             {
147 
148             }
149         }
150 
151         //功能一
152         static void FuncOne(string[] argsT)
153         {
154             if (argsT[0].Equals("-s") && IsFile(argsT[1]))
155             {
156                 
157                 FuncBase(argsT[1], false);
158 
159             }
160             else
161             {
162                 Console.WriteLine("无效参数");
163             }
164 
165         }
166         //功能二/三选择
167         static void FuncTwoThreeFour(string[] argS)
168         {
169             string argT = "";
170             argT = argS[0];
171             if (IsDir(argT))
172             {
173                 FuncThree(argT);
174             }
175             else if (argT.Equals("-s")) {
176                 if (Console.IsInputRedirected)
177                 {
178                     FuncBase("", false);
179                 }
180                 else {
181                     Console.WriteLine("无效参数");
182                 }
183                 
184             }
185             else
186             {
187                 FuncTwo(argT);
188 
189             }
190         }
191 
192         //功能二 参数文件名 不带后缀
193         static void FuncTwo(string fName)
194         {
195             fName = fName + fExtension;
196             if (IsFile(fName))
197             {
198                 FuncBase(fName, true);
199 
200             }
201             else
202             {
203                 Console.WriteLine("无效参数");
204             }
205         }
206 
207         //功能三 输入为文件目录 对该目录下txt文本统计字数
208         static void FuncThree(string dicName)
209         {
210             //获取txt文件列表 完整路径+带后缀的文件名
211             List<String> filesPath = ScanDir(dicName);
212             int flag = 0;//用于控制横线格式输出
213             String filePath = "";
214             for (flag =0;flag < filesPath.Count;flag++)
215             {
216                 if (flag != 0 )
217                 {
218                     Console.WriteLine("----");
219                 }
220                 filePath = filesPath[flag];
221                 Console.WriteLine(Path.GetFileNameWithoutExtension(filePath));
222                 FuncBase(filePath, true);
223             }
224         }
225 
226         //功能四 从dos输入一段文字 进行统计
227         static void FuncFour(string fContent)
228         {
229             FuncBaseT(fContent);
230         }
231         //判断是否为目录
232         static bool IsDir(String fDir)
233         {
234             bool res = false;
235             if (Directory.Exists(fDir))
236             {
237                 res = true;
238             }
239             return res;
240         }
241         //判断是否为文件
242         static bool IsFile(String fPos)
243         {
244             bool res = false;
245             if (File.Exists(fPos))
246             {
247                 res = true;
248             }
249             return res;
250         }
251         //扫描目录 扫描拓展名为.txt
252         static List<string> ScanDir(String fFir)
253         {
254             List<string> filesPath = new List<string>();
255             DirectoryInfo dir = new DirectoryInfo(fFir);
256             //fFir为某个目录,如: “D:\Program Files”
257             FileInfo[] inf = dir.GetFiles();
258             //int fNum = 0;
259             foreach (FileInfo finf in inf)
260             {
261                 if (finf.Extension.Equals(fExtension))
262                 {
263                     filesPath.Add(finf.FullName);
264                 }
265             }
266             return filesPath;
267         }
268         static void Main(string[] args)
269         {
270             int argNum = 0;//输入参数个数决定功能
271             argNum = args.Length;
272             //零个参数 功能4-2 换行接受字符串
273             //1个参数 功能二或者功能三 ,如果为参数1为目录则为功能三,若为文件则为功能二,若为“-s”则可能为功能4-1(重定向)
274             //2个参数 功能一
275             //其他情况 参数无效
276             switch (argNum)
277             {
278                 case 0:
279                 //    Console.ReadLine(); //接收字符串
280                     ConsoleKeyInfo consoleKeyInfo;//获得输入键盘输入 2017/9/25 19:43:47
281                     Console.TreatControlCAsInput = true;//将复制Ctrl+C作为普通输入
282                     String input = "";//输入字符串
283                     consoleKeyInfo = Console.ReadKey();//获取字符串
284                     input += consoleKeyInfo.KeyChar;
285                     /*warning 多行输入操作 不支持单词删除后再输入单词 不允许回退操作 不支持特殊功能键操作*/
286                     // F6 退出 Ctrl+z
287                     while (!(consoleKeyInfo.Key == ConsoleKey.F6 || (consoleKeyInfo.Modifiers == ConsoleModifiers.Control && consoleKeyInfo.Key == ConsoleKey.Z)))
288                     {
289                         consoleKeyInfo = Console.ReadKey();
290                         if (consoleKeyInfo.Key == ConsoleKey.Enter)
291                         {
292                             input += Environment.NewLine;//输入字符串换行,则在字符串尾部加上系统的换行符
293                             Console.SetCursorPosition(0, Console.CursorTop + 1);//换行之后,控制台光标移动到新的一行首部
294                         }
295                         input += consoleKeyInfo.KeyChar;
296                     }
297                     Console.WriteLine("\r\n");//输入字符串和输出结果直接的空行
298                     FuncFour(input);
299                     break;
300                 case 1:
301                     FuncTwoThreeFour(args);
302                     break;
303                 case 2:
304                     FuncOne(args);
305                     break;
306                 default:
307                     Console.WriteLine("无效参数"); break;
308 
309             }
310 
311         }
312     }
313 }
View Code

  其实通过观察,每种功能的输出大同小异,输入的参数个数也有规律可循。功能一是输入两个参数;功能二和功能三是输入一个参数,通过判断这个参数是否为目录或者文件,来决定程序的功能;功能四可认为是两个小功能,功能4-1是一个参数输入和重定向文件输入,而功能4-2则是无参数输入,然后换行,接着输入的就是待测试的字符串。因此可以通过判断输入参数的个数和检验参数的形式来决定程序执行什么功能。于是,我将四个功能分别抽象成四个函数,主函数只负责传递参数,四个功能函数检验参数合法性。同时将输出形式分别用函数funcBaseT和函数funcBase来完成,由于功能一、二、三和功能4-1处理输入输出时基本一致,于是函数funcBase作为四个功能的输出处理函数,而功能4-2输入和输出和其他三种功能存在差异,所以我单独用一个函数funcBaseT负责其输出。

五、程序运行结果截图

  • 功能一:

  • 功能二:

  • 功能三:

  • 功能四:

可分解为功能4-1和功能4-2(手动输入和复制粘贴ctrl+C)。

六、关于词频统计的PSP

  就9/18日晚上正式写代码开始,预计的总时间与实际消耗时间差了36分钟左右。主要花费的时间是在做与程序相关的一些技术原型和对程序整体运行的流程设计。实际开始写代码,占据(9/18)总时间比例46.9%。事实上还有9/17晚上和9/18早上做一些技术原型花的时间(约600min)并没算进来。