Использование awk для обнаружения многобайтового символа UTF-8 - PullRequest
1 голос
/ 24 апреля 2020

Я использую awk (символическую ссылку на gawk на моем компьютере), чтобы прочитать файл и получить количество символов в строке, чтобы проверить, имеет ли файл фиксированную ширину. Затем я могу повторно использовать следующий скрипт с параметром -b --characters-as-bytes, чтобы увидеть, имеет ли файл фиксированную ширину в байтах.

#!/usr/bin/awk -f

BEGIN {
    width = -1;
}

{
    len = length($0);

    if (width == -1) {
        width = len;
    } else if (len != 0 && len != width) {
        exit 1;
    }
}

Я хочу сделать что-то похожее, чтобы проверить, имеет ли каждая строка в файле одинаковое количество байтов и символов, позволяющее предположить, что все символы являются одним байтом (я понимаю, что это ложные отрицания). Проблема в том, что я хотел бы один раз прогнать файл и вспомнить первое несоответствие. Есть ли способ установить параметр -b из скрипта awk, аналогичный тому, как вы можете настроить FS. Если это невозможно, я открыт для вариантов вне awk. Я всегда могу просто написать это в C, если нужно, но я хотел убедиться, что там уже ничего нет.

Эффективность - это то, к чему я стремлюсь. Наличие этой информации поможет мне пропустить дорогостоящий процесс, поэтому я не считаю, что это само по себе дорого. Я имею дело с файлами, длина которых может превышать 100 миллионов строк.

Уточнение

Я хочу что-то подобное выше. Примерно так

#!/usr/bin/awk -f
{
    if (length($0) != bytelength($0))
        exit 1;
}

Мне не нужен вывод. Я просто отключу код возврата ($? в bash). Так что выход 1, если это не удается. Очевидно, длина байта не является функцией. Я просто ищу способ достичь этого без запуска awk дважды.

UPDATE

Решение SunDep работает для того, что я описал выше:

awk -F '' -l ordchr '{for(i=1;i<=NF;i++) if(ord($i)<0) {exit 1;}}'

Я работал в предположении, что awk будет считать старший символ с однобайтовой кодировкой Windows выше 0x7F как один символ, но на самом деле он вообще не считается. Таким образом, длина байта все равно не будет равна длине. Я думаю, мне нужно будет написать это в C для чего-то, что специфицирует c.

Заключение

Так что я думаю, что плохо справился с объяснением своего проблема. Я получаю данные, которые закодированы в однобайтовом кодировании в стиле UTF-8 или Windows ', например, CP1252. Я хотел проверить, есть ли в файле многобайтовые символы, и выйти, если он найден. Сначала я хотел сделать это в awk, но играть с файлами, которые могут иметь другую кодировку, оказалось сложно.

В двух словах, если принять файл с одним символом:

CHARACTER  FILE_ENCODING     ALL_SINGLE_BYTE   IN_HEX
á          UTF-8             false             0xC3 0xA1
á          CP1252            true              0xE1
a          ANY               true              0x61

Ответы [ 3 ]

2 голосов
/ 24 апреля 2020

Вы, похоже, нацелены на UTF-8 специально. Действительно, первый многобайтовый символ в кодировке UTF-8 начинается с 0b11xxxxxx, а следующий байт должен быть 0b10xxxxxx, где x представляет любое значение (из wikipedia ).

Так что вы можете обнаружить такую ​​последовательность с помощью sed, сопоставив шестнадцатеричные диапазоны, и выйти с ненулевым статусом выхода, если найдено:

LC_ALL=C sed -n '/[\xC0-\xFF][\x80-\xBF]/q1'

Ie. совпадают байты в диапазонах [0b11000000-0b11111111][0b10000000-0b10111111].

Я думаю, \x?? и q оба являются расширениями GNU для sed.

1 голос
/ 24 апреля 2020

Лучший ответ - imho, на самом деле ответ с grep, предоставленный Sundeep в комментарии. Вы должны попытаться заставить это работать. Ответ ниже использует sed аналогичным образом. Я, вероятно, удалю его, так как он действительно ничего не добавляет к решению grep.

Как насчет этого?

