3 字符串、切片、二进制列和正则表达式

字符串

形式

字符串一般被单引号''(可以内嵌双引号)、双引号""(可以内嵌单引号)、三重引号(多行,与多行注释的格式一致)包围,并使用\作为转义字符。

letters = 'cnblogs'
letters = '''
I couldn't wait for you to come clear the cupboards
But now you're going to leave with nothing but a sign
Another evening I'll be sitting reading in between your lines
Because I miss you all the time
So, get away
'''
# 实际上,多行字符串可以直接表示为:
letters = 'I couldn\'t wait for you to come clear the cupboards'
'But now you\'re going to leave with nothing but a sign'
'Another evening I\'ll be sitting reading in between your lines'
'Because I miss you all the time'
'So, get away'

切片

用法

包括字符串、列表、元组在内的多个有序可迭代数据类型都支持切片运算。iters[i:j:k]表示可迭代对象下标从ij - 1的步长为k的子列(:k可以省略,默认为1),即遵循左闭右开的原则。
ijk可以是一切整数,即使其小于0,或者大于len(iters)。同时,i可以大于等于j。考虑k>0时:

  1. ij时,切片为空。
  2. ilen(iters)时,切片为空。
  3. j>len(iters)时,切片的有效右边界为len(iters)
  4. ij为负数时,对应表示的下标将从可迭代对象的右侧开始计数,例如iters[-1]表示最后一个元素,iters[-2]表示倒数第二个元素。
  5. i省略时,默认i = 0;当j省略时,默认j = len(iters)
letters = 'python'
arrays = [1, 2, 3, 4, 5]
print(letters[1:2]) # y
print(letters[3:8]) # hon,不会报错,但是右边界为6。
print(letters[2:2]) # ``,不会报错,但输出为空。
print(letters[7:10]) # ``,不会报错,但输出为空。
print(arrays[:]) # [1, 2, 3, 4, 5],可以作为复制对象的方式。
print(arrays[1:]) # [2, 3, 4, 5]
print(arrays[:-1]) # [1, 2, 3, 4]
print(arrays[-4:-2]) # [2, 3]
print(arrays[:-9]) # []

k<0时,表示倒序的步长。

arrays = [1, 2, 3, 4, 5]
print(arrays[::-1]) # [5, 4, 3, 2, 1],可以作为获取倒序对象的方式。

注意点

切片是对原对象的一次“拷贝”,与直接赋值不同(直接将对象的头地址提供给新变量),切片将某一段值“复制”下来并申请的内存空间来存储。因此在切片上修改非引用对象不会导致原对象的修改,在原对象上修改非引用对象也不会导致已有的切片的值。

既然切片是浅拷贝,则说明切片仅仅复制了父对象,对于父对象下的一干子对象都没有复制:

arrays = [1, 2, 3, 4]
lines = ['abcde', arrays, 17, 18] # lines父对象中包含了arrays子对象。
result = lines[1:] # 根据浅拷贝的定义,切片与原对象对arrays的引用仍然是同一个。
print(result) # [[1, 2, 3, 4], 17, 18]
# 对切片中非引用对象的修改只修改了切片,没有修改到原对象。
result.append(19)
print(lines) # ['abcde', [1, 2, 3, 4], 17, 18]
print(result) # [[1, 2, 3, 4], 17, 18, 19]
# 对切片中引用对象的修改涉及到了子对象arrays,浅拷贝并没有拷贝arrays。
arrays.append(5)
print(lines) # ['abcde', [1, 2, 3, 4, 5], 17, 18],导致了原对象的修改。
print(result) # [[1, 2, 3, 4, 5], 17, 18, 19]

运算

字符串的运算支持加法和乘法,表示“连接”和“重复”。

letters1, letters2 = 'abcde', 'fgh'
print(letters1 + letters2) # abcdefgh
print(letters1 * 2) # abcdeabcde

此外有r表达式,表示不做任何处理的字符串:

letters1, letters2, letters3 = r'a\b\c\naa', 'a\b\c\naa', 'a\\b\\c\\naa'
print(letters1) # a\b\c\naa
# letters2被转义:\b->退格,\n->换行,同时由于\c无法转义会抛出警告。
print(letters2) # \c[换行]aa
print(letters3) # a\b\c\naa

