Instrukcje: używanie LINQ do wykonywania zapytań dotyczących plików i katalogów

Wiele operacji systemu plików jest zasadniczo zapytań i dlatego dobrze nadaje się do podejścia LINQ. Te zapytania nie są destruktywne. Nie zmieniają zawartości oryginalnych plików ani folderów. Zapytania nie powinny powodować żadnych skutków ubocznych. Ogólnie rzecz biorąc, każdy kod (w tym zapytania, które wykonują operacje tworzenia/aktualizowania/usuwania), które modyfikują dane źródłowe, powinny być oddzielone od kodu, który tylko wykonuje zapytania dotyczące danych.

Istnieje pewna złożoność związana z tworzeniem źródła danych, które dokładnie reprezentuje zawartość systemu plików i bezpiecznie obsługuje wyjątki. Przykłady w tej sekcji tworzą kolekcję FileInfo migawek obiektów reprezentujących wszystkie pliki w określonym folderze głównym i wszystkich jego podfolderach. Rzeczywisty stan każdego z nich FileInfo może ulec zmianie w czasie między rozpoczęciem i zakończeniem wykonywania zapytania. Można na przykład utworzyć listę FileInfo obiektów do użycia jako źródło danych. Jeśli spróbujesz uzyskać dostęp do Length właściwości w zapytaniu, FileInfo obiekt próbuje uzyskać dostęp do systemu plików w celu zaktualizowania wartości Length. Jeśli plik już nie istnieje, otrzymasz element FileNotFoundException w zapytaniu, mimo że nie wysyłasz zapytań bezpośrednio do systemu plików.

Zapytanie o pliki o określonym atrybucie lub nazwie

W tym przykładzie pokazano, jak znaleźć wszystkie pliki, które mają określone rozszerzenie nazwy pliku (na przykład ".txt") w określonym drzewie katalogów. Pokazuje również, jak zwrócić najnowszy lub najstarszy plik w drzewie na podstawie czasu utworzenia. Może być konieczne zmodyfikowanie pierwszego wiersza wielu przykładów niezależnie od tego, czy używasz tego kodu w systemie Windows, Mac lub Linux.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);
var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                where file.Extension == ".txt"
                orderby file.Name
                select file;

// Uncomment this block to see the full query
// foreach (FileInfo fi in fileQuery)
// {
//    Console.WriteLine(fi.FullName);
// }

var newestFile = (from file in fileQuery
                  orderby file.CreationTime
                  select new { file.FullName, file.CreationTime })
                  .Last();

Console.WriteLine($"\r\nThe newest .txt file is {newestFile.FullName}. Creation time: {newestFile.CreationTime}");

Jak grupować pliki według rozszerzenia

W tym przykładzie pokazano, jak linQ może służyć do wykonywania zaawansowanych operacji grupowania i sortowania na listach plików lub folderów. Przedstawiono również sposób stronicowania danych wyjściowych w oknie konsoli przy użyciu Skip metod i Take .

Poniższe zapytanie pokazuje, jak zgrupować zawartość określonego drzewa katalogów według rozszerzenia nazwy pliku.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

int trimLength = startFolder.Length;

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

var queryGroupByExt = from file in fileList
                      group file by file.Extension.ToLower() into fileGroup
                      orderby fileGroup.Count(), fileGroup.Key
                      select fileGroup;

// Iterate through the outer collection of groups.
foreach (var filegroup in queryGroupByExt.Take(5))
{
    Console.WriteLine($"Extension: {filegroup.Key}");
    var resultPage = filegroup.Take(20);

    //Execute the resultPage query
    foreach (var f in resultPage)
    {
        Console.WriteLine($"\t{f.FullName.Substring(trimLength)}");
    }
    Console.WriteLine();
}

Dane wyjściowe z tego programu mogą być długie, w zależności od szczegółów lokalnego systemu plików i ustawień startFolder . Aby włączyć wyświetlanie wszystkich wyników, w tym przykładzie pokazano, jak stronicować wyniki. Wymagana jest pętla zagnieżdżona foreach , ponieważ każda grupa jest wyliczana oddzielnie.

