2012-01-07 11 views
10

Mam procedurę, która musi być dostarczona ze znormalizowanymi ciągami. Jednak nadchodzące dane niekoniecznie są czyste, a String.Normalize() podnosi ArgumentException, jeśli ciąg zawiera nieprawidłowe punkty kodu.Jak usunąć nieprawidłowe znaki z ciągu?

Co chcę zrobić, to po prostu zastąpić te punkty kodowe znakiem odrzucającym, takim jak "?". Ale aby to zrobić, potrzebuję skutecznego sposobu przeszukiwania ciągu, aby znaleźć je w pierwszej kolejności. Jaki jest dobry sposób na zrobienie tego?

Poniższy kod działa, ale w zasadzie jest to użycie polecenia próbka/catch jako surowego wyrażenia if, więc wydajność jest fatalna. Jestem po prostu dzielenie go ilustrują zachowanie szukam:

private static string ReplaceInvalidCodePoints(string aString, string replacement) 
{ 
    var builder = new StringBuilder(aString.Length); 
    var enumerator = StringInfo.GetTextElementEnumerator(aString); 

    while (enumerator.MoveNext()) 
    { 
     string nextElement; 
     try { nextElement = enumerator.GetTextElement().Normalize(); } 
     catch (ArgumentException) { nextElement = replacement; } 
     builder.Append(nextElement); 
    } 

    return builder.ToString(); 
} 

(edit :) myślę konwersji tekstu do UTF-32 tak, że mogę szybko iteracyjne nad nim i sprawdzić, czy każdy dword odpowiada ważnemu punktowi kodowemu. Czy istnieje funkcja, która to zrobi? Jeśli nie, to czy istnieje tam lista nieprawidłowych zakresów?

+0

zauważyć, że z powodu zastępczych parach, to nie będzie możliwe, aby po prostu patrzeć przy dowolnym "DWORD" i powiedz, czy jest to prawidłowy punkt kodowy. –

+1

UTF-32 nie używa par zastępczych. –

+0

W jaki sposób otrzymujesz te złe dane? Jeśli czytasz to z klasą "Kodowanie", te znaki powinny być domyślnie usunięte. – porges

Odpowiedz

8

Wydaje się, że jedynym sposobem na to jest „ręcznie” jak zrobiłeś. Oto wersja, która daje takie same wyniki jak ty, ale jest nieco szybszy (około 4 razy w ciągu ciąg wszystkich chars górę do char.MaxValue mniej poprawa górę do U+10FFFF) i nie wymagają kodu unsafe. Ja również uproszczone i skomentował mój IsCharacter sposób, aby wyjaśnić każdy wybór:

static string ReplaceNonCharacters(string aString, char replacement) 
{ 
    var sb = new StringBuilder(aString.Length); 
    for (var i = 0; i < aString.Length; i++) 
    { 
     if (char.IsSurrogatePair(aString, i)) 
     { 
      int c = char.ConvertToUtf32(aString, i); 
      i++; 
      if (IsCharacter(c)) 
       sb.Append(char.ConvertFromUtf32(c)); 
      else 
       sb.Append(replacement); 
     } 
     else 
     { 
      char c = aString[i]; 
      if (IsCharacter(c)) 
       sb.Append(c); 
      else 
       sb.Append(replacement); 
     } 
    } 
    return sb.ToString(); 
} 

static bool IsCharacter(int point) 
{ 
    return point < 0xFDD0 || // everything below here is fine 
     point > 0xFDEF && // exclude the 0xFFD0...0xFDEF non-characters 
     (point & 0xfffE) != 0xFFFE; // exclude all other non-characters 
} 
+0

Po prostu próbowałem tego. Dane wyjściowe są identyczne jak dane wejściowe, nieprawidłowe i wszystkie. –

+0

Po prostu wykonałem więcej testów. Wydaje się, że kodowanie UTF-16 zastępuje zepsute punkty kodowe, ale nie dotyczy "nie-znaków". Ciekawy! – porges

+0

Problem nie jest złamany surogaty, to pełne codepoints, które są zdefiniowane jako non-character. U + FFFF, na przykład. –

0

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx powinien mieć informacje, których szukasz, odnosząc się do listy ważnych/nieważnych punktów kodowych w języku C#. Jeśli chodzi o to, jak to zrobić, trochę zajęłoby mi sformułowanie poprawnej odpowiedzi. Ten link powinien pomóc Ci zacząć.

+0

Nie widzę prawidłowej/nieważnej listy punktów kodowych w tych dokumentach - czy możesz wskazać nam to? Dzięki – Rup

+0

Spójrz w górnej części strony, pod którym jest napisane „Uwagi” i stwierdza: _ ".NET Framework używa struktury Char do reprezentowania znak Unicode Standard Unicode identyfikuje każdy znak Unicode z unikalnym 21-. bitowa liczba skalarna nazywana punktem kodowym i definiuje formę kodowania UTF-16, która określa, w jaki sposób punkt kodowy jest zakodowany w sekwencji jednej lub więcej 16-bitowych wartości. Każda 16-bitowa wartość mieści się w zakresie od szesnastkowych 0x0000 do 0xFFFF i jest przechowywana w strukturze Char. wartość obiektu znak to jego (porządkowej) wartość liczbowa 16 bitów. "_ – th3n3wguy

+0

Ok, ale tu problemem jest to, że 'String.Normalise' odrzuca zakresy 0xfdd0-EF 0xfffe-f jako nieprawidłowe punkty kodowe. To informacje, których szukaliśmy i nie widzę tego na stronie 'System.Char'. – Rup

3

Poszedłem naprzód z rozwiązaniem wskazanym w edycji.

