84

I need to detect corrupted text file where there are invalid (non-ASCII) utf-8, Unicode or binary characters.

�>t�ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½w�ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½o��������ï¿ï¿½_��������������������o����������������������￿����ß����������ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½~�ï¿ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½}���������}w��׿��������������������������������������ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½~������������������������������������_������������������������������������������������������������������������������^����ï¿ï¿½s�����������������������������?�������������ï¿ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½w�������������ï¿ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½}����������ï¿ï¿½ï¿½ï¿½ï¿½y����������������ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½ï¿½o�������������������������}��

what I have tried:

iconv -f utf-8 -t utf-8 -c file.csv 

this converts a file from utf-8 encoding to utf-8 encoding and -c is for skipping invalid utf-8 characters. However at the end those illegal characters still got printed. Are there any other solutions in bash on linux or other languages?

Mohamed Ali JAMAOUI
  • 14,275
  • 14
  • 73
  • 117
user121196
  • 30,032
  • 57
  • 148
  • 198
  • 9
    If the characters you see in the file are the same you see on this web page, you cannot use iconv: they actually are valid utf-8 characters. The command line you pasted actually removes invalid utf-8 codes: what I can suppose, based on the repetition of the same pattern, is that you may have an UTF-8 read as ASCII and output as UTF-8 again, resulting in some garbled double encoding (or it may just be binary that happens to be valid utf-8, albeit unreadable). – Alex Mazzariol Apr 15 '15 at 07:55
  • Are you trying to find out whether or not a file has corrupted characters (detecting), or are you also trying to remove the characters (deleting)? – ASCIIThenANSI Apr 15 '15 at 17:49
  • @ASCIIThenANSI: I'm trying to detect if a file has corrupted characters. I'm also interested in deleting them. – user121196 Apr 16 '15 at 01:11
  • 7
    "invalid (non-ASCII) utf-8" doesn't make any sense. All Unicode code points (more than 100,000 of them) other than the first 128 can be encoded in **valid** UTF-8, and they are all non-ASCII. You have to specify the range of your desired code points, or just say ASCII. – 4ae1e1 Apr 16 '15 at 07:54
  • @4ae1e1 I assumed he meant non-ASCII characters, I looked, and I don't see any invalid characters. Everything looks like Unicode or UTF-8 characters, and I can't tell where 'binary' characters are. – ASCIIThenANSI Apr 16 '15 at 12:56
  • Before you award the bounty, perhaps you could still clarify what you mean by "invalid (non-ASCII) utf-8, Unicode or binary characters"? Do you mean anything which is not ASCII (and so, technically, also not UTF-8, in the sense that the allowed characters are all identical to ASCII and don't need encoding) or anything which is not valid UTF-8 (which makes sense, but doesn't match your example), or something else? – tripleee Apr 22 '15 at 04:49

10 Answers10

169

Assuming you have your locale set to UTF-8 (see locale output), this works well to recognize invalid UTF-8 sequences:

grep -axv '.*' file.txt

Explanation (from grep man page):

  • -a, --text: treats file as text, essential prevents grep to abort once finding an invalid byte sequence (not being utf8)
  • -v, --invert-match: inverts the output showing lines not matched
  • -x '.*' (--line-regexp): means to match a complete line consisting of any utf8 character.

Hence, there will be output, which is the lines containing the invalid not utf8 byte sequence containing lines (since inverted -v)

Pablo Bianchi
  • 1,824
  • 1
  • 26
  • 30
Blaf
  • 2,050
  • 1
  • 11
  • 14
  • 26
    this code helped me. Some short discritption about the used options would have helped me more directly. Used options here: `-a` treats file as text, essential prevents `grep` to abort once finding an invalid byte sequence (not being utf8), `-v` inverts the output showing lines not matched, finally `-x '.*'` means to match a complete line consisting of any utf8 character. Hence there will be output, which is the lines containing the invalid not utf8 byte sequence containing lines (since inverted `-v`). – humanityANDpeace Mar 24 '17 at 11:36
  • 3
    How do I tell if my locale is set to UTF-8? – Russell Silva Apr 18 '17 at 23:53
  • 4
    Check output of `locale`. – Blaf Apr 19 '17 at 08:31
  • Thanks! That saved me a bit of a headache. +1 for "-a" parameter! – GTodorov May 21 '20 at 08:28
  • 2
    Which part of the `locale` output is used by `grep` to determine encoding? Could you suggest a command like `CORRECT_VAR_NAME=en_US.UTF-8 grep -axv '.*' file.txt`? – Chris L. Barnes Jan 09 '21 at 19:50
  • 1
    @ChrisL.Barnes I typically don't care about that variable and always use `LC_ALL=en_US.UTF-8 grep -axv '.*'` for simplicity – phuclv Oct 28 '22 at 03:11
  • You can also add the `-n` parameter so `grep` tells you the line number, where it found the invalid byte. – Peter A Aug 14 '23 at 09:32
