C# LINQ学习笔记三:LINQ to OBJECT之操作字符串
一、统计单词在字符串中出现的次数
请注意,若要执行计数,请先调用Split方法来创建词数组。Split方法存在性能开销,如果对字符串执行的唯一操作是计数词,则应考虑改用Matches或
IndexOf方法。
class Program { static void Main(string[] args) { #region LINQ 统计单词在字符串中出现的次数 const string text = @"Historically, the world of data and the world of objects" + @" have not been well integrated. Programmers work in C# or Visual Basic" + @" and also in SQL or XQuery. On the one side are concepts such as classes," + @" objects, fields, inheritance, and .NET Framework APIs. On the other side" + @" are tables, columns, rows, nodes, and separate languages for dealing with" + @" them. Data types often require translation between the two worlds; there are" + @" different standard functions. Because the object world has no notion of query, a" + @" query can only be represented as a string without compile-time type checking or" + @" IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to" + @" objects in memory is often tedious and error-prone."; const string searchWord = "data"; //字符串转换成数组 var source = text.Split(new[] { '.', '?', '!', ' ', ';', ':', ',' }, StringSplitOptions.RemoveEmptyEntries); //创建查询,并忽略大小写比较。 var query = from word in source where string.Equals(word, searchWord, StringComparison.InvariantCultureIgnoreCase) select word; //统计匹配数量 var wordCount = query.Count(); Console.WriteLine($"{wordCount} occurrences(s) of the search word \"{searchWord}\" were found."); Console.Read(); #endregion } }
View Code
运行结果如下:
二、查询包含指定一组单词的句子
此示例演示如何查找文本文件中包含指定一组单词中每个单词匹配项的句子。虽然在此示例中搜索条件数组是硬编码的,但也可以在运行时动态填充此
数组。
class Program { static void Main(string[] args) { #region LINQ 查询包含指定一组单词的句子 const string text = @"Historically, the world of data and the world of objects " + @"have not been well integrated. Programmers work in C# or Visual Basic " + @"and also in SQL or XQuery. On the one side are concepts such as classes, " + @"objects, fields, inheritance, and .NET Framework APIs. On the other side " + @"are tables, columns, rows, nodes, and separate languages for dealing with " + @"them. Data types often require translation between the two worlds; there are " + @"different standard functions. Because the object world has no notion of query, a " + @"query can only be represented as a string without compile-time type checking or " + @"IntelliSense support in the IDE. Transferring data from SQL tables or XML trees to " + @"objects in memory is often tedious and error-prone."; //将文本块切割成数组 var sentences = text.Split('.', '?', '!'); //定义搜索条件,此列表可以在运行时动态添加。 string[] wordsToMatch = { "Historically", "data", "integrated" }; var query = from sentence in sentences let t = sentence.Split(new char[] { '.', '?', '!', ' ', ';', ':', ',' }, StringSplitOptions.RemoveEmptyEntries) where t.Distinct().Intersect(wordsToMatch).Count() == wordsToMatch.Length //去重,取交集后的数量对比。 select sentence; foreach (var sentence in query) { Console.WriteLine(sentence); } Console.Read(); #endregion } }
View Code
运行结果如下:
查询运行时首先将文本拆分成句子,然后将句子拆分成包含每个单词的字符串数组。对于每个这样的数组,Distinct<TSource> 方法移除所有重复的单词,
然后查询对单词数组和wordstoMatch数组执行Intersect<TSource>操作。如果交集的计数与wordsToMatch数组的计数相同,则在单词中找到了所有的单词,
然后返回原始句子。
在对Split的调用中,使用标点符号作为分隔符,以从字符串中移除标点符号。如果您没有这样做,则假如您有一个字符串“Historically,”,该字符串不会
与wordsToMatch数组中的“Historically”相匹配。根据源文本中标点的类型,您可能必须使用其他分隔符。
三、在字符串中查询字符
因为String类实现泛型IEnumerable<T>接口,所以可以将任何字符串作为字符序列进行查询。但是,这不是LINQ的常见用法。若要执行复杂的模式匹配操
作,请使用Regex类。
下面的示例查询一个字符串以确定它包含的数字的数目。
class Program { static void Main(string[] args) { #region LINQ 在字符串中查询字符 const string source = "ABCDE99F-J74-12-89A"; //只选择数字的字符 var digits = from character in source where char.IsDigit(character) select character; Console.Write("Digit:"); foreach (var digit in digits) { Console.Write($"{digit} "); } Console.WriteLine(); //选择第一个"-"之前的所有字符 var query = source.TakeWhile(x => x != '-'); foreach (var character in query) { Console.Write(character); } Console.Read(); #endregion } }
View Code
运行结果如下:
四、正则表达式结合LINQ查询
此示例演示如何使用Regex类创建正则表达式以便在文本字符串中进行更复杂的匹配。使用LINQ查询可以方便地对您要用正则表达式搜索的文件进行准确
筛选以及对结果进行加工。
class Program { static void Main(string[] args) { #region LINQ 正则表达式结合LINQ查询 //请根据不同版本的VS进行路径修改 const string floder = @"C:\Program Files (x86)\Microsoft Visual Studio\"; var fileInfoes = GetFiles(floder); //创建正则表达式来寻找所有的"Visual" var searchTerm = new Regex(@"http://(www.w3.org|www.npmjs.org)"); //搜索每一个“.html”文件 //通过where找到匹配项 //注意:select中的变量要求显示声明其类型,因为MatchCollection不是泛型IEnumerable集合。 var query = from fileInfo in fileInfoes where fileInfo.Extension == ".html" let text = File.ReadAllText(fileInfo.FullName) let matches = searchTerm.Matches(text) where matches.Count > 0 select new { name = fileInfo.FullName, matchValue = from Match match in matches select match.Value }; Console.WriteLine($"The term \"{searchTerm}\" was found in:"); Console.WriteLine(); foreach (var q in query) { //修剪匹配找到的文件中的路径 Console.WriteLine($"name==>{q.name.Substring(floder.Length - 1)}"); //输出找到的匹配值 foreach (var v in q.matchValue) { Console.WriteLine($"matchValue==>{v}"); } //输出空白行 Console.WriteLine(); } Console.Read(); #endregion } /// <summary> /// 获取指定路径的文件信息 /// </summary> /// <param name="path"></param> /// <returns></returns> private static IList<FileInfo> GetFiles(string path) { var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories); return files.Select(file => new FileInfo(file)).ToList(); } }
View Code
运行结果如下:
五、查找两个集合间的差异
此示例演示如何使用LINQ对两个字符串列表进行比较,并输出那些位于text1.txt中但不在text2.txt中的行。
Bankov, PeterHolm, MichaelGarcia, HugoPotra, CristinaNoriega, FabricioAw, Kam FooBeebe, AnnToyoshima, TimGuy, Wey YuanGarcia, Debra
text1.txt
Liu, JinghaoBankov, PeterHolm, MichaelGarcia, HugoBeebe, AnnGilchrist, BethMyrcha, JacekGiakoumakis, LeoMcLin, NkengeEl Yassir, Mehdi
text2.txt
class Program { static void Main(string[] args) { #region LINQ 查找两个集合间的差异 //创建数据源 var text1 = File.ReadAllLines(@"..\..\text1.txt"); var text2 = File.ReadAllLines(@"..\..\text2.txt"); //创建查询,这里必须使用方法语法。 var query = text1.Except(text2); //执行查询 Console.WriteLine("The following lines are in text1.txt but not text2.txt"); foreach (var name in query) { Console.WriteLine(name); } Console.Read(); #endregion } }
View Code
运行结果如下:
注:某些类型的查询操作(如 Except<TSource>、Distinct<TSource>、Union<TSource> 和 Concat<TSource>)只能用基于方法的语法表示。
六、排序或过滤任意单词或字段的文本数据
下面的示例演示如何按结构化文本(如逗号分隔值)行中的任意字段对该文本行进行排序,可在运行时动态指定该字段。
假定scores.csv中的字段表示学生的ID号,后面跟着四个测验分数。
111, 97, 92, 81, 60112, 75, 84, 91, 39113, 88, 94, 65, 91114, 97, 89, 85, 82115, 35, 72, 91, 70116, 99, 86, 90, 94117, 93, 92, 80, 87118, 92, 90, 83, 78119, 68, 79, 88, 92120, 99, 82, 81, 79121, 96, 85, 91, 60122, 94, 92, 91, 91
scores.csv
class Program { static void Main(string[] args) { #region LINQ 排序或过滤任意单词或字段的文本数据 //创建数据源 var scores = File.ReadAllLines(@"..\..\scores.csv"); //可以改为0~4的任意值 const int sortIndex = 1; //演示从方法返回查询变量,非查询结果。 foreach (var score in SplitSortQuery(scores, sortIndex)) { Console.WriteLine(score); } Console.Read(); #endregion } /// <summary> /// 分割字符串排序 /// </summary> /// <param name="scores"></param> /// <param name="num"></param> /// <returns></returns> private static IEnumerable<string> SplitSortQuery(IEnumerable<string> scores, int num) { var query = from line in scores let fields = line.Split(',') orderby fields[num] descending select line; return query; } }
View Code
运行结果如下:
七、对一个分割的文件的字段重新排序
逗号分隔值 (CSV) 文件是一种文本文件,通常用于存储电子表格数据或其他由行和列表示的表格数据。通过使用Split方法分隔字段,可以非常轻松地使用
LINQ来查询和操作CSV文件。事实上,可以使用此技术来重新排列任何结构化文本行部分。此技术不局限于CSV文件。
在下面的示例中,假定有三列分别代表学生的“姓氏”、“名字”和“ID”,这些字段基于学生的姓氏按字母顺序排列。查询生成一个新序列,其中首先出现的是
ID列,后面的第二列组合了学生的名字和姓氏。根据ID字段重新排列各行,结果保存到新文件,但不修改原始数据。
Adams,Terry,120Fakhouri,Fadi,116Feng,Hanying,117Garcia,Cesar,114Garcia,Debra,115Garcia,Hugo,118Mortensen,Sven,113O'Donnell,Claire,112Omelchenko,Svetlana,111Tucker,Lance,119Tucker,Michael,122Zabokritski,Eugene,121
spread.csv
class Program { static void Main(string[] args) { #region LINQ 对一个分割的文件的字段重新排序 //数据源 var lines = File.ReadAllLines(@"..\..\spread.csv"); //将旧数据的第2列的字段放到第一位,逆向结合第0列和第1列的字段。 var query = from line in lines let t = line.Split(',') orderby t[2] select $"{t[2]} {t[1]} {t[0]}"; foreach (var item in query) { Console.WriteLine(item); } Console.Read(); #endregion } }
View Code
运行结果如下:
八、组合和比较字符串集合
此示例演示如何合并包含文本行的文件,然后排序结果。具体来说,此示例演示如何对两组文本行执行简单的串联、联合和交集。
注:text1.txt及text2.txt与五、的一致。
class Program { static void Main(string[] args) { #region LINQ 组合和比较字符串集合 var text1 = File.ReadAllLines(@"..\..\text1.txt"); var text2 = File.ReadAllLines(@"..\..\text2.txt"); //简单连接并排序,重复保存。 var concatQuery = text1.Concat(text2).OrderBy(x => x); OutputQueryResult(concatQuery, "Simple concatenate and sort,duplicates are preserved:"); //基于默认字符串比较器连接,并删除重名。 var unionQuery = text1.Union(text2).OrderBy(x => x); OutputQueryResult(unionQuery, "Union removes duplicate names:"); //查找在两个文件中出现的名称 var intersectQuery = text1.Intersect(text2).OrderBy(x => x); OutputQueryResult(intersectQuery, "Merge based on intersect:"); //在每个列表中找到匹配的字段,使用concat将两个结果合并,然后使用默认的字符串比较器进行排序。 const string nameMatch = "Garcia"; var matchQuery1 = from name in text1 let t = name.Split(',') where t[0] == nameMatch select name; var matchQuery2 = from name in text2 let t = name.Split(',') where t[0] == nameMatch select name; var temp = matchQuery1.Concat(matchQuery2).OrderBy(x => x); OutputQueryResult(temp, $"Concat based on partial name match \"{nameMatch}\":"); Console.Read(); #endregion } /// <summary> /// 输出查询结果 /// </summary> /// <param name="querys"></param> /// <param name="title"></param> private static void OutputQueryResult(IEnumerable<string> querys, string title) { Console.WriteLine(Environment.NewLine + title); foreach (var query in querys) { Console.WriteLine(query); } Console.WriteLine($"Total {querys.Count()} names in list."); } }
View Code
运行结果如下:
九、从多个源中填充对象集合
不要尝试将内存中的数据或文件系统中的数据与仍在数据库中的数据相联接。此种跨域联接会生成未定义的结果,因为数据库查询和其他类型的源定义联
接运算的方式可能不同。另外,如果数据库中的数据量足够大,则存在此类运算引发内存不足异常的风险。若要将数据库数据与内存中的数据相联接,请首
先对数据库查询调用ToList或ToArray,然后对返回的集合执行联接。
class Program { static void Main(string[] args) { #region LINQ 从多个源中填充对象集合 //spread.csv每行包含姓氏、名字和身份证号,以逗号分隔。例如,Omelchenko,Svetlana,111 var names = File.ReadAllLines(@"..\..\spread.csv"); //scores.csv每行包括身份证号码和四个测试评分,以逗号分隔。例如,111,97,92,81,60 var scores = File.ReadAllLines(@"..\..\scores.csv"); //使用一个匿名的类型合并数据源。 //注:动态创建一个int的考试成绩成员列表。 //跳过分割字符串中的第一项,因为它是学生的身份证,不是一个考试成绩。 var students = from name in names let t1 = name.Split(',') from score in scores let t2 = score.Split(',') where t1[2] == t2[0] select new { FirstName = t1[0], LastName = t1[1], ID = Convert.ToInt32(t1[2]), ExamScores = (from score in t2.Skip(1) select Convert.ToInt32(score)).ToList() }; foreach (var student in students) { Console.WriteLine($"The average score of {student.FirstName} {student.LastName} is {student.ExamScores.Average()}."); } Console.Read(); #endregion } }
View Code
运行结果如下:
十、使用group将一个文件拆分成多个文件
此示例演示一种进行以下操作的方法:合并两个文件的内容,然后创建一组以新方式组织数据的新文件。
注:text1.txt及text2.txt与五、的一致。
class Program { static void Main(string[] args) { #region LINQ 使用group将一个文件拆分成多个文件 var text1 = File.ReadAllLines(@"..\..\text1.txt"); var text2 = File.ReadAllLines(@"..\..\text2.txt"); //并集:连接并删除重复的名字 var mergeQuery = text1.Union(text2); //根据姓氏的首字母对姓名进行分组 var query = from name in mergeQuery let t = name.Split(',') group name by t[0][0] into g orderby g.Key select g; //注意嵌套的 foreach 循环 foreach (var g in query) { var fileName = @"testFile_" + g.Key + ".txt"; Console.WriteLine(g.Key + ":"); //写入文件 using (var sw = new StreamWriter(fileName)) { foreach (var name in g) { sw.WriteLine(name); Console.WriteLine(" " + name); } } } Console.Read(); #endregion } }
View Code
运行结果如下:
十一、向不同的文件中加入内容
此示例演示如何联接两个逗号分隔文件中的数据,这两个文件共享一个用作匹配键的共同值。如果您必须将两个电子表格的数据或一个电子表格和一个其
他格式的文件的数据组合为一个新文件,则此技术很有用,还可以修改此示例以适合任意种类的结构化文本。
class Program { static void Main(string[] args) { #region LINQ 向不同的文件中加入内容 var names = File.ReadAllLines(@"..\..\spread.csv"); var scores = File.ReadAllLines(@"..\..\scores.csv"); //该查询基于ID连接两个不同的电子表格 var query = from name in names let t1 = name.Split(',') from score in scores let t2 = score.Split(',') where t1[2] == t2[0] orderby t1[0] select $"{t1[0]},{t2[1]},{t2[2]},{t2[3]},{t2[4]}"; //输出 OutputQueryResult(query, "Merge two spreadsheets:"); Console.Read(); #endregion } /// <summary> /// 输出查询结果 /// </summary> /// <param name="querys"></param> /// <param name="title"></param> private static void OutputQueryResult(IEnumerable<string> querys, string title) { Console.WriteLine(Environment.NewLine + title); foreach (var query in querys) { Console.WriteLine(query); } Console.WriteLine($"Total {querys.Count()} names in list."); } }
View Code
运行结果如下:
十二、计算一个CSV文本文件中的列值
此示例演示如何对.csv文件的列执行诸如Sum、Average、Min和Max等聚合计算,此示例可以应用于其他类型的结构化文本。
class Program { static void Main(string[] args) { #region LINQ 计算一个CSV文本文件中的列值 var scores = File.ReadAllLines(@"..\..\scores.csv"); //指定要计算的列 const int examNum = 3; //+1表示跳过第一列 //统计单列 SingleColumn(scores, examNum + 1); Console.WriteLine(); //统计多列 MultiColumns(scores); Console.Read(); #endregion } /// <summary> /// 统计单列 /// </summary> /// <param name="lines"></param> /// <param name="examNum"></param> private static void SingleColumn(IEnumerable<string> lines, int examNum) { Console.WriteLine("Single Column Query:"); //查询步骤: //1.分割字符串 //2.对要计算的列的值转换为int var query = from line in lines let t = line.Split(',') select Convert.ToInt32(t[examNum]); //对指定的列进行统计 var average = query.Average(); var max = query.Max(); var min = query.Min(); Console.WriteLine($"Exam #{examNum}: Average:{average:##.##} High Score:{max} Low Score:{min}"); } /// <summary> /// 统计多列 /// </summary> /// <param name="lines"></param> private static void MultiColumns(IEnumerable<string> lines) { Console.WriteLine("Multi Column Query:"); //查询步骤: //1.分割字符串 //2.跳过ID列(第一列) //3.将当前行的每个评分都转换成int,并选择整个序列作为一行结果。 var query1 = from line in lines let t1 = line.Split(',') let t2 = t1.Skip(1) select (from t in t2 select Convert.ToInt32(t)); //执行查询并缓存结果以提高性能 var results = query1.ToList(); //找出结果的列数 var count = results[0].Count(); //执行统计 for (var i = 0; i < count; i++) { var query2 = from result in results select result.ElementAt(i); var average = query2.Average(); var max = query2.Max(); var min = query2.Min(); //#1表示第一次考试 Console.WriteLine($"Exam #{i + 1} Average: {average:##.##} High Score: {max} Low Score: {min}"); } } }
View Code
运行结果如下: