数据存储之Python操作关系型数据库MySQL
原创不易,转载前请注明博主的链接地址:Blessy_Zhu https://blog.csdn.net/weixin_42555080
本次代码的环境:
运行平台: Windows
Python版本: Python3.x
IDE: PyCharm
一 概述
前面这三篇博客数据存储之文件存储(一)-TXT文件存储 ,数据存储之文件存储(二)-JSON文件存储,数据存储之文件存储(三)-CSV文件存储讲了数据保存为文本格式的三个方法:JSON文件存储、TXT文件存储、CSV文件存储。这篇文章主要讲的是将具有逻辑关系的数据存到关系型数据库中。关系型数据库有很多种,比较常见的有:MySQL、Oracle、SQL Server等。
关系型数据库的官方解释是:
关系数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。现实世界中的各种实体以及实体之间的各种联系均用关系模型来表示。关系模型是由埃德加·科德于1970年首先提出的,并配合“科德十二定律”。现如今虽然对此模型有一些批评意见,但它还是数据存储的传统标准。标准数据查询语言SQL就是一种基于关系数据库的语言,这种语言执行对关系数据库中数据的检索和操作。 关系模型由关系数据结构、关系操作集合、关系完整性约束三部分组成。
简单说,关系型数据库是由多张能互相联接的二维行列表格组成的数据库。
二 Python操作MySQL
关系型数据库比较多,这里介绍比较常用的MySQL数据库进行探究。首先,确保自己电脑上安装了MySQL数据库,可参考博客MySQL数据库下载与安装,如果想实现数据库的可视化可以安装navicat,安装可参考博客Navicat安装与连接MySQL,同时在IDE上安装了PyMySQL库,这是一个操作MySQL的Python库。
2.1 连接数据库
做好上面的工作后,接下来开始尝试着连接数据库。这里给出连接数据库的代码:
import pymysql
db = pymysql.connect(host = 'localhost',user = 'root',password = '密码不告诉你啦',port = 3306)
cursor = db.cursor()
cursor.execute('SELECT VERSION()')
data = cursor.fetchone()
print('DataBase version:',data)
cursor.execute('CREATE DATABASE test1 DEFAULT CHARACTER SET utf8')
db.close()
运行结果如下:
这里通过PyMySQL的connect()方法声明一个MySQL连接对象db,此时需要传人MySQL运行的host (即IP)。由于MySQL在本地运行,所以传人的是localhost。如果MySQL在远程运行,则传人其公网IP地址。后续的参数user即用户名,password 即密码,port 即端口( 默认为3306 )。
连接成功后,需要再调用cursor()方法获得MySQL的操作游标,利用游标来执行SQL语句。这里我们执行了两句SQL,直接用execute()方法执行即可。第一句 SQL用于获得MySQL的当前版本,然后调用fetchone()方法获得第一条数据, 也就得到了版本号。第二句SQL执行创建数据库的操作,数据库名叫作test1,默认编码为UTF-8。由于该语句不是查询语句,所以直接执行后就成功创建了数据库test1.最后一定要通过close()方法将数据库关闭。
2.2 在数据库中创建数据表
为了简单的演示Python如何操作MySQL,并创建表格,这里只写一些简单的数据字段。
import pymysql
db = pymysql.connect(host = 'localhost',user = 'root',password = '不能告诉你密码',port = 3306,db = 'test1')
cursor = db.cursor()
sql = 'CREATE TABLE IF NOT EXISTS works(id VARCHAR(255) NOT NULL,name VARCHAR(255) NOT NULL,age INT NOT NULL,PRIMARY KEY(id))'
cursor.execute(sql)
db.close()
运行结果:
上面的语句只是定义了id、name、age这些字段的一个works表。当然,为了演示,这里只指定了最简单的几个字段。实际上,在爬虫过程中,会根据爬取结果设计特定的字段。
2.3 在以创建的数据表中插入数据
学过数据库的都会知道,数据一定要有ACID四个特性,即原子性、一致性、隔离性、持久性。这里面涉及事务的问题,事务机制可以确保数据的一致性,也就是一个操作要不发生要不不发生。比如插入一条数据,要么全部插人,要么都不插人,不会存在直插入一半这样的问题,这就是事务的原子性。
属性 | 解释 |
---|---|
原子性( atomicity ) | 事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做 |
一致性 ( consistency ) | 事务必须使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的 |
隔离性( isolation ) | 一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰 |
持久性( durability ) | 持续性也称永久性( permanence),指一个事务且提交, 它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响 |
既然了解到了数据的ACID四个特性,接下来要在这四个特性的前提下操作数据库。
import pymysql
id = '001'
user = 'Bob'
age = 20
db = pymysql.connect(host = 'localhost',user = 'root',password = '不能告诉你密码',port = 3306,db = 'test1')
cursor = db.cursor()
sql = 'INSERT INTO works(id,name,age) values(%s,%s,%s)'
try:
cursor.execute(sql,(id,user,age))
db.commit()
except:
db.rollback()
db.close()
结果:
前面的内容上面已经讲过来,接下来构造了一个SQL语句,其value值没有用字符串拼接的方式来构造,如:sql = ‘INSERT INTO works(id, nane, age) values (’ + id + ‘,’ + name + ', ’ + age + ‘)’
这样的写法烦琐而且不直观,所以选择直接用格式化符%s来实现。有几个value写几个%s,我们只需要在execute()方法的第一个参数传人该SQL语句,value 值用统一的元组(如上面的cursor.execute(sql,(id,user,age))中的(id,user,age)就是通过元组的方式传入的)传过来就好了。这样的写法既可以避免字符串拼接的麻烦,又可以避免引号冲突的问题。之后值得注意的是,需要执行db对象的commit()方法才可实现数据插人,这个方法才是真正将语句提交到数据库执行的方法。对于数据插人、更新、删除操作,都需要调用该方法才能生效。接下来,我们加了一层异常处理。如果执行失败,则调用rollback()执行数据回滚,相当于什么都没有发生过。这一步骤的操作就是为了满足前面讲到的数据库操作必须满足ACID四个特性。
try:
cursor.execute(sql)
db.commit()
except:
db.rollback()
这个代码段可以保证数据的一致性。
2.4 动态增加数据表字段
上面数据插人的操作是通过构造SQL语句实现的,但是很明显,这有一个极其不方便的地方,比如突然增加了性别字段gender,此时SQL语句就需要改成:
sql = ‘INSERT INTO works(id,name,age,gender) values(%s,%s,%s,%s)’,同时cursor.execute(sql,(id,user,age))这里面的元组也需要改变。这显然不是我们想要的。在很多情况下,我们要达到的效果是插人方法无需改动,做成一个通用方法,只需要传人一个动态变化的字典就好了。比如,构造这样一个字典:
{
'id': '001',
'name': ' Bob',
'age': 20
}
然后SOL语句会根据字典动态构造,元组也动态构造,这样才能实现通用的插人方法。所以,这里我们需要改写一下插人方法:
data = {
'id': '001',
'name': ' Bob',
'age': 20
}
table = 'works'
keys = ', '.join(data. keys())
values = ','.join(['%s'] * len(data))
sql = 'INSERT INTO {table}({keys}) VALUES ({values})'.format(table=table, keys=keys, values=values)
try:
if cursor.execute(sq1,tuple(data.values())):
print('Successful')
db.commit()
except:
print('Failed')
db.rollback()
db.close()
接下来对上面的代码解释一下:这里我们传人的数据是字典,并将其定义为data变量。表名也定义成变量table。接下来,就需要构造一个动态的SQL语句了:首先,需要构造插人的字段id、 name 和age。这里只需要将data的键名拿过来,然后用逗号分隔即可。所以’,’.join(data.keys())的结果就是 id, name, age, 然后需要构造多个%s当作占位符,有几个字段构造几个即可。比如,这里有三个字段,就需要构造%s, %s, %s。 这里首先定义了长度为1的数组[ ‘%s ],然后用乘法(即*号)将其扩充为["%s’, ‘%s’, ‘%s’], 再调用join()方法,最终变成%s, %s, %s。最后,我们再利用字符串的format()方法将表名、字段名和占位符构造出来。最终的SQL语句就被动态构造成了:INSERT INTO works(id, name, age) VALUES (%s, %s, %s)最后,为execute()方法的第一个 参数传人sql变量,第二个参数传人data的键值构造的元组,就可以成功插人数据了。
这样就可以通过改变字典里面的数据就可以动态的修改SQL语句的内容了。
2.5 update更新数据库
更新数据库,是实际上就是对已经存在的数据进行某些字段的修改。它实际上也是执行SQL语句,可以手动改写一条一条的SQL语句,然后执行。如下面代码段:
sql 'UPDATE works SET age = %s WHERE name = %s'
try:
cursor.execute(sq1,(25,'Bob')):
db. commit()
except:
db.rollback()
db.close()
这样手动一条一条的语句改写,只适合简单数据的更新。但是爬虫抓取工程中,数据量是比较大的,而且对于重复的数据,希望他是可以只更新数据而不再重复保存一个。这里实现的办法还是动态构造SQL语句,通过字典传值得办法,更新数据。
import pymysql
db = pymysql.connect(host = 'localhost',user = 'root',password = '不能告诉你哦',port = 3306,db = 'test1')
cursor = db.cursor()
data = {
'id': '001',
'name': 'Bob',
'age': 22
}
table = 'works'
keys = ', '.join(data. keys())
values = ','.join(['%s'] * len(data))
sql = 'INSERT INTO {table}({keys}) VALUES ({values}) ON DUPLICATE KEY UPDATE'.format(table=table, keys=keys, values=values)
update = ','.join(["{key} = %s".format(key = key)for key in data])
sql += update
try:
if cursor.execute(sql,tuple(data.values())*2):
print('Successful')
db.commit()
except:
print('Failed')
db.rollback()
db.close()
这里构造的SQL语句其实是插人语句, 但是在后面加了ON DUPLICATE KEY UPDATE。 这行代码的意思是如果主键已经存在,就执行更新操作。比如,传人的数据id仍然为001但是年龄有所变化,由20变成了22, 此时这条数据不会被插人,而是直接更新id为001的数据。完整的SQL构造出来是这样的:
INSERT INTO works(id, name, age) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE id = %s, name = %s, age=%s
这里就变成了6个%s。所以在后面的execute()方法的第二个参数元组就需要乘以2变成原来的2倍。
2.6 delete和search数据库
关于delete操作,相对较为简单,只需要DELETE语句即可,然后执行相应的语句。对于查询,会用到SELECT语句,我们通过刚才建立的works数据表,查询里面年龄大于等于22岁的记录。首先看一下数据库里面的works表里面都有哪些内容:
接下来将这些数据查询出来,代码如下:
import pymysql
db = pymysql.connect(host = 'localhost',user = 'root',password = '不告诉你哦',port = 3306,db = 'test1')
cursor = db.cursor()
sql = 'SELECT * FROM works WHERE age>=21'
try:
cursor.execute(sql)
print('count:',cursor.rowcount)
one = cursor.fetchone()
print('The first One:',one)
results = cursor.fetchall()
print('Results:',results)
print('Results Type:',type(results))
for row in results:
print(row)
except:
print('Error')
结果:
这里构造了一条SQL语句,将年龄22岁及以上的学生查询出来,然后将其传给execute()方法。注意,这里不再需要db的commit()方法。接着,调用cursor的rowcount属性获取查询结果的条数,当前示例中是4条。然后调用了fetchone()方法, 这个方法可以获取结果的第一条数据, 返回结果是元组形式,元组的无素顺序跟字段对应。随后, 又调用了fetchall()方法,它可以得到结果的所有数据。然后将其结果和类型打印出来,它是二重元组,每个元素都是一条记录, 我们将其遍历输出来。
但是这里需要注意一个问题, 这里显示的是3条数据而不是4条,fetchall()方法不是获取所有数据吗?这是因为它的内部实现有一个偏移指针用来指向查询结果,最开始偏移指针指向第一条数据,取一次之后,指针偏移到下条数据,这样再取的话,就会取到下一条数据了 。我们最初调用了一次fetchone()方法,这样结果的偏移指针就指向下一条数据,fetchall()方法返回的是偏移指针指向的数据一直到结束的所有数据,所以该方法获取的结果就只剩3个了。
此外,我们还可以用while循环加fetchone()方法来获取所有数据,而不是用fetchall()全部一起获取出来。fetchall()会将结果以元组形式全部返回,如果数据量很大,那么占用的开销会非常高。因此,推荐使用如下方法来逐条取数据:
sql = 'SELECT * FROM works WHERE age>=21'
try:
cursor.execute(sql)
print('count:',cursor.rowcount)
row= cursor.fetchone()
while row:
print('Row:',row)
row = cursor.fetchone()
except:
print('Error')
结果:
这样就通过row = cursor.fetchone(),每次循环之后指针就会偏移一条数据,取出所有的数据了。
三 总结
这篇文章主要讲了Python操作关系型数据库MySQL,包括链接数据库、创建数据库数据表、往数据库里面插入数据、事务的ACID四个特性、更新数据库、删除和查询数据库。以上内容参考资料:夏敏捷《Python程序设计-从基础到开发》,崔庆才《Python3 网络爬虫开发实战》,[挪]芒努斯·利·海特兰德(Magnus Lie Hetland)《Python基础教程第3版 Python编程从入门到实践 》,并对以上作者表示感谢。这篇文章就到这里了,欢迎大佬们多批评指正,也欢迎大家积极评论多多交流。