22

I would grep for non ASCII characters.

With GNU grep with pcre (due to -P, not available always. On FreeBSD you can use pcregrep in package pcre2) you can do:

grep -P "[\x80-\xFF]" file

Reference in How Do I grep For all non-ASCII Characters in UNIX. So, in fact, if you only want to check whether the file contains non ASCII characters, you can just say:

if grep -qP "[\x80-\xFF]" file ; then echo "file contains ascii"; fi
#        ^
#        silent grep

To remove these characters, you can use:

sed -i.bak 's/[\d128-\d255]//g' file

This will create a file.bak file as backup, whereas the original file will have its non ASCII characters removed. Reference in Remove non-ascii characters from csv.

Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • The sed command has a couple of issues: For many versions of sed (e.g. CentOS 7, sed 4.2.2), putting the notation \dNNN does not work inside the brackets. Usable one-liners are the perl example from http://stackoverflow.com/questions/3337936/remove-non-ascii-characters-from-csv/3337960#3337960 or the tr from http://stackoverflow.com/questions/15034944/trying-to-delete-non-ascii-characters-only . Also, the search and replace is missing the "g" at the end; as expressed (and if it worked) it would only get the first non-ascii character per "line" in the file. – Mike S Apr 21 '15 at 17:09
  • `-P` doesn't work in FreeBSD. Users beware they may need GNU Grep. – Janus Troelsen Jun 24 '17 at 15:36
  • 1
    Actually FreeBSD uses gnu grep too , just not with pcre. So I clarified the edit – Janus Troelsen Jun 26 '17 at 11:55
  • 9
    At the risk of restating the already obvious, there are millions of *valid* UTF-8 sequences which are not ASCII. Removing those is clearly the wrong solution. – tripleee Dec 23 '17 at 19:15
  • @tripleee I totally agree and I am shocked about the numerous upvotes ignoring this -- not so small -- "feature" of this answer ... doesn't even caution about it. – 0xC0000022L Feb 09 '23 at 15:38
17

Try this, in order to find non-ASCII characters from the shell.

Command:

$ perl -ne 'print "$. $_" if m/[\x80-\xFF]/'  utf8.txt

Output:

2 Pour être ou ne pas être
4 Byť či nebyť
5 是或不
Bouramas
  • 1,077
  • 1
  • 17
  • 36
11

What you are looking at is by definition corrupted. Apparently, you are displaying the file as it is rendered in Latin-1; the three characters � represent the three byte values 0xEF 0xBF 0xBD. But those are the UTF-8 encoding of the Unicode REPLACEMENT CHARACTER U+FFFD which is the result of attempting to convert bytes from an unknown or undefined encoding into UTF-8, and which would properly be displayed as � (if you have a browser from this century, you should see something like a black diamond with a question mark in it; but this also depends on the font you are using etc).

screen shot of the above text

So your question about "how to detect" this particular phenomenon is easy; the Unicode code point U+FFFD is a dead giveaway, and the only possible symptom from the process you are implying.

These are not "invalid Unicode" or "invalid UTF-8" in the sense that this is a valid UTF-8 sequence which encodes a valid Unicode code point; it's just that the semantics of this particular code point is "this is a replacement character for a character which could not be represented properly", i.e. invalid input.

As for how to prevent it in the first place, the answer is really simple, but also rather uninformative -- you need to identify when and how the incorrect encoding took place, and fix the process which produced this invalid output.

To just remove the U+FFFD characters, try something like

perl -CSD -pe 's/\x{FFFD}//g' file

but again, the proper solution is to not generate these erroneous outputs in the first place.

To actually answer the question about how to remove only invalid code points, try

iconv -f UTF-8 -t UTF-8//IGNORE broken-utf8.txt >fixed-utf8.txt

