编码问题 char 看起来正确但不是

Encoding Problem char looks right but is not

首先不是常见的utf8问题。我的应用程序的所有部分都设置为 utf8 并且工作正常。

我通过 PHP 通过 IMAP 收到一封邮件并获取标题。在这个标题中,我有一个特殊的字符。来自德语的 ö。现在我在我的数据库中搜索是否有带有此标题的条目。我知道有一个。数据库使用 utf8mb4_general_ci 进行编码,以便能够存储 4 位 utf8 编码的特殊字符。

来自邮件的标题:

Fw: Auflösungsvertrag

数据库中的条目:

Fw: Auflösungsvertrag

我将光标放在 ö 后面并尝试删除它。首先 ö 切换为 o,在第二次按下删除键后它完全消失了。如果我现在键入并在键盘上输入 ö,MySQL 会找到该条目。

如果我把两者都放在记事本里++ 你看

Fw: Auflösungsvertrag

FW:Auflösungsvertrag

如果将编码转换为 ASCII,您会得到

Fw: AufloÌ^sungsvertrag

Fw: Auflösungsvertrag

所以你现在可以看到两个 ö 的编码不同,但它们显示正确。所以我的 mySQL select 没有找到数据库条目。

谁能给我解释一下,并给我一个提示,让我使用 php 命令将第一个编码字符串转换为第二个编码字符串?

我再描述一下这个问题是如何产生的:

我写了一个票务系统。我发出的每封邮件都会在主题中添加门票 ID。如果我发出一封邮件,我会在外发 table 中将其写入数据库。然后一个 cronjob 将这个邮件发送出去 asyncron。我使用 PHP 邮件程序并通过 SMTP 发送。

我通过 IMAP 和 PHP IMAP 类 获取传入邮件。如果一封邮件在主题中带有 TID,我会将这封邮件合并到数据库中的票证中。所有工单条目均按 TID 列分组。

现在的问题是,如果您将邮件从系统发送到同一系统内的另一个邮件地址,您会将邮件合并到现有票证中。

这就是为什么我通过搜索发件人地址、收件人地址和标题来查看每封收到邮件的外发 table。如果我找到邮件,我就知道系统已经发出了。

所以如果我发送邮件,我有第一个编码。如果我再次收到相同的邮件,它会有其他编码。两种编码接缝都是有效的 utf8 编码。在网站上的任何地方我都能得到正确的字符,在数据库中我也能正确显示它。仅当我通过 PDO 进行 SQL 查询时,MySQL 将它们视为两个不同的字符。

这是我解决这个问题的方法,根据我的说法,它必须在数据库端使用一次性指令一劳永逸地修复,而不仅仅是 PHP 上的技巧,你会每次遇到问题时都要重复一遍。

首先我复制了你的 2 个字符串:

Auflösungsvertrag
Auflösungsvertrag

进入 Notepad++,其中我有(非常方便的)HEX 插件。

当我将文本转换为 HEX 时,我有那些值

4175666c6fcc8873756e677376657274726167
4175666cc3b673756e677376657274726167

如果我们拆分它,我们很容易看到导致问题的 2 ö 的十六进制

4175666c 6fcc88 73756e677376657274726167
4175666c c3b6   73756e677376657274726167

现在的诀窍是告诉 MySQL 将所有具有这些十六进制值的字符从一个替换为另一个,即 6fcc88c3b6

你可以用这个使用 UNHEX() 函数的语句来做到这一点

UPDATE your_table
SET your_column=REPLACE(your_column, UNHEX('6fcc88'), UNHEX('c3b6'))

下面的示例和复制

架构 (MySQL v8.0)

/* Creating test data  - Row 1 and 2 are identical */
create table test (id int, txt varchar(50), txthex varchar(100));
INSERT INTO test (id,txt,txthex) VALUES (1, 'Auflösungsvertrag', '4175666c6fcc8873756e677376657274726167');
INSERT INTO test (id,txt,txthex) VALUES (2, 'Auflösungsvertrag', '4175666c6fcc8873756e677376657274726167');
INSERT INTO test (id,txt,txthex) VALUES (3, 'Auflösungsvertrag','4175666cc3b673756e677376657274726167');
    

应用修复

/* Running oneshot fix on row 2 only */                                                      
UPDATE test 
SET txt=REPLACE(txt, UNHEX('6fcc88'), UNHEX('c3b6'))
WHERE id=2

检查查询

SELECT  id, txt, txthex hex_original, 
        CAST(UNHEX(txthex) AS CHAR(30)) unexed_original , 
        HEX(txt) hex_replaced
FROM test;
id txt hex_original unexed_original hex_replaced
1 Auflösungsvertrag 4175666c6fcc8873756e677376657274726167 Auflösungsvertrag 4175666C6FCC8873756E677376657274726167
2 Auflösungsvertrag 4175666c6fcc8873756e677376657274726167 Auflösungsvertrag 4175666CC3B673756E677376657274726167
3 Auflösungsvertrag 4175666cc3b673756e677376657274726167 Auflösungsvertrag 4175666CC3B673756E677376657274726167

我找到了解决方案。

主题叫做 Unicode 等价性,有规范化的方法。 https://en.wikipedia.org/wiki/Unicode_equivalence

PHP 也有一个 class。 https://www.php.net/manual/de/normalizer.normalize.php

我不得不打电话

normalizer_normalize( $myString, Normalizer::NFKC );