Python高级编程之数据库sqlite3(二)

1. 事务

关系型数据库的关键特性之一是使用事务(transaction)维护一致的内部状态。启用事务时,在提交结果并刷新输出到真正的数据之前,可以通过一个连接完成多个变更,而不会影响任何其他用户。

保留变更

不论通过插入(insert)还是更新(update)语句改变数据库,都需要显式地调用commit()保存变更。这个要求为应用提供了一个机会,可以将多个变更一同完成,使它们以一种“原子”方式保存而不是增量保存,这样就可以避免同事连接到数据库的不同客户只看到部分更新的情况。可以利用一个使用了多个数据库连接的程序来查看调用commit()的效果。用第一个连接插入一个新行,然后两次尝试使用不同的连接读回这个数据行。
import sqlite3

db_filename = 'todo.db'

def show_projects(conn):
    cursor = conn.cursor()
    cursor.execute('select name, description from project')
    for name, desc in cursor.fetchall():
        print '  ', name
    return

with sqlite3.connect(db_filename) as conn1:
    print 'Befor changes:'
    show_projects(conn1)
 
    # Insert in one cursor
    cursor1 = conn1.cursor()
    cursor1.execute("""
    insert into project(name, description, deadling)
    values('virtualenvwrapper', 'Virtualenv Extension', '2011-01-01')
    """)
 
    print '\nAfter changes in conn1:'
    show_projects(conn1)
 
    # Select from another connection, without committing first
    print '\nBefor commit:'
    with sqlite3.connect(db_filenames) as conn2:
        shwo_project(conn2)
 
    # Commit then select from another connection
    conn1.commit()
    print '\nAfter commit:'
    with sqlite3.connect(db_filename) as conn3:
        show_projects(conn3)
再conn1中变更如果不执行commit(),变更只有在conn1才能看到,其他的连接是看不到的。只有当执行commit()后,其他连接才能看到变更。
$ python transaction_commit.py
Befor changes:
   pymotw

After changes in conn1:
   pymotw
   virtualenvwrapper

Befor commit:
   pymotw

After commit:
   pymotw
   virtualenvwrapper

丢弃变更

可以使用rollback()完全丢弃未提交的变更。commit()和rollback()方法通常在同一个try:exeption块的不同部分调用,有错误时就会触发回滚。
import sqlite3

db_filename = 'todo.db'

def show_projects(conn):
    cursor = conn.cursor()
    cursor.execute('select name, description from project')
    for name, desc in cursor.fetchall():
        print '    ', name
    return

with sqlite3.connect(db_filename) as conn:
    print 'Before changes:'
    show_projects(conn)
    try:
        # Insert
        cursor = conn.cursor()
        cursor.execute("""delete from project
        where name = 'virtualenvwrapper'
        """)

        # Show the settings
        print '\nAfter delete:'
        show_projects(conn)

        # Pretend the processing caused an error
        raise RuntimeError('simulated error')
    except Exception, err:
        # Discard the changes
        print 'ERROR:', err
        conn.rollback()
    else:
        #Save the changes
        conn.commit()

    # Show the results
    print '\nAfter rollback:'
    show_projects(conn)
调用rollback()后,对数据库的修改不复存在。
$ python transaction_rollback.py 
Before changes:
     pymotw
     virtualenvwrapper

After delete:
     pymotw
ERROR: simulated error

After rollback:
     pymotw
     virtualenvwrapper


2. 隔离级别

sqlit3支持3种加锁模式,也称为隔离级别(isolation level),这会控制使用任何技术避免连接之间不兼容的变更。打开一个连接时可以传入一个字符串作为isolation_level参数来设置隔离级别,所以不同的连接可以使用不同的隔离级别值。
下面展示了使用同一数据库的不同连接时,不同的隔离级别对 于线程中事件的顺序会有什么影响。这里创建了4个线程。两个线程会更新现有的行,将变更写入数据库。另外两个线程尝试从task表读取所有的行。
import logging
import sqlite3
import sys
import threading
import time

logging.basicConfig(
    level = logging.DEBUG,
    format = '%(asctime)s (%(threadName)-10s) %(message)s',
    )

db_filename = 'todo.db'
isolation_level = sys.argv[1]