(You are not revealing the encoding of your example data. It is possible that it has an additional corruption. If what you are showing us is a copy/paste of the UTF-8 rendering of the data, it has been "double-encoded". In other words, somebody took -- already corrupted, as per the above -- UTF-8 text and told the computer to convert it from Latin-1 to UTF-8. Undoing that is easy; just convert it "back" to Latin-1. What you obtain should then be the original UTF-8 data before the superfluous incorrect conversion.

iconv -f utf-8 -t latin-1 mojibake-utf8.txt >fixed-utf8.txt

See also mojibake.)

tripleee
  • 175,061
  • 34
  • 275
  • 318
3

This Perl program should remove all non-ASCII characters:

 foreach $file (@ARGV) {
   open(IN, $file);
   open(OUT, "> super-temporary-utf8-replacement-file-which-should-never-be-used-EVER");
   while (<IN>) {
     s/[^[:ascii:]]//g;
     print OUT "$_";
   }
   rename "super-temporary-utf8-replacement-file-which-should-never-be-used-EVER", $file;
}

What this does is it takes files as input on the command-line, like so:
perl fixutf8.pl foo bar baz
Then, for each line, it replaces each instance of a non-ASCII character with nothing (deletion).
It then writes this modified line out to super-temporary-utf8-replacement-file-which-should-never-be-used-EVER (named so it dosen't modify any other files.)
Afterwards, it renames the temporary file to that of the original one.

This accepts ALL ASCII characters (including DEL, NUL, CR, etc.), in case you have some special use for them. If you want only printable characters, simply replace :ascii: with :print: in s///.

I hope this helps! Please let me know if this wasn't what you were looking for.

ASCIIThenANSI
  • 865
  • 1
  • 9
  • 27
3

The following C program detects invalid utf8 characters. It was tested and used on a linux system.

/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <stdio.h>
#include <stdlib.h>

void usage( void ) {
    printf( "Usage: test_utf8 file ...\n" );

    return;
}

int line_number = 1;
int char_number = 1;
char *file_name = NULL;

void inv_char( void ) {
    printf( "%s: line : %d - char %d\n", file_name, line_number, char_number );

    return;
}

int main( int argc, char *argv[]) {

    FILE *out = NULL;
    FILE *fh = NULL;

//    printf( "argc: %d\n", argc );

    if( argc < 2 ) {
        usage();
        exit( 1 );
    }

//    printf( "File: %s\n", argv[1] );

    file_name = argv[1];

    fh = fopen( file_name, "rb" );
    if( ! fh ) {
        printf( "Could not open file '%s'\n", file_name );
        exit( 1 );
    }

    int utf8_type = 1;
    int utf8_1 = 0;
    int utf8_2 = 0;
    int utf8_3 = 0;
    int utf8_4 = 0;
    int byte_count = 0;
    int expected_byte_count = 0;

    int cin = fgetc( fh );
    while( ! feof( fh ) ) {
        switch( utf8_type ) {
            case 1:
                if( (cin & 0x80) ) {
                    if( (cin & 0xe0) == 0xc0 ) {
                        utf8_1 = cin;
                        utf8_type = 2;
                        byte_count = 1;
                        expected_byte_count = 2;
                        break;
                    }

                    if( (cin & 0xf0) == 0xe0 ) {
                        utf8_1 = cin;
                        utf8_type = 2;
                        byte_count = 1;
                        expected_byte_count = 3;
                        break;
                    }

                    if( (cin & 0xf8) == 0xf0 ) {
                        utf8_1 = cin;
                        utf8_type = 2;
                        byte_count = 1;
                        expected_byte_count = 4;
                        break;
                    }

                    inv_char();
                    utf8_type = 1;
                    break;
                }

                break;

            case 2:
            case 3:
            case 4:
//                printf( "utf8_type - %d\n", utf8_type );
//                printf( "%c - %02x\n", cin, cin );
                if( (cin & 0xc0) == 0x80 ) {
                    if( utf8_type == expected_byte_count ) {
                        utf8_type = 1;
                        break;
                    }

                    byte_count = utf8_type;
                    utf8_type++;

                    if( utf8_type == 5 ) {
                        utf8_type = 1;
                    }

                    break;
                }

                inv_char();
                utf8_type = 1;
                break;

            default:
                inv_char();
                utf8_type = 1;
                break;
        }

        if( cin == '\n' ) {
            line_number ++;
            char_number = 0;
        }

        if( out != NULL ) {
            fputc( cin, out );
        }

//        printf( "lno: %d\n", line_number );

        cin = fgetc( fh );
        char_number++;
    }

    fclose( fh );

    return 0;
}
tripleee
  • 175,061
  • 34
  • 275
  • 318
DBA
  • 31
  • 1
3

... I'm trying to detect if a file has corrupted characters. I'm also interested in deleting them.

This is easy with ugrep and takes just one line:

ugrep -q -e "." -N "\p{Unicode}" file.csv && echo "file is corrupted"

To remove invalid Unicode characters:

ugrep "\p{Unicode}" --format="%o" file.csv

The first command matches any character with -e "." except valid Unicode with -N "\p{Unicode}" that is a "negative pattern" to skip.

The second command matches a Unicode character "\p{Unicode}" and writes it with --format="%o".

Dr. Alex RE
  • 1,772
  • 1
  • 15
  • 23
  • 1
    For future reference, if someone needs to use this to search which files are corrupted in a folder(for example, .java files): `ugrep -e "." -N "\p{Unicode}" **/*.java` should print a list of files that match the expression(and are, therefore, corrupted) – Badashi Mar 31 '20 at 15:40
  • I just used *ugrep.exe* as provided for Windows users on their github page. To use a glob to identify files I had to use the *-g* switch – Bernhard Döbler Aug 28 '20 at 17:40
1

I am probably repeating what other have said already. But i think your invalid characters get still printed because they may be valid. The Universal Character Set is the attempt to reference the worldwide frequently used characters to be able to write robust software which is not relying on a special character-set.

So i think your problem may be one of the following both - in assumption that your overall target is to handle this (malicious) input from utf-files in general:

  1. There are invalid utf8 characters (better called invalid byte sequences - for this i'd like to refer to the corresponding Wikipedia-Article).
  2. There are absent equivalents in your current display-font which are substituted by a special symbol or shown as their binary ASCII-equivalent (f.e. - i therefore would like to refer to the following so-post: UTF-8 special characters don't show up).

So in my opinion you have two possible ways to handle this:

  1. Transform the all characters from utf8 into something handleable - f.e. ASCII - this can be done f.e. with iconv -f utf-8 -t ascii -o file_in_ascii.txt file_in_utf8.txt. But be careful transferring from one the wider character-space (utf) into a smaller one might cause a data loss.
  2. Handle utf(8) correctly - this is how the world is writing stuff. If you think you might have to rely on ASCII-chars because of any limitating post-processing step, stop and rethink. In most cases the post-processor already supports utf, it's probably better to find out how to utilize it. You're making your stuff future- and bullet-proof.

Handling utf might seem to be tricky, the following steps may help you to accomplish utf-readyness:

  • Be able to display utf correctly or ensure that your display-stack (os, terminal and so on) is able to display an adequate subset of unicode (which, of course, should meet your needs), this may prevent the need of a hex-editor in many cases. Unfortunately utf is too big, to come in one font, but a good point to start at is this so-post: https://stackoverflow.com/questions/586503/complete-monospaced-unicode-font
  • Be able to filter invalid byte sequences. And there are many ways to achieve that, this ul-post shows a plenty variety of these ways: Filtering invalid utf8 - i want to especially point out the 4th answer which suggests to use uconv which allows you to set a callback-handler for invalid sequences.
  • Read a bit more about unicode.
Community
  • 1
  • 1
Florian Neumann
  • 5,587
  • 1
  • 39
  • 48
0

A very dirty solution in python 3

import sys
with open ("cur.txt","r",encoding="utf-8") as f:
    for i in f:
            for c in i:
                 if(ord(c)<128):
                     print(c,end="")

The output should be:

>two_o~}}w~_^s?w}yo}
xitij
  • 117
  • 2
0

Using Ubuntu 22.04, I get more correct answer by using:

grep -axv -P '.*' file.txt

The original answer without the -P, seems to give false positives for a lot of asian characters, like:

    <lei:LegalName xml:lang="ko">피씨에이생명보험주식회사</lei:LegalName>
    <lei:LegalName xml:lang="ko">린드먼 부품소재 전문투자조합 1</lei:LegalName>
    <lei:LegalName xml:lang="ko">비엔피파리바 카디프손해보험 주식회사</lei:LegalName>

These characters do pass the scanning of the isutf8 utility.