Text and Bytes

人类使用文本,计算机使用字节序列。

字符、码位和字节表示

字符串是一个字符序列

这是一个很简单的东西,问题是字符是什么?

Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分。

  • 字符的标识,即码位,是 0~1 114 111 的数字(十进制),在 Unicode 标准中以 4~6 个十六进制数字表示,而且加前缀“U+”。例如,字母 A 的码位是 U+0041,欧元符号的码位是 U+20AC,高音谱号的码位是 U+1D11E。在 Unicode 6.3 中(这是 Python 3.4 使用的标准),约 10% 的有效码位有对应的字符。
  • 字符的具体表述取决于所用的编码。编码是在码位和字节序列之间转换时使用的算法。在 UTF-8 编码中,A(U+0041)的码位编码成单个字节 \x41,而在 UTF-16LE 编码中编码成两个字节 \x41\x00。再举个例子,欧元符号(U+20AC)在 UTF-8 编码中是三个字节——\xe2\x82\xac,而在 UTF-16LE 中编码成两个字节:\xac\x20
  • 编码:码位 —> bytes
  • 解码:bytes —> 码位

pyencode1

  1. b 有 12 个 bytes
  2. str 对象长度为 4
  3. byte 的各个对象是 256 内的整数

Python 的 bytes 和 bytearray

对于上面的 b, 我们有类型 bytes,这表示 Python 的 immutable byte array,同时 Python 有可变的 bytearray

  1. 取 index 返回的是 256内整数
  2. bytes 对象切片还是 bytes 对象

虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有 ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。

  • 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
  • 制表符、换行符、回车符和 \ 对应的字节,使用转义序列 \t\n\r\\
  • 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。

此外,我们在处理加密/解密的时候常看到二进制序列,并用 hex 等表示,实际上:

二进制序列有个类方法是 str 没有的,名为 fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列.

除了 bytes 和 bytearray, Python 有 memoryview, 用于避免 copy. memoryview 对象复制等情况下不会 copy

编码问题

EncodeError

实际上,在强制你处理错误的语言中,bytes —> String 总是会需要你处理(尽管大部分时候不会有什么错误)

把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出 UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。

我们从 fluent Python 中选取对应图片:

屏幕快照 2019-07-17 上午12.47.08

可以看到,encode 的时候,可能由于字符集中无法处理你的字符,需要额外的 ignore 或者其他处理方法

我个人而言,处理 utf-8 的时候没有这种问题,但是别的情况下确实会碰到?

DecodeError

Decode 的时候,更容易因为你的数据来源产生问题:

EncodeError

其他

Python 中,过去你需要在 shebang 之类的地方注明 Unicode,否则会产生 SyntaxError

字节用了什么编码

之前的猜测文件格式中,似乎我们读取文件的一部分 — 判断文件是文本文件/图片/其他,这种行为还算常见。

实际上,HTML/XML这种玩意会给出自己的编码,shebang 这类也有类似的。其余的我们可以根据模式推测,比如利用这个包:https://github.com/chardet/chardet

二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可以在文本内容的开头添加一个字节序标记。参见下一节。

规范 unicode

Unicode 的 compare 等操作应当规范化,比如用 NFC/NFD 等

OS

Rust 有一种奇妙的类型叫 OsStr, 实际上,GNU/Linux 内核不理解 Unicode.

Python 会自动为我们转化,但是同时也支持 b'xxx' 的 api。

Python 的 os 模块也提供了 fsencode fsdecode:

为了便于手动处理字符串或字节序列形式的文件名或路径名,os 模块提供了特殊的编码和解码函数。

> fsencode(filename)
>

>

  如果 filenamestr 类型(此外还可能是 bytes 类型),使用 sys.getfilesystemencoding() 返回的编解码器把 filename 编码成字节序列;否则,返回未经修改的 filename 字节序列。

> fsdecode(filename)
>

>

  如果 filenamebytes 类型(此外还可能是 str 类型),使用 sys.getfilesystemencoding() 返回的编解码器把 filename 解码成字符串;否则,返回未经修改的 filename 字符串。

在 Unix 衍生平台中,这些函数使用 surrogateescape 错误处理方式(参见下述附注栏)以避免遇到意外字节序列时卡住。Windows 使用的错误处理方式是 strict

C/C++

https://sf-zhou.github.io/programming/chinese_encoding.html