def writer():
    #my_name = threading.currentThread().name
    with sqlite3.connect(db_filename,
            isolation_level = isolation_level) as conn:
        cursor = conn.cursor()
        cursor.execute('update task set priority = priority + 1')
        logging.debug('waiting to synchronize')
        ready.wait() # synchronize threads
        logging.debug('PAUSINE')
        time.sleep(1)
        conn.commit()
        logging.debug('CHANGES COMMITTED')
    return

def reader():
    #my_name = threading.currentThread().name
    with sqlite3.connect(db_filename,
            isolation_level = isolation_level) as conn:
        cursor = conn.cursor()
        logging.debug('waiting to synchronize')
        ready.wait() # synchronize threads
        logging.debug('wait over')
        cursor.execute('select * from task')
        logging.debug('SELECT EXECUTED')
        results = cursor.fetchall()
        logging.debug('result fetched')
    return

if __name__ == '__main__':
    ready = threading.Event()

    threads = [
            threading.Thread(name='Reader 1', target = reader),
            threading.Thread(name='Reader 2', target = reader),
            threading.Thread(name='Writer 1', target = writer),
            threading.Thread(name='Writer 2', target = writer),
            ]

    [ t.start() for t in threads ]

    time.sleep(1)
    logging.debug('setting ready')
    ready.set()

    [ t.join() for t in threads ]
这些线程使用threading模块的一个Event完成同步。writer()函数连接数据库,并完成数据库修改,不过在事件触发前并不提交。reader()函数连接数据库,然后等待查询数据库,直到出现同步事件。

延迟

默认的隔离级别是DEFERRED。使用延迟(Deferred)模式会锁定数据库,但只是在修改真正开始时锁定一次。前面的所有例子都使用了延迟模式。
$ python isolation_levels.py DEFERRED
2015-01-08 23:17:29,144 (Reader 1  ) waiting to synchronize
2015-01-08 23:17:29,144 (Reader 2  ) waiting to synchronize
2015-01-08 23:17:29,145 (Writer 1  ) waiting to synchronize
2015-01-08 23:17:30,146 (MainThread) setting ready
2015-01-08 23:17:30,147 (Reader 1  ) wait over
2015-01-08 23:17:30,147 (Reader 1  ) SELECT EXECUTED
2015-01-08 23:17:30,147 (Reader 1  ) result fetched
2015-01-08 23:17:30,147 (Reader 2  ) wait over
2015-01-08 23:17:30,148 (Reader 2  ) SELECT EXECUTED
2015-01-08 23:17:30,148 (Reader 2  ) result fetched
2015-01-08 23:17:30,148 (Writer 1  ) PAUSINE
2015-01-08 23:17:31,155 (Writer 1  ) CHANGES COMMITTED
2015-01-08 23:17:31,178 (Writer 2  ) waiting to synchronize
2015-01-08 23:17:31,178 (Writer 2  ) PAUSINE
2015-01-08 23:17:32,183 (Writer 2  ) CHANGES COMMITTED

立即

采用立即(Immediate)模式时,修改一开始时就会锁定数据库,从而在事务提交之前避免其他游标修改数据库。如果数据库有复杂的写操作,但是阅读器比书写器更多,这种模式就非常合适,因为事务进行中不会阻塞阅读器。
$ python isolation_levels.py IMMEDIATE
2015-01-08 23:41:21,768 (Reader 2  ) waiting to synchronize
2015-01-08 23:41:21,769 (Writer 1  ) waiting to synchronize
2015-01-08 23:41:21,769 (Reader 1  ) waiting to synchronize
2015-01-08 23:41:22,769 (MainThread) setting ready
2015-01-08 23:41:22,769 (Reader 2  ) wait over
2015-01-08 23:41:22,770 (Reader 2  ) SELECT EXECUTED
2015-01-08 23:41:22,770 (Reader 2  ) result fetched
2015-01-08 23:41:22,770 (Writer 1  ) PAUSINE
2015-01-08 23:41:22,770 (Reader 1  ) wait over
2015-01-08 23:41:22,770 (Reader 1  ) SELECT EXECUTED
2015-01-08 23:41:22,771 (Reader 1  ) result fetched
2015-01-08 23:41:23,776 (Writer 1  ) CHANGES COMMITTED
2015-01-08 23:41:23,801 (Writer 2  ) waiting to synchronize
2015-01-08 23:41:23,801 (Writer 2  ) PAUSINE
2015-01-08 23:41:24,807 (Writer 2  ) CHANGES COMMITTED

