【问题标题】:freebcp: "Unicode data is odd byte size for column. Should be even byte size"freebcp:“Unicode 数据是列的奇数字节大小。应该是偶数字节大小”
【发布时间】:2016-12-23 08:51:18
【问题描述】:

这个文件可以正常工作(UTF-8):

$ cat ok.txt
291054  Ţawī Rifā

此文件导致错误 (UTF-8):

$ cat bad.txt
291054  Ţawī Rifā‘

这是消息:

$ freebcp 'DB.dbo.table' in bad.txt ... -c
Starting copy...
Msg 20050, Level 4
Attempt to convert data stopped by syntax error in source field

Msg 4895, Level 16, State 2
Server '...', Line 1
    Unicode data is odd byte size for column 2. Should be even byte size.
Msg 20018, Level 16
General SQL Server error: Check messages from the SQL Server

唯一不同的是最后一个字符,即unicode 2018(左单引号)

知道是什么导致了这个错误吗?

SQL Server 使用 UTF-16LE(尽管我相信 TDS 以 UCS-2LE 开头并切换)

有问题的列是nvarchar(200)

这是错误前发送的数据包:

packet.c:741:Sending packet
0000 07 01 00 56 00 00 01 00-81 02 00 00 00 00 00 08 |...V.... ........|
0010 00 38 09 67 00 65 00 6f-00 6e 00 61 00 6d 00 65 |.8.g.e.o .n.a.m.e|
0020 00 69 00 64 00 00 00 00-00 09 00 e7 90 01 09 04 |.i.d.... ...ç....|
0030 d0 00 34 04 6e 00 61 00-6d 00 65 00 d1 ee 70 04 |Ð.4.n.a. m.e.Ñîp.|
0040 00 13 00 62 01 61 00 77-00 2b 01 20 00 52 00 69 |...b.a.w .+. .R.i|
0050 00 66 00 01 01 18      -                        |.f....|

