引言

这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python3.8。为了使开发更简单、快捷,本文使用了JupyterLab。

Python3 明确区分了人类可读的字符串和原始的字节序列。

新内容简介

新增了对emoji表示字符的描述。

字符问题

字符串是个简单的概念:一个字符串是一个字符序列。问题在于字符的定义。

在 2021 年,“字符”的最佳定义是 Unicode 字符。因此,从 Python 3 的str 对象中获取的元素是 Unicode 字符,这相当于从 Python 2 的
unicode 对象中获取的元素,而不是从 Python 2 的 str 对象中获取的原始字节序列。

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

  • 字符的标识,即码位,是 0~1 114 111 的数字(十进制),在Unicode 标准中以 4~6 个十六进制数字表示,而且加前缀“U+”。
  • 字符的具体表述取决于所用的编码。

把码位转换为字节序列的过程是编码;从字节序列转换为码位是解码。

s = 'café'
len(s) # 有4个Unicode字符
4
b = s.encode('utf8') # 使用utf-8编码字符串到字节序列
b # 字节序列以字面量b开头
b'caf\xc3\xa9'
len(b) # 字节序列b有5个字节
5
b.decode('utf8') # 把字节序列解码成str对象
'café'

字节概要

Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变bytes 类型和 Python 2.6 添加的可变 bytearray 类型。

bytesbytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。然而,二进制序列
的切片始终是同一类型的二进制序列,包括长度为 1 的切片。

cafe = bytes('café', encoding='utf_8') # bytes对象可以以字符串构建,指定一个编码
cafe
b'caf\xc3\xa9'
cafe[0] # 每个元素都是range(256)的整数
99
cafe[:1] # bytes的切片还是bytes,哪怕长度为1的切片。
b'c'
cafe_arr = bytearray(cafe)
cafe_arr # bytearray 对象没有字面量句法,而是以 bytearray() 和字节序列字面量参数的形式显示。
bytearray(b'caf\xc3\xa9')
cafe_arr[-1:] # bytearray的切片还是bytearray
bytearray(b'\xa9')

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

  • 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
  • 制表符、换行符、回车符和 \ 对应的字节,使用转义序列\t\n\r\
  • 如果两个字符串分隔符'"都出现在字节序列,那么整个序列由分隔,内部的转义为\’
  • ( 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。

我们看到的是 b’caf\xc3\xa9’:前 3 个字节b’caf’ 在可打印的 ASCII 范围内,后两个字节使用十六进制转义序列。

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

bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

其他构建bytesbytearray实例的方法是通过以下参数调动它们的构造函数:

  • 一个str和一个encoding关键字参数
  • 一个可迭代对象,提供0~255之间的数值
  • 一个实现了缓冲协议的对象(如,bytesbytearraymemoryviewarray.array),可从源对象拷贝字节到新建二进制序列

从一个类缓冲(buffer-like)对象构建一个二进制序列是低层操作,可能涉及类型转换,见示例:

import array
numbers = array.array('h', [-2, -1, 0, 1, 2]) # 类型代码 h 创建一个短整型(16位)数组
octets = bytes(numbers) # octets保存组成numbers字节序列的副本
octets # 通过10个字节表示5个短整型
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'

从任何类缓冲源创建一个bytesbytearray始终会复制源中的字节序列。反之,memoryview对象可以让你在二进制数据结构之间共享内存。

基本编码器/解码器

Python自动超过了100种编解码器(codecs),用于文本和字节之间相互转换。每个codec有一个名称,像utf_8,通常还有别名,比如utf8utf-8U8,你可以在像open()/str.encode()/bytes.decode()中当成encoding参数传入。下面的例子展示了一些文本以三种不同的字节序列编码:

for codec in ['latin_1', 'utf_8', 'utf_16']:print(codec, 'El Niño'.encode(codec), sep='\t')
latin_1  b'El Ni\xf1o'
utf_8   b'El Ni\xc3\xb1o'
utf_16  b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

理解编码/解码问题

虽然有个一般性的 UnicodeError 异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时) 或 UnicodeDecodeError(把二进制序列转换成字符串时)。如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。接下来的几节说明如何处理这些错误。

处理UnicodeEncodeError

