Python-自动化指南-繁琐工作自动化-第三版-十-
Python 自动化指南(繁琐工作自动化)第三版(十)
原文:
automatetheboringstuff.com/译者:飞龙
15 GOOGLE SHEETS

Google Sheets 是一个免费的、基于网络的电子表格应用程序,任何拥有 Google 账户或 Gmail 地址的人都可以使用,它已经成为一个功能丰富的 Excel 竞争对手。Google Sheets 有自己的 API,但这个 API 的学习和使用可能会让人感到困惑。本章介绍了 EZSheets 第三方库,它为你提供了一个更简单的方式来执行常见操作,处理 Google Sheets API 的细节,这样你就不必学习它们。
安装和设置 EZSheets
你可以通过遵循附录 A 中的说明使用 pip 命令行工具安装 EZSheets。
在你的 Python 脚本可以使用 EZSheets 访问和编辑你的 Google Sheets 电子表格之前,你需要一个凭证 JSON 文件和两个令牌 JSON 文件。创建凭证有五个部分:
-
创建一个新的 Google Cloud 项目。
-
为你的项目启用 Google Sheets API 和 Google Drive API。
-
配置 OAuth 同意屏幕。
-
创建凭证。
-
使用凭证文件登录。
这可能看起来像是一项繁重的工作,但你只需设置一次,而且这是免费的。你需要一个 Google/Gmail 账户;我强烈建议创建一个新的 Google 账户而不是使用现有的一个,以防止你的 Python 脚本中的错误影响到你个人 Google 账户中的电子表格。在整个章节中,我会说 你的 Google 账户 和 你的 Gmail 电子邮件地址 来指代拥有你 Python 程序访问的电子表格的 Google 账户。
Google 可能会稍微更改其 Google Cloud Console 网站上的布局或措辞。然而,我概述的基本步骤应该保持不变。
创建新的 Google Cloud 项目
首先,你需要设置一个 Google Cloud 项目。在你的浏览器中,转到 console.cloud.google.com 并使用你的用户名和密码登录你的 Google 账户。你将被带到入门页面。在页面顶部,点击 选择项目。在出现的弹出窗口中,点击 新建项目。这应该会带你到一个新的项目页面。
Google Cloud 会为你生成一个项目名称,例如“我的项目 23135”,以及一个随机项目 ID,例如“macro-nuance-362516”。这些值不会对使用你的 Python 脚本的用户可见,你可以将项目名称更改为你想要的任何名称,但你不能更改项目 ID。我只是使用网站为我生成的默认名称。你可以将位置设置为“无组织”。免费 Google 账户最多可以有 12 个项目,但你只需要一个项目来创建你想要的全部 Python 脚本。点击蓝色 创建 按钮来创建项目。
启用 Sheets 和 Drive API
在 console.cloud.google.com 页面上,点击左上角的 导航 按钮。(图标有三个水平条纹,通常被称为 汉堡 图标。)转到 APIs & Services 库 以访问 API 库页面。您将看到许多用于 Gmail、Google Maps、Google Cloud Storage 和其他 Google 服务的 Google API。我们需要允许我们的项目使用 Google Sheets 和 Google Drive API。EZSheets 使用 Google Drive API 上传和下载电子表格文件。
滚动到页面底部,找到 Google Sheets API,并点击它,或者将“Google Sheets API”输入搜索栏以找到它。这应该会带您到 Google Sheets API 页面。点击蓝色的 启用 按钮以启用您的 Google Cloud 项目使用 Google Sheets API。您将被重定向到 APIs & Services已启用 API & Services 页面,在那里您可以找到有关您的 Python 脚本使用此 API 频率的信息。重复此过程以启用 Google Drive API。
接下来,您需要配置您项目的 OAuth 同意屏幕。
配置 OAuth 同意屏幕
当用户首次运行 import ezsheets 时,OAuth 同意屏幕将显示给用户。在 步骤 1 OAuth 同意屏幕 页面上,选择 外部 并点击蓝色的 创建 按钮。下一页应显示 OAuth 同意屏幕的外观。为 App 名称字段选择一个名称(我使用一些通用的名称,例如“Python Google API 脚本”),并在用户支持电子邮件和开发者联系信息字段中输入您的电子邮件地址。然后点击 保存并继续 按钮。
在 步骤 2 范围 页面上,定义您项目的范围,即项目允许访问的资源权限。点击 添加或删除范围 按钮,并在出现的新面板中,通过表格勾选 .../auth/drive(Google Drive API)和 .../auth/spreadsheets(Google Sheets API)的范围复选框。然后,点击蓝色的 更新 按钮,然后点击 保存并继续。
步骤 3 测试用户 页面要求您添加您将与之交互的电子表格所属的 Google 账户的 Gmail 电子邮件地址。除非您通过 Google 的应用程序审核流程,否则您的脚本将仅限于与您在此步骤中提供的电子邮件地址进行交互。点击 + 添加用户 按钮。在出现的新的面板中,输入您的 Google 账户的 Gmail 地址,并点击蓝色的 添加 按钮。然后点击 保存并继续。
步骤 4 摘要 页面提供了之前步骤的摘要。如果所有信息看起来都正确,点击 返回仪表板 按钮。下一步是为您的项目创建凭证。
创建凭证
首先,您需要创建一个凭证文件。EZSheets 需要这个文件来使用 Google API,即使是公开共享的电子表格也是如此。从导航侧边栏菜单中,点击 APIs & Services 然后点击 Credentials 以转到凭证页面。然后点击页面顶部的 + 创建凭证 链接。应该会打开一个子菜单,询问您想创建哪种凭证:API 密钥、OAuth 客户端 ID 或服务帐户。点击 OAuth Client ID。
在下一页,选择应用程序类型为 桌面应用程序,并将名称保留为默认的“桌面客户端 1”。如果您想的话,也可以将其更改为不同的名称;这不会显示在您的 Python 脚本用户面前。点击蓝色 创建 按钮。
应该会出现一个弹出窗口。点击 下载 JSON 下载凭证文件,该文件可能具有类似 client_secret_282792235794-p2o9gfcub4htibfg2u207gcomco9nqm7.apps.googleusercontent.com.json 的名称。将其放在与您的 Python 脚本相同的文件夹中。为了简单起见,您还可以将 JSON 文件重命名为 credentials-sheets.json。EZSheets 会搜索 credentials-sheets.json 或任何匹配 client_secret_.json* 格式的文件。
使用凭证文件登录
在凭证 JSON 文件所在的同一文件夹中运行 Python 交互式外壳,然后运行 import ezsheets。EZSheets 会通过调用 ezsheets.init() 函数自动检查当前工作目录中的凭证 JSON 文件。如果找到该文件,EZSheets 将启动您的网络浏览器到 OAuth 同意屏幕以生成令牌文件。EZSheets 还需要这些名为 token-drive.pickle 和 token-sheets.pickle 的令牌文件,以及凭证文件来访问电子表格。生成令牌文件是一个一次性设置步骤,下次您运行 import ezsheets 时不会发生。
使用 Google 帐户登录。这必须是您在配置 Google Cloud 项目的 OAuth 同意屏幕时为“测试用户”提供的同一电子邮件地址。您应该会收到一条警告消息,内容为“Google 没有验证此应用”,这是正常的,因为您是应用创建者。点击 继续 链接。您应该到达另一个页面,上面写着“Python Google API 脚本希望访问您的 Google 帐户”(或您在 OAuth 同意屏幕设置中给出的任何名称)。点击 继续。您将来到一个显示“身份验证流程已完成”的普通网页。现在您可以关闭浏览器窗口。
完成对 Sheets API 的身份验证流程后,您必须在打开的下一个窗口中为 Drive API 重复此过程。关闭第二个窗口后,您现在应该会在与您的凭证 JSON 文件相同的文件夹中看到 token-drive.pickle 和 token-sheets.pickle 文件。将这些文件视为密码,不要分享它们:它们可以用来登录并访问您的 Google Sheets 电子表格。
撤销凭证文件
如果您不小心与某人分享了凭证或令牌文件,他们无法更改您的 Google 账户密码,但他们将能够访问您的电子表格。您可以通过登录到console.developers.google.com来撤销这些文件。然后,在侧边栏中点击凭证链接。然后,在 OAuth 2.0 客户端 ID 表中,点击您不小心分享的凭证文件旁边的垃圾桶图标。一旦撤销,凭证和令牌文件就变得无用,您可以删除它们。然后,您将需要生成新的凭证 JSON 文件和令牌文件。
电子表格对象
在 Google Sheets 中,一个电子表格可以包含多个工作表(也称为工作表),每个工作表包含单元格的列和行。单元格包含数据,如数字、日期或文本片段。单元格还具有字体、宽度和高度以及背景颜色等属性。图 15-1 显示了标题为Sweigart Books的电子表格,包含两个工作表,分别命名为Books和Websites。您可以通过访问autbor.com/examplegs在浏览器中查看此电子表格。每个工作表的第一列标记为A,第一行标记为1。(这与 Python 列表不同,Python 列表的第一个元素出现在索引 0 处。)

图 15-1:标题为 Sweigart Books 的电子表格,包含两个工作表,Books 和 Websites
虽然您的大部分工作将涉及修改Sheet对象,但您也可以修改Spreadsheet对象,正如您将在下一节中看到的。
创建、上传和列出电子表格
您可以从现有的 Google Sheets 电子表格、一个新的空白电子表格或上传的 Excel 电子表格创建一个新的Spreadsheet对象。所有 Google Sheets 电子表格都有一个唯一的 ID,可以在其 URL 中找到,在spreadsheets/d/部分之后和/edit部分之前。例如,在 URLdocs.google.com/spreadsheets/d/1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/edit#gid=0/中,ID 将是1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI。
Google Sheets 电子表格表示为ezsheets.Spreadsheet对象,该对象具有id、url和title属性。您可以使用Spreadsheet()函数创建一个新的空白电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Title of My New Spreadsheet'
>>> ss.title
'Title of My New Spreadsheet'
>>> ss.url
'https://docs.google.com/spreadsheets/d/1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40/'
>>> ss.id
'1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40'
您也可以通过传递其 ID 或 URL,或者重定向到其 URL 的 URL 来加载现有的电子表格:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss2 = ezsheets.Spreadsheet('https://docs.google.com/spreadsheets/d/1TzOJxh
NKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/')
>>> ss3 = ezsheets.Spreadsheet('1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI')
>>> ss1 == ss2 == ss3 # These are the same spreadsheet.
True
要上传现有的 Excel、OpenOffice、CSV 或 TSV 电子表格到 Google Sheets,请将电子表格的文件名传递给 ezsheets.upload()。在交互式外壳中输入以下内容,将 my_spreadsheet.xlsx 替换为您自己的电子表格文件:
>>> import ezsheets
>>> ss = ezsheets.upload('`my_spreadsheet.xlsx`')
>>> ss.title
'`my_spreadsheet`'
您可以通过调用 listSpreadsheets() 函数列出您 Google 账户中的电子表格。此函数返回一个字典,其键是电子表格 ID,其值是每个电子表格的标题。它包括您账户的 回收站 文件夹中的已删除电子表格。在上传电子表格后,尝试在交互式外壳中输入以下内容:
>>> ezsheets.listSpreadsheets()
{'`1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU`': 'Education Data'}
一旦您获得了 Spreadsheet 对象,您就可以使用其属性和方法来操作托管在 Google Sheets 上的在线电子表格。
访问电子表格属性
虽然实际数据生活在电子表格的各个工作表中,但 Spreadsheet 对象具有以下属性来操作电子表格本身:title、id、url、sheetTitles 和 sheets。让我们检查autbor.com/examplegs上的电子表格。您的 Google 账户有权查看但没有修改权限,但您可以将工作表复制到您自己的账户中新建的电子表格中:
>>> import ezsheets
>>> example_ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss = ezsheets.Spreadsheet()
>>> example_ss.sheets[0].copyTo(ss)
>>> ss.sheets[0].delete() # Delete the Sheet1 sheet.
>>> ss.url
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
新复制的电子表格将具有标题 Copy of Books,因为 Books 是原始电子表格的名称。继续使用以下代码进行交互式外壳示例:
>>> ss.title # The title of the spreadsheet
'Untitled spreadsheet'
>>> ss.title = 'Sweigart Books' # Change the title.
>>> ss.id # The unique ID (a read-only attribute)
'15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM'
>>> ss.url # The original URL (a read-only attribute)
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
>>> ss.sheetTitles # The titles of all the Sheet objects
('Copy of Books',)
>>> ss.sheets # The Sheet objects in this Spreadsheet, in order
(<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>,)
>>> ss.sheets[0] # The first Sheet object in this Spreadsheet
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss['Copy of Books'] # Sheets can also be accessed by title.
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss.Sheet('New blank sheet') # Create a new sheet.
<Sheet sheetId=1759616008, title='New blank sheet', rowCount=1000, columnCount=26>
>>> ss.sheets[1].delete() # Delete the second Sheet object in this Spreadsheet.
如果有人在浏览器中更改了电子表格,您的脚本可以通过调用 refresh() 方法来更新 Spreadsheet 对象以匹配在线数据:
>>> ss.refresh()
这将不仅刷新 Spreadsheet 对象的属性,还包括它包含的 Sheet 对象中的数据。您将实时看到对 Spreadsheet 对象所做的更改。
下载和上传电子表格
您可以以多种格式下载 Google Sheets 电子表格:Excel、OpenOffice、CSV、TSV 和 PDF。您还可以将其下载为包含电子表格数据的 HTML 文件 ZIP 文件。EZSheets 包含针对这些选项的函数:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss.title
'Sweigart Books (DO NOT DELETE)'
>>> ss.downloadAsExcel() # Downloads the spreadsheet as an Excel file
'Sweigart_Books.xlsx'
>>> ss.downloadAsODS() # Downloads the spreadsheet as an OpenOffice file
'Sweigart_Books.ods'
>>> ss.downloadAsCSV() # Downloads only the first sheet as a CSV file
'Sweigart_Books.csv'
>>> ss.downloadAsTSV() # Downloads only the first sheet as a TSV file
'Sweigart_Books.tsv'
>>> ss.downloadAsPDF() # Downloads the spreadsheet as a PDF
'Sweigart_Books.pdf'
>>> ss.downloadAsHTML() # Downloads the spreadsheet as a ZIP of HTML files
'Sweigart_Books.zip'
注意,CSV 或 TSV 格式的文件只能包含一个工作表;因此,如果您以这些格式之一下载 Google Sheets 电子表格,您将只获得第一个工作表。要下载其他工作表,您需要在下载之前重新排列 Sheet 对象。
下载函数都返回下载文件的文件名字符串。您还可以通过将新文件名传递给下载函数来为电子表格指定自己的文件名:
>>> ss.downloadAsExcel('`a_different_filename`.xlsx')
'`a_different_filename`.xlsx'
该函数返回本地文件名。
删除电子表格
要删除电子表格,请调用 delete() 方法:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet() # Create the spreadsheet.
>>> ezsheets.listSpreadsheets() # Confirm that we've created a spreadsheet.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
>>> ss.delete() # Delete the spreadsheet.
>>> ezsheets.listSpreadsheets() # Spreadsheets in the Trash folder are still listed.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
delete()方法将把您的电子表格移动到 Google Drive 上的垃圾箱文件夹。您可以在drive.google.com/drive/trash查看您的垃圾箱文件夹的内容。请注意,垃圾箱中的电子表格仍会出现在listSpreadsheets()返回的字典中。要永久删除电子表格,请将permanent关键字参数传递为True:
>>> ss.delete(permanent=True)
>>> ezsheets.listSpreadsheets()
{}
通常,使用自动化脚本来永久删除电子表格不是一个好主意,因为无法恢复脚本中的错误意外删除的电子表格。即使免费的 Google Drive 账户也有数 GB 的存储空间可用,所以您很可能不需要担心释放空间。
工作表对象
Spreadsheet对象将有一个或多个Sheet对象。Sheet对象代表每个工作表中数据的行和列。您可以使用方括号运算符和一个整数索引来访问这些工作表。
Spreadsheet对象的sheets属性包含一个元组,其中包含按电子表格中出现的顺序排列的Sheet对象。要访问电子表格中的Sheet对象,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet() # Starts with a sheet named Sheet1
>>> sheet2 = ss.Sheet('Spam')
>>> sheet3 = ss.Sheet('Eggs')
>>> ss.sheets # The Sheet objects in this Spreadsheet, in order
(<Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26>, <Sheet sheetId=284204004,
title='Spam', rowCount=1000, columnCount=26>, <Sheet sheetId=1920032872, title='Eggs',
rowCount=1000, columnCount=26>)
>>> ss.sheets[0] # Gets the first Sheet object in this Spreadsheet
<Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26>
Spreadsheet对象的sheetTitles属性包含一个元组,其中包含所有工作表的标题。例如,请在交互式外壳中输入以下内容:
>>> ss.sheetTitles # The titles of all the Sheet objects in this Spreadsheet
('Sheet1', 'Spam', 'Eggs')
一旦您有一个Sheet对象,您可以使用Sheet对象的方法从它读取数据并向它写入数据,如下一节所述。
读取和写入数据
就像在 Excel 中一样,Google Sheets 工作表有包含数据的单元格列和行。您可以使用方括号运算符[]从这些单元格中读取和写入数据。例如,要创建一个新的电子表格并向其中添加数据,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'My Spreadsheet'
>>> sheet = ss.sheets[0] # Get the first sheet in this spreadsheet.
>>> sheet.title
'Sheet1'
>>> sheet['A1'] = 'Name' # Set the value in cell A1.
>>> sheet['B1'] = 'Age'
>>> sheet['C1'] = 'Favorite Movie'
>>> sheet['A1'] # Read the value in cell A1.
'Name'
>>> sheet['A2'] # Empty cells return a blank string.
''
>>> sheet[2, 1] # Column 2, Row 1 is the same address as B1.
'Age'
>>> sheet['A2'] = 'Alice'
>>> sheet['B2'] = 30
>>> sheet['C2'] = 'RoboCop'
>>> sheet['B2'] # Note that all data is returned as strings.
'30'
这些说明应该生成一个看起来像图 15-2 的 Google Sheets 电子表格。

图 15-2 使用示例说明创建的电子表格
当首次加载Spreadsheet对象时,Sheet对象中的所有数据都会被加载,因此数据可以立即读取。然而,将值写入在线电子表格需要网络连接,可能需要大约一秒钟。如果你有数千个单元格需要更新,逐个更新它们可能会相当慢。相反,接下来的几节将展示如何一次性更新整个行和列。
列和行的地址
单元格地址在 Google Sheets 中的工作方式与 Excel 相同。唯一的区别是,与 Python 的基于 0 的列表索引不同,Google Sheets 的列和行是基于 1 的:第一列或行在索引 1,而不是 0。您可以使用convertAddress()函数将'A2'字符串样式的地址转换为(column, row)元组样式的地址(反之亦然)。getColumnLetterOf()和getColumnNumberOf()函数也将列地址在字母和数字之间进行转换。例如,将以下内容输入到交互式外壳中:
>>> import ezsheets
>>> ezsheets.convertAddress('A2') # Converts addresses...
(1, 2)
>>> ezsheets.convertAddress(1, 2) # ...and converts them back, too.
'A2'
>>> ezsheets.getColumnLetterOf(2)
'B'
>>> ezsheets.getColumnNumberOf('B')
2
>>> ezsheets.getColumnLetterOf(999)
'ALK'
>>> ezsheets.getColumnNumberOf('ZZZ')
18278
如果您在源代码中输入地址,字符串样式的'A2'地址很方便。但如果您在循环遍历地址范围并需要一个列的数字标识符时,(column, row)元组样式的地址更方便。当您需要在这两种格式之间进行转换时,convertAddress()、getColumnLetterOf()和getColumnNumberOf()函数很有帮助。
读取和写入整个列和行
如前所述,逐个单元格写入数据往往需要花费太多时间。幸运的是,EZSheets 提供了用于同时读取和写入整个列和行的Sheet方法。getColumn()、getRow()、updateColumn()和updateRow()方法将分别读取和写入列和行。这些方法会向 Google Sheets 服务器发送请求以更新电子表格,因此需要您连接到互联网。在本节的示例中,我们将从第十四章上传produceSales3.xlsx到 Google Sheets。您可以从本书的在线资源中下载它。前八行看起来像表 15-1。
表 15-1:produceSales3.xlsx 电子表格的前八行
| A | B | C | D | |
|---|---|---|---|---|
| 1 | 产品 | 每磅成本 | 销售磅数 | 总计 |
| 2 | 马铃薯 | 0.86 | 21.6 | 18.58 |
| 3 | 秋葵 | 2.26 | 38.6 | 87.24 |
| 4 | 菜豆 | 2.69 | 32.8 | 88.23 |
| 5 | 西瓜 | 0.66 | 27.3 | 18.02 |
| 6 | 大蒜 | 1.19 | 4.9 | 5.83 |
| 7 | 胡萝卜 | 2.27 | 1.1 | 2.5 |
| 8 | 芦笋 | 2.49 | 37.9 | 94.37 |
要上传此电子表格,请将produceSales3.xlsx文件放在当前工作目录中,并在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.upload('produceSales3.xlsx')
>>> sheet = ss.sheets[0]
>>> sheet.getRow(1) # The first row is row 1, not row 0.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> sheet.getRow(2)
['Potatoes', '0.86', '21.6', '18.58', '', '']
>>> sheet.getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getColumn('A') # The same result as getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getRow(3)
['Okra', '2.26', '38.6', '87.24', '', '']
>>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230'])
>>> sheet.getRow(3)
['Pumpkin', '11.50', '20', '230', '', '']
>>> columnOne = sheet.getColumn(1)
>>> for i, value in enumerate(columnOne):
... # Make the Python list contain uppercase strings:
... columnOne[i] = value.upper()
...
>>> sheet.updateColumn(1, columnOne) # Update the entire column in one request.
getRow()和getColumn()函数将特定行或列中每个单元格的数据作为值列表检索。请注意,空单元格在列表中变为空字符串值。您可以将列号或字母传递给getColumn(),以告诉它检索特定列的数据。前面的示例显示,getColumn(1)和getColumn('A')返回相同的列表。
updateRow()和updateColumn()函数将分别用传递给函数的值列表覆盖行或列中的数据。在此示例中,第三行最初包含关于秋葵的信息,但updateRow()调用将其替换为关于南瓜的数据。再次调用sheet.getRow(3)以查看第三行的新值。
如果你有很多单元格需要更新,逐个更新单元格会很慢。获取一个列或行作为列表,更新列表,然后使用列表更新整个列或行要快得多,因为你可以通过一次请求向 Google 的云服务做出所有更改。
要一次性获取所有行,请调用getRows()方法以返回一个列表列表。外层列表中的内层列表代表表格的单行。你可以修改此数据结构中的值来更改某些行的产品名称、销售磅数和总成本。然后,你可以通过在交互式外壳中输入以下内容将其传递给updateRows()方法:
>>> rows = sheet.getRows() # Get every row in the spreadsheet.
>>> rows[0] # Examine the values in the first row.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> rows[1]
['POTATOES', '0.86', '21.6', '18.58', '', '']
>>> rows[1][0] = 'PUMPKIN' # Change the produce name.
>>> rows[1]
['PUMPKIN', '0.86', '21.6', '18.58', '', '']
>>> rows[10]
['OKRA', '2.26', '40', '90.4', '', '']
>>> rows[10][2] = '400' # Change the pounds sold.
>>> rows[10][3] = '904' # Change the total.
>>> rows[10]
['OKRA', '2.26', '400', '904', '', '']
>>> sheet.updateRows(rows) # Update the online spreadsheet with the changes.
你可以通过传递getRows()返回的列表列表到updateRows()中,并修改第 1 行和第 10 行所做的更改,来一次性更新整个表格。
注意,Google Sheets 电子表格中的行末有空字符串。这是因为上传的表格有6列,但我们只有4列的数据。你可以使用rowCount和columnCount属性来读取表格中的行数和列数。然后,通过设置这些值,你可以改变表格的大小:
>>> sheet.rowCount # The number of rows in the sheet
23758
>>> sheet.columnCount # The number of columns in the sheet
6
>>> sheet.columnCount = 4 # Change the number of columns to 4.
>>> sheet.columnCount # Now the number of columns in the sheet is 4.
4
这些说明应该删除produceSales3.xlsx电子表格的第五和第六列,如图 15-3 所示。

图 15-3:更改列数为四之前的表格(顶部)和之后的表格(底部)
根据 Google 的文档,Google Sheets 表格可以包含多达 1000 万个单元格。然而,为了最小化更新和刷新数据所需的时间,最好只创建所需大小的表格。
创建、移动和删除表格
所有 Google Sheets 表格都以一个名为Sheet1的单个表格开始。你可以使用Sheet()方法将额外的表格添加到表格列表的末尾,该方法接受一个可选的字符串作为新表格的标题。可选的第二个参数可以指定新表格的整数索引。要创建电子表格并将其添加到其中,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Multiple Sheets'
>>> ss.sheetTitles
('Sheet1',)
>>> ss.Sheet('Spam') # Create a new sheet at the end of the list of sheets.
<Sheet sheetId=2032744541, title='Spam', rowCount=1000, columnCount=26>
>>> ss.Sheet('Eggs') # Create another new sheet.
<Sheet sheetId=417452987, title='Eggs', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss.Sheet('Bacon', 0) # Create a sheet at index 0 in the list of sheets.
<Sheet sheetId=814694991, title='Bacon', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
这些说明向电子表格中添加了三个新的表格:Bacon、Spam和Eggs(除了默认的Sheet1)。电子表格中的表格是有序的,除非你传递第二个参数到Sheet()指定表格的索引,否则新表格将添加到列表的末尾。在这里,你创建标题为Bacon的表格,索引为0,使Bacon成为电子表格中的第一个表格,并将其他三个表格向右移动一个位置。这与insert()列表方法的行为类似。
你可以在屏幕底部的标签上看到新的表格,如图 15-4 所示。

图 15-4:添加了 Spam、Eggs 和 Bacon 表格后的 Multiple Sheets 电子表格
您可以通过表的索引属性获取表的顺序,然后为此属性分配一个新的索引以重新排序表:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].index
0
>>> ss.sheets[0].index = 2 # Move the sheet at index 0 to index 2.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Bacon', 'Eggs')
>>> ss.sheets[2].index = 0 # Move the sheet at index 2 to index 0.
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
Sheet 对象的 delete() 方法将从电子表格中删除该表。如果您想保留该表但删除其中包含的数据,请调用 clear() 方法以清除所有单元格并使其成为空白表。在交互式外壳中输入以下内容:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].delete() # Delete the sheet at index 0: the "Bacon" sheet.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss['Spam'].delete() # Delete the "Spam" sheet.
>>> ss.sheetTitles
('Sheet1', 'Eggs')
>>> sheet = ss['Eggs'] # Assign a variable to the "Eggs" sheet.
>>> sheet.delete() # Delete the "Eggs" sheet.
>>> ss.sheetTitles
('Sheet1',)
>>> ss.sheets[0].clear() # Clear all the cells on the "Sheet1" sheet.
>>> ss.sheetTitles # The "Sheet1" sheet is empty but still exists.
('Sheet1',)
删除表是永久的;无法恢复数据。但是,您可以通过使用 copyTo() 方法将它们复制到另一个电子表格来备份表,如下一节所述。
复制表
每个 Spreadsheet 对象都包含一个有序的 Sheet 对象列表,您可以使用此列表来重新排序表(如前节所示)或将其复制到其他电子表格。要将 Sheet 对象复制到另一个 Spreadsheet 对象,请调用 copyTo() 方法。将其目标 Spreadsheet 对象作为参数传递。要创建两个电子表格并将第一个电子表格的数据复制到另一个表,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet()
>>> ss1.title = 'First Spreadsheet'
>>> ss1.sheets[0].title = 'Spam' # ss1 will have a sheet named Spam.
>>> ss2 = ezsheets.Spreadsheet()
>>> ss2.title = 'Second Spreadsheet'
>>> ss2.sheets[0].title = 'Eggs' # ss2 will have a sheet named Eggs.
>>> ss1[0]
<Sheet sheetId=0, title='Spam', rowCount=1000, columnCount=26>
>>> ss1[0].updateRow(1, ['Some', 'data', 'in', 'the', 'first', 'row'])
>>> ss1[0].copyTo(ss2) # Copy the ss1's Sheet1 to the ss2 spreadsheet.
>>> ss2.sheetTitles # ss2 now contains a copy of ss1's Sheet1.
('Eggs', 'Copy of Spam')
复制的表在目标电子表格表的列表末尾带有“副本”前缀。如果您愿意,您可以更改它们的 index 属性以在新的电子表格中重新排序。
Google 表单
您的 Google 账户还为您提供了访问 Google 表单的权限,网址为 forms.google.com/。您可以使用 Google 表单创建调查、活动注册或反馈表单,然后接收用户提交的答案,这些答案将显示在 Google Sheets 电子表格中。使用 EZSheets,您的 Python 程序可以访问电子表格中的这些数据。
在第十九章中,您将学习如何安排您的 Python 程序在规定的时间定期运行。您可以编写一个程序,定期检查 Google 表单电子表格中的回复,并检测任何它之前未见过的新的条目。然后,使用第二十章中的信息,您可以让程序发送短信给您,这样您就可以在表单填写完毕时获得实时通知。
正如您所看到的,Python 被广泛认为是连接多个现有软件系统的“胶水”语言,让您创建一个比其各部分总和更强大的自动化流程。
项目 11:伪造区块链加密货币骗局
在这个项目中,我们将使用 Google Sheets 作为伪造的区块链来跟踪 Boringcoin 的交易,Boringcoin 是我推广的一种加密货币骗局。(结果证明,投资者和客户并不关心您的区块链产品是否使用真实的区块链数据结构;他们仍然会给您钱。)
URL autbor.com/boringcoin 重定向到 Boringcoin 区块链的 Google Sheets URL。该电子表格有三个列:交易的发件人、交易的收件人和交易金额。金额从发件人扣除并添加到收件人。如果发件人是 'PRE-MINE',这笔钱将凭空产生并添加到收件人账户中。图 15-5 展示了此 Google Sheet。

