Partager via


Guide pratique pour utiliser LINQ pour interroger des chaînes

Les chaînes sont stockées sous la forme d’une séquence de caractères. En tant que séquence de caractères, elles peuvent être interrogées à l’aide de LINQ. Dans cet article, plusieurs exemples de requêtes permettent d’interroger des chaînes de caractères ou de mots différents, de filtrer des chaînes ou de combiner des requêtes avec des expressions régulières.

Comment rechercher des caractères dans une chaîne

L’exemple suivant interroge une chaîne pour déterminer le nombre de chiffres qu’elle contient.

string aString = "ABCDE99F-J74-12-89A";

// Select only those characters that are numbers
var stringQuery = from ch in aString
                  where Char.IsDigit(ch)
                  select ch;

// Execute the query
foreach (char c in stringQuery)
    Console.Write(c + " ");

// Call the Count method on the existing query.
int count = stringQuery.Count();
Console.WriteLine($"Count = {count}");

// Select all characters before the first '-'
var stringQuery2 = aString.TakeWhile(c => c != '-');

// Execute the second query
foreach (char c in stringQuery2)
    Console.Write(c);
/* Output:
  Output: 9 9 7 4 1 2 8 9
  Count = 8
  ABCDE99F
*/

La requête précédente montre comment traiter une chaîne comme une séquence de caractères.

Guide pratique pour compter les occurrences d’un mot dans une chaîne

L’exemple suivant montre comment utiliser une requête LINQ pour compter les occurrences d’un mot spécifié dans une chaîne. Pour effectuer le décompte, la méthode Split est d’abord appelée pour créer un tableau de mots. La méthode Split a un coût de performance. Si la seule opération sur la chaîne consiste à compter les mots, il est préférable d’utiliser les méthodes Matches ou IndexOf.

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 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. 
    """;

string searchTerm = "data";

//Convert the string into an array of words
char[] separators = ['.', '?', '!', ' ', ';', ':', ','];
string[] source = text.Split(separators, StringSplitOptions.RemoveEmptyEntries);

// Create the query.  Use the InvariantCultureIgnoreCase comparison to match "data" and "Data"
var matchQuery = from word in source
                 where word.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase)
                 select word;

// Count the matches, which executes the query.
int wordCount = matchQuery.Count();
Console.WriteLine($"""{wordCount} occurrences(s) of the search term "{searchTerm}" were found.""");
/* Output:
   3 occurrences(s) of the search term "data" were found.
*/

La requête précédente montre comment vous pouvez afficher des chaînes sous forme de séquences de mots, après avoir fractionné une chaîne en une séquence de mots.

Comment trier ou filtrer des données texte par mot ou par champ

L’exemple suivant montre comment trier les lignes d’un texte structuré, telles que des valeurs séparées par des virgules, à l’aide d’un champ de la ligne. Le champ peut être spécifié dynamiquement au moment de l’exécution. Supposons que les champs du fichier scores.csv représentent le numéro d’ID d’un étudiant, suivi d’une série de quatre résultats de tests :

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

La requête suivante trie les lignes en fonction de la note obtenue au premier examen, stockée dans la deuxième colonne :

// Create an IEnumerable data source
string[] scores = File.ReadAllLines("scores.csv");

// Change this to any value from 0 to 4.
int sortField = 1;

Console.WriteLine($"Sorted highest to lowest by field [{sortField}]:");

// Split the string and sort on field[num]
var scoreQuery = from line in scores
                 let fields = line.Split(',')
                 orderby fields[sortField] descending
                 select line;

foreach (string str in scoreQuery)
{
    Console.WriteLine(str);
}
/* Output (if sortField == 1):
   Sorted highest to lowest by field [1]:
    116, 99, 86, 90, 94
    120, 99, 82, 81, 79
    111, 97, 92, 81, 60
    114, 97, 89, 85, 82
    121, 96, 85, 91, 60
    122, 94, 92, 91, 91
    117, 93, 92, 80, 87
    118, 92, 90, 83, 78
    113, 88, 94, 65, 91
    112, 75, 84, 91, 39
    119, 68, 79, 88, 92
    115, 35, 72, 91, 70
 */

La requête précédente montre comment vous pouvez manipuler des chaînes en les divisant en champs et en interrogeant les champs individuels.

Comment rechercher des phrases contenant des mots spécifiques

L’exemple suivant montre comment rechercher dans un fichier texte les phrases contenant des correspondances pour chacun d’un ensemble de mots spécifié. Même si le tableau des termes de recherche est codé en dur, il peut également être alimenté de manière dynamique au moment de l’exécution. La requête retourne les phrases qui contiennent les mots « Historiquement », « données » et « intégré ».

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 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.
""";

