2012-05-07 27 views
9

Tengo una base de datos sql-server 2010 compartida entre dos aplicaciones. Una aplicación que tenemos control, y la otra aplicación es una aplicación de terceros que creó la base de datos en primer lugar. Nuestra aplicación es un CRM construido sobre la aplicación webmail de terceros.Detección de codificación UTF-8 incorrecta: lista de caracteres incorrectos para olfatear?

La base de datos contiene columnas varchar y está codificada en latín-1. La aplicación de terceros está escrita en php y no le importa codificar correctamente los datos, por lo que incluye bytes codificados en utf-8 en las columnas varchar, donde se interpretan como latin-1 y se ven como basura.

Nuestra aplicación CRM está escrita en .Net, que detecta automágicamente que la intercalación de la base de datos es diferente a la codificación de la cadena en la memoria, por lo que cuando .Net escribe en una base de datos, convierte los bytes para que coincidan con la codificación de la base de datos.

Entonces ... los datos escritos en la base de datos desde nuestra aplicación se ven correctos en la base de datos, pero los datos de la aplicación de terceros no.

cuando nuestra aplicación escribe Nombre = Céline, que se almacena en el PP como Céline

cuando la aplicación de correo web escribe Nombre = Céline se almacena en el PP como CÃ © line

necesidades de aplicaciones

Nuestro CRM para mostrar los contactos que se crearon en cualquier sistema. Así que estoy escribiendo una clase EncodingSniffer que busca caracteres marcados que indican que es una cadena mal codificada y los convierte.

Actualmente tengo:

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

la que funciona muy bien para la visualización de CÃ © line como Céline, pero tengo que añadir a la lista.

¿Alguien sabe de un recurso para obtener todas las formas posibles en que los caracteres especiales utf-8 podrían interpretarse como iso-8859-1?

Gracias

Aclaración: Desde que estoy trabajando en .Net. La cadena, cuando se carga en la memoria de la base de datos, se convierte a Unicode UTF-16. Entonces, independientemente de si estaba codificado correctamente en la base de datos. Ahora se representa como bytes UTF16. Necesito poder analizar los bytes UTF-16, y determinar si están dañados debido a que los bytes utf-8 se rellenan en una base de datos iso-8859-1 ... claro como el barro ¿verdad?

Esto es lo que tengo hasta ahora. Se ha limpiado la pantalla de la mayoría de los caracteres mal codificados, pero todavía tengo problemas con É por ejemplo: Éric se almacena en el db por webmail como à ‰ ric, pero después de detectar la codificación incorrecta y cambiarla de nuevo, se muestra como ? ric en cuanto a un usuario que dispone de 2500, cientos de contactos que había que codifican cuestiones, el E es el único que no se muestra correctamente ...

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

Así que: E se está convirtiendo a 195' Ã ', 8240' ‰ '

+0

es una opción para cambiar la codificación de DB a UTF-8, parece que la solución más simple ya que no hay un 1 a 1 conversión entre Unicode y latino-1 –

+0

comprobar si la cadena es UTF válida -8 podría ser un mejor enfoque. (Probablemente también sea menos costoso) – Mat

+0

@Mat, eso es esencialmente lo que estoy tratando de hacer, simplemente no sé cómo. y el enfoque de olfatear a los malos fue lo mejor que se me ocurrió. ¿Cómo harías para probar utf-8 válido? – Michael

Respuesta

0

Probablemente deberías tratar de decodificar la cadena de bytes como UTF-8, y si obtienes un error, asume que es ISO-8859-1 en su lugar.

El texto codificado como ISO-8859-1 rara vez "pasa" a ser también válido UTF-8 ...a menos que sea ISO-8859-1 que solo contenga ASCII, pero en ese caso no tienes ningún problema, por supuesto. Entonces este método es razonablemente robusto.

Ignorando qué caracteres ocurren con más frecuencia que otros en el lenguaje real, aquí hay un análisis ingenuo que supone que cada carácter se produce con la misma frecuencia. Intentemos averiguar con qué frecuencia ISO-8859-1 se puede confundir con UTF-8, lo que da como resultado mojibake. También asumo que los caracteres de control C1 (U + 0080 a U + 009F) no ocurren.

Para cualquier byte dado en la cadena de bytes. Si el byte está cerca del final de la cadena, es aún más probable que detecte UTF-8 con formato incorrecto porque se sabe que algunas secuencias de bytes no serán lo suficientemente largas como para ser UTF-8 válidas. Pero suponiendo que el byte no está cerca del final de la cadena:

  • p (byte decodes as ASCII) = 0.57. Esto no proporciona información sobre si la cadena es ASCII, ISO-8859-1 o UTF-8.
  • Si este byte es 0x80 a 0xc1 o 0xf8 a 0xff, no puede ser UTF-8, por lo que lo detectará. p = 0.33
  • Si este primer byte es 0xc2 a 0xdf (p = 0.11), entonces podría ser válido UTF-8, pero solo si es seguido por un byte con valor entre 0x80 y 0xbf. La probabilidad de que el siguiente byte no esté en ese rango es 192/224 = 0.86. Entonces la probabilidad de que UTF-8 falle aquí es 0.09
  • Si el primer byte es 0xe0 a 0xef entonces podría ser válido UTF-8 pero solo si es seguido por 2 bytes de continuación. La probabilidad de que detecte UTF-8 malo es por lo tanto (16/224) * (1- (0.14 * 0.14)) = 0.07
  • Similar para 0xf0 hasta 0xf7, la probabilidad es (8/224) * (1- (0.14 * 0.14 * 0.14)) = 0.04.

En cada byte en una cadena larga, la probabilidad de detectar mal UTF-8 es 0.33 + 0.09 + 0.07 + 0.04 = 0.53.

Por lo tanto, para una cadena larga, la probabilidad de que ISO-8859-1 pase silenciosamente a través de un decodificador UTF-8 es muy pequeña: ¡aproximadamente se reduce a la mitad para cada carácter adicional!

Este análisis por supuesto asume caracteres aleatorios ISO-8859-1. En la práctica, la tasa de errores de detección no será tan buena como esa (principalmente debido al hecho de que la mayoría de los bytes en el texto del mundo real son realmente ASCII), pero seguirá siendo muy bueno.

+0

¿Alguien puede proporcionar una muestra del código .net de esto? No puedo encontrar algo que arroje una excepción. Todo lo que intento solo hace que la codificación se vea aún más desordenada. – Michael

+0

En realidad, creo que veo por qué esto no funciona. Dado que todas las cadenas en .Net son UTF-16, los bytes originales de la base de datos ya se han modificado en el momento en que trato de decodificarlos en el código de la aplicación. Así que obtengo mi matriz de bytes de una cadena utf-16 y trato de decodificar a utf-8 ... – Michael

0

¡Gracias a @Michael por hacer más del 99% del trabajo!

Aquí hay una versión de PowerShell del guión de Michael para cualquier persona que ayude. Esta es también la sugerencia de @ Qubei de la página de códigos Windows-1252 para resolver el problema É; sin embargo, le permite modificar estas codificaciones en caso de que sus datos se hayan dañado a través de una combinación diferente de codificaciones.

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

demostración

#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 

Función adicional usado en mi demo

No soy un fan de la Invoke-SqlCmd cmdlet, por lo que rodó mi propia.

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

Código con resaltado de sintaxis disponible aquí: https://gist.githubusercontent.com/JohnLBevan/4c791aa60e85a2e992eff4f415267d47/ (ligeramente modificado) de lo anterior, pero no significativamente). – JohnLBevan

Cuestiones relacionadas