11

我有一个在两个应用程序之间共享的 sql-server 2010 数据库。我们可以控制一个应用程序,而另一个应用程序是一个第三方应用程序,它首先创建了数据库。我们的应用程序是建立在第三方网络邮件应用程序之上的 CRM。

该数据库包含 varchar 列并且是 latin-1 编码的。第三方应用程序是用 php 编写的,并不关心数据的正确编码,因此它将 utf-8 编码字节填充到 varchar 列中,在那里它们被解释为 latin-1 并且看起来像垃圾。

我们的 CRM 应用程序是用 .Net 编写的,它会自动检测数据库排序规则与内存中字符串的编码不同,因此当 .Net 写入数据库时​​,它会转换字节以匹配数据库编码。

所以......从我们的应用程序写入数据库的数据在数据库中看起来是正确的,但来自第三方应用程序的数据却不是。

当我们的应用程序写入 FirstName = Céline 时,它​​作为 Céline 存储在数据库中

当网络邮件应用程序写入 FirstName = Céline 时,它​​作为 Céline 存储在数据库中

我们的 CRM 应用程序需要显示在任一系统中创建的联系人。因此,我正在编写一个 EncodingSniffer 类,该类查找指示其编码不佳的字符串的标记字符并将它们转换。

目前我有:

私有静态字符串 [] _flaggedChars = 新字符串 [] {
            “©”
        };

这非常适合将 Céline 显示为 Céline,但我需要添加到列表中。

有谁知道一种资源来获取 utf-8 特殊字符可以被解释为 iso-8859-1 的所有可能方式?

谢谢

澄清: 因为我在.Net 工作。当从数据库加载到内存中时,该字符串将转换为 Unicode UTF-16。因此,无论它是否在数据库中正确编码。它现在表示为 UTF16 字节。我需要能够分析这些 UTF-16 字节,并确定它们是否由于 utf-8 字节被填充到 iso-8859-1 数据库中而搞砸了.... 像泥巴一样清楚吗?

这是我到目前为止所拥有的。它已经清除了大多数错误编码字符的显示,但我仍然遇到了 É 的问题,例如:Éric 通过 webmail 存储在数据库中作为 Éric,但在检测到错误编码并将其改回后,它显示为 ?? ric 查看拥有 2500 个联系人的用户,其中数百个有编码问题,É 是唯一不能正确显示的东西......

public static Regex CreateRegex()
    {
        string specials = "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö";

        List<string> flags = new List<string>();
        foreach (char c in specials)
        {
            string interpretedAsLatin1 = Encoding.GetEncoding("iso-8859-1").GetString(Encoding.UTF8.GetBytes(c.ToString())).Trim();//take the specials, treat them as utf-8, interpret them as latin-1
            if (interpretedAsLatin1.Length > 0)//utf-8 chars made up of 2 bytes, interpreted as two single byte latin-1 chars.
                flags.Add(interpretedAsLatin1);
        }

        string regex = string.Empty;
        foreach (string s in flags)
        {
            if (regex.Length > 0)
                regex += '|';
            regex += s;
        }
        return new Regex("(" + regex + ")");
    }

    public static string CheckUTF(string data)
    {
        Match match = CreateRegex().Match(data);
        if (match.Success)
            return Encoding.UTF8.GetString(Encoding.GetEncoding("iso-8859-1").GetBytes(data));//from iso-8859-1 (latin-1) to utf-8
        else
            return data;
    }

所以:É 被转换为 195'Ã',8240'‰'

4

2 回答 2

1

您可能应该尝试将字节字符串解码为 UTF-8,如果出现错误,请假设它是 ISO-8859-1。

编码为 ISO-8859-1 的文本很少“碰巧”也是有效的 UTF-8 ...所有,当然。所以这个方法是相当稳健的。

忽略实际语言中哪些字符比其他字符更频繁地出现,这是一个天真的分析,假设每个字符以相同的频率出现。让我们尝试找出有效的 ISO-8859-1 被误认为 UTF-8 导致 mojibake 的频率。我还假设不会出现 C1 控制字符(U+0080 到 U+009F)。

对于字节字符串中的任何给定字节。如果字节接近字符串的末尾,那么您更有可能检测到格式错误的 UTF-8,因为已知某些字节序列的长度不足以成为有效的 UTF-8。但假设字节不在字符串末尾附近:

  • p(字节解码为 ASCII)= 0.57。这没有提供有关字符串是 ASCII、ISO-8859-1 还是 UTF-8 的信息。
  • 如果这个字节是 0x80 到 0xc1 或 0xf8 到 0xff,它不能是 UTF-8,所以你会检测到。p=0.33
  • 如果第一个字节是 0xc2 到 0xdf (p=0.11),那么它可能是有效的 UTF-8,但前提是它后面跟着一个值在 0x80 和 0xbf 之间的字节。下一个字节不在该范围内的概率是 192/224 = 0.86。所以 UTF-8 在这里失败的概率是 0.09
  • 如果第一个字节是 0xe0 到 0xef,那么它可能是有效的 UTF-8,但前提是它后面跟着 2 个连续字节。因此,您将检测到错误 UTF-8 的概率为 (16/224)*(1-(0.14*0.14)) = 0.07
  • 对于 0xf0 到 0xf7 类似,概率为 (8/224)*(1-(0.14*0.14*0.14)) = 0.04。

在长字符串中的每个字节处,检测到错误 UTF-8 的概率为 0.33+0.09+0.07+0.04 = 0.53。

因此对于长字符串,ISO-8859-1 静默通过 UTF-8 解码器的概率非常小:每增加一个字符,它大约减半!

这种分析当然假设随机 ISO-8859-1 字符。实际上,错误检测率不会那么好(主要是因为现实世界文本中的大多数字节实际上都是 ASCII),但它仍然会非常好。

于 2012-05-07T16:28:20.900 回答
0

感谢@Michael 完成了超过 99% 的工作!

这是 Michael 脚本的 PowerShell 版本,适用于任何人。这也是@Qubei 建议的Windows-1252代码页/编码来解决É问题;虽然允许您修改这些编码,以防您的数据通过不同的编码组合损坏。

#based on c# in question: https://stackoverflow.com/questions/10484833/detecting-bad-utf-8-encoding-list-of-bad-characters-to-sniff
function Convert-CorruptCodePageString {
    [CmdletBinding(DefaultParameterSetName = 'ByInputText')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByInputText')]
        [string]$InputText
        ,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByInputObject')]
        [PSObject]$InputObject
        ,
        [Parameter(Mandatory = $true, ParameterSetName = 'ByInputObject')]
        [string]$Property
        ,
        [Parameter()]
        [System.Text.Encoding]$SourceEncoding = [System.Text.Encoding]::GetEncoding('Windows-1252')
        ,
        [Parameter()]
        [System.Text.Encoding]$DestinationEncoding = [system.Text.Encoding]::UTF8
        ,
        [Parameter()]
        [string]$DodgyChars = 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö'
    )
    begin {
        [string]$InvalidCharRegex = ($DodgyChars.ToCharArray() | %{
            [byte[]]$dodgyCharBytes = $DestinationEncoding.GetBytes($_.ToString())
            $SourceEncoding.GetString($dodgyCharBytes,0,$dodgyCharBytes.Length).Trim()
        })  -join '|'   
    }
    process {
        if ($PSCmdlet.ParameterSetName -eq 'ByInputText') {
            $InputObject = $null
        } else {
            $InputText = $InputObject."$Property"
        }
        [bool]$IsLikelyCorrupted = $InputText -match $InvalidCharRegex
        if ($IsLikelyCorrupted) { #only bother to decrupt if we think it's corrupted
            [byte[]]$bytes = $SourceEncoding.GetBytes($InputText)
            [string]$outputText = $DestinationEncoding.GetString($bytes,0,$bytes.Length)
        } else {
            [string]$outputText = $InputText
        }
        [pscustomobject]@{
            InputString = $InputText
            OutputString = $outputText
            InputObject = $InputObject
            IsLikelyCorrupted = $IsLikelyCorrupted
        }        
    }
}

演示

#demo of using a simple string without the function (may cause corruption since this doesn't check if the characters being replaced are those likely to have been corrupted / thus is more likely to cause corruption in many strings).
$x = 'Strømmen'
$bytes = [System.Text.Encoding]::GetEncoding('Windows-1252').GetBytes($x)
[system.Text.Encoding]::UTF8.GetString($bytes,0,$bytes.Length)

#demo using the function
$x | Convert-CorruptCodePageString

#demo of checking all records in a table for an issue / reporting those with issues
#amend SQL Query, MyDatabaseInstance, and MyDatabaseCatlogue to point to your DB / query the relevant table
Invoke-SQLQuery -Query 'Select [Description], [RecId] from [DimensionFinancialTag] where [Description] is not null and [Description] > ''''' -DbInstance $MyDatabaseInstance -DbCatalog $MyDatabaseCatalog |
    Convert-CorruptCodePageString -Property 'Description' | 
    ?{$_.IsLikelyCorrupted} | 
    ft @{N='RecordId';E={$_.InputObject.RecId}}, InputString, OutputString 

我的演示中使用的附加功能

我不是Invoke-SqlCmdcmdlet 的粉丝,所以我自己动手。

function Invoke-SQLQuery {
    [CmdletBinding(DefaultParameterSetName = 'ByQuery')]
    param (
        [Parameter(Mandatory = $true)]
        [string]$DbInstance
        ,
        [Parameter(Mandatory = $true)]
        [string]$DbCatalog
        ,
        [Parameter(Mandatory = $true, ParameterSetName = 'ByQuery')]
        [string]$Query
        ,
        [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')]
        [string]$Path
        ,
        [Parameter(Mandatory = $false)]
        [hashtable]$Params = @{}
        ,
        [Parameter(Mandatory = $false)]
        [int]$CommandTimeoutSeconds = 30 #this is the SQL default
        ,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential=[System.Management.Automation.PSCredential]::Empty 
    )
    begin {
        write-verbose "Call to 'Execute-SQLQuery'"
        $connectionString = ("Server={0};Database={1}" -f $DbInstance,$DbCatalog)
        if ($Credential -eq [System.Management.Automation.PSCredential]::Empty) {
            $connectionString = ("{0};Integrated Security=True" -f $connectionString)
        } else {
            $connectionString = ("{0};User Id={1};Password={2}" -f $connectionString, $Credential.UserName, $Credential.GetNetworkCredential().Password)    
            $PSCmdlet.Name    
        }
        $connection = New-Object System.Data.SqlClient.SqlConnection
        $connection.ConnectionString = $connectionString
        $connection.Open()    
    }
    process {
        #create the command & assign the connection
        $cmd = new-object -TypeName 'System.Data.SqlClient.SqlCommand'
        $cmd.Connection = $connection

        #load in our query
        switch ($PSCmdlet.ParameterSetName) {
            'ByQuery' {$cmd.CommandText = $Query; break;}
            'ByPath' {$cmd.CommandText = Get-Content -Path $Path -Raw; break;}
            default {throw "ParameterSet $($PSCmdlet.ParameterSetName) not recognised by Invoke-SQLQuery"}
        }
        #assign parameters as required 
        #NB: these don't need declare statements in our query; so a query of 'select @demo myDemo' would be sufficient for us to pass in a parameter with name @demo and have it used
        #we can also pass in parameters that don't exist; they're simply ignored (sometimes useful if writing generic code that has optional params)
        $Params.Keys | %{$cmd.Parameters.AddWithValue("@$_", $Params[$_]) | out-null}

        $reader = $cmd.ExecuteReader()
        while (-not ($reader.IsClosed)) {
            $table = new-object 'System.Data.DataTable'
            $table.Load($reader)
            write-verbose "TableName: $($table.TableName)" #NB: table names aren't always available
            $table | Select-Object -ExcludeProperty RowError, RowState, Table, ItemArray, HasErrors
        }

    }
    end {
        $connection.Close()
    }
}
于 2017-08-09T16:47:53.467 回答