一、前言

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

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

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

三、需要注意的地方

  需要注意四个功能(实际上是五个功能,功能四分为功能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。

完整代码如下。

 using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions; namespace wf
{
class Program
{
static private String fExtension = ".txt";//文件为文本类型
static private Regex regex1 = new Regex("(\\d|\\r|\\n)");//空格替换数字和换行
static private Regex regex2 = new Regex(@"\p{Pc}|\p{Ps}|\p{Pe}|\p{Pi}|\p{Pf}|\p{Po}|\p{N}|\p{C}");//空格替换标点 短横线不替换
static private Regex regex3 = new Regex("(\\w+)'\\w+");////舍去上撇号之后内容
//功能四-2对应处理
static void FuncBaseT(string s)
{
string[] sl;//保存空格分割出的单词
int Freq = , wordNum = ;//Freq,单词出现频率 单词总数(重复也算)
Dictionary<String, int> mp = new Dictionary<string, int>();//key:单词,value:词频
s = regex1.Replace(s, " ");
s = regex3.Replace(s, "$1");
s = regex2.Replace(s, " ");
s = s.ToLower();//小写单词 避免单词大小写造成同一个单词被记作不同单词
sl = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (string sElem in sl)
{
wordNum++;
if (!mp.ContainsKey(sElem))
{
mp.Add(sElem, );
}
else
{
mp.TryGetValue(sElem, out Freq);
mp.Remove(sElem);
mp.Add(sElem, (Freq + )); }
}
//输出不重复的单词
Console.WriteLine("{0,-20}{1,10}", "total", mp.Count);
// ling 对字典排序 取TOP10 单词
mp = mp.OrderByDescending(r => r.Value).ToDictionary(r => r.Key, r => r.Value);//排序
int flag = ;
foreach (KeyValuePair<string, int> kvp in mp)
{
Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
flag++;
if (flag >= )
break;
} }
//功能一、二、三 4-1对应处理
static void FuncBase(string ifName, bool isShowWords)
{
Dictionary<String, int> mp = new Dictionary<string, int>();//key:单词,value:词频
StreamReader ioData = null;
Boolean isFuncF = (ifName.Count() == );//是否为功能4-1 重定向文件
try
{
if (isFuncF)
{
//获取控制台重定向文件数据流 2017/9/22 14:29:03
ioData = new StreamReader(Console.OpenStandardInput());
}
else {
//通过文件路径读取文件内容
FileStream isr = new FileStream(ifName, FileMode.Open, FileAccess.Read);
ioData = new StreamReader(isr);
} try
{
string s;
string[] sl;
int Freq = , wordNum = ;//Freq,单词出现频率 单词总数(重复也算)
while ((s = ioData.ReadLine()) != null)
{
s = regex1.Replace(s, " ");
s = regex3.Replace(s, "$1");
s = regex2.Replace(s, " ");
s = s.ToLower();//小写单词 避免单词大小写造成同一个单词被记作不同单词
sl = s.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (string sElem in sl)
{
wordNum++;
if (!mp.ContainsKey(sElem))
{
mp.Add(sElem, );
}
else
{
mp.TryGetValue(sElem, out Freq);
mp.Remove(sElem);
mp.Add(sElem, (Freq + )); }
}
}
}
finally
{
ioData.Close(); //功能4-1 输入与输出之间有换行
if (isFuncF)
{
Console.WriteLine("\r\n");
} //不重复单词数
if (isShowWords)
{
Console.WriteLine("{0,-20}{1,10} words", "total", mp.Count);//功能二和三
}
else
{
Console.WriteLine("{0,-20}{1,10}", "total", mp.Count);//功能一 功能4-1
} ////功能4-1 输出总数与单次频率显示之间无换行,但是功能一、二、三有换行
if (!isFuncF)
{
Console.WriteLine("\r\n");
} // ling 对字典排序 取TOP10 单词
mp = mp.OrderByDescending(r => r.Value).ToDictionary(r => r.Key, r => r.Value);//排序
int flag = ;
foreach (KeyValuePair<string, int> kvp in mp)
{
Console.WriteLine("{0,-20}{1,10}", kvp.Key, kvp.Value);
flag++;
if (flag >= )
break;
}
}
}
catch (IOException e)
{ }
} //功能一
static void FuncOne(string[] argsT)
{
if (argsT[].Equals("-s") && IsFile(argsT[]))
{ FuncBase(argsT[], false); }
else
{
Console.WriteLine("无效参数");
} }
//功能二/三选择
static void FuncTwoThreeFour(string[] argS)
{
string argT = "";
argT = argS[];
if (IsDir(argT))
{
FuncThree(argT);
}
else if (argT.Equals("-s")) {
if (Console.IsInputRedirected)
{
FuncBase("", false);
}
else {
Console.WriteLine("无效参数");
} }
else
{
FuncTwo(argT); }
} //功能二 参数文件名 不带后缀
static void FuncTwo(string fName)
{
fName = fName + fExtension;
if (IsFile(fName))
{
FuncBase(fName, true); }
else
{
Console.WriteLine("无效参数");
}
} //功能三 输入为文件目录 对该目录下txt文本统计字数
static void FuncThree(string dicName)
{
//获取txt文件列表 完整路径+带后缀的文件名
List<String> filesPath = ScanDir(dicName);
int flag = ;//用于控制横线格式输出
String filePath = "";
for (flag =;flag < filesPath.Count;flag++)
{
if (flag != )
{
Console.WriteLine("----");
}
filePath = filesPath[flag];
Console.WriteLine(Path.GetFileNameWithoutExtension(filePath));
FuncBase(filePath, true);
}
} //功能四 从dos输入一段文字 进行统计
static void FuncFour(string fContent)
{
FuncBaseT(fContent);
}
//判断是否为目录
static bool IsDir(String fDir)
{
bool res = false;
if (Directory.Exists(fDir))
{
res = true;
}
return res;
}
//判断是否为文件
static bool IsFile(String fPos)
{
bool res = false;
if (File.Exists(fPos))
{
res = true;
}
return res;
}
//扫描目录 扫描拓展名为.txt
static List<string> ScanDir(String fFir)
{
List<string> filesPath = new List<string>();
DirectoryInfo dir = new DirectoryInfo(fFir);
//fFir为某个目录,如: “D:\Program Files”
FileInfo[] inf = dir.GetFiles();
//int fNum = 0;
foreach (FileInfo finf in inf)
{
if (finf.Extension.Equals(fExtension))
{
filesPath.Add(finf.FullName);
}
}
return filesPath;
}
static void Main(string[] args)
{
int argNum = ;//输入参数个数决定功能
argNum = args.Length;
//零个参数 功能4-2 换行接受字符串
//1个参数 功能二或者功能三 ,如果为参数1为目录则为功能三,若为文件则为功能二,若为“-s”则可能为功能4-1(重定向)
//2个参数 功能一
//其他情况 参数无效
switch (argNum)
{
case :
// Console.ReadLine(); //接收字符串
ConsoleKeyInfo consoleKeyInfo;//获得输入键盘输入 2017/9/25 19:43:47
Console.TreatControlCAsInput = true;//将复制Ctrl+C作为普通输入
String input = "";//输入字符串
consoleKeyInfo = Console.ReadKey();//获取字符串
input += consoleKeyInfo.KeyChar;
/*warning 多行输入操作 不支持单词删除后再输入单词 不允许回退操作 不支持特殊功能键操作*/
// F6 退出 Ctrl+z
while (!(consoleKeyInfo.Key == ConsoleKey.F6 || (consoleKeyInfo.Modifiers == ConsoleModifiers.Control && consoleKeyInfo.Key == ConsoleKey.Z)))
{
consoleKeyInfo = Console.ReadKey();
if (consoleKeyInfo.Key == ConsoleKey.Enter)
{
input += Environment.NewLine;//输入字符串换行,则在字符串尾部加上系统的换行符
Console.SetCursorPosition(, Console.CursorTop + );//换行之后,控制台光标移动到新的一行首部
}
input += consoleKeyInfo.KeyChar;
}
Console.WriteLine("\r\n");//输入字符串和输出结果直接的空行
FuncFour(input);
break;
case :
FuncTwoThreeFour(args);
break;
case :
FuncOne(args);
break;
default:
Console.WriteLine("无效参数"); break; } }
}
}

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

五、程序运行结果截图

  • 功能一:

词频统计V2.5-LMLPHP

  • 功能二:

词频统计V2.5-LMLPHP

  • 功能三:

词频统计V2.5-LMLPHP词频统计V2.5-LMLPHP

  • 功能四:

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

词频统计V2.5-LMLPHP

词频统计V2.5-LMLPHP

词频统计V2.5-LMLPHP

六、关于词频统计的PSP

词频统计V2.5-LMLPHP

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

05-02 02:39