使用PHP将所有类型的智能引号转换

33

我正在尝试在处理文本时将所有类型的智能引号转换为常规引号。但是,我编写的以下函数似乎仍然缺乏支持和适当的设计。

我该如何正确地获取所有引号字符的转换?

function convert_smart_quotes($string)
{
    $quotes = array(
        "\xC2\xAB"   => '"', // « (U+00AB) in UTF-8
        "\xC2\xBB"   => '"', // » (U+00BB) in UTF-8
        "\xE2\x80\x98" => "'", // ‘ (U+2018) in UTF-8
        "\xE2\x80\x99" => "'", // ’ (U+2019) in UTF-8
        "\xE2\x80\x9A" => "'", // ‚ (U+201A) in UTF-8
        "\xE2\x80\x9B" => "'", // ‛ (U+201B) in UTF-8
        "\xE2\x80\x9C" => '"', // “ (U+201C) in UTF-8
        "\xE2\x80\x9D" => '"', // ” (U+201D) in UTF-8
        "\xE2\x80\x9E" => '"', // „ (U+201E) in UTF-8
        "\xE2\x80\x9F" => '"', // ‟ (U+201F) in UTF-8
        "\xE2\x80\xB9" => "'", // ‹ (U+2039) in UTF-8
        "\xE2\x80\xBA" => "'", // › (U+203A) in UTF-8
    );
    $string = strtr($string, $quotes);

    // Version 2
    $search = array(
        chr(145),
        chr(146),
        chr(147),
        chr(148),
        chr(151)
    );
    $replace = array("'","'",'"','"',' - ');
    $string = str_replace($search, $replace, $string);

    // Version 3
    $string = str_replace(
        array('‘','’','“','”'),
        array("'", "'", '"', '"'),
        $string
    );

    // Version 4
    $search = array(
        '‘', 
        '’', 
        '“', 
        '”', 
        '—',
        '–',
    );
    $replace = array("'","'",'"','"',' - ', '-');
    $string = str_replace($search, $replace, $string);

    return $string;
}

注意:这个问题是一个完整的查询,涵盖了所有报价的范围,包括"Microsoft"引用在此提问。这就像询问所有轮胎尺寸是询问汽车轮胎尺寸的“重复”。

你替换智能引号的目的是什么?通常最好保留它们;如果您在处理字符方面遇到问题,那么您可能也会遇到所有其他非ASCII字符的问题,这些问题不会因为隐藏智能引号而消失。这段代码试图同时处理UTF-8和ISO-8859-1格式的文本以及HTML,这是一项混乱的业务,通常会严重破坏许多Unicode字符,而不仅仅是引号。 - bobince
@bobince,我正在进行字符串解析,引号字符对我很重要。我会按原样处理其余的Unicode字形。 - Xeoncross
@bobince 我很乐意奖励一个能够处理其他字符的答案 - 但我的担忧是识别所有引号符号,以便我可以解析字符串而不必担心其他形式。 - Xeoncross
@bobince 如果需要的话,我可以在引用解析之前使用$string = html_entity_decode(iconv('utf-8', 'utf-8', $string)); - Xeoncross
1
如果您的输入确实是HTML格式的文本内容,那么这将很好地工作。有一个微小的区别:在非基于XML的HTML中,范围在€Ÿ€ÿ)内的字符引用会被Web浏览器解码为具有相同编号的Windows代码页1252代码单元的字符,而不是像您期望的那样是U+0080到U+00FF的字符。PHP不会复制这个历史怪癖,并将在字符串中保留这些格式不正确的引用的和号序列。 - bobince
显示剩余5条评论
2个回答

93

您需要类似这样的内容(假设输入为UTF-8,并忽略CJK(中文,日文,韩文)):

