查看这些统计数据,以了解这个数据库有多大!它拥有的数据比我们迄今为止所使用的任何其他数据库都要多!
这里是详细说明实体及其关系的 ER 图。
.schema 显示了在这个数据库中创建的表。为了实现实体 Person 和 Movie 之间的多对多关系,我们在 ER 图中有一个联合表,称为 stars,它将 people 和 movies 的 ID 列作为外键列引用!
要查看 movies 表,我们可以从表中选择并限制结果。
在底层,当运行查找 Cars 的查询时,我们触发了对表 movies 的 扫描——也就是说,表 movies 是逐行从上到下扫描,以找到所有标题为 Cars 的行。
我们可以优化这个查询,使其比扫描更高效。就像教科书通常有索引一样,数据库表也可以有索引。在数据库术语中,索引是一种用于加速从表中检索行的结构。
我们可以使用以下命令为 movies 表中的 "title" 列创建索引。
在上一个例子中,一旦创建了索引,我们就假设 SQL 会使用它来查找电影。然而,我们也可以通过在查询之前使用 SQLite 命令EXPLAIN QUERY PLAN来明确地看到这一点。
要删除我们刚刚创建的索引,请运行:
数据库没有隐式算法来优化搜索吗?
是否建议为每个可能需要的列创建不同的索引?
我们将运行以下查询来找到汤姆·汉克斯主演的所有电影。
为了了解哪种索引可以帮助加快这个查询的速度,我们可以在查询之前再次运行EXPLAIN QUERY PLAN。这显示查询需要两次扫描——people和stars。由于我们是通过 ID 搜索movies,SQLite 会自动为这个 ID 创建索引,所以不需要扫描movies表!
让我们创建两个索引来加快这个查询的速度。
现在,我们使用相同的嵌套查询运行EXPLAIN QUERY PLAN。我们可以观察到
现在所有的扫描都变成了使用索引的搜索,这很好!
在people表上的搜索使用了一种称为COVERING INDEX的东西
覆盖索引意味着查询所需的所有信息都可以在索引本身中找到。而不是两步:
使用索引来搜索表,覆盖索引意味着我们只需一步(只是第一步)进行搜索。
要让stars表上的搜索也使用覆盖索引,我们可以在为stars创建的索引中添加"movie_id"。这将确保要查找的信息(电影 ID)和要搜索的值(人物 ID)都包含在索引中。
首先,让我们删除stars表上现有的索引实现。
接下来,我们创建新的索引。
运行以下命令将证明我们现在有两个覆盖索引,这应该会导致搜索速度大大加快!
确保我们已运行.timer on,然后我们可以执行上述查询以找到汤姆·汉克斯主演的所有电影,并观察其运行时间。现在查询的运行速度比没有索引时快得多(在讲座中,速度快了一个数量级)!
索引看起来非常有帮助,但它们也有权衡——它们在数据库中占用额外的空间,因此虽然我们获得了查询速度的提升,但我们确实失去了空间。
索引以称为 B 树或平衡树的数据结构存储在数据库中。树数据结构看起来像这样:
在这种情况下,会复制"titles"列。这个副本会被排序,然后通过指向电影 ID 将它们链接回movies表中的原始行。这在下图中进行了可视化。
虽然这有助于我们轻松地可视化此列的索引,但在现实中,索引不是一个单独的列,而是被拆分成许多节点。这是因为如果数据库有大量数据,比如我们的 IMDb 示例,将一个列全部存储在内存中可能不可行。
然而,如果我们有包含索引部分的多个节点,我们还需要节点来导航到正确的部分。例如,考虑以下节点。左侧节点根据电影标题是否在 Frozen 之前、在 Frozen 和 Soul 之间或 Soul 之后按字母顺序,将我们引导到索引的正确部分!
上述表示是一个 B 树!这是 SQLite 中索引存储的方式。
这是一个只包含表的一部分行的索引,允许我们节省一个完整索引所占用的空间。
这在我们知道用户只查询表的一小部分行时特别有用。在 IMDb 的情况下,可能用户更有可能查询一部新发布的电影,而不是一部 15 年前的电影。让我们尝试创建一个部分索引,该索引存储 2023 年发布的电影标题。
我们可以检查搜索 2023 年发布的电影是否使用了新的索引。
这表明movies表是使用部分索引进行扫描的。
索引是否保存在模式中?
有方法可以删除我们数据库中的未使用空间。SQLite 允许我们“真空”数据——这清理了之前已删除的数据(实际上并没有删除,只是标记为可用空间以供下一个INSERT使用)。
在讲座中,这个命令向我们展示了数据库的大小大约是 158 百万字节,或者说 158 兆字节。
我们现在可以连接到我们的数据库并删除我们之前创建的索引。
现在,如果我们再次运行 Unix 命令,我们会看到数据库的大小没有减少!要真正清理已删除的空间,我们需要对它进行真空。我们可以在 SQLite 中运行以下命令。
这可能需要一秒钟或两秒钟的时间来运行。在再次运行 Unix 命令检查数据库大小时,我们应该看到更小的尺寸。一旦我们删除所有索引并再次真空,数据库的大小将比 158 MB 小得多(在讲座中大约是 100 MB)。
是否有可能使真空过程更快?
如果一个删除某些行的查询实际上并没有删除它们,而只是将它们标记为已删除,我们是否还能检索这些行?
到目前为止,我们已经看到了如何优化单个查询。现在,我们将探讨如何允许一次不仅仅是一个查询,而是多个查询同时进行。
并发是数据库同时处理多个查询或交互的方式。想象一下,一个网站或金融服务数据库在同时承受大量流量。在这些情况下,并发尤为重要。
一些数据库事务可以是多部分的。例如,考虑一个银行的数据库。以下是一个存储账户余额的accounts表的视图。
一个事务可能是从一个账户向另一个账户转账。例如,爱丽丝试图向鲍勃发送 10 美元。
要完成这个事务,我们需要向鲍勃的账户中添加 10 美元,并从爱丽丝的账户中减去 10 美元。如果有人看到在第一次更新鲍勃的账户之后但在第二次更新爱丽丝的账户之前的accounts数据库的状态,他们可能会对银行持有的总金额有一个错误的理解。
对于外部观察者来说,它应该看起来像事务的不同部分是同时发生的。在数据库术语中,事务是一个单独的工作单元——不能分解成更小的部分。
事务具有一些属性,可以使用 ACID 首字母缩写词来记住:
原子性:不能分解成更小的部分,
一致性:不应违反数据库约束,
隔离性:如果多个用户访问数据库,他们的事务不能相互干扰,
持久性:在数据库内部发生任何故障的情况下,所有由事务更改的数据都将保持不变。
首先,我们想查看accounts表中已经存在的数据。
我们在此处记录鲍勃的 ID 是 2,爱丽丝的 ID 是 1,这对我们的查询将很有用。
要将 10 美元从爱丽丝的账户转移到鲍勃的账户,我们可以编写以下事务。
注意,UPDATE语句写在开始事务和提交事务的命令之间。如果我们执行查询是在写入UPDATE语句之后,但没有提交,那么两个UPDATE语句都不会执行!这有助于保持事务的原子性。通过以这种方式更新我们的表,我们无法看到中间步骤。
如果我们再次尝试运行上述事务——爱丽丝试图再给鲍勃支付 10 美元——它应该无法运行,因为爱丽丝的账户余额为 0。(accounts表中的"balance"列有一个检查约束,以确保它具有非负值。我们可以运行.schema来检查这一点。)
我们实现事务回滚的方式是使用ROLLBACK。一旦我们开始一个事务并写入一些 SQL 语句,如果其中任何一个失败,我们可以使用ROLLBACK来结束它,将所有值回滚到事务前的状态。这有助于保持事务的一致性。
事务可以帮助防止竞争条件。
当多个实体同时访问并基于共享值做出决策时,会发生竞争条件,这可能导致数据库中的不一致性。未解决的竞争条件可以被黑客利用来操纵数据库。
在讲座中,讨论了一个竞争条件的例子,其中两个用户合作可以利用数据库中的暂时不一致性来抢劫银行。
然而,事务是隔离处理的,以避免首先出现不一致。处理我们数据库中类似数据的每个事务都将按顺序处理。这有助于防止敌对攻击可以利用的不一致性。
为了使事务按顺序进行,SQLite 和其他数据库管理系统使用数据库上的锁。数据库中的表可能处于几种不同的状态:
未锁定(UNLOCKED):这是没有用户访问数据库时的默认状态,
共享(SHARED):当事务从数据库读取数据时,它获得共享锁,允许其他事务同时从数据库中读取,
排他(EXCLUSIVE):如果一个事务需要写入或更新数据,它将获得对数据库的排他锁,不允许其他事务同时发生(甚至不允许读取)
我们如何决定何时一个事务可以获得排他锁?我们如何优先处理不同类型的交易?
锁定的粒度是什么?我们是锁定数据库、表还是表的行?
这取决于数据库管理系统(DBMS)。在 SQLite 中,我们可以通过运行以下排他性事务来实现这一点:
如果我们现在不完成这笔交易,而是尝试通过不同的终端连接到数据库以读取表,我们将得到一个错误,表明数据库已被锁定!当然,这是一种非常粗略的锁定方式,因为它锁定了整个数据库。由于 SQLite 在这方面比较粗略,因此它有一个模块用于优先处理事务并确保只获得最短必要的排他锁。
简介
MySQL
创建 cards 表
创建 stations 表
创建 swipes 表
修改表
存储过程
带有参数的存储过程
PostgreSQL
创建 PostgreSQL 表
使用 MySQL 进行扩展
访问控制
SQL 注入攻击
问题
Fin
到目前为止,在本课程中,我们已经学习了如何设计和创建自己的数据库,读取和写入数据,以及最近如何优化我们的查询。现在,我们将了解如何以更大的规模来做这些事情。
可扩展性是增加或减少应用程序或数据库容量以满足需求的能力。
社交媒体平台和银行系统是可能需要随着其规模扩大和用户增加而扩展的应用程序的例子。
在本讲中,我们将使用不同的数据库管理系统,如 MySQL 和 PostgreSQL,这些系统可以用于扩展数据库。
SQLite 是一个嵌入式数据库,但 MySQL 和 PostgreSQL 是数据库服务器——它们通常运行在它们自己的专用硬件上,我们可以通过互联网连接到它们来运行我们的 SQL 查询。这使得它们能够将数据存储在 RAM 中,从而实现更快的查询。
我们将使用我们在之前的讲座中使用的 MBTA 数据库。以下是一个 ER 图,显示了实体 Card、Swipe 和 Station 以及这些实体之间的关系。
我们想要使用这个模式在 MySQL 中创建一个数据库!在终端上,让我们连接到一个 MySQL 服务器。
在这个终端命令中,-u 表示用户。我们提供我们想要连接到数据库的用户——root(在这种情况下与数据库管理员同义)。
127.0.0.1 是互联网上本地主机的地址(我们的电脑)。
3306 是我们想要连接的端口,这是 MySQL 的默认端口。将主机和端口的组合视为我们试图连接的数据库的地址!
命令末尾的-p表示我们希望在连接时提示输入密码。
由于这是一个完整的数据库服务器,其中可能包含许多数据库。要显示所有现有的数据库,我们使用以下 MySQL 命令。
这返回了一些服务器中已经存在的默认数据库。
我们将执行一些操作来设置 MBTA 数据库。我们已经看到了如何在 SQLite 中完成这些操作,所以让我们专注于 MySQL 的语法差异!
创建新数据库:
我们使用反引号而不是引号来标识 SQL 语句中的表名和其他变量。
要将当前数据库更改为mbta:
MySQL 在类型上比 SQLite 有更多的粒度。例如,一个整数可以是TINYINT、SMALLINT、MEDIUMINT、INT或BIGINT,这取决于我们想要存储的数字的大小。以下表格显示了我们可以存储在每个整数类型中的数字的大小和范围。
这些范围假设我们想要使用有符号整数。如果我们使用无符号整数,每个整数类型可以存储的最大值将翻倍。
现在我们将使用INT数据类型为 ID 列创建表cards。由于INT可以存储高达 40 亿的数字,它应该足够大,可以满足我们的使用案例!
注意,我们使用关键字AUTO_INCREMENT与 ID 一起使用,这样 MySQL 会自动插入下一个数字作为新行的 ID。
如果 ID 列不是无符号整数怎么办?我们如何表示这一点?
创建表后,我们可以通过运行以下命令来查看现有表的列表:
要获取关于表的更多详细信息,我们可以使用DESCRIBE命令。
为了处理文本,MySQL 提供了许多类型。两种常用的类型是CHAR——一个固定宽度的字符串,和VARCHAR——一个可变长度的字符串。MySQL 还有一个TEXT类型,但与 SQLite 不同,这种类型用于更长的文本块,如段落、书籍的页面等。根据文本的长度,它可以是TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT之一。此外,我们还有BLOB类型来存储二进制字符串。
MySQL 还提供了两种其他文本类型:ENUM和SET。Enum 将列限制为我们提供的选项列表中的单个预定义选项。例如,衬衫尺寸可以枚举为 M、L、XL 等。Set 允许在单个单元格中存储多个选项,这在电影类型等场景中很有用。
现在,让我们在 MySQL 中创建stations表。
我们选择VARCHAR作为站名,因为名字可能长度不一。然而,一个站点所在的线路是波士顿现有的地铁线路之一。由于我们知道这些值可能是什么,我们可以使用ENUM类型。
我们也像在 SQLite 中一样,使用列约束UNIQUE和NOT NULL。
在运行描述此表的命令后,我们看到一个类似的输出,列出了表中的每一列。在Key字段下,主键通过PRI被识别,任何具有唯一值的列通过UNI被识别。NULL字段告诉我们哪些列允许NULL值,对于stations表来说,没有列允许NULL值。
我们能否将表作为ENUM的输入?
如果我们不知道一段文本的长度,并使用类似VARCHAR(300)来表示它,这是否可以?
我们可以使用DATE、YEAR、TIME、DATETIME和TIMESTAMP(用于更精确的时间)来存储我们的日期和时间值。最后三个允许可选参数来指定我们想要存储时间的精度。
在 SQLite 中,我们有REAL数据类型。在这里,我们的选项是表下面的FLOAT和DOUBLE PRECISION。
在 MySQL 中,也有一种使用十进制(固定精度)类型的方法。使用这种方法,我们将指定要表示的数字中的位数以及小数点后的位数。
让我们现在创建swipes表。
我们为滑动金额选择的精度是 2。这是为了确保在没有任何舍入的情况下,可以添加或减去分。
我们在 SQLite 中创建表时使用的列约束仍然存在,包括确保滑动金额不是负数的检查。
如果我们在创建表后描述它,我们将看到熟悉的输出。Key字段有一个新值,对于外键列,MUL(多个)表示它们可能有重复的值,因为它们是外键。
当我们向列添加约束时,它们是否有生效的优先级?
MySQL 有类型亲和力吗?
MySQL 允许我们比 SQLite 更根本地修改表。
如果我们想要向一个站点可能所在的线路中添加一条银色线路,我们可以这样做。
这允许我们修改 line 列并更改其类型,使得 ENUM 现在包括银色作为选项。
还要注意,我们除了使用熟悉的 SQLite 中的 ALTER TABLE 构造外,还使用了 MODIFY 关键字。
存储过程是一种自动化 SQL 语句并重复运行它们的方式。
为了演示存储过程,我们再次使用之前讲座中提到的数据库——波士顿 MFA 数据库。
回想一下,我们在 SQLite 中使用视图来实现 MFA 数据库中 collections 的软删除功能。一个名为 current_collections 的视图显示了所有未被标记为已删除的集合。现在,我们将使用 MySQL 中的存储过程来完成类似的功能。
让我们导航到已经在我们的 MySQL 服务器上创建的 MFA 数据库。
在描述 collections 表时,我们看到 deleted 列不存在,需要添加到表中。
由于 deleted 列只有 0 或 1 的值,使用 TINYINT 是安全的。我们还将其默认值设置为 0,因为我们希望保留表中已有的所有集合。
在我们创建存储过程之前,我们需要将分隔符从 ; 改为其他东西。与 SQLite 不同,在 BEGIN 和 END(这里需要一个存储过程)之间我们可以输入多个语句,并以 ; 结尾,而 MySQL 在遇到 ; 时会提前结束语句。
现在,我们编写存储过程。
注意我们如何在存储过程名称旁边使用空括号,这可能会让人联想到其他编程语言中的函数。与函数类似,我们也可以调用存储过程来运行它们。
创建此过程后,我们必须将分隔符重置为 ;。
让我们尝试调用这个过程来看看当前的集合。在这个时候,查询应该输出 collections 表中的所有行,因为我们还没有进行软删除。
如果我们软删除“黎明时分工作的农民”并再次调用该过程,我们会发现已删除的行不包括在输出中。
我们能否向存储过程添加参数,即用一些输入来调用它们?
我们能否像函数一样从一个存储过程调用另一个存储过程?
你能在 MySQL 的表中留下任何注释或备注吗?
在我们之前与 MFA 数据库合作时,我们有一个名为transactions的表来记录购买的或出售的艺术品,我们也可以在这里创建它。
现在,如果一件艺术品因为出售而从collections中被删除,我们也希望更新transactions表中的这一信息。通常,这将是两个不同的查询,但通过存储过程,我们可以给这个序列一个名称。
这个过程参数的选择是绘画或艺术品的 ID,因为它是一个唯一的标识符。
我们现在可以调用这个过程来出售特定的物品。假设我们想要出售“想象中的风景”。
我们可以显示collections和transactions表中的数据,以验证所做的更改。
如果我多次调用sell同一个 ID 会发生什么?它可能会被多次添加到transactions表中。通过使用一些常规的编程结构,存储过程在逻辑和复杂性上可以得到相当大的改进。以下列表包含了一些在 MySQL 中可用的流行结构。
到目前为止,在本讲座中,我们看到了如何使用 MySQL,这让我们能够扩展 SQLite 所能提供的能力。
我们现在将通过与 MySQL 相同的流程来探索 PostgreSQL 的功能。我们将使用一些现有的 SQLite 数据库并将它们转换为 PostgreSQL。
回到之前提到的 MBTA 数据库,它有一个名为cards的表,让我们看看 PostgreSQL 为我们提供了哪些数据类型。
我们可以观察到这里的选项比 MySQL 少。PostgreSQL 也提供了无符号整数,类似于 MySQL。这意味着在处理无符号整数时,每个整数类型可以存储的最大值是这里显示的两倍。
序列
让我们通过打开 PSQL(PostgreSQL 的命令行界面)来连接到数据库服务器。
要查看所有数据库,我们可以运行\l,它会弹出一个列表。
要创建 MBTA 数据库,我们可以运行:
要连接到这个特定的数据库,我们可以运行\c "mbta"。
要列出数据库中的所有表,我们可以运行\dt。然而,目前数据库中还没有表。
最后,我们可以按照提议创建cards表,我们为 ID 列使用SERIAL数据类型。
要在 PostgreSQL 中描述一个表,我们可以使用像 \d "cards" 这样的命令。运行此命令后,我们会看到有关此表的一些信息,但格式与 MySQL 略有不同。
你如何在 PostgreSQL 中知道你的查询是否导致错误?
stations 表以类似 MySQL 的方式创建。
我们可以在 PostgreSQL 中像在 MySQL 中一样使用 VARCHAR。为了使事情简单,我们可以说 "line" 列也是 VARCHAR 类型。
我们接下来想要创建 swipes 表。回想一下,滑动类型可以标记卡的进入、退出或资金存入。类似于 MySQL,我们可以使用 ENUM 来捕获这些选项,但不要将其包含在列定义中。相反,我们创建自己的类型。
PostgreSQL 有 TIMESTAMP、DATE、TIME 和 INTERVAL 类型来表示日期和时间值。INTERVAL 用于捕获某物持续了多长时间,或时间之间的距离。类似于 MySQL,我们可以使用这些类型指定精度。
与 PostgreSQL 中的实数类型相比,一个关键的区别是 DECIMAL 类型被称为 NUMERIC。
我们现在可以继续创建 swipes 表,如下所示。
要退出 PostgreSQL,我们使用命令 \q。
这里的一种方法是通过垂直扩展数据库。垂直扩展是通过增加数据库服务器的计算能力来增加容量。
另一种方法是水平扩展。这意味着通过在多个服务器之间分配负载来增加容量。当我们水平扩展时,我们在多个服务器上保留数据库的副本(复制)。
复制主要有三种模式:单主模式、多主模式和领导者无模式。单主复制涉及单个数据库服务器处理传入的写入,然后将这些更改复制到其他服务器,而多主复制涉及多个服务器接收更新,导致复杂性增加。领导者无模式采用完全不同的方法,不要求有领导者。
在这里,我们将重点关注 单主复制模式。在这个模式中,跟随数据库服务器是一个只读副本:一个只能从中读取数据的数据库副本。领导者服务器被指定处理对数据库的写入。
一旦领导者处理完写请求,它可以在做其他任何事情之前等待跟随者复制更改。这被称为同步复制。虽然这确保了数据库始终一致,但它可能对查询的响应速度太慢。在金融或医疗保健等数据一致性至关重要的应用程序中,我们可能会选择这种通信方式,尽管它有缺点。
另一种类型是异步复制,其中领导者以异步方式与跟随者数据库通信,以确保更改被复制。这种方法可以用于社交媒体应用程序,其中响应速度至关重要。
另一种流行的扩展方式称为分片。这涉及到将数据库分割成多个数据库服务器上的碎片。关于分片的一个注意事项:我们希望避免出现数据库热点,或者一个比其他服务器更频繁被访问的数据库服务器。这可能会给该服务器造成过载。
当我们不使用复制进行分片时,会出现另一个问题。在这种情况下,如果其中一个服务器宕机,我们将有一个不完整的数据库。这会创建一个单点故障:如果一个系统宕机,我们的整个系统将无法使用。
让我们创建一个名为 Carter 的新用户(在这里你可以尝试使用你自己的名字)!
当我们创建这个新用户时,默认情况下它只有很少的权限。尝试以下查询。
这只显示了服务器中的一些默认数据库。
让我们通过讨论上周的一个例子来探讨如何通过用户授权来授予用户访问权限。我们有一个rideshare数据库和一个rides表。在这个表中,我们存储了乘客的名字,这是个人身份信息(PII)。我们创建了一个名为analysis的视图,匿名化了乘客的名字,目的是只与分析师或其他用户共享这个视图。
然而,这个用户可以访问的数据库部分只有analysis视图。我们现在可以看到这个视图中的数据,但不能从原始的rides表中看到!我们刚刚展示了 MySQL 访问控制的好处:我们可以让多个用户访问数据库,但只允许一些用户访问机密数据。
提高我们数据库安全性的方法之一是使用访问控制和仅向每个用户授予必要的权限。然而,使用 SQL 数据库的应用程序也可能受到攻击——其中之一就是 SQL 注入攻击。
正如其名所示,这涉及到一个恶意用户注入一些 SQL 短语,以在我们的应用程序中以不希望的方式完成现有查询。
在上面的例子中,用户 Carter 像往常一样输入了他们的用户名和密码。然而,一个恶意用户可能会输入不同的内容,比如字符串“password’ OR ‘1’ = 1”作为他们的密码。在这种情况下,他们试图获取用户和密码的整个数据库的访问权限。
在 MySQL 中,我们可以使用预编译语句来防止 SQL 注入攻击。让我们用之前创建的用户连接到 MySQL 并切换到bank数据库。
一个可以运行的 SQL 注入攻击示例,可以用来显示accounts表中的所有用户账户。
预编译语句是 SQL 中的一个语句,我们可以在稍后插入值。对于上面的查询,我们可以编写一个预编译语句。
预编译语句中的问号充当防止意外执行 SQL 代码的安全措施。
要实际运行这个语句并检查某人的余额,我们接受用户输入作为变量,然后将其插入到预编译语句中。
在上面的代码中,想象一下SET语句是通过应用程序获取用户的 ID!@是 MySQL 中变量的约定。
预编译语句清理输入以确保没有恶意 SQL 代码被注入。让我们尝试运行上面相同的语句,但使用一个恶意的 ID。
这也给出了与之前代码相同的结果——它显示了 ID 为 1 的用户的余额,没有其他内容!因此,我们已经防止了可能的 SQL 注入攻击。
在这个预编译语句的例子中,它是否只考虑了变量中的第一个条件?
这是否与我们在 Python 中执行 SQL 查询时不应该使用格式化字符串的原因相似?
简介
Web 编程
HTML(超文本标记语言)
文档对象模型 (DOM)
更多 HTML 元素
表单
CSS(层叠样式表)
响应式设计
Bootstrap
Sass(语法上出色的样式表)
在本课程中,我们将从 CS50 停止的地方继续,深入到网络应用程序的设计和创建。我们将通过在课程中进行多个项目的工作来培养我们的网页设计技能,包括一个开放式的最终项目,您将有机会创建自己的网站!
课程主题: 我们将在稍后进行更详细的介绍,但以下是本课程我们将要工作的简要概述:
HTML 和 CSS(一种用于概述网页的标记语言,以及使我们的网站更具视觉吸引力的方法)
Git(用于版本控制和协作)
Python(一种广泛使用的编程语言,我们将用它来使我们的网站更具动态性)
Django(我们将用于网站后端的流行网络框架)
SQL、模型和迁移(一种用于存储和检索数据的语言,以及使与 SQL 数据库交互更简单的 Django 特定方法)
JavaScript(一种用于使网站更快、更互动的编程语言)
用户界面(用于使网站尽可能易于使用的各种方法)
测试、CI、CD(了解用于确保网页更新顺利进行的各种方法)
可扩展性和安全性(确保我们的网站可以同时被许多用户访问,并且它们免受恶意意图的侵害)
HTML 是一种标记语言,用于定义网页的结构。它由您的网页浏览器(Safari、Google Chrome、Firefox 等)解释,以便在屏幕上显示内容。
让我们从编写一个简单的 HTML 文件开始吧!
之后,页面由嵌套的HTML 元素(如html和body)组成,每个元素都有一个开始和结束标签,分别用<element>表示开始和</element>表示结束。
注意到每个内部元素都比上一个元素缩进得更深一些。虽然浏览器不一定要求这样做,但这在您的代码中保持这种缩进将非常有帮助。
HTML 元素可以包括属性,这些属性为浏览器提供了关于元素的额外信息。例如,当我们把lang="en"包含在我们的初始标签中时,我们是在告诉浏览器我们正在使用英语作为我们的主要语言。
在 HTML 元素内部,我们通常希望包含一个head标签和一个body标签。head标签将包含关于您页面的信息,这些信息不一定显示,而body标签将包含访问网站的用户实际看到的内容。
在head内部,我们为我们的网页添加了一个title,您会注意到它显示在浏览器顶部的标签上。
最后,我们在主体中包含了文本“Hello, world!”,这是页面的可见部分。
您可能想要使用许多 HTML 元素来自定义您的页面,包括标题、列表和加粗部分。在接下来的示例中,我们将看到其中的一些实际应用。
另一点需要注意的是:<!-- -->在 HTML 中提供了注释,因此我们将在下面使用它来解释一些元素。
当这个页面渲染时,看起来就像这样:
在创建网站时,另一组非常重要的元素是如何从用户那里收集信息。您可以使用 HTML 表单允许用户输入信息,该表单可以包含几种不同类型的输入。在课程的后期,我们将学习如何处理表单提交后的信息。
正如其他 HTML 元素一样,您不需要记住这些,W3 Schools 是学习这些元素的一个很好的资源!
CSS 用于自定义网站的样式。
在我们刚开始的时候,我们可以向任何 HTML 元素添加一个 style 属性,以便将其应用于一些 CSS。
我们通过改变元素的 CSS 属性来更改样式,例如写color: blue或text-align: center。
在下面的示例中,我们对我们的第一个文件进行了一些微小的修改,以使其标题更加多彩:
虽然我们可以像上面那样为我们的网页进行样式设置,但要实现更好的设计,我们应该能够将样式从单个行中移除。
这里的 CSS 属性太多,无法一一介绍,但就像 HTML 元素一样,通常很容易在 Google 上搜索类似“将字体改为蓝色 CSS”的内容来获取结果。其中一些最常见的是:
color: 文本的颜色
text-align: 元素在页面上的放置位置
background-color: 可以设置为任何颜色
width: 以像素或页面百分比为单位
height: 以像素或页面百分比为单位
padding: 在元素内部应留出多少空间
margin: 在元素外部应留出多少空间
font-family: 页面上文本的字体类型
font-size: 以像素为单位
border: 大小、类型(实线、虚线等)和颜色
让我们利用我们刚刚学到的知识来改进上面的海洋表格。以下是一些 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
元素类型
除了逗号用于多个选择器之外,还有几种其他方式可以指定您想要样式的元素。这个来自讲座的表格提供了一些,我们将在下面通过几个示例进行说明:
后代选择器: 在这里,我们使用后代选择器来仅对位于无序列表中的列表项应用样式:
属性作为选择器: 我们还可以根据分配给 HTML 元素的属性来缩小我们的选择范围,使用方括号。例如,在以下链接列表中,我们选择仅使指向亚马逊的链接变红:
在按钮的情况下,我们会在按钮选择器中添加 :hover 以指定仅在悬停时的设计:
现在,许多人使用除电脑以外的设备浏览网站,例如智能手机和平板电脑。确保您的网站对所有设备上的用户都是可读的非常重要。
我们可以通过了解视口来实现这一点。视口是屏幕上在任何给定时间内对用户实际可见的部分。默认情况下,许多网页假设视口在所有设备上都是相同的,这就是导致许多网站(尤其是较老的网站)在移动设备上难以交互的原因。
在移动设备上改善网站外观的一个简单方法是在我们 HTML 文件的头部添加以下行。这一行告诉移动设备使用一个与您使用的设备相同宽度的视口,而不是一个更大的视口。
以下是一个媒体查询的例子,让我们尝试在屏幕缩小到一定大小时简单地改变屏幕颜色。我们通过输入@media后跟括号中的查询类型来表示媒体查询:
我们可以通过在 HTML 文件的头部添加一行来将 Bootstrap 包含到我们的代码中:
到目前为止,我们已经找到了几种消除 CSS 中冗余的方法,例如将其移动到单独的文件或使用 Bootstrap,但仍有一些地方我们可以进行改进。例如,如果我们想让几个元素有不同的样式,但所有元素的颜色都相同,怎么办?如果我们后来决定要更改颜色,那么我们就需要在几个不同的元素中更改它。
在使用 Sass 的同时,我们还可以物理地嵌套我们的样式,而不是使用我们之前提到的 CSS 选择器。例如,如果我们只想将一些样式应用于 div 中的段落和无序列表,我们可以编写以下代码:
一旦编译成 CSS,我们就会得到一个看起来像这样的文件:
介绍
Git
GitHub
提交
合并冲突
分支
欢迎回到第一讲!在第 0 讲中,我们介绍了 HTML、CSS 和 Sass 作为我们可以用来创建一些基本网页的工具。今天,我们将学习如何使用 Git 和 GitHub 来帮助我们开发网络编程应用。
允许我们在不影响主代码库的情况下,在不同的 分支 上进行代码更改和测试,然后将两个分支合并在一起。
允许我们在意识到我们犯了一个错误后,将代码回滚到之前的版本。
让我们从创建一个在线新仓库开始
点击右上角的 + 按钮,然后点击“新建仓库”
创建一个描述你项目的仓库名称
(可选)为你的仓库提供描述
(可选)决定你是否想添加一个 README 文件,这是一个描述你的新仓库的文件。
一旦我们有了仓库,我们可能还想向其中添加一些文件。为了做到这一点,我们将创建一个新的 远程 仓库的副本,或者克隆,将其作为我们电脑上的 本地 仓库。
点击你仓库页面上的绿色“克隆或下载”按钮,并复制弹出的 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 会自动将文件转换为一种格式,清楚地说明冲突是什么。以下是一个示例,其中相同的行以两种不同的方式被添加:
在上面的例子中,你添加了 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 checkout <分支名称> 在分支之间切换,并对每个分支提交任何更改。
当我们准备好合并两个分支时,我们将检出我们希望保留的分支(几乎总是主分支),然后运行命令 git merge <其他分支名称>。这将被处理得类似于推送或拉取,并且可能会出现合并冲突。
有一些特定于 GitHub 的有用功能,可以在你工作在项目时提供帮助:
Forking:作为一个 GitHub 用户,你有权限“Fork”任何你能够访问的仓库,这将创建一个属于你的仓库的副本。我们通过点击右上角的“Fork”按钮来完成这个操作。
Pull Requests:一旦你 fork 了一个仓库并对你的版本进行了更改,你可能希望请求将这些更改添加到仓库的主版本中。例如,如果你想向 Bootstrap 添加一个新功能,你可以 fork 仓库,进行一些更改,然后提交一个 pull request。这个 pull request 然后可以被 Bootstrap 仓库的管理人员评估,并可能被接受。人们进行一些编辑然后请求将它们合并到主仓库的过程对于所谓的“开源软件”至关重要,或者说是由多个开发者的贡献创建的软件。
GitHub Pages:GitHub Pages 是一种简单的方式来将静态站点发布到网络上。(我们稍后会学习静态站点与动态站点的区别。)为了做到这一点:
创建一个新的 GitHub 仓库。
将这些更改推送到 GitHub。
导航到你的仓库的设置页面,滚动到 GitHub Pages,并在下拉菜单中选择 master 分支。
滚动到设置页面中的 GitHub Pages 部分,几分钟后,你应该会看到一个通知,显示“您的站点已发布在:…”,包括一个你可以找到你站点的 URL!
这节课的内容就到这里!下次,我们将探讨 Python!
简介
Python
变量
字符串格式化
条件
序列
字符串
列表
元组
集合
字典
循环
函数
模块
面向对象编程
函数式编程
装饰器
Lambda 函数
异常
到目前为止,我们已经讨论了如何使用 HTML 和 CSS 构建简单的网页,以及如何使用 Git 和 GitHub 来跟踪代码的更改并与他人协作。
今天,我们将深入探讨 Python,这是我们将在整个课程中使用的两种主要编程语言之一。
Python 是一种非常强大且广泛使用的语言,它将使我们能够快速构建相当复杂的网络应用程序。在本课程中,我们将使用 Python 3,尽管 Python 2 在某些地方仍在使用。在查看外部资源时,请务必确保它们使用的是同一版本。
让我们从许多编程语言开始的地方开始:Hello, world。这个用 Python 编写的程序看起来是这样的:
要分解那行代码中发生的事情,Python 语言内置了一个名为print的函数,它接受括号中的参数,并在命令行上显示该参数。
任何编程语言的关键部分都是创建和操作变量的能力。为了在 Python 中给变量赋值,其语法看起来像这样:
每一行都将=右侧的值取出来,并存储在左侧的变量名中。
int: 一个整数
float: 一个十进制数
str: 一个字符串,或字符序列
bool: 一个值为True或False的值
NoneType: 表示没有值的一个特殊值(None)。
当在终端上运行时,程序看起来是这样的:
在这里有一些要点需要指出:
在第一行中,我们不是将变量名赋给一个显式的值,而是将其赋给input函数返回的任何值。
在第二行中,我们使用+运算符来组合,或连接两个字符串。在 Python 中,+运算符可以用来加数字或连接字符串和列表。
要表示我们正在使用格式化字符串,我们只需在引号前添加一个f。例如,我们可以在"Hello, " + name的用法上写f"Hello, {name}"以获得相同的结果。我们甚至可以在这个字符串中插入一个函数,并将我们上面的程序转换为一行:
了解上述程序的工作原理,Python 中的条件语句包含一个关键字(if、elif或else),然后(除了else情况外)是一个布尔表达式,或者是一个评估为True或False的表达式。然后,我们想要在某个表达式为真时运行的代码将直接缩进在语句下方。缩进是 Python 语法的一部分。
让我们更仔细地看看这个特定的异常:如果我们看到底部,我们会看到我们遇到了一个TypeError,这通常意味着 Python 期望某个变量是某种类型,但发现它是另一种类型。在这种情况下,异常告诉我们我们不能使用>符号来比较一个str和一个int,然后在上面的代码行 2 中我们可以看到这个比较发生了。
在这种情况下,很明显0是一个整数,所以我们的num变量必须是字符串。这是因为input函数总是返回一个字符串,我们必须指定使用int函数将其转换为(或强制类型转换为)整数。这意味着我们的第一行现在看起来像这样:
Python 语言最强大的部分之一是它能够处理数据序列,而不仅仅是单个变量。
几种序列在某些方面相似,但在其他方面不同。在解释这些差异时,我们将使用术语可变/不可变和有序/无序。可变意味着一旦定义了一个序列,我们就可以改变该序列的各个元素,而有序意味着对象的顺序很重要。
有序: 是
可变: 否
我们已经稍微了解了一些字符串,但除了变量之外,我们可以将字符串视为字符序列。这意味着我们可以在字符串中访问单个元素!例如:
打印出字符串中的第一个(或索引-0)字符,在这个例子中恰好是H,然后打印出第二个(或索引-1)字符,它是a。
有序: 是
可变: 是
有序: 是
可变: 否
有序: 否
可变: 不适用
有序:否
可变:是
我们已经看到了一些 Python 函数,例如print和input,但现在我们将深入编写我们自己的函数。为了开始,我们将编写一个接受一个数字并将其平方的函数:
注意我们如何使用def关键字来表示我们正在定义一个函数,我们正在接受一个名为x的单个输入,并且我们使用return关键字来表示函数的输出应该是什么。
我们可以像调用其他函数一样调用这个函数:使用括号:
或者,我们可以选择导入整个 functions 模块,然后使用点符号来访问 square 函数:
我们可以导入许多内置的 Python 模块,例如 math 或 csv,这些模块为我们提供了访问更多函数的权限。此外,我们还可以下载更多的模块来访问更多的功能!我们将花费大量时间使用 Django 模块,我们将在下一讲中讨论。
现在,让我们看看我们如何实际使用上面的类来创建一个对象:
现在,让我们看看一个更有趣的例子,在这个例子中,我们不仅存储一个点的坐标,而是创建一个表示航空公司航班的类:
然而,这个类是有缺陷的,因为我们虽然设定了容量,但我们仍然可能添加过多的乘客。让我们增强它,以便在添加乘客之前检查航班上是否有空位:
现在,让我们通过实例化一些对象来尝试我们创建的类:
函数式编程使得装饰器的概念成为可能,装饰器是一种高阶函数,可以修改另一个函数。例如,我们可以编写一个装饰器,用于在函数开始和结束时发出通知。然后,我们可以使用一个 @ 符号应用这个装饰器。
Lambda 函数为 Python 中创建函数提供了另一种方式。例如,如果我们想定义之前定义过的相同的square函数,我们可以这样写:
其中输入位于:的左侧,输出位于右侧。
这在我们不想为单个、小规模的使用编写整个单独的函数时非常有用。例如,如果我们想要对一些对象进行排序,但一开始不清楚如何排序。想象一下,我们有一个包含人名和房屋的人名列表,但我们希望按人名排序:
然而,这却给我们留下了错误:
这是因为 Python 不知道如何比较两个字典以检查一个是否小于另一个。
我们可以通过在排序函数中包含一个key参数来解决此问题,该参数指定我们希望用于排序的字典部分:
虽然这样做是可行的,但我们不得不编写一个只使用一次的整个函数,我们可以通过使用 lambda 函数来使我们的代码更易读:
在这次讲座中,我们遇到了几种不同的异常,所以现在我们将探讨一些处理它们的新方法。
在下面的代码块中,我们将从用户那里获取两个整数,并尝试除以它们:
在许多情况下,这个程序运行良好:
然而,当我们尝试除以 0 时,我们会遇到问题:
在这种情况下,当我们再次尝试时:
然而,当用户输入非数字的 x 和 y 时,我们仍然会遇到错误:
我们可以用类似的方式解决这个问题!
这就是本次讲座的全部内容!下次,我们将使用 Python 的Django模块来构建一些应用程序!