【自动化运维新手村】初见Python

摘要

首先说明,以下几类读者朋友们请自行对号入座:

  • 对CMDB很了解但对于Python还没有上手的读者,强烈建议阅读此篇;
  • 了解过Python基本的数据结构,但又没有经常在实践中运用的读者,建议阅读此篇;
  • 已经可以熟练写出Python脚本,但对CMDB不是很了解的读者,建议阅读此篇;
  • 即了解Python,又了解CMDB的读者,可以出门左转,看下一篇。

看到这里的读者可能会对标题产生很多问号,为什么上来先讲CMDB,CMDB和自动化运维以及Python到底有多么紧密的联系以至于它可以放到第一篇的第一讲。

如果是资深的运维老手,一定能了解CMDB在日常运维中所起到的至关重要的作用,它是每一个运维工程师都会用到的一个应用,他可以说是整个运维体系的基石,CMDB实际上非常庞大,也涉及到很多的运维理念,我们暂且略过不提,但从CMDB的实现上来讲涉及到非常多的数据结构以及相对应的操作;同时Python作为我们该系列构建自动化运维体系的主要编程语言,首要任务就是先掌握Python的基本数据结构,但对于还没有上手Python的读者来说,如何能避免网络上大片枯燥的基本数据结构的介绍,生动准确的上手Python呢,下面我们就从CMDB来切入,给大家深入浅出讲解Python的基本数据结构。


CMDB简介

简单赘述以下,CMDB的英文全称是Configuration Management Database,中文名叫配置管理数据库,它几乎贯穿了运维的每个环节。在实际的项目中,CMDB常常被认为是构建其它ITIL(Information Technology Infrastructure Library,IT基础架构库)流程的基础,ITIL项目的成败与是否成功建立CMDB有非常大的关系。

对于一些中大型的互联网公司必然都有自研的CMDB系统,而一些初创公司可能采用开源的CMDB工具或者部分运维工程师日常使用Excel表格充当简易的CMDB功能,我们的目的并不是让大家去构建一个新的CMDB去推翻原有系统,也不是说用Excel表格就不如用Python来的高级,而是能让大家从CMDB自顶向下的拆解,来更生动的体会到Python基础数据结构的运用

CMDBv1.0

CMDB的成品很复杂,但作为讲解Python基本数据结构的范例,我们就先实现一个简易的1.0版本。
CMDBv1.0只需要通过一个Python脚本就可以做到资产数据的增删改查。

可能会有一些有CMDB使用经验的朋友会有质疑,觉得这也叫CMDB,还请大家稍安勿躁,任何大型系统的构建都是经过日积月累的迭代,但我可以保证,在项目冷启动阶段,这样一个稍微简陋的1.0版本,就可以起到基本的资产管理的作用。

下面假设我们已经有了一个Python脚本,名叫 cmdb-v1.0.py ,我们简单的演示以下它的操作