互斥

互斥(Exclusive)模式会对所有阅读器和书写器锁定数据库。如果数据库性能很重要,这种情况下就要限制使用这种模式,因为每个互斥的连接都会阻塞所有其他用户。
$ python isolation_levels.py EXCLUSIVE
2015-01-09 02:52:04,412 (Reader 1  ) waiting to synchronize
2015-01-09 02:52:04,412 (Reader 2  ) waiting to synchronize
2015-01-09 02:52:04,413 (Writer 1  ) waiting to synchronize
2015-01-09 02:52:05,415 (MainThread) setting ready
2015-01-09 02:52:05,415 (Reader 1  ) wait over
2015-01-09 02:52:05,415 (Reader 2  ) wait over
2015-01-09 02:52:05,415 (Writer 1  ) PAUSINE
2015-01-09 02:52:06,423 (Writer 1  ) CHANGES COMMITTED
2015-01-09 02:52:06,446 (Reader 1  ) SELECT EXECUTED
2015-01-09 02:52:06,446 (Reader 1  ) result fetched
2015-01-09 02:52:06,446 (Reader 2  ) SELECT EXECUTED
2015-01-09 02:52:06,446 (Reader 2  ) result fetched
2015-01-09 02:52:06,447 (Writer 2  ) waiting to synchronize
2015-01-09 02:52:06,447 (Writer 2  ) PAUSINE
2015-01-09 02:52:07,453 (Writer 2  ) CHANGES COMMITTED
由于第一个书写器已经开始修改,所以阅读器和第二个书写器会阻塞,直到第一个书写器提交。sleep()调用在书写器线程中引入一个人为的延迟,已强制其他连接已阻塞这一事实。

自动提交

连接的isolation_level参数还可以设置参数为None,以启用自动提交(autocommit)模式。启用自动提交时,每个execute()调用会在语句完成时自动提交。自动提交模式很适合简短的事务,如向一个表插入少量数据。数据库锁定时间尽可能短,所以线程间竞争的可能性小。
autocommit.py中删除了commit()的显示调用,并向隔离级别设置为None,不过除此之外,其他内容都与isolation_levels.py相同。但是输出是不同的,因为两个书写器线程会在阅读器开始查询之前完成工作。
$ python autocommit.py None
2015-01-10 07:11:58,890 (Reader 1  ) waiting to synchronize
2015-01-10 07:11:58,891 (Reader 2  ) waiting to synchronize
2015-01-10 07:11:58,907 (Writer 1  ) waiting to synchronize
2015-01-10 07:11:58,913 (Writer 2  ) waiting to synchronize
2015-01-10 07:11:59,893 (MainThread) setting ready
2015-01-10 07:11:59,893 (Reader 1  ) wait over
2015-01-10 07:11:59,894 (Reader 1  ) SELECT EXECUTED
2015-01-10 07:11:59,894 (Reader 1  ) result fetched
2015-01-10 07:11:59,893 (Reader 2  ) wait over
2015-01-10 07:11:59,895 (Reader 2  ) SELECT EXECUTED
2015-01-10 07:11:59,895 (Reader 2  ) result fetched
2015-01-10 07:11:59,895 (Writer 1  ) PAUSINE
2015-01-10 07:11:59,895 (Writer 2  ) PAUSINE

3. 内存中数据库

SQlite支持在RAM中管理整个数据库,而不是依赖一个磁盘文件。如果测试运行之间不需要保存数据库,或者要尝试一个模式或者其他数据库特性,此时内存中数据库对于自动测试会很有用。要打开一个内存中数据库,创建Connection时可以使用串':memory:'而不是一个文件名。每个':memory:'连接会创建一个单独的数据库实例,所以一个连接中游标所做的修改不会影响其他连接。

4. 导出数据库类型

内存数据库的内容可以使用Connection的iterdump()方法保存。iterdump()方法返回迭代器生成一系列字符串,这些 字符串将共同构造相应的SQL指令来重新创建数据库的状态。
import sqlite3

schema_filename = 'todo_schema.sql'

