在C++中将所有带重音符号的字母转换为普通字母

15

问题

如何在C++(或C)中将所有重音字母更改为正常字母?

我的意思是像eéèêaàäâçc这样的字符将变成eeeeaaaacc

我已经尝试过的方法

我尝试了手动解析字符串并逐个替换每一个字符,但我认为肯定有一种更好/更简单的方法,我还不知道(可以保证我不会忘记任何重音字母)。

我想知道标准库中是否已经有映射表,或者所有带重音符号的字符是否可以使用某些数学函数轻松地映射到“正常”字母上(例如floor(charCode-131/5) + 61))。


3
怀疑。最好的情况是它将取决于地区。 - Lightness Races in Orbit
5
输入的编码是什么?如果是Latin1,你可以使用替换字符的查找表。但如果是UTF-8,则会涉及到归一化问题,这会让事情变得复杂。 - Daniel Fischer
3
您可以在以下链接中找到答案:使用ICU去除变音符号的代码在C中从UTF8字符串中删除变音符号 - Christian Ammer
1
有人能告诉我为什么在任何地方都需要这种转换吗? - Rookie
你想要这个干什么? - Ignacio Vazquez-Abrams
显示剩余6条评论
7个回答

12
char* removeAccented( char* str ) {
    char *p = str;
    while ( (*p)!=0 ) {
        const char*
        //   "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"
        tr = "AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuypy";
        unsigned char ch = (*p);
        if ( ch >=192 ) {
            (*p) = tr[ ch-192 ];
        }
        ++p; // https://dev59.com/i2Yq5IYBdhLWcg3w7EwF
    }
    return str;
}

这看起来是一个优雅的解决方案,但你应该检查 ch < 256,因为在某些平台上 char 的值可能大于255,这样数组就会溢出,对于像Žž这样的字符尤其如此。 - Pavel Jiri Strnad

8
你首先应该定义“重音字母”的含义。如果你有的是扩展的8位ASCII码和一个用于代码128以上的国家代码页,或者是一些UTF8编码的字符串,那么需要做的工作大不相同。
然而,你应该看看libicu,它提供了好的基于Unicode的重音字母操作所需的内容。
但这并不能解决所有问题。例如,如果你收到一些中文或俄文信件怎么办?如果你得到土耳其的大写字母I点?去掉"I"上的点?这样做会改变文本的意思...等等。这种问题在Unicode中是无穷无尽的。甚至传统的排序顺序也取决于国家...

1
使用ICU是正确的答案。它已经安装并可用于每个现代系统,甚至像iPhone和Android这样的设备上也可以使用。您只需要链接到它即可使用它。(当然,如何做取决于系统/操作系统。) - Dúthomhas

7

我只是在理论上了解。基本上,您需要执行Unicode规范化,然后进行一些分解,清除所有变音符号,再次组合。


2
假设这些值只是char,我会创建一个包含目标值的数组,然后用数组中对应的成员替换每个字符:
char replacement[256];
int n(0);
std::generate_n(replacement, 256, [=]() mutable -> unsigned char { return n++; });
replacement[static_cast<unsigned char>('é')] = 'e';
// ...
std::transform(s.begin(), s.end(), s.begin(),
               [&](unsigned char c){ return replacement[c]; });

由于问题也标有C:,当使用C时,您需要创建适当的循环来执行相同的操作,但从概念上讲,它只是一样的。类似地,如果您无法使用C++2011,则应使用适当的函数对象代替lambda函数。

显然,replacement数组可以只设置一次,并且使用比上面概述的更智能的方法。不过,原则应该是有效的。如果你需要替换Unicode字符,那么事情会变得更加有趣:首先,数组会相当大,此外字符可能需要多个单词才能被更改。


“假设”——那是一个非常大的假设。 - n. m.
2
它们不是 char,至少如果输入是 UTF-8 格式的话,应该如此。 - phihag
@phihag:看起来所提到的字符都包含在ISO/IEC 8859-1(ISO Latin1)中。如果这已经足够,为什么还要使用UTF-8呢?此外,关于公式的陈述似乎表明了一种相对简单的方法来表示这些字符。 - Dietmar Kühl
@DietmarKühl 这些字符可能被 Latin-1 覆盖,但不一定以 Latin-1 编码。 - phihag

2
这是你可以使用 ISO/IEC 8859-1(基于ASCII标准的字符编码)做的事情:
  • 如果编码范围是从192 - 197,则替换为A
  • 如果编码范围是从224 - 229,则替换为a
  • 如果编码范围是从200 - 203,则替换为E
  • 如果编码范围是从232 - 235,则替换为e
  • 如果编码范围是从204 - 207,则替换为I
  • 如果编码范围是从236 - 239,则替换为i
  • 如果编码范围是从210 - 214,则替换为O
  • 如果编码范围是从242 - 246,则替换为o
  • 如果编码范围是从217 - 220,则替换为U
  • 如果编码范围是从249 - 252,则替换为u
