代码之家  ›  专栏  ›  技术社区  ›  Juan Pablo Califano

如何在适当的位置反转UTF-8字符串?

  •  13
  • Juan Pablo Califano  · 技术社区  · 16 年前

    最近有人问 algorithm for reversing a string in place in C . 大多数建议的解决方案在处理非单字节字符串时都有问题。所以,我想知道什么是专门处理UTF-8字符串的好算法。

    我想出了一些代码,作为答案发布,但我很高兴看到其他人的想法或建议。我更喜欢使用实际的代码,所以我选择了C,因为它似乎是这个网站上最流行的语言之一,但我不介意您的代码是否是另一种语言,只要熟悉命令式语言的人能够合理地理解它。而且,由于这是为了了解这种算法如何在低层实现(低层的意思是处理字节),所以我们的想法是避免为核心代码使用库。

    笔记:

    我对算法本身、它的性能以及如何进行优化感兴趣(我的意思是算法级的优化,不是用++i等代替i++;我也对实际的基准不感兴趣)。

    我并不打算在生产代码或“重新发明轮子”中实际使用它。这只是出于好奇,也是一种锻炼。

    我使用的是C字节数组,所以我假设你可以得到字符串的长度,而不需要遍历字符串,直到你找到一个nul。 也就是说,我不考虑找到字符串长度的复杂性。但是,例如,如果您使用的是C,那么您可以在调用核心代码之前使用strlen()来考虑这一点。

    编辑:

    正如MikeF所指出的,我的代码(以及这里发布的其他人的代码)不处理复合字符。关于这些的一些信息 here . 我不熟悉这个概念,但如果这意味着存在“组合字符”,即只有与其他“基本”字符/代码点组合时才有效的字符/代码点,则可以使用此类字符的查找表来保留反转时“全局”字符(“基本”+“组合”字符)的顺序。

    5 回复  |  直到 11 年前
        1
  •  12
  •   Michael Burr    16 年前

    我将进行一次传递,将字节反转,然后进行第二次传递,将任何多字节字符中的字节(很容易在UTF8中检测到)反转回正确的顺序。

    你完全可以在一次传球中完成这个动作,但是我不会费心,除非常规赛变成了瓶颈。

        2
  •  7
  •   tzot    16 年前

    此代码假定输入的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
    χαρά → άραχ
    και → ιακ
    γεια → αιεγ
    

    如果代码太神秘,我会很高兴地澄清。

        3
  •  6
  •   Mike F    16 年前

    同意你的方法是唯一明智的方法。

    就我个人而言,我不喜欢在每个处理它的函数中重新验证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 );
    }
    
        4
  •  5
  •   Juan Pablo Califano    16 年前

    我的初步方法可以概括如下:

    1)简单地反转字节

    2)向后运行字符串,并在执行过程中修复utf8序列。

    在第二步和第一步中处理非法序列,我们检查字符串是否处于“同步”状态(即,如果字符串以合法的前导字节开头)。

    编辑:改进了反向()中前导字节的验证

    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;
            }
        }
    } 
    
        5
  •  -3
  •   gnud    16 年前

    最佳解决方案:

    1. 转换为宽字符字符串
    2. 反转新字符串

    从不,从不,从不,从不将单个字节视为字符。