32 votes

SQL Server Insertion en masse d'un fichier CSV avec des guillemets incohérents

Est-il possible de BULK INSERT (SQL Server) dans un fichier CSV dont les champs ne sont qu'OCCASIONNELLEMENT entourés de guillemets ? Plus précisément, les guillemets n'entourent que les champs qui contiennent un ",".

En d'autres termes, j'ai des données qui ressemblent à ceci (la première ligne contient les en-têtes) :

id, company, rep, employees
729216,INGRAM MICRO INC.,"Stuart, Becky",523
729235,"GREAT PLAINS ENERGY, INC.","Nelson, Beena",114
721177,GEORGE WESTON BAKERIES INC,"Hogan, Meg",253

Comme les guillemets ne sont pas cohérents, je ne peux pas utiliser '","' comme délimiteur, et je ne sais pas comment créer un fichier de format qui tienne compte de cela.

J'ai essayé d'utiliser ',' comme délimiteur et de le charger dans une table temporaire où chaque colonne est un varchar, puis d'utiliser un traitement maladroit pour supprimer les guillemets, mais cela ne fonctionne pas non plus, car les champs qui contiennent ',' sont divisés en plusieurs colonnes.

Malheureusement, je n'ai pas la possibilité de manipuler le fichier CSV au préalable.

C'est sans espoir ?

Merci d'avance pour tout conseil.

Au fait, j'ai vu ce message Importation en masse SQL à partir de csv mais dans ce cas, CHAQUE champ était systématiquement entouré de guillemets. Donc, dans ce cas, il pourrait utiliser ',' comme délimiteur, puis enlever les guillemets après.

19voto

Macros Points 4564

Il n'est pas possible de faire une insertion en masse pour ce fichier, d'après MSDN :

Pour être utilisable comme fichier de données pour l'importation en masse, un fichier CSV doit respecter les restrictions suivantes :

  • Les champs de données ne contiennent jamais le terminateur de champ.
  • Aucune ou toutes les valeurs d'un champ de données sont placées entre guillemets ("").

( http://msdn.microsoft.com/en-us/library/ms188609.aspx )

Un simple traitement de texte devrait suffire pour que le fichier soit prêt à être importé. Vous pouvez également demander à vos utilisateurs de formater le fichier conformément aux lignes directrices ou d'utiliser d'autres éléments que la virgule comme délimiteur (par exemple, |).

1 votes

Macros - merci pour cela. Cela semble définitif. J'ai pensé à prétraiter le fichier, par exemple en remplaçant toutes les virgules par des points, mais je ne sais pas comment distinguer les virgules qui séparent les champs des virgules dans les valeurs. Existe-t-il un moyen simple de le faire ?

0 votes

Les expressions régulières peuvent être utiles, mais je ne suis pas sûr qu'elles puissent gérer des conditions telles que des virgules multiples à l'intérieur des guillemets et plusieurs chaînes de caractères entre guillemets sur une même ligne. Algorithmiquement, vous pourriez analyser chaque chaîne en remplaçant chaque virgule par un tuyau jusqu'à ce que vous atteigniez un ", auquel cas le remplacement est désactivé jusqu'à ce que vous atteigniez un guillemet fermant. Mais ce n'est peut-être pas la méthode la plus efficace !

1 votes

J'ai eu la même idée : analyser ligne par ligne, champ par champ, voire même caractère par caractère. Avec un peu d'huile de coude, ça pourrait marcher, mais je doute que ce soit très efficace. J'espérais qu'il y aurait une réponse rapide et facile. Il devrait y en avoir une - on dirait que cela doit arriver souvent, puisqu'il semble qu'Excel formate les données de cette manière lorsque vous essayez d'enregistrer une feuille de calcul en tant que fichier CSV. Mais bon.

18voto

Chris Clark Points 1397

Vous allez devoir prétraiter le fichier, point final.

Si vous avez vraiment besoin de faire ça, voici le code. Je l'ai écrit parce que je n'avais absolument pas le choix. C'est un code utilitaire et je n'en suis pas fier, mais il fonctionne. L'approche n'est pas de faire en sorte que SQL comprenne les champs entre guillemets, mais plutôt de manipuler le fichier pour utiliser un délimiteur entièrement différent.

EDIT : Voici le code dans un repo github. Il a été amélioré et est maintenant accompagné de tests unitaires ! https://github.com/chrisclark/Redelim-it

Cette fonction prend un fichier d'entrée et remplacera toutes les virgules de délimitation des champs (PAS les virgules à l'intérieur des champs entre guillemets, juste les virgules de délimitation réelles) par un nouveau délimiteur. Vous pouvez alors dire à sql server d'utiliser le nouveau délimiteur de champ au lieu d'une virgule. Dans la version de la fonction ici, le délimiteur est < TMP > (Je suis sûr que cela n'apparaîtra pas dans le csv original - si c'est le cas, préparez-vous à des explosions).

