一、前言
作业具体要求见[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();
- 困难二: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 }
- 困难三: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 }
- 困难四: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);//排序
- 困难五:C#格式化输出
1 //格式码 2 Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
- 困难六: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 }
- 困难七: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 }
三、需要注意的地方
需要注意四个功能(实际上是五个功能,功能四分为功能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 }
其实通过观察,每种功能的输出大同小异,输入的参数个数也有规律可循。功能一是输入两个参数;功能二和功能三是输入一个参数,通过判断这个参数是否为目录或者文件,来决定程序的功能;功能四可认为是两个小功能,功能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)并没算进来。