root> # python3 cmdb-v1.0.py init beijing  // 初始化beijing IDC
{
  "beijing": {
    "idc": "beijing",
    "switch": {},
    "router": {}
  }
}
root> # python3 cmdb-v1.0.py add /beijing/switch   // 添加beijing IDC的IP地址是10.0.0.1的交换机信息
{
  "beijing": {
    "idc": "beijing",
    "switch": {
    	"10.0.0.1": {
              "ip": "10.0.0.1",
              "manufacturer": "cisco",
              "hostname": "cisco-nx95-00-00-01",
              "hardware": "nexus9500",
              "role": "asw",
              "port": [
                "Eth1/1/0",
                "Eth1/1/1",
                "Eth1/1/2"
              ],
              "stack": True
            }
        }
    },
    "router": {}
  }
}
root> # python3 cmdb-v1.0.py get /beijing/switch/10.0.0.1  // 读取beijing IDC的IP地址是10.0.0.1的交换机信息
{
  "ip": "10.0.0.1",
  "manufacturer": "cisco",
  "hostname": "cisco-nx95-00-00-01",
  "hardware": "nexus9500",
  "role": "asw",
  "port": [
    "Eth1/1/0",
    "Eth1/1/1",
    "Eth1/1/2"
  ],
  "stack": true
}
root> # python .\cmdb-v1.0.py update /beijing/switch/10.0.0.1/hostname '\"test\"'  // 将 beijing IDC的IP地址是10.0.0.1的交换机主机名修改为 test
root> # python .\cmdb-v1.0.py get /beijing/switch/10.0.0.1  // 读取验证相关信息
{
  "ip": "10.0.0.1",
  "manufacturer": "cisco",
  "hostname": "test",
  "hardware": "nexus9500",
  "role": "asw",
  "port": [
    "Eth1/1/0",
    "Eth1/1/1",
    "Eth1/1/2"
  ],
  "stack": true
}
root> # python .\cmdb-v1.0.py delete /beijing/switch/10.0.0.1/role  // 删除beijing IDC的IP地址是10.0.0.1的交换机的角色属性
root> # python .\cmdb-v1.0.py get /beijing/switch/10.0.0.1  // 读取验证相关信息
{
  "ip": "10.0.0.1",
  "manufacturer": "cisco",
  "hostname": "test",
  "hardware": "nexus9500",
  "port": [
    "Eth1/1/0",
    "Eth1/1/1",
    "Eth1/1/2"
  ],
  "stack": true
}
root> # python .\cmdb-v1.0.py delete /beijing/switch/10.0.0.1/port Eth1/1/0  // 删除beijing IDC的IP地址是10.0.0.1的交换机端口属性中的 Eth1/1/0
root> # python .\cmdb-v1.0.py get /beijing/switch/10.0.0.1  // 读取验证相关信息
{
  "ip": "10.0.0.1",
  "manufacturer": "cisco",
  "hostname": "test",
  "hardware": "nexus9500",
  "port": [
    "Eth1/1/1",
    "Eth1/1/2"
  ],
  "stack": true
}

上面演示的几个步骤包括了地域的初始化,资产信息的增删改查,大家可以发现整个1.0版本中,数据源的结构是比较清晰的,几乎涉及到了Python中最常用的数据类型,以及不同数据类型的常用操作,所以这也是我想以CMDB为例切入Python的原因。

Python

从笔者个人经历来说,写过Python,Ja,Golang,至今仍然觉得Python是一门十分优秀的语言,能够持续霸占最热门语言的前三甲,确实有其独到之处

Python优缺点
  • 优点
    • 简单
    • 免费、开源
    • 高层语言面向对象
    • 可扩展性
    • 丰富的库
  • 缺点
    • 性能,虽然有一部分网友还是对Python颇有微词,但如果非要从Python的众多缺点中挑一个最重要的一点的话,那就是性能问题,但性能问题绝对不是我们弃用Python的原因,目前仍然有诸多方法可以保证Python支持企业级应用平稳运行迭代,而且就连字节如此大体量的公司很多地方都仍然使用Python进行开发

CMDB拆解及Python基本数据类型

CMDB 拆解

根据上面的演示大家应该已经大概了解CMDBv1.0版本的数据源大概长什么样子了,它的层级的划分其实是根据每个公司不同的实际场景决定的,我们这里就暂且先考虑普适情况,即idc为顶层,其包含了switch和router,然后再包含具体的设备信息和属性,如下:

{
  "beijing": {
    "idc": "beijing",
    "switch": {
      "10.0.0.1": {
        "ip": "10.0.0.1",
        "manufacturer": "cisco",
        "hostname": "cisco-nx95-00-00-01",
        "hardware": "nexus9500",
        "role": "asw",
        "port": [
          "Eth1/1/0",
          "Eth1/1/1",
          "Eth1/1/2"
        ],
        "stack": true
      },
      "10.0.0.2": {
        "ip": "10.0.0.2",
        "manufacturer": "cisco",
        "hostname": "cisco-nx95-00-00-02",
        "hardware": "nexus9500",
        "role": "dsw",
        "port": [
          "GEth1/1/0",
          "GEth1/1/1",
          "GEth1/1/2"
        ],
        "stack": true
      }
    },
    "router": {
      "10.0.0.3": {
        "ip": "10.0.0.3",
        "manufacturer": "cisco",
        "hostname": "cisco-nx95-00-00-01",
        "hardware": "nexus9500",
        "role": "br",
        "port": [
          "TGEth1/0/0/1",
          "TGEth1/0/0/2",
          "TGEth1/0/0/3"
        ],
        "bgp_as": 64512
      }
    }
  },
  "shanghai": {
    "idc": "shanghai",
    "switch": {
      "10.0.1.1": {
        "ip": "10.0.1.1",
        "manufacturer": "cisco",
        "hostname": "cisco-nx95-00-01-01",
        "hardware": "nexus9500",
        "role": "asw",
        "port": [
          "Eth1/1/0",
          "Eth1/1/1",
          "Eth1/1/2"
        ],
        "stack": false
      }
    },
    "router": {
      "10.0.1.3": {
        "ip": "10.0.1.3",
        "manufacturer": "cisco",
        "hostname": "cisco-nx95-00-01-01",
        "hardware": "nexus9500",
        "role": "br",
        "port": [
          "TGEth1/0/0/1",
          "TGEth1/0/0/2",
          "TGEth1/0/0/3"
        ],
        "bgp_as": 64512
      }
    }
  }
}

想必很多读者都听过数据结构,构建一个完善且可扩展的CMDB非常需要一个合适的数据结构,当然很多计算机专业的同学应该了解,数据结构是一门十分复杂的学科,无法在短时间内将其讲解清楚,感兴趣的同学可以阅读番外篇详细了解,此处我们先简短的介绍一下需要用到的一些概念:

数据结构(英语:data structure)是计算机中存储、组织数据的方式,不同种类的数据结构适合于不同种类的应用;常见的数据结构有,栈,队列,数组,链表,树,图,堆,散列表=

其实我们目前只需要用到数组散列表(又称哈希表)两种数据结构,我先通俗易懂的讲解一下这两种数据结构

  • 数组, 可以将其理解为一个容器,里面可以装很多元素,只不过这些元素必须是相同类型的, 他们可以用下标的位置进行存取,如
|a | b | c | d | e | f | g | h | i | j |
 0   1   2   3   4   5   6   7   8   9

值得注意的是数组的下标永远都是从0开始,这个对于初期接触编程的读者朋友来说可能会有点儿不适应

  • 散列表,可以将其理解为通讯录,通讯录里的人不可以重名,每个人的名字都对应他的个人信息,个人信息可以存储任何数据,如
"jack": "19098090000",
"allen": {"age": 20, "gender": "male"},
"john": {"city": "shanghai", "family": ["father", "mother", "sister"]},

Python基础数据操作

通过上述的介绍,我们了解到了CMDB-v1.0的数据源长什么样子,以及它使用了什么样的数据结构,那么接下来就是如何用Python来表示它,这就涉及到了Python的几大数据类型

  • 字符串: 上述数据源中用到最多的类型就是字符串,如"ip","cisco", "role"
  • 整数:Python可以处理任意大小的整数,当然包括负整数,在程序中的表示方法和数学上的写法一模一样,如 1, 64512, -100
  • 浮点数:浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,如1.233.14-9.011.5e111.5e-21
  • 布尔值:布尔值和布尔代数的表示完全一致,一个布尔值只有True、False两种值
  • 列表:Python中列表即为数据结构中的数组,一种有序的集合,可以随时添加和删除其中的元素
  • 元组:另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改
  • 集合:也是一组key的集合,但在set中,没有重复的key
  • 字典:dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度,dict即为数据结构中的散列表

Tips
上述说列表和元组为有序序列,并不是说列表和元组中的元素会按大小顺序排列,而是说列表和元组中的每个元素的排列是固定的,即不管print多少次,显示的结果是一样的;但字典和集合中的元素不是有序的,print出的结果可能会不一样;这种现象其实是由于不同的数据结构在计算机内存中不同的存储和表示方法造成的,后续会在番外篇中详细解释。

