2012-05-07 11 views
9

Ho un database sql-server 2010 condiviso tra due applicazioni. Un'applicazione su cui abbiamo il controllo e l'altra applicazione è un'app di terze parti che ha creato il database in primo luogo. La nostra app è un CRM costruito sopra l'app webmail di terze parti.Rilevamento della codifica UTF-8 errata: elenco di caratteri errati da annusare?

Il database contiene colonne varchar ed è codificato in latin-1. L'app di terze parti è scritta in php e non si preoccupa di codificare correttamente i dati, quindi inserisce byte codificati utf-8 nelle colonne varchar, dove vengono interpretati come latin-1 e sembrano immondizia.

La nostra app CRM è scritta in .Net, che rileva automaticamente che la raccolta del database è diversa dalla codifica della stringa in memoria, quindi quando .Net scrive su un database, converte i byte in modo che corrispondano alla codifica del database.

Quindi ... i dati scritti nel db dalla nostra app sembrano corretti nel db, ma i dati dall'app di terze parti no.

quando la nostra applicazione scrive FirstName = Céline, viene memorizzato nel DB come Céline

quando l'applicazione webmail scrive FirstName = Céline è memorizzato nel db come CÃ © line

nostro CRM esigenze app per visualizzare i contatti che sono stati creati in entrambi i sistemi. Quindi sto scrivendo una classe EncodingSniffer che cerca caratteri contrassegnati che indicano una stringa scarsamente codificata e li converte.

Attualmente ho:

 
private static string[] _flaggedChars = new string[] { 
      "é" 
     }; 

che funziona alla grande per la visualizzazione di CÃ © line come Céline, ma ho bisogno di aggiungere alla lista.

Qualcuno sa di una risorsa per ottenere tutti i possibili modi in cui i caratteri speciali di utf-8 potrebbero essere interpretati come iso-8859-1?

Grazie

Chiarimento: Dal momento che sto lavorando in .Net. La stringa, caricata in memoria dal database, viene convertita in Unicode UTF-16. Quindi, indipendentemente dal fatto che sia stato codificato correttamente nel database. Ora è rappresentato come byte UTF16. Devo essere in grado di analizzare i byte UTF-16 e determinare se sono stati rovinati a causa del fatto che utf-8 byte sono stati inseriti in un database iso-8859-1 .... chiaro come il fango giusto?

Ecco quello che ho finora. Ha ripulito la maggior parte dei caratteri non correttamente codificati, ma ho ancora problemi con É ad esempio: Éric è memorizzato nel db da webmail come à ‰ ric, ma dopo aver rilevato una codifica errata e averlo cambiato, viene visualizzato come ? ric Guardando un utente che dispone di 2500 contatti, centinaia dei quali erano codificano le questioni, la E è l'unica cosa che non viene visualizzata correttamente ...

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; 
    } 

Quindi: É viene convertita in 195' Ã ', 8240' ‰ '

+0

è un'opzione per modificare la codifica DB a UTF-8, sembra la soluzione più semplice in quanto non c'è un 1 a 1 conversione tra unicode e latin-1 –

+0

Test se la stringa è UTF valida -8 potrebbe essere un approccio migliore. (Probabilmente anche meno costoso.) – Mat

+0

@Mat, è essenzialmente quello che sto cercando di fare, semplicemente non so come. e il cattivo approccio al fiuto del personaggio è stato il migliore che ho trovato. Come faresti a testare l'utf-8 valido? – Michael

risposta

0

Probabilmente dovresti solo provare a decodificare la stringa di byte come UTF-8, e se ricevi un errore, supponiamo invece che sia ISO-8859-1.

Il testo codificato come ISO-8859-1 raramente "accade" anche per essere UTF-8 valido ...a meno che non sia ISO-8859-1 che in realtà contiene solo ASCII, ma in tal caso non si ha alcun problema, naturalmente. Quindi questo metodo è ragionevolmente robusto.