$chr_map = array(
   // Windows codepage 1252
   "\xC2\x82" => "'", // U+0082⇒U+201A single low-9 quotation mark
   "\xC2\x84" => '"', // U+0084⇒U+201E double low-9 quotation mark
   "\xC2\x8B" => "'", // U+008B⇒U+2039 single left-pointing angle quotation mark
   "\xC2\x91" => "'", // U+0091⇒U+2018 left single quotation mark
   "\xC2\x92" => "'", // U+0092⇒U+2019 right single quotation mark
   "\xC2\x93" => '"', // U+0093⇒U+201C left double quotation mark
   "\xC2\x94" => '"', // U+0094⇒U+201D right double quotation mark
   "\xC2\x9B" => "'", // U+009B⇒U+203A single right-pointing angle quotation mark

   // Regular Unicode     // U+0022 quotation mark (")
                          // U+0027 apostrophe     (')
   "\xC2\xAB"     => '"', // U+00AB left-pointing double angle quotation mark
   "\xC2\xBB"     => '"', // U+00BB right-pointing double angle quotation mark
   "\xE2\x80\x98" => "'", // U+2018 left single quotation mark
   "\xE2\x80\x99" => "'", // U+2019 right single quotation mark
   "\xE2\x80\x9A" => "'", // U+201A single low-9 quotation mark
   "\xE2\x80\x9B" => "'", // U+201B single high-reversed-9 quotation mark
   "\xE2\x80\x9C" => '"', // U+201C left double quotation mark
   "\xE2\x80\x9D" => '"', // U+201D right double quotation mark
   "\xE2\x80\x9E" => '"', // U+201E double low-9 quotation mark
   "\xE2\x80\x9F" => '"', // U+201F double high-reversed-9 quotation mark
   "\xE2\x80\xB9" => "'", // U+2039 single left-pointing angle quotation mark
   "\xE2\x80\xBA" => "'", // U+203A single right-pointing angle quotation mark
);
$chr = array_keys  ($chr_map); // but: for efficiency you should
$rpl = array_values($chr_map); // pre-calculate these two arrays
$str = str_replace($chr, $rpl, html_entity_decode($str, ENT_QUOTES, "UTF-8"));

以下是背景信息:

每个Unicode字符都属于且仅属于一个"通用类别",其中可以包含引号字符的是以下几种:

这些页面对于检查是否漏掉了什么很有用 - 还有一个分类索引

有时在启用Unicode的正则表达式中匹配这些类别是很有用的。

此外,Unicode字符具有"属性",你感兴趣的是Quotation_Mark。不幸的是,这些在正则表达式中是无法访问的。

在维基百科上,你可以找到具有Quotation_Mark属性的字符组。最终参考资料是unicode.org上的PropList.txt,但这是一个ASCII文本文件。

