首页 > 编程语言 >《流畅的Python第二版》读书笔记——文本和字节序列

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

时间:2023-08-02 23:31:39浏览次数:48  
标签:编码 字节 字符 Python 读书笔记 sys Unicode 序列


引言

这是《流畅的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 line
  1, but no encoding declared; see https://python.org/dev/peps/pep-0263/
  for details

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

# coding: cp1252

print('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第二版》读书笔记——文本和字节序列_ico

在 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 sys

expressions = """
        locale.getpreferredencoding()
        type(my_file)
        my_file.encoding
        sys.stdout.isatty()
        sys.stdout.encoding
        sys.stdin.isatty()
        sys.stdin.encoding
        sys.stderr.isatty()
        sys.stderr.encoding
        sys.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() -> False
           sys.stdout.encoding -> 'UTF-8'
            sys.stdin.isatty() -> False
            sys.stdin.encoding -> 'gbk'
           sys.stderr.isatty() -> False
           sys.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 name

print(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-8

Trying 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 pyuca
  Downloading 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,读书笔记,sys,Unicode,序列
From: https://blog.51cto.com/greyfoss/6944530

相关文章

  • 使用python进行贝叶斯统计分析|附代码数据
    原文链接:http://tecdat.cn/?p=7637最近我们被客户要求撰写关于贝叶斯统计的研究报告,包括一些图形和统计输出。本文讲解了使用PyMC3进行基本的贝叶斯统计分析过程. ( 点击文末“阅读原文”获取完整代码数据******** )。  #Importsimportpymc3aspm#python的概率......
  • 贝叶斯网络python实战(以泰坦尼克号数据集为例,pgmpy库)
    贝叶斯网络python实战(以泰坦尼克号数据集为例,pgmpy库)leida_wt 2019-03-2423:05:36  16815  收藏 140分类专栏: 机器学习 文章标签: pgmpy 贝叶斯网络 泰坦尼克 机器学习 图网络版权 文章目录贝叶斯网络简介贝叶斯推断思路贝叶斯网络贝叶斯网络的实现应用步骤泰坦尼克......
  • Python教程(6)——Python变量的基础类型。|整数类型|浮点数类型|字符串类型|布尔类型|
    学习编程语言,不得不忽视变量这个概念。Python中的变量是用于存储数据的名称,你可以将值赋给变量,并在程序的其他地方使用该变量来引用该值。变量在程序中起到存储和操作数据的作用。如果学过C/C++语言的同学,定义了变量后,需要加个类型的限制,比如intage=28doublemoney=10.2......
  • process explorer 可以查看进程的网络通信情况 收发字节数
      网络里可以查看的选项:  DLL里可以看到: ......
  • 《深度管理》读书笔记1
    书籍信息参考:https://book.douban.com/subject/30258984/ 一 “赢得漂亮”意味着你能长时间地维持优秀的业绩,因为你拒绝屈服于严酷而招致压力的捷径,暂时将他人当成业绩的牺牲品。你需要让精力充沛、鼓足干劲的人们共同工作。你的策略的强大取决于你的团队在第一线的执行力,要......
  • 高效Python-2-1 剖析(Profiling 性能分析)
    2从内置功能中获取最高性能本章包括剖析代码以发现速度和内存瓶颈更有效地利用现有的Python数据结构了解Python分配典型数据结构的内存成本使用懒编程技术处理大量数据有很多工具和库可以帮助我们编写更高效的Python。但是,在我们深入研究提高性能的所有外部选项之前,让我......
  • 反射机制--python
    引用:https://www.cnblogs.com/vipchenwei/p/6991209.htmlhttps://www.cnblogs.com/vipchenwei/p/6991209.html 1.反射是什么:反射就是通过字符串的形式,导入模块;通过字符串的形式,去模块寻找指定函数,并执行。利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)......
  • python-glob查找特定文件
    python-glob查找特定文件目录python-glob查找特定文件只用到三个匹配符glob.globglob.iglob案例当前路径下所有py文件当前路径文件tmp下py文件glob模块可以查找当前脚本目录(相对路径)下或者某一指定目录(绝对路径)符合特定规则的文件路径名glob文件名模式匹配,不用遍历整个目录判......
  • python-sys模块
    python-sys模块目录python-sys模块sys.versionsys.argvsys.platformsys.modulessys.path参考资料"sys"即"system","系统"。该模块提供了一些接口,用于访问Python解释器自身使用和维护的变量,不是主机操作系统。sys.argv #获取命令行参数列表,第一个元素是程序本身sys.ex......
  • Python基础day57 Django模板继承和模型层
    模板之标签就是在模板里面使用流程控制:if、else、elseif、for标签看起来是这样的:{%tag%}for标签{%forpersoninperson_list%}{{forloop}}<p>{{person.name}}</p>{%endfor%}{%forpersoninperson_list%}{#判断list是否有值,没有就走empty#}......