假设x是数字的代码,请按以下方式处理大写字母:
  • y = floor((x - 192) / 6)
  • 如果y ≤ 2,则z = ((y + 1) * 4) + 61,否则z = (y * 6) + 61
对于小写字母,请执行以下操作:
  • y = floor((x - 224) / 6)
  • 如果y ≤ 2,则z = ((y + 1) * 4) + 93,否则z = (y * 6) + 93
最终答案z是所需字母的ASCII代码。
请注意,此方法仅适用于您使用的字符集为ISO/IEC 8859-1

这在Java中对我起作用了,我猜它应该在C++中也能起作用。如果不行,请告诉我。 - bane
2
它肯定可以以一种微不足道的方式使用ASCII,因为那里没有大于127的字符。它可能可以使用不是ASCII的东西,但很可能无法使用其他东西不是ASCII的。 - n. m.
2
如果我们受限于0-255范围,我可能会使用查找表而不是所有这些if - Matteo Italia

1

恐怕这里没有简单的解决方法。

在我工作的应用程序中,我们使用内部代码页表来解决这个问题,每个代码页表(比如1250、1251、1252等)都包含实际的代码页字母和非变音等效字母。这些表是使用c#自动生成的,其中包含一些非常方便的类(实际上还带有一些启发式算法),而且Java也可以快速实现。

这实际上是为了多字节带代码页数据而设计的,但它也可以用于UNICODE字符串(只需在所有表中搜索给定的UNICODE字母即可)。


我很感兴趣,为什么会需要这种转换呢? - Rookie
在搜索中,用户只能输入ASCII字符,但可以搜索包含带音符字符的文本。 - marcinj

0

我的使用情况是需要对一长串字符串进行不区分大小写的排序,其中一些字符串可能带有变音符号。例如,我想让"Añasco Municipio"排在"Anchorage Municipality"之前,而不是像原来那样排在"Abbeville County"之前。

我的字符串编码为UTF-8,但有可能包含一些扩展ASCII字符,而不是正确的UTF-8 Unicode。我本可以将所有字符串升级到UTF-8,然后使用能够进行UTF-8字符串比较的库,但我想要完全控制速度和决定如何将变音字符映射到非变音字符。 (我的选择包括将男性序数指示符视为“o”,并将版权字符视为c。)

下面的“双字节”代码是UTF-8序列。 “单字节”代码是扩展ASCII。

这就是我得到代码的地方:

http://www.ascii-code.com/

http://www.endmemo.com/unicode/unicodeconverter.php

void SimplifyStringForSorting( string *s, bool changeToLowerCase )
{
    // C0 C1 C2 C3 C4 C5 E0 E1 E2 E3 E4 E5 AA // one-byte codes for "a"
    // C3 80 C3 81 C3 82 C3 83 C3 84 C3 85 C3 A0 C3 A1 C3 A2 C3 A3 C3 A4 C3 A5 C2 AA // two-byte codes for "a"
    
    // C8 C9 CA CB E8 E9 EA EB // one-byte codes for "e"
    // C3 88 C3 89 C3 8A C3 8B C3 A8 C3 A9 C3 AA C3 AB // two-byte codes for "e"
    
    // CC CD CE CF EC ED EE EF // one-byte codes for "i"
    // C3 8C C3 8D C3 8E C3 8F C3 AC C3 AD C3 AE C3 AF // two-byte codes for "i"
    
    // D2 D3 D4 D5 D6 F2 F3 F4 F5 F6 BA // one-byte codes for "o"
    // C3 92 C3 93 C3 94 C3 95 C3 96 C3 B2 C3 B3 C3 B4 C3 B5 C3 B6 C2 BA // two-byte codes for "o"
    
    // D9 DA DB DC F9 FA FB FC // one-byte codes for "u"
    // C3 99 C3 9A C3 9B C3 9C C3 B9 C3 BA C3 BB C3 BC // two-byte codes for "u"
    
    // A9 C7 E7 // one-byte codes for "c"
    // C2 A9 C3 87 C3 A7 // two-byte codes for "c"
    
    // D1 F1 // one-byte codes for "n"
    // C3 91 C3 B1 // two-byte codes for "n"
    
    // AE // one-byte codes for "r"
    // C2 AE // two-byte codes for "r"
    
    // DF // one-byte codes for "s"
    // C3 9F // two-byte codes for "s"
    
    // 8E 9E // one-byte codes for "z"
    // C5 BD C5 BE // two-byte codes for "z"
    
    // 9F DD FD FF // one-byte codes for "y"
    // C5 B8 C3 9D C3 BD C3 BF // two-byte codes for "y"
    
    int n = s->size();
    int pos = 0;
    for ( int i = 0 ; i < n ; i++, pos++ )
    {
        unsigned char c = (unsigned char)s->at( i );
        if ( c >= 0x80 )
        {
            if ( i < ( n - 1 ) && (unsigned char)s->at( i + 1 ) >= 0x80 )
            {
                unsigned char c2 = SimplifyDoubleCharForSorting( c, (unsigned char)s->at( i + 1 ), changeToLowerCase );
                if ( c2 < 0x80 )
                {
                    s->at( pos ) = c2;
                    i++;
                }
                else
                {
                    // s->at( pos ) = SimplifySingleCharForSorting( c, changeToLowerCase );
                    // if it's a double code we don't recognize, skip both characters;
                    // this does mean that we lose the chance to handle back-to-back extended ascii characters
                    // but we'll assume that is less likely than a unicode "combining character" or other
                    // unrecognized unicode character for data
                    i++;
                }
            }
            else
            {
                unsigned char c2 = SimplifySingleCharForSorting( c, changeToLowerCase );
                if ( c2 < 0x80 )
                {
                    s->at( pos ) = c2;
                }
                else
                {
                    // skip unrecognized single-byte codes
                    pos--;
                }
            }
        }
        else
        {
            if ( changeToLowerCase && c >= 'A' && c <= 'Z' )
            {
                s->at( pos ) = c + ( 'a' - 'A' );
            }
            else
            {
                s->at( pos ) = c;
            }
        }
    }
    if ( pos < n )
    {
        s->resize( pos );
    }
}

