Filtrowanie niepoprawnego utf8

50

Mam plik tekstowy w nieznanym lub mieszanym kodowaniu. Chcę zobaczyć wiersze zawierające sekwencję bajtów, która jest niepoprawna UTF-8 (przez przesłanie pliku tekstowego do jakiegoś programu). Odpowiednio chcę odfiltrować wiersze, które są poprawne UTF-8. Innymi słowy, szukam .grep [notutf8]

Idealne rozwiązanie byłoby przenośne, krótkie i możliwe do uogólnienia w stosunku do innych kodowań, ale jeśli uważasz, że najlepszym sposobem jest upieczenie w definicji UTF-8 , śmiało.

Gilles „SO- przestań być zły”
źródło
Zobacz także keithdevens.com/weblog/archive/2004/Jun/29/UTF-8.regex, aby zapoznać się z możliwym wyrażeniem regularnym.
Mikel
@Mikel: lub unix.stackexchange.com/questions/6460/...… Miałem nadzieję na coś mniej niezgrabnie.
Gilles „SO- przestań być zły”

Odpowiedzi:

34

Jeśli chcesz użyć grep, możesz:

grep -axv '.*' file

w ustawieniach regionalnych UTF-8, aby uzyskać linie, które mają co najmniej nieprawidłową sekwencję UTF-8 (działa to przynajmniej z GNU Grep).

vinc17
źródło
Poza -atym jest to wymagane do pracy przez POSIX. Jednak GNU grepprzynajmniej nie dostrzega zakodowanych w znakach UTF-8 znaków zastępczych UTF-16 lub znaków kodowych powyżej 0x10FFFF.
Stéphane Chazelas,
1
@ StéphaneChazelas I odwrotnie, -ajest to potrzebne GNU grep(który, jak zakładam, nie jest zgodny z POSIX). Jeśli chodzi o obszar zastępczym i codepoints Powyższe 0x10FFFF, jest to bug to (co może tłumaczyć , że ). W tym celu dodawanie -Ppowinno działać z GNU grep2.21 (ale jest powolne); jest wadliwy przynajmniej w Debianie grep / 2.20-4 .
vinc17
Przepraszamy, moje złe, zachowanie nie jest określone w POSIX, ponieważ grepjest narzędziem tekstowym (oczekuje się, że będzie działać tylko przy wprowadzaniu tekstu), więc przypuszczam, że zachowanie GNU grep jest tak samo ważne jak tutaj.
Stéphane Chazelas,
@ StéphaneChazelas Potwierdzam, że POSIX mówi: „Pliki wejściowe powinny być plikami tekstowymi”. (choć nie w części opisu, co jest nieco mylące). Oznacza to również, że w przypadku nieprawidłowych sekwencji zachowanie jest niezdefiniowane przez POSIX. Stąd potrzeba znajomości implementacji, takiej jak GNU grep(której celem jest uznanie nieprawidłowych sekwencji za niepasujące) i możliwych błędów.
vinc17,
1
Mam załączeniu przyjęty odpowiedź na to jedno (przepraszam, Peter.O ponieważ jest prosty i działa dobrze dla mojego pierwotnego przypadku użycia, który jest heurystyczny odróżnić UTF-8 od innych wspólnych kodowania (zwłaszcza kodowanie 8-bitowe). Stéphane Chazelas i Peter.O udzielają dokładniejszych odpowiedzi w zakresie zgodności z UTF-8
Gilles „SO- przestań być zły”
33

Myślę, że prawdopodobnie chcesz iconv . Służy do konwersji między zestawami kodów i obsługuje absurdalną liczbę formatów. Na przykład, aby usunąć wszystko, co nie jest poprawne w UTF-8, możesz użyć:

iconv -c -t UTF-8 < input.txt > output.txt

Bez opcji -c zgłasza problemy z konwersją do stderr, więc z kierunkiem procesu możesz zapisać ich listę. Innym sposobem byłoby usunięcie elementów innych niż UTF8, a następnie

diff input.txt output.txt

po listę, gdzie dokonano zmian.

frabjous
źródło
Okej iconv -c -t UTF-8 <input.txt | diff input.txt - | sed -ne 's/^< //p'. Nie będzie to jednak działało jako potok, ponieważ musisz odczytać dane wejściowe dwa razy (nie, teenie zrobi się, może blokować w zależności od tego, ile buforowania iconvi diffzrobić).
Gilles „SO- przestań być zły”
2
Losowa uwaga: dane wejściowe i wyjściowe mogą nie być tym samym plikiem lub skończysz na pustym pliku
drahnr
1
Lub użyj substytucji procesu, jeśli twoja powłoka go obsługujediff <(iconv -c -t UTF-8 <input.txt) input.txt
Karl
Jak to zrobić i zrobić wyjście do tego samego pliku co wejście. Właśnie to zrobiłem i otrzymałem pusty plik iconv -c -t UTF-8 <input.txt> input.txt
Costas Vrahimis
1
Dzięki .. Pozwala to na przywrócenie zepsutego zrzutu postgresql utf-8, ale nie odrzuca ważnego utf-8
Superbiji,
21

Edycja: Naprawiłem błąd literowy w wyrażeniu regularnym. Potrzebowałem „\ x80”, a nie \ 80 .

Wyrażenie regularne, aby odfiltrować nieprawidłowe formularze UTF-8, w celu ścisłego przestrzegania UTF-8, jest następujące

perl -l -ne '/
 ^( ([\x00-\x7F])              # 1-byte pattern
   |([\xC2-\xDF][\x80-\xBF])   # 2-byte pattern
   |((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF])) # 3-byte pattern
   |((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2}))       # 4-byte pattern
  )*$ /x or print'