with sqlite3.connect(':memory:') as conn:
    conn.row_factory = sqlite3.Row

    print 'Creating schema'
    with open(schema_filename, 'rt') as f:
        schema = f.read()
    conn.executescript(schema)
    print "Inserting initial data"
    conn.execute("""
    insert  into project (name, description, deadline)
    values('pymotw','Python Module of the Week', '2015-01-10')
    """)

    data = [
    ('write about select', 'done', '2015-01-11', 'pymotw'),
    ('write about random', 'waiting', '2015-01-12', 'pymotw'),
    ('write about sqlite3', 'active', '2015-01-10', 'pymotw'),
    ]

    conn.executemany("""
    insert into task (details, status, deadline, project)
    values (?, ?, ?, ?)
    """, data)

    print 'Dumping:'
    for text in conn.iterdump():
        print text
iterdump()也适用于保存到文件的数据库,不过对于未保存的数据库最为有用。这里对输出做了一些编辑器调整,从而使其保证语法正确的前提下适合在页面中显示。

Creating schema
Inserting initial data
Dumping:
BEGIN TRANSACTION;
CREATE TABLE project(
        name text primary key,
        description text,
        deadline date
        );
INSERT INTO "project" VALUES('pymotw','Python Module of the Week','2015-01-10');
CREATE TABLE task(
        id integer primary key autoincrement not null,
        priority integer default 1,
        details text,
        status text,
        deadline date,
        completed_on date,
        project text not null references project(name)
        );
INSERT INTO "task" VALUES(1,1,'write about select','done','2015-01-11',NULL,'pymotw');
INSERT INTO "task" VALUES(2,1,'write about random','waiting','2015-01-12',NULL,'pymotw');
INSERT INTO "task" VALUES(3,1,'write about sqlite3','active','2015-01-10',NULL,'pymotw');
DELETE FROM "sqlite_sequence";
INSERT INTO "sqlite_sequence" VALUES('task',3);
COMMIT;
查看todo_schema.sql,其内容如下:
create table project(
        name text primary key,
        description text,
        deadline date
        );

create table task(
        id integer primary key autoincrement not null,
        priority integer default 1,
        details text,
        status text,
        deadline date,
        completed_on date,
        project text not null references project(name)
        );

5. SQL中使用Python函数

SQL语法支持在查询中调用函数,可以再列表中调用,也可以再select语句中的where字句中调用。利用这个特性,从查询返回数据库之前可以先处理数据,可以用于不同格式之间转换,完成一些计算(否则使用纯SQL会很麻烦),以重用应用代码。
import sqlite3

db_filename = 'todo.db'

def encrypt(s):
    print 'Encrypting %r' % s
    return s.encode('rot-13')


def decrypt(s):
    print 'Decrypting %r' % s
    return s.encode('rot-13')


with sqlite3.connect(db_filename) as conn:
    conn.create_function('encrypt',1, encrypt)
    conn.create_function('decrypt', 1, decrypt)
    cursor = conn.cursor()


    # Raw values
    print 'Original values:'
    query = 'select id, details from task'
    cursor.execute(query)
    for row in cursor.fetchall():
        print row

    print '\nEncrypting...'
    query = 'update task set details = encrypt(details)'
    cursor.execute(query)
    for row in cursor.fetchall():
        print row

    print '\nDecrypting in query...'
    query = 'select id, decrypt(details) from task'
    cursor.execute(query)
    for row in cursor.fetchall():
        print row
函数使用Connection的create_function()方法提供。参数包括函数名(即SQL中使用函数名)、函数所取的参数个数,以及要提供的Python函数。
$ python create_function.py 
Original values:
(1, u'write about select')
(2, u'write about random')
(3, u'write about sqlite3')
(4, u'finish revieing markup')
(5, u'revise chapter intros')
(6, u'subtitle')

Encrypting...
Encrypting u'write about select'
Encrypting u'write about random'
Encrypting u'write about sqlite3'
Encrypting u'finish revieing markup'
Encrypting u'revise chapter intros'
Encrypting u'subtitle'

Decrypting in query...
Decrypting u'jevgr nobhg fryrpg'
Decrypting u'jevgr nobhg enaqbz'
Decrypting u'jevgr nobhg fdyvgr3'
Decrypting u'svavfu erivrvat znexhc'
Decrypting u'erivfr puncgre vagebf'
Decrypting u'fhogvgyr'
(1, u'write about select')
(2, u'write about random')
(3, u'write about sqlite3')
(4, u'finish revieing markup')
(5, u'revise chapter intros')
(6, u'subtitle')