Donc après avoir exécuté cette fonction, vous importez dans sql en faisant quelque chose comme :

BULK INSERT MyTable
FROM 'C:\FileCreatedFromThisFunction.csv'
WITH
(
FIELDTERMINATOR = '<*TMP*>',
ROWTERMINATOR = '\n'
)

Et sans plus attendre, la terrible, horrible fonction que je m'excuse par avance de vous infliger (edit - j'ai posté un programme fonctionnel qui fait cela au lieu de la simple fonction sur mon blog ici ) :

Private Function CsvToOtherDelimiter(ByVal InputFile As String, ByVal OutputFile As String) As Integer

        Dim PH1 As String = "<*TMP*>"

        Dim objReader As StreamReader = Nothing
        Dim count As Integer = 0 'This will also serve as a primary key'
        Dim sb As New System.Text.StringBuilder

        Try
            objReader = New StreamReader(File.OpenRead(InputFile), System.Text.Encoding.Default)
        Catch ex As Exception
            UpdateStatus(ex.Message)
        End Try

        If objReader Is Nothing Then
            UpdateStatus("Invalid file: " & InputFile)
            count = -1
            Exit Function
        End If

        'grab the first line
    Dim line = reader.ReadLine()
    'and advance to the next line b/c the first line is column headings
    If hasHeaders Then
        line = Trim(reader.ReadLine)
    End If

    While Not String.IsNullOrEmpty(line) 'loop through each line

        count += 1

        'Replace commas with our custom-made delimiter
        line = line.Replace(",", ph1)

        'Find a quoted part of the line, which could legitimately contain commas.
        'In that case we will need to identify the quoted section and swap commas back in for our custom placeholder.
        Dim starti = line.IndexOf(ph1 & """", 0)
        If line.IndexOf("""",0) = 0 then starti=0

        While starti > -1 'loop through quoted fields

            Dim FieldTerminatorFound As Boolean = False

            'Find end quote token (originally  a ",)
            Dim endi As Integer = line.IndexOf("""" & ph1, starti)

            If endi < 0 Then
                FieldTerminatorFound = True
                If endi < 0 Then endi = line.Length - 1
            End If

            While Not FieldTerminatorFound

                'Find any more quotes that are part of that sequence, if any
                Dim backChar As String = """" 'thats one quote
                Dim quoteCount = 0
                While backChar = """"
                    quoteCount += 1
                    backChar = line.Chars(endi - quoteCount)
                End While

                If quoteCount Mod 2 = 1 Then 'odd number of quotes. real field terminator
                    FieldTerminatorFound = True
                Else 'keep looking
                    endi = line.IndexOf("""" & ph1, endi + 1)
                End If
            End While

            'Grab the quoted field from the line, now that we have the start and ending indices
            Dim source = line.Substring(starti + ph1.Length, endi - starti - ph1.Length + 1)

            'And swap the commas back in
            line = line.Replace(source, source.Replace(ph1, ","))

            'Find the next quoted field
            '                If endi >= line.Length - 1 Then endi = line.Length 'During the swap, the length of line shrinks so an endi value at the end of the line will fail
            starti = line.IndexOf(ph1 & """", starti + ph1.Length)

        End While

            line = objReader.ReadLine

        End While

        objReader.Close()

        SaveTextToFile(sb.ToString, OutputFile)

        Return count

    End Function

8voto

murmy Points 41

J'ai trouvé la réponse de Chris très utile, mais je voulais l'exécuter depuis le serveur SQL en utilisant T-SQL (et non CLR), j'ai donc converti son code en code T-SQL. J'ai donc converti son code en code T-SQL. Puis j'ai fait un pas de plus en enveloppant le tout dans une procédure stockée qui fait ce qui suit :

  1. utiliser l'insertion en masse pour importer initialement le fichier CSV
  2. nettoyer les lignes en utilisant le code de Chris
  3. retourner les résultats sous forme de tableau

Pour mes besoins, j'ai encore nettoyé les lignes en supprimant les guillemets autour des valeurs et en convertissant deux guillemets doubles en un guillemet double (je pense que c'est la bonne méthode).

CREATE PROCEDURE SSP_CSVToTable

-- Add the parameters for the stored procedure here
@InputFile nvarchar(4000)
, @FirstLine int

AS

BEGIN

-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

--convert the CSV file to a table
--clean up the lines so that commas are handles correctly

DECLARE @sql nvarchar(4000)
DECLARE @PH1 nvarchar(50)
DECLARE @LINECOUNT int -- This will also serve as a primary key
DECLARE @CURLINE int
DECLARE @Line nvarchar(4000)
DECLARE @starti int
DECLARE @endi int
DECLARE @FieldTerminatorFound bit
DECLARE @backChar nvarchar(4000)
DECLARE @quoteCount int
DECLARE @source nvarchar(4000)
DECLARE @COLCOUNT int
DECLARE @CURCOL int
DECLARE @ColVal nvarchar(4000)

-- new delimiter
SET @PH1 = '†'

-- create single column table to hold each line of file
CREATE TABLE [#CSVLine]([line] nvarchar(4000))

-- bulk insert into temp table
-- cannot use variable path with bulk insert
-- so we must run using dynamic sql
SET @Sql = 'BULK INSERT #CSVLine
FROM ''' + @InputFile + '''
WITH
(
FIRSTROW=' + CAST(@FirstLine as varchar) + ',
FIELDTERMINATOR = ''\n'',
ROWTERMINATOR = ''\n''
)'

-- run dynamic statement to populate temp table
EXEC(@sql)

-- get number of lines in table
SET @LINECOUNT = @@ROWCOUNT

-- add identity column to table so that we can loop through it
ALTER TABLE [#CSVLine] ADD [RowId] [int] IDENTITY(1,1) NOT NULL

IF @LINECOUNT > 0
BEGIN
    -- cycle through each line, cleaning each line
    SET @CURLINE = 1
    WHILE @CURLINE <= @LINECOUNT
    BEGIN
        -- get current line
        SELECT @line = line
          FROM #CSVLine
         WHERE [RowId] = @CURLINE

        -- Replace commas with our custom-made delimiter
        SET @Line = REPLACE(@Line, ',', @PH1)

        -- Find a quoted part of the line, which could legitimately contain commas.
        -- In that case we will need to identify the quoted section and swap commas back in for our custom placeholder.
        SET @starti = CHARINDEX(@PH1 + '"' ,@Line, 0)
        If CHARINDEX('"', @Line, 0) = 0 SET @starti = 0

        -- loop through quoted fields
        WHILE @starti > 0 
        BEGIN
            SET @FieldTerminatorFound = 0

            -- Find end quote token (originally  a ",)
            SET @endi = CHARINDEX('"' + @PH1, @Line, @starti)  -- sLine.IndexOf("""" & PH1, starti)

            IF @endi < 1
            BEGIN
                SET @FieldTerminatorFound = 1
                If @endi < 1 SET @endi = LEN(@Line) - 1
            END

            WHILE @FieldTerminatorFound = 0
            BEGIN
                -- Find any more quotes that are part of that sequence, if any
                SET @backChar = '"' -- thats one quote
                SET @quoteCount = 0

                WHILE @backChar = '"'
                BEGIN
                    SET @quoteCount = @quoteCount + 1
                    SET @backChar = SUBSTRING(@Line, @endi-@quoteCount, 1) -- sLine.Chars(endi - quoteCount)
                END

                IF (@quoteCount % 2) = 1
                BEGIN
                    -- odd number of quotes. real field terminator
                    SET @FieldTerminatorFound = 1
                END
                ELSE 
                BEGIN
                    -- keep looking
                    SET @endi = CHARINDEX('"' + @PH1, @Line, @endi + 1) -- sLine.IndexOf("""" & PH1, endi + 1)
                END

            END

            -- Grab the quoted field from the line, now that we have the start and ending indices
            SET @source = SUBSTRING(@Line, @starti + LEN(@PH1), @endi - @starti - LEN(@PH1) + 1) 
            -- sLine.Substring(starti + PH1.Length, endi - starti - PH1.Length + 1)

            -- And swap the commas back in
            SET @Line = REPLACE(@Line, @source, REPLACE(@source, @PH1, ','))
            --sLine.Replace(source, source.Replace(PH1, ","))

            -- Find the next quoted field
            -- If endi >= line.Length - 1 Then endi = line.Length 'During the swap, the length of line shrinks so an endi value at the end of the line will fail
            SET @starti = CHARINDEX(@PH1 + '"', @Line, @starti + LEN(@PH1))
            --sLine.IndexOf(PH1 & """", starti + PH1.Length)

        END

        -- get table based on current line
        IF OBJECT_ID('tempdb..#Line') IS NOT NULL
            DROP TABLE #Line

        -- converts a delimited list into a table
        SELECT *
        INTO #Line
        FROM dbo.iter_charlist_to_table(@Line,@PH1)

        -- get number of columns in line
        SET @COLCOUNT = @@ROWCOUNT

        -- dynamically create CSV temp table to hold CSV columns and lines
        -- only need to create once
        IF OBJECT_ID('tempdb..#CSV') IS NULL
        BEGIN
            -- create initial structure of CSV table
            CREATE TABLE [#CSV]([Col1] nvarchar(100))

            -- dynamically add a column for each column found in the first line
            SET @CURCOL = 1
            WHILE @CURCOL <= @COLCOUNT
            BEGIN
                -- first column already exists, don't need to add
                IF @CURCOL > 1 
                BEGIN
                    -- add field
                    SET @sql = 'ALTER TABLE [#CSV] ADD [Col' + Cast(@CURCOL as varchar) + '] nvarchar(100)'

                    --print @sql

                    -- this adds the fields to the temp table
                    EXEC(@sql)
                END

                -- go to next column
                SET @CURCOL = @CURCOL + 1
            END
        END

        -- build dynamic sql to insert current line into CSV table
        SET @sql = 'INSERT INTO [#CSV] VALUES('

        -- loop through line table, dynamically adding each column value
        SET @CURCOL = 1
        WHILE @CURCOL <= @COLCOUNT
        BEGIN
            -- get current column
            Select @ColVal = str 
              From #Line 
             Where listpos = @CURCOL

            IF LEN(@ColVal) > 0
            BEGIN
                -- remove quotes from beginning if exist
                IF LEFT(@ColVal,1) = '"'
                    SET @ColVal = RIGHT(@ColVal, LEN(@ColVal) - 1)

                -- remove quotes from end if exist
                IF RIGHT(@ColVal,1) = '"'
                    SET @ColVal = LEFT(@ColVal, LEN(@ColVal) - 1)
            END

            -- write column value
            -- make value sql safe by replacing single quotes with two single quotes
            -- also, replace two double quotes with a single double quote
            SET @sql = @sql + '''' + REPLACE(REPLACE(@ColVal, '''',''''''), '""', '"') + ''''

            -- add comma separater except for the last record
            IF @CURCOL <> @COLCOUNT
                SET @sql = @sql + ','

            -- go to next column
            SET @CURCOL = @CURCOL + 1
        END

        -- close sql statement
        SET @sql = @sql + ')'

        --print @sql

        -- run sql to add line to table
        EXEC(@sql)

        -- move to next line
        SET @CURLINE = @CURLINE + 1

    END

END

-- return CSV table
SELECT * FROM [#CSV]

END

GO

La procédure stockée utilise cette fonction d'aide qui analyse une chaîne de caractères dans un tableau (merci Erland Sommarskog !):

CREATE FUNCTION [dbo].[iter_charlist_to_table]
                (@list      ntext,
                 @delimiter nchar(1) = N',')
     RETURNS @tbl TABLE (listpos int IDENTITY(1, 1) NOT NULL,
                         str     varchar(4000),
                         nstr    nvarchar(2000)) AS

BEGIN
  DECLARE @pos      int,
          @textpos  int,
          @chunklen smallint,
          @tmpstr   nvarchar(4000),
          @leftover nvarchar(4000),
          @tmpval   nvarchar(4000)

  SET @textpos = 1
  SET @leftover = ''
  WHILE @textpos <= datalength(@list) / 2
  BEGIN
     SET @chunklen = 4000 - datalength(@leftover) / 2
     SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
     SET @textpos = @textpos + @chunklen

     SET @pos = charindex(@delimiter, @tmpstr)

     WHILE @pos > 0
     BEGIN
        SET @tmpval = ltrim(rtrim(left(@tmpstr, @pos - 1)))
        INSERT @tbl (str, nstr) VALUES(@tmpval, @tmpval)
        SET @tmpstr = substring(@tmpstr, @pos + 1, len(@tmpstr))
        SET @pos = charindex(@delimiter, @tmpstr)
     END

     SET @leftover = @tmpstr
  END

  INSERT @tbl(str, nstr) VALUES (ltrim(rtrim(@leftover)), ltrim(rtrim(@leftover)))

RETURN

END

Voici comment je l'appelle en T-SQL. Dans ce cas, j'insère les résultats dans une table temporaire, donc je crée d'abord la table temporaire :

    -- create temp table for file import
CREATE TABLE #temp
(
    CustomerCode nvarchar(100) NULL,
    Name nvarchar(100) NULL,
    [Address] nvarchar(100) NULL,
    City nvarchar(100) NULL,
    [State] nvarchar(100) NULL,
    Zip nvarchar(100) NULL,
    OrderNumber nvarchar(100) NULL,
    TimeWindow nvarchar(100) NULL,
    OrderType nvarchar(100) NULL,
    Duration nvarchar(100) NULL,
    [Weight] nvarchar(100) NULL,
    Volume nvarchar(100) NULL
)

-- convert the CSV file into a table
INSERT #temp
EXEC [dbo].[SSP_CSVToTable]
     @InputFile = @FileLocation
    ,@FirstLine = @FirstImportRow

Je n'ai pas beaucoup testé les performances, mais cela fonctionne bien pour ce dont j'ai besoin - importer des fichiers CSV de moins de 1000 lignes. Cependant, il pourrait s'étouffer sur des fichiers très volumineux.

J'espère que quelqu'un d'autre y trouvera également son compte.

A la vôtre !

0 votes

Murmy, c'est incroyablement génial !

5voto

VenerableAgents Points 274

J'ai également créé une fonction pour convertir un CSV en un format utilisable pour l'insertion en masse. J'ai utilisé le post répondu par Chris Clark comme point de départ pour créer la fonction C# suivante.

J'ai fini par utiliser une expression régulière pour trouver les champs. J'ai ensuite recréé le fichier ligne par ligne, en l'écrivant dans un nouveau fichier au fur et à mesure, évitant ainsi que le fichier entier ne soit chargé en mémoire.

private void CsvToOtherDelimiter(string CSVFile, System.Data.Linq.Mapping.MetaTable tbl)
{
    char PH1 = '|';
    StringBuilder ln;

    //Confirm file exists. Else, throw exception
    if (File.Exists(CSVFile))
    {
        using (TextReader tr = new StreamReader(CSVFile))
        {
            //Use a temp file to store our conversion
            using (TextWriter tw = new StreamWriter(CSVFile + ".tmp"))
            {
                string line = tr.ReadLine();
                //If we have already converted, no need to reconvert.
                //NOTE: We make the assumption here that the input header file 
                //      doesn't have a PH1 value unless it's already been converted.
                if (line.IndexOf(PH1) >= 0)
                {
                    tw.Close();
                    tr.Close();
                    File.Delete(CSVFile + ".tmp");
                    return;
                }
                //Loop through input file
                while (!string.IsNullOrEmpty(line))
                {
                    ln = new StringBuilder();

                    //1. Use Regex expression to find comma separated values 
                    //using quotes as optional text qualifiers 
                    //(what MS EXCEL does when you import a csv file)
                    //2. Remove text qualifier quotes from data
                    //3. Replace any values of PH1 found in column data 
                    //with an equivalent character
                    //Regex:  \A[^,]*(?=,)|(?:[^",]*"[^"]*"[^",]*)+|[^",]*"[^"]*\Z|(?<=,)[^,]*(?=,)|(?<=,)[^,]*\Z|\A[^,]*\Z
                    List<string> fieldList = Regex.Matches(line, @"\A[^,]*(?=,)|(?:[^"",]*""[^""]*""[^"",]*)+|[^"",]*""[^""]*\Z|(?<=,)[^,]*(?=,)|(?<=,)[^,]*\Z|\A[^,]*\Z")
                            .Cast<Match>()
                            .Select(m => RemoveCSVQuotes(m.Value).Replace(PH1, '¦'))
                            .ToList<string>();

                    //Add the list of fields to ln, separated by PH1
                    fieldList.ToList().ForEach(m => ln.Append(m + PH1));

                    //Write to file. Don't include trailing PH1 value.
                    tw.WriteLine(ln.ToString().Substring(0, ln.ToString().LastIndexOf(PH1)));

                    line = tr.ReadLine();
                }

                tw.Close();
            }
            tr.Close();

            //Optional:  replace input file with output file
            File.Delete(CSVFile);
            File.Move(CSVFile + ".tmp", CSVFile);
        }
    }
    else
    {
        throw new ArgumentException(string.Format("Source file {0} not found", CSVFile));
    }
}
//The output file no longer needs quotes as a text qualifier, so remove them
private string RemoveCSVQuotes(string value)
{
    //if is empty string, then remove double quotes
    if (value == @"""""") value = "";
    //remove any double quotes, then any quotes on ends
    value = value.Replace(@"""""", @"""");
    if (value.Length >= 2)
        if (value.Substring(0, 1) == @"""")
            value = value.Substring(1, value.Length - 2);
    return value;
}

1 votes

Fonctionne très bien mais remplace les fichiers et ne prend pas en compte les caractères accentués. Veillez donc à inclure l'encodage dans le lecteur de flux. A part cela. Merci!=]

2voto

Brett Points 46

Cela peut être plus compliqué ou plus compliqué que ce que vous êtes prêt à utiliser, mais ...

Si vous pouvez implémenter la logique d'analyse des lignes en champs en VB ou C#, vous pouvez le faire en utilisant une fonction CLR à valeur de table (TVF).

Un TVF CLR peut être un moyen performant de lire des données à partir d'une source externe lorsque vous souhaitez qu'un code C# ou VB sépare les données en colonnes et/ou ajuste les valeurs.

Vous devez être prêt à ajouter un assemblage CLR à votre base de données (et un assemblage qui autorise les opérations externes ou non sécurisées afin qu'il puisse ouvrir des fichiers). Cela peut être un peu compliqué ou compliqué, mais cela peut valoir la peine pour la flexibilité que vous obtenez.

J'avais quelques fichiers volumineux qui devaient être régulièrement chargés dans des tables aussi rapidement que possible, mais certaines traductions de code devaient être effectuées sur certaines colonnes et un traitement spécial était nécessaire pour charger des valeurs qui auraient autrement provoqué des erreurs de type de données avec une simple insertion en masse.

En résumé, un TVF CLR vous permet d'exécuter du code C# ou VB sur chaque ligne du fichier avec des performances similaires à celles d'une insertion en masse (même si vous devrez peut-être vous préoccuper de la journalisation). L'exemple de la documentation SQL Server vous permet de créer un TVF pour lire le journal des événements, que vous pouvez utiliser comme point de départ.

Notez que le code du TVF CLR ne peut accéder à la base de données qu'au cours d'une phase d'initialisation, avant le traitement de la première ligne (par exemple, il n'y a pas de recherche pour chaque ligne - il faut utiliser un TVF normal pour ce faire). D'après votre question, vous ne semblez pas en avoir besoin.

Notez également que les colonnes de sortie de chaque TVF CLR doivent être explicitement spécifiées. Vous ne pouvez donc pas en écrire un générique réutilisable pour chaque fichier csv différent que vous pourriez avoir.

Vous pourriez écrire un TVF CLR pour lire des lignes entières du fichier, renvoyant un ensemble de résultats à une colonne, puis utiliser des TVF normaux pour lire à partir de cet ensemble pour chaque type de fichier. Cela nécessite que le code d'analyse de chaque ligne soit écrit en T-SQL, mais évite d'avoir à écrire de nombreux TVF CLR.

0 votes

Brett, désolé - j'étais en vacances et je viens de voir cette réponse. CLR-TVF n'est pas quelque chose que je connais bien, mais je vais certainement m'y intéresser. Merci beaucoup pour cette suggestion vraiment intéressante !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X