• 在线客服

  • 扫描二维码
    下载博学谷APP

  • 扫描二维码
    关注博学谷微信公众号

  • 意见反馈

原创 Python异常处理

发布时间:2021-08-13 18:55:16 浏览 4519 来源:博学谷 作者:逍遥老师

    课程导学

    哈喽,这位同学你好啊,我是逍遥老师。本节课程我们来一起聊聊 -- Python异常处理

     

    不知道你在日常生活中有没有遇到过下面这种情况?

     

    假设我们每周都会在操场跑步5公里,来锻炼身体。

     

    正常情况下,我们可以按照自己习惯的速度,顺利的跑完全程,然后回到家里休息。然而,可能偶尔会有“意外”发生。比如:跑着跑着,突然下暴雨了... ...那么我们就不能继续跑步了。

         跑步

     

    又或者:某次跑步突然身体不适,那我们可能会停下来,休息一下,等待身体恢复之后,继续跑,又或者实在太不舒服,直接放弃了跑步,选择休息。

     休息

     

    除此之外,可能还会有其他各种情况,都会导致我们正在进行的跑步活动,临时中断。我们需要解决掉出现的问题后,才可以继续完成跑步。严重的情况下,例如下冰雹或者临时有其他事情,我们可能会直接放弃本次跑步计划。这,就是我们生活中,遇到的“异常”。

     

    同样的,我们的计算机程序在运行时,有时也会由于各种不可控的原因出现错误。例如:找不到需要的文件、网络错误、用户输入不对等等。这时,计算机程序就会出现无法继续运行的情况,甚至会直接退出。比如:我们常见的手机APP闪退,网站出现500错误,或者Windows里的如下错误:

     出现错误

     

    相信大家遇到这样的情况,都很恼火吧?但计算机程序也是由技术人员编写的,这种情况,是他们也不能处理的“异常情况”。

     

    对于用Python编写的程序。当 Python 检测到一个错误时,解释器无法继续执行,反而出现了一些错误的提示,这就是所谓的"Python异常"。因此,为了让我们的程序可以稳定地长久运行,你需要知道如何正确处理异常。

     

    在本次课程中,你将会学到:

    Python中的异常是什么

    如何捕获和处理异常

    异常是如何传递的

    怎样自定义和抛出异常

     

    学完本节课程,你可以独立进行异常处理,让自己的程序长久稳定运行。即使遇到错误,也可以正确处理并快速恢复正常运行。

    第一关 Python异常简介

    1. Python程序中的异常

    前面说了那么多,那Python异常到底是怎样的呢?别急,我们先来看看Python程序中的异常是怎么呈现的。

     出现异常

    如上图所示,当我们想要运行python程序文件 test.py 时,在终端键入python test.py并按下回车之后,屏幕上在“-----test--1---”之后 ,会出现的一串信息:

     

    Traceback (most recent call last):

      File "C:/workspace/boxuegu/exception/test.py", line 2, in <module>

        open('123.txt','r')

    FileNotFoundError: [Errno 2] No such file or directory: '123.txt'

     

    这个就是Python异常信息。

     

    图片中这段异常,意思是说:文件test.py在第二行出现了错误,这是一个编号为2的“输入输出型”的错误,名叫“123.txt”的文件不存在。

     

    那么为什么会出现这样的异常呢?

    我们来看一下这叫做test.py的python程序文件的源代码长啥样:

    print('-----test--1---')

    open('123.txt','r')

    print('-----test--2---')

     

    对应着运行结果,我们看到,原本程序在open()打开文件操作之后,还要打印”-----test--2---“,但结果却没有执行打印”-----test--2---“这个动作。

    这是因为,我们在使用open()函数尝试打开一个不存在的文件“123.txt”,当找不到“123.txt” 文件时,程序就会抛出给我们一个“FileNotFoundError 类型的错误,No such file or directory:123.txt”(没有 123.txt 这样的文件或目录)。

     

    当 Python 检测到一个错误,并且无法处理时,就会出现一些错误的提示,这就是所谓的"异常"。

     

    如果一直没有对异常进行处理,那么程序就会强制退出执行。如同我们前面的例子,当你想要跑步时,突然下起了大雨,你还没有雨伞,那原本的跑步任务就会强制终止。

     

    常见的异常及继承结构

    在 Python 中,异常会通过一个对象(object)来表示,对象中存储着关于异常的信息。我们在编程时,经常遇见的一些异常有:

     常见异常

     

    除上述常见异常,在Python 3中,还内置了其他种类的异常,在这里我们列举部分例子,你可能不会经常遇到它们:

     其他异常

     

    完整的Python 3内置异常,你可以在Python官网查询到:

    https://docs.python.org/3/tutorial/errors.html

     

    由上述讲解可以看到,每个异常对应着一种特殊的错误场景,如同我们的例子中,意外中断跑步的原因可能有很多种:下雨、生病、临时事务、场地问题等。不同的原因,导致的结果以及应对的方法可能会有所不同。比如下雨,我们可以等5分钟看雨是否停下,也可以穿戴雨衣。而突然的生病或者受伤,就不得不中止跑步了。

     

    在前面的Python异常命名中,我们发现:Python将异常以类class的形式定义。我们已经学过Python的面向对象特性。因此,我们知道,具体的异常,都是这些类所创建的示例instance

     

    在Python中,所有的异常和错误,都继承自 BaseException 这个基类。但在实际开发中,我们还会更多地与Exception这个父类打交道。Exception 继承自BaseException,实现了我们处理异常中需要的常见方法。Python内置的异常,也大多继承自Exception,并以自身的异常描述,作为名字,命名规则往往为AbcException或XyzError这样。

     

    同样,我们用跑步任务作为例子,我们可能遇到 天气状况,而天气状况,又分为几种情况:下小雨、下大雨、沙尘、雾霾等。因此,它们都可以认为是天气状况的子类。类似的,身体原因,也可以作为一个父类,具体到岔气、膝盖受伤等情况,则可以视作身体原因的子类。我们可以针对所有的天气原因做同样的处理,例如:中断跑步;也可以针对不同身体原因,做不同的处理,比如:遇到岔气情况,就休息5分钟再跑。这跟程序中的异常处理,是很类似的。

     

    第二关 异常的处理

    1. 导言

    在上一关我们一起认识了什么是异常,什么情况下程序会出现异常。我们知道了异常会带来严重的问题,例如:导致程序中断。甚至,有的异常是不可避免的,并且有那么多不同种类的异常,这都要怎么处理呢?

     

    别担心,Python为我们提供了简便的异常处理方式。如同我们的例子,跑步时出现了下雨、伤病、临时有事等情况,我们当然不能继续无脑的跑下去。而应该去了解和应对这些情况,让我们下一次跑步时,不受到任何影响,从而实现长期通过跑步锻炼身体的目标。这也是我们本章要重点学习的内容。

     

     

    2. 捕获异常

    首先,作为计算机,它需要我们告知它,哪里可能会有异常,以及会有什么样的异常,即异常的捕获

     

    在 Python 中,与其它语言类似,我们会使用 try...except...捕获异常 。

     异常捕获

    先看如下代码示例:

    try:

        print('-----test--1---')

        open('123.txt','r')

        print('-----test--2---')

    except IOError:

        pass

     

    学员运行

     

    发现有什么不同的了吗?

     

    第二段“-----test--2---”同样没有打印出来,程序却正常退出了。这是因为,这里我们用 try 和 except 捕获到了 IOError 异常,并添加了处理的方法。因此,print('-----test--2---')语句并没有被执行。

     

    我们通过try 关键字,包含逻辑代码,表示在这里可能会出现异常,需要处理。

    程序执行到某条语句时如果发生了异常,则不会继续执行,而是立刻寻找跟随try的except语句,并进入except包含的代码中执行。这就是异常的捕获。

     

    而except 关键字里的 pass 表示虽然捕获了异常,但什么也不做;如果把 pass 改为 print 语句,那么就会输出其他信息。也就是说,这里也可以执行其他我们想要的逻辑,比如断开连接、关闭文件、释放资源、记录日志等常见的异常处理逻辑。

     

    这就是 Python 的异常捕获处理。

     

     

     

    总结一下,try...catch...的使用方法如下:

    把可能出现问题的代码,放在 try 中

    把处理异常的代码,放在 except 中,异常发生时,系统会中断后续的代码执行,而进入except内的代码。

     

    是不是挺简单的?

     

    3.except 捕获多个异常

    在我们的跑步例子中,我们会遇到天气不好、伤病等多种异常情况。那Python可以捕获多种异常吗?答案当然是肯定的。

     

    首先,我们知道,根据传递给except的异常类型,我们可以指定捕获处理特定的某一种异常,如下

    try:

        print(num)

    except IOError:

        print('产生错误了')

    上面的程序,我们已经使用 except 来捕获异常了,但为什么还会看到错误的信息提示呢?

     

    结果如下:

     结果

     

    答案在于:except 捕获的错误类型是 IOError,而此时程序产生的异常为 NameError,所以 except 并没有生效。

     

    于是我们将代码稍作修改,如下:

    try:

        print(num)

    except NameError:

        print('产生错误了')

    再次运行后的结果为:

     出现错误

     

    可以看到,将 IOError 更改为 NameError后,就可以顺利进入到 except 语句里面捕获到异常了。由此,我们知道,在 except后 需要对应正确类型的异常,才会进入到相应的错误处理代码中。

     

    除了捕获单个异常,我们还可以捕获多个异常,在实际开发中,这种情况很常见。Python 对此也提供了很好的支持,看下面的例子:

    #coding=utf-8

    try:

        print('-----test--1---')

        open('123.txt','r') # 如果 123.txt 文件不存在,那么会产生 IOError 异常

        print('-----test--2---')

        print(num)# 如果 num 变量没有定义,那么会产生 NameError 异常

     

    except (IOError,NameError):

        #如果想通过一次 except 捕获到多个异常可以用一个元组的方式

    可以看到,当捕获多个异常时,是可以把要捕获的异常的类型名字,放到 except 后,并使用元组的方式进行传递的。这样无论是 IOError,还是 NameError,都会进入到 except 的代码块中。

     

    4. 获取异常的信息描述

    在捕获到异常时,除了中断程序,请想一想,我们还会经常做哪些处理呢?

     

    最常见的是:打印或者用日志记录下异常的信息,以便让我们通过输出结果,后续查看异常的内容。

     

    前面我们已经提到,Python中的异常,是通过类来定义的,因此,我们需要获取到异常的类实例,才能访问异常的信息。请记住:通过 as 关键字,我们就可以在 except 的语句内使用异常对象实例,如下:

    try:

        print(a)

    except NameError as result:

        print(result)

     

    在上面的例子中,result 是 NameError 的实例化对象,我们可以直接在 except 代码块内,将它打印出来,则程序会输出错误的描述。这跟我们自己编写代码遇到错误时系统的错误输出内容是一样的。

     

    对于多个异常,也可以使用类似的方法:

     

     

    try:

        open('a.txt')

    except (NameError, IOError) as result:

        print("哎呀,捕获到了异常")

        print("异常的基本信息是: ", result)

     

    运行结果如下:

     运行结果

    这样,我们就可以将错误信息,记录到日志或者邮件通知等其他输出源,让我们更好地维护系统。

     

    5.分别处理不同异常

    在跑步的例子中,我们可能会有这样的要求:如果是阵雨,跑步暂停,我们躲雨几钟,等雨停了之后继续跑步;如果遇到岔气,我们休息5分钟,等内脏肌肉状态恢复,继续跑步;但如果是膝盖疼痛等伤病情况出现,我们就放弃本次跑步,回家休息。也就是说,我们针对不同的异常情况,选择了不同的处理方式。是不是更符合真实逻辑呢?

     

    在我们写Python代码时,一定也会遇到类似的情况,对不同类型的异常,进行不同逻辑的处理,要怎么实现呢?

     

    很简单,except 的用法和逻辑判断关键字 if 的用法很类似,我们可以用多个 except 处理多种不同的异常,根据传递的异常类型,进入不同的后续代码逻辑,如下:

    try:

        print(num)

    except NameError:

        print('产生了命名错误')

    except IOError:

        print('产生了文件 IO 错误')

    except:

        print('出现了其他异常')

    在上面的代码中,当 try 包含的程序出现了 NameError 时,会进入到第一个 except 中,输出“产生了命名错误”,而当 try 中的程序出现了 IOError 时,则会进入到第二个 except 中,输出“产生了文件 IO 错误”。而如果出现了非上述两种异常的其他异常时,则会进入到最后一个 except 中。

    请注意:它后面是没有指定任何异常类型的。

     

    因此,我们可以用except做到用不同的方式,处理不同类型的异常错误。这是不是和 if 语句的多个条件很像呢?

     

    6.使用 else 处理非异常情况

    前一节中,我们说except很像if,可以传入不同的异常类型作为条件。那么,有没有跟else类似的用法呢?当然有!而且是完全相同的用法!

     

    类似if...else...的用法,我们也可以使用 else,和 except 进行搭配!当没有发生异常时,进入 else 后面的代码继续运行。

    try:

        num = 100

        print(num)

    except NameError as result:

        print("捕获到了异常, 信息是: ", result)

    else:

        print("程序正常运行,没有异常。")

    运行结果

     

     

    是不是和我们之前学过的逻辑表达式if... else...很像?

    请注意:else指的是没有异常时,进入的逻辑,而并非没有捕捉到异常时,代码进入的逻辑

     

    因此,如果上面的代码里,程序发生了NameError之外的其他类型的异常,而没有被捕捉到,except语句和else语句都不会被执行,而程序会因为异常而退出。

     

    注意:Python与其他语言不一样,它认为使用try...except...else可以更清晰地描述逻辑控制流程,而其他语言例如Java,则没有else这种控制流关键字。

    7 .try... finally...

    这部分,我们同样用跑步的例子打比方:我们无论是跑完全程还是中途停止,都需要慢走几分钟,来降低心率、喝几口水来补充水分,让身体缓慢恢复,是不是这样呢?

     

    作为程序,也是一样, 比如文件关闭,释放锁,把数据库连接返还给连接池等。try...finally...语句就是用来处理这样的情况的。在程序中,如果一段代码必须要执行,即无论异常是否产生都要执行,那么此时就需要使用 finally,将这段代码包裹起来。

     

    我们看下面的例子:

    import time

    try:

        f = open('test.txt')

        try:

            while True:

                content = f.readline()

                if len(content) == 0:

                    break

                time.sleep(2)

                print(content)

        except:

            #如果在读取文件的过程中,产生了异常,那么就会捕获到

            #比如 按下了 ctrl+c

            pass

        finally:

            f.close()

            print('关闭文件')

    except:

        print("没有这个文件")

    test.txt 文件中每一行数据打印,我们故意在每打印一行之前用 time.sleep 方法暂停 2 秒钟。这样做的原因是让程序运行得慢一些。在程序运行的时候,按 Ctrl+c 中断(取消)程序。

     

    这样,我们就可以观察到 KeyboardInterrupt 异常被触发,程序退出。但是在程序退出之前,finally 从句仍然被执行,把文件关闭。这样就确保了无论程序是否正确运行,都不会阻塞如文件、网络、内存等一些公共资源,提高了系统的稳定性和代码逻辑的严密性。

    8.小结

    经过本章的学习,我们知道,异常捕获和处理主要由 try/except/else/finally 几部分构成,请你一定要牢牢记住下面的代码示例:

     

    try:

         # 正常的代码逻辑

    except A:

         # Exception A 的处理逻辑

    except B:

         # Exception B 的处理逻辑

    except:

         # 其他异常的处理逻辑

    else:

         # 如果没发生异常,执行这里

    finally:

         # 无论是否发生异常,最后执行这里

    #    

    Python的异常处理看似复杂,实际上只要记住上面的总结规律即可。

     

     

    问题:以下哪个组合是错误的搭配

    A. try... except...                            

    错误答案:try...except...是最基本的异常捕获及处理方式

    B. try... else...                                

    正确答案:  else必须跟在一个except之后,因此这是错误的用法

    C. try ... except...finally...               

    错误答案:在try...except...基础上加上finally,可以确保无论是否发生异常都执行finally里面的代码

    D. try... finally...

    错误答案:不使用except表示不处理异常,但finally内的代码仍会被执行,而异常会向上抛出

     

    第三关 异常的传递

    1. 导言

    在上一章中,我们学习了使用try来捕获异常,并学习了使用except等方式,对异常进行处理。而有时候,我们的代码可能被封装在某个函数体中,这个函数又被其他的函数所调用,以此类推,会产生多层函数调用栈。这时,就可能会出现内部的函数没有足够的信息去正确处理异常。

     

    再次拿我们跑步的例子来说明,我们人脑指挥身体开始跑步,身体指挥手部摆手,腿部跑动。这时如果膝盖感受到了疼痛,膝盖实际上并不知道该怎么办,于是传递给身体,通过脑电波告诉跑者,膝盖发生了疼痛,跑者这时才决定暂停跑步。

     

    这就涉及到一个问题——异常的传递,异常需要从初始发生的地方,告知它的调用者,从而做出正确的异常处理。你可能觉得会很复杂,实际上,Python的异常传递非常简单直观,let's go!

     

    try 嵌套传递

    在Python中,如果我们不使用 except 去处理异常,那么异常就会向“外层”传递。

     

    第一种传递过程是向 try 语句外层传递,如果外层有另外一个 try 语句嵌套,则外层的 try 会捕获到这个异常。看下面的例子:

    import time

    try:

        f = open('test.txt')

        try:

            print(num)

        finally:

            f.close()

            print('关闭文件')

    except:

        print("发生了错误")

     

    当这段代码文件同一目录下没有'test.txt'这个文件时,我们运行会得到如下结果:

     结果

    当'test.txt'存在时,运行上述代码,我们会看到下面的结果:

     出现结果

    可以看到,第二个 try 语句打印不存在的变量,会产生 NameError,在执行完 finally 语句内的关闭文件后,异常被外层嵌套的 try 语句对应的 except 捕获到。

     

    总结一下:对于 try 包含的代码,如果没有用 except 去捕获异常,则异常会向外层传递,直到遇到下一层的 try 和 except。

     

    3.函数调用传递

    前面的代码,是不是看起来有点啰嗦?

     

    使用多个try嵌套异常,是我们比较少会书写的代码。而在实际的项目中,我们更常见到的情况是:在函数的多级调用中,被调用的函数出现了异常。

     

    与前面的例子类似,如果在函数中没有使用 except 捕获异常,它会向函数的调用方逐层传递,直到遇到 try 和对应的 except 将其捕获。参考下面的例子:

    def test1():

        print("----test1-1----")

        print(num)

        print("----test1-2----")

     

     

    def test2():

        print("----test2-1----")

        test1()

        print("----test2-2----")

     

     

    def test3():

        try:

            print("----test3-1----")

            test1()

            print("----test3-2----")

        except Exception as result:

            print("捕获到了异常,信息是:%s" % result)

     

        print("----test3-2----")

     

     

    test3()

    print("------华丽的分割线-----")

    test2()

    print("这里永远也不会被执行")

    运行后结果如下:

     运行结果

     

    可以看到,第一次调用 test3(),test1()中产生的异常,由 test3()中的 try...except...捕获到,并打印出错误的信息。系统继续执行,第二次 test2()中没有做异常处理,则 test1()的异常向上传递,直至没有代码对它进行处理,程序最后运行中断。

    这就是函数调用传递,需要你理解记忆。

    第四关 自定义异常

    使用 raise 抛出自定义异常

    我们在跑步中,也可能没遇到任何问题,就单方面的由于“心情不好”而中断了本次跑步。因此我们需要自己去创建“心情不好”这个异常类,并对它进行捕获和处理。那这时候要怎么做呢?又或者我们相对某种特定的异常,需要进行统一的处理、或是描述,又要怎么实现呢?

     

    这就需要我们来自定义自己的异常了。

     

    我们已经学习了如何捕获且处理异常,同时,我们也可以针对自己写的代码逻辑,主动向外层调用者传递异常,也叫做”抛出异常“,以告诉别人程序产生了错误。

     

    我们已经知道,所有的异常,都是 Python 内置的 Exception 类的子类,因此我们可以定义一个继承自 Exception 的类,作为自定义异常,如下:

    class ShortInputException(Exception):

        '''自定义的异常类'''

        def __init__(self, length, atleast):

            #super().__init__()

            # 我们可以向类中添加属性

            self.length = length

            self.atleast = atleast

     

    在 Python 中,我们可以用 raise 语句来抛出一个异常,继续完成我们的例子:

    class ShortInputException(Exception):

        '''自定义的异常类'''

        def __init__(self, length, atleast):

            #super().__init__()

            # 我们可以向类中添加属性

            self.length = length

            self.atleast = atleast

     

    def main():

        try:

            s = input('请输入 --> ')

            if len(s) < 3:

                # raise 引发一个你定义的异常

                raise ShortInputException(len(s), 3)

        except ShortInputException as result:#x 这个变量被绑定到了错误的实例

            print('ShortInputException: 输入的长度是 %d,长度至少应是 %d'% (result.length, result.atleast))

        else:

            print('没有异常发生.')

     

    main()

    运行结果如下:

     结果如下

     

    在上述代码中,

    首先,我们自定义了一个异常类 ShortInputException,它继承了 Exception 父类;

    然后,当我们在代码中使用 raise ShortInputException 抛出异常,在随后的 except 中,我们指定接收异常的类型为 ShortInputException,就可捕获到它;

    最后,我们通过 as 操作符,将异常对象赋值给 result 变量。

    这样我们可以在 except 的主体内,访问到它的 length 和 atleast 属性。

     

    请注意:以上程序中,关于代码#super().__init__()的说明:

    这一行代码,可以调用也可以不调用。

     

    但这里逍遥老师还是建议你调用。

    因为__init__方法一般是用来对创建完的对象进行初始化工作,如果在子类中重写了父类__init__方法,这就意味着父类中的很多初始化工作没有做,这样就不保证程序的稳定了。所以你在以后的开发中,如果重写了父类的__init__方法,最好是先调用父类的这个方法,然后再添加自己的功能。

     

     

    第五关 异常的应用与总结

    1. 异常的应用

    怎么样,以上就是Python的异常的处理,是不是很直观也不复杂?

     

    学习完了以上内容,你已经知道了如何在Python中处理异常和使用异常。而在实际的项目代码中,我们还需要确定哪些代码需要进行异常处理,即:预测异常。

     

    这里我们一般基于以下原则:依赖于外部环境,且不可控的代码,需要捕获处理异常”常见的异常应用场景有:

    用户输入:不确定用户会输入什么样的数据导致出错

    网络连接:网络连接可能会中断

    文件读写:文件可能不存在

    数据库查找:想要查找的数据可能不存在

    远程API调用:输入的参数、URL等可能不正确或对方服务出错

    ...

    你也自己可以思考一下,你还遇到哪些异常的应用场景?

    2.异常处理原则

    当你遇到符合条件的异常场景时,可以使用ry来捕获异常。而捕获之后,该如何处理异常呢?我们的原则是:让程序可以恢复运行状态”。

     

    因此,except语句中的逻辑,除了记录日志,打印错误外,我们也尽量让程序可以继续运行。比如网络出错,则尝试重新连接;数据库查找不到,则返回空列表等等。特别是在一些长时间运行的程序和服务中,正确的异常处理显得格外重要。

     

    你可以查看一下自己过去写的代码,看看有哪里可能导致系统出现不确定的错误。然后试着加入try except异常处理,让你的的系统可以正确的应对,稳定运行。

     

    课程总结

    好了,本节课程就到这里,在本门课程中,我们学习到:

     课程总结

     

    如果以上内容你均已掌握,以后就可以通过异常处理,让自己的程序长久稳定运行,即使遇到错误,也可以快速的正确处理并恢复正常运行啦。

     

    接下来,逍遥老师推荐你继续学习下面这个案例课程 --《定制群发微信消息》。在这个课程中我们通过实际案例并巧用异常处理,不到50行代码,就让我们的工作变得更轻松。想知逍遥老师是怎样做到的么?想把异常处理学习的更透彻么?一起来学吧!

     

     

     

     

     

     

     

    申请免费试学名额    

在职想转行提升,担心学不会?根据个人情况规划学习路线,闯关式自适应学习模式保证学习效果
讲师一对一辅导,在线答疑解惑,指导就业!

上一篇: 要不要学Python?还在犹豫要不要学Python的同学看过来 下一篇: 8月份的编程薪酬排名出来啦,看看是谁突出了重围!

相关推荐 更多

热门文章

  • 前端是什么
  • 前端开发的工作职责
  • 前端开发需要会什么?先掌握这三大核心关键技术
  • 前端开发的工作方向有哪些?
  • 简历加分-4步写出HR想要的简历
  • 程序员如何突击面试?两大招带你拿下面试官
  • 程序员面试技巧
  • 架构师的厉害之处竟然是这……
  • 架构师书籍推荐
  • 懂了这些,才能成为架构师
  • 查看更多

扫描二维码,了解更多信息

博学谷二维码