unsigned char SimplifyDoubleCharForSorting( unsigned char c1, unsigned char c2, bool changeToLowerCase )
{
    // C3 80 C3 81 C3 82 C3 83 C3 84 C3 85 C3 A0 C3 A1 C3 A2 C3 A3 C3 A4 C3 A5 C2 AA // two-byte codes for "a"
    // C3 88 C3 89 C3 8A C3 8B C3 A8 C3 A9 C3 AA C3 AB // two-byte codes for "e"
    // C3 8C C3 8D C3 8E C3 8F C3 AC C3 AD C3 AE C3 AF // two-byte codes for "i"
    // C3 92 C3 93 C3 94 C3 95 C3 96 C3 B2 C3 B3 C3 B4 C3 B5 C3 B6 C2 BA // two-byte codes for "o"
    // C3 99 C3 9A C3 9B C3 9C C3 B9 C3 BA C3 BB C3 BC // two-byte codes for "u"
    // C2 A9 C3 87 C3 A7 // two-byte codes for "c"
    // C3 91 C3 B1 // two-byte codes for "n"
    // C2 AE // two-byte codes for "r"
    // C3 9F // two-byte codes for "s"
    // C5 BD C5 BE // two-byte codes for "z"
    // C5 B8 C3 9D C3 BD C3 BF // two-byte codes for "y"
    
    if ( c1 == 0xC2 )
    {
        if ( c2 == 0xAA ) { return 'a'; }
        if ( c2 == 0xBA ) { return 'o'; }
        if ( c2 == 0xA9 ) { return 'c'; }
        if ( c2 == 0xAE ) { return 'r'; }
    }
    
    if ( c1 == 0xC3 )
    {
        if ( c2 >= 0x80 && c2 <= 0x85 ) { return changeToLowerCase ? 'a' : 'A'; }
        if ( c2 >= 0xA0 && c2 <= 0xA5 ) { return 'a'; }
        if ( c2 >= 0x88 && c2 <= 0x8B ) { return changeToLowerCase ? 'e' : 'E'; }
        if ( c2 >= 0xA8 && c2 <= 0xAB ) { return 'e'; }
        if ( c2 >= 0x8C && c2 <= 0x8F ) { return changeToLowerCase ? 'i' : 'I'; }
        if ( c2 >= 0xAC && c2 <= 0xAF ) { return 'i'; }
        if ( c2 >= 0x92 && c2 <= 0x96 ) { return changeToLowerCase ? 'o' : 'O'; }
        if ( c2 >= 0xB2 && c2 <= 0xB6 ) { return 'o'; }
        if ( c2 >= 0x99 && c2 <= 0x9C ) { return changeToLowerCase ? 'u' : 'U'; }
        if ( c2 >= 0xB9 && c2 <= 0xBC ) { return 'u'; }
        if ( c2 == 0x87 ) { return changeToLowerCase ? 'c' : 'C'; }
        if ( c2 == 0xA7 ) { return 'c'; }
        if ( c2 == 0x91 ) { return changeToLowerCase ? 'n' : 'N'; }
        if ( c2 == 0xB1 ) { return 'n'; }
        if ( c2 == 0x9F ) { return 's'; }
        if ( c2 == 0x9D ) { return changeToLowerCase ? 'y' : 'Y'; }
        if ( c2 == 0xBD || c2 == 0xBF ) { return 'y'; }
    }
    
    if ( c1 == 0xC5 )
    {
        if ( c2 == 0xBD ) { return changeToLowerCase ? 'z' : 'Z'; }
        if ( c2 == 0xBE ) { return 'z'; }
        if ( c2 == 0xB8 ) { return changeToLowerCase ? 'y' : 'Y'; }
    }
    
    return c1;
}

