hav-cs50-merge-09
哈佛 CS50 中文官方笔记(十)
第五课
-
简介
-
索引
- 问题
-
跨多表索引
-
空间权衡
-
时间权衡
-
部分索引
- 问题
-
真空
- 问题
-
并发
-
事务
-
竞争条件
-
问题
-
-
结束
简介
-
这周,我们将学习如何优化我们的 SQL 查询,无论是时间还是空间。我们还将学习如何并发地运行查询。
-
我们将在一个新的数据库的背景下做所有这些,即互联网电影数据库,或更广为人知的 IMDb。我们的 SQLite 数据库是从您可能之前在 imdb.com 看过的庞大在线电影数据库编译而成的。
-
查看这些统计数据,以了解这个数据库有多大!它拥有的数据比我们迄今为止所使用的任何其他数据库都要多!
![IMDb 数据库统计数据]()
-
这里是详细说明实体及其关系的 ER 图。
![IMDb ER 图 — 人物、电影和评分实体]()
索引
-
让我们打开这个名为
movies.db的数据库在 SQLite 中。 -
.schema显示了在这个数据库中创建的表。为了实现实体 Person 和 Movie 之间的多对多关系,我们在 ER 图中有一个联合表,称为stars,它将people和movies的 ID 列作为外键列引用! -
要查看
movies表,我们可以从表中选择并限制结果。SELECT * FROM "movies" LIMIT 5; -
要查找与电影 Cars 相关的信息,我们会运行以下查询。
SELECT * FROM "movies" WHERE "title" = 'Cars';-
假设我们想找出这个查询运行了多长时间。SQLite 有一个命令
.timer on,它使我们能够计时我们的查询。 -
在运行上述查询以再次查找 Cars 时,我们可以看到三个不同的时间测量值与结果一起显示。
-
“实际”时间表示计时器时间,或执行查询并获得结果之间的时间。这是我们关注的衡量时间。在讲座中执行此查询所花费的时间大约是 0.1 秒!
-
-
在底层,当运行查找 Cars 的查询时,我们触发了对表
movies的 扫描——也就是说,表movies是逐行从上到下扫描,以找到所有标题为 Cars 的行。 -
我们可以优化这个查询,使其比扫描更高效。就像教科书通常有索引一样,数据库表也可以有索引。在数据库术语中,索引是一种用于加速从表中检索行的结构。
-
我们可以使用以下命令为
movies表中的"title"列创建索引。CREATE INDEX "title_index" ON "movies" ("title");- 在创建这个索引后,我们再次运行查询以查找名为《汽车总动员》的电影。在这次运行中,所需时间显著缩短(在讲座中,几乎比第一次快八倍)!
-
在上一个例子中,一旦创建了索引,我们就假设 SQL 会使用它来查找电影。然而,我们也可以通过在查询之前使用 SQLite 命令
EXPLAIN QUERY PLAN来明确地看到这一点。 -
要删除我们刚刚创建的索引,请运行:
DROP INDEX "title_index";- 在删除索引后,再次使用
EXPLAIN QUERY PLAN与SELECT查询一起运行将表明计划将回退到扫描整个数据库。
- 在删除索引后,再次使用
问题
数据库没有隐式算法来优化搜索吗?
- 对于某些列来说,它们确实有。在 SQLite 和大多数其他数据库管理系统中,如果我们指定一个列是主键,则会自动创建一个索引,通过该索引我们可以搜索主键。然而,对于像
"title"这样的常规列,则不会有自动优化。
是否建议为每个可能需要的列创建不同的索引?
- 虽然这似乎很有用,但在空间和时间上存在权衡,因为之后将数据插入带有索引的表中需要花费时间。我们很快就会看到更多关于这方面的内容!
跨多表索引
-
我们将运行以下查询来找到汤姆·汉克斯主演的所有电影。
SELECT "title" FROM "movies" WHERE "id" IN ( SELECT "movie_id" FROM "stars" WHERE "person_id" = ( SELECT "id" FROM "people" WHERE "name" = 'Tom Hanks' ) ); -
为了了解哪种索引可以帮助加快这个查询的速度,我们可以在查询之前再次运行
EXPLAIN QUERY PLAN。这显示查询需要两次扫描——people和stars。由于我们是通过 ID 搜索movies,SQLite 会自动为这个 ID 创建索引,所以不需要扫描movies表! -
让我们创建两个索引来加快这个查询的速度。
CREATE INDEX "person_index" ON "stars" ("person_id"); CREATE INDEX "name_index" ON "people" ("name"); -
现在,我们使用相同的嵌套查询运行
EXPLAIN QUERY PLAN。我们可以观察到-
现在所有的扫描都变成了使用索引的搜索,这很好!
-
在
people表上的搜索使用了一种称为COVERING INDEX的东西
-
-
覆盖索引意味着查询所需的所有信息都可以在索引本身中找到。而不是两步:
-
在索引中查找相关信息,
-
使用索引来搜索表,覆盖索引意味着我们只需一步(只是第一步)进行搜索。
-
-
要让
stars表上的搜索也使用覆盖索引,我们可以在为stars创建的索引中添加"movie_id"。这将确保要查找的信息(电影 ID)和要搜索的值(人物 ID)都包含在索引中。 -
首先,让我们删除
stars表上现有的索引实现。DROP INDEX "person_index"; -
接下来,我们创建新的索引。
CREATE INDEX "person_index" ON "stars" ("person_id", "movie_id"); -
运行以下命令将证明我们现在有两个覆盖索引,这应该会导致搜索速度大大加快!
EXPLAIN QUERY PLAN SELECT "title" FROM "movies" WHERE "id" IN ( SELECT "movie_id" FROM "stars" WHERE "person_id" = ( SELECT "id" FROM "people" WHERE "name" = 'Tom Hanks' ) ); -
确保我们已运行
.timer on,然后我们可以执行上述查询以找到汤姆·汉克斯主演的所有电影,并观察其运行时间。现在查询的运行速度比没有索引时快得多(在讲座中,速度快了一个数量级)!
空间权衡
-
索引看起来非常有帮助,但它们也有权衡——它们在数据库中占用额外的空间,因此虽然我们获得了查询速度的提升,但我们确实失去了空间。
-
索引以称为 B 树或平衡树的数据结构存储在数据库中。树数据结构看起来像这样:
![树数据结构]()
- 注意到树中有许多节点,每个节点通过箭头与其他几个节点相连。根节点,或树起源的节点,有三个子节点。树边缘的一些节点不指向任何其他节点。这些被称为叶节点。
-
让我们考虑如何为
movies表的"title"列创建索引。如果电影标题按字母顺序排序,那么使用二分搜索查找特定电影会容易得多。 -
在这种情况下,会复制
"titles"列。这个副本会被排序,然后通过指向电影 ID 将它们链接回movies表中的原始行。这在下图中进行了可视化。![索引:指向原始电影 ID 的标题排序副本]()
-
虽然这有助于我们轻松地可视化此列的索引,但在现实中,索引不是一个单独的列,而是被拆分成许多节点。这是因为如果数据库有大量数据,比如我们的 IMDb 示例,将一个列全部存储在内存中可能不可行。
-
然而,如果我们有包含索引部分的多个节点,我们还需要节点来导航到正确的部分。例如,考虑以下节点。左侧节点根据电影标题是否在 Frozen 之前、在 Frozen 和 Soul 之间或 Soul 之后按字母顺序,将我们引导到索引的正确部分!
![索引节点拆分为部分]()
-
上述表示是一个 B 树!这是 SQLite 中索引存储的方式。
时间权衡
- 与我们之前讨论的空间权衡类似,它也会使将数据插入列并添加到索引中花费更长的时间。每次向索引添加一个值时,B 树都需要遍历以确定该值应该添加的位置!
部分索引
-
这是一个只包含表的一部分行的索引,允许我们节省一个完整索引所占用的空间。
-
这在我们知道用户只查询表的一小部分行时特别有用。在 IMDb 的情况下,可能用户更有可能查询一部新发布的电影,而不是一部 15 年前的电影。让我们尝试创建一个部分索引,该索引存储 2023 年发布的电影标题。
CREATE INDEX "recents" ON "movies" ("titles") WHERE "year" = 2023; -
我们可以检查搜索 2023 年发布的电影是否使用了新的索引。
EXPLAIN QUERY PLAN SELECT "title" FROM "movies" WHERE "year" = 2023;这表明
movies表是使用部分索引进行扫描的。
问题
索引是否保存在模式中?
- 是的,在 SQLite 中,它们是这样的!我们可以通过运行
.schema来确认,我们将在数据库模式中看到创建的索引列表。
真空
-
有方法可以删除我们数据库中的未使用空间。SQLite 允许我们“真空”数据——这清理了之前已删除的数据(实际上并没有删除,只是标记为可用空间以供下一个
INSERT使用)。 -
要在终端上查找
movies.db的大小,我们可以使用 Unix 命令du -b movies.db -
在讲座中,这个命令向我们展示了数据库的大小大约是 158 百万字节,或者说 158 兆字节。
-
我们现在可以连接到我们的数据库并删除我们之前创建的索引。
DROP INDEX "person_index"; -
现在,如果我们再次运行 Unix 命令,我们会看到数据库的大小没有减少!要真正清理已删除的空间,我们需要对它进行真空。我们可以在 SQLite 中运行以下命令。
VACUUM;这可能需要一秒钟或两秒钟的时间来运行。在再次运行 Unix 命令检查数据库大小时,我们应该看到更小的尺寸。一旦我们删除所有索引并再次真空,数据库的大小将比 158 MB 小得多(在讲座中大约是 100 MB)。
问题
是否有可能使真空过程更快?
- 每个真空过程所需的时间可能不同,这取决于我们试图真空的空间量以及找到需要释放的位和字节有多容易!
如果一个删除某些行的查询实际上并没有删除它们,而只是将它们标记为已删除,我们是否还能检索这些行?
- 在法医学方面受过训练的人能够找到我们认为已删除但实际上仍然在我们的电脑上的数据。在 SQLite 的情况下,在执行真空操作后,将无法再次找到已删除的行。
并发
-
到目前为止,我们已经看到了如何优化单个查询。现在,我们将探讨如何允许一次不仅仅是一个查询,而是多个查询同时进行。
-
并发是数据库同时处理多个查询或交互的方式。想象一下,一个网站或金融服务数据库在同时承受大量流量。在这些情况下,并发尤为重要。
-
一些数据库事务可以是多部分的。例如,考虑一个银行的数据库。以下是一个存储账户余额的
accounts表的视图。![银行数据库中的账户表。爱丽丝向鲍勃发送 10 美元。]()
-
一个事务可能是从一个账户向另一个账户转账。例如,爱丽丝试图向鲍勃发送 10 美元。
-
要完成这个事务,我们需要向鲍勃的账户中添加 10 美元,并从爱丽丝的账户中减去 10 美元。如果有人看到在第一次更新鲍勃的账户之后但在第二次更新爱丽丝的账户之前的
accounts数据库的状态,他们可能会对银行持有的总金额有一个错误的理解。
-
事务
-
对于外部观察者来说,它应该看起来像事务的不同部分是同时发生的。在数据库术语中,事务是一个单独的工作单元——不能分解成更小的部分。
-
事务具有一些属性,可以使用 ACID 首字母缩写词来记住:
-
原子性:不能分解成更小的部分,
-
一致性:不应违反数据库约束,
-
隔离性:如果多个用户访问数据库,他们的事务不能相互干扰,
-
持久性:在数据库内部发生任何故障的情况下,所有由事务更改的数据都将保持不变。
-
-
让我们在终端中打开
bank.db,这样我们就可以实现从爱丽丝到鲍勃转账的事务了! -
首先,我们想查看
accounts表中已经存在的数据。SELECT * FROM "accounts";我们在此处记录鲍勃的 ID 是 2,爱丽丝的 ID 是 1,这对我们的查询将很有用。
-
要将 10 美元从爱丽丝的账户转移到鲍勃的账户,我们可以编写以下事务。
BEGIN TRANSACTION; UPDATE "accounts" SET "balance" = "balance" + 10 WHERE "id" = 2; UPDATE "accounts" SET "balance" = "balance" - 10 WHERE "id" = 1; COMMIT;注意,
UPDATE语句写在开始事务和提交事务的命令之间。如果我们执行查询是在写入UPDATE语句之后,但没有提交,那么两个UPDATE语句都不会执行!这有助于保持事务的原子性。通过以这种方式更新我们的表,我们无法看到中间步骤。 -
如果我们再次尝试运行上述事务——爱丽丝试图再给鲍勃支付 10 美元——它应该无法运行,因为爱丽丝的账户余额为 0。(
accounts表中的"balance"列有一个检查约束,以确保它具有非负值。我们可以运行.schema来检查这一点。) -
我们实现事务回滚的方式是使用
ROLLBACK。一旦我们开始一个事务并写入一些 SQL 语句,如果其中任何一个失败,我们可以使用ROLLBACK来结束它,将所有值回滚到事务前的状态。这有助于保持事务的一致性。BEGIN TRANSACTION; UPDATE "accounts" SET "balance" = "balance" + 10 WHERE "id" = 2; UPDATE "accounts" SET "balance" = "balance" - 10 WHERE "id" = 1; -- Invokes constraint error ROLLBACK;
竞争条件
-
事务可以帮助防止竞争条件。
-
当多个实体同时访问并基于共享值做出决策时,会发生竞争条件,这可能导致数据库中的不一致性。未解决的竞争条件可以被黑客利用来操纵数据库。
-
在讲座中,讨论了一个竞争条件的例子,其中两个用户合作可以利用数据库中的暂时不一致性来抢劫银行。
-
然而,事务是隔离处理的,以避免首先出现不一致。处理我们数据库中类似数据的每个事务都将按顺序处理。这有助于防止敌对攻击可以利用的不一致性。
-
为了使事务按顺序进行,SQLite 和其他数据库管理系统使用数据库上的锁。数据库中的表可能处于几种不同的状态:
-
未锁定(UNLOCKED):这是没有用户访问数据库时的默认状态,
-
共享(SHARED):当事务从数据库读取数据时,它获得共享锁,允许其他事务同时从数据库中读取,
-
排他(EXCLUSIVE):如果一个事务需要写入或更新数据,它将获得对数据库的排他锁,不允许其他事务同时发生(甚至不允许读取)
-
问题
我们如何决定何时一个事务可以获得排他锁?我们如何优先处理不同类型的交易?
- 可以使用不同的算法来做出这些决定。例如,我们总是可以选择最先发生的交易。如果需要排他性交易,则没有其他交易可以同时运行,这是确保表的一致性所必需的缺点。
锁定的粒度是什么?我们是锁定数据库、表还是表的行?
-
这取决于数据库管理系统(DBMS)。在 SQLite 中,我们可以通过运行以下排他性事务来实现这一点:
BEGIN EXCLUSIVE TRANSACTION;如果我们现在不完成这笔交易,而是尝试通过不同的终端连接到数据库以读取表,我们将得到一个错误,表明数据库已被锁定!当然,这是一种非常粗略的锁定方式,因为它锁定了整个数据库。由于 SQLite 在这方面比较粗略,因此它有一个模块用于优先处理事务并确保只获得最短必要的排他锁。
完成
- 这将我们带到了关于 SQL 优化的第五讲结论!
第六讲
-
简介
-
MySQL
-
创建
cards表- 问题
-
创建
stations表- 问题
-
创建
swipes表- 问题
-
修改表
-
存储过程
- 问题
-
带有参数的存储过程
-
PostgreSQL
- 问题
-
创建 PostgreSQL 表
-
使用 MySQL 进行扩展
-
访问控制
-
SQL 注入攻击
-
问题
-
Fin
简介
-
到目前为止,在本课程中,我们已经学习了如何设计和创建自己的数据库,读取和写入数据,以及最近如何优化我们的查询。现在,我们将了解如何以更大的规模来做这些事情。
-
可扩展性是增加或减少应用程序或数据库容量以满足需求的能力。
-
社交媒体平台和银行系统是可能需要随着其规模扩大和用户增加而扩展的应用程序的例子。
-
在本讲中,我们将使用不同的数据库管理系统,如 MySQL 和 PostgreSQL,这些系统可以用于扩展数据库。
-
SQLite 是一个嵌入式数据库,但 MySQL 和 PostgreSQL 是数据库服务器——它们通常运行在它们自己的专用硬件上,我们可以通过互联网连接到它们来运行我们的 SQL 查询。这使得它们能够将数据存储在 RAM 中,从而实现更快的查询。
MySQL
-
我们将使用我们在之前的讲座中使用的 MBTA 数据库。以下是一个 ER 图,显示了实体 Card、Swipe 和 Station 以及这些实体之间的关系。
![MBTA 数据库的 ER 图,包含 Card、Swipe 和 Station 实体]()
- 作为提醒,使用地铁的乘客有一个 CharlieCard,在车站刷卡以获得进入权限。乘客可以充值卡片,在某些情况下,他们还需要刷卡才能离开车站。MBTA 不存储有关乘客的信息,但只跟踪卡片。
-
我们想要使用这个模式在 MySQL 中创建一个数据库!在终端上,让我们连接到一个 MySQL 服务器。
mysql -u root -h 127.0.0.1 -P 3306 -p-
在这个终端命令中,
-u表示用户。我们提供我们想要连接到数据库的用户——root(在这种情况下与数据库管理员同义)。 -
127.0.0.1是互联网上本地主机的地址(我们的电脑)。 -
3306是我们想要连接的端口,这是 MySQL 的默认端口。将主机和端口的组合视为我们试图连接的数据库的地址! -
命令末尾的
-p表示我们希望在连接时提示输入密码。
-
-
由于这是一个完整的数据库服务器,其中可能包含许多数据库。要显示所有现有的数据库,我们使用以下 MySQL 命令。
SHOW DATABASES;这返回了一些服务器中已经存在的默认数据库。
-
我们将执行一些操作来设置 MBTA 数据库。我们已经看到了如何在 SQLite 中完成这些操作,所以让我们专注于 MySQL 的语法差异!
-
创建新数据库:
CREATE DATABASE `mbta`;我们使用反引号而不是引号来标识 SQL 语句中的表名和其他变量。
-
要将当前数据库更改为
mbta:USE `mbta`;
-
创建cards表
-
MySQL 在类型上比 SQLite 有更多的粒度。例如,一个整数可以是
TINYINT、SMALLINT、MEDIUMINT、INT或BIGINT,这取决于我们想要存储的数字的大小。以下表格显示了我们可以存储在每个整数类型中的数字的大小和范围。![MySQL 中整数类型的表格]()
这些范围假设我们想要使用有符号整数。如果我们使用无符号整数,每个整数类型可以存储的最大值将翻倍。
-
现在我们将使用
INT数据类型为 ID 列创建表cards。由于INT可以存储高达 40 亿的数字,它应该足够大,可以满足我们的使用案例!CREATE TABLE `cards` ( `id` INT AUTO_INCREMENT, PRIMARY KEY(`id`) );注意,我们使用关键字
AUTO_INCREMENT与 ID 一起使用,这样 MySQL 会自动插入下一个数字作为新行的 ID。
问题
如果 ID 列不是无符号整数怎么办?我们如何表示这一点?
- 是的,我们可以在创建整数时显式地将 ID 设置为无符号整数,通过添加关键字
UNSIGNED。
创建stations表
-
创建表后,我们可以通过运行以下命令来查看现有表的列表:
SHOW TABLES; -
要获取关于表的更多详细信息,我们可以使用
DESCRIBE命令。DESCRIBE `cards`; -
为了处理文本,MySQL 提供了许多类型。两种常用的类型是
CHAR——一个固定宽度的字符串,和VARCHAR——一个可变长度的字符串。MySQL 还有一个TEXT类型,但与 SQLite 不同,这种类型用于更长的文本块,如段落、书籍的页面等。根据文本的长度,它可以是TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT之一。此外,我们还有BLOB类型来存储二进制字符串。 -
MySQL 还提供了两种其他文本类型:
ENUM和SET。Enum 将列限制为我们提供的选项列表中的单个预定义选项。例如,衬衫尺寸可以枚举为 M、L、XL 等。Set 允许在单个单元格中存储多个选项,这在电影类型等场景中很有用。 -
现在,让我们在 MySQL 中创建
stations表。CREATE TABLE `stations` ( `id` INT AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL UNIQUE, `line` ENUM('blue', 'green', 'orange', 'red') NOT NULL, PRIMARY KEY(`id`) );-
我们选择
VARCHAR作为站名,因为名字可能长度不一。然而,一个站点所在的线路是波士顿现有的地铁线路之一。由于我们知道这些值可能是什么,我们可以使用ENUM类型。 -
我们也像在 SQLite 中一样,使用列约束
UNIQUE和NOT NULL。
-
-
在运行描述此表的命令后,我们看到一个类似的输出,列出了表中的每一列。在
Key字段下,主键通过PRI被识别,任何具有唯一值的列通过UNI被识别。NULL字段告诉我们哪些列允许NULL值,对于stations表来说,没有列允许NULL值。
问题
我们能否将表作为
ENUM的输入?
- 这可能可以通过嵌套
SELECT语句来实现,但如果表中的值随时间变化,这可能不是一个好主意。最好明确地将值作为ENUM的选项。
如果我们不知道一段文本的长度,并使用类似
VARCHAR(300)来表示它,这是否可以?
- 虽然这是可以的,但这里有一个权衡。每插入一行数据,我们将失去 300 字节的内存,如果我们最终只存储非常小的字符串,这可能不值得。可能更好的是从较小的长度开始,然后在需要时更改表以增加长度。
创建swipes表
-
MySQL 为我们提供了一些存储日期和时间的选项,而 SQLite 则必须使用数值类型来存储。
-
我们可以使用
DATE、YEAR、TIME、DATETIME和TIMESTAMP(用于更精确的时间)来存储我们的日期和时间值。最后三个允许可选参数来指定我们想要存储时间的精度。 -
在 SQLite 中,我们有
REAL数据类型。在这里,我们的选项是表下面的FLOAT和DOUBLE PRECISION。![MySQL 中的实际数据类型]()
- 由于浮点数的不精确性,需要指定字节数来确定精度。这意味着在有限的内存中,浮点数只能表示到一定的精度。字节越多,表示数字的精度就越高。
-
在 MySQL 中,也有一种使用十进制(固定精度)类型的方法。使用这种方法,我们将指定要表示的数字中的位数以及小数点后的位数。
-
让我们现在创建
swipes表。CREATE TABLE `swipes` ( `id` INT AUTO_INCREMENT, `card_id` INT, `station_id` INT, `type` ENUM('enter', 'exit', 'deposit') NOT NULL, `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `amount` DECIMAL(5,2) NOT NULL CHECK(`amount` != 0), PRIMARY KEY(`id`), FOREIGN KEY(`station_id`) REFERENCES `stations`(`id`), FOREIGN KEY(`card_id`) REFERENCES `cards`(`id`) );-
注意到使用
DEFAULT CURRENT_TIMESTAMP来指示如果未提供值,则应自动填充时间戳以存储当前时间。 -
我们为滑动金额选择的精度是 2。这是为了确保在没有任何舍入的情况下,可以添加或减去分。
-
我们在 SQLite 中创建表时使用的列约束仍然存在,包括确保滑动金额不是负数的检查。
-
-
如果我们在创建表后描述它,我们将看到熟悉的输出。
Key字段有一个新值,对于外键列,MUL(多个)表示它们可能有重复的值,因为它们是外键。
问题
当我们向列添加约束时,它们是否有生效的优先级?
- 不,约束以组合方式共同工作。MySQL 允许我们在创建表时以任何顺序添加约束。
MySQL 有类型亲和力吗?
- 不完全是这样。MySQL 确实有数据类型,如
INT和VARCHAR,但与 SQLite 不同,它不会允许我们输入不同类型的数据并尝试转换它。
修改表
-
MySQL 允许我们比 SQLite 更根本地修改表。
-
如果我们想要向一个站点可能所在的线路中添加一条银色线路,我们可以这样做。
ALTER TABLE `stations` MODIFY `line` ENUM('blue', 'green', 'orange', 'red', 'silver') NOT NULL;-
这允许我们修改
line列并更改其类型,使得ENUM现在包括银色作为选项。 -
还要注意,我们除了使用熟悉的 SQLite 中的
ALTER TABLE构造外,还使用了MODIFY关键字。
-
存储过程
-
存储过程是一种自动化 SQL 语句并重复运行它们的方式。
-
为了演示存储过程,我们再次使用之前讲座中提到的数据库——波士顿 MFA 数据库。
-
回想一下,我们在 SQLite 中使用视图来实现 MFA 数据库中
collections的软删除功能。一个名为current_collections的视图显示了所有未被标记为已删除的集合。现在,我们将使用 MySQL 中的存储过程来完成类似的功能。 -
让我们导航到已经在我们的 MySQL 服务器上创建的 MFA 数据库。
USE `mfa`; -
在描述
collections表时,我们看到deleted列不存在,需要添加到表中。ALTER TABLE `collections` ADD COLUMN `deleted` TINYINT DEFAULT 0;由于
deleted列只有 0 或 1 的值,使用TINYINT是安全的。我们还将其默认值设置为 0,因为我们希望保留表中已有的所有集合。 -
在我们创建存储过程之前,我们需要将分隔符从
;改为其他东西。与 SQLite 不同,在BEGIN和END(这里需要一个存储过程)之间我们可以输入多个语句,并以;结尾,而 MySQL 在遇到;时会提前结束语句。delimiter // -
现在,我们编写存储过程。
CREATE PROCEDURE `current_collection`() BEGIN SELECT `title`, `accession_number`, `acquired` FROM `collections` WHERE `deleted` = 0; END//注意我们如何在存储过程名称旁边使用空括号,这可能会让人联想到其他编程语言中的函数。与函数类似,我们也可以调用存储过程来运行它们。
-
创建此过程后,我们必须将分隔符重置为
;。delimiter ; -
让我们尝试调用这个过程来看看当前的集合。在这个时候,查询应该输出
collections表中的所有行,因为我们还没有进行软删除。CALL current_collection(); -
如果我们软删除“黎明时分工作的农民”并再次调用该过程,我们会发现已删除的行不包括在输出中。
UPDATE `collections` SET `deleted` = 1 WHERE `title` = 'Farmers working at dawn';
问题
我们能否向存储过程添加参数,即用一些输入来调用它们?
- 是的,我们可以,很快就会看到一个例子!
我们能否像函数一样从一个存储过程调用另一个存储过程?
- 是的。你可以在存储过程中放入你写的几乎所有 SQL 语句。
你能在 MySQL 的表中留下任何注释或备注吗?
- 这绝对可能是一个有用的功能!你可以在
schema.sql文件中留下注释,描述模式不同部分的意图,但也许还有在 SQL 表中添加注释的方法。
带参数的存储过程
-
在我们之前与 MFA 数据库合作时,我们有一个名为
transactions的表来记录购买的或出售的艺术品,我们也可以在这里创建它。CREATE TABLE `transactions` ( `id` INT AUTO_INCREMENT, `title` VARCHAR(64) NOT NULL, `action` ENUM('bought', 'sold') NOT NULL, PRIMARY KEY(`id`) ); -
现在,如果一件艺术品因为出售而从
collections中被删除,我们也希望更新transactions表中的这一信息。通常,这将是两个不同的查询,但通过存储过程,我们可以给这个序列一个名称。delimiter // CREATE PROCEDURE `sell`(IN `sold_id` INT) BEGIN UPDATE `collections` SET `deleted` = 1 WHERE `id` = `sold_id`; INSERT INTO `transactions` (`title`, `action`) VALUES ((SELECT `title` FROM `collections` WHERE `id` = `sold_id`), 'sold'); END// delimiter ;这个过程参数的选择是绘画或艺术品的 ID,因为它是一个唯一的标识符。
-
我们现在可以调用这个过程来出售特定的物品。假设我们想要出售“想象中的风景”。
CALL `sell`(2);我们可以显示
collections和transactions表中的数据,以验证所做的更改。 -
如果我多次调用
sell同一个 ID 会发生什么?它可能会被多次添加到transactions表中。通过使用一些常规的编程结构,存储过程在逻辑和复杂性上可以得到相当大的改进。以下列表包含了一些在 MySQL 中可用的流行结构。![MySQL 中的编程结构]()
PostgreSQL
-
到目前为止,在本讲座中,我们看到了如何使用 MySQL,这让我们能够扩展 SQLite 所能提供的能力。
-
我们现在将通过与 MySQL 相同的流程来探索 PostgreSQL 的功能。我们将使用一些现有的 SQLite 数据库并将它们转换为 PostgreSQL。
-
回到之前提到的 MBTA 数据库,它有一个名为
cards的表,让我们看看 PostgreSQL 为我们提供了哪些数据类型。- 整数
![PostgreSQL 中的整数类型]()
我们可以观察到这里的选项比 MySQL 少。PostgreSQL 也提供了无符号整数,类似于 MySQL。这意味着在处理无符号整数时,每个整数类型可以存储的最大值是这里显示的两倍。
-
序列
- 序列也是整数,但它们是序列号,通常用于主键。
-
让我们通过打开 PSQL(PostgreSQL 的命令行界面)来连接到数据库服务器。
psql postgresql://postgres@127.0.0.1:5432/postgres我们可以以默认的 Postgres 用户或管理员身份登录。
-
要查看所有数据库,我们可以运行
\l,它会弹出一个列表。 -
要创建 MBTA 数据库,我们可以运行:
CREATE DATABASE "mbta"; -
要连接到这个特定的数据库,我们可以运行
\c "mbta"。 -
要列出数据库中的所有表,我们可以运行
\dt。然而,目前数据库中还没有表。 -
最后,我们可以按照提议创建
cards表,我们为 ID 列使用SERIAL数据类型。CREATE TABLE "cards" ( "id" SERIAL, PRIMARY KEY("id") ); -
要在 PostgreSQL 中描述一个表,我们可以使用像
\d "cards"这样的命令。运行此命令后,我们会看到有关此表的一些信息,但格式与 MySQL 略有不同。
问题
你如何在 PostgreSQL 中知道你的查询是否导致错误?
- 如果你按下回车键,而数据库服务器没有显示 ptu,你就知道可能存在错误。也可能 PostgreSQL 会给你一些有用的错误消息,以帮助你找到正确的方向。
创建 PostgreSQL 表
-
stations表以类似 MySQL 的方式创建。CREATE TABLE "stations" ( "id" SERIAL, "name" VARCHAR(32) NOT NULL UNIQUE, "line" VARCHAR(32) NOT NULL, PRIMARY KEY("id") );我们可以在 PostgreSQL 中像在 MySQL 中一样使用
VARCHAR。为了使事情简单,我们可以说"line"列也是VARCHAR类型。 -
我们接下来想要创建
swipes表。回想一下,滑动类型可以标记卡的进入、退出或资金存入。类似于 MySQL,我们可以使用ENUM来捕获这些选项,但不要将其包含在列定义中。相反,我们创建自己的类型。CREATE TYPE "swipe_type" AS ENUM('enter', 'exit', 'deposit'); -
PostgreSQL 有
TIMESTAMP、DATE、TIME和INTERVAL类型来表示日期和时间值。INTERVAL用于捕获某物持续了多长时间,或时间之间的距离。类似于 MySQL,我们可以使用这些类型指定精度。 -
与 PostgreSQL 中的实数类型相比,一个关键的区别是
DECIMAL类型被称为NUMERIC。 -
我们现在可以继续创建
swipes表,如下所示。CREATE TABLE "swipes" ( "id" SERIAL, "card_id" INT, "station_id" INT, "type" "swipe_type" NOT NULL, "datetime" TIMESTAMP NOT NULL DEFAULT now(), "amount" NUMERIC(5,2) NOT NULL CHECK("amount" != 0), PRIMARY KEY("id"), FOREIGN KEY("station_id") REFERENCES "stations"("id"), FOREIGN KEY("card_id") REFERENCES "cards"("id") );对于默认的时间戳,我们使用 PostgreSQL 提供的函数
now(),它给我们当前的时戳! -
要退出 PostgreSQL,我们使用命令
\q。
使用 MySQL 进行扩展
-
考虑一个需求增长的应用程序数据库服务器。随着来自应用程序的读取和写入数量的增加,服务器处理查询的等待时间也会增加。
-
这里的一种方法是通过垂直扩展数据库。垂直扩展是通过增加数据库服务器的计算能力来增加容量。
-
另一种方法是水平扩展。这意味着通过在多个服务器之间分配负载来增加容量。当我们水平扩展时,我们在多个服务器上保留数据库的副本(复制)。
-
复制主要有三种模式:单主模式、多主模式和领导者无模式。单主复制涉及单个数据库服务器处理传入的写入,然后将这些更改复制到其他服务器,而多主复制涉及多个服务器接收更新,导致复杂性增加。领导者无模式采用完全不同的方法,不要求有领导者。
-
在这里,我们将重点关注 单主复制模式。在这个模式中,跟随数据库服务器是一个只读副本:一个只能从中读取数据的数据库副本。领导者服务器被指定处理对数据库的写入。
-
一旦领导者处理完写请求,它可以在做其他任何事情之前等待跟随者复制更改。这被称为同步复制。虽然这确保了数据库始终一致,但它可能对查询的响应速度太慢。在金融或医疗保健等数据一致性至关重要的应用程序中,我们可能会选择这种通信方式,尽管它有缺点。
-
另一种类型是异步复制,其中领导者以异步方式与跟随者数据库通信,以确保更改被复制。这种方法可以用于社交媒体应用程序,其中响应速度至关重要。
-
另一种流行的扩展方式称为分片。这涉及到将数据库分割成多个数据库服务器上的碎片。关于分片的一个注意事项:我们希望避免出现数据库热点,或者一个比其他服务器更频繁被访问的数据库服务器。这可能会给该服务器造成过载。
-
当我们不使用复制进行分片时,会出现另一个问题。在这种情况下,如果其中一个服务器宕机,我们将有一个不完整的数据库。这会创建一个单点故障:如果一个系统宕机,我们的整个系统将无法使用。
访问控制
-
之前,我们使用 root 用户登录 MySQL。然而,我们也可以创建更多用户并给他们一些数据库访问权限。
-
让我们创建一个名为 Carter 的新用户(在这里你可以尝试使用你自己的名字)!
CREATE USER 'carter' IDENTIFIED BY 'password'; -
我们现在可以使用新用户和密码登录 MySQL,就像之前使用 root 用户一样。
-
当我们创建这个新用户时,默认情况下它只有很少的权限。尝试以下查询。
SHOW DATABASES;这只显示了服务器中的一些默认数据库。
-
如果我们再次以 root 用户登录并运行上述查询,会出现更多的数据库!这是因为 root 用户可以访问服务器上的几乎所有内容。
-
让我们通过讨论上周的一个例子来探讨如何通过用户授权来授予用户访问权限。我们有一个
rideshare数据库和一个rides表。在这个表中,我们存储了乘客的名字,这是个人身份信息(PII)。我们创建了一个名为analysis的视图,匿名化了乘客的名字,目的是只与分析师或其他用户共享这个视图。 -
如果我们想与刚刚创建的用户共享
analysis视图,我们可以在以 root 用户登录时执行以下操作。GRANT SELECT ON `rideshare`.`analysis` TO 'carter'; -
现在,让我们以新用户身份登录并验证我们是否可以访问视图。我们现在能够运行
USE `rideshare`; -
然而,这个用户可以访问的数据库部分只有
analysis视图。我们现在可以看到这个视图中的数据,但不能从原始的rides表中看到!我们刚刚展示了 MySQL 访问控制的好处:我们可以让多个用户访问数据库,但只允许一些用户访问机密数据。
SQL 注入攻击
-
提高我们数据库安全性的方法之一是使用访问控制和仅向每个用户授予必要的权限。然而,使用 SQL 数据库的应用程序也可能受到攻击——其中之一就是 SQL 注入攻击。
-
正如其名所示,这涉及到一个恶意用户注入一些 SQL 短语,以在我们的应用程序中以不希望的方式完成现有查询。
-
例如,一个要求用户使用用户名和密码登录的网站可能在数据库上运行如下查询。
SELECT `id` FROM `users` WHERE `user` = 'Carter' AND `password` = 'password'; -
在上面的例子中,用户 Carter 像往常一样输入了他们的用户名和密码。然而,一个恶意用户可能会输入不同的内容,比如字符串“password’ OR ‘1’ = 1”作为他们的密码。在这种情况下,他们试图获取用户和密码的整个数据库的访问权限。
SELECT `id` FROM `users` WHERE `user` = 'Carter' AND `password` = 'password' OR '1' = '1'; -
在 MySQL 中,我们可以使用预编译语句来防止 SQL 注入攻击。让我们用之前创建的用户连接到 MySQL 并切换到
bank数据库。 -
一个可以运行的 SQL 注入攻击示例,可以用来显示
accounts表中的所有用户账户。SELECT * FROM `accounts` WHERE `id` = 1 UNION SELECT * FROM `accounts`; -
预编译语句是 SQL 中的一个语句,我们可以在稍后插入值。对于上面的查询,我们可以编写一个预编译语句。
PREPARE `balance_check` FROM 'SELECT * FROM `accounts` WHERE `id` = ?';预编译语句中的问号充当防止意外执行 SQL 代码的安全措施。
-
要实际运行这个语句并检查某人的余额,我们接受用户输入作为变量,然后将其插入到预编译语句中。
SET @id = 1; EXECUTE `balance_check` USING @id;在上面的代码中,想象一下
SET语句是通过应用程序获取用户的 ID!@是 MySQL 中变量的约定。 -
预编译语句清理输入以确保没有恶意 SQL 代码被注入。让我们尝试运行上面相同的语句,但使用一个恶意的 ID。
SET @id = '1 UNION SELECT * FROM `accounts`'; EXECUTE `balance_check` USING @id;这也给出了与之前代码相同的结果——它显示了 ID 为 1 的用户的余额,没有其他内容!因此,我们已经防止了可能的 SQL 注入攻击。
问题
在这个预编译语句的例子中,它是否只考虑了变量中的第一个条件?
- 预编译语句执行一种称为转义的操作。它找到变量中可能恶意的所有部分,并将它们转义,这样它们实际上就不会被执行。
这是否与我们在 Python 中执行 SQL 查询时不应该使用格式化字符串的原因相似?
- 是的,Python 中的格式字符串也有同样的陷阱,它们容易受到 SQL 注入攻击。
结束
- 这把我们带到了第六讲关于 SQL 缩放和这门课程——CS50 的 SQL 数据库入门——的结论!
网络
第零讲
-
简介
-
Web 编程
-
HTML(超文本标记语言)
-
文档对象模型 (DOM)
-
更多 HTML 元素
-
表单
-
-
CSS(层叠样式表)
-
响应式设计
-
Bootstrap
-
Sass(语法上出色的样式表)
简介
在本课程中,我们将从 CS50 停止的地方继续,深入到网络应用程序的设计和创建。我们将通过在课程中进行多个项目的工作来培养我们的网页设计技能,包括一个开放式的最终项目,您将有机会创建自己的网站!
在本课程中,您需要一个文本编辑器,您可以在计算机上本地编写代码。一些流行的选择包括 Visual Studios Code、Sublime Text、Atom 和 Vim,但可供选择还有很多!
Web 编程
课程主题: 我们将在稍后进行更详细的介绍,但以下是本课程我们将要工作的简要概述:
-
HTML 和 CSS(一种用于概述网页的标记语言,以及使我们的网站更具视觉吸引力的方法)
-
Git(用于版本控制和协作)
-
Python(一种广泛使用的编程语言,我们将用它来使我们的网站更具动态性)
-
Django(我们将用于网站后端的流行网络框架)
-
SQL、模型和迁移(一种用于存储和检索数据的语言,以及使与 SQL 数据库交互更简单的 Django 特定方法)
-
JavaScript(一种用于使网站更快、更互动的编程语言)
-
用户界面(用于使网站尽可能易于使用的各种方法)
-
测试、CI、CD(了解用于确保网页更新顺利进行的各种方法)
-
可扩展性和安全性(确保我们的网站可以同时被许多用户访问,并且它们免受恶意意图的侵害)
HTML(超文本标记语言)
-
HTML 是一种标记语言,用于定义网页的结构。它由您的网页浏览器(Safari、Google Chrome、Firefox 等)解释,以便在屏幕上显示内容。
-
让我们从编写一个简单的 HTML 文件开始吧!
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello!</title>
</head>
<body>
Hello, world!
</body>
<html>
- 当我们在浏览器中打开这个文件时,我们得到:

-
现在,让我们花一些时间来谈谈我们刚刚编写的文件,它对于一个如此简单的页面来说似乎相当复杂。
-
在第一行,我们正在声明(对浏览器来说)我们正在使用 HTML 的最新版本:HTML5。
-
之后,页面由嵌套的HTML 元素(如
html和body)组成,每个元素都有一个开始和结束标签,分别用<element>表示开始和</element>表示结束。 -
注意到每个内部元素都比上一个元素缩进得更深一些。虽然浏览器不一定要求这样做,但这在您的代码中保持这种缩进将非常有帮助。
-
HTML 元素可以包括属性,这些属性为浏览器提供了关于元素的额外信息。例如,当我们把
lang="en"包含在我们的初始标签中时,我们是在告诉浏览器我们正在使用英语作为我们的主要语言。 -
在 HTML 元素内部,我们通常希望包含一个
head标签和一个body标签。head标签将包含关于您页面的信息,这些信息不一定显示,而body标签将包含访问网站的用户实际看到的内容。 -
在
head内部,我们为我们的网页添加了一个title,您会注意到它显示在浏览器顶部的标签上。 -
最后,我们在主体中包含了文本“Hello, world!”,这是页面的可见部分。
-
文档对象模型 (DOM)

- DOM 是一种方便的方式来使用树状结构可视化 HTML 元素之间的关系。上面是我们刚刚编写的页面的 DOM 布局示例。
更多 HTML 元素
-
您可能想要使用许多 HTML 元素来自定义您的页面,包括标题、列表和加粗部分。在接下来的示例中,我们将看到其中的一些实际应用。
-
另一点需要注意的是:
<!-- -->在 HTML 中提供了注释,因此我们将在下面使用它来解释一些元素。
<!DOCTYPE html>
<html lang="en">
<head>
<title>HTML Elements</title>
</head>
<body>
<!-- We can create headings using h1 through h6 as tags. -->
<h1>A Large Heading</h1>
<h2>A Smaller Heading</h2>
<h6>The Smallest Heading</h6>
<!-- The strong and i tags give us bold and italics respectively. -->
A <strong>bold</strong> word and an <i>italicized</i> word!
<!-- We can link to another page (such as cs50's page) using a. -->
View the <a href="https://cs50.harvard.edu/">CS50 Website</a>!
<!-- We used ul for an unordered list and ol for an ordered one. both ordered and unordered lists contain li, or list items. -->
An unordered list:
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ul>
An ordered list:
<ol>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ol>
<!-- Images require a src attribute, which can be either the path to a file on your computer or the link to an image online. It also includes an alt attribute, which gives a description in case the image can't be loaded. -->
An image:
<img src="../../images/duck.jpeg" alt="Rubber Duck Picture">
<!-- We can also see above that for some elements that don't contain other ones, closing tags are not necessary. -->
<!-- Here, we use a br tag to add white space to the page. -->
<br/> <br/>
<!-- A few different tags are necessary to create a table. -->
<table>
<thead>
<th>Ocean</th>
<th>Average Depth</th>
<th>Maximum Depth</th>
</thead>
<tbody>
<tr>
<td>Pacific</td>
<td>4280 m</td>
<td>10911 m</td>
</tr>
<tr>
<td>Atlantic</td>
<td>3646 m</td>
<td>8486 m</td>
</tr>
</tbody>
</table>
</body>
<html>
当这个页面渲染时,看起来就像这样:

- 如果您对此感到担忧,请知道您永远不需要记住这些元素。简单地搜索“HTML 中的图片”就能找到
img标签,这非常容易。学习这些元素的一个特别有用的资源是W3 Schools。
表单
-
在创建网站时,另一组非常重要的元素是如何从用户那里收集信息。您可以使用 HTML 表单允许用户输入信息,该表单可以包含几种不同类型的输入。在课程的后期,我们将学习如何处理表单提交后的信息。
-
正如其他 HTML 元素一样,您不需要记住这些,W3 Schools 是学习这些元素的一个很好的资源!
<!DOCTYPE html>
<html lang="en">
<head>
<title>Forms</title>
</head>
<body>
<form>
<input type="text" placeholder="First Name" name="first">
<input type="password" placeholder="Password" name="password">
<div>
Favorite Color:
<input name="color" type="radio" value="blue"> Blue
<input name="color" type="radio" value="green"> Green
<input name="color" type="radio" value="yellow"> Yellow
<input name="color" type="radio" value="red"> Red
</div>
<input type="submit">
</form>
</body>
</html>

CSS(层叠样式表)
-
CSS 用于自定义网站的样式。
-
在我们刚开始的时候,我们可以向任何 HTML 元素添加一个 style 属性,以便将其应用于一些 CSS。
-
我们通过改变元素的 CSS 属性来更改样式,例如写
color: blue或text-align: center。 -
在下面的示例中,我们对我们的第一个文件进行了一些微小的修改,以使其标题更加多彩:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello!</title>
</head>
<body>
<h1 style="color: blue; text-align: center;">A Colorful Heading!</h1>
Hello, world!
</body>
<html>

- 如果我们为一个外部元素进行样式设置,所有内部元素将自动采用该样式。如果我们把刚才应用在标题标签上的样式移动到
body标签上,我们就可以看到这一点:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello!</title>
</head>
<body style="color: blue; text-align: center;">
<h1 >A Colorful Heading!</h1>
Hello, world!
</body>
<html>

-
虽然我们可以像上面那样为我们的网页进行样式设置,但要实现更好的设计,我们应该能够将样式从单个行中移除。
- 一种方法是在
head中的<style>标签之间添加你的样式。在这些标签内部,我们写上我们想要样式的元素类型以及我们希望应用给它们的样式。例如:
<html lang="en"> <!DOCTYPE html> <head> <title>Hello!</title> <style> h1 { color: blue; text-align: center; } </style> </head> <body> <h1 >A Colorful Heading!</h1> Hello, world! </body> </html>- 另一种方法是在
head中包含一个指向包含一些样式的styles.css文件的<link>元素。这意味着 HTML 文件将看起来像这样:
<html lang="en"> <!DOCTYPE html> <head> <title>Hello!</title> <link rel="stylesheet" href="styles.css"> </head> <body> <h1 >A Colorful Heading!</h1> Hello, world! </body> </html>我们名为
styles.css的文件将看起来像这样:h1 { color: blue; text-align: center; } - 一种方法是在
-
这里的 CSS 属性太多,无法一一介绍,但就像 HTML 元素一样,通常很容易在 Google 上搜索类似“将字体改为蓝色 CSS”的内容来获取结果。其中一些最常见的是:
-
color: 文本的颜色 -
text-align: 元素在页面上的放置位置 -
background-color: 可以设置为任何颜色 -
width: 以像素或页面百分比为单位 -
height: 以像素或页面百分比为单位 -
padding: 在元素内部应留出多少空间 -
margin: 在元素外部应留出多少空间 -
font-family: 页面上文本的字体类型 -
font-size: 以像素为单位 -
border: 大小、类型(实线、虚线等)和颜色
-
-
让我们利用我们刚刚学到的知识来改进上面的海洋表格。以下是一些 HTML 代码作为起点:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Nicer Table</title>
</head>
<body>
<table>
<thead>
<th>Ocean</th>
<th>Average Depth</th>
<th>Maximum Depth</th>
</thead>
<tbody>
<tr>
<td>Pacific</td>
<td>4280 m</td>
<td>10911 m</td>
</tr>
<tr>
<td>Atlantic</td>
<td>3646 m</td>
<td>8486 m</td>
</tr>
</tbody>
</table>
</body>
<html>

- 上面看起来和之前我们有的很相似,但现在,通过在头部元素中包含一个
style标签或一个指向样式表的link,我们添加以下 CSS:
table {
border: 1px solid black;
border-collapse: collapse;
}
td {
border: 1px solid black;
padding: 2px;
}
th {
border: 1px solid black;
padding: 2px;
}
这使我们得到了这个看起来更漂亮的表格:

- 你可能已经想到了,我们当前的 CSS 中存在一些不必要的重复,因为
td和th具有相同的样式。我们可以(并且应该)将其压缩为以下代码,使用逗号来表示样式应应用于多个元素类型。
table {
border: 1px solid black;
border-collapse: collapse;
}
td, th {
border: 1px solid black;
padding: 2px;
}
-
这是对所谓的CSS 选择器的良好介绍。有许多方法可以确定你正在样式的 HTML 元素,其中一些我们将在下面提到:
-
元素类型:这是我们迄今为止一直在做的事情:为相同类型的所有元素进行样式设置。
-
ID: 另一个选择是给我们的 HTML 元素一个 ID,如下所示:
<h1 id="first-header">Hello!</h1>然后使用#first-header{...}通过使用井号来显示我们正在通过 ID 进行搜索。重要的是,没有两个元素可以有相同的 ID,并且没有元素可以有多个 ID。 -
类: 这与 ID 类似,但类可以被多个元素共享,并且单个元素可以有多个类。我们像这样给 HTML 元素添加类:
<h1 class="page-text muted">Hello!</h1>(注意我们只给元素添加了两个类:page-text和muted)。然后我们根据类使用点而不是井号进行样式化:.muted {...}
-
-
现在,我们还得处理可能冲突的 CSS 问题。当标题应该根据其类名是红色,但根据其 ID 是蓝色时会发生什么?CSS 有一个特定的顺序:
-
内联样式
-
id
-
class
-
元素类型
-
-
除了逗号用于多个选择器之外,还有几种其他方式可以指定您想要样式的元素。这个来自讲座的表格提供了一些,我们将在下面通过几个示例进行说明:

后代选择器: 在这里,我们使用后代选择器来仅对位于无序列表中的列表项应用样式:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Using Selectors</title>
<style>
ul li {
color: blue;
}
</style>
</head>
<body>
<ol>
<li>foo</li>
<li> bar
<ul>
<li>hello</li>
<li>goodbye</li>
<li>hello</li>
</ul>
</li>
<li>baz</li>
</ol>
</body>
<html>

属性作为选择器: 我们还可以根据分配给 HTML 元素的属性来缩小我们的选择范围,使用方括号。例如,在以下链接列表中,我们选择仅使指向亚马逊的链接变红:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Using Selectors</title>
<style>
a[href="https://www.amazon.com/"] {
color: red;
}
</style>
</head>
<body>
<ol>
<li><a href="https://www.google.com/">Google</a></li>
<li><a href="https://www.amazon.com/">Amazon</a> </li>
<li><a href="https://www.facebook.com/">Facebook</a></li>
</ol>
</body>
<html>

-
我们不仅可以使用 CSS 永久改变元素的外观,还可以改变其在特定条件下的外观。例如,如果我们想让按钮在鼠标悬停时改变颜色怎么办?我们可以通过使用CSS 伪类来实现这一点,它会在特殊情况下提供额外的样式。我们通过在选择器后添加冒号,然后在该冒号后添加情况来实现这一点。
-
在按钮的情况下,我们会在按钮选择器中添加
:hover以指定仅在悬停时的设计:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Pseudoclasses</title>
<style>
button {
background-color: red;
width: 200px;
height: 50px;
font-size: 24px;
}
button:hover {
background-color: green;
}
</style>
</head>
<body>
<button>Button 1</button>
<button>Button 2</button>
<button>Button 3</button>
</body>
<html>

响应式设计
-
现在,许多人使用除电脑以外的设备浏览网站,例如智能手机和平板电脑。确保您的网站对所有设备上的用户都是可读的非常重要。
-
我们可以通过了解视口来实现这一点。视口是屏幕上在任何给定时间内对用户实际可见的部分。默认情况下,许多网页假设视口在所有设备上都是相同的,这就是导致许多网站(尤其是较老的网站)在移动设备上难以交互的原因。
-
在移动设备上改善网站外观的一个简单方法是在我们 HTML 文件的头部添加以下行。这一行告诉移动设备使用一个与您使用的设备相同宽度的视口,而不是一个更大的视口。
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
另一种处理不同设备的方法是通过媒体查询。媒体查询是一种根据页面如何被查看来改变页面样式的方法。
-
以下是一个媒体查询的例子,让我们尝试在屏幕缩小到一定大小时简单地改变屏幕颜色。我们通过输入
@media后跟括号中的查询类型来表示媒体查询:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Screen Size</title>
<style>
@media (min-width: 600px) {
body {
background-color: red;
}
}
@media (max-width: 599px) {
body {
background-color: blue;
}
}
</style>
</head>
<body>
<h1>Welcome to the page!</h1>
</body>
</html>

- 处理不同屏幕尺寸的另一种方法是使用一个新的 CSS 属性,称为flexbox。这允许我们在元素水平方向上不适应时轻松地将它们包裹到下一行。我们通过将所有元素放入一个我们称之为容器的
div中来实现这一点。然后我们添加一些样式到这个div中,指定我们想要在其中的元素上使用 flexbox 显示。我们还添加了一些额外的样式到内部div中,以更好地说明这里发生的包裹。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Screen Size</title>
<style>
#container {
display: flex;
flex-wrap: wrap;
}
#container > div {
background-color: green;
font-size: 20px;
margin: 20px;
padding: 20px;
width: 200px;
}
</style>
</head>
<body>
<div id="container">
<div>Some text 1!</div>
<div>Some text 2!</div>
<div>Some text 3!</div>
<div>Some text 4!</div>
<div>Some text 5!</div>
<div>Some text 6!</div>
<div>Some text 7!</div>
<div>Some text 8!</div>
<div>Some text 9!</div>
<div>Some text 10!</div>
<div>Some text 11!</div>
<div>Some text 12!</div>
</div>
</body>
</html>

- 另一种流行的页面样式方法是使用 HTML 网格。在这个网格中,我们可以指定样式属性,如列宽和列与行之间的间隙,如下所示。注意,当我们指定列宽时,我们说第三个是
auto,这意味着它应该填充页面的剩余部分。
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Web Page!</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.grid {
background-color: green;
display: grid;
padding: 20px;
grid-column-gap: 20px;
grid-row-gap: 10px;
grid-template-columns: 200px 200px auto;
}
.grid-item {
background-color: white;
font-size: 20px;
padding: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="grid">
<div class="grid-item">1</div>
<div class="grid-item">2</div>
<div class="grid-item">3</div>
<div class="grid-item">4</div>
<div class="grid-item">5</div>
<div class="grid-item">6</div>
<div class="grid-item">7</div>
<div class="grid-item">8</div>
<div class="grid-item">9</div>
<div class="grid-item">10</div>
<div class="grid-item">11</div>
<div class="grid-item">12</div>
</div>
</body>
</html>

Bootstrap
-
结果表明,有许多库是其他人已经编写的,可以使网页的样式化更加简单。在本课程中,我们将使用的一个流行库被称为bootstrap。
-
我们可以通过在 HTML 文件的头部添加一行来将 Bootstrap 包含到我们的代码中:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
-
接下来,我们可以通过导航到他们网站的文档部分来查看一些 Bootstrap 的特性。在这个页面上,你会找到许多可以添加到元素上的类,这些类允许使用 Bootstrap 进行样式化。
-
Bootstrap 的一个流行特性是它们的网格系统。Bootstrap 自动将页面分成 12 列,我们可以通过添加类
col-x(其中x是 1 到 12 之间的数字)来决定一个元素占用多少列。例如,在以下页面中,我们有一行等宽的列,然后是一行中间的列更宽:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Web Page!</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<style>
.row > div {
padding: 20px;
background-color: teal;
border: 2px solid black;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-4">
This is a section.
</div>
<div class="col-4">
This is another section.
</div>
<div class="col-4">
This is a third section.
</div>
</div>
</div>
<br/>
<div class="container">
<div class="row">
<div class="col-3">
This is a section.
</div>
<div class="col-6">
This is another section.
</div>
<div class="col-3">
This is a third section.
</div>
</div>
</div>
</body>
</html>

- 为了提高移动端的响应性,Bootstrap 还允许我们根据屏幕大小指定不同的列宽。在下面的示例中,我们使用
col-lg-3来表示在大型屏幕上元素应占用 3 列,而col-sm-6表示在屏幕较小时元素应占用 6 列:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Web Page!</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<style>
.row > div {
padding: 20px;
background-color: teal;
border: 2px solid black;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-lg-3 col-sm-6">
This is a section.
</div>
<div class="col-lg-3 col-sm-6">
This is another section.
</div>
<div class="col-lg-3 col-sm-6">
This is a third section.
</div>
<div class="col-lg-3 col-sm-6">
This is a fourth section.
</div>
</div>
</div>
</body>
</html>

Sass(Syntactically Awesome Style Sheets)
-
到目前为止,我们已经找到了几种消除 CSS 中冗余的方法,例如将其移动到单独的文件或使用 Bootstrap,但仍有一些地方我们可以进行改进。例如,如果我们想让几个元素有不同的样式,但所有元素的颜色都相同,怎么办?如果我们后来决定要更改颜色,那么我们就需要在几个不同的元素中更改它。
-
Sass 是一种语言,它以多种方式使我们能够更有效地编写 CSS,其中之一就是允许我们使用变量,如下面的示例所示。
-
当使用 Sass 编写代码时,我们创建一个扩展名为
filename.scss的新文件。在这个文件中,我们可以通过在名称前添加一个$符号,然后是一个冒号,再然后是一个值来创建一个新的变量。例如,我们可以写$color: red来设置变量color的值为红色。然后我们使用$color来访问这个变量。以下是我们variables.scss文件的一个示例:
$color: red;
ul {
font-size: 14px;
color: $color;
}
ol {
font-size: 18px;
color: $color;
}
-
现在,为了将这种样式链接到我们的 HTML 文件,我们不能只是链接到
.scss文件,因为大多数网络浏览器只识别.css文件。为了解决这个问题,我们必须在我们的计算机上下载一个名为 Sass 的程序。下载 Sass。然后,在我们的终端中,我们输入sass variables.scss:variables.css命令。这个命令会将名为variables.scss的 .scss 文件编译成名为variables.css的 .css 文件,你可以在你的 HTML 页面中添加对该文件的链接。 -
为了加快这个流程,我们可以使用命令
sass --watch variables.scss:variables.css,这个命令会在检测到.scss文件中的更改时自动更改.css文件。 -
在使用 Sass 的同时,我们还可以物理地嵌套我们的样式,而不是使用我们之前提到的 CSS 选择器。例如,如果我们只想将一些样式应用于 div 中的段落和无序列表,我们可以编写以下代码:
div {
font-size: 18px;
p {
color: blue;
}
ul {
color: green;
}
}
一旦编译成 CSS,我们就会得到一个看起来像这样的文件:
div {
font-size: 18px;
}
div p {
color: blue;
}
div ul {
color: green;
}
- Sass 给我们的另一个特性被称为 继承。这允许我们创建一组基本的样式,可以被多个不同的元素共享。我们通过在类名前添加一个
%符号,添加一些样式,然后在某些样式的开头添加一行@extend %classname来实现这一点。例如,以下代码将message类内的样式应用于下面的每个不同类,从而生成一个看起来像下面的网页。
%message {
font-family: sans-serif;
font-size: 18px;
font-weight: bold;
border: 1px solid black;
padding: 20px;
margin: 20px;
}
.success {
@extend %message;
background-color: green;
}
.warning {
@extend %message;
background-color: orange;
}
.error {
@extend %message;
background-color: red;
}

- 今天的内容就到这里了!
第一讲
-
介绍
-
Git
-
GitHub
-
提交
-
合并冲突
-
分支
- 更多 GitHub 功能
介绍
欢迎回到第一讲!在第 0 讲中,我们介绍了 HTML、CSS 和 Sass 作为我们可以用来创建一些基本网页的工具。今天,我们将学习如何使用 Git 和 GitHub 来帮助我们开发网络编程应用。
Git
-
Git 是一个命令行工具,它将以多种方式帮助我们进行版本控制:
- 允许我们通过在特定时间点保存代码的快照来跟踪我们对代码所做的更改。
![更改文件]()
- 允许我们通过允许多个人从存储在网上的仓库中拉取信息和向仓库推送信息,轻松地在不同的人之间同步代码。
![多用户]()
-
允许我们在不影响主代码库的情况下,在不同的 分支 上进行代码更改和测试,然后将两个分支合并在一起。
-
允许我们在意识到我们犯了一个错误后,将代码回滚到之前的版本。
-
在上述解释中,我们使用了“仓库”这个词,我们还没有解释过。Git 仓库是一个文件位置,我们将存储与特定项目相关的所有文件。这些可以是远程的(存储在线上)或本地的(存储在你的电脑上)。
GitHub
-
GitHub 是一个网站,允许我们在网上远程存储 Git 仓库。
-
让我们从创建一个在线新仓库开始
-
确保你已经设置了 GitHub 账户。如果你还没有,你可以在 这里 创建一个。
-
点击右上角的 + 按钮,然后点击“新建仓库”
-
创建一个描述你项目的仓库名称
-
(可选)为你的仓库提供描述
-
选择仓库应该是公开的(对任何在网络上的人可见)还是私有的(仅对你和你特别授权的人可见)
-
(可选)决定你是否想添加一个 README 文件,这是一个描述你的新仓库的文件。
![新仓库演示]()
-
-
一旦我们有了仓库,我们可能还想向其中添加一些文件。为了做到这一点,我们将创建一个新的 远程 仓库的副本,或者克隆,将其作为我们电脑上的 本地 仓库。
-
通过在终端中输入
git来确保你的电脑上已安装 git。如果没有安装,你可以从这里下载它 这里。 -
点击你仓库页面上的绿色“克隆或下载”按钮,并复制弹出的 url。如果你没有创建一个 README,这个链接将出现在页面顶部的“快速设置”部分。
![克隆和添加]()
-
在你的终端中,运行
git clone <repository url>。这将把仓库下载到你的电脑上。如果你没有创建一个 README 文件,你会收到警告:You appear to have cloned into an empty repository.这是正常的,无需为此担心。![克隆演示]()
-
运行
ls,这是一个列出你当前目录中所有文件和文件夹的命令。你应该能看到你刚刚克隆的仓库的名称。 -
运行
cd <repository name>来更改目录到那个文件夹。 -
运行
touch <new file name>在该文件夹中创建一个新文件。你现在可以编辑该文件。或者,你也可以在你的文本编辑器中打开该文件夹并手动添加新文件。 -
现在,为了让 Git 知道它应该跟踪你新创建的文件,运行
git add <new file name>来跟踪那个特定的文件,或者运行git add .来跟踪该目录下的所有文件。![同一时间]()
-
提交
-
现在,我们将开始了解 Git 真正有用的功能。在修改一个文件后,我们可以 提交 这些更改,对代码的当前状态进行快照。为此,我们运行:
git commit -m "some message",其中消息描述了你刚刚所做的更改。 -
在此更改之后,我们可以运行
git status来查看我们的代码与远程仓库上的代码如何比较。 -
当我们准备好将本地提交发布到 GitHub 时,我们可以运行
git push。现在,当我们用网络浏览器访问 GitHub 时,我们的更改将会反映出来。 -
如果你只修改了现有文件而没有创建新文件,我们不需要使用
git add .然后执行git commit...,我们可以将这压缩成一个命令:git commit -am "some message"。这个命令将提交你做的所有更改。 -
有时候,GitHub 上的远程仓库可能比本地版本更新。在这种情况下,你首先需要提交任何更改,然后运行
git pull将任何远程更改拉到你的仓库中。
合并冲突
-
当使用 Git 工作时,特别是当你与其他人协作时,可能会出现一个称为 合并冲突 的问题。合并冲突发生在两个人试图以相互冲突的方式更改文件时。
-
这通常发生在你执行
git push或git pull时。当这种情况发生时,Git 会自动将文件转换为一种格式,清楚地说明冲突是什么。以下是一个示例,其中相同的行以两种不同的方式被添加:
a = 1
<<<<< HEAD
b = 2
=====
b = 3
>>>>> 56782736387980937883
c = 3
d = 4
e = 5
-
在上面的例子中,你添加了
b = 2这一行,而另一个人写下了b = 3,现在我们必须选择其中一个来保留。这个长数字是一个 哈希,它代表与你的编辑冲突的提交。许多文本编辑器也会提供高亮显示和简单的选项,例如“接受当前”或“接受传入”,这可以节省你删除上面添加的行的时间。 -
另一个可能很有用的 git 命令是
git log,它为你提供了在该仓库上所有提交的历史记录。

-
如果你意识到你犯了一个错误,你可以使用命令
git reset以两种方式之一回滚到之前的提交:-
git reset --hard <commit>将你的代码回滚到指定提交后的确切状态。要指定提交,请使用与提交关联的提交哈希,这可以通过如上所示的git log查找。 -
git reset --hard origin/master将你的代码回滚到目前在 Github 上存储的版本。
-
分支
在你为一个项目工作了一段时间之后,你可能决定想要添加一个额外的功能。目前,我们可能只是像下面图形所示那样提交这个新功能的更改。

但如果我们随后发现原始代码中有一个错误,并想要回滚而不更改新功能,这可能会变得有问题。这就是分支变得非常有用的地方。
- 分支是在创建新功能时转向新方向的一种方法,一旦完成,只将这个新功能与你的代码的主要部分或主分支结合起来。这个工作流程看起来更像是下面的图形:

-
你目前正在查看的分支是由
HEAD决定的,它指向两个分支中的一个。默认情况下,HEAD指向主分支,但我们可以检出其他分支。 -
现在,让我们深入了解如何在我们的 git 仓库中实际实现分支:
- 运行
git branch来查看你目前正在工作的分支,它在其名称左侧会有一个星号。
![分支终端]()
- 要创建一个新分支,我们将运行
git checkout -b <新分支名称>
![新分支]()
-
使用命令
git checkout <分支名称>在分支之间切换,并对每个分支提交任何更改。 -
当我们准备好合并两个分支时,我们将检出我们希望保留的分支(几乎总是主分支),然后运行命令
git merge <其他分支名称>。这将被处理得类似于推送或拉取,并且可能会出现合并冲突。
- 运行
更多 GitHub 功能
有一些特定于 GitHub 的有用功能,可以在你工作在项目时提供帮助:
-
Forking:作为一个 GitHub 用户,你有权限“Fork”任何你能够访问的仓库,这将创建一个属于你的仓库的副本。我们通过点击右上角的“Fork”按钮来完成这个操作。
-
Pull Requests:一旦你 fork 了一个仓库并对你的版本进行了更改,你可能希望请求将这些更改添加到仓库的主版本中。例如,如果你想向 Bootstrap 添加一个新功能,你可以 fork 仓库,进行一些更改,然后提交一个 pull request。这个 pull request 然后可以被 Bootstrap 仓库的管理人员评估,并可能被接受。人们进行一些编辑然后请求将它们合并到主仓库的过程对于所谓的“开源软件”至关重要,或者说是由多个开发者的贡献创建的软件。
-
GitHub Pages:GitHub Pages 是一种简单的方式来将静态站点发布到网络上。(我们稍后会学习静态站点与动态站点的区别。)为了做到这一点:
-
创建一个新的 GitHub 仓库。
-
克隆仓库并在本地进行更改,确保包含一个
index.html文件,这将是你网站的着陆页。 -
将这些更改推送到 GitHub。
-
导航到你的仓库的设置页面,滚动到 GitHub Pages,并在下拉菜单中选择 master 分支。
-
滚动到设置页面中的 GitHub Pages 部分,几分钟后,你应该会看到一个通知,显示“您的站点已发布在:…”,包括一个你可以找到你站点的 URL!
-
这节课的内容就到这里!下次,我们将探讨 Python!
第二讲
-
简介
-
Python
-
变量
-
字符串格式化
-
条件
-
序列
-
字符串
-
列表
-
元组
-
集合
-
字典
-
循环
-
-
函数
-
模块
-
面向对象编程
-
函数式编程
-
装饰器
-
Lambda 函数
-
-
异常
简介
-
到目前为止,我们已经讨论了如何使用 HTML 和 CSS 构建简单的网页,以及如何使用 Git 和 GitHub 来跟踪代码的更改并与他人协作。
-
今天,我们将深入探讨 Python,这是我们将在整个课程中使用的两种主要编程语言之一。
Python

-
Python 是一种非常强大且广泛使用的语言,它将使我们能够快速构建相当复杂的网络应用程序。在本课程中,我们将使用 Python 3,尽管 Python 2 在某些地方仍在使用。在查看外部资源时,请务必确保它们使用的是同一版本。
-
让我们从许多编程语言开始的地方开始:Hello, world。这个用 Python 编写的程序看起来是这样的:
print("Hello, world!")
-
要分解那行代码中发生的事情,Python 语言内置了一个名为
print的函数,它接受括号中的参数,并在命令行上显示该参数。 -
要在实际的计算机上编写和运行此程序,你首先需要将此行输入你选择的文本编辑器中,然后将文件保存为
something.py。接下来,你将前往终端,导航到包含你的文件的目录,并输入python something.py。在上面的程序中,单词“Hello, world!”将随后在终端中显示。 -
根据你的计算机配置,你可能需要在文件名前输入
python3而不是python,如果你还没有安装 Python,你可能甚至需要下载 Python。安装 Python 后,我们建议你还要下载 Pip,因为你在课程中稍后需要它。 -
当你在终端中输入
python file.py时,一个名为解释器的程序(你与 Python 一起下载的)会逐行读取你的文件,并执行每一行代码。这与像C或Java这样的语言不同,这些语言在运行之前需要编译成机器代码。
变量
任何编程语言的关键部分都是创建和操作变量的能力。为了在 Python 中给变量赋值,其语法看起来像这样:
a = 28
b = 1.5
c = "Hello!"
d = True
e = None
每一行都将=右侧的值取出来,并存储在左侧的变量名中。
与某些其他编程语言不同,Python 变量类型是推断的,这意味着虽然每个变量都有一个类型,但我们创建变量时不必明确声明它是哪种类型。最常见的变量类型包括:
-
int: 一个整数
-
float: 一个十进制数
-
str: 一个字符串,或字符序列
-
bool: 一个值为
True或False的值 -
NoneType: 表示没有值的一个特殊值(
None)。
现在,我们将编写一个更有趣的程序,可以从用户那里获取输入并问候该用户。为此,我们将使用另一个内置函数input,它向用户显示一个提示,并返回用户提供的任何输入。例如,我们可以在名为name.py的文件中编写以下内容:
name = input("Name: ")
print("Hello, " + name)
当在终端上运行时,程序看起来是这样的:

在这里有一些要点需要指出:
-
在第一行中,我们不是将变量名赋给一个显式的值,而是将其赋给
input函数返回的任何值。 -
在第二行中,我们使用
+运算符来组合,或连接两个字符串。在 Python 中,+运算符可以用来加数字或连接字符串和列表。
格式化字符串
-
虽然我们可以使用
+运算符来组合字符串,就像我们上面做的那样,但在 Python 的最新版本中,还有更简单的方法来处理字符串,称为格式化字符串,或简称为f-字符串。 -
要表示我们正在使用格式化字符串,我们只需在引号前添加一个
f。例如,我们可以在"Hello, " + name的用法上写f"Hello, {name}"以获得相同的结果。我们甚至可以在这个字符串中插入一个函数,并将我们上面的程序转换为一行:
print(f"Hello, {input("Name: ")}")
条件
- 就像在其他编程语言中一样,Python 让我们能够根据不同的条件运行不同的代码段。例如,在下面的程序中,我们将根据用户输入的数字改变我们的输出:
num = input("Number: ")
if num > 0:
print("Number is positive")
elif num < 0:
print("Number is negative")
else:
print("Number is 0")
-
了解上述程序的工作原理,Python 中的条件语句包含一个关键字(
if、elif或else),然后(除了else情况外)是一个布尔表达式,或者是一个评估为True或False的表达式。然后,我们想要在某个表达式为真时运行的代码将直接缩进在语句下方。缩进是 Python 语法的一部分。 -
然而,当我们运行这个程序时,我们会遇到一个异常,看起来像这样:

-
当我们在运行 Python 代码时发生错误,异常就会发生,随着时间的推移,你会越来越擅长解释这些错误,这是一个非常有价值的技能。
-
让我们更仔细地看看这个特定的异常:如果我们看到底部,我们会看到我们遇到了一个
TypeError,这通常意味着 Python 期望某个变量是某种类型,但发现它是另一种类型。在这种情况下,异常告诉我们我们不能使用>符号来比较一个str和一个int,然后在上面的代码行 2 中我们可以看到这个比较发生了。 -
在这种情况下,很明显
0是一个整数,所以我们的num变量必须是字符串。这是因为input函数总是返回一个字符串,我们必须指定使用int函数将其转换为(或强制类型转换为)整数。这意味着我们的第一行现在看起来像这样:
num = int(input("Number: "))
- 现在,程序将按我们预期的那样工作!
序列
Python 语言最强大的部分之一是它能够处理数据序列,而不仅仅是单个变量。
几种序列在某些方面相似,但在其他方面不同。在解释这些差异时,我们将使用术语可变/不可变和有序/无序。可变意味着一旦定义了一个序列,我们就可以改变该序列的各个元素,而有序意味着对象的顺序很重要。
字符串
有序: 是
可变: 否
我们已经稍微了解了一些字符串,但除了变量之外,我们可以将字符串视为字符序列。这意味着我们可以在字符串中访问单个元素!例如:
name = "Harry"
print(name[0])
print(name[1])
打印出字符串中的第一个(或索引-0)字符,在这个例子中恰好是H,然后打印出第二个(或索引-1)字符,它是a。
列表
有序: 是
可变: 是
Python 列表允许你存储任何变量类型。我们使用方括号和逗号创建列表,如下所示。类似于字符串,我们可以打印整个列表,或者打印一些单个元素。我们还可以使用append向列表中添加元素,并使用sort对列表进行排序。
# This is a Python comment names = ["Harry", "Ron", "Hermione"]
# Print the entire list: print(names)
# Print the second element of the list: print(names[1])
# Add a new name to the list: names.append("Draco")
# Sort the list: names.sort()
# Print the new list: print(names)

元组
有序: 是
可变: 否
元组通常用于需要存储两个或三个值的情况,例如一个点的 x 和 y 值。在 Python 代码中,我们使用括号:
point = (12.5, 10.6)
集合
有序: 否
可变: 不适用
集合与列表和元组不同,因为它们是无序的。它们还不同,因为虽然你可以在列表/元组中包含两个或更多相同的元素,但集合只会存储每个值一次。我们可以使用set函数定义一个空集合。然后我们可以使用add和remove向集合中添加和删除元素,并使用len函数来找到集合的大小。请注意,len函数在 Python 的所有序列中都有效。另外,尽管我们两次向集合中添加了4和3,但每个项目在集合中只能出现一次。
# Create an empty set: s = set()
# Add some elements: s.add(1)
s.add(2)
s.add(3)
s.add(4)
s.add(3)
s.add(1)
# Remove 2 from the set s.remove(2)
# Print the set: print(s)
# Find the size of the set: print(f"The set has {len(s)} elements.")
""" This is a python multi-line comment:
Output:
{1, 3, 4}
The set has 3 elements. """
字典
有序:否
可变:是
Python 字典或dict在本课程中特别有用。字典是一组键值对,其中每个键都有一个相应的值,就像字典中的每个词(键)都有一个相应的定义(值)。在 Python 中,我们使用花括号来包含字典,并使用冒号来表示键和值。例如:
# Define a dictionary houses = {"Harry": "Gryffindor", "Draco": "Slytherin"}
# Print out Harry's house print(houses["Harry"])
# Adding values to a dictionary: houses["Hermione"] = "Gryffindor"
# Print out Hermione's House: print(houses["Hermione"])
""" Output:
Gryffindor
Gryffindor """
循环
循环是任何编程语言中极其重要的部分,在 Python 中,它们主要有两种形式:for 循环和while 循环。目前,我们将专注于 for 循环。
- 循环用于遍历一系列元素,并对序列中的每个元素执行一些代码块(如下所示缩进)。例如,以下代码将打印出从 0 到 5 的数字:
for i in [0, 1, 2, 3, 4, 5]:
print(i)
""" Output:
0
1
2
3
4
5 """
- 我们可以使用 Python 的
range函数来简化这段代码,它允许我们轻松地获取一个数字序列。以下代码与上面的代码产生相同的结果:
for i in range(6):
print(i)
""" Output:
0
1
2
3
4
5 """
- 这种循环可以适用于任何序列!例如,如果我们想打印列表中的每个名字,我们可以编写以下代码:
# Create a list: names = ["Harry", "Ron", "Hermione"]
# Print each name: for name in names:
print(name)
""" Output:
Harry
Ron
Hermione """
- 如果我们想更具体一些,我们可以遍历单个名字中的每个字符!
name = "Harry"
for char in name:
print(char)
""" Output:
H
a
r
r
y """
函数
我们已经看到了一些 Python 函数,例如print和input,但现在我们将深入编写我们自己的函数。为了开始,我们将编写一个接受一个数字并将其平方的函数:
def square(x):
return x * x
注意我们如何使用def关键字来表示我们正在定义一个函数,我们正在接受一个名为x的单个输入,并且我们使用return关键字来表示函数的输出应该是什么。
我们可以像调用其他函数一样调用这个函数:使用括号:
for i in range(10):
print(f"The square of {i} is {square(i)}")
""" Output:
The square of 0 is 0
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 7 is 49
The square of 8 is 64
The square of 9 is 81 """
模块
随着我们的项目越来越大,能够在一个文件中编写函数并在另一个文件中运行它们将变得非常有用。在上面的例子中,我们可以创建一个名为functions.py的文件,其中包含以下代码:
def square(x):
return x * x
另一个名为square.py的文件,其中包含以下代码:
for i in range(10):
print(f"The square of {i} is {square(i)}")
然而,当我们尝试运行square.py时,我们遇到了以下错误:

我们遇到这个问题是因为默认情况下,Python 文件之间并不知道彼此,所以我们必须显式地 import 我们刚刚编写的 functions 模块中的 square 函数。现在,当 square.py 看起来像这样:
from functions import square
for i in range(10):
print(f"The square of {i} is {square(i)}")
或者,我们可以选择导入整个 functions 模块,然后使用点符号来访问 square 函数:
import functions
for i in range(10):
print(f"The square of {i} is {functions.square(i)}")
我们可以导入许多内置的 Python 模块,例如 math 或 csv,这些模块为我们提供了访问更多函数的权限。此外,我们还可以下载更多的模块来访问更多的功能!我们将花费大量时间使用 Django 模块,我们将在下一讲中讨论。
面向对象编程
面向对象编程是一种编程范式,或者说是关于编程的一种思考方式,它以可以存储信息和执行动作的对象为中心。
- 类:我们已经看到了 Python 中的一些不同类型的变量,但如果我们想创建自己的类型呢?一个 Python 类 实质上是一个新类型对象的模板,它可以存储信息并执行动作。以下是一个定义二维点的类:
class Point():
# A method defining how to create a point:
def __init__(self, x, y):
self.x = x
self.y = y
- 注意,在上面的代码中,我们使用关键字
self来表示我们正在处理的对象。self应该是 Python 类中任何方法的第一个参数。
现在,让我们看看我们如何实际使用上面的类来创建一个对象:
p = Point(2, 8)
print(p.x)
print(p.y)
""" Output:
2
8 """
现在,让我们看看一个更有趣的例子,在这个例子中,我们不仅存储一个点的坐标,而是创建一个表示航空公司航班的类:
class Flight():
# Method to create new flight with given capacity
def __init__(self, capacity):
self.capacity = capacity
self.passengers = []
# Method to add a passenger to the flight:
def add_passenger(self, name):
self.passengers.append(name)
然而,这个类是有缺陷的,因为我们虽然设定了容量,但我们仍然可能添加过多的乘客。让我们增强它,以便在添加乘客之前检查航班上是否有空位:
class Flight():
# Method to create new flight with given capacity
def __init__(self, capacity):
self.capacity = capacity
self.passengers = []
# Method to add a passenger to the flight:
def add_passenger(self, name):
if not self.open_seats():
return False
self.passengers.append(name)
return True
# Method to return number of open seats
def open_seats(self):
return self.capacity - len(self.passengers)
注意,在上面,我们使用 if not self.open_seats() 这一行来确定是否有空位。这之所以有效,是因为在 Python 中,数字 0 可以解释为 False 的意思,我们还可以使用关键字 not 来表示以下语句的相反,所以 not True 是 False,not False 是 True。因此,如果 open_seats 返回 0,整个表达式将评估为 True
现在,让我们通过实例化一些对象来尝试我们创建的类:
# Create a new flight with o=up to 3 passengers flight = Flight(3)
# Create a list of people people = ["Harry", "Ron", "Hermione", "Ginny"]
# Attempt to add each person in the list to a flight for person in people:
if flight.add_passenger(person):
print(f"Added {person} to flight successfully")
else:
print(f"No available seats for {person}")
""" Output:
Added Harry to flight successfully
Added Ron to flight successfully
Added Hermione to flight successfully
No available seats for Ginny """
函数式编程
除了支持面向对象编程,Python 还支持 函数式编程范式,在这个范式中,函数被当作值来对待,就像任何其他变量一样。
装饰器
函数式编程使得装饰器的概念成为可能,装饰器是一种高阶函数,可以修改另一个函数。例如,我们可以编写一个装饰器,用于在函数开始和结束时发出通知。然后,我们可以使用一个 @ 符号应用这个装饰器。
def announce(f):
def wrapper():
print("About to run the function")
f()
print("Done with the function")
return wrapper
@announce
def hello():
print("Hello, world!")
hello()
""" Output:
About to run the function
Hello, world!
Done with the function """
Lambda 函数
Lambda 函数为 Python 中创建函数提供了另一种方式。例如,如果我们想定义之前定义过的相同的square函数,我们可以这样写:
square = lambda x: x * x
其中输入位于:的左侧,输出位于右侧。
这在我们不想为单个、小规模的使用编写整个单独的函数时非常有用。例如,如果我们想要对一些对象进行排序,但一开始不清楚如何排序。想象一下,我们有一个包含人名和房屋的人名列表,但我们希望按人名排序:
people = [
{"name": "Harry", "house": "Gryffindor"},
{"name": "Cho", "house": "Ravenclaw"},
{"name": "Draco", "house": "Slytherin"}
]
people.sort()
print(people)
然而,这却给我们留下了错误:

这是因为 Python 不知道如何比较两个字典以检查一个是否小于另一个。
我们可以通过在排序函数中包含一个key参数来解决此问题,该参数指定我们希望用于排序的字典部分:
people = [
{"name": "Harry", "house": "Gryffindor"},
{"name": "Cho", "house": "Ravenclaw"},
{"name": "Draco", "house": "Slytherin"}
]
def f(person):
return person["name"]
people.sort(key=f)
print(people)
""" Output:
[{'name': 'Cho', 'house': 'Ravenclaw'}, {'name': 'Draco', 'house': 'Slytherin'}, {'name': 'Harry', 'house': 'Gryffindor'}] """
虽然这样做是可行的,但我们不得不编写一个只使用一次的整个函数,我们可以通过使用 lambda 函数来使我们的代码更易读:
people = [
{"name": "Harry", "house": "Gryffindor"},
{"name": "Cho", "house": "Ravenclaw"},
{"name": "Draco", "house": "Slytherin"}
]
people.sort(key=lambda person: person["name"])
print(people)
""" Output:
[{'name': 'Cho', 'house': 'Ravenclaw'}, {'name': 'Draco', 'house': 'Slytherin'}, {'name': 'Harry', 'house': 'Gryffindor'}] """
异常
在这次讲座中,我们遇到了几种不同的异常,所以现在我们将探讨一些处理它们的新方法。
在下面的代码块中,我们将从用户那里获取两个整数,并尝试除以它们:
x = int(input("x: "))
y = int(input("y: "))
result = x / y
print(f"{x} / {y} = {result}")
在许多情况下,这个程序运行良好:

然而,当我们尝试除以 0 时,我们会遇到问题:

我们可以使用异常处理来处理这种混乱的错误。在下面的代码块中,我们将尝试除以两个数字,如果遇到ZeroDivisionError,则except:
import sys
x = int(input("x: "))
y = int(input("y: "))
try:
result = x / y
except ZeroDivisionError:
print("Error: Cannot divide by 0.")
# Exit the program
sys.exit(1)
print(f"{x} / {y} = {result}")
在这种情况下,当我们再次尝试时:

然而,当用户输入非数字的 x 和 y 时,我们仍然会遇到错误:

我们可以用类似的方式解决这个问题!
import sys
try:
x = int(input("x: "))
y = int(input("y: "))
except ValueError:
print("Error: Invalid input")
sys.exit(1)
try:
result = x / y
except ZeroDivisionError:
print("Error: Cannot divide by 0.")
# Exit the program
sys.exit(1)
print(f"{x} / {y} = {result}")
这就是本次讲座的全部内容!下次,我们将使用 Python 的Django模块来构建一些应用程序!





















浙公网安备 33010602011771号