Работа с ошибкой perl DBD::mysql UTF-8

У нас есть программа, написанная на Perl, которая извлекает данные из базы данных mysql. Для этого мы используем интерфейс DBD::mysql.

Мы можем правильно получить все данные, база данных — UTF8MB4, а приложение perl использует UTF-8.

Код для получения результата sql:

use utf8;
use encoding 'utf8';

...
my $dsn = "DBI:mysql:database=mydatabase;mysql_enable_utf8=1";
my $dbh = DBI->connect($dsn, $userid, $password, { mysql_enable_utf8 => 1 } ) or die $DBI::errstr;

...

my $sth = $dbh->prepare("SELECT addressid, 
                            company, firstname, lastname, 
                            address, zip, city, country,
                            phone, mobile, home,
                            speeddial_phone, speeddial_mobile, speeddial_home,
                            fax, email
                        FROM address
                        WHERE (firstname like ? or lastname like ? or company like ?)
                        LIMIT $sizeLimit
                        ");
$sth->execute(  $searchExpression, $searchExpression, $searchExpression) or die $DBI::errstr;

Пока выражение $searchExpression содержит обычные символы, оно работает нормально. Но как только мы запрашиваем специальные символы не в ASCII, например é ö ä ü и подобные, мы не получаем обратно пустой набор результатов.

Согласно этому сообщению, это связано с ошибкой в ​​драйверах dbd::mysql до версии 4.041_01.

http://blogs.perl.org/users/mike_b/2016/12/dbdmysql-all-your-utf-8-bugs-are-belong-to-us.html

Я пробовал разные вещи, но безрезультатно.

Я включил ведение журнала запросов на сервере mysql, и там я вижу, что параметры со специальными символами поступают в неправильной кодировке.

Здесь вывод журнала mysql в файл:

Time                 Id Command    Argument
180905  9:17:06   403 Connect   inno-ldap-db@localhost on phonebook_innovaphone
                  403 Query     SELECT addressid,
                                company, firstname, lastname,
                                address, zip, city, country,
                                phone, mobile, home,
                                speeddial_phone, speeddial_mobile, speeddial_home,
                                fax, email
                            FROM address
                            WHERE companyid='1' and (firstname like 'andré%' or lastname like 'andré%' or company like 'andré%' )
                            LIMIT 25
                  403 Quit

Поскольку в настоящее время мы не можем обновить систему (это Debian 7, который включает только более старые пакеты, такие как 4.021-1+deb7u3), мне нужно было бы решить эту проблему.

Либо какое-то волшебство для предварительного кодирования/декодирования параметров, либо драйвер odbc, возможно, не столкнется с этой ошибкой?


person André Schild    schedule 04.09.2018    source источник
comment
Содержит ли «$ dsn» «mysql_enable_utf8 = 1»? Я не уверен, что ошибка, на которую вы указываете, является вашей проблемой, поскольку она связана с тем, что DBD::mysql не преобразует переменные «latin1» в «utf8»; но $searchExpression уже должен быть закодирован в utf-8 из-за 'use utf8;' на вершине. Кроме того, вы уверены, что правильно извлекаете данные utf-8? Попробуйте с полями, содержащими не только западные/латинские1 символы (ä,ü и т. д.), но и ą,ă, ш, я.   -  person mosvy    schedule 04.09.2018
comment
Я указал mysql_enable_utf8=1 в соединении, но теперь я также добавил его в $dns без каких-либо различий. Я теперь также добавил строку с указанными выше символами, они извлекаются правильно. Похоже, проблемы есть только у направления perl-›mysql. (Я не вставляю/обновляю данные из perl, только запрашиваю их или как параметр фильтра запроса)   -  person André Schild    schedule 05.09.2018
comment
Что предупреждает utf8, utf8::is_utf8($searchExpression)? вкл\n : выкл\n;' непосредственно перед строкой $sth-›execute?   -  person mosvy    schedule 05.09.2018
comment
@mosvy показывает: utf8 включен   -  person André Schild    schedule 05.09.2018
comment
Я только что попробовал старый Debian Wheezy (libdbd-mysql-perl_4.021-1+deb7u3, libdbi-perl_1.622-1+deb7u1), но не смог воспроизвести. Прости. Единственное, о чем я могу думать, это то, что ошибка находится в другом месте, и $searchExpression уже был дважды закодирован в utf8, прежде чем был передан $sth-›execute; но вы, наверное, уже проверили это.   -  person mosvy    schedule 05.09.2018
comment
@mosvy Спасибо, это была подсказка, которая мне была нужна. Теперь это работает. Смотрите ответ для решения.   -  person André Schild    schedule 05.09.2018


Ответы (2)


Оказалось, что строка (полученная через Net::LDAP::Server) уже была в какой-то кодировке utf8, а потом драйвер mysql еще раз ее закодировал.

Решил проблему, добавив этот код

use Encode qw( decode );
my $decoded = eval { decode('UTF-8', $encoded, Encode::FB_CROAK) }

Код взят из этого сообщения: Правильный способ обнаружения кодирования в Perl

Спасибо за подсказку про двойное кодирование в mosvy

person André Schild    schedule 05.09.2018

Чтобы уточнить немного больше: DBD::mysql не может знать, какую кодировку сервер ожидает для параметров связывания. Кроме того, у него есть та же ошибка юникода, что и у многих модулей CPAN: он игнорирует кодировку хранения, которую использует perl, и просто просматривает внутренние биты и байты, поэтому одна и та же строка в Perl будет выводиться иногда правильно, иногда неправильно, что является корнем. путаницы, которая есть у многих людей: результаты не имеют смысла, потому что внутреннее кодирование строк не является чем-то, что обычно отображается на уровне Perl.

Возможно, правильный способ справиться с этим — попросить пользователя пометить каждый аргумент как двоичный (для столбцов больших двоичных объектов) и что-то еще — это то, что делает DBD::MariaDb — он предполагает, что все является текстом, если вы не переопределите это, и, следовательно, вы не столкнетесь с той же проблемой за счет необходимости выполнять дополнительную работу для двоичных значений.

Вы также можете детерминистически справиться с этим в DBD::mysql, и вот как:

При подключении включите флаг/атрибут mysql_enable_utf8mb4. Вы не должны делать это позже, но последовательность в вопросе также должна работать. Вы также должны использовать кодировку utf8mb4 для вашей базы данных/таблиц/текстовых столбцов.

Это должно надежно работать с данными, считанными из базы данных, поскольку mysql/mariadb будет правильно помечать текст как текст (включая его кодировку), а двоичный файл — как двоичный.

Осталась проблема, что делать при подаче данных. Это легко: при отправке текста убедитесь, что он находится в кодировке Unicode (что означает, что он не закодирован в utf-8, поскольку Perl может напрямую представлять символы Unicode). Затем вы можете либо обновить (не меняет значение строки в Perl, либо закодировать строку в utf8 (что меняет значение в Perl):

# when you have a unicode text string
utf8::upgrade $text_column; # do this
utf8::encode $text_column; # OR that

Любой из них гарантирует, что внутреннее представление строки Perl совместимо с utf-8, чего и ожидает база данных. Первый, вероятно, предпочтительнее, так как он продолжает работать, если драйвер базы данных когда-либо исправлен (или когда вы переключаетесь на DBD::MariaDb)

В качестве альтернативы, если ваши данные уже закодированы в utf-8, можно перейти на более раннюю версию:

# when you have an utf-8 encoded binary string
utf8::downgrade $text_column;

Для столбцов blob/binary необходимо убедиться, что внутреннее представление Perl не в utf-8. Вы можете убедиться в этом, используя utf8::downgrade:

# when you have BLOB data
utf8::downgrade $blob_column;

Это также не меняет значения строки в Perl, но поскольку DBD::mysql не заботится о том, что думает Perl, он сделает все правильно и отправит данные в двоичном виде.

К сожалению, это не совместимая с будущими версия — если вы переключитесь на другой исправный драйвер базы данных, вам, возможно, придется работать с данными BLOB по-другому. Одним из таких способов было бы пометить параметр как SQL_BLOB и выполнить понижение версии, что должно работать с правильными драйверами.

Теперь немного истории.

Путаница в модулях Perl и CPAN, по моему личному мнению, коренится в нескольких разных вещах. Во-первых, впервые реализуя юникод в Perl, люди поняли, что модель юникода, которую они впервые придумали для Perl, не работают должным образом, и захотели ее изменить. К сожалению, книга Camel для документирования Perl уже была обновлена, поэтому они не изменили ее, чтобы она оставалась совместимой с книгой, с некоторыми половинчатыми исправлениями. В результате perl перестал быть полностью совместимым с книгой, а также не полностью совместимым с правильной моделью. Ситуация улучшилась за последние годы, но это заняло много времени и привело к многочисленным жертвам.

Во-вторых, многие справочные страницы, такие как perluniintro, совершенно неверны и закрепляют неверные предположения о том, что perl знает о кодировке своих символьных строк или что так называемый флаг utf8 имеет какое-то отношение к строке, находящейся в юникоде и /или утф-8. Ни то, ни другое не правильно.

И, наконец, XS API был изменен несовместимо: чтобы получить строку символов sina в Perls до Unicode, вы должны были вызвать функцию с именем SvPV, которая просто возвращает указатель на символы, все из которых могут быть сохранены в одном байте по адресу того времени, т.е. двоичный

Вместо того, чтобы оставить это таким образом, более новые версии perl продолжали возвращать внутреннее байтовое представление строки, поэтому модули, которые не были обновлены, будут получать необработанные данные, не имея возможности их правильно интерпретировать, потому что SvPV не возвращает достаточно информации для декодирования данных.

Таким образом, за ночь все модули Perl, работающие со строковыми данными, получили ошибку Perl Unicode Bug(tm), и не все из них были исправлены. И их исправление сейчас может привести к поломке программ, которые каким-то образом устраняли проблемы.

person Remember Monica    schedule 08.05.2021
comment
Какой беспорядок.... :( Но спасибо за полный ответ - person André Schild; 10.05.2021