Jak wykonywać zapytania dotyczące łącznej liczby bajtów w zestawie folderów

W tym przykładzie pokazano, jak pobrać całkowitą liczbę bajtów używanych przez wszystkie pliki w określonym folderze i wszystkie jego podfoldery. Metoda Sum dodaje wartości wszystkich elementów wybranych w klauzuli select . To zapytanie można zmodyfikować, aby pobrać największy lub najmniejszy plik w określonym drzewie katalogów, wywołując metodę Min lub Max zamiast Sum.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

var fileList = Directory.GetFiles(startFolder, "*.*", SearchOption.AllDirectories);

var fileQuery = from file in fileList
                let fileLen = new FileInfo(file).Length
                where fileLen > 0
                select fileLen;

// Cache the results to avoid multiple trips to the file system.
long[] fileLengths = fileQuery.ToArray();

// Return the size of the largest file
long largestFile = fileLengths.Max();

// Return the total number of bytes in all the files under the specified folder.
long totalBytes = fileLengths.Sum();

Console.WriteLine($"There are {totalBytes} bytes in {fileList.Count()} files under {startFolder}");
Console.WriteLine($"The largest file is {largestFile} bytes.");

Ten przykład rozszerza poprzedni przykład, aby wykonać następujące czynności:

  • Jak pobrać rozmiar w bajtach największego pliku.
  • Jak pobrać rozmiar w bajtach najmniejszego pliku.
  • Jak pobrać FileInfo obiekt największy lub najmniejszy plik z co najmniej jednego folderu w określonym folderze głównym.
  • Jak pobrać sekwencję, taką jak 10 największych plików.
  • Jak porządkować pliki w grupach na podstawie rozmiaru pliku w bajtach, ignorując pliki, które są mniejsze niż określony rozmiar.

Poniższy przykład zawiera pięć oddzielnych zapytań, które pokazują, jak wykonywać zapytania i grupować pliki w zależności od rozmiaru pliku w bajtach. Możesz zmodyfikować te przykłady, aby za pomocą zapytania bazować na innej właściwości FileInfo obiektu.

// Return the FileInfo object for the largest file
// by sorting and selecting from beginning of list
FileInfo longestFile = (from file in fileList
                        let fileInfo = new FileInfo(file)
                        where fileInfo.Length > 0
                        orderby fileInfo.Length descending
                        select fileInfo
                        ).First();

Console.WriteLine($"The largest file under {startFolder} is {longestFile.FullName} with a length of {longestFile.Length} bytes");

//Return the FileInfo of the smallest file
FileInfo smallestFile = (from file in fileList
                         let fileInfo = new FileInfo(file)
                         where fileInfo.Length > 0
                         orderby fileInfo.Length ascending
                         select fileInfo
                        ).First();

Console.WriteLine($"The smallest file under {startFolder} is {smallestFile.FullName} with a length of {smallestFile.Length} bytes");

//Return the FileInfos for the 10 largest files
var queryTenLargest = (from file in fileList
                       let fileInfo = new FileInfo(file)
                       let len = fileInfo.Length
                       orderby len descending
                       select fileInfo
                      ).Take(10);

Console.WriteLine($"The 10 largest files under {startFolder} are:");

foreach (var v in queryTenLargest)
{
    Console.WriteLine($"{v.FullName}: {v.Length} bytes");
}

// Group the files according to their size, leaving out
// files that are less than 200000 bytes.
var querySizeGroups = from file in fileList
                      let fileInfo = new FileInfo(file)
                      let len = fileInfo.Length
                      where len > 0
                      group fileInfo by (len / 100000) into fileGroup
                      where fileGroup.Key >= 2
                      orderby fileGroup.Key descending
                      select fileGroup;

foreach (var filegroup in querySizeGroups)
{
    Console.WriteLine($"{filegroup.Key}00000");
    foreach (var item in filegroup)
    {
        Console.WriteLine($"\t{item.Name}: {item.Length}");
    }
}