Ignorando quali caratteri si verificano più frequentemente di altri nel linguaggio corrente, ecco un'analisi ingenua che presuppone che ogni personaggio si presenti con la stessa frequenza. Proviamo a scoprire quanto frequentemente ISO-8859-1 può essere scambiato per UTF-8 con conseguente mojibake. Suppongo anche che i caratteri di controllo C1 (da U + 0080 a U + 009F) non si verifichino.

Per ogni byte specificato nella stringa di byte. Se il byte è vicino alla fine della stringa, è ancora più probabile che rilevi UTF-8 malformato perché alcune sequenze di byte non saranno abbastanza lunghe per essere UTF-8 valido. Ma supponendo che il byte non sia vicino alla fine della stringa:

  • p (il byte decodifica come ASCII) = 0,57. Ciò non fornisce alcuna informazione sul fatto che la stringa sia ASCII, ISO-8859-1 o UTF-8.
  • Se questo byte è da 0x80 a 0xc1 o da 0xf8 a 0xff, non può essere UTF-8, quindi lo rileverai. p = 0,33
  • Se questo primo byte è da 0xc2 a 0xdf (p = 0,11), allora potrebbe essere UTF-8 valido, ma solo se seguito da un byte con valore compreso tra 0x80 e 0xbf. La probabilità che il byte successivo non rientri in tale intervallo è 192/224 = 0,86. Quindi la probabilità che UTF-8 fallisca qui è 0.09
  • Se il primo byte è da 0xe0 a 0xef, allora potrebbe essere UTF-8 valido ma solo se seguito da 2 byte di continuazione. La probabilità che venga rilevato il cattivo UTF-8 è quindi (16/224) * (1- (0,14 * 0,14)) = 0,07
  • Simile per 0xf0 a 0xf7, la probabilità è (8/224) * (1- (0,14 * 0,14 * 0,14)) = 0,04.

Ad ogni byte di una stringa lunga, la probabilità di rilevare il cattivo UTF-8 è 0,33 + 0,09 + 0,07 + 0,04 = 0,53.

Quindi per una stringa lunga, la probabilità che ISO-8859-1 passi silenziosamente attraverso un decoder UTF-8 è molto piccola: si dimezza approssimativamente per ogni carattere aggiuntivo!

Questa analisi ovviamente assume caratteri ISO-8859-1 casuali. In pratica il tasso di rilevamento errato non sarà abbastanza buono (soprattutto perché la maggior parte dei byte nel testo reale sono in realtà ASCII), ma sarà comunque molto buona.

+0

Qualcuno può fornire un esempio di codice .net di questo? Non riesco a trovare qualcosa che genera un'eccezione. Tutto ciò che provo rende la codifica ancora più incasinata. – Michael

+0

In realtà, penso di vedere perché questo non funziona. Poiché tutte le stringhe in .Net sono UTF-16, i byte originali del database sono già stati modificati nel momento in cui provo a decodificarli nel codice dell'applicazione. Quindi sto ricevendo il mio array di byte da una stringa utf-16 e cercando di decodificare in utf-8 ... – Michael

0

Grazie a @Michael per oltre il 99% del lavoro!

Ecco una versione di PowerShell dello script di Michael per chiunque sia d'aiuto. Questo anche il suggerimento di @ Qubei sulla codepage/codifica Windows-1252 per risolvere il problema É; sebbene ti permetta di modificare queste codifiche nel caso in cui i tuoi dati vengano corrotti tramite una diversa combinazione di codifiche.

#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

#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 

Funzione supplementare utilizzato nel mio Demo

io non sono un fan della Invoke-SqlCmd cmdlet, quindi arrotolato la mia.

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() 
    } 
} 
+0

Codice con evidenziazione della sintassi disponibile qui: https://gist.githubusercontent.com/JohnLBevan/4c791aa60e85a2e992eff4f415267d47/ (leggermente ottimizzato da quanto sopra, ma non in modo significativo). – JohnLBevan

Problemi correlati