Python3网络爬虫实战---3二、数据存储:关系型数据库存储:MySQL

上一篇文章: Python3网络爬虫实战---3一、数据存储:文件存储
下一篇文章: Python3网络爬虫实战---3三、数据存储:非关系型数据库存储:MongoDB

关系型数据库基于关系模型的数据库,而关系模型是经过二维表来保存的,因此它的存储方式就是行列组成的表,每一列是一个字段,每一行是一条记录。表能够看做是某个实体的集合,而实体之间存在联系,这就须要表与表之间的关联关系来体现,如主键外键的关联关系,多个表组成一个数据库,也就是关系型数据库。mysql

关系型数据库有多种,如 SQLite、MySQL、Oracle、SQL Server、DB2等等。sql

在本节咱们主要介绍 Python3 下 MySQL 的存储。数据库

在 Python2 中,链接 MySQL 的库大可能是使用 MySQLDB,可是此库官方并不支持 Python3,因此在这里推荐使用的库是 PyMySQL。segmentfault

本节来说解一下 PyMySQL 操做 MySQL 数据库的方法。数组

1. 准备工做

在本节开始以前请确保已经安装好了 MySQL 数据库并正常运行,并且须要安装好 PyMySQL 库,若是没有安装,能够参考第一章的安装说明。网络

2. 链接数据库

在这里咱们首先尝试链接一下数据库,假设当前的 MySQL运行在本地,用户名为 root,密码为 123456,运行端口为 3306,在这里咱们利用 PyMySQL 先链接一下 MySQL 而后建立一个新的数据库,名字叫作 spiders,代码以下:并发

import pymysql

db = pymysql.connect(host='localhost',user='root', password='123456', port=3306)
cursor = db.cursor()
cursor.execute('SELECT VERSION()')
data = cursor.fetchone()
print('Database version:', data)
cursor.execute("CREATE DATABASE spiders DEFAULT CHARACTER SET utf8")
db.close()

运行结果:ide

Database version: ('5.6.22',)

在这里咱们经过 PyMySQL 的 connect() 方法声明了一个 MySQL 链接对象,须要传入 MySQL 运行的 host 即 IP,此处因为 MySQL 在本地运行,因此传入的是 localhost,若是 MySQL 在远程运行,则传入其公网 IP 地址,而后后续的参数 user 即用户名,password 即密码,port 即端口默认 3306。fetch

链接成功以后,咱们须要再调用 cursor() 方法得到 MySQL 的操做游标,利用游标来执行 SQL 语句,例如在这里咱们执行了两句 SQL,用 execute() 方法执行相应的 SQL 语句便可,第一句 SQL 是得到 MySQL 当前版本,而后调用fetchone() 方法来得到第一条数据,也就获得了版本号,另外咱们还执行了建立数据库的操做,数据库名称叫作 spiders,默认编码为 utf-8,因为该语句不是查询语句,因此直接执行后咱们就成功建立了一个数据库 spiders,接着咱们再利用这个数据库进行后续的操做。编码

3. 建立表

通常来讲上面的建立数据库操做咱们只须要执行一次就行了,固然咱们也能够手动来建立数据库,之后咱们的操做都是在此数据库上操做的,因此后文介绍的 MySQL 链接会直接指定当前数据库 spiders,全部操做都是在 spiders 数据库内执行的。

因此这里MySQL的链接就须要额外指定一个参数 db。

而后接下来咱们新建立一个数据表,执行建立表的 SQL 语句便可,建立一个用户表 students,在这里指定三个字段,结构以下:

字段名 含义 类型
id 学号 varchar
name 姓名 varchar
age 年龄 int

建立表的示例代码以下:

import pymysql

db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.cursor()
sql = 'CREATE TABLE IF NOT EXISTS students (id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))'
cursor.execute(sql)
db.close()

运行以后咱们便建立了一个名为 students 的数据表,字段即为上文列举的三个字段。

固然在这里做为演示咱们指定了最简单的几个字段,实际在爬虫过程当中咱们会根据爬取结果设计特定的字段。

4. 插入数据

咱们将数据解析出来后的下一步就是向数据库中插入数据了,例如在这里咱们爬取了一个的学生信息,学号为 20120001,名字为 Bob,年龄为 20,那么如何将该条数据插入数据库呢,实例代码以下:

import pymysql

id = '20120001'
user = 'Bob'
age = 20

db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.cursor()
sql = 'INSERT INTO students(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 students(id, name, age) values(' + id + ', ' + name + ', ' + age + ')'