// Split the text block into an array of sentences.
string[] sentences = text.Split(['.', '?', '!']);

// Define the search terms. This list could also be dynamically populated at run time.
string[] wordsToMatch = [ "Historically", "data", "integrated" ];

// Find sentences that contain all the terms in the wordsToMatch array.
// Note that the number of terms to match is not specified at compile time.
char[] separators = ['.', '?', '!', ' ', ';', ':', ','];
var sentenceQuery = from sentence in sentences
                    let w = sentence.Split(separators,StringSplitOptions.RemoveEmptyEntries)
                    where w.Distinct().Intersect(wordsToMatch).Count() == wordsToMatch.Count()
                    select sentence;

foreach (string str in sentenceQuery)
{
    Console.WriteLine(str);
}
/* Output:
Historically, the world of data and the world of objects have not been well integrated
*/

La requête divise d’abord le texte en phrases, puis chaque phrase en un tableau de chaînes qui contient chacun des mots. Pour chacun de ces tableaux, la méthode Distinct supprime tous les mots en double, puis la requête effectue une opération Intersect sur le tableau de mots et le tableau wordsToMatch. Si le nombre de l’intersection est identique à celui du tableau wordsToMatch, cela signifie que tous les mots ont été trouvés et la phrase d’origine est donc retournée.

L’appel à Split utilise les signes de ponctuation comme séparateurs afin de les supprimer de la chaîne. Si vous ne supprimez pas la ponctuation, vous pourriez par exemple avoir une chaîne « Historiquement » qui ne correspondrait pas à « Historiquement » dans le tableau wordsToMatch. Vous devrez peut-être utiliser des séparateurs supplémentaires, en fonction des types de ponctuation présents dans le texte source.

Guide pratique pour combiner des requêtes LINQ avec des expressions régulières

L’exemple suivant montre comment utiliser la classe Regex pour créer une expression régulière permettant une correspondance plus complexe dans les chaînes de texte. La requête LINQ facilite le filtrage des fichiers à parcourir à l’aide de l’expression régulière, et facilite également la personnalisation des résultats.

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

// Take a snapshot of the file system.
var fileList = from file in Directory.GetFiles(startFolder, "*.*", SearchOption.AllDirectories)
                let fileInfo = new FileInfo(file)
                select fileInfo;

// Create the regular expression to find all things "Visual".
System.Text.RegularExpressions.Regex searchTerm =
    new System.Text.RegularExpressions.Regex(@"microsoft.net.(sdk|workload)");

// Search the contents of each .htm file.
// Remove the where clause to find even more matchedValues!
// This query produces a list of files where a match
// was found, and a list of the matchedValues in that file.
// Note: Explicit typing of "Match" in select clause.
// This is required because MatchCollection is not a
// generic IEnumerable collection.
var queryMatchingFiles =
    from file in fileList
    where file.Extension == ".txt"
    let fileText = File.ReadAllText(file.FullName)
    let matches = searchTerm.Matches(fileText)
    where matches.Count > 0
    select new
    {
        name = file.FullName,
        matchedValues = from System.Text.RegularExpressions.Match match in matches
                        select match.Value
    };

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

foreach (var v in queryMatchingFiles)
{
    // Trim the path a bit, then write
    // the file name in which a match was found.
    string s = v.name.Substring(startFolder.Length - 1);
    Console.WriteLine(s);

    // For this file, write out all the matching strings
    foreach (var v2 in v.matchedValues)
    {
        Console.WriteLine($"  {v2}");
    }
}

Vous pouvez également interroger l’objet MatchCollection retourné par une recherche RegEx. Seule la valeur de chaque correspondance est produite dans les résultats. Cependant, il est également possible d’utiliser LINQ pour effectuer toutes sortes de filtrages, de tris et de regroupements sur cette collection. Étant donné que MatchCollection est une collection IEnumerable non générique, vous devez indiquer explicitement le type de la variable de plage dans la requête.