大多数非UTF编解码器只处理Unicode字符的一个小子集。当转换文本到字节时,如果在目标编码中为定义某个字符,会抛出UnicodeEncodeError,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。

city = 'São Paulo'
city.encode('utf_8') # UTF编码能处理任意字符串
b'S\xc3\xa3o Paulo'
city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
city.encode('iso8859_1')  # iso8859_1也能处理该字符
b'S\xe3o Paulo'
city.encode('cp437') # cp437不能编码'ã',默认的错误处理器会抛出UnicodeEncodeError
---------------------------------------------------------------------------UnicodeEncodeError                        Traceback (most recent call last)Cell In [17], line 1
----> 1 city.encode('cp437')File D:\program_tool\py39\lib\encodings\cp437.py:12, in Codec.encode(self, input, errors)11 def encode(self,input,errors='strict'):
---> 12     return codecs.charmap_encode(input,errors,encoding_map)UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>
city.encode('cp437', errors='ignore') #  errors=ignore处理器跳过不能编码的字符,这通常会导致数据丢失。
b'So Paulo'
city.encode('cp437', errors='replace') # errors=replace用'?'替换不能处理的字符,也会数据丢失,但是用户知道发生什么
b'S?o Paulo'
city.encode('cp437', errors='xmlcharrefreplace') # xmlcharrefreplace用一个XML实体替换不能编码的字符。
b'São Paulo'

ASCII是我知道的所有编码的常见子集,因此,如果文本完全由 ASCII 字符构成,编码应该始终有效。Python3.7增加了一个新的布尔方法str.isacsii()来检查你的Unicode文本是不是纯ACSII。如果是,那么你应该能将它编码成字节而不会抛出异常。

处理UnicodeDecodeError

并不是每个字节存储了一个有效的ASCII字符,并不是每个字节序列都是有效的UTF-8或UTF-16;因此,把字节序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出 UnicodeDecodeError。

另一方面,许多遗留的8-位编码,像cp1252,iso8859_1koi8_r能解码任意字节流,包括随机噪音,而不报告错误。如果程序使用错误的8位编码,解码过程悄无声息,而得到的是无用输出。

下面的例子展示了使用错误的编解码器可能出现乱码字符或抛出UnicodeDecodeError

octets = b'Montr\xe9al' # 这些字节序列是使用latin1编码的“Montréal”;'\xe9' 字节对应“é”。
octets.decode('cp1252') # 通过cp1252可以解码,因为它是latin1的超集
'Montréal'
octets.decode('iso8859_7') # ISO-8859-7用于编码希腊文,因此无法正确解释 '\xe9' 字节,没有抛出错误。
'Montrιal'
octets.decode('koi8_r') # KOI8-R 用于编码俄文;这里,'\xe9' 表示西里尔字母“И”。
'MontrИal'
octets.decode('utf_8') #  utf_8编解码器检测到octets不是有效的 UTF-8 字符串,抛出UnicodeDecodeError。
---------------------------------------------------------------------------UnicodeDecodeError                        Traceback (most recent call last)Cell In [25], line 1
----> 1 octets.decode('utf_8')UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
octets.decode('utf_8', errors='replace') # 使用replace错误处理器来替换未知字符
'Montr�al'

使用预期之外的编码加载模块时抛出的SyntaxError

UTF-8是Python3默认的编码,Python2默认使用ACSII。如果你加载一个.py模块包含非UTF-8且没有声明编码,你会得到以下信息:

SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line1, but no encoding declared; see https://python.org/dev/peps/pep-0263/for details

为了解决该问题,可以在文件头部增加一个魔法 coding注释:

# coding: cp1252print('Olá, Mundo!')

如何获取字节序列编码

如何获取字节序列的编码?简单来说,不能。必须被告知。

就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。例如,如果 b'\x00' 字 节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因为纯文本中不能包含空字符;如果字节序列 b'\x20\x00' 经常出现, 那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是鲜为人知 的 U+2000 EN QUAD 字符——谁知道这是什么呢!

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

BOM:有用的乱码

你可能注意到了,UTF-16 编码的序列开头有几个额外的字节,如下所示:

u16 = 'El Niño'.encode('utf_16')
u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

这些字节为b'\xff\xfe。这是 BOM,即字节序标记(byte-order mark),指明编码时使用 Intel CPU 的小字节序。