6. 定制聚类

聚类函数会收集多个单独的数据,并以某种方式汇总。avg()(取平均值)、min()、max()和count()都是内置聚类函数的例子。
sqlite3使用的聚集器API定义一个包含两个方法的类。处理查询时会对各个数值分别调用一次stop()方法。finalize()方法在查询的最后调用一次,并返回聚集值。下面的例子为mode实现了一个聚集集。它会返回输入中出现最频繁的值。
import sqlite3
import collections

db_filename = 'todo.db'

class Mode(object):
    def __init__(self):
        self.counter = collections.Counter()
    def step(self,value):
        print 'step(%r)' % value
        self.counter[value] += 1
    def finalize(self):
        result, count = self.counter.most_common(1)[0]
        print 'finalize() -> %r (%d times)' % (result, count)
        return result

with sqlite3.connect(db_filename) as conn:
    conn.create_aggregate('mode', 1, Mode)

    cursor = conn.cursor()
    cursor.execute("""
            select mode(deadline) from task where project = 'pymotw'
            """)
    row = cursor.fetchone()
    print 'mode(deadline) is:', row[0]
聚类器用Connection的create_aggregate()方法注册。参数包括函数名(即SQL中使用的函数名)、step()方法所取的参数个数,以及要使用的类。
$python create_aggregate.py
step(u'2014-12-05')
step(u'2015-01-01')
step(u'2015-02-01')
step(u'2014-10-02')
step(u'2014-11-02')
step(u'2014-11-02')
finalize() -> u'2014-11-02' (2 times)
mode(deadline) is: 2014-11-02

7. 定制排序

对比(collation)是一个比较函数,在SQL查询的order by部分使用。对于SQLite无法在内部排序的数据类型,可以使用它来定制比对来比较。例如,我们需要一个定制比对来保存custom_type.py中的pickle对象排序。
import sqlite3
try:
    import cPickle as pickle
except:
    import pickle

db_filename = 'todo.db'

def adapter_func(obj):
    return pickle.dumps(obj)
def converter_func(data):
    return pickle.loads(data)

class MyObj(object):
    def  __init__(self, arg):
        self.arg = arg
    def __str__(self):
        return 'MyObj(%r)' % self.arg
    def __cmp__(self, other):
        return cmp(self.arg, other.arg)

# Register the functions for mainpulating the type.
sqlite3.register_adapter(MyObj, adapter_func)
sqlite3.register_converter("MyObj", converter_func)

def collation_func(a, b):
    a_obj = converter_func(a)
    b_obj = converter_func(b)
    print 'collation_func(%s, %s)' % (a_obj, b_obj)
    return cmp(a_obj, b_obj)

with sqlite3.connect(db_filename,
        detect_types = sqlite3.PARSE_DECLTYPES,
        ) as conn:
    # Define the collation
    conn.create_collation('unpickle', collation_func)

    # Clear the table and insert new values
    conn.execute('delete from obj')
    conn.executemany('insert into obj (data) values (?)',
    [(MyObj(x),) for x in xrange(5,0,-1)],)

    # Query the database for the objects just saved
    print 'Querying:'
    cursor = conn.cursor()
    cursor.execute("""
    select id, data from obj order by data collate unpickle
    """)
    for obj_id, obj in cursor.fetchall():
        print obj_id, obj
对比函数的参数是字节串,所以在完成比较之前必须解除pickle,并转化为为MyObj实例,执行结果为:
$python create_collation.py 
Querying:
collation_func(MyObj(2), MyObj(1))
collation_func(MyObj(4), MyObj(3))
collation_func(MyObj(3), MyObj(1))
collation_func(MyObj(3), MyObj(2))
collation_func(MyObj(5), MyObj(1))
collation_func(MyObj(5), MyObj(2))
collation_func(MyObj(5), MyObj(3))
collation_func(MyObj(5), MyObj(4))
7 MyObj(1)
6 MyObj(2)
5 MyObj(3)
4 MyObj(4)
3 MyObj(5)

8. 线程和连接共享