这样的写法繁琐并且不直观,因此咱们选择直接用格式化符 %s 来实现,有几个 Value 写几个 %s,咱们只须要在 execute() 方法的第一个参数传入该 SQL 语句,Value 值用统一的元组传过来就行了。

这样的写法有既能够避免字符串拼接的麻烦,又能够避免引号冲突的问题。

以后值得注意的是,须要执行 db 对象的 commit() 方法才可实现数据插入,这个方法才是真正将语句提交到数据库执行的方法,对于数据插入、更新、删除操做都须要调用该方法才能生效。

接下来咱们加了一层异常处理,若是执行失败,则调用rollback() 执行数据回滚,至关于什么都没有发生过同样。

在这里就涉及一个事务的问题,事务机制能够确保数据的一致性,也就是这件事要么发生了,要么没有发生,好比插入一条数据,不会存在插入一半的状况,要么所有插入,要么整个一条都不插入,这就是事务的原子性,另外事务还有另外三个属性,一致性、隔离性、持久性,一般成为 ACID 特性。

概括以下:

属性 解释
原子性(atomicity) 一个事务是一个不可分割的工做单位,事务中包括的诸操做要么都作,要么都不作。
一致性(consistency) 事务必须是使数据库从一个一致性状态变到另外一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation) 一个事务的执行不能被其余事务干扰。即一个事务内部的操做及使用的数据对并发的其余事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability) 持续性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其余操做或故障不该该对其有任何影响。

插入、更新、删除操做都是对数据库进行更改的操做,更改操做都必须为一个事务,因此对于这些操做的标准写法就是:

try:
    cursor.execute(sql)
    db.commit()
except:
    db.rollback()

这样咱们即可以保证数据的一致性,在这里的 commit() 和 rollback() 方法就是为事务的实现提供了支持。

好,在上面咱们了解了数据插入的操做,是经过构造一个 SQL 语句来实现的,可是很明显,这里有一个及其不方便的地方,好比又加了一个性别 gender,假如忽然增长了一个字段,那么咱们构造的 SQL 语句就须要改为:

INSERT INTO students(id, name, age, gender) values(%s, %s, %s, %s)

相应的元组参数则须要改为:

(id, name, age, gender)

这显然不是咱们想要的,在不少状况下,咱们要达到的效果是插入方法无需改动,作成一个通用方法,只须要传入一个动态变化的字典给就行了。好比咱们构造这样一个字典:

{
    'id': '20120001',
    'name': 'Bob',
    'age': 20
}

而后 SQL 语句会根据字典动态构造,元组也动态构造,这样才能实现通用的插入方法。因此在这里咱们须要将插入方法改写一下:

data = {
    'id': '20120001',
    'name': 'Bob',
    'age': 20
}
table = 'students'
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(sql, 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 students(id, name, age) VALUES (%s, %s, %s)

最后再 execute() 方法的第一个参数传入 sql 变量,第二个参数传入 data 的键值构造的元组,就能够成功插入数据了。

如此以来,咱们便实现了传入一个字典来插入数据的方法,不须要再去修改 SQL 语句和插入操做了。

5. 更新数据

数据更新操做实际上也是执行 SQL 语句,最简单的方式就是构造一个 SQL 语句而后执行:

sql = 'UPDATE students SET age = %s WHERE name = %s'
try:
   cursor.execute(sql, (25, 'Bob'))
   db.commit()
except:
   db.rollback()
db.close()

在这里一样是用占位符的方式构造 SQL,而后执行 excute() 方法,传入元组形式的参数,一样执行 commit() 方法执行操做。

若是要作简单的数据更新的话,使用此方法是彻底能够的。

可是在实际数据抓取过程当中,在大部分状况下是须要插入数据的,可是咱们关心的是会不会出现重复数据,若是出现了重复数据,咱们更但愿的作法通常是更新数据而不是重复保存一次,另外就是像上文所说的动态构造 SQL 的问题,因此在这里咱们在这里从新实现一种能够作到去重的作法,若是重复则更新数据,若是数据不存在则插入数据,另外支持灵活的字典传值。

data = {
    'id': '20120001',
    'name': 'Bob',
    'age': 21
}

table = 'students'
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 仍然为 20120001,可是年龄有所变化,由 20 变成了 21,但在这条数据不会被插入,而是将 id 为 20120001 的数据更新。

在这里完整的 SQL 构造出来是这样的:

INSERT INTO students(id, name, age) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE id = %s, name = %s, age = %s

相比上面介绍的插入操做的 SQL,后面多了一部份内容,那就是更新的字段,ON DUPLICATE KEY UPDATE 使得主键已存在的数据进行更新,后面跟的是更新的字段内容。因此这里就变成了 6 个 %s。因此在后面的 execute() 方法的第二个参数元组就须要乘以 2 变成原来的 2 倍。

如此一来,咱们就能够实现主键不存在便插入数据,存在则更新数据的功能了。

6. 删除数据

删除操做相对简单,使用 DELETE 语句便可,须要指定要删除的目标表名和删除条件,并且仍然须要使用 db 的 commit() 方法才能生效,实例以下:

table = 'students'
condition = 'age > 20'

sql = 'DELETE FROM  {table} WHERE {condition}'.format(table=table, condition=condition)
try:
    cursor.execute(sql)
    db.commit()
except:
    db.rollback()

db.close()

在这里咱们指定了表的名称,删除条件。由于删除条件可能会有多种多样,运算符好比有大于、小于、等于、LIKE等等,条件链接符好比有 AND、OR 等等,因此再也不继续构造复杂的判断条件,在这里直接将条件看成字符串来传递,以实现删除操做。

7. 查询数据

说完插入、修改、删除等操做,还剩下很是重要的一个操做,那就是查询。

在这里查询用到 SELECT 语句,咱们先用一个实例来感觉一下:

sql = 'SELECT * FROM students WHERE age >= 20'

try:
    cursor.execute(sql)
    print('Count:', cursor.rowcount)
    one = cursor.fetchone()
    print('One:', one)
    results = cursor.fetchall()
    print('Results:', results)
    print('Results Type:', type(results))
    for row in results:
        print(row)
except:
    print('Error')

运行结果:

Count: 4
One: ('20120001', 'Bob', 25)
Results: (('20120011', 'Mary', 21), ('20120012', 'Mike', 20), ('20120013', 'James', 22))
Results Type: <class 'tuple'>
('20120011', 'Mary', 21)
('20120012', 'Mike', 20)
('20120013', 'James', 22)

在这里咱们构造了一条 SQL 语句,将年龄 20 岁及以上的学生查询出来,而后将其传给 execute() 方法便可,注意在这里再也不须要 db 的 commit() 方法。而后咱们能够调用 cursor 的 rowcount 属性获取查询结果的条数,当前示例中获取的结果条数是 4 条。

而后咱们调用了 fetchone() 方法,这个方法能够获取结果的第一条数据,返回结果是元组形式,元组的元素顺序跟字段一一对应,也就是第一个元素就是第一个字段 id,第二个元素就是第二个字段 name,以此类推。随后咱们又调用了fetchall() 方法,它能够获得结果的全部数据,而后将其结果和类型打印出来,它是二重元组,每一个元素都是一条记录。咱们将其遍历输出,将其逐个输出出来。

可是这里注意到一个问题,显示的是4条数据,fetall() 方法不是获取全部数据吗?为何只有3条?这是由于它的内部实现是有一个偏移指针来指向查询结果的,最开始偏移指针指向第一条数据,取一次以后,指针偏移到下一条数据,这样再取的话就会取到下一条数据了。因此咱们最初调用了一次 fetchone() 方法,这样结果的偏移指针就指向了下一条数据,fetchall() 方法返回的是偏移指针指向的数据一直到结束的全部数据,因此 fetchall() 方法获取的结果就只剩 3 个了,因此在这里要理解偏移指针的概念。

因此咱们还能够用 while 循环加 fetchone() 的方法来获取全部数据,而不是用 fetchall() 所有一块儿获取出来,fetchall() 会将结果以元组形式所有返回,若是数据量很大,那么占用的开销会很是高。因此推荐使用以下的方法来逐条取数据:

sql = 'SELECT * FROM students WHERE age >= 20'
try:
    cursor.execute(sql)
    print('Count:', cursor.rowcount)
    row = cursor.fetchone()
    while row:
        print('Row:', row)
        row = cursor.fetchone()
except:
    print('Error')

这样每循环一次,指针就会偏移一条数据,随用随取,简单高效。

8. 结语

本节咱们介绍了 PyMySQL 操做 MySQL 数据库以及一些SQL语句的构造方法,在后文咱们会在实战案例中应用这些操做进行数据存储。

上一篇文章: Python3网络爬虫实战---3一、数据存储:文件存储
下一篇文章: Python3网络爬虫实战---3三、数据存储:非关系型数据库存储:MongoDB
相关文章
相关标签/搜索