Как изменить строку UTF-8 на месте? - PullRequest
13 голосов
/ 14 октября 2008

Недавно кто-то спросил об алгоритме для обращения строки на месте в C . Большинство из предложенных решений имели проблемы при работе с не однобайтовыми строками. Итак, мне было интересно, что может быть хорошим алгоритмом для работы именно со строками utf-8.

Я придумал какой-то код, который я публикую в качестве ответа, но я был бы рад увидеть идеи или предложения других людей. Я предпочел использовать реальный код, поэтому я выбрал C #, так как он кажется одним из самых популярных языков на этом сайте, но я не против, если ваш код написан на другом языке, если это возможно понимают все, кто знаком с императивным языком. И поскольку это предназначено для того, чтобы увидеть, как такой алгоритм может быть реализован на низком уровне (под низкоуровневым я имею в виду только работу с байтами), идея состоит в том, чтобы избегать использования библиотек для основного кода.

Примечания:

Меня интересует сам алгоритм, его производительность и как его можно оптимизировать (я имею в виду оптимизацию на уровне алгоритма, не заменяя i ++ на ++ i и т. Д. На самом деле меня тоже не интересуют реальные тесты).

Я не имею в виду использовать его в производственном коде или "изобретать велосипед". Это просто из любопытства и как упражнение.

Я использую байтовые массивы C #, поэтому я предполагаю, что вы можете получить длину строки без выполнения этой строки, пока не найдете NUL. То есть я не учитываю сложность поиска длины строки. Но если вы используете C, например, вы можете выделить это с помощью strlen () перед вызовом основного кода.

Edit:

Как отмечает Майк Ф., мой код (и код других людей, размещенный здесь) не имеет отношения к составным символам. Некоторая информация о тех здесь . Я не знаком с этой концепцией, но если это означает, что существуют «комбинирующие символы», т. Е. Символы / кодовые точки, которые действительны только в сочетании с другими «базовыми» символами / кодовыми точками, справочную таблицу таких символы могут быть использованы для сохранения порядка «глобального» символа («базовый» + «комбинирующий» символы) при обращении.

Ответы [ 5 ]

12 голосов
/ 14 октября 2008

Я бы сделал один проход, чтобы поменять местами байты, затем второй проход, который меняет байты любых многобайтовых символов (которые легко обнаруживаются в UTF8) обратно в их правильный порядок.

Вы можете определенно справиться с этим в очереди за один проход, но я бы не стал беспокоиться, если бы рутина не стала узким местом.

7 голосов
/ 14 октября 2008

В этом коде предполагается, что входная строка UTF-8 является правильной и правильно сформированной (то есть не более 4 байтов на многобайтовый символ):

#include "string.h"

void utf8rev(char *str)
{
    /* this assumes that str is valid UTF-8 */
    char    *scanl, *scanr, *scanr2, c;

    /* first reverse the string */
    for (scanl= str, scanr= str + strlen(str); scanl < scanr;)
        c= *scanl, *scanl++= *--scanr, *scanr= c;

    /* then scan all bytes and reverse each multibyte character */
    for (scanl= scanr= str; c= *scanr++;) {
        if ( (c & 0x80) == 0) // ASCII char
            scanl= scanr;
        else if ( (c & 0xc0) == 0xc0 ) { // start of multibyte
            scanr2= scanr;
            switch (scanr - scanl) {
                case 4: c= *scanl, *scanl++= *--scanr, *scanr= c; // fallthrough
                case 3: // fallthrough
                case 2: c= *scanl, *scanl++= *--scanr, *scanr= c;
            }
            scanr= scanl= scanr2;
        }
    }
}

// quick and dirty main for testing purposes
#include "stdio.h"

int main(int argc, char* argv[])
{
    char buffer[256];
    buffer[sizeof(buffer)-1]= '\0';

    while (--argc > 0) {
        strncpy(buffer, argv[argc], sizeof(buffer)-1); // don't overwrite final null
        printf("%s → ", buffer);
        utf8rev(buffer);
        printf("%s\n", buffer);
    }
    return 0;
}

Если вы скомпилируете эту программу (пример имени: so199260.c) и запустите ее в среде UTF-8 (в данном случае установка Linux):