如果您需要翻译CJK字符,只需获取它们的码点、确定其翻译并找到其UTF-8编码即可,例如通过在fileformat.info中查找(例如对于U+301E: http://www.fileformat.info/info/unicode/char/301e/index.htm)。
关于Windows代码页1252:Unicode定义前256个码点表示与ISO-8859-1完全相同的字符,但ISO-8859-1经常与Windows代码页1252混淆,因此所有浏览器都将范围0x80-0x9F(在ISO-8859-1中为“空”(更确切地说:它包含控制字符))呈现为Windows代码页1252。维基百科页面中的表格列出了Unicode等效项。
注意:strtr()通常比str_replace()慢。请根据您的输入和PHP版本计时。如果速度足够快,您可以直接使用像我的$chr_map这样的映射。
如果您不确定输入是否为UTF-8编码,并且愿意假设如果不是,则为ISO-8859-1或Windows代码页1252,则可以在任何其他操作之前执行此操作:
if ( !preg_match('/^\\X*$/u', $str)) {
   $str = utf8_encode($str);
}

警告:这个正则表达式在极少数情况下可能无法检测到非UTF-8编码。例如:"Gruß…"/*CP-1252*/=="Gru\xDF\x85" 看起来像是UTF-8,但在这个正则表达式中无法检测出来(U+07C5是N'ko数字5)。这个正则表达式可以稍微改进,但不幸的是,我们不能完全解决编码检测问题。


如果您想将来自Windows代码页1252的0x80-0x9F范围规范化为常规Unicode代码点,则可以执行此操作(并删除上面$chr_map的第一部分):
$normalization_map = array(
   "\xC2\x80" => "\xE2\x82\xAC", // U+20AC Euro sign
   "\xC2\x82" => "\xE2\x80\x9A", // U+201A single low-9 quotation mark
   "\xC2\x83" => "\xC6\x92",     // U+0192 latin small letter f with hook
   "\xC2\x84" => "\xE2\x80\x9E", // U+201E double low-9 quotation mark
   "\xC2\x85" => "\xE2\x80\xA6", // U+2026 horizontal ellipsis
   "\xC2\x86" => "\xE2\x80\xA0", // U+2020 dagger
   "\xC2\x87" => "\xE2\x80\xA1", // U+2021 double dagger
   "\xC2\x88" => "\xCB\x86",     // U+02C6 modifier letter circumflex accent
   "\xC2\x89" => "\xE2\x80\xB0", // U+2030 per mille sign
   "\xC2\x8A" => "\xC5\xA0",     // U+0160 latin capital letter s with caron
   "\xC2\x8B" => "\xE2\x80\xB9", // U+2039 single left-pointing angle quotation mark
   "\xC2\x8C" => "\xC5\x92",     // U+0152 latin capital ligature oe
   "\xC2\x8E" => "\xC5\xBD",     // U+017D latin capital letter z with caron
   "\xC2\x91" => "\xE2\x80\x98", // U+2018 left single quotation mark
   "\xC2\x92" => "\xE2\x80\x99", // U+2019 right single quotation mark
   "\xC2\x93" => "\xE2\x80\x9C", // U+201C left double quotation mark
   "\xC2\x94" => "\xE2\x80\x9D", // U+201D right double quotation mark
   "\xC2\x95" => "\xE2\x80\xA2", // U+2022 bullet
   "\xC2\x96" => "\xE2\x80\x93", // U+2013 en dash
   "\xC2\x97" => "\xE2\x80\x94", // U+2014 em dash
   "\xC2\x98" => "\xCB\x9C",     // U+02DC small tilde
   "\xC2\x99" => "\xE2\x84\xA2", // U+2122 trade mark sign
   "\xC2\x9A" => "\xC5\xA1",     // U+0161 latin small letter s with caron
   "\xC2\x9B" => "\xE2\x80\xBA", // U+203A single right-pointing angle quotation mark
   "\xC2\x9C" => "\xC5\x93",     // U+0153 latin small ligature oe
   "\xC2\x9E" => "\xC5\xBE",     // U+017E latin small letter z with caron
   "\xC2\x9F" => "\xC5\xB8",     // U+0178 latin capital letter y with diaeresis
);
$chr = array_keys  ($normalization_map); // but: for efficiency you should
$rpl = array_values($normalization_map); // pre-calculate these two arrays
$str = str_replace($chr, $rpl, $str);

2
@SebastiánGrignoli,你可以在这里阅读:http://www.regular-expressions.info/unicode.html#grapheme 正如它所说:“你可以将\X视为点的Unicode版本”。更准确地说,它匹配UTF-8非修饰字符,可选择性地后跟修饰字符,从开头(^)到结尾($)。我不知道它是否还检查修饰符对于修改它们的字符的有效性,但可以确定它检查整个字符串是否由有效的UTF-8字节序列(编码有效的Unicode代码点)组成,并且不能以修饰符开头。 - Walter Tross
@SebastiánGrignoli,抱歉,我应该说“组合标记”(\p{M})而不是“修饰符”。 - Walter Tross
1
@WalterTross - 非常感谢您 - 我一直在寻找一些开箱即用的解决方案,但是没有找到。相反,我使用了上面的一部分来创建了一个专门用于此目的的软件包 - 希望您不介意。https://github.com/sebastiansulinski/smart-quotes - Sebastian Sulinski
4
在网上,这个问题的唯一完整且正确的答案(可能并非真正如此,但你明白我的意思)。可惜它在相关搜索中的排名不够靠前。 - John
1
@FrankForte 一般来说是正确的,但如果你仔细阅读,我写了“在任何其他事情之前”。 - Walter Tross
显示剩余4条评论

14
你可以使用这个函数来转换所有的字符:
$output = iconv('UTF-8', 'ASCII//TRANSLIT', $input);

一定要将类型更改为您需要的类型。

(注:此内容来自于另一个类似问题,可在 此处 找到。)


2
要明确的是,这将转换更多的内容,而不仅仅是智能引号,因此可能会产生意想不到的后果。 - John Rix

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