Wyjście (kluczowych linii. Z testu 1 ):

Codepoint
=========  
00001000  Test=1 mode=strict               valid,invalid,fail=(1000,0,0)          
0000E000  Test=1 mode=strict               valid,invalid,fail=(D800,800,0)          
0010FFFF  mode=strict  test-return=(0,0)   valid,invalid,fail=(10F800,800,0)          

P: Jak można utworzyć dane testowe w celu przetestowania wyrażenia regularnego, które filtruje nieprawidłowy kod Unicode?
A. Stwórz swój własny algorytm testowy UTF-8 i złam jego zasady ...
Catch-22 .. Ale w jaki sposób następnie testujesz swój algorytm testowy?

Wyrażenie regularne powyżej zostało przetestowane (przy użyciu iconvjako odniesienie) dla każdej wartości całkowitej od 0x00000do 0x10FFFF.. Ta górna wartość jest maksymalną wartością całkowitą Unicode Codepoint

Według tej strony wikipedii UTF-8 .

  • UTF-8 koduje każdy z 1 112 064 punktów kodowych w zestawie znaków Unicode, używając jednego do czterech 8-bitowych bajtów

Ten numeber (1112064) równa się w zakresie 0x000000do 0x10F7FF, co 0x0800 trwożliwa rzeczywistej maksymalnej całkowitej wartości dla najwyższej kodowy Unikodu:0x10FFFF

Brakuje tego bloku liczb całkowitych w widmie Unicode Codepoints, ponieważ kodowanie UTF-16 musi wykraczać poza pierwotne założenia projektowe za pośrednictwem systemu zwanego parami zastępczymi . Blok 0x0800liczb całkowitych został zarezerwowany do użycia przez UTF-16 .. Blok ten obejmuje zakres 0x00D800do 0x00DFFF. Żaden z tych parametrów nie jest prawidłowymi wartościami Unicode, a zatem są nieprawidłowymi wartościami UTF-8.

W teście 1 , regexzostał przetestowany przed każdym numerem w zakresie codepoints Unicode i pasuje exectly wyniki iconv .. tzn. 0x010F7FF prawidłowe wartości i 0x000800 nieprawidłowe wartości.