Aby zwrócić co najmniej jeden pełny FileInfo obiekt, najpierw zapytanie musi zbadać każdy z nich w źródle danych, a następnie posortować je według wartości właściwości Length. Następnie może zwrócić pojedynczą lub sekwencję o największej długości. Użyj polecenia First , aby zwrócić pierwszy element na liście. Użyj polecenia Take , aby zwrócić pierwszą n liczby elementów. Określ kolejność sortowania malejącego, aby umieścić najmniejsze elementy na początku listy.

Jak wykonywać zapytania dotyczące zduplikowanych plików w drzewie katalogów

Czasami pliki o tej samej nazwie mogą znajdować się w więcej niż jednym folderze. W tym przykładzie pokazano, jak wykonywać zapytania dotyczące takich zduplikowanych nazw plików w określonym folderze głównym. W drugim przykładzie pokazano, jak wykonywać zapytania dotyczące plików, których rozmiar i czasy LastWrite również są zgodne.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

// used in WriteLine to keep the lines shorter
int charsToSkip = startFolder.Length;

// var can be used for convenience with groups.
var queryDupNames = from file in fileList
                    group file.FullName.Substring(charsToSkip) by file.Name into fileGroup
                    where fileGroup.Count() > 1
                    select fileGroup;

foreach (var queryDup in queryDupNames.Take(20))
{
    Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

    foreach (var fileName in queryDup.Take(10))
    {
        Console.WriteLine($"\t{fileName}");
    }   
}

Pierwsze zapytanie używa klucza do określenia dopasowania. Znajduje pliki o tej samej nazwie, ale których zawartość może być inna. Drugie zapytanie używa klucza złożonego do dopasowania do trzech właściwości FileInfo obiektu. To zapytanie jest znacznie bardziej prawdopodobne, aby znaleźć pliki o tej samej nazwie i podobnej lub identycznej zawartości.

    string startFolder = """C:\Program Files\dotnet\sdk""";
    // Or
    // string startFolder = "/usr/local/share/dotnet/sdk";

    // Make the lines shorter for the console display
    int charsToSkip = startFolder.Length;

    // Take a snapshot of the file system.
    DirectoryInfo dir = new DirectoryInfo(startFolder);
    IEnumerable<FileInfo> fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

    // Note the use of a compound key. Files that match
    // all three properties belong to the same group.
    // A named type is used to enable the query to be
    // passed to another method. Anonymous types can also be used
    // for composite keys but cannot be passed across method boundaries
    //
    var queryDupFiles = from file in fileList
                        group file.FullName.Substring(charsToSkip) by
                        (Name: file.Name, LastWriteTime: file.LastWriteTime, Length: file.Length )
                        into fileGroup
                        where fileGroup.Count() > 1
                        select fileGroup;

    foreach (var queryDup in queryDupFiles.Take(20))
    {
        Console.WriteLine($"Filename = {(queryDup.Key.ToString() == string.Empty ? "[none]" : queryDup.Key.ToString())}");

        foreach (var fileName in queryDup)
        {
            Console.WriteLine($"\t{fileName}");
        }
    }
}

Jak wykonywać zapytania dotyczące zawartości plików tekstowych w folderze

W tym przykładzie pokazano, jak wykonywać zapytania dotyczące wszystkich plików w określonym drzewie katalogów, otwierać każdy plik i sprawdzać jego zawartość. Tego typu techniki można użyć do tworzenia indeksów lub odwrotnych indeksów zawartości drzewa katalogów. W tym przykładzie jest wykonywane proste wyszukiwanie ciągów. Jednak bardziej złożone typy dopasowywania wzorców można wykonać za pomocą wyrażenia regularnego.

string startFolder = """C:\Program Files\dotnet\sdk""";
// Or
// string startFolder = "/usr/local/share/dotnet/sdk";

DirectoryInfo dir = new DirectoryInfo(startFolder);

var fileList = dir.GetFiles("*.*", SearchOption.AllDirectories);