方法

Python文档

格式化

Python现版本提供3种常用的字符串格式化方法。

通过%实现格式化

`'%s%%%d' % ('CyberPunk', 2077)` # CyberPunk%2077

通过%?的形式来实现占位符,并以元组的方式顺序表示被格式化的字符串。常见的%?有:

表示 含义
%s 字符串
%d 整数
%u 无符号整数
%o 无符号八进制数
%x 无符号十六进制数
%f 浮点数
%e 科学计数法表示的浮点数
%p 十六进制格式的地址
%% %

通过.format()方法实现格式化

str.format(*args, **kwargs)
'''
执行字符串格式化操作。
调用此方法的字符串可以包含字符串字面值或者以{}括起来的替换域。每个替换域可以包含一个位置参数
的数字索引,或者一个关键字参数的名称。
:return: 字符串副本中每个替换域都会被替换为对应参数的字符串值。
'''
# 使用:
# 以下全部输出CyberPunk 2077。
'Cyber{} {}'.format('Punk', 2077)
'Cyber{1} {0}'.format(2077, 'Punk')
'Cyber{name} {year}'.format(name = 'Punk', year = 2077)
# 此外,可以使用不定长参数的形式向.format()中传递参数。
'Cyber{} {}'.format(*('Punk', 2077))

通过.format()实现数字的格式化:

# 含符号位保留2位小数
'{:.2f}'.format(3.1415926) # 3.14
# 左边补x,宽度为4
'{:x>4d}'.format(314) # x314
# 右边补x,宽度为4
'{:x<4d}'.format(159) # 159x
# {:,}采用逗号,分隔
'{:,}'.format(31415926) # 31,415,926
# {:.n%}n位小数百分比计数
'{:.2%}'.format(0.314) # 31.40%
# {:.ne}n位小数指数计数
'{:.2e}'.format(314159) # 3.14e+05
# {:>nd}n位右对齐、{:<nd}n位左对齐、{:^nd}居中
'{:>10d}'.format(314) # 314
'{:<10d}'.format(314) # 314
'{:^10d}'.format(314) # 314

通过F表达式格式化字符串

F表达式形如f'',在引号中包含被花括号包围的变量名称。

arrays = ['Punk', 2077]
print(f'Cyber{arrays[0]} {arrays[1]}') # CyberPunk 2077
print(f'{arrays[1] + 1 = }') # arrays[1] + 1 = 2078,通过=拼接运算结果。
# 通过:?来格式化小数。
number = 3.14159
f'{number:.2f}' # 保留两位小数。
f'{number:10.2f}' # 宽度为10,精度为2,靠右对齐。
f'{number:010.2f}' # 宽度为10,左侧补0,精度为2。

二进制列bytes

bytes是不可变的二进制列,通常用于处理图像、音视频以及网络编程中。作为字符串类型的高级数据类型,bytes类型的数据支持切片、拼接、查找、替换等许多操作和方法,但是bytes类型中的元素是从0255的整数值,而非Unicode字符,通常表示为b''
一般,通过bytes()函数可以将其他类型的对象转化为bytes类型:

byte_letters = bytes('Python', encoding = 'utf-8')
# 指明编码方式是UTF-8,参数可选,默认为UTF-8。

除此之外,通过encode()decode()函数可以将字符串编码成二进制列或者将二进制列解码成字符串。

str.encode(encoding = 'utf-8', errors = 'strict')
'''
返回编码为bytes的字符串。
:param encoding: 编码规则,可选参数,默认UTF-8。
:param errors: 处理编码错误的方式,可选参数,默认值'strict'会引发UnicodeError。
'''
bytes.decode(encoding = 'utf-8', errors = 'strict')
'''
返回解码为str的字节串。
'''

由于bytes的内容是整数值,因此做相等比较时需要将待比较值转化为数字:

letters = b'Python'
print(letters[0] == ord('P))

正则表达式

标准的正则表达式使用反斜杠字符\表示需要转义的特殊字符。按照此规定,如果在字符串中要匹配'\\',用户在正则表达式中就需要写成'\\\\',但是每个\在Python中必须被表示为\\。因此在Python中,需要使用未作处理的字符串来表示正则表达式:r''

正则表达式的部分特殊字符

字符串

  • .:默认匹配除去\n以外的任意字符。

    如果设定了flag = S,则将匹配任意字符。

  • ^:匹配字符串开头;在M模式下也匹配换行后的首个符号。

  • $:匹配字符串尾或者在字符串尾的换行符的前一个字符,在M模式下也会匹配换行符之前的符号。

    python\njython\n中搜索.ython$,一般会匹配jython;但是在M模式下,可以匹配到python
    python\n中匹配$会找到2个匹配:(换行符之前, 字符串末尾)。

重复次数

  • *:其前的正则式匹配0到任意次重复,并尽量多的匹配字符串。

  • +:其前的正则式匹配1到任意次重复,并尽量多的匹配字符串。

  • ?:其前的正则式匹配01次重复,并尽量多的匹配字符串。

    *+?都采用了贪心思想,尽可能多的匹配字符。例如<.py><.pyc>按照<.*>来匹配将匹配整个字符串。因此在上述3个数量限定符之后在紧跟着?*,将按照非贪婪的方式来匹配:*?+???

  • {m}:其前的正则式指定匹配m次重复;少于m则匹配失败。

    python{6}将匹配pythonnnnnn,但是无法匹配pythonnnnn

  • {m,n}:其前的正则式进行mn次尽量多的匹配。默认m=0,n=+,即mn可以省略。

    {m,n}?{m,n}的非贪心版本。

组合

  • []:表示字符集合。

    特殊字符(不包括转义字符)在[]中将失去特殊意义:例如[p*]将匹配p*[.\n]将匹配.\n

    • [aho]:匹配aho
    • [a-zA-Z0-9]:匹配大小写英文字母和数字0 9
    • [^]:通过^符号取反。当^在起始位置时,正则表达式将匹配不在集合中的字符。例如[^a-z]将匹配不是小写英文字母的字符;[^^]将匹配不是^的字符;[\^]将匹配\^[^]是非法的,将导致re.error
  • |:或运算符。

  • ():正则表达式组合,匹配括号内的任意正则表达式,并标识出组合的开始和结尾。匹配完成提供内容的获取方式。

    • (?):扩展标记法。
    • (?P<name>…):组合匹配到了子字符串可通过符号分组名称name来访问。

    注意:一次正则表达式匹配行为中,贪心模式下重复匹配的格式串对应的结果会被覆盖

    # b = 2。
    # a = 1的结果被覆盖。
    print(findall(r'([a-zA-Z]+ = \d+)+', 'a = 1b = 2'))
    # ['a = 1', 'b = 2']。
    print(findall(r'([a-zA-Z]+ = \d+)', 'a = 1b = 2'))
    # 声明命名param和data。
    result = search(r'((?P<param>[a-zA-Z]+) = (?P<data>\d+))', 'a = 1b = 2')
    # a 1。
    # search()函数仅匹配第一个匹配项。
    print(result.group('param'), result.group('data'))

转义字符

  • \number:匹配数字代表的组合()内容,表示子串的重复,组合编号从1开始。

    import re
    # \1匹配第1个组合匹配成功的内容。
    # [('hijk4', 'lmnopqr7')]。
    print(re.findall(r'([a-z]+[0-4])([a-z]+[5-9])\1', 'abcd4efg3hijk4lmnopqr7hijk4'))
    # []。
    print(re.findall(r'([a-z]+[0-4])([a-z]+[5-9])\1', 'abcd4efg3hijk4lmnopqr7hijk3'))
    # \2匹配第2个组合匹配成功的内容。
    # [('hijk4', 'lmnopqr7')]。
    print(re.findall(r'([a-z]+[0-4])([a-z]+[5-9])\2', 'abcd4efg3hijk4lmnopqr7lmnopqr7'))
    # []。
    print(re.findall(r'([a-z]+[0-4])([a-z]+[5-9])\2', 'abcd4efg3hijk4lmnopqr7lmnopqrst9'))
    # [('hijk4', 'lmnopqr7'), ('op2', 'rstuvw6')]。
    print(re.findall(r'([a-z]+[0-4])([a-z]+[5-9])\1', 'abcd4efg3hijk4lmnopqr7hijk4op2rstuvw6op2'))
    • \number只能用于匹配前99个组合。如果number的第一个数位是0,或者number是三个八进制数,它将不被看作是一个组合,而是八进制的数字值。
    • []字符集合内,任何数字转义都被看作是字符。
  • \A:只匹配字符串开始。
    \Z:只匹配字符串结尾。

  • \b:匹配在单词开始或结尾的空字符串,即\w\W之间的边界,或是\w和字符串开始或结尾之间的边界。

    r'\bpython\b'匹配'python''(python)''java python c',但是不匹配thepython

    \B:匹配不在单词的开始或结尾的空字符串,与\b相反。

  • \d:一般表示[0-9]
    \D:一般表示[^0-9],与\d相反。

  • \s:一般表示[ \t\n\r\f\v],即Unicode空白字符。
    \S:一般表示[^ \t\n\r\f\v],与\s相反。

  • \w:一般表示[a-zA-Z0-9_]
    \W:一般表示[^a-zA-Z0-9_],与\W相反。

  • 其他:正则表达式的用法

正则表达式标志

  • A:使\w\W\b\B\d\D\s\S执行仅限ASCII匹配而不是完整的Unicode匹配。仅对str模式有意义,而被bytes模式忽略。
  • I:忽视大小写的匹配。
  • M:模式字符'^'将匹配字符串的开始和每一行的开头(紧随在换行符之后);而模式字符'$'将匹配字符串的末尾和每一行的末尾(紧接在换行符之前)。
  • S:使'.'特殊字符匹配任意字符,包括换行符。
  • 其他:正则表达式标志

正则表达式函数

  • compile(pattern, flags = 0)
    '''
    编译模式串。
    :param pattern: 模式串。
    :param flags: 标志。
    '''

    当正则表达式可以复用的时候,推荐先使用compile()将其编译成Pattern类型

  • search(pattern, string, flags = 0)
    '''
    扫描字符串string查找正则表达式pattern产生匹配的第一个位置,并返回相应的Match对象。
    :return: 成功查找的Match对象;失败则返回None。
    '''
  • match(pattern, string, flags = 0)
    '''
    如果string开头的零个或多个字符与正则表达式pattern匹配,则返回相应的Match。
    :return: 成功查找的Match对象;失败则返回None。

    注意:即便是M模式,match()也只匹配字符串的开始位置,而不匹配每行开始。

  • fullmatch(pattern, string, flags = 0)
    '''
    如果整个string与正则表达式pattern匹配,则返回相应的Match。
    '''
  • split(pattern, string, maxsplit = 0, flags = 0)
    '''
    用pattern分开string。如果在pattern中捕获到括号,那么所有的组里的文字也会包含在列表里。
    :param maxsplit: 最多进行maxsplit次分隔,剩下的字符全部返回到列表的最后一个元素。
    :return: 分割后的字符串列表。
    :rtype: list。
    '''
    # ['abcd', 'efg', 'h', 'ijkl', 'mnop', 'qr', '']。
    print(split(r'[0-9]+', 'abcd123efg456h78ijkl9mnop012qr566'))
    # ['abcd', 'efg', 'h', 'ijkl9mnop012qr566']。
    print(split(r'[0-9]+', 'abcd123efg456h78ijkl9mnop012qr566'), 3)
  • findall(pattern, string, flags = 0)
    '''
    返回pattern在string中的所有非重叠顺序匹配(包括空匹配)。
    :rtype: list[str] | list[tuple[str]]。
    '''

    re.findall()的返回结果取决于模式中组合的数量:

    1. 如果模式中没有组合,则返回与整个模式匹配的字符串列表list[str]
    2. 如果模式中有且仅有一个组合,返回与该模式组合匹配的字符串列表list[str]
    3. 如果模式中有多个组合,返回与这些组合匹配的字符串元组列表list[tuple[str]]
    import re
    letters = 'abcdefg123hijk456\n\n\t78lmn'
    # ['abcdefg', 'hijk', 'lmn']。
    print(re.findall(r'[a-z]+', letters))
    # ['abcdefg', 'hijk']。
    # 按照[a-z]+[1-9]的规则来匹配字符串,但是返回只考虑组合中的[a-z]+。
    print(re.findall(r'([a-z]+)[1-9]', letters))
    # [('abcdefg', '123'), ('hijk', '456')]。
    print(re.findall(r'([a-z]+)([1-9]+)', letters))
    # [('abcdefg123', 'abcdefg'), ('hijk456', 'hijk')]。
    print(re.findall(r'(([a-z]+)[1-9]+)', letters))
  • finditer(pattern, string, flags = 0)
    '''
    返回pattern在string中的所有非重叠顺序匹配(包括空匹配)。
    :rtype: iter[Match]。
    '''
  • sub(pattern, repl, string, count = 0, flags = 0)
    '''
    :type repl: str | func。
    :param count: 最大替换次数,默认0表示全部替换。
    :return: 使用repl替换在string最左边非重叠出现的pattern获得的字符串;
    如果样式没有找到,则不加改变地返回string。
    '''

    repl参数可以是字符串(包括正则表达式)或函数:

    1. 如果repl是字符串(包括正则表达式),则其中任何反斜杠转义序列都会被处理。

      \n会被转换为换行符;\r会被转换为一个回车符;未知的ASCII字符转义序列被当作错误来处理;其他未知转义序列例如\&会保持原样;\number将用样式中第number组所匹配到的子字符串来替换。

      import re
      # % function name:
      # main
      # repl参数中的\n、\t被正常处理,\1引用main。
      print(
      re.sub(r'def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\):',
      r'% function name: \n\t\1',
      'def main():'
      )
      )
    2. 如果repl是函数(不能使用re.escape()函数),则它针对每次pattern的非重叠出现的情况被调用。该函数接受单个Match参数,并返回替换字符串。

  • escape(pattern)
    '''
    转义pattern中的特殊字符,比如字符串中包含某些诸如'*'、'~'、`.`等正则表达式元字符。
    :return: 转义pattern中特殊字符后的字符串。
    :rtypr: str。
    '''

    该函数不能被用于re.sub()re.subn()的替换字符串

    # abcdefghijklmnopqrstuvwxyz0123456789!\#\$%\&'\*\+\-\.\^_`\|\~:。
    print(re.escape(string.ascii_lowercase + string.digits + '!#$%&'*+-.^_`|~:'))
  • purge()
    '''
    清除正则表达式缓存。
    '''

正则表达式模式对象

class Pattern:
'''
由compile()返回的已编译正则表达式对象。
'''
def search(self, string, pos = 0, endpos = sys.maxsize):
'''
:param pos: 字符串中开始搜索的位置索引,但是实现效果与string[pos:]不完全相等,例如'^'样式字符匹配字符串真正的开头和换行符后面的第一个字符,但不会匹配pos规定的开始位置。
:param endpos: 限定了字符串搜索的结束;如果endpos < pos,则无匹配。
:rtype: Match | None。
'''
# sample = re.compile(#TODO)
# sample.search(string, 0, 100)等价于sample.search(string[:100], 0)
pass
def match(self, string, pos = 0, endpos = sys.maxsize):
'''
:param pos: 字符串中开始搜索的位置索引,但是实现效果与string[pos:]不完全相等,例如'^'样式字符匹配字符串真正的开头和换行符后面的第一个字符,但不会匹配pos规定的开始位置。
:param endpos: 限定了字符串搜索的结束;如果endpos < pos,则无匹配。
:rtype: Match | None。
'''
pass
def fullmatch(self, string, pos = 0, endpos = sys.maxsize):
'''
:param pos: 字符串中开始搜索的位置索引,但是实现效果与string[pos:]不完全相等,例如'^'样式字符匹配字符串真正的开头和换行符后面的第一个字符,但不会匹配pos规定的开始位置。
:param endpos: 限定了字符串搜索的结束;如果endpos < pos,则无匹配。
:rtype: Match | None。
'''
pass
def split(self, string, maxsplit = 0):
pass
def findall(self, string, pos = 0, endpos = sys.maxsize):
pass
def finditer(self, string, pos = 0, endpos = sys.maxsize):
pass
def sub(self, repl, string, count = 0):
pass

正则表达式匹配对象

class Match:
'''
由成功的match()和search()所返回的匹配对象。
'''
def expand(self, template):
'''
获取通过在字符串template上执行反斜杠替换所获得的字符串。
转义符例如'\n'将被转换为适当的字符,而数字反向引用'\1'、'\2'和命名反向
引用'\g<1>'、'\g<name>'将被替换为相应分组的内容。
:type template: str。
:rtype: str。
'''
pass
def group(self, [group1, ...]):
'''
获取匹配后的子组合。
如果只有一个参数,结果是一个字符串;
如果有多个参数,结果是一个元组(每个参数对应一个项);
如果没有参数,第一个组合默认0(整个匹配都被返回)。
:rtype: str | tuple。
'''
pass
def groups(self):
'''
返回一个元组,包含所有匹配的子组,在样式中出现的从1到任意多的组合。
一般等效为通过group()方法读取了所有合法参数的返回值组合得到的元组。
'''
def groupdict(self):
'''
返回一个字典,包含所有的命名子组合,不包含任何未命名子组合。
'''
def start(self, [group]):
'''
返回group匹配到的字串的开始标号(闭区间)。group默认0(整个匹配的子串)。
如果group存在,但未产生匹配,就返回-1。
'''
def end(self, [group]):
'''
返回group匹配到的字串的结束标号(开区间)。group默认0(整个匹配的子串)。
如果group存在,但未产生匹配,就返回-1。
'''
def span(self, [group]):
'''
对于匹配result,返回二元组(result.start(group), result.end(group))。group默认为0,即整个匹配。
如果group没有在这个匹配中,则返回(-1, -1)。
'''

group()方法

  • 如果一个组合参数值为0,相应的返回值就是整个匹配字符串;
  • 如果一个组合参数值属于范围[1,99],结果就是相应的括号组字符串;
  • 如果一个组合参数值是负数,或者大于样式中定义的组数,将引发一个IndexError异常;
  • 如果一个组合包含在样式的一部分,并被匹配多次,将返回最后一个匹配
result = search(r'([a-zA-Z][a-zA-Z0-9]+):(([a-zA-Z]+) = (\d+))+', 'main:a = 1b = 2')
# main:a = 1b = 2。
print(result.group())
# main。
print(result.group(1))
# b = 2。
# 如果一个组合包含在样式的一部分,并被匹配多次,将返回最后一个匹配。
print(result.group(2))
# ('main', 'b')。
print(result.group(1, 3))
# ('main:a = 1b = 2', 'main', 'b = 2', 'b', '2')。
print(result.group(0, 1, 2, 3, 4))
# IndexError: no such group。
print(result.group(0, 1, 2, 3, 4, 5))
# search()只匹配第一个匹配项。
result = search(r'((?P<param>[a-zA-Z]+) = (?P<data>\d+))', 'a = 1b = 2')
# a = 1。
print(result.group())
# a 1。
print(result.group('param'), result.group('data'))

groups()和groupdict()方法

# search()只匹配第一个匹配项。
result = re.search(r'((\?)(?P<param>[a-zA-Z]+) = (?P<data>\d+))', '?a = 1?b = 2')
# ('?a = 1', '?', 'a', '1')。
print(result.groups())
# {'param': 'a', 'data': '1'}。
print(result.groupdict())

start()、end()和span()方法

# search()只匹配第一个匹配项。
result = re.search(r'((\?)(?P<param>[a-zA-Z]+) = (?P<data>\d+))', '?a = 1?b = 2')
# 0。
print(result.start())
# 6。
# 开区间。
print(result.end())
# (0, 6)。
# 左闭右开区间。
print(result.span())

对于一个匹配对象result和一个组合batch,组合batch(等价于result.group(batch))产生的匹配是result.string[result.start(batch):result.end(batch)]
注意:如果batch匹配一个空字符串,将有result.start(batch) == result.end(batch)

result = re.search(r'[a-z]+(\d?)', 'python')
# 0 6 (0, 6)。
# group = 0表示匹配了整个字串。
print(result.start(0), result.end(0), result.span(0))
# 6 6 (6, 6)。
print(result.start(1), result.end(1), result.span(1))
# IndexError: no such group。
print(result.start(2), result.end(2), result.span(2))
posted @   皮皮罴  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示