下面我们就结合CMDB-v1.0的数据源逐一讲解涉及到的数据类型和其操作:

第一个Python程序

学习任何一门编程语言第一个程序都是如何打印出Hello World,Python对此的实现十分简单

# 在命令行模式下,输入python,进入Python的交互模式
>>> print("Hello, World!!!")
# 输出结果为 Hello World
# 输入exit()退出Python交互模式,或者可以直接输入ctrl-D直接退出

Tips

从Python实现打印一行字符串其实可以看出很多这门语言的特点,首先给人的第一感觉就是简洁,代码阅读起来和阅读英文十分相似,其次就是Python程序的运行不需要编译,诸如C++,JAVA,Golang运行前都需要进行编译,这是因为Python是一门解释型语言,具体关于解释型语言和编译型语言的区别,后续会在番外篇中详细解释。

变量

变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。

变量名必须是大小写英文、数字和_的组合,且不能用数字开头,比如:

port_num = 40 # 变量port_num是一个整数
hostname = "cisco-test" # 变量hostname是一个字符串。
stack = True # 变量stack是一个布尔值True

Tips

有过其他语言学习经历的同学可能会了解,程序中定义一个变量时,需要指定这个变量的数据类型,比如 int a = 123;,当把变量a指定为整型时,就无法把字符串再赋值给它,如a='ABC',这样会出发报错,但Python并没有这样的限制,这也是Python的另一大特点,即Python是一门动态类型语言,动态类型语言的一大好处就是灵活,这也是Python易上手的原因之一,但同时,由于在运行时才确定变量的数据类型,相较于静态类型语言,动态类型语言更容易出错,但我们享受其优点的同时,就必须要接受其弊病。更多关于静态语言与动态语言类型的区别,后续会在番外篇中详细解释。

注释

上面的示例代码中我们有使用到注释,注释可以帮我们很好的对代码进行解释说明,利于我们及他人后续阅读

Python的注释一般分为两种

  • 单行注释,可以跟在某行代码的后面,或者写在一个代码块的上面,没有强制的规定, 如

    port_num = 40 # 变量port_num是一个整数
    

    或者

    # 变量port_num是一个整数
    port_num = 40
    
  • 多行注释,顾名思义,可以在多行注释内写多行文本

    """
    变量port_num是一个整数
    这是一个十分复杂的代码
    """
    port_num = 40
    

Tips

程序员之间比较流行的一句话是:今天的代码没写注释,别说其他人以后不认识,明天我自己就不认识了。

数组

在CMDB-v1.0中端口属性的数据类型就是数组,与之相对应的数据结构是列表。

该数组中存储了某台设备上所有的端口号,我们以此为例看看Python中的数组都有哪些常用操作:

  • 如果我们想知道一共有多少端口,可以使用len()方法,len即为length的简称,很多方法名其实是可以根据名称推断出其作用

    len()方法即为求某个可迭代对象的长度,此处我们的可迭代对象为数组,何为可迭代对象,我们会在番外篇中提到

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> len(port)
    # 输出 3
    
  • 如果我们想获取某一个端口,可以使用数组下标索引进行访问,下标索引默认从0开始,最大为数组长度-1,如果超过数组长度,则会报错

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port[0]
    # 输出 Eth1/1/0
    >>> port[2]
    # 输出 Eth1/1/2
    >> port[-1]
    # 等同于上一个,Eth1/1/2,以此类推,-2即为倒数第二个元素,同样不可以超出数组长度
    >>> port[len(port)-1]
    # 输出 Eth1/1/2
    >>> port[3]
    # 会产生 IndexError 错误
    
  • 如果我们想在端口列表中增加一个端口,可以使用append()方法

    append()方法为在数组末尾追加一个元素

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port.append("Eth1/1/3")
    >>> port
    # 输出 ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2", "Eth1/1/3"]
    

    insert()方法可以在数组任意位置插入一个元素

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port.insert(1, "Eth1/1/1/1")
    # 输出 ["Eth1/1/0", "Eth1/1/1/1", Eth1/1/1", "Eth1/1/2"]
    
  • 如果我们想将两个端口列表合并,可以使用extend方法

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port.extend(["Eth1/1/3", "Eth1/1/4"])
    >>> port
    # 输出 ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2", "Eth1/1/3", "Eth1/1/4"]
    
  • 如果我们想修改数组中某个元素,可以直接使用下标索引并对其赋值

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port[1] = "GEth1/1/1"
    >>> port
    # 输出 ["Eth1/1/0", "GEth1/1/1", "Eth1/1/2"]
    
  • 如果我们想删除端口列表中的最后一个端口,可以使用pop()方法

    pop()方法会返回弹出数组的最后一个元素,并将其返回

    >>> port = ["Eth1/1/0", "Eth1/1/1", "Eth1/1/2"]
    >>> port.pop()
    # 输出 Eth1/1/2
    >>> port
    # 输出 ["Eth1/1/0", "Eth1/1/1"]
    

    pop(i)可以弹出数组中任意位置的元素

  • 更多数组相关的操作我们可以在以后的实践中慢慢学习

