PHP中支持Unicode的自然排序算法?

Natural sorting algorithm in PHP with support for Unicode?

提问人:Alix Axel 提问时间:5/7/2009 最后编辑:hippietrailAlix Axel 更新时间:1/13/2016 访问量:12309

问:

是否可以使用自然顺序算法在 PHP 中对带有 Unicode / UTF-8 字符的数组进行排序?例如(此数组中的顺序正确排序):

$array = array
(
    0 => 'Agile',
    1 => 'Ágile',
    2 => 'Àgile',
    3 => 'Âgile',
    4 => 'Ägile',
    5 => 'Ãgile',
    6 => 'Test',
);

如果我尝试使用 asort($array),我会得到以下结果:

Array
(
    [0] => Agile
    [6] => Test
    [2] => Àgile
    [1] => Ágile
    [3] => Âgile
    [5] => Ãgile
    [4] => Ägile
)

并使用 natsort($array):

Array
(
    [2] => Àgile
    [1] => Ágile
    [3] => Âgile
    [5] => Ãgile
    [4] => Ägile
    [0] => Agile
    [6] => Test
)

如何在 PHP 5 下实现返回正确结果顺序(0、1、2、3、4、5、6)的函数?所有多字节字符串函数(mbstring、iconv 等)都可以在我的系统上使用。

编辑:我想 natsort() 值,而不是键 - 我明确定义键(并使用 asort() 而不是 sort())的唯一原因是为了简化找出 unicode 值排序出错的地方的工作。

PHP 数组排序 Unicode UTF-8

评论


答:

1赞 JW. 5/7/2009 #1
natsort($array);
$array = array_values($array);

评论

0赞 Alix Axel 5/7/2009
我示例中的键不是问题,它们只是为了帮助对 unicode 值进行排序。
25赞 Stefan Gehrig 5/7/2009 #2

这个问题并不像乍一看那么容易回答。这是PHP缺乏unicode支持的领域之一。

正如其他海报所建议的那样,natsort() 与要排序的类型的数组的排序无关。你要找的是一种语言环境感知排序机制,因为对带有扩展字符的字符串进行排序始终是所用语言的问题。让我们以德语为例:A 和 Ä 有时可以像是同一个字母一样排序 (DIN 5007/1),有时 Ä 可以按实际的“AE”排序 (DIN 5007/2)。相比之下,在瑞典语中,Ä 位于字母表的末尾。

如果你不使用 Windows,你很幸运,因为 PHP 提供了一些功能。结合使用 setlocale()、usort()、strcoll() 和适合您语言的正确 UTF-8 语言环境,您可以得到如下结果:

$array = array('Àgile', 'Ágile', 'Âgile', 'Ãgile', 'Ägile', 'Agile', 'Test');
$oldLocal = setlocale(LC_COLLATE, '<<your_RFC1766_language_code>>.utf8');
usort($array, 'strcoll');
setlocale(LC_COLLATE, $oldLocal);

请注意,必须使用 UTF-8 语言环境变体才能对 UTF-8 字符串进行排序。我将上面示例中的语言环境重置为其原始值,因为使用 setlocale() 设置语言环境可能会在其他正在运行的 PHP 脚本中引入副作用 - 有关详细信息,请参阅 PHP 手册。

当您使用 Windows 机器时,目前没有解决这个问题的方法,而且我认为在 PHP 6 之前不会有任何解决方案。请参阅我自己关于针对此特定问题的 SO 的问题

评论

