跳转到主要内容

MySQL的latin1编码的Python字符串编解码器

项目描述

作者

Wouter Bolsterlee

许可

3条款BSD

网址

https://github.com/wbolster/mysql-latin1-codec

概述

本项目提供MySQL的latin1编码的Python字符串编解码器,并附带类似iconv的命令行脚本,用于在shell管道中使用。

理由

  • MySQL默认使用latin1编码处理所有文本数据,但其latin1编码实际上是MySQL特定的变体。

  • 由于应用程序编写不当或数据库配置错误,许多现有数据库仍然将数据保存在MySQL的latin1列中,即使这些数据实际上不是latin1数据。

  • MySQL不会对此提出异议,因此这种情况往往不被注意。只有在尝试从其他应用程序访问数据库或尝试使用mysqldump产生的数据库转储进行grep时,问题才会出现。

  • 许多库无法正确支持此编码,使用真正的latin1编码会导致数据在处理过程中损坏,尤其是在数据库包含非西欧语言的文本时。

  • 此字符串编解码器使能够重建MySQL中存储的确切字节。这对于修复此类数据库的文本编码问题是很有价值的工具。为了方便起见,还包括一个类似iconv的命令行界面。

版本历史

  • 版本 2.0 (2014-01-31)

    • 添加了显式的 register() 函数,而不是在模块导入时产生副作用。

  • 版本 1.0(2013-12-04)

    • 初始发布

安装

$ pip install mysql-latin1-codec

该软件包支持 Python 2 和 Python 3。

用法

您可以使用此项目两种方式:作为独立的命令行工具和作为 Python 模块。

命令行工具

命令行工具的行为类似于 iconv

$ python -m mysql_latin1_codec --help
usage: mysql_latin1_codec.py [-h] [-f encoding] [-t encoding] [-o filename]
                             [-c]
                             [inputs [inputs ...]]

iconv-like tool to encode or decode data using MySQL's "latin1" dialect,
which, despite the name, is a mix of cp1252 and ISO-8859-1.

positional arguments:
  inputs                Input file(s) (defaults to stdin)

