PHP: Konwertuj dowolny ciąg do UTF-8 bez znajomości oryginalnego zestawu znaków lub przynajmniej spróbuj

146

Mam aplikację, która obsługuje klientów z całego świata i oczywiście chcę, aby wszystko, co trafia do moich baz danych, było zakodowane w UTF-8.

Głównym problemem dla mnie jest to, że nie wiem, jakie kodowanie będzie miało źródło dowolnego ciągu - może to być z pola tekstowego (użycie <form accept-charset="utf-8">jest przydatne tylko wtedy, gdy użytkownik faktycznie przesłał formularz) lub może to być z przesłanego pliku tekstowego, więc naprawdę nie mam kontroli nad danymi wejściowymi.

To, czego potrzebuję, to funkcja lub klasa, która upewnia się, że zawartość mojej bazy danych jest w miarę możliwości zakodowana w UTF-8. Próbowałem, iconv(mb_detect_encoding($text), "UTF-8", $text); ale to ma problemy (jeśli dane wejściowe to „narzeczona”, zwraca „narzeczona”). Próbowałem wielu rzeczy = /

W przypadku przesyłania plików podoba mi się pomysł poproszenia użytkownika końcowego o określenie używanego przez niego kodowania i pokazanie podglądu tego, jak będzie wyglądać wynik, ale to nie pomaga w walce z paskudnymi hakerami (w rzeczywistości może to zmienić ich życie trochę łatwiej).

Przeczytałem inne pytania SO na ten temat, ale wydaje się, że wszystkie mają subtelne różnice, takie jak „Muszę przeanalizować kanały RSS” lub „Pobieram dane ze stron internetowych” (lub w rzeczywistości „Nie możesz”).

Ale musi być coś, co przynajmniej warto spróbować !

Ponury...
źródło
5
Zasadniczo nie jest możliwe uzyskanie absolutnej poprawności, w rzeczywistości wskaźnik sukcesu w odgadywaniu nieznanego kodowania nie jest oszałamiający. Możliwe jest użycie heurystyki, ale będzie to poprawne mniej niż w 100% przypadków, w zależności od materiału znacznie mniej niż 100%. Musisz być tego świadomy. Może ktoś tutaj może przynajmniej zasugerować bibliotekę z dobrą heurystyką.
deceze
Jasne, wiem, że nie ma idealnego rozwiązania - stąd pragnienie czegoś, co przynajmniej będzie się dobrze sprawdzać.
Ponury ...
to może pomóc: stackoverflow.com/q/505562/642173
Melsi
Czy próbowałeś użyć UTF-8//IGNOREjako drugiego parametru w iconv?
ogień
Tak, właśnie to zrobiłem. Oczywiście nie doskonały, bo wtedy „narzeczona” staje się „narzeczonym”, ale na pewno jest lepiej. Dlaczego TRANSLIT nie działa?
Ponury ...

Odpowiedzi:

255

To, o co prosisz, jest niezwykle trudne. Jeśli to możliwe, najlepiej jest nakłonić użytkownika do określenia kodowania. Zapobieganie atakowi nie powinno być w ten sposób dużo łatwiejsze ani trudniejsze.

Możesz jednak spróbować zrobić to:

iconv(mb_detect_encoding($text, mb_detect_order(), true), "UTF-8", $text);

Ustawienie wartości ścisłej może pomóc uzyskać lepszy wynik.