在小字节序设备中,各个码位的最低有效字节在前面:字母 ‘E’ 的码位是 U+0045(十进制数 69),在字节偏移的第 2 位和第 3 位编码为 69 和 0。

list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]

在大字节序 CPU 中,编码顺序是相反的;‘E’ 编码为 0 和 69。

为了避免混淆,UTF-16 编码在要编码的文本前面加上特殊的不可见字 符 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小字节序系统中, 这个字符编码为 b’\xff\xfe’(十进制数 255, 254)。因为按照设计, U+FFFE 字符不存在,在小字节序编码中,字节序列 b’\xff\xfe’ 必 定是 ZERO WIDTH NO-BREAK SPACE,所以编解码器知道该用哪个字节 序。

处理文本文件

处理文本的最佳实践是“Unicode 三明治”(如下图所示)。 意思是, 要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明 治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他 处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符 串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接 触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串; Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。

在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open 函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要 的编码,所以调用 my_file.read() 方法得到的以及传给 my_file.write(text) 方法的都是字符串对象。

open('cafe.txt', 'w', encoding='utf8').write('café')
4
open('cafe.txt').read()
'caf茅'

上面的代码展示了一个bug,可能你的机器没有。
在写入文件时指定了UTF-8编码,但是读取文件时没有这么做。因此Python假定使用系统默认的编码,于是文件的最后一个字节解码成了字符’茅’而不是’é’。

所以打开文件时始终应该明确传入encoding=参数。

fp = open('cafe.txt', 'w', encoding='utf_8')
fp # 默认情况下,open函数采用文本模式,返回一个TextIOWrapper对象
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
fp.write('café') # 在TextIOWrapper对象上调用write方法返回写入的Unicode字符数
4
fp.close()
import os
os.stat('cafe.txt').st_size # os.stat报告文件有5个字节;UTF-8编码的'é'占两个字节
5
fp2 = open('cafe.txt')
fp2 # 打开文本文件时没有显示指定编码,返回一个TextIOWrapper对象,编码是区域设置中的默认值
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
fp2.encoding # 通过encoding属性,可以发现编码为cp969
'cp936'
fp2.read() # 在cp939中,竟然成了'茅'
'caf茅'
fp3 = open('cafe.txt', encoding='utf_8')  # 使用正确的编码打开那个文件
fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
fp3.read() # 结果符合预期
'café'
fp4 = open('cafe.txt', 'rb')  #  'rb' 标志指明在二进制模式中读取文件
fp4 # 返回的是 BufferedReader 对象,而不是 TextIOWrapper 对象。
<_io.BufferedReader name='cafe.txt'>
fp4.read() # 读取返回的字节序列,结果与预期相符
b'caf\xc3\xa9'

当心编码默认值

有些设定可以影响Python中I/O的编码默认值,看下面的代码

import locale
import sysexpressions = """locale.getpreferredencoding()type(my_file)my_file.encodingsys.stdout.isatty()sys.stdout.encodingsys.stdin.isatty()sys.stdin.encodingsys.stderr.isatty()sys.stderr.encodingsys.getdefaultencoding()sys.getfilesystemencoding()"""my_file = open('dummy', 'w')for expression in expressions.split():value = eval(expression)print(f'{expression:>30} -> {value!r}')
 locale.getpreferredencoding() -> 'cp936'type(my_file) -> <class '_io.TextIOWrapper'>my_file.encoding -> 'cp936'sys.stdout.isatty() -> Falsesys.stdout.encoding -> 'UTF-8'sys.stdin.isatty() -> Falsesys.stdin.encoding -> 'gbk'sys.stderr.isatty() -> Falsesys.stderr.encoding -> 'UTF-8'sys.getdefaultencoding() -> 'utf-8'sys.getfilesystemencoding() -> 'utf-8'

在Windows上,输出如上。Unicode在Windows本身和Windows的Python中支持变得更好。PEP 529-更改Windows文件系统编码为UTF-8(也在Python 3.6中实现),该文件将文件系统编码(用于表示目录和文件的名称)从Microsoft的专有MBC更改为UTF-8。