字典

CMDB-v1.0中的核心数据类型是字典,对应的数据结构是散列表。

字典中存储了某个IDC的名称和其设备信息,我们以此为例看看Python中的字典都有哪些常用操作:

  • 如果我们想知道这个字典是存储的哪个IDC的信息,可以使用键对其进行查找

    字典具有一个性质就是不管存储的数据有多大,根据某个键对其进行查找的速度都会非常快,不会随着字典数据的增加而变慢,这是数据结构中散列表的一个特性,并且字典要求键必须是不可变对象,相关知识我们后续会在番外篇中提到,此处我们暂且以字符串作为字典的键

    >>> data = {{...}, {...}}
    >>> bj_info= data["beijing"]  # 获取beijing IDC的数据
    >>> data.get("beijing")  # 同样为根据键进行查找,当字典中不存在 "beijing" 这个键时会返回 None
    >>> idc_info.get("xiamen", {})  # dict.get() 方法可以接收另外一个参数,作为查找的键值不存在时的默认返回值
    # 输出 {}
    
  • 如果我们想修改IDC的值,可以通过键对其进行赋值

    字典中键和值是一一对应的,一个键只能存储一个值

    >>> device_info = data["beijing"]["switch"]["10.0.0.1"]
    >>> device_info["hostname"] = "test"  # 将device_info设备的hostname修改为test
    
  • 如果我们想知道switch下有哪些设备IP,可以使用dict.keys()方法

    >>> bj_switches = data["beijing"]["switch"]
    >>> bj_switches.keys()
    # 输出 ["10.0.0.1", "10.0.0.2"]
    >>> bj_switches.values()  # 该方法可以获取字典中的所有值,得到beijing IDC的所有switch的详情
    # 输出 [{...}, {...}] 
    
  • 如果我们想给某个设备新增属性信息,可以直接用键去赋值

    >>> switch_info_10_1 = bj_switches.get("10.0.0.1", {})  
    # 赋值时必须保证变量是字典,所以如果此处不用dict.get() 默认返回空字典,那么当不存在查询的数据时就会返回None,给None通过键赋值就会报错
    >>> switch_info_10_1["label"] = "test_label"
    >>> switch_info_10_1
    # 输出 { "label": "test_label", manufacturer": "cisco", "hostname": "cisco-nx95-00-00-01", ...}
    
  • 如果我们想用某个新的设备信息覆盖原有设备的属性信息,可以使用dict.update()方法

    dict.update()方法接收一个字典,用来更新在原有的字典上

    >>> new_dict = { "hostname", "test-00-00-01", "role": "csw" }
    >>> switch_info_10_1.update(new_dict)
    >>> switch_info_10_1
    # 输出 { "manufacturer": "cisco", "hostname": "test-00-00-01", ...}
    
  • 如果我们想删除设备的某个属性,可以使用dict.pop()方法

    dict.pop()方法接收一个键,将该键和其对应的值从字典中删除

    >>> bj_switches["10.0.0.1"].pop("label")  # 删除beijing IDC下10.0.0.1设备的label属性
    
  • 更多字典相关的操作我们可以在以后的实践中慢慢学习