Jeff Day
źródło
5
Proszę spojrzeć na mb_detect_encodingkod źródłowy w swojej dystrybucji php (gdzieś tutaj: ext / mbstring / libmbfl / mbfl / mbfl_ident.c). Ta funkcja w ogóle nie działa prawidłowo. W przypadku niektórych kodowań ma nawet „return true”, lol. Inne są w funkcjach Ctrl + c Ctrl + v. Dzieje się tak, ponieważ nie możesz wykryć kodowania bez jakiegoś słownika lub podejścia statystycznego (takiego jak moje).
Oroboros102
1
Sposób, w jaki to rozumiem, mb_detect_encodingprzechodzi przez listę dostarczonych kodowań i akceptuje pierwszy, który nie ma nieprawidłowych sekwencji bajtów w ciągu ... W przypadku kodowań, które nie mają nieprawidłowych sekwencji bajtów, takich jak ISO-8859-1, zawsze jest to prawda . Brak „inteligentnej” heurystyki, a wyniki różnią się znacznie w zależności od listy (i kolejności) przekazywanych kodowań.
wutz
Wydaje się, że to działa dla mnie. Moi użytkownicy przesyłali tekst na stronie utf8 z tinymce, ale z jakiegoś nieznanego powodu znaki inne niż utf8 czasami trafiały do ​​bazy danych. Naprawiło to, więc bardzo dziękuję.
giorgio79
@Jeff Day - Dzięki za to. Przepraszam za moją ignorancję, co masz na myśli mówiąc „ustawienie ścisłości”?
Ash501
[Jeff Day] wysyła, mb_detect_order()mimo że jest to domyślna wartość tego parametru, ponieważ chciał ustawić ścisłe wykrywanie kodowania na true (trzeci parametr) :)
jave.web
28

W ojczyźnie Rosji mamy 4 popularne kodowania, więc twoje pytanie jest tutaj bardzo pożądane.

Tylko za pomocą kodów znaków symboli nie można wykryć kodowania, ponieważ strony kodowe się przecinają. Niektóre strony kodowe w różnych językach mają nawet pełne przecięcie. Tak, potrzebujemy innego podejścia .

Jedynym sposobem pracy z nieznanymi kodowaniami jest praca z prawdopodobieństwami. Nie chcemy więc odpowiadać na pytanie „co to jest kodowanie tego tekstu?”, Staramy się zrozumieć „ jakie jest najprawdopodobniej kodowanie tego tekstu? ”.

Jeden facet z popularnego rosyjskiego bloga technicznego wymyślił takie podejście:

Zbuduj zakres prawdopodobieństwa kodów znaków w każdym kodowaniu, które chcesz obsługiwać. Możesz go zbudować, używając dużych tekstów w swoim języku (np. Trochę fikcji, użyj Szekspira dla angielskiego i Tołstoja dla rosyjskiego, lol). Otrzymasz coś takiego:

    encoding_1:
    190 => 0.095249209893009,
    222 => 0.095249209893009,
    ...
    encoding_2:
    239 => 0.095249209893009,
    207 => 0.095249209893009,
    ...
    encoding_N:
    charcode => probabilty

Kolejny. Bierzesz tekst w nieznanym kodowaniu i dla każdego kodowania w swoim „słowniku prawdopodobieństwa” szukasz częstotliwości każdego symbolu w nieznanym zakodowanym tekście. Sumowanie prawdopodobieństw symboli. Prawdopodobnie wygrywa kodowanie z wyższą oceną. Lepsze wyniki dla większych tekstów.

Jeśli jesteś zainteresowany , chętnie pomogę Ci w tym zadaniu. Możemy znacznie zwiększyć dokładność, budując listę prawdopodobieństwa z dwoma znakami.

Przy okazji. mb_detect_encoding certanly nie działa. Tak, w ogóle. Proszę spojrzeć na kod źródłowy mb_detect_encoding w "ext / mbstring / libmbfl / mbfl / mbfl_ident.c".

Oroboros102
źródło
11

Prawdopodobnie próbowałeś tego, ale dlaczego nie użyć po prostu funkcji mb_convert_encoding? Spróbuje automatycznie wykryć zestaw znaków podanego tekstu lub możesz przekazać mu listę.

Próbowałem też uruchomić:

$text = "fiancée";
echo mb_convert_encoding($text, "UTF-8");
echo "<br/><br/>";
echo iconv(mb_detect_encoding($text), "UTF-8", $text);

a wyniki są takie same dla obu. Jak widzisz, że Twój tekst jest skracany do słowa „narzeczony”? czy to w bazie danych czy w przeglądarce?

Alexey Gerasimov
źródło
Wygląda na to, że w bazie danych - właśnie spróbowałem z twoim kodem i zgadzam się.
Ponury ...
1
Sprawdź, czy sortowanie, które zdefiniowałeś w tabeli / kolumnie, to również UTF-8.
Alexey Gerasimov
@AlexeyGerasimov Chyba naprawdę muszę to sprawdzić iconv. Próbowałem zrobić prawie czysty sposób mb_ *. Co o tym myślisz
Anthony Rutledge,
5