import sys
from unicodedata import nameprint(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()test_chars = ['\N{HORIZONTAL ELLIPSIS}',       # exists in cp1252, not in cp437'\N{INFINITY}',                  # exists in cp437, not in cp1252'\N{CIRCLED NUMBER FORTY TWO}',  # not in cp437 or in cp1252
]for char in test_chars:print(f'Trying to output {name(char)}:')print(char)
3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]sys.stdout.isatty(): False
sys.stdout.encoding: UTF-8Trying to output HORIZONTAL ELLIPSIS:
…
Trying to output INFINITY:
∞
Trying to output CIRCLED NUMBER FORTY TWO:
㊷

locale.getpreferredencoding() 返回的编码是最重要的:这是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。

因此,关于编码默认值的最佳建议是:别依赖默认值。

如果遵从 Unicode 三明治的建议,而且始终在程序中显式指定编码,那将避免很多问题。可惜,即使把字节序列正确地转换成字符串, Unicode 仍有不尽如人意的地方。

排序Unicode文本

Python比较任何类型的序列时,会一个一个地比较序列中的每项。对于字符串,这意味着比较码位(code point)。不幸地是,如果使用非ASCII字符会产生不可接受的结果。

考虑对下面这些生长在巴西的水果进行排序:

fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

排序规则根据locales会产生变化,但在葡萄牙语和很多其他使用拉丁字母的语言中,变音符号(cedillas)很少起到作用。所以’cajá’排序时被当成是’caja’,且会出现在’caju’的前面。排序的水果列表应该是:

['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

排序非ASCII文本的标准做法是使用locale.strxfrm函数,根据locale模块文档,“将字符串转换为可用于区域设置感知比较的字符串”。

为了开启locale.strxfrm,你首先必须为你的应用设置一个合适的locale,并期望操作系统能支持它:

import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)
pt_BR.UTF-8
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

这样,支持该编码的系统中可以得到正确结果。

所以在使用locale.strxfrm前调用setlocale(LC_COLLATE, <your_locale>)是正确排序的关键。不过,有一些注意事项:

  • 因为locale(区域)设置是全局的,所以不建议在库中调用 setlocale。你的应用程序或框架应该在进程启动时设置区域设置,并且不应该在启动后更改它。
  • 该语言环境(locale)必须安装在操作系统上,否则setlocale会抛出错误:unsupported locale setting exception.
  • 你必须知道如何拼写语言环境名称
  • 该语言环境必须由OS的制造商正确实现

幸运地是,有一个更简单的解决方法:pyuca包。

使用Unicode排序算法进行排序

pyuca,一个纯Python实现的Unicode排序算法(Unicode Collation Algorithm):

!pip install pyuca
Collecting pyucaDownloading pyuca-1.2-py2.py3-none-any.whl (1.5 MB)---------------------------------------- 1.5/1.5 MB 5.8 MB/s eta 0:00:00
Installing collected packages: pyuca
Successfully installed pyuca-1.2
import pyuca
coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

这种方法简单,并且在Linux,MacOS和Windows上都能用。

pyuca没有考虑语言环境。如果需要自定义排序,可以向Collator()构造函数提供自定义排序规则表的路径。

Unicode数据库

Unicode标准提供了一个完整的数据库——以几个结构化的文本文件的形式——不仅包括映射码位指向字符名称的表,还包括有关单个字符及其它们如何关联的元数据。比如,Unicode数据库记录字符是可打印字符、字母、十进制数字还是其他数字符号。这就是str方法isalphaisprintableisdecimalisnumeric的实现原理。str.casefold也使用了Unicode表中的信息。

通过名称找打字符

unicodedata模块提供了遍历字符元数据的函数,包括unicodedata.name(),它返回某个字符的官方名称:

from unicodedata import name
name('A')
'LATIN CAPITAL LETTER A'
name('ã')
'LATIN SMALL LETTER A WITH TILDE'
name('♛')
'BLACK CHESS QUEEN'
name('												

《流畅的Python第二版》读书笔记——文本和字节序列相关推荐

  1. 读书笔记:《流畅的Python》第4章 文本和字节序列

    # 第四章 文本和字节序列"""内容提要:1.Unicode字符串2.二进制序列3.在二者之间转换使用的编码4.字符/码位/字节表述5.bytes/bytearray/m ...

  2. 《流畅的python》第四章 文本和字节序列

    U1. 把码位转换成字节序列的过程是编码(encode),把字节序列转换成码位的过程是解码(decode). 2. bytes和bytearray对象的各个元素是介于0--255之间的整数. 3. 结 ...

  3. 《流畅的Python第二版》读书笔记——函数作为一等对象

    引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 函数是Python的一等( ...

  4. 《流畅的Python第二版》读书笔记——函数中的类型注解

    引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 本章关注于Python在函 ...

  5. python【进阶】4.文本和字节序列

    文章目录 1. 字符.码位和字节表述 4.1字符问题 2. bytes.bytearray 和 memoryview 等二进制序列的独特特性 3. 全部 Unicode 和陈旧字符集的编解码器 4.避 ...

  6. Python核心教程(第二版)读书笔记(三)

    第三章Python基础 2010-04-09 换行  一行过长的语句可以使用反斜杠'\'分解成几行.有两种例外情况一个语句不使用反斜线也可以跨行. 1.在使用闭合操作符时,单一语句可以跨多行.例如:在 ...

  7. 【我的JS第三本】JavaScript_DOM编程艺术第二版读书笔记

    经过前一段时间HTML&CSS的学习,感觉视频加读书是一个比较不错的学习方法,两者相辅相成,互相补充,所以也准备看看关于JavaScript的书. 2015年12月14日,之前使用韩顺平老师的 ...

  8. 《细说PHP》第二版--读书笔记

    第五章 PHP的基本语法 5.2.4 在程序中使用空白的处理 5.3 变量 5.3.1 变量的声明 在php中变量的声明必须是使用一个$符号,后面跟变量名来表示 unset()函数释放指定变量 iss ...

  9. 深入理解JVM(第二版读书笔记)

    一  开始前 HotSpot:http://xiaomogui.iteye.com/blog/857821 http://blog.csdn.net/u011521890/article/detail ...

  10. 刘鹏老师和王超老师的计算广告第二版读书笔记

    广告的定义与目的 广告的基本概念 广告的分类 在线广告的表现形式 横幅广告 文字链广告 富媒体广告 视频广告 交互式广告 社交广告 移动广告 邮件营销广告 广告的基本概念 需求方:可以是广告主.代表广 ...

最新文章

  1. 《Spring Boot极简教程》附录4 Java编程简史
  2. 注册中心ZooKeeper、Eureka、Consul 对比
  3. python数据分析报告主题_【原创】python主题LDA建模和t-SNE可视化数据分析报告论文(代码数据)...
  4. atitit。wondows 右键菜单的管理与位置存储
  5. QT [004] QT SDK 和 QT quick 和 QT creator的区别历史和沿袭
  6. PL/Sql快速执行 insert语句的.sql文件
  7. 并发框架disruptor(高性能内存Queue)
  8. python 爬虫,起点完结榜前100榜单
  9. P1603 斯诺登的密码-字符串加法的妙用
  10. 图像语义分割(12)-重新思考空洞卷积: 为弱监督和半监督语义分割设计的简捷方法
  11. FPGA设计之首——Altera FPGA 选型及官网文档阅读
  12. Linux 异常:The following signatures couldn‘t be verified because the public key is not available
  13. 用户发送的eth值msg.value,在合约中以wei为单位。
  14. 计算机编程音乐,扒取网易云歌单音乐
  15. 模电设计学习笔记(二)——反相放大电路
  16. 小红书如何营销?各大品牌小红书运营投放营销策划方案合集(13份)
  17. 最牛训犬师,专治拆家打架咬人,20多年搞定2000多条狗
  18. 项目管理中软件项目文档的分类管理
  19. 有趣的数学问题-鸽巢原理
  20. 0. Office Web Apps简介

热门文章

  1. 用java输出一个心型图案_开发工程师的浪漫--java打印心形图案
  2. Spring - 事件监听机制 源码解析
  3. iphone开发笔记一 mac os 10.7.2安装与配置
  4. java structs教程_Java教程 实战学习Struts实例
  5. 模拟将本地文件上传至外服务器
  6. 面试系列(九):商汤科技 深度学习平台C++研发
  7. 淘宝客小程序制作(3)-API编写及部署
  8. 三星s8 android版本,三星S8有几个版本?三星S8和三星S8+(plus)各个版本详细区别对比评测...
  9. Reinforcement Learning——Chapter 2 Multi-armed Bandits
  10. 「吕本富」交易的四个阶段