Nie można znaleźć łatwej w użyciu listy prawidłowych zakresów w obszarze Unicode; nawet oficjalna baza danych znaków Unicode wymagała więcej analizowania niż naprawdę chciałem. Zamiast tego napisałem szybki skrypt, aby zapętlić każdą liczbę z zakresu [0x0, 0x10FFFF], przekonwertować ją na string przy użyciu Encoding.UTF32.GetString(BitConverter.GetBytes(code)) i spróbować uzyskać wynik w postaci . Jeśli zostanie zgłoszony wyjątek, wówczas ta wartość nie jest poprawnym punktem kodowym.

Z tych wyników, stworzyłem następującą funkcję:

bool IsValidCodePoint(UInt32 point) 
{ 
    return (point >= 0x0 && point <= 0xfdcf) 
     || (point >= 0xfdf0 && point <= 0xfffd) 
     || (point >= 0x10000 && point <= 0x1fffd) 
     || (point >= 0x20000 && point <= 0x2fffd) 
     || (point >= 0x30000 && point <= 0x3fffd) 
     || (point >= 0x40000 && point <= 0x4fffd) 
     || (point >= 0x50000 && point <= 0x5fffd) 
     || (point >= 0x60000 && point <= 0x6fffd) 
     || (point >= 0x70000 && point <= 0x7fffd) 
     || (point >= 0x80000 && point <= 0x8fffd) 
     || (point >= 0x90000 && point <= 0x9fffd) 
     || (point >= 0xa0000 && point <= 0xafffd) 
     || (point >= 0xb0000 && point <= 0xbfffd) 
     || (point >= 0xc0000 && point <= 0xcfffd) 
     || (point >= 0xd0000 && point <= 0xdfffd) 
     || (point >= 0xe0000 && point <= 0xefffd) 
     || (point >= 0xf0000 && point <= 0xffffd) 
     || (point >= 0x100000 && point <= 0x10fffd); 
} 

pamiętać, że ta funkcja nie jest koniecznie dobre dla czyszczenia ogólnego przeznaczenia, w zależności od potrzeb. Nie wyklucza nieprzypisanych ani zarezerwowanych punktów kodowych, tylko te, które są specjalnie oznaczone jako "nietypowe" (edycja: i niektóre inne, które Normalize() wydaje się dławić, takie jak 0xfffff). Jednak wydaje się, że są to jedyne punkty kodowe, które powodują, że IsNormalized() i Normalize() mogą zgłaszać wyjątki, więc jest to w porządku dla moich celów.

Później chodzi tylko o konwersję ciągu znaków na UTF-32 i przeczesywanie go. Od Encoding.GetBytes() zwraca tablicę bajtów i IsValidCodePoint() oczekuje UInt32 użyłem niebezpiecznego bloku i jakiś odlew w celu wypełnienia luki:

unsafe string ReplaceInvalidCodePoints(string aString, char replacement) 
{ 
    if (char.IsHighSurrogate(replacement) || char.IsLowSurrogate(replacement)) 
     throw new ArgumentException("Replacement cannot be a surrogate", "replacement"); 

    byte[] utf32String = Encoding.UTF32.GetBytes(aString); 

    fixed (byte* d = utf32String) 
    fixed (byte* s = Encoding.UTF32.GetBytes(new[] { replacement })) 
    { 
     var data = (UInt32*)d; 
     var substitute = *(UInt32*)s; 

     for(var p = data; p < data + ((utf32String.Length)/sizeof(UInt32)); p++) 
     { 
      if (!(IsValidCodePoint(*p))) *p = substitute; 
     } 
    } 

    return Encoding.UTF32.GetString(utf32String); 
} 

wydajność jest dobra, stosunkowo mówiąc - kilka rzędów wielkości szybciej niż próbki Użyto w pytanie. Pozostawienie danych w UTF-16 prawdopodobnie byłoby szybsze i bardziej wydajne pod względem pamięci, ale kosztem wielu dodatkowych kodów do czynienia z surogatami. I oczywiście o replacement być char oznacza, że ​​zastępujący znak musi być na BMP.

edit: Oto znacznie bardziej zwięzły wersja IsValidCodePoint():

private static bool IsValidCodePoint(UInt32 point) 
{ 
    return point < 0xfdd0 
     || (point >= 0xfdf0 
      && ((point & 0xffff) != 0xffff) 
      && ((point & 0xfffe) != 0xfffe) 
      && point <= 0x10ffff 
     ); 
} 
+1

Istnieje wyznaczony punkt kodowy dla nieznanych znaków, który należy zastąpić, co najmniej jako domyślny znak zastępczy; U + FFFD. – tripleee

+0

Co jest warte, nie potrzebujesz niebezpiecznego kodu; możesz użyć ['BitConverter.ToUInt32'] (http://msdn.microsoft.com/en-us/library/system.bitconverter.touint32.aspx) do konwersji bajtów w tablicy na' UInt32's. –

+0

Tak, ale tworzy to kolejną kopię danych. –

0

lubię Regex zbliżyć się do najbardziej

public static string StripInvalidUnicodeCharacters(string str) 
{ 
    var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])"); 
    return invalidCharactersRegex.Replace(str, ""); 
} 
+0

W czasie od pierwszego zadawania tego pytania całkowicie zrezygnowałem z używania wyrażenia regularnego do wykonywania takich zadań. Używanie wyrażenia regularnego może zaoszczędzić kilka naciśnięć klawiszy, ale w praktyce staje się mniej czytelne, trudniejsze do debugowania i mniej wydajne. –

+0

@SeanU To jest poprawny punkt. Dostarczyłem rozwiązanie Regex tylko dla kompletności. – mnaoumov

Powiązane problemy