optional arguments:
  -h, --help            show this help message and exit
  -f encoding, --from encoding
                        Source encoding (uses MySQL's "latin1" if omitted)
  -t encoding, --to encoding
                        Target encoding (uses MySQL's "latin1" if omitted)
  -o filename, --output filename
                        Output file (defaults to stdout)
  -c, --skip-invalid    Omit invalid characters from output (not the default)

Python API

在 Python 代码中,只需导入名为 mysql_latin1_codec 的模块,并调用 register() 函数。将在 Python 的编码注册表中注册一个名为 mysql_latin1 的字符串编码。

import mysql_latin1_codec

mysql_latin1_codec.register()

您可以使用正常的 (字节) 字符串上的 .decode().encode() 方法,您还可以将其指定为各种 I/O 函数(如 io.open())的 encoding 参数。示例

# String encoding/decoding round-trip
s1 = u'foobar'
s2 == text.encode('mysql_latin1').decode('mysql_latin1')
assert s1 == s2

# Reading files
import io
with io.open('/path/to/file', 'r', encoding='mysql_latin1') as fp:
    for line in fp:
        pass

实际示例

下面的示例“修复”了包含双重编码 UTF-8 数据的转储,即实际 UTF-8 数据存储在 MySQL latin1 表中。默认情况下,mysqldump 创建 UTF-8 转储,但如果 MySQL 认为数据是 latin1,则将其再次转换,导致双重编码的数据。

$ cat backup-of-broken-database-produced-by-mysqldump.sql \
  | python -m mysql_latin1_codec -f UTF-8 \
  | iconv -c -f UTF-8 -t UTF-8 \
  > legible-text-in-utf8.sql

此示例中的 iconv 管道删除了无效的 UTF-8 序列,同时保留了有效部分。MySQL 截断超出列最大大小的值,但如果 MySQL 不知道它正在处理 UTF-8 数据(因为数据库模式和损坏的应用程序没有告诉它这样做),它将截断字节序列,而不是字符序列。这可能导致在多字节序列中间截断时出现不完整的 UTF-8 序列。由于这些字符无论如何都无法恢复,因此在这种情况下删除它们是正确的解决方案。

在代码中,您可以像上面的示例那样做类似的事情

original = b'...'  # byte string containing doubly-encoded UTF-8 data
s = original.decode('UTF-8').encode('mysql_latin1').decode('UTF-8', 'replace')

另一个示例“修复”包含存储在 MySQL latin1 列中的 GB2312(简体中文)数据的转储,同样被 mysqldump 错误地解释和编码为 UTF-8

$ cat mojibake-crap.sql \
  | python -m mysql_latin1_codec -f UTF-8 \
  | iconv -f GB2312 -t UTF-8 \
  > legible-text-in-utf8.sql

技术背景

MySQL 如何定义 latin1

当指定 latin1 时,MySQL 使用的字符集实际上不是众所周知的 latin1 字符集,官方称为 ISO-8859-1。MySQL 所称的 latin1 实际上是基于 cp-1252(也称为 windows-1252)的自定义编码。

MySQL 文档关于 西欧字符集 9§ 10.1.14.2) 包含以下内容:

latin1 是默认字符集。MySQL 的 latin1 与 Windows 的 cp1252 字符集相同。这意味着它与官方的 ISO 8859-1 或 IANA(互联网分配数字权威机构)的 latin1 相同,除了 IANA latin10x800x9f 之间的代码点视为“未定义”,而 cp1252 以及因此 MySQL 的 latin 将字符分配给这些位置。例如,0x80 是欧元符号。对于 cp1252 中的“未定义”条目,MySQL 将 0x81 转换为 Unicode 0x00810x8d 转换为 0x008d0x8ff 转换为 0x008f0x90 转换为 0x00900x9d 转换为 0x009d

更多详细信息可以在 MySQL 源代码的文件 strings/ctype-latin1.c 中找到

WL#1494 notes:

We'll use cp1252 instead of iso-8859-1.
cp1252 contains printable characters in the range 0x80-0x9F.
In ISO 8859-1, these code points have no associated printable
characters. Therefore, by converting from CP1252 to ISO 8859-1,
one would lose the euro (for instance). Since most people are
unaware of the difference, and since we don't really want a
"Windows ANSI" to differ from a "Unix ANSI", we will:

 - continue to pretend the latin1 character set is ISO 8859-1
 - actually allow the storage of euro etc. so it's actually cp1252

Also we'll map these five undefined cp1252 character:
  0x81, 0x8D, 0x8F, 0x90, 0x9D
into corresponding control characters:
   U+0081, U+008D, U+008F, U+0090, U+009D.
like ISO-8859-1 does. Otherwise, loading "mysqldump"
output doesn't reproduce these undefined characters.

如您所见,此编码与 ISO-8859-1(真正的 latin1)有很大不同,但 MySQL 仍然错误地将它标记为 latin

为什么这可能会成为一个问题

MySQL的latin1编码允许数据库列存储任意数据,而不进行任何验证。这意味着latin1文本列可以存储任何字节序列,例如UTF-8编码的文本(每个字符使用可变数量的字节)或甚至JPEG图像(根本就不是文本)。

这当然不是latin1列的正确用途。即使在现代的Unicode意识世界中,所有处理文本的正确软件都应该使用UTF-8(或其他Unicode编码),但仍很常见会遇到错误配置的数据库或编写不良的软件。大多数应用程序在存储和检索数据时使用相同的(错误的)假设,因此在许多配置中,这仍然“正常工作”,问题可能长时间未被察觉。

使这个问题更加严重的是,MySQL默认使用latin1字符编码,这主要是出于历史和向后兼容性的原因。这意味着许多现实世界的数据库可能错误地配置为在MySQL的latin1编码的列中存储数据,尽管这些列中实际存储的数据根本不是使用latin1编码的。

这可能导致各种问题,例如编码或解码错误、双重编码的文本、字符串操作的故障或错误的截断,这些都可能导致数据损坏。在许多情况下,这表现为mojibake文本。这可能是由于对字节所代表的字符的误解,或者由于双重编码造成的,例如,一个备份脚本将一个转换为UTF-8的latin1列再次转换为UTF-8。

许多工具,如Python的内置文本编码解码器以及iconv(命令行工具和C库)都不能使用这种自定义MySQL编码转换数据。这使得“恢复”例如存储在latin1列中的UTF-8数据变得非常困难,即使你知道自己在做什么以及使用了哪种实际编码。

当在命令行上调用时,此脚本将转换命令行上指定的转储文件(如果没有给出文件,则为标准输入)。数据被解释为UTF-8,并编码为MySQL的latin1,然后写入标准输出。输出是原始数据,可能需要进一步处理,例如使用iconv“重新解释”数据(例如作为UTF-8)。

我不知道你在说什么!

别担心,没关系。

项目详情


下载文件

下载适用于您平台的应用程序。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

mysql-latin1-codec-2.0.tar.gz (7.3 kB 查看哈希值

上传时间

由以下赞助商支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面