Jednak teraz pojawia się problem: * Jak wyrażenie regularne obsługuje wartość UTF-8 poza zakresem; powyżej 0x010FFFF(UTF-8 może rozciągać się do 6 bajtów, przy maksymalnej wartości całkowitej 0x7FFFFFFF ?
Aby wygenerować niezbędne * wartości bajtów UTF-8 inne niż Unicode , użyłem następującej komendy:

  perl -C -e 'print chr 0x'$hexUTF32BE

Aby przetestować ich ważność (w pewien sposób), użyłem Gilles'wyrażenia regularnego UTF-8 ...

  perl -l -ne '/
   ^( [\000-\177]                 # 1-byte pattern
     |[\300-\337][\200-\277]      # 2-byte pattern
     |[\340-\357][\200-\277]{2}   # 3-byte pattern
     |[\360-\367][\200-\277]{3}   # 4-byte pattern
     |[\370-\373][\200-\277]{4}   # 5-byte pattern
     |[\374-\375][\200-\277]{5}   # 6-byte pattern
    )*$ /x or print'

Wyjście „perl's print chr” pasuje do filtrowania wyrażenia regularnego Gillesa. Jeden wzmacnia ważność drugiego .. Nie mogę użyć, iconvponieważ obsługuje tylko prawidłowy podzbiór Standard Unicode szerszego (oryginalnego) UTF-8 standard...

Zaangażowane zakonnice są raczej duże, więc przetestowałem top-of-range, bottom-of-range i kilka skanów krok po kroku, takich jak 11111, 13579, 33333, 53441 ... Wszystkie wyniki są zgodne, więc teraz wszystko, co pozostaje, to przetestowanie wyrażenia regularnego względem tych wartości poza zakresem w stylu UTF-8 (niepoprawne dla Unicode, a zatem również niepoprawne dla samego ścisłego UTF-8).


Oto moduły testowe:

[[ "$(locale charmap)" != "UTF-8" ]] && { echo "ERROR: locale must be UTF-8, but it is $(locale charmap)"; exit 1; }

# Testing the UTF-8 regex
#
# Tests to check that the observed byte-ranges (above) have
#  been  accurately observed and included in the test code and final regex. 
# =========================================================================
: 2 bytes; B2=0 #  run-test=1   do-not-test=0
: 3 bytes; B3=0 #  run-test=1   do-not-test=0
: 4 bytes; B4=0 #  run-test=1   do-not-test=0 

:   regex; Rx=1 #  run-test=1   do-not-test=0

           ((strict=16)); mode[$strict]=strict # iconv -f UTF-16BE  then iconv -f UTF-32BE beyond 0xFFFF)
           ((   lax=32)); mode[$lax]=lax       # iconv -f UTF-32BE  only)

          # modebits=$strict
                  # UTF-8, in relation to UTF-16 has invalid values
                  # modebits=$strict automatically shifts to modebits=$lax
                  # when the tested integer exceeds 0xFFFF
          # modebits=$lax 
                  # UTF-8, in relation to UTF-32, has no restrictione


           # Test 1 Sequentially tests a range of Big-Endian integers
           #      * Unicode Codepoints are a subset ofBig-Endian integers            
           #        ( based on 'iconv' -f UTF-32BE -f UTF-8 )    
           # Note: strict UTF-8 has a few quirks because of UTF-16
                    #    Set modebits=16 to "strictly" test the low range

             Test=1; modebits=$strict
           # Test=2; modebits=$lax
           # Test=3
              mode3wlo=$(( 1*4)) # minimum chars * 4 ( '4' is for UTF-32BE )
              mode3whi=$((10*4)) # minimum chars * 4 ( '4' is for UTF-32BE )


#########################################################################  

# 1 byte  UTF-8 values: Nothing to do; no complexities.

#########################################################################

#  2 Byte  UTF-8 values:  Verifying that I've got the right range values.
if ((B2==1)) ; then  
  echo "# Test 2 bytes for Valid UTF-8 values: ie. values which are in range"
  # =========================================================================
  time \
  for d1 in {194..223} ;do
      #     bin       oct  hex  dec
      # lo  11000010  302   C2  194
      # hi  11011111  337   DF  223
      B2b1=$(printf "%0.2X" $d1)
      #
      for d2 in {128..191} ;do
          #     bin       oct  hex  dec
          # lo  10000000  200   80  128
          # hi  10111111  277   BF  191
          B2b2=$(printf "%0.2X" $d2)
          #
          echo -n "${B2b1}${B2b2}" |
            xxd -p -u -r  |
              iconv -f UTF-8 >/dev/null || { 
                echo "ERROR: Invalid UTF-8 found: ${B2b1}${B2b2}"; exit 20; }
          #
      done
  done
  echo

  # Now do a negated test.. This takes longer, because there are more values.
  echo "# Test 2 bytes for Invalid values: ie. values which are out of range"
  # =========================================================================
  # Note: 'iconv' will treat a leading  \x00-\x7F as a valid leading single,
  #   so this negated test primes the first UTF-8 byte with values starting at \x80
  time \
  for d1 in {128..193} {224..255} ;do 
 #for d1 in {128..194} {224..255} ;do # force a valid UTF-8 (needs $B2b2) 
      B2b1=$(printf "%0.2X" $d1)
      #
      for d2 in {0..127} {192..255} ;do
     #for d2 in {0..128} {192..255} ;do # force a valid UTF-8 (needs $B2b1)
          B2b2=$(printf "%0.2X" $d2)
          #
          echo -n "${B2b1}${B2b2}" |
            xxd -p -u -r |
              iconv -f UTF-8 2>/dev/null && { 
                echo "ERROR: VALID UTF-8 found: ${B2b1}${B2b2}"; exit 21; }
          #
      done
  done
  echo
fi

#########################################################################

#  3 Byte  UTF-8 values:  Verifying that I've got the right range values.
if ((B3==1)) ; then  
  echo "# Test 3 bytes for Valid UTF-8 values: ie. values which are in range"
  # ========================================================================
  time \
  for d1 in {224..239} ;do
      #     bin       oct  hex  dec
      # lo  11100000  340   E0  224
      # hi  11101111  357   EF  239
      B3b1=$(printf "%0.2X" $d1)
      #
      if   [[ $B3b1 == "E0" ]] ; then
          B3b2range="$(echo {160..191})"
          #     bin       oct  hex  dec  
          # lo  10100000  240   A0  160  
          # hi  10111111  277   BF  191
      elif [[ $B3b1 == "ED" ]] ; then
          B3b2range="$(echo {128..159})"
          #     bin       oct  hex  dec  
          # lo  10000000  200   80  128  
          # hi  10011111  237   9F  159
      else
          B3b2range="$(echo {128..191})"
          #     bin       oct  hex  dec
          # lo  10000000  200   80  128
          # hi  10111111  277   BF  191
      fi
      # 
      for d2 in $B3b2range ;do
          B3b2=$(printf "%0.2X" $d2)
          echo "${B3b1} ${B3b2} xx"
          #
          for d3 in {128..191} ;do
              #     bin       oct  hex  dec
              # lo  10000000  200   80  128
              # hi  10111111  277   BF  191
              B3b3=$(printf "%0.2X" $d3)
              #
              echo -n "${B3b1}${B3b2}${B3b3}" |
                xxd -p -u -r  |
                  iconv -f UTF-8 >/dev/null || { 
                    echo "ERROR: Invalid UTF-8 found: ${B3b1}${B3b2}${B3b3}"; exit 30; }
              #
          done
      done
  done
  echo

  # Now do a negated test.. This takes longer, because there are more values.
  echo "# Test 3 bytes for Invalid values: ie. values which are out of range"
  # =========================================================================
  # Note: 'iconv' will treat a leading  \x00-\x7F as a valid leading single,
  #   so this negated test primes the first UTF-8 byte with values starting at \x80
  #
  # real     26m28.462s \ 
  # user     27m12.526s  | stepping by 2
  # sys      13m11.193s /
  #
  # real    239m00.836s \
  # user    225m11.108s  | stepping by 1
  # sys     120m00.538s /
  #
  time \
  for d1 in {128..223..1} {240..255..1} ;do 
 #for d1 in {128..224..1} {239..255..1} ;do # force a valid UTF-8 (needs $B2b2,$B3b3) 
      B3b1=$(printf "%0.2X" $d1)
      #
      if   [[ $B3b1 == "E0" ]] ; then
          B3b2range="$(echo {0..159..1} {192..255..1})"
         #B3b2range="$(> {192..255..1})" # force a valid UTF-8 (needs $B3b1,$B3b3)
      elif [[ $B3b1 == "ED" ]] ; then
          B3b2range="$(echo {0..127..1} {160..255..1})"
         #B3b2range="$(echo {0..128..1} {160..255..1})" # force a valid UTF-8 (needs $B3b1,$B3b3)
      else
          B3b2range="$(echo {0..127..1} {192..255..1})"
         #B3b2range="$(echo {0..128..1} {192..255..1})" # force a valid UTF-8 (needs $B3b1,$B3b3)
      fi
      for d2 in $B3b2range ;do
          B3b2=$(printf "%0.2X" $d2)
          echo "${B3b1} ${B3b2} xx"
          #
          for d3 in {0..127..1} {192..255..1} ;do
         #for d3 in {0..128..1} {192..255..1} ;do # force a valid UTF-8 (needs $B2b1)
              B3b3=$(printf "%0.2X" $d3)
              #
              echo -n "${B3b1}${B3b2}${B3b3}" |
                xxd -p -u -r |
                  iconv -f UTF-8 2>/dev/null && { 
                    echo "ERROR: VALID UTF-8 found: ${B3b1}${B3b2}${B3b3}"; exit 31; }
              #
          done
      done
  done
  echo

fi

#########################################################################

#  Brute force testing in the Astral Plane will take a VERY LONG time..
#  Perhaps selective testing is more appropriate, now that the previous tests 
#     have panned out okay... 
#  
#  4 Byte  UTF-8 values:
if ((B4==1)) ; then  
  echo "# Test 4 bytes for Valid UTF-8 values: ie. values which are in range"
  # ==================================================================
  # real    58m18.531s \
  # user    56m44.317s  | 
  # sys     27m29.867s /
  time \
  for d1 in {240..244} ;do
      #     bin       oct  hex  dec
      # lo  11110000  360   F0  240
      # hi  11110100  364   F4  244  -- F4 encodes some values greater than 0x10FFFF;
      #                                    such a sequence is invalid.
      B4b1=$(printf "%0.2X" $d1)
      #
      if   [[ $B4b1 == "F0" ]] ; then
        B4b2range="$(echo {144..191})" ## f0 90 80 80  to  f0 bf bf bf
        #     bin       oct  hex  dec          010000  --  03FFFF 
        # lo  10010000  220   90  144  
        # hi  10111111  277   BF  191
        #                            
      elif [[ $B4b1 == "F4" ]] ; then
        B4b2range="$(echo {128..143})" ## f4 80 80 80  to  f4 8f bf bf
        #     bin       oct  hex  dec          100000  --  10FFFF 
        # lo  10000000  200   80  128  
        # hi  10001111  217   8F  143  -- F4 encodes some values greater than 0x10FFFF;
        #                                    such a sequence is invalid.
      else
        B4b2range="$(echo {128..191})" ## fx 80 80 80  to  f3 bf bf bf
        #     bin       oct  hex  dec          0C0000  --  0FFFFF
        # lo  10000000  200   80  128          0A0000
        # hi  10111111  277   BF  191
      fi
      #
      for d2 in $B4b2range ;do
          B4b2=$(printf "%0.2X" $d2)
          #
          for d3 in {128..191} ;do
              #     bin       oct  hex  dec
              # lo  10000000  200   80  128
              # hi  10111111  277   BF  191
              B4b3=$(printf "%0.2X" $d3)
              echo "${B4b1} ${B4b2} ${B4b3} xx"
              #
              for d4 in {128..191} ;do
                  #     bin       oct  hex  dec
                  # lo  10000000  200   80  128
                  # hi  10111111  277   BF  191
                  B4b4=$(printf "%0.2X" $d4)
                  #
                  echo -n "${B4b1}${B4b2}${B4b3}${B4b4}" |
                    xxd -p -u -r  |
                      iconv -f UTF-8 >/dev/null || { 
                        echo "ERROR: Invalid UTF-8 found: ${B4b1}${B4b2}${B4b3}${B4b4}"; exit 40; }
                  #
              done
          done
      done
  done
  echo "# Test 4 bytes for Valid UTF-8 values: END"
  echo
fi

########################################################################
# There is no test (yet) for negated range values in the astral plane. #  
#                           (all negated range values must be invalid) #
#  I won't bother; This was mainly for me to ge the general feel of    #     
#   the tests, and the final test below should flush anything out..    #
# Traversing the intire UTF-8 range takes quite a while...             #
#   so no need to do it twice (albeit in a slightly different manner)  #
########################################################################

################################
### The construction of:    ####
###  The Regular Expression ####
###      (de-construction?) ####
################################

#     BYTE 1                BYTE 2       BYTE 3      BYTE 4 
# 1: [\x00-\x7F]
#    ===========
#    ([\x00-\x7F])
#
# 2: [\xC2-\xDF]           [\x80-\xBF]
#    =================================
#    ([\xC2-\xDF][\x80-\xBF])
# 
# 3: [\xE0]                [\xA0-\xBF]  [\x80-\xBF]   
#    [\xED]                [\x80-\x9F]  [\x80-\xBF]
#    [\xE1-\xEC\xEE-\xEF]  [\x80-\xBF]  [\x80-\xBF]
#    ==============================================
#    ((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF]))
#
# 4  [\xF0]                [\x90-\xBF]  [\x80-\xBF]  [\x80-\xBF]    
#    [\xF1-\xF3]           [\x80-\xBF]  [\x80-\xBF]  [\x80-\xBF]
#    [\xF4]                [\x80-\x8F]  [\x80-\xBF]  [\x80-\xBF]
#    ===========================================================
#    ((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2}))
#
# The final regex
# ===============
# 1-4:  (([\x00-\x7F])|([\xC2-\xDF][\x80-\xBF])|((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF]))|((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2})))
# 4-1:  (((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2}))|((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF]))|([\xC2-\xDF][\x80-\xBF])|([\x00-\x7F]))


#######################################################################
#  The final Test; for a single character (multi chars to follow)     #  
#   Compare the return code of 'iconv' against the 'regex'            #
#   for the full range of 0x000000 to 0x10FFFF                        #
#                                                                     #     
#  Note; this script has 3 modes:                                     #
#        Run this test TWICE, set each mode Manually!                 #     
#                                                                     #     
#     1. Sequentially test every value from 0x000000 to 0x10FFFF      #     
#     2. Throw a spanner into the works! Force random byte patterns   #     
#     2. Throw a spanner into the works! Force random longer strings  #     
#        ==============================                               #     
#                                                                     #     
#  Note: The purpose of this routine is to determine if there is any  #
#        difference how 'iconv' and 'regex' handle the same data      #  
#                                                                     #     
#######################################################################
if ((Rx==1)) ; then
  # real    191m34.826s
  # user    158m24.114s
  # sys      83m10.676s
  time { 
  invalCt=0
  validCt=0
   failCt=0
  decBeg=$((0x00110000)) # incement by decimal integer
  decMax=$((0x7FFFFFFF)) # incement by decimal integer
  # 
  for ((CPDec=decBeg;CPDec<=decMax;CPDec+=13247)) ;do
      ((D==1)) && echo "=========================================================="
      #
      # Convert decimal integer '$CPDec' to Hex-digits; 6-long  (dec2hex)
      hexUTF32BE=$(printf '%0.8X\n' $CPDec)  # hexUTF32BE

      # progress count  
      if (((CPDec%$((0x1000)))==0)) ;then
          ((Test>2)) && echo
          echo "$hexUTF32BE  Test=$Test mode=${mode[$modebits]}            "
      fi
      if   ((Test==1 || Test==2 ))
      then # Test 1. Sequentially test every value from 0x000000 to 0x10FFFF
          #
          if   ((Test==2)) ; then
              bits=32
              UTF8="$( perl -C -e 'print chr 0x'$hexUTF32BE |
                perl -l -ne '/^(  [\000-\177]
                                | [\300-\337][\200-\277]
                                | [\340-\357][\200-\277]{2}
                                | [\360-\367][\200-\277]{3}
                                | [\370-\373][\200-\277]{4}
                                | [\374-\375][\200-\277]{5}
                               )*$/x and print' |xxd -p )"
              UTF8="${UTF8%0a}"
              [[ -n "$UTF8" ]] \
                    && rcIco32=0 || rcIco32=1
                       rcIco16=

          elif ((modebits==strict && CPDec<=$((0xFFFF)))) ;then
              bits=16
              UTF8="$( echo -n "${hexUTF32BE:4}" |
                xxd -p -u -r |
                  iconv -f UTF-16BE -t UTF-8 2>/dev/null)" \
                    && rcIco16=0 || rcIco16=1  
                       rcIco32=
          else
              bits=32
              UTF8="$( echo -n "$hexUTF32BE" |
                xxd -p -u -r |
                  iconv -f UTF-32BE -t UTF-8 2>/dev/null)" \
                    && rcIco32=0 || rcIco32=1
                       rcIco16=
          fi
          # echo "1 mode=${mode[$modebits]}-$bits  rcIconv: (${rcIco16},${rcIco32})  $hexUTF32BE "
          #
          #
          #
          if ((${rcIco16}${rcIco32}!=0)) ;then
              # 'iconv -f UTF-16BE' failed produce a reliable UTF-8
              if ((bits==16)) ;then
                  ((D==1)) &&           echo "bits-$bits rcIconv: error    $hexUTF32BE .. 'strict' failed, now trying 'lax'"
                  #  iconv failed to create a  'srict' UTF-8 so   
                  #      try UTF-32BE to get a   'lax' UTF-8 pattern    
                  UTF8="$( echo -n "$hexUTF32BE" |
                    xxd -p -u -r |
                      iconv -f UTF-32BE -t UTF-8 2>/dev/null)" \
                        && rcIco32=0 || rcIco32=1
                  #echo "2 mode=${mode[$modebits]}-$bits  rcIconv: (${rcIco16},${rcIco32})  $hexUTF32BE "
                  if ((rcIco32!=0)) ;then
                      ((D==1)) &&               echo -n "bits-$bits rcIconv: Cannot gen UTF-8 for: $hexUTF32BE"
                      rcIco32=1
                  fi
              fi
          fi
          # echo "3 mode=${mode[$modebits]}-$bits  rcIconv: (${rcIco16},${rcIco32})  $hexUTF32BE "
          #
          #
          #
          if ((rcIco16==0 || rcIco32==0)) ;then
              # 'strict(16)' OR 'lax(32)'... 'iconv' managed to generate a UTF-8 pattern  
                  ((D==1)) &&       echo -n "bits-$bits rcIconv: pattern* $hexUTF32BE"
                  ((D==1)) &&       if [[ $bits == "16" && $rcIco32 == "0" ]] ;then 
                  echo " .. 'lax' UTF-8 produced a pattern"
              else
                  echo
              fi
               # regex test
              if ((modebits==strict)) ;then
                 #rxOut="$(echo -n "$UTF8" |perl -l -ne '/^(([\x00-\x7F])|([\xC2-\xDF][\x80-\xBF])|((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF]))|((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2})))*$/ or print' )"
                                     rxOut="$(echo -n "$UTF8" |
                  perl -l -ne '/^( ([\x00-\x7F])             # 1-byte pattern
                                  |([\xC2-\xDF][\x80-\xBF])  # 2-byte pattern
                                  |((([\xE0][\xA0-\xBF])|([\xED][\x80-\x9F])|([\xE1-\xEC\xEE-\xEF][\x80-\xBF]))([\x80-\xBF]))  # 3-byte pattern
                                  |((([\xF0][\x90-\xBF])|([\xF1-\xF3][\x80-\xBF])|([\xF4][\x80-\x8F]))([\x80-\xBF]{2}))        # 4-byte pattern
                                 )*$ /x or print' )"
               else
                  if ((Test==2)) ;then
                      rx="$(echo -n "$UTF8" |perl -l -ne '/^([\000-\177]|[\300-\337][\200-\277]|[\340-\357][\200-\277]{2}|[\360-\367][\200-\277]{3}|[\370-\373][\200-\277]{4}|[\374-\375][\200-\277]{5})*$/ and print')"
                      [[ "$UTF8" != "$rx" ]] && rxOut="$UTF8" || rxOut=
                      rx="$(echo -n "$rx" |sed -e "s/\(..\)/\1 /g")"  
                  else 
                      rxOut="$(echo -n "$UTF8" |perl -l -ne '/^([\000-\177]|[\300-\337][\200-\277]|[\340-\357][\200-\277]{2}|[\360-\367][\200-\277]{3}|[\370-\373][\200-\277]{4}|[\374-\375][\200-\277]{5})*$/ or print' )"
                  fi
              fi
              if [[ "$rxOut" == "" ]] ;then
                ((D==1)) &&           echo "        rcRegex: ok"
                  rcRegex=0
              else
                  ((D==1)) &&           echo -n "bits-$bits rcRegex: error    $hexUTF32BE .. 'strict' failed,"
                  ((D==1)) &&           if [[  "12" == *$Test* ]] ;then 
                                            echo # "  (codepoint) Test $Test" 
                                        else
                                            echo
                                        fi
                  rcRegex=1
              fi
          fi
          #
      elif [[ $Test == 2 ]]
      then # Test 2. Throw a randomizing spanner into the works! 
          #          Then test the  arbitary bytes ASIS
          #
          hexLineRand="$(echo -n "$hexUTF32BE" |
            sed -re "s/(.)(.)(.)(.)(.)(.)(.)(.)/\1\n\2\n\3\n\4\n\5\n\6\n\7\n\8/" |
              sort -R |
                tr -d '\n')"
          # 
      elif [[ $Test == 3 ]]
      then # Test 3. Test single UTF-16BE bytes in the range 0x00000000 to 0x7FFFFFFF
          #
          echo "Test 3 is not properly implemented yet.. Exiting"
          exit 99 
      else
          echo "ERROR: Invalid mode"
          exit
      fi
      #
      #
      if ((Test==1 || Test=2)) ;then
          if ((modebits==strict && CPDec<=$((0xFFFF)))) ;then
              ((rcIconv=rcIco16))
          else
              ((rcIconv=rcIco32))
          fi
          if ((rcRegex!=rcIconv)) ;then
              [[ $Test != 1 ]] && echo
              if ((rcRegex==1)) ;then
                  echo "ERROR: 'regex' ok, but NOT 'iconv': ${hexUTF32BE} "
              else
                  echo "ERROR: 'iconv' ok, but NOT 'regex': ${hexUTF32BE} "
              fi
              ((failCt++));
          elif ((rcRegex!=0)) ;then
            # ((invalCt++)); echo -ne "$hexUTF32BE  exit-codes $${rcIco16}${rcIco32}=,$rcRegex\t: $(printf "%0.8X\n" $invalCt)\t$hexLine$(printf "%$(((mode3whi*2)-${#hexLine}))s")\r"
              ((invalCt++)) 
          else
              ((validCt++)) 
          fi
          if   ((Test==1)) ;then
              echo -ne "$hexUTF32BE "    "mode=${mode[$modebits]}  test-return=($rcIconv,$rcRegex)   valid,invalid,fail=($(printf "%X" $validCt),$(printf "%X" $invalCt),$(printf "%X" $failCt))          \r"
          else 
              echo -ne "$hexUTF32BE $rx mode=${mode[$modebits]} test-return=($rcIconv,$rcRegex)  val,inval,fail=($(printf "%X" $validCt),$(printf "%X" $invalCt),$(printf "%X" $failCt))\r"
          fi
      fi
  done
  } # End time