Nie ma sposobu na zidentyfikowanie zestawu znaków łańcucha, który jest całkowicie dokładny. Istnieją sposoby, aby spróbować odgadnąć zestaw znaków. Jednym z tych sposobów, prawdopodobnie / obecnie najlepszym w PHP, jest mb_detect_encoding (). Spowoduje to przeskanowanie łańcucha i wyszukanie wystąpień elementów unikalnych dla określonych zestawów znaków. W zależności od twojego ciągu, może nie być takich rozróżnialnych wystąpień.

Weź zestaw znaków ISO-8859-1 w porównaniu z ISO-8859-15 ( http://en.wikipedia.org/wiki/ISO/IEC_8859-15#Changes_from_ISO-8859-1 )

Jest tylko kilka różnych znaków, a co gorsza, są one reprezentowane przez te same bajty. Nie ma sposobu, aby wykryć otrzymanie łańcucha bez znajomości jego kodowania, czy bajt 0xA4 ma oznaczać ¤, czy € w twoim ciągu, więc nie ma sposobu, aby poznać dokładny zestaw znaków.

(Uwaga: możesz dodać czynnik ludzki lub jeszcze bardziej zaawansowaną technikę skanowania (np. To, co sugeruje Oroboros102), aby spróbować ustalić na podstawie otaczającego kontekstu, czy postać powinna być ¤ czy €, chociaż wydaje się to być pomostem za daleko)

Jest więcej dostrzegalnych różnic między np. UTF-8 i ISO-8859-1, więc nadal warto spróbować to rozgryźć, gdy nie jesteś pewien, chociaż możesz i nigdy nie powinieneś polegać na tym, że jest poprawny.

Ciekawa lektura: http://kore-nordmann.de/blog/php_charset_encoding_FAQ.html#how-do-i-determine-the-charset-encoding-of-a-string

Istnieją jednak inne sposoby na zapewnienie prawidłowego zestawu znaków. Jeśli chodzi o formularze, staraj się egzekwować stosowanie UTF-8 tak bardzo, jak to możliwe (sprawdź bałwana, aby upewnić się, że przesyłanie będzie w formacie UTF-8 w każdej przeglądarce: http://intertwingly.net/blog/2010/07/29/Rails-and -Snowmen ) Po wykonaniu tej czynności przynajmniej możesz być pewien, że każdy tekst przesłany za pośrednictwem formularzy to utf_8. Jeśli chodzi o przesłane pliki, spróbuj uruchomić na nim polecenie unix 'file -i' poprzez np. Exec () (jeśli to możliwe na twoim serwerze), aby pomóc w wykryciu (używając BOM dokumentu). Jeśli chodzi o pobieranie danych, możesz odczytać nagłówki HTTP, które zwykle określają zestaw znaków. Podczas analizowania plików XML sprawdź, czy metadane XML zawierają definicję zestawu znaków.

Zamiast próbować automagicznie odgadnąć zestaw znaków, powinieneś najpierw spróbować samemu zapewnić określony zestaw znaków, jeśli to możliwe, lub spróbować pobrać definicję ze źródła, z którego ją otrzymujesz (jeśli ma to zastosowanie), zanim uciekniesz się do wykrywania.

matthiasmullie
źródło
Formularze i linki rejestracyjne e-mail z zaszyfrowanymi danymi. W tym miejscu staram się, aby moje dane wejściowe były UTF-8 lub nic. Co myślisz o mojej odpowiedzi? Doceniamy pomocne komentarze. Dzięki.
Anthony Rutledge,
3

Jest tutaj kilka naprawdę dobrych odpowiedzi i prób odpowiedzi na twoje pytanie. Nie jestem mistrzem kodowania, ale rozumiem Twoje pragnienie posiadania czystego stosu UTF-8 aż do bazy danych. Używam utf8mb4kodowania MySQL dla tabel, pól i połączeń.

Moja sytuacja sprowadzała się do stwierdzenia: „Chcę tylko, aby moje środki dezynfekujące, walidatory, logika biznesowa i przygotowane oświadczenia radziły sobie z UTF-8, gdy dane pochodzą z formularzy HTML lub e-mailowych linków rejestracyjnych”. Tak więc, na swój prosty sposób, zacząłem od tego pomysłu:

  1. Spróbuj wykryć kodowanie: $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];
  2. Jeśli nie można wykryć kodowania, throw new RuntimeException
  3. Jeśli dane wejściowe są UTF-8, kontynuuj.
  4. W przeciwnym razie, jeśli jest ISO-8859-1lubASCII

    za. Próba konwersji na UTF-8 (czekaj, nie zakończono)

    b. Wykryj kodowanie przekonwertowanej wartości

    do. Jeśli raportowane kodowanie i przekonwertowana wartość są takie same UTF-8, kontynuuj.

    re. Jeszcze,throw new RuntimeException

Z mojej klasy abstrakcyjnej Sanitizer

Środek odkażający

    private function isUTF8($encoding, $value)
    {
        return (($encoding === 'UTF-8') && (utf8_encode(utf8_decode($value)) === $value));
    }

    private function utf8tify(&$value)
    {
        $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];

        mb_internal_encoding('UTF-8');
        mb_substitute_character(0xfffd); //REPLACEMENT CHARACTER
        mb_detect_order($encodings);

        $stringEncoding = mb_detect_encoding($value, $encodings, true);

        if (!$stringEncoding) {
            $value = null;
            throw new \RuntimeException("Unable to identify character encoding in sanitizer.");
        }

        if ($this->isUTF8($stringEncoding, $value)) {
            return;
        } else {
            $value = mb_convert_encoding($value, 'UTF-8', $stringEncoding);
            $stringEncoding = mb_detect_encoding($value, $encodings, true);

            if ($this->isUTF8($stringEncoding, $value)) {
                return;
            } else {
                $value = null;
                throw new \RuntimeException("Unable to convert character encoding from ISO-8859-1, or ASCII, to UTF-8 in Sanitizer.");
            }
        }

        return;
    }