图 15-5:存储在 Google Sheet 上的 Boringcoin 假区块链
第一笔交易的发件人是 'PRE-MINE',收件人是 'Al Sweigart',金额是微不足道的 1000000000。然后 'Al Sweigart' 账户将 19116 Boringcoins 转移给 'Miles Bron',后者又将 118 Boringcoins 转移给 'not_a_scammer'。第四笔交易将 16273 Boringcoins 从 'Al Sweigart' 转移给 'some hacker'。(我没有授权这笔交易,并且已经停止使用 python12345 作为我的 Google 账户密码。)
让我们编写两个程序。首先,auditBoringcoin.py 程序检查所有交易并生成所有账户及其当前余额的字典。其次,addBoringcoinTransaction.py 程序为 Google Sheets 的新交易添加一行。这些区块链程序只是为了娱乐,并不是真实的(尽管“真实”的区块链项目,如 NFT 和“web3”,同样是一种幻想)。
第一步:审计假区块链
我们需要编写一个程序来检查整个“区块链”并确定所有账户的当前余额。我们将使用一个字典来保存这些数据,其中键是账户名称的字符串,值是账户中 Boringcoins 的整数。我们还希望程序显示加密货币网络中总共有多少 Boringcoins。我们可以从导入 EZSheets 并设置字典开始:
import ezsheets
ss = ezsheets.Spreadsheet('https://autbor.com/boringcoin')
accounts = {} # Keys are names, and values are amounts.
接下来,我们将遍历电子表格中的每一行,识别发件人、收件人和金额。请注意,Google Sheets 总是返回数据作为字符串,因此我们需要将其转换为整数以对 amount 值进行数学运算:
# Each row is a transaction. Loop over each one:
for row in ss.sheets[0].getRows():
sender, recipient, amount = row[0], row[1], int(row[2])
如果发件人是特殊账户 'PRE-MINE',那么它只是向其他账户提供无限资金的来源。所有最好的加密货币骗局都使用预挖币,我们的也不例外。将金额添加到 accounts 字典中的收件人账户。setdefault() 方法将账户的值设置为 0,如果它在字典中不存在:
if sender == 'PRE-MINE':
# The 'PRE-MINE' sender invents money out of thin air.
accounts.setdefault(recipient, 0)
accounts[recipient] += amount
否则,我们应该从发件人扣除金额并添加到收件人:
else:
# Move funds from the sender to the recipient.
accounts.setdefault(sender, 0)
accounts.setdefault(recipient, 0)
accounts[sender] -= amount
accounts[recipient] += amount
循环结束后,我们可以通过打印 accounts 字典来查看当前的余额。
print(accounts)
作为我们审计的一部分,让我们也浏览这个字典,并将每个人的余额总和加起来,以找出整个网络中有多少 Boringcoins。从 accounts 字典的键值对中开始一个 total 变量,然后使用一个 for 循环遍历每个值。在将每个值添加到 total 后,我们可以打印出 Boringcoins 的总金额:
total = 0
for amount in accounts.values():
total += amount
print('Total Boringcoins:', total)
当我们运行这个程序时,输出看起来像这样:
{'Al Sweigart': 999058553, 'Miles Bron': 38283, 'not_a_scammer': 48441,
'some hacker': 44429, 'Tech Bro': 53424, 'Claire Debella': 54443,
'Credulous Journalist': 50408, 'Birdie Jay': 36832, 'Carol': 82867, 'Mark Z.':
68650, 'Bob': 37920, 'Andi Brand': 57218, 'Eve': 88296, 'Al Sweigart sock
#27': 78080, 'Tax evader': 40937, 'Duke Cody': 17544, 'Lionel Toussaint':
54650, 'some scammer': 2694, 'Alice': 44503, 'David': 41828}
Total Boringcoins: 1000000000
总数是 1000000000,这是有道理的,因为这就是预挖的 Boringcoins 的数量。
第 2 步:制作交易
下一个程序,addBoringcoinTransaction.py,向“区块链”Google Sheets 添加额外的行以添加新交易。它从 sys.argv 列表读取三个命令行参数:发送者、接收者和金额。例如,您可以从终端运行以下命令:
python addBoringcoinTransaction.py "Al Sweigart" Eve 2000
程序将访问 Google Sheets,在底部添加一个空白行,然后用 'Al Sweigart'、'Eve' 和 '2000' 填充它。请注意,在终端中,您需要用双引号将包含空格的任何命令行参数括起来,如 "Al Sweigart";否则,终端会认为它们是两个单独的参数。
addBoringcoinTransactions.py 的开始部分检查命令行参数,并根据它们分配发送者、接收者和金额变量:
import sys, ezsheets
if len(sys.argv) < 4:
print('Usage: python addBoringcoinTransaction.py sender recipient amount')
sys.exit()
# Get the transaction info from the command line arguments:
sender, recipient, amount = sys.argv[1:]
您不需要将 amount 从字符串转换为整数,因为我们将在电子表格中将其作为字符串写入。
接下来,EZSheets 连接到包含虚假区块链的 Google Sheets,并选择工作表中的第一个工作表(索引 0)。请注意,您没有权限编辑 Boringcoin Google Sheets,因此请登录到您的 Google 账户后,在网页浏览器中打开该 URL,然后选择 文件制作副本 将其复制到您的 Google 账户。然后,将 'https://autbor.com/boringcoin' 字符串替换为浏览器地址栏中您的 Google Sheets 的 URL 字符串:
# Change this URL to your copy of the Google Sheet, or else you'll
# get a "The caller does not have permission" error.
ss = ezsheets.Spreadsheet('`https://autbor.com/boringcoin`')
sheet = ss.sheets[0]
最后,您应该得到工作表中的行数,增加一行,然后填写这行的发送者、接收者和金额数据:
# Add one more row to the sheet for a new transaction:
sheet.rowCount += 1
sheet[1, sheet.rowCount] = sender
sheet[2, sheet.rowCount] = recipient
sheet[3, sheet.rowCount] = amount
现在,当您从终端运行 python addBoringcoinTransaction.py "Al Sweigart" Eve 2000 时,Google Sheets 将在底部添加一行,包含 Al Sweigart、Eve 和 2000。您可以通过重新运行 auditBoringcoin.py 程序来查看加密货币网络中每个人的更新后的账户余额。
使用 Google Sheets 作为我们的区块链数据结构是不负责任的、容易出错的,并且是即将发生的安全灾难。这使得它与大多数市场上销售的区块链产品处于同一水平。不要错过!联系我,在金字塔骗局崩溃之前购买 Boringcoin,享受这个有限优惠!
与 Google Sheets 配额一起工作
由于 Google Sheets 是在线的,您可以轻松地在多个用户之间共享电子表格,这些用户可以同时访问电子表格。然而,这也意味着读取和更新电子表格的速度将比读取和更新存储在您硬盘上的本地 Excel 文件慢。此外,Google Sheets 限制了您可以执行多少次读取和写入操作。
根据 Google 的开发者指南,用户每天只能创建 250 个新的电子表格,免费 Google 账户每分钟可以执行几百次请求。您可以在developers.google.com/sheets/api/limits找到 Google 的使用限制。尝试超出此配额将引发googleapiclient.errors.HttpError“配额超出配额组”异常。EZSheets 将自动捕获此异常并重试请求。当这种情况发生时,读取或写入数据的函数调用将需要几秒钟(甚至一分钟左右)才能返回。如果请求继续失败(如果另一个使用相同凭据的脚本也在进行请求,这种情况是可能的),EZSheets 将重新引发此异常。
这意味着,有时您的 EZSheets 方法调用可能需要几秒钟才能返回。如果您想查看您的 API 使用情况或增加配额,请访问console.developers.google.com/iam-admin/quotas页面了解关于付费增加使用量的信息。如果您宁愿自己处理HttpError异常,可以将ezsheets.IGNORE_QUOTA设置为True,当 EZSheets 遇到这些异常时,其方法将引发这些异常。
总结
Google Sheets 是一个流行的在线电子表格应用程序,在您的浏览器中运行。使用 EZSheets 第三方包,您可以下载、创建、读取和修改电子表格。EZSheets 将电子表格表示为Spreadsheet对象,每个对象都包含一个有序的Sheet对象列表。每个工作表都有数据列和行,您可以通过多种方式读取和更新这些数据。
虽然 Google Sheets 使数据共享和协作编辑变得容易,但其主要缺点是速度:您必须通过 Web 请求更新电子表格,这可能需要几秒钟才能执行。但对于大多数用途,这种速度限制不会影响使用 EZSheets 的 Python 脚本。Google Sheets 还限制了您可以更改的频率。
要查看 EZSheets 功能的完整文档,请访问ezsheets.readthedocs.io/。
练习问题
1. 为了使 EZSheets 能够访问 Google Sheets,你需要哪三个文件?
2. EZSheets 有哪些两种类型的对象?
3. 如何从 Google Sheets 电子表格创建 Excel 文件?
4. 如何从 Excel 文件创建 Google Sheets 电子表格?
-
ss变量包含一个Spreadsheet对象。什么代码可以读取标题为 Students 的表格中 B2 单元格的数据? -
你如何找到列 999 的列字母?
-
你如何找出一个表格有多少行和列?
-
如何删除电子表格?这种删除是永久性的吗?
-
哪些函数可以创建一个新的
Spreadsheet对象和一个新的Sheet对象,分别? -
如果你通过频繁的读取和写入请求使用 EZSheets,超过了你的 Google 账户配额,会发生什么?
练习程序
为了练习,编写程序来完成以下任务。
下载 Google 表单数据
我之前提到 Google Forms 允许你创建简单的在线表单,这使得收集人们的信息变得容易。表单中输入的信息存储在 Google Sheets 电子表格中。对于这个项目,编写一个程序可以自动下载用户提交的表单信息。转到 docs.google.com/forms/ 并开始一个新的空白表单。向表单添加字段,要求用户输入姓名和电子邮件地址。然后,点击右上角的 发送 按钮以获取你新表单的链接。尝试在此表单中输入一些示例响应。
在你表单的 响应 选项卡上,点击绿色的 创建电子表格 按钮以创建一个 Google Sheets 电子表格,该电子表格将保存用户提交的响应。你应该能看到此电子表格的第一行中的示例响应。然后,使用 EZSheets 编写一个 Python 脚本来收集此电子表格上的电子邮件地址列表。
将电子表格转换为其他格式
你可以使用 Google Sheets 将电子表格文件转换为其他格式。编写一个将提交的文件传递给 upload() 的脚本。一旦电子表格上传到 Google Sheets,可以使用 downloadAsExcel()、downloadAsODS() 和其他此类函数下载它,以在这些其他格式中创建电子表格的副本。
在电子表格中查找错误
在漫长的一天在会计办公室工作后,我已经完成了一个包含所有豆子总数的电子表格,并将其上传到 Google Sheets。该电子表格是公开可查看的(但不可编辑)。你可以使用以下代码获取此电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg')
通过访问 docs.google.com/spreadsheets/d/1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg 在你的浏览器中查看电子表格。此电子表格的第一个工作表的列是 每个罐子的豆子数量、罐子数量 和 总豆子数量。总豆子数量 列是 每个罐子的豆子数量 和 罐子数量 列中数字的乘积。然而,在这个包含 15,000 行的表格中的一个行有错误。手动检查这么多行太麻烦了。幸运的是,你可以编写一个脚本来检查总数。
作为提示,您可以使用 ss.sheets[0].getRow(rowNum)访问行中的单个单元格,其中 ss 是Spreadsheet对象,rowNum 是行号。请记住,Google Sheets 中的行号从 1 开始,而不是 0。单元格值将是字符串,因此您需要在程序可以处理它们之前将它们转换为整数。表达式int(ss.sheets[0].getRow(2)[0]) * int(ss.sheets[0].getRow(2)[1]) == int(ss.sheets[0].getRow(2)[2])如果第 2 行有正确的总和,则评估为True。将此代码放入循环中,以识别电子表中哪一行有错误的总和。
安装和设置 EZSheets
您可以通过遵循附录 A 中的说明,使用 pip 命令行工具安装 EZSheets。
在您的 Python 脚本可以使用 EZSheets 访问和编辑您的 Google Sheets 电子表格之前,您需要一个凭证 JSON 文件和两个令牌 JSON 文件。创建凭证有五个部分:
-
创建一个新的 Google Cloud 项目。
-
为您的项目启用 Google Sheets API 和 Google Drive API。
-
配置 OAuth 同意屏幕。
-
创建凭证。
-
使用凭证文件登录。
这可能看起来像很多工作,但您只需要执行此设置一次,而且这是免费的。您需要一个 Google/Gmail 账户;我强烈建议创建一个新的 Google 账户而不是使用现有的账户,以防止您的 Python 脚本中的错误影响您个人 Google 账户中的电子表格。在整个章节中,我会说您的 Google 账户和您的 Gmail 电子邮件地址来指代拥有您 Python 程序访问的电子表格的 Google 账户。
Google 可能会稍微更改其 Google Cloud Console 网站上的布局或措辞。然而,我概述的基本步骤应该保持不变。
创建新的 Google Cloud 项目
首先,您需要设置一个 Google Cloud 项目。在您的浏览器中,访问console.cloud.google.com,使用您的用户名和密码登录您的 Google 账户。您将被带到入门页面。在页面顶部,点击选择项目。在出现的弹出窗口中,点击新建项目。这将带您到一个新的项目页面。
Google Cloud 将为您生成一个项目名称,例如“我的项目 23135”,以及一个随机的项目 ID,例如“macro-nuance-362516”。这些值对您的 Python 脚本用户不可见,并且您可以更改项目名称为任何您想要的名称,但您不能更改项目 ID。我只是使用网站为我生成的默认名称。您可以保留位置设置为“无组织”。免费 Google 账户最多可以有 12 个项目,但您只需要一个项目来创建所有 Python 脚本。点击蓝色创建按钮以创建项目。
启用 Sheets 和 Drive API
在 console.cloud.google.com 页面上,点击左上角的 导航 按钮。(图标有三个水平条纹,通常被称为 汉堡 图标。)转到 APIs & Services 库 访问 API 库页面。您将看到许多用于 Gmail、Google Maps、Google Cloud Storage 和其他 Google 服务的 Google API。我们需要允许我们的项目使用 Google Sheets 和 Google Drive API。EZSheets 使用 Google Drive API 上传和下载电子表格文件。
滚动到页面底部,找到 Google Sheets API,点击它,或者将“Google Sheets API”输入到搜索栏中找到它。这应该会带您到 Google Sheets API 页面。点击蓝色的 启用 按钮以启用您的 Google Cloud 项目使用 Google Sheets API。您将被重定向到 APIs & Services已启用 API & 服务 页面,在那里您可以找到有关您的 Python 脚本使用此 API 频率的信息。重复此过程以启用 Google Drive API。
接下来,您需要配置您项目的 OAuth 同意屏幕。
配置 OAuth 同意屏幕
当用户首次运行 import ezsheets 时,OAuth 同意屏幕将显示给用户。在 步骤 1 OAuth 同意屏幕 页面上,选择 外部 并点击蓝色的 创建 按钮。下一页应该显示 OAuth 同意屏幕的外观。为 App Name 字段选择一个名称(我使用一些通用的名称,例如“Python Google API 脚本”),并在 User Support Email 和开发者联系信息字段中输入您的电子邮件地址。然后点击 保存并继续 按钮。
在 步骤 2 范围 页面上,定义您项目的范围,即项目允许访问的资源权限。点击 添加或删除范围 按钮,并在出现的新面板中,通过表格并勾选 .../auth/drive(Google Drive API)和 .../auth/spreadsheets(Google Sheets API)范围的复选框。然后,点击蓝色的 更新 按钮,然后点击 保存并继续。
步骤 3 测试用户 页面要求您添加您 Python 脚本将要交互的电子表格所属的 Google 账户的 Gmail 电子邮件地址。除非您通过 Google 的应用程序审核流程,否则您的脚本将仅限于与您在此步骤中提供的电子邮件地址进行交互。点击 + 添加用户 按钮。在出现的新的面板中,输入您 Google 账户的 Gmail 地址并点击蓝色的 添加 按钮。然后点击 保存并继续。
步骤 4 摘要 页面提供了之前步骤的总结。如果所有信息看起来都正确,请点击 返回仪表板 按钮。下一步是为您的项目创建凭证。
创建凭证
首先,您需要创建一个凭据文件。EZSheets 需要这个文件来使用 Google API,即使是公开共享的电子表格也是如此。从导航侧边栏菜单中,点击APIs & Services然后点击凭据以转到凭据页面。然后点击页面顶部的+ 创建凭据链接。一个子菜单应该会打开,询问您想创建哪种类型的凭据:API 密钥、OAuth 客户端 ID 或服务帐户。点击OAuth 客户端 ID。
在下一页,选择应用程序类型为桌面应用程序,并将名称保留为默认的“桌面客户端 1”。如果您想更改名称,也可以;这不会显示在您的 Python 脚本用户面前。点击蓝色的创建按钮。
应该会弹出一个窗口。点击下载 JSON以下载凭据文件,该文件可能名为 client_secret_282792235794-p2o9gfcub4htibfg2u207gcomco9nqm7.apps.googleusercontent.com.json。将其放在与您的 Python 脚本相同的文件夹中。为了简单起见,您也可以将 JSON 文件重命名为 credentials-sheets.json。EZSheets 会搜索 credentials-sheets.json 或任何符合 client_secret_.json* 格式的文件。
使用凭据文件登录
从与凭据 JSON 文件相同的文件夹中运行 Python 交互式外壳,然后运行 import ezsheets。EZSheets 会通过调用 ezsheets.init() 函数自动检查当前工作目录中的凭据 JSON 文件。如果找到文件,EZSheets 会启动您的网络浏览器到 OAuth 同意屏幕以生成令牌文件。EZSheets 还需要这些名为 token-drive.pickle 和 token-sheets.pickle 的令牌文件,以及凭据文件来访问电子表格。生成令牌文件是一个一次性设置步骤,下次您运行 import ezsheets 时不会发生。
使用您的 Google 账户登录。这必须是您在配置 Google Cloud 项目的 OAuth 同意屏幕时提供的“测试用户”的同一电子邮件地址。您应该会收到一条警告消息,内容为“Google 没有验证此应用”,这是正常的,因为您是应用创建者。点击继续链接。您应该到达另一个页面,上面写着“Python Google API 脚本希望访问您的 Google 账户”(或您在 OAuth 同意屏幕设置中给出的名称)。点击继续。您将来到一个简单的网页,上面写着“身份验证流程已完成”。您现在可以关闭浏览器窗口。
完成对 Sheets API 的身份验证流程后,您必须在打开的下一个窗口中为 Drive API 重复此过程。关闭第二个窗口后,您现在应该会在与您的凭据 JSON 文件相同的文件夹中看到 token-drive.pickle 和 token-sheets.pickle 文件。将这些文件视为密码,不要分享它们:它们可以用来登录并访问您的 Google Sheets 电子表格。
撤销凭据文件
如果您不小心将凭证或令牌文件与某人共享,他们无法更改您的 Google 账户密码,但他们将能够访问您的电子表格。您可以通过登录console.developers.google.com来撤销这些文件。在侧边栏中点击凭证链接。然后,在 OAuth 2.0 客户端 ID 表中,点击您不小心共享的凭证文件旁边的垃圾桶图标。一旦撤销,凭证和令牌文件就变得无用,您可以删除它们。然后,您将需要生成一个新的凭证 JSON 文件和令牌文件。
创建新的 Google Cloud 项目
首先,您需要设置一个 Google Cloud 项目。在您的浏览器中,转到console.cloud.google.com并使用您的用户名和密码登录您的 Google 账户。您将被带到入门页面。在页面顶部,点击选择项目。在出现的弹出窗口中,点击新建项目。这应该会带您到一个新项目页面。
Google Cloud 将为您生成一个项目名称,例如“我的项目 23135”,以及一个随机项目 ID,例如“macro-nuance-362516”。这些值不会对您的 Python 脚本用户可见,并且您可以更改项目名称为任何您想要的名称,但您不能更改项目 ID。我只是使用网站为我生成的默认名称。您可以保留位置设置为“无组织”。免费 Google 账户最多可以有 12 个项目,但您只需要一个项目来创建所有您想要的 Python 脚本。点击蓝色创建按钮以创建项目。
启用 Sheets 和 Drive API
在console.cloud.google.com页面,点击左上角的导航按钮。(图标有三个水平条纹,通常被称为hamburger图标。)转到APIs & Services库以访问 API 库页面。您将看到许多用于 Gmail、Google Maps、Google Cloud Storage 和其他 Google 服务的 Google API。我们需要允许我们的项目使用 Google Sheets 和 Google Drive API。EZSheets 使用 Google Drive API 上传和下载电子表格文件。
滚动页面,找到 Google Sheets API,点击它,或者将“Google Sheets API”输入搜索栏以找到它。这应该会带您到 Google Sheets API 页面。点击蓝色启用按钮以启用您的 Google Cloud 项目使用 Google Sheets API。您将被重定向到APIs & Services已启用 API & 服务页面,在那里您可以找到有关您的 Python 脚本使用此 API 频率的信息。重复此过程以启用 Google Drive API。
接下来,您需要配置您项目的 OAuth 授权屏幕。
配置 OAuth 授权屏幕
当用户首次运行 import ezsheets 时,会出现 OAuth 授权屏幕。在 步骤 1 OAuth 授权屏幕 页面上,选择 外部 并点击蓝色的 创建 按钮。下一页应该显示 OAuth 授权屏幕的外观。为 App 名称字段选择一个名称(我使用一些通用的名称,例如“Python Google API 脚本”),并在用户支持电子邮件和开发者联系信息字段中输入您的电子邮件地址。然后点击 保存并继续 按钮。
在 步骤 2 范围 页面上,定义您项目的范围,即项目允许访问的资源权限。点击 添加或删除范围 按钮,然后在出现的新面板中,通过表格并勾选 .../auth/drive(Google Drive API)和 .../auth/spreadsheets(Google Sheets API)的范围复选框。然后点击蓝色的 更新 按钮,然后点击 保存并继续。
步骤 3 测试用户 页面要求您添加您 Python 脚本将要交互的电子表格的所有者 Google 账户的 Gmail 电子邮件地址。除非您通过 Google 的应用程序审核流程,否则您的脚本将仅限于与您在此步骤中提供的电子邮件地址进行交互。点击 + 添加用户 按钮。在出现的新面板中,输入您 Google 账户的 Gmail 地址并点击蓝色的 添加 按钮。然后点击 保存并继续。
步骤 4 摘要 页面提供了之前步骤的总结。如果所有信息看起来都正确,请点击 返回仪表板 按钮。下一步是创建项目的凭证。
创建凭证
首先,您需要创建一个凭证文件。EZSheets 需要这个文件来使用 Google API,即使是公开共享的电子表格也不例外。从导航侧边栏菜单中,点击 APIs & Services 然后点击 Credentials 以访问凭证页面。然后在页面顶部点击 + 创建凭证 链接。一个子菜单应该打开,询问您想创建哪种类型的凭证:API 密钥、OAuth 客户端 ID 或服务帐户。点击 OAuth 客户端 ID。
在下一页上,选择 桌面应用程序 作为应用程序类型,并将名称保留为默认的“桌面客户端 1”。如果您想的话,也可以将其更改为不同的名称;它不会显示在您的 Python 脚本用户面前。点击蓝色的 创建 按钮。
应该会出现一个弹出窗口。点击 下载 JSON 以下载凭证文件,该文件应具有类似 client_secret_282792235794-p2o9gfcub4htibfg2u207gcomco9nqm7.apps.googleusercontent.com.json 的名称。将其放在与您的 Python 脚本相同的文件夹中。为了简单起见,您还可以将 JSON 文件重命名为 credentials-sheets.json。EZSheets 搜索 credentials-sheets.json 或任何匹配 client_secret_.json* 格式的文件。
使用凭证文件登录
从与凭证 JSON 文件相同的文件夹中运行 Python 交互式外壳,然后运行 import ezsheets。EZSheets 会通过调用 ezsheets.init() 函数自动检查当前工作目录中的凭证 JSON 文件。如果找到文件,EZSheets 会启动您的网络浏览器到 OAuth 同意屏幕以生成令牌文件。EZSheets 还需要这些名为 token-drive.pickle 和 token-sheets.pickle 的令牌文件,以及凭证文件来访问电子表格。生成令牌文件是一个一次性设置步骤,下次您运行 import ezsheets 时不会发生。
使用您的 Google 账户登录。这必须是您在配置 Google Cloud 项目的 OAuth 同意屏幕时为“测试用户”提供的相同电子邮件地址。您应该会收到一条警告信息,内容为“Google 未验证此应用”,这是正常的,因为您是应用创建者。点击 继续 链接。您应该到达另一个页面,上面写着类似于“Python Google API 脚本希望访问您的 Google 账户”的信息(或您在 OAuth 同意屏幕设置中给出的任何名称)。点击 继续。您将来到一个简单的网页,上面写着“身份验证流程已完成。”您现在可以关闭浏览器窗口。
一旦您完成了 Sheets API 的身份验证流程,您必须在下一个打开的窗口中为 Drive API 重复此过程。关闭第二个窗口后,您现在应该会在与您的凭证 JSON 文件相同的文件夹中看到 token-drive.pickle 和 token-sheets.pickle 文件。将这些文件视为密码,不要分享它们:它们可以用来登录并访问您的 Google Sheets 电子表格。
撤销凭证文件
如果您不小心将凭证或令牌文件与某人共享,他们无法更改您的 Google 账户密码,但他们将能够访问您的电子表格。您可以通过登录到 console.developers.google.com 来撤销这些文件。点击侧边栏上的 凭证 链接。然后,在 OAuth 2.0 客户端 ID 表中,点击您不小心共享的凭证文件旁边的垃圾桶图标。一旦撤销,凭证和令牌文件就变得无用,您可以删除它们。然后,您将不得不生成一个新的凭证 JSON 文件和令牌文件。
电子表格对象
在 Google Sheets 中,一个 电子表格 可以包含多个 工作表(也称为 工作表),每个工作表包含单元格的列和行。单元格包含诸如数字、日期或文本片段等数据。单元格还具有字体、宽度和高度以及背景颜色等属性。图 15-1 展示了一个标题为 Sweigart Books 的电子表格,包含两个工作表,分别标题为 Books 和 Websites。您可以通过访问 autbor.com/examplegs 在浏览器中查看此电子表格。每个工作表的第一列标记为 A,第一行标记为 1。(这与 Python 列表不同,其第一个元素出现在索引 0 处。)