fi
exit
Peter.O
źródło
Główny problem z moim wyrażeniem regularnym polega na tym, że dopuszcza niektóre zabronione sekwencje, takie jak \300\200(naprawdę źle: to kod 0 nie jest wyrażony bajtem zerowym!). Myślę, że wyrażenie regularne odrzuca je poprawnie.
Gilles „SO- przestań być zły”
7

Uważam uconv(w icu-devtoolspakiecie w Debianie) za przydatne do sprawdzania danych UTF-8:

$ print '\\xE9 \xe9 \u20ac \ud800\udc00 \U110000' |
    uconv --callback escape-c -t us
\xE9 \xE9 \u20ac \xED\xA0\x80\xED\xB0\x80 \xF4\x90\x80\x80

( \xPomoc w wykrywaniu nieprawidłowych znaków (z wyjątkiem fałszywie dodatnich dobrowolnie wprowadzonych literałem \xE9powyżej)).

(wiele innych miłych zastosowań).

Stéphane Chazelas
źródło
Myślę, że recodemożna go używać podobnie - z tym wyjątkiem, że powinien się nie powieść, jeśli zostanie poproszony o przetłumaczenie nieprawidłowej sekwencji wielobajtowej. Nie jestem jednak pewien; nie zawiedzie na print...|recode u8..u8/x4przykład (co właśnie robi hexdump jak ty powyżej) , ponieważ nic nie robi, ale iconv data data, ale nie zawieść jak recode u8..u2..u8/x4, ponieważ przekłada się następnie drukuje. Ale nie wiem o tym wystarczająco dużo - i jest wiele możliwości.
mikeserv,
Jeśli mam plik, powiedzmy test.txt. Jak powinienem znaleźć nieprawidłowy znak za pomocą twojego rozwiązania? Co oznacza ustwój kod?
jdhao
@Hao, usoznacza Stany Zjednoczone, czyli skrót ASCII. Konwertuje dane wejściowe na ASCII, w których znaki spoza ASCII są konwertowane na \uXXXXnotację, a znaki niebędące znakami na \xXX.
Stéphane Chazelas,
Gdzie powinienem umieścić mój plik, aby użyć twojego skryptu? Czy ostatni wiersz w bloku kodu jest wyjściem twojego kodu? To jest dla mnie trochę mylące.
jdhao
4