妈的这篇文章写的太好了,看来 String 底下是 char, 输入输出中文没毛病是因为文件/终端都是 utf-8.

还有这篇:

https://www.reddit.com/r/cpp/comments/2zv2qo/best_approach_to_utf8_support_as_of_march_2015/

wchar_twstring 字节全部长度都为4了

design flaws: 设计缺陷,大概就是如此

locale 场所、当地

Further, the standard contains the wording “applies the simplest reasonable transformation”, so in effect these functions can do whatever they want.

使用: https://en.cppreference.com/w/cpp/locale/codecvt_utf8

原作者的代码我觉得可以当成范本了:

#include <codecvt>
#include <fstream>
#include <iostream>
int main() {
const std::locale utf8( std::locale(), new std::codecvt_utf8<wchar_t> );
const std::wstring love_cpp = L"我爱C++";
// sets the locale of the stream to the specified locale
std::wcout.imbue(utf8);
std::wcout << love_cpp << std::endl; // 输出 "我爱C++"
std::wcout << love_cpp.length() << std::endl; // 输出 5
std::wcout << love_cpp.substr(0, 2) << std::endl; // 输出 "我爱"
const auto k_output_filename = "test_02.txt";
std::wofstream f_out(k_output_filename, std::ios::out);
f_out.imbue(utf8);
f_out << love_cpp << std::endl;
f_out.close();
std::wifstream f_in(k_output_filename, std::ios::in);
f_in.imbue(utf8);
std::wstring value;
f_in >> value;
std::wcout << (value == love_cpp ? "Yes": "Opps") << std::endl; // 输出 "Yes"
f_in.close();
}

在 C++ 里面,我们也可以用 ICU 等库做进一步处理:https://github.com/unicode-org/icu

Rust

String 和 &str

Rust 有:&[u8] Vec<u8> &str String OsStr OsString CString

(妈的怎么这么烦…不不不我知道这是好东西)

不做 FFI 的话,字符串主要涉及:

&[u8] Vec<u8> &str String 这四种

其中:

  1. Rust 语言提供的只有 str, 通常以 &str 出现,这是 utf-8 字符串/utf-8字符串的引用
  2. String 由标准库提供,可增长

实际上,这俩都是 utf-8 标准的,所以 String, &str 可以很方便的互相转化。

String 有关的很多东西都实现了 Deref<Target=str> 的 trait, 导致他们可以:

fn print_me(msg: &str) { println!("msg = {}", msg); }
fn main() {
let string = "hello world";
print_me(string);
let owned_string = "hello world".to_string(); // or String::from_str("hello world")
print_me(&owned_string);
let counted_string = std::rc::Rc::new("hello world".to_string());
print_me(&counted_string);
let atomically_counted_string = std::sync::Arc::new("hello world".to_string());
print_me(&atomically_counted_string);
}

同样,我们可以看到,&strString是 utf-8 对象,那么对 Vec<u8> 的转化是没问题的,甚至底下就是这么实现的,但是反过来的话应该会有 EncodingError 的问题。

此外,这里介绍下转化:

UTF-8

Strings are always valid UTF-8. This has a few implications, the first of which is that if you need a non-UTF-8 string, consider OsString. It is similar, but without the UTF-8 constraint. The second implication is that you cannot index into a String:

> let s = "hello";
>
> println!("The first letter of s is {}", s[0]); // ERROR!!!
>

>

Indexing is intended to be a constant-time operation, but UTF-8 encoding does not allow us to do this. Furthermore, it’s not clear what sort of thing the index should return: a byte, a codepoint, or a grapheme cluster. The bytes and chars methods return iterators over the first two, respectively.

从 String 文档的 Representation 可以看到

Representation

A String is made up of three components: a pointer to some bytes, a length, and a capacity. The pointer points to an internal buffer String uses to store its data. The length is the number of bytes currently stored in the buffer, and the capacity is the size of the buffer in bytes. As such, the length will always be less than or equal to the capacity.

This buffer is always stored on the heap.

You can look at these with the as_ptr, len, and capacity methods:

> use std::mem;
>
> let story = String::from("Once upon a time...");
>
> let ptr = story.as_ptr();
> let len = story.len();
> let capacity = story.capacity();
>
> // story has nineteen bytes
> assert_eq!(19, len);
>
> // Now that we have our parts, we throw the story away.
> mem::forget(story);
>
> // We can re-build a String out of ptr, len, and capacity. This is all
> // unsafe because we are responsible for making sure the components are
> // valid:
> let s = unsafe { String::from_raw_parts(ptr as *mut _, len, capacity) } ;
>
> assert_eq!(String::from("Once upon a time..."), s);Run
>

>

If a String has enough capacity, adding elements to it will not re-allocate. For example, consider this program:

> let mut s = String::new();
>
> println!("{}", s.capacity());
>
> for _ in 0..5 {
> s.push_str("hello");
> println!("{}", s.capacity());
> }Run
>

>

This will output the following:

> 0
> 5
> 10
> 20
> 20
> 40
>

>

At first, we have no memory allocated at all, but as we append to the string, it increases its capacity appropriately. If we instead use the with_capacity method to allocate the correct capacity initially:

> let mut s = String::with_capacity(25);
>
> println!("{}", s.capacity());
>
> for _ in 0..5 {
> s.push_str("hello");
> println!("{}", s.capacity());
> }Run
>

>

We end up with a different output:

> 25
> 25
> 25
> 25
> 25
> 25
>

>

Here, there’s no need to allocate more memory inside the loop.

可以看到,这个实现类似 Vec.

Vec 有 charbytes 接口,char 实际上是 utf-8 支持的, bytes 则是原始 byte 的接口。(C++/C就不一样了,我们一无所有)。

字符串索引可以:&hello[1..4]

这边 Stringfrom_utf8 可能产生 Result, from_utf8_lossy: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy 则如同我们之前说的,会有补全策略(我不知道更好的称呼叫什么,你知道可以私信我)。

Path 和 &path 和 OsString

回顾我们说 Python 的时候,实际上,在操作系统、Path 等方面,我们不得不涉及到:

Path &path OsString 这些

OsString 文档如下:

A type that can represent owned, mutable platform-native strings, but is cheaply inter-convertible with Rust strings.

The need for this type arises from the fact that:

  • On Unix systems, strings are often arbitrary sequences of non-zero bytes, in many cases interpreted as UTF-8.
  • On Windows, strings are often arbitrary sequences of non-zero 16-bit values, interpreted as UTF-16 when it is valid to do so.
  • In Rust, strings are always valid UTF-8, which may contain zeros.

OsString and OsStr bridge this gap by simultaneously representing Rust and platform-native string values, and in particular allowing a Rust string to be converted into an “OS” string with no cost if possible. A consequence of this is that OsString instances are not NUL terminated; in order to pass to e.g., Unix system call, you should create a CStr.

OsString is to &OsStr as String is to &str: the former in each pair are owned strings; the latter are borrowed references.

Note, OsString and OsStr internally do not necessarily hold strings in the form native to the platform; While on Unix, strings are stored as a sequence of 8-bit values, on Windows, where strings are 16-bit value based as just discussed, strings are also actually stored as a sequence of 8-bit values, encoded in a less-strict variant of UTF-8. This is useful to understand when handling capacity and length values.

这个 OsString 封装了系统的 String,具体可以看这个回答:

Rust为什么会有这么多字符串相似类型? - bombless的回答 - 知乎
https://www.zhihu.com/question/30807740/answer/49521324

  • 你的系统字符串不直接认识 utf-8,需要 OsString 做处理。

库举了 Windows 的例子:https://doc.rust-lang.org/std/ffi/struct.OsString.html#method.to_string_lossy

Path PathBuf

这俩其实是 OsString 的兄弟啦~

CString, raw pointers

最后讲一讲 CString。在 Rust 中数组当然是有长度的,但是在函数间传递的时候一般是传递一个切片,也就是内存中的一个开始地址以及内容的长度,在传递 &str 类型的字符串时原理也是如此。在 C 语言中字符串是不存储长度的,而是以零结尾的字符序列。因此在给 C API 传递字符串时就不能用 Rust 中的字符串了,必须补上末尾的零,因此就有了 CString。

作者:bombless链接:https://www.zhihu.com/question/30807740/answer/49521324来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

MySQL 的字符

Utf8mb4 是很多人 MySQL 踩的坑。Unicode 是一个标准,实际上 MySQL 的 utf8 是假的 utf8,用 1-3 位变长实现。真的 1-4位变长是 utf8mb4.

对于字符的处理抄袭下图:

16a2f479833d3340