图 15-1:标题为 Sweigart Books 的电子表格,包含两个工作表,Books 和 Websites
虽然您的大部分工作将涉及修改 Sheet 对象,但您也可以修改 Spreadsheet 对象,正如您将在下一节中看到的。
创建、上传和列出电子表格
您可以从现有的 Google Sheets 电子表格、新空白电子表格或上传的 Excel 电子表格创建一个新的 Spreadsheet 对象。所有 Google Sheets 电子表格都有一个唯一的 ID,可以在其 URL 中找到,在 spreadsheets/d/ 部分之后和 /edit 部分之前。例如,在 URL docs.google.com/spreadsheets/d/1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/edit#gid=0/ 中,ID 将是 1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI。
Google Sheets 电子表格表示为 ezsheets.Spreadsheet 对象,该对象具有 id、url 和 title 属性。您可以使用 Spreadsheet() 函数创建一个新的空白电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Title of My New Spreadsheet'
>>> ss.title
'Title of My New Spreadsheet'
>>> ss.url
'https://docs.google.com/spreadsheets/d/1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40/'
>>> ss.id
'1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40'
您还可以通过传递其 ID 或 URL,或重定向到其 URL 的 URL 来加载现有的电子表格:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss2 = ezsheets.Spreadsheet('https://docs.google.com/spreadsheets/d/1TzOJxh
NKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/')
>>> ss3 = ezsheets.Spreadsheet('1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI')
>>> ss1 == ss2 == ss3 # These are the same spreadsheet.
True
要将现有的 Excel、OpenOffice、CSV 或 TSV 电子表格上传到 Google Sheets,请将电子表格的文件名传递给 ezsheets.upload()。在交互式外壳中输入以下内容,将 my_spreadsheet.xlsx 替换为您自己的电子表格文件:
>>> import ezsheets
>>> ss = ezsheets.upload('`my_spreadsheet.xlsx`')
>>> ss.title
'`my_spreadsheet`'
您可以通过调用 listSpreadsheets() 函数列出您 Google 账户中的电子表格。此函数返回一个字典,其键是电子表格 ID,其值是每个电子表格的标题。它包括您账户 回收站 文件夹中的已删除电子表格。尝试在上传电子表格后,在交互式外壳中输入以下内容:
>>> ezsheets.listSpreadsheets()
{'`1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU`': 'Education Data'}
一旦您获得了 Spreadsheet 对象,您就可以使用其属性和方法来操作托管在 Google Sheets 上的在线电子表格。
访问电子表格属性
尽管实际数据存在于电子表格的各个单独的工作表中,但 Spreadsheet 对象具有以下属性来操作电子表格本身:title、id、url、sheetTitles 和 sheets。让我们检查 autbor.com/examplegs 上的电子表格。您的 Google 账户具有查看权限但没有修改权限,但您可以将工作表复制到您自己的账户中新建的电子表格:
>>> import ezsheets
>>> example_ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss = ezsheets.Spreadsheet()
>>> example_ss.sheets[0].copyTo(ss)
>>> ss.sheets[0].delete() # Delete the Sheet1 sheet.
>>> ss.url
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
新复制的电子表格将具有 Books 的副本 标题,因为 Books 是原始电子表格的名称。使用以下代码继续交互式外壳示例:
>>> ss.title # The title of the spreadsheet
'Untitled spreadsheet'
>>> ss.title = 'Sweigart Books' # Change the title.
>>> ss.id # The unique ID (a read-only attribute)
'15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM'
>>> ss.url # The original URL (a read-only attribute)
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
>>> ss.sheetTitles # The titles of all the Sheet objects
('Copy of Books',)
>>> ss.sheets # The Sheet objects in this Spreadsheet, in order
(<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>,)
>>> ss.sheets[0] # The first Sheet object in this Spreadsheet
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss['Copy of Books'] # Sheets can also be accessed by title.
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss.Sheet('New blank sheet') # Create a new sheet.
<Sheet sheetId=1759616008, title='New blank sheet', rowCount=1000, columnCount=26>
>>> ss.sheets[1].delete() # Delete the second Sheet object in this Spreadsheet.
如果有人在浏览器中更改了电子表格,您的脚本可以通过调用 refresh() 方法来更新 Spreadsheet 对象以匹配在线数据:
>>> ss.refresh()
这将刷新不仅 Spreadsheet 对象的属性,还包括它包含的 Sheet 对象中的数据。您将实时看到对 Spreadsheet 对象所做的更改应用于在线电子表格。
下载和上传电子表格
您可以将 Google Sheets 电子表格下载为多种格式:Excel、OpenOffice、CSV、TSV 和 PDF。您还可以将其下载为包含电子表格数据的 HTML 文件 ZIP 文件。EZSheets 包含针对这些选项中的每一个的功能:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss.title
'Sweigart Books (DO NOT DELETE)'
>>> ss.downloadAsExcel() # Downloads the spreadsheet as an Excel file
'Sweigart_Books.xlsx'
>>> ss.downloadAsODS() # Downloads the spreadsheet as an OpenOffice file
'Sweigart_Books.ods'
>>> ss.downloadAsCSV() # Downloads only the first sheet as a CSV file
'Sweigart_Books.csv'
>>> ss.downloadAsTSV() # Downloads only the first sheet as a TSV file
'Sweigart_Books.tsv'
>>> ss.downloadAsPDF() # Downloads the spreadsheet as a PDF
'Sweigart_Books.pdf'
>>> ss.downloadAsHTML() # Downloads the spreadsheet as a ZIP of HTML files
'Sweigart_Books.zip'
注意,CSV 或 TSV 格式的文件只能包含一个工作表;因此,如果您以这些格式之一下载 Google Sheets 电子表格,您将只获得第一个工作表。要下载其他工作表,您需要在下载前重新排列 Sheet 对象。
下载函数都返回下载文件的文件名字符串。您还可以通过将新文件名传递给下载函数来指定电子表格的自己的文件名:
>>> ss.downloadAsExcel('`a_different_filename`.xlsx')
'`a_different_filename`.xlsx'
该函数返回本地文件名。
删除电子表格
要删除电子表格,调用 delete() 方法:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet() # Create the spreadsheet.
>>> ezsheets.listSpreadsheets() # Confirm that we've created a spreadsheet.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
>>> ss.delete() # Delete the spreadsheet.
>>> ezsheets.listSpreadsheets() # Spreadsheets in the Trash folder are still listed.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
delete() 方法将您的电子表格移动到 Google Drive 上的 垃圾箱 文件夹。您可以在 drive.google.com/drive/trash 查看您的 垃圾箱 文件夹的内容。请注意,垃圾箱中的电子表格仍会出现在 listSpreadsheets() 返回的字典中。要永久删除电子表格,请将 permanent 关键字参数传递为 True:
>>> ss.delete(permanent=True)
>>> ezsheets.listSpreadsheets()
{}
通常,使用自动化脚本永久删除电子表格不是一个好主意,因为无法恢复脚本中的错误意外删除的电子表格。即使免费的 Google Drive 账户也有数 GB 的存储空间可用,所以您很可能不需要担心释放空间。
创建、上传和列出电子表格
您可以从现有的 Google Sheets 电子表格、一个新的空白电子表格或上传的 Excel 电子表格创建一个新的Spreadsheet对象。所有 Google Sheets 电子表格都有一个唯一的 ID,可以在其 URL 中找到,在spreadsheets/d/部分之后和/edit部分之前。例如,在 URLdocs.google.com/spreadsheets/d/1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/edit#gid=0/中,ID 将是1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI。
Google Sheets 电子表格表示为ezsheets.Spreadsheet对象,它具有id、url和title属性。您可以使用Spreadsheet()函数创建一个新的、空白的电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Title of My New Spreadsheet'
>>> ss.title
'Title of My New Spreadsheet'
>>> ss.url
'https://docs.google.com/spreadsheets/d/1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40/'
>>> ss.id
'1gxz-Qr2-RNtqi_d7wWlsDlbtPLRQigcEXvCtdVwmH40'
您也可以通过传递其 ID 或 URL,或重定向到其 URL 的 URL 来加载现有的电子表格:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss2 = ezsheets.Spreadsheet('https://docs.google.com/spreadsheets/d/1TzOJxh
NKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI/')
>>> ss3 = ezsheets.Spreadsheet('1TzOJxhNKr15tzdZxTqtQ3EmDP6em_elnbtmZIcyu8vI')
>>> ss1 == ss2 == ss3 # These are the same spreadsheet.
True
要将现有的 Excel、OpenOffice、CSV 或 TSV 电子表格上传到 Google Sheets,请将电子表格的文件名传递给ezsheets.upload()。在交互式外壳中输入以下内容,将my_spreadsheet.xlsx替换为您自己的电子表格文件:
>>> import ezsheets
>>> ss = ezsheets.upload('`my_spreadsheet.xlsx`')
>>> ss.title
'`my_spreadsheet`'
您可以通过调用listSpreadsheets()函数列出您 Google 账户中的电子表格。此函数返回一个字典,其键是电子表格 ID,其值是每个电子表格的标题。它包括您账户的回收站文件夹中的已删除电子表格。在上传电子表格后,尝试在交互式外壳中输入以下内容:
>>> ezsheets.listSpreadsheets()
{'`1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU`': 'Education Data'}
一旦您获得了Spreadsheet对象,您就可以使用其属性和方法来操作托管在 Google Sheets 上的在线电子表格。
访问电子表格属性
虽然实际数据生活在电子表格的各个单独的工作表中,但Spreadsheet对象具有以下属性来操作电子表格本身:title、id、url、sheetTitles和sheets。让我们检查autbor.com/examplegs上的电子表格。您的 Google 账户有查看权限但没有修改权限,但您可以将工作表复制到您自己的账户中新建的电子表格:
>>> import ezsheets
>>> example_ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss = ezsheets.Spreadsheet()
>>> example_ss.sheets[0].copyTo(ss)
>>> ss.sheets[0].delete() # Delete the Sheet1 sheet.
>>> ss.url
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
新复制的电子表格将具有标题Books 的副本,因为Books是原始电子表格的名称。使用以下代码继续交互式外壳示例:
>>> ss.title # The title of the spreadsheet
'Untitled spreadsheet'
>>> ss.title = 'Sweigart Books' # Change the title.
>>> ss.id # The unique ID (a read-only attribute)
'15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM'
>>> ss.url # The original URL (a read-only attribute)
'https://docs.google.com/spreadsheets/d/15gjrbgTmUzItRt9KUcL4JajLaQU70xanstB1dXKoSlM/'
>>> ss.sheetTitles # The titles of all the Sheet objects
('Copy of Books',)
>>> ss.sheets # The Sheet objects in this Spreadsheet, in order
(<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>,)
>>> ss.sheets[0] # The first Sheet object in this Spreadsheet
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss['Copy of Books'] # Sheets can also be accessed by title.
<Sheet sheetId=1464919459, title='Copy of Books', rowCount=1000, columnCount=26>
>>> ss.Sheet('New blank sheet') # Create a new sheet.
<Sheet sheetId=1759616008, title='New blank sheet', rowCount=1000, columnCount=26>
>>> ss.sheets[1].delete() # Delete the second Sheet object in this Spreadsheet.
如果有人在浏览器中更改了电子表格,您的脚本可以通过调用refresh()方法来更新Spreadsheet对象以匹配在线数据:
>>> ss.refresh()
这将不仅刷新Spreadsheet对象的属性,还包括它包含的Sheet对象中的数据。您将看到对Spreadsheet对象所做的更改实时应用于在线电子表格。
下载和上传电子表格
你可以以多种格式下载 Google Sheets 电子表格:Excel、OpenOffice、CSV、TSV 和 PDF。你还可以将其下载为包含电子表格数据的 HTML 文件的 ZIP 文件。EZSheets 包含每个这些选项的函数:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('https://autbor.com/examplegs')
>>> ss.title
'Sweigart Books (DO NOT DELETE)'
>>> ss.downloadAsExcel() # Downloads the spreadsheet as an Excel file
'Sweigart_Books.xlsx'
>>> ss.downloadAsODS() # Downloads the spreadsheet as an OpenOffice file
'Sweigart_Books.ods'
>>> ss.downloadAsCSV() # Downloads only the first sheet as a CSV file
'Sweigart_Books.csv'
>>> ss.downloadAsTSV() # Downloads only the first sheet as a TSV file
'Sweigart_Books.tsv'
>>> ss.downloadAsPDF() # Downloads the spreadsheet as a PDF
'Sweigart_Books.pdf'
>>> ss.downloadAsHTML() # Downloads the spreadsheet as a ZIP of HTML files
'Sweigart_Books.zip'
注意,CSV 或 TSV 格式的文件只能包含一个电子表格;因此,如果你以这些格式之一下载 Google Sheets 电子表格,你将只得到第一个电子表格。要下载其他电子表格,你需要在下载之前重新排列Sheet对象。
下载函数都返回下载文件的文件名字符串。你也可以通过将新文件名传递给下载函数来为电子表格指定自己的文件名:
>>> ss.downloadAsExcel('`a_different_filename`.xlsx')
'`a_different_filename`.xlsx'
函数返回本地文件名。
删除电子表格
要删除电子表格,调用delete()方法:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet() # Create the spreadsheet.
>>> ezsheets.listSpreadsheets() # Confirm that we've created a spreadsheet.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
>>> ss.delete() # Delete the spreadsheet.
>>> ezsheets.listSpreadsheets() # Spreadsheets in the Trash folder are still listed.
{'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'}
delete()方法将你的电子表格移动到 Google Drive 上的垃圾箱文件夹。你可以在drive.google.com/drive/trash查看你的垃圾箱文件夹的内容。请注意,垃圾箱中的电子表格仍然会出现在listSpreadsheets()返回的字典中。要永久删除你的电子表格,将permanent关键字参数传递为True:
>>> ss.delete(permanent=True)
>>> ezsheets.listSpreadsheets()
{}
通常情况下,使用自动化脚本来永久删除你的电子表格不是一个好主意,因为一旦你的脚本中的错误意外删除了电子表格,就很难恢复。即使是免费的 Google Drive 账户也有数 GB 的存储空间可用,所以你很可能不需要担心释放空间。
工作表对象
Spreadsheet对象将有一个或多个Sheet对象。Sheet对象代表每个电子表格中的数据行和列。你可以使用方括号运算符和一个整数索引来访问这些电子表格。
Spreadsheet对象的sheets属性包含按电子表格中出现的顺序排列的Sheet对象的元组。要访问电子表格中的Sheet对象,将以下内容输入到交互式 shell 中:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet() # Starts with a sheet named Sheet1
>>> sheet2 = ss.Sheet('Spam')
>>> sheet3 = ss.Sheet('Eggs')
>>> ss.sheets # The Sheet objects in this Spreadsheet, in order
(<Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26>, <Sheet sheetId=284204004,
title='Spam', rowCount=1000, columnCount=26>, <Sheet sheetId=1920032872, title='Eggs',
rowCount=1000, columnCount=26>)
>>> ss.sheets[0] # Gets the first Sheet object in this Spreadsheet
<Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26>
Spreadsheet对象的sheetTitles属性包含所有电子表格标题的元组。例如,将以下内容输入到交互式 shell 中:
>>> ss.sheetTitles # The titles of all the Sheet objects in this Spreadsheet
('Sheet1', 'Spam', 'Eggs')
一旦你有了Sheet对象,你可以使用该对象的Sheet方法从其中读取数据并将其写入,具体说明将在下一节中介绍。
读取和写入数据
就像在 Excel 中一样,Google Sheets 工作表包含包含数据的单元格的列和行。你可以使用方括号运算符[]从这些单元格中读取和写入数据。例如,要创建一个新的电子表格并向其中添加数据,将以下内容输入到交互式 shell 中:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'My Spreadsheet'
>>> sheet = ss.sheets[0] # Get the first sheet in this spreadsheet.
>>> sheet.title
'Sheet1'
>>> sheet['A1'] = 'Name' # Set the value in cell A1.
>>> sheet['B1'] = 'Age'
>>> sheet['C1'] = 'Favorite Movie'
>>> sheet['A1'] # Read the value in cell A1.
'Name'
>>> sheet['A2'] # Empty cells return a blank string.
''
>>> sheet[2, 1] # Column 2, Row 1 is the same address as B1.
'Age'
>>> sheet['A2'] = 'Alice'
>>> sheet['B2'] = 30
>>> sheet['C2'] = 'RoboCop'
>>> sheet['B2'] # Note that all data is returned as strings.
'30'
这些说明应该会生成一个看起来像图 15-2 的 Google Sheets 电子表格。

图 15-2:使用示例说明创建的电子表格
当首次加载Spreadsheet对象时,Sheet对象中的所有数据都会被加载,因此数据可以立即读取。然而,将值写入在线电子表格需要网络连接,可能需要大约一秒钟。如果您有数千个单元格需要更新,逐个更新它们可能会非常慢。相反,接下来的几节将向您展示如何一次性更新整个行和列。
处理列和行
单元格地址在 Google Sheets 中的使用与 Excel 相同。唯一的区别是,与 Python 的基于 0 的列表索引不同,Google Sheets 的列和行是基于 1 的:第一列或行在索引 1,而不是 0。您可以使用convertAddress()函数将'A2'字符串样式地址转换为(column, row)元组样式地址(反之亦然)。getColumnLetterOf()和getColumnNumberOf()函数还可以在字母和数字之间转换列地址。例如,在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ezsheets.convertAddress('A2') # Converts addresses...
(1, 2)
>>> ezsheets.convertAddress(1, 2) # ...and converts them back, too.
'A2'
>>> ezsheets.getColumnLetterOf(2)
'B'
>>> ezsheets.getColumnNumberOf('B')
2
>>> ezsheets.getColumnLetterOf(999)
'ALK'
>>> ezsheets.getColumnNumberOf('ZZZ')
18278
如果您在源代码中输入地址,则'A2'字符串样式地址很方便。但如果您在循环地址范围内并且需要一个列的数字标识符,则(column, row)元组样式地址更方便。当您需要在这两种格式之间转换时,convertAddress()、getColumnLetterOf()和getColumnNumberOf()函数很有帮助。
读取和写入整个列和行
如前所述,逐个单元格写入数据可能需要太长时间。幸运的是,EZSheets 有用于同时读取和写入整个列和行的Sheet方法。getColumn()、getRow()、updateColumn()和updateRow()方法分别用于读取和写入列和行。这些方法会向 Google Sheets 服务器发送请求以更新电子表格,因此您需要连接到互联网。在本节的示例中,我们将从第十四章上传produceSales3.xlsx到 Google Sheets。您可以从本书的在线资源中下载它。前八行看起来像表 15-1。
表 15-1:produceSales3.xlsx 电子表格的前八行
| A | B | C | D | |
|---|---|---|---|---|
| 1 | PRODUCE | COST PER POUND | POUNDS SOLD | TOTAL |
| 2 | Potatoes | 0.86 | 21.6 | 18.58 |
| 3 | Okra | 2.26 | 38.6 | 87.24 |
| 4 | Fava beans | 2.69 | 32.8 | 88.23 |
| 5 | Watermelon | 0.66 | 27.3 | 18.02 |
| 6 | Garlic | 1.19 | 4.9 | 5.83 |
| 7 | Parsnips | 2.27 | 1.1 | 2.5 |
| 8 | Asparagus | 2.49 | 37.9 | 94.37 |
要上传此电子表格,将produceSales3.xlsx文件放入当前工作目录,并在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.upload('produceSales3.xlsx')
>>> sheet = ss.sheets[0]
>>> sheet.getRow(1) # The first row is row 1, not row 0.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> sheet.getRow(2)
['Potatoes', '0.86', '21.6', '18.58', '', '']
>>> sheet.getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getColumn('A') # The same result as getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getRow(3)
['Okra', '2.26', '38.6', '87.24', '', '']
>>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230'])
>>> sheet.getRow(3)
['Pumpkin', '11.50', '20', '230', '', '']
>>> columnOne = sheet.getColumn(1)
>>> for i, value in enumerate(columnOne):
... # Make the Python list contain uppercase strings:
... columnOne[i] = value.upper()
...
>>> sheet.updateColumn(1, columnOne) # Update the entire column in one request.
getRow() 和 getColumn() 函数将特定行或列中每个单元格的数据作为值列表检索。请注意,空单元格在列表中变为空字符串值。您可以将 getColumn() 传递一个列号或字母,以告诉它检索特定列的数据。上一个例子显示 getColumn(1) 和 getColumn('A') 返回相同的列表。
updateRow() 和 updateColumn() 函数将分别用传递给函数的值列表覆盖行或列中的数据。在这个例子中,第三行最初包含关于秋葵的信息,但 updateRow() 调用将其替换为关于南瓜的数据。再次调用 sheet.getRow(3) 来查看第三行的新值。
如果您有很多单元格需要更新,逐个更新单元格会很慢。获取一列或一行作为列表,更新该列表,然后使用列表更新整个列或行会更快,因为您可以通过一次请求向谷歌云服务进行所有更改。
要一次性获取所有行,请调用 getRows() 方法以返回一个列表的列表。外层列表中的内层列表代表工作表的单行。您可以通过修改此数据结构中的值来更改某些行的产品名称、销售磅数和总成本。然后,您可以通过在交互式外壳中输入以下内容将它们传递给 updateRows() 方法:
>>> rows = sheet.getRows() # Get every row in the spreadsheet.
>>> rows[0] # Examine the values in the first row.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> rows[1]
['POTATOES', '0.86', '21.6', '18.58', '', '']
>>> rows[1][0] = 'PUMPKIN' # Change the produce name.
>>> rows[1]
['PUMPKIN', '0.86', '21.6', '18.58', '', '']
>>> rows[10]
['OKRA', '2.26', '40', '90.4', '', '']
>>> rows[10][2] = '400' # Change the pounds sold.
>>> rows[10][3] = '904' # Change the total.
>>> rows[10]
['OKRA', '2.26', '400', '904', '', '']
>>> sheet.updateRows(rows) # Update the online spreadsheet with the changes.
您可以通过将 getRows() 返回的列表的列表(经过对第 1 行和第 10 行的更改)传递给 updateRows() 来在单个请求中更新整个工作表。
注意,谷歌表格中的行在末尾有空字符串。这是因为上传的工作表有 6 列,但我们只有四列数据。您可以使用 rowCount 和 columnCount 属性读取工作表中的行数和列数。然后,通过设置这些值,您可以更改工作表的大小:
>>> sheet.rowCount # The number of rows in the sheet
23758
>>> sheet.columnCount # The number of columns in the sheet
6
>>> sheet.columnCount = 4 # Change the number of columns to 4.
>>> sheet.columnCount # Now the number of columns in the sheet is 4.
4
这些说明应该删除 produceSales3.xlsx 工作表的第五和第六列,如图 15-3 所示。

图 15-3:更改列数为四之前(顶部)和之后(底部)的工作表
根据谷歌的文档,谷歌表格的电子表格可以包含多达 1000 万个单元格。然而,将表格的大小仅限于所需的范围是一个好主意,这样可以最小化更新和刷新数据所需的时间。
创建、移动和删除工作表
所有 Google Sheets 电子表格都以名为Sheet1的单个工作表开始。您可以使用Sheet()方法向工作表列表的末尾添加额外的工作表,该方法接受一个可选的字符串作为新工作表的标题。可选的第二个参数可以指定新工作表的整数索引。要创建电子表格并向其中添加新工作表,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Multiple Sheets'
>>> ss.sheetTitles
('Sheet1',)
>>> ss.Sheet('Spam') # Create a new sheet at the end of the list of sheets.
<Sheet sheetId=2032744541, title='Spam', rowCount=1000, columnCount=26>
>>> ss.Sheet('Eggs') # Create another new sheet.
<Sheet sheetId=417452987, title='Eggs', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss.Sheet('Bacon', 0) # Create a sheet at index 0 in the list of sheets.
<Sheet sheetId=814694991, title='Bacon', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
这些说明向电子表格添加了三个新工作表:Bacon、Spam和Eggs(除了默认的Sheet1)。电子表格中的工作表是有序的,除非您在Sheet()中传递第二个参数指定工作表的索引,否则新工作表将添加到列表的末尾。在这里,您在索引0处创建名为Bacon的工作表,使Bacon成为电子表格中的第一个工作表,并将其他三个工作表向上移动一个位置。这与insert()列表方法的行为类似。
您可以在屏幕底部的标签页中看到新工作表,如图 15-4 所示。

图 15-4:添加了 Spam、Eggs 和 Bacon 工作表后的多工作表电子表格
您可以从工作表的索引属性中获取其顺序,然后为此属性分配一个新的索引以重新排序工作表:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].index
0
>>> ss.sheets[0].index = 2 # Move the sheet at index 0 to index 2.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Bacon', 'Eggs')
>>> ss.sheets[2].index = 0 # Move the sheet at index 2 to index 0.
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
Sheet对象的delete()方法将从电子表格中删除工作表。如果您想保留工作表但删除其中包含的数据,请调用clear()方法以清除所有单元格并使其成为空白工作表。在交互式外壳中输入以下内容:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].delete() # Delete the sheet at index 0: the "Bacon" sheet.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss['Spam'].delete() # Delete the "Spam" sheet.
>>> ss.sheetTitles
('Sheet1', 'Eggs')
>>> sheet = ss['Eggs'] # Assign a variable to the "Eggs" sheet.
>>> sheet.delete() # Delete the "Eggs" sheet.
>>> ss.sheetTitles
('Sheet1',)
>>> ss.sheets[0].clear() # Clear all the cells on the "Sheet1" sheet.
>>> ss.sheetTitles # The "Sheet1" sheet is empty but still exists.
('Sheet1',)
删除工作表是永久的;无法恢复数据。但是,您可以通过使用copyTo()方法将它们复制到另一个电子表格来备份工作表,如下一节所述。
复制工作表
每个Spreadsheet对象都有一个包含其Sheet对象的有序列表,您可以使用此列表重新排序工作表(如前一个部分所示)或将其复制到其他电子表格。要将Sheet对象复制到另一个Spreadsheet对象,请调用copyTo()方法。将其目标Spreadsheet对象作为参数传递。要将第一个电子表格的数据复制到另一个工作表,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet()
>>> ss1.title = 'First Spreadsheet'
>>> ss1.sheets[0].title = 'Spam' # ss1 will have a sheet named Spam.
>>> ss2 = ezsheets.Spreadsheet()
>>> ss2.title = 'Second Spreadsheet'
>>> ss2.sheets[0].title = 'Eggs' # ss2 will have a sheet named Eggs.
>>> ss1[0]
<Sheet sheetId=0, title='Spam', rowCount=1000, columnCount=26>
>>> ss1[0].updateRow(1, ['Some', 'data', 'in', 'the', 'first', 'row'])
>>> ss1[0].copyTo(ss2) # Copy the ss1's Sheet1 to the ss2 spreadsheet.
>>> ss2.sheetTitles # ss2 now contains a copy of ss1's Sheet1.
('Eggs', 'Copy of Spam')
被复制的表格出现在目标电子表格工作表列表的末尾,带有Copy of前缀。如果您愿意,可以更改它们的index属性以在新的电子表格中重新排序。
读取和写入数据
就像在 Excel 中一样,Google Sheets 工作表有包含数据的单元格的列和行。您可以使用方括号运算符[]从这些单元格中读取和写入数据。例如,要创建一个新的电子表格并向其中添加数据,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'My Spreadsheet'
>>> sheet = ss.sheets[0] # Get the first sheet in this spreadsheet.
>>> sheet.title
'Sheet1'
>>> sheet['A1'] = 'Name' # Set the value in cell A1.
>>> sheet['B1'] = 'Age'
>>> sheet['C1'] = 'Favorite Movie'
>>> sheet['A1'] # Read the value in cell A1.
'Name'
>>> sheet['A2'] # Empty cells return a blank string.
''
>>> sheet[2, 1] # Column 2, Row 1 is the same address as B1.
'Age'
>>> sheet['A2'] = 'Alice'
>>> sheet['B2'] = 30
>>> sheet['C2'] = 'RoboCop'
>>> sheet['B2'] # Note that all data is returned as strings.
'30'
这些说明应该会产生一个看起来像图 15-2 的 Google Sheets 电子表格。

图 15-2 按示例说明创建的电子表格
当首次加载Spreadsheet对象时,Sheet对象中的所有数据都会被加载,因此数据读取是瞬时的。然而,将值写入在线电子表格需要网络连接,可能需要大约一秒钟。如果你有数千个单元格需要更新,逐个更新可能会非常慢。相反,接下来的几节将展示如何一次性更新整个行和列。
列和行的地址设置
在 Google Sheets 中,单元格地址的设置与 Excel 类似。唯一的区别是,与 Python 的基于 0 的列表索引不同,Google Sheets 的列和行是基于 1 的:第一列或行在索引 1,而不是 0。你可以使用convertAddress()函数将'A2'字符串样式的地址转换为(column, row)元组样式的地址(反之亦然)。getColumnLetterOf()和getColumnNumberOf()函数也会在字母和数字之间转换列地址。例如,将以下内容输入到交互式外壳中:
>>> import ezsheets
>>> ezsheets.convertAddress('A2') # Converts addresses...
(1, 2)
>>> ezsheets.convertAddress(1, 2) # ...and converts them back, too.
'A2'
>>> ezsheets.getColumnLetterOf(2)
'B'
>>> ezsheets.getColumnNumberOf('B')
2
>>> ezsheets.getColumnLetterOf(999)
'ALK'
>>> ezsheets.getColumnNumberOf('ZZZ')
18278
当你在源代码中输入地址时,字符串样式的'A2'地址很方便。但如果你在遍历一系列地址并且需要一个列的数字标识符时,(column, row)元组样式的地址更方便。当你需要在这两种格式之间转换时,convertAddress()、getColumnLetterOf()和getColumnNumberOf()函数很有帮助。
读取和写入整个列和行
如前所述,逐个单元格写入数据可能需要太长时间。幸运的是,EZSheets 有Sheet方法可以同时读取和写入整个列和行。getColumn()、getRow()、updateColumn()和updateRow()方法分别用于读取和写入列和行。这些方法会向 Google Sheets 服务器发送请求以更新电子表格,因此它们要求你连接到互联网。在本节的示例中,我们将从第十四章上传produceSales3.xlsx到 Google Sheets。你可以从本书的在线资源中下载它。前八行看起来像表 15-1。
表 15-1:produceSales3.xlsx 电子表格的前八行
| A | B | C | D | |
|---|---|---|---|---|
| 1 | 产品 | 每磅成本 | 销售磅数 | 总计 |
| 2 | 马铃薯 | 0.86 | 21.6 | 18.58 |
| 3 | 菜豆 | 2.26 | 38.6 | 87.24 |
| 4 | 蚕豆 | 2.69 | 32.8 | 88.23 |
| 5 | 西瓜 | 0.66 | 27.3 | 18.02 |
| 6 | 大蒜 | 1.19 | 4.9 | 5.83 |
| 7 | 胡萝卜 | 2.27 | 1.1 | 2.5 |
| 8 | 芦笋 | 2.49 | 37.9 | 94.37 |
要上传此电子表格,将produceSales3.xlsx文件放在当前工作目录中,并在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.upload('produceSales3.xlsx')
>>> sheet = ss.sheets[0]
>>> sheet.getRow(1) # The first row is row 1, not row 0.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> sheet.getRow(2)
['Potatoes', '0.86', '21.6', '18.58', '', '']
>>> sheet.getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getColumn('A') # The same result as getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getRow(3)
['Okra', '2.26', '38.6', '87.24', '', '']
>>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230'])
>>> sheet.getRow(3)
['Pumpkin', '11.50', '20', '230', '', '']
>>> columnOne = sheet.getColumn(1)
>>> for i, value in enumerate(columnOne):
... # Make the Python list contain uppercase strings:
... columnOne[i] = value.upper()
...
>>> sheet.updateColumn(1, columnOne) # Update the entire column in one request.
getRow()和getColumn()函数检索特定行或列中每个单元格的数据,作为一个值列表。请注意,空单元格在列表中变为空字符串值。你可以传递一个列号或字母给getColumn()来告诉它检索特定列的数据。上一个例子显示getColumn(1)和getColumn('A')返回相同的列表。
updateRow()和updateColumn()函数将分别用传递给函数的值列表覆盖行或列中的数据。在这个例子中,第三行最初包含关于秋葵的信息,但updateRow()调用将其替换为关于南瓜的数据。再次调用sheet.getRow(3)以查看第三行的新值。
如果你要更新很多单元格,逐个更新会很慢。获取一列或一行作为一个列表,更新这个列表,然后用列表更新整个列或行会快得多,因为你可以通过一次请求向谷歌云服务做出所有更改。
要一次性获取所有行,调用getRows()方法以返回一个列表的列表。外层列表中的内层列表代表工作表的单行。你可以修改这个数据结构中的值来更改某些行的产物名称、售出磅数和总成本。然后,你可以通过在交互式外壳中输入以下内容将它们传递给updateRows()方法:
>>> rows = sheet.getRows() # Get every row in the spreadsheet.
>>> rows[0] # Examine the values in the first row.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> rows[1]
['POTATOES', '0.86', '21.6', '18.58', '', '']
>>> rows[1][0] = 'PUMPKIN' # Change the produce name.
>>> rows[1]
['PUMPKIN', '0.86', '21.6', '18.58', '', '']
>>> rows[10]
['OKRA', '2.26', '40', '90.4', '', '']
>>> rows[10][2] = '400' # Change the pounds sold.
>>> rows[10][3] = '904' # Change the total.
>>> rows[10]
['OKRA', '2.26', '400', '904', '', '']
>>> sheet.updateRows(rows) # Update the online spreadsheet with the changes.
你可以通过传递getRows()返回的列表的列表(对第 1 行和第 10 行所做的更改进行了修改)给updateRows()来在一次请求中更新整个工作表。
注意,谷歌表格中的行在末尾有空字符串。这是因为上传的工作表有 6 列,但我们只有四列数据。你可以使用rowCount和columnCount属性读取工作表中的行数和列数。然后,通过设置这些值,你可以更改工作表的大小:
>>> sheet.rowCount # The number of rows in the sheet
23758
>>> sheet.columnCount # The number of columns in the sheet
6
>>> sheet.columnCount = 4 # Change the number of columns to 4.
>>> sheet.columnCount # Now the number of columns in the sheet is 4.
4
这些说明应该删除produceSales3.xlsx工作表的第五和第六列,如图 15-3 所示。

图 15-3:改变列数为四之前的(顶部)和之后的(底部)工作表
根据谷歌的文档,谷歌表格的工作表可以包含多达 1000 万个单元格。然而,将工作表的大小仅限于所需的最小范围是一个好主意,这样可以最小化更新和刷新数据所需的时间。
处理列和行
单元格地址在 Google Sheets 中的工作方式与 Excel 中的相同。唯一的不同之处在于,与 Python 的基于 0 的列表索引不同,Google Sheets 有基于 1 的列和行:第一列或行在索引 1,而不是 0。您可以使用 convertAddress() 函数将 'A2' 字符串样式的地址转换为 (column, row) 元组样式的地址(反之亦然)。getColumnLetterOf() 和 getColumnNumberOf() 函数还将列地址在字母和数字之间进行转换。例如,在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ezsheets.convertAddress('A2') # Converts addresses...
(1, 2)
>>> ezsheets.convertAddress(1, 2) # ...and converts them back, too.
'A2'
>>> ezsheets.getColumnLetterOf(2)
'B'
>>> ezsheets.getColumnNumberOf('B')
2
>>> ezsheets.getColumnLetterOf(999)
'ALK'
>>> ezsheets.getColumnNumberOf('ZZZ')
18278
如果您在源代码中输入地址,字符串样式的 'A2' 地址很方便。但如果您在循环遍历地址范围并需要一个列的数字标识符时,(column, row) 元组样式的地址更方便。当您需要在这两种格式之间转换时,convertAddress()、getColumnLetterOf() 和 getColumnNumberOf() 函数很有帮助。
读取和写入整个列和行
如前所述,逐个单元格写入数据可能需要太长时间。幸运的是,EZSheets 提供了同时读取和写入整个列和行的 Sheet 方法。getColumn()、getRow()、updateColumn() 和 updateRow() 方法分别用于读取和写入列和行。这些方法会向 Google Sheets 服务器发送请求以更新电子表格,因此需要您连接到互联网。在本节的例子中,我们将第十四章的 produceSales3.xlsx 上传到 Google Sheets。您可以从本书的在线资源中下载它。前八行看起来像表 15-1。
表 15-1:produceSales3.xlsx 电子表格的前八行
| A | B | C | D | |
|---|---|---|---|---|
| 1 | PRODUCE | COST PER POUND | POUNDS SOLD | TOTAL |
| 2 | Potatoes | 0.86 | 21.6 | 18.58 |
| 3 | Okra | 2.26 | 38.6 | 87.24 |
| 4 | Fava beans | 2.69 | 32.8 | 88.23 |
| 5 | Watermelon | 0.66 | 27.3 | 18.02 |
| 6 | Garlic | 1.19 | 4.9 | 5.83 |
| 7 | Parsnips | 2.27 | 1.1 | 2.5 |
| 8 | Asparagus | 2.49 | 37.9 | 94.37 |
要上传此电子表格,将 produceSales3.xlsx 文件放在当前工作目录中,并在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.upload('produceSales3.xlsx')
>>> sheet = ss.sheets[0]
>>> sheet.getRow(1) # The first row is row 1, not row 0.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> sheet.getRow(2)
['Potatoes', '0.86', '21.6', '18.58', '', '']
>>> sheet.getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getColumn('A') # The same result as getColumn(1)
['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic',
# --snip--
>>> sheet.getRow(3)
['Okra', '2.26', '38.6', '87.24', '', '']
>>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230'])
>>> sheet.getRow(3)
['Pumpkin', '11.50', '20', '230', '', '']
>>> columnOne = sheet.getColumn(1)
>>> for i, value in enumerate(columnOne):
... # Make the Python list contain uppercase strings:
... columnOne[i] = value.upper()
...
>>> sheet.updateColumn(1, columnOne) # Update the entire column in one request.
getRow() 和 getColumn() 函数从特定行或列的每个单元格中检索数据,作为值列表。请注意,空单元格在列表中变为空字符串值。您可以将 getColumn() 传递一个列号或字母,以告诉它检索特定列的数据。前面的例子显示 getColumn(1) 和 getColumn('A') 返回相同的列表。
updateRow() 和 updateColumn() 函数将分别用传递给函数的值列表覆盖行或列中的数据。在这个例子中,第三行最初包含有关秋葵的信息,但 updateRow() 调用将其替换为有关南瓜的数据。再次调用 sheet.getRow(3) 来查看第三行的新值。
如果您有很多单元格需要更新,逐个更新单元格会很慢。获取一个列或行作为列表,更新列表,然后使用列表更新整个列或行要快得多,因为您可以在一个请求中向 Google 的云服务做出所有更改。
要一次性获取所有行,请调用getRows()方法以返回一个列表的列表。外部列表中的内部列表代表工作表的单行。您可以通过修改此数据结构中的值来更改某些行的产品名称、销售磅数和总成本。然后,您可以通过在交互式 shell 中输入以下内容将它们传递给updateRows()方法:
>>> rows = sheet.getRows() # Get every row in the spreadsheet.
>>> rows[0] # Examine the values in the first row.
['PRODUCE', 'COST PER POUND', 'POUNDS SOLD', 'TOTAL', '', '']
>>> rows[1]
['POTATOES', '0.86', '21.6', '18.58', '', '']
>>> rows[1][0] = 'PUMPKIN' # Change the produce name.
>>> rows[1]
['PUMPKIN', '0.86', '21.6', '18.58', '', '']
>>> rows[10]
['OKRA', '2.26', '40', '90.4', '', '']
>>> rows[10][2] = '400' # Change the pounds sold.
>>> rows[10][3] = '904' # Change the total.
>>> rows[10]
['OKRA', '2.26', '400', '904', '', '']
>>> sheet.updateRows(rows) # Update the online spreadsheet with the changes.
您可以通过传递getRows()返回的列表的列表(对第 1 行和第 10 行的更改进行了修改)来单个请求更新整个工作表。
注意,Google Sheets 电子表格中的行末有空字符串。这是因为上传的表格有6列,但我们只有四列数据。您可以使用rowCount和columnCount属性读取工作表中的行数和列数。然后,通过设置这些值,您可以更改工作表的大小:
>>> sheet.rowCount # The number of rows in the sheet
23758
>>> sheet.columnCount # The number of columns in the sheet
6
>>> sheet.columnCount = 4 # Change the number of columns to 4.
>>> sheet.columnCount # Now the number of columns in the sheet is 4.
4
这些说明应该删除produceSales3.xlsx电子表格的第五和第六列,如图 15-3 所示。

图 15-3:更改列数为四之前的表格(顶部)和之后的表格(底部)
根据 Google 的文档,Google Sheets 电子表格可以包含多达 1000 万个单元格。然而,为了最小化更新和刷新数据所需的时间,最好只创建所需大小的表格。
创建、移动和删除工作表
所有 Google Sheets 电子表格都以名为Sheet1的单个工作表开始。您可以使用Sheet()方法将额外的工作表添加到工作表列表的末尾,该方法接受一个可选的字符串作为新工作表的标题。可选的第二个参数可以指定新工作表的整数索引。要创建电子表格并将其添加到其中,请在交互式 shell 中输入以下内容:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet()
>>> ss.title = 'Multiple Sheets'
>>> ss.sheetTitles
('Sheet1',)
>>> ss.Sheet('Spam') # Create a new sheet at the end of the list of sheets.
<Sheet sheetId=2032744541, title='Spam', rowCount=1000, columnCount=26>
>>> ss.Sheet('Eggs') # Create another new sheet.
<Sheet sheetId=417452987, title='Eggs', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss.Sheet('Bacon', 0) # Create a sheet at index 0 in the list of sheets.
<Sheet sheetId=814694991, title='Bacon', rowCount=1000, columnCount=26>
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
这些说明向电子表格中添加了三个新工作表:Bacon、Spam和Eggs(除了默认的Sheet1)。电子表格中的工作表是有序的,并且新工作表会添加到列表的末尾,除非您在Sheet()中传递第二个参数以指定工作表的索引。在这里,您在索引0处创建标题为Bacon的工作表,使Bacon成为电子表格中的第一个工作表,并将其他三个工作表向右移动一个位置。这与insert()列表方法的行为类似。
您可以在屏幕底部的标签上看到新工作表,如图 15-4 所示。

图 15-4:添加了 Spam、Eggs 和 Bacon 电子表格后的多电子表格
你可以从电子表格的索引属性中获取电子表格的顺序,然后为此属性分配一个新的索引以重新排序电子表格:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].index
0
>>> ss.sheets[0].index = 2 # Move the sheet at index 0 to index 2.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Bacon', 'Eggs')
>>> ss.sheets[2].index = 0 # Move the sheet at index 2 to index 0.
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
Sheet 对象的 delete() 方法将从电子表格中删除电子表格。如果你想保留电子表格但删除其中包含的数据,请调用 clear() 方法来清除所有单元格,使其成为一个空白电子表格。请在交互式外壳中输入以下内容:
>>> ss.sheetTitles
('Bacon', 'Sheet1', 'Spam', 'Eggs')
>>> ss.sheets[0].delete() # Delete the sheet at index 0: the "Bacon" sheet.
>>> ss.sheetTitles
('Sheet1', 'Spam', 'Eggs')
>>> ss['Spam'].delete() # Delete the "Spam" sheet.
>>> ss.sheetTitles
('Sheet1', 'Eggs')
>>> sheet = ss['Eggs'] # Assign a variable to the "Eggs" sheet.
>>> sheet.delete() # Delete the "Eggs" sheet.
>>> ss.sheetTitles
('Sheet1',)
>>> ss.sheets[0].clear() # Clear all the cells on the "Sheet1" sheet.
>>> ss.sheetTitles # The "Sheet1" sheet is empty but still exists.
('Sheet1',)
删除电子表格是永久的;无法恢复数据。然而,你可以通过将它们复制到另一个电子表格来备份电子表格,如下一节所述。
复制电子表格
每个 Spreadsheet 对象都有一个包含其 Sheet 对象的有序列表,你可以使用这个列表来重新排序电子表格(如前节所示)或将其复制到其他电子表格中。要将 Sheet 对象复制到另一个 Spreadsheet 对象,请调用 copyTo() 方法。将目标 Spreadsheet 对象作为参数传递。要创建两个电子表格并将第一个电子表格的数据复制到另一个电子表格中,请在交互式外壳中输入以下内容:
>>> import ezsheets
>>> ss1 = ezsheets.Spreadsheet()
>>> ss1.title = 'First Spreadsheet'
>>> ss1.sheets[0].title = 'Spam' # ss1 will have a sheet named Spam.
>>> ss2 = ezsheets.Spreadsheet()
>>> ss2.title = 'Second Spreadsheet'
>>> ss2.sheets[0].title = 'Eggs' # ss2 will have a sheet named Eggs.
>>> ss1[0]
<Sheet sheetId=0, title='Spam', rowCount=1000, columnCount=26>
>>> ss1[0].updateRow(1, ['Some', 'data', 'in', 'the', 'first', 'row'])
>>> ss1[0].copyTo(ss2) # Copy the ss1's Sheet1 to the ss2 spreadsheet.
>>> ss2.sheetTitles # ss2 now contains a copy of ss1's Sheet1.
('Eggs', 'Copy of Spam')
被复制的电子表格在目标电子表格的电子表格列表末尾带有前缀 Copy of。如果你愿意,你可以更改它们的 index 属性,以在新的电子表格中重新排序。
Google 表单
你的 Google 账户还让你可以访问 Google 表单,网址为 forms.google.com/。你可以使用 Google 表单创建调查、活动注册或反馈表单,然后接收用户提交的答案,这些答案存储在 Google Sheets 电子表格中。使用 EZSheets,你的 Python 程序可以访问这些数据。
在第十九章,你将学习如何安排你的 Python 程序在规定的时间定期运行。你可以编写一个程序,定期检查 Google 表单电子表格中的回复,并检测任何它之前未见过的新的条目。然后,使用第二十章中的信息,你可以让程序发送给你一条短信,这样你就可以在表单填写完毕时获得实时通知。
正如你所见,Python 被广泛认为是将多个现有软件系统连接在一起的“胶水”语言,让你能够创建一个比其各部分总和更强大的自动化流程。
项目 11:模拟区块链加密货币骗局
在这个项目中,我们将使用 Google Sheets 作为模拟区块链来跟踪 Boringcoin 的交易,Boringcoin 是我推广的一种加密货币骗局。(事实证明,投资者和客户并不关心你的区块链产品是否使用真实的区块链数据结构;他们仍然会给你钱。)
URL autbor.com/boringcoin 重定向到 Boringcoin 区块链的 Google Sheets URL。该电子表格有三个列:交易发送者、交易接收者和交易金额。金额从发送者扣除并添加到接收者。如果发送者是'PRE-MINE',这笔钱将凭空产生并添加到接收者账户。图 15-5 显示了此 Google Sheet。