字符串

我们CMDB-v1.0中最多使用到的就是字符串这一数据类型,如"idc", "beijing","ip"等,在Python中使用引号将一串字符引住,即为字符串,引号可以是双引号或者单引号并没有强制要求,但具体如何使用更加规范我们会在番外篇中提到。

下面我们以设备的主机名为例,看看对于字符串有哪些具体的操作方法需要用到, 如"cisco-nx95-00-00-01"

  • 如果我们想查看主机名的长度,可以使用len()方法,上文中提到len()可以获取数组的长度,因为字符串同样也为可迭代对象,所以len()同样可以获取字符串的长度

    >>> hostname = "cisco-nx95-00-00-01"
    >>> len(hostname)
    # 输出 19
    
  • 如果我们将主机名以-分隔,可以使用split()方法,该方法需要传入分隔符,并且返回一个数组

    >>> hostname.split("-")
    # 输出 ["cisco", "nx95", "00", "00", "01"]
    
  • 如果我们想获取字符串的某一段,可以使用切片的方式,因为Python中字符串的存储与数组十分类似,所以切片的方式同时适用于数组和字符串

    >>> hostname[0:5]  # 0可以省略,故等价于 hostname[:5],Python中的切片是一个左闭右开区间,0-5的切片范围不包括下标5
    # 输出 cisco
    >>> hostname[6:len(hostname)]  # 等价于 hostname[6:],切片的区间右侧数字大于等于字符串长度时,都不会报错,此时相当于一直取到字符串末尾
    # 输出 nx95-00-00-01
    >>> hostname[0:len(hostname):2]  # 切片操作可以接受第三个参数,用于表示步长
    # 输出 cson9-00-1
    >>> hostname[::-1]  # 第三个操作为负数时可以将字符串或数组倒置
    # 输出 10-00-00-59xn-ocsic
    
  • 如果我们想获取某个字符所在的位置,可以使用index()方法,该方法接收字符参数,并且返回该字符在字符串中的第一个出现的下标

    >>> hostname.index("-")
    # 输出 5
    
  • 更多字符串相关的操作我们可以在以后的实践中慢慢学习

Tips

字符串是一种十分常见的数据类型,但由于字符串是文本,既然是文本就涉及到不同国家之间的编码问题,关于编码相关的内容我们会在番外篇中详细解释,大家暂时只需要知道目前国际通用的是UTF-8编码即可。

知识总结
  • 介绍了CMDB在自动化运维中的重要性
  • 演示了v1.0版本的CMDB的增删改查操作
  • 讲解了Python常用的数据类型:字符串,数组,字典,以及对它们的常用操作方法

CMDB系列第一节我们就暂且讲解到这里,其实Python的数据类型和其操作还没有全部涉及到,我们先只掌握最常用的即可,更多的类型和内置操作方法可以慢慢积累。

第二节我们就会进入到CMDBv1.0版本的具体代码,为大家讲解Python的基础语句以及函数和面向对象相关的知识。

篇后语

文中我们多次提到部分内容会在番外篇中详细解释,最大的原因是某个知识点如果详细展开,都足以单独写一篇文章,但对于初学者来说,我们完全必要花时间在一些细枝末节上,因为当我们学习一门新知识时,我们最好的方法就是自顶向下逐步拆解,如果一头扎进知识的海洋中,那极有可能“溺亡”。

所以如果一些职场朋友,没有多余的精力去深究细节,就没有必要去看番外篇,当然如果对某个知识点十分感兴趣也可以多做了解;

但对于计算机专业的同学,不管已经毕业或者还未毕业,我都强烈大家建议阅读番外篇,只有基础打的足够牢,才能做到触类旁通。

欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值