处于历史原因,由于必须使用老版本的SQLite,Connection对象不能在线程间共享。每个线程必须创建自己的数据库连接。
import sqlite3
import sys
import threading
import time

db_filename = 'todo.db'
isolation_level = None # autocommit mode

def reader(conn):
    my_name = threading.currentThread().name
    print 'Starting thread'
    try:
        cursor = conn.cursor()
        cursor.execute('select * from task')
        results = cursor.fetchall()
        print 'results fetched'
    except Exception, err:
        print 'ERROR:', err
    return

if __name__ == '__main__':
    with sqlite3.connect(db_filename, isolation_level=isolation_level,) as conn:
        t = threading.Thread(name='Reader 1', target = reader, args=(conn,),)
        t.start()
        t.join()
如果试图在线程之间共享一个连接,会导致一个异常:
Starting thread
ERROR: SQLite objects created in a thread can only be used in that same thread.The object was created in thread id 140473086383872 and this is thread id 140473051047680

9.限制对数据的访问

与其他更大的关系数据库相比,尽管SQLite没有用户访问控制,但是确实提供了一种机制来限制访问。每个连接可以安装一个授权函数(authorizer function),运行时可以根据所需的原则来批准或拒绝访问列。这个授权函数会在解析SQL语句时调用,将传入5个参数。第一个参数是动作码,指示所完成的操作类型(读、写、删除等等)。其余的参数则取决于动作码。对于SQLITE_READ操作,这4个参数分别是表名、列名、SQL语句访问出现的位置(主查询、触发器等等)和None。
import sqlite3

db_filename = 'todo.db'
def authorizer_func(action, table, column, sql_location, ignore):
    print '\nauthorizer_func(%s, %s, %s, %s, %s)' % \
            (action, table, column, sql_location, ignore)

    response = sqlite3.SQLITE_OK  # be permissive by default

    if action == sqlite3.SQLITE_SELECT:
        print 'requesting permission to run a select statement'
        response = sqlite3.SQLITE_OK
    elif action == sqlite3.SQLITE_READ:
        print 'requesting access to column %s.%s from %s' % \
                (table, column, sql_location)
        if column == 'details':
            print '   ignoring details column'
            response = sqlite3.SQLITE_IGNORE
        elif column == 'priority':
            print '    preventing access to priority column'
            response = sqlite3.SQLITE_DENY
    return  response

with sqlite3.connect(db_filename) as conn:
    conn.row_factory = sqlite3.Row
    conn.set_authorizer(authorizer_func)

    print 'Using SQLITE_IGNORE to mask a column value:'
    cursor = conn.cursor()
    cursor.execute(" select id, details from task where project='pymotw' ")
    for row in cursor.fetchall():
        print row['id'], row['details']

    print '\nUsing SQLITE_DENY to deny access to a column:'
    cursor.execute("""
    select id, priority from task where project='pymotw'
    """)
    for row in cursor.fetchall():
        print row['id'], row['priority']
sqlite3中提供了一些矿用的动作码,它们作为常量提供,名字前面都有前缀SQLITE_。每一类SQL语句可以加标志,可以控制对单个列的访问。SQLITE_OK表示能够正常返回列值,而SQLITE_IGNORE则返回查询列为NULL,SQLITE_DENY将限制对该列的访问。该程序中,默认为SQLITE_OK,即能够正常返回,对于detail列,返回NULL,对于priority列的查询,则禁止。
$python set_authorizer.py 
Using SQLITE_IGNORE to mask a column value:

authorizer_func(21, None, None, None, None)
requesting permission to run a select statement

authorizer_func(20, task, id, main, None)
requesting access to column task.id from main

authorizer_func(20, task, details, main, None)
requesting access to column task.details from main
   ignoring details column

authorizer_func(20, task, project, main, None)
requesting access to column task.project from main
1 None
2 None
3 None
4 None
5 None
6 None

Using SQLITE_DENY to deny access to a column:

authorizer_func(21, None, None, None, None)
requesting permission to run a select statement

authorizer_func(20, task, id, main, None)
requesting access to column task.id from main

authorizer_func(20, task, priority, main, None)
requesting access to column task.priority from main
    preventing access to priority column
Traceback (most recent call last):
  File "set_authorizer.py", line 37, in <module>
    """)
sqlite3.DatabaseError: access to task.priority is prohibited










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值