图 15-5:存储在 Google Sheet 上的 Boringcoin 假区块链
第一笔交易发送者是'PRE-MINE',接收者是'Al Sweigart',金额是微不足道的1000000000。然后'Al Sweigart'账户将19116 Boringcoins 转给'Miles Bron','Miles Bron'又将118 Boringcoins 转给'not_a_scammer'。第四笔交易将16273 Boringcoins 从'Al Sweigart'转给'some hacker'。(我没有授权这笔交易,并且从那时起就停止使用python12345作为我的 Google 账户密码。)
让我们编写两个程序。首先,auditBoringcoin.py 程序检查所有交易并生成所有账户及其当前余额的字典。其次,addBoringcoinTransaction.py 程序在 Google Sheets 的末尾添加一行新交易。这些区块链程序只是为了娱乐,并不是真实的(尽管“真实”的区块链项目,如 NFT 和“web3”同样是一种幻想)。
第 1 步:审计假区块链
我们需要编写一个程序来检查整个“区块链”并确定所有账户的当前余额。我们将使用一个字典来保存这些数据,其中键是账户名称的字符串,值是账户中 Boringcoins 的整数。我们还希望程序显示加密货币网络中的总 Boringcoins 数量。我们可以从导入 EZSheets 并设置字典开始:
import ezsheets
ss = ezsheets.Spreadsheet('https://autbor.com/boringcoin')
accounts = {} # Keys are names, and values are amounts.
接下来,我们将遍历电子表格中的每一行,识别发送者、接收者和金额。请注意,Google Sheets 始终以字符串形式返回数据,因此我们需要将其转换为整数以对amount值进行数学运算:
# Each row is a transaction. Loop over each one:
for row in ss.sheets[0].getRows():
sender, recipient, amount = row[0], row[1], int(row[2])
如果发送者是特殊账户'PRE-MINE',那么它只是其他账户无限资金的来源。所有最好的加密货币骗局都使用预挖币,我们的也不例外。将金额添加到accounts字典中的接收者账户。setdefault()方法在字典中不存在账户时将其值设置为0:
if sender == 'PRE-MINE':
# The 'PRE-MINE' sender invents money out of thin air.
accounts.setdefault(recipient, 0)
accounts[recipient] += amount
否则,我们应该从发送者那里扣除金额,并将其添加到接收者那里:
else:
# Move funds from the sender to the recipient.
accounts.setdefault(sender, 0)
accounts.setdefault(recipient, 0)
accounts[sender] -= amount
accounts[recipient] += amount
循环结束后,我们可以通过打印账户字典来查看当前余额。
print(accounts)
作为我们审计的一部分,让我们也浏览一下这个字典,并计算每个人的余额总和,以找出整个网络中有多少 Boringcoins。从0开始创建一个total变量,然后使用一个for循环遍历accounts字典中的每个键值对中的值。在将每个值添加到total后,我们可以打印出 Boringcoins 的总金额:
total = 0
for amount in accounts.values():
total += amount
print('Total Boringcoins:', total)
当我们运行这个程序时,输出看起来像这样:
{'Al Sweigart': 999058553, 'Miles Bron': 38283, 'not_a_scammer': 48441,
'some hacker': 44429, 'Tech Bro': 53424, 'Claire Debella': 54443,
'Credulous Journalist': 50408, 'Birdie Jay': 36832, 'Carol': 82867, 'Mark Z.':
68650, 'Bob': 37920, 'Andi Brand': 57218, 'Eve': 88296, 'Al Sweigart sock
#27': 78080, 'Tax evader': 40937, 'Duke Cody': 17544, 'Lionel Toussaint':
54650, 'some scammer': 2694, 'Alice': 44503, 'David': 41828}
Total Boringcoins: 1000000000
总数是1000000000,这是有道理的,因为这是预挖的 Boringcoins 的数量。
第 2 步:进行交易
下一个程序addBoringcoinTransaction.py向“区块链”Google 表格添加额外的行以添加新交易。它从sys.argv列表中读取三个命令行参数:发送者、接收者和金额。例如,你可以从终端运行以下命令:
python addBoringcoinTransaction.py "Al Sweigart" Eve 2000
程序将访问 Google 表格,在底部添加一个空白行,然后用'Al Sweigart'、'Eve'和'2000'填充它。注意,在终端中,你需要用双引号将包含空格的任何命令行参数括起来,例如"Al Sweigart";否则,终端会认为它们是两个单独的参数。
addBoringcoinTransactions.py的开始部分检查命令行参数,并根据它们分配发送者、接收者和金额变量:
import sys, ezsheets
if len(sys.argv) < 4:
print('Usage: python addBoringcoinTransaction.py sender recipient amount')
sys.exit()
# Get the transaction info from the command line arguments:
sender, recipient, amount = sys.argv[1:]
你不需要将amount从字符串转换为整数,因为我们将在工作表中将其作为字符串写入。
接下来,EZSheets 连接到包含假区块链的 Google Sheets,并选择工作表中的第一个工作表(索引0)。请注意,你没有权限编辑 Boringcoin Google Sheets,所以请登录你的 Google 账户,在网页浏览器中打开该 URL,然后选择文件制作副本以将其复制到你的 Google 账户。然后,将'https://autbor.com/boringcoin'字符串替换为浏览器地址栏中你的 Google 表格 URL 的字符串:
# Change this URL to your copy of the Google Sheet, or else you'll
# get a "The caller does not have permission" error.
ss = ezsheets.Spreadsheet('`https://autbor.com/boringcoin`')
sheet = ss.sheets[0]
最后,你应该得到工作表中的行数,增加一行,然后填写这一行的发送者、接收者和金额数据:
# Add one more row to the sheet for a new transaction:
sheet.rowCount += 1
sheet[1, sheet.rowCount] = sender
sheet[2, sheet.rowCount] = recipient
sheet[3, sheet.rowCount] = amount
现在当你从终端运行python addBoringcoinTransaction.py "Al Sweigart" Eve 2000时,Google Sheets 将会有一个新行,在底部添加了Al Sweigart、Eve和2000。你可以重新运行auditBoringcoin.py程序,以查看加密货币网络中每个人的更新后的账户余额。
使用 Google Sheets 作为我们的区块链数据结构是不负责任的,容易出错,并且是一个即将发生的安全灾难。这使得它与大多数市场上销售的区块链产品处于同一水平。不要错过!联系我,在金字塔骗局崩溃之前购买 Boringcoin 的有限优惠!
第 1 步:审计假区块链
我们需要编写一个程序来检查整个“区块链”,并确定所有账户的当前余额。我们将使用一个字典来保存这些数据,其中键是账户名称的字符串,值是账户中 Boringcoins 的数量。我们还希望程序显示整个加密货币网络中有多少 Boringcoins。我们可以从导入 EZSheets 并设置字典开始:
import ezsheets
ss = ezsheets.Spreadsheet('https://autbor.com/boringcoin')
accounts = {} # Keys are names, and values are amounts.
接下来,我们将遍历电子表格中的每一行,识别发送者、接收者和金额。记住,Google 电子表格总是以字符串的形式返回数据,所以我们需要将其转换为整数来进行amount值的数学运算:
# Each row is a transaction. Loop over each one:
for row in ss.sheets[0].getRows():
sender, recipient, amount = row[0], row[1], int(row[2])
如果发送者是特殊账户'PRE-MINE',那么它只是向其他账户提供无限资金的来源。所有最好的加密货币骗局都使用预挖的硬币,我们的也不例外。将金额添加到accounts字典中的接收者账户。setdefault()方法将字典中不存在账户的值设置为0:
if sender == 'PRE-MINE':
# The 'PRE-MINE' sender invents money out of thin air.
accounts.setdefault(recipient, 0)
accounts[recipient] += amount
否则,我们应该从发送者那里扣除金额,并将其添加到接收者那里:
else:
# Move funds from the sender to the recipient.
accounts.setdefault(sender, 0)
accounts.setdefault(recipient, 0)
accounts[sender] -= amount
accounts[recipient] += amount
循环结束后,我们可以通过打印accounts字典来查看当前的余额。
print(accounts)
作为审计的一部分,让我们也遍历这个字典,计算每个人的余额总和,以找出整个网络中有多少 Boringcoins。从0开始创建一个total变量,然后让一个for循环遍历accounts字典中的每个键值对中的值。在将每个值添加到total后,我们可以打印 Boringcoins 的总数:
total = 0
for amount in accounts.values():
total += amount
print('Total Boringcoins:', total)
当我们运行这个程序时,输出看起来像这样:
{'Al Sweigart': 999058553, 'Miles Bron': 38283, 'not_a_scammer': 48441,
'some hacker': 44429, 'Tech Bro': 53424, 'Claire Debella': 54443,
'Credulous Journalist': 50408, 'Birdie Jay': 36832, 'Carol': 82867, 'Mark Z.':
68650, 'Bob': 37920, 'Andi Brand': 57218, 'Eve': 88296, 'Al Sweigart sock
#27': 78080, 'Tax evader': 40937, 'Duke Cody': 17544, 'Lionel Toussaint':
54650, 'some scammer': 2694, 'Alice': 44503, 'David': 41828}
Total Boringcoins: 1000000000
总数是1000000000,这是有道理的,因为这是预挖的 Boringcoins 的数量。
第 2 步:进行交易
下一个程序*addBoringcoinTransaction.py*向“区块链”Google 电子表格添加额外的行以添加新的交易。它从sys.argv列表中读取三个命令行参数:发送者、接收者和金额。例如,你可以在终端中运行以下命令:
python addBoringcoinTransaction.py "Al Sweigart" Eve 2000
程序将访问 Google 电子表格,在底部添加一个空白行,然后用值'Al Sweigart'、'Eve'和'2000'填充它。注意,在终端中,你需要用双引号将包含空格的任何命令行参数括起来,例如"Al Sweigart";否则,终端会认为它们是两个单独的参数。
*addBoringcoinTransactions.py*的开始部分检查命令行参数,并根据它们分配发送者、接收者和金额变量:
import sys, ezsheets
if len(sys.argv) < 4:
print('Usage: python addBoringcoinTransaction.py sender recipient amount')
sys.exit()
# Get the transaction info from the command line arguments:
sender, recipient, amount = sys.argv[1:]
你不需要将amount从字符串转换为整数,因为我们将在电子表格中以字符串的形式写入它。
接下来,EZSheets 将连接到包含伪造区块链的 Google Sheets,并选择电子表格中的第一个工作表(索引为 0)。请注意,你没有权限编辑 Boringcoin Google Sheets,因此请登录到你的 Google 账户后,在网页浏览器中打开该 URL,然后选择 文件制作副本以将其复制到你的 Google 账户。然后,将 'https://autbor.com/boringcoin' 字符串替换为浏览器地址栏中你的 Google 工作表 URL 的字符串:
# Change this URL to your copy of the Google Sheet, or else you'll
# get a "The caller does not have permission" error.
ss = ezsheets.Spreadsheet('`https://autbor.com/boringcoin`')
sheet = ss.sheets[0]
最后,你应该获取工作表中的行数,将其加一,然后填写这一行的列,包括发送者、接收者和金额数据:
# Add one more row to the sheet for a new transaction:
sheet.rowCount += 1
sheet[1, sheet.rowCount] = sender
sheet[2, sheet.rowCount] = recipient
sheet[3, sheet.rowCount] = amount
现在,当你从终端运行 python addBoringcoinTransaction.py "Al Sweigart" Eve 2000 时,Google Sheets 将在底部添加一行,包含 Al Sweigart、Eve 和 2000。你可以重新运行 auditBoringcoin.py 程序,以查看加密货币网络中每个人的更新后的账户余额。
使用 Google Sheets 存储我们的区块链数据结构是不负责任的,容易出错,并且是一个即将发生的安全灾难。这使得它与大多数市场上销售的区块链产品处于同一水平。不要错过!联系我,在金字塔骗局崩溃之前,抓住这个有限的机会购买 Boringcoin!
与 Google Sheets 配额一起工作
由于 Google Sheets 是在线的,你可以轻松地在多个用户之间共享工作表,这些用户可以同时访问工作表。然而,这也意味着读取和更新工作表的速度将比读取和更新存储在硬盘上本地的 Excel 文件慢。此外,Google Sheets 限制了你可以执行多少次读取和写入操作。
根据谷歌的开发者指南,用户每天只能创建 250 个新的电子表格,免费谷歌账户每分钟可以执行几百次请求。你可以在 developers.google.com/sheets/api/limits 找到谷歌的使用限制。尝试超过这个配额将引发 googleapiclient.errors.HttpError “配额超出配额组”异常。EZSheets 将自动捕获这个异常并重试请求。当这种情况发生时,读取或写入数据的函数调用将在返回之前需要几秒钟(甚至一整分钟或两分钟)。如果请求继续失败(如果另一个使用相同凭据的脚本也在进行请求,这种情况是可能的),EZSheets 将重新引发这个异常。
这意味着,有时你的 EZSheets 方法调用可能需要几秒钟才能返回。如果你想查看你的 API 使用情况或增加配额,请访问 IAM & Admin Quotas 页面,网址为 console.developers.google.com/iam-admin/quotas,了解如何为增加的使用付费。如果你更愿意自己处理 HttpError 异常,可以将 ezsheets.IGNORE_QUOTA 设置为 True,当 EZSheets 遇到这些异常时,其方法将引发这些异常。
摘要
Google Sheets 是一种流行的在线电子表格应用程序,它运行在你的浏览器中。使用 EZSheets 第三方包,你可以下载、创建、读取和修改电子表格。EZSheets 将电子表格表示为 Spreadsheet 对象,每个对象都包含一个有序的 Sheet 对象列表。每个工作表都有数据列和行,你可以通过多种方式读取和更新这些数据。
虽然 Google Sheets 使数据共享和协作编辑变得容易,但其主要缺点是速度:你必须通过网络请求更新电子表格,这可能需要几秒钟才能执行。但对于大多数用途,这种速度限制不会影响使用 EZSheets 的 Python 脚本。Google Sheets 还限制了你可以进行更改的频率。
要了解 EZSheets 功能的完整文档,请访问 ezsheets.readthedocs.io/。
练习问题
1. 你需要哪三个文件才能让 EZSheets 访问 Google Sheets?
2. EZSheets 有哪两种类型的对象?
3. 如何从 Google Sheets 电子表格创建 Excel 文件?
4. 如何从 Excel 文件创建 Google Sheets 电子表格?
5. ss 变量包含一个 Spreadsheet 对象。以下代码将如何从标题为 Students 的工作表中的单元格 B2 读取数据?
6. 如何找到列 999 的列字母?
7. 如何找出工作表有多少行和列?
8. 如何删除电子表格?这种删除是永久的吗?
9. 哪些函数将分别创建一个新的 Spreadsheet 对象和一个新的 Sheet 对象?
10. 如果你通过频繁的读写请求使用 EZSheets 超过了你的 Google 账户配额,会发生什么?
练习程序
为了练习,编写程序执行以下任务。
下载 Google Forms 数据
我之前提到过,Google 表单允许你创建简单的在线表单,这使得收集人们的信息变得容易。表单中输入的信息存储在 Google Sheets 电子表格中。对于这个项目,编写一个可以自动下载用户提交的表单信息的程序。转到 docs.google.com/forms/ 并开始一个新的空白表单。向表单添加字段,要求用户输入姓名和电子邮件地址。然后,点击右上角的 发送 按钮以获取你新表单的链接。尝试在这个表单中输入一些示例响应。
在你表单的 响应 选项卡上,点击绿色的 创建电子表格 按钮来创建一个 Google Sheets 电子表格,该电子表格将保存用户提交的响应。你应该能在该电子表格的第一行看到你的示例响应。然后,使用 EZSheets 编写一个 Python 脚本来收集该电子表格上的电子邮件地址列表。
将电子表格转换为其他格式
你可以使用 Google Sheets 将电子表格文件转换为其他格式。编写一个将提交的文件传递给 upload() 的脚本。一旦电子表格上传到 Google Sheets,使用 downloadAsExcel()、downloadAsODS() 等其他函数下载它,以创建这些其他格式的电子表格副本。
在电子表格中查找错误
在经过漫长的一天豆子计数后,我已经完成了一个包含所有豆子总和的电子表格,并将其上传到 Google Sheets。该电子表格是公开可查看的(但不可编辑)。你可以使用以下代码获取此电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg')
通过访问 docs.google.com/spreadsheets/d/1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg 在你的浏览器中查看电子表格。该电子表格第一列的列是 每个罐子的豆子数量,罐子数量 和 总豆子数量。总豆子数量 列是 每个罐子的豆子数量 和 罐子数量 列中数字的乘积。然而,在这个包含 15,000 行的电子表格中有一个错误。手动检查这么多行太麻烦了。幸运的是,你可以编写一个检查总和的脚本。
作为提示,你可以使用 ss.sheets[0].getRow(rowNum) 来访问一行中的单个单元格,其中 ss 是 Spreadsheet 对象,rowNum 是行号。记住,在 Google Sheets 中,行号从 1 开始,而不是 0。单元格的值将是字符串,所以你需要在程序处理它们之前将它们转换为整数。表达式 int(ss.sheets[0].getRow(2)[0]) * int(ss.sheets[0].getRow(2)[1]) == int(ss.sheets[0].getRow(2)[2]) 如果第二行有正确的总和,则评估为 True。将此代码放入循环中,以识别工作表中哪一行有错误的总和。
下载 Google 表单数据
我之前提到,Google Forms 允许您创建简单的在线表单,这使得收集人们的信息变得容易。表单中输入的信息存储在 Google Sheets 电子表格中。对于这个项目,编写一个可以自动下载用户提交的表单信息的程序。转到docs.google.com/forms/并开始一个新的空白表单。向表单添加询问用户姓名和电子邮件地址的字段。然后,点击右上角的发送按钮以获取您新表单的链接。尝试将一些示例响应输入到这个表单中。
在您表单的响应标签页上,点击绿色的创建电子表格按钮以创建一个将保存用户提交的响应的 Google Sheets 电子表格。您应该看到示例响应出现在此电子表格的前几行中。然后,使用 EZSheets 编写一个 Python 脚本,以收集此电子表格上的电子邮件地址列表。
将电子表格转换为其他格式
您可以使用 Google Sheets 将电子表格文件转换为其他格式。编写一个将提交的文件传递给upload()的脚本。一旦电子表格上传到 Google Sheets,使用downloadAsExcel()、downloadAsODS()和其他此类函数下载它,以在其他格式中创建电子表格的副本。
在电子表格中查找错误
在漫长的一天在会计办公室工作后,我已经完成了一个包含所有豆子总数的电子表格,并将其上传到 Google Sheets。该电子表格可供公开查看(但不能编辑)。您可以使用以下代码获取此电子表格:
>>> import ezsheets
>>> ss = ezsheets.Spreadsheet('1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg')
通过访问docs.google.com/spreadsheets/d/1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg在您的浏览器中查看电子表格。此电子表格的第一个工作表的列是每罐豆子数量、罐子数量和总豆子数。总豆子数列是每罐豆子数量和罐子数量列中数字的乘积。然而,在这个工作表的 15,000 行中有一行有错误。手动检查这么多行太麻烦了。幸运的是,您可以编写一个检查总数的脚本。
作为提示,您可以使用ss.sheets[0].getRow(rowNum)访问行中的单个单元格,其中 ss 是Spreadsheet对象,rowNum 是行号。记住,Google Sheets 中的行号从 1 开始,而不是 0。单元格值将是字符串,因此您需要将它们转换为整数,然后程序才能处理它们。表达式int(ss.sheets[0].getRow(2)[0]) * int(ss.sheets[0].getRow(2)[1]) == int(ss.sheets[0].getRow(2)[2])如果第 2 行有正确的总数,则评估为True。将此代码放入循环中,以识别工作表中哪一行有错误的总数。
16 SQLITE 数据库

你可能已经习惯了将信息组织到电子表格中,如 Excel 或 Google Sheets,但大多数软件将数据存储在称为 数据库 的应用程序中。数据库使你的程序能够轻松检索你想要的具体数据。如果你有一个包含猫的电子表格或文本文件,并想找到名叫 Zophie 的猫的毛色,你可以按 CTRL-F 并输入“Zophie”。但如果你想要找到体重在 3 到 5 千克之间,且在 2023 年 10 月之前出生的所有猫的毛色呢?即使使用第九章中的正则表达式,这也会是一个棘手的编程任务。
数据库允许你执行此类复杂的查询,这些查询是用 结构化查询语言(SQL) 的迷你语言编写的。你将看到 SQL 这个术语被用来指代数据库操作的语言以及理解这种语言的数据库;它通常发音为“es-cue-el”,有时也称为“sequel”。本章将使用 SQLite(发音为“sequel-ite”、“es-cue-lite”或“es-cue-el-ite”)介绍你到 SQL 和数据库概念,SQLite 是 Python 中包含的一个轻量级数据库。
SQLite 是最广泛部署的数据库软件,因为它在所有操作系统上运行,并且足够小,可以嵌入到其他应用程序中。同时,SQLite 的简化使其与其他数据库显著不同。虽然大型数据库软件如 PostgreSQL、MySQL、Microsoft SQL Server 和 Oracle 旨在在专用的服务器硬件上运行,并通过网络访问,SQLite 将整个数据库存储在计算机上的单个文件中。
即使你已经熟悉 SQL 数据库,SQLite 也有其独特的特性,你应该阅读这一章节来学习如何充分利用它。你可以在 sqlite.org/docs.html 找到在线 SQLite 文档,以及在 docs.python.org/3/library/sqlite3.html 找到 Python sqlite3 模块文档。
电子表格与数据库
让我们考虑电子表格和数据库之间的相似之处和不同之处。在电子表格中,行包含单个记录,而列代表每个记录字段中存储的数据类型。例如,图 16-1 是我一些猫的电子表格。列列出了每只猫的名字、生日、毛色和体重(以千克为单位)。

图 16-1:电子表格以具有固定列结构的行存储数据记录。
我们可以将相同的信息存储在数据库中。你可以将数据库中的表想象成电子表格,一个数据库可以包含一个或多个表。表中的每个记录(也称为行或条目)都有不同属性的列。像 SQLite 这样的数据库被称为关系数据库,这里的关系意味着数据库可以包含多个表,并且它们之间存在关系,正如你稍后将会看到的。
电子表格和数据库都会对其包含的数据进行标记。电子表格会自动用字母标记列,用数字标记行。此外,示例中的猫电子表格使用其第一行来给出列的描述性名称。后续的每一行代表一只具体的猫。在 SQL 数据库中,表通常为每个记录的主键设置一个 ID 列:一个可以明确识别记录的唯一整数。在 SQLite 中,这个列被称为rowid,SQLite 会自动将其添加到你的表中。
删除电子表格的行会使下面的所有行上移,改变它们的行号。但数据库记录的主键 ID 是唯一的,不会改变。这在许多情况下都很有用。如果一只猫被改名或体重发生变化怎么办?如果我们想按名字字母顺序重新排列行以列出猫怎么办?每只猫都需要一个唯一的识别号码,无论数据如何变化,这个号码都保持不变。我们可以在电子表格中添加一个行 ID 列来模拟 SQLite 表的rowid列。即使行被删除或移动,这个 ID 值也会保持不变,如图 16-2 所示,其中 ID 为 5 至 10 的猫已被删除。

图 16-2:行 ID 号码,与电子表格行号不同,即使在删除 ID 为 5 至 10 的猫之后,也为每个记录提供了一个唯一的标识符(左图)。(右图)
人们使用电子表格的另一种方式与数据库存储数据的方式完全不同。电子表格可以作为表单的模板,而不是基于行的数据存储。你可能见过类似于图 16-3 的电子表格。

