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.
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
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:
( 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 |).
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:
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é!
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;
}
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:
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$]')
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.
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.
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".
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
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
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
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.
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;
}
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();
}
}
}