【问题讨论】:

    标签: sql-server unicode sql-server-2012 character-encoding freetds


    【解决方案1】:

    更新:这个问题显然已在 FreeTDS v1.00.16 中得到修复,于 2016 年 11 月 4 日发布。


    我可以使用 FreeTDS v1.00.15 重现您的问题。它绝对看起来像 freebcp 中的一个错误,当文本字段的最后一个字符具有 U+20xx 形式的 Unicode 代码点时,它会导致它失败。 (感谢@srutzky 纠正了我关于原因的结论。)正如您所指出的,这有效...

    291054  Ţawī Rifā
    

    ...这失败了...

    291054  Ţawī Rifā‘
    

    ...但我发现这也有效:

    291054  Ţawī Rifā‘x
    

    因此,一个丑陋的解决方法是针对您的输入文件运行一个脚本,该脚本会将一个低位非空格 Unicode 字符附加到每个文本字段(例如,x,即U+0078,如上一个上面的示例),使用freebcp 上传数据,然后对导入的行运行UPDATE 语句以去除多余的字符。

    就个人而言,我倾向于从 FreeTDS 切换到 Microsoft 的 SQL Server ODBC Driver for Linux,使用此处描述的说明安装时,它包括 bcpsqlcmd 实用程序:

    https://gallery.technet.microsoft.com/scriptcenter/SQLCMD-and-BCP-for-Ubuntu-c88a28cc

    我刚刚在 Xubuntu 16.04 下对其进行了测试,尽管我不得不稍微调整程序以使用 libssl.so.1.0.0 而不是 libssl.so.0.9.8libcrypto 也是如此),一旦我安装了 bcp Microsoft 的实用程序在 freebcp 失败的情况下成功。

    如果适用于 Linux 的 SQL Server ODBC 驱动程序无法在 Mac 上运行,那么另一种选择是使用适用于 SQL Server 的 Microsoft JDBC Driver 6.0 和一些 Java 代码,如下所示:

    connectionUrl = "jdbc:sqlserver://servername:49242"
            + ";databaseName=myDb"
            + ";integratedSecurity=false";
    String myUserid = "sa", myPassword = "whatever";
    
    String dataFileSpec = "C:/Users/Gord/Desktop/bad.txt";
    try (
            Connection conn = DriverManager.getConnection(connectionUrl, myUserid, myPassword);
            SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(dataFileSpec, "UTF-8", "\t", false);
            SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) {
        fileRecord.addColumnMetadata(1, "col1", java.sql.Types.NVARCHAR, 50, 0);
        fileRecord.addColumnMetadata(2, "col2", java.sql.Types.NVARCHAR, 50, 0);
        bulkCopy.setDestinationTableName("dbo.freebcptest");
        bulkCopy.writeToServer(fileRecord);
    } catch (Exception e) {
        e.printStackTrace(System.err);
    }
    

    【讨论】:

    • 切换到 Microsoft 的适用于 Linux 的 SQL Server ODBC 驱动程序一种客户端解决方法,以防答案不清楚。
    • 在 mac 上,我认为该驱动程序不适用于 mac/bsd
    • 我添加了一个 JDBC 选项,它应该可以在带有 Java 的 Mac 上工作。
    • 我想给你部分赏金,以感谢你的有益回应。知道该怎么做吗?谢谢
    • @NeilMcGuigan - 我不知道有什么方法可以做到这一点,但谢谢,我很感激这个想法。
    【解决方案2】:

    这个问题与 UTF-8 无关,因为传输数据包(问题底部)显示的数据是 UTF-16 Little Endian(正如 SQL Server 所期望的那样)。它是非常好的 UTF-16LE,除了缺少最后一个字节,就像错误消息所暗示的那样。

    问题很可能是 freetds 中的一个小错误,它错误地应用了旨在从可变长度字符串字段中去除尾随空格的逻辑。你说没有尾随空格?好吧,如果它没有被切断,那么它会更清晰一些(但是,如果它没有被切断,就不会出现这个错误)。那么,让我们看看是什么数据包,看看我们是否可以重构它。

    数据中的错误可能被忽略了,因为数据包包含偶数个字节。但并非所有字段都是双字节的,因此它不需要 是偶数。如果我们知道什么是好的数据(在错误之前),那么我们就可以在数据中找到一个起点并继续前进。最好从 Ţ 开始,因为它有望高于 255 / FF 值,因此占用 2 个字节。下面的任何内容都会有一个00,并且许多角色的两边都有。虽然我们应该能够假设 Little Endian 编码,但最好确定一下。为此,我们需要至少一个字符具有两个非00 字节和不同的字节(其中一个字符是01 用于两个字节,这无助于确定顺序)。此字符串字段的第一个字符 Ţ 确认了这一点,因为它是代码点 0162,但在数据包中显示为 62 01

    以下是字符(与数据包的顺序相同)、它们的 UTF-16 LE 值以及指向其完整详细信息的链接。 62 01 的第一个字符的字节序列为我们提供了起点,因此我们可以忽略 0040 行的初始 00 13 00(为了便于阅读,它们已在下面的副本中删除)。请注意,右边显示的“翻译”不解释Unicode,所以62 01的2字节序列本身显示为62(即小写拉丁文“b”)和01本身(即不可打印字符;显示为“.”)。

    0040 xx xx xx 62 01 61 00 77-00 2b 01 20 00 52 00 69 |...b.a.w .+. .R.i|  
    0050 00 66 00 01 01 18 ??   -                        |.f....|
    

    如您所见,最后一个字符实际上是 18 20(即,由于 Little Endian 编码,一个字节交换的 20 18),而不是从末尾开始读取数据包时可能出现的 01 18。不知何故,最后一个字节 - 十六进制 20 - 丢失了,因此出现了 Unicode data is odd byte size 错误。

    现在,20 本身或后跟00 是一个空格。这可以解释为什么@GordThompson 能够通过在末尾添加一个附加字符来使其工作(最终字符不再可修剪)。这可以通过以 U+20xx 代码点的另一个字符结尾来进一步证明。例如,如果我对此是正确的,那么以 结尾 -- Fraction Slash U+2044 -- 会出现同样的错误,而以 -- Turned Sans-Serif Capital Y U+2144 结尾 -- 即使是之前的 它应该可以正常工作(@GordThompson 很好地证明了以 结尾确实有效,并且以 结尾导致了同样的错误)。

    如果输入文件以null(即00)终止,那么它可能只是20 00 结束序列执行此操作,在这种情况下以换行符结尾可能会修复它。这也可以通过测试包含两行的文件来证明:第 1 行是 bad.txt 中的现有行,第 2 行是应该工作的行。例如:

    291054  Ţawī Rifā‘
    999999  test row, yo!
    

    如果上面直接显示的两行文件有效,则证明它是 U+20xx 代码点的组合代码点是最后一个字符(传输多于文件)暴露了错误。但是,如果这个两行文件也出现错误,那么它证明将 U+20xx 代码点作为字符串字段的最后一个字符是问题(并且假设即使发生此错误也是合理的字符串字段不是该行的最后一个字段,因为在这种情况下已经排除了传输的空终止符)。

    这似乎是 freetds / freebcp 的错误,或者可能有一个配置选项不让它尝试修剪尾随空格,或者可能有一种方法让它将此字段视为 NCHAR 而不是NVARCHAR.

    更新

    @GordThompson 和 OP (@NeilMcGuigan) 都已测试并确认无论字符串字段在文件中的哪个位置都存在此问题:在行的中间、行的末尾、最后一行,而不是在最后一行。因此,这是一个普遍的问题。

    实际上,我找到了源代码,并且由于没有考虑多字节字符集,因此发生问题是有道理的。我将在 GitHub 存储库上提交一个问题。 rtrim 函数的来源在这里:

    https://github.com/FreeTDS/freetds/blob/master/src/dblib/bcp.c#L2267


    关于此声明:

    SQL Server 使用 UTF-16LE(尽管我相信 TDS 以 UCS-2LE 开头并切换)

    从编码的角度来看,UCS-2 和 UTF-16 之间确实没有区别。字节序列是相同的。唯一的区别在于代理对的解释(即高于 U+FFFF / 65535 的代码点)。 UCS-2 保留了用于构建代理对的代码点,但当时没有任何代理对的实现。 UTF-16 只是添加了代理对的实现以创建补充字符。因此,SQL Server 可以毫无问题地存储和检索 UTF-16 LE 数据。唯一的问题是内置函数不知道如何解释代理对,除非排序规则以_SC 结尾(对于S补充C字符),并且这些排序规则是在 SQL Server 2012 中引入的。

    【讨论】:

    • 我没有考虑到缺少的字节可能具有特殊意义 20 - 好点!
    • 已确认。当最后一个字符是 而不是 时,freebcp 没有错误。
    • @GordThompson 太棒了!感谢您测试:)。你有没有偶然测试以分数斜线结尾————来确认它也会导致错误?
    • 是的, 作为字符串的最后一个字符也会导致错误。你似乎成功了!
    • 添加回车也可以(对于 bad.txt 而不是 ok.t​​xt)
    【解决方案3】:

    这可能是源文件的编码问题。

    当您使用非标准字符时,源文件本身可能应该是 unicode。其他编码使用不同的字节数(一个最多三个)来编码一个字符。例如。你的 Unicode 2018 在 UTF-8 中是 0xE2 0x80 0x98

    你的数据包以.R.i.f....| 结尾,而你的ā‘。并且错误显示Server '...', Line 1

    尝试找出源文件的编码(也请查看big and little endian)并尝试将文件转换为确定的 unicode 格式。

    【讨论】:

    • 谢谢Shnugo。在我的问题中添加了更多信息。来源是 UTF-8。 MS SQL Server 是 UCS2LE/UTF16LE 。 Freetds 应该在内部使用 iconv 为您转换...
    • @NeilMcGuigan 我不知道你的工具......也许其他人正在帮忙。我会尝试将源文件转换为另一种格式,只是为了找出原因。祝你好运!
    • @NeilMcGuigan,你没有回到这个,所以这可能已经解决了......如果没有:你可能想阅读这个:stackoverflow.com/q/36402353/5089204许多可以阅读和解码 普通 字符串可以正确解决需要3 个字节 进行UTF-8 编码的字符。
    • 00 52not utf8; R 可能是 UCS2 (Unicode)。但是,00 01 01 18 不同意ā‘。在 UCS2 和 utf8 之间进行转换是可能的,甚至有些简单,但不可能将它们混合在同一个字符串中。
    【解决方案4】:

    这可能会解决它:

    inf 你的 /etc/freetds/freetds.conf

    添加:

    client charset = UTF-8
    

    还发现this 关于标志使用utf-16

    use utf-16 而不是在数据库范围内使用 UCS-2 字符编码使用 UTF-16。较新的 Windows 版本使用此 编码而不是 UCS-2。这可能会导致一些问题,如果客户 假设一个字符总是 2 个字节。

    【讨论】:

    • 您能否重现该问题?
    • @NeilMcGuigan 不只是猜测。要我删除这篇文章吗?
    猜你喜欢
    • 2018-07-23
    • 2011-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-10
    • 1970-01-01
    相关资源
    最近更新 更多