图 16-3:一个格式很多且大小固定的电子表格通常不适合作为数据库。
这些电子表格通常有很多格式,包括背景颜色、合并单元格和不同的字体,以便于人类眼睛看起来美观。虽然基于行的数据电子表格可以无限向下扩展,因为新数据被添加,但这些电子表格通常具有固定的大小和填空设计。它们通常是为了人类打印出来查看,而不是为了 Python 程序从中提取数据。
数据库并不美观;它们只包含原始数据。更重要的是,虽然电子表格允许您将任何数据放入任何单元格,但数据库具有更严格的结构,以便于软件检索数据。如果您的数据看起来像第十四章的示例和第十五章的 EZSheets 库,并且将其留在 Excel 或 Google 电子表格中。
SQLite 与其他 SQL 数据库
如果您习惯于使用其他 SQL 数据库,您可能会想知道 SQLite 与它们相比如何。简而言之,SQLite 在简单性和功能之间取得了平衡。它是一个完整的关联数据库,使用 SQL 读取和写入大量数据,但它在您的 Python 程序中运行,并在单个文件上操作。您的程序导入 sqlite3 模块,就像导入 sys、math 或 Python 标准库中的任何其他模块一样。
这里是 SQLite 与其他数据库软件之间的主要区别:
-
SQLite 数据库存储在一个单独的文件中,您可以像移动、复制或备份任何其他文件一样移动、复制或备份它。
-
SQLite 可以在资源较少的计算机上运行,例如嵌入式设备或几十年前的笔记本电脑。
-
SQLite 是无服务器的;它不需要在您的笔记本电脑上或任何专用服务器硬件上持续运行后台服务器应用程序。没有涉及任何网络连接。
-
从用户的角度来看,SQLite 不需要任何安装或配置。它是 Python 程序的一部分。
-
为了提高性能,SQLite 数据库可以完全存在于内存中,并在程序退出之前保存到文件中。
-
虽然 SQLite 列具有数据类型,如数字和文本,就像其他 SQL 数据库一样,但 SQLite 并不严格强制执行列的数据类型。
-
SQLite 中没有权限设置或用户角色。SQLite 没有像其他 SQL 数据库中的
GRANT或REVOKE语句。 -
SQLite 是公共领域软件;您可以在商业用途或任何您想要的方式下使用它,而不受限制。
SQLite 的主要缺点是它无法高效地处理数百或数千个同时写入操作(例如,来自社交媒体网络应用程序)。除此之外,SQLite 与任何数据库一样强大,能够可靠地处理 GB 或甚至 TB 的数据,以及同时读取操作,快速且容易。
SQLite 并不是将其定位为其他数据库软件的竞争对手,而是定位为使用open()函数处理文本文件(或您将在第十八章中学习的 JSON、XML 和 CSV 文件)的竞争对手。如果您的程序需要存储和快速检索大量数据的能力,SQLite 是 JSON 或电子表格文件更好的替代品。
创建数据库和表
让我们从使用 SQL 创建我们的第一个数据库和表开始。SQL 是一种迷你语言,您可以在 Python 内部与之交互,就像正则表达式对于常规表达式一样。就像正则表达式一样,SQL 查询是以 Python 字符串值的形式编写的。正如您可以编写自己的 Python 代码来执行正则表达式执行的文本模式匹配一样,您可以编写自己的自定义 Python 代码来在 Python 字典和列表中搜索匹配的数据。但是编写正则表达式和 SQL 数据库查询使得这些任务在长期来看更加简单,即使它们最初需要您学习一项新技能。让我们探索如何编写创建新数据库中表的查询。
我们将在名为example.db的文件中创建一个示例 SQLite 数据库来存储有关猫的信息。要创建数据库,首先导入sqlite3模块。(数字3代表 SQLite 主版本 3,这与 Python 3 无关。)SQLite 数据库位于单个文件中。文件的名称可以是任何名称,但按照惯例,我们给它一个.db文件扩展名。.sqlite扩展名也常被使用。
一个数据库可以包含多个表,并且每个表应该存储一种特定类型的数据。例如,一个表可以包含猫的记录,而另一个表可以包含对第一个表中的特定猫进行的疫苗接种记录。您可以将表想象成一个元组的列表,其中每个元组都是一行。cats表本质上与[('Zophie', '2021-01-24', 'black', 5.6), ('Colin', '2016-12-24', 'siamese', 6.2), ...]相同,等等。
让我们创建一个数据库,然后为猫的数据创建一个表,向其中插入一些猫的记录,从数据库中读取数据,并关闭数据库连接。
连接到数据库
编写 SQLite 代码的第一步是通过调用sqlite3.connect()获取数据库文件的Connection对象。在交互式 shell 中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
函数的第一个参数可以是文件名字符串或数据库文件的pathlib.Path对象。如果此文件名不属于现有的 SQLite 数据库,则函数将创建一个包含空数据库的新文件。例如,sqlite3.connect('example.db', isolation_level=None)将连接当前工作目录中名为example.db的数据库文件。如果此文件不存在,则函数将创建一个空文件。
如果你连接的文件存在但不是 SQLite 数据库文件,当你尝试执行查询时,Python 会抛出 sqlite3.DatabaseError: file is not a database 异常。“检查路径有效性”在第十章中解释了如何使用 exists() Path 方法以及 os.path.exists() 函数,这些可以告诉你的程序文件是否存在。
isolation_level=None 关键字参数导致数据库使用自动提交模式。这让你不必在每次 execute() 方法调用后编写 commit() 方法调用。
sqlite3.connect() 函数返回一个 Connection 对象,我们在这些示例中将它存储在一个名为 conn 的变量中。每个 Connection 对象连接到一个 SQLite 数据库文件。当然,你可以为这个 Connection 对象选择任何你喜欢的变量名,如果你的程序同时打开多个数据库,你应该使用更具描述性的变量名。但对于只连接一个数据库的小程序来说,名称 conn 很容易书写且足够描述性。(名称 con 会更短,但容易误解为“console”、“content”或“confusing name for a variable”。)
当你的程序完成数据库操作后,调用 conn.close() 来关闭连接。程序在终止时也会自动关闭连接。
创建表
在连接到一个新的、空白的数据库后,使用 CREATE TABLE SQL 查询创建一个表。要运行 SQL 查询,你必须调用 Connection 对象的 execute() 方法。将查询字符串传递给这个 conn.execute() 方法:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL,
birthdate TEXT, fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
按照惯例,SQL 关键字,如 CREATE 和 TABLE,使用大写字母书写。然而,SQLite 不强制执行这一点;查询 'create table if not exists cats (name text not null, birthdate text, fur text, weight_kg real) strict' 运行得很好。表和列名也不区分大小写,但惯例是将它们写成小写,并且用下划线分隔多个单词,如 weight_kg。
如果你尝试创建一个已经存在的表(没有 IF NOT EXISTS 部分),CREATE TABLE 语句会抛出 sqlite3.OperationalError: table cats already exists 异常。包含这个部分是避免触发这个异常的快捷方式,你几乎总是希望将其添加到你的 CREATE TABLE 查询中。
在我们的示例中,我们使用 CREATE TABLE IF NOT EXISTS 关键字后跟表名 cats。表名之后是一组括号,包含列名和数据类型。
定义数据类型
SQLite 有六种数据类型:
NULL 类似于 Python 的 None
INT 或 INTEGER 类似于 Python 的 int 类型
REAL 指的是数学术语 实数;类似于 Python 的 float 类型
TEXT 类似于 Python 的 str 类型
BLOB 简称 Binary Large Object;类似于 Python 的 bytes 类型,并且对于在数据库中存储整个文件很有用
SQLite 有自己的数据类型,因为它不仅仅是为 Python 构建的;其他编程语言也可以与 SQLite 数据库交互。
与其他 SQL 数据库软件不同,SQLite 对其列的数据类型没有严格的要求。这意味着 SQLite 默认会乐意将字符串 'Hello' 存储在 INTEGER 列中,而不会引发异常。但 SQLite 的数据类型也不是完全装饰性的;如果可能,SQLite 会自动 转换(即更改)数据到列的数据类型,这被称为 类型亲和性。例如,如果你将字符串 '42' 添加到 INTEGER 列中,SQLite 会自动将值存储为整数 42,因为该列对整数具有类型亲和性。然而,如果你将字符串 'Hello' 添加到 INTEGER 列中,SQLite 会存储 'Hello'(不会出错),因为尽管有整数类型亲和性,'Hello' 无法转换为整数。
STRICT 关键字为该表启用 严格模式。在严格模式下,每个列都必须指定一个数据类型,如果你尝试将错误类型的数据插入表中,SQLite 将引发 sqlite3.IntegrityError 异常。SQLite 仍然会自动将数据转换为列的数据类型;将 '42' 插入 INTEGER 列将插入整数 42。然而,字符串 'Hello' 无法转换为整数,因此尝试插入它将引发异常。我强烈建议使用严格模式;它可以提前警告你由于将错误数据插入表中而导致的错误。
SQLite 在版本 3.37.0 中添加了 STRICT 关键字,该版本由 Python 3.11 及以后的版本使用。早期版本不了解严格模式,如果你尝试使用它,将会报告语法错误。你可以通过检查 sqlite3.sqlite_version 变量来始终检查 Python 正在使用的 SQLite 版本,该变量看起来可能如下所示:
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.`xx`.`xx`'
SQLite 没有布尔数据类型,因此可以使用 INTEGER 来表示布尔数据;你可以存储 1 来表示 True,存储 0 来表示 False。SQLite 也没有日期、时间或日期时间数据类型。相反,你可以使用 TEXT 数据类型来存储一个格式,如表 16-1 中列出的字符串。
表 16-1:SQLite 中日期、时间和日期时间的推荐格式
| 格式 | 示例 |
|---|---|
YYYY-MM-DD |
'2035-10-31' |
YYYY-MM-DD HH:MM:SS |
'2035-10-31 16:30:00' |
YYYY-MM-DD HH:MM:SS.SSS |
'2035-10-31 16:30:00.407' |
HH:MM:SS |
'16:30:00' |
HH:MM:SS.SSS |
'16:30:00.407' |
name TEXT NOT NULL 中的 NOT NULL 部分指定 Python 的 None 值不能存储在 name 列中。这是一种使表列成为必需的好方法。
SQLite 表自动创建一个包含唯一主键整数的 rowid 列。即使你的 cats 表中有两只猫的名字、生日、毛色和体重巧合地相同,rowid 也能让你区分它们。
列出表格和列
所有 SQLite 数据库都有一个名为 sqlite_schema 的表,其中列出了关于数据库的元数据,包括其所有表。要列出 SQLite 数据库中的表,运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('cats',)]
输出显示了刚刚创建的 cats 表。(我在第 394 页的“从数据库读取数据”中解释了 SELECT 语句的语法。)要获取 cats 表中列的信息,运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('PRAGMA TABLE_INFO(cats)').fetchall()
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2,
'fur', 'TEXT', 0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0)]
此查询返回一个元组列表,每个元组描述了表中的一列。例如,(1, 'birthdate', 'TEXT', 0, None, 0) 这个元组提供了关于 birthdate 列的以下信息:
列位置 1 表示该列是表中的第二列。列号是从 0 开始的,就像 Python 列表索引一样,所以第一列位于位置 0。
名称 'birthdate' 是列的名称。记住,SQLite 列和表名不区分大小写。
数据类型 'TEXT' 是 birthdate 列的 SQLite 数据类型。
列是否为 NOT NULL 0 表示 False,意味着该列不是 NOT NULL(即,你可以在这个列中放置 None 值)。
默认值 如果未指定其他值,则 None 是插入的默认值。
列是否是主键 0 表示 False,意味着这个列不是主键列。
注意,sqlite_schema 表本身并没有被列为一个表。你永远不会需要自己修改 sqlite_schema 表,这样做很可能会损坏数据库,使其无法读取。
CRUD 数据库操作
CRUD 是数据库执行的四项基本操作的缩写:创建数据、读取数据、更新数据、删除数据。在 SQLite 中,我们分别使用 INSERT、SELECT、UPDATE 和 DELETE 语句来执行这些操作。以下是每个语句的示例,我们稍后将作为字符串传递给 conn.execute():
-
INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6) -
SELECT rowid, * FROM cats ORDER BY fur -
UPDATE cats SET fur = "gray tabby" WHERE rowid = 1 -
DELETE FROM cats WHERE rowid = 1
大多数应用程序和社交媒体网站实际上只是 CRUD 数据库的复杂用户界面。当你发布照片或回复时,你实际上是在某个数据库中创建记录。当你滚动社交媒体时间线时,你正在从数据库中读取记录。当你编辑或删除帖子时,你正在执行更新或删除操作。无论你正在学习新的应用程序、编程语言或查询语言,都要使用 CRUD 缩写来提醒自己应该了解哪些基本操作。
将数据插入数据库
现在我们已经创建了数据库和 cats 表,让我们插入我的宠物猫的记录。我家大约有 300 只猫,使用 SQLite 数据库可以帮助我跟踪它们。一个 INSERT 语句可以向表中添加新记录。将以下代码输入到交互式外壳中:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL, birthdate TEXT,
fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
>>> conn.execute('INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6)')
<sqlite3.Cursor object at 0x00000162842E78C0>
这个INSERT查询向cats表添加了一行新数据。括号内是该列的逗号分隔值。对于INSERT查询,括号是必需的。请注意,当插入TEXT值时,我使用了双引号("),因为我已经在查询字符串中使用了单引号(')。sqlite3模块使用单引号或双引号作为其TEXT值。
事务
一个INSERT语句开始一个事务,这是数据库中的一个工作单元。事务必须通过ACID 测试,这是一个数据库概念,意味着事务是:
原子性 事务要么完全执行,要么完全不执行。
一致性 事务不会违反约束,例如列的NOT NULL规则。
隔离性 一个事务不会影响其他事务。
持久性 如果提交,事务结果将被写入持久存储,例如硬盘。
SQLite 是一个符合 ACID 的数据库;它甚至通过了在事务过程中模拟计算机断电的测试,因此您可以有很高的信心,数据库文件不会处于损坏、不可用的状态。SQLite 查询要么将数据完全插入数据库,要么根本不插入。
SQL 注入攻击
一种称为SQL 注入攻击的黑客技术可以改变您的查询以执行您未打算执行的操作。这些技术超出了本书的范围,并且如果您的程序不接受来自互联网陌生人的数据,那么这很可能不是问题。但为了防止这种情况,在将数据插入或更新到数据库中时,每次引用变量时都使用?问号语法。
例如,如果我想根据存储在变量中的数据插入一个新的猫记录,我不应该像这样直接将变量插入到查询字符串中,使用 Python:
>>> cat_name = 'Zophie'
>>> cat_bday = '2021-01-24'
>>> fur_color = 'black'
>>> cat_weight = 5.6
>>> conn.execute(f'INSERT INTO cats VALUES ("{cat_name}", "{cat_bday}",
"{fur_color}", {cat_weight})')
<sqlite3.Cursor object at 0x0000022B91BB7C40>
如果这些变量的值来自用户输入,例如一个 Web 应用表单,黑客可能可以指定会改变查询意义的字符串。相反,我应该在查询字符串中使用一个?,然后在查询字符串之后传递一个变量列表:
>>> conn.execute('INSERT INTO cats VALUES (?, ?, ?, ?)', [cat_name, cat_bday,
fur_color, cat_weight])
<sqlite3.Cursor object at 0x0000022B91BB7C40>
execute()方法在确保变量值不会导致 SQL 注入攻击后,将查询字符串中的?占位符替换为变量值。虽然此类攻击不太可能适用于您的代码,但使用?占位符而不是自己格式化查询字符串是一个好习惯。
从数据库中读取数据
数据库中一旦有数据,您就可以使用SELECT查询来读取它。将以下内容输入到交互式 shell 中,以从example.db数据库中读取数据:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
SELECT查询的execute()方法调用返回一个Cursor对象。为了获取实际数据,我们在该Cursor对象上调用fetchall()方法。每条记录作为元组列表中的元组返回。每个元组中的数据按表列的顺序出现。
而不是自己编写 Python 代码来遍历这个元组列表,你可以让 SQLite 提取你想要的具体信息。示例 SELECT 查询包含四个部分:
-
SELECT关键字 -
你想要检索的列,其中
*表示“除了rowid之外的所有列” -
FROM关键字 -
要检索数据的表;在这种情况下,是
cats表
如果你只想检索 cats 表中记录的 rowid 和 name 列,你的查询将如下所示:
>>> conn.execute('SELECT rowid, name FROM cats').fetchall()
[(1, 'Zophie')]
你还可以使用 SQL 来过滤查询结果,正如你将在下一节中学习的。
遍历查询结果
fetchall() 方法将你的 SELECT 查询结果作为元组列表返回。常见的编码模式是使用 for 循环来对每个元组执行某些操作。例如,从 nostarch.com/automate-boring-stuff-python-3rd-edition 下载 sweigartcats.db 文件,然后将其输入到交互式 shell 中以处理其数据:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> for row in conn.execute('SELECT * FROM cats'):
... print('Row data:', row)
... print(row[0], 'is one of my favorite cats.')
...
Row data: ('Zophie', '2021-01-24', 'gray tabby', 5.6)
Zophie is one of my favorite cats.
Row data: ('Miguel', '2016-12-24', 'siamese', 6.2)
Miguel is one of my favorite cats.
Row data: ('Jacob', '2022-02-20', 'orange and white', 5.5)
Jacob is one of my favorite cats.
# --snip--
for 循环可以在不调用 fetchall() 的情况下遍历 conn.execute() 返回的行数据元组,并且 for 循环体内的代码可以单独操作每一行,因为 row 变量填充了查询的行数据元组。然后代码可以通过元组的整数索引访问列:索引 0 为名称,索引 1 为出生日期,依此类推。
过滤检索数据
我们的 SELECT 查询已经检索了表中的每一行,但我们可能只想检索符合某些过滤标准的行。使用 sweigartcats.db 文件,在 SELECT 语句中添加一个 WHERE 子句以提供搜索参数,例如具有黑色毛发:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE fur = "black"').fetchall()
[❶('Zophie', '2021-01-24', 'black', 5.6), ('Toby', '2021-05-17', 'black',
6.8), ('Thor', '2013-05-14', 'black', 5.2), ('Sassy', '2017-08-20', 'black',
7.5), ('Hope', '2016-05-22', 'black', 7.6)]
在这个例子中,WHERE 子句 WHERE fur = "black" 将只检索 fur 列中包含 "black" 的记录数据。
SQLite 为 WHERE 子句定义了自己的运算符,但它们与 Python 的运算符类似:=, !=, <, >, <=, >=, AND, OR, 和 NOT。请注意,SQLite 使用 **=** 运算符表示“等于”,而 Python 使用 == 运算符来达到这个目的。在运算符的两侧,你可以放置列名或字面值。
比较将在表中的每一行发生。例如,对于 WHERE fur = "black",SQLite 执行以下比较:
-
因为
fur是'black'并且'black'='black'是真,SQLite 将包含第 ❶ 行在结果中。 -
对于行
(2, 'Miguel', '2016-12-24', 'siamese', 6.2),fur是'siamese'并且'siamese'='black'是假,所以它不包括该行在结果中。 -
对于行
(3, 'Jacob', '2022-02-20', 'orange and white', 5.5),fur是'orange and white'并且'orange and white'='black'是假,所以它不包括该行在结果中。
... 以此类推,对于 cats 表中的每一行。
让我们用一个更复杂的 WHERE 子句继续上一个例子:WHERE fur = "black" OR birthdate >= "2024-01-01"。我们还可以使用 pprint.pprint() 函数来“美化打印”返回的列表:
>>> import pprint
>>> matching_cats = conn.execute('SELECT * FROM cats WHERE fur = "black"
OR birthdate >= "2024-01-01"').fetchall()
>>> pprint.pprint(matching_cats)
[('Zophie', '2021-01-24', 'black', 5.6),
('Toby', '2021-05-17', 'black', 6.8),
('Taffy', '2024-12-09', 'white', 7.0),
('Hollie', '2024-08-07', 'calico', 6.0),
('Lewis', '2024-03-19', 'orange tabby', 5.1),
('Thor', '2013-05-14', 'black', 5.2),
('Shell', '2024-06-16', 'tortoiseshell', 6.5),
('Jasmine', '2024-09-05', 'orange tabby', 6.3),
('Sassy', '2017-08-20', 'black', 7.5),
('Hope', '2016-05-22', 'black', 7.6)]
结果 matching_cats 列表中的所有猫要么是黑色的毛,要么是出生日期在 2024 年 1 月 1 日之后。请注意,出生日期只是一个字符串。虽然比较运算符如 >= 通常在字符串上执行字母顺序比较,但只要出生日期格式是 YYYY-MM-DD,它们也可以执行时间比较。
LIKE 运算符允许您匹配值的开始或结束,将百分号 (%) 作为通配符处理。例如,name LIKE "%y" 匹配所有以 'y' 结尾的名称,而 name LIKE "Ja%" 匹配所有以 'Ja' 开头的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%y"').fetchall()
[(5, 'Toby'), (11, 'Molly'), (12, 'Dusty'), (17, 'Mandy'), (18, 'Taffy'), (25, 'Rocky'), (27,
'Bobby'), (30, 'Misty'), (34, 'Mitsy'), (38, 'Colby'), (40, 'Riley'), (46, 'Ruby'), (65,
'Daisy'), (67, 'Crosby'), (72, 'Harry'), (77, 'Sassy'), (85, 'Lily'), (93, 'Spunky')]
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "Ja%"').fetchall()
[(3, 'Jacob'), (49, 'Java'), (75, 'Jasmine'), (80, 'Jamison')]
您也可以在字符串的开始和结束处放置百分号,以匹配中间的任何文本。例如,name LIKE "%ob%" 匹配所有包含 'ob' 的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%ob%"').fetchall()
[(3, 'Jacob'), (5, 'Toby'), (27, 'Bobby')]
LIKE 运算符执行不区分大小写的匹配,因此 name LIKE "%ob%" 也匹配 '%OB%'、'%Ob%' 和 '%oB%'。要进行区分大小写的匹配,请使用 GLOB 运算符和 * 作为通配符:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name GLOB "*m*"').fetchall()
[(4, 'Gumdrop'), (9, 'Thomas'), (44, 'Sam'), (63, 'Cinnamon'), (75, 'Jasmine'),
(79, 'Samantha'), (80, 'Jamison')]
当使用 name LIKE "%m%" 时,无论是小写还是大写的 m 都可以匹配,而 name GLOB "*m*" 只匹配小写的 m。
SQLite 的广泛运算符和功能与任何完整编程语言相媲美。您可以在 SQLite 文档中了解更多信息,文档地址为 www.sqlite.org/lang_expr.html。
排序结果
虽然您可以通过调用 Python 的 sort() 方法来对 fetchall() 返回的列表进行排序,但通过在 SELECT 查询中添加 ORDER BY 子句,让 SQLite 为您排序数据会更简单。例如,如果我想按毛色对猫进行排序,我可以输入以下内容:
>>> import sqlite3, pprint
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> pprint.pprint(conn.execute('SELECT * FROM cats ORDER BY fur').fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Toby', '2021-05-17', 'black', 6.8),
('Thor', '2013-05-14', 'black', 5.2),
# --snip--
('Celine', '2015-04-18', 'white', 7.3),
('Daisy', '2019-03-19', 'white', 6.0)]
如果您的查询中有一个 WHERE 子句,则 ORDER BY 子句必须跟在其后。您还可以根据多个列对行进行排序。例如,如果您想首先按毛色排序行,然后在每个毛色内按出生日期排序行,请运行以下命令:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur, birthdate')
>>> pprint.pprint(cur.fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Elton', '2020-05-28', 'bengal', 5.4),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Thor', '2013-05-14', 'black', 5.2),
('Hope', '2016-05-22', 'black', 7.6),
# --snip--
('Ginger', '2020-09-22', 'white', 5.8),
('Taffy', '2024-12-09', 'white', 7.0)]
ORDER BY 子句首先列出 fur 列,然后是 birthdate 列,列之间用逗号分隔。默认情况下,这些排序是升序的:最小的值排在前面,然后是较大的值。要按降序排序,请在列名后添加 DESC 关键字。如果您想使查询更明确和易读,也可以使用 ASC 关键字来指定升序。为了练习使用这些关键字,请在交互式外壳中输入以下内容:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur ASC, birthdate DESC')
>>> pprint.pprint(cur.fetchall())
[('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Iris', '2017-07-13', 'bengal', 6.8),
('Toby', '2021-05-17', 'black', 6.8),
('Sassy', '2017-08-20', 'black', 7.5),
# --snip--
('Mitsy', '2015-05-29', 'white', 5.0),
('Celine', '2015-04-18', 'white', 7.3)]
输出列表按毛色升序列出猫(bengal 在 white 之前)。在每个毛色内,猫按出生日期降序排序(2023-12-22 在 2020-05-28 之前)。
限制结果数量
如果你只想查看 SELECT 查询返回的前几行,你可能尝试使用 Python 列切片来限制结果。例如,使用 [:3] 切片来显示 cats 表中的前三行:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()[:3] # This is inefficient.
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码可以运行,但效率不高;它首先从表中获取所有行,然后丢弃除了前三行之外的所有内容。对于你的程序来说,直接从数据库中获取前三行会更高效。你可以使用 LIMIT 子句来实现这一点:
>>> conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码比获取所有行的代码运行得更快,特别是对于具有大量行的表。如果你的 SELECT 查询中包含 WHERE 和 ORDER BY 子句,LIMIT 子句必须跟在这些子句之后,如下面的示例所示:
>>> conn.execute('SELECT * FROM cats WHERE fur="orange" ORDER BY birthdate LIMIT 4').fetchall()
[('Mittens', '2013-07-03', 'orange', 7.4), ('Piers', '2014-07-08', 'orange', 5.2),
('Misty', '2016-07-08', 'orange', 5.2), ('Blaze', '2023-01-16', 'orange', 7.4)]
你还可以添加一些其他的子句到你的 SELECT 查询中,但这些内容超出了本章的范围。你可以在 SQLite 文档中了解更多关于它们的信息。
为更快的数据读取创建索引
在前面的章节中,我们运行了一个 SELECT 查询来根据匹配的名称查找记录。你可以通过在 name 列上创建索引来加快这个搜索过程。一个 SQL 索引 是一种组织列数据的结构。因此,使用这些列的 WHERE 子句的查询将表现得更好。缺点是索引会占用更多的存储空间,因此插入或更新数据的查询会稍微慢一些,因为 SQLite 必须同时更新数据的索引。如果你的数据库很大,而你读取数据比插入或更新数据的频率更高,创建索引可能是值得的。然而,你应该进行测试以验证索引实际上是否提高了性能。
要在 cats 表的 names 和 birthdate 列上创建索引,请运行以下 CREATE INDEX 查询:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('CREATE INDEX idx_name ON cats (name)')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('CREATE INDEX idx_birthdate ON cats (birthdate)')
<sqlite3.Cursor object at 0x0000013EC121A040>
索引需要名称,并且按照惯例,我们根据它们应用的列来命名它们,并加上 idx_ 前缀。索引名称在整个数据库中是全局的,所以如果数据库包含多个具有 birthdate 名称的表,你可能还想在索引名称中包含表名,例如 idx_cats_birthdate。要查看表中存在的所有索引,请使用 SELECT 查询检查内置的 sqlite_schema 表:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_name',), ('idx_birthdate',)]
如果你改变主意或者发现索引没有提高性能,你可以使用 DROP INDEX 查询来删除它们:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',), ('idx_name',)]
>>> conn.execute('DROP INDEX idx_name')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',)]
对于只有几千条记录的小型数据库,你可以安全地忽略索引,因为它们提供的优势微乎其微。然而,如果你发现你的数据库查询花费的时间明显增加,创建索引可能会提高它们的性能。
更新数据库中的数据
一旦你向表中插入行,你就可以使用 UPDATE 语句来更改行。例如,让我们更新记录 (1, 'Zophie', '2021-01-24', 'black', 5.6),将 sweigartcats.db 文件中的毛色从 'black' 更改为 'gray tabby':
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
>>> conn.execute('UPDATE cats SET fur = "gray tabby" WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6)]
UPDATE 语句有以下部分:
-
UPDATE关键字 -
包含要更新行的表的名称
-
SET子句,用于指定要更新的列以及要更新的值 -
WHERE子句,用于指定要更新的行
您可以一次更新多个列,通过逗号分隔它们。例如,查询 'UPDATE cats SET fur = "black", weight_kg = 6 WHERE rowid = 1' 将 fur 和 weight 列的值分别更新为 "black" 和 6。
UPDATE 查询更新 WHERE 子句为真的每一行。如果您运行了查询 'UPDATE cats SET fur = "gray tabby" WHERE name = "Zophie"',则更新将应用于所有名为 Zophie 的猫。这可能比您打算的猫要多!这就是为什么在大多数更新查询中,WHERE 子句使用 rowid 列的主键来指定要更新的单个记录。主键唯一标识一行,因此在 WHERE 子句中使用它确保您只更新您打算更新的那一行。
在更新数据时忘记 WHERE 子句是一个常见的错误。例如,如果您想进行查找和替换,将所有 'white and orange' 毛发的猫更改为 'orange and white' 毛发,您将运行以下操作:
>>> conn.execute('UPDATE cats SET fur = "orange and white" WHERE fur = "white and orange"')
如果您忘记包含 WHERE 子句,更新将应用于表中的每一行。突然之间,您所有的猫都会长出橙色和白色的毛!
为了避免这种错误,在您的 UPDATE 查询中始终包含一个 WHERE 子句,即使您打算对每一行应用更改。在这种情况下,您可以使用 WHERE 1。由于 1 是 SQLite 用于布尔 True 的值,这告诉 SQLite 对每一行应用更改。在查询末尾添加多余的 WHERE 1 可能看起来很愚蠢,但它让您避免了可能导致实际数据丢失的危险错误。
从数据库中删除数据
您可以使用 DELETE 查询从表中删除行。例如,要从 cats 表中删除 Zophie,请在 sweigartcats.db 文件上运行以下操作:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT rowid, * FROM cats WHERE rowid = 1').fetchall()
[(1, 'Zophie', '2021-01-24', 'gray tabby', 5.6)]
>>> conn.execute('DELETE FROM cats WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000020322D183C0>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[]
DELETE 语句有以下部分:
-
DELETE FROM关键字 -
包含要删除行的表的名称
-
WHERE子句,用于指定要删除的行
与 INSERT 语句一样,在 DELETE 语句中始终包含一个 WHERE 子句至关重要;否则,您将从表中删除每一行。如果您打算删除每一行,请使用 WHERE 1 以便您可以识别任何没有 WHERE 子句的 DELETE 语句作为错误。
事务回滚
你可能有时想一起运行多个查询,或者根本不运行这些查询,但你不知道你想做什么,直到你至少运行了一些查询。处理这种情况的一种方法是从一个新的事务开始,执行查询,然后要么将所有查询提交到数据库以完成事务,要么回滚它们,使数据库看起来好像没有进行任何查询。
通常,每次你在自动提交模式下连接到 SQLite 数据库并调用conn.execute()时,都会开始和完成一个新的事务。然而,你也可以运行一个BEGIN查询来开始一个新的事务;然后,你可以通过调用conn.commit()来完成事务,或者通过调用conn.rollback()撤销所有查询。
例如,让我们向cats表添加两只新的猫,然后回滚事务,这样表就不会改变:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('BEGIN')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Socks", "2022-04-04", "white", 4.2)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Fluffy", "2022-10-30", "gray", 4.5)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.rollback() # This undoes the INSERT statements.
>>> conn.execute('SELECT * FROM cats WHERE name = "Socks"').fetchall()
[]
>>> conn.execute('SELECT * FROM cats WHERE name = "Fluffy"').fetchall()
[]
新猫 Socks 和 Fluffy 没有被插入到数据库中。
另一方面,如果你想应用你运行的所有查询,调用conn.commit()将更改提交到数据库:
>>> conn.execute('BEGIN')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Socks", "2022-04-04", "white", 4.2)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Fluffy", "2022-10-30", "gray", 4.5)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.commit()
>>> conn.execute('SELECT * FROM cats WHERE name = "Socks"').fetchall()
[('Socks', '2022-04-04', 'white', 4.2)]
>>> conn.execute('SELECT * FROM cats WHERE name = "Fluffy"').fetchall()
[('Fluffy', '2022-10-30', 'gray', 4.5)]
现在数据库中记录了 Socks 和 Fluffy 这两只猫的信息。
备份数据库
我的一个朋友曾经对一个专门经营收藏体育卡的电子商务网站使用的数据库进行修改。她必须纠正几张卡片上的几个命名错误,就在她输入UPDATE cards SET name = 'Chris Clemons'时,她的猫踩到了她的键盘上,按下了回车键。由于没有WHERE子句,查询更新了网站上销售的数千张卡片的每一张。
幸运的是,她有数据库的备份,所以她可以将它恢复到之前的状态。(这特别有用,因为同样的事情以完全相同的方式再次发生,让她怀疑猫是故意这么做的。)
如果程序当前没有访问 SQLite 数据库,你可以通过简单地复制数据库文件来备份它。Python 程序可能通过调用shutil.copy('sweigartcats.db', 'backup.db')来完成,如第十一章所述。然而,如果你的软件不断读取或更新数据库的内容,那么你需要使用Connection对象的backup()方法。例如,在交互式 shell 中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> backup_conn = sqlite3.connect('backup.db', isolation_level=None)
>>> conn.backup(backup_conn)
backup()方法安全地将*sweigartcats.db*数据库的内容备份到正在运行的查询之间的*backup.db*文件中。现在你的数据已经安全备份,你的猫可以随意踩在你的键盘上。
修改和删除表
在数据库中创建一个表并插入行之后,你可能想重命名表或其列。你可能还希望向表中添加或删除列,甚至删除整个表。你可以使用ALTER TABLE查询来执行这些操作。
以下交互式 shell 示例从一个全新的sweigartcats.db数据库文件开始。运行一个ALTER TABLE RENAME查询将cats表重命名为felines:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('cats',)]
>>> conn.execute('ALTER TABLE cats RENAME TO felines') # Rename the table.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('felines',)]
要在表中重命名列,运行一个ALTER TABLE RENAME COLUMN查询。例如,让我们将fur列重命名为description:
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall()[2] # List the third column.
(2, 'fur', 'TEXT', 0, None, 0)
>>> conn.execute('ALTER TABLE felines RENAME COLUMN fur TO description')
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall()[2] # List the third column.
(2, 'description', 'TEXT', 0, None, 0)
要向表中添加新列,运行一个ALTER TABLE ADD COLUMN查询。例如,让我们向felines表添加一个名为is_loved的新列,包含一个布尔值。SQLite 使用0表示假值,1表示真值;我们将is_loved的默认值设置为1:
>>> conn.execute('ALTER TABLE felines ADD COLUMN is_loved INTEGER DEFAULT 1')
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> import pprint
>>> pprint.pprint(conn.execute('SELECT * FROM felines LIMIT 3').fetchall())
[('Zophie', '2021-01-24', 'gray tabby', 5.6, 1),
('Miguel', '2016-12-24', 'siamese', 6.2, 1),
('Jacob', '2022-02-20', 'orange and white', 5.5, 1)]
结果表明is_loved列并不需要,因为我为所有的猫都存储了1,所以我可以用ALTER TABLE DROP COLUMN查询删除该列:
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall() # List all columns.
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2, 'description', 'TEXT',
0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0), (4, 'is_loved', 'INTEGER', 0, '1', 0)]
>>> conn.execute('ALTER TABLE felines DROP COLUMN is_loved') # Delete the column.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall() # List all columns.
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2, 'description', 'TEXT',
0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0)]
删除的列中存储的任何数据也将被删除。
如果你想要删除整个表,运行一个DROP TABLE查询:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('felines',)]
>>> conn.execute('DROP TABLE felines') # Delete the entire table.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[]
尽量限制你更改表和列的频率,因为你还需要更新程序中的查询以匹配。
使用外键连接多个表
SQLite 表的结构相当严格;例如,每一行都有一个固定的列数。但现实世界的数据通常比单个表能够捕捉到的要复杂得多。在关系型数据库中,我们可以在多个表中存储复杂的数据,并且可以创建称为外键的链接。
假设我们想要存储关于我们的猫接受的疫苗接种的信息。我们无法只是向cats表添加列,因为每只猫可能只有一次疫苗接种或多次。此外,对于每次疫苗接种,我们还想列出疫苗接种日期和提供疫苗接种的医生姓名。SQL 表在存储列列表方面并不擅长。你不会想要有名为vaccination1、vaccination2、vaccination3等列,同样的原因,你也不会想要名为vaccination1和vaccination2的变量。如果你创建太多的列或变量,你的代码会变得冗长、难以阅读。如果你创建得太少,你将不得不不断更新程序以添加所需的更多列。
每当你需要向行添加不同数量的数据时,将添加的数据作为单独表中行的列表会更合理,然后让这些行引用主表中的行。在我们的sweigartcats.db数据库中,通过在交互式 shell 中输入以下内容来添加第二个vaccinations表:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('PRAGMA foreign_keys = ON')
<sqlite3.Cursor object at 0x000001E730AD03C0>
>>> conn.execute('CREATE TABLE IF NOT EXISTS vaccinations (vaccine TEXT,
date_administered TEXT, administered_by TEXT, cat_id INTEGER,
FOREIGN KEY(cat_id) REFERENCES cats(rowid)) STRICT')
<sqlite3.Cursor object at 0x000001CA42767D40>
新的vaccinations表有一个名为cat_id的列,其类型为INTEGER。这个列中的整数值与cats表中行的rowid值相匹配。我们称cat_id列为外键,因为它引用了另一个表的主键列。
在cats表中,猫 Zophie 的rowid为1。为了记录她的疫苗接种,我们在vaccinations表中插入新的行,其中cat_id值为1:
>>> conn.execute('INSERT INTO vaccinations VALUES ("rabies", "2023-06-06", "Dr. Echo", 1)')
<sqlite3.Cursor object at 0x000001CA42767D40>
>>> conn.execute('INSERT INTO vaccinations VALUES ("FeLV", "2023-06-06", "Dr. Echo", 1)')
<sqlite3.Cursor object at 0x000001CA42767D40>
>>> conn.execute('SELECT * FROM vaccinations').fetchall()
[('rabies', '2023-06-06', 'Dr. Echo', 1), ('FeLV', '2023-06-06', 'Dr. Echo', 1)]
我们可以使用其他猫的 rowid 记录疫苗接种。如果我们想为 Mango 添加疫苗接种记录,我们可以在 cats 表中找到 Mango 的 rowid,然后使用该值添加到 vaccinations 表的 cat_id 列:
>>> conn.execute('SELECT rowid, * FROM cats WHERE name = "Mango"').fetchall()
[(23, 'Mango', '2017-02-12', 'tuxedo', 6.8)]
>>> conn.execute('INSERT INTO vaccinations VALUES ("rabies", "2023-07-11", "Dr. Echo", 23)')
<sqlite3.Cursor object at 0x000001CA42767D40>
我们还可以执行一种名为 内连接 的 SELECT 查询,它返回两个表中的相关联的行。例如,将以下内容输入到交互式外壳中,以检索与 cats 表数据连接的 vaccinations 行:
>>> conn.execute('SELECT * FROM cats INNER JOIN vaccinations ON cats.rowid =
vaccinations.cat_id').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6, 'rabies', '2023-06-06', 'Dr. Echo', 1),
('Zophie', '2021-01-24', 'gray tabby', 5.6, 'FeLV', '2023-06-06', 'Dr. Echo', 1),
('Mango', '2017-02-12', 'tuxedo', 6.8, 'rabies', '2023-07-11', 'Dr. Echo', 23)]
注意,虽然您可以将 cat_id 设置为一个 INTEGER 列并用作外键,而不实际设置 FOREIGN KEY(cat_id) REFERENCES cats(rowid) 语法,但外键有几个安全功能来确保您的数据保持一致性。例如,您不能使用一个不存在的猫的 cat_id 插入或更新疫苗接种记录。SQLite 还会强制您在删除猫之前删除该猫的所有疫苗接种记录,以避免留下“孤儿”疫苗接种记录。
这些安全功能默认是禁用的。您可以通过在调用 sqlite3.connect() 之后运行 PRAGMA 查询来启用它们:
>>> conn.execute('PRAGMA foreign_keys = ON')
外键和连接有额外的功能,但它们超出了本书的范围。
内存数据库和备份
如果您的程序正在执行大量查询,您可以通过使用 内存数据库 显着提高数据库的速度。这些数据库完全存储在计算机的内存中,而不是在计算机硬盘上的文件中。这使得更改变得非常快。然而,您需要记住使用 backup() 方法将内存数据库保存到文件。如果您的程序在运行过程中崩溃,您将丢失整个内存数据库,就像您会丢失程序变量的值一样。
以下示例创建了一个内存数据库,然后将其保存到文件 test.db:
>>> import sqlite3
>>> memory_db_conn = sqlite3.connect(':memory:',
isolation_level=None) # Create an in-memory database.
>>> memory_db_conn.execute('CREATE TABLE test (name TEXT, number REAL)')
<sqlite3.Cursor object at 0x000001E730AD0340>
>>> memory_db_conn.execute('INSERT INTO test VALUES ("foo", 3.14)')
<sqlite3.Cursor object at 0x000001D9B0A07EC0>
>>> file_db_conn = sqlite3.connect('test.db', isolation_level=None)
>>> memory_db_conn.backup(file_db_conn) # Save the database to test.db.
您也可以使用 backup() 方法将 SQLite 数据库文件加载到内存中:
>>> import sqlite3
>>> file_db_conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> memory_db_conn = sqlite3.connect(':memory:', isolation_level=None)
>>> file_db_conn.backup(memory_db_conn)
>>> memory_db_conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
使用内存数据库有一些缺点。如果您的程序由于未处理的异常而崩溃,您将丢失数据库。您可以通过将代码包裹在捕获任何未处理异常的 try 语句中,然后使用 except 语句将文件保存到数据库中来减轻这种风险。第四章介绍了使用 try 和 except 语句的异常处理。
数据库复制
您可以通过在 Connection 对象上调用 iterdump() 方法来获取数据库的副本。此方法返回一个生成重新创建数据库所需的 SQLite 查询文本的迭代器。您可以在 for 循环中使用迭代器或将它们传递给 list() 函数以将它们转换为字符串列表。例如,要获取重新创建 sweigartcats.db 数据库所需的 SQLite 查询,请在交互式外壳中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> with open('sweigartcats-queries.txt', 'w', encoding='utf-8') as fileObj:
... for line in conn.iterdump():
... fileObj.write(line + '\n')
此代码创建一个 sweigartcats-queries.txt 文件,其中包含以下 SQLite 查询,可以重新创建数据库:
BEGIN TRANSACTION;
CREATE TABLE "cats" (name TEXT NOT NULL, birthdate TEXT, fur TEXT, weight_kg REAL) STRICT;
INSERT INTO "cats" VALUES('Zophie','2021-01-24','gray tabby',5.6);
INSERT INTO "cats" VALUES('Miguel','2016-12-24','siamese',6.2);
INSERT INTO "cats" VALUES('Jacob','2022-02-20','orange and white',5.5);
# --snip--
INSERT INTO "cats" VALUES('Spunky','2015-09-04','gray',5.9);
INSERT INTO "cats" VALUES('Shadow','2021-01-18','calico',6.0);
COMMIT;
这些查询的文本几乎肯定比原始数据库要大,但查询的优势在于它们是可读的,并且易于在复制和粘贴到你的 Python 代码或 SQLite 应用程序之前进行编辑,正如我们接下来将要讨论的。
SQLite Apps
有时,你可能想直接调查 SQLite 数据库,而不必编写所有这些额外的 Python 代码。你可以通过安装 sqlite3 命令来实现,该命令从终端命令行窗口运行,并在 sqlite.org/cli.html 上有文档说明。
在 Windows 上,从 sqlite.org/download.html 下载标记为“用于管理 SQLite 数据库文件的命令行工具捆绑包”的文件,并将 sqlite3.exe 程序放置在系统 PATH 上的一个文件夹中。(有关 PATH 环境变量和终端窗口的信息,请参阅第十二章。)sqlite3 命令在 macOS 上预先安装。对于 UbuntuLinux,运行 sudo apt install sqlite3 来安装它。
接下来,在终端窗口中运行 sqlite3 example.db 以连接到 example.db 数据库。如果此文件不存在,sqlite3 将创建一个包含空数据库的文件。你可以将 SQL 查询输入到这个工具中,尽管与传递给 conn.execute() 的查询不同,它们必须以分号结尾。
例如,将以下内容输入到终端窗口中:
C:\Users\Al>sqlite3 example.db
SQLite version 3.`xx.xx`
Enter ".help" for usage hints.
sqlite> CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL,
birthdate TEXT, fur TEXT, weight_kg REAL) STRICT;
sqlite> INSERT INTO cats VALUES ('Zophie', '2021-01-24', 'gray tabby', 4.7);
sqlite> SELECT * from cats;
Zophie|2021-01-24|gray tabby|4.7
如此例所示,sqlite3 命令行工具为你提供了一个 SQLite 交互式外壳,你可以在其 sqlite> 提示符处输入查询。.help 命令显示其他命令,例如 .tables(显示数据库中的表)和 .headers(允许你打开或关闭列标题):
sqlite> .tables
cats
sqlite> .headers on
sqlite> SELECT * from cats;
name|birthdate|fur|weight_kg
Zophie|2021-01-24|gray tabby|4.7
如果命令行工具对你来说太简略,还有免费的开源应用程序可以在 Windows、macOS 和 Linux 上以图形用户界面 (GUI) 显示 SQLite 数据库:
-
DB Browser for SQLite (
sqlitebrowser.org) -
SQLite Studio (
sqlitestudio.pl) -
DBeaver Community (
dbeaver.io)
虽然这些 GUI 应用程序使与 SQLite 数据库一起工作变得更容易,但学习 SQLite 查询的基于文本的语法仍然值得。
摘要
计算机使处理大量数据成为可能,但仅仅将数据放入文本文件,甚至电子表格,可能不足以很好地组织数据,以便你有效地使用它。如 SQLite 这样的 SQL 数据库提供了一种高级方法,不仅可以存储大量信息,还可以通过 SQL 语言检索你想要的确切数据。
SQLite 是一个令人印象深刻的数据库,Python 的标准库中包含sqlite3模块。SQLite 的 SQL 版本与其他关系数据库中使用的版本不同,但它们足够相似,学习 SQLite 可以为数据库的一般知识提供一个很好的介绍。
SQLite 数据库位于单个文件中,没有专用服务器。它们可以包含多个表(你可以将其视为类似于电子表格),每个表可以包含多个列。要编辑表中的值,你可以使用INSERT、SELECT、UPDATE和DELETE查询执行 CRUD 操作(创建、读取、更新和删除)。要更改表和列本身,你可以使用ALTER TABLE和DROP TABLE查询。最后,外键允许你使用称为连接的技术将多个表中的记录链接在一起。
SQLite 和数据库的内容远不止一章所能涵盖。如果你想更深入地了解 SQL 数据库,我推荐 Anthony DeBarros 的《Practical SQL》第 2 版(No Starch Press,2022 年出版)。
练习问题
-
获取名为
example.db的 SQLite 数据库的Connection对象的 Python 指令是什么? -
以下 Python 指令将创建一个名为
students的新表,包含名为first_name、last_name和favorite_color的TEXT列是什么? -
如何以自动提交模式连接到 SQLite 数据库?
-
SQLite 中
INTEGER和REAL数据类型之间的区别是什么? -
严格模式为表增加了什么?
-
查询
'SELECT * FROM cats'中的*代表什么? -
CRUD 代表什么?
-
ACID 代表什么?
-
什么查询可以向表中添加新记录?
-
什么查询可以删除表中的记录?
-
如果在
UPDATE查询中未指定WHERE子句会发生什么? -
索引是什么?以下代码将创建一个名为
birthdate的列在名为cats的表中的索引? -
外键是什么?
-
如何删除名为
cats的表? -
创建内存数据库时指定什么“文件名”?
-
如何将数据库复制到另一个数据库?
练习程序
为了练习,编写程序来完成以下任务。
猫疫苗接种检查器
从本书的资源中下载我的猫的数据库sweigartcats.db,链接为nostarch.com/automate-boring-stuff-python-3rd-edition。编写一个程序打开此数据库,并列出所有未接种疫苗的猫,疫苗名称为'rabies'、'FeLV'和'FVRCP'。此外,通过查找在猫的生日之前接种的所有疫苗来检查数据库中的错误。
餐饮成分数据库
编写一个程序,使用以下 SQL 查询创建两个表,一个用于餐食,一个用于成分:
CREATE TABLE IF NOT EXISTS meals (name TEXT) STRICT
CREATE TABLE IF NOT EXISTS ingredients (name TEXT,
meal_id INTEGER, FOREIGN KEY(meal_id) REFERENCES meals
(rowid)) STRICT
然后,编写一个程序,提示用户输入。如果用户输入'quit',程序应退出。用户还可以输入一个新的餐名,后跟一个冒号和一个以逗号分隔的配料列表:'meal:ingredient1,ingredient2'。将餐点和其配料保存到meals和ingredients表中。
最后,用户可以输入餐名或配料名。如果该名称出现在meals表中,程序应列出该餐的配料。如果该名称出现在ingredients表中,程序应列出使用该配料的每一餐。例如,程序的输出可能如下所示:
> onigiri:rice,nori,salt,sesame seeds
Meal added: onigiri
> chicken and rice:chicken,rice,cream of chicken soup
Meal added: chicken and rice
> onigiri
Ingredients of onigiri:
rice
nori
salt
sesame seeds
> chicken
Meals that use chicken:
chicken and rice
> rice
Meals that use rice:
onigiri
chicken and rice
> quit
电子表格与数据库
让我们考虑电子表格和数据库之间的相似之处和不同之处。在电子表格中,行包含单个记录,而列代表每个记录字段中存储的数据类型。例如,图 16-1 展示的是我一些猫的电子表格。列列出了每只猫的名字、生日、毛色和体重(以千克为单位)。

