web-dev-qa-db-fra.com

SQL Server Bulk insertion de fichier CSV avec des guillemets incohérents

Est-il possible d'insérer en bloc (SQL Server) un fichier CSV dans lequel les champs sont seulement entourés de guillemets? Plus précisément, les guillemets entourent uniquement les champs contenant un ",".

En d'autres termes, j'ai des données qui ressemblent à ceci (la première ligne contient des 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 en tient compte.

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 kludgy pour supprimer les guillemets, mais cela ne fonctionne pas non plus, car les champs contenant ',' sont divisés en plusieurs colonnes.

Malheureusement, je n'ai pas la capacité de manipuler le fichier CSV à l'avance.

Est-ce que c'est sans espoir?

Merci d'avance pour vos conseils.

En passant, j’ai vu cet article Importation groupée SQL à partir de csv , mais dans ce cas, CHAQUE champ était systématiquement placé entre guillemets. Ainsi, dans ce cas, il pourrait utiliser "," comme délimiteur, puis effacer les guillemets par la suite.

32
mattstuehler

Vous allez avoir besoin de prétraiter le fichier, point à la ligne.

Si vous avez vraiment besoin de faire cela, voici le code. J'ai écrit cela parce que je n'avais absolument pas le choix. C'est un code utilitaire et je n'en suis pas fier, mais ça marche. L'approche n'est pas de faire en sorte que SQL comprenne les champs cités, mais de manipuler le fichier pour utiliser un délimiteur totalement différent.

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

Cette fonction prend un fichier d'entrée et remplacera toutes les virgules de délimitation de champ (PAS les virgules dans les champs de texte cité, mais uniquement les champs de délimitation réels) par un nouveau délimiteur. Vous pouvez ensuite dire à SQL Server d’utiliser le nouveau délimiteur de champ au lieu d’une virgule. Dans la version de la fonction ici, l'espace réservé est <TMP> (je suis convaincu que cela n'apparaîtra pas dans le CSV d'origine - si tel est le cas, préparez-vous à des explosions).

Par conséquent, après avoir exécuté cette fonction, vous importez dans SQL en procédant comme suit:

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

Et sans plus tarder, la fonction terrible que je vous prie de m'excuser d'avance de vous avoir infligé (éditez - j'ai posté un programme de travail qui fait cela au lieu de simplement la 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
18
Chris Clark

Il n'est pas possible d'effectuer une insertion en bloc pour ce fichier à partir de MSDN:

Pour être utilisable en tant que fichier de données pour une importation en bloc, 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 à préparer le fichier à l'importation. Vous pouvez également demander aux utilisateurs de formater le fichier conformément à ces instructions ou d'utiliser un délimiteur autre qu'une virgule (par exemple |).

19
Macros

J'ai trouvé la réponse de Chris très utile, mais je voulais l'exécuter à partir de SQL Server avec T-SQL (et non avec CLR). J'ai donc converti son code en code T-SQL. Mais ensuite, je suis allé un peu plus loin en enrobant le tout dans une procédure stockée ayant les effets suivants:

  1. utiliser une insertion en bloc pour importer initialement le fichier CSV
  2. nettoyer les lignes en utilisant le code de Chris
  3. renvoyer 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'assistance qui analyse une chaîne dans une table (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 de T-SQL. Dans ce cas, j'insère les résultats dans une table temporaire, je crée donc 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 comportant moins de 1 000 lignes. Cependant, cela pourrait s’étouffer avec des fichiers très volumineux.

Espérons que quelqu'un d'autre le trouve également utile.

À votre santé!

8
murmy

J'ai également créé une fonction permettant de convertir un fichier CSV en un format utilisable pour l'insertion en bloc. J'ai utilisé la réponse de 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 de charger le fichier entier 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;
}
5
VenerableAgents

Le plus souvent, ce problème est dû au fait que les utilisateurs exportent un fichier Excel au format CSV.

Il y a deux façons de contourner ce problème:

  1. Exporter à partir d'Excel à l'aide d'une macro, selon la suggestion de Microsoft
  2. Ou le moyen vraiment facile:
    • Ouvrez le fichier CSV dans Excel. 
    • Enregistrer en tant que fichier Excel. (.xls ou .xlsx). 
    • Importez ce fichier dans SQL Server en tant que un fichier Excel .
    • Riez pour vous car vous n’aviez pas à coder les solutions ci-dessus .... muhahahaha 

Import as Excel file

Voici quelques SQL si vous voulez vraiment écrire un script (après avoir enregistré le fichier CSV au format Excel):

select * 
into SQLServerTable FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0', 
    'Excel 8.0;Database=D:\testing.xls;HDR=YES', 
    'SELECT * FROM [Sheet1$]')