[[ -z "$(LANG=C sed -z '/[\x80-\xFF]/d' <(echo -e 'one\ntwo\nth⌫ree'))" ]]
echo $?
  • <(echo -e 'one\ntwo\nth⌫ree') это просто файл примера с многобайтовым символом
  • вся команда sed выполняет одно из двух действий:
    • выводит пустую строку, если файл содержит многобайтовый символ
    • выводит полный текст файл, если он не
  • [[ -z string ]] возвращает 0 или 1, если длина строки равна нулю.
1 голос
/ 24 апреля 2020

Примечание : код в этом ответе может использоваться для обнаружения допустимых многобайтовых символов UTF-8. Также произойдет сбой, если есть недопустимые последовательности байтов UTF-8. Тем не менее, не гарантирует, что ваш файл предназначен для UTF-8. Весь действительный код UTF-8 также является допустимым CP1252, но не весь CP1252 является действительным UTF-8.

Так что, похоже, это может быть проблемой ниши. Для меня это означает, что время прибегнуть к C. Это должно сработать, но, в духе вопроса, я не приму его, если кто-то может придумать решение awk.

Вот мое решение C, которое я назвал hasmultibyte :

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

void check_for_multibyte(FILE* in) 
{
        int c = 0;
        while ((c = getc(in)) != EOF) {
                /* Floating continuation byte */
                if ((c & 0xC0) == 0x80)
                        exit(5);

                /* utf8 multi-byte start */
                if ((c & 0xC0) == 0xC0) {
                        int continuations = 1;
                        switch (c & 0xF0) {
                        case 0xF0:
                                continuations = 3;
                                break;
                        case 0xE0:
                                continuations = 2;
                        }   
                        int i = 0;
                        for (; i < continuations; ++i)
                                if ((getc(in) & 0xC0) != 0x80)
                                        exit(5);

                        exit(0);
                }   
        }   
}

int main (int argc, char** argv)
{
        FILE* in = stdin;
        int i = 1;
        do {
                if (i != argc) {
                        in = fopen(argv[i], "r");
                        if (!in) {
                                perror(argv[i]);
                                exit(EXIT_FAILURE);
                        }   
                }   

                check_for_multibyte(in);

                if (in != stdin)
                        fclose(in);
        } while (++i < argc);

        return 5;
}

В оболочке вы можете использовать его следующим образом:

if hasmultibyte file.txt; then
    ...
fi

Он также будет считывать из stdin, если файл не указан, если вы хотите использовать его на конец конвейера:

if cat file.txt | hasmultibyte; then
    ...
fi

TEST

Вот тест программы. Я создал 3 файла с именем Hernández:

name_ascii.txt  - Uses a instead of á.
name_cp1252.txt - Encoded in CP1252
name_utf-8.txt  - Encoded in UTF-8 (default)

Вы видите is из-за неверного UTF-8, который ожидает терминал. На самом деле это символ á в CP1252.

> file name_*
name_ascii.txt:  ASCII text
name_cp1252.txt: ISO-8859 text
name_utf-8.txt:  UTF-8 Unicode text
> cat name_*
Hernandez
Hern�ndez
Hernández
> hasmultibyte name_ascii.txt && echo multibyte
> hasmultibyte name_cp1252.txt && echo multibyte
> hasmultibyte name_utf-8.txt && echo multibyte
multibyte

Обновление

Этот код был обновлен с оригинала. Он был изменен, чтобы прочитать первый байт многобайтового символа и прочитать, сколько байтов должен быть символ. Это можно определить следующим образом.

first byte    number of bytes
110xxxxx      2
1110xxxx      3
1111xxxx      4

Этот метод более надежен и уменьшит неточности. Исходный метод искал байт вида 11xxxxxx и проверил следующий байт на наличие байта продолжения (10xxxxxx). Это приведет к ложному срабатыванию, учитывая что-то вроде â„x в файле CP1252. В двоичном виде это 11100010 10000100 01111000. Первый байт требует 3 байта символа, второй - это байт продолжения, а третий - нет. Это недопустимая последовательность UTF-8.

Дополнительное тестирование

> # create files
> echo "â„¢" | iconv -f UTF-8 -t CP1252 > 3byte.txt
> echo "Ââ„¢" | iconv -f UTF-8 -t CP1252 > 3byte_fail.txt
> echo "â„x" | iconv -f UTF-8 -t CP1252 > 3byte_fail2.txt

> hasmultibyte 3byte.txt; echo $? 
0
> hasmultibyte 3byte_fail.txt; echo $? 
5
> hasmultibyte 3byte_fail2.txt; echo $? 
5
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...