图 16-1:电子表格以具有固定列结构的行存储数据记录。
我们可以将相同的信息存储在数据库中。你可以将数据库表视为电子表格,数据库可以包含一个或多个表。表为每个记录(也称为行或条目)具有不同属性的字段。像 SQLite 这样的数据库被称为关系数据库,其中关系意味着数据库可以包含多个表,并且它们之间存在关系,你将在后面看到。
电子表格和数据库都会对其包含的数据进行标记。电子表格会自动用字母标记列,用数字标记行。此外,示例猫电子表格使用其第一行来给出列的描述性名称。后续的每一行代表一只猫。在 SQL 数据库中,表通常为每个记录的主键有一个 ID 列:一个可以明确识别记录的唯一整数。在 SQLite 中,这个列称为rowid,SQLite 会自动将其添加到你的表中。
删除电子表格中的行会将其下方的所有行向上移动,改变它们的行号。但数据库记录的主键 ID 是唯一的,不会改变。这在许多情况下都很有用。如果一只猫被改名或体重发生变化怎么办?如果我们想按名字字母顺序重新排列行以列出猫怎么办?每只猫都需要一个唯一的识别号,无论数据如何变化,这个号都保持不变。我们可以在电子表格中添加一个行 ID 列来模拟 SQLite 表的rowid列。即使行被删除或移动,这个 ID 值也会保持不变,如图 16-2 所示,其中行 ID 为 5 到 10 的猫被删除。

图 16-2:行 ID 号,与电子表格的行号不同,即使在删除 ID 为 5 到 10 的猫之后,也为每条记录提供了一个唯一的标识符(左图),而在删除 ID 为 5 到 10 的行之后,行号直接从 ID 值为 4 的行跳到 ID 值为 11 的行(右图)。
人们使用电子表格的另一种方式与数据库存储数据的方式完全不同。电子表格可以作为表单的模板,而不是基于行的数据存储。你可能见过类似于图 16-3 的电子表格。

