web-dev-qa-db-fra.com

SQL Server Bulk Insert interprète correctement certains caractères Unicode mais pas les autres?

Pour une raison quelconque, l'insertion en bloc de MS SQL Server 2016 interprète/traduit mal les caractères Unicode:

  • C9 (É) en 2B (+)
  • A1 (¡) dans ED (í)
  • A0 () dans E1 (á)
  • AE (®) dans AB (")
  • CB (Ë) en 2D (-)
  • D1 (Ñ) en 2D (-)
  • 92 (’) en C6 (Æ)
  • 96 (-) dans FB (û)

c'est-à-dire que Notepad ++ et xxd montrent que le fichier plat a 0xC9, mais après l'insertion en vrac, le tableau affiche "+" et cast comme varbinary dans SQL Server l'affiche comme 0x2B. La sauvegarde a également 0xC9.

J'insère en bloc 25 fichiers plats dans MS SQL Server 2016. Ce sont des données de 15 Go et j'utilise un délimiteur de champ pipe () et CRLF délimiteur de ligne.

J'insère en vrac dans la structure tronquée d'une sauvegarde qui m'est fournie. Et quand je compare à la sauvegarde, il y a des différences. REMARQUE: je dois attendre 25 heures pour la sauvegarde à partir de la source de données, mais je peux obtenir les fichiers plats en 15 minutes.

Certaines différences sont acceptables (rechercher et remplacer j'applique aux fichiers plats), mais beaucoup sont dues à une mauvaise interprétation des caractères Unicode.

La structure d'un exemple de table est la suivante:

CREATE TABLE [dbo].[obfuscated_name](
    [ob_1] [int] NOT NULL,
    [ob_2] [int] NOT NULL,
    [ob_3] [int] NOT NULL,
    [ob_4] [nvarchar](300) NULL
) ON [PRIMARY]

Le classement de la base de données est par défaut: SQL_Latin1_General_CP1_CI_AS. Aucune colonne n'a un classement différent. Ce classement doit utiliser la page de codes 1252, qui doit interpréter correctement les caractères avec lesquels j'ai des problèmes.

Mon processus s'exécute par rapport à des données de production en constante évolution, je crains donc que d'autres modifications n'apparaissent, et j'aimerais connaître la source du problème plutôt que d'essayer d'isoler le problème et de mettre à jour manuellement les interprétations erronées.

5
Dave Goldsmith

Ce n'est pas un bogue dans SQL Server (ou même dans Windows), ni une situation qui nécessite l'étape supplémentaire de conversion du fichier en un autre encodage (c'est-à-dire en "Unicode", qui dans le monde Windows signifie "UTF-16 Little Endian "). Ce n'est qu'une simple mauvaise communication.

La source de la panne de communication (c'est toujours la même chose, non ;-) n'est tout simplement pas d'accord sur la nature des données source. Lorsque vous déplacez des données de caractères d'un endroit à un autre, il est important de spécifier l'encodage des deux côtés. Oui le SQL_Latin1_General_CP1_* Les classements utilisent la page de codes 1252. Cependant, si vous ne le dites pas BULK INSERT ou BCP.exe quelle est la page de codes du fichier source, alors ils supposeront que la page de codes est la valeur par défaut du système.

La documentation de BULK INSERT indique même (pour le CODEPAGE = argument):

'OEM' (par défaut) = Colonnes de char , varchar , ou le type de données texte est converti de la page de codes OEM du système vers la page de codes SQL Server.

La documentation pour BCP.exe indique (pour le -C commutateur):

OEM = page de codes par défaut utilisée par le client. Il s'agit de la page de codes par défaut utilisée si -C n'est pas spécifié.

Et la page de codes par défaut pour Windows est (au moins pour les systèmes en anglais américain): 437. Vous pouvez le voir si vous exécutez ce qui suit dans une invite de commande:

C:\> CHCP

Il renverra:

Active code page: 437

Mais votre fichier source n'a pas été codé à l'aide de la page de codes 437. Il a été codé à l'aide de la page de codes 1252.

Voici donc ce qui se passe:

  1. Les octets sont des octets. Et les octets représentant des données de caractères ne peuvent être interprétés que via un encodage. Tout ce qui lit un fichier ne lit pas les caractères du fichier, il lit les octets du fichier et affiche les caractères en fonction du codage spécifié.
  2. BULK INSERT/BCP lit en octet 0xC9 . 0xC9 s'affiche comme É lors de l'utilisation de la page de codes 1252.
  3. BULK INSERT/BCP ne reçoit pas de page de code source, il vérifie donc la page de code actuelle pour le processus et est informé: 437 .
  4. BULK INSERT/BCP a maintenant un octet de 0xC9 pour la page de codes 437 (qui s'affiche comme , mais BULK INSERT/BCP ne l'affiche pas, vous ne le verrez donc pas)
  5. BULK INSERT/BCP insère ces données dans une colonne à l'aide d'un classement qui spécifie la page de codes 1252 .
  6. SQL Server voit que les données entrantes utilisent une page de codes différente de celle utilisée par la destination et doit donc convertir les données entrantes de sorte que les caractères semblent être les mêmes (autant que possible), même si les valeurs sous-jacentes changent.
  7. Le mappage de la page de codes 437 à la page de codes 1252 indique que l'octet 0xC9 est mappé sur l'octet 0x2B . De même, octet 0xAE (qui est ® sur la page de codes 1252) sur la page de codes 437 (qui s'affiche comme «) correspond à l'octet 0xAB sur la page de code 1252 (car il s'affiche également sous la forme «).

L'exemple suivant montre cette conversion pour tous les caractères mentionnés dans la question:

DECLARE @CodePageConversion TABLE
(
   [ActualSource_CP1252] AS CONVERT(VARCHAR(10), CONVERT(BINARY(1),
                    [PerceivedSource_CP437])) COLLATE SQL_Latin1_General_CP1_CI_AS,

   [PerceivedSource_CP437] VARCHAR(10) COLLATE SQL_Latin1_General_CP437_CI_AS,

   [Source_Value] AS (CONVERT(BINARY(1), [PerceivedSource_CP437])),

   [Destination_CP1252] AS (CONVERT(VARCHAR(10), [PerceivedSource_CP437]
                  COLLATE SQL_Latin1_General_CP1_CI_AS)),

   [CP1252_Value] AS (CONVERT(BINARY(1), CONVERT(VARCHAR(10),
                  [PerceivedSource_CP437] COLLATE SQL_Latin1_General_CP1_CI_AS)))
);

INSERT INTO @CodePageConversion
VALUES      (0xC9), (0xA1), (0xA0), (0xAE), (0xCB), (0xD1), (0x92), (0x96);

SELECT * FROM @CodePageConversion;

qui renvoie:

ActualSource_CP1252  PerceivedSource_CP437  Source_Value  Destination_CP1252  CP1252_Value
É                    ╔                      0xC9          +                   0x2B
¡                    í                      0xA1          í                   0xED
                     á                      0xA0          á                   0xE1
®                    «                      0xAE          «                   0xAB
Ë                    ╦                      0xCB          -                   0x2D
Ñ                    ╤                      0xD1          -                   0x2D
’                    Æ                      0x92          Æ                   0xC6
–                    û                      0x96          û                   0xFB

Les caractères pour 0xC9, 0XCB et 0xD1 n'existent pas dans la page de code 1252, donc les mappages "les mieux adaptés" sont utilisés, c'est pourquoi vous vous retrouvez avec le + et - caractères après la conversion.

De plus, même si la colonne de destination utilise NVARCHAR, tous ces mappages sont identiques, vous verrez donc exactement le même comportement.

Donc, vos choix sont:

  1. Si vous utilisez le T-SQL BULK INSERT commande, spécifiez le WITH CODEPAGE = option avec l'une des valeurs suivantes:

    1. 'ACP' (c'est la même chose que '1252')
    2. 'RAW' (ceci utilise la page de codes du classement de la colonne si vous l'insérez dans VARCHAR, ou est identique à 'OEM'/Page de code 437 lors de l'insertion dans NVARCHAR)
    3. '1252' (c'est la même chose que 'ACP')
  2. Ou, si vous utilisez BCP.exe , indiquez que le fichier entrant utilise la page de codes 1252 via le -C commutateur de ligne de commande avec l'une des valeurs suivantes (voir les remarques dans l'option # 1):

    1. ACP
    2. RAW
    3. 1252

Veuillez noter que:

  1. J'ai testé avec BULK INSERT, en insérant dans une colonne VARCHAR, en utilisant le jeu de caractères noté dans la question, et le ACP (que je crois pour [~ # ~] une [~ # ~] NSI [~ # ~] c [~ # ~] ode [~ # ~] p [~ # ~] âge), RAW et 1252 les valeurs ont toutes produit les bons résultats.
  2. Ne spécifiant pas WITH CODEPAGE = a produit les mêmes résultats que ceux rapportés par l'O.P. dans la question. Cela revenait à spécifier WITH CODEPAGE = 'OEM'.
  3. Lors de l'insertion dans une colonne NVARCHAR, ACP et 1252 a fonctionné comme souhaité, mais RAW a produit le même résultat que OEM (c'est-à-dire en utilisant la page de codes 437 au lieu de la page de codes 1252 qui est spécifiée par le classement de la colonne).
  4. J'ai testé en utilisant BCP.exe , et sans spécifier le -C switch did not utilise la page de codes du processus. Cela signifie que l'utilisation de [~ # ~] chcp [~ # ~] pour modifier la commande La page de codes de l'invite n'a eu aucun effet: la page de codes 437 était toujours utilisée comme la page de code source.

P.S. Étant donné que les données ici sont toutes codées sur 8 bits, il n'y a pas de "caractères Unicode" car aucun Unicode n'est utilisé.

7
Solomon Rutzky

Pour résoudre ce problème, j'ai dû utiliser:

BULK INSERT table FROM '\\path'
WITH (ERRORFILE = '\\error_path', FIELDTERMINATOR = 'term', DATAFILETYPE = 'widechar')

Le point clé étant DATAFILETYPE = 'widechar'

Et j'ai dû enregistrer le fichier plat en tant que type unicode avec MS Notepad.

Si vous rencontrez le même problème et avez besoin d'une meilleure solution pour convertir des fichiers plats en unicode, recherchez: https://stackoverflow.com/questions/623330/how-to-convert-txt-file-into-unicode

0
Dave Goldsmith