Można by argumentować, że powinienem oddzielić zagadnienia związane z kodowaniem od mojej Sanitizerklasy abstrakcyjnej i po prostu wstrzyknąć Encoderobiekt do konkretnej instancji podrzędnej Sanitizer. Jednak głównym problemem związanym z moim podejściem jest to, że bez większej wiedzy po prostu odrzucam typy kodowania, których nie chcę (i polegam na funkcjach PHP mb_ *). Bez dalszych badań nie mogę wiedzieć, czy to boli niektóre populacje, czy nie (lub, jeśli tracę ważne informacje). Muszę się więc dowiedzieć więcej. Znalazłem ten artykuł.

To, co każdy programista absolutnie, pozytywnie musi wiedzieć o kodowaniu i zestawach znaków do pracy z tekstem

Co więcej, co się stanie, gdy zaszyfrowane dane zostaną dodane do moich łączy rejestracyjnych e-mail (przy użyciu OpenSSLlub mcrypt)? Czy może to przeszkadzać w dekodowaniu? A co z Windows-1252? A co z konsekwencjami dla bezpieczeństwa? Użycie utf8_decode()i utf8_encode()w Sanitizer::isUTF8jest wątpliwe.

Ludzie wskazywali na niedociągnięcia w funkcjach PHP mb_ *. Nigdy nie poświęcałem czasu na badanie iconv, ale jeśli działa lepiej niż funkcje mb_ *, daj mi znać.

Anthony Rutledge
źródło
Znalazłem to, stackoverflow.com/a/3521396/1429677 doskonała odpowiedź na ten problem, oto lib github.com/neitanod/forceutf8
Llewellyn
2

Głównym problemem dla mnie jest to, że nie wiem, jakie kodowanie będzie miało źródło dowolnego ciągu - może to być z pola tekstowego (użycie jest przydatne tylko wtedy, gdy użytkownik faktycznie przesłał formularz) lub może to być z przesłanego pliku tekstowego, więc naprawdę nie mam kontroli nad danymi wejściowymi.