3
Rebecca

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

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

Un TVF CLR peut être un moyen performant de lire des données depuis 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 permet des opérations externes ou non sécurisées pour pouvoir ouvrir des fichiers). Cela peut être un peu compliqué ou impliqué, mais cela peut valoir la peine pour la flexibilité que vous obtenez.

Certains fichiers volumineux devaient être chargés régulièrement dans les 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 un insert brut.

En bref, un fichier TVF CLR vous permet d’exécuter du code C # ou VB sur chaque ligne du fichier avec une insertion en bloc comme pour des performances (bien que vous deviez peut-être vous inquiéter de la journalisation). L'exemple de la documentation de SQL Server vous permet de créer un fichier TVF à lire dans le journal des événements que vous pouvez utiliser comme point de départ.

Notez que le code dans le fichier TVF CLR ne peut accéder à la base de données que dans une étape init avant le traitement de la première ligne (par exemple, pas de recherche pour chaque ligne - vous utilisez un fichier TVF normal au-dessus de cette opération). Vous ne semblez pas avoir besoin de cela en fonction de votre question.

Notez également que les colonnes de sortie de chaque fichier TVF CLR doivent être spécifiées explicitement. Par conséquent, vous ne pouvez pas en écrire un générique qui soit réutilisable pour chaque fichier csv différent.

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

2
Brett

Une autre méthode - en supposant que vous n’ayez pas de charge de champs ou que vous ne vous attendiez pas à ce qu’un devis apparaisse dans les données, vous utiliseriez la fonction REPLACE.

UPDATE dbo.tablename 
        SET dbo.tablename.target_field = REPLACE(t.importedValue, '"', '')
FROM #tempTable t
WHERE dbo.tablename.target_id = t.importedID;

Je l'ai utilisé. Je ne peux faire aucune réclamation concernant la performance. C'est juste un moyen rapide et sale de résoudre le problème.

2
twleblanc

