PHP7.1 json_encode() 浮点数问题

142
这不是一个问题,更像是一个提醒。我更新了一个使用json_encode()的应用程序到PHP7.1.1后,发现在处理浮点数时会将其扩展到17位。根据文档,PHP 7.1.x开始使用serialize_precision代替精度来编码双精度值。我猜这导致了一个例子值

472.185

在通过json_encode()处理后变成了

472.18500000000006

自从我发现这个问题后,我已经恢复到了PHP 7.0.16,并且再也没有遇到使用json_encode()时出现的这个问题。我还尝试升级到PHP 7.1.2,但最终还是回到了PHP 7.0.16。

这个问题的原因源于PHP - Floating Number Precision,不过归根结底是由于在json_encode()中使用了serialize_precision而非precision。

如果有人知道如何解决这个问题,我很乐意听听相关的解释和修复方案。

多维数组摘录(之前):

[staticYaxisInfo] => Array
                    (
                        [17] => stdClass Object
                            (
                                [variable_id] => 17
                                [static] => 1
                                [min] => 0
                                [max] => 472.185
                                [locked_static] => 1
                            )

                    )

经过调用 json_encode() 函数后...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },

8
ini_set('serialize_precision', 14); ini_set('precision', 14); 的作用是让序列化结果像以前一样,但如果您真的依赖于浮点数的特定精度,那么您可能做错了什么。 - apokryfos
1
"如果有人知道这个问题的解决方案" - 什么问题?我在这里看不到任何问题。如果你使用PHP解码JSON,你会得到你编码的值。如果你使用另一种语言解码它,很可能你会得到相同的值。无论哪种方式,如果你用12位数字打印值,你会得到原始(“正确”的)值。你的应用程序所使用的浮点数需要超过12位小数精度吗? - axiac
18
@axiac 472.185不等于472.18500000000006。这是一个向浏览器发出AJAX请求的一部分,该值需要保持其原始状态。 - Gwi7d31
5
我试图避免使用字符串转换,因为最终产品是Highcharts,而它不接受字符串。如果你拿一个浮点数值,将其强制转换为字符串,发送出去,然后让JavaScript再将该字符串解释回浮点数,我认为这样做非常低效和粗糙。你觉得呢? - Gwi7d31
1
@axiac 我注意到你的PHP json_decode()确实会带回原始的浮点数值。然而,当javascript将JSON字符串转换回对象时,它不会将该值转换回类似于472.185的值,就像你可能在暗示的那样...因此产生了问题。我将坚持我现在使用的方法。 - Gwi7d31
显示剩余5条评论
13个回答

128
这件事情困扰了我一段时间,直到我最终找到了指向 这个 bug 的链接以及指向 这篇 RFC 的链接。目前,json_encode() 使用的是 EG(precision),其默认设置为 14。这意味着最多只能使用 14 位数字来显示(打印)该数字。IEEE 754 双精度浮点数支持更高的精度,并且 serialize()/var_export() 使用的 PG(serialize_precision) 默认设置为 17,以提高准确性。由于 json_encode() 使用的是 EG(precision),所以它会删除分数部分的低位数位并破坏原始值,即使 PHP 的浮点数可以容纳更精确的浮点数值也是如此。

强调一下,这篇 RFC 建议引入一个新的设置 EG(precision)=-1 和 PG(serialize_precision)=-1,它使用 zend_dtoa() 的模式 0,该模式使用更好的算法来四舍五入浮点数(-1 用于表示 0 模式)

简而言之,有一种新方法可以让 PHP 7.1 的 json_encode 使用新的、改进的精度引擎。在 php.ini 文件中,您需要将 serialize_precision 更改为

serialize_precision = -1

你可以使用这个命令行来验证它是否可用

php -r '$price = ["price" => round("45.99", 2)]; echo json_encode($price);'

你应该获得

{"price":45.99}

G(precision)=-1PG(serialize_precision)=-1也可以在PHP 5.4中使用。 - kittygirl
4
请注意 serialize_precision = -1。使用 -1 时,这段代码 echo json_encode([528.56 * 100]); 输出的结果为 [52855.99999999999] - vl.lapikov
4
@vl.lapikov 这更像是一个通用浮点数错误,这里有一个demo,你可以清楚地看到这不仅仅是json_encode的问题。 - Machavity
我原以为这只是PHP的傻瓜行为,但事实证明WHM(我们服务器使用的恶劣配置管理器)默认将serialize_precision设置为100,因此每次调用json_encode时,我们都会得到100位数值。肯定有人说:“精度?没问题,我们需要很多!”而不知道他们在做什么。注释掉它并使用默认值(现在是-1)就解决了问题。 - Glenn Maynard

57

作为插件开发人员,我无法通用地访问服务器的php.ini设置。因此,基于Machavity的答案,我编写了这个小代码片段,您可以在PHP脚本中使用它。只需将其放置在脚本顶部,json_encode将继续像往常一样工作。

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'serialize_precision', -1 );
}
在某些情况下,需要设置一个或多个变量。我将此作为第二种解决方案添加,因为我不确定第二种解决方案在所有已证明第一种解决方案有效的情况下是否有效。

在某些情况下需要设置更多变量。我添加了这个作为第二个解决方案,因为我不能确定第二个解决方案在所有情况下都像第一个解决方案一样有效。

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}

4
注意一下,因为你的插件可能会改变开发应用程序的意外设置。但是,我个人认为,我不确定这个选项有多具破坏性... 哈哈 - igorsantos07
请注意,更改精度值(第二个示例)可能会对您在其他数学运算中使用的操作产生更大的影响。https://www.php.net/manual/en/ini.core.php#ini.precision - Ricardo Martins
根据文档, 默认精度为14。上述修复将其增加到17。因此应该更加精确。你同意吗? - alev
@alev 我的意思是只需更改serialize_precision就足够了,不会影响应用程序可能遇到的其他PHP行为。 - Ricardo Martins

22

我通过将精度和序列化精度都设置为相同的值(10)来解决了这个问题:

ini_set('precision', 10);
ini_set('serialize_precision', 10);

您还可以在php.ini中进行设置


1
这是唯一对我有效的方法...设置两个值。或者将serialize_precision设置为14,因为这是精度的默认值,但将两个值设置为相同的更安全。 - Tim Pickup

7
我正在对货币价值进行编码,例如330.46被编码成330.4600000000000363797880709171295166015625。如果你不想或不能更改PHP设置,并且提前知道数据结构,那么有一个非常简单的解决方案可以解决这个问题。只需要将其转换为字符串(以下两种方式都可以实现相同的效果):
$data['discount'] = (string) $data['discount'];
$data['discount'] = '' . $data['discount'];

针对我的使用情况,这是一个快速有效的解决方案。只需注意,这意味着当您从JSON中解码它时,它将是一个字符串,因为它将用双引号包装。


1
如果将其作为JSON字符串存在问题,只需将其转换回浮点数,例如(float)(string) $data['discount']; - Ben Holness

4

我曾经遇到同样的问题, 但是仅仅将serialize_precision设置为-1并没有解决问题。我还需要再进行一步操作,即将精度值从14更新为17(因为我的PHP7.0 ini文件中是这样设定的)。显然,更改该数字的值会改变计算浮点数的值。


4

在php 7.2.32上的解决办法是在php.ini中设置:

precision=10
serialize_precision=10

2
$val1 = 5.5;
$val2 = (1.055 - 1) * 100;
$val3 = (float)(string) ((1.055 - 1) * 100);
var_dump(json_encode(['val1' => $val1, 'val2' => $val2, 'val3' => $val3]));

{
  "val1": 5.5,
  "val2": 5.499999999999994,
  "val3": 5.5
}

1
对我来说,问题出在将JSON_NUMERIC_CHECK作为json_encode()的第二个参数传递时,它会将所有(不仅仅是整数)数字类型转换为int。

1

使用number_format把它存储为你需要的精度,然后使用JSON_NUMERIC_CHECK选项json_encode

$foo = array('max' => number_format(472.185, 3, '.', ''));
print_r(json_encode($foo, JSON_NUMERIC_CHECK));

你得到:

你得到:

{"max": 472.185}

请注意,这将使源对象中的所有数字字符串在生成的JSON中编码为数字。

2
我已在PHP 7.3中进行了测试,但它无法正常工作(输出仍具有过高的精度)。显然,自PHP 7.1以来,JSON_NUMERIC_CHECK标志已经失效 - https://www.php.net/manual/de/json.constants.php#123167 - Philipp

0

同样的问题。在PHP 7.4上,我尝试了不同的解决方案,但只有这种组合对我起作用:

precision = 14
serialize_precision = 14

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