unsigned char SimplifySingleCharForSorting( unsigned char c, bool changeToLowerCase )
{
    // C0 C1 C2 C3 C4 C5 E0 E1 E2 E3 E4 E5 AA // one-byte codes for "a"
    // C8 C9 CA CB E8 E9 EA EB // one-byte codes for "e"
    // CC CD CE CF EC ED EE EF // one-byte codes for "i"
    // D2 D3 D4 D5 D6 F2 F3 F4 F5 F6 BA // one-byte codes for "o"
    // D9 DA DB DC F9 FA FB FC // one-byte codes for "u"
    // A9 C7 E7 // one-byte codes for "c"
    // D1 F1 // one-byte codes for "n"
    // AE // one-byte codes for "r"
    // DF // one-byte codes for "s"
    // 8E 9E // one-byte codes for "z"
    // 9F DD FD FF // one-byte codes for "y"
    
    if ( ( c >= 0xC0 && c <= 0xC5 ) || ( c >= 0xE1 && c <= 0xE5 ) || c == 0xAA )
    {
        return ( ( c >= 0xC0 && c <= 0xC5 ) && !changeToLowerCase ) ? 'A' : 'a';
    }
    
    if ( ( c >= 0xC8 && c <= 0xCB ) || ( c >= 0xE8 && c <= 0xEB ) )
    {
        return ( c > 0xCB || changeToLowerCase ) ? 'e' : 'E';
    }
    
    if ( ( c >= 0xCC && c <= 0xCF ) || ( c >= 0xEC && c <= 0xEF ) )
    {
        return ( c > 0xCF || changeToLowerCase ) ? 'i' : 'I';
    }
    
    if ( ( c >= 0xD2 && c <= 0xD6 ) || ( c >= 0xF2 && c <= 0xF6 ) || c == 0xBA )
    {
        return ( ( c >= 0xD2 && c <= 0xD6 ) && !changeToLowerCase ) ? 'O' : 'o';
    }
    
    if ( ( c >= 0xD9 && c <= 0xDC ) || ( c >= 0xF9 && c <= 0xFC ) )
    {
        return ( c > 0xDC || changeToLowerCase ) ? 'u' : 'U';
    }
    
    if ( c == 0xA9 || c == 0xC7 || c == 0xE7 )
    {
        return ( c == 0xC7 && !changeToLowerCase ) ? 'C' : 'c';
    }
    
    if ( c == 0xD1 || c == 0xF1 )
    {
        return ( c == 0xD1 && !changeToLowerCase ) ? 'N' : 'n';
    }
    
    if ( c == 0xAE )
    {
        return 'r';
    }
    
    if ( c == 0xDF )
    {
        return 's';
    }
    
    if ( c == 0x8E || c == 0x9E )
    {
        return ( c == 0x8E && !changeToLowerCase ) ? 'Z' : 'z';
    }
    
    if ( c == 0x9F || c == 0xDD || c == 0xFD || c == 0xFF )
    {
        return ( ( c == 0x9F || c == 0xDD ) && !changeToLowerCase ) ? 'Y' : 'y';
    }
    
    return c;
}

这好像不处理组合字符。 - melpomene
@melpomene:然而,有了UTF8输入,我们可以立即删除所有组合重音,因为在较低的ASCII范围内没有映射到无重音字符的组合重音。(因此您可以认为它应该删除所有汉字,因为没有一个映射到低ASCII。) - Jongware
melpomene和Rad Lexus:谢谢,是的,我没有考虑合并字符。我已经修改了代码以跳过无法识别的双字符序列(请参见代码中的注释),这将删除组合码以及其他无法识别的双字节序列。我现在也忽略无法识别的单字节序列,希望这能以合理的方式处理中文、三字节序列等。我意识到所有这些都很奇怪,试图同时处理UTF-8和扩展ASCII,但希望它对以英语为中心的排序工作良好。 - M Katz
顺便问一下,你知道在现实环境中组合字符与预组合字符相比有多常见吗?我猜对于像音标和元音上的重音符等“正常”的扩展 ASCII 类型字符来说,组合字符相对较少? - M Katz
干得好,正是我想要的!问题(针对您很久以前的代码):在调用SimplifySingleChar之后,s->at(pos);不应该是's->at(pos) = c2;吗? - mackworth
@mackworth,是的,对不起。实际上我早就在我的代码库中修复了它,但是没有在这里修复它...现在已经修复了。 - M Katz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接