string searchTerm = "change";

var queryMatchingFiles = from file in fileList
                         where file.Extension == ".txt"
                         let fileText = File.ReadAllText(file.FullName)
                         where fileText.Contains(searchTerm)
                         select file.FullName;

// Execute the query.
Console.WriteLine($"""The term "{searchTerm}" was found in:""");
foreach (string filename in queryMatchingFiles)
{
    Console.WriteLine(filename);
}

Jak porównać zawartość dwóch folderów

W tym przykładzie przedstawiono trzy sposoby porównywania dwóch list plików:

  • Wysyłając zapytanie o wartość logiczną określającą, czy dwie listy plików są identyczne.
  • Wysyłając zapytanie o przecięcie, aby pobrać pliki, które znajdują się w obu folderach.
  • Wykonując zapytanie o różnicę zestawu, aby pobrać pliki, które znajdują się w jednym folderze, ale nie w drugim.

Przedstawione tutaj techniki można dostosować do porównywania sekwencji obiektów dowolnego typu.

Klasa FileComparer pokazana tutaj pokazuje, jak używać niestandardowej klasy porównującej razem z standardowymi operatorami zapytań. Klasa nie jest przeznaczona do użycia w rzeczywistych scenariuszach. Używa on tylko nazwy i długości w bajtach każdego pliku, aby określić, czy zawartość każdego folderu jest taka sama, czy nie. W rzeczywistym scenariuszu należy zmodyfikować ten porównujący, aby przeprowadzić bardziej rygorystyczne sprawdzanie równości.

// This implementation defines a very simple comparison
// between two FileInfo objects. It only compares the name
// of the files being compared and their length in bytes.
class FileCompare : IEqualityComparer<FileInfo>
{
    public bool Equals(FileInfo? f1, FileInfo? f2)
    {
        return (f1?.Name == f2?.Name &&
                f1?.Length == f2?.Length);
    }

    // Return a hash that reflects the comparison criteria. According to the
    // rules for IEqualityComparer<T>, if Equals is true, then the hash codes must
    // also be equal. Because equality as defined here is a simple value equality, not
    // reference identity, it is possible that two or more objects will produce the same
    // hash code.
    public int GetHashCode(FileInfo fi)
    {
        string s = $"{fi.Name}{fi.Length}";
        return s.GetHashCode();
    }
}