Vous devriez pouvoir spécifier non seulement le séparateur de champs, qui devrait être [], mais également le qualificateur de texte, qui dans ce cas serait ["]. Utiliser [] pour entourer cela, afin d'éviter toute confusion avec".

1
Kibbee

J'ai trouvé peu de problèmes en ayant "," dans nos champs comme Mike, "456 2nd St, Apt 5".

La solution à ce problème est @ http://crazzycoding.blogspot.com/2010/11/import-csv-file-into-sql-server-using.html

Merci, - Ashish

1
ashish.chotalia

Un prétraitement est nécessaire.

La fonction PowerShell Import-CSV prend en charge ce type de fichier. Export-CSV encapsulera ensuite chaque valeur entre guillemets.

Un seul fichier:

Import-Csv import.csv | Export-Csv -NoTypeInformation export.csv

Pour fusionner de nombreux fichiers avec les chemins C:\year\input_date.csv:

$inputPath = 'C:\????\input_????????.csv'
$outputPath = 'C:\merged.csv'
Get-ChildItem $inputPath |
  Select -ExpandProperty FullName |
  Import-CSV |
  Export-CSV -NoTypeInformation -Path $outputPath

PowerShell peut généralement être exécuté avec SQL Server Agent à l'aide d'un compte proxy PowerShell.

Si les délimiteurs ne sont pas gérés correctement, spécifiez explicitement un autre délimiteur.

 Export-CSV -NoTypeInformation -Delimiter ';' -Path $outputPath
1
user10191093

Chris, .__ Merci pour tout ça !! Tu as sauvé mes biscuits !! Je ne pouvais pas croire que le chargeur en vrac ne gèrerait pas ce problème lorsque XL fait un travail aussi agréable .. ces types ne se voient-ils pas dans les couloirs ???. ce que j'ai piraté ensemble. C'est déprimant et sale mais ça fonctionne comme un champion! J'ai codé en dur le délimiteur et commenté l'en-tête car ils n'étaient pas nécessaires pour mon application.

J'aimerais pouvoir également y coller une bonne grosse bière. 

Geeze, je n'ai aucune idée pourquoi le module final et la classe publique sont en dehors du bloc de code ... srry!

    Module Module1

    Sub Main()

        Dim arrArgs() As String = Command.Split(",")
        Dim i As Integer
        Dim obj As New ReDelimIt()

        Console.Write(vbNewLine & vbNewLine)

        If arrArgs(0) <> Nothing Then
            For i = LBound(arrArgs) To UBound(arrArgs)
                Console.Write("Parameter " & i & " is " & arrArgs(i) & vbNewLine)
            Next


            obj.ProcessFile(arrArgs(0), arrArgs(1))

        Else
            Console.Write("Usage Test1 <inputfile>,<outputfile>")
        End If

        Console.Write(vbNewLine & vbNewLine)
    End Sub

 End Module

 Public Class ReDelimIt

    Public Function ProcessFile(ByVal InputFile As String, ByVal OutputFile As String) As Integer

        Dim ph1 As String = "|"

        Dim objReader As System.IO.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 System.IO.StreamReader(System.IO.File.OpenRead(InputFile), System.Text.Encoding.Default)
        Catch ex As Exception
            MsgBox(ex.Message)
        End Try

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

        'grab the first line
        Dim line = objReader.ReadLine()
        'and advance to the next line b/c the first line is column headings
        'Removed Check Headers can put in if needed.
        'If chkHeaders.Checked Then
        'line = objReader.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)

            While starti > -1 'loop through quoted fields

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

                'The end quote token could be a false positive because there could occur a ", sequence.
                'It would be double-quoted ("",) so check for that here
                Dim check1 = line.IndexOf("""""" & ph1, starti)

                'A """, sequence can occur if a quoted field ends in a quote.
                'In this case, the above check matches, but we actually SHOULD process this as an end quote token
                Dim check2 = line.IndexOf("""""""" & ph1, starti)

                'If we are in the check1 ("",) situation, keep searching for an end quote token
                'The +1 and +2 accounts for the extra length of the checked sequences
                While (endi = check1 + 1 AndAlso endi <> check2 + 2) 'loop through "false" tokens in the quoted fields
                    endi = line.IndexOf("""" & ph1, endi + 1)
                    check1 = line.IndexOf("""""" & ph1, check1 + 1)
                    check2 = line.IndexOf("""""""" & ph1, check2 + 1)
                End While

                'We have searched for an end token (",) but can't find one, so that means the line ends in a "
                If endi < 0 Then endi = line.Length - 1

                '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

            'Add our primary key to the line
            ' Removed for now
            'If chkAddKey.Checked Then
            'line = String.Concat(count.ToString, ph1, line)
            ' End If

            sb.AppendLine(line)

            line = objReader.ReadLine

        End While

        objReader.Close()

        SaveTextToFile(sb.ToString, OutputFile)

        Return count

    End Function

    Public Function SaveTextToFile(ByVal strData As String, ByVal FullPath As String) As Boolean
        Dim bAns As Boolean = False
        Dim objReader As System.IO.StreamWriter
        Try
            objReader = New System.IO.StreamWriter(FullPath, False, System.Text.Encoding.Default)
            objReader.Write(strData)
            objReader.Close()
            bAns = True
        Catch Ex As Exception
            Throw Ex
        End Try
        Return bAns
    End Function

End Class
1
Indigo42

Parler de la pratique ... Dans SQL Server 2017, vous pouvez fournir un "qualificateur de texte" composé de guillemets, sans "remplacer" votre délimiteur. J'insère en vrac plusieurs fichiers qui ressemblent à l'exemple de l'OP. Mes fichiers sont ".csv" et ils ont des qualificateurs de texte incohérents qui ne sont trouvés que lorsque la valeur contient une virgule. Je ne sais pas du tout quelle version de SQL Server cette fonctionnalité/fonctionnalité a commencé à fonctionner, mais je sais que cela fonctionne dans SQL Server 2017 Standard. Plutôt facile.

0
practicalSQL

Ce code fonctionne pour moi:

 public bool CSVFileRead(string fullPathWithFileName, string fileNameModified, string tableName)
    {
        SqlConnection con = new SqlConnection(ConfigurationSettings.AppSettings["dbConnectionString"]);
        string filepath = fullPathWithFileName;
        StreamReader sr = new StreamReader(filepath);
        string line = sr.ReadLine();
        string[] value = line.Split(',');
        DataTable dt = new DataTable();
        DataRow row;
        foreach (string dc in value)
        {
            dt.Columns.Add(new DataColumn(dc));
        }
        while (!sr.EndOfStream)
        {
            //string[] stud = sr.ReadLine().Split(',');
            //for (int i = 0; i < stud.Length; i++)
            //{
            //    stud[i] = stud[i].Replace("\"", "");
            //}
            //value = stud;
            value = sr.ReadLine().Split(',');
            if (value.Length == dt.Columns.Count)
            {
                row = dt.NewRow();
                row.ItemArray = value;
                dt.Rows.Add(row);
            }
        }
        SqlBulkCopy bc = new SqlBulkCopy(con.ConnectionString, SqlBulkCopyOptions.TableLock);
        bc.DestinationTableName = tableName;
        bc.BatchSize = dt.Rows.Count;
        con.Open();
        bc.WriteToServer(dt);
        bc.Close();
        con.Close();

        return true;
    }
0
Vishal sen

Je réunis ci-dessous pour résoudre mon cas. Je devais pré-traiter des fichiers très volumineux et résoudre les citations incohérentes. Il suffit de le coller dans une application C # vierge, de définir les consts selon vos besoins et de partir. Cela a fonctionné sur de très gros fichiers CSV de plus de 10 Go.

namespace CsvFixer
{
    using System.IO;
    using System.Text;

    public class Program
    {
        private const string delimiter = ",";
        private const string quote = "\"";
        private const string inputFile = "C:\\temp\\input.csv";
        private const string fixedFile = "C:\\temp\\fixed.csv";

        /// <summary>
        /// This application fixes inconsistently quoted csv (or delimited) files with support for very large file sizes.
        /// For example :  1223,5235234,8674,"Houston","London, UK",3425,Other text,stuff 
        /// Must become :  "1223","5235234","8674","Houston","London, UK","3425","Other text","stuff" 
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // Use streaming to allow for large files. 
            using (StreamWriter outfile = new StreamWriter(fixedFile))
            {
                using (FileStream fs = File.Open(inputFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                using (BufferedStream bs = new BufferedStream(fs))
                using (StreamReader sr = new StreamReader(bs))
                {
                    string currentLine;

                    // Read each input line in and write each fixed line out
                    while ((currentLine = sr.ReadLine()) != null)
                    {
                        outfile.WriteLine(FixLine(currentLine, delimiter, quote));
                    }
                }
            }
        }

        /// <summary>
        /// Fully quote a partially quoted line 
        /// </summary>
        /// <param name="line">The partially quoted line</param>
        /// <returns>The fully quoted line</returns>
        private static string FixLine(string line, string delimiter, string quote)
        {
            StringBuilder fixedLine = new StringBuilder();

            // Split all on the delimiter, acceptinmg that some quoted fields 
            // that contain the delimiter wwill be split in to many pieces.
            string[] fieldParts = line.Split(delimiter.ToCharArray());

            // Loop through the fields (or parts of fields)
            for (int i = 0; i < fieldParts.Length; i++)
            {
                string currentFieldPart = fieldParts[i];

                // If the current field part starts and ends with a quote it is a field, so write it to the result
                if (currentFieldPart.StartsWith(quote) && currentFieldPart.EndsWith(quote))
                {
                    fixedLine.Append(string.Format("{0}{1}", currentFieldPart, delimiter));
                }
                // else if it starts with a quote but doesnt end with one, it is part of a lionger field.
                else if (currentFieldPart.StartsWith(quote))
                {
                    // Add the start of the field
                    fixedLine.Append(string.Format("{0}{1}", currentFieldPart, delimiter));

                    // Append any additional field parts (we will only hit the end of the field when 
                    // the last field part finishes with a quote. 
                    while (!fieldParts[++i].EndsWith(quote))
                    {
                        fixedLine.Append(string.Format("{0}{1}", fieldParts[i], delimiter));
                    }

                    // Append the last field part - i.e. the part containing the closing quote
                    fixedLine.Append(string.Format("{0}{1}", fieldParts[i], delimiter));
                }
                else
                {
                    // The field has no quotes, add the feildpart with quote as bookmarks 
                    fixedLine.Append(string.Format("{0}{1}{0}{2}", quote, currentFieldPart, delimiter));
                }
            }

            // Return the fixed string 
            return fixedLine.ToString();
        }
    }
}
0
Murray Foxcroft