Nie sądzę, żeby to był problem. Aplikacja zna źródło danych wejściowych. Jeśli pochodzi z formularza, użyj w swoim przypadku kodowania UTF-8. To działa. Po prostu sprawdź, czy podane dane są poprawnie zakodowane (walidacja). Należy pamiętać, że nie wszystkie bazy danych obsługują UTF-8 w jego pełnym zakresie.

Jeśli jest to plik, nie zapiszesz go zakodowanego w formacie UTF-8 w bazie danych, ale w formie binarnej. Kiedy ponownie wyprowadzasz plik, użyj również wyjścia binarnego, wtedy jest to całkowicie przezroczyste.

Twój pomysł jest fajny, że użytkownik może powiedzieć kodowanie, czy i tak może to stwierdzić po pobraniu pliku, ponieważ jest on binarny.

Muszę więc przyznać, że nie widzę konkretnego problemu, który poruszysz w swoim pytaniu. Ale może możesz dodać więcej szczegółów na temat twojego problemu.

hakre
źródło
Czy zobaczyłbyś moją odpowiedź i nie zgadzasz się z nią? Konstruktywne komentarze są mile widziane. Dzięki.
Anthony Rutledge,
1

Możesz skonfigurować zestaw wskaźników, aby spróbować odgadnąć, które kodowanie jest używane. Ponownie, nie jest doskonały, ale może wyłapać niektóre błędy z mb_detect_encoding ().

Parris Varney
źródło
Tak, mówiąc o mb_detect_encoding()pudłach, czy myślisz, że moja odpowiedź ma szansę na śnieżkę latem na Saharze?
Anthony Rutledge
1

Jeśli chcesz „zabrać to na konsolę”, polecam enca. W przeciwieństwie do raczej uproszczonego mb_detect_encoding, używa „mieszanki analizowania, analizy statystycznej, zgadywania i czarnej magii w celu określenia ich kodowania” (lol - patrz strona podręcznika ). Jednak zwykle musisz przekazać język pliku wejściowego, jeśli chcesz wykryć takie kodowania specyficzne dla kraju. (Jednak mb_detect_encodingzasadniczo ma te same wymagania, ponieważ kodowanie musiałoby pojawić się „we właściwym miejscu” na liście przekazywanych kodowań, aby w ogóle było wykrywalne).

encapojawił się również tutaj: Jak znaleźć kodowanie pliku w systemie Unix za pomocą skryptów

wutz
źródło
1

Wygląda na to, że odpowiedź na Twoje pytanie jest dość wyczerpująca, ale mam podejście, które może uprościć sprawę:

Miałem podobny problem, próbując zwrócić dane ciągów z mysql, nawet konfigurując zarówno bazę danych, jak i php, aby zwracały ciągi sformatowane do utf-8. Jedynym sposobem, w jaki otrzymałem błąd, było zwrócenie ich z bazy danych.

Wreszcie, żeglując po Internecie, znalazłem naprawdę łatwy sposób, aby sobie z tym poradzić:

Biorąc pod uwagę, że możesz zapisywać wszystkie te typy danych ciągów w swoim mysql w różnych formatach i zestawieniach, wystarczy, że w pliku połączenia php ustaw sortowanie na utf-8, na przykład:

$connection = new mysqli($server, $user, $pass, $db);
$connection->set_charset("utf8");

Oznacza to, że najpierw zapisujesz dane w dowolnym formacie lub sortowaniu i konwertujesz je dopiero po powrocie do pliku php.

Mam nadzieję, że to było pomocne!

Quel Pino
źródło
-2
public function convertToUtf8($text) {
    if(!$this->html)
        $this->html = cURL('http://'.$this->url, array('timeout' => 15));

    $html = $this->html;
    preg_match('/<meta.*?charset=(|\")(.*?)("|\")/i', $html, $matches);

    $charset = $matches[2];

    if($charset)
        return mb_convert_encoding($text, 'UTF-8', $charset);
    else
        return $text;
}

cURL domyślne opcje:

curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

Próbowałem czegoś takiego. Pomogło mi. Jeśli zostanie znaleziony w informacjach o meta charset, konwertuję, w przeciwnym razie nic nie robię.

littlealien
źródło
errr, czy możesz sprawdzić swoją funkcję i poprawić zmienne?
Martin
Co to jest $ url? Co to jest $ html?
Martin