public static void CompareDirectories()
{
    string pathA = """C:\Program Files\dotnet\sdk\8.0.104""";
    string pathB = """C:\Program Files\dotnet\sdk\8.0.204""";

    DirectoryInfo dir1 = new DirectoryInfo(pathA);
    DirectoryInfo dir2 = new DirectoryInfo(pathB);

    IEnumerable<FileInfo> list1 = dir1.GetFiles("*.*", SearchOption.AllDirectories);
    IEnumerable<FileInfo> list2 = dir2.GetFiles("*.*", SearchOption.AllDirectories);

    //A custom file comparer defined below
    FileCompare myFileCompare = new FileCompare();

    // This query determines whether the two folders contain
    // identical file lists, based on the custom file comparer
    // that is defined in the FileCompare class.
    // The query executes immediately because it returns a bool.
    bool areIdentical = list1.SequenceEqual(list2, myFileCompare);

    if (areIdentical == true)
    {
        Console.WriteLine("the two folders are the same");
    }
    else
    {
        Console.WriteLine("The two folders are not the same");
    }

    // Find the common files. It produces a sequence and doesn't
    // execute until the foreach statement.
    var queryCommonFiles = list1.Intersect(list2, myFileCompare);

    if (queryCommonFiles.Any())
    {
        Console.WriteLine($"The following files are in both folders (total number = {queryCommonFiles.Count()}):");
        foreach (var v in queryCommonFiles.Take(10))
        {
            Console.WriteLine(v.Name); //shows which items end up in result list
        }
    }
    else
    {
        Console.WriteLine("There are no common files in the two folders.");
    }

    // Find the set difference between the two folders.
    var queryList1Only = (from file in list1
                          select file)
                          .Except(list2, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list1 but not list2 (total number = {queryList1Only.Count()}):");
    foreach (var v in queryList1Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }

    var queryList2Only = (from file in list2
                          select file)
                          .Except(list1, myFileCompare);

    Console.WriteLine();
    Console.WriteLine($"The following files are in list2 but not list1 (total number = {queryList2Only.Count()}:");
    foreach (var v in queryList2Only.Take(10))
    {
        Console.WriteLine(v.FullName);
    }
}

Jak zmienić kolejność pól pliku rozdzielanego

Plik wartości rozdzielanych przecinkami (CSV) to plik tekstowy, który jest często używany do przechowywania danych arkusza kalkulacyjnego lub innych danych tabelarycznych reprezentowanych przez wiersze i kolumny. Split Używając metody do oddzielania pól, można łatwo wykonywać zapytania i manipulować plikami CSV przy użyciu LINQ. W rzeczywistości ta sama technika może służyć do zmiany kolejności części dowolnego ustrukturyzowanego wiersza tekstu; nie jest ograniczona do plików CSV.

W poniższym przykładzie przyjęto założenie, że trzy kolumny reprezentują uczniów "imię", "imię" i "identyfikator". Pola są w porządku alfabetycznym na podstawie nazwisk rodzinnych uczniów. Zapytanie tworzy nową sekwencję, w której jest wyświetlana pierwsza kolumna ID, a następnie druga kolumna, która łączy imię i nazwisko ucznia oraz nazwę rodzinną. Wiersze są zmieniane zgodnie z polem ID. Wyniki są zapisywane w nowym pliku, a oryginalne dane nie są modyfikowane. Poniższy tekst przedstawia zawartość pliku spreadsheet1.csv używanego w poniższym przykładzie:

Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121

Poniższy kod odczytuje plik źródłowy i zmienia kolejność każdej kolumny w pliku CSV, aby zmienić kolejność kolumn:

string[] lines = File.ReadAllLines("spreadsheet1.csv");

// Create the query. Put field 2 first, then
// reverse and combine fields 0 and 1 from the old field
IEnumerable<string> query = from line in lines
                            let fields = line.Split(',')
                            orderby fields[2]
                            select $"{fields[2]}, {fields[1]} {fields[0]}";

File.WriteAllLines("spreadsheet2.csv", query.ToArray());

/* Output to spreadsheet2.csv:
111, Svetlana Omelchenko
112, Claire O'Donnell
113, Sven Mortensen
114, Cesar Garcia
115, Debra Garcia
116, Fadi Fakhouri
117, Hanying Feng
118, Hugo Garcia
119, Lance Tucker
120, Terry Adams
121, Eugene Zabokritski
122, Michael Tucker
*/

Jak podzielić plik na wiele plików przy użyciu grup

W tym przykładzie pokazano jeden ze sposobów scalania zawartości dwóch plików, a następnie tworzenia zestawu nowych plików, które organizują dane w nowy sposób. Zapytanie używa zawartości dwóch plików. Poniższy tekst przedstawia zawartość pierwszego pliku, names1.txt:

Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra

Drugi plik, names2.txt, zawiera inny zestaw nazw, z których niektóre są wspólne z pierwszym zestawem:

Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi

Poniższy kod wysyła zapytanie do obu plików, przyjmuje połączenie obu plików, a następnie zapisuje nowy plik dla każdej grupy, zdefiniowany przez pierwszą literę nazwy rodziny:

string[] fileA = File.ReadAllLines("names1.txt");
string[] fileB = File.ReadAllLines("names2.txt");

// Concatenate and remove duplicate names
var mergeQuery = fileA.Union(fileB);

// Group the names by the first letter in the last name.
var groupQuery = from name in mergeQuery
                 let n = name.Split(',')[0]
                 group name by n[0] into g
                 orderby g.Key
                 select g;

foreach (var g in groupQuery)
{
    string fileName = $"testFile_{g.Key}.txt";

    Console.WriteLine(g.Key);

    using StreamWriter sw = new StreamWriter(fileName);
    foreach (var item in g)
    {
        sw.WriteLine(item);
        // Output to console for example purposes.
        Console.WriteLine($"   {item}");
    }
}
/* Output:
    A
       Aw, Kam Foo
    B
       Bankov, Peter
       Beebe, Ann
    E
       El Yassir, Mehdi
    G
       Garcia, Hugo
       Guy, Wey Yuan
       Garcia, Debra
       Gilchrist, Beth
       Giakoumakis, Leo
    H
       Holm, Michael
    L
       Liu, Jinghao
    M
       Myrcha, Jacek
       McLin, Nkenge
    N
       Noriega, Fabricio
    P
       Potra, Cristina
    T
       Toyoshima, Tim
 */

Jak dołączać zawartość z plików o różnej zawartości

W tym przykładzie pokazano, jak połączyć dane z dwóch plików rozdzielonych przecinkami, które mają wspólną wartość używaną jako pasujący klucz. Ta technika może być przydatna, jeśli musisz połączyć dane z dwóch arkuszy kalkulacyjnych lub z arkusza kalkulacyjnego i z pliku, który ma inny format, w nowy plik. Możesz zmodyfikować przykład, aby pracować z dowolnym rodzajem tekstu strukturalnego.

Poniższy tekst przedstawia zawartość scores.csv. Plik reprezentuje dane arkusza kalkulacyjnego. Kolumna 1 to identyfikator ucznia, a kolumny od 2 do 5 to wyniki testów.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Poniższy tekst przedstawia zawartość names.csv. Plik reprezentuje arkusz kalkulacyjny zawierający imię i nazwisko ucznia oraz identyfikator ucznia.

Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122

Dołącz zawartość z innych plików, które zawierają powiązane informacje. Plik names.csv zawiera nazwę ucznia oraz numer identyfikacyjny. Scores.csv pliku zawiera identyfikator i zestaw czterech wyników testów. Poniższe zapytanie łączy wyniki z nazwami uczniów przy użyciu identyfikatora jako pasującego klucza. Kod jest pokazany w poniższym przykładzie:

string[] names = File.ReadAllLines(@"names.csv");
string[] scores = File.ReadAllLines(@"scores.csv");

var scoreQuery = from name in names
                  let nameFields = name.Split(',')
                  from id in scores
                  let scoreFields = id.Split(',')
                  where Convert.ToInt32(nameFields[2]) == Convert.ToInt32(scoreFields[0])
                  select $"{nameFields[0]},{scoreFields[1]},{scoreFields[2]},{scoreFields[3]},{scoreFields[4]}";

Console.WriteLine("\r\nMerge two spreadsheets:");
foreach (string item in scoreQuery)
{
    Console.WriteLine(item);
}
Console.WriteLine("{0} total names in list", scoreQuery.Count());
/* Output:
Merge two spreadsheets:
Omelchenko, 97, 92, 81, 60
O'Donnell, 75, 84, 91, 39
Mortensen, 88, 94, 65, 91
Garcia, 97, 89, 85, 82
Garcia, 35, 72, 91, 70
Fakhouri, 99, 86, 90, 94
Feng, 93, 92, 80, 87
Garcia, 92, 90, 83, 78
Tucker, 68, 79, 88, 92
Adams, 99, 82, 81, 79
Zabokritski, 96, 85, 91, 60
Tucker, 94, 92, 91, 91
12 total names in list
 */

Jak obliczyć wartości kolumn w pliku tekstowym CSV

W tym przykładzie pokazano, jak wykonywać zagregowane obliczenia, takie jak Sum, Average, Min i Max w kolumnach pliku .csv. Przykładowe zasady, które są tutaj wyświetlane, można zastosować do innych typów tekstu strukturalnego.

Poniższy tekst przedstawia zawartość scores.csv. Załóżmy, że pierwsza kolumna reprezentuje identyfikator ucznia, a kolejne kolumny reprezentują wyniki z czterech egzaminów.

111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91

Poniższy tekst przedstawia sposób użycia Split metody w celu przekonwertowania każdego wiersza tekstu na tablicę. Każdy element tablicy reprezentuje kolumnę. Na koniec tekst w każdej kolumnie jest konwertowany na jego reprezentację liczbową.

public class SumColumns
{
    public static void SumCSVColumns(string fileName)
    {
        string[] lines = File.ReadAllLines(fileName);

        // Specifies the column to compute.
        int exam = 3;

        // Spreadsheet format:
        // Student ID    Exam#1  Exam#2  Exam#3  Exam#4
        // 111,          97,     92,     81,     60

        // Add one to exam to skip over the first column,
        // which holds the student ID.
        SingleColumn(lines, exam + 1);
        Console.WriteLine();
        MultiColumns(lines);
    }

    static void SingleColumn(IEnumerable<string> strs, int examNum)
    {
        Console.WriteLine("Single Column Query:");

        // Parameter examNum specifies the column to
        // run the calculations on. This value could be
        // passed in dynamically at run time.

        // Variable columnQuery is an IEnumerable<int>.
        // The following query performs two steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) convert the element at position examNum to an int
        //    and select it.
        var columnQuery = from line in strs
                          let elements = line.Split(',')
                          select Convert.ToInt32(elements[examNum]);

        // Execute the query and cache the results to improve
        // performance. This is helpful only with very large files.
        var results = columnQuery.ToList();

        // Perform aggregate calculations Average, Max, and
        // Min on the column specified by examNum.
        double average = results.Average();
        int max = results.Max();
        int min = results.Min();

        Console.WriteLine($"Exam #{examNum}: Average:{average:##.##} High Score:{max} Low Score:{min}");
    }

    static void MultiColumns(IEnumerable<string> strs)
    {
        Console.WriteLine("Multi Column Query:");

        // Create a query, multiColQuery. Explicit typing is used
        // to make clear that, when executed, multiColQuery produces
        // nested sequences. However, you get the same results by
        // using 'var'.

        // The multiColQuery query performs the following steps:
        // 1) use Split to break each row (a string) into an array
        //    of strings,
        // 2) use Skip to skip the "Student ID" column, and store the
        //    rest of the row in scores.
        // 3) convert each score in the current row from a string to
        //    an int, and select that entire sequence as one row
        //    in the results.
        var multiColQuery = from line in strs
                            let elements = line.Split(',')
                            let scores = elements.Skip(1)
                            select (from str in scores
                                    select Convert.ToInt32(str));

        // Execute the query and cache the results to improve
        // performance.
        // ToArray could be used instead of ToList.
        var results = multiColQuery.ToList();

        // Find out how many columns you have in results.
        int columnCount = results[0].Count();

        // Perform aggregate calculations Average, Max, and
        // Min on each column.
        // Perform one iteration of the loop for each column
        // of scores.
        // You can use a for loop instead of a foreach loop
        // because you already executed the multiColQuery
        // query by calling ToList.
        for (int column = 0; column < columnCount; column++)
        {
            var results2 = from row in results
                           select row.ElementAt(column);
            double average = results2.Average();
            int max = results2.Max();
            int min = results2.Min();

            // Add one to column because the first exam is Exam #1,
            // not Exam #0.
            Console.WriteLine($"Exam #{column + 1} Average: {average:##.##} High Score: {max} Low Score: {min}");
        }
    }
}
/* Output:
    Single Column Query:
    Exam #4: Average:76.92 High Score:94 Low Score:39

    Multi Column Query:
    Exam #1 Average: 86.08 High Score: 99 Low Score: 35
    Exam #2 Average: 86.42 High Score: 94 Low Score: 72
    Exam #3 Average: 84.75 High Score: 91 Low Score: 65
    Exam #4 Average: 76.92 High Score: 94 Low Score: 39
 */

Jeśli plik jest plikiem rozdzielanym tabulatorem, wystarczy zaktualizować argument w metodzie Split na \t.