Python ma wbudowaną unicodefunkcję od wersji 2.0.

#!/usr/bin/env python2
import sys
for line in sys.stdin:
    try:
        unicode(line, 'utf-8')
    except UnicodeDecodeError:
        sys.stdout.write(line)

W Pythonie 3 unicodezostał złożony str. Należy przekazać obiekt podobny do bajtu , tutaj podstawowe bufferobiekty dla standardowych deskryptorów .

#!/usr/bin/env python3
import sys
for line in sys.stdin.buffer:
    try:
        str(line, 'utf-8')
    except UnicodeDecodeError:
        sys.stdout.buffer.write(line)
Gilles „SO- przestań być zły”
źródło
Ten python 2nie oznacza oflagowania znaków niebędących znakami UTF-16 zakodowanych w UTF-8 (przynajmniej w wersji 2.7.6).
Stéphane Chazelas,
@ StéphaneChazelas Dammit. Dzięki. Do tej pory przeprowadzałem tylko testy nominalne, później przeprowadzę testową baterię Petera.
Gilles „SO- przestań być zły”,
1

Natknąłem się na podobny problem (szczegółowo w sekcji „Kontekst”) i przybyłem z następującym rozwiązaniem ftfy_line_by_line.py :

#!/usr/bin/env python3
import ftfy, sys
with open(sys.argv[1], mode='rt', encoding='utf8', errors='replace') as f:
  for line in f:
    sys.stdout.buffer.write(ftfy.fix_text(line).encode('utf8', 'replace'))
    #print(ftfy.fix_text(line).rstrip().decode(encoding="utf-8", errors="replace"))