$ so199260 γεια και χαρά français АДЖИ a♠♡♢♣b
a♠♡♢♣b → b♣♢♡♠a
АДЖИ → ИЖДА
français → siaçnarf
χαρά → άραχ
και → ιακ
γεια → αιεγ

Если код слишком загадочный, я с удовольствием уточню.

6 голосов
/ 14 октября 2008

Согласитесь, что ваш подход - единственный разумный способ сделать это на месте.

Лично мне не нравится повторная проверка UTF8 внутри каждой функции, которая имеет с ним дело, и обычно я делаю только то, что нужно, чтобы избежать сбоев; это добавляет гораздо меньше кода. Не знаю много C #, так что вот он в C:

( отредактировано для исключения strlen )

void reverse( char *start, char *end )
{
    while( start < end )
    {
        char c = *start;
        *start++ = *end;
        *end-- = c;
    }
}

char *reverse_char( char *start )
{
    char *end = start;
    while( (end[1] & 0xC0) == 0x80 ) end++;
    reverse( start, end );
    return( end+1 );
}

void reverse_string( char *string )
{
    char *end = string;
    while( *end ) end = reverse_char( end );
    reverse( string, end-1 );
}
5 голосов
/ 14 октября 2008

Мой первоначальный подход можно обобщить так:

1) Наивно переворачивать байты

2) Запустите строку назад и исправьте последовательности utf8.

Недопустимые последовательности обрабатываются на втором этапе, и на первом этапе мы проверяем, находится ли строка в «sync» (то есть, начинается ли она с допустимого начального байта).

РЕДАКТИРОВАТЬ: улучшена проверка для старшего байта в Reverse ()

class UTF8Utils {


    public static void Reverse(byte[] str) {
        int len = str.Length;
        int i   = 0;
        int j   = len - 1;

        //  first, check if the string is "synced", i.e., it starts
        //  with a valid leading character. Will check for illegal 
        //  sequences thru the whole string later.
        byte leadChar = str[0];

        //  if it starts with 10xx xxx, it's a trailing char...
        //  if it starts with 1111 10xx or 1111 110x 
        //  it's out of the 4 bytes range.
    //  EDIT: added validation for 7 bytes seq and 0xff
        if( (leadChar & 0xc0) == 0x80 ||
            (leadChar & 0xfc) == 0xf8 ||
            (leadChar & 0xfe) == 0xfc ||
        (leadChar & 0xff) == 0xfe ||
        leadChar == 0xff) {

            throw new Exception("Illegal UTF-8 sequence");

        }

        //  reverse bytes in-place naïvely
        while(i < j) {
            byte tmp = str[i];
            str[i]  = str[j];
            str[j]  = tmp;
            i++;
            j--;
        }
        //  now, run the string again to fix the multibyte sequences
        UTF8Utils.ReverseMbSequences(str);

    }

    private static void ReverseMbSequences(byte[] str) {
        int i = str.Length - 1;
        byte leadChar = 0;
        int nBytes  = 0;

        //  loop backwards thru the reversed buffer
        while(i >= 0) {
            //  since the first byte in the unreversed buffer is assumed to be
            //  the leading char of that byte, it seems safe to assume that the  
            //  last byte is now the leading char. (Given that the string is
            //  not out of sync -- we checked that out already)
            leadChar = str[i];

            //  check how many bytes this sequence takes and validate against
            //  illegal sequences
            if(leadChar < 0x80) {
                nBytes = 1;
            } else if((leadChar & 0xe0) == 0xc0) {
                if((str[i-1] & 0xc0) != 0x80) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 2;
            } else if ((leadChar & 0xf0) == 0xe0) {
                if((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 3;
            } else if ((leadChar & 0xf8) == 0xf0) {
                if((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ||
                    (str[i-3] & 0xc0) != 0x80  ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 4;
            } else {
                throw new Exception("Illegal UTF-8 sequence");
            }

            //  now, reverse the current sequence and then continue
            //  whith the next one
            int back    = i;
            int front   = back - nBytes + 1;

            while(front < back) {
                byte tmp = str[front];
                str[front] = str[back];
                str[back] = tmp;
                front++;
                back--;
            }
            i -= nBytes;
        }
    }
} 
0 голосов
/ 14 октября 2008

Лучшее решение:

  1. Преобразовать в широкую строку символов
  2. Перевернуть новую строку

Никогда, никогда, никогда, никогда не рассматривайте отдельные байты как символы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...