图 16-3:一个格式化很多且尺寸固定的电子表格通常不适合作为数据库使用。
这些电子表格通常有很多格式化,包括背景颜色、合并单元格和不同的字体,以便于人类眼睛看起来很漂亮。虽然基于行的数据电子表格可以无限向下扩展,随着新数据的添加而扩展,但这些电子表格通常具有固定的大小和填空设计。它们通常是为了人类打印出来查看,而不是为了 Python 程序从中提取数据。
数据库在视觉上并不吸引人;它们只包含原始数据。更重要的是,虽然电子表格允许你将任何数据放入任何单元格,但数据库有一个更严格的结构,以便于软件检索数据。如果你的数据看起来像第十四章的例子和第十五章的 EZSheets 库,并且将其留在 Excel 或 Google 电子表格中。
SQLite 与其它 SQL 数据库的比较
如果你习惯于使用其他 SQL 数据库,你可能想知道 SQLite 是如何比较的。简而言之,SQLite 在简单性和功能之间找到了平衡。它是一个完整的数据库,使用 SQL 读取和写入大量数据,但它在你的 Python 程序中运行,并在单个文件上操作。你的程序导入sqlite3模块,就像导入sys、math或 Python 标准库中的任何其他模块一样。
下面是 SQLite 与其他数据库软件之间的主要区别:
-
SQLite 数据库存储在一个单独的文件中,你可以像其他任何文件一样移动、复制或备份它。
-
SQLite 可以在资源较少的计算机上运行,例如嵌入式设备或几十年前的笔记本电脑。
-
SQLite 是无服务器数据库;它不需要在笔记本电脑上持续运行的后台服务器应用程序,也不需要任何专用服务器硬件。没有涉及网络连接。
-
从用户的角度来看,SQLite 不需要任何安装或配置。它是 Python 程序的一部分。
-
为了获得更快的性能,SQLite 数据库可以完全存在于内存中,并在程序退出前保存到文件。
-
虽然 SQLite 列有数据类型,如数字和文本,就像其他 SQL 数据库一样,但 SQLite 并不严格强制执行列的数据类型。
-
SQLite 中没有权限设置或用户角色。与其他 SQL 数据库不同,SQLite 没有类似
GRANT或REVOKE的语句。 -
SQLite 是公有领域软件;你可以无限制地用于商业或任何你想要的方式。
SQLite 的主要缺点是它无法高效地处理数百或数千个同时进行的写操作(例如,来自社交媒体网络应用)。除此之外,SQLite 与任何数据库一样强大,能够可靠地处理 GB 或甚至 TB 的数据,以及同时进行的读操作,快速且容易。
SQLite 并非将其定位为其他数据库软件的竞争对手,而是作为使用 open() 函数处理文本文件(或第十八章中将要学习的 JSON、XML 和 CSV 文件)的竞争对手。如果你的程序需要存储和快速检索大量数据的能力,SQLite 相比 JSON 或电子表格文件是一个更好的替代方案。
创建数据库和表
让我们从使用 SQL 创建我们的第一个数据库和表开始。SQL 是一种小语言,你可以在 Python 中与之交互,就像正则表达式对于正则表达式一样。就像正则表达式一样,SQL 查询是以 Python 字符串值的形式编写的。就像你可以编写自己的 Python 代码来执行正则表达式执行的文字模式匹配一样,你 可以 编写自己的自定义 Python 代码来在 Python 字典和列表中搜索匹配的数据。但编写正则表达式和 SQL 数据库查询使得这些任务在长期来看变得更加简单,即使它们最初需要你学习一项新技能。让我们探索如何编写查询以在新的数据库中创建表。
我们将在名为 example.db 的文件中创建一个示例 SQLite 数据库来存储有关猫的信息。要创建数据库,首先导入 sqlite3 模块。(数字 3 代表 SQLite 的主要版本 3,这与 Python 3 无关。)SQLite 数据库位于单个文件中。文件名可以是任何名称,但按照惯例,我们给它一个 .db 文件扩展名。.sqlite 扩展名也常被使用。
数据库可以包含多个表,每个表应存储一种特定类型的数据。例如,一个表可以包含猫的记录,而另一个表可以包含对第一个表中的特定猫进行的疫苗接种记录。你可以将表视为一个元组的列表,其中每个元组都是一行。cats表本质上与[('Zophie', '2021-01-24', 'black', 5.6), ('Colin', '2016-12-24', 'siamese', 6.2), ...]相同,等等。
让我们创建一个数据库,然后为猫的数据创建一个表,向其中插入一些猫的记录,从数据库中读取数据,并关闭数据库连接。
连接到数据库
编写 SQLite 代码的第一步是通过调用sqlite3.connect()获取数据库文件的Connection对象。在交互式 shell 中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
函数的第一个参数可以是数据库文件名的字符串或pathlib.Path对象。如果此文件名不属于现有的 SQLite 数据库,则函数会创建一个包含空数据库的新文件。例如,sqlite3.connect('example.db', isolation_level=None)将当前工作目录中的名为example.db的数据库文件连接到数据库。如果此文件不存在,则函数会创建一个空文件。
如果你连接的文件存在但不是 SQLite 数据库文件,当你尝试执行查询时,Python 会抛出sqlite3.DatabaseError: file is not a database异常。“第十章的‘检查路径有效性’”解释了如何使用exists() Path方法和os.path.exists()函数,这些方法可以告诉你的程序文件是否存在。
isolation_level=None关键字参数导致数据库使用自动提交模式。这让你不必在每次调用execute()方法后都编写commit()方法调用。
sqlite3.connect()函数返回一个Connection对象,在这些示例中我们将其存储在名为conn的变量中。每个Connection对象连接到一个 SQLite 数据库文件。当然,你可以为这个Connection对象选择任何你喜欢的变量名,如果你的程序同时打开多个数据库,你应该使用更具描述性的变量名。但对于只连接一个数据库的小程序来说,名称conn易于编写且足够描述性。(名称con会更短,但容易误解为“console”、“content”或“confusing”的变量名。)
当你的程序完成数据库操作后,调用conn.close()来关闭连接。程序在终止时也会自动关闭连接。
创建表
在连接到一个新的空数据库后,使用CREATE TABLE SQL 查询创建一个表。要运行 SQL 查询,你必须调用Connection对象的execute()方法。将查询字符串传递给conn.execute()方法:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL,
birthdate TEXT, fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
按照惯例,SQL 关键字,如 CREATE 和 TABLE,使用大写字母书写。然而,SQLite 并不强制执行这一点;查询 'create table if not exists cats (name text not null, birthdate text, fur text, weight_kg real) strict' 运行得很好。表和列名也不区分大小写,但惯例是将它们写成小写,并且用下划线分隔多个单词,如 weight_kg。
如果你尝试创建一个已经存在的表(没有 IF NOT EXISTS 部分),CREATE TABLE 语句将引发 sqlite3.OperationalError: table cats already exists 异常。包含这一部分是避免这种异常的快速方法,你几乎总是希望将其添加到你的 CREATE TABLE 查询中。
在我们的示例中,我们在 CREATE TABLE IF NOT EXISTS 关键字后面跟随着表名 cats。在表名之后是一组括号,包含列名和数据类型。
定义数据类型
SQLite 有六种数据类型:
NULL 类似于 Python 的 None
INT 或 INTEGER 类似于 Python 的 int 类型
REAL 指的是数学术语 实数;类似于 Python 的 float 类型
TEXT 类似于 Python 的 str 类型
BLOB 简称 Binary Large Object;类似于 Python 的 bytes 类型,并且适用于在数据库中存储整个文件
SQLite 有自己的数据类型,因为它不仅仅是为 Python 构建的;其他编程语言也可以与 SQLite 数据库交互。
与其他 SQL 数据库软件不同,SQLite 对其列的数据类型并不严格。这意味着 SQLite 默认会乐意将字符串 'Hello' 存储在 INTEGER 列中而不会引发异常。但 SQLite 的数据类型也不完全是装饰性的;如果可能,SQLite 会自动 转换(即更改)数据到列的数据类型,这被称为 类型亲和力。例如,如果你将字符串 '42' 添加到 INTEGER 列中,SQLite 会自动将值存储为整数 42,因为该列对整数有类型亲和力。然而,如果你将字符串 'Hello' 添加到 INTEGER 列中,SQLite 将存储 'Hello'(不会出错),因为尽管有整数类型亲和力,但 'Hello' 无法转换为整数。
STRICT 关键字为该表启用 严格模式。在严格模式下,每个列都必须指定数据类型,如果你尝试将错误类型的数据插入到表中,SQLite 将引发一个 sqlite3.IntegrityError 异常。尽管如此,SQLite 仍会自动将数据转换为列的数据类型;将 '42' 插入到 INTEGER 列中会插入整数 42。然而,字符串 'Hello' 无法转换为整数,因此尝试插入它将引发异常。我强烈建议使用严格模式;它可以提前警告你由于将错误数据插入表中而导致的错误。
SQLite 在版本 3.37.0 中添加了 STRICT 关键字,该关键字由 Python 3.11 及更高版本使用。早期版本不了解严格模式,如果在尝试使用它时将报告语法错误。您可以通过检查 sqlite3.sqlite_version 变量来始终检查 Python 正在使用的 SQLite 版本,该变量看起来可能如下所示:
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.`xx`.`xx`'
SQLite 没有布尔数据类型,因此使用 INTEGER 来表示布尔数据;您可以使用 1 来表示 True,使用 0 来表示 False。SQLite 也没有日期、时间或日期时间数据类型。相反,您可以使用 TEXT 数据类型来存储一个字符串,该字符串的格式在表 16-1 中列出。
表 16-1:SQLite 中日期、时间和日期时间的推荐格式
| 格式 | 示例 |
|---|---|
YYYY-MM-DD |
'2035-10-31' |
YYYY-MM-DD HH:MM:SS |
'2035-10-31 16:30:00' |
YYYY-MM-DD HH:MM:SS.SSS |
'2035-10-31 16:30:00.407' |
HH:MM:SS |
'16:30:00' |
HH:MM:SS.SSS |
'16:30:00.407' |
name TEXT NOT NULL 中的 NOT NULL 部分指定 Python None 值不能存储在 name 列中。这是一种使表列强制性的好方法。
SQLite 表自动创建一个包含唯一主键整数的 rowid 列。即使您的 cats 表中有两只猫的名字、生日、毛色和体重巧合地相同,rowid 也能让您区分它们。
列出表和列
所有 SQLite 数据库都有一个名为 sqlite_schema 的表,其中列出了有关数据库的元数据,包括其所有表。要列出 SQLite 数据库中的表,请运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('cats',)]
输出显示了我们刚刚创建的 cats 表。(我在第 394 页的“从数据库中读取数据”中解释了 SELECT 语句的语法。)要获取 cats 表中列的信息,请运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('PRAGMA TABLE_INFO(cats)').fetchall()
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2,
'fur', 'TEXT', 0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0)]
此查询返回一个元组列表,每个元组描述了表的一列。例如,(1, 'birthdate', 'TEXT', 0, None, 0) 元组提供了关于 birthdate 列的以下信息:
列位置 1 表示该列在表中是第二列。列号从零开始,类似于 Python 列表索引,因此第一列位于位置 0。
名称 'birthdate' 是该列的名称。请记住,SQLite 列和表名不区分大小写。
数据类型 'TEXT' 是 birthdate 列的 SQLite 数据类型。
列是否 NOT NULL 0 表示 False,即该列不是 NOT NULL(即,您可以在该列中放置 None 值)。
默认值 如果没有指定其他值,则插入 None 作为默认值。
列是否是主键 0 表示 False,意味着此列不是主键列。
注意,sqlite_schema 表本身并未列为表。您永远不需要自己修改 sqlite_schema 表,这样做可能会损坏数据库,使其不可读。
连接到数据库
编写 SQLite 代码的第一步是通过调用sqlite3.connect()获取数据库文件的一个Connection对象。在交互式 shell 中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
函数的第一个参数可以是数据库文件的字符串或pathlib.Path对象。如果此文件名不属于现有的 SQLite 数据库,则函数会创建一个包含空数据库的新文件。例如,sqlite3.connect('example.db', isolation_level=None)将当前工作目录中的名为example.db的数据库文件连接起来。如果此文件不存在,则函数会创建一个空文件。
如果你连接的文件存在但不是 SQLite 数据库文件,一旦尝试执行查询,Python 会引发一个sqlite3.DatabaseError: file is not a database异常。“检查路径有效性”在第十章中解释了如何使用exists() Path 方法以及os.path.exists()函数,这些可以告诉你的程序文件是否存在。
isolation_level=None关键字参数会导致数据库使用自动提交模式。这让你不必在每次调用execute()方法后都编写commit()方法调用。
sqlite3.connect()函数返回一个Connection对象,在这些示例中我们将其存储在名为conn的变量中。每个Connection对象连接到一个 SQLite 数据库文件。当然,你可以为这个Connection对象选择任何你喜欢的变量名,如果你的程序同时打开多个数据库,你应该使用更具描述性的变量名。但对于只连接一个数据库的小程序来说,名称conn易于书写且足够描述性。(名称con会更短,但容易误解为“console”、“content”或“confusing”的变量名。)
当你的程序完成数据库操作后,调用conn.close()来关闭连接。程序在终止时也会自动关闭连接。
创建表
连接到一个新、空的数据库后,使用CREATE TABLE SQL 查询创建一个表。要运行 SQL 查询,必须调用Connection对象的execute()方法。将查询字符串传递给conn.execute()方法:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL,
birthdate TEXT, fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
按照惯例,SQL 关键字,如CREATE和TABLE,使用大写字母书写。然而,SQLite 并不强制执行这一点;查询'create table if not exists cats (name text not null, birthdate text, fur text, weight_kg real) strict'运行得很好。表和列名也不区分大小写,但惯例是将它们写成小写,并用下划线分隔多个单词,如weight_kg。
如果尝试创建一个已经存在的表(没有使用IF NOT EXISTS部分),CREATE TABLE语句会引发一个sqlite3.OperationalError: table cats already exists异常。包含这一部分是一个快速避免触发此异常的方法,你几乎总是希望将其添加到你的CREATE TABLE查询中。
在我们的例子中,我们使用 CREATE TABLE IF NOT EXISTS 关键字后跟表名 cats。在表名之后是一组括号,包含列名和数据类型。
定义数据类型
SQLite 有六种数据类型:
NULL - 类似于 Python 的 None
INT 或 INTEGER - 类似于 Python 的 int 类型
REAL - 指的是数学术语 实数;类似于 Python 的 float 类型
TEXT - 类似于 Python 的 str 类型
BLOB - 简称 Binary Large Object;类似于 Python 的 bytes 类型,并且可以用于在数据库中存储整个文件。
SQLite 有自己的数据类型,因为它不仅仅是为 Python 构建的;其他编程语言也可以与 SQLite 数据库交互。
与其他 SQL 数据库软件不同,SQLite 对其列的数据类型并不严格。这意味着 SQLite 默认会乐意将字符串 'Hello' 存储在 INTEGER 列中而不会引发异常。但 SQLite 的数据类型也不是完全装饰性的;如果可能,SQLite 会自动 转换(即更改)数据到列的数据类型,这被称为 类型亲和力。例如,如果你将字符串 '42' 添加到 INTEGER 列中,SQLite 会自动将值存储为整数 42,因为该列对整数有类型亲和力。然而,如果你将字符串 'Hello' 添加到 INTEGER 列中,SQLite 将存储 'Hello'(不会出错),因为尽管有整数类型亲和力,但 'Hello' 不能转换为整数。
STRICT 关键字为该表启用 严格模式。在严格模式下,每个列都必须指定一个数据类型,如果你尝试将错误类型的数据插入到表中,SQLite 将引发一个 sqlite3.IntegrityError 异常。SQLite 仍然会自动将数据转换为列的数据类型;将 '42' 插入到 INTEGER 列中会插入整数 42。然而,字符串 'Hello' 不能转换为整数,因此尝试插入它将引发异常。我强烈建议使用严格模式;它可以给你一个早期警告,关于由于将错误数据插入到你的表中而引起的错误。
SQLite 在版本 3.37.0 中添加了 STRICT 关键字,该版本由 Python 3.11 及以后的版本使用。早期版本不了解严格模式,如果你尝试使用它,将会报告语法错误。你可以通过检查 sqlite3.sqlite_version 变量来始终检查 Python 正在使用的 SQLite 版本,它看起来可能如下所示:
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.`xx`.`xx`'
SQLite 没有布尔数据类型,因此可以使用 INTEGER 来表示布尔数据;你可以存储一个 1 来表示 True,一个 0 来表示 False。SQLite 也没有日期、时间或日期时间数据类型。相反,你可以使用 TEXT 数据类型来存储一个格式在表 16-1 中列出的字符串。
表 16-1:SQLite 中日期、时间和日期时间的推荐格式
| 格式 | 示例 |
|---|---|
YYYY-MM-DD |
'2035-10-31' |
YYYY-MM-DD HH:MM:SS |
'2035-10-31 16:30:00' |
YYYY-MM-DD HH:MM:SS.SSS |
'2035-10-31 16:30:00.407' |
HH:MM:SS |
'16:30:00' |
HH:MM:SS.SSS |
'16:30:00.407' |
name TEXT NOT NULL 中的 NOT NULL 部分指定 Python 的 None 值不能存储在 name 列中。这是一种使表列强制性的好方法。
SQLite 表自动创建一个包含唯一主键整数的 rowid 列。即使你的 cats 表中有两只猫的名字、生日、毛色和体重巧合地相同,rowid 也能让你区分它们。
列出表和列
所有 SQLite 数据库都有一个名为 sqlite_schema 的表,其中列出了数据库的元数据,包括其所有表。要列出 SQLite 数据库中的表,请运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('cats',)]
输出显示了刚刚创建的 cats 表。(我在第 394 页的“从数据库中读取数据”中解释了 SELECT 语句的语法。)要获取 cats 表中列的信息,请运行以下查询:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('PRAGMA TABLE_INFO(cats)').fetchall()
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2,
'fur', 'TEXT', 0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0)]
此查询返回一个元组列表,每个元组描述了表的一个列。例如,(1, 'birthdate', 'TEXT', 0, None, 0) 这个元组提供了关于 birthdate 列的以下信息:
列位置 中的 1 表示该列是表中的第二个。列号从 0 开始,就像 Python 列表索引一样,所以第一列位于位置 0。
名称 ‘出生日期’ 是该列的名称。请记住,SQLite 的列和表名不区分大小写。
数据类型 'TEXT' 是 birthdate 列的 SQLite 数据类型。
列是否为 NOT NULL 中的 0 表示 False,意味着该列不是 NOT NULL(即,你可以在这个列中放置 None 值)。
默认值 None 是如果没有指定其他值时插入的默认值。
列是否是主键 中的 0 表示 False,意味着这个列不是主键列。
注意,sqlite_schema 表本身并未列出为表。你永远不会需要自己修改 sqlite_schema 表,这样做可能会损坏数据库,使其不可读。
CRUD 数据库操作
CRUD 是数据库执行的四项基本操作的缩写:创建数据、读取数据、更新数据和删除数据。在 SQLite 中,我们分别使用 INSERT、SELECT、UPDATE 和 DELETE 语句执行这些操作。以下是每个语句的示例,我们稍后将作为字符串传递给 conn.execute():
-
INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6) -
SELECT rowid, * FROM cats ORDER BY fur -
UPDATE cats SET fur="gray tabby" WHERE rowid=1 -
DELETE FROM cats WHERE rowid=1
大多数应用程序和社交媒体网站实际上只是 CRUD 数据库的复杂用户界面。当你发布照片或回复时,你实际上是在某个数据库中创建记录。当你滚动社交媒体时间线时,你正在从数据库中读取记录。当你编辑或删除帖子时,你正在执行更新或删除操作。无论你正在学习新的应用程序、编程语言还是查询语言,都要使用 CRUD 缩写来提醒自己应该了解哪些基本操作。
将数据插入数据库
现在我们已经创建了数据库和cats表,让我们插入我的宠物猫的记录。我家大约有 300 只猫,使用 SQLite 数据库帮助我跟踪它们。一个INSERT语句可以向表中添加新记录。将以下代码输入到交互式 shell 中:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL, birthdate TEXT,
fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
>>> conn.execute('INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6)')
<sqlite3.Cursor object at 0x00000162842E78C0>
这个INSERT查询向cats表添加了一行。括号内是该列的逗号分隔的值。对于INSERT查询,括号是必需的。注意,当插入TEXT值时,我使用了双引号("),因为我已经在查询字符串中使用了单引号(')。sqlite3模块使用单引号或双引号作为其TEXT值。
事务
一个INSERT语句开始一个事务,这是数据库中的一个工作单元。事务必须通过ACID 测试,这是一个数据库概念,意味着事务是:
原子性 事务要么完全执行,要么完全不执行。
一致 事务不会违反约束,例如列的NOT NULL规则。
隔离性 一个事务不会影响其他事务。
持久 如果提交,事务结果将被写入持久存储,例如硬盘。
SQLite 是一个符合 ACID 的数据库;它甚至通过了模拟在事务过程中计算机断电的测试,因此你有很高的保证,数据库文件不会被留下一个损坏的、不可用的状态。SQLite 查询要么完全将数据插入数据库,要么根本不插入。
SQL 注入攻击
一种称为SQL 注入攻击的黑客技术可以改变你的查询以执行你未打算做的事情。这些技术超出了本书的范围,如果你的程序不接受来自互联网陌生人的数据,那么这很可能不是你的代码问题。但为了防止这种情况,在插入或更新数据库中的数据时,每次引用变量时都使用?问号语法。
例如,如果我想根据存储在变量中的数据插入一个新的猫记录,我不应该直接使用 Python 将这些变量插入到查询字符串中,如下所示:
>>> cat_name = 'Zophie'
>>> cat_bday = '2021-01-24'
>>> fur_color = 'black'
>>> cat_weight = 5.6
>>> conn.execute(f'INSERT INTO cats VALUES ("{cat_name}", "{cat_bday}",
"{fur_color}", {cat_weight})')
<sqlite3.Cursor object at 0x0000022B91BB7C40>
如果这些变量的值来自用户输入,如网络应用程序表单,黑客可能可以指定会改变查询意义的字符串。相反,我应该在使用 ? 的查询字符串中,然后传递变量列表作为查询字符串之后的参数:
>>> conn.execute('INSERT INTO cats VALUES (?, ?, ?, ?)', [cat_name, cat_bday,
fur_color, cat_weight])
<sqlite3.Cursor object at 0x0000022B91BB7C40>
execute() 方法在确保变量值不会导致 SQL 注入攻击后,将查询字符串中的 ? 占位符替换为变量值。虽然此类攻击不太可能适用于你的代码,但使用 ? 占位符而不是自己格式化查询字符串是一个好习惯。
从数据库中读取数据
一旦数据库中有数据,你可以使用 SELECT 查询来读取它。将以下内容输入到交互式 shell 中以从 example.db 数据库读取数据:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
SELECT 查询的 execute() 方法调用返回一个 Cursor 对象。为了获取实际数据,我们在该 Cursor 对象上调用 fetchall() 方法。每条记录作为元组列表中的元组返回。每个元组中的数据按表列的顺序出现。
而不是编写 Python 代码来自己遍历这个元组列表,你可以让 SQLite 提取你想要的具体信息。示例 SELECT 查询有四个部分:
-
SELECT关键字 -
你想要检索的列,其中
*表示“除了rowid之外的所有列” -
FROM关键字 -
要检索数据的表;在这种情况下,是
cats表
如果你只想获取 cats 表中的 rowid 和 name 列的记录,你的查询将如下所示:
>>> conn.execute('SELECT rowid, name FROM cats').fetchall()
[(1, 'Zophie')]
你也可以使用 SQL 来过滤查询结果,你将在下一节中学习。
遍历查询结果
fetchall() 方法返回你的 SELECT 查询结果,以元组列表的形式。一个常见的编码模式是使用这个数据在一个 for 循环中为每个元组执行一些操作。例如,从 nostarch.com/automate-boring-stuff-python-3rd-edition 下载 sweigartcats.db 文件,然后进入交互式 shell 处理其数据:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> for row in conn.execute('SELECT * FROM cats'):
... print('Row data:', row)
... print(row[0], 'is one of my favorite cats.')
...
Row data: ('Zophie', '2021-01-24', 'gray tabby', 5.6)
Zophie is one of my favorite cats.
Row data: ('Miguel', '2016-12-24', 'siamese', 6.2)
Miguel is one of my favorite cats.
Row data: ('Jacob', '2022-02-20', 'orange and white', 5.5)
Jacob is one of my favorite cats.
# --snip--
for 循环可以在不调用 fetchall() 的情况下遍历 conn.execute() 返回的行数据元组,并且循环体内的代码可以单独操作每一行,因为 row 变量填充了查询的行数据元组。然后代码可以使用元组的整数索引来访问列:索引 0 为名称,索引 1 为出生日期,依此类推。
过滤检索数据
我们的 SELECT 查询已经检索了表中的每一行,但我们可能只想获取符合某些筛选条件的行。使用 sweigartcats.db 文件,在 SELECT 语句中添加一个 WHERE 子句以提供搜索参数,例如具有黑色毛发:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE fur = "black"').fetchall()
[❶('Zophie', '2021-01-24', 'black', 5.6), ('Toby', '2021-05-17', 'black',
6.8), ('Thor', '2013-05-14', 'black', 5.2), ('Sassy', '2017-08-20', 'black',
7.5), ('Hope', '2016-05-22', 'black', 7.6)]
在这个例子中,WHERE 子句 WHERE fur = "black" 将仅检索 fur 列中包含 "black" 的记录。
SQLite 为 WHERE 子句定义了自己的运算符,但它们与 Python 的运算符类似:=, !=, <, >, <=, >=, AND, OR, 和 NOT。请注意,SQLite 使用 **= 运算符表示“等于”,而 Python 使用 == 运算符来达到这个目的。在运算符的两侧,你可以放置列名或字面值。
比较将在表中的每一行发生。例如,对于 WHERE fur = "black",SQLite 执行以下比较:
-
因为
fur是'black',而'black'='black'是 true,所以 SQLite 将该行包含在结果中。 -
对于行
(2, 'Miguel', '2016-12-24', 'siamese', 6.2),fur是'siamese',而'siamese'='black'是 false,所以它不包括在结果中。 -
对于行
(3, 'Jacob', '2022-02-20', 'orange and white', 5.5),fur是'orange and white',而'orange and white'='black'是 false,所以它不包括在结果中。
... 以此类推,对于 cats 表中的每一行。
让我们用一个更复杂的 WHERE 子句继续前面的例子:WHERE fur = "black" OR birthdate >= "2024-01-01"。我们还可以使用 pprint.pprint() 函数来“美化打印”返回的列表:
>>> import pprint
>>> matching_cats = conn.execute('SELECT * FROM cats WHERE fur = "black"
OR birthdate >= "2024-01-01"').fetchall()
>>> pprint.pprint(matching_cats)
[('Zophie', '2021-01-24', 'black', 5.6),
('Toby', '2021-05-17', 'black', 6.8),
('Taffy', '2024-12-09', 'white', 7.0),
('Hollie', '2024-08-07', 'calico', 6.0),
('Lewis', '2024-03-19', 'orange tabby', 5.1),
('Thor', '2013-05-14', 'black', 5.2),
('Shell', '2024-06-16', 'tortoiseshell', 6.5),
('Jasmine', '2024-09-05', 'orange tabby', 6.3),
('Sassy', '2017-08-20', 'black', 7.5),
('Hope', '2016-05-22', 'black', 7.6)]
结果 matching_cats 列表中的所有猫要么有黑色毛发,要么出生日期在 2024 年 1 月 1 日之后。请注意,出生日期只是一个字符串。虽然比较运算符如 >= 通常在字符串上执行字母顺序比较,但只要出生日期格式是 YYYY-MM-DD,它们也可以执行时间比较。
LIKE 运算符允许你匹配值的开始或结束,将百分号 (%) 作为通配符处理。例如,name LIKE "%y" 匹配所有以 'y' 结尾的名字,而 name LIKE "Ja%" 匹配所有以 'Ja' 开头的名字:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%y"').fetchall()
[(5, 'Toby'), (11, 'Molly'), (12, 'Dusty'), (17, 'Mandy'), (18, 'Taffy'), (25, 'Rocky'), (27,
'Bobby'), (30, 'Misty'), (34, 'Mitsy'), (38, 'Colby'), (40, 'Riley'), (46, 'Ruby'), (65,
'Daisy'), (67, 'Crosby'), (72, 'Harry'), (77, 'Sassy'), (85, 'Lily'), (93, 'Spunky')]
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "Ja%"').fetchall()
[(3, 'Jacob'), (49, 'Java'), (75, 'Jasmine'), (80, 'Jamison')]
你也可以在字符串的开始和结束处放置百分号来匹配中间的任何文本。例如,name LIKE "%ob%" 匹配所有包含 'ob' 的名字:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%ob%"').fetchall()
[(3, 'Jacob'), (5, 'Toby'), (27, 'Bobby')]
LIKE 运算符执行不区分大小写的匹配,所以 name LIKE "%ob%" 也匹配 '%OB%'、'%Ob%' 和 '%oB%'。要执行区分大小写的匹配,请使用 GLOB 运算符和 * 作为通配符字符:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name GLOB "*m*"').fetchall()
[(4, 'Gumdrop'), (9, 'Thomas'), (44, 'Sam'), (63, 'Cinnamon'), (75, 'Jasmine'),
(79, 'Samantha'), (80, 'Jamison')]
虽然 name LIKE "%m%" 匹配小写或大写的 m,但 name GLOB "*m*" 只匹配小写的 m。
SQLite 的广泛运算符和功能与任何完整编程语言相媲美。你可以在 SQLite 文档中了解更多信息,请参阅 www.sqlite.org/lang_expr.html。
排序结果
虽然你可以通过调用 Python 的 sort() 方法来对 fetchall() 返回的列表进行排序,但通过在 SELECT 查询中添加 ORDER BY 子句,让 SQLite 为你排序数据会更简单。例如,如果我想按毛色对猫进行排序,我可以输入以下内容:
>>> import sqlite3, pprint
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> pprint.pprint(conn.execute('SELECT * FROM cats ORDER BY fur').fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Toby', '2021-05-17', 'black', 6.8),
('Thor', '2013-05-14', 'black', 5.2),
# --snip--
('Celine', '2015-04-18', 'white', 7.3),
('Daisy', '2019-03-19', 'white', 6.0)]
如果你的查询中有 WHERE 子句,那么 ORDER BY 子句必须跟在其后。你也可以根据多个列来排序行。例如,如果你想首先按毛色排序行,然后在每个毛色内按出生日期排序行,请运行以下命令:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur, birthdate')
>>> pprint.pprint(cur.fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Elton', '2020-05-28', 'bengal', 5.4),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Thor', '2013-05-14', 'black', 5.2),
('Hope', '2016-05-22', 'black', 7.6),
# --snip--
('Ginger', '2020-09-22', 'white', 5.8),
('Taffy', '2024-12-09', 'white', 7.0)]
ORDER BY 子句首先列出 fur 列,然后是 birthdate 列,两者之间用逗号分隔。默认情况下,这些排序是升序的:最小的值排在前面,然后是更大的值。要按降序排序,请在列名后添加 DESC 关键字。你也可以使用 ASC 关键字来指定升序,如果你想让你的查询更明确和易读。为了练习使用这些关键字,请在交互式外壳中输入以下内容:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur ASC, birthdate DESC')
>>> pprint.pprint(cur.fetchall())
[('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Iris', '2017-07-13', 'bengal', 6.8),
('Toby', '2021-05-17', 'black', 6.8),
('Sassy', '2017-08-20', 'black', 7.5),
# --snip--
('Mitsy', '2015-05-29', 'white', 5.0),
('Celine', '2015-04-18', 'white', 7.3)]
输出列表按毛色升序列出猫('bengal' 在 'white' 之前)。在每个毛色内,猫按出生日期降序排序('2023-12-22' 在 '2020-05-28' 之前)。
限制结果数量
如果你只想查看 SELECT 查询返回的前几行,你可能尝试使用 Python 列切片来限制结果。例如,使用 [:3] 切片来显示 cats 表中的前三行:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()[:3] # This is inefficient.
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码可以工作,但效率不高;它首先从表中检索所有行,然后丢弃除了前三行之外的所有内容。对于你的程序来说,直接从数据库中检索前三行会更快。你可以使用 LIMIT 子句来实现这一点:
>>> conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
与检索所有行的代码相比,这段代码运行得更快,特别是对于行数较多的表。如果你的 SELECT 查询中包含 WHERE 和 ORDER BY 子句,那么 LIMIT 子句必须跟在它们后面,如下例所示:
>>> conn.execute('SELECT * FROM cats WHERE fur="orange" ORDER BY birthdate LIMIT 4').fetchall()
[('Mittens', '2013-07-03', 'orange', 7.4), ('Piers', '2014-07-08', 'orange', 5.2),
('Misty', '2016-07-08', 'orange', 5.2), ('Blaze', '2023-01-16', 'orange', 7.4)]
你还可以在 SELECT 查询中添加一些其他子句,但它们超出了本章的范围。你可以在 SQLite 文档中了解更多关于它们的信息。
为更快的数据读取创建索引
在前面的部分中,我们运行了一个 SELECT 查询来根据匹配的名称查找记录。你可以通过在 name 列上创建索引来加快这个搜索。SQL 索引 是一种组织列数据的结构。因此,使用这些列的 WHERE 子句的查询将表现得更好。缺点是索引会占用一点额外的存储空间,因此插入或更新数据的查询会稍微慢一些,因为 SQLite 必须同时更新数据的索引。如果你的数据库很大,并且你比插入或更新数据更频繁地从中读取数据,创建索引可能是值得的。然而,你应该进行测试以验证索引实际上是否提高了性能。
要在 cats 表的 names 和 birthdate 列上创建索引,请运行以下 CREATE INDEX 查询:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('CREATE INDEX idx_name ON cats (name)')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('CREATE INDEX idx_birthdate ON cats (birthdate)')
<sqlite3.Cursor object at 0x0000013EC121A040>
索引需要名称,并且按照惯例,我们根据它们应用的列来命名它们,并加上idx_前缀。索引名称在整个数据库中是全局的,所以如果数据库包含多个名为birthdate的列的表,你可能还想在索引名称中包含表名,例如idx_cats_birthdate。要查看表中存在的所有索引,请使用SELECT查询检查内置的sqlite_schema表:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_name',), ('idx_birthdate',)]
如果你改变主意或者发现索引没有提高性能,你可以使用DROP INDEX查询来删除它们:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',), ('idx_name',)]
>>> conn.execute('DROP INDEX idx_name')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',)]
对于只有几千条记录的小型数据库,你可以安全地忽略索引,因为它们提供的益处微乎其微。然而,如果你发现数据库查询需要明显的时间,创建索引可能会提高它们的性能。
在数据库中更新数据
一旦你向表中插入行,你可以使用UPDATE语句来更改行。例如,让我们更新记录(1, 'Zophie', '2021-01-24', 'black', 5.6),将sweigartcats.db文件中的毛色从'black'更改为'gray tabby':
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
>>> conn.execute('UPDATE cats SET fur = "gray tabby" WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6)]
UPDATE语句有以下部分:
-
UPDATE关键字 -
要更新的行的表名
-
SET子句,它指定了要更新的列以及更新到的值 -
WHERE子句,它指定了要更新的行
你可以一次更新多个列,通过逗号分隔它们。例如,查询'UPDATE cats SET fur = "black", weight_kg = 6 WHERE rowid = 1'将fur和weight列的值分别更新为"black"和6。
UPDATE查询更新所有满足WHERE子句的行。如果你运行了查询'UPDATE cats SET fur = "gray tabby" WHERE name = "Zophie"',更新将适用于所有名为 Zophie 的猫。这可能会比你预期的更多猫!这就是为什么在大多数更新查询中,WHERE子句使用rowid列的主键来指定要更新的单个记录。主键唯一标识一行,所以在WHERE子句中使用它确保你只更新你打算更新的那一行。
在更新数据时,忘记WHERE子句是一个常见的错误。例如,如果你想进行查找和替换,将所有毛色为'white and orange'的猫更改为'orange and white'毛色,你会运行以下查询:
>>> conn.execute('UPDATE cats SET fur = "orange and white" WHERE fur = "white and orange"')
如果你忘记包含WHERE子句,更新将应用于表中的每一行,并且你的所有猫都会突然长出橙色和白色的毛!
为了避免这种错误,在UPDATE查询中始终包含一个WHERE子句,即使你打算对每一行应用更改。在这种情况下,你可以使用WHERE 1。由于1是 SQLite 用于布尔True的值,这告诉 SQLite 将对每一行应用更改。在查询末尾添加多余的WHERE 1可能看起来很愚蠢,但它让你避免了可能导致实际数据丢失的危险错误。
从数据库中删除数据
你可以使用DELETE查询从表中删除行。例如,要从cats表中删除 Zophie,请在sweigartcats.db文件上运行以下命令:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT rowid, * FROM cats WHERE rowid = 1').fetchall()
[(1, 'Zophie', '2021-01-24', 'gray tabby', 5.6)]
>>> conn.execute('DELETE FROM cats WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000020322D183C0>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[]
DELETE语句有以下部分:
-
DELETE FROM关键字 -
要删除的行的表名
-
WHERE子句,用于指定要删除的行
与INSERT语句一样,在DELETE语句中始终包含一个WHERE子句至关重要;否则,你将删除表中的每一行。如果你打算删除每一行,请使用WHERE 1,这样你就可以识别出没有WHERE子句的任何DELETE语句作为错误。
将数据插入数据库
现在我们已经创建了数据库和cats表,让我们插入我的宠物猫的记录。我家里大约有 300 只猫,使用 SQLite 数据库帮助我跟踪它们。INSERT语句可以向表中添加新记录。将以下代码输入到交互式 shell 中:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL, birthdate TEXT,
fur TEXT, weight_kg REAL) STRICT')
<sqlite3.Cursor object at 0x00000201B2399540>
>>> conn.execute('INSERT INTO cats VALUES ("Zophie", "2021-01-24", "black", 5.6)')
<sqlite3.Cursor object at 0x00000162842E78C0>
这个INSERT查询向cats表添加了一行新数据。括号内是其列的逗号分隔值。对于INSERT查询,括号是必需的。注意,当插入TEXT值时,我使用了双引号("),因为我已经在查询字符串中使用了单引号(')。sqlite3模块使用单引号或双引号作为其TEXT`值。
事务
INSERT语句开始一个事务,它是数据库中的一个工作单元。事务必须通过ACID 测试,这是一个数据库概念,意味着事务是:
原子性 事务要么完全执行,要么完全不执行。
一致性 事务不会违反约束,例如列的NOT NULL规则。
隔离 一个事务不会影响其他事务。
持久性 如果提交,事务结果将写入持久存储,如硬盘。
SQLite 是一个符合 ACID 标准的数据库;它甚至通过了模拟在事务过程中计算机断电的测试,因此你有很高的保证,数据库文件不会被留下处于损坏、不可用的状态。SQLite 查询要么完全将数据插入数据库,要么根本不插入。
SQL 注入攻击
一类被称为SQL 注入攻击的黑客技术可以改变你的查询以执行你未打算做的事情。这些技术超出了本书的范围,如果你的程序不接受来自互联网陌生人的数据,那么这很可能不是你代码的问题。但为了防止这种情况,在插入或更新数据库中的数据时,每次引用变量时都使用?问号语法。
例如,如果我想根据存储在变量中的数据插入一个新的猫记录,我不应该直接使用 Python 将这些变量插入到查询字符串中,如下所示:
>>> cat_name = 'Zophie'
>>> cat_bday = '2021-01-24'
>>> fur_color = 'black'
>>> cat_weight = 5.6
>>> conn.execute(f'INSERT INTO cats VALUES ("{cat_name}", "{cat_bday}",
"{fur_color}", {cat_weight})')
<sqlite3.Cursor object at 0x0000022B91BB7C40>
如果这些变量的值来自用户输入,例如 Web 应用程序表单,黑客可能可以指定会改变查询意义的字符串。相反,我应该在使用查询字符串时使用?,然后传递一个列表参数,该参数跟在查询字符串后面:
>>> conn.execute('INSERT INTO cats VALUES (?, ?, ?, ?)', [cat_name, cat_bday,
fur_color, cat_weight])
<sqlite3.Cursor object at 0x0000022B91BB7C40>
execute()方法在确保变量值不会导致 SQL 注入攻击后,将查询字符串中的?占位符替换为变量值。虽然此类攻击不太可能适用于你的代码,但使用?占位符而不是自己格式化查询字符串是一个好习惯。
事务
一个INSERT语句开始一个事务,这是数据库中的一个工作单元。事务必须通过ACID 测试,这是一个数据库概念,意味着事务是:
原子性:事务要么完全执行,要么完全不执行。
一致性:事务不会违反约束,例如列的NOT NULL规则。
隔离:一个事务不会影响其他事务。
持久性:如果提交,事务结果将被写入持久存储,例如硬盘。
SQLite 是一个符合 ACID 的数据库;它甚至通过了模拟在事务过程中计算机断电的测试,因此你有很高的保证,数据库文件不会被留下一个损坏的、不可用的状态。SQLite 查询要么完全将数据插入数据库,要么根本不插入。
SQL 注入攻击
一类被称为SQL 注入攻击的黑客技术可以改变你的查询以执行你未打算做的事情。这些技术超出了本书的范围,如果你的程序不接受来自互联网陌生人的数据,那么这很可能不是你代码的问题。但为了防止这种情况,在插入或更新数据库中的数据时,每次引用变量时都使用?问号语法。
例如,如果我想根据存储在变量中的数据插入一个新的猫记录,我不应该直接使用 Python 将这些变量插入到查询字符串中,如下所示:
>>> cat_name = 'Zophie'
>>> cat_bday = '2021-01-24'
>>> fur_color = 'black'
>>> cat_weight = 5.6
>>> conn.execute(f'INSERT INTO cats VALUES ("{cat_name}", "{cat_bday}",
"{fur_color}", {cat_weight})')
<sqlite3.Cursor object at 0x0000022B91BB7C40>
如果这些变量的值来自用户输入,例如 Web 应用程序表单,黑客可能可以指定会改变查询意义的字符串。相反,我应该在使用查询字符串时使用?,然后传递一个列表参数,该参数跟在查询字符串后面:
>>> conn.execute('INSERT INTO cats VALUES (?, ?, ?, ?)', [cat_name, cat_bday,
fur_color, cat_weight])
<sqlite3.Cursor object at 0x0000022B91BB7C40>
execute()方法在确保变量值不会导致 SQL 注入攻击后,将查询字符串中的?占位符替换为变量值。虽然此类攻击不太可能适用于你的代码,但使用?占位符而不是自己格式化查询字符串是一个好习惯。
从数据库中读取数据
一旦数据库中有数据,你可以使用SELECT查询来读取它。将以下内容输入到交互式 shell 中,以从*example.db*数据库中读取数据:
>>> import sqlite3
>>> conn = sqlite3.connect('example.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
SELECT查询的execute()方法调用返回一个Cursor对象。为了获取实际数据,我们在该Cursor对象上调用fetchall()方法。每条记录作为元组列表中的元组返回。元组中的数据按表的列顺序出现。
而不是编写 Python 代码来自己遍历这个元组列表,你可以让 SQLite 提取你想要的具体信息。示例SELECT查询有四个部分:
-
SELECT关键字 -
你想要检索的列,其中
*表示“除了rowid之外的所有列” -
FROM关键字 -
用于从表中检索数据的表;在这种情况下,是
cats表
如果你只想获取cats表中记录的rowid和name列,你的查询将看起来像这样:
>>> conn.execute('SELECT rowid, name FROM cats').fetchall()
[(1, 'Zophie')]
你也可以使用 SQL 来过滤查询结果,你将在下一节中了解到。
遍历查询结果
fetchall()方法将你的SELECT查询结果作为元组列表返回。常见的编码模式是使用for循环来对每个元组执行一些操作。例如,从nostarch.com/automate-boring-stuff-python-3rd-edition下载*sweigartcats.db*文件,然后将其输入到交互式 shell 中处理其数据:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> for row in conn.execute('SELECT * FROM cats'):
... print('Row data:', row)
... print(row[0], 'is one of my favorite cats.')
...
Row data: ('Zophie', '2021-01-24', 'gray tabby', 5.6)
Zophie is one of my favorite cats.
Row data: ('Miguel', '2016-12-24', 'siamese', 6.2)
Miguel is one of my favorite cats.
Row data: ('Jacob', '2022-02-20', 'orange and white', 5.5)
Jacob is one of my favorite cats.
# --snip--
for循环可以在不调用fetchall()的情况下遍历conn.execute()返回的行数据元组,并且for循环体内的代码可以单独操作每一行,因为row变量填充了查询的行数据元组。然后代码可以通过元组的整数索引访问列:索引0为名称,索引1为出生日期,依此类推。
过滤检索数据
我们的SELECT查询已经检索了表中的每一行,但我们可能只想获取符合某些过滤标准的行。使用sweigartcats.db文件,向SELECT语句中添加一个WHERE子句以提供搜索参数,例如具有黑色毛发:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE fur = "black"').fetchall()
[❶('Zophie', '2021-01-24', 'black', 5.6), ('Toby', '2021-05-17', 'black',
6.8), ('Thor', '2013-05-14', 'black', 5.2), ('Sassy', '2017-08-20', 'black',
7.5), ('Hope', '2016-05-22', 'black', 7.6)]
在这个例子中,WHERE子句WHERE fur = "black"将只检索fur列中包含"black"的记录。
SQLite 为WHERE子句定义了自己的运算符,但它们与 Python 的运算符类似:=, !=, <, >, <=, >=, AND, OR, 和 NOT。请注意,SQLite 使用**=**运算符表示“等于”,而 Python 使用==运算符来达到这个目的。在运算符的两侧,您可以放置列名或字面值。
比较将在表中的每一行进行。例如,对于WHERE fur = "black",SQLite 执行以下比较:
-
因为
fur是'black',并且'black'='black'是真的,所以 SQLite 将包含行❶在结果中。 -
对于行
(2, 'Miguel', '2016-12-24', 'siamese', 6.2),fur是'siamese',而'siamese'='black'是假的,所以它不包括该行在结果中。 -
对于行
(3, 'Jacob', '2022-02-20', 'orange and white', 5.5),fur是'orange and white',而'orange and white'='black'是假的,所以它不包括该行在结果中。
... 以此类推,对于cats表中的每一行。
让我们用一个更复杂的WHERE子句继续之前的例子:WHERE fur = "black" 或 birthdate >= "2024-01-01"。同时,我们也可以使用pprint.pprint()函数来“美化打印”返回的列表:
>>> import pprint
>>> matching_cats = conn.execute('SELECT * FROM cats WHERE fur = "black"
OR birthdate >= "2024-01-01"').fetchall()
>>> pprint.pprint(matching_cats)
[('Zophie', '2021-01-24', 'black', 5.6),
('Toby', '2021-05-17', 'black', 6.8),
('Taffy', '2024-12-09', 'white', 7.0),
('Hollie', '2024-08-07', 'calico', 6.0),
('Lewis', '2024-03-19', 'orange tabby', 5.1),
('Thor', '2013-05-14', 'black', 5.2),
('Shell', '2024-06-16', 'tortoiseshell', 6.5),
('Jasmine', '2024-09-05', 'orange tabby', 6.3),
('Sassy', '2017-08-20', 'black', 7.5),
('Hope', '2016-05-22', 'black', 7.6)]
所有在matching_cats列表中的猫要么有黑色毛发,要么出生日期在 2024 年 1 月 1 日之后。请注意,出生日期只是一个字符串。虽然比较运算符如>=通常在字符串上执行字母顺序比较,但只要出生日期格式是YYYY-MM-DD,它们也可以执行时间比较。
LIKE运算符允许您匹配值的开始或结束,将百分号(%)视为通配符。例如,name LIKE "%y"匹配所有以'y'结尾的名称,而name LIKE "Ja%"匹配所有以'Ja'开头的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%y"').fetchall()
[(5, 'Toby'), (11, 'Molly'), (12, 'Dusty'), (17, 'Mandy'), (18, 'Taffy'), (25, 'Rocky'), (27,
'Bobby'), (30, 'Misty'), (34, 'Mitsy'), (38, 'Colby'), (40, 'Riley'), (46, 'Ruby'), (65,
'Daisy'), (67, 'Crosby'), (72, 'Harry'), (77, 'Sassy'), (85, 'Lily'), (93, 'Spunky')]
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "Ja%"').fetchall()
[(3, 'Jacob'), (49, 'Java'), (75, 'Jasmine'), (80, 'Jamison')]
您也可以在字符串的开始和结束处放置百分号,以匹配中间的任何文本。例如,name LIKE "%ob%"匹配所有包含'ob'的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%ob%"').fetchall()
[(3, 'Jacob'), (5, 'Toby'), (27, 'Bobby')]
LIKE运算符进行不区分大小写的匹配,因此name LIKE "%ob%"也匹配'%OB%'、'%Ob%'和'%oB%'。要进行区分大小写的匹配,请使用GLOB运算符和*作为通配符字符:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name GLOB "*m*"').fetchall()
[(4, 'Gumdrop'), (9, 'Thomas'), (44, 'Sam'), (63, 'Cinnamon'), (75, 'Jasmine'),
(79, 'Samantha'), (80, 'Jamison')]
当name LIKE "%m%"匹配时,无论是小写还是大写的m,name GLOB "*m*"只匹配小写的m。
SQLite 的广泛运算符和功能与任何完整的编程语言相媲美。您可以在 SQLite 文档中了解更多信息,文档地址为www.sqlite.org/lang_expr.html。
排序结果
虽然您可以通过调用 Python 的sort()方法来始终对fetchall()返回的列表进行排序,但通过在SELECT查询中添加ORDER BY子句,让 SQLite 为您排序数据会更简单。例如,如果我想按毛发颜色对猫进行排序,我可以输入以下内容:
>>> import sqlite3, pprint
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> pprint.pprint(conn.execute('SELECT * FROM cats ORDER BY fur').fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Toby', '2021-05-17', 'black', 6.8),
('Thor', '2013-05-14', 'black', 5.2),
# --snip--
('Celine', '2015-04-18', 'white', 7.3),
('Daisy', '2019-03-19', 'white', 6.0)]
如果你的查询中有 WHERE 子句,则 ORDER BY 子句必须跟在它之后。你也可以根据多个列对行进行排序。例如,如果你想首先按毛色排序行,然后在每种毛色内按出生日期排序行,请运行以下查询:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur, birthdate')
>>> pprint.pprint(cur.fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Elton', '2020-05-28', 'bengal', 5.4),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Thor', '2013-05-14', 'black', 5.2),
('Hope', '2016-05-22', 'black', 7.6),
# --snip--
('Ginger', '2020-09-22', 'white', 5.8),
('Taffy', '2024-12-09', 'white', 7.0)]
ORDER BY 子句首先列出 fur 列,然后是 birthdate 列,两者之间用逗号分隔。默认情况下,这些排序是升序的:最小的值排在前面,然后是较大的值。要按降序排序,请在列名后添加 DESC 关键字。你也可以使用 ASC 关键字来指定升序,如果你想使查询更明确和易读。为了练习使用这些关键字,请在交互式外壳中输入以下内容:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur ASC, birthdate DESC')
>>> pprint.pprint(cur.fetchall())
[('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Iris', '2017-07-13', 'bengal', 6.8),
('Toby', '2021-05-17', 'black', 6.8),
('Sassy', '2017-08-20', 'black', 7.5),
# --snip--
('Mitsy', '2015-05-29', 'white', 5.0),
('Celine', '2015-04-18', 'white', 7.3)]
输出列表按毛色升序列出猫(bengal 在 white 之前)。在每种毛色中,猫按出生日期降序排序(2023-12-22 在 2020-05-28 之前)。
限制结果数量
如果你只想查看由你的 SELECT 查询返回的前几行,你可能尝试使用 Python 列切片来限制结果。例如,使用 [:3] 切片来只显示 cats 表中的前三行:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()[:3] # This is inefficient.
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码可以工作,但效率不高;它首先从表中获取所有行,然后丢弃除了前三行之外的所有内容。对于你的程序来说,直接从数据库中获取前三行会更高效。你可以使用 LIMIT 子句来实现:
>>> conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码比获取所有行的代码运行得更快,尤其是在行数较多的表中。如果你的 SELECT 查询中包含 WHERE 和 ORDER BY 子句,则 LIMIT 子句必须跟在它们之后,如下例所示:
>>> conn.execute('SELECT * FROM cats WHERE fur="orange" ORDER BY birthdate LIMIT 4').fetchall()
[('Mittens', '2013-07-03', 'orange', 7.4), ('Piers', '2014-07-08', 'orange', 5.2),
('Misty', '2016-07-08', 'orange', 5.2), ('Blaze', '2023-01-16', 'orange', 7.4)]
你可以在 SELECT 查询中添加一些其他子句,但它们超出了本章的范围。你可以在 SQLite 文档中了解更多关于它们的信息。
为更快的数据读取创建索引
在前面的部分中,我们运行了一个 SELECT 查询来根据匹配的名称查找记录。你可以通过在 name 列上创建索引来加快这个搜索。SQL 索引 是一种组织列数据的结构。因此,使用这些列的 WHERE 子句的查询将表现得更好。缺点是索引会占用一点额外的存储空间,因此插入或更新数据的查询会稍微慢一些,因为 SQLite 必须同时更新数据的索引。如果你的数据库很大,并且你比插入或更新数据更频繁地从中读取数据,创建索引可能是值得的。然而,你应该进行测试以验证索引实际上是否提高了性能。
要在 cats 表的 names 和 birthdate 列上创建索引,请运行以下 CREATE INDEX 查询:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('CREATE INDEX idx_name ON cats (name)')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('CREATE INDEX idx_birthdate ON cats (birthdate)')
<sqlite3.Cursor object at 0x0000013EC121A040>
索引需要名称,并且按照惯例,我们根据它们应用的列来命名它们,并加上idx_前缀。索引名称在整个数据库中是全局的,所以如果数据库包含多个名为birthdate的列的表,你可能还希望在索引名称中包含表名,例如idx_cats_birthdate。要查看表中存在的所有索引,可以使用SELECT查询检查内置的sqlite_schema表:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_name',), ('idx_birthdate',)]
如果你改变主意或发现索引没有提高性能,你可以使用DROP INDEX查询来删除它们:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',), ('idx_name',)]
>>> conn.execute('DROP INDEX idx_name')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',)]
对于只有几千条记录的小型数据库,你可以安全地忽略索引,因为它们提供的优势是可以忽略不计的。然而,如果你发现数据库查询花费的时间明显增加,创建索引可能会提高它们的性能。
遍历查询结果
fetchall()方法将你的SELECT查询结果作为元组列表返回。常见的编码模式是使用这个数据在一个for循环中执行对每个元组的操作。例如,从nostarch.com/automate-boring-stuff-python-3rd-edition下载*sweigartcats.db*文件,然后将其输入到交互式 shell 中处理其数据:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> for row in conn.execute('SELECT * FROM cats'):
... print('Row data:', row)
... print(row[0], 'is one of my favorite cats.')
...
Row data: ('Zophie', '2021-01-24', 'gray tabby', 5.6)
Zophie is one of my favorite cats.
Row data: ('Miguel', '2016-12-24', 'siamese', 6.2)
Miguel is one of my favorite cats.
Row data: ('Jacob', '2022-02-20', 'orange and white', 5.5)
Jacob is one of my favorite cats.
# --snip--
for循环可以遍历conn.execute()返回的行数据元组,而不调用fetchall(),for循环体内的代码可以单独操作每一行,因为row变量填充了查询的行数据元组。然后代码可以使用元组的整数索引来访问列:索引0用于名称,索引1用于出生日期,依此类推。
过滤检索数据
我们的SELECT查询已经检索了表中的每一行,但我们可能只想检索符合某些筛选条件的行。使用*sweigartcats.db*文件,在SELECT语句中添加一个WHERE子句来提供搜索参数,例如具有黑色毛发的:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE fur = "black"').fetchall()
[❶('Zophie', '2021-01-24', 'black', 5.6), ('Toby', '2021-05-17', 'black',
6.8), ('Thor', '2013-05-14', 'black', 5.2), ('Sassy', '2017-08-20', 'black',
7.5), ('Hope', '2016-05-22', 'black', 7.6)]
在这个例子中,WHERE子句WHERE fur = "black"将只检索具有'black'的fur列的记录。
SQLite 为WHERE子句定义了自己的运算符,但它们与 Python 的运算符类似:=, !=, <, >, <=, >=, AND, OR, 和 NOT。请注意,SQLite 使用=运算符表示“等于”,而 Python 使用==运算符来表示这个目的。在运算符的任一侧,你可以放置列名或字面值。
比较将在表中的每一行发生。例如,对于WHERE fur = "black",SQLite 会进行以下比较:
-
因为
fur是'black',并且'black'='black'是真的,SQLite 将包含行❶在结果中。 -
对于行
(2, 'Miguel', '2016-12-24', 'siamese', 6.2),fur是'siamese',而'siamese'='black'是假的,所以它不包括该行在结果中。 -
对于行
(3, 'Jacob', '2022-02-20', 'orange and white', 5.5),fur是'orange and white',而'orange and white'='black'是 false,因此它不会将该行包含在结果中。
... 以此类推,对于 cats 表中的每一行。
让我们用一个更复杂的 WHERE 子句继续前面的例子:WHERE fur = "black" OR birthdate >= "2024-01-01"。我们还可以使用 pprint.pprint() 函数来“美化打印”返回的列表:
>>> import pprint
>>> matching_cats = conn.execute('SELECT * FROM cats WHERE fur = "black"
OR birthdate >= "2024-01-01"').fetchall()
>>> pprint.pprint(matching_cats)
[('Zophie', '2021-01-24', 'black', 5.6),
('Toby', '2021-05-17', 'black', 6.8),
('Taffy', '2024-12-09', 'white', 7.0),
('Hollie', '2024-08-07', 'calico', 6.0),
('Lewis', '2024-03-19', 'orange tabby', 5.1),
('Thor', '2013-05-14', 'black', 5.2),
('Shell', '2024-06-16', 'tortoiseshell', 6.5),
('Jasmine', '2024-09-05', 'orange tabby', 6.3),
('Sassy', '2017-08-20', 'black', 7.5),
('Hope', '2016-05-22', 'black', 7.6)]
结果 matching_cats 列表中的所有猫要么有黑色毛发,要么出生日期在 2024 年 1 月 1 日之后。请注意,出生日期只是一个字符串。虽然比较运算符如 >= 通常在字符串上执行字母顺序比较,但只要出生日期格式是 YYYY-MM-DD,它们也可以执行时间比较。
LIKE 操作符允许您匹配值的开始或结束部分,将百分号(%)视为通配符。例如,name LIKE "%y" 匹配所有以 'y' 结尾的名称,而 name LIKE "Ja%" 匹配所有以 'Ja' 开头的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%y"').fetchall()
[(5, 'Toby'), (11, 'Molly'), (12, 'Dusty'), (17, 'Mandy'), (18, 'Taffy'), (25, 'Rocky'), (27,
'Bobby'), (30, 'Misty'), (34, 'Mitsy'), (38, 'Colby'), (40, 'Riley'), (46, 'Ruby'), (65,
'Daisy'), (67, 'Crosby'), (72, 'Harry'), (77, 'Sassy'), (85, 'Lily'), (93, 'Spunky')]
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "Ja%"').fetchall()
[(3, 'Jacob'), (49, 'Java'), (75, 'Jasmine'), (80, 'Jamison')]
您也可以在字符串的开始和结束处放置百分号,以匹配中间任何位置的文本。例如,name LIKE "%ob%" 匹配所有包含 'ob' 的名称:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name LIKE "%ob%"').fetchall()
[(3, 'Jacob'), (5, 'Toby'), (27, 'Bobby')]
LIKE 操作符进行不区分大小写的匹配,因此 name LIKE "%ob%" 也匹配 '%OB%'、'%Ob%' 和 '%oB%'。要进行区分大小写的匹配,请使用 GLOB 操作符和 * 作为通配符:
>>> conn.execute('SELECT rowid, name FROM cats WHERE name GLOB "*m*"').fetchall()
[(4, 'Gumdrop'), (9, 'Thomas'), (44, 'Sam'), (63, 'Cinnamon'), (75, 'Jasmine'),
(79, 'Samantha'), (80, 'Jamison')]
当 name LIKE "%m%" 匹配小写或大写的 m 时,name GLOB "*m*" 仅匹配小写的 m。
SQLite 的广泛操作符和功能与任何完整编程语言相媲美。您可以在 SQLite 文档中了解更多信息,文档地址为 www.sqlite.org/lang_expr.html。
排序结果
虽然您始终可以通过调用 Python 的 sort() 方法来对 fetchall() 返回的列表进行排序,但通过在 SELECT 查询中添加 ORDER BY 子句,让 SQLite 为您排序数据更容易。例如,如果我想按毛发颜色对猫进行排序,我可以输入以下内容:
>>> import sqlite3, pprint
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> pprint.pprint(conn.execute('SELECT * FROM cats ORDER BY fur').fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Toby', '2021-05-17', 'black', 6.8),
('Thor', '2013-05-14', 'black', 5.2),
# --snip--
('Celine', '2015-04-18', 'white', 7.3),
('Daisy', '2019-03-19', 'white', 6.0)]
如果您的查询中有一个 WHERE 子句,则 ORDER BY 子句必须跟在其后。您还可以根据多个列对行进行排序。例如,如果您想首先按毛发颜色排序行,然后在每个毛发颜色内按出生日期排序行,请运行以下命令:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur, birthdate')
>>> pprint.pprint(cur.fetchall())
[('Iris', '2017-07-13', 'bengal', 6.8),
('Elton', '2020-05-28', 'bengal', 5.4),
('Ruby', '2023-12-22', 'bengal', 5.0),
('Thor', '2013-05-14', 'black', 5.2),
('Hope', '2016-05-22', 'black', 7.6),
# --snip--
('Ginger', '2020-09-22', 'white', 5.8),
('Taffy', '2024-12-09', 'white', 7.0)]
ORDER BY 子句首先列出 fur 列,然后是 birthdate 列,列之间用逗号分隔。默认情况下,这些排序是升序的:最小的值排在前面,然后是较大的值。要按降序排序,请在列名后添加 DESC 关键字。您还可以使用 ASC 关键字来指定升序,如果您希望查询明确且易于阅读。为了练习使用这些关键字,请在交互式外壳中输入以下内容:
>>> cur = conn.execute('SELECT * FROM cats ORDER BY fur ASC, birthdate DESC')
>>> pprint.pprint(cur.fetchall())
[('Ruby', '2023-12-22', 'bengal', 5.0),
('Elton', '2020-05-28', 'bengal', 5.4),
('Iris', '2017-07-13', 'bengal', 6.8),
('Toby', '2021-05-17', 'black', 6.8),
('Sassy', '2017-08-20', 'black', 7.5),
# --snip--
('Mitsy', '2015-05-29', 'white', 5.0),
('Celine', '2015-04-18', 'white', 7.3)]
输出列表按毛色升序列出猫(bengal在white之前)。在每个毛色中,猫按出生日期降序排序(2023-12-22在2020-05-28之前)。
限制结果数量
如果你只想查看SELECT查询返回的前几行,你可以尝试使用 Python 列表切片来限制结果。例如,使用[:3]切片来显示cats表中的前三行:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats').fetchall()[:3] # This is inefficient.
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码是有效的,但效率不高;它首先从表中获取所有行,然后丢弃除了前三行之外的所有行。对于你的程序来说,从数据库中只获取前三行会更快。你可以使用LIMIT子句来实现这一点:
>>> conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
这段代码比获取所有行的代码运行得更快,特别是对于行数较多的表。如果你的SELECT查询中包含WHERE和ORDER BY子句,则LIMIT子句必须跟在它们后面,如下例所示:
>>> conn.execute('SELECT * FROM cats WHERE fur="orange" ORDER BY birthdate LIMIT 4').fetchall()
[('Mittens', '2013-07-03', 'orange', 7.4), ('Piers', '2014-07-08', 'orange', 5.2),
('Misty', '2016-07-08', 'orange', 5.2), ('Blaze', '2023-01-16', 'orange', 7.4)]
你还可以在SELECT查询中添加一些其他子句,但它们超出了本章的范围。你可以在 SQLite 文档中了解更多关于它们的信息。
为更快地读取数据创建索引
在前面的章节中,我们运行了一个SELECT查询来根据匹配的名称查找记录。你可以在name列上创建索引来加快这个搜索。SQL 索引是一种组织列数据的结构。因此,使用这些列的WHERE子句的查询将表现得更好。缺点是索引会占用一点额外的存储空间,所以插入或更新数据的查询会稍微慢一些,因为 SQLite 还必须更新数据的索引。如果你的数据库很大,并且你比插入或更新数据更频繁地从中读取数据,创建索引可能是值得的。然而,你应该进行测试以验证索引实际上是否提高了性能。
要在cats表中的names和birthdate列上创建索引,请运行以下CREATE INDEX查询:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('CREATE INDEX idx_name ON cats (name)')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('CREATE INDEX idx_birthdate ON cats (birthdate)')
<sqlite3.Cursor object at 0x0000013EC121A040>
索引需要名称,并且按照惯例,我们根据它们应用的列来命名,并加上idx_前缀。索引名称在整个数据库中是全局的,所以如果数据库包含多个名为birthdate的列,你可能还想在索引名称中包含表名,例如idx_cats_birthdate。要查看表中存在的所有索引,可以使用SELECT查询检查内置的sqlite_schema表:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_name',), ('idx_birthdate',)]
如果你改变主意或者发现索引没有提高性能,你可以使用DROP INDEX查询来删除它们:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',), ('idx_name',)]
>>> conn.execute('DROP INDEX idx_name')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type = "index" AND
tbl_name = "cats"').fetchall()
[('idx_birthdate',)]
对于只有几千条记录的小型数据库,你可以安全地忽略索引,因为它们提供的优势微乎其微。然而,如果你发现你的数据库查询花费的时间明显增加,创建索引可能会提高它们的性能。
更新数据库中的数据
一旦您向表中插入行,您就可以使用 UPDATE 语句更改行。例如,让我们更新记录 (1, 'Zophie', '2021-01-24', 'black', 5.6),将 sweigartcats.db 文件中的毛色从 'black' 更改为 'gray tabby':
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'black', 5.6)]
>>> conn.execute('UPDATE cats SET fur = "gray tabby" WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000013EC121A040>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6)]
UPDATE 语句有以下部分:
-
UPDATE关键字 -
包含要更新的行的表的名称
-
SET子句,用于指定要更新的列以及要更新的值 -
WHERE子句,用于指定要更新的行
您可以通过逗号分隔来同时更新多个列。例如,查询 'UPDATE cats SET fur = "black", weight_kg = 6 WHERE rowid = 1' 将 fur 和 weight 列的值分别更新为 "black" 和 6。
UPDATE 查询更新 WHERE 子句为真的每一行。如果您运行了查询 'UPDATE cats SET fur = "gray tabby" WHERE name = "Zophie"',更新将应用于所有名为 Zophie 的猫。这可能比您预期的猫要多!这就是为什么在大多数更新查询中,WHERE 子句使用 rowid 列的主键来指定要更新的单个记录。主键唯一标识一行,因此在 WHERE 子句中使用它确保您只更新您打算更新的那一行。
在更新数据时忘记 WHERE 子句是一个常见的错误。例如,如果您想进行查找和替换,将所有 'white and orange' 毛色的猫更改为 'orange and white' 毛色,您将运行以下命令:
>>> conn.execute('UPDATE cats SET fur = "orange and white" WHERE fur = "white and orange"')
如果您忘记包含 WHERE 子句,更新将应用于表中的每一行。突然之间,您的所有猫都会长出橙色和白色的毛!
为了避免这种错误,始终在您的 UPDATE 查询中包含一个 WHERE 子句,即使您打算将更改应用于每一行。在这种情况下,您可以使用 WHERE 1。由于 1 是 SQLite 用于布尔 True 的值,这告诉 SQLite 将更改应用于每一行。在查询末尾添加一个多余的 WHERE 1 可能看起来很愚蠢,但它让您避免了可能轻易清除实际数据的危险错误。
从数据库中删除数据
您可以使用 DELETE 查询从表中删除行。例如,要从 cats 表中删除 Zophie,请在 sweigartcats.db 文件上运行以下命令:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT rowid, * FROM cats WHERE rowid = 1').fetchall()
[(1, 'Zophie', '2021-01-24', 'gray tabby', 5.6)]
>>> conn.execute('DELETE FROM cats WHERE rowid = 1')
<sqlite3.Cursor object at 0x0000020322D183C0>
>>> conn.execute('SELECT * FROM cats WHERE rowid = 1').fetchall()
[]
DELETE 语句有以下部分:
-
DELETE FROM关键字 -
包含要删除的行的表的名称
-
WHERE子句,用于指定要删除的行
与 INSERT 语句一样,在 DELETE 语句中始终包含一个 WHERE 子句至关重要;否则,您将从表中删除所有行。如果您打算删除所有行,请使用 WHERE 1,这样您就可以识别出没有 WHERE 子句的任何 DELETE 语句作为错误。
回滚事务
你有时可能想一次性运行多个查询,或者根本不运行这些查询,但你不知道你想做什么,直到你至少运行了一些查询。处理这种情况的一种方法是从一个新的事务开始,执行查询,然后要么将所有查询 提交 到数据库以完成事务,要么 回滚 所有查询,使数据库看起来就像没有进行过任何操作。
通常,每次你在自动提交模式下连接到 SQLite 数据库并调用 conn.execute() 时,都会开始和完成一个新的事务。然而,你也可以运行一个 BEGIN 查询来开始一个新的事务;然后,你可以通过调用 conn.commit() 来完成事务,或者通过调用 conn.rollback() 来撤销所有查询。
例如,让我们向 cats 表中添加两只新猫,然后回滚事务,以便表保持不变:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('BEGIN')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Socks", "2022-04-04", "white", 4.2)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Fluffy", "2022-10-30", "gray", 4.5)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.rollback() # This undoes the INSERT statements.
>>> conn.execute('SELECT * FROM cats WHERE name = "Socks"').fetchall()
[]
>>> conn.execute('SELECT * FROM cats WHERE name = "Fluffy"').fetchall()
[]
新的猫咪,Socks 和 Fluffy,没有被插入到数据库中。
另一方面,如果你想应用你运行的所有查询,调用 conn.commit() 将更改提交到数据库:
>>> conn.execute('BEGIN')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Socks", "2022-04-04", "white", 4.2)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.execute('INSERT INTO cats VALUES ("Fluffy", "2022-10-30", "gray", 4.5)')
<sqlite3.Cursor object at 0x00000219C8BF7C40>
>>> conn.commit()
>>> conn.execute('SELECT * FROM cats WHERE name = "Socks"').fetchall()
[('Socks', '2022-04-04', 'white', 4.2)]
>>> conn.execute('SELECT * FROM cats WHERE name = "Fluffy"').fetchall()
[('Fluffy', '2022-10-30', 'gray', 4.5)]
现在,Socks 和 Fluffy 这两只猫在数据库中有记录了。
备份数据库
我的一个朋友曾经对一个专门经营收藏体育卡片的电子商务网站使用的数据库进行修改。她需要纠正几张卡片的一些命名错误,就在她刚刚输入 UPDATE cards SET name = 'Chris Clemons' 时,她的猫走过来踩到了键盘,按下了回车键。由于没有 WHERE 子句,这个查询更新了网站上出售的成千上万张卡片的每一张。
幸运的是,她有数据库的备份,所以她可以将其恢复到之前的状态。(这特别有用,因为同样的事情以完全相同的方式再次发生,使她怀疑猫是故意这么做的。)
如果程序当前没有访问 SQLite 数据库,你可以通过简单地复制数据库文件来备份它。一个 Python 程序可能会通过调用 shutil.copy('sweigartcats.db', 'backup.db') 来做这件事,如第十一章所述。然而,如果你的软件不断读取或更新数据库的内容,那么你需要使用 Connection 对象的 backup() 方法。例如,在交互式外壳中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> backup_conn = sqlite3.connect('backup.db', isolation_level=None)
>>> conn.backup(backup_conn)
backup() 方法在运行其他查询之间安全地将 sweigartcats.db 数据库的内容备份到 backup.db 文件中。现在你的数据已经安全备份,你的猫可以随意踩在你的键盘上了。
修改和删除表格
在数据库中创建一个表并插入行之后,你可能想要重命名表或其列。你可能还希望向表中添加或删除列,甚至删除整个表。你可以使用 ALTER TABLE 查询来执行这些操作。
以下交互式 shell 示例从一个全新的*sweigartcats.db*数据库文件开始。运行一个ALTER TABLE RENAME查询将cats表格重命名为felines:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('cats',)]
>>> conn.execute('ALTER TABLE cats RENAME TO felines') # Rename the table.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('felines',)]
要在表中重命名列,运行一个ALTER TABLE RENAME COLUMN查询。例如,让我们将fur列重命名为description:
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall()[2] # List the third column.
(2, 'fur', 'TEXT', 0, None, 0)
>>> conn.execute('ALTER TABLE felines RENAME COLUMN fur TO description')
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall()[2] # List the third column.
(2, 'description', 'TEXT', 0, None, 0)
要向表格中添加新列,运行一个ALTER TABLE ADD COLUMN查询。例如,让我们向felines表格添加一个名为is_loved的新列,它包含一个布尔值。SQLite 使用0表示假值,使用1表示真值;我们将is_loved的默认值设置为1:
>>> conn.execute('ALTER TABLE felines ADD COLUMN is_loved INTEGER DEFAULT 1')
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> import pprint
>>> pprint.pprint(conn.execute('SELECT * FROM felines LIMIT 3').fetchall())
[('Zophie', '2021-01-24', 'gray tabby', 5.6, 1),
('Miguel', '2016-12-24', 'siamese', 6.2, 1),
('Jacob', '2022-02-20', 'orange and white', 5.5, 1)]
结果表明is_loved列是不必要的,因为我为所有的猫都存储了1,所以我可以使用ALTER TABLE DROP COLUMN查询删除该列:
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall() # List all columns.
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2, 'description', 'TEXT',
0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0), (4, 'is_loved', 'INTEGER', 0, '1', 0)]
>>> conn.execute('ALTER TABLE felines DROP COLUMN is_loved') # Delete the column.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('PRAGMA TABLE_INFO(felines)').fetchall() # List all columns.
[(0, 'name', 'TEXT', 1, None, 0), (1, 'birthdate', 'TEXT', 0, None, 0), (2, 'description', 'TEXT',
0, None, 0), (3, 'weight_kg', 'REAL', 0, None, 0)]
存储在已删除列中的任何数据也将被删除。
如果你想要删除整个表格,运行一个DROP TABLE查询:
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[('felines',)]
>>> conn.execute('DROP TABLE felines') # Delete the entire table.
<sqlite3.Cursor object at 0x000001EDDB477C40>
>>> conn.execute('SELECT name FROM sqlite_schema WHERE type="table"').fetchall()
[]
尽量限制你更改表格和列的频率,因为你还需要更新程序中的查询以匹配。
使用外键连接多个表格
SQLite 表格的结构相当严格;例如,每一行都有一个固定数量的列。但现实世界的数据通常比单个表格能捕捉到的更复杂。在关系型数据库中,我们可以在多个表格中存储复杂数据,并且可以创建称为外键的链接。
假设我们想要存储关于我们的猫接受的疫苗接种信息。我们无法仅仅向cats表格中添加列,因为每只猫可能只有一次疫苗接种或多次。此外,对于每一次疫苗接种,我们还想列出疫苗接种日期和接种它的兽医的名字。SQL 表格在存储列列表方面并不擅长。你不会希望有名为vaccination1、vaccination2、vaccination3等列,同样的原因,你也不会希望有名为vaccination1和vaccination2的变量。如果你创建太多的列或变量,你的代码会变得冗长、难以阅读。如果你创建得太少,你将不得不不断更新程序以添加所需的更多列。
当你有一系列变化的数据要添加到一行时,将添加的数据作为单独表格中的行列出,然后让这些行引用主表中的行,这样做更有意义。在我们的*sweigartcats.db*数据库中,通过在交互式 shell 中输入以下内容来添加第二个vaccinations表格:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> conn.execute('PRAGMA foreign_keys = ON')
<sqlite3.Cursor object at 0x000001E730AD03C0>
>>> conn.execute('CREATE TABLE IF NOT EXISTS vaccinations (vaccine TEXT,
date_administered TEXT, administered_by TEXT, cat_id INTEGER,
FOREIGN KEY(cat_id) REFERENCES cats(rowid)) STRICT')
<sqlite3.Cursor object at 0x000001CA42767D40>
新的vaccinations表格有一个名为cat_id的INTEGER类型列。这个列中的整数值与cats表格中行的rowid值相匹配。我们称cat_id列为外键,因为它引用了另一个表的键列。
在cats表格中,猫 Zophie 的rowid是1。为了记录她的疫苗接种,我们在vaccinations表格中插入新的行,并设置cat_id值为1:
>>> conn.execute('INSERT INTO vaccinations VALUES ("rabies", "2023-06-06", "Dr. Echo", 1)')
<sqlite3.Cursor object at 0x000001CA42767D40>
>>> conn.execute('INSERT INTO vaccinations VALUES ("FeLV", "2023-06-06", "Dr. Echo", 1)')
<sqlite3.Cursor object at 0x000001CA42767D40>
>>> conn.execute('SELECT * FROM vaccinations').fetchall()
[('rabies', '2023-06-06', 'Dr. Echo', 1), ('FeLV', '2023-06-06', 'Dr. Echo', 1)]
我们可以通过使用其他猫的 rowid 来记录它们的疫苗接种。如果我们想为 Mango 添加疫苗接种记录,我们可以在 cats 表中找到 Mango 的 rowid,然后使用该值在 vaccinations 表中添加记录:
>>> conn.execute('SELECT rowid, * FROM cats WHERE name = "Mango"').fetchall()
[(23, 'Mango', '2017-02-12', 'tuxedo', 6.8)]
>>> conn.execute('INSERT INTO vaccinations VALUES ("rabies", "2023-07-11", "Dr. Echo", 23)')
<sqlite3.Cursor object at 0x000001CA42767D40>
我们还可以执行一种名为 inner join 的 SELECT 查询,它返回两个表中的链接行。例如,将以下内容输入到交互式外壳中,以检索与 cats 表数据连接的 vaccinations 行:
>>> conn.execute('SELECT * FROM cats INNER JOIN vaccinations ON cats.rowid =
vaccinations.cat_id').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6, 'rabies', '2023-06-06', 'Dr. Echo', 1),
('Zophie', '2021-01-24', 'gray tabby', 5.6, 'FeLV', '2023-06-06', 'Dr. Echo', 1),
('Mango', '2017-02-12', 'tuxedo', 6.8, 'rabies', '2023-07-11', 'Dr. Echo', 23)]
注意,虽然您可以将 cat_id 设置为一个 INTEGER 列并用作外键,而不实际设置 FOREIGN KEY(cat_id) REFERENCES cats(rowid) 语法,但外键有几个安全特性来确保您的数据保持一致性。例如,您不能使用一个不存在的猫的 cat_id 插入或更新疫苗接种记录。SQLite 还强制您在删除猫之前删除该猫的所有疫苗接种记录,以避免留下“孤儿”疫苗接种记录。
这些安全功能默认是禁用的。您可以通过在调用 sqlite3.connect() 之后运行 PRAGMA 查询来启用它们:
>>> conn.execute('PRAGMA foreign_keys = ON')
外键和连接还有其他功能,但它们超出了本书的范围。
内存数据库和备份
如果您的程序正在执行大量查询,您可以通过使用 内存数据库 显着提高数据库的速度。这些数据库完全存储在计算机的内存中,而不是存储在计算机硬盘上的文件中。这使得更改变得非常快。然而,您需要记住使用 backup() 方法将内存数据库保存到文件。如果您的程序在运行过程中崩溃,您将丢失整个内存数据库,就像您会丢失程序变量的值一样。
以下示例创建了一个内存数据库,然后将其保存到文件 test.db 的数据库中:
>>> import sqlite3
>>> memory_db_conn = sqlite3.connect(':memory:',
isolation_level=None) # Create an in-memory database.
>>> memory_db_conn.execute('CREATE TABLE test (name TEXT, number REAL)')
<sqlite3.Cursor object at 0x000001E730AD0340>
>>> memory_db_conn.execute('INSERT INTO test VALUES ("foo", 3.14)')
<sqlite3.Cursor object at 0x000001D9B0A07EC0>
>>> file_db_conn = sqlite3.connect('test.db', isolation_level=None)
>>> memory_db_conn.backup(file_db_conn) # Save the database to test.db.
您也可以使用 backup() 方法将 SQLite 数据库文件加载到内存中:
>>> import sqlite3
>>> file_db_conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> memory_db_conn = sqlite3.connect(':memory:', isolation_level=None)
>>> file_db_conn.backup(memory_db_conn)
>>> memory_db_conn.execute('SELECT * FROM cats LIMIT 3').fetchall()
[('Zophie', '2021-01-24', 'gray tabby', 5.6), ('Miguel', '2016-12-24',
'siamese', 6.2), ('Jacob', '2022-02-20', 'orange and white', 5.5)]
使用内存数据库有一些缺点。如果您的程序因未处理的异常而崩溃,您将丢失数据库。您可以通过将代码包裹在一个 try 语句中,该语句捕获任何未处理的异常,然后使用 except 语句将文件保存到数据库中来减轻这种风险。第四章介绍了使用 try 和 except 语句进行异常处理。
数据库复制
您可以通过在 Connection 对象上调用 iterdump() 方法来获取数据库的副本。此方法返回一个迭代器,它生成重新创建数据库所需的 SQLite 查询文本。您可以在 for 循环中使用迭代器或将它们传递给 list() 函数以将它们转换为字符串列表。例如,要获取重新创建 sweigartcats.db 数据库所需的 SQLite 查询,请在交互式外壳中输入以下内容:
>>> import sqlite3
>>> conn = sqlite3.connect('sweigartcats.db', isolation_level=None)
>>> with open('sweigartcats-queries.txt', 'w', encoding='utf-8') as fileObj:
... for line in conn.iterdump():
... fileObj.write(line + '\n')
此代码创建一个 sweigartcats-queries.txt 文件,其中包含以下 SQLite 查询,可以重新创建数据库:
BEGIN TRANSACTION;
CREATE TABLE "cats" (name TEXT NOT NULL, birthdate TEXT, fur TEXT, weight_kg REAL) STRICT;
INSERT INTO "cats" VALUES('Zophie','2021-01-24','gray tabby',5.6);
INSERT INTO "cats" VALUES('Miguel','2016-12-24','siamese',6.2);
INSERT INTO "cats" VALUES('Jacob','2022-02-20','orange and white',5.5);
# --snip--
INSERT INTO "cats" VALUES('Spunky','2015-09-04','gray',5.9);
INSERT INTO "cats" VALUES('Shadow','2021-01-18','calico',6.0);
COMMIT;
这些查询的文本几乎肯定比原始数据库要大,但这些查询的优点是可读性强,易于在复制和粘贴到你的 Python 代码或 SQLite 应用程序之前进行编辑,正如我们接下来将要讨论的。
SQLite 应用程序
有时,你可能想在不编写所有这些额外 Python 代码的情况下直接调查 SQLite 数据库。你可以通过安装 sqlite3 命令来实现,该命令从终端命令行窗口运行,并在 sqlite.org/cli.html 上有文档说明。
在 Windows 上,从 sqlite.org/download.html 下载标记为“用于管理 SQLite 数据库文件的命令行工具包”的文件,并将 sqlite3.exe 程序放置在系统 PATH 上的一个文件夹中。(有关 PATH 环境变量和终端窗口的信息,请参阅第十二章。)sqlite3 命令在 macOS 上是预安装的。对于 UbuntuLinux,运行 sudo apt install sqlite3 来安装它。
接下来,在终端窗口中运行 sqlite3 example.db 以连接到 example.db 中的数据库。如果此文件不存在,sqlite3 将创建一个包含空数据库的文件。你可以将 SQL 查询输入到这个工具中,但与传递给 conn.execute() 的查询不同,它们必须以分号结尾。
例如,将以下内容输入到终端窗口中:
C:\Users\Al>sqlite3 example.db
SQLite version 3.`xx.xx`
Enter ".help" for usage hints.
sqlite> CREATE TABLE IF NOT EXISTS cats (name TEXT NOT NULL,
birthdate TEXT, fur TEXT, weight_kg REAL) STRICT;
sqlite> INSERT INTO cats VALUES ('Zophie', '2021-01-24', 'gray tabby', 4.7);
sqlite> SELECT * from cats;
Zophie|2021-01-24|gray tabby|4.7
正如这个例子所示,sqlite3 命令行工具为你提供了一个 SQLite 交互式外壳,你可以在其 sqlite> 提示符下输入查询。.help 命令显示其他命令,例如 .tables(显示数据库中的表)和 .headers(允许你打开或关闭列标题):
sqlite> .tables
cats
sqlite> .headers on
sqlite> SELECT * from cats;
name|birthdate|fur|weight_kg
Zophie|2021-01-24|gray tabby|4.7
如果命令行工具对你来说太简单了,还有免费的开源应用程序,可以在 Windows、macOS 和 Linux 的图形用户界面(GUI)中显示 SQLite 数据库:
-
DB Browser for SQLite (
sqlitebrowser.org) -
SQLite Studio (
sqlitestudio.pl) -
DBeaver Community (
dbeaver.io)
虽然这些图形用户界面应用程序使得与 SQLite 数据库一起工作变得更容易,但学习 SQLite 查询的基于文本的语法仍然值得。
摘要
计算机使得处理大量数据成为可能,但仅仅将数据放入文本文件,甚至是一个电子表格,可能不足以很好地组织数据,以便你有效地使用它。如 SQLite 这样的 SQL 数据库提供了一种高级方法,不仅可以存储大量信息,还可以通过 SQL 语言检索你想要的确切数据。
SQLite 是一个令人印象深刻的数据库,Python 的标准库中包含sqlite3模块。SQLite 的 SQL 版本与其他关系数据库中使用的版本不同,但它足够相似,学习 SQLite 可以为数据库的一般知识提供一个良好的介绍。
SQLite 数据库位于单个文件中,没有专用服务器。它们可以包含多个表(你可以将其视为类似于电子表格),每个表可以包含多个列。要编辑表中的值,你可以使用INSERT、SELECT、UPDATE和DELETE查询执行 CRUD 操作(创建、读取、更新和删除)。要更改表和列本身,你可以使用ALTER TABLE和DROP TABLE查询。最后,外键允许你使用称为连接的技术将多个表中的记录链接在一起。
SQLite 和数据库的内容远不止一章所能涵盖的。如果你想了解有关 SQL 数据库的一般知识,我推荐 Anthony DeBarros 所著的《Practical SQL》第 2 版(No Starch Press,2022 年)。
练习问题
-
获取名为
example.db的 SQLite 数据库的Connection对象需要哪些 Python 指令? -
以下 Python 指令将创建一个名为
students的新表,其中包含名为first_name、last_name和favorite_color的TEXT列? -
如何在自动提交模式下连接到 SQLite 数据库?
-
SQLite 中的
INTEGER和REAL数据类型有什么区别? -
严格模式为表添加了什么?
-
查询
'SELECT * FROM cats'中的*代表什么? -
CRUD 代表什么?
-
ACID 代表什么?
-
什么查询可以向表中添加新记录?
-
什么查询从表中删除记录?
-
如果在
UPDATE查询中未指定WHERE子句会发生什么? -
索引是什么?以下代码将如何为名为
cats的表中的birthdate列创建索引? -
外键是什么?
-
如何删除名为
cats的表? -
创建内存数据库时指定什么“文件名”?
-
如何将数据库复制到另一个数据库?
练习程序
为了练习,编写程序来完成以下任务。
猫咪疫苗接种检查器
从本书的资源中下载我的猫咪数据库sweigartcats.db,网址为nostarch.com/automate-boring-stuff-python-3rd-edition。编写一个程序打开此数据库,并列出所有没有名为'rabies'、'FeLV'和'FVRCP'疫苗的猫咪。此外,通过查找在猫咪生日之前接种的所有疫苗来检查数据库中的错误。
餐饮成分数据库
编写一个程序,使用以下 SQL 查询创建两个表,一个用于餐饮,一个用于成分:
CREATE TABLE IF NOT EXISTS meals (name TEXT) STRICT
CREATE TABLE IF NOT EXISTS ingredients (name TEXT,
meal_id INTEGER, FOREIGN KEY(meal_id) REFERENCES meals
(rowid)) STRICT
然后,编写一个程序,提示用户输入。如果用户输入'quit',程序应退出。用户还可以输入一个新的菜肴名称,后跟一个冒号和以逗号分隔的成分列表:'meal:ingredient1,ingredient2'。将菜肴及其成分保存到meals和ingredients表中。
最后,用户可以输入一道菜或成分的名称。如果该名称出现在meals表中,程序应列出该菜的成分。如果该名称出现在ingredients表中,程序应列出所有使用该成分的菜。例如,程序的输出可能如下所示:
> onigiri:rice,nori,salt,sesame seeds
Meal added: onigiri
> chicken and rice:chicken,rice,cream of chicken soup
Meal added: chicken and rice
> onigiri
Ingredients of onigiri:
rice
nori
salt
sesame seeds
> chicken
Meals that use chicken:
chicken and rice
> rice
Meals that use rice:
onigiri
chicken and rice
> quit
猫疫苗接种检查器
从本书的资源中下载我的猫的sweigartcats.db数据库,链接为nostarch.com/automate-boring-stuff-python-3rd-edition。编写一个程序打开此数据库,并列出所有未接种疫苗名为'rabies'、'FeLV'和'FVRCP'的猫。还要通过查找在猫生日之前接种的所有疫苗来检查数据库中的错误。
菜肴成分数据库
编写一个程序,使用以下 SQL 查询创建两个表,一个用于菜肴,一个用于成分:
CREATE TABLE IF NOT EXISTS meals (name TEXT) STRICT
CREATE TABLE IF NOT EXISTS ingredients (name TEXT,
meal_id INTEGER, FOREIGN KEY(meal_id) REFERENCES meals
(rowid)) STRICT
然后,编写一个程序,提示用户输入。如果用户输入'quit',程序应退出。用户还可以输入一个新的菜肴名称,后跟一个冒号和以逗号分隔的成分列表:'meal:ingredient1,ingredient2'。将菜肴及其成分保存到meals和ingredients表中。
最后,用户可以输入一道菜或成分的名称。如果该名称出现在meals表中,程序应列出该菜的成分。如果该名称出现在ingredients表中,程序应列出所有使用该成分的菜。例如,程序的输出可能如下所示:
> onigiri:rice,nori,salt,sesame seeds
Meal added: onigiri
> chicken and rice:chicken,rice,cream of chicken soup
Meal added: chicken and rice
> onigiri
Ingredients of onigiri:
rice
nori
salt
sesame seeds
> chicken
Meals that use chicken:
chicken and rice
> rice
Meals that use rice:
onigiri
chicken and rice
> quit


浙公网安备 33010602011771号