Używanie kodowania + zamień + ftfy, aby automatycznie naprawić Mojibake i inne poprawki.

Kontekst

Zebrałem> 10GiB CSV podstawowych metadanych systemu plików, używając następującego skryptu gen_basic_files_metadata.csv.sh , działającego zasadniczo:

find "${path}" -type f -exec stat --format="%i,%Y,%s,${hostname},%m,%n" "{}" \;

Problemem było z niezgodnego kodowania nazwami całym systemie plików, powodując UnicodeDecodeErrorpodczas dalszego przetwarzania w aplikacjach pytona ( csvsql się bardziej specyficzne).

Dlatego zastosowałem powyżej skryptu ftfy i trzeba było

Pamiętaj, że ftfy działa dość wolno, przetwarzanie tych> 10GiB zajęło:

real    147m35.182s
user    146m14.329s
sys     2m8.713s

podczas gdy sha256sum dla porównania:

real    6m28.897s
user    1m9.273s
sys     0m6.210s

na procesorze Intel (R) Core i7-3520M @ 2.90GHz + 16GiB RAM (i dane na dysku zewnętrznym)

Grzegorz Wierzowiecki
źródło
I tak, wiem, że to polecenie find nie koduje poprawnie nazw plików zawierających cudzysłowy zgodnie ze standardem csv
Grzegorz Wierzowiecki