1赞 Alix Axel 5/7/2009
很棒的洞察力,我正在 Windows 上开发,但这将在 *nix 机器上运行。如果我没记错的话,PHP 5.3 将支持这种排序,尽管是某种类,但是我想避免自己依赖 set_locale(),主要有两个原因:1) 它是不可预测的(取决于操作系统可用的区域设置)和 2) 它不是线程安全的,可能会导致服务器上的意外行为。
0赞 Alix Axel 5/7/2009
使用 ord() 函数的多字节版本进行排序会得到与简单 sort() 完全相同的结果。=(
1赞 Stefan Gehrig 5/7/2009
关于您的第一条评论:您是绝对正确的,我的回答中提出的解决方案不是人们可能期望的解决方案,因为它既不便携也不没有副作用。但是:这是目前唯一的一个 - 除了使用例如 ext/mbstring 在字符和字节级别实现自己的排序之外。
0赞 Alix Axel 5/8/2009
关于我的第二条评论,我使用 mbstring 扩展来编写原始 PHP ord() 函数的多字节等效代码,但它给我的结果与 sort() 函数相同。
1赞 Stefan Gehrig 5/8/2009
是的,在MySQL服务器上对数据进行排序将是一个可行的工作。MySQL不受这些限制的影响。您可以通过为数据选择正确的拼件来控制排序顺序。
13赞 Alix Axel 9/6/2010 #3

搞定了!

$array = array('Ägile', 'Ãgile', 'Test', 'カタカナ', 'かたかな', 'Ágile', 'Àgile', 'Âgile', 'Agile');

function Sortify($string)
{
    return preg_replace('~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|tilde|uml);~i', '$1' . chr(255) . '$2', htmlentities($string, ENT_QUOTES, 'UTF-8'));
}

array_multisort(array_map('Sortify', $array), $array);

输出:

Array
(
    [0] => Agile
    [1] => Ágile
    [2] => Âgile
    [3] => Àgile
    [4] => Ãgile
    [5] => Ägile
    [6] => Test
    [7] => かたかな
    [8] => カタカナ
)

更好的是:

if (extension_loaded('intl') === true)
{
    collator_asort(collator_create('root'), $array);
}

感谢@tchrist!

评论

3赞 tchrist 2/19/2011
听起来您在这里真正需要的是 Unicode 排序规则算法 (UCA)。我在这个答案中有一个Perl演示,我为那些可能没有合适的库可以调用的人提供了一个shell可调用的版本。也许这在这方面也可能有所帮助。
0赞 Alix Axel 2/20/2011
@tchrist:UCA是我要找的,我一会儿会仔细看看你的答案,谢谢你的提醒!;)
0赞 Anthony 4/19/2011 #4

我在这个问题上苦苦挣扎。

排序:

Array
(
    [xa] => África
    [xo] => Australasia
    [cn] => China
    [gb] => Reino Unido
    [us] => Estados Unidos
    [ae] => Emiratos Árabes Unidos
    [jp] => Japón
    [lk] => Sri Lanka
    [xe] => Europa Del Este
    [xw] => Europa Del Oeste
    [fr] => Francia
    [de] => Alemania
    [be] => Bélgica
    [nl] => Holanda
    [es] => España
)

把 África 放在最后。我用这段肮脏的小代码解决了它(这适合我的目的和我的时间框架):

$sort = array();
foreach($retval AS $key => $value) {
    $v = str_replace('ä', 'a', $value);
    $v = str_replace('Ä', 'A', $v);
    $v = str_replace('Á', 'A', $v);
    $v = str_replace('é', 'e', $v);
    $v = str_replace('ö', 'o', $v);
    $v = str_replace('ó', 'o', $v);
    $v = str_replace('Ö', 'O', $v);
    $v = str_replace('ü', 'u', $v);
    $v = str_replace('Ü', 'U', $v);
    $v = str_replace('ß', 'S', $v);
    $v = str_replace('ñ', 'n', $v);
    $sort[] = "$v|$key|$value";
}
sort($sort);

$retval = array();
foreach($sort AS $value) {
    $arr = explode('|', $value);
    $retval[$arr[1]] = $arr[2]; 
}

评论

0赞 Alix Axel 4/19/2011
你是法国人吗?你可能想看看我对这个问题的回答,我的方法音译做得更好一些,并且该函数还保留了值和非数字键的关联。preg_replacearray_multisort
0赞 Елин Й. 6/9/2015 #5

我还有另一种解决方法,可以解决那些不起作用且未启用该模块的问题:setlocaleintl

// The array to be sorted
$countries = array(
  'AT' => Österreich,
  'DE' => Deutschland,
  'CH' => Schweiz,
);

// Extend this array to your needs.
$utf_sort_map = array(
  "ä" => "a",
  "Ä" => "A",
  "Å" => "A",
  "ö" => "o",
  "Ö" => "O",
  "ü" => "u",
  "Ü" => "U",
);

uasort($my_array, function($a, $b) use ($utf_sort_map) {
  $initial_a = mb_substr($a, 0, 1);
  $initial_b = mb_substr($b, 0, 1);

  if (isset($utf_sort_map[$initial_a]) || isset($utf_sort_map[$initial_b])) {
    if (isset($utf_sort_map[$initial_a])) {
      $initial_a = $utf_sort_map[$initial_a];
    }

    if (isset($utf_sort_map[$initial_b])) {
      $initial_b = $utf_sort_map[$initial_b];
    }

    if ($initial_a == $initial_b) {
      return mb_substr($a, 1) < mb_substr($b, 1) ? -1 : 1;
    }
    else {
      return $initial_a < $initial_b ? -1 : 1;
    }
  }

  return $a < $b ? -1 : 1;
});