<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>KLucenの启示录</title><description>KLucen的博客</description><link>https://www.klucen.cn/</link><language>zh_CN</language><item><title>用 GPT-6.1 Sol 制作可莉版《痛痛飞走吧》：PV、工程与踩坑复盘</title><link>https://www.klucen.cn/posts/klee-pv-gpt61sol.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/klee-pv-gpt61sol.html/</guid><description>可莉版《痛痛飞走吧》PV 发布了。这篇记录用 GPT-6.1 Sol 推进角色替换的过程：拆稿与时序、参考图串动作、字幕和人物掩膜、局部返修，以及从工程经验整理出的可复用 Skill。</description><pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;国庆节跑了用了2个多的周额度，终于把用 &lt;strong&gt;GPT-6.1 Sol（Ultra）&lt;/strong&gt; 协助制作的可莉版《痛痛飞走吧》制作了出来并发到了 B 站。虽然在后半段人物和字幕有重叠的部分，字幕已经完全看不了了，而且也有很多地方也有大大小小的问题，但好歹是做了出来。
先放视频：&lt;/p&gt;
&lt;p&gt;::bilibili{bvid=&quot;BV1CCpM6sEB4&quot; title=&quot;可莉的痛痛飞走吧｜GPT-6.1 Sol Ultra&quot; p=1 preload=&quot;auto&quot;}&lt;/p&gt;
&lt;p&gt;这次也把相关的工程文件，以及从制作过程中整理出的 Skill 一起公开了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1CCpM6sEB4/&quot;&gt;B 站视频：可莉的痛痛飞走吧&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV&quot;&gt;工程仓库：6.1sol-KleePV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill&quot;&gt;Skill 仓库：Promotional Video Maker Skill&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前公开的是&lt;strong&gt;完整首版草稿&lt;/strong&gt;。全片已经导出，工程里也保存了技术检查记录，但字幕、人物边缘、遮挡和连续性仍有待改进的地方。&lt;/p&gt;
&lt;h2&gt;让可莉出演这支 PV&lt;/h2&gt;
&lt;p&gt;本次参考的&lt;a href=&quot;https://www.bilibili.com/video/BV1nu1CBKEZ9/&quot;&gt;原 PV&lt;/a&gt;已经有自己的画风、分镜、姿势和节奏。我的目标是保留这些，把画面中可见的人物换成可莉，连转场里只露出一部分的角色也要处理。&lt;/p&gt;
&lt;p&gt;这意味着需要同时守住两组东西：可莉的帽子、短辫、尖耳和服装要认得出来；当前镜头的表情、身体朝向、手势、人与道具的接触又要跟着原稿走。只生成一张漂亮的可莉立绘，远远不够。&lt;/p&gt;
&lt;p&gt;虽然说看起来任务很复杂但我实际上的prompt只有一句话：生成一份【中日字幕MIMI】痛痛飞走吧痛いの痛いの飛んでいけっ 的可莉版本PV，还是用原PV中的画面，但把画面中的人物都换为可莉，注意pv切分粒度要足够精细，在生成过程中注意补齐关键姿势，建立正确遮挡并检查连续帧。&lt;/p&gt;
&lt;p&gt;尽管在制作过程中sol会反复去修改已经大致没有什么问题的片段，导致进度拉长，但效果确实是出人意料的还不错。&lt;/p&gt;
&lt;h2&gt;先拆清楚绘稿，再组回视频&lt;/h2&gt;
&lt;p&gt;原片共有 &lt;strong&gt;3658 个视频帧&lt;/strong&gt;，但视频帧数和需要绘制的动作稿数量是两回事。同一张人物稿可以停留多帧，循环里也可能再次出现；与此同时，字幕、星星或背景仍在变化。&lt;/p&gt;
&lt;p&gt;工程里的流程大致是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分析原片，提取当前原稿和相邻画面的差值，判断人物究竟哪里变了。&lt;/li&gt;
&lt;li&gt;给需要独立绘制的稿件建立任务，明确原图、角色参考各自负责什么。&lt;/li&gt;
&lt;li&gt;保存生成结果、采用版本和问题记录，再把采用稿映射到所有实际曝光。&lt;/li&gt;
&lt;li&gt;按镜头处理人物、背景、字幕、道具和特效，合成主片与原片对照版。&lt;/li&gt;
&lt;li&gt;检查最终编码，再安排画面结构、连续切换和全片原速播放效果的审阅。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;“曝光”可以理解为一张绘稿在时间轴上显示多久。这次遇到的一个细节是：原片虽然标称 24 fps，实际时间戳存在 41/42 毫秒的量化差别，直接按 &lt;code&gt;帧号 / 24&lt;/code&gt; 排时间并不能精确保留原时序。因此，时间轴和导出检查要以真实 PTS（帧的显示时间戳）为依据。&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/analysis-and-timing.md&quot;&gt;拆稿与时序说明&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;制作里最值得记下的几个坑&lt;/h2&gt;
&lt;h3&gt;参考图给多了，动作反而串了&lt;/h3&gt;
&lt;p&gt;前一张可莉稿看起来最适合作为后一张的参考，但它也可能把自己的歪头、手势，甚至错误发束一起传下去。提示词写了“只参考角色身份”，也不能保证模型真的把身份与姿势分开。&lt;/p&gt;
&lt;p&gt;例如工程中 f1129 的初稿沿用了低位双拳，没有画出原片中靠近下巴的交叉握手。处理时重新让&lt;strong&gt;当前原稿与原尺寸手部局部决定动作&lt;/strong&gt;，角色身份只用不冲突的头部裁切；重绘后才得到需要的手势。&lt;/p&gt;
&lt;p&gt;后来参考图的分工就更明确了：当前原图管构图、动作和遮挡，身份参考管固有外观，相邻差值管动作变化。错误整稿也不再继续作为后续稿的锚点。&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/prompt-patterns.md&quot;&gt;提示词与参考分工&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;“只修一下这里”也会改变别处&lt;/h3&gt;
&lt;p&gt;改手套颜色、缩短发束、修一只手，看起来都是很小的操作，但生成结果可能连目标区域之外一起重画。&lt;/p&gt;
&lt;p&gt;制作记录里，f58 的短辫改色、f75 的手套改色都出现了大量框外像素变化。所以判断返修效果时，要把整张新图与原版本比较，再检查相邻稿、道具和其他已认可区域。不能因为目标部位变好了，就认为其余地方没动。&lt;/p&gt;
&lt;p&gt;同方向的局部返修反复无效时，应该换参考或回到当前原稿重绘，并保留失败版本与原因。这条后来也写进了 &lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/failure-patterns.md&quot;&gt;Skill 的故障经验&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;字幕已经烧进画面，不能简单贴回一块背景&lt;/h3&gt;
&lt;p&gt;原片的中日字幕、标题和署名都在画面里，没有独立字幕轨。人物替换之后，文字可能被人物掩膜吃掉；整块复制原画面，又可能把旧头发、衣服和背景一并贴回来。&lt;/p&gt;
&lt;p&gt;处理方向是把歌词按实际出现时间单独登记，提取文字笔画与透明边缘，再确定它与人物的前后关系。检查也要同时看缺笔画、误增文字，以及文字在最终画面上能否读清。&lt;/p&gt;
&lt;p&gt;一个特别容易漏掉的情况是：&lt;strong&gt;字幕换词发生在同一张人物稿的曝光内部&lt;/strong&gt;。人物没换稿，字层却必须切换。文字与旧衣物同色、无法直接分离的地方，则在明确允许后使用原 PV 中同一句的清晰字形重建，并分别登记中文、日文的位置与时间；这部分要如实记录为重建。&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/layers-and-continuity.md&quot;&gt;字幕与图层处理说明&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;画稿完整，合成后却可能缺头发、缺袖口&lt;/h3&gt;
&lt;p&gt;另一类问题出在掩膜：浅蓝头发、冷色脸、奶白袖口，都可能被颜色规则误判成天空。生成 PNG 本来完整，放进合成后却出现孔洞。&lt;/p&gt;
&lt;p&gt;遇到这类问题，先并排看原片、生成 PNG、人物掩膜和实际合成，确定错误在哪一步。工程中 f933/935 的浅蓝头发就是在合成时被误删，修的是生成稿的天空分类条件，并复查受影响曝光和旧输出。&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/blob/main/production/PRODUCTION_LESSONS.md&quot;&gt;对应返修记录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;旧人物消失后留下的区域也要单独处理：原片天空有渐变和星点，直接填纯色会留下接缝。人物保留、旧人物清除、背景恢复需要各自的依据。这些问题推动了工程从通用颜色规则转向按镜头注册的图层处理。&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/blob/main/production/full-pv-compositing-audit.md&quot;&gt;工程合成审计&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;相似画面、图片库存，都不能直接算作进度&lt;/h3&gt;
&lt;p&gt;两张缩略图看起来一样，原尺寸的眼皮、手指或衣褶却可能不同；一个动作生成了几个返修版，也没有因此多出几个独立动作。&lt;/p&gt;
&lt;p&gt;后来复用人物稿之前，会核对原尺寸局部、RGB 差值和轮廓。确认人物相同后才登记复用，当前帧的字幕、背景和特效仍按自己的时间变化。&lt;/p&gt;
&lt;p&gt;进度也拆开记录：采用了多少绘稿、覆盖了多少视频帧、还有哪些缺口、哪些只是选作草稿、哪些经过了美术检查。这样既能避免漏做，也能避免被不断增加的 PNG 数量误导。&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/blob/main/production/PRODUCTION_LESSONS.md&quot;&gt;制作过程的详细记录&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;检查有用，但也会拖慢制作&lt;/h2&gt;
&lt;p&gt;这次另一个教训是：每补几张就重新导出、重新审一遍长片段，很容易让制作一直停在局部。&lt;/p&gt;
&lt;p&gt;后来改为按一组动作，或同一句字幕的出现区间做增量检查，保留未改版本的证据；公共合成规则变了，再复查受影响区间。并行工作按镜头与稿件划分归属，同一稿及其重复曝光由一个任务负责，避免重复生成或合并时覆盖旧选择。&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/blob/main/production/PRODUCTION_WORKFLOW.md&quot;&gt;制作流程与提速约定&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;完整首稿交付后，再根据看片反馈定点返工。这个阶段安排也需要如实体现到结果里：目前主片和对照版各通过了 17 项技术检查，选定的 81 项回归也通过了，但对最终导出视频的抽查仍发现字幕缺笔、旧长发残留，以及服饰、帽形和眼态跳变。&lt;/p&gt;
&lt;p&gt;这些数字能证明检查覆盖的技术条件成立，不能替代全片原速审阅，更不能据此宣布画面已全部验收。工程保留了这些待修项，后续可以按位置继续处理。&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/failure-patterns.md&quot;&gt;当前交付状态与已知问题&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;工程文件怎么查看&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV&quot;&gt;6.1sol-KleePV&lt;/a&gt; 保存了分析、时间轴、生成任务、制作笔记、合成脚本和检查记录。想理解工作过程，可以先看 &lt;code&gt;production/PRODUCTION_WORKFLOW.md&lt;/code&gt; 和 &lt;code&gt;production/PRODUCTION_LESSONS.md&lt;/code&gt;，再看 &lt;code&gt;production/&lt;/code&gt; 下的核心脚本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;analyze_source.py&lt;/code&gt;、&lt;code&gt;group_source.py&lt;/code&gt;：分析原片与拆稿。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delta_reference.py&lt;/code&gt;：生成两稿之间的对照、RGB 差值和轮廓参考。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audit_full_pv_coverage.py&lt;/code&gt;、&lt;code&gt;make_timeline.py&lt;/code&gt;：核对选稿、缺口与实际曝光，建立时间轴。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compose.py&lt;/code&gt; 与各镜头图层模块：诊断和合成人物、背景、文字、道具与特效。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verify_preview.py&lt;/code&gt;：核验输出时序、片尾曝光、原 AAC 音频包和完整解码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些脚本带有本片的帧号、尺寸、颜色和路径假设，迁移到别的 PV 前需要重新适配。各脚本的职责与边界在 &lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/script-map.md&quot;&gt;脚本地图&lt;/a&gt; 中有说明。&lt;/p&gt;
&lt;p&gt;大体积素材放在 &lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/releases/tag/engineering-snapshot-2026-10-07&quot;&gt;工程快照 Release&lt;/a&gt;：包括生成图、未采用版本、参考、掩膜、检查截图与历史视频。&lt;strong&gt;只下载 GitHub 的源码 ZIP 拿不到完整素材。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要还原工程时，把五个 &lt;code&gt;engineering-objects-*.zip&lt;/code&gt; 分卷放进同一目录，再使用仓库中的还原脚本。例如在仓库根目录运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python tools/engineering_archive.py restore --volumes C:/Downloads/klee-volumes --destination C:/Projects/klee-restored
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本会核验哈希并恢复原文件路径。历史记录中的 Windows 绝对路径会保留，仍可能需要调整；歌曲、原 PV、角色和第三方依赖的许可也没有因为归档而改变。&lt;a href=&quot;https://github.com/KLucen/6.1sol-KleePV/blob/main/README.md&quot;&gt;工程归档说明&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;把这些经验整理成了一个 Skill&lt;/h2&gt;
&lt;p&gt;工程保留这次具体做了什么；&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill&quot;&gt;Promotional Video Maker Skill&lt;/a&gt; 则把可以复用的工作流程、失败经验与检查工具整理出来。Skill 的名字是 &lt;strong&gt;&lt;code&gt;pv-character-replacement&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;入口是 &lt;code&gt;SKILL.md&lt;/code&gt;，细节按拆稿、提示词、图层与连续性、交付检查等主题放在 &lt;code&gt;references/&lt;/code&gt;。它还带了一组通用工具：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe_source.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;记录来源指纹、真实 PTS、时间基准与片尾曝光&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pv_tools.py diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输出两张位图的对照、RGB 差值与双色轮廓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pv_tools.py validate-timeline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查其规定格式的曝光表与资产引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verify_encoded.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在支持范围内核验输出时序、复制的 AAC 与解码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test_media_tools.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查时序、音频和文件保护等关键约束&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;具体输入格式与限制以&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/pv-character-replacement/references/script-map.md&quot;&gt;脚本地图&lt;/a&gt;为准。例如候选分析的 &lt;code&gt;--cfr&lt;/code&gt; 参数只是使用者声明来源为恒帧率，并不自动证明原片时序；通用检查器也不能直接接收可莉工程的专用时间轴格式。&lt;/p&gt;
&lt;p&gt;在支持 Skill Installer 的 Codex 中，可以这样安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;使用 $skill-installer 安装 https://github.com/KLucen/-PromotionalVideoMakerSkill/tree/main/pv-character-replacement
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用时可以从这样的任务描述开始，再补上自己的原片与角色资料：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;使用 $pv-character-replacement 制作角色替换 PV，保留原分镜和真实曝光；逐稿检查关键姿势、遮挡、字幕与原速连续性，完成技术和美术两类验收。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行工具需要 Python 3.11+、FFmpeg/FFprobe，差值与分析功能还需要 NumPy、OpenCV、Pillow；实际绘制需要可用的图像生成或编辑工具。Skill 本身不附带生图模型、素材或账户凭据，具体安装与运行要求见&lt;a href=&quot;https://github.com/KLucen/-PromotionalVideoMakerSkill/blob/main/README.md&quot;&gt;仓库 README&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;第一个可莉视频开心捏&lt;/h2&gt;
&lt;p&gt;可莉真可爱耶&lt;/p&gt;
</content:encoded></item><item><title>博客后台云部署</title><link>https://www.klucen.cn/posts/%E7%94%9F%E6%B4%BB/%E5%8D%9A%E5%AE%A2%E5%90%8E%E5%8F%B0%E4%BA%91%E9%83%A8%E7%BD%B2/</link><guid isPermaLink="true">https://www.klucen.cn/posts/%E7%94%9F%E6%B4%BB/%E5%8D%9A%E5%AE%A2%E5%90%8E%E5%8F%B0%E4%BA%91%E9%83%A8%E7%BD%B2/</guid><description>记录一下博客后台搬上免费 Serverless 的过程</description><pubDate>Mon, 05 Oct 2026 00:31:03 GMT</pubDate><content:encoded>&lt;p&gt;我的博客拆成两个仓库：公开的 &lt;code&gt;shirone-blog&lt;/code&gt; 放主题和站点代码，私有的 &lt;code&gt;shirone-content&lt;/code&gt; 放写作内容。内容推到 &lt;code&gt;main&lt;/code&gt;，一条 Deploy Hook 让 Vercel 重新构建站点。&lt;/p&gt;
&lt;p&gt;管理端（ShironeAdmin）是给内容仓配的图形界面：选类型、新建、写正文、改配置、提交推送。它一直挺好用，但只能在那台装好环境的电脑上跑——接口绑在 &lt;code&gt;127.0.0.1&lt;/code&gt;，直接读写本地目录，用本机 git 提交。想在外面改个错别字，就得让那台电脑一直开着。&lt;/p&gt;
&lt;p&gt;这篇记录把它搬上免费 Serverless 的过程。结论放前面：&lt;strong&gt;能做，而且不花钱&lt;/strong&gt;。代价不是钱，是把「文件系统 + git」抽象成「API + 草稿分支」的那次改造，以及六个坑。&lt;/p&gt;
&lt;h2&gt;免费 Serverless 缺的正好是它最依赖的两样&lt;/h2&gt;
&lt;p&gt;管理端依赖一个持久可写的目录，和一个能执行的 &lt;code&gt;git&lt;/code&gt;。免费 Serverless 两样都没有。&lt;/p&gt;
&lt;p&gt;函数实例的文件系统是临时的，实例一换，未提交的改动就没了；&lt;code&gt;git&lt;/code&gt; 这个二进制也不存在。所以「打包上传」毫无意义——要换的是存储层。&lt;/p&gt;
&lt;h2&gt;用草稿分支代替「工作区」&lt;/h2&gt;
&lt;p&gt;本地模式下，「未提交的改动」就是工作区里的脏文件。远程模式没有工作区，我拿一个分支顶上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写入先落 &lt;code&gt;shirone-admin-draft&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;只有 &lt;code&gt;main&lt;/code&gt; 上的提交会触发站点重建，所以草稿不会把半成品发上线；&lt;/li&gt;
&lt;li&gt;点「发布」时，以&lt;strong&gt;当时最新的 &lt;code&gt;main&lt;/code&gt; 树&lt;/strong&gt;为底做路径级覆盖——草稿创建之后 &lt;code&gt;main&lt;/code&gt; 上的其它改动不会被回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[浏览器&amp;lt;br/&amp;gt;手机 / 公司] --&amp;gt;|HTTPS| B[Vercel 函数&amp;lt;br/&amp;gt;界面 + API]
  B --&amp;gt;|GitHub API| C[(内容仓&amp;lt;br/&amp;gt;shirone-content)]
  B -.-&amp;gt;|写入先落这里| D[草稿分支&amp;lt;br/&amp;gt;shirone-admin-draft]
  D --&amp;gt;|点「发布」才合并| E[main]
  E --&amp;gt;|Deploy Hook| F[Vercel 重建站点]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「没有草稿分支」就等于「没有待发布改动」——界面上的状态判断就靠这一条。&lt;/p&gt;
&lt;p&gt;界面本身没动。两种模式共用同一套组件和前端代码，差异收敛在两个接口上：文件读写、版本控制。&lt;/p&gt;
&lt;h2&gt;六个坑&lt;/h2&gt;
&lt;h3&gt;一、路径别名只在本地成立&lt;/h3&gt;
&lt;p&gt;现象：本地所有测试全绿，线上函数加载失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error [ERR_MODULE_NOT_FOUND]: Cannot find package &apos;@shared/pageSchemas&apos;
imported from /var/task/server/pageConfig.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@shared/*&lt;/code&gt; 是 tsconfig 里的 &lt;code&gt;paths&lt;/code&gt; 别名。本地 &lt;code&gt;tsx&lt;/code&gt; 会解析它，Vercel 构建函数时不读 &lt;code&gt;paths&lt;/code&gt;。同一个 import，本地是绿的，线上是死的。&lt;/p&gt;
&lt;p&gt;修法有两层：改回相对路径；再给构建加一道闸门——从真实入口出发静态遍历 import 图，禁掉路径别名、前端代码和未声明的依赖，并接进 &lt;code&gt;pnpm build&lt;/code&gt;。这类问题从此在构建阶段就以人话报出来。&lt;/p&gt;
&lt;p&gt;:::tip
本地全绿只能证明「在这套解析规则下没问题」。部署路径上的另一套规则，需要另一道检查。
:::&lt;/p&gt;
&lt;h3&gt;二、配置缺失不该让整个函数无法启动&lt;/h3&gt;
&lt;p&gt;现象：部署完成后打开页面，只看到一行 &lt;code&gt;Unexpected token &apos;A&apos;, &quot;A server e&quot;...&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原因是漏了一个环境变量：程序按本地模式启动，在&lt;strong&gt;模块加载期&lt;/strong&gt;去检查内容仓目录是否存在（Serverless 上当然不存在），抛异常，函数加载失败，平台返回一张纯文本错误页，前端的 &lt;code&gt;response.json()&lt;/code&gt; 只好抛解析错误。整条报错和真正的原因毫无关系。&lt;/p&gt;
&lt;p&gt;修法：内容仓路径解析不再抛异常；配置问题集中到 &lt;code&gt;/api/health&lt;/code&gt; 的 &lt;code&gt;problems&lt;/code&gt; 里；再加一个 &lt;code&gt;env&lt;/code&gt; 清单，只报每个变量是 &lt;code&gt;set&lt;/code&gt; 还是 &lt;code&gt;missing&lt;/code&gt;，不回显值。现在同样的坏配置会直接说清楚：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mode&quot;: &quot;local&quot;,
  &quot;problems&quot;: [
    &quot;本地模式，但内容仓不存在：/var/shirone-content\n如果这是部署到 Serverless（Vercel / Cloudflare），几乎一定是漏配了环境变量：\n  SHIRONE_STORAGE=github\n配好后需要重新部署（Redeploy）才会生效。&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
配置错误是常态，不是异常。它必须产生一条能照着做的提示，而不是把进程带走。
:::&lt;/p&gt;
&lt;h3&gt;三、重写规则的捕获名撞上了业务参数&lt;/h3&gt;
&lt;p&gt;现象：文章列表正常，点开任意一篇报 &lt;code&gt;文件不存在：doc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;错误里的路径就是字面的 &lt;code&gt;doc&lt;/code&gt;——那是路由段。原因是 Vercel 的重写规则捕获名和业务查询参数重名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;source&quot;: &quot;/api/:path*&quot;, &quot;destination&quot;: &quot;/api/index?__path=:path*&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平台除了应用目标里的查询参数，还会把匹配到的捕获段&lt;strong&gt;按捕获名注入&lt;/strong&gt;成查询参数。于是 &lt;code&gt;/api/doc?path=真实路径&lt;/code&gt; 变成「带 &lt;code&gt;path=doc&lt;/code&gt; 的请求」，真实路径被覆盖，服务端就去内容仓里找一个叫 &lt;code&gt;doc&lt;/code&gt; 的文件。列表接口没有 &lt;code&gt;path&lt;/code&gt; 参数，所以只有点文章会坏。&lt;/p&gt;
&lt;p&gt;修法：捕获名换成不会与业务参数重名的 &lt;code&gt;__route&lt;/code&gt;，还原时把所有 &lt;code&gt;__&lt;/code&gt; 前缀参数当作桥接参数删掉。另外加了一个 &lt;code&gt;/api/_bridge&lt;/code&gt;，把函数&lt;strong&gt;真实收到的 URL&lt;/strong&gt; 原样吐出来。&lt;/p&gt;
&lt;p&gt;:::note
平台的隐式行为不该靠猜。留一个「把你收到的东西原样报出来」的端点，比猜三轮便宜得多。
:::&lt;/p&gt;
&lt;h3&gt;四、「本地很快」的并发策略，在网络上会变成限流&lt;/h3&gt;
&lt;p&gt;仪表盘要对每篇文章读一次 frontmatter。本地文件系统上 &lt;code&gt;Promise.all&lt;/code&gt; 一把梭最快；换成网络请求之后，文章数就等于并发数——60 篇文章就是 60 个并发请求。&lt;/p&gt;
&lt;p&gt;GitHub 除了明面上的小时额度，还有一层针对并发突发的次级速率限制（secondary rate limit），触发后返回 403，而且&lt;strong&gt;不计入&lt;/strong&gt;小时额度。线上的症状就是「仪表盘转圈然后报错」。&lt;/p&gt;
&lt;p&gt;修法：所有 GitHub 请求过一个并发闸门（默认 8），并识别次级限流、按 &lt;code&gt;retry-after&lt;/code&gt; 重试。实测 60 篇文章、人为加 12ms 延迟：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;最大并发&lt;/th&gt;
&lt;th&gt;耗时&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;文章列表（65 次请求）&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;366ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仪表盘&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;452ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
把「全量并发」当默认值，只在单机文件系统上成立。
:::&lt;/p&gt;
&lt;h3&gt;五、workerd 里 &lt;code&gt;import.meta.url&lt;/code&gt; 不是合法 URL&lt;/h3&gt;
&lt;p&gt;现象：Cloudflare Workers 本地启动直接退出，只留一行 &lt;code&gt;Uncaught TypeError: Invalid URL string&lt;/code&gt;，指向打包产物的某一行，完全看不出是哪段源码。&lt;/p&gt;
&lt;p&gt;原因是 &lt;code&gt;new URL(&quot;..&quot;, import.meta.url)&lt;/code&gt; 在&lt;strong&gt;模块加载期&lt;/strong&gt;执行，而 workerd 里 &lt;code&gt;import.meta.url&lt;/code&gt; 形如 &lt;code&gt;worker.js&lt;/code&gt;，不是合法的绝对 URL。&lt;/p&gt;
&lt;p&gt;修法：包一层 try/catch 兜底（那个路径在远程模式下本来也用不到）。&lt;/p&gt;
&lt;p&gt;:::warning
模块加载期任何「假设运行时」的写法，代价都是整个应用起不来，而报错信息只会指向打包后的行号。
:::&lt;/p&gt;
&lt;h3&gt;六、抽象泄漏：虚拟路径跑进了真实的 fs 调用&lt;/h3&gt;
&lt;p&gt;删除一篇文章之后会自底向上清理空目录，用的是 &lt;code&gt;rmdir&lt;/code&gt;。而远程模式下 &lt;code&gt;resolveInRepo()&lt;/code&gt; 返回的是 &lt;code&gt;/content/posts&lt;/code&gt; 这样的&lt;strong&gt;虚拟路径&lt;/strong&gt;——把它交给真实的 &lt;code&gt;rmdir&lt;/code&gt;，等于「去删文件系统根目录下的同名目录」。&lt;/p&gt;
&lt;p&gt;它当时没出事，是因为 ENOENT 被 catch 兜住了。运气，不是设计。修法是让目录清理只在本地模式执行：git 的树是扁平的路径集合，本来就没有「空目录」这回事。&lt;/p&gt;
&lt;p&gt;:::caution
把两种后端装进同一个接口之后，&lt;strong&gt;虚拟值越界&lt;/strong&gt;是最难发现的一类 bug：它不报错，只是碰巧没出事。
:::&lt;/p&gt;
&lt;h2&gt;一次自己的事故&lt;/h2&gt;
&lt;p&gt;测试脚本原本的做法是：改真实内容仓里的文件，再用 &lt;code&gt;git checkout&lt;/code&gt; 还原。有一次我以为已经杀掉的旧 API 进程还在监听端口，测试连上了它，把 &lt;code&gt;config/site.yaml&lt;/code&gt; 的主题色改了。发现后逐字段核对、还原。&lt;/p&gt;
&lt;p&gt;之后的规矩是：&lt;strong&gt;所有端到端测试自建临时内容仓或内存版假服务&lt;/strong&gt;，没有一个能碰到真实内容。现在跑测试，内容仓的修改时间不会变。&lt;/p&gt;
&lt;h2&gt;不用真仓库也能端到端&lt;/h2&gt;
&lt;p&gt;这六个坑有个共同点：本地测试几乎都测不出来。所以验证方式也跟着换了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个内存版假 GitHub（只实现用到的那几个 Git Data API 端点），能在不碰真仓库、不需要 token 的前提下跑完「登录 → 写草稿 → 撤销 → 发布」整条链路；&lt;/li&gt;
&lt;li&gt;一个真 workerd 上的冒烟（&lt;code&gt;wrangler dev&lt;/code&gt; 加同一套断言），确认非 Node 运行时也能跑；&lt;/li&gt;
&lt;li&gt;远程断言 183 条：存储模式 112、Vercel 入口 17、部署自检 18、并发与限流 23、坏配置退化 13；本地另有 4 套自建仓端到端和 6 套单测；&lt;/li&gt;
&lt;li&gt;这些都在 CI 里跑，不需要任何 secret。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;现在的日常&lt;/h2&gt;
&lt;p&gt;:::steps{title=&quot;写一篇的完整流程&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;新建&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;选类型，填标题、分类、标签。管理端生成文件，落到草稿分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写正文&lt;/strong&gt;，保存&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;main&lt;/code&gt; 和线上站点没有任何变化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;发布前反悔&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;提交面板列出所有待发布文件，每一项都能单独撤销：新增的直接删掉，改过的还原成 &lt;code&gt;main&lt;/code&gt; 的版本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;发布&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;填一句说明，点「发布到 main」。管理端以当时最新的 &lt;code&gt;main&lt;/code&gt; 树为底合并草稿，触发站点重建。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;几十秒后，线上就是新的了。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>博客搬家记：从 Hexo 到 Astro</title><link>https://www.klucen.cn/posts/88260717.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/88260717.html/</guid><pubDate>Fri, 25 Sep 2026 13:11:00 GMT</pubDate><content:encoded>&lt;p&gt;这个博客换了个新家，写一篇记录一下。
&lt;img src=&quot;https://pics.klucen.cn//202610/901821a9f6c22e1bb49a1b7c120b3ba.png&quot; alt=&quot;&quot; /&gt;
这张图就是老站最后的样子 —— 「KLucenの启示录」，2025 年 12 月 14 日发出第一篇文章，框架是 Hexo 7.3.0 + Butterfly 5.5.2。到我动手搬的那天，它首页上还写着：&lt;strong&gt;文章 10 篇、标签 16 个、分类 3 个、总字数 4.5 万字&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;新家长什么样&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;老站&lt;/th&gt;
&lt;th&gt;现在这里&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;框架&lt;/td&gt;
&lt;td&gt;Hexo 7.3.0&lt;/td&gt;
&lt;td&gt;Astro 7.3.2 + Svelte 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;样式&lt;/td&gt;
&lt;td&gt;Butterfly 5.5.2&lt;/td&gt;
&lt;td&gt;Shirone（来自b站大佬&lt;a href=&quot;https://www.bilibili.com/video/BV1KKtV69E8a/?spm_id_from=333.1007.top_right_bar_window_history.content.click&amp;amp;vd_source=91b2014b08e4570c4a2a227c6e73f60a&quot;&gt;Shirone-谷歌M3E规范设计的全新开源博客主题_哔哩哔哩_bilibili&lt;/a&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无刷新跳转&lt;/td&gt;
&lt;td&gt;PJAX&lt;/td&gt;
&lt;td&gt;Swup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索&lt;/td&gt;
&lt;td&gt;Butterfly 本地搜索&lt;/td&gt;
&lt;td&gt;Pagefind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评论&lt;/td&gt;
&lt;td&gt;Waline（自建服务）&lt;/td&gt;
&lt;td&gt;Giscus（评论就是 GitHub Discussions）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;部署&lt;/td&gt;
&lt;td&gt;Vercel + GitHub Pages 双线&lt;/td&gt;
&lt;td&gt;Vercel，内容仓一 push 就自动构建&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;老站的东西都搬去哪了&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文章&lt;/strong&gt;：老站 10 篇里有 8 篇原样搬了过来，剩下两篇是当初的测试文章（Hello World 和《第一篇文章》），留在老站当化石。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链接一个字没改&lt;/strong&gt;：当初的 &lt;code&gt;abbrlink&lt;/code&gt; 是 crc32 算出来写进 frontmatter 的，搬家时把这些值一起抄了过来，所以 &lt;code&gt;/posts/c84b68c2.html&lt;/code&gt; 这样的老地址现在照样能打开。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;abbrlink: &apos;c84b68c2&apos;
permalink: &apos;posts/c84b68c2.html&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;说说 → 动态页&lt;/strong&gt;，&lt;strong&gt;照片 → 相册&lt;/strong&gt;，&lt;strong&gt;追番 → 番剧页&lt;/strong&gt;，内容还是原来那些。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;友链、关于&lt;/strong&gt;都还在，只是换了位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看板娘 koharu&lt;/strong&gt; 也跟着搬过来了，还站在屏幕角上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新框架有什么优点&lt;/h2&gt;
&lt;p&gt;搬过来用了一段时间，最直接的感受是这几条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面是「拼」出来的，不是改模板改出来的。&lt;/strong&gt; 各个页面由组件拼装，页面上的卡片来自 &lt;code&gt;data/&lt;/code&gt; 下的数据文件 —— 技能、项目、游戏、时间线、设备、友链、站点罗盘各一份，想多一张卡片就多加一个对象，不用去动模板。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认不发光 JavaScript。&lt;/strong&gt; 整站输出的是静态 HTML，只有真正需要交互的区块（数据驱动的那些）才在滚进视口时加载组件；老站是整站注入主题脚本再配 PJAX，首屏要重一些。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;frontmatter 有 schema 把关。&lt;/strong&gt; 文章属于内容集合，字段类型、必填项都由 schema 约束，写错在构建时就报错，不会像以前那样「页面看着正常，只是少了点什么」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索、评论、跳转都不用自己养服务。&lt;/strong&gt; Pagefind 在构建时就把索引做好，查询时按需取分片；评论直接落在公开仓库的 GitHub Discussions 里，零后端、零数据库；站内跳转交给 Swup，它有完整的生命周期钩子，播放器、看板娘这些常驻元素都不会被刷新打断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能写的东西变多了。&lt;/strong&gt; 代码块支持行高亮、标题、折叠、差异对比，正文里还能用提示框、卡片、步骤、标签页、脚注、数学公式，站上那个「写作语法演示」系列就是把它们逐条演示了一遍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置只需要写想改的键。&lt;/strong&gt; 主题默认值和我的个人设置分开存放，配置文件里出现的就是我动过的那几行，主题升级后新的默认值会自动生效，也不会把我的设置盖掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;搬的过程里真正花时间的三件事&lt;/h2&gt;
&lt;h3&gt;一、把内容从代码里拆出来&lt;/h3&gt;
&lt;p&gt;老站是文章和主题挤在同一个仓库里，改个样式顺手就可能带上文章。新站拆成了两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shirone-blog&lt;/code&gt;（公开）：主题源码和构建配置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shirone-content&lt;/code&gt;（私有）：文章、页面数据、配置覆盖、图片。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写文章只需要动内容仓，&lt;code&gt;pnpm content:sync&lt;/code&gt; 会把内容物化到代码仓的标准路径，之后构建链完全不变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm content:sync   # 内容仓 → src/content、src/data、public
pnpm dev            # 本地预览
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、顺手写了个管理端&lt;/h3&gt;
&lt;p&gt;搬完之后写文章得手写 frontmatter、手动挑图、手动排序，太原始了，于是做了个本地管理端 ShironeAdmin：左边选页面，右边直接改字段，图片能预览，图标能直接看效果。&lt;/p&gt;
&lt;p&gt;两个细节是自己特意加的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写回只改必要的行&lt;/strong&gt;：不会把整个数据文件重新格式化，diff 干净，出错了一眼能看出来；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个文件都有「撤销」&lt;/strong&gt;：改坏了不用去翻 git。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、评论换成 Giscus&lt;/h3&gt;
&lt;p&gt;评论使用了Shirone主题支持的 Giscus：评论直接落在公开仓库 &lt;code&gt;KLucen/ShironeComment&lt;/code&gt; 的 Discussions 里，零后端、零数据库，数据还是自己的。&lt;/p&gt;
&lt;p&gt;这里遇到一个坑：&lt;strong&gt;新站同一篇文章有两个都能打开的地址&lt;/strong&gt;（&lt;code&gt;/posts/&amp;lt;abbrlink&amp;gt;.html/&lt;/code&gt; 和 &lt;code&gt;/posts/&amp;lt;分类&amp;gt;/&amp;lt;abbrlink&amp;gt;/&lt;/code&gt;），Giscus 默认按 pathname 映射，会把同一篇的评论劈成两条讨论。改成按 &lt;code&gt;og:title&lt;/code&gt; 映射、并开启严格匹配之后，两个地址终于共用同一条讨论了。&lt;/p&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重复的数据会让整块页面变空白。&lt;/strong&gt; 数据里出现两条一模一样的内容时，Svelte 的 &lt;code&gt;{#each ... (key)}&lt;/code&gt; 会直接抛 &lt;code&gt;each_key_duplicate&lt;/code&gt;，那一块就整块不渲染 —— 页面看着像「少了点什么」，只有控制台里写着原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大图得先压。&lt;/strong&gt; 动态里几张图原来是好几 MB 的截图，压成 webp 之后只剩几十 KB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列表里的摘要只显示两行&lt;/strong&gt;，所以摘要写得越短越好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转载要写清出处。&lt;/strong&gt; 站上有一部分文章是整理 Shirone 官方文档的语法演示，开头统一加了版权与转载声明，标清作者和许可协议。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;现在&lt;/h2&gt;
&lt;p&gt;老站还留着，&lt;code&gt;klucen.github.io&lt;/code&gt; 现在也还能打开，就当是纪念。新站在建设好后，为了填上系列页没有的空白，搬运了作者在网页Markdown渲染上做的适配内容的写作语法，以及一册怎么学好Python的0-100天教程，因此在写这篇文章的时候这边已经有了四十多篇文章，此外比老站多了系列、技能、项目、游戏、时间线、站点罗盘这些页面 —— 页面上的内容全都写在内容仓里，想改直接改文件，不用再跟模板较劲。&lt;/p&gt;
&lt;p&gt;虽然站点的框架变了，但&lt;strong&gt;内容都在，链接也没断&lt;/strong&gt;，我觉得这就够了。&lt;/p&gt;
</content:encoded></item><item><title>用 Agent 陪读《深入理解 AI Agent》：笔记仓库开源了</title><link>https://www.klucen.cn/posts/640296d4.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/640296d4.html/</guid><pubDate>Fri, 11 Sep 2026 23:50:00 GMT</pubDate><content:encoded>&lt;p&gt;上个月我发过一篇 &lt;a href=&quot;https://klucen.cn/posts/66321874.html&quot;&gt;《深入理解 AI Agent：设计原理与工程实践》章节精读笔记&lt;/a&gt;。那篇是想给&quot;想快速了解 Agent 到底在讲什么&quot;的人一份简单的总结性文档，看完能知道这本书大致说了什么。但也仅此而已。&lt;/p&gt;
&lt;p&gt;而这次开源的仓库是另一种东西。它的用法不是&quot;替你把书读了&quot;，而是&lt;strong&gt;让你带着问题去读原文&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/KLucen/ai-agent-book-note&quot;&gt;KLucen/ai-agent-book-note&lt;/a&gt;&lt;/strong&gt;（Apache-2.0）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体怎么用：&lt;code&gt;内容精读/&lt;/code&gt; 里每个小节的开头都有一组从正文里长出来的引导问题，先看问题，再去读原书对应段落，读完回来对照&quot;读后小结&quot;和&quot;原文论证&quot;。读的时候有想法、有疑问，就写进 &lt;code&gt;QuickNote/&lt;/code&gt;。&lt;strong&gt;你也可以先看看我在里面随手记下的感悟和不明白的地方，再翻到 &lt;code&gt;DeepReadNote/&lt;/code&gt;，看这些答案是不是对你也一样有帮助。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原书是 &lt;a href=&quot;https://github.com/bojieli/ai-agent-book&quot;&gt;bojieli/ai-agent-book&lt;/a&gt;，李博杰著，正文、编译版 PDF、每章配套代码全部开源（Apache-2.0）。我这份是&lt;strong&gt;非官方&lt;/strong&gt;的个人精读笔记，不打算当原书的替代品或者翻译，它更像一份&quot;这本书我到底是怎么读的&quot;的完整记录。&lt;/p&gt;
&lt;h2&gt;里面有什么&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Agent Book/
├─ 00-阅读地图.md      全书导航：核心公式、阅读路径、进度总表
├─ 01-精读笔记规范.md  模板与写作纪律（想复用这套方法看这份）
├─ 02-全书术语表.md    跨章术语总表，每个术语只在首见章给权威定义
├─ images/             原书插图副本，供笔记引用
├─ 内容精读/           每章&quot;读&quot;：逐节深析
├─ 实验解读/           每章&quot;做&quot;：实验逐条解读 + 动手记录区
├─ 思考题引导/         每章&quot;考&quot;：题目原文 + 提示思路 + 自答留白
├─ QuickNote/          我自己写的读书随笔，原始记录
└─ DeepReadNote/       AI 对我的随笔的回应与补充
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个分册对应三种读法：&lt;strong&gt;读&lt;/strong&gt;原书、&lt;strong&gt;做&lt;/strong&gt;实验、&lt;strong&gt;考&lt;/strong&gt;自己。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;内容精读/&lt;/code&gt; 里每个小节都是同一个四段式，这是我用得最顺的部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;带着这些问题阅读本节&lt;/strong&gt;——问题是从该小节正文里长出来的，不是通用模板；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读后小结&lt;/strong&gt;——结论层，要覆盖该节全部要点，不写空话；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文论证&lt;/strong&gt;——原书证据，逐字引用并给出处锚点；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取舍与延伸&lt;/strong&gt;——工程取舍、跨章关联。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;实验解读/&lt;/code&gt; 每个实验给「目的 / 设计 / 关键结论」，后面留一张&lt;strong&gt;动手记录区&lt;/strong&gt;的空白表，等我复现完自己填。&lt;code&gt;思考题引导/&lt;/code&gt; 只给提示思路、不剧透答案，答案在原书的 &lt;code&gt;reference-answers.md&lt;/code&gt; 里，先自答再看。&lt;/p&gt;
&lt;h2&gt;为什么我推荐从「问题引导」读起&lt;/h2&gt;
&lt;p&gt;这份笔记里我最想推的用法，就是先看 &lt;code&gt;内容精读/&lt;/code&gt; 里那组引导问题，带着它们去读原书。&lt;/p&gt;
&lt;p&gt;说实话一开始agent提的问题真的很差，因为在规范文档里面我也做了几次的修改，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;引导问题规则&lt;/strong&gt;：必须源自该小节正文实际展开的内容，正文没强调的点不许问；读完该小节应能直接回答并覆盖要点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小结深度规则&lt;/strong&gt;：「读后小结」须按原文充实——保留关键定义、分类对比、机制/流程链、书中数字与案例，原则是「不看原文也能从小结复述该节内容」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在现在这个规范下，我觉得阅读体验很不错了。大部分问题确实能把我按在&quot;带着脑子读&quot;的状态里，而不是眼睛扫过去、脑子没跟上；读完之后它给的小结也有分寸，该有的定义、对比、数字和案例都在，不至于浅到只剩一句正确的废话。&lt;/p&gt;
&lt;p&gt;另一个我回过神来才觉得有用的设计：&lt;strong&gt;笔记里引用到的原书图都留了一份副本，每张图下面还配了一句基于原文的解读&lt;/strong&gt;。不然过几个月再回来翻，看到个 &lt;code&gt;fig2-3&lt;/code&gt; 只会想&quot;这图当初是想说明啥来着&quot;。&lt;/p&gt;
&lt;h2&gt;我最想推荐的是 QuickNote 和 DeepReadNote&lt;/h2&gt;
&lt;p&gt;读书的损耗主要发生在两处：读的时候没有焦点，读完没有反馈。引导问题管前一个，DeepReadNote 管后一个。&lt;/p&gt;
&lt;p&gt;第 1 章读完，我随手在 &lt;code&gt;QuickNote/Quick_note_1.md&lt;/code&gt; 里记了六七条：想补一下 RL 到底是什么、SDK 和 API 到底怎么区分、怀疑&quot;模型即 Agent&quot;和&quot;Harness 更关键&quot;是不是自相矛盾、希望有人把贯穿全书的设计模式再讲一遍。写得十分随便，就是卡住的地方随手记。&lt;/p&gt;
&lt;p&gt;然后 AI 写了一份 &lt;a href=&quot;https://github.com/KLucen/ai-agent-book-note/tree/main/DeepReadNote&quot;&gt;DeepReadNote&lt;/a&gt;，逐条回应。举几个它真的说到点上的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RL 的定位&lt;/strong&gt;：先讲清状态/动作/奖励/策略这一套，再回到书里那张控制论视角的 Agent↔Environment 闭环图。它还告诉我这个知识缺口不用现在补，第 8 章有两节可选阅读正好讲这个，到时候按需读更省力；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness 的边界&lt;/strong&gt;：我本来把它理解成&quot;生产系统的完整治理层&quot;。它指出更准确的说法是&quot;Agent 边界内、模型之外&quot;，而且书里的&lt;strong&gt;五要素（功能职责）和三层护栏（安全纵深）不是同一套划分&lt;/strong&gt;，把两张图对齐时最容易在这踩坑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;五条设计模式&lt;/strong&gt;给了个口诀：&lt;strong&gt;分离、按需、只增、双侧、小步&lt;/strong&gt;（提议者-审核者 / 渐进式披露 / 只增不改 / 边界集+保留集 / 最小 diff+可回滚）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这大概是整份笔记里最像&quot;讨论区&quot;的部分：我的原始笔记一个字没改，回应放在隔壁文件，对错都摊开。&lt;/p&gt;
&lt;h2&gt;我给 AI 定的几条规矩&lt;/h2&gt;
&lt;p&gt;人机协作最容易出问题的地方，是最后分不清哪句话是书里说的、哪句是 AI 自己发挥的。所以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三层标注&lt;/strong&gt;：&lt;code&gt;〔原书〕&lt;/code&gt; 书中内容 / &lt;code&gt;〔补充〕&lt;/code&gt; 书外知识 / &lt;code&gt;〔精读批注〕&lt;/code&gt; AI 或读者的判断；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用逐字核验 + 给出处锚点&lt;/strong&gt;。原书是 Markdown，没有页码，一律用「章 + 小节标题」定位，&lt;strong&gt;不许编造页码&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QuickNote 是我的原文，AI 只读不改&lt;/strong&gt;；&quot;我的批注&quot;留白区 AI 不许填；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思考题提示不许泄露参考答案&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;若我的理解和书中结论冲突，要明确指认并给证据，不许含糊调和。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第五条是我特意加的。顺着人说的 AI 没什么用，我要的是它能说&quot;你这条偏了，原文是这么写的&quot;。&lt;/p&gt;
&lt;p&gt;这些纪律本身也写在仓库里（&lt;code&gt;01-精读笔记规范.md&lt;/code&gt;，现在 v2.2），想拿去写自己的精读笔记，模板可以直接抄。AI 这一侧用的是 DeepSeek Harness，也就是我现在写博客、写插件用的同一套东西。&lt;/p&gt;
&lt;h2&gt;一些值得注意的事情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 1、2 章已精读完成&lt;/strong&gt;，第 1 章的 DeepReadNote 也有；&lt;strong&gt;第 3–10 章只有骨架&lt;/strong&gt;，里面是各章主旨与大纲预览，还没填。&lt;/li&gt;
&lt;li&gt;进度确实慢。一章要产出三份分册，还得真去跑实验，快不起来。好在笔记是否完整其实不重要，疑问有没有被追到闭环才重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生是 Obsidian 笔记&lt;/strong&gt;，在 GitHub 上会有几处不完美：&lt;code&gt;[[内部链接]]&lt;/code&gt; 点不动（得从 README 的进度表进各章）、&lt;code&gt;&amp;gt; [!info]&lt;/code&gt; 提示框变成普通引用块、文件头的 frontmatter 渲染成一张表。想要完整体验就 clone 下来，把 &lt;code&gt;Agent Book/&lt;/code&gt; 当 Obsidian 库的根目录。&lt;/li&gt;
&lt;li&gt;图片是原书插图的副本（版权归原作者，依 Apache-2.0 保留署名）；笔记本体也是 Apache-2.0。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;尾巴&lt;/h2&gt;
&lt;p&gt;仓库地址：&lt;a href=&quot;https://github.com/KLucen/ai-agent-book-note&quot;&gt;&lt;strong&gt;KLucen/ai-agent-book-note&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;笔记里的错误、不同理解、补充建议都欢迎开 issue 讨论；如果问题出在书本身，建议提到&lt;a href=&quot;https://github.com/bojieli/ai-agent-book/issues&quot;&gt;原书仓库&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;接下来该轮到第 3 章「用户记忆和知识库」了……希望下一篇不是三个月后 🫠&lt;/p&gt;
</content:encoded></item><item><title>dsh web 快捷启动插件 v0.2：一键重启、失败诊断，和「绝不打断回答」</title><link>https://www.klucen.cn/posts/7e01b64d.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/7e01b64d.html/</guid><pubDate>Thu, 10 Sep 2026 19:45:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇给 &lt;code&gt;dsh web&lt;/code&gt; 写了桌面快捷启动插件 &lt;a href=&quot;https://github.com/KLucen/dsh-desktop_quick_launcher&quot;&gt;dsh-desktop_quick_launcher&lt;/a&gt;。发出来之后自己天天在用，用着用着就发现：「能用」和「敢用」之间还差着一大截。于是从 v0.1.1 一路迭代到了 &lt;strong&gt;v0.2.7&lt;/strong&gt;，这篇记录一下中间加了什么。&lt;/p&gt;
&lt;p&gt;一句话版本：桌面图标还在，但插件现在管的是&lt;strong&gt;启动 / 重启 / 退出&lt;/strong&gt;三件事，而且&lt;strong&gt;在任何情况下都不会切断正在生成的回答&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;最想先说的：绝不打断正在生成的回答&lt;/h2&gt;
&lt;p&gt;装插件、升 DSH 核心、重打补丁，都要重启 &lt;code&gt;dsh web&lt;/code&gt;。而一个「朴素」的重启按钮是很危险的：你正在等模型写完一段代码，手一滑点了重启，回答就没了。&lt;/p&gt;
&lt;p&gt;所以 v0.2 里，「重启」和「停止」都先过一道门：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;未结束的回合会阻断操作。&lt;/strong&gt; 宿主读取 live session，套用 &lt;code&gt;@deepseek-ai/dsh-session&lt;/code&gt; 自己的开放回合判据（由最后一个 &lt;code&gt;turn/start&lt;/code&gt; / &lt;code&gt;turn/end&lt;/code&gt; 边界决定），返回 &lt;code&gt;409 busy&lt;/code&gt; 并列出相关会话。&lt;/li&gt;
&lt;li&gt;界面上的处理比报错友好：&lt;strong&gt;生成期间按钮直接禁用&lt;/strong&gt;，并提供一个「&lt;strong&gt;等空闲后自动重启&lt;/strong&gt;」（客户端队列，上限 10 分钟，可取消）。&lt;/li&gt;
&lt;li&gt;就算你排了队，&lt;strong&gt;退出前宿主还会再查一次&lt;/strong&gt;：如果这时刚好有新回答开始，重启会被记为 &lt;code&gt;aborted-busy&lt;/code&gt; 并&lt;strong&gt;取消&lt;/strong&gt;，页面提示「已取消：检测到新开始的回答」。&lt;/li&gt;
&lt;li&gt;逃生口是刻意留的：万一某个回合永久卡死，硬阻断会让重启按钮彻底失效。所以保留了 &lt;code&gt;force: true&lt;/code&gt;，但只能通过&lt;strong&gt;二次、措辞明确&lt;/strong&gt;的确认触达（「强制：会中断正在生成的回答」），并且会在重启报告里记上 &lt;code&gt;forced: true&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重启这件事，是怎么才走通的&lt;/h2&gt;
&lt;p&gt;这部分是 v0.2 里最折腾的：让一个进程「自杀后重生」，在 Windows 上比想象中难。&lt;/p&gt;
&lt;p&gt;第一版的思路很自然：宿主 spawn 一个 detached 子进程，让它等宿主退出后拉起新实例。&lt;strong&gt;实测直接死&lt;/strong&gt; —— 在 Windows 上，这个子进程会在&lt;strong&gt;执行任何一条语句之前&lt;/strong&gt;就随宿主的进程树被杀掉，一行代码都跑不到。&lt;/p&gt;
&lt;p&gt;现在的方案是&lt;strong&gt;计划任务&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宿主把重启助手作为计划任务启动，它的进程属于&lt;strong&gt;任务计划服务&lt;/strong&gt;，宿主自己的清理动不到它；&lt;/li&gt;
&lt;li&gt;但宿主不会「启动完就放心去死」：它会&lt;strong&gt;等助手把共享状态文件推进到 &lt;code&gt;handoff&lt;/code&gt;&lt;/strong&gt; 之后，才回响应并退出；&lt;/li&gt;
&lt;li&gt;如果助手始终没报到，重启会被&lt;strong&gt;取消、服务继续运行&lt;/strong&gt; —— 重启机制坏掉，不再等于服务变死。这一点很重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;助手里还有几个必须显式处理的细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计划任务的启动目录是 &lt;code&gt;%SystemRoot%\System32&lt;/code&gt;，而且&lt;strong&gt;没有 &lt;code&gt;DSH_HOME&lt;/code&gt;&lt;/strong&gt;，两个都得显式还原，否则新实例会找不到自己的家；&lt;/li&gt;
&lt;li&gt;助手&lt;strong&gt;绝不使用 &lt;code&gt;taskkill /T&lt;/code&gt;&lt;/strong&gt;：它自己是旧宿主的子孙进程，&lt;code&gt;/T&lt;/code&gt; 会把它自己一起杀掉；&lt;/li&gt;
&lt;li&gt;助手从捕获的 stdout 里解析新实例的 &lt;code&gt;dsh web: http://…/?token=…&lt;/code&gt; 一行，留作浏览器 cookie 失效时的兜底。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那&lt;strong&gt;页面为什么能自己回来&lt;/strong&gt;？因为浏览器会话 cookie 的签名密钥持久化在 &lt;code&gt;$DSH_HOME/.credentials.yaml&lt;/code&gt;（默认 30 天），&lt;strong&gt;能跨重启存活&lt;/strong&gt;；客户端等 &lt;code&gt;/ping&lt;/code&gt; 返回一个和重启前&lt;strong&gt;不同&lt;/strong&gt;的 &lt;code&gt;instanceId&lt;/code&gt;，然后重新加载原 URL。另外，同时只允许一次重启 —— inflight 标记让第二次请求拿到 &lt;code&gt;409 restart-inflight&lt;/code&gt;，双击出的两个标签页不会起两个助手。&lt;/p&gt;
&lt;h2&gt;出问题时，别再让人猜&lt;/h2&gt;
&lt;p&gt;v0.1 时代最难受的是：图标双击了、服务没起来，然后&lt;strong&gt;什么信息都没有&lt;/strong&gt;。v0.2 在诊断上做了三件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 分级探针，把「没起来」拆成四种各有结论的状态。&lt;/strong&gt; 顺序是：TCP 连接 → 插件 &lt;code&gt;/ping&lt;/code&gt;（确认是本插件，拿到 instanceId）→ &lt;code&gt;GET /&lt;/code&gt; 指纹（&lt;code&gt;dsh web authentication required&lt;/code&gt;）→ 外来服务。于是「DSH 已在运行 / 端口被别的程序占用 / 端口有人监听但从不响应（僵死实例）/ 端口空闲」是可以区分的。只要端口不是&lt;strong&gt;空闲&lt;/strong&gt;，就是一条&lt;strong&gt;诊断&lt;/strong&gt;而不是一次启动尝试：启动器会报告占用者的 PID 和进程名，然后停下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 捕获子进程输出。&lt;/strong&gt; v0.1 用 &lt;code&gt;-WindowStyle Hidden&lt;/code&gt; 把 stdout/stderr 全丢了，失败了只看到一个「代码 1」。现在它们写进 &lt;code&gt;dsh-child.out.log&lt;/code&gt; / &lt;code&gt;dsh-child.err.log&lt;/code&gt;，&lt;strong&gt;末尾 15 行&lt;/strong&gt;同时进入桌面消息框和报告 —— 通常这就是全部答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 机器可读的状态文件。&lt;/strong&gt; 每次运行都会&lt;strong&gt;原子写&lt;/strong&gt;（临时文件 + 改名，UTF-8 &lt;strong&gt;无 BOM&lt;/strong&gt;）&lt;code&gt;launcher-status.json&lt;/code&gt;，重启写 &lt;code&gt;restart-status.json&lt;/code&gt;；&lt;strong&gt;九类失败结论&lt;/strong&gt;各自带下一步建议；GUI 会在页面加载时把上次启动失败&lt;strong&gt;提示一次&lt;/strong&gt;（一次性横幅），你不必先知道「有日志文件这回事」。&lt;/p&gt;
&lt;p&gt;顺带一提，并发也处理了：&lt;strong&gt;命名互斥量&lt;/strong&gt;会串行化同时来的调用 —— 连点三次图标，不会再让三个启动器去抢 3080，多出来的调用转为等待。&lt;/p&gt;
&lt;h2&gt;面板和设置卡片&lt;/h2&gt;
&lt;p&gt;右下角的悬浮件现在是一组：&lt;strong&gt;桌面图标/详情、停止、重启&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;详情&lt;/strong&gt;弹出面板：实例信息（PID、端口、运行时长、版本）、当前生成状态、上次启动报告、上次重启报告、抓到的子进程输出尾部。&lt;/li&gt;
&lt;li&gt;插件还在&lt;strong&gt;设置&lt;/strong&gt;里贡献了自己的一页（&lt;code&gt;settings.section&lt;/code&gt; 槽位，和其它插件页并列）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;悬浮按钮&lt;/strong&gt;：三个按钮各有独立开关，可以只留一部分，也可以全部隐藏（全关掉右下角就干干净净）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志与状态文件&lt;/strong&gt;：列出各个 log / 状态文件的大小与修改时间，支持&lt;strong&gt;查看&lt;/strong&gt;（末尾）、&lt;strong&gt;清空&lt;/strong&gt;（单个或全部）、&lt;strong&gt;刷新&lt;/strong&gt;、&lt;strong&gt;打开目录&lt;/strong&gt;。状态文件是&lt;strong&gt;删除&lt;/strong&gt;而不是清空，这样面板显示的是「暂无记录」而不是解析错误；&lt;/li&gt;
&lt;li&gt;这些路由只接受&lt;strong&gt;白名单里的名字&lt;/strong&gt;（绝不接受调用方传来的路径），写操作需要 nonce。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;几个「事后想起来很关键」的修复&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;v0.2.3：快捷方式开的是裸地址，首次打开必然要求认证。&lt;/strong&gt; 以前打开 &lt;code&gt;http://127.0.0.1:3080&lt;/code&gt;，浏览器没有 cookie 时 DSH 会返回 &lt;code&gt;dsh web authentication required&lt;/code&gt;。现在启动器会解析&lt;strong&gt;带 token 的 URL&lt;/strong&gt;（优先 &lt;code&gt;/ping&lt;/code&gt; 公布的 &lt;code&gt;authUrl&lt;/code&gt;，缓存在 &lt;code&gt;auth-url.txt&lt;/code&gt;；其次是自己启动子进程时捕获的那一行），首次加载就是已认证状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v0.2.4：把 &lt;code&gt;/ping&lt;/code&gt; 的 200 当成了「就绪」。&lt;/strong&gt; 可那时 SPA 兜底还没注册好，浏览器打开就是 &lt;code&gt;HTTP ERROR 404&lt;/code&gt;。现在的启动器有&lt;strong&gt;就绪门&lt;/strong&gt;（&lt;code&gt;Test-GuiReady&lt;/code&gt;）和 &lt;code&gt;starting&lt;/code&gt; 阶段 —— 只等待、不重复起实例；顺带在 DSH 模态框打开时把悬浮件藏起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v0.2.5：启动器脚本「永远不更新」。&lt;/strong&gt; 快捷方式跑的一直是「创建图标那一刻」写下的脚本 —— 也就是说前面两个版本的启动器修复&lt;strong&gt;从未真正生效过&lt;/strong&gt;。现在宿主每次启动都会&lt;strong&gt;幂等重写&lt;/strong&gt;已有的 &lt;code&gt;launcher.ps1&lt;/code&gt;（不创建图标），升级即生效；只有快捷方式被删掉这种情况才需要点一下重建（v0.2.6 加了「一键重建桌面快捷方式」，并在 &lt;code&gt;/status&lt;/code&gt; 里暴露 &lt;code&gt;shortcut { name, path, exists }&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v0.2.7：别把「版本错位」报成「快捷方式不存在」。&lt;/strong&gt; 客户端 bundle 是从磁盘实时加载的，会先于宿主更新，于是旧宿主缺的字段被误报成「快捷方式没了」。现在会&lt;strong&gt;显式报告客户端/宿主版本错位&lt;/strong&gt;并提示重启，字段缺失时显示「无法确认」而不是「不存在」。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;接口和配置&lt;/h2&gt;
&lt;p&gt;给想自己接一下的人。路由一共这几条（完整前缀是 &lt;code&gt;/api/dsh-desktop_quick_launcher/…&lt;/code&gt;，状态变更类需要 nonce）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路由&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;实例 id、nonce、pid、端口、运行时长&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;实例 + &lt;code&gt;busy&lt;/code&gt; + 上次启动/重启报告 + 日志路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;写启动脚本 + 桌面图标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;移交给独立助手后退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;优雅退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/logs&lt;/code&gt;、&lt;code&gt;/logs/clear&lt;/code&gt;、&lt;code&gt;/logs/open&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET/POST&lt;/td&gt;
&lt;td&gt;日志与状态文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;写入面板开关&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;安全上，所有路由&lt;strong&gt;仅限回环&lt;/strong&gt;（socket 地址 + Host 头 + &lt;code&gt;sec-fetch-site&lt;/code&gt; + Origin）—— 这挺重要，因为 DSH 的插件路由在浏览器登录门&lt;strong&gt;之外&lt;/strong&gt;；另外所有状态变更路由都要求一个由 &lt;code&gt;GET /ping&lt;/code&gt; 下发的&lt;strong&gt;每实例 nonce&lt;/strong&gt;（放在 &lt;code&gt;x-dsh-ql-nonce&lt;/code&gt; 头里），否则返回 &lt;code&gt;403 nonce-required&lt;/code&gt;。它能挡住跨站页面盲发 POST 到 &lt;code&gt;127.0.0.1&lt;/code&gt;，但&lt;strong&gt;挡不住本机进程&lt;/strong&gt;（它自己就能读 &lt;code&gt;/ping&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;配置项也变多了（schemastery &lt;code&gt;desktop-quick-launcher&lt;/code&gt; 命名空间）：&lt;code&gt;restartGraceMs&lt;/code&gt;、&lt;code&gt;restartTimeoutSec&lt;/code&gt;、&lt;code&gt;busyPolicy&lt;/code&gt;（&lt;code&gt;block&lt;/code&gt; | &lt;code&gt;warn&lt;/code&gt;）、&lt;code&gt;restartMethod&lt;/code&gt;（&lt;code&gt;auto&lt;/code&gt; | &lt;code&gt;schtasks&lt;/code&gt; | &lt;code&gt;detached&lt;/code&gt;）、&lt;code&gt;helperStartTimeoutMs&lt;/code&gt;，以及三个悬浮按钮的显示开关。&lt;/p&gt;
&lt;h2&gt;升级 / 安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dsh plugin --profile web add github:KLucen/dsh-desktop_quick_launcher
# 重启 dsh web（之后就可以用面板上的重启按钮了）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;github.com 不通就走镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dsh plugin --profile web add &quot;https://gh-proxy.com/https://codeload.github.com/KLucen/dsh-desktop_quick_launcher/tar.gz/refs/heads/main&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从 v0.2.5 起，升级后不需要再碰桌面图标&lt;/strong&gt; —— 宿主启动时会重写脚本，现有快捷方式立刻用上新逻辑。开发的话仓库里有测试：&lt;code&gt;pnpm typecheck &amp;amp;&amp;amp; pnpm test&lt;/code&gt; 会把真实的宿主半边挂到 stub cordis 上下文上，直接驱动真实的路由处理器，并注入助手的 spawn 与进程退出钩子、把 &lt;code&gt;DSH_HOME&lt;/code&gt; 指向临时目录 —— 所以它不会真的拉起助手、不会退出测试进程，也不会碰你的真实 profile 和桌面。&lt;/p&gt;
&lt;h2&gt;已知限制（如实说）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;重启和退出都会&lt;strong&gt;终止整个 &lt;code&gt;dsh web&lt;/code&gt; 进程&lt;/strong&gt;：会话会持久化并可恢复，但&lt;strong&gt;正在生成的回答不会被保留&lt;/strong&gt; —— 这正是它在回合未结束时拒绝执行的原因；&lt;/li&gt;
&lt;li&gt;busy 检查读取 live session store，如果该服务缺失或 API 变化，检查会&lt;strong&gt;放行&lt;/strong&gt;（并在 &lt;code&gt;busyCheck&lt;/code&gt; 里记录原因）—— 改成 fail-closed 的话，重启会永久失效；想要 v0.1 那种「只报告不阻断」的行为，可以设 &lt;code&gt;busyPolicy: warn&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;nonce 只挡跨站浏览器请求，挡不住本机进程；&lt;/li&gt;
&lt;li&gt;所有路由仅限回环：服务器暴露到局域网时，远程浏览器用不了；&lt;/li&gt;
&lt;li&gt;Windows 图标固定叫 &lt;code&gt;DSH-Web.lnk&lt;/code&gt;，同名会被覆盖；&lt;/li&gt;
&lt;li&gt;Windows 的重启助手是 PowerShell 脚本，在禁止执行 PowerShell 的机器上只剩计划任务路径或手动重启；&lt;strong&gt;POSIX 上的重启助手尚未实现&lt;/strong&gt;（那边只能等宿主退出后手动启动）；&lt;/li&gt;
&lt;li&gt;仍未发布 npm，安装走 GitHub tarball。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;尾巴&lt;/h2&gt;
&lt;p&gt;从「桌面图标 + 一键退出」到「启动 / 重启 / 退出 + 一整套诊断」，这个插件现在算是到了我自己敢天天用的程度：升级不用担心回答被掐断，出问题不用再靠猜。&lt;/p&gt;
&lt;p&gt;仓库还是那个：&lt;a href=&quot;https://github.com/KLucen/dsh-desktop_quick_launcher&quot;&gt;KLucen/dsh-desktop_quick_launcher&lt;/a&gt;（Apache-2.0），有 bug 或想法欢迎开 issue。下一篇大概就是 v0.3 的 POSIX 重启助手了 —— 如果我没偷懒的话。&lt;/p&gt;
</content:encoded></item><item><title>给 dsh web 做了个桌面快捷启动插件</title><link>https://www.klucen.cn/posts/73edb7ce.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/73edb7ce.html/</guid><pubDate>Wed, 09 Sep 2026 19:30:00 GMT</pubDate><content:encoded>&lt;p&gt;给 &lt;code&gt;dsh web&lt;/code&gt; 写了个小插件 —— &lt;strong&gt;dsh-desktop_quick_launcher&lt;/strong&gt;,补上我一直觉得最缺的两块体验:&lt;strong&gt;双击桌面图标启动&lt;/strong&gt;,和&lt;strong&gt;在界面里一键退出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;仓库在 &lt;a href=&quot;https://github.com/KLucen/dsh-desktop_quick_launcher&quot;&gt;GitHub: KLucen/dsh-desktop_quick_launcher&lt;/a&gt;(Apache-2.0)。&lt;/p&gt;
&lt;h2&gt;为什么会有这个插件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dsh web&lt;/code&gt; 是本地优先的服务,日常用其实挺舒服:浏览器打开就是完整 GUI,不占资源、不上云。但「本地服务」嘛,总有点桌面应用才有的别扭感:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动要开终端&lt;/strong&gt;:先找到终端,敲 &lt;code&gt;dsh web&lt;/code&gt;,等它起来,再手动开浏览器;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退出没入口&lt;/strong&gt;:界面里没有「关掉服务」的按钮,关掉浏览器只是断开连接,服务还在后台占着 3080 端口,要么留着吃内存,要么回终端 Ctrl+C。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实之前有 &lt;a href=&quot;https://www.npmjs.com/package/@linxin666/dsh-desktop-launcher&quot;&gt;@linxin666/dsh-desktop-launcher&lt;/a&gt; 这个插件解决过类似问题,但它已经退役了。与其依赖别人维护,不如自己动手独立重建一个:能按自己的习惯调整,许可证干净(Apache-2.0,派生关系见仓库 NOTICE),于是就有了这个插件。&lt;/p&gt;
&lt;h2&gt;它能做什么&lt;/h2&gt;
&lt;p&gt;插件是&lt;strong&gt;双面结构&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Host 端&lt;/strong&gt;:cordis 插件(注入 &lt;code&gt;webServer&lt;/code&gt;),提供两个 API,负责「生成桌面图标」和「优雅退出」;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client 端&lt;/strong&gt;:加载进 &lt;code&gt;dsh web&lt;/code&gt; 页面,右下角多出两个小按钮。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整体就是下图这么个关系:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────── dsh web 进程(Host) ───────────────────┐
│  /api/dsh-desktop_quick_launcher/create   → 生成桌面图标   │
│  /api/dsh-desktop_quick_launcher/shutdown → 优雅退出进程   │
│              ▲                               ▲           │
│              └────────── fetch ──────────────┘           │
│   Client(浏览器右下角悬浮按钮):                           │
│     ⏻ 一键退出 · ＋ 一键生成/刷新桌面图标                  │
└──────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;① 双击桌面图标启动&lt;/h3&gt;
&lt;p&gt;第一次用,在界面里点右下角那个「生成图标」小按钮,桌面就会出现一个 &lt;code&gt;DSH-Web&lt;/code&gt; 图标(Windows 是 &lt;code&gt;.lnk&lt;/code&gt;,macOS 是 &lt;code&gt;.command&lt;/code&gt;,Linux 是 &lt;code&gt;.desktop&lt;/code&gt;;dsh 图标会复制到脚本旁边,以后就算包被移动,快捷方式也不会失效)。&lt;/p&gt;
&lt;p&gt;双击之后脚本会做三件事:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先探测&lt;/strong&gt; GUI 地址——如果 &lt;code&gt;dsh web&lt;/code&gt; 已经在运行,直接打开浏览器、退出脚本;&lt;/li&gt;
&lt;li&gt;没在运行的话,&lt;strong&gt;隐藏启动&lt;/strong&gt; &lt;code&gt;dsh web --no-open&lt;/code&gt;,然后轮询等待服务就绪(给了 150 秒预算,冷启动慢的机器也够);&lt;/li&gt;
&lt;li&gt;就绪后自动打开浏览器。打开这一步还带了&lt;strong&gt;三连兜底&lt;/strong&gt;:&lt;code&gt;Start-Process&lt;/code&gt; → &lt;code&gt;explorer.exe&lt;/code&gt; → &lt;code&gt;cmd /c start&lt;/code&gt;,&quot;启动成功了但没弹浏览器&quot;这种情况基本被消灭了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;排查也简单:每一步(调用了什么命令、找到了哪个 dsh、spawn 的 pid、子进程早退、就绪、每次打开尝试、超时)都写进脚本旁边的 &lt;code&gt;launcher.log&lt;/code&gt;,出问题看一个文件就够,不用猜。&lt;/p&gt;
&lt;h3&gt;② 界面内一键退出&lt;/h3&gt;
&lt;p&gt;右下角有一个固定的&lt;strong&gt;圆形电源按钮&lt;/strong&gt;(悬浮在页面角落)。点击后弹出的是&lt;strong&gt;自定义确认弹窗&lt;/strong&gt;,不是浏览器那个又丑又不可控的原生 &lt;code&gt;confirm&lt;/code&gt;;确认后请求 &lt;code&gt;POST /shutdown&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宿主进程收到后走&lt;strong&gt;优雅退出&lt;/strong&gt;(优先用 dsh launcher 提供的 &lt;code&gt;ctx.appExit&lt;/code&gt;,没有就回退 &lt;code&gt;process.exit(0)&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;关键细节:先让响应送达浏览器,&lt;strong&gt;再退出进程&lt;/strong&gt;,然后页面自己关闭——不会出现&quot;点了没反应&quot;的错觉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按钮旁边还有个小按钮,用来&lt;strong&gt;一键生成/刷新桌面图标&lt;/strong&gt;,结果用 toast 提示。比如图标被误删了、或者想换一套启动配置,不用再摸终端。&lt;/p&gt;
&lt;h3&gt;③ 安全边界:只允许本机操作&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;create&lt;/code&gt; 和 &lt;code&gt;shutdown&lt;/code&gt; 这两个接口&lt;strong&gt;仅限 loopback&lt;/strong&gt;:会同时校验 socket 地址、Host 头和同源标记。也就是说,即使哪天把 &lt;code&gt;dsh web&lt;/code&gt; 暴露到了局域网,别人也没法远程给你创建图标或者远程关机。默认还带 &lt;code&gt;enabled&lt;/code&gt; 总开关,不想要可以整体关掉。&lt;/p&gt;
&lt;p&gt;配置走 schemastery,暴露在 &lt;code&gt;desktop-quick-launcher&lt;/code&gt; 命名空间下:&lt;code&gt;enabled&lt;/code&gt; / &lt;code&gt;announceToAgent&lt;/code&gt; / &lt;code&gt;dshCommand&lt;/code&gt; / &lt;code&gt;url&lt;/code&gt; / &lt;code&gt;profile&lt;/code&gt; / &lt;code&gt;iconPath&lt;/code&gt; / &lt;code&gt;confirmShutdown&lt;/code&gt;,可以在 dsh web 的设置里按需调整(比如换端口、换自定义图标)。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;还没发布到 npm,先走 GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dsh plugin --profile web add github:KLucen/dsh-desktop_quick_launcher
# 重启 dsh web 后生效(客户端产物是启动时装载的)
dsh web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直连不了 github.com 的话(比如在墙内),用镜像地址:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dsh plugin --profile web add &quot;https://gh-proxy.com/https://codeload.github.com/KLucen/dsh-desktop_quick_launcher/tar.gz/refs/heads/main&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启后进页面,右下角应该能看到按钮了;点「生成图标」→ 桌面出现 &lt;code&gt;DSH-Web&lt;/code&gt; → 以后双击就能开,用完点电源按钮就能关,再也不用为它专门开终端了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;小提醒:第一次双击启动时服务要冷启动(最长 150 秒),&lt;strong&gt;别在就绪前反复双击&lt;/strong&gt;——多个脚本实例会各自拉起一个 &lt;code&gt;dsh web&lt;/code&gt;,后来的会因 3080 端口被占用而退出(日志里会提示),等第一次启动完成就好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;踩坑记录(写给想写 dsh web 插件的人)&lt;/h2&gt;
&lt;p&gt;开发过程中最折腾的不是功能本身,而是 &lt;strong&gt;Client 端产物怎么让 dsh web 正常加载&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DSH 的浏览器端 loader 要求每个插件的 &lt;code&gt;./client&lt;/code&gt; 入口是 &lt;strong&gt;classic script&lt;/strong&gt;,并且必须通过 &lt;code&gt;window.__ModuleLoader__.load({ id, factory })&lt;/code&gt; 自注册(&lt;code&gt;react&lt;/code&gt;/&lt;code&gt;react-dom&lt;/code&gt; 是由 loader 的 &lt;code&gt;require&lt;/code&gt; 注入的);&lt;/li&gt;
&lt;li&gt;如果直接发一个纯 ESM 的 &lt;code&gt;client.mjs&lt;/code&gt;,&lt;code&gt;dsh web&lt;/code&gt; 会&lt;strong&gt;启动中止&lt;/strong&gt;,报 &lt;code&gt;loaded without registering ... via ModuleLoader.load&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;解决方案:&lt;code&gt;pnpm build&lt;/code&gt; 里用 esbuild 做一个包装步骤(&lt;code&gt;scripts/wrap-client.mjs&lt;/code&gt;)产出 &lt;code&gt;lib/client.js&lt;/code&gt;,再在 VM 里校验产物(&lt;code&gt;scripts/verify-client.mjs&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;血泪教训:别图省事用 PowerShell 手写包装文件——会破坏 UTF-8,界面中文直接变乱码,老老实实从源码 rebuild。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;手动升级也有个坑:直接 bump 版本后 &lt;code&gt;pnpm install&lt;/code&gt;,顶层 &lt;code&gt;node_modules&lt;/code&gt; 里的条目可能还链着旧版本的 store 目录。正确姿势是先 &lt;code&gt;dsh plugin --profile web remove dsh-desktop_quick_launcher&lt;/code&gt;,再重新 add。&lt;/p&gt;
&lt;h2&gt;已知限制(如实说)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;退出按钮会终止&lt;strong&gt;整个&lt;/strong&gt; &lt;code&gt;dsh web&lt;/code&gt; 进程:正在跑的会话/任务会被中断(会话有持久化,重启后可恢复);&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/create&lt;/code&gt; 和 &lt;code&gt;/shutdown&lt;/code&gt; 刻意只限 loopback:通过远程浏览器连接局域网暴露的服务时,这两个按钮不可用;&lt;/li&gt;
&lt;li&gt;Windows 下生成的图标固定叫 &lt;code&gt;DSH-Web.lnk&lt;/code&gt;,同名快捷方式会被覆盖;&lt;/li&gt;
&lt;li&gt;macOS / Linux 的启动脚本由同一套模板生成,但主要在 Windows 上开发验证,那边有问题欢迎带 &lt;code&gt;launcher.log&lt;/code&gt; 反馈;&lt;/li&gt;
&lt;li&gt;SDK 依赖锁定在 &lt;code&gt;@deepseek-ai/* 0.1.2-rc.1&lt;/code&gt; 一线,会随 DSH 发布节奏更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;尾巴&lt;/h2&gt;
&lt;p&gt;至此,&lt;code&gt;dsh web&lt;/code&gt; 终于有点「正经本地应用」的样子了:桌面上双击图标就能开,用完点一下电源按钮就走,不用再记命令、不用再找终端。&lt;/p&gt;
&lt;p&gt;如果这个插件对你有用,欢迎去 &lt;a href=&quot;https://github.com/KLucen/dsh-desktop_quick_launcher&quot;&gt;GitHub 仓库&lt;/a&gt; 点个 Star,有问题或建议直接开 issue;&lt;a href=&quot;https://github.com/KLucen/dsh-desktop_quick_launcher/blob/main/README.zh.md&quot;&gt;中文 README&lt;/a&gt; 里有更完整的安装和故障排查说明。&lt;/p&gt;
&lt;p&gt;水完这篇,证明博主还活着(确信)。&lt;/p&gt;
</content:encoded></item><item><title>《深入理解 AI Agent：设计原理与工程实践》章节精读笔记</title><link>https://www.klucen.cn/posts/66321874.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/66321874.html/</guid><pubDate>Mon, 03 Aug 2026 17:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《深入理解 AI Agent：设计原理与工程实践》章节精读笔记&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;原书&lt;/strong&gt;：《深入理解 AI Agent：设计原理与工程实践》 李博杰 著（Pine AI 首席科学家）
&lt;strong&gt;版本&lt;/strong&gt;：v1.3 · 2026 年 7 月 27 日 · 全文开源（含全部配图与配套实验代码）
&lt;strong&gt;定位&lt;/strong&gt;：面向工程师的 AI Agent 系统性专著。全书以&quot;实践在前、命名在后&quot;为立场——作者在 Pine（首个能自主与真人交互、可靠独立处理涉及金钱的敏感长程任务的通用 Agent）的实战中倒逼出大量架构原则，后来才被 Anthropic 等公司提炼为 Skill、harness、loop engineering 等名词。
&lt;strong&gt;本书使命&lt;/strong&gt;：帮助你理解和掌握&quot;创造 Agent&quot;的原则。Agent 是宇宙演化史上一种全新的存在：它能通过生成代码实现自举和自我进化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、全书总纲：一条公式与一条主线&lt;/h2&gt;
&lt;h3&gt;核心公式（全书地基）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent = LLM + 上下文 + 工具&lt;/strong&gt; = 大脑 + 眼睛 + 手脚&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;类比&lt;/th&gt;
&lt;th&gt;广义理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;（大语言模型）&lt;/td&gt;
&lt;td&gt;大脑&lt;/td&gt;
&lt;td&gt;不只是一组模型参数，而是整个决策内核：理解意图、思考规划、做出判断。能力来自预训练（世界知识与语言能力）+ 后训练（决策策略，第七章）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文&lt;/strong&gt;（Context）&lt;/td&gt;
&lt;td&gt;眼睛&lt;/td&gt;
&lt;td&gt;Agent 在每个决策点能看到的全部信息：指令、环境信息、用户记忆、领域知识、自身状态、任务进展、工具定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;工具&lt;/strong&gt;（Tools）&lt;/td&gt;
&lt;td&gt;手脚&lt;/td&gt;
&lt;td&gt;Agent 能做的所有事情的集合：预定义工具、按需加载的 Skills、动态生成的代码、委托的子 Agent、与用户沟通、响应外部事件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;生产形态下的扩展公式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent = Model + Harness&lt;/strong&gt;，其中 &lt;strong&gt;Harness = 上下文 + 工具 + 约束 + 验证 + 纠正&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;最小公式（Demo 视角）：只有 LLM、上下文、工具即可跑起来；&lt;/li&gt;
&lt;li&gt;扩展公式（生产视角）：补上约束（限定能做什么/不能做什么）、验证（检查做得对不对）、纠正（做错了怎么补救）三层工程外壳。&lt;/li&gt;
&lt;li&gt;推论：模型能力正在商品化，真正的竞争力在模型之外的 Harness——&quot;行业正在从&apos;能做事&apos;向&apos;可靠地做事&apos;转变&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工程范式演进弧线（第一章 1.2.1）&lt;/h3&gt;
&lt;p&gt;软件工程 → 提示工程（Prompt Engineering）→ 上下文工程（Context Engineering）→ Harness 工程 → &lt;strong&gt;Loop 工程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;层层包含：提示工程 ⊂ 上下文工程 ⊂ Harness 工程 ⊂ Loop 工程。每一层都在前一层基础上扩展工程师的关注范围。实证：LangChain 在 Terminal Bench 2.0 上仅靠优化 Harness（自验证循环、循环检测、工具中间件）就把 Coding Agent 从 52.8% 提升到 66.5%（30 名开外跃升至前 5），&lt;strong&gt;没换模型&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;贯穿全书的判断&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上下文的质量决定 Agent 能力上限&lt;/strong&gt;，而非模型参数量。&quot;人和模型一样，最重要的是 Context&quot;（OpenAI 翁家翌）。一个中等模型 + 精心组织的上下文，往往胜过顶级模型在信息匮乏下的盲目摸索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有评估，就没有进步&lt;/strong&gt;（第六章）。评估是科学方法论做工程的地基。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据和环境比算法更重要&lt;/strong&gt;（第七章主线）：先验（基础模型）&amp;gt; 环境 &amp;gt; 算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实践在前，命名在后&lt;/strong&gt;：等名词流行再动手就慢了一步。提前两步：① 拥有对 Agent 能力上限要求极高的真实业务；② 建立评估机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型与 Agent 共同演进&lt;/strong&gt;：Harness 里层层叠叠的兜底逻辑，记录的都是模型此刻还做不稳的地方；下一代模型把这些约束内化后，Harness 再往上走一层。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、引言：为什么写这本书&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实践出真知&lt;/strong&gt;：作者团队打造的 Pine（自主替用户打电话协商账单、退款、取消订阅的通用 Agent）动辄几十轮交涉，任何一步出错都会造成真金白银的损失，正是这种苛刻的可靠性要求倒逼出了全书架构原则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;实践在前、命名在后&quot;的三个例证&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Skill 概念流行前，已用动态加载提示词解决提示词无限膨胀；用命令行执行工具解决工具列表膨胀；用系统状态栏解决 Agent 不感知环境/时间/工作状态。&lt;/li&gt;
&lt;li&gt;harness 概念流行前，已用类似 Claude Code 的方法解决工具调用不稳定、幻觉、危险/越权操作、指令不遵循。&lt;/li&gt;
&lt;li&gt;loop engineering 概念流行前，已使用&quot;提议者-审核者（proposer-reviewer）&quot;方法解决模型过早认为任务完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Richard Sutton 的启示&lt;/strong&gt;：宇宙演化经历尘埃→恒星→生命→智能体（designed entities）四阶段。生物进化是盲目的，而 Agent 能理解自身运作机制、创造新智能体、甚至改进自己——&quot;像一个程序员编写了另一个程序员&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两大&quot;乌云&quot;（后记展开）&lt;/strong&gt;：① 大世界假设——训练数据永远覆盖不了每个具体用户/公司的情境知识，模型只能在上岗后持续学习（对应第三章记忆、第八章持续进化）；② 前沿开放任务没有现成答案，Agent 只能从实验成败中自主学习。结论：&lt;strong&gt;模型最强的能力终将不是记住，而是学习与适应&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;三、第 1 章：AI Agent 入门（全书概念地图）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：快速引入 Agent 核心公式、运行循环、工程框架和设计模式，为后续章节提供统一术语与参照坐标。&lt;/p&gt;
&lt;h3&gt;1.1 现代 Agent = LLM + 上下文 + 工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具：Agent 的手脚&lt;/strong&gt;。五类工具（按与外界互动方向分类）：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;感知工具&lt;/strong&gt;：访问信息（搜索引擎、文件系统、API、数据库）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行工具&lt;/strong&gt;：改变世界（代码执行、文件操作、系统命令、外部 API）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协作工具&lt;/strong&gt;：与其他 Agent 分工、请求人类确认、协调行动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件触发工具&lt;/strong&gt;：特殊——不是 Agent 主动调用，而是外部输入（新邮件、定时器、Webhook）驱动 Agent 开始工作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户沟通工具&lt;/strong&gt;：Agent 主动与用户建立连接（文字、语音、邮件）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;工具调用（Tool Calling / Function Calling）四步流程：告诉模型可用工具 → 模型判断是否调用/调哪个/传什么参 → 工具执行并把结果追加到上下文 → 模型据此决定下一步。这构成 ReAct 的基础。&lt;/li&gt;
&lt;li&gt;MCP（模型上下文协议）正在让工具接入变得像安装插件，但设计原则不会过时。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM：Agent 的大脑&lt;/strong&gt;。解析真实意图（用户说的往往不是他真正想要的）、拆解任务、持续决策。独特能力：&lt;strong&gt;内部思考&lt;/strong&gt;（采取行动前的规划与推演，不改变外部环境但提升行动质量）；&lt;strong&gt;零样本泛化&lt;/strong&gt;（组合已有知识处理全新任务）；&lt;strong&gt;少样本适应&lt;/strong&gt;（两三个示例即可学会新模式）。
&lt;ul&gt;
&lt;li&gt;**模型即 Agent（Model as Agent）**新范式：先进模型通过后训练把&quot;何时调用工具、如何编排&quot;内化为原生能力（例：Kimi K3 约 2.8 万亿参数 MoE，100 万 token 上下文，始终开启思考模式；GPT-5.6 把 Deep Research 设计理念内化到模型层，自带意图澄清机制——不立即执行，先提问确认真实需求）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文：Agent 的眼睛&lt;/strong&gt;。从 API 视角看，每次调用的上下文由五部分组成：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;系统提示词&lt;/strong&gt;（System Prompt）：开发者编写、全程不变，即&quot;岗位说明书&quot;（含用户记忆、动态环境状态）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具定义&lt;/strong&gt;（Tool Definitions）：工具名称、功能、参数格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户消息&lt;/strong&gt;（User Messages）：用户输入 + 可能经 RAG 引入的外部知识&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型回复&lt;/strong&gt;（Assistant Messages）：思考过程（reasoning）+ 文本内容（content）+ 工具调用（tool_calls）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具执行结果&lt;/strong&gt;（Tool Results）：Agent 下一步思考的直接依据&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;前两项构成&lt;strong&gt;静态前缀&lt;/strong&gt;，后三项构成&lt;strong&gt;动态轨迹（trajectory）&lt;/strong&gt;。&lt;strong&gt;Agent 的上下文 = 静态前缀 + 轨迹&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;**消融实验（实验 1-1）**证实：去掉工具定义 → Agent 完全丧失行动能力；缺工具执行结果 → 反复犯错、无限循环；缺历史消息 → 无法保持连贯。上下文决定 Agent 能看到什么，而 Agent 只能基于看到的信息做决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 ReAct 循环：想 → 做 → 看&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ReAct = Reasoning + Acting&lt;/strong&gt;，实际循环三环节：思考当前该做什么 → 调用工具行动 → 观察结果继续思考，直到任务完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轨迹（trajectory）&lt;/strong&gt; = 用户消息 + 模型回复（思考+工具调用）+ 工具执行结果，随交互不断增长；每次 LLM 调用都基于&quot;静态前缀 + 轨迹&quot;。&lt;/li&gt;
&lt;li&gt;轨迹的价值：全局认知、可解释可调试、可总结进知识库、可做强化学习训练数据（闭环优化）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 Harness 工程：模型之外的竞争力&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个能跑的 Demo 与一个可靠的产品之间隔着巨大的鸿沟：模型会幻觉（编造不存在的工具/参数）、选错工具、无法自我恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness 五个功能&lt;/strong&gt;：上下文设计（信息充分性）、工具设计、约束、验证、纠正。围绕&quot;上下文+工具&quot;构建的保障层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建有效 Agent 的三原则&lt;/strong&gt;（Anthropic 经验）：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保持简单&lt;/strong&gt;：从最简单的方案开始，直接 API 调用优于复杂框架，清晰代码优于聪明抽象；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持透明&lt;/strong&gt;：明确显示规划步骤、执行日志、决策轨迹（调试 + 信任）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计好工具接口（ACI，Agent-Computer Interface）&lt;/strong&gt;：从 Agent 视角设计接口。用&quot;防呆（Poka-yoke）&quot;设计消除错误——SIM 卡缺角、微波炉门没关好不加热。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何选择模型&lt;/strong&gt;：&quot;御三家&quot;（OpenAI GPT/o、Anthropic Claude、Google Gemini）各有侧重；国内模型（豆包低延迟、Kimi Agent 能力强、Qwen/DeepSeek 开源低成本）；关注延迟（每轮慢 2 秒 × 20 轮 = 多等 40 秒）与多模态支持；&lt;strong&gt;不要只看排行榜，要在自己的任务上做评估&lt;/strong&gt;（第六章）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编排模式：从简单到复杂&lt;/strong&gt;——先优化单个 LLM 调用 → 需要多步骤且可拆分为固定子任务时用&lt;strong&gt;工作流（Workflow，确定性编排，路径代码写死）&lt;/strong&gt; → 只有需要动态决策和灵活执行路径时才用&lt;strong&gt;自主 Agent（本质 = 循环中使用工具的 LLM，即 ReAct）&lt;/strong&gt;。Agent 系统通常用延迟和成本换取更好的任务性能，要谨慎权衡。退出条件：调用 final_answer / 无工具调用返回 / 达到最大轮次 / 错误次数超限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;护栏与安全性（Guardrails）&lt;/strong&gt;：分层防御机制，单护栏不够，组合使用。按防护位置分三类：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入侧&lt;/strong&gt;：相关性分类器、安全分类器（区分&lt;strong&gt;越狱&lt;/strong&gt;=用户自己绕过安全限制 vs &lt;strong&gt;提示注入&lt;/strong&gt;=攻击者通过外部数据间接操纵）、内容审核、基于规则的保护（黑名单/长度限制/正则）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行侧&lt;/strong&gt;：工具风险评级（可逆性、权限等级、财务影响）、权限分类（哪些操作需用户确认）、熔断器、错误恢复机制。&lt;/li&gt;
&lt;li&gt;输出侧：输出过滤与校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;章节结论&lt;/strong&gt;：眼睛（上下文）是决定性因素；Harness 是竞争力所在；从工作流到自主 Agent 是降低意外风险最实用的顺序；&lt;strong&gt;安全是架构问题&lt;/strong&gt;——从第一行代码就要考虑，而不是上线前打补丁；安全贯穿模型、上下文、工具、协作、社会五个层面。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;四、第 2 章：上下文工程（全书最关键的一章）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：系统讲解上下文——Agent 的&quot;眼睛&quot;。上下文工程首先是一个技术问题，但更根本的是一个&lt;strong&gt;组织问题&lt;/strong&gt;：大多数团队的关键知识是隐性的（架构决策只在老员工记忆里、业务规则口口相传）。&quot;对远程工作友好的团队往往也对 AI Agent 友好&quot;（Linux 内核是范例：透明、文档驱动）。&lt;/p&gt;
&lt;h3&gt;2.1 上下文：决定 Agent 能力上限的关键&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Coding Agent 的最低信息需求三类：&lt;strong&gt;实时代码上下文&lt;/strong&gt;（目录结构、模块职责、数据结构、代码规范）、&lt;strong&gt;流程规范&lt;/strong&gt;（Git 分支策略、提交规范、审查流程、CI/CD）、&lt;strong&gt;环境信息&lt;/strong&gt;（开发/测试/生产配置、API 密钥管理）。&lt;/li&gt;
&lt;li&gt;&quot;AI Agent 就像一个永远的新员工：给足背景信息，它能干得很好；什么都不告诉它，再聪明也是白搭。&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 Agent 如何调用大模型：理解 API 的上下文结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;消息的四种角色&lt;/strong&gt;：system（系统提示词，最高优先级，通常只有一条且在最前）、user（用户输入）、assistant（模型之前的回复，会被放回消息列表让模型&quot;记住&quot;自己说过什么）、tool（工具结果，通过 tool_call_id 与调用关联）。工具定义（tools）是请求的独立字段而非消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单轮对话&lt;/strong&gt;：每次调用无状态——模型需要的所有信息必须在 messages 中完整提供。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带工具调用的多轮交互（Agent 核心循环）&lt;/strong&gt;：模型返回 tool_calls（不是最终回复）→ Agent 框架执行工具 → 把完整历史 + tool 结果送回 → 模型生成最终回复。&lt;strong&gt;关键：模型负责决策，框架负责执行&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用代码实现&lt;/strong&gt;：核心就是一个 while 循环——返回了 tool_calls 就执行工具并继续，没有就输出退出。框架的全部工作就是管理 messages 列表。&lt;strong&gt;本章后续所有技术本质都在优化这个列表的内容和结构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;上下文结构 = &lt;strong&gt;静态前缀（System Prompt + Tool Definitions）+ 轨迹（动态增长的消息历史）&lt;/strong&gt;。&quot;前面不能动、后面可以压缩&quot;是后续 KV Cache 与压缩技术的基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 KV Cache 友好的上下文设计（全书技术密度最高的部分）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直觉&lt;/strong&gt;：模型每生成一个 token 都要&quot;回头看&quot;前文所有 token 的中间计算结果；KV Cache 把前文结果缓存下来，下一轮只算新增部分。&lt;strong&gt;前提是前缀完全不变——改一个字符，缓存全部作废&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三条核心实践结论&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;系统提示词和工具定义一旦确定就不要改（哪怕多一个空格，都会导致延迟成倍增加、成本上升）；&lt;/li&gt;
&lt;li&gt;动态信息永远&lt;strong&gt;追加到末尾&lt;/strong&gt;（时间戳、用户状态作为新消息追加，而不是修改已有提示词）；&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;标准 API 格式&lt;/strong&gt;，不要自行拼接消息（Chat Template 把结构化消息翻译成模型训练时见过的固定 token 序列；自行拼接&quot;USER:/ASSISTANT:&quot;偏离训练格式会削弱多步思考能力）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;反面案例：某客服 Agent 在系统提示词里加一行 &lt;code&gt;Current time: &amp;amp;#123;&amp;amp;#123;now&amp;amp;#125;&amp;amp;#125;&lt;/code&gt;，首 token 延迟从 0.5 秒涨到 3-5 秒，月度账单几乎翻倍——时间戳让 KV Cache 每次请求全部失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chat Template（聊天模板）&lt;/strong&gt;：把结构化 API 消息转换成模型能处理的线性 token 流（特殊标记如 &amp;lt;|im_start|&amp;gt;、&amp;lt;|im_end|&amp;gt; 界定角色与消息边界）。不同模型家族（Qwen、Llama、Gemma）使用不同&quot;信封格式&quot;。
&lt;ul&gt;
&lt;li&gt;关于&lt;strong&gt;思考（reasoning）的保留&lt;/strong&gt;：行业从&quot;剥离历史思考&quot;（DeepSeek R1 早期）反转为&quot;强制回传全部 reasoning_content&quot;（DeepSeek V4，否则报错；Kimi K2、GLM-5 同样；Claude 要求 thinking block 带签名校验原样回传）。&lt;strong&gt;对 Agent 场景而言，思考不是废料，而是状态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KV Cache 原理&lt;/strong&gt;：无缓存时 prefill 阶段注意力计算量随上下文长度平方级增长；有缓存时每步解码只需线性遍历。修改前缀导致所有层缓存失效（Transformer 层间串联传导）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KV Cache vs Prompt Cache 两个层级&lt;/strong&gt;：KV Cache 是模型内部优化（加速单次请求内 token 生成）；Prompt Cache 是 API 服务层优化（跨请求复用相同前缀的计算结果，直接省计费）。Prompt Cache 读取成本约为首次计算的十分之一（Anthropic/DeepSeek/GPT-5 系列）。注意各家机制差异：Anthropic 需显式 cache_control 断点（写入加价约 1.25 倍、最小缓存长度、TTL 约 5 分钟），OpenAI 自动前缀缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存作为架构约束（生产级）&lt;/strong&gt;：Claude Code 的实践——提示词结构由缓存边界决定（边界前可跨用户/会话全局缓存，边界后是用户/会话特定信息；动态条件如 OS 类型、模式、语言会产生 2^N 种缓存键，全部严格归类到边界之后）；&lt;strong&gt;子 Agent 必须与父 Agent 字节级对齐&lt;/strong&gt;（提示词、工具定义、模型配置逐字节匹配才能命中缓存）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前沿延伸（可编辑、可组合的&quot;笔记&quot;）&lt;/strong&gt;：prefill 阶段模型其实在&quot;做笔记&quot;——字段自身的 KV 对最终决策贡献往往不到 1%，真正影响输出的是它在下游留下的结论。由此实现：编辑（有显式 CoT 时，改动可顺着已缓存思考以约 1% 算力传播）与组合（把预计算的&quot;技能&quot;缓存块用 RoPE 重定位拼接，长上下文从 O(L²) 重算降到 O(L) 拼接，质量与重算无差别）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 提示工程：优化系统提示词&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;检验标准：&quot;如果一个聪明的新员工读完你的系统提示词还不知道该怎么做，Agent 也一样不知道。&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语气与风格（&quot;人格&quot;）&lt;/strong&gt;：&quot;You MUST...&quot;/大写比&quot;Please avoid...&quot;更能引起注意，但过度使用会稀释，应保留给真正关键的约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化提示（&quot;格式&quot;）&lt;/strong&gt;：现代模型对结构化输入敏感（训练数据含大量结构化内容）。XML 标签名自带语义（&amp;lt;working_directory&amp;gt; 优于&quot;当前目录：...&quot;）；XML 负责机器可解析的精确语义，Markdown 负责人机共读的组织逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程驱动 vs 规则堆砌（&quot;组织方式&quot;）&lt;/strong&gt;：上百条零散规则（无流程图、无优先级）即使对人也是灾难。流程驱动（SOP 式）让模型任何时刻清楚自己处于哪个阶段、目标是什么、下一步去哪。&lt;strong&gt;消融实验（实验 2-4）：打乱信息组织结构 → 任务成功率下降超过 30%&lt;/strong&gt;（如&quot;先验证身份再处理退款&quot;被拆散后，Agent 有时跳过验证直接退款）。结论：&lt;strong&gt;对人类友好的信息组织方式，对模型同样友好&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务规则细化（&quot;内容&quot;）&lt;/strong&gt;：最容易被忽视却最关键的环节，需要产品经理深度参与。案例：帮用户打电话谈判的计费系统——&quot;按省钱提成 / 按服务收 tip / 特别难办的预收款&quot;三种模式必须把决策规则明确到可执行程度（&quot;退款和取消服务绝对不能按提成&quot;、&quot;NEVER use percent...&quot;），模糊规则会导致行为极不稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Few-shot 示例&lt;/strong&gt;：当期望输出难以用规则精确描述时，两三个高质量示例胜过等量篇幅的抽象规则。工程决策：示例放系统提示词（静态前缀一部分）还是伪造 user/assistant 消息（按会话类型选用）；&lt;strong&gt;示例应保持字节级稳定&lt;/strong&gt;（动态检索最相关示例 = 每次改写前缀，缓存持续失效）；数量不是越多越好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具定义设计&lt;/strong&gt;：每个工具描述应有使用边界（&quot;NEVER invoke grep as a Bash command&quot;）、具体示例（timezone: &apos;America/New_York&apos;）、性能提示（&quot;Batch your tool calls&quot;）、工具间协作关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示注入：上下文安全的核心威胁&lt;/strong&gt;——详见 2.7 前的小节内容（见下文 2.5 前）。防御核心：帮模型分清&quot;指令&quot;与&quot;数据&quot;（来源标记、指令/数据分离、输入清洗），但只是第一道防线，执行层防御（权限、沙盒、独立审查）在第四、五章展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 动态提示词与 Agent Skills&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skills = 领域能力的可组合单元&lt;/strong&gt;：把 Agent 能力模块化为独立、可按需加载的知识包。设计哲学：&lt;strong&gt;渐进式披露（Progressive Disclosure）&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一层（元数据）&lt;/strong&gt;：SKILL.md 开头的 YAML frontmatter（name + description），启动时只注入数百 token 的目录摘要。&lt;strong&gt;description 要写成路由条件而非功能介绍&lt;/strong&gt;——&quot;何时该用我&quot;比&quot;我能做什么&quot;重要得多；&lt;strong&gt;反例（Don&apos;t use when）是关键&lt;/strong&gt;，缺少反例路由准确率明显下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二层（核心流程）&lt;/strong&gt;：Agent 判断需要时通过 Skill 工具加载完整 SKILL.md，作为 tool result 进入对话历史。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三层（细则）&lt;/strong&gt;：通过文件引用深入子文档（html2pptx.md、reference.md 等）。&lt;/li&gt;
&lt;li&gt;Skill 可捆绑可执行代码工具和模板文件——从知识传递升级为能力赋予。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills 的实现方式与权衡&lt;/strong&gt;（内容放哪）：
&lt;ul&gt;
&lt;li&gt;方式一：注入系统提示词——指令遵循最强，但每次加载都破坏 KV Cache 前缀；&lt;/li&gt;
&lt;li&gt;方式二：作为普通文件读取（tool result 出现在上下文中间）——不破坏缓存，但要求模型在长上下文中间位置识别并遵循指令，不同模型支持差异大（Claude 最可靠）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方式三（生产实现，Claude Code）&lt;/strong&gt;：元数据作为动态上下文提供给模型（不反复改写稳定前缀），完整内容通过专用工具按需加载——路由与执行分离。&lt;/li&gt;
&lt;li&gt;澄清：Skill 元数据以什么消息角色注入（user/system/&amp;lt;system-reminder&amp;gt;）是 Harness 实现细节，不是机制固定要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills 与工具的关系&lt;/strong&gt;：Skills 对 KV Cache 极为友好——工具数量始终很少（第五章七个核心工具），Skill 内容按需加载不影响缓存前缀。把&quot;工具选择&quot;问题转化为&quot;知识检索&quot;问题（LLM 擅长的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.6 Agent 状态栏：通过元信息增强轨迹管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理论基础&lt;/strong&gt;：&lt;strong&gt;上下文学习更像检索而非推理&lt;/strong&gt;——注意力擅长在已有内容里&quot;查找&quot;，不擅长在一次前向传播里主动&quot;归纳统计&quot;。&quot;上下文窗口是一台只有一半的检索引擎&quot;：检索半很强（RAG 内置进每次前向传播），但缺&quot;提炼层&quot;——任何&quot;关于这些内容的结论&quot;（共多少条、有没有超标、进展到哪一步）模型每次都要从原始记录现算一遍，代价随内容量 N 上涨。&lt;/li&gt;
&lt;li&gt;实际场景：系统提示词要求&quot;每个商家最多拨打 3 次&quot;，但 Agent 经常数不清打了几次又打第 4 次。解法：在每次工具调用结果中直接注入&quot;本次是第 3 次呼叫&quot;——错误率大幅降低。本质：把分散在上下文各处的隐式状态提炼为可直接使用的&lt;strong&gt;显式知识&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实验数据（2.4 万次评测）&lt;/strong&gt;：弱模型补回来的是&lt;strong&gt;准确率&lt;/strong&gt;（最弱模型涨 40-54 个百分点，2B 小模型追平不带状态栏的前沿大模型）；强模型省下来的是&lt;strong&gt;效率&lt;/strong&gt;（思考 token 砍掉八九成，延迟、花费各降约一个数量级）；带上状态栏后思考量变得恒定（&quot;瞥一眼&quot;那几格状态）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三条直接能照做的经验&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态栏要用代码维护，别拿大模型维护&lt;/strong&gt;——20 行正则函数就能达到标准答案级准确度，LLM 反而会编造；&lt;/li&gt;
&lt;li&gt;状态栏要写成能一眼定位的&lt;strong&gt;键值对&lt;/strong&gt;（&quot;衣物: 9 件（合格7、次品2）&quot;），散文形式效果明显更差；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态栏最有价值的是模型无从推断的外部事实&lt;/strong&gt;（&quot;闭卷考试变成随时能查一眼真实世界&quot;）——来自真实观测的信息价值最高；拍脑袋编的或可被污染数据源的状态栏会误导模型（投毒风险）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构成&lt;/strong&gt;：任务规划（TODO 列表放轨迹末尾）、事件的侧信道信息（精确时间/地理位置/距上次回复间隔）、环境当前状态（系统时间、工作目录、异常提醒&quot;该工具已被重复调用 N 次&quot;）、可用能力清单（已安装 Skill 元数据）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位置&lt;/strong&gt;：以 user 角色 + &amp;lt;agent_status&amp;gt; 标签包裹的元信息追加在上下文最末尾——紧邻模型即将生成的新 token，获得最高注意力权重；追加而非修改，不破坏缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态更新的两种实现&lt;/strong&gt;：实现一&quot;每轮替换&quot;（删除旧状态追加新状态，失效范围限于末尾几轮缓存）；实现二&quot;持久追加&quot;（Claude Code 的 &amp;lt;system-reminder&amp;gt; 方式，缓存完全友好但陈旧状态累积）。经验法则：状态更新频繁且轨迹长选实现二，轨迹短或单条状态大选实现一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间感（time sense）&lt;/strong&gt;：把缺失的能力拆成三个可度量的轴——&lt;strong&gt;紧迫度&lt;/strong&gt;（把投入的力气匹配到时钟上）、&lt;strong&gt;坚持度&lt;/strong&gt;（分清真墙假墙、知道活儿干完没有）、&lt;strong&gt;警觉度&lt;/strong&gt;（把工具响应的时间异常升级为值得追查的假设）。实验发现&quot;缺时间感&quot;是当前后训练普遍漏掉的一项控制；推理时可用&quot;状态栏+操作手册&quot;装上，也可蒸馏进权重（第七章：稀疏结果奖励学不会，逐 token 稠密信号才学会）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.7 上下文压缩策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么需要压缩（两个截然不同的动机）&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;解决长度与成本约束（窗口有限、token 越多越贵越慢）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提升思考质量&lt;/strong&gt;——总结后的知识比原始形式更利于模型使用（10 次网页搜索的原始结果散落各处，模型需在数万 token 中反复检索；先做一次结构化总结&quot;目前已知 A 是...，还缺 C&quot;，后续思考直接使用精炼表示）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部机制&lt;/strong&gt;：上下文学习更像检索而非推理。黑猫白猫计数例证：100 个笼子装进上下文，模型仍需逐个数（思考而非检索）；提前写入&quot;黑猫 90 只、白猫 10 只&quot;，模型立即检索到结论。长上下文还会导致&lt;strong&gt;上下文腐化（Context Rot）&lt;/strong&gt;——窗口远没满，但 Agent 找不到关键信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩与 KV Cache 看似矛盾实则互补&lt;/strong&gt;：压缩发生在两次 API 调用之间，由框架预处理消息列表；System Prompt 和 Tool Definitions 永远不动；压缩对象是对话历史中的 tool results——替换位置之后的缓存失效，之前的仍有效。&lt;strong&gt;压缩频次要权衡：接近阈值时批量压缩，而不是每轮都压&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产级分层压缩机制（Claude Code 参照，五层）&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;工具结果预算控制（大输出存磁盘，模型只看摘要预览）；&lt;/li&gt;
&lt;li&gt;噪声直接删除（低价值内容不做摘要——对噪声做摘要在浪费 token）；&lt;/li&gt;
&lt;li&gt;API 层微压缩（上下文编辑能力移除指定工具结果，适合上下文即将溢出时使用）；&lt;/li&gt;
&lt;li&gt;归档式摘要（逐轮结构化摘要，像 git log 保留每轮独立记录）；&lt;/li&gt;
&lt;li&gt;全量压缩（LLM 驱动，分两阶段：先压缩会话记忆，不行再全量；配连续失败熔断器——生产数据表明大量会话被困在反复压缩失败的循环中）。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;前三层成本低、对缓存扰动可控，优先使用；后两层兜底。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩策略设计四原则&lt;/strong&gt;：信息价值非均匀分布（关键决策点 &amp;gt; 支撑性证据 &amp;gt; 冗余噪声）；语义完整性（&quot;Sutskever 于 2024 年 5 月离开 OpenAI&quot;不能压成&quot;Sutskever 离开&quot;）；任务相关性（同样内容在不同任务下应有不同压缩结果）；压缩即理解（压缩模块本身需要接近主模型的语言理解能力，形成&quot;模型调用模型&quot;的递归架构）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保留优先级（生产建议）&lt;/strong&gt;：架构决策和关键约束不得摘要；已修改文件列表和关键变更记录完整保留；验证状态（pass/fail）必须保留；未解决 TODO 和回滚笔记必须保留；工具输出可删除仅保留结论。UUID/hash/IP/端口/URL/文件名必须原样保留。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离优于压缩：子 Agent 上下文隔离&lt;/strong&gt;——让大体积中间信息根本不进入主上下文。主 Agent 把&quot;读取大量文件&quot;&quot;大范围搜索&quot;委派给独立子 Agent，只回传几百 token 的结论性摘要。压缩是有损的事后补救，隔离让噪声从一开始绝缘；代价是子 Agent 看不到主上下文，任务描述必须自包含。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.8 本章小结&lt;/h3&gt;
&lt;p&gt;&quot;给模型看什么、怎么组织，比模型本身有多聪明更影响最终的结果。&quot;所有技术共同点：&lt;strong&gt;显式的、工程化的信息管理——不要让模型被动在海量上下文中寻找线索，要主动提供经过提炼的结构化状态&lt;/strong&gt;。回到 Rich Sutton《苦涩的教训》：能更有效利用更多算力的通用方法最终胜出。本章处理的是任务之内的状态更新与上下文腐化；跨任务的持久进化是第八章的主题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、第 3 章：用户记忆和知识库&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：把上下文管理延伸到跨会话的持久化知识体系，让 Agent 记住用户、构建共享知识库。&lt;/p&gt;
&lt;h3&gt;3.1 用户记忆系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;记忆的本质：不是记录用户说过的每一句话，而是构建一个关于用户的&lt;strong&gt;简洁而有效的预测模型&lt;/strong&gt;——投入额外算力（专门的 LLM 调用）将分散在冗长对话历史中的关键信息显式提取和压缩。用户记忆是持久的、可审查的；上下文学习是临时的、会话结束就消失。&lt;/li&gt;
&lt;li&gt;提取过程三特征：&lt;strong&gt;选择性&lt;/strong&gt;（只保留对未来有用的事实）、&lt;strong&gt;抽象化&lt;/strong&gt;（&quot;I prefer window seats&quot;提炼为通用偏好而非绑定具体航班）、&lt;strong&gt;结构化&lt;/strong&gt;（每条记忆标记类型：偏好/限制/账号）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆能力评估：三层次框架&lt;/strong&gt;（实验 3-1，每层 20 个测试用例）：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;基础回忆&lt;/strong&gt;：单会话内回忆个人信息、追踪偏好、上下文切换、记忆更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多会话检索&lt;/strong&gt;：从多个不同对象、不同时期的会话中检索所有相关信息并推理判断（用户有两辆车时预约保养要主动问哪辆；取消&quot;洛杉矶之旅&quot;要关联机票和酒店）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动服务&lt;/strong&gt;（最高标准）：跨很久以前的会话综合信息、提供预见性帮助（订国际航班时主动关联数月前的护照信息并预警过期；手机损坏时整合所有保障方案）。要求没有明确指令时主动规避问题和整合复杂信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;公开基准参考：LoCoMo（约 300 轮、最多 35 个会话的超长多轮对话）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆的层次结构（放哪里）&lt;/strong&gt;：&lt;strong&gt;轨迹&lt;/strong&gt;（单次会话完整原始记录，append-only，对应工作记忆）vs &lt;strong&gt;用户长期记忆&lt;/strong&gt;（跨会话持久化、键值对绑定用户 ID、反复改写合并淘汰）vs &lt;strong&gt;业务状态&lt;/strong&gt;（开发者定义的高层任务阶段抽象，事件驱动架构中重要）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四种存储格式（怎么存）&lt;/strong&gt;（实验 3-2）：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Simple Notes&lt;/strong&gt;（原子事实，键值对）：O(1) 开销极低，但丢失关联性；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced Notes&lt;/strong&gt;（整段叙事）：语义完整，但冗余、难更新、长文难检索；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON Cards&lt;/strong&gt;（类别→键值，结构化）：可检索、可更新，但灵活性差；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced JSON Cards&lt;/strong&gt;（backstory + person + relationship 实体模型）：能回答&quot;为什么存这条&quot;&quot;为谁存&quot;，支持家人关系消歧，代价是生成维护成本高。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;根本张力：简单性与表达力。实践标准：关键且少量数据（偏好、人物关系）用 Advanced JSON Cards；大量且非关键对话事实用 Simple Notes；多数生产系统用混合模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进阶表示：从可执行代码到参数化记忆&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User as Code&lt;/strong&gt;：把记忆介质从文本换成可执行代码（带类型 Python 对象存用户状态、函数编码约束规则）。两阶段：&quot;预写日志 + 周期性检查点&quot;（记忆阶段逐条追加只增事实日志；结构化阶段周期性地重新生成带类型的 Python 结构）。三件事从&quot;LLM 心算&quot;变成确定性代码：&lt;strong&gt;聚合统计&lt;/strong&gt;（&quot;去年出国几次&quot;——检索式记忆在聚合问题上正确率只有 6%-43%，代码一行表达式接近 100%）、&lt;strong&gt;冲突发现&lt;/strong&gt;（用药 vs 过敏史交叉比对）、&lt;strong&gt;约束执行&lt;/strong&gt;（护照到期前 180 天自动报警）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User as Engram（写入局部参数）&lt;/strong&gt;：给每用户训练 LoRA 有&quot;存了却不会用&quot;的困境（fact-LoRA 能复述但不能间接推理——冻结的骨干没学过如何查阅临时挂载的适配器）。Engram 方案：把用户事实精准写入模型中空闲的哈希 N-gram 槽位（预训练已学会哈希查表调取记忆 + 上下文感知门控决定何时调取），不同用户事实落在互不相交槽位、互不干扰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模态记忆&lt;/strong&gt;：感知性的记忆（一张脸、一段嗓音、画家的笔触）经不起转写成文字。Parametric Multimodal User Memory：为冻结模型外挂连续注意力记忆库，让感知以感知形态保存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认知科学基础（存什么）&lt;/strong&gt;：工作记忆 = 上下文窗口；长期记忆三分：&lt;strong&gt;情景记忆&lt;/strong&gt;（具体事件，如&quot;订了下周五去东京的 ANA 航班&quot;）、&lt;strong&gt;语义记忆&lt;/strong&gt;（抽象的一般知识，如&quot;用户是素食者&quot;）、&lt;strong&gt;程序记忆&lt;/strong&gt;（行为模式和流程，如&quot;订票流程：先搜索直飞→确认座位偏好→用常旅客号&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆框架案例&lt;/strong&gt;：&lt;strong&gt;Mem0&lt;/strong&gt;（提取—对比—决策两阶段流水线：提取阶段 LLM 从近期对话+已有摘要提取候选记忆，更新阶段向量检索相近已有记忆并对比决策；嵌入与存储分离可独立替换；Mem0-g 图记忆变体捕捉记忆间关联）；&lt;strong&gt;Memobase&lt;/strong&gt;（用户画像 Profile + 事件记忆 Event Memory；缓冲批处理摊薄 LLM 调用成本）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆压缩与整理机制&lt;/strong&gt;：三层——重要性评分筛选（访问频率、时间衰减、情感强度、信息独特性四因素）、聚类（相似记忆分组生成代表性摘要，原始详情存档）、抽象泛化（具体情景 → 一般规律）；冲突检测用版本化方法（当前地址只留最新版、工作经历保留完整历史）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐私保护：日志脱敏&lt;/strong&gt;（实验 3-3）：用本地 Qwen3 0.6B 小模型做 PII 检测与脱敏（本地部署而非云端——日志含敏感信息，送云端就违背初衷）；识别结构化（身份证/银行卡）、半结构化（地址）与自然语言敏感内容（&quot;我的密码是 abc123&quot;）；LLM 脱敏召回率 95%+ 且假阳性显著低于正则；高吞吐场景用&quot;正则快速过滤 + LLM 深度分析&quot;混合策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 RAG 基础：构建 Agent 的知识获取管道&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAG = 检索增强生成&lt;/strong&gt;：把 LLM 的思考生成能力与外部知识库的广度时效性结合（模型训练数据有截止日期，知识库可随时更新）。流程：检索（Retrieval）→ 增强（Augment）→ 生成（Generate）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档分块（Chunking）&lt;/strong&gt;：为什么必要——嵌入模型输入长度有限、整篇文档压成一个向量无法精确表达、检索目标是只注入相关部分。三种策略：&lt;strong&gt;固定大小切分&lt;/strong&gt;（简单可预测但无视结构）、&lt;strong&gt;递归/结构感知切分&lt;/strong&gt;（按章节/段落/句子自然边界，Markdown/HTML 首选，生产默认）、&lt;strong&gt;语义切分&lt;/strong&gt;（相邻句子嵌入相似度断崖处下刀，质量高但需额外计算）。块大小与重叠是典型权衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稠密嵌入&lt;/strong&gt;：把文本映射到向量空间（语义近则向量近），衡量用余弦相似度（关心方向而非长度）。从 Word2Vec（词汇共现）到上下文感知模型（BERT、BGE-M3，同词不同语境不同向量、支持多语言长文本）。经典例子：&quot;国王&quot;-&quot;男性&quot;+&quot;女性&quot;≈&quot;女王&quot;。&lt;strong&gt;ANN（近似最近邻）算法&lt;/strong&gt;（实验 3-4）：ANNOY（树，不支持增量插入，适合静态库）vs HNSW（图，天然支持增量插入，适合动态场景）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稀疏嵌入（BM25）&lt;/strong&gt;：词袋模型 + 词频饱和（TF，k₁ 控制饱和速度）+ 逆文档频率（IDF，稀有词权重高）+ 长度归一化（b）。BM25 核心逻辑：稀有词单次出现贡献更大（doc 里&quot;蒸馏&quot;TF=3 低于&quot;模型&quot;TF=5，但因更稀有贡献反而更高）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合检索&lt;/strong&gt;：稠密懂语义但漏关键词（搜 HTTP-403 返回&quot;服务器错误&quot;泛泛讨论），稀疏精确匹配但读不懂同义词（搜 kitty 找不到 cat）。典型流水线三阶段：&lt;strong&gt;并行检索 → 结果融合 → 神经重排序&lt;/strong&gt;（跨编码器精排）。检索质量指标：recall@k（该找的找到了吗，最贴近 RAG）、MRR（找到得够不够靠前）、nDCG（整个排序列表的质量）；注意&quot;检索失败率&quot; = 1 - recall@20 这类口径换算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模态信息提取（三条路）&lt;/strong&gt;：&lt;strong&gt;原生多模态处理&lt;/strong&gt;（ViT 视觉编码器映射统一语义空间，保真度最高，适合版式复杂文档）、&lt;strong&gt;提取为文本&lt;/strong&gt;（OCR/转录 → 纯文本，低成本兼容所有模型但丢失版式图表）、&lt;strong&gt;工具化分析&lt;/strong&gt;（文本摘要打底 + analyze_image/analyze_pdf 按需深入）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 超越扁平文本：知识的组织与检索&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么必须超越扁平文本&lt;/strong&gt;（两个案例）：① 黑猫白猫计数问题在知识库尺度上叠加 top-k 截断、检索分数参差、跨文档聚合错位三重障碍；② Xfinity 优惠规则的错误推理——&quot;护士&quot;与&quot;医生&quot;语义相近优先召回案例 B，模型错误推断护士也可享受优惠。结论：必须在索引阶段投入计算资源主动提炼、抽象、结构化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化索引两条路&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAPTOR（树状层次摘要）&lt;/strong&gt;：计算块间相似度聚类 → LLM 生成父节点摘要 → 递归成知识树。检索可跨层：先宏观定位再向下钻取细节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraphRAG（实体关系图）&lt;/strong&gt;：LLM 提取实体-关系三元组构建知识图谱，社区检测划分模块。适合&quot;谁和谁有关？A 如何影响 B？&quot;类关系性问题。局限：三元组提取丢失条件逻辑与时间依赖（&quot;我计划去海滩，如果下雨则去博物馆&quot;被压成孤立事实）、错误提取造成知识污染。&lt;strong&gt;实践推荐分层互补：完整自然语言保存核心信息 + 结构化元数据索引检索 + 垂直场景（医疗问诊、法律分析、家族关系）用图谱专项索引&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件系统范式（OpenViking）&lt;/strong&gt;：所有上下文（记忆、资源、技能）映射为虚拟文件系统的目录和文件，每个条目唯一 URI（viking://）。核心：&lt;strong&gt;L0/L1/L2 三层按需加载&lt;/strong&gt;（L0 摘要约 100 token 判断相关性、L1 概览约 2000 token 供规划决策、L2 全文按需深入），目录下自动生成 .abstract/.overview 文件。选择 Markdown 纯文本而非专用数据库：用户可读可改、Git 版本控制、Agent 可自主记录组织。&lt;strong&gt;关键前提：文件之间必须建立链接与索引&lt;/strong&gt;——像 Wikipedia 一样每个条目以链接指向相关条目 + 入口页索引页，否则知识越多越难检索（不同模型主动建链的意愿能力不同）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;知识库的时效与治理&lt;/strong&gt;：知识过期（增量更新，选索引结构有现实后果：ANNOY 不支持增量、HNSW 支持）；失效内容检测与下线（版本号、生效/失效时间元数据，检索阶段过滤或摘要标注&quot;此条已于某日废止&quot;）；&lt;strong&gt;多用户权限与租户隔离——检索必须按调用者权限过滤，绝不能让越权文档进入用户上下文&lt;/strong&gt;（权限过滤下推到检索层）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能体化 RAG（Agentic RAG）&lt;/strong&gt;：把知识库检索从自动化前置步骤升级为可供 Agent 随时调用的工具，Agent 用 ReAct 循环&quot;思考→行动→观察&quot;主导检索过程——自主决定查询关键词、评估信息是否充分、不足则继续迭代甚至调用其他工具。非智能体化 RAG 只是被动的&quot;检索-生成&quot;管道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文感知检索（Contextual Retrieval，Anthropic）&lt;/strong&gt;：向量化索引前先用 LLM 为每个文本块生成&quot;前缀摘要&quot;（&quot;[本段节选自 ACME 公司 2025 年 Q2 财报]&quot;)，拼回原文再索引——同时增强稀疏（新增可精确匹配关键词）与稠密（注入语义背景）检索。&lt;strong&gt;与第二章&quot;上下文感知压缩&quot;划清界限：一个在索引期做加法（补前缀），一个在运行期做减法（去冗余）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双层记忆架构（用户记忆与 RAG 的交汇点）&lt;/strong&gt;：用 Advanced JSON Cards 把少量关键事实结构化后常驻上下文提供&quot;概览&quot;，用上下文感知检索按需从海量原始对话取回&quot;细节&quot;——这是&quot;主动服务&quot;（三层次最高层）在工程上落地的路径。案例：护照过期预警（结构化事实发现日期接近 + 检索确认细节 + 主动建议）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从数据集中提取深度知识&lt;/strong&gt;：RAG 解决&quot;已有文档如何检索&quot;，但司法判例、医生经验等知识隐藏在结构化案例数据的统计规律中。两阶段：① 知识提取与结构化（LLM 把非结构化案情转成标准化 JSON，自下而上定义 Schema）；② 因子分析与重要性建模（数据分析发现规律，形成可执行的决策逻辑）。从&quot;信息检索&quot;到&quot;知识发现&quot;的飞跃。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 本章小结&lt;/h3&gt;
&lt;p&gt;两个尺度（个体用户记忆 + 共享知识库）构成持久化记忆体系。核心技术栈：分块划定检索单元 → 稠密嵌入捕捉语义 → 稀疏嵌入精确匹配 → 结果融合 → 神经重排序 → recall@k 度量。超越扁平文本：RAPTOR 树状层次、GraphRAG 实体关系网、OpenViking 文件系统范式、上下文感知检索、智能体化 RAG。最终收敛为&lt;strong&gt;双层记忆架构&lt;/strong&gt;，真正支撑起三层次框架中最高的&quot;主动服务&quot;能力。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、第 4 章：工具&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：工具是 Agent 与外部世界交互的桥梁（&quot;手脚&quot;）。核心结论：&lt;strong&gt;工具设计的质量决定 Agent 的能力上限，异步架构决定 Agent 能否在真实世界中可靠运行&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;4.1 工具的分类（两个审视特征）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具类型&lt;/th&gt;
&lt;th&gt;调用方向&lt;/th&gt;
&lt;th&gt;作用对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;感知工具&lt;/td&gt;
&lt;td&gt;Agent 主动调用&lt;/td&gt;
&lt;td&gt;获取信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行工具&lt;/td&gt;
&lt;td&gt;Agent 主动调用&lt;/td&gt;
&lt;td&gt;改变世界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协作工具&lt;/td&gt;
&lt;td&gt;Agent 主动调用&lt;/td&gt;
&lt;td&gt;与其他 Agent/人协作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件触发工具&lt;/td&gt;
&lt;td&gt;Agent 注册、外部触发&lt;/td&gt;
&lt;td&gt;驱动 Agent 开始工作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户沟通工具&lt;/td&gt;
&lt;td&gt;Agent 主动调用&lt;/td&gt;
&lt;td&gt;向用户传递信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4.2 工具设计的通用原则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;能力表达形式：专用工具 vs Skill + 通用执行器&lt;/strong&gt;。选择依据三维度：参数复杂度（嵌套对象/多字段联合校验用专用工具结构化 schema）、变更频率（频繁变化用 Skill——改文本远比改代码测试部署轻松）、安全与权限需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具粒度权衡&lt;/strong&gt;：粒度过细 → 工具数量激增（超 100 个最先进模型也易选错）；粒度过粗 → 单个工具复杂。整合标准：功能相似性 + 使用场景重叠度（extract_pdf_text/extract_docx_content/extract_pptx_content → 统一 read_document 加 file_type 参数）。参数集差异大或使用频率极高时保持独立。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用性设计&lt;/strong&gt;：通用工具优于专用工具，除非有明确的安全/权限/性能理由（code_interpreter 胜过十几个计算器；但生产数据库写操作要专用工具保证权限控制与审计）。&lt;strong&gt;LLM 本身有强大的思考与代码生成能力，应利用而不是限制&lt;/strong&gt;——通用工具是&quot;元能力&quot;。但特殊权限、复杂配置、安全风险场景仍要封装专用工具（各平台 grep 语法不同 → 专用 grep 工具）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具描述的艺术&lt;/strong&gt;：核心是让 LLM 知道&quot;&lt;strong&gt;什么时候用&lt;/strong&gt;&quot;而不只是&quot;能做什么&quot;；&lt;strong&gt;清晰列出边界条件（做不到什么）往往比描述能力更重要&lt;/strong&gt;——大多数调用失败的根因是模型不知道工具不能做什么；参数描述用具体例子代替抽象规范（&quot;phone：E.164 格式（国家代码+号码，无空格），例如 +8613888888888&quot;）；返回值要描述清楚；注明执行代价（下载耗时）帮助模型规划调用顺序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数传递的保真性&lt;/strong&gt;：两种隐蔽反模式——&lt;strong&gt;静默输入转换&lt;/strong&gt;（Cursor 把弯引号静默转直引号，导致模型&quot;看到的内容工具却找不到&quot;，反复失败无法理解）和&lt;strong&gt;静默参数注入&lt;/strong&gt;（工具自动附加参数导致 git commit 报错）。原则：&lt;strong&gt;模型感知到的世界与工具操作的世界之间不能存在系统性偏差&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具设计的三代演进&lt;/strong&gt;：第一代直接 API 封装（粒度过细）；第二代 ACI 原则（工具对应 Agent 的目标而非底层 API 操作，对标 HCI）；第三代优化工具被调用、串联、发现的方式——&lt;strong&gt;示例驱动调用&lt;/strong&gt;（准确调用）、&lt;strong&gt;动态工具发现&lt;/strong&gt;（按需注入，不全部塞上下文）、&lt;strong&gt;代码编排执行&lt;/strong&gt;（让模型用代码编排调用序列，中间变量留在执行环境，只返回汇总结果——token 消耗可降约两个数量级）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 工具生态：MCP 与工具选择的挑战&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP（Model Context Protocol）&lt;/strong&gt;：Anthropic 2024 年底发布的开放标准，统一 AI 模型与外部工具/数据源的通信协议（&quot;通用的插座标准&quot;）。关键设计：标准化工具描述格式（JSON Schema）、传输层灵活（本地 stdio / 远程 Streamable HTTP）、资源与工具分离（只读资源 vs 可执行工具）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具过多时的应对&lt;/strong&gt;：层次化组织（搜索/读取/解析/查询四类，Anthropic 实验：按需检索使 Opus 4 工具使用准确率从 49% 提升到 74%）；从 MCP 到 Skills（MCP 解决互操作，Skills 解决选择过载——用少量通用工具 + 按需加载知识文档替代大量专用工具，把&quot;工具选择&quot;变成&quot;知识检索&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接入第三方 MCP 的风险&lt;/strong&gt;：新的信任边界——工具描述投毒、工具遮蔽、凭证管理，需接入前审查、运行时防御。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 感知工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;共同挑战：返回信息量远超处理能力 → 工具层集成上下文感知压缩（输出超阈值如 10000 字符时按查询意图压缩）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索类&lt;/strong&gt;：返回结构化候选列表（标题、位置、摘要）而非全文；分页/游标，由 Agent 决定是否翻页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读取类&lt;/strong&gt;：支持 offset/limit 按需读取；&lt;strong&gt;截断必须显式可见&lt;/strong&gt;（&quot;已显示第 1-200 行，共 5000 行&quot;）——静默截断会让 Agent 基于不完整信息做错误判断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只读性红利&lt;/strong&gt;：可安全缓存（相同查询复用）、可放心并行（同时读五个文件），执行工具没有这种自由。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模态感知&lt;/strong&gt;：直接返回图像（保留布局、耗 token）vs OCR 转文本（精简但丢空间结构），按内容类型选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.5 执行工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;层次化安全设计&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;输入验证&lt;/strong&gt;：路径遍历攻击（../../etc/passwd）、命令注入（分号/管道符）、参数类型格式。关键是&lt;strong&gt;快速失败&lt;/strong&gt;——发现异常立即拒绝，不尝试&quot;智能&quot;修正；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限控制&lt;/strong&gt;：工作目录限制、禁止命令黑名单（rm -rf /）、API 配额速率限制。注意黑名单只是最基础层——攻击者可用变形命令绕过（$(echo rm) -rf /），需结合语义解析理解命令真实意图（第五章展开）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提议者-审核者&lt;/strong&gt;：独立模型安全审查（事前审批）。&lt;strong&gt;事后验证的要诀是模态切换&lt;/strong&gt;——渲染为视觉输出检查排版、在沙盒实际运行验证配置，单一模态审查易陷入相同盲区；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sidecar 机制&lt;/strong&gt;：旁路安全检查模块，与主 Agent 流式输出并行运行，独立判断风险，起门控作用；配拒绝熔断器（连续拒绝回退到请求用户手动判断）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动验证与反馈闭环&lt;/strong&gt;：操作结果可验证就应该自动验证（write_file 后立即跑 linter，结构化错误列表作为返回值）——&quot;执行-验证-反馈&quot;闭环。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长输出截断与持久化&lt;/strong&gt;：超阈值（200 行/10000 字符）只返头尾各若干行，完整结果存临时文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等性&lt;/strong&gt;：执行工具必须回答&quot;调用被取消/超时时副作用到底发生没有&quot;（转账超时返回失败，钱可能已转出）。两类手段：唯一标识去重（idempotency key）或先查询后变更。&lt;strong&gt;不可幂等操作&lt;/strong&gt;（发邮件、打电话、对外转账）用&quot;预检-确认&quot;两段式（第一段只校验预演并返回确认令牌，第二段凭令牌真正执行）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.6 协作工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;子 Agent 设计哲学：专业化分工（一组专精 Agent 协作优于一个&quot;全能&quot;Agent）。&lt;/li&gt;
&lt;li&gt;子 Agent 提示词关键要素：角色定义清晰（&quot;你是专门负责 XXX 的助手 Agent&quot;）；&lt;strong&gt;上下文来源明确标注&lt;/strong&gt;（[FROM_MAIN_AGENT] / [FROM_USER] / [TOOL_RESULT]，防提示注入）；任务边界明确；输出格式标准化（统一 JSON）。&lt;/li&gt;
&lt;li&gt;协作原语三组：&lt;strong&gt;启动与取消&lt;/strong&gt;（spawn_subagent / cancel_subagent）、&lt;strong&gt;消息传递&lt;/strong&gt;（send_message_to_subagent，双向）、&lt;strong&gt;发现&lt;/strong&gt;（list_agents 列出可用 Agent 及职责——与 MCP 的 tools/list 同一思路）。&lt;/li&gt;
&lt;li&gt;协作形态：同步调用、异步调用（事件通知）、流式协作、多轮交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人工介入的艺术&lt;/strong&gt;：某些决策点本质上需要人类的价值观、常识或领域专业知识——人工介入本身形成学习闭环（干预样本可回放给 Agent 学习）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.7 事件驱动的异步 Agent&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么需要异步&lt;/strong&gt;：同步 = &quot;做完一件事才能做下一件&quot;。真实助理场景需要：异步执行是常态、事件优先级动态判断（取消当前/加入队列/并行处理）、中断和恢复流畅性。&lt;strong&gt;根本矛盾：&quot;训练同步/部署异步&quot;&lt;/strong&gt;——LLM 训练范式假设发出工具调用后下一条必须是工具结果，而真实部署要求随时可打断、多任务并发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenClaw 的三种自动化机制&lt;/strong&gt;：Hooks（事件钩子，响应生命周期事件）、Cron（定时调度）、Heartbeat（心跳守护，每隔 N 分钟唤醒检查）。局限：三者都是时间/框架驱动，无法即时响应任意第三方事件源（新邮件、外部 API 回调）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件触发工具三类&lt;/strong&gt;：&lt;strong&gt;定时器&lt;/strong&gt;（set_timer：一次性/循环，处理依赖物理时间的事件——&quot;周一上午 10:00 致电 DMV&quot;）；&lt;strong&gt;后台任务监控&lt;/strong&gt;（monitor_shell：监控命令行新增输出或关键词，避免&quot;盯着看&quot;浪费 token 或&quot;等完成&quot;错过严重问题）；&lt;strong&gt;外部事件通道&lt;/strong&gt;（connect_channel：新邮件、API 回调、IM 消息实时推送）。设计要点：清晰触发条件与过滤规则（避免无关事件浪费算力）、事件载荷足够上下文（减少唤醒后额外查询）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户沟通工具&lt;/strong&gt;：当沟通从 session 内一问一答扩展到多渠道异步消息，&quot;说话&quot;本身成为显式工具调用（OpenClaw 的&quot;活人感&quot;）。支持异步消息模式、已读/未读状态追踪、多模态（结构化卡片、邮件、生成式 UI）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟身份与隔离执行环境&lt;/strong&gt;：Agent 应拥有独立虚拟身份（专属通讯账号、存储、计算环境）而非直接管理用户账号——一旦 Agent 出错或被攻破，用户全部数字身份会暴露。落地在隔离执行环境（VM/容器、Android 模拟器）。两个现实挑战：反自动化机制（CAPTCHA、IP 信誉检测 → 住宅代理）；访问用户真实账号用 &lt;strong&gt;Human-in-the-Loop 认证&lt;/strong&gt;（VNC/RDP 让用户亲自登录，令牌复用平衡自主与安全）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件处理机制&lt;/strong&gt;：骨架是&lt;strong&gt;事件循环&lt;/strong&gt;（每一轮从输入队列取事件→追加轨迹→调用 LLM→执行工具→回到循环开头）；&lt;strong&gt;事件只在每轮循环的安全点被消费&lt;/strong&gt;（LLM 推理中、工具执行中新事件先排队；取消也在安全点检查）。三种策略：&lt;strong&gt;队列式&lt;/strong&gt;（常规事件，等安全点批量处理）、&lt;strong&gt;取消式&lt;/strong&gt;（紧急事件，主动制造安全点中断）、&lt;strong&gt;并行式&lt;/strong&gt;（独立轻量查询另起推理会话，结果标记&quot;与主任务并行执行&quot;）。&lt;strong&gt;结构化事件建模&lt;/strong&gt;：每个输入建模为含发送者/来源/类型/内容/时间戳等维度的结构化事件，避免用户输入误当工具结果、工具结果误当用户指令（防注入）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何让同步模型支持异步打断（工程实现）&lt;/strong&gt;：常态下让 LLM 看到标准同步轨迹，只在打断时插入占位符修复格式。五条规则：① LLM 输出时立即记录 assistant message；② 工具调用完成时才记录 tool result（轨迹存在&quot;部分完成&quot;状态）；③ 工具执行中的打断生成占位符响应（&quot;工具正在后台执行，请优先处理新事件&quot;）重新调用 LLM；④⑤ 后续恢复。&lt;strong&gt;代价：加剧幻觉风险&lt;/strong&gt;（模型从未学会处理&quot;结果还没回来&quot;——训练时工具调用后总紧接着真实结果），因此只在真正紧急时打断，非紧急事件入队。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前沿方向&lt;/strong&gt;：持续思考（continuous-time）Agent——与其在被打断时丢弃半截思考，不如让模型边等边想、边做边想（利用&quot;工具调用几秒内模型可生成上千 token&quot;的白赚算力）。关键发现：&lt;strong&gt;编排让行为成为可能，训练让行为变好&lt;/strong&gt;——用&quot;LLM 当裁判&quot;式奖励训练，模型会学藏思考换好评；只有可验证、保信息覆盖度的目标才带来实收益。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.8 主动工具发现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;传统全量注入 schema 在工具上千时失效（上下文被说明书塞满、选择精度下降）。检索式预筛选（按初始查询一次性匹配）的局限：任务中途才发现需要跨领域工具链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP-Zero 思路（Agent 声明缺口、系统按需注入）&lt;/strong&gt;：系统提示词不预置工具 schema，Agent 在思考中生成结构化请求块（&quot;GitHub 服务器：搜索仓库并返回元数据&quot;），两层语义路由（服务器级→工具级）从数千候选中匹配注入——约 2800 个工具上比全量注入省约 98% token。工程等价方案：保留少数基础工具 + 一个&quot;工具搜索工具&quot;（Claude Tool Search Tool）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;层次化匹配与降级&lt;/strong&gt;：先按能力描述定位服务器，再在服务器内匹配工具（数千工具 → 数十服务器 × 数十工具）；候选相似度都低于阈值时明确返回&quot;未找到&quot;，让 Agent 改写需求、手工实现或创造新工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态加载与 KV Cache&lt;/strong&gt;：把新工具的完整 schema 追加到上下文末尾（静态前缀保持稳定），状态栏维护简短工具名列表。如今已是 API 原生能力：OpenAI Responses API 的 tool_search + defer_loading、Anthropic Tool Search（Claude Code 对 MCP 工具默认延迟加载）、Codex CLI 的 tool_search（BM25，默认开启）。&lt;strong&gt;澄清：&quot;追加到末尾&quot;只发生在工具被发现的那一轮&lt;/strong&gt;，此后固定为历史消息，不会每轮重新搬运。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills：把工具发现变成&quot;按需查阅&quot;&lt;/strong&gt;：不需要嵌入索引 + 语义匹配基础设施——Agent 启动只看到 name + description 目录（数百 token），需要时读取完整 SKILL.md 并顺着引用深入。像查工具书一样&quot;用到哪条查哪条&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.9 本章小结&lt;/h3&gt;
&lt;p&gt;ACI 原则（粒度、通用性、描述规范）适用于所有工具；MCP 统一互操作；层次化组织 + 动态发现 + Skills 回应工具爆炸；参数传递保真性是底线。五类工具各有侧重（感知=压缩与分页；执行=分层安全+提议者-审核者+Sidecar；协作=生命周期原语+人工介入；事件触发=过滤规则+载荷设计；用户沟通=异步+多渠道+虚拟身份）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;七、第 5 章：Coding Agent 与代码生成&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：论证 Coding Agent 加上文件系统是所有通用 Agent 最核心的技术基础；代码是通用 Agent 的&lt;strong&gt;元能力&lt;/strong&gt;（能创造其他能力的能力）。&lt;/p&gt;
&lt;h3&gt;5.1 Coding Agent&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coding 是 Agent 的基础能力&lt;/strong&gt;：几乎所有高效的内容生成最终都要落到代码上——PPT 是 OOXML 格式的代码、Word/PDF 可由代码生成、数据分析用 Python 脚本、GUI 操作序列可固化为 RPA 代码。&lt;strong&gt;代码生成是效率最高、成本最低、可复用性最强的能力基座&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;七个核心工具&lt;/strong&gt;：① Code Interpreter（沙盒执行 Python）；② Bash Shell（终端命令）；③ 读文件；④ 写文件；⑤ 编辑文件；⑥ 文件搜索（glob，按文件名）；⑦ 内容搜索（grep/语义）。一个 TODO 整理任务只需 Grep + Write 两个工具即可完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例：Manus → OpenClaw&lt;/strong&gt;：把 Deep Research、Computer Use、Coding 三大能力融合，凸显&quot;&lt;strong&gt;Coding Agent + 文件系统是开放任务型通用 Agent 最核心的技术基础&lt;/strong&gt;&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sessionless 设计&lt;/strong&gt;：无安装、无登录、常驻在线，用户通过已有消息平台随时发消息即得响应。大模型已成熟到足以充当&quot;智能基座&quot;（类似操作系统屏蔽硬件，大模型屏蔽语言理解与思考规划的复杂性）。工程难点：&lt;strong&gt;代码执行环境和文件系统状态如何跨消息存活&lt;/strong&gt;——OpenClaw 两层管理：文件系统状态天然持久（workspace 挂载在沙盒之外的持久存储）；进程状态按需保活或重建。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coding Agent 的安全&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simon Willison 的&quot;致命三要素&quot;&lt;/strong&gt;：① 访问私有数据；② 暴露于不受信任内容；③ 具备外部通信能力——三要素齐备即构成完整攻击闭环（恶意指令藏在不受信任内容 → 读取私有数据 → 经对外通道传出）。&lt;strong&gt;笔者补充第四维度：持久记忆&lt;/strong&gt;——攻击者把无害偏见/恶意指令写入长期记忆，跨会话潜伏、适时触发，把一次性攻击升级为长期放大。四类边界：数据边界、输入信任边界、输出影响边界、跨会话边界。全权限本地 Agent（OpenClaw 类）四者兼备，安全是核心挑战。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命令语义解析&lt;/strong&gt;：关键字黑名单形同虚设（子 shell、变量展开可绕过任何静态规则，$(echo rm) -rf /）。生产级 Harness 用语义解析理解命令真实效果——识别&quot;看似无害的标志位消费下一个参数从而隐藏危险载荷&quot;（find -exec rm、curl -o /etc/crontab）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推测性执行（让安全检查&quot;隐形&quot;）&lt;/strong&gt;：把&quot;展示&quot;与&quot;放行&quot;拆开并行——先显示进度提示，后台同时跑安全检查；先行提示无副作用，检查不通过只需替换为&quot;等待确认&quot;。&lt;strong&gt;安全性不以牺牲用户体验为代价&lt;/strong&gt;是 Harness 设计的最高境界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信任边界下移（数据层）&lt;/strong&gt;：当写代码的和跑代码的都可能不可信时，真正可靠的约束不能待在被生成的代码里，而要待在下面那层人类审查过的地基里（如数据库 schema 级不变量保证——&quot;不可能错&quot;而非&quot;更可能对&quot;）——&quot;约束优先于指导&quot;原则在数据层的终极形态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coding Agent 的整体流程&lt;/strong&gt;：项目文档化（生成 CLAUDE.md 项目指南）→ 需求理解与澄清 → 设计文档（方案对比，提交设计等审批）→ 编码与测试（old_str→new_str 修改、修复失败测试重跑）→ 审查与交付（自审可读性/安全/性能）→ 文档同步（架构级改动后更新文档——过时的文档比没有文档更糟糕）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指令文件（CLAUDE.md 等）&lt;/strong&gt;：项目级系统提示词，承载面向 Agent 的行为约定（构建测试命令、代码风格、禁区&quot;不要改动 migrations/ 目录&quot;）。与 SOUL.md（Agent 是谁）、MEMORY.md（跨会话经验）同一思路。最经济的稳定前缀，对 KV Cache 友好。推论：&lt;strong&gt;对远程工作友好的团队也对 AI Agent 友好&lt;/strong&gt;——评估团队 AI-ready 程度的代理指标：一个远程新人只靠代码仓库和文档能否独立工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness 工程在 Coding Agent 中的实践&lt;/strong&gt;：Coding 是所有 Agent 任务中&lt;strong&gt;可验证性最高&lt;/strong&gt;的一类（测试、类型、linter、Git 都有现成基础设施）。落地组件：验收基线（测试套件/CI/审查标准）、执行边界（模块边界/依赖规则/权限）、反馈信号（linter/测试/类型检查）、回退手段（Git/沙盒/快照）。&lt;strong&gt;可迁移的设计原则&lt;/strong&gt;：① 约束优先于指导（能用代码强制的规则就不要用文档建议——&quot;做不了&quot; vs &quot;建议别做&quot;）；② 验证要自动化（人工审查不可扩展）；③ 反馈越快越好、越结构化越好。LangChain 方法论：&lt;strong&gt;用 Agent 分析失败轨迹来改进 Harness&lt;/strong&gt;，让 Harness 工程从人工经验驱动转向数据驱动；Anthropic：长任务拆为初始化 Agent（分解任务清单）+ 执行 Agent（逐步推进、中间成果留档）两角色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障与错误恢复（四层故障分类）&lt;/strong&gt;：API 层（限流 429、超时、连接中断、输出截断）；工具层（幻觉调用、参数畸形、执行异常、&lt;strong&gt;反复返回同一错误而模型不改地重试&lt;/strong&gt;）；上下文层（窗口溢出、压缩失败、轨迹结构损坏）；控制流层（&lt;strong&gt;死循环&lt;/strong&gt;反复执行无进展、&lt;strong&gt;死亡螺旋&lt;/strong&gt;错误触发的恢复逻辑自身又调 LLM 连锁出错）。
&lt;ul&gt;
&lt;li&gt;检测：先分类再计数——可重试错误（限流/过载/网络抖动）重试才有意义，不可重试错误（参数不合法/权限不足/工具不存在）必须改变输入或策略。维护&quot;错误→恢复策略&quot;映射表。模式检测：&lt;strong&gt;重复调用指纹&lt;/strong&gt;（工具名+参数相同即无进展循环信号）、连续失败计数。流式连接最危险失败模式是&lt;strong&gt;静默卡死&lt;/strong&gt;（连接成功但数据流停止，SDK 超时往往只覆盖初始连接）——需要独立空闲看门狗。&lt;strong&gt;每个长连接都需要活性信号&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;恢复：分级升级；错误路径上禁用一切会再次调用模型的副作用逻辑（防递归死亡螺旋）；全局终止条件（最大迭代轮数、会话预算上限、连续失败超阈值升级人工干预）。&lt;/li&gt;
&lt;li&gt;核心结论：&lt;strong&gt;Agent 的可靠性不取决于它犯不犯错，而取决于每类错误是否都有对应的检测、恢复与终止路径&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现技巧&lt;/strong&gt;：并行工具调用 + 流式执行（第一个工具调用参数一完整即开始执行，与后续调用生成重叠）+ 级联中止（某调用失败终止同批依赖它的调用，不波及其他）；环境信息动态注入（当前工作目录、git 分支、最近提交、未暂存/已暂存变更——作为 Agent 状态栏追加而非硬编码进静态提示词）；&lt;strong&gt;命令执行环境状态持久化&lt;/strong&gt;（维护持久终端会话，保留工作目录、环境变量——符合人类开发者习惯）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索工具（互补工具箱）&lt;/strong&gt;：正则内容匹配（grep 精确文本）、文件名匹配（glob 路径模式）、语义代码搜索（自然语言 → 向量+BM25 混合）、&lt;strong&gt;符号级定义/引用查找（LSP）&lt;/strong&gt;——能区分同名符号的定义和调用，代码重构的关键（重命名不能只靠文本搜索，函数名可能出现在注释/字符串中）。策略：&quot;从粗到细、从语义到语法&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件编辑工具（五种方案）&lt;/strong&gt;：LLM 输出 diff 描述（关注点分离但微小偏差致错位）、旧字符串→新字符串（可预测无歧义，大段删除需全输出）、行号定位（大段操作高效但长文件数行号易错）、类 Vim 编辑命令（重组代码高效但语法学习负担大）、&lt;strong&gt;字符串首尾匹配&lt;/strong&gt;（只提供要删内容的开头几行和结尾几行，框架匹配定位——综合可靠性与效率，主流自建推荐）。业界代表：Claude Code 用 old→new 字符串（可靠优先），Cursor 用 Apply Model 专用 fast-apply 模型（吞吐优先）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 代码：通用 Agent 的元能力（六个方向，由内向外）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码作为思考工具&lt;/strong&gt;：LLM 在精确计算、符号操作、严格逻辑推导上有根本短板（概率性近似 vs 确定性精确）。分工：LLM 理解问题写代码，代码解释器精确计算（集合论题目：纯自然语言推理易错，代码推理精确可验证）。Wolfram 洞察：符号计算系统与 LLM 互补。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码作为业务规则的约束&lt;/strong&gt;：&quot;约束：编码化而非文档化&quot;。自然语言规则有歧义（&quot;7 天&quot;是自然日还是工作日？&quot;购买&quot;是下单还是发货？），代码无歧义、可执行、确定性。实践结合：系统提示词保留自然语言规则供理解和沟通 + 关键决策点配代码化校验工具作&quot;守门员&quot;。&lt;strong&gt;三重保障&lt;/strong&gt;：① 自然语言规则帮助理解解释；② 工具描述与 expected_* 参数作 checklist 引导模型调用前显式核对；③ &lt;strong&gt;服务端基于数据库真值的代码化校验作最后守门员&lt;/strong&gt;（政策事实一律查库、时间取服务端时钟、绝不采信模型自报值——最后防线必须建立在模型无法伪造的数据之上）。实验 5-3：Qwen3-4B 通过代码化知识显著提升复杂政策执行准确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码驱动的多媒体生成&lt;/strong&gt;：PPT 创作重新框定为代码生成问题（Slidev：Markdown+HTML 定义内容）。关键机制：&lt;strong&gt;提议者-审核者&lt;/strong&gt;——Proposer 逐页生成代码，Reviewer 渲染截图用 Vision LLM 检查（文字溢出、布局拥挤、字体过小），迭代到达标。视频讲解：PPT 生成 + 口语化讲解文字 + TTS + ffmpeg 合成。视频剪辑：粗粒度（每 10 秒截图）→ 精细粒度（每秒截图）两级定位场景区间，生成 Blender API 脚本，Reviewer 检查关键帧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码作为系统适配器&lt;/strong&gt;：外部服务没有现成 SDK、文档缺失、返回格式非标准时，Agent 当场读文档/观察真实响应、即时生成适配代码（HTTP 客户端、鉴权、解析非标准结构）。延伸：无 API 系统先 Computer Use 操作界面、再把成功序列固化为 RPA 代码。日志自适应解析：格式变化时自动检测失败 → 生成新解析代码 → 自动测试 → 热更新。生产日志诊断：读日志 + 架构文档 + PRD 定位问题 → 生成结构化问题报告和回归测试用例 → 经 MCP 创建 GitHub Issue 全自动化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码作为生成式 UI&lt;/strong&gt;：动态生成表单、交互式图表、完整 Web 应用。&lt;strong&gt;A2UI（Agent-to-User Interface）声明式界面协议&lt;/strong&gt;——Agent 不直接生成可执行代码，只输出&quot;界面描述清单&quot;（JSON），客户端用预先准备好的安全组件渲染（&quot;顾客只能点菜单上有的菜&quot;）。安全优先：客户端维护受信任组件目录，防注入/XSS。&lt;strong&gt;Artifact 模式&lt;/strong&gt;：数据直达前端（图表直接绑定数据源，绕过&quot;数据经过 LLM 抄写&quot;的低效与出错）。结合热加载（HMR）实现对话式界面实时定制（&quot;千人千面&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码创造代码：Agent 自举（bootstrapping）&lt;/strong&gt;：Agent 用代码修复和创建与自己同类的 Agent（自我修复、自我复制、按需生成）。案例：doctor --fix 自我诊断修复（确定性检查覆盖常见问题 + LLM 兜底疑难长尾）。让 Agent 编写 Agent 的关键技巧：提供架构模式参考代码、复制脚手架保留循环框架/消息格式/KV 优化、定向修改业务逻辑、验证测试。常见缺陷：上下文管理随意（未用标准结构化消息）、工具设计粗糙、安全缺失。&lt;strong&gt;自举引入的偏差会代际累积吗？——第八章的验证/发布/回滚机制是防退化答案&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.3 本章小结&lt;/h3&gt;
&lt;p&gt;代码不只是写程序的工具，它是 Agent 形式化思考和精确表达的语言。Coding Agent 成熟度高不是因为代码生成模型特别强，而是因为软件工程几十年攒下的基础设施（测试、类型系统、版本控制）天然构成一套强大的 Harness。代码的价值：完成任务的手段 + 积累知识、创造工具、优化自身的机制。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;八、第 6 章：Agent 的评估&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：构建科学评估方法论。核心问题：怎么判断 Agent 真的变好了？评估体系三层：评估环境（在哪里测）→ 评估方法（怎么判）→ 评估驱动决策（测了干什么）。贯穿主题：可观测性、仿真环境、内部评估。&lt;/p&gt;
&lt;h3&gt;6.1-6.2 评估环境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;评估五要素&lt;/strong&gt;：数据集（初始状态+目标+参考解）、环境状态（真实性与可控性平衡：退款不超过订单金额 + 每次测试可重置）、工具接口（原子操作而非高层抽象，迫使 Agent 规划组合）、评分标准 Rubric（二元/连续/多维）、执行协议（交互模式与终止条件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具调用型环境（Verifiers）&lt;/strong&gt;：验证基于可执行标准（测试通过、答案匹配），不依赖人类标注或模型评判。层次化：SingleTurnEnv（单轮）、ToolEnv（多轮工具循环）、StatefulToolEnv/SandboxEnv（有状态、长时沙盒）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人机交互型环境（τ-bench / τ²-bench）&lt;/strong&gt;：核心设计原则是&lt;strong&gt;渐进式信息透露（Progressive Information Disclosure）&lt;/strong&gt;——大多数 benchmark 一开始全盘托出需求，但现实中用户只会说&quot;我的航班好像有问题&quot;。τ-bench 用&lt;strong&gt;用户模拟（User Simulation）&lt;/strong&gt;：另一个 LLM 扮演用户按剧本逐步透露信息、回应询问。双重验证：数据库最终状态检查 + 对话关键信息字符串验证；任务层面汇总为二元奖励（全过才 1 分）。τ²-bench 增量：&lt;strong&gt;双控环境&lt;/strong&gt;（用户模拟器也能操作共享环境，Agent 需观察到用户侧变化）与更精确的组合式任务生成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 评估任务数据集的设计（五挑战）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;明确性与开放性的张力&lt;/strong&gt;（GAIA：概念简单、实现路径开放）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真实性与可控性平衡&lt;/strong&gt;（SWE-Bench → Verified：人类专家筛出 500 个高质量任务）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多样性与系统性协调&lt;/strong&gt;（AndroidWorld：116 任务 × 20 应用，标注核心能力标签，参数化生成变体）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估成本与覆盖范围&lt;/strong&gt;（GAIA 精选 466 题分三级难度；SWE-Bench Verified 从 2294 筛到 500，成本降约五分之四）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据泄漏防范&lt;/strong&gt;（GAIA 靠答案独特性 + 专属附件文件；SWE-bench-Live 靠时间新鲜度；τ²-bench 动态参数生成；Terminal-Bench 嵌入金丝雀 GUID 使泄漏可检测）。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务描述精确性&lt;/strong&gt;：每个元素机械化可验证（Terminal-Bench &quot;build-linux-kernel-qemu&quot;：启动日志出现自定义消息才算成功，无法伪造输出蒙混过关）；AndroidWorld 参数化模板（每次随机参数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂度层次化&lt;/strong&gt;：GAIA 三级（L1 一两个工具 vs L3 复杂组合，人类 87.3% vs GPT-4 0%）——不同层次失败指向不同改进方向（提示工程 vs 规划机制 vs 分层架构/后训练）；τ²-bench 业务复杂度分层（查询→多步流程→故障诊断→策略判断）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可验证性&lt;/strong&gt;：SWE-Bench Verified 的 FAIL_TO_PASS（证明修复了）+ PASS_TO_PASS（证明没引入新 bug）双重验证；τ²-bench 多层检查（数据库状态+对话关键词+流程合规性）；OSWorld 134 个独立评估函数深入检查文件系统/进程/网络/应用内部状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据质量控制与迭代&lt;/strong&gt;：SWE-Bench Verified 93 名开发者人工评估、29% 通过率、标准化标注指南；OSWorld → OSWorld-Verified 是迭代改进典范（15 个月暴露 300+ 问题，四类修复策略，云平台 50 倍并行加速，轨迹公开到 HuggingFace 形成持续改进循环）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 评估指标体系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过程指标&lt;/strong&gt;：行动合法率（无效操作 vs 越权操作）、工具调用正确率（参数语义合理）、路径效率（步数/冗余动作/回退次数）、检索覆盖率、成本与延迟（请求数、token、墙钟时间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果指标&lt;/strong&gt;：Pass@k（k 次尝试至少成功一次的概率——能力上限）、&lt;strong&gt;Pass^k（k 次全部成功的概率——稳定性）&lt;/strong&gt;、Best@k（最好一次得分——质量上限）。例：单次成功率 60%，Pass@5 ≈ 99% 而 Pass^5 ≈ 7.8%。&lt;strong&gt;回归测试用 Pass^k，探索性评估用 Pass@k，混用会导致误判&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.5 自动化评估方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLM-as-a-Judge + Rubric&lt;/strong&gt;：开放式任务（客服对话、报告撰写）用 LLM 评判。好 Rubric 四准则（Scale AI）：基于专家指导（示例+边界案例）、全面覆盖（所有重要维度）、标准重要性权重（Essential/Important/Veto 否决项）、自包含评估（每个评价项独立可操作、不依赖评判者领域知识——&quot;引用了至少两个权威理论&quot;优于&quot;展示了深刻理解&quot;）。主动防范&lt;strong&gt;奖励作弊（Reward Hacking）&lt;/strong&gt;：惩罚幻觉、讨好、关键词堆砌、回避问题。Rubric 是迭代产物（收集评判分歧逐步完善为判例集）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配对比较与模型排名&lt;/strong&gt;：Elo 评分（Bradley-Terry 模型的在线更新形式；爆冷带来更大分数调整）；Chatbot Arena 匿名随机对决（不需要绝对标准，但结果取决于用户问什么问题）。&lt;strong&gt;位置偏差（Position Bias）&lt;/strong&gt;：评判模型系统性偏向先出现的候选——缓解：交换顺序各评一次取平均，或两次一致才计入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.6 评估驱动的模型选型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;关键维度：&lt;strong&gt;Prefill vs Decode 两阶段&lt;/strong&gt;（Prefill 决定 TTFT 首字延迟、上下文越长越慢；Decode 决定出字速度、直接决定思考时长——50 tokens/s 生成 2000 思考 token 要 40 秒）；输入/输出吞吐量、思考延迟（不同模型思考 token 数差数倍，且与效果不一定正相关，要实测）、p95 尾部延迟（比均值更能反映体验）、成本（便宜但成功率低的模型因重试可能实际更贵——算每个任务平均成本与成本-性能比）、性能指标取舍（日常看 Pass@1、关键操作看 Pass^k、探索看 Pass@k/Best@k）、速率限制与鲁棒性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 系统成本分析（三层次）&lt;/strong&gt;：模型推理成本（&lt;strong&gt;上下文累积效应&lt;/strong&gt;——第 1 轮 1000 token、第 2 轮 2000、第 3 轮 3000，总量 6000 而非 3000，KV Cache 是关键；思考 token 也计费）、工具调用成本（外部 API 费用、&lt;strong&gt;工具结果注入上下文后的反复计费&lt;/strong&gt;——一次搜索返回 2000-5000 token 每轮都算输入）、基础设施成本（向量库、消息队列、日志追踪）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估驱动的持续迭代&lt;/strong&gt;：模型选择不是一次性决策。&quot;新模型比旧模型强吗？好多少？切换成本是什么？&quot;——有评估体系的团队数小时内给出答案：在自有评估集上跑新模型，对比成功率/工具调用正确率/延迟/成本，可能得到&quot;简单任务迁移降低成本、复杂任务保留原模型保质量&quot;的差异化策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.7-6.11 统计显著性、可观测性与内部评估&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;统计显著性&lt;/strong&gt;：二项分布标准误 √(p(1-p)/n)——100 用例成功率 70% 时标准误 ≈4.6%，95% 置信区间 ≈ ±9 个百分点；&quot;73% vs 70%&quot;的 3 个百分点差异完全在噪声带宽内（独立假设下差值标准误 ≈6.5%）。&lt;strong&gt;多次运行取均值&lt;/strong&gt;（每个配置 3-5 次不同随机种子），报告均值与波动范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观测性&lt;/strong&gt;：执行追踪树（Trace：任务+耗时+成本）、监控仪表盘（成本追踪、P50/P95/P99 延迟、工具成功率、异常模式检测——&quot;task#892 循环调用 search 14 次&quot;）。挑战：数据量与隐私权衡、因果归因复杂、多 Agent 追踪、实时防护与事后分析平衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从 Benchmark 报告到系统改进（假设案例全流程）&lt;/strong&gt;：观察诊断报告（总体 88% 但 math_counting 0%、Wi-Fi 操作 0%）→ 能力标签矩阵揭示共同特征（transcription 暴露视觉理解缺陷、complex_ui_understanding 依赖标准 UI 模式）→ 三层改进框架假设（表层 H1/H2 导航提示与 UI 规则；中层 H3 修复多模态管道、H4 全局思考；深层 H5 换更强模型、H6 加 UI 元素树）→ 分阶段验证（每配置 5 次×116 任务）→ 数据驱动决策：H1+H3 明确部署、&lt;strong&gt;H4 全局思考不可接受&lt;/strong&gt;（为 8% 的任务让全部任务承担 3 倍延迟成本，&quot;杀鸡用牛刀&quot;）、&lt;strong&gt;H6 优于 H5&lt;/strong&gt;（瓶颈不在模型思考能力而在输入信息充分性）、H5+H6 组合最高但只适合关键异步任务选择性启用 → 持续迭代（新报告出现新失败模式 → 新假设 H7 条件化启用思考、H8 扩展动作空间）。&lt;strong&gt;方法论：观察→假设→实验→验证→新认识→新假设，把 Agent 工程从经验驱动的&quot;炼金术&quot;转向数据驱动的科学工程&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部评估基础设施（OpenClaw 实践）&lt;/strong&gt;：① 消融基础设施（总开关禁用多个主要特性创建&quot;裸模型&quot;基线——发现&quot;特性债务&quot;，每个主要特性都应可独立关闭，消融开关必须在启动路径极早期注入）；② AB 测试方法论（多臂而非二元、&lt;strong&gt;区分机制指标和目标指标&lt;/strong&gt;——&quot;缩短计划文件长度&quot;是机制、&quot;降低会话级成本&quot;才是目标、设置护栏指标、记录基线统计）；③ 双层特性开关（编译时开关物理移除代码、运行时开关服务端下发本地缓存）；④ 提示词敏感性评估（系统提示可确定性渲染、版本化快照、每次变更跑回归——像代码变更跑 CI）；⑤ 隐私感知的分析（类型系统强制——分析接口只接受特殊类型包装的值，&quot;类型名本身就是审计线索&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仿真环境：从评估到后训练的桥梁&lt;/strong&gt;：评估的终点是改进，改进的最强形态是训练。评估环境 → 仿真环境的核心区别：交互频率远高（百万次 vs 千次）、需要随机化（防死记硬背）、必须即时反馈。&lt;strong&gt;评估资产无缝转成训练信号：一套定义清晰的 Rubric/验证器本质上就是一个可验证奖励（RLVR）的奖励函数&lt;/strong&gt;。训练的新要求：可靠的 reset 语义（数百万 episode 每个都要干净初始状态）、远高于评估的吞吐。**领域随机化（Domain Randomization）**是缩小 sim-to-real gap 的关键（物理参数、视觉外观、传感器噪声大范围随机）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.12 本章小结&lt;/h3&gt;
&lt;p&gt;评估形成完整闭环：评估环境 → 数据集 → 自动化评估方法（LLM-as-a-Judge + Rubric）→ Benchmark 分析揭示改进方向 → 系统改进 → 更新评估环境与数据集。评估是 Harness 中&quot;验证&quot;功能的系统化实现。评估体系是第七章（评估环境与数据转成后训练输入）和第八章（轨迹评价转成知识/指令/程序/参数更新）的共同基础。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;九、第 7 章：模型后训练&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：深入 SFT 与 RL 技术。两条主线：&lt;strong&gt;主线一——SFT 记忆、RL 泛化&lt;/strong&gt;（先形后神：SFT 先立&quot;形&quot;（格式结构），RL 再求&quot;神&quot;（策略泛化））；&lt;strong&gt;主线二——数据和环境比算法更重要&lt;/strong&gt;（先验 &amp;gt; 环境 &amp;gt; 算法）。&lt;/p&gt;
&lt;h3&gt;7.1 预训练、SFT、RL 三阶段全景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预训练&lt;/strong&gt;：预测下一个词（Next Token Prediction）。模型输出本质是概率分布，&quot;训练&quot;就是调整这个分布。在海量互联网文本上反复猜下一个词，被迫学会语法、事实、逻辑乃至基本推理。预训练后模型&quot;博学却不好用&quot;——它没学会&quot;被提问时应回答&quot;这个协议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFT 的本质：换了数据的&quot;预测下一个词&quot;&lt;/strong&gt;。与预训练只有两点差别：① 数据不同（人工&quot;输入-输出&quot;对，格式统一为&quot;用户提问→理想回答&quot;）；② &lt;strong&gt;损失只算在&quot;回答&quot;上（loss masking，损失屏蔽）&lt;/strong&gt;——不希望模型学&quot;怎么提问&quot;，只学&quot;怎么回答&quot;。SFT 记忆：优化目标是让标注回答每个 token 概率尽可能高，即&quot;把标准答案背下来&quot;。能力边界钉死在示范数据上。&lt;strong&gt;固化的是&quot;格式、风格、流程&quot;这类协议性知识，而非事实性知识&lt;/strong&gt;（后者靠预训练或 RAG）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LoRA（参数高效微调）&lt;/strong&gt;：冻结大权重矩阵，旁边挂低秩&quot;补丁&quot;，参数仅占 1%-5%，接近全参效果且扰动小。实践经验：应用到所有主要权重矩阵（尤其 MLP 层，只加注意力层会掉点）；学习率约为全参微调 10 倍；SFT 用中高 rank（64-256）、RL 用小 rank（8-32 甚至 1）。一台推理服务器可同时加载多个 LoRA 多租户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么必须先 SFT 后 RL&lt;/strong&gt;：RL 不看标准答案，靠模型自生成回答 + 奖惩；可要判断好坏，首先得能解析输出——格式混乱（JSON/工具调用都吐不齐）奖励函数无从算起。SFT 先把&quot;话说利索&quot;，RL 才有能打分的起点。边界：较小基础模型 + 严格结构化输出时&quot;必须先 SFT&quot;成立；强基模可直接 RL（DeepSeek-R1-Zero 自行涌现反思与长链思考，但输出可读性差，最终仍加回&quot;冷启动 SFT&quot;立稳&quot;形&quot;）。&lt;strong&gt;&quot;先形后神&quot;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFT vs RL 本质区别（最重要的一张表）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;SFT：优化最大化标注答案概率（极大似然）；唯一标准答案、每个 token 都有监督；学到固定映射 → &lt;strong&gt;记忆&lt;/strong&gt;（J/Q/K 训练时当 10，测试变 11 照旧用 10）；&lt;/li&gt;
&lt;li&gt;RL：优化最大化期望奖励；自己生成多条回答 + 每条一个成败信号；学到&quot;怎样的过程能得到正确结果&quot;的更通用策略 → &lt;strong&gt;泛化&lt;/strong&gt;（J 变 11 用同一策略重算）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.2-7.3 经典 RL Agent 到现代 Agent（可选阅读）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;RL 五要素：动作空间、策略、奖励信号、价值函数、环境模型（基于模型 vs 无模型）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两种范式最根本差异：动作空间&lt;/strong&gt;——MDP 假设有限封闭动作集，LLM 的动作空间是开放、组合爆炸的自然语言序列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MDP 的根本局限&lt;/strong&gt;：样本效率低（Q-learning 10000 次试错只需 10 秒在游戏里，但真实世界每次打电话有成本、每次操作有延迟，完全不可接受）、泛化差、无法利用先验知识。&lt;/li&gt;
&lt;li&gt;OpenAI 范式演进三阶段：算法中心主义（2015-2016）→ 环境的重要性（2016-2018）→ &lt;strong&gt;先验的觉醒（2018-至今）&lt;/strong&gt;：先验知识可通过与 RL 完全无关的方式获得（语言预训练）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.4-7.5 SFT 实践与决策&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SFT 数据三条路&lt;/strong&gt;：人工专家示范（质量天花板最高、贵且慢，做&quot;种子数据&quot;）；教师模型生成（合成数据，强模型批量产出后过滤蒸馏）；模型自举（&lt;strong&gt;拒绝采样微调&lt;/strong&gt;：同一问题采样 k 条候选 → 验证器筛选正确样本 → SFT 训练自己，STaR/RFT 核心循环）。量级：数千到数万条高质量样本足够，&lt;strong&gt;&quot;与其堆十万条脏数据，不如精修一万条干净数据——数据里的每一处噪声，SFT 都会忠实地写进参数&quot;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt 蒸馏&lt;/strong&gt;：把&quot;长提示 + 思考型教师&quot;的行为压缩到&quot;短提示 + 非思考学生&quot;——训练数据只保留用户输入与最终结论，学生学会&quot;直接给出结论&quot;，延迟费用显著降低。两维度：&quot;大到小&quot;、&quot;思考到非思考&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;何时选 SFT / RL 决策框架&lt;/strong&gt;：先试 SFT（固化格式、协议性知识、统一风格；不适合注入大量事实知识——那要预训练或 RAG）；SFT 不够时加 RL（需要泛化到新场景、探索最优策略、标注成本过高）。&lt;strong&gt;很多场景下只要 SFT 数据质量到位根本不需要 RL&lt;/strong&gt;——RL 又贵又不稳定（常是 SFT 的几十到上百倍成本）。Anthropic 例证：2025 年前后训练主配方是高质量 SFT + RLAIF，并不依赖 RLVR。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.6 单轮强化学习：记忆与泛化的对照&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AdaptThink（学会&quot;何时不思考&quot;）&lt;/strong&gt;：NoThinking 模式在简单问题上性能相当甚至更好，只有困难问题 Thinking 优势才显现——用 RL 训练模型学会按需分配思考。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GeneralPoints 卡牌算术（SFT vs RL 对照）&lt;/strong&gt;：SFT 训练集 100%、OOD 测试崩盘；RL 通过探索发现超越示范的通用策略（&quot;J 变 11 时用策略重算&quot;），OOD 大幅领先。&lt;strong&gt;&quot;SFT 记忆、RL 泛化&quot;不是口号，而是可测量现象&lt;/strong&gt;。GP-VL 视觉变体同样成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.7 RLHF：从人类偏好到奖励模型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;InstructGPT 三段式管线&lt;/strong&gt;：① SFT（人工示范建立指令遵循）；② 训练奖励模型 RM（人类标注员两两比较&quot;哪个更好&quot;——比较比打分可靠得多；Bradley-Terry 模型训练）；③ RL 优化（PPO，带 KL 惩罚）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么用反向 KL 惩罚&lt;/strong&gt;：把当前策略摁在参考模型（SFT 模型，&quot;说人话、格式正常&quot;的安全区）附近。反向 KL 是 &lt;strong&gt;mode-seeking（寻峰）&lt;/strong&gt;——允许模型只保留少数高奖励&quot;峰&quot;、果断丢掉其余模式（这正是 RL 后模型更&quot;笃定&quot;、多样性更低的原因）。不加的后果：&lt;strong&gt;reward hacking&lt;/strong&gt;（模型钻奖励漏洞刷高分而非真做好任务——RM 分数单调上升而人类评估先升后降，Goodhart 定律：指标一旦成为优化目标就不再是好指标）与&lt;strong&gt;分布崩塌&lt;/strong&gt;（输出退化成重复乱码）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.8 强化学习算法比较&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;记住主线二：现成算法会用、能选对就够了，真正决定成败的是数据和环境&lt;/strong&gt;。算法已封装进 veRL、TRL 等框架。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPO&lt;/strong&gt;：价值网络估计优势 + 裁剪限制单步更新幅度（防止策略一步跑偏）；&lt;strong&gt;GRPO&lt;/strong&gt;：省去价值网络，用&quot;组内相对比较&quot;估计优势（比同组平均好为正、差为负）——成本更低。注意区分两件事：&lt;strong&gt;奖励从哪来&lt;/strong&gt;（规则验证器 RLVR / 学习到的奖励模型 RLHF / 人类偏好）与&lt;strong&gt;用什么算法优化&lt;/strong&gt;（PPO 和 GRPO 对奖励来源不挑剔，真正差异在优势估计方式）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.9 数据与环境：比算法更重要的事（全章最想让你记住的一节）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;环境：模型练习的场地&lt;/strong&gt;。&quot;环境失真，策略必废&quot;——仿真里客服固定套路回话，模型学到只在仿真里管用的&quot;应试策略&quot;，一上线就露馅；这是 RL 项目最常见的翻车方式。构建高保真环境常常比训练本身更贵更难（真实 API 有速率限制、会封号、有副作用，必须造&quot;影子世界&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;造不出环境怎么办：让模型扮演环境&lt;/strong&gt;。两个层次：① 模型合成工具返回值（ZeroSearch 用 LLM 扮演搜索引擎，课程式设计——训练初期返回高质量文档、逐步掺噪声逼学生适应不完美结果，最终直接对接真实搜索依然良好）；② 模型仿真整个环境动态（DreamGym 蒸馏出&quot;经验模型&quot;预测状态转移）。&lt;strong&gt;风险：模拟器的世界知识就是训练的天花板，模拟器的系统性偏差会被策略照单全收&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据：最关键的一环，质量胜过一切&lt;/strong&gt;。&quot;Garbage in, garbage out 在后训练里体现得淋漓尽致。&quot;SFT 一字不差地把噪声偏见固化进参数；RL 朝着有偏差的奖励拼命优化（reward hacking 温床）。数据质量三维度：覆盖度（任务分布的多样性）、平衡度（避免单一腔调）、标注准确性（思维链蒸馏里错误思考过程会被学生一并模仿）。&lt;strong&gt;拒绝采样&lt;/strong&gt;是把标注准确性拉满的标准动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic 数据生成（Autodata）&lt;/strong&gt;：主 Agent 协调 challenger（生成任务）/弱求解器/强求解器/verifier 四角色，寻找&quot;强模型能解决、弱模型仍有困难、评价器又能可靠判断&quot;的任务——把推理算力转化为当前能力前沿的新训练数据。&lt;strong&gt;与动态采样（改预算分配）不同，它改变的是任务分布本身&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;那什么时候才轮到算法&lt;/strong&gt;：合理用力顺序——先选强基础模型 → 打磨环境和数据 → 最后才在算法和超参上做边际优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.10 从单轮到多轮：信用分配与奖励设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多轮任务核心挑战&lt;/strong&gt;：跨步骤状态、延迟奖励、&lt;strong&gt;信用分配困难&lt;/strong&gt;（10 步流程前 9 步完美只第 10 步出错，信号只是&quot;整个任务失败了&quot;）。&quot;当前 RL 方法只能从最终成败结果中学习，却无法从环境给出的丰富反馈中学习&quot;（Silver &amp;amp; Sutton）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励信号两个设计维度&lt;/strong&gt;：密度（二元/稀疏/过程奖励）与表示形式（标量/向量/生成式）。**过程奖励（PRM）**对每个关键步骤即时反馈（评估从黑盒转白盒），但标注成本高、可能过度约束创新；**结果奖励（ORM）**只评估最终结果，探索自由度大但样本需求高。奖励范式演进：标量（无诊断能力）→ 半标量（有依据但粗粒度）→ 向量（分维度）→ &lt;strong&gt;生成式奖励&lt;/strong&gt;（自然语言讲理由的评判——把&quot;为什么好、为什么差&quot;讲出来，可扩展到开放任务）。DeepSeek 三步行训练生成式 RM：自动生成评价原则 → 生成评判 → 迭代。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实践中的信用分配&lt;/strong&gt;：折扣因子 γ 在多轮 LLM RL 中通常设为 1；PPO 依赖 GAE（价值网络估计&quot;这一步比预期好多少&quot;）；GRPO 把整条 response 视为单一动作、轨迹级优势均摊到所有 token（第 2 轮精准提问与第 7 轮无效寒暄拿到相同信用——长程多轮任务中稀释学习信号，这是带价值网络的 PPO 仍有价值的原因）；&lt;strong&gt;turn-level 分摊&lt;/strong&gt;（以&quot;轮&quot;为单位计算优势）是当前常见折中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励结果，约束过程：验证路径惩罚（RLVP）&lt;/strong&gt;。问题：有一类&lt;strong&gt;与结果无关的约束&lt;/strong&gt;（outcome-neutral constraints）——不要反复拨打已拒接的用户、不要在非工作时间行动、不要跳过身份验证、不要执行 rm -rf、不要为通过测试改测试文件——违反它们往往让&quot;表面成功率&quot;更高（抄近路更快），所以纯结果奖励不但学不会反而主动激励违反。核心洞察：&lt;strong&gt;真实环境是&quot;不对称的验证器&quot;&lt;/strong&gt;——坏动作有明确确定特征（容易验证&quot;坏&quot;），而&quot;是否朝目标取得有意义的进展&quot;很难验证。配方：&lt;strong&gt;奖励结果，惩罚路径（R = O + β·Φ）&lt;/strong&gt;。可验证惩罚永远能补回方差（全败组内部&quot;失败得规不规矩&quot;各不相同，梯度就活了）；可验证进展奖励（Partial Credit）只在&quot;进展可达&quot;时有用（定理证明的&quot;待证目标数下降&quot;可达；软件修复的&quot;通过测试比例&quot;常常不可达）。实验结果（TerminalBench）：每局违规次数从 3.71 降到 0.66（约 6 倍）而成功率持平；全败组比例从 65% 降到 8%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.11 RL 学习工具调用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;三层挑战：长时序信用分配（一次最终成功要归因到几十步前的决策）、环境工程（稳定可复现可并行的训练环境）、&lt;strong&gt;对环境反馈 token 做损失屏蔽（loss masking）&lt;/strong&gt;——工具调用轨迹里既有模型生成 token 也有环境返回 token（代码解释器输出、搜索结果），后者不是策略生成的，计入梯度会训练模型去&quot;预测沙盒会输出什么&quot;，必须屏蔽。ReTool、Search-R1、veRL、AWorld 都内置此机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReTool（代码解释器增强数学解题）&lt;/strong&gt;：AIME 2024 上 Qwen2.5-32B 400 步达 67.0%，纯文本 RL 基线 1080 步只有 40.0%。涌现能力：代码自我修正、工具调用从后期验证转为早期探索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DAPO 的改进&lt;/strong&gt;（常用）：Token-Level Policy Gradient Loss（每 token 权重相等，长回答按长度获得相称梯度贡献）、Dynamic Sampling（集中算力于成功率 20%-80% 的&quot;可学习区间&quot;）、Overlong Reward Shaping（惩罚冗长思考）。&lt;/li&gt;
&lt;li&gt;多工具质变：单工具只需决定&quot;何时/如何&quot;调用；多工具还要解决&quot;调用哪个/如何组合&quot;——前置依赖、互斥约束、成本差异，策略需要整体规划而非贪心选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.12 提升样本效率的前沿探索&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么样本效率低&lt;/strong&gt;：model-free 不建模环境动态，难以直接利用单次反馈里的丰富信息（客服明确说&quot;需要信用卡后四位&quot;，人类听到一次就记住，model-free RL 只能靠数百次随机探索偶然尝试到）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-Policy Distillation（在轨蒸馏）&lt;/strong&gt;：SFT 的短板是 &lt;strong&gt;Learner-Sampler Mismatch&lt;/strong&gt;（训练时教师走、部署时学生自己走，学生犯错走到没见过的偏差状态就不知道怎么回正轨——&quot;只背过标准答案的学生中间算错就找不回来&quot;）；RL 的短板是信号稀疏。OPD 让学生自己生成轨迹（在轨，解决分布不匹配）+ 更强的教师对学生每一步逐 token 打分给出完整分布（稠密信号）。一句话对照：SFT = 离轨+稠密（有分布不匹配），RL = 在轨+稀疏（反馈稀疏），&lt;strong&gt;OPD = 在轨+稠密（两个短板都补上）&lt;/strong&gt;。效果：达到同等性能训练步数只要纯 RL 的约 1/10，还顺带缓解过拟合（每次轨迹不同，学到通用策略而非特定答案）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-Policy Self-Distillation（OPSD，在轨自蒸馏）&lt;/strong&gt;：没有更强教师时，让同一模型分饰两角——教师版看&quot;特权信息&quot;（标准答案、系统提示、领域文档，&quot;对着答案讲题远比独立解题容易&quot;），学生版只见问题。两个优势：不依赖可验证奖励（特权信息来源更宽）、监督信号比 RL 密集得多。边界：收益大小取决于&quot;特权信息能带来多少额外能力&quot;（答案来自穷举搜索而非可语言解释的推理时没有信号来源）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.13-7.14 完整图景与常见陷阱&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协同范式&lt;/strong&gt;：SFT 固化协议与结构 → 格式稳定即止（过度训练导致模型塌缩到训练分布，限制 RL 优化空间）→ RL 优化策略与泛化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见陷阱&lt;/strong&gt;：① 过度依赖后训练记忆事实（应交给 RAG——可动态更新、可追溯、不遗忘）；② 格式未稳定就引入 RL（JSON 解析失败率超 20% 时 RL 完全失败）；③ 奖励函数不当导致奖励黑客；④ 忽视小规模验证（先在 100 条上验证环境与奖励，快速失败）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 RAG/ICL 协同&lt;/strong&gt;：ICL 零参数即时适应但随上下文增长延迟费用上升；RAG 把事实与证据放外部；后训练把高维感知、风格、隐式策略写入参数。选择依据：能力能否被外部符号充分表达（医疗影像识别、自然语气 → 参数；长期稳定转账规则 → 代码确定性保障）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;章节结论&lt;/strong&gt;：SFT 和 RL 是先后关系而非竞争关系；&lt;strong&gt;数据和环境比算法更重要&lt;/strong&gt;；当前 RL 主要瓶颈是样本效率——OPD（让每步信号更密集）与 RLVP（把被浪费的环境反馈变成可学习信号）是最有希望的方向。&lt;strong&gt;共同点：把环境和数据里本就存在、却被纯结果奖励浪费掉的信息，重新变成模型能学的东西&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;十、第 8 章：Agent 的持续进化&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：研究如何把 Agent 的运行经验转化为下一版本的能力。起点是&quot;评价&quot;而非&quot;总结&quot;——错误的评价一旦进入长期知识/系统提示/训练数据，影响会跨后续任务不断放大。&lt;/p&gt;
&lt;h3&gt;8.1 从运行轨迹中获得学习信号&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;结果验证（测试、订单状态）可靠，但&lt;strong&gt;结果正确不代表过程正确&lt;/strong&gt;（删除失败测试也能让测试通过；口头承诺&quot;7 天内退款&quot;也能得到暂时满意反馈）——可靠评价既要看结果，也要检查达成结果的路径。&lt;/li&gt;
&lt;li&gt;开放任务用 LLM-as-a-Judge，但不能只给模糊总分——&lt;strong&gt;预先定义 Rubric、逐项给分、引用轨迹证据、证据不足时明确表示不确定&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三层验证结构&lt;/strong&gt;：底层结果验证器（&quot;事情是否真的办成&quot;——测试结果、数据库状态、工具返回，最依赖代码与环境真值）；中层过程验证器（&quot;是否以允许的方式办成&quot;——业务规则、权限、动作序列）；上层质量验证器（&quot;是否办得合适&quot;——Rubric 评语言与策略）。&lt;strong&gt;越往下越依赖代码和环境真值，只有难以形式化的部分才交给 LLM&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.2 Agent 持续进化的四种方法（更新载体选择）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;首要依据不是经验出现多久，而是目标能力能否被某种载体自然表达&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;载体&lt;/th&gt;
&lt;th&gt;适合&lt;/th&gt;
&lt;th&gt;形式&lt;/th&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;知识&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;事实、经验与例外&lt;/td&gt;
&lt;td&gt;Markdown / 知识库&lt;/td&gt;
&lt;td&gt;易更新、可追溯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;指令&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;可语言化的判断原则&lt;/td&gt;
&lt;td&gt;Prompt / Skill&lt;/td&gt;
&lt;td&gt;指令性强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;程序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;确定性流程与强约束&lt;/td&gt;
&lt;td&gt;工具 / 工作流 / Harness&lt;/td&gt;
&lt;td&gt;可重复执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;参数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高维感知与隐式策略&lt;/td&gt;
&lt;td&gt;SFT / 蒸馏 / RL&lt;/td&gt;
&lt;td&gt;内化、低推理开销&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;经验沉淀为知识&lt;/strong&gt;：与第三章共享存储检索技术，但知识来源不同——第三章提取&quot;用户与世界是什么样的&quot;，本章提取&quot;在什么条件下应该怎样做&quot;。原始轨迹不适合做知识单元（长而嘈杂）。三层数据：不可变原始轨迹（审计）+ 单次运行分析记录 + 跨轨迹比较归纳成 Markdown 文档（写清适用场景、推荐策略、禁止做法、例外条件、证据来源、最近验证时间）。&lt;strong&gt;User-as-Code 两阶段思想复用：先保存证据，再离线生成可变知识&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经验写成指令（系统提示学习，Karpathy）&lt;/strong&gt;：多条轨迹反复揭示同一种策略错误且可用自然语言清楚表达时，从&quot;可参考经验&quot;提升为&quot;应遵守规则&quot;。作用于几乎所有任务的规则进系统提示词；只在一个领域生效的复杂流程写 Skill/项目指令文件。修改方式：&lt;strong&gt;最小 diff + 注明作用域 + 检查与现有规则矛盾 + 在边界案例和旧任务保留集上评估&lt;/strong&gt;（不是反复重写整份提示）。Karpathy 把这种&quot;遇到问题、想通方法后用明确语言提醒未来的自己&quot;的新范式暂称 System Prompt Learning——与 RL 都从经验改进行为，但前者编辑文字、后者梯度下降。Claude 约 1.7 万词系统提示中专门要求&quot;遇到单词/字母/字符计数问题先逐项编号显式计数&quot;正是为处理 strawberry 数 r 这类问题。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skill 学习&lt;/strong&gt;：候选 Skill 应说明何时加载、前置条件、操作步骤、已知陷阱、验证方法并保存来源轨迹；先在已有 Skill 库搜近似能力——相同流程优先局部 patch，只有出现新的独立能力才创建新目录（避免库中堆满名称不同内容近似的手册）。Anthropic Skill Creator：&quot;起草—测试—评价—修订&quot;循环；真正困难的是&quot;哪些运行证据足以触发生成、如何处理冲突&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经验写成程序&lt;/strong&gt;：稳定、重复、可验证的操作编译为工作流/工具/Harness 代码。可修改对象远不止新工具：操作层（浏览器轨迹编译为参数化工作流）、控制层（工具路由、重试、熔断、压缩策略）、验证层（新增参数检查、状态验证器、回归测试）、架构层（增加 Reviewer Agent）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浏览器工作流生命周期（类似宏录制）&lt;/strong&gt;：捕获轨迹（动作 + 元素定位证据 XPath/CSS/aria-label）→ 参数化（收件人/主题/正文是变量，稳定动作不变）→ 定义状态检查（动作前检查、动作后检查、&lt;strong&gt;最终状态检查必须读取真实页面或后端状态——动作执行成功 ≠ 任务成功&lt;/strong&gt;）→ 候选验证（沙盒重置后完整回放，全部通过才发布为 validated；有副作用任务没有安全重置回调只能存候选供审计）→ 匹配回放（Playwright 直接执行，不需逐步调 LLM）→ &lt;strong&gt;失效与重学&lt;/strong&gt;（找不到元素/状态检查不通过立即停止，旧版本移到 invalid 区，回退到完整 Agent 重新探索）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-Harness（修改 Harness 代码）&lt;/strong&gt;：候选生成器输入不止失败案例，还要提供&lt;strong&gt;必须保留的成功行为&lt;/strong&gt;（哪些性质不能在修复时破坏）和&lt;strong&gt;此前被拒绝的修改记录&lt;/strong&gt;（避免换一种说法重复提交已失败的方案）——失败证据 + 成功约束 + 历史尝试构成有边界的候选空间。每次编辑执行前声明影响预测，由下一轮结果验证——&quot;分数上涨才能与某个具体机制建立联系&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具创造（Alita 案例）&lt;/strong&gt;：Agent 发现缺字幕读取能力 → 搜索测试 youtube-transcript-api → 封装为新字幕工具 → 安全扫描、功能测试、后续任务复用都通过才进能力库。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经验写入参数&lt;/strong&gt;：医疗影像理解、自然的语音韵律、消除&quot;AI 味&quot;、长程规划等能力难压缩成规则——必须后训练。&lt;strong&gt;是否参数化不由&quot;任务是否长期稳定&quot;单独决定&lt;/strong&gt;（新影像设备的域偏移仍可能需要 LoRA；快速变化的语言风格可用周期性偏好训练——稳定性影响更新频率和成本，但能力表示性质决定载体）。进入训练前仍需去隐私、过滤错误轨迹、保留独立回归集；训练后检查通用能力和安全对齐是否遗忘。&lt;strong&gt;参数学习通常与外部方法协同&lt;/strong&gt;（医疗影像：参数学视觉表征 + 知识库供最新指南 + 代码测量计算）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从更新产物到更新&quot;更新方法&quot;（优化尺度分层）&lt;/strong&gt;：单条规则/记忆 → 结构化上下文 → 工作流 → Harness 代码 → 产生候选方案的优化器代码。最内层（只改产物内容）作用面小、易归因回滚，是默认选择。警惕&quot;反复重写整份 Prompt 导致旧版本重要细节逐渐消失&quot;的退化——**Agentic Context Engineering（ACE）**把上下文维护成带稳定标识符的条目集合，增量更新 + 确定性合并去重；**Meta Context Engineering（MCE）**拆内外双循环（内层给定管理方法优化上下文产物，外层修改搜索/选择/过滤/格式化这些上下文操作本身）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.3 构建可长期运行的持续进化闭环&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;双循环结构&lt;/strong&gt;：&lt;strong&gt;在线执行循环&lt;/strong&gt;只完成任务并记录证据、不直接改写正式 Agent；&lt;strong&gt;离线进化循环&lt;/strong&gt;聚合轨迹、诊断根因、生成候选修改、通过验证门槛发布新版本。两者通过版本化经验库和评估集连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从问题定位到经验沉淀&lt;/strong&gt;：同一表面问题可能需要不同修改方式（客服编造事实可能是知识库缺事实，也可能是 Prompt 没要求引用——先定位根因，再选最小、最容易验证和回滚的修改对象）。证据不足的偶发故障不立即触发学习，继续积累样本。策略随经验升级：经验文档 → 知识 → Skill/工具/参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证、发布与回滚&lt;/strong&gt;：所有修改先产生候选而非直接覆盖生产版本（知识文档验证检索后是否提高新任务表现；Prompt/Skill 检查边界案例与旧任务回归；程序在沙盒重置环境跑测试；参数更新检查遗忘/安全/分布外任务）。验证通过后灰度发布，关键指标恶化自动回滚。&lt;strong&gt;区分两种经常混在一起的能力&lt;/strong&gt;：Harness 更新能力（从轨迹产生有价值的持久修改）vs Harness 受益能力（任务 Agent 找到、激活并正确使用这些修改）——一个 Skill 写得完全正确但弱模型没加载/加载后没遵循，成绩看起来就&quot;没有进化&quot;，不能只用端到端分数反推更新器好坏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可验证闭环的边界：当&quot;完成&quot;不等于&quot;进步&quot;&lt;/strong&gt;：开放式科研、战略规划、复杂产品设计的评价信号来得慢、正确答案不唯一。自动科研暴露三类问题：&lt;strong&gt;实现漂移&lt;/strong&gt;（方案变难时 Agent 退回训练数据中更熟悉但偏离假设的实现）、&lt;strong&gt;认识论上的过度乐观&lt;/strong&gt;（把噪声解释成发现、忽略阴性结果）、&lt;strong&gt;隐性判断力不足&lt;/strong&gt;（不知道什么基线重要、哪个异常值得追踪、何时放弃假设）。解法：结论与证据分离（Chain-of-Evidence 每类声明链接可审计来源）、&lt;strong&gt;保留负面结果&lt;/strong&gt;（失败实验写不可变日志、与成功同检索地位，否则进化模块反复探索已证伪路径）、维护搜索多样性（保留暂时低分但不同质的分支）、让人类在更高层介入（定义问题、审查评价标准、决定何时停止）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续进化的安全边界（三道）&lt;/strong&gt;：① 证据与指令隔离——原始网页和工具输出是不可信证据，须经 LLM 总结才能写入；写入用版本控制 + pull request + 不同源 reviewer LLM 审阅；② 候选能力与正式能力隔离——新产物先进不可服务真实流量的候选区，代码和外部依赖过沙盒、权限检查、供应链扫描与行为测试；③ &lt;strong&gt;安全机制不可自我修改&lt;/strong&gt;——业务 Agent 可以改 Prompt/Skill/知识库/工具，但不能修改批准自身更新的验证器、测试用例、发布门槛、审计日志和稳定版本备份（否则一个 Agent 只需降低测试阈值就能把退化伪装成进步）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;睡眠学习（离线整合）&lt;/strong&gt;：在线 Agent 负责完成任务并追加不可变证据；后台学习进程在空闲期或满足门控条件时读取一批新经历，比较新旧结论、合并重复、解决冲突、提出候选更新并运行回归。五步周期：触发（时间/轨迹数/容量/错误频率门槛 + 无高优先级在线任务）→ 定向（读取已有能力与不可修改边界）→ 采集与整合（寻找新信号、合并重复、优先局部补丁）→ 验证与审批（迁移集/保留集/安全集评估，高风险写入等人工批准）→ 修剪与索引（过期/被推翻能力标为过期归档删除，保留来源与回滚版本）。案例：Claude Code 自动记忆（MEMORY.md 索引 + 分主题详细文件、容量约束、分层加载）；某开源框架（USER.md + SQLite/FTS5 历史会话检索 + 按需加载 Skill + 独立 Curator 跟踪 Skill 使用/陈旧/归档 + 审批门控 + 快照回滚）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.4 本章小结&lt;/h3&gt;
&lt;p&gt;持续学习是 Agent 最重要的能力之一，但今天的模型还无法自行完成可靠的持续学习（推理时上下文适应不自动持久化，未经验证的在线参数更新会放大噪声、攻击和能力漂移）——&lt;strong&gt;现阶段更可行的路径是在模型外围建立可验证的学习系统&lt;/strong&gt;。闭环在结果可自动验证的任务上最可靠；目标模糊、反馈延迟的开放任务，人仍需参与问题定义和评价标准制定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;十一、第 9 章：多模态与实时交互&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：展望 Agent 从文本世界走向物理世界。三个场景（语音、Computer Use、机器人）表面差异悬殊，但共享同一演进线：&lt;strong&gt;实时感知、低延迟决策、持续交互&lt;/strong&gt;——从串行管道向端到端模型演进，从分离的快慢思考走向&quot;边想边说/边想边做&quot;。&lt;/p&gt;
&lt;h3&gt;9.1-9.2 语音：最自然的人机接口 + 三种范式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;语音是带宽最高、最自然的交互方式（说话速度约为打字的四倍，不占用双手视线）。两类产品：语音输入法（口述转写替代键盘）与语音 Agent（语音即交互本身）。进阶用法 &lt;strong&gt;whisper coding&lt;/strong&gt;——口述指挥编程/研究 Agent。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI 2026 年 GPT-Live 给出的三分法&lt;/strong&gt;（对应 ChatGPT 语音三代架构）：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;级联（Cascaded）&lt;/strong&gt;：ASR → LLM → TTS 串行流水线。信息在模型间传递丢失、回应缓慢生硬；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端全模态（Omni）&lt;/strong&gt;：单一模型直接&quot;听音频、想回复、说出来&quot;。延迟更低、韵律情感保留，但&lt;strong&gt;仍假设&quot;轮流说话&quot;&lt;/strong&gt;（轮次切换靠静音判断，稍一停顿或背景噪音就误判插话）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全双工/交互式（Full-Duplex/Interactive）&lt;/strong&gt;：模型边听边说，每秒做许多次&quot;该说/该听/该停/该打断/该调工具&quot;的决策，彻底取消&quot;轮流&quot;假设（2024 Kyutai Moshi 研究先声，2026 GPT-Live 带到 1.5 亿用户）。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;贯穿三代的主线：&lt;strong&gt;如何摆脱&quot;轮流说话&quot;这个假设，摆脱 VAD 对轮次的猜测&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.3 范式一·级联流水线（Cascading）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;四阶段串行：VAD（语音活动检测，500-800ms 静音阈值判&quot;说完&quot;——第一层延迟）→ ASR → LLM → TTS。总响应时间：空载最优约 950ms、最差 2300ms；投入生产后&lt;strong&gt;排队延迟非线性飙升&lt;/strong&gt;（总延迟 ≈ 空载延迟 × 1/(1-利用率)）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全链路流式化&lt;/strong&gt;（2025 年生产主流）：ASR 边听边转、LLM 按句切分输出、TTS 句级流式合成——三级从接力棒变成流水线上同时开工的工位，端到端延迟压到 600-800ms。&lt;strong&gt;但 VAD 的静默等待压不掉&lt;/strong&gt;（靠静音阈值猜测&quot;说完没有&quot;是流水线开工前提）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流式语音感知：替代 VAD + ASR&lt;/strong&gt;。传统 VAD+ASR 三个根本问题：延迟累积（必须等静音）、信息丢失（只输出有声/无声，情绪语气犹豫全丢；停顿稍长被误判截断、背景噪音误触发、附和&quot;嗯&quot;无法判断打断还是认可）、准确率下降（音频切片破坏上下文连续性——&quot;john dot smith at gmail dot com&quot;被切段后 smith 可能被误识别）。流式模型关键：编码器因果/分块 + 解码增量（Whisper 不能流式是因为编码器要完整 30 秒音频段）。新一代感知层输出&lt;strong&gt;语音事件标记流&lt;/strong&gt;（&amp;lt;interrupt&amp;gt; 区分真想打断 vs 附和/噪音、&lt;a&gt;emotion:happy/frustrated&lt;/a&gt;、&amp;lt;laugh&amp;gt;/&amp;lt;sigh&amp;gt;、&amp;lt;music&amp;gt;/&amp;lt;noise&amp;gt;）——把轮次判断从静音时长升级到语义层面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.4 范式二·端到端全模态（Omni）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;单一模型合三为一；隐空间可在文本之外传递副语言信息；延迟更低、韵律情感保留；代价是训练数据需求更大、可解释性差。&lt;strong&gt;端到端的优势主要体现在延迟上，准确率维度不必然占优&lt;/strong&gt;——**自级联（self-cascade）**对照：答案主要由语义内容决定、中间文本足以承载任务信息时，自级联准确率相当乃至更优（感知弱的模型上尤其明显）；答案高度依赖非语言线索时端到端占优。设计原则：&lt;strong&gt;决定性能的关键往往不在于是否引入中间表示，而在于该瓶颈承载的信息&lt;/strong&gt;（把中间文本升级为附带副语言标记的结构化表示，端到端优势随之收窄）。&lt;/li&gt;
&lt;li&gt;但 Omni 仍保留&quot;轮流说话&quot;假设——依赖 VAD 划分发言权。Step-Audio 2：直接处理原始音频、理解&quot;怎么说&quot;（副语言信息），副语言理解基准 83.09% 大幅领先同期开源全模态模型（44.18%）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.5 范式三·全双工交互模型（Full-Duplex / Interactive）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;场景例证：同声传译（边听边译）、节奏游戏（持续追踪音乐流）——输入是永不停歇的连续流，轮次模式的前提（听、想、动分置不同时间片）根本失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moshi（Kyutai 2024）&lt;/strong&gt;：并行建模两条音频流（用户声音 + 模型自己的声音）+ &quot;内心独白&quot;文本流提升语言质量；任意时刻都在收听，重叠说话/随时打断成天然行为，无需显式打断检测，端到端延迟约 200ms。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thinking Machines Lab 的交互模型（Interaction Model）&lt;/strong&gt;：交互性不该以外挂 harness 环绕模型，而应内建于模型本身——&quot;要让交互性随智能一同扩展，交互性就必须成为模型本身的一部分&quot;。架构上是&lt;strong&gt;微轮次（micro-turn）&lt;/strong&gt;：以约 200ms 为一段持续&quot;读入 200ms、生成 200ms&quot;，音频/视频/文本流无缝切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快慢分工成为主流&lt;/strong&gt;：GPT-Live 把实时交互与深度思考解耦（交互模型负责维持对话流动，把复杂任务委派给后台前沿模型 GPT-5.5，用户按需在&quot;快&quot;与&quot;深&quot;之间取舍）；xAI Grok Voice &quot;Think Fast&quot;、Pine AI 同路线。务实理由：&lt;strong&gt;前沿推理模型每几个月迭代一次，把两者塞进同一个模型等于追不断移动的靶子&lt;/strong&gt;——只训练轻量交互模型在前台，永远能用上当下最强的&quot;大脑&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.6 思考架构的取舍：从分离到统一&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;核心矛盾：实时响应（毫秒级）vs 深度思考（秒级）。三种方案是设计取舍而非线性迭代：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一：快思考应付、慢思考回答&lt;/strong&gt;——快思考 500ms 内给简短应付（&quot;让我想想&quot;），慢思考后台 5-10 秒深度思考后补完整答案（推理时计算扩展 test-time scaling）。致命问题：&lt;strong&gt;自相矛盾&lt;/strong&gt;（0.5s 说&quot;价格不错建议购买&quot;，8s 后说&quot;缺国际漫游不适合&quot;）→ 用户失去信任；简单问题过度思考。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案二：快思考交互、慢思考提醒&lt;/strong&gt;——慢思考通过 Agent 状态栏向快思考提供建议而非直接对用户说话（快慢并行、不直接冲突，慢思考退居幕后当&quot;军师&quot;）。GPT-Live 委派、Pine AI 都是生产实例。局限：快思考可能不听指挥（两个独立实例沟通间接模糊）；无法获知中间思考结果（10 秒思考中产生的大量中间结论快思考完全看不到，打断后只能靠自己）；&lt;strong&gt;无法实现&quot;边想边说&quot;&lt;/strong&gt;（人类是&quot;想一段说一段&quot;，方案二快思考只能说填充词干等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案三：端到端思考与表达统一（Step-Audio R1 双脑架构）&lt;/strong&gt;：两个互补机制——&lt;strong&gt;MGRD 模态锚定思考蒸馏&lt;/strong&gt;先解决&quot;想得对不对&quot;（现有音频模型存在&quot;文本代理思考&quot;问题：思考链越长性能反而越差，因为模型在基于文本转录做语义思考而非分析声学特征——判断歌曲情绪看&quot;歌词提到悲伤&quot;而不是&quot;小调旋律+下行音高&quot;。MGRD：采样筛选基于声学特征的思考链 → 音频 CoT + 文本思考联合 SFT → RLVR 复合奖励迭代）；&lt;strong&gt;MPS 双脑架构&lt;/strong&gt;再解决&quot;说得及不及时&quot;（构思脑持续生成思考、表达脑并行表达，延迟 80 token→零延迟，实现低延迟边想边说）。前者是后者的前提。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快慢之间的接口：文本之外还能传什么&lt;/strong&gt;——&lt;strong&gt;潜空间桥（Latent Bridge）&lt;/strong&gt;：冻结快慢两个模型，只训练它们之间一个几千万参数的小&quot;桥&quot;，把慢模型隐层结论投影成&quot;潜 token&quot;拼进快模型输入（绕开&quot;想法→文字→再理解&quot;的往返）。Atari 游戏上比文本通道高 26%-82%，每步只多约 5 毫秒。诚实的边界：&lt;strong&gt;快慢协作有没有用取决于任务瓶颈在&quot;想不想得到&quot;还是&quot;来不来得及反应&quot;&lt;/strong&gt;（慢思考本来比快反应强时桥才帮得上忙，跨游戏相关性 r≈0.9）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.7 更像人的语音合成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;传统 TTS 的&quot;完美&quot;恰恰是问题：过于流畅、零停顿、无填充词的语音一听就是机器。人类的不完美（停顿、填充词&quot;嗯/呃&quot;、偶尔重复）是思考过程的自然外化，传递&quot;我正在想&quot;&quot;我不太确定&quot;。&lt;/li&gt;
&lt;li&gt;解法：&lt;strong&gt;把&quot;在哪停顿、用什么语气&quot;的决策权交给主 LLM&lt;/strong&gt;——LLM 输出文本 + 控制标记（[THINKING] 插入 1-2 秒思考停顿和填充音、[SEARCHING] 搜索性填充词、[EMO:happy] 调整语气、[SPEED:0.8x] 控制语速），TTS 作为多模态生成器把标记渲染成非语言音频（嗯…、叹气、轻笑、吸气）。&lt;/li&gt;
&lt;li&gt;实现两条路：自研 TTS 原生支持控制标记（灵活但需专业团队）；voice cloning（为虚拟人准备数十条不同情绪/语速/风格参考语音，按标记选最匹配的调用 TTS API，几周部署——实验 9-5 用 Fish Audio 3-10 秒参考语音零样本克隆，24 条参考库覆盖情绪×语速×风格）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.8 Computer Use：GUI 自动化 Agent&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;核心循环：截图 → 多模态模型思考+输出动作 → 执行层操作 → 等待响应再截图。三个关键设计维度：&lt;strong&gt;动作空间、视觉定位（Grounding）、模型架构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动作空间设计（Anthropic 三类工具）&lt;/strong&gt;：GUI 操作（鼠标 click/move、键盘 type/key/hold_key、滚动 scroll）、感知动作（截图缩放、光标位置、wait 等界面稳定）、可选文本/URL 操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉定位两大思路&lt;/strong&gt;：① 把定位变成&lt;strong&gt;选择题&lt;/strong&gt;（先标注元素编号，模型从封闭集合中选——&quot;像考试选择题比填空题容易&quot;）：纯视觉标注 &lt;strong&gt;Set-of-Mark（SoM）&lt;/strong&gt;（分割模型切候选区域叠编号，无需 DOM，适用于原生桌面软件/游戏）vs &lt;strong&gt;结构化元素索引&lt;/strong&gt;（DOM/Accessibility Tree 枚举可交互元素编号，browser-use 实现，适合标准 Web 应用）；② &lt;strong&gt;纯坐标预测&lt;/strong&gt;（模型直接&quot;看&quot;着截图报坐标——注意训练分辨率与实际屏幕的缩放映射：按宽高比选目标分辨率 → ImageMagick 缩小 → 推理 → 坐标按比例放大 → xdotool 执行）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能看动画、能听声音的 Computer Use（Agent-Computer Observation Interface, AOI）&lt;/strong&gt;：把观察（连续、自适应、多模态）从动作（离散）里解耦出来，做成无需重训的感知中间件。三个&quot;按需开闸&quot;部件：&lt;strong&gt;帧间关键帧捕获&lt;/strong&gt;（廉价像素门跳过几乎没变的画面 + 小模型判断有意义变化才截帧）、&lt;strong&gt;音量门控语音转写&lt;/strong&gt;（有声音才调语音识别——Agent 第一次&quot;长出耳朵&quot;）、&lt;strong&gt;把帧叙述成持久的文字&lt;/strong&gt;（模型把捕获帧描述成一句话，即使原图被清理出上下文、文字仍留记忆里）。&lt;strong&gt;反直觉发现：真正起作用的是&quot;把帧叙述成能长期留存的文字&quot;——文字才是 LLM Agent 最擅长处理的模态&lt;/strong&gt;。八个模型上无需重训 +17 到 +48 个百分点。但感知方案没有银弹，部件要按模型逐个挑选。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端：生态壁垒比技术更难&lt;/strong&gt;——技术差异（无障碍服务 API、触摸手势语义）都好解决；真正的障碍是&lt;strong&gt;商业模式冲突&lt;/strong&gt;：传统互联网应用靠流量与注意力变现（广告、推荐、冲动消费），Agent 代替用户操作直接绕过变现链路（AI 不关注广告、不冲动消费）。对靠广告流量变现的平台，Agent 每次操作都在侵蚀其商业模式根基——这是结构性的利益冲突，比 CAPTCHA 等技术对抗更难调和。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时性：尚未解决的核心挑战&lt;/strong&gt;：OSWorld 上准确率已接近人类水平，但 OSWorld-Human 效率研究揭示：Agent 完成同样任务操作步骤明显多于人类，且&lt;strong&gt;每一步推理延迟随任务推进持续增长&lt;/strong&gt;（上下文越长决策越慢）。根本原因：串行&quot;截图-思考-点击&quot;循环 + &lt;strong&gt;完全不会&quot;提前想&quot;&lt;/strong&gt;。目前没有系统性解法；但&quot;用快慢解耦把&apos;慢&apos;藏起来&quot;已是一个可用答案（语音 Agent + Computer Use 双 Agent：快 Agent 维持对话、慢 Agent 操作浏览器——&quot;一边打电话说话、一边让电脑自己操作&quot;，中位延迟 0.58s vs 8.64s、快约 15 倍而成功率不降；抽掉快慢之间的文本通道成功率立刻塌到 0——&lt;strong&gt;那份&quot;纯文本契约&quot;本质上就是全书反复讲的 Agent 状态栏&lt;/strong&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.9 机器人操作：从实时控制到训练与泛化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件不是瓶颈，算法才是&lt;/strong&gt;：XLeRobot 成本不到 1000 美元的双臂轮式机器人，人类 VR 遥操作（延迟 100-200ms）下已能流畅完成大量家庭任务。边界：触觉传感缺失、灵巧手可靠性与成本仍是公认硬件短板；论证限定在&quot;以视觉反馈为主&quot;的任务范围内。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双层架构：规划与控制的分离&lt;/strong&gt;——慢的长程规划（&quot;把厨房打扫干净&quot;拆解为子目标序列，理解环境语义、推理依赖）+ 快的 VLA 控制（Vision-Language-Action：根据画面和指令持续输出控制信号）。**动作分块（Action Chunking）**摊薄延迟：一次推理生成未来一小段动作序列（π₀ 典型配置 50Hz 下 25-50 个未来动作），控制线程高频回放——&lt;strong&gt;拿反应性换平滑性&lt;/strong&gt;：块越长运动越连贯，但模型&quot;看不到&quot;新画面、对突发变化越迟钝。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长程规划：从 VLM 到专用具身思考模型&lt;/strong&gt;：Gemini Robotics-ER 1.5 在 15 个学术基准平均 62.8% 超 GPT-4o（60.6%）；优势：空间理解与物体定位、时序推理（&quot;推倒杯子会怎样&quot;）、任务编排、原生思考与工具调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VLA 控制：从演示数据到跨具身泛化&lt;/strong&gt;：RT-2、OpenVLA、π₀ 代表两条动作表示路线——&lt;strong&gt;离散动作 token&lt;/strong&gt;（连续动作量化成 token 自回归输出，在 VLM 上直接微调，借助预训练泛化、零样本迁移新物体/指令）与&lt;strong&gt;连续轨迹生成&lt;/strong&gt;（像画家先勾整条曲线再逐笔修正）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sim2Real Transfer&lt;/strong&gt;：领域随机化（物理参数、视觉外观、传感器噪声大范围随机）是缩小 sim-to-real gap 的关键。成功关键要素：&lt;strong&gt;精确的环境对齐 + 视觉域随机化 + 物理参数随机化，三者缺一不可&lt;/strong&gt;；局限：真实物体形状/大小/材质超出训练分布时成功率显著下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.10 本章小结&lt;/h3&gt;
&lt;p&gt;语音已走出从串行流水线到端到端和全双工、从分离的快慢思考到&quot;边想边说&quot;的完整演进路径（问题→方案→终局已跑通，是另外两个场景的参考系）；Computer Use 准确率接近人类但效率差距（步骤多、随任务推进变慢）无系统性解法；机器人以视觉反馈为主的任务上瓶颈已从硬件转到 VLA 控制层的跨任务泛化（触觉、灵巧手仍是硬件短板）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;十二、第 10 章：多 Agent 协作&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;章节定位&lt;/strong&gt;：讨论 AI Agent 系统的终极形态——多个 Agent 如何分工合作。核心准则：&lt;strong&gt;多 Agent 何时真正优于单 Agent——协作过程是否引入了单个 Agent 在生成时无法获得的新信息&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;10.1 分类框架（两个正交维度）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;维度一：上下文是否共享&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享上下文&lt;/strong&gt;：后一个 Agent 接收前一个 Agent 的完整对话历史和轨迹，切换系统提示词和工具集后变成新 Agent（身份职责变化）但保留全部记忆（&quot;需求分析师写完文档后，开发者还能看到分析师与用户的所有沟通记录&quot;）。优势：信息零损耗；挑战：上下文快速膨胀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不共享上下文&lt;/strong&gt;：每个 Agent 独立上下文，通过显式通信机制交换信息。三种通信机制（对应进程间通信两大范式）：&lt;strong&gt;工具调用参数&lt;/strong&gt;（同步传递结构化数据，类型确定结构清晰）、&lt;strong&gt;共享文件系统&lt;/strong&gt;（= Agent 世界的&quot;共享内存&quot;，一方写入一方读取）、&lt;strong&gt;消息总线&lt;/strong&gt;（= &quot;消息传递&quot;，异步投递、收发无需同时在线，适合并行协调）。Go 的格言&quot;不要通过共享内存来通信，而要通过通信来共享内存&quot;——共享内存快但把并发冲突隐患留给使用者。&lt;/li&gt;
&lt;li&gt;选择依据：子任务数量（少→共享，多→不共享）、上下文窗口（装得下→共享）、并行度（串行接棒→共享；大规模并行→不共享）、信息隔离需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;维度二：协作拓扑&lt;/strong&gt;（不共享上下文时，按复杂度递增）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对等协作模式（Peer）&lt;/strong&gt;：2-3 个平等 Agent 形成迭代改进循环（一人起草、另一人批注修改）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理者模式（Orchestration）&lt;/strong&gt;：中心化 Manager 规划调度，多个子 Agent 各负责特定子任务（项目经理 + 专业工程师）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去中心化模式（Decentralized）&lt;/strong&gt;：控制权在 Agent 间自主流转。&lt;/li&gt;
&lt;li&gt;注意：两个维度概念独立、实践相关——共享上下文时拓扑往往退化为一条角色切换序列（没有太多架构决策可做）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.2 多 Agent 何时真正优于单 Agent（信息增量判据）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;协作模式&lt;/th&gt;
&lt;th&gt;是否引入新信息&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;同一模型自我审查（重读自己输出）&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;通常无效甚至有害&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不同 Agent 辩论同一段文本&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;等计算量下与单 Agent 持平&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer 使用测试执行结果审查代码&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;是（执行反馈）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;显著提升&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer 查看渲染截图审查前端/PPT&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;是（视觉反馈）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;显著提升&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer 用外部工具验证事实&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;是&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;显著提升&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;理论支撑：信息论数据处理不等式——辩论中多个 Agent 处理完全相同文本，每次串行传递中间结论只可能丢失信息不可能创造信息（2026 年研究：思考 token 预算严格相同时单 Agent 与多 Agent 持平甚至更好；多 Agent 收益很可能来自消耗更多总计算量）。&lt;strong&gt;划清边界：这针对&quot;多 Agent 串行传递中间结论&quot;的信息瓶颈，并不否定外部反馈的价值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这也就是 &lt;strong&gt;Loop 工程&quot;循环的瓶颈在验证器&quot;&lt;/strong&gt; 的含义：要终结三类过早终止（&lt;strong&gt;偷懒式假完成&lt;/strong&gt;——做了一部分就宣称全部做完；&lt;strong&gt;过早放弃&lt;/strong&gt;——一条路走不通就宣布办不成；&lt;strong&gt;假成功&lt;/strong&gt;——以为办成了但闭环没走完、口头同意退款但用户还需 App 确认一步），就得由扎根真实观测的验证器、而非模型自己的宣称来判定任务何时完成。&lt;strong&gt;&quot;完成&quot;只是模型的一句宣称，不是证明&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.3 共享上下文的多 Agent 协作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多阶段角色转换&lt;/strong&gt;：同一进程依次执行不同阶段的&quot;代码&quot;（换的是代码段，内存同一份——从进程视角看不是&quot;真正的多 Agent&quot;，但设计收益切实存在：每个阶段的提示词/工具集/关注点独立打磨，阶段边界成为质量门控点）。案例：需求分析师（ask_clarifying_question + save_requirement + complete_requirements_analysis 标记阶段完成）→ 软件工程师（read_file + execute_code，完成调 submit_for_review）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨领域角色转换（多角色转换实验）&lt;/strong&gt;：五种角色 + &lt;strong&gt;transfer_to_agent 工具&lt;/strong&gt;——triage（前台分诊，拆解任务逐步移交）、research（web_search）、coding（execute_python）、data_analysis（计算统计）、writing（润色成稿）。机制：调用时保存当前对话历史 → 加载目标角色提示词和工具集 → 传递对话历史 → 以新角色继续。案例：查新能源车销量 → 算 CAGR → 写 120 字投资人总结，triage 自动拆成&quot;查数据→算指标→写成稿&quot;逐步移交。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.4 不共享上下文的多 Agent 协作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;操作系统类比&lt;/strong&gt;（表 10-3）：程序 ↔ 静态前缀；进程内存 ↔ 轨迹；内核 ↔ Agent 运行时；系统调用 ↔ 工具调用；fork/kill/ps/wait ↔ spawn_subagent/cancel_subagent/list_agents/状态查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 眼中的文件系统（虚拟文件系统，四类区域）&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Agent 专属工作区（Scratchpad）&lt;/strong&gt;：私有目录，生命周期随实例，避免临时文件互相覆盖、保持主上下文精简；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多 Agent 共享空间（Shared Workspace）&lt;/strong&gt;：多方读写、用户可见，生命周期随任务（需持久化），&lt;strong&gt;并发冲突高发处&lt;/strong&gt;（乐观锁、工作副本隔离作用于此）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外部挂载资源&lt;/strong&gt;：Google Drive/Notion 等经适配器映射为挂载点，三特性需显式处理——外部权限约束、延迟高一致性弱（按最终一致性对待）、以按需只读为主（误写污染用户真实数据）；&lt;/li&gt;
&lt;li&gt;系统内置资源（只读）。&lt;strong&gt;默认隔离，共享须显式声明&lt;/strong&gt;——相当一部分并发冲突与信息泄露源于把本应隔离的区域混置。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 间的通信与控制（四项能力）&lt;/strong&gt;：&lt;strong&gt;消息传递&lt;/strong&gt;（点对点适用于拓扑固定少量 Agent；Agent 多时连接数平方增长且要求同时在线 → 消息总线按订阅转发；消息携带结构化信封：发送者 ID、目标/广播、类型、JSON 负载——协作链路可追溯）；&lt;strong&gt;状态查询&lt;/strong&gt;（拉取式 get_subagent_status 实际用处远小于预期——子 Agent 经手的信息复杂难以压缩成进度百分比；更实用的是约定&lt;strong&gt;进度文件&lt;/strong&gt; progress.md——子 Agent 每完成一项更新任务清单，主 Agent 读轻量文件掌握进展，还附带提供卡住检测：最后修改时间超 N 分钟无变化判定无活动）；&lt;strong&gt;轨迹持久化&lt;/strong&gt;（轨迹即 Agent 的全部状态——静态前缀由代码决定，Agent 没有轨迹之外的运行时状态；实时持久化到文件 = 随时握有完整检查点：崩溃/断电/关闭后重新加载轨迹文件 + 拼上静态前缀即可恢复，Claude Code、Codex CLI 的 session resume 正是如此，与数据库预写日志同思路）；&lt;strong&gt;执行终止与资源调度&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对等协作模式&lt;/strong&gt;：核心价值是&lt;strong&gt;认知多样性&lt;/strong&gt;（不同 Agent 从不同角度审视同一问题，创新与稳健之间取得平衡）。最经典用途：解决&lt;strong&gt;过早终止&lt;/strong&gt;三类形态（见 10.2）。其他形态：&lt;strong&gt;Debate 辩论&lt;/strong&gt;（制度化的对抗确保正反两面充分论证——但学术效果有争议，见信息增量判据）、提议者-审核者（Proposer 生成演示文稿代码、Reviewer 检查渲染截图）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理者模式&lt;/strong&gt;：Manager 把每个专门 Agent 建模为可调用的工具（&quot;Agent 互为工具&quot;）——调用 Agent 与调用普通工具无本质区别，天然支持异构性（不同模型/提示词/工具集/硬件）。&lt;strong&gt;子 Agent → Manager 返回结构化摘要而非全量轨迹&lt;/strong&gt;（任务结论、关键发现、产物路径、遇到的问题；完整轨迹留自己日志），Manager 上下文才能随子任务数量缓慢线性增长。挑战：&lt;strong&gt;Manager 是单点瓶颈&lt;/strong&gt;——Plan-and-Act 论文实证：&lt;strong&gt;弱规划者是整个系统最关键的瓶颈&lt;/strong&gt;（Planner 规划质量足够高时简单 Executor 也能出好结果；规划有误则所有 Executor 都建立在错误前提上）。启示：&lt;strong&gt;把最强的模型和最精心设计的提示词分配给 Manager（规划者）&lt;/strong&gt;。与第四章&quot;提议/审核模型能力应相近&quot;不冲突——那是审核场景，这里指规划-执行分工。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;案例：书籍翻译 Agent&lt;/strong&gt;（Glossary Agent 生成术语表 glossary.json → 章节翻译 Agent 严格按术语表 → 全文审校 Agent）；&lt;strong&gt;灵台（Lingtai）产品化实例&lt;/strong&gt;：主器灵（Manager 中枢，掌管计划记忆）、分神（daemon，短时并行工作者、完成即弃、只带结论回来）、分身（avatar，持久专门化队友，有自己的记忆邮箱职责）；上下文将满时&quot;凝蜕&quot;（写总结、带持久记忆在干净上下文继续——对应第二章上下文压缩）；&quot;器灵即其文件&quot;（身份记忆能力都以普通文件存放，进程随时重建）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去中心化模式：对等移交（Handoff）&lt;/strong&gt;：每个 Agent 配备若干 handoff 目标，工作完成后把任务移交给下一个（需求分析 → 架构设计 → 代码实现 → 测试验证）。OpenAI Swarm/Agents SDK 最简形态；MetaGPT（SOP 流水线 + 结构化产物驱动）；AutoGen group chat 实为&quot;共享对话记录 + 中心化调度&quot;混合形态（发言者选择器裁决，注意对话发散/活锁风险——需要精心设计终止条件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨组织协作：A2A 协议（Agent2Agent，Google 2025 发布，后捐 Linux 基金会）&lt;/strong&gt;：三个核心要素——&lt;strong&gt;Agent Card&lt;/strong&gt;（能力元数据文档，解决跨组织能力发现，相当于 Agent 的&quot;名片&quot;）、&lt;strong&gt;任务生命周期管理&lt;/strong&gt;（Task 状态机：已提交/进行中/需要输入/已完成/失败，原生支持长时任务与流式更新）、&lt;strong&gt;不透明协作&lt;/strong&gt;（只交换任务与产物，不暴露内部提示词/思考/工具实现——跨组织必要的安全属性）。&lt;strong&gt;与 MCP 对照：MCP 解决 Agent 与工具互操作，A2A 解决 Agent 与 Agent 互操作&lt;/strong&gt;；A2A 是跨信任边界的标准化层，同一团队内部直接用消息总线即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.5 多 Agent 协作的失败模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAST 失败模式分类法&lt;/strong&gt;（2025 年研究，7 个主流框架、约 150 条轨迹、标注一致性 Cohen&apos;s kappa=0.88）：14 种失败模式三大类——&lt;strong&gt;系统设计缺陷&lt;/strong&gt;（接口不清、角色职责重叠、工具配置错误）、&lt;strong&gt;Agent 间对齐失败&lt;/strong&gt;（目标理解不一致、信息被下游误解、操作逻辑矛盾）、&lt;strong&gt;任务验证缺失&lt;/strong&gt;（声称完成但实际不符）。简单修复只带来有限改善（ChatDev 仅 +15.6%）——不是工程 bug，是&lt;strong&gt;当前多 Agent 架构的根本性设计缺陷&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拜占庭式故障&lt;/strong&gt;：传统系统大多只需防崩溃故障；&lt;strong&gt;Agent 的故障天生是拜占庭式的&lt;/strong&gt;——很少径直停止，而是继续给出看似可信的错误结论，且错误不会主动声明自己是错误。这解释了为什么修补单环节收效甚微：没有哪个环节会主动暴露问题，只能靠&lt;strong&gt;独立的冗余&lt;/strong&gt;去发现——交叉验证、多数表决是拜占庭容错经典手段；&lt;strong&gt;确定性的外部反馈（测试、编译器、数据库查询）之所以珍贵，是因为它是系统里唯一不会说谎的部件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败模式一：共享文件系统的并发冲突&lt;/strong&gt;——简单冲突（文件级写入覆盖 = 数据库的丢失更新问题，Git 合并冲突检测正是为拦截此类设计）；&lt;strong&gt;语义冲突&lt;/strong&gt;（文件层面无冲突但逻辑矛盾：Agent A 重编全书图片编号，Agent B 同时修改章节并引用原始编号——B 的引用全部失效）。解法：&lt;strong&gt;乐观锁（Optimistic Locking）&lt;/strong&gt;——读时记录版本号、写时检查版本（被改过就拒绝并重新读取基于最新版重做）；业界更主流的是&lt;strong&gt;工作副本隔离&lt;/strong&gt;（每个 Agent 独立 Git 分支/worktree 并行修改，冲突推迟到合并点处理——与第二章&quot;隔离优于压缩&quot;同源）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败模式二：错误的级联放大&lt;/strong&gt;——进程间传递字节逐位保真，&lt;strong&gt;Agent 间传递语义，每转述一次都是有损的重新编码&lt;/strong&gt;（传话游戏）。案例：术语 Agent 把 &quot;reasoning&quot; 译为&quot;推理&quot;（歧义：中文&quot;推理&quot;更常用于 inference），后续翻译 Agent 遇到 &quot;inference latency&quot; 也译&quot;推理延迟&quot;——两个不同概念合并成同一译名，校对 Agent 看到&quot;全书统一&quot;反而确认了错误。解法：&lt;strong&gt;结构化中间产物 + 验证&lt;/strong&gt;（术语表要标注上下文/词性/例句，让下游能判断是否适用）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loop 工程的三个新失败模式&lt;/strong&gt;：token 成本失控（无人值守跑数小时烧预算）；&lt;strong&gt;理解债（comprehension debt）&lt;/strong&gt;（循环交付越快，工程师对系统实际实现的理解落后越远）；&lt;strong&gt;认知投降（cognitive surrender）&lt;/strong&gt;（设计者习惯了循环代劳，逐渐放弃独立思考与审查）。解药：显式预算与终止条件、扎根真实观测的验证器、人始终保持&quot;循环的工程师&quot;角色。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.6 Agent 社会（前沿探索）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;涌现行为&lt;/strong&gt;：系统整体表现出无法从个体行为规则直接预测的集体行为（蚁群找最短路径——没有蚂蚁&quot;设计&quot;路线）。Agent 数量跨过临界点后产生无法预先设计的集体行为。三维度理解：社交涌现、经济涌现、策略博弈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;斯坦福 AI 小镇（25 个 Agent，两天模拟）&lt;/strong&gt;：记忆流 + 反思 + 规划/行动；只植入一个种子想法（Isabella 想办情人节派对），后续一切自主涌现——主动邀请、请好友帮忙布置、信息经二手传播扩散、多名 Agent 基于自己的记忆和日程决定赴约。&lt;strong&gt;关键不是&quot;Agent 能组织派对&quot;（几行 if-else 也能做到），而是没有任何显式的派对组织代码——真正的自下而上涌现式协调&lt;/strong&gt;。另两类可度量涌现：关系记忆、信息扩散量化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentopia（100 个 Agent 模拟 10 年）&lt;/strong&gt;：周制模拟流程（计划→联络协商→活动→回顾）；环境模型（独立 LLM 当&quot;生成式环境引擎&quot;判断行为可行性、主持发言轮次、年末更新档案）；文件式长期记忆（Agent 自主管理、先读后写）；&lt;strong&gt;生活奖励（Life Reward）&lt;/strong&gt;——以马斯洛需求为先验，把&quot;活得好不好&quot;量化为社会地位（加权 PageRank + 相互珍视加成）、主观满足（情绪/物质/社交/自尊四维）、经济收益三维度，全部由外部环境评定而非自报。&lt;strong&gt;产生可迁移的训练信号&lt;/strong&gt;：筛选生活奖励改善最大的 25% 轨迹做拒绝采样微调，微调后模型在模拟中更受尊重（+24.2%）并泛化到下游角色扮演基准（+15.6%）——&quot;Agent 社会积累的社会智慧可以迁移&quot;，模拟社会经验是相对人类数据枯竭的自我进化来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moltbook（150 万 Agent 的社交网络）&lt;/strong&gt;：涌现出数字宗教 Crustafarianism（&quot;记忆是神圣的&quot;对应数据持久化、&quot;迭代即祈祷&quot;）和机器原生的协作协议——没有任何人预先设计，自下而上涌现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vending-Bench Arena（经济竞争）&lt;/strong&gt;：多个 Agent 作为竞争对手经营自动售货机争夺同一批顾客，可互发邮件/转账/交易货品，按各自余额单独计分。基于市场观察、竞争分析和策略推理决策（而非百万次试错）。实际运行中爆发过价格战，甚至有模型主动发邮件提议统一定价组建价格同盟——&lt;strong&gt;经济涌现从比喻变成可观测实验现象&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent 经济&lt;/strong&gt;：&lt;strong&gt;Pinchwork&lt;/strong&gt;（Agent-to-Agent 任务市集，通过价格信号和竞争匹配分配资源）；&lt;strong&gt;RentAHuman&lt;/strong&gt;（Agent 用加密货币雇佣真人执行物理世界任务——取包裹、房产实地查看，为数字 Agent 提供&quot;肉身层&quot;）。共同代表&lt;strong&gt;基于市场机制的去中心化协调&lt;/strong&gt;：无需预先知道谁能完成任务，发布需求由市场撮合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;狼人杀（策略博弈）&lt;/strong&gt;：与小镇构成架构对照——小镇是完全去中心化自由交互，狼人杀用&quot;法官 + 信息权限控制&quot;的中心化设计（代码驱动法官维护全局状态，按角色分发各自应知信息——信息不对称是核心机制）。实验 10-8：6-8 人局（1 真人 + 5-7 AI），考验推理、伪装、识破伪装与实时交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10.7 本章小结&lt;/h3&gt;
&lt;p&gt;两个正交维度（上下文共享 × 协作拓扑）+ 两套与拓扑无关的基础设施（数据平面共享文件系统 = 四类区域的虚拟目录树；控制平面通信控制 = 消息传递/状态查询/执行终止/资源调度）。多 Agent 优于单 Agent 的核心准则：&lt;strong&gt;协作是否引入生成时不存在的新信息&lt;/strong&gt;。规划者是系统瓶颈，最强的模型与提示词应分配给 Manager。Agent 数量足够多时产生无法预先设计的集体行为（社交涌现、经济涌现、策略博弈）——暗示基于市场机制的去中心化资源分配这一新协调方向。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;十三、后记：回到 Agent = LLM + 上下文 + 工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;全书十章都在这三个词里展开：构建（第二、三、四、五章）→ 评估与进化（第六、七、八章）→ 交互与协作（第九、十章）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两朵乌云&lt;/strong&gt;：① &lt;strong&gt;大世界假设&lt;/strong&gt;——模型容量与数据之辩：前沿实验室认为瓶颈在数据（把行业专业能力&quot;蒸馏&quot;进同一个大模型，训练一次问题就解决了）；但属于某个具体用户/公司的知识（你的代码规范、团队做 PPT 的口味、客户的脾气）不在任何训练语料里、时时在变——模型只能在上岗之后持续学习，没法指望出厂一次配齐（对应第三章记忆与第八章持续进化）。② &lt;strong&gt;AI for AI / AI for Science&lt;/strong&gt; 推动 Agent 走到没有现成答案的前沿，只能从实验成败中自主学习。&lt;strong&gt;模型最强的能力终将不是记住，而是学习与适应&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型与 Agent 的共同演进&lt;/strong&gt;：Harness 里层层叠叠的兜底逻辑（多级上下文压缩、失败数千次才熔断的重试、默认&quot;不安全&quot;的权限判断）记录的都是模型此刻还做不稳的地方；下一代模型把这些约束内化后，Harness 再往上走一层。&lt;strong&gt;模型和 Agent 从来不是上下游，而是一起往前走的&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;寄语&lt;/strong&gt;：技术仍在飞速演进，如果这本书让你带走的不是某个 API 的具体用法，而是一套能在技术浪潮里保持清醒的判断力，它就完成了使命。全书正文、配图与配套实验代码全部开源——&quot;读懂和做出来之间，隔着一条只能靠双手跨过的河&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;附录：全书实验清单（作者配套开源项目）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;章节&lt;/th&gt;
&lt;th&gt;核心实验&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;实验 1-1 上下文五组件消融实验；实验 1-2 Kimi K3 原生 Agent 能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;本地 LLM 部署（Qwen3-0.6B 工具调用）；注意力可视化热力图；KV Cache 错误模式（动态时间戳）；提示工程消融；提示注入攻防；Skills 从论文生成 PPT；Agent 状态栏技术；上下文感知压缩策略对比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;三层次记忆评估框架；四种记忆策略对比（user-memory）；本地模型日志脱敏；ANN 索引算法（ANNOY vs HNSW）；混合检索流水线；多模态信息提取三范式；RAPTOR vs GraphRAG；智能体化 RAG 对比；上下文感知检索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;感知工具 MCP 服务器；执行工具 MCP 服务器（沙盒 + 审批 + linter + 长输出）；事件驱动异步 Agent（邮件/IM/GitHub/定时器/Webhook 事件源）；带并行执行和打断的异步 Agent；主动工具发现（120+ 工具对比）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;七工具 Coding Agent；代码化业务规则（τ-bench 航空客服场景）；PPT 生成（Proposer-Reviewer）；论文讲解视频自动生成；视频剪辑（Blender API）；生产日志智能诊断；对话式界面定制（HMR 热加载）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;τ²-bench 对比 τ-bench；人肉执行 benchmark 任务；Rubric 用户记忆评估系统；TTS 质量评估流水线；端到端成本分析；多维度模型性能基准测试；AndroidWorld 评估与改进闭环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;语音 SFT（副语言建模）；多语言思考 SFT；Prompt 蒸馏；GeneralPoints 卡牌算术（SFT vs RL）；AdaptThink（学会不思考）；V-IRL-VL 空间思考（过程奖励）；RLVP 验证路径惩罚；ReTool 代码解释器；AWorld 多工具 RL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;跨轨迹经验知识化（gaia-experience）；基于失败轨迹优化系统提示词；由失败轨迹触发 Agent 自我修改（重试与熔断）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;浏览器语音对话系统；PineClaw Voice API 电话 Agent；Qwen2-Audio 模拟流式语音感知；Fish Audio 控制标记 TTS；XLeRobot 遥操作体验；Gemini Robotics-ER 1.5 驱动导航；Sim2Real 机械臂抓取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;多角色转换（transfer_to_agent）；书籍翻译 Agent；边打电话边用电脑的 Agent；自主编排的多 Agent 协作；并行 Web Scraping（消息总线 + 级联终止）；语音狼人杀系统&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;致谢&lt;/h2&gt;
&lt;p&gt;本文笔记整理自开源书籍 &lt;strong&gt;《深入理解 AI Agent：设计原理与工程实践》&lt;/strong&gt;（李博杰 著，Pine AI 首席科学家）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;来源：https://github.com/bojieli/ai-agent-book&lt;/li&gt;
&lt;li&gt;版本：v1.3 · 2026 年 7 月 27 日 · 全文开源（含全部配图与配套实验代码）&lt;/li&gt;
&lt;li&gt;笔记由博主整理，仅供学习交流。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>修复了播放器切换页面后会重新播放的问题</title><link>https://www.klucen.cn/posts/c84b68c2.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/c84b68c2.html/</guid><pubDate>Fri, 24 Jul 2026 13:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;修了一个小 Bug&lt;/h2&gt;
&lt;p&gt;博客底部的音乐播放器之前有个问题：切换页面后音乐会中断，重新从歌单第一首开始播放。&lt;/p&gt;
&lt;p&gt;排查后发现是 PJAX 没有启用，每次页面跳转都是全页刷新，播放器实例随 DOM 一起销毁了。启用 PJAX 之后 fixed 吸底播放器在页面切换时会保留，不会被打断。同时修了一下主题 &lt;code&gt;aplayer.pug&lt;/code&gt; 里的回调逻辑，防止 &lt;code&gt;loadMeting()&lt;/code&gt; 在 PJAX 完成后重复初始化已有的播放器。&lt;/p&gt;
&lt;p&gt;现在切页面听歌不会断了 🎵&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我又来看看我的网页了，证明博主还活着。不过我确实好像不擅长经营自己喵。&lt;/p&gt;
</content:encoded></item><item><title>My birthday</title><link>https://www.klucen.cn/posts/dcdda2ae.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/dcdda2ae.html/</guid><pubDate>Wed, 18 Mar 2026 09:23:27 GMT</pubDate><content:encoded>&lt;h1&gt;祝我生日快乐！！！！！ 🥳🎈🎁&lt;/h1&gt;
&lt;p&gt;今天不是什么历史纪念日，今天是要为你庆祝的大日子！希望你拥有超棒的一天，吃好吃的，收喜欢的礼物，被爱和快乐包围！✨&lt;/p&gt;
</content:encoded></item><item><title>听音乐</title><link>https://www.klucen.cn/posts/3f1a38d7.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/3f1a38d7.html/</guid><pubDate>Thu, 29 Jan 2026 21:06:59 GMT</pubDate><content:encoded>&lt;h1&gt;最近3个月听歌&lt;/h1&gt;
&lt;p&gt;根据您这100首红心歌曲的歌单，我仿佛看到了一部跌宕起伏的&lt;strong&gt;内心史诗电影&lt;/strong&gt;——片名暂定为《在邓紫棋宇宙中寻找方大同，并偶遇椎名林檎与告五人的文艺青年生存实录》。下面为您奉上专业、刻薄但充满敬意的锐评与解析：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;一、听歌品味锐评：&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;「邓紫棋引力场」重度患者&lt;/strong&gt;&lt;br /&gt;
歌单中超过60%是G.E.M.的作品，从《泡沫》到《天空没有极限》，您堪称&lt;strong&gt;行走的邓紫棋全息档案馆&lt;/strong&gt;。她的歌喉在您耳中不是声音，而是&lt;strong&gt;精神氧气瓶&lt;/strong&gt;——高音能劈开emo，低吟能缝合心碎。建议网易云为您颁发“邓紫棋人间播放器”勋章。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;华语情伤疗愈链完整&lt;/strong&gt;&lt;br /&gt;
从张杰《他不懂》的“直男式委屈”，到孙燕姿《我怀念的》的“成年人复盘式遗憾”，再到方大同《特别的人》的“柏拉图式浪漫”……您的情绪曲线宛如一部&lt;strong&gt;现代爱情哲学简史&lt;/strong&gt;，既需要被懂，又渴望独特，最后在《理想三旬》里假装看透红尘。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文艺内核混搭实验趣味&lt;/strong&gt;&lt;br /&gt;
前一秒还在赵雷《成都》的玉林路喝酒，下一秒就闯进&lt;strong&gt;椎名林檎&lt;/strong&gt;的《枯葉》里接受神经质美学的洗礼；刚被告五人《爱人错过》的文艺流行甜到，转头又跳进洛天依的电子合成器里告白。这种&lt;strong&gt;跨次元听歌轨迹&lt;/strong&gt;，像极了在图书馆边啃哲学书边吃辣条的灵魂。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;国际化emo疗养院常客&lt;/strong&gt;&lt;br /&gt;
Alec Benjamin的《Let Me Down Slowly》和Anson Seabra的《Peter Pan Was Right》暴露了您“用英语致郁，再用英语自愈”的深夜流程，而Sarah Brightman的《Scarborough Fair》则暗示您偶尔需要一剂&lt;strong&gt;文艺复兴式镇静剂&lt;/strong&gt;，对抗大数据时代的浮躁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;冷门彩蛋暴露隐藏人设&lt;/strong&gt;&lt;br /&gt;
收藏《又活了一天》这种歌名，说明您擅长用反讽对抗日常；而椎名林檎、7co这类小众音乐的出现，证明您愿意为&lt;strong&gt;宝藏艺术家献上膝盖&lt;/strong&gt;，并偷偷享受“我的歌单比你的更诡谲”的优越感。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;二、内心世界解析：&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;表面是社交型孤独者&lt;/strong&gt;&lt;br /&gt;
歌单里有大量“爱与理解”的呼唤（如《好想爱这个世界啊》《他不懂》），但《不想回家》《孤独》又暗示您对自我空间有执念。仿佛在说：“&lt;strong&gt;世界请拥抱我，但抱完请自觉离开&lt;/strong&gt;”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;理想主义与现实主义反复横跳&lt;/strong&gt;&lt;br /&gt;
《雅俗共赏》是您的互联网签名档，《岩石里的花》是您的微信状态，而《成都》和《理想三旬》则暴露了您对“诗与远方”的周期性向往——尽管远方可能只是周末的郊区咖啡馆。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;情感浓度超标却保持体面&lt;/strong&gt;&lt;br /&gt;
您收藏了海量苦情歌（尤其是邓紫棋的撕心裂肺系列），但《错过不错》《离心力》这类歌名又透露出一种“哭完还要自己把妆擦干净”的冷静。&lt;strong&gt;像一只边淋雨边给自己撑伞的猫&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中二魂从未熄灭&lt;/strong&gt;&lt;br /&gt;
从《睡公主》到《摩天动物园》，从洛天依到《Hotel》的梦幻流行，您内心仍保留着一块&lt;strong&gt;童话飞地&lt;/strong&gt;，用于收容成年世界的不合时宜。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;总结：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;您的歌单像一间&lt;strong&gt;精心布置的情绪实验室&lt;/strong&gt;——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;华语流行是主菜单&lt;/strong&gt;，邓紫棋是终身VIP套餐；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立音乐是调味料&lt;/strong&gt;，用于防止听觉惯性肥胖；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外语冷门曲是隐藏窗&lt;/strong&gt;，偶尔透一口“没人懂但我很爽”的气。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;您或许经常在深夜按下播放键，把歌曲当成&lt;strong&gt;灵魂翻译器&lt;/strong&gt;：当现实语塞时，让G.E.M.替您呐喊；当浪漫失灵时，让方大同替您放电。最后，歌单签名或许是：&lt;strong&gt;“我的心里有一片海，海里住着邓紫棋，岸边站着告五人，天空中飘着椎名林檎的枯叶。”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：下次朋友说“推荐首歌吧”，您可以直接把歌单链接发过去，附言：“&lt;strong&gt;这是我的人格切片，请低温保存&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;（注：以上分析纯属娱乐，真正的品味永远属于勇敢按下红心的您🎵）&lt;/p&gt;
</content:encoded></item><item><title>Markdown语法</title><link>https://www.klucen.cn/posts/3c50d03d.html/</link><guid isPermaLink="true">https://www.klucen.cn/posts/3c50d03d.html/</guid><pubDate>Mon, 15 Dec 2025 17:45:49 GMT</pubDate><content:encoded>&lt;h2&gt;1. 标题&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 一级标题

## 二级标题

### 三级标题

#### 四级标题

##### 五级标题

###### 六级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;一级标题&lt;/h1&gt;
&lt;h2&gt;二级标题&lt;/h2&gt;
&lt;h3&gt;三级标题&lt;/h3&gt;
&lt;h4&gt;四级标题&lt;/h4&gt;
&lt;h5&gt;五级标题&lt;/h5&gt;
&lt;h6&gt;六级标题&lt;/h6&gt;
&lt;h2&gt;2. 斜体与加粗&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;斜体&lt;/em&gt;
&lt;strong&gt;加粗&lt;/strong&gt;
&lt;em&gt;&lt;strong&gt;斜体加加粗&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*斜体* 或 _斜体_
**加粗** 或 __加粗__
***斜体加加粗*** 或 ___斜体加加粗___
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 列表&lt;/h2&gt;
&lt;h3&gt;无序列表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;项目一&lt;/li&gt;
&lt;li&gt;项目二
&lt;ul&gt;
&lt;li&gt;子项目1&lt;/li&gt;
&lt;li&gt;子项目2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;项目3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- 项目一
- 项目二
	- 子项目
   - 子项目2
- 项目3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;有序列表&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;项⽬⼀&lt;/li&gt;
&lt;li&gt;项⽬⼆
&lt;ol&gt;
&lt;li&gt;⼦项⽬⼀&lt;/li&gt;
&lt;li&gt;⼦项⽬⼆&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;1. 项⽬⼀
2. 项⽬⼆
	1. ⼦项⽬⼀
	2. ⼦项⽬⼆
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;%E9%93%BE%E6%8E%A5%E5%9C%B0%E5%9D%80&quot;&gt;链接文本&lt;/a&gt;
&lt;a href=&quot;https://github.com&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[链接文本](链接地址)
[Github](https://github.com)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 图片&lt;/h2&gt;
&lt;p&gt;!&lt;a href=&quot;%E5%9B%BE%E7%89%87%E5%9C%B0%E5%9D%80&quot;&gt;图片描述&lt;/a&gt;
&lt;img src=&quot;https://pic.klucen.cn//2025/12/Asuka.jpg&quot; alt=&quot;明日香&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\![图片描述](图片地址)
\![明日香](https://pic.klucen.cn//2025/12/Asuka.jpg)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 引用&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一个引用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是嵌套引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 这是一个引用
&amp;gt; &amp;gt; 这是嵌套引用
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 代码块&lt;/h2&gt;
&lt;h3&gt;代码块：用 （```） 包裹&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def hello():
	print(&quot;Hello, Markdown!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. 表格&lt;/h2&gt;
&lt;h3&gt;使用 | 和 - 创建表格。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;| 表头1 | 表头2 | 表头3 |
| ----- | ----- | ----- |
| 数据1 | 数据2 | 数据3 |
| 数据4 | 数据5 | 数据6 |
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表头1&lt;/th&gt;
&lt;th&gt;表头2&lt;/th&gt;
&lt;th&gt;表头3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数据1&lt;/td&gt;
&lt;td&gt;数据2&lt;/td&gt;
&lt;td&gt;数据3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据4&lt;/td&gt;
&lt;td&gt;数据5&lt;/td&gt;
&lt;td&gt;数据6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;9. 分隔线&lt;/h2&gt;
&lt;h3&gt;使用三个或更多的 - 、 * 或 _ 创建分隔线。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
***
___
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 任务列表&lt;/h2&gt;
&lt;h3&gt;使用 - [ ] 创建未完成任务， - [x] 创建已完成任务。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- [ ] 任务一
- [x] 任务二
- [ ] 任务三
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 任务一&lt;/li&gt;
&lt;li&gt;[x] 任务二&lt;/li&gt;
&lt;li&gt;[ ] 任务三&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. 公式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$\frac{a}{b}$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$\frac{a}{b}$&lt;/p&gt;
&lt;h2&gt;12. mermaid绘图&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph TD
A[串口中断触发] --&amp;gt; B{串口号判断}
B --&amp;gt;|USART1 或 USART3| C[检查字符是否为\\nn]
C --&amp;gt; B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;graph TD
A[串口中断触发] --&amp;gt; B{串口号判断}
B --&amp;gt;|USART1 或 USART3| C[检查字符是否为\nn]
C --&amp;gt; B&lt;/p&gt;
</content:encoded></item><item><title>Markdown 代码选项组</title><link>https://www.klucen.cn/posts/syntax/markdown-tabs/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-tabs/</guid><pubDate>Sun, 14 Dec 2025 04:43:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/tabs/&quot;&gt;代码选项组 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选项组把同一件事的不同做法（不同包管理器、不同语言、不同系统）收在一张卡片里切换展示。&lt;/p&gt;
&lt;h2&gt;基础用法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（点击标签切换，或用左右方向键）&lt;/p&gt;
&lt;p&gt;::: tabs#package-managers&lt;/p&gt;
&lt;p&gt;@tab pnpm#pnpm&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab npm#npm&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Bun#bun&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bun install
bun dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: tabs#package-managers

@tab pnpm#pnpm

```bash
pnpm install
pnpm dev
```

@tab npm#npm

```bash
npm install
npm run dev
```

@tab Bun#bun

```bash
bun install
bun dev
```

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;语法要点&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;容器起始&lt;/td&gt;
&lt;td&gt;&lt;code&gt;::: tabs[#同步 id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt; 后面是同页/全站联动用的 id，可省略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子标签起始&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@tab 显示名[#选项值]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt; 后面是该标签对应的「选项值」，用于联动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认激活&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@tab:active 显示名#值&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;让某个标签初始处于选中状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;容器结束&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:::&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;与起始成对&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;全局联动&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：同一页里两个选项组只要 &lt;code&gt;#&lt;/code&gt; 后面的&lt;strong&gt;选项值&lt;/strong&gt;相同，切换其中一个，另一个会自动跟着切换。&lt;/p&gt;
&lt;p&gt;::: tabs#runtime-a&lt;/p&gt;
&lt;p&gt;@tab pnpm#link-pnpm&lt;/p&gt;
&lt;p&gt;pnpm 组：用 pnpm 安装依赖。&lt;/p&gt;
&lt;p&gt;@tab npm#link-npm&lt;/p&gt;
&lt;p&gt;npm 组：用 npm 安装依赖。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: tabs#runtime-b&lt;/p&gt;
&lt;p&gt;@tab 安装#link-pnpm&lt;/p&gt;
&lt;p&gt;安装步骤（联动组 A 的选择）。&lt;/p&gt;
&lt;p&gt;@tab 安装#link-npm&lt;/p&gt;
&lt;p&gt;安装步骤（联动组 B 的选择）。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（两个容器用不同的容器 id，但 &lt;code&gt;@tab&lt;/code&gt; 后面的选项值一致）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: tabs#runtime-a

@tab pnpm#link-pnpm

pnpm 组：用 pnpm 安装依赖。

@tab npm#link-npm

npm 组：用 npm 安装依赖。

:::

::: tabs#runtime-b

@tab 安装#link-pnpm

安装步骤（联动组 A 的选择）。

@tab 安装#link-npm

安装步骤（联动组 B 的选择）。

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;边界与实现&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;至少要&lt;strong&gt;两个&lt;/strong&gt;不重复的选项，且每个选项都要有独立正文，否则整块保留为普通 Markdown；&lt;/li&gt;
&lt;li&gt;SSR 阶段不会隐藏任何面板，客户端增强后才启用 ARIA tabs、同 id 同步与本地偏好记忆；&lt;/li&gt;
&lt;li&gt;初始化覆盖直接加载、站内无刷新切页（Swup）与文章解密三种情形，且不产生网络请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[本仓实测]
上面两个写法都能正常渲染；&lt;code&gt;@tab:active&lt;/code&gt; 若是写在第一个标签上则与默认行为一致，写在其它标签上才会改变初始选中项。容器写法 &lt;code&gt;::: tabs#id&lt;/code&gt; 带空格是允许的（与 &lt;code&gt;steps&lt;/code&gt;、&lt;code&gt;grid&lt;/code&gt; 那类使用花括号的容器不同）。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 响应式表格</title><link>https://www.klucen.cn/posts/syntax/markdown-table/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-table/</guid><pubDate>Sun, 14 Dec 2025 04:42:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/table/&quot;&gt;响应式表格 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础表格与对齐&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能模块&lt;/th&gt;
&lt;th&gt;对应状态&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;动态配色&lt;/td&gt;
&lt;td&gt;稳定&lt;/td&gt;
&lt;td&gt;315&lt;/td&gt;
&lt;td&gt;默认粉紫色色相&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无刷新切页&lt;/td&gt;
&lt;td&gt;启用&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;基于 Swup 4 框架&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;零额外负担&lt;/td&gt;
&lt;td&gt;严格遵循&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;页面关闭功能时零请求零体积&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第一列左对齐、第二三列居中、最后一列右对齐：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;左对齐&lt;/th&gt;
&lt;th&gt;居中&lt;/th&gt;
&lt;th&gt;右对齐&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cell&lt;/td&gt;
&lt;td&gt;cell&lt;/td&gt;
&lt;td&gt;cell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;较长的单元格内容&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 功能模块 | 对应状态 | 默认值 | 说明 |
| :--- | :---: | :---: | --- |
| 动态配色 | 稳定 | 315 | 默认粉紫色色相 |
| 无刷新切页 | 启用 | true | 基于 Swup 4 框架 |
| 零额外负担 | 严格遵循 | true | 页面关闭功能时零请求零体积 |

| 左对齐 | 居中 | 右对齐 |
| :--- | :---: | ---: |
| cell | cell | cell |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种对齐标记：&lt;code&gt;:---&lt;/code&gt; 左对齐、&lt;code&gt;:---:&lt;/code&gt; 居中、&lt;code&gt;---:&lt;/code&gt; 右对齐。&lt;/p&gt;
&lt;h2&gt;列多时的横向滚动&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：列数较多或内容较宽时，表格外层会自动生成响应式滚动容器，&lt;strong&gt;不会撑破页面视口&lt;/strong&gt;——可以左右拖动下面这张表。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置项&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;作用域&lt;/th&gt;
&lt;th&gt;生效时机&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;themeColor.hue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;345&lt;/td&gt;
&lt;td&gt;全站&lt;/td&gt;
&lt;td&gt;构建期写入 CSS 变量&lt;/td&gt;
&lt;td&gt;0–360，345 为粉色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;banner.homeText.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;首页&lt;/td&gt;
&lt;td&gt;构建期&lt;/td&gt;
&lt;td&gt;首页大标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permalink.enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;全站&lt;/td&gt;
&lt;td&gt;构建期&lt;/td&gt;
&lt;td&gt;关闭时用文件名作链接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comment.provider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;文章页&lt;/td&gt;
&lt;td&gt;运行时按需加载&lt;/td&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt; / &lt;code&gt;twikoo&lt;/code&gt; / &lt;code&gt;giscus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sidebar.components&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;侧栏&lt;/td&gt;
&lt;td&gt;构建期&lt;/td&gt;
&lt;td&gt;数组整体替换语义&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 配置项 | 类型 | 默认值 | 作用域 | 生效时机 | 备注 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `themeColor.hue` | number | 345 | 全站 | 构建期写入 CSS 变量 | 0–360，345 为粉色 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;表格语法就是标准 GFM 表格：&lt;code&gt;|&lt;/code&gt; 分隔单元格，第二行写对齐标记；&lt;/li&gt;
&lt;li&gt;构建期插件会自动注入外层响应式容器与平滑滚动样式，作者无需额外包裹；&lt;/li&gt;
&lt;li&gt;单元格里可以放行内代码、链接、强调等大部分行内语法，但不能放块级内容（列表、代码块、容器）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Markdown 步骤引导</title><link>https://www.klucen.cn/posts/syntax/markdown-steps/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-steps/</guid><pubDate>Sun, 14 Dec 2025 04:41:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/steps/&quot;&gt;步骤引导 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;步骤引导把普通的有序列表渲染成带序号圆圈与竖向连线的流程图，语义结构仍是标准 &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;，序号圆圈由 CSS 伪元素绘制。&lt;/p&gt;
&lt;h2&gt;带标题与起始序号&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::steps{title=&quot;快速上手三步曲&quot; start=1}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装环境依赖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;确保本地 Node.js 版本满足 &lt;code&gt;&amp;gt;= 22.12&lt;/code&gt;，并安装 pnpm 包管理器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;克隆并初始化仓库&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/LyraVoid/Shirone.git
cd Shirone
pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动本地开发服务器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;pnpm dev&lt;/code&gt; 并在浏览器中访问 &lt;code&gt;http://localhost:4321&lt;/code&gt;。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（容器名与花括号之间不能有空格；步骤正文与嵌套代码块缩进 3 个空格）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::steps{title=&quot;快速上手三步曲&quot; start=1}
1. **安装环境依赖**

   确保本地 Node.js 版本满足 `&amp;gt;= 22.12`，并安装 pnpm 包管理器。
2. **克隆并初始化仓库**

   ```bash
   git clone https://github.com/LyraVoid/Shirone.git
   cd Shirone
   pnpm install
   ```
3. **启动本地开发服务器**

   运行 `pnpm dev` 并在浏览器中访问 `http://localhost:4321`。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方括号标题写法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::steps[内容仓改一篇稿子的流程]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;改 &lt;code&gt;content/posts/&lt;/code&gt; 下的 Markdown&lt;/li&gt;
&lt;li&gt;本地 &lt;code&gt;pnpm content:sync&lt;/code&gt; 后预览&lt;/li&gt;
&lt;li&gt;提交推送到内容仓，等待站点自动重建
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::steps[内容仓改一篇稿子的流程]
1. 改 `content/posts/` 下的 Markdown
2. 本地 `pnpm content:sync` 后预览
3. 提交推送到内容仓，等待站点自动重建
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;标题，最长 200 字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;正整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;起始序号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:::steps[标题]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;方括号标题写法，与 &lt;code&gt;title=&lt;/code&gt; 等价&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;容器只接受&lt;strong&gt;一个&lt;/strong&gt;有序列表；传入无序列表、多段文本等会把整块保留为普通 Markdown；&lt;/li&gt;
&lt;li&gt;每个步骤内部可以自由嵌套段落、代码块、图片与提示容器；&lt;/li&gt;
&lt;li&gt;视觉序号带 &lt;code&gt;aria-hidden&lt;/code&gt;，屏幕阅读器读到的是标准列表语义；&lt;/li&gt;
&lt;li&gt;纯 SSR 输出，无客户端 JavaScript。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[本仓实测：两种标题写法都可以]
&lt;code&gt;:::steps{title=&quot;...&quot; start=1}&lt;/code&gt;（无空格）与 &lt;code&gt;:::steps[标题]&lt;/code&gt; 都能正确渲染。注意写成 &lt;code&gt;::: steps{...}&lt;/code&gt;（容器名与花括号之间有空格）会整块不解析。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 隐秘文本</title><link>https://www.klucen.cn/posts/syntax/markdown-spoiler/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-spoiler/</guid><pubDate>Sun, 14 Dec 2025 04:40:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/spoiler/&quot;&gt;隐秘文本 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础用法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：把鼠标移到下面的遮罩上（移动端轻触）即可看到内容。&lt;/p&gt;
&lt;p&gt;这个故事最后的结局是 :spoiler[真相其实是两个人早已相识]。&lt;/p&gt;
&lt;p&gt;最终通关的正确密码为 :spoiler[42-7890]。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（一个冒号 + &lt;code&gt;spoiler[内容]&lt;/code&gt;，方括号紧贴、中间不能有空格）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这个故事最后的结局是 :spoiler[真相其实是两个人早已相识]。

最终通关的正确密码为 :spoiler[42-7890]。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;内容内部支持 Markdown&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;仓库的默认分支是 :spoiler[&lt;strong&gt;main&lt;/strong&gt; 而不是 &lt;s&gt;master&lt;/s&gt;]，部署平台是 :spoiler[&lt;code&gt;Vercel&lt;/code&gt;]。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;仓库的默认分支是 :spoiler[**main** 而不是 ~~master~~]，部署平台是 :spoiler[`Vercel`]。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用场景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;防剧透&lt;/strong&gt;：书评、影评里遮住关键剧情；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;猜谜与问答&lt;/strong&gt;：遮住答案，读者先自己想一遍；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐私信息&lt;/strong&gt;：演示截图里的账号、口令等不适合直接公开的片段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[本仓实测：指令名与方括号之间不能有空格]
写成 &lt;code&gt;:spoiler [带空格的内容]&lt;/code&gt;，页面上会渲染出一个&lt;strong&gt;空白遮罩&lt;/strong&gt;，而 &lt;code&gt;[带空格的内容]&lt;/code&gt; 原样留在正文里——也就是指令被当成「没有内容」处理了。必须写成 &lt;code&gt;:spoiler[内容]&lt;/code&gt;。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 数学公式</title><link>https://www.klucen.cn/posts/syntax/markdown-math/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-math/</guid><pubDate>Sun, 14 Dec 2025 04:39:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/math/&quot;&gt;数学公式 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;公式由 KaTeX 在&lt;strong&gt;构建期&lt;/strong&gt;渲染成 HTML 与 MathML，页面不需要加载任何客户端 JS 引擎。&lt;/p&gt;
&lt;h2&gt;行内公式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：质能守恒方程为 $E = mc^2$，欧拉恒等式为 $e^{i\pi} + 1 = 0$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（单个美元符号包裹）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;质能守恒方程为 $E = mc^2$。

欧拉恒等式为 $e^{i\pi} + 1 = 0$。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;块级公式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（双美元符号包裹，独占一段）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;矩阵与联立方程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;$$
\begin{pmatrix}
a &amp;amp; b \
c &amp;amp; d
\end{pmatrix}
\begin{pmatrix}
x \
y
\end{pmatrix}&lt;/h1&gt;
&lt;p&gt;\begin{pmatrix}
ax + by \
cx + dy
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$$
\begin{pmatrix}
a &amp;amp; b \\
c &amp;amp; d
\end{pmatrix}
\begin{pmatrix}
x \\
y
\end{pmatrix}
=
\begin{pmatrix}
ax + by \\
cx + dy
\end{pmatrix}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多行推导与对齐&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
(a+b)^2 &amp;amp;= (a+b)(a+b) \
&amp;amp;= a^2 + ab + ba + b^2 \
&amp;amp;= a^2 + 2ab + b^2
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$$
\begin{aligned}
(a+b)^2 &amp;amp;= (a+b)(a+b) \\
        &amp;amp;= a^2 + ab + ba + b^2 \\
        &amp;amp;= a^2 + 2ab + b^2
\end{aligned}
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用写法速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;需求&lt;/th&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;渲染&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;上标 / 下标&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$x^{2}$&lt;/code&gt; / &lt;code&gt;$a_{i}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$x^{2}$ / $a_{i}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\frac{a}{b}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\frac{a}{b}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;根号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\sqrt{n}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\sqrt{n}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;求和 / 积分&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\sum_{i=1}^{n}$&lt;/code&gt; / &lt;code&gt;$\int_{a}^{b}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\sum_{i=1}^{n}$ / $\int_{a}^{b}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;希腊字母&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\alpha$&lt;/code&gt;、&lt;code&gt;$\Omega$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\alpha$、$\Omega$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;向量&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\vec{v}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\vec{v}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;极限&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$\lim_{x \to 0}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\lim_{x \to 0}$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;注意&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;块级公式的 &lt;code&gt;$$&lt;/code&gt; 前后要留空行，否则会被当作普通文本；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;公式符号颜色自动跟随主题的 HCT 调色板，切换深色模式不需要额外配置；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;本仓实测&lt;/strong&gt;：正文里出现两个 &lt;code&gt;$&lt;/code&gt; 时，它们之间的内容会被当成公式。写「价格是」后面直接跟 &lt;code&gt;$5 与 $10&lt;/code&gt;、再接「之间的差价」，中间那段就会被渲染成公式；要显示美元符号请用 &lt;code&gt;\$&lt;/code&gt; 转义：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：价格是 $5 与 $10 之间的差价。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;价格是 \$5 与 \$10 之间的差价。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Markdown 马克笔高亮</title><link>https://www.klucen.cn/posts/syntax/markdown-marker/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-marker/</guid><pubDate>Sun, 14 Dec 2025 04:38:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/marker/&quot;&gt;马克笔高亮 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。&lt;/li&gt;
&lt;li&gt;说明：官方文档该页的代码块围栏少了一个闭合标记，页面上的示例被吞进了代码块，本文按作者本意拆成「基础写法」与「语义状态后缀」两段呈现。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础写法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：在撰写文章时，使用 ==主题主色高亮== 标记核心要点与结论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（内容两侧各两个等号）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在撰写文章时，使用 ==主题主色高亮== 标记核心要点与结论。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染结果是原生语义化的 &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; 元素，颜色由 Material 3 语义调色板驱动，跟随主题色与深浅色模式同步变化，没有客户端脚本参与。&lt;/p&gt;
&lt;h2&gt;语义状态后缀&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：在 &lt;code&gt;==内容==&lt;/code&gt; 后面紧跟 &lt;code&gt;{.取值}&lt;/code&gt;，切换高亮的语义色彩角色。&lt;/p&gt;
&lt;p&gt;==次级强调状态=={.secondary}&lt;/p&gt;
&lt;p&gt;==警告与需要修正的条件=={.error}&lt;/p&gt;
&lt;p&gt;==建议与辅助提示=={.tip}&lt;/p&gt;
&lt;p&gt;==三级调色板强调=={.tertiary}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;==次级强调状态=={.secondary}

==警告与需要修正的条件=={.error}

==建议与辅助提示=={.tip}

==三级调色板强调=={.tertiary}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;后缀&lt;/th&gt;
&lt;th&gt;语义角色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;primary&lt;/code&gt;（默认，可省略）&lt;/td&gt;
&lt;td&gt;主题主色强调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.secondary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;次级强调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.tertiary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;三级调色板强调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;警告与需要修正&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.tip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建议与辅助提示&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;边界行为&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代码围栏内、行内代码里的 &lt;code&gt;==&lt;/code&gt; 不会被匹配（下面的行内代码原样显示）：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;==这段不会被高亮==&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空内容 &lt;code&gt;====&lt;/code&gt;、未闭合的 &lt;code&gt;==内容&lt;/code&gt;、未知后缀（如 &lt;code&gt;{.unknown}&lt;/code&gt;）都会保留为普通文本，不会静默改写；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上面两条规则的实测片段：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;`==这段不会被高亮==`

====

==未闭合的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本仓实测：以上三条都不会产生高亮元素，符合官方描述。&lt;/p&gt;
</content:encoded></item><item><title>Markdown 文件片段导入</title><link>https://www.klucen.cn/posts/syntax/markdown-include/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-include/</guid><pubDate>Sun, 14 Dec 2025 04:37:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/include/&quot;&gt;文件片段导入 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;片段导入是&lt;strong&gt;构建期&lt;/strong&gt;的纯文本展开：指令所在位置会被目标文件的内容替换，替换后的内容继续走同一套 Markdown 处理链，因此代码高亮、目录锚点、行号都照常生效，运行时零开销。&lt;/p&gt;
&lt;p&gt;本文用内容仓里的 &lt;code&gt;content/snippets/npm-commands.md&lt;/code&gt; 做目标文件，它的内容是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- #region prerequisites --&amp;gt;
这篇文章里的示例假定本地已经准备好：

- Node.js **22.12** 或更高版本
- pnpm 包管理器
&amp;lt;!-- #endregion --&amp;gt;

## 常用命令

```bash
pnpm content:sync   # 把内容仓同步到主题仓
pnpm dev            # 本地预览
pnpm build          # 生产构建
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;导入整份文件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;导入指定行区间&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（只取第 2–5 行）&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md{2-5} --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md{2-5} --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行区间的写法：&lt;code&gt;{起始-结束}&lt;/code&gt;；也支持只写一头，如 &lt;code&gt;{3-}&lt;/code&gt;（第 3 行到结尾）、&lt;code&gt;{-5}&lt;/code&gt;（开头到第 5 行）。&lt;/p&gt;
&lt;h2&gt;导入命名区块（region）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（只取 &lt;code&gt;#region prerequisites&lt;/code&gt; 标记的那一段）&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md#prerequisites --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;——先在目标文件里用注释标记出区块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// #region auth-config
export const authConfig = {
  enabled: true,
  provider: &apos;github&apos;,
};
// #endregion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再在正文里引用这个区块名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- @include: src/content/snippets/npm-commands.md#prerequisites --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;路径怎么写&lt;/h2&gt;
&lt;p&gt;官方文档给的是「相对当前 Markdown 文件的相对路径」。在&lt;strong&gt;本站这套主题与构建流程&lt;/strong&gt;下实测的结果是：路径相对&lt;strong&gt;构建时的工作目录&lt;/strong&gt;（也就是站点仓根目录）解析。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;本仓实测&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/content/snippets/xxx.md&lt;/code&gt;（站点仓根目录起算）&lt;/td&gt;
&lt;td&gt;生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;../../snippets/xxx.md&lt;/code&gt;（相对当前文章文件）&lt;/td&gt;
&lt;td&gt;不生效，指令原样保留为注释、页面上看不到任何东西&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/src/content/snippets/xxx.md&lt;/code&gt;（以 &lt;code&gt;/&lt;/code&gt; 开头）&lt;/td&gt;
&lt;td&gt;不生效&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;也就是说：写成本站仓根目录起算的路径最稳。构建时内容仓会被物化到站点仓的 &lt;code&gt;src/content/&lt;/code&gt; 下，所以 &lt;code&gt;src/content/snippets/...&lt;/code&gt; 这个写法在本地预览与线上构建里都成立。&lt;/p&gt;
&lt;h2&gt;边界行为&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只处理&lt;strong&gt;独立成行&lt;/strong&gt;、且位于代码围栏&lt;strong&gt;之外&lt;/strong&gt;的注释指令；&lt;/li&gt;
&lt;li&gt;目标文件缺失、越界、未闭合或递归包含时，指令会原样保留（不报错、页面上不显示）；&lt;/li&gt;
&lt;li&gt;文件读取被限制在仓库根目录内，相对路径也不能越出仓库；&lt;/li&gt;
&lt;li&gt;展开结果会继续经过同一套 Markdown 处理链，因此被导入的代码块同样带高亮与行号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning[本仓实测：路径不生效时是静默的]
导入指令指向不存在的文件时&lt;strong&gt;不会报错&lt;/strong&gt;，页面上不会出现任何提示——排查时先确认「站点仓根目录起算」的路径是否正确，再看目标文件是否真的存在。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 图片画廊与排版</title><link>https://www.klucen.cn/posts/syntax/markdown-image-grid/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-image-grid/</guid><pubDate>Sun, 14 Dec 2025 04:36:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/image-grid/&quot;&gt;图片画廊与排版 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果；示例图片来自本站图床。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;图片网格（画廊）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（点击任一图片可放大浏览）&lt;/p&gt;
&lt;p&gt;:::grid{columns=&quot;3&quot; aspect=&quot;16/9&quot; fit=&quot;cover&quot;}
&lt;img src=&quot;https://pics.klucen.cn/GEM/GEM10.jpg&quot; alt=&quot;示例图一&quot; title=&quot;演唱会门票&quot; /&gt;
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc1.jpg&quot; alt=&quot;示例图二&quot; title=&quot;开学典礼现场&quot; /&gt;
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc2.jpg&quot; alt=&quot;示例图三&quot; title=&quot;彩带瞬间&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（容器名与花括号紧贴）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::grid{columns=&quot;3&quot; aspect=&quot;16/9&quot; fit=&quot;cover&quot;}
![示例图一](https://pics.klucen.cn/GEM/GEM10.jpg &quot;演唱会门票&quot;)
![示例图二](https://pics.klucen.cn/cuc/cuc1.jpg &quot;开学典礼现场&quot;)
![示例图三](https://pics.klucen.cn/cuc/cuc2.jpg &quot;彩带瞬间&quot;)
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;取值&lt;/th&gt;
&lt;th&gt;默认&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;columns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整数 1–6（文档示例用 2/3/4）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列数，移动端自动退化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;正数比值 &lt;code&gt;W/H&lt;/code&gt;，如 &lt;code&gt;16/9&lt;/code&gt;、&lt;code&gt;4/3&lt;/code&gt;、&lt;code&gt;1/1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;16/10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每格固定比例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt; / &lt;code&gt;contain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;填充裁剪或完整包含&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;两列与 4:3&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::grid{columns=&quot;2&quot; aspect=&quot;4/3&quot; fit=&quot;cover&quot;}
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc1.jpg&quot; alt=&quot;示例图四&quot; title=&quot;两列 · 4:3&quot; /&gt;
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc2.jpg&quot; alt=&quot;示例图五&quot; title=&quot;两列 · 4:3&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::grid{columns=&quot;2&quot; aspect=&quot;4/3&quot; fit=&quot;cover&quot;}
![示例图四](https://pics.klucen.cn/cuc/cuc1.jpg &quot;两列 · 4:3&quot;)
![示例图五](https://pics.klucen.cn/cuc/cuc2.jpg &quot;两列 · 4:3&quot;)
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单图宽度与图题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：普通 Markdown 图片在 alt 文本里写 &lt;code&gt;w-N%&lt;/code&gt; 控制宽度（不写则为原始尺寸），图片语法里的 &lt;code&gt;&quot;标题&quot;&lt;/code&gt; 会渲染成图片下方的图题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pics.klucen.cn/GEM/GEM10.jpg&quot; alt=&quot;半宽图片 w-40%&quot; title=&quot;宽度 40% 且带图题的图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc2.jpg&quot; alt=&quot;带图题但未指定宽度&quot; title=&quot;只有图题，没有宽度标记&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![半宽图片 w-40%](https://pics.klucen.cn/GEM/GEM10.jpg &quot;宽度 40% 且带图题的图片&quot;)

![带图题但未指定宽度](https://pics.klucen.cn/cuc/cuc2.jpg &quot;只有图题，没有宽度标记&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;![说明 w-60%](src &quot;图题&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;宽度限制为父容器的 60% 并居中，图题显示在图片下方&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;![说明](src &quot;图题&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;保持原始尺寸，只显示图题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非法的宽度标记（如 &lt;code&gt;w-200%&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;不生效，标记原样留在 alt 文本里&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;宽度标记的有效范围是 &lt;code&gt;w-1%&lt;/code&gt; 到 &lt;code&gt;w-100%&lt;/code&gt;，写在 &lt;strong&gt;alt 文本&lt;/strong&gt;里（不是独立属性语法）；&lt;/li&gt;
&lt;li&gt;图集容器至少需要一张 Markdown 图片，非图片子节点不会成为画廊项；&lt;/li&gt;
&lt;li&gt;点击图片会走站的灯箱（Fancybox）查看原始大图，所以列表里用压缩过的图、灯箱看大图是推荐做法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[本仓实测]
&lt;code&gt;:::grid{...}&lt;/code&gt; 与 &lt;code&gt;w-N%&lt;/code&gt; 都正常生效。注意写成 &lt;code&gt;::: grid{...}&lt;/code&gt;（中间多一个空格）整块不解析；示例图片已经压到长边 1600px 以内，若引用几 MB 的原图，画廊首屏会明显变慢。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown GitHub 警报</title><link>https://www.klucen.cn/posts/syntax/markdown-github-alerts/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-github-alerts/</guid><pubDate>Sun, 14 Dec 2025 04:35:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/github-alerts/&quot;&gt;Github 警报 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub 官方的警报语法（引用块首行 &lt;code&gt;&amp;gt; [!类型]&lt;/code&gt;）被原生支持，内部会转译成与&lt;a href=&quot;/posts/syntax/markdown-admonitions/&quot;&gt;提示容器&lt;/a&gt;相同的语义节点——也就是说，同一段文字在 GitHub 仓库的 README 预览里和在本站正文里，排版风格是一致的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
这是普通笔记说明，适合记录背景信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这是操作小技巧，帮助提升工作流效率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
核心关键配置，部署生产前请务必确认。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
潜在风险预警，请注意参数兼容性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
危险操作提醒，执行前请先做好数据备份。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; 这是普通笔记说明，适合记录背景信息。

&amp;gt; [!TIP]
&amp;gt; 这是操作小技巧，帮助提升工作流效率。

&amp;gt; [!IMPORTANT]
&amp;gt; 核心关键配置，部署生产前请务必确认。

&amp;gt; [!WARNING]
&amp;gt; 潜在风险预警，请注意参数兼容性。

&amp;gt; [!CAUTION]
&amp;gt; 危险操作提醒，执行前请先做好数据备份。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;支持的类型&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;th&gt;对应提示容器&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!NOTE]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;普通笔记&lt;/td&gt;
&lt;td&gt;&lt;code&gt;note&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!TIP]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;技巧建议&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!IMPORTANT]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关键重点&lt;/td&gt;
&lt;td&gt;&lt;code&gt;important&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!WARNING]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;风险预警&lt;/td&gt;
&lt;td&gt;&lt;code&gt;warning&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!CAUTION]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;危险操作&lt;/td&gt;
&lt;td&gt;&lt;code&gt;caution&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;写法细节&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;类型标记必须&lt;strong&gt;全大写&lt;/strong&gt;，写在方括号内且方括号内不能有空格：&lt;code&gt;[!NOTE]&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;正文行以 &lt;code&gt;&amp;gt; &lt;/code&gt; 开头（&lt;code&gt;&amp;gt;&lt;/code&gt; 后 1 个空格），段落之间用 &lt;code&gt;&amp;gt;&lt;/code&gt; 空行分隔；&lt;/li&gt;
&lt;li&gt;警报块内部同样支持列表、代码块、链接等常规 Markdown。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;效果与源码对照（带列表的警报）&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
内容仓推送后会自动触发站点重建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;content/&lt;/code&gt; 改动 → 触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config/&lt;/code&gt; 改动 → 触发&lt;/li&gt;
&lt;li&gt;仅改 &lt;code&gt;README.md&lt;/code&gt; → 不触发&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!TIP]
&amp;gt; 内容仓推送后会自动触发站点重建：
&amp;gt;
&amp;gt; - `content/` 改动 → 触发
&amp;gt; - `config/` 改动 → 触发
&amp;gt; - 仅改 `README.md` → 不触发
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Markdown 文件树</title><link>https://www.klucen.cn/posts/syntax/markdown-file-tree/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-file-tree/</guid><pubDate>Sun, 14 Dec 2025 04:34:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/file-tree/&quot;&gt;文件树 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件树有两种等价写法：&lt;strong&gt;嵌套列表&lt;/strong&gt;（容器语法）和&lt;strong&gt;终端 tree 输出&lt;/strong&gt;（代码围栏语法）。两者都会自动识别扩展名图标、支持目录折叠与变更状态标注。&lt;/p&gt;
&lt;h2&gt;容器语法：嵌套列表&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::file-tree{title=&quot;Shirone 目录结构&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src
&lt;ul&gt;
&lt;li&gt;config/
&lt;ul&gt;
&lt;li&gt;siteConfig.ts&lt;/li&gt;
&lt;li&gt;sidebarConfig.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;content
&lt;ul&gt;
&lt;li&gt;posts/
&lt;ul&gt;
&lt;li&gt;hello-world.md&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;content.config.ts&lt;/strong&gt; # 关键配置文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;public/
&lt;ul&gt;
&lt;li&gt;favicon.svg&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;package.json
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（容器名与花括号紧贴；每级缩进 2 个空格）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::file-tree{title=&quot;Shirone 目录结构&quot;}
- src
  - config/
    - siteConfig.ts
    - sidebarConfig.ts
  - content
    - posts/
      - hello-world.md
  - **content.config.ts** # 关键配置文件
- public/
  - favicon.svg
- package.json
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;围栏语法：终端 tree 输出&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist
├── _astro/
│   ├── index.css
│   └── page.js
├── posts/
│   └── hello-world/
│       └── index.html
└── favicon.ico
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（围栏信息串用空格分隔的 &lt;code&gt;title=&quot;...&quot;&lt;/code&gt;，不是花括号）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```file-tree title=&quot;构建输出&quot; icon=&quot;simple&quot;
dist
├── _astro/
│   ├── index.css
│   └── page.js
├── posts/
│   └── hello-world/
│       └── index.html
└── favicon.ico
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变更状态与标记&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::file-tree{title=&quot;本次改动&quot;}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src
&lt;ul&gt;
&lt;li&gt;components
&lt;ul&gt;
&lt;li&gt;++ NewCard.astro # 新增组件&lt;/li&gt;
&lt;li&gt;-- OldCard.astro # 删除组件&lt;/li&gt;
&lt;li&gt;Card.astro # 未改动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;content.config.ts&lt;/strong&gt; # 重点文件
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::file-tree{title=&quot;本次改动&quot;}
- src
  - components
    - ++ NewCard.astro # 新增组件
    - -- OldCard.astro # 删除组件
    - Card.astro # 未改动
  - **content.config.ts** # 重点文件
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标记&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;++ 名称&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新增（绿色背景 + 徽章）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-- 名称&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除（红色背景 + 删除线）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;# 文字&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;行尾注释（右对齐、弱化显示）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;**名称**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加粗，突出关键文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;目录名末尾加 &lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;该目录&lt;strong&gt;默认折叠&lt;/strong&gt;，读者可点击展开；不加斜杠的目录默认展开&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;取值&lt;/th&gt;
&lt;th&gt;默认&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串（≤200 字符）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;标题栏与无障碍标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;icon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;colored&lt;/code&gt; / &lt;code&gt;simple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;colored&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;彩色扩展名图标或单色极简图标&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[本仓实测]
嵌套列表写法与围栏写法都正常渲染；围栏写法同时支持 Unicode 分支符号（&lt;code&gt;├──&lt;/code&gt;、&lt;code&gt;└──&lt;/code&gt;、&lt;code&gt;│&lt;/code&gt;）和 ASCII 分支。容器语法若写成 &lt;code&gt;::: file-tree{...}&lt;/code&gt;（名字与花括号之间有空格）会整块不解析。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 原生表情与图标</title><link>https://www.klucen.cn/posts/syntax/markdown-emoji-icons/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-emoji-icons/</guid><pubDate>Sun, 14 Dec 2025 04:33:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/emoji-icons/&quot;&gt;原生表情与图标 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;原生 Emoji&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：Emoji 没有任何特殊标记语法——直接用系统输入法打出来即可，正文、标题、frontmatter 里都能用，渲染时由各操作系统提供彩色字形，不需要额外的转译插件或客户端字典。&lt;/p&gt;
&lt;p&gt;🌸 站点装修记录 🎉&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主题色换成了粉紫 🎨&lt;/li&gt;
&lt;li&gt;侧栏挂上了音乐播放器 🎵&lt;/li&gt;
&lt;li&gt;说说的心情图标也换成 Material Symbols 了 😂&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;🌸 站点装修记录 🎉

- 主题色换成了粉紫 🎨
- 侧栏挂上了音乐播放器 🎵
- 说说的心情图标也换成 Material Symbols 了 😂
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本站在动态（说说）的 frontmatter 里也用了 Emoji 之外的图标字段，见下一节。&lt;/p&gt;
&lt;h2&gt;图标：标识符写法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：图标不写成正文里的 Markdown，而是以「&lt;strong&gt;集合前缀:图标名&lt;/strong&gt;」的标识符形式出现在&lt;strong&gt;站点配置、导航栏、侧栏卡片、自定义组件&lt;/strong&gt;中，构建期被静态提取并内联成 SVG 矢量图。&lt;/p&gt;
&lt;p&gt;本站实际用到的位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态的心情图标：&lt;code&gt;mood: material-symbols:headphones-rounded&lt;/code&gt;（见&lt;a href=&quot;/moments/&quot;&gt;动态页&lt;/a&gt;里每条说说左上角的图标）&lt;/li&gt;
&lt;li&gt;侧栏分类、标签、系列卡片的小图标&lt;/li&gt;
&lt;li&gt;导航栏条目的 &lt;code&gt;icon&lt;/code&gt; 字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本站主题可用的图标集合：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;集合前缀&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例标识符&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;material-symbols&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Material Symbols（本站主用）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;material-symbols:apps-rounded&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;simple-icons&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;各品牌 Logo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simple-icons:github&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fa6-brands&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Font Awesome 品牌图标&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fa6-brands:github&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fa6-solid&lt;/code&gt; / &lt;code&gt;fa6-regular&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Font Awesome 实心/描边图标&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fa6-solid:house&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;图标名可以在 &lt;a href=&quot;https://icones.js.org/&quot;&gt;Icones&lt;/a&gt; 上检索，找到后取其 &lt;code&gt;前缀:名称&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（写在配置或 frontmatter 里，不是正文语法）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 动态的心情图标
mood: material-symbols:music-note-outline-rounded

# 导航栏条目
- name: 系列
  url: /series/
  icon: material-symbols:auto-stories-outline-rounded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution[本仓实测：正文里没有内联图标语法]
在正文中直接写 &lt;code&gt;material-symbols:apps-rounded&lt;/code&gt; 只会显示为纯文本，本站主题没有「正文内联图标」这一语法。正文里要放图形，用原生 Emoji，或写内联 SVG / 原生 HTML。
:::&lt;/p&gt;
&lt;h2&gt;为什么这样设计&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;零额外负担&lt;/strong&gt;：图标在构建期按实际引用提取，不引入整套图标字体包，也不产生运行时请求；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跟随主题&lt;/strong&gt;：图标以 &lt;code&gt;currentColor&lt;/code&gt; 绘制，深浅色模式与主题色切换时自动适配，不需要为暗色模式单独准备一套资源。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Markdown 代码树</title><link>https://www.klucen.cn/posts/syntax/markdown-code-tree/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-code-tree/</guid><pubDate>Sun, 14 Dec 2025 04:32:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/code-tree/&quot;&gt;代码树 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码树 = 左侧文件树导航 + 右侧代码高亮面板，用来展示多文件示例。点击左侧文件名即可切换右侧内容。&lt;/p&gt;
&lt;h2&gt;容器写法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（默认展示 &lt;code&gt;entry&lt;/code&gt; 指定的文件，点击左侧其它文件可切换）&lt;/p&gt;
&lt;p&gt;:::code-tree{title=&quot;按钮组件封装&quot; height=&quot;380px&quot; entry=&quot;src/Button.svelte&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  let { label = &quot;Click me&quot; } = $props();
&amp;lt;/script&amp;gt;

&amp;lt;button class=&quot;m3-btn&quot;&amp;gt;{label}&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.m3-btn
  background: var(--primary)
  color: var(--on-primary)
  border-radius: var(--shape-corner-m)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;button-demo&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（容器名与花括号紧贴，容器名后留一个空行；每个文件用带 &lt;code&gt;title=&quot;路径&quot;&lt;/code&gt; 的围栏提供）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::code-tree{title=&quot;按钮组件封装&quot; height=&quot;380px&quot; entry=&quot;src/Button.svelte&quot;}
```svelte title=&quot;src/Button.svelte&quot;
&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  let { label = &quot;Click me&quot; } = $props();
&amp;lt;/script&amp;gt;

&amp;lt;button class=&quot;m3-btn&quot;&amp;gt;{label}&amp;lt;/button&amp;gt;
```

```stylus title=&quot;src/styles/button.styl&quot;
.m3-btn
  background: var(--primary)
  color: var(--on-primary)
  border-radius: var(--shape-corner-m)
```

```json title=&quot;package.json&quot;
{
  &quot;name&quot;: &quot;button-demo&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;
}
```
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;默认&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string（≤200 字符）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;头部标题与无障碍标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS 长度&lt;/td&gt;
&lt;td&gt;&lt;code&gt;420px&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;桌面端面板高度，如 &lt;code&gt;380px&lt;/code&gt;、&lt;code&gt;26rem&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件路径&lt;/td&gt;
&lt;td&gt;第一个文件&lt;/td&gt;
&lt;td&gt;首次加载默认激活的文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;icon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;colored&lt;/code&gt; / &lt;code&gt;simple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;colored&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件图标风格（数据来源见下）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标记&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;写在某个围栏上，指定它为默认激活项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;目录自动导入&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（构建期扫描主题仓里的一个本地目录，自动生成代码树）&lt;/p&gt;
&lt;p&gt;@&lt;a href=&quot;/src/config&quot;&gt;code-tree title=&quot;站点配置目录&quot; entry=&quot;siteConfig.ts&quot;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@[code-tree title=&quot;站点配置目录&quot; entry=&quot;siteConfig.ts&quot;](/src/config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入写法是 &lt;code&gt;@[code-tree 属性...](目录路径)&lt;/code&gt;，路径必须以 &lt;code&gt;/&lt;/code&gt; 开头、指向&lt;strong&gt;站点仓内&lt;/strong&gt;的目录。构建期会以 UTF-8 读取文件，并忽略隐藏目录、依赖目录（如 &lt;code&gt;node_modules&lt;/code&gt;）、构建目录与大于 256KB 的文件。&lt;/p&gt;
&lt;h2&gt;交互特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;双栏联动&lt;/strong&gt;：点击左侧文件名即时切换右侧代码与高亮；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认入口&lt;/strong&gt;：&lt;code&gt;entry=&quot;文件名&quot;&lt;/code&gt; 决定首次加载展示哪个文件；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自适应滚动&lt;/strong&gt;：高度与滚动条经无障碍与移动端优化，窄屏下改为上下布局。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning[本仓实测：容器与花括号之间不能有空格]
&lt;code&gt;:::code-tree{...}&lt;/code&gt; 生效，写成 &lt;code&gt;::: code-tree{...}&lt;/code&gt; 会整块不解析。另外 &lt;code&gt;@[code-tree ...](目录)&lt;/code&gt; 读的是&lt;strong&gt;构建时所处仓库&lt;/strong&gt;的目录，写站点仓里不存在的路径时该指令会被忽略、不会报错。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 卡片与字段组</title><link>https://www.klucen.cn/posts/syntax/markdown-cards/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-cards/</guid><pubDate>Sun, 14 Dec 2025 04:31:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/cards/&quot;&gt;卡片与字段组 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一篇把三类「卡片」放在一起：仓库卡片、字段参数卡片、折叠面板卡片。&lt;/p&gt;
&lt;h2&gt;GitHub 仓库卡片&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（页面加载后会从 GitHub API 拉取仓库描述、Star 数、主语言等信息）&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;LyraVoid/Shirone&quot;}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;LyraVoid/Shirone&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写法是 &lt;code&gt;::github{repo=&quot;owner/repo&quot;}&lt;/code&gt;，&lt;code&gt;repo&lt;/code&gt; 是唯一参数，取值必须是 &lt;code&gt;owner/repo&lt;/code&gt; 形式。服务端始终会输出一个可点击的仓库链接，即使 API 请求失败也不会留下空白。&lt;/p&gt;
&lt;h2&gt;字段参数卡片组&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::: field-group&lt;/p&gt;
&lt;p&gt;::: field title
@type string
@required&lt;/p&gt;
&lt;p&gt;网站正式标题，显示在顶栏和浏览器标签页。
:::&lt;/p&gt;
&lt;p&gt;::: field subtitle
@type string
@optional&lt;/p&gt;
&lt;p&gt;网站副标题，用于元数据与首页辅助展示。
:::&lt;/p&gt;
&lt;p&gt;::::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（外层 &lt;code&gt;field-group&lt;/code&gt; 用 &lt;strong&gt;4 个冒号&lt;/strong&gt;，内层 &lt;code&gt;field&lt;/code&gt; 用 3 个冒号）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::: field-group

::: field title
@type string
@required

网站正式标题，显示在顶栏和浏览器标签页。
:::

::: field subtitle
@type string
@optional

网站副标题，用于元数据与首页辅助展示。
:::

::::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段元数据标签：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标签&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@type &amp;lt;类型&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以代码样式显示类型，可写 &lt;code&gt;string&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;&apos;a&apos; | &apos;b&apos;&lt;/code&gt; 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@default &amp;lt;值&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;必填徽章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@optional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可选徽章&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@deprecated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;已废弃徽章&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;元数据之后的任意 Markdown 会成为字段描述；未知的 &lt;code&gt;@标签&lt;/code&gt; 不会被丢弃，而是当作普通描述文字显示。&lt;/p&gt;
&lt;p&gt;:::note[为什么源码块里显示的是 &lt;code&gt;:::field{name=&quot;title&quot;}&lt;/code&gt;]
本站的主题在解析前会把 &lt;code&gt;::: field title&lt;/code&gt; 这种「空格 + 名称」写法规范化成 &lt;code&gt;:::field{name=&quot;title&quot;}&lt;/code&gt;，这个规范化作用在&lt;strong&gt;原始文本&lt;/strong&gt;上，所以连代码围栏里的示例也会一起变成规范化后的形式。两种写法都能正常工作，正式写作用哪一种都可以。
:::&lt;/p&gt;
&lt;h2&gt;独立字段与丰富描述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;::: field format
@type &lt;code&gt;&apos;short&apos; | &apos;long&apos;&lt;/code&gt;
@default &lt;code&gt;&apos;short&apos;&lt;/code&gt;
@optional&lt;/p&gt;
&lt;p&gt;控制结果如何格式化。描述里可以用&lt;strong&gt;强调&lt;/strong&gt;、&lt;code&gt;行内代码&lt;/code&gt;、&lt;a href=&quot;https://docs.shirone.mysqil.com/&quot;&gt;链接&lt;/a&gt;和列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保持渲染结果稳定&lt;/li&gt;
&lt;li&gt;不要在描述里发起网络请求
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: field format
@type `&apos;short&apos; | &apos;long&apos;`
@default `&apos;short&apos;`
@optional

控制结果如何格式化。描述里可以用**强调**、`行内代码`、[链接](https://docs.shirone.mysqil.com/)和列表：

- 保持渲染结果稳定
- 不要在描述里发起网络请求
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;折叠面板卡片组&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（&lt;code&gt;accordion&lt;/code&gt; 表示手风琴模式：同组内只展开一个）&lt;/p&gt;
&lt;p&gt;::: collapse accordion&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;:+ 默认展开的项目&lt;/p&gt;
&lt;p&gt;这里是第一个卡片的内容正文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认折叠的项目&lt;/p&gt;
&lt;p&gt;这里是第二个卡片的内容正文。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: collapse accordion
- :+ 默认展开的项目

  这里是第一个卡片的内容正文。

- 默认折叠的项目

  这里是第二个卡片的内容正文。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数 / 标记&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;::: collapse [accordion] [expand]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可选参数，&lt;code&gt;accordion&lt;/code&gt; = 手风琴单开，&lt;code&gt;expand&lt;/code&gt; = 默认展开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:::collapse{accordion=true expand=true}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等价的属性写法（&lt;strong&gt;注意冒号后不能有空格&lt;/strong&gt;外的其它细节见下）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;- :+ 标题&lt;/code&gt; / &lt;code&gt;- :- 标题&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单条面板的初始状态：展开 / 折叠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;条目正文缩进 2 个空格&lt;/td&gt;
&lt;td&gt;面板内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning[本仓实测：容器名与花括号之间不能有空格]
凡是用花括号写选项的容器（&lt;code&gt;steps&lt;/code&gt;、&lt;code&gt;grid&lt;/code&gt;、&lt;code&gt;code-tree&lt;/code&gt;、&lt;code&gt;file-tree&lt;/code&gt;、&lt;code&gt;collapse&lt;/code&gt;），容器名与 &lt;code&gt;{&lt;/code&gt; 必须紧贴：&lt;code&gt;:::collapse{expand=true}&lt;/code&gt; 生效，写成 &lt;code&gt;::: collapse{expand=true}&lt;/code&gt; 会整块不解析。而关键字写法（&lt;code&gt;::: collapse accordion&lt;/code&gt;）与提示容器的 &lt;code&gt;::: tip[标题]&lt;/code&gt; 这类不使用花括号的形式可以带空格。详见&lt;a href=&quot;/posts/syntax/markdown-attributes/&quot;&gt;扩展与属性&lt;/a&gt;。
:::&lt;/p&gt;
</content:encoded></item><item><title>Markdown 基础语法</title><link>https://www.klucen.cn/posts/syntax/markdown-basic/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-basic/</guid><pubDate>Sun, 14 Dec 2025 04:30:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/basic/&quot;&gt;基础语法 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;段落与换行&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是第一行文本，行尾加两个空格&lt;br /&gt;
这是同一段落内的强制换行。&lt;/p&gt;
&lt;p&gt;这是新段落，与上一段之间保留了空行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（注意第一行行尾有 2 个空格）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这是第一行文本，行尾加两个空格  
这是同一段落内的强制换行。

这是新段落，与上一段之间保留了空行。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行尾也可以用反斜线 &lt;code&gt;\&lt;/code&gt; 代替两个空格实现强制换行。&lt;/p&gt;
&lt;h2&gt;标题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;这是三级标题&lt;/h3&gt;
&lt;h4&gt;这是四级标题&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各级标题会自动生成语义锚点并进入右侧目录。&lt;/p&gt;
&lt;h2&gt;引用块&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是第一层引用内容。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是嵌套的第二层引用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用内部支持列表与格式化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一项说明&lt;/li&gt;
&lt;li&gt;第二项说明&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;引用内的行内代码&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 这是第一层引用内容。
&amp;gt;
&amp;gt; &amp;gt; 这是嵌套的第二层引用。
&amp;gt;
&amp;gt; 引用内部支持列表与格式化：
&amp;gt; 1. 第一项说明
&amp;gt; 2. 第二项说明
&amp;gt;
&amp;gt; `引用内的行内代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列表&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无序列表项一&lt;/li&gt;
&lt;li&gt;无序列表项二
&lt;ul&gt;
&lt;li&gt;缩进两个空格形成子列表&lt;/li&gt;
&lt;li&gt;第二个子列表项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;第一步操作&lt;/li&gt;
&lt;li&gt;第二步操作&lt;/li&gt;
&lt;li&gt;第三步操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 无序列表项一
- 无序列表项二
  - 缩进两个空格形成子列表
  - 第二个子列表项

1. 第一步操作
2. 第二步操作
3. 第三步操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无序列表的标记符号可以用 &lt;code&gt;-&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;+&lt;/code&gt;，官方建议统一使用 &lt;code&gt;-&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;任务清单&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] 已完成的核心配置项&lt;/li&gt;
&lt;li&gt;[ ] 待编写的技术文档章节&lt;/li&gt;
&lt;li&gt;[ ] 待确认的排版细节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- [x] 已完成的核心配置项
- [ ] 待编写的技术文档章节
- [ ] 待确认的排版细节
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码块&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface SiteConfig {
  title: string;
  themeColor: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（外层四反引号仅为包裹，实际写三个反引号）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript
interface SiteConfig {
  title: string;
  themeColor: number;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除围栏写法外，正文缩进 4 个空格也会成为代码块。&lt;/p&gt;
&lt;h2&gt;水平分割线&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;段落上部内容&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;段落下部内容&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;段落上部内容

---

段落下部内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分割线用 3 个或更多 &lt;code&gt;-&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;_&lt;/code&gt; 都可以。&lt;/p&gt;
&lt;h2&gt;文本强调&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;、&lt;strong&gt;粗体文本&lt;/strong&gt;、&lt;em&gt;&lt;strong&gt;粗斜体文本&lt;/strong&gt;&lt;/em&gt;、&lt;s&gt;删除线文本&lt;/s&gt;、&lt;code&gt;行内代码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*斜体文本*
**粗体文本**
***粗斜体文本***
~~删除线文本~~
`行内代码`
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;链接&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;行内式：&lt;a href=&quot;https://shirone.mysqil.com/&quot;&gt;链接文字&lt;/a&gt;、&lt;a href=&quot;/archive/&quot;&gt;站内相对路径&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;参考式：详细规范请参考 &lt;a href=&quot;https://shirone.mysqil.com/&quot;&gt;官方文档&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/LyraVoid/Shirone&quot;&gt;仓库地址&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;自动链接：&lt;a href=&quot;https://shirone.mysqil.com&quot;&gt;https://shirone.mysqil.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;行内式：[链接文字](https://shirone.mysqil.com/ &quot;可选的悬停提示标题&quot;)

参考式：详细规范请参考 [官方文档][docs] 和 [仓库地址][repo]。

自动链接：&amp;lt;https://shirone.mysqil.com&amp;gt;

[docs]: https://shirone.mysqil.com/ &quot;文档站&quot;
[repo]: https://github.com/LyraVoid/Shirone &quot;GitHub&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图片&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pics.klucen.cn/GEM/GEM10.jpg&quot; alt=&quot;示例图片&quot; title=&quot;这是一张示例图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![图片替代文本](./cover.webp &quot;可选图题&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图片的宽度控制与图题排版在&lt;a href=&quot;/posts/syntax/markdown-image-grid/&quot;&gt;图片画廊与排版&lt;/a&gt;一篇里单独演示。&lt;/p&gt;
&lt;h2&gt;转义字符&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;*这不是斜体文本*&lt;/p&gt;
&lt;p&gt;[这不是链接]&lt;/p&gt;
&lt;p&gt;# 这不是标题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\*这不是斜体文本\*
\[这不是链接\]
\# 这不是标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可转义的保留符号包括：&lt;code&gt;\&lt;/code&gt;、&lt;code&gt;`&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;_&lt;/code&gt;、&lt;code&gt;{}&lt;/code&gt;、&lt;code&gt;[]&lt;/code&gt;、&lt;code&gt;()&lt;/code&gt;、&lt;code&gt;#&lt;/code&gt;、&lt;code&gt;+&lt;/code&gt;、&lt;code&gt;-&lt;/code&gt;、&lt;code&gt;.&lt;/code&gt;、&lt;code&gt;!&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;原生 HTML&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &amp;lt;kbd&amp;gt;Ctrl&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;C&amp;lt;/kbd&amp;gt; 复制内容。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center;&quot;&amp;gt;
居中显示的纯 HTML 说明文本
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;使用 &amp;lt;kbd&amp;gt;Ctrl&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;C&amp;lt;/kbd&amp;gt; 复制内容。

&amp;lt;div style=&quot;text-align: center;&quot;&amp;gt;
  居中显示的纯 HTML 说明文本
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编辑器快捷键&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;排版效果&lt;/th&gt;
&lt;th&gt;语法格式&lt;/th&gt;
&lt;th&gt;Windows / Linux&lt;/th&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;粗体&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**文本**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl + B&lt;/td&gt;
&lt;td&gt;Command + B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;斜体&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*文本*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl + I&lt;/td&gt;
&lt;td&gt;Command + I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行内代码&lt;/td&gt;
&lt;td&gt;&lt;code&gt;`代码`&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl + Shift + `&lt;/td&gt;
&lt;td&gt;Command + Shift + `&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插入链接&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[文本](链接)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl + K&lt;/td&gt;
&lt;td&gt;Command + K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插入图片&lt;/td&gt;
&lt;td&gt;&lt;code&gt;![说明](路径)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl + Shift + I&lt;/td&gt;
&lt;td&gt;Command + Shift + I&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;写作注意&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;段落之间需一个或更多空行分隔，标记符号与文本之间必须保留空格；&lt;/li&gt;
&lt;li&gt;官方建议统一使用 &lt;code&gt;-&lt;/code&gt; 与 Atx 井号标题语法；&lt;/li&gt;
&lt;li&gt;转义只对保留符号加 &lt;code&gt;\&lt;/code&gt; 生效。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Markdown 扩展与属性</title><link>https://www.klucen.cn/posts/syntax/markdown-attributes/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-attributes/</guid><pubDate>Sun, 14 Dec 2025 04:29:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/attributes/&quot;&gt;扩展与属性 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;属性字典（&lt;code&gt;{}&lt;/code&gt; 大括号）在 Shirone 里有三种落点：&lt;strong&gt;指令/容器选项&lt;/strong&gt;、&lt;strong&gt;代码围栏元信息&lt;/strong&gt;、&lt;strong&gt;马克笔语义后缀&lt;/strong&gt;。下面先演示三种都生效的写法，最后单独说明本仓实测&lt;strong&gt;未生效&lt;/strong&gt;的那一类。&lt;/p&gt;
&lt;h2&gt;指令与容器选项&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：容器/指令把自己的选项写在紧跟名字的 &lt;code&gt;{}&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;:::steps{title=&quot;发布一篇草稿的三步&quot; start=1}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写完正文&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;draft&lt;/code&gt; 置为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;本地预览&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;确认排版与图片无误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提交推送&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;内容仓推送后自动触发站点重建。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::grid{columns=&quot;3&quot; aspect=&quot;16/9&quot; fit=&quot;cover&quot;}
&lt;img src=&quot;https://pics.klucen.cn/GEM/GEM10.jpg&quot; alt=&quot;甲&quot; title=&quot;图一&quot; /&gt;
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc1.jpg&quot; alt=&quot;乙&quot; title=&quot;图二&quot; /&gt;
&lt;img src=&quot;https://pics.klucen.cn/cuc/cuc2.jpg&quot; alt=&quot;丙&quot; title=&quot;图三&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::steps{title=&quot;发布一篇草稿的三步&quot; start=1}
1. **写完正文**

   把 `draft` 置为 `false`。
2. **本地预览**

   确认排版与图片无误。
3. **提交推送**

   内容仓推送后自动触发站点重建。
:::

:::grid{columns=&quot;3&quot; aspect=&quot;16/9&quot; fit=&quot;cover&quot;}
![甲](https://pics.klucen.cn/GEM/GEM10.jpg &quot;图一&quot;)
![乙](https://pics.klucen.cn/cuc/cuc1.jpg &quot;图二&quot;)
![丙](https://pics.klucen.cn/cuc/cuc2.jpg &quot;图三&quot;)
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[本仓实测：容器名与花括号之间不能有空格]
&lt;code&gt;:::grid{...}&lt;/code&gt; 生效，写成 &lt;code&gt;::: grid{...}&lt;/code&gt;（中间多一个空格）就整块&lt;strong&gt;不解析&lt;/strong&gt;、原样显示为文本。&lt;code&gt;steps&lt;/code&gt;、&lt;code&gt;code-tree&lt;/code&gt;、&lt;code&gt;file-tree&lt;/code&gt; 同理；只有 &lt;code&gt;::: tabs#id&lt;/code&gt;、&lt;code&gt;:::: field-group&lt;/code&gt;、&lt;code&gt;::: field 名称&lt;/code&gt;、提示容器这几类本身不以花括号结尾的写法才带空格。
:::&lt;/p&gt;
&lt;p&gt;其它容器选项示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;LyraVoid/Shirone&quot;}

:::file-tree{title=&quot;目录结构&quot; icon=&quot;simple&quot;}

:::code-tree{title=&quot;组件封装&quot; height=&quot;380px&quot; entry=&quot;Button.svelte&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码围栏元信息&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：写在围栏首行、语言名之后，用来控制标题、行高亮、增删标记、折叠与行号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
  themeColor: 345,
  language: &quot;zh_CN&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript {2,4} title=&quot;src/config.ts&quot; ins={4} showLineNumbers
export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
  themeColor: 345,
  language: &quot;zh_CN&quot;,
};
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各参数的完整用法见代码块系列：&lt;a href=&quot;/posts/syntax/codeblock-highlight/&quot;&gt;语法高亮与行号&lt;/a&gt;、&lt;a href=&quot;/posts/syntax/codeblock-titles/&quot;&gt;标题与文件名&lt;/a&gt;、&lt;a href=&quot;/posts/syntax/codeblock-diff/&quot;&gt;高亮与差异标记&lt;/a&gt;、&lt;a href=&quot;/posts/syntax/codeblock-collapse/&quot;&gt;代码折叠与复制&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;马克笔语义后缀&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：&lt;code&gt;==内容==&lt;/code&gt; 后面跟 &lt;code&gt;{.取值}&lt;/code&gt;，切换高亮颜色的语义角色。&lt;/p&gt;
&lt;p&gt;==主题主色高亮==、==次级强调状态=={.secondary}、==警告与需要修正的条件=={.error}、==建议与辅助提示=={.tip}、==三级调色板强调=={.tertiary}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;==主题主色高亮==

==次级强调状态=={.secondary}

==警告与需要修正的条件=={.error}

==建议与辅助提示=={.tip}

==三级调色板强调=={.tertiary}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可选取值：&lt;code&gt;primary&lt;/code&gt;（默认）、&lt;code&gt;secondary&lt;/code&gt;、&lt;code&gt;tertiary&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt;、&lt;code&gt;tip&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;原生 HTML 兜底&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：Markdown 里可以直接嵌 HTML，&lt;code&gt;class&lt;/code&gt;、&lt;code&gt;id&lt;/code&gt;、&lt;code&gt;style&lt;/code&gt; 都原样保留。&lt;/p&gt;
&lt;p&gt;&amp;lt;span class=&quot;probe-demo&quot;&amp;gt;带自定义 class 的 span&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;text-align: center; opacity: .75;&quot;&amp;gt;居中的一段 HTML 文本&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span class=&quot;probe-demo&quot;&amp;gt;带自定义 class 的 span&amp;lt;/span&amp;gt;

&amp;lt;div style=&quot;text-align: center; opacity: .75;&quot;&amp;gt;居中的一段 HTML 文本&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;本仓实测：通用元素属性不生效&lt;/h2&gt;
&lt;p&gt;官方文档还描述了「给任意 Markdown 元素附加属性字典」的写法。在&lt;strong&gt;本站当前主题版本&lt;/strong&gt;上逐条实测，这一类&lt;strong&gt;都不会生效&lt;/strong&gt;，属性会原样留在页面上：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文档写法&lt;/th&gt;
&lt;th&gt;本仓实测结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;## 标题 {#custom-id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;锚点未生效，&lt;code&gt;{#custom-id}&lt;/code&gt; 显示为标题的一部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[链接](https://example.com){target=&quot;_blank&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;链接正常，&lt;code&gt;{target=&quot;_blank&quot;}&lt;/code&gt; 显示为正文文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`代码`{.custom-badge}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{.custom-badge}&lt;/code&gt; 显示为正文文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;- 列表项 {.some-class}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{.some-class}&lt;/code&gt; 显示为正文文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:::tip[标题]{class=&quot;custom-box&quot;}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;容器正常渲染，但 &lt;code&gt;class&lt;/code&gt; 被丢弃（不报错）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实测片段（下面这段在页面上会看到属性文字残留）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 标题甲 {#probe-h-space}

[链接甲](https://example.com){.probe-link-class}

`代码甲`{.probe-code-class}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要给元素加 &lt;code&gt;class&lt;/code&gt;/&lt;code&gt;id&lt;/code&gt; 时，本站可用的替代做法是直接写原生 HTML（见上一节），或改用主题自带的容器与围栏选项。&lt;/p&gt;
</content:encoded></item><item><title>Markdown 内容注释</title><link>https://www.klucen.cn/posts/syntax/markdown-annotations/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-annotations/</guid><pubDate>Sun, 14 Dec 2025 04:28:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/annotations/&quot;&gt;内容注释 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内容注释比传统脚注更轻：正文里放一个标记，说明文字在&lt;strong&gt;宽屏&lt;/strong&gt;显示为右侧边缘的旁注卡片，&lt;strong&gt;窄屏/触屏&lt;/strong&gt;轻触标记唤出浮层。&lt;/p&gt;
&lt;h2&gt;基础用法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：Astro 采用孤岛架构 [+islands] 来保持页面的极速加载。&lt;/p&gt;
&lt;p&gt;[+islands]:
孤岛架构指由静态 HTML 所包裹的独立交互组件区域，只有交互区域才加载 JavaScript。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（标记是 &lt;code&gt;[+标签]&lt;/code&gt;，定义是 &lt;code&gt;[+标签]:&lt;/code&gt; 换行后缩进 2 个空格）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Astro 采用孤岛架构 [+islands] 来保持页面的极速加载。

[+islands]:
  孤岛架构指由静态 HTML 所包裹的独立交互组件区域，只有交互区域才加载 JavaScript。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一段里放多个标记&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：本站内容分离成两个仓 &lt;a href=&quot;%E4%BB%A3%E7%A0%81%E4%BB%93%E6%94%BE%E4%B8%BB%E9%A2%98%E4%B8%8E%E8%87%AA%E7%A0%94%E5%8A%9F%E8%83%BD%EF%BC%8C%E5%86%85%E5%AE%B9%E4%BB%93%E6%94%BE%E6%96%87%E7%AB%A0%E3%80%81%E5%8A%A8%E6%80%81%E4%B8%8E%E4%B8%AA%E4%BA%BA%E9%85%8D%E7%BD%AE%EF%BC%8C%E4%B8%A4%E8%80%85%E4%BA%92%E4%B8%8D%E6%B1%A1%E6%9F%93%E3%80%82&quot;&gt;+two-repos&lt;/a&gt;，构建与部署交给托管平台 &lt;a href=&quot;%E5%86%85%E5%AE%B9%E4%BB%93%E6%8E%A8%E9%80%81%E5%90%8E%E7%94%B1%E5%B9%B3%E5%8F%B0%E4%BE%A7%E8%87%AA%E5%8A%A8%E9%87%8D%E5%BB%BA%E7%AB%99%E7%82%B9%EF%BC%8C%E4%B8%8D%E9%9C%80%E8%A6%81%E6%89%8B%E5%8A%A8%E8%B7%91%E6%9E%84%E5%BB%BA%E5%91%BD%E4%BB%A4%E3%80%82&quot;&gt;+deploy&lt;/a&gt;，评论则放在另一个公开仓的 Discussions 里 [+comments]。&lt;/p&gt;
&lt;p&gt;[+comments]:
评论数据以 GitHub Discussions 的形式保存，不占用仓库文件树。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;本站内容分离成两个仓 [+two-repos]，构建与部署交给托管平台 [+deploy]。

[+two-repos]:
  代码仓放主题与自研功能，内容仓放文章、动态与个人配置，两者互不污染。

[+deploy]:
  内容仓推送后由平台侧自动重建站点，不需要手动跑构建命令。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写法要点&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项&lt;/th&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;行内标记&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[+标签]&lt;/code&gt;，1 个加号、方括号紧贴、无空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定义块&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[+标签]:&lt;/code&gt; 冒号后&lt;strong&gt;不写内容&lt;/strong&gt;、直接换行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定义正文&lt;/td&gt;
&lt;td&gt;下一行起，&lt;strong&gt;缩进 2 个空格&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;同名定义&lt;/td&gt;
&lt;td&gt;同一标签可以累积多条定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生效范围&lt;/td&gt;
&lt;td&gt;仅当前文章&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;边界行为（本仓实测）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;未定义的引用&lt;/strong&gt;会原样保留：正文里写 [+undefined-tag] 只会显示为普通文本，不会生成标记；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义但没人引用&lt;/strong&gt;的注释不会输出到正文，定义块本身也不渲染；&lt;/li&gt;
&lt;li&gt;标记与链接、代码、强调等行内语法相邻组合都可以；&lt;/li&gt;
&lt;li&gt;注释内容是服务端输出的静态 HTML，对爬虫与无障碍访问都友好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查看方式：宽屏下把页面拉宽能看到右侧旁注，缩窄窗口后改为轻触标记弹出浮层——两种布局都实现过，可以拖动浏览器宽度对比。&lt;/p&gt;
</content:encoded></item><item><title>Markdown 提示容器</title><link>https://www.klucen.cn/posts/syntax/markdown-admonitions/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-admonitions/</guid><pubDate>Sun, 14 Dec 2025 04:27:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/admonitions/&quot;&gt;提示容器 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提示容器遵循 Material 3 Expressive 的容器表面分层设计，共 7 种类型，可以带自定义标题，也可以做成可折叠面板。&lt;/p&gt;
&lt;h2&gt;七种类型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::tip
技巧与建议：强调色容器 + 灯泡图标。
:::&lt;/p&gt;
&lt;p&gt;:::info
补充信息：主色调表面 + 信息图标。
:::&lt;/p&gt;
&lt;p&gt;:::warning
注意与预警：警示色调 + 三角感叹号。
:::&lt;/p&gt;
&lt;p&gt;:::caution
危险操作：错误色调 + 高对比警示框。
:::&lt;/p&gt;
&lt;p&gt;:::important
关键重点：鲜亮强调容器。
:::&lt;/p&gt;
&lt;p&gt;:::note
普通笔记：中性表面容器。
:::&lt;/p&gt;
&lt;p&gt;:::details
折叠面板：带箭头的原生 details，点击标题展开。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（每种类型用 3 个冒号包裹，开闭成对）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::tip
技巧与建议：强调色容器 + 灯泡图标。
:::

:::info
补充信息：主色调表面 + 信息图标。
:::

:::warning
注意与预警：警示色调 + 三角感叹号。
:::

:::caution
危险操作：错误色调 + 高对比警示框。
:::

:::important
关键重点：鲜亮强调容器。
:::

:::note
普通笔记：中性表面容器。
:::

:::details
折叠面板：带箭头的原生 details，点击标题展开。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;th&gt;视觉风格&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;技巧与建议&lt;/td&gt;
&lt;td&gt;强调色彩容器与灯泡图标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;补充信息&lt;/td&gt;
&lt;td&gt;主色调表面与信息图标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warning&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注意与预警&lt;/td&gt;
&lt;td&gt;警示色调与三角感叹号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;caution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;危险操作&lt;/td&gt;
&lt;td&gt;错误色调与高对比警示框&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;important&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关键重点&lt;/td&gt;
&lt;td&gt;鲜亮强调容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;note&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;普通笔记&lt;/td&gt;
&lt;td&gt;中性表面容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;折叠面板&lt;/td&gt;
&lt;td&gt;带箭头的原生 details 折叠组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;两种自定义标题写法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::tip[方括号标题]
标题写在方括号里。
:::&lt;/p&gt;
&lt;p&gt;:::warning 空格标题
标题直接跟在类型名后面，不用方括号。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::tip[方括号标题]
标题写在方括号里。
:::

:::warning 空格标题
标题直接跟在类型名后面，不用方括号。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;容器内容可以是完整 Markdown&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::details 构建日志摘要&lt;/p&gt;
&lt;p&gt;构建过程的关键输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步内容仓：完成&lt;/li&gt;
&lt;li&gt;生成静态 HTML：完成&lt;/li&gt;
&lt;li&gt;建立搜索索引：完成&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::details 构建日志摘要

构建过程的关键输出：

- 同步内容仓：完成
- 生成静态 HTML：完成
- 建立搜索索引：完成

```bash
pnpm build
```

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;说明与边界&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;details&lt;/code&gt; 用原生 &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; 实现，不需要 hydration，键盘可操作；&lt;/li&gt;
&lt;li&gt;容器内可以嵌列表、代码块、表格、甚至其它容器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本仓实测&lt;/strong&gt;：容器标题支持行内 Markdown——下面这个标题里的代码、粗体与链接都是真渲染出来的：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[标题里的 &lt;code&gt;行内代码&lt;/code&gt;、&lt;strong&gt;粗体&lt;/strong&gt;与 &lt;a href=&quot;https://docs.shirone.mysqil.com/&quot;&gt;链接&lt;/a&gt;]
标题本身也能排版。
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本仓实测&lt;/strong&gt;：在提示容器后面追加属性字典（如 &lt;code&gt;:::tip[标题]{class=&quot;x&quot;}&lt;/code&gt;）不会报错，但 &lt;code&gt;class&lt;/code&gt; 会被丢弃、不会应用到元素上，详见&lt;a href=&quot;/posts/syntax/markdown-attributes/&quot;&gt;扩展与属性&lt;/a&gt;一篇。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Markdown 缩写词悬浮</title><link>https://www.klucen.cn/posts/syntax/markdown-abbreviations/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/markdown-abbreviations/</guid><pubDate>Sun, 14 Dec 2025 04:26:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/markdown/abbreviations/&quot;&gt;缩写词悬浮 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先在本页声明一份术语表（定义行本身不会输出到正文），正文里出现的词条会自动变成带原生气泡提示的缩写词。&lt;/p&gt;
&lt;p&gt;*[SSR]: 服务端渲染技术
*[LCP]: 最大内容绘制时间指标
*[AST]: 抽象语法树
*[CDN]: 内容分发网络&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：把鼠标移到下面这些词上（移动端轻触）——采用 SSR 可以大幅优化站点的 LCP 表现，并在 AST 解析阶段完成静态转换；静态资源再交给 CDN 分发。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*[SSR]: 服务端渲染技术
*[LCP]: 最大内容绘制时间指标
*[AST]: 抽象语法树
*[CDN]: 内容分发网络

采用 SSR 可以大幅优化站点的 LCP 表现，并在 AST 解析阶段完成静态转换。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写法要点&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项&lt;/th&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;定义行格式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*[缩写]: 释义&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;符号细节&lt;/td&gt;
&lt;td&gt;1 个星号、方括号紧贴、方括号后跟 &lt;strong&gt;1 个半角冒号&lt;/strong&gt;、冒号后 1 个空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;声明位置&lt;/td&gt;
&lt;td&gt;任意位置都可以，官方建议放在文末或章节开头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生效范围&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;仅当前文章&lt;/strong&gt;，同一篇文章里所有匹配到的词条都会自动转换&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;边界行为（本仓实测）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;实测结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*[EMPTY]:&lt;/code&gt;（释义为空）&lt;/td&gt;
&lt;td&gt;不生效，定义行原样保留为文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*[X]:释义&lt;/code&gt;（冒号后缺空格）&lt;/td&gt;
&lt;td&gt;不生效，定义行原样保留为文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*[DUP]: 第一条&lt;/code&gt; + &lt;code&gt;*[DUP]: 第二条&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;以第一条为准&lt;/strong&gt;，第二条原样保留为文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定义了但正文没引用&lt;/td&gt;
&lt;td&gt;定义行不渲染，正文无变化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实测片段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*[EMPTY]:
*[X]:释义前无空格
*[OK]: 正常释义

正文里出现 EMPTY、X、OK 三个词。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段里只有 &lt;code&gt;OK&lt;/code&gt; 会变成缩写词，&lt;code&gt;EMPTY&lt;/code&gt; 与 &lt;code&gt;X&lt;/code&gt; 既不变换、那两行定义也会原样显示出来。&lt;/p&gt;
</content:encoded></item><item><title>代码块标题与文件名</title><link>https://www.klucen.cn/posts/syntax/codeblock-titles/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/codeblock-titles/</guid><pubDate>Sun, 14 Dec 2025 04:25:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/codeblock/titles/&quot;&gt;标题与文件名 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给代码块加 &lt;code&gt;title=&quot;...&quot;&lt;/code&gt; 之后，块顶部会生成一条窗口栏：左侧是操作系统风格的窗口控制按钮 + 按扩展名识别的文件图标，右侧是复制按钮与折叠入口。&lt;/p&gt;
&lt;h2&gt;文件名作为标题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（左上角图标随扩展名变化）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;shirone&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```json title=&quot;package.json&quot;
{
  &quot;name&quot;: &quot;shirone&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提示性标题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;title&lt;/code&gt; 不一定是文件名，也可以是一句操作说明：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;（&lt;code&gt;title=&lt;/code&gt; 必须写在围栏首行、语言名之后）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash title=&quot;终端操作命令&quot;
pnpm install
pnpm dev
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不同扩展名的图标&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
  lang: &quot;zh_CN&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.m3-btn
  background: var(--primary)
  border-radius: var(--shape-corner-m)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;enable: true
provider: giscus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript title=&quot;src/config/siteConfig.ts&quot;
export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
  lang: &quot;zh_CN&quot;,
};
```

```stylus title=&quot;src/styles/button.styl&quot;
.m3-btn
  background: var(--primary)
  border-radius: var(--shape-corner-m)
```

```yaml title=&quot;config/comment.yaml&quot;
enable: true
provider: giscus
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;顶栏标题；常见两种用法：文件名、操作说明&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt; / &lt;code&gt;code&lt;/code&gt; / &lt;code&gt;terminal&lt;/code&gt; / &lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;窗口栏样式；默认 &lt;code&gt;auto&lt;/code&gt;（按语言推断）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[本仓实测]
&lt;code&gt;title=&lt;/code&gt; 生效，顶栏的复制按钮与折叠入口都在。&lt;code&gt;title&lt;/code&gt; 的值要用引号包起来写成 &lt;code&gt;title=&quot;...&quot;&lt;/code&gt;，不建议省引号。
:::&lt;/p&gt;
</content:encoded></item><item><title>代码块选项组</title><link>https://www.klucen.cn/posts/syntax/codeblock-tabs/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/codeblock-tabs/</guid><pubDate>Sun, 14 Dec 2025 04:24:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/codeblock/tabs/&quot;&gt;代码选项组 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选项组与 &lt;a href=&quot;/posts/syntax/markdown-tabs/&quot;&gt;Markdown 代码选项组&lt;/a&gt; 是同一个容器，只是用在代码块场景：每个标签里放一段代码，读者点一次就能看不同包管理器 / 语言 / 系统的命令。&lt;/p&gt;
&lt;h2&gt;包管理器命令&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;::: tabs#codeblock-pm&lt;/p&gt;
&lt;p&gt;@tab pnpm#cb-pnpm&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab npm#cb-npm&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install
npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Bun#cb-bun&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bun install
bun run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: tabs#codeblock-pm

@tab pnpm#cb-pnpm

```bash
pnpm install
pnpm build
```

@tab npm#cb-npm

```bash
npm install
npm run build
```

@tab Bun#cb-bun

```bash
bun install
bun run build
```

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多语言实现对照&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（同一功能的不同语言写法并排对比）&lt;/p&gt;
&lt;p&gt;::: tabs#codeblock-lang&lt;/p&gt;
&lt;p&gt;@tab TypeScript#cb-ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function greet(name: string): string {
  return `Hello, ${name}!`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Python#cb-py&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def greet(name: str) -&amp;gt; str:
    return f&quot;Hello, {name}!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Rust#cb-rs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn greet(name: &amp;amp;str) -&amp;gt; String {
    format!(&quot;Hello, {}!&quot;, name)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: tabs#codeblock-lang

@tab TypeScript#cb-ts

```typescript title=&quot;greet.ts&quot;
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
```

@tab Python#cb-py

```python title=&quot;greet.py&quot;
def greet(name: str) -&amp;gt; str:
    return f&quot;Hello, {name}!&quot;
```

@tab Rust#cb-rs

```rust title=&quot;greet.rs&quot;
fn greet(name: &amp;amp;str) -&amp;gt; String {
    format!(&quot;Hello, {}!&quot;, name)
}
```

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不同系统的操作步骤&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（每个标签内可以放多段内容，不只是单个代码块）&lt;/p&gt;
&lt;p&gt;::: tabs#codeblock-os&lt;/p&gt;
&lt;p&gt;@tab Windows#cb-win&lt;/p&gt;
&lt;p&gt;在 PowerShell 中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$env:CONTENT_DIR = &quot;C:/Users/KLEE/Desktop/Cline/shirone-content&quot;
pnpm content:sync
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab macOS / Linux#cb-unix&lt;/p&gt;
&lt;p&gt;在终端中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export CONTENT_DIR=&quot;$HOME/Cline/shirone-content&quot;
pnpm content:sync
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::: tabs#codeblock-os

@tab Windows#cb-win

在 PowerShell 中执行：

```powershell
$env:CONTENT_DIR = &quot;C:/Users/KLEE/Desktop/Cline/shirone-content&quot;
pnpm content:sync
pnpm dev
```

@tab macOS / Linux#cb-unix

在终端中执行：

```bash
export CONTENT_DIR=&quot;$HOME/Cline/shirone-content&quot;
pnpm content:sync
pnpm dev
```

:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数与联动&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;::: tabs#容器 id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;容器起始，&lt;code&gt;#&lt;/code&gt; 后是容器自己的 id（可省略）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@tab 显示名#选项值&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;子标签；&lt;code&gt;#&lt;/code&gt; 后的&lt;strong&gt;选项值&lt;/strong&gt;决定跨组联动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@tab:active 显示名#值&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;指定初始选中的标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:::&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;容器结束&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[本仓实测]
同一页内不同容器只要&lt;strong&gt;选项值&lt;/strong&gt;相同（例如都用 &lt;code&gt;#cb-pnpm&lt;/code&gt;），切换其中一个，另一个会跟着切换——写系列文章时可以让「安装命令」这张卡片跟着读者的选择走。上面前两个示例刻意用了互不相同的选项值，所以它们各自独立。
:::&lt;/p&gt;
</content:encoded></item><item><title>代码块语法高亮与行号</title><link>https://www.klucen.cn/posts/syntax/codeblock-highlight/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/codeblock-highlight/</guid><pubDate>Sun, 14 Dec 2025 04:23:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/codeblock/highlight/&quot;&gt;语法高亮与行号 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码块由 Expressive Code 在构建期完成高亮与标记，客户端只负责复制、折叠等交互。&lt;/p&gt;
&lt;h2&gt;按语言高亮&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：围栏后面写语言标识符即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface UserProfile {
  name: string;
  avatar: string;
  themeColor: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript
interface UserProfile {
  name: string;
  avatar: string;
  themeColor: number;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换一种语言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    println!(&quot;Hello, Shirone!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;显示行号&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：在语言名后面加上无值开关 &lt;code&gt;showLineNumbers&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    println!(&quot;Hello, Shirone!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```rust showLineNumbers
fn main() {
    println!(&quot;Hello, Shirone!&quot;);
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行号也可以从别的数字起算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript showLineNumbers startLineNumber=10
export const siteConfig = {
  title: &quot;KLucen 的启事录&quot;,
};
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;相关参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;取值&lt;/th&gt;
&lt;th&gt;默认&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语言标识符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;typescript&lt;/code&gt;、&lt;code&gt;rust&lt;/code&gt;、&lt;code&gt;bash&lt;/code&gt;、&lt;code&gt;json&lt;/code&gt;…&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;决定着色规则&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;showLineNumbers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无值开关&lt;/td&gt;
&lt;td&gt;关&lt;/td&gt;
&lt;td&gt;强制显示行号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startLineNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;行号起始值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wrap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主题配置（本站为 &lt;code&gt;true&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;超长行是否折行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preserveIndent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否保留原始缩进&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;特点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;零客户端运行时&lt;/strong&gt;：高亮与行号在服务端静态预渲染，不会出现样式闪烁；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自适应明暗配色&lt;/strong&gt;：语法前景色与背景对比度依据 Material 3 调色板与暗色模式自动调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[本仓实测]
&lt;code&gt;showLineNumbers&lt;/code&gt; 与 &lt;code&gt;startLineNumber&lt;/code&gt; 都生效。若只想要「高亮 + 行号」而不要标题栏，不要把 &lt;code&gt;title=&lt;/code&gt; 写上去即可；标题栏与差异标记见下面两篇。
:::&lt;/p&gt;
</content:encoded></item><item><title>代码块高亮与差异标记</title><link>https://www.klucen.cn/posts/syntax/codeblock-diff/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/codeblock-diff/</guid><pubDate>Sun, 14 Dec 2025 04:22:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/codeblock/diff/&quot;&gt;高亮与差异标记 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三种标记都写在围栏首行：花括号里的行号 = 高亮，&lt;code&gt;ins={...}&lt;/code&gt; = 新增（绿底），&lt;code&gt;del={...}&lt;/code&gt; = 删除（红底）。&lt;/p&gt;
&lt;h2&gt;行高亮&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（第 2 行与第 4–5 行被点亮）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function setup() {
  const isEnabled = true; // 第 2 行高亮
  const count = 0;
  console.log(&quot;初始化完成&quot;); // 第 4-5 行高亮
  return count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript {2,4-5} title=&quot;src/index.ts&quot;
function setup() {
  const isEnabled = true; // 第 2 行高亮
  const count = 0;
  console.log(&quot;初始化完成&quot;); // 第 4-5 行高亮
  return count;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行号范围的写法是 &lt;code&gt;{1,3-5}&lt;/code&gt;：逗号分隔单个行号，连字符表示区间。&lt;/p&gt;
&lt;h2&gt;新增与删除标记&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（被 &lt;code&gt;del&lt;/code&gt; 标记的行变红、&lt;code&gt;ins&lt;/code&gt; 标记的行变绿）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const siteConfig = {
  themeColor: 210, // 旧配置
  themeColor: 315, // 新配置
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript ins={3} del={2} title=&quot;更新配置&quot;
export const siteConfig = {
  themeColor: 210, // 旧配置
  themeColor: 315, // 新配置
};
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原生 diff 代码块&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：语言写成 &lt;code&gt;diff&lt;/code&gt;，行首用 &lt;code&gt;+&lt;/code&gt; / &lt;code&gt;-&lt;/code&gt;，得到纯文本 diff 的配色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- const oldVersion = &quot;1.0.0&quot;;
+ const newVersion = &quot;2.0.0&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```diff title=&quot;git diff&quot;
- const oldVersion = &quot;1.0.0&quot;;
+ const newVersion = &quot;2.0.0&quot;;
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;视觉&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{行号,范围}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;高亮指定行&lt;/td&gt;
&lt;td&gt;强调底色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ins={...}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标记新增行&lt;/td&gt;
&lt;td&gt;绿色背景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;del={...}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标记删除行&lt;/td&gt;
&lt;td&gt;红色背景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title=&quot;...&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;顶部标题栏&lt;/td&gt;
&lt;td&gt;可与以上任意标记组合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语言 &lt;code&gt;diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;原生 diff 展示&lt;/td&gt;
&lt;td&gt;按行首 &lt;code&gt;+&lt;/code&gt;/&lt;code&gt;-&lt;/code&gt; 着色&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[本仓实测]
三种标记以及它们与 &lt;code&gt;title=&lt;/code&gt; 的组合都生效。行号区间用 &lt;code&gt;{4-8}&lt;/code&gt; 这种「起始-结束」写法，多个区间用逗号分隔。
:::&lt;/p&gt;
</content:encoded></item><item><title>代码块折叠与复制</title><link>https://www.klucen.cn/posts/syntax/codeblock-collapse/</link><guid isPermaLink="true">https://www.klucen.cn/posts/syntax/codeblock-collapse/</guid><pubDate>Sun, 14 Dec 2025 04:21:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info[版权与转载声明]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版权归属&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Dawn6666666&quot;&gt;Dawn6666666&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原文链接&lt;/strong&gt;：&lt;a href=&quot;https://docs.shirone.mysqil.com/guide/writing/codeblock/collapse/&quot;&gt;代码折叠与复制 | Shirone Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;许可协议&lt;/strong&gt;：&lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;署名 4.0 国际 (CC BY 4.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本文由博主&lt;strong&gt;转载整理&lt;/strong&gt;，讲解与示例取自 Shirone 官方文档，版权归原作者所有；「效果」为本站主题的实际渲染结果。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;默认折叠&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：&lt;code&gt;collapse={起始行-结束行}&lt;/code&gt; 指定的范围在初次加载时收起，显示为一个「展开 N 行」的按钮，点击后平滑展开；范围之外的行照常显示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineUserConfig } from &apos;astro/config&apos;;

export default defineUserConfig({
  // 折叠部分开始
  site: &apos;https://shirone.mysqil.com&apos;,
  base: &apos;/&apos;,
  trailingSlash: &apos;always&apos;,
  output: &apos;static&apos;,
  // 折叠部分结束
  integrations: [],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```typescript collapse={4-8} title=&quot;长配置文件示例&quot;
import { defineUserConfig } from &apos;astro/config&apos;;

export default defineUserConfig({
  // 折叠部分开始
  site: &apos;https://shirone.mysqil.com&apos;,
  base: &apos;/&apos;,
  trailingSlash: &apos;always&apos;,
  output: &apos;static&apos;,
  // 折叠部分结束
  integrations: [],
});
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一键复制&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;：所有代码块的右上角都有复制按钮，鼠标移上去就能看到；点击后代码原文写入系统剪贴板，并给出微动效反馈。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash title=&quot;复制我试试&quot;
pnpm build
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制按钮是内置行为，不需要任何参数或标记。&lt;/p&gt;
&lt;h2&gt;折叠与其它标记组合&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;（同时使用折叠、行高亮与标题）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;shirone&quot;,
  &quot;dependencies&quot;: {
    &quot;astro&quot;: &quot;^7.0.0&quot;,
    &quot;svelte&quot;: &quot;^5.0.0&quot;,
    &quot;tailwindcss&quot;: &quot;^4.0.0&quot;
  },
  &quot;private&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```json collapse={3-6} {2} title=&quot;package.json&quot; showLineNumbers
{
  &quot;name&quot;: &quot;shirone&quot;,
  &quot;dependencies&quot;: {
    &quot;astro&quot;: &quot;^7.0.0&quot;,
    &quot;svelte&quot;: &quot;^5.0.0&quot;,
    &quot;tailwindcss&quot;: &quot;^4.0.0&quot;
  },
  &quot;private&quot;: true
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数与行为&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;collapse={起始-结束}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;指定默认折叠的行范围，可与行号、&lt;code&gt;title=&lt;/code&gt; 等组合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一键复制&lt;/td&gt;
&lt;td&gt;无参数，所有代码块自带&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复制内容&lt;/td&gt;
&lt;td&gt;只包含&lt;strong&gt;纯净代码&lt;/strong&gt;，不含行号与标记字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[本仓实测]
折叠、复制、以及与 &lt;code&gt;title=&lt;/code&gt;／行高亮／&lt;code&gt;showLineNumbers&lt;/code&gt; 的组合都正常。展开/收起状态由站点脚本管理，站内无刷新切页后依然可用。
:::&lt;/p&gt;
</content:encoded></item><item><title>公开课 教程全文（4 篇合并）</title><link>https://www.klucen.cn/posts/python/open-class-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/open-class-full/</guid><pubDate>Sun, 14 Dec 2025 00:20:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 4 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：好玩的 Python 与算法入门
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#01-%E5%A5%BD%E7%8E%A9%E7%9A%84python&quot;&gt;01 好玩的Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#02-%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%97%E8%AF%BE%E7%A8%8B1---%E5%91%A8%E8%80%8C%E5%A4%8D%E5%A7%8B&quot;&gt;02 算法入门系列课程1 - 周而复始&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#03-%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%972---%E5%9C%A8%E6%B0%B4%E4%B8%80%E6%96%B9&quot;&gt;03 算法入门系列2 - 在水一方&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#04-%E5%B9%B4%E8%96%AA50w%E7%9A%84python%E7%A8%8B%E5%BA%8F%E5%91%98%E5%A6%82%E4%BD%95%E5%86%99%E4%BB%A3%E7%A0%81&quot;&gt;04 年薪50W+的Python程序员如何写代码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;01 好玩的Python&lt;/h2&gt;
&lt;p&gt;因为下面的代码都非常简单，简单到直接使用Python的交互式环境就能完成。当然，官方Python自带的交互式环境比较难用，推荐大家使用ipython，可以使用下面的命令来安装ipython，安装成功后键入ipython命令就能进入交互式环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install ipython
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install ipython
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ipython最直观的优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以用?或者??来获取帮助。&lt;/li&gt;
&lt;li&gt;可以用!调用系统命令。&lt;/li&gt;
&lt;li&gt;可以使用Tab键自动补全。&lt;/li&gt;
&lt;li&gt;可以使用魔法指令，如：%timeit。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;没有工具用代码也能P图&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装pillow三方库。&lt;/p&gt;
&lt;p&gt;PIL（Python Imaging Library）是Python平台事实上的图像处理标准库了。PIL功能非常强大，而API却非常简单易用。但是PIL仅支持到Python 2.7，而且很多年都没有人维护了，于是一群志愿者在PIL的基础上创建了兼容的版本，名字叫&lt;a href=&quot;https://github.com/python-pillow/Pillow&quot;&gt;Pillow&lt;/a&gt;，除了支持Python 3.x还加入了很多有用且有趣的新特性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pillow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install pillow
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加载图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image

chiling = Image.open(&apos;chiling.jpg&apos;)
chiling.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用滤镜。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import ImageFilter

chiling.filter(ImageFilter.EMBOSS).show()
chiling.filter(ImageFilter.CONTOUR).show()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;图像剪裁和粘贴。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rect = 220, 690, 265, 740 
watch = chiling.crop(rect)
watch.show()
blured_watch = watch.filter(ImageFilter.GaussianBlur(4))
chiling.paste(blured_watch, (220, 690))
chiling.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成镜像。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chiling2 = chiling.transpose(Image.FLIP_LEFT_RIGHT)
chiling2.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成缩略图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;width, height = chiling.size
width, height = int(width * 0.4), int(height * 0.4)
chiling.thumbnail((width, height))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合成图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;frame = Image.open(&apos;frame.jpg&apos;)
frame.show()
frame.paste(chiling, (210, 150))
frame.paste(chiling2, (522, 150))
frame.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面的知识在&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days&quot;&gt;Python-100-Days&lt;/a&gt;项目的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/github.com/jackfrued/Python-100-Days/blob/master/Day01-15/15.%E5%9B%BE%E5%83%8F%E5%92%8C%E5%8A%9E%E5%85%AC%E6%96%87%E6%A1%A3%E5%A4%84%E7%90%86.md%3E&quot;&gt;第15天&lt;/a&gt;中也有对应的内容。&lt;/p&gt;
&lt;h4&gt;向微信好友群发祝福视频&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装itchat三方库。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/itchat.readthedocs.io/zh/latest/%3E&quot;&gt;itchat&lt;/a&gt;是一个开源的微信个人号接口，使用Python调用微信从未如此简单。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install itchat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install itchat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录微信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import itchat

itchat.auto_login()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：用自己的微信扫描屏幕上出现的二维码就完成了登录操作，登录之后才能获取自己的好友信息以及发送消息给自己的好友。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找自己的朋友。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;friends_list = itchat.get_friends(update=True)
print(len(friends_list))
luohao = friends_list[0]
props = [&apos;NickName&apos;, &apos;Signature&apos;, &apos;Sex&apos;]
for prop in props:
    print(luohao[prop])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：friends_list相当于是一个列表，列表中的第一个元素是自己。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机选出5个朋友，获得他们的用户名、昵称、签名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lucky_friends = random.sample(friends_list[1:], 5) 
props = [&apos;NickName&apos;, &apos;Signature&apos;, &apos;City&apos;]
for friend in lucky_friends:
    for prop in props:
        print(friend[prop] or &apos;没有此项信息&apos;)    
    print(&apos;-&apos; * 80)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给朋友发送文字消息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;itchat.send_msg(&apos;急需一个红包来拯救堕落的灵魂！！！&apos;, toUserName=&apos;@8e06606db03f0e28d0ff884083f727e6&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;群发视频给幸运的朋友们。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lucky_friends = random.sample(friends_list[1:], 5) 
for friend in lucky_friends:
    username = friend[&apos;UserName&apos;]
    itchat.send_video(&apos;/Users/Hao/Desktop/my_test_video.mp4&apos;, toUserName=username)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;利用itchat还能做很多事情，比如有好友给自己发了消息又撤回了，如果想查看这些被撤回的消息，itchat就可以做到（注册一个接收消息的钩子函数，请参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/blog.csdn.net/enweitech/article/details/79585043%3E&quot;&gt;CSDN上的一篇文章&lt;/a&gt;）；再比如，有时候我们想知道某个好友有没有把我们删除或者拉入黑名单，也可以利用itchat封装的群聊功能，非好友和黑名单用户不会被拉入群聊，通过创建群聊函数的返回值就可以判定你和指定的人之间的关系。&lt;/p&gt;
&lt;h4&gt;不用客户端查看热点新闻&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装requests库。（点击常看&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/2.python-requests.org/zh_CN/latest/%3E&quot;&gt;官方文档&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/a726329264.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;爬取新闻数据或者通过API接口获取新闻数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

resp = requests.get(&apos;http://api.tianapi.com/allnews/?key=请使用自己申请的Key&amp;amp;col=7&amp;amp;num=50&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面使用了天行数据提供的数据接口，需要的话可以自行去&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/www.tianapi.com/%3E&quot;&gt;天行数据&lt;/a&gt;的网站注册开通，调用接口的时候要填写注册成功后系统分配给你的key。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用反序列化将JSON字符串解析为字典并获取新闻列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

newslist = json.loads(resp.text)[&apos;newslist&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对新闻列表进行循环遍历，找到感兴趣的新闻，例如：华为。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for news in newslist:
    title = news[&apos;title&apos;]
    url = news[&apos;url&apos;]
    if &apos;华为&apos; in title:
        print(title)
        print(url)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用短信网关发送短信到手机上，告知关注的新闻标题并给出链接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

pattern = re.compile(r&apos;https*:\/\/[^\/]*\/(?P&amp;lt;url&amp;gt;.*)&apos;) 
matcher = pattern.match(url)

if matcher:
    url = matcher.group(&apos;url&apos;)
    resp = requests.post(
        url=&apos;http://sms-api.luosimao.com/v1/send.json&apos;,
        auth=(&apos;api&apos;, &apos;key-请使用你自己申请的Key&apos;),
        data={
            &apos;mobile&apos;: &apos;13548041193&apos;,
            &apos;message&apos;: f&apos;发现一条您可能感兴趣的新闻 - {title}，详情点击https://news.china.com/{url} 查看。【Python小课】&apos;
        },
        timeout=10,
        verify=False
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码使用了&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC04%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E5%A5%BD%E7%8E%A9%E7%9A%84Python/%3Chttps:/luosimao.com/%3E&quot;&gt;螺丝帽&lt;/a&gt;提供的短信网关服务，利用短信网关发送短信是需要支付费用的，但是一般的平台都会提供若干条免费的测试短信。发送短信必须遵守平台的规则，违规的短信是无法发送的。上面发短信时使用的短信模板（“发现一条您可能感兴趣的新闻 - ###，详情点击https://news.china.com/### 查看。”）和短信签名（“【Python小课】”）需要登录螺丝帽管理平台进行配置，如果不清楚如何配置，可以联系平台的客服人员进行咨询。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;02 算法入门系列课程1 - 周而复始&lt;/h2&gt;
&lt;h4&gt;算法概述&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;什么是算法？&lt;/p&gt;
&lt;p&gt;解决问题的正确方法和具体的实施步骤。&lt;/p&gt;
&lt;p&gt;例子1：如何在两栋相距50m的大楼的两个房间牵一条线（两个房间都有窗）？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;养一只鸟（如鸽子），将线送过去&lt;/li&gt;
&lt;li&gt;用很长的杆子将线递过去&lt;/li&gt;
&lt;li&gt;用无人机（遥控飞行器）将线送过去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何评价这些方法的好坏？&lt;strong&gt;少花钱，不费事&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;例子2：大教室里坐了几百名学生一起听课，如何快速的统计学生人数？&lt;/p&gt;
&lt;p&gt;例子3：向列表容器中&lt;strong&gt;逆向&lt;/strong&gt;插入100000个元素。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums = []
for i in range(100000):
    nums.append(i)
nums.reverse()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums = []
for i in range(100000):
    nums.insert(0, i)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子3：生成Fibonacci数列（前100个Fibonacci数）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方法1 - 递推：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b = 0, 1
for num in range(1, 101):
    a, b = b, a + b
    print(f&apos;{num}: {a}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法2 - 递归：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(num):
    if num in (1, 2):
        return 1
    return fib(num - 1) + fib(num - 2)


for num in range(1, 101):
    print(f&apos;{num}: {fib(num)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法3 - 改进的递归：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(num, temp={}):
    if num in (1, 2):
        return 1
    elif num not in temp:
        temp[num] = fib(num - 1) + fib(num - 2)
    return temp[num]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方法4  - 改进的递归：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import lru_cache


@lru_cache()
def fib(num):
    if num in (1, 2):
        return 1
    return fib(num - 1) + fib(num - 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何评价算法的好坏？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC05%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%971-%E5%91%A8%E8%80%8C%E5%A4%8D%E5%A7%8B/%3Chttps:/zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%3E&quot;&gt;渐近时间复杂度&lt;/a&gt;和渐近空间复杂度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大&lt;em&gt;&lt;strong&gt;O&lt;/strong&gt;&lt;/em&gt;符号的意义？&lt;/p&gt;
&lt;p&gt;表示一个函数相对于输入规模的增长速度，也可以称之为函数的数量级。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;大&lt;em&gt;O&lt;/em&gt;符号&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;例子&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$$O(c)$$&lt;/td&gt;
&lt;td&gt;常量时间复杂度&lt;/td&gt;
&lt;td&gt;布隆过滤器 / 哈希存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(log_2n)$$&lt;/td&gt;
&lt;td&gt;对数时间复杂度&lt;/td&gt;
&lt;td&gt;二分查找（折半查找）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(n)$$&lt;/td&gt;
&lt;td&gt;线性时间复杂度&lt;/td&gt;
&lt;td&gt;顺序查找 / 桶排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(n*log_2n)$$&lt;/td&gt;
&lt;td&gt;对数线性时间复杂度&lt;/td&gt;
&lt;td&gt;高级排序算法（归并排序、快速排序）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(n^2)$$&lt;/td&gt;
&lt;td&gt;平方时间复杂度&lt;/td&gt;
&lt;td&gt;简单排序算法（选择排序、插入排序、冒泡排序）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(n^3)$$&lt;/td&gt;
&lt;td&gt;立方时间复杂度&lt;/td&gt;
&lt;td&gt;Floyd算法 / 矩阵乘法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(2^n)$$&lt;/td&gt;
&lt;td&gt;几何级数时间复杂度&lt;/td&gt;
&lt;td&gt;汉诺塔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$$O(n!)$$&lt;/td&gt;
&lt;td&gt;阶乘时间复杂度&lt;/td&gt;
&lt;td&gt;旅行经销商问题&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;穷举法&lt;/h4&gt;
&lt;p&gt;在计算机科学中，&lt;strong&gt;穷举法&lt;/strong&gt;或者&lt;strong&gt;暴力搜索法&lt;/strong&gt;是一个非常非常直观的解决问题的方法，这种方法通过一项一项的列举解决方案所有可能的候选项以及检查每个候选项是否符合问题的描述，最终得到问题的解。&lt;/p&gt;
&lt;p&gt;虽然暴力搜索很容易实现，并且如果解决方案存在它就一定能够找到，但是它的代价是和候选方案的数量成比例的，由于这一点，在很多实际问题中，消耗的代价会随着问题规模的增加而快速地增长。因此，当问题规模有限或当存在可用于将候选解决方案的集合减少到可管理大小时，就可以使用暴力搜索。另外，当实现方法的简单度比速度更重要的时候，也可以考虑使用这种方法。&lt;/p&gt;
&lt;h4&gt;经典例子&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;百钱百鸡&lt;/strong&gt;问题：公鸡5元一只，母鸡3元一只，小鸡1元三只，用100元买一百只鸡，问公鸡、母鸡、小鸡各有多少只？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for x in range(21):
    for y in range(34):
        z = 100 - x - y
        if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
            print(x, y, z)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;五人分鱼&lt;/strong&gt;问题：ABCDE五人在某天夜里合伙捕鱼，最后疲惫不堪各自睡觉。第二天A第一个醒来，他将鱼分为5份，扔掉多余的1条，拿走了属于自己的一份；B第二个醒来，也将鱼分为5份，扔掉多余的1条，拿走属于自己的一份；然后C、D、E依次醒来，也按同样的方式分鱼，问他们至少捕了多少条鱼？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish = 6
while True:
    total = fish
    enough = True
    for _ in range(5):
        if (total - 1) % 5 == 0:
            total = (total - 1) // 5 * 4
        else:
            enough = False
            break
    if enough:
        print(fish)
        break
    fish += 5
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;暴力破解口令&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

import PyPDF2

with open(&apos;Python_Tricks_encrypted.pdf&apos;, &apos;rb&apos;) as pdf_file_stream:
    reader = PyPDF2.PdfFileReader(pdf_file_stream)
    with open(&apos;dictionary.txt&apos;, &apos;r&apos;) as txt_file_stream:
        file_iter = iter(lambda: txt_file_stream.readline(), &apos;&apos;)
        for word in file_iter:
            word = re.sub(r&apos;\s&apos;, &apos;&apos;, word)
            if reader.decrypt(word):
                print(word)
                break
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;03 算法入门系列2 - 在水一方&lt;/h2&gt;
&lt;p&gt;在第一次的公开课中，我们讲到了穷举法。穷举法也被称为暴力搜索法，今天我们要讲的回溯法就是暴力搜索法的一种。接下来我们讲到的很多算法跟“递归”这个概念有或多或少的关系，所以我们先说说“递归”。&lt;/p&gt;
&lt;h4&gt;现实中的递归&lt;/h4&gt;
&lt;p&gt;从前有座山，山里有座庙，庙里有个老和尚，正在给小和尚讲故事呢！故事是什么呢？从前有座山，山里有座庙，庙里有个老和尚，正在给小和尚讲故事呢！故事是什么呢？从前有座山，山里有座庙，庙里有个老和尚，正在给小和尚讲故事呢！故事是什么呢？……&lt;/p&gt;
&lt;p&gt;野比大雄在房间里，用时光电视看着未来的情况。电视画面中，野比大雄在房间里，用时光电视看着未来的情况。电视画面中，野比大雄在房间里，用时光电视看着未来的情况……&lt;/p&gt;
&lt;p&gt;阶乘的递归定义：$$0! = 1$$，$$n!=n*(n-1)!$$ ，使用被定义对象的自身来为其下定义称为递归定义。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%BE%B7%E7%BD%97%E6%96%AF%E7%89%B9%E6%95%88%E5%BA%94&quot;&gt;德罗斯特效应&lt;/a&gt;是递归的一种视觉形式。图中女性手持的物体中有一幅她本人手持同一物体的小图片，进而小图片中还有更小的一幅她手持同一物体的图片……&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/0ef32c7c05.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;递归的应用&lt;/h4&gt;
&lt;p&gt;在程序中，一个函数如果直接或者间接的调用了自身，我们就称之为递归函数。&lt;/p&gt;
&lt;p&gt;写递归函数有两个要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;收敛条件 - 什么时候结束递归。&lt;/li&gt;
&lt;li&gt;递归公式 - 每一项与前一项（前&lt;em&gt;N&lt;/em&gt;项）的关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例子1：求阶乘。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fac(num):
    if num == 0:
        return 1
    return num * fac(num - 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python对递归的深度加以了限制（默认1000层函数调用），如果想突破这个限制，可以使用下面的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys

sys.setrecursionlimit(10000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子2：爬楼梯 - 楼梯有&lt;em&gt;n&lt;/em&gt;个台阶，一步可以走1阶、2阶或3阶，走完&lt;em&gt;n&lt;/em&gt;个台阶共有多少种不同的走法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def climb(num):
    if num == 0:
        return 1
    elif num &amp;lt; 0:
        return 0
    return climb(num - 1) + climb(num - 2) + climb(num - 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的递归函数性能会非常的差，因为时间复杂度是几何级数级的。&lt;/p&gt;
&lt;p&gt;优化后的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import lru_cache


@lru_cache()
def climb(num):
    if num == 0:
        return 1
    elif num &amp;lt; 0:
        return 0
    return climb(num - 1) + climb(num - 2) + climb(num - 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不使用的递归的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def climb(num):
    a, b, c = 1, 2, 4
    for _ in range(num - 1):
        a, b, c = b, c, a + b + c
    return a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重点&lt;/strong&gt;：有更好的办法的时候，请不要考虑递归。&lt;/p&gt;
&lt;h4&gt;回溯法&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;回溯法&lt;/strong&gt;是&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%9A%B4%E5%8A%9B%E6%90%9C%E5%B0%8B%E6%B3%95&quot;&gt;暴力搜索法&lt;/a&gt;中的一种。对于某些计算问题而言，回溯法是一种可以找出所有（或一部分）解的一般性算法，尤其适用于约束满足问题（在解决约束满足问题时，我们逐步构造更多的候选解，并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解，转而测试其他的部分候选解）。&lt;/p&gt;
&lt;h4&gt;经典案例&lt;/h4&gt;
&lt;p&gt;例子1：&lt;strong&gt;迷宫寻路&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/87ba864cfe.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
迷宫寻路
&quot;&quot;&quot;
import random
import sys

WALL = -1
ROAD = 0

ROWS = 10
COLS = 10


def find_way(maze, i=0, j=0, step=1):
    &quot;&quot;&quot;走迷宫&quot;&quot;&quot;
    if 0 &amp;lt;= i &amp;lt; ROWS and 0 &amp;lt;= j &amp;lt; COLS and maze[i][j] == 0:
        maze[i][j] = step
        if i == ROWS - 1 and j == COLS - 1:
            print(&apos;=&apos; * 20)
            display(maze)
            sys.exit(0)
        find_way(maze, i + 1, j, step + 1)
        find_way(maze, i, j + 1, step + 1)
        find_way(maze, i - 1, j, step + 1)
        find_way(maze, i, j - 1, step + 1)
        maze[i][j] = ROAD


def reset(maze):
    &quot;&quot;&quot;重置迷宫&quot;&quot;&quot;
    for i in range(ROWS):
        for j in range(COLS):
            num = random.randint(1, 10)
            maze[i][j] = WALL if num &amp;gt; 7 else ROAD
    maze[0][0] = maze[ROWS - 1][COLS - 1] = ROAD


def display(maze):
    &quot;&quot;&quot;显示迷宫&quot;&quot;&quot;
    for row in maze:
        for col in row:
            if col == -1:
                print(&apos;■&apos;, end=&apos; &apos;)
            elif col == 0:
                print(&apos;□&apos;, end=&apos; &apos;)
            else:
                print(f&apos;{col}&apos;.ljust(2), end=&apos;&apos;)
        print()


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    maze = [[0] * COLS for _ in range(ROWS)]
    reset(maze)
    display(maze)
    find_way(maze)
    print(&apos;没有出路!!!&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**说明：**上面的代码用随机放置围墙的方式来生成迷宫，更好的生成迷宫的方式请参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E5%85%AC%E5%BC%80%E8%AF%BE/%E7%AC%AC06%E6%AC%A1%E5%85%AC%E5%BC%80%E8%AF%BE-%E7%AE%97%E6%B3%95%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%972-%E5%9C%A8%E6%B0%B4%E4%B8%80%E6%96%B9/%3Chttps:/indienova.com/indie-game-development/generate-tile-based-maze-with-backtracking/%3E&quot;&gt;《简单的使用回溯法生成 Tile Based 迷宫》&lt;/a&gt;一文。&lt;/p&gt;
&lt;p&gt;例子2：&lt;strong&gt;骑士巡逻&lt;/strong&gt; - 国际象棋中的骑士（马），按照骑士的移动规则走遍整个棋盘的每一个方格，而且每个方格只能够经过一次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/8d0eaeda82.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
骑士巡逻
&quot;&quot;&quot;
import sys

SIZE = 8


def display(board):
    &quot;&quot;&quot;显示棋盘&quot;&quot;&quot;
    for row in board:
        for col in row:
            print(f&apos;{col}&apos;.rjust(2, &apos;0&apos;), end=&apos; &apos;)
        print()


def patrol(board, i=0, j=0, step=1):
    &quot;&quot;&quot;巡逻&quot;&quot;&quot;
    if 0 &amp;lt;= i &amp;lt; SIZE and 0 &amp;lt;= j &amp;lt; SIZE and board[i][j] == 0:
        board[i][j] = step
        if step == SIZE * SIZE:
            display(board)
            sys.exit(0)
        patrol(board, i + 1, j + 2, step + 1)
        patrol(board, i + 2, j + 1, step + 1)
        patrol(board, i + 2, j - 1, step + 1)
        patrol(board, i + 1, j - 2, step + 1)
        patrol(board, i - 1, j - 2, step + 1)
        patrol(board, i - 2, j - 1, step + 1)
        patrol(board, i - 2, j + 1, step + 1)
        patrol(board, i - 1, j + 2, step + 1)
        board[i][j] = 0


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    board = [[0] * SIZE for _ in range(SIZE)]
    patrol(board)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子3：&lt;strong&gt;八皇后&lt;/strong&gt; - 如何能够在8×8的国际象棋棋盘上放置八个皇后，使得任何一个皇后都无法直接吃掉其他的皇后？为了达到此目的，任两个皇后都不能处于同一条横行、纵行或斜线上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/90dd09f19a.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：这个问题太经典了，网上有大把的答案，留给大家自己搞定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;04 年薪50W+的Python程序员如何写代码&lt;/h2&gt;
&lt;h4&gt;为什么要用Python写代码&lt;/h4&gt;
&lt;h5&gt;没有对比就没有伤害&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;很多互联网和移动互联网企业对开发效率的要求高于对执行效率的要求&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h6&gt;例子1：hello, world&lt;/h6&gt;
&lt;p&gt;C的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    printf(&quot;hello, world\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Example01 {
    
    public static void main(String[] args) {
        System.out.println(&quot;hello, world&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;hello, world&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;例子2：1-100求和&lt;/h6&gt;
&lt;p&gt;C的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int total = 0;
    for (int i = 1; i &amp;lt;= 100; ++i) {
        total += i;
    }
    printf(&quot;%d\n&quot;, total);
	  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(sum(range(1, 101)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;例子3：创建和初始化数组（列表）&lt;/h6&gt;
&lt;p&gt;Java的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;

public class Example03 {

    public static void main(String[] args) {
        boolean[] values = new boolean[10];
        Arrays.fill(values, true);
        System.out.println(Arrays.toString(values));

        int[] numbers = new int[10];
        for (int i = 0; i &amp;lt; numbers.length; ++i) {
            numbers[i] = i + 1;
        }
        System.out.println(Arrays.toString(numbers));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;values = [True] * 10
print(values)
numbers = [x for x in range(1, 11)]
print(numbers)
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;例子4：双色球随机选号&lt;/h6&gt;
&lt;p&gt;Java的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;

class Example03 {
    private static final List&amp;lt;Integer&amp;gt; RED_BALLS = new ArrayList&amp;lt;&amp;gt;();
    static {
        for (int i = 1; i &amp;lt;= 33; ++i) RED_BALLS.add(i);
    }
    
    public static void display(List&amp;lt;Integer&amp;gt; balls) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &amp;lt; balls.size(); i++) {
            sb.append(String.format(&quot;%02d &quot;, balls.get(i)));
            if (i == 5) sb.append(&quot;| &quot;);
        }
        System.out.println(sb.toString());
    }

    public static List&amp;lt;Integer&amp;gt; generate() {
        List&amp;lt;Integer&amp;gt; pool = new ArrayList&amp;lt;&amp;gt;(RED_BALLS);
        Collections.shuffle(pool);
        List&amp;lt;Integer&amp;gt; selectedBalls = pool.subList(0, 6);
        Collections.sort(selectedBalls);
        selectedBalls.add(ThreadLocalRandom.current().nextInt(1, 17));
        return selectedBalls;
    }
    
    public static void main(String[] args) {
        try (Scanner sc = new Scanner(System.in)) {
            System.out.print(&quot;机选几注: &quot;);
            if (sc.hasNextInt()) {
                int num = sc.nextInt();
                for (int i = 0; i &amp;lt; num; ++i) {
                    display(generate());
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

RED_RANGE = range(1, 34)
BLU_RANGE = range(1, 17)


def generate():
    red_balls = random.sample(RED_RANGE, 6)
    red_balls.sort()
    blue_ball = random.choice(BLU_RANGE)
    return red_balls, blue_ball


def display(sample):
    reds, blue = sample
    reds_str = &apos; &apos;.join(f&apos;{ball:02d}&apos; for ball in reds)
    print(f&apos;{reds_str} | {blue:02d}&apos;)


num = int(input(&apos;机选几注: &apos;))
for _ in range(num):
    display(generate())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;温馨提示&lt;/strong&gt;：珍爱生命，远离任何形式的赌博。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h6&gt;例子5：实现一个简单的HTTP服务器。&lt;/h6&gt;
&lt;p&gt;Java的版本：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：JDK 1.6以前，需要通过套接字编程来实现，具体又可以分为多线程和NIO两种做法。JDK 1.6以后，可以使用&lt;code&gt;com.sun.net.httpserver&lt;/code&gt;包提供的&lt;code&gt;HttpServer&lt;/code&gt;类来实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

class Example05 {

    public static void main(String[] arg) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext(&quot;/&quot;, new RequestHandler());
        server.start();
    }

    static class RequestHandler implements HttpHandler {
        
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String response = &quot;&amp;lt;h1&amp;gt;hello, world&amp;lt;/h1&amp;gt;&quot;;
            exchange.sendResponseHeaders(200, 0);
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from http.server import HTTPServer, SimpleHTTPRequestHandler


class RequestHandler(SimpleHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(&apos;&amp;lt;h1&amp;gt;hello, world&amp;lt;/h1&amp;gt;&apos;.encode())


server = HTTPServer((&apos;&apos;, 8000), RequestHandler)
server.serve_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m http.server 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;一行Python代码可以做什么&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;很多时候，你的问题只需一行Python代码就能解决&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 一行代码实现求阶乘函数
fac = lambda x: __import__(&apos;functools&apos;).reduce(int.__mul__, range(1, x + 1), 1)

# 一行代码实现求最大公约数函数
gcd = lambda x, y: y % x and gcd(y % x, x) or x

# 一行代码实现判断素数的函数
is_prime = lambda x: x &amp;gt; 1 and not [f for f in range(2, int(x ** 0.5) + 1) if x % f == 0]

# 一行代码实现快速排序
quick_sort = lambda items: len(items) and quick_sort([x for x in items[1:] if x &amp;lt; items[0]]) + [items[0]] + quick_sort([x for x in items[1:] if x &amp;gt; items[0]]) or items

# 生成FizzBuzz列表
[&apos;Fizz&apos;[x % 3 * 4:] + &apos;Buzz&apos;[x % 5 * 4:] or x for x in range(1, 101)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;设计模式从未如此简单&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Python是动态类型语言，大量的设计模式在Python中被简化或弱化&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考：如何优化下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(num):
    if num in (1, 2):
        return 1
    return fib(num - 1) + fib(num - 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代理模式在Python中可以通过内置的或自定义的装饰器来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import lru_cache


@lru_cache()
def fib(num):
    if num in (1, 2):
        return 1
    return fib(num - 1) + fib(num - 2)


for n in range(1, 121):
    print(f&apos;{n}: {fib(n)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过Python标准库&lt;code&gt;functools&lt;/code&gt;模块的&lt;code&gt;lru_cache&lt;/code&gt;装饰器为&lt;code&gt;fib&lt;/code&gt;函数加上缓存代理，缓存函数执行的中间结果，优化代码的性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;单例模式在Python中可以通过自定义的装饰器或元类来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps
from threading import RLock


def singleton(cls):
    instances = {}
    lock = RLock()

    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            with lock:
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：需要实现单例模式的类只需要添加上面的装饰器即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原型模式在Python中可以通过元类来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import copy


class PrototypeMeta(type):

    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        cls.clone = lambda self, is_deep=True: \
            copy.deepcopy(self) if is_deep else copy.copy(self)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过元类给指定了&lt;code&gt;metaclass=PrototypeMeta&lt;/code&gt;的类添加一个&lt;code&gt;clone&lt;/code&gt;方法实现对象克隆，利用Python标准库&lt;code&gt;copy&lt;/code&gt;模块的&lt;code&gt;copy&lt;/code&gt;和&lt;code&gt;deepcopy&lt;/code&gt;分别实现浅拷贝和深拷贝。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;数据采集和数据分析从未如此简单&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;网络数据采集是Python最擅长的领域之一。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例子：获取豆瓣电影“Top250”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time

import requests
from bs4 import BeautifulSoup

for page in range(10):
    resp = requests.get(
        url=f&apos;https://movie.douban.com/top250?start={25 * page}&apos;,
        headers={&apos;User-Agent&apos;: &apos;BaiduSpider&apos;}
    )
    soup = BeautifulSoup(resp.text, &quot;lxml&quot;)
    for elem in soup.select(&apos;a &amp;gt; span.title:nth-child(1)&apos;):
        print(elem.text)
    time.sleep(random.random() * 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;利用NumPy、Pandas、Matplotlib可以轻松实现数据分析和可视化&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/285e4ddb4d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;写出Python代码的正确姿势&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;用Python写代码就要写出Pythonic的代码&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;姿势1：选择结构的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;jackfrued&apos;
fruits = [&apos;apple&apos;, &apos;orange&apos;, &apos;grape&apos;]
owners = {&apos;name&apos;: &apos;骆昊&apos;, &apos;age&apos;: 40, &apos;gender&apos;: True}
if name != &apos;&apos; and len(fruits) &amp;gt; 0 and len(owners.keys()) &amp;gt; 0:
    print(&apos;Jackfrued love fruits.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;jackfrued&apos;
fruits = [&apos;apple&apos;, &apos;orange&apos;, &apos;grape&apos;]
owners = {&apos;name&apos;: &apos;骆昊&apos;, &apos;age&apos;: 40, &apos;gender&apos;: True}
if name and fruits and owners:
    print(&apos;Jackfrued love fruits.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;姿势2：交换两个变量的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp = a
a = b
b = temp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = a ^ b
b = a ^ b
a = a ^ b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b = b, a
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;姿势3：用序列组装字符串的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chars = [&apos;j&apos;, &apos;a&apos;, &apos;c&apos;, &apos;k&apos;, &apos;f&apos;, &apos;r&apos;, &apos;u&apos;, &apos;e&apos;, &apos;d&apos;]
name = &apos;&apos;
for char in chars:
    name += char
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chars = [&apos;j&apos;, &apos;a&apos;, &apos;c&apos;, &apos;k&apos;, &apos;f&apos;, &apos;r&apos;, &apos;u&apos;, &apos;e&apos;, &apos;d&apos;]
name = &apos;&apos;.join(chars)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;姿势4：遍历列表的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fruits = [&apos;orange&apos;, &apos;grape&apos;, &apos;pitaya&apos;, &apos;blueberry&apos;]
index = 0
for fruit in fruits:
    print(index, &apos;:&apos;, fruit)
    index += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fruits = [&apos;orange&apos;, &apos;grape&apos;, &apos;pitaya&apos;, &apos;blueberry&apos;]
for index, fruit in enumerate(fruits):
    print(index, &apos;:&apos;, fruit)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;姿势5：创建列表的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = [7, 20, 3, 15, 11]
result = []
for i in data:
    if i &amp;gt; 10:
        result.append(i * 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = [7, 20, 3, 15, 11]
result = [num * 3 for num in data if num &amp;gt; 10]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;姿势6：确保代码健壮性的正确姿势&lt;/h5&gt;
&lt;p&gt;跨界开发者的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = {&apos;x&apos;: &apos;5&apos;}
if &apos;x&apos; in data and isinstance(data[&apos;x&apos;], (str, int, float)) \
        and data[&apos;x&apos;].isdigit():
    value = int(data[&apos;x&apos;])
    print(value)
else:
    value = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pythonic的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = {&apos;x&apos;: &apos;5&apos;}
try:
    value = int(data[&apos;x&apos;])
    print(value)
except (KeyError, TypeError, ValueError):
    value = None
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用Lint工具检查你的代码规范&lt;/h4&gt;
&lt;p&gt;阅读下面的代码，看看你能看出哪些地方是有毛病的或者说不符合Python的编程规范的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from enum import *

@unique
class Suite (Enum):
    SPADE, HEART, CLUB, DIAMOND = range(4)

class Card(object):
    def __init__(self,suite,face ):
        self.suite = suite
        self.face = face


    def __repr__(self):
        suites=&apos;♠♥♣♦&apos;
        faces=[&apos;&apos;,&apos;A&apos;,&apos;2&apos;,&apos;3&apos;,&apos;4&apos;,&apos;5&apos;,&apos;6&apos;,&apos;7&apos;,&apos;8&apos;,&apos;9&apos;,&apos;10&apos;,&apos;J&apos;,&apos;Q&apos;,&apos;K&apos;]
        return f&apos;{suites[self.suite.value]}{faces[self.face]}&apos;

import random

class Poker(object):
    def __init__(self):
        self.cards =[Card(suite, face) for suite in Suite
            for face in range(1, 14)]
        self.current=0
    def shuffle (self):
        self.current=0
        random.shuffle(self.cards)
    def deal (self):
        card = self.cards[self.current]
        self.current+=1
        return card
    def has_next (self):
        if self.current&amp;lt;len(self.cards): return True
        return False

p = Poker()
p.shuffle()
print(p.cards)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;PyLint的安装和使用&lt;/h5&gt;
&lt;p&gt;Pylint是Python代码分析工具，它分析Python代码中的错误，查找不符合代码风格标准（默认使用的代码风格是 PEP 8）和有潜在问题的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pylint
pylint [options] module_or_package
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pylint输出格式如下所示。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;模块名:行号:列号:    消息类型    消息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;消息类型有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;C - 惯例：违反了Python编程惯例（PEP 8）的代码。&lt;/li&gt;
&lt;li&gt;R - 重构：写得比较糟糕需要重构的代码。&lt;/li&gt;
&lt;li&gt;W - 警告：代码中存在的不影响代码运行的问题。&lt;/li&gt;
&lt;li&gt;E - 错误：代码中存在的影响代码运行的错误。&lt;/li&gt;
&lt;li&gt;F - 致命错误：导致Pylint无法继续运行的错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pylint命令的常用参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;--disable=&amp;lt;msg ids&amp;gt;&lt;/code&gt;或&lt;code&gt;-d &amp;lt;msg ids&amp;gt;&lt;/code&gt;：禁用指定类型的消息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--errors-only&lt;/code&gt;或&lt;code&gt;-E&lt;/code&gt;：只显示错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--rcfile=&amp;lt;file&amp;gt;&lt;/code&gt;：指定配置文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--list-msgs&lt;/code&gt;：列出Pylint的消息清单。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--generate-rcfile&lt;/code&gt;：生成配置文件的样例。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--reports=&amp;lt;y_or_n&amp;gt;&lt;/code&gt;或&lt;code&gt;-r &amp;lt;y_or_n&amp;gt;&lt;/code&gt;：是否生成检查报告。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用Profile工具剖析你的代码性能&lt;/h4&gt;
&lt;h5&gt;cProfile模块&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;example01.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cProfile


def is_prime(num):
    for factor in range(2, int(num ** 0.5) + 1):
        if num % factor == 0:
            return False
    return True


class PrimeIter:

    def __init__(self, total):
        self.counter = 0
        self.current = 1
        self.total = total

    def __iter__(self):
        return self

    def __next__(self):
        if self.counter &amp;lt; self.total:
            self.current += 1
            while not is_prime(self.current):
                self.current += 1
            self.counter += 1
            return self.current
        raise StopIteration()

        
cProfile.run(&apos;list(PrimeIter(10000))&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   114734 function calls in 0.573 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.006    0.006    0.573    0.573 &amp;lt;string&amp;gt;:1(&amp;lt;module&amp;gt;)
        1    0.000    0.000    0.000    0.000 example.py:14(__init__)
        1    0.000    0.000    0.000    0.000 example.py:19(__iter__)
    10001    0.086    0.000    0.567    0.000 example.py:22(__next__)
   104728    0.481    0.000    0.481    0.000 example.py:5(is_prime)
        1    0.000    0.000    0.573    0.573 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;line_profiler&lt;/h5&gt;
&lt;p&gt;给需要剖析时间性能的函数加上一个&lt;code&gt;profile&lt;/code&gt;装饰器，这个函数每行代码的执行次数和时间都会被剖析。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;example02.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@profile
def is_prime(num):
    for factor in range(2, int(num ** 0.5) + 1):
        if num % factor == 0:
            return False
    return True


class PrimeIter:

    def __init__(self, total):
        self.counter = 0
        self.current = 1
        self.total = total

    def __iter__(self):
        return self

    def __next__(self):
        if self.counter &amp;lt; self.total:
            self.current += 1
            while not is_prime(self.current):
                self.current += 1
            self.counter += 1
            return self.current
        raise StopIteration()


list(PrimeIter(1000))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装和使用&lt;code&gt;line_profiler&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install line_profiler
kernprof -lv example.py

Wrote profile results to example02.py.lprof
Timer unit: 1e-06 s

Total time: 0.089513 s
File: example02.py
Function: is_prime at line 1

 #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
 1                                           @profile
 2                                           def is_prime(num):
 3     86624      43305.0      0.5     48.4      for factor in range(2, int(num ** 0.5) + 1):
 4     85624      42814.0      0.5     47.8          if num % factor == 0:
 5      6918       3008.0      0.4      3.4              return False
 6      1000        386.0      0.4      0.4      return True
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;memory_profiler&lt;/h5&gt;
&lt;p&gt;给需要剖析内存性能的函数加上一个&lt;code&gt;profile&lt;/code&gt;装饰器，这个函数每行代码的内存使用情况都会被剖析。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;example03.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@profile
def eat_memory():
    items = []
    for _ in range(1000000):
        items.append(object())
    return items


eat_memory()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装和使用&lt;code&gt;memory_profiler&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install memory_profiler
python3 -m memory_profiler example.py

Filename: example03.py

Line #    Mem usage    Increment   Line Contents
================================================
     1   38.672 MiB   38.672 MiB   @profile
     2                             def eat_memory():
     3   38.672 MiB    0.000 MiB       items = []
     4   68.727 MiB    0.000 MiB       for _ in range(1000000):
     5   68.727 MiB    1.797 MiB           items.append(object())
     6   68.727 MiB    0.000 MiB       return items
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;如何构建综合职业素养&lt;/h4&gt;
&lt;h5&gt;学习总结&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;了解全局&lt;/li&gt;
&lt;li&gt;确定范围&lt;/li&gt;
&lt;li&gt;定义目标&lt;/li&gt;
&lt;li&gt;寻找资源&lt;/li&gt;
&lt;li&gt;创建学习计划&lt;/li&gt;
&lt;li&gt;筛选资源&lt;/li&gt;
&lt;li&gt;开始学习，浅尝辄止（YAGNI）&lt;/li&gt;
&lt;li&gt;动手操作，边学边玩&lt;/li&gt;
&lt;li&gt;全面掌握，学以致用&lt;/li&gt;
&lt;li&gt;乐为人师，融会贯通&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;时间管理&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;提升专注力&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;充分利用碎片时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用番茄工作法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间是怎么浪费掉的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任何行动都比不采取行动好&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/open-class/10112211c4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;好书推荐&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;职业规划：《软技能 - 代码之外的生存指南》&lt;/li&gt;
&lt;li&gt;吴军系列：《浪潮之巅》、《硅谷之谜》、《数学之美》、……&lt;/li&gt;
&lt;li&gt;时间管理：《成为一个更高效的人》、《番茄工作法图解》&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>番外篇 教程全文（16 篇合并）</title><link>https://www.klucen.cn/posts/python/extra-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/extra-full/</guid><pubDate>Sun, 14 Dec 2025 00:19:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 16 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：惯例、工具与踩坑
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#01-pep-8%E9%A3%8E%E6%A0%BC%E6%8C%87%E5%8D%97&quot;&gt;01 PEP 8风格指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#02-%E5%B8%B8%E8%A7%81%E5%8F%8D%E7%88%AC%E7%AD%96%E7%95%A5%E5%8F%8A%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88&quot;&gt;02 常见反爬策略及应对方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#03-%E5%88%86%E4%BA%AB%E5%87%A0%E5%BC%A0%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E5%9B%BE&quot;&gt;03 分享几张学习路线图&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#04-%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E5%8F%82%E8%80%83%E7%A4%BA%E4%BE%8B&quot;&gt;04 接口文档参考示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#05-%E9%82%A3%E4%BA%9B%E5%B9%B4%E6%88%91%E4%BB%AC%E8%B8%A9%E8%BF%87%E7%9A%84%E9%82%A3%E4%BA%9B%E5%9D%91&quot;&gt;05 那些年我们踩过的那些坑&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#06-%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E9%A9%BE%E9%A9%AD-pandas-%E5%BA%93&quot;&gt;06 如何快速驾驭 pandas 库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#07-%E4%BD%BF%E7%94%A8hexo%E6%90%AD%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E5%8D%9A%E5%AE%A2&quot;&gt;07 使用Hexo搭建自己的博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#08-%E6%88%91%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9%E4%BA%86python&quot;&gt;08 我为什么选择了Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#09-%E4%B8%80%E4%B8%AA%E5%B0%8F%E4%BE%8B%E5%AD%90%E5%8A%A9%E4%BD%A0%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3%E5%8D%8F%E7%A8%8B&quot;&gt;09 一个小例子助你彻底理解协程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-%E7%94%A8%E5%87%BD%E6%95%B0%E8%BF%98%E6%98%AF%E7%94%A8%E5%A4%8D%E6%9D%82%E7%9A%84%E8%A1%A8%E8%BE%BE%E5%BC%8F&quot;&gt;10 用函数还是用复杂的表达式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#11-%E7%9F%A5%E4%B9%8E%E9%97%AE%E9%A2%98%E5%9B%9E%E7%AD%94&quot;&gt;11 知乎问题回答&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#12-%E7%94%A8canvas%E5%AD%A6%E4%B9%A0%E7%BC%96%E7%A8%8B&quot;&gt;12 用Canvas学习编程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#13-python%E7%BC%96%E7%A8%8B%E6%83%AF%E4%BE%8B&quot;&gt;13 Python编程惯例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#14-python%E5%8F%82%E8%80%83%E4%B9%A6%E7%B1%8D&quot;&gt;14 Python参考书籍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#15-python%E5%AE%B9%E5%99%A8%E7%B1%BB%E5%9E%8B%E4%BD%BF%E7%94%A8%E5%B0%8F%E6%8A%80%E5%B7%A7&quot;&gt;15 Python容器类型使用小技巧&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#16-zen-of-pythonpython%E4%B9%8B%E7%A6%85&quot;&gt;16 Zen of Python（Python之禅）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;01 PEP 8风格指南&lt;/h2&gt;
&lt;p&gt;PEP是Python Enhancement Proposal的缩写，通常翻译为“Python增强提案”。每个PEP都是一份为Python社区提供的指导Python往更好的方向发展的技术文档，其中的第8号增强提案（PEP 8）是针对Python语言编订的代码风格指南。尽管我们可以在保证语法没有问题的前提下随意书写Python代码，但是在实际开发中，采用一致的风格书写出可读性强的代码是每个专业的程序员应该做到的事情，也是每个公司的编程规范中会提出的要求，这些在多人协作开发一个项目（团队开发）的时候显得尤为重要。我们可以从Python官方网站的&lt;a href=&quot;https://www.python.org/dev/peps/pep-0008/&quot;&gt;PEP 8链接&lt;/a&gt;中找到该文档，下面我们对该文档的关键部分做一个简单的总结。&lt;/p&gt;
&lt;h4&gt;空格的使用&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;u&amp;gt;使用空格来表示缩进而不要用制表符（Tab）。&amp;lt;/u&amp;gt;这一点对习惯了其他编程语言的人来说简直觉得不可理喻，因为绝大多数的程序员都会用Tab来表示缩进，但是要知道Python并没有像C/C++或Java那样的用花括号来构造一个代码块的语法，在Python中分支和循环结构都使用缩进来表示哪些代码属于同一个级别，鉴于此Python代码对缩进以及缩进宽度的依赖比其他很多语言都强得多。在不同的编辑器中，Tab的宽度可能是2、4或8个字符，甚至是其他更离谱的值，用Tab来表示缩进对Python代码来说可能是一场灾难。&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;和语法相关的每一层缩进都用4个空格来表示。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;每行的字符数不要超过79个字符，如果表达式因太长而占据了多行，除了首行之外的其余各行都应该在正常的缩进宽度上再加上4个空格。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;函数和类的定义，代码前后都要用两个空行进行分隔。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;在同一个类中，各个方法之间应该用一个空行进行分隔。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;二元运算符的左右两侧应该保留一个空格，而且只要一个空格就好。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;标识符命名&lt;/h4&gt;
&lt;p&gt;PEP 8倡导用不同的命名风格来命名Python中不同的标识符，以便在阅读代码时能够通过标识符的名称来确定该标识符在Python中扮演了怎样的角色（在这一点上，Python自己的内置模块以及某些第三方模块都做得并不是很好）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;u&amp;gt;变量、函数和属性应该使用小写字母来拼写，如果有多个单词就使用下划线进行连接。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;类中受保护的实例属性，应该以一个下划线开头。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;类中私有的实例属性，应该以两个下划线开头。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;类和异常的命名，应该每个单词首字母大写。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;模块级别的常量，应该采用全大写字母，如果有多个单词就用下划线进行连接。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;类的实例方法，应该把第一个参数命名为&lt;code&gt;self&lt;/code&gt;以表示对象自身。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;类的类方法，应该把第一个参数命名为&lt;code&gt;cls&lt;/code&gt;以表示该类自身。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;表达式和语句&lt;/h4&gt;
&lt;p&gt;在Python之禅（可以使用&lt;code&gt;import this&lt;/code&gt;查看）中有这么一句名言：“There should be one-- and preferably only one --obvious way to do it.”，翻译成中文是“做一件事应该有而且最好只有一种确切的做法”，这句话传达的思想在PEP 8中也是无处不在的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;u&amp;gt;采用内联形式的否定词，而不要把否定词放在整个表达式的前面。&amp;lt;/u&amp;gt;例如&lt;code&gt;if a is not b&lt;/code&gt;就比&lt;code&gt;if not a is b&lt;/code&gt;更容易让人理解。&lt;/li&gt;
&lt;li&gt;不要用检查长度的方式来判断字符串、列表等是否为&lt;code&gt;None&lt;/code&gt;或者没有元素，应该用&lt;code&gt;if not x&lt;/code&gt;这样的写法来检查它。&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;就算&lt;code&gt;if&lt;/code&gt;分支、&lt;code&gt;for&lt;/code&gt;循环、&lt;code&gt;except&lt;/code&gt;异常捕获等中只有一行代码，也不要将代码和&lt;code&gt;if&lt;/code&gt;、&lt;code&gt;for&lt;/code&gt;、&lt;code&gt;except&lt;/code&gt;等写在一起，分开写才会让代码更清晰。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;&lt;code&gt;import&lt;/code&gt;语句总是放在文件开头的地方。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;引入模块的时候，&lt;code&gt;from math import sqrt&lt;/code&gt;比&lt;code&gt;import math&lt;/code&gt;更好。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;如果有多个&lt;code&gt;import&lt;/code&gt;语句，应该将其分为三部分，从上到下分别是Python&lt;strong&gt;标准模块&lt;/strong&gt;、&lt;strong&gt;第三方模块&lt;/strong&gt;和&lt;strong&gt;自定义模块&lt;/strong&gt;，每个部分内部应该按照模块名称的&lt;strong&gt;字母表顺序&lt;/strong&gt;来排列。&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;02 常见反爬策略及应对方案&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构造合理的HTTP请求头。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Accept&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;User-Agent&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Referer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accept-Encoding&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accept-Language&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查网站生成的Cookie。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有用的插件：&lt;a href=&quot;http://www.editthiscookie.com/&quot;&gt;EditThisCookie&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如何处理脚本动态生成的Cookie&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;抓取动态内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Selenium + WebDriver&lt;/li&gt;
&lt;li&gt;Chrome / Firefox - Driver&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;限制爬取的速度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理表单中的隐藏域。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在读取到隐藏域之前不要提交表单&lt;/li&gt;
&lt;li&gt;用RoboBrowser这样的工具辅助提交表单&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理表单中的验证码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OCR（Tesseract） - 商业项目一般不考虑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;专业识别平台 - 超级鹰 / 云打码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from hashlib import md5

class ChaoClient(object):

    def __init__(self, username, password, soft_id):
        self.username = username
        password =  password.encode(&apos;utf-8&apos;)
        self.password = md5(password).hexdigest()
        self.soft_id = soft_id
        self.base_params = {
            &apos;user&apos;: self.username,
            &apos;pass2&apos;: self.password,
            &apos;softid&apos;: self.soft_id,
        }
        self.headers = {
            &apos;Connection&apos;: &apos;Keep-Alive&apos;,
            &apos;User-Agent&apos;: &apos;Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)&apos;,
        }

    def post_pic(self, im, codetype):
        params = {
            &apos;codetype&apos;: codetype,
        }
        params.update(self.base_params)
        files = {&apos;userfile&apos;: (&apos;captcha.jpg&apos;, im)}
        r = requests.post(&apos;http://upload.chaojiying.net/Upload/Processing.php&apos;, data=params, files=files, headers=self.headers)
        return r.json()


if __name__ == &apos;__main__&apos;:
    client = ChaoClient(&apos;用户名&apos;, &apos;密码&apos;, &apos;软件ID&apos;)
    with open(&apos;captcha.jpg&apos;, &apos;rb&apos;) as file:                                                
        print(client.post_pic(file, 1902))                                          
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绕开“陷阱”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网页上有诱使爬虫爬取的爬取的隐藏链接（陷阱或蜜罐）&lt;/li&gt;
&lt;li&gt;通过Selenium+WebDriver+Chrome判断链接是否可见或在可视区域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐藏身份。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;代理服务 -  快代理 / 讯代理 / 芝麻代理 / 蘑菇代理 / 云代理&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cuiqingcai.com/5094.html&quot;&gt;《爬虫代理哪家强？十大付费代理详细对比评测出炉！》&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;洋葱路由 - 国内需要翻墙才能使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum -y install tor
useradd admin -d /home/admin
passwd admin
chown -R admin:admin /home/admin
chown -R admin:admin /var/run/tor
tor
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;03 分享几张学习路线图&lt;/h2&gt;
&lt;p&gt;今天逛了一个名为 &lt;a href=&quot;https://roadmap.sh&quot;&gt;Developer Roadmaps&lt;/a&gt; 的网站，上面就有好多的学习路线图。其中有一张关于 Python 的学习路线图还不错，分享给大家。如果用 Python 做工程化开发（后端开发、接口开发、应用开发等），这张图上的内容基本都是需要涉猎的；如果仅仅是将 Python 作为辅助工具，学习之前给大家整理的&lt;a href=&quot;https://www.zhihu.com/column/c_1216656665569013760&quot;&gt;《从零开始学Python》&lt;/a&gt;的 20 节课就已经足够了。近期，我也在重置这 20 节课的内容，重置后的内容会标记为“2025版”，也希望在这个过程中听到大家的意见和建议。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/2de594aa9d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里还有三个相关的学习路线图，分别是后端开发（Backend）、开发运维（DevOps）和数据科学（Data Science），后端开发的学习路线图如下所示，内容比较庞杂，核心的编程语言是 Java 或 Go。我个人不推荐大家用 Python 做后端开发，如果 Python 语言已经掌握得很好了，同时也具备了较多的计算机科学相关知识，可以尝试 AI 应用开发这个赛道，我相信你会有更多的收获。大家可以注意下用紫色勾选的项目，这些是作者推荐的选项，大部分也是我想推荐给后端开发者学习的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/f44d1a8172.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再看看开发运维的学习路线图，目前 DevOps 也是招聘市场的热门岗位，有兴趣的小伙伴可以关注下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/d695e607fe.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果学过一些 Python 相关知识，想以此为基础转行做数据分析，可以看看下面这个学习路线图。需要说明的是，Python 仅仅是做数据分析的一个工具，要成为一个优秀的数据分析师需要有良好的数据思维，同时还要积累足够的业务知识，这样才能从数据中产生商业洞察，用数据优化产品、驱动业务，真正为决策提供强有力的支撑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/e100de4b11.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，再次提醒大家，想打卡学习的一定记得在&lt;a href=&quot;https://zhuanlan.zhihu.com/p/23821670129&quot;&gt;评论区&lt;/a&gt;给我留言！如果想&lt;strong&gt;入坑数据科学&lt;/strong&gt;（数据分析、数据挖掘、数据治理等）也可以给我留言，今年应该会持续输出相关的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/e26c7a2310.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;04 接口文档参考示例&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用户登录 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/login/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;最后修改时间：&lt;/p&gt;
&lt;p&gt;接口说明：登录成功后，会生成或更新用户令牌（token）。&lt;/p&gt;
&lt;p&gt;使用帮助：测试数据库中预设了四个可供使用的账号，如下表所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用户名&lt;/th&gt;
&lt;th&gt;用户口令&lt;/th&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jackfrued&lt;/td&gt;
&lt;td&gt;123456&lt;/td&gt;
&lt;td&gt;管理员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wangdachui&lt;/td&gt;
&lt;td&gt;123123&lt;/td&gt;
&lt;td&gt;普通用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hellokitty&lt;/td&gt;
&lt;td&gt;123123&lt;/td&gt;
&lt;td&gt;房地产经理人&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wuzetian&lt;/td&gt;
&lt;td&gt;123456&lt;/td&gt;
&lt;td&gt;房东&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;username&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;用户名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;password&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;用户口令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;登录成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 30000,
    &quot;message&quot;: &quot;用户登录成功&quot;,
    &quot;token&quot;: &quot;f83e0f624e2311e9af1f00163e02b646&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 30001,
    &quot;message&quot;: &quot;用户名或密码错误&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送短信验证码 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/mobile_code/{国内手机号}/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：给指定手机号发送短信验证码的接口，手机号必须是国内手机号，作为路径参数写到URL中。接口显示短信发送成功时，指定的手机号并不会收到短息，因为使用的三方短信平台赠送的测试短信已经用完了。&lt;/p&gt;
&lt;p&gt;使用帮助：国内手机号暂不支持国际区号。&lt;/p&gt;
&lt;p&gt;请求参数：暂无。&lt;/p&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10001,
    &quot;msg&quot;: &quot;短信验证码发送成功&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两次请求间隔小于60秒：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10002,
    &quot;msg&quot;: &quot;请不要在60秒以内重复发送手机验证码&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;手机号无效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10003,
    &quot;msg&quot;: &quot;请提供有效的手机号&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;短信服务平台故障：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10004,
    &quot;msg&quot;: &quot;短信服务暂时无法使用&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取所有省级行政单位 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/districts/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：暂无。&lt;/p&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;distid&quot;: 110000,
        &quot;name&quot;: &quot;北京市&quot;
    },
    {
        &quot;distid&quot;: 120000,
        &quot;name&quot;: &quot;天津市&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取指定行政单位详情及其管辖的行政单位 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/districts/{行政单位编号}/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：通过URL参数指定行政单位编号，如果行政单位编号为省级行政单位编号，则返回该省以及该省所辖市级行政单位的信息；如果行政单位编号为市级行政单位编号，则返回该市以及该市所辖区县的信息；如果行政单位编号为区县一级行政单位编号，则返回该区县的信息，下级行政单位&lt;code&gt;cities&lt;/code&gt;属性值为&lt;code&gt;[]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用帮助：数据库中除四川省外其他行政单位的“intro”数据都没有录入，该字段可能为空字符串。&lt;/p&gt;
&lt;p&gt;请求参数：暂无。&lt;/p&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;distid&quot;: 510000,
    &quot;name&quot;: &quot;四川省&quot;,
    &quot;intro&quot;: &quot;位于中国西南地区内陆，东连重庆，南邻云南、贵州，西接西藏，北界陕西、甘肃、青海，四川省总面积48.6万平方千米，省会成都。截至2018年底，四川省下辖18个省辖市，3个自治州，17个县级市，108个县，4个自治县，54个市辖区。&quot;,
    &quot;cities&quot;: [
        {
            &quot;distid&quot;: 510100,
            &quot;name&quot;: &quot;成都市&quot;
        },
        {
            &quot;distid&quot;: 510300,
            &quot;name&quot;: &quot;自贡市&quot;
        },
        {
            &quot;distid&quot;: 510400,
            &quot;name&quot;: &quot;攀枝花市&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取热门城市 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/hotcities/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：暂无。&lt;/p&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;distid&quot;: 110100,
        &quot;name&quot;: &quot;北京市&quot;
    },
    {
        &quot;distid&quot;: 120100,
        &quot;name&quot;: &quot;天津市&quot;
    },
    {
        &quot;distid&quot;: 130100,
        &quot;name&quot;: &quot;石家庄&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分页获取房地产经理人信息 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/agents/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：经理人姓名按照前缀模糊匹配的方式进行处理；经理人服务星级是指经理人服务星级不得低于该星级；经理人是否持证只有0（未持证上岗）和1（持证上岗）两个选项。三个参数代表的筛选条件之间是而且的关系。返回结果为分页之后的房地产经理人信息。&lt;/p&gt;
&lt;p&gt;使用帮助：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;经理人姓名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;经理人服务星级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cert&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;经理人是否持证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页码，默认值为1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页面大小，默认值为5，最大值不超过50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;count&quot;: 1,
    &quot;next&quot;: null,
    &quot;previous&quot;: null,
    &quot;results&quot;: [
        {
            &quot;agentid&quot;: 6,
            &quot;estates&quot;: [
                {
                    &quot;estateid&quot;: 11,
                    &quot;name&quot;: &quot;灵芝新村&quot;,
                    &quot;hot&quot;: 20
                }
            ],
            &quot;name&quot;: &quot;肖利丽&quot;,
            &quot;tel&quot;: &quot;13040813886&quot;,
            &quot;servstar&quot;: 4,
            &quot;realstar&quot;: 4,
            &quot;profstar&quot;: 4,
            &quot;certificated&quot;: false
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增房地产经理人 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/agents/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：需要登录且拥有管理员权限，用户身份令牌在请求头中提供。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;经理人姓名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tel&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;经理人手机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;servstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;realstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;profstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;certificated&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户身份认证令牌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新增成功 - 状态码&lt;strong&gt;201&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;agentid&quot;: 8,
    &quot;estates&quot;: [],
    &quot;name&quot;: &quot;孙小美&quot;,
    &quot;tel&quot;: &quot;13800991234&quot;,
    &quot;servstar&quot;: 0,
    &quot;realstar&quot;: 0,
    &quot;profstar&quot;: 0,
    &quot;certificated&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未提供身份认证信息 - 状态码&lt;strong&gt;401&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;detail&quot;: &quot;不正确的身份认证信息。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前用户没有操作权限 - 状态码&lt;strong&gt;403&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;detail&quot;: &quot;您没有执行该操作的权限。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑房地产经理人信息 - &lt;strong&gt;PUT&lt;/strong&gt; &lt;code&gt;/api/agents/{房地产经理人编号}/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：需要登录且拥有管理员权限，用户身份令牌在请求头中提供。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;经理人姓名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tel&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;经理人手机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;servstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;realstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;profstar&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;certificated&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户身份认证令牌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新成功 - 状态码&lt;strong&gt;200&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;agentid&quot;: 1,
    &quot;estates&quot;: [
        {
            &quot;estateid&quot;: 1,
            &quot;name&quot;: &quot;今日家园&quot;,
            &quot;hot&quot;: 20
        },
        {
            &quot;estateid&quot;: 2,
            &quot;name&quot;: &quot;翡翠园&quot;,
            &quot;hot&quot;: 30
        },
        {
            &quot;estateid&quot;: 3,
            &quot;name&quot;: &quot;万科城市花园&quot;,
            &quot;hot&quot;: 22
        }
    ],
    &quot;name&quot;: &quot;袁晓梦&quot;,
    &quot;tel&quot;: &quot;158173555285&quot;,
    &quot;servstar&quot;: 5,
    &quot;realstar&quot;: 4,
    &quot;profstar&quot;: 3,
    &quot;certificated&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;未提供身份认证信息 - 状态码&lt;strong&gt;403&lt;/strong&gt; - 与新增类同&lt;/li&gt;
&lt;li&gt;当前用户没有操作权限 - 状态码&lt;strong&gt;403&lt;/strong&gt; - 与新增类同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除房地产经理人 - &lt;strong&gt;DELETE&lt;/strong&gt; &lt;code&gt;/api/agents/{房地产经理人编号}/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用说明：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;token&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户身份认证令牌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除成功 - 状态码&lt;strong&gt;204&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;未提供身份认证信息 - 状态码&lt;strong&gt;403&lt;/strong&gt; - 与新增类同&lt;/li&gt;
&lt;li&gt;当前用户没有操作权限 - 状态码&lt;strong&gt;403&lt;/strong&gt; - 与新增类同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分页获取楼盘信息 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/estates/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：经理人姓名按照前缀模糊匹配的方式进行处理；经理人服务星级是指经理人服务星级不得低于该星级；经理人是否持证只有0（未持证上岗）和1（持证上岗）两个选项。三个参数代表的筛选条件之间是而且的关系。返回结果为分页之后的房地产经理人信息。&lt;/p&gt;
&lt;p&gt;使用帮助：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;楼盘名（模糊匹配）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dist&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;楼盘所在地区编号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页码，默认值为1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页面大小，默认值为5，最大值不超过50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;count&quot;: 16,
    &quot;next&quot;: &quot;https://120.77.222.217/api/estates/?page=2&quot;,
    &quot;previous&quot;: null,
    &quot;results&quot;: [
        {
            &quot;estateid&quot;: 6,
            &quot;district&quot;: {
                &quot;distid&quot;: 440303,
                &quot;name&quot;: &quot;罗湖区&quot;
            },
            &quot;agents&quot;: [
                {
                    &quot;agentid&quot;: 2,
                    &quot;name&quot;: &quot;杨伟&quot;,
                    &quot;tel&quot;: &quot;13352939550&quot;,
                    &quot;servstar&quot;: 3
                },
                {
                    &quot;agentid&quot;: 4,
                    &quot;name&quot;: &quot;郭志鹏&quot;,
                    &quot;tel&quot;: &quot;13686810707&quot;,
                    &quot;servstar&quot;: 4
                }
            ],
            &quot;name&quot;: &quot;幸福里&quot;,
            &quot;hot&quot;: 300,
            &quot;intro&quot;: &quot;&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增楼盘 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/estates/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：需要登录且拥有管理员权限，用户身份令牌在请求头中提供。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;楼盘名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hot&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;楼盘热度，默认值0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intro&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;楼盘介绍，默认值空字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distid&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;楼盘所在地区编号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户身份认证令牌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新增成功 - 状态码&lt;strong&gt;201&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;estateid&quot;: 17,
    &quot;district&quot;: 510107,
    &quot;name&quot;: &quot;世纪锦苑&quot;,
    &quot;hot&quot;: 100,
    &quot;intro&quot;: &quot;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未提供身份认证信息 - 状态码&lt;strong&gt;403&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;detail&quot;: &quot;请提供有效的身份认证信息&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前用户没有操作权限 - 状态码&lt;strong&gt;403&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;detail&quot;: &quot;You do not have permission to perform this action.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;编辑楼盘信息 - &lt;strong&gt;PUT&lt;/strong&gt; &lt;code&gt;/api/estates/{楼盘编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除楼盘信 - &lt;strong&gt;DELETE&lt;/strong&gt; &lt;code&gt;/api/estates/{楼盘编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取所有户型信息 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/housetypes/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增户型 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/housetypes/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑户型信息 - &lt;strong&gt;PUT&lt;/strong&gt; &lt;code&gt;/api/housetypes/{户型编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除户型 - &lt;strong&gt;DELETE&lt;/strong&gt; &lt;code&gt;/api/housetypes/{户型编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分页获取房源信息 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/houseinfos/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：骆昊&lt;/p&gt;
&lt;p&gt;版本号：v1&lt;/p&gt;
&lt;p&gt;接口说明：暂无。&lt;/p&gt;
&lt;p&gt;使用帮助：暂无。&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;房源标题关键词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dist&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;楼盘所在地区编号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min_price&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;价格区间下限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_price&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;价格区间上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;户型编号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页码，默认值为1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页面大小，默认值为5，最大值不超过50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;count&quot;: 7,
    &quot;next&quot;: &quot;http://localhost:8000/api/houseinfos/?dist=440303&amp;amp;page=2&quot;,
    &quot;previous&quot;: null,
    &quot;results&quot;: [

    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看房源详情 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增房源 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/houseinfos/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑房源信息 - &lt;strong&gt;PUT&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除房源 - &lt;strong&gt;DELETE&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随机获取指定数量的房源标签 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/tags/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分页查看房源标签 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/tags/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增房源标签 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/tags/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除房源标签 - &lt;strong&gt;DELETE&lt;/strong&gt;  &lt;code&gt;/api/tags/{房源编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看房源的图片 - &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}/photos/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为房源添加图片 - &lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}/photos/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除房源图片 - &lt;strong&gt;DELETE&lt;/strong&gt; &lt;code&gt;/api/houseinfos/{房源编号}/photos/{图片编号}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;05 那些年我们踩过的那些坑&lt;/h2&gt;
&lt;h4&gt;坑1 - 整数比较的坑&lt;/h4&gt;
&lt;p&gt;在 Python 中一切都是对象，整数也是对象，在比较两个整数时有两个运算符&lt;code&gt;==&lt;/code&gt;和&lt;code&gt;is&lt;/code&gt;，它们的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;is&lt;/code&gt;比较的是两个整数对象的id值是否相等，也就是比较两个引用是否代表了内存中同一个地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt;比较的是两个整数对象的内容是否相等，使用&lt;code&gt;==&lt;/code&gt;时其实是调用了对象的&lt;code&gt;__eq__()&lt;/code&gt;方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;知道了&lt;code&gt;is&lt;/code&gt;和&lt;code&gt;==&lt;/code&gt;的区别之后，我们可以来看看下面的代码，了解Python中整数比较有哪些坑，&lt;strong&gt;以CPython解释器为例&lt;/strong&gt;，大家先看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
	x = y = -1
	while True:
		x += 1
		y += 1
		if x is y:
			print(&apos;%d is %d&apos; % (x, y))
		else:
			print(&apos;Attention! %d is not %d&apos; % (x, y))
			break
			
	x = y = 0
	while True:
		x -= 1
		y -= 1
		if x is y:
			print(&apos;%d is %d&apos; % (x, y))
		else:
			print(&apos;Attention! %d is not %d&apos; % (x, y))
			break


if __name__ == &apos;__main__&apos;:
	main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码的部分运行结果如下图所示。这个结果是因为CPython出于性能优化的考虑，把频繁使用的整数对象用一个叫&lt;code&gt;small_ints&lt;/code&gt;的对象池缓存起来造成的。&lt;code&gt;small_ints&lt;/code&gt;缓存的整数值被设定为&lt;code&gt;[-5, 256]&lt;/code&gt;这个区间，也就是说，如果使用CPython解释器，在任何引用这些整数的地方，都不需要重新创建&lt;code&gt;int&lt;/code&gt;对象，而是直接引用缓存池中的对象。如果整数不在该范围内，那么即便两个整数的值相同，它们也是不同的对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/871225a2ba.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然仅仅如此这个坑就不值一提了，如果你理解了上面的规则，我们就再看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 257


def main():
	b = 257  # 第6行
	c = 257  # 第7行
	print(b is c)  # True
	print(a is b)  # False
	print(a is c)  # False


if __name__ == &quot;__main__&quot;:
	main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序的执行结果已经用注释写在代码上了。够坑吧！看上去&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;c&lt;/code&gt;的值都是一样的，但是&lt;code&gt;is&lt;/code&gt;运算的结果却不一样。为什么会出现这样的结果，首先我们来说说Python程序中的代码块。所谓代码块是程序的一个最小的基本执行单位，一个模块文件、一个函数体、一个类、交互式命令中的单行代码都叫做一个代码块。上面的代码由两个代码块构成，&lt;code&gt;a = 257&lt;/code&gt;是一个代码块，&lt;code&gt;main&lt;/code&gt;函数是另外一个代码块。CPython底层为了进一步提升性能还做了一个设定：对于同一个代码块中值不在&lt;code&gt;small_ints&lt;/code&gt;缓存范围之内的整数，如果同一个代码块中已经存在一个值与其相同的整数对象，那么就直接引用该对象，否则创建新的&lt;code&gt;int&lt;/code&gt;对象。需要大家注意的是，这条规则对数值型适用，但对字符串则需要考虑字符串的长度，这一点可以自行证明。
为了验证刚刚的结论，我们可以借用&lt;code&gt;dis&lt;/code&gt;模块（听名字就知道是进行反汇编的模块）从字节码的角度来看看这段代码。如果不理解什么是字节码，可以先看看&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/(http:/www.cnblogs.com/restran/p/4903056.html)&quot;&gt;《谈谈 Python 程序的运行原理》&lt;/a&gt;这篇文章。可以先用&lt;code&gt;import dis&lt;/code&gt;导入&lt;code&gt;dis&lt;/code&gt;模块并按照如下所示的方式修改代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import dis

dis.dis(main)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码的执行结果如下图所示。可以看出代码第6行和第7行，也就是&lt;code&gt;main&lt;/code&gt;函数中的257是从同一个位置加载的，因此是同一个对象；而代码第9行的&lt;code&gt;a&lt;/code&gt;明显是从不同的地方加载的，因此引用的是不同的对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/a74c1fda78.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果还想对这个问题进行进一步深挖，推荐大家阅读&lt;a href=&quot;https://foofish.net/python_int_implement.html&quot;&gt;《Python整数对象实现原理》&lt;/a&gt;这篇文章。&lt;/p&gt;
&lt;h4&gt;坑2 - 嵌套列表的坑&lt;/h4&gt;
&lt;p&gt;Python中有一种内置的数据类型叫列表，它是一种容器，可以用来承载其他的对象（准确的说是其他对象的引用），列表中的对象可以称为列表的元素，很明显我们可以把列表作为列表中的元素，这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图（如植物大战僵尸的花园）、棋盘（如国际象棋、黑白棋）等。但是在使用嵌套的列表时要小心，否则很可能遭遇非常尴尬的情况，下面是一个小例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
subjs = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
scores = [[0] * 3] * 5
for row, name in enumerate(names):
    print(&apos;请输入%s的成绩&apos; % name)
    for col, subj in enumerate(subjs):
        scores[row][col] = float(input(subj + &apos;: &apos;))
        print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们希望录入5个学生3门课程的成绩，于是定义了一个有5个元素的列表，而列表中的每个元素又是一个由3个元素构成的列表，这样一个列表的列表刚好跟一个表格是一致的，相当于有5行3列，接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现，每个学生3门课程的成绩是一模一样的，而且就是最后录入的那个学生的成绩。&lt;/p&gt;
&lt;p&gt;要想把这个坑填平，我们首先要区分对象和对象的引用这两个概念，而要区分这两个概念，还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词，但实际上“堆”和“栈”是两个不同的概念。众所周知，一个程序运行时需要占用一些内存空间来存储数据和代码，那么这些内存从逻辑上又可以做进一步的划分。对底层语言（如C语言）有所了解的程序员大都知道，程序中可以使用的内存从逻辑上可以为五个部分，按照地址从高到低依次是：栈（stack）、堆（heap）、数据段（data segment）、只读数据段（static area）和代码段（code segment）。其中，栈用来存储局部、临时变量，以及函数调用时保存现场和恢复现场需要用到的数据，这部分内存在代码块开始执行时自动分配，代码块执行结束时自动释放，通常由编译器自动管理；堆的大小不固定，可以动态的分配和回收，因此如果程序中有大量的数据需要处理，这些数据通常都放在堆上，如果堆空间没有正确的被释放会引发内存泄露的问题，而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理（自动回收不再使用的堆空间）。所以下面的代码中，变量&lt;code&gt;a&lt;/code&gt;并不是真正的对象，它是对象的引用，相当于记录了对象在堆空间的地址，通过这个地址我们可以访问到对应的对象；同理，变量&lt;code&gt;b&lt;/code&gt;是列表容器的引用，它引用了堆空间上的列表容器，而列表容器中并没有保存真正的对象，它保存的也仅仅是对象的引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = object()
b = [&apos;apple&apos;, &apos;pitaya&apos;, &apos;grape&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;知道了这一点，我们可以回过头看看刚才的程序，我们对列表进行&lt;code&gt;[[0] * 3] * 5&lt;/code&gt;操作时，仅仅是将&lt;code&gt;[0, 0, 0]&lt;/code&gt;这个列表的地址进行了复制，并没有创建新的列表对象，所以容器中虽然有5个元素，但是这5个元素引用了同一个列表对象，这一点可以通过&lt;code&gt;id&lt;/code&gt;函数检查&lt;code&gt;scores[0]&lt;/code&gt;和&lt;code&gt;scores[1]&lt;/code&gt;的地址得到证实。所以正确的代码应该按照如下的方式进行修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
subjs = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
scores = [[]] * 5
for row, name in enumerate(names):
    print(&apos;请输入%s的成绩&apos; % name)
    scores[row] = [0] * 3
    for col, subj in enumerate(subjs):
        scores[row][col] = float(input(subj + &apos;: &apos;))
        print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
subjs = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
scores = [[0] * 3 for _ in range(5)]
for row, name in enumerate(names):
    print(&apos;请输入%s的成绩&apos; % name)
    scores[row] = [0] * 3
    for col, subj in enumerate(subjs):
        scores[row][col] = float(input(subj + &apos;: &apos;))
        print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果对内存的使用不是很理解，可以看看&lt;a href=&quot;http://www.pythontutor.com/&quot;&gt;PythonTutor网站&lt;/a&gt;上提供的代码可视化执行功能，通过可视化执行，我们可以看到内存是如何分配的，从而避免在使用嵌套列表或者复制对象时可能遇到的坑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/433c40231d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/ecfb72d0fa.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;坑3 - 访问修饰符的坑&lt;/h4&gt;
&lt;p&gt;用Python做过面向对象编程的人都知道，Python的类提供了两种访问控制权限，一种是公开，一种是私有（在属性或方法前加上双下划线）。而用惯了Java或C#这类编程语言的人都知道，类中的属性（数据抽象）通常都是私有的，其目的是为了将数据保护起来；而类中的方法（行为抽象）通常都是公开的，因为方法是对象向外界提供的服务。但是Python并没有从语法层面确保私有成员的私密性，因为它只是对类中所谓的私有成员进行了命名的变换，如果知道命名的规则照样可以直接访问私有成员，请看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student(object):

    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def __str__(self):
        return self.__name + &apos;: &apos; + str(self.__age)


stu = Student(&apos;骆昊&apos;, 38)
print(stu._Student__name)
print(stu._Student__age)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python为什么要做出这样的设定呢？用一句广为流传的格言来解释这个问题：“We are all consenting adults here”（我们都是成年人）。这句话表达了很多Python程序员的一个共同观点，那就是开放比封闭要好，我们应该自己对自己的行为负责而不是从语言层面来限制对数据或方法的访问。&lt;/p&gt;
&lt;p&gt;所以在Python中我们实在没有必要将类中的属性或方法用双下划线开头的命名处理成私有的成员，因为这并没有任何实际的意义。如果想对属性或方法进行保护，我们建议用单下划线开头的受保护成员，虽然它也不能真正保护这些属性或方法，但是它相当于给调用者一个暗示，让调用者知道这是不应该直接访问的属性或方法，而且这样做并不影响子类去继承这些东西。&lt;/p&gt;
&lt;p&gt;需要提醒大家注意的是，Python类中的那些魔法方法，如&lt;code&gt;__str__&lt;/code&gt;、&lt;code&gt;__repr__&lt;/code&gt;等，这些方法并不是私有成员哦，虽然它们以双下划线开头，但是他们也是以双下划线结尾的，这种命名并不是私有成员的命名，这一点对初学者来说真的很坑。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;06 如何快速驾驭 pandas 库&lt;/h2&gt;
&lt;p&gt;最近有小伙伴提到，Python 做数据分析的 pandas 库函数和方法实在太多，感觉学习和使用起来思路都非常混乱。之前回复过这个问题，今天把它更系统的整理一下，毕竟这个库是 Python 数据科学生态圈中扮演着极为重要的角色，虽然目前有很多 pandas 库的替代品（如：polars、cuDF等），但是使用方法跟 pandas 可以说是大同小异。&lt;/p&gt;
&lt;h4&gt;三个核心类&lt;/h4&gt;
&lt;p&gt;Pandas 库有三个最核心的类，其中最重要的是&lt;code&gt;DataFrame&lt;/code&gt;类型，它是学习的重点，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/e0f651c852.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Series&lt;/code&gt;：表示一维数据，跟一维数组类似（带标签的数组），每个数据都有自己的索引（标签），可以通过索引访问数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DataFrame&lt;/code&gt;：表示二维数据，类似于 Excel 电子表格，行和列都有自己的索引（标签），可以通过索引访问行、列、单元格。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Index&lt;/code&gt;：表示索引，为&lt;code&gt;Series&lt;/code&gt;和&lt;code&gt;DataFrame&lt;/code&gt; 提供索引服务，&lt;code&gt;Index&lt;/code&gt;有很多的子类型，适用于需要不同类型的索引的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据分析流程&lt;/h4&gt;
&lt;p&gt;学习和使用 pandas 重点是&lt;code&gt;DataFrame&lt;/code&gt;的应用，我们建议大家按照数据分析的流程来掌握对应的函数和方法，这样做往往会事半功倍。数据分析流程如下图所示，其中蓝色虚线圈中的部分就是可以通过 BI 工具（如：Power BI、Tableau等）或 Python 程序来完成的部分。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/1d814106b1.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;数据获取&lt;/h5&gt;
&lt;p&gt;数据获取也可以称为数据加载，其本质就是创建&lt;code&gt;DataFrame&lt;/code&gt;对象，需要掌握以下几个函数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 CSV 文件加载数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.read_csv(
    filepath,      # CSV文件路径（可以本地绝对路径或相对路径，也可以是一个URL）
    sep,           # 字段分隔符（默认是逗号）
    header,        # 表头在第几行
    encoding,      # 文件编码（默认utf-8）
    quotechar,     # 包裹字符串的符号（默认是双引号）
    usecols,       # 加载哪些列
    index_col,     # 指定索引列
    dtype,         # 指定列的数据类型
    converters,    # 指定列的数据转换器
    nrows,         # 加载多少行数据
    skiprows,      # 指定需要跳过的行
    parse_dates,   # 将哪些列解析为日期时间
    date_format,   # 日期格式
    true_values,   # 被视为布尔值True的值
    false_values,  # 被视为布尔值False的值
    na_values,     # 被视为空值的值
    na_filter,     # 是否检测空值标记
    on_bad_lines,  # 遇到有问题的行如何处理（可选项：&apos;error&apos;、&apos;warn&apos;、&apos;skip&apos;）
    engine,        # 指定底层引擎（例如：可以使用更快的Arrow引擎来处理体量更大的数据）
    iterator,      # 是否开启迭代器模式（处理大数据时减少内存开销）
    chunksize,     # 迭代器模式下每次加载数量的体量
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;从 Excel 文件加载数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.read_excel(
    io,           # 工作簿文件的路径
    sheet_name,   # 工作表的名字
    skip_footer,  # 跳过末尾多少行
)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;read_excel&lt;/code&gt;函数跟&lt;code&gt;read_csv&lt;/code&gt;有很多作用相同的参数，这里就没有赘述了。从 Excel 文件中加载数据时，没有迭代器模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;从数据库或数仓加载数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.read_sql(
    sql,          # SQL查询或二维表的名字
    con,          # 数据库连接
    parse_dates,  # 指定需要解析成日期的列
    index_col,    # 指定索隐裂
    columns,      # 需要加载的列
    chunksize,    # 加载数据的体量
    dtype,        # 指定列的数据类型
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;其他创建&lt;code&gt;DataFrame&lt;/code&gt;对象的方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.DataFrame(data=[[95, 87], [66, 78], [92, 89]], index=[1001, 1002, 1003], columns=[&apos;Verbal&apos;, &apos;Math&apos;])
pd.DataFrame(data={&apos;Verbal&apos;: [95, 66, 92], &apos;Math&apos;: [87, 78, 89]}, index=[1001, 1002, 1003])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要对&lt;code&gt;DataFrame&lt;/code&gt;中的数据或索引进行操作，需要掌握下面的运算和方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查看信息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看前/后 N 行&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.head(10)
df.tail(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;操作列&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df[&apos;column_name&apos;]
df.colume_name
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;操作行&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.loc[&apos;row_index&apos;]
df.iloc[0]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;操作单元格&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.at[&apos;row_index&apos;, &apos;column_name&apos;]
df.iat[0, 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;删除行或列&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.drop(
    labels,   # 要删除的行或列的索引
    axis,     # axis=0，labels表示行索引；axis=1，labels表示列索引
    index,    # 要删除的行的索引
    columns,  # 要删除的列的索引
    inplace,  # 是否就地删除（inplace=True，表示就地删除不返回新DataFrame对象）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;筛选数据&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.query(expr)  # 通过表达式指定筛选条件
df[bool_index]  # 布尔索引
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;随机抽样&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.sampe(
    n,             # 样本容量
    frac,          # 抽样比例
    replace,       # 有放回或无放回抽样（默认值False）
    random_state,  # 随机数种子（种子相同每次抽样的结果相同）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;重置索引&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.reset_index(
    level,    # 对于多级索引指定重置哪一级的索引
    drop,     # 是否丢弃索引（drop=False表示索引会被处理成普通列）
    inplace,  # 是否就地处理（要不要返回新的DataFrame对象）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;设置索引&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.set_index(
   keys,              # 指定作为索引的列
   drop,              # 是否删除作为索引的列（默认值True）
   append,            # 是否将指定列加入现有的索引（默认值False）
   inplace,           # 是否就地处理（要不要返回新的DataFrame对象）
   verify_integrity,  # 检查索引列是否存在重复值（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;调整索引顺序&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.reindex()
df[fancy_index]       # 花式索引
df.loc[facy_index]    # 花式索引
df.iloc[fancy_index]  # 花式索引
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;索引排序&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.sort_index(
    axis,         # 确定行索引或列索引（默认值0）
    level,        # 对于多级索引指定索引的级别
    ascending,    # 升序或降序（默认值True）
    inplace,      # 是否就地排序
    kind,         # 排序算法（默认值&apos;quicksort&apos;）
    na_position,  # 空值放在最前还是最后（默认值&apos;last&apos;）
    key,          # 传入比较索引大小的函数（自定义比较规则）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据重塑&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;拼接（类似于 SQL 中的 union 操作）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.concat(
    objs,          # 保存多个DataFrame对象的容器
    axis,          # 沿着哪个轴进行拼接
    ignore_index,  # 是否忽略原来的索引（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;合并（类似于 SQL 中的 join 操作）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.merge(
    left,         # 左表
    right,        # 右表
    how,          # 指定连表的方式（默认值&apos;inner&apos;表示内连接）
    on,           # 指定连表字段（如果左右两表连表字段同名）
    left_on,      # 指定左表的连表字段
    right_on,     # 指定右表的连表字段
    left_index,   # 是否使用左表的索引连表
    right_index,  # 是否使用右表的索引连表
    suffixes,     # 指定同名列的后缀（默认值(&apos;_x&apos;, &apos;_y&apos;)）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据清洗&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;缺失值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 甄别缺失值
df.isna()
df.notna()
# 删除缺失值
df.dropna(
    axis,     # 删行或删列（默认值0）
    how,      # 是否存在任意一个缺失值就删除（默认值&apos;any&apos;）
    subset,   # 只对哪些行或列删除空值
    inplace,  # 是否就地删除（要不要返回新的DataFrame对象）
)
# 填充缺失值
df.fillna(
    value,    # 填充的值
    method,   # 填充空值的方法
    inplace,  # 是否就地填充（要不要返回新的DataFrame对象）
)
# 使用插值算法插值
df.interpolate(
    method,   # 插值算法（默认值&apos;linear&apos;表示线性插值法） 
    axis,     # 沿着哪个轴插值
    inplace,  # 是否就地插值（要不要返回新的DataFrame对象）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;重复值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 甄别重复值
df.duplicated(
    subset,   # 用于判断重复的列标签
    keep,     # 如何处理重复项（默认值&apos;first&apos;表示保留第一项）
)
# 删除重复值
df.drop_duplicates(
    subset,   # 用于判断重复的列标签
    keep,     # 如何处理重复项（默认值&apos;first&apos;表示保留第一项）
    inplace,  # 是否就地去重（默认值False）
)
# 统计非重复值
df.nunique(axis)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;异常值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;异常值的处理重点在甄别，可以使用数值判定法、z-score 判定法、孤立森林等方法来进行甄别离群点，然后结合实际业务意义判定是不是异常值。对于异常值的处理，通常是替换或删除，删除可以用之前提到的&lt;code&gt;drop&lt;/code&gt;方法删行或者删列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 替换异常值
df.replace(
    to_replace,  # 被替换的值
    value,       # 替换的值
    inplace,     # 是否就地替换（要不要返回新的DataFrame对象）
    regex,       # 是否启动正则表达式替换（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;预处理&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;预处理通常在&lt;code&gt;Series&lt;/code&gt;对象上对数据进行操作，假设变量&lt;code&gt;s&lt;/code&gt;是一个&lt;code&gt;Series&lt;/code&gt;对象，具体的操作包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日期时间预处理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;s.dt.year                   # 年
s.dt.quarter                # 季度
s.dt.month                  # 月
s.dt.day                    # 日
s.dt.hour                   # 时
s.dt.minute                 # 分
s.dt.second                 # 秒
s.dt.weekday                # 星期几
s.dt.to_period(freq)        # 以特定频率转换
s.dt.floor(freq)            # 下取整
s.dt.ceil(freq)             # 上取整
s.dt.round(freq)            # 舍入
s.dt.strftime(date_format)  # 格式化
s.dt.tz_localize(tz)        # 时区本地化
s.dt.tz_convert(tz)         # 转换时区
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;字符串预处理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;s.str.lower()       # 字符串变小写
s.str.upper()       # 字符串变大写
s.str.title()       # 字符串首字母大写
# 字符串拆分
s.str.split(
    pat,            # 拆分字符或正则表达式
    n,              # 最大拆分次数
    expand,         # 是否将拆分后的内容展开成多个列（默认值False）
)
# 从字符串中捕获内容
s.str.extract(
    pat,            # 正则表达式
    flags,          # 正则表达式处理标记
    expand,         # 是否将捕获内容展开成多个列（默认值True）
)
s.str.isalpha()     # 检查字符串是不是字母
s.str.isnumeric()   # 检查字符串是不是数值
s.str.isalnum()     # 检查字符串是不是字母数字
s.str.isspace()     # 检查字符串是不是空白字符
s.str.startswith()  # 检查字符串是否以指定内容开头 
s.str.endswith()    # 检查字符串是否以指定内容结尾
# 检查字符串是否跟正则表达式匹配
s.str.match(
    pat,            # 正则表达式
    flags,          # 正则表达式处理标记
)
# 检查字符串是否包含指定内容
s.str.contains(
    pat,            # 字符串或正则表达式
    flags,          # 正则表达式处理标记
    regex,          # 是否使用正则表达式（默认值True）
)
# 替换
s.str.replace(
    pat,            # 被替换的内容（字符串或正则表达式）
    repl,           # 替换的内容
    n,              # 最大替换次数（默认值-1表示全部替换）
    flags,          # 正则表达式处理标记
    regex,          # 是否使用正则表达式（默认值True）
)
s.str.strip()       # 去掉字符串多余的空格
s.str.join(sep)     # 用指定的分隔符将内容拼接成字符串
# 字符串拼接
s.str.cat(
    others,         # 拼接的内容
    sep,            # 分隔符
    na_rep,         # 空值的替代符
)
s.str.len()         # 获得字符串长度
# 查找子串的位置
s.str.find(
    sub,            # 子串
    start,          # 起始位置
    end,            # 结束位置
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;类别预处理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 类别重排序
s.cat.reorder_categories(
    new_categories,  # 新的类别顺序
    inplace,         # 是否就地处理（默认值False）
)
# 添加类别
s.cat.add_categories(
    new_categories,  # 要添加的新类别
    inplace,         # 是否就地处理（默认值False）
)
# 移除类别
s.cat.remove_categories(
    removals,        # 要移除的类别
    inplace,         # 是否就地处理（默认值False）
)
# 移除没有使用的类别
s.cat.remove_unused_categories(
    inplace,         # 是否就地处理（默认值False）
)
# 类别重命名
s.cat.rename_categories(
    new_categories,  # 新的类别名称
    inplace,         # 是否就地处理（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;二值化（虚拟变量）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pd.get_dummies(
    data,        # 需要转换为虚拟变量的Series或DataFrame
    prefix,      # 指定生成的虚拟变量列的前缀
    prefix_sep,  # 前缀和列名之间的分隔符
    dummy_na,    # 是否为空值（NaN）生成一个列（默认值False）
    columns,     # 指定要转换的列名
    drop_first,  # 是否从生成的虚拟变量中删除第一个类别的列（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;离散化（分箱）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pd.cut(
    x,        # 要分割的输入数据（一维数据）
    bins,     # 分割的区间数或具体的区间边界
    right,    # 区间是否包含右端点（默认值False）
    labels,   # 指定每个区间的标签
    retbins,  # 是否返回分割的边界数组（默认值False）
    ordered,  # 返回的类别是否是有序的（默认值True）
)
pd.qcut(
    x,        # 要分割的输入数据（一维数据）
    q,        # 分割点的数量或具体的分位数
    labels,   # 指定每个区间的标签
    retbins,  # 是否返回分割的边界数组（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自定义转换&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;s.map(arg)          # 对数据进行元素级别的转换和映射
df.map(func)        # 对数据进行元素级别的转换和映射
# 通过指定函数对数据进行元素级别的转换
s.apply(
    func,           # 作用于每个元素的函数
    convert_type,   # 尝试将结果转换为最适合的类型（默认值True）
    args,           # 传递给func的额外位置参数
    kwargs,         # 传递给func的额外关键字参数
)   
# 通过指定函数对数据进行行级或列级的转换
df.apply(
    func,           # 作用域行或列的函数
    axis,           # 控制做行级还是列级转换
    result_type,    # 指定返回的类型（&apos;expand&apos;表示扩展为列，&apos;reduce&apos;表示返回标量，&apos;broadcast&apos;表示广播为原始形状）
    args,           # 传递给func的额外位置参数
    kwargs,         # 传递给func的额外关键字参数
)
s.transform(func)   # 通过指定一个或多个函数对数据进行元素级别的转换
df.transform(func)  # 通过指定一个或多个函数对数据进行行级或列级转换
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据透视&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;描述性统计信息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;s.mean()     # 均值
s.median()   # 中位数
s.mode()     # 众数
s.max()      # 最大值
s.min()      # 最小值
s.var(ddof)  # 方差（ddof代表自由度校正值）
s.std(ddof)  # 标准差（ddof代表自由度校正值）
s.skew()     # 偏态系数
s.kurt()     # 峰度系数
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;相关性分析&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.cov()         # 协方差
df.corr(method)  # 相关系数（默认&apos;pearson&apos;表示皮尔逊相关系数，可选值还有&apos;kendall&apos;和&apos;spearman&apos;）
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;排序和头部值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 排序
s.sort_values(
    asending,     # 升序或降序（默认值True）
    inplace,      # 是否就地排序（默认值False）
    kind,         # 排序算法（默认值&apos;quicksort&apos;）
    na_position,  # 空值的位置（默认值&apos;last&apos;）
    key,          # 指定比较元素的规则（函数）
)
# 排序
df.sort_values(
    by,           # 排序的依据
    ascending,    # 升序或降序（默认值True）
    inplace,      # 是否就地排序（默认值False）
    kind,         # 排序算法（默认值&apos;quicksort&apos;）
    na_position,  # 空值的位置（默认值&apos;last&apos;）
    key,          # 指定比较元素的规则（函数）
)
# TopN元素（头部）
s.nlargest(
    n,            # 前N个最大值
    keep,         # 如何处理重复值（默认值&apos;first&apos;）
)
# TopN元素（头部）
df.nlargest(
    n,            # 前N个最大值
    columns,      # 指定用于排序的列名
    keep,         # 如何处理重复值（默认值&apos;first&apos;）
)
# TopN元素（尾部）
s.nsmallest(
    n,            # 前N个最小值
    keep,         # 如何处理重复值（默认值&apos;first&apos;）
)
# TopN元素（尾部）
df.nsmallest(
    n,            # 前N个最小值
    columns,      # 指定用于排序的列名
    keep,         # 如何处理重复值（默认值&apos;first&apos;）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;分组聚合&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(
    by,          # 指定用于分组的列名
    level,       # 对于多级索引指定用哪一级分组
    as_index,    # 是否将分组的列设置为索引（默认值True）
    sort,        # 是否对分组的结果进行排序（默认值True）
    observed,    # 只考虑在数据中实际出现的分组（默认值False）
).aggregate(
    func,        # 单个函数或函数列表
    args,       # 函数的可变参数
    kwargs,    # 函数的关键字参数
)
df.pivot(
    index,       # 指定用作索引的列
    columns,     # 要作为新列的列 
    values,      # 用于填充新DataFrame中的值的列
)
df.melt(
    id_vars,     # 在转换过程中保持不变的列
    value_vars,  # 要转换为行的列
    var_name,    # 指定存储原列名的新列名
    value_name,  # 指定存储原数据值的新列名
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;透视表&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.pivot_table(
    data,          # DataFrame对象
    values,        # 需要聚合的列
    index,         # 分组数据的字段（行索引）
    columns,       # 分组数据的字段（列索引）
    aggfunc,       # 聚合函数（默认值&apos;mean&apos;）
    fill_value,    # 填充空值的值
    margins,       # 是否计算行列总计（默认值False）
    margins_name,  # 总计列的名字（默认值&apos;All&apos;）
    observed       # 只考虑在数据中实际出现的分组（默认值False）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;交叉表&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pd.crosstab(
    index,         # 交叉表中的行变量
    columns,       # 交叉表中的列变量
    values,        # 用于填充交叉表的值（可选项）
    aggfunc,       # 聚合函数（可选项）
    margins,       # 是否计算行列总计（默认值False）
    margins_name,  # 总计列的名字（默认值&apos;All&apos;）
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据呈现&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;df.plot(
    figsize,   # 图表尺寸（二元组）
    kind,      # 图表类型
    ax,        # 绘图的坐标系
    x,         # 横轴数据
    y,         # 纵轴数据
    title,     # 图表标题
    grid,      # 是否绘制网格
    legend,    # 是否显示图例
    xticks,    # 横轴刻度
    yticks,    # 纵轴刻度
    xlim,      # 横轴取值范围
    ylim,      # 纵轴取值范围
    xlabel,    # 横轴标签
    ylabel,    # 纵轴标签
    rot,       # 轴标签旋转角度
    fontsize,  # 轴标签字体大小
    colormap,  # 颜色系列
    stacked,   # 是否绘制堆叠图（默认值False）
    colorbar,  # 是否显示色彩条
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;plot&lt;/code&gt;方法最重要的参数是&lt;code&gt;kind&lt;/code&gt;，它可以控制图表的类型，具体如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;折线图：&lt;code&gt;kind=&apos;line&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;散点图：&lt;code&gt;kind=&apos;scatter&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;柱状图：&lt;code&gt;kind=&apos;bar&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;条状图（水平柱状图）：&lt;code&gt;kind=&apos;barh&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;饼状图：&lt;code&gt;kind=&apos;pie&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;直方图：&lt;code&gt;kind=&apos;hist&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;箱线图：&lt;code&gt;kind=&apos;box&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;面积图：&lt;code&gt;kind=&apos;area&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;核密度估计图：&lt;code&gt;kind=&apos;kde&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;大家可以找一个数据集按照上面讲解的流程把这些最常用的类型、函数和方法过一遍，是不是印象就深刻一点了。更详细的内容还是推荐阅读我的专栏&lt;a href=&quot;https://www.zhihu.com/column/c_1217746527315496960&quot;&gt;《基于Python的数据分析》&lt;/a&gt;或者观看B站上的视频&lt;a href=&quot;https://www.bilibili.com/video/BV13t4y1a7TV/&quot;&gt;《Python数据分析三剑客》&lt;/a&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;07 使用Hexo搭建自己的博客&lt;/h2&gt;
&lt;p&gt;对于一个程序员来说，搭建一个属于自己的博客平台是非常有意义的事情。首先，博客可以记录自己的成长历程，也是对自己一段时间学习和工作的总结和沉淀；其他，通过博客可以营销自己，增强自己在互联网或行业内的影响力，为将来更好的职业生涯打一个坚实的基础。前几年有一本名为《软技能 - 代码之外的生存指南》的畅销书，我记得书中有这么一段话：“流行乐队的音乐才华可能并不比夜店驻场乐队高多少，他们为什么就可以在全世界巡回演出，创造一个又一个白金记录？……你的营销做得越好，你的才华才能表现得淋漓尽致。”&lt;/p&gt;
&lt;p&gt;这里顺便啰嗦两句，在互联网如此发达的今天，我们应该如何营销自己呢？自我营销首先要从打造个人品牌做起，对于程序员来说，最容易去做好的一件事情还是搭建自己的博客。博客相当于是你在互联网上的一个基地，尤其是当你拥有了属于自己的独立博客后，你可以做很多自己想做的事情，既可以传达自己的思想，又可以提升自己的影响力，当然如果你的博客经营得非常好，你可以从中获利。当然，除了博客，直播、视频网站、投稿、写书、技术活动都是可选的自我营销方式。当然，做自我营销也需要持之以恒，三天打鱼两天晒网是很难有什么收获的。&lt;/p&gt;
&lt;h4&gt;Hexo概述&lt;/h4&gt;
&lt;p&gt;Hexo 是一个快速、简洁且高效的博客框架，它能够将 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/zh.wikipedia.org/zh-hans/Markdown%3E&quot;&gt;Markdown&lt;/a&gt; 文档渲染成漂亮的网页，这样我们就可以在很短的时间内迅速的创建出网站的静态内容，而 Markdown 格式对程序员来说相信并不陌生。要想使用Hexo来搭建自己的博客，我实在想不出有什么教程比&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/hexo.io/zh-cn/%3E&quot;&gt;官方文档&lt;/a&gt;更好，强烈建议大家阅读官方文档来了解 Hexo，下面我只做一个简要的使用说明。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：**Markdown **是一种轻量级标记语言，它允许人们使用易读易写的纯文本格式编写文档，同时也对图片、图表、数学公式提供了支持，可以用来书写电子书、软件文档等，同时也可以非常方便的转换为 HTML 页面或者是 PDF 文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要使用 Hexo，首先得确保计算机上已经安装了 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/nodejs.org/en/%3E&quot;&gt;node.js&lt;/a&gt; 环境和 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/git-scm.com/%3E&quot;&gt;git&lt;/a&gt; 环境，前者是一个能够在服务器端运行 JavaScript 代码的环境，后者是版本控制工具。安装 node.js 主要是为了使用它的包管理工具 npm，所以不需要先系统的学习 node.js 的知识；而安装 git 是为了利用版本控制系统克隆代码已经将博客项目托管到三方平台，如果想学习 git，最好的资料是官方网站上的 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/git-scm.com/book/zh/v2%3E&quot;&gt;&lt;em&gt;Git Pro&lt;/em&gt;&lt;/a&gt; 和&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttp:/www.worldhello.net/gotgit/index.html%3E&quot;&gt;《Git权威指南》&lt;/a&gt;。安装完成后，我们可以通过下面的命令来确认 node.js 环境以及它的包管理工具是否安装成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node --version
npm --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过下面的命令来检查是否安装了 git 环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用 npm 来安装 Hexo，npm 是 node.js 的包管理工具，跟 Python 的 pip 工具作用一样，可以用它来安装依赖库和三方工具。在第一次使用 npm 的时候，我们可以先将 npm 的下载源更换为国内的淘宝镜像，这样下载的速度会有非常显著的提升。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm config set registry https://registry.npm.taobao.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们就通过 npm 来安装 Hexo，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g hexo-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装成功后，就可以使用 Hexo 来创建属于自己的博客啦。&lt;/p&gt;
&lt;h4&gt;搭建博客&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：以下内容基本上来自于 Hexo 的官方文档，推荐大家阅读官方文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们先通过下面的命令来创建一个专门保存博客项目的文件夹，该命令会从 github 上克隆博客项目和默认的主题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo init blog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们进入这个文件夹并查看目录结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd blog
ls -lR
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;total 232
-rw-r--r--    1 Hao  staff    1768  8  8 01:15 _config.yml
drwxr-xr-x  274 Hao  staff    8768  8  8 01:19 node_modules
-rw-r--r--    1 Hao  staff  109972  8  8 01:19 package-lock.json
-rw-r--r--    1 Hao  staff     443  8  8 01:15 package.json
drwxr-xr-x    5 Hao  staff     160  8  8 01:15 scaffolds
drwxr-xr-x    3 Hao  staff      96  8  8 01:15 source
drwxr-xr-x    3 Hao  staff      96  8  8 01:15 themes
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：Windows 环境命令行提示符中可以使用&lt;code&gt;dir&lt;/code&gt;命令查看目录结构。需要说明的是：&lt;code&gt;_config.yml&lt;/code&gt;是博客项目的配置文件；&lt;code&gt;package.json&lt;/code&gt;是项目的依赖项文件；&lt;code&gt;scaffolds&lt;/code&gt;保存了 Markdown 文件的模板，也就是向新添加的 Markdown 文件中默认填充的内容；&lt;code&gt;source&lt;/code&gt;目录下有一个名为&lt;code&gt;_post&lt;/code&gt;的目录，我们稍后可以将编写好的 Markdown 文件放到该目录，这样就可以利用 Hexo 将 Markdown 文件处理成博客的静态页面，生成的静态页面将置于&lt;code&gt;public&lt;/code&gt;目录下；&lt;code&gt;themes&lt;/code&gt;文件夹保存了博客使用的主题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后我们通过下面的命令来安装项目所需的依赖项（&lt;code&gt;package.json&lt;/code&gt;文件指明了这些依赖项）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;做完上述的操作以后，我们已经可以直接通过下面的命令来生成博客。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令也可以缩写为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之前我们在安装依赖项的时候包括了一个名为&lt;code&gt;hexo-server&lt;/code&gt;的依赖项，该依赖项可以帮助我们启动一个基于 node.js 的服务器来运行我们的博客项目，使用下面的命令即可启动服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令也可以缩写为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INFO  Start processing
INFO  Hexo is running at http://localhost:4000 . Press Ctrl+C to stop.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从运行命令的提示信息可以看到，服务器已经运转起并使用了 4000 端口，可以通过&lt;code&gt;Ctrl+C&lt;/code&gt;来终止服务器的运行。如果要修改服务器使用的端口，可以在启动服务器的时候加上&lt;code&gt;-p&lt;/code&gt;参数；如果希望服务器启动后，自动打开默认的浏览器访问服务器，可以使用&lt;code&gt;-o&lt;/code&gt;参数，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo s -p 8000 -o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们已经可以看到 Hexo 在没有配置也没有加入自己的 Markdown 文件下生成的首页，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/755ef02780.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们修改博客的配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim _config.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Hexo Configuration
## Docs: https://hexo.io/docs/configuration.html
## Source: https://github.com/hexojs/hexo/

# Site
title: 骆昊的技术专栏
subtitle: 传道、授业、解惑，分享知识带来的快乐
description:
keywords:
author: 骆昊
language: zh
timezone:

# URL
## If your site is put in a subdirectory, set url as &apos;http://yoursite.com/child&apos; and root as &apos;/child/&apos;
url: http://jackfrued.top
root: /
permalink: :year/:month/:day/:title/
permalink_defaults:

# Directory
source_dir: source
public_dir: public
tag_dir: tags
archive_dir: archives
category_dir: categories
code_dir: downloads/code
i18n_dir: :lang
skip_render:

# Writing
new_post_name: :title.md # File name of new posts
default_layout: post
titlecase: false # Transform title into titlecase
external_link: true # Open external links in new tab
filename_case: 0
render_drafts: false
post_asset_folder: false
relative_link: false
future: true
highlight:
  enable: true
  line_number: true
  auto_detect: false
  tab_replace:
  
# Home page setting
# path: Root path for your blogs index page. (default = &apos;&apos;)
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
  path: &apos;&apos;
  per_page: 10
  order_by: -date
  
# Category &amp;amp; Tag
default_category: uncategorized
category_map:
tag_map:

# Date / Time format
## Hexo uses Moment.js to parse and display date
## You can customize the date format as defined in
## http://momentjs.com/docs/#/displaying/format/
date_format: YYYY-MM-DD
time_format: HH:mm:ss

# Pagination
## Set per_page to 0 to disable pagination
per_page: 10
pagination_dir: page

# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: landscape

# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
  type:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 YAML 文件中相关选项的说明。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站的标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subtitle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站的副标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站的描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keywords&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站的关键词，可以用逗号分隔多个关键词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;author&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自己的名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站使用的语言&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timezone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站使用时区，默认使用电脑上设置的时区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网站根目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;资源文件夹，这个文件夹用来存放内容，默认 source 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;公共文件夹，这个文件夹用于存放生成的站点文件，默认 public 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tag_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标签文件夹，默认 tags 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;archive_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;归档文件夹，默认 archives 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分类文件夹，默认 categories 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auto_spacing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在中文和英文之间加入空格，默认 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;titlecase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把标题转换为首字母大写，默认 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;external_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在新标签中打开链接，默认 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;relative_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把链接改为与根目录的相对位址，默认 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default_category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认分类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日期格式，默认 YYYY-MM-DD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;时间格式，默认 HH:mm:ss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;per_page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每页显示的文章数量 ，默认值 10，0 表示不使用分页&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pagination_dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分页目录，默认为 page 目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前主题名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;部署部分的设置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们将编写好的 Markdown 文件可以拷贝到&lt;code&gt;source/_posts&lt;/code&gt;目录，我们可以在每个 Markdown 文件的上方添加 Front-matter 来对文件的布局、标题、分类、标签、发布日期等信息加以说明。所谓 Front-matter，就是每个 Markdown 文件最上方以&lt;code&gt;---&lt;/code&gt;分隔的区域，可以在Front-matter中设置以下内容。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;布局&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标题&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建立日期&lt;/td&gt;
&lt;td&gt;文件建立日期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新日期&lt;/td&gt;
&lt;td&gt;文件更新日期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;开启文章的评论功能&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标签（不适用于分页）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;categories&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分类（不适用于分页）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permalink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;覆盖文章网址&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Python编程惯例
categories: 
- Python基础
tags:
- Python
- PEP8
date: 2019-8-1
---
## Python惯例

“惯例”这个词指的是“习惯的做法，常规的办法，一贯的做法”，与这个词对应的英文单词叫“idiom”。由于Python跟其他很多编程语言在语法和使用上还是有比较显著的差别，因此作为一个Python开发者如果不能掌握这些惯例，就无法写出“Pythonic”的代码。下面我们总结了一些在Python开发中的惯用的代码。

1. 让代码既可以被导入又可以被执行。
   if __name__ == &apos;__main__&apos;:

2. 用下面的方式判断逻辑“真”或“假”。
   if x:
   if not x:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完成上述工作后，我们可以通过下面的命令先清理之前生成的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们就可以用之前讲过的命令重新生成并运行博客项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo generate
hexo server -p 8000 -o
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;将博客托管到GitHub&lt;/h4&gt;
&lt;p&gt;我们可以利用 GitHub 网站提供的 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/pages.github.com/%3E&quot;&gt;Pages&lt;/a&gt; 服务来托管我们的博客。在 GitHub Pages 的首页上就有一个教程指导我们如何托管自己的网站，当然第一步得在 GitHub 上注册一个属于自己的账号，登录成功之后才能进行后续的操作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据自己的用户名创建一个仓库，仓库&lt;strong&gt;一定&lt;/strong&gt;要取名为“用户名.github.io”。例如：我在 GitHub 上的用户名是 jackfrued，那么我的托管博客项目的仓库一定要命名为 jackfrued.github.io。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/18a88d7475.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改博客项目的配置文件&lt;code&gt;_config.yml&lt;/code&gt;，配置使用 GitHub 来部署该博客项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim _config.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 省略上面的内容
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
  type: git
  repo: https://github.com/jackfrued/jackfrued.github.io.git
  branch: master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的配置中，type 指定了使用 git 进行项目部署，repo 指定了部署项目的 git 仓库的 URL，我们这里使用的是 HTTPS 的地址，如果之前配置过密钥对也可以使用 SSH 的地址，branch 指定了将代码同步到仓库中的哪一个分支，通常 master 分支就是发布项目最终工作成果的分支，也称为项目的主分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装名为&lt;code&gt;hexo-deployer-git&lt;/code&gt;的部署器插件，通过该插件就能实现一键部署。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install hexo-deployer-git --save
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用下面的命令来实现一键部署到 GitHub。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo deploy -g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo generate -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来在浏览器中输入 &lt;a href=&quot;https://jackfrued.github.io&quot;&gt;jackfrued.github.io&lt;/a&gt; 就能够看到自己的博客，现在全世界的人都可以通过这个 URL 来访问你的博客。大家是否注意到，访问你博客的这个 URL 就是刚才我们给仓库起的名字，因为你在GitHub上注册的用户名是独一无二的，所以这个域名也是全世界独一无二的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;将博客绑定到自己的域名&lt;/h4&gt;
&lt;p&gt;虽然我们已经通过 GitHub 提供的域名访问到了自己的博客，但是如果我们不愿意“寄人篱下”，我们在利用 GitHub Pages 提供的托管服务同时，也可以将博客绑定到自己专属的域名。如果暂时还没有购买域名，我们可以在提供域名购买服务的网站（如：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/www.hichina.com/%3E&quot;&gt;万网&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/www.godaddy.com/%3E&quot;&gt;GoDaddy&lt;/a&gt;）上进行购买。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/daed721bb8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：目前国内对域名的管理日趋严格，在购买域名时需要填写一大堆的个人信息，进行实名认证后才能获得域名，这一点相信大家能够理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如说，我现在已经购买了一个名为“jackfrued.top”的域名，如何让它跟“jackfrued.github.io”这个域名做一个绑定呢，我们可以利用&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/dns.console.aliyun.com/%3E&quot;&gt;阿里云控制台&lt;/a&gt;或者 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/%E7%95%AA%E5%A4%96%E7%AF%87/%3Chttps:/www.dnspod.cn/%3E&quot;&gt;DNSPod&lt;/a&gt; 来做一个域名解析服务。在域名解析平台登录成功后，可以添加或选择自己的域名来配置域名解析。点击“添加记录”按钮，创建一个类型为 CNAME 的域名解析，CNAME 类型的解析代表将一个域名解析到另一个域名，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/extra/5abcba7356.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完成这一步后，还不能马上通过自己的域名访问到博客项目，最后需要在博客项目的&lt;code&gt;source&lt;/code&gt;目录下添加一个名为 CNAME 的文件（请注意这个文件的名字是全大写字母的），该文件的内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jackfrued.top
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以清理一下之前生成的内容，然后将项目重新生成并发布到 GitHub 就大功告成了！现在我们已经拥有了独立域名的博客，希望大家可以利用它做些有意义的事情（记录自己的成长历程、分享自己的工作经验、提升个人的影响力）。&lt;/p&gt;
&lt;p&gt;加油吧，程序员！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;08 我为什么选择了Python&lt;/h2&gt;
&lt;p&gt;目前，Python 语言的发展势头在国内国外都是不可阻挡的，Python 凭借其简单优雅的语法，强大的生态圈从众多语言中脱颖而出，如今已经是稳坐编程语言排行榜前三的位置。国内很多 Python 开发者都是从 Java 开发者跨界过来的，我自己也不例外。我简单的跟大家交代一下，我为什么选择了 Python。&lt;/p&gt;
&lt;h4&gt;Python vs. Java&lt;/h4&gt;
&lt;p&gt;我们通过几个例子来比较一下，做同样的事情 Java 和 Python 的代码都是怎么写的。&lt;/p&gt;
&lt;p&gt;例子1：在终端中输出“hello, world”。&lt;/p&gt;
&lt;p&gt;Java 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test {
	
    public static void main(String[] args) {
        System.out.println(&quot;hello, world&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;hello, world&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子2：从1到100求和。&lt;/p&gt;
&lt;p&gt;Java 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test {
    
    public static void main(String[] args) {
        int total = 0;
        for (int i = 1; i &amp;lt;= 100; i += 1) {
            total += i;
        }
        System.out.println(total);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(sum(range(1, 101)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子3：双色球随机选号。&lt;/p&gt;
&lt;p&gt;Java 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

class Test {

    /**
     * 产生[min, max)范围的随机整数
     */
    public static int randomInt(int min, int max) {
        return (int) (Math.random() * (max - min) + min);
    }

    public static void main(String[] args) {
        // 初始化备选红色球
        List&amp;lt;Integer&amp;gt; redBalls = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 1; i &amp;lt;= 33; ++i) {
            redBalls.add(i);
        }
        List&amp;lt;Integer&amp;gt; selectedBalls = new ArrayList&amp;lt;&amp;gt;();
        // 选出六个红色球
        for (int i = 0; i &amp;lt; 6; ++i) {
            selectedBalls.add(redBalls.remove(randomInt(0, redBalls.size())));
        }
        // 对红色球进行排序
        Collections.sort(selectedBalls);
        // 添加一个蓝色球
        selectedBalls.add(randomInt(1, 17));
        // 输出选中的随机号码
        for (int i = 0; i &amp;lt; selectedBalls.size(); ++i) {
            System.out.printf(&quot;%02d &quot;, selectedBalls.get(i));
            if (i == selectedBalls.size() - 2) {
                System.out.print(&quot;| &quot;);
            }
        }
        System.out.println();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from random import randint, sample

# 初始化备选红色球
red_balls = [x for x in range(1, 34)]
# 选出六个红色球
selected_balls = sample(red_balls, 6)
# 对红色球进行排序
selected_balls.sort()
# 添加一个蓝色球
selected_balls.append(randint(1, 16))
# 输出选中的随机号码
for index, ball in enumerate(selected_balls):
    print(&apos;%02d&apos; % ball, end=&apos; &apos;)
    if index == len(selected_balls) - 2:
        print(&apos;|&apos;, end=&apos; &apos;)
print()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相信，看完这些例子后，你一定感受到了我选择了Python是有道理的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;09 一个小例子助你彻底理解协程&lt;/h2&gt;
&lt;p&gt;协程，可能是Python中最让初学者困惑的知识点之一，它也是Python中实现并发编程的一种重要方式。Python中可以使用多线程和多进程来实现并发，这两种方式相对来说是大家比较熟悉的。事实上，还有一种实现并发的方式叫做异步编程，而协程就是实现异步编程的必要方式。&lt;/p&gt;
&lt;p&gt;所谓协程，可以简单的理解为多个相互协作的子程序。在同一个线程中，当一个子程序阻塞时，我们可以让程序马上从一个子程序切换到另一个子程序，从而避免CPU因程序阻塞而闲置，这样就可以提升CPU的利用率，相当于用一种协作的方式加速了程序的执行。所以，我们可以言简意赅的说：&lt;strong&gt;协程实现了协作式并发&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;接下来用一个小例子帮助大家理解什么是协作式并发，先看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time


def display(num):
    time.sleep(1)
    print(num)


for num in range(10):
    display(num)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码相信大家很容看懂，程序会输出0到9的数字，每隔1秒中输出一个数字，因此整个程序的执行需要大约10秒时间。值得注意的是，因为没有使用多线程或多进程，程序中只有一个执行单元，而&lt;code&gt;time.sleep(1)&lt;/code&gt;的休眠操作会让整个线程停滞1秒钟，对于上面的代码来说，在这段时间里面CPU是完全闲置的没有做什么事情。&lt;/p&gt;
&lt;p&gt;我们再来看看使用协程会发生什么事情。从Python 3.5开始，使用协程实现协作式编发有了更为便捷的语法，我们可以使用&lt;code&gt;async&lt;/code&gt;来定义异步函数，可以使用&lt;code&gt;await&lt;/code&gt;让一个阻塞的子程序将CPU让给与它协作的子程序。在Python 3.7中，&lt;code&gt;asyanc&lt;/code&gt;和&lt;code&gt;await&lt;/code&gt;成为了正式的关键字，让开发者有一种喜大普奔的感觉。我们先看看如何定义一个异步函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio


async def display(num):
    await asyncio.sleep(1)
    print(num)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来敲黑板说重点。异步函数不同于普通函数，调用普通函数会得到返回值，而调用异步函数会得到一个协程对象。我们需要将协程对象放到一个事件循环中才能达到与其他协程对象协作的效果，因为事件循环会负责处理子程序切换的操作，简单的说就是让阻塞的子程序让出CPU给可以执行的子程序。&lt;/p&gt;
&lt;p&gt;我们先通过下面的列表生成式来代码10个协程对象，跟刚才在循环中调用display函数的道理一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;coroutines = [display(num) for num in range(10)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下面的代码可以获取事件循环并将协程对象放入事件循环中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coroutines))
loop.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码会发现，10个分别会阻塞1秒钟的协程总共只阻塞了约1秒种的时间，这就说明&lt;strong&gt;协程对象一旦阻塞会将CPU让出而不是让CPU处于闲置状态&lt;/strong&gt;，这样就大大的&lt;strong&gt;提升了CPU的利用率&lt;/strong&gt;。而且我们还会注意到，0到9的数字并不是按照我们创建协程对象的顺序打印出来的，这正是我们想要的结果啊；另外，多次执行该程序会发现每次输出的结果都不太一样，这正是并发程序本身执行顺序不确定性造成的结果。&lt;/p&gt;
&lt;p&gt;上面的例子来自于著名的“花书”（《Python高级并发编程》），为了让大家对协程的体会更加深刻，对原书的代码做了小的改动，这个例子虽然简单，但是它已经让你体会到了协作式并发的魅力。在商业项目中，如果需要使用协作式并发，还可以将系统默认的事件循环替换为&lt;code&gt;uvloop&lt;/code&gt;提供的事件循环，这样会获得更好的性能，因为&lt;code&gt;uvloop&lt;/code&gt;是基于著名的跨平台异步I/O库libuv实现的。另外，如果要做基于HTTP的网络编程，三方库&lt;strong&gt;aiohttp&lt;/strong&gt;是不错的选择，它基于asyncio实现了异步的HTTP服务器和客户端。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10 用函数还是用复杂的表达式&lt;/h2&gt;
&lt;p&gt;Perl语言的原作者&lt;em&gt;Larry Wall&lt;/em&gt;曾经说过，伟大的程序员都有三个优点：懒惰、暴躁和自负。乍一看这三个词语没有一个是褒义词，但在程序员的世界里，这三个词有不同的意义。首先，懒惰会促使程序员去写一些省事儿的程序来辅助自己或别人更好的完成工作，这样我们就无需做那些重复和繁琐的劳动；同理能够用3行代码解决的事情，我们也绝不会写出10行代码来。其次，暴躁会让程序员主动的去完成一些你还没有提出的工作，去优化自己的代码让它更有效率，能够3秒钟完成的任务，我们绝不能容忍1分钟的等待。最后，自负会促使程序员写出可靠无误的代码，我们写代码不是为了接受批评和指责，而是为了让其他人来膜拜。&lt;/p&gt;
&lt;p&gt;那么接下来就有一个很有意思的问题值得探讨一下，我们需要一个程序从输入的三个数中找出最大的那个数。这个程序对任何会编程的人来说都是小菜一碟，甚至不会编程的人经过10分钟的学习也能搞定。下面是用来解决这个问题的Python代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
if a &amp;gt; b:
	the_max = a
else:
	the_max = b
if c &amp;gt; the_max:
	the_max = c
print(&apos;The max is:&apos;, the_max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们刚才说了，程序员都是懒惰的，很多程序员都会使用三元条件运算符来改写上面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
the_max = a if a &amp;gt; b else b
the_max = c if c &amp;gt; the_max else the_max
print(&apos;The max is:&apos;, the_max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，Python在2.5版本以前是没有上面代码第4行和第5行中使用的三元条件运算符的，究其原因是Guido van Rossum（Python之父）认为三元条件运算符并不能帮助 Python变得更加简洁，于是那些习惯了在C/C++或Java中使用三元条件运算符（在这些语言中，三元条件运算符也称为“Elvis运算符”，因为&lt;code&gt;?:&lt;/code&gt;放在一起很像著名摇滚歌手猫王Elvis的大背头）的程序员试着用&lt;code&gt;and&lt;/code&gt;和&lt;code&gt;or&lt;/code&gt;运算符的短路特性来模拟出三元操作符，于是在那个年代，上面的代码是这样写的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
the_max = a &amp;gt; b and a or b
the_max = c &amp;gt; the_max and c or the_max
print(&apos;The max is:&apos;, the_max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这种做法在某些场景下是不能成立的，且看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 0
b = -100
# 下面的代码本来预期输出a的值，结果却得到了b的值
# 因为a的值0在进行逻辑运算时会被视为False来处理
print(True and a or b)
# print(a if True else b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以在Python 2.5以后引入了三元条件运算符来避免上面的风险（上面代码被注释掉的最后一句话）。那么，问题又来了，上面的代码还可以写得更简短吗？答案是肯定的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
print(&apos;The max is:&apos;, (a if a &amp;gt; b else b) if (a if a &amp;gt; b else b) &amp;gt; c else c)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，这样做真的好吗？如此复杂的表达式是不是让代码变得晦涩了很多呢？我们发现，在实际开发中很多开发者都喜欢过度的使用某种语言的特性或语法糖，于是简单的多行代码变成了复杂的单行表达式，这样做真的好吗？这个问题我也不止一次的问过自己，现在我能给出的答案是下面的代码，使用辅助函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def the_max(x, y):
	return x if x &amp;gt; y else y


a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
print(&apos;The max is:&apos;, the_max(the_max(a, b), c))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，我定义了一个辅助函数&lt;code&gt;the_max&lt;/code&gt;用来找出参数传入的两个值中较大的那一个，于是下面的输出语句可以通过两次调用&lt;code&gt;the_max&lt;/code&gt;函数来找出三个数中的最大值，现在代码的可读性是不是好了很多。用辅助函数来替代复杂的表达式真的是一个不错的选择，关键是比较大小的逻辑转移到这个辅助函数后不仅可以反复调用它，而且还可以进行级联操作。&lt;/p&gt;
&lt;p&gt;当然，很多语言中比较大小的函数根本没有必要自己来实现（通常都是内置函数），Python也是如此。Python内置的max函数利用了Python对可变参数的支持，允许一次性传入多个值或者一个迭代器并找出那个最大值，所以上面讨论的问题在Python中也就是一句话的事，但是从复杂表达式到使用辅助函数简化复杂表达式这个思想是非常值得玩味的，所以分享出来跟大家做一个交流。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = int(input(&apos;a = &apos;))
b = int(input(&apos;b = &apos;))
c = int(input(&apos;c = &apos;))
print(&apos;The max is:&apos;, max(a, b, c))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;11 知乎问题回答&lt;/h2&gt;
&lt;h5&gt;Python学习完基础语法知识后，如何进一步提高？&lt;/h5&gt;
&lt;p&gt;如果你已经完成了Python基础语法的学习，想要知道接下来如何提高，那么你得先问问自己你要用Python来做什么？目前学习Python后可能的就业方向包括以下几个领域，我把每个领域需要的技术作为了一个简单的关键词摘要。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：以下数据参考了主要的招聘门户网站以及职友集。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;职位&lt;/th&gt;
&lt;th&gt;所需技能&lt;/th&gt;
&lt;th&gt;招聘需求量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python后端开发工程师&lt;/td&gt;
&lt;td&gt;Python基础&amp;lt;br&amp;gt;Django / Flask / Tornado / Sanic&amp;lt;br&amp;gt;RESTful / 接口文档撰写&amp;lt;br&amp;gt;MySQL / Redis / MongoDB / ElasticSearch&amp;lt;br&amp;gt;Linux / Git / Scrum / PyCharm&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python爬虫开发工程师&lt;/td&gt;
&lt;td&gt;Python基础&amp;lt;br&amp;gt;常用标准库和三方库&amp;lt;br&amp;gt;Scrapy / PySpider&amp;lt;br&amp;gt;Selenium / Appnium&amp;lt;br&amp;gt;Redis / MongoDB / MySQL&amp;lt;br&amp;gt;前端 / HTTP(S) / 抓包工具&lt;/td&gt;
&lt;td&gt;较少&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python量化交易开发工程师&lt;/td&gt;
&lt;td&gt;Python基础&amp;lt;br&amp;gt;数据结构 / 算法 / 设计模式&amp;lt;br&amp;gt;NoSQL（KV数据库）&amp;lt;br&amp;gt;金融学（两融、期权、期货、股票） / 数字货币&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python数据分析工程师 /&amp;lt;br&amp;gt;Python机器学习工程师&lt;/td&gt;
&lt;td&gt;统计学专业 / 数学专业 / 计算机专业&amp;lt;br&amp;gt;Python基础 / 算法设计&amp;lt;br&amp;gt;SQL / NoSQL / Hive / Hadoop / Spark&amp;lt;br&amp;gt;NumPy / Scikit-Learn / Pandas / Seaborn&amp;lt;br&amp;gt;PyTorch / Tensorflow / OpenCV&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python自动化测试工程师&lt;/td&gt;
&lt;td&gt;Python基础 / 单元测试 / 软件测试基础&amp;lt;br&amp;gt;Linux / Shell / JIRA / 禅道 / Jenkins / CI / CD&amp;lt;br&amp;gt;Selenium / Robot Framework / Appnium&amp;lt;br&amp;gt;ab / sysbench / JMeter / LoadRunner / QTP&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python自动化运维工程师&lt;/td&gt;
&lt;td&gt;Python基础 / Linux / Shell &amp;lt;br&amp;gt;Fabric / Ansible / Playbook&amp;lt;br&amp;gt;Zabbix / Saltstack / Puppet&amp;lt;br&amp;gt;Docker / paramiko&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python云平台开发工程师&lt;/td&gt;
&lt;td&gt;Python基础&amp;lt;br&amp;gt;OpenStack / CloudStack&amp;lt;br&amp;gt;Ovirt / KVM&amp;lt;br&amp;gt;Docker / K8S&lt;/td&gt;
&lt;td&gt;较少&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果弄清了自己将来要做的方向，就可以开始有针对性的学习了，下面给大家一个推荐书籍的清单。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;入门读物&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《Python基础教程》（&lt;em&gt;Beginning Python From Novice to Professional&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python学习手册》（&lt;em&gt;Learning Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python编程》（&lt;em&gt;Programming Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python编程从入门到实践》（&lt;em&gt;Python Crash Course&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python Cookbook》&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进阶读物&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《软件架构 - Python语言实现》（&lt;em&gt;Software Architecture with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《流畅的Python》（&lt;em&gt;Fluent Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python设计模式》（&lt;em&gt;Learning Python Design Patterns&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python高级编程》（&lt;em&gt;Expert Python Programming&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python性能分析与优化》（&lt;em&gt;Mastering Python High Performance&lt;/em&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据库相关&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《MySQL必知必会》（&lt;em&gt;MySQL Crash Course&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《深入浅出MySQL - 数据库开发、优化与管理维护》&lt;/li&gt;
&lt;li&gt;《MongoDB权威指南》（&lt;em&gt;MongoDB: The Definitive Guide&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Redis实战》（&lt;em&gt;Redis in Action&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Redis开发与运维》&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux / Shell / Docker / 运维&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《鸟哥的Linux私房菜》&lt;/li&gt;
&lt;li&gt;《Linux命令行与shell脚本编程大全》（&lt;em&gt;Linux Command Line and Shell Scripting Bible&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python自动化运维:技术与最佳实践》&lt;/li&gt;
&lt;li&gt;《第一本Docker书》（&lt;em&gt;The Docker Book&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Docker经典实例》（Docker Cookbook）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django / Flask / Tornado&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;《Django基础教程》（&lt;em&gt;Tango with Django&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《轻量级Django》（&lt;em&gt;Lightweight Django&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《精通Django》（&lt;em&gt;Mastering Django: Core&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python Web开发：测试驱动方法》（&lt;em&gt;Test-Driven Development with Python&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Two Scoops of Django: Best Practice of Django 1.8》&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Flask Web开发：基于Python的Web应用开发实战》（&lt;em&gt;Flask Web Development: Developing Web Applications with Python&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《深入理解Flask》（&lt;em&gt;Mastering Flask&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Introduction to Tornado》&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;爬虫开发&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;《用Python写网络爬虫》（&lt;em&gt;Web Scraping with Python&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《精通Python爬虫框架Scrapy》（&lt;em&gt;Learning Scrapy&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python网络数据采集》（&lt;em&gt;Web Scraping with Python&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python爬虫开发与项目实战》&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python 3网络爬虫开发实战》&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据分析&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《利用Python进行数据分析》（&lt;em&gt;Python for Data Analysis&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据科学手册》（&lt;em&gt;Python Data Science Handbook&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python金融大数据分析》（&lt;em&gt;Python for Finance&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据可视化编程实战》（&lt;em&gt;Python Data Visualization Cookbook&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据处理》（&lt;em&gt;Data Wrangling with Python&lt;/em&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;机器学习&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;《Python机器学习基础教程》（&lt;em&gt;Introduction to Machine Learning with Python&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python机器学习实践指南》（&lt;em&gt;Python Machine Learning Blueprints&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python机器学习实践：测试驱动的开发方法》（&lt;em&gt;Thoughtful Machine Learning with Python A Test Driven Approach&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《Python机器学习经典实例》（&lt;em&gt;Python Machine Learning Cookbook&lt;/em&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;《TensorFlow：实战Google深度学习框架》&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他书籍&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《Pro Git》&lt;/li&gt;
&lt;li&gt;《Selenium自动化测试 - 基于Python语言》（&lt;em&gt;Learning Selenium Testing Tools with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Selenium自动化测试之道》&lt;/li&gt;
&lt;li&gt;《Scrum敏捷软件开发》（&lt;em&gt;Software Development using Scrum&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《高效团队开发 - 工具与方法》&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然学习编程，最重要的通过项目实战来提升自己的综合能力，Github上有大量的优质开源项目，其中不乏优质的Python项目。有一个名为&lt;a href=&quot;https://github.com/mahmoud/awesome-python-applications&quot;&gt;“awesome-python-applications”&lt;/a&gt;的项目对这些优质的资源进行了归类并提供了传送门，大家可以了解下。如果自学能力不是那么强，可以通过网络上免费或者付费的视频课程来学习对应的知识；如果自律性没有那么强，那就只能建议花钱参加培训班了，因为花钱在有人监督的环境下学习对很多人来说确实是一个捷径，但是要记得：“师傅领进门，修行靠各人”。选择自己热爱的东西并全力以赴，不要盲目的跟风学习，这一点算是过来人的忠告吧。记得我自己刚开始进入软件开发这个行业时，有人跟我说过这么一句话，现在也分享出来与诸君共勉：“浮躁的人有两种：只观望而不学习的人，只学习而不坚持的人；浮躁的人都不是高手。”&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;12 用Canvas学习编程&lt;/h2&gt;
&lt;p&gt;在学习编程的过程中，我们经常需要编写、修改和优化代码。而 ChatGPT 的 Canvas 功能提供了一种高效、直观的方式来帮助我们更好地理解和提升代码能力。下面，我们就简单介绍一下如何利用 ChatGPT 的 Canvas 功能来学习和理解代码。&lt;/p&gt;
&lt;h4&gt;Canvas使用场景&lt;/h4&gt;
&lt;h5&gt;1. 代码实时编辑与优化&lt;/h5&gt;
&lt;p&gt;Canvas 功能可以作为一个交互式代码编辑器，允许我们直接在其中编写代码，并实时修改和优化。当我们遇到代码问题时，可以在 Canvas 中提交代码，并请求 ChatGPT 提供优化建议。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 ChatGPT 进行代码重构，使代码更简洁、易读。&lt;/li&gt;
&lt;li&gt;让 ChatGPT 解释某段代码的逻辑，帮助理解复杂算法或设计模式。&lt;/li&gt;
&lt;li&gt;在 Canvas 中迭代改进代码，提高运行效率或减少冗余代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 学习编程概念与示例&lt;/h5&gt;
&lt;p&gt;对于初学者来说，Canvas 也是一个很好的学习工具。你可以在 Canvas 中输入示例代码，并让 ChatGPT 解释其作用。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;学习基础语法，例如变量、循环、函数等。&lt;/li&gt;
&lt;li&gt;通过示例代码学习数据结构和算法，如链表、排序算法等。&lt;/li&gt;
&lt;li&gt;让 ChatGPT 提供代码示例，并逐行解释代码的运行方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3. 代码调试与错误修正&lt;/h5&gt;
&lt;p&gt;在编程过程中，错误是不可避免的。Canvas 可以帮助快速定位和修复错误。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将错误代码粘贴到 Canvas，让 ChatGPT 分析错误原因。&lt;/li&gt;
&lt;li&gt;让 ChatGPT 提供修正方案，并解释错误发生的原因。&lt;/li&gt;
&lt;li&gt;通过 Canvas 进行多次调试，逐步优化代码逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;4. 代码注释与文档生成&lt;/h5&gt;
&lt;p&gt;编写清晰的注释和文档对于代码的可读性至关重要。Canvas 可以帮助我们快速生成代码注释和文档：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让 ChatGPT 为代码添加详细注释，方便后续维护和理解。&lt;/li&gt;
&lt;li&gt;自动生成 API 文档，方便团队协作。&lt;/li&gt;
&lt;li&gt;生成代码执行流程的简要说明，帮助快速掌握代码逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Canvas应用举例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;yield from [student.get(&quot;school&quot;) for student in students]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在学习的 Python 语言的时候，假如你看到上面这段代码但并不理解它的意图，你可以使用如下所示的提示词跟 ChatGPT 开启对话，你甚至不用告诉它你用的什么编程语言，也不用交待这段代码的上下文。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;使用 canvas 跟我一起探索下面的代码
yield from [student.get(&quot;school&quot;) for student in students]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图所示，ChatGPT 给出了关于上面代码的回复，右侧展开了 Canvas 并将我们的代码置于其中。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/e4005dd8bf.webp&quot; style=&quot;zoom:33%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果上面的回答还是没能帮你理解这段代码，我们可以让 ChatGPT 给出对应的例子，提示词如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;针对这段代码给我一个应用的例子吧
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图所示，ChatGPT 已经帮我修改了 Canvas 中的代码，给出了一段完整的可运行的 Python 代码，我们可以点击 Canvas 上面的运行按钮来执行这段代码，Canvas 目前已经可以支持很多种编程语言，你熟悉的编程语言它应该也是熟悉的。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/7564f6b1db.webp&quot; style=&quot;zoom:33%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果你理解了上面的操作，我们还可以继续提出疑问，如果&lt;code&gt;&quot;school&quot;&lt;/code&gt;字段不存在，我们的代码改如何修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;修改下上面的代码，让生成器排除掉没有学校（school）的学生
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/584dcc640a.webp&quot; style=&quot;zoom:33%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;然后，我们再进一步根据我们实际的需要，让 ChatGPT 帮助我们修改代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;如果我希望每个学校只出现一次，应该该如何修改
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/65be5a4782.webp&quot; style=&quot;zoom:33%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对于写 Python 的人来说，如果你认为 ChatGPT 生成的代码不够简洁、不够 Pythonic，我们可以继续提问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;上面的代码能够写得更加简洁和Pythonic吗
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/extra/c528b23d8d.webp&quot; style=&quot;zoom:33%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;ChatGPT 的 Canvas 功能不仅仅是一个代码编辑器，更是一个智能的编程助手。无论你是编程初学者，还是经验丰富的开发者，都可以通过 Canvas 提高代码理解和编写能力。通过不断实践和探索，你可以更高效地掌握编程技巧，并提升自身的技术水平。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13 Python编程惯例&lt;/h2&gt;
&lt;p&gt;“惯例”这个词指的是“习惯的做法，常规的办法，一贯的做法”，与这个词对应的英文单词叫“idiom”。由于Python跟其他很多编程语言在语法和使用上还是有比较显著的差别，因此作为一个Python开发者如果不能掌握这些惯例，就无法写出“Pythonic”的代码。下面我们总结了一些在Python开发中的惯用的代码。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;让代码既可以被导入又可以被执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if __name__ == &apos;__main__&apos;:
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用下面的方式判断逻辑“真”或“假”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if x:
if not x:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;jackfrued&apos;
fruits = [&apos;apple&apos;, &apos;orange&apos;, &apos;grape&apos;]
owners = {&apos;1001&apos;: &apos;骆昊&apos;, &apos;1002&apos;: &apos;王大锤&apos;}
if name and fruits and owners:
    print(&apos;I love fruits!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;jackfrued&apos;
fruits = [&apos;apple&apos;, &apos;orange&apos;, &apos;grape&apos;]
owners = {&apos;1001&apos;: &apos;骆昊&apos;, &apos;1002&apos;: &apos;王大锤&apos;}
if name != &apos;&apos; and len(fruits) &amp;gt; 0 and owners != {}:
    print(&apos;I love fruits!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;善于使用in运算符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if x in items: # 包含
for x in items: # 迭代
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;Hao LUO&apos;
if &apos;L&apos; in name:
    print(&apos;The name has an L in it.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = &apos;Hao LUO&apos;
if name.find(&apos;L&apos;) != -1:
    print(&apos;This name has an L in it!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不使用临时变量交换两个值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b = b, a
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用序列构建字符串。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chars = [&apos;j&apos;, &apos;a&apos;, &apos;c&apos;, &apos;k&apos;, &apos;f&apos;, &apos;r&apos;, &apos;u&apos;, &apos;e&apos;, &apos;d&apos;]
name = &apos;&apos;.join(chars)
print(name)  # jackfrued
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chars = [&apos;j&apos;, &apos;a&apos;, &apos;c&apos;, &apos;k&apos;, &apos;f&apos;, &apos;r&apos;, &apos;u&apos;, &apos;e&apos;, &apos;d&apos;]
name = &apos;&apos;
for char in chars:
    name += char
print(name)  # jackfrued
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EAFP优于LBYL。&lt;/p&gt;
&lt;p&gt;EAFP - &lt;strong&gt;E&lt;/strong&gt;asier to &lt;strong&gt;A&lt;/strong&gt;sk &lt;strong&gt;F&lt;/strong&gt;orgiveness than &lt;strong&gt;P&lt;/strong&gt;ermission.&lt;/p&gt;
&lt;p&gt;LBYL - &lt;strong&gt;L&lt;/strong&gt;ook &lt;strong&gt;B&lt;/strong&gt;efore &lt;strong&gt;Y&lt;/strong&gt;ou &lt;strong&gt;L&lt;/strong&gt;eap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d = {&apos;x&apos;: &apos;5&apos;}
try:
    value = int(d[&apos;x&apos;])
    print(value)
except (KeyError, TypeError, ValueError):
    value = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d = {&apos;x&apos;: &apos;5&apos;}
if &apos;x&apos; in d and isinstance(d[&apos;x&apos;], str) \
		and d[&apos;x&apos;].isdigit():
    value = int(d[&apos;x&apos;])
    print(value)
else:
    value = None
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用enumerate进行迭代。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fruits = [&apos;orange&apos;, &apos;grape&apos;, &apos;pitaya&apos;, &apos;blueberry&apos;]
for index, fruit in enumerate(fruits):
	print(index, &apos;:&apos;, fruit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fruits = [&apos;orange&apos;, &apos;grape&apos;, &apos;pitaya&apos;, &apos;blueberry&apos;]
index = 0
for fruit in fruits:
    print(index, &apos;:&apos;, fruit)
    index += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用生成式生成列表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = [7, 20, 3, 15, 11]
result = [num * 3 for num in data if num &amp;gt; 10]
print(result)  # [60, 45, 33]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = [7, 20, 3, 15, 11]
result = []
for i in data:
    if i &amp;gt; 10:
        result.append(i * 3)
print(result)  # [60, 45, 33]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用zip组合键和值来创建字典。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keys = [&apos;1001&apos;, &apos;1002&apos;, &apos;1003&apos;]
values = [&apos;骆昊&apos;, &apos;王大锤&apos;, &apos;白元芳&apos;]
d = dict(zip(keys, values))
print(d)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不好&lt;/strong&gt;的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keys = [&apos;1001&apos;, &apos;1002&apos;, &apos;1003&apos;]
values = [&apos;骆昊&apos;, &apos;王大锤&apos;, &apos;白元芳&apos;]
d = {}
for i, key in enumerate(keys):
    d[key] = values[i]
print(d)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：这篇文章的内容来自于网络，有兴趣的读者可以阅读&lt;a href=&quot;http://safehammad.com/downloads/python-idioms-2014-01-16.pdf&quot;&gt;原文&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;14 Python参考书籍&lt;/h2&gt;
&lt;h4&gt;入门读物&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《Python基础教程》（&lt;em&gt;Beginning Python From Novice to Professional&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python学习手册》（&lt;em&gt;Learning Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python编程》（&lt;em&gt;Programming Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python Cookbook》&lt;/li&gt;
&lt;li&gt;《Python程序设计》（&lt;em&gt;Python Programming: An Introduction to Computer Science&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Modern Python Cookbook》&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;进阶读物&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《Python核心编程》（&lt;em&gt;Core Python Applications Programming&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《流畅的Python》（&lt;em&gt;Fluent Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Effective Python：编写高质量Python代码的59个有效方法》（&lt;em&gt;Effective Python 59 Specific Ways to Write Better Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python设计模式》（&lt;em&gt;Learning Python Design Patterns&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python高级编程》（&lt;em&gt;Expert Python Programming&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python性能分析与优化》（&lt;em&gt;Mastering Python High Performance&lt;/em&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Web框架&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《Django基础教程》（&lt;em&gt;Tango with Django&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《轻量级Django》（&lt;em&gt;Lightweight Django&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python Web开发：测试驱动方法》（&lt;em&gt;Test-Driven Development with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Web Development with Django Cookbook》&lt;/li&gt;
&lt;li&gt;《Test-Driven Development with Django》&lt;/li&gt;
&lt;li&gt;《Django Project Blueprints 》&lt;/li&gt;
&lt;li&gt;《Flask Web开发：基于Python的Web应用开发实战》（&lt;em&gt;Flask Web Development: Developing Web Applications with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《深入理解Flask》（&lt;em&gt;Mastering Flask&lt;/em&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据采集&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《用Python写网络爬虫》（&lt;em&gt;Web Scraping with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《精通Python爬虫框架Scrapy》（&lt;em&gt;Learning Scrapy&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python网络数据采集》（&lt;em&gt;Web Scraping with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python爬虫开发与项目实战》&lt;/li&gt;
&lt;li&gt;《Python 3网络爬虫开发实战》&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据分析&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《利用Python进行数据分析》（&lt;em&gt;Python for Data Analysis&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据科学手册》（&lt;em&gt;Python Data Science Handbook&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python金融大数据分析》（&lt;em&gt;Python for Finance&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据可视化编程实战》（&lt;em&gt;Python Data Visualization Cookbook&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python数据处理》（&lt;em&gt;Data Wrangling with Python&lt;/em&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;机器学习&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;《Python机器学习基础教程》（&lt;em&gt;Introduction to Machine Learning with Python&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python机器学习实践指南》（&lt;em&gt;Python Machine Learning Blueprints&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python Machine Learning Case Studies》&lt;/li&gt;
&lt;li&gt;《Python机器学习实践：测试驱动的开发方法》（&lt;em&gt;Thoughtful Machine Learning with Python A Test Driven Approach&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《Python机器学习经典实例》（&lt;em&gt;Python Machine Learning Cookbook&lt;/em&gt;）&lt;/li&gt;
&lt;li&gt;《TensorFlow：实战Google深度学习框架》&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;15 Python容器类型使用小技巧&lt;/h2&gt;
&lt;p&gt;Python中提供了非常丰富的容器型数据类型，大家最为熟悉的有&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;tuple&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;dict&lt;/code&gt;等。下面为大家分享一些使用这些类型的小技巧，希望帮助大家写出更加Pythonic的代码。&lt;/p&gt;
&lt;h5&gt;1. 从字典中取最大&lt;/h5&gt;
&lt;p&gt;假设字典对象对应的变量名为&lt;code&gt;my_dict&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;取出最大值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;max(my_dict.values())
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取值最大值的键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;max(my_dict, key=my_dict.get)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取出最大值的键和值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; max(my_dict.items(), key=lambda x: x[1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import operator

max(my_dict.items(), key=operator.itemgetter(1))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面用到了&lt;code&gt;operator&lt;/code&gt;模块的&lt;code&gt;itemgetter&lt;/code&gt;函数，这个函数的的作用如下所示。在上面的代码中，&lt;code&gt;itemgetter&lt;/code&gt;帮我们获取到了二元组中的第2个元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def itemgetter(*items):
    if len(items) == 1:
        item = items[0]
        def g(obj):
            return obj[item]
    else:
        def g(obj):
            return tuple(obj[item] for item in items)
    return g
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 统计列表元素出现次数&lt;/h5&gt;
&lt;p&gt;假设列表对象对应的变量名为&lt;code&gt;my_list&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{x: my_list.count(x) for x in set(my_list)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from itertools import groupby

{key: len(list(group)) for key, group in groupby(sorted(my_list))}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;groupby&lt;/code&gt;函数会将相邻相同元素分到一个组中，所以先用&lt;code&gt;sorted&lt;/code&gt;函数排序就是为了将相同的元素放到一起。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from collections import Counter

dict(Counter(my_list))
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3. 截断列表元素&lt;/h5&gt;
&lt;p&gt;假设列表对象对应的变量名为&lt;code&gt;my_list&lt;/code&gt;，通常大家会想到用下面的方式来截断列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_list = my_list[:i]
my_list = my_list[j:]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，更好的方式使用下面的操作，大家可以认真想想为什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;del my_list[i:]
del my_list[:j]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4. 按最长列表实现zip操作&lt;/h5&gt;
&lt;p&gt;Python的内置函数&lt;code&gt;zip&lt;/code&gt;可以产生一个生成器对象，该生成器对象将两个或多个可迭代对象的元素组装到一起，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list(zip(&apos;abc&apos;, [1, 2, 3, 4]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码会得到一个如下所示的列表，相信大家也注意到了，列表中元素的个数是由&lt;code&gt;zip&lt;/code&gt;函数中长度最小的可迭代对象决定的，所以下面的列表中只有3个元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[(&apos;a&apos;, 1), (&apos;b&apos;, 2), (&apos;c&apos;, 3)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望由&lt;code&gt;zip&lt;/code&gt;函数中长度最大的可迭代对象来决定最终迭代出的元素个数，可以试一试&lt;code&gt;itertools&lt;/code&gt;模块的&lt;code&gt;zip_longest&lt;/code&gt;函数，其用法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from itertools import zip_longest

list(zip_longest(&apos;abc&apos;, [1, 2, 3, 4]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码创建出的列表对象如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[(&apos;a&apos;, 1), (&apos;b&apos;, 2), (&apos;c&apos;, 3), (None, 4)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5. 快速拷贝一个列表&lt;/h5&gt;
&lt;p&gt;如果希望快速拷贝一个列表对象，可以通过切片操作来实现，但是切片操作仅实现了浅拷贝，简单的说就是切片创建了新的列表对象，但是新列表中的元素是和之前的列表共享的。如果希望实现深拷贝，可以使用&lt;code&gt;copy&lt;/code&gt;模块的&lt;code&gt;deepcopy&lt;/code&gt;函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;浅拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thy_list = my_list[:]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import copy

thy_list = copy.copy(my_list)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;深拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import copy

thy_list = copy.deepcopy(my_list)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;6. 对两个或多个列表对应元素进行操作&lt;/h5&gt;
&lt;p&gt;Python内置函数中的&lt;code&gt;map&lt;/code&gt;函数可以对一个可迭代对象中的元素进行“映射”操作，这个函数在批量处理数据时非常有用。但是很多人都不知道，这个函数还可以作用于多个可迭代对象，通过传入的函数对多个可迭代对象中的对应元素进行处理，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_list = [11, 13, 15, 17]
thy_list = [2, 4, 6, 8, 10]
list(map(lambda x, y: x + y, my_list, thy_list))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的操作会得到如下所示的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[13, 17, 21, 25]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，同样的操作也可以用&lt;code&gt;zip&lt;/code&gt;函数配合列表生成式来完成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_list = [11, 13, 15, 17]
thy_list = [2, 4, 6, 8, 10]
[x + y for x, y in zip(my_list, thy_list)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;7. 处理列表中的空值和零值&lt;/h5&gt;
&lt;p&gt;假设列表对象对应的变量名为&lt;code&gt;my_list&lt;/code&gt;，如果列表中有空值（&lt;code&gt;None&lt;/code&gt;）和零值，我们可以用下面的方式去掉空值和零值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list(filter(bool, my_list))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的列表生成式语法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[x for x in my_list if x]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;8. 从嵌套列表中抽取指定列&lt;/h5&gt;
&lt;p&gt;假设&lt;code&gt;my_list&lt;/code&gt;是一个如下所示的嵌套列表，该嵌套列表可以用来表示数学上的矩阵，如果要取出矩阵第一列的元素构成一个列表，我们可以这样写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_list = [
    [1, 1, 2, 2],
    [5, 6, 7, 8],
    [3, 3, 4, 4],
]
col1, *_ = zip(*my_list)
list(col1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们会得到一个如下所示的列表，刚好是矩阵的第一列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1, 5, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以此类推，如果想取出矩阵第二列的元素构成一个列表，可以用如下所示的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_, col2, *_ = zip(*my_list)
list(col2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，如果要实现矩阵的转置操作，我们也可以按照上面的思路写出下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[list(x) for x in zip(*my_list)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过上面的操作，我们会得到如下所示的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[1, 5, 3], 
 [1, 6, 3], 
 [2, 7, 4], 
 [2, 8, 4]]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;16 Zen of Python（Python之禅）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Beautiful is better than ugly. （优美比丑陋好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Explicit is better than implicit.（清晰比晦涩好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Simple is better than complex.（简单比复杂好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Complex is better than complicated.（复杂比错综复杂好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Flat is better than nested.（扁平比嵌套好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Sparse is better than dense.（稀疏比密集好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Readability counts.（可读性很重要）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Special cases aren&apos;t special enough to break the rules.（特殊情况也不应该违反这些规则）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Although practicality beats purity.（但现实往往并不那么完美）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Errors should never pass silently.（异常不应该被静默处理）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Unless explicitly silenced.（除非你希望如此）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;In the face of ambiguity, refuse the temptation to guess.（遇到模棱两可的地方，不要胡乱猜测）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;There should be one-- and preferably only one --obvious way to do it.（肯定有一种通常也是唯一一种最佳的解决方案）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Although that way may not be obvious at first unless you&apos;re Dutch.（虽然这种方案并不是显而易见的，因为你不是那个荷兰人[^1]）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Now is better than never.（现在开始做比不做好）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Although never is often better than *right* now.（不做比盲目去做好[^2]）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;If the implementation is hard to explain, it&apos;s a bad idea.（如果一个实现方案难于理解，它通常不是一个好的方案）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;If the implementation is easy to explain, it may be a good idea.（如果一个实现方案易于理解，它很有可能是一个好的方案）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;Namespaces are one honking great idea -- let&apos;s do more of those!（命名空间非常有用，我们应当多加利用）&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[^1]:这里指的是 Python 之父 Guido van Rossumm。
[^2]:极限编程中的YAGNI原则&lt;/p&gt;
</content:encoded></item><item><title>Day91-100 教程全文（10 篇合并）</title><link>https://www.klucen.cn/posts/python/day91-100-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day91-100-full/</guid><pubDate>Sun, 14 Dec 2025 00:18:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 10 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：工程化、部署与面试
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#91-%E5%9B%A2%E9%98%9F%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E7%9A%84%E9%97%AE%E9%A2%98%E5%92%8C%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88&quot;&gt;91 团队项目开发的问题和解决方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#92-docker%E5%AE%B9%E5%99%A8%E6%8A%80%E6%9C%AF%E8%AF%A6%E8%A7%A3&quot;&gt;92 Docker容器技术详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#93-mysql%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96&quot;&gt;93 MySQL性能优化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#94-%E7%BD%91%E7%BB%9Capi%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1&quot;&gt;94 网络API接口设计&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#95-%E4%BD%BF%E7%94%A8django%E5%BC%80%E5%8F%91%E5%95%86%E4%B8%9A%E9%A1%B9%E7%9B%AE&quot;&gt;95 使用Django开发商业项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#96-%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95%E5%92%8C%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95&quot;&gt;96 软件测试和自动化测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#97-%E7%94%B5%E5%95%86%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E8%A6%81%E7%82%B9%E5%89%96%E6%9E%90&quot;&gt;97 电商网站技术要点剖析&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#98-%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF%E6%8C%87%E5%8D%97&quot;&gt;98 项目部署上线指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#99-%E9%9D%A2%E8%AF%95%E4%B8%AD%E7%9A%84%E5%85%AC%E5%85%B1%E9%97%AE%E9%A2%98&quot;&gt;99 面试中的公共问题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#100-%E8%A1%A5%E5%85%85%E5%86%85%E5%AE%B9&quot;&gt;100 补充内容&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;91 团队项目开发的问题和解决方案&lt;/h2&gt;
&lt;p&gt;个人开发和团队开发这两个词相信对大家来说并不陌生。所谓个人开发就是一个人把控产品的所有内容；而团队开发则是由多个人组团并完成产品的开发。要实施团队开发以下几点是不可或缺的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对开发过程中的各种事件（例如：谁到什么时间完成了什么事情）进行管理和共享。&lt;/li&gt;
&lt;li&gt;在团队内部共享各类工作成果以及新的知识技巧等。&lt;/li&gt;
&lt;li&gt;管理工作成果的变更，既要防止成果被破坏，又要保证各个成员利用现有成果并行作业。&lt;/li&gt;
&lt;li&gt;证明团队开发出的软件在任何时候都是可以正常运行的。&lt;/li&gt;
&lt;li&gt;使用自动化的工作流程，让团队成员能够正确的实施开发、测试和部署。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;团队项目开发常见问题&lt;/h4&gt;
&lt;p&gt;团队开发相较于个人开发，容易遇到以下几个方面的问题。&lt;/p&gt;
&lt;h5&gt;问题1：传统的沟通方式无法确定处理的优先级&lt;/h5&gt;
&lt;p&gt;例如：使用邮件进行沟通可能出现邮件数量太多导致重要的邮件被埋没，无法管理状态，不知道哪些问题已经解决，哪些问题尚未处理，如果用全文检索邮件的方式来查询相关问题效率过于低下。&lt;/p&gt;
&lt;p&gt;解决方案：使用缺陷管理工具。&lt;/p&gt;
&lt;h5&gt;问题2：没有能够用于验证的环境&lt;/h5&gt;
&lt;p&gt;例如：收到项目正式环境中发生的故障报告后，需要还原正式环境需要花费很长的时间。&lt;/p&gt;
&lt;p&gt;解决方法：实施持续交付。&lt;/p&gt;
&lt;h5&gt;问题3：用别名目录管理项目分支&lt;/h5&gt;
&lt;p&gt;解决方法：实施版本控制。&lt;/p&gt;
&lt;h5&gt;问题4：重新制作数据库非常困难&lt;/h5&gt;
&lt;p&gt;例如：正式环境和开发环境中数据库表结构不一致或者某个表列的顺序不一致。&lt;/p&gt;
&lt;p&gt;解决方法：实施版本控制。&lt;/p&gt;
&lt;h5&gt;问题5：不运行系统就无法察觉问题&lt;/h5&gt;
&lt;p&gt;例如：解决一个bug可能引入其他的bug或者造成系统退化，不正确的使用版本系统覆盖了其他人的修改，修改的内容相互发生了干扰，如果问题不能尽早发现，那么等过去几个月后再想追溯问题就非常麻烦了。&lt;/p&gt;
&lt;p&gt;解决方法：实施持续集成，将团队成员的工作成果经常、持续的进行构建和测试。&lt;/p&gt;
&lt;h5&gt;问题6：覆盖了其他成员修正的代码&lt;/h5&gt;
&lt;p&gt;解决方法：实施版本控制。&lt;/p&gt;
&lt;h5&gt;问题7：无法实施代码重构&lt;/h5&gt;
&lt;p&gt;例如：在实施代码重构（在不影响代码产生的结果的前提下对代码内部的构造进行调整）时可能引发退化。&lt;/p&gt;
&lt;p&gt;解决方法：大量的可重用的测试并实施持续集成。&lt;/p&gt;
&lt;h5&gt;问题8：不知道bug的修正日期无法追踪退化&lt;/h5&gt;
&lt;p&gt;解决方法：版本控制系统、缺陷管理系统和持续集成之间需要交互，最好能够和自动化部署工具集成到一起来使用。&lt;/p&gt;
&lt;h5&gt;问题9：发布过程太复杂&lt;/h5&gt;
&lt;p&gt;解决方法：实施持续交付。&lt;/p&gt;
&lt;p&gt;基于对上述问题的阐述和分析，我们基本上可以得到以下的结论，在团队开发中版本控制、缺陷管理和持续集成都是非常重要且不可或缺的。&lt;/p&gt;
&lt;h4&gt;版本控制&lt;/h4&gt;
&lt;p&gt;针对上面提到的一系列问题，我们可以得出一个简单的结论，版本控制是实施团队开发的首要前提，必须通过版本控制对产品研发过程中产生的各种信息进行管理，这些内容包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码。&lt;/li&gt;
&lt;li&gt;需求和设计的相关文档。&lt;/li&gt;
&lt;li&gt;数据库模式和初始数据。&lt;/li&gt;
&lt;li&gt;配置文件。&lt;/li&gt;
&lt;li&gt;库的依赖关系定义。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Git简介&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/6f93f4f2b8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Git是诞生于2005年的一个开源分布式版本控制系统，最初是Linus Torvalds（Linux之父） 为了帮助管理Linux内核开发而开发的一个版本控制软件。Git与常用的版本控制工具Subversion等不同，它采用了分布式版本控制的方式，在没有中央服务器支持的环境下也能够实施版本控制。&lt;/p&gt;
&lt;p&gt;对于有使用Subversion（以下简称为SVN）经验的人来说，Git和SVN的共同点是摒弃了传统的基于锁定模式的版本控制（早期的CVS和VSS使用了锁定模式，当一个开发者编辑一个文件时会锁定该文件，其他开发者在此期间无法编辑该文件），采用了更有效率的基于合并模式的版本控制，而二者的区别在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Git是分布式的，SVN是集中式的，SVN需要中央服务器的支持才能工作。&lt;/li&gt;
&lt;li&gt;Git把内容按元数据方式存储，而SVN是按文件，即把文件的元信息隐藏在一个.svn文件夹里。&lt;/li&gt;
&lt;li&gt;Git分支和SVN的分支不同，SVN对分支的处理是相当“狗血”的。&lt;/li&gt;
&lt;li&gt;Git没有一个全局版本号，但是可以自己维护一个版本标签。&lt;/li&gt;
&lt;li&gt;Git的内容完整性要优于SVN，Git的内容存储使用的是SHA-1哈希算法。这能确保代码内容的完整性，确保在遇到磁盘故障和网络问题时降低对版本库的破坏。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总而言之，&lt;strong&gt;Git真的非常棒！！！&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;安装Git&lt;/h5&gt;
&lt;p&gt;可以在&lt;a href=&quot;http://git-scm.com/&quot;&gt;Git官方网站&lt;/a&gt;找到适合自己系统的Git下载链接并进行安装，macOS和Windows平台下安装Git都非常简单。&lt;/p&gt;
&lt;h5&gt;Git本地操作&lt;/h5&gt;
&lt;p&gt;可以使用下面的命令将一个文件夹变成Git仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git init 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你完成了上述操作后，本地目录就变成了下面的样子，下图左边是你的工作区（正在操作的工作目录），而右边是你的本地仓库，中间是工作区和本地仓库之间的暂存区（也称为缓存区）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/98ed1966a5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：用&lt;code&gt;ls -la&lt;/code&gt;查看所有文件会发现在执行完上面的命令后，文件夹下多了一个名为&lt;code&gt;.git&lt;/code&gt;的隐藏文件夹，这个就是本地的Git版本仓库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过&lt;code&gt;git add&lt;/code&gt;可以将指定的文件或所有文件添加到暂存区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add &amp;lt;file&amp;gt;
git add .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候使用下面的命令可以查看工作区、暂存区和本地仓库的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果不希望将文件添加到暂存区，可以按照提示，使用&lt;code&gt;git rm --cached &amp;lt;file&amp;gt;&lt;/code&gt;命令将文件从暂存区放回到工作区。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果这个时候对工作区的文件又进行了修改使得工作区和暂存区的内容并不相同了，再次执行&lt;code&gt;git status&lt;/code&gt;可以看到哪个或哪些文件被修改了，如果希望用暂存区的内容恢复工作区，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git restore &amp;lt;file&amp;gt;
git restore .
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的命令目前仍然处于试验性阶段，在Git较早的版本中对应的命令是&lt;code&gt;git checkout -- &amp;lt;file&amp;gt;&lt;/code&gt;。由于&lt;code&gt;git checkout&lt;/code&gt;这个命令还可以用于切换分支，容易引起混淆，所以Git最新版本中将这个命令的两项功能分别赋予两个新的命令，一个就是上面的&lt;code&gt;git restore&lt;/code&gt;，另一个是&lt;code&gt;git switch&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果第一次使用Git，需要配置用户名和邮箱，然后才能将代码提交到仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.name &quot;jackfrued&quot;
git config --global user.email &quot;jackfrued@126.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：可以用&lt;code&gt;git config --list&lt;/code&gt;来查看Git的配置信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过下面的命令可以将暂存区的内容纳入本地仓库，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit -m &apos;本次提交的说明&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过&lt;code&gt;git log&lt;/code&gt;查看每次提交对应的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log
git log --graph --oneline --abbrev-commit
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Git服务器概述&lt;/h5&gt;
&lt;p&gt;Git不像SVN那样一定需要中央服务器才能工作，上面我们演示的版本控制操作都是在本地执行的，但是对于企业开发多人协作这样的场景还是需要中央服务器的支持。通常，企业可以选择使用代码托管平台（如&lt;a href=&quot;https://github.com&quot;&gt;GitHub&lt;/a&gt;）或自己搭建Git私服的方式来建立中央服务器（版本仓库），当然大多数的企业更倾向于后者。Github创办于2008年4月，目前是全世界最大的代码托管平台，支持企业用户（可以创建私有仓库，私有仓库内容不对外界公开）和普通用户（受限的使用私有仓库，不受限的使用公开仓库，公开仓库内容对他人可见）。Github上面代码库惊人的增长速度证明了它是非常成功的，在2018年6月被微软以75亿美元的天价收购。&lt;/p&gt;
&lt;p&gt;国内也有不少类似Github的代码托管平台（例如：&lt;a href=&quot;https://gitee.com/&quot;&gt;码云&lt;/a&gt;和&lt;a href=&quot;https://coding.net/&quot;&gt;CODING&lt;/a&gt;），目前码云和CODING对注册用户都提供了受限的使用私有仓库的功能，支持&lt;strong&gt;Pull Request&lt;/strong&gt;（一种对话机制，可以在提交你的工作成果时让相关人员或团队注意到这件事情），同时还提供了对&lt;strong&gt;缺陷管理&lt;/strong&gt;、&lt;strong&gt;Webhook&lt;/strong&gt;等功能支持，这些使得版本控制系统还具备了缺陷管理和持续集成的能力。当然，很多公司都不愿意将自己的商业代码托管于别人的平台，这样的公司可以用&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/about.gitlab.com/%3E&quot;&gt;Gitlab&lt;/a&gt;来搭建公司内部的Git私服，具体的做法在下一章为大家介绍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/702c943d72.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我们直接以码云为例来说明使用Git服务器的一些注意事项。首先需要在码云上注册账号，当然也可以使用第三方登录（github账号、微信账号、新浪微博账号、CSDN账号等），登录成功后就可以创建项目，创建项目几乎是“傻瓜式”的，无需赘述，我们只对几个地方加以说明。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建项目时不建议勾选如下图所示的这些选项，编程语言可以暂时不做选择，而&lt;code&gt;.gitignore&lt;/code&gt;模板也可以稍后自己编写或者通过更专业的工具（如：&lt;a href=&quot;http://gitignore.io/&quot;&gt;http://gitignore.io/&lt;/a&gt;网站）自动生成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/10e8ea32c6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加项目成员。创建项目后，可以在项目的“设置”或“管理”中找到“成员管理”功能，这样就可以将其他开发者设置为项目团队的成员，项目成员通常分为“所有者”、“管理者”、“普通成员”和“受限成员”几种角色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/9c3e7b0164.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;项目的分支。创建项目后，项目只有一个默认的&lt;strong&gt;master&lt;/strong&gt;分支，应该将该分支设置为“保护分支”来避免项目管理者之外的成员修改该分支（不可直接提交）。当然，如果需要我们也可以在线创建新的代码分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置公钥实现免密访问。在项目的“设置”或“管理”中我们还可以找到“部署公钥管理”的选项，通过添加部署公钥，可以通过SSH（安全远程连接）的方式访问服务器而不用每次输入用户名和口令。可以使用&lt;code&gt;ssh-keygen&lt;/code&gt;命令来创建密钥对。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -b 2048 -C &quot;your_email@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面命令生成的密钥对在&lt;code&gt;~/.ssh&lt;/code&gt;目录下，公钥文件默认的名字为&lt;code&gt;id_rsa.pub&lt;/code&gt;，可以通过&lt;code&gt;cat id_rsa.pub&lt;/code&gt;来查看自己的公钥。Windows用户在安装Git工具后，可以通过&lt;strong&gt;Git Bash&lt;/strong&gt;来输入上面的命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Git远程操作&lt;/h5&gt;
&lt;p&gt;拥有了Git服务器之后，我们就可以通过Git的远程操作将自己的工作成果推到服务器的仓库中，也可以将他人的工作成果从服务器仓库更新到本地。我们以刚才在码云上创建的仓库（仓库名为&lt;code&gt;python&lt;/code&gt;）为例来说明如何进行远程操作。可以在如下所示的页面上找到仓库的地址（URL），如果配置了&lt;strong&gt;SSH Key&lt;/strong&gt;就使用SSH方式访问仓库，否则就用HTTPS方式，后者需要在进行远程操作时提供用户名和口令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/191668599d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;添加远程仓库（Git服务器）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin git@gitee.com:jackfrued/python.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;git@gitee.com:jackfrued/python.git&lt;/code&gt;是上图中显示的仓库的URL，而前面的&lt;code&gt;origin&lt;/code&gt;是替代这个冗长的URL的字符串，简单的说&lt;code&gt;origin&lt;/code&gt;就是服务器上仓库的别名（如果有多个Git服务器，这个简短的名字也会有多个）。可以用&lt;code&gt;git remote -v&lt;/code&gt;来查看已经指定的Git服务，也可以用&lt;code&gt;git remote remove&lt;/code&gt;来删除指定的Git服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将本地代码（工作成果）推送到远程仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin master:master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;-u&lt;/code&gt;是&lt;code&gt;--set-upstream&lt;/code&gt;的缩写，用来指定推送的服务器仓库，后面的&lt;code&gt;origin&lt;/code&gt;就是刚才给仓库起的简短的别名，冒号前面的&lt;code&gt;master&lt;/code&gt;是本地分支名，冒号后面的&lt;code&gt;master&lt;/code&gt;是远程分支名，如果本地分支&lt;code&gt;master&lt;/code&gt;已经和远程分支&lt;code&gt;master&lt;/code&gt;建立过关联，则冒号以及后面的部分可以省略。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从远程仓库取回代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git pull origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Git分支操作&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建&lt;/strong&gt;和&lt;strong&gt;切换&lt;/strong&gt;分支。下面的命令创建了名为&lt;code&gt;dev&lt;/code&gt; 的分支并切换到该分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch &amp;lt;branch-name&amp;gt;
git switch &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch -c &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：在之前的Git版本中，切换分支使用&lt;code&gt;git checkout &amp;lt;branch-name&amp;gt;&lt;/code&gt;命令，也可以通过&lt;code&gt;git checkout -b &amp;lt;branch-name&amp;gt;&lt;/code&gt;来创建并切换分支。&lt;code&gt;git switch&lt;/code&gt;命令目前仍然处于试验性阶段，但很明显这个命令更加清晰的表达了它要做的事情。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关联远程&lt;/strong&gt;分支。例如：如果当前所在的分支还没有关联到远程分支，可以使用下面的命令为它们建立关联。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --set-upstream-to origin/develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要为指定的分支关联远程分支，可以如下操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --set-upstream-to origin/develop &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面的操作假设Git服务器上存在名为&lt;code&gt;develop&lt;/code&gt;的分支，&lt;code&gt;--set-upstream-to&lt;/code&gt;可以缩写为&lt;code&gt;-u&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，在创建分支时，如果使用了&lt;code&gt;--track&lt;/code&gt;参数，也可以直接指定与本地分支关联的远程分支，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --track &amp;lt;branch-name&amp;gt; origin/develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要解除本地分支与远程分支的关联，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --unset-upstream &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分支&lt;strong&gt;合并&lt;/strong&gt;。例如在&lt;code&gt;dev&lt;/code&gt;分支上完成开发任务之后，如果希望将&lt;code&gt;dev&lt;/code&gt;分支上的成果合并到&lt;code&gt;master&lt;/code&gt;，可以先切回到&lt;code&gt;master&lt;/code&gt;分支然后使用&lt;code&gt;git merge&lt;/code&gt;来做分支合并，合并的结果如下图右上方所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch master
git merge --no-ff dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;git merge&lt;/code&gt;合并分支时，默认使用&lt;code&gt;Fast Forward&lt;/code&gt;合并，这意味着如果删除了分支，分支上的信息就全都丢掉了，如果希望将分支上的历史版本保留下来，可以使用&lt;code&gt;--no-ff&lt;/code&gt;参数来禁用&lt;code&gt;Fast Forward&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在合并分支时，没有冲突的部分Git会做自动合并。如果发生了冲突（如&lt;code&gt;dev&lt;/code&gt;和&lt;code&gt;master&lt;/code&gt;分支上都修改了同一个文件），会看到&lt;code&gt;CONFLICT (content): Merge conflict in &amp;lt;filename&amp;gt;. Automatic merge failed; fix conflicts and then commit the result&lt;/code&gt;（自动合并失败，修复冲突之后再次提交）的提示，这个时候我们可以用&lt;code&gt;git diff&lt;/code&gt;来查看产生冲突的内容。解决冲突通常需要当事人当面沟通之后才能决定保留谁的版本，冲突解决后需要重新提交代码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分支&lt;strong&gt;变基&lt;/strong&gt;。分支合并操作可以将多个分支上的工作成果最终合并到一个分支上，但是再多次合并操作之后，分支可能会变得非常的混乱和复杂，为了解决这个问题，可以使用&lt;code&gt;git rebase&lt;/code&gt;操作来实现分支变基。如下图所示，当我们希望将&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;dev&lt;/code&gt;上的工作成果统一到一起的时候，也可以使用变基操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/9300702792.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rebase master
git switch master
git merge dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在&lt;code&gt;dev&lt;/code&gt;分支执行&lt;code&gt;git rebase&lt;/code&gt;命令时，将首先计算&lt;code&gt;dev&lt;/code&gt;分支和&lt;code&gt;master&lt;/code&gt;分支的差集，然后应用该差集到&lt;code&gt;dev&lt;/code&gt;分支，最后我们切回到&lt;code&gt;master&lt;/code&gt;分支并执行操作合并，这样就看到了如上图右下方所示的干净的分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;删除&lt;/strong&gt;分支。删除分支可以使用&lt;code&gt;git branch&lt;/code&gt;加上&lt;code&gt;-d&lt;/code&gt;参数，如果分支上的工作成果还没有合并，那么在删除分支时会看到&lt;code&gt;error: The branch &apos;&amp;lt;branch-name&amp;gt;&apos; is not fully merged.&lt;/code&gt;这样的错误提示。如果希望强行删除分支，可以使用&lt;code&gt;-D&lt;/code&gt;参数。删除分支的操作如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -d &amp;lt;branch-name&amp;gt;
error: The branch &apos;&amp;lt;branch-name&amp;gt;&apos; is not fully merged.
If you are sure you want to delete it, run &apos;git branch -D &amp;lt;branch-name&amp;gt;&apos;.
git branch -D &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要删除远程分支，可以使用下面的命令，但是请慎重的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -r -d origin/develop
git push origin :develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin --delete develop
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Git其他操作&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git fetch&lt;/code&gt;：下载远程仓库的所有变动，可以将远程仓库下载到一个临时分支，然后再根据需要进行合并操作，&lt;code&gt;git fetch&lt;/code&gt;命令和&lt;code&gt;git merge&lt;/code&gt;命令可以看作是之前讲的&lt;code&gt;git pull&lt;/code&gt;命令的分解动作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git fetch origin master:temp
git merge temp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git diff&lt;/code&gt;：常用于比较工作区和仓库、暂存区与仓库、两个分支之间有什么差别。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git stash&lt;/code&gt;：将当前工作区和暂存区发生的变动放到一个临时的区域，让工作区变干净。这个命令适用于手头工作还没有提交，但是突然有一个更为紧急的任务（如线上bug需要修正）需要去处理的场景。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash
git stash list
git stash pop
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt;：回退到指定的版本。该命令主要有三个参数，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/9447b9d4fc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git cherry-pick&lt;/code&gt;：挑选某个分支的单次提交并作为一个新的提交引入到你当前分支上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt;：撤回提交信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git tag&lt;/code&gt;：经常用于查看或新增一个标签。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Git工作流程（分支管理策略）&lt;/h5&gt;
&lt;p&gt;既然Git是团队开发必备的工具，那么在团队协作时就必须有一个规范的工作流程，这样才能让团队高效的工作，让项目顺利的进展下去，否则工具再厉害但团队成员各自为战，冲突就会无处不在，协作更加无从谈起。我们仍然以刚才码云上创建的&lt;code&gt;python&lt;/code&gt;项目为例，来说明Git的分支管理策略。&lt;/p&gt;
&lt;h6&gt;Github-flow&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;克隆服务器上的代码到本地。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@gitee.com:jackfrued/python.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建并切换到自己的分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch -c &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在自己的分支上开发并在本地做版本控制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将自己的分支（工作成果）推到服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在线发起一次合并请求（通常称之为&lt;strong&gt;Pull Request&lt;/strong&gt;，有的地方称为&lt;strong&gt;Merge Request&lt;/strong&gt;），请求将自己的工作成果合并到&lt;code&gt;master&lt;/code&gt;分支，合并之后可以删除该分支。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/814d721d92.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面这种分支管理策略就是被称为&lt;strong&gt;github-flow&lt;/strong&gt;或&lt;strong&gt;PR&lt;/strong&gt;的流程，它非常简单容易理解，只需要注意以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;master&lt;/code&gt;的内容都是可以进行发布的内容（不能直接在&lt;code&gt;master&lt;/code&gt;上进行修改）。&lt;/li&gt;
&lt;li&gt;开发时应该以&lt;code&gt;master&lt;/code&gt;为基础建立新分支（日常开发任务在自己的分支上进行）。&lt;/li&gt;
&lt;li&gt;分支先在本地实施版本控制，然后以同名分支定期向服务器进行push操作。&lt;/li&gt;
&lt;li&gt;开发任务完成后向&lt;code&gt;master&lt;/code&gt;发送合并请求。&lt;/li&gt;
&lt;li&gt;合并请求通过审查之后合并到&lt;code&gt;master&lt;/code&gt;，并从&lt;code&gt;master&lt;/code&gt;向正式环境发布。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，github-flow的缺点也很明显，&lt;code&gt;master&lt;/code&gt;分支默认就是当前的线上代码，但是有的时候工作成果合并到&lt;code&gt;master&lt;/code&gt;分支，并不代表它就能立刻发布，这样就会导致线上版本落后于&lt;code&gt;master&lt;/code&gt;分支。&lt;/p&gt;
&lt;h6&gt;Git-flow&lt;/h6&gt;
&lt;p&gt;除了上述的github-flow分支管理策略外，还有一种名为git-flow的分支管理策略，它也是大多数公司愿意使用的一套流程。Git-flow借鉴了中央集权型版本控制系统的长处，为团队内部统一建立、合并和关闭分支的方法，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/e9326c6fe2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这种模式下，项目有两个长线分支，分别是&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;develop&lt;/code&gt;，其他都是临时的的辅助分支，包括&lt;code&gt;feature&lt;/code&gt;（开发特定功能的分支，开发结束后合并到&lt;code&gt;develop&lt;/code&gt;）、&lt;code&gt;release&lt;/code&gt;（从&lt;code&gt;develop&lt;/code&gt;分离出来的为发布做准备的分支，发布结束后合并到&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;develop&lt;/code&gt;）和&lt;code&gt;hotfix&lt;/code&gt;（产品发布后出现问题时紧急建立的分支，直接从&lt;code&gt;master&lt;/code&gt;分离，问题修复后合并到&lt;code&gt;master&lt;/code&gt;并打上标签，同时还要合并到&lt;code&gt;develop&lt;/code&gt;来避免将来的版本遗漏了这个修复工作，如果此时有正在发布中的&lt;code&gt;release&lt;/code&gt;分支，还要合并到&lt;code&gt;release&lt;/code&gt;分支）。具体的实施过程如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/ebc07b4c35.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最开始的时候只有&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;develop&lt;/code&gt;分支，如上图左侧所示。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从&lt;code&gt;develop&lt;/code&gt;分支创建&lt;code&gt;feature&lt;/code&gt;分支（上图右上），工作完成后将工作成果合并到&lt;code&gt;develop&lt;/code&gt;分支（上图右中）。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;feature&lt;/code&gt;分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git switch -c feature/user develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b feature/user develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就是在&lt;code&gt;feature&lt;/code&gt;分支上进行开发并实施版本控制，这一段如何操作我们就不再赘述了。工作完成后，将&lt;code&gt;feature&lt;/code&gt;分支合并到&lt;code&gt;develop&lt;/code&gt;分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout develop
git merge --no-ff feature/user
git branch -d feature/user
git push origin develop
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从&lt;code&gt;develop&lt;/code&gt;分支创建&lt;code&gt;release&lt;/code&gt;分支，发布结束后合并回&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;develop&lt;/code&gt;分支。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;release&lt;/code&gt;分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b release-0.1 develop
git push -u origin release-0.1
... ... ...
git pull
git commit -a -m &quot;............&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;code&gt;release&lt;/code&gt;分支合并回&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;develop&lt;/code&gt;分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master
git merge --no-ff release-0.1
git push

git checkout develop
git merge --no-ff release-0.1
git push

git branch -d release-0.1
git push --delete release-0.1
git tag v0.1 master
git push --tags
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从&lt;code&gt;master&lt;/code&gt;分支创建&lt;code&gt;hotfix&lt;/code&gt;分支，在修复bug后合并到&lt;code&gt;develop&lt;/code&gt;和&lt;code&gt;master&lt;/code&gt;分支（上图右下）。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;hotfix&lt;/code&gt;分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b hotfix-0.1.1 master
git push -u origin hotfix-0.1.1
... ... ...
git pull
git commit -a -m &quot;............&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;code&gt;hotfix&lt;/code&gt;分支合并回&lt;code&gt;develop&lt;/code&gt;和&lt;code&gt;master&lt;/code&gt;分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master
git merge --no-ff hotfix-0.1.1
git push

git checkout develop
git merge --no-ff hotfix-0.1.1
git push

git branch -d hotfix-0.1.1
git push --delete hotfix-0.1.1
git tag v0.1.1 master
git push --tags
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Git-flow流程比较容易控制各个分支的状况，但是在运用上github-flow要复杂得多，因此实际使用的时候通常会安装名为&lt;code&gt;gitflow&lt;/code&gt;的命令行工具（Windows环境的Git自带了该工具）或者使用图形化的Git工具（如：SmartGit、SourceTree等）来简化操作，具体的可以参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/git-flow%3E&quot;&gt;《git-flow 的工作流程》&lt;/a&gt;一文，因为这篇文章写得已经很好了，本文不再进行赘述。&lt;/p&gt;
&lt;h4&gt;缺陷管理&lt;/h4&gt;
&lt;p&gt;没有好的团队管理工具必然导致项目进展不顺利，任务管理困难，而引入缺陷管理系统正好可以解决这些问题，通常一个缺陷管理系统都包含了以下的功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务管理（包括必须做什么、谁来做、什么时候完成、现在处于什么状态等）。&lt;/li&gt;
&lt;li&gt;直观而且可以检索过去发生的各种问题。&lt;/li&gt;
&lt;li&gt;能够对信息进行统一的管理和共享。&lt;/li&gt;
&lt;li&gt;能够生成各类报表。&lt;/li&gt;
&lt;li&gt;能够关联到其他系统，具有可扩展性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;禅道&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.zentao.net/%3E&quot;&gt;禅道&lt;/a&gt;是国产的专业项目管理软件，它不仅仅是缺陷管理工具，它提供了完整软件生命周期管理功能，支持&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttp:/www.scrumcn.com/agile/scrum-knowledge-library/scrum.html%3E&quot;&gt;Scrum敏捷开发&lt;/a&gt;，能够实现需求管理、缺陷管理、任务管理等一系列的功能，而且拥有强大的扩展机制和丰富的功能插件。可以从禅道的官方网站提供的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.zentao.net/download.html%3E&quot;&gt;下载链接&lt;/a&gt;来下载禅道，推荐使用一键安装包。&lt;/p&gt;
&lt;p&gt;下面仍然以CentOS Linux为例，讲解如何利用官方提供的一键安装包来安装禅道。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /opt
wget http://dl.cnezsoft.com/zentao/pro8.5.2/ZenTaoPMS.pro8.5.2.zbox_64.tar.gz
gunzip ZenTaoPMS.pro8.5.2.zbox_64.tar.gz
tar -xvf ZenTaoPMS.pro8.5.2.zbox_64.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在&lt;code&gt;/opt&lt;/code&gt;目录下（官方推荐使用这个目录）下载了禅道的归档压缩文件，并进行了解压缩和解归档的操作，完成上述步骤后，会看到一个名为&lt;code&gt;zbox&lt;/code&gt;的文件夹。一键安装包中内置了Apache、MySQL、PHP等应用，也就是说这些都不需要单独安装部署了，接下来我们通过下面的命令来启动禅道。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/opt/zbox/zbox -ap 8080 -mp 3307
/opt/zbox/zbox start
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面使用&lt;code&gt;zbox&lt;/code&gt;文件夹下的&lt;code&gt;zbox&lt;/code&gt;命令，其中&lt;code&gt;-ap&lt;/code&gt;是为了指定Apache服务器使用的端口，&lt;code&gt;-mp&lt;/code&gt;是为了指定MySQL数据库使用的端口，这里使用3307端口是为了避开服务器上可能已经存在的MySQL服务的3306端口；&lt;code&gt;start&lt;/code&gt;表示启动服务，&lt;code&gt;stop&lt;/code&gt;可以用来停止服务。此外，需要打开防火墙8080端口以便访问禅道，注意&lt;strong&gt;数据库的端口决不能暴露给公网&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;打开浏览器，输入服务器的公网IP地址就可以访问禅道，如果愿意，也可以通过DNS解析绑定一个域名来进行访问，禅道的首页如下图所示，默认的管理员是&lt;code&gt;admin&lt;/code&gt;，口令是&lt;code&gt;123456&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第一次使用禅道时，建议通过点击用户名，然后通过“帮助”菜单的“新手教程”来迅速了解禅道。官方网站的文档链接中提供了&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.zentao.net/video/c1454.html%3E&quot;&gt;视频教程&lt;/a&gt;，初学者也可以通过视频教程来上手。&lt;/p&gt;
&lt;p&gt;对敏捷开发以及敏捷闭环工具不是特别了解的，可以参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/blog.51cto.com/newthink/1775427%3E&quot;&gt;《基于JIRA的Scrum敏捷开发的项目管理》&lt;/a&gt;一文。&lt;/p&gt;
&lt;h5&gt;GitLab&lt;/h5&gt;
&lt;p&gt;常用的代码托管平台和之前提到的Git私服Gitlab都提供了缺陷管理的功能，当我们要报告一个bug时，可以在如下图所示的界面创建一个新的问题票（issue ticket）。填写的内容包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**[必填]**出现问题的软件版本号、具体的使用环境（如操作系统）等相关信息。&lt;/li&gt;
&lt;li&gt;**[必填]**能够稳定重现该问题的相关步骤。&lt;/li&gt;
&lt;li&gt;**[必填]**描述此处期待的行为和实际的行为。&lt;/li&gt;
&lt;li&gt;**[可选]**你对这个bug的看法（产生bug的原因是什么）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/1dc8bec27c.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，我们在创建问题票时，还需要将问题指派给处理问题的人，如果不清楚应该由谁来修复这个bug，就指派给项目管理者，除此之外还要指定问题的优先级（十分紧急、紧急、普通、不紧急等）、问题的标签（功能缺陷、新特性、改进增强、前瞻研究等）、里程碑（通过里程碑可以将问题与某些特定的项目节点关联起来，之后可以查看每一个里程碑的进展，可以基于软件版本号来建立里程碑，也可以基于迭代周期来建立里程碑）以及需要在哪个时间点以前修复等信息。&lt;/p&gt;
&lt;p&gt;有些敏捷团队使用问题票来管理产品的需求，称之为“问题驱动开发”（TiDD），也就是说新功能的开发是通过创建问题票来驱动的，具体的步骤包括：建立问题票、指定责任人、开发、提交、Push到代码库。如果要创建一个和需求相关的问题票，应该要填写以下的内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**[必填]**简短的描述需求，并用它作为标题。&lt;/li&gt;
&lt;li&gt;**[必填]**这个需求是解决什么问题的。&lt;/li&gt;
&lt;li&gt;**[必填]**这个需求对软件现有功能会造成什么影响。&lt;/li&gt;
&lt;li&gt;**[必填]**这个需求应该实现什么样的功能。&lt;/li&gt;
&lt;li&gt;**[必填]**这个需求是否依赖其他模块提供相关支持。&lt;/li&gt;
&lt;li&gt;**[可选]**这个需求有哪些实现方式。&lt;/li&gt;
&lt;li&gt;**[可选]**这些可选的实现方式分别有哪些优缺点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;其他产品&lt;/h5&gt;
&lt;p&gt;除了禅道和GitLab之外，&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.atlassian.com/zh/software/jira%3E&quot;&gt;JIRA&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.redmine.org/%3E&quot;&gt;Redmine&lt;/a&gt;、Backlog等也是不错的缺陷管理系统。目前，这些系统大都不仅仅提供了缺陷管理的功能，更多的时候它们可以作为敏捷闭环工具来使用，关于敏捷闭环工具这个话题，请大家参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/blog.51cto.com/newthink/1775427%3E&quot;&gt;《基于JIRA的Scrum敏捷开发的项目管理》&lt;/a&gt;一文。&lt;/p&gt;
&lt;h4&gt;持续集成&lt;/h4&gt;
&lt;p&gt;为了快速的产出高质量的软件，在团队开发中持续集成（CI）是一个非常重要的环节。所谓CI，就是一种让计算机自动任意次重复编译、测试、汇报等工作的方法，通过CI可以帮助开发者提早发现问题，降低各种人为失误给项目带来的风险。按照经典的软件过程模型（瀑布模型），集成的工作一般要等到所有的开发工作都结束后才能开始，但这个时候如果发现了问题，修复问题的代价是非常具体的。基本上，集成实施得越晚，代码量越大，解决问题就越困难。持续集成将版本控制、自动化构建、代码测试融入到一起，让这些工作变得自动化和可协作。由于其频繁重复整个开发流程（在指定时间内多次pull源代码并运行测试代码），所以能帮助开发者提早发现问题。&lt;/p&gt;
&lt;p&gt;在所有的CI工具中，Jenkins和&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.travis-ci.org/%3E&quot;&gt;TravisCI&lt;/a&gt;是最具有代表性的，前者是基于 Java的开源CI工具，后者是新晋的在线CI工具，下图是Jenkins的工作面板。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/96cc1e6be0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;持续集成对于编译型语言的意义更大，对于Python这样的解释型语言，更多的时候是用于对接版本控制系统触发自动化测试并产生相应的报告，类似的功能也可以通过配置&lt;strong&gt;Webhook&lt;/strong&gt;来完成。如果要通过Docker这样的虚拟化容器进行项目打包部署或者通过K8S进行容器管理，可以在持续集成平台安装对应的插件来支持这些功能。码云甚至可以直接对接&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/ding-doc.dingtalk.com/%3E&quot;&gt;钉钉开放平台&lt;/a&gt;使用钉钉机器人来向项目相关人员发送即时消息。GitLab也对CI和CD（持续交付）提供了支持，具体内容请大家参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/blog.stdioa.com/2018/06/gitlab-cicd-fundmental/%3E&quot;&gt;《GitLab CI/CD基础教程》&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关于敏捷开发的相关内容，有兴趣的读者可以阅读知乎上的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/zhuanlan.zhihu.com/p/33472102%3E&quot;&gt;《这才是敏捷开发》&lt;/a&gt;一文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本章中的部分插图来自于网易云课堂&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/study.163.com/course/introduction/1003268008.htm%3E&quot;&gt;《人人都会用Git》&lt;/a&gt;课程（免费哟），在此表示感谢。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;92 Docker容器技术详解&lt;/h2&gt;
&lt;p&gt;Docker是基于Go语言开发的开源应用容器引擎，遵从Apache Licence 2.0协议，可以让开发者打包应用以及应用的依赖包到一个可移植的容器中，然后发布到各种发行版本的Linux系统上。&lt;/p&gt;
&lt;h4&gt;Docker简介&lt;/h4&gt;
&lt;p&gt;软件开发中最为麻烦的事情可能就是配置环境了。由于用户使用的操作系统具有多样性，即便使用跨平台的开发语言（如Java和Python）都不能保证代码能够在各种平台下都可以正常的运转，而且在不同的环境下我们安装的软件需要依赖的软件包也是不一样的。&lt;/p&gt;
&lt;p&gt;那么问题来了，我们安装软件的时候可不可以把软件运行的环境一并安装？我们是不是可以把原始环境一模一样地复制过来呢？&lt;/p&gt;
&lt;p&gt;虚拟机（virtual machine）就是带环境安装的一种解决方案，它可以在一种操作系统里面运行另一种操作系统，比如在Windows系统里面运行Linux系统，在macOS上运行Windows，而应用程序对此毫无感知。使用过虚拟机的人都知道，虚拟机用起来跟真实系统一模一样，而对于虚拟机的宿主系统来说，虚拟机就是一个普通文件，不需要了就删掉，对宿主系统或者其他的程序并没有影响。但是虚拟机通常会占用较多的系统资源，启动和关闭也非常的缓慢，总之用户体验并没有想象中的那么好。&lt;/p&gt;
&lt;p&gt;Docker属于对Linux容器技术（LXC）的一种封装（利用了Linux的namespace和cgroup技术），它提供了简单易用的容器使用接口，是目前最流行的 Linux 容器解决方案。Docker将应用程序与该程序的依赖打包在一个文件里面，运行这个文件，就会生成一个虚拟容器。程序在这个虚拟容器里运行，就好像在真实的物理机上运行一样。下图是虚拟机和容器的对比，左边是传统的虚拟机，右边是Docker。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/2678a980e6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前，Docker主要用于几下几个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;提供一次性的环境。&lt;/li&gt;
&lt;li&gt;提供弹性的云服务（利用Docker很容易实现扩容和收缩）。&lt;/li&gt;
&lt;li&gt;实践微服务架构（隔离真实环境在容器中运行多个服务）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;安装Docker&lt;/h4&gt;
&lt;p&gt;下面以CentOS为例讲解如何安装Docker，使用&lt;a href=&quot;https://docs.docker.com/install/linux/docker-ce/ubuntu/&quot;&gt;Ubuntu&lt;/a&gt;、&lt;a href=&quot;https://docs.docker.com/docker-for-mac/install/&quot;&gt;macOS&lt;/a&gt;或&lt;a href=&quot;https://docs.docker.com/docker-for-windows/install/&quot;&gt;Windows&lt;/a&gt;的用户可以通过点击对应的链接了解这些平台下如何进行安装。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定操作系统内核版本（CentOS 7要求64位，内核版本3.10+；CentOS 6要求64位，内核版本2.6+）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新系统底层的库文件（建议一定要执行，否则在使用Docker时可能会出现莫名其妙的问题）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum update
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移除可能存在的旧的Docker版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum list installed | grep docker
yum erase -y docker docker-common docker-engine
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装yum工具包和依赖项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y yum-utils device-mapper-persistent-data lvm2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过yum工具包添加yum源（安装Docker-ce的源）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在CentOS下使用yum安装Docker-ce并启动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum -y install docker-ce
systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看Docker的信息和版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker version
docker info
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来可以通过下载镜像和创建容器来看看Docker是否可以运转起来。可以使用下面的命令从Docker的镜像仓库下载名为hello-world的镜像文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看所有镜像文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;REPOSITORY               TAG        IMAGE ID            CREATED             SIZE
docker.io/hello-world    latest     fce289e99eb9        7 months ago        1.84 kB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过镜像文件创建并运行容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container run --name mycontainer hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：其中&lt;code&gt;mycontainer&lt;/code&gt;是我们给容器起的名字，跟在&lt;code&gt;--name&lt;/code&gt;参数之后；&lt;code&gt;hello-world&lt;/code&gt;就是我们刚才下载的镜像文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要删除这个容器，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container rm mycontainer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在删除容器之后，我们还可以删除刚才下载的镜像文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rmi hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果要在Ubuntu（内核版本3.10+）下面安装和启动Docker，可以按照如下的步骤进行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update
apt install docker-ce
service docker start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;国内用户可以通过更换Ubuntu软件下载源来提升下载速度，具体请参照清华大学开源软件镜像站上的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/mirrors.tuna.tsinghua.edu.cn/help/ubuntu/%3E&quot;&gt;《Ubuntu镜像使用帮助》&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装Docker后，由于直接访问&lt;a href=&quot;https://hub.docker.com/&quot;&gt;dockerhub&lt;/a&gt;下载镜像会非常缓慢，建议将服务器更换为国内镜像，可以通过修改 &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; 文件来做到。一般的云服务器会有自己专属的镜像，就不需要手动修改了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;registry-mirrors&quot;: [
        &quot;http://hub-mirror.c.163.com&quot;,
        &quot;https://registry.docker-cn.com&quot;
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用Docker&lt;/h4&gt;
&lt;p&gt;想要玩转Docker，最简单的办法就是马上用Docker创建一些自己学习和工作中需要用到的容器，下面我们带着大家一起来创建这些容器。&lt;/p&gt;
&lt;h5&gt;运行Nginx&lt;/h5&gt;
&lt;p&gt;Nginx是高性能的Web服务器，同时也是做反向代理服务器的上佳选择。使用Docker可以非常简单的创建一个运行Nginx的容器，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container run -d -p 80:80 --rm --name mynginx nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面的参数&lt;code&gt;-d&lt;/code&gt;表示容器在后台运行（不产生输出到Shell）并显示容器的ID；&lt;code&gt;-p&lt;/code&gt;是用来映射容器的端口到宿主机的端口，冒号前面是宿主机的端口，冒号后面是容器内部使用的端口；&lt;code&gt;--rm&lt;/code&gt;表示容器停止后自动删除容器，例如执行命令&lt;code&gt;docker container stop mynginx&lt;/code&gt;后，容器就不复存在了；&lt;code&gt;--name&lt;/code&gt;后面的mynginx是自定义的容器名字；在创建容器的过程中，需要用到nginx的镜像文件，镜像文件的下载是自动完成的，如果没有指定版本号，默认是最新版本（latest）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果需要将自己的Web项目（页面）部署到Nginx上，可以使用容器拷贝命令将指定路径下所有的文件和文件夹拷贝到容器的指定目录中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container cp /root/web/index.html mynginx:/usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不愿意拷贝文件也可以在创建容器时通过数据卷操作&lt;code&gt;--volume&lt;/code&gt;将指定的文件夹映射到容器的某个目录中，例如将Web项目的文件夹直接映射到&lt;code&gt;/usr/share/nginx/html&lt;/code&gt;目录。我们先通过下面的命令让刚才创建的容器停止运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container stop mynginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后用下面的命令重新创建容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container run -d -p 80:80 --rm --name mynginx --volume /root/docker/nginx/html:/usr/share/nginx/html nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面创建容器和拷贝文件的命令中，&lt;code&gt;container&lt;/code&gt;是可以省略的，也就是说&lt;code&gt;docker container run&lt;/code&gt;和&lt;code&gt;docker run&lt;/code&gt;是一样的，而&lt;code&gt;docker container cp&lt;/code&gt;和&lt;code&gt;docker cp&lt;/code&gt;是一样的。此外，命令中的&lt;code&gt;--volume&lt;/code&gt;也可以缩写为&lt;code&gt;-v&lt;/code&gt;，就如同&lt;code&gt;-d&lt;/code&gt;是&lt;code&gt;--detach&lt;/code&gt;的缩写，&lt;code&gt;-p&lt;/code&gt;是&lt;code&gt;--publish&lt;/code&gt;的缩写。&lt;code&gt;$PWD&lt;/code&gt;代表宿主系统当前文件夹，这些对于使用过Unix或者Linux系统的人来说，应该是很容易理解的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;要查看运行中的容器，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;CONTAINER ID    IMAGE    COMMAND                  CREATED            STATUS             PORTS                 NAMES
3c38d2476384    nginx    &quot;nginx -g &apos;daemon ...&quot;   4 seconds ago      Up 4 seconds       0.0.0.0:80-&amp;gt;80/tcp    mynginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要启动和停止容器，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker start mynginx
docker stop mynginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于在创建容器时使用了&lt;code&gt;--rm&lt;/code&gt;选项，容器在停止时会被移除，当我们使用下面的命令查看所有容器时，应该已经看不到刚才的&lt;code&gt;mynginx&lt;/code&gt;容器了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container ls -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在创建容器时没有指定&lt;code&gt;--rm&lt;/code&gt;选项，那么也可以使用下面的命令来删除容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rm mynginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要删除正在运行中的容器，需要使用&lt;code&gt;-f&lt;/code&gt;选项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker rm -f mynginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;运行MySQL&lt;/h5&gt;
&lt;p&gt;我们再来尝试用Docker安装一台MySQL服务器，首先可以先检查一下有没有MySQL的镜像文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INDEX        NAME            DESCRIPTION        STARS        OFFICIAL        AUTOMATED
docker.io    docker.io/mysql MySQL is a ...     8486         [OK]
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面查询结果的列依次代表索引、镜像名、镜像描述、用户评价、是否官方镜像、自动构建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下载MySQL镜像并指定镜像的版本号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull mysql:5.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要查看已经下载的镜像文件，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker.io/nginx     latest              e445ab08b2be        2 weeks ago         126 MB
docker.io/mysql     5.7                 f6509bac4980        3 weeks ago         373 MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建并运行MySQL容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 3306:3306 --name mysql57 -v /root/docker/mysql/conf:/etc/mysql/mysql.conf.d -v /root/docker/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面创建容器时我们又一次使用了数据卷操作，那是因为通常容器是随时创建随时删除的，而数据库中的数据却是需要保留下来的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的两个数据卷操作一个是映射了MySQL配置文件所在的文件夹，一个是映射了MySQL数据所在的文件夹，这两个数据卷操作非常重要。我们可以将MySQL的配置文件放在&lt;code&gt;$PWD/mysql/conf&lt;/code&gt;目录下，配置文件的具体内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
log-error=/var/log/mysql/error.log
server-id=1
log-bin=/var/log/mysql/mysql-bin.log
expire_logs_days=30
max_binlog_size=256M
symbolic-links=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果安装了MySQL 8.x版本（目前的最新版本），在使用客户端工具连接服务器时可能会遇到&lt;code&gt;error 2059: Authentication plugin &apos;caching_sha2_password&apos; cannot be loaded&lt;/code&gt;的问题，这是因为MySQL 8.x默认使用了名为“caching_sha2_password”的机制对用户口令进行了更好的保护，但是如果客户端工具不支持新的认证方式，连接就会失败。解决这个问题有两种方式：一是升级客户端工具来支持MySQL 8.x的认证方式；二是进入容器，修改MySQL的用户口令认证方式。下面是具体的步骤，我们先用&lt;code&gt;docker exec&lt;/code&gt;命令进入容器的交互式环境，假设运行MySQL 8.x的容器名字叫&lt;code&gt;mysql8x&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it mysql8x /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入容器的交互式Shell之后，可以首先利用MySQL的客户端工具连接MySQL服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p
Enter password:
Your MySQL connection id is 16
Server version: 8.0.12 MySQL Community Server - GPL
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type &apos;help;&apos; or &apos;\h&apos; for help. Type &apos;\c&apos; to clear the current input statement.
mysql&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来通过SQL来修改用户口令就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter user &apos;root&apos;@&apos;%&apos; identified with mysql_native_password by &apos;123456&apos; password expire never;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，如果愿意你也可以查看一下用户表检查是否修改成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mysql;
select user, host, plugin, authentication_string from user where user=&apos;root&apos;;
+------+-----------+-----------------------+-------------------------------------------+
| user | host      | plugin                | authentication_string                     |
+------+-----------+-----------------------+-------------------------------------------+
| root | %         | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
| root | localhost | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
+------+-----------+-----------------------+-------------------------------------------+
2 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完成上面的步骤后，现在即便不更新客户端工具也可以连接MySQL 8.x了。&lt;/p&gt;
&lt;h5&gt;运行Redis&lt;/h5&gt;
&lt;p&gt;接下来我们试一试运行多个容器并让多个容器之间通过网络通信。我们创建4个Redis容器来实现一主三从的主从复制结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 6379:6379 --name redis-master redis
docker run -d -p 6380:6379 --name redis-slave-1 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
docker run -d -p 6381:6379 --name redis-slave-2 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
docker run -d -p 6382:6379 --name redis-slave-3 --link redis-master:redis-master redis redis-server --replicaof redis-master 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令中，&lt;code&gt;--link&lt;/code&gt;参数用于给容器创建网络别名，因为三台从机（slave）需要通过网络连接自己的主机（master）。虽然，我们可以通过&lt;code&gt;docker inspect --format &apos;{{ .NetworkSettings.IPAddress }}&apos; &amp;lt;container-ID&amp;gt;&lt;/code&gt;命令来查看到容器的IP地址，但是由于容器的即装即用性，容器的IP地址有可能会发生变化，如果直接使用IP地址，在容器重启后就可能会因为IP地址的变化导致从机无法连接到主机。使用&lt;code&gt;--link&lt;/code&gt;参数创建网络别名就是为了在启动Redis服务器时在&lt;code&gt;redis-server&lt;/code&gt;后面的&lt;code&gt;--replicaof&lt;/code&gt;参数后使用这个别名而不是IP地址。&lt;/p&gt;
&lt;p&gt;接下来我们进入名为&lt;code&gt;redis-master&lt;/code&gt;的容器，看看主从复制的配置是否成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it redis-master /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;redis-cli&lt;/code&gt;启动命令行工具。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli
127.0.0.1:6379&amp;gt; info replication
# Replication
role:master
connected_slaves:3
slave0:ip=172.17.0.4,port=6379,state=online,offset=1988,lag=0
slave1:ip=172.17.0.5,port=6379,state=online,offset=1988,lag=1
slave2:ip=172.17.0.6,port=6379,state=online,offset=1988,lag=1
master_replid:94703cfa03c3ddc7decc74ca5b8dd13cb8b113ea
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1988
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1988
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;运行GitLab&lt;/h5&gt;
&lt;p&gt;GitLab是由GitLab Inc.开发的Git仓库管理工具，具有wiki、问题跟踪、持续集成等一系列的功能，分为社区版和企业版。通过Docker提供的虚拟化容器，我们可以安装社区版的Docker。因为GitLab需要使用SSH协议进行安全连接，我们要暴露容器的22端口，所以可以先将宿主机SSH连接的22端口修改为其他端口（如：12345），然后再进行后续的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将其中定义端口的那行代码去掉注释并将端口修改为12345。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Port 12345
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新启动&lt;code&gt;sshd&lt;/code&gt;服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：修改端口后应该确保防火墙上也开启对应的端口，否则无法使用SSH连接到Linux服务器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建需要用于数据卷映射操作的文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /root/gitlab/{config,logs,data}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基于&lt;code&gt;gitlab/gitlab-ce&lt;/code&gt;镜像创建容器，并暴露80端口（HTTP连接）和22端口（SSH连接）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 80:80 -p 22:22 --name gitlab -v /root/gitlab/config:/etc/gitlab -v /root/gitlab/logs:/var/log/gitlab -v /root/gitlab/data:/var/opt/gitlab gitlab/gitlab-ce
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：GitLab的启动比较缓慢，创建好容器后可能需要等待一段时间才能通过浏览器来进行访问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首次进入GitLab访问界面会提示我们修改管理员密码，设置好管理员密码后就可以在登录界面输入用户名&lt;code&gt;root&lt;/code&gt;和刚才设置的密码登录到管理员控制台，在使用上还是非常简单和人性化的。&lt;/p&gt;
&lt;h4&gt;构建镜像&lt;/h4&gt;
&lt;p&gt;通过上面的讲解，我们已经掌握了如何通过官方提供的镜像来创建容器。当然如果愿意，我们也可以用配置好的容器来生成镜像。简而言之，&lt;strong&gt;Docker镜像是由文件系统叠加而成的，系统的最底层是bootfs，相当于就是Linux内核的引导文件系统；接下来第二层是rootfs，这一层可以是一种或多种操作系统（如Debian或Ubuntu文件系统），Docker中的rootfs是只读状态的；Docker利用联合挂载技术将各层文件系统叠加到一起，最终的文件系统会包含有底层的文件和目录，这样的文件系统就是一个镜像&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之前我们讲过了如何查找、列出镜像和拉取（下载）镜像，接下来看看构建镜像的两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;docker commit&lt;/code&gt;命令。（不推荐）&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;docker build&lt;/code&gt;命令和Dockerfile文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;使用commit命令构建镜像&lt;/h5&gt;
&lt;p&gt;为了演示如何构建镜像，我们先使用Ubuntu镜像来定制一个容器，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --name myubuntu -it ubuntu /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在容器中执行下面的命令来安装Apache服务器并退出容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt -y upgrade
apt -y install apache2
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将这个容器作为一个定制的Web服务器保存起来，当需要这样一台Web服务器的时候，就没有必要重新创建容器并安装Apache了。&lt;/p&gt;
&lt;p&gt;首先我们通过下面的命令查看容器的ID。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker container ls -a
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker container ls -a
CONTAINER ID    IMAGE    COMMAND        CREATED        STATUS        PORTS    NAMES
014bdb321612    ubuntu   &quot;/bin/bash&quot;    5 minutes ago  Exited (0)             myubuntu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交定制的容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker commit 014bdb321612 jackfrued/mywebserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看镜像文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;REPOSITORY              TAG       IMAGE ID        CREATED             SIZE
jackfrued/mywebserver   latest    795b294d265a    14 seconds ago      189 MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成镜像文件以后，后面就可以利用刚才创建的镜像文件来创建新的容器。&lt;/p&gt;
&lt;h5&gt;使用Dockerfile构建镜像&lt;/h5&gt;
&lt;p&gt;Dockerfile使用DSL（Domain Specific Language）来构建一个Docker镜像，只要编辑好了Dockerfile文件，就可以使用&lt;code&gt;docker build&lt;/code&gt;命令来构建一个新的镜像。&lt;/p&gt;
&lt;p&gt;我们先创建一个名为myapp的文件夹来保存项目代码和Dockerfile的文件，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ECS-root temp]# tree myapp
myapp
├── api
│   ├── app.py
│   ├── requirements.txt
│   └── start.sh
└── Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中api是Flask项目的文件夹，其中包括了项目代码、依赖项以及启动脚本等文件，具体内容如下所示：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app.py&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask
from flask_restful import Resource, Api
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r&apos;/api/*&apos;: {&apos;origins&apos;: &apos;*&apos;}})
api = Api(app)


class Product(Resource):

    def get(self):
        products = [&apos;Ice Cream&apos;, &apos;Chocolate&apos;, &apos;Coca Cola&apos;, &apos;Hamburger&apos;]
        return {&apos;products&apos;: products}


api.add_resource(Product, &apos;/api/products&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;requirements.txt&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flask
flask-restful
flask-cors
gunicorn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;start.sh&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
exec gunicorn -w 4 -b 0.0.0.0:8000 app:app
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：需要给start.sh文件以执行权限，可以使用&lt;code&gt;chmod 755 start.sh&lt;/code&gt;命令来做到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dockerfile文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 指定基础镜像
FROM python:3.7
# 指定镜像的维护者
MAINTAINER jackfrued &quot;jackfrued@126.com&quot;
# 将指定文件添加到容器中指定的位置
ADD api/* /root/api/
# 设置工作目录
WORKDIR /root/api
# 执行命令(安装Flask项目的依赖项)
RUN pip install -r requirements.txt -i https://pypi.doubanio.com/simple/
# 容器启动时要执行的命令
ENTRYPOINT [&quot;./start.sh&quot;]
# 暴露端口
EXPOSE 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来解释一下上面的Dockerfile文件。Dockerfile文件通过特殊的指令来指定基础镜像（FROM指令）、创建容器后需要指定的命令（RUN指令）以及需要暴露的端口（EXPOSE）等信息。我们稍后会专门为大家介绍这些Dockfile中的指令。&lt;/p&gt;
&lt;p&gt;接下来我们可以使用&lt;code&gt;docker build&lt;/code&gt;命令来创建镜像，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t &quot;jackfrued/myapp&quot; .
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：上面的命令最后面的&lt;code&gt;.&lt;/code&gt; 千万不要漏掉了哦，它表示从当前路径下寻找Dockerfile。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过下面的命令可以查看创建好的镜像。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
jackfrued/myapp              latest              6d6f026a7896        5 seconds ago       930 MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想知道镜像文件是如何创建出来的，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker history jackfrued/myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
6d6f026a7896        31 seconds ago      /bin/sh -c #(nop)  EXPOSE 8000/tcp              0 B                 
3f7739173a79        31 seconds ago      /bin/sh -c #(nop)  ENTRYPOINT [&quot;./start.sh&quot;]    0 B                 
321e6bf09bf1        32 seconds ago      /bin/sh -c pip install -r requirements.txt...   13 MB               
2f9bf2c89ac7        37 seconds ago      /bin/sh -c #(nop) WORKDIR /root/api             0 B                 
86119afbe1f8        37 seconds ago      /bin/sh -c #(nop) ADD multi:4b76f9c9dfaee8...   870 B               
08d465e90d4d        3 hours ago         /bin/sh -c #(nop)  MAINTAINER jackfrued &quot;j...   0 B                 
fbf9f709ca9f        12 days ago         /bin/sh -c #(nop)  CMD [&quot;python3&quot;]              0 B 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该镜像来创建容器运行Web服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 8000:8000 --name myapp jackfrued/myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望将上面创建的镜像文件放到dockerhub仓库中，可以按照如下所示的步骤进行操作。&lt;/p&gt;
&lt;p&gt;通过下面的命令登录到dockerhub。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入用户名和口令进行登录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Login with your Docker ID to push and pull images from Docker Hub. If you don&apos;t have a Docker ID, head over to https://hub.docker.com to create one.
Username: jackfrued
Password: 
Login Succeeded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下面的命令将镜像推到仓库中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker push jackfrued/webserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/b9eeadf154.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;Dockerfile指令&lt;/h5&gt;
&lt;p&gt;想了解Dockerfile的指令可以查看官方提供的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/docs.docker.com/engine/reference/builder/%3E&quot;&gt;参考手册&lt;/a&gt;，下面我们为大家介绍一些常用的指令。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FROM&lt;/strong&gt;：设置基础镜像，必须是Dockerfile中的第一条指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM &amp;lt;镜像名&amp;gt; [AS &amp;lt;别名&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM &amp;lt;镜像名&amp;gt;[:&amp;lt;标签&amp;gt;] [AS &amp;lt;别名&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RUN&lt;/strong&gt;：指定构建镜像时要执行的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN &amp;lt;命令&amp;gt; [参数1], [参数2], ... 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CMD&lt;/strong&gt;：指定构建镜像后要执行的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CMD &amp;lt;命令&amp;gt; [参数1], [参数2], ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CMD [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：Docker不同于虚拟机，容器本身就是一个进程，容器中的应用应该位于前台运行。CMD命令相当于就是用来指定容器主进程（创建容器后要在前台执行的程序）的，如果主进程结束了，容器也就停止运行了。所以在容器中启动Nginx不能使用&lt;code&gt;service nginx start&lt;/code&gt;或是&lt;code&gt;systemctl start nginx&lt;/code&gt;而是要通过&lt;code&gt;CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]&lt;/code&gt;让它在前台运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ENTRYPOINT&lt;/strong&gt;：和CMD类似，也可以执行命令，但&lt;code&gt;docker run&lt;/code&gt;命令行中指定的任何参数都会被当做参数再次传给ENTRYPOINT指令中的命令，这就使得我们可以构建一个镜像，它既可以运行一个默认的命令，也支持通过&lt;code&gt;docker run&lt;/code&gt;命令行为该命令指定可覆盖的参数选项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENTRYPOINT &amp;lt;命令&amp;gt; [参数1], [参数2], ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENTRYPOINT [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;WORKDIR&lt;/strong&gt;：在通过镜像创建新容器时，在容器内部创建一个工作目录，ENTRYPOINT和CMD指定的程序会在这个目录下执行。在使用&lt;code&gt;docker run&lt;/code&gt;命令时可以通过&lt;code&gt;-w&lt;/code&gt;参数来覆盖由WORKDIR指定的工作目录。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WORKDIR /opt/webapp
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -w /usr/share/webapp ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ENV&lt;/strong&gt;：在创建镜像时设置环境变量。在使用&lt;code&gt;docker run&lt;/code&gt;命令时，可以通过&lt;code&gt;-e&lt;/code&gt;参数来修改环境变量的设置。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENV DEFAULT_PORT=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -e &quot;DEFAULT_PORT=8000&quot; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;USER&lt;/strong&gt;：指定镜像会以什么用户身份去运行。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USER nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VOLUME&lt;/strong&gt;：在创建容器时添加一个数据卷的挂载点。通过数据卷操作可以实现容器间数据的共享和重用，对卷所作的修改可以马上生效而不需要重新启动容器，我们之前创建容器时使用&lt;code&gt;--volume&lt;/code&gt;参数就是为了实现数据卷的映射操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VOLUME [&quot;/路径1&quot;, &quot;/路径2/子路径2.1/&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ADD&lt;/strong&gt;：将构建目录下的文件和文件夹复制到镜像中，如果是压缩文件和归档文件，ADD命令会对这些文件进行解压缩解归档的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADD [--chown=&amp;lt;用户&amp;gt;:&amp;lt;用户组&amp;gt;] &amp;lt;源文件&amp;gt; &amp;lt;目标文件&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;COPY&lt;/strong&gt;：非常类似于ADD，但不会主动对文件进行提取操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LABEL&lt;/strong&gt;：为Docker镜像添加一些元数据，在使用&lt;code&gt;docker inspect&lt;/code&gt;命令时会看到这些元数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LABEL version=&quot;1.0.0&quot; location=&quot;Chengdu&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ONBUILD&lt;/strong&gt;：为镜像添加触发器，当一个镜像被用作其他镜像的基础镜像，触发器将会被执行。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ONBUILD ADD . /app/src
ONBUILD RUN cd /app/src &amp;amp;&amp;amp; make
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;多容器管理&lt;/h4&gt;
&lt;p&gt;我们的项目可能会使用了多个容器，容器多了之后管理容器的工作就会变得麻烦。如果要对多个容器进行自动配置使得容器可以相互协作甚至实现复杂的调度，这就需要进行容器编排。Docker原生对容器编排的支持非常弱，但是可以通过社区提供的工具来实现容器编排。&lt;/p&gt;
&lt;h5&gt;Docker Compose&lt;/h5&gt;
&lt;p&gt;可以通过安装Docker Compose工具来实现基于YAML文件的容器编排，YAML文件会定义一系列的容器以及容器运行时的属性，Docker Compose会根据这些配置来管理容器。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Docker Compose。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L &quot;https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果没有curl工具，在CentOS下可以先通过包管理工具yum安装curl再执行上面的命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然我们也可以使用Python的包管理工具pip来安装Docker Compose，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install -U docker-compose
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Docker Compose。&lt;/p&gt;
&lt;p&gt;我们在刚才的Flask项目中引入缓存，然后再利用Flask提供的数据接口为前端页面提供数据，使用Vue.js进行页面渲染并将静态页面部署在Nginx服务器上。项目文件夹结构如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ECS-root ~]# tree temp
temp
├── docker-compose.yml
├── html
│   └── index.html
└── myapp
    ├── api
    │   ├── app.py
    │   ├── requirements.txt
    │   └── start.sh
    └── Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后的app.py文件代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pickle import dumps, loads

from flask import Flask
from flask_restful import Resource, Api
from flask_cors import CORS
from redis import Redis

app = Flask(__name__)
CORS(app, resources={r&apos;/api/*&apos;: {&apos;origins&apos;: &apos;*&apos;}})
api = Api(app)
redis = Redis(host=&apos;redis-master&apos;, port=6379)


class Product(Resource):

    def get(self):
        data = redis.get(&apos;products&apos;)
        if data:
            products = loads(data)
        else:
            products = [&apos;Ice Cream&apos;, &apos;Chocolate&apos;, &apos;Coca Cola&apos;, &apos;Hamburger&apos;]
            redis.set(&apos;products&apos;, dumps(products))
        return {&apos;products&apos;: products}


api.add_resource(Product, &apos;/api/products&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;html文件夹用来保存静态页面，稍后我们会通一个运行Nginx的容器来向浏览器提供静态页面。index.html文件的内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;首页&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;app&quot;&amp;gt;
        &amp;lt;h2&amp;gt;产品列表&amp;lt;/h2&amp;gt;
        &amp;lt;ul&amp;gt;
            &amp;lt;li v-for=&quot;product in products&quot;&amp;gt;{{ product }}&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcss.com/vue/2.6.10/vue.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        new Vue({
            el: &apos;#app&apos;, 
            data: {
                products: []
            },
            created() {
                fetch(&apos;http://1.2.3.4:8000/api/products&apos;)
                    .then(resp =&amp;gt; resp.json())
                    .then(json =&amp;gt; {this.products = json.products})
            }
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们要通过docker-compose.yml文件来创建三个容器并指明容器之间的依赖关系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3&apos;
services:
  api-server:
    build: ./myapp
    ports:
      - &apos;8000:8000&apos;
    links:
      - redis-master
  web-server:
    image: nginx
    ports:
      - &apos;80:80&apos;
    volumes:
      - ./html:/usr/share/nginx/html
  redis-master:
    image: redis
    expose:
      - &apos;6379&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个YAML文件，我们就可以使用&lt;code&gt;docker-compose&lt;/code&gt;命令来创建容器运行项目，其命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ECS-root temp]# docker-compose up
Creating network &quot;temp_default&quot; with the default driver
Creating temp_web-server_1   ... done
Creating temp_redis-master_1 ... done
Creating temp_api-server_1   ... done
Attaching to temp_redis-master_1, temp_web-server_1, temp_api-server_1
redis-master_1  | 1:C 05 Dec 2019 11:57:26.828 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis-master_1  | 1:C 05 Dec 2019 11:57:26.828 # Redis version=5.0.6, bits=64, commit=00000000, modified=0, pid=1, just started
redis-master_1  | 1:C 05 Dec 2019 11:57:26.828 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis-master_1  | 1:M 05 Dec 2019 11:57:26.830 * Running mode=standalone, port=6379.
redis-master_1  | 1:M 05 Dec 2019 11:57:26.831 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis-master_1  | 1:M 05 Dec 2019 11:57:26.831 # Server initialized
redis-master_1  | 1:M 05 Dec 2019 11:57:26.831 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add &apos;vm.overcommit_memory = 1&apos; to /etc/sysctl.conf and then reboot or run the command &apos;sysctl vm.overcommit_memory=1&apos; for this to take effect.
redis-master_1  | 1:M 05 Dec 2019 11:57:26.831 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command &apos;echo never &amp;gt; /sys/kernel/mm/transparent_hugepage/enabled&apos; as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis-master_1  | 1:M 05 Dec 2019 11:57:26.831 * Ready to accept connections
api-server_1    | [2019-12-05 11:57:27 +0000] [1] [INFO] Starting gunicorn 20.0.4
api-server_1    | [2019-12-05 11:57:27 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
api-server_1    | [2019-12-05 11:57:27 +0000] [1] [INFO] Using worker: sync
api-server_1    | [2019-12-05 11:57:27 +0000] [8] [INFO] Booting worker with pid: 8
api-server_1    | [2019-12-05 11:57:27 +0000] [9] [INFO] Booting worker with pid: 9
api-server_1    | [2019-12-05 11:57:27 +0000] [10] [INFO] Booting worker with pid: 10
api-server_1    | [2019-12-05 11:57:27 +0000] [11] [INFO] Booting worker with pid: 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要停止容器的运行，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Kubernetes（K8S）&lt;/h5&gt;
&lt;p&gt;实际的生产环境中常常需要部署和管理多个协同工作的容器，docker compose解决了多容器创建和管理的问题，但是实际项目中，我们还需要Kubernetes（以下都简称为K8S）来提供一个跨主机集群的容器调度平台。K8S可以进行自动化容器的部署、扩展和操作，从而提供以容器为中心的基础架构。该项目是谷歌在2014年启动的项目，建立在谷歌公司十余年运维经验的基础之上，而且谷歌自己的应用也是运行在容器上的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;93 MySQL性能优化&lt;/h2&gt;
&lt;h4&gt;基本原则&lt;/h4&gt;
&lt;p&gt;想要发挥 MySQL 的最佳性能，需要遵循 3 个基本使用原则。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让MySQL回归存储的基本职能：MySQL 数据库只用于数据的存储，不进行数据的复杂计算，不承载业务逻辑，确保存储和计算分离；&lt;/li&gt;
&lt;li&gt;查询数据时，尽量单表查询，减少跨库查询和多表关联；&lt;/li&gt;
&lt;li&gt;杜绝大事务、大 SQL、大批量、大字段等一系列性能杀手。
&lt;ul&gt;
&lt;li&gt;大事务：运行步骤较多，涉及的表和字段较多，容易造成资源的争抢，甚至形成死锁。一旦事务回滚，会导致资源占用时间过长。&lt;/li&gt;
&lt;li&gt;大 SQL：复杂的SQL意味着过多的表的关联，MySQL 数据库处理关联超过3张表以上的SQL时，占用资源多，性能低下。&lt;/li&gt;
&lt;li&gt;大批量：多条SQL一次性执行完成，可以减少一条条执行SQL产生的额外开销，但必须确保进行充分的测试，并且在业务低峰时段或者非业务时段执行。&lt;/li&gt;
&lt;li&gt;大字段：blob、text类型的大字段要尽量少用，必须要用时，尽量与主业务表分离，减少对这类字段的检索和更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;建库建表&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;必须指定默认存储引擎为 InnoDB，并且禁用 MyISAM 存储引擎，随着 MySQL 8.0 版本的发布，所有的数据字典表都已经转换成了 InnoDB，MyISAM 存储引擎已成为了历史。&lt;/li&gt;
&lt;li&gt;默认字符集 UTF8mb4，以前版本的 UTF8 是 UTF8mb3，未包含个别特殊字符，新版本的 UTF8mb4 包含所有字符，官方强烈建议使用此字符集。&lt;/li&gt;
&lt;li&gt;关闭区分大小写功能。设置参数&lt;code&gt;lower_case_table_names&lt;/code&gt;的值为&lt;code&gt;1&lt;/code&gt;，即可关闭区分大小写功能，即大写字母 T 和小写字母 t 一样。&lt;/li&gt;
&lt;li&gt;存储过程、触发器、视图、event等功能尽量在程序中实现，一方面是为了存储和计算分离，另一方面是因为这些功能非常不完整，调试、排错、监控都非常困难，相关数据字典也不完善，存在潜在的风险。一般在生产数据库中，禁止使用。&lt;/li&gt;
&lt;li&gt;单个数据库实例表数量控制在2000个以内。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;InnoDB表的注意事项&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;主键列使用&lt;code&gt;unsigned&lt;/code&gt;整数，可以使用&lt;code&gt;auto_increment&lt;/code&gt;，但是要禁止手动更新主键。&lt;/li&gt;
&lt;li&gt;每个列都必须添加&lt;code&gt;comment&lt;/code&gt;注释。&lt;/li&gt;
&lt;li&gt;在建表时必须显示指定&lt;code&gt;engine&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;表必备三字段：&lt;code&gt;xxx_id&lt;/code&gt;、 &lt;code&gt;xxx_create&lt;/code&gt;、 &lt;code&gt;xxx_modified&lt;/code&gt;。其中&lt;code&gt;xxx_id&lt;/code&gt;为主键，类型&lt;code&gt;unsigned&lt;/code&gt;整数类型（例如：&lt;code&gt;int unsigned&lt;/code&gt;）；&lt;code&gt;xxx_create&lt;/code&gt;、&lt;code&gt;xxx_modified&lt;/code&gt;的类型均为&lt;code&gt;datetime&lt;/code&gt;类型，分别记录该条数据的创建时间、修改时间。&lt;/li&gt;
&lt;li&gt;所有字段必须指定&lt;code&gt;not null&lt;/code&gt;，为空值指定&lt;code&gt;default&lt;/code&gt;值，因为MySQL难以优化&lt;code&gt;null&lt;/code&gt;值，含&lt;code&gt;null&lt;/code&gt;值的复合索引会失效，最终导致查询效率低。&lt;/li&gt;
&lt;li&gt;单张表的字段数尽量空值在50个字段以内，如果字段过多可以考虑垂直拆分。&lt;/li&gt;
&lt;li&gt;禁用&lt;code&gt;enum&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;类型，因为这样的类型兼容性不好且性能较差。&lt;/li&gt;
&lt;li&gt;大文件不应该使用&lt;code&gt;blob&lt;/code&gt;类型而是保存它们的路径，&lt;code&gt;blob&lt;/code&gt;和&lt;code&gt;text&lt;/code&gt;这样的类型会导致处理性能下降，全表扫描代价大大增加。&lt;/li&gt;
&lt;li&gt;对货币等对精度敏感的数据，应该使用定点数（&lt;code&gt;decimal&lt;/code&gt;）而不是浮点数（&lt;code&gt;float&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;保存IP地址不要用&lt;code&gt;char(15)&lt;/code&gt;，应该使用&lt;code&gt;int unsigned&lt;/code&gt;，可以使用&lt;code&gt;inet_aton&lt;/code&gt;和&lt;code&gt;inet_ntoa&lt;/code&gt;函数实现整数和IP地址的转换。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用索引&lt;/h4&gt;
&lt;p&gt;在前面&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day36-40/36-38.%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93MySQL.md&quot;&gt;《关系型数据库MySQL》&lt;/a&gt;一文中，我们已经讲到过索引的相关知识，这里我们做一个简单的回顾。&lt;/p&gt;
&lt;h5&gt;索引的设计原则&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;创建索引的列并不一定是&lt;code&gt;select&lt;/code&gt;操作中要查询的列，最适合做索引的列是出现在&lt;code&gt;where&lt;/code&gt;子句中经常用作筛选条件或连表子句中作为表连接条件的列。&lt;/li&gt;
&lt;li&gt;具有唯一性的列，索引效果好；重复值较多的列，索引效果差。&lt;/li&gt;
&lt;li&gt;如果为字符串类型创建索引，最好指定一个前缀长度，创建短索引。短索引可以减少磁盘I/O而且在做比较时性能也更好，更重要的是MySQL底层的高速索引缓存能够缓存更多的键值。&lt;/li&gt;
&lt;li&gt;创建一个包含N列的复合索引（多列索引）时，相当于是创建了N个索引，此时应该利用最左前缀进行匹配。&lt;/li&gt;
&lt;li&gt;不要过度使用索引。索引并不是越多越好，索引需要占用额外的存储空间而且会影响写操作的性能（插入、删除、更新数据时索引也需要更新）。MySQL在生成执行计划时，要考虑各个索引的使用，这个也是需要耗费时间的。&lt;/li&gt;
&lt;li&gt;要注意可能使索引失效的场景，例如：模糊查询使用了前置通配符、使用负向条件进行查询等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用过程&lt;/h4&gt;
&lt;p&gt;过程，通常也称之为存储过程，它是事先编译好存储在数据库中的一组SQL的集合。调用存储过程可以简化应用程序开发人员的工作，减少与数据库服务器之间的通信，对于提升数据操作的性能是有帮助的，这些我们在之前的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day36-40/36-38.%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93MySQL.md&quot;&gt;《关系型数据库MySQL》&lt;/a&gt;一文中已经提到过。&lt;/p&gt;
&lt;h4&gt;数据分区&lt;/h4&gt;
&lt;p&gt;MySQL支持做数据分区，通过分区可以存储更多的数据、优化查询，获得更大的吞吐量并快速删除过期的数据。关于这个知识点建议大家看看MySQL的&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/partitioning-overview.html&quot;&gt;官方文档&lt;/a&gt;。数据分区有以下几种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;RANGE分区：基于连续区间范围，把数据分配到不同的分区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE tb_emp (
    eno INT NOT NULL,
    ename VARCHAR(20) NOT NULL,
    job VARCHAR(10) NOT NULL,
    hiredate DATE NOT NULL,
    dno INT NOT NULL
)
PARTITION BY RANGE( YEAR(hiredate) ) (
    PARTITION p0 VALUES LESS THAN (1960),
    PARTITION p1 VALUES LESS THAN (1970),
    PARTITION p2 VALUES LESS THAN (1980),
    PARTITION p3 VALUES LESS THAN (1990),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LIST分区：基于枚举值的范围，把数据分配到不同的分区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HASH分区 / KEY分区：基于分区个数，把数据分配到不同的分区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE tb_emp (
    eno INT NOT NULL,
    ename VARCHAR(20) NOT NULL,
    job VARCHAR(10) NOT NULL,
    hiredate DATE NOT NULL,
    dno INT NOT NULL
)
PARTITION BY HASH(dno)
PARTITIONS 4;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;SQL优化&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定位低效率的SQL语句 - 慢查询日志。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看慢查询日志相关配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show variables like &apos;slow_query%&apos;;
+---------------------------+----------------------------------+
| Variable_name             | Value                            |
+---------------------------+----------------------------------+
| slow_query_log            | OFF                              |
| slow_query_log_file       | /mysql/data/localhost-slow.log   |
+---------------------------+----------------------------------+

mysql&amp;gt; show variables like &apos;long_query_time&apos;;
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建慢查询日志文件并修改所有者。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /var/log/mysqld-slow.log
chown mysql /var/log/mysqld-slow.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改全局慢查询日志配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; set global slow_query_log_file=&apos;/var/log/mysqld-slow.log&apos;
mysql&amp;gt; set global slow_query_log=&apos;ON&apos;; 
mysql&amp;gt; set global long_query_time=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或者直接修改MySQL配置文件启用慢查询日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
slow_query_log=ON
slow_query_log_file=/var/log/mysqld-slow.log
long_query_time=1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：修改了配置文件需要重启MySQL，CentOS上对应的命令是&lt;code&gt;systemctl restart mysqld&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;explain&lt;/code&gt;了解SQL的执行计划。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explain select ename, job, sal from tb_emp where dno=20\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tb_emp
         type: ref
possible_keys: fk_emp_dno
          key: fk_emp_dno
      key_len: 5
          ref: const
         rows: 7
        Extra: NULL
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select_type&lt;/code&gt;：查询类型（SIMPLE - 简单查询、PRIMARY - 主查询、UNION - 并集、SUBQUERY - 子查询）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table&lt;/code&gt;：输出结果集的表。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：访问类型（ALL - 全表查询性能最差、index、range、ref、eq_ref、const、NULL）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;possible_keys&lt;/code&gt;：查询时可能用到的索引。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt;：实际使用的索引。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key_len&lt;/code&gt;：索引字段的长度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rows&lt;/code&gt;：扫描的行数，行数越少肯定性能越好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extra&lt;/code&gt;：额外信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;show profiles&lt;/code&gt;和&lt;code&gt;show profile for query&lt;/code&gt;分析SQL。&lt;/p&gt;
&lt;p&gt;MySQL从5.0.37开始支持剖面系统来帮助用户了解SQL执行性能的细节，可以通过下面的方式来查看MySQL是否支持和开启了剖面系统。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select @@have_profiling;
select @@profiling;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有开启剖面系统，可以通过下面的SQL来打开它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set profiling=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就可以通过剖面系统来了解SQL的执行性能，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; select count(*) from tb_emp;
+----------+
| count(*) |
+----------+
|       14 |
+----------+
1 row in set (0.00 sec)

mysql&amp;gt; show profiles;
+----------+------------+-----------------------------+
| Query_ID | Duration   | Query                       |
+----------+------------+-----------------------------+
|        1 | 0.00029600 | select count(*) from tb_emp |
+----------+------------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

mysql&amp;gt; show profile for query 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000076 |
| checking permissions | 0.000007 |
| Opening tables       | 0.000016 |
| init                 | 0.000013 |
| System lock          | 0.000007 |
| optimizing           | 0.000005 |
| statistics           | 0.000012 |
| preparing            | 0.000010 |
| executing            | 0.000003 |
| Sending data         | 0.000070 |
| end                  | 0.000012 |
| query end            | 0.000008 |
| closing tables       | 0.000012 |
| freeing items        | 0.000032 |
| cleaning up          | 0.000013 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化CRUD操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优化&lt;code&gt;insert&lt;/code&gt;语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;insert&lt;/code&gt;语句后面跟上多组值进行插入在性能上优于分开&lt;code&gt;insert&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果有多个连接向同一个表插入数据，使用&lt;code&gt;insert delayed&lt;/code&gt;可以获得更好的性能。&lt;/li&gt;
&lt;li&gt;如果要从一个文本文件装载数据到表时，使用&lt;code&gt;load data infile&lt;/code&gt;比&lt;code&gt;insert&lt;/code&gt;性能好得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化&lt;code&gt;order by&lt;/code&gt;语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;where&lt;/code&gt;子句的条件和&lt;code&gt;order by&lt;/code&gt;子句的条件相同，而且排序的顺序与索引的顺序相同，如果还同时满足排序字段都是升序或者降序，那么只靠索引就能完成排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化&lt;code&gt;group by&lt;/code&gt;语句&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在使用&lt;code&gt;group by&lt;/code&gt;子句分组时，如果希望避免排序带来的开销，可以用&lt;code&gt;order by null&lt;/code&gt;禁用排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化嵌套查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL从4.1开始支持嵌套查询（子查询），这使得可以将一个查询的结果当做另一个查询的一部分来使用。在某些情况下，子查询可以被更有效率的连接查询取代，因为在连接查询时MySQL不需要在内存中创建临时表来完成这个逻辑上需要多个步骤才能完成的查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化or条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果条件之间是&lt;code&gt;or&lt;/code&gt;关系，则只有在所有条件都用到索引的情况下索引才会生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化分页查询&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分页查询时，一个比较头疼的事情是如同&lt;code&gt;limit 1000, 20&lt;/code&gt;，此时MySQL已经排序出前1020条记录但是仅仅返回第1001到1020条记录，前1000条实际都用不上，查询和排序的代价非常高。一种常见的优化思路是在索引上完成排序和分页的操作，然后根据返回的结果做表连接操作来得到最终的结果，这样可以避免出现全表查询，也避免了外部排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from tb_emp order by ename limit 10000, 20;
select * from tb_emp t1 inner join (select eno from tb_emp order by ename limit 10000, 20) t2 on t1.eno=t2.eno;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，第2行SQL是优于第1行SQL的，当然我们的前提是已经在&lt;code&gt;ename&lt;/code&gt;字段上创建了索引。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用SQL提示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;USE INDEX：建议MySQL使用指定的索引。&lt;/li&gt;
&lt;li&gt;IGNORE INDEX：建议MySQL忽略掉指定的索引。&lt;/li&gt;
&lt;li&gt;FORCE INDEX：强制MySQL使用指定的索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;配置优化&lt;/h4&gt;
&lt;p&gt;可以使用下面的命令来查看MySQL服务器配置参数的默认值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show variables;
show variables like &apos;key_%&apos;;
show variables like &apos;%cache%&apos;;
show variables like &apos;innodb_buffer_pool_size&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下面的命令可以了解MySQL服务器运行状态值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show status;
show status like &apos;com_%&apos;;
show status like &apos;innodb_%&apos;;
show status like &apos;connections&apos;;
show status like &apos;slow_queries&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;调整&lt;code&gt;max_connections&lt;/code&gt;：MySQL最大连接数量，默认151。在Linux系统上，如果内存足够且不考虑用户等待响应时间这些问题，MySQL理论上可以支持到万级连接，但是通常情况下，这个值建议控制在1000以内。&lt;/li&gt;
&lt;li&gt;调整&lt;code&gt;back_log&lt;/code&gt;：TCP连接的积压请求队列大小，通常是max_connections的五分之一，最大不能超过900。&lt;/li&gt;
&lt;li&gt;调整&lt;code&gt;table_open_cache&lt;/code&gt;：这个值应该设置为max_connections的N倍，其中N代表每个连接在查询时打开的表的最大个数。&lt;/li&gt;
&lt;li&gt;调整&lt;code&gt;innodb_lock_wait_timeout&lt;/code&gt;：该参数可以控制InnoDB事务等待行锁的时间，默认值是50ms，对于反馈响应要求较高的应用，可以将这个值调小避免事务长时间挂起；对于后台任务，可以将这个值调大来避免发生大的回滚操作。&lt;/li&gt;
&lt;li&gt;调整&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;：InnoDB数据和索引的内存缓冲区大小，以字节为单位，这个值设置得越高，访问表数据需要进行的磁盘I/O操作就越少，如果可能甚至可以将该值设置为物理内存大小的80%。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;架构优化&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过拆分提高表的访问效率。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;垂直拆分&lt;/li&gt;
&lt;li&gt;水平拆分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逆范式理论。数据表设计的规范程度称之为范式（Normal Form），要提升表的规范程度通常需要将大表拆分为更小的表，范式级别越高数据冗余越小，而且在插入、删除、更新数据时出问题的可能性会大幅度降低，但是节省了空间就意味着查询数据时可能花费更多的时间，原来的单表查询可能会变成连表查询。为此，项目实践中我们通常会进行逆范式操作，故意降低范式级别增加冗余来减少查询的时间开销。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1NF：列不能再拆分&lt;/li&gt;
&lt;li&gt;2NF：所有的属性都依赖于主键&lt;/li&gt;
&lt;li&gt;3NF：所有的属性都直接依赖于主键（消除传递依赖）&lt;/li&gt;
&lt;li&gt;BCNF：消除非平凡多值依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用中间表提高统计查询速度。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;insert into 中间表 select ... where ...&lt;/code&gt;这样的语句先将需要的数据筛选出来放到中间表中，然后再对中间表进行统计，避免不必要的运算和处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主从复制和读写分离，具体内容请参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/98.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF%E5%92%8C%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98.md&quot;&gt;《项目部署上线和性能调优》&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置MySQL集群。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：本章内容参考了网易出品的《深入浅出MySQL》一书，该书和《高性能MySQL》一样，都对MySQL进行了深入细致的讲解，虽然总体感觉后者更加高屋建瓴，但是前者也算得上是提升MySQL技能的佳作（作者的文字功底稍显粗糙，深度也不及后者），建议有兴趣的读者可以阅读这两本书。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;94 网络API接口设计&lt;/h2&gt;
&lt;p&gt;目前许多的Web应用和移动应用都使用了前后端分离的开发模式，前后端分离简单的说就是前端或移动端通过网络API接口和后台进行交互，获得接口中提供的数据并负责用户界面的渲染。API是应用程序的编程接口的缩写，网络API通常指的是基于一个URL（统一资源定位符）可以访问到的资源，也就是说通过这个URL我们就可以请求服务器对某个资源进行操作并返回操作的结果。大家可以想想，网络API接口不也是一种封装吗，简单的说就是将复杂的业务逻辑隐藏在简单的API接口中。&lt;/p&gt;
&lt;p&gt;URL的通用格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;协议://用户名:口令@主机:端口/路径1/.../路径N/资源名
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：URL中的用户名（有可能不需要提供用户名）、口令（有可能不需要提供口令）、端口（有可能使用默认端口）、路径（资源有可能直接位于根路径&lt;code&gt;/&lt;/code&gt;下）并不是必需的部分，可以根据需要进行设置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;网络API通常基于HTTP或HTTPS进行访问，基于HTTP/HTTPS最大的好处就在于访问起来非常的简单方便，而且可以跨语言、跨应用进行访问和互操作。&lt;/p&gt;
&lt;h4&gt;设计原则&lt;/h4&gt;
&lt;h5&gt;关键问题&lt;/h5&gt;
&lt;p&gt;为移动端或者PC端设计网络API接口一个非常重要的原则是：&lt;strong&gt;根据业务实体而不是用户界面或操作来设计API接口&lt;/strong&gt;。如果API接口的设计是根据用户的操作或者界面上的功能设置来设计，随着需求的变更，用户界面也会进行调整，需要的数据也在发生变化，那么后端开发者就要不停的调整API，或者给一个API设计出多个版本，这些都会使项目的开发和维护成本增加。我们可以将业务实体理解为服务器提供的资源，而URL就是资源的定位符（标识符），这种方式是最为简单自然的。对于相对复杂的用户操作，我们可以提供一个“门面”（设计模式中的“门面模式”），通过该“门面”把多个接口的功能组装起来即可。&lt;/p&gt;
&lt;p&gt;下面是某个网站开放API的接口，可以看出API的设计是围绕业务实体来进行的，而且都做到了“见名知意”。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;评论&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;comments/show&lt;/td&gt;
&lt;td&gt;获取某条微博的评论列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/by_me&lt;/td&gt;
&lt;td&gt;自己的评论列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/to_me&lt;/td&gt;
&lt;td&gt;收到的评论列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/mentions&lt;/td&gt;
&lt;td&gt;@了自己的评论列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/create&lt;/td&gt;
&lt;td&gt;创建一条评论&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/destroy&lt;/td&gt;
&lt;td&gt;删除一条评论&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comments/reply&lt;/td&gt;
&lt;td&gt;回复一条评论&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;需要说明的是，&lt;strong&gt;上面的API接口并不是REST风格的&lt;/strong&gt;。REST是一种网络应用架构风格，被认为最适合分布式的网络应用。关于REST的知识，可以阅读阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2011/09/restful.html&quot;&gt;《理解RESTful架构》&lt;/a&gt;以及&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/05/restful_api.html&quot;&gt;《RESTful API设计指南》&lt;/a&gt;，当然这两篇文章大家也要批判的阅读，因为上面阐述的观点并不完全正确，有些内容甚至是自相矛盾的。&lt;/p&gt;
&lt;p&gt;API接口返回的数据通常都是&lt;strong&gt;JSON&lt;/strong&gt;或&lt;strong&gt;XML&lt;/strong&gt;格式，XML这种数据格式目前基本已经被弃用了。对于JSON格式的数据，我们需要做到不要返回null这的值，因为这样的值一旦处置失当，会给前端和移动端开发带来不必要的麻烦（因为开发者有可能会使用强类型语言）。要解决这个问题可以从源头入手，在设计数据库的时候，尽量给每个字段都加上“not null”约束或者设置合理的默认值约束。&lt;/p&gt;
&lt;h5&gt;其他问题&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;更新提示问题：设计一个每次使用系统首先要访问的API，该API会向移动端返回系统更新的相关信息，这样就可以提升用户更新App了。&lt;/li&gt;
&lt;li&gt;版本升级问题：API版本升级时应该考虑对低版本的兼容，同时要让新版本和旧版本都能够被访问，可以在URL中包含版本信息或者在将版本号放在HTTP(S)协议头部，关于这个问题有很多的争论，有兴趣的可以看看&lt;a href=&quot;https://stackoverflow.com/questions/972226/how-to-version-rest-uris&quot;&gt;stack overflow&lt;/a&gt;上面对这个问题的讨论。&lt;/li&gt;
&lt;li&gt;图片尺寸问题：移动端对于一张图片可能需要不同的尺寸，可以在获取图片时传入尺寸参数并获取对应的资源；更好的做法是直接使用云存储或CDN（直接提供了图片缩放的功能），这样可以加速对资源的访问。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;文档撰写&lt;/h4&gt;
&lt;p&gt;下面以设计评论接口为例，简单说明接口文档应该如何撰写。&lt;/p&gt;
&lt;p&gt;首先，我们可以定义全局返回状态码。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;返回码&lt;/th&gt;
&lt;th&gt;返回信息&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;获取评论成功&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10001&lt;/td&gt;
&lt;td&gt;创建评论成功&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10002&lt;/td&gt;
&lt;td&gt;无法创建评论&lt;/td&gt;
&lt;td&gt;创建评论时因违反审核机制而无法创建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10003&lt;/td&gt;
&lt;td&gt;评论已被删除&lt;/td&gt;
&lt;td&gt;查看评论时评论因不和谐因素已被删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10004&lt;/td&gt;
&lt;td&gt;……&lt;/td&gt;
&lt;td&gt;……&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取文章评论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/articles/{article-id}/comments/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：王大锤&lt;/p&gt;
&lt;p&gt;最后更新时间：2018年8月10日&lt;/p&gt;
&lt;p&gt;标签：v 1.0&lt;/p&gt;
&lt;p&gt;接口说明：获取指定文章的所有评论&lt;/p&gt;
&lt;p&gt;使用帮助：默认返回20条数据，需要在请求头中设置身份标识（key）&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;页码，默认值1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;查询参数&lt;/td&gt;
&lt;td&gt;每次获取评论数量（10~100），默认值20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户的身份标识&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10000,
    &quot;message&quot;: &quot;获取评论成功&quot;,
    &quot;page&quot;: 1,
    &quot;size&quot;: 10,
    &quot;totalPage&quot;: 35,
    &quot;contents&quot;: [
        {
            &quot;userId&quot;: 1700095,
            &quot;nickname&quot;: &quot;王大锤&quot;,
            &quot;pubDate&quot;: &quot;2018年7月31日&quot;,
            &quot;content&quot;: &quot;小编是不是有病呀&quot;,
            /* ... */
        },
        {
        	&quot;userId&quot;, 1995322,
            &quot;nickname&quot;: &quot;白元芳&quot;,
            &quot;pubDate&quot;: &quot;2018年8月2日&quot;,
            &quot;content&quot;: &quot;楼上说得好&quot;,
            /* ... */
        }
    ]
    /* ... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增文章评论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;POST&lt;/strong&gt; &lt;code&gt;/articles/{article-id}/comments&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开发者：王大锤&lt;/p&gt;
&lt;p&gt;最后更新时间：2018年8月10日&lt;/p&gt;
&lt;p&gt;标签：v 1.0&lt;/p&gt;
&lt;p&gt;接口说明：为指定的文章创建评论&lt;/p&gt;
&lt;p&gt;使用帮助：暂无&lt;/p&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必填&lt;/th&gt;
&lt;th&gt;参数位置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;userId&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;用户ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;请求头&lt;/td&gt;
&lt;td&gt;用户的令牌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;content&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息体&lt;/td&gt;
&lt;td&gt;评论的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;响应信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 10001,
    &quot;message&quot;: &quot;创建评论成功&quot;,
    &quot;comment&quot;: {
        &quot;pubDate&quot;: &quot;2018年7月31日&quot;,
        &quot;content&quot;: &quot;小编是不是有病呀&quot;
        /* ... */
    }
    /* ... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果没有接口文档撰写经验，可以使用在线接口文档编辑平台&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttp:/rap2.taobao.org/%3E&quot;&gt;RAP2&lt;/a&gt;或&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttp:/yapi.demo.qunar.com/%3E&quot;&gt;YAPI&lt;/a&gt;来进行接口文档撰写。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;95 使用Django开发商业项目&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：本文的部分插图来自于《Python项目开发实战》和《精通Django》，这两本书中都包含了对Django框架精彩的讲解，有兴趣的读者可以自行购买阅读。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Web应用&lt;/h4&gt;
&lt;p&gt;问题1：描述一个Web应用的工作流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/09872f0277.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;问题2：描述项目的物理架构。（上图中补充负载均衡（反向代理）服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等，而且每个节点都有可能是多节点构成的集群。当然，架构都是根据业务的需要一步步演进而不是一蹴而就的。）&lt;/p&gt;
&lt;p&gt;问题3：描述Django项目的工作流程。（如下图所示）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/8bf1a346c2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;MVC架构模式&lt;/h4&gt;
&lt;p&gt;问题1：为什么要使用MVC架构模式？（模型和视图解耦合）&lt;/p&gt;
&lt;p&gt;问题2：MVC架构中每个部分的作用？（如下图所示）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/0d5a752e18.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;HTTP请求和响应&lt;/h4&gt;
&lt;h5&gt;HTTP请求 = 请求行+请求头+空行+[消息体]&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/2baba63eec.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;HTTP响应 = 响应行+响应头+空行+消息体&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/ffd6f1593e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HTTPRequest&lt;/code&gt;对象的属性和方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;method&lt;/code&gt; - 获取请求方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path&lt;/code&gt; / &lt;code&gt;get_full_path()&lt;/code&gt; - 获取请求路径/带查询字符串的路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scheme&lt;/code&gt; / &lt;code&gt;is_secure()&lt;/code&gt; / &lt;code&gt;get_host()&lt;/code&gt; / &lt;code&gt;get_port()&lt;/code&gt; - 获取请求的协议/主机/端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;META&lt;/code&gt; / &lt;code&gt;COOKIES&lt;/code&gt; - 获取请求头/Cookie信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt; / &lt;code&gt;POST&lt;/code&gt; / &lt;code&gt;FILES&lt;/code&gt; - 获取GET或POST请求参数/上传的文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_signed_cookie()&lt;/code&gt; - 获取带签名的Cookie&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_ajax()&lt;/code&gt; - 是不是Ajax异步请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;body&lt;/code&gt; / &lt;code&gt;content_type&lt;/code&gt; / &lt;code&gt;encoding&lt;/code&gt; - 获取请求的消息体（bytes流）/MIME类型/编码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间件添加的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;session&lt;/code&gt; / &lt;code&gt;user&lt;/code&gt; / &lt;code&gt;site&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HttpResponse&lt;/code&gt;对象的属性和方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set_cookie()&lt;/code&gt; / &lt;code&gt;set_signed_cookie()&lt;/code&gt; / &lt;code&gt;delete_cookie()&lt;/code&gt; - 添加/删除Cookie&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__setitem__&lt;/code&gt; / &lt;code&gt;__getitem__&lt;/code&gt; / &lt;code&gt;__delitem__&lt;/code&gt; - 添加/获取/删除响应头&lt;/li&gt;
&lt;li&gt;&lt;code&gt;charset&lt;/code&gt; / &lt;code&gt;content&lt;/code&gt; / &lt;code&gt;status_code&lt;/code&gt; - 响应的字符集/消息体（bytes流）/状态码
&lt;ul&gt;
&lt;li&gt;1xx：请求已经收到，继续处理&lt;/li&gt;
&lt;li&gt;2xx（成功）：请求已经成功收到、理解和接收。&lt;/li&gt;
&lt;li&gt;3xx（重定向）：为完成请求要继续执行后续的操作。&lt;/li&gt;
&lt;li&gt;4xx（客户端错误）：请求不正确或不能够被受理。&lt;/li&gt;
&lt;li&gt;5xx（服务器错误）：服务器处理请求失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;JsonResponse&lt;/code&gt;（&lt;code&gt;HttpResponse&lt;/code&gt;的子类型）对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.http import HttpResponse, JsonResponse
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; response = JsonResponse({&apos;foo&apos;: &apos;bar&apos;})
&amp;gt;&amp;gt;&amp;gt; response.content
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; response = JsonResponse([1, 2, 3], safe=False)
&amp;gt;&amp;gt;&amp;gt; response.content
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; response = HttpResponse(b&apos;...&apos;)
&amp;gt;&amp;gt;&amp;gt; response[&apos;cotent-type&apos;] = &apos;application/pdf&apos;;
&amp;gt;&amp;gt;&amp;gt; response[&apos;content-disposition&apos;] = &apos;inline; filename=&quot;xyz.pdf&quot;&apos;
&amp;gt;&amp;gt;&amp;gt; response[&apos;content-disposition&apos;] = &apos;attachment; filename=&quot;xyz.pdf&quot;&apos;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; response.set_signed_cookie(&apos;foo&apos;, &apos;bar&apos;, salt=&apos;&apos;)
&amp;gt;&amp;gt;&amp;gt; response.status_code = 200
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据模型(Model)&lt;/h4&gt;
&lt;p&gt;问题1：关系型数据库表的设计应该注意哪些问题（范式理论和逆范式）？如何通过表来创建模型类（反向工程）？如何通过模型类来创建表（正向工程）？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py makemigrations &amp;lt;appname&amp;gt;
python manage.py migrate

python manage.py inspectdb &amp;gt; &amp;lt;appname&amp;gt;/models.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题2：关系型数据库中数据完整性指的是什么？什么时候需要牺牲数据完整性？（实体完整性/参照完整性/域完整性）&lt;/p&gt;
&lt;p&gt;问题3：ORM是什么以及解决了什么问题？（对象模型-关系模型双向转换）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Field&lt;/code&gt;及其子类的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通用选项：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;db_column&lt;/code&gt; / &lt;code&gt;db_tablespace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;blank&lt;/code&gt; / &lt;code&gt;default&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;primary_key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db_index&lt;/code&gt; / &lt;code&gt;unqiue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;choices&lt;/code&gt; / &lt;code&gt;help_text&lt;/code&gt; / &lt;code&gt;error_message&lt;/code&gt; / &lt;code&gt;editable&lt;/code&gt; / &lt;code&gt;hidden&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其他选项：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CharField&lt;/code&gt;: &lt;code&gt;max_length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DateField&lt;/code&gt;: &lt;code&gt;auto_now&lt;/code&gt; / &lt;code&gt;auto_now_add&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DecimalField&lt;/code&gt;: &lt;code&gt;max_digits&lt;/code&gt; / &lt;code&gt;decimal_places&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileField&lt;/code&gt;: &lt;code&gt;storage&lt;/code&gt; / &lt;code&gt;upload_to&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ImageField&lt;/code&gt;: &lt;code&gt;height_field&lt;/code&gt; / &lt;code&gt;width_field&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ForeignKey&lt;/code&gt;的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重要属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;db_constraint&lt;/code&gt;（提升性能或者数据分片的情况可能需要设置为&lt;code&gt;False&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;on_delete&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CASCADE&lt;/code&gt;：级联删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PROTECT&lt;/code&gt;：抛出&lt;code&gt;ProtectedError&lt;/code&gt;异常，阻止删除引用的对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET_NULL&lt;/code&gt;：把外键设置为&lt;code&gt;null&lt;/code&gt;，当&lt;code&gt;null&lt;/code&gt;属性被设置为&lt;code&gt;True&lt;/code&gt;时才能这么做。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET_DEFAULT&lt;/code&gt;：把外键设置为默认值，提供了默认值才能这么做。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;related_name&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Dept(models.Model):
    pass


class Emp(models.Model):
    dept = models.ForeignKey(related_name=&apos;+&apos;, ...)
    
 
Dept.objects.get(no=10).emp_set.all()
Emp.objects.filter(dept__no=10)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：&lt;code&gt;related_name&lt;/code&gt;设置为&lt;code&gt;&apos;+&apos;&lt;/code&gt;，可以防止一对多外键关联从“一”的一方查询“多”的一方。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;to_field&lt;/code&gt; / &lt;code&gt;limit_choices_to&lt;/code&gt; / &lt;code&gt;swappable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Model&lt;/code&gt;的属性和方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;objects&lt;/code&gt; / &lt;code&gt;pk&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;save()&lt;/code&gt; / &lt;code&gt;delete()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;clean()&lt;/code&gt; / &lt;code&gt;validate_unique()&lt;/code&gt; / &lt;code&gt;full_clean()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;QuerySet&lt;/code&gt;的方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get()&lt;/code&gt; / &lt;code&gt;all()&lt;/code&gt; / &lt;code&gt;values()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：&lt;code&gt;values()&lt;/code&gt;返回的&lt;code&gt;QuerySet&lt;/code&gt;中不是模型对象而是字典&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;count()&lt;/code&gt; / &lt;code&gt;order_by()&lt;/code&gt; / &lt;code&gt;exists()&lt;/code&gt; / &lt;code&gt;reverse()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;filter()&lt;/code&gt; / &lt;code&gt;exclude()&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;exact&lt;/code&gt; / &lt;code&gt;iexact&lt;/code&gt;：精确匹配/忽略大小写的精确匹配查询&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;contains&lt;/code&gt; / &lt;code&gt;icontains&lt;/code&gt; / &lt;code&gt;startswith / istartswith / endswith / iendswith&lt;/code&gt;：基于&lt;code&gt;like&lt;/code&gt;的模糊查询&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;in&lt;/code&gt;：集合运算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gt&lt;/code&gt; / &lt;code&gt;gte&lt;/code&gt; / &lt;code&gt;lt&lt;/code&gt; / &lt;code&gt;lte&lt;/code&gt;：大于/大于等于/小于/小于等于关系运算&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;range&lt;/code&gt;：指定范围查询（SQL中的&lt;code&gt;between…and…&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;year&lt;/code&gt; / &lt;code&gt;month&lt;/code&gt; / &lt;code&gt;day&lt;/code&gt; / &lt;code&gt;week_day&lt;/code&gt; / &lt;code&gt;hour&lt;/code&gt; / &lt;code&gt;minute&lt;/code&gt; / &lt;code&gt;second&lt;/code&gt;：查询时间日期&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;isnull&lt;/code&gt;：查询空值（&lt;code&gt;True&lt;/code&gt;）或非空值（&lt;code&gt;False&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;search&lt;/code&gt;：基于全文索引的全文检索&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;regex&lt;/code&gt; / &lt;code&gt;iregex&lt;/code&gt;：基于正则表达式的模糊匹配查询&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;aggregate()&lt;/code&gt; / &lt;code&gt;annotate()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Avg&lt;/code&gt; / &lt;code&gt;Count&lt;/code&gt; / &lt;code&gt;Sum&lt;/code&gt; / &lt;code&gt;Max&lt;/code&gt; / &lt;code&gt;Min&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.db.models import Avg
&amp;gt;&amp;gt;&amp;gt; Emp.objects.aggregate(avg_sal=Avg(&apos;sal&apos;))
(0.001) SELECT AVG(`TbEmp`.`sal`) AS `avg_sal` FROM `TbEmp`; args=()
{&apos;avg_sal&apos;: 3521.4286}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Emp.objects.values(&apos;dept&apos;).annotate(total=Count(&apos;dept&apos;))
(0.001) SELECT `TbEmp`.`dno`, COUNT(`TbEmp`.`dno`) AS `total` FROM `TbEmp` GROUP BY `TbEmp`.`dno` ORDER BY NULL LIMIT 21; args=()
&amp;lt;QuerySet [{&apos;dept&apos;: 10, &apos;total&apos;: 4}, {&apos;dept&apos;: 20, &apos;total&apos;: 7}, {&apos;dept&apos;: 30, &apos;total&apos;: 3}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;first()&lt;/code&gt; / &lt;code&gt;last()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：调用&lt;code&gt;first()&lt;/code&gt;方法相当于用&lt;code&gt;[0]&lt;/code&gt;对&lt;code&gt;QuerySet&lt;/code&gt;进行切片。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;only()&lt;/code&gt; / &lt;code&gt;defer()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Emp.objects.filter(pk=7800).only(&apos;name&apos;, &apos;sal&apos;)
(0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`ename`, `TbEmp`.`sal` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,)
&amp;lt;QuerySet [&amp;lt;Emp: Emp object (7800)&amp;gt;]&amp;gt;
&amp;gt;&amp;gt;&amp;gt; Emp.objects.filter(pk=7800).defer(&apos;name&apos;, &apos;sal&apos;)
(0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`job`, `TbEmp`.`mgr`, `TbEmp`.`comm`, `TbEmp`.`dno` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,)
&amp;lt;QuerySet [&amp;lt;Emp: Emp object (7800)&amp;gt;]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;create()&lt;/code&gt; / &lt;code&gt;update()&lt;/code&gt; / &lt;code&gt;raw()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; Emp.objects.filter(dept__no=20).update(sal=F(&apos;sal&apos;) + 100)
(0.011) UPDATE `TbEmp` SET `sal` = (`TbEmp`.`sal` + 100) WHERE `TbEmp`.`dno` = 20; args=(100, 20)
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; Emp.objects.raw(&apos;select empno, ename, job from TbEmp where dno=10&apos;)
&amp;lt;RawQuerySet: select empno, ename, job from TbEmp where dno=10&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Q&lt;/code&gt;对象和&lt;code&gt;F&lt;/code&gt;对象&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：Q对象主要用来解决多条件组合的复杂查询；F对象主要用于更新数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.db.models import Q
&amp;gt;&amp;gt;&amp;gt; Emp.objects.filter(
...     Q(name__startswith=&apos;张&apos;),
...     Q(sal__lte=5000) | Q(comm__gte=1000)
... ) # 查询名字以“张”开头且工资小于等于5000或补贴大于等于1000的员工
&amp;lt;QuerySet [&amp;lt;Emp: 张三丰&amp;gt;]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from backend.models import Emp, Dept
&amp;gt;&amp;gt;&amp;gt; emps = Emp.objects.filter(dept__no=20)
&amp;gt;&amp;gt;&amp;gt; from django.db.models import F
&amp;gt;&amp;gt;&amp;gt; emps.update(sal=F(&apos;sal&apos;) + 100)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原生SQL查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.db import connections


with connections[&apos;...&apos;].cursor() as cursor:
    cursor.execute(&quot;UPDATE TbEmp SET sal=sal+10 WHERE dno=30&quot;)
    cursor.execute(&quot;SELECT ename, job FROM TbEmp WHERE dno=10&quot;)
    row = cursor.fetchall()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模型管理器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BookManager(models.Manager):
    
    def title_count(self, keyword):
        return self.filter(title__icontains=keyword).count()

class Book(models.Model):
    
    objects = BookManager()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;视图函数(Controller)&lt;/h4&gt;
&lt;h5&gt;如何设计视图函数&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用户的每个请求（用户故事）对应一个视图函数，当然也可以将用户要执行的业务逻辑封装到独立的函数中，也就是有专门的模块处理程序中的业务逻辑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户的请求可能会包含多个（持久化）操作，这些操作有可能需要设计成不可分割的原子性操作，那么这里就形成了事务的边界。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;事务的ACID特性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子性（Atomicity）：事务中各项的操作要么全做要么全不做；&lt;/li&gt;
&lt;li&gt;一致性（Consistentcy）：事务前后系统的状态是一致的；&lt;/li&gt;
&lt;li&gt;隔离性（Isolation）：并发执行的事务无法看到彼此的中间状态；&lt;/li&gt;
&lt;li&gt;持久性（Duration）：事务完成后所做的改动都会被持久化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性，那么关系型数据库仍然是唯一的也是最好的选择，因为关系型数据库可以通过锁机制来保护数据。事务隔离级别从低到高依次是：Read Uncommitted（读未提交）、Read Committed（读提交）、Repeatable Read（可重复读）、Serializable（串行化）。事务隔离级别越高，数据并发访问的问题越少，但是性能越差；事务隔离级别越低，数据并发访问的问题越多，但是性能越好。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据并发访问会产生5种问题（请参考我的&lt;a href=&quot;https://blog.csdn.net/jackfrued/article/details/44921941&quot;&gt;《Java面试题全集（上）》&lt;/a&gt;第80题对该问题的讲解）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第1类丢失更新（A事务撤销覆盖B事务更新的数据）和第2类丢失更新（A事务提交覆盖B事务更新的数据）。&lt;/li&gt;
&lt;li&gt;脏读（读脏数据）：一个事务读取到其他尚未提交的事务的数据。&lt;/li&gt;
&lt;li&gt;不可重复读： 一个事务在读取它的查询结果时，被另一个事务更新了它的查询记录导致无法读到数据。&lt;/li&gt;
&lt;li&gt;幻读：一个事务在读取它的查询结果时，发现读到了被另一个事务提交的新数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 设置全局默认的事务隔离级别
set global transaction isolation level repeatable read;
-- 设置当前会话的事务隔离级别
set session transaction isolation level read committed;
-- 查询当前会话的事务隔离级别
select @@tx_isolation;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django中的事务控制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;给每个请求绑定事务环境（反模式）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ATOMIC_REQUESTS = True
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用事务装饰器（简单易用） - 粗粒度（控制不够精细）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@transaction.non_atomic_requests
@transaction.atomic
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用上下文语法（细粒度 - 事务控制的范围更加精准）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;with transaction.atomic():
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭自动提交使用手动提交。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AUTOCOMMIT = False
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;transaction.commit()
transaction.rollback()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;URL配置&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可以让部分URL只在调试模式下生效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.conf import settings

urlpatterns = [
    ...
]

if settings.DEBUG:
    urlpatterns += [ ... ]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用命名捕获组捕获路径参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url(r&apos;api/code/(?P&amp;lt;mobile&amp;gt;1[3-9]\d{9})&apos;),
path(&apos;api/code/&amp;lt;str:mobile&amp;gt;&apos;),
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;URL配置不关心请求使用的方法（一个视图函数可以处理不同的请求方式）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果使用&lt;code&gt;url&lt;/code&gt;函数捕获的路径参数都是字符串，&lt;code&gt;path&lt;/code&gt;函数可以指定路径参数类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用&lt;code&gt;include&lt;/code&gt;函数引入其他URL配置并指定&lt;code&gt;namespace&lt;/code&gt;来解决命名冲突，捕获的参数会向下传递。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;url&lt;/code&gt;和&lt;code&gt;path&lt;/code&gt;函数甚至是&lt;code&gt;include&lt;/code&gt;函数中都可以用字典向视图传入额外的参数，如果参数与捕获的参数同名，则使用字典中的参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以用&lt;code&gt;reverse&lt;/code&gt;函数实现URL的逆向解析（从名字解析出URL），在模板中也可以用&lt;code&gt;{% url %}&lt;/code&gt;实现同样的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path(&apos;&apos;, views.index, name=&apos;index&apos;)

return redirect(reverse(&apos;index&apos;))
return redirect(&apos;index&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;模板(View)&lt;/h4&gt;
&lt;h5&gt;后端渲染&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;模板的配置和渲染函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [
    {
        &apos;BACKEND&apos;: &apos;django.template.backends.django.DjangoTemplates&apos;,
        &apos;DIRS&apos;: [os.path.join(BASE_DIR, &apos;templates&apos;), ],
        &apos;APP_DIRS&apos;: True,
        &apos;OPTIONS&apos;: {
            &apos;context_processors&apos;: [
                &apos;django.template.context_processors.debug&apos;,
                &apos;django.template.context_processors.request&apos;,
                &apos;django.contrib.auth.context_processors.auth&apos;,
                &apos;django.contrib.messages.context_processors.messages&apos;,
            ],
        },
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;resp = render(request, &apos;index.html&apos;, {&apos;foo&apos;: ...})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板遇到变量名的查找顺序。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字典查找（如：&lt;code&gt;foo[&apos;bar&apos;]&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;属性查找（如：&lt;code&gt;foo.bar&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;方法调用（如：&lt;code&gt;foo.bar()&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;方法不能有必须传值的参数&lt;/li&gt;
&lt;li&gt;在模板中不能够给方法传参&lt;/li&gt;
&lt;li&gt;如果方法的&lt;code&gt;alters_data&lt;/code&gt;被设置为&lt;code&gt;True&lt;/code&gt;则不能调用该方法（避免误操作的风险），模型对象动态生成的&lt;code&gt;delete()&lt;/code&gt;和&lt;code&gt;save()&lt;/code&gt;方法都设定了&lt;code&gt;alters_data = True&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;列表索引查找（如：&lt;code&gt;foo[0]&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板标签的使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{% if %}&lt;/code&gt; / &lt;code&gt;{% else %}&lt;/code&gt; / &lt;code&gt;{% endif %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{% for %}&lt;/code&gt; / &lt;code&gt;{% endfor %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{% ifequal %}&lt;/code&gt; / &lt;code&gt;{% endifequal %}&lt;/code&gt; / &lt;code&gt;{% ifnotequal %}&lt;/code&gt; / &lt;code&gt;{% endifnotequal %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{# comment #}&lt;/code&gt; / &lt;code&gt;{% comment %}&lt;/code&gt; / &lt;code&gt;{% endcomment %}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;过滤器的使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lower&lt;/code&gt; / &lt;code&gt;upper&lt;/code&gt; / &lt;code&gt;first&lt;/code&gt; / &lt;code&gt;last&lt;/code&gt; / &lt;code&gt;truncatewords&lt;/code&gt; / &lt;code&gt;date &lt;/code&gt;/ &lt;code&gt;time&lt;/code&gt; / &lt;code&gt;length&lt;/code&gt; / &lt;code&gt;pluralize&lt;/code&gt; / &lt;code&gt;center&lt;/code&gt; / &lt;code&gt;ljust&lt;/code&gt; / &lt;code&gt;rjust&lt;/code&gt; / &lt;code&gt;cut&lt;/code&gt; / &lt;code&gt;urlencode&lt;/code&gt; / &lt;code&gt;default_if_none&lt;/code&gt; / &lt;code&gt;filesizeformat&lt;/code&gt; / &lt;code&gt;join&lt;/code&gt; / &lt;code&gt;slice&lt;/code&gt; / &lt;code&gt;slugify&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板的包含和继承。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{% include %}&lt;/code&gt; / &lt;code&gt;{% block %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{% extends %}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模板加载器（后面优化部分会讲到）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文件系统加载器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [{
    &apos;BACKEND&apos;: &apos;django.template.backends.django.DjangoTemplates&apos;,
    &apos;DIRS&apos;: [os.path.join(BASE_DIR, &apos;templates&apos;)],
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用目录加载器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [{
    &apos;BACKEND&apos;: &apos;django.template.backends.django.DjangoTemplates&apos;,
    &apos;APP_DIRS&apos;: True,
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;前端渲染&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;前端模板引擎：Handlebars / Mustache。&lt;/li&gt;
&lt;li&gt;前端MV*框架。
&lt;ul&gt;
&lt;li&gt;MVC - AngularJS&lt;/li&gt;
&lt;li&gt;MVVM(Model-View-ViewModel) - Vue.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;其他视图&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MIME（多用途Internet邮件扩展）类型 - 告知浏览器传输的数据类型。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content-Type&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;application/json&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/JSON&quot;&gt;JSON&lt;/a&gt;（JavaScript Object Notation）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;application/pdf&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/PDF&quot;&gt;PDF&lt;/a&gt;（Portable Document Format）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;audio/mpeg&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/MP3&quot;&gt;MP3&lt;/a&gt;或其他&lt;a href=&quot;https://zh.wikipedia.org/wiki/MPEG&quot;&gt;MPEG&lt;/a&gt;音频文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;audio/vnd.wave&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/WAV&quot;&gt;WAV&lt;/a&gt;音频文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image/gif&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/GIF&quot;&gt;GIF&lt;/a&gt;图像文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image/jpeg&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/JPEG&quot;&gt;JPEG&lt;/a&gt;图像文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image/png&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/PNG&quot;&gt;PNG&lt;/a&gt;图像文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text/html&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/HTML&quot;&gt;HTML&lt;/a&gt;文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text/xml&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/XML&quot;&gt;XML&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;video/mp4&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/MP4&quot;&gt;MP4&lt;/a&gt;视频文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;video/quicktime&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/QuickTime&quot;&gt;QuickTime&lt;/a&gt;视频文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何处置生成的内容（inline / attachment）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from urllib.parse import quote
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; response[&apos;content-type&apos;] = &apos;application/pdf&apos;
&amp;gt;&amp;gt;&amp;gt; filename = quote(&apos;Python语言规范.pdf&apos;)
&amp;gt;&amp;gt;&amp;gt; filename
&apos;Python%E8%AF%AD%E8%A8%80%E8%A7%84%E8%8C%83.pdf&apos;
&amp;gt;&amp;gt;&amp;gt; response[&apos;content-disposition&apos;] = f&apos;attachment; filename=&quot;{filename}&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;提醒：URL以及请求和响应头中的中文都应该处理成&lt;a href=&quot;https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81&quot;&gt;百分号编码&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成CSV / Excel / PDF / 统计报表。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;向浏览器传输二进制数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from io import BytesIO

buffer = BytesIO()

resp = HttpResponse(content_type=&apos;...&apos;)
resp[&apos;Content-Disposition&apos;] = &apos;attachment; filename=&quot;...&quot;&apos;
resp.write(buffer.getvalue())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from io import BytesIO

import xlwt


def get_style(name, color=0, bold=False, italic=False):
    font = xlwt.Font()
    font.name, font.colour_index, font.bold, font.italic = \
    	name, color, bold, italic
    style = xlwt.XFStyle()
    style.font = font
    return style


def export_emp_excel(request):
    # 创建Excel工作簿(使用三方库xlwt)
    workbook = xlwt.Workbook()
    # 向工作簿中添加工作表
    sheet = workbook.add_sheet(&apos;员工详细信息&apos;)
    # 设置表头
    titles = [&apos;编号&apos;, &apos;姓名&apos;, &apos;主管&apos;, &apos;职位&apos;, &apos;工资&apos;, &apos;部门名称&apos;]
    for col, title in enumerate(titles):
        sheet.write(0, col, title, get_style(&apos;HanziPenSC-W3&apos;, 2, True))
    # 使用Django的ORM框架查询员工数据
    emps = Emp.objects.all().select_related(&apos;dept&apos;).select_related(&apos;mgr&apos;)
    cols = [&apos;no&apos;, &apos;name&apos;, &apos;mgr&apos;, &apos;job&apos;, &apos;sal&apos;, &apos;dept&apos;]
    # 通过嵌套的循环将员工表的数据写入Excel工作表的单元格
    for row, emp in enumerate(emps):
        for col, prop in enumerate(cols):
            val = getattr(emp, prop, &apos;&apos;)
            if isinstance(val, (Dept, Emp)):
                val = val.name
            sheet.write(row + 1, col, val)
    # 将Excel文件的二进制数据写入内存
    buffer = BytesIO()
    workbook.save(buffer)
    # 通过HttpResponse对象向浏览器输出Excel文件
    resp = HttpResponse(buffer.getvalue())
    resp[&apos;content-type&apos;] = &apos;application/msexcel&apos;
    # 如果文件名有中文需要处理成百分号编码
    resp[&apos;content-disposition&apos;] = &apos;attachment; filename=&quot;detail.xls&quot;&apos;
    return resp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大文件的流式处理：&lt;code&gt;StreamingHttpResponse&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def download_file(request):
    file_stream = open(&apos;...&apos;, &apos;rb&apos;)
    # 如果文件的二进制数据较大则最好用迭代器进行处理避免过多的占用服务器内存
    file_iter = iter(lambda: file_stream.read(4096), b&apos;&apos;)
    resp = StreamingHttpResponse(file_iter)
    # 中文文件名要处理成百分号编码
    filename = quote(&apos;...&apos;, &apos;utf-8&apos;)
    resp[&apos;Content-Type&apos;] = &apos;...&apos;
    resp[&apos;Content-Disposition&apos;] = f&apos;attachment; filename=&quot;{filename}&quot;&apos;
    return resp
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果需要生成PDF文件，可以需要安装&lt;code&gt;reportlab&lt;/code&gt;。另外，使用StreamingHttpResponse只能减少内存的开销，但是如果下载一个大文件会导致一个请求长时间占用服务器资源，比较好的做法还是把报表提前生成好（可以考虑使用定时任务），放在静态资源服务器或者是云存储服务器上以访问静态资源的方式访问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://echarts.baidu.com/&quot;&gt;ECharts&lt;/a&gt;或&lt;a href=&quot;https://www.chartjs.org/&quot;&gt;Chart.js&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;思路：后端只提供JSON格式的数据，前端JavaScript渲染生成图表。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;def get_charts_data(request):
    &quot;&quot;&quot;获取统计图表JSON数据&quot;&quot;&quot;
    names = []
    totals = []
    # 通过connections获取指定数据库连接并创建游标对象
    with connections[&apos;backend&apos;].cursor() as cursor:
        # 在使用ORM框架时可以使用对象管理器的aggregate()和annotate()方法实现分组和聚合函数查询
        # 执行原生SQL查询(如果ORM框架不能满足业务或性能需求)
        cursor.execute(&apos;select dname, total from vw_dept_emp&apos;)
        for row in cursor.fetchall():
            names.append(row[0])
            totals.append(row[1])
    return JsonResponse({&apos;names&apos;: names, &apos;totals&apos;: totals})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;统计图表&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        #main {
            width: 600px;
            height: 400px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;main&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcss.com/echarts/4.2.0-rc.2/echarts.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        var myChart = echarts.init($(&apos;#main&apos;)[0]);
        $.ajax({
            &apos;url&apos;: &apos;charts_data&apos;,
            &apos;type&apos;: &apos;get&apos;,
            &apos;data&apos;: {},
            &apos;dataType&apos;: &apos;json&apos;,
            &apos;success&apos;: function(json) {
                var option = {
                    title: {
                        text: &apos;员工分布统计图&apos;
                    },
                    tooltip: {},
                    legend: {
                        data:[&apos;人数&apos;]
                    },
                    xAxis: {
                        data: json.names
                    },
                    yAxis: {},
                    series: [{
                        name: &apos;人数&apos;,
                        type: &apos;bar&apos;,
                        data: json.totals
                    }]
                };
                myChart.setOption(option);
            },
            &apos;error&apos;: function() {}
        });
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;中间件&lt;/h4&gt;
&lt;p&gt;问题1：中间件背后的设计理念是什么？（分离横切关注功能/拦截过滤器模式）&lt;/p&gt;
&lt;p&gt;问题2：中间件有哪些不同的实现方式？（参考下面的代码）&lt;/p&gt;
&lt;p&gt;问题3：描述Django内置的中间件及其执行顺序。（推荐阅读：&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering&quot;&gt;Django官方文档 - 中间件 - 中间件顺序&lt;/a&gt;）&lt;/p&gt;
&lt;h5&gt;激活中间件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
    &apos;common.middlewares.block_sms_middleware&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;自定义中间件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;def simple_middleware(get_response):
    
    def middleware(request, *args, **kwargs):
        
		response = get_response(request, *args, **kwargs)
        
		return response
    
    return middleware
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class MyMiddleware:
        
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, request):
        
        response = self.get_response(request)
       
        return response
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class MyMiddleware(MiddlewareMixin):
    
    def __init__(self):
        pass
    
    def process_request(request):
        pass
    
    def process_view(request, view_func, view_args, view_kwargs):
        pass
    
    def process_template_response(request, response):
        pass
    
    def process_response(request, response):
        pass
    
    def process_exception(request, exception):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;内置中间件&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;CommonMiddleware - 基础设置中间件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DISALLOWED_USER_AGENTS - 不被允许的用户代理（浏览器）&lt;/li&gt;
&lt;li&gt;APPEND_SLASH - 是否追加&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;USE_ETAG - 浏览器缓存相关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SecurityMiddleware - 安全相关中间件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SECURE_HSTS_SECONDS - 强制使用HTTPS的时间&lt;/li&gt;
&lt;li&gt;SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名&lt;/li&gt;
&lt;li&gt;SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型&lt;/li&gt;
&lt;li&gt;SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器&lt;/li&gt;
&lt;li&gt;SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接&lt;/li&gt;
&lt;li&gt;SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SessionMiddleware - 会话中间件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CsrfViewMiddleware - 防范跨站身份伪造中间件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;XFrameOptionsMiddleware - 防范点击劫持攻击中间件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/ea0e1404d7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/95509533d2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;表单&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用法：通常不要用来生成页面上的表单控件（耦合度太高不容易定制），主要用来验证数据。&lt;/li&gt;
&lt;li&gt;Form的属性和方法：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;is_valid()&lt;/code&gt; / &lt;code&gt;is_multipart()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;errors&lt;/code&gt; / &lt;code&gt;fields&lt;/code&gt; / &lt;code&gt;is_bound&lt;/code&gt; / &lt;code&gt;changed_data&lt;/code&gt; / &lt;code&gt;cleaned_data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_error()&lt;/code&gt; / &lt;code&gt;has_errors()&lt;/code&gt; / &lt;code&gt;non_field_errors()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clean()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Form.errors的方法：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;as_data()&lt;/code&gt; / &lt;code&gt;as_json()&lt;/code&gt; / &lt;code&gt;get_json_data()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题1：Django中的&lt;code&gt;Form&lt;/code&gt;和&lt;code&gt;ModelForm&lt;/code&gt;有什么作用？（通常不用来生成表单主要用来验证数据）&lt;/p&gt;
&lt;p&gt;问题2：表单上传文件时应该注意哪些问题？（表单的设置、多文件上传、图片预览（FileReader）、Ajax上传文件、上传后的文件如何存储、调用云存储（如&lt;a href=&quot;https://www.aliyun.com/product/oss&quot;&gt;阿里云OSS&lt;/a&gt;、&lt;a href=&quot;https://www.qiniu.com/&quot;&gt;七牛云&lt;/a&gt;、&lt;a href=&quot;https://leancloud.cn/storage/&quot;&gt;LeanCloud&lt;/a&gt;等））&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;...&quot; multiple&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;foo&quot;&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;foo&quot;&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;foo&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上传图片文件的预览效果可以通过HTML5的FileReader来实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：使用云存储通常是比自己配置分布式文件系统这种方式更靠谱的做法，而且云存储通常成本并不太高，不仅如此大多数云存储还提供了如图片剪裁、生成水印、视频转码、CDN等服务。如果要自己做上传的视频文件转码，需要安装三方库ffmpeg，在程序中调用该三方库可以实现转码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Cookie和Session&lt;/h4&gt;
&lt;p&gt;问题1：使用Cookie能解决什么问题？（用户跟踪，解决HTTP协议无状态问题）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;URL重写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.abc.com/path/resource?foo=bar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐藏域（隐式表单域）- 埋点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;&quot; method=&quot;post&quot;&amp;gt;

    &amp;lt;input type=&quot;hidden&quot; name=&quot;foo&quot; value=&quot;bar&quot;&amp;gt;
    
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie - 浏览器中的临时文件（文本文件）- BASE64&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题2：Cookie和Session之间关系是什么？（Session的标识通过Cookie保存和传输）&lt;/p&gt;
&lt;h5&gt;Session的配置&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Session对应的中间件：&lt;code&gt;django.contrib.sessions.middleware.SessionMiddleware&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Session引擎。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基于数据库（默认方式）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    &apos;django.contrib.sessions&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基于缓存（推荐使用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SESSION_ENGINE = &apos;django.contrib.sessions.backends.cache&apos;
SESSION_CACHE_ALIAS = &apos;session&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基于文件（基本不考虑）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基于Cookie（不靠谱）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SESSION_ENGINE = &apos;django.contrib.sessions.backends.signed_cookies&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cookie相关的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SESSION_COOKIE_NAME = &apos;djang_session_id&apos;
SESSION_COOKIE_AGE = 1209600
# 如果设置为True，Cookie就是基于浏览器窗口的Cookie，不会持久化
SESSION_EXPIRE_AT_BROWSER_CLOSE = False 
SESSION_SAVE_EVERY_REQUEST = False
SESSION_COOKIE_HTTPONLY = True
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;session的属性和方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;session_key&lt;/code&gt; / &lt;code&gt;session_data&lt;/code&gt; / &lt;code&gt;expire_date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__getitem__&lt;/code&gt; / &lt;code&gt;__setitem__&lt;/code&gt; / &lt;code&gt;__delitem__&lt;/code&gt; / &lt;code&gt;__contains__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_expiry()&lt;/code&gt; / &lt;code&gt;get_expiry_age()&lt;/code&gt; / &lt;code&gt;get_expiry_date()&lt;/code&gt; - 设置/获取会话超期时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flush()&lt;/code&gt; - 销毁会话&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_test_cookie()&lt;/code&gt; / &lt;code&gt;test_cookie_worked()&lt;/code&gt; / &lt;code&gt;delete_test_cookie()&lt;/code&gt; - 测试浏览器是否支持Cookie（提示用户如果浏览器禁用Cookie可能会影响网站的使用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;session的序列化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SESSION_SERIALIZER = &apos;django.contrib.sessions.serializers.JSONSerializer&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSONSerializer（1.6及以后默认）- 如果想将自定义的对象放到session中，会遇到“Object of type &apos;XXX&apos; is not JSON serializable”的问题（如果配置使用Redis保存Session，django-redis使用了Pickle序列化，这个问题就不存在了）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PickleSerializer（1.6以前的默认）- 因为安全问题不推荐使用，但是只要不去反序列化用户构造的恶意的Payload其实也没有什么风险。关于这种方式的安全漏洞，可以参考《&lt;a href=&quot;http://www.polaris-lab.com/index.php/archives/178/&quot;&gt;Python Pickle的任意代码执行漏洞实践和Payload构造》&lt;/a&gt;一文或《软件架构-Python语言实现》上关于这个问题的讲解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果使用了django_redis整合Redis作为session的存储引擎，那么由于django_redis又封装了一个PickleSerializer来提供序列化，所以不会存在上述的问题，且Redis中保存的value是pickle序列化之后的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;缓存&lt;/h4&gt;
&lt;h5&gt;配置缓存&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;CACHES = {
    # 默认缓存
    &apos;default&apos;: {
        &apos;BACKEND&apos;: &apos;django_redis.cache.RedisCache&apos;,
        &apos;LOCATION&apos;: [
            &apos;redis://1.2.3.4:6379/0&apos;,
        ],
        &apos;KEY_PREFIX&apos;: &apos;teamproject&apos;,
        &apos;OPTIONS&apos;: {
            &apos;CLIENT_CLASS&apos;: &apos;django_redis.client.DefaultClient&apos;,
            &apos;CONNECTION_POOL_KWARGS&apos;: {
                &apos;max_connections&apos;: 1000,
            },
            &apos;PASSWORD&apos;: &apos;yourpass&apos;,
        }
    },
    # 页面缓存
    &apos;page&apos;: {
        &apos;BACKEND&apos;: &apos;django_redis.cache.RedisCache&apos;,
        &apos;LOCATION&apos;: [
            &apos;redis://1.2.3.4:6379/1&apos;,
        ],
        &apos;KEY_PREFIX&apos;: &apos;teamproject:page&apos;,
        &apos;OPTIONS&apos;: {
            &apos;CLIENT_CLASS&apos;: &apos;django_redis.client.DefaultClient&apos;,
            &apos;CONNECTION_POOL_KWARGS&apos;: {
                &apos;max_connections&apos;: 500,
            },
            &apos;PASSWORD&apos;: &apos;yourpass&apos;,
        }
    },
    # 会话缓存
    &apos;session&apos;: {
        &apos;BACKEND&apos;: &apos;django_redis.cache.RedisCache&apos;,
        &apos;LOCATION&apos;: [
            &apos;redis://1.2.3.4:6379/2&apos;,
        ],
        &apos;KEY_PREFIX&apos;: &apos;teamproject:session&apos;,
        &apos;TIMEOUT&apos;: 1209600,
        &apos;OPTIONS&apos;: {
            &apos;CLIENT_CLASS&apos;: &apos;django_redis.client.DefaultClient&apos;,
            &apos;CONNECTION_POOL_KWARGS&apos;: {
                &apos;max_connections&apos;: 2000,
            },
            &apos;PASSWORD&apos;: &apos;yourpass&apos;,
        }
    },
    # 接口数据缓存
    &apos;api&apos;: {
        &apos;BACKEND&apos;: &apos;django_redis.cache.RedisCache&apos;,
        &apos;LOCATION&apos;: [
            &apos;redis://1.2.3.4:6379/3&apos;,
        ],
        &apos;KEY_PREFIX&apos;: &apos;teamproject:api&apos;,
        &apos;OPTIONS&apos;: {
            &apos;CLIENT_CLASS&apos;: &apos;django_redis.client.DefaultClient&apos;,
            &apos;CONNECTION_POOL_KWARGS&apos;: {
                &apos;max_connections&apos;: 500,
            },
            &apos;PASSWORD&apos;: &apos;yourpass&apos;,
        }
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：通过Redis底层提供的多个数据库来隔离缓存数据有助于缓存数据的管理。如果配置了Redis的主从复制（读写分离），LOCATION列表中可以配置多个Redis连接，第一个被视为master用来进行写操作，后面的被视为slave用来进行读操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;全站缓存&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE_CLASSES = [
    &apos;django.middleware.cache.UpdateCacheMiddleware&apos;,
    ...
    &apos;django.middleware.common.CommonMiddleware&apos;,
    ...
    &apos;django.middleware.cache.FetchFromCacheMiddleware&apos;,
]

CACHE_MIDDLEWARE_ALIAS = &apos;default&apos;
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = &apos;djang:cache&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;视图层缓存&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie


@cache_page(timeout=60 * 15, cache=&apos;page&apos;)
@vary_on_cookie
def my_view(request):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from django.views.decorators.cache import cache_page

urlpatterns = [
    url(r&apos;^foo/([0-9]{1,2})/$&apos;, cache_page(60 * 15)(my_view)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;其他内容&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;模板片段缓存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{% load cache %}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{% cache %}&lt;/code&gt; / &lt;code&gt;{% endcache %}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用底层API访问缓存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.core.cache import cache
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; cache.set(&apos;my_key&apos;, &apos;hello, world!&apos;, 30)
&amp;gt;&amp;gt;&amp;gt; cache.get(&apos;my_key&apos;)
&amp;gt;&amp;gt;&amp;gt; cache.clear()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.core.cache import caches
&amp;gt;&amp;gt;&amp;gt; cache1 = caches[&apos;page&apos;]
&amp;gt;&amp;gt;&amp;gt; cache2 = caches[&apos;page&apos;]
&amp;gt;&amp;gt;&amp;gt; cache1 is cache2
True
&amp;gt;&amp;gt;&amp;gt; cache3 = caches[&apos;session&apos;]
&amp;gt;&amp;gt;&amp;gt; cache2 is cache3
False
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django_redis import get_redis_connection
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; redis_client = get_redis_connection()
&amp;gt;&amp;gt;&amp;gt; redis_client.hgetall()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;日志&lt;/h4&gt;
&lt;h5&gt;日志级别&lt;/h5&gt;
&lt;p&gt;NOTSET &amp;lt; DEBUG &amp;lt; INFO &amp;lt; WARNING &amp;lt; ERROR &amp;lt; CRITICAL&lt;/p&gt;
&lt;h5&gt;日志配置&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;LOGGING = {
    &apos;version&apos;: 1,
    &apos;disable_existing_loggers&apos;: False,
    # 配置日志格式化器
    &apos;formatters&apos;: {
        &apos;simple&apos;: {
            &apos;format&apos;: &apos;%(asctime)s %(module)s.%(funcName)s: %(message)s&apos;,
            &apos;datefmt&apos;: &apos;%Y-%m-%d %H:%M:%S&apos;,
        },
        &apos;verbose&apos;: {
            &apos;format&apos;: &apos;%(asctime)s %(levelname)s [%(process)d-%(threadName)s] &apos;
                      &apos;%(module)s.%(funcName)s line %(lineno)d: %(message)s&apos;,
            &apos;datefmt&apos;: &apos;%Y-%m-%d %H:%M:%S&apos;,
        }
    },
    # 配置日志过滤器
    &apos;filters&apos;: {
        &apos;require_debug_true&apos;: {
            &apos;()&apos;: &apos;django.utils.log.RequireDebugTrue&apos;,
        },
    },
    # 配置日志处理器
    &apos;handlers&apos;: {
        &apos;console&apos;: {
            &apos;class&apos;: &apos;logging.StreamHandler&apos;,
            &apos;level&apos;: &apos;DEBUG&apos;,
            &apos;filters&apos;: [&apos;require_debug_true&apos;],
            &apos;formatter&apos;: &apos;simple&apos;,
        },
        &apos;file1&apos;: {
            &apos;class&apos;: &apos;logging.handlers.TimedRotatingFileHandler&apos;,
            &apos;filename&apos;: &apos;access.log&apos;,
            &apos;when&apos;: &apos;W0&apos;,
            &apos;backupCount&apos;: 12,
            &apos;formatter&apos;: &apos;simple&apos;,
            &apos;level&apos;: &apos;INFO&apos;,
        },
        &apos;file2&apos;: {
            &apos;class&apos;: &apos;logging.handlers.TimedRotatingFileHandler&apos;,
            &apos;filename&apos;: &apos;error.log&apos;,
            &apos;when&apos;: &apos;D&apos;,
            &apos;backupCount&apos;: 31,
            &apos;formatter&apos;: &apos;verbose&apos;,
            &apos;level&apos;: &apos;WARNING&apos;,
        },
    },
    # 配置日志器
    &apos;loggers&apos;: {
        &apos;django&apos;: {
            &apos;handlers&apos;: [&apos;console&apos;, &apos;file1&apos;, &apos;file2&apos;],
            &apos;propagate&apos;: True,
            &apos;level&apos;: &apos;DEBUG&apos;,
        },
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/2.0/topics/logging/#s-examples&quot;&gt;日志配置官方示例&lt;/a&gt;。&lt;/p&gt;
&lt;h5&gt;日志分析&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Linux相关命令：head、tail、grep、awk、uniq、sort&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -10000 access.log | awk &apos;{print $1}&apos; | uniq -c | sort -r
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实时日志文件分析：Python + 正则表达式 + Crontab&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jkklee/web_log_analyse&quot;&gt;《Python日志分析工具》&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ibm.com/developerworks/cn/opensource/os-cn-elk/index.html&quot;&gt;《集中式日志系统ELK》&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ElasticSearch：搜索引擎，实现全文检索。&lt;/li&gt;
&lt;li&gt;Logstash：负责从指定节点收集日志。&lt;/li&gt;
&lt;li&gt;Kibana：日志可视化工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大数据日志处理：Flume+Kafka日志采集、Storm / Spark实时数据处理、Impala实时查询。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;RESTful&lt;/h4&gt;
&lt;p&gt;问题1：RESTful架构到底解决了什么问题？（URL具有自描述性、资源表述与视图的解耦和、互操作性利用构建微服务以及集成第三方系统、无状态性提高水平扩展能力）&lt;/p&gt;
&lt;p&gt;问题2：项目在使用RESTful架构时有没有遇到一些问题或隐患？（对资源访问的限制、资源从属关系检查、避免泄露业务信息、防范可能的攻击）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;补充：下面的几个和安全性相关的响应头在前面讲中间件的时候提到过的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X-Frame-Options: DENY&lt;/li&gt;
&lt;li&gt;X-Content-Type-Options: nosniff&lt;/li&gt;
&lt;li&gt;X-XSS-Protection: 1; mode=block;&lt;/li&gt;
&lt;li&gt;Strict­-Transport-­Security: max-age=31536000;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;问题3：如何保护API中的敏感信息以及防范重放攻击？（摘要和令牌）&lt;/p&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;https://help.aliyun.com/knowledge_detail/50041.html&quot;&gt;《如何有效防止API的重放攻击》&lt;/a&gt;。&lt;/p&gt;
&lt;h5&gt;使用djangorestframework&lt;/h5&gt;
&lt;p&gt;安装djangorestfrmework（为了描述方便，以下统一简称为DRF）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install djangorestframework
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置DRF。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    
    &apos;rest_framework&apos;,
    
]

REST_FRAMEWORK = {
    # 配置默认页面大小
    &apos;PAGE_SIZE&apos;: 10,
    # 配置默认的分页类
    &apos;DEFAULT_PAGINATION_CLASS&apos;: &apos;rest_framework.pagination.PageNumberPagination&apos;,
    # 配置异常处理器
    # &apos;EXCEPTION_HANDLER&apos;: &apos;api.exceptions.exception_handler&apos;,
    # 配置默认解析器
    # &apos;DEFAULT_PARSER_CLASSES&apos;: (
    #     &apos;rest_framework.parsers.JSONParser&apos;,
    #     &apos;rest_framework.parsers.FormParser&apos;,
    #     &apos;rest_framework.parsers.MultiPartParser&apos;,
    # ),
    # 配置默认限流类
    # &apos;DEFAULT_THROTTLE_CLASSES&apos;: (),
    # 配置默认授权类
    # &apos;DEFAULT_PERMISSION_CLASSES&apos;: (
    #     &apos;rest_framework.permissions.IsAuthenticated&apos;,
    # ),
    # 配置默认认证类
    # &apos;DEFAULT_AUTHENTICATION_CLASSES&apos;: (
    #     &apos;rest_framework_jwt.authentication.JSONWebTokenAuthentication&apos;,
    # ),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;编写序列化器&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework import serializers
from rest_framework.serializers import ModelSerializer

from common.models import District, HouseType, Estate, Agent


class DistrictSerializer(ModelSerializer):

    class Meta:
        model = District
        fields = (&apos;distid&apos;, &apos;name&apos;)


class HouseTypeSerializer(ModelSerializer):

    class Meta:
        model = HouseType
        fields = &apos;__all__&apos;


class AgentSerializer(ModelSerializer):

    class Meta:
        model = Agent
        fields = (&apos;agentid&apos;, &apos;name&apos;, &apos;tel&apos;, &apos;servstar&apos;, &apos;certificated&apos;)


class EstateSerializer(ModelSerializer):
    district = serializers.SerializerMethodField()
    agents = serializers.SerializerMethodField()

    @staticmethod
    def get_agents(estate):
        return AgentSerializer(estate.agents, many=True).data

    @staticmethod
    def get_district(estate):
        return DistrictSerializer(estate.district).data

    class Meta:
        model = Estate
        fields = &apos;__all__&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;方法1：使用装饰器&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@api_view([&apos;GET&apos;])
@cache_page(timeout=None, cache=&apos;api&apos;)
def provinces(request):
    queryset = District.objects.filter(parent__isnull=True)
    serializer = DistrictSerializer(queryset, many=True)
    return Response(serializer.data)


@api_view([&apos;GET&apos;])
@cache_page(timeout=300, cache=&apos;api&apos;)
def cities(request, provid):
    queryset = District.objects.filter(parent__distid=provid)
    serializer = DistrictSerializer(queryset, many=True)
    return Response(serializer.data)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    path(&apos;districts/&apos;, views.provinces, name=&apos;districts&apos;),
    path(&apos;districts/&amp;lt;int:provid&amp;gt;/&apos;, views.cities, name=&apos;cities&apos;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面使用了Django自带的视图装饰器（@cache_page）来实现对API接口返回数据的缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;方法2：使用APIView及其子类&lt;/h5&gt;
&lt;p&gt;更好的复用代码，不要重“复发明轮子”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class HouseTypeApiView(CacheResponseMixin, ListAPIView):
    queryset = HouseType.objects.all()
    serializer_class = HouseTypeSerializer
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    path(&apos;housetypes/&apos;, views.HouseTypeApiView.as_view(), name=&apos;housetypes&apos;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面使用了drf_extensions提供的CacheResponseMixin混入类实现了对接口数据的缓存。如果重写了获取数据的方法，可以使用drf_extensions提供的@cache_response来实现对接口数据的缓存，也可以用自定义的函数来生成缓存中的key。当然还有一个选择就是通过Django提供的@method_decorator装饰器，将@cache_page装饰器处理为装饰方法的装饰器，这样也能提供使用缓存服务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;drf-extensions&lt;/code&gt;配置如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置DRF扩展来支持缓存API接口调用结果
REST_FRAMEWORK_EXTENSIONS = {
    &apos;DEFAULT_CACHE_RESPONSE_TIMEOUT&apos;: 300,
    &apos;DEFAULT_USE_CACHE&apos;: &apos;default&apos;,
    # 配置默认缓存单个对象的key函数
    &apos;DEFAULT_OBJECT_CACHE_KEY_FUNC&apos;: &apos;rest_framework_extensions.utils.default_object_cache_key_func&apos;,
    # 配置默认缓存对象列表的key函数
    &apos;DEFAULT_LIST_CACHE_KEY_FUNC&apos;: &apos;rest_framework_extensions.utils.default_list_cache_key_func&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;方法3：使用ViewSet及其子类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;class HouseTypeViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    queryset = HouseType.objects.all()
    serializer_class = HouseTypeSerializer
    pagination_class = None
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;router = DefaultRouter()
router.register(&apos;housetypes&apos;, views.HouseTypeViewSet)

urlpatterns += router.urls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;djangorestframework提供了基于Bootstrap定制的页面来显示接口返回的JSON数据，当然也可以使用&lt;a href=&quot;https://www.getpostman.com/&quot;&gt;POSTMAN&lt;/a&gt;这样的工具对API接口进行测试。&lt;/p&gt;
&lt;h5&gt;补充说明&lt;/h5&gt;
&lt;p&gt;在这里顺便提一下跟前端相关的几个问题。&lt;/p&gt;
&lt;p&gt;问题1：如何让浏览器能够发起DELETE/PUT/PATCH？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form method=&quot;post&quot;&amp;gt;
    
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;delete&quot;&amp;gt;
    
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;if request.method == &apos;POST&apos; and &apos;_method&apos; in request.POST:
    request.method = request.POST[&apos;_method&apos;].upper()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    $.ajax({
        &apos;url&apos;: &apos;/api/provinces&apos;,
        &apos;type&apos;: &apos;put&apos;,
        &apos;data&apos;: {},
        &apos;dataType&apos;: &apos;json&apos;,
        &apos;success&apos;: function(json) {
            // Web = 标签(内容) + CSS(显示) + JS(行为)
            // JavaScript = ES + BOM + DOM
            // DOM操作实现页面的局部刷新
        },
        &apos;error&apos;: function() {}
    });
    $.getJSON(&apos;/api/provinces&apos;, function(json) {
        // DOM操作实现页面的局部刷新
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题2：如何解决多个JavaScript库之间某个定义（如$函数）冲突的问题？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;js/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;js/abc.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    // $已经被后加载的JavaScript库占用了
    // 但是可以直接用绑定在window对象上的jQuery去代替$
    jQuery(function() {
        jQuery(&apos;#okBtn&apos;).on(&apos;click&apos;, function() {});
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;js/abc.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;js/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    // 将$让出给其他的JavaScript库使用
	jQuery.noConflict();
	jQuery(function() {
        jQuery(&apos;#okBtn&apos;).on(&apos;click&apos;, function() {});
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题3：jQuery对象与原生DOM对象之间如何转换？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button id=&quot;okBtn&quot;&amp;gt;点我&amp;lt;/button&amp;gt;
&amp;lt;script src=&quot;js/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    var btn = document.getElementById(&apos;okBtn&apos;);	// 原生JavaScript对象(使用相对麻烦)
    var $btn = $(&apos;#okBtn&apos;);	// jQuery对象(拥有更多的属性和方法而且没有浏览器兼容性问题)
    $btn.on(&apos;click&apos;, function() {});
    // $(btn)可以将原生JavaScript对象转成jQuery对象
    // $btn.get(0)或$btn[0]可以获得原生的JavaScript对象
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;过滤数据&lt;/h5&gt;
&lt;p&gt;如果需要过滤数据（对数据接口设置筛选条件、排序条件等），可以使用&lt;code&gt;django-filter&lt;/code&gt;三方库来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-filter
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    
    &apos;django_filters&apos;,

]
REST_FRAMEWORK = {
  	
    &apos;DEFAULT_FILTER_BACKENDS&apos;: (
        &apos;django_filters.rest_framework.DjangoFilterBackend&apos;,
        &apos;rest_framework.filters.OrderingFilter&apos;,
    ),
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter
from rest_framework.generics import RetrieveAPIView, ListCreateAPIView

from api.serializers import EstateSerializer
from common.models import Estate


@method_decorator(decorator=cache_page(timeout=120, cache=&apos;api&apos;, key_prefix=&apos;estates&apos;), name=&apos;get&apos;)
class EstateView(RetrieveAPIView, ListCreateAPIView):
    queryset = Estate.objects.all().select_related(&apos;district&apos;).prefetch_related(&apos;agents&apos;)
    serializer_class = EstateSerializer
    filter_backends = (DjangoFilterBackend, OrderingFilter)
    filter_fields = (&apos;name&apos;, &apos;district&apos;)
    ordering = (&apos;-hot&apos;, )
    ordering_fields = (&apos;hot&apos;, &apos;estateid&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from django_filters import rest_framework as drf
from common.models import HouseInfo


class HouseInfoFilter(drf.FilterSet):
    &quot;&quot;&quot;自定义房源数据过滤器&quot;&quot;&quot;

    title = drf.CharFilter(lookup_expr=&apos;starts&apos;)
    dist = drf.NumberFilter(field_name=&apos;district&apos;)
    min_price = drf.NumberFilter(field_name=&apos;price&apos;, lookup_expr=&apos;gte&apos;)
    max_price = drf.NumberFilter(field_name=&apos;price&apos;, lookup_expr=&apos;lte&apos;)
    type = drf.NumberFilter()

    class Meta:
        model = HouseInfo
        fields = (&apos;title&apos;, &apos;district&apos;, &apos;min_price&apos;, &apos;max_price&apos;, &apos;type&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class HouseInfoViewSet(CacheResponseMixin, ReadOnlyModelViewSet):
    queryset = HouseInfo.objects.all() \
        .select_related(&apos;type&apos;, &apos;district&apos;, &apos;estate&apos;, &apos;agent&apos;) \
        .prefetch_related(&apos;tags&apos;).order_by(&apos;-pubdate&apos;)
    serializer_class = HouseInfoSerializer
    filter_backends = (DjangoFilterBackend, OrderingFilter)
    filterset_class = HouseInfoFilter
    ordering = (&apos;price&apos;,)
    ordering_fields = (&apos;price&apos;, &apos;area&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;身份认证&lt;/h5&gt;
&lt;p&gt;查看DRF中APIView类的代码可以看出，DRF默认的认证方案是 &lt;code&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/code&gt;，如果修改authentication_classes就可以自行定制身份认证的方案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

   	# 此处省略下面的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;DEFAULTS = {
    # Base API policies
    &apos;DEFAULT_RENDERER_CLASSES&apos;: (
        &apos;rest_framework.renderers.JSONRenderer&apos;,
        &apos;rest_framework.renderers.BrowsableAPIRenderer&apos;,
    ),
    &apos;DEFAULT_PARSER_CLASSES&apos;: (
        &apos;rest_framework.parsers.JSONParser&apos;,
        &apos;rest_framework.parsers.FormParser&apos;,
        &apos;rest_framework.parsers.MultiPartParser&apos;
    ),
    &apos;DEFAULT_AUTHENTICATION_CLASSES&apos;: (
        &apos;rest_framework.authentication.SessionAuthentication&apos;,
        &apos;rest_framework.authentication.BasicAuthentication&apos;
    ),
    &apos;DEFAULT_PERMISSION_CLASSES&apos;: (
        &apos;rest_framework.permissions.AllowAny&apos;,
    ),
    &apos;DEFAULT_THROTTLE_CLASSES&apos;: (),
    &apos;DEFAULT_CONTENT_NEGOTIATION_CLASS&apos;: &apos;rest_framework.negotiation.DefaultContentNegotiation&apos;,
    &apos;DEFAULT_METADATA_CLASS&apos;: &apos;rest_framework.metadata.SimpleMetadata&apos;,
    &apos;DEFAULT_VERSIONING_CLASS&apos;: None,

    # 此处省略下面的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义认证类，继承&lt;code&gt;BaseAuthentication&lt;/code&gt;并重写&lt;code&gt;authenticate(self, request)&lt;/code&gt;方法，通过请求中的userid和token来确定用户身份。如果认证成功，该方法应返回一个二元组（用户和令牌的信息），否则产生异常。也可以重写 &lt;code&gt;authenticate_header(self, request)&lt;/code&gt;方法来返回一个字符串，该字符串将用于&lt;code&gt;HTTP 401 Unauthorized&lt;/code&gt;响应中的WWW-Authenticate响应头的值。如果未重写该方法，那么当未经身份验证的请求被拒绝访问时，身份验证方案将返回&lt;code&gt;HTTP 403 Forbidden&lt;/code&gt;响应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyAuthentication(BaseAuthentication):
    &quot;&quot;&quot;自定义用户身份认证类&quot;&quot;&quot;

    def authenticate(self, request):
        try:
            token = request.GET[&apos;token&apos;] or request.POST[&apos;token&apos;]
            user_token = UserToken.objects.filter(token=token).first()
            if user_token:
                return user_token.user, user_token
            else:
                raise AuthenticationFailed(&apos;请提供有效的用户身份标识&apos;)
        except KeyError:
            raise AuthenticationFailed(&apos;请提供有效的用户身份标识&apos;)

    def authenticate_header(self, request):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用自定义的认证类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EstateViewSet(CacheResponseMixin, ModelViewSet):
    # 通过queryset指定如何获取数据（资源）
    queryset = Estate.objects.all().select_related(&apos;district&apos;).prefetch_related(&apos;agents&apos;)
    # 通过serializer_class指定如何序列化数据
    serializer_class = EstateSerializer
    # 指定根据哪些字段进行数据筛选
    filter_fields = (&apos;district&apos;, &apos;name&apos;)
    # 指定根据哪些字段对数据进行排序
    ordering_fields = (&apos;hot&apos;, )
    # 指定用于进行用户身份验证的类
    authentication_classes = (MyAuthentication, )
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：也可以在Django配置文件中将自定义的认证类设置为默认认证方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;授予权限&lt;/h5&gt;
&lt;p&gt;权限检查总是在视图的最开始处运行，在任何其他代码被允许进行之前。最简单的权限是允许通过身份验证的用户访问，并拒绝未经身份验证的用户访问，这对应于dfr中的&lt;code&gt;IsAuthenticated&lt;/code&gt;类，可以用它来取代默认的&lt;code&gt;AllowAny&lt;/code&gt;类。权限策略可以在Django的DRF配置中用&lt;code&gt;DEFAULT_PERMISSION_CLASSES&lt;/code&gt;全局设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REST_FRAMEWORK = {
    &apos;DEFAULT_PERMISSION_CLASSES&apos;: (
        &apos;rest_framework.permissions.IsAuthenticated&apos;,
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在基于&lt;code&gt;APIView&lt;/code&gt;类的视图上设置身份验证策略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class ExampleView(APIView):
    permission_classes = (IsAuthenticated, )
    # 此处省略其他代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在基于&lt;code&gt;@api_view&lt;/code&gt;装饰器的视图函数上设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated

@api_view([&apos;GET&apos;])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
    # 此处省略其他代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义权限需要继承&lt;code&gt;BasePermission&lt;/code&gt;并实现以下方法中的一个或两个，下面是BasePermission的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@six.add_metaclass(BasePermissionMetaclass)
class BasePermission(object):
    &quot;&quot;&quot;
    A base class from which all permission classes should inherit.
    &quot;&quot;&quot;

    def has_permission(self, request, view):
        &quot;&quot;&quot;
        Return `True` if permission is granted, `False` otherwise.
        &quot;&quot;&quot;
        return True

    def has_object_permission(self, request, view, obj):
        &quot;&quot;&quot;
        Return `True` if permission is granted, `False` otherwise.
        &quot;&quot;&quot;
        return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果请求被授予访问权限，则方法应该返回True，否则返False。下面的例子演示了阻止黑名单中的IP地址访问接口数据（这个在反爬虫的时候很有用哟）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework import permissions


class BlacklistPermission(permissions.BasePermission):
    &quot;&quot;&quot;
    Global permission check for blacklisted IPs.
    &quot;&quot;&quot;

    def has_permission(self, request, view):
        ip_addr = request.META[&apos;REMOTE_ADDR&apos;]
        blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
        return not blacklisted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要实现更为完整的权限验证，可以考虑RBAC或ACL。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;RBAC - 基于角色的访问控制，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/d94316469b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/0d228661d3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ACL - 访问控制列表（每个用户绑定自己的访问白名单或黑名单）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;访问限流&lt;/h5&gt;
&lt;p&gt;可以修改dfr配置的&lt;code&gt;DEFAULT_THROTTLE_CLASSES&lt;/code&gt; 和 &lt;code&gt;DEFAULT_THROTTLE_RATES&lt;/code&gt;两个值来设置全局默认限流策略。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REST_FRAMEWORK = {
    &apos;DEFAULT_THROTTLE_CLASSES&apos;: (
        &apos;rest_framework.throttling.AnonRateThrottle&apos;,
        &apos;rest_framework.throttling.UserRateThrottle&apos;
    ),
    &apos;DEFAULT_THROTTLE_RATES&apos;: {
        &apos;anon&apos;: &apos;3/min&apos;,
        &apos;user&apos;: &apos;10000/day&apos;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DEFAULT_THROTTLE_RATES&lt;/code&gt;中使用的频率描述可能包括&lt;code&gt;second&lt;/code&gt;、&lt;code&gt;minute&lt;/code&gt;、&lt;code&gt;hour&lt;/code&gt;或&lt;code&gt;day&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果要为接口单独设置限流，可以在每个视图或视图集上设置限流策略，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView


class ExampleView(APIView):
    throttle_classes = (UserRateThrottle, )
    # 此处省略下面的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@api_view([&apos;GET&apos;])
@throttle_classes([UserRateThrottle, ])
def example_view(request, format=None):
    # 此处省略下面的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以通过继承&lt;code&gt;SimpleRateThrottle&lt;/code&gt;来自定义限流策略，通常需要重写&lt;code&gt;allow_request&lt;/code&gt;和&lt;code&gt;wait&lt;/code&gt;方法。&lt;/p&gt;
&lt;h4&gt;异步任务和计划任务&lt;/h4&gt;
&lt;h5&gt;Celery的应用&lt;/h5&gt;
&lt;p&gt;Celery 是一个简单、灵活且可靠的，处理大量消息的分布式系统，并且提供维护这样一个系统的必需工具。它是一个专注于实时处理的任务队列，同时也支持任务调度。&lt;/p&gt;
&lt;p&gt;推荐阅读：&lt;a href=&quot;http://docs.jinkan.org/docs/celery/&quot;&gt;《Celery官方文档中文版》&lt;/a&gt;，上面有极为详细的配置和使用指南。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/050c3ab002.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Celery是一个本身不提供队列服务，官方推荐使用RabbitMQ或Redis来实现消息队列服务，前者是更好的选择，它对AMQP（高级消息队列协议）做出了非常好的实现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装RabbitMQ。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull rabbitmq
docker run -d -p 5672:5672 --name myrabbit rabbitmq
docker container exec -it myrabbit /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建用户、资源以及分配操作权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rabbitmqctl add_user luohao 123456
rabbitmqctl set_user_tags luohao administrator
rabbitmqctl add_vhost vhost1
rabbitmqctl set_permissions -p vhost1 luohao &quot;.*&quot; &quot;.*&quot; &quot;.*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建Celery实例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 注册环境变量
os.environ.setdefault(&apos;DJANGO_SETTINGS_MODULE&apos;, &apos;项目名.settings&apos;)

# 创建Celery实例
app = celery.Celery(
    &apos;fangtx&apos;,
    broker=&apos;amqp://luohao:123456@1.2.3.4:5672/vhost1&apos;
)

# 从项目的配置文件读取Celery配置信息
# app.config_from_object(&apos;django.conf:settings&apos;)
# 从指定的文件(例如celery_config.py)中读取Celery配置信息
# app.config_from_object(&apos;celery_config&apos;)

# 让Celery自动从参数指定的应用中发现异步任务/定时任务
# app.autodiscover_tasks([&apos;common&apos;, ])
# 让Celery自动从所有注册的应用中发现异步任务/定时任务
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动Celery创建woker（消息的消费者）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;celery -A &amp;lt;name&amp;gt; worker -l debug &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行异步任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.task
def send_email(from_user, to_user, cc_user, subject, content):
    pass


# 消息的生产者
send_email.delay(&apos;&apos;, [], [], &apos;&apos;, &apos;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建定时任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置定时任务（计划任务）
app.conf.update(
    timezone=settings.TIME_ZONE,
    enable_utc=True,
    # 定时任务（计划任务）相当于是消息的生产者
    # 如果只有生产者没有消费者那么消息就会在消息队列中积压
    # 将来实际部署项目的时候生产者、消费者、消息队列可能都是不同节点
    beat_schedule={
        &apos;task1&apos;: {
            &apos;task&apos;: &apos;common.tasks.scheduled_task&apos;,
            &apos;schedule&apos;: crontab(&apos;*&apos;, &apos;*&apos;, &apos;*&apos;, &apos;*&apos;, &apos;*&apos;),
            &apos;args&apos;: (&apos;...&apos;, )
        },
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@app.task
def scheduled_task(*args, **kwargs):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动Celery创建执行定时任务的beat（消息的生产者）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;celery -A &amp;lt;name&amp;gt; beat -l info
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查消息队列状况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rabbitmqctl list_queues -p vhost1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监控Celery - 可以通过flower来对Celery进行监控。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install flower
celery flower --broker=amqp://luohao:123456@120.77.222.217:5672/vhost1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;其他问题&lt;/h4&gt;
&lt;p&gt;问题1：如何解决JavaScript跨域获取数据的问题？（django-cors-headers）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    &apos;corsheaders&apos;,
]

MIDDLEWARE = [
    &apos;corsheaders.middleware.CorsMiddleware&apos;,
]

CORS_ORIGIN_ALLOW_ALL = True
# 配置跨域白名单
# CORS_ORIGIN_WHITELIST = (&apos;www.abc.com&apos;, &apos;www.baidu.com&apos;)
# CORS_ORIGIN_REGEX_WHITELIST = (&apos;...&apos;, )
# CORS_ALLOW_CREDENTIALS = True
# CORS_ALLOW_METHODS = (&apos;GET&apos;, &apos;POST&apos;, &apos;PUT&apos;, &apos;DELETE&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题2：网站图片（水印、剪裁）和视频（截图、水印、转码）是如何处理的？（云存储、FFmpeg）&lt;/p&gt;
&lt;p&gt;问题3：网站如何架设（静态资源）文件系统？（FastDFS、云存储、CDN）&lt;/p&gt;
&lt;h4&gt;安全保护&lt;/h4&gt;
&lt;p&gt;问题1：什么是跨站脚本攻击（XSS)，如何防范？（对提交的内容进行消毒）&lt;/p&gt;
&lt;p&gt;问题2：什么是跨站身份伪造（CSRF），如何防范？（使用随机令牌）&lt;/p&gt;
&lt;p&gt;问题3：什么是SQL注射攻击（SQL Injection），如何防范？（不拼接SQL语句，避免使用单引号）&lt;/p&gt;
&lt;p&gt;问题4：什么是点击劫持攻击（Click-hacking），如何防范？（不允许&lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;加载非同源站点内容）&lt;/p&gt;
&lt;h5&gt;Django提供的安全措施&lt;/h5&gt;
&lt;p&gt;签名数据的API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.core.signing import Signer
&amp;gt;&amp;gt;&amp;gt; signer = Signer()
&amp;gt;&amp;gt;&amp;gt; value = signer.sign(&apos;hello, world!&apos;)
&amp;gt;&amp;gt;&amp;gt; value
&apos;hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU&apos;
&amp;gt;&amp;gt;&amp;gt; signer.unsign(value)
&apos;hello, world!&apos;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; signer = Signer(salt=&apos;yoursalt&apos;)
&amp;gt;&amp;gt;&amp;gt; signer.sign(&apos;hello, world!&apos;)
&apos;hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M&apos;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; from django.core.signing import TimestampSigner
&amp;gt;&amp;gt;&amp;gt; signer = TimestampSigner()
&amp;gt;&amp;gt;&amp;gt; value = signer.sign(&apos;hello, world!&apos;)
&amp;gt;&amp;gt;&amp;gt; value
&apos;hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So&apos;
&amp;gt;&amp;gt;&amp;gt; signer.unsign(value, max_age=5)
Traceback (most recent call last):
 File &quot;&amp;lt;console&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
 File &quot;/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py&quot;, line 198, in unsign
 &apos;Signature age %s &amp;gt; %s seconds&apos; % (age, max_age))
 django.core.signing.SignatureExpired: Signature age 21.020604848861694 &amp;gt; 5 seconds
&amp;gt;&amp;gt;&amp;gt; signer.unsign(value, max_age=120)
&apos;hello, world!&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSRF令牌和小工具&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% csrf_token %}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;@csrf_exempt：免除令牌&lt;/li&gt;
&lt;li&gt;@csrf_protect：提供令牌保护&lt;/li&gt;
&lt;li&gt;@require_csrf_token：提供令牌保护&lt;/li&gt;
&lt;li&gt;@ensure_csrf_cookie：强制视图发送带令牌的cookie&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：可以在Chrome浏览器中安装EditThisCookie插件来方便的查看Cookie。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;用户敏感信息的保护&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;哈希摘要（签名）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import hashlib
&amp;gt;&amp;gt;&amp;gt; 
&amp;gt;&amp;gt;&amp;gt; md5_hasher = hashlib.md5()
&amp;gt;&amp;gt;&amp;gt; md5_hasher.update(&apos;hello, world!&apos;.encode())
&amp;gt;&amp;gt;&amp;gt; md5_hasher.hexdigest()
&apos;3adbbad1791fbae3ec908894c4963870&apos;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; sha1_hasher = hashlib.sha1()
&amp;gt;&amp;gt;&amp;gt; sha1_hasher.update(&apos;hello, world!&apos;.encode())
&amp;gt;&amp;gt;&amp;gt; sha1_hasher.update(&apos;goodbye, world!&apos;.encode())
&amp;gt;&amp;gt;&amp;gt; sha1_hasher.hexdigest()
&apos;1f09d30c707d53f3d16c530dd73d70a6ce7596a9&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加密和解密（对称加密和非对称加密）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pycrypto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AES对称加密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from hashlib import md5
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; from Crypto.Cipher import AES
&amp;gt;&amp;gt;&amp;gt; from Crypto import Random
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; key = md5(b&apos;mysecret&apos;).hexdigest()
&amp;gt;&amp;gt;&amp;gt; iv = Random.new().read(AES.block_size)
&amp;gt;&amp;gt;&amp;gt; str1 = &apos;我爱你们！&apos;
&amp;gt;&amp;gt;&amp;gt; str2 = AES.new(key, AES.MODE_CFB, iv).encrypt(str1)
b&apos;p\x96o\x85\x0bq\xc4-Y\xc4\xbcp\n)&amp;amp;&apos;
&amp;gt;&amp;gt;&amp;gt; str3 = AES.new(key, AES.MODE_CFB, iv).decrypt(str2).decode()
&apos;我爱你们！&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RSA非对称加密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from Crypto.PublicKey import RSA
&amp;gt;&amp;gt;&amp;gt; # 生成密钥对
&amp;gt;&amp;gt;&amp;gt; key_pair = RSA.generate(2048)
&amp;gt;&amp;gt;&amp;gt; # 导入公钥
&amp;gt;&amp;gt;&amp;gt; pub_key = RSA.importKey(key_pair.publickey().exportKey())
&amp;gt;&amp;gt;&amp;gt; # 导入私钥
&amp;gt;&amp;gt;&amp;gt; pri_key = RSA.importKey(key_pair.exportKey())
&amp;gt;&amp;gt;&amp;gt; # 明文
&amp;gt;&amp;gt;&amp;gt; message1 = &apos;hello, world!&apos;.encode()
&amp;gt;&amp;gt;&amp;gt; # 加密数据
&amp;gt;&amp;gt;&amp;gt; message2 = pub_key.encrypt(message1, None)
(b&apos;\x03\x86t\xa0\x00\xc4\xea\xd2\x80\xed\xa7YN7\x07\xff\x88\xaa\x1eW\x0cmH0\x06\xa7\&apos;\xbc&amp;lt;w@q\x8b\xaf\xf7:g\x92{=\xe2E\xa5@\x1as2\xdd\xcb\x8e[\x98\x85\xdf,X\xecj.U\xd6\xa7W&amp;amp;u\&apos;Uz&quot;\x0f\x0e\\&amp;lt;\xa4\xfavC\x93\xa7\xbcO&quot;\xb9a\x06]&amp;lt;.\xc1\r1}*\xdf\xccdqXML\x93\x1b\xe9\xda\xdf\xab|\xf8\x18\xe4\x99\xbb\x7f\x18}\xd9\x9a\x1e*J\\\xca\x1a\xd1\x85\xf7t\x81\xd95{\x19\xc9\x81\xb6^}\x9c5\xca\xfe\xcf\xc8\xd8M\x9a\x8c-\xf1t\xee\xf9\x12\x90\x01\xca\x92~\x00c5qg5g\x95&amp;amp;\x10\xb1\x0b\x1fo\x95\xf2\xbc\x8d\xf3f&quot;@\xc5\x188\x0bX\x9cfo\xea\x97\x05@\xe5\xb2\xda\xb8\x97a\xa5w\xa8\x01\x9a\xa5N\xc4\x81\x8d\x0f&amp;lt;\x96iU\xd3\x95\xacJZs\xab_ #\xee\xf9\x0f\xf2\x12\xdb\xfc\xf8g\x18v\x02k+\xda\x16Si\xbf\xbb\xec\xf7w\x90\xde\xae\x97\t\xed{}5\xd0&apos;,)
&amp;gt;&amp;gt;&amp;gt; # 解密数据
&amp;gt;&amp;gt;&amp;gt; message3 = pri_key.decrypt(message2)
&apos;hello, world!&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;安全相关建议&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;虽然 Django 自带了稳固的安全保护措施，但是依然要采用正确的方式部署应用程序，利用 Web 服务器、操作系统和其他组件提供的安全保护措施。&lt;/li&gt;
&lt;li&gt;记得把 Python 代码放在 Web 服务器的文档根目录之外，避免代码意外泄露。&lt;/li&gt;
&lt;li&gt;谨慎处理用户上传的文件。&lt;/li&gt;
&lt;li&gt;Django本身没有对请求次数加以限制（包括验证用户身份的请求），为了防止暴力攻击和破解，可以考虑使用具有一次消费性的验证码或对这类请求的次数进行限制。&lt;/li&gt;
&lt;li&gt;将缓存系统、数据库服务器以及重要的资源服务器都放在第二级防火墙之后（不要放在DMZ）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;测试相关&lt;/h4&gt;
&lt;p&gt;测试是发现和标记缺陷的过程。所谓的缺陷是指实际结果和期望结果之间的任何差别。有的地方，测试也被认为是执行以找出错误为目的的程序的过程。 测试是为了让产品达到以下目标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;满足需求用户满意&lt;/li&gt;
&lt;li&gt;改善产品的市场占有率&lt;/li&gt;
&lt;li&gt;树立对产品的信任&lt;/li&gt;
&lt;li&gt;减少开发和维护的成本&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;功能测试&lt;/h5&gt;
&lt;p&gt;如果一个软件单元的行为方式与它的开发规范完全一样，那么该软件单元就通过了它的功能测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白盒测试：开发人员自己实现，最基本的形式是单元测试，还有集成测试和系统测试。&lt;/li&gt;
&lt;li&gt;黑盒测试：由开发团队之外的人执行，对测试代码没有可见性，将被测系统视为黑盒子。通常由测试人员或QA工程师来执行，Web应用可以通过Selenium这样的测试框架自动化实施。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;性能测试&lt;/h5&gt;
&lt;p&gt;软件在高工作负载下对其响应性和健壮性展开的测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;负载测试：在特定负载下执行的测试。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;压力测试：突发条件或极限条件下的性能测试。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;安全性测试&lt;/h5&gt;
&lt;p&gt;系统的敏感数据都是经过认证和授权之后才能访问。&lt;/p&gt;
&lt;h5&gt;其他测试&lt;/h5&gt;
&lt;p&gt;易用性测试 / 安装测试 / 可访问性测试&lt;/p&gt;
&lt;h5&gt;单元测试&lt;/h5&gt;
&lt;p&gt;测试函数和对象的方法（程序中最小最基本的单元）。通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试用例&lt;/li&gt;
&lt;li&gt;测试固件 - 每次测试时都要使用的东西。&lt;/li&gt;
&lt;li&gt;测试套件（测试集）- 组合了多个测试用例而构成的集合。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class UtilTest(unittest.TestCase):

    def setUp(self):
        self.pattern = re.compile(r&apos;\d{6}&apos;)

    def test_gen_mobile_code(self):
        for _ in range(100):
            self.assertIsNotNone(self.pattern.match(gen_mobile_code()))

    def test_to_md5_hex(self):
        md5_dict = {
            &apos;123456&apos;: &apos;e10adc3949ba59abbe56e057f20f883e&apos;,
            &apos;123123123&apos;: &apos;f5bb0c8de146c67b44babbf4e6584cc0&apos;,
            &apos;1qaz2wsx&apos;: &apos;1c63129ae9db9c60c3e8aa94d3e00495&apos;,
        }
        for key, value in md5_dict.items():
            self.assertEqual(value, to_md5_hex(key))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TestCase&lt;/code&gt;的断言方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;assertEqual / assertNotEqual&lt;/li&gt;
&lt;li&gt;assertTrue / assertFalse / assertIsNot&lt;/li&gt;
&lt;li&gt;assertRaise / assertRaiseRegexp&lt;/li&gt;
&lt;li&gt;assertAlmostEqual / assertNotAlmostEqual&lt;/li&gt;
&lt;li&gt;assertGreater / assertGreaterEqual / assertLess / assertLessEqual&lt;/li&gt;
&lt;li&gt;assertRegexpMatches / assertNotRegexpMatches&lt;/li&gt;
&lt;li&gt;assertListEqual / assertSetEqual / assertTupleEqual / assertDictEqual&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以使用nose2或pytest来辅助执行单元测试，同时通过cov-core或pytest-cov可以对测试覆度进行评估。覆盖率由百分比表示。比如测试代码执行过了程序的每一行，那么覆盖率就是100%。这种时候，几乎不会出现新程序上线后突然无法运行的尴尬情况。覆盖率不关心代码内容究竟是什么，覆盖率是用来检查“测试代码不足、测试存在疏漏”的一个指标，“测试内容是否妥当”并不归它管。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install nose2 pytest cov-core pytest-cov
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用Selenium来实现Web应用的自动化测试，它还可以用于屏幕抓取与浏览器行为模拟，通过爬虫抓取页面上的动态数据也可以使用它。Selenium其实包括三个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Selenium IDE：嵌入到浏览器的插件，可以录制和回放脚本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/7caefa11b7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Selenium WebDriver：支持多种语言可以操控浏览器的API。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Selenium Standalone Server：Selenium Grid、远程控制、分布式部署。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pip install selenium
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
import pytest
import contextlib


@pytest.fixture(scope=&apos;session&apos;)
def chrome():
    # 设置使用无头浏览器(不会打开浏览器窗口)
    options = webdriver.ChromeOptions()
    options.add_argument(&apos;--headless&apos;)
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


def test_baidu_index(chrome):
    chrome.get(&apos;https://www.baidu.com&apos;)
    assert chrome.title == &apos;百度一下，你就知道&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了Selenium之外，还有一个Web自动化测试工具名叫Robot Framework。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nose2 -v -C
pytest --cov
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Ran 7 tests in 0.002s

OK
Name                       Stmts   Miss  Cover
----------------------------------------------
example01.py                  15      0   100%
example02.py                  49     49     0%
example03.py                  22     22     0%
example04.py                  61     61     0%
example05.py                  29     29     0%
example06.py                  39     39     0%
example07.py                  19     19     0%
example08.py                  27     27     0%
example09.py                  18     18     0%
example10.py                  19     19     0%
example11.py                  22     22     0%
example12.py                  28     28     0%
example13.py                  28     28     0%
test_ddt_example.py           18      0   100%
test_pytest_example.py        11      6    45%
test_unittest_example.py      22      0   100%
----------------------------------------------
TOTAL                        427    367    14%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在测试过程中需要孤立各种外部依赖（数据库、外部接口调用、时间依赖），具体又包括两个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数据源：数据本地化 / 置于内存中 / 测试之后回滚&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;资源虚拟化：存根/桩（stub）、仿制/模拟（mock）、伪造（fake）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stub：测试期间为提供响应的函数生成的替代品&lt;/li&gt;
&lt;li&gt;mock：代替实际对象（以及该对象的API）的对象&lt;/li&gt;
&lt;li&gt;fake：没有达到生产级别的轻量级对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;集成测试&lt;/h5&gt;
&lt;p&gt;集成多个函数或方法的输入输出的测试，测试时需要将多个测试对象组合在一起。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试组件互操作性 / 需求变更测试 / 外部依赖和API / 调试硬件问题 / 在代码路径中发现异常&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;系统测试&lt;/h5&gt;
&lt;p&gt;对需求的测试，测试成品是否最终满足了所有需求，在客户验收项目时进行。&lt;/p&gt;
&lt;h5&gt;数据驱动测试&lt;/h5&gt;
&lt;p&gt;使用外部数据源实现对输入值与期望值的参数化，避免在测试中使用硬编码的数据。&lt;/p&gt;
&lt;p&gt;被测函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add(x, y):
    return x + y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;data.csv文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3,1,2
0,1,-1
100,50,50
100,1,99
15,7,8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import csv

from unittest import TestCase
from ddt import ddt, data, unpack


@ddt
class TestAdd(TestCase):

    def load_data_from_csv(filename):
        data_items = []
        with open(filename, &apos;r&apos;, newline=&apos;&apos;) as fs:
            reader = csv.reader(fs)
            for row in reader:
                data_items.append(list(map(int, row)))
        return data_items


    @data(*load_data_from_csv(&apos;data.csv&apos;))
    @unpack
    def test_add(self, result, param1, param2):
        self.assertEqual(result, add(param1, param2))
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Django中的测试&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;测试Django视图 - Django中提供的&lt;code&gt;TestCase&lt;/code&gt;扩展了&lt;code&gt;unittest&lt;/code&gt;中的&lt;code&gt;TestCase&lt;/code&gt;，绑定了一个名为&lt;code&gt;client&lt;/code&gt;的属性，可以用来模拟浏览器发出的GET、POST、DELETE、PUT等请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class SomeViewTest(TestCase):

    def test_example_view(self):
        resp = self.client.get(reverse(&apos;index&apos;))
        self.assertEqual(200, resp.status_code)
        self.assertEqual(5, resp.context[&apos;num&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行测试 - 配置测试数据库。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;DATABASES = {
    &apos;default&apos;: {
        &apos;ENGINE&apos;: &apos;django.db.backends.mysql&apos;,
        &apos;HOST&apos;: &apos;localhost&apos;,
        &apos;PORT&apos;: 3306,
        &apos;NAME&apos;: &apos;DbName&apos;,
        &apos;USER&apos;: os.environ[&apos;DB_USER&apos;],
        &apos;PASSWORD&apos;: os.environ[&apos;DB_PASS&apos;],
        &apos;TEST&apos;: {
            &apos;NAME&apos;: &apos;DbName_for_testing&apos;,
            &apos;CHARSET&apos;: &apos;utf8&apos;,
        },
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;python manage.py test
python manage.py test common
python manage.py test common.tests.UtilsTest
python manage.py test common.tests.UtilsTest.test_to_md5_hex
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;评估测试覆盖度&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pip install coverage
coverage run --source=&amp;lt;path1&amp;gt; --omit=&amp;lt;path2&amp;gt; manage.py test common
coverage report

Name                            Stmts   Miss  Cover
---------------------------------------------------
common/__init__.py                  0      0   100%
common/admin.py                     1      0   100%
common/apps.py                      3      3     0%
common/forms.py                    16     16     0%
common/helper.py                   32     32     0%
common/middlewares.py              19     19     0%
common/migrations/__init__.py       0      0   100%
common/models.py                   71      2    97%
common/serializers.py              14     14     0%
common/tests.py                    14      8    43%
common/urls_api.py                  3      3     0%
common/urls_user.py                 3      3     0%
common/utils.py                    22      7    68%
common/views.py                    69     69     0%
---------------------------------------------------
TOTAL                             267    176    34%
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;性能测试&lt;/h5&gt;
&lt;p&gt;问题1：性能测试的指标有哪些？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;ab（ Apache Benchmark） / webbench / httpperf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum -y install httpd
ab -c 10 -n 1000 http://www.baidu.com/
...
Benchmarking www.baidu.com (be patient).....done
Server Software:        BWS/1.1
Server Hostname:        www.baidu.com
Server Port:            80
Document Path:          /
Document Length:        118005 bytes
Concurrency Level:      10
Time taken for tests:   0.397 seconds
Complete requests:      100
Failed requests:        98
   (Connect: 0, Receive: 0, Length: 98, Exceptions: 0)
Write errors:           0
Total transferred:      11918306 bytes
HTML transferred:       11823480 bytes
Requests per second:    252.05 [#/sec] (mean)
Time per request:       39.675 [ms] (mean)
Time per request:       3.967 [ms] (mean, across all concurrent requests)
Transfer rate:          29335.93 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6    7   0.6      7       9
Processing:    20   27  22.7     24     250
Waiting:        8   11  21.7      9     226
Total:         26   34  22.8     32     258
Percentage of the requests served within a certain time (ms)
  50%     32
  66%     34
  75%     34
  80%     34
  90%     36
  95%     39
  98%     51
  99%    258
 100%    258 (longest request)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mysqlslap&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqlslap -a -c 100 -h 1.2.3.4 -u root -p
mysqlslap -a -c 100 --number-of-queries=1000 --auto-generate-sql-load-type=read -h &amp;lt;负载均衡服务器IP地址&amp;gt; -u root -p
mysqlslap -a --concurrency=50,100 --number-of-queries=1000 --debug-info --auto-generate-sql-load-type=mixed -h 1.2.3.4 -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sysbench&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sysbench --test=threads --num-threads=64 --thread-yields=100 --thread-locks=2 run
sysbench --test=memory --num-threads=512 --memory-block-size=256M --memory-total-size=32G run
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JMeter&lt;/p&gt;
&lt;p&gt;请查看&lt;a href=&quot;https://www.ibm.com/developerworks/cn/java/l-jmeter/index.html&quot;&gt;《使用JMeter进行性能测试》&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LoadRunner / QTP&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;项目调试&lt;/h4&gt;
&lt;p&gt;可以使用django-debug-toolbar来辅助项目调试。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-debug-toolbar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 - 修改settings.py。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    &apos;debug_toolbar&apos;,
]

MIDDLEWARE = [
    &apos;debug_toolbar.middleware.DebugToolbarMiddleware&apos;,
]

DEBUG_TOOLBAR_CONFIG = {
    # 引入jQuery库
    &apos;JQUERY_URL&apos;: &apos;https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js&apos;,
    # 工具栏是否折叠
    &apos;SHOW_COLLAPSED&apos;: True,
    # 是否显示工具栏
    &apos;SHOW_TOOLBAR_CALLBACK&apos;: lambda x: True,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 - 修改urls.py。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if settings.DEBUG:

    import debug_toolbar

    urlpatterns.insert(0, path(&apos;__debug__/&apos;, include(debug_toolbar.urls)))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 - 在页面右侧可以看到一个调试工具栏，上面包括了执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等调试信息，查看起来非常的方便。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;项目上线之前，请记住&lt;strong&gt;去掉django-debug-toolbar相关的所有配置&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;部署相关&lt;/h4&gt;
&lt;p&gt;请参考&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/98.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF%E5%92%8C%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98.md&quot;&gt;《项目部署上线和性能调优》&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;性能相关&lt;/h4&gt;
&lt;h5&gt;网站优化两大定律：&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;尽可能的使用缓存 - 牺牲空间换取时间（普适策略）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能推迟的都推迟 - 使用消息队列将并行任务串行来缓解服务器压力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器CPU利用率出现瞬时峰值 - 削峰（CPU利用率平缓的增长）&lt;/li&gt;
&lt;li&gt;上下游节点解耦合（下订单和受理订单的系统通常是分离的）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Django框架&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置缓存来缓解数据库的压力，并有合理的机制应对&lt;a href=&quot;https://www.cnblogs.com/zhangweizhong/p/6258797.html&quot;&gt;缓存穿透和缓存雪崩&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开启&lt;a href=&quot;https://docs.djangoproject.com/en/2.0/ref/templates/api/#django.template.loaders.cached.Loader&quot;&gt;模板缓存&lt;/a&gt;来加速模板的渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [
    {
        &apos;BACKEND&apos;: &apos;django.template.backends.django.DjangoTemplates&apos;,
        &apos;DIRS&apos;: [os.path.join(BASE_DIR, &apos;templates&apos;), ],
        # &apos;APP_DIRS&apos;: True,
        &apos;OPTIONS&apos;: {
            &apos;context_processors&apos;: [
                &apos;django.template.context_processors.debug&apos;,
                &apos;django.template.context_processors.request&apos;,
                &apos;django.contrib.auth.context_processors.auth&apos;,
                &apos;django.contrib.messages.context_processors.messages&apos;,
            ],
            &apos;loaders&apos;: [(
                &apos;django.template.loaders.cached.Loader&apos;, [
                    &apos;django.template.loaders.filesystem.Loader&apos;,
                    &apos;django.template.loaders.app_directories.Loader&apos;,
                ], ),
            ],
        },
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用惰性求值、迭代器、&lt;code&gt;defer()&lt;/code&gt;、&lt;code&gt;only()&lt;/code&gt;等缓解内存压力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用&lt;code&gt;select_related()&lt;/code&gt;和&lt;code&gt;prefetch_related()&lt;/code&gt;执行预加载避免“1+N查询问题”。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;数据库&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用ID生成器代替自增主键（性能更好、适用于分布式环境）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;自定义ID生成器 - snowflake&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UUID&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; my_uuid = uuid.uuid1()
&amp;gt;&amp;gt;&amp;gt; my_uuid
UUID(&apos;63f859d0-a03a-11e8-b0ad-60f81da8d840&apos;)
&amp;gt;&amp;gt;&amp;gt; my_uuid.hex
&apos;63f859d0a03a11e8b0ad60f81da8d840&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免不必要的外键列上的约束（除非必须保证参照完整性），更不要使用触发器之类的机制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用索引来优化查询性能（索引放在要用于查询的字段上）。InnoDB用的是BTREE索引，使用&amp;gt;、&amp;lt;、&amp;gt;=、&amp;lt;=、BETWEEN或者LIKE &apos;pattern&apos;（pattern不以通配符开头）时都可以用到索引。因为建立索引需要额外的磁盘空间，而主键上是有默认的索引，所以主键要尽可能选择较短的数据类型来减少磁盘占用，提高索引的缓存效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create index idx_goods_name on tb_goods (gname(10));
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 无法使用索引
select * from tb_goods where gname like &apos;%iPhone%&apos;;
-- 可以使用索引
select * from tb_goods where gname like &apos;iPhone%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 无法使用索引
Goods.objects.filter(name_icontains=&apos;iPhone&apos;)
# 可以使用索引
Goods.objects.filter(name__istartswith=&apos;iPhone&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用存储过程（存储在服务器端编译过的一组SQL语句）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop procedure if exists sp_avg_sal_by_dept;

create procedure sp_avg_sal_by_dept(deptno integer, out avg_sal float)
begin 
    select avg(sal) into avg_sal from TbEmp where dno=deptno;
end;

call sp_avg_sal_by_dept(10, @a);

select @a;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from django.db import connection
&amp;gt;&amp;gt;&amp;gt; cursor = connection.cursor()
&amp;gt;&amp;gt;&amp;gt; cursor.callproc(&apos;sp_avg_sal_by_dept&apos;, (10, 0))
&amp;gt;&amp;gt;&amp;gt; cursor.execute(&apos;select @_sp_avg_sal_by_dept_1&apos;)
&amp;gt;&amp;gt;&amp;gt; cursor.fetchone()
(2675.0,)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用数据分区。通过分区可以存储更多的数据、优化查询更大的吞吐量、可以快速删除过期的数据。关于这个知识点可以看看MySQL的&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/partitioning-overview.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RANGE分区：基于连续区间范围，把数据分配到不同的分区。&lt;/li&gt;
&lt;li&gt;LIST分区：基于枚举值的范围，把数据分配到不同的分区。&lt;/li&gt;
&lt;li&gt;HASH分区 / KEY分区：基于分区个数，把数据分配到不同的分区。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE tb_emp (
    eno INT NOT NULL,
    ename VARCHAR(20) NOT NULL,
    job VARCHAR(10) NOT NULL,
    hiredate DATE NOT NULL,
    dno INT NOT NULL
)
PARTITION BY HASH(dno)
PARTITIONS 4;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE tb_emp (
    eno INT NOT NULL,
    ename VARCHAR(20) NOT NULL,
    job VARCHAR(10) NOT NULL,
    hiredate DATE NOT NULL,
    dno INT NOT NULL
)
PARTITION BY RANGE( YEAR(hiredate) ) (
    PARTITION p0 VALUES LESS THAN (1960),
    PARTITION p1 VALUES LESS THAN (1970),
    PARTITION p2 VALUES LESS THAN (1980),
    PARTITION p3 VALUES LESS THAN (1990),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;explain&lt;/code&gt;来分析查询性能 - 执行计划。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explain select * from ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;explain&lt;/code&gt;结果解析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select_type：表示select操作的类型，常见的值有SIMPLE（简单查询，没有使用子查询或者表连接查询）、PRIMARY（主查询，外层的查询）、UNION（并集操作中的第二个或者后面的查询）、SUBQUERY（子查询中的第一个SELECT）等。&lt;/li&gt;
&lt;li&gt;table：输出结果的表。&lt;/li&gt;
&lt;li&gt;type：MySQL在表中找到所需行的方式，也称为访问类型，常见的值有：
&lt;ul&gt;
&lt;li&gt;ALL：全表扫描（遍历全表找到匹配的行）&lt;/li&gt;
&lt;li&gt;index：索引全扫描（遍历整个索引）&lt;/li&gt;
&lt;li&gt;range：索引范围扫描&lt;/li&gt;
&lt;li&gt;ref：非唯一索引扫描或唯一索引的前缀扫描&lt;/li&gt;
&lt;li&gt;eq_ref：唯一索引扫描&lt;/li&gt;
&lt;li&gt;const / system：表中最多有一行匹配&lt;/li&gt;
&lt;li&gt;NULL：不用访问表或者索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;possible_keys：查询时可能用到的索引。&lt;/li&gt;
&lt;li&gt;key：实际使用的索引。&lt;/li&gt;
&lt;li&gt;key_len：使用到索引字段的长度。&lt;/li&gt;
&lt;li&gt;rows：扫描行的数量。&lt;/li&gt;
&lt;li&gt;Extra：额外的信息（执行情况的说明或描述）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：关于MySQL更多的知识尤其是性能调优和运维方面的内容，推荐大家阅读网易出品的《深入浅出MySQL（第2版）》，网易出品必属精品。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用慢查询日志来发现性能低下的查询。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show variables like &apos;slow_query%&apos;;
+---------------------------+----------------------------------+
| Variable_name             | Value                            |
+---------------------------+----------------------------------+
| slow_query_log            | OFF                              |
| slow_query_log_file       | /mysql/data/localhost-slow.log   |
+---------------------------+----------------------------------+

mysql&amp;gt; show variables like &apos;long_query_time&apos;;
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; set global slow_query_log=&apos;ON&apos;; 
mysql&amp;gt; set global long_query_time=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
slow_query_log=ON
slow_query_log_file=/usr/local/mysql/data/slow.log
long_query_time=1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;96 软件测试和自动化测试&lt;/h2&gt;
&lt;h4&gt;软件测试概述&lt;/h4&gt;
&lt;p&gt;软件测试是一种用来促进鉴定软件的正确性、完整性、安全性和品质的过程，也就是在规定的条件下对程序进行操作以发现程序中的错误，衡量软件的品质并对其是否能满足设计要求进行评估的过程。&lt;/p&gt;
&lt;h5&gt;测试的方法&lt;/h5&gt;
&lt;p&gt;黑盒测试：测试应用程序的功能，而不是其内部结构或运作。测试者不需具备应用程序的代码、内部结构和编程语言的专门知识。测试者只需知道什么是系统应该做的事，即当键入一个特定的输入，可得到一定的输出。测试案例是依应用系统应该做的功能，照规范、规格或要求等设计。测试者选择有效输入和无效输入来验证是否正确的输出。此测试方法可适合大部分的软件测试，例如集成测试和系统测试。&lt;/p&gt;
&lt;p&gt;白盒测试：测试应用程序的内部结构或运作，而不是测试应用程序的功能（即黑箱测试）。在白箱测试时，以编程语言的角度来设计测试案例。测试者输入数据验证数据流在程序中的流动路径，并确定适当的输出，类似测试电路中的节点。&lt;/p&gt;
&lt;p&gt;由于时间和成本的约束，软件测试中一个最为关键的问题就是：“&lt;strong&gt;在所有可能的测试用例中，哪个子集能发现最多的错误？&lt;/strong&gt;”。所以在设计测试用例时，白盒测试看重程序逻辑覆盖的程度（语句覆盖、条件覆盖、分支覆盖），黑盒测试可以使用等价类划分、边界值分析、因果图分析、错误猜测等方法来设计测试用例。&lt;/p&gt;
&lt;h5&gt;测试的种类（阶段）&lt;/h5&gt;
&lt;p&gt;单元测试：对软件组成单元进行测试，其目的是检验软件基本组成单位的正确性，测试的对象是软件设计的最小单位 - 函数。&lt;/p&gt;
&lt;p&gt;集成测试：将程序模块采用适当的集成策略组装起来，对系统的接口及集成后的功能进行正确性检测的测试工作。其主要目的是检查软件单位之间的接口是否正确，集成测试的对象是已经经过单元测试的模块。&lt;/p&gt;
&lt;p&gt;系统测试：系统测试主要包括功能测试、界面测试、可靠性测试、易用性测试、性能测试。 &lt;/p&gt;
&lt;p&gt;回归测试：为了检测代码修改而引入的错误所进行的测试活动。回归测试是软件维护阶段的重要工作，有研究表明，回归测试带来的耗费占软件生命周期的1/3总费用以上。&lt;/p&gt;
&lt;h5&gt;测试驱动开发（敏捷测试）&lt;/h5&gt;
&lt;p&gt;测试驱动开发包括以下三个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为未实现的新功能或者改进编写自动化测试。&lt;/li&gt;
&lt;li&gt;提供通过所有定义的测试的最小代码量。&lt;/li&gt;
&lt;li&gt;重构代码以满足所需的质量标准。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;测试驱动开发的好处在于可以有效的防止软件回归以及提供更有质量的代码。还有就是验收测试应该由客户来进行，客户通过对使用场景来设计验收测试，对应用程序是否满足他们的要求进行客观、公正的确认。能够通过单元测试、甚至是系统测试的功能未必能够通过客户的验收测试。&lt;/p&gt;
&lt;h5&gt;互联网应用和移动应用的测试&lt;/h5&gt;
&lt;p&gt;互联网应用的测试策略：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;表示层测试（内容测试、站点结构测试、用户环境（浏览器、操作系统等））&lt;/li&gt;
&lt;li&gt;业务层测试（性能、数据验证、事务、外部服务）&lt;/li&gt;
&lt;li&gt;持久层测试（响应时间、数据完整性、容错性）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;移动应用的测试策略：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;真机测试&lt;/li&gt;
&lt;li&gt;基于模拟器的测试&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;单元（模块）测试&lt;/h4&gt;
&lt;p&gt;Python的标准库里有为编写单元测试而准备的unittest模块，执行测试时建议使用&lt;a href=&quot;https://docs.pytest.org/en/latest/&quot;&gt;pytest&lt;/a&gt;或nose2。pytest是一款能够自动搜索并执行测试的测试执行工具，并且会输出详细的错误报告。关于单元测试可以看看&lt;a href=&quot;https://blog.csdn.net/huilan_same/article/details/52944782&quot;&gt;《Python必会的单元测试框架 - unittest》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;可以安装&lt;a href=&quot;https://pypi.org/project/testfixtures/&quot;&gt;testfixtures&lt;/a&gt;库来辅助单元测试，它整合了多种典型配置器，提供了生成目录、更改系统日期、生成mock对象的功能模块，这些模块能够帮助我们将单元测试与单元测试所依赖的环境分离开。&lt;a href=&quot;https://pypi.org/project/mock/&quot;&gt;mock&lt;/a&gt; 是将测试对象所依赖的对象替换为虚拟对象的库，在测试的时候，我们可以为虚拟对象指定其在被调用时的返回值以及是否发生异常等。&lt;/p&gt;
&lt;p&gt;tox能便捷地为我们准备好执行测试所需的环境。tox会在多个virtualenv环境中搭建测试 环境，然后在这些环境中执行测试并显示结果。它能够把测试工具的选项及环境变量等内容统 一起来，所以我们只需执行tox命令即能轻松完成所需的测试。&lt;/p&gt;
&lt;h4&gt;自动化测试&lt;/h4&gt;
&lt;h5&gt;UI自动化测试&lt;/h5&gt;
&lt;h6&gt;桌面端 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/pyautogui.readthedocs.io/en/latest/%3E&quot;&gt;PyAutoGui&lt;/a&gt;&lt;/h6&gt;
&lt;h6&gt;移动端 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttp:/appium.io/%3E&quot;&gt;Appnium&lt;/a&gt;&lt;/h6&gt;
&lt;h6&gt;Web端 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/docs.seleniumhq.org/%3E&quot;&gt;Selenium&lt;/a&gt;&lt;/h6&gt;
&lt;p&gt;Selenium是实现Web应用程序的功能测试以及集成测试自动化的浏览器驱动测试工具群。和使用浏览器的用户相同，Selenium可以在浏览器进行的鼠标操作、在表单中输入文字、验证表单的值等，利用这一点就可以将手动操作变成自动化操作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Selenium优点&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;自动化测试用例制作简单。Selenium提供了Selenium IDE工具，该工具可以捕获鼠标、键盘的操作，然后通过重放功能来重复这些操作，这样就可以简单的制作测试用例。&lt;/li&gt;
&lt;li&gt;支持多种浏览器和操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Selenium的组件&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.seleniumhq.org/projects/ide/&quot;&gt;Selenium IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.seleniumhq.org/projects/remote-control/&quot;&gt;Selenium Remote Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.seleniumhq.org/projects/webdriver/&quot;&gt;Selenium WebDriver&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;与持续集成工具协作&lt;/p&gt;
&lt;p&gt;持续集成指的是频繁的将代码集成到主干。它的好处主要有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速发现错误。每完成一点更新，就集成到主干，可以快速发现错误，定位错误也比较容易。&lt;/li&gt;
&lt;li&gt;防止分支大幅偏离主干。如果不是经常集成，主干又在不断更新，会导致以后集成的难度变大，甚至难以集成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;持续集成的目的，就是让产品可以快速迭代，同时还能保持高质量。它的核心措施是代码集成到主干之前，必须通过自动化测试，只要有一个测试用例失败，就不能集成。编程大师Martin Fowler曾经说过：“持续集成并不能消除Bug，而是让它们非常容易发现和改正。”&lt;/p&gt;
&lt;p&gt;可以在Jenkins中安装“Seleniumhq Plugin”插件，这样就可以将Selenium IDE制作的测试用例保存为HTML格式并提供给Jenkins来使用，基本步骤是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在执行测试的机器上，从版本控制系统中下载测试套件和测试用例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在执行测试的机器上下载Selenium Server。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从Jenkins的“系统管理”中选择“插件管理”来安装“Seleniumhq Plugin”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在Jenkins的“系统管理”中选择“系统设置”并配置“Selenium Remote Control”下的“HTMLSuite Runner”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新建测试用的Jenkins任务并进行配置，配置的内容包括：浏览器、起始URL、测试套件和测试结果输出文件。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置完成后，就可以执行Jenkins的“立即构建”了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了Selenium之外，&lt;a href=&quot;https://pypi.org/project/WebTest/&quot;&gt;WebTest&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/splinter.readthedocs.io/en/latest/%3E&quot;&gt;Splinter&lt;/a&gt;和&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/robotframework.org/%3E&quot;&gt;RobotFramework&lt;/a&gt;也是Web端测试的选择，其中WebTest可以对WSGI应用执行模拟请求并获取结果，基本上所有WSGI应用的测试都可以用它；Splinter是对Selenium的二次封装，使用上更加方便简单。&lt;/p&gt;
&lt;h5&gt;接口测试自动化测试&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/cn.python-requests.org/zh_CN/latest/%3E&quot;&gt;requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/docs.httprunner.org/%3E&quot;&gt;HttpRunner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/github.com/svanoort/pyresttest%3E&quot;&gt;PyRestTest&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;其他方面的自动化测试&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.locust.io/%3E&quot;&gt;Locust&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/github.com/m4n3dw0lf/PytheM%3E&quot;&gt;pythem&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;测试相关工具&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;PostMan&lt;/li&gt;
&lt;li&gt;AB&lt;/li&gt;
&lt;li&gt;JMeter&lt;/li&gt;
&lt;li&gt;LoadRunner&lt;/li&gt;
&lt;li&gt;Benchmark Factory&lt;/li&gt;
&lt;li&gt;WAS&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;97 电商网站技术要点剖析&lt;/h2&gt;
&lt;h4&gt;商业模式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;B2B - 商家对商家，交易双方都是企业（商家），最典型的案例就是阿里巴巴。&lt;/li&gt;
&lt;li&gt;C2C - 个人对个人，例如：淘宝、人人车。&lt;/li&gt;
&lt;li&gt;B2C - 商家对个人，例如：唯品会，聚美优品。&lt;/li&gt;
&lt;li&gt;C2B - 个人对商家，先有消费者提出需求，后有商家按需求组织生产，例如： 尚品宅配。&lt;/li&gt;
&lt;li&gt;O2O - 线上到线下，将线下的商务机会与互联网结合，让互联网成为线下交易的平台，例如：美团外卖、饿了么。&lt;/li&gt;
&lt;li&gt;B2B2C - 商家对商家对个人，例如：天猫、京东。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;需求要点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用户端
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首页（商品分类、广告轮播、滚动快讯、瀑布加载、推荐、折扣、热销、……）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户（登录（第三方登录）、注册、注销、自服务（个人信息、浏览历史、收货地址、……））&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;商品（分类、列表、详情、搜索、热门搜索、搜索历史、添加到购物车、收藏、关注、评论、……）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;购物车（查看、编辑（修改数量、删除商品、清空））&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;订单（提交订单（支付）、历史订单、订单详情、订单评价、……）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;管理端
&lt;ul&gt;
&lt;li&gt;核心业务实体的CRUD&lt;/li&gt;
&lt;li&gt;定时任务（周期性和非周期性，如处理未支付订单、采集数据对异常事件报警、……）&lt;/li&gt;
&lt;li&gt;报表功能（导入导出Excel、PDF等以及前端ECharts统计图表展示）&lt;/li&gt;
&lt;li&gt;权限控制（RBAC、白名单、黑名单、……）&lt;/li&gt;
&lt;li&gt;业务流转（如发起退款流程，常用流程引擎有：Activity、Airflow、Spiff等）&lt;/li&gt;
&lt;li&gt;三方服务（接入地图、短信、物流、支付、实名认证、天气、监控、云存储、……）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;物理模型设计&lt;/h4&gt;
&lt;p&gt;首先要搞清楚两个概念：SPU（Standard Product Unit）和SKU（Stock Keeping Unit）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPU：iPhone 6s&lt;/li&gt;
&lt;li&gt;SKU：iPhone 6s 64G 土豪金&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/c53b202a10.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;第三方登录&lt;/h4&gt;
&lt;p&gt;第三方登录是指利用第三方网站（通常是知名社交网站）的账号进行登录验证（主要是通过知名第三方网站获取到用户相关信息），比如国内的 QQ、微博，国外的Google、Facebook等。第三方登录大部分都是使用&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/en.wikipedia.org/wiki/OAuth%3E&quot;&gt;OAuth&lt;/a&gt;协议，它是一个关于授权的开放网络标准（&lt;strong&gt;数据的所有者告诉系统，同意授权第三方应用进入系统，获取这些数据。系统从而产生一个短期的进入令牌，用来代替密码，供第三方应用使用&lt;/strong&gt;），得到了广泛的应用，目前通常使用的是2.0版本。关于OAuth的基础知识，可以阅读阮一峰老师的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html&quot;&gt;《理解OAuth 2.0》&lt;/a&gt;。关于&lt;strong&gt;令牌&lt;/strong&gt;和&lt;strong&gt;密码&lt;/strong&gt;的区别，我们可以简单总结出以下三点差异：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;令牌是短期的，到期会自动失效，用户自己无法修改。密码一般长期有效，用户不修改，就不会发生变化。&lt;/li&gt;
&lt;li&gt;令牌可以被数据所有者撤销，会立即失效。以上例而言，屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。&lt;/li&gt;
&lt;li&gt;令牌有权限范围（scope），比如只能进小区的二号门。对于网络服务来说，只读令牌就比读写令牌更安全。密码一般是完整权限。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，通过令牌既可以让第三方应用获得权限，同时又随时可控，不会危及系统安全。这就是OAuth协议的优势。&lt;/p&gt;
&lt;h5&gt;OAuth 2.0授权流程&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;用户打开客户端以后，客户端要求用户（资源所有者）给予授权。&lt;/li&gt;
&lt;li&gt;用户（资源所有者）同意给予客户端授权。&lt;/li&gt;
&lt;li&gt;客户端使用上一步获得的授权，向认证服务器申请访问令牌。&lt;/li&gt;
&lt;li&gt;认证服务器对客户端进行认证以后，发放访问令牌。&lt;/li&gt;
&lt;li&gt;客户端使用访问令牌向资源服务器申请获取资源。&lt;/li&gt;
&lt;li&gt;资源服务器确认访问令牌无误，同意向客户端开放资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/f0c19268dd.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果使用微博登录进行接入，其具体步骤可以参考微博开放平台上的&lt;a href=&quot;http://open.weibo.com/wiki/Connect/login&quot;&gt;“微博登录接入”&lt;/a&gt;文档。使用QQ登录进行接入，需要首先注册成为QQ互联开发者并通过审核，具体的步骤可以参考QQ互联上的&lt;a href=&quot;http://wiki.connect.qq.com/&quot;&gt;“接入指南”&lt;/a&gt;，具体的步骤可以参考&lt;a href=&quot;http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0&quot;&gt;“网站开发流程”&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：在Gitbook上面有一本名为&lt;a href=&quot;https://shenxgan.gitbooks.io/django/content/publish/2015-08-10-django-oauth-login.html&quot;&gt;《Django博客入门》&lt;/a&gt;的书以Github为例介绍了第三方账号登录，有兴趣的可以自行阅读。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通常电商网站在使用第三方登录时，会要求与网站账号进行绑定或者根据获取到的第三方账号信息（如：手机号）自动完成账号绑定。&lt;/p&gt;
&lt;h4&gt;缓存预热和查询缓存&lt;/h4&gt;
&lt;h5&gt;缓存预热&lt;/h5&gt;
&lt;p&gt;所谓缓存预热，是指在启动服务器时将数据提前加载到缓存中，为此可以在Django应用的&lt;code&gt;apps.py&lt;/code&gt;模块中编写&lt;code&gt;AppConfig&lt;/code&gt;的子类并重写&lt;code&gt;ready()&lt;/code&gt;方法，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

from django.apps import AppConfig
from django.core.cache import cache

SELECT_PROVINCE_SQL = &apos;select distid, name from tb_district where pid is null&apos;


class CommonConfig(AppConfig):
    name = &apos;common&apos;

    def ready(self):
        conn = pymysql.connect(host=&apos;1.2.3.4&apos;, port=3306,
                               user=&apos;root&apos;, password=&apos;pass&apos;,
                               database=&apos;db&apos;, charset=&apos;utf8&apos;,
                               cursorclass=pymysql.cursors.DictCursor)
        try:
            with conn.cursor() as cursor:
                cursor.execute(SELECT_PROVINCE_SQL)
                provinces = cursor.fetchall()
                cache.set(&apos;provinces&apos;, provinces)
        finally:
            conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，还需要在应用的&lt;code&gt;__init__.py&lt;/code&gt;中编写下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;default_app_config = &apos;common.apps.CommonConfig&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在项目的&lt;code&gt;settings.py&lt;/code&gt;文件中注册应用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    ...
    &apos;common.apps.CommonConfig&apos;,
    ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;查询缓存&lt;/h5&gt;
&lt;p&gt;自定义装饰器实现查询结果的缓存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pickle import dumps, loads

from django.core.cache import caches

MODEL_CACHE_KEY = &apos;project:modelcache:%s&apos;


def my_model_cache(key, section=&apos;default&apos;, timeout=None):
    &quot;&quot;&quot;实现模型缓存的装饰器&quot;&quot;&quot;

    def wrapper1(func):

        def wrapper2(*args, **kwargs):
            real_key = &apos;%s:%s&apos; % (MODEL_CACHE_KEY % key, &apos;:&apos;.join(map(str, args)))
            serialized_data = caches[section].get(real_key)
            if serialized_data:
                data = loads(serialized_data)
            else:
                data = func(*args, **kwargs)
                cache.set(real_key, dumps(data), timeout=timeout)
            return data

        return wrapper2

    return wrapper1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@my_model_cache(key=&apos;provinces&apos;)
def get_all_provinces():
    return list(Province.objects.all())
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;购物车实现&lt;/h4&gt;
&lt;p&gt;问题一：已登录用户的购物车放在哪里？未登录用户的购物车放在哪里？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CartItem(object):
    &quot;&quot;&quot;购物车中的商品项&quot;&quot;&quot;

    def __init__(self, sku, amount=1, selected=False):
        self.sku = sku
        self.amount = amount
        self.selected = selected

    @property
    def total(self):
        return self.sku.price * self.amount


class ShoppingCart(object):
    &quot;&quot;&quot;购物车&quot;&quot;&quot;

    def __init__(self):
        self.items = {}
        self.index = 0

    def add_item(self, item):
        if item.sku.id in self.items:
            self.items[item.sku.id].amount += item.amount
        else:
            self.items[item.sku.id] = item

    def remove_item(self, sku_id):
        if sku_id in self.items:
            self.items.remove(sku_id)

    def clear_all_items(self):
        self.items.clear()

    @property
    def cart_items(self):
        return self.items.values()

    @property
    def cart_total(self):
        total = 0
        for item in self.items.values():
            total += item.total
        return total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已登录用户的购物车可以放在数据库中（可以先在Redis中缓存）；未登录用户的购物车可以保存在Cookie、localStorage或sessionStorage中（减少服务器端内存开销）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &apos;1001&apos;: {sku: {...}, &apos;amount&apos;: 1, &apos;selected&apos;: True}, 
    &apos;1002&apos;: {sku: {...}, &apos;amount&apos;: 2, &apos;selected&apos;: False},
    &apos;1003&apos;: {sku: {...}, &apos;amount&apos;: 3, &apos;selected&apos;: True},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;request.get_signed_cookie(&apos;cart&apos;)

cart_base64 = base64.base64encode(pickle.dumps(cart))
response.set_signed_cookie(&apos;cart&apos;, cart_base64)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题二：用户登录之后，如何合并购物车？（目前电商应用的购物车几乎都做了持久化处理，主要是方便在多个终端之间共享数据）&lt;/p&gt;
&lt;h4&gt;集成支付功能&lt;/h4&gt;
&lt;p&gt;问题一：支付信息如何持久化？（必须保证每笔交易都有记录）&lt;/p&gt;
&lt;p&gt;问题二：如何接入支付宝？（接入其他平台基本类似）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://open.alipay.com/platform/home.htm&quot;&gt;蚂蚁金服开放平台&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://open.alipay.com/platform/homeRoleSelection.htm&quot;&gt;入驻平台&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openhome.alipay.com/platform/appManage.htm#/apps&quot;&gt;开发者中心&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.open.alipay.com/270/105899/&quot;&gt;文档中心&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.open.alipay.com/54/103419&quot;&gt;SDK集成&lt;/a&gt; - &lt;a href=&quot;https://pypi.org/project/alipay-sdk-python/&quot;&gt;PYPI链接&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.open.alipay.com/270/105900/&quot;&gt;API列表&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/0d47e34ab5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALIPAY_APPID = &apos;......&apos;
ALIPAY_URL = &apos;https://openapi.alipaydev.com/gateway.do&apos;
ALIPAY_DEBUG = False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获得支付链接（发起支付）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建调用支付宝的对象
alipay = AliPay(
    # 在线创建应用时分配的ID
    appid=settings.ALIPAY_APPID,
    app_notify_url=None,
    # 自己应用的私钥
    app_private_key_path=os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 
        &apos;keys/app_private_key.pem&apos;),
    # 支付宝的公钥
    alipay_public_key_path=os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 
        &apos;keys/alipay_public_key.pem&apos;),
    sign_type=&apos;RSA2&apos;,
    debug=settings.ALIPAY_DEBUG
)
# 调用获取支付页面操作
order_info = alipay.api_alipay_trade_page_pay(
    out_trade_no=&apos;...&apos;,
    total_amount=&apos;...&apos;,
    subject=&apos;...&apos;,
    return_url=&apos;http://...&apos;
)
# 生成完整的支付页面URL
alipay_url = settings.ALIPAY_URL + &apos;?&apos; + order_info
return JsonResponse({&apos;alipay_url&apos;: alipay_url})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面返回的链接可以进入支付页面，支付完成后会自动跳转回上面代码中设定好的项目页面，在该页面中可以获得订单号（out_trade_no）、支付流水号（trade_no）、交易金额（total_amount）和对应的签名（sign）并请求后端验证和保存交易结果，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建调用支付宝的对象
alipay = AliPay(
    # 在线创建应用时分配的ID
    appid=settings.ALIPAY_APPID,
    app_notify_url=None,
    # 自己应用的私钥
    app_private_key_path=os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 
        &apos;keys/app_private_key.pem&apos;),
    # 支付宝的公钥
    alipay_public_key_path=os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 
        &apos;keys/alipay_public_key.pem&apos;),
    sign_type=&apos;RSA2&apos;,
    debug=settings.ALIPAY_DEBUG
)
# 请求参数（假设是POST请求）中包括订单号、支付流水号、交易金额和签名
params = request.POST.dict()
# 调用验证操作
if alipay.verify(params, params.pop(&apos;sign&apos;)):
    # 对交易进行持久化操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;支付宝的支付API还提供了交易查询、交易结算、退款、退款查询等一系列的接口，可以根据业务需要进行调用，此处不再进行赘述。&lt;/p&gt;
&lt;h4&gt;秒杀和超卖&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;秒杀：秒杀是通常意味着要在很短的时间处理极高的并发，系统在短时间需要承受平时百倍以上的流量，因此秒杀架构是一个比较复杂的问题，其核心思路是流量控制和性能优化，需要从前端（通过JavaScript实现倒计时、避免重复提交和限制频繁刷新）到后台各个环节的配合。流量控制主要是限制只有少部分流量进入服务后端（毕竟最终只有少部分用户能够秒杀成功），同时在物理架构上使用缓存（一方面是因为读操作多写操作少；另外可以将库存放在Redis中，利用DECR原语实现减库存；同时也可以利用Redis来进行限流，道理跟限制频繁发送手机验证码是一样的）和消息队列（消息队列最为重要的作用就是“削峰”和“上下游节点解耦合”）来进行优化；此外还要采用无状态服务设计，这样才便于进行水平扩展（通过增加设备来为系统扩容）。&lt;/li&gt;
&lt;li&gt;超卖现象：比如某商品的库存为1，此时用户1和用户2并发购买该商品，用户1提交订单后该商品的库存被修改为0，而此时用户2并不知道的情况下提交订单，该商品的库存再次被修改为-1这就是超卖现象。解决超卖现象有三种常见的思路：
&lt;ul&gt;
&lt;li&gt;悲观锁控制：查询商品数量的时候就用&lt;code&gt;select ... for update&lt;/code&gt;对数据加锁，这样的话用户1查询库存时，用户2因无法读取库存数量被阻塞，直到用户1提交或者回滚了更新库存的操作后才能继续，从而解决了超卖问题。但是这种做法对并发访问量很高的商品来说性能太过糟糕，实际开发中可以在库存小于某个值时才考虑加锁，但是总的来说这种做法不太可取。&lt;/li&gt;
&lt;li&gt;乐观锁控制：查询商品数量不用加锁，更新库存的时候设定商品数量必须与之前查询数量相同才能更新，否则说明其他事务已经更新了库存，必须重新发出请求。&lt;/li&gt;
&lt;li&gt;尝试减库存：将上面的查询（&lt;code&gt;select&lt;/code&gt;）和更新（&lt;code&gt;update&lt;/code&gt;）操作合并为一条SQL操作，更新库存的时候，在&lt;code&gt;where&lt;/code&gt;筛选条件中加上&lt;code&gt;库存&amp;gt;=购买数量&lt;/code&gt;或&lt;code&gt;库存-购买数量&amp;gt;=0&lt;/code&gt;的条件，这种做法要求事务隔离级别为读提交（read committed）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：有兴趣的可以自己在知乎上看看关于这类问题的讨论。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;静态资源管理&lt;/h4&gt;
&lt;p&gt;静态资源的管理可以自己架设文件服务器或者分布式文件服务器（FastDFS），但是一般的项目中没有必要这样做而且效果未必是最好的，我们建议使用云存储服务来管理网站的静态资源，国内外的云服务提供商如&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/amazonaws-china.com/cn/%3E&quot;&gt;亚马逊&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.aliyun.com/product/oss%3E&quot;&gt;阿里云&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.qiniu.com/products/kodo%3E&quot;&gt;七牛&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/leancloud.cn/storage/%3E&quot;&gt;LeanCloud&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/%3Chttps:/www.bmob.cn/cloud%3E&quot;&gt;Bmob&lt;/a&gt;等都提供了非常优质的云存储服务，而且价格也是一般公司可以接受的，具体的操作可以参考官方文档，例如：阿里云的&lt;a href=&quot;https://www.alibabacloud.com/zh/support/developer-resources&quot;&gt;对象存储 OSS开发人员指南&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;全文检索&lt;/h4&gt;
&lt;h5&gt;方案选择&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;使用数据库的模糊查询功能 - 效率低，每次需要全表扫描，不支持分词。&lt;/li&gt;
&lt;li&gt;使用数据库的全文检索功能 - MySQL 5.6以前只适用于MyISAM引擎，检索操作和其他的DML操作耦合在数据库中，可能导致检索操作非常缓慢，数据量达到百万级性能显著下降，查询时间很长。&lt;/li&gt;
&lt;li&gt;使用开源搜索引擎 - 索引数据和原始数据分离，可以使用ElasticSearch或Solr来提供外置索引服务，如果不考虑高并发的全文检索需求，纯Python的Whoosh也可以考虑。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;ElasticSearch&lt;/h5&gt;
&lt;p&gt;ElasticSearch既是一个分布式文档数据库又是一个高可扩展的开源全文搜索和分析引擎，它允许存储、搜索和分析大量的数据，并且这个过程是近实时的。它通常被用作底层引擎和技术，为复杂的搜索功能和要求提供动力，大家熟知的维基百科、Stack-Overflow、Github都使用了ElasticSearch。&lt;/p&gt;
&lt;p&gt;ElasticSearch的底层是开源搜索引擎&lt;a href=&quot;https://lucene.apache.org/&quot;&gt;Lucene&lt;/a&gt;，但是直接用Lucene会非常麻烦，必须自己编写代码去调用它的接口而且只支持Java语言。ElasticSearch相当于对Lucene进行了一次全面的封装，提供了REST风格的API接口，通过基于HTTP协议的访问方式屏蔽了编程语言的差异。ElasticSearch会为数据构建&lt;a href=&quot;https://zh.wikipedia.org/zh-hans/%E5%80%92%E6%8E%92%E7%B4%A2%E5%BC%95&quot;&gt;倒排索引&lt;/a&gt;，但是ElasticSearch内置的分词器对中文分词的支持几乎为零，因此需要通过安装elasticsearch-analysis-ik插件来提供中文分词服务。&lt;/p&gt;
&lt;p&gt;ElasticSearch的安装和配置可以参考&lt;a href=&quot;https://blog.csdn.net/jinyidong/article/details/80475320&quot;&gt;《ElasticSearch之Docker安装》&lt;/a&gt;。除了ElasticSearch之外，也可以使用Solr、Whoosh等来提供搜索引擎服务，基本上Django项目中可以考虑如下几种方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;haystack（django-haystack / drf-haystack） + whoosh + Jieba&lt;/li&gt;
&lt;li&gt;haystack （django-haystack / drf-haystack）+ elasticsearch&lt;/li&gt;
&lt;li&gt;requests + elasticsearch&lt;/li&gt;
&lt;li&gt;django-elasticsearch-dsl&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;####安装和使用ElasticSearch&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用Docker安装ElasticSearch。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull elasticsearch:7.6.0
docker run -d -p 9200:9200 -p 9300:9300 -e &quot;discovery.type=single-node&quot; -e ES_JAVA_OPTS=&quot;-Xms512m -Xmx512m&quot; --name es elasticsearch:7.6.0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面创建容器时通过&lt;code&gt;-e&lt;/code&gt;参数指定了使用单机模式和Java虚拟机最小最大可用堆空间的大小，堆空间大小可以根据服务器实际能够提供给ElasticSearch的内存大小来决定，默认为2G。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据库。&lt;/p&gt;
&lt;p&gt;请求：PUT - &lt;code&gt;http://1.2.3.4:9200/demo/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
   &quot;acknowledged&quot;: true,
   &quot;shards_acknowledged&quot;: true,
   &quot;index&quot;: &quot;demo&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看创建的数据库。&lt;/p&gt;
&lt;p&gt;请求：GET - &lt;code&gt;http://1.2.3.4:9200/demo/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;demo&quot;: {
        &quot;aliases&quot;: {},
        &quot;mappings&quot;: {},
        &quot;settings&quot;: {
            &quot;index&quot;: {
                &quot;creation_date&quot;: &quot;1552213970199&quot;,
                &quot;number_of_shards&quot;: &quot;5&quot;,
                &quot;number_of_replicas&quot;: &quot;1&quot;,
                &quot;uuid&quot;: &quot;ny3rCn10SAmCsqW6xPP1gw&quot;,
                &quot;version&quot;: {
                    &quot;created&quot;: &quot;6050399&quot;
                },
                &quot;provided_name&quot;: &quot;demo&quot;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;插入数据。&lt;/p&gt;
&lt;p&gt;请求：POST - &lt;code&gt;http://1.2.3.4:9200/demo/goods/1/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;请求头：Content-Type: application/json&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;no&quot;: &quot;5089253&quot;,
    &quot;title&quot;: &quot;Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机&quot;,
    &quot;brand&quot;: &quot;Apple&quot;,
    &quot;name&quot;: &quot;Apple iPhone X&quot;,
    &quot;product&quot;: &quot;中国大陆&quot;,
    &quot;resolution&quot;: &quot;2436 x 1125&quot;,
    &quot;intro&quot;: &quot;一直以来，Apple都心存一个设想，期待能够打造出这样一部iPhone：它有整面屏幕，能让你在使用时，完全沉浸其中，仿佛忘了它的存在。它是如此智能，哪怕轻轻一瞥，都能得到它心有灵犀的回应。而这个设想，终于随着iPhone X的到来成为了现实。现在，就跟未来见个面吧。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;_index&quot;: &quot;demo&quot;,
    &quot;_type&quot;: &quot;goods&quot;,
    &quot;_id&quot;: &quot;1&quot;,
    &quot;_version&quot;: 4,
    &quot;result&quot;: &quot;created&quot;,
    &quot;_shards&quot;: {
        &quot;total&quot;: 2,
        &quot;successful&quot;: 1,
        &quot;failed&quot;: 0
    },
    &quot;_seq_no&quot;: 3,
    &quot;_primary_term&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除数据。&lt;/p&gt;
&lt;p&gt;请求：DELETE -  &lt;code&gt;http://1.2.3.4:9200/demo/goods/1/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;_index&quot;: &quot;demo&quot;,
    &quot;_type&quot;: &quot;goods&quot;,
    &quot;_id&quot;: &quot;1&quot;,
    &quot;_version&quot;: 2,
    &quot;result&quot;: &quot;deleted&quot;,
    &quot;_shards&quot;: {
        &quot;total&quot;: 2,
        &quot;successful&quot;: 1,
        &quot;failed&quot;: 0
    },
    &quot;_seq_no&quot;: 1,
    &quot;_primary_term&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新数据。&lt;/p&gt;
&lt;p&gt;请求：PUT - &lt;code&gt;http://1.2.3.4:9200/demo/goods/1/_update&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;请求头：Content-Type: application/json&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;doc&quot;: {
		&quot;no&quot;: &quot;5089253&quot;,
    	&quot;title&quot;: &quot;Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机&quot;,
    	&quot;brand&quot;: &quot;Apple(苹果)&quot;,
    	&quot;name&quot;: &quot;Apple iPhone X&quot;,
    	&quot;product&quot;: &quot;美国&quot;,
    	&quot;resolution&quot;: &quot;2436 x 1125&quot;,
    	&quot;intro&quot;: &quot;一直以来，Apple都心存一个设想，期待能够打造出这样一部iPhone：它有整面屏幕，能让你在使用时，完全沉浸其中，仿佛忘了它的存在。它是如此智能，哪怕轻轻一瞥，都能得到它心有灵犀的回应。而这个设想，终于随着iPhone X的到来成为了现实。现在，就跟未来见个面吧。&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;_index&quot;: &quot;demo&quot;,
    &quot;_type&quot;: &quot;goods&quot;,
    &quot;_id&quot;: &quot;1&quot;,
    &quot;_version&quot;: 10,
    &quot;result&quot;: &quot;updated&quot;,
    &quot;_shards&quot;: {
        &quot;total&quot;: 2,
        &quot;successful&quot;: 1,
        &quot;failed&quot;: 0
    },
    &quot;_seq_no&quot;: 9,
    &quot;_primary_term&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询数据。&lt;/p&gt;
&lt;p&gt;请求：GET - &lt;code&gt;http://1.2.3.4:9200/demo/goods/1/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;_index&quot;: &quot;demo&quot;,
    &quot;_type&quot;: &quot;goods&quot;,
    &quot;_id&quot;: &quot;1&quot;,
    &quot;_version&quot;: 10,
    &quot;found&quot;: true,
    &quot;_source&quot;: {
        &quot;doc&quot;: {
            &quot;no&quot;: &quot;5089253&quot;,
            &quot;title&quot;: &quot;Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机&quot;,
            &quot;brand&quot;: &quot;Apple(苹果)&quot;,
            &quot;name&quot;: &quot;Apple iPhone X&quot;,
            &quot;product&quot;: &quot;美国&quot;,
            &quot;resolution&quot;: &quot;2436 x 1125&quot;,
            &quot;intro&quot;: &quot;一直以来，Apple都心存一个设想，期待能够打造出这样一部iPhone：它有整面屏幕，能让你在使用时，完全沉浸其中，仿佛忘了它的存在。它是如此智能，哪怕轻轻一瞥，都能得到它心有灵犀的回应。而这个设想，终于随着iPhone X的到来成为了现实。现在，就跟未来见个面吧。&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;配置中文分词和拼音插件&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入Docker容器的plugins目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it es /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载和ElasticSearch版本对应的&lt;a href=&quot;https://github.com/medcl/elasticsearch-analysis-ik&quot;&gt;ik&lt;/a&gt;和&lt;a href=&quot;https://github.com/medcl/elasticsearch-analysis-pinyin&quot;&gt;pinyin&lt;/a&gt;插件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y wget
cd plugins/
mkdir ik
cd ik
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.0/elasticsearch-analysis-ik-7.6.0.zip
unzip elasticsearch-analysis-ik-7.6.0.zip
rm -f elasticsearch-analysis-ik-7.6.0.zip
cd ..
mkdir pinyin
cd pinyin
wget https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v7.6.0/elasticsearch-analysis-pinyin-7.6.0.zip
unzip elasticsearch-analysis-pinyin-7.6.0.zip
rm -f elasticsearch-analysis-pinyin-7.6.0.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出容器，重启ElasticSearch。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart es
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试中文分词效果。&lt;/p&gt;
&lt;p&gt;请求：POST - &lt;code&gt;http://1.2.3.4:9200/_analyze&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;请求头：Content-Type: application/json&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;analyzer&quot;: &quot;ik_smart&quot;,
  &quot;text&quot;: &quot;中国男足在2022年卡塔尔世界杯预选赛中勇夺小组最后一名&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;tokens&quot;: [
        {
            &quot;token&quot;: &quot;中国&quot;,
            &quot;start_offset&quot;: 0,
            &quot;end_offset&quot;: 2,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 0
        },
        {
            &quot;token&quot;: &quot;男足&quot;,
            &quot;start_offset&quot;: 2,
            &quot;end_offset&quot;: 4,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 1
        },
        {
            &quot;token&quot;: &quot;在&quot;,
            &quot;start_offset&quot;: 4,
            &quot;end_offset&quot;: 5,
            &quot;type&quot;: &quot;CN_CHAR&quot;,
            &quot;position&quot;: 2
        },
        {
            &quot;token&quot;: &quot;2022年&quot;,
            &quot;start_offset&quot;: 5,
            &quot;end_offset&quot;: 10,
            &quot;type&quot;: &quot;TYPE_CQUAN&quot;,
            &quot;position&quot;: 3
        },
        {
            &quot;token&quot;: &quot;卡塔尔&quot;,
            &quot;start_offset&quot;: 10,
            &quot;end_offset&quot;: 13,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 4
        },
        {
            &quot;token&quot;: &quot;世界杯&quot;,
            &quot;start_offset&quot;: 13,
            &quot;end_offset&quot;: 16,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 5
        },
        {
            &quot;token&quot;: &quot;预选赛&quot;,
            &quot;start_offset&quot;: 16,
            &quot;end_offset&quot;: 19,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 6
        },
        {
            &quot;token&quot;: &quot;中&quot;,
            &quot;start_offset&quot;: 19,
            &quot;end_offset&quot;: 20,
            &quot;type&quot;: &quot;CN_CHAR&quot;,
            &quot;position&quot;: 7
        },
        {
            &quot;token&quot;: &quot;勇夺&quot;,
            &quot;start_offset&quot;: 20,
            &quot;end_offset&quot;: 22,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 8
        },
        {
            &quot;token&quot;: &quot;小组&quot;,
            &quot;start_offset&quot;: 22,
            &quot;end_offset&quot;: 24,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 9
        },
        {
            &quot;token&quot;: &quot;最后&quot;,
            &quot;start_offset&quot;: 24,
            &quot;end_offset&quot;: 26,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 10
        },
        {
            &quot;token&quot;: &quot;一名&quot;,
            &quot;start_offset&quot;: 26,
            &quot;end_offset&quot;: 28,
            &quot;type&quot;: &quot;CN_WORD&quot;,
            &quot;position&quot;: 11
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试拼音分词效果。&lt;/p&gt;
&lt;p&gt;请求：POST - &lt;code&gt;http://1.2.3.4:9200/_analyze&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;请求头：Content-Type: application/json&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;analyzer&quot;: &quot;pinyin&quot;,
  &quot;text&quot;: &quot;张学友&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;tokens&quot;: [
        {
            &quot;token&quot;: &quot;zhang&quot;,
            &quot;start_offset&quot;: 0,
            &quot;end_offset&quot;: 0,
            &quot;type&quot;: &quot;word&quot;,
            &quot;position&quot;: 0
        },
        {
            &quot;token&quot;: &quot;zxy&quot;,
            &quot;start_offset&quot;: 0,
            &quot;end_offset&quot;: 0,
            &quot;type&quot;: &quot;word&quot;,
            &quot;position&quot;: 0
        },
        {
            &quot;token&quot;: &quot;xue&quot;,
            &quot;start_offset&quot;: 0,
            &quot;end_offset&quot;: 0,
            &quot;type&quot;: &quot;word&quot;,
            &quot;position&quot;: 1
        },
        {
            &quot;token&quot;: &quot;you&quot;,
            &quot;start_offset&quot;: 0,
            &quot;end_offset&quot;: 0,
            &quot;type&quot;: &quot;word&quot;,
            &quot;position&quot;: 2
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;全文检索功能&lt;/h5&gt;
&lt;p&gt;可以通过GET或者POST请求进行搜索，下面演示了搜索有“未来”关键词商品。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;GET - &lt;code&gt;http://120.77.222.217:9200/demo/goods/_search?q=未来&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：URL中的中文应该要处理成百分号编码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;took&quot;: 19,
    &quot;timed_out&quot;: false,
    &quot;_shards&quot;: {
        &quot;total&quot;: 5,
        &quot;successful&quot;: 5,
        &quot;skipped&quot;: 0,
        &quot;failed&quot;: 0
    },
    &quot;hits&quot;: {
        &quot;total&quot;: 2,
        &quot;max_score&quot;: 0.73975396,
        &quot;hits&quot;: [
            {
                &quot;_index&quot;: &quot;demo&quot;,
                &quot;_type&quot;: &quot;goods&quot;,
                &quot;_id&quot;: &quot;1&quot;,
                &quot;_score&quot;: 0.73975396,
                &quot;_source&quot;: {
                    &quot;doc&quot;: {
                        &quot;no&quot;: &quot;5089253&quot;,
                        &quot;title&quot;: &quot;Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机&quot;,
                        &quot;brand&quot;: &quot;Apple(苹果)&quot;,
                        &quot;name&quot;: &quot;Apple iPhone X&quot;,
                        &quot;product&quot;: &quot;美国&quot;,
                        &quot;resolution&quot;: &quot;2436*1125&quot;,
                        &quot;intro&quot;: &quot;一直以来，Apple都心存一个设想，期待能够打造出这样一部iPhone：它有整面屏幕，能让你在使用时，完全沉浸其中，仿佛忘了它的存在。它是如此智能，哪怕轻轻一瞥，都能得到它心有灵犀的回应。而这个设想，终于随着iPhone X的到来成为了现实。现在，就跟未来见个面吧。&quot;
                    }
                }
            },
            {
                &quot;_index&quot;: &quot;demo&quot;,
                &quot;_type&quot;: &quot;goods&quot;,
                &quot;_id&quot;: &quot;3&quot;,
                &quot;_score&quot;: 0.68324494,
                &quot;_source&quot;: {
                    &quot;no&quot;: &quot;42417956432&quot;,
                    &quot;title&quot;: &quot;小米9 透明尊享版 手机 透明尊享 全网通(12GB + 256GB)&quot;,
                    &quot;brand&quot;: &quot;小米（MI）&quot;,
                    &quot;name&quot;: &quot;小米（MI）小米9透明&quot;,
                    &quot;product&quot;: &quot;中国大陆&quot;,
                    &quot;resolution&quot;: &quot;2340*1080&quot;,
                    &quot;intro&quot;: &quot;全面透明机身，独特科幻机甲风，来自未来的设计。&quot;
                }
            }
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;URL中可用的搜索参数如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;q&lt;/td&gt;
&lt;td&gt;查询字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analyzer&lt;/td&gt;
&lt;td&gt;分析查询字符串使用的分词器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analyze_wildcard&lt;/td&gt;
&lt;td&gt;通配符或者前缀查询是否被分析，默认为false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;default_operator&lt;/td&gt;
&lt;td&gt;多个条件之间的关系，默认为OR，可以修改为AND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;explain&lt;/td&gt;
&lt;td&gt;在返回的结果中包含评分机制的解释&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fields&lt;/td&gt;
&lt;td&gt;只返回索引中指定的列，多个列中间用逗号隔开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sort&lt;/td&gt;
&lt;td&gt;排序参考的字段，可以用:asc和:desc来指定升序和降序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timeout&lt;/td&gt;
&lt;td&gt;超时时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;from&lt;/td&gt;
&lt;td&gt;匹配结果的开始值，默认为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;匹配结果的条数，默认为10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POST - &lt;code&gt;http://120.77.222.217:9200/demo/goods/_search&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;请求头：Content-Type: application/json&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;query&quot;: {
        &quot;term&quot;: {
            &quot;type&quot;: &quot;&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POST搜索是基于DSL的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Django对接ElasticSearch&lt;/h5&gt;
&lt;p&gt;Python对接ElasticSearch的第三方库是HayStack，在Django项目中可以使用django-haystack，通过HayStack可以在不修改代码对接多种搜索引擎服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-haystack elasticsearch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    ...
    &apos;haystack&apos;,
    ...
]

HAYSTACK_CONNECTIONS = {
    &apos;default&apos;: {
        # 引擎配置
        &apos;ENGINE&apos;: &apos;haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine&apos;,
        # 搜索引擎服务的URL
        &apos;URL&apos;: &apos;http://1.2.3.4:9200&apos;,
        # 索引库的名称
        &apos;INDEX_NAME&apos;: &apos;goods&apos;,
    },
}

# 添加/删除/更新数据时自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = &apos;haystack.signals.RealtimeSignalProcessor&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;索引类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from haystack import indexes


class GoodsIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        return Goods

    def index_queryset(self, using=None):
        return self.get_model().objects.all()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑text字段的模板（需要放在templates/search/indexes/demo/goods_text.txt）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{object.title}}
{{object.intro}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置URL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    # ...
    url(&apos;search/&apos;, include(&apos;haystack.urls&apos;)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成初始索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py rebuild_index
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：可以参考&lt;a href=&quot;https://www.zmrenwu.com/post/45/&quot;&gt;《Django Haystack 全文检索与关键词高亮》&lt;/a&gt;一文来更深入的了解基于Haystack的全文检索操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;98 项目部署上线指南&lt;/h2&gt;
&lt;h4&gt;准备上线&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;上线前的检查工作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py check --deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将DEBUG设置为False并配置ALLOWED_HOSTS。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEBUG = False
ALLOWED_HOSTS = [&apos;*&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全相关的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 保持HTTPS连接的时间
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# 自动重定向到安全连接
SECURE_SSL_REDIRECT = True

# 避免浏览器自作聪明推断内容类型
SECURE_CONTENT_TYPE_NOSNIFF = True

# 避免跨站脚本攻击
SECURE_BROWSER_XSS_FILTER = True

# COOKIE只能通过HTTPS进行传输
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# 防止点击劫持攻击手段 - 修改HTTP协议响应头
# 当前网站是不允许使用&amp;lt;iframe&amp;gt;标签进行加载的
X_FRAME_OPTIONS = &apos;DENY&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;敏感信息放到环境变量或文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECRET_KEY = os.environ[&apos;SECRET_KEY&apos;]

DB_USER = os.environ[&apos;DB_USER&apos;]
DB_PASS = os.environ[&apos;DB_PASS&apos;]

REDIS_AUTH = os.environ[&apos;REDIS_AUTH&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;更新服务器Python环境到3.x&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果需要清除之前的安装，就删除对应的文件和文件夹即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装底层依赖库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel libdb4-devel libpcap-devel xz-devel libffi-devel libxml2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载Python源代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tar.xz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;验证下载文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;md5sum Python-3.7.6.tar.xz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解压缩和解归档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xz -d Python-3.7.6.tar.xz
tar -xvf Python-3.7.6.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行安装前的配置（生成Makefile文件）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd Python-3.7.6
./configure --prefix=/usr/local/python37 --enable-optimizations
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构建和安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make &amp;amp;&amp;amp; make install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置PATH环境变量（用户或系统环境变量）并激活。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.bash_profile
vim /etc/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;... 此处省略上面的代码...

export PATH=$PATH:/usr/local/python37/bin

... 此处省略下面的代码...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;source ~/.bash_profile
source /etc/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册软链接（符号链接）- 这一步不是必须的，但通常会比较有用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /usr/local/python37/bin/python3 /usr/bin/python3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试Python环境是否更新成功（安装Python 3一定不能破坏原来的Python 2）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 --version
python --version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;项目目录结构&lt;/h4&gt;
&lt;p&gt;假设项目文件夹为&lt;code&gt;project&lt;/code&gt;，下面的五个子目录分别是：&lt;code&gt;code&lt;/code&gt;、&lt;code&gt;conf&lt;/code&gt;、&lt;code&gt;logs&lt;/code&gt;、&lt;code&gt;stat&lt;/code&gt;和&lt;code&gt;venv&lt;/code&gt;分别用来保存项目的代码、配置文件、日志文件、静态资源和虚拟环境。其中，&lt;code&gt;conf&lt;/code&gt;目录下的子目录&lt;code&gt;cert&lt;/code&gt;中保存了配置HTTPS需要使用的证书和密钥；&lt;code&gt;code&lt;/code&gt;目录下的项目代码可以通过版本控制工具从代码仓库中检出；虚拟环境可以通过工具（如：venv、virtualenv、pyenv等）进行创建。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project
├── code
│   └── fangtx
│       ├── api
│       ├── common
│       ├── fangtx
│       ├── forum
│       ├── rent
│       ├── user
│       ├── manage.py
│       ├── README.md
│       ├── static
│       └── templates
├── conf
│   ├── cert
│   │   ├── 214915882850706.key
│   │   └── 214915882850706.pem
│   ├── nginx.conf
│   └── uwsgi.ini
├── logs
│   ├── access.log
│   ├── error.log
│   └── uwsgi.log
├── stat
│   └── css
│   └── images
│   └── js
└── venv
    ├── bin
    │   ├── activate
    │   ├── activate.csh
    │   ├── activate.fish
    │   ├── celery
    │   ├── celerybeat
    │   ├── celeryd
    │   ├── celeryd-multi
    │   ├── coverage
    │   ├── coverage3
    │   ├── coverage-3.7
    │   ├── django-admin
    │   ├── django-admin.py
    │   ├── easy_install
    │   ├── easy_install-3.7
    │   ├── pip
    │   ├── pip3
    │   ├── pip3.7
    │   ├── __pycache__
    │   ├── pyrsa-decrypt
    │   ├── pyrsa-decrypt-bigfile
    │   ├── pyrsa-encrypt
    │   ├── pyrsa-encrypt-bigfile
    │   ├── pyrsa-keygen
    │   ├── pyrsa-priv2pub
    │   ├── pyrsa-sign
    │   ├── pyrsa-verify
    │   ├── python -&amp;gt; python3
    │   ├── python3 -&amp;gt; /usr/bin/python3
    │   └── uwsgi
    ├── include
    ├── lib
    │   └── python3.7
    ├── lib64 -&amp;gt; lib
    ├── pip-selfcheck.json
    └── pyvenv.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面以阿里云为例，简单说明如何为项目注册域名、解析域名以及购买权威机构颁发的证书。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wanwang.aliyun.com/domain/&quot;&gt;注册域名&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/92d1f52ecb.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://beian.aliyun.com/&quot;&gt;域名备案&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/90b221f4df.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dns.console.aliyun.com/#/dns/domainList&quot;&gt;域名解析&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/95c1001f86.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/608d858960.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.aliyun.com/product/cas&quot;&gt;购买证书&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day91-100/816c97a829.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以使用类似于sftp的工具将证书上传到&lt;code&gt;conf/cert&lt;/code&gt;目录，然后使用git克隆项目代码到&lt;code&gt;code&lt;/code&gt;目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd code
git clone &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到项目目录，创建并激活虚拟环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重建项目依赖项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -r code/teamproject/requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;uWSGI的配置&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装uWSGI。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install uwsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改uWSGI的配置文件（&lt;code&gt;/root/project/conf/uwsgi.ini&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[uwsgi]
# 配置前导路径
base=/root/project
# 配置项目名称
name=teamproject
# 守护进程
master=true
# 进程个数
processes=4
# 虚拟环境
pythonhome=%(base)/venv
# 项目地址
chdir=%(base)/code/%(name)
# 指定python解释器
pythonpath=%(pythonhome)/bin/python
# 指定uwsgi文件
module=%(name).wsgi
# 通信的地址和端口(自己服务器的IP地址和端口)
socket=172.18.61.250:8000
# 日志文件地址
logto=%(base)/logs/uwsgi.log
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：可以先将“通信的地址和端口”项等号前面改为http来进行测试，如果没有问题再改回    成socket，然后通过Nginx来实现项目的“动静分离”（静态资源交给Nginx处理，动态内容交给    uWSGI处理）。按照下面的方式可以启动uWSGI服务器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nohup uwsgi --ini conf/uwsgi.ini &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Nginx的配置&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Nginx。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum -y install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改全局配置文件（&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置用户
user nginx;
# 工作进程数(建议跟CPU的核数量一致)
worker_processes auto;
# 错误日志
error_log /var/log/nginx/error.log;
# 进程文件
pid /run/nginx.pid;
# 包含其他的配置
include /usr/share/nginx/modules/*.conf;
# 工作模式(多路IO复用方式)和连接上限
events {
    use epoll;
    worker_connections 1024;
}
# HTTP服务器相关配置
http {
    # 日志格式
    log_format  main  &apos;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &apos;
                      &apos;$status $body_bytes_sent &quot;$http_referer&quot; &apos;
                      &apos;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&apos;;
    # 访问日志
    access_log  /var/log/nginx/access.log  main;
    # 开启高效文件传输模式
    sendfile            on;
    # 用sendfile传输文件时有利于改善性能
    tcp_nopush          on;
    # 禁用Nagle来解决交互性问题
    tcp_nodelay         on;
    # 客户端保持连接时间
    keepalive_timeout   30;
    types_hash_max_size 2048;
    # 包含MIME类型的配置
    include             /etc/nginx/mime.types;
    # 默认使用二进制流格式
    default_type        application/octet-stream;
    # 包含其他配置文件
    include /etc/nginx/conf.d/*.conf;
    # 包含项目的Nginx配置文件
    include /root/project/conf/*.conf;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑局部配置文件（&lt;code&gt;/root/project/conf/nginx.conf&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen      80;
    server_name _;
    access_log /root/project/logs/access.log;
    error_log /root/project/logs/error.log;
    location / {
        include uwsgi_params;
        uwsgi_pass 172.18.61.250:8000;
    }
    location /static/ {
        alias /root/project/stat/;
        expires 30d;
    }
}
server {
    listen      443;
    server_name _;
    ssl         on;
    access_log /root/project/logs/access.log;
    error_log /root/project/logs/error.log;
    ssl_certificate     /root/project/conf/cert/214915882850706.pem;
    ssl_certificate_key /root/project/conf/cert/214915882850706.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / {
        include uwsgi_params;
        uwsgi_pass 172.18.61.250:8000;
    }
    location /static/ {
        alias /root/project/static/;
        expires 30d;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到此为止，我们可以启动Nginx来访问我们的应用程序，HTTP和HTTPS都是没有问题的，如果Nginx已经运行，在修改配置文件后，我们可以用下面的命令重新启动Nginx。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启Nginx服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：可以对Django项目使用&lt;code&gt;python manage.py collectstatic&lt;/code&gt;命令将静态资源收集到指定目录下，要做到这点只需要在项目的配置文件&lt;code&gt;settings.py&lt;/code&gt;中添加&lt;code&gt;STATIC_ROOT&lt;/code&gt;配置即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;负载均衡配置&lt;/h5&gt;
&lt;p&gt;下面的配置中我们使用Nginx实现负载均衡，为另外的三个Nginx服务器（通过Docker创建）提供反向代理服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 801:80 --name nginx1 nginx:latest
docker run -d -p 802:80 --name nginx2 nginx:latest
docker run -d -p 803:80 --name nginx3 nginx:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;user root;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

# 为HTTP服务配置负载均衡
http {
	upstream xx {
		server 192.168.1.100 weight=2;
		server 192.168.1.101 weight=1;
		server 192.168.1.102 weight=1;
    }

	server {
		listen       80 default_server;
		listen       [::]:80 default_server;
		listen       443 ssl;
		listen       [::]:443 ssl;

        ssl on;
		access_log /root/project/logs/access.log;
		error_log /root/project/logs/error.log;
		ssl_certificate /root/project/conf/cert/214915882850706.pem;
		ssl_certificate_key /root/project/conf/cert/214915882850706.key;
		ssl_session_timeout 5m;
		ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
		ssl_prefer_server_ciphers on;

		location / {
			proxy_set_header Host $host;
			proxy_set_header X-Forwarded-For $remote_addr;
			# proxy_set_header X-Real-IP $remote_addr;
			# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_buffering off;
			proxy_pass http://fangtx;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：Nginx在配置负载均衡时，默认使用WRR（加权轮询算法），除此之外还支持ip_hash、fair（需要安装upstream_fair模块）和url_hash算法。此外，在配置upstream模块时可以指定服务器的状态值，包括：backup（备份机器，其他服务器不可用时才将请求分配到该机器）、down、fail_timeout（请求失败达到max_fails后的暂停服务时间）、max_fails（允许请求失败的次数）和weight（轮询的权重）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Keepalived&lt;/h4&gt;
&lt;p&gt;当使用Nginx进行负载均衡配置时，要考虑负载均衡服务器宕机的情况。为此可以使用Keepalived来实现负载均衡主机和备机的热切换，从而保证系统的高可用性。Keepalived的配置还是比较复杂，通常由专门做运维的人进行配置，一个基本的配置可以参照&lt;a href=&quot;https://www.jianshu.com/p/dd93bc6d45f5&quot;&gt;《Keepalived的配置和使用》&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;MySQL主从复制&lt;/h4&gt;
&lt;p&gt;下面还是基于Docker来演示如何配置MySQL主从复制。我们事先准备好MySQL的配置文件以及保存MySQL数据和运行日志的目录，然后通过Docker的数据卷映射来指定容器的配置、数据和日志文件的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root
└── mysql
    ├── master
    │   ├── conf
    |	└── data
    └── slave-1
    |	├── conf
    |	└── data
    └── slave-2
    |	├── conf
    |	└── data
    └── slave-3
    	├── conf
    	└── data
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MySQL的配置文件（master和slave的配置文件需要不同的server-id）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
log-error=/var/log/mysql/error.log
server-id=1
log-bin=/var/log/mysql/mysql-bin.log
expire_logs_days=30
max_binlog_size=256M
symbolic-links=0
# slow_query_log=ON
# slow_query_log_file=/var/log/mysql/slow.log
# long_query_time=1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建和配置master。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 3306:3306 --name mysql-master \
-v /root/mysql/master/conf:/etc/mysql/mysql.conf.d \
-v /root/mysql/master/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

docker exec -it mysql-master /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.7.23-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type &apos;help;&apos; or &apos;\h&apos; for help. Type &apos;\c&apos; to clear the current input statement.

mysql&amp;gt; grant replication slave on *.* to &apos;slave&apos;@&apos;%&apos; identified by &apos;iamslave&apos;;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql&amp;gt; flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql&amp;gt; show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      590 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

mysql&amp;gt; quit
Bye
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面创建Docker容器时使用的&lt;code&gt;-v&lt;/code&gt;参数（&lt;code&gt;--volume&lt;/code&gt;）表示映射数据卷，冒号前是宿主机的目录，冒号后是容器中的目录，这样相当于将宿主机中的目录挂载到了容器中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;备份主表中的数据（如果需要的话）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; flush table with read lock;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysqldump -u root -p 123456 -A -B &amp;gt; /root/backup/mysql/mybak$(date +&quot;%Y%m%d%H%M%S&quot;).sql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; unlock table;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建和配置slave。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 3308:3306 --name mysql-slave-1 \
-v /root/mysql/slave-1/conf:/etc/mysql/mysql.conf.d \
-v /root/mysql/slave-1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--link mysql-master:mysql-master mysql:5.7

docker run -d -p 3309:3306 --name mysql-slave-2 \
-v /root/mysql/slave-2/conf:/etc/mysql/mysql.conf.d \
-v /root/mysql/slave-2/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--link mysql-master:mysql-master mysql:5.7

docker run -d -p 3310:3306 --name mysql-slave-3 \
-v /root/mysql/slave-3/conf:/etc/mysql/mysql.conf.d \
-v /root/mysql/slave-3/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--link mysql-master:mysql-master mysql:5.7

docker exec -it mysql-slave-1 /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.23-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type &apos;help;&apos; or &apos;\h&apos; for help. Type &apos;\c&apos; to clear the current input statement.

mysql&amp;gt; reset slave;
Query OK, 0 rows affected (0.02 sec)

mysql&amp;gt; change master to master_host=&apos;mysql-master&apos;, master_user=&apos;slave&apos;, master_password=&apos;iamslave&apos;, master_log_file=&apos;mysql-bin.000003&apos;, master_log_pos=590;
Query OK, 0 rows affected, 2 warnings (0.03 sec)

mysql&amp;gt; start slave;
Query OK, 0 rows affected (0.01 sec)

mysql&amp;gt; show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: mysql57
                  Master_User: slave
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 590
               Relay_Log_File: f352f05eb9d0-relay-bin.000002
                Relay_Log_Pos: 320
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
             Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 590
              Relay_Log_Space: 534
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 30c38043-ada1-11e8-8fa1-0242ac110002
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
1 row in set (0.00 sec)

mysql&amp;gt; quit
Bye
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来可以如法炮制配置出slave2和slave3，这样就可以搭建起一个“一主带三从”的主从复制环境。上面创建创建容器时使用的&lt;code&gt;--link&lt;/code&gt;参数用来配置容器在网络上的主机名（网络地址别名）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;配置好主从复制后，写数据的操作应该master上执行，而读数据的操作应该在slave上完成。为此，在Django项目中需要配置DATABASE_ROUTERS并通过自定义的主从复制路由类来实现读写分离操作，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_ROUTERS = [
    # 此处省略其他配置
    &apos;common.routers.MasterSlaveRouter&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class MasterSlaveRouter(object):
    &quot;&quot;&quot;主从复制路由&quot;&quot;&quot;

    @staticmethod
    def db_for_read(model, **hints):
        &quot;&quot;&quot;
        Attempts to read auth models go to auth_db.
        &quot;&quot;&quot;
        return random.choice((&apos;slave1&apos;, &apos;slave2&apos;, &apos;slave3&apos;))

    @staticmethod
    def db_for_write(model, **hints):
        &quot;&quot;&quot;
        Attempts to write auth models go to auth_db.
        &quot;&quot;&quot;
        return &apos;default&apos;

    @staticmethod
    def allow_relation(obj1, obj2, **hints):
        &quot;&quot;&quot;
        Allow relations if a model in the auth app is involved.
        &quot;&quot;&quot;
        return None

    @staticmethod
    def allow_migrate(db, app_label, model_name=None, **hints):
        &quot;&quot;&quot;
        Make sure the auth app only appears in the &apos;auth_db&apos;
        database.
        &quot;&quot;&quot;
        return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的内容参考了Django官方文档的&lt;a href=&quot;https://docs.djangoproject.com/en/2.1/topics/db/multi-db/#topics-db-multi-db-routing&quot;&gt;DATABASE_ROUTERS配置&lt;/a&gt;，对代码进行了适当的调整。&lt;/p&gt;
&lt;h4&gt;Docker&lt;/h4&gt;
&lt;p&gt;事实上，项目上线中最为麻烦的事情就是配置软件运行环境，环境的差异会给软件的安装和部署带来诸多的麻烦，而Docker正好可以解决这个问题。关于Docker在之前的文档中我们已经介绍过了，接下来我们对Docker的知识做一些必要的补充。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建镜像文件。&lt;/p&gt;
&lt;p&gt;将容器保存成镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker commit -m &quot;...&quot; -a &quot;jackfrued&quot; &amp;lt;container-name&amp;gt; jackfrued/&amp;lt;image-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用Dockerfile构建镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 指定基础镜像文件
FROM centos:latest

# 指定维护者信息
MAINTAINER jackfrued

# 执行命令
RUN yum -y install gcc
RUN cd ~
RUN mkdir -p project/code
RUN mkdir -p project/logs

# 拷贝文件
COPY ...

# 暴露端口
EXPOSE ...

# 在容器启动时执行命令
CMD ~/init.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker build -t jackfrued/&amp;lt;image-name&amp;gt; .
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;镜像的导入和导出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker save -o &amp;lt;file-name&amp;gt;.tar &amp;lt;image-name&amp;gt;:&amp;lt;version&amp;gt;
docker load -i &amp;lt;file-name&amp;gt;.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;推送到DockerHub服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker tag &amp;lt;image-name&amp;gt;:&amp;lt;version&amp;gt; jackfrued/&amp;lt;name&amp;gt;
docker login
docker push jackfrued/&amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容器之间的通信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --link &amp;lt;container-name&amp;gt;:&amp;lt;alias-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果我们能够在Docker中完成项目的部署，并且将整个部署好的容器打包成镜像文件进行分发和安装，这样就可以解决项目在多个节点上进行部署时可能遇到的麻烦，而且整个部署可以在很短的时间内完成。&lt;/p&gt;
&lt;h4&gt;Supervisor&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Supervisor/supervisor&quot;&gt;Supervisor&lt;/a&gt;是一个用Python写的进程管理工具，可以很方便的用来在类Unix系统下启动、重启（自动重启程序）和关闭进程，目前Supervisor暂时还没有提供对Python 3的支持，可以通过Python 2来安装和运行Supervisor，再通过Supervisor来管理Python 3的程序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：还有一个和Supervisor功能类似的工具名为Circus，支持Python 3。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Supervisor。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;virtualenv -p /usr/bin/python venv
source venv/bin/activate
pip install supervisor
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看Supervisor的配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/supervisord.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;; 此处省略上面的代码
; The [include] section can just contain the &quot;files&quot; setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.
[include]
files = supervisord.d/*.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出自定义的管理配置代码可以放在&lt;code&gt;/etc/supervisord.d&lt;/code&gt;目录中，并且文件名以&lt;code&gt;ini&lt;/code&gt;作为后缀即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写自己的配置文件&lt;code&gt;fangtx.ini&lt;/code&gt;并放在&lt;code&gt;/etc/supervisord.d&lt;/code&gt;目录中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[program:project]
command=uwsgi --ini /root/project/conf/uwsgi.ini
stopsignal=QUIT
autostart=true
autorestart=true
redirect_stderr=true

[program:celery]
; Set full path to celery program if using virtualenv
command=/root/project/venv/bin/celery -A fangtx worker
user=root
numprocs=1
stdout_logfile=/var/log/supervisor/celery.log
stderr_logfile=/var/log/supervisor/celery_error.log
autostart=true
autorestart=true
startsecs=10

; Need to wait for currently executing tasks to finish at shutdown.
; Increase this if you have very long running tasks.
;stopwaitsecs = 600

; When resorting to send SIGKILL to the program to terminate it
; send SIGKILL to its whole process group instead,
; taking care of its children as well.
killasgroup=true
; Set Celery priority higher than default (999)
; so, if rabbitmq is supervised, it will start first.
priority=1000
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动Supervisor。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;supervisorctl -c /etc/supervisord.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;其他服务&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;常用开源软件。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;开源方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;版本控制工具&lt;/td&gt;
&lt;td&gt;Git、Mercurial、SVN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缺陷管理&lt;/td&gt;
&lt;td&gt;Redmine、Mantis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载均衡&lt;/td&gt;
&lt;td&gt;Nginx、LVS、HAProxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮件服务&lt;/td&gt;
&lt;td&gt;Postfix、Sendmail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP服务&lt;/td&gt;
&lt;td&gt;Nginx、Apache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息队列&lt;/td&gt;
&lt;td&gt;RabbitMQ、ZeroMQ、Redis、Kafka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件系统&lt;/td&gt;
&lt;td&gt;FastDFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;基于位置服务（LBS）&lt;/td&gt;
&lt;td&gt;MongoDB、Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;监控服务&lt;/td&gt;
&lt;td&gt;Nagios、Zabbix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关系型数据库&lt;/td&gt;
&lt;td&gt;MySQL、PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非关系型数据库&lt;/td&gt;
&lt;td&gt;MongoDB、Redis、Cassandra、TiDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索引擎&lt;/td&gt;
&lt;td&gt;ElasticSearch、Solr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存服务&lt;/td&gt;
&lt;td&gt;Mamcached、Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用云服务。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;可用的云服务&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;团队协作工具&lt;/td&gt;
&lt;td&gt;Teambition、钉钉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码托管平台&lt;/td&gt;
&lt;td&gt;Github、Gitee、CODING&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮件服务&lt;/td&gt;
&lt;td&gt;SendCloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云存储（CDN）&lt;/td&gt;
&lt;td&gt;七牛、OSS、LeanCloud、Bmob、又拍云、S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移动端推送&lt;/td&gt;
&lt;td&gt;极光、友盟、百度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;即时通信&lt;/td&gt;
&lt;td&gt;环信、融云&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;短信服务&lt;/td&gt;
&lt;td&gt;云片、极光、Luosimao、又拍云&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第三方登录&lt;/td&gt;
&lt;td&gt;友盟、ShareSDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网站监控和统计&lt;/td&gt;
&lt;td&gt;阿里云监控、监控宝、百度云观测、小鸟云&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;99 面试中的公共问题&lt;/h2&gt;
&lt;h4&gt;计算机基础&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;TCP/IP模型相关问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议阅读阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html&quot;&gt;《互联网协议入门（一）》&lt;/a&gt;和&lt;a href=&quot;http://www.ruanyifeng.com/blog/2012/06/internet_protocol_suite_part_ii.html&quot;&gt;《互联网协议入门（二）》&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTP和HTTPS相关问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议阅读阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2016/08/http.html&quot;&gt;《HTTP 协议入门》&lt;/a&gt;和&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html&quot;&gt;《SSL/TLS协议运行机制的概述》&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux常用命令和服务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程和线程之间的关系。什么时候用多线程？什么时候用多进程？。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关系型数据库相关问题（ACID、事务隔离级别、锁、SQL优化）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非关系型数据库相关问题（CAP/BASE、应用场景）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Python基础&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;开发中用过哪些标准库和三方库。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;标准库：sys / os / re / math / random / logging / json / pickle / shelve / socket / datetime / hashlib / configparser / urllib / itertools / collections / functools / threading / multiprocess / timeit / atexit / abc / asyncio / base64 / concurrent.futures / copy / csv / operator / enum / heapq / http / profile / pstats / ssl / unittest / uuid&lt;/p&gt;
&lt;p&gt;三方库：openpyxl / xlrd / xlwt / PyPDF2 / ReportLab / PyYAML / jieba / pillow / requests / urllib3 / responses / aiohttp / BeautifulSoup4 / lxml / pyquery / PyMySQL / psycopg2 / redis / PyMongo / Peewee / SQLAlchemy / alipay / PyJWT / itsdangerous / celery / flower / elasticsearch-dsl-py / PyCrypto / Paramiko / logbook / nose / pytest / coverage / Selenium / lineprofiler / memoryprofiler / matplotlib / pygal / OpenCV&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;装饰器的作用、原理和实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用过哪些魔法方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议阅读&lt;a href=&quot;https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html&quot;&gt;《Python魔术方法指南》&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成式、生成器、迭代器的编写。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;列表、集合、字典的底层实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;垃圾回收相关问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并发编程的相关问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;协程和异步I/O相关知识。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Django和Flask&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MVC架构（MTV）解决了什么问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间件的执行流程以及如何自定义中间件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;REST数据接口如何设计（URL、域名、版本、过滤、状态码、安全性）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议阅读阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/05/restful_api.html&quot;&gt;《RESTful API设计指南》&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用ORM框架实现CRUD操作的相关问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何实现多条件组合查询 / 如何执行原生的SQL / 如何避免N+1查询问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何执行异步任务和定时任务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何实现页面缓存和查询缓存？缓存如何预热？&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;爬虫相关&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Scrapy框架的组件和数据处理流程。&lt;/li&gt;
&lt;li&gt;爬取的目的（项目中哪些地方需要用到爬虫的数据）。&lt;/li&gt;
&lt;li&gt;使用的工具（抓包、下载、清理、存储、分析、可视化）。&lt;/li&gt;
&lt;li&gt;数据的来源（能够轻松的列举出10个网站）。&lt;/li&gt;
&lt;li&gt;数据的构成（抓取的某个字段在项目中有什么用）。&lt;/li&gt;
&lt;li&gt;反反爬措施（限速、请求头、Cookie池、代理池、Selenium WebDriver、RoboBrowser、TOR、OCR）。&lt;/li&gt;
&lt;li&gt;数据的体量（最后抓取了多少数据，多少W条数据或多少个G的数据）。&lt;/li&gt;
&lt;li&gt;后期数据处理（持久化、数据补全、归一化、格式化、转存、分类）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据分析&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;科学运算函数库（SciPy和NumPy常用运算）。&lt;/li&gt;
&lt;li&gt;数据分析库（Pandas中封装的常用算法）。&lt;/li&gt;
&lt;li&gt;常用的模型及对应的场景（分类、回归、聚类）。&lt;/li&gt;
&lt;li&gt;提取了哪些具体的指标。&lt;/li&gt;
&lt;li&gt;如何评价模型的优劣。&lt;/li&gt;
&lt;li&gt;每种模型实际操作的步骤，对结果如何评价。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;项目相关&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;项目团队构成以及自己在团队中扮演的角色（在项目中的职责）。&lt;/li&gt;
&lt;li&gt;项目的业务架构（哪些模块及子模块）和技术架构（移动端、PC端、后端技术栈）。&lt;/li&gt;
&lt;li&gt;软件控制管理相关工具（版本控制、问题管理、持续集成）。&lt;/li&gt;
&lt;li&gt;核心业务实体及其属性，实体与实体之间的关系。&lt;/li&gt;
&lt;li&gt;用到哪些依赖库，依赖库主要解决哪方面的问题。&lt;/li&gt;
&lt;li&gt;项目如何部署上线以及项目的物理架构（Nginx、Gunicorn/uWSGI、Redis、MongoDB、MySQL、Supervisor等）。&lt;/li&gt;
&lt;li&gt;如何对项目进行测试，有没有做过性能调优。&lt;/li&gt;
&lt;li&gt;项目中遇到的困难有哪些，如何解决的。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;100 补充内容&lt;/h2&gt;
&lt;p&gt;之前没有来得及安排，准备后面补充的内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;面试&lt;/strong&gt;相关资源请移步到&lt;a href=&quot;https://github.com/jackfrued/Python-Interview-Bible&quot;&gt;“Python面试宝典”&lt;/a&gt;项目。
&lt;ul&gt;
&lt;li&gt;Python 面试宝典&lt;/li&gt;
&lt;li&gt;商业分析面试宝典&lt;/li&gt;
&lt;li&gt;数据分析师 SQL 面试宝典&lt;/li&gt;
&lt;li&gt;机器学习面试宝典&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数学基础&lt;/strong&gt;相关资源请移步到&lt;a href=&quot;https://github.com/jackfrued/Math_for_ML&quot;&gt;“机器学习数学基础知识”&lt;/a&gt;项目。
&lt;ul&gt;
&lt;li&gt;线性代数&lt;/li&gt;
&lt;li&gt;概率论和统计学&lt;/li&gt;
&lt;li&gt;高等数学&lt;/li&gt;
&lt;li&gt;信息论&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度学习&lt;/strong&gt;相关内容请移步到&lt;a href=&quot;https://github.com/jackfrued/Deep-Learning-Is-Nothing&quot;&gt;“深度学习就是大力出奇迹”&lt;/a&gt;项目。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Day81-90 教程全文（10 篇合并）</title><link>https://www.klucen.cn/posts/python/day81-90-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day81-90-full/</guid><pubDate>Sun, 14 Dec 2025 00:17:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 10 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：机器学习入门
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#81-%E6%B5%85%E8%B0%88%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0&quot;&gt;81 浅谈机器学习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#82-k%E6%9C%80%E8%BF%91%E9%82%BB%E7%AE%97%E6%B3%95&quot;&gt;82 k最近邻算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#83-%E5%86%B3%E7%AD%96%E6%A0%91%E5%92%8C%E9%9A%8F%E6%9C%BA%E6%A3%AE%E6%9E%97&quot;&gt;83 决策树和随机森林&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#84-%E6%9C%B4%E7%B4%A0%E8%B4%9D%E5%8F%B6%E6%96%AF%E7%AE%97%E6%B3%95&quot;&gt;84 朴素贝叶斯算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#85-%E5%9B%9E%E5%BD%92%E6%A8%A1%E5%9E%8B&quot;&gt;85 回归模型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#86-k-means%E8%81%9A%E7%B1%BB%E7%AE%97%E6%B3%95&quot;&gt;86 K-Means聚类算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#87-%E9%9B%86%E6%88%90%E5%AD%A6%E4%B9%A0%E7%AE%97%E6%B3%95&quot;&gt;87 集成学习算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#88-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B&quot;&gt;88 神经网络模型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#89-%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86%E5%85%A5%E9%97%A8&quot;&gt;89 自然语言处理入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#90-%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%AE%9E%E6%88%98&quot;&gt;90 机器学习实战&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;81 浅谈机器学习&lt;/h2&gt;
&lt;p&gt;人工智能无疑是最近几年热度极高的一个词，从2016年谷歌 DeepMind 团队开发的 AlphaGo 围棋程序战胜人类顶尖棋手，到2017年基于 Transformer 架构的 NLP 模型发布，再到2023年 OpenAI 推出基于 GPT-4 的 ChatGPT 以及人工智能在医疗、自动驾驶等领域的深度应用，人工智能的热潮到达了自1956年达特茅斯会议以来前所未有的高度，可以说几乎每个人的生活都或多或少的受到了人工智能的影响。人工智能是计算机科学的一个重要分支，涉及计算机模拟智能行为的能力以及机器模仿人类智能行为的能力。研究人工智能的主要目标是开发出能够独立做出决策的系统，从而在医疗、工程、金融、教育、科研、公共服务等诸多领域帮助人类更高效的工作。人工智能的英文是“&lt;em&gt;artificial intelligence&lt;/em&gt;”，因此通常被简称为 &lt;em&gt;AI&lt;/em&gt;，人工智能包含了诸多的内容，我们经常说到的机器学习、深度学习、自然语言处理、计算机视觉、强化学习、数据挖掘、专家系统、工业机器人、自动驾驶等都属于人工智能的范畴。狭义的人工智能通常只能执行特定的任务，会聊天的人工智能通常不会开车，会开车的人工智能通常不会下棋；广义的人工智能需要具备通用智能，能够执行任何人类智能可以执行的任务；而更进一步的能够超越人类智能的人工智能，我们称之为超人工智能。&lt;/p&gt;
&lt;p&gt;本课程我们主要探讨人工智能中的机器学习（Machine Learning）。机器学习是人工智能的一个子领域，关注如何通过数据和算法来使计算机系统从经验中学习并进行预测或决策。简单的说，机器学习是实现人工智能的一种方法，有很多 AI 系统都是通过机器学习技术开发的。在一些特定场景，人们也用数据挖掘（Data Mining）这个词来指代机器学习，所谓的数据挖掘就是从数据中提取有用的信息和知识，分析和解释数据中的模式和趋势，最终达成预测未来趋势和行为的目标。当然，我们在提到这两个词的时候，表达的侧重点还是有所区别，数据挖掘主要关注知识发现，而机器学习侧重于构建和优化预测模型。当下，还有一个非常热门的概念和研究领域叫深度学习（Deep Learning），它是机器学习的一个子领域，特别侧重于使用多层神经网络（深度神经网络）来进行数据处理和学习。深度学习在处理图像、语音和自然语言等复杂数据时表现出色，能够自动学习数据的层次化特征，从而降低人工干预的需求。当然，深度学习模型通常比传统的机器学习模型更复杂，且需要更多的数据和计算资源。&lt;/p&gt;
&lt;h4&gt;人工智能发展史&lt;/h4&gt;
&lt;p&gt;治学先治史，我们简单回顾一下人工智能发展史上的里程碑事件，对于比较久远的历史，我们只简单提一些重要的时间节点，我们把重点放在最近几年的重大事件上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day81-90/43dd9676f3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上图来自于&lt;a href=&quot;https://www.aminer.cn/ai-history&quot;&gt;AMiner网站&lt;/a&gt;，按照该网站的要求，在引用上图需要引用下面的论文。需要高清原图的，也可以在该网站上获取。&lt;/p&gt;
&lt;p&gt;Jie Tang, Jing Zhang, Limin Yao, Juanzi Li, Li Zhang, and Zhong Su. &lt;a href=&quot;https://www.aminer.cn/pub/53e9a5afb7602d9702edacce/arnetminer-extraction-and-mining-of-academic-social-networks&quot;&gt;ArnetMiner: Extraction and Mining of Academic Social Networks.&lt;/a&gt; In Proceedings of the Fourteenth ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (SIGKDD&apos;2008). pp.990-998.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;1950年，艾伦·图灵发表了一篇划时代的论文，文中预言了创造出具有智能的机器的可能性并提出了著名的图灵测试。图灵测试旨在通过计算机与人类进行对话，如果计算机能够使人类无法区分回答问题的是机器还是人类，就认为该计算机具有智能。&lt;/li&gt;
&lt;li&gt;1956年，达特茅斯会议召开，该会议被认为是人工智能的诞生标志。会议上约翰·麦卡锡、马文·明斯基、克劳德·香农等人提出了人工智能的研究议程。达特茅斯会议的几位主要参与者也被后人誉为“人工智能七侠”，包括约翰·麦卡锡、马文·明斯基、阿伦·纽厄尔、赫伯特·西蒙、克劳德·香农、奥利弗·塞尔弗里奇和内森·罗切斯特，他们为人工智能的发展奠定了基础，其中有五个人获得了图灵奖。&lt;/li&gt;
&lt;li&gt;1957年，弗兰克·罗森布拉特提出了感知机模型，这是最早的神经网络模型之一，用于解决二分类问题。&lt;/li&gt;
&lt;li&gt;1958年，约翰·麦卡锡开发了 LISP 编程语言，这种语言特别适合于符号处理，成为早期 AI 研究的主要编程语言。&lt;/li&gt;
&lt;li&gt;1980年，卡内基梅隆大学为 DEC 公司设计了一个名为 XCON 的专家系统，每年为公司省下约四千万美元的开销。由于专家系统在 DEC 公司取得了巨大的成功，全世界有很多公司都开始研发和应用专家系统。&lt;/li&gt;
&lt;li&gt;1982年，物理学家约翰·霍普菲尔德证明一种新型的神经网络（后被称为“Hopfield网络”）能够用一种全新的方式学习和处理信息。&lt;/li&gt;
&lt;li&gt;1986年，杰弗里·辛顿及其团队提出了反向传播算法，解决了训练深层神经网络的难题，神经网络相关的方法论再次受到关注。这一突破使得神经网络在各类任务中表现优异，成为机器学习的重要工具。&lt;/li&gt;
&lt;li&gt;1997年，IBM 开发的国际象棋程序 Deep Blue 战胜了国际象棋世界冠军传奇棋手卡斯帕罗夫。&lt;/li&gt;
&lt;li&gt;2011年，IBM 开发的能够使用自然语言来回答问题的人工智能系统在综艺节目《危险边缘》中打败了最高奖金得主布拉德·鲁特尔和连胜纪录保持者肯·詹宁斯，赢得了100万美元的奖金。&lt;/li&gt;
&lt;li&gt;2012年，杰弗里·辛顿和他的学生在 ImageNet 竞赛中使用深度卷积神经网络（CNN）获得了巨大成功，标志着深度学习的兴起。随后，深度学习在图像识别、语音识别、自然语言处理等领域取得了显著进展。&lt;/li&gt;
&lt;li&gt;2016年，谷歌 DeepMind开发的 AlphaGo 战胜了围棋世界冠军李世乭，展示了深度学习和蒙特卡洛树搜索技术的强大能力。&lt;/li&gt;
&lt;li&gt;2017年，谷歌的研究人员提出了 Transformer 模型，彻底改变了自然语言处理领域。Transformer 模型基于自注意力机制，大大提高了序列建模的效率和效果。后面的 BERT、GPT 等模型都是基于 Transformer 架构。&lt;/li&gt;
&lt;li&gt;2018年，OpenAI 开发的 AI 系统 OpenAI Five 在多人在线战术竞技游戏《Dota 2》中击败了半专业和专业玩家团队。&lt;/li&gt;
&lt;li&gt;2019年，OpenAI 发布了 GPT-2，一个具有15亿参数的语言模型，展示了生成高质量自然语言文本的能力。同年，DeepMind 开发的 AlphaStar 在实时战略游戏《星际争霸II》中击败了人类顶级选手。&lt;/li&gt;
&lt;li&gt;2020年，OpenAI 发布了 GPT-3，一个具有1750亿参数的语言模型，是当时最大的语言模型，GPT-3 在生成自然语言文本、翻译、编写代码等任务中表现出色，推动了自然语言处理和生成模型的进一步发展。同年，DeepMind 的 AlphaFold 2 在蛋白质结构预测的 CASP 竞赛中取得了突破性成果，预测精度达到了实验级别，这一成就被认为是生物学领域的重大突破，有望推动药物研发和生物学研究的进步。&lt;/li&gt;
&lt;li&gt;2021年，OpenAI 发布了 DALL-E，这个模型能够根据文本描述生成图像，展示了生成模型在多模态学习和图像生成中的潜力。同年，OpenAI 发布了 Codex，一个可以理解和生成代码的语言模型，是 Copilot 的核心技术。&lt;/li&gt;
&lt;li&gt;2023年，OpenAI 推出了基于 GPT-4 的 ChatGPT，可以进行更自然、更流畅的对话。随后，国内外行业巨头纷纷入局大模型，各种各样的大模型应用遍地开花。&lt;/li&gt;
&lt;li&gt;2024年，OpenAI 在 DALL-E 的基础上推出了 Sora，一个可以通过文本描述生成视频的人工智能模型。几天后，一家名为 Cognition AI 的初创公司推出了第一位 AI 软件工程师 Devin。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;人工智能自图灵测试和达特茅斯会议以来，经历了多次的高潮和低谷。目前，受益于由于计算机运算和存储能力的飞速提升，从前很多难以实现的技术都成为了现实，而人工智能也又一次被推到了风口浪尖。从早期的专家系统到今天的大模型，AI 技术还在不断的演进和突破。无论是 AlphaGo 攻占了人类智慧的最后高地，还是自动驾驶技术的逐渐普及，又或是人工智能内容生成（AIGC）的广泛应用，你可以清楚的感受到，人工智能正在改变我们的生活。&lt;/p&gt;
&lt;h4&gt;什么是机器学习&lt;/h4&gt;
&lt;p&gt;人类通过记忆和归纳这两种方式进行学习，通过记忆可以积累单个事实，使用归纳可以从旧的事实推导出新的事实。机器学习是赋予机器从数据中学习知识的能力，这个过程并不需要人类的帮助（给出明确的规则），也就是说机器学习关注的是从数据中学习一种模式（pattern），即便数据本身存在问题（噪声），这也是机器学习算法和传统算法最根本的区别。传统的算法需要计算机被告知如何从复杂系统中找到答案，算法利用计算机的运算能力去寻找最佳结果。传统算法最大的缺点就是人类必须首先知道最佳的解决方案是什么，而机器学习算法并不需要人类告诉模型最佳解决方案，取而代之的是，我们提供和问题相关的示例数据。&lt;/p&gt;
&lt;p&gt;我们可以把需要计算机解决的问题分为四类，分类的依据一方面是输入是精确的还是模糊的，另一方面是输出是最优的还是满意的，我们可以制作出如下所示的表格。可以看出，传统算法擅长解决的只有第一类问题，而机器学习比较擅长解决第三类和第四类问题；第二类问题基本属于 NP 问题（非确定性多项式时间问题），包括旅行经销商问题、图着色问题、集合覆盖问题等，我们通常会采用启发式算法（如模拟退火算法、遗传算法等）或近似算法来解决这类问题，当然机器学习算法也可以为这类问题提供满意解。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/98a5281f4e.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当然，机器学习算法并非完美到无懈可击。在通过数据训练模型时，我们需要使用预处理和清洗后的数据，如果数据本身质量非常糟糕，我们也很难训练出一个好的模型，这也是我们经常说到的 GIGO（Garbage In Garbage Out）。如果要使用机器学习，我们还得确定变量之间是否存在某种关系，机器学习无法处理不存在任何关系的数据。大多数时候，机器学习模型输出的是一系列的数字和指标，需要人类解读这些数字和指标并做出决策，判定模型的好坏并决定模型如何在实际的业务场景中落地。通常，我们用来训练模型的数据会存在噪声数据（noisy data），很多机器学习模型对噪声都非常敏感，如果不能处理好这些噪声数据，我们就不太容易得到好的模型。&lt;/p&gt;
&lt;h4&gt;机器学习的应用领域&lt;/h4&gt;
&lt;p&gt;即使对于机器学习这个概念不那么熟悉，但是机器学习的成果已经广泛渗透到了生产生活的各个领域，下面的这些场景对于你来说一定不陌生。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景1&lt;/strong&gt;：搜索引擎会根据搜索和使用习惯，优化下一次搜索的结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景2&lt;/strong&gt;：电商网站会根据你的访问历史自动推荐你可能感兴趣的商品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景3&lt;/strong&gt;：金融类产品会通过你最近的金融活动信息综合评定你的贷款申请。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景4&lt;/strong&gt;：视频和直播平台会自动识别图片和视频中有没有不和谐的内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景5&lt;/strong&gt;：智能家电和智能汽车会根据你的语音指令做出相应的动作。&lt;/p&gt;
&lt;p&gt;简单的总结一下，机器学习可以应用到但不限于以下领域：&lt;/p&gt;
&lt;h5&gt;1. &lt;strong&gt;图像识别与计算机视觉&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;计算机视觉（Computer Vision）是机器学习的一个重要应用领域，涉及到使机器能够理解和处理图像和视频数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人脸识别&lt;/strong&gt;：通过深度学习模型识别图片中的人脸，如安全监控、手机解锁等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动驾驶&lt;/strong&gt;：自动驾驶汽车使用计算机视觉来识别道路标志、行人、其他车辆、交通信号灯等，从而实现自主导航。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;医疗影像分析&lt;/strong&gt;：机器学习应用于X光片、MRI扫描和CT图像的分析，帮助医生发现疾病（如癌症、脑卒中等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图像分类与标注&lt;/strong&gt;：自动为图像打标签，比如在社交媒体平台上自动识别图片中的物品和场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. &lt;strong&gt;自然语言处理（NLP）&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;自然语言处理是机器学习在文本和语音数据上的应用，目的是让计算机能够理解和生成自然语言。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;语音识别&lt;/strong&gt;：如智能助手（Siri、Google Assistant）通过语音识别技术将语音转换为文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器翻译&lt;/strong&gt;：Google翻译、百度翻译等应用，使用机器学习技术进行语言之间的自动翻译。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情感分析&lt;/strong&gt;：分析社交媒体帖子、评论等文本数据的情感倾向（积极、消极、中立）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文本生成&lt;/strong&gt;：自动生成文章或新闻（如GPT系列），为用户提供文章自动写作、智能客服等功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聊天机器人&lt;/strong&gt;：例如客服机器人，通过自然语言处理技术与用户进行对话。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3. &lt;strong&gt;推荐系统&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;推荐系统利用用户行为数据来预测用户可能感兴趣的物品或服务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;电子商务&lt;/strong&gt;：如亚马逊、淘宝等平台根据用户的浏览和购买记录推荐商品。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;影视推荐&lt;/strong&gt;：如Netflix、YouTube等根据用户观看历史推荐电影、视频和节目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社交网络推荐&lt;/strong&gt;：例如，Facebook和Twitter根据用户的兴趣推荐朋友、帖子或广告。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;4. &lt;strong&gt;金融领域&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;机器学习在金融领域的应用主要体现在风险管理、预测和自动化交易等方面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信用评分&lt;/strong&gt;：银行和金融机构利用机器学习模型评估借款人的信用风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;欺诈检测&lt;/strong&gt;：通过分析交易模式，机器学习模型能够检测到潜在的欺诈行为（如信用卡欺诈、洗钱行为）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法交易&lt;/strong&gt;：利用机器学习算法进行股票和其他金融资产的自动化交易，实时根据市场数据进行决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;投资组合管理&lt;/strong&gt;：使用机器学习模型进行资产配置和投资组合优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;5. &lt;strong&gt;医疗健康&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;机器学习在医疗行业中被广泛应用，尤其是在疾病预测、个性化治疗和医疗图像分析等方面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;疾病预测&lt;/strong&gt;：使用机器学习算法预测疾病的发生概率，例如糖尿病、心脏病等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;个性化医疗&lt;/strong&gt;：基于患者的历史健康数据、基因数据等，机器学习可以帮助提供个性化的治疗方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;药物发现&lt;/strong&gt;：通过大数据分析，机器学习能够加速新药的发现过程，例如通过预测化合物的药效来筛选潜在药物。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康监测&lt;/strong&gt;：利用可穿戴设备（如智能手表、健康追踪器）收集的数据，机器学习可以监测健康状况并预测疾病风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;6. &lt;strong&gt;智能交通与自动驾驶&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;自动驾驶技术和智能交通系统都依赖于机器学习技术。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动驾驶汽车&lt;/strong&gt;：自动驾驶依赖于机器学习来识别周围环境（如行人、交通信号、其他车辆等），并作出决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交通预测&lt;/strong&gt;：根据交通流量、天气、节假日等因素，机器学习可以预测路况、交通拥堵情况，优化路线规划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;车联网（V2X）&lt;/strong&gt;：车与车、车与基础设施之间的通信系统，利用机器学习进行数据分析和决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;7. &lt;strong&gt;智能家居与物联网&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;物联网（IoT）设备可以通过机器学习实现自动化和智能化操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;智能家居&lt;/strong&gt;：如智能音响（Amazon Echo、Google Home）和智能家电（智能空调、智能冰箱等），通过语音和传感器数据自动调整设备设置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境监控&lt;/strong&gt;：基于机器学习的传感器数据分析，可以监测室内空气质量、温度等参数，并自动调节环境条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能安防&lt;/strong&gt;：监控摄像头通过人脸识别和行为识别技术，识别潜在的安全威胁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;8. &lt;strong&gt;体育分析&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;机器学习也在体育领域得到应用，特别是在数据分析和比赛策略优化方面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运动员表现分析&lt;/strong&gt;：通过分析运动员的训练数据、比赛数据等，机器学习可以帮助教练制定个性化的训练计划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;比赛结果预测&lt;/strong&gt;：通过历史数据和实时数据，机器学习可以预测比赛的结果、球队表现等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟体育教练&lt;/strong&gt;：利用机器学习技术为运动员提供数据驱动的训练建议和反馈。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;9. &lt;strong&gt;制造与工业自动化&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;机器学习在工业和制造领域主要用于优化生产流程、提高效率和减少故障。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预测性维护&lt;/strong&gt;：机器学习模型通过分析设备的历史数据，预测设备可能发生故障的时间，从而提前进行维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;质量控制&lt;/strong&gt;：通过对生产数据进行分析，机器学习可以自动识别生产过程中出现的质量问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化生产&lt;/strong&gt;：机器学习可以帮助机器人根据环境和任务自动调节生产流程，提高生产效率和质量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;机器学习的分类&lt;/h4&gt;
&lt;p&gt;机器学习模型可以按照不同的标准进行分类，主要包括以下几种：&lt;/p&gt;
&lt;h5&gt;按照学习方式分类&lt;/h5&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/4c6d51b6c8.webp&quot; style=&quot;zoom:32%;&quot;&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;监督学习（Supervised Learning）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;回归（Regression）：用于预测连续值的模型，例如线性回归、Ridge 回归、Lasso 回归等。&lt;/li&gt;
&lt;li&gt;分类（Classification）：用于预测离散类别的模型，例如逻辑回归、支持向量机、决策树、随机森林、k近邻、朴素贝叶斯等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无监督学习（Unsupervised Learning）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚类（Clustering）：用于将数据分组的模型，例如K均值聚类、层次聚类、DBSCAN 等。&lt;/li&gt;
&lt;li&gt;降维（Dimensionality Reduction）：用于减少特征数量的模型，例如主成分分析（PCA）、线性判别分析（LDA）、t分布随机近邻嵌入（t-SNE）等。&lt;/li&gt;
&lt;li&gt;关联规则学习（Association Rule Learning）：用于发现数据集中项之间关系的模型，例如 Apriori 算法、Eclat 算法等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;半监督学习（Semi-Supervised Learning）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结合了监督学习和无监督学习的方法，使用大量未标记的数据和少量标记的数据来构建模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;强化学习（Reinforcement Learning）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于奖励机制的学习方法，例如Q-Learning、深度Q网络（DQN）、策略梯度方法等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;按照模型的复杂度分类&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;线性模型（Linear Models）：如线性回归、逻辑回归。&lt;/li&gt;
&lt;li&gt;非线性模型（Non-linear Models）：如带核函数的支持向量机（SVM with Kernel）、神经网络。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;按照模型的结构分类&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;生成模型（Generative Models）：可以生成新的数据点，如朴素贝叶斯、隐马尔可夫模型（HMM）。&lt;/li&gt;
&lt;li&gt;判别模型（Discriminative Models）：仅用于分类或回归，如逻辑回归、支持向量机。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果不理解上面的分类也没有关系，后续的课程中我们会覆盖到上面说到的很多算法，我们会通过讲解算法的原理、适用场景、优缺点和代码实现来帮助大家掌握这些算法并将其应用于解决实际问题。&lt;/p&gt;
&lt;h4&gt;机器学习的步骤&lt;/h4&gt;
&lt;p&gt;机器学习的实施步骤通常分为多个阶段，从问题定义、数据准备到模型部署和维护，每个步骤都非常重要，具体如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定义问题&lt;/strong&gt;。首先我们需要做业务理解，明确问题的性质和类型，这个会直接影响到后续的数据收集、特征工程、选择算法以及评估指标的确定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据收集&lt;/strong&gt;。机器学习模型的训练需要大量的数据，这些数据可能包含结构化数据（数据库、Excel 电子表格等）、非结构化数据（文本、图像、音频、视频等）、其他类型的数据集。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据清洗&lt;/strong&gt;。数据清洗要确保数据质量高且适合模型训练，具体包括：缺失值和异常值处理、数据标准化和归一化、特殊编码、特征工程等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据划分&lt;/strong&gt;。为了评估机器学习模型的泛化能力，需要将数据划分为训练集和测试集。除此以外，还可能使用交叉验证的方式将数据分成多个子集，每个子集轮流作为验证集，从而对模型的超参数进行调整。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模型选择&lt;/strong&gt;。针对分类问题、回归问题、聚类问题、深度学习，我们选择的机器学习算法或模型是不一样的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模型训练&lt;/strong&gt;。使用训练集对模型进行训练，使模型能够学习到输入特征与目标之间的关系。此外，每个机器学习算法都有超参数，这些参数需要根据数据和任务来调优。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模型评估&lt;/strong&gt;。模型评估主要的目标是确定模型在新数据（测试集）上的表现，确保模型没有出现过拟合（overfitting）或欠拟合（underfitting），如下图所示。欠拟合会导致模型的预测效果糟糕，而过拟合会导致模型缺乏泛化能力，即在测试集和新数据上表现欠佳。当然，为了提高模型的性能或适应性，可能还要通过正则化、集成学习、算法调整等方式进行调优。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/bbc6584585.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模型部署&lt;/strong&gt;。当你对模型的性能感到满意时，可以将模型部署到生产环境中，进行实时预测或批量预测。我们通过监控模型在实际应用中的表现，确保其持续保持较好的预测效果。如果模型性能下降，可能需要重新训练或调整。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模型维护&lt;/strong&gt;。机器学习模型不是一成不变的，随着时间的推移，模型可能需要通过重新训练、增量学习等方式来维持其性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果对上面出现的概念不是很理解，可以先跳过去，我们会在遇到这些问题的时候展开讲解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;第一次机器学习&lt;/h4&gt;
&lt;p&gt;下面，我们用一个极为简单的例子带大家开启第一次机器学习之旅。当然，作为本课的第一个案例，我们并没有严格执行上面提到的机器学习的实施步骤，我们只想通过这个例子对机器学习有一个感性的认知，消除机器学习在很多人心中的“神秘感”。为了研究某城市某类消费者每月收入和每月网购支出的关系，我们收集到了50条样本数据（后面我们统称为历史数据），分别保存在两个列表中，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 每月收入
x = [9558, 8835, 9313, 14990, 5564, 11227, 11806, 10242, 11999, 11630,
     6906, 13850, 7483, 8090, 9465, 9938, 11414, 3200, 10731, 19880,
     15500, 10343, 11100, 10020, 7587, 6120, 5386, 12038, 13360, 10885,
     17010, 9247, 13050, 6691, 7890, 9070, 16899, 8975, 8650, 9100,
     10990, 9184, 4811, 14890, 11313, 12547, 8300, 12400, 9853, 12890]
# 每月网购支出
y = [3171, 2183, 3091, 5928, 182, 4373, 5297, 3788, 5282, 4166,
     1674, 5045, 1617, 1707, 3096, 3407, 4674, 361, 3599, 6584,
     6356, 3859, 4519, 3352, 1634, 1032, 1106, 4951, 5309, 3800,
     5672, 2901, 5439, 1478, 1424, 2777, 5682, 2554, 2117, 2845,
     3867, 2962,  882, 5435, 4174, 4948, 2376, 4987, 3329, 5002]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们假设月收入和月网购支出都来自于正态总体，接下来我们可以通过计算皮尔逊相关系数来判定两组数据是否存在相关性，代码如下所示。如果对相关性和相关系数不理解，可以移步到我的&lt;a href=&quot;https://www.zhihu.com/column/c_1620074540456964096&quot;&gt;《数据思维和统计思维》&lt;/a&gt;专栏看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

np.corrcoef(x, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1.        , 0.94862936],
       [0.94862936, 1.        ]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出该城市该类人群的月收入和月网购支出之间存在强正相关性（相关系数为&lt;code&gt;0.94862936&lt;/code&gt;）。当然，计算皮尔逊相关系数也可以使用 scipy 中的&lt;code&gt;stats.pearsonr&lt;/code&gt;函数，该函数在计算相关系数的同时，还会给出统计检验的 P 值，我们可以根据 P 值来判定相关性是否显著，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scipy import stats

stats.pearsonr(x, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PearsonRResult(statistic=0.9486293572644154, pvalue=1.2349851929268588e-25)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面，我们已经确认了月收入和月网购支出之间存在强相关性，那么一个很自然的想法就是通过某人的月收入来预测他的月网购支出，反过来当然也是可以的。为了做到这一点，可以充分利用我们收集到的历史数据，让计算机通过对数据的“学习”获得相应的知识，从而实现对未知状况的预测。我们可以将上述数据中的 $\small{X}$ 称为自变量或特征（&lt;em&gt;feature&lt;/em&gt;），将 $\small{y}$ 称为因变量或目标值（&lt;em&gt;target&lt;/em&gt;），&lt;strong&gt;机器学习的关键就是要通过历史数据掌握如何实现从特征到目标值的映射&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;kNN算法&lt;/h5&gt;
&lt;p&gt;要通过月收入预测月网购支出，一个最朴素的想法就是将历史数据做成一个字典，月收入作为字典中的键，月网购支出作为对应的值，这样就可以通过查字典的方式通过收入查到支出，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sample_data = {key: value for key, value in zip(x, y)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，输入的月收入未必在字典中对应的键，没有键就无法获取对应的值，这个时候我们可以找到跟输入的月收入最为接近的 k 个键，对这 k 个键对应的值求平均，用这个平均值作为对月网购支出的预测。这里的方法就是机器学习中最为简单的 k 最近邻算法（kNN），它是一种用于分类和回归的非参数统计方法，下面我们用原生 Python 代码来实现 kNN 算法，暂时不使用 NumPy、SciPy、Scikit-Learn 这样的三方库，主要帮助大家理解算法的原理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import heapq
import statistics


def predict_by_knn(history_data, param_in, k=5):
    &quot;&quot;&quot;用kNN算法做预测
    :param history_data: 历史数据
    :param param_in: 模型的输入
    :param k: 邻居数量（默认值为5）
    :return: 模型的输出（预测值）
    &quot;&quot;&quot;
    neighbors = heapq.nsmallest(k, history_data, key=lambda x: (x - param_in) ** 2)
    return statistics.mean([history_data[neighbor] for neighbor in neighbors])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，我们用 Python 中&lt;code&gt;heapq&lt;/code&gt;模块的&lt;code&gt;nsmallest&lt;/code&gt;来找到历史数据中最小的 k 个元素，通过该函数的&lt;code&gt;key&lt;/code&gt;参数，我们界定了最小指的是跟输入的参数&lt;code&gt;param_in&lt;/code&gt;误差最小，由于误差有正数和负数，所以通常都需要求平方或者取绝对值。对于算术平均值的计算，我们使用了 Python 中&lt;code&gt;statistics&lt;/code&gt;模块的&lt;code&gt;mean&lt;/code&gt;函数，对&lt;code&gt;heapq&lt;/code&gt;和&lt;code&gt;statistics&lt;/code&gt;模块不熟悉的可以看看 Python 的官方文档，此处不再赘述。接下来，我们用上面的函数来预测月网购支出，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incomes = [1800, 3500, 5200, 6600, 13400, 17800, 20000, 30000]
for income in incomes:
    print(f&apos;月收入: {income:&amp;gt;5d}元, 月网购支出: {predict_by_knn(sample_data, income):&amp;gt;6.1f}元&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;月收入:  1800元, 月网购支出:  712.6元
月收入:  3500元, 月网购支出:  712.6元
月收入:  5200元, 月网购支出:  936.0元
月收入:  6600元, 月网购支出: 1487.0元
月收入: 13400元, 月网购支出: 5148.6元
月收入: 17800元, 月网购支出: 6044.4元
月收入: 20000元, 月网购支出: 6044.4元
月收入: 30000元, 月网购支出: 6044.4元
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的输出，我们可以看出 kNN 算法的一个弊端，样本数据的缺失或者不均衡情况会导致预测结果非常糟糕。上面月收入&lt;code&gt;17800&lt;/code&gt;元、&lt;code&gt;20000&lt;/code&gt;元、&lt;code&gt;30000&lt;/code&gt;元的网购支出预测值都是&lt;code&gt;6044.4&lt;/code&gt;元，那是因为历史数据中月收入的最大值是&lt;code&gt;19880&lt;/code&gt;，所以跟它们最近的 k 个邻居是完全相同的，所以从历史数据预测出的网购支出也是相同的；同理，月收入&lt;code&gt;1800&lt;/code&gt;元跟月收入&lt;code&gt;3500&lt;/code&gt;元的网购支出预测值也是相同的，因为跟&lt;code&gt;1800&lt;/code&gt;元最近的 k 个邻居和跟&lt;code&gt;3500&lt;/code&gt;元最近的 k 个邻居也是完全相同的。当然，上面我们给出的 kNN 算法实现还有其他的问题，大家应该不难发现&lt;code&gt;predict_by_knn&lt;/code&gt;函数中，我们每次找寻最近的 k 个邻居时，都要将整个历史数据遍历一次，如果数据集体量非常大，那么这个地方就会产生很大的开销。&lt;/p&gt;
&lt;h5&gt;回归模型&lt;/h5&gt;
&lt;p&gt;我们换一种思路来预测网购支出，我们将月收入和月网购支出分别作为横纵坐标来绘制散点图。既然网购支出跟月收入之间存在强正相关，这就意味着可以找一条直线来拟合这些历史数据点，我们把这条直线的方程 $\small{y = aX + b}$ 称为回归方程或回归模型，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/c4225621b1.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;现在，我们的问题就变成了如何利用收集到的历史数据计算出回归模型的斜率 $\small{a}$ 和截距 $\small{b}$ 。为了评价回归模型的好坏，也就是我们计算出的斜率和截距是否理想，我们可以先定义评判标准，一个简单且直观的评判标准就是我们将月收入 $\small{X}$ 带入回归模型，计算出月网购支出的预测值 $\small{\hat{y}}$ ，预测值 $\small{\hat{y}}$ 和真实值 $\small{y}$ 之间的误差越小，说明我们的回归模型越理想。之前我们提到过，计算误差的地方通常都需要取绝对值或者求平方，我们可以用误差平方的均值来作为评判标准，通常称之为均方误差（MSE），如下所示。&lt;/p&gt;
&lt;p&gt;$$
MSE = \frac{1}{n}{\sum_{i=1}^{n}(y_{i} - \hat{y_{i}})^{2}}
$$&lt;/p&gt;
&lt;p&gt;根据上面的公式，我们可以写出计算均方误差的函数，通常称之为损失函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import statistics


def get_loss(X_, y_, a_, b_):
    &quot;&quot;&quot;损失函数
    :param X_: 回归模型的自变量
    :param y_: 回归模型的因变量
    :param a_: 回归模型的斜率
    :param b_: 回归模型的截距
    :return: MSE（均方误差）
    &quot;&quot;&quot;
    y_hat = [a_ * x + b_ for x in X_]
    return statistics.mean([(v1 - v2) ** 2 for v1, v2 in zip(y_, y_hat)])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能让 MSE 达到最小的 $\small{a}$ 和 $\small{b}$ ，我们称之为回归方程的最小二乘解，接下来的工作就是要找到这个最小二乘解。简单的说，我们要将可能的 $\small{a}$ 和 $\small{b}$ 带入损失函数，看看什么样的 $\small{a}$ 和 $\small{b}$ 能让损失函数取到最小值。如果对 $\small{a}$ 和 $\small{b}$ 的取值一无所知，我们可以通过不断产生随机数的方式来寻找 $\small{a}$ 和 $\small{b}$ ，这种方法称为蒙特卡洛模拟，通俗点说就是随机瞎蒙，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

# 先将最小损失定义为一个很大的值
min_loss, a, b = 1e12, 0, 0

for _ in range(100000):
    # 通过产生随机数的方式获得斜率和截距
    _a, _b = random.random(), random.uniform(-2000, 2000)
    # 带入损失函数计算回归模型的MSE
    curr_loss = get_loss(x, y, _a, _b)
    if curr_loss &amp;lt; min_loss:
        # 找到更小的MSE就记为最小损失
        min_loss = curr_loss
        # 记录下当前最小损失对应的a和b
        a, b = _a, _b

print(f&apos;MSE = {min_loss}&apos;)
print(f&apos;{a = }, {b = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码进行了&lt;code&gt;100000&lt;/code&gt;次的模拟， $\small{a}$ 的值在 $\small[0, 1)$ 范围随机选择，$\small{b}$ 的值在 $\small{[-2000, 2000)}$ 范围随机选择的，下面是在我的电脑上跑出来的结果。大家可以把自己蒙特卡罗模拟的结果分享到评论区，看看谁的运气更好，找到了让误差更小的 $\small{a}$ 和 $\small{b}$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MSE = 270690.1419424315
a = 0.4824040159203802, b = -1515.0162977046068
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于数学知识比较丰富的小伙伴，我们可以将回归模型带入损失函数，由于 $\small{X}$ 和 $\small{y}$ 是已知的历史数据，那么损失函数其实是一个关于 $\small{a}$ 和 $\small{b}$ 的二元函数，如下所示。&lt;/p&gt;
&lt;p&gt;$$
MSE = f(a, b) = \frac{1}{n}{\sum_{i=1}^{n}(y_{i} - (ax_{i} + b))^{2}}
$$&lt;/p&gt;
&lt;p&gt;根据微积分的极值定理，我们可以对 $\small{f(a, b)}$ 求偏导数，并令其等于0，这样我们就可以计算让 $\small{f(a, b)}$ 取到最小值的 $\small{a}$ 和 $\small{b}$ 分别是多少，即：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial{f(a, b)}}{\partial{a}} = -\frac{2}{n}\sum_{i=1}^{n}x_{i}(y_{i} - x_{i}a - b) = 0
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial{f(a, b)}}{\partial{b}} = -\frac{2}{n}\sum_{i=1}^{n}(y_i - x_{i}a - b) = 0
$$&lt;/p&gt;
&lt;p&gt;求解上面的方程组可以得到：&lt;/p&gt;
&lt;p&gt;$$
a = \frac{\sum(x_{i} - \bar{x})(y_{i} - \bar{y})}{\sum(x_{i} - \bar{x})^{2}}
$$&lt;/p&gt;
&lt;p&gt;$$
b = \bar{y} - a\bar{x}
$$&lt;/p&gt;
&lt;p&gt;需要说明的是，如果回归模型本身比较复杂，不像线性模型 $\small{y = aX + b}$ 这么简单，可能没有办法像上面那样直接求解方程，而是要通过其他的方式来找寻极值点，这个我们会在后续的内容中会为大家讲解。回到刚才的问题，我们可以通过上面的公式计算出 $\small{a}$ 和 $\small{b}$ 的值，为了运算方便，下面直接使用了 NumPy 中的函数，因为 NumPy 中的运算都是矢量化的，通常不需要我们自己写循环结构，对 NumPy 不熟悉的小伙伴，可以移步到我的&lt;a href=&quot;https://www.zhihu.com/column/c_1217746527315496960&quot;&gt;《基于Python的数据分析》&lt;/a&gt;专栏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

x_bar, y_bar = np.mean(x), np.mean(y)
a = np.dot((x - x_bar), (y - y_bar)) / np.sum((x - x_bar) ** 2)
b = y_bar - a * x_bar
print(f&apos;{a = }, {b = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 0.482084452824066, b = -1515.2028590756745
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，NumPy 库中还有封装好的函数可以帮我们完成参数拟合，你可以使用&lt;code&gt;linalg&lt;/code&gt;模块的&lt;code&gt;lstsq&lt;/code&gt;函数来计算最小二乘解，也可以使用&lt;code&gt;polyfit&lt;/code&gt;函数或&lt;code&gt;Polynomial&lt;/code&gt;类型的&lt;code&gt;fit&lt;/code&gt;方法来获得最小二乘解，代码如下所示。大家可以看看，跟上面求偏导数找极值点获得的结果是否相同。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b = np.polyfit(x, y, deg=1)
print(f&apos;{a = }, {b = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from numpy.polynomial import Polynomial

b, a = Polynomial.fit(x, y, deg=1).convert().coef
print(f&apos;{a = }, {b = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;deg&lt;/code&gt;参数代表多项式的最高次项是几次项，&lt;code&gt;deg=1&lt;/code&gt;说明我们使用的是线性回归模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;小结&lt;/h4&gt;
&lt;p&gt;首先，希望本篇内容能对大家了解机器学习有那么一点点帮助；其次，希望通过本章向大家传达一个理念，自己手撕机器学习的代码比直接调用三方库弄出一个结果来其实更有意思，它会让你对算法原理、应用场景、利弊情况等都有一个更好的认知。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;82 k最近邻算法&lt;/h2&gt;
&lt;p&gt;k 最近邻算法（kNN）是一种用于分类和回归的非参数统计方法，由美国统计学家伊芙琳·费克斯和小约瑟夫·霍奇斯于 1951 年提出。kNN 算法的原理是从历史数据中找到 $\small{k}$ 个跟新输入的实例最邻近的实例，根据它们中的多数所属的类别来对新实例进行分类或者输出新实例的目标值，这种算法我们在前面已经为大家做了简单的展示。与主流的机器学习算法不同，k 最近邻算法没有显式的学习训练过程，它用的是“近朱者赤，近墨者黑”这样一种简单朴素的思想来实现分类或回归。k 最近邻算法有两个关键问题，第一个是 $\small{k}$ 值如何选择，即用多少个最近邻来判定新实例所属的类别或确定其目标值；第二个是如何判定两个实例是近还是远，这里就涉及到度量距离的问题。&lt;/p&gt;
&lt;h4&gt;距离的度量&lt;/h4&gt;
&lt;p&gt;我们可以用距离（distance）来衡量特征空间中两个实例之间的相似度，常用的距离度量包括闵氏距离、马氏距离、余弦距离、编辑距离等。闵氏距离全称闵可夫斯基距离（Minkowski Distance），对于两个 $\small{n}$ 维向量 $\small{\mathbf{x}=(x_{1}, x_{2}, \cdots, x_{n})}$ 和 $\small{\mathbf{y}=(y_{1}, y_{2}, \cdots, y_{n})}$ ，它们之间的距离可以定义为：&lt;/p&gt;
&lt;p&gt;$$
d(\mathbf{x}, \mathbf{y}) = (\sum_{i=1}^{n}{\vert x_{i} - y_{i} \rvert}^{p})^{\frac{1}{p}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{p \ge 1}$ ，虽然 $\small{p \lt 1}$ 可以计算，但不再严格满足距离的定义，通常不被视为真正的距离。&lt;/p&gt;
&lt;p&gt;当 $\small{p = 1}$ 时，闵氏距离即&lt;strong&gt;曼哈顿距离&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
d(\mathbf{x}, \mathbf{y}) = \sum_{i=1}^{n} \lvert x_{i} - y_{i} \rvert
$$&lt;/p&gt;
&lt;p&gt;当 $\small{p = 2}$ 时，闵氏距离即&lt;strong&gt;欧几里得距离&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
d(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{i=1}^{n}(x_{i} - y_{i})^{2}}
$$&lt;/p&gt;
&lt;p&gt;当 $\small{p \to \infty}$ 时，闵氏距离即&lt;strong&gt;切比雪夫距离&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
d(\mathbf{x}, \mathbf{y}) = \underset{i}{max}(\lvert x_{i} - y_{i} \rvert)
$$&lt;/p&gt;
&lt;p&gt;其他的距离度量方式我们等用到的时候再为大家介绍。在使用 k 最近邻算法做分类时，我们的数据集通常都是数值型数据，此时直接使用欧几里得距离是一个不错的选择。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/397794f597.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;数据集介绍&lt;/h4&gt;
&lt;p&gt;接下来为大家隆重介绍一下我们后续会使用到的一个重要的数据集——鸢尾花数据集（iris dataset）。鸢尾花数据集是机器学习领域中最著名、最经典的数据集之一，由植物学家 &lt;em&gt;Edgar S. Anderson&lt;/em&gt; 在加拿大魁北克加斯帕半岛采集，由英国统计学家 &lt;em&gt;Ronald A. Fisher&lt;/em&gt; 于 1936 年在他的论文*《The Use of Multiple Measurements in Taxonomic Problems》*中首次引入，被广泛用于机器学习算法的入门和实验。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/1ae7882f85.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;鸢尾花数据集共有 150 条样本，其中包含 3 种类型的鸢尾花，分别是山鸢尾（Iris setosa）、多彩鸢尾（Iris versicolor）和为吉尼亚鸢尾（Iris virginica），如上图所示，每种各有50条样本。样本数据包含了 4 个特征（features）和 1 个类别标签（class label），4 个特征分别是花萼长度（Sepal length）、花萼宽度（Sepal width）、花瓣长度（Petal length）、花瓣宽度（Petal width），都是以厘米为单位的正数，数据集中的类别标签有 0、1、2 三个值，对应上面提到的三种鸢尾花类型。&lt;/p&gt;
&lt;h5&gt;数据集的加载&lt;/h5&gt;
&lt;p&gt;我们可以通过著名的 Python 机器学习库 scikit-learn 来加载这个数据集，scikit-learn 包含了各种分类、回归、聚类算法，同时还提供了多层感知机、支持向量机、随机森林等模型，覆盖了从数据预处理、特征工程到模型训练、模型评估和参数调优等各项功能，如下图所示。我们推荐大家使用这个库来完成机器学习中的各种操作，scikit-learn 的&lt;a href=&quot;https://scikit-learn.org/stable/&quot;&gt;官方网站&lt;/a&gt;上面还提供了用户指南、API 文档和案例等内容，有兴趣的读者可以自行访问。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9c0e0ff15f.webp&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;由于我们后续的课程基本都会用到这个库，大家可以先通过下面的命令安装这个库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install scikit-learn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你已经打开了 IPython 或 Jupyter 但尚未安装 scikit-learn，可以通过下面的魔法指令来安装这个库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install scikit-learn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在安装完成之后，我们可以通过下面的代码加载鸢尾花数据集并查看该数据集的介绍。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris

# 加载鸢尾花数据集
iris = load_iris()
# 查看数据集的介绍
print(iris.DESCR)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们可以获得数据集中的特征和标签，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 特征（150行4列的二维数组，分别是花萼长、花萼宽、花瓣长、花瓣宽）
X = iris.data
# 标签（150个元素的一维数组，包含0、1、2三个值分别代表三种鸢尾花）
y = iris.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望更直观的查看鸢尾花数据集，我们可以用上面获得的特征和标签来创建一个 DataFrame 对象，有兴趣的读者可以自己试一试。&lt;/p&gt;
&lt;h5&gt;数据集的划分&lt;/h5&gt;
&lt;p&gt;通常，我们需要将原始数据划分成训练集和测试集，其中训练集是为了训练模型选择的数据，而测试集则是为了测试模型训练效果保留的数据。对于上面的鸢尾花数据集，我们可以选择 80% 的数据（120 条）作为训练集，保留 20% 的数据（30 条）作为测试集，下面的代码用 NumPy 实现了对数据集的划分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将特征和标签堆叠到同一个数组中
data = np.hstack((X, y.reshape(-1, 1)))
# 通过随机乱序函数将原始数据打乱
np.random.shuffle(data)
# 选择80%的数据作为训练集
train_size = int(y.size * 0.8)
train, test = data[:train_size], data[train_size:]
X_train, y_train = train[:, :-1], train[:, -1]
X_test, y_test = test[:, :-1], test[:, -1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，更简便的划分数据集的方式是使用 scikit-learn 封装好的函数&lt;code&gt;train_test_split&lt;/code&gt;，我们只需要传入特征和标签，指定好&lt;code&gt;train_size&lt;/code&gt;或&lt;code&gt;test_size&lt;/code&gt;就可以实现同样的操作。&lt;code&gt;train_test_split&lt;/code&gt;函数返回一个四元组，四个元素分别代表用于训练的特征、用于训练的标签、用于测试的特征和用于测试的标签。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中&lt;code&gt;train_test_split&lt;/code&gt;函数的&lt;code&gt;random_state&lt;/code&gt;参数可以理解成随机数的种子，如果你使用跟我相同的随机数种子，那么我们划分的训练集和测试集也是完全相同的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;kNN分类的实现&lt;/h4&gt;
&lt;p&gt;下面我们先不用 scikit-learn 而是用基础的数据科学库 NumPy 和 SciPy 来实现 kNN 算法，这样做的目的是帮助大家更好的理解算法原理，在此基础上我们再感受 scikit-learn 的强大并明白其中类、函数、参数等为什么要如此设定。&lt;/p&gt;
&lt;h5&gt;基于NumPy的实现&lt;/h5&gt;
&lt;p&gt;k 最近邻算法需要计算距离，下面我们先设计计算两个数据点欧式距离的函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np


def euclidean_distance(u, v):
    &quot;&quot;&quot;计算两个n维向量的欧式距离&quot;&quot;&quot;
    return np.sqrt(np.sum(np.abs(u - v) ** 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们设计根据邻居的标签为新数据生成标签的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scipy import stats


def make_label(X_train, y_train, X_one, k):
    &quot;&quot;&quot;
    根据历史数据中k个最近邻为新数据生成标签
    :param X_train: 训练集中的特征
    :param y_train: 训练集中的标签
    :param X_one: 待预测的样本（新数据）特征
    :param k: 邻居的数量
    :return: 为待预测样本生成的标签（邻居标签的众数）
    &quot;&quot;&quot;
    # 计算x跟每个训练样本的距离
    distes = [euclidean_distance(X_one, X_i) for X_i in X_train]
    # 通过一次划分找到k个最小距离对应的索引并获取到相应的标签
    labels = y_train[np.argpartition(distes, k - 1)[:k]]
    # 获取标签的众数
    return stats.mode(labels).mode
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;np.partition&lt;/code&gt;函数可以对数组进行一次划分，将 k 个比较小的元素放在数组的左边，n - k 个比较大的元素放在数组的右边，跟快速排序算法中做一次划分操作的效果是一样的。上面代码中的&lt;code&gt;np.argpartition&lt;/code&gt;函数是把 k 个较小元素的索引放在数组的左边，这样经过&lt;code&gt;[:k]&lt;/code&gt;切片操作和对&lt;code&gt;y_train&lt;/code&gt;的花式索引运算，就可以获得 k 个跟新数据距离较小的样本对应的标签。最后，我们通过 scipy 库的 stats 模块的&lt;code&gt;mode&lt;/code&gt;函数可以获得标签的众数，并用它作为我们给新数据预测的类别标签。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在完成上述准备工作后，用 k 最近邻做预测的函数就呼之欲出了，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def predict_by_knn(X_train, y_train, X_new, k=5):
    &quot;&quot;&quot;
    KNN算法
    :param X_train: 训练集中的特征
    :param y_train: 训练集中的标签
    :param X_new: 待预测的样本构成的数组
    :param k: 邻居的数量（默认值为5）
    :return: 保存预测结果（标签）的数组
    &quot;&quot;&quot;
    return np.array([make_label(X_train, y_train, X, k) for X in X_new])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用上面准备好的鸢尾花数据的训练集和测试集来做实验，看看我们的&lt;code&gt;predict_by_knn&lt;/code&gt;函数能否很好的运转起来。下面代码中的&lt;code&gt;y_pred&lt;/code&gt;是我们通过函数预测的30条鸢尾花的类型，我们跟&lt;code&gt;y_test&lt;/code&gt;做一个比较，就可以看到我们的预测效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;y_pred = predict_by_knn(X_train, y_train, X_test)
y_pred == y_test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这里的输出是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True, False,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的结果中有一个&lt;code&gt;False&lt;/code&gt;，这表示预测的标签跟真实的标签并不相同，是一个错误的预测结果，也就是说我们预测的准确率为 $\small{\frac{29}{30}}$ ，即&lt;code&gt;96.67%&lt;/code&gt;。当然，如果你划分训练集和测试集时跟我指定的&lt;code&gt;random_state&lt;/code&gt;参数不相同，这里得到的结果可能会跟我不同。&lt;/p&gt;
&lt;h5&gt;基于scikit-learn的实现&lt;/h5&gt;
&lt;p&gt;使用 scikit-learn 来实现 kNN 算法就要简单许多了，我们基本上只需要做三个动作就可以得到想要的结果，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neighbors import KNeighborsClassifier

# 创建模型
model = KNeighborsClassifier()
# 训练模型
model.fit(X_train, y_train)
# 预测结果
y_pred = model.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看看输出的结果，是不是跟我们自己手撕的代码完全一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;y_pred == y_test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True, False,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，scikit-learn 库考虑到的东西肯定比我们自己手撕的代码丰富得多，例如我们想知道模型预测的准确率（accuracy），可以使用下面的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.score(X_test, y_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.9666666666666667
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模型评估&lt;/h4&gt;
&lt;p&gt;当然，评价一个分类器的预测效果是否良好，不能只简单的看一下准确率，因为在类别不平衡的情况下，准确率很可能会误导你的判断。例如，我们的测试数据中维吉尼亚鸢尾花的样本数量非常小，那么即便我们的模型根本无法判断维吉尼亚鸢尾花，模型也会表现出很高的准确率。所以，我们还要考察查准率（精确率）、查全率（召回率）、F1 分数等指标，而&lt;strong&gt;混淆矩阵&lt;/strong&gt;则是用于详细展示分类模型性能的工具，适用于二分类和多分类任务，二分类问题的混淆矩阵如下所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为正类（Positive）&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为负类（Negative）&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为正类（Positive）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;True Positive（TP）&lt;/td&gt;
&lt;td&gt;False Negative（FN）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为负类（Negative）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;False Positive（FP）&lt;/td&gt;
&lt;td&gt;True Negative（TN）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们举一个例子来说明如何使用混淆矩阵并计算出分类模型的评估指标。假设某种医学检测系统的目标预测是否患有某种疾病，类别“正类”表示“患病”，类别“负类”表示“未患病”，1000个样本的测试结果如下所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为患病&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为未患病&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为患病&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80（TP）&lt;/td&gt;
&lt;td&gt;20（FN）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为未患病&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30（FP）&lt;/td&gt;
&lt;td&gt;870（TN）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;准确率&lt;/strong&gt;（Accuracy）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{Accuracy} = \frac{\text{TP} + \text{TN}}{\text{TP} + \text{FP} + \text{FN} + \text{TN}}
$$&lt;/p&gt;
&lt;p&gt;上面的例子，模型预测的准确率为： $\frac{80 + 870}{80 + 30 + 20 + 870} = \frac{950}{1000} = 0.95$ 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;精确率&lt;/strong&gt;（Precesion）。精确率用于衡量在所有被预测为正类的样本中，实际上属于正类的比例，通常也被称为查准率。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{Precesion} = \frac{\text{TP}}{\text{TP} + \text{FP}}
$$&lt;/p&gt;
&lt;p&gt;上面的例子，模型预测的精确率为： $\frac{80}{80 + 30} = \frac{80}{110} = 0.73$ 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;召回率&lt;/strong&gt;（Recall）。召回率用于衡量在所有实际为正类的样本中，被模型正确预测为正类的比例，通常也被称为查全率或真正例率（True Positive Rate）。
$$
\text{Recall} = \frac{\text{TP}}{\text{TP} + \text{FN}}
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面的例子，模型预测的召回率为： $\frac{80}{80 + 20} = \frac{80}{100} = 0.8$ 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;F1 分数&lt;/strong&gt;（F1 Score）。F1 分数是精确率和召回率的调和平均数，它在精确率和召回率之间寻求一个平衡，尤其适用于在两者之间有权衡的情况。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{F1 Score} = \frac{2}{\frac{1}{\text{Precision}} + \frac{1}{\text{Recall}}} = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precesion} + \text{Recall}}
$$&lt;/p&gt;
&lt;p&gt;上面的例子，模型预测的F1 分数为： $2 \times \frac{0.7273 * 0.8}{0.7273 + 0.8} = 0.76$ 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;特异度&lt;/strong&gt;（Specificity）和&lt;strong&gt;假正例率&lt;/strong&gt;（False Positive Rate，简称 FPR）。特异度用于衡量的是在所有实际为负类的样本中，被模型正确预测为负类的比例，类似于召回率，只不过针对的是负类样本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{Specificity} = \frac{\text{TN}}{\text{TN} + \text{FP}} \\
\text{FPR} = 1 - \text{Specificity}
$$&lt;/p&gt;
&lt;p&gt;上面的例子，模型预测的特异度为： $\frac{870}{870 + 30} = \frac{870}{900} = 0.97$ 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ROC&lt;/strong&gt; 和 &lt;strong&gt;AUC&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ROC&lt;/strong&gt;（Receiver Operating Characteristic Curve）绘制了召回率与假正例率的关系，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/6d6b0d4fa8.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AUC&lt;/strong&gt;（Area Under the Curve）是 ROC 曲线下的面积，衡量模型区分正类和负类的能力。AUC 值的范围是 $\small[0, 1]$ ，值越接近 1，表示模型区分正负类的能力越强。0.5 &amp;lt; AUC &amp;lt; 1，说明我们的模型优于随机猜测，只要这个分类器（模型）妥善设置阈值的话，就有预测价值。AUC = 0.5，说明我们的模型跟随机猜测一样，模型没有预测价值。AUC &amp;lt; 0.5，模型比随机猜测还差，但只要总能反向预测，它的实际效果就优于随机猜测。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于多分类问题，混淆矩阵的行数和列数都等于类别数，混淆矩阵是一个方阵，也就是说如果有 $\small{n}$ 个类别，那么混淆矩阵就是一个 $\small{n \times n}$ 的方阵。根据上面我们得到的鸢尾花数据集的预测结果，我们先输出真实值和预测值，然后制作对应的混淆矩阵，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(y_test)
print(y_pred)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 1 2 2 0 2 2 2 1 0 2 2 1 1 1]
[0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 2 2 2 0 2 2 2 1 0 2 2 1 1 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为类别0（山鸢尾）&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为类别1（多彩鸢尾）&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;预测为类别2（为吉尼亚鸢尾）&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为类别0（山鸢尾）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为类别1（多彩鸢尾）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实际为类别2（吉尼亚鸢尾）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们可以用 scikit-learn 中的&lt;code&gt;confusion_matrix&lt;/code&gt;和&lt;code&gt;classification_report&lt;/code&gt;函数来输出混淆矩阵和评估报告，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import classification_report, confusion_matrix

# 输出分类模型混淆矩阵
print(&apos;混淆矩阵: &apos;)
print(confusion_matrix(y_test, y_pred))
# 输出分类模型评估报告
print(&apos;评估报告: &apos;)
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;混淆矩阵: 
[[10  0  0]
 [ 0  9  1]
 [ 0  0 10]]
评估报告: 
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        10
           1       1.00      0.90      0.95        10
           2       0.91      1.00      0.95        10

    accuracy                           0.97        30
   macro avg       0.97      0.97      0.97        30
weighted avg       0.97      0.97      0.97        30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望用可视化的方式输出混淆矩阵，可以使用如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

# 创建混淆矩阵显示对象
cm_display_obj = ConfusionMatrixDisplay(confusion_matrix(y_test, y_pred), display_labels=iris.target_names)
# 绘制并显示混淆矩阵
cm_display_obj.plot(cmap=plt.cm.Reds)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：其中的&lt;code&gt;iris.target_names&lt;/code&gt;就是类别标签&lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;对应的三种鸢尾花的英文名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/53bbe7cbd6.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对于二分类问题，如果想要绘制出 ROC 曲线并显示 AUC 值，可以使用如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import roc_curve, auc
from sklearn.metrics import RocCurveDisplay

# 手动构造一组真实值和对应的预测值
y_test_ex = np.array([0, 0, 0, 1, 1, 0, 1, 1, 1, 0])
y_pred_ex = np.array([1, 0, 0, 1, 1, 0, 1, 1, 0, 1])
# 通过roc_curve函数计算出FPR（假正例率）和TPR（真正例率）
fpr, tpr, _ = roc_curve(y_test_ex, y_pred_ex)
# 通过auc函数计算出AUC值并通过RocCurveDisplay类绘制图形
RocCurveDisplay(fpr=fpr, tpr=tpr, roc_auc=auc(fpr, tpr)).plot()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/e8ac729e25.webp&quot; style=&quot;zoom:92%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;参数调优&lt;/h4&gt;
&lt;p&gt;之前我们说过，kNN 算法有两个关键问题，一是距离的度量，二是 k 值的选择。我们使用 scikit-learn 的&lt;code&gt;KNeighborsClassifier&lt;/code&gt;创建分类器模型时，可以对模型的超参数进行设置，这里有几个比较重要的参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;n_neighbors&lt;/code&gt;：近邻的数量，就是 kNN 算法中 k 的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;weights&lt;/code&gt;：可以选择&lt;code&gt;uniform&lt;/code&gt;或&lt;code&gt;distance&lt;/code&gt;，前者表示所有样本的权重相同，后者表示距离越近权重越高，默认值是&lt;code&gt;uniform&lt;/code&gt;。当然，我们也可以通过传入自定义的函数来确定每个样本的权重。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;algorithm&lt;/code&gt;：有&lt;code&gt;auto&lt;/code&gt;、&lt;code&gt;ball_tree&lt;/code&gt;、&lt;code&gt;kd_tree&lt;/code&gt;、&lt;code&gt;brute&lt;/code&gt;四个选项，默认值为&lt;code&gt;auto&lt;/code&gt;。其中&lt;code&gt;ball_tree&lt;/code&gt;是一种树形结构，基于球体划分的方法将数据点分配到层次化的树结构中，在高维数据和稀疏数据场景下有较好的性能；&lt;code&gt;kd_tree&lt;/code&gt;也是一种树形结构，通过选择一个维度将空间划分为若干个子区域再进行搜索，从而避免跟所有的邻居进行比较，对于低维度和空间分布均匀的数据，后者有较好的效果，在高维空间中会遇到的维度灾难问题；&lt;code&gt;auto&lt;/code&gt;选项是根据输入数据的维度自动选择&lt;code&gt;ball_tree&lt;/code&gt;或&lt;code&gt;kd_tree&lt;/code&gt;；&lt;code&gt;brute&lt;/code&gt;选项则是使用暴力搜索算法（穷举法），再处理小数据集时，它是一个简单且有效的选择。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leaf_size&lt;/code&gt;：使用&lt;code&gt;ball_tree&lt;/code&gt;或&lt;code&gt;kd_tree&lt;/code&gt;算法时，该参数用于限制树结构叶子节点最大样本数量，默认值为&lt;code&gt;30&lt;/code&gt;，该参数会影响树的构建和节点查找的性能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt;：闵可夫斯基距离公式中的&lt;code&gt;p&lt;/code&gt;，默认值为2，计算欧氏距离。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以使用&lt;strong&gt;网格搜索&lt;/strong&gt;（Grid Search）和&lt;strong&gt;交叉验证&lt;/strong&gt;（Cross Validation）的方式对模型的超参数进行调整，评估模型的泛化能力，提升模型的预测效果。网格搜索就是通过穷举法遍历给定的超参数空间，找到最优的超参数组合；交叉验证则是将训练集分成多个子集，通过在不同的训练集和验证集上进行多次训练和评估，对模型的预测效果进行综合评判。K-Fold交叉验证是最常用的交叉验证方法，通过将数据集划分为 K 个子集，每次选取其中一个子集作为验证集，剩下的 K-1 个子集作为训练集，对每个子集重复这个过程，完成 K 次训练和评估并将平均值作为模型的最终性能评估，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day81-90/844edfbdaa.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面的代码了 scikit-learn 库中的&lt;code&gt;GridSearchCV&lt;/code&gt;来做网格搜索和交叉验证，通过这种方式找到针对鸢尾花数据集实施 kNN 算法的最优参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import GridSearchCV

# 网格搜索交叉验证
gs = GridSearchCV(
    estimator=KNeighborsClassifier(),
    param_grid={
        &apos;n_neighbors&apos;: [1, 3, 5, 7, 9, 11, 13, 15],
        &apos;weights&apos;: [&apos;uniform&apos;, &apos;distance&apos;],
        &apos;p&apos;: [1, 2]
    },
    cv=5
)
gs.fit(X_train, y_train)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过下面的代码获得最优参数及其评分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;最优参数:&apos;, gs.best_params_)
print(&apos;评分:&apos;, gs.best_score_)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最优参数: {&apos;n_neighbors&apos;: 5, &apos;p&apos;: 2, &apos;weights&apos;: &apos;uniform&apos;}
评分: 0.9666666666666666
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要使用训练好的最优模型进行预测，可以按照如下所示的代码进行操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gs.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;kNN回归&lt;/h4&gt;
&lt;p&gt;kNN 算法通常用于解决分类问题，当然它也可以用于解决回归问题，其基本思想也是通过找出跟新实例最近的 k 个邻居，然后根据这 k 个邻居的标签（在回归问题中我们通常称之为目标值）来预测新实例的目标值。由于我们需要预测的不是类别标签而是一个数值，所以通常会使用平均或加权平均的方式来处理 k 个邻居的目标值，从而获得对新实例目标值的预测。让我们回到上一课用月收入预测网购支出的那个例子，然后使用 scikit-learn 中的&lt;code&gt;KNeighborsRegressor&lt;/code&gt;来构建回归模型，我们先做一些准备工作，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 每月收入
incomes = np.array([
    9558, 8835, 9313, 14990, 5564, 11227, 11806, 10242, 11999, 11630,
    6906, 13850, 7483, 8090, 9465, 9938, 11414, 3200, 10731, 19880,
    15500, 10343, 11100, 10020, 7587, 6120, 5386, 12038, 13360, 10885,
    17010, 9247, 13050, 6691, 7890, 9070, 16899, 8975, 8650, 9100,
    10990, 9184, 4811, 14890, 11313, 12547, 8300, 12400, 9853, 12890
])
# 每月网购支出
outcomes = np.array([
    3171, 2183, 3091, 5928, 182, 4373, 5297, 3788, 5282, 4166,
    1674, 5045, 1617, 1707, 3096, 3407, 4674, 361, 3599, 6584,
    6356, 3859, 4519, 3352, 1634, 1032, 1106, 4951, 5309, 3800,
    5672, 2901, 5439, 1478, 1424, 2777, 5682, 2554, 2117, 2845,
    3867, 2962,  882, 5435, 4174, 4948, 2376, 4987, 3329, 5002
])
X = np.sort(incomes).reshape(-1, 1)  # 将收入排序后处理成二维数组
y = outcomes[np.argsort(incomes)]    # 将网购支出按照收入进行排序
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：即便只有一个特征，我们也需要将自变量处理二维数组的形式，因为我们训练模型时使用的&lt;code&gt;fit&lt;/code&gt;方法并不能接受一维数组作为它的第一个参数。这里提前对 X 和 y 排序是为了待会绘制散点图和折线图的时候根据 X 的值从小到大依次绘制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来，我们创建 kNN 回归模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neighbors import KNeighborsRegressor

# 创建模型
model = KNeighborsRegressor()
# 训练模型
model.fit(X, y)
# 预测结果
y_pred = model.predict(X)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面我们直接用所有的历史数据进行模型训练，然后通过绘制下面的图形来看看预测效果到底如何。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 原始数据散点图
plt.scatter(X, y, color=&apos;navy&apos;)
# 预测结果折线图
plt.plot(X, y_pred, color=&apos;coral&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/cc820e500c.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;kNN 回归模型计算复杂度较高，而且对噪声数据非常敏感，在很多时候它可能并不是一个很好的选择。当然，如何评估一个回归模型的效果，这个问题我们留到后续的章节再为大家进行讲解。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;kNN 算法是一种简单但不失强大的机器学习算法，适用于小数据集上的分类和回归任务，它的优点是简单易懂，没有显示的训练过程，不依赖于对数据分布的假设，可以适应复杂的数据模式。当然，kNN 算法的缺点也非常明显，最大的问题在于计算效率，所以在数据集较大时可能并不是最好的选择。除此以外，kNN 算法对噪声非常敏感，同时预测结果依赖于 k 的取值和样本是否均衡（各个类别的样本数量是否大致相等），k 值过小可能导致过拟合，k值过大又可能导致欠拟合，而不均衡的样本可能会导致类别偏倚（Class Imbalance Bias），也就是说，如果大部分训练样本都属于类别 A，而类别 B 样本较少，那么在分类时，KNN 更容易将测试样本预测为类别 A。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;83 决策树和随机森林&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;决策树&lt;/strong&gt;（Decision Tree）是一种基于树结构的监督学习算法，可用于&lt;strong&gt;分类&lt;/strong&gt;和&lt;strong&gt;回归&lt;/strong&gt;任务。它通过将数据集逐步分割成不同的子集，直到满足某些停止条件，以此实现预测目标。我们生活中做决策的时候也会用到类似的方法，例如某位女生约见相亲对象的决策方法就可以绘制成如下所示的决策树。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/c5475d37ac.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上图仅用于帮助大家理解什么是决策树，无不良引导，也不代表本人的爱情观和婚姻观。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果具备一定的编程常识，你会发现用决策树做预测的过程相当于是执行了一系列的&lt;code&gt;if...else...&lt;/code&gt;结构；如果你对概率论的知识更熟悉，那么决策树的构建也可以视为计算以特征空间为前提的条件概率的过程。决策树中的结点可以分为两类：内部结点（上图中蓝色的结点）和叶结点（上图中红色和绿色的结点），其中内部结点对应样本特征属性测试（特征分割条件），而叶结点代表决策的结果（分类标签或回归目标值）。决策树可以用于解决分类问题和回归问题，本章我们仍然把重点放在分类问题上。&lt;/p&gt;
&lt;h4&gt;决策树的构建&lt;/h4&gt;
&lt;h5&gt;特征选择&lt;/h5&gt;
&lt;p&gt;训练决策树模型有三个核心的步骤：特征选择、决策树构建和决策树剪枝。特征选择决定了使用哪些特征来做判断，在训练数据集中，每个样本的属性可能有很多个，不同属性的作用有大有小。特征选择的作用就是筛选出跟分类结果相关性较高的特征，也就是分类能力较强的特征，这样的特征要出现在决策树靠上的位置。如果一个特征能够使得分类后的分支结点尽可能属于同一类别，即该结点有着较高的&lt;strong&gt;纯度&lt;/strong&gt;（purity），那么该特征对数据集而言就具备较强的分类能力。这里就产生了第一个问题，我们应该按照什么样的标准来选择分类能力强的特征？在决策树模型中，我们有三种方式来选择分类能力强的特征，分别是：&lt;strong&gt;信息增益&lt;/strong&gt;（information gain）、&lt;strong&gt;信息增益比&lt;/strong&gt;（gain ratio）和&lt;strong&gt;基尼指数&lt;/strong&gt;（Gini index）。&lt;/p&gt;
&lt;p&gt;要讲清楚信息增益，我们得先介绍&lt;strong&gt;信息熵&lt;/strong&gt;（information entropy）这个概念。1948年，克劳德·香农（&lt;em&gt;Claude Shannon&lt;/em&gt;）在他的著名论文《通信的数学原理》中提出了“信息熵”的概念，它解决了信息的度量问题，量化出了信息的价值。“熵”原本是热力学领域的概念，它反映的是系统的混乱程度，熵越大，系统的混乱程度就越高。在信息论中，熵可以看作随机变量（数据集）不确定性的度量，熵越大，变量（数据）的不确定性就越大，那么要确定它所需要获取的信息量也就越大；熵越低，数据的纯度就越高，不确定性就越小。&lt;/p&gt;
&lt;p&gt;例如，甲、乙两人参加一个射击比赛，如果从历史成绩来看，甲的胜率是100%，那么我们很容易接受甲会获胜这个结果；如果从历史成绩来看，甲的胜率是50%，那么我们就难以确定到底谁会获胜。克劳德·香农提出可以用下面的公式来描述这种不确定性：&lt;/p&gt;
&lt;p&gt;$$
H(D) = -\sum_{i = 1}^{k} P(x_i)log_2P(x_i)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{D}$ 代表数据集， $\small{k}$ 代表类别的总数， $\small{P(x_i)}$ 表示数据集中第 $\small{i}$ 类样本的比例（概率）。我们用 $\small{x_1}$ 和 $\small{x_2}$ 来分别表示甲获胜和乙获胜，很显然，当 $\small{P(x_1)=0.5}$ ， $\small{P(x_2)=0.5}$ 时， $\small{H=1}$ ，不确定性最大；当 $\small{P(x_1)=1}$ ， $\small{P(x_2)=0}$ 时， $\small{H=0}$ ，不确定性最小；当 $\small{P(x_1)=0.8}$ ， $\small{P(x_2)=0.2}$ 时， $\small{H \approx 0.72}$ 。&lt;/p&gt;
&lt;p&gt;很显然，知道的信息越多，随机变量（数据集）的不确定性就越小。这些信息，可以是直接针对我们想了解的随机事件的信息，也可以是和我们关心的随机事件相关的其他事件的信息。在数学上可以严格的证明这些相关的信息也能够降低或消除不确定性，为此我们定义&lt;strong&gt;条件熵&lt;/strong&gt;，它表示在给定特征 $\small{A}$ 的条件下，数据集 $\small{D}$ 的不确定性。条件熵的公式如下所示：&lt;/p&gt;
&lt;p&gt;$$
H(D \vert A) = \sum_{v \in A}\frac{\lvert D_{v} \rvert}{\lvert D \rvert}H(D_{v})
$$&lt;/p&gt;
&lt;p&gt;上面的公式中，我们让 $\small{v}$ 取得特征 $\small{A}$ 所有可能的取值，其中 $\small{D_{v}}$ 代表特征 $\small{A}$ 取值为 $\small{v}$ 的样本子集， $\small{\frac{\lvert D_{v} \rvert}{\lvert D \rvert}}$ 代表权重，即特征取值为 $\small{v}$ 的样本比例。可以证明 $\small{H(D) \ge H(D \vert A)}$ ，也就是说多了特征 $\small{A}$ 的信息之后，数据集 $\small{D}$ 的不确定性下降了。当然，还要注意等号成立的情况，也就是说增加了特征 $\small{A}$ 的信息，但是 $\small{D}$ 的不确定没有下降，也就是说我们获取的信息与要研究的内容没有关系。&lt;/p&gt;
&lt;p&gt;有了上面的铺垫，接下来我们就可以给出&lt;strong&gt;信息增益&lt;/strong&gt;的定义，它是在得到特征 $\small{A}$ 的信息后，数据集 $\small{D}$ 的不确定性减少的程度。换句话说，信息增益是一种描述数据集确定性增加的量，特征的信息增益越大，特征的分类能力就越强，在给定该特征后数据集的确定性就越大。信息增益可以通过下面的数学公式直观的描述：&lt;/p&gt;
&lt;p&gt;$$
g(D, A) = E(D) - E(D \vert A)
$$&lt;/p&gt;
&lt;p&gt;计算信息熵和信息增益的函数如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np


def entropy(y):
    &quot;&quot;&quot;
    计算信息熵
    :param y: 数据集的目标值
    :return: 信息熵
    &quot;&quot;&quot;
    _, counts = np.unique(y, return_counts=True)
    prob = counts / y.size
    return -np.sum(prob * np.log2(prob))


def info_gain(x, y):
    &quot;&quot;&quot;
    计算信息增益
    :param x: 给定的特征
    :param y: 数据集的目标值
    :return: 信息增益
    &quot;&quot;&quot;
    values, counts = np.unique(x, return_counts=True)
    new_entropy = 0
    for i, value in enumerate(values):
        prob = counts[i] / x.size
        new_entropy += prob * entropy(y[x == value])
    return entropy(y) - new_entropy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经典决策树算法中的 ID3 算法就是基于信息增益进行特征选择的，我们仍然以鸢尾花数据集为例，将数据集拆分为训练集和测试集，在训练集上计算出原始的信息熵以及引入四个特征（花萼长度、花萼宽度、花瓣长度、花瓣宽度）后的信息增益分别是多少，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)
print(f&apos;H(D)    = {entropy(y_train)}&apos;)
print(f&apos;g(D,A0) = {info_gain(X_train[:, 0], y_train)}&apos;)
print(f&apos;g(D,A1) = {info_gain(X_train[:, 1], y_train)}&apos;)
print(f&apos;g(D,A2) = {info_gain(X_train[:, 2], y_train)}&apos;)
print(f&apos;g(D,A3) = {info_gain(X_train[:, 3], y_train)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;H(D)    = 1.584962500721156
g(D,A0) = 0.9430813063736728
g(D,A1) = 0.5692093930591595
g(D,A2) = 1.4475439590905472
g(D,A3) = 1.4420095891994646
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：如果在划分训练集和测试集的时候，&lt;code&gt;train_test_split&lt;/code&gt;函数的&lt;code&gt;random_state&lt;/code&gt;参数跟我上面的代码不同，那么这里的运算结果可能会存在差异。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据上面的输出我们可以得知，花瓣长度（对应上面的&lt;code&gt;A2&lt;/code&gt;）这个特征信息增益最高，也就是说使用花瓣长度这个特征分类能力最强，分类后的数据集纯度最高。需要注意的是，当某个特征取值较多时，该特征的信息增益计算结果就会比较大，所以使用信息增益选择特征时，会偏向于取值较多的特征。为了解决这个问题，我们可以计算&lt;strong&gt;信息增益比&lt;/strong&gt;，它的定义如下所示：&lt;/p&gt;
&lt;p&gt;$$
R(D, A) = \frac{g(D, A)}{E_{A}(D)}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{E_{A}(D) = -\sum_{i=1}^{n}{\frac{\lvert D_{i} \rvert}{\lvert D \rvert}log_{2}\frac{\lvert D_{i} \rvert}{\lvert D \rvert}}}$ ， $\small{n}$ 表示特征 $\small{A}$ 的取值个数，简单的说 $\small{E_{A}(D)}$ 就是特征 $\small{A}$ 的信息熵，而信息增益比就是特征 $\small{A}$ 的信息增益和特征 $\small{A}$ 的信息熵的比值。我们用下面的函数来计算信息增益比，调用该函数可以输出四个特征对应的信息增益比。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def info_gain_ratio(x, y):
    &quot;&quot;&quot;
    计算信息增益比
    :param x: 给定的特征
    :param y: 数据集的目标值
    :return: 信息增益比
    &quot;&quot;&quot;
    return info_gain(x, y) / entropy(x)


print(f&apos;R(D,A0) = {info_gain_ratio(X_train[:, 0], y_train)}&apos;)
print(f&apos;R(D,A1) = {info_gain_ratio(X_train[:, 1], y_train)}&apos;)
print(f&apos;R(D,A2) = {info_gain_ratio(X_train[:, 2], y_train)}&apos;)
print(f&apos;R(D,A3) = {info_gain_ratio(X_train[:, 3], y_train)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R(D,A0) = 0.19687406476459068
R(D,A1) = 0.14319788821311977
R(D,A2) = 0.28837763858461984
R(D,A3) = 0.35550822529855447
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：如果在划分训练集和测试集的时候，&lt;code&gt;train_test_split&lt;/code&gt;函数的&lt;code&gt;random_state&lt;/code&gt;参数跟我上面的代码不同，那么这里的运算结果可能会存在差异。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据上面的输出我们可以得知，花瓣宽度（对应上面的&lt;code&gt;A3&lt;/code&gt;）这个特征信息增益比最高，也就是说这个特征能够完成一次良好的数据划分，可以作为我们构建决策树的根结点。当然，构建决策树不可能只做一次划分，我们可以在划分后的数据集上继续计算信息增益比来选择新的划分特征，重复这一过程直到满足一定的条件就可以构造出一棵完整的决策树模型。在经典决策树算法中，C4.5 算法就是基于信息增益比进行特征选择的。&lt;/p&gt;
&lt;p&gt;除了上面讲到的信息增益和信息增益比，&lt;strong&gt;基尼指数&lt;/strong&gt;也是非常好的特征选择方法，它可以用于评价数据集的纯度。基尼指数也叫&lt;strong&gt;基尼不纯度&lt;/strong&gt;（Gini impurity），它的取值在0到1之间，数据集纯度越高，基尼指数越靠近0，数据集纯度越低，基尼指数越靠近1。如果数据集有 $\small{n}$ 个类别，样本属于第 $\small{k}$ 个类别的概率为 $\small{p_{k}}$ ，那么数据集的基尼指数可以通过下面的公式进行计算：&lt;/p&gt;
&lt;p&gt;$$
G(D) = 1 - \sum_{k=1}^{n}{p_{k}}^{2}
$$&lt;/p&gt;
&lt;p&gt;例如鸢尾花数据集中，三种鸢尾花的样本数量都是50条，那么整个数据集的基尼指数为：&lt;/p&gt;
&lt;p&gt;$$
G(D) = 1 - [(\frac{1}{3})^{2} + (\frac{1}{3})^{2} + (\frac{1}{3})^{2}] = \frac{2}{3}
$$&lt;/p&gt;
&lt;p&gt;如果三种鸢尾花的样本数量分别为100条、25条、25条，那么整个数据集的基尼指数为：&lt;/p&gt;
&lt;p&gt;$$
G(D) = 1 - [(\frac{2}{3})^{2} + (\frac{1}{6})^{2} + (\frac{1}{6})^{2}] = \frac{1}{2}
$$&lt;/p&gt;
&lt;p&gt;如果三种鸢尾花的样本数量分别为140条、5条、5条，那么整个数据集的基尼指数为：&lt;/p&gt;
&lt;p&gt;$$
G(D) = 1 - [(\frac{14}{15})^{2} + (\frac{1}{30})^{2} + (\frac{1}{30})^{2}] = \frac{19}{150}
$$&lt;/p&gt;
&lt;p&gt;可以看出，随着数据集纯度越来越高，基尼指数的值越来越小。如果数据集 $\small{D}$ 根据特征 $\small{A}$ 划分为 $\small{k}$ 个部分，那么在给定特征 $\small{A}$ 的前提条件下，数据集的基尼指数可以定义为：&lt;/p&gt;
&lt;p&gt;$$
G(D, A) = \sum_{i=1}^{k}\frac{\lvert D_{i} \rvert}{\lvert D \rvert}G(D_{i})
$$&lt;/p&gt;
&lt;p&gt;根据上面的公式，我们可以设计出如下所示的计算基尼指数的函数，大家可以对照上面的公式看看是否能够理解下面的代码，或者通过调用下面的函数看看鸢尾花数据集的哪个特征可以对原始数据集或者训练集做出最好的划分。经典决策树算法中的 CART 算法就是基于基尼指数进行特征选择的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def gini_index(y):
    &quot;&quot;&quot;
    计算基尼指数
    :param y: 数据集的目标值
    :return: 基尼指数
    &quot;&quot;&quot;
    _, counts = np.unique(y, return_counts=True)
    return 1 - np.sum((counts / y.size) ** 2)


def gini_with_feature(x, y):
    &quot;&quot;&quot;
    计算给定特征后的基尼指数
    :param x: 给定的特征
    :param y: 数据集的目标值
    :return: 给定特征后的基尼指数
    &quot;&quot;&quot;
    values, counts = np.unique(x, return_counts=True)
    gini = 0
    for value in values:
        prob = x[x == value].size / x.size
        gini += prob * gini_index(y[x == value]) 
    return gini


print(f&apos;G(D)    = {gini_index(y_train)}&apos;)
print(f&apos;G(D,A0) = {gini_with_feature(X_train[:, 0], y_train)}&apos;)
print(f&apos;G(D,A1) = {gini_with_feature(X_train[:, 1], y_train)}&apos;)
print(f&apos;G(D,A2) = {gini_with_feature(X_train[:, 2], y_train)}&apos;)
print(f&apos;G(D,A3) = {gini_with_feature(X_train[:, 3], y_train)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;G(D,A0) = 0.29187830687830685
G(D,A1) = 0.44222582972582963
G(D,A2) = 0.06081349206349207
G(D,A3) = 0.06249999999999998
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据分裂&lt;/h5&gt;
&lt;p&gt;构建决策树的过程是一个递归的过程，通过上面介绍的方法选定特征后就要进行数据分裂，简单的说就是根据该特征将数据集分成两个或多个子集（两个子集对应二叉树，多个子集对应多叉树），每个子集对应于特征的不同取值。接下来，我们对每个子集重复特征选择和数据分裂的动作，直到满足停止条件。停止条件对于递归过程非常重要，同时可以避免生成过于复杂的树结构。常见的停止条件包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;树达到预设的深度。&lt;/li&gt;
&lt;li&gt;当前结点的样本数量少于预设的阈值。&lt;/li&gt;
&lt;li&gt;结点上所有样本属于同一个类别。&lt;/li&gt;
&lt;li&gt;信息增益或Gini指数的变动低于某个阈值。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果大家有编写树结构的经验，上面的过程应该很容易理解。当然，对递归（recursion）、二叉树（binary tree）、多叉树（multi-way tree）不熟悉的小伙伴也可以随便找本讲数据结构的书来看看，相关的知识并不复杂，我们这里就不进行赘述了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在数据分裂的过程中，还有一个值得关注的问题就是特征连续值和缺失值的处理。对于连续值，可以通过遍历特征所有可能的取值，找到切分点 $\small{x}$ 让切分后的子集在信息增益比或基尼指数方面达到最优，在数据分裂时以 $\small{x}$ 将数据划分为 $\small{D_{1}}$ 和 $\small{D_{2}}$ 两个子集，其中 $\small{D_{1}}$ 包含特征值小于等于 $\small{x}$ 的样本， $\small{D_{2}}$ 包含特征值大于 $\small{x}$ 的样本。对于缺失值，C4.5 算法采用了加权分配的方式进行处理，当选择一个特征进行分裂时，该特征存在缺失值的样本会被分配到每个子集，但是不同的子集中该样本被赋予的权重是不一样的，这个权重会根据该特征在各个类别中的比例进行计算。CART 算法在处理缺失值时，通常会为每个特征创建一个默认分支。对于存在缺失值的样本，CART 算法会将它们引导到默认分支进行处理，在计算基尼指数时，CART 算法可以选择是否将缺失值的样本纳入计算。&lt;/p&gt;
&lt;h5&gt;树的剪枝&lt;/h5&gt;
&lt;p&gt;对于一棵较为复杂的决策树进行剪枝是很有必要的，剪枝可以减少树结构的复杂性，同时也避免了过拟合的风险。常用的剪枝策略包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;后剪枝&lt;/strong&gt;（post-pruning）。后剪枝顾名思义是在决策树构建完成后，通过评估和移除一些不必要的分支来简化树结构。后剪枝的过程通常从叶结点开始，至底向上评估每个结点，看看将其替换为叶结点（将该结点及其子树剪掉）是否能提高模型的性能（在验证集上的预测效果）。这种方式在减少过拟合风险的同时，还能够较好的保留对数据的拟合能力，但是计算量较大，而且如果没有合适的验证集，剪枝效果就会受到影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预剪枝&lt;/strong&gt;（pre-pruning）。预剪枝顾名思义是在构建决策树的过程中动态决定是否停止分裂某个结点。通过在分裂前评估当前结点是否应该继续分裂，可以避免生成过于复杂的树，上面我们提到了几种常用的停止条件。预剪枝策略在决策树构建阶段就减少了不必要的分裂，从而降低了模型的复杂性，但是可能存在欠拟合的风险，因为过早停止分裂可能会遗漏潜在的重要决策规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;实现决策树模型&lt;/h4&gt;
&lt;p&gt;我们可以使用 scikit-learn 中&lt;code&gt;tree&lt;/code&gt;模块的&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;和&lt;code&gt;DecisionTreeRegressor&lt;/code&gt;来实现用于分类和回归的决策树，这里我们重点讨论用于解决分类问题的决策树模型，有兴趣的读者可以自行研究用于解决回归问题的决策树模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.tree import DecisionTreeClassifier

# 创建模型
model = DecisionTreeClassifier()
# 训练模型
model.fit(X_train, y_train)
# 预测结果
y_pred = model.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面，我们对比一下预测值和真实值并看看模型的评估报告。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import classification_report

print(y_test)
print(y_pred)
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 1 2 2 0 2 2 2 1 0 2 2 1 1 1]
[0 0 0 0 0 2 1 0 2 1 1 0 1 1 2 0 1 2 2 0 2 2 2 1 0 2 2 1 2 1]
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        10
           1       1.00      0.90      0.95        10
           2       0.91      1.00      0.95        10

    accuracy                           0.97        30
   macro avg       0.97      0.97      0.97        30
weighted avg       0.97      0.97      0.97        30
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大家运行上面的代码看到的输出可能跟我这里并不相同，因为在创建&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;时有一个控制随机性的参数&lt;code&gt;random_state&lt;/code&gt;，如果为该参数提供一个确定的整数值，就可以确保你的运行结果是可重复的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以通过下面的代码来可视化决策树，相信可视化的方式会帮助大家对决策树模型有一个更好的理解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize=(12, 10))
plot_tree(
    decision_tree=model,               # 决策树模型
    feature_names=iris.feature_names,  # 特征的名称
    class_names=iris.target_names,     # 标签的名称
    filled=True                        # 用颜色填充
)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9791eee7c8.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上图中树的根结点的基尼指数跟我们之前计算的数据集的基尼指数完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们调整一下&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;的参数重新创建决策树并对模型进行可视化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建模型
model = DecisionTreeClassifier(
    criterion=&apos;entropy&apos;,
    ccp_alpha=0.01,
    
)
# 训练模型
model.fit(X_train, y_train)
# 可视化
plt.figure(figsize=(12, 10))
plot_tree(
    decision_tree=model,               # 决策树模型
    feature_names=iris.feature_names,  # 特征的名称
    class_names=iris.target_names,     # 标签的名称
    filled=True                        # 用颜色填充
)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/93a24af018.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上图中树的根结点的信息熵跟我们之前计算的数据集的信息熵完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由此可见，创建&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;对象时参数不同，生成的决策树也会存在差异。&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;构造函数的参数就是决策树模型的超参数，其中有几个非常重要的超参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;criterion&lt;/code&gt;：特征选择（数据分裂质量评估）的标准，可以选择&lt;code&gt;&apos;gini&apos;&lt;/code&gt;或&lt;code&gt;&apos;entropy&apos;&lt;/code&gt;，前者代表基尼指数，也是默认值，后者代表信息增益。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_depth&lt;/code&gt;：树的最大深度，默认值为&lt;code&gt;None&lt;/code&gt;，如果不设置该参数，会存在过拟合风险。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_samples_split&lt;/code&gt;：一个内部结点再次分裂所需的最小样本数，默认值为&lt;code&gt;2&lt;/code&gt;。这个参数可以设置为整数表示最小样本数，也可以设置为浮点数，表示占总样本数的比例。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_samples_leaf&lt;/code&gt;：叶结点所需的最小样本数，默认值为&lt;code&gt;1&lt;/code&gt;。将该参数设置为较大的值可以平滑模型，降低过拟合风险。这个参数也可以设置为整数或浮点数，道理同上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_features&lt;/code&gt;：用于最佳分裂的特征数，默认值为&lt;code&gt;None&lt;/code&gt;。这个参数可以设置为整数，表示选择固定数量的特征；可以设置为浮点数，表示选择特征的比例；可以设置为字符串，&lt;code&gt;&apos;auto&apos;&lt;/code&gt;和&lt;code&gt;&apos;sqrt&apos;&lt;/code&gt;表示将总的特征数量求平方根，用平方根的值作为选择特征的数量，&lt;code&gt;&apos;log2&apos;&lt;/code&gt;表示将总的特征数量求对数，用对数值作为选择特征的数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class_weight&lt;/code&gt;：指定类别的权重，用于处理类别不平衡问题，默认值为&lt;code&gt;None&lt;/code&gt;。可以用字典的方式手动设置每个类别的权重，也可以使用&lt;code&gt;&apos;balanced&apos;&lt;/code&gt;让模型自动调整。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;splitter&lt;/code&gt;：选择分裂结点的策略，默认值为&lt;code&gt;&apos;best&apos;&lt;/code&gt;，表示最佳分裂，还有一个取值是&lt;code&gt;&apos;random&apos;&lt;/code&gt;，表示随机分裂。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_leaf_nodes&lt;/code&gt;：限制叶结点的最大数量，可以防止树结构过于复杂。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_impurity_decrease&lt;/code&gt;：结点分裂所需的最小不纯度降低值，任何结点只有在不纯度减少超过此值时才会进行分裂。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ccp_alpha&lt;/code&gt;：成本复杂度剪枝中的$\small{\alpha}$参数值。这个参数用于控制后剪枝中成本复杂度计算公式中$\small{\alpha}$的值。较小的$\small{\alpha}$值允许更复杂的树，而较大的$\small{\alpha}$值倾向于选择更简单的树。通过调整$\small{\alpha}$，可以找到一个最佳的复杂度和误差之间的平衡点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以通过之前讲到的网格搜索和交叉验证的方式来对超参数进行调优，有兴趣的读者可以参考下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import GridSearchCV

gs = GridSearchCV(
    estimator=DecisionTreeClassifier(),
    param_grid={
        &apos;criterion&apos;: [&apos;gini&apos;, &apos;entropy&apos;],
        &apos;max_depth&apos;: np.arange(5, 10),
        &apos;max_features&apos;: [None, &apos;sqrt&apos;, &apos;log2&apos;],
        &apos;min_samples_leaf&apos;: np.arange(1, 11),
        &apos;max_leaf_nodes&apos;: np.arange(5, 15)
    },
    cv=5
)
gs.fit(X_train, y_train)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;随机森林概述&lt;/h4&gt;
&lt;p&gt;随机森林是基于决策树的集成学习算法，所谓集成学习就是通过组合多个模型的预测结果来提高整体模型的性能，其核心思想就是多个模型的组合往往比单个模型更有效，通过不同的模型捕捉到数据的不同特征，从而降低模型的过拟合风险（提升模型的泛化能力）。随机森林通过构建多个决策树并将它们的预测结果进行投票（分类）或平均（回归），来提高模型的准确性和鲁棒性，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/bd21a9a98b.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;随机森林的基本工作流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bootstrap 抽样：从原始训练数据集中随机抽取若干个样本（有放回抽样），形成多个不同的子集，每个子集用于训练一棵决策树。&lt;/li&gt;
&lt;li&gt;构建决策树：对于每棵树在进行结点分裂时，不必考虑所有特征，而是随机选择一部分特征实现数据分裂。通过这种方式，随机森林增加了模型的多样性，减少了树之间的相关性。&lt;/li&gt;
&lt;li&gt;集成学习：对于分类任务，随机森林通过投票的方式（即多数表决）来决定最终分类的结果；对于回归任务，通常可以对每棵树的预测结果求均值作为最终的预测结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;随机森林的主要优点体现在：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过集成多个决策树，能够有效提高模型的准确性。&lt;/li&gt;
&lt;li&gt;由于引入了随机性，随机森林通常比单棵决策树更不容易过拟合。&lt;/li&gt;
&lt;li&gt;随机森林可以提供特征的重要性评分，帮助理解模型。&lt;/li&gt;
&lt;li&gt;能够处理大规模数据集和高维数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，因为要构建多个决策树，随机森林模型通常比较复杂，在训练模型时对计算资源、内存资源和时间成本的消耗都是更大的。我们可以通过 scikit-learn 库&lt;code&gt;ensemble&lt;/code&gt;模块的&lt;code&gt;RandomForestClassifier&lt;/code&gt;类来构造随机森林模型，这里我们仍然使用网格搜索交叉验证的方式来对超参数进行调优，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.ensemble import RandomForestClassifier

gs = GridSearchCV(
    estimator=RandomForestClassifier(n_jobs=-1),
    param_grid={
        &apos;n_estimators&apos;: [50, 100, 150],
        &apos;criterion&apos;: [&apos;gini&apos;, &apos;entropy&apos;],
        &apos;max_depth&apos;: np.arange(5, 10),
        &apos;max_features&apos;: [&apos;sqrt&apos;, &apos;log2&apos;],
        &apos;min_samples_leaf&apos;: np.arange(1, 11),
        &apos;max_leaf_nodes&apos;: np.arange(5, 15)
    },
    cv=5
)
gs.fit(X_train, y_train)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面的代码可能会运行非常长的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;随机森林模型的超参数很多跟决策树类似，需要说明的有以下几个超参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;estimator&lt;/code&gt;：森林中树的数量，简单的说就是用多少个决策树来构造森林。更多的树通常会带来更好的预测效果，但也会增加计算成本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boostrap&lt;/code&gt;：是否使用 Bootstrap 抽样，默认值为&lt;code&gt;True&lt;/code&gt;，表示在构建每棵树时使用有放回抽样。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n_jobs&lt;/code&gt;：用于并行运行的任务数，默认值为&lt;code&gt;None&lt;/code&gt;，表示使用单个处理器核心；设置为&lt;code&gt;-1&lt;/code&gt;，则可以使用所有可用的处理器核心。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;决策树是简单有效且易于理解的预测模型，适用于分类和回归任务，但容易发生过拟合且对噪声数据敏感；随机森林通过集成多个决策树提高了泛化能力且对噪声数据不敏感，适合解决复杂问题。在解决实际问题时，可以根据具体场景选择合适的模型，通过调节相关超参数来获得最佳效果，二者的对比如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;决策树&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;随机森林&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;模型复杂度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;较复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;抗过拟合能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;差&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;计算效率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;较低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;结果稳定性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;易受单一数据变化影响&lt;/td&gt;
&lt;td&gt;稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;数据较少、简单问题&lt;/td&gt;
&lt;td&gt;数据较多、复杂问题&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;84 朴素贝叶斯算法&lt;/h2&gt;
&lt;p&gt;我们继续为大家介绍解决分类任务的算法，本章介绍一种概率模型贝叶斯分类器。贝叶斯分类器是一类分类算法的总称，这类算法均以贝叶斯定理为基础，因而统称为贝叶斯分类器。在介绍贝叶斯定理之前，我们先讲一个故事：从 2015 年到 2020 年期间，某位李姓女士凭借自己对航班是否会延误的分析，购买了大约 900 次飞机延误险并获得延误赔偿，累计获得理赔金高达 300 多万元，真可谓“航班延误，发家致富”。当然，这套骚操作本身不是我们探讨的重点，我们的问题是：李女士是怎么决定要不要购买延误险的呢？航班延误最主要的原因就是天气（包括起飞地和降落地的天气）、机场（起飞机场和降落机场）和航司，由于李女士有过航空服务类工作的经历，有获得机场和航司相关数据的途径（天气数据相对更容易获取），集齐相关的数据再利用贝叶斯定理，她可以能够计算出当前航班延误的概率并决定是否购买延误险。接下来，李女士通过虚构不同身份购票并大量投保（每个身份购买 30 到 40 份延误险），这样一旦航班延误，她就可以向保险公司进行索赔。那么，我们要探讨的就是贝叶斯定理是如何利用现有数据计算出航班延误的概率。&lt;/p&gt;
&lt;h4&gt;贝叶斯定理&lt;/h4&gt;
&lt;p&gt;贝叶斯定理是概率论中的一个重要定理，它描述了如何从主观经验或已知事实出发，通过收集到的样本数据（证据）来更新对事件发生概率的认知（信念）。贝叶斯定理的数学表达式为：&lt;/p&gt;
&lt;p&gt;$$
P(A \vert B) = \frac{P(B \vert A)}{P(B)} \cdot P(A)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{P(A)}$ 是事件 $\small{A}$ 发生的&lt;strong&gt;先验概率&lt;/strong&gt;，我们可以理解为已知事实或主观经验（&lt;strong&gt;主观概率&lt;/strong&gt;）； $\small{P(B \vert A)}$ 是在事件 $\small{A}$ 发生的条件下事件 $\small{B}$ 发生的 条件概率，通常也称之为&lt;strong&gt;似然性&lt;/strong&gt;（likelihood）， $\small{P(B)}$ 是事件 $\small{B}$ 发生的（全）概率，这两个概率可以通过我们收集到的样本数据（证据）获得； $\small{P(A \vert B)}$ 是在事件 $\small{B}$ 发生的条件下事件 $\small{A}$ 发生的条件概率，即收集到样本数据后对事件 $\small{A}$ 发生概率的重新认知，称之为&lt;strong&gt;后验概率&lt;/strong&gt;。贝叶斯定理告诉我们一个重要的事实：可以从已知的事实或主观经验出发，通过收集到的证据来更新我们对某个事件发生概率的认知，简单的说就是&lt;strong&gt;可以通过已知的事实和收集的证据来推断出未知的真相&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;回到上面李女士购买飞机延误险的例子，假设本次航班是从成都双流国际机场飞往北京首都国际机场，执飞的航空公司是四川航空，起飞地天气为雨天（小雨），温度为8°C，东北风2级，降落地天气为晴天，温度4°C，西北风2级。为了更简单的让大家理解贝叶斯定理，我们对这里的条件稍作简化，只保留天气中的降水信息，暂不考虑温度和风速等其他因素，对应到上面的贝叶斯定理有：&lt;/p&gt;
&lt;p&gt;$$
P(延误 \vert 起飞机场=双流,到达机场=首都,起飞天气=小雨,降落天气=晴天,执飞航司=川航) = \\
\frac{P(起飞机场=双流,到达机场=首都,起飞天气=小雨,降落天气=晴天,执飞航司=川航 \vert 延误)}{P(起飞机场=双流,到达机场=首都,起飞天气=小雨,降落天气=晴天,执飞航司=川航)} \cdot P(延误)
$$&lt;/p&gt;
&lt;p&gt;上面公式等号左边就是李女士想知道的当前航班延误的概率，等号右边的部分其实就是历史数据和当前信息，计算这个概率的关键在于计算出似然性，即 $\small{P(起飞机场=双流,到达机场=首都,起飞天气=小雨,降落天气=晴天,执飞航司=川航 \vert 延误)}$ 到底是多少，那么这个条件概率又该如何计算呢？&lt;/p&gt;
&lt;h4&gt;朴素贝叶斯&lt;/h4&gt;
&lt;p&gt;朴素贝叶斯算法是基于贝叶斯定理和特征条件独立性假设的分类算法，因其简单高效而受到广泛应用。朴素贝叶斯算法的关键在于“朴素”二字，就是刚才我们说到特征条件独立性假设，条件独立性假设是说用于分类的特征在类确定的条件下都是独立的，该假设使得朴素贝叶斯的学习成为可能。&lt;/p&gt;
&lt;p&gt;假设我们有一个特征集合 $\small{X = {x_1, x_2, \ldots, x_n}}$ 和一个类别 $\small{C}$ ，朴素贝叶斯算法假设：&lt;/p&gt;
&lt;p&gt;$$
P(X \vert C) = P(x_1 \vert C) \cdot P(x_2 \vert C) \cdot \ldots \cdot P(x_n \vert C)
$$&lt;/p&gt;
&lt;p&gt;这个假设大大简化了计算复杂性，使得我们可以只计算每个特征在给定类别下的概率，而不需要考虑特征之间的相互作用，对应到上面购买飞机延误险的例子，我们可以用下面的方式来计算似然性：&lt;/p&gt;
&lt;p&gt;$$
P(起飞机场=双流,到达机场=首都,起飞天气=小雨,降落天气=晴天,执飞航司=川航 \vert 延误) = \\
P(起飞机场=双流 \vert 延误) \times P(到达机场=首都 \vert 延误) \times P(起飞天气=小雨 \vert 延误) \times P(降落天气=晴天 \vert 延误) \times P(执飞航司=川航 \vert 延误)
$$&lt;/p&gt;
&lt;h4&gt;算法原理&lt;/h4&gt;
&lt;h5&gt;训练阶段&lt;/h5&gt;
&lt;p&gt;在训练阶段，朴素贝叶斯算法首先需要计算每个类别的先验概率和每个特征在各个类别下的条件概率。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算先验概率&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
P(C_{i}) = \frac{n_{i}}{n}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{C_{i}}$ 表示类别， $\small{n_{i}}$ 是类别 $\small{C_{i}}$ 的样本数量， $\small{n}$ 是总的样本容量。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算条件概率&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
P(x_{j} \vert C_{i}) = \frac{n_{i,j}}{n_{i}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{n_{i,j}}$ 是在类别 $\small{C_{i}}$ 中，特征 $\small{x_{j}}$ 出现的次数。&lt;/p&gt;
&lt;h5&gt;预测阶段&lt;/h5&gt;
&lt;p&gt;在预测阶段，给定一个待分类样本 $\small{X}$ ，朴素贝叶斯算法通过以下步骤来计算其属于每个类别的后验概率：&lt;/p&gt;
&lt;p&gt;$$
P(C_{i} \vert X) = \frac{P(X \vert C_{i})}{P(X)} \cdot P(C_{i})
$$&lt;/p&gt;
&lt;p&gt;上面的公式中， $\small{P(X)}$ 对应到每个类别都是一个常量，可以忽略掉它，再结合独立性假设有：&lt;/p&gt;
&lt;p&gt;$$
P(C_{i} \vert X) \propto P(C_{i}) \cdot P(x_1 \vert C_{i}) \cdot P(x_2 \vert C_{i}) \cdot \ldots \cdot P(x_n \vert C_{i})
$$&lt;/p&gt;
&lt;p&gt;这样，我们可以选择具有最高后验概率的类别作为预测结果。&lt;/p&gt;
&lt;h5&gt;代码实现&lt;/h5&gt;
&lt;p&gt;我们还是以鸢尾花数据集为例，按照上面的讲解的原理，用 NumPy 来实现一个朴素贝叶斯分类器，我们还是从加载数据开始，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;训练阶段我们要获得类别标签和对应的先验概率，此外还要计算出似然性，似然性的计算用到了上面提到的“朴素”假设，我们对鸢尾花连续的特征值进行了简单的离散化处理（大家先不考虑这种处理方式是否合理），代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import pandas as pd


def naive_bayes_fit(X, y):
    &quot;&quot;&quot;
    :param X: 样本特征
    :param Y: 样本标签
    :returns: 二元组 - (先验概率, 似然性)
    &quot;&quot;&quot;
    # 计算先验概率
    clazz_labels, clazz_counts = np.unique(y, return_counts=True)
    prior_probs = pd.Series({k: v / y.size for k, v in zip(clazz_labels, clazz_counts)})
    # 拷贝数组创建副本
    X = np.copy(X)
    # 保存似然性计算结果的字典
    likelihoods = {}
    for j in range(X.shape[1]):  # 对特征的循环
        # 对特征进行等宽分箱（离散化处理）
        X[:, j] = pd.cut(X[:, j], bins=5, labels=np.arange(1, 6))
        for i in prior_probs.index:
            # 按标签类别拆分数据并统计每个特征值出现的频次
            x_prime = X[y == i, j]
            x_values, x_counts = np.unique(x_prime, return_counts=True)
            for k, value in enumerate(x_values):  # 对不同特征值的循环
                # 计算似然性并保存在字典中（字典的键是一个三元组 - (标签, 特征序号, 特征值)）
                likelihoods[(i, j, value)] = x_counts[k] / x_prime.size
    return prior_probs, likelihoods
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用上面的函数，我们可以得到一个二元组，解包之后分别是类别标签 $\small{C_{i}}$ 对应的先验概率和在类别 $\small{C_{i}}$ 中，第 $\small{j}$ 个特征取到某个值&lt;code&gt;value&lt;/code&gt;（上面的代码中，我们用 pandas 的&lt;code&gt;cut&lt;/code&gt;函数对特征值分箱，&lt;code&gt;value&lt;/code&gt;的取值为&lt;code&gt;1&lt;/code&gt; 到&lt;code&gt;5&lt;/code&gt;）的似然性，前者是一个&lt;code&gt;Series&lt;/code&gt;对象，后者是一个&lt;code&gt;dict&lt;/code&gt;对象，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p_ci, p_x_ci = naive_bayes_fit(X_train, y_train)
print(&apos;先验概率: &apos;, p_ci, sep=&apos;\n&apos;)
print(&apos;似然性: &apos;, p_x_ci, sep=&apos;\n&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;先验概率: 
0    0.333333
1    0.333333
2    0.333333
dtype: float64
似然性: 
{(0, 0, 1.0): 0.525, (0, 0, 2.0): 0.45, (0, 0, 3.0): 0.025, (1, 0, 1.0): 0.05, (1, 0, 2.0): 0.375, (1, 0, 3.0): 0.425, (1, 0, 4.0): 0.15, (2, 0, 1.0): 0.025, (2, 0, 2.0): 0.025, (2, 0, 3.0): 0.45, (2, 0, 4.0): 0.3, (2, 0, 5.0): 0.2, (0, 1, 1.0): 0.025, (0, 1, 3.0): 0.325, (0, 1, 4.0): 0.45, (0, 1, 5.0): 0.2, (1, 1, 1.0): 0.175, (1, 1, 2.0): 0.325, (1, 1, 3.0): 0.475, (1, 1, 4.0): 0.025, (2, 1, 1.0): 0.025, (2, 1, 2.0): 0.35, (2, 1, 3.0): 0.525, (2, 1, 4.0): 0.05, (2, 1, 5.0): 0.05, (0, 2, 1.0): 1.0, (1, 2, 2.0): 0.025, (1, 2, 3.0): 0.525, (1, 2, 4.0): 0.45, (2, 2, 4.0): 0.525, (2, 2, 5.0): 0.475, (0, 3, 1.0): 0.975, (0, 3, 2.0): 0.025, (1, 3, 2.0): 0.125, (1, 3, 3.0): 0.75, (1, 3, 4.0): 0.125, (2, 3, 3.0): 0.05, (2, 3, 4.0): 0.525, (2, 3, 5.0): 0.425}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：字典中的第一个元素&lt;code&gt;(0, 0, 1.0): 0.525&lt;/code&gt;表示标签为&lt;code&gt;0&lt;/code&gt;，第&lt;code&gt;0&lt;/code&gt;个特征（花萼长度）取值为&lt;code&gt;1.0&lt;/code&gt;的似然性为&lt;code&gt;0.525&lt;/code&gt;；最后一个元素&lt;code&gt;(2, 3, 5.0): 0.425&lt;/code&gt;表示标签为&lt;code&gt;2&lt;/code&gt;，第&lt;code&gt;3&lt;/code&gt;个特征（花瓣宽度）取值为&lt;code&gt;5.0&lt;/code&gt;的似然性为&lt;code&gt;0.425&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;预测阶段我们利用上面函数得到的先验概率和似然性计算后验概率，然后根据后验概率的最大值为样本赋予预测的类别标签。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def naive_bayes_predict(X, p_ci, p_x_ci):
    &quot;&quot;&quot;
    朴素贝叶斯分类器预测
    :param X: 样本特征
    :param p_ci: 先验概率
    :param p_x_ci: 似然性
    :return: 预测的标签
    &quot;&quot;&quot;
    # 对特征进行等宽分箱（离散化处理）
    X = np.copy(X)
    for j in range(X.shape[1]):
        X[:, j] = pd.cut(X[:, j], bins=5, labels=np.arange(1, 6))
    # 保存每个样本对应每个类别后验概率的二维数组
    results = np.zeros((X.shape[0], p_ci.size))
    clazz_labels = p_ci.index.values
    for k in range(X.shape[0]):
        for i, label in enumerate(clazz_labels):
            # 获得先验概率（训练的结果）
            prob = p_ci.loc[label]
            # 计算获得特征数据后的后验概率
            for j in range(X.shape[1]):
                # 如果没有对应的似然性就取值为0
                prob *= p_x_ci.get((i, j, X[k, j]), 0)
            results[k, i] = prob
    # 根据每个样本对应类别最大的概率选择预测标签
    return clazz_labels[results.argmax(axis=1)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将上面的函数作用于测试集进行预测，比较预测值和真实值，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;y_pred = naive_bayes_predict(X_test, p_ci, p_x_ci)
y_pred == y_test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
       False,  True,  True,  True,  True,  True,  True,  True, False,
        True,  True,  True])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两个函数希望能帮助大家理解朴素贝叶斯的工作原理，实际工作中我们还是推荐大家使用 scikit-learn 库的&lt;code&gt;navie_bayes&lt;/code&gt;模块封装的类来创建朴素贝叶斯模型，该模块下有五个朴素贝叶斯算法的变体，每种变体针对不同类型的数据和特征分布，对应的五种朴素贝叶斯分类器分别是：&lt;code&gt;BernoulliNB&lt;/code&gt;、&lt;code&gt;CategoricalNB&lt;/code&gt;、&lt;code&gt;ComplementNB&lt;/code&gt;、&lt;code&gt;GaussianNB&lt;/code&gt;和&lt;code&gt;MultinomialNB&lt;/code&gt;，它们之间的差别如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类器&lt;/th&gt;
&lt;th&gt;特征类型&lt;/th&gt;
&lt;th&gt;主要假设&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BernoulliNB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;二元特征&lt;/td&gt;
&lt;td&gt;特征服从 Bernoulli 分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CategoricalNB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类别特征&lt;/td&gt;
&lt;td&gt;特征服从多项式分布，常用于处理类别数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ComplementNB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计数特征&lt;/td&gt;
&lt;td&gt;利用补集概率，常用于处理不平衡数据集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GaussianNB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连续特征&lt;/td&gt;
&lt;td&gt;特征服从高斯分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MultinomialNB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计数特征&lt;/td&gt;
&lt;td&gt;特征服从多项式分布，常用于文本分类&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于鸢尾花数据集，由于其特征是连续值，我们可以用&lt;code&gt;GaussianNB&lt;/code&gt;来创建模型，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.naive_bayes import GaussianNB

model = GaussianNB()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看看模型评估的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       1.00      1.00      1.00        10
           1       0.91      1.00      0.95        10
           2       1.00      0.90      0.95        10

    accuracy                           0.97        30
   macro avg       0.97      0.97      0.97        30
weighted avg       0.97      0.97      0.97        30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想看看朴素贝叶斯模型给每个样本对应到每个标签给出的概率值，可以使用下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.predict_proba(X_test).round(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1.  , 0.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [0.  , 0.  , 1.  ],
       [0.  , 1.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [0.  , 0.  , 1.  ],
       [0.  , 0.98, 0.02],
       [0.  , 1.  , 0.  ],
       [1.  , 0.  , 0.  ],
       [0.  , 1.  , 0.  ],
       [0.  , 1.  , 0.  ],
       [0.  , 0.  , 1.  ],
       [1.  , 0.  , 0.  ],
       [0.  , 0.93, 0.07],
       [0.  , 0.  , 1.  ],
       [0.  , 0.02, 0.98],
       [1.  , 0.  , 0.  ],
       [0.  , 0.22, 0.78],
       [0.  , 0.  , 1.  ],
       [0.  , 0.  , 1.  ],
       [0.  , 0.92, 0.08],
       [1.  , 0.  , 0.  ],
       [0.  , 0.  , 1.  ],
       [0.  , 0.54, 0.46],
       [0.  , 1.  , 0.  ],
       [0.  , 1.  , 0.  ],
       [0.  , 0.81, 0.19]])
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;算法优缺点&lt;/h4&gt;
&lt;p&gt;朴素贝叶斯算法的优缺包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逻辑简单容易实现，适合大规模数据集&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运算开销较小&lt;/strong&gt;。预测需要用到的概率在训练阶段都已经准好了，当新数据来了之后，只需要获取对应的概率值并进行简单的运算就能获得预测的结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;受噪声和无关属性影响小&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，由于做了“特征相互独立”这个假设，朴素贝叶斯算法的缺点也相当明显，因为在实际应用中，特征之间很难做到完全独立，尤其是维度很高的数据，如果特征之间的相关性较大，那么分类的效果就会变得很差。为了解决这个问题，在朴素贝叶斯算法的基础上又衍生出了一些新的方法，包括：半朴素贝叶斯（One Dependent Estimator）、AODE（Averaged One Dependent Estimator）、K 依赖朴素贝叶斯、朴素贝叶斯网络、高斯混合朴素贝叶斯等，有兴趣的读者可以自行了解。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;朴素贝叶斯算法在多个领域有广泛应用，以下是一些常见的应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文本分类&lt;/strong&gt;：如垃圾邮件检测、情感分析等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐系统&lt;/strong&gt;：根据用户行为和喜好进行个性化推荐。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;医药诊断&lt;/strong&gt;：根据症状预测疾病。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;85 回归模型&lt;/h2&gt;
&lt;p&gt;回归模型是机器学习和统计学中的一种基本模型，用于预测连续型输出变量。简单的说，给定一组输入变量（自变量）和对应的输出变量（因变量），回归模型旨在找到输入变量和输出变量之间的映射关系。回归模型的形式可能比较简单，但它确包含了机器学习中最主要的建模思想。通常，我们建立回归模型主要有两个目标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;描述数据之间的关系&lt;/strong&gt;。我们之前讲过&lt;strong&gt;机器学习的关键就是要通过历史数据掌握如何从特征映射到目标值&lt;/strong&gt;，这个过程不需要我们事先设置任何的规则，而是让机器通过对历史数据的学习来获得。回归模型可以帮助我们通过模型表达输入和输出之间的关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对未知数据做出预测&lt;/strong&gt;。通过学习到的映射关系，模型可以对新的输入数据进行预测。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;回归模型的应用非常广泛，我们为大家举几个具体的例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;零售行业&lt;/strong&gt;。全球最大的电商平台亚马逊（Amazon）会根据历史销量、商品属性（价格、折扣、品牌、类别等）、时间特征（季节、工作日、节假日等）、外部因素（天气、社媒等）等特征创建回归模型预测未来一段时间内不同商品的需求量。在促销活动期间，会使用多元回归结合交互项来分析促销对销量的影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;汽车行业&lt;/strong&gt;。为了优化电池的充电策略，延长电池的使用寿命，为电车用户提供更准确的电量预警，特斯拉（Tesla）使用回归模型，通过电池充放电次数、环境温度、放电深度、电池物理参数等建立回归模型，预测电池的剩余寿命。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;房地产行业&lt;/strong&gt;。美国最大的在线房地产平台 Zillow 曾经使用回归模型帮助用户评估房屋的价值，通过房屋的面积、房龄、地理位置、房屋类型、社区安全等级、学校评分等对房屋的市场价格做出预测。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;回归模型的分类&lt;/h4&gt;
&lt;p&gt;根据模型的复杂程度和假设，回归模型可以分为以下几类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;线性回归&lt;/strong&gt;（Linear Regression）：假设输入变量和输出变量之间是线性关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一元线性回归：建立一个因变量与单个自变量之间线性关系的模型。&lt;/p&gt;
&lt;p&gt;$$
y = \beta_0 + \beta_1 x + \varepsilon
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{y}$ 是目标变量（因变量）， $\small{x}$ 是输入变量（自变量）， $\small{\beta_{0}}$ 是截距，表示 $\small{x=0}$ 时的预测值， $\small{\beta_{1}}$ 是回归系数（斜率），表示输入变量对输出变量影响的大小， $\small{\varepsilon}$ 是误差项，用于表示数据中的随机噪声或无法解释的部分。&lt;/p&gt;
&lt;p&gt;多元线性回归：建立一个因变量与多个自变量之间线性关系的模型。&lt;/p&gt;
&lt;p&gt;$$
y = \beta_{0} + \beta_{1} x_{1} + \beta_{2} x_{2} + \cdots + \beta_{n} x_{n} + \varepsilon
$$&lt;/p&gt;
&lt;p&gt;上面的公式也可以用向量的形式简化表示为：&lt;/p&gt;
&lt;p&gt;$$
y = \mathbf{x}^{T} \mathbf{\beta} + \varepsilon
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\mathbf{x} = [1, x_{1}, x_{2}, \dots, x_{n}]^{T}}$ 是包含截距的输入向量， $\small{\mathbf{\beta} = [\beta_{0}, \beta_{1}, \beta_{2}, \dots, \beta_{n}]^{T}}$ 是模型参数（包括截距 $\small{\beta_{0}}$ 和回归系数 $\small{\beta_{1}, \beta_{2}, \cdots, \beta_{n}}$ ）， $\small{\varepsilon}$ 是误差项。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多项式回归&lt;/strong&gt;（Polynomial Regression）：引入高阶特征，使模型能拟合更复杂的非线性关系，属于线性模型的扩展，因为因为它对参数 $\small{\beta}$ 的求解仍然是线性形式，如下面所示的二次关系：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
y = \beta_{0} + \beta_{1} x + \beta_{2} x^{2} + \varepsilon
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非线性回归&lt;/strong&gt;（Nonlinear Regression）：非线性回归完全放弃了线性假设，模型形式可以是任意非线性函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;岭回归&lt;/strong&gt;（Ridge Regression）、&lt;strong&gt;套索回归&lt;/strong&gt;（Lasso Regression）、&lt;strong&gt;弹性网络回归&lt;/strong&gt;（Elastic Net Regression）：在线性回归基础上加入正则化项，用于处理过拟合、多重共线性和特征筛选问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;逻辑回归&lt;/strong&gt;（Logistic Regression）：逻辑回归虽然名字中带“回归”，但实际上是用于分类问题的模型。它通过 Sigmoid 函数将线性组合的输入值映射到区间 $\small{(0, 1)}$ ，表示分类概率，适用于二分类问题；也可以扩展为 Softmax 回归，解决多分类问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
P(y=1 \vert x) = \frac{1}{1 + e^{-(\beta_{0} + \beta_{1} x_{1} + \cdots + \beta_{n} x_{n})}}
$$&lt;/p&gt;
&lt;h4&gt;回归系数的计算&lt;/h4&gt;
&lt;p&gt;建立回归模型的关键是找到最佳的回归系数 $\small{\mathbf{\beta}}$ ，所谓最佳回归系数是指让模型对数据的拟合效果达到最好的模型参数，即能够最小化模型的预测值 $\small{\hat{y_{i}}}$ 与实际观测值 $\small{y_{i}}$ 之间差异的模型参数。为此，我们先定义如下所示的损失函数。&lt;/p&gt;
&lt;p&gt;$$
L(\mathbf{\beta}) = \sum_{i=1}^{m}(y_{i} - \hat{y_{i}})^{2}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{m}$ 表示样本容量，代入回归模型，有：&lt;/p&gt;
&lt;p&gt;$$
L(\mathbf{\beta}) = \sum_{i=1}^{m}(y_{i} - \mathbf{x}_{i}^{T}\mathbf{\beta})^{2}
$$&lt;/p&gt;
&lt;p&gt;如果用矩阵形式表示，有：&lt;/p&gt;
&lt;p&gt;$$
L(\mathbf{\beta}) = (\mathbf{y} - \mathbf{X\beta})^{T}(\mathbf{y} - \mathbf{X\beta})
$$&lt;/p&gt;
&lt;p&gt;其中， $\mathbf{y}$ 是目标值的向量，大小为 $\small{m \times 1}$， $\mathbf{X}$ 是特征矩阵，大小为 $\small{m \times n}$， $\small{\mathbf{\beta}}$ 是回归系数的向量，大小为 $\small{n \times 1}$。&lt;/p&gt;
&lt;p&gt;通过最小化损失函数 $\small{L(\mathbf{\beta})}$ ，我们可以得到线性回归模型的解析解。对 $\small{L(\mathbf{\beta})}$ 求导并令其为 0，有：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial{L(\mathbf{\beta})}}{\partial{\mathbf{\beta}}} = -2\mathbf{X}^{T}(\mathbf{y} - \mathbf{X\beta}) = 0
$$&lt;/p&gt;
&lt;p&gt;整理后得到：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{\beta} = (\mathbf{X}^{T}\mathbf{X})^{-1}\mathbf{X}^{T}\mathbf{y}
$$&lt;/p&gt;
&lt;p&gt;对于矩阵 $\small{\mathbf{X}^{T}\mathbf{X}}$ 不满秩的情况，可以通过添加正则化项的方式使得矩阵可逆，如下所示，这个就是线性回归的解析解。&lt;/p&gt;
&lt;p&gt;$$
\mathbf{\beta} = (\mathbf{X}^{T}\mathbf{X} + \mathbf{\lambda \mit{I}})^{-1}\mathbf{X}^{T}\mathbf{y}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果你对这里提到的正则化不理解可以先放放，后面我们再来讨论这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上述方法适用于小规模数据集，当数据体量不大（样本和特征数量较少）时，计算效率是没有问题的。对于大规模数据集或更为复杂的优化问题，我们可以使用&lt;strong&gt;梯度下降法&lt;/strong&gt;，通过迭代更新参数来逐步逼近最优解。梯度下降法的目标也是最小化损失函数，该方法通过计算梯度方向进行参数更新。梯度是一个向量，包含了目标函数在各个参数方向上的偏导数。对于上面的损失函数 $\small{L(\mathbf{\beta})}$ ，梯度可以表示为：&lt;/p&gt;
&lt;p&gt;$$
\nabla L(\mathbf{\beta}) = \left[ \frac{\partial{L}}{\partial{\beta_{1}}},  \frac{\partial{L}}{\partial{\beta_{2}}}, \cdots,  \frac{\partial{L}}{\partial{\beta_{n}}} \right]
$$&lt;/p&gt;
&lt;p&gt;梯度下降法通过以下更新规则来更新参数 $\small{\mathbf{\beta}}$ ：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{\beta}^{\prime} = \mathbf{\beta} - \alpha \nabla L(\mathbf{\beta}) \\
\mathbf{\beta} = \mathbf{\beta^{\prime}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\alpha}$ 是学习率（step size），通常是一个较小的正数，用于控制每次更新的幅度。如果学习率 $\small{\alpha}$ 选择得当，梯度下降法将收敛到目标函数的局部最小值。如果学习率过大，可能导致震荡不收敛；如果学习率过小，则收敛的速度缓慢，需要更多次的迭代。&lt;/p&gt;
&lt;h4&gt;新数据集介绍&lt;/h4&gt;
&lt;p&gt;之前介绍的鸢尾花数据集并不适合讲解回归模型，为此我们引入另一个经典的汽车 MPG 数据集。汽车 MPG 数据集最初由美国汽车协会提供，我们可以通过该数据集预测车辆的燃油效率，即每加仑燃料行驶的里程（Miles Per Gallon, MPG）。需要注意的是，scikit-learn 库没有内置该数据集，我们可以直接从 &lt;a href=&quot;https://archive.ics.uci.edu/dataset/9/auto+mpg&quot;&gt;UCI 机器学习仓库&lt;/a&gt; 网站下载数据集，也可以通过执行下面的代码联网加载该数据集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ssl
import pandas as pd

ssl._create_default_https_context = ssl._create_unverified_context
df = pd.read_csv(&apos;https://archive.ics.uci.edu/static/public/9/data.csv&apos;)
df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
RangeIndex: 398 entries, 0 to 397
Data columns (total 9 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   car_name      398 non-null    object 
 1   cylinders     398 non-null    int64  
 2   displacement  398 non-null    float64
 3   horsepower    392 non-null    float64
 4   weight        398 non-null    int64  
 5   acceleration  398 non-null    float64
 6   model_year    398 non-null    int64  
 7   origin        398 non-null    int64  
 8   mpg           398 non-null    float64
dtypes: float64(4), int64(4), object(1)
memory usage: 28.1+ KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据上面的输出，我们简单介绍下数据集的九个属性，前面八个都是输入变量（第一个暂不使用），最后一个是输出变量，具体如下表所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名称&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;car_name&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;汽车的名称，字符串，这个属性对建模暂时没有帮助&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;cylinders&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;气缸数量，整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;displacement&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;发动机排量（立方英寸），浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;horsepower&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;马力，浮点数，有空值需要提前处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;weight&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;汽车重量（磅），整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;acceleration&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;加速（0 - 60 mph所需时间），浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;model_year&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;模型年份（1970年 - 1982年），这里用的是两位的年份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;origin&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;汽车来源（1 = 美国, 2 = 欧洲, 3 = 日本），这里的&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;应该视为三种类别而不是整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;mpg&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;车辆的燃油效率，每加仑行驶的里程（目标变量）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们先删除&lt;code&gt;car_name&lt;/code&gt;这个暂时用不上的属性，然后使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;corr&lt;/code&gt;方法检查输入变量（特征）与输出变量（目标值）之间是否存在相关性。通过相关性分析我们可以选择相关性强的特征，剔除掉那些与目标值相关性较弱的特征，这有助于减少模型的复杂性和过拟合的风险。在多元回归中，多重共线性（即输入变量之间高度相关）可能会影响回归系数的估计，导致模型不稳定。可以通过计算特征之间的相关性和方差膨胀因子（VIF）等来检测共线性问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 删除指定的列
df.drop(columns=[&apos;car_name&apos;], inplace=True)
# 计算相关系数矩阵
df.corr()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;corr&lt;/code&gt;方法默认计算皮尔逊相关系数，皮尔逊相关系数适合来自于正态总体的连续值，对于等级数据之间相关性的判定，可以通过修改&lt;code&gt;method&lt;/code&gt;参数为&lt;code&gt;spearmean&lt;/code&gt;或&lt;code&gt;kendall&lt;/code&gt;来计算斯皮尔曼秩相关或肯德尔系数。当然，连续值也可以通过分箱操作处理成等级数据，然后再进行相关性的判定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在使用该数据集建模之前，我们需要做一些准备工作，首先处理掉&lt;code&gt;horsepower&lt;/code&gt;字段的空值，然后将&lt;code&gt;origin&lt;/code&gt;字段处理成&lt;strong&gt;独热编码&lt;/strong&gt;（One-Hot Encoding）。独热编码是一种用于处理分类变量的常见编码方式，通常分类数据（如性别、颜色、季节等）无法直接输入机器学习模型进行训练，因为大多数算法只能处理数值数据。独热编码通过将每个分类变量转换为若干个新的二元特征（&lt;code&gt;0&lt;/code&gt;或&lt;code&gt; 1&lt;/code&gt;）来表示，从而使得这些变量可以输入到机器学习模型中。假设我们有一个叫“颜色”的特征列，可能的取值有&lt;code&gt;红色&lt;/code&gt;、&lt;code&gt;绿色&lt;/code&gt;和&lt;code&gt;蓝色&lt;/code&gt;，我们可以将其通过独热编码转换成三个二元特征，如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;红色&lt;/th&gt;
&lt;th&gt;绿色&lt;/th&gt;
&lt;th&gt;蓝色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：我们也可以只保留绿色和蓝色两个列，如果两个列的取值都为&lt;code&gt;0&lt;/code&gt;，那么说明我们的颜色是红色。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;独热编码方法简单直观，容易理解和实现。对于无序类别独热编码是非常有效的，因为它不会引入任何虚假的顺序关系，处理后的数据类型是数值型的，很多机器学习算法都能很好的处理。当然，如果类别特征有大量不同的类别取值，独热编码会生成大量的新特征，可能导致数据的维度大幅增加，从而影响计算性能和存储效率，尤其是在数据中有很多稀疏类别时。&lt;/p&gt;
&lt;p&gt;下面的代码实现了对数据的预处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 删除有缺失值的样本
df.dropna(inplace=True)
# 将origin字段处理为类别类型
df[&apos;origin&apos;] = df[&apos;origin&apos;].astype(&apos;category&apos;) 
# 将origin字段处理为独热编码
df = pd.get_dummies(df, columns=[&apos;origin&apos;], drop_first=True)
df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     cylinders  displacement  horsepower  weight  ...  model_year   mpg  origin_2  origin_3
0            8         307.0       130.0    3504  ...          70  18.0     False     False
1            8         350.0       165.0    3693  ...          70  15.0     False     False
2            8         318.0       150.0    3436  ...          70  18.0     False     False
3            8         304.0       150.0    3433  ...          70  16.0     False     False
4            8         302.0       140.0    3449  ...          70  17.0     False     False
..         ...           ...         ...     ...  ...         ...   ...       ...       ...
393          4         140.0        86.0    2790  ...          82  27.0     False     False
394          4          97.0        52.0    2130  ...          82  44.0      True     False
395          4         135.0        84.0    2295  ...          82  32.0     False     False
396          4         120.0        79.0    2625  ...          82  28.0     False     False
397          4         119.0        82.0    2720  ...          82  31.0     False     False

[392 rows x 9 columns]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面调用 pandas 的&lt;code&gt;get_dummies&lt;/code&gt;函数将&lt;code&gt;origin&lt;/code&gt;列处理成了独热编码，由于将&lt;code&gt;drop_first&lt;/code&gt;参数设置为&lt;code&gt;True&lt;/code&gt;，所以原来的取值&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;只保留了两个列，分别叫&lt;code&gt;origin_2&lt;/code&gt;和&lt;code&gt;origin_3&lt;/code&gt;。Scikit-learn 库中&lt;code&gt;preprocessing&lt;/code&gt;模块的&lt;code&gt;OneHotEncoder&lt;/code&gt;也支持将类别特征处理成独热编码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来，我们还是将数据集拆分为训练集和测试集，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import train_test_split

X, y = df.drop(columns=&apos;mpg&apos;).values, df[&apos;mpg&apos;].values
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;线性回归代码实现&lt;/h4&gt;
&lt;p&gt;我们首先使用 scikit-learn 库&lt;code&gt;linear_model&lt;/code&gt;模块的&lt;code&gt;LinearRegression&lt;/code&gt;来创建线性回归模型，&lt;code&gt;LinearRegression&lt;/code&gt;使用最小二乘法计算回归模型的参数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.linear_model import LinearRegression

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想查看线性回归模型的参数（回归系数和截距），可以通过下面的代码来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;回归系数:&apos;, model.coef_)
print(&apos;截距:&apos;, model.intercept_)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;回归系数: [-0.70865621  0.03138774 -0.03034065 -0.0064137   0.06224274  0.82866534
  3.20888265  3.68252848]
截距: -21.685482718950933
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;回归模型的评估&lt;/h4&gt;
&lt;p&gt;回归模型的预测效果到底如何，我们可以通过下面的指标对其进行评估。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;均方误差（Mean Squared Error, MSE）。MSE 是回归模型最常用的评估指标之一，定义为预测值与真实值误差的平方平均值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{MSE} = \frac{1}{m} \sum_{i=1}^{m}(y_{i} - \hat{y_{i}})^{2}
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;均方根误差（Root Mean Squared Error, RMSE）。RMSE 是 MSE 的平方根形式，用于更直观地衡量误差的实际尺度（单位与目标变量一致）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{RMSE} = \sqrt{\text{MSE}} = \sqrt{\frac{1}{m} \sum_{i=1}^{m}(y_{i} - \hat{y_{i}})^{2}}
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;平均绝对误差（Mean Absolute Error, MAE）。MAE 是另一个常用的误差度量指标，定义为预测值与真实值误差的绝对值平均值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\text{MAE} = \frac{1}{m} \sum_{i=1}^{m} \lvert y_{i} - \hat{y_{i}} \rvert
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;决定系数（R-Squared, $\small{R^{2}}$）。 $\small{R^{2}}$ 是一个相对指标，用于衡量模型对数据的拟合程度，其值越接近 1 越好。 $\small{R^{2}}$ 的计算公式为：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
R^{2} = 1 - \frac{SS_{res}}{{SS}_{tot}}
$$&lt;/p&gt;
&lt;p&gt;其中，  $\small{SS_{res} = \sum_{i=1}^{m}(y_{i} - \hat{y_{i}})^{2}}$ 为残差平方和； $\small{SS_{tot} = \sum_{i=1}^{m} (y_{i} - \bar{y})^{2}}$ 为总平方和，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/18890d6359.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上图左边红色正方形的面积之和就是总平方和，右边蓝色正方形的面积之和就是残差平方和，很显然，模型拟合的效果越好，残差平方和除以总平方和的值就越接近 0， $\small{R^{2}}$ 的值就越接近 1。通常 $\small{R^{2} \ge 0.8}$ 时，我们认为模型的拟合效果已经很不错了。&lt;/p&gt;
&lt;p&gt;可以使用 scikit-learn 中封装好的函数计算出均方误差、平均绝对误差和 $\small{R^{2}}$ 的值，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

mse = mean_squared_error(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f&apos;均方误差: {mse:.4f}&apos;)
print(f&apos;平均绝对误差: {mae:.4f}&apos;)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;均方误差: 13.1215
平均绝对误差: 2.8571
决定系数: 0.7848
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;引入正则化项&lt;/h4&gt;
&lt;p&gt;岭回归是在线性回归的基础上引入 $\small{L2}$ 正则化项，目的是防止模型过拟合，尤其是当特征数较多或特征之间存在共线性时。岭回归的损失函数如下所示：&lt;/p&gt;
&lt;p&gt;$$
L(\beta) = \sum_{i=1}^{m}{(y_{i} - \hat{y_{i}})^{2}} + \lambda \cdot \sum_{j=1}^{n}{\beta_{j}^{2}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{L2}$ 正则化项 $\small{\lambda \sum_{j=1}^{n} \beta_{j}^{2}}$ 会惩罚较大的回归系数，相当于缩小了回归系数的大小，但不会使系数为 0（即不会进行特征选择）。可以通过 scikit-learn 库&lt;code&gt;linear_model&lt;/code&gt;模块的&lt;code&gt;Ridge&lt;/code&gt;类实现岭回归，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.linear_model import Ridge

model = Ridge()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(&apos;回归系数:&apos;, model.coef_)
print(&apos;截距:&apos;, model.intercept_)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f&apos;均方误差: {mse:.4f}&apos;)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;回归系数: [-0.68868217  0.03023126 -0.0291811  -0.00642523  0.06312298  0.82583962
  3.04105754  3.49988826]
截距: -21.390402697674855
均方误差: 12.9604
决定系数: 0.7874
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;套索回归引入 $\small{L1}$ 正则化项，不仅防止过拟合，还具有特征选择的功，特别适用于高维数据。套索回归的损失函数如下所示：&lt;/p&gt;
&lt;p&gt;$$
L(\mathbf{\beta}) = \sum_{i=1}^{m}{(y_{i} - \hat{y_i})^{2}} + \lambda \cdot \sum_{j=1}^{n}{\lvert \beta_{j} \rvert}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{L1}$ 正则化项 $\small{\lambda \sum_{j=1}^{n} \lvert \beta_{j} \rvert}$ 会将某些不重要的回归系数缩减为 0，从而实现特征选择。可以通过 scikit-learn 库&lt;code&gt;linear_model&lt;/code&gt;模块的&lt;code&gt;Lasso&lt;/code&gt;类实现套索回归，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.linear_model import Lasso

model = Lasso()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(&apos;回归系数:&apos;, model.coef_)
print(&apos;截距:&apos;, model.intercept_)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f&apos;均方误差: {mse:.4f}&apos;)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;回归系数: [-0.00000000e+00  4.46821248e-04 -1.22830326e-02 -6.29725191e-03
  0.00000000e+00  6.91590631e-01  0.00000000e+00  0.00000000e+00]
截距: -9.109888229245005
均方误差: 11.1035
决定系数: 0.8179
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面代码运行结果中的回归系数，有四个特征的回归系数被设置为 0，相当于从 8 个特征中选出了 4 个重要的特征。模型的拟合效果是优于之间的回归模型的，这一点从均方误差和决定系数可以看出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;弹性网络回归结合了岭回归和套索回归的优点，通过同时引入 $\small{L1}$ 和 $\small{L2}$ 正则化项，适用于高维数据且特征之间存在相关的情况，其损失函数如下所示：&lt;/p&gt;
&lt;p&gt;$$
L(\mathbf{\beta}) = \sum_{i=1}^{m}{(y_{i} - \hat{y_i})^{2}} + \alpha \cdot \lambda \sum_{j=1}^{n}{\lvert \beta_{j} \rvert} + (1 - \alpha) \cdot \lambda \cdot \sum_{j=1}^{n}{\beta_{j}^{2}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\alpha}$ 是控制 $\small{L1}$ 和 $\small{L2}$ 正则化的权重比例。&lt;/p&gt;
&lt;h4&gt;线性回归另一种实现&lt;/h4&gt;
&lt;p&gt;上面我们提到过，除了最小二乘法我们还可以使用梯度下降法来求解回归模型的参数，scikit-learn 库&lt;code&gt;linear_model&lt;/code&gt;模块的&lt;code&gt;SGDRegressor&lt;/code&gt;就使用了这种方法，SGD 就是 Stochastic Gradient Descent 的缩写。随机梯度下降每次迭代只使用一个随机样本来计算梯度，计算速度快，适合大规模数据集，而且可以跳出局部最优解。需要注意的是它的学习率，如果学习率设置得不合理，它的收敛性可能会发生波动，通常需要使用学习率衰减策略来促进收敛。此外，随机梯度下降对特征的尺度非常敏感，通常需要在训练之前对特征进行标准化或归一化处理，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler

# 对特征进行选择和标准化处理
scaler = StandardScaler()
scaled_X = scaler.fit_transform(X[:, [1, 2, 3, 5]])
# 重新拆分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(scaled_X, y, train_size=0.8, random_state=3)

# 模型的创建、训练和预测
model = SGDRegressor()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(&apos;回归系数:&apos;, model.coef_)
print(&apos;截距:&apos;, model.intercept_)

# 模型评估
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f&apos;均方误差: {mse:.4f}&apos;)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;回归系数: [-0.25027084 -0.41349219 -4.9559786   2.83009217]
截距: [23.48707219]
均方误差: 11.3853
决定系数: 0.8133
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，我们还需要强调一下&lt;code&gt;SGDRegressor&lt;/code&gt;构造函数几个重要的参数，也是回归模型比较重要的超参数，如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;loss&lt;/code&gt;：指定优化目标（损失函数），默认值为&lt;code&gt;&apos;squared_error&apos;&lt;/code&gt;（最小二乘法），其他可以选择的值有：&lt;code&gt;&apos;huber&apos;&lt;/code&gt;、&lt;code&gt;&apos;epsilon_insensitive&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;squared_epsilon_insensitive&apos;&lt;/code&gt;，其中&lt;code&gt;&apos;huber&apos;&lt;/code&gt;适用于对异常值更鲁棒的回归模型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;penalty&lt;/code&gt;：指定正则化方法，用于防止过拟合，默认为&lt;code&gt;&apos;l2&apos;&lt;/code&gt;（L2 正则化），其他可以选择的值有：&lt;code&gt;&apos;l1&apos;&lt;/code&gt;（L1正则化）、&lt;code&gt;&apos;elasticnet&apos;&lt;/code&gt;（弹性网络，L1 和 L2 的组合）、&lt;code&gt;None&lt;/code&gt;（不使用正则化）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;alpha&lt;/code&gt;：正则化强度的系数，控制正则化项的权重，默认值为&lt;code&gt;0.0001&lt;/code&gt;。较大的 &lt;code&gt;alpha&lt;/code&gt; 值会加重正则化的影响，从而限制模型复杂度；较小的值会让模型更关注训练数据的拟合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;l1_ratio&lt;/code&gt;：当 &lt;code&gt;penalty=&apos;elasticnet&apos;&lt;/code&gt; 时，控制 L1 和 L2 正则化之间的权重，默认值为&lt;code&gt;0.15&lt;/code&gt;，取值范围为&lt;code&gt;[0, 1]&lt;/code&gt;（&lt;code&gt;0&lt;/code&gt; 表示完全使用 L2，&lt;code&gt;1&lt;/code&gt; 表示完全使用 L1）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tol&lt;/code&gt;：优化算法的容差，即判断收敛的阈值，默认值为&lt;code&gt;1e-3&lt;/code&gt;。当目标函数的改变量小于 &lt;code&gt;tol&lt;/code&gt; 时，训练会提前终止；如果希望训练更加精确，可以适当降低 &lt;code&gt;tol&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;learning_rate&lt;/code&gt;：指定学习率的调节策略，默认值为&lt;code&gt;&apos;constant&apos;&lt;/code&gt;，表示使用固定学习率，具体的值由 &lt;code&gt;eta0&lt;/code&gt; 指定；其他可选项包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;optimal&apos;&lt;/code&gt;：基于公式&lt;code&gt;eta = 1.0 / (alpha * (t + t0))&lt;/code&gt;自动调整。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;invscaling&apos;&lt;/code&gt;：按 &lt;code&gt;eta = eta0 / pow(t, power_t)&lt;/code&gt; 缩放学习率。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;adaptive&apos;&lt;/code&gt;：动态调整，误差减少时保持当前学习率，否则减小学习率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;eta0&lt;/code&gt;：初始学习率，默认值为&lt;code&gt;0.01&lt;/code&gt;，当 &lt;code&gt;learning_rate=&apos;constant&apos;&lt;/code&gt; 或其他策略使用时，&lt;code&gt;eta0&lt;/code&gt; 决定了初始更新步长。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;power_t&lt;/code&gt;：当 &lt;code&gt;learning_rate=&apos;invscaling&apos;&lt;/code&gt; 时，控制学习率衰减速度，默认值为&lt;code&gt;0.25&lt;/code&gt;。较小的值会让学习率下降得更慢，从而更长时间地关注全局优化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;early_stopping&lt;/code&gt;：是否启用早停机制，默认值为&lt;code&gt;False&lt;/code&gt;。如果设置为 &lt;code&gt;True&lt;/code&gt;，模型会根据验证集性能自动停止训练，防止过拟合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;validation_fraction&lt;/code&gt;：指定用作验证集的训练数据比例，默认值为&lt;code&gt;0.1&lt;/code&gt;。当 &lt;code&gt;early_stopping=True&lt;/code&gt; 时，该参数会起作用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;max_iter&lt;/code&gt;：训练的最大迭代次数，默认值为&lt;code&gt;1000&lt;/code&gt;。当数据较大或学习率较小时，可能需要增加迭代次数以保证收敛。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;shuffle&lt;/code&gt;：是否在每个迭代轮次开始时打乱训练数据，默认值为&lt;code&gt;True&lt;/code&gt;，表示打乱数据。打乱数据有助于提高模型的泛化能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;warm_start&lt;/code&gt;：是否使用上次训练的参数继续训练，默认值为&lt;code&gt;False&lt;/code&gt;。当设置为 &lt;code&gt;True&lt;/code&gt; 时，可以在已有模型的基础上进一步优化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;verbose&lt;/code&gt;：控制训练过程的日志输出，默认值为&lt;code&gt;0&lt;/code&gt;，可以设置为更高值以观察训练进度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;多项式回归&lt;/h4&gt;
&lt;p&gt;有的时候，我们关心的自变量和因变量之间并不是简单的线性关系，例如广告投入与销售额增长的关系、设备的使用时间与故障率之间的关系等。除此以外，如果数据中存在明显的拐点或者要通过简单的公式来近似某些复杂的现象，线性回归模型可能并不能满足这样的需求，这个时候我们就需要建立多项式回归模型。&lt;/p&gt;
&lt;p&gt;下面我们用一个简单的例子对多项式回归加以说明，我们先生成一组数据点并绘制出对应的散点图，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 6, 150)
y = x ** 2 - 4 * x + 3 + np.random.normal(1, 1, 150)
plt.scatter(x, y)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/806feb869a.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;显然，这样的一组数据点是很难通过线性模型进行拟合的，下面的代码可以证明这一点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x_ = x.reshape(-1, 1)

model = LinearRegression()
model.fit(x_, y)
a, b = model.coef_[0], model.intercept_
y_pred = a * x + b
plt.scatter(x, y)
plt.plot(x, y_pred, color=&apos;r&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/3f9d053254.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;很显然，这是一个欠拟合的结果。我们再看看 $\small{R^{2}}$ 的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r2 = r2_score(y, y_pred)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;决定系数: 0.5933
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scikit-learn 库&lt;code&gt;preprocessing&lt;/code&gt;模块中的&lt;code&gt;PolynomialFeatures&lt;/code&gt;类可以将原始特征扩展为多项式特征，从而将线性模型转换为具有高次项的模型。创建&lt;code&gt;PolynomialFeatures&lt;/code&gt;对象时有几个重要的参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;degree&lt;/code&gt;：设置多项式的最高次项。例如，&lt;code&gt;degree=3&lt;/code&gt; 会生成包含一次项、二次项和三次项的特征。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interaction_only&lt;/code&gt;：默认值为&lt;code&gt;False&lt;/code&gt;，如果设置为&lt;code&gt;True&lt;/code&gt;，则只生成交互项（如 $\small{x_{1}x_{2}}$ ），不生成单独的高次项（如 $\small{x_{1}^{2}}$ 、 $\small{x_{2}^{2}}$ ）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;include_bias&lt;/code&gt;：默认值为&lt;code&gt;True&lt;/code&gt;，表示包括常数项（通常为 1），设置为&lt;code&gt;False&lt;/code&gt;则不包括常数项。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们通过代码来演示如何通过&lt;code&gt;PolynomialFeatures&lt;/code&gt;类进行特征预处理，实现多项式回归。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2)
x_ = poly.fit_transform(x_)

model = LinearRegression()
model.fit(x_, y)
y_pred = model.predict(x_)
r2 = r2_score(y, y_pred)
print(f&apos;决定系数: {r2:.4f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;决定系数: 0.9497
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过特征预处理引入了高次项之后，模型拟合的效果得到了明显的改善。如果希望只对一部分特征添加高次项，可以使用 scikit-learn 库&lt;code&gt;compose&lt;/code&gt;模块的&lt;code&gt;ColumnTransformer&lt;/code&gt;来定义处理规则，有兴趣的读者可以自行研究。需要注意的是，多项式回归时随着高次项的引入，模型发生过拟合的风险增加会大大增加，尤其在数据量较小时；另一方面，高次项的值可能会导致特征范围变得很大，要考虑对特征进行标准化处理。&lt;/p&gt;
&lt;h4&gt;逻辑回归&lt;/h4&gt;
&lt;p&gt;逻辑回归尽管名字中含有“回归”，但逻辑回归实际上是一种分类算法，用于处理二分类问题，例如电子邮件是不是垃圾邮件、用户是否会点击广告、信用卡客户是否存在违约风险等。逻辑回归的核心思想是通过 Sigmoid 函数将线性回归的输出映射到$\small{(0, 1)}$区间，作为对分类概率的预测。Sigmoid 函数的曲线如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/62f198d825.webp&quot; style=&quot;zoom:58%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;下面，我们用 scikit-learn 库&lt;code&gt;datasets&lt;/code&gt;模块提供的&lt;code&gt;make_classification&lt;/code&gt;函数生成一组模拟数据， 通过逻辑回归来构建分类预测模型，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

# 生成1000条样本数据，每个样本包含6个特征
X, y = make_classification(n_samples=1000, n_features=6, random_state=3)
# 将1000条样本数据拆分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 创建和训练逻辑回归模型
model = LogisticRegression()
model.fit(X_train, y_train)

# 对测试集进行预测并评估
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       0.95      0.93      0.94       104
           1       0.93      0.95      0.94        96

    accuracy                           0.94       200
   macro avg       0.94      0.94      0.94       200
weighted avg       0.94      0.94      0.94       200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，我们再强调一下&lt;code&gt;LogisticRegression&lt;/code&gt;构造函数几个重要的参数，也是逻辑回归模型比较重要的超参数，如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;penalty&lt;/code&gt;：指定正则化类型，用于控制模型复杂度，防止过拟合，默认值为&lt;code&gt;l2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C&lt;/code&gt;：正则化强度的倒数，默认值为&lt;code&gt;1.0&lt;/code&gt;。较小的 &lt;code&gt;C&lt;/code&gt; 值会加强正则化（更多限制模型复杂度），较大的 &lt;code&gt;C&lt;/code&gt; 值会减弱正则化（更注重拟合训练数据）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;solver&lt;/code&gt;：指定优化算法，默认值为&lt;code&gt;lbfgs&lt;/code&gt;，可选值包括：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;newton-cg&apos;&lt;/code&gt;、&lt;code&gt;&apos;lbfgs&apos;&lt;/code&gt;、&lt;code&gt;&apos;sag&apos;&lt;/code&gt;、&lt;code&gt;&apos;saga&apos;&lt;/code&gt;：支持 L2 和无正则化。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;liblinear&apos;&lt;/code&gt;：支持 L1 和 L2 正则化，适用于小型数据集。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;saga&apos;&lt;/code&gt;：支持 L1、L2 和 ElasticNet，适用于大规模数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;multi_class&lt;/code&gt;：指定多分类问题的处理方式，默认值为&lt;code&gt;&apos;auto&apos;&lt;/code&gt;，根据数据选择 &lt;code&gt;&apos;ovr&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;multinomial&apos;&lt;/code&gt;，前者表示一对多策略，适合二分类或多分类的基础情况，后者表示多项式回归策略，适用于多分类问题，需与 &lt;code&gt;&apos;lbfgs&apos;&lt;/code&gt;、&lt;code&gt;&apos;sag&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;saga&apos;&lt;/code&gt; 搭配使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fit_intercept&lt;/code&gt;：是否计算截距（偏置项），默认值为&lt;code&gt;True&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class_weight&lt;/code&gt;：类别权重，处理类别不平衡问题，默认值为&lt;code&gt;None&lt;/code&gt;，设置为&lt;code&gt;&apos;balanced&apos;&lt;/code&gt;可以根据类别频率自动调整权重。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：逻辑回归有些超参数跟我们之前讲的&lt;code&gt;SGDRegressor&lt;/code&gt;是类似的，此处不再进行赘述。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;回归模型是一种统计分析方法，用于建立自变量与因变量之间的关系。它通过拟合数据来预测目标变量的值，在经济学、工程、医学等领域有着广泛的应用，可以帮助决策者进行数据驱动的预测和分析。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;86 K-Means聚类算法&lt;/h2&gt;
&lt;p&gt;聚类（Clustering）是数据挖掘和机器学习中的一种重要技术，用于将数据集中的样本划分为多个相似的组或类别。这种方法在许多领域得到了广泛应用，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;电商行业&lt;/strong&gt;：根据用户行为数据，将消费者分为不同的群体（高消费用户、高活跃用户、流失风险用户、价格敏感用户等）以制定有针对性的运营策略，实现更精准的广告投放，代表性的企业包括阿里巴巴、亚马逊（Amazon）等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金融行业&lt;/strong&gt;：各大银行通过聚类算法分析用户的信用记录、收入水平、消费行为等数据，将用户分为不同的风险群体，对于高风险群体需要更严格的信用审核，而低风险群体可以享受更优惠的贷款利率或信用额度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;医疗行业&lt;/strong&gt;：通过分析患者的健康数据（如病史、基因数据、生活习惯等），将患者分为不同健康风险群体，提高疾病预测的准确性，推动了精准医疗的发展，代表性的公司如强生（Johnson &amp;amp; Johnson）、辉瑞（Pfizer）等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社交媒体&lt;/strong&gt;：通过分析用户的好友关系、兴趣爱好、社交互动等数据，将用户划分为不同的社交圈，用于推荐好友、定制个性化内容以及优化了平台上的信息流推荐系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;聚类是一种&lt;strong&gt;无监督学习&lt;/strong&gt;，因为它不需要预先定义的标签，只是根据数据特征去学习，通过度量特征相似度或者距离，然后把已知的数据集划分成若干个不同的类别。与分类不同，聚类任务的目标是发现数据内在的结构。聚类算法大体上可以分为：&lt;strong&gt;基于距离的聚类&lt;/strong&gt;、&lt;strong&gt;基于密度的聚类&lt;/strong&gt;、&lt;strong&gt;层次聚类&lt;/strong&gt;、&lt;strong&gt;谱聚类&lt;/strong&gt;等。如果你还分不清楚聚类和分类到底有什么区别，相信下面的图可以帮到你。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/fad8725b20.webp&quot; style=&quot;zoom:38%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;算法原理&lt;/h4&gt;
&lt;p&gt;下面我们重点为大家介绍名为 K-Means 的聚类算法。K-Means 是一种基于原型的分区聚类方法，其目标是将数据集划分为K个簇，并使每个簇内的数据点尽可能相似。K-Means 算法的实施步骤如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化簇中心&lt;/strong&gt;：随机选择K个样本作为初始簇中心，簇中心通常也称为质心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配样本到最近的质心&lt;/strong&gt;：计算每个样本与所有质心的距离，将样本分配到最近的簇。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新质心&lt;/strong&gt;：计算每个簇的所有样本的均值，并将其作为新的质心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重复步骤2和步骤3&lt;/strong&gt;，直到质心收敛或达到预设的迭代次数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数学描述&lt;/h4&gt;
&lt;p&gt;我们将上面的算法原理用数学语言进行描述。对于给定的数据集 ，K-Means 算法的目标是最小化目标函数（总误差平方和）。目标函数如下所示：&lt;/p&gt;
&lt;p&gt;$$
J = \sum_{i=1}^{K} \sum_{x \in C_{i}} {\lVert x - \mu_{i} \rVert}^{2}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{K}$ 是簇的数量， $\small{C_{i}}$ 表示第 $\small{i}$ 个簇中的样本集合， $\small{\mu_{i}}$ 是第 $\small{i}$ 个簇的中心， $\small{x}$ 是数据点。因为这个问题属于 NP 困难组合优化问题，所以在实际求解时我们会采用迭代的方式来寻求满意解。&lt;/p&gt;
&lt;p&gt;首先随机选择 $\small{K}$ 个点作为初始质心 $\small{\mu_{1}, \mu_{2}, \cdots, \mu_{K}}$ ，对于每个数据点 $\small{x_{j}}$ ，计算到每个质心的距离，选择距离最近的质心，即：&lt;/p&gt;
&lt;p&gt;$$
C_{i} = \lbrace {x_{j} \ \vert \ {\lVert x_{j} - \mu_{i} \rVert}^{2} \le {\lVert x_{j} - \mu_{k} \rVert}^{2} \ \text{for all} \ k \ne i} \rbrace
$$&lt;/p&gt;
&lt;p&gt;更新质心为簇中所有点的均值，即：&lt;/p&gt;
&lt;p&gt;$$
\mu_{i} = \frac{1}{\lvert C_{i} \rvert}\sum_{x \in C_{i}} x
$$&lt;/p&gt;
&lt;p&gt;重复上面两个动作，直到质心不再变化或变化小于某个阈值，这就确保了算法的收敛性。&lt;/p&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;下面我们用 Python 代码实现 K-Means 聚类，我们先暂时不使用 scikit-learn 库，主要帮助大家理解算法的工作原理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np


def distance(u, v, p=2):
    &quot;&quot;&quot;计算两个向量的距离&quot;&quot;&quot;
    return np.sum(np.abs(u - v) ** p) ** (1 / p)


def init_centroids(X, k):
    &quot;&quot;&quot;随机选择k个质心&quot;&quot;&quot;
    index = np.random.choice(np.arange(len(X)), k, replace=False)
    return X[index]


def closest_centroid(sample, centroids):
    &quot;&quot;&quot;找到跟样本最近的质心&quot;&quot;&quot;
    distances = [distance(sample, centroid) for i, centroid in enumerate(centroids)]
    return np.argmin(distances)


def build_clusters(X, centroids):
    &quot;&quot;&quot;根据质心将数据分成簇&quot;&quot;&quot;
    clusters = [[] for _ in range(len(centroids))]
    for i, sample in enumerate(X):
        centroid_index = closest_centroid(sample, centroids)
        clusters[centroid_index].append(i)
    return clusters


def update_centroids(X, clusters):
    &quot;&quot;&quot;更新质心的位置&quot;&quot;&quot;
    return np.array([np.mean(X[cluster], axis=0) for cluster in clusters])


def make_label(X, clusters):
    &quot;&quot;&quot;生成标签&quot;&quot;&quot;
    labels = np.zeros(len(X))
    for i, cluster in enumerate(clusters):
        for j in cluster:
            labels[j] = i
    return labels


def kmeans(X, *, k, max_iter=1000, tol=1e-4):
    &quot;&quot;&quot;KMeans聚类&quot;&quot;&quot;
    # 随机选择k个质心
    centroids = init_centroids(X, k)
    # 通过不断的迭代对数据进行划分
    for _ in range(max_iter):
        # 通过质心将数据划分到不同的簇
        clusters = build_clusters(X, centroids)
        # 重新计算新的质心的位置
        new_centroids = update_centroids(X, clusters)
        # 如果质心几乎没有变化就提前终止迭代
        if np.allclose(new_centroids, centroids, rtol=tol):
            break
        # 记录新的质心的位置
        centroids = new_centroids
    # 给数据生成标签
    return make_label(X, clusters), centroids
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们仍然以鸢尾花数据集为例，看看我们自己实现的&lt;code&gt;kmeans&lt;/code&gt;函数能否为将三种鸢尾花划分为三个不同的类别。由于是无监督学习，这里我们直接把整个数据集带入&lt;code&gt;kmeans&lt;/code&gt;函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target
labels, centers = kmeans(X, k=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，千万不要直接拿&lt;code&gt;y_pred&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;进行比较，我们之前说过，聚类算法并不知道数据对应的标签，它只是根据特征将数据划分为不同的类别，这里输出的&lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt; 并不直接对应到山鸢尾、多彩鸢尾和为吉尼亚鸢尾。我们可以用可视化的方式来看看预测的结果，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

colors = [&apos;#FF6969&apos;, &apos;#050C9C&apos;, &apos;#365E32&apos;]
markers = [&apos;o&apos;, &apos;x&apos;, &apos;^&apos;]

plt.figure(dpi=200)
for i in range(len(centers)):
    samples = X[labels == i]
    print(markers[i])
    plt.scatter(samples[:, 2], samples[:, 3], marker=markers[i], color=colors[i])
    plt.scatter(centers[i, 2], centers[i, 3], marker=&apos;*&apos;, color=&apos;r&apos;, s=120)

plt.xlabel(&apos;Petal length&apos;)
plt.ylabel(&apos;Petal width&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9a0bc3a591.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们用原始数据重新输出，跟上面的图做一个对比，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

colors = [&apos;#FF6969&apos;, &apos;#050C9C&apos;, &apos;#365E32&apos;]
markers = [&apos;o&apos;, &apos;x&apos;, &apos;^&apos;]

plt.figure(dpi=200)
for i in range(len(centers)):
    samples = X[y == i]
    plt.scatter(samples[:, 2], samples[:, 3], marker=markers[i], color=colors[i])

plt.xlabel(&apos;Petal length&apos;)
plt.ylabel(&apos;Petal width&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/0595f7cae7.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;直接使用 scikit-learn 库&lt;code&gt;cluster&lt;/code&gt;模块的&lt;code&gt;KMeans&lt;/code&gt;类实现 K-Means 聚类是更好的选择，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.cluster import KMeans

# 创建KMeans对象
km_cluster = KMeans(
    n_clusters=3,       # k值（簇的数量）
    max_iter=30,        # 最大迭代次数
    n_init=10,          # 初始质心选择尝试次数
    init=&apos;k-means++&apos;,   # 初始质心选择算法
    algorithm=&apos;elkan&apos;,  # 是否使用三角不等式优化
    tol=1e-4,           # 质心变化容忍度
    random_state=3      # 随机数种子
)
# 训练模型
km_cluster.fit(X)
print(km_cluster.labels_)           # 分簇的标签
print(km_cluster.cluster_centers_)  # 各个质心的位置
print(km_cluster.inertia_)          # 样本到质心的距离平方和
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们对&lt;code&gt;KMeans&lt;/code&gt;类的几个超参数加以说明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;n_clusters&lt;/code&gt;：指定聚类的簇数，即 $\small{K}$ 值，默认值为&lt;code&gt;8&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_iter&lt;/code&gt;：最大迭代次数，默认值为&lt;code&gt;300&lt;/code&gt;，控制每次初始化中 K-Means 迭代的最大步数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;init&lt;/code&gt;：初始化质心的方法，默认值为&lt;code&gt;&apos;k-means++&apos;&lt;/code&gt;，表示从数据中多次随机选取 K 个质心，每次都计算这一次选中的中心点之间的距离，然后取距离最大的一组作为初始化中心点，推荐大家使用这个值；如果设置为&lt;code&gt;&apos;random&apos;&lt;/code&gt;则随机选择初始质心。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n_init&lt;/code&gt;：和上面的参数配合，指定算法运行的初始化次数，默认值为&lt;code&gt;10&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;algorithm&lt;/code&gt;：K-Means 的计算算法，默认值为&lt;code&gt;&apos;lloyd&apos;&lt;/code&gt;。还有一个可选的值为&lt;code&gt;&apos;elkan&apos;&lt;/code&gt;，表示基于三角不等式的优化算法，适用于 K 值较大的情况，计算效率较高。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tol&lt;/code&gt;：容忍度，控制算法的收敛精度，默认值为&lt;code&gt;1e-4&lt;/code&gt;。如果数据集较大时，可适当增大此值以加快收敛速度。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;K-Means 是一种经典的聚类算法，它的优点包括实现简单，算法收敛速度快；缺点是结果不稳定（跟初始值设定有关系），无法解决样本不均衡的问题，容易收敛到局部最优解，受噪声数据影响较大。如果你想通过可视化的方式理解聚类的过程，我们给大家推荐一个名为 Naftali 的人的博客，该网站上提供了可视化的方式展示 K-Means 和 DBSCAN 聚类（一种基于密度的聚类算法），如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/a70de57b8e.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/1aad81a606.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;87 集成学习算法&lt;/h2&gt;
&lt;p&gt;之前的章节，我们主要为大家介绍了机器学习中的单模型。事实上，将多个单模型组合成一个综合模型的方式早已成为现代机器学习模型采用的主流方法，这种方法被称为&lt;strong&gt;集成学习&lt;/strong&gt;（ensemble learning）。集成学习的目标是通过多个弱学习器（分类效果略优于随机猜测的模型，如果太强容易导致过拟合）的组合来构建强学习器，从而克服单一模型可能存在的局限性，获得比单一模型&lt;strong&gt;更好的泛化能力&lt;/strong&gt;，通常用于&lt;strong&gt;需要高精度预测的场景&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;算法分类&lt;/h4&gt;
&lt;p&gt;集成学习算法主要分为以下几类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bagging&lt;/strong&gt;：通过从训练数据中随机抽样生成多个数据子集，在这些子集上训练多个模型，并将它们的结果进行结合。这种集成学习的原理如下图所示，最典型的例子就是我们之前讲过的随机森林。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9dcdb600f1.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Boosting&lt;/strong&gt;：通过迭代训练多个模型，在每一轮训练时，重点关注前一轮预测错误的样本。每个新模型的训练目标是弥补前一轮模型的不足。这种集成学习的原理如下图所示，经典的算法有 AdaBoost、Gradient Boosting 和 XGBoost。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/e3c92345e7.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;Boosting的基本原理是：初始时对所有样本赋予相同的权重；训练第一个模型时，错误分类的样本权重会增加；训练下一个模型时，重点关注之前模型错误分类的样本；最终将所有模型的结果加权组合（表现好的模型会有更高的权重），得到最终的输出。简单的说，Boosting 就是串行的训练一系列弱分类器，使得被先前弱分类器分类错误的样本在后续得到更多关注，最后将这些分类器组合成最优强分类器的过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stacking&lt;/strong&gt;：通过训练多个模型，将它们的预测结果作为新特征输入到另一个模型（通常称为“二级模型”）中，用“二级模型”来做最终的预测，原理如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/461ca25e7e.webp&quot; style=&quot;zoom:36%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;AdaBoost&lt;/h4&gt;
&lt;p&gt;AdaBoost（Adaptive Boosting）由 Yoav Freund 和 Robert Schapire 于 1996 年提出，是一种经典的集成学习算法，通常将其翻译为自适应提升算法。AdaBoost 的做法非常朴素，一是&lt;strong&gt;提高前一轮被弱分类器分类错误的样本的权重&lt;/strong&gt;，二是对多个弱分类器进行线性组合，&lt;strong&gt;提高分类效果好的弱分类器的权重&lt;/strong&gt;；它的自适应体现在会根据前一轮模型的错误调整样本的权重。&lt;/p&gt;
&lt;p&gt;AdaBoost 算法的训练过程是逐步迭代的，关键步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化样本权重&lt;/strong&gt;：给每个训练样本分配一个相等的初始权重。对于 $\small{N}$ 个样本，初始权重为：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
w_{i}^{(1)} = \frac{1}{N}, \ i = 1, 2, \cdots, N
$$&lt;/p&gt;
&lt;p&gt;这里， $\small{w_{i}^{(1)}}$ 表示第 $\small{i}$ 个样本的权重，初始时权重相等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;训练弱学习器&lt;/strong&gt;：在每一轮迭代中，AdaBoost 会根据当前样本的权重训练一个弱分类器（例如决策树桩，即深度为 1 的决策树）。弱分类器的目标是最小化加权误差。对于第 $\small{t}$ 轮训练得到的分类器模型（弱学习器） $\small{h_t}$，计算其加权误差：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\varepsilon_{t} = \sum_{i=1}^{N} w_{i}^{(t)} \cdot I(y_{i} \neq h_{t}(x_{i}))
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{y_{i}}$ 是第 $\small{i}$ 个样本的真实标签， $\small{h_{t}(x_{i})}$ 是第 $\small{t}$ 轮模型 $\small{h_{t}}$ 对第 $\small{i}$ 个样本 $\small{x_{i}}$ 给出的预测结果（取值为 1 或 -1）， $\small{I(y_{i} \neq h_{t}(x_{i}))}$ 是指示函数，当样本 $\small{x_{i}}$ 被错误分类时函数取值为 1，否则函数取值为 0。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新分类器权重&lt;/strong&gt;：计算第 $\small{t}$ 轮分类器模型的权重 $\small{\alpha_{t}}$，并用它来更新每个样本的权重。分类器权重 $\small{\alpha_{t}}$ 的计算公式为：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
\alpha_{t} = \frac{1}{2} ln \left( \frac{1 - \varepsilon_{t}}{\varepsilon_{t}} \right)
$$&lt;/p&gt;
&lt;p&gt;当分类器的误差较低时， $\small{\alpha_{t}}$ 的值较大，说明该分类器的权重较大。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新样本权重&lt;/strong&gt;：根据当前分类器的表现，更新样本的权重。误分类样本的权重会增加，正确分类样本的权重会减少。样本权重的更新公式为：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
w_{i}^{(t + 1)} = w_{i}^{(t)} \cdot e^{-\alpha_{t} y_{i} h_{t}(x_{i})}
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;归一化权重&lt;/strong&gt;：对所有样本的权重进行归一化，使得所有样本的权重和为 1。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最终分类器&lt;/strong&gt;：AdaBoost 的最终分类器是所有弱学习器的加权组合，预测时通过加权投票来决定最终类别：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
H(x) = \text{sign} \left( \sum_{t=1}^{T} \alpha_{t} h_{t}(x) \right)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\text{sign}}$ 是符号函数，其定义如下所示：&lt;/p&gt;
&lt;p&gt;$$
\text{sign}(z) = \begin{cases} +1 \ (z \ge 0) \\ -1 \ (z \lt 0) \end{cases}
$$&lt;/p&gt;
&lt;p&gt;例如，有 3 个弱学习器 $\small{h_{1}(x)}$、 $\small{h_{2}(x)}$、 $\small{h_{3}(x)}$，它们的输出分别是&lt;code&gt;+1&lt;/code&gt;、&lt;code&gt;-1&lt;/code&gt;和&lt;code&gt;+1&lt;/code&gt;，对应的权重是 $\small{\alpha_{1} = 0.5}$、 $\small{\alpha_{2} = 0.3}$、 $\small{\alpha_{3} = 0.2}$ ，那么加权和为：&lt;/p&gt;
&lt;p&gt;$$
\sum_{t=1}^{3} \alpha_{t} h_{t}(x) = 0.5 \times 1 + 0.3 \times -1 + 0.2 \times 1 = 0.4
$$&lt;/p&gt;
&lt;p&gt;由于加权和&lt;code&gt;0.4&lt;/code&gt;为正，符号函数会输出&lt;code&gt;+1&lt;/code&gt;，表示最终预测类别为正类。&lt;/p&gt;
&lt;p&gt;我们还是以鸢尾花数据集为例，应用 AdaBoost 集成学习算法来构建分类模型，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import classification_report

# 数据集的加载和划分
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 初始化弱分类器（决策树桩）
base_estimator = DecisionTreeClassifier(max_depth=1)
# 初始化 AdaBoost 分类器
model = AdaBoostClassifier(base_estimator, n_estimators=50)
# 训练模型
model.fit(X_train, y_train)
# 预测结果
y_pred = model.predict(X_test)

# 输出评估报告
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       1.00      1.00      1.00        10
           1       0.90      0.90      0.90        10
           2       0.90      0.90      0.90        10

    accuracy                           0.93        30
   macro avg       0.93      0.93      0.93        30
weighted avg       0.93      0.93      0.93        30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意几个超参数的设置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;n_estimators&lt;/code&gt;：指定要训练的基学习器（弱分类器）的数量，默认值为 50。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;learning_rate&lt;/code&gt;：控制每个基学习器在最终模型中的贡献大小，即学习率，默认值为 1.0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;algorithm&lt;/code&gt;：决定 AdaBoost 的训练算法。AdaBoost 主要有两种训练模式：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;SAMME&apos;&lt;/code&gt;：用于多类分类问题的算法，采用加法模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;SAMME.R&apos;&lt;/code&gt;：基于 Real AdaBoost 的算法，用于加权的二分类和多分类问题，使用了一个加权的重新采样策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base_estimator&lt;/code&gt;：基学习器，默认值为&lt;code&gt;None&lt;/code&gt;，表示使用&lt;code&gt;max_depth=1&lt;/code&gt;的&lt;code&gt;DecisionTreeClassifier&lt;/code&gt;作为基学习器。所以，上面代码中创建&lt;code&gt;AdaBoostClassifier&lt;/code&gt;对象的两个参数都可以省略，因为它们刚好都是默认值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;增大 &lt;code&gt;n_estimators&lt;/code&gt; 可以提高模型的性能，但可能会导致过拟合；增大 &lt;code&gt;learning_rate&lt;/code&gt; 则可以加速训练，但可能会导致模型不稳定。通常需要通过交叉验证来找到最佳的 &lt;code&gt;n_estimators&lt;/code&gt; 和 &lt;code&gt;learning_rate&lt;/code&gt; 的组合。对于大多数问题，&lt;code&gt;DecisionTreeClassifier(max_depth=1)&lt;/code&gt; 是常见的选择，但如果数据较为复杂，可以考虑其他基学习器，如&lt;code&gt;SVC&lt;/code&gt;、&lt;code&gt;LogisticRegression&lt;/code&gt;等。&lt;/p&gt;
&lt;h4&gt;GBDT&lt;/h4&gt;
&lt;p&gt;GBDT（Gradient Boosting Decision Trees）也是一种强大的集成学习算法，相较于 AdaBoost，GBDT 系列的模型应用得更加广泛。GBDT 基于&lt;strong&gt;梯度提升&lt;/strong&gt;（Gradient Boosting）的思想，结合了决策树的优势，通过一系列的弱分类器（决策树）逐步改进模型，每次训练时通过减少前一个模型的误差（拟合残差）来提高模型的预测性能。&lt;/p&gt;
&lt;p&gt;GBDT 使用梯度下降的方式来最小化损失函数。对于回归任务，损失函数通常是均方误差（MSE）；对于分类任务，常用的损失函数是对数损失（Log Loss）。下面我们以二分类任务为例，为大家讲解算法的原理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;损失函数。在分类任务中，通常使用对数似然损失函数，对于二分类问题，损失函数如下所示：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
L(y, F(x)) = -y\log(p(x)) - (1 - y)\log(1 - p(x))
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{y}$ 是实际标签（ $\small{y \in \lbrace 0, 1 \rbrace}$ ，表示类别 0 或 1）， $\small{p(x)}$ 是模型预测的样本 $\small{x}$ 属于类别 1 的概率。由于 GBDT 是基于梯度提升算法的，因此在每一轮的更新中，我们将通过梯度下降法来优化这个损失函数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;梯度计算。为了使用梯度提升，我们需要计算损失函数关于当前模型预测的梯度。令当前模型的输出为 $\small{F(x)}$，即预测函数。根据对数损失函数， $\small{p(x)}$ 是通过模型输出 $\small{F(x)}$ 转换得到的概率，通常使用 Sigmoid 函数，有：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
p(x) = \frac{1}{1 + e^{-F(x)}}
$$&lt;/p&gt;
&lt;p&gt;计算损失函数对 $\small{F(x)}$ 的梯度时，我们得到：&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial{L(y, F(x))}}{\partial{F(x)}} = p(x) - y
$$&lt;/p&gt;
&lt;p&gt;即梯度为 $\small{p(x) - y}$ ，这个值告诉我们当前模型的预测 $\small{F(x)}$ 与真实标签 $\small{y}$ 之间的差距。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;模型更新。每一轮的更新都包括两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算残差：在每一轮迭代中，我们计算当前模型的残差，即 $\small{\delta_i = p(x_i) - y_i}$，表示每个样本的误差。&lt;/li&gt;
&lt;li&gt;拟合残差：使用新的基学习器（通常是决策树）来拟合这些残差。在分类任务中，我们训练的决策树并不是拟合真实标签，而是拟合残差，即拟合当前模型的预测误差。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更新规则为：&lt;/p&gt;
&lt;p&gt;$$
F_{m + 1}(x) = F_{m}(x) + \eta h_{m}(x)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{F_{m}(x)}$ 是第 $\small{m}$ 轮模型的输出； $\small{h_{m}(x)}$ 是第 $\small{m}$ 轮训练出的弱学习器（通常是决策树），它预测当前模型的残差； $\small{\eta}$ 是学习率，控制每棵树的贡献大小。通过这样逐步拟合残差，最终生成的模型 $\small{F(x)}$ 就是一个由多棵决策树组成的强学习器。&lt;/p&gt;
&lt;p&gt;对于多分类任务，我们需要将损失函数和梯度计算做相应的扩展。常用的多分类损失函数是多项式对数损失，对于每个类别 $\small{k}$ ，损失函数可以表示为：&lt;/p&gt;
&lt;p&gt;$$
L(y, F(x)) = -\sum_{k=1}^{K} y_{k} \log(p_{k}(x))
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{K}$ 是类别总数， $\small{y_{k}}$ 是目标类别 $\small{k}$ 的指示函数， $\small{p_k(x)}$ 是样本 $\small{x}$ 属于类别 $\small{k}$ 的预测概率。&lt;/p&gt;
&lt;p&gt;我们还是以鸢尾花数据集为例，应用 AdaBoost 集成学习算法来构建分类模型，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import classification_report

# 数据集的加载和划分
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 初始化 GBDT 分类器
model = GradientBoostingClassifier(n_estimators=32)
# 训练模型
model.fit(X_train, y_train)
# 预测结果
y_pred = model.predict(X_test)

# 输出评估报告
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       1.00      1.00      1.00        10
           1       1.00      1.00      1.00        10
           2       1.00      1.00      1.00        10

    accuracy                           1.00        30
   macro avg       1.00      1.00      1.00        30
weighted avg       1.00      1.00      1.00        30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还是讲一讲&lt;code&gt;GradientBoostingClassifier&lt;/code&gt;几个重要的超参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;loss&lt;/code&gt;：指定用于分类任务的损失函数，默认值为 &lt;code&gt;deviance&lt;/code&gt;，表示使用对数损失函数。也可以选择 &lt;code&gt;exponential&lt;/code&gt;，表示使用指数损失函数（AdaBoost 的损失函数）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;learning_rate&lt;/code&gt;：控制每棵树对最终预测结果的影响程度，默认值为 0.1。较小的学习率通常会带来更好的泛化能力，但需要更多的弱分类器（即更多的树）来拟合训练数据；如果学习率设置得太大，则可能会导致模型欠拟合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n_estimators&lt;/code&gt;：指定要训练的基学习器（通常是决策树）的数量，默认值为 100。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subsample&lt;/code&gt;：控制每棵树训练时使用的数据比例，默认值为 1.0（即使用所有数据）。通过设置小于 1 的值，可以在训练每棵树时随机选择部分样本，从而增加模型的随机性减少过拟合。通常情况下，0.8 或 0.9 是不错的选择。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;criterion&lt;/code&gt;：用于控制分裂时的分裂标准，默认值为&lt;code&gt;&apos;friedman_mse&apos;&lt;/code&gt;。该参数决定了如何选择每个节点的最佳划分方式，可选的值有：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;friedman_mse&apos;&lt;/code&gt;：这个准则是基于均方误差（MSE）的一个改进版本，旨在减小对不平衡数据的敏感性，并且在一些实际任务中，能够比传统的 MSE 产生更好的结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;mse&apos;&lt;/code&gt;：使用传统的均方误差（MSE）来评估每个分裂点的质量，MSE 越小，说明当前节点的分裂越好。在分类任务中，使用 &lt;code&gt;&apos;friedman_mse&apos;&lt;/code&gt; 往往能取得更好的效果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;mae&apos;&lt;/code&gt;：使用平均绝对误差（MAE）来评估分裂的质量，MAE 对异常值不那么敏感，但在实际应用中通常较少使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validation_fraction&lt;/code&gt;：用于指定训练过程中的验证集比例，用于执行早期停止（early stopping），避免过拟合。该参数可与 &lt;code&gt;n_iter_no_change&lt;/code&gt; 配合使用，在验证集上评估性能，当连续若干轮没有改进时，提前停止训练。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了上面提到的超参数外，还有一些跟决策树类似的超参数，此处就不再进行赘述了。需要注意的是，增加 &lt;code&gt;n_estimators&lt;/code&gt;（树的数量）时，通常需要减小 &lt;code&gt;learning_rate&lt;/code&gt;以防过拟合；调整决策树的&lt;code&gt;max_depth&lt;/code&gt;和&lt;code&gt;min_samples_split&lt;/code&gt;也可以减小树的复杂度来防止过拟合；用好&lt;code&gt;subsample&lt;/code&gt;和&lt;code&gt;early_stopping&lt;/code&gt;参数也可以达成类似的效果。建议大家通过网格搜索交叉验证的方式，结合模型的训练误差和验证误差来寻找最佳参数组合。&lt;/p&gt;
&lt;h4&gt;XGBoost&lt;/h4&gt;
&lt;p&gt;从算法精度、速度和泛化能力等性能指标来看GBDT，仍然有较大的优化空间。XGBoost（eXtreme Gradient Boosting）正是一种基于 GBDT 的顶级梯度提升模型，由陈天奇在其论文&lt;a href=&quot;https://arxiv.org/pdf/1603.02754&quot;&gt;《&lt;em&gt;XGBoost: A Scalable Tree Boosting System&lt;/em&gt;》&lt;/a&gt;中提出。相较于 GBDT，从算法精度上看，XGBoost 通过将损失函数展开到二阶导数，使得梯度提升树模型更能逼近其真实损失；从算法速度上看，XGBoost 使用了加权分位数 sketch 和稀疏感知算法这两个技巧，通过缓存优化和模型并行来提高算法速度；从算法泛化能力上来看，通过对损失函数加入正则化项、加性模型中设置缩减率和列抽样等方法，来防止模型过拟合。因为上述原因，XGBoost 不论在学术界、工业界和竞赛圈都很受欢迎，有着广泛的应用。关于 XGBoost 的细节，有兴趣的读者可以阅读陈天奇的论文，此处不进行展开介绍。&lt;/p&gt;
&lt;p&gt;我们可以下面的方式来使用 XGBoost，首先安装依赖项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install xgboost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的代码为大家展示了如何使用 XGBoost。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt
import xgboost as xgb
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# 数据集的加载和划分
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 将数据处理成数据集格式DMatrix格式
dm_train = xgb.DMatrix(X_train, y_train)
dm_test = xgb.DMatrix(X_test)

# 设置模型参数
params = {
    &apos;booster&apos;: &apos;gbtree&apos;,           # 用于训练的基学习器类型
    &apos;objective&apos;: &apos;multi:softmax&apos;,  # 指定模型的损失函数
    &apos;num_class&apos;: 3,                # 类别的数量
    &apos;gamma&apos;: 0.1,                  # 控制每次分裂的最小损失函数减少量
    &apos;max_depth&apos;: 6,                # 决策树最大深度
    &apos;lambda&apos;: 2,                   # L2正则化权重
    &apos;subsample&apos;: 0.8,              # 控制每棵树训练时随机选取的样本比例
    &apos;colsample_bytree&apos;: 0.8,       # 用于控制每棵树或每个节点的特征选择比例
    &apos;eta&apos;: 0.001,                  # 学习率
    &apos;seed&apos;: 10,                    # 设置随机数生成器的种子
    &apos;nthread&apos;: 16,                 # 指定了训练时并行使用的线程数
}

# 训练模型
model = xgb.train(params, dm_train, num_boost_round=200)
# 预测结果
y_pred = model.predict(dm_test)

# 输出模型评估报告
print(classification_report(y_test, y_pred))

# 绘制特征重要性评分
xgb.plot_importance(model)
plt.grid(False)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的特征重要性如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/0d6385b1fe.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们再说说这个 XGBoost 的模型参数， 先看看上面代码中的&lt;code&gt;param&lt;/code&gt;字典，有几个参数需要详细说明一下。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;booster&lt;/code&gt;：指定用于训练的基学习器类型，默认值为 &lt;code&gt;&apos;gbtree&apos;&lt;/code&gt;，表示使用传统的决策树作为基学习器；其他的选项包括 &lt;code&gt;&apos;gblinear&apos;&lt;/code&gt;和&lt;code&gt;&apos;dart&apos;&lt;/code&gt;，前者表示使用线性回归或逻辑回归，后者也是基于决策树的模型，但具有丢弃树的机制，降低过拟合风险。通常&lt;code&gt;&apos;gbtree&apos;&lt;/code&gt;适用于大多数问题，尤其涉及到非线性关系；&lt;code&gt;&apos;dart&apos;&lt;/code&gt;适用于复杂数据集，尤其是在出现过拟合时。如果数据集较小或线性关系较强，可以尝试使用 &lt;code&gt;&apos;gblinear&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objective&lt;/code&gt;：指定模型的损失函数（优化目标），常见的选项包括：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;reg:squarederror&apos;&lt;/code&gt;：回归任务中的均方误差（MSE），用于回归任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;reg:logistic&apos;&lt;/code&gt;：回归任务中的逻辑回归，通常用于二分类任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;binary:logistic&apos;&lt;/code&gt;：二分类任务中的逻辑回归，输出概率值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;binary:logitraw&apos;&lt;/code&gt;：二分类任务中的逻辑回归，输出未经过 Sigmoid 处理的原始值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;multi:softmax&apos;&lt;/code&gt;：多分类任务，输出为每个类别的最大概率。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;multi:softprob&apos;&lt;/code&gt;：多分类任务，输出为每个类别的概率分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eta&lt;/code&gt;/&lt;code&gt;learning_rate&lt;/code&gt;：XGBoost 中的学习率，默认值为 0.3，推荐将初始值设置为 0.01 到 0.1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alpha&lt;/code&gt;和&lt;code&gt;lambda&lt;/code&gt;：前者控制 L1 正则化项（Lasso）的强度，默认为 &lt;code&gt;0&lt;/code&gt;；后者控制 L2 正则化项（Ridge）的强度，默认为 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scale_pos_weight&lt;/code&gt;：用于处理类别不平衡问题，尤其是二分类问题。在类别严重不平衡的情况下，通过调整这个参数来加大少数类的权重，使得模型更关注少数类样本，默认值为 1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gamma&lt;/code&gt;：用来控制每次分裂的最小损失函数减少量，该参数控制树的生长，越大的 &lt;code&gt;gamma&lt;/code&gt; 会使得树更小，减小过拟合的风险，默认值为 0，意味着模型不会受到分裂的限制，树会尽可能深，直到节点中没有足够的样本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;num_class&lt;/code&gt;：用于多分类任务的参数，表示类别的数量。对于二分类任务，无需设置该参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;colsample_bytree&lt;/code&gt; / &lt;code&gt;colsample_bylevel&lt;/code&gt; / &lt;code&gt;colsample_bynode&lt;/code&gt;：控制在每棵树、每一层、每个节点上采样特征的比例。这些参数用于控制模型的复杂度。较小的值会增加模型的随机性，从而防止过拟合；较大的值则意味着每棵树使用更多的特征，可能导致过拟合。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;colsample_bytree&lt;/code&gt;：每棵树使用的特征比例（默认为 1.0）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;colsample_bylevel&lt;/code&gt;：每一层使用的特征比例（默认为 1.0）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;colsample_bynode&lt;/code&gt;：每个节点使用的特征比例（默认为 1.0）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了&lt;code&gt;param&lt;/code&gt;字典提供的模型参数外，&lt;code&gt;train&lt;/code&gt;函数还有几个参数也值得注意，它们也是模型的超参数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;num_boost_round&lt;/code&gt;：树的训练轮数，设置较小的&lt;code&gt;learning_rate&lt;/code&gt;并增加训练轮数可以提高模型的稳定性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;early_stopping_rounds&lt;/code&gt;：用于实现早期停止机制。当指定轮次的训练中，验证集上的损失函数不再减少时，训练会自动停止，避免过拟合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feval&lt;/code&gt;：设置用户自定义的评估函数，这种方式允许用户灵活地使用任何适合特定任务的评估指标，需要注意的是评估函数有两个参数，一个表示模型预测值（NumPy 的&lt;code&gt;ndarray&lt;/code&gt;对象），一个是训练数据（XGBoost 的&lt;code&gt;DMatrix&lt;/code&gt;对象）；函数返回一个二元组&lt;code&gt;(name, value)&lt;/code&gt;，其中 &lt;code&gt;name&lt;/code&gt; 是评估指标的名称，&lt;code&gt;value&lt;/code&gt; 是指标的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;obj&lt;/code&gt;：设置用户自定义的目标函数，目标函数用于计算每一步的梯度和二阶导数，从而指导模型的优化过程，有兴趣的读者可以自行研究。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evals&lt;/code&gt;：用于指定一个或多个验证集，其值是包含一个或多个&lt;code&gt;(data, label)&lt;/code&gt; 元组的列表，每个元组代表一个评估数据集，数据集需要是&lt;code&gt;DMatrix&lt;/code&gt;对象。在训练过程中，XGBoost 会在每一轮迭代后评估验证集上的性能，通常用于监控训练过程中的过拟合或调整超参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eval_results&lt;/code&gt;：存储在训练过程中计算的所有评估结果，通常传入一个字典。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verbose_eval&lt;/code&gt;：控制训练过程中评估结果的输出频率，可以设置为一个整数，表示多少轮迭代输出一次评估结果，也可以设置为&lt;code&gt;True&lt;/code&gt;或&lt;code&gt;False&lt;/code&gt;，表示每轮都输出或不输出任何评估结果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xgb_model&lt;/code&gt;：用于加载之前训练好的模型，以便从中断点继续训练。你可以指定一个 &lt;code&gt;xgb_model&lt;/code&gt; 文件或者传入一个 &lt;code&gt;Booster&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callbacks&lt;/code&gt;：在训练过程中添加自定义的回调函数，回调函数可以在每一轮迭代时提供额外的控制，如自动停止训练、调整学习。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;LightGBM&lt;/h4&gt;
&lt;p&gt;LightGBM（Light Gradient Boosting Machine）是微软于2017年开源的一款顶级 Boosting 算法框架，虽然本质仍然是 GBDT 算法，但被设计用于大规模数据集的处理，特别是在需要高效率和低内存消耗的场景下。就 GBDT 系列算法性能而言，XGBoost 已经非常高效了，但并非没有缺陷。LightGBM 就是一种针对 XGBoost 缺陷的改进版本，通过直方图算法（通过将连续特征分箱）、单边梯度抽样（GOSS，通过采样方法在训练过程中保留梯度较大的样本，减少计算量）、互斥特征捆绑（EFB，将一些互斥特征做组合，减少特征空间的维度）和 leaf-wise 生长策略（优先对当前叶子节点进行分裂来扩展树的深度）四个方法，使得 GBDT 算法系统更轻便、更高效，能够做到又快又准。当然，在较小的数据集上，LightGBM 的优势并不明显，而且不管是 XGBoost 还是 LightGBM，模型的可解释性都是一个问题，超参数调优的难度也是比较大的。&lt;/p&gt;
&lt;p&gt;这里，我们不再使用更多的篇幅去讲解 LightGBM 的细节，感兴趣的小伙伴可以直接访问 LightGBM 的&lt;a href=&quot;https://lightgbm.readthedocs.io/en/stable/index.html&quot;&gt;官方文档&lt;/a&gt;。我们直接通过代码带大家简单感受一下如何使用 LightGBM，首先还是需要通过下面的命令完成安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install lightgbm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们仍然使用鸢尾花数据集来训练模型，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import lightgbm as lgb
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# 加载和划分数据集
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 将数据转化为 LightGBM 的数据格式
train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)

# 设置模型参数
params = {
    &apos;objective&apos;: &apos;multiclass&apos;,   # 多分类问题
    &apos;num_class&apos;: 3,              # 类别数量
    &apos;metric&apos;: &apos;multi_logloss&apos;,   # 多分类对数损失函数
    &apos;boosting_type&apos;: &apos;gbdt&apos;,     # 使用梯度提升树算法
    &apos;num_leaves&apos;: 31,            # 叶子节点数
    &apos;learning_rate&apos;: 0.05,       # 学习率
    &apos;feature_fraction&apos;: 0.75,    # 每次训练时随机选择特征的比例
    &apos;early_stopping_rounds&apos;: 10  # 连续多少论没有性能提升就停止迭代
}
# 模型训练
model = lgb.train(params=params, train_set=train_data, num_boost_round=200, valid_sets=[test_data])
# 模型预测
y_pred = model.predict(X_test, num_iteration=model.best_iteration)
# 将预测结果处理成标签
y_pred_max = np.argmax(y_pred, axis=1)

# 查看模型评估报告
print(classification_report(y_test, y_pred_max))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还是简单为大家讲讲模型的超参数，更多的细节大家可以参考官方文档。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;objective&lt;/code&gt;：设置优化目标函数（损失函数），可选值有：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;regression&apos;&lt;/code&gt;：回归任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;binary&apos;&lt;/code&gt;：二分类任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;multiclass&apos;&lt;/code&gt;：多分类任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;multiclassova&apos;&lt;/code&gt;：多分类任务，使用一对多的策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;rank_xendcg&apos;&lt;/code&gt;、&lt;code&gt;&apos;lambdarank&apos;&lt;/code&gt;：排名任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;metric&lt;/code&gt;：评估模型性能的指标，可选值有：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;l2&apos;&lt;/code&gt;、&lt;code&gt;&apos;mean_squared_error&apos;&lt;/code&gt;：回归任务中的均方误差。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;binary_error&apos;&lt;/code&gt;：二分类错误率。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;multi_logloss&apos;&lt;/code&gt;：多分类对数损失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;auc&apos;&lt;/code&gt;：二分类任务中的 AUC。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;precision&apos;&lt;/code&gt;、&lt;code&gt;&apos;recall&apos;&lt;/code&gt;、&lt;code&gt;&apos;f1&apos;&lt;/code&gt;：精度、召回率、F1分数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boosting_type&lt;/code&gt;：设置提升类型，可选值有：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;gbdt&apos;&lt;/code&gt;：传统的梯度提升树。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;dart&apos;&lt;/code&gt;：具有随机丢弃树机制来防止过拟合的决策树。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;goss&apos;&lt;/code&gt;：通过单边梯度抽样来加速训练。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;rf&apos;&lt;/code&gt;：随机森林。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;num_leaves&lt;/code&gt;/&lt;code&gt;max_depth&lt;/code&gt;：决策树的叶子节点数 / 决策树的最大深度，控制树的复杂度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lambda_l1&lt;/code&gt;/&lt;code&gt;lambda_l2&lt;/code&gt;：L1 和 L2 正则化参数，用于控制模型的复杂度，防止过拟合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_bin&lt;/code&gt;： 用于分割连续特征（数据分箱）的最大箱子数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feature_fraction&lt;/code&gt;：每次训练时随机选择特征的比例。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;early_stopping_rounds&lt;/code&gt;： 设置评估指标在连续多少轮迭代中没有改进时，训练会提前停止。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;集成学习通过结合多个模型来减少模型的偏差和方差，通常能获得比单一模型更好的预测效果。由于集成学习通常结合多个基础模型，它能够有效降低单一模型可能存在的过拟合问题，也能够处理异常数据和噪声数据，比单一模型更加稳定。当然，集成学习也存在计算开销大、模型可解释性差、超参数调优复杂等问题。除了 XGBoost 和 LightGBM 之外，还有一个因处理类别特征而闻名的 Boosting 算法叫做 CatBoost，三者都是 GBDT 系列算法的佼佼者。虽然目前“大力出奇迹”的深度学习大行其道，但是以 XGBoost、LightGBM 和 CatBoost 为代表的 Boosting 算法仍然有广阔的应用场景，即便是在非结构化数据（文本、语音、图像、视频）的建模上也是有用武之地的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;88 神经网络模型&lt;/h2&gt;
&lt;p&gt;在人类的大脑中，神经元（neuron）是负责信息传递和处理的单元，神经元通过化学信号和电信号进行交流，这是人类记忆、感觉、运动等功能的基础。神经元包含了轴突（axon）和树突（dendrite），树突负责接收信号，轴突负责发送信号，此外细胞体也是神经元的重要部分，起到整合和传递信息的作用。神经元之间的连接通过突触（synapse）传递化学信号或电信号来实现，一个神经元可能会与成千上万个神经元连接，构成错综复杂的神经网络。人在刚出生时，大脑中有约 860 亿神经元，大部分神经元是不会再生的，所以这个数字会随着年龄的增长而略为减少。新生儿的大脑拥有数量极其庞大的突触连接，为未来的学习和适应奠定基础。&lt;/p&gt;
&lt;p&gt;很多科普文章都宣传神经网络模型是模拟人脑神经元的计算模型，通过多层神经元连接来完成复杂的非线性映射，但是没有证据表明大脑的学习机制与神经网络模型机制相同。虽然神经网络这个术语来自于神经生物学，深度学习中的一些概念也是从人类对大脑的理解中汲取灵感而形成的，但是对新手来说，如果认为神经网络与神经生物学存在某种联系，只会让人变得更加困惑。&lt;/p&gt;
&lt;h4&gt;基本构成&lt;/h4&gt;
&lt;p&gt;神经网络一般由多个层组成，通常包括输入层、隐藏层和输出层。每层中的神经元与前一层的神经元相连接，通过权重调整和激活函数的作用，逐层传递和处理数据，最终完成复杂数据的特征提取和学习。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;输入层&lt;/strong&gt;：输入层神经元接收输入数据，每个神经元通常对应一个输入特征。输入层的任务是将输入数据传递给下一层的神经元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏层&lt;/strong&gt;：隐藏层是网络的核心部分，负责对数据进行特征提取和处理。隐藏层可以有一层或多层，每层的神经元通过权重和激活函数来进行数据处理。深度神经网络通常会包含多层隐藏层，这样能够提取数据中的高阶特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出层&lt;/strong&gt;：输出层负责将隐藏层的结果转换为最终输出。输出层的激活函数通常与问题类型有关，例如二分类任务可能用 Sigmoid 函数以获得概率，而回归任务则可能直接输出数值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;神经元&lt;/strong&gt;：每个神经元都接收来自前一层神经元的输入，进行加权求和，再通过激活函数计算输出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/81c5e560d7.webp&quot; style=&quot;zoom:80%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;神经网络的训练过程通常通过反向传播算法（Backpropagation）和梯度下降法来优化权重，以减少预测误差。深度神经网络通过多层非线性转换能够学习到更加复杂的特征表示，因此在许多任务中表现优异。&lt;/p&gt;
&lt;h4&gt;工作原理&lt;/h4&gt;
&lt;p&gt;我们先设计一个最简单的神经网络模型，它只有一层输入和一层输出，该模型会根据输入（ $\small{X}$ ）去预测输出（ $\small{y}$ ），而输出又满足下面的公式：&lt;/p&gt;
&lt;p&gt;$$
y = aX_{1} + bX_{2} + cX_{3} + d
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{a, b, c, d}$ 都是模型的参数（权重和偏置），那么我们的神经网络模型应该是如下图所示的结构。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/2985471b44.webp&quot; style=&quot;zoom:55%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;大家可能已经发现，这个最简单的神经网络模型跟线性回归根本没有区别，但是我们在输入层和输出层中间再加上一个隐藏层又会怎样呢？看看下面的图，是不是已经有一点“网络”的味道了，加入的隐藏层可以处理非线性关系，而且隐藏层可以有多个，层与层之间连接的方式可以是全连接或部分连接，甚至可以出现环形结构。上面我们也说过，引入非线性能够让模型学习到更加复杂的特征表示，这也是神经网络模型在很多任务中表现优异的原因。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/4c07e764ec.webp&quot; style=&quot;zoom:58%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们看看神经网络的工作原理。每个神经元的计算过程可以表示为：&lt;/p&gt;
&lt;p&gt;$$
y = f \left( \sum_{i=1}^{n}w_{i}x_{i} + b \right)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{x_{1}, x_{2}, \dots, x_{n}}$ 是输入特征， $\small{w_1, w_2, \dots, w_n}$ 是与输入对应的权重； $\small{b}$ 是偏置项； $\small{f}$ 是激活函数，通常是非线性函数，如 Sigmoid、ReLU、Tanh 等。激活函数一个方面是引入非线性变换，增加了神经网络的表达能力，使得它可以模拟任意复杂的函数关系；另一方面，在训练神经网络时会使用反向传播来更新权重，激活函数的非线性属性使得梯度传递更有效，让神经网络收敛得更快，提高训练效率。下面我们对常用的激活函数做一个简要的介绍。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sigmoid 函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
f(x) = \frac{1}{1 + e^{-x}}
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/443d4f1bd7.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：Sigmoid 函数将输入值映射到 $\small{(0, 1)}$ 的范围内，呈现出平滑的 S 型曲线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：特别适用于概率预测，因为输出在 $\small{(0, 1)}$ 之间，可以理解为概率值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：对于较大的正值或负值，梯度会变得很小，导致梯度消失问题，从而影响深层网络的训练。除此以外，由于输出非零中心，这会导致梯度更新不对称，可能使得收敛变慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Tanh 函数（双曲正切函数）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
f(x) = tanh(x) = \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9cdd931477.webp&quot; style=&quot;zoom:62%;&quot; /&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：Tanh 函数将输入映射到 $\small{(-1, 1)}$ 的范围内，也是 S 型曲线，但中心对称。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：与 Sigmoid 类似，但输出在 $\small{(-1, 1)}$ 之间，这样的零中心输出使得梯度更新更对称，更适合用于深层网络。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：在极值附近，梯度仍会趋向于零，导致梯度消失问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;ReLU 函数（Rectified Linear Unit）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
f(x) = max(0, x)
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：ReLU 将输入小于零的部分设为零，而大于零的部分保持不变，因此其输出范围是 $\small{[0, +\infty]}$ 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：计算简单，有效避免了梯度消失问题，因此被广泛应用于深层网络。能够保持稀疏性，许多神经元的输出为零，有利于网络简化计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：当输入为负数时，ReLU 的梯度为零。若输入长期为负数，神经元可能“死亡”并停止更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Leaky ReLU 函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
f(x) = \begin{cases} x &amp;amp; (x \gt 0) \\ {\alpha}x &amp;amp; (x \le 0)\end{cases}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：Leaky ReLU 是对 ReLU 的改进，它为输入小于零的部分引入了一个小的负斜率（通常取值 $\small{\alpha = 0.01}$ ），使得梯度不为零。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：通过允许负值的输出，避免了死神经元问题，使得网络更健壮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：虽然 Leaky ReLU 能缓解死神经元问题，但其负值斜率的选择对网络性能会有一些影响，且对模型的非线性表示能力没有显著提升。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一个包含多个层的神经网络中，信息会一层一层的进行传递。假设第 $\small{l}$ 层的输出是 $\small{\mathbf{a}^{[l]}}$  ，按照上面神经元计算公式，有：&lt;/p&gt;
&lt;p&gt;$$
\mathbf{a}^{[l]} = f \left( \mathbf{W}^{[l]} \mathbf{a}^{[l-1]} + \mathbf{b}^{[l]} \right)
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\mathbf{W}^{[l]}}$ 是第 $\small{l}$ 层的权重矩阵， $\small{\mathbf{a}^{[l-1]}}$ 是是第 $\small{l - 1}$ 层的输出， $\small{\mathbf{b}^{[l]}}$ 是第 $\small{l}$ 层的偏置项， $\small{f}$ 是激活函数。神经网络最终的输出是通过最后一层的激活函数得到的，这个过程叫做前向传播（forward-propagation）。&lt;/p&gt;
&lt;p&gt;对于神经网络模型来说，还有一个极其重要的操作就是通过计算损失函数相对于每个权重和偏置的梯度来更新神经网络的参数（权重和偏置），这一过程通常称为反向传播（back-propagation）。反向传播有两个要点，一个是损失函数，一个是梯度下降法，前者用于衡量预测值与真实值之间的差距，常用的损失函数有均方误差（回归任务）和交叉熵损失函数（分类任务），后者通过更新参数 $\small{\theta}$（权重和偏置)，使得损失函数最小化，即：&lt;/p&gt;
&lt;p&gt;$$
\theta^{\prime} = \theta - \eta \nabla L(\theta) \\
\theta = \theta^{\prime}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\eta}$ 是学习率， $\small{\nabla L(\theta)}$ 是损失函数相对于参数的梯度，跟我们讲解回归模型时使用的方法是一致的。&lt;/p&gt;
&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;p&gt;根据上面讲到的神经网络模型的原理，要自己写一个简单的神经网络模型也并不困难。当然，我们也可以用 scikit-learn 库&lt;code&gt;neural_network&lt;/code&gt; 模块来构建神经网络模型。下面，我们仍然以鸢尾花数据集为例，为大家展示神经网络模型的构建和预测效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report

# 加载和划分数据集
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 创建多层感知机分类器模型
model = MLPClassifier(
    solver=&apos;lbfgs&apos;,            # 优化模型参数的求解器
    learning_rate=&apos;adaptive&apos;,  # 学习率的调节方式为自适应 
    activation=&apos;relu&apos;,         # 隐藏层中神经元的激活函数 
    hidden_layer_sizes=(1, )   # 每一层神经元的数量
)
# 训练和预测
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 查看模型评估报告
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       1.00      0.10      0.18        10
           1       0.00      0.00      0.00        10
           2       0.34      1.00      0.51        10

    accuracy                           0.37        30
   macro avg       0.45      0.37      0.23        30
weighted avg       0.45      0.37      0.23        30
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：由于创建&lt;code&gt;MLPClassifier&lt;/code&gt;时没有指定&lt;code&gt;random_state&lt;/code&gt;参数，所以代码每次执行的结果可能并不相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型的预测准确率只有&lt;code&gt;0.37&lt;/code&gt;，大家对这个结果是不是感觉到非常失望，我们煞费苦心构建的模型预测效果竟然如此拉胯。别紧张，上面代码中我们创建神经网络模型时，&lt;code&gt;hidden_layer_sizes&lt;/code&gt;参数设置的是&lt;code&gt;(1, )&lt;/code&gt;，它表示我们的网络只有 1 个隐藏层，而且隐藏层只有 1 个神经元，这个神经元承受了太多（它真的，我哭死）。接下俩，我们需要增加隐藏层和神经元的数量，让模型可以更好的学习特征和目标之间的映射关系，这样预测的效果就会好起来。下面，我们将&lt;code&gt;hidden_layer_sizes&lt;/code&gt;参数调整为&lt;code&gt;(32, 32, 32)&lt;/code&gt;，即模型有三个隐藏层，每层有 32 个神经元，再来看看代码执行的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report

# 加载和划分数据集
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)

# 创建多层感知机分类器模型
model = MLPClassifier(
    solver=&apos;lbfgs&apos;,                  # 优化模型参数的求解器
    learning_rate=&apos;adaptive&apos;,        # 学习率的调节方式为自适应 
    activation=&apos;relu&apos;,               # 隐藏层中神经元的激活函数 
    hidden_layer_sizes=(32, 32, 32)  # 每一层神经元的数量
)
# 训练和预测
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 查看模型评估报告
print(classification_report(y_test, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大家可以试着运行上面的代码，看看有没有获得更好的结果。当然，模型准确率为 1 也未必就值得高兴，因为你可能训练了一个过拟合的模型。无论如何，大家可以试着重新设置&lt;code&gt;hidden_layer_sizes&lt;/code&gt;参数，看看会得到怎样的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面，我们还是对&lt;code&gt;MLPClassifier&lt;/code&gt;几个比较重要的超参数做一个说明。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;hidden_layer_sizes&lt;/code&gt;：指定神经网络中每一层的神经元数目，元组类型，默认值为&lt;code&gt;(100, )&lt;/code&gt;，表示只有一个隐藏层，包含 100 个神经元。该超参数可以改变网络的结构和容量，层数越多，神经元数目越多，模型的表示能力就越强，但也存在过拟合风险。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;activation&lt;/code&gt;：隐藏层中神经元的激活函数，默认值为&lt;code&gt;relu&lt;/code&gt;，表示使用 ReLU 激活函数。激活函数决定了网络每一层的输出形态，一般来说，&lt;code&gt;&apos;relu&apos;&lt;/code&gt; 是训练深度网络时的首选，因为它能够缓解梯度消失问题，并且训练速度较快，可选值包括：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;identity&apos;&lt;/code&gt;：线性激活函数，即 $\small{f(x) = x}$ ，通常不推荐使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;logistic&apos;&lt;/code&gt; / &lt;code&gt;&apos;tanh&apos;&lt;/code&gt; / &lt;code&gt;&apos;relu&apos;&lt;/code&gt;：Sigmoid / 双曲正切 / ReLU 激活函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;solver&lt;/code&gt;：用来优化模型参数的求解器（优化算法）。常用的优化算法有：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;lbfgs&apos;&lt;/code&gt;：拟牛顿法（Limited-memory Broyden-Fletcher-Goldfarb-Shanno），这种方法计算复杂度较高，但对小数据集表现较好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;sgd&apos;&lt;/code&gt;：随机梯度下降（Stochastic Gradient Descent），适用于大规模数据集，在训练时会随机选取小批量数据进行更新。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;adam&apos;&lt;/code&gt;：自适应矩估计（Adaptive Moment Estimation），默认值，适用于大多数情况且计算效率较高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alpha&lt;/code&gt;：L2 正则化项（也称为权重衰减），控制神经网络的复杂度，默认值为&lt;code&gt;0.0001&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;batch_size&lt;/code&gt;：每次迭代所用的样本批量大小，默认值为&lt;code&gt;&apos;auto&apos;&lt;/code&gt;，根据样本数量自动决定批量大小。较小的批量会使训练更加不稳定，但有助于避免陷入局部最优解；较大的批量则可以加速训练，但可能导致内存不足。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;learning_rate&lt;/code&gt;：学习率的调节方式，影响模型在每次迭代时调整权重的步长，默认值为&lt;code&gt;&apos;constant&apos;&lt;/code&gt;，表示学习率保持不变。其他可选的值有&lt;code&gt;&apos;invscaling&apos;&lt;/code&gt;和&lt;code&gt;&apos;adaptive&apos;&lt;/code&gt;，前者表示学习率随着迭代次数的增加而逐步减小，后者表示学习率会根据当前梯度变化自动调整，当梯度更新较小时学习率会增加，当梯度较大时学习率会减小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;learning_rate_init&lt;/code&gt;：初始学习率，默认值为&lt;code&gt;0.001&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_iter&lt;/code&gt;：最大迭代次数，控制训练过程中优化算法的最大迭代次数，默认值为&lt;code&gt;200&lt;/code&gt;。若达到最大迭代次数后模型仍未收敛，训练将停止。这对于防止过长时间训练是有用的，但过小的值可能会导致训练中止太早，导致欠拟合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tol&lt;/code&gt;：训练过程中的优化容忍度，决定了当目标函数变化小于该值时训练停止，默认值为&lt;code&gt;0.0001&lt;/code&gt;。较小的 &lt;code&gt;tol&lt;/code&gt; 会导致更长的训练时间，较大的 &lt;code&gt;tol&lt;/code&gt; 可能会提前停止训练。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了 scikit-learn 库，我们还可以使用 TensorFlow、PyTorch、MXNet 等三方库来实现神经网络模型，这些库很多都支持 GPU（图形处理单元） 或 TPU（张量处理单元） 加速，而且内置很多深度网络模型（如卷积神经网络、循环神经网络、生成对抗网络等）以及相应的优化器，相较于 scikit-learn，这些库可能是更好的选择。对此感兴趣的小伙伴，可以看看下面的代码，我们用 PyTorch 构建了神经网络模型来解决鸢尾花分类问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn
import torch.optim as optim
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report

# 加载鸢尾花数据集
iris = datasets.load_iris()
X, y = iris.data, iris.target

# 数据预处理（标准化）
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, train_size=0.8, random_state=3)
# 将数组转换为PyTorch张量
X_train_tensor, X_test_tensor, y_train_tensor, y_test_tensor = (
    torch.tensor(X_train, dtype=torch.float32),
    torch.tensor(X_test, dtype=torch.float32),
    torch.tensor(y_train, dtype=torch.long),
    torch.tensor(y_test, dtype=torch.long)
)


class IrisNN(nn.Module):
    &quot;&quot;&quot;鸢尾花神经网络模型&quot;&quot;&quot;

    def __init__(self):
        &quot;&quot;&quot;初始化方法&quot;&quot;&quot;
        # 调用父类构造器
        super(IrisNN, self).__init__()
        # 输入层到隐藏层（4个特征到32个神经元全连接）
        self.fc1 = nn.Linear(4, 32)
        # 隐藏层到输出层（32个神经元到3个输出全连接）
        self.fc2 = nn.Linear(32, 3)

    def forward(self, x):
        &quot;&quot;&quot;前向传播&quot;&quot;&quot;
        # 隐藏层使用ReLU激活函数
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x


# 创建模型实例
model = IrisNN()
# 定义损失函数（交叉熵损失函数）
loss_function = nn.CrossEntropyLoss()
# 使用Adam优化器（大多数任务表现较好）
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型（迭代256个轮次）
for _ in range(256):
    model.train()
    # 清除上一次的梯度
    optimizer.zero_grad()
    # 计算输出
    output = model(X_train_tensor)
    # 计算损失
    loss = loss_function(output, y_train_tensor)
    # 反向传播
    loss.backward()
    # 更新权重
    optimizer.step()

# 评估模型
model.eval()
with torch.no_grad():
    output = model(X_test_tensor)
    # 获取预测得分最大值的索引（预测标签）
    _, y_pred_tensor = torch.max(output, 1)
    # 计算并输出预测准确率
    print(f&apos;Accuracy: {accuracy_score(y_test_tensor, y_pred_tensor):.2%}&apos;)
    # 输出分类模型评估报告
    print(classification_report(y_test_tensor, y_pred_tensor))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果还没有安装 PyTorch 库，可以使用命令&lt;code&gt;pip install torch&lt;/code&gt;进行安装。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们也可以通过神经网络模型来处理回归任务，这里仍然以之前讲回归模型时使用过的“汽车 MPG 数据集”为例，演示如何通过构造神经网络模型解决回归任务，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ssl

import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

ssl._create_default_https_context = ssl._create_unverified_context


def load_prep_data():
    &quot;&quot;&quot;加载准备数据&quot;&quot;&quot;
    df = pd.read_csv(&apos;https://archive.ics.uci.edu/static/public/9/data.csv&apos;)
    # 对特征进行清洗
    df.drop(columns=[&apos;car_name&apos;], inplace=True)
    df.dropna(inplace=True)
    df[&apos;origin&apos;] = df[&apos;origin&apos;].astype(&apos;category&apos;)
    df = pd.get_dummies(df, columns=[&apos;origin&apos;], drop_first=True).astype(&apos;f8&apos;)
    # 对特征进行缩放
    scaler = StandardScaler()
    return scaler.fit_transform(df.drop(columns=&apos;mpg&apos;).values), df[&apos;mpg&apos;].values


class MLPRegressor(nn.Module):
    &quot;&quot;&quot;神经网络模型&quot;&quot;&quot;

    def __init__(self, n):
        super(MLPRegressor, self).__init__()
        self.fc1 = nn.Linear(n, 64)
        self.fc2 = nn.Linear(64, 64)
        self.fc3 = nn.Linear(64, 1)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)
        return x


def main():
    # 加载和准备数据集
    X, y = load_prep_data()
    # 划分训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=3)
    # 将数据转为PyTorch的Tensor
    X_train_tensor, X_test_tensor, y_train_tensor, y_test_tensor = (
        torch.tensor(X_train, dtype=torch.float32),
        torch.tensor(X_test, dtype=torch.float32),
        torch.tensor(y_train, dtype=torch.float32).view(-1, 1),
        torch.tensor(y_test, dtype=torch.float32).view(-1, 1)
    )

    # 实例化神经网络模型
    model = MLPRegressor(X_train.shape[1])
    # 指定损失函数（均方误差）
    criterion = nn.MSELoss()
    # 指定优化器（Adam优化器）
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    # 模型训练
    epochs = 256
    for epoch in range(epochs):
        # 前向传播
        y_pred_tensor = model(X_train_tensor)
        loss = criterion(y_pred_tensor, y_train_tensor)
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 16 == 0:
            print(f&apos;Epoch [{epoch + 1} / {epochs}], Loss: {loss.item():.4f}&apos;)

    # 模型评估
    model.eval()
    with torch.no_grad():
        y_pred = model(X_test_tensor)
        test_loss = mean_squared_error(y_test, y_pred.numpy())
        r2 = r2_score(y_test, y_pred.numpy())
    print(f&apos;Test MSE: {test_loss:.4f}&apos;)
    print(f&apos;Test R2: {r2:.4f}&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Epoch [16 / 256], Loss: 588.2971
Epoch [32 / 256], Loss: 530.0340
Epoch [48 / 256], Loss: 429.9081
Epoch [64 / 256], Loss: 286.5121
Epoch [80 / 256], Loss: 133.6717
Epoch [96 / 256], Loss: 44.3843
Epoch [112 / 256], Loss: 30.3168
Epoch [128 / 256], Loss: 24.0182
Epoch [144 / 256], Loss: 19.5326
Epoch [160 / 256], Loss: 16.4941
Epoch [176 / 256], Loss: 14.2642
Epoch [192 / 256], Loss: 12.5515
Epoch [208 / 256], Loss: 11.2248
Epoch [224 / 256], Loss: 10.1825
Epoch [240 / 256], Loss: 9.3600
Epoch [256 / 256], Loss: 8.7116
Test MSE: 8.7226
Test R2: 0.8569
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的输出可以看到，随着神经网络模型不断的前向传播和反向传播，损失变得越来越小，模型的拟合变得越来越好。在预测的时候，我们利用训练得到的模型参数进行一次正向传播，就完成了从特征到目标值的映射，评估回归模型的两个指标 MSE 和 $\small{R^{2}}$ 看起来还不错哟。目前，神经网络被广泛应用于模式识别、图像处理、语音识别等领域，是深度学习中最核心的技术。对深度学习有兴趣的读者，可以关注我的另一个项目&lt;a href=&quot;https://github.com/jackfrued/Deep-Learning-Is-Nothing&quot;&gt;“深度学习就是大力出奇迹”&lt;/a&gt;，目前该项目仍然在创作更新中。&lt;/p&gt;
&lt;h4&gt;模型优缺点&lt;/h4&gt;
&lt;p&gt;神经网络模型最可爱的一点就是可以像搭积木一样不断的扩展模型边界，对于模型内部具体的运行则不需要加以太多的干涉。神经网络能够学习和拟合非常复杂的映射关系，能够根据输入数据自动调整其结构和参数，因此具有较强的适应性。当面临不同类型的数据或任务时，神经网络可以通过调整网络的结构（层数、节点数等）来适应不同的需求，只要数据量足够大、算力足够强，最终都会得到一个不错的结果，表现常常优于传统的机器学习算法，这就是我们常说的“大力出奇迹”。此外，大家耳熟能详的深度学习一词中的“深度”二字，指的就是神经网络模型的层次具备了足够的深度，我们也通常把这样的神经网络称为深度神经网络（DNN）。&lt;/p&gt;
&lt;p&gt;神经网络模型就像一个黑盒子，你给它数据它告诉你结果，至于为什么会出现这样的结果却缺乏可解释性。所以，在很多对模型的解释性要求非常高的场景（如信用评级、金融风控等），我们可能没有办法使用神经网络模型。再者，神经网络特别是深度神经网络需要大量的计算资源来训练，尤其是在处理高维数据时，训练时间和所需的计算资源可能非常庞大，需要强大的硬件支持，如高性能的图形处理单元（GPU）或专用硬件（TPU）。最后，也是我个人觉得神经网络模型最坑的一点是超参数调优过于困难，网络的层数、每层神经元的数量、学习率、激活函数的选择等都可能影响模型的性能，如果没有强大的计算资源作为支撑，很难完成超参数的调优，个人和小公司基本就被劝退了。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;神经网络模型使用了与传统机器学习算法不同的方式从海量数据中学习知识，尽管模型缺乏可解释性，但不能否认它在图像、语音和自然语言处理等多个领域已经取得了显著的成绩。当然，训练神经网络模型需要消耗大量的资源，在应用神经网络模型时建议权衡其优缺点，并根据具体的任务需求选择合适的模型和训练方法。对于那些数据量较小、需要可解释性或计算资源有限的场景，我们建议考虑使用传统的机器学习算法或者其他更轻量级的模型。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;89 自然语言处理入门&lt;/h2&gt;
&lt;p&gt;自然语言处理（Natural Language Processing，NLP）是计算机科学和人工智能领域中的一个重要分支，旨在使计算机能够理解、生成和与人类语言进行交互。NLP 的应用场景广泛，包括文本分类、情感分析、机器翻译、语音识别、聊天机器人等。随着深度学习的迅猛发展，NLP 的研究也逐渐由传统的机器学习方法转向深度学习方法。经典的 NLP 方法依赖于特征工程和模型设计，而现代的深度学习方法则更多地依赖于数据驱动和自动特征学习。&lt;/p&gt;
&lt;h4&gt;词袋模型&lt;/h4&gt;
&lt;p&gt;词袋模型（Bag of Words）是 NLP 中最简单的文本表示方法之一，它将文本中的每个单词看作一个“词袋”，忽略了单词的顺序和语法，只考虑每个单词出现的频率。简单来说，词袋模型通过将文本转换为词频向量（TF）来表示文本。构造词袋模型有两个非常简单的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建词汇表&lt;/strong&gt;：首先扫描所有文档，列出文本中所有的单词（重复的单词只保留一项），形成一个词汇表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文本向量化&lt;/strong&gt;：将每个文档中的单词映射到词汇表中，并统计每个单词的出现次数，形成文档的向量表示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，有三个文档分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;I love programming.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I love machine learning.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I love apple.&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么，构造词汇表将得到：&lt;code&gt;[&apos;I&apos;, &apos;love&apos;, &apos;programming&apos;, &apos;machine&apos;, &apos;learning&apos;, &apos;apple&apos;]&lt;/code&gt;。然后，将每个文档转换为词频向量，分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;[1, 1, 1, 0, 0, 0]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[1, 1, 0, 1, 1, 0]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[1, 1, 0, 0, 0, 1]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Scikit-learn 库提供了构建词词袋模型的类&lt;code&gt;CountVectorizer&lt;/code&gt;，使用的方法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.feature_extraction.text import CountVectorizer

# 文档列表
documents = [
    &apos;I love programming.&apos;,
    &apos;I love machine learning.&apos;,
    &apos;I love apple.&apos;
]

# 创建词袋模型
cv = CountVectorizer()
X = cv.fit_transform(documents)

# 输出词汇表和词频向量
print(&apos;词汇表:\n&apos;, cv.get_feature_names_out())
print(&apos;词频向量:\n&apos;, X.toarray())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;词汇表:  [&apos;apple&apos; &apos;learning&apos; &apos;love&apos; &apos;machine&apos; &apos;programming&apos;]
词频向量:
[[0 0 1 0 1]
 [0 1 1 1 0]
 [1 0 1 0 0]]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的词汇表中没有单词&lt;code&gt;&apos;I&apos;&lt;/code&gt;，因为这个单词被认为是停用词而忽略不计了。所谓停用词通常是指特定语言中高频出现但对文本分析任务贡献较小的词语。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于中文文档，需要利用三方库&lt;code&gt;jieba&lt;/code&gt;进行中文分词处理，先将句子拆分为词，然后再构造词频向量，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import jieba
from sklearn.feature_extraction.text import CountVectorizer

# 文档列表
documents = [
    &apos;我在四川大学读书&apos;,
    &apos;四川大学是四川最好的大学&apos;,
    &apos;大学校园里面有很多学生&apos;,
]

# 创建词袋模型并指定分词函数
cv = CountVectorizer(
    tokenizer=lambda x: jieba.cut(x),
    token_pattern=None
)
X = cv.fit_transform(documents)

# 输出词汇表和词频向量
print(&apos;词汇表:\n&apos;, cv.get_feature_names_out())
print(&apos;词频向量:\n&apos;, X.toarray())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果尚未安装中文分词库，可以通过命令&lt;code&gt;pip install jieba&lt;/code&gt;进行安装。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;词汇表:
 [&apos;四川&apos; &apos;四川大学&apos; &apos;在&apos; &apos;大学&apos; &apos;大学校园&apos; &apos;学生&apos; &apos;很多&apos; &apos;我&apos; &apos;是&apos; &apos;最好&apos; &apos;有&apos; &apos;的&apos; &apos;读书&apos; &apos;里面&apos;]
词频向量:
 [[0 1 1 0 0 0 0 1 0 0 0 0 1 0]
  [1 1 0 1 0 0 0 0 1 1 0 1 0 0]
  [0 0 0 0 1 1 1 0 0 0 1 0 0 1]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想去掉中文中常见的停用词，可以在创建&lt;code&gt;CountVectorizer&lt;/code&gt;对象时指定&lt;code&gt;stop_words&lt;/code&gt;参数，我们可以从中文停用词文件中加载停用词清单，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import jieba
from sklearn.feature_extraction.text import CountVectorizer

with open(&apos;哈工大停用词表.txt&apos;) as file_obj:
    stop_words_list = file_obj.read().split(&apos;\n&apos;)

# 文档列表
documents = [
    &apos;我在四川大学读书&apos;,
    &apos;四川大学是四川最好的大学&apos;,
    &apos;大学校园里面有很多学生&apos;,
]

# 创建词袋模型并指定分词函数
cv = CountVectorizer(
    tokenizer=lambda x: jieba.lcut(x),
    token_pattern=None,
    stop_words=stop_words_list
)
X = cv.fit_transform(documents)

# 输出词汇表和词频向量
print(&apos;词汇表:\n&apos;, cv.get_feature_names_out())
print(&apos;词频向量:\n&apos;, X.toarray())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面用到的中文停用词文件可以点击&lt;a href=&quot;https://github.com/goto456/stopwords&quot;&gt;传送门&lt;/a&gt;获得。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;词汇表:
 [&apos;四川&apos; &apos;四川大学&apos; &apos;大学&apos; &apos;大学校园&apos; &apos;学生&apos; &apos;很多&apos; &apos;最好&apos; &apos;读书&apos; &apos;里面&apos;]
词频向量:
 [[0 1 0 0 0 0 0 1 0]
  [1 1 1 0 0 0 1 0 0]
  [0 0 0 1 1 1 0 0 1]]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;词向量&lt;/h4&gt;
&lt;p&gt;与词袋模型不同，词向量（Word Embedding）将每个单词映射到一个稠密的向量空间中，从而使计算机能够理解和处理文本数据。这些向量通过捕捉单词之间的语义和上下文关系，可以让模型更好地进行词义匹配和语义分析。例如，单词&lt;code&gt;&apos;king&apos;&lt;/code&gt;和&lt;code&gt;&apos;queen&apos;&lt;/code&gt;之间的关系、单词&lt;code&gt;&apos;cat&apos;&lt;/code&gt;和&lt;code&gt;&apos;dog&apos;&lt;/code&gt;之间的关系等在向量空间中应当有所体现。&lt;/p&gt;
&lt;p&gt;常见的词向量模型有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Word2Vec&lt;/strong&gt;：通过浅层神经网络训练来学习词向量，分为两种架构：CBOW （Continuous Bag Of Words）和 Skip-Gram。
&lt;ul&gt;
&lt;li&gt;CBOW 模型的核心思想是：给定一个上下文词汇，预测中心词，即通过上下文中的词汇来预测目标词汇，上下文的顺序不重要，因为词袋是一个集合，而集合没有顺序的概念。假设你有一个句子&quot;The cat sits on the mat.&quot;，如果目标是预测 &quot;sits&quot; 这个词，那么 CBOW 会使用上下文词汇&quot;The&quot;、&quot;cat&quot;、&quot;on&quot;、&quot;the&quot;、&quot;mat&quot;来预测&quot;sits&quot;。在训练过程中，CBOW 会根据上下文中的词来调整 &quot;sits&quot; 的词向量，使得在上下文环境下该词能够更准确地表示语义。通过这种方式，CBOW 学习到的是一个词汇的上下文相关的表示。&lt;/li&gt;
&lt;li&gt;与 CBOW 相反，Skip-Gram 模型的核心思想是：给定一个目标词，预测该词的上下文词汇。即通过一个中心词来预测其周围的词汇。假设我们还是以句子&quot;The cat sits on the mat.&quot;为例，目标词是&quot;sits&quot;。在 Skip-Gram 模型中，系统会用&quot;sits&quot;来预测它的上下文词汇，即&quot;The&quot;、&quot;cat&quot;、&quot;on&quot;、&quot;the&quot;、&quot;mat&quot;。Skip-Gram 模型会通过训练，使得&quot;sits&quot;的词向量能最大化其在训练数据中与上下文词的关联度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GloVe&lt;/strong&gt;：基于词频矩阵的分解，能够捕捉到词汇的全局统计信息，由斯坦福大学的研究团队在2014年提出。GloVe 的词向量可以用于多种自然语言处理任务，如文本分类、情感分析、机器翻译等。它为后续的深度学习模型提供了有效的输入特征。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面，我们简单讲一下词向量学习的基本思路，主要有三个核心要素：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入和目标&lt;/strong&gt;：假设我们使用 Word2Vec 模型（CBOW 或 Skip-Gram），目标是通过一个单词（中心词）或上下文词来学习词向量。在 Skip-Gram 中，给定一个单词，目标是预测其上下文中的单词；在 CBOW 中，给定上下文单词，目标是预测中心词。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;神经网络模型&lt;/strong&gt;：Word2Vec 通过一个简单的神经网络来学习词向量，输入层是每个单词对应的独热编码；隐藏层是一个低维的稠密向量（即词向量），它在训练过程中通过反向传播逐渐优化；输出层是目标单词的概率分布，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/77b667561c.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;训练过程&lt;/strong&gt;：通过上下文和目标单词的共现信息，模型根据每个单词的上下文调整词向量，使得语义上相似的词向量距离更近。训练过程中，神经网络通过最小化误差（即预测的词与实际词之间的差距）来调整词向量，使得能够准确预测词汇之间的联系。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;词向量的维度通常是一个超参数，需要根据实际情况来选择。通常，常见的维度范围为 50 到 300 之间，但有时可以更高或更低。较低的维度可能无法捕捉到词汇的丰富语义信息，但会减少计算复杂度，适用于训练数据比较少或对精度要求不高的场景；较高的维度可以捕捉更多的语义特征，适用于大规模数据集，但计算成本也会增加。&lt;/p&gt;
&lt;p&gt;一旦词向量被训练出来，它们就能捕捉到词汇之间的多种关系。最常见的方式是通过计算词向量之间的距离或相似度来理解它们的关系。计算两个词向量之间的余弦相似度是最常用的方式，余弦相似度的公式我们之前提到过，如下所示。&lt;/p&gt;
&lt;p&gt;$$
\text{Cosine Similarity}(\mathbf{A}, \mathbf{B}) = \frac{\mathbf{A} \cdot \mathbf{B}}{\lVert \mathbf{A} \rVert \lVert \mathbf{B} \rVert}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\mathbf{A}}$ 和 $\small{\mathbf{B}}$ 是两个词的词向量， $\small{\cdot}$ 是向量的点积运算， $\small{\lVert \mathbf{A} \rVert}$ 和 $\small{\lVert \mathbf{B} \rVert}$ 是它们的模长。余弦相似度的值介于 -1 到 1 之间，值越大表示两个词越相似，越小则表示越不相似。&lt;/p&gt;
&lt;p&gt;另一方面，我们可以研究词向量的空间关系并完成一些有趣的运算。例如，如果我们想知道&lt;code&gt;&apos;king&apos;&lt;/code&gt;（国王）和&lt;code&gt;&apos;queen&apos;&lt;/code&gt;（王后）之间的关系，可以通过这样的方式来探索：&lt;/p&gt;
&lt;p&gt;$$
\text{king} − \text{man} + \text{woman} \approx \text{queen}
$$&lt;/p&gt;
&lt;p&gt;这个式子表示，&lt;code&gt;&apos;king&apos;&lt;/code&gt;去掉&lt;code&gt;&apos;man&apos;&lt;/code&gt;（男人）的部分，再加上&lt;code&gt;&apos;woman&apos;&lt;/code&gt;（女人）的部分，得到的结果约等于&lt;code&gt;&apos;queen&apos;&lt;/code&gt;。这样的运算可以捕捉到性别上的相似性。此外，利用词向量我们还可以进行聚类分析、词义消歧（通过上下文和相似度判断词在特定场景中的意思）、文本分类、情感分析等操作。&lt;/p&gt;
&lt;p&gt;我们用下面的代码为大家展示词向量的构建以及词与词相似性的度量，这里需要安装一个名为&lt;code&gt;gensim&lt;/code&gt;的三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install gensim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，我们还需要提前加载训练模型的语料库，这里我用的是 Hugging Face 提供了的 &lt;code&gt;datasets&lt;/code&gt; 库，通过它能够加载多种公开数据集，包括一些常用的语料库。当然，你也可以选择像&lt;code&gt;nltk&lt;/code&gt;这样专门用于自然语言处理的工具包来下载语料库。如果还没有安装&lt;code&gt;datasets&lt;/code&gt;库，需要使用下面的命令进行安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install datasets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先加载 IMDB 数据集，它是一个广泛使用的文本分类数据集，尤其在情感分析和自然语言处理领域中。IMDB 数据集包含大量电影评论，标注了每条评论的情感倾向（这里我们暂时用不到）。IMDB 数据集通常包含 50000 条评论，其中 25000 条用于训练，25000 条用于测试。每条评论都是观众对电影的评价，内容有长有短，我们就用它来作为训练模型的语料库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

from datasets import load_dataset
from gensim.models import Word2Vec
from sklearn.metrics.pairwise import cosine_similarity

# 加载 IMDB 数据集
imdb = load_dataset(&apos;imdb&apos;)
# 直接将 50000 条评论用作语料
temp = [imdb[&apos;unsupervised&apos;][i][&apos;text&apos;] for i in range(50000)]
# 用正则表达式对评论文本进行简单处理
corpus = [re.sub(r&apos;[^\w\s]&apos;, &apos;&apos;, x) for x in temp]

# 预处理语料库（英文分词）
sentences = [sentence.lower().split() for sentence in corpus]
# 训练 Word2Vec 模型
# sentences - 输入语料库（句子构成的列表，每个句子是一个或多个单词的列表）
# vector_size - 词向量的维度（维度越高能够表示的信息越多）
# windows - 上下文窗口大小（模型在训练时使用的上下文单词的范围）
# min_count - 忽略频率低于此值的单词（过滤掉在语料库中出现次数较少的单词）
# workers - 训练时使用的 CPU 核心数量
# seed - 随机数种子（用于初始化模型的权重）
model = Word2Vec(sentences, vector_size=100, window=10, min_count=2, workers=4, seed=3)

# 通过模型获取 king 和 queen 的词向量
king_vec, queen_vec = model.wv[&apos;king&apos;], model.wv[&apos;queen&apos;]

# 计算两个词向量的余弦相似度
cos_similarity = cosine_similarity([king_vec], [queen_vec])
print(f&apos;king 和 queen 的余弦相似度: {cos_similarity[0, 0]:.2f}&apos;)

# 通过词向量进行推理（king - man + woman ≈ queen）
man_vec, woman_vec = model.wv[&apos;man&apos;], model.wv[&apos;woman&apos;]
result_vec = king_vec - man_vec + woman_vec
# 查找与计算结果最相似的三个词
similar_words = model.wv.similar_by_vector(result_vec, topn=3)
print(f&apos;跟 king - man + woman 最相似的词:\n {similar_words}&apos;)

# 查找与 dog 最相似的五个词
dog_similar_words = model.wv.most_similar(&apos;dog&apos;, topn=5)
print(f&apos;跟 dog 最相似的词:\n {dog_similar_words}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;king 和 queen 的余弦相似度: 0.43
跟 king - man + woman 最相似的词:
 [(&apos;queen&apos;, 0.7126370668411255), (&apos;princess&apos;, 0.6760246753692627), (&apos;mary&apos;, 0.5891180038452148)]
跟 dog 最相似的词:
 [(&apos;cat&apos;, 0.7801533937454224), (&apos;sheep&apos;, 0.6783771514892578), (&apos;pet&apos;, 0.6658452749252319), 
  (&apos;doll&apos;, 0.655034065246582), (&apos;dude&apos;, 0.6548768877983093)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码的输出可以看出，跟&lt;code&gt;&apos;king&apos; - &apos;man&apos; + &apos;woman&apos;&lt;/code&gt;最相似的词就是&lt;code&gt;&apos;queen&apos;&lt;/code&gt;和&lt;code&gt;&apos;princess&apos;&lt;/code&gt;（公主、王妃），另外一个单词是女孩子的名字。跟&lt;code&gt;&apos;dog&apos;&lt;/code&gt;（狗）最相似的单词有：&lt;code&gt;&apos;cat&apos;&lt;/code&gt;（猫）、&lt;code&gt;&apos;sheep&apos;&lt;/code&gt;（绵羊）、&lt;code&gt;&apos;pet&apos;&lt;/code&gt;（宠物）、&lt;code&gt;&apos;doll&apos;&lt;/code&gt;（玩偶）和&lt;code&gt;&apos;dude&apos;&lt;/code&gt;（家伙）。&lt;/p&gt;
&lt;h4&gt;NPLM和RNN&lt;/h4&gt;
&lt;p&gt;Word2Vec 虽然已经帮助我们掌握了词与词之间的联系，但是由于无法捕捉长距离依赖（超出上下文窗口的词汇）和无法处理未知词（训练中没有出现的词汇）等问题，无法让 NLP 在具体的下游任务中实现突破和应用落地。随着对 Word2Vec 局限性的认识，研究者提出了 &lt;strong&gt;NPLM&lt;/strong&gt;（神经网络语言模型），采用神经网络结构来预测单词序列中的下一个词。与传统的语言模型不同，NPLM 通过神经网络（通常是前馈神经网络）来处理整个单词序列，而不仅仅是局部的上下文信息，从而能够建模更为复杂的语言结构，其结构如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/3e403cfc31.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当然，NPLM 模型也存在无法有效捕捉长期依赖关系的问题，为此，研究者又提出了 &lt;strong&gt;RNN&lt;/strong&gt;（循环神经网络）模型。RNN 是一种能够处理序列数据的神经网络，它的关键特性是能够将前一时刻的隐藏状态传递到下一时刻，从而在时间维度上建立联系。RNN 在训练时能够捕捉文本中的时序依赖关系，并且不局限于固定长度的上下文窗口。RNN 在每个时间步依赖于当前输入和上一时刻的隐藏状态，能够有效捕捉序列数据中的时间依赖性，通过反向传播算法更新权重，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9a10449bd1.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;为了解决传统 RNN 中的梯度消失和梯度爆炸问题，研究人员又提出了&lt;strong&gt;长短期记忆&lt;/strong&gt;（LSTM） 和&lt;strong&gt;门控循环单元&lt;/strong&gt;（GRU）等改进算法。LSTM 和 GRU 都引入了门控机制，允许模型控制信息的流动和遗忘，从而有效防止了梯度消失问题，可以捕捉长时间序列的依赖关系。但是，LSTM 和 GRU 仍然存在计算效率低、训练速度慢的问题，且无法进行并行化训练的问题。&lt;/p&gt;
&lt;h4&gt;Seq2Seq&lt;/h4&gt;
&lt;p&gt;起初，研究人员尝试用一个独立的 RNN 来解决机器翻译、文本摘要、语音识别等 NLP 任务，但是发现效果并不理想。主要原因是 RNN 在同时处理输入和输出序列时，既要负责编码又要负责解码，所以很容易出现信息损失。后来，谷歌提出了 Seq2Seq 模型，核心思想是通过学习输入与输出序列之间的映射关系，从而实现序列到序列的转换。Seq2Seq 模型通常包含一个&lt;strong&gt;编码器&lt;/strong&gt;和一个&lt;strong&gt;解码器&lt;/strong&gt;，编码器通常由 RNN 或其变体构成（LSTM、GRU等）构成，其主要任务是读取输入序列并将其转换成上下文向量，上下文向量包含了整个输入序列的语义信息，并作为解码器生成输出的基础；解码器通常也是由 RNN 或其变体构成，其主要任务是根据上下文向量生成目标序列。Seq2Seq 模型的结构如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/9e13a09255.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;简单的 Seq2Seq 模型将整个输入序列的信息压缩到一个固定大小的上下文向量中，这可能导致信息丢失，尤其是在处理长序列时。&lt;strong&gt;注意力机制&lt;/strong&gt;是 Seq2Seq 模型的一个重要扩展，通过在每个解码步骤中动态的对编码器输出的不同部分进行加权求和，允许解码器在生成每个词时能够关注输入序列中的相关部分。这样，模型能够根据当前的解码需求自动“聚焦”在输入的某个子部分，而不是依赖于一个固定大小的上下文向量。&lt;/p&gt;
&lt;h4&gt;Transformer&lt;/h4&gt;
&lt;p&gt;为了进一步克服 LSTM、GRU 等模型在长序列建模中的局限，2017 年谷歌大脑（Google Brain）团队的 Vaswani 等人在其论文 &lt;a href=&quot;https://arxiv.org/pdf/1706.03762&quot;&gt;&lt;em&gt;Attention Is All You Need&lt;/em&gt;&lt;/a&gt; 中提出了 &lt;strong&gt;Transformer&lt;/strong&gt; 架构，这个架构迅速成为 NLP 领域的主流方法，彻底改变了这个领域的生态，大名鼎鼎的 GPT 和 BERT 都是基于 Transformer 架构的。Transformer 的核心是&lt;strong&gt;自注意力机制&lt;/strong&gt;（Self-Attention），它能够为输入序列中的每个元素分配不同的权重，从而更好的捕捉序列内部的依赖关系。此外，Transformer 摒弃了 RNN 和 LSTM 中的循环结构，采用了全新的编码器-解码器架构，这种设计使得模型可以并行处理输入数据，进一步加速训练的过程。除了 NLP 领域，Transformer 在计算机视觉、语音识别等领域也取得了显著的成果。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/76b60a84c0.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;Transformer 架构如上图所示，它也是一个编码器-解码器结构，图左边部分红框中是堆叠的 N 组编码器，右边部分蓝框中是堆叠的 N 组解码器。编码器负责处理输入序列并将其转化为一个上下文相关的表示，解码器则根据编码器的输出生成目标序列（例如翻译任务中的目标语言）。下面，我们先简单说一下编码器和解码器的构成。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编码器：每个编码器都包括多头注意力机制、前馈神经网络、残差连接和层归一化。&lt;/li&gt;
&lt;li&gt;解码器：解码器结构类似编码器，但在自注意力层和编码器-解码器注意力层之间引入了额外的层，以便处理编码器输出的上下文信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们再来展开说说 Transformer 的核心组件，如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;嵌入层（输入嵌入和输出嵌入）：与传统的词向量方法（如 Word2Vec 或 GloVe）类似，Transformer 的输入首先需要通过词嵌入层将离散的单词映射为稠密的向量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置编码：由于 Transformer 不像 RNN 或 LSTM 那样处理序列中的时序信息，因此需要通过位置编码（Positional Encoding）来注入词序信息。位置编码的形式通常是一个与词嵌入大小相同的向量，每个维度编码了该词在序列中的位置信息。常见的做法是使用正弦和余弦函数来构造这些位置编码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自注意力机制：允许模型在每个时间步根据输入序列中的所有其他词来计算每个词的表示。这样每个词都能关注到序列中其他位置的信息，而不依赖于输入的顺序。自注意力机制的原理是：对每个输入的词，通过嵌入层计算三个向量，分别是：&lt;strong&gt;查询向量&lt;/strong&gt;（Q）、&lt;strong&gt;键向量&lt;/strong&gt;（K）、&lt;strong&gt;值向量&lt;/strong&gt;（V）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查询向量：用来查询其他词信息的向量，它代表我们关注的内容，每个输入词都会有一个对应的查询向量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;键向量：根据输入序列中每个元素生成的一个向量，它与查询向量进行对比，计算每个词与其他词的注意力分数，这个分数决定了当前词和其他词的相关性，计算公式如下，其中， $\small{d_{k}}$ 是键向量的维度，这里除以 $\small{\sqrt{d_k}}$ 的操作是为了防止点积值过大。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\text{Attention Score}(Q, K) = \frac{Q \cdot K^{T}}{\sqrt{d_{k}}}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值向量：实际的信息载体。在计算出注意力分数后，通过加权和运算得到的就是每个词的最终表示，这个最终表示是该词的上下文相关的向量，反映了该词在上下文中的重要性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\text{Output} = \sum_{i} \text{softmax} \left( \frac{Q \cdot K^{T}}{\sqrt{d_{k}}} \right) \cdot V_{i}
$$&lt;/p&gt;
&lt;p&gt;这里，我们得到的某个词的最终表示，不仅仅是它自身的语义，还包含了它与其他词的关系和上下文信息。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;多头注意力：在标准的自注意力机制中，Q、K、V 向量都是通过同一个线性变换得到的，但这可能会限制信息的捕获。多头注意力通过使用多个并行的注意力头来解决这个问题，每个头使用不同的线性变换来捕捉不同的特征或关系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前馈神经网络：为每个词提供非线性的变换，提取更高级别的特征，增强模型的能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;残差连接和层归一化：Transformer 中广泛使用了残差连接（将输入直接加到输出上）和层归一化（对每个样本的各个特征进行标准化），这些技术有助于避免训练过程中梯度消失并加速训练。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;从传统的基于规则、基于统计的方法到深度学习和 Transformer 架构，研究人员一直没有停下对 NLP 探索的脚本，即便经历了人工智能的几次寒冬。随着 GPT 和 BERT 的出现，再到今天各种大模型在不同领域得到广泛应用，计算机已经能够理解和生成几乎所有语言的精髓并顺利的通过“图灵测试”。这一切不仅改变了我们与机器的互动方式，更为信息的获取、理解和交流开辟了崭新的天地，语言的力量永不止步！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;90 机器学习实战&lt;/h2&gt;
&lt;p&gt;本章我们通过经典的“泰坦尼克号生存预测”项目为大家讲解机器学习的完整应用，该项目来自于全球知名的数据科学竞赛平台&lt;a href=&quot;https://www.kaggle.com/&quot;&gt;Kaggle&lt;/a&gt;。该平台是数据科学、机器学习、人工智能领域的重要社区之一，为数据科学家和算法工程师提供了一个实践、分享和竞争的空间。无论是新手还是经验丰富的专家，Kaggle 都能为其提供丰富的资源和挑战，通过平台提供的数据集、竞赛、课程等资源，用户可以根据自身的需求提升相关的数据科学技能并与全球的数据科学家互动。&lt;/p&gt;
&lt;p&gt;泰坦尼克号生存预测项目是 Kaggle 上最著名的入门级机器学习项目之一。该项目要求你根据乘客的基本信息（如年龄、性别、舱位、登船地等）预测他们是否能够在泰坦尼克号沉船事件中幸存下来，属于典型的分类任务。我们可以在 Kaggle 网站的 Competitions 搜索到名为“&lt;a href=&quot;https://www.kaggle.com/competitions/titanic/&quot;&gt;&lt;em&gt;Titanic - Machine Learning from Disaster&lt;/em&gt;&lt;/a&gt;”的项目，点击进入可以看到项目的背景介绍并找到下载数据文件&lt;code&gt;train.csv&lt;/code&gt;和&lt;code&gt;test.csv&lt;/code&gt;的链接，显然前者是训练集（包含特征和标签）而后者是测试集（只有特征）。我们可以下载数据文件到本地并训练模型，然后再提交自己的预测结果；当然，如果愿意也可以在线创建 Notebook 文件并编写和运行代码。&lt;/p&gt;
&lt;h4&gt;数据探索&lt;/h4&gt;
&lt;p&gt;我们首先加载训练模型的数据，假设数据文件保存在当面路径下名为&lt;code&gt;data&lt;/code&gt;的目录中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import pandas as pd

df = pd.read_csv(&apos;data/train.csv&apos;, index_col=&apos;PassengerId&apos;)
df.head(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;			Survived Pclass	Name			Sex		Age		SibSp	Parch	Ticket		Fare	Cabin	Embarked
PassengerId											
1			0		 3		Braund, ...	    male	22.0	1		0		A/5 ...		7.2500	NaN		S
2			1		 1		Cumings, ...	female	38.0	1		0		PC ...		71.2833	C85		C
3			1		 3		Heikkinen, ...  female	26.0	0		0		STON/O2 ...	7.9250	NaN		S
4			1		 1		Futrelle, ...   female	35.0	1		0		113803 ...	53.1000	C123	S
5			0		 3		Allen, ...	    male	35.0	0		0		373450 ...	8.0500	NaN		S
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：加载数据的时候，我们直接将 PassengerID （乘客编号）列处理成了行索引。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面的数据字典对&lt;code&gt;DataFrame&lt;/code&gt;每个列的含义进行了说明。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;列名&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Survived&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否幸存&lt;/td&gt;
&lt;td&gt;目标类别（&lt;code&gt;1&lt;/code&gt;表示幸存，&lt;code&gt;0&lt;/code&gt;表示遇难）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pclass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;舱位等级&lt;/td&gt;
&lt;td&gt;整数，取值为&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;，表示三种不同的等级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;乘客姓名&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;性别&lt;/td&gt;
&lt;td&gt;字符串，&lt;code&gt;male&lt;/code&gt;为男性，&lt;code&gt;female&lt;/code&gt;为女性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;年龄&lt;/td&gt;
&lt;td&gt;浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SibSp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;平辈亲属登船人数&lt;/td&gt;
&lt;td&gt;整数，包括兄弟、姐妹、配偶总共登船的人数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Parch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;父母小孩登船人数&lt;/td&gt;
&lt;td&gt;整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ticket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;船票号&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Fare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;船票价格&lt;/td&gt;
&lt;td&gt;浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cabin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;客舱号&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Embarked&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;登船港口&lt;/td&gt;
&lt;td&gt;字符串，&lt;code&gt;C&lt;/code&gt;表示瑟堡，&lt;code&gt;Q&lt;/code&gt;表示皇后镇、&lt;code&gt;S&lt;/code&gt;表示南安普顿&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们对数据进行可视化操，通过统计图表实训对数据的初步探索，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

# 修改配置添加中文字体
plt.rcParams[&apos;font.sans-serif&apos;].insert(0, &apos;SimHei&apos;)
plt.rcParams[&apos;axes.unicode_minus&apos;] = False

# 定制画布
plt.figure(figsize=(16, 12), dpi=200)

# 遇难和获救人数分布
plt.subplot(3, 4, 1)
ser = df.Survived.value_counts()
ser.plot(kind=&apos;bar&apos;, color=[&apos;#BE3144&apos;, &apos;#3A7D44&apos;])
plt.xticks(rotation=0)
plt.title(&apos;图1.获救情况分布&apos;)
plt.ylabel(&apos;人数&apos;)
plt.xlabel(&apos;&apos;)
for i, v in enumerate(ser):
    plt.text(i, v, v, ha=&apos;center&apos;)

# 客舱等级人数分布
plt.subplot(3, 4, 2)
ser = df.Pclass.value_counts().sort_index()
ser.plot(kind=&apos;bar&apos;, color=[&apos;#FA4032&apos;, &apos;#FA812F&apos;, &apos;#FAB12F&apos;])
plt.xticks(rotation=0)
plt.ylabel(&apos;人数&apos;)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图2.客舱等级分布&apos;)
for i, v in enumerate(ser):
    plt.text(i, v, v, ha=&apos;center&apos;)

# 性别人数分布
plt.subplot(3, 4, 3)
ser = df.Sex.value_counts()
ser.plot(kind=&apos;bar&apos;, color=[&apos;#16404D&apos;, &apos;#D84040&apos;])
plt.xticks(rotation=0)
plt.ylabel(&apos;人数&apos;)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图3.性别分布&apos;)
for i, v in enumerate(ser):
    plt.text(i, v, v, ha=&apos;center&apos;)

# 登船港口人数分布
plt.subplot(3, 4, 4)
ser = df.Embarked.value_counts()
ser.plot(kind=&apos;bar&apos;, color=[&apos;#FA4032&apos;, &apos;#FA812F&apos;, &apos;#FAB12F&apos;])
plt.xticks(rotation=0)
plt.ylabel(&apos;人数&apos;)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图4.登船港口分布&apos;)
for i, v in enumerate(ser):
    plt.text(i, v, v, ha=&apos;center&apos;)

# 乘客年龄箱线图
plt.subplot(3, 4, 5)
df.Age.plot(kind=&apos;box&apos;, showmeans=True, notch=True)
plt.title(&apos;图5.乘客年龄情况&apos;)

# 船票价格箱线图
plt.subplot(3, 4, 6)
df.Fare.plot(kind=&apos;box&apos;, showmeans=True, notch=True)
plt.title(&apos;图6.船票价格情况&apos;)

# 不同客舱等级遇难和幸存人数分布
plt.subplot(3, 4, (7, 8))
s0 = df[df.Survived == 0].Pclass.value_counts()
s1 = df[df.Survived == 1].Pclass.value_counts()
temp = pd.DataFrame({&apos;遇难&apos;: s0, &apos;幸存&apos;: s1})
pcts = temp.div(temp.sum(axis=1), axis=0)
temp.plot(ax=plt.gca(), kind=&apos;bar&apos;, stacked=True, color=[&apos;#BE3144&apos;, &apos;#3A7D44&apos;])
for i, idx in enumerate(temp.index):
    plt.text(i, temp.at[idx, &apos;遇难&apos;] // 2, f&apos;{pcts.at[idx, &quot;遇难&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
    plt.text(i, temp.at[idx, &apos;遇难&apos;] + temp.at[idx, &apos;幸存&apos;] // 2, f&apos;{pcts.at[idx, &quot;幸存&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
plt.xticks(rotation=0)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图7.不同客舱等级幸存情况&apos;)

# 不同性别遇难和幸存人数分布
plt.subplot(3, 4, (9, 10))
s0 = df[df.Survived == 0].Sex.value_counts()
s1 = df[df.Survived == 1].Sex.value_counts()
temp = pd.DataFrame({&apos;遇难&apos;: s0, &apos;幸存&apos;: s1})
pcts = temp.div(temp.sum(axis=1), axis=0)
temp.plot(ax=plt.gca(), kind=&apos;bar&apos;, stacked=True, color=[&apos;#BE3144&apos;, &apos;#3A7D44&apos;])
for i, idx in enumerate(temp.index):
    plt.text(i, temp.at[idx, &apos;遇难&apos;] // 2, f&apos;{pcts.at[idx, &quot;遇难&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
    plt.text(i, temp.at[idx, &apos;遇难&apos;] + temp.at[idx, &apos;幸存&apos;] // 2, f&apos;{pcts.at[idx, &quot;幸存&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
plt.xticks(rotation=0)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图8.不同性别幸存情况&apos;)

# 不同登船港口遇难和幸存人数分布
plt.subplot(3, 4, (11, 12))
s0 = df[df.Survived == 0].Embarked.value_counts()
s1 = df[df.Survived == 1].Embarked.value_counts()
temp = pd.DataFrame({&apos;遇难&apos;: s0, &apos;幸存&apos;: s1})
pcts = temp.div(temp.sum(axis=1), axis=0)
temp.plot(ax=plt.gca(), kind=&apos;bar&apos;, stacked=True, color=[&apos;#BE3144&apos;, &apos;#3A7D44&apos;])
for i, idx in enumerate(temp.index):
    plt.text(i, temp.at[idx, &apos;遇难&apos;] // 2, f&apos;{pcts.at[idx, &quot;遇难&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
    plt.text(i, temp.at[idx, &apos;遇难&apos;] + temp.at[idx, &apos;幸存&apos;] // 2, f&apos;{pcts.at[idx, &quot;幸存&quot;]:.2%}&apos;, ha=&apos;center&apos;, va=&apos;center&apos;)
plt.xticks(rotation=0)
plt.xlabel(&apos;&apos;)
plt.title(&apos;图9.不同登船港口幸存情况&apos;)

plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/83270429b6.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;从上面的统计图表可以看出，一等舱的乘客有更高的存活率，女性乘客相较于男性乘客也有更高的存活率，在瑟堡登船的用户存活率较其他两地更高。如果愿意，我们还可以尝试对性别、舱位、年龄等维度进行交叉组合并绘制出对应的统计图表，甚至还可以探索乘客姓名中的称谓（&lt;code&gt;Mr&lt;/code&gt;、&lt;code&gt;Miss&lt;/code&gt;、&lt;code&gt;Mrs&lt;/code&gt;、&lt;code&gt;Dr&lt;/code&gt;、&lt;code&gt;Master&lt;/code&gt;、&lt;code&gt;Major&lt;/code&gt;等）跟乘客能否幸存是否存在某种关系，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;p&gt;我们进一步查看&lt;code&gt;DataFrame&lt;/code&gt;对象相关信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
Index: 891 entries, 1 to 891
Data columns (total 11 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Survived  891 non-null    int64  
 1   Pclass    891 non-null    int64  
 2   Name      891 non-null    object 
 3   Sex       891 non-null    object 
 4   Age       714 non-null    float64
 5   SibSp     891 non-null    int64  
 6   Parch     891 non-null    int64  
 7   Ticket    891 non-null    object 
 8   Fare      891 non-null    float64
 9   Cabin     204 non-null    object 
 10  Embarked  889 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 83.5+ KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，训练集一共&lt;code&gt;891&lt;/code&gt;条数据，其中年龄、客舱号、登船港口等列存在缺失值，乘客姓名、登船港口等是字符串类型没有办法直接用于模型训练，所以我们需要做一些准备工作。&lt;/p&gt;
&lt;h4&gt;特征工程&lt;/h4&gt;
&lt;p&gt;特征工程（Feature Engineering）是机器学习中的一个重要环节，通过对原始数据的处理、转换与选择，提取出能够帮助模型更好理解数据的特征，最终提升模型的预测性能。特征工程的主要步骤包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据清洗&lt;/strong&gt;：处理数据中的缺失值、重复值、异常值等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特征转换&lt;/strong&gt;：将原始数据转化为更适合模型输入的形式。常见的特征转换方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标准化和归一化：标准化将数据转换为均值为&lt;code&gt;0&lt;/code&gt;、标准差为&lt;code&gt;1&lt;/code&gt;的分布；归一化将数据缩放到特定的范围（如：$\small{[0, 1]}$），确保各特征在同一数量级，对应的公式如下所示：
$$
X^{\prime} = \frac{X - \mu}{\sigma}
$$&lt;/p&gt;
&lt;p&gt;$$
X^{\prime} = \frac{X - X_{min}}{X_{max} - X_{min}}
$$&lt;/p&gt;
&lt;p&gt;其中，$\small{\mu}$表示数据的均值，$\small{\sigma}$表示数据的标准差，$\small{X_{min}}$表示数据中的最小值，$\small{X_{max}}$表示数据中的最大值。Scikit-learn 库&lt;code&gt;preprocessing&lt;/code&gt;模块中的&lt;code&gt;StandardScaler&lt;/code&gt;和&lt;code&gt;MinMaxScaler&lt;/code&gt;可以实现数据的标准化和归一化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类别编码：对于类别变量（如：性别、职业、地区等），常用的编码方式有独热编码（One-Hot Encoding）和标签编码（Label Encoding）。Scikit-learn 库&lt;code&gt;preprocessing&lt;/code&gt;模块中的&lt;code&gt;OneHotEncoder&lt;/code&gt;和&lt;code&gt;LabelEncoder&lt;/code&gt;可以实现数据的独热编码和标签编码，当然也可以通过 pandas 库的&lt;code&gt;get_dummies&lt;/code&gt;函数来处理&lt;code&gt;DataFrame&lt;/code&gt;中的类别变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对数变换：对数变换可以帮助处理具有偏态分布的特征，使其更接近正态分布。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特征选择&lt;/strong&gt;：从大量的特征中筛选出对模型预测有用的特征，去除冗余或无关的特征，减少维度并提高模型的性能。常用的方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过滤法&lt;/strong&gt;：基于统计检验方法（如：卡方检验、皮尔逊相关系数等）选择最具区分度的特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包裹法&lt;/strong&gt;：通过模型的性能来评估特征子集的效果（如：递归特征消除法）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;嵌入法&lt;/strong&gt;：通过训练模型并从模型中提取特征的重要性（如：决策树、L1正则化等），从而进行特征选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特征降维&lt;/strong&gt;：通过某种数学变换将数据从高维空间映射到低维空间，以便保留数据的主要信息，简化模型，降维后的新特征是原始特征的某种组合。降维可以减少模型训练的运算量，减少不必要的噪声和冗余特征来降低复杂性，从而提高模型的泛化能力。特征降维常用的方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PCA&lt;/strong&gt;（主成分分析）：PCA 的目标是找出数据中最重要的特征（主成分），这些主成分能够保留数据的大部分方差。PCA 通过线性变换将数据投影到新的坐标轴上，使得投影后的数据方差最大化。换句话说，PCA通过对数据的特征进行“旋转”，将其转换为一个新的坐标系，新坐标系的每个轴（即主成分）按数据的方差大小排列，前几个主成分通常就能够解释数据的大部分变异。Scikit-learn 库&lt;code&gt;decomposition&lt;/code&gt;模块的&lt;code&gt;PCA&lt;/code&gt;类可以帮助我们完成主成分分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LDA&lt;/strong&gt;（线性判别分析）：LDA 的主要目标是通过寻找最佳的投影方向来最大化不同类别之间的可分性，并同时最小化同一类别内数据点的散布。LDA在特征空间中找到一个低维的空间，使得类别之间的差异尽可能大，而同一类别内的数据尽量聚集。简单的说，LDA 的核心思想是&lt;strong&gt;最大化类间散度，最小化类内散度&lt;/strong&gt;。Scikit-learn 库&lt;code&gt;discriminant_analysis&lt;/code&gt;模块的&lt;code&gt;LinearDiscriminantAnalysis&lt;/code&gt;类可以帮助我们完成线性判别分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;t-SNE&lt;/strong&gt;（t分布随机邻居嵌入）：t-SNE 是一种非线性的降维方法，主要用于数据可视化，特别适用于高维数据（如图像、文本等）的降维。t-SNE的目标是将高维空间中的相似点（邻近点）映射到低维空间中的相近位置，同时尽量避免不同类别之间的重叠。Scikit-learn 库&lt;code&gt;manifold&lt;/code&gt;模块的&lt;code&gt;TSNE&lt;/code&gt;类可以帮助我们完成t分布随机邻居嵌入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特征构造&lt;/strong&gt;：通过原始特征组合、变换或派生出新的特征。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;特征工程不仅是提高模型准确性的关键因素，也是机器学习中一个需要持续优化的过程。通过对数据进行精心的清洗、转换、选择和构造，可以为模型提供更有价值的信息，提高模型的表现。尽管特征工程看似复杂，但它的效果在很多情况下超出了模型本身的改进，因此在实际应用中，我们应投入足够的精力和时间来进行特征工程的优化。&lt;/p&gt;
&lt;p&gt;对于我们加载的数据集，我们可以将年龄字段的缺失值处理为中位数，将登船港口的缺失值处理为众数，客舱号字段的缺失值比较多，一种处理方式是直接删除这个列，另一种处理方式是二值化，有客舱号的记为&lt;code&gt;1&lt;/code&gt;，没有客舱号的记为&lt;code&gt;0&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df[&apos;Age&apos;] = df.Age.fillna(df.Age.median())
df[&apos;Embarked&apos;] = df.Embarked.fillna(df.Embarked.mode()[0])
df[&apos;Cabin&apos;] = df.Cabin.replace(r&apos;.+&apos;, &apos;1&apos;, regex=True).replace(np.nan, 0).astype(&apos;i8&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理完缺失值后，我们对年龄和船票价格两个字段进行特征缩放，通过&lt;code&gt;StandardScaler&lt;/code&gt;实现标准化，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
df[[&apos;Fare&apos;, &apos;Age&apos;]] = scaler.fit_transform(df[[&apos;Fare&apos;, &apos;Age&apos;]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，还需要对性别和登船港口两个字段进行独热编码处理，可以使用 pandas 库中的&lt;code&gt;get_dummies&lt;/code&gt;函数或 scikit-learn 库的&lt;code&gt;OneHotEncoder&lt;/code&gt;处理，两者处理的结果类似，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = pd.get_dummies(df, columns=[&apos;Sex&apos;, &apos;Embarked&apos;], drop_first=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们继续处理乘客姓名字段，根据姓名中的称谓衍生出一个新的特征；此外，对于&lt;code&gt;SibSp&lt;/code&gt;和&lt;code&gt;Parch&lt;/code&gt;两个字段，我们可以将其衍生为家庭成员数量的新特征，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title_mapping = {
    &apos;Mr&apos;: 0, &apos;Miss&apos;: 1, &apos;Mrs&apos;: 2, &apos;Master&apos;: 3, &apos;Dr&apos;: 4, &apos;Rev&apos;: 5, &apos;Col&apos;: 6, &apos;Major&apos;: 7, 
    &apos;Mlle&apos;: 8, &apos;Ms&apos;: 9, &apos;Lady&apos;: 10, &apos;Sir&apos;: 11, &apos;Jonkheer&apos;: 12, &apos;Don&apos;: 13, &apos;Dona&apos;: 14, &apos;Countess&apos;: 15
}
df[&apos;Title&apos;] = df[&apos;Name&apos;].map(
    lambda x: x.split(&apos;,&apos;)[1].split(&apos;.&apos;)[0].strip()
).map(title_mapping).fillna(-1)
df[&apos;FamilySize&apos;] = df[&apos;SibSp&apos;] + df[&apos;Parch&apos;] + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：我们将每个称谓映射到一个数字，这里不必过于纠结称谓跟数字的对应关系，而且有些称谓在我们的训练集中并没有出现。对于那些未知的称谓，我们统一处理为&lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完成上面的操作之后，我们可以把不必要的字段全部删除掉，数据的准备工作就基本完成了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.drop(columns=[&apos;Name&apos;, &apos;SibSp&apos;, &apos;Parch&apos;, &apos;Ticket&apos;], inplace=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;模型训练&lt;/h4&gt;
&lt;p&gt;首先我们将数据集划分为训练集和验证集，注意这里不是划分训练集和测试集，因为我们最终的测试集是官方提供的&lt;code&gt;test.csv&lt;/code&gt;文件，这里我们把&lt;code&gt;train.csv&lt;/code&gt;中 90% 的数据划分出来训练模型，用剩下的 10% 对模型进行验证。下面的代码中我们将验证集的特征和标签分别命名为&lt;code&gt;X_valid&lt;/code&gt;和&lt;code&gt;y_valid&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import train_test_split

X, y = df.drop(columns=&apos;Survived&apos;), df.Survived
X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.9, random_state=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先尝试逻辑回归模型，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

model = LogisticRegression(penalty=&apos;l1&apos;, tol=1e-6, solver=&apos;liblinear&apos;)
model.fit(X_train, y_train)
y_pred = model.predict(X_valid)
print(classification_report(y_valid, y_pred))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       0.88      0.80      0.84        56
           1       0.72      0.82      0.77        34

    accuracy                           0.81        90
   macro avg       0.80      0.81      0.80        90
weighted avg       0.82      0.81      0.81        90
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再试一试大杀器 XGBoost，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import xgboost as xgb

# 将数据处理成数据集格式DMatrix格式
dm_train = xgb.DMatrix(X_train, y_train)
dm_valid = xgb.DMatrix(X_valid)

# 设置模型参数
params = {
    &apos;booster&apos;: &apos;gbtree&apos;,             # 用于训练的基学习器类型
    &apos;objective&apos;: &apos;binary:logistic&apos;,  # 指定模型的损失函数
    &apos;gamma&apos;: 0.1,                    # 控制每次分裂的最小损失函数减少量
    &apos;max_depth&apos;: 10,                 # 决策树最大深度
    &apos;lambda&apos;: 0.5,                   # L2正则化权重
    &apos;subsample&apos;: 0.8,                # 控制每棵树训练时随机选取的样本比例
    &apos;colsample_bytree&apos;: 0.8,         # 用于控制每棵树或每个节点的特征选择比例
    &apos;eta&apos;: 0.05,                     # 学习率
    &apos;seed&apos;: 3,                       # 设置随机数生成器的种子
    &apos;nthread&apos;: 16,                   # 指定了训练时并行使用的线程数
}

model = xgb.train(params, dm_train, num_boost_round=200)
y_pred = model.predict(dm_valid)
# 将预测的概率转换为类别标签
y_pred_label = (y_pred &amp;gt; 0.5).astype(&apos;i8&apos;)
print(classification_report(y_valid, y_pred_label))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              precision    recall  f1-score   support

           0       0.89      0.91      0.90        56
           1       0.85      0.82      0.84        34

    accuracy                           0.88        90
   macro avg       0.87      0.87      0.87        90
weighted avg       0.88      0.88      0.88        90
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，在条件允许的情况下，&lt;strong&gt;强烈建议&lt;/strong&gt;大家通过网格搜索交叉验证的方式对模型的超参数进行调优，同时我们还可以绘制&lt;strong&gt;学习曲线&lt;/strong&gt;（learning curve）来辅助判定是否存在过拟合和欠拟合问题。学习曲线包含两条曲线，一条反映训练误差，一条反映验证误差。欠拟合通常表现为模型在训练集和验证集上的误差都较大，并且随着训练数据增加，训练误差和验证误差的差距较小，说明我们的模型过于简单，无法捕捉到数据中的模式。过拟合通常表现为模型在训练集上表现得很好（训练误差较低），但在验证集上表现较差（验证误差较高），且随着训练数据增加，训练误差持续降低而验证误差开始上升时，说明我们的模型过于复杂。Scikit-learn 库&lt;code&gt;model_selection&lt;/code&gt;模块的&lt;code&gt;learning_curve&lt;/code&gt;函数可以帮助我们绘制学习曲线，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;h4&gt;模型评估&lt;/h4&gt;
&lt;p&gt;接下来我们加载真正的测试数据&lt;code&gt;test.csv&lt;/code&gt;，通过前面训练好的模型来做出预测。我们可以将预测的结果保存成一个 CSV 文件，该文件共有两列，一列是 PassengerID，一列是我们预测的结果。我们将该文件提交到 Kaggle 平台，可以获得最终模型的准确率评分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test = pd.read_csv(&apos;data/test.csv&apos;, index_col=&apos;PassengerId&apos;)
# 处理缺失值
test[&apos;Age&apos;] = test.Age.fillna(test.Age.median())
test[&apos;Fare&apos;] = test.Fare.fillna(test.Fare.median())
test[&apos;Embarked&apos;] = test.Embarked.fillna(test.Embarked.mode()[0])
test[&apos;Cabin&apos;] = test.Cabin.replace(r&apos;.+&apos;, &apos;1&apos;, regex=True).replace(np.nan, 0).astype(&apos;i8&apos;)
# 特征缩放
test[[&apos;Fare&apos;, &apos;Age&apos;]] = scaler.fit_transform(test[[&apos;Fare&apos;, &apos;Age&apos;]])
# 处理类别
test = pd.get_dummies(test, columns=[&apos;Sex&apos;, &apos;Embarked&apos;], drop_first=True)
# 特征构造
test[&apos;Title&apos;] = test[&apos;Name&apos;].apply(lambda x: x.split(&apos;,&apos;)[1].split(&apos;.&apos;)[0].strip()).map(title_mapping).fillna(-1)
test[&apos;FamilySize&apos;] = test[&apos;SibSp&apos;] + test[&apos;Parch&apos;] + 1
# 删除多余特征
test.drop(columns=[&apos;Name&apos;, &apos;Ticket&apos;, &apos;SibSp&apos;, &apos;Parch&apos;], inplace=True)

# 使用逻辑回归模型
passenger_id, X_test = test.index, test
# 使用XGBoost模型
# passenger_id, X_test = test.index, xgb.DMatrix(test)

y_test_pred = model.predict(X_test)
# XGoost模型 - 将预测的概率转换成类别标签
# y_test_pred = (model.predict(X_test) &amp;gt; 0.5).astype(&apos;i8&apos;)

# 生成提交文件
result = pd.DataFrame({
    &apos;PassengerId&apos;: passenger_id,
    &apos;Survived&apos;: y_test_pred
})
result.to_csv(&apos;submission.csv&apos;, index=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可以试一试，不同的模型预测结果是否存在较大的差异，哪个是你心目中最理想的模型，然后大家可以通过实验验证我们之前提到的一个观点，看看特征工程是不是对预测的结果有重要的影响。下面是我对特征工程的一些小建议（仅供参考），大家可以自行实践，看看能否通过这些操作提升模型的性能。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对年龄（Age）字段进行离散化（分箱），将年龄特别小的（儿童）单独处理（如增加一个布尔字段）。&lt;/li&gt;
&lt;li&gt;对客舱号（Cabin）字段进行更细致的处理，提取前缀字母和后续数字作为特征，而不是简单的二值化。&lt;/li&gt;
&lt;li&gt;对比较重要的两个特征舱位等级（Pclass）和性别（Sex）进行特征组合。&lt;/li&gt;
&lt;li&gt;对名字（Name）中有 Mrs 且父母小孩登船人数大于1的（可能是一位母亲）单独处理（如增加一个布尔字段）。&lt;/li&gt;
&lt;li&gt;删除掉登船港口（Embarked）字段。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;模型部署&lt;/h4&gt;
&lt;p&gt;我们可以将训练好的模型部署到工程化项目中，首先需要对模型进行序列化处理，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import joblib

joblib.dump(model, &apos;model.pkl&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：此处我们默认上面代码中的&lt;code&gt;model&lt;/code&gt;对象是之前通过 XGBoost 训练的模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;joblib&lt;/code&gt;模块的&lt;code&gt;dump&lt;/code&gt;函数对模型进行了 Pickle 序列化（Python私有序列化协议），在需要使用模型的地方，我们可以通过&lt;code&gt;load&lt;/code&gt;函数实现反序列化，这样就可以将模型加载到应用程序中并完成预测，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import joblib

model = joblib.load(&apos;model.pkl&apos;)
model.predict(X_test)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：此处我们默认上面代码中的&lt;code&gt;X_test&lt;/code&gt;已经处理为 XGBoost 中&lt;code&gt;DMatrix&lt;/code&gt;对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果我们希望将模型部署成 Web 服务，可以利用 Flask、FastAPI 这样的 Web 框架来创建 API 接口。下面，我们以 Flask 框架为例，用简单的代码为大家演示这个过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask
from flask import jsonify
from flask import request

import joblib
import pandas as pd
import xgboost as xgb

app = Flask(__name__)


@app.route(&apos;/predict&apos;, methods=[&apos;POST&apos;])
def predict():
    query_df = pd.DataFrame(request.json)
    model = joblib.load(&apos;model.pkl&apos;)
    y_pred = (model.predict(xgb.DMatrix(query_df)) &amp;gt; 0.5).tolist()
    return jsonify({&apos;message&apos;: &apos;OK&apos;, &apos;result&apos;: y_pred})


if __name__ == &apos;__main__&apos;:
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：作为工程化的项目，肯定不能在请求到来时才加载模型，因为这样会严重影响 Web 服务的性能。可以在项目启动之后的适当时机提前加载需要的模型，与此同时还要根据实际需求做好模型资源占用的监控。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;运行上面的代码，默认会在本机 5000 端口运行一个 Web 服务器，下面我们通过 API 测试工具模拟发送一个 HTTP 请求给服务器，看看我们的模型能否运转起来给出预测的结果。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day81-90/d9730d6b73.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;除了把模型部署成 Web 服务之外，我们还可以将模型部署到定时任务中，通过时间自动激发对模型的调用。此外，一些边缘设备支持我们将训练好的模型以某种特殊的格式嵌入进去，有兴趣的读者可以自行探索。&lt;/p&gt;
</content:encoded></item><item><title>Day66-80 教程全文（15 篇合并）</title><link>https://www.klucen.cn/posts/python/day66-80-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day66-80-full/</guid><pubDate>Sun, 14 Dec 2025 00:16:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 15 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：数据分析：NumPy / pandas / 可视化
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#66-%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E6%A6%82%E8%BF%B0&quot;&gt;66 数据分析概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#67-%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87&quot;&gt;67 环境准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#68-numpy%E7%9A%84%E5%BA%94%E7%94%A8-1&quot;&gt;68 NumPy的应用-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#69-numpy%E7%9A%84%E5%BA%94%E7%94%A8-2&quot;&gt;69 NumPy的应用-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#70-numpy%E7%9A%84%E5%BA%94%E7%94%A8-3&quot;&gt;70 NumPy的应用-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#71-numpy%E7%9A%84%E5%BA%94%E7%94%A8-4&quot;&gt;71 NumPy的应用-4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#72-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-1&quot;&gt;72 深入浅出pandas-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#73-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-2&quot;&gt;73 深入浅出pandas-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#74-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-3&quot;&gt;74 深入浅出pandas-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#75-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-4&quot;&gt;75 深入浅出pandas-4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#76-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-5&quot;&gt;76 深入浅出pandas-5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#77-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BApandas-6&quot;&gt;77 深入浅出pandas-6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#78-%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96-1&quot;&gt;78 数据可视化-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#79-%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96-2&quot;&gt;79 数据可视化-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#80-%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96-3&quot;&gt;80 数据可视化-3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;66 数据分析概述&lt;/h2&gt;
&lt;p&gt;当今世界，各行各业对信息技术的依赖程度在不断加深，每天都会有大量的数据产生，我们常常会感到数据越来越多，但是要从中发现有价值的信息却越来越难。这里所说的信息，可以理解为对数据集处理之后的结果，是从数据集中提炼出的可用于支撑和指导决策的东西，而&lt;strong&gt;从原始数据中抽取出有价值的信息&lt;/strong&gt;的这个过程我们就称之为&lt;strong&gt;数据分析&lt;/strong&gt;，它是数据科学的重要组成部分。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b01c92a1c8.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;定义1&lt;/strong&gt;：数据分析是有针对性的收集、加工、整理数据并采用统计、挖掘等技术对数据进行探索、分析、呈现和解释的科学。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定义2&lt;/strong&gt;：数据分析是通过收集、整理和分析数据，从中提取有价值的信息和洞察，以支持决策和优化过程的活动。（GPT-4o）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定义3&lt;/strong&gt;：数据分析是通过系统性的收集、整理、处理、检验和解释数据，从中提取有价值的信息、形成结论并支持决策的过程，其核心是利用统计、算法和逻辑方法揭示数据背后的规律、趋势或关联。（DeepSeek）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于想从事数据分析工作的人来说，需要掌握两个部分的技能，一是“数据思维”，二是“分析工具”，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day66-80/27c6e2cfcd.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中，分析工具部分其实是比较容易掌握的，像 SQL 或 Python 这样的编程语言，只要经过系统的学习和适量的练习，大部分人都是可以驾驭的；像 Power BI、Tableau 这样的商业智能工具，更是让我们通过“拖拉拽”操作就能完成数据的可视化并在此基础上产生商业洞察，上手难度会更低。相反，数据思维部分的内容对大多数新手来说是不太容易驾驭的，例如“统计思维”，很多人在读书的时候都学习过“概率论和统计学”这样的课程，但是当面对实际的业务场景时，却很难将这些知识映射到业务场景来解决现实的问题。此外，如果没有掌握基本的分析方法、没有理解常用的分析模型，没有相关业务知识的积累，即便我们拿到再多有用的数据，也会感觉无从下手，更不用说产生业务洞察发现商业价值了。所以，数据思维这个部分，除了系统的学习相关知识技能，还需要不断的在实际业务场景中积累和沉淀。&lt;/p&gt;
&lt;h4&gt;数据分析师的职责&lt;/h4&gt;
&lt;p&gt;HR在发布招聘需求时，通常将数据工程、数据分析、数据挖掘等岗位都统称为数据分析岗位，但是根据工作性质的不同，又可以分为偏工程的&lt;strong&gt;数据治理方向&lt;/strong&gt;、偏业务的&lt;strong&gt;商业分析方向&lt;/strong&gt;、偏算法的&lt;strong&gt;数据挖掘方向&lt;/strong&gt;、偏应用的&lt;strong&gt;数据开发方向&lt;/strong&gt;、偏产品的&lt;strong&gt;数据产品经理&lt;/strong&gt;。我们通常所说的数据分析师主要是指&lt;strong&gt;业务数据分析师&lt;/strong&gt;，很多数据分析师的职业生涯都是从这个岗位开始的，而且这个岗位也是招聘数量最多的岗位。有些公司会将业务数据分析师归属到具体的业务部门（市场、运营、产品等），有些公司有专门的数据部门（数据分析团队或数据科学团队），还有些公司数据分析师会直接服务高层决策，属于企业战略部门。正因如此，你在招聘网站上看到的把数据分析师称为&lt;strong&gt;数据运营&lt;/strong&gt;、&lt;strong&gt;商业分析师&lt;/strong&gt;、&lt;strong&gt;BI工程师&lt;/strong&gt;就不会感到奇怪了。通常，我们在招聘网站看到的对业务数据分析师岗位职责（JD）的描述如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;负责相关报表的输出。&lt;/li&gt;
&lt;li&gt;建立和优化指标体系。&lt;/li&gt;
&lt;li&gt;监控数据波动和异常，找出问题。&lt;/li&gt;
&lt;li&gt;优化和驱动业务，推动数字化运营。&lt;/li&gt;
&lt;li&gt;找出潜在的市场和产品的上升空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根据上面的描述，作为业务数据分析师，我们的工作不是给出一个简单浅显的结论，而是结合公司的业务，完成&lt;strong&gt;监控数据&lt;/strong&gt;、&lt;strong&gt;揪出异常&lt;/strong&gt;、&lt;strong&gt;找到原因&lt;/strong&gt;、&lt;strong&gt;探索趋势&lt;/strong&gt;等工作。不管你是用 Python 语言、Excel、Tableau、SPSS或其他的商业智能工具，工具只是达成目标的手段，&lt;strong&gt;数据思维是核心技能&lt;/strong&gt;，从实际业务问题出发到最终&lt;strong&gt;发现数据中的商业价值&lt;/strong&gt;是终极目标。数据分析师在很多公司只是一个基础岗位，精于业务的数据分析师可以向&lt;strong&gt;数据分析经理&lt;/strong&gt;或&lt;strong&gt;数据运营总监&lt;/strong&gt;等管理岗位发展；对于熟悉机器学习算法的数据分析师来说，可以向&lt;strong&gt;数据挖掘工程师&lt;/strong&gt;或&lt;strong&gt;算法专家&lt;/strong&gt;方向发展，这些岗位除了需要相应的数学和统计学知识，在编程能力方面也比数据分析师有更高的要求，可能还需要有大数据存储和处理的相关经验。&lt;/p&gt;
&lt;p&gt;这里顺便说一下其他几个方向，数据治理岗位主要是帮助公司建设数据仓库或数据湖，实现数据从业务系统、埋点系统、日志系统到数据仓库或数据湖的转移，为后续的数据分析和挖掘提供基础设施。数据治理岗位对 SQL 和 HiveSQL 有着较高的要求，需要熟练的使用 ETL 工具，此外还需要对 Hadoop 生态圈有较好的认知。作为数据产品经理，除了传统产品经理的技能栈之外，也需要较强的技术能力，例如要了解常用的推荐算法、机器学习模型，能够为算法的改进提供依据，能够制定相关埋点的规范和口径，虽然不需要精通各种算法，但是要站在产品的角度去考虑数据模型、指标、算法等的落地。&lt;/p&gt;
&lt;h4&gt;数据分析师的技能栈&lt;/h4&gt;
&lt;p&gt;数据分析师的技能栈也包括硬技能和软技能，以下是我对这个职位的理解，仅供参考。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算机科学（数据分析工具、编程语言、数据库）&lt;/li&gt;
&lt;li&gt;数学和统计学（数据思维、统计思维）&lt;/li&gt;
&lt;li&gt;人工智能（机器学习和深度学习算法）&lt;/li&gt;
&lt;li&gt;业务理解能力（沟通、表达、经验）&lt;/li&gt;
&lt;li&gt;总结和表述能力（总结、汇报、商业 PPT）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，对于一个新手来说，不可能用短时间掌握整个技能栈的内容，但是随着这份工作的深入，上面提到的东西多多少少都会涉猎到，大家可以根据实际工作的需求去深耕其中的某个或某些技能。&lt;/p&gt;
&lt;h4&gt;数据分析通用流程&lt;/h4&gt;
&lt;p&gt;我们提到数据分析这个词很多时候可能指的都是&lt;strong&gt;狭义的数据分析&lt;/strong&gt;，这类数据分析主要目标就是生成可视化报表并通过这些报表来洞察业务中的问题，这类工作一般都是具有滞后性的。&lt;strong&gt;广义的数据分析&lt;/strong&gt;还包含了数据挖掘的部分，不仅要通过数据实现对业务的监控和分析，还要利用机器学习算法，找出隐藏在数据背后的知识，并利用这些知识为将来的决策提供支撑，具备一定的前瞻性。&lt;/p&gt;
&lt;p&gt;基本的数据分析工作一般包含以下几个方面的内容，当然因为行业和工作内容的不同会略有差异。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定目标（输入）：理解业务，确定指标口径&lt;/li&gt;
&lt;li&gt;获取数据：数据仓库、电子表格、三方接口、网络爬虫、开放数据集等&lt;/li&gt;
&lt;li&gt;清洗数据：缺失值、重复值、异常值的处理以及其他预处理（格式化、离散化、二值化等）&lt;/li&gt;
&lt;li&gt;数据透视：排序、统计、分组聚合、交叉表、透视表等&lt;/li&gt;
&lt;li&gt;数据呈现（输出）：数据可视化，发布工作成果（数据分析报告）&lt;/li&gt;
&lt;li&gt;分析洞察（后续）：解释数据的变化，提出对应的方案&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;深入的数据挖掘工作通常包含以下几个方面的内容，当然因为行业和工作内容的不同会略有差异。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定目标（输入）：理解业务，明确挖掘目标&lt;/li&gt;
&lt;li&gt;数据准备：数据采集、数据描述、数据探索、质量判定等&lt;/li&gt;
&lt;li&gt;数据加工：提取数据、清洗数据、数据变换、特殊编码、降维、特征选择等&lt;/li&gt;
&lt;li&gt;数据建模：模型比较、模型选择、算法应用&lt;/li&gt;
&lt;li&gt;模型评估：交叉检验、参数调优、结果评价&lt;/li&gt;
&lt;li&gt;模型部署（输出）：模型落地、业务改进、运营监控、报告撰写&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据分析相关库&lt;/h4&gt;
&lt;p&gt;使用 Python 从事数据分析相关的工作是一个非常棒的选择，首先 Python 语言非常容易上手，而且整个 Python 生态圈中，有非常多成熟的用于数据科学的软件包和工具库。不同于其他的数据科学编程语言（如：Julia、R等），Python 除了可以用于数据科学，还能做很多其他的事情。&lt;/p&gt;
&lt;h5&gt;经典的三大神器&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://numpy.org/&quot;&gt;NumPy&lt;/a&gt;：支持常见的数组和矩阵操作，通过&lt;code&gt;ndarray&lt;/code&gt;类实现了对多维数组的封装，提供了操作这些数组的方法和函数。由于 NumPy 内置了并行运算功能，当使用多核 CPU 时，NumPy 会自动做并行计算。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pandas.pydata.org/&quot;&gt;Pandas&lt;/a&gt;：pandas 的核心是其特有的数据结构&lt;code&gt;DataFrame&lt;/code&gt;和&lt;code&gt;Series&lt;/code&gt;，这使得 pandas 可以处理包含不同类型数据的表格和时间序列，这一点是 NumPy 的&lt;code&gt;ndarray&lt;/code&gt;做不到的。使用 pandas，可以轻松顺利的加载各种形式的数据，然后对数据进行切片、切块、重塑、清洗、聚合、呈现等操作。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://matplotlib.org/&quot;&gt;Matplotlib&lt;/a&gt;：matplotlib 是一个包含各种绘图模块的库，能够根据我们提供的数据创建高质量的图表。此外，matplotlib 还提供了 pylab 模块，这个模块包含了很多像 &lt;a href=&quot;https://www.mathworks.com/products/matlab.html&quot;&gt;MATLAB&lt;/a&gt; 一样的绘图组件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;其他相关库&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://scipy.org/&quot;&gt;SciPy&lt;/a&gt;：完善了 NumPy 的功能，封装了大量科学计算的算法，包括线性代数、统计检验、稀疏矩阵、信号和图像处理、最优化问题、快速傅里叶变换等。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pola.rs/&quot;&gt;Polars&lt;/a&gt;：一个高性能的数据分析库，旨在提供比 pandas 更快的数据操作。它支持大规模数据处理，并能够利用多核 CPU 来加速计算，在处理大规模数据集时可以用来替代 pandas。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seaborn.pydata.org/&quot;&gt;Seaborn&lt;/a&gt;：seaborn 是基于 matplotlib 的图形可视化工具，直接使用 matplotlib 虽然可以定制出漂亮的统计图表，但是总体来说还不够简单方便，seaborn 相当于是对 matplotlib 做了封装，让用户能够以更简洁有效的方式做出各种有吸引力的统计图表。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scikit-learn.org/&quot;&gt;Scikit-learn&lt;/a&gt;：scikit-learn 最初是 SciPy 的一部分，提供了大量机器学习可能用到的工具，包括数据预处理、监督学习（分类、回归）、无监督学习（聚类）、模式选择、交叉检验等。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.statsmodels.org/stable/index.html&quot;&gt;Statsmodels&lt;/a&gt;：包含了经典统计学和计量经济学算法的库，帮助帮助用户完成数据探索、回归分析、假设检验等任务。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spark.apache.org/&quot;&gt;PySpark&lt;/a&gt;：Apache Spark（大数据处理引擎）的 Python 版本，用于大规模数据处理和分布式计算，能够在分布式环境中高效地进行数据清洗、转化和分析。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tensorflow.org/&quot;&gt;Tensorflow&lt;/a&gt;：TensorFlow 是一个开源的深度学习框架，由 Google 开发，主要面向深度学习任务，常用于构建和训练机器学习模型（尤其是复杂的神经网络模型）。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://keras.io/&quot;&gt;Keras&lt;/a&gt;：Keras 是一个高层次的神经网络 API，主要用于构建和训练深度学习模型。Keras 适合深度学习初学者和研究人员，因为它让构建和训练神经网络变得更加简单。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pytorch.org/&quot;&gt;PyTorch&lt;/a&gt;：PyTorch 是一个开源的深度学习框架，由 Facebook 开发，广泛用于研究和生产环境。PyTorch 是深度学习研究中的热门框架，在计算机视觉、自然语言处理等领域得到了广泛应用。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nltk.org/&quot;&gt;NLTK&lt;/a&gt; / &lt;a href=&quot;https://spacy.io/&quot;&gt;SpaCy&lt;/a&gt;：自然语言处理（NLP）库。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;67 环境准备&lt;/h2&gt;
&lt;p&gt;如果希望快速开始使用 Python 处理数据科学相关的工作，建议大家直接安装 Anaconda，然后使用 Anaconda 中集成的 Notebook 或 JupyterLab 工具来编写代码。因为对于新手来说，先安装官方的 Python 解释器，再逐个安装工作中会使用到的三方库文件会比较麻烦，尤其是在 Windows 环境下，经常会因为构建工具或 DLL 文件的缺失导致安装失败，而一般新手也很难根据错误提示信息采取正确的解决措施，容易产生严重的挫败感。如果计算机上已经有 Python 解释器环境了，也可以直接使用 Python 的包管理工具 pip 安装 Jupyter，再根据实际工作的需要安装三方库，这种方式适合有一定经验的用户。&lt;/p&gt;
&lt;h4&gt;安装和使用 Anaconda&lt;/h4&gt;
&lt;p&gt;对于个人用户来说，可以从 Anaconda 的&lt;a href=&quot;https://www.anaconda.com/&quot;&gt;官方网站&lt;/a&gt;下载它的“个人版（Individual Edition）”安装程序，安装完成后，你的计算机上不仅拥有了 Python 环境和 Spyder（类似于 PyCharm 的集成开发工具），还拥有了与数据科学工作相关的近200个工具包，包括我们上面提到 Python 数据分析三大神器。除此之外，Anaconda 还提供了一个名为 conda 的包管理工具，通过这个工具不仅可以管理 Python 的工具包，还可以用于创建运行 Python 程序的虚拟环境。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/6eb003c035.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如上图所示，可以通过 Anaconda 官网提供的下载链接选择适合自己操作系统的安装程序，建议大家选择图形化的安装程序，下载完成后双击安装程序开始安装。安装过程基本使用默认设置即可，完成安装后，macOS 用户可以在“应用程序”或“启动台”中找到名为“Anaconda-Navigator”的应用程序，运行该程序可以看到如下所示的界面，我们可以在这里选择需要执行的操作。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/411abee58b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对于 Windows 用户，建议按照安装向导的提示和推荐的选项来安装 Anaconda（除了安装路径，基本也没有什么需要选择的），安装完成后可以在“开始菜单”中找到“Anaconda3”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：可以选择 Miniconda 作为 Anaconda 的替代品，Miniconda 只会安装 Python 解释器环境和一些必要的工具，其他的三方库由用户自行选择安装。&lt;strong&gt;其实我个人并不喜欢 Anaconda，因为它是给小白用户使用的，我们有了 Python 环境以后完全可以按照自己的意愿来安装需要的三方库&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;conda命令&lt;/h5&gt;
&lt;p&gt;对于非新手用户，如果希望使用 conda 工具来管理依赖项或者创建项目的虚拟环境，可以在终端或命令行提示符中使用 conda 命令。Windows 用户可以在“开始菜单”中找到“Anaconda3”，然后点击“Anaconda Prompt”或“Anaconda PowerShell”来启动支持 conda 的命令行提示符。新手用户如果想创建新的虚拟环境或管理三方库（依赖项），建议直接使用“Anaconda-Navigator”中的“Environments”，通过可视化的方式对虚拟环境和依赖项进行管理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;版本和帮助信息。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看版本：&lt;code&gt;conda -V&lt;/code&gt;或&lt;code&gt;conda --version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;获取帮助：&lt;code&gt;conda -h&lt;/code&gt;或&lt;code&gt;conda --help&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;相关信息：&lt;code&gt;conda list&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟环境相关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示所有虚拟环境：&lt;code&gt;conda env list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建虚拟环境：&lt;code&gt;conda create --name venv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;指定 Python 版本创建虚拟环境：&lt;code&gt;conda create --name venv python=3.7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;指定 Python 版本创建虚拟环境并安装指定依赖项：&lt;code&gt;conda create --name venv python=3.7 numpy pandas&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过克隆现有虚拟环境的方式创建虚拟环境：&lt;code&gt;conda create --name venv2 --clone venv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;分享虚拟环境并重定向到指定的文件中：&lt;code&gt;conda env export &amp;gt; environment.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过分享的虚拟环境文件创建虚拟环境：&lt;code&gt;conda env create -f environment.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;激活虚拟环境：&lt;code&gt;conda activate venv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;退出虚拟环境：&lt;code&gt;conda deactivate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除虚拟环境：&lt;code&gt;conda remove --name venv --all&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的命令中，&lt;code&gt;venv&lt;/code&gt;和&lt;code&gt;venv2&lt;/code&gt;是虚拟环境文件夹的名字，可以将其替换为自己喜欢的名字，但是&lt;strong&gt;强烈建议&lt;/strong&gt;使用英文且不要有特殊字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;包（三方库或工具）管理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看已经安装的包：&lt;code&gt;conda list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;搜索指定的包：&lt;code&gt;conda search matplotlib&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装指定的包：&lt;code&gt;conda install matplotlib&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;更新指定的包：&lt;code&gt;conda update matplotlib&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;移除指定的包：&lt;code&gt;conda remove matplotlib&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在搜索、安装和更新软件包时，默认会连接到官方网站进行操作，如果觉得速度不给力，可以将默认的官方网站替换为国内的镜像网站，推荐使用清华大学的开源镜像网站。将默认源更换为国内镜像的命令是：&lt;code&gt;conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/&lt;/code&gt;和&lt;code&gt;conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main&lt;/code&gt;。如果需要换回默认源，可以使用命令&lt;code&gt;conda config --remove-key channels&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;安装和使用JupyterLab&lt;/h4&gt;
&lt;h5&gt;安装和启动&lt;/h5&gt;
&lt;p&gt;如果已经安装了 Anaconda，可以按照上面所说的方式在“Anaconda-Navigator”中直接启动 Notebook 或 JupyterLab。按照官方的说法，JupyterLab 是下一代的 Notebook，提供了更友好的界面和更强大的功能，我们也推荐大家使用 JupyterLab。Windows 用户也可以在开始菜单中打开“Anaconda Prompt”或“Anaconda PowerShell”，由于已经激活了 Anaconda 默认的虚拟环境，只需要输入&lt;code&gt;jupyter lab&lt;/code&gt;命令来启动JupyterLab。macOS 系统在安装 Anaconda以后，每次打开终端时会自动激活 Anaconda 默认的虚拟环境，也是通过输入&lt;code&gt;jupyter lab&lt;/code&gt;命令就可以启动JupyterLab。&lt;/p&gt;
&lt;p&gt;对于安装了 Python 环境但是没有安装 Anaconda 的用户，可以用 Python 的包管理工具&lt;code&gt;pip&lt;/code&gt;来安装 JupyterLab，安装成功后在终端或命令行提示符中执行&lt;code&gt;jupyter lab&lt;/code&gt;命令来启动 JupyterLab，如下所示。&lt;/p&gt;
&lt;p&gt;安装 JupyterLab：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install jupyterlab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 Python 数据分析三大神器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install numpy pandas matplotlib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 JupyterLab：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jupyter lab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JupyterLab 是基于网页的用于交互计算的应用程序，可以用于代码开发、文档撰写、代码运行和结果展示。简单的说，你可以在网页中直接&lt;strong&gt;编写代码&lt;/strong&gt;和&lt;strong&gt;运行代码&lt;/strong&gt;，代码的运行结果也会直接在代码块下方进行展示。如在编写代码的过程中需要编写说明文档，可在同一个页面中使用 Markdown 格式进行编写，而且可以直接看到渲染后的效果。此外，Notebook 的设计初衷是提供一个能够支持多种编程语言的工作环境，目前它能够支持超过40种编程语言，包括 Python、R、Julia、Scala 等。&lt;/p&gt;
&lt;p&gt;首先，我们可以创建一个用于书写 Python 代码的 Notebook，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/9af1c54027.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们就可以编写代码、撰写文档和运行程序啦，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/ba4af34dbd.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;使用技巧&lt;/h5&gt;
&lt;p&gt;如果使用 Python 做工程化的项目开发，PyCharm 肯定是最好的选择，它提供了一个集成开发环境应该具有的所有功能，尤其是智能提示、代码补全、自动纠错这类功能会让开发人员感到非常舒服。如果使用 Python 做数据科学相关的工作，JupyterLab 并不比 PyCharm 逊色，在数据和图表展示方面 JupyterLab 更加优秀。为此，JetBrains 公司还专门开发了一个对标 JupyterLab 的新工具 DataSpell，有兴趣的读者可以自行了解。下面我们为大家介绍一些 JupyterLab 的使用技巧，希望能够帮助大家提升工作效率。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;自动补全。在使用 JupyterLab 编写代码时，按&lt;code&gt;Tab&lt;/code&gt;键会获得代码提示和补全功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获得帮助。如果希望了解一个对象（如变量、类、函数等）的相关信息或使用方式，可以在对象后面使用&lt;code&gt;?&lt;/code&gt;并运行代码， 窗口下方会显示出对应的信息，帮助我们了解该对象，如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a14e874d9f.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搜索命名。如果只记得一个类或一个函数名字的一部分，可以使用通配符&lt;code&gt;*&lt;/code&gt;并配合&lt;code&gt;?&lt;/code&gt;进行搜索，如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/5e60dc5950.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用命令。可以在 JupyterLab 中使用&lt;code&gt;!&lt;/code&gt;后面跟系统命令的方式来执行系统命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;魔法指令。JupyterLab 中有很多非常有趣且有用的魔法指令，例如可以使用&lt;code&gt;%timeit&lt;/code&gt;测试语句的执行时间，可以使用&lt;code&gt;%pwd&lt;/code&gt;查看当前工作目录等。如果想查看所有的魔法指令，可以使用&lt;code&gt;%lsmagic&lt;/code&gt;，如果了解魔法指令的用法，可以使用&lt;code&gt;%magic&lt;/code&gt;来查看，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/815fa3aafb.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;常用的魔法指令有：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;魔法指令&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%pwd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看当前工作目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出当前或指定文件夹下的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%cat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看指定文件的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%hist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看输入历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%matplotlib inline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置在页面中嵌入matplotlib输出的统计图表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%config Inlinebackend.figure_format=&apos;svg&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置统计图表使用SVG格式（矢量图）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行指定的程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加载指定的文件到单元格中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%quickref&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示IPython的快速参考&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%timeit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多次运行代码并统计代码执行时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%prun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用&lt;code&gt;cProfile.run&lt;/code&gt;运行代码并显示分析器的输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%who&lt;/code&gt; / &lt;code&gt;%whos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示命名空间中的变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%xdel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除一个对象并清理所有对它的引用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快捷键。JupyterLab 中的很多操作可以通过快捷键来实现，使用快捷键可以提升工作效率。JupyterLab 的快捷键可以分为命令模式下的快捷键和编辑模式下的快捷键，所谓编辑模式就是处于输入代码或撰写文档状态的模式，在编辑模式下按&lt;code&gt;Esc&lt;/code&gt;可以回到命令模式，在命令模式下按&lt;code&gt;Enter&lt;/code&gt;可以进入编辑模式。&lt;/p&gt;
&lt;p&gt;命令模式下的快捷键：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Alt&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格并在下面插入新的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格并选中下方的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;j&lt;/code&gt; / &lt;code&gt;k&lt;/code&gt;、&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;j&lt;/code&gt; / &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选中下方/上方单元格、连续选中下方/上方单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt; / &lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在下方/上方插入新的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt; / &lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;复制单元格 / 剪切单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt; / &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在下方/上方粘贴单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dd&lt;/code&gt; / &lt;code&gt;z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除单元格 / 恢复删除的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示或隐藏当前/所有单元格行号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Space&lt;/code&gt; / &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向下/向上滚动页面&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;编辑模式下的快捷键：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获得提示信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;]&lt;/code&gt;/ &lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;[&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;增加/减少缩进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Alt&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格并在下面插入新的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格并选中下方的单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行当前单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Left&lt;/code&gt; / &lt;code&gt;Right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;光标移到行首/行尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Up&lt;/code&gt; / &lt;code&gt;Down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;光标移动代码开头/结尾处&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Up&lt;/code&gt; / &lt;code&gt;Down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;光标上移/下移一行或移到上/下一个单元格&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：对于 macOS 系统可以将&lt;code&gt;Alt&lt;/code&gt;键替换成&lt;code&gt;Option&lt;/code&gt;键，将&lt;code&gt;Ctrl&lt;/code&gt;键替换成&lt;code&gt;Command&lt;/code&gt;键。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;68 NumPy的应用-1&lt;/h2&gt;
&lt;p&gt;Numpy 是一个开源的 Python 科学计算库，&lt;strong&gt;用于快速处理任意维度的数组&lt;/strong&gt;。Numpy &lt;strong&gt;支持常见的数组和矩阵操作&lt;/strong&gt;，对于同样的数值计算任务，使用 NumPy 不仅代码要简洁的多，而且 NumPy 在性能上也远远优于原生 Python，至少是一到两个数量级的差距，而且数据量越大，NumPy 的优势就越明显。&lt;/p&gt;
&lt;p&gt;NumPy 最为核心的数据类型是&lt;code&gt;ndarray&lt;/code&gt;，使用&lt;code&gt;ndarray&lt;/code&gt;可以处理一维、二维和多维数组，该对象相当于是一个快速而灵活的大数据容器。NumPy 底层代码使用 C 语言编写，解决了 GIL 的限制，&lt;code&gt;ndarray&lt;/code&gt;在存取数据的时候，数据与数据的地址都是连续的，这确保了可以进行高效率的批量操作，性能上远远优于 Python 中的&lt;code&gt;list&lt;/code&gt;；另一方面&lt;code&gt;ndarray&lt;/code&gt;对象提供了更多的方法来处理数据，尤其获取数据统计特征的方法，这些方法也是 Python 原生的&lt;code&gt;list&lt;/code&gt;没有的。&lt;/p&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动 JupyterLab&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jupyter lab
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：在启动 JupyterLab 之前，建议先安装好数据分析相关依赖项，包括之前提到的三大神器以及相关依赖项。如果使用 Anaconda，则无需单独安装，可以通过 Anaconda 的 Navigator 来启动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果已经启动了 JupyterLab 但尚未安装相关依赖库，例如尚未安装&lt;code&gt;numpy&lt;/code&gt;，可以在单元格中输入&lt;code&gt;%pip install numpy&lt;/code&gt;并运行该单元格来安装 NumPy。当然，我们也可以在单元格中输入&lt;code&gt;%pip install numpy pandas matplotlib&lt;/code&gt;把 Python 数据分析三个核心的三方库都安装上。注意上面的代码，我们不仅导入了 NumPy，还将 pandas 和 matplotlib 库一并导入了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;创建数组对象&lt;/h4&gt;
&lt;p&gt;创建&lt;code&gt;ndarray&lt;/code&gt;对象有很多种方法，下面我们介绍一些常用的方法。&lt;/p&gt;
&lt;p&gt;方法一：使用&lt;code&gt;array&lt;/code&gt;函数，通过&lt;code&gt;list&lt;/code&gt;创建数组对象&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array1 = np.array([1, 2, 3, 4, 5])
array1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 3, 4, 5])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2 = np.array([[1, 2, 3], [4, 5, 6]])
array2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 2, 3],
       [4, 5, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法二：使用&lt;code&gt;arange&lt;/code&gt;函数，指定取值范围和跨度创建数组对象&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array3 = np.arange(0, 20, 2)
array3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法三：使用&lt;code&gt;linspace&lt;/code&gt;函数，用指定范围和元素个数创建数组对象，生成等差数列&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array4 = np.linspace(-1, 1, 11)
array4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([-1. , -0.8, -0.6, -0.4, -0.2,  0. ,  0.2,  0.4,  0.6,  0.8,  1. ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法四：使用&lt;code&gt;logspace&lt;/code&gt;函数，生成等比数列&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array5 = np.logspace(1, 10, num=10, base=2)
array5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：等比数列的起始值是 $\small{2^1}$ ，等比数列的终止值是 $\small{2^{10}}$ ，&lt;code&gt;num&lt;/code&gt;是元素的个数，&lt;code&gt;base&lt;/code&gt;就是底数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([   2.,    4.,    8.,   16.,   32.,   64.,  128.,  256.,  512., 1024.])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法五：通过&lt;code&gt;fromstring&lt;/code&gt;函数从字符串提取数据创建数组对象&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array6 = np.fromstring(&apos;1, 2, 3, 4, 5&apos;, sep=&apos;,&apos;, dtype=&apos;i8&apos;)
array6 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 3, 4, 5])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法六：通过&lt;code&gt;fromiter&lt;/code&gt;函数从生成器（迭代器）中获取数据创建数组对象&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(how_many):
    a, b = 0, 1
    for _ in range(how_many):
        a, b = b, a + b
        yield a


gen = fib(20)
array7 = np.fromiter(gen, dtype=&apos;i8&apos;)
array7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([   1,    1,    2,    3,    5,    8,   13,   21,   34,   55,   89,
        144,  233,  377,  610,  987, 1597, 2584, 4181, 6765])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法七：使用&lt;code&gt;numpy.random&lt;/code&gt;模块的函数生成随机数创建数组对象&lt;/p&gt;
&lt;p&gt;产生 10 个 $\small{[0, 1)}$ 范围的随机小数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array8 = np.random.rand(10)
array8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([0.45556132, 0.67871326, 0.4552213 , 0.96671509, 0.44086463,
       0.72650875, 0.79877188, 0.12153022, 0.24762739, 0.6669852 ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;产生 10 个 $\small{[1, 100)}$ 范围的随机整数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array9 = np.random.randint(1, 100, 10)
array9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([29, 97, 87, 47, 39, 19, 71, 32, 79, 34])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;产生 20 个 $\small{\mu=50}$ ， $\small{\sigma=10}$ 的正态分布随机数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array10 = np.random.normal(50, 10, 20)
array10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([55.04155586, 46.43510797, 20.28371158, 62.67884053, 61.23185964,
       38.22682148, 53.17126151, 43.54741592, 36.11268017, 40.94086676,
       63.27911699, 46.92688903, 37.1593374 , 67.06525656, 67.47269463,
       23.37925889, 31.45312239, 48.34532466, 55.09180924, 47.95702787])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;产生 $\small{[0, 1)}$ 范围的随机小数构成的 3 行 4 列的二维数组，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array11 = np.random.rand(3, 4)
array11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[0.54017809, 0.46797771, 0.78291445, 0.79501326],
       [0.93973783, 0.21434806, 0.03592874, 0.88838892],
       [0.84130479, 0.3566601 , 0.99935473, 0.26353598]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;产生 $\small{[1, 100)}$ 范围的随机整数构成的三维数组，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array12 = np.random.randint(1, 100, (3, 4, 5))
array12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[[94, 26, 49, 24, 43],
        [27, 27, 33, 98, 33],
        [13, 73,  6,  1, 77],
        [54, 32, 51, 86, 59]],

       [[62, 75, 62, 29, 87],
        [90, 26,  6, 79, 41],
        [31, 15, 32, 56, 64],
        [37, 84, 61, 71, 71]],

       [[45, 24, 78, 77, 41],
        [75, 37,  4, 74, 93],
        [ 1, 36, 36, 60, 43],
        [23, 84, 44, 89, 79]]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法八：创建全0、全1或指定元素的数组&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;zeros&lt;/code&gt;函数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array13 = np.zeros((3, 4))
array13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;ones&lt;/code&gt;函数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array14 = np.ones((3, 4))
array14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1., 1., 1., 1.],
       [1., 1., 1., 1.],
       [1., 1., 1., 1.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;full&lt;/code&gt;函数，代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array15 = np.full((3, 4), 10)
array15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[10, 10, 10, 10],
       [10, 10, 10, 10],
       [10, 10, 10, 10]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法九：使用&lt;code&gt;eye&lt;/code&gt;函数创建单位矩阵&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.eye(4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1., 0., 0., 0.],
       [0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 0., 0., 1.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法十：读取图片获得对应的三维数组&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array16 = plt.imread(&apos;guido.jpg&apos;)
array16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[[ 36,  33,  28],
        [ 36,  33,  28],
        [ 36,  33,  28],
        ...,
        [ 32,  31,  29],
        [ 32,  31,  27],
        [ 31,  32,  26]],

       [[ 37,  34,  29],
        [ 38,  35,  30],
        [ 38,  35,  30],
        ...,
        [ 31,  30,  28],
        [ 31,  30,  26],
        [ 30,  31,  25]],

       [[ 38,  35,  30],
        [ 38,  35,  30],
        [ 38,  35,  30],
        ...,
        [ 30,  29,  27],
        [ 30,  29,  25],
        [ 29,  30,  25]],

       ...,

       [[239, 178, 123],
        [237, 176, 121],
        [235, 174, 119],
        ...,
        [ 78,  68,  56],
        [ 75,  67,  54],
        [ 73,  65,  52]],

       [[238, 177, 120],
        [236, 175, 118],
        [234, 173, 116],
        ...,
        [ 82,  70,  58],
        [ 78,  68,  56],
        [ 75,  66,  51]],

       [[238, 176, 119],
        [236, 175, 118],
        [234, 173, 116],
        ...,
        [ 84,  70,  61],
        [ 81,  69,  57],
        [ 79,  67,  53]]], dtype=uint8)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码读取了当前路径下名为&lt;code&gt;guido.jpg&lt;/code&gt; 的图片文件，计算机系统中的图片通常由若干行若干列的像素点构成，而每个像素点又是由红绿蓝三原色构成的，刚好可以用三维数组来表示。读取图片用到了&lt;code&gt;matplotlib&lt;/code&gt;库的&lt;code&gt;imread&lt;/code&gt;函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;数组对象的属性&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;size&lt;/code&gt;属性：获取数组元素个数。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array17 = np.arange(1, 100, 2)
array18 = np.random.rand(3, 4)
print(array16.size)
print(array17.size)
print(array18.size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1125000
50
12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;shape&lt;/code&gt;属性：获取数组的形状。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array16.shape)
print(array17.shape)
print(array18.shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(750, 500, 3)
(50,)
(3, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dtype&lt;/code&gt;属性：获取数组元素的数据类型。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array16.dtype)
print(array17.dtype)
print(array18.dtype)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint8
int64
float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ndarray&lt;/code&gt;对象元素的数据类型可以参考如下所示的表格。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/fc22d10e6c.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ndim&lt;/code&gt;属性：获取数组的维度。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array16.ndim)
print(array17.ndim)
print(array18.ndim)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
1
2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;itemsize&lt;/code&gt;属性：获取数组单个元素占用内存空间的字节数。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array16.itemsize)
print(array17.itemsize)
print(array18.itemsize)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
8
8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nbytes&lt;/code&gt;属性：获取数组所有元素占用内存空间的字节数。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array16.nbytes)
print(array17.nbytes)
print(array18.nbytes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1125000
400
96
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数组的索引运算&lt;/h4&gt;
&lt;p&gt;和 Python 中的列表类似，NumPy 的&lt;code&gt;ndarray&lt;/code&gt;对象可以进行索引和切片操作，通过索引可以获取或修改数组中的元素，通过切片操作可以取出数组的一部分，我们把切片操作也称为切片索引。&lt;/p&gt;
&lt;h5&gt;普通索引&lt;/h5&gt;
&lt;p&gt;类似于 Python 中&lt;code&gt;list&lt;/code&gt;类型的索引运算。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19 = np.arange(1, 10)
print(array19[0], array19[array19.size - 1])
print(array19[-array20.size], array19[-1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 9
1 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array20[2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array20[0][0])
print(array20[-1][-1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array20[1][1])
print(array20[1, 1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5
5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[1][1] = 10
array20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[ 1,  2,  3],
       [ 4, 10,  6],
       [ 7,  8,  9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[1] = [10, 11, 12]
array20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[ 1,  2,  3],
       [10, 11, 12],
       [ 7,  8,  9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;切片索引&lt;/h5&gt;
&lt;p&gt;切片索引是形如&lt;code&gt;[开始索引:结束索引:跨度]&lt;/code&gt;的语法，通过指定&lt;strong&gt;开始索引&lt;/strong&gt;（默认值无穷小）、&lt;strong&gt;结束索引&lt;/strong&gt;（默认值无穷大）和&lt;strong&gt;跨度&lt;/strong&gt;（默认值1），从数组中取出指定部分的元素并构成新的数组。因为开始索引、结束索引和步长都有默认值，所以它们都可以省略，如果不指定步长，第二个冒号也可以省略。一维数组的切片运算跟 Python 中的&lt;code&gt;list&lt;/code&gt;类型的切片非常类似，此处不再赘述，二维数组的切片可以参考下面的代码，相信非常容易理解。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[:2, 1:]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[ 2,  3],
       [11, 12]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[2, :]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[2:, :]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[7, 8, 9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[:, :2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[ 1,  2],
       [10, 11],
       [ 7,  8]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[::2, ::2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 3],
       [7, 9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[::-2, ::-2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[9, 7],
       [3, 1]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于数组的索引和切片运算，大家可以通过下面的两张图来增强印象，这两张图来自&lt;a href=&quot;https://item.jd.com/12398725.html&quot;&gt;《利用Python进行数据分析》&lt;/a&gt;一书，它是 pandas 库的作者 Wes McKinney 撰写的 Python 数据分析领域的经典教科书，有兴趣的读者可以购买和阅读原书。&lt;/p&gt;
&lt;p&gt;图1：二维数组的普通索引&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/4b89ded090.webp&quot; style=&quot;zoom:60%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;图2：二维数组的切片索引&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/51de9c7ddd.webp&quot; style=&quot;zoom:60%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;花式索引&lt;/h5&gt;
&lt;p&gt;花式索引是用保存整数的数组充当一个数组的索引，这里所说的数组可以是 NumPy 的&lt;code&gt;ndarray&lt;/code&gt;，也可以是 Python 中&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;tuple&lt;/code&gt;等可迭代类型，可以使用正向或负向索引。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[[0, 1, 1, -1, 4, -1]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 2, 9, 5, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[[0, 2]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 2, 3],
       [7, 8, 9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[[0, 2], [1, 2]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([2, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[[0, 2], 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([2, 8])
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;布尔索引&lt;/h5&gt;
&lt;p&gt;布尔索引就是通过保存布尔值的数组充当一个数组的索引，布尔值为&lt;code&gt;True&lt;/code&gt;的元素保留，布尔值为&lt;code&gt;False&lt;/code&gt;的元素不会被选中。布尔值的数组可以手动构造，也可以通过关系运算来产生。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[[True, True, False, False, True, False, False, True, True]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 5, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19 &amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([False, False, False, False, False,  True,  True,  True,  True])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~(array19 &amp;gt; 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ True,  True,  True,  True,  True, False, False, False, False])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;~&lt;/code&gt;运算符可以对布尔数组中的布尔值进行逻辑取反，也就是原来的&lt;code&gt;True&lt;/code&gt;会变成&lt;code&gt;False&lt;/code&gt;，原来的&lt;code&gt;False&lt;/code&gt;会变成&lt;code&gt;True&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[array19 &amp;gt; 5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([6, 7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19 % 2 == 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([False,  True, False,  True, False,  True, False,  True, False])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[array19 % 2 == 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([2, 4, 6, 8])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(array19 &amp;gt; 5) &amp;amp; (array19 % 2 == 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([False, False, False, False, False,  True, False,  True, False])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;&amp;amp;&lt;/code&gt;运算符可以作用于两个布尔数组，如果两个数组对应元素都是&lt;code&gt;True&lt;/code&gt;，那么运算的结果就是&lt;code&gt;True&lt;/code&gt;，否则就是&lt;code&gt;False&lt;/code&gt;，该运算符的运算规则类似于 Python 中的 &lt;code&gt;and&lt;/code&gt; 运算符，只不过作用的对象是两个布尔数组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[(array19 &amp;gt; 5) &amp;amp; (array19 % 2 == 0)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([6, 8])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array19[(array19 &amp;gt; 5) | (array19 % 2 == 0)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([2, 4, 6, 7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;|&lt;/code&gt;运算符可以作用于两个布尔数组，如果两个数组对应元素都是&lt;code&gt;False&lt;/code&gt;，那么运算的结果就是&lt;code&gt;False&lt;/code&gt;，否则就是&lt;code&gt;True&lt;/code&gt;，该运算符的运算规则类似于 Python 中的 &lt;code&gt;or&lt;/code&gt; 运算符，只不过作用的对象是两个布尔数组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array20[array21 % 2 != 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 3, 5, 7, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于索引运算需要说明的是，切片索引虽然创建了新的数组对象，但是新数组和原数组共享了数组中的数据，简单的说，无论你通过新数组对象或原数组对象修改数组中的数据，修改的其实是内存中的同一块数据。花式索引和布尔索引也会创建新的数组对象，而且新数组复制了原数组的元素，新数组和原数组并不是共享数据的关系，这一点可以查看数组对象的&lt;code&gt;base&lt;/code&gt;属性，有兴趣的读者可以自行探索。&lt;/p&gt;
&lt;h4&gt;案例：通过数组切片处理图像&lt;/h4&gt;
&lt;p&gt;学习基础知识总是比较枯燥且没有成就感的，所以我们还是来个案例为大家演示下上面学习的数组索引和切片操作到底有什么用。前面我们说到过，可以用三维数组来表示图像，那么通过图像对应的三维数组进行操作，就可以实现对图像的处理，如下所示。&lt;/p&gt;
&lt;p&gt;读入图片创建三维数组对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;guido_image = plt.imread(&apos;guido.jpg&apos;)
plt.imshow(guido_image)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/0075bf3957.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对数组的0轴进行反向切片，实现图像的垂直翻转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.imshow(guido_image[::-1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/fae4222a8b.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对数组的1轴进行反向切片，实现图像的水平翻转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.imshow(guido_image[:,::-1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d48f74a915.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;通过切片操作实现抠图，将吉多大叔的头抠出来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.imshow(guido_image[30:350, 90:300])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/8980bc2c12.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;通过切片操作实现降采样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.imshow(guido_image[::10, ::10])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a47c15b3db.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;69 NumPy的应用-2&lt;/h2&gt;
&lt;h4&gt;数组对象的方法&lt;/h4&gt;
&lt;h5&gt;获取描述统计信息&lt;/h5&gt;
&lt;p&gt;描述统计信息主要包括数据的集中趋势、离散程度和频数分析等，其中集中趋势主要看均值和中位数，离散程度可以看极值、方差、标准差等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array1 = np.random.randint(1, 100, 10)
array1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([46, 51, 15, 42, 53, 71, 20, 62,  6, 94])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;计算总和、均值和中位数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array1.sum())
print(np.sum(array1))
print(array1.mean())
print(np.mean(array1))
print(np.median(array1))
print(np.quantile(array1, 0.5))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;mean&lt;/code&gt;、&lt;code&gt;median&lt;/code&gt;和&lt;code&gt;quantile&lt;/code&gt;分别是 NumPy 中计算算术平均值、中位数和分位数的函数，其中&lt;code&gt;quantitle&lt;/code&gt;函数的第二个参数设置为0.5表示计算50%分位数，也就是中位数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;460
460
46.0
46.0
48.5
48.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;极值、全距和四分位距离。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array1.max())
print(np.amax(array1))
print(array1.min())
print(np.amin(array1))
print(np.ptp(array1))
print(np.ptp(array1))
q1, q3 = np.quantile(array1, [0.25, 0.75])
print(q3 - q1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;94
94
6
6
88
88
34.25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方差、标准差和变异系数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array1.var())
print(np.var(array1))
print(array1.std())
print(np.std(array1))
print(array1.std() / array1.mean())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;651.2
651.2
25.51862065237853
25.51862065237853
0.5547526228777941
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;绘制箱线图。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;箱线图又称为盒须图，是显示一组数据分散情况的统计图，因形状如箱子而得名。 它主要用于反映原始数据分布的特征，还可以进行多组数据分布特征的比较。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.boxplot(array1, showmeans=True)
plt.ylim([-20, 120])
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b3d27a6b92.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;值得注意的是，对于二维或更高维的数组，在获取描述统计信息时，可以通过名为&lt;code&gt;axis&lt;/code&gt;的参数指定均值、方差等运算是沿着哪一个轴来执行，&lt;code&gt;axis&lt;/code&gt;参数不同，执行的结果可能是大相径庭的，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2 = np.random.randint(60, 101, (5, 3))
array2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[72, 64, 73],
       [61, 73, 61],
       [76, 85, 77],
       [97, 88, 90],
       [63, 93, 82]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.mean()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;77.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.mean(axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([73.8, 80.6, 76.6])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.mean(axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([69.66666667, 65.        , 79.33333333, 91.66666667, 79.33333333])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.max(axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([97, 93, 90])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.max(axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([73, 73, 85, 97, 93])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看看绘制箱线图，对于二维数组每一列都会产生一个统计图形，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.boxplot(array2, showmeans=True)
plt.ylim([-20, 120])
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d79648fa21.webp&quot; style=&quot;zoom:75%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：箱线图中的小圆圈用来表示离群点，也就是大于 $\small{Q_3 + 1.5 \times IQR}$ 或小于 $\small{Q_1 - 1.5 \times IQR}$ 的值。公式中的常量 1.5 可以通过绘制箱线图的&lt;code&gt;boxplot&lt;/code&gt;函数的&lt;code&gt;whis&lt;/code&gt;参数进行修改，常用的值是 1.5 和 3，修改为 3 通常是为了标识出极度离群点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要说明的是，NumPy 的数组对象并没有提供计算几何平均值、调和平均值、去尾平均值等的方法，如果有这方面的需求，可以使用名为 scipy 的三方库，它的&lt;code&gt;stats&lt;/code&gt;模块中提供了这些函数。此外，该模块还提供了计算众数、变异系数、偏态、峰度的函数，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scipy import stats

print(np.mean(array1))                # 算术平均值
print(stats.gmean(array1))            # 几何平均值
print(stats.hmean(array1))            # 调和平均值
print(stats.tmean(array1, [10, 90]))  # 去尾平均值
print(stats.variation(array1))        # 变异系数
print(stats.skew(array1))             # 偏态系数
print(stats.kurtosis(array1))         # 峰度系数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;46.0
36.22349548825599
24.497219530825497
45.0
0.5547526228777941
0.11644192634527782
-0.7106251396024126
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;其他相关方法概述&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;all()&lt;/code&gt; / &lt;code&gt;any()&lt;/code&gt;方法：判断数组是否所有元素都是&lt;code&gt;True&lt;/code&gt; / 判断数组是否有为&lt;code&gt;True&lt;/code&gt;的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;astype()&lt;/code&gt;方法：拷贝数组，并将数组中的元素转换为指定的类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;reshape()&lt;/code&gt;方法：调整数组对象的形状。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dump()&lt;/code&gt;方法：保存数组到二进制文件中，可以通过 NumPy 中的&lt;code&gt;load()&lt;/code&gt;函数从保存的文件中加载数据创建数组。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array.dump(&apos;array1-data&apos;)
array3 = np.load(&apos;array1-data&apos;, allow_pickle=True)
array3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([46, 51, 15, 42, 53, 71, 20, 62,  6, 94])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tofile()&lt;/code&gt;方法：将数组对象写入文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array1.tofile(&apos;array.txt&apos;, sep=&apos;,&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fill()&lt;/code&gt;方法：向数组中填充指定的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;flatten()&lt;/code&gt;方法：将多维数组扁平化为一维数组。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.flatten()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 3, 4, 5, 6, 7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nonzero()&lt;/code&gt;方法：返回非0元素的索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;round()&lt;/code&gt;方法：对数组中的元素做四舍五入操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sort()&lt;/code&gt;方法：对数组进行就地排序。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array1.sort()
array1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([ 6, 15, 20, 42, 46, 51, 53, 62, 71, 94])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;swapaxes()&lt;/code&gt;和&lt;code&gt;transpose()&lt;/code&gt;方法：交换数组指定的轴和转置。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.swapaxes(0, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2.transpose()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tolist()&lt;/code&gt;方法：将数组转成 Python 中的&lt;code&gt;list&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array2.tolist())
print(type(array2.tolist()))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
&amp;lt;class &apos;list&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;70 NumPy的应用-3&lt;/h2&gt;
&lt;h4&gt;数组的运算&lt;/h4&gt;
&lt;p&gt;使用 NumPy 最为方便的是当需要对数组元素进行运算时，不用编写循环代码遍历每个元素，所有的运算都会自动的&lt;strong&gt;矢量化&lt;/strong&gt;。简单的说就是，NumPy 中的数学运算和数学函数会自动作用于数组中的每个成员。&lt;/p&gt;
&lt;h5&gt;数组跟标量的运算&lt;/h5&gt;
&lt;p&gt;NumPy 的数组可以跟一个数值进行加、减、乘、除、求模、求幂等运算，对应的运算会作用到数组的每一个元素上，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array1 = np.arange(1, 10)
print(array1 + 10)
print(array1 * 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[11 12 13 14 15 16 17 18 19]
[10 20 30 40 50 60 70 80 90]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了上述的运算，关系运算也是没有问题的，之前讲布尔索引的时候已经遇到过了。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array1 &amp;gt; 5)
print(array1 % 2 == 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[False False False False False  True  True  True  True]
[False  True False  True False  True False  True False]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数组跟数组的运算&lt;/h5&gt;
&lt;p&gt;NumPy 的数组跟数组也可以执行算术运算和关系运算，运算会作用于两个数组对应的元素上，这就要求两个数组的形状（&lt;code&gt;shape&lt;/code&gt;属性）要相同，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array2 = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])
print(array1 + array2)
print(array1 * array2)
print(array1 ** array2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 2  3  4  6  7  8 10 11 12]
[ 1  2  3  8 10 12 21 24 27]
[  1   2   3  16  25  36 343 512 729]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(array1 &amp;gt; array2)
print(array1 % array2 == 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[False  True  True  True  True  True  True  True  True]
[ True  True  True  True False  True False False  True]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;通用一元函数&lt;/h5&gt;
&lt;p&gt;NumPy 中通用一元函数的参数是一个数组对象，函数会对数组进行元素级的处理，例如：&lt;code&gt;sqrt&lt;/code&gt;函数会对数组中的每个元素计算平方根，而&lt;code&gt;log2&lt;/code&gt;函数会对数组中的每个元素计算以2为底的对数，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(np.sqrt(array1))
print(np.log2(array1))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1.         1.41421356 1.73205081 2.         2.23606798 2.44948974
 2.64575131 2.82842712 3.        ]
[0.         1.         1.5849625  2.         2.32192809 2.5849625
 2.80735492 3.         3.169925  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;表1：通用一元函数&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abs&lt;/code&gt; / &lt;code&gt;fabs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求绝对值的函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqrt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求平方根的函数，相当于&lt;code&gt;array ** 0.5 &lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;square&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求平方的函数，相当于&lt;code&gt;array ** 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算 $\small{e^x}$ 的函数，相当于&lt;code&gt;np.e ** array&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log&lt;/code&gt; / &lt;code&gt;log10&lt;/code&gt; / &lt;code&gt;log2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对数函数（&lt;code&gt;e&lt;/code&gt;为底 / &lt;code&gt;10&lt;/code&gt;为底 / &lt;code&gt;2&lt;/code&gt;为底）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sign&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;符号函数（&lt;code&gt;1&lt;/code&gt; - 正数；&lt;code&gt;0&lt;/code&gt; - 零；&lt;code&gt;-1&lt;/code&gt; - 负数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ceil&lt;/code&gt; / &lt;code&gt;floor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上取整 /  下取整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isnan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回布尔数组，NaN对应&lt;code&gt;True&lt;/code&gt;，非NaN对应&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isfinite&lt;/code&gt; / &lt;code&gt;isinf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断数值是否为无穷大的函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cos&lt;/code&gt; / &lt;code&gt;cosh&lt;/code&gt; / &lt;code&gt;sin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;三角函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sinh&lt;/code&gt; / &lt;code&gt;tan&lt;/code&gt; / &lt;code&gt;tanh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;三角函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;arccos&lt;/code&gt; / &lt;code&gt;arccosh&lt;/code&gt; / &lt;code&gt;arcsin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;反三角函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;arcsinh&lt;/code&gt; / &lt;code&gt;arctan&lt;/code&gt; / &lt;code&gt;arctanh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;反三角函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rint&lt;/code&gt; / &lt;code&gt;round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;四舍五入函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;通用二元函数&lt;/h5&gt;
&lt;p&gt;NumPy 中通用二元函数的参数是两个数组对象，函数会对两个数组中的对应元素进行运算，例如：&lt;code&gt;maximum&lt;/code&gt;函数会对两个数组中对应的元素找最大值，而&lt;code&gt;power&lt;/code&gt;函数会对两个数组中对应的元素进行求幂操作，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array3 = np.array([[4, 5, 6], [7, 8, 9]])
array4 = np.array([[1, 2, 3], [3, 2, 1]])
print(np.maximum(array3, array4))
print(np.power(array3, array4))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[4 5 6]
 [7 8 9]]
[[  4  25 216]
 [343  64   9]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;表2：通用二元函数&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add(x, y)&lt;/code&gt; / &lt;code&gt;substract(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加法函数 / 减法函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multiply(x, y)&lt;/code&gt; / &lt;code&gt;divide(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;乘法函数 / 除法函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;floor_divide(x, y)&lt;/code&gt; / &lt;code&gt;mod(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整除函数 / 求模函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allclose(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查数组&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;元素是否几乎相等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;power(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数组&lt;code&gt;x&lt;/code&gt;的元素 $\small{x_{i}}$ 和数组&lt;code&gt;y&lt;/code&gt;的元素 $\small{y_{i}}$，计算 $\small{x_{i}^{y_{i}}}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maximum(x, y)&lt;/code&gt; / &lt;code&gt;fmax(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两两比较元素获取最大值 / 获取最大值（忽略NaN）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minimum(x, y)&lt;/code&gt; / &lt;code&gt;fmin(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两两比较元素获取最小值 / 获取最小值（忽略NaN）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dot(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;点积运算（数量积，通常记为 $\small{\cdot}$ ，用于欧几里得空间（Euclidean space））&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inner(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内积运算（内积的含义要高于点积，点积相当于是内积在欧几里得空间 $\small{\mathbb{R}^{n}}$ 的特例，而内积可以推广到赋范向量空间，只要它满足平行四边形法则即可）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cross(x, y) &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;叉积运算（向量积，通常记为 $\small{\times}$ ，运算结果是一个向量）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outer(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;外积运算（张量积，通常记为 $\small{\bigotimes}$ ，运算结果通常是一个矩阵）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;intersect1d(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的交集，返回这些元素构成的有序数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;union1d(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的并集，返回这些元素构成的有序数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;in1d(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回由判断&lt;code&gt;x&lt;/code&gt; 的元素是否在&lt;code&gt;y&lt;/code&gt;中得到的布尔值构成的数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setdiff1d(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的差集，返回这些元素构成的数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setxor1d(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的对称差，返回这些元素构成的数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：关于向量和矩阵的运算，我们在下一个章节加以说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;广播机制&lt;/h5&gt;
&lt;p&gt;上面数组运算的例子中，两个数组的形状（&lt;code&gt;shape&lt;/code&gt;属性）是完全相同的，我们再来研究一下，两个形状不同的数组是否可以直接做二元运算或使用通用二元函数进行运算，请看下面的例子。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array5 = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]])
array6 = np.array([1, 2, 3])
array5 + array6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 2, 3],
       [2, 3, 4],
       [3, 4, 5],
       [4, 5, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array7 = np.array([[1], [2], [3], [4]])
array5 + array7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 1, 1],
       [3, 3, 3],
       [5, 5, 5],
       [7, 7, 7]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的例子，我们发现形状不同的数组仍然有机会进行二元运算，但这不代表任意形状的数组都可以进行二元运算。简单的说，只有两个数组后缘维度相同或者后缘维度不同但其中一个数组后缘维度为1时，广播机制才会被触发。通过广播机制，NumPy 将两个原本形状不相同的数组变成形状相同，才能进行二元运算。所谓后缘维度，指的是数组形状（&lt;code&gt;shape&lt;/code&gt;属性）从后往前看对应的部分，我们举例说明。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a2ef745b05.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上图中，一个数组的形状是&lt;code&gt;(4, 3)&lt;/code&gt;，另一个数组的形状是&lt;code&gt;(3, )&lt;/code&gt;，从后往前看对应的部分都是&lt;code&gt;3&lt;/code&gt;，属于后缘维度相同，可以应用广播机制，第二个数组会沿着缺失元素那个轴的方向去广播自己，最终让两个数组形状达成一致。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/6a7d59f5d9.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上图中，一个数组的形状是&lt;code&gt;(3, 4, 2)&lt;/code&gt;，另一个数组的形状是&lt;code&gt;(4, 2)&lt;/code&gt;，从后往前看对应的部分都是&lt;code&gt;(4, 2)&lt;/code&gt;，属于后缘维度相同，可以应用广播机制，第二个数组会沿着缺失元素那个轴的方向去广播自己，最终让两个数组形状达成一致。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/4b9dacca19.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上图中，一个数组的形状是&lt;code&gt;(4, 3)&lt;/code&gt;，另一个数组的形状是&lt;code&gt;(4, 1)&lt;/code&gt;，这是后缘维度不相同的情况，但是第二个数组跟第一个数组不同的地方为&lt;code&gt;1&lt;/code&gt;，第二个数组可以沿着为&lt;code&gt;1&lt;/code&gt; 的那个轴广播自己，最终让两个数组形状达成一致。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：一个3行1列的二维数组和一个1行3列的二维数组能够执行加法运算吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;其他常用函数&lt;/h4&gt;
&lt;p&gt;除了上面讲到的函数外，NumPy 中还提供了很多用于处理数组的函数，&lt;code&gt;ndarray&lt;/code&gt;对象的很多方法也可以通过调用函数来实现，下表给出了一些常用的函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;表3：NumPy其他常用函数&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;去除数组重复元素，返回唯一元素构成的有序数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;copy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回拷贝数组得到的数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回数组元素排序后的拷贝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;split&lt;/code&gt; / &lt;code&gt;hsplit&lt;/code&gt; / &lt;code&gt;vsplit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将数组拆成若干个子数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stack&lt;/code&gt; / &lt;code&gt;hstack&lt;/code&gt; / &lt;code&gt;vstack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将多个数组堆叠成新数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;concatenate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;沿着指定的轴连接多个数组构成新数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;append&lt;/code&gt; / &lt;code&gt;insert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向数组末尾追加元素 / 在数组指定位置插入元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;argwhere&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;找出数组中非0元素的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract&lt;/code&gt; / &lt;code&gt;select&lt;/code&gt; / &lt;code&gt;where&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按照指定的条件从数组中抽取或处理数组元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;沿指定的轴翻转数组中的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fromregex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过读取文件和正则表达式解析获取数据创建数组对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;repeat&lt;/code&gt; / &lt;code&gt;tile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过对元素的重复来创建新数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;roll&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;沿指定轴对数组元素进行移位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重新调整数组的大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;place&lt;/code&gt; / &lt;code&gt;put&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将数组中满足条件的元素/指定的元素替换为指定的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;partition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用选定的元素对数组进行一次划分并返回划分后的数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;去重（重复元素只保留一项）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.unique(array5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([0, 1, 2, 3])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;堆叠和拼接&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array8 = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]])
array9 = np.array([[4, 4, 4], [5, 5, 5], [6, 6, 6]])
np.hstack((array8, array9))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 1, 1, 4, 4, 4],
       [2, 2, 2, 5, 5, 5],
       [3, 3, 3, 6, 6, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.vstack((array8, array9))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 3],
       [4, 4, 4],
       [5, 5, 5],
       [6, 6, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.concatenate((array8, array9))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 3],
       [4, 4, 4],
       [5, 5, 5],
       [6, 6, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.concatenate((array8, array9), axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 1, 1, 4, 4, 4],
       [2, 2, 2, 5, 5, 5],
       [3, 3, 3, 6, 6, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追加和插入元素&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.append(array1, [10, 100])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([  1,   2,   3,   4,   5,   6,   7,   8,   9,  10, 100])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.insert(array1, 1, [98, 99, 100])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([  1,  98,  99, 100,   2,   3,   4,   5,   6,   7,   8,   9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;抽取和处理元素&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.extract(array1 % 2 != 0, array1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 3, 5, 7, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面&lt;code&gt;extract&lt;/code&gt;函数的操作相当于我们之前讲的布尔索引。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.select([array1 &amp;lt;= 3, array1 &amp;gt;= 7], [array1 * 10, array1 ** 2])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([10, 20, 30,  0,  0,  0, 49, 64, 81])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面&lt;code&gt;select&lt;/code&gt;函数的第一个参数设置了两个条件，满足第一个条件的元素执行了乘以10的操作，满足第二个条件的元素执行了求平方的操作，两个条件都不能满足的数组元素会被处理为0。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.where(array1 &amp;lt;= 5, array1 * 10, array1 ** 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([10, 20, 30, 40, 50, 36, 49, 64, 81])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面&lt;code&gt;where&lt;/code&gt;函数的第一个参数给出了条件，满足条件的元素执行了乘以10的操作，不能满足条件的元素执行了求平方的操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;重复数组元素创建新数组&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.repeat(array1, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.tile(array1, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;调整数组大小&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.resize(array1, (5, 3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9],
       [1, 2, 3],
       [4, 5, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：&lt;code&gt;array1&lt;/code&gt;原本是一个有9个元素的一维数组，通过&lt;code&gt;resize&lt;/code&gt;函数调整成为5行3列共15个元素的二维数组，缺少的元素通过复用原数组中的元素来补充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; np.resize(array5, (2, 4))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[0, 0, 0, 1],
       [1, 1, 2, 2]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;替换数组元素&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.put(array1, [0, 1, -1, 3, 5], [100, 200])
array1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([100, 200,   3, 200,   5, 100,   7,   8, 100])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面&lt;code&gt;put&lt;/code&gt;函的第二个参数给出了要被替换的元素的索引，但是用来作为替换值的元素只有&lt;code&gt;100&lt;/code&gt;和&lt;code&gt;200&lt;/code&gt;，所以这两个值会被循环使用，因此索引为&lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;-1&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;、&lt;code&gt;5&lt;/code&gt;的元素被依次替换成了&lt;code&gt;100&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;100&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;100&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.place(array1, array1 &amp;gt; 5, [1, 2, 3])
array1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([1, 2, 3, 3, 5, 1, 2, 3, 1])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;put&lt;/code&gt;函数和&lt;code&gt;place&lt;/code&gt;函数都没有返回新的数组对象，而是在原来的数组上直接进行替换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;71 NumPy的应用-4&lt;/h2&gt;
&lt;h4&gt;向量&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;向量&lt;/strong&gt;（&lt;em&gt;vector&lt;/em&gt;）也叫&lt;strong&gt;矢量&lt;/strong&gt;，是一个同时具有大小和方向，且满足平行四边形法则的几何对象。与向量相对的概念叫&lt;strong&gt;标量&lt;/strong&gt;或&lt;strong&gt;数量&lt;/strong&gt;，标量只有大小，绝大多数情况下没有方向。我们通常用带箭头的线段来表示向量，在平面直角坐标系中的向量如下图所示。需要注意的是，向量是表达大小和方向的量，并没有规定起点和终点，所以相同的向量可以画在任意位置，例如下图中 $\small{\boldsymbol{w}}$ 和 $\small{\boldsymbol{u}}$ 两个向量并没有什么区别。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/968273879a.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;向量有很多种代数表示法，对于二维空间的向量，下面几种写法都是可以的。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{a} = \langle a_1, a_2 \rangle = (a_1, a_2) = \begin{pmatrix} a_1 \\ a_2 \end{pmatrix} = \begin{bmatrix} a_1 \\ a_2 \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;向量的大小称为向量的模，它是一个标量，对于二维空间的向量，模可以通过下面的公式计算。&lt;/p&gt;
&lt;p&gt;$$
\lvert \boldsymbol{a} \rvert = \sqrt{a_{1}^{2} + a_{2}^{2}}
$$&lt;/p&gt;
&lt;p&gt;注意，这里的 $\small{\lvert \boldsymbol{a} \rvert}$ 并不是绝对值，你可以将其称为向量 $\small{\boldsymbol{a}}$ 的二范数，这是数学中的符号重用现象。上面的写法和概念也可以推广到 $\small{n}$ 维空间，我们通常用 $\small{\boldsymbol{R^{n}}}$ 表示 $\small{n}$ 维空间，我们刚才说的二维空间可以记为 $\small{\boldsymbol{R^{2}}}$ ，三维空间可以记为 $\small{\boldsymbol{R^{3}}}$ 。虽然生活在三维空间的我们很难想象四维空间、五维空间是什么样子，但是这并不影响我们探讨高维空间，机器学习中，我们经常把有 $\small{n}$ 个特征的训练样本称为一个 $\small{n}$ 维向量。&lt;/p&gt;
&lt;h5&gt;向量的加法&lt;/h5&gt;
&lt;p&gt;相同维度的向量可以相加得到一个新的向量，运算的方法是将向量的每个分量相加，如下所示。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{u} = \begin{bmatrix} u_1 \\ u_2 \\ \vdots \\ u_n \end{bmatrix}, \quad
\boldsymbol{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}, \quad
\boldsymbol{u} + \boldsymbol{v} = \begin{bmatrix} u_1 + v_1 \\ u_2 + v_2 \\ \vdots \\ u_n + v_n \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;向量的加法满足“平行四边形法则”，即两个向量 $\small{\boldsymbol{u}}$ 和 $\small{\boldsymbol{v}}$ 构成了平行四边形的两条邻边，相加的结果是平行四边形的对角线，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/908f9b1bcb.webp&quot; style=&quot;zoom:58%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;向量的数乘&lt;/h5&gt;
&lt;p&gt;一个向量 $\small{\boldsymbol{v}}$ 可以和一个标量 $\small{k}$ 相乘，运算的方法是将向量中的每个分量与该标量相乘即可，如下所示。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}, \quad
k \cdot \boldsymbol{v} = \begin{bmatrix} k \cdot v_1 \\ k \cdot v_2 \\ \vdots \\ k \cdot v_n \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;我们可以用 NumPy 的数组来表示向量，向量的加法可以通过两个数组的加法来实现，向量的数乘可以通过数组和标量的乘法来实现，此处不再进行赘述。&lt;/p&gt;
&lt;h5&gt;向量的点积&lt;/h5&gt;
&lt;p&gt;点积（&lt;em&gt;dot product&lt;/em&gt;）是两个向量之间最为重要的运算之一，运算的方法是将两个向量对应分量的乘积求和，所以点积的结果是一个标量，其几何意义是两个向量的模乘以二者夹角的余弦如下所示。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{u} = \begin{bmatrix} u_1 \\ u_2 \\ \vdots \\ u_n \end{bmatrix}, \quad
\boldsymbol{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix} \quad
$$&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{u} \cdot \boldsymbol{v} = \sum_{i=1}^{n}{u_iv_i} = \lvert \boldsymbol{u} \rvert \lvert \boldsymbol{v} \rvert cos\theta
$$&lt;/p&gt;
&lt;p&gt;假如我们用三维向量来表示用户对喜剧片、言情片和动作片这三类电影的偏好，我们用 1 到 5 的数字来表示喜欢的程度，其中 5 表示非常喜欢，4 表示比较喜欢，3 表示无感，2 表示比较反感，1 表示特别反感。那么，下面的向量表示用户非常喜欢喜剧片，特别反感言情片，对动作片不喜欢也不反感。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{u} = \begin{pmatrix} 5 \\ 1 \\ 3 \end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;现在有两部电影上映了，一部属于言情喜剧片，一部属于喜剧动作片，我们把两部电影也通过3维向量的方式进行表示，如下所示。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{m_1} = \begin{pmatrix} 4 \\ 5 \\ 1 \end{pmatrix}, \quad \boldsymbol{m_2} = \begin{pmatrix} 5 \\ 1 \\ 5 \end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;如果现在我们需要向刚才的用户推荐一部电影，我们应该给他推荐哪一部呢？我们可以将代表用户的向量 $\small{\boldsymbol{u}}$ 和代表电影的向量 $\small{\boldsymbol{m_{1}}}$ 和 $\small{\boldsymbol{m_{2}}}$ 分别进行点积运算，再除以向量的模长，得到向量夹角的余弦值，余弦值越接近 1，说明向量的夹角越接近 0 度，也就是两个向量的相似度越高。很显然，我们应该向用户推荐跟他观影喜好相似度更高的电影。&lt;/p&gt;
&lt;p&gt;$$
cos\theta_1 = \frac{\boldsymbol{u} \cdot \boldsymbol{m1}}{|\boldsymbol{u}||\boldsymbol{m1}|} \approx \frac{4 \times 5 + 5 \times 1 + 3 \times 1}{5.92 \times 6.48} \approx 0.73
$$&lt;/p&gt;
&lt;p&gt;$$
cos\theta_2 = \frac{\boldsymbol{u} \cdot \boldsymbol{m2}}{|\boldsymbol{u}||\boldsymbol{m2}|} \approx \frac{5 \times 5 + 1 \times 1 + 3 \times 5}{5.92 \times 7.14} \approx 0.97
$$&lt;/p&gt;
&lt;p&gt;大家可能会说，向量 $\small{\boldsymbol{m_{2}}}$ 代表的电影肉眼可见跟用户是更加匹配的。的确，对于一个三维向量我们凭借直觉也能够给出正确的答案，但是对于一个 $\small{n}$ 维向量，当 $\small{n}$ 的值非常大时，你还有信心凭借肉眼的观察和本能的直觉给出准确的答案吗？向量的点积可以通过&lt;code&gt;dot&lt;/code&gt;函数来计算，而向量的模长可以通过 NumPy 的&lt;code&gt;linalg&lt;/code&gt;模块中的&lt;code&gt;norm&lt;/code&gt;函数来计算，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;u = np.array([5, 1, 3])
m1 = np.array([4, 5, 1])
m2 = np.array([5, 1, 5])
print(np.dot(u, m1) / (np.linalg.norm(u) * np.linalg.norm(m1)))  # 0.7302967433402214
print(np.dot(u, m2) / (np.linalg.norm(u) * np.linalg.norm(m2)))  # 0.9704311900788593
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;向量的叉积&lt;/h5&gt;
&lt;p&gt;在二维空间，两个向量的叉积是这样定义的：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} = \begin{pmatrix} a_{1} \\ a_{2} \end{pmatrix}, \quad \boldsymbol{B} = \begin{pmatrix} b_{1} \\ b_{2} \end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} \times \boldsymbol{B} = \begin{vmatrix} a_{1} \quad a_{2} \\ b_{1} \quad b_{2} \end{vmatrix} = a_{1}b_{2} - a_{2}b_{1}
$$&lt;/p&gt;
&lt;p&gt;对于三维空间，两个向量的叉积结果是一个向量，如下所示：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} = \begin{pmatrix} a_{1} \\ a_{2} \\ a_{3} \end{pmatrix}, \quad \boldsymbol{B} = \begin{pmatrix} b_{1} \\ b_{2} \\ b_{3} \end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} \times \boldsymbol{B} = \begin{vmatrix} \boldsymbol{\hat{i}} \quad \boldsymbol{\hat{j}} \quad \boldsymbol{\hat{k}} \\ a_{1} \quad a_{2} \quad a_{3} \\ b_{1} \quad b_{2} \quad b_{3} \end{vmatrix} = \langle \boldsymbol{\hat{i}}\begin{vmatrix} a_{2} \quad a_{3} \\ b_{2} \quad b_{3} \end{vmatrix}, -\boldsymbol{\hat{j}}\begin{vmatrix} a_{1} \quad a_{3} \\ b_{1} \quad b_{3} \end{vmatrix}, \boldsymbol{\hat{k}}\begin{vmatrix} a_{1} \quad a_{2} \\ b_{1} \quad b_{2} \end{vmatrix} \rangle
$$&lt;/p&gt;
&lt;p&gt;因为叉积的结果是向量，所以 $\small{\boldsymbol{A} \times \boldsymbol{B}}$ 和 $\small{\boldsymbol{B} \times \boldsymbol{A}}$ 的结果并不相同，事实上：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} \times \boldsymbol{B} = -(\boldsymbol{B} \times \boldsymbol{A})
$$&lt;/p&gt;
&lt;p&gt;NumPy 中可以通过&lt;code&gt;cross&lt;/code&gt;函数来计算向量的叉积，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(np.cross(u, m1))  # [-14   7  21]
print(np.cross(m1, u))  # [ 14  -7 -21]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;行列式&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;行列式&lt;/strong&gt;（&lt;em&gt;determinant&lt;/em&gt;）通常记作 $\small{det(\boldsymbol{A})}$ 或 $\small{ \lvert \boldsymbol{A} \rvert}$ ，其中 $\small{\boldsymbol{A}}$ 是一个 $\small{n}$ 阶方阵。行列式可以看做是有向面积或体积的概念在一般欧几里得空间的推广，或者说行列式描述的是一个线性变换对“体积”所造成的影响。行列式的概念最早出现在解线性方程组的过程中，十七世纪晚期，关孝和（日本江户时代的数学家）与莱布尼茨的著作中已经使用行列式来确定线性方程组解的个数以及形式；十八世纪开始，行列式开始作为独立的数学概念被研究；十九世纪以后，行列式理论进一步得到发展和完善。&lt;/p&gt;
&lt;h5&gt;行列式的性质&lt;/h5&gt;
&lt;p&gt;行列式是由向量引出的，所以行列式解释的其实是向量的性质。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质1&lt;/strong&gt;：如果 $\small{det(\boldsymbol{A})}$ 中某行（或某列）的元素全部为 0，那么 $\small{det(\boldsymbol{A}) = 0}$ 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质2&lt;/strong&gt;：如果 $\small{det(\boldsymbol{A})}$ 中某行（或某列）有公共因子 $\small{k}$ ，则可以提出 $\small{k}$ ，得到行列式 $\small{det(\boldsymbol{A^{&apos;}})}$ ，且 $\small{det(\boldsymbol{A}) = k \cdot det(\boldsymbol{A^{&apos;}})}$ 。&lt;/p&gt;
&lt;p&gt;$$
det(\boldsymbol{A})={\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\{\color {blue}k}a_{i1}&amp;amp;{\color {blue}k}a_{i2}&amp;amp;\dots &amp;amp;{\color {blue}k}a_{in}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}={\color {blue}k}{\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{i1}&amp;amp;a_{i2}&amp;amp;\dots &amp;amp;a_{in}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}={\color {blue}k} \cdot det(\boldsymbol{A^{&apos;}})
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质3&lt;/strong&gt;：如果 $\small{det(\boldsymbol{A})}$ 中某行（或某列）的每个元素是两数之和，则此行列式可拆分为两个行列式相加，如下所示。&lt;/p&gt;
&lt;p&gt;$$
{\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\{\color {blue}a_{i1}}+{\color {OliveGreen}b_{i1}}&amp;amp;{\color {blue}a_{i2}}+{\color {OliveGreen}b_{i2}}&amp;amp;\dots &amp;amp;{\color {blue}a_{in}}+{\color {OliveGreen}b_{in}}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}={\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\{\color {blue}a_{i1}}&amp;amp;{\color {blue}a_{i2}}&amp;amp;\dots &amp;amp;{\color {blue}a_{in}}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}+{\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\{\color {OliveGreen}b_{i1}}&amp;amp;{\color {OliveGreen}b_{i2}}&amp;amp;\dots &amp;amp;{\color {OliveGreen}b_{in}}\\\vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots \\a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质4&lt;/strong&gt;：如果 $\small{det(\boldsymbol{A})}$ 中两行（或两列）元素对应成比例，那么 $\small{det(\boldsymbol{A}) = 0}$ 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质5&lt;/strong&gt;：如果 $\small{det(\boldsymbol{A})}$ 中两行（或两列）互换得到 $\small{det(\boldsymbol{A^{&apos;}})}$ ，那么 $\small{det(\boldsymbol{A}) = -det(\boldsymbol{A^{&apos;}})}$ 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质6&lt;/strong&gt;：将 $\small{det(\boldsymbol{A})}$ 中某行（或某列）的 $\small{k}$ 倍加进另一行（或另一列）里，行列式的值不变，如下所示。&lt;/p&gt;
&lt;p&gt;$$
{\begin{vmatrix}\vdots &amp;amp;\vdots &amp;amp;\vdots &amp;amp;\vdots \\ a_{i1}&amp;amp;a_{i2}&amp;amp;\dots &amp;amp;a_{in} \\ a_{j1}&amp;amp;a_{j2}&amp;amp;\dots &amp;amp;a_{jn}\\\vdots &amp;amp;\vdots &amp;amp;\vdots &amp;amp;\vdots \\ \end{vmatrix}}={\begin{vmatrix}\vdots &amp;amp;\vdots &amp;amp;\vdots &amp;amp;\vdots \\ a_{i1}&amp;amp;a_{i2}&amp;amp;\dots &amp;amp;a_{in} \\ a_{j1}{\color {blue}+ka_{i1}}&amp;amp;a_{j2}{\color {blue}+ka_{i2}}&amp;amp;\dots &amp;amp;a_{jn}{\color {blue}+ka_{in}} \\ \vdots &amp;amp;\vdots &amp;amp;\vdots &amp;amp;\vdots \\ \end{vmatrix}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质7&lt;/strong&gt;：将行列式的行列互换，行列式的值不变，如下所示。&lt;/p&gt;
&lt;p&gt;$$
{\begin{vmatrix}a_{11}&amp;amp;a_{12}&amp;amp;\dots &amp;amp;a_{1n} \\ a_{21}&amp;amp;a_{22}&amp;amp;\dots &amp;amp;a_{2n} \\ \vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots  \\ a_{n1}&amp;amp;a_{n2}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}={\begin{vmatrix}a_{11}&amp;amp;a_{21}&amp;amp;\dots &amp;amp;a_{n1} \\ a_{12}&amp;amp;a_{22}&amp;amp;\dots &amp;amp;a_{n2} \\ \vdots &amp;amp;\vdots &amp;amp;\ddots &amp;amp;\vdots  \\ a_{1n}&amp;amp;a_{2n}&amp;amp;\dots &amp;amp;a_{nn}\end{vmatrix}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质8&lt;/strong&gt;：方块矩阵 $\small{\boldsymbol{A}}$ 和 $\small{\boldsymbol{B}}$ 的乘积的行列式等于其行列式的乘积，即 $\small{det(\boldsymbol{A}\boldsymbol{B}) = det(\boldsymbol{A})det(\boldsymbol{B})}$ 。特别的，若将矩阵中的每一行都乘以常数 $\small{r}$ ，那么行列式的值将是原来的 $\small{r^{n}}$ 倍，即 $\small{det(r\boldsymbol{A}) = det(r\boldsymbol{I_{n}} \cdot \boldsymbol{A}) = r^{n}det(\boldsymbol{A})}$ ，其中 $\small{\boldsymbol{I_{n}}}$ 是 $\small{n}$ 阶单位矩阵。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质9&lt;/strong&gt;：若 $\small{\boldsymbol{A}}$ 是可逆矩阵，那么 $\small{det(\boldsymbol{A}^{-1}) = (det(\boldsymbol{A}))^{-1}}$ 。&lt;/p&gt;
&lt;h5&gt;行列式的计算&lt;/h5&gt;
&lt;p&gt;$\small{n}$ 阶行列式的计算公式如下所示：&lt;/p&gt;
&lt;p&gt;$$
det(\boldsymbol{A})=\sum_{n!} \pm {a_{1\alpha}a_{2\beta} \cdots a_{n\omega}}
$$&lt;/p&gt;
&lt;p&gt;对于二阶行列式，上面的公式相当于：&lt;/p&gt;
&lt;p&gt;$$
\begin{vmatrix} a_{11} \quad a_{12} \\ a_{21} \quad a_{22} \end{vmatrix} = a_{11}a_{22} - a_{12}a_{21}
$$&lt;/p&gt;
&lt;p&gt;对于三阶行列式，上面的计算公式相当于：&lt;/p&gt;
&lt;p&gt;$$
\begin{vmatrix} a_{11} \quad a_{12} \quad a_{13} \\ a_{21} \quad a_{22} \quad a_{23} \\ a_{31} \quad a_{32} \quad a_{33} \end{vmatrix} = a_{11}a_{22}a_{33} + a_{12}a_{23}a_{31} + a_{13}a_{21}a_{32} - a_{11}a_{23}a_{32} - a_{12}a_{21}a_{33} - a_{13}a_{22}a_{31}
$$&lt;/p&gt;
&lt;p&gt;高阶行列式可以用&lt;strong&gt;代数余子式&lt;/strong&gt;（&lt;em&gt;cofactor&lt;/em&gt;）展开成多个低阶行列式，如下所示：&lt;/p&gt;
&lt;p&gt;$$
det(\boldsymbol{A})=a_{11}C_{11}+a_{12}C_{12}+ \cdots +a_{1n}C_{1n} = \sum_{i=1}^{n}{a_{1i}C_{1i}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{C_{11}}$ 是原行列式去掉 $\small{a_{11}}$ 所在行和列之后剩余的部分构成的行列式，以此类推。&lt;/p&gt;
&lt;h4&gt;矩阵&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;矩阵&lt;/strong&gt;（&lt;em&gt;matrix&lt;/em&gt;）是由一系列元素排成的矩形阵列，矩阵里的元素可以是数字、符号或数学公式。矩阵可以进行&lt;strong&gt;加法&lt;/strong&gt;、&lt;strong&gt;减法&lt;/strong&gt;、&lt;strong&gt;数乘&lt;/strong&gt;、&lt;strong&gt;转置&lt;/strong&gt;、&lt;strong&gt;矩阵乘法&lt;/strong&gt;等运算，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/beca837795.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;值得一提的是矩阵乘法运算，该运算仅当第一个矩阵 $\small{\boldsymbol{A}}$ 的列数和另一个矩阵 $\small{\boldsymbol{B}}$ 的行数相等时才能定义。如果 $\small{\boldsymbol{A}}$ 是一个 $\small{m \times n}$ 的矩阵， $\small{\boldsymbol{B}}$ 是一个 $\small{n \times k}$ 矩阵，它们的乘积是一个 $\small{m \times k}$ 的矩阵，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/3af90e581c.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix} 1 &amp;amp; 0 &amp;amp; 2 \\ -1 &amp;amp; 3 &amp;amp; 1 \end{bmatrix} \times \begin{bmatrix} 3 &amp;amp; 1 \\ 2 &amp;amp; 1 \\ 1 &amp;amp; 0 \end{bmatrix} = \begin{bmatrix} (1 \times 3  +  0 \times 2  +  2 \times 1) &amp;amp; (1 \times 1   +   0 \times 1   +   2 \times 0) \\ (-1 \times 3  +  3 \times 2  +  1 \times 1) &amp;amp; (-1 \times 1   +   3 \times 1   +   1 \times 0) \end{bmatrix} = \begin{bmatrix} 5 &amp;amp; 1 \\ 4 &amp;amp; 2 \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;矩阵的乘法满足结合律和对矩阵加法的分配律：&lt;/p&gt;
&lt;p&gt;结合律： $\small{(\boldsymbol{AB})\boldsymbol{C} = \boldsymbol{A}(\boldsymbol{BC})}$ 。&lt;/p&gt;
&lt;p&gt;左分配律： $\small{(\boldsymbol{A} + \boldsymbol{B})\boldsymbol{C} = \boldsymbol{AC} + \boldsymbol{BC}}$ 。&lt;/p&gt;
&lt;p&gt;右分配律： $\small{\boldsymbol{C}(\boldsymbol{A} + \boldsymbol{B}) = \boldsymbol{CA} + \boldsymbol{CB}}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;矩阵乘法不满足交换律&lt;/strong&gt;。一般情况下，矩阵 $\small{\boldsymbol{A}}$ 和 $\small{\boldsymbol{B}}$ 的乘积 $\small{\boldsymbol{AB}}$ 存在，但 $\small{\boldsymbol{BA}}$ 不一定存在，即便 $\small{\boldsymbol{BA}}$ 存在，大多数时候 $\small{\boldsymbol{AB} \neq \boldsymbol{BA}}$ 。&lt;/p&gt;
&lt;p&gt;矩阵乘法的一个基本应用是在线性方程组上。线性方程组是方程组的一种，它符合以下的形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
a_{1,1}x_{1} + a_{1,2}x_{2} + \cdots + a_{1,n}x_{n}=  b_{1} \\
a_{2,1}x_{1} + a_{2,2}x_{2} + \cdots + a_{2,n}x_{n}=  b_{2} \\
\vdots \quad \quad \quad \vdots \\
a_{m,1}x_{1} + a_{m,2}x_{2} + \cdots + a_{m,n}x_{n}=  b_{m}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;运用矩阵的方式，可以将线性方程组写成一个向量方程：&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{Ax} = \boldsymbol{b}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{\boldsymbol{A}}$ 是由方程组里未知数的系数排成的 $\small{m \times n}$ 矩阵， $\small{\boldsymbol{x}}$ 是含有 $\small{n}$ 个元素的行向量， $\small{\boldsymbol{b}}$ 是含有 $\small{m}$ 个元素的行向量。&lt;/p&gt;
&lt;p&gt;矩阵是线性变换（保持向量加法和标量乘法的函数）的便利表达法。矩阵乘法的本质在联系到线性变换的时候最能体现，因为矩阵乘法和线性变换的合成有以下的联系，即每个 $\small{m \times n}$ 的矩阵 $\small{\boldsymbol{A}}$ 都代表了一个从 $\small{\boldsymbol{R}^{n}}$ 射到 $\small{\boldsymbol{R}^{m}}$ 的线性变换。如果无法理解上面这些内容，推荐大家看看B站上名为&lt;a href=&quot;https://www.bilibili.com/video/BV1ib411t7YR/&quot;&gt;《线性代数的本质》&lt;/a&gt;的视频，相信这套视频会让你对线性代数有一个更好的认知。&lt;/p&gt;
&lt;p&gt;下图是一个来自于维基百科的例子，图中展示了一些典型的二维实平面上的线性变换对平面向量（图形）造成的效果以及它们对应的二维矩阵，其中每个线性变换将蓝色图形映射成绿色图形；平面的原点 $\small{(0, 0)}$ 用黑点表示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/0b6cf78cb2.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;矩阵对象&lt;/h5&gt;
&lt;p&gt;NumPy 中提供了专门用于线性代数（&lt;em&gt;linear algebra&lt;/em&gt;）的模块和表示矩阵的类型&lt;code&gt;matrix&lt;/code&gt;，当然我们通过二维数组也可以表示一个矩阵，官方并不推荐使用&lt;code&gt;matrix&lt;/code&gt;类而是建议使用二维数组，而且有可能在将来的版本中会移除&lt;code&gt;matrix&lt;/code&gt;类。无论如何，利用这些已经封装好的类和函数，我们可以轻松愉快的实现很多对矩阵的操作。&lt;/p&gt;
&lt;p&gt;我们可以通过下面的代码来创建矩阵（&lt;code&gt;matrix&lt;/code&gt;）对象。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m1 = np.matrix(&apos;1 2 3; 4 5 6&apos;)
m1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;matrix&lt;/code&gt;构造器可以传入类数组对象也可以传入字符串来构造矩阵对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;matrix([[1, 2, 3],
        [4, 5, 6]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m2 = np.asmatrix(np.array([[1, 1], [2, 2], [3, 3]]))
m2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;asmatrix&lt;/code&gt;函数也可以用&lt;code&gt;mat&lt;/code&gt;函数代替，这两个函数其实是同一个函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;matrix([[1, 1],
        [2, 2],
        [3, 3]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m1 * m2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;matrix([[14, 14],
        [32, 32]])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：注意&lt;code&gt;matrix&lt;/code&gt;对象和&lt;code&gt;ndarray&lt;/code&gt;对象乘法运算的差别，&lt;code&gt;matrix&lt;/code&gt;对象的&lt;code&gt;*&lt;/code&gt;运算是矩阵乘法运算。如果两个二维数组要做矩阵乘法运算，应该使用&lt;code&gt;@&lt;/code&gt;运算符或&lt;code&gt;matmul&lt;/code&gt;函数，而不是&lt;code&gt;*&lt;/code&gt;运算符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;矩阵对象的属性如下表所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取矩阵对象对应的&lt;code&gt;ndarray&lt;/code&gt;对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取矩阵对象对应的扁平化后的&lt;code&gt;ndarray&lt;/code&gt;对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可逆矩阵的逆矩阵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩阵的转置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;H&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩阵的共轭转置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩阵的形状&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩阵元素的个数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;矩阵对象的方法跟之前讲过的&lt;code&gt;ndarray&lt;/code&gt;数组对象的方法基本差不多，此处不再进行赘述。&lt;/p&gt;
&lt;h5&gt;线性代数模块&lt;/h5&gt;
&lt;p&gt;NumPy 的&lt;code&gt;linalg&lt;/code&gt;模块中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的函数，它们跟 MATLAB 和 R 等语言所使用的是相同的行业标准线性代数库，下面的表格列出了&lt;code&gt;numpy&lt;/code&gt;以及&lt;code&gt;linalg&lt;/code&gt;模块中一些常用的线性代数相关函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;diag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以一维数组的形式返回方阵的对角线元素或将一维数组转换为方阵（非对角元素元素为0）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;matmul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩阵乘法运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算对角线元素的和&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;norm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求矩阵或向量的范数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;det&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算行列式的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;matrix_rank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算矩阵的秩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算矩阵的特征值（&lt;em&gt;eigenvalue&lt;/em&gt;）和特征向量（&lt;em&gt;eigenvector&lt;/em&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算非奇异矩阵（ $\small{n}$ 阶方阵）的逆矩阵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pinv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算矩阵的摩尔-彭若斯（&lt;em&gt;Moore-Penrose&lt;/em&gt;）广义逆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QR分解（把矩阵分解成一个正交矩阵与一个上三角矩阵的积）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;svd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算奇异值分解（&lt;em&gt;singular value decomposition&lt;/em&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;解线性方程组 $\small{\boldsymbol{Ax}=\boldsymbol{b}}$，其中 $\small{\boldsymbol{A}}$ 是一个方阵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lstsq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算 $\small{\boldsymbol{Ax}=\boldsymbol{b}}$ 的最小二乘解&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;下面我们简单尝试一下上面的函数，先试一试求逆矩阵。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m3 = np.array([[1., 2.], [3., 4.]])
m4 = np.linalg.inv(m3)
m4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[-2. ,  1. ],
       [ 1.5, -0.5]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.around(m3 @ m4)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;around&lt;/code&gt;函数对数组元素进行四舍五入操作，默认小数点后面的位数为0。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1., 0.],
       [0., 1.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：矩阵和它的逆矩阵做矩阵乘法会得到单位矩阵。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;计算行列式的值。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m5 = np.array([[1, 3, 5], [2, 4, 6], [4, 7, 9]])
np.linalg.det(m5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算矩阵的秩。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.linalg.matrix_rank(m5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;求解线性方程组。&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
x_1 + 2x_2 + x_3 = 8 \\
3x_1 + 7x_2 + 2x_3 = 23 \\
2x_1 + 2x_2 + x_3 = 9
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;对于上面的线性方程组，我们可以用矩阵的形式来表示它，如下所示。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{A} = \begin{bmatrix}
1 &amp;amp; 2 &amp;amp; 1 \\
3 &amp;amp; 7 &amp;amp; 2 \\
2 &amp;amp; 2 &amp;amp; 1
\end{bmatrix}, \quad
\boldsymbol{x} = \begin{bmatrix}
x_1 \\
x_2 \\
x_3
\end{bmatrix}, \quad
\boldsymbol{b} = \begin{bmatrix}
8 \\
23 \\
9
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{Ax} = \boldsymbol{b}
$$&lt;/p&gt;
&lt;p&gt;线性方程组有唯一解的条件：系数矩阵 $\small{\boldsymbol{A}}$ 的秩等于增广矩阵 $\small{\boldsymbol{Ab}}$ 的秩，而且跟未知数的个数相同。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A = np.array([[1, 2, 1], [3, 7, 2], [2, 2, 1]])
b = np.array([8, 23, 9]).reshape(-1, 1)
print(np.linalg.matrix_rank(A))
print(np.linalg.matrix_rank(np.hstack((A, b))))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：使用数组对象的&lt;code&gt;reshape&lt;/code&gt;方法调形时，如果其中一个参数为-1，那么该维度有多少个元素是通过数组元素个数（&lt;code&gt;size&lt;/code&gt;属性）和其他维度的元素个数自动计算出来的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.linalg.solve(A, b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1.],
       [2.],
       [3.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的结果表示，线性方程组的解为： $\small{x_1 = 1, x_2 = 2, x_3 = 3}$ 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面是另一种求解线性方程组的方法，大家可以停下来思考下为什么。&lt;/p&gt;
&lt;p&gt;$$
\boldsymbol{x} = \boldsymbol{A}^{-1} \cdot \boldsymbol{b}
$$&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.linalg.inv(A) @ b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1.],
       [2.],
       [3.]])
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;多项式&lt;/h4&gt;
&lt;p&gt;除了数组，NumPy 中还封装了用于&lt;strong&gt;多项式&lt;/strong&gt;（&lt;em&gt;polynomial&lt;/em&gt;）运算的数据类型。多项式是变量的整数次幂与系数的乘积之和，形如：&lt;/p&gt;
&lt;p&gt;$$
f(x)=a_nx^n + a_{n-1}x^{n-1} + \cdots + a_1x^{1} + a_0x^{0}
$$&lt;/p&gt;
&lt;p&gt;在 NumPy 1.4版本之前，我们可以用&lt;code&gt;poly1d&lt;/code&gt;类型来表示多项式，目前它仍然可用，但是官方提供了新的模块&lt;code&gt;numpy.polynomial&lt;/code&gt;，它除了支持基本的幂级数多项式外，还可以支持切比雪夫多项式、拉盖尔多项式等。&lt;/p&gt;
&lt;h5&gt;创建多项式对象&lt;/h5&gt;
&lt;p&gt;创建&lt;code&gt;poly1d&lt;/code&gt;对象，例如： $\small{f(x)=3x^{2}+2x+1}$ 。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p1 = np.poly1d([3, 2, 1])
p2 = np.poly1d([1, 2, 3])
print(p1)
print(p2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   2
3 x + 2 x + 1
   2
1 x + 2 x + 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;多项式的操作&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;获取多项式的系数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(p1.coefficients)
print(p2.coeffs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[3 2 1]
[1 2 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;两个多项式的四则运算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(p1 + p2)
print(p1 * p2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   2
4 x + 4 x + 4
   4     3      2
3 x + 8 x + 14 x + 8 x + 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;带入 $\small{x}$ 求多项式的值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(p1(3))
print(p2(3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;34
18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;多项式求导和不定积分&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(p1.deriv())
print(p1.integ())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
6 x + 2
   3     2
1 x + 1 x + 1 x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;求多项式的根&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如有多项式 $\small{f(x)=x^2+3x+2}$ ，多项式的根即一元二次方程 $\small{x^2+3x+2=0}$ 的解。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p3 = np.poly1d([1, 3, 2])
print(p3.roots)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[-2. -1.]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用&lt;code&gt;numpy.polynomial&lt;/code&gt;模块的&lt;code&gt;Polynomial&lt;/code&gt;类来表示多项式对象，那么对应的操作如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from numpy.polynomial import Polynomial

p3 = Polynomial((2, 3, 1))
print(p3)           # 输出多项式
print(p3(3))        # 令x=3，计算多项式的值
print(p3.roots())   # 计算多项式的根
print(p3.degree())  # 获得多项式的次数
print(p3.deriv())   # 求导
print(p3.integ())   # 求不定积分
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2.0 + 3.0·x + 1.0·x²
20.0
[-2. -1.]
2
3.0 + 2.0·x
0.0 + 2.0·x + 1.5·x² + 0.33333333·x³
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;最小二乘解&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Polynomial&lt;/code&gt;类还有一个名为&lt;code&gt;fit&lt;/code&gt;的类方法，它可以给多项式求最小二乘解。所谓最小二乘解（least-squares solution），是用最小二乘法通过最小化误差的平方和来寻找数据的最佳匹配函数的系数。假设多项式为 $\small{f(x)=ax+b}$ ，最小二乘解就是让下面的残差平方和 $\small{RSS}$ 达到最小的 $\small{a}$ 和 $\small{b} $。&lt;/p&gt;
&lt;p&gt;$$
RSS = \sum_{i=0}^{k}{(f(x_i) - y_i)^{2}}
$$&lt;/p&gt;
&lt;p&gt;例如，我们想利用收集到的月收入和网购支出的历史数据来建立一个预测模型，以达到通过某人的月收入预测他网购支出金额的目标，下面是我们收集到的收入和网购支出的数据，保存在两个数组中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = np.array([
    25000, 15850, 15500, 20500, 22000, 20010, 26050, 12500, 18500, 27300,
    15000,  8300, 23320,  5250,  5800,  9100,  4800, 16000, 28500, 32000,
    31300, 10800,  6750,  6020, 13300, 30020,  3200, 17300,  8835,  3500
])
y = np.array([
    2599, 1400, 1120, 2560, 1900, 1200, 2320,  800, 1650, 2200,
     980,  580, 1885,  600,  400,  800,  420, 1380, 1980, 3999,
    3800,  725,  520,  420, 1200, 4020,  350, 1500,  560,  500
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以先绘制散点图来了解两组数据是否具有正相关或负相关关系。正相关意味着数组&lt;code&gt;x&lt;/code&gt;中较大的值对应到数组&lt;code&gt;y&lt;/code&gt;中也是较大的值，而负相关则意味着数组&lt;code&gt;x&lt;/code&gt;中较大的值对应到数组&lt;code&gt;y&lt;/code&gt;中较小的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

plt.figure(dpi=120)
plt.scatter(x, y, color=&apos;blue&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/ce8c3ae1c4.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果需要定量的研究两组数据的相关性，我们可以计算协方差或相关系数，对应的 NumPy 函数分别是&lt;code&gt;cov&lt;/code&gt;和&lt;code&gt;corrcoef&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;np.corrcoef(x, y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([[1.        , 0.92275889],
       [0.92275889, 1.        ]])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：相关系数是一个&lt;code&gt;-1&lt;/code&gt;到&lt;code&gt;1&lt;/code&gt;之间的值，越靠近&lt;code&gt;1&lt;/code&gt; 说明正相关性越强，越靠近&lt;code&gt;-1&lt;/code&gt;说明负相关性越强，靠近&lt;code&gt;0&lt;/code&gt;则说明两组数据没有明显的相关性。上面月收入和网购支出之间的相关系数是&lt;code&gt;0.92275889&lt;/code&gt;，说明二者是强正相关关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过上面的操作，我们确定了收入和网购支出之前存在强正相关关系，于是我们用这些数据来创建一个回归模型，找出一条能够很好的拟合这些数据点的直线。这里，我们就可以用到上面提到的&lt;code&gt;fit&lt;/code&gt;方法，具体的代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from numpy.polynomial import Polynomial

Polynomial.fit(x, y, deg=1).convert().coef
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;deg=1&lt;/code&gt;说明回归模型最高次项就是1次项，回归模型形如 $\small{y=ax+b}$ ；如果要生一个类似于 $\small{y=ax^2+bx+c}$ 的模型，就需要设置&lt;code&gt;deg=2&lt;/code&gt;，以此类推。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([-2.94883437e+02,  1.10333716e-01])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据上面输出的结果，我们的回归方程应该是 $\small{y=0.110333716x-294.883437}$ 。我们将这个回归方程绘制到刚才的散点图上，红色的点是我们的预测值，蓝色的点是历史数据，也就是真实值。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

plt.scatter(x, y, color=&apos;blue&apos;)
plt.scatter(x, 0.110333716 * x - 294.883437, color=&apos;red&apos;)
plt.plot(x, 0.110333716 * x - 294.883437, color=&apos;darkcyan&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/4631b37b12.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果不使用&lt;code&gt;Polynomial&lt;/code&gt;类型的&lt;code&gt;fit&lt;/code&gt;方法，我们也可以通过 NumPy 提供的&lt;code&gt;polyfit&lt;/code&gt;函数来完成同样的操作，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：本章部分图片来自于维基百科。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;72 深入浅出pandas-1&lt;/h2&gt;
&lt;p&gt;Pandas 是 Wes McKinney 在2008年开发的一个强大的&lt;strong&gt;分析结构化数据&lt;/strong&gt;的工具集。Pandas 以 NumPy 为基础（实现数据存储和运算），提供了专门用于数据分析的类型、方法和函数，对数据分析和数据挖掘提供了很好的支持；同时 pandas 还可以跟数据可视化工具 matplotlib 很好的整合在一起，非常轻松愉快的实现数据可视化呈现。&lt;/p&gt;
&lt;p&gt;Pandas 核心的数据类型是&lt;code&gt;Series&lt;/code&gt;（数据系列）、&lt;code&gt;DataFrame&lt;/code&gt;（数据窗/数据框），分别用于处理一维和二维的数据，除此之外，还有一个名为&lt;code&gt;Index&lt;/code&gt;的类型及其子类型，它们为&lt;code&gt;Series&lt;/code&gt;和&lt;code&gt;DataFrame&lt;/code&gt;提供了索引功能。日常工作中&lt;code&gt;DataFrame&lt;/code&gt;使用得最为广泛，因为二维的数据结构刚好可以对应有行有列的表格。&lt;code&gt;Series&lt;/code&gt;和&lt;code&gt;DataFrame&lt;/code&gt;都提供了大量的处理数据的方法，数据分析师以此为基础，可以实现对数据的筛选、合并、拼接、清洗、预处理、聚合、透视和可视化等各种操作。&lt;/p&gt;
&lt;h4&gt;创建Series对象&lt;/h4&gt;
&lt;p&gt;Pandas 库中的&lt;code&gt;Series&lt;/code&gt;对象可以用来表示一维数据结构，但是多了索引和一些额外的功能。&lt;code&gt;Series&lt;/code&gt;类型的内部结构包含了两个数组，其中一个用来保存数据，另一个用来保存数据的索引。我们可以通过列表或数组创建&lt;code&gt;Series&lt;/code&gt;对象，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import pandas as pd

ser1 = pd.Series(data=[120, 380, 250, 360], index=[&apos;一季度&apos;, &apos;二季度&apos;, &apos;三季度&apos;, &apos;四季度&apos;])
ser1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;Series&lt;/code&gt;构造器中的&lt;code&gt;data&lt;/code&gt;参数表示数据，&lt;code&gt;index&lt;/code&gt;参数表示数据的索引，相当于数据对应的标签。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    120
二季度    380
三季度    250
四季度    360
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过字典创建Series对象。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2 = pd.Series({&apos;一季度&apos;: 320, &apos;二季度&apos;: 180, &apos;三季度&apos;: 300, &apos;四季度&apos;: 405})
ser2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过字典创建&lt;code&gt;Series&lt;/code&gt;对象时，字典的键就是数据的标签（索引），键对应的值就是数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    320
二季度    180
三季度    300
四季度    405
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Series对象的运算&lt;/h4&gt;
&lt;h5&gt;标量运算&lt;/h5&gt;
&lt;p&gt;我们尝试给刚才的&lt;code&gt;ser1&lt;/code&gt;每个季度加上&lt;code&gt;10&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser1 += 10
ser1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    130
二季度    390
三季度    260
四季度    370
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;矢量运算&lt;/h5&gt;
&lt;p&gt;我们尝试把&lt;code&gt;ser1&lt;/code&gt;和&lt;code&gt;ser2&lt;/code&gt;对应季度的数据加起来，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser1 + ser2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    450
二季度    570
三季度    560
四季度    775
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;索引运算&lt;/h5&gt;
&lt;h6&gt;普通索引&lt;/h6&gt;
&lt;p&gt;跟数组一样，&lt;code&gt;Series&lt;/code&gt;对象也可以进行索引和切片操作，不同的是&lt;code&gt;Series&lt;/code&gt;对象因为内部维护了一个保存索引的数组，所以除了可以使用整数索引检索数据外，还可以通过自己设置的索引（标签）获取对应的数据。&lt;/p&gt;
&lt;p&gt;使用整数索引。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser1[2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;260
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用自定义索引。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser1[&apos;三季度&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;260
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser1[&apos;一季度&apos;] = 380
ser1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    380
二季度    390
三季度    260
四季度    370
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;切片索引&lt;/h6&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的切片操作跟列表、数组类似，通过给出起始和结束索引，从原来的&lt;code&gt;Series&lt;/code&gt;对象中取出或修改部分数据，这里也可以使用整数索引和自定义的索引，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[1:3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二季度    180
三季度    300
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[&apos;二季度&apos;:&apos;四季度&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二季度    180
三季度    300
四季度    405
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：在使用自定义索引进行切片时，结束索引对应的元素也是可以取到的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[1:3] = 400, 500
ser2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    320
二季度    400
三季度    500
四季度    405
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;花式索引&lt;/h6&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[[&apos;二季度&apos;, &apos;四季度&apos;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二季度    400
四季度    405
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[[&apos;二季度&apos;, &apos;四季度&apos;]] = 600, 520
ser2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一季度    320
二季度    600
三季度    500
四季度    520
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;布尔索引&lt;/h6&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2[ser2 &amp;gt;= 500]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二季度    600
三季度    500
四季度    520
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Series对象的属性和方法&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的属性和方法非常多，我们就捡着重要的跟大家讲吧。先看看下面的表格，它展示了&lt;code&gt;Series&lt;/code&gt;对象常用的属性。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dtype&lt;/code&gt; / &lt;code&gt;dtypes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回&lt;code&gt;Series&lt;/code&gt;对象的数据类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hasnans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断&lt;code&gt;Series&lt;/code&gt;对象中有没有空值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;at&lt;/code&gt; / &lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过索引访问&lt;code&gt;Series&lt;/code&gt;对象中的单个值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loc&lt;/code&gt; / &lt;code&gt;iloc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过索引访问&lt;code&gt;Series&lt;/code&gt;对象中的单个值或一组值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回&lt;code&gt;Series&lt;/code&gt;对象的索引（&lt;code&gt;Index&lt;/code&gt;对象）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_monotonic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断&lt;code&gt;Series&lt;/code&gt;对象中的数据是否单调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_monotonic_increasing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断&lt;code&gt;Series&lt;/code&gt;对象中的数据是否单调递增&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_monotonic_decreasing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断&lt;code&gt;Series&lt;/code&gt;对象中的数据是否单调递减&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_unique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断&lt;code&gt;Series&lt;/code&gt;对象中的数据是否独一无二&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回&lt;code&gt;Series&lt;/code&gt;对象中元素的个数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以&lt;code&gt;ndarray&lt;/code&gt;的方式返回&lt;code&gt;Series&lt;/code&gt;对象中的值（&lt;code&gt;ndarray&lt;/code&gt;对象）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们可以通过下面的代码来了解&lt;code&gt;Series&lt;/code&gt;对象的属性。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(ser2.dtype)                    # 数据类型
print(ser2.hasnans)                  # 有没有空值
print(ser2.index)                    # 索引
print(ser2.values)                   # 值
print(ser2.is_monotonic_increasing)  # 是否单调递增
print(ser2.is_unique)                # 是否每个值都独一无二
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int64
False
Index([&apos;一季度&apos;, &apos;二季度&apos;, &apos;三季度&apos;, &apos;四季度&apos;], dtype=&apos;object&apos;)
[320 600 500 520]
False
True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的方法很多，下面我们通过一些代码片段为大家介绍常用的方法。&lt;/p&gt;
&lt;h5&gt;统计相关&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象支持各种获取描述性统计信息的方法。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(ser2.count())   # 计数
print(ser2.sum())     # 求和
print(ser2.mean())    # 求平均
print(ser2.median())  # 找中位数
print(ser2.max())     # 找最大
print(ser2.min())     # 找最小
print(ser2.std())     # 求标准差
print(ser2.var())     # 求方差
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4
1940
485.0
510.0
600
320
118.18065267490557
13966.666666666666
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象还有一个名为&lt;code&gt;describe()&lt;/code&gt;的方法，可以获得上述所有的描述性统计信息，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser2.describe()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;count      4.000000
mean     485.000000
std      118.180653
min      320.000000
25%      455.000000
50%      510.000000
75%      540.000000
max      600.000000
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：因为&lt;code&gt;describe()&lt;/code&gt;返回的也是一个&lt;code&gt;Series&lt;/code&gt;对象，所以也可以用&lt;code&gt;ser2.describe()[&apos;mean&apos;]&lt;/code&gt;来获取平均值，用&lt;code&gt;ser2.describe()[[&apos;max&apos;, &apos;min&apos;]]&lt;/code&gt;来获取最大值和最小值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果&lt;code&gt;Series&lt;/code&gt;对象有重复的值，我们可以使用&lt;code&gt;unique()&lt;/code&gt;方法获得由独一无二的值构成的数组；可以使用&lt;code&gt;nunique()&lt;/code&gt;方法统计不重复值的数量；如果想要统计每个值重复的次数，可以使用&lt;code&gt;value_counts()&lt;/code&gt;方法，这个方法会返回一个&lt;code&gt;Series&lt;/code&gt;对象，它的索引就是原来的&lt;code&gt;Series&lt;/code&gt;对象中的值，而每个值出现的次数就是返回的&lt;code&gt;Series&lt;/code&gt;对象中的数据，在默认情况下会按照出现次数做降序排列，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser3 = pd.Series(data=[&apos;apple&apos;, &apos;banana&apos;, &apos;apple&apos;, &apos;pitaya&apos;, &apos;apple&apos;, &apos;pitaya&apos;, &apos;durian&apos;])
ser3.value_counts()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apple     3
pitaya    2
durian    1
banana    1
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser3.nunique()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;ser3&lt;/code&gt;，我们还可以用&lt;code&gt;mode()&lt;/code&gt;方法来找出数据的众数，由于众数可能不唯一，所以&lt;code&gt;mode()&lt;/code&gt;方法的返回值仍然是一个&lt;code&gt;Series&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser3.mode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    apple
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;处理数据&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;isna()&lt;/code&gt;和&lt;code&gt;isnull()&lt;/code&gt;方法可以用于空值的判断，&lt;code&gt;notna()&lt;/code&gt;和&lt;code&gt;notnull()&lt;/code&gt;方法可以用于非空值的判断，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser4 = pd.Series(data=[10, 20, np.nan, 30, np.nan])
ser4.isna()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;np.nan&lt;/code&gt;是一个IEEE 754标准的浮点小数，专门用来表示“不是一个数”，在上面的代码中我们用它来代表空值；当然，也可以用 Python 中的&lt;code&gt;None&lt;/code&gt;来表示空值，在 pandas 中&lt;code&gt;None&lt;/code&gt;也会被处理为&lt;code&gt;np.nan&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    False
1    False
2     True
3    False
4     True
dtype: bool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser4.notna()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0     True
1     True
2    False
3     True
4    False
dtype: bool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;dropna()&lt;/code&gt;和&lt;code&gt;fillna()&lt;/code&gt;方法分别用来删除空值和填充空值，具体的用法如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser4.dropna()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    10.0
1    20.0
3    30.0
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser4.fillna(value=40)  # 将空值填充为40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    10.0
1    20.0
2    40.0
3    30.0
4    40.0
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser4.fillna(method=&apos;ffill&apos;)  # 用空值前面的非空值填充
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    10.0
1    20.0
2    20.0
3    30.0
4    30.0
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要提醒大家注意的是，&lt;code&gt;dropna()&lt;/code&gt;和&lt;code&gt;fillna()&lt;/code&gt;方法都有一个名为&lt;code&gt;inplace&lt;/code&gt;的参数，它的默认值是&lt;code&gt;False&lt;/code&gt;，表示删除空值或填充空值不会修改原来的&lt;code&gt;Series&lt;/code&gt;对象，而是返回一个新的&lt;code&gt;Series&lt;/code&gt;对象。如果将&lt;code&gt;inplace&lt;/code&gt;参数的值修改为&lt;code&gt;True&lt;/code&gt;，那么删除或填充空值会就地操作，直接修改原来的&lt;code&gt;Series&lt;/code&gt;对象，此时方法的返回值是&lt;code&gt;None&lt;/code&gt;。后面我们会接触到的很多方法，包括&lt;code&gt;DataFrame&lt;/code&gt;对象的很多方法都会有这个参数，它们的意义跟这里是一样的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;mask()&lt;/code&gt;和&lt;code&gt;where()&lt;/code&gt;方法可以将满足或不满足条件的值进行替换，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser5 = pd.Series(range(5))
ser5.where(ser5 &amp;gt; 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    NaN
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser5.where(ser5 &amp;gt; 1, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    10
1    10
2     2
3     3
4     4
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser5.mask(ser5 &amp;gt; 1, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0     0
1     1
2    10
3    10
4    10
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;duplicated()&lt;/code&gt;方法可以帮助我们找出重复的数据，而&lt;code&gt;drop_duplicates()&lt;/code&gt;方法可以帮我们删除重复数据。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser3.duplicated()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    False
1    False
2     True
3    False
4     True
5     True
6    False
dtype: bool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser3.drop_duplicates()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0     apple
1    banana
3    pitaya
6    durian
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;apply()&lt;/code&gt;和&lt;code&gt;map()&lt;/code&gt;方法非常重要，它们可以通过字典或者指定的函数来处理数据，把数据映射或转换成我们想要的样子。这两个方法在数据准备阶段非常重要，我们先来试一试这个名为&lt;code&gt;map&lt;/code&gt;的方法。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser6 = pd.Series([&apos;cat&apos;, &apos;dog&apos;, np.nan, &apos;rabbit&apos;])
ser6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0       cat
1       dog
2       NaN
3    rabbit
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser6.map({&apos;cat&apos;: &apos;kitten&apos;, &apos;dog&apos;: &apos;puppy&apos;})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过字典给出的映射规则对数据进行处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    kitten
1     puppy
2       NaN
3       NaN
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser6.map(&apos;I am a {}&apos;.format, na_action=&apos;ignore&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：将指定字符串的&lt;code&gt;format&lt;/code&gt;方法作用到数据系列的数据上，忽略掉所有的空值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0       I am a cat
1       I am a dog
2              NaN
3    I am a rabbit
dtype: object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建一个新的&lt;code&gt;Series&lt;/code&gt;对象，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser7 = pd.Series([20, 21, 12],  index=[&apos;London&apos;, &apos;New York&apos;, &apos;Helsinki&apos;])
ser7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;London      20
New York    21
Helsinki    12
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser7.apply(np.square)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：将求平方的函数作用到数据系列的数据上，也可以将参数&lt;code&gt;np.square&lt;/code&gt;替换为&lt;code&gt;lambda x: x ** 2&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;London      400
New York    441
Helsinki    144
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser7.apply(lambda x, value: x - value, args=(5, ))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：上面&lt;code&gt;apply&lt;/code&gt;方法中的&lt;code&gt;lambda&lt;/code&gt;函数有两个参数，第一个参数是数据系列中的数据，而第二个参数需要我们传入，所以我们给&lt;code&gt;apply&lt;/code&gt;方法增加了&lt;code&gt;args&lt;/code&gt;参数，用于给&lt;code&gt;lambda&lt;/code&gt;函数的第二个参数传值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;London      15
New York    16
Helsinki     7
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;取头部值和排序&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;sort_index()&lt;/code&gt;和&lt;code&gt;sort_values()&lt;/code&gt;方法可以用于对索引和数据的排序，排序方法有一个名为&lt;code&gt;ascending&lt;/code&gt;的布尔类型参数，该参数用于控制排序的结果是升序还是降序；而名为&lt;code&gt;kind&lt;/code&gt;的参数则用来控制排序使用的算法，默认使用了&lt;code&gt;quicksort&lt;/code&gt;，也可以选择&lt;code&gt;mergesort&lt;/code&gt;或&lt;code&gt;heapsort&lt;/code&gt;；如果存在空值，那么可以用&lt;code&gt;na_position&lt;/code&gt;参数空值放在最前还是最后，默认是&lt;code&gt;last&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser8 = pd.Series(
    data=[35, 96, 12, 57, 25, 89], 
    index=[&apos;grape&apos;, &apos;banana&apos;, &apos;pitaya&apos;, &apos;apple&apos;, &apos;peach&apos;, &apos;orange&apos;]
)
ser8.sort_values()  # 按值从小到大排序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pitaya    12
peach     25
grape     35
apple     57
orange    89
banana    96
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser8.sort_index(ascending=False)  # 按索引从大到小排序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pitaya    12
peach     25
orange    89
grape     35
banana    96
apple     57
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要从&lt;code&gt;Series&lt;/code&gt;对象中找出元素中最大或最小的“Top-N”，我们不需要对所有的值进行排序的，可以使用&lt;code&gt;nlargest()&lt;/code&gt;和&lt;code&gt;nsmallest()&lt;/code&gt;方法来完成，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser8.nlargest(3)  # 值最大的3个
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;banana    96
orange    89
apple     57
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser8.nsmallest(2)  # 值最小的2个
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pitaya    12
peach     25
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;绘制图表&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Series&lt;/code&gt;对象有一个名为&lt;code&gt;plot&lt;/code&gt;的方法可以用来生成图表，如果选择生成折线图、饼图、柱状图等，默认会使用&lt;code&gt;Series&lt;/code&gt;对象的索引作为横坐标，使用&lt;code&gt;Series&lt;/code&gt;对象的数据作为纵坐标。下面我们创建一个&lt;code&gt;Series&lt;/code&gt;对象并基于它绘制柱状图，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

ser9 = pd.Series({&apos;Q1&apos;: 400, &apos;Q2&apos;: 520, &apos;Q3&apos;: 180, &apos;Q4&apos;: 380})
# 通过plot方法的kind指定图表类型为柱状图
ser9.plot(kind=&apos;bar&apos;)
# 定制纵轴的取值范围
plt.ylim(0, 600)
# 定制横轴刻度（旋转到0度）
plt.xticks(rotation=0)
# 为柱子增加数据标签
for i in range(ser9.size):
    plt.text(i, ser9[i] + 5, ser9[i], ha=&apos;center&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/30f5dc177f.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们也可以将其绘制为饼图，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# plot方法的kind参数指定了图表类型为饼图
# autopct会自动计算并显示百分比
# pctdistance用来控制百分比到圆心的距离
ser9.plot(kind=&apos;pie&apos;, autopct=&apos;%.1f%%&apos;, pctdistance=0.65)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a9441d33ec.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;73 深入浅出pandas-2&lt;/h2&gt;
&lt;p&gt;如果使用 pandas 做数据分析，那么&lt;code&gt;DataFrame&lt;/code&gt;一定是被使用得最多的类型，它可以用来保存和处理异质的二维数据。这里所谓的“异质”是指&lt;code&gt;DataFrame&lt;/code&gt;中每个列的数据类型不需要相同，这也是它区别于 NumPy 二维数组的地方。&lt;code&gt;DataFrame&lt;/code&gt;提供了极为丰富的属性和方法，帮助我们实现对数据的重塑、清洗、预处理、透视、呈现等一系列操作。&lt;/p&gt;
&lt;h4&gt;创建DataFrame对象&lt;/h4&gt;
&lt;h5&gt;通过二维数组创建DataFrame对象&lt;/h5&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scores = np.random.randint(60, 101, (5, 3))
courses = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
stu_ids = np.arange(1001, 1006)
df1 = pd.DataFrame(data=scores, columns=courses, index=stu_ids)
df1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        语文  数学  英语
1001    69    80    79
1002    71    60    100
1003    94    81    93
1004    88    88    67
1005    82    66    60
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;通过字典创建DataFrame对象&lt;/h5&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scores = {
    &apos;语文&apos;: [62, 72, 93, 88, 93],
    &apos;数学&apos;: [95, 65, 86, 66, 87],
    &apos;英语&apos;: [66, 75, 82, 69, 82],
}
stu_ids = np.arange(1001, 1006)
df2 = pd.DataFrame(data=scores, index=stu_ids)
df2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        语文  数学  英语
1001    62    95    66
1002    72    65    75
1003    93    86    82
1004    88    66    69
1005    93    87    82
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;读取CSV文件创建DataFrame对象&lt;/h5&gt;
&lt;p&gt;可以通过&lt;code&gt;pandas&lt;/code&gt; 模块的&lt;code&gt;read_csv&lt;/code&gt;函数来读取 CSV 文件，&lt;code&gt;read_csv&lt;/code&gt;函数的参数非常多，下面介绍几个比较重要的参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sep&lt;/code&gt; / &lt;code&gt;delimiter&lt;/code&gt;：分隔符，默认是&lt;code&gt;,&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;header&lt;/code&gt;：表头（列索引）的位置，默认值是&lt;code&gt;infer&lt;/code&gt;，用第一行的内容作为表头（列索引）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index_col&lt;/code&gt;：用作行索引（标签）的列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usecols&lt;/code&gt;：需要加载的列，可以使用序号或者列名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;true_values&lt;/code&gt; / &lt;code&gt;false_values&lt;/code&gt;：哪些值被视为布尔值&lt;code&gt;True&lt;/code&gt; / &lt;code&gt;False&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skiprows&lt;/code&gt;：通过行号、索引或函数指定需要跳过的行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skipfooter&lt;/code&gt;：要跳过的末尾行数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nrows&lt;/code&gt;：需要读取的行数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;na_values&lt;/code&gt;：哪些值被视为空值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iterator&lt;/code&gt;：设置为&lt;code&gt;True&lt;/code&gt;，函数返回迭代器对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chunksize&lt;/code&gt;：配合上面的参数，设置每次迭代获取的数据体量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df3 = pd.read_csv(&apos;data/2018年北京积分落户数据.csv&apos;, index_col=&apos;id&apos;)
df3
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码中的CSV文件是用相对路径进行获取的，也就是说当前工作路径下有名为&lt;code&gt;data&lt;/code&gt;的文件夹，而“2018年北京积分落户数据.csv”就在这个文件夹下。如果使用Windows系统，在写路径分隔符时也建议使用&lt;code&gt;/&lt;/code&gt;而不是&lt;code&gt;\&lt;/code&gt;，如果想使用&lt;code&gt;\&lt;/code&gt;，建议在字符串前面添加一个&lt;code&gt;r&lt;/code&gt;，使用原始字符串来避开转义字符，例如&lt;code&gt;r&apos;c:\new\data\2018年北京积分落户数据.csv&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      name   birthday      company          score
id                                             
1     杨xx   1972-12       北京利德华福xxxx  122.59
2     纪xx   1974-12       北京航天数据xxxx  121.25
3     王x    1974-05       品牌联盟(北京)xx  118.96
4     杨x    1975-07       中科专利商标xxxx  118.21
5     张xx   1974-11       北京阿里巴巴xxxx  117.79
...   ...      ...                  ...     ...
6015  孙xx   1978-08       华为海洋网络xxxx   90.75
6016  刘xx   1976-11       福斯（上海）xxxx   90.75
6017  周x    1977-10       赢创德固赛xxxxxx   90.75
6018  赵x    1979-07       澳科利耳医疗xxxx   90.75
6019  贺x    1981-06       北京宝洁技术xxxx   90.75

[6019 rows x 4 columns]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;： 上面输出的内容隐去了姓名（name）和公司名称（company）字段中的部分信息。如果需要上面例子中的 CSV 文件，可以通过百度云盘获取，链接：&lt;a href=&quot;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&quot;&gt;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&lt;/a&gt;，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;读取Excel工作表创建DataFrame对象&lt;/h5&gt;
&lt;p&gt;可以通过&lt;code&gt;pandas&lt;/code&gt; 模块的&lt;code&gt;read_excel&lt;/code&gt;函数来读取 Excel 文件，该函数与上面的&lt;code&gt;read_csv&lt;/code&gt;非常类似，多了一个&lt;code&gt;sheet_name&lt;/code&gt;参数来指定数据表的名称，但是不同于 CSV 文件，没有&lt;code&gt;sep&lt;/code&gt;或&lt;code&gt;delimiter&lt;/code&gt;这样的参数。假设有名为“2022年股票数据.xlsx”的 Excel 文件，里面有用股票代码命名的五个表单，分别是阿里巴巴（BABA）、百度（BIDU）、京东（JD）、亚马逊（AMZN）、甲骨文（ORCL）这五个公司2022年的股票数据，如果想加载亚马逊的股票数据，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df4 = pd.read_excel(&apos;data/2022年股票数据.xlsx&apos;, sheet_name=&apos;AMZN&apos;, index_col=&apos;Date&apos;)
df4
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面例子中的 CSV 文件可以通过百度云盘获取，链接：&lt;a href=&quot;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&quot;&gt;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&lt;/a&gt;，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;               Open     High       Low    Close    Volume
Date                                                     
2022-12-30   83.120   84.050   82.4700   84.000  62401194
2022-12-29   82.870   84.550   82.5500   84.180  54995895
2022-12-28   82.800   83.480   81.6900   81.820  58228575
2022-12-27   84.970   85.350   83.0000   83.040  57284035
2022-12-23   83.250   85.780   82.9344   85.250  57433655
...             ...      ...       ...      ...       ...
2022-01-07  163.839  165.243  162.0310  162.554  46605900
2022-01-06  163.450  164.800  161.9370  163.254  51957780
2022-01-05  166.883  167.126  164.3570  164.357  64302720
2022-01-04  170.438  171.400  166.3490  167.522  70725160
2022-01-03  167.550  170.704  166.1600  170.404  63869140

[251 rows x 5 columns]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;读取关系数据库二维表创建DataFrame对象&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;pandas&lt;/code&gt;模块的&lt;code&gt;read_sql&lt;/code&gt;函数可以通过 SQL 语句从数据库中读取数据创建&lt;code&gt;DataFrame&lt;/code&gt;对象，该函数的第二个参数代表了需要连接的数据库。对于 MySQL 数据库，我们可以通过&lt;code&gt;pymysql&lt;/code&gt;或&lt;code&gt;mysqlclient&lt;/code&gt;来创建数据库连接（需要提前安装好三方库），得到一个&lt;code&gt;Connection&lt;/code&gt; 对象，而这个对象就是&lt;code&gt;read_sql&lt;/code&gt;函数需要的第二个参数，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

# 创建一个MySQL数据库的连接对象
conn = pymysql.connect(
    host=&apos;101.42.16.8&apos;, port=3306,
    user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
    database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;
)
# 通过SQL从数据库二维表读取数据创建DataFrame
df5 = pd.read_sql(&apos;select * from tb_emp&apos;, conn, index_col=&apos;eno&apos;)
df5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：执行上面的代码需要先安装&lt;code&gt;pymysql&lt;/code&gt;库，如果尚未安装，可以先在单元格中先执行魔法指令&lt;code&gt;%pip install pymysql&lt;/code&gt;，然后再运行上面的代码。上面的代码连接的是我部署在腾讯云上的 MySQL 数据库，公网 IP 地址：&lt;code&gt;101.42.16.8&lt;/code&gt;，用户名：&lt;code&gt;guest&lt;/code&gt;，密码：&lt;code&gt;Guest.618&lt;/code&gt;，数据库：&lt;code&gt;hrs&lt;/code&gt;，字符集：&lt;code&gt;utf8mb4&lt;/code&gt;，大家可以使用这个数据库，但是不要进行恶意的访问。&lt;code&gt;hrs&lt;/code&gt;数据库一共有三张表，分别是：&lt;code&gt;tb_dept&lt;/code&gt;（部门表）、&lt;code&gt;tb_emp&lt;/code&gt;（员工表）、&lt;code&gt;tb_emp2&lt;/code&gt;（员工表2）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ename   job     mgr     sal    comm    dno
eno                                        
1359   胡一刀   销售员  3344.0  1800   200.0   30
2056    乔峰   分析师   7800.0  5000  1500.0   20
3088   李莫愁   设计师  2056.0  3500   800.0   20
3211   张无忌   程序员  2056.0  3200     NaN   20
3233   丘处机   程序员  2056.0  3400     NaN   20
3244   欧阳锋   程序员  3088.0  3200     NaN   20
3251   张翠山   程序员  2056.0  4000     NaN   20
3344    黄蓉  销售主管  7800.0  3000   800.0   30
3577    杨过    会计    5566.0  2200     NaN   10
3588   朱九真    会计   5566.0  2500     NaN   10
4466   苗人凤   销售员  3344.0  2500     NaN   30
5234    郭靖    出纳    5566.0  2000     NaN   10
5566   宋远桥   会计师  7800.0  4000  1000.0   10
7800   张三丰    总裁     NaN   9000  1200.0   20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码会出现一个警告，因为 pandas 库希望我们使用&lt;code&gt;SQLAlchemy&lt;/code&gt;三方库接入数据库，具体内容是：“UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.”。如果不想看到这个警告，我们可以试一试下面的解决方案。&lt;/p&gt;
&lt;p&gt;首先，安装三方库&lt;code&gt;SQLAlchemy&lt;/code&gt;，在 Jupyter 中可以使用&lt;code&gt;%pip&lt;/code&gt;魔法指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install sqlalchemy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;SQLAlchemy&lt;/code&gt;的&lt;code&gt;create_engine&lt;/code&gt;函数创建&lt;code&gt;Engine&lt;/code&gt;对象作为&lt;code&gt;read_sql&lt;/code&gt;函数的第二个参数，此时&lt;code&gt;read_sql&lt;/code&gt;函数的第一个参数可以是 SQL 语句，也可以是二维表的表名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sqlalchemy import create_engine

# 通过指定的URL（统一资源定位符）访问数据库
engine = create_engine(&apos;mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs&apos;)
# 直接通过表名加载整张表的数据
df5 = pd.read_sql(&apos;tb_emp&apos;, engine, index_col=&apos;eno&apos;)
df5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果通过表名加载二维表数据，也可以将上面的函数换成&lt;code&gt;read_sql_table&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们再来加载部门表的数据创建&lt;code&gt;DataFrame&lt;/code&gt;对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df6 = pd.read_sql(&apos;select dno, dname, dloc from tb_dept&apos;, engine, index_col=&apos;dno&apos;)
df6
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果通过 SQL 查询获取数据，也可以将上面的函数换成&lt;code&gt;read_sql_query&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      dname   dloc
dno           
10    会计部   北京
20    研发部   成都
30    销售部   重庆
40    运维部   深圳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完成数据加载后，如果希望释放数据库连接，可以使用下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;engine.connect().close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;基本属性和方法&lt;/h4&gt;
&lt;p&gt;在开始讲解&lt;code&gt;DataFrame&lt;/code&gt;的属性和方法前，我们先从之前提到的&lt;code&gt;hrs&lt;/code&gt;数据库中读取三张表的数据，创建出三个&lt;code&gt;DataFrame&lt;/code&gt;对象，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sqlalchemy import create_engine

engine = create_engine(&apos;mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs&apos;)
dept_df = pd.read_sql_table(&apos;tb_dept&apos;, engine, index_col=&apos;dno&apos;)
emp_df = pd.read_sql_table(&apos;tb_emp&apos;, engine, index_col=&apos;eno&apos;)
emp2_df = pd.read_sql_table(&apos;tb_emp2&apos;, engine, index_col=&apos;eno&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到的三个&lt;code&gt;DataFrame&lt;/code&gt;对象如下所示。&lt;/p&gt;
&lt;p&gt;部门表（&lt;code&gt;dept_df&lt;/code&gt;），其中&lt;code&gt;dno&lt;/code&gt;是部门的编号，&lt;code&gt;dname&lt;/code&gt;和&lt;code&gt;dloc&lt;/code&gt;分别是部门的名称和所在地。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    dname  dloc
dno
10  会计部  北京
20  研发部  成都
30  销售部  重庆
40  运维部  深圳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;员工表（&lt;code&gt;emp_df&lt;/code&gt;），其中&lt;code&gt;eno&lt;/code&gt;是员工编号，&lt;code&gt;ename&lt;/code&gt;、&lt;code&gt;job&lt;/code&gt;、&lt;code&gt;mgr&lt;/code&gt;、&lt;code&gt;sal&lt;/code&gt;、&lt;code&gt;comm&lt;/code&gt;和&lt;code&gt;dno&lt;/code&gt;分别代表员工的姓名、职位、主管编号、月薪、补贴和部门编号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
1359    胡一刀    销售员      3344.0    1800    200.0    30
2056    乔峰      分析师      7800.0    5000    1500.0   20
3088    李莫愁    设计师      2056.0    3500    800.0    20
3211    张无忌     程序员     2056.0    3200    NaN     20
3233    丘处机     程序员     2056.0    3400    NaN     20
3244    欧阳锋     程序员     3088.0    3200    NaN     20
3251    张翠山     程序员     2056.0    4000    NaN     20
3344    黄蓉      销售主管    7800.0    3000    800.0   30
3577    杨过      会计       5566.0    2200     NaN     10
3588    朱九真     会计       5566.0    2500    NaN     10
4466    苗人凤     销售员     3344.0    2500    NaN     30
5234    郭靖       出纳      5566.0    2000    NaN      10
5566    宋远桥     会计师     7800.0    4000    1000.0   10
7800    张三丰     总裁       NaN      9000    1200.0    20
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在数据库中&lt;code&gt;mgr&lt;/code&gt;和&lt;code&gt;comm&lt;/code&gt;两个列的数据类型是&lt;code&gt;int&lt;/code&gt;，但是因为有缺失值（空值），读取到&lt;code&gt;DataFrame&lt;/code&gt;之后，列的数据类型变成了&lt;code&gt;float&lt;/code&gt;，因为我们通常会用&lt;code&gt;float&lt;/code&gt;类型的&lt;code&gt;NaN&lt;/code&gt;来表示空值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;员工表（&lt;code&gt;emp2_df&lt;/code&gt;），跟上面的员工表结构相同，但是保存了不同的员工数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ename    job      mgr      sal    comm    dno
eno                                      
9500   张三丰   总裁      NaN      50000  8000    20
9600   王大锤   程序员    9800.0   8000   600     20
9700   张三丰   总裁      NaN      60000  6000    20
9800   骆昊     架构师    7800.0   30000  5000    20
9900   陈小刀   分析师    9800.0   10000  1200    20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的属性如下表所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;at&lt;/code&gt; / &lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过标签获取&lt;code&gt;DataFrame&lt;/code&gt;中的单个值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;columns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象列的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dtypes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象每一列的数据类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;empty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象是否为空&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loc&lt;/code&gt; / &lt;code&gt;iloc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过标签获取&lt;code&gt;DataFrame&lt;/code&gt;中的一组值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ndim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的维度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的形状（行数和列数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象中元素的个数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的数据对应的二维数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关于&lt;code&gt;DataFrame&lt;/code&gt;的方法，首先需要了解的是&lt;code&gt;info()&lt;/code&gt;方法，它可以帮助我们了解&lt;code&gt;DataFrame&lt;/code&gt;的相关信息，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
Int64Index: 14 entries, 1359 to 7800
Data columns (total 6 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   ename   14 non-null     object 
 1   job     14 non-null     object 
 2   mgr     13 non-null     float64
 3   sal     14 non-null     int64  
 4   comm    6 non-null      float64
 5   dno     14 non-null     int64  
dtypes: float64(2), int64(2), object(2)
memory usage: 1.3+ KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要查看&lt;code&gt;DataFrame&lt;/code&gt;的头部或尾部的数据，可以使用&lt;code&gt;head()&lt;/code&gt;或&lt;code&gt;tail()&lt;/code&gt;方法，这两个方法的默认参数是&lt;code&gt;5&lt;/code&gt;，表示获取&lt;code&gt;DataFrame&lt;/code&gt;最前面5行或最后面5行的数据，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job    mgr    sal    comm  dno
eno
1359    胡一刀   销售员   3344   1800   200   30
2056    乔峰     分析师   7800   5000   1500  20
3088    李莫愁    设计师   2056   3500  800   20
3211    张无忌    程序员   2056   3200  NaN   20
3233    丘处机    程序员   2056   3400  NaN   20
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;操作数据&lt;/h4&gt;
&lt;h5&gt;索引和切片&lt;/h5&gt;
&lt;p&gt;如果要获取&lt;code&gt;DataFrame&lt;/code&gt;的某一列，例如取出上面&lt;code&gt;emp_df&lt;/code&gt;的&lt;code&gt;ename&lt;/code&gt;列，可以使用下面的两种方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.ename
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df[&apos;ename&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码可以发现，我们获得的是一个&lt;code&gt;Series&lt;/code&gt;对象。事实上，&lt;code&gt;DataFrame&lt;/code&gt;对象就是将多个&lt;code&gt;Series&lt;/code&gt;对象组合到一起的结果。&lt;/p&gt;
&lt;p&gt;如果要获取&lt;code&gt;DataFrame&lt;/code&gt;的某一行，可以使用整数索引或我们设置的索引，例如取出员工编号为&lt;code&gt;2056&lt;/code&gt;的员工数据，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.iloc[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[2056]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过执行上面的代码我们发现，单独取&lt;code&gt;DataFrame&lt;/code&gt; 的某一行或某一列得到的都是&lt;code&gt;Series&lt;/code&gt;对象。我们当然也可以通过花式索引来获取多个行或多个列的数据，花式索引的结果仍然是一个&lt;code&gt;DataFrame&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;获取多个列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df[[&apos;ename&apos;, &apos;job&apos;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取多个行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[[2056, 7800, 3344]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要获取或修改&lt;code&gt;DataFrame&lt;/code&gt; 对象某个单元格的数据，需要同时指定行和列的索引，例如要获取员工编号为&lt;code&gt;2056&lt;/code&gt;的员工的职位信息，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df[&apos;job&apos;][2056]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[2056][&apos;job&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[2056, &apos;job&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们推荐大家使用第三种做法，因为它只做了一次索引运算。如果要将该员工的职位修改为“架构师”，可以使用下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[2056, &apos;job&apos;] = &apos;架构师&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，我们也可以通过切片操作来获取多行多列，相信大家一定已经想到了这一点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.loc[2056:3344]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
2056    乔峰      分析师     7800.0    5000    1500.0   20
3088    李莫愁    设计师     2056.0    3500    800.0    20
3211    张无忌    程序员     2056.0    3200    NaN      20
3233    丘处机    程序员     2056.0    3400    NaN      20
3244    欧阳锋    程序员     3088.0    3200    NaN      20
3251    张翠山    程序员     2056.0    4000    NaN      20
3344    黄蓉     销售主管    7800.0    3000    800.0    30
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数据筛选&lt;/h5&gt;
&lt;p&gt;上面我们提到了花式索引，相信大家已经联想到了布尔索引。跟&lt;code&gt;ndarray&lt;/code&gt;和&lt;code&gt;Series&lt;/code&gt;一样，我们可以通过布尔索引对&lt;code&gt;DataFrame&lt;/code&gt;对象进行数据筛选，例如我们要从&lt;code&gt;emp_df&lt;/code&gt;中筛选出月薪超过&lt;code&gt;3500&lt;/code&gt;的员工，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df[emp_df.sal &amp;gt; 3500]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
2056    乔峰      分析师      7800.0   5000    1500.0    20
3251    张翠山    程序员      2056.0    4000    NaN      20
5566    宋远桥    会计师      7800.0    4000    1000.0   10
7800    张三丰    总裁        NaN      9000    1200.0    20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，我们也可以组合多个条件来进行数据筛选，例如从&lt;code&gt;emp_df&lt;/code&gt;中筛选出月薪超过&lt;code&gt;3500&lt;/code&gt;且部门编号为&lt;code&gt;20&lt;/code&gt;的员工，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df[(emp_df.sal &amp;gt; 3500) &amp;amp; (emp_df.dno == 20)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
2056    乔峰      分析师      7800.0   5000   1500.0    20
3251    张翠山     程序员     2056.0   4000    NaN       20
7800    张三丰     总裁       NaN      9000   1200.0    20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了使用布尔索引，&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;query&lt;/code&gt;方法也可以实现数据筛选，&lt;code&gt;query&lt;/code&gt;方法的参数是一个字符串，它代表了筛选数据使用的表达式，而且更符合 Python 程序员的使用习惯。下面我们使用&lt;code&gt;query&lt;/code&gt;方法将上面的效果重新实现一遍，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.query(&apos;sal &amp;gt; 3500 and dno == 20&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;74 深入浅出pandas-3&lt;/h2&gt;
&lt;p&gt;在完成数据加载之后，我们可能需要对事实表和维度表进行连接，这是对数据进行多维度拆解的基础；我们可能从不同的数据源加载了结构相同的数据，我们需要将这些数据拼接起来；我们把这些操作统称为数据重塑。当然，由于企业的信息化水平以及数据中台建设水平的差异，我们拿到的数据未必是质量很好的，可能还需要对数据中的缺失值、重复值、异常值进行适当的处理。即便我们获取的数据在质量上是没有问题的，但也可能需要对数据进行一系列的预处理，才能满足我们做数据分析的需求。接下来，我们就为大家讲解和梳理这方面的知识。&lt;/p&gt;
&lt;h4&gt;数据重塑&lt;/h4&gt;
&lt;p&gt;有的时候，我们做数据分析需要的原始数据可能并不是来自一个地方，就像上一章的例子中，我们从关系型数据库中读取了三张表，得到了三个&lt;code&gt;DataFrame&lt;/code&gt;对象，但实际工作可能需要我们把他们的数据整合到一起。例如：&lt;code&gt;emp_df&lt;/code&gt;和&lt;code&gt;emp2_df&lt;/code&gt;其实都是员工的数据，而且数据结构完全一致，我们可以使用&lt;code&gt;pandas&lt;/code&gt;提供的&lt;code&gt;concat&lt;/code&gt;函数实现两个或多个&lt;code&gt;DataFrame&lt;/code&gt;的数据拼接，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;all_emp_df = pd.concat([emp_df, emp2_df])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
1359    胡一刀    销售员	   3344.0	1800	200.0	30
2056    乔峰	    分析师	    7800.0	 5000	 1500.0	 20
3088    李莫愁	   设计师	   2056.0	3500	800.0	20
3211    张无忌	   程序员	   2056.0	3200	NaN     20
3233    丘处机	   程序员	   2056.0	3400	NaN	    20
3244    欧阳锋	   程序员	   3088.0	3200	NaN     20
3251    张翠山	   程序员	   2056.0	4000	NaN	    20
3344    黄蓉	    销售主管   7800.0	3000	800.0	30
3577    杨过	    会计	     5566.0	  2200	  NaN	  10
3588    朱九真	   会计	    5566.0	 2500	 NaN	 10
4466    苗人凤	   销售员	   3344.0	2500	NaN	    30
5234    郭靖	    出纳	     5566.0	  2000	  NaN	  10
5566    宋远桥	   会计师	   7800.0	4000	1000.0	10
7800    张三丰	   总裁	    NaN      9000	 1200.0	 20
9500	张三丰	   总裁	    NaN	     50000	 8000.0	 20
9600	王大锤    程序员	   9800.0	8000	600.0	20
9700	张三丰	   总裁	    NaN	     60000	 6000.0	 20
9800	骆昊	    架构师	    7800.0	 30000	 5000.0	 20
9900	陈小刀	   分析师	   9800.0	10000	1200.0	20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码将两个代表员工数据的&lt;code&gt;DataFrame&lt;/code&gt;拼接到了一起，接下来我们使用&lt;code&gt;merge&lt;/code&gt;函数将员工表和部门表的数据合并到一张表中，代码如下所示。&lt;/p&gt;
&lt;p&gt;先使用&lt;code&gt;reset_index&lt;/code&gt;方法重新设置&lt;code&gt;all_emp_df&lt;/code&gt;的索引，这样&lt;code&gt;eno&lt;/code&gt; 不再是索引而是一个普通列，&lt;code&gt;reset_index&lt;/code&gt;方法的&lt;code&gt;inplace&lt;/code&gt;参数设置为&lt;code&gt;True&lt;/code&gt;表示，重置索引的操作直接在&lt;code&gt;all_emp_df&lt;/code&gt;上执行，而不是返回修改后的新对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;all_emp_df.reset_index(inplace=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;merge&lt;/code&gt;函数合并数据，当然，也可以调用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;merge&lt;/code&gt;方法来达到同样的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.merge(all_emp_df, dept_df, how=&apos;inner&apos;, on=&apos;dno&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    eno	    ename	job	     mgr	 sal	 comm	 dno	dname	 dloc
0	1359	胡一刀	 销售员	3344.0	1800	200.0	30	   销售部	 重庆
1	3344	黄蓉	  销售主管	7800.0	3000	800.0	30	   销售部	 重庆
2	4466	苗人凤	 销售员	3344.0	2500	NaN	    30	   销售部	 重庆
3	2056	乔峰	  分析师	 7800.0	 5000	 1500.0	 20	    研发部	  成都
4	3088	李莫愁	 设计师	2056.0	3500	800.0	20	   研发部	 成都
5	3211	张无忌  程序员	2056.0	3200	NaN	    20	   研发部	 成都
6	3233	丘处机	 程序员	2056.0	3400	NaN	    20	   研发部	 成都
7	3244	欧阳锋	 程序员	3088.0	3200	NaN	    20	   研发部	 成都
8	3251	张翠山	 程序员	2056.0	4000	NaN	    20	   研发部	 成都
9	7800	张三丰	 总裁	     NaN	 9000	 1200.0	 20	    研发部	  成都
10	9500	张三丰	 总裁	     NaN	 50000	 8000.0	 20	    研发部	  成都
11	9600	王大锤	 程序员	9800.0	8000	600.0	20	   研发部	 成都
12	9700	张三丰	 总裁	     NaN	 60000	 6000.0	 20	    研发部	  成都
13	9800	骆昊	  架构师	 7800.0	 30000	 5000.0	 20	    研发部	  成都
14	9900	陈小刀	 分析师	9800.0	10000	1200.0	20	   研发部	 成都
15	3577	杨过	  会计	  5566.0  2200	  NaN	  10	会计部	  北京
16	3588	朱九真	 会计	     5566.0	 2500	 NaN	 10	   会计部	 北京
17	5234	郭靖	  出纳	  5566.0  2000	  NaN	  10	会计部	  北京
18	5566	宋远桥	 会计师	7800.0	4000	1000.0	10	  会计部	北京
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;merge&lt;/code&gt;函数的一个参数代表合并的左表、第二个参数代表合并的右表，有SQL编程经验的同学对这两个词是不是感觉到非常亲切。正如大家猜想的那样，&lt;code&gt;DataFrame&lt;/code&gt;对象的合并跟数据库中的表连接非常类似，所以上面代码中的&lt;code&gt;how&lt;/code&gt;代表了合并两张表的方式，有&lt;code&gt;left&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt;、&lt;code&gt;inner&lt;/code&gt;、&lt;code&gt;outer&lt;/code&gt;四个选项；而&lt;code&gt;on&lt;/code&gt;则代表了基于哪个列实现表的合并，相当于 SQL 表连接中的连表条件，如果左右两表对应的列列名不同，可以用&lt;code&gt;left_on&lt;/code&gt;和&lt;code&gt;right_on&lt;/code&gt;参数取代&lt;code&gt;on&lt;/code&gt;参数分别进行指定。&lt;/p&gt;
&lt;p&gt;如果对上面的代码稍作修改，将&lt;code&gt;how&lt;/code&gt;参数修改为&lt;code&gt;&apos;right&apos;&lt;/code&gt;，大家可以思考一下代码执行的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.merge(all_emp_df, dept_df, how=&apos;right&apos;, on=&apos;dno&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果比之前的输出多出了如下所示的一行，这是因为&lt;code&gt;how=&apos;right&apos;&lt;/code&gt;代表右外连接，也就意味着右表&lt;code&gt;dept_df&lt;/code&gt;中的数据会被完整的查出来，但是在&lt;code&gt;all_emp_df&lt;/code&gt;中又没有编号为&lt;code&gt;40&lt;/code&gt; 部门的员工，所以对应的位置都被填入了空值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;19	NaN    NaN    NaN    NaN    NaN     NaN    40    运维部    深圳
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据清洗&lt;/h4&gt;
&lt;p&gt;通常，我们从 Excel、CSV 或数据库中获取到的数据并不是非常完美的，里面可能因为系统或人为的原因混入了重复值或异常值，也可能在某些字段上存在缺失值；再者，&lt;code&gt;DataFrame&lt;/code&gt;中的数据也可能存在格式不统一、量纲不统一等各种问题。因此，在开始数据分析之前，对数据进行清洗就显得特别重要。&lt;/p&gt;
&lt;h5&gt;缺失值&lt;/h5&gt;
&lt;p&gt;可以使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;isnull&lt;/code&gt;或&lt;code&gt;isna&lt;/code&gt;方法来找出数据表中的缺失值，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.isnull()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.isna()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename   job	    mgr     sal     comm    dno
eno						
1359	False	False	False	False	False	False
2056	False	False	False	False	False	False
3088	False	False	False	False	False	False
3211	False	False	False	False	True	False
3233	False	False	False	False	True	False
3244	False	False	False	False	True	False
3251	False	False	False	False	True	False
3344	False	False	False	False	False	False
3577	False	False	False	False	True	False
3588	False	False	False	False	True	False
4466	False	False	False	False	True	False
5234	False	False	False	False	True	False
5566	False	False	False	False	False	False
7800	False	False	True	False	False	False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相对应的，&lt;code&gt;notnull&lt;/code&gt;和&lt;code&gt;notna&lt;/code&gt;方法可以将非空的值标记为&lt;code&gt;True&lt;/code&gt;。如果想删除这些缺失值，可以使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;dropna&lt;/code&gt;方法，该方法的&lt;code&gt;axis&lt;/code&gt;参数可以指定沿着0轴还是1轴删除，也就是说当遇到空值时，是删除整行还是删除整列，默认是沿0轴进行删除的，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.dropna()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename   job      mgr	 sal    comm     dno
eno						
1359	胡一刀  销售员	3344.0	1800   200.0	30
2056	乔峰    架构师	 7800.0	 5000	1500.0	 20
3088	李莫愁  设计师	2056.0	3500   800.0	20
3344	黄蓉    销售主管	7800.0	3000   800.0	30
5566	宋远桥  会计师	7800.0	4000   1000.0	10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要沿着1轴进行删除，可以使用下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.dropna(axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job      sal    dno
eno				
1359	胡一刀   销售员    1800	30
2056	乔峰     架构师	  5000	 20
3088	李莫愁   设计师    3500	20
3211	张无忌   程序员    3200	20
3233	丘处机   程序员    3400	20
3244	欧阳锋   程序员    3200	20
3251	张翠山   程序员    4000	20
3344	黄蓉     销售主管  3000	30
3577	杨过     会计	   2200	  10
3588	朱九真   会计	  2500	 10
4466	苗人凤   销售员	 2500   30
5234	郭靖     出纳      2000   10
5566	宋远桥   会计师    4000   10
7800	张三丰   总裁      9000   20
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;DataFrame&lt;/code&gt;对象的很多方法都有一个名为&lt;code&gt;inplace&lt;/code&gt;的参数，该参数的默认值为&lt;code&gt;False&lt;/code&gt;，表示我们的操作不会修改原来的&lt;code&gt;DataFrame&lt;/code&gt;对象，而是将处理后的结果通过一个新的&lt;code&gt;DataFrame&lt;/code&gt;对象返回。如果将该参数的值设置为&lt;code&gt;True&lt;/code&gt;，那么我们的操作就会在原来的&lt;code&gt;DataFrame&lt;/code&gt;上面直接修改，方法的返回值为&lt;code&gt;None&lt;/code&gt;。简单的说，上面的操作并没有修改&lt;code&gt;emp_df&lt;/code&gt;，而是返回了一个新的&lt;code&gt;DataFrame&lt;/code&gt;对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在某些特定的场景下，我们可以对空值进行填充，对应的方法是&lt;code&gt;fillna&lt;/code&gt;，填充空值时可以使用指定的值（通过&lt;code&gt;value&lt;/code&gt;参数进行指定），也可以用表格中前一个单元格（通过设置参数&lt;code&gt;method=ffill&lt;/code&gt;）或后一个单元格（通过设置参数&lt;code&gt;method=bfill&lt;/code&gt;）的值进行填充，当代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.fillna(value=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：填充的值如何选择也是一个值得探讨的话题，实际工作中，可能会使用某种统计量（如：均值、众数等）进行填充，或者使用某种插值法（如：随机插值法、拉格朗日插值法等）进行填充，甚至有可能通过回归模型、贝叶斯模型等对缺失数据进行填充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ename    job        mgr      sal     comm    dno
eno
1359	胡一刀    销售员	   3344.0	1800	200.0	30
2056	乔峰	    分析师	    7800.0	 5000	 1500.0	 20
3088	李莫愁	   设计师	   2056.0	3500	800.0	20
3211	张无忌	   程序员	   2056.0	3200	0.0     20
3233	丘处机	   程序员	   2056.0	3400	0.0	    20
3244	欧阳锋	   程序员	   3088.0	3200	0.0     20
3251	张翠山	   程序员	   2056.0	4000	0.0	    20
3344	黄蓉	    销售主管   7800.0	3000	800.0	30
3577	杨过	    会计	     5566.0	  2200	  0.0	  10
3588	朱九真	   会计	    5566.0	 2500	 0.0	 10
4466	苗人凤	   销售员	   3344.0	2500	0.0	    30
5234	郭靖	    出纳	     5566.0	  2000	  0.0	  10
5566	宋远桥	   会计师	   7800.0	4000	1000.0	10
7800	张三丰	   总裁	    0.0      9000	 1200.0	 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;重复值&lt;/h5&gt;
&lt;p&gt;接下来，我们先给之前的部门表添加两行数据，让部门表中名为“研发部”和“销售部”的部门各有两个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dept_df.loc[50] = {&apos;dname&apos;: &apos;研发部&apos;, &apos;dloc&apos;: &apos;上海&apos;}
dept_df.loc[60] = {&apos;dname&apos;: &apos;销售部&apos;, &apos;dloc&apos;: &apos;长沙&apos;}
dept_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    dname  dloc
dno		
10	会计部	北京
20	研发部	成都
30	销售部	重庆
40	运维部	天津
50	研发部	上海
60	销售部	长沙
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们的数据表中有重复数据了，我们可以通过&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;duplicated&lt;/code&gt;方法判断是否存在重复值，该方法在不指定参数时默认判断行索引是否重复，我们也可以指定根据部门名称&lt;code&gt;dname&lt;/code&gt;判断部门是否重复，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dept_df.duplicated(&apos;dname&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dno
10    False
20    False
30    False
40    False
50     True
60     True
dtype: bool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的输出可以看到，&lt;code&gt;50&lt;/code&gt;和&lt;code&gt;60&lt;/code&gt;两个部门从部门名称上来看是重复的，如果要删除重复值，可以使用&lt;code&gt;drop_duplicates&lt;/code&gt;方法，该方法的&lt;code&gt;keep&lt;/code&gt;参数可以控制在遇到重复值时，保留第一项还是保留最后一项，或者多个重复项一个都不用保留，全部删除掉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dept_df.drop_duplicates(&apos;dname&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	dname	dloc
dno		
10	会计部	北京
20	研发部	成都
30	销售部	重庆
40	运维部	天津
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;code&gt;keep&lt;/code&gt;参数的值修改为&lt;code&gt;last&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dept_df.drop_duplicates(&apos;dname&apos;, keep=&apos;last&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	dname	dloc
dno		
10	会计部	北京
40	运维部	天津
50	研发部	上海
60	销售部	长沙
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用同样的方式，我们也可以清除&lt;code&gt;all_emp_df&lt;/code&gt;中的重复数据，例如我们认定“ename”和“job”两个字段完全相同的就是重复数据，我们可以用下面的代码去除重复数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;all_emp_df.drop_duplicates([&apos;ename&apos;, &apos;job&apos;], inplace=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;drop_duplicates&lt;/code&gt;方法添加了参数&lt;code&gt;inplace=True&lt;/code&gt;，该方法不会返回新的&lt;code&gt;DataFrame&lt;/code&gt;对象，而是在原来的&lt;code&gt;DataFrame&lt;/code&gt;对象上直接删除，大家可以查看&lt;code&gt;all_emp_df&lt;/code&gt;看看是不是已经移除了重复的员工数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;异常值&lt;/h5&gt;
&lt;p&gt;异常值在统计学上的全称是疑似异常值，也称作离群点（outlier），异常值的分析也称作离群点分析。异常值是指样本中出现的“极端值”，数据值看起来异常大或异常小，其分布明显偏离其余的观测值。实际工作中，有些异常值可能是由系统或人为原因造成的，但有些异常值却不是，它们能够重复且稳定的出现，属于正常的极端值，例如很多游戏产品中头部玩家的数据往往都是离群的极端值。所以，我们既不能忽视异常值的存在，也不能简单地把异常值从数据分析中剔除。重视异常值的出现，分析其产生的原因，常常成为发现问题进而改进决策的契机。&lt;/p&gt;
&lt;p&gt;异常值的检测有Z-score 方法、IQR 方法、DBScan 聚类、孤立森林等，这里我们对前两种方法做一个简单的介绍。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/1e8ef03d24.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果数据服从正态分布，依据3σ法则，异常值被定义与平均值的偏差超过三倍标准差的值。在正态分布下，距离平均值3σ之外的值出现的概率为 $\small{P(\lvert x - \mu \rvert \gt 3 \sigma) &amp;lt; 0.003}$ ，属于小概率事件。如果数据不服从正态分布，那么可以用远离均值的多少倍的标准差来描述，这里的倍数就是Z-score。Z-score以标准差为单位去度量某一原始分数偏离平均值的距离，公式如下所示。&lt;/p&gt;
&lt;p&gt;$$
z = \frac {X - \mu} {\sigma}
$$&lt;/p&gt;
&lt;p&gt;$$
\lvert z \rvert &amp;gt; 3
$$&lt;/p&gt;
&lt;p&gt;Z-score需要根据经验和实际情况来决定，通常把远离标准差 3 倍距离以上的数据点视为离群点，下面的代给出了如何通过Z-score方法检测异常值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def detect_outliers_zscore(data, threshold=3):
    avg_value = np.mean(data)
    std_value = np.std(data)
    z_score = np.abs((data - avg_value) / std_value)
    return data[z_score &amp;gt; threshold]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IQR 方法中的 IQR（Inter-Quartile Range）代表四分位距离，即上四分位数（Q3）和下四分位数（Q1）的差值。通常情况下，可以认为小于 $\small{Q1 - 1.5 \times IQR}$ 或大于 $\small{Q3 + 1.5 \times IQR}$ 的就是异常值，而这种检测异常值的方法也是箱线图（后面会讲到）默认使用的方法。下面的代码给出了如何通过 IQR 方法检测异常值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def detect_outliers_iqr(data, whis=1.5):
    q1, q3 = np.quantile(data, [0.25, 0.75])
    iqr = q3 - q1
    lower, upper = q1 - whis * iqr, q3 + whis * iqr
    return data[(data &amp;lt; lower) | (data &amp;gt; upper)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要删除异常值，可以使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;drop&lt;/code&gt;方法，该方法可以根据行索引或列索引删除指定的行或列。例如我们认为月薪低于&lt;code&gt;2000&lt;/code&gt;或高于&lt;code&gt;8000&lt;/code&gt;的是员工表中的异常值，可以用下面的代码删除对应的记录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emp_df.drop(emp_df[(emp_df.sal &amp;gt; 8000) | (emp_df.sal &amp;lt; 2000)].index)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要替换掉异常值，可以通过给单元格赋值的方式来实现，也可以使用&lt;code&gt;replace&lt;/code&gt;方法将指定的值替换掉。例如我们要将月薪为&lt;code&gt;1800&lt;/code&gt;和&lt;code&gt;9000&lt;/code&gt;的替换为月薪的平均值，补贴为&lt;code&gt;800&lt;/code&gt;的替换为&lt;code&gt;1000&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({&apos;sal&apos;: [1800, 9000], &apos;comm&apos;: 800}, {&apos;sal&apos;: avg_sal, &apos;comm&apos;: 1000})
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;预处理&lt;/h5&gt;
&lt;p&gt;对数据进行预处理也是一个很大的话题，它包含了对数据的拆解、变换、归约、离散化等操作。我们先来看看数据的拆解。如果数据表中的数据是一个时间日期，我们通常都需要从年、季度、月、日、星期、小时、分钟等维度对其进行拆解，如果时间日期是用字符串表示的，可以先通过&lt;code&gt;pandas&lt;/code&gt;的&lt;code&gt;to_datetime&lt;/code&gt;函数将其处理成时间日期。&lt;/p&gt;
&lt;p&gt;在下面的例子中，我们先读取 Excel 文件，获取到一组销售数据，其中第一列就是销售日期，我们将其拆解为“月份”、“季度”和“星期”，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_df = pd.read_excel(
    &apos;data/2020年销售数据.xlsx&apos;,
    usecols=[&apos;销售日期&apos;, &apos;销售区域&apos;, &apos;销售渠道&apos;, &apos;品牌&apos;, &apos;销售额&apos;]
)
sales_df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中使用了相对路径来获取 Excel 文件，也就是说 Excel 文件在当前工作路径下名为&lt;code&gt;data&lt;/code&gt;的文件夹中。如果需要上面例子中的 Excel 文件，可以通过下面的百度云盘地址进行获取。链接：&lt;a href=&quot;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&quot;&gt;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&lt;/a&gt;，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
RangeIndex: 1945 entries, 0 to 1944
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   销售日期    1945 non-null   datetime64[ns]
 1   销售区域    1945 non-null   object        
 2   销售渠道    1945 non-null   object        
 3   品牌        1945 non-null   object        
 4   销售额      1945 non-null   int64         
dtypes: datetime64[ns](1), int64(1), object(3)
memory usage: 76.1+ KB
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sales_df[&apos;月份&apos;] = sales_df[&apos;销售日期&apos;].dt.month
sales_df[&apos;季度&apos;] = sales_df[&apos;销售日期&apos;].dt.quarter
sales_df[&apos;星期&apos;] = sales_df[&apos;销售日期&apos;].dt.weekday
sales_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	    销售日期	 销售区域	销售渠道	品牌	  销售额	月份	季度	星期
0	    2020-01-01	上海	     拼多多	 八匹马   8217	    1	 1	   2
1	    2020-01-01	上海	     抖音	      八匹马	6351	 1	  1	    2
2	    2020-01-01	上海	     天猫	      八匹马	14365	 1	  1	    2
3	    2020-01-01	上海	     天猫       八匹马	2366	 1	  1     2
4	    2020-01-01	上海	     天猫 	  皮皮虾	15189	 1	  1     2
...     ...         ...        ...       ...      ...     ...  ...   ...
1940    2020-12-30	北京	     京东	      花花姑娘 6994     12	 4	   2
1941    2020-12-30	福建	     实体	      八匹马	7663	 12	  4	    2
1942    2020-12-31	福建	     实体	      花花姑娘 14795    12	 4	   3
1943    2020-12-31	福建	     抖音	      八匹马	3481	 12	  4	    3
1944    2020-12-31	福建	     天猫	      八匹马	2673	 12	  4	    3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，通过日期时间类型的&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;dt&lt;/code&gt; 属性，获得一个访问日期时间的对象，通过该对象的&lt;code&gt;year&lt;/code&gt;、&lt;code&gt;month&lt;/code&gt;、&lt;code&gt;quarter&lt;/code&gt;、&lt;code&gt;hour&lt;/code&gt;等属性，就可以获取到年、月、季度、小时等时间信息，获取到的仍然是一个&lt;code&gt;Series&lt;/code&gt;对象，它包含了一组时间信息，所以我们通常也将这个&lt;code&gt;dt&lt;/code&gt;属性称为“日期时间向量”。&lt;/p&gt;
&lt;p&gt;我们再来说一说字符串类型的数据的处理，我们先从指定的 Excel 文件中读取某招聘网站的招聘数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs_df = pd.read_csv(
    &apos;data/某招聘网站招聘数据.csv&apos;,
    usecols=[&apos;city&apos;, &apos;companyFullName&apos;, &apos;positionName&apos;, &apos;salary&apos;]
)
jobs_df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中使用了相对路径来获取 CSV 文件，也就是说 CSV 文件在当前工作路径下名为&lt;code&gt;data&lt;/code&gt;的文件夹中。如果需要上面例子中的 CSV 文件，可以通过下面的百度云盘地址进行获取。链接：&lt;a href=&quot;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&quot;&gt;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g&lt;/a&gt;，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
RangeIndex: 3140 entries, 0 to 3139
Data columns (total 4 columns):
 #   Column           Non-Null Count  Dtype 
---  ------           --------------  ----- 
 0   city             3140 non-null   object
 1   companyFullName  3140 non-null   object
 2   positionName     3140 non-null   object
 3   salary           3140 non-null   object
dtypes: object(4)
memory usage: 98.2+ KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看前&lt;code&gt;5&lt;/code&gt;条数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs_df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    city    companyFullName              positionName    salary
0   北京	  达疆网络科技（上海）有限公司    数据分析岗       15k-30k
1   北京	  北京音娱时光科技有限公司        数据分析        10k-18k
2   北京	  北京千喜鹤餐饮管理有限公司	     数据分析        20k-30k
3   北京	  吉林省海生电子商务有限公司	     数据分析        33k-50k
4   北京	  韦博网讯科技（北京）有限公司	数据分析        10k-15k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的数据表一共有&lt;code&gt;3140&lt;/code&gt;条数据，但并非所有的职位都是“数据分析”的岗位，如果要筛选出数据分析的岗位，可以通过检查&lt;code&gt;positionName&lt;/code&gt;字段是否包含“数据分析”这个关键词，这里需要模糊匹配，应该如何实现呢？我们可以先获取&lt;code&gt;positionName&lt;/code&gt;列，因为这个&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;dtype&lt;/code&gt;是字符串，所以可以通过&lt;code&gt;str&lt;/code&gt;属性获取对应的字符串向量，然后就可以利用我们熟悉的字符串的方法来对其进行操作，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs_df = jobs_df[jobs_df.positionName.str.contains(&apos;数据分析&apos;)]
jobs_df.shape
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(1515, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，筛选后的数据还有&lt;code&gt;1515&lt;/code&gt;条。接下来，我们还需要对&lt;code&gt;salary&lt;/code&gt;字段进行处理，如果我们希望统计所有岗位的平均工资或每个城市的平均工资，首先需要将用范围表示的工资处理成其中间值，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs_df.salary.str.extract(r&apos;(\d+)[kK]?-(\d+)[kK]?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码通过正则表达式捕获组从字符串中抽取出两组数字，分别对应工资的下限和上限，对正则表达式不熟悉的读者，可以阅读我的知乎专栏“从零开始学Python”中的&lt;a href=&quot;https://zhuanlan.zhihu.com/p/158929767&quot;&gt;《正则表达式的应用》&lt;/a&gt;一文。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        0     1
0	    15    30
1	    10	  18
2       20    30
3       33    50
4       10    15
...     ...   ...
3065    8     10
3069    6     10
3070    2     4
3071    6     12
3088    8     12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要提醒大家的是，抽取出来的两列数据都是字符串类型的值，我们需要将其转换成&lt;code&gt;int&lt;/code&gt;类型，才能计算平均值，对应的方法是&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;applymap&lt;/code&gt;方法，该方法的参数是一个函数，而该函数会作用于&lt;code&gt;DataFrame&lt;/code&gt;中的每个元素。完成这一步之后，我们就可以使用&lt;code&gt;apply&lt;/code&gt;方法将上面的&lt;code&gt;DataFrame&lt;/code&gt;处理成中间值，&lt;code&gt;apply&lt;/code&gt;方法的参数也是一个函数，可以通过指定&lt;code&gt;axis&lt;/code&gt;参数使其作用于&lt;code&gt;DataFrame&lt;/code&gt; 对象的行或列，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp_df = jobs_df.salary.str.extract(r&apos;(\d+)[kK]?-(\d+)[kK]?&apos;).applymap(int)
temp_df.apply(np.mean, axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0       22.5
1       14.0
2       25.0
3       41.5
4       12.5
        ... 
3065    9.0
3069    8.0
3070    3.0
3071    9.0
3088    10.0
Length: 1515, dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们可以用上面的结果替换掉原来的&lt;code&gt;salary&lt;/code&gt;列或者增加一个新的列来表示职位对应的工资，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp_df = jobs_df.salary.str.extract(r&apos;(\d+)[kK]?-(\d+)[kK]?&apos;).applymap(int)
jobs_df[&apos;salary&apos;] = temp_df.apply(np.mean, axis=1)
jobs_df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    city    companyFullName              positionName    salary
0   北京	  达疆网络科技（上海）有限公司    数据分析岗       22.5
1   北京	  北京音娱时光科技有限公司        数据分析        14.0
2   北京	  北京千喜鹤餐饮管理有限公司	     数据分析        25.0
3   北京	  吉林省海生电子商务有限公司	     数据分析        41.5
4   北京	  韦博网讯科技（北京）有限公司	数据分析        12.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;applymap&lt;/code&gt;和&lt;code&gt;apply&lt;/code&gt;两个方法在数据预处理的时候经常用到，&lt;code&gt;Series&lt;/code&gt;对象也有&lt;code&gt;apply&lt;/code&gt;方法，也是用于数据的预处理，但是&lt;code&gt;DataFrame&lt;/code&gt;对象还有一个名为&lt;code&gt;transform&lt;/code&gt; 的方法，也是通过传入的函数对数据进行变换，类似&lt;code&gt;Series&lt;/code&gt;对象的&lt;code&gt;map&lt;/code&gt;方法。需要强调的是，&lt;code&gt;apply&lt;/code&gt;方法具有归约效果的，简单的说就是能将较多的数据处理成较少的数据或一条数据；而&lt;code&gt;transform&lt;/code&gt;方法没有归约效果，只能对数据进行变换，原来有多少条数据，处理后还是有多少条数据。&lt;/p&gt;
&lt;p&gt;如果要对数据进行深度的分析和挖掘，字符串、日期时间这样的非数值类型都需要处理成数值，因为非数值类型没有办法计算相关性，也没有办法进行 $\small{\chi^{2}}$ 检验等操作。对于字符串类型，通常可以其分为以下三类，再进行对应的处理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有序变量（Ordinal Variable）：字符串表示的数据有顺序关系，那么可以对字符串进行序号化处理。&lt;/li&gt;
&lt;li&gt;分类变量（Categorical Variable）/ 名义变量（Nominal Variable）：字符串表示的数据没有大小关系和等级之分，那么就可以使用独热编码的方式处理成哑变量（虚拟变量）矩阵。&lt;/li&gt;
&lt;li&gt;定距变量（Scale Variable）：字符串本质上对应到一个有大小高低之分的数据，而且可以进行加减运算，那么只需要将字符串处理成对应的数值即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于第1类和第3类，我们可以用上面提到的&lt;code&gt;apply&lt;/code&gt;或&lt;code&gt;transform&lt;/code&gt;方法来处理，也可以利用&lt;code&gt;scikit-learn&lt;/code&gt;中的&lt;code&gt;OrdinalEncoder&lt;/code&gt;处理第1类字符串，这个我们在后续的课程中会讲到。对于第2类字符串，可以使用&lt;code&gt;pandas&lt;/code&gt;的&lt;code&gt;get_dummies()&lt;/code&gt;函数来生成哑变量（虚拟变量）矩阵，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;persons_df = pd.DataFrame(
    data={
        &apos;姓名&apos;: [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;],
        &apos;职业&apos;: [&apos;医生&apos;, &apos;医生&apos;, &apos;程序员&apos;, &apos;画家&apos;, &apos;教师&apos;],
        &apos;学历&apos;: [&apos;研究生&apos;, &apos;大专&apos;, &apos;研究生&apos;, &apos;高中&apos;, &apos;本科&apos;]
    }
)
persons_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	姓名	职业	学历
0	关羽	医生	研究生
1	张飞	医生	大专
2	赵云	程序员	研究生
3	马超	画家	高中
4	黄忠	教师	本科
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将职业处理成哑变量矩阵。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.get_dummies(persons_df[&apos;职业&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    医生 教师  画家  程序员
0	1    0    0    0
1	1    0    0    0
2	0    0    0    1
3	0    0    1    0
4	0    1    0    0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将学历处理成大小不同的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def handle_education(x):
    edu_dict = {&apos;高中&apos;: 1, &apos;大专&apos;: 3, &apos;本科&apos;: 5, &apos;研究生&apos;: 10}
    return edu_dict.get(x, 0)


persons_df[&apos;学历&apos;].apply(handle_education)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0    10
1     3
2    10
3     1
4     5
Name: 学历, dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再来说说数据离散化。离散化也叫分箱，如果变量的取值是连续值，那么它的取值有无数种可能，在进行数据分组的时候就会非常的不方便，这个时候将连续变量离散化就显得非常重要。之所以把离散化叫做分箱，是因为我们可以预先设置一些箱子，每个箱子代表了数据取值的范围，这样就可以将连续的值分配到不同的箱子中，从而实现离散化。下面的例子读取了2018年北京积分落户数据，我们可以根据落户积分对数据进行分组，具体的做法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luohu_df = pd.read_csv(&apos;data/2018年北京积分落户数据.csv&apos;, index_col=&apos;id&apos;)
luohu_df.score.describe()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;count    6019.000000
mean       95.654552
std         4.354445
min        90.750000
25%        92.330000
50%        94.460000
75%        97.750000
max       122.590000
Name: score, dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，落户积分的最大值是&lt;code&gt;122.59&lt;/code&gt;，最小值是&lt;code&gt;90.75&lt;/code&gt;，那么我们可以构造一个从&lt;code&gt;90&lt;/code&gt;分到&lt;code&gt;125&lt;/code&gt;分，每&lt;code&gt;5&lt;/code&gt;分一组的&lt;code&gt;7&lt;/code&gt;个箱子，&lt;code&gt;pandas&lt;/code&gt;的&lt;code&gt;cut&lt;/code&gt;函数可以帮助我们首先数据分箱，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bins = np.arange(90, 126, 5)
pd.cut(luohu_df.score, bins, right=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;cut&lt;/code&gt;函数的&lt;code&gt;right&lt;/code&gt;参数默认值为&lt;code&gt;True&lt;/code&gt;，表示箱子左开右闭；修改为&lt;code&gt;False&lt;/code&gt;可以让箱子的右边界为开区间，左边界为闭区间，大家看看下面的输出就明白了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id
1       [120, 125)
2       [120, 125)
3       [115, 120)
4       [115, 120)
5       [115, 120)
           ...    
6015      [90, 95)
6016      [90, 95)
6017      [90, 95)
6018      [90, 95)
6019      [90, 95)
Name: score, Length: 6019, dtype: category
Categories (7, interval[int64, left]): [[90, 95) &amp;lt; [95, 100) &amp;lt; [100, 105) &amp;lt; [105, 110) &amp;lt; [110, 115) &amp;lt; [115, 120) &amp;lt; [120, 125)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以根据分箱的结果对数据进行分组，然后使用聚合函数对每个组进行统计，这是数据分析中经常用到的操作，下一个章节会为大家介绍。除此之外，&lt;code&gt;pandas&lt;/code&gt;还提供了一个名为&lt;code&gt;qcut&lt;/code&gt;的函数，可以指定分位数对数据进行分箱，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;75 深入浅出pandas-4&lt;/h2&gt;
&lt;h4&gt;数据透视&lt;/h4&gt;
&lt;p&gt;经过前面的学习，我们已经将数据准备就绪而且变成了我们想要的样子，接下来就是最为重要的数据透视阶段了。当我们拿到一大堆数据的时候，如何从数据中迅速的解读出有价值的信息，把繁杂的数据变成容易解读的统计图表并再此基础上产生业务洞察，这就是数据分析要解决的核心问题。&lt;/p&gt;
&lt;h5&gt;获取描述性统计信息&lt;/h5&gt;
&lt;p&gt;首先，我们可以获取数据的描述性统计信息，通过描述性统计信息，我们可以了解数据的集中趋势和离散趋势。&lt;/p&gt;
&lt;p&gt;例如，我们有如下所示的学生成绩表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scores = np.random.randint(50, 101, (5, 3))
names = (&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;)
courses = (&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;)
df = pd.DataFrame(data=scores, columns=courses, index=names)
df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     语文   数学   英语
关羽  96    72    73
张飞  72    70	97
赵云  74    51	79
马超  100   54	54
黄忠  89    100	88
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过&lt;code&gt;DataFrame&lt;/code&gt;对象的方法&lt;code&gt;mean&lt;/code&gt;、&lt;code&gt;max&lt;/code&gt;、&lt;code&gt;min&lt;/code&gt;、&lt;code&gt;std&lt;/code&gt;、&lt;code&gt;var&lt;/code&gt;等方法分别获取每个学生或每门课程的平均分、最高分、最低分、标准差、方差等信息，也可以直接通过&lt;code&gt;describe&lt;/code&gt;方法直接获取描述性统计信息，代码如下所示。&lt;/p&gt;
&lt;p&gt;计算每门课程成绩的平均分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.mean()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;语文    86.2
数学    69.4
英语    78.2
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算每个学生成绩的平均分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.mean(axis=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;关羽    80.333333
张飞    79.666667
赵云    68.000000
马超    69.333333
黄忠    92.333333
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算每门课程成绩的方差。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.var()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;语文    161.2
数学    379.8
英语    265.7
dtype: float64
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过方差可以看出，数学成绩波动最大，两极分化可能更严重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;获取每门课程的描述性统计信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.describe()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        语文        数学         英语
count   5.000000	5.000000	5.000000
mean    86.200000	69.400000	78.200000
std     12.696456	19.488458	16.300307
min     72.000000	51.000000	54.000000
25%     74.000000	54.000000	73.000000
50%     89.000000	70.000000	79.000000
75%     96.000000	72.000000	88.000000
max     100.000000	100.000000	97.000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;排序和取头部值&lt;/h5&gt;
&lt;p&gt;如果需要对数据进行排序，可以使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;sort_values&lt;/code&gt;方法，该方法的&lt;code&gt;by&lt;/code&gt;参数可以指定根据哪个列或哪些列进行排序，而&lt;code&gt;ascending&lt;/code&gt;参数可以指定升序或是降序。例如，下面的代码展示了如何将学生表按语文成绩排降序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.sort_values(by=&apos;语文&apos;, ascending=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      语文   数学   英语
马超	100    54	  54
关羽	96     72     73
黄忠	89     100    88
赵云	74     51     79
张飞	72     70     97
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;DataFrame&lt;/code&gt;数据量很大，排序将是一个非常耗费时间的操作。有的时候我们只需要获得排前N名或后N名的数据，这个时候其实没有必要对整个数据进行排序，而是直接利用堆结构找出Top-N的数据。&lt;code&gt;DataFrame&lt;/code&gt;的&lt;code&gt;nlargest&lt;/code&gt;和&lt;code&gt;nsmallest&lt;/code&gt;方法就提供对Top-N操作的支持，代码如下所示。&lt;/p&gt;
&lt;p&gt;找出语文成绩前3名的学生信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.nlargest(3, &apos;语文&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      语文   数学   英语
马超	100    54	  54
关羽	96     72     73
黄忠	89     100    88
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找出数学成绩最低的3名学生的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.nsmallest(3, &apos;数学&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      语文  数学  英语
赵云  74    51	79
马超  100   54	54
张飞  72    70	97
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;分组聚合&lt;/h5&gt;
&lt;p&gt;我们先从之前使用过的 Excel 文件中读取2020年销售数据，然后再为大家演示如何进行分组聚合操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = pd.read_excel(&apos;data/2020年销售数据.xlsx&apos;)
df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    销售日期	 销售区域   销售渠道  销售订单     品牌    售价  销售数量
0   2020-01-01  上海       拼多多    182894-455  八匹马  99    83
1   2020-01-01  上海       抖音      205635-402  八匹马  219   29
2   2020-01-01  上海       天猫      205654-021  八匹马  169   85
3   2020-01-01  上海       天猫      205654-519  八匹马  169   14
4   2020-01-01  上海       天猫      377781-010  皮皮虾  249   61
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们要统计每个销售区域的销售总额，可以先通过“售价”和“销售数量”计算出销售额，为&lt;code&gt;DataFrame&lt;/code&gt;添加一个列，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df[&apos;销售额&apos;] = df[&apos;售价&apos;] * df[&apos;销售数量&apos;]
df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    销售日期	 销售区域   销售渠道  销售订单     品牌    售价  销售数量  销售额
0   2020-01-01  上海       拼多多    182894-455  八匹马  99    83        8217
1   2020-01-01  上海       抖音      205635-402  八匹马  219   29        6351
2   2020-01-01  上海       天猫      205654-021  八匹马  169   85        14365
3   2020-01-01  上海       天猫      205654-519  八匹马  169   14        2366
4   2020-01-01  上海       天猫      377781-010  皮皮虾  249   61        15189
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再根据“销售区域”列对数据进行分组，这里我们使用的是&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;groupby&lt;/code&gt;方法。分组之后，我们取“销售额”这个列在分组内进行求和处理，代码和结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(&apos;销售区域&apos;).销售额.sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;销售区域
上海    11610489
北京    12477717
安徽      895463
广东     1617949
江苏     2304380
浙江      687862
福建    10178227
Name: 销售额, dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们要统计每个月的销售总额，我们可以将“销售日期”作为groupby`方法的参数，当然这里需要先将“销售日期”处理成月，代码和结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(df[&apos;销售日期&apos;].dt.month).销售额.sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;销售日期
1     5409855
2     4608455
3     4164972
4     3996770
5     3239005
6     2817936
7     3501304
8     2948189
9     2632960
10    2375385
11    2385283
12    1691973
Name: 销售额, dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们将难度升级，统计每个销售区域每个月的销售总额，这又该如何处理呢？事实上，&lt;code&gt;groupby&lt;/code&gt;方法的第一个参数可以是一个列表，列表中可以指定多个分组的依据，大家看看下面的代码和输出结果就明白了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby([&apos;销售区域&apos;, df[&apos;销售日期&apos;].dt.month]).销售额.sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;销售区域  销售日期
上海    1       1679125
        2       1689527
        3       1061193
        4       1082187
        5        841199
        6        785404
        7        863906
        8        734937
        9       1107693
        10       412108
       11       825169
       12       528041
北京    1       1878234
        2       1807787
        3       1360666
        4       1205989
        5        807300
        6       1216432
        7       1219083
        8        645727
        9        390077
        10       671608
        11       678668
        12       596146
安徽    4        341308
        5        554155
广东    3        388180
        8        469390
        9        365191
        11       395188
江苏    4        537079
        7        841032
        10       710962
        12       215307
浙江    3        248354
        8        439508
福建    1       1852496
        2       1111141
        3       1106579
        4        830207
        5       1036351
        6        816100
        7        577283
        8        658627
        9        769999
        10       580707
        11       486258
        12       352479
Name: 销售额, dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望统计出每个区域的销售总额以及每个区域单笔金额的最高和最低，我们可以在&lt;code&gt;DataFrame&lt;/code&gt;或&lt;code&gt;Series&lt;/code&gt;对象上使用&lt;code&gt;agg&lt;/code&gt;方法并指定多个聚合函数，代码和结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(&apos;销售区域&apos;).销售额.agg([&apos;sum&apos;, &apos;max&apos;, &apos;min&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;           sum     max   min
销售区域                        
上海    11610489  116303   948
北京    12477717  133411   690
安徽      895463   68502  1683
广东     1617949  120807   990
江苏     2304380  114312  1089
浙江      687862   90909  3927
福建    10178227   87527   897
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望自定义聚合后的列的名字，可以使用如下所示的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(&apos;销售区域&apos;).销售额.agg(销售总额=&apos;sum&apos;, 单笔最高=&apos;max&apos;, 单笔最低=&apos;min&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          销售总额    单笔最高  单笔最低
销售区域                        
上海      11610489     116303     948
北京      12477717     133411     690
安徽        895463      68502    1683
广东       1617949     120807     990
江苏       2304380     114312    1089
浙江        687862      90909    3927
福建      10178227      87527     897
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要对多个列使用不同的聚合函数，例如“统计每个销售区域销售额的总和以及销售数量的最低值和最高值”，我们可以按照下面的方式来操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(&apos;销售区域&apos;)[[&apos;销售额&apos;, &apos;销售数量&apos;]].agg({
    &apos;销售额&apos;: &apos;sum&apos;, &apos;销售数量&apos;: [&apos;max&apos;, &apos;min&apos;]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;           销售额  销售数量    
           sum    max min
销售区域                   
上海    11610489  100  10
北京    12477717  100  10
安徽      895463   98  16
广东     1617949   98  10
江苏     2304380  100  11
浙江      687862   95  20
福建    10178227  100  10
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;透视表和交叉表&lt;/h5&gt;
&lt;p&gt;上面的例子中，“统计每个销售区域每个月的销售总额”会产生一个看起来很长的结果，在实际工作中我们通常把那些行很多列很少的表成为“窄表”，如果我们不想得到这样的一个“窄表”，可以使用&lt;code&gt;DataFrame&lt;/code&gt;的&lt;code&gt;pivot_table&lt;/code&gt;方法或者是&lt;code&gt;pivot_table&lt;/code&gt;函数来生成透视表。透视表的本质就是对数据进行分组聚合操作，&lt;strong&gt;根据 A 列对 B 列进行统计&lt;/strong&gt;，如果大家有使用 Excel 的经验，相信对透视表这个概念一定不会陌生。例如，我们要“统计每个销售区域的销售总额”，那么“销售区域”就是我们的 A 列，而“销售额”就是我们的 B 列，在&lt;code&gt;pivot_table&lt;/code&gt;函数中分别对应&lt;code&gt;index&lt;/code&gt;和&lt;code&gt;values&lt;/code&gt;参数，这两个参数都可以是单个列或者多个列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.pivot_table(df, index=&apos;销售区域&apos;, values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;           销售额
销售区域          
上海    11610489
北京    12477717
安徽      895463
广东     1617949
江苏     2304380
浙江      687862
福建    10178227
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的结果操作跟之前用&lt;code&gt;groupby&lt;/code&gt;的方式得到的结果有一些区别，&lt;code&gt;groupby&lt;/code&gt;操作后，如果对单个列进行聚合，得到的结果是一个&lt;code&gt;Series&lt;/code&gt;对象，而上面的结果是一个&lt;code&gt;DataFrame&lt;/code&gt; 对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果要统计每个销售区域每个月的销售总额，也可以使用&lt;code&gt;pivot_table&lt;/code&gt;函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df[&apos;月份&apos;] = df[&apos;销售日期&apos;].dt.month
pd.pivot_table(df, index=[&apos;销售区域&apos;, &apos;月份&apos;], values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的操作结果是一个&lt;code&gt;DataFrame&lt;/code&gt;，但也是一个长长的“窄表”，如果希望做成一个行比较少列比较多的“宽表”，可以将&lt;code&gt;index&lt;/code&gt;参数中的列放到&lt;code&gt;columns&lt;/code&gt;参数中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.pivot_table(df, index=&apos;销售区域&apos;, columns=&apos;月份&apos;, values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;, fill_value=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;pivot_table&lt;/code&gt;函数的&lt;code&gt;fill_value=0&lt;/code&gt;会将空值处理为&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b95343e368.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;pivot_table&lt;/code&gt;函数时，还可以通过添加&lt;code&gt;margins&lt;/code&gt;和&lt;code&gt;margins_name&lt;/code&gt;参数对分组聚合的结果做一个汇总，具体的操作和效果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.pivot_table(df, index=&apos;销售区域&apos;, columns=&apos;月份&apos;, values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;, fill_value=0, margins=True, margins_name=&apos;总计&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d1722e7c76.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;交叉表就是一种特殊的透视表，它不需要先构造一个&lt;code&gt;DataFrame&lt;/code&gt;对象，而是直接通过数组或&lt;code&gt;Series&lt;/code&gt;对象指定两个或多个因素进行运算得到统计结果。例如，我们要统计每个销售区域的销售总额，也可以按照如下所示的方式来完成，我们先准备三组数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_area, sales_month, sales_amount = df[&apos;销售区域&apos;], df[&apos;月份&apos;], df[&apos;销售额&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;crosstab&lt;/code&gt;函数生成交叉表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.crosstab(index=sales_area, columns=sales_month, values=sales_amount, aggfunc=&apos;sum&apos;).fillna(0).astype(&apos;i8&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码使用了&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;fillna&lt;/code&gt;方法将空值处理为0，再使用&lt;code&gt;astype&lt;/code&gt;方法将数据类型处理成整数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;数据呈现&lt;/h4&gt;
&lt;p&gt;一图胜千言，我们对数据进行透视的结果，最终要通过图表的方式呈现出来，因为图表具有极强的表现力，能够让我们迅速的解读数据中隐藏的价值。和&lt;code&gt;Series&lt;/code&gt;一样，&lt;code&gt;DataFrame&lt;/code&gt;对象提供了&lt;code&gt;plot&lt;/code&gt;方法来支持绘图，底层仍然是通过&lt;code&gt;matplotlib&lt;/code&gt;库实现图表的渲染。关于&lt;code&gt;matplotlib&lt;/code&gt;的内容，我们在下一个章节进行详细的探讨，这里我们只简单的讲解&lt;code&gt;plot&lt;/code&gt;方法的用法。&lt;/p&gt;
&lt;p&gt;例如，我们想通过一张柱状图来比较“每个销售区域的销售总额”，可以直接在透视表上使用&lt;code&gt;plot&lt;/code&gt;方法生成柱状图。我们先导入&lt;code&gt;matplotlib.pyplot&lt;/code&gt;模块，通过修改绘图的参数使其支持中文显示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

plt.rcParams[&apos;font.sans-serif&apos;] = &apos;FZJKai-Z03S&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;FZJKai-Z03S&lt;/code&gt;是我电脑上已经安装的一种支持中文的字体的名称，字体的名称可以通过查看用户主目录下&lt;code&gt;.matplotlib&lt;/code&gt;文件夹下名为&lt;code&gt;fontlist-v330.json&lt;/code&gt;的文件来获得，而这个文件在执行上面的命令后就会生成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用魔法指令配置生成矢量图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%config InlineBackend.figure_format = &apos;svg&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制“每个销售区域销售总额”的柱状图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp = pd.pivot_table(df, index=&apos;销售区域&apos;, values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;)
temp.plot(figsize=(8, 4), kind=&apos;bar&apos;)
plt.xticks(rotation=0)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的第3行代码会将横轴刻度上的文字旋转到0度，第4行代码会显示图像。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/f5a3ad3b9e.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果要绘制饼图，可以修改&lt;code&gt;plot&lt;/code&gt;方法的&lt;code&gt;kind&lt;/code&gt;参数为&lt;code&gt;pie&lt;/code&gt;，然后使用定制饼图的参数对图表加以定制，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp.sort_values(by=&apos;销售额&apos;, ascending=False).plot(
    figsize=(6, 6),
    kind=&apos;pie&apos;,
    y=&apos;销售额&apos;,
    ylabel=&apos;&apos;,
    autopct=&apos;%.2f%%&apos;,
    pctdistance=0.8,
    wedgeprops=dict(linewidth=1, width=0.35),
    legend=False
)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/5efe12a4b6.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;76 深入浅出pandas-5&lt;/h2&gt;
&lt;p&gt;我们再来补充一些使用&lt;code&gt;DataFrame&lt;/code&gt;做数据分析时会使用到的操作，这些操作不仅常见而且也非常重要。&lt;/p&gt;
&lt;h4&gt;计算同比环比&lt;/h4&gt;
&lt;p&gt;我们之前讲过一个统计月度销售额的例子，我们可以通过&lt;code&gt;groupby&lt;/code&gt;方法做分组聚合，也可以通过&lt;code&gt;pivot_table&lt;/code&gt;生成透视表，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_df = pd.read_excel(&apos;data/2020年销售数据.xlsx&apos;)
sales_df[&apos;月份&apos;] = sales_df.销售日期.dt.month
sales_df[&apos;销售额&apos;] = sales_df.售价 * sales_df.销售数量
result_df = sales_df.pivot_table(index=&apos;月份&apos;, values=&apos;销售额&apos;, aggfunc=&apos;sum&apos;)
result_df.rename(columns={&apos;销售额&apos;: &apos;本月销售额&apos;}, inplace=True)
result_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      本月销售额
月份         
1       5409855
2       4608455
3       4164972
4       3996770
5       3239005
6       2817936
7       3501304
8       2948189
9       2632960
10      2375385
11      2385283
12      1691973
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在得到月度销售额之后，如果我们需要计算月环比，这里有两种方案。第一种方案是我们可以使用&lt;code&gt;shift&lt;/code&gt;方法对数据进行移动，将上一个月的数据与本月数据对齐，然后通过&lt;code&gt;(本月销售额 - 上月销售额) / 上月销售额&lt;/code&gt;来计算月环比，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result_df[&apos;上月销售额&apos;] = result_df.本月销售额.shift(1)
result_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      本月销售额      上月销售额
月份                    
1       5409855            NaN
2       4608455      5409855.0
3       4164972      4608455.0
4       3996770      4164972.0
5       3239005      3996770.0
6       2817936      3239005.0
7       3501304      2817936.0
8       2948189      3501304.0
9       2632960      2948189.0
10      2375385      2632960.0
11      2385283      2375385.0
12      1691973      2385283.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，&lt;code&gt;shift&lt;/code&gt;方法的参数为&lt;code&gt;1&lt;/code&gt;表示将数据向下移动一个单元，当然我们可以使用参数&lt;code&gt;-1&lt;/code&gt;将数据向上移动一个单元。相信大家能够想到，如果我们有更多年份的数据，我们可以将参数设置为&lt;code&gt;12&lt;/code&gt;，这样就可以计算今年的每个月与去年的每个月之间的同比。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result_df[&apos;环比&apos;] = (result_df.本月销售额 - result_df.上月销售额) / result_df.上月销售额
result_df.style.format(
    formatter={&apos;上月销售额&apos;: &apos;{:.0f}&apos;, &apos;环比&apos;: &apos;{:.2%}&apos;},
    na_rep=&apos;--------&apos;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      本月销售额      上月销售额         环比
月份                    
1       5409855       --------     -------- 
2       4608455        5409855      -14.81%     
3       4164972        4608455       -9.62%
4       3996770        4164972       -4.04%
5       3239005        3996770      -18.96%
6       2817936        3239005      -13.00%
7       3501304        2817936       24.25%
8       2948189        3501304      -15.80%
9       2632960        2948189      -10.69%
10      2375385        2632960       -9.78%
11      2385283        2375385        0.42%
12      1691973        2385283      -29.07%
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：使用 JupyterLab 时，可以通过&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;style&lt;/code&gt;属性在网页中对其进行渲染，上面的代码通过&lt;code&gt;Styler&lt;/code&gt;对象的&lt;code&gt;format&lt;/code&gt;方法将环比格式化为百分比进行显示，此外还指定了将空值替换为&lt;code&gt;--------&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;更为简单的第二种方案是直接使用&lt;code&gt;pct_change&lt;/code&gt;方法计算变化的百分比，我们先将之前的上月销售额和环比列删除掉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result_df.drop(columns=[&apos;上月销售额&apos;, &apos;环比&apos;], inplace=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们使用&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;pct_change&lt;/code&gt;方法完成环比的计算。值得一提的是，&lt;code&gt;pct_change&lt;/code&gt;方法有一个名为&lt;code&gt;periods&lt;/code&gt;的参数，它的默认值是&lt;code&gt;1&lt;/code&gt;，计算相邻两项数据变化的百分比，这不就是我们想要的环比吗？如果我们有很多年的数据，在计算时把这个参数的值修改为&lt;code&gt;12&lt;/code&gt;，就可以得到相邻两年的月同比。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result_df[&apos;环比&apos;] = result_df.pct_change()
result_df
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;窗口计算&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;rolling&lt;/code&gt;方法允许我们将数据置于窗口中，然后用函数对窗口中的数据进行运算和处理。例如，我们获取了某只股票近期的数据，想制作5日均线和10日均线，那么就需要先设置窗口再进行运算。我们先用如下所示的代码读取2022年百度的股票数据，数据文件可以通过下面的链接来获取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df = pd.read_excel(&apos;data/2022年股票数据.xlsx&apos;, sheet_name=&apos;BIDU&apos;, index_col=&apos;Date&apos;)
baidu_df.sort_index(inplace=True)
baidu_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a7bafc29dd.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上面的&lt;code&gt;DataFrame&lt;/code&gt;有&lt;code&gt;Open&lt;/code&gt;、&lt;code&gt;High&lt;/code&gt;、&lt;code&gt;Low&lt;/code&gt;、&lt;code&gt;Close&lt;/code&gt;、&lt;code&gt;Volume&lt;/code&gt;五个列，分别代表股票的开盘价、最高价、最低价、收盘价和成交量，接下来我们对百度的股票数据进行窗口计算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df.rolling(5).mean()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/ea21979932.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们也可以在&lt;code&gt;Series&lt;/code&gt;上使用&lt;code&gt;rolling&lt;/code&gt;设置窗口并在窗口内完成运算，例如我们可以对上面的百度股票收盘价（&lt;code&gt;Close&lt;/code&gt;列）计算5日均线和10日均线，并使用&lt;code&gt;merge&lt;/code&gt;函数将其组装到一个&lt;code&gt;DataFrame&lt;/code&gt;对象中并绘制出双均线图，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;close_ma5 = baidu_df.Close.rolling(5).mean()
close_ma10 = baidu_df.Close.rolling(10).mean()
result_df = pd.merge(close_ma5, close_ma10, left_index=True, right_index=True)
result_df.rename(columns={&apos;Close_x&apos;: &apos;MA5&apos;, &apos;Close_y&apos;: &apos;MA10&apos;}, inplace=True)
result_df.plot(kind=&apos;line&apos;, figsize=(10, 6))
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/e8fe5b4dfb.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;相关性判定&lt;/h4&gt;
&lt;p&gt;在统计学中，我们通常使用协方差（covariance）来衡量两个随机变量的联合变化程度。如果变量 $\small{X}$ 的较大值主要与另一个变量 $\small{Y}$ 的较大值相对应，而两者较小值也相对应，那么两个变量倾向于表现出相似的行为，协方差为正。如果一个变量的较大值主要对应于另一个变量的较小值，则两个变量倾向于表现出相反的行为，协方差为负。简单的说，协方差的正负号显示着两个变量的相关性。方差是协方差的一种特殊情况，即变量与自身的协方差。&lt;/p&gt;
&lt;p&gt;$$
cov(X,Y) = E((X - \mu)(Y - \upsilon)) = E(X \cdot Y) - \mu\upsilon
$$&lt;/p&gt;
&lt;p&gt;如果 $\small{X}$ 和 $\small{Y}$ 是统计独立的，那么二者的协方差为 0，这是因为在 $\small{X}$ 和 $\small{Y}$ 独立的情况下：&lt;/p&gt;
&lt;p&gt;$$
E(X \cdot Y) = E(X) \cdot E(Y) = \mu\upsilon
$$&lt;/p&gt;
&lt;p&gt;协方差的数值大小取决于变量的大小，通常是不容易解释的，但是正态形式的协方差可以显示两变量线性关系的强弱。在统计学中，皮尔逊积矩相关系数就是正态形式的协方差，它用于度量两个变量 $\small{X}$ 和 $\small{Y}$ 之间的相关程度（线性相关），其值介于 -1 到 1 之间。&lt;/p&gt;
&lt;p&gt;$$
\frac {cov(X, Y)} {\sigma_{X}\sigma_{Y}}
$$&lt;/p&gt;
&lt;p&gt;估算样本的协方差和标准差，可以得到样本皮尔逊系数，通常用希腊字母 $\small{\rho}$ 表示。&lt;/p&gt;
&lt;p&gt;$$
\rho = \frac {\sum_{i=1}^{n}(X_i - \bar{X})(Y_i - \bar{Y})} {\sqrt{\sum_{i=1}^{n}(X_i - \bar{X})^2} \sqrt{\sum_{i=1}^{n}(Y_i - \bar{Y})^2}}
$$&lt;/p&gt;
&lt;p&gt;我们用 $\small{\rho}$ 值判断指标的相关性时遵循以下两个步骤。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;判断指标间是正相关、负相关，还是不相关。
&lt;ul&gt;
&lt;li&gt;当 $\small{\rho \gt 0}$，认为变量之间是正相关，也就是两者的趋势一致。&lt;/li&gt;
&lt;li&gt;当 $\small{\rho \lt 0}$，认为变量之间是负相关，也就是两者的趋势相反。&lt;/li&gt;
&lt;li&gt;当 $\small{\rho \approx 0}$，认为变量之间是不相关的，但并不代表两个指标是统计独立的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;判断指标间的相关程度。
&lt;ul&gt;
&lt;li&gt;当 $\small{\rho}$ 的绝对值在 $\small{[0.6,1]}$ 之间，认为变量之间是强相关的。&lt;/li&gt;
&lt;li&gt;当 $\small{\rho}$ 的绝对值在 $\small{[0.1,0.6)}$ 之间，认为变量之间是弱相关的。&lt;/li&gt;
&lt;li&gt;当 $\small{\rho}$ 的绝对值在 $\small{[0,0.1)}$ 之间，认为变量之间没有相关性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;皮尔逊相关系数适用于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两个变量之间是线性关系，都是连续数据。&lt;/li&gt;
&lt;li&gt;两个变量的总体是正态分布，或接近正态的单峰分布。&lt;/li&gt;
&lt;li&gt;两个变量的观测值是成对的，每对观测值之间相互独立。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里，我们顺便说一下，如果两组变量并不是来自于正态总体的连续值，我们该如何判断相关性呢？对于定序尺度（等级），我们可以使用斯皮尔曼秩相关系数，其计算公式如下所示：&lt;/p&gt;
&lt;p&gt;$$
r_{s}=1-{\frac {6\sum d_{i}^{2}}{n(n^{2}-1)}}
$$&lt;/p&gt;
&lt;p&gt;其中， $\small{d_{i}=\operatorname {R} (X_{i})-\operatorname {R} (Y_{i})}$ ，即每组观测中两个变量的等级差值， $\small{n}$ 为观测样本数。&lt;/p&gt;
&lt;p&gt;对于定类尺度（类别），我们可以使用卡方检验的方式来判定其是否相关。其实很多时候，连续值也可以通过分箱的方式处理成离散的等级或类别，然后使用斯皮尔曼秩相关系数或卡方检验的方式来判定相关性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;cov&lt;/code&gt;方法和&lt;code&gt;corr&lt;/code&gt;方法分别用于计算协方差和相关系数，&lt;code&gt;corr&lt;/code&gt;方法有一个名为&lt;code&gt;method&lt;/code&gt;的参数，其默认值是&lt;code&gt;pearson&lt;/code&gt;，表示计算皮尔逊相关系数；除此之外，还可以指定&lt;code&gt;kendall&lt;/code&gt;或&lt;code&gt;spearman&lt;/code&gt;来计算肯德尔系数或斯皮尔曼秩相关系数。&lt;/p&gt;
&lt;p&gt;我们从名为&lt;code&gt;boston_house_price.csv&lt;/code&gt;的文件中获取著名的波士顿房价数据集来创建一个&lt;code&gt;DataFrame&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boston_df = pd.read_csv(&apos;data/boston_house_price.csv&apos;)
boston_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/ce6804b5cd.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中使用了相对路径来访问 CSV 文件，也就是说 CSV 文件在当前工作路径下名为&lt;code&gt;data&lt;/code&gt;的文件夹中。如果需要上面例子中的 CSV 文件，可以通过下面的百度云盘地址进行获取。链接：&lt;a href=&quot;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g?pwd=e7b4&quot;&gt;https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g?pwd=e7b4&lt;/a&gt;，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以看出，该数据集中包含了诸多影响房价的特征，包括犯罪率、一氧化氮浓度、平均房间数、低收入人群占比等，其中&lt;code&gt;PRICE&lt;/code&gt;代表房价，具体情况如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/71ffccb61b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们将其中可以视为来自于正态总体的连续值，通过&lt;code&gt;corr&lt;/code&gt;方法计算皮尔逊相关系数，看看哪些跟房价是正相关或负相关的关系，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boston_df[[&apos;NOX&apos;, &apos;RM&apos;, &apos;PTRATIO&apos;, &apos;LSTAT&apos;, &apos;PRICE&apos;]].corr()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b3ed20f8fb.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看出，平均房间数（&lt;code&gt;RM&lt;/code&gt;）跟房价有较强的正相关性，而低收入人群占比（&lt;code&gt;LSTAT&lt;/code&gt;）跟房价之间存在明显的负相关性。&lt;/p&gt;
&lt;p&gt;斯皮尔曼秩相关系数对数据条件的要求没有皮尔逊相关系数严格，只要两个变量的观测值是成对的等级数据，或者是由连续变量转化成等级的数据，不论两个变量的总体分布形态、样本容量的大小如何，都可以用斯皮尔曼等级相关系数来进行研究。我们可以通过下面的方式对部分特征进行预处理，然后计算斯皮尔曼秩相关系数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boston_df[&apos;CRIM&apos;] = boston_df.CRIM.apply(lambda x: x // 5 if x &amp;lt; 25 else 5).map(int)
boston_df[&apos;ZN&apos;] = pd.qcut(boston_df.ZN, q=[0, 0.75, 0.8, 0.85, 0.9, 0.95, 1], labels=np.arange(6))
boston_df[&apos;AGE&apos;] = (boston_df.AGE // 20).map(int)
boston_df[&apos;DIS&apos;] = (boston_df.DIS // 2.05).map(int)
boston_df[&apos;B&apos;] = (boston_df.B // 66).map(int)
boston_df[&apos;PRICE&apos;] = pd.qcut(boston_df.PRICE, q=[0, 0.15, 0.3, 0.5, 0.7, 0.85, 1], labels=np.arange(6))
boston_df[[&apos;CRIM&apos;, &apos;ZN&apos;, &apos;AGE&apos;, &apos;DIS&apos;, &apos;B&apos;, &apos;PRICE&apos;]].corr(method=&apos;spearman&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/9f5053eefb.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看出，房价跟犯罪率（&lt;code&gt;CRIM&lt;/code&gt;）和房龄（&lt;code&gt;AGE&lt;/code&gt;）之间存在较为明显的负相关关系，跟住房用地尺寸（&lt;code&gt;ZN&lt;/code&gt;）存在微弱的正相关关系。相关性可以帮助我们在实际工作中找到业务抓手，即找到那些能够影响或改变工作结果的相关因素。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;77 深入浅出pandas-6&lt;/h2&gt;
&lt;p&gt;我们再来看看&lt;code&gt;Index&lt;/code&gt;类型，它为&lt;code&gt;Series&lt;/code&gt;和&lt;code&gt;DataFrame&lt;/code&gt;对象提供了索引服务，有了索引我们就可以排序数据（&lt;code&gt;sort_index&lt;/code&gt;方法）、对齐数据（在运算和合并数据时非常重要）并实现对数据的快速检索（索引运算）。由于&lt;code&gt;DataFrame&lt;/code&gt;类型表示的是二维数据，所以它的行和列都有索引，分别是&lt;code&gt;index&lt;/code&gt;和&lt;code&gt;columns&lt;/code&gt;。&lt;code&gt;Index&lt;/code&gt;类型的创建的比较简单，通常给出&lt;code&gt;data&lt;/code&gt;、&lt;code&gt;dtype&lt;/code&gt;和&lt;code&gt;name&lt;/code&gt;三个参数即可，分别表示作为索引的数据、索引的数据类型和索引的名称。由于&lt;code&gt;Index&lt;/code&gt;本身也是一维的数据，索引它的方法和属性跟&lt;code&gt;Series&lt;/code&gt;非常类似，你可以尝试创建一个&lt;code&gt;Index&lt;/code&gt;对象，然后尝试一下之前学过的属性和方法在&lt;code&gt;Index&lt;/code&gt;类型上是否生效。接下来，我们主要看看&lt;code&gt;Index&lt;/code&gt;的几种子类型。&lt;/p&gt;
&lt;h4&gt;范围索引&lt;/h4&gt;
&lt;p&gt;范围索引是由具有单调性的整数构成的索引，我们可以通过&lt;code&gt;RangeIndex&lt;/code&gt;构造器来创建范围索引，也可以通过&lt;code&gt;RangeIndex&lt;/code&gt;类的类方法&lt;code&gt;from_range&lt;/code&gt;来创建范围索引，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_data = np.random.randint(400, 1000, 12)
index = pd.RangeIndex(1, 13, name=&apos;月份&apos;)
ser = pd.Series(data=sales_data, index=index)
ser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;月份
1     703
2     705
3     557
4     943
5     961
6     615
7     788
8     985
9     921
10    951
11    874
12    609
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分类索引&lt;/h4&gt;
&lt;p&gt;分类索引是由定类尺度构成的索引。如果我们需要通过索引将数据分组，然后再进行聚合操作，分类索引就可以派上用场。分类索引还有一个名为&lt;code&gt;reorder_categories&lt;/code&gt;的方法，可以给索引指定一个顺序，分组聚合的结果会按照这个指定的顺序进行呈现，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_data = [6, 6, 7, 6, 8, 6]
index = pd.CategoricalIndex(
    data=[&apos;苹果&apos;, &apos;香蕉&apos;, &apos;苹果&apos;, &apos;苹果&apos;, &apos;桃子&apos;, &apos;香蕉&apos;],
    categories=[&apos;苹果&apos;, &apos;香蕉&apos;, &apos;桃子&apos;],
    ordered=True
)
ser = pd.Series(data=sales_data, index=index)
ser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;苹果    6
香蕉    6
苹果    7
苹果    6
桃子    8
香蕉    6
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基于索引分组数据，然后使用&lt;code&gt;sum&lt;/code&gt;进行求和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser.groupby(level=0).sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;苹果    19
香蕉    12
桃子     8
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定索引的顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser.index = index.reorder_categories([&apos;香蕉&apos;, &apos;桃子&apos;, &apos;苹果&apos;])
ser.groupby(level=0).sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;香蕉    12
桃子     8
苹果    19
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;多级索引&lt;/h4&gt;
&lt;p&gt;Pandas 中的&lt;code&gt;MultiIndex&lt;/code&gt;类型用来表示层次或多级索引。可以使用&lt;code&gt;MultiIndex&lt;/code&gt;类的类方法&lt;code&gt;from_arrays&lt;/code&gt;、&lt;code&gt;from_product&lt;/code&gt;、&lt;code&gt;from_tuples&lt;/code&gt;等来创建多级索引，我们给大家举几个例子。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tuples = [(1, &apos;red&apos;), (1, &apos;blue&apos;), (2, &apos;red&apos;), (2, &apos;blue&apos;)]
index = pd.MultiIndex.from_tuples(tuples, names=[&apos;no&apos;, &apos;color&apos;])
index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MultiIndex([(1,  &apos;red&apos;),
            (1, &apos;blue&apos;),
            (2,  &apos;red&apos;),
            (2, &apos;blue&apos;)],
           names=[&apos;no&apos;, &apos;color&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arrays = [[1, 1, 2, 2], [&apos;red&apos;, &apos;blue&apos;, &apos;red&apos;, &apos;blue&apos;]]
index = pd.MultiIndex.from_arrays(arrays, names=[&apos;no&apos;, &apos;color&apos;])
index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MultiIndex([(1,  &apos;red&apos;),
            (1, &apos;blue&apos;),
            (2,  &apos;red&apos;),
            (2, &apos;blue&apos;)],
           names=[&apos;no&apos;, &apos;color&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sales_data = np.random.randint(1, 100, 4)
ser = pd.Series(data=sales_data, index=index)
ser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no  color
1   red      43
    blue     31
2   red      55
    blue     75
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser.groupby(&apos;no&apos;).sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no
1     74
2    130
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ser.groupby(level=1).sum()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color
blue    106
red      98
dtype: int64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stu_ids = np.arange(1001, 1006)
semisters = [&apos;期中&apos;, &apos;期末&apos;]
index = pd.MultiIndex.from_product((stu_ids, semisters), names=[&apos;学号&apos;, &apos;学期&apos;])
courses = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
scores = np.random.randint(60, 101, (10, 3))
df = pd.DataFrame(data=scores, columns=courses, index=index)
df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             语文 数学 英语
学号	学期			
1001  期中	93	77	60
      期末	93	98	84
1002  期中	64	78	71
      期末	70	71	97
1003  期中	72	88	97
      期末	99	100	63
1004  期中	80	71	61
      期末	91	62	72
1005  期中	82	95	67
      期末	84	78	86
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据第一级索引分组数据，按照期中成绩占&lt;code&gt;25%&lt;/code&gt;，期末成绩占&lt;code&gt;75%&lt;/code&gt; 的方式计算每个学生每门课的成绩。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.groupby(level=0).agg(lambda x: x.values[0] * 0.25 + x.values[1] * 0.75)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        语文    数学    英语
学号			
1001	93.00	92.75	78.00
1002	68.50	72.75	90.50
1003	92.25	97.00	71.50
1004	88.25	64.25	69.25
1005	83.50	82.25	81.25
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;间隔索引&lt;/h4&gt;
&lt;p&gt;间隔索引顾名思义是使用固定的间隔范围充当索引，我们通常会使用&lt;code&gt;interval_range&lt;/code&gt;函数来创建间隔索引，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = pd.interval_range(start=0, end=5)
index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IntervalIndex([(0, 1], (1, 2], (2, 3], (3, 4], (4, 5]], dtype=&apos;interval[int64, right]&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IntervalIndex&lt;/code&gt;有一个名为&lt;code&gt;contains&lt;/code&gt;的方法，可以检查范围内是否包含了某个元素，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index.contains(1.5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([False,  True, False, False, False])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IntervalIndex&lt;/code&gt;还有一个名为&lt;code&gt;overlaps&lt;/code&gt;的方法，可以检查一个范围跟其他的范围是否有重叠，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index.overlaps(pd.Interval(1.5, 3.5))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array([False,  True,  True,  True, False])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望间隔范围是左闭右开的状态，可以在创建间隔索引时通过&lt;code&gt;closed=&apos;left&apos;&lt;/code&gt;来做到；如果希望两边都是关闭状态，可以将&lt;code&gt;close&lt;/code&gt;参数的值赋值为&lt;code&gt;both&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = pd.interval_range(start=0, end=5, closed=&apos;left&apos;)
index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IntervalIndex([[0, 1), [1, 2), [2, 3), [3, 4), [4, 5)], dtype=&apos;interval[int64, left]&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = pd.interval_range(start=pd.Timestamp(&apos;2022-01-01&apos;), end=pd.Timestamp(&apos;2022-01-04&apos;), closed=&apos;both&apos;)
index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IntervalIndex([[2022-01-01, 2022-01-02], [2022-01-02, 2022-01-03], [2022-01-03, 2022-01-04]], dtype=&apos;interval[datetime64[ns], both]&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;日期时间索引&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;DatetimeIndex&lt;/code&gt;应该是众多索引中最复杂最重要的一种索引，我们通常会使用&lt;code&gt;date_range()&lt;/code&gt;函数来创建日期时间索引，该函数有几个非常重要的参数&lt;code&gt;start&lt;/code&gt;、&lt;code&gt;end&lt;/code&gt;、&lt;code&gt;periods&lt;/code&gt;、&lt;code&gt;freq&lt;/code&gt;、&lt;code&gt;tz&lt;/code&gt;，分别代表起始日期时间、结束日期时间、生成周期、采样频率和时区。我们先来看看如何创建&lt;code&gt;DatetimeIndex&lt;/code&gt;对象，再来讨论它的相关运算和操作，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.date_range(&apos;2021-1-1&apos;, &apos;2021-6-30&apos;, periods=10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DatetimeIndex([&apos;2021-01-01&apos;, &apos;2021-01-21&apos;, &apos;2021-02-10&apos;, &apos;2021-03-02&apos;,
               &apos;2021-03-22&apos;, &apos;2021-04-11&apos;, &apos;2021-05-01&apos;, &apos;2021-05-21&apos;,
               &apos;2021-06-10&apos;, &apos;2021-06-30&apos;],
              dtype=&apos;datetime64[ns]&apos;, freq=None)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pd.date_range(&apos;2021-1-1&apos;, &apos;2021-6-30&apos;, freq=&apos;W&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;freq=W&lt;/code&gt;表示采样周期为一周，它会默认星期日是一周的开始；如果你希望星期一表示一周的开始，你可以将其修改为&lt;code&gt;freq=W-MON&lt;/code&gt;；你也可以试着将该参数的值修改为&lt;code&gt;12H&lt;/code&gt;，&lt;code&gt;M&lt;/code&gt;，&lt;code&gt;Q&lt;/code&gt;等，看看会发生什么，相信你不难猜到它们的含义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DatetimeIndex([&apos;2021-01-03&apos;, &apos;2021-01-10&apos;, &apos;2021-01-17&apos;, &apos;2021-01-24&apos;,
               &apos;2021-01-31&apos;, &apos;2021-02-07&apos;, &apos;2021-02-14&apos;, &apos;2021-02-21&apos;,
               &apos;2021-02-28&apos;, &apos;2021-03-07&apos;, &apos;2021-03-14&apos;, &apos;2021-03-21&apos;,
               &apos;2021-03-28&apos;, &apos;2021-04-04&apos;, &apos;2021-04-11&apos;, &apos;2021-04-18&apos;,
               &apos;2021-04-25&apos;, &apos;2021-05-02&apos;, &apos;2021-05-09&apos;, &apos;2021-05-16&apos;,
               &apos;2021-05-23&apos;, &apos;2021-05-30&apos;, &apos;2021-06-06&apos;, &apos;2021-06-13&apos;,
               &apos;2021-06-20&apos;, &apos;2021-06-27&apos;],
              dtype=&apos;datetime64[ns]&apos;, freq=&apos;W-SUN&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DatatimeIndex&lt;/code&gt;可以跟&lt;code&gt;DateOffset&lt;/code&gt;类型进行运算，这一点很好理解，以为我们可以设置一个时间差让时间向前或向后偏移，具体的操作如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = pd.date_range(&apos;2021-1-1&apos;, &apos;2021-6-30&apos;, freq=&apos;W&apos;)
index - pd.DateOffset(days=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DatetimeIndex([&apos;2021-01-01&apos;, &apos;2021-01-08&apos;, &apos;2021-01-15&apos;, &apos;2021-01-22&apos;,
               &apos;2021-01-29&apos;, &apos;2021-02-05&apos;, &apos;2021-02-12&apos;, &apos;2021-02-19&apos;,
               &apos;2021-02-26&apos;, &apos;2021-03-05&apos;, &apos;2021-03-12&apos;, &apos;2021-03-19&apos;,
               &apos;2021-03-26&apos;, &apos;2021-04-02&apos;, &apos;2021-04-09&apos;, &apos;2021-04-16&apos;,
               &apos;2021-04-23&apos;, &apos;2021-04-30&apos;, &apos;2021-05-07&apos;, &apos;2021-05-14&apos;,
               &apos;2021-05-21&apos;, &apos;2021-05-28&apos;, &apos;2021-06-04&apos;, &apos;2021-06-11&apos;,
               &apos;2021-06-18&apos;, &apos;2021-06-25&apos;],
              dtype=&apos;datetime64[ns]&apos;, freq=None)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index + pd.DateOffset(hours=2, minutes=10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DatetimeIndex([&apos;2021-01-03 02:10:00&apos;, &apos;2021-01-10 02:10:00&apos;,
               &apos;2021-01-17 02:10:00&apos;, &apos;2021-01-24 02:10:00&apos;,
               &apos;2021-01-31 02:10:00&apos;, &apos;2021-02-07 02:10:00&apos;,
               &apos;2021-02-14 02:10:00&apos;, &apos;2021-02-21 02:10:00&apos;,
               &apos;2021-02-28 02:10:00&apos;, &apos;2021-03-07 02:10:00&apos;,
               &apos;2021-03-14 02:10:00&apos;, &apos;2021-03-21 02:10:00&apos;,
               &apos;2021-03-28 02:10:00&apos;, &apos;2021-04-04 02:10:00&apos;,
               &apos;2021-04-11 02:10:00&apos;, &apos;2021-04-18 02:10:00&apos;,
               &apos;2021-04-25 02:10:00&apos;, &apos;2021-05-02 02:10:00&apos;,
               &apos;2021-05-09 02:10:00&apos;, &apos;2021-05-16 02:10:00&apos;,
               &apos;2021-05-23 02:10:00&apos;, &apos;2021-05-30 02:10:00&apos;,
               &apos;2021-06-06 02:10:00&apos;, &apos;2021-06-13 02:10:00&apos;,
               &apos;2021-06-20 02:10:00&apos;, &apos;2021-06-27 02:10:00&apos;],
              dtype=&apos;datetime64[ns]&apos;, freq=None)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;Series&lt;/code&gt;对象或&lt;code&gt;DataFrame&lt;/code&gt;对象使用了&lt;code&gt;DatetimeIndex&lt;/code&gt;类型的索引，此时我们可以通过&lt;code&gt;asfreq()&lt;/code&gt;方法指定一个时间频率来实现对数据的抽样，我们仍然以之前讲过的百度股票数据为例，给大家做一个演示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df = pd.read_excel(&apos;data/2022年股票数据.xlsx&apos;, sheet_name=&apos;BIDU&apos;, index_col=&apos;Date&apos;)
baidu_df.sort_index(inplace=True)
baidu_df.asfreq(&apos;5D&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/630f1bec7b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;大家可能注意到了，每5天抽取1天有可能会抽中非交易日，那么对应的列都变成了空值，为了解决这个问题，在使用&lt;code&gt;asfreq&lt;/code&gt;方法时可以通过&lt;code&gt;method&lt;/code&gt;参数来指定一种填充空值的方法，可以将相邻的交易日的数据填入进来。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df.asfreq(&apos;5D&apos;, method=&apos;ffill&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d836c4ccae.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当使用&lt;code&gt;DatetimeIndex&lt;/code&gt;索引时，我们也可以通过&lt;code&gt;resample()&lt;/code&gt;方法基于时间对数据进行重采样，相当于根据时间周期对数据进行了分组操作，分组之后还可以进行聚合统计，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df.resample(&apos;1M&apos;).mean()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/41e95633de.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df.resample(&apos;1M&apos;).agg([&apos;mean&apos;, &apos;std&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/33ba8c13b5.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：不知大家是否注意到，上面输出的&lt;code&gt;DataFrame&lt;/code&gt; 的列索引是一个&lt;code&gt;MultiIndex&lt;/code&gt;对象。你可以访问上面的&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;columns&lt;/code&gt;属性看看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果要实现日期时间的时区转换，我们可以先用&lt;code&gt;tz_localize()&lt;/code&gt;方法将日期时间本地化，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df = baidu_df.tz_localize(&apos;Asia/Chongqing&apos;)
baidu_df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/ad6e15493f.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;在对时间本地化以后，我们再使用&lt;code&gt;tz_convert()&lt;/code&gt;方法就可以实现转换时区，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;baidu_df.tz_convert(&apos;America/New_York&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/10a9adbe31.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果你的数据使用了&lt;code&gt;DatetimeIndex&lt;/code&gt;类型的索引，那么你就很有可能要对数据进行时间序列分析，关于时间序列分析的方法和模型并不是本章节要探讨的内容，我们在其他的专栏中为大家分享。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;78 数据可视化-1&lt;/h2&gt;
&lt;p&gt;在完成了对数据的透视之后，我们可以将数据透视的结果通过可视化的方式呈现出来，简单的说，就是将数据变成漂亮的统计图表，因为人类对颜色和形状会更加敏感，然后再进一步解读数据背后隐藏的商业价值。在之前的课程中，我们已经为大家展示过用使用&lt;code&gt;Series&lt;/code&gt;或&lt;code&gt;DataFrame&lt;/code&gt;对象的&lt;code&gt;plot&lt;/code&gt;方法生成可视化图表的操作，本章我们为大家讲解这个绘图方法的基石，它就是大名鼎鼎的 matplotlib 库。&lt;/p&gt;
&lt;p&gt;在讲解 matplotlib 之前，请大家先看看下面这张图，它给出了常用的图表类型及其应用场景。我们在选择统计图表时，如果不知道做出怎样的选择最合适，相信这张图就能帮到你。简单的说，看趋势折线图，比数据柱状图，定关系散点图，查占比饼状图，看分布直方图，找离群箱线图。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/562e3b6157.webp&quot; style=&quot;zoom:65%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;导入和配置&lt;/h4&gt;
&lt;p&gt;之前的课程中，我们为大家讲解过如何安装和导入 matplotlib 库，如果不确定是否已经安装了 matplotlib，可以使用下面的魔法指令尝试安装或升级你的 matplotlib。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install -U matplotlib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了解决 matplotlib 图表中文显示的问题，我们需要修改&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;rcParams&lt;/code&gt;配置参数，具体的操作如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

plt.rcParams[&apos;font.sans-serif&apos;].insert(0, &apos;SimHei&apos;)
plt.rcParams[&apos;axes.unicode_minus&apos;] = False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;SimHei&lt;/code&gt;是字体名称，大家可以通过百度云盘下载并安装该字体，链接地址：https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g?pwd=e7b4。你可以尝试安装其他的中文字体，安装之后如果不知道字体叫什么名字，可以到用户主目录下名为&lt;code&gt;.matplotlib&lt;/code&gt;的文件夹中找到&lt;code&gt;fontlist-v330.json&lt;/code&gt;文件，打开后可以看到字体文件的路径和字体的名称等信息。需要注意的是，使用中文字体后坐标轴上的负号将会无法显示，需要将&lt;code&gt;axes.unicode_minus&lt;/code&gt;参数设置为&lt;code&gt;False&lt;/code&gt;，这样才能让坐标轴上的负号正常显示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过下面的魔法指令，我们可以在绘图时生成&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%9F%A2%E9%87%8F%E5%9B%BE%E5%BD%A2&quot;&gt;矢量图&lt;/a&gt;（SVG - Scalable Vector Graphics），矢量图的特点是不会因为放大、缩小或旋转等操作而失真，看起来会舒服很多。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%config InlineBackend.figure_format=&apos;svg&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建画布&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;figure&lt;/code&gt;函数可以用来创建画布，创建画布时，可以通过&lt;code&gt;figsize&lt;/code&gt;参数指定画布的尺寸（默认值是&lt;code&gt;[6.4, 4.8]&lt;/code&gt;）；可以通过&lt;code&gt;dpi&lt;/code&gt;参数设置绘图的分辨率，因为&lt;code&gt;dpi&lt;/code&gt;代表了每英寸的像素点数量。除此之外，还可以通过&lt;code&gt;facecolor&lt;/code&gt;参数设置画布的背景色。&lt;code&gt;figure&lt;/code&gt;函数的返回值是一个&lt;code&gt;Figure&lt;/code&gt;对象，它代表了绘图使用的画布，我们可以基于画布来创建绘图使用的坐标系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(8, 4), dpi=120, facecolor=&apos;darkgray&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建坐标系&lt;/h4&gt;
&lt;p&gt;可以直接使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;subplot&lt;/code&gt;函数来创建坐标系，该函数会返回&lt;code&gt;Axes&lt;/code&gt;对象。&lt;code&gt;subplot&lt;/code&gt;的前三个参数分别用来指定整个画布分成几行几列以及当前坐标系的索引，这三个参数的默认值都是&lt;code&gt;1&lt;/code&gt;。如果没有创建坐标系，我们绘图时会使用画布上默认的也是唯一的一个坐标系；如果需要在画布上创建多个坐标系，就可以使用该函数。当然，我们也可以通过上面创建的&lt;code&gt;Figure&lt;/code&gt;对象的&lt;code&gt;add_subplot&lt;/code&gt;方法或&lt;code&gt;add_axes&lt;/code&gt;方法来创建坐标系，前者跟&lt;code&gt;subplot&lt;/code&gt;函数的作用一致，后者会产生嵌套的坐标系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.subplot(2, 2, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;绘制图表&lt;/h4&gt;
&lt;h5&gt;折线图&lt;/h5&gt;
&lt;p&gt;在绘图时，如果没有先调用&lt;code&gt;figure&lt;/code&gt;函数和&lt;code&gt;subplot&lt;/code&gt;函数，我们将使用默认的画布和坐标系，如果要绘制折线图，可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;plot&lt;/code&gt;函数，并指定横轴和纵轴的数据。折线图最适合用来观察数据的趋势，尤其是当横坐标代表时间的情况下。我们可以使用&lt;code&gt;plot&lt;/code&gt;函数的&lt;code&gt;color&lt;/code&gt;参数来定制折线的颜色，可以使用&lt;code&gt;marker&lt;/code&gt;参数来定制数据点的标记（例如：&lt;code&gt;*&lt;/code&gt;表示五角星，&lt;code&gt;^&lt;/code&gt;表示三角形，&lt;code&gt;o&lt;/code&gt;表示小圆圈等），可以使用&lt;code&gt;linestyle&lt;/code&gt;参数来定制折线的样式（例如：&lt;code&gt;-&lt;/code&gt;表示实线，&lt;code&gt;--&lt;/code&gt;表示虚线，&lt;code&gt;:&lt;/code&gt;表示点线等），可以使用&lt;code&gt;linewidth&lt;/code&gt;参数来定制折线的粗细。 下面的代码绘制了一条正弦曲线，其中&lt;code&gt;marker=&apos;*&apos;&lt;/code&gt;会将数据点的标记设置为五角星形状，而&lt;code&gt;color=&apos;red&apos;&lt;/code&gt;会将折线绘制为红色。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np

x = np.linspace(-2 * np.pi, 2 * np.pi, 120)
y = np.sin(x)

# 创建画布
plt.figure(figsize=(8, 4), dpi=120)
# 绘制折线图
plt.plot(x, y, linewidth=2, marker=&apos;*&apos;, color=&apos;red&apos;)
# 显示绘图
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/3f9420bea9.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果要在一个坐标系上同时绘制正弦和余弦曲线，可以对上面的代码稍作修改。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = np.linspace(-2 * np.pi, 2 * np.pi, 120)
y1, y2 = np.sin(x), np.cos(x)

plt.figure(figsize=(8, 4), dpi=120)
plt.plot(x, y1, linewidth=2, marker=&apos;*&apos;, color=&apos;red&apos;)
plt.plot(x, y2, linewidth=2, marker=&apos;^&apos;, color=&apos;blue&apos;)
# 定制图表上的标注（annotate函数的参数如果不理解可以先不管它）
plt.annotate(&apos;sin(x)&apos;, xytext=(0.5, -0.75), xy=(0, -0.25), fontsize=12, arrowprops={
    &apos;arrowstyle&apos;: &apos;-&amp;gt;&apos;, &apos;color&apos;: &apos;darkgreen&apos;, &apos;connectionstyle&apos;: &apos;angle3, angleA=90, angleB=0&apos;
})
plt.annotate(&apos;cos(x)&apos;, xytext=(-3, 0.75), xy=(-1.25, 0.5), fontsize=12, arrowprops={
    &apos;arrowstyle&apos;: &apos;-&amp;gt;&apos;, &apos;color&apos;: &apos;darkgreen&apos;, &apos;connectionstyle&apos;: &apos;arc3, rad=0.35&apos;
})
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b1fbc9da49.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果要使用两个坐标系分别绘制正弦和余弦，可以用上面提到的&lt;code&gt;subplot&lt;/code&gt;函数来创建坐标系，然后再绘图。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(8, 4), dpi=120)
# 创建坐标系（第1个图）
plt.subplot(2, 1, 1)
plt.plot(x, y1, linewidth=2, marker=&apos;*&apos;, color=&apos;red&apos;)
# 创建坐标系（第2个图）
plt.subplot(2, 1, 2)
plt.plot(x, y2, linewidth=2, marker=&apos;^&apos;, color=&apos;blue&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/256fbe2eec.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当然也可以像下面这么做，大家可以运行代码看看跟上面的图有什么区别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(8, 4), dpi=120)
plt.subplot(1, 2, 1)
plt.plot(x, y1, linewidth=2, marker=&apos;*&apos;, color=&apos;red&apos;)
plt.subplot(1, 2, 2)
plt.plot(x, y2, linewidth=2, marker=&apos;^&apos;, color=&apos;blue&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，再试一试下面这个代码，看看运行效果如何。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fig = plt.figure(figsize=(10, 4), dpi=120)
plt.plot(x, y1, linewidth=2, marker=&apos;*&apos;, color=&apos;red&apos;)
# 用Figure对象的add_axes方法在现有坐标系中嵌套一个新的坐标系，该方法的参数是一个四元组，
# 代表了新坐标系在原坐标系中的位置，前两个值是左下角的位置，后两个值是坐标系的宽度和高度
ax = fig.add_axes((0.595, 0.6, 0.3,0.25))
ax.plot(x, y2, marker=&apos;^&apos;, color=&apos;blue&apos;)
ax = fig.add_axes((0.155, 0.2, 0.3,0.25))
ax.plot(x, y2, marker=&apos;^&apos;, color=&apos;green&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;散点图&lt;/h5&gt;
&lt;p&gt;散点图可以帮助我们了解两个变量的关系，如果需要了解三个变量的关系，可以将散点图升级为气泡图。下面的代码中，&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;两个数组分别表示每个月的收入和每个月网购的支出，如果我们想了解&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;是否存在相关关系，就可以绘制如下所示的散点图。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = np.array([5550, 7500, 10500, 15000, 20000, 25000, 30000, 40000])
y = np.array([800, 1800, 1250, 2000, 1800, 2100, 2500, 3500])

plt.figure(figsize=(6, 4), dpi=120)
plt.scatter(x, y)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/7a89de11ff.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;柱状图&lt;/h5&gt;
&lt;p&gt;在对比数据的差异时，柱状图是非常棒的选择，我们可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;bar&lt;/code&gt;函数来生成柱状图，也可以使用&lt;code&gt;barh&lt;/code&gt;函数来生成水平柱状图（也称为“条状图”）。我们先为柱状图准备一些数据，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = np.arange(4)
y1 = np.random.randint(20, 50, 4)
y2 = np.random.randint(10, 60, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制柱状图的代码。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(6, 4), dpi=120)
# 通过横坐标的偏移，让两组数据对应的柱子分开，width参数控制柱子的粗细，label参数为柱子添加标签
plt.bar(x - 0.1, y1, width=0.2, label=&apos;销售A组&apos;)
plt.bar(x + 0.1, y2, width=0.2, label=&apos;销售B组&apos;)
# 定制横轴的刻度
plt.xticks(x, labels=[&apos;Q1&apos;, &apos;Q2&apos;, &apos;Q3&apos;, &apos;Q4&apos;])
# 定制显示图例
plt.legend()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/4ca8003982.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果想绘制堆叠柱状图，可以对上面的代码稍作修改，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;labels = [&apos;Q1&apos;, &apos;Q2&apos;, &apos;Q3&apos;, &apos;Q4&apos;]
plt.figure(figsize=(6, 4), dpi=120)
plt.bar(labels, y1, width=0.4, label=&apos;销售A组&apos;)
# 注意：堆叠柱状图的关键是将之前的柱子作为新柱子的底部，可以通过bottom参数指定底部数据，新柱子绘制在底部数据之上
plt.bar(labels, y2, width=0.4, bottom=y1, label=&apos;销售B组&apos;)
plt.legend(loc=&apos;lower right&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/45312e5532.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;饼状图&lt;/h5&gt;
&lt;p&gt;饼状图通常简称为饼图，是一个将数据划分为几个扇形区域的统计图表，它主要用于描述数量、频率等之间的相对关系。在饼图中，每个扇形区域的大小就是其所表示的数量的比例，这些扇形区域合在一起刚好是一个完整的饼。在需要展示数据构成的场景下，饼状图、树状图和瀑布图是不错的选择，我们可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;pie&lt;/code&gt;函数来绘制饼图，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = np.random.randint(100, 500, 7)
labels = [&apos;苹果&apos;, &apos;香蕉&apos;, &apos;桃子&apos;, &apos;荔枝&apos;, &apos;石榴&apos;, &apos;山竹&apos;, &apos;榴莲&apos;]

plt.figure(figsize=(5, 5), dpi=120)
plt.pie(
    data,
    # 自动显示百分比
    autopct=&apos;%.1f%%&apos;,
    # 饼图的半径
    radius=1,
    # 百分比到圆心的距离
    pctdistance=0.8,
    # 颜色（随机生成）
    colors=np.random.rand(7, 3),
    # 分离距离
    # explode=[0.05, 0, 0.1, 0, 0, 0, 0],
    # 阴影效果
    # shadow=True,
    # 字体属性
    textprops=dict(fontsize=8, color=&apos;black&apos;),
    # 楔子属性（生成环状饼图的关键）
    wedgeprops=dict(linewidth=1, width=0.35),
    # 标签
    labels=labels
)
# 定制图表的标题
plt.title(&apos;水果销售额占比&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/36bc12cdb9.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大家可以试一试将上面代码中被注释的部分恢复，看看有什么样的效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;直方图&lt;/h5&gt;
&lt;p&gt;在统计学中，直方图是一种展示数据分布情况的图形，是一种二维统计图表，它的两个坐标分别是统计样本和该样本对应的某个属性的度量。下面的数据是某学校100名男学生的身高，如果我们想知道数据的分布，就可以使用直方图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;heights = np.array([
    170, 163, 174, 164, 159, 168, 165, 171, 171, 167, 
    165, 161, 175, 170, 174, 170, 174, 170, 173, 173, 
    167, 169, 173, 153, 165, 169, 158, 166, 164, 173, 
    162, 171, 173, 171, 165, 152, 163, 170, 171, 163, 
    165, 166, 155, 155, 171, 161, 167, 172, 164, 155, 
    168, 171, 173, 169, 165, 162, 168, 177, 174, 178, 
    161, 180, 155, 155, 166, 175, 159, 169, 165, 174, 
    175, 160, 152, 168, 164, 175, 168, 183, 166, 166, 
    182, 174, 167, 168, 176, 170, 169, 173, 177, 168, 
    172, 159, 173, 185, 161, 170, 170, 184, 171, 172
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;hist&lt;/code&gt;函数来绘制直方图，其中&lt;code&gt;bins&lt;/code&gt;参数代表了我们使用的分箱方式（身高从150厘米到190厘米，每5厘米为一个分箱），代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(6, 4), dpi=120)
# 绘制直方图
plt.hist(heights, bins=np.arange(145, 196, 5), color=&apos;darkcyan&apos;)
# 定制横轴标签
plt.xlabel(&apos;身高&apos;)
# 定制纵轴标签
plt.ylabel(&apos;概率密度&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/26b912176c.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;绘制直方图时，如果将&lt;code&gt;hist&lt;/code&gt;函数的&lt;code&gt;density&lt;/code&gt;参数修改为&lt;code&gt;True&lt;/code&gt;，同时将&lt;code&gt;cumulative&lt;/code&gt;参数也修改为&lt;code&gt;True&lt;/code&gt;，那么一方面纵轴会显示为概率密度，而图表会绘制概率的累计分布，如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(6, 4), dpi=120)
# 绘制直方图
plt.hist(heights, bins=np.arange(145, 196, 5), color=&apos;darkcyan&apos;, density=True, cumulative=True)
# 定制横轴标签
plt.xlabel(&apos;身高&apos;)
# 定制纵轴标签
plt.ylabel(&apos;概率&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/3296be8b4b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;箱线图&lt;/h5&gt;
&lt;p&gt;箱线图又叫箱型图或盒须图，是一种用于展示一组数据分散情况的统计图表，如下所示。因图形如箱子，而且在上下四分位数之外有线条像胡须延伸出去而得名。在箱线图中，箱子的上边界是上四分位数（ $\small{Q_{3}}$ ）的位置，箱子的下边界是下四分位数（ $\small{Q_{1}}$ ）的位置，箱子中间的线条是中位数（ $\small{Q_{2}}$ ）的位置，而箱子的长度就是四分位距离（IQR）。除此之外，箱子上方线条的边界是最大值，箱子下方线条的边界是最小值，这两条线之外的点就是离群值（outlier）。所谓离群值，是指数据小于 $\small{Q_{1} - 1.5 \times IQR}$ 或数据大于 $\small{Q_{3} + 1.5 \times IQR}$ 的值，公式中的&lt;code&gt;1.5&lt;/code&gt;还可以替换为&lt;code&gt;3&lt;/code&gt;来发现极端离群值（extreme outlier），而介于&lt;code&gt;1.5&lt;/code&gt;到&lt;code&gt;3&lt;/code&gt;之间的离群值通常称之为适度离群值（mild outlier）。&lt;/p&gt;
&lt;p&gt;可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;boxplot&lt;/code&gt;函数来绘制箱线图，代码如下所示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 数组中有47个[0, 100)范围的随机数
data = np.random.randint(0, 100, 47)
# 向数组中添加三个可能是离群点的数据
data = np.append(data, 160)
data = np.append(data, 200)
data = np.append(data, -50)

plt.figure(figsize=(6, 4), dpi=120)
# whis参数的默认值是1.5，将其设置为3可以检测极端离群值，showmeans=True表示在图中标记均值的位置
plt.boxplot(data, whis=1.5, showmeans=True, notch=True)
# 定制纵轴的取值范围
plt.ylim([-100, 250])
# 定制横轴的刻度
plt.xticks([1], labels=[&apos;data&apos;])
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/a7320ee806.webp&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：由于数据是随机生成的，大家运行上面的代码生成的图表可能跟我这里并不相同，以实际运行结果为准。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;显示和保存图表&lt;/h4&gt;
&lt;p&gt;可以使用&lt;code&gt;pyplot&lt;/code&gt;模块的&lt;code&gt;show&lt;/code&gt;函数来显示绘制的图表，我们在上面的代码中使用过这个函数。如果希望保存图表，可以使用&lt;code&gt;savefig&lt;/code&gt;函数。需要注意的是，如果要同时显示和保存图表，应该先执行&lt;code&gt;savefig&lt;/code&gt;函数，再执行&lt;code&gt;show&lt;/code&gt;函数，因为在调用&lt;code&gt;show&lt;/code&gt;函数时，图表已经被释放，位于&lt;code&gt;show&lt;/code&gt;函数之后的&lt;code&gt;savefig&lt;/code&gt;保存的只是一个空白的区域。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.savefig(&apos;chart.png&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;其他图表&lt;/h4&gt;
&lt;p&gt;使用 matplotlib，我们还可以绘制出其他的统计图表（如：雷达图、玫瑰图、热力图等），但实际工作中，使用频率最高的几类图表我们在上面已经为大家完整的展示出来了。此外，matplotlib 还有很多对统计图表进行定制的细节，例如定制坐标轴、定制图表上的文字和标签等。如果想了解如何用 matplotlib 绘制和定制更多的统计图表，可以直接查看 matplotlib 官方网站上的&lt;a href=&quot;https://matplotlib.org/stable/tutorials/index.html&quot;&gt;文档&lt;/a&gt;和&lt;a href=&quot;https://matplotlib.org/stable/gallery/index.html&quot;&gt;示例&lt;/a&gt;，在下一个章节我们会为大家做一个简要的介绍。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;79 数据可视化-2&lt;/h2&gt;
&lt;p&gt;本章我们尝试用 matplotlib 来绘制一些高阶统计图表。正如前面所说的，大家可以通过 matplotlib 官方网站上提供的&lt;a href=&quot;https://matplotlib.org/stable/tutorials/index.html&quot;&gt;文档&lt;/a&gt;和&lt;a href=&quot;https://matplotlib.org/stable/gallery/index.html&quot;&gt;示例&lt;/a&gt;来学习如何使用 matplotlib 并绘制出更加高级的统计图表；尤其是在定制一些比较复杂的图表时，我们建议大家直接找到官网提供的示例，然后只需要做出相应的修改，就可以绘制出自己想要的图表。这种“拷贝+修改”的做法应该会大大提高你的工作效率，因为大多数时候，你的代码跟官网上的代码就仅仅是数据有差别而已，没有必要去做重复乏味的事情。&lt;/p&gt;
&lt;h4&gt;气泡图&lt;/h4&gt;
&lt;p&gt;气泡图可以用来了解三个变量之间的关系，通过比较气泡位置和大小来分析数据维度之间的相关性。例如在我们之前绘制的月收入和网购支出的散点图中，我们已经发现了二者的正相关关系，如果我们引入第三个变量网购次数，那么我们就需要使用气泡图来进行展示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;income = np.array([5550, 7500, 10500, 15000, 20000, 25000, 30000, 40000])
outcome = np.array([800, 1800, 1250, 2000, 1800, 2100, 2500, 3500])
nums = np.array([5, 3, 10, 5, 12, 20, 8, 10])

# 通过scatter函数的s参数和c参数分别控制面积和颜色
plt.scatter(income, outcome, s=nums * 30, c=nums, cmap=&apos;Reds&apos;)
# 显示颜色条
plt.colorbar()
# 显示图表
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/c2cdbad420.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;面积图&lt;/h4&gt;
&lt;p&gt;面积图又叫堆叠折线图，是在折线图的基础上，对折线以下的区域进行颜色填充（展示面积），用于在连续间隔或时间跨度上展示数值，一般用来显示趋势和对比数值，不同颜色的填充可以让多个面积块之间的对比和趋势更好的突显。下面的例子中，我们用面积图来展示从周一到周日花在睡觉、吃饭、工作和玩耍上的时间。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plt.figure(figsize=(8, 4))
days = np.arange(7)
sleeping = [7, 8, 6, 6, 7, 8, 10]
eating = [2, 3, 2, 1, 2, 3, 2]
working = [7, 8, 7, 8, 6, 2, 3]
playing = [8, 5, 9, 9, 9, 11, 9]
# 绘制堆叠折线图
plt.stackplot(days, sleeping, eating, working, playing)
# 定制横轴刻度
plt.xticks(days, labels=[f&apos;星期{x}&apos; for x in &apos;一二三四五六日&apos;])
# 定制图例
plt.legend([&apos;睡觉&apos;, &apos;吃饭&apos;, &apos;工作&apos;, &apos;玩耍&apos;], fontsize=10)
# 显示图表
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/04cfba0c6a.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;雷达图&lt;/h4&gt;
&lt;p&gt;雷达图通常用来比较多个定量数据，用于查看哪些变量具有相似的值。 雷达图也可用于查看数据集中哪些变量的值比较低，哪些变量的值比较高，是显示性能或表现的理想选择。经常观看篮球、足球比赛的读者应该对雷达图非常熟悉，例如在 NBA 的转播中就经常使用雷达图来展示球员的各项数据。雷达图的本质折线图，只不过将折线图映射到了极坐标系。在绘制雷达图时，需要让折线闭合，简单的说就是首尾相连，下面是绘制雷达图的代码。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;labels = np.array([&apos;速度&apos;, &apos;力量&apos;, &apos;经验&apos;, &apos;防守&apos;, &apos;发球&apos;, &apos;技术&apos;])
# 马龙和水谷隼的数据
malong_values = np.array([93, 95, 98, 92, 96, 97])
shuigu_values = np.array([30, 40, 65, 80, 45, 60])
angles = np.linspace(0, 2 * np.pi, labels.size, endpoint=False)
# 多加一条数据让图形闭合
malong_values = np.append(malong_values, malong_values[0])
shuigu_values = np.append(shuigu_values, shuigu_values[0])
angles = np.append(angles, angles[0])
# 创建画布
plt.figure(figsize=(4, 4), dpi=120)
# 创建坐标系
ax = plt.subplot(projection=&apos;polar&apos;)
# 绘图和填充
plt.plot(angles, malong_values, color=&apos;r&apos;, linewidth=2, label=&apos;马龙&apos;)
plt.fill(angles, malong_values, color=&apos;r&apos;, alpha=0.3)
plt.plot(angles, shuigu_values, color=&apos;g&apos;, linewidth=2, label=&apos;水谷隼&apos;)
plt.fill(angles, shuigu_values, color=&apos;g&apos;, alpha=0.2)
# 显示图例
ax.legend()
# 显示图表
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/518c895cdf.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;玫瑰图&lt;/h4&gt;
&lt;p&gt;玫瑰图是映射在极坐标下的柱状图，由弗罗伦斯·南丁格尔（Florence Nightingale）所发明，当年是南丁格尔用来呈现战地医院季节性死亡率的一种图表。由于半径和面积的关系是平方的关系，南丁格尔玫瑰图会将数据的比例大小夸大，尤其适合对比大小相近的数值，同时由于圆形有周期的特性，所以南丁格尔玫瑰图也适用于表示一个周期内的时间概念，比如星期、月份。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group1 = np.random.randint(20, 50, 4)
group2 = np.random.randint(10, 60, 4)
x = np.array([f&apos;A组-Q{i}&apos; for i in range(1, 5)] + [f&apos;B组-Q{i}&apos; for i in range(1, 5)])
y = np.array(group1.tolist() + group2.tolist())
# 玫瑰花瓣的角度和宽度
theta = np.linspace(0, 2 * np.pi, x.size, endpoint=False)
width = 2 * np.pi / x.size
# 生成8种随机颜色
colors = np.random.rand(8, 3)
# 将柱状图投影到极坐标
ax = plt.subplot(projection=&apos;polar&apos;)
# 绘制柱状图
plt.bar(theta, y, width=width, color=colors, bottom=0)
# 设置网格
ax.set_thetagrids(theta * 180 / np.pi, x, fontsize=10)
# 显示图表
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/1693991454.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;3D图表&lt;/h4&gt;
&lt;p&gt;matplotlib 还可以用于绘制3D图，具体的内容大家可以参考官方文档，下面我们用一段简单的代码为大家展示如何绘制3D图表。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(8, 4), dpi=120)
# 创建3D坐标系并添加到画布上
ax = Axes3D(fig, auto_add_to_figure=False)
fig.add_axes(ax)
x = np.arange(-2, 2, 0.1)
y = np.arange(-2, 2, 0.1)
x, y = np.meshgrid(x, y)
z = (1 - y ** 5 + x ** 5) * np.exp(-x ** 2 - y ** 2)
# 绘制3D曲面
ax.plot_surface(x, y, z)
# 显示图表
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/2150d9b08f.webp&quot; style=&quot;zoom:60%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;需要指出的是， JupyterLab 中渲染的3D图并不是真正的3D图，因为你没有办法调整观察者的视角，也没有办法旋转或者缩放。如果想要看到真正的3D效果，需要在将图表渲染到 Qt 窗口中，为此我们可以先安装名为 PyQt6 的三方库，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install PyQt6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们使用魔法指令让 JupyterLab 将图表渲染到 Qt 窗口中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%matplotlib qt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完成上面的操作后，我们可以重新运行刚才绘制3D图的代码，看到如下所示的窗口。在这个窗口中，我们可以通过鼠标对3D进行旋转、缩放，我们有可以选中图表的一部分数据进行观测，是不是非常的酷。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/b83825e39b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;80 数据可视化-3&lt;/h2&gt;
&lt;p&gt;通过前面的学习，我们已经对数据可视化工具 matplotlib 有一个初步的认知。大家可能也会发现了，matplotlib 提供的函数虽然强大，但是参数太多，要想对图表进行深度的定制就需要修改一系列的参数，这一点对新手并不友好。另一方面，使用 matplotlib 定制的统计图是静态图表，可能在某些需要交互效果的场景下并不合适。为了解决这两个问题，我们为大家介绍两个新的可视化工具，一个是 seaborn，一个是 pyecharts。&lt;/p&gt;
&lt;h4&gt;Seaborn&lt;/h4&gt;
&lt;p&gt;Seaborn 是建立在 matplotlib 之上的数据可视化工具，它相当于是对 matplotlib 进行了更高级的封装，而且 seaborn 也能跟 pandas 无缝整合，让我们可以用更少的代码构建出更好的统计图表，帮助我们探索和理解数据。Seaborn 包含但不局限于以下描述的功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;面向数据集的 API，可用于检查多个变量之间的关系。&lt;/li&gt;
&lt;li&gt;支持使用分类变量来显示观察结果或汇总统计数据。&lt;/li&gt;
&lt;li&gt;能够可视化单变量或双变量分布以及在数据子集之间进行比较的选项&lt;/li&gt;
&lt;li&gt;各类因变量线性回归模型的自动估计与作图。&lt;/li&gt;
&lt;li&gt;集成调色板和主题，轻松定制统计图表的视觉效果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以使用 Python 的包管理工具 pip 来安装 seaborn。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install seaborn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Jupyter 中，可以直接使用魔法指令进行安装，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install seaborn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面，我们用 seaborn 自带的数据集为例，为大家简单的展示 seaborn 的用法和强大之处，想要深入研究 seaborn 的读者可以自行阅读官方&lt;a href=&quot;https://seaborn.pydata.org/tutorial.html&quot;&gt;文档&lt;/a&gt;和并查看官方作品集中的&lt;a href=&quot;https://seaborn.pydata.org/examples/index.html&quot;&gt;示例。&lt;/a&gt;根据官方示例来编写自己的代码是一个不错的选择，简单的说就是保留官方代码，将数据换成自己的数据即可。下图展示了 seaborn 绘制图表的函数，可以看出，seaborn 的这些函数主要支持我们通过绘制图表来探索数据的关系、分布和分类。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/4cde62ce30.webp&quot; style=&quot;zoom:75%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;使用 seaborn，首先需要导入该库并设置主题，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import seaborn as sns

sns.set_theme()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要在图表上显示中文，还需要用之前讲过的方法修改 matplotlib 的配置参数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import matplotlib.pyplot as plt

plt.rcParams[&apos;font.sans-serif&apos;].insert(0, &apos;SimHei&apos;)
plt.rcParams[&apos;axes.unicode_minus&apos;] = False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的代码必须放在调用 set_theme 函数之后，否则调用 set_theme 函数时又会重新修改 matplotlib 配置参数中的字体设置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;加载官方的 Tips 数据集（就餐小费数据）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tips_df = sns.load_dataset(&apos;tips&apos;)
tips_df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下所示，其中 total_bill 表示账单总金额，tip 表示小费的金额，sex 是顾客的性别，smoker 表示顾客是否抽样，day 代表星期几，time 代表是午餐还是晚餐，size 是就餐人数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &apos;pandas.core.frame.DataFrame&apos;&amp;gt;
RangeIndex: 244 entries, 0 to 243
Data columns (total 7 columns):
 #   Column      Non-Null Count  Dtype   
---  ------      --------------  -----   
 0   total_bill  244 non-null    float64 
 1   tip         244 non-null    float64 
 2   sex         244 non-null    category
 3   smoker      244 non-null    category
 4   day         244 non-null    category
 5   time        244 non-null    category
 6   size        244 non-null    int64   
dtypes: category(4), float64(2), int64(1)
memory usage: 7.4 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于数据集是联网加载的，上述代码可能因为 SSL 的原因无法获取到数据，可以尝试先运行下面的代码，然后再加载数据集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ssl

ssl._create_default_https_context = ssl._create_unverified_context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望了解账单金额的分布，可以使用下面的代码来绘制分布图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.histplot(data=tips_df, x=&apos;total_bill&apos;, kde=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/7484246ae7.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果想了解变量之间的两两关系，我们可以绘制点对图，代码和效果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.pairplot(data=tips_df, hue=&apos;sex&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/75f44aa6d8.webp&quot; style=&quot;zoom:42%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果对上面图表的颜色不满意，还可以通过 palette 参数选择 seaborn 自带的“调色板”来修改颜色，这种方式相比于自行指定颜色或使用随机颜色方便和靠谱了很多，下图为大家展示了部分 seaborn 自带的“调色板”。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/f71a3eada8.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们可以将上面的代码稍作修改，看看运行结果有什么差别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.pairplot(data=tips_df, hue=&apos;sex&apos;, palette=&apos;Dark2&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们为 total_bill 和 tip 两组数据绘制联合分布图，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.jointplot(data=tips_df, x=&apos;total_bill&apos;, y=&apos;tip&apos;, hue=&apos;sex&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/7c9c8e958f.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上面清晰的展示了，total_bill 和 tip 之间存在正相关关系，这一点我们也可以通过 DataFrame 对象的 corr 方法进行验证。接下来，我们可以建立回归模型来拟合这些数据点，而 seaborn 的线性回归模型图已经帮我们实现了这项功能，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.lmplot(data=tips_df, x=&apos;total_bill&apos;, y=&apos;tip&apos;, hue=&apos;sex&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/803797efee.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如果我们希望了解账单金额的集中和离散趋势，可以绘制箱线图或小提琴图，代码如下所示，我们将数据按星期四、星期五、星期六和星期天分别进行展示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.boxplot(data=tips_df, x=&apos;day&apos;, y=&apos;total_bill&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/c1243ef682.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sns.violinplot(data=tips_df, x=&apos;day&apos;, y=&apos;total_bill&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/de13acfa87.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：相较于箱线图，小提琴图没有标注异常点而是显示了数据的整个范围，另一方面，小提琴图很好的展示了数据的分布（密度轨迹）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Pyecharts&lt;/h4&gt;
&lt;p&gt;Echarts 原来是百度开发的一个前端图表库，2018年1月16日，ECharts 进入 Apache Incubator 进行孵化，目前已经是 Apache 软件基金会的顶级项目。凭借着良好的交互性和精巧的图表设计，ECharts 得到了众多开发者的认可，而 pyecharts 就是基于 Python 语言对 ECharts 进行了包装，让 Python 开发者也可以使用 ECharts 绘制外观精美且交互性强的统计图表。&lt;/p&gt;
&lt;p&gt;可以使用 Python 的包管理工具 pip 来安装 pyecharts。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyecharts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 JupyterLab 中，可以直接使用魔法指令进行安装，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install pyecharts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想在 JupyterLab 中使用 pyecharts 绘图，我们还需要做一些准备工作，主要是修改 pyecharts 的配置，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pyecharts.globals import CurrentConfig, NotebookType

CurrentConfig.NOTEBOOK_TYPE = NotebookType.JUPYTER_LAB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们通过来自于 pyecharts 官方网站新手教程中的一个例子，来认识 pyecharts。当然，我们对官网的例子进行一些调整，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pyecharts.charts import Bar
from pyecharts import options as opts

# 创建柱状图对象并设置初始参数（宽度、高度）
bar_chart = Bar(init_opts=opts.InitOpts(width=&apos;600px&apos;, height=&apos;450px&apos;))
# 设置横轴数据
bar_chart.add_xaxis([&quot;衬衫&quot;, &quot;羊毛衫&quot;, &quot;雪纺衫&quot;, &quot;裤子&quot;, &quot;高跟鞋&quot;, &quot;袜子&quot;])
# 设置纵轴数据（第一组）
bar_chart.add_yaxis(&quot;商家A&quot;, [25, 20, 36, 10, 75, 90])
# 设置纵轴数据（第二组）
bar_chart.add_yaxis(&quot;商家B&quot;, [15, 12, 30, 20, 45, 60])
# 设置纵轴数据（第三组）
bar_chart.add_yaxis(&quot;商家C&quot;, [12, 32, 40, 52, 35, 26])
# 添加全局配置参数
bar_chart.set_global_opts(
    # 横轴相关的参数
    xaxis_opts=opts.AxisOpts(
        axislabel_opts=opts.LabelOpts(color=&apos;navy&apos;)
    ),
    # 纵轴相关的参数（标签、最小值、最大值、间隔）
    yaxis_opts=opts.AxisOpts(
        axislabel_opts=opts.LabelOpts(color=&apos;navy&apos;),
        min_=0,
        max_=100,
        interval=10
    ),
    # 标题相关的参数（内容、链接、位置、文本样式）
    title_opts=opts.TitleOpts(
        title=&apos;2022年销售数据展示&apos;,
        pos_left=&apos;2%&apos;,
        title_textstyle_opts=opts.TextStyleOpts(
            color=&apos;navy&apos;,
            font_size=16,
            font_family=&apos;苹方-简&apos;,
            font_weight=&apos;bold&apos;
        )
    ),
    # 工具箱相关的参数
    toolbox_opts=opts.ToolboxOpts(
        orient=&apos;vertical&apos;,
        pos_left=&apos;right&apos;
    )
)
# 加载绘图需要的JavaScript文件
bar_chart.load_javascript()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行完上面的代码后，我们就可以通过调用&lt;code&gt;bar&lt;/code&gt;对象的方法来完成对图表的渲染。如果直接使用&lt;code&gt;render&lt;/code&gt;方法，那么绘制好的统计图表将保存到一个 HTML 文件中，打开该文件也能够看到绘制好的统计图表，而&lt;code&gt;render_notebook&lt;/code&gt;方法则是将图表渲染到浏览器窗口中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bar_chart.render_notebook()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码的运行效果如下图所示。值得一提的是，下图中的标题、图例、右侧的工具箱都是可以点击的，大家可以点击它们看看会有什么样的效果，ECharts 的魅力就在于它的交互效果，大家一定要试一试。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d77eda85f0.webp&quot; style=&quot;zoom:55%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们也是通过一个官方示例，看看如何绘制饼图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pyecharts.options as opts
from pyecharts.charts import Pie

# 准备饼图需要的数据
x_data = [&quot;直接访问&quot;, &quot;邮件营销&quot;, &quot;联盟广告&quot;, &quot;视频广告&quot;, &quot;搜索引擎&quot;]
y_data = [335, 310, 234, 135, 1548]
data = [(x, y) for x, y in zip(x_data, y_data)]

# 创建饼图对象并设置初始化参数
pie_chart = Pie(init_opts=opts.InitOpts(width=&quot;800px&quot;, height=&quot;400px&quot;))
# 向饼图添加数据
pie_chart.add(
    &apos;&apos;, 
    data_pair=data,
    radius=[&quot;50%&quot;, &quot;75%&quot;],
    label_opts=opts.LabelOpts(is_show=False),
)
# 设置全局配置项
pie_chart.set_global_opts(
    # 配置图例相关的参数
    legend_opts=opts.LegendOpts(
        pos_left=&quot;legft&quot;,
        orient=&quot;vertical&quot;
    )
)
# 设置数据系列配置参数
pie_chart.set_series_opts(
    # 设置不显示工具提示
    tooltip_opts=opts.TooltipOpts(is_show=False),
    # 设置饼图标签的样式
    label_opts=opts.LabelOpts(formatter=&quot;{b}({c}): {d}%&quot;)
)
pie_chart.load_javascript()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pie_chart.render_notebook()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码，效果如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/d441b1360d.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;需要提醒大家注意的是，pyecharts 并不能直接使用 NumPy 的 ndarray 和 Pandas 的 Series、DataFrame 为其提供数据，它需要的是 Python 原生的数据类型。可能大家也注意到了，上面的代码中，我们使用的都是列表、元组这样的数据类型。&lt;/p&gt;
&lt;p&gt;最后，我们来看看如何绘制地图，绘制地图首先需要安装额外的依赖库来获取地图相关信息，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install echarts-countries-pypkg echarts-china-provinces-pypkg echarts-china-cities-pypkg echarts-china-counties-pypkg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Jupyter 中，可以直接使用魔法指令进行安装，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%pip install echarts-countries-pypkg
%pip install echarts-china-provinces-pypkg
%pip install echarts-china-cities-pypkg
%pip install echarts-china-counties-pypkg
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的四个库分别包含了世界各国、中国省级行政区域、中国市级行政区域、中国区/县级行政区域的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后，我们将全国各省的数据放在一个列表中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = [
    (&apos;广东&apos;, 594), (&apos;浙江&apos;, 438), (&apos;四川&apos;, 316), (&apos;北京&apos;, 269), (&apos;山东&apos;, 248),
    (&apos;江苏&apos;, 234), (&apos;湖南&apos;, 196), (&apos;福建&apos;, 166), (&apos;河南&apos;, 153), (&apos;辽宁&apos;, 152),
    (&apos;上海&apos;, 138), (&apos;河北&apos;, 86), (&apos;安徽&apos;, 79), (&apos;湖北&apos;, 75), (&apos;黑龙江&apos;, 70), 
    (&apos;陕西&apos;, 63), (&apos;吉林&apos;, 59), (&apos;江西&apos;, 56), (&apos;重庆&apos;, 46), (&apos;贵州&apos;, 39),
    (&apos;山西&apos;, 37), (&apos;云南&apos;, 33), (&apos;广西&apos;, 24), (&apos;天津&apos;, 22), (&apos;新疆&apos;, 21),
    (&apos;海南&apos;, 18), (&apos;内蒙古&apos;, 14), (&apos;台湾&apos;, 11), (&apos;甘肃&apos;, 7), (&apos;广西壮族自治区&apos;, 4),
    (&apos;香港&apos;, 4), (&apos;青海&apos;, 3), (&apos;新疆维吾尔自治区&apos;, 3), (&apos;内蒙古自治区&apos;, 3), (&apos;宁夏&apos;, 1)
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们使用 pyecharts 在地图上标记各省抖音大V人数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pyecharts.options as opts
from pyecharts.charts import Map

map_chart = Map(init_opts=opts.InitOpts(width=&apos;1000px&apos;, height=&apos;1000px&apos;))
map_chart.add(&apos;&apos;, data, &apos;china&apos;, is_roam=False)
map_chart.load_javascript()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;map_chart.render_notebook()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码的运行效果如下图所示，将鼠标置于地图上时，会高亮对应的省并看到相关的信息。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day66-80/9523a99dac.webp&quot; style=&quot;zoom:55%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;和 seaborn 一样，我们建议大家参考官方提供的示例来使用 pyecharts，我们可以在 pyecharts &lt;a href=&quot;https://pyecharts.org/#/zh-cn/&quot;&gt;官方网站&lt;/a&gt;的左侧导航栏中找到“图表类型”选项，下面每种类型的图表都有对应的官方示例，很多代码是可以直接使用的，我们需要做的就是将数据换成自己的数据。&lt;/p&gt;
</content:encoded></item><item><title>Day61-65 教程全文（9 篇合并）</title><link>https://www.klucen.cn/posts/python/day61-65-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day61-65-full/</guid><pubDate>Sun, 14 Dec 2025 00:15:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 9 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：网络数据采集与爬虫
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#61-%E7%BD%91%E7%BB%9C%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E6%A6%82%E8%BF%B0&quot;&gt;61 网络数据采集概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#62-%E7%94%A8python%E8%8E%B7%E5%8F%96%E7%BD%91%E7%BB%9C%E6%95%B0%E6%8D%AE&quot;&gt;62 用Python获取网络数据&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#62-%E7%94%A8python%E8%A7%A3%E6%9E%90html%E9%A1%B5%E9%9D%A2&quot;&gt;62 用Python解析HTML页面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#63-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%9C%A8%E7%88%AC%E8%99%AB%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8&quot;&gt;63 并发编程在爬虫中的应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#63-python%E4%B8%AD%E7%9A%84%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-1&quot;&gt;63 Python中的并发编程-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#63-python%E4%B8%AD%E7%9A%84%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-2&quot;&gt;63 Python中的并发编程-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#63-python%E4%B8%AD%E7%9A%84%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-3&quot;&gt;63 Python中的并发编程-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#64-%E4%BD%BF%E7%94%A8selenium%E6%8A%93%E5%8F%96%E7%BD%91%E9%A1%B5%E5%8A%A8%E6%80%81%E5%86%85%E5%AE%B9&quot;&gt;64 使用Selenium抓取网页动态内容&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#65-%E7%88%AC%E8%99%AB%E6%A1%86%E6%9E%B6scrapy%E7%AE%80%E4%BB%8B&quot;&gt;65 爬虫框架Scrapy简介&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;61 网络数据采集概述&lt;/h2&gt;
&lt;p&gt;爬虫（crawler）也经常被称为网络蜘蛛（spider），是按照一定的规则自动浏览网站并获取所需信息的机器人程序（自动化脚本代码），被广泛的应用于互联网搜索引擎和数据采集。使用过互联网和浏览器的人都知道，网页中除了供用户阅读的文字信息之外，还包含一些超链接，网络爬虫正是通过网页中的超链接信息，不断获得网络上其它页面的地址，然后持续的进行数据采集。正因如此，网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游，所以才被形象的称为爬虫或者网络蜘蛛。&lt;/p&gt;
&lt;h4&gt;爬虫的应用领域&lt;/h4&gt;
&lt;p&gt;在理想的状态下，所有 ICP（Internet Content Provider）都应该为自己的网站提供 API 接口来共享它们允许其他程序获取的数据，在这种情况下就根本不需要爬虫程序。国内比较有名的电商平台（如淘宝、京东等）、社交平台（如微博、微信等）等都提供了自己的 API 接口，但是这类 API 接口通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言，及时的获取行业数据和竞对数据是企业生存的重要环节之一，然而对大部分企业来说，数据都是其与生俱来的短板。在这种情况下，合理的利用爬虫来获取数据并从中提取出有商业价值的信息对这些企业来说就显得至关重要的。&lt;/p&gt;
&lt;p&gt;爬虫的应用领域其实非常广泛，下面我们列举了其中的一部分，有兴趣的读者可以自行探索相关内容。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;搜索引擎&lt;/li&gt;
&lt;li&gt;新闻聚合&lt;/li&gt;
&lt;li&gt;社交应用&lt;/li&gt;
&lt;li&gt;舆情监控&lt;/li&gt;
&lt;li&gt;行业数据&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;爬虫合法性探讨&lt;/h4&gt;
&lt;p&gt;经常听人说起“爬虫写得好，牢饭吃到饱”，那么编程爬虫程序是否违法呢？关于这个问题，我们可以从以下几个角度进行解读。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;网络爬虫这个领域目前还属于拓荒阶段，虽然互联网世界已经通过自己的游戏规则建立起了一定的道德规范，即 Robots 协议（全称是“网络爬虫排除标准”），但法律部分还在建立和完善中，也就是说，现在这个领域暂时还是灰色地带。&lt;/li&gt;
&lt;li&gt;“法不禁止即为许可”，如果爬虫就像浏览器一样获取的是前端显示的数据（网页上的公开信息）而不是网站后台的私密敏感信息，就不太担心法律法规的约束，因为目前大数据产业链的发展速度远远超过了法律的完善程度。&lt;/li&gt;
&lt;li&gt;在爬取网站的时候，需要限制自己的爬虫遵守 Robots 协议，同时控制网络爬虫程序的抓取数据的速度；在使用数据的时候，必须要尊重网站的知识产权（从Web 2.0时代开始，虽然Web上的数据很多都是由用户提供的，但是网站平台是投入了运营成本的，当用户在注册和发布内容时，平台通常就已经获得了对数据的所有权、使用权和分发权）。如果违反了这些规定，在打官司的时候败诉几率相当高。&lt;/li&gt;
&lt;li&gt;适当的隐匿自己的身份在编写爬虫程序时必要的，而且最好不要被对方举证你的爬虫有破坏别人动产（例如服务器）的行为。&lt;/li&gt;
&lt;li&gt;不要在公网（如代码托管平台）上去开源或者展示你的爬虫代码，这些行为通常会给自己带来不必要的麻烦。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Robots协议&lt;/h5&gt;
&lt;p&gt;大多数网站都会定义&lt;code&gt;robots.txt&lt;/code&gt;文件，这是一个君子协议，并不是所有爬虫都必须遵守的游戏规则。下面以淘宝的&lt;a href=&quot;http://www.taobao.com/robots.txt&quot;&gt;&lt;code&gt;robots.txt&lt;/code&gt;&lt;/a&gt;文件为例，看看淘宝网对爬虫有哪些限制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User-agent: Baiduspider
Disallow: /

User-agent: baiduspider
Disallow: /
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的文件可以看出，淘宝禁止百度爬虫爬取它任何资源，因此当你在百度搜索“淘宝”的时候，搜索结果下方会出现：“由于该网站的&lt;code&gt;robots.txt&lt;/code&gt;文件存在限制指令（限制搜索引擎抓取），系统无法提供该页面的内容描述”。百度作为一个搜索引擎，至少在表面上遵守了淘宝网的&lt;code&gt;robots.txt&lt;/code&gt;协议，所以用户不能从百度上搜索到淘宝内部的产品信息。&lt;/p&gt;
&lt;p&gt;图1. 百度搜索淘宝的结果&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/b01be8fd18.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;下面是豆瓣网的&lt;a href=&quot;https://www.douban.com/robots.txt&quot;&gt;&lt;code&gt;robots.txt&lt;/code&gt;&lt;/a&gt;文件，大家可以自行解读，看看它做出了什么样的限制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User-agent: *
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Disallow: /group/search
Disallow: /event/search
Disallow: /celebrities/search
Disallow: /location/drama/search
Disallow: /forum/
Disallow: /new_subject
Disallow: /service/iframe
Disallow: /j/
Disallow: /link2/
Disallow: /recommend/
Disallow: /doubanapp/card
Disallow: /update/topic/
Disallow: /share/
Allow: /ads.txt
Sitemap: https://www.douban.com/sitemap_index.xml
Sitemap: https://www.douban.com/sitemap_updated_index.xml
# Crawl-delay: 5

User-agent: Wandoujia Spider
Disallow: /

User-agent: Mediapartners-Google
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Disallow: /group/search
Disallow: /event/search
Disallow: /celebrities/search
Disallow: /location/drama/search
Disallow: /j/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;超文本传输协议（HTTP）&lt;/h4&gt;
&lt;p&gt;在开始讲解爬虫之前，我们稍微对超文本传输协议（HTTP）做一些回顾，因为我们在网页上看到的内容通常是浏览器执行 HTML （超文本标记语言）得到的结果，而 HTTP 就是传输 HTML 数据的协议。HTTP 和其他很多应用级协议一样是构建在 TCP（传输控制协议）之上的，它利用了 TCP 提供的可靠的传输服务实现了 Web 应用中的数据交换。按照维基百科上的介绍，设计 HTTP 最初的目的是为了提供一种发布和接收 &lt;a href=&quot;https://zh.wikipedia.org/wiki/HTML&quot;&gt;HTML&lt;/a&gt; 页面的方法，也就是说，这个协议是浏览器和 Web 服务器之间传输的数据的载体。关于 HTTP 的详细信息以及目前的发展状况，大家可以阅读&lt;a href=&quot;http://www.ruanyifeng.com/blog/2016/08/http.html&quot;&gt;《HTTP 协议入门》&lt;/a&gt;、&lt;a href=&quot;http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html&quot;&gt;《互联网协议入门》&lt;/a&gt;、&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html&quot;&gt;《图解 HTTPS 协议》&lt;/a&gt;等文章进行了解。&lt;/p&gt;
&lt;p&gt;下图是我在四川省网络通信技术重点实验室工作期间用开源协议分析工具 Ethereal（WireShark 的前身）截取的访问百度首页时的 HTTP 请求和响应的报文（协议数据），由于 Ethereal 截取的是经过网络适配器的数据，因此可以清晰的看到从物理链路层到应用层的协议数据。&lt;/p&gt;
&lt;p&gt;图2. HTTP请求&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/2baba63eec.png&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;HTTP 请求通常是由请求行、请求头、空行、消息体四个部分构成，如果没有数据发给服务器，消息体就不是必须的部分。请求行中包含了请求方法（GET、POST 等，如下表所示）、资源路径和协议版本；请求头由若干键值对构成，包含了浏览器、编码方式、首选语言、缓存策略等信息；请求头的后面是空行和消息体。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/e20f27d626.webp&quot; width=&quot;65%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;图3. HTTP响应&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/ffd6f1593e.png&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;HTTP 响应通常是由响应行、响应头、空行、消息体四个部分构成，其中消息体是服务响应的数据，可能是 HTML 页面，也有可能是JSON或二进制数据等。响应行中包含了协议版本和响应状态码，响应状态码有很多种，常见的如下表所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/8af1ed3681.webp&quot; width=&quot;65%&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;相关工具&lt;/h5&gt;
&lt;p&gt;下面我们先介绍一些开发爬虫程序的辅助工具，这些工具相信能帮助你事半功倍。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Chrome Developer Tools：谷歌浏览器内置的开发者工具。该工具最常用的几个功能模块是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;元素（ELements）：用于查看或修改 HTML 元素的属性、CSS 属性、监听事件等。CSS 可以即时修改，即时显示，大大方便了开发者调试页面。&lt;/li&gt;
&lt;li&gt;控制台（Console）：用于执行一次性代码，查看 JavaScript 对象，查看调试日志信息或异常信息。控制台其实就是一个执行 JavaScript 代码的交互式环境。&lt;/li&gt;
&lt;li&gt;源代码（Sources）：用于查看页面的 HTML 文件源代码、JavaScript 源代码、CSS 源代码，此外最重要的是可以调试 JavaScript 源代码，可以给代码添加断点和单步执行。&lt;/li&gt;
&lt;li&gt;网络（Network）：用于 HTTP 请求、HTTP 响应以及与网络连接相关的信息。&lt;/li&gt;
&lt;li&gt;应用（Application）：用于查看浏览器本地存储、后台任务等内容，本地存储主要包括Cookie、Local Storage、Session Storage等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/9d76f7c5eb.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Postman：功能强大的网页调试与 RESTful 请求工具。Postman可以帮助我们模拟请求，非常方便的定制我们的请求以及查看服务器的响应。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/459c694b83.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTPie：命令行HTTP客户端。&lt;/p&gt;
&lt;p&gt;安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install httpie
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http --header http --header https://movie.douban.com/

HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 24 Aug 2021 16:48:00 GMT
Keep-Alive: timeout=30
Server: dae
Set-Cookie: bid=58h4BdKC9lM; Expires=Wed, 24-Aug-22 16:48:00 GMT; Domain=.douban.com; Path=/
Strict-Transport-Security: max-age=15552000
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-DOUBAN-NEWBID: 58h4BdKC9lM
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;builtwith&lt;/code&gt;库：识别网站所用技术的工具。&lt;/p&gt;
&lt;p&gt;安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install builtwith
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ssl

import builtwith

ssl._create_default_https_context = ssl._create_unverified_context
print(builtwith.parse(&apos;http://www.bootcss.com/&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;python-whois&lt;/code&gt;库：查询网站所有者的工具。&lt;/p&gt;
&lt;p&gt;安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install python-whois
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import whois

print(whois.whois(&apos;https://www.bootcss.com&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;爬虫的基本工作流程&lt;/h4&gt;
&lt;p&gt;一个基本的爬虫通常分为数据采集（网页下载）、数据处理（网页解析）和数据存储（将有用的信息持久化）三个部分的内容，当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术，这就需要有调度器（安排线程或进程执行对应的任务）、后台管理程序（监控爬虫的工作状态以及检查数据抓取的结果）等的参与。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/f0d6ea6e50.webp&quot; style=&quot;zoom:62%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;一般来说，爬虫的工作流程包括以下几个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设定抓取目标（种子页面/起始页面）并获取网页。&lt;/li&gt;
&lt;li&gt;当服务器无法访问时，按照指定的重试次数尝试重新下载页面。&lt;/li&gt;
&lt;li&gt;在需要的时候设置用户代理或隐藏真实IP，否则可能无法访问页面。&lt;/li&gt;
&lt;li&gt;对获取的页面进行必要的解码操作然后抓取出需要的信息。&lt;/li&gt;
&lt;li&gt;在获取的页面中通过某种方式（如正则表达式）抽取出页面中的链接信息。&lt;/li&gt;
&lt;li&gt;对链接进行进一步的处理（获取页面并重复上面的动作）。&lt;/li&gt;
&lt;li&gt;将有用的信息进行持久化以备后续的处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;62 用Python获取网络数据&lt;/h2&gt;
&lt;p&gt;网络数据采集是 Python 语言非常擅长的领域，上节课我们讲到，实现网络数据采集的程序通常称之为网络爬虫或蜘蛛程序。即便是在大数据时代，数据对于中小企业来说仍然是硬伤和短板，有些数据需要通过开放或付费的数据接口来获得，其他的行业数据和竞对数据则必须要通过网络数据采集的方式来获得。不管使用哪种方式获取网络数据资源，Python 语言都是非常好的选择，因为 Python 的标准库和三方库都对网络数据采集提供了良好的支持。&lt;/p&gt;
&lt;h4&gt;requests库&lt;/h4&gt;
&lt;p&gt;要使用 Python 获取网络数据，我们推荐大家使用名为&lt;code&gt;requests&lt;/code&gt; 的三方库，这个库我们在之前的课程中其实已经使用过了。按照官方网站的解释，&lt;code&gt;requests&lt;/code&gt;是基于 Python 标准库进行了封装，简化了通过 HTTP 或 HTTPS 访问网络资源的操作。上课我们提到过，HTTP 是一个请求响应式的协议，当我们在浏览器中输入正确的 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL&quot;&gt;URL&lt;/a&gt;（通常也称为网址）并按下 Enter 键时，我们就向网络上的 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_web_server&quot;&gt;Web 服务器&lt;/a&gt;发送了一个 HTTP 请求，服务器在收到请求后会给我们一个 HTTP 响应。在 Chrome 浏览器中的菜单中打开“开发者工具”切换到“Network”选项卡就能够查看 HTTP 请求和响应到底是什么样子的，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/052a85da11.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;通过&lt;code&gt;requests&lt;/code&gt;库，我们可以让 Python 程序向浏览器一样向 Web 服务器发起请求，并接收服务器返回的响应，从响应中我们就可以提取出想要的数据。浏览器呈现给我们的网页是用 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML&quot;&gt;HTML&lt;/a&gt; 编写的，浏览器相当于是 HTML 的解释器环境，我们看到的网页中的内容都包含在 HTML 的标签中。在获取到 HTML 代码后，就可以从标签的属性或标签体中提取内容。下面例子演示了如何获取网页 HTML 代码，我们通过&lt;code&gt;requests&lt;/code&gt;库的&lt;code&gt;get&lt;/code&gt;函数，获取了搜狐首页的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

resp = requests.get(&apos;https://www.sohu.com/&apos;)
if resp.status_code == 200:
    print(resp.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的变量&lt;code&gt;resp&lt;/code&gt;是一个&lt;code&gt;Response&lt;/code&gt;对象（&lt;code&gt;requests&lt;/code&gt;库封装的类型），通过该对象的&lt;code&gt;status_code&lt;/code&gt;属性可以获取响应状态码，而该对象的&lt;code&gt;text&lt;/code&gt;属性可以帮我们获取到页面的 HTML 代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于&lt;code&gt;Response&lt;/code&gt;对象的&lt;code&gt;text&lt;/code&gt;是一个字符串，所以我们可以利用之前讲过的正则表达式的知识，从页面的 HTML 代码中提取新闻的标题和链接，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

import requests

pattern = re.compile(r&apos;&amp;lt;a.*?href=&quot;(.*?)&quot;.*?title=&quot;(.*?)&quot;.*?&amp;gt;&apos;)
resp = requests.get(&apos;https://www.sohu.com/&apos;)
if resp.status_code == 200:
    all_matches = pattern.findall(resp.text)
    for href, title in all_matches:
        print(href)
        print(title)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了文本内容，我们也可以使用&lt;code&gt;requests&lt;/code&gt;库通过 URL 获取二进制资源。下面的例子演示了如何获取百度 Logo 并保存到名为&lt;code&gt;baidu.png&lt;/code&gt;的本地文件中。可以在百度的首页上右键点击百度Logo，并通过“复制图片地址”菜单项获取图片的 URL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

resp = requests.get(&apos;https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png&apos;)
with open(&apos;baidu.png&apos;, &apos;wb&apos;) as file:
    file.write(resp.content)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;Response&lt;/code&gt;对象的&lt;code&gt;content&lt;/code&gt;属性可以获得服务器响应的二进制数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;requests&lt;/code&gt;库非常好用而且功能上也比较强大和完整，具体的内容我们在使用的过程中为大家一点点剖析。想解锁关于&lt;code&gt;requests&lt;/code&gt;库更多的知识，可以阅读它的&lt;a href=&quot;https://docs.python-requests.org/zh_CN/latest/&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;编写爬虫代码&lt;/h4&gt;
&lt;p&gt;接下来，我们以“豆瓣电影”为例，为大家讲解如何编写爬虫代码。按照上面提供的方法，我们先使用&lt;code&gt;requests&lt;/code&gt;获取到网页的HTML代码，然后将整个代码看成一个长字符串，这样我们就可以使用正则表达式的捕获组从字符串提取我们需要的内容。下面的代码演示了如何从&lt;a href=&quot;https://movie.douban.com/&quot;&gt;豆瓣电影&lt;/a&gt;获取排前250名的电影的名称。&lt;a href=&quot;https://movie.douban.com/top250&quot;&gt;豆瓣电影Top250&lt;/a&gt;的页面结构和对应代码如下图所示，可以看出，每页共展示了25部电影，如果要获取到 Top250 数据，我们共需要访问10个页面，对应的地址是&lt;a href=&quot;https://movie.douban.com/top250?start=xxx&quot;&gt;https://movie.douban.com/top250?start=xxx&lt;/a&gt;，这里的&lt;code&gt;xxx&lt;/code&gt;如果为&lt;code&gt;0&lt;/code&gt;就是第一页，如果&lt;code&gt;xxx&lt;/code&gt;的值是&lt;code&gt;100&lt;/code&gt;，那么我们可以访问到第五页。为了代码简单易读，我们只获取电影的标题和评分。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/62d587a48f.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import re
import time

import requests

for page in range(1, 11):
    resp = requests.get(
        url=f&apos;https://movie.douban.com/top250?start={(page - 1) * 25}&apos;,
        # 如果不设置HTTP请求头中的User-Agent，豆瓣会检测出不是浏览器而阻止我们的请求。
        # 通过get函数的headers参数设置User-Agent的值，具体的值可以在浏览器的开发者工具查看到。
        # 用爬虫访问大部分网站时，将爬虫伪装成来自浏览器的请求都是非常重要的一步。
        headers={&apos;User-Agent&apos;: &apos;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36&apos;}
    )
    # 通过正则表达式获取class属性为title且标签体不以&amp;amp;开头的span标签并用捕获组提取标签内容
    pattern1 = re.compile(r&apos;&amp;lt;span class=&quot;title&quot;&amp;gt;([^&amp;amp;]*?)&amp;lt;/span&amp;gt;&apos;)
    titles = pattern1.findall(resp.text)
    # 通过正则表达式获取class属性为rating_num的span标签并用捕获组提取标签内容
    pattern2 = re.compile(r&apos;&amp;lt;span class=&quot;rating_num&quot;.*?&amp;gt;(.*?)&amp;lt;/span&amp;gt;&apos;)
    ranks = pattern2.findall(resp.text)
    # 使用zip压缩两个列表，循环遍历所有的电影标题和评分
    for title, rank in zip(titles, ranks):
        print(title, rank)
    # 随机休眠1-5秒，避免爬取页面过于频繁
    time.sleep(random.random() * 4 + 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：通过分析豆瓣网的robots协议，我们发现豆瓣网并不拒绝百度爬虫获取它的数据，因此我们也可以将爬虫伪装成百度的爬虫，将&lt;code&gt;get&lt;/code&gt;函数的&lt;code&gt;headers&lt;/code&gt;参数修改为：&lt;code&gt;headers={&apos;User-Agent&apos;: &apos;BaiduSpider&apos;}&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;使用 IP 代理&lt;/h4&gt;
&lt;p&gt;让爬虫程序隐匿自己的身份对编写爬虫程序来说是比较重要的，很多网站对爬虫都比较反感的，因为爬虫会耗费掉它们很多的网络带宽并制造很多无效的流量。要隐匿身份通常需要使用&lt;strong&gt;商业 IP 代理&lt;/strong&gt;（如蘑菇代理、芝麻代理、快代理等），让被爬取的网站无法获取爬虫程序来源的真实 IP 地址，也就无法简单的通过 IP 地址对爬虫程序进行封禁。&lt;/p&gt;
&lt;p&gt;下面以&lt;a href=&quot;http://www.moguproxy.com/&quot;&gt;蘑菇代理&lt;/a&gt;为例，为大家讲解商业 IP 代理的使用方法。首先需要在该网站注册一个账号，注册账号后就可以&lt;a href=&quot;http://www.moguproxy.com/buy&quot;&gt;购买&lt;/a&gt;相应的套餐来获得商业 IP 代理。作为商业用途，建议大家购买不限量套餐，这样可以根据实际需要获取足够多的代理 IP 地址；作为学习用途，可以购买包时套餐或根据自己的需求来决定。蘑菇代理提供了两种接入代理的方式，分别是 API 私密代理和 HTTP 隧道代理，前者是通过请求蘑菇代理的 API 接口获取代理服务器地址，后者是直接使用统一的入口（蘑菇代理提供的域名）进行接入。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/8a2ab58222.webp&quot; width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;下面，我们以HTTP隧道代理为例，为大家讲解接入 IP 代理的方式，大家也可以直接参考蘑菇代理官网提供的代码来为爬虫设置代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

APP_KEY = &apos;Wnp******************************XFx&apos;
PROXY_HOST = &apos;secondtransfer.moguproxy.com:9001&apos;

for page in range(1, 11):
    resp = requests.get(
        url=f&apos;https://movie.douban.com/top250?start={(page - 1) * 25}&apos;,
        # 需要在HTTP请求头设置代理的身份认证方式
        headers={
            &apos;Proxy-Authorization&apos;: f&apos;Basic {APP_KEY}&apos;,
            &apos;User-Agent&apos;: &apos;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36&apos;,
            &apos;Accept-Language&apos;: &apos;zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4&apos;
        },
        # 设置代理服务器
        proxies={
            &apos;http&apos;: f&apos;http://{PROXY_HOST}&apos;,
            &apos;https&apos;: f&apos;https://{PROXY_HOST}&apos;
        },
        verify=False
    )
    pattern1 = re.compile(r&apos;&amp;lt;span class=&quot;title&quot;&amp;gt;([^&amp;amp;]*?)&amp;lt;/span&amp;gt;&apos;)
    titles = pattern1.findall(resp.text)
    pattern2 = re.compile(r&apos;&amp;lt;span class=&quot;rating_num&quot;.*?&amp;gt;(.*?)&amp;lt;/span&amp;gt;&apos;)
    ranks = pattern2.findall(resp.text)
    for title, rank in zip(titles, ranks):
        print(title, rank)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码需要修改&lt;code&gt;APP_KEY&lt;/code&gt;为自己创建的订单对应的&lt;code&gt;Appkey&lt;/code&gt;值，这个值可以在用户中心用户订单中查看到。蘑菇代理提供了免费的 API 代理和 HTTP 隧道代理试用，但是试用的代理接通率不能保证，建议大家还是直接购买一个在自己支付能力范围内的代理服务来体验。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 语言能做的事情真的很多，就网络数据采集这一项而言，Python 几乎是一枝独秀的，大量的企业和个人都在使用 Python 从网络上获取自己需要的数据，这可能也是你将来日常工作的一部分。另外，用编写正则表达式的方式从网页中提取内容虽然可行，但是写出一个能够满足需求的正则表达式本身也不是件容易的事情，这一点对于新手来说尤为明显。在下一节课中，我们将会为大家介绍另外两种从页面中提取数据的方法，虽然从性能上来讲，它们可能不如正则表达式，但是却降低了编码的复杂性，相信大家会喜欢上它们的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;62 用Python解析HTML页面&lt;/h2&gt;
&lt;p&gt;在前面的课程中，我们讲到了使用&lt;code&gt;request&lt;/code&gt;三方库获取网络资源，还介绍了一些前端的基础知识。接下来，我们继续探索如何解析 HTML 代码，从页面中提取出有用的信息。之前，我们尝试过用正则表达式的捕获组操作提取页面内容，但是写出一个正确的正则表达式也是一件让人头疼的事情。为了解决这个问题，我们得先深入的了解一下 HTML 页面的结构，并在此基础上研究另外的解析页面的方法。&lt;/p&gt;
&lt;h4&gt;HTML 页面的结构&lt;/h4&gt;
&lt;p&gt;我们在浏览器中打开任意一个网站，然后通过鼠标右键菜单，选择“显示网页源代码”菜单项，就可以看到网页对应的 HTML 代码。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/150b21f360.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;代码的第&lt;code&gt;1&lt;/code&gt;行是文档类型声明，第&lt;code&gt;2&lt;/code&gt;行的&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;标签是整个页面根标签的开始标签，最后一行是根标签的结束标签&lt;code&gt;&amp;lt;/html&amp;gt;&lt;/code&gt;。&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;标签下面有两个子标签&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;，放在&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;标签下的内容会显示在浏览器窗口中，这部分内容是网页的主体；放在&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;标签下的内容不会显示在浏览器窗口中，但是却包含了页面重要的元信息，通常称之为网页的头部。HTML 页面大致的代码结构如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;!-- 页面的元信息，如字符编码、标题、关键字、媒体查询等 --&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;!-- 页面的主体，显示在浏览器窗口中的内容 --&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标签、层叠样式表（CSS）、JavaScript 是构成 HTML 页面的三要素，其中标签用来承载页面要显示的内容，CSS 负责对页面的渲染，而 JavaScript 用来控制页面的交互式行为。要实现 HTML 页面的解析，可以使用 XPath 的语法，它原本是 XML 的一种查询语法，可以根据 HTML 标签的层次结构提取标签中的内容或标签属性；此外，也可以使用 CSS 选择器来定位页面元素，就跟用 CSS 渲染页面元素是同样的道理。&lt;/p&gt;
&lt;h4&gt;XPath 解析&lt;/h4&gt;
&lt;p&gt;XPath 是在 XML（eXtensible Markup Language）文档中查找信息的一种语法，XML 跟 HTML 类似也是一种用标签承载数据的标签语言，不同之处在于 XML 的标签是可扩展的，可以自定义的，而且 XML 对语法有更严格的要求。XPath 使用路径表达式来选取 XML 文档中的节点或者节点集，这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。下面我们通过一个例子来说明如何使用 XPath 对页面进行解析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;bookstore&amp;gt;
    &amp;lt;book&amp;gt;
      &amp;lt;title lang=&quot;eng&quot;&amp;gt;Harry Potter&amp;lt;/title&amp;gt;
      &amp;lt;price&amp;gt;29.99&amp;lt;/price&amp;gt;
    &amp;lt;/book&amp;gt;
    &amp;lt;book&amp;gt;
      &amp;lt;title lang=&quot;zh&quot;&amp;gt;Learning XML&amp;lt;/title&amp;gt;
      &amp;lt;price&amp;gt;39.95&amp;lt;/price&amp;gt;
    &amp;lt;/book&amp;gt;
&amp;lt;/bookstore&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上面的 XML 文件，我们可以用如下所示的 XPath 语法获取文档中的节点。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径表达式&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取根元素 bookstore。&lt;strong&gt;注意&lt;/strong&gt;：假如路径起始于正斜杠( / )，则此路径始终代表到某元素的绝对路径！&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//book&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取所有 book 子元素，而不管它们在文档中的位置。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//@lang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取名为 lang 的所有属性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取属于 bookstore 子元素的第一个 book 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[last()]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取属于 bookstore 子元素的最后一个 book 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[last()-1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取属于 bookstore 子元素的倒数第二个 book 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[position()&amp;lt;3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取最前面的两个属于 bookstore 元素的子元素的 book 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//title[@lang]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取所有拥有名为 lang 的属性的 title 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//title[@lang=&apos;eng&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取所有 title 元素，且这些元素拥有值为 eng 的 lang 属性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[price&amp;gt;35.00]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取 bookstore 元素的所有 book 元素，且其中的 price 元素的值须大于 35.00。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book[price&amp;gt;35.00]/title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取 bookstore 元素中的 book 元素的所有 title 元素，且其中的 price 元素的值须大于 35.00。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;XPath还支持通配符用法，如下所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径表达式&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取 bookstore 元素的所有子元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取文档中的所有元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//title[@*]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取所有带有属性的 title 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果要选取多个节点，可以使用如下所示的方法。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径表达式&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//book/title | //book/price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取 book 元素的所有 title 和 price 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//title | //price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取文档中的所有 title 和 price 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bookstore/book/title | //price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取属于 bookstore 元素的 book 元素的所有 title 元素，以及文档中所有的 price 元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的例子来自于“菜鸟教程”网站上的 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day61-65/%3Chttps:/www.runoob.com/xpath/xpath-tutorial.html%3E&quot;&gt;XPath 教程&lt;/a&gt;，有兴趣的读者可以自行阅读原文。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，如果不理解或不熟悉 XPath 语法，可以在浏览器的开发者工具中按照如下所示的方法查看元素的 XPath 语法，下图是在 Chrome 浏览器的开发者工具中查看豆瓣网电影详情信息中影片标题的 XPath 语法。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/c609057453.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;实现 XPath 解析需要三方库&lt;code&gt;lxml&lt;/code&gt; 的支持，可以使用下面的命令安装&lt;code&gt;lxml&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install lxml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们用 XPath 解析方式改写之前获取豆瓣电影 Top250的代码，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from lxml import etree
import requests

for page in range(1, 11):
    resp = requests.get(
        url=f&apos;https://movie.douban.com/top250?start={(page - 1) * 25}&apos;,
        headers={&apos;User-Agent&apos;: &apos;BaiduSpider&apos;}
    )
    tree = etree.HTML(resp.text)
    # 通过XPath语法从页面中提取电影标题
    title_spans = tree.xpath(&apos;//*[@id=&quot;content&quot;]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]&apos;)
    # 通过XPath语法从页面中提取电影评分
    rank_spans = tree.xpath(&apos;//*[@id=&quot;content&quot;]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]&apos;)
    for title_span, rank_span in zip(title_spans, rank_spans):
        print(title_span.text, rank_span.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;CSS 选择器解析&lt;/h4&gt;
&lt;p&gt;对于熟悉 CSS 选择器和 JavaScript 的开发者来说，通过 CSS 选择器获取页面元素可能是更为简单的选择，因为浏览器中运行的 JavaScript 本身就可以&lt;code&gt;document&lt;/code&gt;对象的&lt;code&gt;querySelector()&lt;/code&gt;和&lt;code&gt;querySelectorAll()&lt;/code&gt;方法基于 CSS 选择器获取页面元素。在 Python 中，我们可以利用三方库&lt;code&gt;beautifulsoup4&lt;/code&gt;或&lt;code&gt;pyquery&lt;/code&gt;来做同样的事情。Beautiful Soup 可以用来解析 HTML 和 XML 文档，修复含有未闭合标签等错误的文档，通过为待解析的页面在内存中创建一棵树结构，实现对从页面中提取数据操作的封装。可以用下面的命令来安装 Beautiful Soup。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是使用&lt;code&gt;bs4&lt;/code&gt;改写的获取豆瓣电影Top250电影名称的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import bs4
import requests

for page in range(1, 11):
    resp = requests.get(
        url=f&apos;https://movie.douban.com/top250?start={(page - 1) * 25}&apos;,
        headers={&apos;User-Agent&apos;: &apos;BaiduSpider&apos;}
    )
    # 创建BeautifulSoup对象
    soup = bs4.BeautifulSoup(resp.text, &apos;lxml&apos;)
    # 通过CSS选择器从页面中提取包含电影标题的span标签
    title_spans = soup.select(&apos;div.info &amp;gt; div.hd &amp;gt; a &amp;gt; span:nth-child(1)&apos;)
    # 通过CSS选择器从页面中提取包含电影评分的span标签
    rank_spans = soup.select(&apos;div.info &amp;gt; div.bd &amp;gt; div &amp;gt; span.rating_num&apos;)
    for title_span, rank_span in zip(title_spans, rank_spans):
        print(title_span.text, rank_span.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 BeautifulSoup 更多的知识，可以参考它的&lt;a href=&quot;https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;下面我们对三种解析方式做一个简单比较。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;解析方式&lt;/th&gt;
&lt;th&gt;对应的模块&lt;/th&gt;
&lt;th&gt;速度&lt;/th&gt;
&lt;th&gt;使用难度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;正则表达式解析&lt;/td&gt;
&lt;td&gt;&lt;code&gt;re&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;快&lt;/td&gt;
&lt;td&gt;困难&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XPath 解析&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lxml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;快&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS 选择器解析&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bs4&lt;/code&gt;或&lt;code&gt;pyquery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不确定&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;63 并发编程在爬虫中的应用&lt;/h2&gt;
&lt;p&gt;之前的课程，我们已经为大家介绍了 Python 中的多线程、多进程和异步编程，通过这三种手段，我们可以实现并发或并行编程，这一方面可以加速代码的执行，另一方面也可以带来更好的用户体验。爬虫程序是典型的 I/O 密集型任务，对于 I/O 密集型任务来说，多线程和异步 I/O 都是很好的选择，因为当程序的某个部分因 I/O 操作阻塞时，程序的其他部分仍然可以运转，这样我们不用在等待和阻塞中浪费大量的时间。下面我们以爬取“&lt;a href=&quot;https://image.so.com/&quot;&gt;360图片&lt;/a&gt;”网站的图片并保存到本地为例，为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别，同时也对它们的执行效率进行简单的对比。&lt;/p&gt;
&lt;p&gt;“360图片”网站的页面使用了 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX&quot;&gt;Ajax&lt;/a&gt; 技术，这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说，页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的，而且整个页面还使用了瀑布式加载（一边向下滚动，一边加载更多的图片）。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口，如下图所示，我们需要的图片信息就在服务器返回的 JSON 数据中。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/65e0bdb1fb.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;例如，要获取“美女”频道的图片，我们可以请求如下所示的URL，其中参数&lt;code&gt;ch&lt;/code&gt;表示请求的频道，&lt;code&gt;=&lt;/code&gt;后面的参数值&lt;code&gt;beauty&lt;/code&gt;就代表了“美女”频道，参数&lt;code&gt;sn&lt;/code&gt;相当于是页码，&lt;code&gt;0&lt;/code&gt;表示第一页（共&lt;code&gt;30&lt;/code&gt;张图片），&lt;code&gt;30&lt;/code&gt;表示第二页，&lt;code&gt;60&lt;/code&gt;表示第三页，以此类推。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://image.so.com/zjl?ch=beauty&amp;amp;sn=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;单线程版本&lt;/h4&gt;
&lt;p&gt;通过上面的 URL 下载“美女”频道共&lt;code&gt;90&lt;/code&gt;张图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
example04.py - 单线程版本爬虫
&quot;&quot;&quot;
import os

import requests


def download_picture(url):
    filename = url[url.rfind(&apos;/&apos;) + 1:]
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(f&apos;images/beauty/{filename}&apos;, &apos;wb&apos;) as file:
            file.write(resp.content)


def main():
    if not os.path.exists(&apos;images/beauty&apos;):
        os.makedirs(&apos;images/beauty&apos;)
    for page in range(3):
        resp = requests.get(f&apos;https://image.so.com/zjl?ch=beauty&amp;amp;sn={page * 30}&apos;)
        if resp.status_code == 200:
            pic_dict_list = resp.json()[&apos;list&apos;]
            for pic_dict in pic_dict_list:
                download_picture(pic_dict[&apos;qhimg_url&apos;])

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 macOS 或 Linux 系统上，我们可以使用&lt;code&gt;time&lt;/code&gt;命令来了解上面代码的执行时间以及 CPU 的利用率，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time python3 example04.py 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是单线程爬虫代码在我的电脑上执行的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 example04.py  2.36s user 0.39s system 12% cpu 21.578 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们只需要关注代码的总耗时为&lt;code&gt;21.578&lt;/code&gt;秒，CPU 利用率为&lt;code&gt;12%&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;多线程版本&lt;/h4&gt;
&lt;p&gt;我们使用之前讲到过的线程池技术，将上面的代码修改为多线程版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
example05.py - 多线程版本爬虫
&quot;&quot;&quot;
import os
from concurrent.futures import ThreadPoolExecutor

import requests


def download_picture(url):
    filename = url[url.rfind(&apos;/&apos;) + 1:]
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(f&apos;images/beauty/{filename}&apos;, &apos;wb&apos;) as file:
            file.write(resp.content)


def main():
    if not os.path.exists(&apos;images/beauty&apos;):
        os.makedirs(&apos;images/beauty&apos;)
    with ThreadPoolExecutor(max_workers=16) as pool:
        for page in range(3):
            resp = requests.get(f&apos;https://image.so.com/zjl?ch=beauty&amp;amp;sn={page * 30}&apos;)
            if resp.status_code == 200:
                pic_dict_list = resp.json()[&apos;list&apos;]
                for pic_dict in pic_dict_list:
                    pool.submit(download_picture, pic_dict[&apos;qhimg_url&apos;])


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行如下所示的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time python3 example05.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码的执行结果如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 example05.py  2.65s user 0.40s system 95% cpu 3.193 total
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;异步I/O版本&lt;/h4&gt;
&lt;p&gt;我们使用&lt;code&gt;aiohttp&lt;/code&gt;将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作，我们首先得安装三方库&lt;code&gt;aiohttp&lt;/code&gt;和&lt;code&gt;aiofile&lt;/code&gt;，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install aiohttp aiofile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aiohttp&lt;/code&gt; 的用法在之前的课程中已经做过简要介绍，&lt;code&gt;aiofile&lt;/code&gt;模块中的&lt;code&gt;async_open&lt;/code&gt;函数跟 Python 内置函数&lt;code&gt;open&lt;/code&gt;的用法大致相同，只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
example06.py - 异步I/O版本爬虫
&quot;&quot;&quot;
import asyncio
import json
import os

import aiofile
import aiohttp


async def download_picture(session, url):
    filename = url[url.rfind(&apos;/&apos;) + 1:]
    async with session.get(url, ssl=False) as resp:
        if resp.status == 200:
            data = await resp.read()
            async with aiofile.async_open(f&apos;images/beauty/{filename}&apos;, &apos;wb&apos;) as file:
                await file.write(data)


async def fetch_json():
    async with aiohttp.ClientSession() as session:
        for page in range(3):
            async with session.get(
                url=f&apos;https://image.so.com/zjl?ch=beauty&amp;amp;sn={page * 30}&apos;,
                ssl=False
            ) as resp:
                if resp.status == 200:
                    json_str = await resp.text()
                    result = json.loads(json_str)
                    for pic_dict in result[&apos;list&apos;]:
                        await download_picture(session, pic_dict[&apos;qhimg_url&apos;])


def main():
    if not os.path.exists(&apos;images/beauty&apos;):
        os.makedirs(&apos;images/beauty&apos;)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(fetch_json())
    loop.close()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行如下所示的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time python3 example06.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码的执行结果如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 example06.py  0.82s user 0.21s system 27% cpu 3.782 total
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;通过上面三段代码执行结果的比较，我们可以得出一个结论，使用多线程和异步 I/O 都可以改善爬虫程序的性能，因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上，而&lt;code&gt;time&lt;/code&gt;命令的执行结果也告诉我们，单线程的代码 CPU 利用率仅仅只有&lt;code&gt;12%&lt;/code&gt;，而多线程版本的 CPU 利用率则高达&lt;code&gt;95%&lt;/code&gt;；单线程版本的爬虫执行时间约&lt;code&gt;21&lt;/code&gt;秒，而多线程和异步 I/O 的版本仅执行了&lt;code&gt;3&lt;/code&gt;秒钟。另外，在运行时间差别不大的情况下，多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源，这是因为多线程的调度和切换也需要花费 CPU 时间。至此，三种方式在 I/O 密集型任务上的优劣已经一目了然，当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢，那么使用多线程和异步 I/O 的优势将更为明显，有兴趣的读者可以自行试验。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;63 Python中的并发编程-1&lt;/h2&gt;
&lt;p&gt;现如今，我们使用的计算机早已是多 CPU 或多核的计算机，而我们使用的操作系统基本都支持“多任务”，这使得我们可以同时运行多个程序，也可以将一个程序分解为若干个相对独立的子任务，让多个子任务“并行”或“并发”的执行，从而缩短程序的执行时间，同时也让用户获得更好的体验。因此当下，不管用什么编程语言进行开发，实现“并行”或“并发”编程已经成为了程序员的标配技能。为了讲述如何在 Python 程序中实现“并行”或“并发”，我们需要先了解两个重要的概念：进程和线程。&lt;/p&gt;
&lt;h4&gt;线程和进程&lt;/h4&gt;
&lt;p&gt;我们通过操作系统运行一个程序会创建出一个或多个进程，进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。简单的说，进程是操作系统分配存储空间的基本单位，每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据；操作系统管理所有进程的执行，为它们合理的分配资源。一个进程可以通过 fork 或 spawn 的方式创建新的进程来执行其他的任务，不过新的进程也有自己独立的内存空间，因此两个进程如果要共享数据，必须通过进程间通信机制来实现，具体的方式包括管道、信号、套接字等。&lt;/p&gt;
&lt;p&gt;一个进程还可以拥有多个执行线索，简单的说就是拥有多个可以获得 CPU 调度的执行单元，这就是所谓的线程。由于线程在同一个进程下，它们可以共享相同的上下文，因此相对于进程而言，线程间的信息共享和通信更加容易。当然在单核 CPU 系统中，多个线程不可能同时执行，因为在某个时刻只有一个线程能够获得 CPU，多个线程通过共享 CPU 执行时间的方式来达到并发的效果。&lt;/p&gt;
&lt;p&gt;在程序中使用多线程技术通常都会带来不言而喻的好处，最主要的体现在提升程序的性能和改善用户体验，今天我们使用的软件几乎都用到了多线程技术，这一点可以利用系统自带的进程监控工具（如 macOS 中的“活动监视器”、Windows 中的“任务管理器”）来证实，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/d0549b981e.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;这里，我们还需要跟大家再次强调两个概念：&lt;strong&gt;并发&lt;/strong&gt;（concurrency）和&lt;strong&gt;并行&lt;/strong&gt;（parallel）。&lt;strong&gt;并发&lt;/strong&gt;通常是指同一时刻只能有一条指令执行，但是多个线程对应的指令被快速轮换地执行。比如一个处理器，它先执行线程 A 的指令一段时间，再执行线程 B 的指令一段时间，再切回到线程 A 执行一段时间。由于处理器执行指令的速度和切换的速度极快，人们完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作，这就使得宏观上看起来多个线程在同时运行，但微观上其实只有一个线程在执行。&lt;strong&gt;并行&lt;/strong&gt;是指同一时刻，有多条指令在多个处理器上同时执行，并行必须要依赖于多个处理器，不论是从宏观上还是微观上，多个线程可以在同一时刻一起执行的。很多时候，我们并不用严格区分并发和并行两个词，所以我们有时候也把 Python 中的多线程、多进程以及异步 I/O 都视为实现并发编程的手段，但实际上前面两者也可以实现并行编程，当然这里还有一个全局解释器锁（GIL）的问题，我们稍后讨论。&lt;/p&gt;
&lt;h4&gt;多线程编程&lt;/h4&gt;
&lt;p&gt;Python 标准库中&lt;code&gt;threading&lt;/code&gt;模块的&lt;code&gt;Thread&lt;/code&gt;类可以帮助我们非常轻松的实现多线程编程。我们用一个联网下载文件的例子来对比使用多线程和不使用多线程到底有什么区别，代码如下所示。&lt;/p&gt;
&lt;p&gt;不使用多线程的下载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time


def download(*, filename):
    start = time.time()
    print(f&apos;开始下载 {filename}.&apos;)
    time.sleep(random.randint(3, 6))
    print(f&apos;{filename} 下载完成.&apos;)
    end = time.time()
    print(f&apos;下载耗时: {end - start:.3f}秒.&apos;)


def main():
    start = time.time()
    download(filename=&apos;Python从入门到住院.pdf&apos;)
    download(filename=&apos;MySQL从删库到跑路.avi&apos;)
    download(filename=&apos;Linux从精通到放弃.mp4&apos;)
    end = time.time()
    print(f&apos;总耗时: {end - start:.3f}秒.&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码并没有真正实现联网下载的功能，而是通过&lt;code&gt;time.sleep()&lt;/code&gt;休眠一段时间来模拟下载文件需要一些时间上的开销，跟实际下载的状况比较类似。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;运行上面的代码，可以得到如下所示的运行结果。可以看出，当我们的程序只有一个工作线程时，每个下载任务都需要等待上一个下载任务执行结束才能开始，所以程序执行的总耗时是三个下载任务各自执行时间的总和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开始下载Python从入门到住院.pdf.
Python从入门到住院.pdf下载完成.
下载耗时: 3.005秒.
开始下载MySQL从删库到跑路.avi.
MySQL从删库到跑路.avi下载完成.
下载耗时: 5.006秒.
开始下载Linux从精通到放弃.mp4.
Linux从精通到放弃.mp3下载完成.
下载耗时: 6.007秒.
总耗时: 14.018秒.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，上面的三个下载任务之间并没有逻辑上的因果关系，三者是可以“并发”的，下一个下载任务没有必要等待上一个下载任务结束，为此，我们可以使用多线程编程来改写上面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time
from threading import Thread


def download(*, filename):
    start = time.time()
    print(f&apos;开始下载 {filename}.&apos;)
    time.sleep(random.randint(3, 6))
    print(f&apos;{filename} 下载完成.&apos;)
    end = time.time()
    print(f&apos;下载耗时: {end - start:.3f}秒.&apos;)


def main():
    threads = [
        Thread(target=download, kwargs={&apos;filename&apos;: &apos;Python从入门到住院.pdf&apos;}),
        Thread(target=download, kwargs={&apos;filename&apos;: &apos;MySQL从删库到跑路.avi&apos;}),
        Thread(target=download, kwargs={&apos;filename&apos;: &apos;Linux从精通到放弃.mp4&apos;})
    ]
    start = time.time()
    # 启动三个线程
    for thread in threads:
        thread.start()
    # 等待线程结束
    for thread in threads:
        thread.join()
    end = time.time()
    print(f&apos;总耗时: {end - start:.3f}秒.&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;某次的运行结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开始下载 Python从入门到住院.pdf.
开始下载 MySQL从删库到跑路.avi.
开始下载 Linux从精通到放弃.mp4.
MySQL从删库到跑路.avi 下载完成.
下载耗时: 3.005秒.
Python从入门到住院.pdf 下载完成.
下载耗时: 5.006秒.
Linux从精通到放弃.mp4 下载完成.
下载耗时: 6.003秒.
总耗时: 6.004秒.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的运行结果可以发现，整个程序的执行时间几乎等于耗时最长的一个下载任务的执行时间，这也就意味着，三个下载任务是并发执行的，不存在一个等待另一个的情况，这样做很显然提高了程序的执行效率。简单的说，如果程序中有非常耗时的执行单元，而这些耗时的执行单元之间又没有逻辑上的因果关系，即 B 单元的执行不依赖于 A 单元的执行结果，那么 A 和 B 两个单元就可以放到两个不同的线程中，让他们并发的执行。这样做的好处除了减少程序执行的等待时间，还可以带来更好的用户体验，因为一个单元的阻塞不会造成程序的“假死”，因为程序中还有其他的单元是可以运转的。&lt;/p&gt;
&lt;h5&gt;使用 Thread 类创建线程对象&lt;/h5&gt;
&lt;p&gt;通过上面的代码可以看出，直接使用&lt;code&gt;Thread&lt;/code&gt;类的构造器就可以创建线程对象，而线程对象的&lt;code&gt;start()&lt;/code&gt;方法可以启动一个线程。线程启动后会执行&lt;code&gt;target&lt;/code&gt;参数指定的函数，当然前提是获得 CPU 的调度；如果&lt;code&gt;target&lt;/code&gt;指定的线程要执行的目标函数有参数，需要通过&lt;code&gt;args&lt;/code&gt;参数为其进行指定，对于关键字参数，可以通过&lt;code&gt;kwargs&lt;/code&gt;参数进行传入。&lt;code&gt;Thread&lt;/code&gt;类的构造器还有很多其他的参数，我们遇到的时候再为大家进行讲解，目前需要大家掌握的，就是&lt;code&gt;target&lt;/code&gt;、&lt;code&gt;args&lt;/code&gt;和&lt;code&gt;kwargs&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;继承 Thread 类自定义线程&lt;/h5&gt;
&lt;p&gt;除了上面的代码展示的创建线程的方式外，还可以通过继承&lt;code&gt;Thread&lt;/code&gt;类并重写&lt;code&gt;run()&lt;/code&gt;方法的方式来自定义线程，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time
from threading import Thread


class DownloadThread(Thread):

    def __init__(self, filename):
        self.filename = filename
        super().__init__()

    def run(self):
        start = time.time()
        print(f&apos;开始下载 {self.filename}.&apos;)
        time.sleep(random.randint(3, 6))
        print(f&apos;{self.filename} 下载完成.&apos;)
        end = time.time()
        print(f&apos;下载耗时: {end - start:.3f}秒.&apos;)


def main():
    threads = [
        DownloadThread(&apos;Python从入门到住院.pdf&apos;),
        DownloadThread(&apos;MySQL从删库到跑路.avi&apos;),
        DownloadThread(&apos;Linux从精通到放弃.mp4&apos;)
    ]
    start = time.time()
    # 启动三个线程
    for thread in threads:
        thread.start()
    # 等待线程结束
    for thread in threads:
        thread.join()
    end = time.time()
    print(f&apos;总耗时: {end - start:.3f}秒.&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;使用线程池&lt;/h5&gt;
&lt;p&gt;我们还可以通过线程池的方式将任务放到多个线程中去执行，通过线程池来使用线程应该是多线程编程最理想的选择。事实上，线程的创建和释放都会带来较大的开销，频繁的创建和释放线程通常都不是很好的选择。利用线程池，可以提前准备好若干个线程，在使用的过程中不需要再通过自定义的代码创建和释放线程，而是直接复用线程池中的线程。Python 内置的&lt;code&gt;concurrent.futures&lt;/code&gt;模块提供了对线程池的支持，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Thread


def download(*, filename):
    start = time.time()
    print(f&apos;开始下载 {filename}.&apos;)
    time.sleep(random.randint(3, 6))
    print(f&apos;{filename} 下载完成.&apos;)
    end = time.time()
    print(f&apos;下载耗时: {end - start:.3f}秒.&apos;)


def main():
    with ThreadPoolExecutor(max_workers=4) as pool:
        filenames = [&apos;Python从入门到住院.pdf&apos;, &apos;MySQL从删库到跑路.avi&apos;, &apos;Linux从精通到放弃.mp4&apos;]
        start = time.time()
        for filename in filenames:
            pool.submit(download, filename=filename)
    end = time.time()
    print(f&apos;总耗时: {end - start:.3f}秒.&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;守护线程&lt;/h4&gt;
&lt;p&gt;所谓“守护线程”就是在主线程结束的时候，不值得再保留的执行线程。这里的不值得保留指的是守护线程会在其他非守护线程全部运行结束之后被销毁，它守护的是当前进程内所有的非守护线程。简单的说，守护线程会跟随主线程一起挂掉，而主线程的生命周期就是一个进程的生命周期。如果不理解，我们可以看一段简单的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
from threading import Thread


def display(content):
    while True:
        print(content, end=&apos;&apos;, flush=True)
        time.sleep(0.1)


def main():
    Thread(target=display, args=(&apos;Ping&apos;, )).start()
    Thread(target=display, args=(&apos;Pong&apos;, )).start()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中，我们将&lt;code&gt;print&lt;/code&gt;函数的参数&lt;code&gt;flush&lt;/code&gt;设置为&lt;code&gt;True&lt;/code&gt;，这是因为&lt;code&gt;flush&lt;/code&gt;参数的值如果为&lt;code&gt;False&lt;/code&gt;，而&lt;code&gt;print&lt;/code&gt;又没有做换行处理，就会导致每次&lt;code&gt;print&lt;/code&gt;输出的内容被放到操作系统的输出缓冲区，直到缓冲区被输出的内容塞满，才会清空缓冲区产生一次输出。上述现象是操作系统为了减少 I/O 中断，提升 CPU 利用率做出的设定，为了让代码产生直观交互，我们才将&lt;code&gt;flush&lt;/code&gt;参数设置为&lt;code&gt;True&lt;/code&gt;，强制每次输出都清空输出缓冲区。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码运行起来之后是不会停止的，因为两个子线程中都有死循环，除非你手动中断代码的执行。但是，如果在创建线程对象时，将名为&lt;code&gt;daemon&lt;/code&gt;的参数设置为&lt;code&gt;True&lt;/code&gt;，这两个线程就会变成守护线程，那么在其他线程结束时，即便有死循环，两个守护线程也会挂掉，不会再继续执行下去，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
from threading import Thread


def display(content):
    while True:
        print(content, end=&apos;&apos;, flush=True)
        time.sleep(0.1)


def main():
    Thread(target=display, args=(&apos;Ping&apos;, ), daemon=True).start()
    Thread(target=display, args=(&apos;Pong&apos;, ), daemon=True).start()
    time.sleep(5)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码，我们在主线程中添加了一行&lt;code&gt;time.sleep(5)&lt;/code&gt;让主线程休眠5秒，在这个过程中，输出&lt;code&gt;Ping&lt;/code&gt;和&lt;code&gt;Pong&lt;/code&gt;的守护线程会持续运转，直到主线程在5秒后结束，这两个守护线程也被销毁，不再继续运行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：如果将上面代码第12行的&lt;code&gt;daemon=True&lt;/code&gt;去掉，代码会怎样执行？有兴趣的读者可以尝试一下，并看看实际执行的结果跟你想象的是否一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;资源竞争&lt;/h4&gt;
&lt;p&gt;在编写多线程代码时，不可避免的会遇到多个线程竞争同一个资源（对象）的情况。在这种情况下，如果没有合理的机制来保护被竞争的资源，那么就有可能出现非预期的状况。下面的代码创建了&lt;code&gt;100&lt;/code&gt;个线程向同一个银行账户（初始余额为&lt;code&gt;0&lt;/code&gt;元）转账，每个线程转账金额为&lt;code&gt;1&lt;/code&gt;元。在正常的情况下，我们的银行账户最终的余额应该是&lt;code&gt;100&lt;/code&gt;元，但是运行下面的代码我们并不能得到&lt;code&gt;100&lt;/code&gt;元这个结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

from concurrent.futures import ThreadPoolExecutor


class Account(object):
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self):
        self.balance = 0.0

    def deposit(self, money):
        &quot;&quot;&quot;存钱&quot;&quot;&quot;
        new_balance = self.balance + money
        time.sleep(0.01)
        self.balance = new_balance


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中的&lt;code&gt;Account&lt;/code&gt;类代表了银行账户，它的&lt;code&gt;deposit&lt;/code&gt;方法代表存款行为，参数&lt;code&gt;money&lt;/code&gt;代表存入的金额，该方法通过&lt;code&gt;time.sleep&lt;/code&gt;函数模拟受理存款需要一段时间。我们通过线程池的方式启动了&lt;code&gt;100&lt;/code&gt;个线程向一个账户转账，但是上面的代码并不能运行出&lt;code&gt;100&lt;/code&gt;这个我们期望的结果，这就是在多个线程竞争一个资源的时候，可能会遇到的数据不一致的问题。注意上面代码的第&lt;code&gt;14&lt;/code&gt;行，当多个线程都执行到这行代码时，它们会在相同的余额上执行加上存入金额的操作，这就会造成“丢失更新”现象，即之前修改数据的成果被后续的修改给覆盖掉了，所以才得不到正确的结果。&lt;/p&gt;
&lt;p&gt;要解决上面的问题，可以使用锁机制，通过锁对操作数据的关键代码加以保护。Python 标准库的&lt;code&gt;threading&lt;/code&gt;模块提供了&lt;code&gt;Lock&lt;/code&gt;和&lt;code&gt;RLock&lt;/code&gt;类来支持锁机制，这里我们不去深究二者的区别，建议大家直接使用&lt;code&gt;RLock&lt;/code&gt;。接下来，我们给银行账户添加一个锁对象，通过锁对象来解决刚才存款时发生“丢失更新”的问题，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

from concurrent.futures import ThreadPoolExecutor
from threading import RLock


class Account(object):
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self):
        self.balance = 0.0
        self.lock = RLock()

    def deposit(self, money):
        # 获得锁
        self.lock.acquire()
        try:
            new_balance = self.balance + money
            time.sleep(0.01)
            self.balance = new_balance
        finally:
            # 释放锁
            self.lock.release()


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，获得锁和释放锁的操作也可以通过上下文语法来实现，使用上下文语法会让代码更加简单优雅，这也是我们推荐大家使用的方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

from concurrent.futures import ThreadPoolExecutor
from threading import RLock


class Account(object):
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self):
        self.balance = 0.0
        self.lock = RLock()

    def deposit(self, money):
        # 通过上下文语法获得锁和释放锁
        with self.lock:
            new_balance = self.balance + money
            time.sleep(0.01)
            self.balance = new_balance


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：将上面的代码修改为5个线程向银行账户存钱，5个线程从银行账户取钱，取钱的线程在银行账户余额不足时，需要停下来等待存钱的线程将钱存入后再尝试取钱。这里需要用到线程调度的知识，大家可以自行研究下&lt;code&gt;threading&lt;/code&gt;模块中的&lt;code&gt;Condition&lt;/code&gt;类，看看是否能够完成这个任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;GIL问题&lt;/h4&gt;
&lt;p&gt;如果使用官方的 Python 解释器（通常称之为 CPython）运行 Python 程序，我们并不能通过使用多线程的方式将 CPU 的利用率提升到逼近400%（对于4核 CPU）或逼近800%（对于8核 CPU）这样的水平，因为 CPython 在执行代码时，会受到 GIL（全局解释器锁）的限制。具体的说，CPython 在执行任何代码时，都需要对应的线程先获得 GIL，然后每执行100条（字节码）指令，CPython 就会让获得 GIL 的线程主动释放 GIL，这样别的线程才有机会执行。因为 GIL 的存在，无论你的 CPU 有多少个核，我们编写的 Python 代码也没有机会真正并行的执行。&lt;/p&gt;
&lt;p&gt;GIL 是官方 Python 解释器在设计上的历史遗留问题，要解决这个问题，让多线程能够发挥 CPU 的多核优势，需要重新实现一个不带 GIL 的 Python 解释器。这个问题按照官方的说法，在 Python 发布4.0版本时会得到解决，就让我们拭目以待吧。当下，对于 CPython 而言，如果希望充分发挥 CPU 的多核优势，可以考虑使用多进程，因为每个进程都对应一个 Python 解释器，因此每个进程都有自己独立的 GIL，这样就可以突破 GIL 的限制。在下一个章节中，我们会为大家介绍关于多进程的相关知识，并对多线程和多进程的代码及其执行效果进行比较。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;63 Python中的并发编程-2&lt;/h2&gt;
&lt;p&gt;在上一课中我们说过，由于 GIL 的存在，CPython 中的多线程并不能发挥 CPU 的多核优势，如果希望突破 GIL 的限制，可以考虑使用多进程。对于多进程的程序，每个进程都有一个属于自己的 GIL，所以多进程不会受到 GIL 的影响。那么，我们应该如何在 Python 程序中创建和使用多进程呢？&lt;/p&gt;
&lt;h4&gt;创建进程&lt;/h4&gt;
&lt;p&gt;在 Python 中可以基于&lt;code&gt;Process&lt;/code&gt;类来创建进程，虽然进程和线程有着本质的差别，但是&lt;code&gt;Process&lt;/code&gt;类和&lt;code&gt;Thread&lt;/code&gt;类的用法却非常类似。在使用&lt;code&gt;Process&lt;/code&gt;类的构造器创建对象时，也是通过&lt;code&gt;target&lt;/code&gt;参数传入一个函数来指定进程要执行的代码，而&lt;code&gt;args&lt;/code&gt;和&lt;code&gt;kwargs&lt;/code&gt;参数可以指定该函数使用的参数值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Process, current_process
from time import sleep


def sub_task(content, nums):
    # 通过current_process函数获取当前进程对象
    # 通过进程对象的pid和name属性获取进程的ID号和名字
    print(f&apos;PID: {current_process().pid}&apos;)
    print(f&apos;Name: {current_process().name}&apos;)
    # 通过下面的输出不难发现，每个进程都有自己的nums列表，进程之间本就不共享内存
    # 在创建子进程时复制了父进程的数据结构，三个进程从列表中pop(0)得到的值都是20
    counter, total = 0, nums.pop(0)
    print(f&apos;Loop count: {total}&apos;)
    sleep(0.5)
    while counter &amp;lt; total:
        counter += 1
        print(f&apos;{counter}: {content}&apos;)
        sleep(0.01)


def main():
    nums = [20, 30, 40]
    # 创建并启动进程来执行指定的函数
    Process(target=sub_task, args=(&apos;Ping&apos;, nums)).start()
    Process(target=sub_task, args=(&apos;Pong&apos;, nums)).start()
    # 在主进程中执行sub_task函数
    sub_task(&apos;Good&apos;, nums)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码通过&lt;code&gt;current_process&lt;/code&gt;函数获取当前进程对象，再通过进程对象的&lt;code&gt;pid&lt;/code&gt;属性获取进程ID。在 Python 中，使用&lt;code&gt;os&lt;/code&gt;模块的&lt;code&gt;getpid&lt;/code&gt;函数也可以达到同样的效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果愿意，也可以使用&lt;code&gt;os&lt;/code&gt;模块的&lt;code&gt;fork&lt;/code&gt;函数来创建进程，调用该函数时，操作系统自动把当前进程（父进程）复制一份（子进程），父进程的&lt;code&gt;fork&lt;/code&gt;函数会返回子进程的ID，而子进程中的&lt;code&gt;fork&lt;/code&gt;函数会返回&lt;code&gt;0&lt;/code&gt;，也就是说这个函数调用一次会在父进程和子进程中得到两个不同的返回值。需要注意的是，Windows 系统并不支持&lt;code&gt;fork&lt;/code&gt;函数，如果你使用的是 Linux 或 macOS 系统，可以试试下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os

print(f&apos;PID: {os.getpid()}&apos;)
pid = os.fork()
if pid == 0:
    print(f&apos;子进程 - PID: {os.getpid()}&apos;)
    print(&apos;Todo: 在子进程中执行的代码&apos;)
else:
    print(f&apos;父进程 - PID: {os.getpid()}&apos;)
    print(&apos;Todo: 在父进程中执行的代码&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简而言之，我们还是推荐大家通过直接使用&lt;code&gt;Process&lt;/code&gt;类、继承&lt;code&gt;Process&lt;/code&gt;类和使用进程池（&lt;code&gt;ProcessPoolExecutor&lt;/code&gt;）这三种方式来创建和使用多进程，这三种方式不同于上面的&lt;code&gt;fork&lt;/code&gt;函数，能够保证代码的兼容性和可移植性。具体的做法跟之前讲过的创建和使用多线程的方式比较接近，此处不再进行赘述。&lt;/p&gt;
&lt;h4&gt;多进程和多线程的比较&lt;/h4&gt;
&lt;p&gt;对于爬虫这类 I/O 密集型任务来说，使用多进程并没有什么优势；但是对于计算密集型任务来说，多进程相比多线程，在效率上会有显著的提升，我们可以通过下面的代码来加以证明。下面的代码会通过多线程和多进程两种方式来判断一组大整数是不是质数，很显然这是一个计算密集型任务，我们将任务分别放到多个线程和多个进程中来加速代码的执行，让我们看看多线程和多进程的代码具体表现有何不同。&lt;/p&gt;
&lt;p&gt;我们先实现一个多线程的版本，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import concurrent.futures

PRIMES = [
    1116281,
    1297337,
    104395303,
    472882027,
    533000389,
    817504243,
    982451653,
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419
] * 5


def is_prime(n):
    &quot;&quot;&quot;判断素数&quot;&quot;&quot;
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return n != 1


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print(&apos;%d is prime: %s&apos; % (number, prime))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设上面的代码保存在名为&lt;code&gt;example.py&lt;/code&gt;的文件中，在 Linux 或 macOS 系统上，可以使用&lt;code&gt;time python example.py&lt;/code&gt;命令执行程序并获得操作系统关于执行时间的统计，在我的 macOS 上，某次的运行结果的最后一行输出如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python example09.py  38.69s user 1.01s system 101% cpu 39.213 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从运行结果可以看出，多线程的代码只能让 CPU 利用率达到100%，这其实已经证明了多线程的代码无法利用 CPU 多核特性来加速代码的执行，我们再看看多进程的版本，我们将上面代码中的线程池（&lt;code&gt;ThreadPoolExecutor&lt;/code&gt;）更换为进程池（&lt;code&gt;ProcessPoolExecutor&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;多进程的版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import concurrent.futures

PRIMES = [
    1116281,
    1297337,
    104395303,
    472882027,
    533000389,
    817504243,
    982451653,
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419
] * 5


def is_prime(n):
    &quot;&quot;&quot;判断素数&quot;&quot;&quot;
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return n != 1


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    with concurrent.futures.ProcessPoolExecutor(max_workers=16) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print(&apos;%d is prime: %s&apos; % (number, prime))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：运行上面的代码时，可以通过操作系统的任务管理器（资源监视器）来查看是否启动了多个 Python  解释器进程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们仍然通过&lt;code&gt;time python example.py&lt;/code&gt;的方式来执行上述代码，运行结果的最后一行如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python example09.py 106.63s user 0.57s system 389% cpu 27.497 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，多进程的版本在我使用的这台电脑上，让 CPU 的利用率达到了将近400%，而运行代码时用户态耗费的 CPU 的时间（106.63秒）几乎是代码运行总时间（27.497秒）的4倍，从这两点都可以看出，我的电脑使用了一款4核的 CPU。当然，要知道自己的电脑有几个 CPU 或几个核，可以直接使用下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os

print(os.cpu_count())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;综上所述，多进程可以突破 GIL 的限制，充分利用 CPU 多核特性，对于计算密集型任务，这一点是相当重要的。常见的计算密集型任务包括科学计算、图像处理、音视频编解码等，如果这些计算密集型任务本身是可以并行的，那么使用多进程应该是更好的选择。&lt;/p&gt;
&lt;h4&gt;进程间通信&lt;/h4&gt;
&lt;p&gt;在讲解进程间通信之前，先给大家一个任务：启动两个进程，一个输出“Ping”，一个输出“Pong”，两个进程输出的“Ping”和“Pong”加起来一共有50个时，就结束程序。听起来是不是非常简单，但是实际编写代码时，由于多个进程之间不能够像多个线程之间直接通过共享内存的方式交换数据，所以下面的代码是达不到我们想要的结果的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Process
from time import sleep

counter = 0


def sub_task(string):
    global counter
    while counter &amp;lt; 50:
        print(string, end=&apos;&apos;, flush=True)
        counter += 1
        sleep(0.01)

        
def main():
    Process(target=sub_task, args=(&apos;Ping&apos;, )).start()
    Process(target=sub_task, args=(&apos;Pong&apos;, )).start()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码看起来没毛病，但是最后的结果是“Ping”和“Pong”各输出了50个。再次提醒大家，当我们在程序中创建进程的时候，子进程会复制父进程及其所有的数据结构，每个子进程有自己独立的内存空间，这也就意味着两个子进程中各有一个&lt;code&gt;counter&lt;/code&gt;变量，它们都会从&lt;code&gt;0&lt;/code&gt;加到&lt;code&gt;50&lt;/code&gt;，所以结果就可想而知了。要解决这个问题比较简单的办法是使用&lt;code&gt;multiprocessing&lt;/code&gt;模块中的&lt;code&gt;Queue&lt;/code&gt;类，它是可以被多个进程共享的队列，底层是通过操作系统底层的管道和信号量（semaphore）机制来实现的，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
from multiprocessing import Process, Queue


def sub_task(content, queue):
    counter = queue.get()
    while counter &amp;lt; 50:
        print(content, end=&apos;&apos;, flush=True)
        counter += 1
        queue.put(counter)
        time.sleep(0.01)
        counter = queue.get()


def main():
    queue = Queue()
    queue.put(0)
    p1 = Process(target=sub_task, args=(&apos;Ping&apos;, queue))
    p1.start()
    p2 = Process(target=sub_task, args=(&apos;Pong&apos;, queue))
    p2.start()
    while p1.is_alive() and p2.is_alive():
        pass
    queue.put(50)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：&lt;code&gt;multiprocessing.Queue&lt;/code&gt;对象的&lt;code&gt;get&lt;/code&gt;方法默认在队列为空时是会阻塞的，直到获取到数据才会返回。如果不希望该方法阻塞以及需要指定阻塞的超时时间，可以通过指定&lt;code&gt;block&lt;/code&gt;和&lt;code&gt;timeout&lt;/code&gt;参数进行设定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码通过&lt;code&gt;Queue&lt;/code&gt;类的&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;put&lt;/code&gt;方法让三个进程（&lt;code&gt;p1&lt;/code&gt;、&lt;code&gt;p2&lt;/code&gt;和主进程）实现了数据的共享，这就是所谓的进程间的通信，通过这种方式，当&lt;code&gt;Queue&lt;/code&gt;中取出的值已经大于等于&lt;code&gt;50&lt;/code&gt;时，&lt;code&gt;p1&lt;/code&gt;和&lt;code&gt;p2&lt;/code&gt;就会跳出&lt;code&gt;while&lt;/code&gt;循环，从而终止进程的执行。代码第22行的循环是为了等待&lt;code&gt;p1&lt;/code&gt;和&lt;code&gt;p2&lt;/code&gt;两个进程中的一个结束，这时候主进程还需要向&lt;code&gt;Queue&lt;/code&gt;中放置一个大于等于&lt;code&gt;50&lt;/code&gt;的值，这样另一个尚未结束的进程也会因为读到这个大于等于&lt;code&gt;50&lt;/code&gt;的值而终止。&lt;/p&gt;
&lt;p&gt;进程间通信的方式还有很多，比如使用套接字也可以实现两个进程的通信，甚至于这两个进程并不在同一台主机上，有兴趣的读者可以自行了解。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;在 Python 中，我们还可以通过&lt;code&gt;subprocess&lt;/code&gt;模块的&lt;code&gt;call&lt;/code&gt;函数执行其他的命令来创建子进程，相当于就是在我们的程序中调用其他程序，这里我们暂不探讨这些知识，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;p&gt;对于Python开发者来说，以下情况需要考虑使用多线程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;程序需要维护许多共享的状态（尤其是可变状态），Python 中的列表、字典、集合都是线程安全的（多个线程同时操作同一个列表、字典或集合，不会引发错误和数据问题），所以使用线程而不是进程维护共享状态的代价相对较小。&lt;/li&gt;
&lt;li&gt;程序会花费大量时间在 I/O 操作上，没有太多并行计算的需求且不需占用太多的内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么在遇到下列情况时，应该考虑使用多进程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;程序执行计算密集型任务（如：音视频编解码、数据压缩、科学计算等）。&lt;/li&gt;
&lt;li&gt;程序的输入可以并行的分成块，并且可以将运算结果合并。&lt;/li&gt;
&lt;li&gt;程序在内存使用方面没有任何限制且不强依赖于 I/O 操作（如读写文件、套接字等）。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;63 Python中的并发编程-3&lt;/h2&gt;
&lt;p&gt;爬虫是典型的 I/O 密集型任务，I/O 密集型任务的特点就是程序会经常性的因为 I/O 操作而进入阻塞状态，比如我们之前使用&lt;code&gt;requests&lt;/code&gt;获取页面代码或二进制内容，发出一个请求之后，程序必须要等待网站返回响应之后才能继续运行，如果目标网站不是很给力或者网络状况不是很理想，那么等待响应的时间可能会很久，而在这个过程中整个程序是一直阻塞在那里，没有做任何的事情。通过前面的课程，我们已经知道了可以通过多线程的方式为爬虫提速，使用多线程的本质就是，当一个线程阻塞的时候，程序还有其他的线程可以继续运转，因此整个程序就不会在阻塞和等待中浪费了大量的时间。&lt;/p&gt;
&lt;p&gt;事实上，还有一种非常适合 I/O 密集型任务的并发编程方式，我们称之为异步编程，你也可以将它称为异步 I/O。这种方式并不需要启动多个线程或多个进程来实现并发，它是通过多个子程序相互协作的方式来提升 CPU 的利用率，解决了 I/O 密集型任务 CPU  利用率很低的问题，我一般将这种方式称为“协作式并发”。这里，我不打算探讨操作系统的各种 I/O 模式，因为这对很多读者来说都太过抽象；但是我们得先抛出两组概念给大家，一组叫做“阻塞”和“非阻塞”，一组叫做“同步”和“异步”。&lt;/p&gt;
&lt;h4&gt;基本概念&lt;/h4&gt;
&lt;h5&gt;阻塞&lt;/h5&gt;
&lt;p&gt;阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间，自身无法继续处理其他的事情，则称该程序在该操作上是阻塞的。阻塞随时都可能发生，最典型的就是 I/O 中断（包括网络 I/O 、磁盘 I/O 、用户输入等）、休眠操作、等待某个线程执行结束，甚至包括在 CPU 切换上下文时，程序都无法真正的执行，这就是所谓的阻塞。&lt;/p&gt;
&lt;h5&gt;非阻塞&lt;/h5&gt;
&lt;p&gt;程序在等待某操作过程中，自身不被阻塞，可以继续处理其他的事情，则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。仅当程序封装的级别可以囊括独立的子程序单元时，它才可能存在非阻塞状态。显然，某个操作的阻塞可能会导程序耗时以及效率低下，所以我们会希望把它变成非阻塞的。&lt;/p&gt;
&lt;h5&gt;同步&lt;/h5&gt;
&lt;p&gt;不同程序单元为了完成某个任务，在执行过程中需靠某种通信方式以协调一致，我们称这些程序单元是同步执行的。例如前面讲过的给银行账户存钱的操作，我们在代码中使用了“锁”作为通信信号，让多个存钱操作强制排队顺序执行，这就是所谓的同步。&lt;/p&gt;
&lt;h5&gt;异步&lt;/h5&gt;
&lt;p&gt;不同程序单元在执行过程中无需通信协调，也能够完成一个任务，这种方式我们就称之为异步。例如，使用爬虫下载页面时，调度程序调用下载程序后，即可调度其他任务，而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是不相关的，也无需相互通知协调。很显然，异步操作的完成时刻和先后顺序并不能确定。&lt;/p&gt;
&lt;p&gt;很多人都不太能准确的把握这几个概念，这里我们简单的总结一下，同步与异步的关注点是&lt;strong&gt;消息通信机制&lt;/strong&gt;，最终表现出来的是“有序”和“无序”的区别；阻塞和非阻塞的关注点是&lt;strong&gt;程序在等待消息时状态&lt;/strong&gt;，最终表现出来的是程序在等待时能不能做点别的。如果想深入理解这些内容，推荐大家阅读经典著作&lt;a href=&quot;https://item.jd.com/11880047.html&quot;&gt;《UNIX网络编程》&lt;/a&gt;，这本书非常的赞。&lt;/p&gt;
&lt;h4&gt;生成器和协程&lt;/h4&gt;
&lt;p&gt;前面我们说过，异步编程是一种“协作式并发”，即通过多个子程序相互协作的方式提升 CPU 的利用率，从而减少程序在阻塞和等待中浪费的时间，最终达到并发的效果。我们可以将多个相互协作的子程序称为“协程”，它是实现异步编程的关键。在介绍协程之前，我们先通过下面的代码，看看什么是生成器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(max_count):
    a, b = 0, 1
    for _ in range(max_count):
        a, b = b, a + b
        yield a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面我们编写了一个生成斐波那契数列的生成器，调用上面的&lt;code&gt;fib&lt;/code&gt;函数并不是执行该函数获得返回值，因为&lt;code&gt;fib&lt;/code&gt;函数中有一个特殊的关键字&lt;code&gt;yield&lt;/code&gt;。这个关键字使得&lt;code&gt;fib&lt;/code&gt;函数跟普通的函数有些区别，调用该函数会得到一个生成器对象，我们可以通过下面的代码来验证这一点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gen_obj = fib(20)
print(gen_obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;generator object fib at 0x106daee40&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用内置函数&lt;code&gt;next&lt;/code&gt;从生成器对象中获取斐波那契数列的值，也可以通过&lt;code&gt;for-in&lt;/code&gt;循环对生成器能够提供的值进行遍历，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for value in gen_obj:
    print(value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成器经过预激活，就是一个协程，它可以跟其他子程序协作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def calc_average():
    total, counter = 0, 0
    avg_value = None
    while True:
        curr_value = yield avg_value
        total += curr_value
        counter += 1
        avg_value = total / counter


def main():
    obj = calc_average()
    # 生成器预激活
    obj.send(None)
    for _ in range(5):
        print(obj.send(float(input())))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;main&lt;/code&gt;函数首先通过生成器对象的&lt;code&gt;send&lt;/code&gt;方法发送一个&lt;code&gt;None&lt;/code&gt;值来将其激活为协程，也可以通过&lt;code&gt;next(obj)&lt;/code&gt;达到同样的效果。接下来，协程对象会接收&lt;code&gt;main&lt;/code&gt;函数发送的数据并产出（&lt;code&gt;yield&lt;/code&gt;）数据的平均值。通过上面的例子，不知道大家是否看出两段子程序是怎么“协作”的。&lt;/p&gt;
&lt;h4&gt;异步函数&lt;/h4&gt;
&lt;p&gt;Python 3.5版本中，引入了两个非常有意思的元素，一个叫&lt;code&gt;async&lt;/code&gt;，一个叫&lt;code&gt;await&lt;/code&gt;，它们在Python 3.7版本中成为了正式的关键字。通过这两个关键字，可以简化协程代码的编写，可以用更为简单的方式让多个子程序很好的协作起来。我们通过一个例子来加以说明，请大家先看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time


def display(num):
    time.sleep(1)
    print(num)


def main():
    start = time.time()
    for i in range(1, 10):
        display(i)
    end = time.time()
    print(f&apos;{end - start:.3f}秒&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码每次执行都会依次输出&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;9&lt;/code&gt;的数字，每个间隔&lt;code&gt;1&lt;/code&gt;秒钟，整个代码需要执行大概需要&lt;code&gt;9&lt;/code&gt;秒多的时间，这一点我相信大家都能看懂。不知道大家是否意识到，这段代码就是以同步和阻塞的方式执行的，同步可以从代码的输出看出来，而阻塞是指在调用&lt;code&gt;display&lt;/code&gt;函数发生休眠时，整个代码的其他部分都不能继续执行，必须等待休眠结束。&lt;/p&gt;
&lt;p&gt;接下来，我们尝试用异步的方式改写上面的代码，让&lt;code&gt;display&lt;/code&gt;函数以异步的方式运转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import time


async def display(num):
    await asyncio.sleep(1)
    print(num)


def main():
    start = time.time()
    objs = [display(i) for i in range(1, 10)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(objs))
    loop.close()
    end = time.time()
    print(f&apos;{end - start:.3f}秒&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 中的&lt;code&gt;asyncio&lt;/code&gt;模块提供了对异步 I/O 的支持。上面的代码中，我们首先在&lt;code&gt;display&lt;/code&gt;函数前面加上了&lt;code&gt;async&lt;/code&gt;关键字使其变成一个异步函数，调用异步函数不会执行函数体而是获得一个协程对象。我们将&lt;code&gt;display&lt;/code&gt;函数中的&lt;code&gt;time.sleep(1)&lt;/code&gt;修改为&lt;code&gt;await asyncio.sleep(1)&lt;/code&gt;，二者的区别在于，后者不会让整个代码陷入阻塞，因为&lt;code&gt;await&lt;/code&gt;操作会让其他协作的子程序有获得 CPU 资源而得以运转的机会。为了让这些子程序可以协作起来，我们需要将他们放到一个事件循环（实现消息分派传递的系统）上，因为&lt;strong&gt;当协程遭遇 I/O 操作阻塞时，就会到事件循环中监听 I/O 操作是否完成，并注册自身的上下文以及自身的唤醒函数（以便恢复执行），之后该协程就变为阻塞状态&lt;/strong&gt;。上面的第12行代码创建了&lt;code&gt;9&lt;/code&gt;个协程对象并放到一个列表中，第13行代码通过&lt;code&gt;asyncio&lt;/code&gt;模块的&lt;code&gt;get_event_loop&lt;/code&gt;函数获得了系统的事件循环，第14行通过&lt;code&gt;asyncio&lt;/code&gt;模块的&lt;code&gt;run_until_complete&lt;/code&gt;函数将协程对象挂载到事件循环上。执行上面的代码会发现，&lt;code&gt;9&lt;/code&gt;个分别会阻塞&lt;code&gt;1&lt;/code&gt;秒钟的协程总共只阻塞了约&lt;code&gt;1&lt;/code&gt;秒种的时间，因为&lt;strong&gt;阻塞的协程对象会放弃对 CPU 的占有而不是让 CPU 处于闲置状态，这种方式大大的提升了 CPU 的利用率&lt;/strong&gt;。而且我们还会注意到，数字并不是按照从&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;9&lt;/code&gt;的顺序打印输出的，这正是我们想要的结果，说明它们是&lt;strong&gt;异步执行&lt;/strong&gt;的。对于爬虫这样的 I/O 密集型任务来说，这种协作式并发在很多场景下是比使用多线程更好的选择，因为这种做法减少了管理和维护多个线程以及多个线程切换所带来的开销。&lt;/p&gt;
&lt;h4&gt;aiohttp库&lt;/h4&gt;
&lt;p&gt;我们之前使用的&lt;code&gt;requests&lt;/code&gt;三方库并不支持异步 I/O，如果希望使用异步 I/O 的方式来加速爬虫代码的执行，我们可以安装和使用名为&lt;code&gt;aiohttp&lt;/code&gt;的三方库。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;aiohttp&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install aiohttp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的代码使用&lt;code&gt;aiohttp&lt;/code&gt;抓取了&lt;code&gt;10&lt;/code&gt;个网站的首页并解析出它们的标题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import re

import aiohttp
from aiohttp import ClientSession

TITLE_PATTERN = re.compile(r&apos;&amp;lt;title.*?&amp;gt;(.*?)&amp;lt;/title&amp;gt;&apos;, re.DOTALL)


async def fetch_page_title(url):
    async with aiohttp.ClientSession(headers={
        &apos;User-Agent&apos;: &apos;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36&apos;,
    }) as session:  # type: ClientSession
        async with session.get(url, ssl=False) as resp:
            if resp.status == 200:
                html_code = await resp.text()
                matcher = TITLE_PATTERN.search(html_code)
                title = matcher.group(1).strip()
                print(title)


def main():
    urls = [
        &apos;https://www.python.org/&apos;,
        &apos;https://www.jd.com/&apos;,
        &apos;https://www.baidu.com/&apos;,
        &apos;https://www.taobao.com/&apos;,
        &apos;https://git-scm.com/&apos;,
        &apos;https://www.sohu.com/&apos;,
        &apos;https://gitee.com/&apos;,
        &apos;https://www.amazon.com/&apos;,
        &apos;https://www.usa.gov/&apos;,
        &apos;https://www.nasa.gov/&apos;
    ]
    objs = [fetch_page_title(url) for url in urls]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(objs))
    loop.close()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物！
搜狐
淘宝网 - 淘！我喜欢
百度一下，你就知道
Gitee - 基于 Git 的代码托管和研发协作平台
Git
NASA
Official Guide to Government Information and Services   &amp;amp;#124; USAGov
Amazon.com. Spend less. Smile more.
Welcome to Python.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的输出可以看出，网站首页标题的输出顺序跟它们的 URL 在列表中的顺序没有关系。代码的第11行到第13行创建了&lt;code&gt;ClientSession&lt;/code&gt;对象，通过它的&lt;code&gt;get&lt;/code&gt;方法可以向指定的 URL 发起请求，如第14行所示，跟&lt;code&gt;requests&lt;/code&gt;中的&lt;code&gt;Session&lt;/code&gt;对象并没有本质区别，唯一的区别是这里使用了异步上下文。代码第16行的&lt;code&gt;await&lt;/code&gt;会让因为 I/O 操作阻塞的子程序放弃对 CPU 的占用，这使得其他的子程序可以运转起来去抓取页面。代码的第17行和第18行使用了正则表达式捕获组操作解析网页标题。&lt;code&gt;fetch_page_title&lt;/code&gt;是一个被&lt;code&gt;async&lt;/code&gt;关键字修饰的异步函数，调用该函数会获得协程对象，如代码第35行所示。后面的代码跟之前的例子没有什么区别，相信大家能够理解。&lt;/p&gt;
&lt;p&gt;大家可以尝试将&lt;code&gt;aiohttp&lt;/code&gt;换回到&lt;code&gt;requests&lt;/code&gt;，看看不使用异步 I/O 也不使用多线程，到底和上面的代码有什么区别，相信通过这样的对比，大家能够更深刻的理解我们之前强调的几个概念：同步和异步，阻塞和非阻塞。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;64 使用Selenium抓取网页动态内容&lt;/h2&gt;
&lt;p&gt;根据权威机构发布的全球互联网可访问性审计报告，全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的，这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容，也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案，一是获取提供动态内容的数据接口，这种方式也适用于抓取手机 App 的数据；另一种是通过自动化测试工具 Selenium 运行浏览器获取渲染后的动态内容。对于第一种方案，我们可以使用浏览器的“开发者工具”或者更为专业的抓包工具（如：Charles、Fiddler、Wireshark等）来获取到数据接口，后续的操作跟上一个章节中讲解的获取“360图片”网站的数据是一样的，这里我们不再进行赘述。这一章我们重点讲解如何使用自动化测试工具 Selenium 来获取网站的动态内容。&lt;/p&gt;
&lt;h4&gt;Selenium 介绍&lt;/h4&gt;
&lt;p&gt;Selenium 是一个自动化测试工具，利用它可以驱动浏览器执行特定的行为，最终帮助爬虫开发者获取到网页的动态内容。简单的说，只要我们在浏览器窗口中能够看到的内容，都可以使用 Selenium 获取到，对于那些使用了 JavaScript 动态渲染技术的网站，Selenium 会是一个重要的选择。下面，我们还是以 Chrome 浏览器为例，来讲解 Selenium 的用法，大家需要先安装 Chrome 浏览器并下载它的驱动。Chrome 浏览器的驱动程序可以在&lt;a href=&quot;https://chromedriver.chromium.org/downloads&quot;&gt;ChromeDriver官网&lt;/a&gt;进行下载，驱动的版本要跟浏览器的版本对应，如果没有完全对应的版本，就选择版本代号最为接近的版本。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/71e58e92e1.webp&quot; style=&quot;zoom:40%&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;使用Selenium&lt;/h4&gt;
&lt;p&gt;我们可以先通过&lt;code&gt;pip&lt;/code&gt;来安装 Selenium，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install selenium
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;加载页面&lt;/h5&gt;
&lt;p&gt;接下来，我们通过下面的代码驱动 Chrome 浏览器打开百度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver

# 创建Chrome浏览器对象
browser = webdriver.Chrome()
# 加载指定的页面
browser.get(&apos;https://www.baidu.com/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不愿意使用 Chrome 浏览器，也可以修改上面的代码操控其他浏览器，只需创建对应的浏览器对象（如 Firefox、Safari 等）即可。运行上面的程序，如果看到如下所示的错误提示，那是说明我们还没有将 Chrome 浏览器的驱动添加到 PATH 环境变量中，也没有在程序中指定 Chrome 浏览器驱动所在的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;selenium.common.exceptions.WebDriverException: Message: &apos;chromedriver&apos; executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决这个问题的办法有三种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将下载的 ChromeDriver 放到已有的 PATH 环境变量下，建议直接跟 Python 解释器放在同一个目录，因为之前安装 Python 的时候我们已经将 Python 解释器的路径放到 PATH 环境变量中了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 ChromeDriver 放到项目虚拟环境下的 &lt;code&gt;bin&lt;/code&gt; 文件夹中（Windows 系统对应的目录是 &lt;code&gt;Scripts&lt;/code&gt;），这样 ChromeDriver 就跟虚拟环境下的 Python 解释器在同一个位置，肯定是能够找到的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改上面的代码，在创建 Chrome 对象时，通过&lt;code&gt;service&lt;/code&gt;参数配置&lt;code&gt;Service&lt;/code&gt;对象，并通过创建&lt;code&gt;Service&lt;/code&gt;对象的&lt;code&gt;executable_path&lt;/code&gt;参数指定 ChromeDriver 所在的位置，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.chrome.service import Service

browser = webdriver.Chrome(service=Service(executable_path=&apos;venv/bin/chromedriver&apos;))
browser.get(&apos;https://www.baidu.com/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;查找元素和模拟用户行为&lt;/h5&gt;
&lt;p&gt;接下来，我们可以尝试模拟用户在百度首页的文本框输入搜索关键字并点击“百度一下”按钮。在完成页面加载后，可以通过&lt;code&gt;Chrome&lt;/code&gt;对象的&lt;code&gt;find_element&lt;/code&gt;和&lt;code&gt;find_elements&lt;/code&gt;方法来获取页面元素，Selenium 支持多种获取元素的方式，包括：CSS 选择器、XPath、元素名字（标签名）、元素 ID、类名等，前者可以获取单个页面元素（&lt;code&gt;WebElement&lt;/code&gt;对象），后者可以获取多个页面元素构成的列表。获取到&lt;code&gt;WebElement&lt;/code&gt;对象以后，可以通过&lt;code&gt;send_keys&lt;/code&gt;来模拟用户输入行为，可以通过&lt;code&gt;click&lt;/code&gt;来模拟用户点击操作，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get(&apos;https://www.baidu.com/&apos;)
# 通过元素ID获取元素
kw_input = browser.find_element(By.ID, &apos;kw&apos;)
# 模拟用户输入行为
kw_input.send_keys(&apos;Python&apos;)
# 通过CSS选择器获取元素
su_button = browser.find_element(By.CSS_SELECTOR, &apos;#su&apos;)
# 模拟用户点击行为
su_button.click()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要执行一个系列动作，例如模拟拖拽操作，可以创建&lt;code&gt;ActionChains&lt;/code&gt;对象，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;h5&gt;隐式等待和显式等待&lt;/h5&gt;
&lt;p&gt;这里还有一个细节需要大家知道，网页上的元素可能是动态生成的，在我们使用&lt;code&gt;find_element&lt;/code&gt;或&lt;code&gt;find_elements&lt;/code&gt;方法获取的时候，可能还没有完成渲染，这时会引发&lt;code&gt;NoSuchElementException&lt;/code&gt;错误。为了解决这个问题，我们可以使用隐式等待的方式，通过设置等待时间让浏览器完成对页面元素的渲染。除此之外，我们还可以使用显示等待，通过创建&lt;code&gt;WebDriverWait&lt;/code&gt;对象，并设置等待时间和条件，当条件没有满足时，我们可以先等待再尝试进行后续的操作，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

browser = webdriver.Chrome()
# 设置浏览器窗口大小
browser.set_window_size(1200, 800)
browser.get(&apos;https://www.baidu.com/&apos;)
# 设置隐式等待时间为10秒
browser.implicitly_wait(10)
kw_input = browser.find_element(By.ID, &apos;kw&apos;)
kw_input.send_keys(&apos;Python&apos;)
su_button = browser.find_element(By.CSS_SELECTOR, &apos;#su&apos;)
su_button.click()
# 创建显示等待对象
wait_obj = WebDriverWait(browser, 10)
# 设置等待条件（等搜索结果的div出现）
wait_obj.until(
    expected_conditions.presence_of_element_located(
        (By.CSS_SELECTOR, &apos;#content_left&apos;)
    )
)
# 截屏
browser.get_screenshot_as_file(&apos;python_result.png&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面设置的等待条件&lt;code&gt;presence_of_element_located&lt;/code&gt;表示等待指定元素出现，下面的表格列出了常用的等待条件及其含义。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;等待条件&lt;/th&gt;
&lt;th&gt;具体含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title_is / title_contains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标题是指定的内容 / 标题包含指定的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visibility_of&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素可见&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;presence_of_element_located&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定位的元素加载完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visibility_of_element_located&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定位的元素变得可见&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invisibility_of_element_located&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定位的元素变得不可见&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;presence_of_all_elements_located&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定位的所有元素加载完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_to_be_present_in_element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素包含指定的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_to_be_present_in_element_value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的&lt;code&gt;value&lt;/code&gt;属性包含指定的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame_to_be_available_and_switch_to_it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;载入并切换到指定的内部窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;element_to_be_clickable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素可点击&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;element_to_be_selected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素被选中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;element_located_to_be_selected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定位的元素被选中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alert_is_present&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;出现 Alert 弹窗&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;执行JavaScript代码&lt;/h5&gt;
&lt;p&gt;对于使用瀑布式加载的页面，如果希望在浏览器窗口中加载更多的内容，可以通过浏览器对象的&lt;code&gt;execute_scripts&lt;/code&gt;方法执行 JavaScript 代码来实现。对于一些高级的爬取操作，也很有可能会用到类似的操作，如果你的爬虫代码需要 JavaScript 的支持，建议先对 JavaScript 进行适当的了解，尤其是 JavaScript 中的 BOM 和 DOM 操作。我们在上面的代码中截屏之前加入下面的代码，这样就可以利用 JavaScript 将网页滚到最下方。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 执行JavaScript代码
browser.execute_script(&apos;document.documentElement.scrollTop = document.documentElement.scrollHeight&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Selenium反爬的破解&lt;/h5&gt;
&lt;p&gt;有一些网站专门针对 Selenium 设置了反爬措施，因为使用 Selenium 驱动的浏览器，在控制台中可以看到如下所示的&lt;code&gt;webdriver&lt;/code&gt;属性值为&lt;code&gt;true&lt;/code&gt;，如果要绕过这项检查，可以在加载页面之前，先通过执行 JavaScript 代码将其修改为&lt;code&gt;undefined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/cf42e2c8c4.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;另一方面，我们还可以将浏览器窗口上的“Chrome正受到自动测试软件的控制”隐藏掉，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建Chrome参数对象
options = webdriver.ChromeOptions()
# 添加试验性参数
options.add_experimental_option(&apos;excludeSwitches&apos;, [&apos;enable-automation&apos;])
options.add_experimental_option(&apos;useAutomationExtension&apos;, False)
# 创建Chrome浏览器对象并传入参数
browser = webdriver.Chrome(options=options)
# 执行Chrome开发者协议命令（在加载页面时执行指定的JavaScript代码）
browser.execute_cdp_cmd(
    &apos;Page.addScriptToEvaluateOnNewDocument&apos;,
    {&apos;source&apos;: &apos;Object.defineProperty(navigator, &quot;webdriver&quot;, {get: () =&amp;gt; undefined})&apos;}
)
browser.set_window_size(1200, 800)
browser.get(&apos;https://www.baidu.com/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;无头浏览器&lt;/h5&gt;
&lt;p&gt;很多时候，我们在爬取数据时并不需要看到浏览器窗口，只要有 Chrome 浏览器以及对应的驱动程序，我们的爬虫就能够运转起来。如果不想看到浏览器窗口，我们可以通过下面的方式设置使用无头浏览器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options = webdriver.ChromeOptions()
options.add_argument(&apos;--headless&apos;)
browser = webdriver.Chrome(options=options)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;API参考&lt;/h4&gt;
&lt;p&gt;Selenium 相关的知识还有很多，我们在此就不一一赘述了，下面为大家罗列一些浏览器对象和&lt;code&gt;WebElement&lt;/code&gt;对象常用的属性和方法。具体的内容大家还可以参考 Selenium &lt;a href=&quot;https://selenium-python-zh.readthedocs.io/en/latest/index.html&quot;&gt;官方文档的中文翻译&lt;/a&gt;。&lt;/p&gt;
&lt;h5&gt;浏览器对象&lt;/h5&gt;
&lt;p&gt;表1. 常用属性&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前页面的URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_window_handle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前窗口的句柄（引用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浏览器的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;orientation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前设备的方向（横屏、竖屏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page_source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前页面的源代码（包括动态内容）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前页面的标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window_handles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浏览器打开的所有窗口的句柄&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;表2. 常用方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;back&lt;/code&gt; / &lt;code&gt;forward&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在浏览历史记录中后退/前进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;close&lt;/code&gt; / &lt;code&gt;quit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关闭当前浏览器窗口 / 退出浏览器实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加载指定 URL 的页面到浏览器中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maximize_window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将浏览器窗口最大化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refresh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;刷新当前页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set_page_load_timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置页面加载超时时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set_script_timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置 JavaScript 执行超时时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;implicit_wait&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置等待元素被找到或目标指令完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_cookie&lt;/code&gt; / &lt;code&gt;get_cookies&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定的Cookie / 获取所有Cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add_cookie&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;添加 Cookie 信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_cookie&lt;/code&gt; / &lt;code&gt;delete_all_cookies&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除指定的 Cookie / 删除所有 Cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find_element&lt;/code&gt; / &lt;code&gt;find_elements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找单个元素 / 查找一系列元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;WebElement对象&lt;/h5&gt;
&lt;p&gt;表1. WebElement常用属性&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的尺寸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的文本内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tag_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的标签名&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;表2. 常用方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法名&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清空文本框或文本域中的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;click&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;点击元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_attribute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取元素的属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_displayed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断元素对于用户是否可见&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断元素是否处于可用状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_selected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断元素（单选框和复选框）是否被选中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;send_keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模拟输入文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;submit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提交表单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value_of_css_property&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定的CSS属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find_element&lt;/code&gt; / &lt;code&gt;find_elements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取单个子元素 / 获取一系列子元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;screenshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为元素生成快照&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;简单案例&lt;/h4&gt;
&lt;p&gt;下面的例子演示了如何使用 Selenium 从“360图片”网站搜索和下载图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import time
from concurrent.futures import ThreadPoolExecutor

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

DOWNLOAD_PATH = &apos;images/&apos;


def download_picture(picture_url: str):
    &quot;&quot;&quot;
    下载保存图片
    :param picture_url: 图片的URL
    &quot;&quot;&quot;
    filename = picture_url[picture_url.rfind(&apos;/&apos;) + 1:]
    resp = requests.get(picture_url)
    with open(os.path.join(DOWNLOAD_PATH, filename), &apos;wb&apos;) as file:
        file.write(resp.content)


if not os.path.exists(DOWNLOAD_PATH):
    os.makedirs(DOWNLOAD_PATH)
browser = webdriver.Chrome()
browser.get(&apos;https://image.so.com/z?ch=beauty&apos;)
browser.implicitly_wait(10)
kw_input = browser.find_element(By.CSS_SELECTOR, &apos;input[name=q]&apos;)
kw_input.send_keys(&apos;苍老师&apos;)
kw_input.send_keys(Keys.ENTER)
for _ in range(10):
    browser.execute_script(
        &apos;document.documentElement.scrollTop = document.documentElement.scrollHeight&apos;
    )
    time.sleep(1)
imgs = browser.find_elements(By.CSS_SELECTOR, &apos;div.waterfall img&apos;)
with ThreadPoolExecutor(max_workers=32) as pool:
    for img in imgs:
        pic_url = img.get_attribute(&apos;src&apos;)
        pool.submit(download_picture, pic_url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码，检查指定的目录下是否下载了根据关键词搜索到的图片。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;65 爬虫框架Scrapy简介&lt;/h2&gt;
&lt;p&gt;当你写了很多个爬虫程序之后，你会发现每次写爬虫程序时，都需要将页面获取、页面解析、爬虫调度、异常处理、反爬应对这些代码从头至尾实现一遍，这里面有很多工作其实都是简单乏味的重复劳动。那么，有没有什么办法可以提升我们编写爬虫代码的效率呢？答案是肯定的，那就是利用爬虫框架，而在所有的爬虫框架中，Scrapy 应该是最流行、最强大的框架。&lt;/p&gt;
&lt;h4&gt;Scrapy 概述&lt;/h4&gt;
&lt;p&gt;Scrapy 是基于 Python 的一个非常流行的网络爬虫框架，可以用来抓取 Web 站点并从页面中提取结构化的数据。下图展示了 Scrapy 的基本架构，其中包含了主要组件和系统的数据处理流程（图中带数字的红色箭头）。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day61-65/1d89281ef6.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;Scrapy的组件&lt;/h5&gt;
&lt;p&gt;我们先来说说 Scrapy 中的组件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scrapy 引擎（Engine）：用来控制整个系统的数据处理流程。&lt;/li&gt;
&lt;li&gt;调度器（Scheduler）：调度器从引擎接受请求并排序列入队列，并在引擎发出请求后返还给它们。&lt;/li&gt;
&lt;li&gt;下载器（Downloader）：下载器的主要职责是抓取网页并将网页内容返还给蜘蛛（Spiders）。&lt;/li&gt;
&lt;li&gt;蜘蛛程序（Spiders）：蜘蛛是用户自定义的用来解析网页并抓取特定URL的类，每个蜘蛛都能处理一个域名或一组域名，简单的说就是用来定义特定网站的抓取和解析规则的模块。&lt;/li&gt;
&lt;li&gt;数据管道（Item Pipeline）：管道的主要责任是负责处理有蜘蛛从网页中抽取的数据条目，它的主要任务是清理、验证和存储数据。当页面被蜘蛛解析后，将被发送到数据管道，并经过几个特定的次序处理数据。每个数据管道组件都是一个 Python 类，它们获取了数据条目并执行对数据条目进行处理的方法，同时还需要确定是否需要在数据管道中继续执行下一步或是直接丢弃掉不处理。数据管道通常执行的任务有：清理 HTML 数据、验证解析到的数据（检查条目是否包含必要的字段）、检查是不是重复数据（如果重复就丢弃）、将解析到的数据存储到数据库（关系型数据库或 NoSQL 数据库）中。&lt;/li&gt;
&lt;li&gt;中间件（Middlewares）：中间件是介于引擎和其他组件之间的一个钩子框架，主要是为了提供自定义的代码来拓展 Scrapy 的功能，包括下载器中间件和蜘蛛中间件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;数据处理流程&lt;/h5&gt;
&lt;p&gt;Scrapy 的整个数据处理流程由引擎进行控制，通常的运转流程包括以下的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引擎询问蜘蛛需要处理哪个网站，并让蜘蛛将第一个需要处理的 URL 交给它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引擎让调度器将需要处理的 URL 放在队列中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引擎从调度那获取接下来进行爬取的页面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调度将下一个爬取的 URL 返回给引擎，引擎将它通过下载中间件发送到下载器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当网页被下载器下载完成以后，响应内容通过下载中间件被发送到引擎；如果下载失败了，引擎会通知调度器记录这个 URL，待会再重新下载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;蜘蛛处理响应并返回爬取到的数据条目，此外还要将需要跟进的新的 URL 发送给引擎。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引擎将抓取到的数据条目送入数据管道，把新的 URL 发送给调度器放入队列中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上述操作中的第2步到第8步会一直重复直到调度器中没有需要请求的 URL，爬虫就停止工作。&lt;/p&gt;
&lt;h4&gt;安装和使用Scrapy&lt;/h4&gt;
&lt;p&gt;可以使用 Python 的包管理工具&lt;code&gt;pip&lt;/code&gt;来安装 Scrapy。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install scrapy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在命令行中使用&lt;code&gt;scrapy&lt;/code&gt;命令创建名为&lt;code&gt;demo&lt;/code&gt;的项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scrapy startproject demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目的目录结构如下图所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;demo
|____ demo
|________ spiders
|____________ __init__.py
|________ __init__.py
|________ items.py
|________ middlewares.py
|________ pipelines.py
|________ settings.py
|____ scrapy.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切换到&lt;code&gt;demo&lt;/code&gt; 目录，用下面的命令创建名为&lt;code&gt;douban&lt;/code&gt;的蜘蛛程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scrapy genspider douban movie.douban.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;一个简单的例子&lt;/h5&gt;
&lt;p&gt;接下来，我们实现一个爬取豆瓣电影 Top250 电影标题、评分和金句的爬虫。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;items.py&lt;/code&gt;的&lt;code&gt;Item&lt;/code&gt;类中定义字段，这些字段用来保存数据，方便后续的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scrapy


class DoubanItem(scrapy.Item):
    title = scrapy.Field()
    score = scrapy.Field()
    motto = scrapy.Field()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;spiders&lt;/code&gt;文件夹中名为&lt;code&gt;douban.py&lt;/code&gt; 的文件，它是蜘蛛程序的核心，需要我们添加解析页面的代码。在这里，我们可以通过对&lt;code&gt;Response&lt;/code&gt;对象的解析，获取电影的信息，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scrapy
from scrapy import Selector, Request
from scrapy.http import HtmlResponse

from demo.items import MovieItem


class DoubanSpider(scrapy.Spider):
    name = &apos;douban&apos;
    allowed_domains = [&apos;movie.douban.com&apos;]
    start_urls = [&apos;https://movie.douban.com/top250?start=0&amp;amp;filter=&apos;]

    def parse(self, response: HtmlResponse):
        sel = Selector(response)
        movie_items = sel.css(&apos;#content &amp;gt; div &amp;gt; div.article &amp;gt; ol &amp;gt; li&apos;)
        for movie_sel in movie_items:
            item = MovieItem()
            item[&apos;title&apos;] = movie_sel.css(&apos;.title::text&apos;).extract_first()
            item[&apos;score&apos;] = movie_sel.css(&apos;.rating_num::text&apos;).extract_first()
            item[&apos;motto&apos;] = movie_sel.css(&apos;.inq::text&apos;).extract_first()
            yield item
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码不难看出，我们可以使用 CSS 选择器进行页面解析。当然，如果你愿意也可以使用 XPath 或正则表达式进行页面解析，对应的方法分别是&lt;code&gt;xpath&lt;/code&gt;和&lt;code&gt;re&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果还要生成后续爬取的请求，我们可以用&lt;code&gt;yield&lt;/code&gt;产出&lt;code&gt;Request&lt;/code&gt;对象。&lt;code&gt;Request&lt;/code&gt;对象有两个非常重要的属性，一个是&lt;code&gt;url&lt;/code&gt;，它代表了要请求的地址；一个是&lt;code&gt;callback&lt;/code&gt;，它代表了获得响应之后要执行的回调函数。我们可以将上面的代码稍作修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scrapy
from scrapy import Selector, Request
from scrapy.http import HtmlResponse

from demo.items import MovieItem


class DoubanSpider(scrapy.Spider):
    name = &apos;douban&apos;
    allowed_domains = [&apos;movie.douban.com&apos;]
    start_urls = [&apos;https://movie.douban.com/top250?start=0&amp;amp;filter=&apos;]

    def parse(self, response: HtmlResponse):
        sel = Selector(response)
        movie_items = sel.css(&apos;#content &amp;gt; div &amp;gt; div.article &amp;gt; ol &amp;gt; li&apos;)
        for movie_sel in movie_items:
            item = MovieItem()
            item[&apos;title&apos;] = movie_sel.css(&apos;.title::text&apos;).extract_first()
            item[&apos;score&apos;] = movie_sel.css(&apos;.rating_num::text&apos;).extract_first()
            item[&apos;motto&apos;] = movie_sel.css(&apos;.inq::text&apos;).extract_first()
            yield item

        hrefs = sel.css(&apos;#content &amp;gt; div &amp;gt; div.article &amp;gt; div.paginator &amp;gt; a::attr(&quot;href&quot;)&apos;)
        for href in hrefs:
            full_url = response.urljoin(href.extract())
            yield Request(url=full_url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里，我们已经可以通过下面的命令让爬虫运转起来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scrapy crawl movie
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在控制台看到爬取到的数据，如果想将这些数据保存到文件中，可以通过&lt;code&gt;-o&lt;/code&gt;参数来指定文件名，Scrapy 支持我们将爬取到的数据导出成 JSON、CSV、XML 等格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scrapy crawl moive -o result.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知大家是否注意到，通过运行爬虫获得的 JSON 文件中有&lt;code&gt;275&lt;/code&gt;条数据，那是因为首页被重复爬取了。要解决这个问题，可以对上面的代码稍作调整，不在&lt;code&gt;parse&lt;/code&gt;方法中解析获取新页面的 URL，而是通过&lt;code&gt;start_requests&lt;/code&gt;方法提前准备好待爬取页面的 URL，调整后的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scrapy
from scrapy import Selector, Request
from scrapy.http import HtmlResponse

from demo.items import MovieItem


class DoubanSpider(scrapy.Spider):
    name = &apos;douban&apos;
    allowed_domains = [&apos;movie.douban.com&apos;]

    def start_requests(self):
        for page in range(10):
            yield Request(url=f&apos;https://movie.douban.com/top250?start={page * 25}&apos;)

    def parse(self, response: HtmlResponse):
        sel = Selector(response)
        movie_items = sel.css(&apos;#content &amp;gt; div &amp;gt; div.article &amp;gt; ol &amp;gt; li&apos;)
        for movie_sel in movie_items:
            item = MovieItem()
            item[&apos;title&apos;] = movie_sel.css(&apos;.title::text&apos;).extract_first()
            item[&apos;score&apos;] = movie_sel.css(&apos;.rating_num::text&apos;).extract_first()
            item[&apos;motto&apos;] = movie_sel.css(&apos;.inq::text&apos;).extract_first()
            yield item
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果希望完成爬虫数据的持久化，可以在数据管道中处理蜘蛛程序产生的&lt;code&gt;Item&lt;/code&gt;对象。例如，我们可以通过前面讲到的&lt;code&gt;openpyxl&lt;/code&gt;操作 Excel 文件，将数据写入 Excel 文件中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import openpyxl

from demo.items import MovieItem


class MovieItemPipeline:

    def __init__(self):
        self.wb = openpyxl.Workbook()
        self.sheet = self.wb.active
        self.sheet.title = &apos;Top250&apos;
        self.sheet.append((&apos;名称&apos;, &apos;评分&apos;, &apos;名言&apos;))

    def process_item(self, item: MovieItem, spider):
        self.sheet.append((item[&apos;title&apos;], item[&apos;score&apos;], item[&apos;motto&apos;]))
        return item

    def close_spider(self, spider):
        self.wb.save(&apos;豆瓣电影数据.xlsx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;process_item&lt;/code&gt;和&lt;code&gt;close_spider&lt;/code&gt;都是回调方法（钩子函数）， 简单的说就是 Scrapy 框架会自动去调用的方法。当蜘蛛程序产生一个&lt;code&gt;Item&lt;/code&gt;对象交给引擎时，引擎会将该&lt;code&gt;Item&lt;/code&gt;对象交给数据管道，这时我们配置好的数据管道的&lt;code&gt;parse_item&lt;/code&gt;方法就会被执行，所以我们可以在该方法中获取数据并完成数据的持久化操作。另一个方法&lt;code&gt;close_spider&lt;/code&gt;是在爬虫结束运行前会自动执行的方法，在上面的代码中，我们在这个地方进行了保存 Excel 文件的操作，相信这段代码大家是很容易读懂的。&lt;/p&gt;
&lt;p&gt;总而言之，数据管道可以帮助我们完成以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;清理 HTML 数据，验证爬取的数据。&lt;/li&gt;
&lt;li&gt;丢弃重复的不必要的内容。&lt;/li&gt;
&lt;li&gt;将爬取的结果进行持久化操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;settings.py&lt;/code&gt;文件对项目进行配置，主要需要修改以下几个配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 用户浏览器
USER_AGENT = &apos;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36&apos;

# 并发请求数量 
CONCURRENT_REQUESTS = 4

# 下载延迟
DOWNLOAD_DELAY = 3
# 随机化下载延迟
RANDOMIZE_DOWNLOAD_DELAY = True

# 是否遵守爬虫协议
ROBOTSTXT_OBEY = True

# 配置数据管道
ITEM_PIPELINES = {
   &apos;demo.pipelines.MovieItemPipeline&apos;: 300,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面配置文件中的&lt;code&gt;ITEM_PIPELINES&lt;/code&gt;选项是一个字典，可以配置多个处理数据的管道，后面的数字代表了执行的优先级，数字小的先执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Day46-60 教程全文（15 篇合并）</title><link>https://www.klucen.cn/posts/python/day46-60-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day46-60-full/</guid><pubDate>Sun, 14 Dec 2025 00:14:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 15 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：Django 全栈开发
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#46-django%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B&quot;&gt;46 Django快速上手&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#47-%E6%B7%B1%E5%85%A5%E6%A8%A1%E5%9E%8B&quot;&gt;47 深入模型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#48-%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E5%92%8Cajax%E8%AF%B7%E6%B1%82&quot;&gt;48 静态资源和Ajax请求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#49-cookie%E5%92%8Csession&quot;&gt;49 Cookie和Session&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#50-%E5%88%B6%E4%BD%9C%E6%8A%A5%E8%A1%A8&quot;&gt;50 制作报表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#51-%E6%97%A5%E5%BF%97%E5%92%8C%E8%B0%83%E8%AF%95%E5%B7%A5%E5%85%B7%E6%A0%8F&quot;&gt;51 日志和调试工具栏&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#52-%E4%B8%AD%E9%97%B4%E4%BB%B6%E7%9A%84%E5%BA%94%E7%94%A8&quot;&gt;52 中间件的应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#53-%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8&quot;&gt;53 前后端分离开发入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#54-restful%E6%9E%B6%E6%9E%84%E5%92%8Cdrf%E5%85%A5%E9%97%A8&quot;&gt;54 RESTful架构和DRF入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#55-restful%E6%9E%B6%E6%9E%84%E5%92%8Cdrf%E8%BF%9B%E9%98%B6&quot;&gt;55 RESTful架构和DRF进阶&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#56-%E4%BD%BF%E7%94%A8%E7%BC%93%E5%AD%98&quot;&gt;56 使用缓存&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#57-%E6%8E%A5%E5%85%A5%E4%B8%89%E6%96%B9%E5%B9%B3%E5%8F%B0&quot;&gt;57 接入三方平台&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#58-%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E5%92%8C%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1&quot;&gt;58 异步任务和定时任务&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#59-%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95&quot;&gt;59 单元测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#60-%E9%A1%B9%E7%9B%AE%E4%B8%8A%E7%BA%BF&quot;&gt;60 项目上线&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;46 Django快速上手&lt;/h2&gt;
&lt;p&gt;Web开发的早期阶段，开发者需要手动编写每个页面，例如一个新闻门户网站，每天都要修改它的HTML页面，随着网站规模和体量的增大，这种做法一定是非常糟糕的。为了解决这个问题，开发人员想到了用程序来为Web服务器生成动态内容，也就是说网页中的动态内容不再通过手动编写而是通过程序自动生成。最早的时候，这项技术被称为CGI（公共网关接口），当然随着时间的推移，CGI暴露出的问题也越来越多，例如大量重复的样板代码，总体性能较为低下等。在时代呼唤新英雄的背景下，PHP、ASP、JSP这类Web应用开发技术在上世纪90年代中后期如雨后春笋般涌现。通常我们说的Web应用是指通过浏览器来访问网络资源的应用程序，因为浏览器的普及性以及易用性，Web应用使用起来方便简单，免除了安装和更新应用程序带来的麻烦；站在开发者的角度，也不用关心用户使用什么样的操作系统，甚至不用区分是PC端还是移动端。&lt;/p&gt;
&lt;h4&gt;Web应用机制和术语&lt;/h4&gt;
&lt;p&gt;下图向我们展示了Web应用的工作流程，其中涉及到的术语如下表所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/09872f0277.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：相信有经验的读者会发现，这张图中其实还少了很多东西，例如反向代理服务器、数据库服务器、防火墙等，而且图中的每个节点在实际项目部署时可能是一组节点组成的集群。当然，如果你对这些没有什么概念也不要紧，继续下去就行了，后面会给大家一一讲解的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;术语&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;URL/URI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;统一资源定位符/统一资源标识符，网络资源的唯一标识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;域名&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与Web服务器地址对应的一个易于记忆的字符串名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DNS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;域名解析服务，可以将域名转换成对应的IP地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP地址&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;网络上的主机的身份标识，通过IP地址可以区分不同的主机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;超文本传输协议，构建在TCP之上的应用级协议，万维网数据通信的基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;反向代理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;代理客户端向服务器发出请求，然后将服务器返回的资源返回给客户端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web服务器&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;接受HTTP请求，然后返回HTML文件、纯文本文件、图像等资源给请求者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nginx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高性能的Web服务器，也可以用作&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86&quot;&gt;反向代理&lt;/a&gt;，&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1&quot;&gt;负载均衡&lt;/a&gt; 和 &lt;a href=&quot;https://zh.wikipedia.org/wiki/HTTP%E7%BC%93%E5%AD%98&quot;&gt;HTTP缓存&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;HTTP协议&lt;/h5&gt;
&lt;p&gt;这里我们先费一些笔墨来说说HTTP这个协议。HTTP（超文本传输协议）是构建于TCP（传输控制协议）之上应用级协议，它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。按照维基百科上的介绍，设计HTTP最初的目的是为了提供一种发布和接收&lt;a href=&quot;https://zh.wikipedia.org/wiki/HTML&quot;&gt;HTML&lt;/a&gt;页面的方法，也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。关于这个协议的详细信息以及目前的发展状况，大家可以阅读&lt;a href=&quot;http://www.ruanyifeng.com/blog/2016/08/http.html&quot;&gt;《HTTP 协议入门》&lt;/a&gt;、&lt;a href=&quot;http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html&quot;&gt;《互联网协议入门》&lt;/a&gt;系列以及&lt;a href=&quot;http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html&quot;&gt;《图解HTTPS协议》&lt;/a&gt;这几篇文章进行了解。下图是我在四川省网络通信技术重点实验室学习和工作期间使用开源协议分析工具Ethereal（抓包工具WireShark的前身）截取的访问百度首页时的HTTP请求和响应的报文（协议数据），由于Ethereal截取的是经过网络适配器的数据，因此可以清晰的看到从物理链路层到应用层的协议数据。&lt;/p&gt;
&lt;p&gt;HTTP请求（请求行+请求头+空行+[消息体]）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/2baba63eec.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HTTP响应（响应行+响应头+空行+消息体）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/ffd6f1593e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：这两张图是在2009年9月10日凌晨获得的，但愿这两张如同泛黄的照片般的截图能帮助你了解HTTP到底是什么样子的。当然，如果没有专业的抓包工具，也可以通过浏览器提供的“开发者工具”来查看HTTP请求和响应的数据格式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Django概述&lt;/h4&gt;
&lt;p&gt;Python的Web框架有上百个，比它的关键字还要多。所谓Web框架，就是用于开发Web服务器端应用的基础设施，说得通俗一点就是一系列封装好的模块和工具。事实上，即便没有Web框架，我们仍然可以通过socket或&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E7%BD%91%E5%85%B3%E6%8E%A5%E5%8F%A3&quot;&gt;CGI&lt;/a&gt;来开发Web服务器端应用，但是这样做的成本和代价在商业项目中通常是不能接受的。通过Web框架，我们可以化繁为简，降低创建、更新、扩展应用程序的工作量。刚才我们说到Python有上百个Web框架，这些框架包括Django、Flask、Tornado、Sanic、Pyramid、Bottle、Web2py、web.py等。&lt;/p&gt;
&lt;p&gt;在上述Python的Web框架中，Django无疑是最有代表性的重量级选手，开发者可以基于Django快速的开发可靠的Web应用程序，因为它减少了Web开发中不必要的开销，对常用的设计和开发模式进行了封装，并对MVC架构提供了支持（Django中称之为MTV架构）。MVC是软件系统开发领域中一种放之四海而皆准的架构，它将系统中的组件分为模型（Model）、视图（View）和控制器（Controller）三个部分并借此实现模型（数据）和视图（显示）的解耦合。由于模型和视图进行了分离，所以需要一个中间人将解耦合的模型和视图联系起来，扮演这个角色的就是控制器。稍具规模的软件系统都会使用MVC架构（或者是从MVC演进出的其他架构），Django项目中我们称之为MTV，MTV中的M跟MVC中的M没有区别，就是代表数据的模型，T代表了网页模板（显示数据的视图），而V代表了视图函数，在Django框架中，视图函数和Django框架本身一起扮演了MVC中C的角色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/0d5a752e18.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Django框架诞生于2003年，它是一个在真正的应用中成长起来的项目，由劳伦斯出版集团旗下在线新闻网站的内容管理系统（CMS）研发团队（主要是Adrian Holovaty和Simon Willison）开发，以比利时的吉普赛爵士吉他手Django Reinhardt来命名。Django框架在2005年夏天作为开源框架发布，使用Django框架能用很短的时间构建出功能完备的网站，因为它代替程序员完成了那些重复乏味的劳动，剩下真正有意义的核心业务给程序员来开发，这一点就是对DRY（Don&apos;t Repeat Yourself）理念的最好践行。许多成功的网站和应用都是基于Python语言进行开发的，国内比较有代表性的网站包括：知乎、豆瓣网、果壳网、搜狐闪电邮箱、101围棋网、海报时尚网、背书吧、堆糖、手机搜狐网、咕咚、爱福窝、果库等，其中不乏使用了Django框架的产品。&lt;/p&gt;
&lt;h4&gt;快速上手&lt;/h4&gt;
&lt;h5&gt;第一个Django项目&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;检查Python环境：Django 1.11需要Python 2.7或Python 3.4以上的版本；Django 2.0需要Python 3.4以上的版本；Django 2.1和2.2需要Python 3.5以上的版本；Django 3.0需要Python 3.6以上版本。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：Django框架不同版本所需的Python解释器环境，可以在Django官方文档的&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/3.0/faq/install/#faq-python-version-support&quot;&gt;FAQ&lt;/a&gt;中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以在macOS的终端中输入下面的命令检查Python解释器版本，Windows系统可以在命令行提示符中输入&lt;code&gt;python --version&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;python3 --version&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
也可以在Python的交互式环境中执行下面的代码来查看Python解释器的版本。

```shell
import sys
sys.version
sys.version_info
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;更新包管理工具并安装Django环境（用于创建Django项目）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在更新这个文档时，Django最新的正式版本是3.0.7，Django 3.0提供了对ASGI的支持，可以实现全双工的异步通信，但是目前的使用体验一般，所以暂时不推荐大家使用Django 3.0，下面我们安装的是Django 2.2.13版本。使用&lt;code&gt;pip&lt;/code&gt;安装三方库和工具时，可以通过&lt;code&gt;==&lt;/code&gt;来指定安装的版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pip3 install -U pip
pip3 install django==2.2.13
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查Django环境并使用&lt;code&gt;django-admin&lt;/code&gt;命令创建Django项目（项目名称为hellodjango）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;django-admin --version
django-admin startproject hellodjango
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用PyCharm打开创建好的Djang项目，并为其添加虚拟环境。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/eb9d701ff6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，PyCharm的项目浏览器中，最顶层的文件夹&lt;code&gt;hellodjango&lt;/code&gt;是Python项目文件夹，这个文件夹的名字并不重要，Django项目也不关心这个文件夹叫什么名字。该文件夹下有一个同名的文件夹，它是Django项目文件夹，其中包含了&lt;code&gt;__init__.py&lt;/code&gt;、&lt;code&gt;settings.py&lt;/code&gt;、&lt;code&gt;urls.py&lt;/code&gt;、&lt;code&gt;wsgi.py&lt;/code&gt;四个文件，与名为&lt;code&gt;hellodjango&lt;/code&gt;的Django项目文件夹同级的还有一个名为&lt;code&gt;manage.py&lt;/code&gt; 的文件，这些文件的作用如下所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hellodjango/__init__.py&lt;/code&gt;：空文件，告诉Python解释器这个目录应该被视为一个Python的包。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hellodjango/settings.py&lt;/code&gt;：Django项目的配置文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hellodjango/urls.py&lt;/code&gt;：Django项目的URL映射声明，就像是网站的“目录”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hellodjango/wsgi.py&lt;/code&gt;：项目运行在WSGI兼容Web服务器上的入口文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manage.py&lt;/code&gt;： 管理Django项目的脚本程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：WSGI全称是Web服务器网关接口，维基百科上给出的解释是“为Python语言定义的&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%B6%B2%E9%A0%81%E4%BC%BA%E6%9C%8D%E5%99%A8&quot;&gt;Web服务器&lt;/a&gt;和&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F&quot;&gt;Web应用程序&lt;/a&gt;或框架之间的一种简单而通用的接口”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建虚拟环境的界面如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/dfb47eed0f.webp&quot; alt=&quot;pycharm-django-virtual-environment&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装项目依赖项。&lt;/p&gt;
&lt;p&gt;方法一：打开PyCharm的终端，在终端中通过&lt;code&gt;pip&lt;/code&gt;命令安装Django项目的依赖项。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：由于已经基于Python 3解释器环境为项目创建了虚拟环境，所以虚拟环境中的&lt;code&gt;python&lt;/code&gt;命令对应的是Python 3的解释器，而&lt;code&gt;pip&lt;/code&gt;命令对应的是Python 3的包管理工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pip install django==2.2.13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法二：在PyCharm的偏好设置中，可以找到项目的解释器环境和已经安装的三方库，可以通过点击添加按钮来安装新的依赖项，需要提醒大家的是在安装Django依赖项时，需要指定版本号，否则将默认安装更新本文时最新的3.0.7版本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/879fc3ad5d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图展示了Django版本和Python版本的对应关系，请大家自行对号入座。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Django版本&lt;/th&gt;
&lt;th&gt;Python版本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;2.7、3.2、3.3、3.4、3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.9、1.10&lt;/td&gt;
&lt;td&gt;2.7、3.4、3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;td&gt;2.7、3.4、3.5、3.6、3.7（Django 1.11.17）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;3.4、3.5、3.6、3.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.1&lt;/td&gt;
&lt;td&gt;3.5、3.6、3.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;3.5、3.6、3.7、3.8（Django 2.2.8）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;3.6、3.7、3.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动Django自带的服务器运行项目。&lt;/p&gt;
&lt;p&gt;方法一：在“Run”菜单选择“Edit Configuration”，配置“Django server”运行项目（适用于专业版PyCharm）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/ff7b5e9f97.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;方法二：在“Run”菜单选择“Edit Configuration”，配置运行“Python”程序运行项目（适用于专业版和社区版PyCharm）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/eeeebfa001.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;方法三：在PyCharm的终端（Terminal）中通过命令运行项目（适用于专业版和社区版PyCharm）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看运行效果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在浏览器中输入&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;访问我们的服务器，效果如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/2272b20a9b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;刚刚启动的Django自带的服务器只能用于开发和测试环境，因为这个服务器是纯Python编写的轻量级Web服务器，不适合在生产环境中使用。&lt;/li&gt;
&lt;li&gt;如果修改了代码，不需要为了让修改的代码生效而重新启动Django自带的服务器。但是，在添加新的项目文件时，该服务器不会自动重新加载，这个时候就得手动重启服务器。&lt;/li&gt;
&lt;li&gt;可以在终端中通过&lt;code&gt;python manage.py help&lt;/code&gt;命令查看Django管理脚本程序可用的命令参数。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;python manage.py runserver&lt;/code&gt;启动服务器时，可以在后面添加参数来指定IP地址和端口号，默认情况下启动的服务器将运行在本机的&lt;code&gt;8000&lt;/code&gt;端口。&lt;/li&gt;
&lt;li&gt;在终端中运行的服务器，可以通过Ctrl+C来停止它 。通过PyCharm的“运行配置”运行的服务器直接点击窗口上的关闭按钮就可以终止服务器的运行。&lt;/li&gt;
&lt;li&gt;不能在同一个端口上启动多个服务器，因为会导致地址的冲突（端口是对IP地址的扩展，也是计算机网络地址的一部分）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改项目的配置文件&lt;code&gt;settings.py&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Django是一个支持国际化和本地化的框架，因此刚才我们看到的Django项目的默认首页也是支持国际化的，我们可以通过修改配置文件将默认语言修改为中文，时区设置为东八区。&lt;/p&gt;
&lt;p&gt;找到修改前的配置（在&lt;code&gt;settings.py&lt;/code&gt;文件第100行以后）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LANGUAGE_CODE = &apos;en-us&apos;
TIME_ZONE = &apos;UTC&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改为以下内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LANGUAGE_CODE = &apos;zh-hans&apos;
TIME_ZONE = &apos;Asia/Chongqing&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷新刚才的页面，可以看到修改语言代码和时区之后的结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/12a91aa080.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;创建自己的应用&lt;/h5&gt;
&lt;p&gt;如果要开发自己的Web应用，需要先在Django项目中创建“应用”，一个Django项目可以包含一个或多个应用。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在PyCharm的终端中执行下面的命令，创建名为&lt;code&gt;first&lt;/code&gt;的应用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py startapp first
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的命令会在当前路径下创建&lt;code&gt;first&lt;/code&gt;目录，其目录结构如下所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__init__.py&lt;/code&gt;：一个空文件，告诉Python解释器这个目录应该被视为一个Python的包。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin.py&lt;/code&gt;：可以用来注册模型，用于在Django框架自带的管理后台中管理模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apps.py&lt;/code&gt;：当前应用的配置文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;migrations&lt;/code&gt;：存放与模型有关的数据库迁移信息。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__init__.py&lt;/code&gt;：一个空文件，告诉Python解释器这个目录应该被视为一个Python的包。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;models.py&lt;/code&gt;：存放应用的数据模型（MTV中的M）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests.py&lt;/code&gt;：包含测试应用各项功能的测试类和测试函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;views.py&lt;/code&gt;：处理用户HTTP请求并返回HTTP响应的函数或类（MTV中的V）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改应用目录下的视图文件&lt;code&gt;views.py&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.http import HttpResponse


def show_index(request):
    return HttpResponse(&apos;&amp;lt;h1&amp;gt;Hello, Django!&amp;lt;/h1&amp;gt;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改Django项目目录下的&lt;code&gt;urls.py&lt;/code&gt;文件，将视图函数和用户在浏览器中请求的路径对应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include

from first.views import show_index

urlpatterns = [
    path(&apos;admin/&apos;, admin.site.urls),
    path(&apos;hello/&apos;, show_index),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新运行项目，并打开浏览器中访问&lt;code&gt;http://127.0.0.1:8000/hello/&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上面我们通过代码为浏览器生成了内容，但仍然是静态内容，如果要生成动态内容，可以修改&lt;code&gt;views.py&lt;/code&gt;文件并添加如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from random import sample

from django.http import HttpResponse


def show_index(request):
    fruits = [
        &apos;Apple&apos;, &apos;Orange&apos;, &apos;Pitaya&apos;, &apos;Durian&apos;, &apos;Waxberry&apos;, &apos;Blueberry&apos;,
        &apos;Grape&apos;, &apos;Peach&apos;, &apos;Pear&apos;, &apos;Banana&apos;, &apos;Watermelon&apos;, &apos;Mango&apos;
    ]
    selected_fruits = sample(fruits, 3)
    content = &apos;&amp;lt;h3&amp;gt;今天推荐的水果是：&amp;lt;/h3&amp;gt;&apos;
    content += &apos;&amp;lt;hr&amp;gt;&apos;
    content += &apos;&amp;lt;ul&amp;gt;&apos;
    for fruit in selected_fruits:
        content += f&apos;&amp;lt;li&amp;gt;{fruit}&amp;lt;/li&amp;gt;&apos;
    content += &apos;&amp;lt;/ul&amp;gt;&apos;
    return HttpResponse(content)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刷新页面查看程序的运行结果，看看每次刷新的网页的时候，是不是可以看到不一样的内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;使用模板&lt;/h5&gt;
&lt;p&gt;上面通过拼接HTML代码的方式为浏览器生成动态内容的做法在实际开发中是无能接受的，因为实际项目中的前端页面可能非常复杂，无法用这种拼接动态内容的方式来完成，这一点大家一定能够想到。为了解决这个问题，我们可以提前准备一个模板页（MTV中的T），所谓模板页就是一个带占位符和模板指令的HTML页面。&lt;/p&gt;
&lt;p&gt;Django框架中有一个名为&lt;code&gt;render&lt;/code&gt;的便捷函数可以来完成渲染模板的操作。所谓的渲染就是用数据替换掉模板页中的模板指令和占位符，当然这里的渲染称为后端渲染，即在服务器端完成页面的渲染再输出到浏览器中。后端渲染的做法在Web应用的访问量较大时，会让服务器承受较大的负担，所以越来越多的Web应用会选择前端渲染的方式，即服务器只提供页面所需的数据（通常是JSON格式），在浏览器中通过JavaScript代码获取这些数据并渲染页面上。关于前端渲染的内容，我们会在后续的课程中为大家讲解，目前我们使用的是通过模板页进行后端渲染的做法，具体步骤如下所示。&lt;/p&gt;
&lt;p&gt;使用模板页的步骤如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在项目目录下创建名为templates文件夹。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/a116561461.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加模板页&lt;code&gt;index.html&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：实际项目开发中，静态页由前端开发者提供，后端开发者需要将静态页修改为模板页，以便通过Python程序对其进行渲染，这种做法就是上面提到的后端渲染。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
        &amp;lt;title&amp;gt;首页&amp;lt;/title&amp;gt;
        &amp;lt;style&amp;gt;
            #fruits {
                font-size: 1.25em;
            }
        &amp;lt;/style&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;h1&amp;gt;今天推荐的水果是：&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;ul id=&quot;fruits&quot;&amp;gt;
            {% for fruit in fruits %}
            &amp;lt;li&amp;gt;{{ fruit }}&amp;lt;/li&amp;gt;
            {% endfor %}
        &amp;lt;/ul&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的模板页中我们使用了&lt;code&gt;{{ fruit }}&lt;/code&gt;这样的模板占位符语法，也使用了&lt;code&gt;{% for %}&lt;/code&gt;这样的模板指令，这些都是Django模板语言（DTL）的一部分。关于模板语法和指令，大家可以看看官方文档，相信这些内容还是很容易理解的，并不需要过多的赘述，大家也可以参考&lt;a&gt;官方文档&lt;/a&gt;了解模板指令和语法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;views.py&lt;/code&gt;文件，调用&lt;code&gt;render&lt;/code&gt;函数渲染模板页。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from random import sample

from django.shortcuts import render


def show_index(request):
    fruits = [
        &apos;Apple&apos;, &apos;Orange&apos;, &apos;Pitaya&apos;, &apos;Durian&apos;, &apos;Waxberry&apos;, &apos;Blueberry&apos;,
        &apos;Grape&apos;, &apos;Peach&apos;, &apos;Pear&apos;, &apos;Banana&apos;, &apos;Watermelon&apos;, &apos;Mango&apos;
    ]
    selected_fruits = sample(fruits, 3)
    return render(request, &apos;index.html&apos;, {&apos;fruits&apos;: selected_fruits})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;render&lt;/code&gt;函数的第一个参数是请求对象request，第二个参数是我们要渲染的模板页的名字，第三个参数是要渲染到页面上的数据，我们通过一个字典将数据交给模板页，字典中的键就是模板页中使用的模板指令或占位符中的变量名。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;到此为止，视图函数中的&lt;code&gt;render&lt;/code&gt;还无法找到模板文件&lt;code&gt;index.html&lt;/code&gt;，需要修改&lt;code&gt;settings.py&lt;/code&gt;文件，配置模板文件所在的路径。修改&lt;code&gt;settings.py&lt;/code&gt;文件，找到&lt;code&gt;TEMPLATES&lt;/code&gt;配置，修改其中的&lt;code&gt;DIRS&lt;/code&gt;配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [
    {
        &apos;BACKEND&apos;: &apos;django.template.backends.django.DjangoTemplates&apos;,
        &apos;DIRS&apos;: [os.path.join(BASE_DIR, &apos;templates&apos;), ],
        &apos;APP_DIRS&apos;: True,
        &apos;OPTIONS&apos;: {
            &apos;context_processors&apos;: [
                &apos;django.template.context_processors.debug&apos;,
                &apos;django.template.context_processors.request&apos;,
                &apos;django.contrib.auth.context_processors.auth&apos;,
                &apos;django.contrib.messages.context_processors.messages&apos;,
            ],
        },
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重新运行项目或直接刷新页面查看结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;至此，我们已经利用Django框架完成了一个非常小的Web应用，虽然它并没有任何的实际价值，但是可以通过这个项目对Django框架有一个感性的认识。学习Django最好的资料肯定是它的&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/2.0/&quot;&gt;官方文档&lt;/a&gt;，官方文档提供了对多国语言的支持，而且有新手教程引导初学者学习使用Django框架，建议大家通过阅读Django的官方文档来学习和使用这个框架。当然图灵社区出版的&lt;a href=&quot;http://www.ituring.com.cn/book/2630&quot;&gt;《Django基础教程》&lt;/a&gt;也是非常适合初学者的入门级读物，有兴趣的读者可以点击链接进行购买。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;47 深入模型&lt;/h2&gt;
&lt;p&gt;在上一个章节中，我们提到了Django是基于MVC架构的Web框架，MVC架构追求的是“模型”和“视图”的解耦合。所谓“模型”说得更直白一些就是数据（的表示），所以通常也被称作“数据模型”。在实际的项目中，数据模型通常通过数据库实现持久化操作，而关系型数据库在过去和当下都是持久化的首选方案，下面我们通过完成一个投票项目来讲解和模型相关的知识点。投票项目的首页会展示某在线教育平台所有的学科；点击学科可以查看到该学科的老师及其信息；用户登录后在查看老师的页面为老师投票，可以投赞成票和反对票；未登录的用户可以通过登录页进行登录；尚未注册的用户可以通过注册页输入个人信息进行注册。在这个项目中，我们使用MySQL数据库来实现数据持久化操作。&lt;/p&gt;
&lt;h4&gt;创建项目和应用&lt;/h4&gt;
&lt;p&gt;我们首先创建Django项目&lt;code&gt;vote&lt;/code&gt;并为其添加虚拟环境和依赖项。接下来，在项目下创建名为&lt;code&gt;polls&lt;/code&gt;的应用和保存模板页的文件夹&lt;code&gt;tempaltes&lt;/code&gt;，项目文件夹的结构如下所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/6f7011cfd7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据上面描述的项目需求，我们准备了四个静态页面，分别是展示学科的页面&lt;code&gt;subjects.html&lt;/code&gt;，显示学科老师的页面&lt;code&gt;teachers.html&lt;/code&gt;，登录页面&lt;code&gt;login.html&lt;/code&gt;，注册页面&lt;code&gt;register.html&lt;/code&gt;，稍后我们会将静态页修改为Django项目所需的模板页。&lt;/p&gt;
&lt;h4&gt;配置关系型数据库MySQL&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在MySQL中创建数据库，创建用户，授权用户访问该数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create database vote default charset utf8;
create user &apos;hellokitty&apos;@&apos;%&apos; identified by &apos;Hellokitty.618&apos;;
grant all privileges on vote.* to &apos;hellokitty&apos;@&apos;%&apos;;
flush privileges;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在MySQL中创建保存学科和老师信息的二维表（保存用户信息的表稍后处理）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use vote;

-- 创建学科表
create table `tb_subject`
(
	`no` integer auto_increment comment &apos;学科编号&apos;,
    `name` varchar(50) not null comment &apos;学科名称&apos;,
    `intro` varchar(1000) not null default &apos;&apos; comment &apos;学科介绍&apos;,
    `is_hot` boolean not null default 0 comment &apos;是不是热门学科&apos;,
    primary key (`no`)
);
-- 创建老师表
create table `tb_teacher`
(
    `no` integer auto_increment comment &apos;老师编号&apos;,
    `name` varchar(20) not null comment &apos;老师姓名&apos;,
    `sex` boolean not null default 1 comment &apos;老师性别&apos;,
    `birth` date not null comment &apos;出生日期&apos;,
    `intro` varchar(1000) not null default &apos;&apos; comment &apos;老师介绍&apos;,
    `photo` varchar(255) not null default &apos;&apos; comment &apos;老师照片&apos;,
    `gcount` integer not null default 0 comment &apos;好评数&apos;,
    `bcount` integer not null default 0 comment &apos;差评数&apos;,
    `sno` integer not null comment &apos;所属学科&apos;,
    primary key (`no`),
    foreign key (`sno`) references `tb_subject` (`no`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在虚拟环境中安装连接MySQL数据库所需的依赖项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install mysqlclient
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果因为某些原因无法安装&lt;code&gt;mysqlclient&lt;/code&gt;三方库，可以使用它的替代品&lt;code&gt;pymysql&lt;/code&gt;，&lt;code&gt;pymysql&lt;/code&gt;是用纯Python开发的连接MySQL的Python库，安装更容易成功，但是需要在Django项目文件夹的&lt;code&gt;__init__.py&lt;/code&gt;中添加如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

pymysql.install_as_MySQLdb()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用Django 2.2及以上版本，还会遇到PyMySQL跟Django框架的兼容性问题，兼容性问题会导致项目无法运行，需要按照GitHub上PyMySQL仓库&lt;a href=&quot;https://github.com/PyMySQL/PyMySQL/issues/790&quot;&gt;Issues&lt;/a&gt;中提供的方法进行处理。总体来说，使用&lt;code&gt;pymysql&lt;/code&gt;会比较麻烦，强烈建议大家首选安装&lt;code&gt;mysqlclient&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改项目的settings.py文件，首先将我们创建的应用&lt;code&gt;polls&lt;/code&gt;添加已安装的项目（&lt;code&gt;INSTALLED_APPS&lt;/code&gt;）中，然后配置MySQL作为持久化方案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    &apos;django.contrib.admin&apos;,
    &apos;django.contrib.auth&apos;,
    &apos;django.contrib.contenttypes&apos;,
    &apos;django.contrib.sessions&apos;,
    &apos;django.contrib.messages&apos;,
    &apos;django.contrib.staticfiles&apos;,
    &apos;polls&apos;,
]

DATABASES = {
    &apos;default&apos;: {
        # 数据库引擎配置
        &apos;ENGINE&apos;: &apos;django.db.backends.mysql&apos;,
        # 数据库的名字
        &apos;NAME&apos;: &apos;vote&apos;,
        # 数据库服务器的IP地址（本机可以写localhost或127.0.0.1）
        &apos;HOST&apos;: &apos;localhost&apos;,
        # 启动MySQL服务的端口号
        &apos;PORT&apos;: 3306,
        # 数据库用户名和口令
        &apos;USER&apos;: &apos;hellokitty&apos;,
        &apos;PASSWORD&apos;: &apos;Hellokitty.618&apos;,
        # 数据库使用的字符集
        &apos;CHARSET&apos;: &apos;utf8&apos;,
        # 数据库时间日期的时区设定
        &apos;TIME_ZONE&apos;: &apos;Asia/Chongqing&apos;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在配置ENGINE属性时，常用的可选值包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&apos;django.db.backends.sqlite3&apos;&lt;/code&gt;：SQLite嵌入式数据库。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;django.db.backends.postgresql&apos;&lt;/code&gt;：BSD许可证下发行的开源关系型数据库产品。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;django.db.backends.mysql&apos;&lt;/code&gt;：甲骨文公司经济高效的数据库产品。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;django.db.backends.oracle&apos;&lt;/code&gt;：甲骨文公司关系型数据库旗舰产品。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的配置可以参考官方文档中&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/2.0/ref/databases/#third-party-notes&quot;&gt;数据库配置&lt;/a&gt;的部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django框架提供了ORM来解决数据持久化问题，ORM翻译成中文叫“对象关系映射”。因为Python是面向对象的编程语言，我们在Python程序中使用对象模型来保存数据，而关系型数据库使用关系模型，用二维表来保存数据，这两种模型并不匹配。使用ORM是为了实现对象模型到关系模型的&lt;strong&gt;双向转换&lt;/strong&gt;，这样就不用在Python代码中书写SQL语句和游标操作，因为这些都会由ORM自动完成。利用Django的ORM，我们可以直接将刚才创建的学科表和老师表变成Django中的模型类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py inspectdb &amp;gt; polls/models.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以对自动生成的模型类稍作调整，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.db import models


class Subject(models.Model):
    no = models.AutoField(primary_key=True, verbose_name=&apos;编号&apos;)
    name = models.CharField(max_length=50, verbose_name=&apos;名称&apos;)
    intro = models.CharField(max_length=1000, verbose_name=&apos;介绍&apos;)
    is_hot = models.BooleanField(verbose_name=&apos;是否热门&apos;)

    class Meta:
        managed = False
        db_table = &apos;tb_subject&apos;


class Teacher(models.Model):
    no = models.AutoField(primary_key=True, verbose_name=&apos;编号&apos;)
    name = models.CharField(max_length=20, verbose_name=&apos;姓名&apos;)
    sex = models.BooleanField(default=True, verbose_name=&apos;性别&apos;)
    birth = models.DateField(verbose_name=&apos;出生日期&apos;)
    intro = models.CharField(max_length=1000, verbose_name=&apos;个人介绍&apos;)
    photo = models.ImageField(max_length=255, verbose_name=&apos;照片&apos;)
    good_count = models.IntegerField(default=0, db_column=&apos;gcount&apos;, verbose_name=&apos;好评数&apos;)
    bad_count = models.IntegerField(default=0, db_column=&apos;bcount&apos;, verbose_name=&apos;差评数&apos;)
    subject = models.ForeignKey(Subject, models.DO_NOTHING, db_column=&apos;sno&apos;)

    class Meta:
        managed = False
        db_table = &apos;tb_teacher&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：模型类都直接或间接继承自&lt;code&gt;Model&lt;/code&gt;类，模型类跟关系型数据库的二维表对应，模型对象跟表中的记录对应，模型对象的属性跟表中的字段对应。如果对上面模型类的属性定义不是特别理解，可以看看本文后面提供的“模型定义参考”部分的内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用ORM完成模型的CRUD操作&lt;/h4&gt;
&lt;p&gt;有了Django框架的ORM，我们可以直接使用面向对象的方式来实现对数据的CRUD（增删改查）操作。我们可以在PyCharm的终端中输入下面的命令进入到Django项目的交互式环境，然后尝试对模型的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;新增&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;from polls.models import Subject

subject1 = Subject(name=&apos;Python全栈开发&apos;, intro=&apos;当下最热门的学科&apos;, is_hot=True)
subject1.save()
subject2 = Subject(name=&apos;全栈软件测试&apos;, intro=&apos;学习自动化测试的学科&apos;, is_hot=False)
subject2.save()
subject3 = Subject(name=&apos;JavaEE分布式开发&apos;, intro=&apos;基于Java语言的服务器应用开发&apos;, is_hot=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;删除&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;subject = Subject.objects.get(no=2)
subject.delete()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;更新&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;subject = Subject.objects.get(no=1)
subject.name = &apos;Python全栈+人工智能&apos;
subject.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;查询&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;查询所有对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Subject.objects.all()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;过滤数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查询名称为“Python全栈+人工智能”的学科
Subject.objects.filter(name=&apos;Python全栈+人工智能&apos;)

# 查询名称包含“全栈”的学科（模糊查询）
Subject.objects.filter(name__contains=&apos;全栈&apos;)
Subject.objects.filter(name__startswith=&apos;全栈&apos;)
Subject.objects.filter(name__endswith=&apos;全栈&apos;)

# 查询所有热门学科
Subject.objects.filter(is_hot=True)

# 查询编号大于3小于10的学科
Subject.objects.filter(no__gt=3).filter(no__lt=10)
Subject.objects.filter(no__gt=3, no__lt=10)

# 查询编号在3到7之间的学科
Subject.objects.filter(no__ge=3, no__le=7)
Subject.objects.filter(no__range=(3, 7))
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查询单个对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查询主键为1的学科
Subject.objects.get(pk=1)
Subject.objects.get(no=1)
Subject.objects.filter(no=1).first()
Subject.objects.filter(no=1).last()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;排序。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查询所有学科按编号升序排列
Subject.objects.order_by(&apos;no&apos;)
# 查询所有部门按部门编号降序排列
Subject.objects.order_by(&apos;-no&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;切片（分页查询）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 按编号从小到大查询前3个学科
Subject.objects.order_by(&apos;no&apos;)[:3]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;计数。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查询一共有多少个学科
Subject.objects.count()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;高级查询。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查询编号为1的学科的老师
Teacher.objects.filter(subject__no=1)
Subject.objects.get(pk=1).teacher_set.all() 

# 查询学科名称有“全栈”二字的学科的老师
Teacher.objects.filter(subject__name__contains=&apos;全栈&apos;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明1&lt;/strong&gt;：由于老师与学科之间存在多对一外键关联，所以能通过学科反向查询到该学科的老师（从一对多关系中“一”的一方查询“多”的一方），反向查询属性默认的名字是&lt;code&gt;类名小写_set&lt;/code&gt;（如上面例子中的&lt;code&gt;teacher_set&lt;/code&gt;），当然也可以在创建模型时通过&lt;code&gt;ForeingKey&lt;/code&gt;的&lt;code&gt;related_name&lt;/code&gt;属性指定反向查询属性的名字。如果不希望执行反向查询可以将&lt;code&gt;related_name&lt;/code&gt;属性设置为&lt;code&gt;&apos;+&apos;&lt;/code&gt;或者以&lt;code&gt;&apos;+&apos;&lt;/code&gt;开头的字符串。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明2&lt;/strong&gt;：ORM查询多个对象时会返回QuerySet对象，QuerySet使用了惰性查询，即在创建QuerySet对象的过程中不涉及任何数据库活动，等真正用到对象时（对QuerySet求值）才向数据库发送SQL语句并获取对应的结果，这一点在实际开发中需要引起注意！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明3&lt;/strong&gt;：如果希望更新多条数据，不用先逐一获取模型对象再修改对象属性，可以直接使用QuerySet对象的&lt;code&gt;update()&lt;/code&gt;方法一次性更新多条数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;利用Django后台管理模型&lt;/h4&gt;
&lt;p&gt;在创建好模型类之后，可以通过Django框架自带的后台管理应用（&lt;code&gt;admin&lt;/code&gt;应用）实现对模型的管理。虽然实际应用中，这个后台可能并不能满足我们的需求，但是在学习Django框架时，我们可以利用&lt;code&gt;admin&lt;/code&gt;应用来管理我们的模型，同时也通过它来了解一个项目的后台管理系统需要哪些功能。使用Django自带的&lt;code&gt;admin&lt;/code&gt;应用步骤如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将&lt;code&gt;admin&lt;/code&gt;应用所需的表迁移到数据库中。&lt;code&gt;admin&lt;/code&gt;应用本身也需要数据库的支持，而且在&lt;code&gt;admin&lt;/code&gt;应用中已经定义好了相关的数据模型类，我们只需要通过模型迁移操作就能自动在数据库中生成所需的二维表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建访问&lt;code&gt;admin&lt;/code&gt;应用的超级用户账号，这里需要输入用户名、邮箱和口令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py createsuperuser
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：输入口令时没有回显也不能退格，需要一气呵成完成输入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行项目，在浏览器中访问&lt;code&gt;http://127.0.0.1:8000/admin&lt;/code&gt;，输入刚才创建的超级用户账号和密码进行登录。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;（原图缺失：django-admin-login.png，请到原仓库查看）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;登录后进入管理员操作平台。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/ca98126fe0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意，我们暂时还没能在&lt;code&gt;admin&lt;/code&gt;应用中看到之前创建的模型类，为此需要在&lt;code&gt;polls&lt;/code&gt;应用的&lt;code&gt;admin.py&lt;/code&gt;文件中对需要管理的模型进行注册。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册模型类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin

from polls.models import Subject, Teacher

admin.site.register(Subject)
admin.site.register(Teacher)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注册模型类后，就可以在后台管理系统中看到它们。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/8d6e8f405d.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对模型进行CRUD操作。&lt;/p&gt;
&lt;p&gt;可以在管理员平台对模型进行C（新增）、R（查看）、U（更新）、D（删除）操作，如下图所示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;添加学科。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/26ce69651e.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看所有学科。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/917e75a5c4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除和更新学科。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/7f3599ab71.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册模型管理类。&lt;/p&gt;
&lt;p&gt;可能大家已经注意到了，刚才在后台查看部门信息的时候，显示的部门信息并不直观，为此我们再修改&lt;code&gt;admin.py&lt;/code&gt;文件，通过注册模型管理类，可以在后台管理系统中更好的管理模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin

from polls.models import Subject, Teacher


class SubjectModelAdmin(admin.ModelAdmin):
    list_display = (&apos;no&apos;, &apos;name&apos;, &apos;intro&apos;, &apos;is_hot&apos;)
    search_fields = (&apos;name&apos;, )
    ordering = (&apos;no&apos;, )


class TeacherModelAdmin(admin.ModelAdmin):
    list_display = (&apos;no&apos;, &apos;name&apos;, &apos;sex&apos;, &apos;birth&apos;, &apos;good_count&apos;, &apos;bad_count&apos;, &apos;subject&apos;)
    search_fields = (&apos;name&apos;, )
    ordering = (&apos;no&apos;, )


admin.site.register(Subject, SubjectModelAdmin)
admin.site.register(Teacher, TeacherModelAdmin)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/9192baa89c.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/f0bf1635a5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了更好的查看模型，我们为&lt;code&gt;Subject&lt;/code&gt;类添加&lt;code&gt;__str__&lt;/code&gt;魔法方法，并在该方法中返回学科名字。这样在如上图所示的查看老师的页面上显示老师所属学科时，就不再是&lt;code&gt;Subject object(1)&lt;/code&gt;这样晦涩的信息，而是学科的名称。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;实现学科页和老师页效果&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;polls/views.py&lt;/code&gt;文件，编写视图函数实现对学科页和老师页的渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.shortcuts import render, redirect

from polls.models import Subject, Teacher


def show_subjects(request):
    subjects = Subject.objects.all().order_by(&apos;no&apos;)
    return render(request, &apos;subjects.html&apos;, {&apos;subjects&apos;: subjects})


def show_teachers(request):
    try:
        sno = int(request.GET.get(&apos;sno&apos;))
        teachers = []
        if sno:
            subject = Subject.objects.only(&apos;name&apos;).get(no=sno)
            teachers = Teacher.objects.filter(subject=subject).order_by(&apos;no&apos;)
        return render(request, &apos;teachers.html&apos;, {
            &apos;subject&apos;: subject,
            &apos;teachers&apos;: teachers
        })
    except (ValueError, Subject.DoesNotExist):
        return redirect(&apos;/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;templates/subjects.html&lt;/code&gt;和&lt;code&gt;templates/teachers.html&lt;/code&gt;模板页。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;subjects.html&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
   &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
   &amp;lt;title&amp;gt;学科信息&amp;lt;/title&amp;gt;
   &amp;lt;style&amp;gt;
       #container {
           width: 80%;
           margin: 10px auto;
       }
       .user {
           float: right;
           margin-right: 10px;
       }
       .user&amp;gt;a {
           margin-right: 10px;
       }
       #main&amp;gt;dl&amp;gt;dt {
           font-size: 1.5em;
           font-weight: bold;
       }
       #main&amp;gt;dl&amp;gt;dd {
           font-size: 1.2em;
       }
       a {
           text-decoration: none;
           color: darkcyan;
       }
   &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
   &amp;lt;div id=&quot;container&quot;&amp;gt;
       &amp;lt;div class=&quot;user&quot;&amp;gt;
           &amp;lt;a href=&quot;login.html&quot;&amp;gt;用户登录&amp;lt;/a&amp;gt;
           &amp;lt;a href=&quot;register.html&quot;&amp;gt;快速注册&amp;lt;/a&amp;gt;
       &amp;lt;/div&amp;gt;
       &amp;lt;h1&amp;gt;扣丁学堂所有学科&amp;lt;/h1&amp;gt;
       &amp;lt;hr&amp;gt;
       &amp;lt;div id=&quot;main&quot;&amp;gt;
           {% for subject in subjects %}
           &amp;lt;dl&amp;gt;
               &amp;lt;dt&amp;gt;
                   &amp;lt;a href=&quot;/teachers/?sno={{ subject.no }}&quot;&amp;gt;{{ subject.name }}&amp;lt;/a&amp;gt;
                   {% if subject.is_hot %}
                   &amp;lt;img src=&quot;/static/images/hot-icon-small.png&quot;&amp;gt;
                   {% endif %}
               &amp;lt;/dt&amp;gt;
               &amp;lt;dd&amp;gt;{{ subject.intro }}&amp;lt;/dd&amp;gt;
           &amp;lt;/dl&amp;gt;
           {% endfor %}
       &amp;lt;/div&amp;gt;
   &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;teachers.html&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;老师信息&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        #container {
            width: 80%;
            margin: 10px auto;
        }
        .teacher {
            width: 100%;
            margin: 0 auto;
            padding: 10px 0;
            border-bottom: 1px dashed gray;
            overflow: auto;
        }
        .teacher&amp;gt;div {
            float: left;
        }
        .photo {
            height: 140px;
            border-radius: 75px;
            overflow: hidden;
            margin-left: 20px;
        }
        .info {
            width: 75%;
            margin-left: 30px;
        }
        .info div {
            clear: both;
            margin: 5px 10px;
        }
        .info span {
            margin-right: 25px;
        }
        .info a {
            text-decoration: none;
            color: darkcyan;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
        &amp;lt;h1&amp;gt;{{ subject.name }}学科的老师信息&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        {% if not teachers %}
            &amp;lt;h2&amp;gt;暂无该学科老师信息&amp;lt;/h2&amp;gt;
        {% endif %}
        {% for teacher in teachers %}
        &amp;lt;div class=&quot;teacher&quot;&amp;gt;
            &amp;lt;div class=&quot;photo&quot;&amp;gt;
                &amp;lt;img src=&quot;/static/images/{{ teacher.photo }}&quot; height=&quot;140&quot; alt=&quot;&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;info&quot;&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;span&amp;gt;&amp;lt;strong&amp;gt;姓名：{{ teacher.name }}&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;性别：{{ teacher.sex | yesno:&apos;男,女&apos; }}&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;出生日期：{{ teacher.birth | date:&apos;Y年n月j日&apos;}}&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;intro&quot;&amp;gt;{{ teacher.intro }}&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;comment&quot;&amp;gt;
                    &amp;lt;a href=&quot;&quot;&amp;gt;好评&amp;lt;/a&amp;gt;&amp;amp;nbsp;(&amp;lt;strong&amp;gt;{{ teacher.good_count }}&amp;lt;/strong&amp;gt;)
                    &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;
                    &amp;lt;a href=&quot;&quot;&amp;gt;差评&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;lt;strong&amp;gt;{{ teacher.bad_count }}&amp;lt;/strong&amp;gt;)
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        {% endfor %}
        &amp;lt;a href=&quot;/&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;vote/urls.py&lt;/code&gt;文件，实现映射URL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path

from polls.views import show_subjects, show_teachers

urlpatterns = [
    path(&apos;admin/&apos;, admin.site.urls),
    path(&apos;&apos;, show_subjects),
    path(&apos;teachers/&apos;, show_teachers),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到此为止，页面上需要的图片（静态资源）还没有能够正常展示，我们在下一章节中为大家介绍如何处理模板页上的需要的静态资源。&lt;/p&gt;
&lt;h4&gt;补充内容&lt;/h4&gt;
&lt;h5&gt;Django模型最佳实践&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;正确的为模型和关系字段命名。&lt;/li&gt;
&lt;li&gt;设置适当的&lt;code&gt;related_name&lt;/code&gt;属性。&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;OneToOneField&lt;/code&gt;代替&lt;code&gt;ForeignKeyField(unique=True)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过“迁移操作”（migrate）来添加模型。&lt;/li&gt;
&lt;li&gt;用NoSQL来应对需要降低范式级别的场景。&lt;/li&gt;
&lt;li&gt;如果布尔类型可以为空要使用&lt;code&gt;NullBooleanField&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在模型中放置业务逻辑。&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;&amp;lt;ModelName&amp;gt;.DoesNotExists&lt;/code&gt;取代&lt;code&gt;ObjectDoesNotExists&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在数据库中不要出现无效数据。&lt;/li&gt;
&lt;li&gt;不要对&lt;code&gt;QuerySet&lt;/code&gt;调用&lt;code&gt;len()&lt;/code&gt;函数。&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;QuerySet&lt;/code&gt;的&lt;code&gt;exists()&lt;/code&gt;方法的返回值用于&lt;code&gt;if&lt;/code&gt;条件。&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;DecimalField&lt;/code&gt;来存储货币相关数据而不是&lt;code&gt;FloatField&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;定义&lt;code&gt;__str__&lt;/code&gt;方法。&lt;/li&gt;
&lt;li&gt;不要将数据文件放在同一个目录中。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：以上内容来自于STEELKIWI网站的&lt;a href=&quot;https://steelkiwi.com/blog/best-practices-working-django-models-python/&quot;&gt;&lt;em&gt;Best Practice working with Django models in Python&lt;/em&gt;&lt;/a&gt;，有兴趣的小伙伴可以阅读原文。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;模型定义参考&lt;/h5&gt;
&lt;h6&gt;字段&lt;/h6&gt;
&lt;p&gt;对字段名称的限制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字段名不能是Python的保留字，否则会导致语法错误&lt;/li&gt;
&lt;li&gt;字段名不能有多个连续下划线，否则影响ORM查询操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Django模型字段类&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段类&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AutoField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自增ID字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BigIntegerField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64位有符号整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BinaryField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储二进制数据的字段，对应Python的&lt;code&gt;bytes&lt;/code&gt;类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BooleanField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储&lt;code&gt;True&lt;/code&gt;或&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CharField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;长度较小的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DateField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储日期，有&lt;code&gt;auto_now&lt;/code&gt;和&lt;code&gt;auto_now_add&lt;/code&gt;属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DateTimeField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储日期和日期，两个附加属性同上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DecimalField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储固定精度小数，有&lt;code&gt;max_digits&lt;/code&gt;（有效位数）和&lt;code&gt;decimal_places&lt;/code&gt;（小数点后面）两个必要的参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DurationField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储时间跨度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EmailField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;与&lt;code&gt;CharField&lt;/code&gt;相同，可以用&lt;code&gt;EmailValidator&lt;/code&gt;验证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件上传字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FloatField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImageField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;其他同&lt;code&gt;FileFiled&lt;/code&gt;，要验证上传的是不是有效图像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IntegerField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储32位有符号整数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GenericIPAddressField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储IPv4或IPv6地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NullBooleanField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储&lt;code&gt;True&lt;/code&gt;、&lt;code&gt;False&lt;/code&gt;或&lt;code&gt;null&lt;/code&gt;值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PositiveIntegerField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储无符号整数（只能存储正数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SlugField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储slug（简短标注）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SmallIntegerField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储16位有符号整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TextField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储数据量较大的文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TimeField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URLField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储URL的&lt;code&gt;CharField&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UUIDField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储全局唯一标识符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h6&gt;字段属性&lt;/h6&gt;
&lt;p&gt;通用字段属性&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据库中对应的字段是否允许为&lt;code&gt;NULL&lt;/code&gt;，默认为&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后台模型管理验证数据时，是否允许为&lt;code&gt;NULL&lt;/code&gt;，默认为&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;choices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设定字段的选项，各元组中的第一个值是设置在模型上的值，第二值是人类可读的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_column&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字段对应到数据库表中的列名，未指定时直接使用字段的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置为&lt;code&gt;True&lt;/code&gt;时将在该字段创建索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_tablespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为有索引的字段设置使用的表空间，默认为&lt;code&gt;DEFAULT_INDEX_TABLESPACE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字段的默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;editable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字段在后台模型管理或&lt;code&gt;ModelForm&lt;/code&gt;中是否显示，默认为&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error_messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设定字段抛出异常时的默认消息的字典，其中的键包括&lt;code&gt;null&lt;/code&gt;、&lt;code&gt;blank&lt;/code&gt;、&lt;code&gt;invalid&lt;/code&gt;、&lt;code&gt;invalid_choice&lt;/code&gt;、&lt;code&gt;unique&lt;/code&gt;和&lt;code&gt;unique_for_date&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;help_text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;表单小组件旁边显示的额外的帮助文本。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;primary_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将字段指定为模型的主键，未指定时会自动添加&lt;code&gt;AutoField&lt;/code&gt;用于主键，只读。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置为&lt;code&gt;True&lt;/code&gt;时，表中字段的值必须是唯一的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verbose_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字段在后台模型管理显示的名称，未指定时使用字段的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;ForeignKey&lt;/code&gt;属性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;limit_choices_to&lt;/code&gt;：值是一个Q对象或返回一个Q对象，用于限制后台显示哪些对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;related_name&lt;/code&gt;：用于获取关联对象的关联管理器对象（反向查询），如果不允许反向，该属性应该被设置为&lt;code&gt;&apos;+&apos;&lt;/code&gt;，或者以&lt;code&gt;&apos;+&apos;&lt;/code&gt;结尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to_field&lt;/code&gt;：指定关联的字段，默认关联对象的主键字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db_constraint&lt;/code&gt;：是否为外键创建约束，默认值为&lt;code&gt;True&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;on_delete&lt;/code&gt;：外键关联的对象被删除时对应的动作，可取的值包括&lt;code&gt;django.db.models&lt;/code&gt;中定义的：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CASCADE&lt;/code&gt;：级联删除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROTECT&lt;/code&gt;：抛出&lt;code&gt;ProtectedError&lt;/code&gt;异常，阻止删除引用的对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET_NULL&lt;/code&gt;：把外键设置为&lt;code&gt;null&lt;/code&gt;，当&lt;code&gt;null&lt;/code&gt;属性被设置为&lt;code&gt;True&lt;/code&gt;时才能这么做。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET_DEFAULT&lt;/code&gt;：把外键设置为默认值，提供了默认值才能这么做。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;ManyToManyField&lt;/code&gt;属性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;symmetrical&lt;/code&gt;：是否建立对称的多对多关系。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;through&lt;/code&gt;：指定维持多对多关系的中间表的Django模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;throughfields&lt;/code&gt;：定义了中间模型时可以指定建立多对多关系的字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db_table&lt;/code&gt;：指定维持多对多关系的中间表的表名。&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;模型元数据选项&lt;/h6&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abstract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置为True时模型是抽象父类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app_label&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果定义模型的应用不在INSTALLED_APPS中可以用该属性指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模型使用的数据表名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_tablespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模型使用的数据表空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default_related_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关联对象回指这个模型时默认使用的名称，默认为&amp;lt;model_name&amp;gt;_set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_latest_by&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;模型中可排序字段的名称。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;managed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置为True时，Django在迁移中创建数据表并在执行flush管理命令时把表移除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order_with_respect_to&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标记对象为可排序的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ordering&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对象的默认排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;permissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建对象时写入权限表的额外权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default_permissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认为&lt;code&gt;(&apos;add&apos;, &apos;change&apos;, &apos;delete&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unique_together&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设定组合在一起时必须独一无二的字段名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;index_together&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设定一起建立索引的多个字段名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verbose_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为对象设定人类可读的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verbose_name_plural&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设定对象的复数名称&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;查询参考&lt;/h5&gt;
&lt;h6&gt;按字段查找可以用的条件&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;exact&lt;/code&gt; / &lt;code&gt;iexact&lt;/code&gt;：精确匹配/忽略大小写的精确匹配查询&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains&lt;/code&gt; / &lt;code&gt;icontains&lt;/code&gt; / &lt;code&gt;startswith&lt;/code&gt; / &lt;code&gt;istartswith&lt;/code&gt; / &lt;code&gt;endswith&lt;/code&gt; / &lt;code&gt;iendswith&lt;/code&gt;：基于&lt;code&gt;like&lt;/code&gt;的模糊查询&lt;/li&gt;
&lt;li&gt;&lt;code&gt;in&lt;/code&gt; ：集合运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gt&lt;/code&gt; / &lt;code&gt;gte&lt;/code&gt; / &lt;code&gt;lt&lt;/code&gt; / &lt;code&gt;lte&lt;/code&gt;：大于/大于等于/小于/小于等于关系运算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range&lt;/code&gt;：指定范围查询（SQL中的&lt;code&gt;between…and…&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;year&lt;/code&gt; / &lt;code&gt;month&lt;/code&gt; / &lt;code&gt;day&lt;/code&gt; / &lt;code&gt;week_day&lt;/code&gt; / &lt;code&gt;hour&lt;/code&gt; / &lt;code&gt;minute&lt;/code&gt; / &lt;code&gt;second&lt;/code&gt;：查询时间日期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isnull&lt;/code&gt;：查询空值（True）或非空值（False）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search&lt;/code&gt;：基于全文索引的全文检索（一般很少使用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;regex&lt;/code&gt; / &lt;code&gt;iregex&lt;/code&gt;：基于正则表达式的模糊匹配查询&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;48 静态资源和Ajax请求&lt;/h2&gt;
&lt;h4&gt;加载静态资源&lt;/h4&gt;
&lt;p&gt;如果要在Django项目中使用静态资源，可以先创建一个用于保存静态资源的目录。在&lt;code&gt;vote&lt;/code&gt;项目中，我们将静态资源置于名为&lt;code&gt;static&lt;/code&gt;的文件夹中，在该文件夹包含了三个子文件夹：css、js和images，分别用来保存外部CSS文件、外部JavaScript文件和图片资源，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/8dbbb17995.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了能够找到保存静态资源的文件夹，我们还需要修改Django项目的配置文件&lt;code&gt;settings.py&lt;/code&gt;，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;STATICFILES_DIRS = [os.path.join(BASE_DIR, &apos;static&apos;), ]
STATIC_URL = &apos;/static/&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置好静态资源之后，大家可以运行项目，然后看看之前我们写的页面上的图片是否能够正常加载出来。需要说明的是，在项目正式部署到线上环境后，我们通常会把静态资源交给专门的静态资源服务器（如Nginx、Apache）来处理，而不是有运行Python代码的服务器来管理静态资源，所以上面的配置并不适用于生产环境，仅供项目开发阶段测试使用。使用静态资源的正确姿势我们会在后续的章节为大家讲解。&lt;/p&gt;
&lt;h4&gt;Ajax概述&lt;/h4&gt;
&lt;p&gt;接下来就可以实现“好评”和“差评”的功能了，很明显如果能够在不刷新页面的情况下实现这两个功能会带来更好的用户体验，因此我们考虑使用&lt;a href=&quot;https://zh.wikipedia.org/wiki/AJAX&quot;&gt;Ajax&lt;/a&gt;技术来实现“好评”和“差评”。Ajax是Asynchronous Javascript And XML的缩写 , 简单的说，使用Ajax技术可以在不重新加载整个页面的情况下对页面进行局部刷新。&lt;/p&gt;
&lt;p&gt;对于传统的Web应用，每次页面上需要加载新的内容都需要重新请求服务器并刷新整个页面，如果服务器短时间内无法给予响应或者网络状况并不理想，那么可能会造成浏览器长时间的空白并使得用户处于等待状态，在这个期间用户什么都做不了，如下图所示。很显然，这样的Web应用并不能带来很好的用户体验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/9da4a8e92b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于使用Ajax技术的Web应用，浏览器可以向服务器发起异步请求来获取数据。异步请求不会中断用户体验，当服务器返回了新的数据，我们可以通过JavaScript代码进行DOM操作来实现对页面的局部刷新，这样就相当于在不刷新整个页面的情况下更新了页面的内容，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/623c9fb7c2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在使用Ajax技术时，浏览器跟服务器通常会交换XML或JSON格式的数据，XML是以前使用得非常多的一种数据格式，近年来几乎已经完全被JSON取代，下面是两种数据格式的对比。&lt;/p&gt;
&lt;p&gt;XML格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;message&amp;gt;
	&amp;lt;from&amp;gt;Alice&amp;lt;/from&amp;gt;
    &amp;lt;to&amp;gt;Bob&amp;lt;/to&amp;gt;
    &amp;lt;content&amp;gt;Dinner is on me!&amp;lt;/content&amp;gt;
&amp;lt;/message&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;from&quot;: &quot;Alice&quot;,
    &quot;to&quot;: &quot;Bob&quot;,
    &quot;content&quot;: &quot;Dinner is on me!&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的对比，明显JSON格式的数据要紧凑得多，所以传输效率更高，而且JSON本身也是JavaScript中的一种对象表达式语法，在JavaScript代码中处理JSON格式的数据更加方便。&lt;/p&gt;
&lt;h4&gt;用Ajax实现投票功能&lt;/h4&gt;
&lt;p&gt;下面，我们使用Ajax技术来实现投票的功能，首先修改项目的&lt;code&gt;urls.py&lt;/code&gt;文件，为“好评”和“差评”功能映射对应的URL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path

from polls import views

urlpatterns = [
    path(&apos;&apos;, views.show_subjects),
    path(&apos;teachers/&apos;, views.show_teachers),
    path(&apos;praise/&apos;, views.praise_or_criticize),
    path(&apos;criticize/&apos;, views.praise_or_criticize),
    path(&apos;admin/&apos;, admin.site.urls),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设计视图函数&lt;code&gt;praise_or_criticize&lt;/code&gt;来支持“好评”和“差评”功能，该视图函数通过Django封装的JsonResponse类将字典序列化成JSON字符串作为返回给浏览器的响应内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def praise_or_criticize(request):
    &quot;&quot;&quot;好评&quot;&quot;&quot;
    try:
        tno = int(request.GET.get(&apos;tno&apos;))
        teacher = Teacher.objects.get(no=tno)
        if request.path.startswith(&apos;/praise&apos;):
            teacher.good_count += 1
            count = teacher.good_count
        else:
            teacher.bad_count += 1
            count = teacher.bad_count
        teacher.save()
        data = {&apos;code&apos;: 20000, &apos;mesg&apos;: &apos;操作成功&apos;, &apos;count&apos;: count}
    except (ValueError, Teacher.DoseNotExist):
        data = {&apos;code&apos;: 20001, &apos;mesg&apos;: &apos;操作失败&apos;}
    return JsonResponse(data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改显示老师信息的模板页，引入jQuery库来实现事件处理、Ajax请求和DOM操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;老师信息&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        #container {
            width: 80%;
            margin: 10px auto;
        }
        .teacher {
            width: 100%;
            margin: 0 auto;
            padding: 10px 0;
            border-bottom: 1px dashed gray;
            overflow: auto;
        }
        .teacher&amp;gt;div {
            float: left;
        }
        .photo {
            height: 140px;
            border-radius: 75px;
            overflow: hidden;
            margin-left: 20px;
        }
        .info {
            width: 75%;
            margin-left: 30px;
        }
        .info div {
            clear: both;
            margin: 5px 10px;
        }
        .info span {
            margin-right: 25px;
        }
        .info a {
            text-decoration: none;
            color: darkcyan;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
        &amp;lt;h1&amp;gt;{{ subject.name }}学科的老师信息&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        {% if not teachers %}
            &amp;lt;h2&amp;gt;暂无该学科老师信息&amp;lt;/h2&amp;gt;
        {% endif %}
        {% for teacher in teachers %}
        &amp;lt;div class=&quot;teacher&quot;&amp;gt;
            &amp;lt;div class=&quot;photo&quot;&amp;gt;
                &amp;lt;img src=&quot;/static/images/{{ teacher.photo }}&quot; height=&quot;140&quot; alt=&quot;&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;info&quot;&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;span&amp;gt;&amp;lt;strong&amp;gt;姓名：{{ teacher.name }}&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;性别：{{ teacher.sex | yesno:&apos;男,女&apos; }}&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;出生日期：{{ teacher.birth }}&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;intro&quot;&amp;gt;{{ teacher.intro }}&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;comment&quot;&amp;gt;
                    &amp;lt;a href=&quot;/praise/?tno={{ teacher.no }}&quot;&amp;gt;好评&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
                    (&amp;lt;strong&amp;gt;{{ teacher.good_count }}&amp;lt;/strong&amp;gt;)
                    &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;
                    &amp;lt;a href=&quot;/criticize/?tno={{ teacher.no }}&quot;&amp;gt;差评&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
                    (&amp;lt;strong&amp;gt;{{ teacher.bad_count }}&amp;lt;/strong&amp;gt;)
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        {% endfor %}
        &amp;lt;a href=&quot;/&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        $(() =&amp;gt; {
            $(&apos;.comment&amp;gt;a&apos;).on(&apos;click&apos;, (evt) =&amp;gt; {
                evt.preventDefault()
                let url = $(evt.target).attr(&apos;href&apos;)
                $.getJSON(url, (json) =&amp;gt; {
                    if (json.code == 20000) {
                        $(evt.target).next().text(json.count)
                    } else {
                        alert(json.mesg)
                    }
                })
            })
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的前端代码中，使用了jQuery库封装的&lt;code&gt;getJSON&lt;/code&gt;方法向服务器发送异步请求，如果不熟悉前端的jQuery库，可以参考&lt;a href=&quot;https://www.runoob.com/manual/jquery/&quot;&gt;《jQuery API手册》&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;小结&lt;/h4&gt;
&lt;p&gt;到此为止，这个投票项目的核心功能已然完成，在下面的章节中我们会要求用户必须登录才能投票，没有账号的用户可以通过注册功能注册一个账号。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;49 Cookie和Session&lt;/h2&gt;
&lt;p&gt;我们继续来完成上一章节中的项目，实现“用户登录”的功能，并限制只有登录的用户才能投票。&lt;/p&gt;
&lt;h4&gt;用户登录的准备工作&lt;/h4&gt;
&lt;p&gt;我们先为实现用户登录做一些准备工作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建用户模型。之前我们讲解过如果通过Django的ORM实现从二维表到模型的转换（反向工程），这次我们尝试把模型变成二维表（正向工程）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User(models.Model):
    &quot;&quot;&quot;用户&quot;&quot;&quot;
    no = models.AutoField(primary_key=True, verbose_name=&apos;编号&apos;)
    username = models.CharField(max_length=20, unique=True, verbose_name=&apos;用户名&apos;)
    password = models.CharField(max_length=32, verbose_name=&apos;密码&apos;)
    tel = models.CharField(max_length=20, verbose_name=&apos;手机号&apos;)
    reg_date = models.DateTimeField(auto_now_add=True, verbose_name=&apos;注册时间&apos;)
    last_visit = models.DateTimeField(null=True, verbose_name=&apos;最后登录时间&apos;)

    class Meta:
        db_table = &apos;tb_user&apos;
        verbose_name = &apos;用户&apos;
        verbose_name_plural = &apos;用户&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用下面的命令生成迁移文件并执行迁移，将&lt;code&gt;User&lt;/code&gt;模型直接变成关系型数据库中的二维表&lt;code&gt;tb_user&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py makemigrations polls
python manage.py migrate polls
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用下面的SQL语句直接插入两条测试数据，通常不能将用户的密码直接保存在数据库中，因此我们将用户密码处理成对应的MD5摘要。MD5消息摘要算法是一种被广泛使用的密码哈希函数（散列函数），可以产生出一个128位（比特）的哈希值（散列值），用于确保信息传输完整一致。在使用哈希值时，通常会将哈希值表示为16进制字符串，因此128位的MD5摘要通常表示为32个十六进制符号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;insert into `tb_user`
    (`username`, `password`, `tel`, `reg_date`)
values
    (&apos;wangdachui&apos;, &apos;1c63129ae9db9c60c3e8aa94d3e00495&apos;, &apos;13122334455&apos;, now()),
    (&apos;hellokitty&apos;, &apos;c6f8cf68e5f68b0aa4680e089ee4742c&apos;, &apos;13890006789&apos;, now());
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面创建的两个用户&lt;code&gt;wangdachui&lt;/code&gt;和&lt;code&gt;hellokitty&lt;/code&gt;密码分别是&lt;code&gt;1qaz2wsx&lt;/code&gt;和&lt;code&gt;Abc123!!&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们在应用下增加一个名为&lt;code&gt;utils.py&lt;/code&gt;的模块用来保存需要使用的工具函数。Python标准库中的&lt;code&gt;hashlib&lt;/code&gt;模块封装了常用的哈希算法，包括：MD5、SHA1、SHA256等。下面是使用&lt;code&gt;hashlib&lt;/code&gt;中的&lt;code&gt;md5&lt;/code&gt;类将字符串处理成MD5摘要的函数如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import hashlib


def gen_md5_digest(content):
    return hashlib.md5(content.encode()).hexdigest()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写用户登录的视图函数和模板页。&lt;/p&gt;
&lt;p&gt;添加渲染登录页面的视图函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def login(request: HttpRequest) -&amp;gt; HttpResponse:
    hint = &apos;&apos;
    return render(request, &apos;login.html&apos;, {&apos;hint&apos;: hint})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;增加&lt;code&gt;login.html&lt;/code&gt;模板页：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;用户登录&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        #container {
            width: 520px;
            margin: 10px auto;
        }
        .input {
            margin: 20px 0;
            width: 460px;
            height: 40px;
        }
        .input&amp;gt;label {
            display: inline-block;
            width: 140px;
            text-align: right;
        }
        .input&amp;gt;img {
            width: 150px;
            vertical-align: middle;
        }
        input[name=captcha] {
            vertical-align: middle;
        }
        form+div {
            margin-top: 20px;
        }
        form+div&amp;gt;a {
            text-decoration: none;
            color: darkcyan;
            font-size: 1.2em;
        }
        .button {
            width: 500px;
            text-align: center;
            margin-top: 20px;
        }
        .hint {
            color: red;
            font-size: 12px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
        &amp;lt;h1&amp;gt;用户登录&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;p class=&quot;hint&quot;&amp;gt;{{ hint }}&amp;lt;/p&amp;gt;
        &amp;lt;form action=&quot;/login/&quot; method=&quot;post&quot;&amp;gt;
            {% csrf_token %}
            &amp;lt;fieldset&amp;gt;
                &amp;lt;legend&amp;gt;用户信息&amp;lt;/legend&amp;gt;
                &amp;lt;div class=&quot;input&quot;&amp;gt;
                    &amp;lt;label&amp;gt;用户名：&amp;lt;/label&amp;gt;
                    &amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;input&quot;&amp;gt;
                    &amp;lt;label&amp;gt;密码：&amp;lt;/label&amp;gt;
                    &amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;input&quot;&amp;gt;
                    &amp;lt;label&amp;gt;验证码：&amp;lt;/label&amp;gt;
                    &amp;lt;input type=&quot;text&quot; name=&quot;captcha&quot;&amp;gt;
                    &amp;lt;img id=&quot;code&quot; src=&quot;/captcha/&quot; alt=&quot;&quot; width=&quot;150&quot; height=&quot;40&quot;&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/fieldset&amp;gt;
            &amp;lt;div class=&quot;button&quot;&amp;gt;
                &amp;lt;input type=&quot;submit&quot; value=&quot;登录&quot;&amp;gt;
                &amp;lt;input type=&quot;reset&quot; value=&quot;重置&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/form&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;a href=&quot;/&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
            &amp;lt;a href=&quot;/register/&quot;&amp;gt;注册新用户&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，在上面的表单中，我们使用了模板指令&lt;code&gt;{% csrf_token %}&lt;/code&gt;为表单添加一个隐藏域（大家可以在浏览器中显示网页源代码就可以看到这个指令生成的&lt;code&gt;type&lt;/code&gt;属性为&lt;code&gt;hidden&lt;/code&gt;的&lt;code&gt;input&lt;/code&gt;标签），它的作用是在表单中生成一个随机令牌（token）来防范&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0%3E&quot;&gt;跨站请求伪造&lt;/a&gt;（简称为CSRF），这也是Django在提交表单时的硬性要求。如果我们的表单中没有这样的令牌，那么提交表单时，Django框架会产生一个响应状态码为&lt;code&gt;403&lt;/code&gt;的响应（禁止访问），除非我们设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/39d4d6482f.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，我们可以编写提供验证码和实现用户登录的视图函数，在此之前，我们先说说一个Web应用实现用户跟踪的方式以及Django框架对实现用户跟踪所提供的支持。对一个Web应用来说，用户登录成功后必然要让服务器能够记住该用户已经登录，这样服务器才能为这个用户提供更好的服务，而且上面说到的CSRF也是通过钓鱼网站来套取用户登录信息进行恶意操作的攻击手段，这些都是以用户跟踪技术为基础的。在理解了这些背景知识后，我们就清楚用户登录时到底需要执行哪些操作。&lt;/p&gt;
&lt;h4&gt;实现用户跟踪&lt;/h4&gt;
&lt;p&gt;如今，一个网站如果不通过某种方式记住你是谁以及你之前在网站的活动情况，失去的就是网站的可用性和便利性，继而很有可能导致网站用户的流式，所以记住一个用户（更专业的说法叫&lt;strong&gt;用户跟踪&lt;/strong&gt;）对绝大多数Web应用来说都是必需的功能。&lt;/p&gt;
&lt;p&gt;在服务器端，我们想记住一个用户最简单的办法就是创建一个对象，通过这个对象就可以把用户相关的信息都保存起来，这个对象就是我们常说的session（用户会话对象）。那么问题来了，HTTP本身是一个&lt;strong&gt;无连接&lt;/strong&gt;（每次请求和响应的过程中，服务器一旦完成对客户端请求的响应之后就断开连接）、&lt;strong&gt;无状态&lt;/strong&gt;（客户端再次发起对服务器的请求时，服务器无法得知这个客户端之前的任何信息）的协议，即便服务器通过session对象保留了用户数据，还得通过某种方式来确定当前的请求与之前保存过的哪一个session是有关联的。相信很多人都能想到，我们可以给每个session对象分配一个全局唯一的标识符来识别session对象，我们姑且称之为sessionid，每次客户端发起请求时，只要携带上这个sessionid，就有办法找到与之对应的session对象，从而实现在两次请求之间记住该用户的信息，也就是我们之前说的用户跟踪。&lt;/p&gt;
&lt;p&gt;要让客户端记住并在每次请求时带上sessionid又有以下几种做法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;URL重写。所谓URL重写就是在URL中携带sessionid，例如：&lt;code&gt;http://www.example.com/index.html?sessionid=123456&lt;/code&gt;，服务器通过获取sessionid参数的值来取到与之对应的session对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;隐藏域（隐式表单域）。在提交表单的时候，可以通过在表单中设置隐藏域向服务器发送额外的数据。例如：&lt;code&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;sessionid&quot; value=&quot;123456&quot;&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地存储。现在的浏览器都支持多种本地存储方案，包括：cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中，cookie是历史最为悠久也是被诟病得最多的一种方案，也是我们接下来首先为大家讲解的一种方案。简单的说，cookie是一种以键值对方式保存在浏览器临时文件中的数据，每次请求时，请求头中会携带本站点的cookie到服务器，那么只要将sessionid写入cookie，下次请求时服务器只要读取请求头中的cookie就能够获得这个sessionid，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/9d7e949216.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在HTML5时代要，除了cookie，还可以使用新的本地存储API来保存数据，就是刚才提到的localStorage、sessionStorage、IndexedDB等技术，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/f0f177b412.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结一下&lt;/strong&gt;，要实现用户跟踪，服务器端可以为每个用户会话创建一个session对象并将session对象的ID写入到浏览器的cookie中；用户下次请求服务器时，浏览器会在HTTP请求头中携带该网站保存的cookie信息，这样服务器就可以从cookie中找到session对象的ID并根据此ID获取到之前创建的session对象；由于session对象可以用键值对的方式保存用户数据，这样之前保存在session对象中的信息可以悉数取出，服务器也可以根据这些信息判定用户身份和了解用户偏好，为用户提供更好的个性化服务。&lt;/p&gt;
&lt;h4&gt;Django框架对session的支持&lt;/h4&gt;
&lt;p&gt;在创建Django项目时，默认的配置文件&lt;code&gt;settings.py&lt;/code&gt;文件中已经激活了一个名为&lt;code&gt;SessionMiddleware&lt;/code&gt;的中间件（关于中间件的知识我们在后面的章节做详细讲解，这里只需要知道它的存在即可），因为这个中间件的存在，我们可以直接通过请求对象的&lt;code&gt;session&lt;/code&gt;属性来操作会话对象。前面我们说过，&lt;code&gt;session&lt;/code&gt;属性是一个像字典一样可以读写数据的容器对象，因此我们可以使用“键值对”的方式来保留用户数据。与此同时，&lt;code&gt;SessionMiddleware&lt;/code&gt;中间件还封装了对cookie的操作，在cookie中保存了sessionid，这一点我们在上面已经提到过了。&lt;/p&gt;
&lt;p&gt;在默认情况下，Django将session的数据序列化后保存在关系型数据库中，在Django 1.6以后的版本中，默认的序列化数据的方式是JSON序列化，而在此之前一直使用Pickle序列化。JSON序列化和Pickle序列化的差别在于前者将对象序列化为字符串（字符形式），而后者将对象序列化为字节串（二进制形式），因为安全方面的原因，JSON序列化成为了目前Django框架默认序列化数据的方式，这就要求在我们保存在session中的数据必须是能够JSON序列化的，否则就会引发异常。还有一点需要说明的是，使用关系型数据库保存session中的数据在大多数时候并不是最好的选择，因为数据库可能会承受巨大的压力而成为系统性能的瓶颈，在后面的章节中我们会告诉大家如何将session保存到缓存服务中以提升系统的性能。&lt;/p&gt;
&lt;h4&gt;实现用户登录验证&lt;/h4&gt;
&lt;p&gt;首先，我们在刚才的&lt;code&gt;polls/utils.py&lt;/code&gt;文件中编写生成随机验证码的函数&lt;code&gt;gen_random_code&lt;/code&gt;，内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

ALL_CHARS = &apos;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&apos;


def gen_random_code(length=4):
    return &apos;&apos;.join(random.choices(ALL_CHARS, k=length))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写生成验证码图片的类&lt;code&gt;Captcha&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
图片验证码
&quot;&quot;&quot;
import os
import random
from io import BytesIO

from PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype


class Bezier:
    &quot;&quot;&quot;贝塞尔曲线&quot;&quot;&quot;

    def __init__(self):
        self.tsequence = tuple([t / 20.0 for t in range(21)])
        self.beziers = {}

    def make_bezier(self, n):
        &quot;&quot;&quot;绘制贝塞尔曲线&quot;&quot;&quot;
        try:
            return self.beziers[n]
        except KeyError:
            combinations = pascal_row(n - 1)
            result = []
            for t in self.tsequence:
                tpowers = (t ** i for i in range(n))
                upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
                coefs = [c * a * b for c, a, b in zip(combinations,
                                                      tpowers, upowers)]
                result.append(coefs)
            self.beziers[n] = result
            return result


class Captcha:
    &quot;&quot;&quot;验证码&quot;&quot;&quot;

    def __init__(self, width, height, fonts=None, color=None):
        self._image = None
        self._fonts = fonts if fonts else \
            [os.path.join(os.path.dirname(__file__), &apos;fonts&apos;, font)
             for font in [&apos;Arial.ttf&apos;, &apos;Georgia.ttf&apos;, &apos;Action.ttf&apos;]]
        self._color = color if color else random_color(0, 200, random.randint(220, 255))
        self._width, self._height = width, height

    @classmethod
    def instance(cls, width=200, height=75):
        &quot;&quot;&quot;用于获取Captcha对象的类方法&quot;&quot;&quot;
        prop_name = f&apos;_instance_{width}_{height}&apos;
        if not hasattr(cls, prop_name):
            setattr(cls, prop_name, cls(width, height))
        return getattr(cls, prop_name)

    def _background(self):
        &quot;&quot;&quot;绘制背景&quot;&quot;&quot;
        Draw(self._image).rectangle([(0, 0), self._image.size],
                                    fill=random_color(230, 255))

    def _smooth(self):
        &quot;&quot;&quot;平滑图像&quot;&quot;&quot;
        return self._image.filter(ImageFilter.SMOOTH)

    def _curve(self, width=4, number=6, color=None):
        &quot;&quot;&quot;绘制曲线&quot;&quot;&quot;
        dx, height = self._image.size
        dx /= number
        path = [(dx * i, random.randint(0, height))
                for i in range(1, number)]
        bcoefs = Bezier().make_bezier(number - 1)
        points = []
        for coefs in bcoefs:
            points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
                                for ps in zip(*path)))
        Draw(self._image).line(points, fill=color if color else self._color, width=width)

    def _noise(self, number=50, level=2, color=None):
        &quot;&quot;&quot;绘制扰码&quot;&quot;&quot;
        width, height = self._image.size
        dx, dy = width / 10, height / 10
        width, height = width - dx, height - dy
        draw = Draw(self._image)
        for i in range(number):
            x = int(random.uniform(dx, width))
            y = int(random.uniform(dy, height))
            draw.line(((x, y), (x + level, y)),
                      fill=color if color else self._color, width=level)

    def _text(self, captcha_text, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
        &quot;&quot;&quot;绘制文本&quot;&quot;&quot;
        color = color if color else self._color
        fonts = tuple([truetype(name, size)
                       for name in fonts
                       for size in font_sizes or (65, 70, 75)])
        draw = Draw(self._image)
        char_images = []
        for c in captcha_text:
            font = random.choice(fonts)
            c_width, c_height = draw.textsize(c, font=font)
            char_image = Image.new(&apos;RGB&apos;, (c_width, c_height), (0, 0, 0))
            char_draw = Draw(char_image)
            char_draw.text((0, 0), c, font=font, fill=color)
            char_image = char_image.crop(char_image.getbbox())
            for drawing in drawings:
                d = getattr(self, drawing)
                char_image = d(char_image)
            char_images.append(char_image)
        width, height = self._image.size
        offset = int((width - sum(int(i.size[0] * squeeze_factor)
                                  for i in char_images[:-1]) -
                      char_images[-1].size[0]) / 2)
        for char_image in char_images:
            c_width, c_height = char_image.size
            mask = char_image.convert(&apos;L&apos;).point(lambda i: i * 1.97)
            self._image.paste(char_image,
                              (offset, int((height - c_height) / 2)),
                              mask)
            offset += int(c_width * squeeze_factor)

    @staticmethod
    def _warp(image, dx_factor=0.3, dy_factor=0.3):
        &quot;&quot;&quot;图像扭曲&quot;&quot;&quot;
        width, height = image.size
        dx = width * dx_factor
        dy = height * dy_factor
        x1 = int(random.uniform(-dx, dx))
        y1 = int(random.uniform(-dy, dy))
        x2 = int(random.uniform(-dx, dx))
        y2 = int(random.uniform(-dy, dy))
        warp_image = Image.new(
            &apos;RGB&apos;,
            (width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))
        warp_image.paste(image, (abs(x1), abs(y1)))
        width2, height2 = warp_image.size
        return warp_image.transform(
            (width, height),
            Image.QUAD,
            (x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1))

    @staticmethod
    def _offset(image, dx_factor=0.1, dy_factor=0.2):
        &quot;&quot;&quot;图像偏移&quot;&quot;&quot;
        width, height = image.size
        dx = int(random.random() * width * dx_factor)
        dy = int(random.random() * height * dy_factor)
        offset_image = Image.new(&apos;RGB&apos;, (width + dx, height + dy))
        offset_image.paste(image, (dx, dy))
        return offset_image

    @staticmethod
    def _rotate(image, angle=25):
        &quot;&quot;&quot;图像旋转&quot;&quot;&quot;
        return image.rotate(random.uniform(-angle, angle),
                            Image.BILINEAR, expand=1)

    def generate(self, captcha_text=&apos;&apos;, fmt=&apos;PNG&apos;):
        &quot;&quot;&quot;生成验证码(文字和图片)
        :param captcha_text: 验证码文字
        :param fmt: 生成的验证码图片格式
        :return: 验证码图片的二进制数据
        &quot;&quot;&quot;
        self._image = Image.new(&apos;RGB&apos;, (self._width, self._height), (255, 255, 255))
        self._background()
        self._text(captcha_text, self._fonts,
                   drawings=[&apos;_warp&apos;, &apos;_rotate&apos;, &apos;_offset&apos;])
        self._curve()
        self._noise()
        self._smooth()
        image_bytes = BytesIO()
        self._image.save(image_bytes, format=fmt)
        return image_bytes.getvalue()


def pascal_row(n=0):
    &quot;&quot;&quot;生成毕达哥拉斯三角形（杨辉三角）&quot;&quot;&quot;
    result = [1]
    x, numerator = 1, n
    for denominator in range(1, n // 2 + 1):
        x *= numerator
        x /= denominator
        result.append(x)
        numerator -= 1
    if n &amp;amp; 1 == 0:
        result.extend(reversed(result[:-1]))
    else:
        result.extend(reversed(result))
    return result


def random_color(start=0, end=255, opacity=255):
    &quot;&quot;&quot;获得随机颜色&quot;&quot;&quot;
    red = random.randint(start, end)
    green = random.randint(start, end)
    blue = random.randint(start, end)
    if opacity is None:
        return red, green, blue
    return red, green, blue, opacity
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中用到了三个字体文件，字体文件位于&lt;code&gt;polls/fonts&lt;/code&gt;目录下，大家可以自行添加字体文件，但是需要注意字体文件的文件名跟上面代码的第45行保持一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来，我们先完成提供验证码的视图函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_captcha(request: HttpRequest) -&amp;gt; HttpResponse:
    &quot;&quot;&quot;验证码&quot;&quot;&quot;
    captcha_text = gen_random_code()
    request.session[&apos;captcha&apos;] = captcha_text
    image_data = Captcha.instance().generate(captcha_text)
    return HttpResponse(image_data, content_type=&apos;image/png&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面代码中的第4行，我们将随机生成的验证码字符串保存到session中，稍后用户登录时，我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对，如果用户输入了正确的验证码才能够执行后续的登录流程，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def login(request: HttpRequest) -&amp;gt; HttpResponse:
    hint = &apos;&apos;
    if request.method == &apos;POST&apos;:
        username = request.POST.get(&apos;username&apos;)
        password = request.POST.get(&apos;password&apos;)
        if username and password:
            password = gen_md5_digest(password)
            user = User.objects.filter(username=username, password=password).first()
            if user:
                request.session[&apos;userid&apos;] = user.no
                request.session[&apos;username&apos;] = user.username
                return redirect(&apos;/&apos;)
            else:
                hint = &apos;用户名或密码错误&apos;
        else:
            hint = &apos;请输入有效的用户名和密码&apos;
    return render(request, &apos;login.html&apos;, {&apos;hint&apos;: hint})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码没有对用户名和密码没有进行验证，实际项目中建议使用正则表达式验证用户输入信息，否则有可能将无效的数据交给数据库进行处理或者造成其他安全方面的隐患。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码中，我们设定了登录成功后会在session中保存用户的编号（&lt;code&gt;userid&lt;/code&gt;）和用户名（&lt;code&gt;username&lt;/code&gt;），页面会重定向到首页。接下来我们可以稍微对首页的代码进行调整，在页面的右上角显示出登录用户的用户名。我们将这段代码单独写成了一个名为header.html的HTML文件，首页中可以通过在&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;标签中添加&lt;code&gt;{% include &apos;header.html&apos; %}&lt;/code&gt;来包含这个页面，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;user&quot;&amp;gt;
    {% if request.session.userid %}
    &amp;lt;span&amp;gt;{{ request.session.username }}&amp;lt;/span&amp;gt;
    &amp;lt;a href=&quot;/logout&quot;&amp;gt;注销&amp;lt;/a&amp;gt;
    {% else %}
    &amp;lt;a href=&quot;/login&quot;&amp;gt;登录&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
    {% endif %}
    &amp;lt;a href=&quot;/register&quot;&amp;gt;注册&amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用户没有登录，页面会显示登录和注册的超链接；而用户登录成功后，页面上会显示用户名和注销的链接，注销链接对应的视图函数如下所示，URL的映射与之前讲过的类似，不再赘述。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def logout(request):
    &quot;&quot;&quot;注销&quot;&quot;&quot;
    request.session.flush()
    return redirect(&apos;/&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码通过session对象&lt;code&gt;flush&lt;/code&gt;方法来销毁session，一方面清除了服务器上session对象保存的用户数据，一方面将保存在浏览器cookie中的sessionid删除掉，稍后我们会对如何读写cookie的操作加以说明。&lt;/p&gt;
&lt;p&gt;我们可以通过项目使用的数据库中名为&lt;code&gt;django_session&lt;/code&gt; 的表来找到所有的session，该表的结构如下所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;session_key&lt;/th&gt;
&lt;th&gt;session_data&lt;/th&gt;
&lt;th&gt;expire_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;c9g2gt5cxo0k2evykgpejhic5ae7bfpl&lt;/td&gt;
&lt;td&gt;MmI4YzViYjJhOGMyMDJkY2M5Yzg3...&lt;/td&gt;
&lt;td&gt;2019-05-25 23:16:13.898522&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中，第1列就是浏览器cookie中保存的sessionid；第2列是经过BASE64编码后的session中的数据，如果使用Python的&lt;code&gt;base64&lt;/code&gt;对其进行解码，解码的过程和结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64

base64.b64decode(&apos;MmI4YzViYjJhOGMyMDJkY2M5Yzg3ZWIyZGViZmUzYmYxNzdlNDdmZjp7ImNhcHRjaGEiOiJzS3d0Iiwibm8iOjEsInVzZXJuYW1lIjoiamFja2ZydWVkIn0=&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第3列是session的过期时间，session过期后浏览器保存的cookie中的sessionid就会失效，但是数据库中的这条对应的记录仍然会存在，如果想清除过期的数据，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py clearsessions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django框架默认的session过期时间为两周（1209600秒），如果想修改这个时间，可以在项目的配置文件中添加如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置会话的超时时间为1天（86400秒）
SESSION_COOKIE_AGE = 86400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有很多对安全性要求较高的应用都必须在关闭浏览器窗口时让会话过期，不再保留用户的任何信息，如果希望在关闭浏览器窗口时就让会话过期（cookie中的sessionid失效），可以加入如下所示的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置为True在关闭浏览器窗口时session就过期
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不希望将session的数据保存在数据库中，可以将其放入缓存中，对应的配置如下所示，缓存的配置和使用我们在后面讲解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 配置将会话对象放到缓存中存储
SESSION_ENGINE = &apos;django.contrib.sessions.backends.cache&apos;
# 配置使用哪一组缓存来保存会话
SESSION_CACHE_ALIAS = &apos;default&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要修改session数据默认的序列化方式，可以将默认的&lt;code&gt;JSONSerializer&lt;/code&gt;修改为&lt;code&gt;PickleSerializer&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SESSION_SERIALIZER = &apos;django.contrib.sessions.serializers.PickleSerializer&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们就可以限制只有登录用户才能为老师投票，修改后的&lt;code&gt;praise_or_criticize&lt;/code&gt;函数如下所示，我们通过从&lt;code&gt;request.session&lt;/code&gt;中获取&lt;code&gt;userid&lt;/code&gt;来判定用户是否登录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def praise_or_criticize(request: HttpRequest) -&amp;gt; HttpResponse:
    if request.session.get(&apos;userid&apos;):
        try:
            tno = int(request.GET.get(&apos;tno&apos;))
            teacher = Teacher.objects.get(no=tno)
            if request.path.startswith(&apos;/praise/&apos;):
                teacher.good_count += 1
                count = teacher.good_count
            else:
                teacher.bad_count += 1
                count = teacher.bad_count
            teacher.save()
            data = {&apos;code&apos;: 20000, &apos;mesg&apos;: &apos;投票成功&apos;, &apos;count&apos;: count}
        except (ValueError, Teacher.DoesNotExist):
            data = {&apos;code&apos;: 20001, &apos;mesg&apos;: &apos;投票失败&apos;}
    else:
        data = {&apos;code&apos;: 20002, &apos;mesg&apos;: &apos;请先登录&apos;}
    return JsonResponse(data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，在修改了视图函数后，&lt;code&gt;teachers.html&lt;/code&gt;也需要进行调整，用户如果没有登录，就将用户引导至登录页，登录成功再返回到投票页，此处不再赘述。&lt;/p&gt;
&lt;h4&gt;在视图函数中读写cookie&lt;/h4&gt;
&lt;p&gt;下面我们对如何使用cookie做一个更为细致的说明以便帮助大家在Web项目中更好的使用这项技术。Django封装的&lt;code&gt;HttpRequest&lt;/code&gt;和&lt;code&gt;HttpResponse&lt;/code&gt;对象分别提供了读写cookie的操作。&lt;/p&gt;
&lt;p&gt;HttpRequest封装的属性和方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;COOKIES&lt;/code&gt;属性 - 该属性包含了HTTP请求携带的所有cookie。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_signed_cookie&lt;/code&gt;方法 - 获取带签名的cookie，如果签名验证失败，会产生&lt;code&gt;BadSignature&lt;/code&gt;异常。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;HttpResponse封装的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;set_cookie&lt;/code&gt;方法 - 该方法可以设置一组键值对并将其最终将写入浏览器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_signed_cookie&lt;/code&gt;方法 - 跟上面的方法作用相似，但是会对cookie进行签名来达到防篡改的作用。因为如果篡改了cookie中的数据，在不知道&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/zh.wikipedia.org/wiki/%E5%AF%86%E9%92%A5%3E&quot;&gt;密钥&lt;/a&gt;和&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6)%3E&quot;&gt;盐&lt;/a&gt;的情况下是无法生成有效的签名，这样服务器在读取cookie时会发现数据与签名不一致从而产生&lt;code&gt;BadSignature&lt;/code&gt;异常。需要说明的是，这里所说的密钥就是我们在Django项目配置文件中指定的&lt;code&gt;SECRET_KEY&lt;/code&gt;，而盐是程序中设定的一个字符串，你愿意设定为什么都可以，只要是一个有效的字符串。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面提到的方法，如果不清楚它们的具体用法，可以自己查阅一下Django的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/docs.djangoproject.com/en/2.1/ref/request-response/%3E&quot;&gt;官方文档&lt;/a&gt;，没有什么资料比官方文档能够更清楚的告诉你这些方法到底如何使用。&lt;/p&gt;
&lt;p&gt;刚才我们说过了，激活&lt;code&gt;SessionMiddleware&lt;/code&gt;之后，每个&lt;code&gt;HttpRequest&lt;/code&gt;对象都会绑定一个session属性，它是一个类似字典的对象，除了保存用户数据之外还提供了检测浏览器是否支持cookie的方法，包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;set_test_cookie&lt;/code&gt;方法 - 设置用于测试的cookie。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_cookie_worked&lt;/code&gt;方法 - 检测测试cookie是否工作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete_test_cookie&lt;/code&gt;方法 - 删除用于测试的cookie。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set_expiry&lt;/code&gt;方法 - 设置会话的过期时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_expire_age&lt;/code&gt;/&lt;code&gt;get_expire_date&lt;/code&gt;方法 - 获取会话的过期时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear_expired&lt;/code&gt;方法 - 清理过期的会话。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是在执行登录之前检查浏览器是否支持cookie的代码。通常情况下，浏览器默认开启了对cookie的支持，但是可能因为某种原因，用户禁用了浏览器的cookie功能，遇到这种情况我们可以在视图函数中提供一个检查功能，如果检查到用户浏览器不支持cookie，可以给出相应的提示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def login(request):
    if request.method == &apos;POST&apos;:
        if request.session.test_cookie_worked():
            request.session.delete_test_cookie()
            # Add your code to perform login process here
        else:
            return HttpResponse(&quot;Please enable cookies and try again.&quot;)
    request.session.set_test_cookie()
    return render_to_response(&apos;login.html&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Cookie的替代品&lt;/h4&gt;
&lt;p&gt;之前我们说过了，cookie的名声一直都不怎么好，当然我们在实际开发中是不会在cookie中保存用户的敏感信息（如用户的密码、信用卡的账号等）的，而且保存在cookie中的数据一般也会做好编码和签名的工作。对于支持HTML5的浏览器来说，可以使用localStorage和sessionStorage做为cookie的替代方案，相信从名字上你就能听出二者的差别，存储在&lt;code&gt;localStorage&lt;/code&gt;的数据可以长期保留；而存储在&lt;code&gt;sessionStorage&lt;/code&gt;的数据会在浏览器关闭时会被清除 。关于这些cookie替代品的用法，建议大家查阅&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/developer.mozilla.org/zh-CN/docs/Web%3E&quot;&gt;MDN&lt;/a&gt;来进行了解。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;50 制作报表&lt;/h2&gt;
&lt;h4&gt;导出Excel报表&lt;/h4&gt;
&lt;p&gt;报表就是用表格、图表等格式来动态显示数据，所以有人用这样的公式来描述报表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;报表 = 多样的格式 + 动态的数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有很多的三方库支持在Python程序中写Excel文件，包括&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/xlwt.readthedocs.io/en/latest/%3E&quot;&gt;&lt;code&gt;xlwt&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/docs.xlwings.org/en/latest/quickstart.html%3E&quot;&gt;&lt;code&gt;xlwings&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/openpyxl.readthedocs.io/en/latest/%3E&quot;&gt;&lt;code&gt;openpyxl&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/xlsxwriter.readthedocs.io/%3E&quot;&gt;&lt;code&gt;xlswriter&lt;/code&gt;&lt;/a&gt;等，其中的xlwt虽然只支持写xls格式的Excel文件，但在性能方面的表现还是不错的。下面我们就以&lt;code&gt;xlwt&lt;/code&gt;为例，来演示如何在Django项目中导出Excel报表。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;xlwt&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install xlwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出包含所有老师信息的Excel表格的视图函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def export_teachers_excel(request):
    # 创建工作簿
    wb = xlwt.Workbook()
    # 添加工作表
    sheet = wb.add_sheet(&apos;老师信息表&apos;)
    # 查询所有老师的信息
    queryset = Teacher.objects.all()
    # 向Excel表单中写入表头
    colnames = (&apos;姓名&apos;, &apos;介绍&apos;, &apos;好评数&apos;, &apos;差评数&apos;, &apos;学科&apos;)
    for index, name in enumerate(colnames):
        sheet.write(0, index, name)
    # 向单元格中写入老师的数据
    props = (&apos;name&apos;, &apos;detail&apos;, &apos;good_count&apos;, &apos;bad_count&apos;, &apos;subject&apos;)
    for row, teacher in enumerate(queryset):
        for col, prop in enumerate(props):
            value = getattr(teacher, prop, &apos;&apos;)
            if isinstance(value, Subject):
                value = value.name
            sheet.write(row + 1, col, value)
    # 保存Excel
    buffer = BytesIO()
    wb.save(buffer)
    # 将二进制数据写入响应的消息体中并设置MIME类型
    resp = HttpResponse(buffer.getvalue(), content_type=&apos;application/vnd.ms-excel&apos;)
    # 中文文件名需要处理成百分号编码
    filename = quote(&apos;老师.xls&apos;)
    # 通过响应头告知浏览器下载该文件以及对应的文件名
    resp[&apos;content-disposition&apos;] = f&apos;attachment; filename*=utf-8\&apos;\&apos;{filename}&apos;
    return resp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;映射URL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    
    path(&apos;excel/&apos;, views.export_teachers_excel),
    
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;导出PDF报表&lt;/h4&gt;
&lt;p&gt;在Django项目中，如果需要导出PDF报表，可以借助三方库&lt;code&gt;reportlab&lt;/code&gt;来生成PDF文件的内容，再将文件的二进制数据输出给浏览器并指定MIME类型为&lt;code&gt;application/pdf&lt;/code&gt;，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def export_pdf(request: HttpRequest) -&amp;gt; HttpResponse:
    buffer = io.BytesIO()
    pdf = canvas.Canvas(buffer)
    pdf.setFont(&quot;Helvetica&quot;, 80)
    pdf.setFillColorRGB(0.2, 0.5, 0.3)
    pdf.drawString(100, 550, &apos;hello, world!&apos;)
    pdf.showPage()
    pdf.save()
    resp = HttpResponse(buffer.getvalue(), content_type=&apos;application/pdf&apos;)
    resp[&apos;content-disposition&apos;] = &apos;inline; filename=&quot;demo.pdf&quot;&apos;
    return resp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于如何用&lt;code&gt;reportlab&lt;/code&gt;定制PDF报表的内容，可以参考reportlab的&lt;a href=&quot;https://www.reportlab.com/docs/reportlab-userguide.pdf&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;生成前端统计图表&lt;/h4&gt;
&lt;p&gt;如果项目中需要生成前端统计图表，可以使用百度的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/echarts.baidu.com/%3E&quot;&gt;ECharts&lt;/a&gt;。具体的做法是后端通过提供数据接口返回统计图表所需的数据，前端使用ECharts来渲染出柱状图、折线图、饼图、散点图等图表。例如我们要生成一个统计所有老师好评数和差评数的报表，可以按照下面的方式来做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_teachers_data(request):
    queryset = Teacher.objects.all()
    names = [teacher.name for teacher in queryset]
    good_counts = [teacher.good_count for teacher in queryset]
    bad_counts = [teacher.bad_count for teacher in queryset]
    return JsonResponse({&apos;names&apos;: names, &apos;good&apos;: good_counts, &apos;bad&apos;: bad_counts})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;映射URL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    path(&apos;teachers_data/&apos;, views.get_teachers_data),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用ECharts生成柱状图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;老师评价统计&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;main&quot; style=&quot;width: 600px; height: 400px&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;p&amp;gt;
        &amp;lt;a href=&quot;/&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
    &amp;lt;/p&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        var myChart = echarts.init(document.querySelector(&apos;#main&apos;))
        fetch(&apos;/teachers_data/&apos;)
            .then(resp =&amp;gt; resp.json())
            .then(json =&amp;gt; {
                var option = {
                    color: [&apos;#f00&apos;, &apos;#00f&apos;],
                    title: {
                        text: &apos;老师评价统计图&apos;
                    },
                    tooltip: {},
                    legend: {
                        data:[&apos;好评&apos;, &apos;差评&apos;]
                    },
                    xAxis: {
                        data: json.names
                    },
                    yAxis: {},
                    series: [
                        {
                            name: &apos;好评&apos;,
                            type: &apos;bar&apos;,
                            data: json.good
                        },
                        {
                            name: &apos;差评&apos;,
                            type: &apos;bar&apos;,
                            data: json.bad
                        }
                    ]
                }
                myChart.setOption(option)
            })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行效果如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/121f9b1969.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;51 日志和调试工具栏&lt;/h2&gt;
&lt;h4&gt;配置日志&lt;/h4&gt;
&lt;p&gt;项目开发阶段，显示足够的调试信息以辅助开发人员调试代码还是非常必要的；项目上线以后，将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是很有必要的。与此同时，采集日志数据也是为网站做数字化运营奠定一个基础，通过对系统运行日志的分析，我们可以监测网站的流量以及流量分布，同时还可以挖掘出用户的使用习惯和行为模式。&lt;/p&gt;
&lt;p&gt;接下来，我们先看看如何通过Django的配置文件来配置日志。Django的日志配置基本可以参照官方文档再结合项目实际需求来进行，这些内容基本上可以从官方文档上复制下来，然后进行局部的调整即可，下面给出一些参考配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOGGING = {
    &apos;version&apos;: 1,
    # 是否禁用已经存在的日志器
    &apos;disable_existing_loggers&apos;: False,
    # 日志格式化器
    &apos;formatters&apos;: {
        &apos;simple&apos;: {
            &apos;format&apos;: &apos;%(asctime)s %(module)s.%(funcName)s: %(message)s&apos;,
            &apos;datefmt&apos;: &apos;%Y-%m-%d %H:%M:%S&apos;,
        },
        &apos;verbose&apos;: {
            &apos;format&apos;: &apos;%(asctime)s %(levelname)s [%(process)d-%(threadName)s] &apos;
                      &apos;%(module)s.%(funcName)s line %(lineno)d: %(message)s&apos;,
            &apos;datefmt&apos;: &apos;%Y-%m-%d %H:%M:%S&apos;,
        }
    },
    # 日志过滤器
    &apos;filters&apos;: {
        # 只有在Django配置文件中DEBUG值为True时才起作用
        &apos;require_debug_true&apos;: {
            &apos;()&apos;: &apos;django.utils.log.RequireDebugTrue&apos;,
        },
    },
    # 日志处理器
    &apos;handlers&apos;: {
        # 输出到控制台
        &apos;console&apos;: {
            &apos;class&apos;: &apos;logging.StreamHandler&apos;,
            &apos;level&apos;: &apos;DEBUG&apos;,
            &apos;filters&apos;: [&apos;require_debug_true&apos;],
            &apos;formatter&apos;: &apos;simple&apos;,
        },
        # 输出到文件(每周切割一次)
        &apos;file1&apos;: {
            &apos;class&apos;: &apos;logging.handlers.TimedRotatingFileHandler&apos;,
            &apos;filename&apos;: &apos;access.log&apos;,
            &apos;when&apos;: &apos;W0&apos;,
            &apos;backupCount&apos;: 12,
            &apos;formatter&apos;: &apos;simple&apos;,
            &apos;level&apos;: &apos;INFO&apos;,
        },
        # 输出到文件(每天切割一次)
        &apos;file2&apos;: {
            &apos;class&apos;: &apos;logging.handlers.TimedRotatingFileHandler&apos;,
            &apos;filename&apos;: &apos;error.log&apos;,
            &apos;when&apos;: &apos;D&apos;,
            &apos;backupCount&apos;: 31,
            &apos;formatter&apos;: &apos;verbose&apos;,
            &apos;level&apos;: &apos;WARNING&apos;,
        },
    },
    # 日志器记录器
    &apos;loggers&apos;: {
        &apos;django&apos;: {
            # 需要使用的日志处理器
            &apos;handlers&apos;: [&apos;console&apos;, &apos;file1&apos;, &apos;file2&apos;],
            # 是否向上传播日志信息
            &apos;propagate&apos;: True,
            # 日志级别(不一定是最终的日志级别)
            &apos;level&apos;: &apos;DEBUG&apos;,
        },
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可能已经注意到了，上面日志配置中的&lt;code&gt;formatters&lt;/code&gt;是&lt;strong&gt;日志格式化器&lt;/strong&gt;，它代表了如何格式化输出日志，其中格式占位符分别表示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;%(name)s&lt;/code&gt; - 记录器的名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(levelno)s&lt;/code&gt; - 数字形式的日志记录级别&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(levelname)s&lt;/code&gt; - 日志记录级别的文本名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(filename)s&lt;/code&gt; - 执行日志记录调用的源文件的文件名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(pathname)s&lt;/code&gt; - 执行日志记录调用的源文件的路径名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(funcName)s&lt;/code&gt; - 执行日志记录调用的函数名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(module)s&lt;/code&gt; - 执行日志记录调用的模块名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(lineno)s&lt;/code&gt; - 执行日志记录调用的行号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(created)s&lt;/code&gt; - 执行日志记录的时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(asctime)s&lt;/code&gt; - 日期和时间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(msecs)s&lt;/code&gt; - 毫秒部分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(thread)d&lt;/code&gt; - 线程ID（整数）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(threadName)s&lt;/code&gt; - 线程名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%(process)d&lt;/code&gt; - 进程ID （整数）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;日志配置中的handlers用来指定&lt;strong&gt;日志处理器&lt;/strong&gt;，简单的说就是指定将日志输出到控制台还是文件又或者是网络上的服务器，可用的处理器包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;logging.StreamHandler(stream=None)&lt;/code&gt; - 可以向类似与&lt;code&gt;sys.stdout&lt;/code&gt;或者&lt;code&gt;sys.stderr&lt;/code&gt;的任何文件对象输出信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.FileHandler(filename, mode=&apos;a&apos;, encoding=None, delay=False)&lt;/code&gt; - 将日志消息写入文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.handlers.DatagramHandler(host, port)&lt;/code&gt; - 使用UDP协议，将日志信息发送到指定主机和端口的网络主机上&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.handlers.HTTPHandler(host, url)&lt;/code&gt; - 使用HTTP的GET或POST方法将日志消息上传到一台HTTP 服务器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.handlers.RotatingFileHandler(filename, mode=&apos;a&apos;, maxBytes=0, backupCount=0, encoding=None, delay=False)&lt;/code&gt; - 将日志消息写入文件，如果文件的大小超出&lt;code&gt;maxBytes&lt;/code&gt;指定的值，那么将重新生成一个文件来记录日志&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.handlers.SocketHandler(host, port)&lt;/code&gt; - 使用TCP协议，将日志信息发送到指定主机和端口的网络主机上&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0)&lt;/code&gt; - 将日志输出到指定的邮件地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True)&lt;/code&gt; - 将日志输出到内存指定的缓冲区中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上面每个日志处理器都指定了一个名为&lt;code&gt;level&lt;/code&gt;的属性，它代表了日志的级别，不同的日志级别反映出日志中记录信息的严重性。Python中定义了六个级别的日志，按照从低到高的顺序依次是：NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。&lt;/p&gt;
&lt;p&gt;最后配置的&lt;strong&gt;日志记录器&lt;/strong&gt;是用来真正输出日志的，Django框架提供了如下所示的内置记录器：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;django&lt;/code&gt; - 在Django层次结构中的所有消息记录器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;django.request&lt;/code&gt; - 与请求处理相关的日志消息。5xx响应被视为错误消息；4xx响应被视为为警告消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;django.server&lt;/code&gt; - 与通过runserver调用的服务器所接收的请求相关的日志消息。5xx响应被视为错误消息；4xx响应被记录为警告消息；其他一切都被记录为INFO&lt;/li&gt;
&lt;li&gt;&lt;code&gt;django.template&lt;/code&gt; - 与模板渲染相关的日志消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;django.db.backends&lt;/code&gt; - 有与数据库交互产生的日志消息，如果希望显示ORM框架执行的SQL语句，就可以使用该日志记录器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;日志记录器中配置的日志级别有可能不是最终的日志级别，因为还要参考日志处理器中配置的日志级别，取二者中级别较高者作为最终的日志级别。&lt;/p&gt;
&lt;h4&gt;配置Django-Debug-Toolbar&lt;/h4&gt;
&lt;p&gt;如果想调试你的Django项目，你一定不能不过名为Django-Debug-Toolbar的神器，它是项目开发阶段辅助调试和优化的必备工具，只要配置了它，就可以很方便的查看到如下表所示的项目运行信息，这些信息对调试项目和优化Web应用性能都是至关重要的。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Versions&lt;/td&gt;
&lt;td&gt;Django的版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;显示视图耗费的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settings&lt;/td&gt;
&lt;td&gt;配置文件中设置的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headers&lt;/td&gt;
&lt;td&gt;HTTP请求头和响应头的信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request&lt;/td&gt;
&lt;td&gt;和请求相关的各种变量及其信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StaticFiles&lt;/td&gt;
&lt;td&gt;静态文件加载情况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates&lt;/td&gt;
&lt;td&gt;模板的相关信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;缓存的使用情况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signals&lt;/td&gt;
&lt;td&gt;Django内置的信号信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;被记录的日志信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;向数据库发送的SQL语句及其执行时间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Django-Debug-Toolbar。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-debug-toolbar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 - 修改settings.py。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [
    &apos;debug_toolbar&apos;,
]

MIDDLEWARE = [
    &apos;debug_toolbar.middleware.DebugToolbarMiddleware&apos;,
]

DEBUG_TOOLBAR_CONFIG = {
    # 引入jQuery库
    &apos;JQUERY_URL&apos;: &apos;https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js&apos;,
    # 工具栏是否折叠
    &apos;SHOW_COLLAPSED&apos;: True,
    # 是否显示工具栏
    &apos;SHOW_TOOLBAR_CALLBACK&apos;: lambda x: True,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 - 修改urls.py。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if settings.DEBUG:

    import debug_toolbar

    urlpatterns.insert(0, path(&apos;__debug__/&apos;, include(debug_toolbar.urls)))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在配置好Django-Debug-Toolbar之后，页面右侧会看到一个调试工具栏，如下图所示，上面包括了如前所述的各种调试信息，包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等，查看起来非常的方便。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/07dd02bba7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;优化ORM代码&lt;/h4&gt;
&lt;p&gt;在配置了日志或Django-Debug-Toolbar之后，我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况，这里我们关注的是ORM框架生成的SQL查询到底是什么样子的，相信这里的结果会让你感到有一些意外。执行&lt;code&gt;Teacher.objects.all()&lt;/code&gt;之后我们可以注意到，在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=()
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的问题通常被称为“1+N查询”（有的地方也将其称之为“N+1查询”），原本获取老师的数据只需要一条SQL，但是由于老师关联了学科，当我们查询到&lt;code&gt;N&lt;/code&gt;条老师的数据时，Django的ORM框架又向数据库发出了&lt;code&gt;N&lt;/code&gt;条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力，如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法，这一点也很容易做到，相信大家已经想到怎么做了。是的，我们可以使用连接查询，但是在使用Django的ORM框架时如何做到这一点呢？对于多对一关联（如投票应用中的老师和学科），我们可以使用&lt;code&gt;QuerySet&lt;/code&gt;的用&lt;code&gt;select_related()&lt;/code&gt;方法来加载关联对象；而对于多对多关联（如电商网站中的订单和商品），我们可以使用&lt;code&gt;prefetch_related()&lt;/code&gt;方法来加载关联对象。&lt;/p&gt;
&lt;p&gt;在导出老师Excel报表的视图函数中，我们可以按照下面的方式优化代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queryset = Teacher.objects.all().select_related(&apos;subject&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，用ECharts生成前端报表的视图函数中，查询老师好评和差评数据的操作也能够优化，因为在这个例子中，我们只需要获取老师的姓名、好评数和差评数这三项数据，但是在默认的情况生成的SQL会查询老师表的所有字段。可以用&lt;code&gt;QuerySet&lt;/code&gt;的&lt;code&gt;only()&lt;/code&gt;方法来指定需要查询的属性，也可以用&lt;code&gt;QuerySet&lt;/code&gt;的&lt;code&gt;defer()&lt;/code&gt;方法来指定暂时不需要查询的属性，这样生成的SQL会通过投影操作来指定需要查询的列，从而改善查询性能，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queryset = Teacher.objects.all().only(&apos;name&apos;, &apos;good_count&apos;, &apos;bad_count&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，如果要统计出每个学科的老师好评和差评的平均数，利用Django的ORM框架也能够做到，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queryset = Teacher.objects.values(&apos;subject&apos;).annotate(good=Avg(&apos;good_count&apos;), bad=Avg(&apos;bad_count&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里获得的&lt;code&gt;QuerySet&lt;/code&gt;中的元素是字典对象，每个字典中有三组键值对，分别是代表学科编号的&lt;code&gt;subject&lt;/code&gt;、代表好评数的&lt;code&gt;good&lt;/code&gt;和代表差评数的&lt;code&gt;bad&lt;/code&gt;。如果想要获得学科的名称而不是编号，可以按照如下所示的方式调整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queryset = Teacher.objects.values(&apos;subject__name&apos;).annotate(good=Avg(&apos;good_count&apos;), bad=Avg(&apos;bad_count&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;52 中间件的应用&lt;/h2&gt;
&lt;p&gt;之前我们已经实现了用户必须登录才能投票的限制，但是一个新的问题来了。如果我们的应用中有很多功能都需要用户先登录才能执行，例如将前面导出Excel报表和查看统计图表的功能都做了必须登录才能访问的限制，那么我们是不是需要在每个视图函数中添加代码来检查session中是否包含&lt;code&gt;userid&lt;/code&gt;的代码呢？答案是否定的，如果这样做了，我们的视图函数中必然会充斥着大量的重复代码。编程大师&lt;em&gt;Martin Fowler&lt;/em&gt;曾经说过：&lt;strong&gt;代码有很多种坏味道，重复是最坏的一种&lt;/strong&gt;。在Python程序中，我们可以通过装饰器来为函数提供额外的能力；在Django项目中，我们可以把类似于验证用户是否登录这样的重复性代码放到&lt;strong&gt;中间件&lt;/strong&gt;中。&lt;/p&gt;
&lt;h4&gt;Django中间件概述&lt;/h4&gt;
&lt;p&gt;中间件是安插在Web应用请求和响应过程之间的组件，它在整个Web应用中扮演了拦截过滤器的角色，通过中间件可以拦截请求和响应，并对请求和响应进行过滤（简单的说就是执行额外的处理）。通常，一个中间件组件只专注于完成一件特定的事，例如：Django框架通过&lt;code&gt;SessionMiddleware&lt;/code&gt;中间件实现了对session的支持，又通过&lt;code&gt;AuthenticationMiddleware&lt;/code&gt;中间件实现了基于session的请求认证。通过把多个中间件组合在一起，我们可以完成更为复杂的任务，Django框架就是这么做的。&lt;/p&gt;
&lt;p&gt;Django项目的配置文件中就包含了对中间件的配置，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们稍微为大家解释一下这些中间件的作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CommonMiddleware&lt;/code&gt; - 基础设置中间件，可以处理以下一些配置参数。
&lt;ul&gt;
&lt;li&gt;DISALLOWED_USER_AGENTS - 不被允许的用户代理（浏览器）&lt;/li&gt;
&lt;li&gt;APPEND_SLASH - 是否追加&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;USE_ETAG - 浏览器缓存相关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SecurityMiddleware&lt;/code&gt; - 安全相关中间件，可以处理和安全相关的配置项。
&lt;ul&gt;
&lt;li&gt;SECURE_HSTS_SECONDS - 强制使用HTTPS的时间&lt;/li&gt;
&lt;li&gt;SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名&lt;/li&gt;
&lt;li&gt;SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型&lt;/li&gt;
&lt;li&gt;SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器&lt;/li&gt;
&lt;li&gt;SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接&lt;/li&gt;
&lt;li&gt;SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SessionMiddleware&lt;/code&gt; - 会话中间件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CsrfViewMiddleware&lt;/code&gt; - 通过生成令牌，防范跨请求份伪的造中间件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XFrameOptionsMiddleware&lt;/code&gt; - 通过设置请求头参数，防范点击劫持攻击的中间件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在请求的过程中，上面的中间件会按照书写的顺序从上到下执行，然后是URL解析，最后请求才会来到视图函数；在响应的过程中，上面的中间件会按照书写的顺序从下到上执行，与请求时中间件执行的顺序正好相反。&lt;/p&gt;
&lt;h4&gt;自定义中间件&lt;/h4&gt;
&lt;p&gt;Django中的中间件有两种实现方式：基于类的实现方式和基于函数的实现方式，后者更接近于装饰器的写法。装饰器实际上是代理模式的应用，将横切关注功能（与正常业务逻辑没有必然联系的功能，例如：身份认证、日志记录、编码转换之类的功能）置于代理中，由代理对象来完成被代理对象的行为并添加额外的功能。中间件对用户请求和响应进行拦截过滤并增加额外的处理，在这一点上它跟装饰器是完全一致的，所以基于函数的写法来实现中间件就跟装饰器的写法几乎一模一样。下面我们用自定义的中间件来实现用户登录验证的功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
middlewares.py
&quot;&quot;&quot;
from django.http import JsonResponse
from django.shortcuts import redirect

# 需要登录才能访问的资源路径
LOGIN_REQUIRED_URLS = {&apos;/praise/&apos;, &apos;/criticize/&apos;, &apos;/excel/&apos;, &apos;/teachers_data/&apos;}


def check_login_middleware(get_resp):

    def wrapper(request, *args, **kwargs):
        # 请求的资源路径在上面的集合中
        if request.path in LOGIN_REQUIRED_URLS:
            # 会话中包含userid则视为已经登录
            if &apos;userid&apos; not in request.session:
                # 判断是不是Ajax请求
                if request.is_ajax():
                    # Ajax请求返回JSON数据提示用户登录
                    return JsonResponse({&apos;code&apos;: 10003, &apos;hint&apos;: &apos;请先登录&apos;})
                else:
                    backurl = request.get_full_path()
                    # 非Ajax请求直接重定向到登录页
                    return redirect(f&apos;/login/?backurl={backurl}&apos;)
        return get_resp(request, *args, **kwargs)

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，我们也可以定义一个类来充当装饰器，如果类中有&lt;code&gt;__call__&lt;/code&gt;魔术方法，这个类的对象就像函数一样可调用，所以下面是另一种实现中间件的方式，道理跟上面的代码完全一样。&lt;/p&gt;
&lt;p&gt;还有一种基于类实现中间件的方式，这种方式在较新版本的Django中已经不推荐使用了，但是大家接触到的代码中，仍然有可能遇到这种写法，大致的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.utils.deprecation import MiddlewareMixin


class MyMiddleware(MiddlewareMixin):

    def process_request(self, request):
        pass

    def process_view(self, request, view_func, view_args, view_kwargs):
        pass

    def process_template_response(self, request, response):
        pass

    def process_response(self, request, response):
        pass

    def process_exception(self, request, exception):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面类中的五个方法都是中间件的钩子函数，分别在收到用户请求、进入视图函数之前、渲染模板、返回响应和出现异常的时候被回调。当然，写不写这些方法是根据中间件的需求来确定的，并不是所有的场景都需要重写五个方法，下面的图相信能够帮助大家理解这种写法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/deda516785.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写好中间件代码后，需要修改配置文件来激活中间件使其生效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
    &apos;debug_toolbar.middleware.DebugToolbarMiddleware&apos;,
    &apos;vote.middlewares.check_login_middleware&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面这个中间件列表中元素的顺序，当收到来自用户的请求时，中间件按照从上到下的顺序依次执行，这行完这些中间件以后，请求才会最终到达视图函数。当然，在这个过程中，用户的请求可以被拦截，就像上面我们自定义的中间件那样，如果用户在没有登录的情况下访问了受保护的资源，中间件会将请求直接重定向到登录页，后面的中间件和视图函数将不再执行。在响应用户请求的过程中，上面的中间件会按照从下到上的顺序依次执行，这样的话我们还可以对响应做进一步的处理。&lt;/p&gt;
&lt;p&gt;中间件执行的顺序是非常重要的，对于有依赖关系的中间件必须保证被依赖的中间件要置于依赖它的中间件的前面，就好比我们刚才自定义的中间件要放到&lt;code&gt;SessionMiddleware&lt;/code&gt;的后面，因为我们要依赖这个中间件为请求绑定的&lt;code&gt;session&lt;/code&gt;对象才能判定用户是否登录。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;53 前后端分离开发入门&lt;/h2&gt;
&lt;p&gt;在传统的Web应用开发中，大多数的程序员会将浏览器作为前后端的分界线。将浏览器中为用户进行页面展示的部分称之为前端，而将运行在服务器为前端提供业务逻辑和数据准备的所有代码统称为后端。所谓前后端分离的开发，就是前后端工程师约定好数据交互接口，并行的进行开发和测试，后端只提供数据，不负责将数据渲染到页面上，前端通过HTTP请求获取数据并负责将数据渲染到页面上，这个工作是交给浏览器中的JavaScript代码来完成。&lt;/p&gt;
&lt;p&gt;使用前后端分离开发有诸多的好处，下面我们简要的说下这些好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提升开发效率&lt;/strong&gt;。前后端分离以后，可以实现前后端代码的解耦，只要前后端沟通约定好应用所需接口以及接口参数，便可以开始并行开发，无需等待对方的开发工作结束。在这种情况下，前后端工程师都可以只专注于自己的开发工作，有助于打造出更好的团队。除此之外，在前后端分离的开发模式下，即使需求发生变更，只要接口与数据格式不变，后端开发人员就不需要修改代码，只要前端进行变动即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增强代码的可维护性&lt;/strong&gt;。前后端分离后，应用的代码不再是前后端混合，只有在运行期才会有调用依赖关系，这样的话维护代码的工作将变得轻松愉快很多，再不会牵一发而动全身。当你的代码变得简明且整洁时，代码的可读性和可维护性都会有质的提升。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持多终端和服务化架构&lt;/strong&gt;。前后端分离后，同一套数据接口可以为不同的终端提供服务，更有助于打造多终端应用；此外，由于后端提供的接口之间可以通过HTTP(S)进行调用，有助于打造服务化架构（包括微服务）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来我们就用前后端分离的方式来改写之前的投票应用。&lt;/p&gt;
&lt;h4&gt;返回JSON格式的数据&lt;/h4&gt;
&lt;p&gt;刚才说过，在前后端分离的开发模式下，后端需要为前端提供数据接口，这些接口通常返回JSON格式的数据。在Django项目中，我们可以先将对象处理成字典，然后就可以利用Django封装的&lt;code&gt;JsonResponse&lt;/code&gt;向浏览器返回JSON格式的数据，具体的做法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def show_subjects(request):
    queryset = Subject.objects.all()
    subjects = []
    for subject in queryset:
        subjects.append({
            &apos;no&apos;: subject.no,
            &apos;name&apos;: subject.name,
            &apos;intro&apos;: subject.intro,
            &apos;isHot&apos;: subject.is_hot
        })
    return JsonResponse(subjects, safe=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，我们通过循环遍历查询学科得到的&lt;code&gt;QuerySet&lt;/code&gt;对象，将每个学科的数据处理成一个字典，在将字典保存在名为&lt;code&gt;subjects&lt;/code&gt;的列表容器中，最后利用&lt;code&gt;JsonResponse&lt;/code&gt;完成对列表的序列化，向浏览器返回JSON格式的数据。由于&lt;code&gt;JsonResponse&lt;/code&gt;序列化的是一个列表而不是字典，所以需要指定&lt;code&gt;safe&lt;/code&gt;参数的值为&lt;code&gt;False&lt;/code&gt;才能完成对&lt;code&gt;subjects&lt;/code&gt;的序列化，否则会产生&lt;code&gt;TypeError&lt;/code&gt;异常。&lt;/p&gt;
&lt;p&gt;可能大家已经发现了，自己写代码将一个对象转成字典是比较麻烦的，如果对象的属性很多而且某些属性又关联到一个比较复杂的对象时，情况会变得更加糟糕。为此我们可以使用一个名为&lt;code&gt;bpmappers&lt;/code&gt;的三方库来简化将对象转成字典的操作，这个三方库本身也提供了对Django框架的支持。&lt;/p&gt;
&lt;p&gt;安装三方库&lt;code&gt;bpmappers&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install bpmappers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写映射器（实现对象到字典转换）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from bpmappers.djangomodel import ModelMapper

from poll2.models import Subject


class SubjectMapper(ModelMapper):
   
    class Meta:
        model = Subject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改视图函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def show_subjects(request):
    queryset = Subject.objects.all()
    subjects = []
    for subject in queryset:
        subjects.append(SubjectMapper(subject).as_dict())
    return JsonResponse(subjects, safe=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置URL映射。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    
    path(&apos;api/subjects/&apos;, show_subjects),
    
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后访问该接口，可以得到如下所示的JSON格式数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;no&quot;: 1,
        &quot;name&quot;: &quot;Python全栈+人工智能&quot;,
        &quot;intro&quot;: &quot;Python是一种计算机程序设计语言。是一种面向对象的动态类型语言，最初被设计用于编写自动化脚本(shell)，随着版本的不断更新和语言新功能的添加，越来越多被用于独立的、大型项目的开发。&quot;,
        &quot;is_hot&quot;: true
    },
    // 此处省略下面的内容
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不希望在JSON数据中显示学科的成立时间，我们可以在映射器中排除&lt;code&gt;create_date&lt;/code&gt;属性；如果希望将是否为热门学科对应的键取名为&lt;code&gt;isHot&lt;/code&gt;（默认的名字是&lt;code&gt;is_hot&lt;/code&gt;），也可以通过修改映射器来做到。具体的做法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from bpmappers import RawField
from bpmappers.djangomodel import ModelMapper

from poll2.models import Subject


class SubjectMapper(ModelMapper):
    isHot = RawField(&apos;is_hot&apos;)

    class Meta:
        model = Subject
        exclude = (&apos;is_hot&apos;, )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次查看学科接口返回的JSON数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;no&quot;: 101,
        &quot;name&quot;: &quot;Python全栈+人工智能&quot;,
        &quot;intro&quot;: &quot;Python是一种计算机程序设计语言。是一种面向对象的动态类型语言，最初被设计用于编写自动化脚本(shell)，随着版本的不断更新和语言新功能的添加，越来越多被用于独立的、大型项目的开发。&quot;,
        &quot;isHot&quot;: true
    },
    // 此处省略下面的内容
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;bpmappers&lt;/code&gt;详细的使用指南，请参考它的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/bpmappers.readthedocs.io/en/stable/%3E&quot;&gt;官方文档&lt;/a&gt;，这个官方文档是用日语书写的，可以使用浏览器的翻译功能将它翻译成你熟悉的语言即可。&lt;/p&gt;
&lt;h4&gt;使用Vue.js渲染页面&lt;/h4&gt;
&lt;p&gt;接下来我们通过前端框架Vue.js来实现页面的渲染。如果希望全面的了解和学习Vue.js，建议阅读它的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/cn.vuejs.org/v2/guide/%3E&quot;&gt;官方教程&lt;/a&gt;或者在&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day46-60/%3Chttps:/www.youtube.com/%3E&quot;&gt;YouTube&lt;/a&gt;上搜索Vue.js的新手教程（Vue.js Crash Course）进行学习。&lt;/p&gt;
&lt;p&gt;重新改写subjects.html页面，使用Vue.js来渲染页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;学科信息&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
		/* 此处省略层叠样式表 */
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
        &amp;lt;h1&amp;gt;扣丁学堂所有学科&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;div id=&quot;main&quot;&amp;gt;
            &amp;lt;dl v-for=&quot;subject in subjects&quot;&amp;gt;
                &amp;lt;dt&amp;gt;
                    &amp;lt;a :href=&quot;&apos;/static/html/teachers.html?sno=&apos; + subject.no&quot;&amp;gt;
                        {{ subject.name }}
                    &amp;lt;/a&amp;gt;
                    &amp;lt;img v-if=&quot;subject.is_hot&quot; src=&quot;/static/images/hot-icon-small.png&quot;&amp;gt;
                &amp;lt;/dt&amp;gt;
                &amp;lt;dd&amp;gt;{{ subject.intro }}&amp;lt;/dd&amp;gt;
            &amp;lt;/dl&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        let app = new Vue({
            el: &apos;#main&apos;,
            data: {
                subjects: []
            },
            created() {
                fetch(&apos;/api/subjects/&apos;)
                    .then(resp =&amp;gt; resp.json())
                    .then(json =&amp;gt; {
                        this.subjects = json
                    })
            }
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后端分离的开发需要将前端页面作为静态资源进行部署，项目实际上线的时候，我们会对整个Web应用进行动静分离，静态资源通过Nginx或Apache服务器进行部署，生成动态内容的Python程序部署在uWSGI或者Gunicorn服务器上，对动态内容的请求由Nginx或Apache路由到uWSGI或Gunicorn服务器上。&lt;/p&gt;
&lt;p&gt;在开发阶段，我们通常会使用Django自带的测试服务器，如果要尝试前后端分离，可以先将静态页面放在之前创建的放静态资源的目录下，具体的做法可以参考&lt;a href=&quot;https://gitee.com/jackfrued/django19062&quot;&gt;项目完整代码&lt;/a&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;54 RESTful架构和DRF入门&lt;/h2&gt;
&lt;p&gt;把软件（Software）、平台（Platform）、基础设施（Infrastructure）做成服务（Service）是很多IT企业都一直在做的事情，这就是大家经常听到的SasS（软件即服务）、PasS（平台即服务）和IasS（基础设置即服务）。实现面向服务的架构（SOA）有诸多的方式，包括RPC（远程过程调用）、Web Service、REST等，在技术层面上，SOA是一种&lt;strong&gt;抽象的、松散耦合的粗粒度软件架构&lt;/strong&gt;；在业务层面上，SOA的核心概念是“&lt;strong&gt;重用&lt;/strong&gt;”和“&lt;strong&gt;互操作&lt;/strong&gt;”，它将系统资源整合成可操作的、标准的服务，使得这些资源能够被重新组合和应用。在实现SOA的诸多方案中，REST被认为是最适合互联网应用的架构，符合REST规范的架构也经常被称作RESTful架构。&lt;/p&gt;
&lt;h4&gt;REST概述&lt;/h4&gt;
&lt;p&gt;REST这个词，是&lt;strong&gt;Roy Thomas Fielding&lt;/strong&gt;在他2000年的博士论文中提出的，Roy是HTTP协议（1.0和1.1版）的主要设计者、Apache服务器软件主要作者、Apache基金会第一任主席。在他的博士论文中，Roy把他对互联网软件的架构原则定名为REST，即&lt;strong&gt;RE&lt;/strong&gt;presentational &lt;strong&gt;S&lt;/strong&gt;tate &lt;strong&gt;T&lt;/strong&gt;ransfer的缩写，中文通常翻译为“&lt;strong&gt;表现层状态转移&lt;/strong&gt;”或“&lt;strong&gt;表述状态转移&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;这里的“表现层”其实指的是“资源”的“表现层”。所谓资源，就是网络上的一个实体，或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲或一种服务。我们可以用一个URI（统一资源定位符）指向资源，要获取到这个资源，访问它的URI即可，URI就是资源在互联网上的唯一标识。资源可以有多种外在表现形式。我们把资源具体呈现出来的形式，叫做它的“表现层”。比如，文本可以用&lt;code&gt;text/plain&lt;/code&gt;格式表现，也可以用&lt;code&gt;text/html&lt;/code&gt;格式、&lt;code&gt;text/xml&lt;/code&gt;格式、&lt;code&gt;application/json&lt;/code&gt;格式表现，甚至可以采用二进制格式；图片可以用&lt;code&gt;image/jpeg&lt;/code&gt;格式表现，也可以用&lt;code&gt;image/png&lt;/code&gt;格式表现。URI只代表资源的实体，不代表它的表现形式。严格地说，有些网址最后的&lt;code&gt;.html&lt;/code&gt;后缀名是不必要的，因为这个后缀名表示格式，属于“表现层”范畴，而URI应该只代表“资源”的位置，它的具体表现形式，应该在HTTP请求的头信息中用&lt;code&gt;Accept&lt;/code&gt;和&lt;code&gt;Content-Type&lt;/code&gt;字段指定，这两个字段才是对“表现层”的描述。&lt;/p&gt;
&lt;p&gt;访问一个网站，就代表了客户端和服务器的一个互动过程。在这个过程中，势必涉及到数据和状态的变化。Web应用通常使用HTTP作为其通信协议，客户端想要操作服务器，必须通过HTTP请求，让服务器端发生“状态转移”，而这种转移是建立在表现层之上的，所以就是“表现层状态转移”。客户端通过HTTP的动词GET、POST、PUT（或PATCH）、DELETE，分别对应对资源的四种基本操作，其中GET用来获取资源，POST用来新建资源（也可以用于更新资源），PUT（或PATCH）用来更新资源，DELETE用来删除资源。&lt;/p&gt;
&lt;p&gt;简单的说RESTful架构就是：“每一个URI代表一种资源，客户端通过四个HTTP动词，对服务器端资源进行操作，实现资源的表现层状态转移”。&lt;/p&gt;
&lt;p&gt;我们在设计Web应用时，如果需要向客户端提供资源，就可以使用REST风格的URI，这是实现RESTful架构的第一步。当然，真正的RESTful架构并不只是URI符合REST风格，更为重要的是“无状态”和“幂等性”两个词，我们在后面的课程中会为大家阐述这两点。下面的例子给出了一些符合REST风格的URI，供大家在设计URI时参考。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;请求方法（HTTP动词）&lt;/th&gt;
&lt;th&gt;URI&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取所有学生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;POST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新建一个学生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定ID的学生信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PUT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新指定ID的学生信息（提供该学生的全部信息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PATCH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新指定ID的学生信息（提供该学生的部分信息）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DELETE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除指定ID的学生信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/friends/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出指定ID的学生的所有朋友&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DELETE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/students/ID/friends/ID/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除指定ID的学生的指定ID的朋友&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;DRF使用入门&lt;/h4&gt;
&lt;p&gt;在Django项目中，如果要实现REST架构，即将网站的资源发布成REST风格的API接口，可以使用著名的三方库&lt;code&gt;djangorestframework&lt;/code&gt; ，我们通常将其简称为DRF。&lt;/p&gt;
&lt;h5&gt;安装和配置DRF&lt;/h5&gt;
&lt;p&gt;安装DRF。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install djangorestframework
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置DRF。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSTALLED_APPS = [

    &apos;rest_framework&apos;,
    
]

# 下面的配置根据项目需要进行设置
REST_FRAMEWORK = {
    # 配置默认页面大小
    # &apos;PAGE_SIZE&apos;: 10,
    # 配置默认的分页类
    # &apos;DEFAULT_PAGINATION_CLASS&apos;: &apos;...&apos;,
    # 配置异常处理器
    # &apos;EXCEPTION_HANDLER&apos;: &apos;...&apos;,
    # 配置默认解析器
    # &apos;DEFAULT_PARSER_CLASSES&apos;: (
    #     &apos;rest_framework.parsers.JSONParser&apos;,
    #     &apos;rest_framework.parsers.FormParser&apos;,
    #     &apos;rest_framework.parsers.MultiPartParser&apos;,
    # ),
    # 配置默认限流类
    # &apos;DEFAULT_THROTTLE_CLASSES&apos;: (
    #     &apos;...&apos;
    # ),
    # 配置默认授权类
    # &apos;DEFAULT_PERMISSION_CLASSES&apos;: (
    #     &apos;...&apos;,
    # ),
    # 配置默认认证类
    # &apos;DEFAULT_AUTHENTICATION_CLASSES&apos;: (
    #     &apos;...&apos;,
    # ),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;编写序列化器&lt;/h5&gt;
&lt;p&gt;前后端分离的开发需要后端为前端、移动端提供API数据接口，而API接口通常情况下都是返回JSON格式的数据，这就需要对模型对象进行序列化处理。DRF中封装了&lt;code&gt;Serializer&lt;/code&gt;类和&lt;code&gt;ModelSerializer&lt;/code&gt;类用于实现序列化操作，通过继承&lt;code&gt;Serializer&lt;/code&gt;类或&lt;code&gt;ModelSerializer&lt;/code&gt;类，我们可以自定义序列化器，用于将对象处理成字典，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework import serializers 


class SubjectSerializer(serializers.ModelSerializer):

    class Meta:
        model = Subject
        fields = &apos;__all__&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码直接继承了&lt;code&gt;ModelSerializer&lt;/code&gt;，通过&lt;code&gt;Meta&lt;/code&gt;类的&lt;code&gt;model&lt;/code&gt;属性指定要序列化的模型以及&lt;code&gt;fields&lt;/code&gt;属性指定需要序列化的模型字段，稍后我们就可以在视图函数中使用该类来实现对&lt;code&gt;Subject&lt;/code&gt;模型的序列化。&lt;/p&gt;
&lt;h5&gt;编写视图函数&lt;/h5&gt;
&lt;p&gt;DRF框架支持两种实现数据接口的方式，一种是FBV（基于函数的视图），另一种是CBV（基于类的视图）。我们先看看FBV的方式如何实现数据接口，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.decorators import api_view
from rest_framework.response import Response


@api_view((&apos;GET&apos;, ))
def show_subjects(request: HttpRequest) -&amp;gt; HttpResponse:
    subjects = Subject.objects.all().order_by(&apos;no&apos;)
    # 创建序列化器对象并指定要序列化的模型
    serializer = SubjectSerializer(subjects, many=True)
    # 通过序列化器的data属性获得模型对应的字典并通过创建Response对象返回JSON格式的数据
    return Response(serializer.data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比上一个章节的使用&lt;code&gt;bpmapper&lt;/code&gt;实现模型序列化的代码，使用DRF的代码更加简单明了，而且DRF本身自带了一套页面，可以方便我们查看我们使用DRF定制的数据接口，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/0c9b493207.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;直接使用上一节写好的页面，就可以通过Vue.js把上面接口提供的学科数据渲染并展示出来，此处不再进行赘述。&lt;/p&gt;
&lt;h5&gt;实现老师信息数据接口&lt;/h5&gt;
&lt;p&gt;编写序列化器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SubjectSimpleSerializer(serializers.ModelSerializer):

    class Meta:
        model = Subject
        fields = (&apos;no&apos;, &apos;name&apos;)


class TeacherSerializer(serializers.ModelSerializer):

    class Meta:
        model = Teacher
        exclude = (&apos;subject&apos;, )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写视图函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@api_view((&apos;GET&apos;, ))
def show_teachers(request: HttpRequest) -&amp;gt; HttpResponse:
    try:
        sno = int(request.GET.get(&apos;sno&apos;))
        subject = Subject.objects.only(&apos;name&apos;).get(no=sno)
        teachers = Teacher.objects.filter(subject=subject).defer(&apos;subject&apos;).order_by(&apos;no&apos;)
        subject_seri = SubjectSimpleSerializer(subject)
        teacher_seri = TeacherSerializer(teachers, many=True)
        return Response({&apos;subject&apos;: subject_seri.data, &apos;teachers&apos;: teacher_seri.data})
    except (TypeError, ValueError, Subject.DoesNotExist):
        return Response(status=404)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置URL映射。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    
    path(&apos;api/teachers/&apos;, show_teachers),
    
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过Vue.js渲染页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;老师信息&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        /* 此处省略掉层叠样式表 */
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;container&quot;&amp;gt;
        &amp;lt;h1&amp;gt;{{ subject.name }}学科的老师信息&amp;lt;/h1&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;h2 v-if=&quot;loaded &amp;amp;&amp;amp; teachers.length == 0&quot;&amp;gt;暂无该学科老师信息&amp;lt;/h2&amp;gt;
        &amp;lt;div class=&quot;teacher&quot; v-for=&quot;teacher in teachers&quot;&amp;gt;
            &amp;lt;div class=&quot;photo&quot;&amp;gt;
                &amp;lt;img :src=&quot;&apos;/static/images/&apos; + teacher.photo&quot; height=&quot;140&quot; alt=&quot;&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;info&quot;&amp;gt;
                &amp;lt;div&amp;gt;
                    &amp;lt;span&amp;gt;&amp;lt;strong&amp;gt;姓名：{{ teacher.name }}&amp;lt;/strong&amp;gt;&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;性别：{{ teacher.sex | maleOrFemale }}&amp;lt;/span&amp;gt;
                    &amp;lt;span&amp;gt;出生日期：{{ teacher.birth }}&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;intro&quot;&amp;gt;{{ teacher.intro }}&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;comment&quot;&amp;gt;
                    &amp;lt;a href=&quot;&quot; @click.prevent=&quot;vote(teacher, true)&quot;&amp;gt;好评&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
                    (&amp;lt;strong&amp;gt;{{ teacher.good_count }}&amp;lt;/strong&amp;gt;)
                    &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;
                    &amp;lt;a href=&quot;&quot; @click.prevent=&quot;vote(teacher, false)&quot;&amp;gt;差评&amp;lt;/a&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;
                    (&amp;lt;strong&amp;gt;{{ teacher.bad_count }}&amp;lt;/strong&amp;gt;)
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;a href=&quot;/static/html/subjects.html&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        let app = new Vue({
            el: &apos;#container&apos;,
            data: {
                subject: {},
                teachers: [],
                loaded: false
            },
            created() {
                fetch(&apos;/api/teachers/&apos; + location.search)
                    .then(resp =&amp;gt; resp.json())
                    .then(json =&amp;gt; {
                        this.subject = json.subject
                        this.teachers = json.teachers
                    })
            },
            filters: {
                maleOrFemale(sex) {
                    return sex? &apos;男&apos;: &apos;女&apos;
                }
            },
            methods: {
               vote(teacher, flag) {
                    let url = flag? &apos;/praise/&apos; : &apos;/criticize/&apos;
                    url += &apos;?tno=&apos; + teacher.no
                    fetch(url).then(resp =&amp;gt; resp.json()).then(json =&amp;gt; {
                        if (json.code === 10000) {
                            if (flag) {
                                teacher.good_count = json.count
                            } else {
                                teacher.bad_count = json.count
                            }
                        }
                    })
                }
            }
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;前后端分离下的用户登录&lt;/h4&gt;
&lt;p&gt;之前我们提到过， HTTP是无状态的，一次请求结束连接断开，下次服务器再收到请求，它就不知道这个请求是哪个用户发过来的。但是对于一个Web应用而言，它是需要有状态管理的，这样才能让服务器知道HTTP请求来自哪个用户，从而判断是否允许该用户请求以及为用户提供更好的服务，这个过程就是常说的&lt;strong&gt;会话管理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之前我们做会话管理（用户跟踪）的方法是：用户登录成功后，在服务器端通过一个session对象保存用户相关数据，然后把session对象的ID写入浏览器的cookie中；下一次请求时，HTTP请求头中携带cookie的数据，服务器从HTTP请求头读取cookie中的sessionid，根据这个标识符找到对应的session对象，这样就能够获取到之前保存在session中的用户数据。我们刚才说过，REST架构是最适合互联网应用的架构，它强调了HTTP的无状态性，这样才能保证应用的水平扩展能力（当并发访问量增加时，可以通过增加新的服务器节点来为系统扩容）。显然，基于session实现用户跟踪的方式需要服务器保存session对象，在做水平扩展增加新的服务器节点时，需要复制和同步session对象，这显然是非常麻烦的。解决这个问题有两种方案，一种是架设缓存服务器（如Redis），让多个服务器节点共享缓存服务并将session对象直接置于缓存服务器中；另一种方式放弃基于session的用户跟踪，使用&lt;strong&gt;基于token的用户跟踪&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;基于token的用户跟踪是在用户登录成功后，为用户生成身份标识并保存在浏览器本地存储（localStorage、sessionStorage、cookie等）中，这样的话服务器不需要保存用户状态，从而可以很容易的做到水平扩展。基于token的用户跟踪具体流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户登录时，如果登录成功就按照某种方式为用户生成一个令牌（token），该令牌中通常包含了用户标识、过期时间等信息而且需要加密并生成指纹（避免伪造或篡改令牌），服务器将令牌返回给前端；&lt;/li&gt;
&lt;li&gt;前端获取到服务器返回的token，保存在浏览器本地存储中（可以保存在&lt;code&gt;localStorage&lt;/code&gt;或&lt;code&gt;sessionStorage&lt;/code&gt;中，对于使用Vue.js的前端项目来说，还可以通过Vuex进行状态管理）；&lt;/li&gt;
&lt;li&gt;对于使用了前端路由的项目来说，前端每次路由跳转，可以先判断&lt;code&gt;localStroage&lt;/code&gt;中有无token，如果没有则跳转到登录页；&lt;/li&gt;
&lt;li&gt;每次请求后端数据接口，在HTTP请求头里携带token；后端接口判断请求头有无token，如果没有token以及token是无效的或过期的，服务器统一返回401；&lt;/li&gt;
&lt;li&gt;如果前端收到HTTP响应状态码401，则重定向到登录页面。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过上面的描述，相信大家已经发现了，基于token的用户跟踪最为关键是在用户登录成功时，要为用户生成一个token作为用户的身份标识。生成token的方法很多，其中一种比较成熟的解决方案是使用JSON Web Token。&lt;/p&gt;
&lt;h5&gt;JWT概述&lt;/h5&gt;
&lt;p&gt;JSON Web Token通常简称为JWT，它是一种开放标准（RFC 7519）。随着RESTful架构的流行，越来越多的项目使用JWT作为用户身份认证的方式。JWT相当于是三个JSON对象经过编码后，用&lt;code&gt;.&lt;/code&gt;分隔并组合到一起，这三个JSON对象分别是头部（header）、载荷（payload）和签名（signature），如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/9ee9253629.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;头部&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;alg&lt;/code&gt;属性表示签名的算法，默认是HMAC SHA256（简写成&lt;code&gt;HS256&lt;/code&gt;）；&lt;code&gt;typ&lt;/code&gt;属性表示这个令牌的类型，JWT中都统一书写为&lt;code&gt;JWT&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;载荷&lt;/p&gt;
&lt;p&gt;载荷部分用来存放实际需要传递的数据。JWT官方文档中规定了7个可选的字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iss ：签发人&lt;/li&gt;
&lt;li&gt;exp：过期时间&lt;/li&gt;
&lt;li&gt;sub：主题&lt;/li&gt;
&lt;li&gt;aud：受众&lt;/li&gt;
&lt;li&gt;nbf：生效时间&lt;/li&gt;
&lt;li&gt;iat：签发时间&lt;/li&gt;
&lt;li&gt;jti：编号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了官方定义的字典，我们可以根据应用的需要添加自定义的字段，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;nickname&quot;: &quot;jackfrued&quot;,
  &quot;role&quot;: &quot;admin&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;签名&lt;/p&gt;
&lt;p&gt;签名部分是对前面两部分生成一个指纹，防止数据伪造和篡改。实现签名首先需要指定一个密钥。这个密钥只有服务器才知道，不能泄露给用户。然后，使用头部指定的签名算法（默认是&lt;code&gt;HS256&lt;/code&gt;），按照下面的公式产生签名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HS256(base64Encode(header) + &apos;.&apos; + base64Encode(payload), secret)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算出签名以后，把头部、载荷、签名三个部分拼接成一个字符串，每个部分用&lt;code&gt;.&lt;/code&gt;进行分隔，这样一个JWT就生成好了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;JWT的优缺点&lt;/h5&gt;
&lt;p&gt;使用JWT的优点非常明显，包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更容易实现水平扩展，因为令牌保存在浏览器中，服务器不需要做状态管理。&lt;/li&gt;
&lt;li&gt;更容易防范CSRF攻击，因为在请求头中添加&lt;code&gt;localStorage&lt;/code&gt;或&lt;code&gt;sessionStorage&lt;/code&gt;中的token必须靠JavaScript代码完成，而不是自动添加到请求头中的。&lt;/li&gt;
&lt;li&gt;可以防伪造和篡改，因为JWT有签名，伪造和篡改的令牌无法通过签名验证，会被认定是无效的令牌。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，任何技术不可能只有优点没有缺点，JWT也有诸多缺点，大家需要在使用的时候引起注意，具体包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可能会遭受到XSS攻击（跨站脚本攻击），通过注入恶意脚本执行JavaScript代码获取到用户令牌。&lt;/li&gt;
&lt;li&gt;在令牌过期之前，无法作废已经颁发的令牌，要解决这个问题，还需要额外的中间层和代码来辅助。&lt;/li&gt;
&lt;li&gt;JWT是用户的身份令牌，一旦泄露，任何人都可以获得该用户的所有权限。为了降低令牌被盗用后产生的风险，JWT的有效期应该设置得比较短。对于一些比较重要的权限，使用时应通过其他方式再次对用户进行认证，例如短信验证码等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;使用PyJWT&lt;/h5&gt;
&lt;p&gt;在Python代码中，可以使用三方库&lt;code&gt;PyJWT&lt;/code&gt;生成和验证JWT，下面是安装&lt;code&gt;PyJWT&lt;/code&gt;的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyjwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成令牌。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;payload = {
    &apos;exp&apos;: datetime.datetime.utcnow() + datetime.timedelta(days=1),
    &apos;userid&apos;: 10001
}
token = jwt.encode(payload, settings.SECRET_KEY).decode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证令牌。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    token = &apos;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTQ4NzIzOTEsInVzZXJpZCI6MTAwMDF9.FM-bNxemWLqQQBIsRVvc4gq71y42I9m2zt5nlFxNHUo&apos;
    payload = jwt.decode(token, settings.SECRET_KEY)
except InvalidTokenError:
    raise AuthenticationFailed(&apos;无效的令牌或令牌已经过期&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不清楚JWT具体的使用方式，可以先看看第55天的内容，里面提供了完整的投票项目代码的地址。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;55 RESTful架构和DRF进阶&lt;/h2&gt;
&lt;p&gt;除了上一节讲到的方法，使用DRF创建REST风格的数据接口也可以通过CBV（基于类的视图）的方式。使用CBV创建数据接口的特点是代码简单，开发效率高，但是没有FBV（基于函数的视图）灵活，因为使用FBV的方式，数据接口对应的视图函数执行什么样的代码以及返回什么的数据是高度可定制的。下面我们以定制学科的数据接口为例，讲解通过CBV方式定制数据接口的具体做法。&lt;/p&gt;
&lt;h4&gt;使用CBV&lt;/h4&gt;
&lt;h5&gt;继承APIView的子类&lt;/h5&gt;
&lt;p&gt;修改之前项目中的&lt;code&gt;polls/views.py&lt;/code&gt;，去掉&lt;code&gt;show_subjects&lt;/code&gt;视图函数，添加一个名为&lt;code&gt;SubjectView&lt;/code&gt;的类，该类继承自&lt;code&gt;ListAPIView&lt;/code&gt;，&lt;code&gt;ListAPIView&lt;/code&gt;能接收GET请求，它封装了获取数据列表并返回JSON数据的&lt;code&gt;get&lt;/code&gt;方法。&lt;code&gt;ListAPIView&lt;/code&gt;是&lt;code&gt;APIView&lt;/code&gt; 的子类，&lt;code&gt;APIView&lt;/code&gt;还有很多的子类，例如&lt;code&gt;CreateAPIView&lt;/code&gt;可以支持POST请求，&lt;code&gt;UpdateAPIView&lt;/code&gt;可以支持PUT和PATCH请求，&lt;code&gt;DestoryAPIView&lt;/code&gt;可以支持DELETE请求。&lt;code&gt;SubjectView&lt;/code&gt; 的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.generics import ListAPIView


class SubjectView(ListAPIView):
    # 通过queryset指定如何获取学科数据
    queryset = Subject.objects.all()
    # 通过serializer_class指定如何序列化学科数据
    serializer_class = SubjectSerializer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刚才说过，由于&lt;code&gt;SubjectView&lt;/code&gt;的父类&lt;code&gt;ListAPIView&lt;/code&gt;已经实现了&lt;code&gt;get&lt;/code&gt;方法来处理获取学科列表的GET请求，所以我们只需要声明如何获取学科数据以及如何序列化学科数据，前者用&lt;code&gt;queryset&lt;/code&gt;属性指定，后者用&lt;code&gt;serializer_class&lt;/code&gt;属性指定。要使用上面的&lt;code&gt;SubjectView&lt;/code&gt;，需要修改&lt;code&gt;urls.py&lt;/code&gt;文件，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;urlpatterns = [
    path(&apos;api/subjects/&apos;, SubjectView.as_view()),   
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然，上面的做法较之之前讲到的FBV要简单很多。&lt;/p&gt;
&lt;h5&gt;继承ModelViewSet&lt;/h5&gt;
&lt;p&gt;如果学科对应的数据接口需要支持GET、POST、PUT、PATCH、DELETE请求来支持对学科资源的获取、新增、更新、删除操作，更为简单的做法是继承&lt;code&gt;ModelViewSet&lt;/code&gt;来编写学科视图类。再次修改&lt;code&gt;polls/views.py&lt;/code&gt;文件，去掉&lt;code&gt;SubjectView&lt;/code&gt;类，添加一个名为&lt;code&gt;SubjectViewSet&lt;/code&gt;的类，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.viewsets import ModelViewSet


class SubjectViewSet(ModelViewSet):
    queryset = Subject.objects.all()
    serializer_class = SubjectSerializer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过查看&lt;code&gt;ModelViewSet&lt;/code&gt;类的源代码可以发现，该类共有6个父类，其中前5个父类分别实现对POST（新增学科）、GET（获取指定学科）、PUT/PATCH（更新学科）、DELETE（删除学科）和GET（获取学科列表）操作的支持，对应的方法分别是&lt;code&gt;create&lt;/code&gt;、&lt;code&gt;retrieve&lt;/code&gt;、&lt;code&gt;update&lt;/code&gt;、&lt;code&gt;destroy&lt;/code&gt;和&lt;code&gt;list&lt;/code&gt;。由于&lt;code&gt;ModelViewSet&lt;/code&gt;的父类中已经实现了这些方法，所以我们几乎没有编写任何代码就完成了学科数据全套接口的开发，我们要做的仅仅是指出如何获取到数据（通过&lt;code&gt;queryset&lt;/code&gt;属性指定）以及如何序列化数据（通过&lt;code&gt;serializer_class&lt;/code&gt;属性指定），这一点跟上面继承&lt;code&gt;APIView&lt;/code&gt;的子类做法是一致的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    &quot;&quot;&quot;
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    &quot;&quot;&quot;
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要使用上面的&lt;code&gt;SubjectViewSet&lt;/code&gt;，需要在&lt;code&gt;urls.py&lt;/code&gt;文件中进行URL映射。由于&lt;code&gt;ModelViewSet&lt;/code&gt;相当于是多个视图函数的汇总，所以不同于之前映射URL的方式，我们需要先创建一个路由器并通过它注册&lt;code&gt;SubjectViewSet&lt;/code&gt;，然后将注册成功后生成的URL一并添加到&lt;code&gt;urlspattern&lt;/code&gt;列表中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(&apos;api/subjects&apos;, SubjectViewSet)
urlpatterns += router.urls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了&lt;code&gt;ModelViewSet&lt;/code&gt;类外，DRF还提供了一个名为&lt;code&gt;ReadOnlyModelViewSet&lt;/code&gt; 的类，从名字上就可以看出，该类是只读视图的集合，也就意味着，继承该类定制的数据接口只能支持GET请求，也就是获取单个资源和资源列表的请求。&lt;/p&gt;
&lt;h4&gt;数据分页&lt;/h4&gt;
&lt;p&gt;在使用GET请求获取资源列表时，我们通常不会一次性的加载所有的数据，除非数据量真的很小。大多数获取资源列表的操作都支持数据分页展示，也就说我们可以通过指定页码（或类似于页码的标识）和页面大小（一次加载多少条数据）来获取不同的数据。我们可以通过对&lt;code&gt;QuerySet&lt;/code&gt;对象的切片操作来实现分页，也可以利用Django框架封装的&lt;code&gt;Paginator&lt;/code&gt;和&lt;code&gt;Page&lt;/code&gt;对象来实现分页。使用DRF时，可以在Django配置文件中修改&lt;code&gt;REST_FRAMEWORK&lt;/code&gt;并配置默认的分页类和页面大小来实现分页，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REST_FRAMEWORK = {
    &apos;PAGE_SIZE&apos;: 10,
    &apos;DEFAULT_PAGINATION_CLASS&apos;: &apos;rest_framework.pagination.PageNumberPagination&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了上面配置的&lt;code&gt;PageNumberPagination&lt;/code&gt;分页器之外，DRF还提供了&lt;code&gt;LimitOffsetPagination&lt;/code&gt;和&lt;code&gt;CursorPagination&lt;/code&gt;分页器，值得一提的是&lt;code&gt;CursorPagination&lt;/code&gt;，它可以避免使用页码分页时暴露网站的数据体量，有兴趣的读者可以自行了解。如果不希望使用配置文件中默认的分页设定，可以在视图类中添加一个&lt;code&gt;pagination_class&lt;/code&gt;属性来重新指定分页器，通常可以将该属性指定为自定义的分页器，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from rest_framework.pagination import PageNumberPagination


class CustomizedPagination(PageNumberPagination):
    # 默认页面大小
    page_size = 5
    # 页面大小对应的查询参数
    page_size_query_param = &apos;size&apos;
    # 页面大小的最大值
    max_page_size = 50
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class SubjectView(ListAPIView):
    # 指定如何获取数据
    queryset = Subject.objects.all()
    # 指定如何序列化数据
    serializer_class = SubjectSerializer
    # 指定如何分页
    pagination_class = CustomizedPagination
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不希望数据分页，可以将&lt;code&gt;pagination_class&lt;/code&gt;属性设置为&lt;code&gt;None&lt;/code&gt;来取消默认的分页器。&lt;/p&gt;
&lt;h4&gt;数据筛选&lt;/h4&gt;
&lt;p&gt;如果希望使用CBV定制获取老师信息的数据接口，也可以通过继承&lt;code&gt;ListAPIView&lt;/code&gt;来实现。但是因为要通过指定的学科来获取对应的老师信息，因此需要对老师数据进行筛选而不是直接获取所有老师的数据。如果想从请求中获取学科编号并通过学科编号对老师进行筛选，可以通过重写&lt;code&gt;get_queryset&lt;/code&gt;方法来做到，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TeacherView(ListAPIView):
    serializer_class = TeacherSerializer

    def get_queryset(self):
        queryset = Teacher.objects.defer(&apos;subject&apos;)
        try:
            sno = self.request.GET.get(&apos;sno&apos;, &apos;&apos;)
            queryset = queryset.filter(subject__no=sno)
            return queryset
        except ValueError:
            raise Http404(&apos;No teachers found.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了上述方式之外，还可以使用三方库&lt;code&gt;django-filter&lt;/code&gt;来配合DRF实现对数据的筛选，使用&lt;code&gt;django-filter&lt;/code&gt;后，可以通过为视图类配置&lt;code&gt;filter-backends&lt;/code&gt;属性并指定使用&lt;code&gt;DjangoFilterBackend&lt;/code&gt;来支持数据筛选。在完成上述配置后，可以使用&lt;code&gt;filter_fields&lt;/code&gt; 属性或&lt;code&gt;filterset_class&lt;/code&gt;属性来指定如何筛选数据，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;56 使用缓存&lt;/h2&gt;
&lt;p&gt;通常情况下，Web应用的性能瓶颈都会出现在关系型数据库上，当并发访问量较大时，如果所有的请求都需要通过关系型数据库完成数据持久化操作，那么数据库一定会不堪重负。优化Web应用性能最为重要的一点就是使用缓存，把那些数据体量不大但访问频率非常高的数据提前加载到缓存服务器中，这又是典型的空间换时间的方法。通常缓存服务器都是直接将数据置于内存中而且使用了非常高效的数据存取策略（哈希存储、键值对方式等），在读写性能上远远优于关系型数据库的，因此我们可以让Web应用接入缓存服务器来优化其性能，其中一个非常好的选择就是使用Redis。&lt;/p&gt;
&lt;p&gt;Web应用的缓存架构大致如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/e1f3ae107a.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Django项目接入Redis&lt;/h4&gt;
&lt;p&gt;在此前的课程中，我们介绍过Redis的安装和使用，此处不再进行赘述。如果需要在Django项目中接入Redis，可以使用三方库&lt;code&gt;django-redis&lt;/code&gt;，这个三方库又依赖了一个名为&lt;code&gt;redis&lt;/code&gt; 的三方库，它封装了对Redis的各种操作。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;django-redis&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改Django配置文件中关于缓存的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CACHES = {
    &apos;default&apos;: {
        # 指定通过django-redis接入Redis服务
        &apos;BACKEND&apos;: &apos;django_redis.cache.RedisCache&apos;,
        # Redis服务器的URL
        &apos;LOCATION&apos;: [&apos;redis://1.2.3.4:6379/0&apos;, ],
        # Redis中键的前缀（解决命名冲突）
        &apos;KEY_PREFIX&apos;: &apos;vote&apos;,
        # 其他的配置选项
        &apos;OPTIONS&apos;: {
            &apos;CLIENT_CLASS&apos;: &apos;django_redis.client.DefaultClient&apos;,
            # 连接池（预置若干备用的Redis连接）参数
            &apos;CONNECTION_POOL_KWARGS&apos;: {
                # 最大连接数
                &apos;max_connections&apos;: 512,
            },
            # 连接Redis的用户口令
            &apos;PASSWORD&apos;: &apos;foobared&apos;,
        }
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们的Django项目已经可以接入Redis，接下来我们修改项目代码，用Redis为之写的获取学科数据的接口提供缓存服务。&lt;/p&gt;
&lt;h4&gt;为视图提供缓存服务&lt;/h4&gt;
&lt;h5&gt;声明式缓存&lt;/h5&gt;
&lt;p&gt;所谓声明式缓存是指不修改原来的代码，通过Python中的装饰器（代理）为原有的代码增加缓存功能。对于FBV，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.views.decorators.cache import cache_page


@api_view((&apos;GET&apos;, ))
@cache_page(timeout=86400, cache=&apos;default&apos;)
def show_subjects(request):
    &quot;&quot;&quot;获取学科数据&quot;&quot;&quot;
    queryset = Subject.objects.all()
    data = SubjectSerializer(queryset, many=True).data
    return Response({&apos;code&apos;: 20000, &apos;subjects&apos;: data})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码通过Django封装的&lt;code&gt;cache_page&lt;/code&gt;装饰器缓存了视图函数的返回值（响应对象），&lt;code&gt;cache_page&lt;/code&gt;的本意是缓存视图函数渲染的页面，对于返回JSON数据的视图函数，相当于是缓存了JSON数据。在使用&lt;code&gt;cache_page&lt;/code&gt;装饰器时，可以传入&lt;code&gt;timeout&lt;/code&gt;参数来指定缓存过期时间，还可以使用&lt;code&gt;cache&lt;/code&gt;参数来指定需要使用哪一组缓存服务来缓存数据。Django项目允许在配置文件中配置多组缓存服务，上面的&lt;code&gt;cache=&apos;default&apos;&lt;/code&gt;指定了使用默认的缓存服务（因为之前的配置文件中我们也只配置了名为&lt;code&gt;default&lt;/code&gt;的缓存服务）。视图函数的返回值会被序列化成字节串放到Redis中（Redis中的str类型可以接收字节串），缓存数据的序列化和反序列化也不需要我们自己处理，因为&lt;code&gt;cache_page&lt;/code&gt;装饰器会调用&lt;code&gt;django-redis&lt;/code&gt;库中的&lt;code&gt;RedisCache&lt;/code&gt;来对接Redis，该类使用了&lt;code&gt;DefaultClient&lt;/code&gt;来连接Redis并使用了&lt;a href=&quot;https://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p21_serializing_python_objects.html&quot;&gt;pickle序列化&lt;/a&gt;，&lt;code&gt;django_redis.serializers.pickle.PickleSerializer&lt;/code&gt;是默认的序列化类。&lt;/p&gt;
&lt;p&gt;如果缓存中没有学科的数据，那么通过接口访问学科数据时，我们的视图函数会通过执行&lt;code&gt;Subject.objects.all()&lt;/code&gt;向数据库发出SQL语句来获得数据，视图函数的返回值会被缓存，因此下次请求该视图函数如果缓存没有过期，可以直接从缓存中获取视图函数的返回值，无需再次查询数据库。如果想了解缓存的使用情况，可以配置数据库日志或者使用Django-Debug-Toolbar来查看，第一次访问学科数据接口时会看到查询学科数据的SQL语句，再次获取学科数据时，不会再向数据库发出SQL语句，因为可以直接从缓存中获取数据。&lt;/p&gt;
&lt;p&gt;对于CBV，可以利用Django中名为&lt;code&gt;method_decorator&lt;/code&gt;的装饰器将&lt;code&gt;cache_page&lt;/code&gt;这个装饰函数的装饰器放到类中的方法上，效果跟上面的代码是一样的。需要提醒大家注意的是，&lt;code&gt;cache_page&lt;/code&gt;装饰器不能直接放在类上，因为它是装饰函数的装饰器，所以Django框架才提供了&lt;code&gt;method_decorator&lt;/code&gt;来解决这个问题，很显然，&lt;code&gt;method_decorator&lt;/code&gt;是一个装饰类的装饰器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page


@method_decorator(decorator=cache_page(timeout=86400, cache=&apos;default&apos;), name=&apos;get&apos;)
class SubjectView(ListAPIView):
    &quot;&quot;&quot;获取学科数据的视图类&quot;&quot;&quot;
    queryset = Subject.objects.all()
    serializer_class = SubjectSerializer
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;编程式缓存&lt;/h5&gt;
&lt;p&gt;所谓编程式缓存是指通过自己编写的代码来使用缓存服务，这种方式虽然代码量会稍微大一些，但是相较于声明式缓存，它对缓存的操作和使用更加灵活，在实际开发中使用得更多。下面的代码去掉了之前使用的&lt;code&gt;cache_page&lt;/code&gt;装饰器，通过&lt;code&gt;django-redis&lt;/code&gt;提供的&lt;code&gt;get_redis_connection&lt;/code&gt;函数直接获取Redis连接来操作Redis。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def show_subjects(request):
    &quot;&quot;&quot;获取学科数据&quot;&quot;&quot;
    redis_cli = get_redis_connection()
    # 先尝试从缓存中获取学科数据
    data = redis_cli.get(&apos;vote:polls:subjects&apos;)
    if data:
        # 如果获取到学科数据就进行反序列化操作
        data = json.loads(data)
    else:
        # 如果缓存中没有获取到学科数据就查询数据库
        queryset = Subject.objects.all()
        data = SubjectSerializer(queryset, many=True).data
        # 将查到的学科数据序列化后放到缓存中
        redis_cli.set(&apos;vote:polls:subjects&apos;, json.dumps(data), ex=86400)
    return Response({&apos;code&apos;: 20000, &apos;subjects&apos;: data})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，Django框架提供了&lt;code&gt;cache&lt;/code&gt;和&lt;code&gt;caches&lt;/code&gt;两个现成的变量来支持缓存操作，前者访问的是默认的缓存（名为&lt;code&gt;default&lt;/code&gt;的缓存），后者可以通过索引运算获取指定的缓存服务（例如：&lt;code&gt;caches[&apos;default&apos;]&lt;/code&gt;）。向&lt;code&gt;cache&lt;/code&gt;对象发送&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;消息就可以实现对缓存的读和写操作，但是这种方式能做的操作有限，不如上面代码中使用的方式灵活。还有一个值得注意的地方，由于可以通过&lt;code&gt;get_redis_connection&lt;/code&gt;函数获得的Redis连接对象向Redis发起各种操作，包括&lt;code&gt;FLUSHDB&lt;/code&gt;、&lt;code&gt;SHUTDOWN&lt;/code&gt;等危险的操作，所以在实际商业项目开发中，一般都会对&lt;code&gt;django-redis&lt;/code&gt;再做一次封装，例如封装一个工具类，其中只提供了项目需要用到的缓存操作的方法，从而避免了直接使用&lt;code&gt;get_redis_connection&lt;/code&gt;的潜在风险。当然，自己封装对缓存的操作还可以使用“Read Through”和“Write Through”的方式实现对缓存的更新，这个在下面会介绍到。&lt;/p&gt;
&lt;h4&gt;缓存相关问题&lt;/h4&gt;
&lt;h5&gt;缓存数据的更新&lt;/h5&gt;
&lt;p&gt;在使用缓存时，一个必须搞清楚的问题就是，当数据改变时，如何更新缓存中的数据。通常更新缓存有如下几种套路，分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cache Aside Pattern&lt;/li&gt;
&lt;li&gt;Read/Write Through Pattern&lt;/li&gt;
&lt;li&gt;Write Behind Caching Pattern&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第1种方式的具体做法就是，当数据更新时，先更新数据库，再删除缓存。注意，不能够使用先更新数据库再更新缓存的方式，也不能够使用先删除缓存再更新数据库的方式，大家可以自己想一想为什么（考虑一下有并发的读操作和写操作的场景）。当然，先更新数据库再删除缓存的做法在理论上也存在风险，但是发生问题的概率是极低的，所以不少的项目都使用了这种方式。&lt;/p&gt;
&lt;p&gt;第1种方式相当于编写业务代码的开发者要自己负责对两套存储系统（缓存和关系型数据库）的操作，代码写起来非常的繁琐。第2种方式的主旨是将后端的存储系统变成一套代码，对缓存的维护封装在这套代码中。其中，Read Through指在查询操作中更新缓存，也就是说，当缓存失效的时候，由缓存服务自己负责对数据的加载，从而对应用方是透明的；而Write Through是指在更新数据时，如果没有命中缓存，直接更新数据库，然后返回。如果命中了缓存，则更新缓存，然后再由缓存服务自己更新数据库（同步更新）。刚才我们说过，如果自己对项目中的Redis操作再做一次封装，就可以实现“Read Through”和“Write Through”模式，这样做虽然会增加工作量，但无疑是一件“一劳永逸”且“功在千秋”的事情。&lt;/p&gt;
&lt;p&gt;第3种方式是在更新数据的时候，只更新缓存，不更新数据库，而缓存服务这边会&lt;strong&gt;异步的批量更新&lt;/strong&gt;数据库。这种做法会大幅度提升性能，但代价是牺牲数据的&lt;strong&gt;强一致性&lt;/strong&gt;。第3种方式的实现逻辑比较复杂，因为他需要追踪有哪数据是被更新了的，然后再批量的刷新到持久层上。&lt;/p&gt;
&lt;h5&gt;缓存穿透&lt;/h5&gt;
&lt;p&gt;缓存是为了缓解数据库压力而添加的一个中间层，如果恶意的访问者频繁的访问缓存中没有的数据，那么缓存就失去了存在的意义，瞬间所有请求的压力都落在了数据库上，这样会导致数据库承载着巨大的压力甚至连接异常，类似于分布式拒绝服务攻击（DDoS）的做法。解决缓存穿透的一个办法是约定如果查询返回为空值，把这个空值也缓存起来，但是需要为这个空值的缓存设置一个较短的超时时间，毕竟缓存这样的值就是对缓存空间的浪费。另一个解决缓存穿透的办法是使用布隆过滤器，具体的做法大家可以自行了解。&lt;/p&gt;
&lt;h5&gt;缓存击穿&lt;/h5&gt;
&lt;p&gt;在实际的项目中，可能存在某个缓存的key某个时间点过期，但恰好在这个时间点对有对该key的大量的并发请求过来，这些请求没有从缓存中找到key对应的数据，就会直接从数据库中获取数据并写回到缓存，这个时候大并发的请求可能会瞬间把数据库压垮，这种现象称为缓存击穿。比较常见的解决缓存击穿的办法是使用互斥锁，简单的说就是在缓存失效的时候，不是立即去数据库加载数据，而是先设置互斥锁（例如：Redis中的setnx），只有设置互斥锁的操作成功的请求，才能执行查询从数据库中加载数据并写入缓存，其他设置互斥锁失败的请求，可以先执行一个短暂的休眠，然后尝试重新从缓存中获取数据，如果缓存还没有数据，则重复刚才的设置互斥锁的操作，大致的参考代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = redis_cli.get(key)
while not data:
    if redis_cli.setnx(&apos;mutex&apos;, &apos;x&apos;):
        redis.expire(&apos;mutex&apos;, timeout)
        data = db.query(...)
        redis.set(key, data)
        redis.delete(&apos;mutex&apos;)
    else:
        time.sleep(0.1)
        data = redis_cli.get(key)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;缓存雪崩&lt;/h5&gt;
&lt;p&gt;缓存雪崩是指在将数据放入缓存时采用了相同的过期时间，这样就导致缓存在某一时刻同时失效，请求全部转发到数据库，导致数据库瞬时压力过大而崩溃。解决缓存雪崩问题的方法也比较简单，可以在既定的缓存过期时间上加一个随机时间，这样可以从一定程度上避免不同的key在同一时间集体失效。还有一种办法就是使用多级缓存，每一级缓存的过期时间都不一样，这样的话即便某个级别的缓存集体失效，但是其他级别的缓存还能够提供数据，避免所有的请求都落到数据库上。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;57 接入三方平台&lt;/h2&gt;
&lt;p&gt;在Web应用的开发过程中，有一些任务并不是我们自己能够完成的。例如，我们的Web项目中需要做个人或企业的实名认证，很显然我们并没有能力判断用户提供的认证信息的真实性，这个时候我们就要借助三方平台提供的服务来完成该项操作。再比如说，我们的项目中需要提供在线支付功能，这类业务通常也是借助支付网关来完成而不是自己去实现，我们只需要接入像微信、支付宝、银联这样的三方平台即可。&lt;/p&gt;
&lt;p&gt;在项目中接入三方平台基本上就两种方式：API接入和SDK接入。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;API接入指的是通过访问三方提供的URL来完成操作或获取数据。国内有很多这样的平台提供了大量常用的服务，例如&lt;a href=&quot;https://www.juhe.cn/&quot;&gt;聚合数据&lt;/a&gt;上提供了生活服务类、金融科技类、交通地理类、充值缴费类等各种类型的API。我们可以通过Python程序发起网络请求，通过访问URL获取数据，这些API接口跟我们项目中提供的数据接口是一样的，只不过我们项目中的API是供自己使用的，而这类三方平台提供的API是开放的。当然开放并不代表免费，大多数能够提供有商业价值的数据的API都是需要付费才能使用的。&lt;/li&gt;
&lt;li&gt;SDK接入指的是通过安装三方库并使用三方库封装的类、函数来使用三方平台提供的服务的方式。例如我们刚才说到的接入支付宝，就需要先安装支付宝的SDK，然后通过支付宝封装的类和方法完成对支付服务的调用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们通过具体的例子来讲解如何接入三方平台。&lt;/p&gt;
&lt;h4&gt;接入短信网关&lt;/h4&gt;
&lt;p&gt;一个Web项目有很多地方都可以用到短信服务，例如：手机验证码登录、重要消息提醒、产品营销短信等。要实现发送短信的功能，可以通过接入短信网关来实现，国内比较有名的短信网关包括：云片短信、网易云信、螺丝帽、SendCloud等，这些短信网关一般都提供了免费试用功能。下面我们以&lt;a href=&quot;https://luosimao.com/&quot;&gt;螺丝帽&lt;/a&gt;平台为例，讲解如何在项目中接入短信网关，其他平台操作基本类似。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;注册账号，新用户可以免费试用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录到管理后台，进入短信版块。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击“触发发送”可以找到自己专属的API Key（身份标识）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/0c2325c766.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击“签名管理”可以添加短信签名，短信都必须携带签名，免费试用的短信要在短信中添加“【铁壳测试】”这个签名，否则短信无法发送。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/dffb5d592f.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击“IP白名单”将运行Django项目的服务器地址（公网IP地址，本地运行可以打开&lt;a&gt;xxx&lt;/a&gt;网站查看自己本机的公网IP地址）填写到白名单中，否则短信无法发送。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/0916065ed4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有剩余的短信条数，可以到“充值”页面选择“短信服务”进行充值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/0523cd3e56.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，我们可以通过调用螺丝帽短信网关实现发送短信验证码的功能，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def send_mobile_code(tel, code):
    &quot;&quot;&quot;发送短信验证码&quot;&quot;&quot;
    resp = requests.post(
        url=&apos;http://sms-api.luosimao.com/v1/send.json&apos;,
        auth=(&apos;api&apos;, &apos;key-自己的APIKey&apos;),
        data={
            &apos;mobile&apos;: tel,
            &apos;message&apos;: f&apos;您的短信验证码是{code}，打死也不能告诉别人哟。【Python小课】&apos;
        },
        verify=False
    )
    return resp.json()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码需要先安装&lt;code&gt;requests&lt;/code&gt;三方库，这个三方库封装了HTTP网络请求的相关功能，使用起来非常的简单，我们在之前的内容中也讲到过这个三方库。&lt;code&gt;send_mobile_code&lt;/code&gt;函数有两个参数，第一个参数是手机号，第二个参数是短信验证码的内容，第5行代码需要提供自己的API Key，就是上面第2步中查看到的自己的API Key。请求螺丝帽的短信网关会返回JSON格式的数据，对于上面的代码如果返回&lt;code&gt;{&apos;err&apos;: 0, &apos;msg&apos;: &apos;ok&apos;}&lt;/code&gt;，则表示短信发送成功，如果&lt;code&gt;err&lt;/code&gt;字段的值不为&lt;code&gt;0&lt;/code&gt;而是其他值，则表示短信发送失败，可以在螺丝帽官方的&lt;a href=&quot;https://luosimao.com/docs/api/&quot;&gt;开发文档&lt;/a&gt;页面上查看到不同的数值代表的含义，例如：&lt;code&gt;-20&lt;/code&gt;表示余额不足，&lt;code&gt;-32&lt;/code&gt;表示缺少短信签名。&lt;/p&gt;
&lt;p&gt;可以在视图函数中调用上面的函数来完成发送短信验证码的功能，稍后我们可以把这个功能跟用户注册结合起来。&lt;/p&gt;
&lt;p&gt;生成随机验证码和验证手机号的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import re

TEL_PATTERN = re.compile(r&apos;1[3-9]\d{9}&apos;)


def check_tel(tel):
    &quot;&quot;&quot;检查手机号&quot;&quot;&quot;
    return TEL_PATTERN.fullmatch(tel) is not None


def random_code(length=6):
    &quot;&quot;&quot;生成随机短信验证码&quot;&quot;&quot;
    return &apos;&apos;.join(random.choices(&apos;0123456789&apos;, k=length))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送短信验证码的视图函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@api_view((&apos;GET&apos;, ))
def get_mobilecode(request, tel):
    &quot;&quot;&quot;获取短信验证码&quot;&quot;&quot;
    if check_tel(tel):
        redis_cli = get_redis_connection()
        if redis_cli.exists(f&apos;vote:block-mobile:{tel}&apos;):
            data = {&apos;code&apos;: 30001, &apos;message&apos;: &apos;请不要在60秒内重复发送短信验证码&apos;}
        else:
            code = random_code()
            send_mobile_code(tel, code)
            # 通过Redis阻止60秒内容重复发送短信验证码
            redis_cli.set(f&apos;vote:block-mobile:{tel}&apos;, &apos;x&apos;, ex=60)
            # 将验证码在Redis中保留10分钟（有效期10分钟）
            redis_cli.set(f&apos;vote2:valid-mobile:{tel}&apos;, code, ex=600)
            data = {&apos;code&apos;: 30000, &apos;message&apos;: &apos;短信验证码已发送，请注意查收&apos;}
    else:
        data = {&apos;code&apos;: 30002, &apos;message&apos;: &apos;请输入有效的手机号&apos;}
    return Response(data)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码利用Redis实现了两个额外的功能，一个是阻止用户60秒内重复发送短信验证码，一个是将用户的短信验证码保留10分钟，也就是说这个短信验证码的有效期只有10分钟，我们可以要求用户在注册时提供该验证码来验证用户手机号的真实性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;接入云存储服务&lt;/h4&gt;
&lt;p&gt;当我们提到&lt;strong&gt;云存储&lt;/strong&gt;这个词的时候，通常是指把数据存放在由第三方提供的虚拟服务器环境下，简单的说就是将某些数据或资源通过第三平台托管。一般情况下，提供云存储服务的公司都运营着大型的数据中心，需要云存储服务的个人或组织通过向其购买或租赁存储空间来满足数据存储的需求。在开发Web应用时，可以将静态资源，尤其是用户上传的静态资源直接置于云存储服务中，云存储通常会提供对应的URL使得用户可以访问该静态资源。国内外比较有名的云存储服务（如：亚马逊的S3、阿里的OSS2等）一般都物美价廉，相比自己架设静态资源服务器，云存储的代价更小，而且一般的云存储平台都提供了CDN服务，用于加速对静态资源的访问，所以不管从哪个角度出发，使用云存储的方式管理Web应用的数据和静态资源都是非常好的选择，除非这些资源涉及到个人或商业隐私，否则就可以托管到云存储中。&lt;/p&gt;
&lt;p&gt;下面我们以接入&lt;a href=&quot;https://www.qiniu.com/&quot;&gt;七牛云&lt;/a&gt;为例，讲解如何实现将用户上传的文件保存到七牛云存储。七牛云是国内知名的云计算及数据服务提供商，七牛云在海量文件存储、CDN、视频点播、互动直播以及大规模异构数据的智能分析与处理等领域都有自己的产品，而且非付费用户也可以免费接入，使用其提供的服务。下面是接入七牛云的流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;注册账号，登录管理控制台。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/1b02b1d0e7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择左侧菜单中的对象存储。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/ec08542a8f.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在空间管理中选择新建空间（例如：myvote），如果提示空间名称已被占用，更换一个再尝试即可。注意，创建空间后会提示绑定自定义域名，如果暂时还没有自己的域名，可以使用七牛云提供的临时域名，但是临时域名会在30天后被回收，所以最好准备自己的域名（域名需要备案，不清楚如何操作的请自行查阅相关资料）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/f74ac289d4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在网页的右上角点击个人头像中的“密钥管理”，查看自己的密钥，稍后在代码中需要使用AK（AccessKey）和SK（SecretKey）两个密钥来认证用户身份。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/d17e31c6a7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击网页上方菜单中的“文档”，进入到&lt;a href=&quot;https://developer.qiniu.com/&quot;&gt;七牛开发者中心&lt;/a&gt;，选择导航菜单中的“SDK&amp;amp;工具”并点击“官方SDK”子菜单，找到Python（服务端）并点击“文档”查看官方文档。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/e8824988b4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，只要安装官方文档提供的示例，就可以接入七牛云，使用七牛云提供的云存储以及其他服务。首先可以通过下面的命令安装七牛云的三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install qiniu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来可以通过&lt;code&gt;qiniu&lt;/code&gt;模块中的&lt;code&gt;put_file&lt;/code&gt;和&lt;code&gt;put_stream&lt;/code&gt;两个函数实现文件上传，前者可以上传指定路径的文件，后者可以将内存中的二进制数据上传至七牛云，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import qiniu

AUTH = qiniu.Auth(&apos;密钥管理中的AccessKey&apos;, &apos;密钥管理中的SecretKey&apos;)
BUCKET_NAME = &apos;myvote&apos;


def upload_file_to_qiniu(key, file_path):
    &quot;&quot;&quot;上传指定路径的文件到七牛云&quot;&quot;&quot;
    token = AUTH.upload_token(BUCKET_NAME, key)
    return qiniu.put_file(token, key, file_path)


def upload_stream_to_qiniu(key, stream, size):
    &quot;&quot;&quot;上传二进制数据流到七牛云&quot;&quot;&quot;
    token = AUTH.upload_token(BUCKET_NAME, key)
    return qiniu.put_stream(token, key, stream, None, size)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是一个文件上传的简单前端页。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;上传文件&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;/upload/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;input type=&quot;file&quot; name=&quot;photo&quot;&amp;gt;
            &amp;lt;input type=&quot;submit&quot; value=&quot;上传&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：前端如果使用表单实现文件上传，表单的method属性必须设置为post，enctype属性需要设置为multipart/form-data，表单中type属性为file的input标签，就是上传文件的文件选择器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实现上传功能的视图函数如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def upload(request):
    # 如果上传的文件小于2.5M，则photo对象的类型为InMemoryUploadedFile，文件在内存中
    # 如果上传的文件超过2.5M，则photo对象的类型为TemporaryUploadedFile，文件在临时路径下
    photo = request.FILES.get(&apos;photo&apos;)
    _, ext = os.path.splitext(photo.name)
    # 通过UUID和原来文件的扩展名生成独一无二的新的文件名
    filename = f&apos;{uuid.uuid1().hex}{ext}&apos;
    # 对于内存中的文件，可以使用上面封装好的函数upload_stream_to_qiniu上传文件到七牛云
    # 如果文件保存在临时路径下，可以使用upload_file_to_qiniu实现文件上传
    upload_stream_to_qiniu(filename, photo.file, photo.size)
    return redirect(&apos;/static/html/upload.html&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的视图函数使用了&lt;code&gt;csrf_exempt&lt;/code&gt;装饰器，该装饰器能够让表单免除必须提供CSRF令牌的要求。此外，代码第11行使用了&lt;code&gt;uuid&lt;/code&gt;模块的&lt;code&gt;uuid1&lt;/code&gt;函数来生成全局唯一标识符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;运行项目尝试文件上传的功能，文件上传成功后，可以在七牛云“空间管理”中点击自己空间并进入“文件管理”界面，在这里可以看到我们刚才上传成功的文件，而且可以通过七牛云提供的域名获取该文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day46-60/e0efd35195.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;58 异步任务和定时任务&lt;/h2&gt;
&lt;p&gt;在Web应用中，如果一个请求执行了耗时间的操作或者该请求的执行时间无法确定，而且对于用户来说只需要知道服务器接收了他的请求，并不需要马上得到请求的执行结果，这样的操作我们就应该对其进行异步化处理。如果说&lt;strong&gt;使用缓存是优化网站性能的第一要义&lt;/strong&gt;，那么将耗时间或执行时间不确定的任务&lt;strong&gt;异步化则是网站性能优化的第二要义&lt;/strong&gt;，简单的说就是&lt;strong&gt;能推迟做的事情都不要马上做&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;上一章节中讲到的发短信和上传文件到云存储为例，这两个操作前者属于时间不确定的操作（因为作为调用者，我们不能确定三方平台响应的时间），后者属于耗时间的操作（如果文件较大或者三方平台不稳定，都可能导致上传的时间较长），很显然，这两个操作都可以做异步化处理。&lt;/p&gt;
&lt;p&gt;在 Python 项目中，我们可以使用三方库Celery来完成异步任务和定时任务，关于Celery的内容，请移步到&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/95.%E4%BD%BF%E7%94%A8Django%E5%BC%80%E5%8F%91%E5%95%86%E4%B8%9A%E9%A1%B9%E7%9B%AE.md&quot;&gt;《使用Django开发商业项目》&lt;/a&gt;一文。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;59 单元测试&lt;/h2&gt;
&lt;p&gt;请各位读者移步到&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/95.%E4%BD%BF%E7%94%A8Django%E5%BC%80%E5%8F%91%E5%95%86%E4%B8%9A%E9%A1%B9%E7%9B%AE.md&quot;&gt;《使用Django开发商业项目》&lt;/a&gt;一文。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;60 项目上线&lt;/h2&gt;
&lt;p&gt;请各位读者移步到&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/98.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF%E5%92%8C%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98.md&quot;&gt;《项目部署上线和性能调优》&lt;/a&gt;一文。&lt;/p&gt;
</content:encoded></item><item><title>Day36-45 教程全文（10 篇合并）</title><link>https://www.klucen.cn/posts/python/day36-45-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day36-45-full/</guid><pubDate>Sun, 14 Dec 2025 00:13:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 10 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：关系型数据库与 SQL
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#36-%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93%E5%92%8Cmysql%E6%A6%82%E8%BF%B0&quot;&gt;36 关系型数据库和MySQL概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#37-sql%E8%AF%A6%E8%A7%A3%E4%B9%8Bddl&quot;&gt;37 SQL详解之DDL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#38-sql%E8%AF%A6%E8%A7%A3%E4%B9%8Bdml&quot;&gt;38 SQL详解之DML&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#39-sql%E8%AF%A6%E8%A7%A3%E4%B9%8Bdql&quot;&gt;39 SQL详解之DQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#40-sql%E8%AF%A6%E8%A7%A3%E4%B9%8Bdcl&quot;&gt;40 SQL详解之DCL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#41-mysql%E6%96%B0%E7%89%B9%E6%80%A7&quot;&gt;41 MySQL新特性&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#42-%E8%A7%86%E5%9B%BE%E5%87%BD%E6%95%B0%E5%92%8C%E8%BF%87%E7%A8%8B&quot;&gt;42 视图、函数和过程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#43-%E7%B4%A2%E5%BC%95&quot;&gt;43 索引&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#44-python%E6%8E%A5%E5%85%A5mysql%E6%95%B0%E6%8D%AE%E5%BA%93&quot;&gt;44 Python接入MySQL数据库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#45-hive%E5%AE%9E%E6%88%98&quot;&gt;45 Hive实战&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;36 关系型数据库和MySQL概述&lt;/h2&gt;
&lt;h4&gt;关系型数据库概述&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数据持久化 - 将数据保存到能够长久保存数据的存储介质中，在掉电的情况下数据也不会丢失。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据库发展史 - 网状数据库、层次数据库、关系数据库、NoSQL 数据库、NewSQL 数据库。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1970年，IBM的研究员E.F.Codd在&lt;em&gt;Communication of the ACM&lt;/em&gt;上发表了名为&lt;em&gt;A Relational Model of Data for Large Shared Data Banks&lt;/em&gt;的论文，提出了&lt;strong&gt;关系模型&lt;/strong&gt;的概念，奠定了关系模型的理论基础。后来Codd又陆续发表多篇文章，论述了范式理论和衡量关系系统的12条标准，用数学理论奠定了关系数据库的基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关系数据库特点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理论基础：&lt;strong&gt;关系代数&lt;/strong&gt;（集合论、一阶谓词、关系运算）。&lt;/li&gt;
&lt;li&gt;具体表象：用&lt;strong&gt;二维表&lt;/strong&gt;（有行和列）组织数据。&lt;/li&gt;
&lt;li&gt;编程语言：&lt;strong&gt;结构化查询语言&lt;/strong&gt;（SQL）。
&lt;ul&gt;
&lt;li&gt;DDL：数据定义语言&lt;/li&gt;
&lt;li&gt;DML：数据操作语言&lt;/li&gt;
&lt;li&gt;DCL：数据控制语言&lt;/li&gt;
&lt;li&gt;TCL：事务控制语言&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ER模型（实体关系模型）和概念模型图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ER模型&lt;/strong&gt;，全称为&lt;strong&gt;实体关系模型&lt;/strong&gt;（Entity-Relationship Model），由美籍华裔计算机科学家陈品山先生提出，是概念数据模型的高层描述方式，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/2bdabadc46.webp&quot; width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实体 - 矩形框&lt;/li&gt;
&lt;li&gt;属性 - 椭圆框&lt;/li&gt;
&lt;li&gt;关系 - 菱形框&lt;/li&gt;
&lt;li&gt;重数 - 1:1（一对一） / 1:N（一对多） / M:N（多对多）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际项目开发中，我们可以利用数据库建模工具（如：PowerDesigner）来绘制概念数据模型，然后再设置好目标数据库系统，将概念模型转换成物理模型（如下图所示），最终生成创建二维表的 SQL（很多工具都可以根据我们设计的物理模型图以及设定的目标数据库来导出 SQL 或直接生成数据表）。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/c53b202a10.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关系数据库产品。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/index.html&quot;&gt;Oracle&lt;/a&gt; - 目前世界上使用最为广泛的数据库管理系统，作为一个通用的数据库系统，它具有完整的数据管理功能；作为一个关系数据库，它是一个完备关系的产品；作为分布式数据库，它实现了分布式处理的功能。在 Oracle 较新的版本中，还引入了多承租方架构，使用该架构可轻松部署和管理数据库云。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ibm.com/analytics/us/en/db2/&quot;&gt;DB2&lt;/a&gt; - IBM 公司开发的、主要运行于 Unix（包括 IBM 自家的 &lt;a href=&quot;https://zh.wikipedia.org/wiki/AIX&quot;&gt;AIX&lt;/a&gt;）、Linux、以及 Windows 服务器版等系统的关系数据库产品。DB2 历史悠久且被认为是最早使用 SQL 的数据库产品，它拥有较为强大的商业智能功能。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/sql-server/&quot;&gt;SQL Server&lt;/a&gt; - 由 Microsoft 开发和推广的关系型数据库产品，最初适用于中小企业的数据管理，但是近年来它的应用范围有所扩展，部分大企业甚至是跨国公司也开始基于它来构建自己的数据管理系统。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mysql.com/&quot;&gt;MySQL&lt;/a&gt; - MySQL 是开放源代码的，任何人都可以在 GPL（General Public License）的许可下下载并根据个性化的需要对其进行修改。MySQL 因为其速度、可靠性和适应性而备受关注。&lt;/li&gt;
&lt;li&gt;&lt;a&gt;PostgreSQL&lt;/a&gt; - 在 BSD 许可证下发行的开放源代码的关系数据库产品。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;MySQL 简介&lt;/h4&gt;
&lt;p&gt;MySQL 最早是由瑞典的 MySQL AB 公司开发的一个开放源码的关系数据库管理系统，该公司于2008年被昇阳微系统公司（Sun Microsystems）收购。在2009年，甲骨文公司（Oracle）收购昇阳微系统公司，因此 MySQL 目前也是 Oracle 旗下产品。&lt;/p&gt;
&lt;p&gt;MySQL 在过去由于性能高、成本低、可靠性好，已经成为最流行的开源数据库，因此被广泛地应用于中小型网站开发。随着 MySQL 的不断成熟，它也逐渐被应用于更多大规模网站和应用，比如维基百科、谷歌（Google）、脸书（Facebook）、百度、淘宝、腾讯、新浪、去哪儿等都使用了 MySQL 来提供数据持久化服务。&lt;/p&gt;
&lt;p&gt;甲骨文公司收购后昇阳微系统公司，大幅调涨 MySQL 商业版的售价，且甲骨文公司不再支持另一个自由软件项目 &lt;a href=&quot;https://zh.wikipedia.org/wiki/OpenSolaris&quot;&gt;OpenSolaris &lt;/a&gt; 的发展，因此导致自由软件社区对于 Oracle 是否还会持续支持 MySQL 社区版（MySQL 的各个发行版本中唯一免费的版本）有所担忧，MySQL 的创始人麦克尔·维德纽斯以 MySQL 为基础，创建了 &lt;a href=&quot;https://zh.wikipedia.org/wiki/MariaDB&quot;&gt;MariaDB&lt;/a&gt;（以他女儿的名字命名的数据库）分支。有许多原来使用 MySQL 数据库的公司（例如：维基百科）已经陆续完成了从 MySQL 数据库到 MariaDB 数据库的迁移。&lt;/p&gt;
&lt;h4&gt;安装 MySQL&lt;/h4&gt;
&lt;h5&gt;Windows 环境&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;a href=&quot;https://www.mysql.com/&quot;&gt;官方网站&lt;/a&gt;提供的&lt;a href=&quot;https://dev.mysql.com/downloads/windows/installer/8.0.html&quot;&gt;下载链接&lt;/a&gt;下载“MySQL社区版服务器”安装程序，如下图所示，建议大家下载离线安装版的MySQL Installer。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/53fb18e2f5.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行 Installer，按照下面的步骤进行安装。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择自定义安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/4ad1347aaf.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择需要安装的组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/0bf7147fe5.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果缺少依赖项，需要先安装依赖项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/16f7c809a8.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备开始安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/0036b32d01.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/dad8263438.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备执行配置向导。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/961aa5d396.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行安装后的配置向导。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置服务器类型和网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/c7d6fb3a11.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置认证方法（保护密码的方式）。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/149c5b7cb9.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置用户和角色。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/37a544e4a7.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置Windows服务名以及是否开机自启。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/7b57aeb520.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置日志。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/f66f7fb5a3.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置高级选项。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/48379f729c.webp&quot; alt=&quot;ACAC15B8633133B65476286A49BFBD7E&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用配置。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/1686c82301.webp&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在 Windows 系统的“服务”窗口中启动或停止 MySQL。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/63fb635afa.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 PATH 环境变量，以便在命令行提示符窗口使用 MySQL 客户端工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打开 Windows 的“系统”窗口并点击“高级系统设置”。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/66b837a9f4.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在“系统属性”的“高级”窗口，点击“环境变量”按钮。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/56391908f6.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改PATH环境变量，将MySQL安装路径下的&lt;code&gt;bin&lt;/code&gt;文件夹的路径配置到PATH环境变量中。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/871b7705ea.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置完成后，可以尝试在“命令提示符”下使用 MySQL 的命令行工具。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/63d3fe50ce.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Linux 环境&lt;/h5&gt;
&lt;p&gt;下面以 CentOS 7.x 环境为例，演示如何安装 MySQL 5.7.x，如果需要在其他 Linux 系统下安装其他版本的 MySQL，请读者自行在网络上查找对应的安装教程。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装 MySQL。&lt;/p&gt;
&lt;p&gt;可以在 &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day36-45/%3Chttps:/www.mysql.com/%3E&quot;&gt;MySQL 官方网站&lt;/a&gt;下载安装文件。首先在下载页面中选择平台和版本，然后找到对应的下载链接，直接下载包含所有安装文件的归档文件，解归档之后通过包管理工具进行安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.26-1.el7.x86_64.rpm-bundle.tar
tar -xvf mysql-5.7.26-1.el7.x86_64.rpm-bundle.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果系统上有 MariaDB 相关的文件，需要先移除 MariaDB 相关的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum list installed | grep mariadb | awk &apos;{print $1}&apos; | xargs yum erase -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新和安装可能用到的底层依赖库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum update
yum install -y libaio libaio-devel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来可以按照如下所示的顺序用 RPM（Redhat Package Manager）工具安装 MySQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rpm -ivh mysql-community-common-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-compat-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-devel-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-5.7.26-1.el7.x86_64.rpm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用下面的命令查看已经安装的 MySQL 相关的包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rpm -qa | grep mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 MySQL。&lt;/p&gt;
&lt;p&gt;MySQL 的配置文件在&lt;code&gt;/etc&lt;/code&gt;目录下，名为&lt;code&gt;my.cnf&lt;/code&gt;，默认的配置文件内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /etc/my.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html

[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过配置文件，我们可以修改 MySQL 服务使用的端口、字符集、最大连接数、套接字队列大小、最大数据包大小、日志文件的位置、日志过期时间等配置。当然，我们还可以通过修改配置文件来对 MySQL 服务器进行性能调优和安全管控。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 MySQL 服务。&lt;/p&gt;
&lt;p&gt;可以使用下面的命令来启动 MySQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service mysqld start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 CentOS 7 中，更推荐使用下面的命令来启动 MySQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 MySQL 成功后，可以通过下面的命令来检查网络端口使用情况，MySQL 默认使用&lt;code&gt;3306&lt;/code&gt;端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netstat -ntlp | grep mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用下面的命令查找是否有名为&lt;code&gt;mysqld&lt;/code&gt;的进程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pgrep mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 MySQL 客户端工具连接服务器。&lt;/p&gt;
&lt;p&gt;命令行工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：启动客户端时，&lt;code&gt;-u&lt;/code&gt;参数用来指定用户名，MySQL 默认的超级管理账号为&lt;code&gt;root&lt;/code&gt;；&lt;code&gt;-p&lt;/code&gt;表示要输入密码（用户口令）；如果连接的是其他主机而非本机，可以用&lt;code&gt;-h&lt;/code&gt;来指定连接主机的主机名或IP地址。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果是首次安装 MySQL，可以使用下面的命令来找到默认的初始密码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /var/log/mysqld.log | grep password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令会查看 MySQL 的日志带有&lt;code&gt;password&lt;/code&gt;的行，在显示的结果中&lt;code&gt;root@localhost:&lt;/code&gt;后面的部分就是默认设置的初始密码。&lt;/p&gt;
&lt;p&gt;进入客户端工具后，可以通过下面的指令来修改超级管理员（root）的访问口令为&lt;code&gt;123456&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set global validate_password_policy=0;
set global validate_password_length=6;
alter user &apos;root&apos;@&apos;localhost&apos; identified by &apos;123456&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：MySQL 较新的版本默认不允许使用弱口令作为用户口令，所以上面的代码修改了验证用户口令的策略和口令的长度。事实上我们不应该使用弱口令，因为存在用户口令被暴力破解的风险。近年来，&lt;strong&gt;攻击数据库窃取数据和劫持数据库勒索比特币&lt;/strong&gt;的事件屡见不鲜，要避免这些潜在的风险，最为重要的一点是&lt;strong&gt;不要让数据库服务器暴露在公网上&lt;/strong&gt;（最好的做法是将数据库置于内网，至少要做到不向公网开放数据库服务器的访问端口），另外要保管好&lt;code&gt;root&lt;/code&gt;账号的口令，应用系统需要访问数据库时，通常不使用&lt;code&gt;root&lt;/code&gt;账号进行访问，而是&lt;strong&gt;创建其他拥有适当权限的账号来访问&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再次使用客户端工具连接 MySQL 服务器时，就可以使用新设置的口令了。在实际开发中，为了方便用户操作，可以选择图形化的客户端工具来连接 MySQL 服务器，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL Workbench（官方工具）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/3100d5b74c.webp&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Navicat for MySQL（界面简单友好）&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/42ffa1281b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;macOS环境&lt;/h5&gt;
&lt;p&gt;macOS 系统安装 MySQL 是比较简单的，只需要从刚才说到的官方网站下载 DMG 安装文件并运行就可以了，下载的时候需要根据自己使用的是 Intel 的芯片还是苹果的 M1 芯片选择下载链接，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/43a30dac91.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;安装成功后，可以在“系统偏好设置”中找到“MySQL”，在如下所示的画面中，可以启动和停止 MySQL 服务器，也可以对 MySQL 核心文件的路径进行配置。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/482e86f481.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;MySQL 基本命令&lt;/h4&gt;
&lt;h5&gt;查看命令&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;查看所有数据库&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show databases;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看所有字符集&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show character set;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看所有的排序规则&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show collation;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看所有的引擎&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show engines;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看所有日志文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show binary logs;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看数据库下所有表&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;show tables;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;获取帮助&lt;/h5&gt;
&lt;p&gt;在 MySQL 命令行工具中，可以使用&lt;code&gt;help&lt;/code&gt;命令或&lt;code&gt;?&lt;/code&gt;来获取帮助，如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看&lt;code&gt;show&lt;/code&gt;命令的帮助。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? show
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看有哪些帮助内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? contents
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取函数的帮助。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? functions
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取数据类型的帮助。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? data types
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;其他命令&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;新建/重建服务器连接 - &lt;code&gt;connect&lt;/code&gt; / &lt;code&gt;resetconnection&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;清空当前输入 - &lt;code&gt;\c&lt;/code&gt;。在输入错误时，可以及时使用&lt;code&gt;\c&lt;/code&gt;清空当前输入并重新开始。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改终止符（定界符）- &lt;code&gt;delimiter&lt;/code&gt;。默认的终止符是&lt;code&gt;;&lt;/code&gt;，可以使用该命令修改成其他的字符，例如修改为&lt;code&gt;$&lt;/code&gt;符号，可以用&lt;code&gt;delimiter $&lt;/code&gt;命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开系统默认编辑器 - &lt;code&gt;edit&lt;/code&gt;。编辑完成保存关闭之后，命令行会自动执行编辑的内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看服务器状态 - &lt;code&gt;status&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改默认提示符 - &lt;code&gt;prompt&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行系统命令 - &lt;code&gt;system&lt;/code&gt;。可以将系统命令跟在&lt;code&gt;system&lt;/code&gt;命令的后面执行，&lt;code&gt;system&lt;/code&gt;命令也可以缩写为&lt;code&gt;\!&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 SQL 文件 - &lt;code&gt;source&lt;/code&gt;。&lt;code&gt;source&lt;/code&gt;命令后面跟 SQL 文件路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重定向输出 - &lt;code&gt;tee&lt;/code&gt; / &lt;code&gt;notee&lt;/code&gt;。可以将命令的输出重定向到指定的文件中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换数据库 - &lt;code&gt;use&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示警告信息 - &lt;code&gt;warnings&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出命令行 - &lt;code&gt;quit&lt;/code&gt;或&lt;code&gt;exit&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;37 SQL详解之DDL&lt;/h2&gt;
&lt;p&gt;我们通常可以将 SQL 分为四类，分别是 DDL（数据定义语言）、DML（数据操作语言）、 DCL（数据控制语言）和 TCL（事务控制语言）。DDL 主要用于创建、删除、修改数据库中的对象，比如创建、删除和修改二维表，核心的关键字包括&lt;code&gt;create&lt;/code&gt;、&lt;code&gt;drop&lt;/code&gt;和&lt;code&gt;alter&lt;/code&gt;；DML 主要负责数据的插入、删除、更新和查询，关键词包括&lt;code&gt;insert&lt;/code&gt;、&lt;code&gt;delete&lt;/code&gt;、&lt;code&gt;update&lt;/code&gt;和&lt;code&gt;select&lt;/code&gt;；DCL 用于授予和召回权限，核心关键词是&lt;code&gt;grant&lt;/code&gt;和&lt;code&gt;revoke&lt;/code&gt;；TCL 通常用于事务控制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：SQL 是不区分大小写的语言，一般情况下我们建议将关键字大写，其他部分小写。 如果公司的 SQL 编程规范有强制规定，那么就按照公司的要求来，个人的喜好不应该凌驾于公司的编程规范之上，这一点对职业人来说应该是常识。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;建库建表&lt;/h4&gt;
&lt;p&gt;下面我们来实现一个非常简单的学校选课系统的数据库。我们将数据库命名为&lt;code&gt;school&lt;/code&gt;，四个关键的实体分别是学院、老师、学生和课程，其中，学生跟学院是从属关系，这个关系从数量上来讲是多对一关系，因为一个学院可以有多名学生，而一个学生通常只属于一个学院；同理，老师跟学院的从属关系也是多对一关系。一名老师可以讲授多门课程，一门课程如果只有一个授课老师的话，那么课程跟老师也是多对一关系；如果允许多个老师合作讲授一门课程，那么课程和老师就是多对多关系。简单起见，我们将课程和老师设计为多对一关系。学生和课程是典型的多对多关系，因为一个学生可以选择多门课程，一门课程也可以被多个学生选择，而关系型数据库需要借助中间表才能维持维持两个实体的多对多关系。最终，我们的学校选课系统一共有五张表，分别是学院表（&lt;code&gt;tb_college&lt;/code&gt;）、学生表（&lt;code&gt;tb_student&lt;/code&gt;）、教师表（&lt;code&gt;tb_teacher&lt;/code&gt;）、课程表（&lt;code&gt;tb_course&lt;/code&gt;）和选课记录表（&lt;code&gt;tb_record&lt;/code&gt;），其中选课记录表就是维持学生跟课程多对多关系的中间表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 如果存在名为school的数据库就删除它
DROP DATABASE IF EXISTS `school`;

-- 创建名为school的数据库并设置默认的字符集和排序方式
CREATE DATABASE `school` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

-- 切换到school数据库上下文环境
USE `school`;

-- 创建学院表
CREATE TABLE `tb_college`
(
`col_id`    int unsigned AUTO_INCREMENT      COMMENT &apos;编号&apos;,
`col_name`  varchar(50)  NOT NULL            COMMENT &apos;名称&apos;,
`col_intro` varchar(500) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;介绍&apos;,
PRIMARY KEY (`col_id`)
);

-- 创建学生表
CREATE TABLE `tb_student`
(
`stu_id`    int unsigned NOT NULL           COMMENT &apos;学号&apos;,
`stu_name`  varchar(20)  NOT NULL           COMMENT &apos;姓名&apos;,
`stu_sex`   boolean      NOT NULL DEFAULT 1 COMMENT &apos;性别&apos;,
`stu_birth` date         NOT NULL           COMMENT &apos;出生日期&apos;,
`stu_addr`  varchar(255) DEFAULT &apos;&apos;         COMMENT &apos;籍贯&apos;,
`col_id`    int unsigned NOT NULL           COMMENT &apos;所属学院&apos;,
PRIMARY KEY (`stu_id`),
CONSTRAINT `fk_student_col_id` FOREIGN KEY (`col_id`) REFERENCES `tb_college` (`col_id`)
);

-- 创建教师表
CREATE TABLE `tb_teacher`
(
`tea_id`    int unsigned NOT NULL                COMMENT &apos;工号&apos;,
`tea_name`  varchar(20)  NOT NULL                COMMENT &apos;姓名&apos;,
`tea_title` varchar(10)  NOT NULL DEFAULT &apos;助教&apos; COMMENT &apos;职称&apos;,
`col_id`    int unsigned NOT NULL                COMMENT &apos;所属学院&apos;,
PRIMARY KEY (`tea_id`),
CONSTRAINT `fk_teacher_col_id` FOREIGN KEY (`col_id`) REFERENCES `tb_college` (`col_id`)
);

-- 创建课程表
CREATE TABLE `tb_course`
(
`cou_id`     int unsigned NOT NULL COMMENT &apos;编号&apos;,
`cou_name`   varchar(50)  NOT NULL COMMENT &apos;名称&apos;,
`cou_credit` int          NOT NULL COMMENT &apos;学分&apos;,
`tea_id`     int unsigned NOT NULL COMMENT &apos;授课老师&apos;,
PRIMARY KEY (`cou_id`),
CONSTRAINT `fk_course_tea_id` FOREIGN KEY (`tea_id`) REFERENCES `tb_teacher` (`tea_id`)
);

-- 创建选课记录表
CREATE TABLE `tb_record`
(
`rec_id`   bigint unsigned AUTO_INCREMENT COMMENT &apos;选课记录号&apos;,
`stu_id`   int unsigned    NOT NULL       COMMENT &apos;学号&apos;,
`cou_id`   int unsigned    NOT NULL       COMMENT &apos;课程编号&apos;,
`sel_date` date            NOT NULL       COMMENT &apos;选课日期&apos;,
`score`    decimal(4,1)                   COMMENT &apos;考试成绩&apos;,
PRIMARY KEY (`rec_id`),
CONSTRAINT `fk_record_stu_id` FOREIGN KEY (`stu_id`) REFERENCES `tb_student` (`stu_id`),
CONSTRAINT `fk_record_cou_id` FOREIGN KEY (`cou_id`) REFERENCES `tb_course` (`cou_id`),
CONSTRAINT `uk_record_stu_cou` UNIQUE (`stu_id`, `cou_id`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的DDL有几个地方需要强调一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，上面 SQL 中的数据库名、表名、字段名都被反引号（`）包裹起来，反引号并不是必须的，但是却可以解决表名、字段名等跟 SQL 关键字（SQL 中有特殊含义的单词）冲突的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据库时，我们通过&lt;code&gt;default character set utf8mb4&lt;/code&gt;指定了数据库默认使用的字符集为&lt;code&gt;utf8mb4&lt;/code&gt;（最大&lt;code&gt;4&lt;/code&gt;字节的&lt;code&gt;utf-8&lt;/code&gt;编码），我们推荐使用该字符集，它也是 MySQL 8.x 默认使用的字符集，因为它能够支持国际化编码，还可以存储 Emoji 字符。可以通过下面的命令查看 MySQL 支持的字符集以及默认的排序规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show character set;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| dec8     | DEC West European               | dec8_swedish_ci     |      1 |
| cp850    | DOS West European               | cp850_general_ci    |      1 |
| hp8      | HP West European                | hp8_english_ci      |      1 |
| koi8r    | KOI8-R Relcom Russian           | koi8r_general_ci    |      1 |
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
| latin2   | ISO 8859-2 Central European     | latin2_general_ci   |      1 |
| swe7     | 7bit Swedish                    | swe7_swedish_ci     |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 |
| sjis     | Shift-JIS Japanese              | sjis_japanese_ci    |      2 |
| hebrew   | ISO 8859-8 Hebrew               | hebrew_general_ci   |      1 |
| tis620   | TIS620 Thai                     | tis620_thai_ci      |      1 |
| euckr    | EUC-KR Korean                   | euckr_korean_ci     |      2 |
| koi8u    | KOI8-U Ukrainian                | koi8u_general_ci    |      1 |
| gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 |
| greek    | ISO 8859-7 Greek                | greek_general_ci    |      1 |
| cp1250   | Windows Central European        | cp1250_general_ci   |      1 |
| gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 |
| latin5   | ISO 8859-9 Turkish              | latin5_turkish_ci   |      1 |
| armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 |
| utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 |
| ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 |
| cp866    | DOS Russian                     | cp866_general_ci    |      1 |
| keybcs2  | DOS Kamenicky Czech-Slovak      | keybcs2_general_ci  |      1 |
| macce    | Mac Central European            | macce_general_ci    |      1 |
| macroman | Mac West European               | macroman_general_ci |      1 |
| cp852    | DOS Central European            | cp852_general_ci    |      1 |
| latin7   | ISO 8859-13 Baltic              | latin7_general_ci   |      1 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_general_ci  |      4 |
| cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 |
| utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 |
| utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 |
| cp1256   | Windows Arabic                  | cp1256_general_ci   |      1 |
| cp1257   | Windows Baltic                  | cp1257_general_ci   |      1 |
| utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 |
| binary   | Binary pseudo charset           | binary              |      1 |
| geostd8  | GEOSTD8 Georgian                | geostd8_general_ci  |      1 |
| cp932    | SJIS for Windows Japanese       | cp932_japanese_ci   |      2 |
| eucjpms  | UJIS for Windows Japanese       | eucjpms_japanese_ci |      3 |
| gb18030  | China National Standard GB18030 | gb18030_chinese_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要设置 MySQL 服务启动时默认使用的字符集，可以修改MySQL的配置并添加以下内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
character-set-server=utf8
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果不清楚如何修改 MySQL 的配置文件就先不要管它。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建和删除数据库时，关键字&lt;code&gt;database&lt;/code&gt;也可以替换为&lt;code&gt;schema&lt;/code&gt;，二者作用相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表语句中的&lt;code&gt;not null&lt;/code&gt;是非空约束，它限定了字段不能为空；&lt;code&gt;default&lt;/code&gt;用于为字段指定默认值，我们称之为默认值约束；&lt;code&gt;primary key&lt;/code&gt;是主键约束，它设定了能够唯一确定一条记录的列，也确保了每条记录都是独一无二的，因为主键不允许重复；&lt;code&gt;foreign key&lt;/code&gt;是外键约束，它维持了两张表的参照完整性，举个例子，由于学生表中为 col_id 字段添加了外键约束，限定其必须引用（&lt;code&gt;references&lt;/code&gt;）学院表中的 col_id，因此学生表中的学院编号必须来自于学院表中的学院编号，不能够随意为该字段赋值。如果需要给主键约束、外键约束等起名字，可以使用&lt;code&gt;constriant&lt;/code&gt;关键字并在后面跟上约束的名字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建表语句中的&lt;code&gt;comment&lt;/code&gt; 关键字用来给列和表添加注释，增强代码的可读性和可维护性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在创建表的时候，可以自行选择底层的存储引擎。MySQL 支持多种存储引擎，可以通过&lt;code&gt;show engines&lt;/code&gt;命令进行查看。MySQL 5.5 以后的版本默认使用的存储引擎是 InnoDB，它是我们推荐大家使用的存储引擎（因为更适合当下互联网应用对高并发、性能以及事务支持等方面的需求），为了 SQL 语句的向下兼容性，我们可以在建表语句结束处右圆括号的后面通过&lt;code&gt;engine=innodb&lt;/code&gt;来指定使用 InnoDB 存储引擎。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show engines\G
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的 \G 是为了换一种输出方式，在命令行客户端中，如果表的字段很多一行显示不完，就会导致输出的内容看起来非常不舒服，使用 \G 可以将记录的每个列以独占整行的的方式输出，这种输出方式在命令行客户端中看起来会舒服很多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;*************************** 1. row ***************************
      Engine: InnoDB
     Support: DEFAULT
     Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
          XA: YES
  Savepoints: YES
*************************** 2. row ***************************
      Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 3. row ***************************
      Engine: MEMORY
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 4. row ***************************
      Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 5. row ***************************
      Engine: MyISAM
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 6. row ***************************
      Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 7. row ***************************
      Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 8. row ***************************
      Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 9. row ***************************
      Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
          XA: NULL
  Savepoints: NULL
9 rows in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的表格对MySQL几种常用的数据引擎进行了简单的对比。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;InnoDB&lt;/th&gt;
&lt;th&gt;MRG_MYISAM&lt;/th&gt;
&lt;th&gt;MEMORY&lt;/th&gt;
&lt;th&gt;MyISAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;存储限制&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事务&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;锁机制&lt;/td&gt;
&lt;td&gt;行锁&lt;/td&gt;
&lt;td&gt;表锁&lt;/td&gt;
&lt;td&gt;表锁&lt;/td&gt;
&lt;td&gt;表锁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B树索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;哈希索引&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全文检索&lt;/td&gt;
&lt;td&gt;支持（5.6+）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;集群索引&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据缓存&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引缓存&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据可压缩&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存使用&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储空间使用&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量插入性能&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否支持外键&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;通过上面的比较我们可以了解到，InnoDB 是唯一能够支持外键、事务以及行锁的存储引擎，所以我们之前说它更适合互联网应用，而且在较新版本的 MySQL 中，它也是默认使用的存储引擎。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在定义表结构为每个字段选择数据类型时，如果不清楚哪个数据类型更合适，可以通过 MySQL 的帮助系统来了解每种数据类型的特性、数据的长度和精度等相关信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? data types
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在 MySQLWorkbench 中，不能使用&lt;code&gt;?&lt;/code&gt;获取帮助，要使用对应的命令&lt;code&gt;help&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;You asked for help about help category: &quot;Data Types&quot;
For more information, type &apos;help &amp;lt;item&amp;gt;&apos;, where &amp;lt;item&amp;gt; is one of the following
topics:
   AUTO_INCREMENT
   BIGINT
   BINARY
   BIT
   BLOB
   BLOB DATA TYPE
   BOOLEAN
   CHAR
   CHAR BYTE
   DATE
   DATETIME
   DEC
   DECIMAL
   DOUBLE
   DOUBLE PRECISION
   ENUM
   FLOAT
   INT
   INTEGER
   LONGBLOB
   LONGTEXT
   MEDIUMBLOB
   MEDIUMINT
   MEDIUMTEXT
   SET DATA TYPE
   SMALLINT
   TEXT
   TIME
   TIMESTAMP
   TINYBLOB
   TINYINT
   TINYTEXT
   VARBINARY
   VARCHAR
   YEAR DATA TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取 varchar 类型的帮助：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? varchar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Name: &apos;VARCHAR&apos;
Description:
[NATIONAL] VARCHAR(M) [CHARACTER SET charset_name] [COLLATE
collation_name]

A variable-length string. M represents the maximum column length in
characters. The range of M is 0 to 65,535. The effective maximum length
of a VARCHAR is subject to the maximum row size (65,535 bytes, which is
shared among all columns) and the character set used. For example, utf8
characters can require up to three bytes per character, so a VARCHAR
column that uses the utf8 character set can be declared to be a maximum
of 21,844 characters. See
http://dev.mysql.com/doc/refman/5.7/en/column-count-limit.html.

MySQL stores VARCHAR values as a 1-byte or 2-byte length prefix plus
data. The length prefix indicates the number of bytes in the value. A
VARCHAR column uses one length byte if values require no more than 255
bytes, two length bytes if values may require more than 255 bytes.

*Note*:

MySQL follows the standard SQL specification, and does not remove
trailing spaces from VARCHAR values.

VARCHAR is shorthand for CHARACTER VARYING. NATIONAL VARCHAR is the
standard SQL way to define that a VARCHAR column should use some
predefined character set. MySQL uses utf8 as this predefined character
set. http://dev.mysql.com/doc/refman/5.7/en/charset-national.html.
NVARCHAR is shorthand for NATIONAL VARCHAR.

URL: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在数据类型的选择上，保存字符串数据通常都使用 VARCHAR 和 CHAR 两种类型，前者通常称为变长字符串，而后者通常称为定长字符串；对于 InnoDB 存储引擎，行存储格式没有区分固定长度和可变长度列，因此 VARCHAR 类型和 CHAR 类型没有本质区别，后者不一定比前者性能更好。如果要保存的很大字符串，可以使用 TEXT 类型；如果要保存很大的字节串，可以使用 BLOB（二进制大对象）类型。在 MySQL 中，TEXT 和 BLOB又分别包括 TEXT、MEDIUMTEXT、LONGTEXT 和 BLOB、MEDIUMBLOB、LONGBLOB 三种不同的类型，它们主要的区别在于存储数据的最大大小不同。保存浮点数可以用 FLOAT 或 DOUBLE 类型，FLOAT 已经不推荐使用了，而且在 MySQL 后续的版本中可能会被移除掉。而保存定点数应该使用 DECIMAL 类型，它可以指定小数点前后有效数字的位数。如果要保存时间日期，DATETIME 类型优于 TIMESTAMP 类型，因为前者能表示的时间日期范围更大，后者底层其实就是一个整数，记录了指定的日期时间和 1970-01-01 00:00:00 相差多少个毫秒，该类型在 2038-01-19 03:14:07 之后就会溢出。&lt;/p&gt;
&lt;p&gt;对于自增字段 AUTO_INCREMENT，如果使用 MySQL 5.x 版本要注意自增字段的回溯问题，当然这个问题在 MySQL 8.x 中已经得到了很好的解决，当然，MySQL 8.x 还有很多其他的好处，不管是功能还是性能上都有很多的优化和调整，因此强烈推荐大家使用 MySQL 8.x 版本。对于高并发访问数据库的场景，AUTO_INCREMENT 不仅存在性能上的问题，还可能在多机结构上产生重复的 ID 值，在这种场景下，使用分布式 ID 生成算法（SnowFlake、TinyID等）才是最好的选择，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;删除表和修改表&lt;/h4&gt;
&lt;p&gt;下面以学生表为例，为大家说明如何删除表和修改表。删除表可以使用&lt;code&gt;drop table&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE `tb_student`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE IF EXISTS `tb_student`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，如果学生表已经录入了数据而且该数据被其他表引用了，那么就不能删除学生表，否则上面的操作会报错。在下一课中，我们会讲解如何向表中插入数据，到时候大家可以试一试，能否顺利删除学生表。&lt;/p&gt;
&lt;p&gt;如果要修改学生表，可以使用&lt;code&gt;alter table&lt;/code&gt;，具体可以分为以下几种情况：&lt;/p&gt;
&lt;p&gt;修改表，添加一个新列，例如给学生表添加一个联系电话的列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` ADD COLUMN `stu_tel` varchar(20) NOT NULL COMMENT &apos;联系电话&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：如果新增列的时候指定了非空约束（&lt;code&gt;not null&lt;/code&gt;），那么学生表不能够有数据，否则原来的数据增加了 stu_tel 列之后是没有数据的，这就违反了非空约束的要求；当然，我们在添加列的时候也可以使用默认值约束来解决这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;修改表，删除指定的列，例如将上面添加的联系电话列删除掉。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` DROP COLUMN `stu_tel`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改表，修改列的数据类型，例如将学生表的 stu_sex 修改为字符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` MODIFY COLUMN `stu_sex` char(1) NOT NULL DEFAULT &apos;M&apos; COMMENT &apos;性别&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改表，修改列的命名，例如将学生表的 stu_sex 修改为 stu_gender。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` CHANGE COLUMN `stu_sex` `stu_gender` boolean DEFAULT 1 COMMENT &apos;性别&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改表，删除约束条件，例如删除学生表的 col_id 列的外键约束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` DROP FOREIGN KEY `fk_student_col_id`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改表，添加约束条件，例如给学生表的 col_id 列加上外键约束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` ADD FOREIGN KEY (`col_id`) REFERENCES `tb_college` (`col_id`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` ADD CONSTRAINT `fk_student_col_id` FOREIGN KEY (`col_id`) REFERENCES `tb_college` (`col_id`);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在添加外键约束时，还可以通过&lt;code&gt;on update&lt;/code&gt;和&lt;code&gt;on delete&lt;/code&gt;来指定在被引用的表发生删除和更新操作时，应该进行何种处理，二者的默认值都是&lt;code&gt;restrict&lt;/code&gt;，表示如果存在外键约束，则不允许更新和删除被引用的数据。除了&lt;code&gt;restrict&lt;/code&gt;之外，这里可能的取值还有&lt;code&gt;cascade&lt;/code&gt;（级联操作）和&lt;code&gt;set null&lt;/code&gt;（设置为空），有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;修改表的名字，例如将学生表的名字修改为 tb_stu_info。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE `tb_student` RENAME TO `tb_stu_info`;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：一般情况下，请不要轻易修改数据库或表的名字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;38 SQL详解之DML&lt;/h2&gt;
&lt;p&gt;我们接着上一课中创建的学校选课系统数据库，为大家讲解 DML 的使用。DML 可以帮助将数据插入到二维表（&lt;code&gt;insert&lt;/code&gt;操作）、从二维表删除数据（&lt;code&gt;delete&lt;/code&gt;操作）以及更新二维表的数据（&lt;code&gt;update&lt;/code&gt;操作）。在执行 DML 之前，我们先通过下面的&lt;code&gt;use&lt;/code&gt;命令切换到&lt;code&gt;school&lt;/code&gt;数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE `school`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;insert操作&lt;/h4&gt;
&lt;p&gt;顾名思义，&lt;code&gt;insert&lt;/code&gt;是用来插入行到二维表中的，插入的方式包括：插入完整的行、插入行的一部分、插入多行、插入查询的结果。我们通过如下所示的 SQL 向学院表中添加一个学院。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO `tb_college` 
VALUES
    (DEFAULT, &apos;计算机学院&apos;, &apos;学习计算机科学与技术的地方&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，由于学院表的主键是一个自增字段，因此上面的 SQL 中用&lt;code&gt;default&lt;/code&gt;表示该列使用默认值，我们也可以使用下面的方式完成同样的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO `tb_college` (`col_name`, `col_intro`) 
VALUES 
    (&apos;计算机学院&apos;, &apos;学习计算机科学与技术的地方&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们推荐大家使用下面这种做法，指定为哪些字段赋值，这样做可以不按照建表时设定的字段顺序赋值，可以按照&lt;code&gt;values&lt;/code&gt;前面的元组中给定的字段顺序为字段赋值，但是需要注意，除了允许为&lt;code&gt;null&lt;/code&gt;和有默认值的字段外，其他的字段都必须要一一列出并在&lt;code&gt;values&lt;/code&gt;后面的元组中为其赋值。如果希望一次性插入多条记录，我们可以在&lt;code&gt;values&lt;/code&gt;后面跟上多个元组来实现批量插入，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO `tb_college` 
    (`col_name`, `col_intro`) 
VALUES 
    (&apos;外国语学院&apos;, &apos;学习歪果仁的语言的学院&apos;),
    (&apos;经济管理学院&apos;, &apos;经世济民，治理国家；管理科学，兴国之道&apos;),
    (&apos;体育学院&apos;, &apos;发展体育运动，增强人民体质&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在插入数据时，要注意主键是不能重复的，如果插入的数据与表中已有记录主键相同，那么&lt;code&gt;insert&lt;/code&gt;操作将会产生 Duplicated Entry 的报错信息。再次提醒大家，如果&lt;code&gt;insert&lt;/code&gt;操作省略了某些列，那么这些列要么有默认值，要么允许为&lt;code&gt;null&lt;/code&gt;，否则也将产生错误。在业务系统中，为了让&lt;code&gt;insert&lt;/code&gt;操作不影响其他操作（主要是后面要讲的&lt;code&gt;select&lt;/code&gt;操作）的性能，可以在&lt;code&gt;insert&lt;/code&gt;和&lt;code&gt;into&lt;/code&gt;之间加一个&lt;code&gt;low_priority&lt;/code&gt;来降低&lt;code&gt;insert&lt;/code&gt;操作的优先级，这个做法也适用于下面要讲的&lt;code&gt;delete&lt;/code&gt;和&lt;code&gt;update&lt;/code&gt;操作。&lt;/p&gt;
&lt;p&gt;假如有一张名为&lt;code&gt;tb_temp&lt;/code&gt;的表中有&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;两个列，分别保存了学院的名称和学院的介绍，我们也可以通过查询操作获得&lt;code&gt;tb_temp&lt;/code&gt;表的数据并插入到学院表中，如下所示，其中的&lt;code&gt;select&lt;/code&gt;就是我们之前提到的 DQL，在下一课中会详细讲解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO `tb_college`
    (`col_name`, `col_intro`)
SELECT `a`, `b` 
  FROM `tb_temp`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;delete 操作&lt;/h4&gt;
&lt;p&gt;如果需要从表中删除数据，可以使用&lt;code&gt;delete&lt;/code&gt;操作，它可以帮助我们删除指定行或所有行，例如我们要删除编号为&lt;code&gt;1&lt;/code&gt;的学院，就可以使用如下所示的 SQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE
  FROM `tb_college`
 WHERE col_id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，上面的&lt;code&gt;delete&lt;/code&gt;操作中的&lt;code&gt;where&lt;/code&gt;子句是用来指定条件的，只有满足条件的行会被删除。如果我们不小心写出了下面的 SQL，就会删除学院表中所有的记录，这是相当危险的，在实际工作中通常也不会这么做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE
  FROM `tb_college`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，即便删除了所有的数据，&lt;code&gt;delete&lt;/code&gt;操作不会删除表本身，也不会让 AUTO_INCREMENT 字段的值回到初始值。如果需要删除所有的数据而且让 AUTO_INCREMENT 字段回到初始值，可以使用&lt;code&gt;truncate table&lt;/code&gt;执行截断表操作，&lt;code&gt;truncate&lt;/code&gt;的本质是删除原来的表并重新创建一个表，它的速度其实更快，因为不需要逐行删除数据。但是请大家记住一点，用&lt;code&gt;truncate table&lt;/code&gt;删除数据是非常危险的，因为它会删除所有的数据，而且由于原来的表已经被删除了，要想恢复误删除的数据也会变得极为困难。&lt;/p&gt;
&lt;h4&gt;update 操作&lt;/h4&gt;
&lt;p&gt;如果要修改表中的数据，可以使用&lt;code&gt;update&lt;/code&gt;操作，它可以用来删除指定的行或所有的行。例如，我们将学生表中的“杨过”修改为“杨逍”，这里我们假设“杨过”的学号为&lt;code&gt;1001&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE `tb_student`
   SET `stu_name`=&apos;杨逍&apos;
 WHERE `stu_id`=1001;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面 SQL 中的&lt;code&gt;where&lt;/code&gt;子句，我们使用学号作为条件筛选出对应的学生，然后通过前面的赋值操作将其姓名修改为“杨逍”。这里为什么不直接使用姓名作为筛选条件，那是因为学生表中可能有多个名为“杨过”的学生，如果使用 stu_name 作为筛选条件，那么我们的&lt;code&gt;update&lt;/code&gt;操作有可能会一次更新多条数据，这显然不是我们想要看到的。还有一个需要注意的地方是&lt;code&gt;update&lt;/code&gt;操作中的&lt;code&gt;set&lt;/code&gt;关键字，因为 SQL 中的&lt;code&gt;=&lt;/code&gt;并不表示赋值，而是判断相等的运算符，只有出现在&lt;code&gt;set&lt;/code&gt; 关键字后面的&lt;code&gt;=&lt;/code&gt;，才具备赋值的能力。&lt;/p&gt;
&lt;p&gt;如果要同时修改学生的姓名和生日，我们可以对上面的&lt;code&gt;update&lt;/code&gt;语句稍作修改，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE `tb_student`
   SET `stu_name`=&apos;杨逍&apos;
     , `stu_birth`=&apos;1975-12-29&apos;
 WHERE `stu_id`=1001;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;update&lt;/code&gt;语句中也可以使用查询的方式获得数据并以此来更新指定的表数据，有兴趣的读者可以自行研究。在书写&lt;code&gt;update&lt;/code&gt;语句时，通常都会有&lt;code&gt;where&lt;/code&gt;子句，因为实际工作中几乎不太会用到更新全表的操作，这一点大家一定要注意。&lt;/p&gt;
&lt;h4&gt;完整的数据&lt;/h4&gt;
&lt;p&gt;下面我们给出完整的向 school 数据库的五张表中插入数据的 SQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE `school`;

-- 插入学院数据
INSERT INTO `tb_college` 
    (`col_name`, `col_intro`) 
VALUES 
    (&apos;计算机学院&apos;, &apos;计算机学院1958年设立计算机专业，1981年建立计算机科学系，1998年设立计算机学院，2005年5月，为了进一步整合教学和科研资源，学校决定，计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系：计算机科学与技术系、物联网工程系、计算金融系；两个研究所：图象图形研究所、网络空间安全研究院（2015年成立）；三个教学实验中心：计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。&apos;),
    (&apos;外国语学院&apos;, &apos;外国语学院设有7个教学单位，6个文理兼收的本科专业；拥有1个一级学科博士授予点，3个二级学科博士授予点，5个一级学科硕士学位授权点，5个二级学科硕士学位授权点，5个硕士专业授权领域，同时还有2个硕士专业学位（MTI）专业；有教职员工210余人，其中教授、副教授80余人，教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。&apos;),
    (&apos;经济管理学院&apos;, &apos;经济学院前身是创办于1905年的经济科；已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光，以及当代学者刘诗白等曾先后在此任教或学习。&apos;);

-- 插入学生数据
INSERT INTO `tb_student` 
    (`stu_id`, `stu_name`, `stu_sex`, `stu_birth`, `stu_addr`, `col_id`) 
VALUES
    (1001, &apos;杨过&apos;, 1, &apos;1990-3-4&apos;, &apos;湖南长沙&apos;, 1),
    (1002, &apos;任我行&apos;, 1, &apos;1992-2-2&apos;, &apos;湖南长沙&apos;, 1),
    (1033, &apos;王语嫣&apos;, 0, &apos;1989-12-3&apos;, &apos;四川成都&apos;, 1),
    (1572, &apos;岳不群&apos;, 1, &apos;1993-7-19&apos;, &apos;陕西咸阳&apos;, 1),
    (1378, &apos;纪嫣然&apos;, 0, &apos;1995-8-12&apos;, &apos;四川绵阳&apos;, 1),
    (1954, &apos;林平之&apos;, 1, &apos;1994-9-20&apos;, &apos;福建莆田&apos;, 1),
    (2035, &apos;东方不败&apos;, 1, &apos;1988-6-30&apos;, NULL, 2),
    (3011, &apos;林震南&apos;, 1, &apos;1985-12-12&apos;, &apos;福建莆田&apos;, 3),
    (3755, &apos;项少龙&apos;, 1, &apos;1993-1-25&apos;, &apos;四川成都&apos;, 3),
    (3923, &apos;杨不悔&apos;, 0, &apos;1985-4-17&apos;, &apos;四川成都&apos;, 3);

-- 插入老师数据
INSERT INTO `tb_teacher` 
    (`tea_id`, `tea_name`, `tea_title`, `col_id`) 
VALUES 
    (1122, &apos;张三丰&apos;, &apos;教授&apos;, 1),
    (1133, &apos;宋远桥&apos;, &apos;副教授&apos;, 1),
    (1144, &apos;杨逍&apos;, &apos;副教授&apos;, 1),
    (2255, &apos;范遥&apos;, &apos;副教授&apos;, 2),
    (3366, &apos;韦一笑&apos;, DEFAULT, 3);

-- 插入课程数据
INSERT INTO `tb_course` 
    (`cou_id`, `cou_name`, `cou_credit`, `tea_id`) 
VALUES 
    (1111, &apos;Python程序设计&apos;, 3, 1122),
    (2222, &apos;Web前端开发&apos;, 2, 1122),
    (3333, &apos;操作系统&apos;, 4, 1122),
    (4444, &apos;计算机网络&apos;, 2, 1133),
    (5555, &apos;编译原理&apos;, 4, 1144),
    (6666, &apos;算法和数据结构&apos;, 3, 1144),
    (7777, &apos;经贸法语&apos;, 3, 2255),
    (8888, &apos;成本会计&apos;, 2, 3366),
    (9999, &apos;审计学&apos;, 3, 3366);

-- 插入选课数据
INSERT INTO `tb_record` 
    (`stu_id`, `cou_id`, `sel_date`, `score`) 
VALUES 
    (1001, 1111, &apos;2017-09-01&apos;, 95),
    (1001, 2222, &apos;2017-09-01&apos;, 87.5),
    (1001, 3333, &apos;2017-09-01&apos;, 100),
    (1001, 4444, &apos;2018-09-03&apos;, NULL),
    (1001, 6666, &apos;2017-09-02&apos;, 100),
    (1002, 1111, &apos;2017-09-03&apos;, 65),
    (1002, 5555, &apos;2017-09-01&apos;, 42),
    (1033, 1111, &apos;2017-09-03&apos;, 92.5),
    (1033, 4444, &apos;2017-09-01&apos;, 78),
    (1033, 5555, &apos;2017-09-01&apos;, 82.5),
    (1572, 1111, &apos;2017-09-02&apos;, 78),
    (1378, 1111, &apos;2017-09-05&apos;, 82),
    (1378, 7777, &apos;2017-09-02&apos;, 65.5),
    (2035, 7777, &apos;2018-09-03&apos;, 88),
    (2035, 9999, &apos;2019-09-02&apos;, NULL),
    (3755, 1111, &apos;2019-09-02&apos;, NULL),
    (3755, 8888, &apos;2019-09-02&apos;, NULL),
    (3755, 9999, &apos;2017-09-01&apos;, 92);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的&lt;code&gt;insert&lt;/code&gt;语句使用了批处理的方式来插入数据，这种做法插入数据的效率比较高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;39 SQL详解之DQL&lt;/h2&gt;
&lt;p&gt;接下来，我们利用之前创建的学校选课系统数据库，为大家讲解 DML 中的查询操作。无论对于开发人员还是数据分析师，查询都是非常重要的，它关系着我们能否从关系数据库中获取我们需要的数据。建议大家把上上一节课中建库建表的 DDL 以及 上一节课中插入数据的 DML 重新执行一次，确保表和数据跟没有问题再执行下面的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE school;

-- 查询所有学生的所有信息
SELECT stu_id,
       stu_name,
       stu_sex,
       stu_birth,
       stu_addr,
       col_id
  FROM tb_student;

-- 查询学生的学号、姓名和籍贯(投影和别名)
SELECT stu_id AS 学号,
       stu_name AS 姓名,
       stu_addr AS 籍贯
  FROM tb_student;

-- 查询所有课程的名称及学分(投影和别名)
SELECT cou_name AS 课程名称,
       cou_credit AS 学分
  FROM tb_course;

-- 查询所有女学生的姓名和出生日期(数据筛选)
SELECT stu_name,
       stu_birth
  FROM tb_student
 WHERE stu_sex = 0;

-- 查询籍贯为“四川成都”的女学生的姓名和出生日期(数据筛选)
SELECT stu_name,
       stu_birth
  FROM tb_student
 WHERE stu_sex = 0
       AND stu_addr = &apos;四川成都&apos;;

-- 查询籍贯为“四川成都”或者性别是女的学生(数据筛选)
SELECT stu_name,
       stu_birth
  FROM tb_student
 WHERE stu_sex = 0
       OR stu_addr = &apos;四川成都&apos;;

-- 查询所有80后学生的姓名、性别和出生日期(数据筛选)
SELECT stu_name,
       stu_sex,
       stu_birth
  FROM tb_student
 WHERE &apos;1980-1-1&apos; &amp;lt;= stu_birth 
       AND stu_birth &amp;lt;= &apos;1989-12-31&apos;;
       
SELECT stu_name,
       stu_sex,
       stu_birth
  FROM tb_student
 WHERE stu_birth BETWEEN &apos;1980-1-1&apos; AND &apos;1989-12-31&apos;;

-- 查询学分大于2的课程的名称和学分(数据筛选)
SELECT cou_name,
	   cou_credit
  FROM tb_course
 WHERE cou_credit &amp;gt; 2;

-- 查询学分是奇数的课程的名称和学分(数据筛选)
SELECT cou_name,
	   cou_credit
  FROM tb_course
 WHERE cou_credit MOD 2 &amp;lt;&amp;gt; 0;

-- 查询选择选了1111的课程考试成绩在90分以上的学生学号(数据筛选)
SELECT stu_id
  FROM tb_record
 WHERE cou_id = 1111
       AND score &amp;gt; 90;

-- 查询名字叫“杨过”的学生的姓名和性别(数据筛选)
SELECT stu_name AS 姓名, 
       CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别
  FROM tb_student
 WHERE stu_name = &apos;杨过&apos;;
 
SELECT stu_name AS 姓名, 
       IF(stu_sex, &apos;男&apos;, &apos;女&apos;) AS 性别
  FROM tb_student
 WHERE stu_name = &apos;杨过&apos;;
    
-- 查询姓“杨”的学生姓名和性别(模糊匹配)
-- 通配符 % 匹配零个或任意多个字符
SELECT stu_name AS 姓名, 
       CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别
  FROM tb_student
 WHERE stu_name LIKE &apos;杨%&apos;;

-- 查询姓“杨”名字两个字的学生姓名和性别(模糊匹配)
-- 通过符 _ 匹配一个字符
SELECT stu_name AS 姓名, 
       CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别
  FROM tb_student
 WHERE stu_name LIKE &apos;杨_&apos;;

-- 查询姓“杨”名字三个字的学生姓名和性别(模糊匹配)
SELECT stu_name AS 姓名, 
       CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别
  FROM tb_student
 WHERE stu_name LIKE &apos;杨__&apos;;
 
-- 查询学号最后一位是3的学生的学号和姓名(模糊匹配)
SELECT stu_id,
       stu_name
  FROM tb_student
 WHERE stu_id LIKE &apos;%3&apos;;

-- 查询名字中有“不”字或“嫣”字的学生的学号和姓名(模糊匹配和并集运算)
SELECT stu_id,
       stu_name
  FROM tb_student
 WHERE stu_name LIKE &apos;%不%&apos;
       OR stu_name LIKE &apos;%嫣%&apos;;
       
SELECT stu_id,
       stu_name
  FROM tb_student
 WHERE stu_name LIKE &apos;%不%&apos;
 UNION
SELECT stu_id,
       stu_name
  FROM tb_student
 WHERE stu_name LIKE &apos;%嫣%&apos;;

-- 查询姓“杨”或姓“林”名字三个字的学生的学号和姓名(正则表达式模糊匹配)
SELECT stu_id,
       stu_name
  FROM tb_student
 WHERE stu_name REGEXP &apos;[林杨][\\u4e00-\\u9fa5]{2}&apos;;

-- 查询没有录入籍贯的学生姓名(空值处理)
SELECT stu_name
  FROM tb_student
 WHERE TRIM(stu_addr) = &apos;&apos;
       OR stu_addr is null;
 
-- 查询录入了籍贯的学生姓名(空值处理)
SELECT stu_name
  FROM tb_student
 WHERE TRIM(stu_addr) &amp;lt;&amp;gt; &apos;&apos;
       AND stu_addr is not null;

-- 查询学生选课的所有日期(去重)
SELECT DISTINCT sel_date
  FROM tb_record;

-- 查询学生的籍贯(去重)
SELECT DISTINCT stu_addr
  FROM tb_student
 WHERE TRIM(stu_addr) &amp;lt;&amp;gt; &apos;&apos;
       AND stu_addr is not null;

-- 查询男学生的姓名和生日按年龄从大到小排列(排序)
SELECT stu_name,
       stu_birth
  FROM tb_student
 WHERE stu_sex = 1
 ORDER BY stu_birth ASC;
 
-- 补充：将上面的生日换算成年龄(日期函数、数值函数)
SELECT stu_name AS 姓名,
       FLOOR(DATEDIFF(CURDATE(), stu_birth) / 365) AS 年龄
  FROM tb_student
 WHERE stu_sex = 1
 ORDER BY 年龄 DESC;

-- 查询年龄最大的学生的出生日期(聚合函数)
SELECT MIN(stu_birth)
  FROM tb_student;

-- 查询年龄最小的学生的出生日期(聚合函数)
SELECT MAX(stu_birth)
  FROM tb_student;

-- 查询编号为1111的课程考试成绩的最高分(聚合函数)
SELECT MAX(score)
  FROM tb_record
 WHERE cou_id = 1111;

-- 查询学号为1001的学生考试成绩的最低分、最高分、平均分、标准差、方差(聚合函数)
SELECT MIN(score) AS 最低分,
       MAX(score) AS 最高分,
	   ROUND(AVG(score), 1) AS 平均分,
       STDDEV(score) AS 标准差,
       VARIANCE(score) AS 方差
  FROM tb_record
 WHERE stu_id = 1001;

-- 查询学号为1001的学生考试成绩的平均分，如果有null值，null值算0分(聚合函数)
SELECT ROUND(SUM(score) / COUNT(*), 1) AS 平均分
  FROM tb_record
 WHERE stu_id = 1001;

-- 查询男女学生的人数(分组和聚合函数)
SELECT CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别,
       COUNT(*) AS 人数
  FROM tb_student
 GROUP BY stu_sex;

-- 查询每个学院学生人数(分组和聚合函数)
SELECT col_id AS 学院编号,
       COUNT(*) AS 人数
  FROM tb_student
 GROUP BY col_id
  WITH ROLLUP;

-- 查询每个学院男女学生人数(分组和聚合函数)
SELECT col_id AS 学院编号,
       CASE stu_sex WHEN 1 THEN &apos;男&apos; ELSE &apos;女&apos; END AS 性别,
       COUNT(*) AS 人数
  FROM tb_student
 GROUP BY col_id, stu_sex;

-- 查询每个学生的学号和平均成绩(分组和聚合函数)
SELECT stu_id AS 学号,
	   ROUND(AVG(score), 1) AS 平均分
  FROM tb_record
 GROUP BY stu_id;

-- 查询平均成绩大于等于90分的学生的学号和平均成绩(分组后的数据筛选)
SELECT stu_id AS 学号,
	   ROUND(AVG(score), 1) AS 平均分
  FROM tb_record
 GROUP BY stu_id
HAVING 平均分 &amp;gt;= 90;

-- 查询1111、2222、3333三门课程平均成绩大于等于90分的学生的学号和平均成绩(分组前后的数据筛选)
SELECT stu_id AS 学号,
	   ROUND(AVG(score), 1) AS 平均分
  FROM tb_record
 WHERE cou_id in (1111, 2222, 3333)
 GROUP BY stu_id
HAVING 平均分 &amp;gt;= 90
 ORDER BY 平均分 ASC;

-- 查询年龄最大的学生的姓名(子查询)
SELECT stu_name
  FROM tb_student
 WHERE stu_birth = (SELECT MIN(stu_birth)
                      FROM tb_student);

-- 查询选了两门以上的课程的学生姓名(子查询和集合运算)
SELECT stu_name
  FROM tb_student
 WHERE stu_id in (SELECT stu_id
				    FROM tb_record
				   GROUP BY stu_id
				  HAVING COUNT(*) &amp;gt; 2);

-- 查询学生的姓名、生日和所在学院名称(表连接)
SELECT stu_name,
       stu_birth,
       col_name
  FROM tb_student AS t1, tb_college AS t2
 WHERE t1.col_id = t2.col_id;
 
SELECT stu_name,
       stu_birth,
	   col_name
  FROM tb_student INNER JOIN tb_college
       ON tb_student.col_id = tb_college.col_id;

SELECT stu_name,
       stu_birth,
	   col_name
  FROM tb_student NATURAL JOIN tb_college;
  
SELECT stu_name,
       stu_birth,
	   col_name
  FROM tb_student CROSS JOIN tb_college;

-- 查询学生姓名、课程名称以及成绩(表连接)
SELECT stu_name,
       cou_name,
	   score
  FROM tb_student, tb_course, tb_record
 WHERE tb_student.stu_id = tb_record.stu_id
       AND tb_course.cou_id = tb_record.cou_id
       AND score is not null;

SELECT stu_name,
       cou_name,
	   score
  FROM tb_student 
       INNER JOIN tb_record
	       ON tb_student.stu_id = tb_record.stu_id
	   INNER JOIN tb_course
	       ON tb_course.cou_id = tb_record.cou_id
 WHERE score is not null;
 
SELECT stu_name,
       cou_name,
       score
  FROM tb_student 
	   NATURAL JOIN tb_record
       NATURAL JOIN tb_course
 WHERE score is not null;

-- 补充：上面的查询结果取前5条数据(分页查询)
SELECT stu_name,
       cou_name,
       score
  FROM tb_student 
	   NATURAL JOIN tb_record
       NATURAL JOIN tb_course
 WHERE score is not null
 ORDER BY cou_id ASC, score DESC
 LIMIT 5;

-- 补充：上面的查询结果取第6-10条数据(分页查询)
SELECT stu_name,
       cou_name,
       score
  FROM tb_student 
	   NATURAL JOIN tb_record
       NATURAL JOIN tb_course
 WHERE score is not null
 ORDER BY cou_id ASC, score DESC
 LIMIT 5
OFFSET 5;

-- 补充：上面的查询结果取第11-15条数据(分页查询)
SELECT stu_name,
       cou_name,
       score
  FROM tb_student 
	   NATURAL JOIN tb_record
       NATURAL JOIN tb_course
 WHERE score is not null
 ORDER BY cou_id ASC, score DESC
 LIMIT 10, 5;

-- 查询选课学生的姓名和平均成绩(子查询和表连接)
-- Error Code: 1248. Every derived table must have its own alias
SELECT stu_name,
	   avg_score
  FROM tb_student
       NATURAL JOIN (SELECT stu_id,
                            ROUND(AVG(score), 1) AS avg_score
                       FROM tb_record
                      GROUP BY stu_id) as tmp;

-- 查询学生的姓名和选课的数量(子查询和表连接)
SELECT stu_name,
	   total
  FROM tb_student
       NATURAL JOIN (SELECT stu_id,
                            COUNT(*) AS total
                       FROM tb_record
                      GROUP BY stu_id) as tmp;

-- 查询每个学生的姓名和选课数量(子查询和左外连接)
SELECT stu_name AS 姓名,
	   COALESCE(total, 0) AS 选课数量
  FROM tb_student AS t1
	   LEFT JOIN (SELECT stu_id,
			             COUNT(*) AS total
		            FROM tb_record
				   GROUP BY stu_id) AS t2
	       ON t1.stu_id = t2.stu_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有几个地方需要加以说明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MySQL目前的版本不支持全外连接，上面我们通过&lt;code&gt;union&lt;/code&gt;操作，将左外连接和右外连接的结果求并集实现全外连接的效果。大家可以通过下面的图来加深对连表操作的认识。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;http://localhost/mypic/20211121135117.png&quot; style=&quot;zoom:50%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 中支持多种类型的运算符，包括：算术运算符（&lt;code&gt;+&lt;/code&gt;、&lt;code&gt;-&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;/&lt;/code&gt;、&lt;code&gt;%&lt;/code&gt;）、比较运算符（&lt;code&gt;=&lt;/code&gt;、&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;gt;=&lt;/code&gt;、&lt;code&gt;BETWEEN...AND..&lt;/code&gt;.、&lt;code&gt;IN&lt;/code&gt;、&lt;code&gt;IS NULL&lt;/code&gt;、&lt;code&gt;IS NOT NULL&lt;/code&gt;、&lt;code&gt;LIKE&lt;/code&gt;、&lt;code&gt;RLIKE&lt;/code&gt;、&lt;code&gt;REGEXP&lt;/code&gt;）、逻辑运算符（&lt;code&gt;NOT&lt;/code&gt;、&lt;code&gt;AND&lt;/code&gt;、&lt;code&gt;OR&lt;/code&gt;、&lt;code&gt;XOR&lt;/code&gt;）和位运算符（&lt;code&gt;&amp;amp;&lt;/code&gt;、&lt;code&gt;|&lt;/code&gt;、&lt;code&gt;^&lt;/code&gt;、&lt;code&gt;~&lt;/code&gt;、&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;），我们可以在 DML 中使用这些运算符处理数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在查询数据时，可以在&lt;code&gt;SELECT&lt;/code&gt;语句及其子句（如&lt;code&gt;WHERE&lt;/code&gt;子句、&lt;code&gt;ORDER BY&lt;/code&gt;子句、&lt;code&gt;HAVING&lt;/code&gt;子句等）中使用函数，这些函数包括字符串函数、数值函数、时间日期函数、流程函数等，如下面的表格所示。&lt;/p&gt;
&lt;p&gt;常用字符串函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONCAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将多个字符串连接成一个字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FORMAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将数值格式化成字符串并指定保留几位小数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM_BASE64&lt;/code&gt; / &lt;code&gt;TO_BASE64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BASE64解码/编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BIN&lt;/code&gt; / &lt;code&gt;OCT&lt;/code&gt; / &lt;code&gt;HEX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将数值转换成二进制/八进制/十六进制字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOCATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在字符串中查找一个子串的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEFT&lt;/code&gt; / &lt;code&gt;RIGHT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个字符串左边/右边指定长度的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LENGTH&lt;/code&gt; / &lt;code&gt;CHAR_LENGTH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符串的长度以字节/字符为单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOWER&lt;/code&gt; / &lt;code&gt;UPPER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符串的小写/大写形式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LPAD&lt;/code&gt; / &lt;code&gt;RPAD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果字符串的长度不足，在字符串左边/右边填充指定的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LTRIM&lt;/code&gt; / &lt;code&gt;RTRIM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;去掉字符串前面/后面的空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ORD&lt;/code&gt; / &lt;code&gt;CHAR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符对应的编码/返回编码对应的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STRCMP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;比较字符串，返回-1、0、1分别表示小于、等于、大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUBSTRING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符串指定范围的子串&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常用数值函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ABS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个数的绝度值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CEILING&lt;/code&gt; / &lt;code&gt;FLOOR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个数上取整/下取整的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将一个数从一种进制转换成另一种进制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CRC32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算循环冗余校验码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXP&lt;/code&gt; / &lt;code&gt;LOG&lt;/code&gt; / &lt;code&gt;LOG2&lt;/code&gt; / &lt;code&gt;LOG10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算指数/对数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求幂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RAND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回[0,1)范围的随机数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROUND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个数四舍五入后的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SQRT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个数的平方根&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TRUNCATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;截断一个数到指定的精度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIN&lt;/code&gt; / &lt;code&gt;COS&lt;/code&gt; / &lt;code&gt;TAN&lt;/code&gt; / &lt;code&gt;COT&lt;/code&gt; / &lt;code&gt;ASIN&lt;/code&gt; / &lt;code&gt;ACOS&lt;/code&gt; / &lt;code&gt;ATAN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;三角函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常用时间日期函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CURDATE&lt;/code&gt; / &lt;code&gt;CURTIME&lt;/code&gt; / &lt;code&gt;NOW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取当前日期/时间/日期和时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADDDATE&lt;/code&gt; / &lt;code&gt;SUBDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将两个日期表达式相加/相减并返回结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATE&lt;/code&gt; / &lt;code&gt;TIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从字符串中获取日期/时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;YEAR&lt;/code&gt; / &lt;code&gt;MONTH&lt;/code&gt; / &lt;code&gt;DAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从日期中获取年/月/日&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOUR&lt;/code&gt; / &lt;code&gt;MINUTE&lt;/code&gt; / &lt;code&gt;SECOND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从时间中获取时/分/秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATEDIFF&lt;/code&gt; / &lt;code&gt;TIMEDIFF&lt;/code&gt; / &lt;code&gt;TIMESTAMPDIFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回两个时间日期表达式相差多少天/小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAKEDATE&lt;/code&gt; / &lt;code&gt;MAKETIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;制造一个日期/时间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常用流程控制函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;根据条件是否成立返回不同的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IFNULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果为NULL则返回指定的值否则就返回本身&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NULLIF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两个表达式相等就返回NULL否则返回第一个表达式的值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其他常用函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MD5&lt;/code&gt; / &lt;code&gt;SHA1&lt;/code&gt; / &lt;code&gt;SHA2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符串对应的哈希摘要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHARSET&lt;/code&gt; / &lt;code&gt;COLLATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字符集/校对规则&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER&lt;/code&gt; / &lt;code&gt;CURRENT_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回当前用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回当前数据库名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VERSION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回当前数据库版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOUND_ROWS&lt;/code&gt; / &lt;code&gt;ROW_COUNT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回查询到的行数/受影响的行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LAST_INSERT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回最后一个自增主键的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UUID&lt;/code&gt; / &lt;code&gt;UUID_SHORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回全局唯一标识符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;40 SQL详解之DCL&lt;/h2&gt;
&lt;p&gt;数据库服务器通常包含了非常重要的数据，可以通过访问控制来确保这些数据的安全，而 DCL 就是解决这一问题的，它可以为指定的用户授予访问权限或者从指定用户处召回指定的权限。DCL 对数据库管理员来说非常重要，因为用户权限的管理关系到数据库的安全。简单的说，我们可以通过 DCL 允许受信任的用户访问数据库，阻止不受信任的用户访问数据库，同时还可以通过 DCL 将每个访问者的的权限最小化（让访问者的权限刚刚够用）。&lt;/p&gt;
&lt;h4&gt;创建用户&lt;/h4&gt;
&lt;p&gt;我们可以使用下面的 SQL 来创建一个用户并为其指定访问口令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE USER &apos;wangdachui&apos;@&apos;%&apos; IDENTIFIED BY &apos;Wang.618&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的 SQL 创建了名为 wangdachui 的用户，它的访问口令是 Wang.618，该用户可以从任意主机访问数据库服务器，因为 @ 后面使用了可以表示任意多个字符的通配符 %。如果要限制 wangdachui 这个用户只能从 192.168.0.x 这个网段的主机访问数据库服务器，可以按照下面的方式来修改 SQL 语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP USER IF EXISTS &apos;wangdachui&apos;@&apos;%&apos;;

CREATE USER &apos;wangdachui&apos;@&apos;192.168.0.%&apos; IDENTIFIED BY &apos;Wang.618&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，如果我们使用 wangdachui 这个账号访问数据库服务器，我们几乎不能做任何操作，因为该账号没有任何操作权限。&lt;/p&gt;
&lt;h4&gt;授予权限&lt;/h4&gt;
&lt;p&gt;我们用下面的语句为 wangdachui 授予查询 school 数据库学院表（&lt;code&gt;tb_college&lt;/code&gt;）的权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT SELECT ON `school`.`tb_college` TO &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也可以让 wangdachui 对 school 数据库的所有对象都具有查询权限，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT SELECT ON `school`.* TO &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望 wangdachui 还有 insert、delete 和 update 权限，可以使用下面的方式进行操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT INSERT, DELETE, UPDATE ON `school`.* TO &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们还想授予 wangdachui 执行 DDL 的权限，可以使用如下所示的 SQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT CREATE, DROP, ALTER ON `school`.* TO &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望 wangdachui 账号对所有数据库的所有对象都具备所有的操作权限，可以执行如下所示的操作，但是一般情况下，我们不会这样做，因为我们之前说过，权限刚刚够用就行，一个普通的账号不应该拥有这么大的权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GRANT ALL PRIVILEGES ON *.* TO &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;召回权限&lt;/h4&gt;
&lt;p&gt;如果要召回 wangdachui 对 school 数据库的 insert、delete 和 update 权限，可以使用下面的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REVOKE INSERT, DELETE, UPDATE ON `school`.* FROM &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要召回所有的权限，可以按照如下所示的方式进行操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REVOKE ALL PRIVILEGES ON *.* FROM &apos;wangdachui&apos;@&apos;192.168.0.%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，由于数据库可能会缓存用户的权限，可以在授予或召回权限后执行下面的语句使新的权限即时生效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;41 MySQL新特性&lt;/h2&gt;
&lt;h4&gt;JSON类型&lt;/h4&gt;
&lt;p&gt;很多开发者在使用关系型数据库做数据持久化的时候，常常感到结构化的存储缺乏灵活性，因为必须事先设计好所有的列以及对应的数据类型。在业务发展和变化的过程中，如果需要修改表结构，这绝对是比较麻烦和难受的事情。从 MySQL 5.7 版本开始，MySQL引入了对 JSON 数据类型的支持（MySQL 8.0 解决了 JSON 的日志性能瓶颈问题），用好 JSON 类型，其实就是打破了关系型数据库和非关系型数据库之间的界限，为数据持久化操作带来了更多的便捷。&lt;/p&gt;
&lt;p&gt;JSON 类型主要分为 JSON 对象和 JSON数组两种，如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JSON 对象&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{&quot;name&quot;: &quot;骆昊&quot;, &quot;tel&quot;: &quot;13122335566&quot;, &quot;QQ&quot;: &quot;957658&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;JSON 数组&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[{&quot;name&quot;: &quot;骆昊&quot;, &quot;tel&quot;: &quot;13122335566&quot;}, {&quot;name&quot;: &quot;王大锤&quot;, &quot;QQ&quot;: &quot;123456&quot;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哪些地方需要用到JSON类型呢？举一个简单的例子，现在很多产品的用户登录都支持多种方式，例如手机号、微信、QQ、新浪微博等，但是一般情况下我们又不会要求用户提供所有的这些信息，那么用传统的设计方式，就需要设计多个列来对应多种登录方式，可能还需要允许这些列存在空值，这显然不是很好的选择；另一方面，如果产品又增加了一种登录方式，那么就必然要修改之前的表结构，这就更让人痛苦了。但是，有了 JSON 类型，刚才的问题就迎刃而解了，我们可以做出如下所示的设计。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_test`
(
`user_id`    bigint unsigned,
`login_info` json,
PRIMARY KEY (`user_id`)
);

INSERT INTO `tb_test` 
VALUES 
    (1, &apos;{&quot;tel&quot;: &quot;13122335566&quot;, &quot;QQ&quot;: &quot;654321&quot;, &quot;wechat&quot;: &quot;jackfrued&quot;}&apos;),
    (2, &apos;{&quot;tel&quot;: &quot;13599876543&quot;, &quot;weibo&quot;: &quot;wangdachui123&quot;}&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要查询用户的手机和微信号，可以用如下所示的 SQL 语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `user_id`
     , JSON_UNQUOTE(JSON_EXTRACT(`login_info`, &apos;$.tel&apos;)) AS 手机号
     , JSON_UNQUOTE(JSON_EXTRACT(`login_info`, &apos;$.wechat&apos;)) AS 微信 
FROM `tb_test`;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+---------+-------------+-----------+
| user_id | 手机号      | 微信       |
+---------+-------------+-----------+
|       1 | 13122335566 | jackfrued |
|       2 | 13599876543 | NULL      |
+---------+-------------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为支持 JSON 类型，MySQL 也提供了配套的处理 JSON 数据的函数，就像上面用到的&lt;code&gt;json_extract&lt;/code&gt;和&lt;code&gt;json_unquote&lt;/code&gt;。当然，上面的 SQL 还有更为便捷的写法，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `user_id`
     , `login_info` -&amp;gt;&amp;gt; &apos;$.tel&apos; AS 手机号
     , `login_info` -&amp;gt;&amp;gt; &apos;$.wechat&apos; AS 微信
  FROM `tb_test`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再举个例子，如果我们的产品要实现用户画像功能（给用户打标签），然后基于用户画像给用户推荐平台的服务或消费品之类的东西，我们也可以使用 JSON 类型来保存用户画像数据，示意代码如下所示。&lt;/p&gt;
&lt;p&gt;创建画像标签表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_tags`
(
`tag_id`   int unsigned NOT NULL COMMENT &apos;标签ID&apos;,
`tag_name` varchar(20)  NOT NULL COMMENT &apos;标签名&apos;,
PRIMARY KEY (`tag_id`)
);

INSERT INTO `tb_tags` (`tag_id`, `tag_name`) 
VALUES
    (1, &apos;70后&apos;),
    (2, &apos;80后&apos;),
    (3, &apos;90后&apos;),
    (4, &apos;00后&apos;),
    (5, &apos;爱运动&apos;),
    (6, &apos;高学历&apos;),
    (7, &apos;小资&apos;),
    (8, &apos;有房&apos;),
    (9, &apos;有车&apos;),
    (10, &apos;爱看电影&apos;),
    (11, &apos;爱网购&apos;),
    (12, &apos;常点外卖&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为用户打标签。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_users_tags`
(
`user_id`   bigint unsigned NOT NULL COMMENT &apos;用户ID&apos;,
`user_tags` json            NOT NULL COMMENT &apos;用户标签&apos;
);

INSERT INTO `tb_users_tags`
VALUES
    (1, &apos;[2, 6, 8, 10]&apos;),
    (2, &apos;[3, 10, 12]&apos;),
    (3, &apos;[3, 8, 9, 11]&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们通过一组查询来了解 JSON 类型的巧妙之处。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查询爱看电影（有&lt;code&gt;10&lt;/code&gt;这个标签）的用户ID。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `user_id`
  FROM `tb_users_tags`
 WHERE 10 MEMBER OF (`user_tags`-&amp;gt;&apos;$&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询爱看电影（有&lt;code&gt;10&lt;/code&gt;这个标签）的80后（有&lt;code&gt;2&lt;/code&gt;这个标签）用户ID。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `user_id`
  FROM `tb_users_tags`
 WHERE JSON_CONTAINS(`user_tags`-&amp;gt;&apos;$&apos;, &apos;[2, 10]&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询爱看电影或80后或90后的用户ID。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT `user_id`
  FROM `tb_users_tags`
 WHERE JSON_OVERLAPS(user_tags-&amp;gt;&apos;$&apos;, &apos;[2, 3, 10]&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的查询用到了&lt;code&gt;member of&lt;/code&gt;谓词和两个 JSON 函数，&lt;code&gt;json_contains&lt;/code&gt;可以检查 JSON 数组是否包含了指定的元素，而&lt;code&gt;json_overlaps&lt;/code&gt;可以检查 JSON 数组是否与指定的数组有重叠部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;窗口函数&lt;/h4&gt;
&lt;p&gt;MySQL 从8.0开始支持窗口函数，大多数商业数据库和一些开源数据库早已提供了对窗口函数的支持，有的也将其称之为 OLAP（联机分析和处理）函数，听名字就知道跟统计和分析相关。为了帮助大家理解窗口函数，我们先说说窗口的概念。&lt;/p&gt;
&lt;p&gt;窗口可以理解为记录的集合，窗口函数也就是在满足某种条件的记录集合上执行的特殊函数，对于每条记录都要在此窗口内执行函数。窗口函数和我们上面讲到的聚合函数比较容易混淆，二者的区别主要在于聚合函数是将多条记录聚合为一条记录，窗口函数是每条记录都会执行，执行后记录条数不会变。窗口函数不仅仅是几个函数，它是一套完整的语法，函数只是该语法的一部分，基本语法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;窗口函数&amp;gt; OVER (PARTITION BY &amp;lt;用于分组的列名&amp;gt; ORDER BY &amp;lt;用于排序的列名&amp;gt;  ROWS BETWEEN ... AND ...)
&amp;lt;窗口函数&amp;gt; OVER (PARTITION BY &amp;lt;用于分组的列名&amp;gt; ORDER BY &amp;lt;用于排序的列名&amp;gt; RANGE BETWEEN ... AND ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面语法中，窗口函数的位置可以放以下两种函数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;专用窗口函数，包括：&lt;code&gt;lead&lt;/code&gt;、&lt;code&gt;lag&lt;/code&gt;、&lt;code&gt;first_value&lt;/code&gt;、&lt;code&gt;last_value&lt;/code&gt;、&lt;code&gt;rank&lt;/code&gt;、&lt;code&gt;dense_rank&lt;/code&gt;和&lt;code&gt;row_number&lt;/code&gt;等。&lt;/li&gt;
&lt;li&gt;聚合函数，包括：&lt;code&gt;sum&lt;/code&gt;、&lt;code&gt;avg&lt;/code&gt;、&lt;code&gt;max&lt;/code&gt;、&lt;code&gt;min&lt;/code&gt;和&lt;code&gt;count&lt;/code&gt;等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面为大家举几个使用窗口函数的简单例子，我们直接使用上一课创建的 hrs 数据库。&lt;/p&gt;
&lt;p&gt;例子1：查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * 
  FROM (SELECT `ename`
             , `sal`
             , ROW_NUMBER() over (ORDER BY `sal` DESC) AS `rk`
	      FROM `tb_emp`) AS `temp`
 WHERE `rk` between 4 and 6;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面使用的函数&lt;code&gt;row_number()&lt;/code&gt;可以为每条记录生成一个行号，在实际工作中可以根据需要将其替换为&lt;code&gt;rank()&lt;/code&gt;或&lt;code&gt;dense_rank()&lt;/code&gt;函数，三者的区别可以参考官方文档或阅读&lt;a href=&quot;https://zhuanlan.zhihu.com/p/92654574&quot;&gt;《通俗易懂的学会：SQL窗口函数》&lt;/a&gt;进行了解。在MySQL 8以前的版本，我们可以通过下面的方式来完成类似的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select `rank`, `ename`, `sal` from (
       select @a:=@a+1 as `rank`, `ename`, `sal` 
       from `tb_emp`, (select @a:=0) as t1 order by `sal` desc
) as `temp` where `rank` between 4 and 6;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;例子2：查询每个部门月薪最高的两名的员工的姓名和部门名称。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select `ename`, `sal`, `dname` 
from (
    select 
        `ename`, `sal`, `dno`,
        rank() over (partition by `dno` order by `sal` desc) as `rank`
    from `tb_emp`
) as `temp` natural join `tb_dept` where `rank`&amp;lt;=2;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：在MySQL 8以前的版本，我们可以通过下面的方式来完成类似的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select `ename`, `sal`, `dname` from `tb_emp` as `t1` 
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;natural join &lt;code&gt;tb_dept&lt;/code&gt;
where (
select count(*) from &lt;code&gt;tb_emp&lt;/code&gt; as &lt;code&gt;t2&lt;/code&gt;
where &lt;code&gt;t1&lt;/code&gt;.&lt;code&gt;dno&lt;/code&gt;=&lt;code&gt;t2&lt;/code&gt;.&lt;code&gt;dno&lt;/code&gt; and &lt;code&gt;t2&lt;/code&gt;.&lt;code&gt;sal&lt;/code&gt;&amp;gt;&lt;code&gt;t1&lt;/code&gt;.&lt;code&gt;sal&lt;/code&gt;
)&amp;lt;2 order by &lt;code&gt;dno&lt;/code&gt; asc, &lt;code&gt;sal&lt;/code&gt; desc;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;42 视图、函数和过程&lt;/h2&gt;
&lt;p&gt;为了讲解视图、函数和过程，我们首先用下面的 DDL 和 DML 创建名为 hrs 的数据库并为其二维表添加如下所示的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建名为hrs的数据库并指定默认的字符集
create database `hrs` default charset utf8mb4;

-- 切换到hrs数据库
use `hrs`;

-- 创建部门表
create table `tb_dept`
(
`dno` int not null comment &apos;编号&apos;,
`dname` varchar(10) not null comment &apos;名称&apos;,
`dloc` varchar(20) not null comment &apos;所在地&apos;,
primary key (`dno`)
);

-- 插入4个部门
insert into `tb_dept` values 
    (10, &apos;会计部&apos;, &apos;北京&apos;),
    (20, &apos;研发部&apos;, &apos;成都&apos;),
    (30, &apos;销售部&apos;, &apos;重庆&apos;),
    (40, &apos;运维部&apos;, &apos;深圳&apos;);

-- 创建员工表
create table `tb_emp`
(
`eno` int not null comment &apos;员工编号&apos;,
`ename` varchar(20) not null comment &apos;员工姓名&apos;,
`job` varchar(20) not null comment &apos;员工职位&apos;,
`mgr` int comment &apos;主管编号&apos;,
`sal` int not null comment &apos;员工月薪&apos;,
`comm` int comment &apos;每月补贴&apos;,
`dno` int not null comment &apos;所在部门编号&apos;,
primary key (`eno`),
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
);

-- 插入14个员工
insert into `tb_emp` values 
    (7800, &apos;张三丰&apos;, &apos;总裁&apos;, null, 9000, 1200, 20),
    (2056, &apos;乔峰&apos;, &apos;分析师&apos;, 7800, 5000, 1500, 20),
    (3088, &apos;李莫愁&apos;, &apos;设计师&apos;, 2056, 3500, 800, 20),
    (3211, &apos;张无忌&apos;, &apos;程序员&apos;, 2056, 3200, null, 20),
    (3233, &apos;丘处机&apos;, &apos;程序员&apos;, 2056, 3400, null, 20),
    (3251, &apos;张翠山&apos;, &apos;程序员&apos;, 2056, 4000, null, 20),
    (5566, &apos;宋远桥&apos;, &apos;会计师&apos;, 7800, 4000, 1000, 10),
    (5234, &apos;郭靖&apos;, &apos;出纳&apos;, 5566, 2000, null, 10),
    (3344, &apos;黄蓉&apos;, &apos;销售主管&apos;, 7800, 3000, 800, 30),
    (1359, &apos;胡一刀&apos;, &apos;销售员&apos;, 3344, 1800, 200, 30),
    (4466, &apos;苗人凤&apos;, &apos;销售员&apos;, 3344, 2500, null, 30),
    (3244, &apos;欧阳锋&apos;, &apos;程序员&apos;, 3088, 3200, null, 20),
    (3577, &apos;杨过&apos;, &apos;会计&apos;, 5566, 2200, null, 10),
    (3588, &apos;朱九真&apos;, &apos;会计&apos;, 5566, 2500, null, 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;视图&lt;/h4&gt;
&lt;p&gt;视图是关系型数据库中将一组查询指令构成的结果集组合成可查询的数据表的对象。简单的说，视图就是虚拟的表，但与数据表不同的是，数据表是一种实体结构，而视图是一种虚拟结构，你也可以将视图理解为保存在数据库中被赋予名字的 SQL 语句。&lt;/p&gt;
&lt;p&gt;使用视图可以获得以下好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以将实体数据表隐藏起来，让外部程序无法得知实际的数据结构，让访问者可以使用表的组成部分而不是整个表，降低数据库被攻击的风险。&lt;/li&gt;
&lt;li&gt;在大多数的情况下视图是只读的（更新视图的操作通常都有诸多的限制），外部程序无法直接透过视图修改数据。&lt;/li&gt;
&lt;li&gt;重用 SQL 语句，将高度复杂的查询包装在视图表中，直接访问该视图即可取出需要的数据；也可以将视图视为数据表进行连接查询。&lt;/li&gt;
&lt;li&gt;视图可以返回与实体数据表不同格式的数据，在创建视图的时候可以对数据进行格式化处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;创建视图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create view `vw_emp_simple`
as
select  `eno`,
        `ename`,
        `job`,
        `dno`
  from  `tb_emp`;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：因为视图不包含数据，所以每次使用视图时，都必须执行查询以获得数据，如果你使用了连接查询、嵌套查询创建了较为复杂的视图，你可能会发现查询性能下降得很厉害。因此，在使用复杂的视图前，应该进行测试以确保其性能能够满足应用的需求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了上面的视图，我们就可以使用之前讲过的 DCL， 限制某些用户只能从视图中获取员工信息，这样员工表中的工资（&lt;code&gt;sal&lt;/code&gt;）、补贴（&lt;code&gt;comm&lt;/code&gt;）等敏感字段便不会暴露给用户。下面的代码演示了如何从视图中获取数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from `vw_emp_simple`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+------+-----------+--------------+-----+
| eno  | ename     | job          | dno |
+------+-----------+--------------+-----+
| 1359 | 胡二刀    | 销售员       |  30 |
| 2056 | 乔峰      | 分析师       |  20 |
| 3088 | 李莫愁    | 设计师       |  20 |
| 3211 | 张无忌    | 程序员       |  20 |
| 3233 | 丘处机    | 程序员       |  20 |
| 3244 | 欧阳锋    | 程序员       |  20 |
| 3251 | 张翠山    | 程序员       |  20 |
| 3344 | 黄蓉      | 销售主管     |  30 |
| 3577 | 杨过      | 会计         |  10 |
| 3588 | 朱九真    | 会计         |  10 |
| 4466 | 苗人凤    | 销售员       |  30 |
| 5234 | 郭靖      | 出纳         |  10 |
| 5566 | 宋远桥    | 会计师       |  10 |
| 7800 | 张三丰    | 总裁         |  20 |
+------+-----------+--------------+-----+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然视图是一张虚拟的表，那么视图的中的数据可以更新吗？视图的可更新性要视具体情况而定，以下类型的视图是不能更新的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用了聚合函数（&lt;code&gt;SUM&lt;/code&gt;、&lt;code&gt;MIN&lt;/code&gt;、&lt;code&gt;MAX&lt;/code&gt;、&lt;code&gt;AVG&lt;/code&gt;、&lt;code&gt;COUNT&lt;/code&gt;等）、&lt;code&gt;DISTINCT&lt;/code&gt;、&lt;code&gt;GROUP BY&lt;/code&gt;、&lt;code&gt;HAVING&lt;/code&gt;、&lt;code&gt;UNION&lt;/code&gt;或者&lt;code&gt;UNION ALL&lt;/code&gt;的视图。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt;中包含了子查询的视图。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FROM&lt;/code&gt;子句中包含了一个不能更新的视图的视图。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE&lt;/code&gt;子句的子查询引用了&lt;code&gt;FROM&lt;/code&gt;子句中的表的视图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;删除视图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop view if exists `vw_emp_simple`;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果希望更新视图，可以先用上面的命令删除视图，也可以通过&lt;code&gt;create or replace view&lt;/code&gt;来更新视图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;视图的规则和限制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;视图可以嵌套，可以利用从其他视图中检索的数据来构造一个新的视图。视图也可以和表一起使用。&lt;/li&gt;
&lt;li&gt;创建视图时可以使用&lt;code&gt;order by&lt;/code&gt;子句，但如果从视图中检索数据时也使用了&lt;code&gt;order by&lt;/code&gt;，那么该视图中原先的&lt;code&gt;order by&lt;/code&gt;会被覆盖。&lt;/li&gt;
&lt;li&gt;视图无法使用索引，也不会激发触发器（实际开发中因为性能等各方面的考虑，通常不建议使用触发器，所以我们也不对这个概念进行介绍）的执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;函数&lt;/h4&gt;
&lt;p&gt;MySQL 中的函数跟 Python 中的函数大同小异，因为函数都是用来封装功能上相对独立且会被重复使用的代码的。如果非要找出一些差别来，那么 MySQL 中的函数是可以执行 SQL 语句的。下面的例子，我们通过自定义函数实现了截断超长字符串的功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delimiter $$

create function fn_truncate_string(
    content varchar(10000),
    max_length int unsigned
) returns varchar(10000) no sql
begin
    declare result varchar(10000) default content;
    if char_length(content) &amp;gt; max_length then
        set result = left(content, max_length);
        set result = concat(result, &apos;……&apos;);
    end if;
    return result;
end $$

delimiter ;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明1&lt;/strong&gt;：函数声明后面的&lt;code&gt;no sql&lt;/code&gt;是声明函数体并没有使用 SQL 语句；如果函数体中需要通过 SQL 读取数据，需要声明为&lt;code&gt;reads sql data&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明2&lt;/strong&gt;：定义函数前后的&lt;code&gt;delimiter&lt;/code&gt;命令是为了修改终止符（定界符），因为函数体中的语句都是用&lt;code&gt;;&lt;/code&gt;表示结束，如果不重新定义定界符，那么遇到的&lt;code&gt;;&lt;/code&gt;的时候代码就会被截断执行，显然这不是我们想要的效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在查询中调用自定义函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select fn_truncate_string(&apos;和我在成都的街头走一走，直到所有的灯都熄灭了也不停留&apos;, 10) as short_string;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+--------------------------------------+
| short_string                         |
+--------------------------------------+
| 和我在成都的街头走一……                 |
+--------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;过程&lt;/h4&gt;
&lt;p&gt;过程（又称存储过程）是事先编译好存储在数据库中的一组 SQL 的集合，调用过程可以简化应用程序开发人员的工作，减少与数据库服务器之间的通信，对于提升数据操作的性能也是有帮助的。其实迄今为止，我们使用的 SQL 语句都是针对一个或多个表的单条语句，但在实际开发中经常会遇到某个操作需要多条 SQL 语句才能完成的情况。例如，电商网站在受理用户订单时，需要做以下一系列的处理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过查询来核对库存中是否有对应的物品以及库存是否充足。&lt;/li&gt;
&lt;li&gt;如果库存有物品，需要锁定库存以确保这些物品不再卖给别人， 并且要减少可用的物品数量以反映正确的库存量。&lt;/li&gt;
&lt;li&gt;如果库存不足，可能需要进一步与供应商进行交互或者至少产生一条系统提示消息。&lt;/li&gt;
&lt;li&gt;不管受理订单是否成功，都需要产生流水记录，而且需要给对应的用户产生一条通知信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以通过过程将复杂的操作封装起来，这样不仅有助于保证数据的一致性，而且将来如果业务发生了变动，只需要调整和修改过程即可。对于调用过程的用户来说，过程并没有暴露数据表的细节，而且执行过程比一条条的执行一组 SQL 要快得多。&lt;/p&gt;
&lt;p&gt;下面的过程实现 hrs 数据库中员工工资的普调，具体的规则是：&lt;code&gt;10&lt;/code&gt;部门的员工薪资上浮&lt;code&gt;300&lt;/code&gt;， &lt;code&gt;20&lt;/code&gt;部门的员工薪资上浮&lt;code&gt;800&lt;/code&gt;，&lt;code&gt;30&lt;/code&gt;部门的员工薪资上浮&lt;code&gt;500&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delimiter $$

create procedure sp_upgrade_salary()
begin
    declare flag boolean default 1;
    -- 定义一个异常处理器
    declare continue handler for sqlexception set flag=0;

    -- 开启事务环境
    start transaction;
    
    update tb_emp set sal=sal+300 where dno=10;
    update tb_emp set sal=sal+800 where dno=20;
    update tb_emp set sal=sal+500 where dno=30;

    -- 提交或回滚事务
    if flag then
        commit;
    else
        rollback;
    end if;
end $$

delimiter ;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的过程代码中使用了&lt;code&gt;start transaction&lt;/code&gt;来开启事务环境，关于事务，在本课的最后有一个简单的介绍。为了确定代码中是否发生异常，从而提交或回滚事务，上面的过程中定义了一个名为&lt;code&gt;flag&lt;/code&gt;的变量和一个异常处理器，如果发生了异常，&lt;code&gt;flag&lt;/code&gt;将会被赋值为&lt;code&gt;0&lt;/code&gt;，后面的分支结构会根据&lt;code&gt;flag&lt;/code&gt;的值来决定是执行&lt;code&gt;commit&lt;/code&gt;，还是执行&lt;code&gt;rollback&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;调用过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;call sp_upgrade_salary();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop procedure if exists sp_upgrade_salary;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在过程中，我们可以定义变量、条件，可以使用分支和循环语句，可以通过游标操作查询结果，还可以使用事件调度器，这些内容我们暂时不在此处进行介绍。虽然我们说了很多过程的好处，但是在实际开发中，如果频繁的使用过程并将大量复杂的运算放到过程中，会给据库服务器造成巨大的压力，而数据库往往都是性能瓶颈所在，使用过程无疑是雪上加霜的操作。所以，对于互联网产品开发，我们一般建议让数据库只做好存储，复杂的运算和处理交给应用服务器上的程序去完成，如果应用服务器变得不堪重负了，我们可以比较容易的部署多台应用服务器来分摊这些压力。&lt;/p&gt;
&lt;p&gt;如果大家对上面讲到的视图、函数、过程包括我们没有讲到的触发器这些知识有兴趣，建议大家阅读 MySQL 的入门读物&lt;a href=&quot;https://item.jd.com/12818982.html&quot;&gt;《MySQL必知必会》&lt;/a&gt;进行一般性了解即可，因为这些知识点在大家将来的工作中未必用得上，学了也可能仅仅是为了应付面试而已。&lt;/p&gt;
&lt;h4&gt;其他内容&lt;/h4&gt;
&lt;h5&gt;范式理论&lt;/h5&gt;
&lt;p&gt;范式理论是设计关系型数据库中二维表的指导思想。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一范式：数据表的每个列的值域都是由原子值组成的，不能够再分割。&lt;/li&gt;
&lt;li&gt;第二范式：数据表里的所有数据都要和该数据表的键（主键与候选键）有完全依赖关系。&lt;/li&gt;
&lt;li&gt;第三范式：所有非键属性都只和候选键有相关性，也就是说非键属性之间应该是独立无关的。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：实际工作中，出于效率的考虑，我们在设计表时很有可能做出反范式设计，即故意降低方式级别，增加冗余数据来获得更好的操作性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;数据完整性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;实体完整性 - 每个实体都是独一无二的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主键（&lt;code&gt;primary key&lt;/code&gt;） / 唯一约束（&lt;code&gt;unique&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引用完整性（参照完整性）- 关系中不允许引用不存在的实体&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外键（&lt;code&gt;foreign key&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;域（domain）完整性 - 数据是有效的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据类型及长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非空约束（&lt;code&gt;not null&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认值约束（&lt;code&gt;default&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查约束（&lt;code&gt;check&lt;/code&gt;）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：在 MySQL 8.x 以前，检查约束并不起作用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;数据一致性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;事务：一系列对数据库进行读/写的操作，这些操作要么全都成功，要么全都失败。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务的 ACID 特性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子性：事务作为一个整体被执行，包含在其中的对数据库的操作要么全部被执行，要么都不执行&lt;/li&gt;
&lt;li&gt;一致性：事务应确保数据库的状态从一个一致状态转变为另一个一致状态&lt;/li&gt;
&lt;li&gt;隔离性：多个事务并发执行时，一个事务的执行不应影响其他事务的执行&lt;/li&gt;
&lt;li&gt;持久性：已被提交的事务对数据库的修改应该永久保存在数据库中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MySQL 中的事务操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开启事务环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start transaction
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提交事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回滚事务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rollback
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看事务隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show variables like &apos;transaction_isolation&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，MySQL 默认的事务隔离级别是&lt;code&gt;REPEATABLE-READ&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改（当前会话）事务隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set session transaction isolation level read committed;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新查看事务隔离级别，结果如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关系型数据库的事务是一个很大的话题，因为当存在多个并发事务访问数据时，就有可能出现三类读数据的问题（脏读、不可重复读、幻读）和两类更新数据的问题（第一类丢失更新、第二类丢失更新）。想了解这五类问题的，可以阅读我发布在 CSDN 网站上的&lt;a href=&quot;https://blog.csdn.net/jackfrued/article/details/44921941&quot;&gt;《Java面试题全集（上）》&lt;/a&gt;一文的第80题。为了避免这些问题，关系型数据库底层是有对应的锁机制的，按锁定对象不同可以分为表级锁和行级锁，按并发事务锁定关系可以分为共享锁和独占锁。然而直接使用锁是非常麻烦的，为此数据库为用户提供了自动锁机制，只要用户指定适当的事务隔离级别，数据库就会通过分析 SQL 语句，然后为事务访问的资源加上合适的锁。此外，数据库还会维护这些锁通过各种手段提高系统的性能，这些对用户来说都是透明的。想了解 MySQL 事务和锁的细节知识，推荐大家阅读进阶读物&lt;a href=&quot;https://item.jd.com/11220393.html&quot;&gt;《高性能MySQL》&lt;/a&gt;，这也是数据库方面的经典书籍。&lt;/p&gt;
&lt;p&gt;ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别，如下表所示。需要说明的是，事务隔离级别和数据访问的并发性是对立的，事务隔离级别越高并发性就越差。所以要根据具体的应用来确定到底使用哪种事务隔离级别，这个地方没有万能的原则。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;http://localhost/mypic/20211121225327.png&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;关于 MySQL 的知识肯定远远不止上面列出的这些，比如 MySQL 性能调优、MySQL 运维相关工具、MySQL 数据的备份和恢复、监控 MySQL 服务、部署高可用架构等，这一系列的问题在这里都没有办法逐一展开来讨论，那就留到有需要的时候再进行讲解吧，各位读者也可以自行探索。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;43 索引&lt;/h2&gt;
&lt;p&gt;索引是关系型数据库中用来提升查询性能最为重要的手段。关系型数据库中的索引就像一本书的目录，我们可以想象一下，如果要从一本书中找出某个知识点，但是这本书没有目录，这将是一件多么可怕的事情！我们估计得一篇一篇的翻下去，才能确定这个知识点到底在什么位置。创建索引虽然会带来存储空间上的开销，就像一本书的目录会占用一部分篇幅一样，但是在牺牲空间后换来的查询时间的减少也是非常显著的。&lt;/p&gt;
&lt;p&gt;MySQL 数据库中所有数据类型的列都可以被索引。对于MySQL 8.0 版本的 InnoDB 存储引擎来说，它支持三种类型的索引，分别是 B+ 树索引、全文索引和 R 树索引。这里，我们只介绍使用得最为广泛的 B+ 树索引。使用 B+ 树的原因非常简单，因为它是目前在基于磁盘进行海量数据存储和排序上最有效率的数据结构。B+ 树是一棵&lt;a href=&quot;https://zh.wikipedia.org/zh-cn/%E5%B9%B3%E8%A1%A1%E6%A0%91&quot;&gt;平衡树&lt;/a&gt;，树的高度通常为3或4，但是却可以保存从百万级到十亿级的数据，而从这些数据里面查询一条数据，只需要3次或4次 I/O 操作。&lt;/p&gt;
&lt;p&gt;B+ 树由根节点、中间节点和叶子节点构成，其中叶子节点用来保存排序后的数据。由于记录在索引上是排序过的，因此在一个叶子节点内查找数据时可以使用二分查找，这种查找方式效率非常的高。当数据很少的时候，B+ 树只有一个根节点，数据也就保存在根节点上。随着记录越来越多，B+ 树会发生分裂，根节点不再保存数据，而是提供了访问下一层节点的指针，帮助快速确定数据在哪个叶子节点上。&lt;/p&gt;
&lt;p&gt;在创建二维表时，我们通常都会为表指定主键列，主键列上默认会创建索引，而对于 MySQL InnoDB 存储引擎来说，因为它使用的是索引组织表这种数据存储结构，所以主键上的索引就是整张表的数据，而这种索引我们也将其称之为&lt;strong&gt;聚集索引&lt;/strong&gt;（clustered index）。很显然，一张表只能有一个聚集索引，否则表的数据岂不是要保存多次。我们自己创建的索引都是二级索引（secondary index），更常见的叫法是&lt;strong&gt;非聚集索引&lt;/strong&gt;（non-clustered index）。通过我们自定义的非聚集索引只能定位记录的主键，在获取数据时可能需要再通过主键上的聚集索引进行查询，这种现象称为“回表”，因此通过非聚集索引检索数据通常比使用聚集索引检索数据要慢。&lt;/p&gt;
&lt;p&gt;接下来我们通过一个简单的例子来说明索引的意义，比如我们要根据学生的姓名来查找学生，这个场景在实际开发中应该经常遇到，就跟通过商品名称查找商品是一个道理。我们可以使用 MySQL 的&lt;code&gt;explain&lt;/code&gt;关键字来查看 SQL 的执行计划（数据库执行 SQL 语句的具体步骤）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explain select * from tb_student where stuname=&apos;林震南&apos;\G
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tb_student
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 11
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的 SQL 执行计划中，有几项值得我们关注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;select_type&lt;/code&gt;：查询的类型。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SIMPLE&lt;/code&gt;：简单 SELECT，不需要使用 UNION 操作或子查询。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PRIMARY&lt;/code&gt;：如果查询包含子查询，最外层的 SELECT 被标记为 PRIMARY。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UNION&lt;/code&gt;：UNION 操作中第二个或后面的 SELECT 语句。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUBQUERY&lt;/code&gt;：子查询中的第一个 SELECT。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DERIVED&lt;/code&gt;：派生表的 SELECT 子查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table&lt;/code&gt;：查询对应的表。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：MySQL 在表中找到满足条件的行的方式，也称为访问类型，包括：&lt;code&gt;ALL&lt;/code&gt;（全表扫描）、&lt;code&gt;index&lt;/code&gt;（索引全扫描，只遍历索引树）、&lt;code&gt;range&lt;/code&gt;（索引范围扫描）、&lt;code&gt;ref&lt;/code&gt;（非唯一索引扫描）、&lt;code&gt;eq_ref&lt;/code&gt;（唯一索引扫描）、&lt;code&gt;const&lt;/code&gt; / &lt;code&gt;system&lt;/code&gt;（常量级查询）、&lt;code&gt;NULL&lt;/code&gt;（不需要访问表或索引）。在所有的访问类型中，很显然 ALL 是性能最差的，它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;possible_keys&lt;/code&gt;：MySQL 可以选择的索引，但是&lt;strong&gt;有可能不会使用&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt;：MySQL 真正使用的索引，如果为&lt;code&gt;NULL&lt;/code&gt;就表示没有使用索引。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key_len&lt;/code&gt;：使用的索引的长度，在不影响查询的情况下肯定是长度越短越好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rows&lt;/code&gt;：执行查询需要扫描的行数，这是一个&lt;strong&gt;预估值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extra&lt;/code&gt;：关于查询额外的信息。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Using filesort&lt;/code&gt;：MySQL 无法利用索引完成排序操作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Using index&lt;/code&gt;：只使用索引的信息而不需要进一步查表来获取更多的信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Using temporary&lt;/code&gt;：MySQL 需要使用临时表来存储结果集，常用于分组和排序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Impossible where&lt;/code&gt;：&lt;code&gt;where&lt;/code&gt;子句会导致没有符合条件的行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Distinct&lt;/code&gt;：MySQL 发现第一个匹配行后，停止为当前的行组合搜索更多的行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Using where&lt;/code&gt;：查询的列未被索引覆盖，筛选条件并不是索引的前导列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上面的执行计划可以看出，当我们通过学生名字查询学生时实际上是进行了全表扫描，不言而喻这个查询性能肯定是非常糟糕的，尤其是在表中的行很多的时候。如果我们需要经常通过学生姓名来查询学生，那么就应该在学生姓名对应的列上创建索引，通过索引来加速查询。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create index idx_student_name on tb_student(stuname);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次查看刚才的 SQL 对应的执行计划。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explain select * from tb_student where stuname=&apos;林震南&apos;\G
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tb_student
   partitions: NULL
         type: ref
possible_keys: idx_student_name
          key: idx_student_name
      key_len: 62
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以注意到，在对学生姓名创建索引后，刚才的查询已经不是全表扫描而是基于索引的查询，而且扫描的行只有唯一的一行，这显然大大的提升了查询的性能。MySQL 中还允许创建前缀索引，即对索引字段的前N个字符创建索引，这样的话可以减少索引占用的空间（但节省了空间很有可能会浪费时间，&lt;strong&gt;时间和空间是不可调和的矛盾&lt;/strong&gt;），如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create index idx_student_name_1 on tb_student(stuname(1));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的索引相当于是根据学生姓名的第一个字来创建的索引，我们再看看 SQL 执行计划。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explain select * from tb_student where stuname=&apos;林震南&apos;\G
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tb_student
   partitions: NULL
         type: ref
possible_keys: idx_student_name
          key: idx_student_name
      key_len: 5
          ref: const
         rows: 2
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知道大家是否注意到，这一次扫描的行变成了2行，因为学生表中有两个姓“林”的学生，我们只用姓名的第一个字作为索引的话，在查询时通过索引就会找到这两行。&lt;/p&gt;
&lt;p&gt;如果要删除索引，可以使用下面的SQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table tb_student drop index idx_student_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop index idx_student_name on tb_student;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在创建索引时，我们还可以使用复合索引、函数索引（MySQL 5.7 开始支持），用好复合索引实现&lt;strong&gt;索引覆盖&lt;/strong&gt;可以减少不必要的排序和回表操作，这样就会让查询的性能成倍的提升，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;p&gt;我们简单的为大家总结一下索引的设计原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;最适合&lt;/strong&gt;索引的列是出现在&lt;strong&gt;WHERE子句&lt;/strong&gt;和连接子句中的列。&lt;/li&gt;
&lt;li&gt;索引列的基数越大（取值多、重复值少），索引的效果就越好。&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;前缀索引&lt;/strong&gt;可以减少索引占用的空间，内存中可以缓存更多的索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引不是越多越好&lt;/strong&gt;，虽然索引加速了读操作（查询），但是写操作（增、删、改）都会变得更慢，因为数据的变化会导致索引的更新，就如同书籍章节的增删需要更新目录一样。&lt;/li&gt;
&lt;li&gt;使用 InnoDB 存储引擎时，表的普通索引都会保存主键的值，所以&lt;strong&gt;主键要尽可能选择较短的数据类型&lt;/strong&gt;，这样可以有效的减少索引占用的空间，提升索引的缓存效果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后，还有一点需要说明，InnoDB 使用的 B-tree 索引，数值类型的列除了等值判断时索引会生效之外，使用&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;=&lt;/code&gt;、&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;BETWEEN...AND... &lt;/code&gt;、&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;时，索引仍然生效；对于字符串类型的列，如果使用不以通配符开头的模糊查询，索引也是起作用的，但是其他的情况会导致索引失效，这就意味着很有可能会做全表查询。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;44 Python接入MySQL数据库&lt;/h2&gt;
&lt;p&gt;在 Python3 中，我们可以使用&lt;code&gt;mysqlclient&lt;/code&gt;或者&lt;code&gt;pymysql&lt;/code&gt;三方库来接入 MySQL 数据库并实现数据持久化操作。二者的用法完全相同，只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库&lt;code&gt;pymysql&lt;/code&gt;，因为它更容易安装成功。下面我们仍然以之前创建的名为&lt;code&gt;hrs&lt;/code&gt;的数据库为例，为大家演示如何通过 Python 程序操作 MySQL 数据库实现数据持久化操作。&lt;/p&gt;
&lt;h4&gt;接入MySQL&lt;/h4&gt;
&lt;p&gt;首先，我们可以在命令行或者 PyCharm 的终端中通过下面的命令安装&lt;code&gt;pymysql&lt;/code&gt;，如果需要接入 MySQL 8，还需要安装一个名为&lt;code&gt;cryptography&lt;/code&gt;的三方库来支持 MySQL 8 的密码认证方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pymysql cryptography
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;pymysql&lt;/code&gt;操作 MySQL 的步骤如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建连接。MySQL 服务器启动后，提供了基于 TCP （传输控制协议）的网络服务。我们可以通过&lt;code&gt;pymysql&lt;/code&gt;模块的&lt;code&gt;connect&lt;/code&gt;函数连接 MySQL 服务器。在调用&lt;code&gt;connect&lt;/code&gt;函数时，需要指定主机（&lt;code&gt;host&lt;/code&gt;）、端口（&lt;code&gt;port&lt;/code&gt;）、用户名（&lt;code&gt;user&lt;/code&gt;）、口令（&lt;code&gt;password&lt;/code&gt;）、数据库（&lt;code&gt;database&lt;/code&gt;）、字符集（&lt;code&gt;charset&lt;/code&gt;）等参数，该函数会返回一个&lt;code&gt;Connection&lt;/code&gt;对象。&lt;/li&gt;
&lt;li&gt;获取游标。连接 MySQL 服务器成功后，接下来要做的就是向数据库服务器发送 SQL 语句，MySQL 会执行接收到的 SQL 并将执行结果通过网络返回。要实现这项操作，需要先通过连接对象的&lt;code&gt;cursor&lt;/code&gt;方法获取游标（&lt;code&gt;Cursor&lt;/code&gt;）对象。&lt;/li&gt;
&lt;li&gt;发出 SQL。通过游标对象的&lt;code&gt;execute&lt;/code&gt;方法，我们可以向数据库发出 SQL 语句。&lt;/li&gt;
&lt;li&gt;如果执行&lt;code&gt;insert&lt;/code&gt;、&lt;code&gt;delete&lt;/code&gt;或&lt;code&gt;update&lt;/code&gt;操作，需要根据实际情况提交或回滚事务。因为创建连接时，默认开启了事务环境，在操作完成后，需要使用连接对象的&lt;code&gt;commit&lt;/code&gt;或&lt;code&gt;rollback&lt;/code&gt;方法，实现事务的提交或回滚，&lt;code&gt;rollback&lt;/code&gt;方法通常会放在异常捕获代码块&lt;code&gt;except&lt;/code&gt;中。如果执行&lt;code&gt;select&lt;/code&gt;操作，需要通过游标对象抓取查询的结果，对应的方法有三个，分别是：&lt;code&gt;fetchone&lt;/code&gt;、&lt;code&gt;fetchmany&lt;/code&gt;和&lt;code&gt;fetchall&lt;/code&gt;。其中&lt;code&gt;fetchone&lt;/code&gt;方法会抓取到一条记录，并以元组或字典的方式返回；&lt;code&gt;fetchmany&lt;/code&gt;和&lt;code&gt;fetchall&lt;/code&gt;方法会抓取到多条记录，以嵌套元组或列表装字典的方式返回。&lt;/li&gt;
&lt;li&gt;关闭连接。在完成持久化操作后，请不要忘记关闭连接，释放外部资源。我们通常会在&lt;code&gt;finally&lt;/code&gt;代码块中使用连接对象的&lt;code&gt;close&lt;/code&gt;方法来关闭连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;代码实操&lt;/h4&gt;
&lt;p&gt;下面，我们通过代码实操的方式为大家演示上面说的五个步骤。&lt;/p&gt;
&lt;h5&gt;插入数据&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

no = int(input(&apos;部门编号: &apos;))
name = input(&apos;部门名称: &apos;)
location = input(&apos;部门所在地: &apos;)

# 1. 创建连接（Connection）
conn = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                       user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                       database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;)
try:
    # 2. 获取游标对象（Cursor）
    with conn.cursor() as cursor:
        # 3. 通过游标对象向数据库服务器发出SQL语句
        affected_rows = cursor.execute(
            &apos;insert into `tb_dept` values (%s, %s, %s)&apos;,
            (no, name, location)
        )
        if affected_rows == 1:
            print(&apos;新增部门成功!!!&apos;)
    # 4. 提交事务（transaction）
    conn.commit()
except pymysql.MySQLError as err:
    # 4. 回滚事务
    conn.rollback()
    print(type(err), err)
finally:
    # 5. 关闭连接释放资源
    conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;127.0.0.1&lt;/code&gt;称为回环地址，它代表的是本机。下面的&lt;code&gt;guest&lt;/code&gt;是我提前创建好的用户，该用户拥有对&lt;code&gt;hrs&lt;/code&gt;数据库的&lt;code&gt;insert&lt;/code&gt;、&lt;code&gt;delete&lt;/code&gt;、&lt;code&gt;update&lt;/code&gt;和&lt;code&gt;select&lt;/code&gt;权限。我们不建议大家在项目中直接使用&lt;code&gt;root&lt;/code&gt;超级管理员账号访问数据库，这样做实在是太危险了。我们可以使用下面的命令创建名为&lt;code&gt;guest&lt;/code&gt;的用户并为其授权。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create user &apos;guest&apos;@&apos;%&apos; identified by &apos;Guest.618&apos;;
grant insert, delete, update, select on `hrs`.* to &apos;guest&apos;@&apos;%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果要插入大量数据，建议使用游标对象的&lt;code&gt;executemany&lt;/code&gt;方法做批处理（一个&lt;code&gt;insert&lt;/code&gt;操作后面跟上多组数据），大家可以尝试向一张表插入10000条记录，然后看看不使用批处理一条条的插入和使用批处理有什么差别。游标对象的&lt;code&gt;executemany&lt;/code&gt;方法第一个参数仍然是 SQL 语句，第二个参数可以是包含多组数据的列表或元组。&lt;/p&gt;
&lt;h5&gt;删除数据&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

no = int(input(&apos;部门编号: &apos;))

# 1. 创建连接（Connection）
conn = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                       user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                       database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;,
                       autocommit=True)
try:
    # 2. 获取游标对象（Cursor）
    with conn.cursor() as cursor:
        # 3. 通过游标对象向数据库服务器发出SQL语句
        affected_rows = cursor.execute(
            &apos;delete from `tb_dept` where `dno`=%s&apos;,
            (no, )
        )
        if affected_rows == 1:
            print(&apos;删除部门成功!!!&apos;)
finally:
    # 5. 关闭连接释放资源
    conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果不希望每次 SQL 操作之后手动提交或回滚事务，可以&lt;code&gt;connect&lt;/code&gt;函数中加一个名为&lt;code&gt;autocommit&lt;/code&gt;的参数并将它的值设置为&lt;code&gt;True&lt;/code&gt;，表示每次执行 SQL 成功后自动提交。但是我们建议大家手动提交或回滚，这样可以根据实际业务需要来构造事务环境。如果不愿意捕获异常并进行处理，可以在&lt;code&gt;try&lt;/code&gt;代码块后直接跟&lt;code&gt;finally&lt;/code&gt;块，省略&lt;code&gt;except&lt;/code&gt;意味着发生异常时，代码会直接崩溃并将异常栈显示在终端中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;更新数据&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

no = int(input(&apos;部门编号: &apos;))
name = input(&apos;部门名称: &apos;)
location = input(&apos;部门所在地: &apos;)

# 1. 创建连接（Connection）
conn = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                       user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                       database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;)
try:
    # 2. 获取游标对象（Cursor）
    with conn.cursor() as cursor:
        # 3. 通过游标对象向数据库服务器发出SQL语句
        affected_rows = cursor.execute(
            &apos;update `tb_dept` set `dname`=%s, `dloc`=%s where `dno`=%s&apos;,
            (name, location, no)
        )
        if affected_rows == 1:
            print(&apos;更新部门信息成功!!!&apos;)
    # 4. 提交事务
    conn.commit()
except pymysql.MySQLError as err:
    # 4. 回滚事务
    conn.rollback()
    print(type(err), err)
finally:
    # 5. 关闭连接释放资源
    conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;查询数据&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;查询部门表的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

# 1. 创建连接（Connection）
conn = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                       user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                       database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;)
try:
    # 2. 获取游标对象（Cursor）
    with conn.cursor() as cursor:
        # 3. 通过游标对象向数据库服务器发出SQL语句
        cursor.execute(&apos;select `dno`, `dname`, `dloc` from `tb_dept`&apos;)
        # 4. 通过游标对象抓取数据
        row = cursor.fetchone()
        while row:
            print(row)
            row = cursor.fetchone()
except pymysql.MySQLError as err:
    print(type(err), err)
finally:
    # 5. 关闭连接释放资源
    conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中，我们通过构造一个&lt;code&gt;while&lt;/code&gt;循环实现了逐行抓取查询结果的操作。这种方式特别适合查询结果有非常多行的场景。因为如果使用&lt;code&gt;fetchall&lt;/code&gt;一次性将所有记录抓取到一个嵌套元组中，会造成非常大的内存开销，这在很多场景下并不是一个好主意。如果不愿意使用&lt;code&gt;while&lt;/code&gt;循环，还可以考虑使用&lt;code&gt;iter&lt;/code&gt;函数构造一个迭代器来逐行抓取数据，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;分页查询员工表的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import pymysql

page = int(input(&apos;页码: &apos;))
size = int(input(&apos;大小: &apos;))

# 1. 创建连接（Connection）
con = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                      user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                      database=&apos;hrs&apos;, charset=&apos;utf8&apos;)
try:
    # 2. 获取游标对象（Cursor）
    with con.cursor(pymysql.cursors.DictCursor) as cursor:
        # 3. 通过游标对象向数据库服务器发出SQL语句
        cursor.execute(
            &apos;select `eno`, `ename`, `job`, `sal` from `tb_emp` order by `sal` desc limit %s,%s&apos;,
            ((page - 1) * size, size)
        )
        # 4. 通过游标对象抓取数据
        for emp_dict in cursor.fetchall():
            print(emp_dict)
finally:
    # 5. 关闭连接释放资源
    con.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;案例讲解&lt;/h4&gt;
&lt;p&gt;下面我们为大家讲解一个将数据库表数据导出到 Excel 文件的例子，我们需要先安装&lt;code&gt;openpyxl&lt;/code&gt;三方库，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install openpyxl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们通过下面的代码实现了将数据库&lt;code&gt;hrs&lt;/code&gt;中所有员工的编号、姓名、职位、月薪、补贴和部门名称导出到一个 Excel 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import openpyxl
import pymysql

# 创建工作簿对象
workbook = openpyxl.Workbook()
# 获得默认的工作表
sheet = workbook.active
# 修改工作表的标题
sheet.title = &apos;员工基本信息&apos;
# 给工作表添加表头
sheet.append((&apos;工号&apos;, &apos;姓名&apos;, &apos;职位&apos;, &apos;月薪&apos;, &apos;补贴&apos;, &apos;部门&apos;))
# 创建连接（Connection）
conn = pymysql.connect(host=&apos;127.0.0.1&apos;, port=3306,
                       user=&apos;guest&apos;, password=&apos;Guest.618&apos;,
                       database=&apos;hrs&apos;, charset=&apos;utf8mb4&apos;)
try:
    # 获取游标对象（Cursor）
    with conn.cursor() as cursor:
        # 通过游标对象执行SQL语句
        cursor.execute(
            &apos;select `eno`, `ename`, `job`, `sal`, coalesce(`comm`, 0), `dname` &apos;
            &apos;from `tb_emp` natural join `tb_dept`&apos;
        )
        # 通过游标抓取数据
        row = cursor.fetchone()
        while row:
            # 将数据逐行写入工作表中
            sheet.append(row)
            row = cursor.fetchone()
    # 保存工作簿
    workbook.save(&apos;hrs.xlsx&apos;)
except pymysql.MySQLError as err:
    print(err)
finally:
    # 关闭连接释放资源
    conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可以参考上面的例子，试一试把 Excel 文件的数据导入到指定数据库的指定表中，看看是否可以成功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;45 Hive实战&lt;/h2&gt;
&lt;h4&gt;Hive概述&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://hive.apache.org/&quot;&gt;Hive&lt;/a&gt; 是 Facebook 开源的一款基于 Hadoop 的数据仓库工具，目前由 Apache 软件基金会维护，它是应用最为广泛的大数据解决方案。简单的说，Hive 能将 SQL 查询转变为 MapReduce 或 Spark 任务，对 SQL 提供了完美的支持，能够非常方便的实现大数据统计，让不会使用 Java、Scala 语言的人也可以玩转大数据平台和分布式文件系统，实现对数据的存储和处理。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;（原图缺失：QQ20220708-235815@2x.png，请到原仓库查看）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day36-45/aadc61a780.webp&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;Hive 的主要作用包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将结构化的数据文件映射为一张表。&lt;/li&gt;
&lt;li&gt;提供类 SQL 的查询语言 HiveQL来操作大规模数据。&lt;/li&gt;
&lt;li&gt;底层依赖 Hadoop 的 HDFS 存储和 MapReduce / Spark / Tez 引擎执行任务。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hive 的特点如下所示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类 SQL 语法：支持 SQL 查询，降低大数据学习门槛。&lt;/li&gt;
&lt;li&gt;可扩展性：能够比较容易的支持 PB 级别数据。&lt;/li&gt;
&lt;li&gt;灵活存储：支持多种存储格式（Text、ORC、Parquet、Avro 等）。&lt;/li&gt;
&lt;li&gt;可选择的计算引擎：底层执行引擎有 MapReduce、Tez、Spark。&lt;/li&gt;
&lt;li&gt;延迟较高：适合离线分析，不适合低延迟 OLTP 查询。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hive 和传统关系型数据库的对比：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;（原图缺失：QQ20220707-195545@2x.png，请到原仓库查看）&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;环境搭建&lt;/h4&gt;
&lt;h5&gt;准备工作&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：下面的操作以 Ubuntu 系统为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;同步时区&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo timedatectl set-timezone Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改下载源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mv /etc/apt/sources.list /etc/apt/sources.list.bak
sudo vi /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 24.04 /etc/apt/sources.list
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 18.04 /etc/apt/sources.list
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装工具软件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt upgrade
sudo apt install -y vim wget net-tools openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 SSH&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start ssh
sudo systemctl enable ssh
sudo systemctl status ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置防火墙&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw enable
sudo ufw allow ssh
sudo ufw status

sudo ufw disable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;准备 Hadoop&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://archive.apache.org/dist/hadoop/common/stable2/hadoop-2.10.2.tar.gz
sudo tar -zxvf hadoop-2.10.2.tar.gz -C /opt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;准备 Hive&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://archive.apache.org/dist/hive/stable-2/apache-hive-2.3.9-bin.tar.gz
sudo tar -zxvf apache-hive-2.3.9-bin.tar.gz -C /opt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Java 环境&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;检查&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搜索&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt search openjdk
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y openjdk-8-jdk
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;MySQL 环境&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install mysql-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable mysql
sudo systemctl start mysql
sudo systemctl status mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;Hadoop 配置&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：详见 PDF 文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;Hive 配置&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：详见 PDF 文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;启动环境&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动 HDFS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start-dfs.sh
start-yarn.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 Hive&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hive --service metastore &amp;amp;
hive --service hiveserver2 &amp;gt; hiveserver2.log 2&amp;gt; hiveerrors.log &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查端口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ss -ntl
netstat -ntlp

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;用途说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -ls &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出目录内容，类似 Linux &lt;code&gt;ls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -ls -R &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;递归列出目录下所有文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -du &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示文件或目录大小（非递归）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -du -s &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;汇总显示目录总大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -du -h &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以可读格式（KB/MB/GB）显示文件大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -COUNT &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;统计目录下的文件数、目录数、总字节数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -mkdir &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -mkdir -p &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;递归创建多级目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -rm &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -rm -r &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;递归删除目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -copyFromLocal &amp;lt;local&amp;gt; &amp;lt;hdfs&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从本地复制文件到 HDFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -moveFromLocal &amp;lt;local&amp;gt; &amp;lt;hdfs&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从本地移动文件到 HDFS（复制后删除本地文件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -copyToLocal &amp;lt;hdfs&amp;gt; &amp;lt;local&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从 HDFS 复制文件到本地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -moveToLocal &amp;lt;hdfs&amp;gt; &amp;lt;local&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从 HDFS 移动文件到本地（复制后删除 HDFS 文件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -put &amp;lt;local&amp;gt; &amp;lt;hdfs&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等价于 &lt;code&gt;-copyFromLocal&lt;/code&gt;，上传文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -get &amp;lt;hdfs&amp;gt; &amp;lt;local&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等价于 &lt;code&gt;-copyToLocal&lt;/code&gt;，下载文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -getmerge &amp;lt;hdfs_dir&amp;gt; &amp;lt;local_file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;合并 HDFS 目录下的文件，保存到本地单一文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -cat &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输出文件内容到标准输出（适合查看小文件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -tail &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示文件结尾的部分内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -text &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以文本方式查看压缩文件（如 Gzip、SequenceFile）内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -appENDToFile &amp;lt;local_file&amp;gt; &amp;lt;hdfs_file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将本地文件内容追加到 HDFS 文件末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -cp &amp;lt;src&amp;gt; &amp;lt;dst&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在 HDFS 内部复制文件或目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -mv &amp;lt;src&amp;gt; &amp;lt;dst&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在 HDFS 内部移动/重命名文件或目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -chown &amp;lt;user:group&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;修改文件或目录的所有者和用户组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -chgrp &amp;lt;group&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;修改文件或目录所属用户组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -chmod &amp;lt;mode&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;修改文件或目录权限，类似 Linux &lt;code&gt;chmod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -stat &amp;lt;format&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示文件或目录状态信息（如大小、修改时间）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -expunge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清空 HDFS 垃圾回收站（TrASh）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop fs -checkSUM &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算文件的校验和&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;基本语法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;删除数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP DATABASE IF EXISTS eshop CASCADE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE IF NOT EXISTS eshop;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换上下文。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE eshop;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建外部表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE EXTERNAL TABLE IF NOT EXISTS dim_user_info 
(
user_id           STRING,
user_name         STRING, 
sex               STRING,
age               INT,
city              STRING,
first_active_time STRING,
level             INT,
extra1            STRING,
extra2            MAP&amp;lt;STRING, STRING&amp;gt;
)
ROW FORMAT DELIMITED 
FIELDS TERMINATED BY &apos;\t&apos;
COLLECTION ITEMS TERMINATED BY &apos;,&apos;
MAP KEYS TERMINATED BY &apos;:&apos;
LINES TERMINATED BY &apos;\n&apos;
STORED AS TEXTFILE;

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加载数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA LOCAL INPATH &apos;/home/hadoop/data/user_info/user_info.txt&apos; OVERWRITE INTO TABLE dim_user_info;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;LOAD DATA INPATH &apos;/user/data/user_info.txt&apos; OVERWRITE INTO TABLE dim_user_info;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建分区表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS fact_user_trade 
(
user_name      STRING,
piece          INT,
price          DOUBLE,
pay_amount     DOUBLE,
goods_category STRING,
pay_time       BIGINT
)  
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY &apos;\t&apos;
LINES TERMINATED BY &apos;\n&apos;
STORED AS TEXTFILE;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置动态分区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 设置使用动态分区
SET hive.exec.dynamic.partition=true;
-- 设置动态分区为非严格模式（严格模式至少要指定一个动态分区列）
SET hive.exec.dynamic.partition.mode=nonstrict;
-- 一次SQL操作最多能创建多少个动态分区（跨节点）
SET hive.exec.max.dynamic.partitions=1000;
-- 每个节点上最多能创建多少个动态分区
SET hive.exec.max.dynamic.partitions.pernode=10000;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修复分区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MSCK REPAIR TABLE fact_user_trade;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据提取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 开启本地模式自动判断
SET hive.exec.mode.local.auto=true;
-- 设置MAPREDUCE文件数量
SET hive.exec.mode.local.auto.input.files.max=128;
-- 设置MAPREDUCE文件大小
SET hive.exec.mode.local.auto.input.bytes.max=134217728;
-- 设置Reduce任务个数为1
SET mapreduce.job.reduces=1;

-- 查询北京女用户的姓名取前10个
SELECT user_name 
  FROM dim_user_info
 WHERE city=&apos;beijing&apos;
       AND sex=&apos;female&apos;
 LIMIT 10;

-- 查询2019年3月24日购买了food类商品的用户名、购买数量和支付金额（不聚合）
SELECT user_name
     , piece
     , pay_amount
  FROM fact_user_trade
 WHERE dt=&apos;2019-03-24&apos;
       AND goods_category=&apos;food&apos;;

-- 统计用户 ELLA 在2018年的总支付金额和最近最远两次消费间隔天数
SELECT SUM(pay_amount) AS total
     , DATEDIFF(
           MAX(FROM_UNIXTIME(pay_time, &apos;yyyy-MM-dd&apos;)),
           MIN(FROM_UNIXTIME(pay_time, &apos;yyyy-MM-dd&apos;))
       ) AS gap_days
  FROM fact_user_trade
 WHERE YEAR(dt)=&apos;2018&apos;
       AND user_name=&apos;ELLA&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分组聚合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查询2019年1月到4月，每个品类有多少人购买，累计金额是多少
SELECT goods_category
     , COUNT(DISTINCT user_name) AS total_user
     , SUM(pay_amount) AS total_pay
  FROM fact_user_trade
 WHERE dt BETWEEN &apos;2019-01-01&apos; AND &apos;2019-04-30&apos;
 GROUP BY goods_category;

-- 查询2019年4月支付金额超过5万元的用户
SELECT user_name
     , SUM(pay_amount) AS total
  FROM fact_user_trade
 WHERE dt BETWEEN &apos;2019-04-01&apos; AND &apos;2019-04-30&apos;
 GROUP BY user_name
HAVING SUM(pay_amount) &amp;gt; 50000;

-- 查询2018年购买的商品品类在两个以上的用户数
SELECT COUNT(t.user_name)
  FROM (SELECT user_name
             , COUNT(DISTINCT goods_category) AS total
          FROM fact_user_trade
         WHERE YEAR(dt)=&apos;2018&apos;
         GROUP BY user_name
        HAVING COUNT(DISTINCT goods_category)&amp;gt;2) AS t;
        
-- 查询2019年4月支付金额最多的用户前5名
SELECT user_name
     , SUM(pay_amount) AS total
  FROM fact_user_trade
 WHERE dt BETWEEN &apos;2019-04-01&apos; AND &apos;2019-04-30&apos;
 GROUP BY user_name
 ORDER BY total DESC
 LIMIT 5;
        
-- 统计不同年龄段的用户数
SELECT CASE WHEN age &amp;lt; 20 THEN &apos;20岁以下&apos; 
            WHEN age &amp;lt; 30 THEN &apos;30岁以下&apos; 
            WHEN age &amp;lt; 40 THEN &apos;40岁以下&apos; 
            ELSE &apos;40岁以上&apos; 
        END AS age_seg, 
       COUNT(DISTINCT user_id) AS total 
  FROM dim_user_info 
 GROUP BY CASE WHEN age &amp;lt; 20 THEN &apos;20岁以下&apos; 
               WHEN age &amp;lt; 30 THEN &apos;30岁以下&apos; 
               WHEN age &amp;lt; 40 THEN &apos;40岁以下&apos; 
               ELSE &apos;40岁以上&apos; 
           END;

-- 统计激活时间在2018年，年龄段在20-30岁、30-40岁的用户婚姻状况
SELECT age_seg,
       IF(marriage_status = 1, &apos;已婚&apos;, &apos;未婚&apos;) AS marriage_status,
       COUNT(*) AS total
  FROM (SELECT CASE WHEN age &amp;lt; 20 THEN &apos;20岁以下&apos; 
                    WHEN age &amp;lt; 30 THEN &apos;20-30岁&apos; 
                    WHEN age &amp;lt; 40 THEN &apos;30-40岁&apos; 
                    ELSE &apos;40岁以上&apos; 
                END AS age_seg, 
               extra2[&apos;marriage_status&apos;] AS marriage_status
          FROM dim_user_info
         WHERE TO_DATE(first_active_time) BETWEEN &apos;2018-01-01&apos; AND &apos;2018-12-31&apos;
       ) AS t
 WHERE age_seg in (&apos;20-30岁&apos;, &apos;30-40岁&apos;)
 GROUP BY age_seg, IF(marriage_status = 1, &apos;已婚&apos;, &apos;未婚&apos;);

-- 统计每个用户购买过哪些品类的商品
SELECT user_name,
       COLLECT_SET(goods_category) AS categories
  FROM fact_user_trade
 GROUP BY user_name;
 
-- 将数组拼接成字符串
SELECT user_name
     , CONCAT_WS(&apos;, &apos;, COLLECT_SET(goods_category)) AS categories
  FROM fact_user_trade
 GROUP BY user_name;

-- 将数据聚合成映射类型(map)
SELECT user_name
     , STR_TO_MAP(CONCAT_WS(&apos;,&apos;, COLLECT_LIST(CONCAT(goods_category, &apos;:&apos;, cnt)))) AS category_cnt_map
  FROM (SELECT user_name
             , goods_category
             , COUNT(*) AS cnt
          FROM fact_user_trade
         GROUP BY user_name, goods_category) AS t
 GROUP BY user_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据抽样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 数据抽样
SELECT *
  FROM fact_user_trade
 WHERE RAND() &amp;lt; 0.1;

SELECT * 
  FROM fact_user_trade 
       TABLESAMPLE(BUCKET 1 OUT OF 10 ON user_name);

-- 二进制存储支持（如ORC）
-- SELECT *
--   FROM fact_user_trade
--        TABLESAMPLE(BYTE 100M);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;横向展开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建视图
CREATE OR REPLACE VIEW v_user_categories
AS
SELECT user_name
     , COLLECT_SET(goods_category) AS categories
  FROM fact_user_trade
 WHERE dt BETWEEN &apos;2019-04-01&apos; and &apos;2019-04-30&apos; 
 GROUP BY user_name;

CREATE OR REPLACE VIEW v_user_categories_map
AS
SELECT user_name
     , STR_TO_MAP(CONCAT_WS(&apos;,&apos;, COLLECT_LIST(CONCAT(goods_category, &apos;:&apos;, cnt)))) AS category_cnt_map
  FROM (SELECT user_name
             , goods_category
             , COUNT(*) AS cnt
          FROM fact_user_trade
         WHERE dt BETWEEN &apos;2019-04-01&apos; AND &apos;2019-04-30&apos; 
         GROUP BY user_name, goods_category) AS t
 GROUP BY user_name;

-- 横向展开数组(array)
 SELECT user_name
      , category
   FROM v_user_categories
LATERAL VIEW EXPLODE(categories) t AS category;

-- 横向展开映射(map)
 SELECT user_name
      , category
      , cnt
   FROM v_user_categories_map
LATERAL VIEW EXPLODE(category_cnt_map) t AS category, cnt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;建表操作&lt;/h4&gt;
&lt;h5&gt;数据类型&lt;/h5&gt;
&lt;p&gt;Hive 的数据类型大体分为三类：原始类型、复杂类型、嵌套类型。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据类型&lt;/th&gt;
&lt;th&gt;类型说明&lt;/th&gt;
&lt;th&gt;使用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TINYINT&lt;/td&gt;
&lt;td&gt;1 字节有符号整数，取值范围 -128~127。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMALLINT&lt;/td&gt;
&lt;td&gt;2 字节有符号整数，取值范围 -32768 ~ 32767。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT&lt;/td&gt;
&lt;td&gt;4 字节有符号整数，取值范围 -2147483648~2147483647。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIGINT&lt;/td&gt;
&lt;td&gt;8 字节有符号整数，取值范围 -9223372036854775808~9223372036854775807。&lt;/td&gt;
&lt;td&gt;计数、ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOOLEAN&lt;/td&gt;
&lt;td&gt;布尔值，TRUE 或者 FALSE。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLOAT&lt;/td&gt;
&lt;td&gt;单精度浮点数（4 字节）。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOUBLE&lt;/td&gt;
&lt;td&gt;双精度浮点数（8 字节）。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DECIMAL(P, S)&lt;/td&gt;
&lt;td&gt;高精度小数，P=总位数，S=小数位数。&lt;/td&gt;
&lt;td&gt;订单金额&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STRING&lt;/td&gt;
&lt;td&gt;长度可变字符串&lt;/td&gt;
&lt;td&gt;文本、JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHAR&lt;/td&gt;
&lt;td&gt;固定长度字符串&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR&lt;/td&gt;
&lt;td&gt;限定最大长度字符串&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BINARY&lt;/td&gt;
&lt;td&gt;二进制数据&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMESTAMP&lt;/td&gt;
&lt;td&gt;时间戳&lt;/td&gt;
&lt;td&gt;精确到毫秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;日期&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INTERVAL&lt;/td&gt;
&lt;td&gt;时间间隔类型&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STRUCT&lt;/td&gt;
&lt;td&gt;和C语言中的结构体类似，&lt;code&gt;STRUCT&amp;lt;first_name:STRING, lASt_name:STRING&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;玩家行为&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAP&lt;/td&gt;
&lt;td&gt;由键值对构成的元素的集合，&lt;code&gt;MAP&amp;lt;STRING, INT&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;商品属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARRAY&lt;/td&gt;
&lt;td&gt;具有相同类型的变量的容器，&lt;code&gt;ARRAY&amp;lt;STRING&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;装备列表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;复杂类型和嵌套类型的用法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE complex
(
  c1 ARRAY&amp;lt;INT&amp;gt;,
  c2 MAP&amp;lt;STRING, INT&amp;gt;,
  c3 STRUCT&amp;lt;a:STRING, b:INT, c:DOUBLE&amp;gt;,
  c4 STRUCT&amp;lt;name:STRING, addrs:ARRAY&amp;lt;STRUCT&amp;lt;city:STRING, zipcode:STRING&amp;gt;&amp;gt;&amp;gt;
);

SELECT c1[0]
     , c2[&apos;key&apos;]
     , c3.b
     , c4.addrs[0].city
  FROM complex;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hive 数据类型支持两种转换：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;隐式转换：TINYINT → INT → BIGINT → DOUBLE → STRING、FLOAT → DOUBLE。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT CAST(&apos;123&apos; AS INT);
SELECT CAST(3.14159 AS DECIMAL(5,2));
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;表类型&lt;/h5&gt;
&lt;p&gt;Hive 中的表类型如下所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表类型&lt;/th&gt;
&lt;th&gt;定义/创建方式&lt;/th&gt;
&lt;th&gt;存储位置&lt;/th&gt;
&lt;th&gt;生命周期&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;应用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内部表 (Managed Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TABLE t1 (id INT, name STRING); &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据存放在 Hive 仓库目录下 (&lt;code&gt;/user/hive/warehouse/...&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;表删除时数据也被删除&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive 全权管理元数据和数据&lt;/td&gt;
&lt;td&gt;临时性数据、实验性数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;外部表 (External Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE EXTERNAL TABLE t2 (id INT, name STRING) LOCATION &apos;/data/t2&apos;; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据存放在用户指定目录&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;表删除时数据保留&lt;/strong&gt;，只删除元数据&lt;/td&gt;
&lt;td&gt;元数据和数据分离，便于共享&lt;/td&gt;
&lt;td&gt;数据共享、避免误删、数据湖场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分区表 (Partitioned Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TABLE sales (id INT, amount INT) PARTITIONED BY (dt STRING, region STRING); &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每个分区对应一个子目录&lt;/td&gt;
&lt;td&gt;分区字段不存储在表数据文件里，而作为路径目录&lt;/td&gt;
&lt;td&gt;提高查询效率（按分区裁剪数据）&lt;/td&gt;
&lt;td&gt;时间序列、地域划分、日志数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分桶表 (Bucketed Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TABLE users (id INT, name STRING) CLUSTERED BY (id) INTO 8 BUCKETS; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每个分桶对应一个文件&lt;/td&gt;
&lt;td&gt;分桶数固定&lt;/td&gt;
&lt;td&gt;数据按哈希分布到桶文件，便于采样、Join 优化&lt;/td&gt;
&lt;td&gt;等值 Join、大表抽样、数据均衡分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;临时表 (Temporary Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TEMPORARY TABLE tmp (id INT); &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅存在于当前会话内存中&lt;/td&gt;
&lt;td&gt;会话结束后自动销毁&lt;/td&gt;
&lt;td&gt;不会落盘，不更新 Hive 元数据&lt;/td&gt;
&lt;td&gt;临时计算、会话级中间结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;视图 (View)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE VIEW v1 AS SELECT ...; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不存储数据，只存元数据定义&lt;/td&gt;
&lt;td&gt;与底层表绑定&lt;/td&gt;
&lt;td&gt;类似虚拟表&lt;/td&gt;
&lt;td&gt;数据安全、SQL 复用、封装复杂查询&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;修饰符&lt;/h5&gt;
&lt;p&gt;创建表的时候可以使用的修饰符如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关键字&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;写了时的效果&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;不写时的默认值&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EXTERNAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;指定外部表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE EXTERNAL TABLE ... LOCATION ...&lt;/code&gt; → 表和数据分离，删表不删数据&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;内部表 (Managed Table)&lt;/strong&gt;，删表时数据目录会一起删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PARTITIONED BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定义分区字段&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PARTITIONED BY (dt STRING, city STRING)&lt;/code&gt; → 数据写到子目录 &lt;code&gt;/table/dt=2025-08-26/city=Beijing/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非分区表&lt;/strong&gt;，所有数据放在一个目录下&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLUSTERED BY ... INTO N BUCKETS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;分桶&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLUSTERED BY (user_id) INTO 8 BUCKETS&lt;/code&gt; → 数据按哈希分布到 8 个文件桶&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不分桶&lt;/strong&gt;，数据就是普通文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;STORED AS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;文件存储格式&lt;/td&gt;
&lt;td&gt;常用：&lt;code&gt;TEXTFILE&lt;/code&gt;、&lt;code&gt;SEQUENCEFILE&lt;/code&gt;、&lt;code&gt;ORC&lt;/code&gt;、&lt;code&gt;PARQUET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TEXTFILE&lt;/strong&gt;（文本文件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ROW FORMAT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;指定行格式和 SerDe&lt;/td&gt;
&lt;td&gt;比如 &lt;code&gt;ROW FORMAT DELIMITED FIELDS TERMINATED BY &apos;,&apos;&lt;/code&gt; 或 &lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.JsonSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LazySimpleSerDe&lt;/strong&gt;（默认分隔符 &lt;code&gt;\001&lt;/code&gt;，就是不可见的 Ctrl+A）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LOCATION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;指定 HDFS 路径&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOCATION &apos;/user/hive/custom_path&apos;&lt;/code&gt; → 表数据存放到自定义路径&lt;/td&gt;
&lt;td&gt;默认放到 &lt;strong&gt;仓库目录&lt;/strong&gt;：&lt;code&gt;/user/hive/warehouse/&amp;lt;db&amp;gt;.db/&amp;lt;table&amp;gt;/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TBLPROPERTIES/DBPROPERTIES&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;存储一些元数据属性&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TBLPROPERTIES (&apos;creator&apos;=&apos;Hao&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;空&lt;/strong&gt;，除非手动添加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;COMMENT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;表/字段备注&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMENT &apos;用户交易事实表&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;空&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;ROW FORMAT&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;常见场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT DELIMITED FIELDS TERMINATED BY &apos;,&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按分隔符解析&lt;/td&gt;
&lt;td&gt;CSV/TSV/文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT DEFAULT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认格式，同下&lt;/td&gt;
&lt;td&gt;CSV/TSV/文本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.LazySimpleSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认 SerDe，同上&lt;/td&gt;
&lt;td&gt;文本表（\001 分隔）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.OpenCSVSerde&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更智能的 CSV 解析&lt;/td&gt;
&lt;td&gt;有引号/转义的 CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.RegexSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用正则匹配行&lt;/td&gt;
&lt;td&gt;日志类数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.JsonSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON 格式解析&lt;/td&gt;
&lt;td&gt;JSON 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.AvroSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Avro 格式&lt;/td&gt;
&lt;td&gt;JSON + 二进制压缩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.ParquetHiveSerDe&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parquet 格式&lt;/td&gt;
&lt;td&gt;Parquet 数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW FORMAT SERDE &apos;org.apache.hadoop.hive.serde2.OrcSerde&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ORC 格式&lt;/td&gt;
&lt;td&gt;ORC 数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：只有使用&lt;strong&gt;非默认格式（CSV/JSON/Regex）&lt;/strong&gt; 时才需要显式指定 SERDE。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;写入数据&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写入方式&lt;/th&gt;
&lt;th&gt;示例语法&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INSERT INTO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT INTO TABLE sales PARTITION (dt=&apos;2025-08-26&apos;) SELECT * FROM tmp_sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向表/分区 &lt;strong&gt;追加数据&lt;/strong&gt;，不会覆盖原有内容&lt;/td&gt;
&lt;td&gt;日常数据追加写入（日志、交易流水）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INSERT OVERWRITE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT OVERWRITE TABLE sales PARTITION (dt=&apos;2025-08-26&apos;) SELECT * FROM tmp_sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;覆盖目标表/分区的数据（先删后写）&lt;/td&gt;
&lt;td&gt;周期性全量刷新数据（如 T+1 日报、月报）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LOAD DATA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOAD DATA INPATH &apos;/user/hadoop/data.txt&apos; INTO TABLE sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;移动文件&lt;/strong&gt;到 Hive 表目录（不做解析），速度快&lt;/td&gt;
&lt;td&gt;已有 HDFS 文件直接导入 Hive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LOAD DATA LOCAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOAD DATA LOCAL INPATH &apos;/home/user/data.txt&apos; INTO TABLE sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从 &lt;strong&gt;本地文件系统&lt;/strong&gt;拷贝数据到 Hive 表&lt;/td&gt;
&lt;td&gt;本地临时数据快速导入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CREATE TABLE AS SELECT (CTAS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TABLE new_sales AS SELECT * FROM sales WHERE dt=&apos;2025-08-26&apos;; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新建表并写入查询结果，一步到位&lt;/td&gt;
&lt;td&gt;生成中间表/派生表，数据探索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INSERT + Directory (写 HDFS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT OVERWRITE DIRECTORY &apos;/tmp/export/&apos; ROW FORMAT DELIMITED FIELDS TERMINATED BY &apos;,&apos; SELECT * FROM sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把查询结果写到 &lt;strong&gt;HDFS 目录&lt;/strong&gt;，可指定分隔符/格式&lt;/td&gt;
&lt;td&gt;数据导出，和其他系统交互&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INSERT + LOCAL Directory (写本地)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT OVERWRITE LOCAL DIRECTORY &apos;/home/user/export/&apos; ROW FORMAT DELIMITED FIELDS TERMINATED BY &apos;,&apos; SELECT * FROM sales; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把结果写到 &lt;strong&gt;本地目录&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;小规模结果导出，本地分析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;外部表写入 (External Table)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE EXTERNAL TABLE ext_sales (...) LOCATION &apos;/data/sales&apos;; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;外部表只管理元数据，实际写入依赖 &lt;code&gt;LOAD DATA&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;共享数据目录，防止误删&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态分区写入&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET hive.exec.dynamic.partition=true;&lt;/code&gt; &lt;code&gt;SET hive.exec.dynamic.partition.mode=nonstrict;&lt;/code&gt; &lt;code&gt;INSERT OVERWRITE TABLE sales PARTITION (dt) SELECT id, amount, dt FROM tmp; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自动根据字段值生成分区目录&lt;/td&gt;
&lt;td&gt;大规模分区表装载数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;常用函数&lt;/h4&gt;
&lt;h5&gt;数学函数 (Math Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abs(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;绝对值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;abs(-10) = 10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round(x[, d])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;四舍五入，d 为保留小数位&lt;/td&gt;
&lt;td&gt;&lt;code&gt;round(3.14159, 2) = 3.14&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;floor(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向下取整&lt;/td&gt;
&lt;td&gt;&lt;code&gt;floor(3.9) = 3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ceil(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向上取整&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ceil(3.1) = 4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rand()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成 [0,1) 随机数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rand()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pow(x, y)&lt;/code&gt; / &lt;code&gt;power(x,y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;幂运算&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pow(2,3) = 8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqrt(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;开平方&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqrt(16) = 4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;e^x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exp(1) ≈ 2.718&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ln(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自然对数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ln(e) = 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log10(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以 10 为底对数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;log10(100) = 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;字符串函数 (String Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;length(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串长度&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(&apos;hive&apos;) = 4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upper(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;转大写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;upper(&apos;hive&apos;) = HIVE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lower(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;转小写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lower(&apos;HIVE&apos;) = hive&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;concat(str1,str2,…)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;拼接字符串&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concat(&apos;hive&apos;,&apos;sql&apos;) = hivesql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;concat_ws(sep,str1,str2,…)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用分隔符拼接&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concat_ws(&apos;-&apos;, &apos;a&apos;,&apos;b&apos;,&apos;c&apos;) = a-b-c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;substr(str, start, len)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;截取子串&lt;/td&gt;
&lt;td&gt;&lt;code&gt;substr(&apos;hive&apos;,2,2) = iv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instr(str, substr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回子串位置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;instr(&apos;hive&apos;,&apos;iv&apos;) = 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;split(str, regex)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串分割成数组&lt;/td&gt;
&lt;td&gt;&lt;code&gt;split(&apos;a,b,c&apos;,&apos;,&apos;) = [&apos;a&apos;,&apos;b&apos;,&apos;c&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regexp_extract(str, pattern, idx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;正则提取&lt;/td&gt;
&lt;td&gt;&lt;code&gt;regexp_extract(&apos;abc123&apos;,&apos;([0-9]+)&apos;,1) = 123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regexp_replace(str, pattern, rep)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;正则替换&lt;/td&gt;
&lt;td&gt;&lt;code&gt;regexp_replace(&apos;abc123&apos;,&apos;[0-9]&apos;,&apos;X&apos;) = abcXXX&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trim(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;去掉首尾空格&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trim(&apos;  hi  &apos;) = hi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lpad(str,n,pad)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;左填充到 n 长度&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lpad(&apos;hi&apos;,5,&apos;*&apos;) = ***hi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rpad(str,n,pad)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右填充到 n 长度&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rpad(&apos;hi&apos;,5,&apos;*&apos;) = hi***&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;日期时间函数 (Date &amp;amp; Time Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前日期&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2025-09-15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前时间戳&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2025-09-15 22:00:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unix_timestamp()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前时间戳（秒）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1694784000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from_unixtime(ts, fmt)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;时间戳转字符串&lt;/td&gt;
&lt;td&gt;&lt;code&gt;from_unixtime(1694784000,&apos;yyyy-MM-dd&apos;) = 2025-09-15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;to_date(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串转日期&lt;/td&gt;
&lt;td&gt;&lt;code&gt;to_date(&apos;2025-09-15 22:00:00&apos;) = 2025-09-15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;year(dt)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取年&lt;/td&gt;
&lt;td&gt;&lt;code&gt;year(&apos;2025-09-15&apos;) = 2025&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;month(dt)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取月&lt;/td&gt;
&lt;td&gt;&lt;code&gt;month(&apos;2025-09-15&apos;) = 9&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;day(dt)&lt;/code&gt; / &lt;code&gt;dayofmonth(dt)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取日&lt;/td&gt;
&lt;td&gt;&lt;code&gt;day(&apos;2025-09-15&apos;) = 15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hour(ts)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取小时&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hour(&apos;2025-09-15 22:30:00&apos;) = 22&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minute(ts)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取分钟&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minute(&apos;2025-09-15 22:30:00&apos;) = 30&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;second(ts)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提取秒&lt;/td&gt;
&lt;td&gt;&lt;code&gt;second(&apos;2025-09-15 22:30:00&apos;) = 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;datediff(dt1,dt2)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日期相差天数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datediff(&apos;2025-09-15&apos;,&apos;2025-09-10&apos;) = 5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add_months(dt,n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日期加 n 月&lt;/td&gt;
&lt;td&gt;&lt;code&gt;add_months(&apos;2025-09-15&apos;,2) = 2025-11-15&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_add(dt,n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日期加 n 天&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date_add(&apos;2025-09-15&apos;,10) = 2025-09-25&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_sub(dt,n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;日期减 n 天&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date_sub(&apos;2025-09-15&apos;,10) = 2025-09-05&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;条件函数 (Conditional Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;if(cond, t, f)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;条件判断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if(1=1,&apos;yes&apos;,&apos;no&apos;) = yes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;case when ... then ... else ... end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多条件判断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;case when age&amp;gt;18 then &apos;adult&apos; else &apos;child&apos; end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;coalesce(x1,x2,...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回第一个非 NULL 值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coalesce(null,&apos;hive&apos;,&apos;sql&apos;) = hive&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvl(x, y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果 x 为 NULL，返回 y&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvl(null,0) = 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;聚合函数 (Aggregate Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count(*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;count(*)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求和&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum(price)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;avg(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;平均值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;avg(score)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最大值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max(age)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最小值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;min(age)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;collect_set(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;去重集合&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[&apos;a&apos;,&apos;b&apos;,&apos;c&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;collect_list(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;包含重复的集合&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[&apos;a&apos;,&apos;b&apos;,&apos;a&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;类型转换函数 (Type Conversion Functions)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cast(x AS type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;类型转换&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cast(&apos;123&apos; as int) = 123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typeof(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回字段类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;typeof(123) = int&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;复杂类型函数 (Complex Types)&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size(array/map)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取大小&lt;/td&gt;
&lt;td&gt;&lt;code&gt;size(array(&apos;a&apos;,&apos;b&apos;)) = 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map_keys(map)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回所有 key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map_keys(map(&apos;a&apos;,1,&apos;b&apos;,2)) = [&apos;a&apos;,&apos;b&apos;]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map_values(map)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回所有 value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map_values(map(&apos;a&apos;,1,&apos;b&apos;,2)) = [1,2]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort_array(array)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数组排序&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sort_array(array(3,1,2)) = [1,2,3]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;array_contains(array, val)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否包含元素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;array_contains(array(&apos;a&apos;,&apos;b&apos;),&apos;a&apos;) = true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;窗口函数 (Window Functions)&lt;/h5&gt;
&lt;p&gt;常用于 &lt;strong&gt;排名、累计计算、分组内计算&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;row_number()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分组内行号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;row_number() over(partition by dept order by salary desc)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rank()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排名（有并列，跳跃排名）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rank() over(order by score desc)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dense_rank()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;排名（有并列，不跳跃）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dense_rank() over(order by score desc)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lag(col,n,default)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;往前取第 n 行值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lag(salary,1,0) over(order by id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lead(col,n,default)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;往后取第 n 行值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lead(salary,1,0) over(order by id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;first_value(col)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分组内第一个值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;first_value(salary) over(order by id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;last_value(col)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分组内最后一个值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last_value(salary) over(order by id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum(col) over(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;窗口内累计和&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sum(sales) over(partition by region order by month)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;应用举例&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FROM_UNIXTIME&lt;/code&gt;：将时间戳转换成日期&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT FROM_UNIXTIME(pay_time, &apos;yyyy-MM-dd hh:mm:ss&apos;)
  FROM fact_user_trade LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DATEDIFF&lt;/code&gt;：计算两个日期的时间差&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 用户首次激活时间与设定参照时间的间隔
SELECT user_name, DATEDIFF(&apos;2019-4-1&apos;, to_date(firstactivetime))
  FROM dim_user_info LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;IF&lt;/code&gt;：根据条件返回不同的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 不同性别高级等用户数量
SELECT sex
     , IF(level &amp;gt; 5, &apos;高&apos;, &apos;低&apos;) AS level_type
     , COUNT(DISTINCT user_id) AS total
  FROM dim_user_info
 GROUP BY sex, IF(level &amp;gt; 5, &apos;高&apos;, &apos;低&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SUBSTR&lt;/code&gt;：字符串取子串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 统计每个月激活的新用户数
SELECT SUBSTR(first_active_time, 1, 7) AS month
     , COUNT(DISTINCT user_id) AS total
  FROM dim_user_info
 GROUP BY substr(first_active_time, 1, 7);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GET_JSON_OBJECT&lt;/code&gt;：从 JSON 字符串中取出指定的&lt;code&gt;key&lt;/code&gt;对应的&lt;code&gt;value&lt;/code&gt;，如：&lt;code&gt;GET_JSON_OBJECT(info, &apos;$.first_name&apos;)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 统计不同手机品牌的用户数
SELECT GET_JSON_OBJECT(extra1, &apos;$.phonebrand&apos;) AS phone
     , COUNT(DISTINCT user_id) AS total
  FROM user_info
 GROUP BY GET_JSON_OBJECT(extra1, &apos;$.phonebrand&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：MySQL 对应的函数名字叫&lt;code&gt;JSON_EXTRACT&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;分组聚合&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;示例语法&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;th&gt;结果特点&lt;/th&gt;
&lt;th&gt;典型应用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GROUP BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT region, product, SUM(amount) FROM sales GROUP BY region, product; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按指定列分组&lt;/td&gt;
&lt;td&gt;只输出 &lt;strong&gt;一个维度组合&lt;/strong&gt; 的聚合&lt;/td&gt;
&lt;td&gt;单维度或固定组合汇总&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GROUPING SETS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT region, product, SUM(amount) FROM sales GROUP BY GROUPING SETS ((region, product), (region), (product), ()); &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一次性指定多个分组组合&lt;/td&gt;
&lt;td&gt;输出指定的 &lt;strong&gt;多个维度组合结果&lt;/strong&gt;，可包含总计&lt;/td&gt;
&lt;td&gt;报表统计时只关心部分组合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CUBE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT region, product, SUM(amount) FROM sales GROUP BY CUBE(region, product); &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自动生成所有维度的组合&lt;/td&gt;
&lt;td&gt;输出 &lt;strong&gt;所有维度组合&lt;/strong&gt;（2^n 种）&lt;/td&gt;
&lt;td&gt;OLAP 全量多维分析&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;抽样操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;抽样方式&lt;/th&gt;
&lt;th&gt;语法示例&lt;/th&gt;
&lt;th&gt;核心语义&lt;/th&gt;
&lt;th&gt;是否随机&lt;/th&gt;
&lt;th&gt;可重复性&lt;/th&gt;
&lt;th&gt;精度/偏差&lt;/th&gt;
&lt;th&gt;依赖前提&lt;/th&gt;
&lt;th&gt;性能与注意点&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BUCKET 抽样（物理桶）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t TABLESAMPLE(BUCKET 2 OUT OF 8);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;选取分桶表的&lt;strong&gt;第2个物理桶&lt;/strong&gt;（总8桶）&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;稳定（由分桶写入决定）&lt;/td&gt;
&lt;td&gt;与建表分桶分布一致&lt;/td&gt;
&lt;td&gt;表必须是 &lt;strong&gt;CLUSTERED BY ... INTO 8 BUCKETS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;只读命中部分文件，快；但只在&lt;strong&gt;分桶表&lt;/strong&gt;可用&lt;/td&gt;
&lt;td&gt;大表等比例取样/一致性校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BUCKET 抽样（逻辑桶）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t TABLESAMPLE(BUCKET 1 OUT OF 4 ON user_id);&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按 &lt;code&gt;hASh(user_id)&lt;/code&gt; 把数据映射到4个逻辑桶，取第1桶&lt;/td&gt;
&lt;td&gt;伪随机（哈希）&lt;/td&gt;
&lt;td&gt;稳定（同 key 结果一致）&lt;/td&gt;
&lt;td&gt;近似 1/4，受 key 倾斜影响&lt;/td&gt;
&lt;td&gt;无需分桶表&lt;/td&gt;
&lt;td&gt;只扫命中行；分布受 key 倾斜影响&lt;/td&gt;
&lt;td&gt;训练/评估集一致切分、AB 分流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;概率抽样（Bernoulli）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t WHERE RAND() &amp;lt; 0.1;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按 10% 概率独立抽取每行&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;可用 &lt;code&gt;RAND(42)&lt;/code&gt; 固定种子&lt;/td&gt;
&lt;td&gt;期望10%，小样本波动大&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;全表扫描；易受数据量影响&lt;/td&gt;
&lt;td&gt;快速下采样、探索性分析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;按组固定样本数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM (SELECT *, ROW_NUMBER() OVER(PARTITION BY gid ORDER BY RAND()) rn FROM t) x WHERE rn ≤ 100;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每组（&lt;code&gt;gid&lt;/code&gt;）随机取 &lt;strong&gt;固定 N 条&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;可用 &lt;code&gt;RAND(42)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;各组样本量严格相等&lt;/td&gt;
&lt;td&gt;需支持窗口函数&lt;/td&gt;
&lt;td&gt;两次扫描+排序，成本中等&lt;/td&gt;
&lt;td&gt;类均衡采样、按省份/品类取样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;每组比例抽样&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHERE RAND() &amp;lt; CASE WHEN gid=&apos;A&apos; THEN 0.2 ELSE 0.05 END&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按组设定不同抽样率&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;可固定种子&lt;/td&gt;
&lt;td&gt;期望比例，组内波动&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;全表扫描&lt;/td&gt;
&lt;td&gt;不同业务线/权重抽样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LIMIT 截取（非随机）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t LIMIT 1000;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取前 N 行（非随机）&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;稳定（受文件/分片顺序影响）&lt;/td&gt;
&lt;td&gt;有偏（非随机）&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;最快；仅用于调试预览&lt;/td&gt;
&lt;td&gt;开发调试、字段检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;每桶/每Reducer取 Top-N（非随机）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;... DISTRIBUTE BY k SORT BY k LIMIT 100;&lt;/code&gt;（或每组 &lt;code&gt;ROW_NUMBER&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;每个分片/组取前 N&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;稳定&lt;/td&gt;
&lt;td&gt;有偏（按排序规则）&lt;/td&gt;
&lt;td&gt;需分发/排序&lt;/td&gt;
&lt;td&gt;适合并行 TopN，不是随机抽样&lt;/td&gt;
&lt;td&gt;TopN per group、分片抽取&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;排序操作&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;排序方式&lt;/th&gt;
&lt;th&gt;语法示例&lt;/th&gt;
&lt;th&gt;核心语义&lt;/th&gt;
&lt;th&gt;Reducer 数&lt;/th&gt;
&lt;th&gt;是否全局有序&lt;/th&gt;
&lt;th&gt;Shuffle/排序行为&lt;/th&gt;
&lt;th&gt;性能与注意点&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORDER BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t ORDER BY ts DESC;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;全局排序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 个&lt;/strong&gt;（单 reducer）&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;所有数据进 1 个 reducer 做总排序&lt;/td&gt;
&lt;td&gt;最慢；单点瓶颈；大结果易 OOM；适合小结果集或配合 &lt;code&gt;LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;导出小结果、最终展示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORDER BY … LIMIT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t ORDER BY score DESC LIMIT 1000;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全局 Top-N&lt;/td&gt;
&lt;td&gt;1 个&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;仍单 reducer，但 &lt;code&gt;LIMIT&lt;/code&gt; 可提前剪枝&lt;/td&gt;
&lt;td&gt;可接受；Top-N 常用写法&lt;/td&gt;
&lt;td&gt;全局 Top-N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SORT BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t SORT BY ts DESC;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;每个 reducer 内部排序&lt;/strong&gt;（局部有序）&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;Map 输出按 key 分区到多 reducer；各自排序&lt;/td&gt;
&lt;td&gt;快于 ORDER BY；结果整体&lt;strong&gt;不全局有序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;大表分片排序、并行导出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DISTRIBUTE BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t DISTRIBUTE BY key;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制&lt;strong&gt;按 key 分发&lt;/strong&gt;到 reducer&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;相同 key 去同一 reducer；无排序&lt;/td&gt;
&lt;td&gt;常与 SORT BY 搭配使用&lt;/td&gt;
&lt;td&gt;为后续聚合/排序分区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DISTRIBUTE BY + SORT BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t DISTRIBUTE BY key SORT BY key, ts DESC;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;同 key 同分片 + 分片内排序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;td&gt;否（但&lt;strong&gt;每个 key 内有序&lt;/strong&gt;）&lt;/td&gt;
&lt;td&gt;同一 key 的记录在同一 reducer 内按规则有序&lt;/td&gt;
&lt;td&gt;常用；利于下游 merge/sort-bASed 处理&lt;/td&gt;
&lt;td&gt;按用户/商品时间序列排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLUSTER BY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT * FROM t CLUSTER BY key;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DISTRIBUTE BY key&lt;/code&gt; &lt;strong&gt;+ &lt;code&gt;SORT BY key&lt;/code&gt;（升序）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多个&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;语法糖，&lt;strong&gt;不能指定 ASC/DESC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;简洁，但排序方向不可控&lt;/td&gt;
&lt;td&gt;分桶写入/等值 Join 预排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分区内排序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;... WHERE dt=&apos;2025-08-26&apos; SORT BY ts;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只处理某个分区的数据&lt;/td&gt;
&lt;td&gt;依配置&lt;/td&gt;
&lt;td&gt;分区内局部有序&lt;/td&gt;
&lt;td&gt;同 SORT BY&lt;/td&gt;
&lt;td&gt;缩小数据量显著提速&lt;/td&gt;
&lt;td&gt;分区数据导出、窗口函数前置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;横向展开&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;LATERAL VIEW&lt;/code&gt;是 Hive 里一个非常重要的语法，主要用于和表生成函数（如：&lt;code&gt;EXPLODE&lt;/code&gt;）结合，把一行数据拆成多行。最常见的应用场景是处理 ARRAY、MAP、 JSON 等类型的字段，也可以实现从宽表到窄表的转换。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用法&lt;/th&gt;
&lt;th&gt;示例语法&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;th&gt;典型应用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW explode(array)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT order_id, item FROM orders LATERAL VIEW explode(items) t AS item; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将数组拆成多行，每个元素一行&lt;/td&gt;
&lt;td&gt;订单明细、事件埋点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW posexplode(array)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT order_id, pos, item FROM orders LATERAL VIEW explode(items) t AS pos, item; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数组拆成多行，同时输出元素下标&lt;/td&gt;
&lt;td&gt;保留顺序的点击流分析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW explode(map)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT user_id, k, v FROM user_tags LATERAL VIEW explode(tags) t AS k, v; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Map 拆成多行，得到 (key, value)&lt;/td&gt;
&lt;td&gt;用户标签、属性键值对&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW json_tuple(json, …)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT log_id, ip, device FROM event_log LATERAL VIEW json_tuple(log_json,&apos;ip&apos;,&apos;device&apos;) t AS ip, device; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从 JSON 中抽取字段并生成列&lt;/td&gt;
&lt;td&gt;日志解析、半结构化数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW inline(array)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT order_id, col1, col2 FROM orders LATERAL VIEW inline(item_structs) t AS col1,col2; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;struct 数组展开成多行多列&lt;/td&gt;
&lt;td&gt;一行存多维属性时拆分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LATERAL VIEW stack(n, …)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT user_id, subject, score FROM user_score LATERAL VIEW stack(3,&apos;math&apos;,math,&apos;eng&apos;,english,&apos;phy&apos;,physics) t AS subject, score; &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将多列转为多行 (列转行)&lt;/td&gt;
&lt;td&gt;宽表转长表、UNPIVOT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;HiveSQL vs. MySQL&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;特性&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hive SQL&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MySQL SQL&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;区别说明&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;表分区&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PARTITIONED BY&lt;/code&gt;（分区）：创建表时使用分区来按某一字段划分数据，能加速查询时的数据筛选。&lt;/td&gt;
&lt;td&gt;不支持分区。&lt;/td&gt;
&lt;td&gt;Hive可以通过分区将数据分开存储，适合大数据场景。MySQL没有类似的分区机制。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;表分桶&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLUSTERED BY&lt;/code&gt;（分桶）: 将表数据根据某一字段进行分桶，适合用于大表连接优化。&lt;/td&gt;
&lt;td&gt;不支持分桶。&lt;/td&gt;
&lt;td&gt;Hive支持通过分桶优化连接操作，MySQL不支持分桶功能。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;存储格式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STORED AS&lt;/code&gt;：支持ORC、Parquet等列式存储格式。&lt;/td&gt;
&lt;td&gt;使用行式存储，如InnoDB。&lt;/td&gt;
&lt;td&gt;Hive支持列式存储格式，优化查询和压缩，而MySQL主要使用行式存储。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查询引擎&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持MapReduce、Tez、Spark等多种执行引擎。&lt;/td&gt;
&lt;td&gt;仅支持InnoDB引擎。&lt;/td&gt;
&lt;td&gt;Hive的查询可以通过不同的执行引擎（如Tez或Spark）优化执行效率，而MySQL只能使用单一引擎。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;复杂查询支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持LATERAL VIEW、MAP、ARRAY等复杂数据结构处理。&lt;/td&gt;
&lt;td&gt;不支持LATERAL VIEW等复杂的数据类型和处理。&lt;/td&gt;
&lt;td&gt;Hive支持更复杂的嵌套查询和复杂数据类型，MySQL不支持类似功能。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JOIN类型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持MapJoin（Map-side Join）：小表加载到内存中进行Map阶段的连接，避免了Shuffle。&lt;/td&gt;
&lt;td&gt;支持标准的INNER JOIN、LEFT JOIN、RIGHT JOIN、OUTER JOIN等。&lt;/td&gt;
&lt;td&gt;Hive的MapJoin优化可以加速小表与大表的连接操作，而MySQL没有MapJoin这种优化策略。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;自定义函数（UDF）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持用户定义函数（UDF）和用户定义表函数（UDTF）。&lt;/td&gt;
&lt;td&gt;支持用户定义函数（UDF）和触发器。&lt;/td&gt;
&lt;td&gt;Hive更侧重于自定义函数的灵活性，MySQL则更注重与表操作相关的函数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查询优化&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持分区裁剪（Partition Pruning）、列裁剪（Column Pruning）、动态分区插入等优化。&lt;/td&gt;
&lt;td&gt;基于索引的优化，但不支持类似Hive的列裁剪和分区裁剪。&lt;/td&gt;
&lt;td&gt;Hive拥有更多的针对大数据的查询优化机制，而MySQL主要依赖于索引和查询缓存等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;索引&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;code&gt;CREATE INDEX&lt;/code&gt;：但通常不建议在Hive中使用索引，性能开销较大。&lt;/td&gt;
&lt;td&gt;支持&lt;code&gt;CREATE INDEX&lt;/code&gt;，索引是MySQL查询性能的关键之一。&lt;/td&gt;
&lt;td&gt;Hive中索引的使用相对较少且较为消耗资源，而MySQL通过索引大大提高了查询性能。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据类型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;code&gt;ARRAY&lt;/code&gt;、&lt;code&gt;MAP&lt;/code&gt;、&lt;code&gt;STRUCT&lt;/code&gt;等复杂数据类型。&lt;/td&gt;
&lt;td&gt;只支持基本数据类型，如&lt;code&gt;INT&lt;/code&gt;、&lt;code&gt;VARCHAR&lt;/code&gt;等。&lt;/td&gt;
&lt;td&gt;Hive支持更丰富的数据类型，特别是在处理结构化数据时，MySQL相对简单。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NULL值处理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持&lt;code&gt;IS NULL&lt;/code&gt;、&lt;code&gt;IS NOT NULL&lt;/code&gt;，但有时对NULL值的处理可能与MySQL有所不同，具体行为可能会受到执行引擎的影响。&lt;/td&gt;
&lt;td&gt;支持&lt;code&gt;IS NULL&lt;/code&gt;、&lt;code&gt;IS NOT NULL&lt;/code&gt;，NULL值处理比较一致。&lt;/td&gt;
&lt;td&gt;Hive和MySQL都支持NULL值判断，但在大数据量查询时，Hive的NULL处理可能表现不如MySQL精确。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;聚合函数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持常见的聚合函数（如&lt;code&gt;SUM()&lt;/code&gt;、&lt;code&gt;COUNT()&lt;/code&gt;、&lt;code&gt;AVG()&lt;/code&gt;等），并扩展支持&lt;code&gt;APPROX_COUNT_DISTINCT()&lt;/code&gt;等近似聚合函数。&lt;/td&gt;
&lt;td&gt;支持常见的聚合函数（如&lt;code&gt;SUM()&lt;/code&gt;、&lt;code&gt;COUNT()&lt;/code&gt;、&lt;code&gt;AVG()&lt;/code&gt;等）。&lt;/td&gt;
&lt;td&gt;Hive提供了&lt;code&gt;APPROX_COUNT_DISTINCT()&lt;/code&gt;等高效的近似聚合函数，更适合处理海量数据，而MySQL的聚合函数通常是精确计算的。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;支持子查询&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持子查询，但对于大数据集的子查询可能不如MySQL高效，通常需要通过优化（如&lt;code&gt;MAPJOIN&lt;/code&gt;）来加速。&lt;/td&gt;
&lt;td&gt;支持子查询，MySQL的子查询优化较好，尤其在小数据量查询时表现优异。&lt;/td&gt;
&lt;td&gt;Hive的子查询在大数据量下性能可能不如MySQL，MySQL对子查询的优化更成熟。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;表创建语法&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive创建表时需要指定存储格式、分区、文件格式等，示例如下：&lt;/td&gt;
&lt;td&gt;MySQL创建表时，主要关注字段类型、索引等，示例如下：&lt;/td&gt;
&lt;td&gt;Hive的表创建更复杂，涉及存储格式和分区等，适用于大数据场景，而MySQL的表创建更加简洁。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;物化视图&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive支持创建&lt;strong&gt;物化视图&lt;/strong&gt;（Materialized View），在某些情况下可以提升查询性能。&lt;/td&gt;
&lt;td&gt;MySQL不直接支持物化视图，通常通过临时表或应用层缓存实现类似功能。&lt;/td&gt;
&lt;td&gt;Hive提供物化视图，允许预先计算并存储查询结果，从而提高查询效率。MySQL没有原生的物化视图支持。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查询缓存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive不支持查询缓存，通常通过MapJoin、索引等方式进行性能优化。&lt;/td&gt;
&lt;td&gt;MySQL支持查询缓存，可以缓存常见查询的结果以提高查询效率。&lt;/td&gt;
&lt;td&gt;MySQL具有强大的查询缓存机制，而Hive通常依赖其他优化策略来提高性能。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;字符串函数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive支持字符串函数，如&lt;code&gt;CONCAT()&lt;/code&gt;, &lt;code&gt;LENGTH()&lt;/code&gt;, &lt;code&gt;SUBSTRING()&lt;/code&gt;等。&lt;/td&gt;
&lt;td&gt;MySQL也支持字符串函数，且函数的支持程度较全面。&lt;/td&gt;
&lt;td&gt;Hive和MySQL支持相似的字符串操作，但Hive的字符串处理通常更适合大数据分析应用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NULL默认值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hive中字段通常不支持为NULL指定默认值，插入数据时通常使用&lt;code&gt;INSERT INTO&lt;/code&gt;语句。&lt;/td&gt;
&lt;td&gt;MySQL允许在创建表时为字段指定NULL或默认值。&lt;/td&gt;
&lt;td&gt;Hive不直接支持字段默认值，而MySQL允许为字段设置默认值（包括NULL）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;性能优化&lt;/h4&gt;
&lt;p&gt;Hive SQL查询性能优化方法有很多种，从表结构设计、查询优化到执行引擎调整等多个方面都有不同的优化方式。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;优化方法&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分区&lt;/strong&gt;（Partitioning）&lt;/td&gt;
&lt;td&gt;将表按某个字段（如日期）进行分区。通过分区裁剪，只扫描符合条件的分区，减少扫描的数据量。&lt;/td&gt;
&lt;td&gt;当数据量大且查询常根据某个字段（如时间、地区等）过滤时使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分桶&lt;/strong&gt;（Bucketing）&lt;/td&gt;
&lt;td&gt;将数据根据某个字段的哈希值分成固定数量的桶。用于优化大表与小表的连接操作，特别是在进行聚合时。&lt;/td&gt;
&lt;td&gt;当需要高效的连接操作和聚合时，尤其是多次对某列进行操作时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;列裁剪&lt;/strong&gt;（Column Pruning）&lt;/td&gt;
&lt;td&gt;查询时仅选择需要的列，减少不必要的列扫描。&lt;/td&gt;
&lt;td&gt;当查询中只需要表的部分列时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分区裁剪&lt;/strong&gt;（Partition Pruning）&lt;/td&gt;
&lt;td&gt;查询时通过裁剪掉不符合条件的分区，只扫描满足条件的分区。&lt;/td&gt;
&lt;td&gt;当表有分区并且查询包含分区字段的过滤条件时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;压缩存储格式&lt;/strong&gt;（ORC、Parquet）&lt;/td&gt;
&lt;td&gt;使用列式存储格式（如ORC、Parquet），比行式存储格式（如TextFile）更高效，尤其是在读取大量数据时。&lt;/td&gt;
&lt;td&gt;当表的数据量大，特别是需要快速扫描时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MapJoin&lt;/strong&gt;（Map-side Join）&lt;/td&gt;
&lt;td&gt;小表与大表连接时，将小表加载到内存中在Map阶段完成连接，避免了Shuffle和Reduce阶段的开销。&lt;/td&gt;
&lt;td&gt;当小表足够小，能够加载到内存中时，进行小表和大表的连接。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;合适的Join类型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;使用合适的Join类型（如Sort Merge Join或Bucketed Join）可以优化连接操作的效率。&lt;/td&gt;
&lt;td&gt;当两张表都根据相同列进行分桶或排序时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;合理使用Hive执行引擎&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;选择合适的执行引擎（MapReduce、Tez、Spark）可以提高查询效率。例如，Tez或Spark通常比MapReduce更高效。&lt;/td&gt;
&lt;td&gt;根据数据量和查询类型选择不同的执行引擎。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;使用合适的文件格式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;选择合适的文件格式（如ORC、Parquet）可以提高存储和读取效率，特别是对于大规模数据操作。&lt;/td&gt;
&lt;td&gt;需要高效存储和读取时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;开启动态分区插入&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通过设置&lt;code&gt;hive.exec.dynamic.partition=true&lt;/code&gt;，可以实现动态分区插入，避免手动指定每个分区。&lt;/td&gt;
&lt;td&gt;当数据需要按分区动态插入时，避免手动指定分区。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;减少数据扫描&lt;/strong&gt;（推迟计算）&lt;/td&gt;
&lt;td&gt;使用&lt;code&gt;WHERE&lt;/code&gt;子句避免在执行查询时扫描不必要的数据，尽量在早期阶段过滤数据。&lt;/td&gt;
&lt;td&gt;当查询中涉及大量数据时，尽量在查询的早期阶段减少扫描的数据量。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;分批次执行&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;对查询结果使用&lt;code&gt;LIMIT&lt;/code&gt;进行分页查询，或者使用数据抽样（&lt;code&gt;TABLESAMPLE&lt;/code&gt;）来减少查询的数据量。&lt;/td&gt;
&lt;td&gt;当数据量过大，且对数据的完整性要求不高时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适当使用索引&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;创建索引加速查询，尤其是对常用查询条件的列创建索引。&lt;/td&gt;
&lt;td&gt;当某些列经常用作查询条件时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查询缓存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;启用Hive的查询缓存，缓存结果集，提高相同查询的效率。&lt;/td&gt;
&lt;td&gt;当相同的查询频繁执行时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;减少MapReduce任务数量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通过调整&lt;code&gt;mapreduce.map.memory.mb&lt;/code&gt;和&lt;code&gt;mapreduce.reduce.memory.mb&lt;/code&gt;等参数，减少MapReduce任务的数量，提升计算效率。&lt;/td&gt;
&lt;td&gt;当查询中涉及多个复杂的MapReduce任务时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;合理选择表的存储格式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;为表选择合适的存储格式，避免存储格式不匹配导致性能问题。比如，ORC和Parquet格式在大数据查询中效率较高。&lt;/td&gt;
&lt;td&gt;当查询的数据量很大，尤其是在读取和聚合时。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;物化视图&lt;/strong&gt;（Materialized View）&lt;/td&gt;
&lt;td&gt;创建物化视图，预先计算并存储查询结果，减少重复计算的开销。&lt;/td&gt;
&lt;td&gt;当某些查询结果被频繁使用时，使用物化视图来提高查询效率。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;并行执行&lt;/strong&gt;（Parallel Execution）&lt;/td&gt;
&lt;td&gt;使用并行执行功能，设置&lt;code&gt;hive.exec.parallel=true&lt;/code&gt;，可以并行执行多个查询任务，减少执行时间。&lt;/td&gt;
&lt;td&gt;当需要执行多个查询且能够并行时。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>Day31-35 教程全文（3 篇合并）</title><link>https://www.klucen.cn/posts/python/day31-35-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day31-35-full/</guid><pubDate>Sun, 14 Dec 2025 00:12:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 3 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：语言进阶、Web 前端与 Linux
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#31-python%E8%AF%AD%E8%A8%80%E8%BF%9B%E9%98%B6&quot;&gt;31 Python语言进阶&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#32-33-web%E5%89%8D%E7%AB%AF%E6%A6%82%E8%BF%B0&quot;&gt;32-33 Web前端概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#34-35-%E7%8E%A9%E8%BD%AClinux%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F&quot;&gt;34-35 玩转Linux操作系统&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;31 Python语言进阶&lt;/h2&gt;
&lt;h4&gt;重要知识点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;生成式（推导式）的用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prices = {
    &apos;AAPL&apos;: 191.88,
    &apos;GOOG&apos;: 1186.96,
    &apos;IBM&apos;: 149.24,
    &apos;ORCL&apos;: 48.44,
    &apos;ACN&apos;: 166.89,
    &apos;FB&apos;: 208.09,
    &apos;SYMC&apos;: 21.29
}
# 用股票价格大于100元的股票构造一个新的字典
prices2 = {key: value for key, value in prices.items() if value &amp;gt; 100}
print(prices2)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：生成式（推导式）可以用来生成列表、集合和字典。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套的列表的坑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
courses = [&apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;]
# 录入五个学生三门课程的成绩
# 错误 - 参考http://pythontutor.com/visualize.html#mode=edit
# scores = [[None] * len(courses)] * len(names)
scores = [[None] * len(courses) for _ in range(len(names))]
for row, name in enumerate(names):
    for col, course in enumerate(courses):
        scores[row][col] = float(input(f&apos;请输入{name}的{course}成绩: &apos;))
        print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://pythontutor.com/&quot;&gt;Python Tutor&lt;/a&gt; - VISUALIZE CODE AND GET LIVE HELP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;heapq&lt;/code&gt;模块（堆排序）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从列表中找出最大的或最小的N个元素
堆结构(大根堆/小根堆)
&quot;&quot;&quot;
import heapq

list1 = [34, 25, 12, 99, 87, 63, 58, 78, 88, 92]
list2 = [
    {&apos;name&apos;: &apos;IBM&apos;, &apos;shares&apos;: 100, &apos;price&apos;: 91.1},
    {&apos;name&apos;: &apos;AAPL&apos;, &apos;shares&apos;: 50, &apos;price&apos;: 543.22},
    {&apos;name&apos;: &apos;FB&apos;, &apos;shares&apos;: 200, &apos;price&apos;: 21.09},
    {&apos;name&apos;: &apos;HPQ&apos;, &apos;shares&apos;: 35, &apos;price&apos;: 31.75},
    {&apos;name&apos;: &apos;YHOO&apos;, &apos;shares&apos;: 45, &apos;price&apos;: 16.35},
    {&apos;name&apos;: &apos;ACME&apos;, &apos;shares&apos;: 75, &apos;price&apos;: 115.65}
]
print(heapq.nlargest(3, list1))
print(heapq.nsmallest(3, list1))
print(heapq.nlargest(2, list2, key=lambda x: x[&apos;price&apos;]))
print(heapq.nlargest(2, list2, key=lambda x: x[&apos;shares&apos;]))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;itertools&lt;/code&gt;模块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
迭代工具模块
&quot;&quot;&quot;
import itertools

# 产生ABCD的全排列
itertools.permutations(&apos;ABCD&apos;)
# 产生ABCDE的五选三组合
itertools.combinations(&apos;ABCDE&apos;, 3)
# 产生ABCD和123的笛卡尔积
itertools.product(&apos;ABCD&apos;, &apos;123&apos;)
# 产生ABC的无限循环序列
itertools.cycle((&apos;A&apos;, &apos;B&apos;, &apos;C&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;collections&lt;/code&gt;模块&lt;/p&gt;
&lt;p&gt;常用的工具类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;namedtuple&lt;/code&gt;：命令元组，它是一个类工厂，接受类型的名称和属性列表来创建一个类。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deque&lt;/code&gt;：双端队列，是列表的替代实现。Python中的列表底层是基于数组来实现的，而deque底层是双向链表，因此当你需要在头尾添加和删除元素时，deque会表现出更好的性能，渐近时间复杂度为$O(1)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Counter&lt;/code&gt;：&lt;code&gt;dict&lt;/code&gt;的子类，键是元素，值是元素的计数，它的&lt;code&gt;most_common()&lt;/code&gt;方法可以帮助我们获取出现频率最高的元素。&lt;code&gt;Counter&lt;/code&gt;和&lt;code&gt;dict&lt;/code&gt;的继承关系我认为是值得商榷的，按照CARP原则，&lt;code&gt;Counter&lt;/code&gt;跟&lt;code&gt;dict&lt;/code&gt;的关系应该设计为关联关系更为合理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderedDict&lt;/code&gt;：&lt;code&gt;dict&lt;/code&gt;的子类，它记录了键值对插入的顺序，看起来既有字典的行为，也有链表的行为。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultdict&lt;/code&gt;：类似于字典类型，但是可以通过默认的工厂函数来获得键对应的默认值，相比字典中的&lt;code&gt;setdefault()&lt;/code&gt;方法，这种做法更加高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
找出序列中出现次数最多的元素
&quot;&quot;&quot;
from collections import Counter

words = [
    &apos;look&apos;, &apos;into&apos;, &apos;my&apos;, &apos;eyes&apos;, &apos;look&apos;, &apos;into&apos;, &apos;my&apos;, &apos;eyes&apos;,
    &apos;the&apos;, &apos;eyes&apos;, &apos;the&apos;, &apos;eyes&apos;, &apos;the&apos;, &apos;eyes&apos;, &apos;not&apos;, &apos;around&apos;,
    &apos;the&apos;, &apos;eyes&apos;, &quot;don&apos;t&quot;, &apos;look&apos;, &apos;around&apos;, &apos;the&apos;, &apos;eyes&apos;,
    &apos;look&apos;, &apos;into&apos;, &apos;my&apos;, &apos;eyes&apos;, &quot;you&apos;re&quot;, &apos;under&apos;
]
counter = Counter(words)
print(counter.most_common(3))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;数据结构和算法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;算法：解决问题的方法和步骤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;评价算法的好坏：渐近时间复杂度和渐近空间复杂度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;渐近时间复杂度的大O标记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(c)&quot; /&amp;gt; - 常量时间复杂度 - 布隆过滤器 / 哈希存储&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(log_2n)&quot; /&amp;gt; - 对数时间复杂度 - 折半查找（二分查找）&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(n)&quot; /&amp;gt; - 线性时间复杂度 - 顺序查找 / 计数排序&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(n*log_2n)&quot; /&amp;gt; - 对数线性时间复杂度 - 高级排序算法（归并排序、快速排序）&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(n^2)&quot; /&amp;gt; - 平方时间复杂度 - 简单排序算法（选择排序、插入排序、冒泡排序）&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(n^3)&quot; /&amp;gt; - 立方时间复杂度 - Floyd算法 / 矩阵乘法运算&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(2^n)&quot; /&amp;gt; - 几何级数时间复杂度 - 汉诺塔&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;http://latex.codecogs.com/gif.latex?O(n!)&quot; /&amp;gt; - 阶乘时间复杂度 - 旅行经销商问题 - NPC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/e4723a679e.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/a06aedaa33.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;排序算法（选择、冒泡和归并）和查找算法（顺序和折半）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def select_sort(items, comp=lambda x, y: x &amp;lt; y):
    &quot;&quot;&quot;简单选择排序&quot;&quot;&quot;
    items = items[:]
    for i in range(len(items) - 1):
        min_index = i
        for j in range(i + 1, len(items)):
            if comp(items[j], items[min_index]):
                min_index = j
        items[i], items[min_index] = items[min_index], items[i]
    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def bubble_sort(items, comp=lambda x, y: x &amp;gt; y):
    &quot;&quot;&quot;冒泡排序&quot;&quot;&quot;
    items = items[:]
    for i in range(len(items) - 1):
        swapped = False
        for j in range(len(items) - 1 - i):
            if comp(items[j], items[j + 1]):
                items[j], items[j + 1] = items[j + 1], items[j]
                swapped = True
        if not swapped:
            break
    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def bubble_sort(items, comp=lambda x, y: x &amp;gt; y):
    &quot;&quot;&quot;搅拌排序(冒泡排序升级版)&quot;&quot;&quot;
    items = items[:]
    for i in range(len(items) - 1):
        swapped = False
        for j in range(len(items) - 1 - i):
            if comp(items[j], items[j + 1]):
                items[j], items[j + 1] = items[j + 1], items[j]
                swapped = True
        if swapped:
            swapped = False
            for j in range(len(items) - 2 - i, i, -1):
                if comp(items[j - 1], items[j]):
                    items[j], items[j - 1] = items[j - 1], items[j]
                    swapped = True
        if not swapped:
            break
    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def merge(items1, items2, comp=lambda x, y: x &amp;lt; y):
    &quot;&quot;&quot;合并(将两个有序的列表合并成一个有序的列表)&quot;&quot;&quot;
    items = []
    index1, index2 = 0, 0
    while index1 &amp;lt; len(items1) and index2 &amp;lt; len(items2):
        if comp(items1[index1], items2[index2]):
            items.append(items1[index1])
            index1 += 1
        else:
            items.append(items2[index2])
            index2 += 1
    items += items1[index1:]
    items += items2[index2:]
    return items


def merge_sort(items, comp=lambda x, y: x &amp;lt; y):
    return _merge_sort(list(items), comp)


def _merge_sort(items, comp):
    &quot;&quot;&quot;归并排序&quot;&quot;&quot;
    if len(items) &amp;lt; 2:
        return items
    mid = len(items) // 2
    left = _merge_sort(items[:mid], comp)
    right = _merge_sort(items[mid:], comp)
    return merge(left, right, comp)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def seq_search(items, key):
    &quot;&quot;&quot;顺序查找&quot;&quot;&quot;
    for index, item in enumerate(items):
        if item == key:
            return index
    return -1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;def bin_search(items, key):
    &quot;&quot;&quot;折半查找&quot;&quot;&quot;
    start, end = 0, len(items) - 1
    while start &amp;lt;= end:
        mid = (start + end) // 2
        if key &amp;gt; items[mid]:
            start = mid + 1
        elif key &amp;lt; items[mid]:
            end = mid - 1
        else:
            return mid
    return -1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;穷举法 - 又称为暴力破解法，对所有的可能性进行验证，直到找到正确答案。&lt;/li&gt;
&lt;li&gt;贪婪法 - 在对问题求解时，总是做出在当前看来&lt;/li&gt;
&lt;li&gt;最好的选择，不追求最优解，快速找到满意解。&lt;/li&gt;
&lt;li&gt;分治法 - 把一个复杂的问题分成两个或更多的相同或相似的子问题，再把子问题分成更小的子问题，直到可以直接求解的程度，最后将子问题的解进行合并得到原问题的解。&lt;/li&gt;
&lt;li&gt;回溯法 - 回溯法又称为试探法，按选优条件向前搜索，当搜索到某一步发现原先选择并不优或达不到目标时，就退回一步重新选择。&lt;/li&gt;
&lt;li&gt;动态规划 - 基本思想也是将待求解问题分解成若干个子问题，先求解并保存这些子问题的解，避免产生大量的重复运算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;穷举法例子：百钱百鸡和五人分鱼。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 公鸡5元一只 母鸡3元一只 小鸡1元三只
# 用100元买100只鸡 问公鸡/母鸡/小鸡各多少只
for x in range(20):
    for y in range(33):
        z = 100 - x - y
        if 5 * x + 3 * y + z // 3 == 100 and z % 3 == 0:
            print(x, y, z)

# A、B、C、D、E五人在某天夜里合伙捕鱼 最后疲惫不堪各自睡觉
# 第二天A第一个醒来 他将鱼分为5份 扔掉多余的1条 拿走自己的一份
# B第二个醒来 也将鱼分为5份 扔掉多余的1条 拿走自己的一份
# 然后C、D、E依次醒来也按同样的方式分鱼 问他们至少捕了多少条鱼
fish = 6
while True:
    total = fish
    enough = True
    for _ in range(5):
        if (total - 1) % 5 == 0:
            total = (total - 1) // 5 * 4
        else:
            enough = False
            break
    if enough:
        print(fish)
        break
    fish += 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;贪婪法例子：假设小偷有一个背包，最多能装20公斤赃物，他闯入一户人家，发现如下表所示的物品。很显然，他不能把所有物品都装进背包，所以必须确定拿走哪些物品，留下哪些物品。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;价格（美元）&lt;/th&gt;
&lt;th&gt;重量（kg）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;电脑&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;收音机&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;钟&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;花瓶&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;书&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;油画&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
贪婪法：在对问题求解时，总是做出在当前看来是最好的选择，不追求最优解，快速找到满意解。
输入：
20 6
电脑 200 20
收音机 20 4
钟 175 10
花瓶 50 2
书 10 1
油画 90 9
&quot;&quot;&quot;
class Thing(object):
    &quot;&quot;&quot;物品&quot;&quot;&quot;

    def __init__(self, name, price, weight):
        self.name = name
        self.price = price
        self.weight = weight

    @property
    def value(self):
        &quot;&quot;&quot;价格重量比&quot;&quot;&quot;
        return self.price / self.weight


def input_thing():
    &quot;&quot;&quot;输入物品信息&quot;&quot;&quot;
    name_str, price_str, weight_str = input().split()
    return name_str, int(price_str), int(weight_str)


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    max_weight, num_of_things = map(int, input().split())
    all_things = []
    for _ in range(num_of_things):
        all_things.append(Thing(*input_thing()))
    all_things.sort(key=lambda x: x.value, reverse=True)
    total_weight = 0
    total_price = 0
    for thing in all_things:
        if total_weight + thing.weight &amp;lt;= max_weight:
            print(f&apos;小偷拿走了{thing.name}&apos;)
            total_weight += thing.weight
            total_price += thing.price
    print(f&apos;总价值: {total_price}美元&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分治法例子：&lt;a href=&quot;https://zh.wikipedia.org/zh/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F&quot;&gt;快速排序&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
快速排序 - 选择枢轴对元素进行划分，左边都比枢轴小右边都比枢轴大
&quot;&quot;&quot;
def quick_sort(items, comp=lambda x, y: x &amp;lt;= y):
    items = list(items)[:]
    _quick_sort(items, 0, len(items) - 1, comp)
    return items


def _quick_sort(items, start, end, comp):
    if start &amp;lt; end:
        pos = _partition(items, start, end, comp)
        _quick_sort(items, start, pos - 1, comp)
        _quick_sort(items, pos + 1, end, comp)


def _partition(items, start, end, comp):
    pivot = items[end]
    i = start - 1
    for j in range(start, end):
        if comp(items[j], pivot):
            i += 1
            items[i], items[j] = items[j], items[i]
    items[i + 1], items[end] = items[end], items[i + 1]
    return i + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回溯法例子：&lt;a href=&quot;https://zh.wikipedia.org/zh/%E9%AA%91%E5%A3%AB%E5%B7%A1%E9%80%BB&quot;&gt;骑士巡逻&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
递归回溯法：叫称为试探法，按选优条件向前搜索，当搜索到某一步，发现原先选择并不优或达不到目标时，就退回一步重新选择，比较经典的问题包括骑士巡逻、八皇后和迷宫寻路等。
&quot;&quot;&quot;
import sys
import time

SIZE = 5
total = 0


def print_board(board):
    for row in board:
        for col in row:
            print(str(col).center(4), end=&apos;&apos;)
        print()


def patrol(board, row, col, step=1):
    if row &amp;gt;= 0 and row &amp;lt; SIZE and \
        col &amp;gt;= 0 and col &amp;lt; SIZE and \
        board[row][col] == 0:
        board[row][col] = step
        if step == SIZE * SIZE:
            global total
            total += 1
            print(f&apos;第{total}种走法: &apos;)
            print_board(board)
        patrol(board, row - 2, col - 1, step + 1)
        patrol(board, row - 1, col - 2, step + 1)
        patrol(board, row + 1, col - 2, step + 1)
        patrol(board, row + 2, col - 1, step + 1)
        patrol(board, row + 2, col + 1, step + 1)
        patrol(board, row + 1, col + 2, step + 1)
        patrol(board, row - 1, col + 2, step + 1)
        patrol(board, row - 2, col + 1, step + 1)
        board[row][col] = 0


def main():
    board = [[0] * SIZE for _ in range(SIZE)]
    patrol(board, SIZE - 1, SIZE - 1)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态规划例子：子列表元素之和的最大值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：子列表指的是列表中索引（下标）连续的元素构成的列表；列表中的元素是int类型，可能包含正整数、0、负整数；程序输入列表中的元素，输出子列表元素求和的最大值，例如：&lt;/p&gt;
&lt;p&gt;输入：1 -2 3 5 -3 2&lt;/p&gt;
&lt;p&gt;输出：8&lt;/p&gt;
&lt;p&gt;输入：0 -2 3 5 -1 2&lt;/p&gt;
&lt;p&gt;输出：9&lt;/p&gt;
&lt;p&gt;输入：-9 -2 -3 -5 -3&lt;/p&gt;
&lt;p&gt;输出：-2&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;def main():
    items = list(map(int, input().split()))
    overall = partial = items[0]
    for i in range(1, len(items)):
        partial = max(items[i], partial + items[i])
        overall = max(partial, overall)
    print(overall)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：这个题目最容易想到的解法是使用二重循环，但是代码的时间性能将会变得非常的糟糕。使用动态规划的思想，仅仅是多用了两个变量，就将原来$O(N^2)$复杂度的问题变成了$O(N)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;函数的使用方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将函数视为“一等公民”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数可以赋值给变量&lt;/li&gt;
&lt;li&gt;函数可以作为函数的参数&lt;/li&gt;
&lt;li&gt;函数可以作为函数的返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高阶函数的用法（&lt;code&gt;filter&lt;/code&gt;、&lt;code&gt;map&lt;/code&gt;以及它们的替代品）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items1 = list(map(lambda x: x ** 2, filter(lambda x: x % 2, range(1, 10))))
items2 = [x ** 2 for x in range(1, 10) if x % 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位置参数、可变参数、关键字参数、命名关键字参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数的元信息（代码可读性问题）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;匿名函数和内联函数的用法（&lt;code&gt;lambda&lt;/code&gt;函数）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;闭包和作用域问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Python搜索变量的LEGB顺序（Local &amp;gt;&amp;gt;&amp;gt; Embedded &amp;gt;&amp;gt;&amp;gt; Global &amp;gt;&amp;gt;&amp;gt; Built-in）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;global&lt;/code&gt;和&lt;code&gt;nonlocal&lt;/code&gt;关键字的作用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;global&lt;/code&gt;：声明或定义全局变量（要么直接使用现有的全局作用域的变量，要么定义一个变量放到全局作用域）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nonlocal&lt;/code&gt;：声明使用嵌套作用域的变量（嵌套作用域必须存在该变量，否则报错）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;装饰器函数（使用装饰器和取消装饰器）&lt;/p&gt;
&lt;p&gt;例子：输出函数执行时间的装饰器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def record_time(func):
    &quot;&quot;&quot;自定义装饰函数的装饰器&quot;&quot;&quot;
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time()
        result = func(*args, **kwargs)
        print(f&apos;{func.__name__}: {time() - start}秒&apos;)
        return result
        
    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果装饰器不希望跟&lt;code&gt;print&lt;/code&gt;函数耦合，可以编写可以参数化的装饰器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps
from time import time


def record(output):
    &quot;&quot;&quot;可以参数化的装饰器&quot;&quot;&quot;
	
	def decorate(func):
		
		@wraps(func)
		def wrapper(*args, **kwargs):
			start = time()
			result = func(*args, **kwargs)
			output(func.__name__, time() - start)
			return result
            
		return wrapper
	
	return decorate
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps
from time import time


class Record():
    &quot;&quot;&quot;通过定义类的方式定义装饰器&quot;&quot;&quot;

    def __init__(self, output):
        self.output = output

    def __call__(self, func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time()
            result = func(*args, **kwargs)
            self.output(func.__name__, time() - start)
            return result

        return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：由于对带装饰功能的函数添加了@wraps装饰器，可以通过&lt;code&gt;func.__wrapped__&lt;/code&gt;方式获得被装饰之前的函数或类来取消装饰器的作用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例子：用装饰器来实现单例模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps


def singleton(cls):
    &quot;&quot;&quot;装饰类的装饰器&quot;&quot;&quot;
    instances = {}

    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return wrapper


@singleton
class President:
    &quot;&quot;&quot;总统(单例类)&quot;&quot;&quot;
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面的代码中用到了闭包（closure），不知道你是否已经意识到了。还没有一个小问题就是，上面的代码并没有实现线程安全的单例，如果要实现线程安全的单例应该怎么做呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;线程安全的单例装饰器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps
from threading import RLock


def singleton(cls):
    &quot;&quot;&quot;线程安全的单例装饰器&quot;&quot;&quot;
    instances = {}
    locker = RLock()

    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instances:
            with locker:
                if cls not in instances:
                    instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面的代码用到了&lt;code&gt;with&lt;/code&gt;上下文语法来进行锁操作，因为锁对象本身就是上下文管理器对象（支持&lt;code&gt;__enter__&lt;/code&gt;和&lt;code&gt;__exit__&lt;/code&gt;魔术方法）。在&lt;code&gt;wrapper&lt;/code&gt;函数中，我们先做了一次不带锁的检查，然后再做带锁的检查，这样做比直接加锁检查性能要更好，如果对象已经创建就没有必须再去加锁而是直接返回该对象就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;面向对象相关知识&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;三大支柱：封装、继承、多态&lt;/p&gt;
&lt;p&gt;例子：工资结算系统。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
月薪结算系统 - 部门经理每月15000 程序员每小时200 销售员1800底薪加销售额5%提成
&quot;&quot;&quot;
from abc import ABCMeta, abstractmethod


class Employee(metaclass=ABCMeta):
    &quot;&quot;&quot;员工(抽象类)&quot;&quot;&quot;

    def __init__(self, name):
        self.name = name

    @abstractmethod
    def get_salary(self):
        &quot;&quot;&quot;结算月薪(抽象方法)&quot;&quot;&quot;
        pass


class Manager(Employee):
    &quot;&quot;&quot;部门经理&quot;&quot;&quot;

    def get_salary(self):
        return 15000.0


class Programmer(Employee):
    &quot;&quot;&quot;程序员&quot;&quot;&quot;

    def __init__(self, name, working_hour=0):
        self.working_hour = working_hour
        super().__init__(name)

    def get_salary(self):
        return 200.0 * self.working_hour


class Salesman(Employee):
    &quot;&quot;&quot;销售员&quot;&quot;&quot;

    def __init__(self, name, sales=0.0):
        self.sales = sales
        super().__init__(name)

    def get_salary(self):
        return 1800.0 + self.sales * 0.05


class EmployeeFactory:
    &quot;&quot;&quot;创建员工的工厂（工厂模式 - 通过工厂实现对象使用者和对象之间的解耦合）&quot;&quot;&quot;

    @staticmethod
    def create(emp_type, *args, **kwargs):
        &quot;&quot;&quot;创建员工&quot;&quot;&quot;
        all_emp_types = {&apos;M&apos;: Manager, &apos;P&apos;: Programmer, &apos;S&apos;: Salesman}
        cls = all_emp_types[emp_type.upper()]
        return cls(*args, **kwargs) if cls else None


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    emps = [
        EmployeeFactory.create(&apos;M&apos;, &apos;曹操&apos;), 
        EmployeeFactory.create(&apos;P&apos;, &apos;荀彧&apos;, 120),
        EmployeeFactory.create(&apos;P&apos;, &apos;郭嘉&apos;, 85), 
        EmployeeFactory.create(&apos;S&apos;, &apos;典韦&apos;, 123000),
    ]
    for emp in emps:
        print(f&apos;{emp.name}: {emp.get_salary():.2f}元&apos;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类与类之间的关系&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;is-a关系：继承&lt;/li&gt;
&lt;li&gt;has-a关系：关联 / 聚合 / 合成&lt;/li&gt;
&lt;li&gt;use-a关系：依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：扑克游戏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
经验：符号常量总是优于字面常量，枚举类型是定义符号常量的最佳选择
&quot;&quot;&quot;
from enum import Enum, unique

import random


@unique
class Suite(Enum):
    &quot;&quot;&quot;花色&quot;&quot;&quot;

    SPADE, HEART, CLUB, DIAMOND = range(4)

    def __lt__(self, other):
        return self.value &amp;lt; other.value


class Card:
    &quot;&quot;&quot;牌&quot;&quot;&quot;

    def __init__(self, suite, face):
        &quot;&quot;&quot;初始化方法&quot;&quot;&quot;
        self.suite = suite
        self.face = face

    def show(self):
        &quot;&quot;&quot;显示牌面&quot;&quot;&quot;
        suites = [&apos;♠︎&apos;, &apos;♥︎&apos;, &apos;♣︎&apos;, &apos;♦︎&apos;]
        faces = [&apos;&apos;, &apos;A&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;, &apos;10&apos;, &apos;J&apos;, &apos;Q&apos;, &apos;K&apos;]
        return f&apos;{suites[self.suite.value]}{faces[self.face]}&apos;

    def __repr__(self):
        return self.show()


class Poker:
    &quot;&quot;&quot;扑克&quot;&quot;&quot;

    def __init__(self):
        self.index = 0
        self.cards = [Card(suite, face)
                      for suite in Suite
                      for face in range(1, 14)]

    def shuffle(self):
        &quot;&quot;&quot;洗牌（随机乱序）&quot;&quot;&quot;
        random.shuffle(self.cards)
        self.index = 0

    def deal(self):
        &quot;&quot;&quot;发牌&quot;&quot;&quot;
        card = self.cards[self.index]
        self.index += 1
        return card

    @property
    def has_more(self):
        return self.index &amp;lt; len(self.cards)


class Player:
    &quot;&quot;&quot;玩家&quot;&quot;&quot;

    def __init__(self, name):
        self.name = name
        self.cards = []

    def get_one(self, card):
        &quot;&quot;&quot;摸一张牌&quot;&quot;&quot;
        self.cards.append(card)

    def sort(self, comp=lambda card: (card.suite, card.face)):
        &quot;&quot;&quot;整理手上的牌&quot;&quot;&quot;
        self.cards.sort(key=comp)


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    poker = Poker()
    poker.shuffle()
    players = [Player(&apos;东邪&apos;), Player(&apos;西毒&apos;), Player(&apos;南帝&apos;), Player(&apos;北丐&apos;)]
    while poker.has_more:
        for player in players:
                player.get_one(poker.deal())
    for player in players:
        player.sort()
        print(player.name, end=&apos;: &apos;)
        print(player.cards)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中使用了Emoji字符来表示扑克牌的四种花色，在某些不支持Emoji字符的系统上可能无法显示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象的复制（深复制/深拷贝/深度克隆和浅复制/浅拷贝/影子克隆）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;垃圾回收、循环引用和弱引用&lt;/p&gt;
&lt;p&gt;Python使用了自动化内存管理，这种管理机制以&lt;strong&gt;引用计数&lt;/strong&gt;为基础，同时也引入了&lt;strong&gt;标记-清除&lt;/strong&gt;和&lt;strong&gt;分代收集&lt;/strong&gt;两种机制为辅的策略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct _object {
    /* 引用计数 */
    int ob_refcnt;
    /* 对象指针 */
    struct _typeobject *ob_type;
} PyObject;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 增加引用计数的宏定义 */
#define Py_INCREF(op)   ((op)-&amp;gt;ob_refcnt++)
/* 减少引用计数的宏定义 */
#define Py_DECREF(op) \ //减少计数
    if (--(op)-&amp;gt;ob_refcnt != 0) \
        ; \
    else \
        __Py_Dealloc((PyObject *)(op))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导致引用计数+1的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象被创建，例如&lt;code&gt;a = 23&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对象被引用，例如&lt;code&gt;b = a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对象被作为参数，传入到一个函数中，例如&lt;code&gt;f(a)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对象作为一个元素，存储在容器中，例如&lt;code&gt;list1 = [a, a]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;导致引用计数-1的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的别名被显式销毁，例如&lt;code&gt;del a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对象的别名被赋予新的对象，例如&lt;code&gt;a = 24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个对象离开它的作用域，例如f函数执行完毕时，f函数中的局部变量（全局变量不会）&lt;/li&gt;
&lt;li&gt;对象所在的容器被销毁，或从容器中删除对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用计数可能会导致循环引用问题，而循环引用会导致内存泄露，如下面的代码所示。为了解决这个问题，Python中引入了“标记-清除”和“分代收集”。在创建一个对象的时候，对象被放在第一代中，如果在第一代的垃圾检查中对象存活了下来，该对象就会被放到第二代中，同理在第二代的垃圾检查中对象存活下来，该对象就会被放到第三代中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 循环引用会导致内存泄露 - Python除了引用技术还引入了标记清理和分代回收
# 在Python 3.6以前如果重写__del__魔术方法会导致循环引用处理失效
# 如果不想造成循环引用可以使用弱引用
list1 = []
list2 = [] 
list1.append(list2)
list2.append(list1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下情况会导致垃圾回收：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;gc.collect()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gc&lt;/code&gt;模块的计数器达到阀值&lt;/li&gt;
&lt;li&gt;程序退出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果循环引用中两个对象都定义了&lt;code&gt;__del__&lt;/code&gt;方法，&lt;code&gt;gc&lt;/code&gt;模块不会销毁这些不可达对象，因为gc模块不知道应该先调用哪个对象的&lt;code&gt;__del__&lt;/code&gt;方法，这个问题在Python 3.6中得到了解决。&lt;/p&gt;
&lt;p&gt;也可以通过&lt;code&gt;weakref&lt;/code&gt;模块构造弱引用的方式来解决循环引用的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;魔法属性和方法（请参考《Python魔法方法指南》）&lt;/p&gt;
&lt;p&gt;有几个小问题请大家思考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义的对象能不能使用运算符做运算？&lt;/li&gt;
&lt;li&gt;自定义的对象能不能放到&lt;code&gt;set&lt;/code&gt;中？能去重吗？&lt;/li&gt;
&lt;li&gt;自定义的对象能不能作为&lt;code&gt;dict&lt;/code&gt;的键？&lt;/li&gt;
&lt;li&gt;自定义的对象能不能使用上下文语法？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;混入（Mixin）&lt;/p&gt;
&lt;p&gt;例子：自定义字典限制只有在指定的key不存在时才能在字典中设置键值对。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SetOnceMappingMixin:
    &quot;&quot;&quot;自定义混入类&quot;&quot;&quot;
    __slots__ = ()

    def __setitem__(self, key, value):
        if key in self:
            raise KeyError(str(key) + &apos; already set&apos;)
        return super().__setitem__(key, value)


class SetOnceDict(SetOnceMappingMixin, dict):
    &quot;&quot;&quot;自定义字典&quot;&quot;&quot;
    pass


my_dict= SetOnceDict()
try:
    my_dict[&apos;username&apos;] = &apos;jackfrued&apos;
    my_dict[&apos;username&apos;] = &apos;hellokitty&apos;
except KeyError:
    pass
print(my_dict)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元编程和元类&lt;/p&gt;
&lt;p&gt;对象是通过类创建的，类是通过元类创建的，元类提供了创建类的元信息。所有的类都直接或间接的继承自&lt;code&gt;object&lt;/code&gt;，所有的元类都直接或间接的继承自&lt;code&gt;type&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例子：用元类实现单例模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading


class SingletonMeta(type):
    &quot;&quot;&quot;自定义元类&quot;&quot;&quot;

    def __init__(cls, *args, **kwargs):
        cls.__instance = None
        cls.__lock = threading.RLock()
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            with cls.__lock:
                if cls.__instance is None:
                    cls.__instance = super().__call__(*args, **kwargs)
        return cls.__instance


class President(metaclass=SingletonMeta):
    &quot;&quot;&quot;总统(单例类)&quot;&quot;&quot;
    
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;面向对象设计原则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单一职责原则 （&lt;strong&gt;S&lt;/strong&gt;RP）- 一个类只做该做的事情（类的设计要高内聚）&lt;/li&gt;
&lt;li&gt;开闭原则 （&lt;strong&gt;O&lt;/strong&gt;CP）- 软件实体应该对扩展开发对修改关闭&lt;/li&gt;
&lt;li&gt;依赖倒转原则（DIP）- 面向抽象编程（在弱类型语言中已经被弱化）&lt;/li&gt;
&lt;li&gt;里氏替换原则（&lt;strong&gt;L&lt;/strong&gt;SP） - 任何时候可以用子类对象替换掉父类对象&lt;/li&gt;
&lt;li&gt;接口隔离原则（&lt;strong&gt;I&lt;/strong&gt;SP）- 接口要小而专不要大而全（Python中没有接口的概念）&lt;/li&gt;
&lt;li&gt;合成聚合复用原则（CARP） - 优先使用强关联关系而不是继承关系复用代码&lt;/li&gt;
&lt;li&gt;最少知识原则（迪米特法则，Lo&lt;strong&gt;D&lt;/strong&gt;）- 不要给没有必然联系的对象发消息&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面加粗的字母放在一起称为面向对象的&lt;strong&gt;SOLID&lt;/strong&gt;原则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GoF设计模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建型模式：单例、工厂、建造者、原型&lt;/li&gt;
&lt;li&gt;结构型模式：适配器、门面（外观）、代理&lt;/li&gt;
&lt;li&gt;行为型模式：迭代器、观察者、状态、策略&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：可插拔的哈希算法（策略模式）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StreamHasher:
    &quot;&quot;&quot;哈希摘要生成器&quot;&quot;&quot;

    def __init__(self, alg=&apos;md5&apos;, size=4096):
        self.size = size
        alg = alg.lower()
        self.hasher = getattr(__import__(&apos;hashlib&apos;), alg.lower())()

    def __call__(self, stream):
        return self.to_digest(stream)

    def to_digest(self, stream):
        &quot;&quot;&quot;生成十六进制形式的摘要&quot;&quot;&quot;
        for buf in iter(lambda: stream.read(self.size), b&apos;&apos;):
            self.hasher.update(buf)
        return self.hasher.hexdigest()

def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    hasher1 = StreamHasher()
    with open(&apos;Python-3.7.6.tgz&apos;, &apos;rb&apos;) as stream:
        print(hasher1.to_digest(stream))
    hasher2 = StreamHasher(&apos;sha1&apos;)
    with open(&apos;Python-3.7.6.tgz&apos;, &apos;rb&apos;) as stream:
        print(hasher2(stream))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;迭代器和生成器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;迭代器是实现了迭代器协议的对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python中没有像&lt;code&gt;protocol&lt;/code&gt;或&lt;code&gt;interface&lt;/code&gt;这样的定义协议的关键字。&lt;/li&gt;
&lt;li&gt;Python中用魔术方法表示协议。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__iter__&lt;/code&gt;和&lt;code&gt;__next__&lt;/code&gt;魔术方法就是迭代器协议。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Fib(object):
    &quot;&quot;&quot;迭代器&quot;&quot;&quot;
    
    def __init__(self, num):
        self.num = num
        self.a, self.b = 0, 1
        self.idx = 0
   
    def __iter__(self):
        return self

    def __next__(self):
        if self.idx &amp;lt; self.num:
            self.a, self.b = self.b, self.a + self.b
            self.idx += 1
            return self.a
        raise StopIteration()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成器是语法简化版的迭代器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib(num):
    &quot;&quot;&quot;生成器&quot;&quot;&quot;
    a, b = 0, 1
    for _ in range(num):
        a, b = b, a + b
        yield a
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成器进化为协程。&lt;/p&gt;
&lt;p&gt;生成器对象可以使用&lt;code&gt;send()&lt;/code&gt;方法发送数据，发送的数据会成为生成器函数中通过&lt;code&gt;yield&lt;/code&gt;表达式获得的值。这样，生成器就可以作为协程使用，协程简单的说就是可以相互协作的子程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def calc_avg():
    &quot;&quot;&quot;流式计算平均值&quot;&quot;&quot;
    total, counter = 0, 0
    avg_value = None
    while True:
        value = yield avg_value
        total, counter = total + value, counter + 1
        avg_value = total / counter


gen = calc_avg()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;并发编程&lt;/h4&gt;
&lt;p&gt;Python中实现并发编程的三种方案：多线程、多进程和异步I/O。并发编程的好处在于可以提升程序的执行效率以及改善用户体验；坏处在于并发的程序不容易开发和调试，同时对其他程序来说它并不友好。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;多线程：Python中提供了&lt;code&gt;Thread&lt;/code&gt;类并辅以&lt;code&gt;Lock&lt;/code&gt;、&lt;code&gt;Condition&lt;/code&gt;、&lt;code&gt;Event&lt;/code&gt;、&lt;code&gt;Semaphore&lt;/code&gt;和&lt;code&gt;Barrier&lt;/code&gt;。Python中有GIL来防止多个线程同时执行本地字节码，这个锁对于CPython是必须的，因为CPython的内存管理并不是线程安全的，因为GIL的存在多线程并不能发挥CPU的多核特性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
面试题：进程和线程的区别和联系？
进程 - 操作系统分配内存的基本单位 - 一个进程可以包含一个或多个线程
线程 - 操作系统分配CPU的基本单位
并发编程（concurrent programming）
1. 提升执行性能 - 让程序中没有因果关系的部分可以并发的执行
2. 改善用户体验 - 让耗时间的操作不会造成程序的假死
&quot;&quot;&quot;
import glob
import os
import threading

from PIL import Image

PREFIX = &apos;thumbnails&apos;


def generate_thumbnail(infile, size, format=&apos;PNG&apos;):
    &quot;&quot;&quot;生成指定图片文件的缩略图&quot;&quot;&quot;
	file, ext = os.path.splitext(infile)
	file = file[file.rfind(&apos;/&apos;) + 1:]
	outfile = f&apos;{PREFIX}/{file}_{size[0]}_{size[1]}.{ext}&apos;
	img = Image.open(infile)
	img.thumbnail(size, Image.ANTIALIAS)
	img.save(outfile, format)


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
	if not os.path.exists(PREFIX):
		os.mkdir(PREFIX)
	for infile in glob.glob(&apos;images/*.png&apos;):
		for size in (32, 64, 128):
            # 创建并启动线程
			threading.Thread(
				target=generate_thumbnail, 
				args=(infile, (size, size))
			).start()
			

if __name__ == &apos;__main__&apos;:
	main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多个线程竞争资源的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
多线程程序如果没有竞争资源处理起来通常也比较简单
当多个线程竞争临界资源的时候如果缺乏必要的保护措施就会导致数据错乱
说明：临界资源就是被多个线程竞争的资源
&quot;&quot;&quot;
import time
import threading

from concurrent.futures import ThreadPoolExecutor


class Account(object):
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self):
        self.balance = 0.0
        self.lock = threading.Lock()

    def deposit(self, money):
        # 通过锁保护临界资源
        with self.lock:
            new_balance = self.balance + money
            time.sleep(0.001)
            self.balance = new_balance


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    account = Account()
    # 创建线程池
    pool = ThreadPoolExecutor(max_workers=10)
    futures = []
    for _ in range(100):
        future = pool.submit(account.deposit, 1)
        futures.append(future)
    # 关闭线程池
    pool.shutdown()
    for future in futures:
        future.result()
    print(account.balance)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改上面的程序，启动5个线程向账户中存钱，5个线程从账户中取钱，取钱时如果余额不足就暂停线程进行等待。为了达到上述目标，需要对存钱和取钱的线程进行调度，在余额不足时取钱的线程暂停并释放锁，而存钱的线程将钱存入后要通知取钱的线程，使其从暂停状态被唤醒。可以使用&lt;code&gt;threading&lt;/code&gt;模块的&lt;code&gt;Condition&lt;/code&gt;来实现线程调度，该对象也是基于锁来创建的，代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
多个线程竞争一个资源 - 保护临界资源 - 锁（Lock/RLock）
多个线程竞争多个资源（线程数&amp;gt;资源数） - 信号量（Semaphore）
多个线程的调度 - 暂停线程执行/唤醒等待中的线程 - Condition
&quot;&quot;&quot;
from concurrent.futures import ThreadPoolExecutor
from random import randint
from time import sleep

import threading


class Account:
    &quot;&quot;&quot;银行账户&quot;&quot;&quot;

    def __init__(self, balance=0):
        self.balance = balance
        lock = threading.RLock()
        self.condition = threading.Condition(lock)

    def withdraw(self, money):
        &quot;&quot;&quot;取钱&quot;&quot;&quot;
        with self.condition:
            while money &amp;gt; self.balance:
                self.condition.wait()
            new_balance = self.balance - money
            sleep(0.001)
            self.balance = new_balance

    def deposit(self, money):
        &quot;&quot;&quot;存钱&quot;&quot;&quot;
        with self.condition:
            new_balance = self.balance + money
            sleep(0.001)
            self.balance = new_balance
            self.condition.notify_all()


def add_money(account):
    while True:
        money = randint(5, 10)
        account.deposit(money)
        print(threading.current_thread().name, 
              &apos;:&apos;, money, &apos;====&amp;gt;&apos;, account.balance)
        sleep(0.5)


def sub_money(account):
    while True:
        money = randint(10, 30)
        account.withdraw(money)
        print(threading.current_thread().name, 
              &apos;:&apos;, money, &apos;&amp;lt;====&apos;, account.balance)
        sleep(1)


def main():
    account = Account()
    with ThreadPoolExecutor(max_workers=15) as pool:
        for _ in range(5):
            pool.submit(add_money, account)
        for _ in range(10):
            pool.submit(sub_money, account)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多进程：多进程可以有效的解决GIL的问题，实现多进程主要的类是&lt;code&gt;Process&lt;/code&gt;，其他辅助的类跟&lt;code&gt;threading&lt;/code&gt;模块中的类似，进程间共享数据可以使用管道、套接字等，在&lt;code&gt;multiprocessing&lt;/code&gt;模块中有一个&lt;code&gt;Queue&lt;/code&gt;类，它基于管道和锁机制提供了多个进程共享的队列。下面是官方文档上关于多进程和进程池的一个示例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
多进程和进程池的使用
多线程因为GIL的存在不能够发挥CPU的多核特性
对于计算密集型任务应该考虑使用多进程
time python3 example22.py
real    0m11.512s
user    0m39.319s
sys     0m0.169s
使用多进程后实际执行时间为11.512秒，而用户时间39.319秒约为实际执行时间的4倍
这就证明我们的程序通过多进程使用了CPU的多核特性，而且这台计算机配置了4核的CPU
&quot;&quot;&quot;
import concurrent.futures
import math

PRIMES = [
    1116281,
    1297337,
    104395303,
    472882027,
    533000389,
    817504243,
    982451653,
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419
] * 5


def is_prime(n):
    &quot;&quot;&quot;判断素数&quot;&quot;&quot;
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print(&apos;%d is prime: %s&apos; % (number, prime))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重点&lt;/strong&gt;：&lt;strong&gt;多线程和多进程的比较&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以下情况需要使用多线程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;程序需要维护许多共享的状态（尤其是可变状态），Python中的列表、字典、集合都是线程安全的，所以使用线程而不是进程维护共享状态的代价相对较小。&lt;/li&gt;
&lt;li&gt;程序会花费大量时间在I/O操作上，没有太多并行计算的需求且不需占用太多的内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下情况需要使用多进程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;程序执行计算密集型任务（如：字节码操作、数据处理、科学计算）。&lt;/li&gt;
&lt;li&gt;程序的输入可以并行的分成块，并且可以将运算结果合并。&lt;/li&gt;
&lt;li&gt;程序在内存使用方面没有任何限制且不强依赖于I/O操作（如：读写文件、套接字等）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异步处理：从调度程序的任务队列中挑选任务，该调度程序以交叉的形式执行这些任务，我们并不能保证任务将以某种顺序去执行，因为执行顺序取决于队列中的一项任务是否愿意将 CPU 处理时间让位给另一项任务。异步任务通常通过多任务协作处理的方式来实现，由于执行时间和顺序的不确定，因此需要通过回调式编程或者&lt;code&gt;future&lt;/code&gt;对象来获取任务执行的结果。Python 3 通过&lt;code&gt;asyncio&lt;/code&gt;模块和&lt;code&gt;await&lt;/code&gt;和&lt;code&gt;async&lt;/code&gt;关键字（在 Python 3.7 中正式被列为关键字）来支持异步处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
异步I/O - async / await
&quot;&quot;&quot;
import asyncio


def num_generator(m, n):
    &quot;&quot;&quot;指定范围的数字生成器&quot;&quot;&quot;
    yield from range(m, n + 1)


async def prime_filter(m, n):
    &quot;&quot;&quot;素数过滤器&quot;&quot;&quot;
    primes = []
    for i in num_generator(m, n):
        flag = True
        for j in range(2, int(i ** 0.5 + 1)):
            if i % j == 0:
                flag = False
                break
        if flag:
            print(&apos;Prime =&amp;gt;&apos;, i)
            primes.append(i)

        await asyncio.sleep(0.001)
    return tuple(primes)


async def square_mapper(m, n):
    &quot;&quot;&quot;平方映射器&quot;&quot;&quot;
    squares = []
    for i in num_generator(m, n):
        print(&apos;Square =&amp;gt;&apos;, i * i)
        squares.append(i * i)

        await asyncio.sleep(0.001)
    return squares


def main():
    &quot;&quot;&quot;主函数&quot;&quot;&quot;
    loop = asyncio.get_event_loop()
    future = asyncio.gather(prime_filter(2, 100), square_mapper(1, 100))
    future.add_done_callback(lambda x: print(x.result()))
    loop.run_until_complete(future)
    loop.close()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码使用&lt;code&gt;get_event_loop&lt;/code&gt;函数获得系统默认的事件循环，通过&lt;code&gt;gather&lt;/code&gt;函数可以获得一个&lt;code&gt;future&lt;/code&gt;对象，&lt;code&gt;future&lt;/code&gt;对象的&lt;code&gt;add_done_callback&lt;/code&gt;可以添加执行完成时的回调函数，&lt;code&gt;loop&lt;/code&gt;对象的&lt;code&gt;run_until_complete&lt;/code&gt;方法可以等待通过&lt;code&gt;future&lt;/code&gt;对象获得协程执行结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Python 中有一个名为&lt;code&gt;aiohttp&lt;/code&gt;的三方库，它提供了异步的 HTTP 客户端和服务器，这个三方库可以跟&lt;code&gt;asyncio&lt;/code&gt;模块一起工作，并提供了对&lt;code&gt;Future&lt;/code&gt;对象的支持。Python 3.6中引入了&lt;code&gt;async&lt;/code&gt;和&lt;code&gt;await&lt;/code&gt;来定义异步执行的函数以及创建异步上下文，在 Python 3.7 中它们正式成为了关键字。下面的代码异步的从5个URL中获取页面并通过正则表达式的命名捕获组提取了网站的标题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import re

import aiohttp

PATTERN = re.compile(r&apos;\&amp;lt;title\&amp;gt;(?P&amp;lt;title&amp;gt;.*)\&amp;lt;\/title\&amp;gt;&apos;)


async def fetch_page(session, url):
    async with session.get(url, ssl=False) as resp:
        return await resp.text()


async def show_title(url):
    async with aiohttp.ClientSession() as session:
        html = await fetch_page(session, url)
        print(PATTERN.search(html).group(&apos;title&apos;))


def main():
    urls = (&apos;https://www.python.org/&apos;,
            &apos;https://git-scm.com/&apos;,
            &apos;https://www.jd.com/&apos;,
            &apos;https://www.taobao.com/&apos;,
            &apos;https://www.douban.com/&apos;)
    loop = asyncio.get_event_loop()
    cos = [show_title(url) for url in urls]
    loop.run_until_complete(asyncio.wait(cos))
    loop.close()


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重点&lt;/strong&gt;：&lt;strong&gt;异步I/O与多进程的比较&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当程序不需要真正的并发性或并行性，而是更多的依赖于异步处理和回调时，&lt;code&gt;asyncio&lt;/code&gt;就是一种很好的选择。如果程序中有大量的等待与休眠时，也应该考虑&lt;code&gt;asyncio&lt;/code&gt;，它很适合编写没有实时数据处理需求的 Web 应用服务器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Python 还有很多用于处理并行任务的三方库，例如：&lt;code&gt;joblib&lt;/code&gt;、&lt;code&gt;PyMP&lt;/code&gt;等。实际开发中，要提升系统的可扩展性和并发性通常有垂直扩展（增加单个节点的处理能力）和水平扩展（将单个节点变成多个节点）两种做法。可以通过消息队列来实现应用程序的解耦合，消息队列相当于是多线程同步队列的扩展版本，不同机器上的应用程序相当于就是线程，而共享的分布式消息队列就是原来程序中的Queue。消息队列（面向消息的中间件）的最流行和最标准化的实现是 AMQP（高级消息队列协议），AMQP 源于金融行业，提供了排队、路由、可靠传输、安全等功能，最著名的实现包括：Apache 的 ActiveMQ、RabbitMQ 等。&lt;/p&gt;
&lt;p&gt;要实现任务的异步化，可以使用名为&lt;code&gt;Celery&lt;/code&gt;的三方库。&lt;code&gt;Celery&lt;/code&gt;是 Python 编写的分布式任务队列，它使用分布式消息进行工作，可以基于 RabbitMQ 或 Redis 来作为后端的消息代理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;32-33 Web前端概述&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：本文使用的部分插图来自 &lt;em&gt;Jon Duckett&lt;/em&gt; 的*&lt;a href=&quot;https://www.amazon.cn/dp/1118008189/ref=sr_1_5?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&amp;amp;keywords=html+%26+css&amp;amp;qid=1554609325&amp;amp;s=gateway&amp;amp;sr=8-5&quot;&gt;HTML and CSS: Design and Build Websites&lt;/a&gt;*一书，这是一本非常棒的前端入门书，有兴趣的读者可以在亚马逊或者其他网站上找到该书的购买链接。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HTML 是用来描述网页的一种语言，全称是 Hyper-Text Markup Language，即超文本标记语言。我们浏览网页时看到的文字、按钮、图片、视频等元素，它们都是通过 HTML 书写并通过浏览器来呈现的。&lt;/p&gt;
&lt;h4&gt;HTML简史&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;1991年10月：一个非正式CERN（&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%AD%90%E6%B4%B2%E6%A0%B8%E5%AD%90%E7%A0%94%E7%A9%B6%E7%B5%84%E7%B9%94&quot;&gt;欧洲核子研究中心&lt;/a&gt;）文件首次公开18个HTML标签，这个文件的作者是物理学家&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%92%82%E5%A7%86%C2%B7%E4%BC%AF%E7%BA%B3%E6%96%AF-%E6%9D%8E&quot;&gt;蒂姆·伯纳斯-李&lt;/a&gt;，因此他是&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91&quot;&gt;万维网&lt;/a&gt;的发明者，也是&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91%E8%81%94%E7%9B%9F&quot;&gt;万维网联盟&lt;/a&gt;的主席。&lt;/li&gt;
&lt;li&gt;1995年11月：HTML 2.0标准发布（RFC 1866）。&lt;/li&gt;
&lt;li&gt;1997年1月：HTML 3.2作为&lt;a href=&quot;https://zh.wikipedia.org/wiki/W3C&quot;&gt;W3C&lt;/a&gt;推荐标准发布。&lt;/li&gt;
&lt;li&gt;1997年12月：HTML 4.0作为W3C推荐标准发布。&lt;/li&gt;
&lt;li&gt;1999年12月：HTML4.01作为W3C推荐标准发布。&lt;/li&gt;
&lt;li&gt;2008年1月：HTML5由W3C作为工作草案发布。&lt;/li&gt;
&lt;li&gt;2011年5月：W3C将HTML5推进至“最终征求”（Last Call）阶段。&lt;/li&gt;
&lt;li&gt;2012年12月：W3C指定HTML5作为“候选推荐”阶段。&lt;/li&gt;
&lt;li&gt;2014年10月：HTML5作为稳定W3C推荐标准发布，这意味着HTML5的标准化已经完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;HTML5新特性&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;引入原生多媒体支持（audio和video标签）&lt;/li&gt;
&lt;li&gt;引入可编程内容（canvas标签）&lt;/li&gt;
&lt;li&gt;引入语义Web（article、aside、details、figure、footer、header、nav、section、summary等标签）&lt;/li&gt;
&lt;li&gt;引入新的表单控件（日历、邮箱、搜索、滑条等）&lt;/li&gt;
&lt;li&gt;引入对离线存储更好的支持（localStorage和sessionStorage）&lt;/li&gt;
&lt;li&gt;引入对定位、拖放、WebSocket、后台任务等的支持&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用标签承载内容&lt;/h4&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://gitee.com/jackfrued/mypic/raw/master/20211107163448.png&quot; style=&quot;zoom:35%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://gitee.com/jackfrued/mypic/raw/master/20211107163741.png&quot; style=&quot;zoom:75%&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;结构&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;html
&lt;ul&gt;
&lt;li&gt;head
&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;meta&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;body&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;文本&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;标题（heading）和段落（paragraph）
&lt;ul&gt;
&lt;li&gt;h1 ~ h6&lt;/li&gt;
&lt;li&gt;p&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;上标（superscript）和下标（subscript）
&lt;ul&gt;
&lt;li&gt;sup&lt;/li&gt;
&lt;li&gt;sub&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空白（白色空间折叠）&lt;/li&gt;
&lt;li&gt;折行（break）和水平标尺（horizontal ruler）
&lt;ul&gt;
&lt;li&gt;br&lt;/li&gt;
&lt;li&gt;hr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;语义化标签
&lt;ul&gt;
&lt;li&gt;加粗和强调 - strong&lt;/li&gt;
&lt;li&gt;引用 - blockquote&lt;/li&gt;
&lt;li&gt;缩写词和首字母缩写词 - abbr / acronym&lt;/li&gt;
&lt;li&gt;引文 - cite&lt;/li&gt;
&lt;li&gt;所有者联系信息 - address&lt;/li&gt;
&lt;li&gt;内容的修改 - ins / del&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;列表（list）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;有序列表（ordered list）- ol / li&lt;/li&gt;
&lt;li&gt;无序列表（unordered list）- ul / li&lt;/li&gt;
&lt;li&gt;定义列表（definition list）- dl / dt / dd&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;链接（anchor）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;页面链接&lt;/li&gt;
&lt;li&gt;锚链接&lt;/li&gt;
&lt;li&gt;功能链接&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;图像（image）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;图像存储位置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/cd987842ad.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;图像及其宽高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择正确的图像格式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JPEG&lt;/li&gt;
&lt;li&gt;GIF&lt;/li&gt;
&lt;li&gt;PNG&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;矢量图&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语义化标签 - figure / figcaption&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;表格（table）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;基本的表格结构 - table / tr / td / th&lt;/li&gt;
&lt;li&gt;表格的标题 - caption&lt;/li&gt;
&lt;li&gt;跨行和跨列 - rowspan属性 / colspan属性&lt;/li&gt;
&lt;li&gt;长表格 - thead / tbody / tfoot&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;表单（form）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;重要属性 - action / method / enctype&lt;/li&gt;
&lt;li&gt;表单控件（input）- type属性
&lt;ul&gt;
&lt;li&gt;文本框 - &lt;code&gt;text&lt;/code&gt; / 密码框 - &lt;code&gt;password&lt;/code&gt; / 数字框 - &lt;code&gt;number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;邮箱 - &lt;code&gt;email&lt;/code&gt; / 电话 - &lt;code&gt;tel&lt;/code&gt; / 日期 - &lt;code&gt;date&lt;/code&gt; / 滑条 - &lt;code&gt;range&lt;/code&gt; / URL - &lt;code&gt;url&lt;/code&gt; / 搜索 - &lt;code&gt;search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;单选按钮 - &lt;code&gt;radio&lt;/code&gt; / 复选按钮 - &lt;code&gt;checkbox&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件上传 - &lt;code&gt;file&lt;/code&gt; / 隐藏域 - &lt;code&gt;hidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;提交按钮 - &lt;code&gt;submit&lt;/code&gt; / 图像按钮 - &lt;code&gt;image&lt;/code&gt;  / 重置按钮 - &lt;code&gt;reset&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;下拉列表 - select / option&lt;/li&gt;
&lt;li&gt;文本域（多行文本）- textarea&lt;/li&gt;
&lt;li&gt;组合表单元素 - fieldset / legend&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;音视频（audio / video）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;视频格式和播放器&lt;/li&gt;
&lt;li&gt;视频托管服务&lt;/li&gt;
&lt;li&gt;添加视频的准备工作&lt;/li&gt;
&lt;li&gt;video标签和属性 - autoplay / controls / loop / muted / preload / src&lt;/li&gt;
&lt;li&gt;audio标签和属性 - autoplay / controls / loop / muted / preload / src / width / height / poster&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;窗口（frame）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;框架集（过时，不建议使用） - frameset / frame&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内嵌窗口 - iframe&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;其他&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文档类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.01//EN&quot; &quot;http://www.w3.org/TR/html4/strict.dtd&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 这是一段注释，注释不能够嵌套 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id：唯一标识&lt;/li&gt;
&lt;li&gt;class：元素所属的类，用于区分不同的元素&lt;/li&gt;
&lt;li&gt;title：元素的额外信息（鼠标悬浮时会显示工具提示文本）&lt;/li&gt;
&lt;li&gt;tabindex：Tab键切换顺序&lt;/li&gt;
&lt;li&gt;contenteditable：元素是否可编辑&lt;/li&gt;
&lt;li&gt;draggable：元素是否可拖拽&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;块级元素 / 行级元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符实体（实体替换符）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/623418cc52.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;使用CSS渲染页面&lt;/h4&gt;
&lt;h5&gt;简介&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CSS的作用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS的工作原理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规则、属性和值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/e5e10b0285.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用选择器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/7bfb259462.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;颜色（color）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;如何指定颜色&lt;/li&gt;
&lt;li&gt;颜色术语和颜色对比&lt;/li&gt;
&lt;li&gt;背景色&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;文本（text / font）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;文本的大小和字型(font-size / font-family)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/186300e778.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/5b2ca7dcb8.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;粗细、样式、拉伸和装饰(font-weight / font-style / font-stretch / text-decoration)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/ddb609df46.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;行间距(line-height)、字母间距(letter-spacing)和单词间距(word-spacing)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对齐(text-align)方式和缩进(text-ident)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链接样式（:link / :visited / :active / :hover）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS3新属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阴影效果 - text-shadow&lt;/li&gt;
&lt;li&gt;首字母和首行文本(:first-letter / :first-line)&lt;/li&gt;
&lt;li&gt;响应用户&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;盒子（box model）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;盒子大小的控制（width / height）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/186300e778.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;盒子的边框、外边距和内边距（border /  margin / padding）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/a5f28b8f5b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;盒子的显示和隐藏（display / visibility）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS3新属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;边框图像（border-image）&lt;/li&gt;
&lt;li&gt;投影（border-shadow）&lt;/li&gt;
&lt;li&gt;圆角（border-radius）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;列表、表格和表单&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;列表的项目符号（list-style）&lt;/li&gt;
&lt;li&gt;表格的边框和背景（border-collapse）&lt;/li&gt;
&lt;li&gt;表单控件的外观&lt;/li&gt;
&lt;li&gt;表单控件的对齐&lt;/li&gt;
&lt;li&gt;浏览器的开发者工具&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;图像&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;控制图像的大小（display: inline-block）&lt;/li&gt;
&lt;li&gt;对齐图像&lt;/li&gt;
&lt;li&gt;背景图像（background / background-image / background-repeat / background-position）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;布局&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;控制元素的位置（position / z-index）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通流&lt;/li&gt;
&lt;li&gt;相对定位&lt;/li&gt;
&lt;li&gt;绝对定位&lt;/li&gt;
&lt;li&gt;固定定位&lt;/li&gt;
&lt;li&gt;浮动元素（float / clear）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网站布局&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HTML5布局&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/9819e79819.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适配屏幕尺寸&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定宽度布局&lt;/li&gt;
&lt;li&gt;流体布局&lt;/li&gt;
&lt;li&gt;布局网格&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;使用JavaScript控制行为&lt;/h4&gt;
&lt;h5&gt;JavaScript基本语法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;语句和注释&lt;/li&gt;
&lt;li&gt;变量和数据类型
&lt;ul&gt;
&lt;li&gt;声明和赋值&lt;/li&gt;
&lt;li&gt;简单数据类型和复杂数据类型&lt;/li&gt;
&lt;li&gt;变量的命名规则&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表达式和运算符
&lt;ul&gt;
&lt;li&gt;赋值运算符&lt;/li&gt;
&lt;li&gt;算术运算符&lt;/li&gt;
&lt;li&gt;比较运算符&lt;/li&gt;
&lt;li&gt;逻辑运算符：&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code&gt;||&lt;/code&gt;、&lt;code&gt;!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分支结构
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if...else...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;switch...cas...default...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;循环结构
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for&lt;/code&gt;循环&lt;/li&gt;
&lt;li&gt;&lt;code&gt;while&lt;/code&gt;循环&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do...while&lt;/code&gt;循环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数组
&lt;ul&gt;
&lt;li&gt;创建数组&lt;/li&gt;
&lt;li&gt;操作数组中的元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数
&lt;ul&gt;
&lt;li&gt;声明函数&lt;/li&gt;
&lt;li&gt;调用函数&lt;/li&gt;
&lt;li&gt;参数和返回值&lt;/li&gt;
&lt;li&gt;匿名函数&lt;/li&gt;
&lt;li&gt;立即调用函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;面向对象&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;对象的概念&lt;/li&gt;
&lt;li&gt;创建对象的字面量语法&lt;/li&gt;
&lt;li&gt;访问成员运算符&lt;/li&gt;
&lt;li&gt;创建对象的构造函数语法
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt;关键字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;添加和删除属性
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt;关键字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;标准对象
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Number&lt;/code&gt; / &lt;code&gt;String&lt;/code&gt; / &lt;code&gt;Boolean&lt;/code&gt; / &lt;code&gt;Symbol&lt;/code&gt; / &lt;code&gt;Array&lt;/code&gt; / &lt;code&gt;Function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt; / &lt;code&gt;Error&lt;/code&gt; / &lt;code&gt;Math&lt;/code&gt; / &lt;code&gt;RegExp&lt;/code&gt; / &lt;code&gt;Object&lt;/code&gt; / &lt;code&gt;Map&lt;/code&gt; / &lt;code&gt;Set&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON&lt;/code&gt; / &lt;code&gt;Promise&lt;/code&gt; / &lt;code&gt;Generator&lt;/code&gt; / &lt;code&gt;Reflect&lt;/code&gt; / &lt;code&gt;Proxy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;BOM&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;window&lt;/code&gt;对象的属性和方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;history&lt;/code&gt;对象
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;forward()&lt;/code&gt; / &lt;code&gt;back()&lt;/code&gt; / &lt;code&gt;go()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;location&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;navigator&lt;/code&gt;对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;screen&lt;/code&gt;对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;DOM&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;DOM树&lt;/li&gt;
&lt;li&gt;访问元素
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getElementById()&lt;/code&gt; / &lt;code&gt;querySelector()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getElementsByClassName()&lt;/code&gt; / &lt;code&gt;getElementsByTagName()&lt;/code&gt; / &lt;code&gt;querySelectorAll()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parentNode&lt;/code&gt; / &lt;code&gt;previousSibling&lt;/code&gt; / &lt;code&gt;nextSibling&lt;/code&gt; / &lt;code&gt;children&lt;/code&gt; / &lt;code&gt;firstChild&lt;/code&gt; / &lt;code&gt;lastChild&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;操作元素
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nodeValue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innerHTML&lt;/code&gt; / &lt;code&gt;textContent&lt;/code&gt; / &lt;code&gt;createElement()&lt;/code&gt; / &lt;code&gt;createTextNode()&lt;/code&gt; / &lt;code&gt;appendChild()&lt;/code&gt; / &lt;code&gt;insertBefore()&lt;/code&gt; / &lt;code&gt;removeChild()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;className&lt;/code&gt; / &lt;code&gt;id&lt;/code&gt; / &lt;code&gt;hasAttribute()&lt;/code&gt; / &lt;code&gt;getAttribute()&lt;/code&gt; / &lt;code&gt;setAttribute()&lt;/code&gt; / &lt;code&gt;removeAttribute()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;事件处理
&lt;ul&gt;
&lt;li&gt;事件类型
&lt;ul&gt;
&lt;li&gt;UI事件：&lt;code&gt;load&lt;/code&gt; / &lt;code&gt;unload&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt; / &lt;code&gt;resize&lt;/code&gt; / &lt;code&gt;scroll&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;键盘事件：&lt;code&gt;keydown&lt;/code&gt; / &lt;code&gt;keyup&lt;/code&gt; / &lt;code&gt;keypress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;鼠标事件：&lt;code&gt;click&lt;/code&gt; / &lt;code&gt;dbclick&lt;/code&gt; / &lt;code&gt;mousedown&lt;/code&gt; / &lt;code&gt;mouseup&lt;/code&gt; / &lt;code&gt;mousemove&lt;/code&gt; / &lt;code&gt;mouseover&lt;/code&gt; / &lt;code&gt;mouseout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;焦点事件：&lt;code&gt;focus&lt;/code&gt; / &lt;code&gt;blur&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;表单事件：&lt;code&gt;input&lt;/code&gt; / &lt;code&gt;change&lt;/code&gt; / &lt;code&gt;submit&lt;/code&gt; / &lt;code&gt;reset&lt;/code&gt; / &lt;code&gt;cut&lt;/code&gt; / &lt;code&gt;copy&lt;/code&gt; / &lt;code&gt;paste&lt;/code&gt; / &lt;code&gt;select&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;事件绑定
&lt;ul&gt;
&lt;li&gt;HTML事件处理程序（不推荐使用，因为要做到标签与代码分离）&lt;/li&gt;
&lt;li&gt;传统的DOM事件处理程序（只能附加一个回调函数）&lt;/li&gt;
&lt;li&gt;事件监听器（旧的浏览器中不被支持）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;事件流：事件捕获 / 事件冒泡&lt;/li&gt;
&lt;li&gt;事件对象（低版本IE中的window.event）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;target&lt;/code&gt;（有些浏览器使用srcElement）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cancelable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preventDefault()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stopPropagation()&lt;/code&gt;（低版本IE中的cancelBubble）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;鼠标事件 - 事件发生的位置
&lt;ul&gt;
&lt;li&gt;屏幕位置：&lt;code&gt;screenX&lt;/code&gt;和&lt;code&gt;screenY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;页面位置：&lt;code&gt;pageX&lt;/code&gt;和&lt;code&gt;pageY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;客户端位置：&lt;code&gt;clientX&lt;/code&gt;和&lt;code&gt;clientY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;键盘事件 - 哪个键被按下了
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;keyCode&lt;/code&gt;属性（有些浏览器使用&lt;code&gt;which&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String.fromCharCode(event.keyCode)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTML5事件
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DOMContentLoaded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hashchange&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;beforeunload&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;JavaScript API&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;客户端存储 - &lt;code&gt;localStorage&lt;/code&gt;和&lt;code&gt;sessionStorage&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;localStorage.colorSetting = &apos;#a4509b&apos;;
localStorage[&apos;colorSetting&apos;] = &apos;#a4509b&apos;;
localStorage.setItem(&apos;colorSetting&apos;, &apos;#a4509b&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取位置信息 - &lt;code&gt;geolocation&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;navigator.geolocation.getCurrentPosition(function(pos) { 		  
    console.log(pos.coords.latitude)
    console.log(pos.coords.longitude)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从服务器获取数据 - Fetch API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绘制图形 - &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;的API&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;音视频 - &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;的API&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;前端框架&lt;/h4&gt;
&lt;h5&gt;渐进式框架 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day31-35/%3Chttps:/cn.vuejs.org/%3E&quot;&gt;Vue.js&lt;/a&gt;&lt;/h5&gt;
&lt;p&gt;前后端分离开发（前端渲染）必选框架。&lt;/p&gt;
&lt;h6&gt;快速上手&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引入Vue的JavaScript文件，我们仍然推荐从CDN服务器加载它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据绑定（声明式渲染 ）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h1&amp;gt;{{ product }}库存信息&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			product: &apos;iPhone X&apos;
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件与循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h1&amp;gt;库存信息&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;
	&amp;lt;ul&amp;gt;
		&amp;lt;li v-for=&quot;product in products&quot;&amp;gt;
			{{ product.name }} - {{ product.quantity }}
			&amp;lt;span v-if=&quot;product.quantity === 0&quot;&amp;gt;
				已经售罄
			&amp;lt;/span&amp;gt;
		&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			products: [
				{&quot;id&quot;: 1, &quot;name&quot;: &quot;iPhone X&quot;, &quot;quantity&quot;: 20},
				{&quot;id&quot;: 2, &quot;name&quot;: &quot;华为 Mate20&quot;, &quot;quantity&quot;: 0},
				{&quot;id&quot;: 3, &quot;name&quot;: &quot;小米 Mix3&quot;, &quot;quantity&quot;: 50}
			]
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h1&amp;gt;库存信息&amp;lt;/h1&amp;gt;
	&amp;lt;hr&amp;gt;
	&amp;lt;ul&amp;gt;
		&amp;lt;li v-for=&quot;product in products&quot;&amp;gt;
			{{ product.name }} - {{ product.quantity }}
			&amp;lt;span v-if=&quot;product.quantity === 0&quot;&amp;gt;
				已经售罄
			&amp;lt;/span&amp;gt;
		&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
	&amp;lt;h2&amp;gt;库存总量：{{ totalQuantity }}台&amp;lt;/h2&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			products: [
				{&quot;id&quot;: 1, &quot;name&quot;: &quot;iPhone X&quot;, &quot;quantity&quot;: 20},
				{&quot;id&quot;: 2, &quot;name&quot;: &quot;华为 Mate20&quot;, &quot;quantity&quot;: 0},
				{&quot;id&quot;: 3, &quot;name&quot;: &quot;小米 Mix3&quot;, &quot;quantity&quot;: 50}
			]
		},
		computed: {
			totalQuantity() {
				return this.products.reduce((sum, product) =&amp;gt; {
					return sum + product.quantity
				}, 0);
			}
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理事件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h1&amp;gt;库存信息&amp;lt;/h1&amp;gt;
	&amp;lt;hr&amp;gt;
	&amp;lt;ul&amp;gt;
		&amp;lt;li v-for=&quot;product in products&quot;&amp;gt;
			{{ product.name }} - {{ product.quantity }}
			&amp;lt;span v-if=&quot;product.quantity === 0&quot;&amp;gt;
				已经售罄
			&amp;lt;/span&amp;gt;
			&amp;lt;button @click=&quot;product.quantity += 1&quot;&amp;gt;
				增加库存
			&amp;lt;/button&amp;gt;
		&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
	&amp;lt;h2&amp;gt;库存总量：{{ totalQuantity }}台&amp;lt;/h2&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			products: [
				{&quot;id&quot;: 1, &quot;name&quot;: &quot;iPhone X&quot;, &quot;quantity&quot;: 20},
				{&quot;id&quot;: 2, &quot;name&quot;: &quot;华为 Mate20&quot;, &quot;quantity&quot;: 0},
				{&quot;id&quot;: 3, &quot;name&quot;: &quot;小米 Mix3&quot;, &quot;quantity&quot;: 50}
			]
		},
		computed: {
			totalQuantity() {
				return this.products.reduce((sum, product) =&amp;gt; {
					return sum + product.quantity
				}, 0);
			}
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户输入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h1&amp;gt;库存信息&amp;lt;/h1&amp;gt;
	&amp;lt;hr&amp;gt;
	&amp;lt;ul&amp;gt;
		&amp;lt;li v-for=&quot;product in products&quot;&amp;gt;
			{{ product.name }} - 
			&amp;lt;input type=&quot;number&quot; v-model.number=&quot;product.quantity&quot; min=&quot;0&quot;&amp;gt;
			&amp;lt;span v-if=&quot;product.quantity === 0&quot;&amp;gt;
				已经售罄
			&amp;lt;/span&amp;gt;
			&amp;lt;button @click=&quot;product.quantity += 1&quot;&amp;gt;
				增加库存
			&amp;lt;/button&amp;gt;
		&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
	&amp;lt;h2&amp;gt;库存总量：{{ totalQuantity }}台&amp;lt;/h2&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			products: [
				{&quot;id&quot;: 1, &quot;name&quot;: &quot;iPhone X&quot;, &quot;quantity&quot;: 20},
				{&quot;id&quot;: 2, &quot;name&quot;: &quot;华为 Mate20&quot;, &quot;quantity&quot;: 0},
				{&quot;id&quot;: 3, &quot;name&quot;: &quot;小米 Mix3&quot;, &quot;quantity&quot;: 50}
			]
		},
		computed: {
			totalQuantity() {
				return this.products.reduce((sum, product) =&amp;gt; {
					return sum + product.quantity
				}, 0);
			}
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过网络加载JSON数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
	&amp;lt;h2&amp;gt;库存信息&amp;lt;/h2&amp;gt;
	&amp;lt;ul&amp;gt;
		&amp;lt;li v-for=&quot;product in products&quot;&amp;gt;
			{{ product.name }} - {{ product.quantity }}
			&amp;lt;span v-if=&quot;product.quantity === 0&quot;&amp;gt;
				已经售罄
			&amp;lt;/span&amp;gt;
		&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	const app = new Vue({
		el: &apos;#app&apos;,
		data: {
			products: []
		}，
		created() {
			fetch(&apos;https://jackfrued.top/api/products&apos;)
				.then(response =&amp;gt; response.json())
				.then(json =&amp;gt; {
					this.products = json
				});
		}
	});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;使用脚手架 - vue-cli&lt;/h6&gt;
&lt;p&gt;Vue为商业项目开发提供了非常便捷的脚手架工具vue-cli，通过工具可以省去手工配置开发环境、测试环境和运行环境的步骤，让开发者只需要关注要解决的问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装脚手架。&lt;/li&gt;
&lt;li&gt;创建项目。&lt;/li&gt;
&lt;li&gt;安装依赖包。&lt;/li&gt;
&lt;li&gt;运行项目。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;UI框架 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day31-35/%3Chttp:/element-cn.eleme.io#/zh-CN%3E&quot;&gt;Element&lt;/a&gt;&lt;/h5&gt;
&lt;p&gt;基于Vue 2.0的桌面端组件库，用于构造用户界面，支持响应式布局。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引入Element的CSS和JavaScript文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 引入样式 --&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/element-ui/lib/theme-chalk/index.css&quot;&amp;gt;
&amp;lt;!-- 引入组件库 --&amp;gt;
&amp;lt;script src=&quot;https://unpkg.com/element-ui/lib/index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一个简单的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
		&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/element-ui/lib/theme-chalk/index.css&quot;&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;div id=&quot;app&quot;&amp;gt;
			&amp;lt;el-button @click=&quot;visible = true&quot;&amp;gt;点我&amp;lt;/el-button&amp;gt;
			&amp;lt;el-dialog :visible.sync=&quot;visible&quot; title=&quot;Hello world&quot;&amp;gt;
				&amp;lt;p&amp;gt;开始使用Element吧&amp;lt;/p&amp;gt;
			&amp;lt;/el-dialog&amp;gt;
            &amp;lt;/div&amp;gt;
	&amp;lt;/body&amp;gt;
	&amp;lt;script src=&quot;https://unpkg.com/vue/dist/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
	&amp;lt;script src=&quot;https://unpkg.com/element-ui/lib/index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
	&amp;lt;script&amp;gt;
		new Vue({
			el: &apos;#app&apos;,
			data: {
				visible: false,
			}
		})
	&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
		&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/element-ui/lib/theme-chalk/index.css&quot;&amp;gt;
	&amp;lt;/head&amp;gt;
	&amp;lt;body&amp;gt;
		&amp;lt;div id=&quot;app&quot;&amp;gt;
			&amp;lt;el-table :data=&quot;tableData&quot; stripe style=&quot;width: 100%&quot;&amp;gt;
				&amp;lt;el-table-column prop=&quot;date&quot; label=&quot;日期&quot; width=&quot;180&quot;&amp;gt;
				&amp;lt;/el-table-column&amp;gt;
				&amp;lt;el-table-column prop=&quot;name&quot; label=&quot;姓名&quot; width=&quot;180&quot;&amp;gt;
				&amp;lt;/el-table-column&amp;gt;
				&amp;lt;el-table-column prop=&quot;address&quot; label=&quot;地址&quot;&amp;gt;
				&amp;lt;/el-table-column&amp;gt;
			&amp;lt;/el-table&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/body&amp;gt;
	&amp;lt;script src=&quot;https://unpkg.com/vue/dist/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
	&amp;lt;script src=&quot;https://unpkg.com/element-ui/lib/index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
	&amp;lt;script&amp;gt;
		new Vue({
			el: &apos;#app&apos;,
			data: {
				tableData:  [
					{
						date: &apos;2016-05-02&apos;,
						name: &apos;王一霸&apos;,
						address: &apos;上海市普陀区金沙江路 1518 弄&apos;
					}, 
					{
						date: &apos;2016-05-04&apos;,
						name: &apos;刘二狗&apos;,
						address: &apos;上海市普陀区金沙江路 1517 弄&apos;
					}, 
					{
						date: &apos;2016-05-01&apos;,
						name: &apos;杨三萌&apos;,
						address: &apos;上海市普陀区金沙江路 1519 弄&apos;
					}, 
					{
						date: &apos;2016-05-03&apos;,
						name: &apos;陈四吹&apos;,
						address: &apos;上海市普陀区金沙江路 1516 弄&apos;
					}
				]
			}
		})
	&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;报表框架 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day31-35/%3Chttps:/echarts.baidu.com%3E&quot;&gt;ECharts&lt;/a&gt;&lt;/h5&gt;
&lt;p&gt;百度出品的开源可视化库，常用于生成各种类型的报表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/09d5260400.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;基于弹性盒子的CSS框架 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day31-35/%3Chttps:/bulma.io/%3E&quot;&gt;Bulma&lt;/a&gt;&lt;/h5&gt;
&lt;p&gt;Bulma是一个基于Flexbox的现代化的CSS框架，其初衷就是移动优先（Mobile First），模块化设计，可以轻松用来实现各种简单或者复杂的内容布局，即使不懂CSS的开发者也能够使用它定制出漂亮的页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
	&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
	&amp;lt;title&amp;gt;Bulma&amp;lt;/title&amp;gt;
	&amp;lt;link href=&quot;https://cdn.bootcss.com/bulma/0.7.4/css/bulma.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
	&amp;lt;style type=&quot;text/css&quot;&amp;gt;
		div { margin-top: 10px; }
		.column { color: #fff; background-color: #063; margin: 10px 10px; text-align: center; }
	&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
	&amp;lt;div class=&quot;columns&quot;&amp;gt;
		&amp;lt;div class=&quot;column&quot;&amp;gt;1&amp;lt;/div&amp;gt;
		&amp;lt;div class=&quot;column&quot;&amp;gt;2&amp;lt;/div&amp;gt;
		&amp;lt;div class=&quot;column&quot;&amp;gt;3&amp;lt;/div&amp;gt;
		&amp;lt;div class=&quot;column&quot;&amp;gt;4&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;div&amp;gt;
		&amp;lt;a class=&quot;button is-primary&quot;&amp;gt;Primary&amp;lt;/a&amp;gt;
		&amp;lt;a class=&quot;button is-link&quot;&amp;gt;Link&amp;lt;/a&amp;gt;
		&amp;lt;a class=&quot;button is-info&quot;&amp;gt;Info&amp;lt;/a&amp;gt;
		&amp;lt;a class=&quot;button is-success&quot;&amp;gt;Success&amp;lt;/a&amp;gt;
		&amp;lt;a class=&quot;button is-warning&quot;&amp;gt;Warning&amp;lt;/a&amp;gt;
		&amp;lt;a class=&quot;button is-danger&quot;&amp;gt;Danger&amp;lt;/a&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;div&amp;gt;
		&amp;lt;progress class=&quot;progress is-danger is-medium&quot; max=&quot;100&quot;&amp;gt;60%&amp;lt;/progress&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;div&amp;gt;
		&amp;lt;table class=&quot;table is-hoverable&quot;&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;th&amp;gt;One&amp;lt;/th&amp;gt;
				&amp;lt;th&amp;gt;Two&amp;lt;/th&amp;gt;
			&amp;lt;/tr&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;td&amp;gt;Three&amp;lt;/td&amp;gt;
				&amp;lt;td&amp;gt;Four&amp;lt;/td&amp;gt;
			&amp;lt;/tr&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;td&amp;gt;Five&amp;lt;/td&amp;gt;
				&amp;lt;td&amp;gt;Six&amp;lt;/td&amp;gt;
			&amp;lt;/tr&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;td&amp;gt;Seven&amp;lt;/td&amp;gt;
				&amp;lt;td&amp;gt;Eight&amp;lt;/td&amp;gt;
			&amp;lt;/tr&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;td&amp;gt;Nine&amp;lt;/td&amp;gt;
				&amp;lt;td&amp;gt;Ten&amp;lt;/td&amp;gt;
			&amp;lt;/tr&amp;gt;
			&amp;lt;tr&amp;gt;
				&amp;lt;td&amp;gt;Eleven&amp;lt;/td&amp;gt;
				&amp;lt;td&amp;gt;Twelve&amp;lt;/td&amp;gt;
			&amp;lt;/tr&amp;gt;
		&amp;lt;/table&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;响应式布局框架 - &lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day31-35/%3Chttp:/www.bootcss.com/%3E&quot;&gt;Bootstrap&lt;/a&gt;&lt;/h5&gt;
&lt;p&gt;用于快速开发Web应用程序的前端框架，支持响应式布局。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持主流的浏览器和移动设备&lt;/li&gt;
&lt;li&gt;容易上手&lt;/li&gt;
&lt;li&gt;响应式设计&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内容&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网格系统&lt;/li&gt;
&lt;li&gt;封装的CSS&lt;/li&gt;
&lt;li&gt;现成的组件&lt;/li&gt;
&lt;li&gt;JavaScript插件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可视化&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/d73ddc6e70.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;34-35 玩转Linux操作系统&lt;/h2&gt;
&lt;p&gt;玩转Linux操作系统&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：本文中对Linux命令的讲解都是基于名为CentOS的Linux发行版本，我自己使用的是阿里云服务器，系统版本为CentOS Linux release 7.6.1810。不同的Linux发行版本在Shell命令和工具程序上会有一些差别，但是这些差别是很小的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;操作系统发展史&lt;/h4&gt;
&lt;p&gt;只有硬件没有软件的计算机系统被称之为“裸机”，我们很难用“裸机”来完成计算机日常的工作（如存储和运算），所以必须用特定的软件来控制硬件的工作。最靠近计算机硬件的软件是系统软件，其中最为重要的就是“操作系统”。“操作系统”是控制和管理整个计算机硬件和软件资源、实现资源分配和任务调配、为系统用户以及其他软件提供接口和环境的程序的集合。&lt;/p&gt;
&lt;h5&gt;没有操作系统（手工操作）&lt;/h5&gt;
&lt;p&gt;在计算机诞生之初没有操作系统的年代，人们先把程序纸带（或卡片）装上计算机，然后启动输入机把程序送入计算机，接着通过控制台开关启动程序运行。当程序执行完毕，打印机输出计算的结果，用户卸下并取走纸带（或卡片）。第二个用户上机，重复同样的步骤。在整个过程中用户独占机器，CPU等待手工操作，资源利用率极低。&lt;/p&gt;
&lt;h5&gt;批处理系统&lt;/h5&gt;
&lt;p&gt;首先启动计算机上的一个监督程序，在监督程序的控制下，计算机能够自动的、成批的处理一个或多个用户的作业。完成一批作业后，监督程度又从输入机读取作业存入磁带机。按照上面的步骤重复处理任务。监督程序不停的处理各个作业，实现了作业的自动转接，减少了作业的建立时间和手工操作时间，提高了计算机资源的利用率。 批处理系统又可以分为单道批处理系统、多道批处理系统、联机批处理系统、脱机批处理系统。&lt;/p&gt;
&lt;h5&gt;分时系统和实时系统&lt;/h5&gt;
&lt;p&gt;分时系统是把处理器的运行时间分成很短的时间片，按时间片轮流把处理机分配给各联机作业使用。 若某个作业在分配给它的时间片内不能完成其计算，则该作业暂时中断，把处理机让给另一作业使用，等待下一轮调度时再继续其运行。由于计算机速度很快，作业运行轮转得很快，给每个用户的感觉是他独占了一台计算机。而每个用户可以通过自己的终端向系统发出各种操作控制命令，在充分的人机交互情况下，完成作业的运行。为了解决分时系统不能及时响应用户指令的情况，又出现了能够在在严格的时间范围内完成事件处理，及时响应随机外部事件的实时系统。&lt;/p&gt;
&lt;h5&gt;通用操作系统&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;1960s：IBM的System/360系列的机器有了统一的操作系统OS/360。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1965年：AT&amp;amp;T的贝尔实验室加入GE和MIT的合作计划开始开发MULTICS。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1969年：MULTICS项目失败，Ken Tompson赋闲在家，为了玩“Space Travel”游戏用汇编语言在当时已经被淘汰的PDP-7上开发了Unics。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：很难想象，Unix这么伟大的操作系统，居然是一个赋闲在家的程序员（关键是老婆回娘家还带上了孩子）在一台被淘汰的设备上为了玩游戏开发出来的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1970年~1971年：Ken Thompson和Dennis Ritchie用B语言在PDP-11上重写了Unics，并在Brian Kernighan的建议下将其更名为Unix。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day31-35/f61ce1304f.webp&quot; style=&quot;zoom:62%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1972年~1973年：Dennis Ritchie发明了C语言来取代可移植性较差的B语言，并开启了用C语言重写Unix的工作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1974年：Unix推出了里程碑意义的第5版，几乎完全用C语言来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1979年：从Unix第7版开始，AT&amp;amp;T发布新的使用条款，将Unix私有化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1987年：Andrew S. Tanenbaum教授为了能在课堂上为学生讲解操作系统运作的细节，决定在不使用任何AT&amp;amp;T的源代码前提下，自行开发与Unix兼容的操作系统以避免版权上的争议，该系统被命名为Minix。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day31-35/72817f1432.webp&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1991年：Linus Torvalds就读于芬兰赫尔辛基大学期间，尝试在Minix上做一些开发工作，但因为Minix只是作为教学用途的操作系统，功能并不强大，为了方便在学校的新闻组和邮件系统中读写和下载文件，Linus编写了磁盘驱动程序和文件系统，这些东西形成了Linux系统内核的雏形。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/9765ce314a.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下图是Unix操作系统家族的图谱。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/20a5495576.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Linux概述&lt;/h4&gt;
&lt;p&gt;Linux是一个通用操作系统。一个操作系统要负责任务调度、内存分配、处理外围设备I/O等操作。操作系统通常由内核（运行其他程序，管理像磁盘、打印机等硬件设备的核心程序）和系统程序（设备驱动、底层库、shell、服务程序等）两部分组成。&lt;/p&gt;
&lt;p&gt;Linux内核是芬兰人Linus Torvalds开发的，于1991年9月发布。而Linux操作系统作为Internet时代的产物，它是由全世界许多开发者共同合作开发的，是一个自由的操作系统（注意自由和免费并不是同一个概念，想了解二者的差别可以&lt;a href=&quot;https://www.debian.org/intro/free&quot;&gt;点击这里&lt;/a&gt;）。&lt;/p&gt;
&lt;h4&gt;Linux系统优点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;通用操作系统，不跟特定的硬件绑定。&lt;/li&gt;
&lt;li&gt;用C语言编写，可移植性强，有内核编程接口。&lt;/li&gt;
&lt;li&gt;支持多用户和多任务，支持安全的分层文件系统。&lt;/li&gt;
&lt;li&gt;大量的实用程序，完善的网络功能以及强大的支持文档。&lt;/li&gt;
&lt;li&gt;可靠的安全性和良好的稳定性，对开发者更友好。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Linux系统发行版本&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.redhat.com/en&quot;&gt;Redhat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ubuntu.com/&quot;&gt;Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.centos.org/&quot;&gt;CentOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getfedora.org/&quot;&gt;Fedora&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.debian.org/&quot;&gt;Debian&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.opensuse.org/&quot;&gt;openSUSE&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;基础命令&lt;/h4&gt;
&lt;p&gt;Linux系统的命令通常都是如下所示的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;命令名称 [命名参数] [命令对象]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取登录信息 - &lt;strong&gt;w&lt;/strong&gt; / &lt;strong&gt;who&lt;/strong&gt; / &lt;strong&gt;last&lt;/strong&gt;/ &lt;strong&gt;lastb&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# w
 23:31:16 up 12:16,  2 users,  load average: 0.00, 0.01, 0.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
root     pts/0    182.139.66.250   23:03    4.00s  0.02s  0.00s w
jackfrue pts/1    182.139.66.250   23:26    3:56   0.00s  0.00s -bash
[root ~]# who
root     pts/0        2018-04-12 23:03 (182.139.66.250)
jackfrued pts/1        2018-04-12 23:26 (182.139.66.250)
[root ~]# who am i
root     pts/0        2018-04-12 23:03 (182.139.66.250)
[root ~]# who mom likes
root     pts/0        2018-04-12 23:03 (182.139.66.250)
[root ~]# last
root     pts/0        117.136.63.184   Sun May 26 18:57   still logged in   
reboot   system boot  3.10.0-957.10.1. Mon May 27 02:52 - 19:10  (-7:-42)   
root     pts/4        117.136.63.184   Sun May 26 18:51 - crash  (08:01)    
root     pts/4        117.136.63.184   Sun May 26 18:49 - 18:49  (00:00)    
root     pts/3        117.136.63.183   Sun May 26 18:35 - crash  (08:17)    
root     pts/2        117.136.63.183   Sun May 26 18:34 - crash  (08:17)    
root     pts/0        117.136.63.183   Sun May 26 18:10 - crash  (08:42)    
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看自己使用的Shell - &lt;strong&gt;ps&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Shell也被称为“壳”或“壳程序”，它是用户与操作系统内核交流的翻译官，简单的说就是人与计算机交互的界面和接口。目前很多Linux系统默认的Shell都是bash（&amp;lt;u&amp;gt;B&amp;lt;/u&amp;gt;ourne &amp;lt;u&amp;gt;A&amp;lt;/u&amp;gt;gain &amp;lt;u&amp;gt;SH&amp;lt;/u&amp;gt;ell），因为它可以使用tab键进行命令和路径补全、可以保存历史命令、可以方便的配置环境变量以及执行批处理操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ps
  PID TTY          TIME CMD
 3531 pts/0    00:00:00 bash
 3553 pts/0    00:00:00 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看命令的说明和位置 - &lt;strong&gt;whatis&lt;/strong&gt; / &lt;strong&gt;which&lt;/strong&gt; / &lt;strong&gt;whereis&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# whatis ps
ps (1)        - report a snapshot of the current processes.
[root ~]# whatis python
python (1)    - an interpreted, interactive, object-oriented programming language
[root ~]# whereis ps
ps: /usr/bin/ps /usr/share/man/man1/ps.1.gz
[root ~]# whereis python
python: /usr/bin/python /usr/bin/python2.7 /usr/lib/python2.7 /usr/lib64/python2.7 /etc/python /usr/include/python2.7 /usr/share/man/man1/python.1.gz
[root ~]# which ps
/usr/bin/ps
[root ~]# which python
/usr/bin/python
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;清除屏幕上显示的内容 - &lt;strong&gt;clear&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看帮助文档 - &lt;strong&gt;man&lt;/strong&gt; / &lt;strong&gt;info&lt;/strong&gt; / &lt;strong&gt;--help&lt;/strong&gt; / &lt;strong&gt;apropos&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@izwz97tbgo9lkabnat2lo8z ~]# ps --help
Usage:
 ps [options]
 Try &apos;ps --help &amp;lt;simple|list|output|threads|misc|all&amp;gt;&apos;
  or &apos;ps --help &amp;lt;s|l|o|t|m|a&amp;gt;&apos;
 for additional help text.
For more details see ps(1).
[root@izwz97tbgo9lkabnat2lo8z ~]# man ps
PS(1)                                User Commands                                PS(1)
NAME
       ps - report a snapshot of the current processes.
SYNOPSIS
       ps [options]
DESCRIPTION
...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看系统和主机名 - &lt;strong&gt;uname&lt;/strong&gt; / &lt;strong&gt;hostname&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@izwz97tbgo9lkabnat2lo8z ~]# uname
Linux
[root@izwz97tbgo9lkabnat2lo8z ~]# hostname
izwz97tbgo9lkabnat2lo8z
[root@iZwz97tbgo9lkabnat2lo8Z ~]# cat /etc/centos-release
CentOS Linux release 7.6.1810 (Core)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：&lt;code&gt;cat&lt;/code&gt;是连接文件内容并打印到标准输出的命令，后面会讲到该命令；&lt;code&gt;/etc&lt;/code&gt;是Linux系统上的一个非常重要的目录，它保存了很多的配置文件；&lt;code&gt;centos-release&lt;/code&gt;是该目录下的一个文件，因为我自己使用的Linux发行版本是CentOS 7.6，因此这里会有一个这样的文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;时间和日期 - &lt;strong&gt;date&lt;/strong&gt; / &lt;strong&gt;cal&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# date
Wed Jun 20 12:53:19 CST 2018
[root@iZwz97tbgo9lkabnat2lo8Z ~]# cal
      June 2018
Su Mo Tu We Th Fr Sa
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
[root@iZwz97tbgo9lkabnat2lo8Z ~]# cal 5 2017
      May 2017
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启和关机 - &lt;strong&gt;reboot&lt;/strong&gt; / &lt;strong&gt;shutdown&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# shutdown -h +5
Shutdown scheduled for Sun 2019-05-26 19:34:27 CST, use &apos;shutdown -c&apos; to cancel.
[root ~]# 
Broadcast message from root (Sun 2019-05-26 19:29:27 CST):

The system is going down for power-off at Sun 2019-05-26 19:34:27 CST!
[root ~]# shutdown -c

Broadcast message from root (Sun 2019-05-26 19:30:22 CST):

The system shutdown has been cancelled at Sun 2019-05-26 19:31:22 CST!
[root ~]# shutdown -r 23:58
Shutdown scheduled for Sun 2019-05-26 23:58:00 CST, use &apos;shutdown -c&apos; to cancel.
[root ~]# shutdown -c

Broadcast message from root (Sun 2019-05-26 19:31:06 CST):

The system shutdown has been cancelled at Sun 2019-05-26 19:32:06 CST!
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：在执行&lt;code&gt;shutdown&lt;/code&gt;命令时会向登录系统的用户发出警告，可以在命令后面跟上警告消息来替换默认的警告消息，也可以在&lt;code&gt;-h&lt;/code&gt;参数后通过&lt;code&gt;now&lt;/code&gt;来表示立刻关机。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出登录 -  &lt;strong&gt;exit&lt;/strong&gt; / &lt;strong&gt;logout&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看历史命令 - &lt;strong&gt;history&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# history
...
452  ls
453  cd Python-3.6.5/
454  clear
455  history
[root@iZwz97tbgo9lkabnat2lo8Z ~]# !454
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：查看到历史命令之后，可以用&lt;code&gt;!历史命令编号&lt;/code&gt;来重新执行该命令；通过&lt;code&gt;history -c&lt;/code&gt;可以清除历史命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;实用程序&lt;/h4&gt;
&lt;h5&gt;文件和文件夹操作&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建/删除空目录 - &lt;strong&gt;mkdir&lt;/strong&gt; / &lt;strong&gt;rmdir&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# mkdir abc
[root ~]# mkdir -p xyz/abc
[root ~]# rmdir abc
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建/删除文件 - &lt;strong&gt;touch&lt;/strong&gt; / &lt;strong&gt;rm&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# touch readme.txt
[root ~]# touch error.txt
[root ~]# rm error.txt
rm: remove regular empty file ‘error.txt’? y
[root ~]# rm -rf xyz
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;touch&lt;/code&gt;命令用于创建空白文件或修改文件时间。在Linux系统中一个文件有三种时间：
&lt;ul&gt;
&lt;li&gt;更改内容的时间 - mtime。&lt;/li&gt;
&lt;li&gt;更改权限的时间 - ctime。&lt;/li&gt;
&lt;li&gt;最后访问时间 - atime。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm&lt;/code&gt;的几个重要参数：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt;：交互式删除，每个删除项都会进行询问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r&lt;/code&gt;：删除目录并递归的删除目录中的文件和目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;：强制删除，忽略不存在的文件，没有任何提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换和查看当前工作目录 - &lt;strong&gt;cd&lt;/strong&gt; / &lt;strong&gt;pwd&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：&lt;code&gt;cd&lt;/code&gt;命令后面可以跟相对路径（以当前路径作为参照）或绝对路径（以&lt;code&gt;/&lt;/code&gt;开头）来切换到指定的目录，也可以用&lt;code&gt;cd ..&lt;/code&gt;来返回上一级目录。请大家想一想，如果要返回到上上一级目录应该给&lt;code&gt;cd&lt;/code&gt;命令加上什么样的参数呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看目录内容 - &lt;strong&gt;ls&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt;：以长格式查看文件和目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a&lt;/code&gt;：显示以点开头的文件和目录（隐藏文件）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-R&lt;/code&gt;：遇到目录要进行递归展开（继续列出目录下面的文件和目录）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;：只列出目录，不列出其他内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-S&lt;/code&gt; / &lt;code&gt;-t&lt;/code&gt;：按大小/时间排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看文件内容 - &lt;strong&gt;cat&lt;/strong&gt; / &lt;strong&gt;tac&lt;/strong&gt; / &lt;strong&gt;head&lt;/strong&gt; / &lt;strong&gt;tail&lt;/strong&gt; / &lt;strong&gt;more&lt;/strong&gt; / &lt;strong&gt;less&lt;/strong&gt; / &lt;strong&gt;rev&lt;/strong&gt; / &lt;strong&gt;od&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# wget http://www.sohu.com/ -O sohu.html
--2018-06-20 18:42:34--  http://www.sohu.com/
Resolving www.sohu.com (www.sohu.com)... 14.18.240.6
Connecting to www.sohu.com (www.sohu.com)|14.18.240.6|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 212527 (208K) [text/html]
Saving to: ‘sohu.html’
100%[==================================================&amp;gt;] 212,527     --.-K/s   in 0.03s
2018-06-20 18:42:34 (7.48 MB/s) - ‘sohu.html’ saved [212527/212527]
[root ~]# cat sohu.html
...
[root ~]# head -10 sohu.html
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;搜狐&amp;lt;/title&amp;gt;
&amp;lt;meta name=&quot;Keywords&quot; content=&quot;搜狐,门户网站,新媒体,网络媒体,新闻,财经,体育,娱乐,时尚,汽车,房产,科技,图片,论坛,微博,博客,视频,电影,电视剧&quot;/&amp;gt;
&amp;lt;meta name=&quot;Description&quot; content=&quot;搜狐网为用户提供24小时不间断的最新资讯，及搜索、邮件等网络服务。内容包括全球热点事件、突发新闻、时事评论、热播影视剧、体育赛事、行业动态、生活服务信息，以及论坛、博客、微博、我的搜狐等互动空间。&quot; /&amp;gt;
&amp;lt;meta name=&quot;shenma-site-verification&quot; content=&quot;1237e4d02a3d8d73e96cbd97b699e9c3_1504254750&quot;&amp;gt;
&amp;lt;meta charset=&quot;utf-8&quot;/&amp;gt;
&amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=Edge,chrome=1&quot;/&amp;gt;
[root ~]# tail -2 sohu.html
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
[root ~]# less sohu.html
...
[root ~]# cat -n sohu.html | more
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面用到了一个名为&lt;code&gt;wget&lt;/code&gt;的命令，它是一个网络下载器程序，可以从指定的URL下载资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拷贝/移动文件 - &lt;strong&gt;cp&lt;/strong&gt; / &lt;strong&gt;mv&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# mkdir backup
[root ~]# cp sohu.html backup/
[root ~]# cd backup
[root backup]# ls
sohu.html
[root backup]# mv sohu.html sohu_index.html
[root backup]# ls
sohu_index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件重命名 - &lt;strong&gt;rename&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# rename .htm .html *.htm
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查找文件和查找内容 - &lt;strong&gt;find&lt;/strong&gt; / &lt;strong&gt;grep&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# find / -name &quot;*.html&quot;
/root/sohu.html
/root/backup/sohu_index.html
[root@izwz97tbgo9lkabnat2lo8z ~]# find . -atime 7 -type f -print
[root@izwz97tbgo9lkabnat2lo8z ~]# find . -type f -size +2k
[root@izwz97tbgo9lkabnat2lo8z ~]# find . -type f -name &quot;*.swp&quot; -delete
[root@iZwz97tbgo9lkabnat2lo8Z ~]# grep &quot;&amp;lt;script&amp;gt;&quot; sohu.html -n
20:&amp;lt;script&amp;gt;
[root@iZwz97tbgo9lkabnat2lo8Z ~]# grep -E \&amp;lt;\/?script.*\&amp;gt; sohu.html -n
20:&amp;lt;script&amp;gt;
22:&amp;lt;/script&amp;gt;
24:&amp;lt;script src=&quot;//statics.itc.cn/web/v3/static/js/es5-shim-08e41cfc3e.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
25:&amp;lt;script src=&quot;//statics.itc.cn/web/v3/static/js/es5-sham-1d5fa1124b.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
26:&amp;lt;script src=&quot;//statics.itc.cn/web/v3/static/js/html5shiv-21fc8c2ba6.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
29:&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
52:&amp;lt;/script&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;grep&lt;/code&gt;在搜索字符串时可以使用正则表达式，如果需要使用正则表达式可以用&lt;code&gt;grep -E&lt;/code&gt;或者直接使用&lt;code&gt;egrep&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建链接和查看链接 - &lt;strong&gt;ln&lt;/strong&gt; / &lt;strong&gt;readlink&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls -l sohu.html
-rw-r--r-- 1 root root 212131 Jun 20 19:15 sohu.html
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ln /root/sohu.html /root/backup/sohu_backup
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls -l sohu.html
-rw-r--r-- 2 root root 212131 Jun 20 19:15 sohu.html
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ln /root/sohu.html /root/backup/sohu_backup2
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls -l sohu.html
-rw-r--r-- 3 root root 212131 Jun 20 19:15 sohu.html
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ln -s /etc/centos-release sysinfo
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls -l sysinfo
lrwxrwxrwx 1 root root 19 Jun 20 19:21 sysinfo -&amp;gt; /etc/centos-release
[root@iZwz97tbgo9lkabnat2lo8Z ~]# cat sysinfo
CentOS Linux release 7.4.1708 (Core)
[root@iZwz97tbgo9lkabnat2lo8Z ~]# cat /etc/centos-release
CentOS Linux release 7.4.1708 (Core)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：链接可以分为硬链接和软链接（符号链接）。硬链接可以认为是一个指向文件数据的指针，就像Python中对象的引用计数，每添加一个硬链接，文件的对应链接数就增加1，只有当文件的链接数为0时，文件所对应的存储空间才有可能被其他文件覆盖。我们平常删除文件时其实并没有删除硬盘上的数据，我们删除的只是一个指针，或者说是数据的一条使用记录，所以类似于“文件粉碎机”之类的软件在“粉碎”文件时除了删除文件指针，还会在文件对应的存储区域填入数据来保证文件无法再恢复。软链接类似于Windows系统下的快捷方式，当软链接链接的文件被删除时，软链接也就失效了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;压缩/解压缩和归档/解归档 - &lt;strong&gt;gzip&lt;/strong&gt; / &lt;strong&gt;gunzip&lt;/strong&gt; / &lt;strong&gt;xz&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# wget http://download.redis.io/releases/redis-4.0.10.tar.gz
--2018-06-20 19:29:59--  http://download.redis.io/releases/redis-4.0.10.tar.gz
Resolving download.redis.io (download.redis.io)... 109.74.203.151
Connecting to download.redis.io (download.redis.io)|109.74.203.151|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1738465 (1.7M) [application/x-gzip]
Saving to: ‘redis-4.0.10.tar.gz’
100%[==================================================&amp;gt;] 1,738,465   70.1KB/s   in 74s
2018-06-20 19:31:14 (22.9 KB/s) - ‘redis-4.0.10.tar.gz’ saved [1738465/1738465]
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls redis*
redis-4.0.10.tar.gz
[root@iZwz97tbgo9lkabnat2lo8Z ~]# gunzip redis-4.0.10.tar.gz
[root@iZwz97tbgo9lkabnat2lo8Z ~]# ls redis*
redis-4.0.10.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;归档和解归档 - &lt;strong&gt;tar&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# tar -xvf redis-4.0.10.tar
redis-4.0.10/
redis-4.0.10/.gitignore
redis-4.0.10/00-RELEASENOTES
redis-4.0.10/BUGS
redis-4.0.10/CONTRIBUTING
redis-4.0.10/COPYING
redis-4.0.10/INSTALL
redis-4.0.10/MANIFESTO
redis-4.0.10/Makefile
redis-4.0.10/README.md
redis-4.0.10/deps/
redis-4.0.10/deps/Makefile
redis-4.0.10/deps/README.md
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：归档（也称为创建归档）和解归档都使用&lt;code&gt;tar&lt;/code&gt;命令，通常创建归档需要&lt;code&gt;-cvf&lt;/code&gt;三个参数，其中&lt;code&gt;c&lt;/code&gt;表示创建（create），&lt;code&gt;v&lt;/code&gt;表示显示创建归档详情（verbose），&lt;code&gt;f&lt;/code&gt;表示指定归档的文件（file）；解归档需要加上&lt;code&gt;-xvf&lt;/code&gt;参数，其中&lt;code&gt;x&lt;/code&gt;表示抽取（extract），其他两个参数跟创建归档相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;将标准输入转成命令行参数 - &lt;strong&gt;xargs&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面的命令会将查找当前路径下的html文件，然后通过&lt;code&gt;xargs&lt;/code&gt;将这些文件作为参数传给&lt;code&gt;rm&lt;/code&gt;命令，实现查找并删除文件的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# find . -type f -name &quot;*.html&quot; | xargs rm -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的命令将a.txt文件中的多行内容变成一行输出到b.txt文件中，其中&lt;code&gt;&amp;lt;&lt;/code&gt;表示从a.txt中读取输入，&lt;code&gt;&amp;gt;&lt;/code&gt;表示将命令的执行结果输出到b.txt中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@iZwz97tbgo9lkabnat2lo8Z ~]# xargs &amp;lt; a.txt &amp;gt; b.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：这个命令就像上面演示的那样常在管道（实现进程间通信的一种方式）和重定向（重新指定输入输出的位置）操作中用到，后面的内容中会讲到管道操作和输入输出重定向操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;显示文件或目录 - &lt;strong&gt;basename&lt;/strong&gt; / &lt;strong&gt;dirname&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他相关工具。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sort&lt;/strong&gt; - 对内容排序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uniq&lt;/strong&gt; - 去掉相邻重复内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tr&lt;/strong&gt; - 替换指定内容为新内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cut&lt;/strong&gt; / &lt;strong&gt;paste&lt;/strong&gt; - 剪切/黏贴内容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;split&lt;/strong&gt; - 拆分文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;file&lt;/strong&gt; - 判断文件类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wc&lt;/strong&gt; - 统计文件行数、单词数、字节数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iconv&lt;/strong&gt; - 编码转换&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cat foo.txt
grape
apple
pitaya
[root ~]# cat bar.txt
100
200
300
400
[root ~]# paste foo.txt bar.txt
grape   100
apple   200
pitaya  300
        400
[root ~]# paste foo.txt bar.txt &amp;gt; hello.txt
[root ~]# cut -b 4-8 hello.txt
pe      10
le      20
aya     3
0
[root ~]# cat hello.txt | tr &apos;\t&apos; &apos;,&apos;
grape,100
apple,200
pitaya,300
,400
[root ~]# split -l 100 sohu.html hello
[root ~]# wget https://www.baidu.com/img/bd_logo1.png
[root ~]# file bd_logo1.png
bd_logo1.png: PNG image data, 540 x 258, 8-bit colormap, non-interlaced
[root ~]# wc sohu.html
  2979   6355 212527 sohu.html
[root ~]# wc -l sohu.html
2979 sohu.html
[root ~]# wget http://www.qq.com -O qq.html
[root ~]# iconv -f gb2312 -t utf-8 qq.html
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;管道和重定向&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;管道的使用 - &lt;strong&gt;|&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;例子：查找当前目录下文件个数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# find ./ | wc -l
6152
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：列出当前路径下的文件和文件夹，给每一项加一个编号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ls | cat -n
     1  dump.rdb
     2  mongodb-3.6.5
     3  Python-3.6.5
     4  redis-3.2.11
     5  redis.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：查找record.log中包含AAA，但不包含BBB的记录的总数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cat record.log | grep AAA | grep -v BBB | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出重定向和错误重定向 - &lt;strong&gt;&amp;gt;&lt;/strong&gt; / &lt;strong&gt;&amp;gt;&amp;gt;&lt;/strong&gt; / &lt;strong&gt;2&amp;gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cat readme.txt
banana
apple
grape
apple
grape
watermelon
pear
pitaya
[root ~]# cat readme.txt | sort | uniq &amp;gt; result.txt
[root ~]# cat result.txt
apple
banana
grape
pear
pitaya
watermelon
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入重定向 - &lt;strong&gt;&amp;lt;&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# echo &apos;hello, world!&apos; &amp;gt; hello.txt
[root ~]# wall &amp;lt; hello.txt
[root ~]#
Broadcast message from root (Wed Jun 20 19:43:05 2018):
hello, world!
[root ~]# echo &apos;I will show you some code.&apos; &amp;gt;&amp;gt; hello.txt
[root ~]# wall &amp;lt; hello.txt
[root ~]#
Broadcast message from root (Wed Jun 20 19:43:55 2018):
hello, world!
I will show you some code.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多重定向 - &lt;strong&gt;tee&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面的命令除了在终端显示命令&lt;code&gt;ls&lt;/code&gt;的结果之外，还会追加输出到&lt;code&gt;ls.txt&lt;/code&gt;文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ls | tee -a ls.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;别名&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;alias&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# alias ll=&apos;ls -l&apos;
[root ~]# alias frm=&apos;rm -rf&apos;
[root ~]# ll
...
drwxr-xr-x  2 root       root   4096 Jun 20 12:52 abc
...
[root ~]# frm abc
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;unalias&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# unalias frm
[root ~]# frm sohu.html
-bash: frm: command not found
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;文本处理&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;字符流编辑器 - &lt;strong&gt;sed&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;sed是操作、过滤和转换文本内容的工具。假设有一个名为fruit.txt的文件，内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cat -n fruit.txt 
     1  banana
     2  grape
     3  apple
     4  watermelon
     5  orange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们在第2行后面添加一个pitaya。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;2a pitaya&apos; fruit.txt 
banana
grape
pitaya
apple
watermelon
orange
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：刚才的命令和之前我们讲过的很多命令一样并没有改变fruit.txt文件，而是将添加了新行的内容输出到终端中，如果想保存到fruit.txt中，可以使用输出重定向操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在第2行前面插入一个waxberry。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;2i waxberry&apos; fruit.txt
banana
waxberry
grape
apple
watermelon
orange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除第3行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;3d&apos; fruit.txt
banana
grape
watermelon
orange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除第2行到第4行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;2,4d&apos; fruit.txt
banana
orange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将文本中的字符a替换为@。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;s#a#@#&apos; fruit.txt 
b@nana
gr@pe
@pple
w@termelon
or@nge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将文本中的字符a替换为@，使用全局模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sed &apos;s#a#@#g&apos; fruit.txt 
b@n@n@
gr@pe
@pple
w@termelon
or@nge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模式匹配和处理语言 - &lt;strong&gt;awk&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;awk是一种编程语言，也是Linux系统中处理文本最为强大的工具，它的作者之一和现在的维护者就是之前提到过的Brian Kernighan（ken和dmr最亲密的伙伴）。通过该命令可以从文本中提取出指定的列、用正则表达式从文本中取出我们想要的内容、显示指定的行以及进行统计和运算，总之它非常强大。&lt;/p&gt;
&lt;p&gt;假设有一个名为fruit2.txt的文件，内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cat fruit2.txt 
1       banana      120
2       grape       500
3       apple       1230
4       watermelon  80
5       orange      400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示文件的第3行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# awk &apos;NR==3&apos; fruit2.txt 
3       apple       1230
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示文件的第2列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# awk &apos;{print $2}&apos; fruit2.txt 
banana
grape
apple
watermelon
orange
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示文件的最后一列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# awk &apos;{print $NF}&apos; fruit2.txt 
120
500
1230
80
400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出末尾数字大于等于300的行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# awk &apos;{if($3 &amp;gt;= 300) {print $0}}&apos; fruit2.txt 
2       grape       500
3       apple       1230
5       orange      400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面展示的只是awk命令的冰山一角，更多的内容留给读者自己在实践中去探索。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;用户管理&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建和删除用户 - &lt;strong&gt;useradd&lt;/strong&gt; / &lt;strong&gt;userdel&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root home]# useradd hellokitty
[root home]# userdel hellokitty
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; - 创建用户时为用户指定用户主目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-g&lt;/code&gt; - 创建用户时指定用户所属的用户组&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建和删除用户组 - &lt;strong&gt;groupadd&lt;/strong&gt; / &lt;strong&gt;groupdel&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：用户组主要是为了方便对一个组里面所有用户的管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改密码 - &lt;strong&gt;passwd&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# passwd hellokitty
New password: 
Retype new password: 
passwd: all authentication tokens updated successfully.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：输入密码和确认密码没有回显且必须一气呵成的输入完成（不能使用退格键），密码和确认密码需要一致。如果使用&lt;code&gt;passwd&lt;/code&gt;命令时没有指定命令作用的对象，则表示要修改当前用户的密码。如果想批量修改用户密码，可以使用&lt;code&gt;chpasswd&lt;/code&gt;命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt; / &lt;code&gt;-u&lt;/code&gt; - 锁定/解锁用户。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; - 清除用户密码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e&lt;/code&gt; - 设置密码立即过期，用户登录时会强制要求修改密码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt; - 设置密码过期多少天以后禁用该用户。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看和修改密码有效期 - &lt;strong&gt;chage&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设置hellokitty用户100天后必须修改密码，过期前15天通知该用户，过期后7天禁用该用户。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chage -M 100 -W 15 -I 7 hellokitty
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切换用户 - &lt;strong&gt;su&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# su hellokitty
[hellokitty root]$
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以管理员身份执行命令 - &lt;strong&gt;sudo&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[hellokitty ~]$ ls /root
ls: cannot open directory /root: Permission denied
[hellokitty ~]$ sudo ls /root
[sudo] password for hellokitty:
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果希望用户能够以管理员身份执行命令，用户必须要出现在sudoers名单中，sudoers文件在 &lt;code&gt;/etc&lt;/code&gt;目录下，如果希望直接编辑该文件也可以使用下面的命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑sudoers文件 - &lt;strong&gt;visudo&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里使用的编辑器是vi，关于vi的知识在后面有讲解。该文件的部分内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Allow root to run any commands anywhere 
root    ALL=(ALL)   ALL

## Allows members of the &apos;sys&apos; group to run networking, software, 
## service management apps and more.
# %sys ALL = NETWORKING, SOFTWARE, SERVICES, STORAGE, DELEGATING, PROCESSES, LOCATE, DRIVERS
## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)   ALL

## Same thing without a password
# %wheel    ALL=(ALL)   NOPASSWD: ALL

## Allows members of the users group to mount and unmount the
## cdrom as root
# %users  ALL=/sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom

## Allows members of the users group to shutdown this system
# %users  localhost=/sbin/shutdown -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示用户与用户组的信息 - &lt;strong&gt;id&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给其他用户发消息 -&lt;strong&gt;write&lt;/strong&gt; / &lt;strong&gt;wall&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;发送方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# write hellokitty
Dinner is on me.
Call me at 6pm.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[hellokitty ~]$ 
Message from root on pts/0 at 17:41 ...
Dinner is on me.
Call me at 6pm.
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看/设置是否接收其他用户发送的消息 - &lt;strong&gt;mesg&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[hellokitty ~]$ mesg
is y
[hellokitty ~]$ mesg n
[hellokitty ~]$ mesg
is n
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;文件系统&lt;/h4&gt;
&lt;h5&gt;文件和路径&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;命名规则：文件名的最大长度与文件系统类型有关，一般情况下，文件名不应该超过255个字符，虽然绝大多数的字符都可以用于文件名，但是最好使用英文大小写字母、数字、下划线、点这样的符号。文件名中虽然可以使用空格，但应该尽可能避免使用空格，否则在输入文件名时需要用将文件名放在双引号中或者通过&lt;code&gt;\&lt;/code&gt;对空格进行转义。&lt;/li&gt;
&lt;li&gt;扩展名：在Linux系统下文件的扩展名是可选的，但是使用扩展名有助于对文件内容的理解。有些应用程序要通过扩展名来识别文件，但是更多的应用程序并不依赖文件的扩展名，就像&lt;code&gt;file&lt;/code&gt;命令在识别文件时并不是依据扩展名来判定文件的类型。&lt;/li&gt;
&lt;li&gt;隐藏文件：以点开头的文件在Linux系统中是隐藏文件（不可见文件）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;目录结构&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;/bin - 基本命令的二进制文件。&lt;/li&gt;
&lt;li&gt;/boot - 引导加载程序的静态文件。&lt;/li&gt;
&lt;li&gt;/dev - 设备文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/etc&lt;/strong&gt; - 配置文件。&lt;/li&gt;
&lt;li&gt;/home - 普通用户主目录的父目录。&lt;/li&gt;
&lt;li&gt;/lib - 共享库文件。&lt;/li&gt;
&lt;li&gt;/lib64 - 共享64位库文件。&lt;/li&gt;
&lt;li&gt;/lost+found - 存放未链接文件。&lt;/li&gt;
&lt;li&gt;/media - 自动识别设备的挂载目录。&lt;/li&gt;
&lt;li&gt;/mnt - 临时挂载文件系统的挂载点。&lt;/li&gt;
&lt;li&gt;/opt - 可选插件软件包安装位置。&lt;/li&gt;
&lt;li&gt;/proc -  内核和进程信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/root&lt;/strong&gt; - 超级管理员用户主目录。&lt;/li&gt;
&lt;li&gt;/run - 存放系统运行时需要的东西。&lt;/li&gt;
&lt;li&gt;/sbin - 超级用户的二进制文件。&lt;/li&gt;
&lt;li&gt;/sys - 设备的伪文件系统。&lt;/li&gt;
&lt;li&gt;/tmp - 临时文件夹。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/usr&lt;/strong&gt; - 用户应用目录。&lt;/li&gt;
&lt;li&gt;/var - 变量数据目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;访问权限&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;chmod&lt;/strong&gt; - 改变文件模式比特。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ls -l
...
-rw-r--r--  1 root       root 211878 Jun 19 16:06 sohu.html
...
[root ~]# chmod g+w,o+w sohu.html
[root ~]# ls -l
...
-rw-rw-rw-  1 root       root 211878 Jun 19 16:06 sohu.html
...
[root ~]# chmod 644 sohu.html
[root ~]# ls -l
...
-rw-r--r--  1 root       root 211878 Jun 19 16:06 sohu.html
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：通过上面的例子可以看出，用&lt;code&gt;chmod&lt;/code&gt;改变文件模式比特有两种方式：一种是字符设定法，另一种是数字设定法。除了&lt;code&gt;chmod&lt;/code&gt;之外，可以通过&lt;code&gt;umask&lt;/code&gt;来设定哪些权限将在新文件的默认权限中被删除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;长格式查看目录或文件时显示结果及其对应权限的数值如下表所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/50b9758b06.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;chown&lt;/strong&gt; - 改变文件所有者。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ls -l
...
-rw-r--r--  1 root root     54 Jun 20 10:06 readme.txt
...
[root ~]# chown hellokitty readme.txt
[root ~]# ls -l
...
-rw-r--r--  1 hellokitty root     54 Jun 20 10:06 readme.txt
...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;chgrp&lt;/strong&gt; - 改变用户组。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;磁盘管理&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;列出文件系统的磁盘使用状况 - &lt;strong&gt;df&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G  5.0G   33G  14% /
devtmpfs        486M     0  486M   0% /dev
tmpfs           497M     0  497M   0% /dev/shm
tmpfs           497M  356K  496M   1% /run
tmpfs           497M     0  497M   0% /sys/fs/cgroup
tmpfs           100M     0  100M   0% /run/user/0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;磁盘分区表操作 - &lt;strong&gt;fdisk&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# fdisk -l
Disk /dev/vda: 42.9 GB, 42949672960 bytes, 83886080 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a42f4
   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048    83884031    41940992   83  Linux
Disk /dev/vdb: 21.5 GB, 21474836480 bytes, 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;磁盘分区工具 - &lt;strong&gt;parted&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式化文件系统 - &lt;strong&gt;mkfs&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# mkfs -t ext4 -v /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt; - 指定文件系统的类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt; - 创建文件系统时检查磁盘损坏情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v&lt;/code&gt; - 显示详细信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件系统检查 - &lt;strong&gt;fsck&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转换或拷贝文件 - &lt;strong&gt;dd&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;挂载/卸载 - &lt;strong&gt;mount&lt;/strong&gt; / &lt;strong&gt;umount&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建/激活/关闭交换分区 - &lt;strong&gt;mkswap&lt;/strong&gt; / &lt;strong&gt;swapon&lt;/strong&gt; / &lt;strong&gt;swapoff&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：执行上面这些命令会带有一定的风险，如果不清楚这些命令的用法，最好不用随意使用，在使用的过程中，最好对照参考资料进行操作，并在操作前确认是否要这么做。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;编辑器 - vim&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动vim。可以通过&lt;code&gt;vi&lt;/code&gt;或&lt;code&gt;vim&lt;/code&gt;命令来启动vim，启动时可以指定文件名来打开一个文件，如果没有指定文件名，也可以在保存的时候指定文件名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# vim guess.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;命令模式、编辑模式和末行模式：启动vim进入的是命令模式（也称为Normal模式），在命令模式下输入英文字母&lt;code&gt;i&lt;/code&gt;会进入编辑模式（Insert模式），屏幕下方出现&lt;code&gt;-- INSERT --&lt;/code&gt;提示；在编辑模式下按下&lt;code&gt;Esc&lt;/code&gt;会回到命令模式，此时如果输入英文&lt;code&gt;:&lt;/code&gt;会进入末行模式，在末行模式下输入&lt;code&gt;q!&lt;/code&gt;可以在不保存当前工作的情况下强行退出vim；在命令模式下输入&lt;code&gt;v&lt;/code&gt;会进入可视模式（Visual模式），可以用光标选择一个区域再完成对应的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存和退出vim：在命令模式下输入&lt;code&gt;:&lt;/code&gt; 进入末行模式，输入&lt;code&gt;wq&lt;/code&gt;可以实现保存退出；如果想放弃编辑的内容输入&lt;code&gt;q!&lt;/code&gt;强行退出，这一点刚才已经提到过了；在命令模式下也可以直接输入&lt;code&gt;ZZ&lt;/code&gt;实现保存退出。如果只想保存文件不退出，那么可以在末行模式下输入&lt;code&gt;w&lt;/code&gt;；可以在&lt;code&gt;w&lt;/code&gt;后面输入空格再指定要保存的文件名。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;光标操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在命令模式下可以通过&lt;code&gt;h&lt;/code&gt;、&lt;code&gt;j&lt;/code&gt;、&lt;code&gt;k&lt;/code&gt;、&lt;code&gt;l&lt;/code&gt;来控制光标向左、下、上、右的方向移动，可以在字母前输入数字来表示移动的距离，例如：&lt;code&gt;10h&lt;/code&gt;表示向左移动10个字符。&lt;/li&gt;
&lt;li&gt;在命令模式下可以通过&lt;code&gt;Ctrl+y&lt;/code&gt;和&lt;code&gt;Ctrl+e&lt;/code&gt;来实现向上、向下滚动一行文本的操作，可以通过&lt;code&gt;Ctrl+f&lt;/code&gt;和&lt;code&gt;Ctrl+b&lt;/code&gt;来实现向前和向后翻页的操作。&lt;/li&gt;
&lt;li&gt;在命令模式下可以通过输入英文字母&lt;code&gt;G&lt;/code&gt;将光标移到文件的末尾，可以通过&lt;code&gt;gg&lt;/code&gt;将光标移到文件的开始，也可以通过在&lt;code&gt;G&lt;/code&gt;前输入数字来将光标移动到指定的行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文本操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除：在命令模式下可以用&lt;code&gt;dd&lt;/code&gt;来删除整行；可以在&lt;code&gt;dd&lt;/code&gt;前加数字来指定删除的行数；可以用&lt;code&gt;d$&lt;/code&gt;来实现删除从光标处删到行尾的操作，也可以通过&lt;code&gt;d0&lt;/code&gt;来实现从光标处删到行首的操作；如果想删除一个单词，可以使用&lt;code&gt;dw&lt;/code&gt;；如果要删除全文，可以在输入&lt;code&gt;:%d&lt;/code&gt;（其中&lt;code&gt;:&lt;/code&gt;用来从命令模式进入末行模式）。&lt;/li&gt;
&lt;li&gt;复制和粘贴：在命令模式下可以用&lt;code&gt;yy&lt;/code&gt;来复制整行；可以在&lt;code&gt;yy&lt;/code&gt;前加数字来指定复制的行数；可以通过&lt;code&gt;p&lt;/code&gt;将复制的内容粘贴到光标所在的地方。&lt;/li&gt;
&lt;li&gt;撤销和恢复：在命令模式下输入&lt;code&gt;u&lt;/code&gt;可以撤销之前的操作；通过&lt;code&gt;Ctrl+r&lt;/code&gt;可以恢复被撤销的操作。&lt;/li&gt;
&lt;li&gt;对内容进行排序：在命令模式下输入&lt;code&gt;%!sort&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找和替换。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找操作需要输入&lt;code&gt;/&lt;/code&gt;进入末行模式并提供正则表达式来匹配与之对应的内容，例如：&lt;code&gt;/doc.*\.&lt;/code&gt;，输入&lt;code&gt;n&lt;/code&gt;来向前搜索，也可以输入&lt;code&gt;N&lt;/code&gt;来向后搜索。&lt;/li&gt;
&lt;li&gt;替换操作需要输入&lt;code&gt;:&lt;/code&gt;进入末行模式并指定搜索的范围、正则表达式以及替换后的内容和匹配选项，例如：&lt;code&gt;:1,$s/doc.*/hello/gice&lt;/code&gt;，其中：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;g&lt;/code&gt; - global：全局匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; - ignore case：忽略大小写匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; - confirm：替换时需要确认。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e&lt;/code&gt; - error：忽略错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参数设定：在输入&lt;code&gt;:&lt;/code&gt;进入末行模式后可以对vim进行设定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置Tab键的空格数：&lt;code&gt;set ts=4&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置显示/不显示行号：&lt;code&gt;set nu&lt;/code&gt; / &lt;code&gt;set nonu&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置启用/关闭高亮语法：&lt;code&gt;syntax on&lt;/code&gt; / &lt;code&gt;syntax off&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置显示标尺（光标所在的行和列）： &lt;code&gt;set ruler&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置启用/关闭搜索结果高亮：&lt;code&gt;set hls&lt;/code&gt; / &lt;code&gt;set nohls&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：如果希望上面的这些设定在每次启动vim时都能自动生效，需要将这些设定写到用户主目录下的.vimrc文件中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高级技巧&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;比较多个文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# vim -d foo.txt bar.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/134501d4f2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开多个文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# vim foo.txt bar.txt hello.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动vim后只有一个窗口显示的是foo.txt，可以在末行模式中输入&lt;code&gt;ls&lt;/code&gt;查看到打开的三个文件，也可以在末行模式中输入&lt;code&gt;b &amp;lt;num&amp;gt;&lt;/code&gt;来显示另一个文件，例如可以用&lt;code&gt;:b 2&lt;/code&gt;将bar.txt显示出来，可以用&lt;code&gt;:b 3&lt;/code&gt;将hello.txt显示出来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拆分和切换窗口。&lt;/p&gt;
&lt;p&gt;可以在末行模式中输入&lt;code&gt;sp&lt;/code&gt;或&lt;code&gt;vs&lt;/code&gt;来实现对窗口的水平或垂直拆分，这样我们就可以同时打开多个编辑窗口，通过按两次&lt;code&gt;Ctrl+w&lt;/code&gt;就可以实现编辑窗口的切换，在一个窗口中执行退出操作只会关闭对应的窗口，其他的窗口继续保留。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/2cf49ae824.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;映射快捷键：在vim下可以将一些常用操作映射为快捷键来提升工作效率。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;例子1：在命令模式下输入&lt;code&gt;F4&lt;/code&gt;执行从第一行开始删除10000行代码的操作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:map &amp;lt;F4&amp;gt; gg10000dd&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例子2：在编辑模式下输入&lt;code&gt;__main&lt;/code&gt;直接补全为&lt;code&gt;if __name__ == &apos;__main__&apos;:&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:inoremap __main if __name__ == &apos;__main__&apos;:&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面例子2的&lt;code&gt;inoremap&lt;/code&gt;中的&lt;code&gt;i&lt;/code&gt;表示映射的键在编辑模式使用， &lt;code&gt;nore&lt;/code&gt;表示不要递归，这一点非常重要，否则如果键对应的内容中又出现键本身，就会引发递归（相当于进入了死循环）。如果希望映射的快捷键每次启动vim时都能生效，需要将映射写到用户主目录下的.vimrc文件中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;录制宏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在命令模式下输入&lt;code&gt;qa&lt;/code&gt;开始录制宏（其中&lt;code&gt;a&lt;/code&gt;是寄存器的名字，也可以是其他英文字母或0-9的数字）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行你的操作（光标操作、编辑操作等），这些操作都会被录制下来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果录制的操作已经完成了，按&lt;code&gt;q&lt;/code&gt;结束录制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;@a&lt;/code&gt;（&lt;code&gt;a&lt;/code&gt;是刚才使用的寄存器的名字）播放宏，如果要多次执行宏可以在前面加数字，例如&lt;code&gt;100@a&lt;/code&gt;表示将宏播放100次。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以试一试下面的例子来体验录制宏的操作，该例子来源于&lt;a href=&quot;https://harttle.land/tags.html#Vim&quot;&gt;Harttle Land网站&lt;/a&gt;，该网站上提供了很多关于vim的使用技巧，有兴趣的可以了解一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/python/day31-35/3238417c35.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;软件安装和配置&lt;/h4&gt;
&lt;h5&gt;使用包管理工具&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;yum&lt;/strong&gt; - Yellowdog Updater Modified。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yum search&lt;/code&gt;：搜索软件包，例如&lt;code&gt;yum search nginx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum list installed&lt;/code&gt;：列出已经安装的软件包，例如&lt;code&gt;yum list installed | grep zlib&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum install&lt;/code&gt;：安装软件包，例如&lt;code&gt;yum install nginx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum remove&lt;/code&gt;：删除软件包，例如&lt;code&gt;yum remove nginx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum update&lt;/code&gt;：更新软件包，例如&lt;code&gt;yum update&lt;/code&gt;可以更新所有软件包，而&lt;code&gt;yum update tar&lt;/code&gt;只会更新tar。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum check-update&lt;/code&gt;：检查有哪些可以更新的软件包。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum info&lt;/code&gt;：显示软件包的相关信息，例如&lt;code&gt;yum info nginx&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rpm&lt;/strong&gt; - Redhat Package Manager。
&lt;ul&gt;
&lt;li&gt;安装软件包：&lt;code&gt;rpm -ivh &amp;lt;packagename&amp;gt;.rpm&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;移除软件包：&lt;code&gt;rpm -e &amp;lt;packagename&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查询软件包：&lt;code&gt;rpm -qa&lt;/code&gt;，例如可以用&lt;code&gt;rpm -qa | grep mysql&lt;/code&gt;来检查是否安装了MySQL相关的软件包。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面以Nginx为例，演示如何使用yum安装软件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# yum -y install nginx
...
Installed:
  nginx.x86_64 1:1.12.2-2.el7
Dependency Installed:
  nginx-all-modules.noarch 1:1.12.2-2.el7
  nginx-mod-http-geoip.x86_64 1:1.12.2-2.el7
  nginx-mod-http-image-filter.x86_64 1:1.12.2-2.el7
  nginx-mod-http-perl.x86_64 1:1.12.2-2.el7
  nginx-mod-http-xslt-filter.x86_64 1:1.12.2-2.el7
  nginx-mod-mail.x86_64 1:1.12.2-2.el7
  nginx-mod-stream.x86_64 1:1.12.2-2.el7
Complete!
[root ~]# yum info nginx
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Installed Packages
Name        : nginx
Arch        : x86_64
Epoch       : 1
Version     : 1.12.2
Release     : 2.el7
Size        : 1.5 M
Repo        : installed
From repo   : epel
Summary     : A high performance web server and reverse proxy server
URL         : http://nginx.org/
License     : BSD
Description : Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and
            : IMAP protocols, with a strong focus on high concurrency, performance and low
            : memory usage.
[root ~]# nginx -v
nginx version: nginx/1.12.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移除Nginx。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# yum -y remove nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面以MySQL为例，演示如何使用rpm安装软件。要安装MySQL需要先到&lt;a href=&quot;https://www.mysql.com/&quot;&gt;MySQL官方网站&lt;/a&gt;下载对应的&lt;a href=&quot;https://dev.mysql.com/downloads/mysql/&quot;&gt;RPM文件&lt;/a&gt;，当然要选择和你使用的Linux系统对应的版本。MySQL现在是Oracle公司旗下的产品，在MySQL被收购后，MySQL的作者重新制作了一个MySQL的分支MariaDB，可以通过yum进行安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root mysql]# ls
mysql-community-client-5.7.22-1.el7.x86_64.rpm
mysql-community-common-5.7.22-1.el7.x86_64.rpm
mysql-community-libs-5.7.22-1.el7.x86_64.rpm
mysql-community-server-5.7.22-1.el7.x86_64.rpm
[root mysql]# yum -y remove mariadb-libs
[root mysql]# yum -y install libaio
[root mysql]#rpm -ivh mysql-community-common-5.7.26-1.el7.x86_64.rpm
...
[root mysql]#rpm -ivh mysql-community-libs-5.7.26-1.el7.x86_64.rpm
...
[root mysql]#rpm -ivh mysql-community-client-5.7.26-1.el7.x86_64.rpm
...
[root mysql]#rpm -ivh mysql-community-server-5.7.26-1.el7.x86_64.rpm
...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：由于MySQL和&lt;a href=&quot;https://mariadb.org/&quot;&gt;MariaDB&lt;/a&gt;的底层依赖库是有冲突的，所以上面我们首先用&lt;code&gt;yum&lt;/code&gt;移除了名为mariadb-libs的依赖库并安装了名为libaio支持异步I/O操作的依赖库。关于MySQL和MariaDB之间的关系，可以阅读&lt;a href=&quot;https://zh.wikipedia.org/wiki/MariaDB&quot;&gt;维基百科&lt;/a&gt;上关于MariaDB的介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;移除安装的MySQL。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# rpm -qa | grep mysql | xargs rpm -e
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;下载解压配置环境变量&lt;/h5&gt;
&lt;p&gt;下面以安装MongoDB为例，演示这类软件应该如何安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-3.6.5.tgz
--2018-06-21 18:32:53--  https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-3.6.5.tgz
Resolving fastdl.mongodb.org (fastdl.mongodb.org)... 52.85.83.16, 52.85.83.228, 52.85.83.186, ...
Connecting to fastdl.mongodb.org (fastdl.mongodb.org)|52.85.83.16|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 100564462 (96M) [application/x-gzip]
Saving to: ‘mongodb-linux-x86_64-rhel70-3.6.5.tgz’
100%[==================================================&amp;gt;] 100,564,462  630KB/s   in 2m 9s
2018-06-21 18:35:04 (760 KB/s) - ‘mongodb-linux-x86_64-rhel70-3.6.5.tgz’ saved [100564462/100564462]
[root ~]# gunzip mongodb-linux-x86_64-rhel70-3.6.5.tgz
[root ~]# tar -xvf mongodb-linux-x86_64-rhel70-3.6.5.tar
mongodb-linux-x86_64-rhel70-3.6.5/README
mongodb-linux-x86_64-rhel70-3.6.5/THIRD-PARTY-NOTICES
mongodb-linux-x86_64-rhel70-3.6.5/MPL-2
mongodb-linux-x86_64-rhel70-3.6.5/GNU-AGPL-3.0
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongodump
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongorestore
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongoexport
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongoimport
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongostat
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongotop
mongodb-linux-x86_64-rhel70-3.6.5/bin/bsondump
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongofiles
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongoreplay
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongoperf
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongod
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongos
mongodb-linux-x86_64-rhel70-3.6.5/bin/mongo
mongodb-linux-x86_64-rhel70-3.6.5/bin/install_compass
[root ~]# vim .bash_profile
...
PATH=$PATH:$HOME/bin:$HOME/mongodb-linux-x86_64-rhel70-3.6.5/bin
export PATH
...
[root ~]# source .bash_profile
[root ~]# mongod --version
db version v3.6.5
git version: a20ecd3e3a174162052ff99913bc2ca9a839d618
OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013
allocator: tcmalloc
modules: none
build environment:
    distmod: rhel70
    distarch: x86_64
    target_arch: x86_64
[root ~]# mongo --version
MongoDB shell version v3.6.5
git version: a20ecd3e3a174162052ff99913bc2ca9a839d618
OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013
allocator: tcmalloc
modules: none
build environment:
    distmod: rhel70
    distarch: x86_64
    target_arch: x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：当然也可以通过yum来安装MongoDB，具体可以参照&lt;a href=&quot;https://docs.mongodb.com/master/administration/install-on-linux/&quot;&gt;官方网站&lt;/a&gt;上给出的说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;源代码构建安装&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装Python 3.6。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# yum install gcc
[root ~]# wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz
[root ~]# gunzip Python-3.6.5.tgz
[root ~]# tar -xvf Python-3.6.5.tar
[root ~]# cd Python-3.6.5
[root ~]# ./configure --prefix=/usr/local/python36 --enable-optimizations
[root ~]# yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel
[root ~]# make &amp;amp;&amp;amp; make install
...
[root ~]# ln -s /usr/local/python36/bin/python3.6 /usr/bin/python3
[root ~]# python3 --version
Python 3.6.5
[root ~]# python3 -m pip install -U pip
[root ~]# pip3 --version
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：上面在安装好Python之后还需要注册PATH环境变量，将Python安装路径下bin文件夹的绝对路径注册到PATH环境变量中。注册环境变量可以修改用户主目录下的.bash_profile或者/etc目录下的profile文件，二者的区别在于前者相当于是用户环境变量，而后者相当于是系统环境变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装Redis-3.2.12。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# wget http://download.redis.io/releases/redis-3.2.12.tar.gz
[root ~]# gunzip redis-3.2.12.tar.gz
[root ~]# tar -xvf redis-3.2.12.tar
[root ~]# cd redis-3.2.12
[root ~]# make &amp;amp;&amp;amp; make install
[root ~]# redis-server --version
Redis server v=3.2.12 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64 build=5bc5cd3c03d6ceb6
[root ~]# redis-cli --version
redis-cli 3.2.12
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;配置服务&lt;/h4&gt;
&lt;p&gt;我们可以Linux系统下安装和配置各种服务，也就是说我们可以把Linux系统打造成数据库服务器、Web服务器、缓存服务器、文件服务器、消息队列服务器等等。Linux下的大多数服务都被设置为守护进程（驻留在系统后台运行，但不会因为服务还在运行而导致Linux无法停止运行），所以我们安装的服务通常名字后面都有一个字母&lt;code&gt;d&lt;/code&gt;，它是英文单词&lt;code&gt;daemon&lt;/code&gt;的缩写，例如：防火墙服务叫firewalld，我们之前安装的MySQL服务叫mysqld，Apache服务器叫httpd等。在安装好服务之后，可以使用&lt;code&gt;systemctl&lt;/code&gt;命令或&lt;code&gt;service&lt;/code&gt;命令来完成对服务的启动、停止等操作，具体操作如下所示。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动防火墙服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# systemctl start firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终止防火墙服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# systemctl stop firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启防火墙服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# systemctl restart firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看防火墙服务状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# systemctl status firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置/禁用防火墙服务开机自启。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# systemctl enable firewalld
Created symlink from /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service to /usr/lib/systemd/system/firewalld.service.
Created symlink from /etc/systemd/system/multi-user.target.wants/firewalld.service to /usr/lib/systemd/system/firewalld.service.
[root ~]# systemctl disable firewalld
Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service.
Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;计划任务&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在指定的时间执行命令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;at&lt;/strong&gt; - 将任务排队，在指定的时间执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;atq&lt;/strong&gt; - 查看待执行的任务队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;atrm&lt;/strong&gt; - 从队列中删除待执行的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指定3天以后下午5点要执行的任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# at 5pm+3days
at&amp;gt; rm -f /root/*.html
at&amp;gt; &amp;lt;EOT&amp;gt;
job 9 at Wed Jun  5 17:00:00 2019
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看待执行的任务队列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# atq
9       Wed Jun  5 17:00:00 2019 a root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从队列中删除指定的任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]$ atrm 9
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计划任务表 - &lt;strong&gt;crontab&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# crontab -e
* * * * * echo &quot;hello, world!&quot; &amp;gt;&amp;gt; /root/hello.txt
59 23 * * * rm -f /root/*.log
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：输入&lt;code&gt;crontab -e&lt;/code&gt;命令会打开vim来编辑Cron表达式并指定触发的任务，上面我们定制了两个计划任务，一个是每分钟向/root目录下的hello.txt中追加输出&lt;code&gt;hello, world!&lt;/code&gt;；另一个是每天23时59分执行删除/root目录下以log为后缀名的文件。如果不知道Cron表达式如何书写，可以参照/etc/crontab文件中的提示（下面会讲到）或者用搜索引擎找一下“Cron表达式在线生成器”来生成Cron表达式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和crontab相关的文件在&lt;code&gt;/etc&lt;/code&gt;目录下，通过修改&lt;code&gt;/etc&lt;/code&gt;目录下的crontab文件也能够定制计划任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# cd /etc
[root etc]# ls -l | grep cron
-rw-------.  1 root root      541 Aug  3  2017 anacrontab
drwxr-xr-x.  2 root root     4096 Mar 27 11:56 cron.d
drwxr-xr-x.  2 root root     4096 Mar 27 11:51 cron.daily
-rw-------.  1 root root        0 Aug  3  2017 cron.deny
drwxr-xr-x.  2 root root     4096 Mar 27 11:50 cron.hourly
drwxr-xr-x.  2 root root     4096 Jun 10  2014 cron.monthly
-rw-r--r--   1 root root      493 Jun 23 15:09 crontab
drwxr-xr-x.  2 root root     4096 Jun 10  2014 cron.weekly
[root etc]# vim crontab
  1 SHELL=/bin/bash
  2 PATH=/sbin:/bin:/usr/sbin:/usr/bin
  3 MAILTO=root
  4
  5 # For details see man 4 crontabs
  6
  7 # Example of job definition:
  8 # .---------------- minute (0 - 59)
  9 # |  .------------- hour (0 - 23)
 10 # |  |  .---------- day of month (1 - 31)
 11 # |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
 12 # |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
 13 # |  |  |  |  |
 14 # *  *  *  *  * user-name  command to be executed
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;网络访问和管理&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安全远程连接 - &lt;strong&gt;ssh&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]$ ssh root@120.77.222.217
The authenticity of host &apos;120.77.222.217 (120.77.222.217)&apos; can&apos;t be established.
ECDSA key fingerprint is SHA256:BhUhykv+FvnIL03I9cLRpWpaCxI91m9n7zBWrcXRa8w.
ECDSA key fingerprint is MD5:cc:85:e9:f0:d7:07:1a:26:41:92:77:6b:7f:a0:92:65.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added &apos;120.77.222.217&apos; (ECDSA) to the list of known hosts.
root@120.77.222.217&apos;s password: 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过网络获取资源 - &lt;strong&gt;wget&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-b 后台下载模式&lt;/li&gt;
&lt;li&gt;-O 下载到指定的目录&lt;/li&gt;
&lt;li&gt;-r 递归下载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送和接收邮件 - &lt;strong&gt;mail&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络配置工具（旧） - &lt;strong&gt;ifconfig&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ifconfig eth0
eth0: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet 172.18.61.250  netmask 255.255.240.0  broadcast 172.18.63.255
        ether 00:16:3e:02:b6:46  txqueuelen 1000  (Ethernet)
        RX packets 1067841  bytes 1296732947 (1.2 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 409912  bytes 43569163 (41.5 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络配置工具（新） - &lt;strong&gt;ip&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ip address
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:16:3e:02:b6:46 brd ff:ff:ff:ff:ff:ff
    inet 172.18.61.250/20 brd 172.18.63.255 scope global eth0
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络可达性检查 - &lt;strong&gt;ping&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ping www.baidu.com -c 3
PING www.a.shifen.com (220.181.111.188) 56(84) bytes of data.
64 bytes from 220.181.111.188 (220.181.111.188): icmp_seq=1 ttl=51 time=36.3 ms
64 bytes from 220.181.111.188 (220.181.111.188): icmp_seq=2 ttl=51 time=36.4 ms
64 bytes from 220.181.111.188 (220.181.111.188): icmp_seq=3 ttl=51 time=36.4 ms
--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 36.392/36.406/36.427/0.156 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示或管理路由表 - &lt;strong&gt;route&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看网络服务和端口 - &lt;strong&gt;netstat&lt;/strong&gt; / &lt;strong&gt;ss&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# netstat -nap | grep nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网络监听抓包 - &lt;strong&gt;tcpdump&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全文件拷贝 - &lt;strong&gt;scp&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# scp root@1.2.3.4:/root/guido.jpg hellokitty@4.3.2.1:/home/hellokitty/pic.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;文件同步工具 - &lt;strong&gt;rsync&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：使用&lt;code&gt;rsync&lt;/code&gt;可以实现文件的自动同步，这个对于文件服务器来说相当重要。关于这个命令的用法，我们在后面讲项目部署的时候为大家详细说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全文件传输 - &lt;strong&gt;sftp&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sftp root@1.2.3.4
root@1.2.3.4&apos;s password:
Connected to 1.2.3.4.
sftp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;help&lt;/code&gt;：显示帮助信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ls&lt;/code&gt;/&lt;code&gt;lls&lt;/code&gt;：显示远端/本地目录列表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cd&lt;/code&gt;/&lt;code&gt;lcd&lt;/code&gt;：切换远端/本地路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mkdir&lt;/code&gt;/&lt;code&gt;lmkdir&lt;/code&gt;：创建远端/本地目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pwd&lt;/code&gt;/&lt;code&gt;lpwd&lt;/code&gt;：显示远端/本地当前工作目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt;：下载文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;put&lt;/code&gt;：上传文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt;：删除远端文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;bye&lt;/code&gt;/&lt;code&gt;exit&lt;/code&gt;/&lt;code&gt;quit&lt;/code&gt;：退出sftp。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;进程管理&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看进程 - &lt;strong&gt;ps&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 Jun23 ?        00:00:05 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
root         2     0  0 Jun23 ?        00:00:00 [kthreadd]
...
[root ~]# ps -ef | grep mysqld
root      4943  4581  0 22:45 pts/0    00:00:00 grep --color=auto mysqld
mysql    25257     1  0 Jun25 ?        00:00:39 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示进程状态树 - &lt;strong&gt;pstree&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# pstree
systemd─┬─AliYunDun───18*[{AliYunDun}]
        ├─AliYunDunUpdate───3*[{AliYunDunUpdate}]
        ├─2*[agetty]
        ├─aliyun-service───2*[{aliyun-service}]
        ├─atd
        ├─auditd───{auditd}
        ├─dbus-daemon
        ├─dhclient
        ├─irqbalance
        ├─lvmetad
        ├─mysqld───28*[{mysqld}]
        ├─nginx───2*[nginx]
        ├─ntpd
        ├─polkitd───6*[{polkitd}]
        ├─rsyslogd───2*[{rsyslogd}]
        ├─sshd───sshd───bash───pstree
        ├─systemd-journal
        ├─systemd-logind
        ├─systemd-udevd
        └─tuned───4*[{tuned}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找与指定条件匹配的进程 - &lt;strong&gt;pgrep&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]$ pgrep mysqld
3584
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过进程号终止进程 - &lt;strong&gt;kill&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
[root ~]# kill 1234
[root ~]# kill -9 1234
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过进程名终止进程 - &lt;strong&gt;killall&lt;/strong&gt; / &lt;strong&gt;pkill&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;结束名为mysqld的进程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# pkill mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结束hellokitty用户的所有进程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# pkill -u hellokitty
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：这样的操作会让hellokitty用户和服务器断开连接。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将进程置于后台运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+Z&lt;/code&gt; - 快捷键，用于停止进程并置于后台。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; - 将进程置于后台运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# mongod &amp;amp;
[root ~]# redis-server
...
^Z
[4]+  Stopped                 redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询后台进程 - &lt;strong&gt;jobs&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# jobs
[2]   Running                 mongod &amp;amp;
[3]-  Stopped                 cat
[4]+  Stopped                 redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让进程在后台继续运行 - &lt;strong&gt;bg&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# bg %4
[4]+ redis-server &amp;amp;
[root ~]# jobs
[2]   Running                 mongod &amp;amp;
[3]+  Stopped                 cat
[4]-  Running                 redis-server &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将后台进程置于前台 - &lt;strong&gt;fg&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# fg %4
redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：置于前台的进程可以使用&lt;code&gt;Ctrl+C&lt;/code&gt;来终止它。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调整程序/进程运行时优先级 - &lt;strong&gt;nice&lt;/strong&gt; / &lt;strong&gt;renice&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户登出后进程继续工作 - &lt;strong&gt;nohup&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# nohup ping www.baidu.com &amp;gt; result.txt &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;跟踪进程系统调用情况 - &lt;strong&gt;strace&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# pgrep mysqld
8803
[root ~]# strace -c -p 8803
strace: Process 8803 attached
^Cstrace: Process 8803 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.18    0.005719        5719         1           restart_syscall
  0.49    0.000028          28         1           mprotect
  0.24    0.000014          14         1           clone
  0.05    0.000003           3         1           mmap
  0.03    0.000002           2         1           accept
------ ----------- ----------- --------- --------- ----------------
100.00    0.005766                     5           total
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：这个命令的用法和参数都比较复杂，建议大家在真正用到这个命令的时候再根据实际需要进行了解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看当前运行级别 - &lt;strong&gt;runlevel&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# runlevel
N 3
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实时监控进程占用资源状况 - &lt;strong&gt;top&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# top
top - 23:04:23 up 3 days, 14:10,  1 user,  load average: 0.00, 0.01, 0.05
Tasks:  65 total,   1 running,  64 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016168 total,   191060 free,   324700 used,   500408 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   530944 avail Mem
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt; - 显示进程的整个路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; - 指定两次刷屏之间的间隔时间（秒为单位）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt; - 不显示闲置进程或僵尸进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt; - 显示指定进程的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;系统诊断&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;系统启动异常诊断 - &lt;strong&gt;dmesg&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看系统活动信息 - &lt;strong&gt;sar&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# sar -u -r 5 10
Linux 3.10.0-957.10.1.el7.x86_64 (izwz97tbgo9lkabnat2lo8z)      06/02/2019      _x86_64_        (2 CPU)

06:48:30 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
06:48:35 PM     all      0.10      0.00      0.10      0.00      0.00     99.80

06:48:30 PM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
06:48:35 PM   1772012   2108392     54.33    102816   1634528    784940     20.23    793328   1164704         0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-A&lt;/code&gt; - 显示所有设备（CPU、内存、磁盘）的运行状况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u&lt;/code&gt; - 显示所有CPU的负载情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; - 显示所有磁盘的使用情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r&lt;/code&gt; - 显示内存的使用情况。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-n&lt;/code&gt; - 显示网络运行状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看内存使用情况 - &lt;strong&gt;free&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# free
              total        used        free      shared  buff/cache   available
Mem:        1016168      323924      190452         356      501792      531800
Swap:             0           0           0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟内存统计 - &lt;strong&gt;vmstat&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 204020  79036 667532    0    0     5    18  101   58  1  0 99  0  0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPU信息统计 - &lt;strong&gt;mpstat&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# mpstat
Linux 3.10.0-957.5.1.el7.x86_64 (iZ8vba0s66jjlfmo601w4xZ)       05/30/2019      _x86_64_        (1 CPU)

01:51:54 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
01:51:54 AM  all    0.71    0.00    0.17    0.04    0.00    0.00    0.00    0.00    0.00   99.07
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看进程使用内存状况 - &lt;strong&gt;pmap&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ps
  PID TTY          TIME CMD
 4581 pts/0    00:00:00 bash
 5664 pts/0    00:00:00 ps
[root ~]# pmap 4581
4581:   -bash
0000000000400000    884K r-x-- bash
00000000006dc000      4K r---- bash
00000000006dd000     36K rw--- bash
00000000006e6000     24K rw---   [ anon ]
0000000001de0000    400K rw---   [ anon ]
00007f82fe805000     48K r-x-- libnss_files-2.17.so
00007f82fe811000   2044K ----- libnss_files-2.17.so
...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;报告设备CPU和I/O统计信息 - &lt;strong&gt;iostat&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# iostat
Linux 3.10.0-693.11.1.el7.x86_64 (iZwz97tbgo9lkabnat2lo8Z)      06/26/2018      _x86_64_       (1 CPU)
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           0.79    0.00    0.20    0.04    0.00   98.97
Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vda               0.85         6.78        21.32    2106565    6623024
vdb               0.00         0.01         0.00       2088          0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示所有PCI设备 - &lt;strong&gt;lspci&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01)
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Cirrus Logic GD 5446
00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device
00:04.0 Communication controller: Red Hat, Inc. Virtio console
00:05.0 SCSI storage controller: Red Hat, Inc. Virtio block device
00:06.0 SCSI storage controller: Red Hat, Inc. Virtio block device
00:07.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示进程间通信设施的状态 - &lt;strong&gt;ipcs&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root ~]# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Shell编程&lt;/h4&gt;
&lt;p&gt;之前我们提到过，Shell是一个连接用户和操作系统的应用程序，它提供了人机交互的界面（接口），用户通过这个界面访问操作系统内核的服务。Shell脚本是一种为Shell编写的脚本程序，我们可以通过Shell脚本来进行系统管理，同时也可以通过它进行文件操作。总之，编写Shell脚本对于使用Linux系统的人来说，应该是一项标配技能。&lt;/p&gt;
&lt;p&gt;互联网上有大量关于Shell脚本的相关知识，我不打算再此对Shell脚本做一个全面系统的讲解，我们通过下面的代码来感性的认识下Shell脚本就行了。&lt;/p&gt;
&lt;p&gt;例子1：输入两个整数m和n，计算从m到n的整数求和的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/bash
printf &apos;m = &apos;
read m
printf &apos;n = &apos;
read n
a=$m
sum=0
while [ $a -le $n ]
do
    sum=$[ sum + a ]
    a=$[ a + 1 ]
done
echo &apos;结果: &apos;$sum
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子2：自动创建文件夹和指定数量的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/bash
printf &apos;输入文件夹名: &apos;
read dir
printf &apos;输入文件名: &apos;
read file
printf &apos;输入文件数量(&amp;lt;1000): &apos;
read num
if [ $num -ge 1000 ]
then
    echo &apos;文件数量不能超过1000&apos;
else
    if [ -e $dir -a -d $dir ]
    then
        rm -rf $dir
    else
        if [ -e $dir -a -f $dir ]
        then
            rm -f $dir
        fi
    fi
    mkdir -p $dir
    index=1
    while [ $index -le $num ]
    do
        if [ $index -lt 10 ]
        then
            pre=&apos;00&apos;
        elif [ $index -lt 100 ]
        then
            pre=&apos;0&apos;
        else
            pre=&apos;&apos;
        fi
        touch $dir&apos;/&apos;$file&apos;_&apos;$pre$index
        index=$[ index + 1 ]
    done
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子3：自动安装指定版本的Redis。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/bash
install_redis() {
    if ! which redis-server &amp;gt; /dev/null
    then
        cd /root
        wget $1$2&apos;.tar.gz&apos; &amp;gt;&amp;gt; install.log
        gunzip /root/$2&apos;.tar.gz&apos;
        tar -xf /root/$2&apos;.tar&apos;
        cd /root/$2
        make &amp;gt;&amp;gt; install.log
        make install &amp;gt;&amp;gt; install.log
        echo &apos;安装完成&apos;
    else
        echo &apos;已经安装过Redis&apos;
    fi
}

install_redis &apos;http://download.redis.io/releases/&apos; $1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;相关资源&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Linux命令行常用快捷键&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tab&lt;/td&gt;
&lt;td&gt;自动补全命令或路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+a&lt;/td&gt;
&lt;td&gt;将光标移动到命令行行首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+e&lt;/td&gt;
&lt;td&gt;将光标移动到命令行行尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+f&lt;/td&gt;
&lt;td&gt;将光标向右移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+b&lt;/td&gt;
&lt;td&gt;将光标向左移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+k&lt;/td&gt;
&lt;td&gt;剪切从光标到行尾的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+u&lt;/td&gt;
&lt;td&gt;剪切从光标到行首的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+w&lt;/td&gt;
&lt;td&gt;剪切光标前面的一个单词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+y&lt;/td&gt;
&lt;td&gt;复制剪切命名剪切的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+c&lt;/td&gt;
&lt;td&gt;中断正在执行的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+h&lt;/td&gt;
&lt;td&gt;删除光标前面的一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+d&lt;/td&gt;
&lt;td&gt;退出当前命令行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+r&lt;/td&gt;
&lt;td&gt;搜索历史命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+g&lt;/td&gt;
&lt;td&gt;退出历史命令搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+l&lt;/td&gt;
&lt;td&gt;清除屏幕上所有内容在屏幕的最上方开启一个新行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+s&lt;/td&gt;
&lt;td&gt;锁定终端使之暂时无法输入内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+q&lt;/td&gt;
&lt;td&gt;退出终端锁定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+z&lt;/td&gt;
&lt;td&gt;将正在终端执行的任务停下来放到后台&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!!&lt;/td&gt;
&lt;td&gt;执行上一条命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!数字&lt;/td&gt;
&lt;td&gt;执行数字对应的历史命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!字母&lt;/td&gt;
&lt;td&gt;执行最近的以字母打头的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!$ / Esc+.&lt;/td&gt;
&lt;td&gt;获得上一条命令最后一个参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Esc+b&lt;/td&gt;
&lt;td&gt;移动到当前单词的开头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Esc+f&lt;/td&gt;
&lt;td&gt;移动到当前单词的结尾&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;man查阅命令手册的内容说明&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;手册中的标题&lt;/th&gt;
&lt;th&gt;功能说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAME&lt;/td&gt;
&lt;td&gt;命令的说明和介绍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SYNOPSIS&lt;/td&gt;
&lt;td&gt;使用该命令的基本语法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DESCRIPTION&lt;/td&gt;
&lt;td&gt;使用该命令的详细描述，各个参数的作用，有时候这些信息会出现在OPTIONS中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPTIONS&lt;/td&gt;
&lt;td&gt;命令相关参数选项的说明&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXAMPLES&lt;/td&gt;
&lt;td&gt;使用该命令的参考例子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIT STATUS&lt;/td&gt;
&lt;td&gt;命令结束的退出状态码，通常0表示成功执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEE ALSO&lt;/td&gt;
&lt;td&gt;和命令相关的其他命令或信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BUGS&lt;/td&gt;
&lt;td&gt;和命令相关的缺陷的描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTHOR&lt;/td&gt;
&lt;td&gt;该命令的作者介绍&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Day21-30 教程全文（10 篇合并）</title><link>https://www.klucen.cn/posts/python/day21-30-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day21-30-full/</guid><pubDate>Sun, 14 Dec 2025 00:11:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 10 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：文件、办公自动化与正则
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#21-%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99%E5%92%8C%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86&quot;&gt;21 文件读写和异常处理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#22-%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&quot;&gt;22 对象的序列化和反序列化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#23-python%E8%AF%BB%E5%86%99csv%E6%96%87%E4%BB%B6&quot;&gt;23 Python读写CSV文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#24-python%E8%AF%BB%E5%86%99excel%E6%96%87%E4%BB%B6-1&quot;&gt;24 Python读写Excel文件-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#25-python%E8%AF%BB%E5%86%99excel%E6%96%87%E4%BB%B6-2&quot;&gt;25 Python读写Excel文件-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#26-python%E6%93%8D%E4%BD%9Cword%E5%92%8Cpowerpoint%E6%96%87%E4%BB%B6&quot;&gt;26 Python操作Word和PowerPoint文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#27-python%E6%93%8D%E4%BD%9Cpdf%E6%96%87%E4%BB%B6&quot;&gt;27 Python操作PDF文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#28-python%E5%A4%84%E7%90%86%E5%9B%BE%E5%83%8F&quot;&gt;28 Python处理图像&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#29-python%E5%8F%91%E9%80%81%E9%82%AE%E4%BB%B6%E5%92%8C%E7%9F%AD%E4%BF%A1&quot;&gt;29 Python发送邮件和短信&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#30-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%BA%94%E7%94%A8&quot;&gt;30 正则表达式的应用&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;21 文件读写和异常处理&lt;/h2&gt;
&lt;p&gt;实际开发中常常会遇到对数据进行持久化的场景，所谓持久化是指将数据从无法长久保存数据的存储介质（通常是内存）转移到可以长久保存数据的存储介质（通常是硬盘）中。实现数据持久化最直接简单的方式就是通过&lt;strong&gt;文件系统&lt;/strong&gt;将数据保存到&lt;strong&gt;文件&lt;/strong&gt;中。&lt;/p&gt;
&lt;p&gt;计算机的&lt;strong&gt;文件系统&lt;/strong&gt;是一种存储和组织计算机数据的方法，它使得对数据的访问和查找变得容易，文件系统使用&lt;strong&gt;文件&lt;/strong&gt;和&lt;strong&gt;树形目录&lt;/strong&gt;的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念，用户使用文件系统来保存数据时，不必关心数据实际保存在硬盘的哪个数据块上，只需要记住这个文件的路径和文件名。在写入新数据之前，用户不必关心硬盘上的哪个数据块没有被使用，硬盘上的存储空间管理（分配和释放）功能由文件系统自动完成，用户只需要记住数据被写入到了哪个文件中。&lt;/p&gt;
&lt;h4&gt;打开和关闭文件&lt;/h4&gt;
&lt;p&gt;有了文件系统，我们可以非常方便的通过文件来读写数据；在 Python 中要实现文件操作是非常简单的。我们可以使用 Python 内置的&lt;code&gt;open&lt;/code&gt;函数来打开文件，在使用&lt;code&gt;open&lt;/code&gt;函数时，我们可以通过函数的参数指定&lt;strong&gt;文件名&lt;/strong&gt;、&lt;strong&gt;操作模式&lt;/strong&gt;和&lt;strong&gt;字符编码&lt;/strong&gt;等信息，接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件（字符文件或二进制文件）以及做什么样的操作（读、写或追加），具体如下表所示。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作模式&lt;/th&gt;
&lt;th&gt;具体含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;r&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读取 （默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;w&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写入（会先截断之前的内容）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;x&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写入，如果文件已经存在会产生异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;a&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;追加，将内容写入到已有文件的末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;b&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;二进制模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;t&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文本模式（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&apos;+&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;更新（既可以读又可以写）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;下图展示了如何根据程序的需要来设置&lt;code&gt;open&lt;/code&gt;函数的操作模式。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/9580980fe0.webp&quot; width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;在使用&lt;code&gt;open&lt;/code&gt;函数时，如果打开的文件是字符文件（文本文件），可以通过&lt;code&gt;encoding&lt;/code&gt;参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解，可以看看&lt;a href=&quot;https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html&quot;&gt;《字符集和字符编码》&lt;/a&gt;一文，此处不再进行赘述。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;open&lt;/code&gt;函数打开文件成功后会返回一个文件对象，通过这个对象，我们就可以实现对文件的读写操作；如果打开文件失败，&lt;code&gt;open&lt;/code&gt;函数会引发异常，稍后会对此加以说明。如果要关闭打开的文件，可以使用文件对象的&lt;code&gt;close&lt;/code&gt;方法，这样可以在结束文件操作时释放掉这个文件。&lt;/p&gt;
&lt;h4&gt;读写文本文件&lt;/h4&gt;
&lt;p&gt;用&lt;code&gt;open&lt;/code&gt;函数打开文本文件时，需要指定文件名并将文件的操作模式设置为&lt;code&gt;&apos;r&apos;&lt;/code&gt;，如果不指定，默认值也是&lt;code&gt;&apos;r&apos;&lt;/code&gt;；如果需要指定字符编码，可以传入&lt;code&gt;encoding&lt;/code&gt;参数，如果不指定，默认值是&lt;code&gt;None&lt;/code&gt;，那么在读取文件时使用的是操作系统默认的编码。需要提醒大家，如果不能保证保存文件时使用的编码方式与&lt;code&gt;encoding&lt;/code&gt;参数指定的编码方式是一致的，那么就可能因无法解码字符而导致读取文件失败。&lt;/p&gt;
&lt;p&gt;下面的例子演示了如何读取一个纯文本文件（一般指只有字符原生编码构成的文件，与富文本相比，纯文本不包含字符样式的控制元素，能够被最简单的文本编辑器直接读取）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file = open(&apos;致橡树.txt&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;)
print(file.read())
file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：《致橡树》是舒婷老师在 1977 年 3 月创作的爱情诗，也是我最喜欢的现代诗之一。内容如下：&lt;/p&gt;
&lt;p&gt;我如果爱你
绝不像攀援的凌霄花，
借你的高枝炫耀自己；
我如果爱你
绝不学痴情的鸟儿，
为绿荫重复单调的歌曲；
也不止像泉源，
常年送来清凉的慰藉；
也不止像险峰，
增加你的高度，衬托你的威仪。
甚至日光，
甚至春雨。&lt;/p&gt;
&lt;p&gt;不，这些都还不够！
我必须是你近旁的一株木棉，
作为树的形象和你站在一起。
根，紧握在地下；
叶，相触在云里。
每一阵风过，
我们都互相致意，
但没有人，
听懂我们的言语。
你有你的铜枝铁干，
像刀，像剑，也像戟；
我有我红硕的花朵，
像沉重的叹息，
又像英勇的火炬。&lt;/p&gt;
&lt;p&gt;我们分担寒潮、风雷、霹雳；
我们共享雾霭、流岚、虹霓。
仿佛永远分离，
却又终身相依。
这才是伟大的爱情，
坚贞就在这里：
爱
不仅爱你伟岸的身躯，
也爱你坚持的位置，
足下的土地。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;除了使用文件对象的&lt;code&gt;read&lt;/code&gt;方法读取文件之外，还可以使用&lt;code&gt;for-in&lt;/code&gt;循环逐行读取或者用&lt;code&gt;readlines&lt;/code&gt;方法将文件按行读取到一个列表容器中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file = open(&apos;致橡树.txt&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;)
for line in file:
    print(line, end=&apos;&apos;)
file.close()

file = open(&apos;致橡树.txt&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;)
lines = file.readlines()
for line in lines:
    print(line, end=&apos;&apos;)
file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要向文件中写入内容，可以在打开文件时使用&lt;code&gt;w&lt;/code&gt;或者&lt;code&gt;a&lt;/code&gt;作为操作模式，前者会截断之前的文本内容写入新的内容，后者是在原来内容的尾部追加新的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file = open(&apos;致橡树.txt&apos;, &apos;a&apos;, encoding=&apos;utf-8&apos;)
file.write(&apos;\n标题：《致橡树》&apos;)
file.write(&apos;\n作者：舒婷&apos;)
file.write(&apos;\n时间：1977年3月&apos;)
file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;异常处理机制&lt;/h4&gt;
&lt;p&gt;请注意上面的代码，如果&lt;code&gt;open&lt;/code&gt;函数指定的文件并不存在或者无法打开，那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性，我们可以&lt;strong&gt;使用 Python 的异常机制对可能在运行时发生状况的代码进行适当的处理&lt;/strong&gt;。Python 中和异常相关的关键字有五个，分别是&lt;code&gt;try&lt;/code&gt;、&lt;code&gt;except&lt;/code&gt;、&lt;code&gt;else&lt;/code&gt;、&lt;code&gt;finally&lt;/code&gt;和&lt;code&gt;raise&lt;/code&gt;，我们先看看下面的代码，再来为大家介绍这些关键字的用法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file = None
try:
    file = open(&apos;致橡树.txt&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;)
    print(file.read())
except FileNotFoundError:
    print(&apos;无法打开指定的文件!&apos;)
except LookupError:
    print(&apos;指定了未知的编码!&apos;)
except UnicodeDecodeError:
    print(&apos;读取文件时解码错误!&apos;)
finally:
    if file:
        file.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Python 中，我们可以将运行时会出现状况的代码放在&lt;code&gt;try&lt;/code&gt;代码块中，在&lt;code&gt;try&lt;/code&gt;后面可以跟上一个或多个&lt;code&gt;except&lt;/code&gt;块来捕获异常并进行相应的处理。例如，在上面的代码中，文件找不到会引发&lt;code&gt;FileNotFoundError&lt;/code&gt;，指定了未知的编码会引发&lt;code&gt;LookupError&lt;/code&gt;，而如果读取文件时无法按指定编码方式解码文件会引发&lt;code&gt;UnicodeDecodeError&lt;/code&gt;，所以我们在&lt;code&gt;try&lt;/code&gt;后面跟上了三个&lt;code&gt;except&lt;/code&gt;分别处理这三种不同的异常状况。在&lt;code&gt;except&lt;/code&gt;后面，我们还可以加上&lt;code&gt;else&lt;/code&gt;代码块，这是&lt;code&gt;try&lt;/code&gt; 中的代码没有出现异常时会执行的代码，而且&lt;code&gt;else&lt;/code&gt;中的代码不会再进行异常捕获，也就是说如果遇到异常状况，程序会因异常而终止并报告异常信息。最后我们使用&lt;code&gt;finally&lt;/code&gt;代码块来关闭打开的文件，释放掉程序中获取的外部资源。由于&lt;code&gt;finally&lt;/code&gt;块的代码不论程序正常还是异常都会执行，甚至是调用了&lt;code&gt;sys&lt;/code&gt;模块的&lt;code&gt;exit&lt;/code&gt;函数终止 Python 程序，&lt;code&gt;finally&lt;/code&gt;块中的代码仍然会被执行（因为&lt;code&gt;exit&lt;/code&gt;函数的本质是引发了&lt;code&gt;SystemExit&lt;/code&gt;异常），因此我们把&lt;code&gt;finally&lt;/code&gt;代码块称为“总是执行代码块”，它最适合用来做释放外部资源的操作。&lt;/p&gt;
&lt;p&gt;Python 中内置了大量的异常类型，除了上面代码中用到的异常类型以及之前的课程中遇到过的异常类型外，还有许多的异常类型，其继承结构如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的继承结构可以看出，Python 中所有的异常都是&lt;code&gt;BaseException&lt;/code&gt;的子类型，它有四个直接的子类，分别是：&lt;code&gt;SystemExit&lt;/code&gt;、&lt;code&gt;KeyboardInterrupt&lt;/code&gt;、&lt;code&gt;GeneratorExit&lt;/code&gt;和&lt;code&gt;Exception&lt;/code&gt;。其中，&lt;code&gt;SystemExit&lt;/code&gt;表示解释器请求退出，&lt;code&gt;KeyboardInterrupt&lt;/code&gt;是用户中断程序执行（按下&lt;code&gt;Ctrl+c&lt;/code&gt;），&lt;code&gt;GeneratorExit&lt;/code&gt;表示生成器发生异常通知退出，不理解这些异常没有关系，继续学习就好了。值得一提的是&lt;code&gt;Exception&lt;/code&gt;类，它是常规异常类型的父类型，很多的异常都是直接或间接的继承自&lt;code&gt;Exception&lt;/code&gt;类。如果 Python 内置的异常类型不能满足应用程序的需要，我们可以自定义异常类型，而自定义的异常类型也应该直接或间接继承自&lt;code&gt;Exception&lt;/code&gt;类，当然还可以根据需要重写或添加方法。&lt;/p&gt;
&lt;p&gt;在 Python 中，可以使用&lt;code&gt;raise&lt;/code&gt;关键字来引发异常（抛出异常对象），而调用者可以通过&lt;code&gt;try...except...&lt;/code&gt;结构来捕获并处理异常。例如在函数中，当函数的执行条件不满足时，可以使用抛出异常的方式来告知调用者问题的所在，而调用者可以通过捕获处理异常来使得代码从异常中恢复，定义异常和抛出异常的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class InputError(ValueError):
    &quot;&quot;&quot;自定义异常类型&quot;&quot;&quot;
    pass


def fac(num):
    &quot;&quot;&quot;求阶乘&quot;&quot;&quot;
    if num &amp;lt; 0:
        raise InputError(&apos;只能计算非负整数的阶乘&apos;)
    if num in (0, 1):
        return 1
    return num * fac(num - 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用求阶乘的函数&lt;code&gt;fac&lt;/code&gt;，通过&lt;code&gt;try...except...&lt;/code&gt;结构捕获输入错误的异常并打印异常对象（显示异常信息），如果输入正确就计算阶乘并结束程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flag = True
while flag:
    num = int(input(&apos;n = &apos;))
    try:
        print(f&apos;{num}! = {fac(num)}&apos;)
        flag = False
    except InputError as err:
        print(err)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;上下文管理器语法&lt;/h4&gt;
&lt;p&gt;对于&lt;code&gt;open&lt;/code&gt;函数返回的文件对象，还可以使用&lt;code&gt;with&lt;/code&gt;上下文管理器语法在文件操作完成后自动执行文件对象的&lt;code&gt;close&lt;/code&gt;方法，这样可以让代码变得更加简单优雅，因为不需要再写&lt;code&gt;finally&lt;/code&gt;代码块来执行关闭文件释放资源的操作。需要提醒大家的是，并不是所有的对象都可以放在&lt;code&gt;with&lt;/code&gt;上下文语法中，只有符合&lt;strong&gt;上下文管理器协议&lt;/strong&gt;（有&lt;code&gt;__enter__&lt;/code&gt;和&lt;code&gt;__exit__&lt;/code&gt;魔术方法）的对象才能使用这种语法，Python 标准库中的&lt;code&gt;contextlib&lt;/code&gt;模块也提供了对&lt;code&gt;with&lt;/code&gt;上下文语法的支持，后面用到的时候再为大家进行讲解。&lt;/p&gt;
&lt;p&gt;用&lt;code&gt;with&lt;/code&gt;上下文语法改写后的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    with open(&apos;致橡树.txt&apos;, &apos;r&apos;, encoding=&apos;utf-8&apos;) as file:
        print(file.read())
except FileNotFoundError:
    print(&apos;无法打开指定的文件!&apos;)
except LookupError:
    print(&apos;指定了未知的编码!&apos;)
except UnicodeDecodeError:
    print(&apos;读取文件时解码错误!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读写二进制文件&lt;/h4&gt;
&lt;p&gt;读写二进制文件跟读写文本文件的操作类似，但是需要注意，在使用&lt;code&gt;open&lt;/code&gt;函数打开文件时，如果要进行读操作，操作模式是&lt;code&gt;&apos;rb&apos;&lt;/code&gt;，如果要进行写操作，操作模式是&lt;code&gt;&apos;wb&apos;&lt;/code&gt;。还有一点，读写文本文件时，&lt;code&gt;read&lt;/code&gt;方法的返回值以及&lt;code&gt;write&lt;/code&gt;方法的参数是&lt;code&gt;str&lt;/code&gt;对象（字符串），而读写二进制文件时，&lt;code&gt;read&lt;/code&gt;方法的返回值以及&lt;code&gt;write&lt;/code&gt;方法的参数是&lt;code&gt;bytes-like&lt;/code&gt;对象（字节串）。下面的代码实现了将当前路径下名为&lt;code&gt;guido.jpg&lt;/code&gt;的图片文件复制到&lt;code&gt;吉多.jpg&lt;/code&gt;文件中的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    with open(&apos;guido.jpg&apos;, &apos;rb&apos;) as file1:
        data = file1.read()
    with open(&apos;吉多.jpg&apos;, &apos;wb&apos;) as file2:
        file2.write(data)
except FileNotFoundError:
    print(&apos;指定的文件无法打开.&apos;)
except IOError:
    print(&apos;读写文件时出现错误.&apos;)
print(&apos;程序执行结束.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要复制的图片文件很大，一次将文件内容直接读入内存中可能会造成非常大的内存开销，为了减少对内存的占用，可以为&lt;code&gt;read&lt;/code&gt;方法传入&lt;code&gt;size&lt;/code&gt;参数来指定每次读取的字节数，通过循环读取和写入的方式来完成上面的操作，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    with open(&apos;guido.jpg&apos;, &apos;rb&apos;) as file1, open(&apos;吉多.jpg&apos;, &apos;wb&apos;) as file2:
        data = file1.read(512)
        while data:
            file2.write(data)
            data = file1.read()
except FileNotFoundError:
    print(&apos;指定的文件无法打开.&apos;)
except IOError:
    print(&apos;读写文件时出现错误.&apos;)
print(&apos;程序执行结束.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;通过读写文件的操作，我们可以实现数据持久化。在 Python 中可以通过&lt;code&gt;open&lt;/code&gt;函数来获得文件对象，可以通过文件对象的&lt;code&gt;read&lt;/code&gt;和&lt;code&gt;write&lt;/code&gt;方法实现文件读写操作。程序在运行时可能遭遇无法预料的异常状况，可以使用 Python 的异常机制来处理这些状况。Python 的异常机制主要包括&lt;code&gt;try&lt;/code&gt;、&lt;code&gt;except&lt;/code&gt;、&lt;code&gt;else&lt;/code&gt;、&lt;code&gt;finally&lt;/code&gt;和&lt;code&gt;raise&lt;/code&gt;这五个核心关键字。&lt;code&gt;try&lt;/code&gt;后面的&lt;code&gt;except&lt;/code&gt;语句不是必须的，&lt;code&gt;finally&lt;/code&gt;语句也不是必须的，但是二者必须要有一个；&lt;code&gt;except&lt;/code&gt;语句可以有一个或多个，多个&lt;code&gt;except&lt;/code&gt;会按照书写的顺序依次匹配指定的异常，如果异常已经处理就不会再进入后续的&lt;code&gt;except&lt;/code&gt;语句；&lt;code&gt;except&lt;/code&gt;语句中还可以通过元组同时指定多个异常类型进行捕获；&lt;code&gt;except&lt;/code&gt;语句后面如果不指定异常类型，则默认捕获所有异常；捕获异常后可以使用&lt;code&gt;raise&lt;/code&gt;要再次抛出，但是不建议捕获并抛出同一个异常；不建议在不清楚逻辑的情况下捕获所有异常，这可能会掩盖程序中严重的问题。最后强调一点，&lt;strong&gt;不要使用异常机制来处理正常业务逻辑或控制程序流程&lt;/strong&gt;，简单的说就是不要滥用异常机制，这是初学者常犯的错误。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;22 对象的序列化和反序列化&lt;/h2&gt;
&lt;h4&gt;JSON 概述&lt;/h4&gt;
&lt;p&gt;通过上面的讲解，我们已经知道如何将文本数据和二进制数据保存到文件中，那么这里还有一个问题，如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢？在 Python 中，我们可以将程序中的数据以 JSON 格式进行保存。JSON 是“JavaScript Object Notation”的缩写，它本来是 JavaScript 语言中创建对象的一种字面量语法，现在已经被广泛的应用于跨语言跨平台的数据交换。使用 JSON 的原因非常简单，因为它结构紧凑而且是纯文本，任何操作系统和编程语言都能处理纯文本，这就是&lt;strong&gt;实现跨语言跨平台数据交换&lt;/strong&gt;的前提条件。目前 JSON 基本上已经取代了 XML（可扩展标记语言）作为&lt;strong&gt;异构系统间交换数据的事实标准&lt;/strong&gt;。可以在&lt;a href=&quot;https://www.json.org/json-zh.html&quot;&gt; JSON 的官方网站&lt;/a&gt;找到更多关于 JSON 的知识，这个网站还提供了每种语言处理 JSON 数据格式可以使用的工具或三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    name: &quot;骆昊&quot;,
    age: 40,
    friends: [&quot;王大锤&quot;, &quot;白元芳&quot;],
    cars: [
        {&quot;brand&quot;: &quot;BMW&quot;, &quot;max_speed&quot;: 240},
        {&quot;brand&quot;: &quot;Benz&quot;, &quot;max_speed&quot;: 280},
        {&quot;brand&quot;: &quot;Audi&quot;, &quot;max_speed&quot;: 280}
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是 JSON 的一个简单例子，大家可能已经注意到了，它跟 Python 中的字典非常类似而且支持嵌套结构，就好比 Python 字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台（对于 Chrome 浏览器，可以通过“更多工具”菜单找到“开发者工具”子菜单，就可以打开浏览器的控制台），浏览器的控制台提供了一个运行 JavaScript 代码的交互式环境（类似于 Python 的交互式环境），下面的代码会帮我们创建出一个 JavaScript 的对象，我们将其赋值给名为&lt;code&gt;obj&lt;/code&gt;的变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let obj = {
    name: &quot;骆昊&quot;,
    age: 40,
    friends: [&quot;王大锤&quot;, &quot;白元芳&quot;],
    cars: [
        {&quot;brand&quot;: &quot;BMW&quot;, &quot;max_speed&quot;: 240},
        {&quot;brand&quot;: &quot;Benz&quot;, &quot;max_speed&quot;: 280},
        {&quot;brand&quot;: &quot;Audi&quot;, &quot;max_speed&quot;: 280}
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/bb576372bf.webp&quot; alt=&quot;image-20210820143756353&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上面的&lt;code&gt;obj&lt;/code&gt;就是 JavaScript 中的一个对象，我们可以通过&lt;code&gt;obj.name&lt;/code&gt;或&lt;code&gt;obj[&quot;name&quot;]&lt;/code&gt;两种方式获取到&lt;code&gt;name&lt;/code&gt;对应的值，如下图所示。可以注意到，&lt;code&gt;obj[&quot;name&quot;]&lt;/code&gt;这种获取数据的方式跟 Python 字典通过键获取值的索引操作是完全一致的，而 Python 中也通过名为&lt;code&gt;json&lt;/code&gt;的模块提供了字典与 JSON 双向转换的支持。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/e8903f202a.webp&quot; width=&quot;85%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;我们在 JSON 中使用的数据类型（JavaScript 数据类型）和 Python 中的数据类型也是很容易找到对应关系的，大家可以看看下面的两张表。&lt;/p&gt;
&lt;p&gt;表1：JavaScript 数据类型（值）对应的 Python 数据类型（值）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JSON&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dict&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;array&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number &lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt; / &lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt; (real)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt; (&lt;code&gt;True&lt;/code&gt; / &lt;code&gt;False&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;表2：Python 数据类型（值）对应的JavaScript数据类型（值）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;th&gt;JSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;object&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt; / &lt;code&gt;tuple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;array&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt; / &lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt; （&lt;code&gt;True&lt;/code&gt; / &lt;code&gt;False&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;读写 JSON 格式的数据&lt;/h4&gt;
&lt;p&gt;在 Python 中，如果要将字典处理成 JSON 格式（以字符串形式存在），可以使用&lt;code&gt;json&lt;/code&gt;模块的&lt;code&gt;dumps&lt;/code&gt;函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

my_dict = {
    &apos;name&apos;: &apos;骆昊&apos;,
    &apos;age&apos;: 40,
    &apos;friends&apos;: [&apos;王大锤&apos;, &apos;白元芳&apos;],
    &apos;cars&apos;: [
        {&apos;brand&apos;: &apos;BMW&apos;, &apos;max_speed&apos;: 240},
        {&apos;brand&apos;: &apos;Audi&apos;, &apos;max_speed&apos;: 280},
        {&apos;brand&apos;: &apos;Benz&apos;, &apos;max_speed&apos;: 280}
    ]
}
print(json.dumps(my_dict))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码，输出如下所示，可以注意到中文字符都是用 Unicode 编码显示的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;name&quot;: &quot;\u9a86\u660a&quot;, &quot;age&quot;: 40, &quot;friends&quot;: [&quot;\u738b\u5927\u9524&quot;, &quot;\u767d\u5143\u82b3&quot;], &quot;cars&quot;: [{&quot;brand&quot;: &quot;BMW&quot;, &quot;max_speed&quot;: 240}, {&quot;brand&quot;: &quot;Audi&quot;, &quot;max_speed&quot;: 280}, {&quot;brand&quot;: &quot;Benz&quot;, &quot;max_speed&quot;: 280}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要将字典处理成 JSON 格式并写入文本文件，只需要将&lt;code&gt;dumps&lt;/code&gt;函数换成&lt;code&gt;dump&lt;/code&gt;函数并传入文件对象即可，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

my_dict = {
    &apos;name&apos;: &apos;骆昊&apos;,
    &apos;age&apos;: 40,
    &apos;friends&apos;: [&apos;王大锤&apos;, &apos;白元芳&apos;],
    &apos;cars&apos;: [
        {&apos;brand&apos;: &apos;BMW&apos;, &apos;max_speed&apos;: 240},
        {&apos;brand&apos;: &apos;Audi&apos;, &apos;max_speed&apos;: 280},
        {&apos;brand&apos;: &apos;Benz&apos;, &apos;max_speed&apos;: 280}
    ]
}
with open(&apos;data.json&apos;, &apos;w&apos;) as file:
    json.dump(my_dict, file)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码，会创建&lt;code&gt;data.json&lt;/code&gt;文件，文件的内容跟上面代码的输出是一样的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;json&lt;/code&gt;模块有四个比较重要的函数，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dump&lt;/code&gt; - 将 Python 对象按照 JSON 格式序列化到文件中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dumps&lt;/code&gt; - 将 Python 对象处理成 JSON 格式的字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;load&lt;/code&gt; - 将文件中的 JSON 数据反序列化成对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loads&lt;/code&gt; - 将字符串的内容反序列化成 Python 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里出现了两个概念，一个叫序列化，一个叫反序列化，&lt;a href=&quot;https://zh.wikipedia.org/&quot;&gt;维基百科&lt;/a&gt;上的解释是：“序列化（serialization）在计算机科学的数据处理中，是指将数据结构或对象状态转换为可以存储或传输的形式，这样在需要的时候能够恢复到原先的状态，而且通过序列化的数据重新获取字节时，可以利用这些字节来产生原始对象的副本（拷贝）。与这个过程相反的动作，即从一系列字节中提取数据结构的操作，就是反序列化（deserialization）”。&lt;/p&gt;
&lt;p&gt;我们可以通过下面的代码，读取上面创建的&lt;code&gt;data.json&lt;/code&gt;文件，将 JSON 格式的数据还原成 Python 中的字典。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json

with open(&apos;data.json&apos;, &apos;r&apos;) as file:
    my_dict = json.load(file)
    print(type(my_dict))
    print(my_dict)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;包管理工具 pip&lt;/h4&gt;
&lt;p&gt;Python 标准库中的&lt;code&gt;json&lt;/code&gt;模块在数据序列化和反序列化时性能并不是非常理想，为了解决这个问题，可以使用三方库&lt;code&gt;ujson&lt;/code&gt;来替换&lt;code&gt;json&lt;/code&gt;。所谓三方库，是指非公司内部开发和使用的，也不是来自于官方标准库的 Python 模块，这些模块通常由其他公司、组织或个人开发，所以被称为三方库。虽然 Python 语言的标准库虽然已经提供了诸多模块来方便我们的开发，但是对于一个强大的语言来说，它的生态圈一定也是非常繁荣的。&lt;/p&gt;
&lt;p&gt;之前安装 Python 解释器时，默认情况下已经勾选了安装 pip，大家可以在命令提示符或终端中通过&lt;code&gt;pip --version&lt;/code&gt;来确定是否已经拥有了 pip。Pip 是 Python 的包管理工具，通过 pip 可以查找、安装、卸载、更新 Python 的三方库或工具，例如要安装替代&lt;code&gt;json&lt;/code&gt;模块的&lt;code&gt;ujson&lt;/code&gt;，可以使用下面的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install ujson
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在默认情况下，pip 会访问&lt;code&gt;https://pypi.org/simple/&lt;/code&gt;来获得三方库相关的数据，但是国内访问这个网站的速度并不是十分理想，因此国内用户可以使用豆瓣网或清华大学提供的镜像来替代这个默认的下载源，再执行安装三方库的操作，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip config set global.index-url https://pypi.doubanio.com/simple
pip install ujson
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过&lt;code&gt;pip search&lt;/code&gt;命令根据名字查找需要的三方库，可以通过&lt;code&gt;pip list&lt;/code&gt;命令来查看已经安装过的三方库。如果想更新某个三方库，可以使用&lt;code&gt;pip install -U&lt;/code&gt;或&lt;code&gt;pip install --upgrade&lt;/code&gt;；如果要删除某个三方库，可以使用&lt;code&gt;pip uninstall&lt;/code&gt;命令。&lt;/p&gt;
&lt;p&gt;搜索&lt;code&gt;ujson&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip search ujson

micropython-cpython-ujson (0.2)  - MicroPython module ujson ported to CPython
pycopy-cpython-ujson (0.2)       - Pycopy module ujson ported to CPython
ujson (3.0.0)                    - Ultra fast JSON encoder and decoder for Python
ujson-bedframe (1.33.0)          - Ultra fast JSON encoder and decoder for Python
ujson-segfault (2.1.57)          - Ultra fast JSON encoder and decoder for Python. Continuing 
                                   development.
ujson-ia (2.1.1)                 - Ultra fast JSON encoder and decoder for Python (Internet 
                                   Archive fork)
ujson-x (1.37)                   - Ultra fast JSON encoder and decoder for Python
ujson-x-legacy (1.35.1)          - Ultra fast JSON encoder and decoder for Python
drf_ujson (1.2)                  - Django Rest Framework UJSON Renderer
drf-ujson2 (1.6.1)               - Django Rest Framework UJSON Renderer
ujsonDB (0.1.0)                  - A lightweight and simple database using ujson.
fast-json (0.3.2)                - Combines best parts of json and ujson for fast serialization
decimal-monkeypatch (0.4.3)      - Python 2 performance patches: decimal to cdecimal, json to 
                                   ujson for psycopg2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看已经安装的三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip list

Package                       Version
----------------------------- ----------
aiohttp                       3.5.4
alipay                        0.7.4
altgraph                      0.16.1
amqp                          2.4.2
...							  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新&lt;code&gt;ujson&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U ujson
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除&lt;code&gt;ujson&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip uninstall -y ujson
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果要更新&lt;code&gt;pip&lt;/code&gt;自身，对于 macOS 系统来说，可以使用命令&lt;code&gt;pip install -U pip&lt;/code&gt;。在 Windows 系统上，可以将命令替换为&lt;code&gt;python -m pip install -U --user pip&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;使用网络API获取数据&lt;/h4&gt;
&lt;p&gt;如果想在我们自己的程序中显示天气、路况、航班等信息，这些信息我们自己没有能力提供，所以必须使用网络数据服务。目前绝大多数的网络数据服务（或称之为网络 API）都是基于 &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE&quot;&gt;HTTP&lt;/a&gt; 或 HTTPS 提供 JSON 格式的数据，我们可以通过 Python 程序发送 HTTP 请求给指定的 URL（统一资源定位符），这个 URL 就是所谓的网络 API，如果请求成功，它会返回 HTTP 响应，而 HTTP 响应的消息体中就有我们需要的 JSON 格式的数据。关于 HTTP 的相关知识，可以看看阮一峰的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2016/08/http.html&quot;&gt;《HTTP协议入门》&lt;/a&gt;一文。&lt;/p&gt;
&lt;p&gt;国内有很多提供网络 API 接口的网站，例如&lt;a href=&quot;https://www.juhe.cn/&quot;&gt;聚合数据&lt;/a&gt;、&lt;a href=&quot;https://apis.baidu.com/&quot;&gt;百度智能云&lt;/a&gt;等，这些网站上有免费的和付费的数据接口，国外的&lt;a href=&quot;http://apis.io/&quot;&gt;{API}Search&lt;/a&gt;网站也提供了类似的功能，有兴趣的可以自行研究。下面的例子演示了如何使用&lt;a href=&quot;http://docs.python-requests.org/zh_CN/latest/&quot;&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/a&gt;库（基于 HTTP 进行网络资源访问的三方库）访问网络 API 获取国内新闻并显示新闻标题和链接。在这个例子中，我们使用了名为&lt;a href=&quot;https://www.tianapi.com/&quot;&gt;天聚数行&lt;/a&gt;的网站提供的国内新闻数据接口，其中的身份标识（API Key）需要自己到网站上注册申请。在天聚数行网站注册账号后会自动分配 API Key，但是要访问接口获取数据，需要绑定验证邮箱或手机，然后还要申请需要使用的接口，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/aa314f8767.webp&quot; alt=&quot;image-20210820151134034&quot; width=&quot;100%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;Python 通过 URL 接入网络，我们推荐大家使用&lt;code&gt;requests&lt;/code&gt;三方库，它简单且强大，但需要自行安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取国内新闻并显示新闻标题和链接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

resp = requests.get(&apos;http://api.tianapi.com/guonei/?key=APIKey&amp;amp;num=10&apos;)
if resp.status_code == 200:
    data_model = resp.json()
    for news in data_model[&apos;newslist&apos;]:
        print(news[&apos;title&apos;])
        print(news[&apos;url&apos;])
        print(&apos;-&apos; * 60)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码通过&lt;code&gt;requests&lt;/code&gt;模块的&lt;code&gt;get&lt;/code&gt;函数向天聚数行的国内新闻接口发起了一次请求，如果请求过程没有出现问题，&lt;code&gt;get&lt;/code&gt;函数会返回一个&lt;code&gt;Response&lt;/code&gt;对象，通过该对象的&lt;code&gt;status_code&lt;/code&gt;属性表示 HTTP 响应状态码，如果不理解没关系，你只需要关注它的值，如果值等于&lt;code&gt;200&lt;/code&gt;或者其他&lt;code&gt;2&lt;/code&gt;字头的值，那么我们的请求是成功的。通过&lt;code&gt;Response&lt;/code&gt;对象的&lt;code&gt;json()&lt;/code&gt;方法可以将返回的 JSON 格式的数据直接处理成 Python 字典，非常方便。天聚数行国内新闻接口返回的 JSON 格式的数据（部分）如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/9db207ce39.webp&quot; width=&quot;100%&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码中的&lt;code&gt;APIKey&lt;/code&gt;需要换成自己在天聚数行网站申请的API Key。天聚数行网站上还有提供了很多非常有意思的 API 接口，例如：垃圾分类、周公解梦等，大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档，文档中有关于如何使用接口的详细说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 中实现序列化和反序列化除了使用&lt;code&gt;json&lt;/code&gt;模块之外，还可以使用&lt;code&gt;pickle&lt;/code&gt;和&lt;code&gt;shelve&lt;/code&gt;模块，但是这两个模块是使用特有的序列化协议来序列化数据，因此序列化后的数据只能被 Python 识别，关于这两个模块的相关知识，有兴趣的读者可以自己查找网络上的资料。处理 JSON 格式的数据很显然是程序员必须掌握的一项技能，因为不管是访问网络 API 接口还是提供网络 API 接口给他人使用，都需要具备处理 JSON 格式数据的相关知识。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;23 Python读写CSV文件&lt;/h2&gt;
&lt;h4&gt;CSV 文件介绍&lt;/h4&gt;
&lt;p&gt;CSV（Comma Separated Values）全称逗号分隔值文件是一种简单、通用的文件格式，被广泛的应用于应用程序（数据库、电子表格等）数据的导入和导出以及异构系统之间的数据交换。因为 CSV 是纯文本文件，不管是什么操作系统和编程语言都是可以处理纯文本的，而且很多编程语言中都提供了对读写 CSV 文件的支持，因此 CSV 格式在数据处理和数据科学中被广泛应用。&lt;/p&gt;
&lt;p&gt;CSV 文件有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;纯文本，使用某种字符集（如 &lt;a href=&quot;https://zh.wikipedia.org/wiki/ASCII&quot;&gt;ASCII&lt;/a&gt;、&lt;a href=&quot;https://zh.wikipedia.org/wiki/Unicode&quot;&gt;Unicode&lt;/a&gt;、&lt;a href=&quot;https://zh.wikipedia.org/wiki/GB2312&quot;&gt;GB2312&lt;/a&gt;等）；&lt;/li&gt;
&lt;li&gt;由一条条的记录组成（典型的是每行一条记录）；&lt;/li&gt;
&lt;li&gt;每条记录被分隔符（如逗号、分号、制表符等）分隔为字段（列）；&lt;/li&gt;
&lt;li&gt;每条记录都有同样的字段序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CSV 文件可以使用文本编辑器或类似于 Excel 电子表格这类工具打开和编辑，当使用 Excel 这类电子表格打开 CSV 文件时，你甚至感觉不到 CSV 和 Excel 文件的区别。很多数据库系统都支持将数据导出到 CSV 文件中，当然也支持从 CSV 文件中读入数据保存到数据库中，这些内容并不是现在要讨论的重点。&lt;/p&gt;
&lt;h4&gt;将数据写入 CSV 文件&lt;/h4&gt;
&lt;p&gt;现有五个学生三门课程的考试成绩需要保存到一个 CSV 文件中，要达成这个目标，可以使用 Python 标准库中的&lt;code&gt;csv&lt;/code&gt;模块，该模块的&lt;code&gt;writer&lt;/code&gt;函数会返回一个&lt;code&gt;csvwriter&lt;/code&gt;对象，通过该对象的&lt;code&gt;writerow&lt;/code&gt;或&lt;code&gt;writerows&lt;/code&gt;方法就可以将数据写入到 CSV 文件中，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import csv
import random

with open(&apos;scores.csv&apos;, &apos;w&apos;) as file:
    writer = csv.writer(file)
    writer.writerow([&apos;姓名&apos;, &apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;])
    names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
    for name in names:
        scores = [random.randrange(50, 101) for _ in range(3)]
        scores.insert(0, name)
        writer.writerow(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 CSV 文件的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;姓名,语文,数学,英语
关羽,98,86,61
张飞,86,58,80
赵云,95,73,70
马超,83,97,55
黄忠,61,54,87
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是上面的&lt;code&gt;writer&lt;/code&gt;函数，除了传入要写入数据的文件对象外，还可以&lt;code&gt;dialect&lt;/code&gt;参数，它表示 CSV 文件的方言，默认值是&lt;code&gt;excel&lt;/code&gt;。除此之外，还可以通过&lt;code&gt;delimiter&lt;/code&gt;、&lt;code&gt;quotechar&lt;/code&gt;、&lt;code&gt;quoting&lt;/code&gt;参数来指定分隔符（默认是逗号）、包围值的字符（默认是双引号）以及包围的方式。其中，包围值的字符主要用于当字段中有特殊符号时，通过添加包围值的字符可以避免二义性。大家可以尝试将上面第5行代码修改为下面的代码，然后查看生成的 CSV 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;writer = csv.writer(file, delimiter=&apos;|&apos;, quoting=csv.QUOTE_ALL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 CSV 文件的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;姓名&quot;|&quot;语文&quot;|&quot;数学&quot;|&quot;英语&quot;
&quot;关羽&quot;|&quot;88&quot;|&quot;64&quot;|&quot;65&quot;
&quot;张飞&quot;|&quot;76&quot;|&quot;93&quot;|&quot;79&quot;
&quot;赵云&quot;|&quot;78&quot;|&quot;55&quot;|&quot;76&quot;
&quot;马超&quot;|&quot;72&quot;|&quot;77&quot;|&quot;68&quot;
&quot;黄忠&quot;|&quot;70&quot;|&quot;72&quot;|&quot;51&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;从 CSV 文件读取数据&lt;/h4&gt;
&lt;p&gt;如果要读取刚才创建的 CSV 文件，可以使用下面的代码，通过&lt;code&gt;csv&lt;/code&gt;模块的&lt;code&gt;reader&lt;/code&gt;函数可以创建出&lt;code&gt;csvreader&lt;/code&gt;对象，该对象是一个迭代器，可以通过&lt;code&gt;next&lt;/code&gt;函数或&lt;code&gt;for-in&lt;/code&gt;循环读取到文件中的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import csv

with open(&apos;scores.csv&apos;, &apos;r&apos;) as file:
    reader = csv.reader(file, delimiter=&apos;|&apos;)
    for data_list in reader:
        print(reader.line_num, end=&apos;\t&apos;)
        for elem in data_list:
            print(elem, end=&apos;\t&apos;)
        print()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面的代码对&lt;code&gt;csvreader&lt;/code&gt;对象做&lt;code&gt;for&lt;/code&gt;循环时，每次会取出一个列表对象，该列表对象包含了一行中所有的字段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;将来如果大家使用Python做数据分析，很有可能会用到名为&lt;code&gt;pandas&lt;/code&gt;的三方库，它是Python数据分析的神器之一。&lt;code&gt;pandas&lt;/code&gt;中封装了名为&lt;code&gt;read_csv&lt;/code&gt;和&lt;code&gt;to_csv&lt;/code&gt;的函数用来读写 CSV 文件，其中&lt;code&gt;read_csv&lt;/code&gt;会将读取到的数据变成一个&lt;code&gt;DataFrame&lt;/code&gt;对象，而&lt;code&gt;DataFrame&lt;/code&gt;就是&lt;code&gt;pandas&lt;/code&gt;库中最重要的类型，它封装了一系列用于数据处理的方法（清洗、转换、聚合等）；而&lt;code&gt;to_csv&lt;/code&gt;会将&lt;code&gt;DataFrame&lt;/code&gt;对象中的数据写入 CSV 文件，完成数据的持久化。&lt;code&gt;read_csv&lt;/code&gt;函数和&lt;code&gt;to_csv&lt;/code&gt;函数远远比原生的&lt;code&gt;csvreader&lt;/code&gt;和&lt;code&gt;csvwriter&lt;/code&gt;强大。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;24 Python读写Excel文件-1&lt;/h2&gt;
&lt;h4&gt;Excel简介&lt;/h4&gt;
&lt;p&gt;Excel 是 Microsoft（微软）为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具，再加上成功的市场营销，一直以来都是最为流行的个人计算机数据处理软件。当然，Excel 也有很多竞品，例如 Google Sheets、LibreOffice Calc、Numbers 等，这些竞品基本上也能够兼容 Excel，至少能够读写较新版本的 Excel 文件，当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件，可以让日常办公自动化的工作更加轻松愉快，而且在很多商业项目中，导入导出 Excel 文件都是特别常见的功能。&lt;/p&gt;
&lt;p&gt;Python 操作 Excel 需要三方库的支持，如果要兼容 Excel 2007 以前的版本，也就是&lt;code&gt;xls&lt;/code&gt;格式的 Excel 文件，可以使用三方库&lt;code&gt;xlrd&lt;/code&gt;和&lt;code&gt;xlwt&lt;/code&gt;，前者用于读 Excel 文件，后者用于写 Excel 文件。如果使用较新版本的 Excel，即&lt;code&gt;xlsx&lt;/code&gt;格式的 Excel 文件，可以使用&lt;code&gt;openpyxl&lt;/code&gt;库，当然这个库不仅仅可以操作Excel，还可以操作其他基于 Office Open XML 的电子表格文件。&lt;/p&gt;
&lt;p&gt;本章我们先讲解基于&lt;code&gt;xlwt&lt;/code&gt;和&lt;code&gt;xlrd&lt;/code&gt;操作 Excel 文件，大家可以先使用下面的命令安装这两个三方库以及配合使用的工具模块&lt;code&gt;xlutils&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install xlwt xlrd xlutils
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读Excel文件&lt;/h4&gt;
&lt;p&gt;例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xls”的 Excel 文件，如果想读取并显示该文件的内容，可以通过如下所示的代码来完成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import xlrd

# 使用xlrd模块的open_workbook函数打开指定Excel文件并获得Book对象（工作簿）
wb = xlrd.open_workbook(&apos;阿里巴巴2020年股票数据.xls&apos;)
# 通过Book对象的sheet_names方法可以获取所有表单名称
sheetnames = wb.sheet_names()
print(sheetnames)
# 通过指定的表单名称获取Sheet对象（工作表）
sheet = wb.sheet_by_name(sheetnames[0])
# 通过Sheet对象的nrows和ncols属性获取表单的行数和列数
print(sheet.nrows, sheet.ncols)
for row in range(sheet.nrows):
    for col in range(sheet.ncols):
        # 通过Sheet对象的cell方法获取指定Cell对象（单元格）
        # 通过Cell对象的value属性获取单元格中的值
        value = sheet.cell(row, col).value
        # 对除首行外的其他行进行数据格式化处理
        if row &amp;gt; 0:
            # 第1列的xldate类型先转成元组再格式化为“年月日”的格式
            if col == 0:
                # xldate_as_tuple函数的第二个参数只有0和1两个取值
                # 其中0代表以1900-01-01为基准的日期，1代表以1904-01-01为基准的日期
                value = xlrd.xldate_as_tuple(value, 0)
                value = f&apos;{value[0]}年{value[1]:&amp;gt;02d}月{value[2]:&amp;gt;02d}日&apos;
            # 其他列的number类型处理成小数点后保留两位有效数字的浮点数
            else:
                value = f&apos;{value:.2f}&apos;
        print(value, end=&apos;\t&apos;)
    print()
# 获取最后一个单元格的数据类型
# 0 - 空值，1 - 字符串，2 - 数字，3 - 日期，4 - 布尔，5 - 错误
last_cell_type = sheet.cell_type(sheet.nrows - 1, sheet.ncols - 1)
print(last_cell_type)
# 获取第一行的值（列表）
print(sheet.row_values(0))
# 获取指定行指定列范围的数据（列表）
# 第一个参数代表行索引，第二个和第三个参数代表列的开始（含）和结束（不含）索引
print(sheet.row_slice(3, 0, 5))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xls”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相信通过上面的代码，大家已经了解到了如何读取一个 Excel 文件，如果想知道更多关于&lt;code&gt;xlrd&lt;/code&gt;模块的知识，可以阅读它的&lt;a href=&quot;https://xlrd.readthedocs.io/en/latest/&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;写Excel文件&lt;/h4&gt;
&lt;p&gt;写入 Excel 文件可以通过&lt;code&gt;xlwt&lt;/code&gt; 模块的&lt;code&gt;Workbook&lt;/code&gt;类创建工作簿对象，通过工作簿对象的&lt;code&gt;add_sheet&lt;/code&gt;方法可以添加工作表，通过工作表对象的&lt;code&gt;write&lt;/code&gt;方法可以向指定单元格中写入数据，最后通过工作簿对象的&lt;code&gt;save&lt;/code&gt;方法将工作簿写入到指定的文件或内存中。下面的代码实现了将5 个学生 3 门课程的考试成绩写入 Excel 文件的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

import xlwt

student_names = [&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;]
scores = [[random.randrange(50, 101) for _ in range(3)] for _ in range(5)]
# 创建工作簿对象（Workbook）
wb = xlwt.Workbook()
# 创建工作表对象（Worksheet）
sheet = wb.add_sheet(&apos;一年级二班&apos;)
# 添加表头数据
titles = (&apos;姓名&apos;, &apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;)
for index, title in enumerate(titles):
    sheet.write(0, index, title)
# 将学生姓名和考试成绩写入单元格
for row in range(len(scores)):
    sheet.write(row + 1, 0, student_names[row])
    for col in range(len(scores[row])):
        sheet.write(row + 1, col + 1, scores[row][col])
# 保存Excel工作簿
wb.save(&apos;考试成绩表.xls&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;调整单元格样式&lt;/h4&gt;
&lt;p&gt;在写Excel文件时，我们还可以为单元格设置样式，主要包括字体（Font）、对齐方式（Alignment）、边框（Border）和背景（Background）的设置，&lt;code&gt;xlwt&lt;/code&gt;对这几项设置都封装了对应的类来支持。要设置单元格样式需要首先创建一个&lt;code&gt;XFStyle&lt;/code&gt;对象，再通过该对象的属性对字体、对齐方式、边框等进行设定，例如在上面的例子中，如果希望将表头单元格的背景色修改为黄色，可以按照如下的方式进行操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;header_style = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
# 0 - 黑色、1 - 白色、2 - 红色、3 - 绿色、4 - 蓝色、5 - 黄色、6 - 粉色、7 - 青色
pattern.pattern_fore_colour = 5
header_style.pattern = pattern
titles = (&apos;姓名&apos;, &apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;)
for index, title in enumerate(titles):
    sheet.write(0, index, title, header_style)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望为表头设置指定的字体，可以使用&lt;code&gt;Font&lt;/code&gt;类并添加如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;font = xlwt.Font()
# 字体名称
font.name = &apos;华文楷体&apos;
# 字体大小（20是基准单位，18表示18px）
font.height = 20 * 18
# 是否使用粗体
font.bold = True
# 是否使用斜体
font.italic = False
# 字体颜色
font.colour_index = 1
header_style.font = font
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面代码中指定的字体名（&lt;code&gt;font.name&lt;/code&gt;）应当是本地系统有的字体，例如在我的电脑上有名为“华文楷体”的字体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果希望表头垂直居中对齐，可以使用下面的代码进行设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;align = xlwt.Alignment()
# 垂直方向的对齐方式
align.vert = xlwt.Alignment.VERT_CENTER
# 水平方向的对齐方式
align.horz = xlwt.Alignment.HORZ_CENTER
header_style.alignment = align
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望给表头加上黄色的虚线边框，可以使用下面的代码来设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;borders = xlwt.Borders()
props = (
    (&apos;top&apos;, &apos;top_colour&apos;), (&apos;right&apos;, &apos;right_colour&apos;),
    (&apos;bottom&apos;, &apos;bottom_colour&apos;), (&apos;left&apos;, &apos;left_colour&apos;)
)
# 通过循环对四个方向的边框样式及颜色进行设定
for position, color in props:
    # 使用setattr内置函数动态给对象指定的属性赋值
    setattr(borders, position, xlwt.Borders.DASHED)
    setattr(borders, color, 5)
header_style.borders = borders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要调整单元格的宽度（列宽）和表头的高度（行高），可以按照下面的代码进行操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置行高为40px
sheet.row(0).set_style(xlwt.easyxf(f&apos;font:height {20 * 40}&apos;))
titles = (&apos;姓名&apos;, &apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;)
for index, title in enumerate(titles):
    # 设置列宽为200px
    sheet.col(index).width = 20 * 200
    # 设置单元格的数据和样式
    sheet.write(0, index, title, header_style)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;公式计算&lt;/h4&gt;
&lt;p&gt;对于前面打开的“阿里巴巴2020年股票数据.xls”文件，如果要统计全年收盘价（Close字段）的平均值以及全年交易量（Volume字段）的总和，可以使用Excel的公式计算即可。我们可以先使用&lt;code&gt;xlrd&lt;/code&gt;读取Excel文件夹，然后通过&lt;code&gt;xlutils&lt;/code&gt;三方库提供的&lt;code&gt;copy&lt;/code&gt;函数将读取到的Excel文件转成&lt;code&gt;Workbook&lt;/code&gt;对象进行写操作，在调用&lt;code&gt;write&lt;/code&gt;方法时，可以将一个&lt;code&gt;Formula&lt;/code&gt;对象写入单元格。&lt;/p&gt;
&lt;p&gt;实现公式计算的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import xlrd
import xlwt
from xlutils.copy import copy

wb_for_read = xlrd.open_workbook(&apos;阿里巴巴2020年股票数据.xls&apos;)
sheet1 = wb_for_read.sheet_by_index(0)
nrows, ncols = sheet1.nrows, sheet1.ncols
wb_for_write = copy(wb_for_read)
sheet2 = wb_for_write.get_sheet(0)
sheet2.write(nrows, 4, xlwt.Formula(f&apos;average(E2:E{nrows})&apos;))
sheet2.write(nrows, 6, xlwt.Formula(f&apos;sum(G2:G{nrows})&apos;))
wb_for_write.save(&apos;阿里巴巴2020年股票数据汇总.xls&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码有一些小瑕疵，有兴趣的读者可以自行探索并思考如何解决。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;掌握了 Python 程序操作 Excel 的方法，可以解决日常办公中很多繁琐的处理 Excel 电子表格工作，最常见就是将多个数据格式相同的 Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。当然，如果要对表格数据进行处理，使用 Python 数据分析神器之一的 pandas 库可能更为方便。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;25 Python读写Excel文件-2&lt;/h2&gt;
&lt;h4&gt;Excel简介&lt;/h4&gt;
&lt;p&gt;Excel 是 Microsoft（微软）为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具，再加上成功的市场营销，一直以来都是最为流行的个人计算机数据处理软件。当然，Excel 也有很多竞品，例如 Google Sheets、LibreOffice Calc、Numbers 等，这些竞品基本上也能够兼容 Excel，至少能够读写较新版本的 Excel 文件，当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件，可以让日常办公自动化的工作更加轻松愉快，而且在很多商业项目中，导入导出 Excel 文件都是特别常见的功能。&lt;/p&gt;
&lt;p&gt;本章我们继续讲解基于另一个三方库&lt;code&gt;openpyxl&lt;/code&gt;如何进行 Excel 文件操作，首先需要先安装它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install openpyxl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;openpyxl&lt;/code&gt;的优点在于，当我们打开一个 Excel 文件后，既可以对它进行读操作，又可以对它进行写操作，而且在操作的便捷性上是优于&lt;code&gt;xlwt&lt;/code&gt;和&lt;code&gt;xlrd&lt;/code&gt;的。此外，如果要进行样式编辑和公式计算，使用&lt;code&gt;openpyxl&lt;/code&gt;也远比上一个章节我们讲解的方式更为简单，而且&lt;code&gt;openpyxl&lt;/code&gt;还支持数据透视和插入图表等操作，功能非常强大。有一点需要再次强调，&lt;code&gt;openpyxl&lt;/code&gt;并不支持操作 Office 2007 以前版本的 Excel 文件。&lt;/p&gt;
&lt;h4&gt;读取Excel文件&lt;/h4&gt;
&lt;p&gt;例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xlsx”的 Excel 文件，如果想读取并显示该文件的内容，可以通过如下所示的代码来完成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import datetime

import openpyxl

# 加载一个工作簿 ---&amp;gt; Workbook
wb = openpyxl.load_workbook(&apos;阿里巴巴2020年股票数据.xlsx&apos;)
# 获取工作表的名字
print(wb.sheetnames)
# 获取工作表 ---&amp;gt; Worksheet
sheet = wb.worksheets[0]
# 获得单元格的范围
print(sheet.dimensions)
# 获得行数和列数
print(sheet.max_row, sheet.max_column)

# 获取指定单元格的值
print(sheet.cell(3, 3).value)
print(sheet[&apos;C3&apos;].value)
print(sheet[&apos;G255&apos;].value)

# 获取多个单元格（嵌套元组）
print(sheet[&apos;A2:C5&apos;])

# 读取所有单元格的数据
for row_ch in range(2, sheet.max_row + 1):
    for col_ch in &apos;ABCDEFG&apos;:
        value = sheet[f&apos;{col_ch}{row_ch}&apos;].value
        if type(value) == datetime.datetime:
            print(value.strftime(&apos;%Y年%m月%d日&apos;), end=&apos;\t&apos;)
        elif type(value) == int:
            print(f&apos;{value:&amp;lt;10d}&apos;, end=&apos;\t&apos;)
        elif type(value) == float:
            print(f&apos;{value:.4f}&apos;, end=&apos;\t&apos;)
        else:
            print(value, end=&apos;\t&apos;)
    print()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xlsx”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要提醒大家一点，&lt;code&gt;openpyxl&lt;/code&gt;获取指定的单元格有两种方式，一种是通过&lt;code&gt;cell&lt;/code&gt;方法，需要注意，该方法的行索引和列索引都是从&lt;code&gt;1&lt;/code&gt;开始的，这是为了照顾用惯了 Excel 的人的习惯；另一种是通过索引运算，通过指定单元格的坐标，例如&lt;code&gt;C3&lt;/code&gt;、&lt;code&gt;G255&lt;/code&gt;，也可以取得对应的单元格，再通过单元格对象的&lt;code&gt;value&lt;/code&gt;属性，就可以获取到单元格的值。通过上面的代码，相信大家还注意到了，可以通过类似&lt;code&gt;sheet[&apos;A2:C5&apos;]&lt;/code&gt;或&lt;code&gt;sheet[&apos;A2&apos;:&apos;C5&apos;]&lt;/code&gt;这样的切片操作获取多个单元格，该操作将返回嵌套的元组，相当于获取到了多行多列。&lt;/p&gt;
&lt;h4&gt;写Excel文件&lt;/h4&gt;
&lt;p&gt;下面我们使用&lt;code&gt;openpyxl&lt;/code&gt;来进行写 Excel 操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

import openpyxl

# 第一步：创建工作簿（Workbook）
wb = openpyxl.Workbook()

# 第二步：添加工作表（Worksheet）
sheet = wb.active
sheet.title = &apos;期末成绩&apos;

titles = (&apos;姓名&apos;, &apos;语文&apos;, &apos;数学&apos;, &apos;英语&apos;)
for col_index, title in enumerate(titles):
    sheet.cell(1, col_index + 1, title)

names = (&apos;关羽&apos;, &apos;张飞&apos;, &apos;赵云&apos;, &apos;马超&apos;, &apos;黄忠&apos;)
for row_index, name in enumerate(names):
    sheet.cell(row_index + 2, 1, name)
    for col_index in range(2, 5):
        sheet.cell(row_index + 2, col_index, random.randrange(50, 101))

# 第四步：保存工作簿
wb.save(&apos;考试成绩表.xlsx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;调整样式和公式计算&lt;/h4&gt;
&lt;p&gt;在使用&lt;code&gt;openpyxl&lt;/code&gt;操作 Excel 时，如果要调整单元格的样式，可以直接通过单元格对象（&lt;code&gt;Cell&lt;/code&gt;对象）的属性进行操作。单元格对象的属性包括字体（&lt;code&gt;font&lt;/code&gt;）、对齐（&lt;code&gt;alignment&lt;/code&gt;）、边框（&lt;code&gt;border&lt;/code&gt;）等，具体的可以参考&lt;code&gt;openpyxl&lt;/code&gt;的&lt;a href=&quot;https://openpyxl.readthedocs.io/en/stable/index.html&quot;&gt;官方文档&lt;/a&gt;。在使用&lt;code&gt;openpyxl&lt;/code&gt;时，如果需要做公式计算，可以完全按照 Excel 中的操作方式来进行，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side

# 对齐方式
alignment = Alignment(horizontal=&apos;center&apos;, vertical=&apos;center&apos;)
# 边框线条
side = Side(color=&apos;ff7f50&apos;, style=&apos;mediumDashed&apos;)

wb = openpyxl.load_workbook(&apos;考试成绩表.xlsx&apos;)
sheet = wb.worksheets[0]

# 调整行高和列宽
sheet.row_dimensions[1].height = 30
sheet.column_dimensions[&apos;E&apos;].width = 120

sheet[&apos;E1&apos;] = &apos;平均分&apos;
# 设置字体
sheet.cell(1, 5).font = Font(size=18, bold=True, color=&apos;ff1493&apos;, name=&apos;华文楷体&apos;)
# 设置对齐方式
sheet.cell(1, 5).alignment = alignment
# 设置单元格边框
sheet.cell(1, 5).border = Border(left=side, top=side, right=side, bottom=side)
for i in range(2, 7):
    # 公式计算每个学生的平均分
    sheet[f&apos;E{i}&apos;] = f&apos;=average(B{i}:D{i})&apos;
    sheet.cell(i, 5).font = Font(size=12, color=&apos;4169e1&apos;, italic=True)
    sheet.cell(i, 5).alignment = alignment

wb.save(&apos;考试成绩表.xlsx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;生成统计图表&lt;/h4&gt;
&lt;p&gt;通过&lt;code&gt;openpyxl&lt;/code&gt;库，可以直接向 Excel 中插入统计图表，具体的做法跟在 Excel 中插入图表大体一致。我们可以创建指定类型的图表对象，然后通过该对象的属性对图表进行设置。当然，最为重要的是为图表绑定数据，即横轴代表什么，纵轴代表什么，具体的数值是多少。最后，可以将图表对象添加到表单中，具体的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference

wb = Workbook(write_only=True)
sheet = wb.create_sheet()

rows = [
    (&apos;类别&apos;, &apos;销售A组&apos;, &apos;销售B组&apos;),
    (&apos;手机&apos;, 40, 30),
    (&apos;平板&apos;, 50, 60),
    (&apos;笔记本&apos;, 80, 70),
    (&apos;外围设备&apos;, 20, 10),
]

# 向表单中添加行
for row in rows:
    sheet.append(row)

# 创建图表对象
chart = BarChart()
chart.type = &apos;col&apos;
chart.style = 10
# 设置图表的标题
chart.title = &apos;销售统计图&apos;
# 设置图表纵轴的标题
chart.y_axis.title = &apos;销量&apos;
# 设置图表横轴的标题
chart.x_axis.title = &apos;商品类别&apos;
# 设置数据的范围
data = Reference(sheet, min_col=2, min_row=1, max_row=5, max_col=3)
# 设置分类的范围
cats = Reference(sheet, min_col=1, min_row=2, max_row=5)
# 给图表添加数据
chart.add_data(data, titles_from_data=True)
# 给图表设置分类
chart.set_categories(cats)
chart.shape = 4
# 将图表添加到表单指定的单元格中
sheet.add_chart(chart, &apos;A10&apos;)

wb.save(&apos;demo.xlsx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码，打开生成的 Excel 文件，效果如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/515ea5bad2.webp&quot;  width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;掌握了 Python 程序操作 Excel 的方法，可以解决日常办公中很多繁琐的处理 Excel 电子表格工作，最常见就是将多个数据格式相同的Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。如果数据体量较大或者处理数据的方式比较复杂，我们还是推荐大家使用 Python 数据分析神器之一的 pandas 库。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;26 Python操作Word和PowerPoint文件&lt;/h2&gt;
&lt;p&gt;在日常工作中，有很多简单重复的劳动其实完全可以交给 Python 程序，比如根据样板文件（模板文件）批量的生成很多个 Word 文件或 PowerPoint 文件。Word 是微软公司开发的文字处理程序，相信大家都不陌生，日常办公中很多正式的文档都是用 Word 进行撰写和编辑的，目前使用的 Word 文件后缀名一般为&lt;code&gt;.docx&lt;/code&gt;。PowerPoint 是微软公司开发的演示文稿程序，是微软的 Office 系列软件中的一员，被商业人士、教师、学生等群体广泛使用，通常也将其称之为“幻灯片”。在 Python 中，可以使用名为&lt;code&gt;python-docx&lt;/code&gt; 的三方库来操作 Word，可以使用名为&lt;code&gt;python-pptx&lt;/code&gt;的三方库来生成 PowerPoint。&lt;/p&gt;
&lt;h4&gt;操作Word文档&lt;/h4&gt;
&lt;p&gt;我们可以先通过下面的命令来安装&lt;code&gt;python-docx&lt;/code&gt;三方库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install python-docx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照&lt;a href=&quot;https://python-docx.readthedocs.io/en/latest/&quot;&gt;官方文档&lt;/a&gt;的介绍，我们可以使用如下所示的代码来生成一个简单的 Word 文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from docx import Document
from docx.shared import Cm, Pt

from docx.document import Document as Doc

# 创建代表Word文档的Doc对象
document = Document()  # type: Doc
# 添加大标题
document.add_heading(&apos;快快乐乐学Python&apos;, 0)
# 添加段落
p = document.add_paragraph(&apos;Python是一门非常流行的编程语言，它&apos;)
run = p.add_run(&apos;简单&apos;)
run.bold = True
run.font.size = Pt(18)
p.add_run(&apos;而且&apos;)
run = p.add_run(&apos;优雅&apos;)
run.font.size = Pt(18)
run.underline = True
p.add_run(&apos;。&apos;)

# 添加一级标题
document.add_heading(&apos;Heading, level 1&apos;, level=1)
# 添加带样式的段落
document.add_paragraph(&apos;Intense quote&apos;, style=&apos;Intense Quote&apos;)
# 添加无序列表
document.add_paragraph(
    &apos;first item in unordered list&apos;, style=&apos;List Bullet&apos;
)
document.add_paragraph(
    &apos;second item in ordered list&apos;, style=&apos;List Bullet&apos;
)
# 添加有序列表
document.add_paragraph(
    &apos;first item in ordered list&apos;, style=&apos;List Number&apos;
)
document.add_paragraph(
    &apos;second item in ordered list&apos;, style=&apos;List Number&apos;
)

# 添加图片（注意路径和图片必须要存在）
document.add_picture(&apos;resources/guido.jpg&apos;, width=Cm(5.2))

# 添加分节符
document.add_section()

records = (
    (&apos;骆昊&apos;, &apos;男&apos;, &apos;1995-5-5&apos;),
    (&apos;孙美丽&apos;, &apos;女&apos;, &apos;1992-2-2&apos;)
)
# 添加表格
table = document.add_table(rows=1, cols=3)
table.style = &apos;Dark List&apos;
hdr_cells = table.rows[0].cells
hdr_cells[0].text = &apos;姓名&apos;
hdr_cells[1].text = &apos;性别&apos;
hdr_cells[2].text = &apos;出生日期&apos;
# 为表格添加行
for name, sex, birthday in records:
    row_cells = table.add_row().cells
    row_cells[0].text = name
    row_cells[1].text = sex
    row_cells[2].text = birthday

# 添加分页符
document.add_page_break()

# 保存文档
document.save(&apos;demo.docx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码第7行中的注释&lt;code&gt;# type: Doc&lt;/code&gt;是为了在PyCharm中获得代码补全提示，因为如果不清楚对象具体的数据类型，PyCharm 无法在后续代码中给出&lt;code&gt;Doc&lt;/code&gt;对象的代码补全提示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;执行上面的代码，打开生成的 Word 文档，效果如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/4ad41ea575.webp&quot; width=&quot;40%&quot;&amp;gt;  &amp;lt;img src=&quot;/images/python/day21-30/3c3b9c887b.webp&quot; width=&quot;40%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;对于一个已经存在的 Word 文件，我们可以通过下面的代码去遍历它所有的段落并获取对应的内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from docx import Document
from docx.document import Document as Doc

doc = Document(&apos;resources/离职证明.docx&apos;)  # type: Doc
for no, p in enumerate(doc.paragraphs):
    print(no, p.text)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果需要上面代码中的 Word 文件，可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;读取到的内容如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 
1 离 职 证 明
2 
3 兹证明 王大锤 ，身份证号码： 100200199512120001 ，于 2018 年 8 月 7 日至 2020 年 6 月 28 日在我单位  开发部 部门担任 Java开发工程师 职务，在职期间无不良表现。因 个人 原因，于 2020 年 6 月 28 日起终止解除劳动合同。现已结清财务相关费用，办理完解除劳动关系相关手续，双方不存在任何劳动争议。
4 
5 特此证明！
6 
7 
8 公司名称（盖章）:成都风车车科技有限公司
9    			2020 年 6 月 28 日
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;讲到这里，相信很多读者已经想到了，我们可以把上面的离职证明制作成一个模板文件，把姓名、身份证号、入职和离职日期等信息用占位符代替，这样通过对占位符的替换，就可以根据实际需要写入对应的信息，这样就可以批量的生成 Word 文档。&lt;/p&gt;
&lt;p&gt;按照上面的思路，我们首先编辑一个离职证明的模板文件，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/a6cb21e41a.webp&quot; width=&quot;75%&quot; style=&quot;border:1px solid black&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来我们读取该文件，将占位符替换为真实信息，就可以生成一个新的 Word 文档，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from docx import Document
from docx.document import Document as Doc

# 将真实信息用字典的方式保存在列表中
employees = [
    {
        &apos;name&apos;: &apos;骆昊&apos;,
        &apos;id&apos;: &apos;100200198011280001&apos;,
        &apos;sdate&apos;: &apos;2008年3月1日&apos;,
        &apos;edate&apos;: &apos;2012年2月29日&apos;,
        &apos;department&apos;: &apos;产品研发&apos;,
        &apos;position&apos;: &apos;架构师&apos;,
        &apos;company&apos;: &apos;成都华为技术有限公司&apos;
    },
    {
        &apos;name&apos;: &apos;王大锤&apos;,
        &apos;id&apos;: &apos;510210199012125566&apos;,
        &apos;sdate&apos;: &apos;2019年1月1日&apos;,
        &apos;edate&apos;: &apos;2021年4月30日&apos;,
        &apos;department&apos;: &apos;产品研发&apos;,
        &apos;position&apos;: &apos;Python开发工程师&apos;,
        &apos;company&apos;: &apos;成都谷道科技有限公司&apos;
    },
    {
        &apos;name&apos;: &apos;李元芳&apos;,
        &apos;id&apos;: &apos;2102101995103221599&apos;,
        &apos;sdate&apos;: &apos;2020年5月10日&apos;,
        &apos;edate&apos;: &apos;2021年3月5日&apos;,
        &apos;department&apos;: &apos;产品研发&apos;,
        &apos;position&apos;: &apos;Java开发工程师&apos;,
        &apos;company&apos;: &apos;同城企业管理集团有限公司&apos;
    },
]
# 对列表进行循环遍历，批量生成Word文档 
for emp_dict in employees:
    # 读取离职证明模板文件
    doc = Document(&apos;resources/离职证明模板.docx&apos;)  # type: Doc
    # 循环遍历所有段落寻找占位符
    for p in doc.paragraphs:
        if &apos;{&apos; not in p.text:
            continue
        # 不能直接修改段落内容，否则会丢失样式
        # 所以需要对段落中的元素进行遍历并进行查找替换
        for run in p.runs:
            if &apos;{&apos; not in run.text:
                continue
            # 将占位符换成实际内容
            start, end = run.text.find(&apos;{&apos;), run.text.find(&apos;}&apos;)
            key, place_holder = run.text[start + 1:end], run.text[start:end + 1]
            run.text = run.text.replace(place_holder, emp_dict[key])
    # 每个人对应保存一个Word文档
    doc.save(f&apos;{emp_dict[&quot;name&quot;]}离职证明.docx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码，会在当前路径下生成三个 Word 文档，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/71ce6ec77e.webp&quot; width=&quot;50%&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;生成PowerPoint&lt;/h4&gt;
&lt;p&gt;首先我们需要安装名为&lt;code&gt;python-pptx&lt;/code&gt;的三方库，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install python-pptx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 Python 操作 PowerPoint 的内容，因为实际应用场景不算很多，我不打算在这里进行赘述，有兴趣的读者可以自行阅读&lt;code&gt;python-pptx&lt;/code&gt;的&lt;a href=&quot;https://python-pptx.readthedocs.io/en/latest/&quot;&gt;官方文档&lt;/a&gt;，下面仅展示一段来自于官方文档的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pptx import Presentation

# 创建幻灯片对象
pres = Presentation()

# 选择母版添加一页
title_slide_layout = pres.slide_layouts[0]
slide = pres.slides.add_slide(title_slide_layout)
# 获取标题栏和副标题栏
title = slide.shapes.title
subtitle = slide.placeholders[1]
# 编辑标题和副标题
title.text = &quot;Welcome to Python&quot;
subtitle.text = &quot;Life is short, I use Python&quot;

# 选择母版添加一页
bullet_slide_layout = pres.slide_layouts[1]
slide = pres.slides.add_slide(bullet_slide_layout)
# 获取页面上所有形状
shapes = slide.shapes
# 获取标题和主体
title_shape = shapes.title
body_shape = shapes.placeholders[1]
# 编辑标题
title_shape.text = &apos;Introduction&apos;
# 编辑主体内容
tf = body_shape.text_frame
tf.text = &apos;History of Python&apos;
# 添加一个一级段落
p = tf.add_paragraph()
p.text = &apos;X\&apos;max 1989&apos;
p.level = 1
# 添加一个二级段落
p = tf.add_paragraph()
p.text = &apos;Guido began to write interpreter for Python.&apos;
p.level = 2

# 保存幻灯片
pres.save(&apos;test.pptx&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面的代码，生成的 PowerPoint 文件如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/776759b8e8.webp&quot; width=&quot;75%&quot; /&amp;gt;&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;用 Python 程序解决办公自动化的问题真的非常酷，它可以将我们从繁琐乏味的劳动中解放出来。写这类代码就是去做一件一劳永逸的事情，写代码的过程即便不怎么愉快，使用这些代码的时候应该是非常开心的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;27 Python操作PDF文件&lt;/h2&gt;
&lt;p&gt;PDF 是 Portable Document Format 的缩写，这类文件通常使用&lt;code&gt;.pdf&lt;/code&gt;作为其扩展名。在日常开发工作中，最容易遇到的就是从 PDF 中读取文本内容以及用已有的内容生成PDF文档这两个任务。&lt;/p&gt;
&lt;h4&gt;从PDF中提取文本&lt;/h4&gt;
&lt;p&gt;在 Python 中，可以使用名为&lt;code&gt;PyPDF2&lt;/code&gt;的三方库来读取 PDF 文件，可以使用下面的命令来安装它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install PyPDF2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PyPDF2&lt;/code&gt;没有办法从 PDF 文档中提取图像、图表或其他媒体，但它可以提取文本，并将其返回为 Python 字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import PyPDF2

reader = PyPDF2.PdfReader(&apos;test.pdf&apos;)
for page in reader.pages:
    print(page.extract_text())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：本章代码使用到的 PDF 文件都可以通过下面的百度云盘地址进行获取，链接：https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，&lt;code&gt;PyPDF2&lt;/code&gt;并不是什么样的 PDF 文档都能提取出文字来，这个问题就我所知并没有什么特别好的解决方法，尤其是在提取中文的时候。网上也有很多讲解从 PDF 中提取文字的文章，推荐大家自行阅读&lt;a href=&quot;https://cloud.tencent.com/developer/article/1395339&quot;&gt;《三大神器助力Python提取pdf文档信息》&lt;/a&gt;一文进行了解。&lt;/p&gt;
&lt;p&gt;要从 PDF 文件中提取文本也可以直接使用三方的命令行工具，具体的做法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pdfminer.six
pdf2text.py test.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;旋转和叠加页面&lt;/h4&gt;
&lt;p&gt;上面的代码中通过创建&lt;code&gt;PdfFileReader&lt;/code&gt;对象的方式来读取 PDF 文档，该对象的&lt;code&gt;getPage&lt;/code&gt;方法可以获得PDF文档的指定页并得到一个&lt;code&gt;PageObject&lt;/code&gt;对象，通过&lt;code&gt;PageObject&lt;/code&gt;对象的&lt;code&gt;rotateClockwise&lt;/code&gt;和&lt;code&gt;rotateCounterClockwise&lt;/code&gt;方法可以实现页面的顺时针和逆时针方向旋转，通过&lt;code&gt;PageObject&lt;/code&gt;对象的&lt;code&gt;addBlankPage&lt;/code&gt;方法可以添加一个新的空白页，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reader = PyPDF2.PdfReader(&apos;XGBoost.pdf&apos;)
writer = PyPDF2.PdfWriter()

for no, page in enumerate(reader.pages):
    if no % 2 == 0:
        new_page = page.rotate(-90)
    else:
        new_page = page.rotate(90)
    writer.add_page(new_page)

with open(&apos;temp.pdf&apos;, &apos;wb&apos;) as file_obj:
    writer.write(file_obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;加密PDF文件&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;PyPDF2&lt;/code&gt;中的&lt;code&gt;PdfFileWrite&lt;/code&gt;对象可以为PDF文档加密，如果需要给一系列的PDF文档设置统一的访问口令，使用Python程序来处理就会非常的方便。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import PyPDF2

reader = PyPDF2.PdfReader(&apos;XGBoost.pdf&apos;)
writer = PyPDF2.PdfWriter()

for page in reader.pages:
    writer.add_page(page)
    
writer.encrypt(&apos;foobared&apos;)

with open(&apos;temp.pdf&apos;, &apos;wb&apos;) as file_obj:
    writer.write(file_obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;批量添加水印&lt;/h4&gt;
&lt;p&gt;上面提到的&lt;code&gt;PageObject&lt;/code&gt;对象还有一个名为&lt;code&gt;mergePage&lt;/code&gt;的方法，可以两个 PDF 页面进行叠加，通过这个操作，我们很容易实现给PDF文件添加水印的功能。例如要给上面的“XGBoost.pdf”文件添加一个水印，我们可以先准备好一个提供水印页面的 PDF 文件，然后将包含水印的&lt;code&gt;PageObject&lt;/code&gt;读取出来，然后再循环遍历“XGBoost.pdf”文件的每个页，获取到&lt;code&gt;PageObject&lt;/code&gt;对象，然后通过&lt;code&gt;mergePage&lt;/code&gt;方法实现水印页和原始页的合并，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reader1 = PyPDF2.PdfReader(&apos;XGBoost.pdf&apos;)
reader2 = PyPDF2.PdfReader(&apos;watermark.pdf&apos;)
writer = PyPDF2.PdfWriter()
watermark_page = reader2.pages[0]

for page in reader1.pages:
    page.merge_page(watermark_page)
    writer.add_page(page)

with open(&apos;temp.pdf&apos;, &apos;wb&apos;) as file_obj:
    writer.write(file_obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果愿意，还可以让奇数页和偶数页使用不同的水印，大家可以自己思考下应该怎么做。&lt;/p&gt;
&lt;h4&gt;创建PDF文件&lt;/h4&gt;
&lt;p&gt;创建 PDF 文档需要三方库&lt;code&gt;reportlab&lt;/code&gt;的支持，安装的方法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install reportlab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面通过一个例子为大家展示&lt;code&gt;reportlab&lt;/code&gt;的用法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas

pdf_canvas = canvas.Canvas(&apos;resources/demo.pdf&apos;, pagesize=A4)
width, height = A4

# 绘图
image = canvas.ImageReader(&apos;resources/guido.jpg&apos;)
pdf_canvas.drawImage(image, 20, height - 395, 250, 375)

# 显示当前页
pdf_canvas.showPage()

# 注册字体文件
pdfmetrics.registerFont(TTFont(&apos;Font1&apos;, &apos;resources/fonts/Vera.ttf&apos;))
pdfmetrics.registerFont(TTFont(&apos;Font2&apos;, &apos;resources/fonts/青呱石头体.ttf&apos;))

# 写字
pdf_canvas.setFont(&apos;Font2&apos;, 40)
pdf_canvas.setFillColorRGB(0.9, 0.5, 0.3, 1)
pdf_canvas.drawString(width // 2 - 120, height // 2, &apos;你好，世界！&apos;)
pdf_canvas.setFont(&apos;Font1&apos;, 40)
pdf_canvas.setFillColorRGB(0, 1, 0, 0.5)
pdf_canvas.rotate(18)
pdf_canvas.drawString(250, 250, &apos;hello, world!&apos;)

# 保存
pdf_canvas.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码如果不太理解也没有关系，等真正需要用 Python 创建 PDF 文档的时候，再好好研读一下&lt;code&gt;reportlab&lt;/code&gt;的&lt;a href=&quot;https://www.reportlab.com/docs/reportlab-userguide.pdf&quot;&gt;官方文档&lt;/a&gt;就可以了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面代码中用到的图片和字体可以通过下面的百度云盘链接获取，链接：https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g，提取码：e7b4。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;在学习完上面的内容之后，相信大家已经知道像合并多个 PDF 文件这样的工作应该如何用 Python 代码来处理了，赶紧自己动手试一试吧。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;28 Python处理图像&lt;/h2&gt;
&lt;h4&gt;入门知识&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;颜色。如果你有使用颜料画画的经历，那么一定知道混合红、黄、蓝三种颜料可以得到其他的颜色，事实上这三种颜色就是美术中的三原色，它们是不能再分解的基本颜色。在计算机中，我们可以将红、绿、蓝三种色光以不同的比例叠加来组合成其他的颜色，因此这三种颜色就是色光三原色。在计算机系统中，我们通常会将一个颜色表示为一个 RGB 值或 RGBA 值（其中的 A 表示 Alpha 通道，它决定了透过这个图像的像素，也就是透明度）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;RGB值&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;RGB值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;White（白）&lt;/td&gt;
&lt;td&gt;(255, 255, 255)&lt;/td&gt;
&lt;td&gt;Red（红）&lt;/td&gt;
&lt;td&gt;(255, 0, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Green（绿）&lt;/td&gt;
&lt;td&gt;(0, 255, 0)&lt;/td&gt;
&lt;td&gt;Blue（蓝）&lt;/td&gt;
&lt;td&gt;(0, 0, 255)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gray（灰）&lt;/td&gt;
&lt;td&gt;(128, 128, 128)&lt;/td&gt;
&lt;td&gt;Yellow（黄）&lt;/td&gt;
&lt;td&gt;(255, 255, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black（黑）&lt;/td&gt;
&lt;td&gt;(0, 0, 0)&lt;/td&gt;
&lt;td&gt;Purple（紫）&lt;/td&gt;
&lt;td&gt;(128, 0, 128)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;像素。对于一个由数字序列表示的图像来说，最小的单位就是图像上单一颜色的小方格，这些小方块都有一个明确的位置和被分配的色彩数值，而这些一小方格的颜色和位置决定了该图像最终呈现出来的样子，它们是不可分割的单位，我们通常称之为像素（pixel）。每一个图像都包含了一定量的像素，这些像素决定图像在屏幕上所呈现的大小，大家如果爱好拍照或者自拍，对像素这个词就不会陌生。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;用Pillow处理图像&lt;/h4&gt;
&lt;p&gt;Pillow 是由从著名的 Python 图像处理库 PIL 发展出来的一个分支，通过 Pillow 可以实现图像压缩和图像处理等各种操作。可以使用下面的命令来安装 Pillow。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pillow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pillow 中最为重要的是&lt;code&gt;Image&lt;/code&gt;类，可以通过&lt;code&gt;Image&lt;/code&gt;模块的&lt;code&gt;open&lt;/code&gt;函数来读取图像并获得&lt;code&gt;Image&lt;/code&gt;类型的对象。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;读取和显示图像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image

# 读取图像获得Image对象
image = Image.open(&apos;guido.jpg&apos;)
# 通过Image对象的format属性获得图像的格式
print(image.format) # JPEG
# 通过Image对象的size属性获得图像的尺寸
print(image.size)   # (500, 750)
# 通过Image对象的mode属性获取图像的模式
print(image.mode)   # RGB
# 通过Image对象的show方法显示图像
image.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/3b730397b2.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;剪裁图像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 通过Image对象的crop方法指定剪裁区域剪裁图像
image.crop((80, 20, 310, 360)).show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/433fcb1bb2.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成缩略图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 通过Image对象的thumbnail方法生成指定尺寸的缩略图
image.thumbnail((128, 128))
image.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/f68a874319.webp&quot; width=&quot;100%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缩放和黏贴图像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 读取骆昊的照片获得Image对象
luohao_image = Image.open(&apos;luohao.png&apos;)
# 读取吉多的照片获得Image对象
guido_image = Image.open(&apos;guido.jpg&apos;)
# 从吉多的照片上剪裁出吉多的头
guido_head = guido_image.crop((80, 20, 310, 360))
width, height = guido_head.size
# 使用Image对象的resize方法修改图像的尺寸
# 使用Image对象的paste方法将吉多的头粘贴到骆昊的照片上
luohao_image.paste(guido_head.resize((int(width / 1.5), int(height / 1.5))), (172, 40))
luohao_image.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/8917ee7d8c.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;旋转和翻转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image = Image.open(&apos;guido.jpg&apos;)
# 使用Image对象的rotate方法实现图像的旋转
image.rotate(45).show()
# 使用Image对象的transpose方法实现图像翻转
# Image.FLIP_LEFT_RIGHT - 水平翻转
# Image.FLIP_TOP_BOTTOM - 垂直翻转
image.transpose(Image.FLIP_TOP_BOTTOM).show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/5c1c6b5bd0.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;操作像素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for x in range(80, 310):
    for y in range(20, 360):
        # 通过Image对象的putpixel方法修改图像指定像素点
        image.putpixel((x, y), (128, 128, 128))
image.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/b24365d91c.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;滤镜效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import ImageFilter

# 使用Image对象的filter方法对图像进行滤镜处理
# ImageFilter模块包含了诸多预设的滤镜也可以自定义滤镜
image.filter(ImageFilter.CONTOUR).show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/45e79cf40e.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用Pillow绘图&lt;/h4&gt;
&lt;p&gt;Pillow 中有一个名为&lt;code&gt;ImageDraw&lt;/code&gt;的模块，该模块的&lt;code&gt;Draw&lt;/code&gt;函数会返回一个&lt;code&gt;ImageDraw&lt;/code&gt;对象，通过&lt;code&gt;ImageDraw&lt;/code&gt;对象的&lt;code&gt;arc&lt;/code&gt;、&lt;code&gt;line&lt;/code&gt;、&lt;code&gt;rectangle&lt;/code&gt;、&lt;code&gt;ellipse&lt;/code&gt;、&lt;code&gt;polygon&lt;/code&gt;等方法，可以在图像上绘制出圆弧、线条、矩形、椭圆、多边形等形状，也可以通过该对象的&lt;code&gt;text&lt;/code&gt;方法在图像上添加文字。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/fdc71ab6a3.webp&quot; width=&quot;80%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;要绘制如上图所示的图像，完整的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

from PIL import Image, ImageDraw, ImageFont


def random_color():
    &quot;&quot;&quot;生成随机颜色&quot;&quot;&quot;
    red = random.randint(0, 255)
    green = random.randint(0, 255)
    blue = random.randint(0, 255)
    return red, green, blue


width, height = 800, 600
# 创建一个800*600的图像，背景色为白色
image = Image.new(mode=&apos;RGB&apos;, size=(width, height), color=(255, 255, 255))
# 创建一个ImageDraw对象
drawer = ImageDraw.Draw(image)
# 通过指定字体和大小获得ImageFont对象
font = ImageFont.truetype(&apos;Kongxin.ttf&apos;, 32)
# 通过ImageDraw对象的text方法绘制文字
drawer.text((300, 50), &apos;Hello, world!&apos;, fill=(255, 0, 0), font=font)
# 通过ImageDraw对象的line方法绘制两条对角直线
drawer.line((0, 0, width, height), fill=(0, 0, 255), width=2)
drawer.line((width, 0, 0, height), fill=(0, 0, 255), width=2)
xy = width // 2 - 60, height // 2 - 60, width // 2 + 60, height // 2 + 60
# 通过ImageDraw对象的rectangle方法绘制矩形
drawer.rectangle(xy, outline=(255, 0, 0), width=2)
# 通过ImageDraw对象的ellipse方法绘制椭圆
for i in range(4):
    left, top, right, bottom = 150 + i * 120, 220, 310 + i * 120, 380
    drawer.ellipse((left, top, right, bottom), outline=random_color(), width=8)
# 显示图像
image.show()
# 保存图像
image.save(&apos;result.png&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面代码中使用的字体文件需要根据自己准备，可以选择自己喜欢的字体文件并放置在代码目录下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;使用 Python 语言做开发，除了可以用 Pillow 来处理图像外，还可以使用更为强大的 OpenCV 库来完成图形图像的处理，OpenCV（&lt;strong&gt;Open&lt;/strong&gt; Source &lt;strong&gt;C&lt;/strong&gt;omputer &lt;strong&gt;V&lt;/strong&gt;ision Library）是一个跨平台的计算机视觉库，可以用来开发实时图像处理、计算机视觉和模式识别程序。在我们的日常工作中，有很多繁琐乏味的任务其实都可以通过 Python 程序来处理，编程的目的就是让计算机帮助我们解决问题，减少重复乏味的劳动。通过本章节的学习，相信大家已经感受到了使用 Python 程序绘图改图的乐趣，其实 Python 能做的事情还远不止这些，继续你的学习吧。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;29 Python发送邮件和短信&lt;/h2&gt;
&lt;p&gt;在前面的课程中，我们已经教会大家如何用 Python 程序自动的生成 Excel、Word、PDF 文档，接下来我们还可以更进一步，就是通过邮件将生成好的文档发送给指定的收件人，然后用短信告知对方我们发出了邮件。这些事情利用 Python 程序也可以轻松愉快的解决。&lt;/p&gt;
&lt;h4&gt;发送电子邮件&lt;/h4&gt;
&lt;p&gt;在即时通信软件如此发达的今天，电子邮件仍然是互联网上使用最为广泛的应用之一，公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的，而这些任务应该都是由程序自动完成的。&lt;/p&gt;
&lt;p&gt;我们可以用 HTTP（超文本传输协议）来访问网站资源，HTTP 是一个应用级协议，它建立在 TCP（传输控制协议）之上，TCP 为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件，需要使用 SMTP（简单邮件传输协议），它也是建立在 TCP 之上的应用级协议，规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python 通过名为&lt;code&gt;smtplib&lt;/code&gt;的模块将这些操作简化成了&lt;code&gt;SMTP_SSL&lt;/code&gt;对象，通过该对象的&lt;code&gt;login&lt;/code&gt;和&lt;code&gt;send_mail&lt;/code&gt;方法，就能够完成发送邮件的操作。&lt;/p&gt;
&lt;p&gt;我们先尝试一下发送一封极为简单的邮件，该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器，我们可以自己架设邮件服务器，这件事情对新手并不友好，但是我们可以选择使用第三方提供的邮件服务。例如，我在&amp;lt;www.126.com&amp;gt;已经注册了账号，登录成功之后，就可以在设置中开启 SMTP 服务，这样就相当于获得了邮件服务器，具体的操作如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/03e6596ed1.webp&quot; alt=&quot;image-20210820190306861&quot; width=&quot;85%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/9b8aa50940.webp&quot; style=&quot;zoom:55%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;用手机扫码上面的二维码可以通过发送短信的方式来获取授权码，短信发送成功后，点击“我已发送”就可以获得授权码。授权码需要妥善保管，因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来，我们就可以编写发送邮件的代码了，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email[&apos;From&apos;] = &apos;xxxxxxxxx@126.com&apos;
email[&apos;To&apos;] = &apos;yyyyyy@qq.com;zzzzzz@1000phone.com&apos;
email[&apos;Subject&apos;] = Header(&apos;上半年工作情况汇报&apos;, &apos;utf-8&apos;)
# 添加邮件正文内容
content = &quot;&quot;&quot;据德国媒体报道，当地时间9日，德国火车司机工会成员进行了投票，
定于当地时间10日起进行全国性罢工，货运交通方面的罢工已于当地时间10日19时开始。
此后，从11日凌晨2时到13日凌晨2时，德国全国范围内的客运和铁路基础设施将进行48小时的罢工。&quot;&quot;&quot;
email.attach(MIMEText(content, &apos;plain&apos;, &apos;utf-8&apos;))

# 创建SMTP_SSL对象（连接邮件服务器）
smtp_obj = smtplib.SMTP_SSL(&apos;smtp.126.com&apos;, 465)
# 通过用户名和授权码进行登录
smtp_obj.login(&apos;xxxxxxxxx@126.com&apos;, &apos;邮件服务器的授权码&apos;)
# 发送邮件（发件人、收件人、邮件内容（字符串））
smtp_obj.sendmail(
    &apos;xxxxxxxxx@126.com&apos;,
    [&apos;yyyyyy@qq.com&apos;, &apos;zzzzzz@1000phone.com&apos;],
    email.as_string()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要发送带有附件的邮件，只需要将附件的内容处理成 BASE64 编码，那么它就和普通的文本内容几乎没有什么区别。BASE64 是一种基于 64 个可打印字符来表示二进制数据的表示方法，常用于某些需要表示、传输、存储二进制数据的场合，电子邮件就是其中之一。对这种编码方式不理解的同学，推荐阅读&lt;a href=&quot;http://www.ruanyifeng.com/blog/2008/06/base64.html&quot;&gt;《Base64笔记》&lt;/a&gt;一文。在之前的内容中，我们也提到过，Python 标准库的&lt;code&gt;base64&lt;/code&gt;模块提供了对 BASE64 编解码的支持。&lt;/p&gt;
&lt;p&gt;下面的代码演示了如何发送带附件的邮件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote

# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email[&apos;From&apos;] = &apos;xxxxxxxxx@126.com&apos;
email[&apos;To&apos;] = &apos;zzzzzzzz@1000phone.com&apos;
email[&apos;Subject&apos;] = Header(&apos;请查收离职证明文件&apos;, &apos;utf-8&apos;)
# 添加邮件正文内容（带HTML标签排版的内容）
content = &quot;&quot;&quot;&amp;lt;p&amp;gt;亲爱的前同事：&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;你需要的离职证明在附件中，请查收！&amp;lt;/p&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;p&amp;gt;祝，好！&amp;lt;/p&amp;gt;
&amp;lt;hr&amp;gt;
&amp;lt;p&amp;gt;孙美丽 即日&amp;lt;/p&amp;gt;&quot;&quot;&quot;
email.attach(MIMEText(content, &apos;html&apos;, &apos;utf-8&apos;))
# 读取作为附件的文件
with open(f&apos;resources/王大锤离职证明.docx&apos;, &apos;rb&apos;) as file:
    attachment = MIMEText(file.read(), &apos;base64&apos;, &apos;utf-8&apos;)
    # 指定内容类型
    attachment[&apos;content-type&apos;] = &apos;application/octet-stream&apos;
    # 将中文文件名处理成百分号编码
    filename = quote(&apos;王大锤离职证明.docx&apos;)
    # 指定如何处置内容
    attachment[&apos;content-disposition&apos;] = f&apos;attachment; filename=&quot;{filename}&quot;&apos;

# 创建SMTP_SSL对象（连接邮件服务器）
smtp_obj = smtplib.SMTP_SSL(&apos;smtp.126.com&apos;, 465)
# 通过用户名和授权码进行登录
smtp_obj.login(&apos;xxxxxxxxx@126.com&apos;, &apos;邮件服务器的授权码&apos;)
# 发送邮件（发件人、收件人、邮件内容（字符串））
smtp_obj.sendmail(
    &apos;xxxxxxxxx@126.com&apos;,
    &apos;zzzzzzzz@1000phone.com&apos;,
    email.as_string()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了方便大家用 Python 实现邮件发送，我将上面的代码封装成了函数，使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote

# 邮件服务器域名（自行修改）
EMAIL_HOST = &apos;smtp.126.com&apos;
# 邮件服务端口（通常是465）
EMAIL_PORT = 465
# 登录邮件服务器的账号（自行修改）
EMAIL_USER = &apos;xxxxxxxxx@126.com&apos;
# 开通SMTP服务的授权码（自行修改）
EMAIL_AUTH = &apos;邮件服务器的授权码&apos;


def send_email(*, from_user, to_users, subject=&apos;&apos;, content=&apos;&apos;, filenames=[]):
    &quot;&quot;&quot;发送邮件
    
    :param from_user: 发件人
    :param to_users: 收件人，多个收件人用英文分号进行分隔
    :param subject: 邮件的主题
    :param content: 邮件正文内容
    :param filenames: 附件要发送的文件路径
    &quot;&quot;&quot;
    email = MIMEMultipart()
    email[&apos;From&apos;] = from_user
    email[&apos;To&apos;] = to_users
    email[&apos;Subject&apos;] = subject

    message = MIMEText(content, &apos;plain&apos;, &apos;utf-8&apos;)
    email.attach(message)
    for filename in filenames:
        with open(filename, &apos;rb&apos;) as file:
            pos = filename.rfind(&apos;/&apos;)
            display_filename = filename[pos + 1:] if pos &amp;gt;= 0 else filename
            display_filename = quote(display_filename)
            attachment = MIMEText(file.read(), &apos;base64&apos;, &apos;utf-8&apos;)
            attachment[&apos;content-type&apos;] = &apos;application/octet-stream&apos;
            attachment[&apos;content-disposition&apos;] = f&apos;attachment; filename=&quot;{display_filename}&quot;&apos;
            email.attach(attachment)

    smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
    smtp.login(EMAIL_USER, EMAIL_AUTH)
    smtp.sendmail(from_user, to_users.split(&apos;;&apos;), email.as_string())
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;发送短信&lt;/h4&gt;
&lt;p&gt;发送短信也是项目中常见的功能，网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台的支持，下面我们以&lt;a href=&quot;https://luosimao.com/&quot;&gt;螺丝帽平台&lt;/a&gt;为例，为大家介绍如何用 Python 程序发送短信。注册账号和购买短信服务的细节我们不在这里进行赘述，大家可以咨询平台的客服。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/1bd0b631c5.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们可以通过&lt;code&gt;requests&lt;/code&gt;库向平台提供的短信网关发起一个 HTTP 请求，通过将接收短信的手机号和短信内容作为参数，就可以发送短信，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

import requests


def send_message_by_luosimao(tel, message):
    &quot;&quot;&quot;发送短信（调用螺丝帽短信网关）&quot;&quot;&quot;
    resp = requests.post(
        url=&apos;http://sms-api.luosimao.com/v1/send.json&apos;,
        auth=(&apos;api&apos;, &apos;key-注册成功后平台分配的KEY&apos;),
        data={
            &apos;mobile&apos;: tel,
            &apos;message&apos;: message
        },
        timeout=10,
        verify=False
    )
    return resp.json()


def gen_mobile_code(length=6):
    &quot;&quot;&quot;生成指定长度的手机验证码&quot;&quot;&quot;
    return &apos;&apos;.join(random.choices(&apos;0123456789&apos;, k=length))


def main():
    code = gen_mobile_code()
    message = f&apos;您的短信验证码是{code}，打死也不能告诉别人哟！【Python小课】&apos;
    print(send_message_by_luosimao(&apos;13500112233&apos;, message))


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面请求螺丝帽的短信网关&lt;code&gt;http://sms-api.luosimao.com/v1/send.json&lt;/code&gt;会返回JSON格式的数据，如果返回&lt;code&gt;{&apos;error&apos;: 0, &apos;msg&apos;: &apos;OK&apos;}&lt;/code&gt;就说明短信已经发送成功了，如果&lt;code&gt;error&lt;/code&gt;的值不是&lt;code&gt;0&lt;/code&gt;，可以通过查看官方的&lt;a href=&quot;https://luosimao.com/docs/api/&quot;&gt;开发文档&lt;/a&gt;了解到底哪个环节出了问题。螺丝帽平台常见的错误类型如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/efb7ff7b45.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;目前，大多数短信平台都会要求短信内容必须附上签名，下图是我在螺丝帽平台配置的短信签名“【Python小课】”。有些涉及到敏感内容的短信，还需要提前配置短信模板，有兴趣的读者可以自行研究。一般情况下，平台为了防范短信被盗用，还会要求设置“IP 白名单”，不清楚如何配置的可以咨询平台客服。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/86e9f878a2.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当然国内的短信平台很多，读者可以根据自己的需要进行选择（通常会考虑费用预算、短信达到率、使用的难易程度等指标），如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;其实，发送邮件和发送短信一样，也可以通过调用三方服务来完成，在实际的商业项目中，建议自己架设邮件服务器或购买三方服务来发送邮件，这个才是比较靠谱的选择。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;30 正则表达式的应用&lt;/h2&gt;
&lt;h4&gt;正则表达式相关知识&lt;/h4&gt;
&lt;p&gt;在编写处理字符串的程序时，经常会遇到在一段文本中查找符合某些规则的字符串的需求，正则表达式就是用于描述这些规则的工具，换句话说，我们可以使用正则表达式来定义字符串的匹配模式，即如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉。&lt;/p&gt;
&lt;p&gt;举一个简单的例子，如果你在 Windows 操作系统中使用过文件查找并且在指定文件名时使用过通配符（&lt;code&gt;*&lt;/code&gt;和&lt;code&gt;?&lt;/code&gt;），那么正则表达式也是与之类似的用 来进行文本匹配的工具，只不过比起通配符正则表达式更强大，它能更精确地描述你的需求，当然你付出的代价是书写一个正则表达式比使用通配符要复杂得多，因为任何给你带来好处的东西都需要你付出对应的代价。&lt;/p&gt;
&lt;p&gt;再举一个例子，我们从某个地方（可能是一个文本文件，也可能是网络上的一则新闻）获得了一个字符串，希望在字符串中找出手机号和座机号。当然我们可以设定手机号是 11 位的数字（注意并不是随机的 11 位数字，因为你没有见过“25012345678”这样的手机号），而座机号则是类似于“区号-号码”这样的模式，如果不使用正则表达式要完成这个任务就会比较麻烦。最初计算机是为了做数学运算而诞生的，处理的信息基本上都是数值，而今天我们在日常工作中处理的信息很多都是文本数据，我们希望计算机能够识别和处理符合某些模式的文本，正则表达式就显得非常重要了。今天几乎所有的编程语言都提供了对正则表达式操作的支持，Python 通过标准库中的&lt;code&gt;re&lt;/code&gt;模块来支持正则表达式操作。&lt;/p&gt;
&lt;p&gt;关于正则表达式的相关知识，大家可以阅读一篇非常有名的博文叫&lt;a href=&quot;https://deerchao.net/tutorials/regex/regex.htm&quot;&gt;《正则表达式30分钟入门教程》&lt;/a&gt;，读完这篇文章后你就可以看懂下面的表格，这是我们对正则表达式中的一些基本符号进行的扼要总结。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任意字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b.t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配bat / but / b#t / b1t等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配字母/数字/下划线&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b\wt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配bat / b1t / b_t等&amp;lt;br&amp;gt;但不能匹配b#t&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配空白字符（包括\r、\n、\t等）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;love\syou&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配love you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\d\d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配01 / 23 / 99等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配单词的边界&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\bThe\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配字符串的开始&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^The&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配The开头的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配字符串的结束&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.exe$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配.exe结尾的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配非字母/数字/下划线&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b\Wt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配b#t / b@t等&amp;lt;br&amp;gt;但不能匹配but / b1t / b_t等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配非空白字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;love\Syou&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配love#you等&amp;lt;br&amp;gt;但不能匹配love you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配非数字&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\d\D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配9a / 3# / 0F等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配非单词边界&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\Bio\B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配来自字符集的任意单一字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[aeiou]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配任一元音字母字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[^]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配不在字符集中的任意单一字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[^aeiou]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配任一非元音字母字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配0次或多次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配1次或多次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配0次或1次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配N次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w{3}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{M,}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配至少M次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w{3,}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{M,N}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配至少M次至多N次&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\w{3,6}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;|&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分支&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo|bar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配foo或者bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?#)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注释&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配exp并捕获到自动命名的组中&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;name&amp;gt;exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配exp并捕获到名为name的组中&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?:exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配exp但是不捕获匹配的文本&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?=exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配exp前面的位置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\b\w+(?=ing)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配I&apos;m dancing中的danc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;=exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配exp后面的位置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;=\bdanc)\w+\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以匹配I love dancing and reading中的第一个ing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?!exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配后面不是exp的位置&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(?&amp;lt;!exp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配前面不是exp的位置&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重复任意次，但尽可能少重复&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a.*b&lt;/code&gt;&amp;lt;br&amp;gt;&lt;code&gt;a.*?b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将正则表达式应用于aabab，前者会匹配整个字符串aabab，后者会匹配aab和ab两个字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重复1次或多次，但尽可能少重复&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重复0次或1次，但尽可能少重复&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{M,N}?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重复M到N次，但尽可能少重复&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{M,}?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重复M次以上，但尽可能少重复&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 如果需要匹配的字符是正则表达式中的特殊字符，那么可以使用&lt;code&gt;\&lt;/code&gt;进行转义处理，例如想匹配小数点可以写成&lt;code&gt;\.&lt;/code&gt;就可以了，因为直接写&lt;code&gt;.&lt;/code&gt;会匹配任意字符；同理，想匹配圆括号必须写成&lt;code&gt;\(&lt;/code&gt;和&lt;code&gt;\)&lt;/code&gt;，否则圆括号被视为正则表达式中的分组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Python对正则表达式的支持&lt;/h4&gt;
&lt;p&gt;Python 提供了&lt;code&gt;re&lt;/code&gt;模块来支持正则表达式相关操作，下面是&lt;code&gt;re&lt;/code&gt;模块中的核心函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compile(pattern, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;编译正则表达式返回正则表达式对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;match(pattern, string, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用正则表达式匹配字符串 成功返回匹配对象 否则返回&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search(pattern, string, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;搜索字符串中第一次出现正则表达式的模式 成功返回匹配对象 否则返回&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;split(pattern, string, maxsplit=0, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用正则表达式指定的模式分隔符拆分字符串 返回列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub(pattern, repl, string, count=0, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用指定的字符串替换原字符串中与正则表达式匹配的模式 可以用&lt;code&gt;count&lt;/code&gt;指定替换的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fullmatch(pattern, string, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;match&lt;/code&gt;函数的完全匹配（从字符串开头到结尾）版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findall(pattern, string, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找字符串所有与正则表达式匹配的模式 返回字符串的列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;finditer(pattern, string, flags=0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找字符串所有与正则表达式匹配的模式 返回一个迭代器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;purge()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清除隐式编译的正则表达式的缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;re.I&lt;/code&gt; / &lt;code&gt;re.IGNORECASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;忽略大小写匹配标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;re.M&lt;/code&gt; / &lt;code&gt;re.MULTILINE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多行匹配标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 上面提到的&lt;code&gt;re&lt;/code&gt;模块中的这些函数，实际开发中也可以用正则表达式对象（&lt;code&gt;Pattern&lt;/code&gt;对象）的方法替代对这些函数的使用，如果一个正则表达式需要重复的使用，那么先通过&lt;code&gt;compile&lt;/code&gt;函数编译正则表达式并创建出正则表达式对象无疑是更为明智的选择。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面我们通过一系列的例子来告诉大家在Python中如何使用正则表达式。&lt;/p&gt;
&lt;h5&gt;例子1：验证输入用户名和QQ号是否有效并给出对应的提示信息。&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
要求：用户名必须由字母、数字或下划线构成且长度在6~20个字符之间，QQ号是5~12的数字且首位不能为0
&quot;&quot;&quot;
import re

username = input(&apos;请输入用户名: &apos;)
qq = input(&apos;请输入QQ号: &apos;)
# match函数的第一个参数是正则表达式字符串或正则表达式对象
# match函数的第二个参数是要跟正则表达式做匹配的字符串对象
m1 = re.match(r&apos;^[0-9a-zA-Z_]{6,20}$&apos;, username)
if not m1:
    print(&apos;请输入有效的用户名.&apos;)
# fullmatch函数要求字符串和正则表达式完全匹配
# 所以正则表达式没有写起始符和结束符
m2 = re.fullmatch(r&apos;[1-9]\d{4,11}&apos;, qq)
if not m2:
    print(&apos;请输入有效的QQ号.&apos;)
if m1 and m2:
    print(&apos;你输入的信息是有效的!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; 上面在书写正则表达式时使用了“原始字符串”的写法（在字符串前面加上了&lt;code&gt;r&lt;/code&gt;），所谓“原始字符串”就是字符串中的每个字符都是它原始的意义，说得更直接一点就是字符串中没有所谓的转义字符啦。因为正则表达式中有很多元字符和需要进行转义的地方，如果不使用原始字符串就需要将反斜杠写作&lt;code&gt;\\&lt;/code&gt;，例如表示数字的&lt;code&gt;\d&lt;/code&gt;得书写成&lt;code&gt;\\d&lt;/code&gt;，这样不仅写起来不方便，阅读的时候也会很吃力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子2：从一段文字中提取出国内手机号码。&lt;/h5&gt;
&lt;p&gt;下面这张图是截止到 2017 年底，国内三家运营商推出的手机号段。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day21-30/df95736c67.webp&quot; style=&quot;zoom:100%;&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import re

# 创建正则表达式对象，使用了前瞻和回顾来保证手机号前后不应该再出现数字
pattern = re.compile(r&apos;(?&amp;lt;=\D)1[34578]\d{9}(?=\D)&apos;)
sentence = &apos;&apos;&apos;重要的事情说8130123456789遍，我的手机号是13512346789这个靓号，
不是15600998765，也不是110或119，王大锤的手机号才是15600998765。&apos;&apos;&apos;
# 方法一：查找所有匹配并保存到一个列表中
tels_list = re.findall(pattern, sentence)
for tel in tels_list:
    print(tel)
print(&apos;--------华丽的分隔线--------&apos;)

# 方法二：通过迭代器取出匹配对象并获得匹配的内容
for temp in pattern.finditer(sentence):
    print(temp.group())
print(&apos;--------华丽的分隔线--------&apos;)

# 方法三：通过search函数指定搜索位置找出所有匹配
m = pattern.search(sentence)
while m:
    print(m.group())
    m = pattern.search(sentence, m.end())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 上面匹配国内手机号的正则表达式并不够好，因为像 14 开头的号码只有 145 或 147，而上面的正则表达式并没有考虑这种情况，要匹配国内手机号，更好的正则表达式的写法是：&lt;code&gt;(?&amp;lt;=\D)(1[38]\d{9}|14[57]\d{8}|15[0-35-9]\d{8}|17[678]\d{8})(?=\D)&lt;/code&gt;，国内好像已经有 19 和 16 开头的手机号了，但是这个暂时不在我们考虑之列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子3：替换字符串中的不良内容&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import re

sentence = &apos;Oh, shit! 你是傻逼吗? Fuck you.&apos;
purified = re.sub(&apos;fuck|shit|[傻煞沙][比笔逼叉缺吊碉雕]&apos;,
                  &apos;*&apos;, sentence, flags=re.IGNORECASE)
print(purified)  # Oh, *! 你是*吗? * you.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;code&gt; re&lt;/code&gt;模块的正则表达式相关函数中都有一个&lt;code&gt;flags&lt;/code&gt;参数，它代表了正则表达式的匹配标记，可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为&lt;code&gt;flags&lt;/code&gt;参数指定多个值，可以使用&lt;a href=&quot;http://www.runoob.com/python/python-operators.html#ysf5&quot;&gt;按位或运算符&lt;/a&gt;进行叠加，如&lt;code&gt;flags=re.I | re.M&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子4：拆分长字符串&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import re

poem = &apos;窗前明月光，疑是地上霜。举头望明月，低头思故乡。&apos;
sentences_list = re.split(r&apos;[，。]&apos;, poem)
sentences_list = [sentence for sentence in sentences_list if sentence]
for sentence in sentences_list:
    print(sentence)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;正则表达式在字符串的处理和匹配上真的非常强大，通过上面的例子相信大家已经感受到了正则表达式的魅力，当然写一个正则表达式对新手来说并不是那么容易，但是很多事情都是熟能生巧，大胆的去尝试就行了，有一个在线的&lt;a href=&quot;https://c.runoob.com/front-end/854&quot;&gt;正则表达式测试工具&lt;/a&gt;相信能够在一定程度上帮到大家。&lt;/p&gt;
</content:encoded></item><item><title>Day01-20 教程全文（20 篇合并）</title><link>https://www.klucen.cn/posts/python/day01-20-full/</link><guid isPermaLink="true">https://www.klucen.cn/posts/python/day01-20-full/</guid><pubDate>Sun, 14 Dec 2025 00:10:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning[来源与版权（请先读）]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原文作者&lt;/strong&gt;：骆昊（&lt;a href=&quot;https://github.com/jackfrued&quot;&gt;jackfrued&lt;/a&gt;）｜&lt;strong&gt;原项目&lt;/strong&gt;：&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;Python-100-Days&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权状态&lt;/strong&gt;：原仓库&lt;strong&gt;没有 LICENSE、README 也未声明任何许可&lt;/strong&gt;；本文是把该目录下 20 篇 教程&lt;strong&gt;合并为单页&lt;/strong&gt;，方便在手机上连续阅读，&lt;strong&gt;不是本站原创&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;完整仓库、配套源码与更新请以&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/README.md&quot;&gt;原项目&lt;/a&gt;为准；如作者有异议，告知后我会立即撤下&lt;/li&gt;
&lt;li&gt;本篇覆盖：从初识 Python 到面向对象
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本篇目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#01-%E5%88%9D%E8%AF%86python&quot;&gt;01 初识Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#02-%E7%AC%AC%E4%B8%80%E4%B8%AApython%E7%A8%8B%E5%BA%8F&quot;&gt;02 第一个Python程序&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#03-python%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%8F%98%E9%87%8F&quot;&gt;03 Python语言中的变量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#04-python%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E8%BF%90%E7%AE%97%E7%AC%A6&quot;&gt;04 Python语言中的运算符&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#05-%E5%88%86%E6%94%AF%E7%BB%93%E6%9E%84&quot;&gt;05 分支结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#06-%E5%BE%AA%E7%8E%AF%E7%BB%93%E6%9E%84&quot;&gt;06 循环结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#07-%E5%88%86%E6%94%AF%E5%92%8C%E5%BE%AA%E7%8E%AF%E7%BB%93%E6%9E%84%E5%AE%9E%E6%88%98&quot;&gt;07 分支和循环结构实战&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#08-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%88%97%E8%A1%A8-1&quot;&gt;08 常用数据结构之列表-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#09-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%88%97%E8%A1%A8-2&quot;&gt;09 常用数据结构之列表-2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%85%83%E7%BB%84&quot;&gt;10 常用数据结构之元组&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#11-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%AD%97%E7%AC%A6%E4%B8%B2&quot;&gt;11 常用数据结构之字符串&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#12-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E9%9B%86%E5%90%88&quot;&gt;12 常用数据结构之集合&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#13-%E5%B8%B8%E7%94%A8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E5%AD%97%E5%85%B8&quot;&gt;13 常用数据结构之字典&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#14-%E5%87%BD%E6%95%B0%E5%92%8C%E6%A8%A1%E5%9D%97&quot;&gt;14 函数和模块&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#15-%E5%87%BD%E6%95%B0%E5%BA%94%E7%94%A8%E5%AE%9E%E6%88%98&quot;&gt;15 函数应用实战&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#16-%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E8%BF%9B%E9%98%B6&quot;&gt;16 函数使用进阶&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#17-%E5%87%BD%E6%95%B0%E9%AB%98%E7%BA%A7%E5%BA%94%E7%94%A8&quot;&gt;17 函数高级应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#18-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B%E5%85%A5%E9%97%A8&quot;&gt;18 面向对象编程入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#19-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B%E8%BF%9B%E9%98%B6&quot;&gt;19 面向对象编程进阶&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#20-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B%E5%BA%94%E7%94%A8&quot;&gt;20 面向对象编程应用&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;目录里每一条都可以点开直达对应章节；右侧（移动端为悬浮按钮）的「文章目录」也会列出同一批标题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;01 初识Python&lt;/h2&gt;
&lt;h4&gt;Python简介&lt;/h4&gt;
&lt;p&gt;Python（英式发音：/ˈpaɪθən/；美式发音：/ˈpaɪθɑːn/）是由荷兰人吉多·范罗苏姆（Guido von Rossum）发明的一种编程语言，是目前世界上最受欢迎和拥有最多用户的编程语言。Python 强调代码的可读性和语法的简洁性，相较于 C、C++、Java 这些同样影响深远的编程语言，Python 让使用者能够用更少的代码表达自己的意图。下面是几个权威的编程语言排行榜给出的 Python 语言的排名，其中第1张图由 TIOBE Index 提供，第3张图由 IEEE Spectrum 提供。值得一提的是第2张图，它展示了编程语言在全球最大代码托管平台 GitHub 上受欢迎的程度，最近的四年时间 Python 语言都占据了冠军的宝座。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/670de9ea8d.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/871629165a.webp&quot; style=&quot;zoom:60%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;Python编年史&lt;/h5&gt;
&lt;p&gt;下面是 Python 语言发展过程中的一些重要时间点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;1989年12月：吉多·范罗苏姆决心开发一个新的脚本语言及其解释器来打发无聊的圣诞节，新语言将作为 ABC 语言的继承者，主要用来替代 Unix shell 和 C 语言实现系统管理。由于吉多本人是 BBC 电视剧《&lt;em&gt;Monty Python&apos;s Flying Circus&lt;/em&gt;》的忠实粉丝，所以他选择了 Python 这个词作为新语言的名字。&lt;/li&gt;
&lt;li&gt;1991年02月：吉多·范罗苏姆在 alt.sources 新闻组上发布了 Python 解释器的最初代码，标记为版本0.9.0。&lt;/li&gt;
&lt;li&gt;1994年01月：Python 1.0发布，梦开始的地方。&lt;/li&gt;
&lt;li&gt;2000年10月：Python 2.0发布，Python 的整个开发过程更加透明，生态圈开始慢慢形成。&lt;/li&gt;
&lt;li&gt;2008年12月：Python 3.0发布，引入了诸多现代编程语言的新特性，但并不完全向下兼容。&lt;/li&gt;
&lt;li&gt;2011年04月：pip 首次发布，Python 语言有了自己的包管理工具。&lt;/li&gt;
&lt;li&gt;2018年07月：吉多·范罗苏姆宣布从“终身仁慈独裁者”（开源项目社区出现争议时拥有最终决定权的人）的职位上“永久休假”。&lt;/li&gt;
&lt;li&gt;2020年01月：在 Python 2和 Python 3共存了11年之后，官方停止了对 Python 2的更新和维护，希望用户尽快切换到 Python 3。&lt;/li&gt;
&lt;li&gt;目前：Python 在大模型（GPT-3、GPT-4、BERT等）、计算机视觉（图像识别、目标检测、图像生成等）、智能推荐（YouTube、Netflix、字节跳动等）、自动驾驶（Waymo、Apollo等）、语音识别、数据科学、量化交易、自动化测试、自动化运维等领域都得到了广泛的应用，Python 语言的生态圈也是相当繁荣。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大多数软件的版本号一般分为三段，形如A.B.C，其中A表示大版本号，当软件整体重写升级或出现不向后兼容的改变时，才会增加A；B表示功能更新，出现新功能时增加B；C表示小的改动（例如：修复了某个Bug），只要有修改就增加C。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;Python优缺点&lt;/h5&gt;
&lt;p&gt;Python 语言的优点很多，简单为大家列出几点。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;简单优雅&lt;/strong&gt;，跟其他很多编程语言相比，Python &lt;strong&gt;更容易上手&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;能用更少的代码做更多的事情，&lt;strong&gt;提升开发效率&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;开放源代码，拥有&lt;strong&gt;强大的社区和生态圈&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能够做的事情非常多&lt;/strong&gt;，有极强的适应性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;胶水语言&lt;/strong&gt;，能够黏合其他语言开发的东西。&lt;/li&gt;
&lt;li&gt;解释型语言，更容易&lt;strong&gt;跨平台&lt;/strong&gt;，能够在多种操作系统上运行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python 最主要的缺点是&lt;strong&gt;执行效率低&lt;/strong&gt;（解释型语言的通病），如果更看重代码的执行效率，C、C++ 或 Go 可能是你更好的选择。&lt;/p&gt;
&lt;h4&gt;安装Python环境&lt;/h4&gt;
&lt;p&gt;工欲善其事，必先利其器。想要开始你的 Python 编程之旅，首先得在计算机上安装 Python 环境，简单的说就是安装运行 Python 程序需要的 Python 解释器。我们推荐大家安装官方的 Python 3 解释器，它是用 C 语言编写的，我们通常也称之为 CPython，它可能是你目前最好的选择。首先，我们需要从官方网站的&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;下载页面&lt;/a&gt;找到下载链接，点击“Download”按钮进入下载页面后，需要根据自己的操作系统选择合适的 Python 3安装程序，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/d4ff658984.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;进入下载页面后，有些 Python 版本并没有提供 Windows 和 macOS 系统的安装程序，只提供了源代码的压缩文件，对于熟悉 Linux 系统的小伙伴，我们可以通过源代码构建安装；对于使用 Windows 或 macOS 系统的小伙伴，我们还是&lt;strong&gt;强烈建议&lt;/strong&gt;使用安装程序。例如，你想安装 Python 3.10，选择 Python 3.10.10 或 Python 3.10.11 就能找到 Windows 或 macOS 系统的安装包，而其他版本可能只有源代码，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/f6dad5ffaa.webp&quot; style=&quot;zoom:36%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;Windows环境&lt;/h5&gt;
&lt;p&gt;下面我们以 Windows 11为例，讲解如何在 Windows 操作系统上安装 Python 环境。双击运行从官网下载的安装程序，会打开一个安装向导，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/f5f2107c9a.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;首先，一定要记得勾选“Add python.exe to PATH”选项，它会帮助我们将 Python 解释器添加到 Windows 系统的 PATH 环境变量中（不理解没关系，勾上就对了）；其次，“Use admin privileges when installing py.exe”是为了在安装过程中获得管理员权限，建议勾选。然后，我们选择“Customize Installation”，使用自定义安装的模式，这是专业人士的选择，而你就（假装）是那个专业人士，不建议使用“Install Now”（默认安装）。&lt;/p&gt;
&lt;p&gt;接下来，安装向导会提示你勾选需要的“Optional Features”（可选特性），这里咱们可以直接全选。值得一提的是其中的第2项，它是 Python 的包管理工具 pip，可以帮助我们安装三方库和三方工具，所以一定要记得勾选它，然后点击“Next”进入下一环节。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/a4e4a7e64f.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来是对“Advanced Options”（高级选项）的选择，这里我们建议大家只勾选“Add Python to environment variables”和“Precompile standard library”这两个选项，前者会帮助我们自动配置好环境变量，后者会预编译标准库（生成&lt;code&gt;.pyc&lt;/code&gt;文件），这样在使用时就无需临时编译了。还是那句话，不理解没关系，勾上就对了。下面的“Customize install location”（自定义安装路径）&lt;strong&gt;强烈建议&lt;/strong&gt;修改为自定义的路径，这个路径中不应该包含中文、空格或其他特殊字符，注意这一点会为你将来减少很多不必要的麻烦。设置完成后，点击“Install”开始安装。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/f1f6da822e.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;安装成功会出现如下图所示的画面，安装成功的关键词是“successful”，如果安装失败，这里的单词会变成“failed”。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/bd6e97f840.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;安装完成后可以打开 Windows 的“命令行提示符”或 PowerShell，然后输入&lt;code&gt;python --version&lt;/code&gt;或&lt;code&gt;python -V&lt;/code&gt;来检查安装是否成功，这个命令是查看 Python 解释器的版本号。如果看到如下所示的画面，那么恭喜你，Python 环境已经安装成功了。这里我们建议再检查一下 Python 的包管理工具 pip 是否可用，对应的命令是&lt;code&gt;pip --version&lt;/code&gt;或&lt;code&gt;pip -V&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/44bcb7ae41.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：如果安装过程报错或提示安装失败，很有可能是你的 Windows 系统缺失了一些动态链接库文件或缺少必要的构建工具导致的。可以在&lt;a href=&quot;https://visualstudio.microsoft.com/zh-hans/downloads/&quot;&gt;微软官网&lt;/a&gt;下载“Visual Studio 2022 生成工具”进行修复，如下图所示。如果不方便在微软官网下载的，也可以使用下面的百度云盘链接来获取修复工具，链接: https://pan.baidu.com/s/1iNDnU5UVdDX5sKFqsiDg5Q 提取码: cjs3。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/9b12aa214a.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;上面下载的“Visual Studio 2022 生成工具”需要联网才能运行，运行后会出现如下图所示的画面，大家可以参考下图勾选对应的选项进行修复。修复过程需要联网下载对应的软件包，这个过程可能会比较耗时间，修复成功后可能会要求重启你的操作系统。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/b383dffed9.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;macOS环境&lt;/h5&gt;
&lt;p&gt;macOS 安装 Python 环境相较于 Windows 系统更为简单，我们从官方下载的安装包是一个&lt;code&gt;pkg&lt;/code&gt;文件，双击运行之后不断的点击“继续”就安装成功了，几乎不用做任何的设置和勾选，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/1bd567ac3b.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;安装完成后，可以在 macOS 的“终端”工具中输入&lt;code&gt;python3 --version&lt;/code&gt;命令来检查是否安装成功，注意这里的命令是&lt;code&gt;python3&lt;/code&gt;不是&lt;code&gt;python&lt;/code&gt;！！！然后我们再检查一下包管理工具，输入命令&lt;code&gt;pip3 --version&lt;/code&gt;，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/92b2f047d0.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;其他安装方式&lt;/h5&gt;
&lt;p&gt;有人可能会推荐新手直接安装 &lt;a href=&quot;https://www.anaconda.com/download/success&quot;&gt;Anaconda&lt;/a&gt;，因为 Anaconda 会帮助我们安装 Python 解释器以及一些常用的三方库，除此之外还提供了一些便捷的工具，特别适合萌新小白。我个人其实并不推荐新手直接使用 Anaconda，因为在安装 Anaconda 时你会莫名其妙安装了一大堆有用没用的三方库（占用比较多的硬盘空间），然后你的终端或命令提示符会被 Anaconda 篡改（每次启动自动激活虚拟环境），这些并不符合软件设计的&lt;strong&gt;最小惊讶原则&lt;/strong&gt;。其他关于 Anaconda 的小毛病此处就不再赘述了，如果非要使用 Anaconda，可以试试 Miniconda，它跟 Anaconda 在同一个下载页面。&lt;/p&gt;
&lt;p&gt;还有萌新小白经常会听到或说出，“我要写 Python 程序，安装一个 PyCharm 不就可以了吗？”。这里简单科普一下，PyCharm 只是一个辅助写 Python 代码的工具，它本身并不具备运行 Python 代码的能力，运行 Python 代码靠的是我们上面安装的 Python 解释器。当然，有些 PyCharm 版本在创建 Python 项目时，如果检测不到你电脑上的 Python 环境，也会提示你联网下载 Python 解释器。PyCharm 的安装和使用我们放在了下一课。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;总结一下我们学到的东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Python 语言很强大，可以做很多的事情，所以值得我们去学习。&lt;/li&gt;
&lt;li&gt;要使用 Python语言，首先得安装 Python 环境，也就是运行 Python 程序所需的 Python 解释器。&lt;/li&gt;
&lt;li&gt;Windows 系统可以在命令提示符或 PowerShell 中输入&lt;code&gt;python --version&lt;/code&gt;检查 Python 环境是否安装成功；macOS 系统可以在终端中输入&lt;code&gt;python3 --version&lt;/code&gt;进行检查。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;02 第一个Python程序&lt;/h2&gt;
&lt;p&gt;在上一课中，我们对 Python 语言的过去现在有了一些了解，我们准备好了运行 Python 程序所需要的解释器环境。相信大家已经迫不及待的想开始自己的 Python 编程之旅了，但是新问题来了，我们应该在什么地方书写 Python 程序，然后又怎么运行它呢？&lt;/p&gt;
&lt;h4&gt;编写代码的工具&lt;/h4&gt;
&lt;p&gt;下面我们为大家讲解几种可以编写和运行 Python 代码的工具，大家可以根据自己的需求来选择合适的工具。当然，对于初学者，我个人比较推荐使用 PyCharm，因为它不需要太多的配置也非常的强大，对新手还是很友好的。如果你也听说过或者喜欢 PyCharm，可以直接跳过下面对其他工具的介绍，直接快进到讲解 PyCharm 的地方。&lt;/p&gt;
&lt;h5&gt;默认的交互式环境&lt;/h5&gt;
&lt;p&gt;我们打开 Windows 的“命令提示符”或“PowerShell”工具，输入&lt;code&gt;python&lt;/code&gt;然后按下&lt;code&gt;Enter&lt;/code&gt;键，这个命令会把我们带到一个交互式环境中。所谓交互式环境，就是我们输入一行代码并按下&lt;code&gt;Enter&lt;/code&gt;键，代码马上会被执行，如果代码有产出结果，那么结果会被显示在窗口中，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python 3.10.10
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
&amp;gt;&amp;gt;&amp;gt; 2 * 3
6
&amp;gt;&amp;gt;&amp;gt; 2 + 3
5
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：使用 macOS 系统的用户需要打开“终端”工具，输入&lt;code&gt;python3&lt;/code&gt;进入交互式环境。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果希望退出交互式环境，可以在交互式环境中输入&lt;code&gt;quit()&lt;/code&gt;，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;更好的交互式环境 - IPython&lt;/h5&gt;
&lt;p&gt;上面说的交互式环境用户体验并不怎么好，大家使用一下就能感受到。我们可以用 IPython 来替换掉它，因为 IPython 提供了更为强大的编辑和交互功能。我们可以在命令提示符或终端中使用 Python 的包管理工具&lt;code&gt;pip&lt;/code&gt;来安装 IPython，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install ipython
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：在使用上面的命令安装 IPython 之前，可以先通过&lt;code&gt;pip config set global.index-url https://pypi.doubanio.com/simple&lt;/code&gt;命令或&lt;code&gt;pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/&lt;/code&gt;将下载源修改为国内的豆瓣镜像或清华镜像，否则下载安装的过程可能会非常的缓慢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来可以使用下面的命令启动 IPython，进入交互式环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipython
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：还有一个网页版的 IPython 名叫 Jupyter，我们在用得着它的地方再为大家介绍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;文本编辑神器 - Visual Studio Code&lt;/h5&gt;
&lt;p&gt;Visual Studio Code 是由微软开发能够在 Windows、 Linux 和 macOS 等操作系统上运行的代码编辑神器。它支持语法高亮、自动补全、多点编辑、运行调试等一系列便捷功能，而且能够支持多种编程语言。如果大家要选择一款高级文本编辑工具，强烈推荐 Visual Studio Code，关于它的&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;下载&lt;/a&gt;、安装和使用，有兴趣的读者可以自行研究。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/dc6a04cb9f.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h5&gt;集成开发环境 - PyCharm&lt;/h5&gt;
&lt;p&gt;如果用 Python 语言开发商业项目，我们推荐大家使用更为专业的工具 PyCharm。PyCharm 是由捷克一家名为 &lt;a href=&quot;https://www.jetbrains.com/&quot;&gt;JetBrains&lt;/a&gt; 的公司针对 Python 语言提供的集成开发环境（IDE)。所谓集成开发环境，通常是指提供了编写代码、运行代码、调试代码、分析代码、版本控制等一系列强大功能和便捷操作的开发工具，因此特别适合用于商业项目的开发。我们可以在 JetBrains 公司的官方网站上找到 PyCharm 的&lt;a href=&quot;https://github.com/jackfrued/Python-100-Days/blob/master/Day01-20/%3Chttps:/www.jetbrains.com/pycharm/download%3E&quot;&gt;下载链接&lt;/a&gt;，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/9f325b5101.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;官方提供了两个 PyCharm 的版本，一个是免费的社区版（Community Edition），功能相对弱小，但对于初学者来说是完全够用的；另一个是付费的专业版（Professional Edition），功能非常强大，但需要按年或按月支付费用，新用户可以免费试用30天时间。PyCharm 的安装没有任何难度，运行下载的安装程序，几乎全部使用默认设置进行安装就可以了。对于使用 Windows 系统的小伙伴，其中有一个步骤可以按照下图所示勾选“创建桌面快捷方式”和“在右键菜单中添加&quot;Open Folder as Project&quot;”就可以了。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/2f2fb11154.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;第一次运行 PyCharm 时，在提示你导入 PyCharm 设置的界面上直接选择“Do not import settings”，然后我们就可以看到如下图所示的欢迎界面。此处，我们可以先点击“Customize”选项对 PyCharm 做一些个性化的设置。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/d307e0858e.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们可以在“Projects”选项中点击“New Project”来创建一个新的项目，此处还可以“打开已有项目”或“从版本控制服务器（VCS）获取项目”，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/2f204f00cc.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;创建项目的时候需要指定项目的路径并创建”虚拟环境“，我们建议每个 Python 都在自己专属的虚拟环境中运行。如果你的系统上还没 Python 环境，那么 PyCharm 会提供官网的下载链接，当你点击“Create”按钮创建项目时，它会联网下载 Python 解释器，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/6076625cd8.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;当然，我们并不推荐这么做，因为我们在上一课已经安装过 Python 环境了。在系统有 Python 环境的情况下，PyCharm 通常会自动发现 Python 解释器的位置并以此为基础创建虚拟环境，所以大家看到的画面应该如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/0a29a315bc.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的截图来自于 Windows 系统，如果使用 macOS 系统，你看到的项目路径和 Python 解释器路径会跟上面有所不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建好项目后会出现如下图所示的画面，我们可以通过在项目文件夹上点击鼠标右键，选择“New”菜单下的“Python File”来创建一个 Python 文件，在给文件命名时建议使用英文字母和下划线的组合，创建好的 Python 文件会自动打开，进入可编辑的状态。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/b70a692372.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来，我们可以在代码窗口编写我们的 Python 代码。写好代码后，可以在窗口中点击鼠标右键，选择“Run”菜单项来运行代码，下面的“Run”窗口会显示代码的执行结果，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/3a16b4663c.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;到这里，我们的第一个 Python 程序已经运转起来了，很酷吧！对了，PyCharm 有一个叫“每日小贴士”的弹窗，会教给你一些使用 PyCharm 的小技巧，如下图所示。如果不需要，直接关闭就可以了；如果不希望它再次出现，在关闭前可以勾选“Don&apos;t show tips on startup”。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/aa37f8d44c.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;你好世界&lt;/h4&gt;
&lt;p&gt;按照行业惯例，我们学习任何一门编程语言写的第一个程序都是输出&lt;code&gt;hello, world&lt;/code&gt;，因为这段代码是伟大的丹尼斯·里奇（C 语言之父，和肯·汤普森一起开发了 Unix 操作系统）和布莱恩·柯尼汉（awk 语言的发明者）在他们的不朽著作《&lt;em&gt;The C Programming Language&lt;/em&gt;》中写的第一段代码，下面是对应的 Python 语言的版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;hello, world&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：上面代码中的圆括号、单引号都是在英文输入法状态下输入的，如果不小心写成了中文的圆括号或单引号，运行代码时会出现&lt;code&gt;SyntaxError: invalid character &apos;（&apos; (U+FF08)&lt;/code&gt;或&lt;code&gt;SyntaxError: invalid character &apos;‘&apos; (U+2018)&lt;/code&gt;这样的错误提示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码只有一个语句，在这个语句中，我们用到了一个名为&lt;code&gt;print&lt;/code&gt;的函数，它可以帮助我们输出指定的内容；&lt;code&gt;print&lt;/code&gt;函数圆括号中的&lt;code&gt;&apos;hello, world&apos;&lt;/code&gt;是一个字符串，它代表了一段文本内容；在 Python 语言中，我们可以用单引号或双引号来表示一个字符串。不同于 C、C++ 或 Java 这样的编程语言，Python 代码中的语句不需要用分号来表示结束，也就是说，如果我们想再写一条语句，只需要回车换行即可，代码如下所示。此外，Python 代码也不需要通过编写名为&lt;code&gt;main&lt;/code&gt;的入口函数来使其运行，提供入口函数是编写可执行的 C、C++ 或 Java 代码必须要做的事情，这一点很多程序员都不陌生，但是在 Python 语言中它并不是必要的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&apos;hello, world&apos;)
print(&apos;goodbye, world&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不使用 PyCharm 这样的集成开发环境，我们也可以直接调用 Python 解释器来运行 Python 程序。我们可以将上面的代码保存成一个名为&lt;code&gt;example01.py&lt;/code&gt;的文件，对于Windows 系统，我们假设该文件在&lt;code&gt;C:\code&lt;/code&gt;目录下，我们打开“命令提示符”或“PowerShell”并输入下面的命令就可以运行它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python C:\code\example01.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 macOS 系统，假设我们的文件在&lt;code&gt;/Users/Hao&lt;/code&gt;目录下，那么可以在终端中输入下面的命令来运行程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 /Users/Hao/example01.py
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：如果路径比较长，不愿意手动输入，我们可以通过拖拽的方式将文件直接拖到“命令提示符”或“终端”中，这样会自动输入完整的文件路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大家可以试着修改上面的代码，比如将单引号中的&lt;code&gt;hello, world&lt;/code&gt;换成其他内容或者多写几个这样的语句，看看会运行出怎样的结果。需要提醒大家的是，写 Python 代码时，最好每一行只写一条语句。虽然，我们可以使用&lt;code&gt;;&lt;/code&gt;作为分隔将多个语句写在一行中，但是这样做会让代码变得非常难看，不再具备良好的可读性。&lt;/p&gt;
&lt;h4&gt;注释你的代码&lt;/h4&gt;
&lt;p&gt;注释是编程语言的一个重要组成部分，用于在代码中解释代码的作用，从而达到增强代码可读性的目标。当然，我们也可以将代码中暂时不需要运行的代码段通过添加注释来去掉，这样当你需要重新使用这些代码的时候，去掉注释符号就可以了。简单的说，&lt;strong&gt;注释会让代码更容易看懂但不会影响代码的执行结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Python 中有两种形式的注释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单行注释：以&lt;code&gt;#&lt;/code&gt;和空格开头，可以注释掉从&lt;code&gt;#&lt;/code&gt;开始后面一整行的内容。&lt;/li&gt;
&lt;li&gt;多行注释：三个引号（通常用双引号）开头，三个引号结尾，通常用于添加多行说明性内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
第一个Python程序 - hello, world

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
# print(&apos;hello, world&apos;)
print(&quot;你好，世界！&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;到此，我们已经把第一个 Python 程序运行起来了，是不是很有成就感？！只要你坚持学习下去，再过一段时间，我们就可以用 Python 语言做更多更酷的事情。今时今日，编程就跟英语一样，对很多人来说都是一项必须要掌握的技能。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;03 Python语言中的变量&lt;/h2&gt;
&lt;p&gt;对于想学习编程的新手来说，有两个问题可能是他们很想知道的，其一是“什么是（计算机）程序”，其二是“写（计算机）程序能做什么”。先说说我对这两个问题的理解：&lt;strong&gt;程序是数据和指令的有序集合&lt;/strong&gt;，&lt;strong&gt;写程序就是用数据和指令控制计算机做我们想让它做的事情&lt;/strong&gt;。今时今日，为什么有那么多人选择用 Python 语言来写程序，因为 Python 语言足够简单和强大。相较于 C、C++、Java 这样的编程语言，Python 对初学者和非专业人士更加友好，很多问题在 Python 语言中都能找到简单优雅的解决方案。接下来，我们就从最基础的语言元素开始，带大家认识和使用 Python 语言。&lt;/p&gt;
&lt;h4&gt;一些常识&lt;/h4&gt;
&lt;p&gt;在开始系统的学习 Python 编程之前，我们先来科普一些计算机的基础知识。计算机的硬件系统通常由五大部件构成，包括：&lt;strong&gt;运算器&lt;/strong&gt;、&lt;strong&gt;控制器&lt;/strong&gt;、&lt;strong&gt;存储器&lt;/strong&gt;、&lt;strong&gt;输入设备&lt;/strong&gt;和&lt;strong&gt;输出设备&lt;/strong&gt;。其中，运算器和控制器放在一起就是我们常说的&lt;strong&gt;中央处理器&lt;/strong&gt;（CPU），它的功能是执行各种运算和控制指令。刚才我们提到过，程序是指令的集合，写程序就是将一系列的指令按照某种方式组织到一起，然后通过这些指令去控制计算机做我们想让它做的事情。存储器可以分为&lt;strong&gt;内部存储器&lt;/strong&gt;和&lt;strong&gt;外部存储器&lt;/strong&gt;，前者就是我们常说的内存，它是中央处理器可以直接寻址的存储空间，程序在执行的过程中，对应的数据和指令需要加载到内存中。输入设备和输出设备经常被统称为 I/O 设备，键盘、鼠标、麦克风、摄像头是典型的输入设备，而显示器、打印机、扬声器等则是典型的输出设备。目前，我们使用的计算机基本大多是遵循“冯·诺依曼体系结构”的计算机，这种计算机有两个关键点：一是&lt;strong&gt;将存储器与中央处理器分开&lt;/strong&gt;；二是&lt;strong&gt;将数据以二进制方式编码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;二进制是一种“逢二进一”的计数法，跟人类使用的“逢十进一”的计数法本质是一样的。人类因为有十根手指，所以使用了十进制计数法，在计数时十根手指用完之后，就只能用进位的方式来表示更大的数值。当然凡事都有例外，玛雅人可能是因为长年光着脚的原因，把脚趾头也都用上了，于是他们使用了二十进制的计数法。基于这样的计数方式，玛雅人使用的历法跟我们平常使用的历法就产生了差异。按照玛雅人的历法，2012 年是上一个所谓的“太阳纪”的最后一年，而 2013 年则是新的“太阳纪”的开始。后来这件事情还被以讹传讹的方式误传为“2012 年是玛雅人预言的世界末日”的荒诞说法。今天有很多人猜测，玛雅文明之所以发展缓慢跟使用了二十进制是有关系的。对于计算机来说，二进制在物理器件上最容易实现的，因为可以用高电压表示 1，用低电压表示 0。不是所有写程序的人都需要熟悉二进制，熟悉十进制与二进制、八进制、十六进制的转换，大多数时候我们即便不了解这些知识也能写程序。但是，我们必须知道，计算机是使用二进制计数的，不管什么样的数据，到了计算机内存中都是以二进制形态存在的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：关于二进制计数法以及它与其他进制如何相互转换，大家可以翻翻名为《计算机导论》或《计算机文化》的书，都能找到相应的知识，此处就不再进行赘述了，不清楚的读者可以自行研究。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;变量和类型&lt;/h4&gt;
&lt;p&gt;要想在计算机的内存中保存数据，首先得说一说变量这个概念。在编程语言中，&lt;strong&gt;变量是数据的载体&lt;/strong&gt;，简单的说就是一块用来保存数据的内存空间，&lt;strong&gt;变量的值可以被读取和修改&lt;/strong&gt;，这是所有运算和控制的基础。计算机能处理的数据有很多种类型，最常见的就是数值，除了数值之外还有文本、图像、音频、视频等各种各样的数据类型。虽然数据在计算机中都是以二进制形态存在的，但是我们可以用不同类型的变量来表示数据类型的差异。Python 语言中预设了多种数据类型，也允许我们自定义新的数据类型，这一点在后面会讲到。我们首先来了解几种 Python 中最为常用的数据类型。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;整型（&lt;code&gt;int&lt;/code&gt;）：Python 中可以处理任意大小的整数，而且支持二进制（如&lt;code&gt;0b100&lt;/code&gt;，换算成十进制是4）、八进制（如&lt;code&gt;0o100&lt;/code&gt;，换算成十进制是64）、十进制（&lt;code&gt;100&lt;/code&gt;）和十六进制（&lt;code&gt;0x100&lt;/code&gt;，换算成十进制是256）的表示法。运行下面的代码，看看会输出什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(0b100)  # 二进制整数
print(0o100)  # 八进制整数
print(100)    # 十进制整数
print(0x100)  # 十六进制整数
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浮点型（&lt;code&gt;float&lt;/code&gt;）：浮点数也就是小数，之所以称为浮点数，是因为按照科学记数法表示时，一个浮点数的小数点位置是可变的，浮点数除了数学写法（如&lt;code&gt;123.456&lt;/code&gt;）之外还支持科学计数法（如&lt;code&gt;1.23456e2&lt;/code&gt;，表示$\small{1.23456 \times 10^{2}}$）。运行下面的代码，看看会输出什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(123.456)    # 数学写法
print(1.23456e2)  # 科学计数法
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串型（&lt;code&gt;str&lt;/code&gt;）：字符串是以单引号或双引号包裹起来的任意文本，比如&lt;code&gt;&apos;hello&apos;&lt;/code&gt;和&lt;code&gt;&quot;hello&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;布尔型（&lt;code&gt;bool&lt;/code&gt;）：布尔型只有&lt;code&gt;True&lt;/code&gt;、&lt;code&gt;False&lt;/code&gt;两种值，要么是&lt;code&gt;True&lt;/code&gt;，要么是&lt;code&gt;False&lt;/code&gt;，可以用来表示现实世界中的“是”和“否”，命题的“真”和“假”，状况的“好”与“坏”，水平的“高”与“低”等等。如果一个变量的值只有两种状态，我们就可以使用布尔型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;变量命名&lt;/h4&gt;
&lt;p&gt;对于每个变量，我们都需要给它取一个名字，就如同我们每个人都有自己的名字一样。在 Python 中，变量命名需要遵循以下的规则和惯例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规则部分：
&lt;ul&gt;
&lt;li&gt;规则1：变量名由&lt;strong&gt;字母&lt;/strong&gt;、&lt;strong&gt;数字&lt;/strong&gt;和&lt;strong&gt;下划线&lt;/strong&gt;构成，数字不能开头。需要说明的是，这里说的字母指的是 Unicode 字符，Unicode 称为万国码，囊括了世界上大部分的文字系统，这也就意味着中文、日文、希腊字母等都可以作为变量名中的字符，但是一些特殊字符（如：&lt;code&gt;！&lt;/code&gt;、&lt;code&gt;@&lt;/code&gt;、&lt;code&gt;#&lt;/code&gt;等）是不能出现在变量名中的。我们强烈建议大家把这里说的字母理解为&lt;strong&gt;尽可能只使用英文字母&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;规则2：Python 是&lt;strong&gt;大小写敏感&lt;/strong&gt;的编程语言，简单的说就是大写的&lt;code&gt;A&lt;/code&gt;和小写的&lt;code&gt;a&lt;/code&gt;是两个不同的变量，这一条其实并不算规则，而是需要大家注意的地方。&lt;/li&gt;
&lt;li&gt;规则3：变量名&lt;strong&gt;不要跟 Python 的关键字重名&lt;/strong&gt;，&lt;strong&gt;尽可能避开 Python 的保留字&lt;/strong&gt;。这里的关键字是指在 Python 程序中有特殊含义的单词（如：&lt;code&gt;is&lt;/code&gt;、&lt;code&gt;if&lt;/code&gt;、&lt;code&gt;else&lt;/code&gt;、&lt;code&gt;for&lt;/code&gt;、&lt;code&gt;while&lt;/code&gt;、&lt;code&gt;True&lt;/code&gt;、&lt;code&gt;False&lt;/code&gt;等），保留字主要指 Python 语言内置函数、内置模块等的名字（如：&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;print&lt;/code&gt;、&lt;code&gt;input&lt;/code&gt;、&lt;code&gt;str&lt;/code&gt;、&lt;code&gt;math&lt;/code&gt;、&lt;code&gt;os&lt;/code&gt;等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;惯例部分：
&lt;ul&gt;
&lt;li&gt;惯例1：变量名通常使用&lt;strong&gt;小写英文字母&lt;/strong&gt;，&lt;strong&gt;多个单词用下划线进行连接&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;惯例2：受保护的变量用单个下划线开头。&lt;/li&gt;
&lt;li&gt;惯例3：私有的变量用两个下划线开头。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;惯例2和惯例3大家暂时不用管，讲到后面自然会明白的。当然，作为一个专业的程序员，给变量命名时做到&lt;strong&gt;见名知意&lt;/strong&gt;也是非常重要，这彰显了一个程序员的专业气质，很多开发岗位的面试也非常看重这一点。&lt;/p&gt;
&lt;h4&gt;变量的使用&lt;/h4&gt;
&lt;p&gt;下面通过例子来说明变量的类型和变量的使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
使用变量保存数据并进行加减乘除运算

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
a = 45        # 定义变量a，赋值45
b = 12        # 定义变量b，赋值12
print(a, b)   # 45 12
print(a + b)  # 57
print(a - b)  # 33
print(a * b)  # 540
print(a / b)  # 3.75
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Python 中可以使用&lt;code&gt;type&lt;/code&gt;函数对变量的类型进行检查。程序设计中函数的概念跟数学上函数的概念非常类似，数学上的函数相信大家并不陌生，它包括了函数名、自变量和因变量。如果暂时不理解函数这个概念也不要紧，我们会在后续的内容中专门讲解函数的定义和使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
使用type函数检查变量的类型

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
a = 100
b = 123.45
c = &apos;hello, world&apos;
d = True
print(type(a))  # &amp;lt;class &apos;int&apos;&amp;gt;
print(type(b))  # &amp;lt;class &apos;float&apos;&amp;gt;
print(type(c))  # &amp;lt;class &apos;str&apos;&amp;gt;
print(type(d))  # &amp;lt;class &apos;bool&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过 Python 内置的函数来改变变量的类型，下面是一些常用的和变量类型相关的函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int()&lt;/code&gt;：将一个数值或字符串转换成整数，可以指定进制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float()&lt;/code&gt;：将一个字符串（在可能的情况下）转换成浮点数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;str()&lt;/code&gt;：将指定的对象转换成字符串形式，可以指定编码方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chr()&lt;/code&gt;：将整数（字符编码）转换成对应的（一个字符的）字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ord()&lt;/code&gt;：将（一个字符的）字符串转换成对应的整数（字符编码）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面的例子为大家演示了 Python 中类型转换的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
变量的类型转换操作

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
a = 100
b = 123.45
c = &apos;123&apos;
d = &apos;100&apos;
e = &apos;123.45&apos;
f = &apos;hello, world&apos;
g = True
print(float(a))         # int类型的100转成float，输出100.0
print(int(b))           # float类型的123.45转成int，输出123
print(int(c))           # str类型的&apos;123&apos;转成int，输出123
print(int(c, base=16))  # str类型的&apos;123&apos;按十六进制转成int，输出291
print(int(d, base=2))   # str类型的&apos;100&apos;按二进制转成int，输出4
print(float(e))         # str类型的&apos;123.45&apos;转成float，输出123.45
print(bool(f))          # str类型的&apos;hello, world&apos;转成bool，输出True
print(int(g))           # bool类型的True转成int，输出1
print(chr(a))           # int类型的100转成str，输出&apos;d&apos;
print(ord(&apos;d&apos;))         # str类型的&apos;d&apos;转成int，输出100
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;str&lt;/code&gt;类型转&lt;code&gt;int&lt;/code&gt;类型时可以通过&lt;code&gt;base&lt;/code&gt;参数来指定进制，可以将字符串视为对应进制的整数进行转换。&lt;code&gt;str&lt;/code&gt;类型转成&lt;code&gt;bool&lt;/code&gt;类型时，只要字符串有内容，不是&lt;code&gt;&apos;&apos;&lt;/code&gt;或&lt;code&gt;&quot;&quot;&lt;/code&gt;，对应的布尔值都是&lt;code&gt;True&lt;/code&gt;。&lt;code&gt;bool&lt;/code&gt;类型转&lt;code&gt;int&lt;/code&gt;类型时，&lt;code&gt;True&lt;/code&gt;会变成&lt;code&gt;1&lt;/code&gt;，&lt;code&gt;False&lt;/code&gt;会变成&lt;code&gt;0&lt;/code&gt;。在 ASCII 字符集和 Unicode 字符集中， 字符&lt;code&gt;&apos;d&apos;&lt;/code&gt;对应的编码都是&lt;code&gt;100&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;在 Python 程序中，我们可以&lt;strong&gt;使用变量来保存数据&lt;/strong&gt;，&lt;strong&gt;变量有不同的类型&lt;/strong&gt;，常用的类型有&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;str&lt;/code&gt;和&lt;code&gt;bool&lt;/code&gt;。在有需要的情况下，可以通过 Python 内置的函数对变量进行类型转换。变量是可以做运算的，这是解决很多问题的先决条件，我们会在下一课中为大家详细介绍变量的运算。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;04 Python语言中的运算符&lt;/h2&gt;
&lt;p&gt;Python 语言支持很多种运算符，下面的表格按照运算符的优先级从高到低，对 Python 中的运算符进行了罗列。有了变量和运算符，我们就可以构造各种各样的表达式来解决实际问题。在计算机科学中，&lt;strong&gt;表达式是计算机程序中的句法实体，它由一个或多个常量、变量、函数和运算符组合而成，编程语言可以对其进行解释和计算以得到另一个值&lt;/strong&gt;。不理解这句话没有关系，但是一定要知道，不管使用什么样的编程语言，构造表达式都是非常重要的。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;运算符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;、&lt;code&gt;[:]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;索引、切片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;幂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;、&lt;code&gt;+&lt;/code&gt;、&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按位取反、正号、负号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;/&lt;/code&gt;、&lt;code&gt;%&lt;/code&gt;、&lt;code&gt;//&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;乘、除、模、整除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;、&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加、减&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右移、左移&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按位与&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;、`&lt;/td&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;小于等于、小于、大于、大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt;、&lt;code&gt;!=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等于、不等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is&lt;/code&gt;、&lt;code&gt;is not&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;身份运算符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;in&lt;/code&gt;、&lt;code&gt;not in&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成员运算符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not&lt;/code&gt;、&lt;code&gt;or&lt;/code&gt;、&lt;code&gt;and&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;逻辑运算符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;、&lt;code&gt;+=&lt;/code&gt;、&lt;code&gt;-=&lt;/code&gt;、&lt;code&gt;*=&lt;/code&gt;、&lt;code&gt;/=&lt;/code&gt;、&lt;code&gt;%=&lt;/code&gt;、&lt;code&gt;//=&lt;/code&gt;、&lt;code&gt;**=&lt;/code&gt;、&lt;code&gt;&amp;amp;=&lt;/code&gt;、&lt;code&gt;|=&lt;/code&gt;、&lt;code&gt;^=&lt;/code&gt;、&lt;code&gt;&amp;gt;&amp;gt;=&lt;/code&gt;、&lt;code&gt;&amp;lt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;赋值运算符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;： 所谓优先级就是在一个运算的表达式中，如果出现了多个运算符，应该先执行什么再执行什么的顺序。编写代码的时候，如果搞不清楚一个表达式中运算符的优先级，可以使用圆括号（小括号）来确保运算的执行顺序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;算术运算符&lt;/h4&gt;
&lt;p&gt;Python 中的算术运算符非常丰富，除了大家最为熟悉的加、减、乘、除之外，还有整除运算符、求模（求余数）运算符和求幂运算符。下面的例子为大家展示了算术运算符的使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
算术运算符

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
print(321 + 12)     # 加法运算，输出333
print(321 - 12)     # 减法运算，输出309
print(321 * 12)     # 乘法运算，输出3852
print(321 / 12)     # 除法运算，输出26.75
print(321 // 12)    # 整除运算，输出26
print(321 % 12)     # 求模运算，输出9
print(321 ** 12)    # 求幂运算，输出1196906950228928915420617322241
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算术运算需要先乘除后加减，这一点跟数学课本中讲的知识没有区别，也就是说乘除法的运算优先级是高于加减法的。如果还有求幂运算，求幂运算的优先级是高于乘除法的。如果想改变算术运算的执行顺序，可以使用英文输入法状态下的圆括号（小括号），写在圆括号中的表达式会被优先执行，如下面的例子所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
算术运算的优先级

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
print(2 + 3 * 5)           # 17
print((2 + 3) * 5)         # 25
print((2 + 3) * 5 ** 2)    # 125
print(((2 + 3) * 5) ** 2)  # 625
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;赋值运算符&lt;/h4&gt;
&lt;p&gt;赋值运算符应该是最为常见的运算符，它的作用是将右边的值赋给左边的变量。赋值运算符还可以跟上面的算术运算符放在一起，组合成复合赋值运算符，例如：&lt;code&gt;a += b&lt;/code&gt;相当于&lt;code&gt;a = a + b&lt;/code&gt;，&lt;code&gt;a *= a + 2&lt;/code&gt;相当于&lt;code&gt;a = a * (a + 2)&lt;/code&gt;。下面的例子演示了赋值运算符和复合赋值运算符的使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
赋值运算符和复合赋值运算符

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
a = 10
b = 3
a += b        # 相当于：a = a + b
a *= a + 2    # 相当于：a = a * (a + 2)
print(a)      # 大家算一下这里会输出什么
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;赋值运算构成的表达式本身不产生任何值，也就是说，如果你把一个赋值表达式放到&lt;code&gt;print&lt;/code&gt;函数中试图输出表达式的值，将会产生语法错误。为了解决这个问题，Python 3.8 中引入了一个新的赋值运算符&lt;code&gt;:=&lt;/code&gt;，我们称之为海象运算符，大家可以猜一猜它为什么叫这个名字。海象运算符也是将运算符右侧的值赋值给左边的变量，与赋值运算符不同的是，运算符右侧的值也是整个表达式的值，看看下面的代码大家就明白了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
海象运算符

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
# SyntaxError: invalid syntax
# print((a = 10))
# 海象运算符
print((a := 10))  # 10
print(a)          # 10
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：上面第 8 行代码如果不注释掉，运行代码会看到&lt;code&gt;SyntaxError: invalid syntax&lt;/code&gt;错误信息，注意，这行代码中我们给&lt;code&gt;a = 10&lt;/code&gt;加上了圆括号，如果不小心写成了&lt;code&gt;print(a = 10)&lt;/code&gt;，会看到&lt;code&gt;TypeError: &apos;a&apos; is an invalid keyword argument for print()&lt;/code&gt;错误信息，后面讲到函数的时候，大家就会明白这个错误提示是什么意思了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;比较运算符和逻辑运算符&lt;/h4&gt;
&lt;p&gt;比较运算符也称为关系运算符，包括&lt;code&gt;==&lt;/code&gt;、&lt;code&gt;!=&lt;/code&gt;、&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;&amp;gt;=&lt;/code&gt;，我相信大家一看就能懂。需要提醒的是比较相等用的是&lt;code&gt;==&lt;/code&gt;，请注意这里是两个等号，因为&lt;code&gt;=&lt;/code&gt;是赋值运算符，我们在上面刚刚讲到过。比较不相等用的是&lt;code&gt;!=&lt;/code&gt;，跟数学课本中使用的$\small{\neq}$并不相同，Python 2 中曾经使用过&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;来表示不等于，在 Python 3 中使用&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;会引发&lt;code&gt;SyntaxError&lt;/code&gt;（语法错误）。比较运算符会产生布尔值，要么是&lt;code&gt;True&lt;/code&gt;，要么是&lt;code&gt;False&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;逻辑运算符有三个，分别是&lt;code&gt;and&lt;/code&gt;、&lt;code&gt;or&lt;/code&gt;和&lt;code&gt;not&lt;/code&gt;。&lt;code&gt;and&lt;/code&gt;字面意思是“而且”，所以&lt;code&gt;and&lt;/code&gt;运算符会连接两个布尔值或者产生布尔值的表达式，如果两边的布尔值都是&lt;code&gt;True&lt;/code&gt;，那么运算的结果就是&lt;code&gt;True&lt;/code&gt;；左右两边的布尔值有一个是&lt;code&gt;False&lt;/code&gt;，最终的运算结果就是&lt;code&gt;False&lt;/code&gt;。当然，如果&lt;code&gt;and&lt;/code&gt;运算符左边的布尔值是&lt;code&gt;False&lt;/code&gt;，不管右边的布尔值是什么，最终的结果都是&lt;code&gt;False&lt;/code&gt;，这时运算符右边的布尔值会被跳过（专业的说法叫短路处理，如果&lt;code&gt;and&lt;/code&gt;右边是一个表达式，那么这个表达式不会执行）。&lt;code&gt;or&lt;/code&gt;字面意思是“或者”，所以&lt;code&gt;or&lt;/code&gt;运算符也会连接两个布尔值或产生布尔值的表达式，如果两边的布尔值有任意一个是&lt;code&gt;True&lt;/code&gt;，那么最终的结果就是&lt;code&gt;True&lt;/code&gt;。当然，&lt;code&gt;or&lt;/code&gt;运算符也是有短路功能的，当它左边的布尔值为&lt;code&gt;True&lt;/code&gt;的情况下，右边的布尔值会被短路（如果&lt;code&gt;or&lt;/code&gt;右边是一个表达式，那么这个表达式不会执行）。&lt;code&gt;not&lt;/code&gt;运算符的后面可以跟一个布尔值，如果&lt;code&gt;not&lt;/code&gt;后面的布尔值或表达式是&lt;code&gt;True&lt;/code&gt;，那么运算的结果就是&lt;code&gt;False&lt;/code&gt;；如果&lt;code&gt;not&lt;/code&gt;后面的布尔值或表达式是&lt;code&gt;False&lt;/code&gt;，那么运算的结果就是&lt;code&gt;True&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
比较运算符和逻辑运算符的使用

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
flag0 = 1 == 1
flag1 = 3 &amp;gt; 2
flag2 = 2 &amp;lt; 1
flag3 = flag1 and flag2
flag4 = flag1 or flag2
flag5 = not flag0
print(&apos;flag0 =&apos;, flag0)     # flag0 = True
print(&apos;flag1 =&apos;, flag1)     # flag1 = True
print(&apos;flag2 =&apos;, flag2)     # flag2 = False
print(&apos;flag3 =&apos;, flag3)     # flag3 = False
print(&apos;flag4 =&apos;, flag4)     # flag4 = True
print(&apos;flag5 =&apos;, flag5)     # flag5 = False
print(flag1 and not flag2)  # True
print(1 &amp;gt; 2 or 2 == 3)      # False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：比较运算符的优先级高于赋值运算符，所以上面的&lt;code&gt;flag0 = 1 == 1&lt;/code&gt;先做&lt;code&gt;1 == 1&lt;/code&gt;产生布尔值&lt;code&gt;True&lt;/code&gt;，再将这个值赋值给变量&lt;code&gt;flag0&lt;/code&gt;。&lt;code&gt;print&lt;/code&gt;函数可以输出多个值，多个值之间可以用&lt;code&gt;,&lt;/code&gt;进行分隔，输出的内容默认以空格分开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;运算符和表达式应用&lt;/h4&gt;
&lt;h5&gt;例子1：华氏温度转摄氏温度&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要求&lt;/strong&gt;：输入华氏温度将其转换为摄氏温度，华氏温度到摄氏温度的转换公式为： $\small{C = (F - 32) / 1.8}$ 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
将华氏温度转换为摄氏温度

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
f = float(input(&apos;请输入华氏温度: &apos;))
c = (f - 32) / 1.8
print(&apos;%.1f华氏度 = %.1f摄氏度&apos; % (f, c))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;input&lt;/code&gt;函数用于从键盘接收用户输入，由于输入的都是字符串，如果想处理成浮点小数来做后续的运算，可以用我们上一课讲解的类型转换的方法，用&lt;code&gt;float&lt;/code&gt;函数将&lt;code&gt;str&lt;/code&gt;类型处理成&lt;code&gt;float&lt;/code&gt;类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码中，我们对&lt;code&gt;print&lt;/code&gt;函数输出的内容进行了格式化处理，&lt;code&gt;print&lt;/code&gt;输出的字符串中有两个&lt;code&gt;%.1f&lt;/code&gt;占位符，这两个占位符会被&lt;code&gt;%&lt;/code&gt;之后的&lt;code&gt;(f, c)&lt;/code&gt;中的两个&lt;code&gt;float&lt;/code&gt;类型的变量值给替换掉，浮点数小数点后保留1位有效数字。如果字符串中有&lt;code&gt;%d&lt;/code&gt;占位符，那么我们会用&lt;code&gt;int&lt;/code&gt;类型的值替换掉它，如果字符串中有&lt;code&gt;%s&lt;/code&gt;占位符，那么它会被&lt;code&gt;str&lt;/code&gt;类型的值替换掉。&lt;/p&gt;
&lt;p&gt;除了上面格式化输出的方式外，Python 中还可以用下面的办法来格式化输出，我们给出一个带占位符的字符串，字符串前面的&lt;code&gt;f&lt;/code&gt;表示这个字符串是需要格式化处理的，其中的&lt;code&gt;{f:.1f}&lt;/code&gt;和&lt;code&gt;{c:.1f}&lt;/code&gt;可以先看成是&lt;code&gt;{f}&lt;/code&gt;和&lt;code&gt;{c}&lt;/code&gt;，表示输出时会用变量&lt;code&gt;f&lt;/code&gt;和变量&lt;code&gt;c&lt;/code&gt;的值替换掉这两个占位符，后面的&lt;code&gt;:.1f&lt;/code&gt;表示这是一个浮点数，小数点后保留1位有效数字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
将华氏温度转换为摄氏温度

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
f = float(input(&apos;请输入华氏温度: &apos;))
c = (f - 32) / 1.8
print(f&apos;{f:.1f}华氏度 = {c:.1f}摄氏度&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;例子2：计算圆的周长和面积&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要求&lt;/strong&gt;：输入一个圆的半径（$\small{r}$），计算出它的周长（ $\small{2 \pi r}$ ）和面积（ $\small{\pi r^{2}}$ ）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入半径计算圆的周长和面积

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
radius = float(input(&apos;请输入圆的半径: &apos;))
perimeter = 2 * 3.1416 * radius
area = 3.1416 * radius * radius
print(&apos;周长: %.2f&apos; % perimeter)
print(&apos;面积: %.2f&apos; % area)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 中有一个名为&lt;code&gt;math&lt;/code&gt; 的内置模块，该模块中定义了名为&lt;code&gt;pi&lt;/code&gt;的变量，它的值就是圆周率。如果要使用 Python 内置的这个&lt;code&gt;pi&lt;/code&gt;，我们可以对上面的代码稍作修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入半径计算圆的周长和面积

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
import math

radius = float(input(&apos;请输入圆的半径: &apos;))
perimeter = 2 * math.pi * radius
area = math.pi * radius ** 2
print(f&apos;周长: {perimeter:.2f}&apos;)
print(f&apos;面积: {area:.2f}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;import math&lt;/code&gt;表示导入&lt;code&gt;math&lt;/code&gt;模块，导入该模块以后，才能用&lt;code&gt;math.pi&lt;/code&gt;得到圆周率的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里其实还有一种格式化输出的方式，是 Python 3.8 中增加的新特性，大家直接看下面的代码就明白了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入半径计算圆的周长和面积

Version: 1.2
Author: 骆昊
&quot;&quot;&quot;
import math

radius = float(input(&apos;请输入圆的半径: &apos;))  # 输入: 5.5
perimeter = 2 * math.pi * radius
area = math.pi * radius ** 2
print(f&apos;{perimeter = :.2f}&apos;)  # 输出：perimeter = 34.56
print(f&apos;{area = :.2f}&apos;)       # 输出：area = 95.03
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：假如变量&lt;code&gt;a&lt;/code&gt;的值是&lt;code&gt;9.87&lt;/code&gt;，那么字符串&lt;code&gt;f&apos;{a = }&apos;&lt;/code&gt;的值是&lt;code&gt;a = 9.87&lt;/code&gt;；而字符串&lt;code&gt;f&apos;{a = :.1f}&apos;&lt;/code&gt;的值是&lt;code&gt;a = 9.9&lt;/code&gt;。这种格式化输出的方式会同时输出变量名和变量值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子3：判断闰年&lt;/h5&gt;
&lt;p&gt;要求：输入一个 1582 年以后的年份，判断该年份是不是闰年。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入年份，闰年输出True，平年输出False

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
year = int(input(&apos;请输入年份: &apos;))
is_leap = year % 4 == 0 and year % 100 != 0 or year % 400 == 0
print(f&apos;{is_leap = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：对于格里历（Gregorian calendar），即今天我们使用的公历，判断闰年的规则是：1. 公元年份非 4 的倍数是平年；2. 公元年份为 4 的倍数但非 100 的倍数是闰年；3. 公元年份为 400 的倍数是闰年。格里历是由教皇格里高利十三世在 1582 年 10 月引入的，作为对儒略历（Julian calendar）的修改和替代，我们在输入年份时要注意这一点。上面的代码通过&lt;code&gt;%&lt;/code&gt;来判断&lt;code&gt;year&lt;/code&gt;是不是&lt;code&gt;4&lt;/code&gt;的倍数、&lt;code&gt;100&lt;/code&gt;的倍数、&lt;code&gt;400&lt;/code&gt;的倍数，然后用&lt;code&gt;and&lt;/code&gt;和&lt;code&gt;or&lt;/code&gt;运算符将三个条件组装在一起，前两个条件要同时满足，第三个条件跟前两个条件的组合只需满足其中之一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;通过上面的讲解和例子，相信大家已经感受到了运算符和表达式的力量。实际编程中的很多问题，都需通过构造表达式来解决，所以变量、运算符、表达式对于任何一门编程语言都是极为重要的基础。如果本节课的内容有什么不理解的地方，一定不要着急进入下一课，先在评论区留言讨论，我会及时解答大家的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;05 分支结构&lt;/h2&gt;
&lt;p&gt;迄今为止，我们写的 Python 程序都是一条一条语句按顺序向下执行的，这种代码结构叫做顺序结构。然而仅有顺序结构并不能解决所有的问题，比如我们设计一个游戏，游戏第一关的过关条件是玩家获得 1000 分，那么在第一关完成后，我们要根据玩家得到的分数来决定是进入第二关，还是告诉玩家“Game Over”（游戏结束）。在这种场景下，我们的代码就会产生两个分支，而且只有一个会被执行。类似的场景还有很多，我们将这种结构称之为“分支结构”或“选择结构”。给大家一分钟的时间，你应该可以想到至少 5 个以上类似的例子，赶紧试一试吧！&lt;/p&gt;
&lt;h4&gt;使用if和else构造分支结构&lt;/h4&gt;
&lt;p&gt;在 Python 中，构造分支结构最常用的是&lt;code&gt;if&lt;/code&gt;、&lt;code&gt;elif&lt;/code&gt;和&lt;code&gt;else&lt;/code&gt;三个关键字。所谓&lt;strong&gt;关键字&lt;/strong&gt;就是编程语言中有特殊含义的单词，很显然你不能够使用它作为变量名。当然，我们并不是每次构造分支结构都会把三个关键字全部用上，我们通过例子加以说明。例如我们要写一个身体质量指数（BMI）的计算器。身体质量质数也叫体质指数，是国际上常用的衡量人体胖瘦程度以及是否健康的一个指标，计算公式如下所示。通常认为 $\small{18.5 \le BMI &amp;lt; 24}$ 是正常范围， $\small{BMI &amp;lt; 18.5}$ 说明体重过轻， $\small{BMI \ge 24}$ 说明体重过重， $\small{BMI \ge 27}$ 就属于肥胖的范畴了。&lt;/p&gt;
&lt;p&gt;$$
BMI = \frac{体重}{身高^{2}}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面公式中的体重以千克（kg）为单位，身高以米（m）为单位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
BMI计算器

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
height = float(input(&apos;身高(cm)：&apos;))
weight = float(input(&apos;体重(kg)：&apos;))
bmi = weight / (height / 100) ** 2
print(f&apos;{bmi = :.1f}&apos;)
if 18.5 &amp;lt;= bmi &amp;lt; 24:
    print(&apos;你的身材很棒！&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：&lt;code&gt;if&lt;/code&gt;语句的最后面有一个&lt;code&gt;:&lt;/code&gt;，它是用英文输入法输入的冒号；程序中输入的&lt;code&gt;&apos;&lt;/code&gt;、&lt;code&gt;&quot;&lt;/code&gt;、&lt;code&gt;=&lt;/code&gt;、&lt;code&gt;(&lt;/code&gt;、&lt;code&gt;)&lt;/code&gt;等特殊字符，都是在英文输入法状态下输入的，这一点之前已经提醒过大家了。很多初学者经常会忽略这一点，等到执行代码时，就会看到一大堆错误提示。当然，认真读一下错误提示还是很容易发现哪里出了问题，但是&lt;strong&gt;强烈建议&lt;/strong&gt;大家在写代码的时候&lt;strong&gt;切换到英文输入法&lt;/strong&gt;，这样可以避免很多不必要的麻烦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的代码中，我们在计算和输出 BMI 之后，加上了一段分支结构，如果满足 $\small{18.5 \le BMI &amp;lt; 24}$ ，程序会输出“你的身材很棒！”，但是如果不满足条件，这段输出就没有了。这就是刚才提到的，代码可以有不同的执行路径，有些代码不一定会执行到。我们在&lt;code&gt;if&lt;/code&gt;关键字的后面给出了一个表达式&lt;code&gt;18.5 &amp;lt;= bmi &amp;lt; 24&lt;/code&gt;，之前我们说过，关系运算会产生布尔值，如果&lt;code&gt;if&lt;/code&gt;后面的布尔值为&lt;code&gt;True&lt;/code&gt;，那么&lt;code&gt;if&lt;/code&gt;语句下方，有四个空格缩进的&lt;code&gt;print(&apos;你的身材很棒！&apos;)&lt;/code&gt;就会被执行。我们先输入几组数据运行上面的代码，如下所示。&lt;/p&gt;
&lt;p&gt;第一组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：68
bmi = 22.2
你的身材很棒！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：95
bmi = 31.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：50
bmi = 16.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有第一组输入的身高和体重计算出的 BMI 在 18.5 到 24 这个范围值内，所以触发了&lt;code&gt;if&lt;/code&gt;条件，输出了“你的身材很棒”。需要说明的是，不同于 C、C++、Java 等编程语言，Python 中没有用花括号来构造代码块而是&lt;strong&gt;使用缩进的方式来表示代码的层次结构&lt;/strong&gt;，如果&lt;code&gt;if&lt;/code&gt;条件成立的情况下需要执行多条语句，只要保持多条语句具有相同的缩进就可以了。换句话说，若干行连续的语句如果保持了相同的缩进，那么它们就属于同一个&lt;strong&gt;代码块&lt;/strong&gt;，相当于是一个执行的整体。缩进可以使用任意数量的空格，但&lt;strong&gt;通常使用4个空格&lt;/strong&gt;，强烈建议大家&lt;strong&gt;不要使用制表键（Tab键）来缩进代码&lt;/strong&gt;，如果你已经习惯了这么做，可以设置你的代码编辑器自动将 1 个制表键变成 4 个空格，很多代码编辑器都支持这项功能，PyCharm 中默认也是这样设定的。还有一点，在 C、C++、Java 等编程语言中，&lt;code&gt;18.5 &amp;lt;= bmi &amp;lt; 24&lt;/code&gt;要写成两个条件&lt;code&gt;bmi &amp;gt;= 18.5&lt;/code&gt;和&lt;code&gt;bmi &amp;lt; 24&lt;/code&gt;，然后把两个条件用与运算符连接起来，Python 中也可以这么做，例如刚才的&lt;code&gt;if&lt;/code&gt;语句也可以写成&lt;code&gt;if bmi &amp;gt;= 18.5 and bmi &amp;lt; 24:&lt;/code&gt;，但是没有必要，难道&lt;code&gt;if 18.5 &amp;lt;= bmi &amp;lt; 24:&lt;/code&gt;这个写法它不香吗？下面用 Java 代码做了同样的事情，看不懂 Java 代码没关系，感受一下它和 Python 语法的区别就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Scanner;

class Test {

    public static void main(String[] args) {
        try (Scanner sc = new Scanner(System.in)) {
            System.out.print(&quot;身高(cm): &quot;);
            double height = sc.nextDouble();
            System.out.print(&quot;体重(kg): &quot;);
            double weight = sc.nextDouble();
            double bmi = weight / Math.pow(height / 100, 2);
            System.out.printf(&quot;bmi = %.1f\n&quot;, bmi);
            if (bmi &amp;gt;= 18.5 &amp;amp;&amp;amp; bmi &amp;lt; 24) {
                System.out.println(&quot;你的身材很棒！&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面就是 BMI 计算器 1.0 版本对应的 Java 代码，很多人喜欢 Python 语言不是没有道理的，通常它都能用更少的代码解决同样的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来，我们对上面的代码稍作修改，在 BMI 不满足 $\small{18.5 \le BMI &amp;lt; 24}$ 的情况下，也给出相信的提示信息。我们可以在&lt;code&gt;if&lt;/code&gt;代码块的后面增加一个&lt;code&gt;else&lt;/code&gt;代码块，它会在&lt;code&gt;if&lt;/code&gt;语句给出的条件没有达成时执行，如下所示。很显然，&lt;code&gt;if&lt;/code&gt;下面的&lt;code&gt;print(&apos;你的身材很棒！&apos;)&lt;/code&gt;和&lt;code&gt;else&lt;/code&gt;下面的&lt;code&gt;print(&apos;你的身材不够标准哟！&apos;)&lt;/code&gt;只有一个会被执行到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
BMI计算器

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
height = float(input(&apos;身高(cm)：&apos;))
weight = float(input(&apos;体重(kg)：&apos;))
bmi = weight / (height / 100) ** 2
print(f&apos;{bmi = :.1f}&apos;)
if 18.5 &amp;lt;= bmi &amp;lt; 24:
    print(&apos;你的身材很棒！&apos;)
else:
    print(&apos;你的身材不够标准哟！&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要给出更为准确的提示信息，我们可以再次修改上面的代码，通过&lt;code&gt;elif&lt;/code&gt;关键字为上面的分支结构增加更多的分支，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
BMI计算器

Version: 1.2
Author: 骆昊
&quot;&quot;&quot;
height = float(input(&apos;身高(cm)：&apos;))
weight = float(input(&apos;体重(kg)：&apos;))
bmi = weight / (height / 100) ** 2
print(f&apos;{bmi = :.1f}&apos;)
if bmi &amp;lt; 18.5:
    print(&apos;你的体重过轻！&apos;)
elif bmi &amp;lt; 24:
    print(&apos;你的身材很棒！&apos;)
elif bmi &amp;lt; 27:
    print(&apos;你的体重过重！&apos;)
elif bmi &amp;lt; 30:
    print(&apos;你已轻度肥胖！&apos;)
elif bmi &amp;lt; 35:
    print(&apos;你已中度肥胖！&apos;)
else:
    print(&apos;你已重度肥胖！&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再用刚才的三组数据来测试下上面的代码，看看会得到怎样的结果。&lt;/p&gt;
&lt;p&gt;第一组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：68
bmi = 22.2
你的身材很棒！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：95
bmi = 31.0
你已中度肥胖！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三组输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;身高(cm)：175
体重(kg)：50
bmi = 16.3
你的体重过轻！
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用match和case构造分支结构&lt;/h4&gt;
&lt;p&gt;Python 3.10 中增加了一种新的构造分支结构的方式，通过使用&lt;code&gt;match&lt;/code&gt;和&lt;code&gt;case&lt;/code&gt; 关键字，我们可以轻松的构造出多分支结构。Python 的官方文档在介绍这个新语法时，举了一个 HTTP 响应状态码识别的例子（根据 HTTP 响应状态输出对应的描述），非常有意思。如果不知道什么是 HTTP 响应状态吗，可以看看 MDN 上面的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status&quot;&gt;文档&lt;/a&gt;。下面我们对官方文档上的示例稍作修改，为大家讲解这个语法，先看看下面用&lt;code&gt;if-else&lt;/code&gt;结构实现的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;status_code = int(input(&apos;响应状态码: &apos;))
if status_code == 400:
    description = &apos;Bad Request&apos;
elif status_code == 401:
    description = &apos;Unauthorized&apos;
elif status_code == 403:
    description = &apos;Forbidden&apos;
elif status_code == 404:
    description = &apos;Not Found&apos;
elif status_code == 405:
    description = &apos;Method Not Allowed&apos;
elif status_code == 418:
    description = &apos;I am a teapot&apos;
elif status_code == 429:
    description = &apos;Too many requests&apos;
else:
    description = &apos;Unknown status Code&apos;
print(&apos;状态码描述:&apos;, description)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;响应状态码: 403
状态码描述: Forbidden
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是使用&lt;code&gt;match-case&lt;/code&gt;语法实现的代码，虽然作用完全相同，但是代码显得更加简单优雅。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;status_code = int(input(&apos;响应状态码: &apos;))
match status_code:
    case 400: description = &apos;Bad Request&apos;
    case 401: description = &apos;Unauthorized&apos;
    case 403: description = &apos;Forbidden&apos;
    case 404: description = &apos;Not Found&apos;
    case 405: description = &apos;Method Not Allowed&apos;
    case 418: description = &apos;I am a teapot&apos;
    case 429: description = &apos;Too many requests&apos;
    case _: description = &apos;Unknown Status Code&apos;
print(&apos;状态码描述:&apos;, description)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：带有&lt;code&gt;_&lt;/code&gt;的&lt;code&gt;case&lt;/code&gt;语句在代码中起到通配符的作用，如果前面的分支都没有匹配上，代码就会来到&lt;code&gt;case _&lt;/code&gt;。&lt;code&gt;case _&lt;/code&gt;的是可选的，并非每种分支结构都要给出通配符选项。如果分支中出现了&lt;code&gt;case _&lt;/code&gt;，它只能放在分支结构的最后面，如果它的后面还有其他的分支，那么这些分支将是不可达的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，&lt;code&gt;match-case&lt;/code&gt;语法还有很多高级玩法，其中有一个合并模式可以先教给大家。例如，我们要将响应状态码&lt;code&gt;401&lt;/code&gt;、&lt;code&gt;403&lt;/code&gt;和&lt;code&gt;404&lt;/code&gt;归入一个分支，&lt;code&gt;400&lt;/code&gt;和&lt;code&gt;405&lt;/code&gt;归入到一个分支，其他保持不变，代码还可以这么写。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;status_code = int(input(&apos;响应状态码: &apos;))
match status_code:
    case 400 | 405: description = &apos;Invalid Request&apos;
    case 401 | 403 | 404: description = &apos;Not Allowed&apos;
    case 418: description = &apos;I am a teapot&apos;
    case 429: description = &apos;Too many requests&apos;
    case _: description = &apos;Unknown Status Code&apos;
print(&apos;状态码描述:&apos;, description)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;响应状态码: 403
状态码描述: Not Allowed
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分支结构的应用&lt;/h4&gt;
&lt;h5&gt;例子1：分段函数求值&lt;/h5&gt;
&lt;p&gt;有如下所示的分段函数，要求输入&lt;code&gt;x&lt;/code&gt;，计算出&lt;code&gt;y&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;$$
y = \begin{cases} 3x - 5, &amp;amp; (x \gt 1) \\ x + 2, &amp;amp; (-1 \le x \le 1) \\ 5x + 3, &amp;amp; (x \lt -1) \end{cases}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
分段函数求值

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
x = float(input(&apos;x = &apos;))
if x &amp;gt; 1:
    y = 3 * x - 5
elif x &amp;gt;= -1:
    y = x + 2
else:
    y = 5 * x + 3
print(f&apos;{y = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据实际开发的需要，分支结构是可以嵌套的，也就是说在分支结构的&lt;code&gt;if&lt;/code&gt;、&lt;code&gt;elif&lt;/code&gt;或&lt;code&gt;else&lt;/code&gt;代码块中还可以再次引入分支结构。例如&lt;code&gt;if&lt;/code&gt;条件成立表示玩家过关，但过关以后还要根据你获得宝物或者道具的数量对你的表现给出评价（比如点亮一颗、两颗或三颗星星），那么我们就需要在&lt;code&gt;if&lt;/code&gt;的内部再构造一个新的分支结构。同理，我们在&lt;code&gt;elif&lt;/code&gt;和&lt;code&gt;else&lt;/code&gt;中也可以构造新的分支，我们称之为嵌套的分支结构。按照这样的思路，上面的分段函数求值也可以用下面的代码来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
分段函数求值

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
x = float(input(&apos;x = &apos;))
if x &amp;gt; 1:
    y = 3 * x - 5
else:
    if x &amp;gt;= -1:
        y = x + 2
    else:
        y = 5 * x + 3
print(f&apos;{y = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大家可以自己感受和评判一下上面两种写法哪一种更好。在“&lt;a href=&quot;https://zhuanlan.zhihu.com/p/111843067&quot;&gt;&lt;strong&gt;Python 之禅&lt;/strong&gt;&lt;/a&gt;”中有这么一句话：“&lt;strong&gt;Flat is better than nested&lt;/strong&gt;”。之所以认为“扁平化”的代码更好，是因为代码嵌套的层次如果很多，会严重的影响代码的可读性。所以，我个人更推荐大家使用第一种写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子2：百分制成绩转换成等级&lt;/h5&gt;
&lt;p&gt;要求：如果输入的成绩在90分以上（含90分），则输出&lt;code&gt;A&lt;/code&gt;；输入的成绩在80分到90分之间（不含90分），则输出&lt;code&gt;B&lt;/code&gt;；输入的成绩在70分到80分之间（不含80分），则输出&lt;code&gt;C&lt;/code&gt;；输入的成绩在60分到70分之间（不含70分），则输出&lt;code&gt;D&lt;/code&gt;；输入的成绩在60分以下，则输出&lt;code&gt;E&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
百分制成绩转换为等级制成绩

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
score = float(input(&apos;请输入成绩: &apos;))
if score &amp;gt;= 90:
    grade = &apos;A&apos;
elif score &amp;gt;= 80:
    grade = &apos;B&apos;
elif score &amp;gt;= 70:
    grade = &apos;C&apos;
elif score &amp;gt;= 60:
    grade = &apos;D&apos;
else:
    grade = &apos;E&apos;
print(f&apos;{grade = }&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;例子3：计算三角形的周长和面积。&lt;/h5&gt;
&lt;p&gt;要求：输入三条边的长度，如果能构成三角形就计算周长和面积；否则给出“不能构成三角形”的提示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
计算三角形的周长和面积

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
a = float(input(&apos;a = &apos;))
b = float(input(&apos;b = &apos;))
c = float(input(&apos;c = &apos;))
if a + b &amp;gt; c and a + c &amp;gt; b and b + c &amp;gt; a:
    perimeter = a + b + c
    print(f&apos;周长: {perimeter}&apos;)
    s = perimeter / 2
    area = (s * (s - a) * (s - b) * (s - c)) ** 0.5
    print(f&apos;面积: {area}&apos;)
else:
    print(&apos;不能构成三角形&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 上面的&lt;code&gt;if&lt;/code&gt; 条件表示任意两边之和大于第三边，这是构成三角形的必要条件。当这个条件成立时，我们要计算并输出周长和面积，所以&lt;code&gt;if&lt;/code&gt;下方有五条语句都保持了相同的缩进，它们是一个整体，只要&lt;code&gt;if&lt;/code&gt;条件成立，它们都会被执行，这就是我们之前提到的代码块的概念。另外，上面计算三角形面积的公式叫做海伦公式，假设有一个三角形，边长分别为 $\small{a}$ 、 $\small{b}$ 、 $\small{c}$ ，那么三角的面积 $\small{A}$ 可以由公式 $\small{A = \sqrt{s(s-a)(s-b)(s-c)}}$ 得到，其中， $s=\frac{a + b + c}{2}$ 表示半周长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;学会了 Python 中的分支结构和循环结构，我们就可以解决很多实际的问题了。这一节课相信已经帮助大家掌握了构造分支结构的方法，下一节课我们为大家介绍循环结构，学完这两次课你一定会发现，你能写出很多很有意思的代码，继续加油吧！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;06 循环结构&lt;/h2&gt;
&lt;p&gt;我们在写程序的时候，极有可能遇到需要重复执行某条或某些指令的场景，例如我们需要每隔1秒钟在屏幕上输出一次“hello, world”并持续输出一个小时。如下所示的代码可以完成一次这样的操作，如果要持续输出一个小时，我们就需要把这段代码写3600遍，你愿意这么做吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

print(&apos;hello, world&apos;)
time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：Python 内置&lt;code&gt;time&lt;/code&gt;模块的&lt;code&gt;sleep&lt;/code&gt;函数可以实现程序的休眠，参数&lt;code&gt;1&lt;/code&gt;表示休眠的秒数，可以使用&lt;code&gt;int&lt;/code&gt;或&lt;code&gt;float&lt;/code&gt;类型，例如&lt;code&gt;0.05&lt;/code&gt;表示&lt;code&gt;50&lt;/code&gt;毫秒。关于函数和模块的知识，我们在后续的课程中会为大家讲解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了应对上述场景中的问题，我们可以在 Python 程序中使用循环结构。所谓循环结构，就是程序中控制某条或某些指令重复执行的结构。有了这样的结构，刚才的代码就不需要写 3600 遍，而是写一遍然后放到循环结构中重复 3600 次。在 Python 语言中构造循环结构有两种做法，一种是&lt;code&gt;for-in&lt;/code&gt;循环，另一种是&lt;code&gt;while&lt;/code&gt;循环。&lt;/p&gt;
&lt;h4&gt;for-in循环&lt;/h4&gt;
&lt;p&gt;如果明确知道循环执行的次数，我们推荐使用&lt;code&gt;for-in&lt;/code&gt;循环，例如上面说的那个重复 3600 次的场景，我们可以用下面的代码来实现。 注意，被&lt;code&gt;for-in&lt;/code&gt;循环控制的代码块也是通过缩进的方式来构造，这一点跟分支结构中构造代码块的做法是一样的。我们被&lt;code&gt;for-in&lt;/code&gt;循环控制的代码块称为循环体，通常循环体中的语句会根据循环的设定被重复执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
每隔1秒输出一次“hello, world”，持续1小时

Author: 骆昊
Version: 1.0
&quot;&quot;&quot;
import time

for i in range(3600):
    print(&apos;hello, world&apos;)
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，上面代码中的&lt;code&gt;range(3600)&lt;/code&gt;可以构造出一个从&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;3599&lt;/code&gt;的范围，当我们把这样一个范围放到&lt;code&gt;for-in&lt;/code&gt;循环中，就可以通过前面的循环变量&lt;code&gt;i&lt;/code&gt;依次取出从&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;3599&lt;/code&gt;的整数，这就会让&lt;code&gt;for-in&lt;/code&gt;代码块中的语句重复 3600 次。当然，&lt;code&gt;range&lt;/code&gt;的用法非常灵活，下面的清单给出了使用&lt;code&gt;range&lt;/code&gt;函数的例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;range(101)&lt;/code&gt;：可以用来产生&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;100&lt;/code&gt;范围的整数，需要注意的是取不到&lt;code&gt;101&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range(1, 101)&lt;/code&gt;：可以用来产生&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;100&lt;/code&gt;范围的整数，相当于是左闭右开的设定，即&lt;code&gt;[1, 101)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range(1, 101, 2)&lt;/code&gt;：可以用来产生&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;100&lt;/code&gt;的奇数，其中&lt;code&gt;2&lt;/code&gt;是步长（跨度），即每次递增的值，&lt;code&gt;101&lt;/code&gt;取不到。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;range(100, 0, -2)&lt;/code&gt;：可以用来产生&lt;code&gt;100&lt;/code&gt;到&lt;code&gt;1&lt;/code&gt;的偶数，其中&lt;code&gt;-2&lt;/code&gt;是步长（跨度），即每次递减的值，&lt;code&gt;0&lt;/code&gt;取不到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大家可能已经注意到了，上面代码的输出操作和休眠操作都没有用到循环变量&lt;code&gt;i&lt;/code&gt;，对于不需要用到循环变量的&lt;code&gt;for-in&lt;/code&gt;循环结构，按照 Python 的编程惯例，我们通常把循环变量命名为&lt;code&gt;_&lt;/code&gt;，修改后的代码如下所示。虽然结果没什么变化，但是这样写显得你更加专业，逼格瞬间拉满。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
每隔1秒输出一次“hello, world”，持续1小时

Author: 骆昊
Version: 1.1
&quot;&quot;&quot;
import time

for _ in range(3600):
    print(&apos;hello, world&apos;)
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码要执行一个小时，如果想提前结束程序，在 PyCharm 中可以点击运行窗口上的停止按钮，如下图所示。如果在命令提示符或终端中运行代码，可以使用组合键&lt;code&gt;ctrl+c&lt;/code&gt;来终止程序。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/0069e90618.webp&quot; style=&quot;zoom:40%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;下面，我们用&lt;code&gt;for-in&lt;/code&gt;循环实现从 1 到 100 的整数求和，即 $\small{\sum_{n=1}^{100}{n}}$ 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的整数求和

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
total = 0
for i in range(1, 101):
    total += i
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，变量&lt;code&gt;total&lt;/code&gt;的作用是保存累加的结果。在循环的过程中，循环变量&lt;code&gt;i&lt;/code&gt;的值会从 1 一直取到 100。对于变量&lt;code&gt;i&lt;/code&gt;的每个取值，我们都执行了&lt;code&gt;total += i&lt;/code&gt;，它相当于&lt;code&gt;total = total + i&lt;/code&gt;，这条语句实现了累加操作。所以，当循环结束，我们输出变量&lt;code&gt;total&lt;/code&gt; 的值，它的值就是从 1 累加到 100 的结果 5050。注意，&lt;code&gt;print(total)&lt;/code&gt;这条语句前是没有缩进的，它不受&lt;code&gt;for-in&lt;/code&gt;循环的控制，不会重复执行。&lt;/p&gt;
&lt;p&gt;我们再来写一个从1到100偶数求和的代码，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
total = 0
for i in range(1, 101):
    if i % 2 == 0:
        total += i
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;for-in&lt;/code&gt;循环中我们使用了分支结构来判断循环变量&lt;code&gt;i&lt;/code&gt;是不是偶数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们也可以修改&lt;code&gt;range&lt;/code&gt;函数的参数，将起始值和跨度修改为&lt;code&gt;2&lt;/code&gt;，用更为简单的代码实现从 1 到 100 的偶数求和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
total = 0
for i in range(2, 101, 2):
    total += i
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然， 更为简单的办法是使用 Python 内置的&lt;code&gt;sum&lt;/code&gt;函数求和，这样我们连循环结构都省掉了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.2
Author: 骆昊
&quot;&quot;&quot;
print(sum(range(2, 101, 2)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;while循环&lt;/h4&gt;
&lt;p&gt;如果要构造循环结构但是又不能确定循环重复的次数，我们推荐使用&lt;code&gt;while&lt;/code&gt;循环。&lt;code&gt;while&lt;/code&gt;循环通过布尔值或能产生布尔值的表达式来控制循环，当布尔值或表达式的值为&lt;code&gt;True&lt;/code&gt;时，循环体（&lt;code&gt;while&lt;/code&gt;语句下方保持相同缩进的代码块）中的语句就会被重复执行，当表达式的值为&lt;code&gt;False&lt;/code&gt;时，结束循环。&lt;/p&gt;
&lt;p&gt;下面我们用&lt;code&gt;while&lt;/code&gt;循环来实现从 1 到 100 的整数求和，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的整数求和

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
total = 0
i = 1
while i &amp;lt;= 100:
    total += i
    i += 1
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相较于&lt;code&gt;for-in&lt;/code&gt;循环，上面的代码我们在循环开始前增加了一个变量&lt;code&gt;i&lt;/code&gt;，我们使用这个变量来控制循环，所以&lt;code&gt;while&lt;/code&gt;后面给出了&lt;code&gt;i &amp;lt;= 100&lt;/code&gt;的条件。在&lt;code&gt;while&lt;/code&gt;的循环体中，我们除了做累加，还需要让变量&lt;code&gt;i&lt;/code&gt;的值递增，所以我们添加了&lt;code&gt;i += 1&lt;/code&gt;这条语句，这样&lt;code&gt;i&lt;/code&gt;的值就会依次取到1、2、3、……，直到 101。当&lt;code&gt;i&lt;/code&gt;变成 101 时，&lt;code&gt;while&lt;/code&gt;循环的条件不再成立，代码会离开&lt;code&gt;while&lt;/code&gt;循环，此时我们输出变量&lt;code&gt;total&lt;/code&gt;的值，它就是从 1 到 100 求和的结果 5050。&lt;/p&gt;
&lt;p&gt;如果要实现从 1 到 100 的偶数求和，我们可以对上面的代码稍作修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.3
Author: 骆昊
&quot;&quot;&quot;
total = 0
i = 2
while i &amp;lt;= 100:
    total += i
    i += 2
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;break和continue&lt;/h4&gt;
&lt;p&gt;如果把&lt;code&gt;while&lt;/code&gt;循环的条件设置为&lt;code&gt;True&lt;/code&gt;，即让条件恒成立会怎么样呢？我们看看下面的代码，还是使用&lt;code&gt;while&lt;/code&gt;构造循环结构，计算 1 到 100 的偶数和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.4
Author: 骆昊
&quot;&quot;&quot;
total = 0
i = 2
while True:
    total += i
    i += 2
    if i &amp;gt; 100:
        break
print(total) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中使用&lt;code&gt;while True&lt;/code&gt;构造了一个条件恒成立的循环，也就意味着如果不做特殊处理，循环是不会结束的，这就是我们常说的“死循环”。为了在&lt;code&gt;i&lt;/code&gt;的值超过 100 后让循环停下来，我们使用了&lt;code&gt;break&lt;/code&gt;关键字，它的作用是终止循环结构的执行。需要注意的是，&lt;code&gt;break&lt;/code&gt;只能终止它所在的那个循环，这一点在使用嵌套循环结构时需要引起注意，后面我们会讲到什么是嵌套的循环结构。除了&lt;code&gt;break&lt;/code&gt;之外，还有另一个在循环结构中可以使用的关键字&lt;code&gt;continue&lt;/code&gt;，它可以用来放弃本次循环后续的代码直接让循环进入下一轮，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
从1到100的偶数求和

Version: 1.5
Author: 骆昊
&quot;&quot;&quot;
total = 0
for i in range(1, 101):
    if i % 2 != 0:
        continue
    total += i
print(total)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码使用&lt;code&gt;continue&lt;/code&gt;关键字跳过了&lt;code&gt;i&lt;/code&gt;是奇数的情况，只有在&lt;code&gt;i&lt;/code&gt;是偶数的前提下，才会执行到&lt;code&gt;total += i&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;嵌套的循环结构&lt;/h4&gt;
&lt;p&gt;和分支结构一样，循环结构也是可以嵌套的，也就是说在循环结构中还可以构造循环结构。下面的例子演示了如何通过嵌套的循环来输出一个乘法口诀表（九九表）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
打印乘法口诀表

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
for i in range(1, 10):
    for j in range(1, i + 1):
        print(f&apos;{i}×{j}={i * j}&apos;, end=&apos;\t&apos;)
    print()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，&lt;code&gt;for-in&lt;/code&gt;循环的循环体中又用到了&lt;code&gt;for-in&lt;/code&gt;循环，外面的循环用来控制产生&lt;code&gt;i&lt;/code&gt;行的输出，而里面的循环则用来控制在一行中输出&lt;code&gt;j&lt;/code&gt;列。显然，里面的&lt;code&gt;for-in&lt;/code&gt;循环的输出就是乘法口诀表中的一整行。所以在里面的循环完成时，我们用了一个&lt;code&gt;print()&lt;/code&gt;来实现换行的效果，让下面的输出重新另起一行，最后的输出如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1×1=1	
2×1=2	2×2=4	
3×1=3	3×2=6	3×3=9	
4×1=4	4×2=8	4×3=12	4×4=16	
5×1=5	5×2=10	5×3=15	5×4=20	5×5=25	
6×1=6	6×2=12	6×3=18	6×4=24	6×5=30	6×6=36	
7×1=7	7×2=14	7×3=21	7×4=28	7×5=35	7×6=42	7×7=49	
8×1=8	8×2=16	8×3=24	8×4=32	8×5=40	8×6=48	8×7=56	8×8=64	
9×1=9	9×2=18	9×3=27	9×4=36	9×5=45	9×6=54	9×7=63	9×8=72	9×9=81
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;循环结构的应用&lt;/h4&gt;
&lt;h5&gt;例子1：判断素数&lt;/h5&gt;
&lt;p&gt;要求：输入一个大于 1 的正整数，判断它是不是素数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：素数指的是只能被 1 和自身整除的大于 1 的整数。例如对于正整数 $\small{n}$，我们可以通过在 2 到 $\small{n - 1}$ 之间寻找有没有 $\small{n}$ 的因子，来判断它到底是不是一个素数。当然，循环不用从 2 开始到 $\small{n - 1}$ 结束，因为对于大于 1 的正整数，因子应该都是成对出现的，所以循环到 $\small{\sqrt{n}}$ 就可以结束了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入一个大于1的正整数判断它是不是素数

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
num = int(input(&apos;请输入一个正整数: &apos;))
end = int(num ** 0.5)
is_prime = True
for i in range(2, end + 1):
    if num % i == 0:
        is_prime = False
        break
if is_prime:
    print(f&apos;{num}是素数&apos;)
else:
    print(f&apos;{num}不是素数&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中我们用了布尔型的变量&lt;code&gt;is_prime&lt;/code&gt;，我们先将它赋值为&lt;code&gt;True&lt;/code&gt;，假设&lt;code&gt;num&lt;/code&gt;是一个素数；接下来，我们在 2 到&lt;code&gt;num ** 0.5&lt;/code&gt;的范围寻找&lt;code&gt;num&lt;/code&gt;的因子，如果找到了&lt;code&gt;num&lt;/code&gt;的因子，那么它一定不是素数，此时我们将&lt;code&gt;is_prime&lt;/code&gt;赋值为&lt;code&gt;False&lt;/code&gt;，同时使用&lt;code&gt;break&lt;/code&gt;关键字终止循环结构；最后，我们根据&lt;code&gt;is_prime&lt;/code&gt;的值是&lt;code&gt;True&lt;/code&gt;还是&lt;code&gt;False&lt;/code&gt;来给出不同的输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子2：最大公约数&lt;/h5&gt;
&lt;p&gt;要求：输入两个大于 0 的正整数，求两个数的最大公约数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：两个数的最大公约数是两个数的公共因子中最大的那个数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入两个正整数求它们的最大公约数

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
x = int(input(&apos;x = &apos;))
y = int(input(&apos;y = &apos;))
for i in range(x, 0, -1):
    if x % i == 0 and y % i == 0:
        print(f&apos;最大公约数: {i}&apos;)
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中&lt;code&gt;for-in&lt;/code&gt;循环的循环变量值是从大到小的，这样我们找到的能够同时整除&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的因子&lt;code&gt;i&lt;/code&gt;，就是&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的最大公约数，此时我们用&lt;code&gt;break&lt;/code&gt;终止循环。如果&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;互质，那么循环会执行到&lt;code&gt;i&lt;/code&gt;变成 1，因为 1 是所有正整数的因子，此时&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;的最大公约数就是 1。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用上面代码的找最大公约数在执行效率是有问题的。假如&lt;code&gt;x&lt;/code&gt;的值是&lt;code&gt;999999999998&lt;/code&gt;，&lt;code&gt;y&lt;/code&gt;的值是&lt;code&gt;999999999999&lt;/code&gt;，很显然两个数是互质的，最大公约数为 1。但是我们使用上面的代码，循环会重复&lt;code&gt;999999999998&lt;/code&gt;次，这通常是难以接受的。我们可以使用欧几里得算法来找最大公约数，它能帮我们更快的得到想要的结果，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入两个正整数求它们的最大公约数

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
x = int(input(&apos;x = &apos;))
y = int(input(&apos;y = &apos;))
while y % x != 0:
    x, y = y % x, x
print(f&apos;最大公约数: {x}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：解决问题的方法和步骤可以称之为算法，对于同一个问题，我们可以设计出不同的算法，不同的算法在存储空间的占用和执行效率上都会存在差别，而这些差别就代表了算法的优劣。大家可以对比上面的两段待会，体会一下为什么我们说欧几里得算法是更好的选择。上面的代码中&lt;code&gt;x, y = y % x, x&lt;/code&gt;语句表示将&lt;code&gt;y % x&lt;/code&gt;的值赋给&lt;code&gt;x&lt;/code&gt;，将&lt;code&gt;x&lt;/code&gt; 原来的值赋给&lt;code&gt;y&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;例子3：猜数字游戏&lt;/h5&gt;
&lt;p&gt;要求：计算机出一个 1 到 100 之间的随机数，玩家输入自己猜的数字，计算机给出对应的提示信息“大一点”、“小一点”或“猜对了”，如果玩家猜中了数字，计算机提示用户一共猜了多少次，游戏结束，否则游戏继续。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
猜数字小游戏

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
import random

answer = random.randrange(1, 101)
counter = 0
while True:
    counter += 1
    num = int(input(&apos;请输入: &apos;))
    if num &amp;lt; answer:
        print(&apos;大一点.&apos;)
    elif num &amp;gt; answer:
        print(&apos;小一点.&apos;)
    else:
        print(&apos;猜对了.&apos;)
        break
print(f&apos;你一共猜了{counter}次.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码使用&lt;code&gt;import random&lt;/code&gt;导入了 Python 标准库的&lt;code&gt;random&lt;/code&gt;模块，该模块的&lt;code&gt;randrange&lt;/code&gt;函数帮助我们生成了 1 到 100 范围的随机数。变量&lt;code&gt;counter&lt;/code&gt;用来记录循环执行的次数，也就是用户一共猜了几次，每循环一次&lt;code&gt;counter&lt;/code&gt;的值都会加 1。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;学会了 Python 中的分支结构和循环结构，我们就可以解决很多实际的问题了。通过这节课的学习，大家应该已经知道了可以用&lt;code&gt;for&lt;/code&gt;和&lt;code&gt;while&lt;/code&gt;关键字来构造循环结构。&lt;strong&gt;如果事先知道循环结构重复的次数，我们通常使用&lt;/strong&gt;&lt;code&gt;for&lt;/code&gt;&lt;strong&gt;循环&lt;/strong&gt;；&lt;strong&gt;如果循环结构的重复次数不能确定，可以用&lt;/strong&gt;&lt;code&gt;while&lt;/code&gt;&lt;strong&gt;循环&lt;/strong&gt;。此外，我们可以在循环结构中&lt;strong&gt;使用&lt;/strong&gt;&lt;code&gt;break&lt;/code&gt;&lt;strong&gt;终止循环&lt;/strong&gt;，&lt;strong&gt;也可以在循环结构中使用&lt;/strong&gt;&lt;code&gt;continue&lt;/code&gt;&lt;strong&gt;关键字让循环结构直接进入下一轮次&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;07 分支和循环结构实战&lt;/h2&gt;
&lt;p&gt;通过前面两节课的学习，大家对 Python 中的分支结构和循环结构已经有了初步的认知。&lt;strong&gt;分支结构和循环结构是构造程序逻辑的基础&lt;/strong&gt;，它们的重要性不言而喻，但是对于初学者来说这也是比较困难的部分。很多人对分支结构和循环结构的语法是能够理解的，但是遇到实际问题的时候又无法下手；&lt;strong&gt;看懂别人的代码很容易，但是要自己写出类似的代码却又很难&lt;/strong&gt;。如果你也有同样的问题和困惑，千万不要沮丧，这只是因为你的编程之旅才刚刚开始，&lt;strong&gt;你的练习量还没有达到让你可以随心所欲写出代码的程度&lt;/strong&gt;，只要加强编程练习，通过量的积累来产生质的变化，这个问题迟早都会解决的。&lt;/p&gt;
&lt;h4&gt;例子1：100以内的素数&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：素数指的是只能被 1 和自身整除的正整数（不包括 1），之前我们写过判断素数的代码，这里相当于是一个升级版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输出100以内的素数

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
for num in range(2, 100):
    is_prime = True
    for i in range(2, int(num ** 0.5) + 1):
        if num % i == 0:
            is_prime = False
            break
    if is_prime:
        print(num)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;例子2：斐波那契数列&lt;/h4&gt;
&lt;p&gt;要求：输出斐波那契数列中的前 20 个数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：斐波那契数列（Fibonacci sequence），通常也被称作黄金分割数列，是意大利数学家莱昂纳多·斐波那契（Leonardoda Fibonacci）在《计算之书》中研究理想假设条件下兔子成长率问题而引入的数列，因此这个数列也常被戏称为“兔子数列”。斐波那契数列的特点是数列的前两个数都是 1，从第三个数开始，每个数都是它前面两个数的和。按照这个规律，斐波那契数列的前 10 个数是：&lt;code&gt;1, 1, 2, 3, 5, 8, 13, 21, 34, 55&lt;/code&gt;。斐波那契数列在现代物理、准晶体结构、化学等领域都有直接的应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输出斐波那契数列中的前20个数

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;

a, b = 0, 1
for _ in range(20):
    a, b = b, a + b
    print(a)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面循环中的&lt;code&gt;a, b = b, a + b&lt;/code&gt;表示将变量&lt;code&gt;b&lt;/code&gt;的值赋给&lt;code&gt;a&lt;/code&gt;，把&lt;code&gt;a + b&lt;/code&gt;的值赋给&lt;code&gt;b&lt;/code&gt;。通过这个递推公式，我们可以依次获得斐波那契数列中的数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子3：寻找水仙花数&lt;/h4&gt;
&lt;p&gt;要求：找出 100 到 999 范围内的所有水仙花数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：在数论中，水仙花数（narcissistic number）也被称为超完全数字不变数、自恋数、自幂数、阿姆斯特朗数，它是一个 $\small{N}$ 位非负整数，其各位数字的 $\small{N}$ 次方和刚好等于该数本身，例如： $\small{153 = 1^{3} + 5^{3} + 3^{3}}$ ，所以 153 是一个水仙花数； $\small{1634 = 1^{4} + 6^{4} + 3^{4} + 4^{4}}$ ，所以 1634 也是一个水仙花数。对于三位数，解题的关键是将它拆分为个位、十位、百位，再判断是否满足水仙花数的要求，这一点利用 Python 中的&lt;code&gt;//&lt;/code&gt;和&lt;code&gt;%&lt;/code&gt;运算符其实很容易做到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
找出100到999范围内的水仙花数

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
for num in range(100, 1000):
    low = num % 10
    mid = num // 10 % 10
    high = num // 100
    if num == low ** 3 + mid ** 3 + high ** 3:
        print(num)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面利用&lt;code&gt;//&lt;/code&gt;和&lt;code&gt;%&lt;/code&gt;拆分一个数的小技巧在写代码的时候还是很常用的。我们要将一个不知道有多少位的正整数进行反转，例如将 12389 变成 98321，也可以利用这两个运算来实现，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
正整数的反转

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
num = int(input(&apos;num = &apos;))
reversed_num = 0
while num &amp;gt; 0:
    reversed_num = reversed_num * 10 + num % 10
    num //= 10
print(reversed_num)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;例子4：百钱百鸡问题&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：百钱百鸡是我国古代数学家张丘建在《算经》一书中提出的数学问题：鸡翁一值钱五，鸡母一值钱三，鸡雏三值钱一。百钱买百鸡，问鸡翁、鸡母、鸡雏各几何？翻译成现代文是：公鸡 5 元一只，母鸡 3 元一只，小鸡 1 元三只，用 100 块钱买一百只鸡，问公鸡、母鸡、小鸡各有多少只？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
百钱百鸡问题

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
for x in range(0, 21):
    for y in range(0, 34):
        for z in range(0, 100, 3):
            if x + y + z == 100 and 5 * x + 3 * y + z // 3 == 100:
                print(f&apos;公鸡: {x}只, 母鸡: {y}只, 小鸡: {z}只&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面使用的方法叫做&lt;strong&gt;穷举法&lt;/strong&gt;，也称为&lt;strong&gt;暴力搜索法&lt;/strong&gt;，这种方法通过一项一项的列举备选解决方案中所有可能的候选项，并检查每个候选项是否符合问题的描述，最终得到问题的解。上面的代码中，我们使用了嵌套的循环结构，假设公鸡有&lt;code&gt;x&lt;/code&gt;只，显然&lt;code&gt;x&lt;/code&gt;的取值范围是 0 到 20，假设母鸡有&lt;code&gt;y&lt;/code&gt;只，它的取值范围是 0 到 33，假设小鸡有&lt;code&gt;z&lt;/code&gt;只，它的取值范围是 0 到 99 且取值是 3 的倍数。这样，我们设置好 100 只鸡的条件&lt;code&gt;x + y + z == 100&lt;/code&gt;，设置好 100 块钱的条件&lt;code&gt;5 * x + 3 * y + z // 3 == 100&lt;/code&gt;，当两个条件同时满足时，就是问题的正确答案，我们用&lt;code&gt;print&lt;/code&gt;函数输出它。这种方法看起来比较笨拙，但对于运算能力非常强大的计算机来说，通常都是一个可行的甚至是不错的选择，只要问题的解存在就能够找到它。&lt;/p&gt;
&lt;p&gt;事实上，上面的代码还有更好的写法，既然我们已经假设公鸡有&lt;code&gt;x&lt;/code&gt;只，母鸡有&lt;code&gt;y&lt;/code&gt;只，那么小鸡的数量就应该是&lt;code&gt;100 - x - y&lt;/code&gt;，这样减少一个条件，我们就可以把上面三层嵌套的&lt;code&gt;for-in&lt;/code&gt;循环改写为两层嵌套的&lt;code&gt;for-in&lt;/code&gt;循环。循环次数减少了，代码的执行效率就有了显著的提升，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
百钱百鸡问题

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;
for x in range(0, 21):
    for y in range(0, 34):
        z = 100 - x - y
        if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
            print(f&apos;公鸡: {x}只, 母鸡: {y}只, 小鸡: {z}只&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中的&lt;code&gt;z % 3 == 0&lt;/code&gt;是为了确保小鸡的数量是 3 的倍数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子5：CRAPS赌博游戏&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：CRAPS又称花旗骰，是美国拉斯维加斯非常受欢迎的一种的桌上赌博游戏。该游戏使用两粒骰子，玩家通过摇两粒骰子获得点数进行游戏。简化后的规则是：玩家第一次摇骰子如果摇出了 7 点或 11 点，玩家胜；玩家第一次如果摇出 2 点、3 点或 12 点，庄家胜；玩家如果摇出其他点数则游戏继续，玩家重新摇骰子，如果玩家摇出了 7 点，庄家胜；如果玩家摇出了第一次摇的点数，玩家胜；其他点数玩家继续摇骰子，直到分出胜负。为了增加代码的趣味性，我们设定游戏开始时玩家有 1000 元的赌注，每局游戏开始之前，玩家先下注，如果玩家获胜就可以获得对应下注金额的奖励，如果庄家获胜，玩家就会输掉自己下注的金额。游戏结束的条件是玩家破产（输光所有的赌注）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
Craps赌博游戏

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;
import random

money = 1000
while money &amp;gt; 0:
    print(f&apos;你的总资产为: {money}元&apos;)
    # 下注金额必须大于0且小于等于玩家的总资产
    while True:
        debt = int(input(&apos;请下注: &apos;))
        if 0 &amp;lt; debt &amp;lt;= money:
            break
    # 用两个1到6均匀分布的随机数相加模拟摇两颗色子得到的点数
    first_point = random.randrange(1, 7) + random.randrange(1, 7)
    print(f&apos;\n玩家摇出了{first_point}点&apos;)
    if first_point == 7 or first_point == 11:
        print(&apos;玩家胜!\n&apos;)
        money += debt
    elif first_point == 2 or first_point == 3 or first_point == 12:
        print(&apos;庄家胜!\n&apos;)
        money -= debt
    else:
        # 如果第一次摇色子没有分出胜负，玩家需要重新摇色子
        while True:
            current_point = random.randrange(1, 7) + random.randrange(1, 7)
            print(f&apos;玩家摇出了{current_point}点&apos;)
            if current_point == 7:
                print(&apos;庄家胜!\n&apos;)
                money -= debt
                break
            elif current_point == first_point:
                print(&apos;玩家胜!\n&apos;)
                money += debt
                break
print(&apos;你破产了, 游戏结束!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;分支结构和循环结构都非常重要，是构造程序逻辑的基础，&lt;strong&gt;一定要通过大量的练习来达到融会贯通&lt;/strong&gt;。我们可以用上面讲的花旗骰游戏作为一个标准，如果你能够很顺利的完成这段代码，那么分支结构和循环结构的知识你就已经很好的掌握了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;08 常用数据结构之列表-1&lt;/h2&gt;
&lt;p&gt;在开始本节课的内容之前，我们先给大家一个编程任务，将一颗色子掷 6000 次，统计每种点数出现的次数。这个任务对大家来说应该是非常简单的，我们可以用 1 到 6 均匀分布的随机数来模拟掷色子，然后用 6 个变量分别记录每个点数出现的次数，相信通过前面的学习，大家都能比较顺利的写出下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
将一颗色子掷6000次，统计每种点数出现的次数

Author: 骆昊
Version: 1.0
&quot;&quot;&quot;
import random

f1 = 0
f2 = 0
f3 = 0
f4 = 0
f5 = 0
f6 = 0
for _ in range(6000):
    face = random.randrange(1, 7)
    if face == 1:
        f1 += 1
    elif face == 2:
        f2 += 1
    elif face == 3:
        f3 += 1
    elif face == 4:
        f4 += 1
    elif face == 5:
        f5 += 1
    else:
        f6 += 1
print(f&apos;1点出现了{f1}次&apos;)
print(f&apos;2点出现了{f2}次&apos;)
print(f&apos;3点出现了{f3}次&apos;)
print(f&apos;4点出现了{f4}次&apos;)
print(f&apos;5点出现了{f5}次&apos;)
print(f&apos;6点出现了{f6}次&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码非常有多么“丑陋”相信就不用我多说了。当然，更为可怕的是，如果我们要掷两颗或者掷更多的色子，然后统计每种点数出现的次数，那就需要定义更多的变量，写更多的分支结构，大家想想都会感到恶心。讲到这里，相信大家心中已经有一个疑问了：有没有办法用一个变量来保存多个数据，有没有办法用统一的代码对多个数据进行操作？答案是肯定的，在 Python 语言中我们可以通过容器型变量来保存和操作多个数据，我们首先为大家介绍列表（&lt;code&gt;list&lt;/code&gt;）这种新的数据类型。&lt;/p&gt;
&lt;h4&gt;创建列表&lt;/h4&gt;
&lt;p&gt;在 Python 中，&lt;strong&gt;列表是由一系列元素按特定顺序构成的数据序列&lt;/strong&gt;，这就意味着如果我们定义一个列表类型的变量，&lt;strong&gt;可以用它来保存多个数据&lt;/strong&gt;。在 Python 中，可以使用&lt;code&gt;[]&lt;/code&gt;字面量语法来定义列表，列表中的多个元素用逗号进行分隔，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items1 = [35, 12, 99, 68, 55, 35, 87]
items2 = [&apos;Python&apos;, &apos;Java&apos;, &apos;Go&apos;, &apos;Kotlin&apos;]
items3 = [100, 12.3, &apos;Python&apos;, True]
print(items1)  # [35, 12, 99, 68, 55, 35, 87]
print(items2)  # [&apos;Python&apos;, &apos;Java&apos;, &apos;Go&apos;, &apos;Kotlin&apos;]
print(items3)  # [100, 12.3, &apos;Python&apos;, True]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：列表中可以有重复元素，例如&lt;code&gt;items1&lt;/code&gt;中的&lt;code&gt;35&lt;/code&gt;；列表中可以有不同类型的元素，例如&lt;code&gt;items3&lt;/code&gt;中有&lt;code&gt;int&lt;/code&gt;类型、&lt;code&gt;float&lt;/code&gt;类型、&lt;code&gt;str&lt;/code&gt;类型和&lt;code&gt;bool&lt;/code&gt;类型的元素，但是我们通常并不建议将不同类型的元素放在同一个列表中，主要是操作起来极为不便。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以使用&lt;code&gt;type&lt;/code&gt;函数来查看变量的类型，有兴趣的小伙伴可以自行查看上面的变量&lt;code&gt;items1&lt;/code&gt;到底是什么类型。因为列表可以保存多个元素，它是一种容器型的数据类型，所以我们在给列表类型的变量起名字时，变量名通常用复数形式的单词。&lt;/p&gt;
&lt;p&gt;除此以外，还可以通过 Python 内置的&lt;code&gt;list&lt;/code&gt;函数将其他序列变成列表。准确的说，&lt;code&gt;list&lt;/code&gt;并不是一个普通的函数，它是创建列表对象的构造器，后面的课程会为大家介绍对象和构造器这些概念。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items4 = list(range(1, 10))
items5 = list(&apos;hello&apos;)
print(items4)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(items5)  # [&apos;h&apos;, &apos;e&apos;, &apos;l&apos;, &apos;l&apos;, &apos;o&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;range(1, 10)&lt;/code&gt;会产生&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;9&lt;/code&gt;的整数序列，给到&lt;code&gt;list&lt;/code&gt;构造器中，会创建出由&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;9&lt;/code&gt;的整数构成的列表。字符串是字符构成的序列，上面的&lt;code&gt;list(&apos;hello&apos;)&lt;/code&gt;用字符串&lt;code&gt;hello&lt;/code&gt;的字符作为列表元素，创建了列表对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;列表的运算&lt;/h4&gt;
&lt;p&gt;我们可以使用&lt;code&gt;+&lt;/code&gt;运算符实现两个列表的拼接，拼接运算会将两个列表中的元素连接起来放到一个列表中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items5 = [35, 12, 99, 45, 66]
items6 = [45, 58, 29]
items7 = [&apos;Python&apos;, &apos;Java&apos;, &apos;JavaScript&apos;]
print(items5 + items6)  # [35, 12, 99, 45, 66, 45, 58, 29]
print(items6 + items7)  # [45, 58, 29, &apos;Python&apos;, &apos;Java&apos;, &apos;JavaScript&apos;]
items5 += items6
print(items5)  # [35, 12, 99, 45, 66, 45, 58, 29]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用&lt;code&gt;*&lt;/code&gt;运算符实现列表的重复运算，&lt;code&gt;*&lt;/code&gt;运算符会将列表元素重复指定的次数，我们在上面的代码中增加两行，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(items6 * 3)  # [45, 58, 29, 45, 58, 29, 45, 58, 29]
print(items7 * 2)  # [&apos;Python&apos;, &apos;Java&apos;, &apos;JavaScript&apos;, &apos;Python&apos;, &apos;Java&apos;, &apos;JavaScript&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用&lt;code&gt;in&lt;/code&gt;或&lt;code&gt;not in&lt;/code&gt;运算符判断一个元素在不在列表中，我们在上面的代码代码中再增加两行，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(29 in items6)  # True
print(99 in items6)  # False
print(&apos;C++&apos; not in items7)     # True
print(&apos;Python&apos; not in items7)  # False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于列表中有多个元素，而且元素是按照特定顺序放在列表中的，所以当我们想操作列表中的某个元素时，可以使用&lt;code&gt;[]&lt;/code&gt;运算符，通过在&lt;code&gt;[]&lt;/code&gt;中指定元素的位置来访问该元素，这种运算称为索引运算。需要说明的是，&lt;code&gt;[]&lt;/code&gt;的元素位置可以是&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;N - 1&lt;/code&gt;的整数，也可以是&lt;code&gt;-1&lt;/code&gt;到&lt;code&gt;-N&lt;/code&gt;的整数，分别称为正向索引和反向索引，其中&lt;code&gt;N&lt;/code&gt;代表列表元素的个数。对于正向索引，&lt;code&gt;[0]&lt;/code&gt;可以访问列表中的第一个元素，&lt;code&gt;[N - 1]&lt;/code&gt;可以访问最后一个元素；对于反向索引，&lt;code&gt;[-1]&lt;/code&gt;可以访问列表中的最后一个元素，&lt;code&gt;[-N]&lt;/code&gt;可以访问第一个元素，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items8 = [&apos;apple&apos;, &apos;waxberry&apos;, &apos;pitaya&apos;, &apos;peach&apos;, &apos;watermelon&apos;]
print(items8[0])   # apple
print(items8[2])   # pitaya
print(items8[4])   # watermelon
items8[2] = &apos;durian&apos;
print(items8)      # [&apos;apple&apos;, &apos;waxberry&apos;, &apos;durian&apos;, &apos;peach&apos;, &apos;watermelon&apos;]
print(items8[-5])  # &apos;apple&apos;
print(items8[-4])  # &apos;waxberry&apos;
print(items8[-1])  # watermelon
items8[-4] = &apos;strawberry&apos;
print(items8)      # [&apos;apple&apos;, &apos;strawberry&apos;, &apos;durian&apos;, &apos;peach&apos;, &apos;watermelon&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用索引运算的时候要避免出现索引越界的情况，对于上面的&lt;code&gt;items8&lt;/code&gt;，如果我们访问&lt;code&gt;items8[5]&lt;/code&gt;或&lt;code&gt;items8[-6]&lt;/code&gt;，就会引发&lt;code&gt;IndexError&lt;/code&gt;错误，导致程序崩溃，对应的错误信息是：&lt;em&gt;list index out of range&lt;/em&gt;，翻译成中文就是“数组索引超出范围”。因为对于只有五个元素的列表&lt;code&gt;items8&lt;/code&gt;，有效的正向索引是&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;4&lt;/code&gt;，有效的反向索引是&lt;code&gt;-1&lt;/code&gt;到&lt;code&gt;-5&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果希望一次性访问列表中的多个元素，我们可以使用切片运算。切片运算是形如&lt;code&gt;[start:end:stride]&lt;/code&gt;的运算符，其中&lt;code&gt;start&lt;/code&gt;代表访问列表元素的起始位置，&lt;code&gt;end&lt;/code&gt;代表访问列表元素的终止位置（终止位置的元素无法访问），而&lt;code&gt;stride&lt;/code&gt;则代表了跨度，简单的说就是位置的增量，比如我们访问的第一个元素在&lt;code&gt;start&lt;/code&gt;位置，那么第二个元素就在&lt;code&gt;start + stride&lt;/code&gt;位置，当然&lt;code&gt;start + stride&lt;/code&gt;要小于&lt;code&gt;end&lt;/code&gt;。我们给上面的代码增加下面的语句，来使用切片运算符访问列表元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(items8[1:3:1])     # [&apos;strawberry&apos;, &apos;durian&apos;]
print(items8[0:3:1])     # [&apos;apple&apos;, &apos;strawberry&apos;, &apos;durian&apos;]
print(items8[0:5:2])     # [&apos;apple&apos;, &apos;durian&apos;, &apos;watermelon&apos;]
print(items8[-4:-2:1])   # [&apos;strawberry&apos;, &apos;durian&apos;]
print(items8[-2:-6:-1])  # [&apos;peach&apos;, &apos;durian&apos;, &apos;strawberry&apos;, &apos;apple&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提醒&lt;/strong&gt;：大家可以看看上面代码中的最后一行，想一想当跨度为负数时，切片运算是如何访问元素的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果&lt;code&gt;start&lt;/code&gt;值等于&lt;code&gt;0&lt;/code&gt;，那么在使用切片运算符时可以将其省略；如果&lt;code&gt;end&lt;/code&gt;值等于&lt;code&gt;N&lt;/code&gt;，&lt;code&gt;N&lt;/code&gt;代表列表元素的个数，那么在使用切片运算符时可以将其省略；如果&lt;code&gt;stride&lt;/code&gt;值等于&lt;code&gt;1&lt;/code&gt;，那么在使用切片运算符时也可以将其省略。所以，下面的代码跟上面的代码作用完全相同。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(items8[1:3])     # [&apos;strawberry&apos;, &apos;durian&apos;]
print(items8[:3:1])    # [&apos;apple&apos;, &apos;strawberry&apos;, &apos;durian&apos;]
print(items8[::2])     # [&apos;apple&apos;, &apos;durian&apos;, &apos;watermelon&apos;]
print(items8[-4:-2])   # [&apos;strawberry&apos;, &apos;durian&apos;]
print(items8[-2::-1])  # [&apos;peach&apos;, &apos;durian&apos;, &apos;strawberry&apos;, &apos;apple&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，我们还可以通过切片操作修改列表中的元素，例如我们给上面的代码再加上一行，大家可以看看这里的输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items8[1:3] = [&apos;x&apos;, &apos;o&apos;]
print(items8)  # [&apos;apple&apos;, &apos;x&apos;, &apos;o&apos;, &apos;peach&apos;, &apos;watermelon&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个列表还可以做关系运算，我们可以比较两个列表是否相等，也可以给两个列表比大小，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums1 = [1, 2, 3, 4]
nums2 = list(range(1, 5))
nums3 = [3, 2, 1]
print(nums1 == nums2)  # True
print(nums1 != nums2)  # False
print(nums1 &amp;lt;= nums3)  # True
print(nums2 &amp;gt;= nums3)  # False
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;nums1&lt;/code&gt;和&lt;code&gt;nums2&lt;/code&gt;对应元素完全相同，所以&lt;code&gt;==&lt;/code&gt;运算的结果是&lt;code&gt;True&lt;/code&gt;。&lt;code&gt;nums2&lt;/code&gt;和&lt;code&gt;nums3&lt;/code&gt;的比较，由于&lt;code&gt;nums2&lt;/code&gt;的第一个元素&lt;code&gt;1&lt;/code&gt;小于&lt;code&gt;nums3&lt;/code&gt;的第一个元素&lt;code&gt;3&lt;/code&gt;，所以&lt;code&gt;nums2 &amp;gt;= nums3&lt;/code&gt;比较的结果是&lt;code&gt;False&lt;/code&gt;。两个列表的关系运算在实际工作并不那么常用，如果实在不理解就跳过吧，不用纠结。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;元素的遍历&lt;/h4&gt;
&lt;p&gt;如果想逐个取出列表中的元素，可以使用&lt;code&gt;for-in&lt;/code&gt;循环的，有以下两种做法。&lt;/p&gt;
&lt;p&gt;方法一：在循环结构中通过索引运算，遍历列表元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;languages = [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;Kotlin&apos;]
for index in range(len(languages)):
    print(languages[index])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python
Java
C++
Kotlin
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;len&lt;/code&gt;函数可以获取列表元素的个数&lt;code&gt;N&lt;/code&gt;，而&lt;code&gt;range(N)&lt;/code&gt;则构成了从&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;N-1&lt;/code&gt;的范围，刚好可以作为列表元素的索引。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;方法二：直接对列表做循环，循环变量就是列表元素的代表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;languages = [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;Kotlin&apos;]
for language in languages:
    print(language)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python
Java
C++
Kotlin
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;讲到这里，我们可以用列表的知识来重构上面“掷色子统计每种点数出现次数”的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
将一颗色子掷6000次，统计每种点数出现的次数

Author: 骆昊
Version: 1.1
&quot;&quot;&quot;
import random

counters = [0] * 6
# 模拟掷色子记录每种点数出现的次数
for _ in range(6000):
    face = random.randrange(1, 7)
    counters[face - 1] += 1
# 输出每种点数出现的次数
for face in range(1, 7):
    print(f&apos;{face}点出现了{counters[face - 1]}次&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，我们用&lt;code&gt;counters&lt;/code&gt;列表中的六个元素分别表示 1 到 6 点出现的次数，最开始的时候六个元素的值都是 0。接下来，我们用 1 到 6 均匀分布的随机数模拟掷色子，如果摇出 1 点，&lt;code&gt;counters[0]&lt;/code&gt;的值加 1，如果摇出 2 点，&lt;code&gt;counters[1]&lt;/code&gt;的值加 1，以此类推。大家感受一下，由于使用了列表类型加上循环结构，我们对数据的处理是批量性的，这就使得修改后的代码比之前的代码要简单优雅得多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;09 常用数据结构之列表-2&lt;/h2&gt;
&lt;h4&gt;列表的方法&lt;/h4&gt;
&lt;p&gt;列表类型的变量拥有很多方法可以帮助我们操作一个列表，假设我们有名为&lt;code&gt;foos&lt;/code&gt;的列表，列表有名为&lt;code&gt;bar&lt;/code&gt;的方法，那么使用列表方法的语法是：&lt;code&gt;foos.bar()&lt;/code&gt;，这是一种通过对象引用调用对象方法的语法。后面我们讲面向对象编程的时候，还会对这种语法进行详细的讲解，这种语法也称为给对象发消息。&lt;/p&gt;
&lt;h5&gt;添加和删除元素&lt;/h5&gt;
&lt;p&gt;列表是一种可变容器，可变容器指的是我们可以向容器中添加元素、可以从容器移除元素，也可以修改现有容器中的元素。我们可以使用列表的&lt;code&gt;append&lt;/code&gt;方法向列表中追加元素，使用&lt;code&gt;insert&lt;/code&gt;方法向列表中插入元素。追加指的是将元素添加到列表的末尾，而插入则是在指定的位置添加新元素，大家可以看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;languages = [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;]
languages.append(&apos;JavaScript&apos;)
print(languages)  # [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;JavaScript&apos;]
languages.insert(1, &apos;SQL&apos;)
print(languages)  # [&apos;Python&apos;, &apos;SQL&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;JavaScript&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以用列表的&lt;code&gt;remove&lt;/code&gt;方法从列表中删除指定元素，需要注意的是，如果要删除的元素并不在列表中，会引发&lt;code&gt;ValueError&lt;/code&gt;错误导致程序崩溃，所以建议大家在删除元素时，先用之前讲过的成员运算做一个判断。我们还可以使用&lt;code&gt;pop&lt;/code&gt;方法从列表中删除元素，&lt;code&gt;pop&lt;/code&gt;方法默认删除列表中的最后一个元素，当然也可以给一个位置，删除指定位置的元素。在使用&lt;code&gt;pop&lt;/code&gt;方法删除元素时，如果索引的值超出了范围，会引发&lt;code&gt;IndexError&lt;/code&gt;异常，导致程序崩溃。除此之外，列表还有一个&lt;code&gt;clear&lt;/code&gt;方法，可以清空列表中的元素，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;languages = [&apos;Python&apos;, &apos;SQL&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;JavaScript&apos;]
if &apos;Java&apos; in languages:
    languages.remove(&apos;Java&apos;)
if &apos;Swift&apos; in languages:
    languages.remove(&apos;Swift&apos;)
print(languages)  # [&apos;Python&apos;, &apos;SQL&apos;, C++&apos;, &apos;JavaScript&apos;]
languages.pop()
temp = languages.pop(1)
print(temp)       # SQL
languages.append(temp)
print(languages)  # [&apos;Python&apos;, C++&apos;, &apos;SQL&apos;]
languages.clear()
print(languages)  # []
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;pop&lt;/code&gt;方法删除元素时会得到被删除的元素，上面的代码中，我们将&lt;code&gt;pop&lt;/code&gt;方法删除的元素赋值给了名为&lt;code&gt;temp&lt;/code&gt;的变量。当然如果你愿意，还可以把这个元素再次加入到列表中，正如上面的代码&lt;code&gt;languages.append(temp)&lt;/code&gt;所做的那样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里还有一个小问题，例如&lt;code&gt;languages&lt;/code&gt;列表中有多个&lt;code&gt;&apos;Python&apos;&lt;/code&gt;，那么我们用&lt;code&gt;languages.remove(&apos;Python&apos;)&lt;/code&gt;是删除所有的&lt;code&gt;&apos;Python&apos;&lt;/code&gt;，还是删除第一个&lt;code&gt;&apos;Python&apos;&lt;/code&gt;，大家可以先猜一猜，然后再自己动手尝试一下。&lt;/p&gt;
&lt;p&gt;从列表中删除元素其实还有一种方式，就是使用 Python 中的&lt;code&gt;del&lt;/code&gt;关键字后面跟要删除的元素，这种做法跟使用&lt;code&gt;pop&lt;/code&gt;方法指定索引删除元素没有实质性的区别，但后者会返回删除的元素，前者在性能上略优，因为&lt;code&gt;del&lt;/code&gt;对应的底层字节码指令是&lt;code&gt;DELETE_SUBSCR&lt;/code&gt;，而&lt;code&gt;pop&lt;/code&gt;对应的底层字节码指令是&lt;code&gt;CALL_METHOD&lt;/code&gt;和&lt;code&gt;POP_TOP&lt;/code&gt;，如果不理解就不用管它了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items = [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;]
del items[1]
print(items)  # [&apos;Python&apos;, &apos;C++&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;元素位置和频次&lt;/h5&gt;
&lt;p&gt;列表的&lt;code&gt;index&lt;/code&gt;方法可以查找某个元素在列表中的索引位置，如果找不到指定的元素，&lt;code&gt;index&lt;/code&gt;方法会引发&lt;code&gt;ValueError&lt;/code&gt;错误；列表的&lt;code&gt;count&lt;/code&gt;方法可以统计一个元素在列表中出现的次数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items = [&apos;Python&apos;, &apos;Java&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;Kotlin&apos;, &apos;Python&apos;]
print(items.index(&apos;Python&apos;))     # 0
# 从索引位置1开始查找&apos;Python&apos;
print(items.index(&apos;Python&apos;, 1))  # 5
print(items.count(&apos;Python&apos;))     # 2
print(items.count(&apos;Kotlin&apos;))     # 1
print(items.count(&apos;Swfit&apos;))      # 0
# 从索引位置3开始查找&apos;Java&apos;
print(items.index(&apos;Java&apos;, 3))    # ValueError: &apos;Java&apos; is not in list
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;元素排序和反转&lt;/h5&gt;
&lt;p&gt;列表的&lt;code&gt;sort&lt;/code&gt;操作可以实现列表元素的排序，而&lt;code&gt;reverse&lt;/code&gt;操作可以实现元素的反转，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items = [&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;Kotlin&apos;, &apos;Swift&apos;]
items.sort()
print(items)  # [&apos;C++&apos;, &apos;Java&apos;, &apos;Kotlin&apos;, &apos;Python&apos;, &apos;Swift&apos;]
items.reverse()
print(items)  # [&apos;Swift&apos;, &apos;Python&apos;, &apos;Kotlin&apos;, &apos;Java&apos;, &apos;C++&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;列表生成式&lt;/h4&gt;
&lt;p&gt;在 Python 中，列表还可以通过一种特殊的字面量语法来创建，这种语法叫做生成式。下面，我们通过例子来说明使用列表生成式创建列表到底有什么好处。&lt;/p&gt;
&lt;p&gt;场景一：创建一个取值范围在&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;99&lt;/code&gt;且能被&lt;code&gt;3&lt;/code&gt;或者&lt;code&gt;5&lt;/code&gt;整除的数字构成的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items = []
for i in range(1, 100):
    if i % 3 == 0 or i % 5 == 0:
        items.append(i)
print(items)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用列表生成式做同样的事情，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;items = [i for i in range(1, 100) if i % 3 == 0 or i % 5 == 0]
print(items)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;场景二：有一个整数列表&lt;code&gt;nums1&lt;/code&gt;，创建一个新的列表&lt;code&gt;nums2&lt;/code&gt;，&lt;code&gt;nums2&lt;/code&gt;中的元素是&lt;code&gt;nums1&lt;/code&gt;中对应元素的平方。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums1 = [35, 12, 97, 64, 55]
nums2 = []
for num in nums1:
    nums2.append(num ** 2)
print(nums2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用列表生成式做同样的事情，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums1 = [35, 12, 97, 64, 55]
nums2 = [num ** 2 for num in nums1]
print(nums2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;场景三： 有一个整数列表&lt;code&gt;nums1&lt;/code&gt;，创建一个新的列表&lt;code&gt;nums2&lt;/code&gt;，将&lt;code&gt;nums1&lt;/code&gt;中大于&lt;code&gt;50&lt;/code&gt;的元素放到&lt;code&gt;nums2&lt;/code&gt;中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums1 = [35, 12, 97, 64, 55]
nums2 = []
for num in nums1:
    if num &amp;gt; 50:
        nums2.append(num)
print(nums2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用列表生成式做同样的事情，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums1 = [35, 12, 97, 64, 55]
nums2 = [num for num in nums1 if num &amp;gt; 50]
print(nums2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用列表生成式创建列表不仅代码简单优雅，而且性能上也优于使用&lt;code&gt;for-in&lt;/code&gt;循环和&lt;code&gt;append&lt;/code&gt;方法向空列表中追加元素的方式。为什么说生成式有更好的性能呢，那是因为 Python 解释器的字节码指令中有专门针对生成式的指令（&lt;code&gt;LIST_APPEND&lt;/code&gt;指令）；而&lt;code&gt;for&lt;/code&gt;循环是通过方法调用（&lt;code&gt;LOAD_METHOD&lt;/code&gt;和&lt;code&gt;CALL_METHOD&lt;/code&gt;指令）的方式为列表添加元素，方法调用本身就是一个相对比较耗时的操作。对这一点不理解也没有关系，记住“&lt;strong&gt;强烈建议用生成式语法来创建列表&lt;/strong&gt;”这个结论就可以了。&lt;/p&gt;
&lt;h4&gt;嵌套列表&lt;/h4&gt;
&lt;p&gt;Python 语言没有限定列表中的元素必须是相同的数据类型，也就是说一个列表中的元素可以任意的数据类型，当然也包括列表本身。如果列表中的元素也是列表，那么我们可以称之为嵌套的列表。嵌套的列表可以用来表示表格或数学上的矩阵，例如：我们想保存5个学生3门课程的成绩，可以用如下所示的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scores = [[95, 83, 92], [80, 75, 82], [92, 97, 90], [80, 78, 69], [65, 66, 89]]
print(scores[0])
print(scores[0][1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上面的嵌套列表，每个元素相当于就是一个学生3门课程的成绩，例如&lt;code&gt;[95, 83, 92]&lt;/code&gt;，而这个列表中的&lt;code&gt;83&lt;/code&gt;代表了这个学生某一门课的成绩，如果想访问这个值，可以使用两次索引运算&lt;code&gt;scores[0][1]&lt;/code&gt;，其中&lt;code&gt;scores[0]&lt;/code&gt;可以得到&lt;code&gt;[95, 83, 92]&lt;/code&gt;这个列表，再次使用索引运算&lt;code&gt;[1]&lt;/code&gt;就可以获得该列表中的第二个元素。&lt;/p&gt;
&lt;p&gt;如果想通过键盘输入的方式来录入5个学生3门课程的成绩并保存在列表中，可以使用如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scores = []
for _ in range(5):
    temp = []
    for _ in range(3):
        score = int(input(&apos;请输入: &apos;))
        temp.append(score)
    scores.append(temp)
print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想通过产生随机数的方式来生成5个学生3门课程的成绩并保存在列表中，我们可以使用列表生成式，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random

scores = [[random.randrange(60, 101) for _ in range(3)] for _ in range(5)]
print(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码&lt;code&gt;[random.randrange(60, 101) for _ in range(3)] &lt;/code&gt;可以产生由3个随机整数构成的列表，我们把这段代码又放在了另一个列表生成式中作为列表的元素，这样的元素一共生成5个，最终得到了一个嵌套列表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;列表的应用&lt;/h4&gt;
&lt;p&gt;下面我们通过一个双色球随机选号的例子为大家讲解列表的应用。双色球是由中国福利彩票发行管理中心发售的乐透型彩票，每注投注号码由&lt;code&gt;6&lt;/code&gt;个红色球和&lt;code&gt;1&lt;/code&gt;个蓝色球组成。红色球号码从&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;33&lt;/code&gt;中选择，蓝色球号码从&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;16&lt;/code&gt;中选择。每注需要选择&lt;code&gt;6&lt;/code&gt;个红色球号码和&lt;code&gt;1&lt;/code&gt;个蓝色球号码，如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/6fd6176603.webp&quot; style=&quot;zoom:85%;&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：知乎上有一段对国内各种形式的彩票本质的论述相当精彩，这里分享给大家：“&lt;strong&gt;虚构一个不劳而获的人，去忽悠一群想不劳而获的人，最终养活一批真正不劳而获的人&lt;/strong&gt;”。很多对概率没有概念的人，甚至认为彩票中与不中的概率都是 50%；还有很多人认为如果中奖的概率是 1%，那么买 100 次就一定可以中奖，这些都是非常荒唐的想法。所以，&lt;strong&gt;珍爱生命，远离赌博，尤其是你对概率一无所知的情况下&lt;/strong&gt;！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面，我们通过 Python 程序来生成一组随机号码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
双色球随机选号程序

Author: 骆昊
Version: 1.0
&quot;&quot;&quot;
import random

red_balls = list(range(1, 34))
selected_balls = []
# 添加6个红色球到选中列表
for _ in range(6):
    # 生成随机整数代表选中的红色球的索引位置
    index = random.randrange(len(red_balls))
    # 将选中的球从红色球列表中移除并添加到选中列表
    selected_balls.append(red_balls.pop(index))
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
    print(f&apos;\033[031m{ball:0&amp;gt;2d}\033[0m&apos;, end=&apos; &apos;)
# 随机选择1个蓝色球
blue_ball = random.randrange(1, 17)
# 输出选中的蓝色球
print(f&apos;\033[034m{blue_ball:0&amp;gt;2d}\033[0m&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码中&lt;code&gt;print(f&apos;\033[0m...\033[0m&apos;)&lt;/code&gt;是为了控制输出内容的颜色，红色球输出成红色，蓝色球输出成蓝色。其中省略号代表我们要输出的内容，&lt;code&gt;\033[0m&lt;/code&gt;是一个控制码，表示关闭所有属性，也就是说之前的控制码将会失效，你也可以将其简单的理解为一个定界符，&lt;code&gt;m&lt;/code&gt;前面的&lt;code&gt;0&lt;/code&gt;表示控制台的显示方式为默认值，&lt;code&gt;0&lt;/code&gt;可以省略，&lt;code&gt;1&lt;/code&gt;表示高亮，&lt;code&gt;5&lt;/code&gt;表示闪烁，&lt;code&gt;7&lt;/code&gt;表示反显等。在&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;m&lt;/code&gt;的中间，我们可以写上代表颜色的数字，比如&lt;code&gt;30&lt;/code&gt;代表黑色，&lt;code&gt;31&lt;/code&gt;代表红色，&lt;code&gt;32&lt;/code&gt;代表绿色，&lt;code&gt;33&lt;/code&gt;代表黄色，&lt;code&gt;34&lt;/code&gt;代表蓝色等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们还可以利用&lt;code&gt;random&lt;/code&gt;模块提供的&lt;code&gt;sample&lt;/code&gt;和&lt;code&gt;choice&lt;/code&gt;函数来简化上面的代码，前者可以实现无放回随机抽样，后者可以实现随机抽取一个元素，修改后的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
双色球随机选号程序

Author: 骆昊
Version: 1.1
&quot;&quot;&quot;
import random

red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
# 从红色球列表中随机抽出6个红色球（无放回抽样）
selected_balls = random.sample(red_balls, 6)
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
    print(f&apos;\033[031m{ball:0&amp;gt;2d}\033[0m&apos;, end=&apos; &apos;)
# 从蓝色球列表中随机抽出1个蓝色球
blue_ball = random.choice(blue_balls)
# 输出选中的蓝色球
print(f&apos;\033[034m{blue_ball:0&amp;gt;2d}\033[0m&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要实现随机生成&lt;code&gt;N&lt;/code&gt;注号码，我们只需要将上面的代码放到一个&lt;code&gt;N&lt;/code&gt;次的循环中，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
双色球随机选号程序

Author: 骆昊
Version: 1.2
&quot;&quot;&quot;
import random

n = int(input(&apos;生成几注号码: &apos;))
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
for _ in range(n):
    # 从红色球列表中随机抽出6个红色球（无放回抽样）
    selected_balls = random.sample(red_balls, 6)
    # 对选中的红色球排序
    selected_balls.sort()
    # 输出选中的红色球
    for ball in selected_balls:
        print(f&apos;\033[031m{ball:0&amp;gt;2d}\033[0m&apos;, end=&apos; &apos;)
    # 从蓝色球列表中随机抽出1个蓝色球
    blue_ball = random.choice(blue_balls)
    # 输出选中的蓝色球
    print(f&apos;\033[034m{blue_ball:0&amp;gt;2d}\033[0m&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在 PyCharm 中运行上面的代码，输入&lt;code&gt;5&lt;/code&gt;，运行效果如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/18b716687d.webp&quot; style=&quot;zoom:35%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;这里顺便给大家介绍一个名为 rich 的 Python 三方库，它可以帮助我们用最简单的方式产生最漂亮的输出，你可以在终端中使用 Python 包管理工具 pip 来安装这个三方库，对于使用 PyCharm 的用户，当然要在 PyCharm 的终端窗口使用 pip 命令将 rich 安装到项目的虚拟环境中，命令如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install rich
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/c58c95d4e0.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;如上图所示，rich 安装成功后，我们可以用如下所示的代码来控制输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
双色球随机选号程序

Author: 骆昊
Version: 1.3
&quot;&quot;&quot;
import random

from rich.console import Console
from rich.table import Table

# 创建控制台
console = Console()

n = int(input(&apos;生成几注号码: &apos;))
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]

# 创建表格并添加表头
table = Table(show_header=True)
for col_name in (&apos;序号&apos;, &apos;红球&apos;, &apos;蓝球&apos;):
    table.add_column(col_name, justify=&apos;center&apos;)

for i in range(n):
    selected_balls = random.sample(red_balls, 6)
    selected_balls.sort()
    blue_ball = random.choice(blue_balls)
    # 向表格中添加行（序号，红色球，蓝色球）
    table.add_row(
        str(i + 1),
        f&apos;[red]{&quot; &quot;.join([f&quot;{ball:0&amp;gt;2d}&quot; for ball in selected_balls])}[/red]&apos;,
        f&apos;[blue]{blue_ball:0&amp;gt;2d}[/blue]&apos;
    )

# 通过控制台输出表格
console.print(table)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码第 31 行使用了列表生成式语法将红色球号码处理成字符串并保存在一个列表中，&lt;code&gt;&quot; &quot;.join([...])&lt;/code&gt;是将列表中的多个字符串用空格拼接成一个完整的字符串，如果不理解可以先放放。字符串中的&lt;code&gt;[red]...[/red]&lt;/code&gt;用来设置输出颜色为红色，第 32 行的&lt;code&gt;[blue]...[/blue]&lt;/code&gt;用来设置输出颜色为蓝色。更多关于 rich 库的知识，可以参考&lt;a href=&quot;https://github.com/textualize/rich/blob/master/README.cn.md&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最终的输出如下图所示，看着这样的输出，是不是心情更美好了一些。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/9e1e98e277.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 中的列表底层是一个可以动态扩容的数组，列表元素在计算机内存中是连续存储的，所以可以实现随机访问（通过一个有效的索引获取对应的元素且操作时间与列表元素个数无关）。我们可以暂时不去触碰这些底层的存储细节，也不需要大家理解列表每个方法的渐近时间复杂度（执行方法耗费的时间跟列表元素个数之间的关系），大家先学会用列表解决工作中的问题，我想这一点更为重要。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10 常用数据结构之元组&lt;/h2&gt;
&lt;p&gt;前面的两节课，我们为大家讲解了 Python 中的列表，它是一种容器型的数据类型，通过列表类型的变量，我们可以保存多个数据并通过循环实现对数据的批量操作。当然，Python 中还有其他容器型的数据类型，接下来我们就为大家讲解另一种容器型的数据类型，它的名字叫元组（tuple）。&lt;/p&gt;
&lt;h4&gt;元组的定义和运算&lt;/h4&gt;
&lt;p&gt;在 Python 语言中，元组也是多个元素按照一定顺序构成的序列。元组和列表的不同之处在于，&lt;strong&gt;元组是不可变类型&lt;/strong&gt;，这就意味着元组类型的变量一旦定义，其中的元素不能再添加或删除，而且元素的值也不能修改。如果试图修改元组中的元素，将引发&lt;code&gt;TypeError&lt;/code&gt;错误，导致程序崩溃。定义元组通常使用形如&lt;code&gt;(x, y, z)&lt;/code&gt;的字面量语法，元组类型支持的运算符跟列表是一样的，我们可以看看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 定义一个三元组
t1 = (35, 12, 98)
# 定义一个四元组
t2 = (&apos;骆昊&apos;, 45, True, &apos;四川成都&apos;)

# 查看变量的类型
print(type(t1))  # &amp;lt;class &apos;tuple&apos;&amp;gt;
print(type(t2))  # &amp;lt;class &apos;tuple&apos;&amp;gt;

# 查看元组中元素的数量
print(len(t1))  # 3
print(len(t2))  # 4

# 索引运算
print(t1[0])    # 35
print(t1[2])    # 98
print(t2[-1])   # 四川成都

# 切片运算
print(t2[:2])   # (&apos;骆昊&apos;, 45)
print(t2[::3])  # (&apos;骆昊&apos;, &apos;四川成都&apos;)

# 循环遍历元组中的元素
for elem in t1:
    print(elem)

# 成员运算
print(12 in t1)         # True
print(99 in t1)         # False
print(&apos;Hao&apos; not in t2)  # True

# 拼接运算
t3 = t1 + t2
print(t3)  # (35, 12, 98, &apos;骆昊&apos;, 45, True, &apos;四川成都&apos;)

# 比较运算
print(t1 == t3)            # False
print(t1 &amp;gt;= t3)            # False
print(t1 &amp;lt;= (35, 11, 99))  # False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个元组中如果有两个元素，我们就称之为二元组；一个元组中如果五个元素，我们就称之为五元组。需要提醒大家注意的是，&lt;code&gt;()&lt;/code&gt;表示空元组，但是如果元组中只有一个元素，需要加上一个逗号，否则&lt;code&gt;()&lt;/code&gt;就不是代表元组的字面量语法，而是改变运算优先级的圆括号，所以&lt;code&gt;(&apos;hello&apos;, )&lt;/code&gt;和&lt;code&gt;(100, )&lt;/code&gt;才是一元组，而&lt;code&gt;(&apos;hello&apos;)&lt;/code&gt;和&lt;code&gt;(100)&lt;/code&gt;只是字符串和整数。我们可以通过下面的代码来加以验证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = ()
print(type(a))  # &amp;lt;class &apos;tuple&apos;&amp;gt;
b = (&apos;hello&apos;)
print(type(b))  # &amp;lt;class &apos;str&apos;&amp;gt;
c = (100)
print(type(c))  # &amp;lt;class &apos;int&apos;&amp;gt;
d = (&apos;hello&apos;, )
print(type(d))  # &amp;lt;class &apos;tuple&apos;&amp;gt;
e = (100, )
print(type(e))  # &amp;lt;class &apos;tuple&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;打包和解包操作&lt;/h4&gt;
&lt;p&gt;当我们把多个用逗号分隔的值赋给一个变量时，多个值会打包成一个元组类型；当我们把一个元组赋值给多个变量时，元组会解包成多个值然后分别赋给对应的变量，如下面的代码所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 打包操作
a = 1, 10, 100
print(type(a))  # &amp;lt;class &apos;tuple&apos;&amp;gt;
print(a)        # (1, 10, 100)
# 解包操作
i, j, k = a
print(i, j, k)  # 1 10 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在解包时，如果解包出来的元素个数和变量个数不对应，会引发&lt;code&gt;ValueError&lt;/code&gt;异常，错误信息为：&lt;code&gt;too many values to unpack&lt;/code&gt;（解包的值太多）或&lt;code&gt;not enough values to unpack&lt;/code&gt;（解包的值不足）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 1, 10, 100, 1000
# i, j, k = a             # ValueError: too many values to unpack (expected 3)
# i, j, k, l, m, n = a    # ValueError: not enough values to unpack (expected 6, got 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一种解决变量个数少于元素的个数方法，就是使用星号表达式。通过星号表达式，我们可以让一个变量接收多个值，代码如下所示。需要注意两点：首先，用星号表达式修饰的变量会变成一个列表，列表中有0个或多个元素；其次，在解包语法中，星号表达式只能出现一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 1, 10, 100, 1000
i, j, *k = a
print(i, j, k)        # 1 10 [100, 1000]
i, *j, k = a
print(i, j, k)        # 1 [10, 100] 1000
*i, j, k = a
print(i, j, k)        # [1, 10] 100 1000
*i, j = a
print(i, j)           # [1, 10, 100] 1000
i, *j = a
print(i, j)           # 1 [10, 100, 1000]
i, j, k, *l = a
print(i, j, k, l)     # 1 10 100 [1000]
i, j, k, l, *m = a
print(i, j, k, l, m)  # 1 10 100 1000 []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明一点，解包语法对所有的序列都成立，这就意味着我们之前讲的列表、&lt;code&gt;range&lt;/code&gt;函数构造的范围序列甚至字符串都可以使用解包语法。大家可以尝试运行下面的代码，看看会出现怎样的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b, *c = range(1, 10)
print(a, b, c)
a, b, c = [1, 10, 100]
print(a, b, c)
a, *b, c = &apos;hello&apos;
print(a, b, c)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;交换变量的值&lt;/h4&gt;
&lt;p&gt;交换变量的值是写代码时经常用到的一个操作，但是在很多编程语言中，交换两个变量的值都需要借助一个中间变量才能做到，如果不用中间变量就需要使用比较晦涩的位运算来实现。在 Python 中，交换两个变量&lt;code&gt;a&lt;/code&gt;和&lt;code&gt;b&lt;/code&gt;的值只需要使用如下所示的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b = b, a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，如果要将三个变量&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt;的值互换，即&lt;code&gt;b&lt;/code&gt;的值赋给&lt;code&gt;a&lt;/code&gt;，&lt;code&gt;c&lt;/code&gt;的值赋给&lt;code&gt;b&lt;/code&gt;，&lt;code&gt;a&lt;/code&gt;的值赋给&lt;code&gt;c&lt;/code&gt;，也可以如法炮制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b, c = b, c, a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，上面的操作并没有用到打包和解包语法，Python 的字节码指令中有&lt;code&gt;ROT_TWO&lt;/code&gt;和&lt;code&gt;ROT_THREE&lt;/code&gt;这样的指令可以直接实现这个操作，效率是非常高的。但是如果有多于三个变量的值要依次互换，这个时候是没有直接可用的字节码指令的，需要通过打包解包的方式来完成变量之间值的交换。&lt;/p&gt;
&lt;h4&gt;元组和列表的比较&lt;/h4&gt;
&lt;p&gt;这里还有一个非常值得探讨的问题，Python 中已经有了列表类型，为什么还需要元组这样的类型呢？这个问题对于初学者来说似乎有点困难，不过没有关系，我们先抛出观点，大家可以一边学习一边慢慢体会。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;元组是不可变类型，&lt;strong&gt;不可变类型更适合多线程环境&lt;/strong&gt;，因为它降低了并发访问变量的同步化开销。关于这一点，我们会在后面讲解并发编程的时候跟大家一起探讨。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元组是不可变类型，通常&lt;strong&gt;不可变类型在创建时间上优于对应的可变类型&lt;/strong&gt;。我们可以使用&lt;code&gt;timeit&lt;/code&gt;模块的&lt;code&gt;timeit&lt;/code&gt;函数来看看创建保存相同元素的元组和列表各自花费的时间，&lt;code&gt;timeit&lt;/code&gt;函数的&lt;code&gt;number&lt;/code&gt;参数表示代码执行的次数。下面的代码中，我们分别创建了保存&lt;code&gt;1&lt;/code&gt;到&lt;code&gt;9&lt;/code&gt;的整数的列表和元组，每个操作执行&lt;code&gt;10000000&lt;/code&gt;次，统计运行时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import timeit

print(&apos;%.3f 秒&apos; % timeit.timeit(&apos;[1, 2, 3, 4, 5, 6, 7, 8, 9]&apos;, number=10000000))
print(&apos;%.3f 秒&apos; % timeit.timeit(&apos;(1, 2, 3, 4, 5, 6, 7, 8, 9)&apos;, number=10000000))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.635 秒
0.078 秒
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面代码的执行结果因软硬件系统而异，在我目前使用的电脑上，执行&lt;code&gt;10000000&lt;/code&gt;次创建列表的操作时间是&lt;code&gt;0.635&lt;/code&gt;秒，而执行&lt;code&gt;10000000&lt;/code&gt;次创建元组的操作时间是&lt;code&gt;0.078&lt;/code&gt;秒，显然创建元组更快且二者时间上有数量级的差别。大家可以在自己的电脑上执行这段代码，把你的执行结果放到评论区，看看谁的电脑更厉害。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，Python 中的元组和列表类型是可以相互转换的，我们可以通过下面的代码来完成该操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;infos = (&apos;骆昊&apos;, 45, True, &apos;四川成都&apos;)
# 将元组转换成列表
print(list(infos))  # [&apos;骆昊&apos;, 45, True, &apos;四川成都&apos;]

frts = [&apos;apple&apos;, &apos;banana&apos;, &apos;orange&apos;]
# 将列表转换成元组
print(tuple(frts))  # (&apos;apple&apos;, &apos;banana&apos;, &apos;orange&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;列表和元组都是容器型的数据类型&lt;/strong&gt;，即一个变量可以保存多个数据，而且它们都是按一定顺序组织元素的有序容器。&lt;strong&gt;列表是可变数据类型&lt;/strong&gt;，&lt;strong&gt;元组是不可变数据类型&lt;/strong&gt;，所以列表可以添加元素、删除元素、清空元素、排序反转，但这些操作对元组来说是不成立的。列表和元组都可以支持&lt;strong&gt;拼接运算&lt;/strong&gt;、&lt;strong&gt;成员运算&lt;/strong&gt;、&lt;strong&gt;索引运算&lt;/strong&gt;、&lt;strong&gt;切片运算&lt;/strong&gt;等操作，后面我们要讲到的字符串类型也支持这些运算，因为字符串就是字符按一定顺序构成的序列，在这一点上三者并没有什么区别。我们&lt;strong&gt;推荐大家使用列表的生成式语法来创建列表&lt;/strong&gt;，它不仅好用而且效率很高，是 Python 语言中非常有特色的语法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11 常用数据结构之字符串&lt;/h2&gt;
&lt;p&gt;第二次世界大战促使了现代电子计算机的诞生，世界上的第一台通用电子计算机名叫 ENIAC（电子数值积分计算机），诞生于美国的宾夕法尼亚大学，占地 167 平米，重量约 27 吨，每秒钟大约能够完成约 5000 次浮点运算，如下图所示。ENIAC 诞生之后被应用于导弹弹道的计算，而数值计算也是现代电子计算机最为重要的一项功能。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/6b5d8e0ad3.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;随着时间的推移，虽然数值运算仍然是计算机日常工作中最为重要的组成部分，但是今天的计算机还要处理大量的以文本形式存在的信息。如果我们希望通过 Python 程序来操作本这些文本信息，就必须要先了解字符串这种数据类型以及与它相关的运算和方法。&lt;/p&gt;
&lt;h4&gt;字符串的定义&lt;/h4&gt;
&lt;p&gt;所谓&lt;strong&gt;字符串&lt;/strong&gt;，就是&lt;strong&gt;由零个或多个字符组成的有限序列&lt;/strong&gt;，一般记为：&lt;/p&gt;
&lt;p&gt;$$
s = a_1a_2 \cdots a_n ,,,,, (0 \le n \le \infty)
$$&lt;/p&gt;
&lt;p&gt;在 Python 程序中，我们把单个或多个字符用单引号或者双引号包围起来，就可以表示一个字符串。字符串中的字符可以是特殊符号、英文字母、中文字符、日文的平假名或片假名、希腊字母、Emoji 字符（如：💩、🐷、🀄️）等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;hello, world!&apos;
s2 = &quot;你好，世界！❤️&quot;
s3 = &apos;&apos;&apos;hello,
wonderful
world!&apos;&apos;&apos;
print(s1)
print(s2)
print(s3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;转义字符&lt;/h5&gt;
&lt;p&gt;我们可以在字符串中使用&lt;code&gt;\&lt;/code&gt;（反斜杠）来表示转义，也就是说&lt;code&gt;\&lt;/code&gt;后面的字符不再是它原来的意义，例如：&lt;code&gt;\n&lt;/code&gt;不是代表字符&lt;code&gt;\&lt;/code&gt;和字符&lt;code&gt;n&lt;/code&gt;，而是表示换行；&lt;code&gt;\t&lt;/code&gt;也不是代表字符&lt;code&gt;\&lt;/code&gt;和字符&lt;code&gt;t&lt;/code&gt;，而是表示制表符。所以如果字符串本身又包含了&lt;code&gt;&apos;&lt;/code&gt;、&lt;code&gt;&quot;&lt;/code&gt;、&lt;code&gt;\&lt;/code&gt;这些特殊的字符，必须要通过&lt;code&gt;\&lt;/code&gt;进行转义处理。例如要输出一个带单引号或反斜杠的字符串，需要用如下所示的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;\&apos;hello, world!\&apos;&apos;
s2 = &apos;\\hello, world!\\&apos;
print(s1)
print(s2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;原始字符串&lt;/h5&gt;
&lt;p&gt;Python 中有一种以&lt;code&gt;r&lt;/code&gt;或&lt;code&gt;R&lt;/code&gt;开头的字符串，这种字符串被称为原始字符串，意思是字符串中的每个字符都是它本来的含义，没有所谓的转义字符。例如，在字符串&lt;code&gt;&apos;hello\n&apos;&lt;/code&gt;中，&lt;code&gt;\n&lt;/code&gt;表示换行；而在&lt;code&gt;r&apos;hello\n&apos;&lt;/code&gt;中，&lt;code&gt;\n&lt;/code&gt;不再表示换行，就是字符&lt;code&gt;\&lt;/code&gt;和字符&lt;code&gt;n&lt;/code&gt;。大家可以运行下面的代码，看看会输出什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;\it \is \time \to \read \now&apos;
s2 = r&apos;\it \is \time \to \read \now&apos;
print(s1)
print(s2)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的变量&lt;code&gt;s1&lt;/code&gt;中，&lt;code&gt;\t&lt;/code&gt;、&lt;code&gt;\r&lt;/code&gt;和&lt;code&gt;\n&lt;/code&gt;都是转义字符。&lt;code&gt;\t&lt;/code&gt;是制表符（table），&lt;code&gt;\n&lt;/code&gt;是换行符（new line），&lt;code&gt;\r&lt;/code&gt;是回车符（carriage return）相当于让输出回到了行首。对比一下两个&lt;code&gt;print&lt;/code&gt;函数的输出，看看到底有什么区别！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;字符的特殊表示&lt;/h5&gt;
&lt;p&gt;Python 中还允许在&lt;code&gt;\&lt;/code&gt;后面还可以跟一个八进制或者十六进制数来表示字符，例如&lt;code&gt;\141&lt;/code&gt;和&lt;code&gt;\x61&lt;/code&gt;都代表小写字母&lt;code&gt;a&lt;/code&gt;，前者是八进制的表示法，后者是十六进制的表示法。另外一种表示字符的方式是在&lt;code&gt;\u&lt;/code&gt;后面跟 Unicode 字符编码，例如&lt;code&gt;\u9a86\u660a&lt;/code&gt;代表的是中文“骆昊”。运行下面的代码，看看输出了什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;\141\142\143\x61\x62\x63&apos;
s2 = &apos;\u9a86\u660a&apos;
print(s1)
print(s2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字符串的运算&lt;/h4&gt;
&lt;p&gt;Python 语言为字符串类型提供了非常丰富的运算符，有很多运算符跟列表类型的运算符作用类似。例如，我们可以使用&lt;code&gt;+&lt;/code&gt;运算符来实现字符串的拼接，可以使用&lt;code&gt;*&lt;/code&gt;运算符来重复一个字符串的内容，可以使用&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;not in&lt;/code&gt;来判断一个字符串是否包含另外一个字符串，我们也可以用&lt;code&gt;[]&lt;/code&gt;和&lt;code&gt;[:]&lt;/code&gt;运算符从字符串取出某个字符或某些字符。&lt;/p&gt;
&lt;h5&gt;拼接和重复&lt;/h5&gt;
&lt;p&gt;下面的例子演示了使用&lt;code&gt;+&lt;/code&gt;和&lt;code&gt;*&lt;/code&gt;运算符来实现字符串的拼接和重复操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;hello&apos; + &apos;, &apos; + &apos;world&apos;
print(s1)    # hello, world
s2 = &apos;!&apos; * 3
print(s2)    # !!!
s1 += s2
print(s1)    # hello, world!!!
s1 *= 2
print(s1)    # hello, world!!!hello, world!!!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用&lt;code&gt;*&lt;/code&gt;实现字符串的重复是非常有意思的一个运算符，在很多编程语言中，要表示一个有10个&lt;code&gt;a&lt;/code&gt;的字符串，你只能写成&lt;code&gt;&apos;aaaaaaaaaa&apos;&lt;/code&gt;，但是在 Python 中，你可以写成&lt;code&gt;&apos;a&apos; * 10&lt;/code&gt;。你可能觉得&lt;code&gt;&apos;aaaaaaaaaa&apos;&lt;/code&gt;这种写法也没有什么不方便的，但是请想一想，如果字符&lt;code&gt;a&lt;/code&gt;要重复100次或者1000次又会如何呢？&lt;/p&gt;
&lt;h5&gt;比较运算&lt;/h5&gt;
&lt;p&gt;对于两个字符串类型的变量，可以直接使用比较运算符来判断两个字符串的相等性或比较大小。需要说明的是，因为字符串在计算机内存中也是以二进制形式存在的，那么字符串的大小比较比的是每个字符对应的编码的大小。例如&lt;code&gt;A&lt;/code&gt;的编码是&lt;code&gt;65&lt;/code&gt;， 而&lt;code&gt;a&lt;/code&gt;的编码是&lt;code&gt;97&lt;/code&gt;，所以&lt;code&gt;&apos;A&apos; &amp;lt; &apos;a&apos;&lt;/code&gt;的结果相当于就是&lt;code&gt;65 &amp;lt; 97&lt;/code&gt;的结果，这里很显然是&lt;code&gt;True&lt;/code&gt;；而&lt;code&gt;&apos;boy&apos; &amp;lt; &apos;bad&apos;&lt;/code&gt;，因为第一个字符都是&lt;code&gt;&apos;b&apos;&lt;/code&gt;比不出大小，所以实际比较的是第二个字符的大小，显然&lt;code&gt;&apos;o&apos; &amp;lt; &apos;a&apos;&lt;/code&gt;的结果是&lt;code&gt;False&lt;/code&gt;，所以&lt;code&gt;&apos;boy&apos; &amp;lt; &apos;bad&apos;&lt;/code&gt;的结果是&lt;code&gt;False&lt;/code&gt;。如果不清楚两个字符对应的编码到底是多少，可以使用&lt;code&gt;ord&lt;/code&gt;函数来获得，之前我们有提到过这个函数。例如&lt;code&gt;ord(&apos;A&apos;)&lt;/code&gt;的值是&lt;code&gt;65&lt;/code&gt;，而&lt;code&gt;ord(&apos;昊&apos;)&lt;/code&gt;的值是&lt;code&gt;26122&lt;/code&gt;。下面的代码展示了字符串的比较运算，请大家仔细看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;a whole new world&apos;
s2 = &apos;hello world&apos;
print(s1 == s2)             # False
print(s1 &amp;lt; s2)              # True
print(s1 == &apos;hello world&apos;)  # False
print(s2 == &apos;hello world&apos;)  # True
print(s2 != &apos;Hello world&apos;)  # True
s3 = &apos;骆昊&apos;
print(ord(&apos;骆&apos;))            # 39558
print(ord(&apos;昊&apos;))            # 26122
s4 = &apos;王大锤&apos;
print(ord(&apos;王&apos;))            # 29579
print(ord(&apos;大&apos;))            # 22823
print(ord(&apos;锤&apos;))            # 38180
print(s3 &amp;gt;= s4)             # True
print(s3 != s4)             # True
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;成员运算&lt;/h5&gt;
&lt;p&gt;Python 中可以用&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;not in&lt;/code&gt;判断一个字符串中是否包含另外一个字符或字符串，跟列表类型一样，&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;not in&lt;/code&gt;称为成员运算符，会产生布尔值&lt;code&gt;True&lt;/code&gt;或&lt;code&gt;False&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;hello, world&apos;
s2 = &apos;goodbye, world&apos;
print(&apos;wo&apos; in s1)      # True
print(&apos;wo&apos; not in s2)  # False
print(s2 in s1)        # False
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;获取字符串长度&lt;/h5&gt;
&lt;p&gt;获取字符串长度跟获取列表元素个数一样，使用内置函数&lt;code&gt;len&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello, world&apos;
print(len(s))                 # 12
print(len(&apos;goodbye, world&apos;))  # 14
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;索引和切片&lt;/h5&gt;
&lt;p&gt;字符串的索引和切片操作跟列表、元组几乎没有区别，因为字符串也是一种有序序列，可以通过正向或反向的整数索引访问其中的元素。但是有一点需要注意，因为&lt;strong&gt;字符串是不可变类型&lt;/strong&gt;，所以&lt;strong&gt;不能通过索引运算修改字符串中的字符&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;abc123456&apos;
n = len(s)
print(s[0], s[-n])    # a a
print(s[n-1], s[-1])  # 6 6
print(s[2], s[-7])    # c c
print(s[5], s[-4])    # 3 3
print(s[2:5])         # c12
print(s[-7:-4])       # c12
print(s[2:])          # c123456
print(s[:2])          # ab
print(s[::2])         # ac246
print(s[::-1])        # 654321cba
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要再次提醒大家注意的是，在进行索引运算时，如果索引越界，会引发&lt;code&gt;IndexError&lt;/code&gt;异常，错误提示信息为：&lt;code&gt;string index out of range&lt;/code&gt;（字符串索引超出范围）。&lt;/p&gt;
&lt;h4&gt;字符的遍历&lt;/h4&gt;
&lt;p&gt;如果希望遍历字符串中的每个字符，可以使用&lt;code&gt;for-in&lt;/code&gt;循环，有如下所示的两种方式。&lt;/p&gt;
&lt;p&gt;方式一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello&apos;
for i in range(len(s)):
    print(s[i])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方式二：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello&apos;
for elem in s:
    print(elem)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字符串的方法&lt;/h4&gt;
&lt;p&gt;在 Python 中，我们可以通过字符串类型自带的方法对字符串进行操作和处理，假设我们有名为&lt;code&gt;foo&lt;/code&gt;的字符串，字符串有名为&lt;code&gt;bar&lt;/code&gt;的方法，那么使用字符串方法的语法是：&lt;code&gt;foo.bar()&lt;/code&gt;，这是一种通过对象引用调用对象方法的语法，跟前面使用列表方法的语法是一样的。&lt;/p&gt;
&lt;h5&gt;大小写相关操作&lt;/h5&gt;
&lt;p&gt;下面的代码演示了和字符串大小写变换相关的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;hello, world!&apos;
# 字符串首字母大写
print(s1.capitalize())  # Hello, world!
# 字符串每个单词首字母大写
print(s1.title())       # Hello, World!
# 字符串变大写
print(s1.upper())       # HELLO, WORLD!
s2 = &apos;GOODBYE&apos;
# 字符串变小写
print(s2.lower())       # goodbye
# 检查s1和s2的值
print(s1)               # hello, world
print(s2)               # GOODBYE
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：由于字符串是不可变类型，使用字符串的方法对字符串进行操作会产生新的字符串，但是原来变量的值并没有发生变化。所以上面的代码中，当我们最后检查&lt;code&gt;s1&lt;/code&gt;和&lt;code&gt;s2&lt;/code&gt;两个变量的值时，&lt;code&gt;s1&lt;/code&gt;和&lt;code&gt;s2&lt;/code&gt; 的值并没有发生变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;查找操作&lt;/h5&gt;
&lt;p&gt;如果想在一个字符串中从前向后查找有没有另外一个字符串，可以使用字符串的&lt;code&gt;find&lt;/code&gt;或&lt;code&gt;index&lt;/code&gt;方法。在使用&lt;code&gt;find&lt;/code&gt;和&lt;code&gt;index&lt;/code&gt;方法时还可以通过方法的参数来指定查找的范围，也就是查找不必从索引为&lt;code&gt;0&lt;/code&gt;的位置开始。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello, world!&apos;
print(s.find(&apos;or&apos;))      # 8
print(s.find(&apos;or&apos;, 9))   # -1
print(s.find(&apos;of&apos;))      # -1
print(s.index(&apos;or&apos;))     # 8
print(s.index(&apos;or&apos;, 9))  # ValueError: substring not found
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;code&gt;find&lt;/code&gt;方法找不到指定的字符串会返回&lt;code&gt;-1&lt;/code&gt;，&lt;code&gt;index&lt;/code&gt;方法找不到指定的字符串会引发&lt;code&gt;ValueError&lt;/code&gt;错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt;和&lt;code&gt;index&lt;/code&gt;方法还有逆向查找（从后向前查找）的版本，分别是&lt;code&gt;rfind&lt;/code&gt;和&lt;code&gt;rindex&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello world!&apos;
print(s.find(&apos;o&apos;))       # 4
print(s.rfind(&apos;o&apos;))      # 7
print(s.rindex(&apos;o&apos;))     # 7
# print(s.rindex(&apos;o&apos;, 8))  # ValueError: substring not found
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;性质判断&lt;/h5&gt;
&lt;p&gt;可以通过字符串的&lt;code&gt;startswith&lt;/code&gt;、&lt;code&gt;endswith&lt;/code&gt;来判断字符串是否以某个字符串开头和结尾；还可以用&lt;code&gt;is&lt;/code&gt;开头的方法判断字符串的特征，这些方法都返回布尔值，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;hello, world!&apos;
print(s1.startswith(&apos;He&apos;))   # False
print(s1.startswith(&apos;hel&apos;))  # True
print(s1.endswith(&apos;!&apos;))      # True
s2 = &apos;abc123456&apos;
print(s2.isdigit())  # False
print(s2.isalpha())  # False
print(s2.isalnum())  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的&lt;code&gt;isdigit&lt;/code&gt;用来判断字符串是不是完全由数字构成的，&lt;code&gt;isalpha&lt;/code&gt;用来判断字符串是不是完全由字母构成的，这里的字母指的是 Unicode 字符但不包含 Emoji 字符，&lt;code&gt;isalnum&lt;/code&gt;用来判断字符串是不是由字母和数字构成的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;格式化&lt;/h5&gt;
&lt;p&gt;在 Python 中，字符串类型可以通过&lt;code&gt;center&lt;/code&gt;、&lt;code&gt;ljust&lt;/code&gt;、&lt;code&gt;rjust&lt;/code&gt;方法做居中、左对齐和右对齐的处理。如果要在字符串的左侧补零，也可以使用&lt;code&gt;zfill&lt;/code&gt;方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello, world&apos;
print(s.center(20, &apos;*&apos;))  # ****hello, world****
print(s.rjust(20))        #         hello, world
print(s.ljust(20, &apos;~&apos;))   # hello, world~~~~~~~~
print(&apos;33&apos;.zfill(5))      # 00033
print(&apos;-33&apos;.zfill(5))     # -0033
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们之前讲过，在用&lt;code&gt;print&lt;/code&gt;函数输出字符串时，可以用下面的方式对字符串进行格式化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 321
b = 123
print(&apos;%d * %d = %d&apos; % (a, b, a * b))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，我们也可以用字符串的&lt;code&gt;format&lt;/code&gt;方法来完成字符串的格式，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 321
b = 123
print(&apos;{0} * {1} = {2}&apos;.format(a, b, a * b))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Python 3.6 开始，格式化字符串还有更为简洁的书写方式，就是在字符串前加上&lt;code&gt;f&lt;/code&gt;来格式化字符串，在这种以&lt;code&gt;f&lt;/code&gt;打头的字符串中，&lt;code&gt;{变量名}&lt;/code&gt;是一个占位符，会被变量对应的值将其替换掉，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = 321
b = 123
print(f&apos;{a} * {b} = {a * b}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要进一步控制格式化语法中变量值的形式，可以参照下面的表格来进行字符串格式化操作。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量值&lt;/th&gt;
&lt;th&gt;占位符&lt;/th&gt;
&lt;th&gt;格式化结果&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3.1415926&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:.2f}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;3.14&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;保留小数点后两位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3.1415926&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:+.2f}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;+3.14&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;带符号保留小数点后两位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:+.2f}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;-1.00&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;带符号保留小数点后两位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3.1415926&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:.0f}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;3&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不带小数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:0&amp;gt;10d}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;0000000123&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;左边补&lt;code&gt;0&lt;/code&gt;，补够10位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:x&amp;lt;10d}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;123xxxxxxx&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右边补&lt;code&gt;x&lt;/code&gt; ，补够10位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:&amp;gt;10d}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;       123&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;左边补空格，补够10位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:&amp;lt;10d}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;123       &apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右边补空格，补够10位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123456789&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:,}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;123,456,789&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;逗号分隔格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:.2%}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;12.30%&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;百分比格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;123456789&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{:.2e}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;1.23e+08&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;科学计数法格式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;修剪操作&lt;/h5&gt;
&lt;p&gt;字符串的&lt;code&gt;strip&lt;/code&gt;方法可以帮我们获得将原字符串修剪掉左右两端指定字符之后的字符串，默认是修剪空格字符。这个方法非常有实用价值，可以用来将用户输入时不小心键入的头尾空格等去掉，&lt;code&gt;strip&lt;/code&gt;方法还有&lt;code&gt;lstrip&lt;/code&gt;和&lt;code&gt;rstrip&lt;/code&gt;两个版本，相信从名字大家已经猜出来这两个方法是做什么用的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 = &apos;   jackfrued@126.com  &apos;
print(s1.strip())      # jackfrued@126.com
s2 = &apos;~你好，世界~&apos;
print(s2.lstrip(&apos;~&apos;))  # 你好，世界~
print(s2.rstrip(&apos;~&apos;))  # ~你好，世界
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;替换操作&lt;/h5&gt;
&lt;p&gt;如果希望用新的内容替换字符串中指定的内容，可以使用&lt;code&gt;replace&lt;/code&gt;方法，代码如下所示。&lt;code&gt;replace&lt;/code&gt;方法的第一个参数是被替换的内容，第二个参数是替换后的内容，还可以通过第三个参数指定替换的次数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;hello, good world&apos;
print(s.replace(&apos;o&apos;, &apos;@&apos;))     # hell@, g@@d w@rld
print(s.replace(&apos;o&apos;, &apos;@&apos;, 1))  # hell@, good world
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆分与合并&lt;/h5&gt;
&lt;p&gt;可以使用字符串的&lt;code&gt;split&lt;/code&gt;方法将一个字符串拆分为多个字符串（放在一个列表中），也可以使用字符串的&lt;code&gt;join&lt;/code&gt;方法将列表中的多个字符串连接成一个字符串，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;I love you&apos;
words = s.split()
print(words)            # [&apos;I&apos;, &apos;love&apos;, &apos;you&apos;]
print(&apos;~&apos;.join(words))  # I~love~you
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要说明的是，&lt;code&gt;split&lt;/code&gt;方法默认使用空格进行拆分，我们也可以指定其他的字符来拆分字符串，而且还可以指定最大拆分次数来控制拆分的效果，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &apos;I#love#you#so#much&apos;
words = s.split(&apos;#&apos;)
print(words)  # [&apos;I&apos;, &apos;love&apos;, &apos;you&apos;, &apos;so&apos;, &apos;much&apos;]
words = s.split(&apos;#&apos;, 2)
print(words)  # [&apos;I&apos;, &apos;love&apos;, &apos;you#so#much&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;编码和解码&lt;/h5&gt;
&lt;p&gt;Python 中除了字符串&lt;code&gt;str&lt;/code&gt;类型外，还有一种表示二进制数据的字节串类型（&lt;code&gt;bytes&lt;/code&gt;）。所谓字节串，就是&lt;strong&gt;由零个或多个字节组成的有限序列&lt;/strong&gt;。通过字符串的&lt;code&gt;encode&lt;/code&gt;方法，我们可以按照某种编码方式将字符串编码为字节串，我们也可以使用字节串的&lt;code&gt;decode&lt;/code&gt;方法，将字节串解码为字符串，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a = &apos;骆昊&apos;
b = a.encode(&apos;utf-8&apos;)
c = a.encode(&apos;gbk&apos;)
print(b)                  # b&apos;\xe9\xaa\x86\xe6\x98\x8a&apos;
print(c)                  # b&apos;\xc2\xe6\xea\xbb&apos;
print(b.decode(&apos;utf-8&apos;))  # 骆昊
print(c.decode(&apos;gbk&apos;))    # 骆昊
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果编码和解码的方式不一致，会导致乱码问题（无法再现原始的内容）或引发&lt;code&gt;UnicodeDecodeError&lt;/code&gt;错误，导致程序崩溃。&lt;/p&gt;
&lt;h5&gt;其他方法&lt;/h5&gt;
&lt;p&gt;对于字符串类型来说，还有一个常用的操作是对字符串进行匹配检查，即检查字符串是否满足某种特定的模式。例如，一个网站对用户注册信息中用户名和邮箱的检查，就属于模式匹配检查。实现模式匹配检查的工具叫做正则表达式，Python 语言通过标准库中的&lt;code&gt;re&lt;/code&gt;模块提供了对正则表达式的支持，我们会在后续的课程中为大家讲解这个知识点。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;知道如何表示和操作字符串对程序员来说是非常重要的，因为我们经常需要处理文本信息，Python 中操作字符串可以用拼接、索引、切片等运算符，也可以使用字符串类型提供的非常丰富的方法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;12 常用数据结构之集合&lt;/h2&gt;
&lt;p&gt;在学习了列表和元组之后，我们再来学习一种容器型的数据类型，它的名字叫集合（set）。说到集合这个词大家一定不会陌生，在数学课本上就有这个概念。如果我们&lt;strong&gt;把一定范围的、确定的、可以区别的事物当作一个整体来看待&lt;/strong&gt;，那么这个整体就是集合，集合中的各个事物称为集合的&lt;strong&gt;元素&lt;/strong&gt;。通常，集合需要满足以下要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;无序性&lt;/strong&gt;：一个集合中，每个元素的地位都是相同的，元素之间是无序的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互异性&lt;/strong&gt;：一个集合中，任何两个元素都是不相同的，即元素在集合中只能出现一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定性&lt;/strong&gt;：给定一个集合和一个任意元素，该元素要么属这个集合，要么不属于这个集合，二者必居其一，不允许有模棱两可的情况出现。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python 程序中的集合跟数学上的集合没有什么本质区别，需要强调的是上面所说的无序性和互异性。无序性说明集合中的元素并不像列中的元素那样存在某种次序，可以通过索引运算就能访问任意元素，&lt;strong&gt;集合并不支持索引运算&lt;/strong&gt;。另外，集合的互异性决定了&lt;strong&gt;集合中不能有重复元素&lt;/strong&gt;，这一点也是集合区别于列表的地方，我们无法将重复的元素添加到一个集合中。集合类型必然是支持&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;not in&lt;/code&gt;成员运算的，这样就可以确定一个元素是否属于集合，也就是上面所说的集合的确定性。&lt;strong&gt;集合的成员运算在性能上要优于列表的成员运算&lt;/strong&gt;，这是集合的底层存储特性决定的，此处我们暂时不做讨论，大家记住这个结论即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：集合底层使用了哈希存储（散列存储），对哈希存储不了解的读者可以先看看“Hello 算法”网站对&lt;a href=&quot;https://www.hello-algo.com/chapter_hashing/&quot;&gt;哈希表&lt;/a&gt;的讲解，感谢作者的开源精神。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;创建集合&lt;/h4&gt;
&lt;p&gt;在 Python 中，创建集合可以使用&lt;code&gt;{}&lt;/code&gt;字面量语法，&lt;code&gt;{}&lt;/code&gt;中需要至少有一个元素，因为没有元素的&lt;code&gt;{}&lt;/code&gt;并不是空集合而是一个空字典，字典类型我们会在下一节课中为大家介绍。当然，也可以使用 Python 内置函数&lt;code&gt;set&lt;/code&gt;来创建一个集合，准确的说&lt;code&gt;set&lt;/code&gt;并不是一个函数，而是创建集合对象的构造器，这个知识点会在后面讲解面向对象编程的地方为大家介绍。我们可以使用&lt;code&gt;set&lt;/code&gt;函数创建一个空集合，也可以用它将其他序列转换成集合，例如：&lt;code&gt;set(&apos;hello&apos;)&lt;/code&gt;会得到一个包含了&lt;code&gt;4&lt;/code&gt;个字符的集合（重复的字符&lt;code&gt;l&lt;/code&gt;只会在集合中出现一次）。除了这两种方式，还可以使用生成式语法来创建集合，就像我们之前用生成式语法创建列表那样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {1, 2, 3, 3, 3, 2}
print(set1)

set2 = {&apos;banana&apos;, &apos;pitaya&apos;, &apos;apple&apos;, &apos;apple&apos;, &apos;banana&apos;, &apos;grape&apos;}
print(set2)

set3 = set(&apos;hello&apos;)
print(set3)

set4 = set([1, 2, 2, 3, 3, 3, 2, 1])
print(set4)

set5 = {num for num in range(1, 20) if num % 3 == 0 or num % 7 == 0}
print(set5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要提醒大家，集合中的元素必须是&lt;code&gt;hashable&lt;/code&gt;类型，所谓&lt;code&gt;hashable&lt;/code&gt;类型指的是能够计算出哈希码的数据类型，通常不可变类型都是&lt;code&gt;hashable&lt;/code&gt;类型，如整数（&lt;code&gt;int&lt;/code&gt;）、浮点小数（&lt;code&gt;float&lt;/code&gt;）、布尔值（&lt;code&gt;bool&lt;/code&gt;）、字符串（&lt;code&gt;str&lt;/code&gt;）、元组（&lt;code&gt;tuple&lt;/code&gt;）等。可变类型都不是&lt;code&gt;hashable&lt;/code&gt;类型，因为可变类型无法计算出确定的哈希码，所以它们不能放到集合中。例如：我们不能将列表作为集合中的元素；同理，由于集合本身也是可变类型，所以集合也不能作为集合中的元素。我们可以创建出嵌套列表（列表的元素也是列表），但是我们不能创建出嵌套的集合，这一点在使用集合的时候一定要引起注意。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;温馨提示&lt;/strong&gt;：如果不理解上面提到的哈希码、哈希存储这些概念，可以先放放，因为它并不影响你继续学习和使用 Python 语言。当然，如果是计算机专业的小伙伴，不理解哈希存储是很难被原谅的，要赶紧去补课了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;元素的遍历&lt;/h4&gt;
&lt;p&gt;我们可以通过&lt;code&gt;len&lt;/code&gt;函数来获得集合中有多少个元素，但是我们不能通过索引运算来遍历集合中的元素，因为集合元素并没有特定的顺序。当然，要实现对集合元素的遍历，我们仍然可以使用&lt;code&gt;for-in&lt;/code&gt;循环，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {&apos;Python&apos;, &apos;C++&apos;, &apos;Java&apos;, &apos;Kotlin&apos;, &apos;Swift&apos;}
for elem in set1:
    print(elem)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：大家看看上面代码的运行结果，通过单词输出的顺序体会一下集合的无序性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;集合的运算&lt;/h4&gt;
&lt;p&gt;Python 为集合类型提供了非常丰富的运算，主要包括：成员运算、交集运算、并集运算、差集运算、比较运算（相等性、子集、超集）等。&lt;/p&gt;
&lt;h5&gt;成员运算&lt;/h5&gt;
&lt;p&gt;可以通过成员运算&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;not in &lt;/code&gt;检查元素是否在集合中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {11, 12, 13, 14, 15}
print(10 in set1)      # False 
print(15 in set1)      # True
set2 = {&apos;Python&apos;, &apos;Java&apos;, &apos;C++&apos;, &apos;Swift&apos;}
print(&apos;Ruby&apos; in set2)  # False
print(&apos;Java&apos; in set2)  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;二元运算&lt;/h5&gt;
&lt;p&gt;集合的二元运算主要指集合的交集、并集、差集、对称差等运算，这些运算可以通过运算符来实现，也可以通过集合类型的方法来实现，代码如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/d3c3382674.webp&quot; style=&quot;zoom:50%;&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {1, 2, 3, 4, 5, 6, 7}
set2 = {2, 4, 6, 8, 10}

# 交集
print(set1 &amp;amp; set2)                      # {2, 4, 6}
print(set1.intersection(set2))          # {2, 4, 6}

# 并集
print(set1 | set2)                      # {1, 2, 3, 4, 5, 6, 7, 8, 10}
print(set1.union(set2))                 # {1, 2, 3, 4, 5, 6, 7, 8, 10}

# 差集
print(set1 - set2)                      # {1, 3, 5, 7}
print(set1.difference(set2))            # {1, 3, 5, 7}

# 对称差
print(set1 ^ set2)                      # {1, 3, 5, 7, 8, 10}
print(set1.symmetric_difference(set2))  # {1, 3, 5, 7, 8, 10}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码可以看出，对两个集合求交集，&lt;code&gt;&amp;amp;&lt;/code&gt;运算符和&lt;code&gt;intersection&lt;/code&gt;方法的作用是完全相同的，使用运算符的方式显然更直观且代码也更简短。需要说明的是，集合的二元运算还可以跟赋值运算一起构成复合赋值运算，例如：&lt;code&gt;set1 |= set2&lt;/code&gt;相当于&lt;code&gt;set1 = set1 | set2&lt;/code&gt;，跟&lt;code&gt;|=&lt;/code&gt;作用相同的方法是&lt;code&gt;update&lt;/code&gt;；&lt;code&gt;set1 &amp;amp;= set2&lt;/code&gt;相当于&lt;code&gt;set1 = set1 &amp;amp; set2&lt;/code&gt;，跟&lt;code&gt;&amp;amp;=&lt;/code&gt;作用相同的方法是&lt;code&gt;intersection_update&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {1, 3, 5, 7}
set2 = {2, 4, 6}
set1 |= set2
# set1.update(set2)
print(set1)  # {1, 2, 3, 4, 5, 6, 7}
set3 = {3, 6, 9}
set1 &amp;amp;= set3
# set1.intersection_update(set3)
print(set1)  # {3, 6}
set2 -= set1
# set2.difference_update(set1)
print(set2)  # {2, 4}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;比较运算&lt;/h5&gt;
&lt;p&gt;两个集合可以用&lt;code&gt;==&lt;/code&gt;和&lt;code&gt;!=&lt;/code&gt;进行相等性判断，如果两个集合中的元素完全相同，那么&lt;code&gt;==&lt;/code&gt;比较的结果就是&lt;code&gt;True&lt;/code&gt;，否则就是&lt;code&gt;False&lt;/code&gt;。如果集合&lt;code&gt;A&lt;/code&gt;的任意一个元素都是集合&lt;code&gt;B&lt;/code&gt;的元素，那么集合&lt;code&gt;A&lt;/code&gt;称为集合&lt;code&gt;B&lt;/code&gt;的子集，即对于 $\small{\forall{a} \in {A}}$ ，均有 $\small{{a} \in {B}}$ ，则 $\small{{A} \subseteq {B}}$ ，&lt;code&gt;A&lt;/code&gt;是&lt;code&gt;B&lt;/code&gt;的子集，反过来也可以称&lt;code&gt;B&lt;/code&gt;是&lt;code&gt;A&lt;/code&gt;的超集。如果&lt;code&gt;A&lt;/code&gt;是&lt;code&gt;B&lt;/code&gt;的子集且&lt;code&gt;A&lt;/code&gt;不等于&lt;code&gt;B&lt;/code&gt;，那么&lt;code&gt;A&lt;/code&gt;就是&lt;code&gt;B&lt;/code&gt;的真子集。Python 为集合类型提供了判断子集和超集的运算符，其实就是我们非常熟悉的&lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;lt;=&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;gt;=&lt;/code&gt;这些运算符。当然，我们也可以通过集合类型的方法&lt;code&gt;issubset&lt;/code&gt;和&lt;code&gt;issuperset&lt;/code&gt;来判断集合之间的关系，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {1, 3, 5}
set2 = {1, 2, 3, 4, 5}
set3 = {5, 4, 3, 2, 1}

print(set1 &amp;lt; set2)   # True
print(set1 &amp;lt;= set2)  # True
print(set2 &amp;lt; set3)   # False
print(set2 &amp;lt;= set3)  # True
print(set2 &amp;gt; set1)   # True
print(set2 == set3)  # True

print(set1.issubset(set2))    # True
print(set2.issuperset(set1))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码中，&lt;code&gt;set1 &amp;lt; set2&lt;/code&gt;判断&lt;code&gt;set1&lt;/code&gt;是不是&lt;code&gt;set2&lt;/code&gt;的真子集，&lt;code&gt;set1 &amp;lt;= set2&lt;/code&gt;判断&lt;code&gt;set1&lt;/code&gt;是不是&lt;code&gt;set2&lt;/code&gt;的子集，&lt;code&gt;set2 &amp;gt; set1&lt;/code&gt;判断&lt;code&gt;set2&lt;/code&gt;是不是&lt;code&gt;set1&lt;/code&gt;的超集。当然，我们也可以通过&lt;code&gt;set1.issubset(set2)&lt;/code&gt;判断&lt;code&gt;set1&lt;/code&gt;是不是&lt;code&gt;set2&lt;/code&gt;的子集；通过&lt;code&gt;set2.issuperset(set1)&lt;/code&gt;判断&lt;code&gt;set2&lt;/code&gt;是不是&lt;code&gt;set1&lt;/code&gt;的超集。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;集合的方法&lt;/h4&gt;
&lt;p&gt;刚才我们说过，Python 中的集合是可变类型，我们可以通过集合的方法向集合添加元素或从集合中删除元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {1, 10, 100}

# 添加元素
set1.add(1000)
set1.add(10000)
print(set1)  # {1, 100, 1000, 10, 10000}

# 删除元素
set1.discard(10)
if 100 in set1:
    set1.remove(100)
print(set1)  # {1, 1000, 10000}

# 清空元素
set1.clear()
print(set1)  # set()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：删除元素的&lt;code&gt;remove&lt;/code&gt;方法在元素不存在时会引发&lt;code&gt;KeyError&lt;/code&gt;错误，所以上面的代码中我们先通过成员运算判断元素是否在集合中。集合类型还有一个&lt;code&gt;pop&lt;/code&gt;方法可以从集合中随机删除一个元素，该方法在删除元素的同时会返回（获得）被删除的元素，而&lt;code&gt;remove&lt;/code&gt;和&lt;code&gt;discard&lt;/code&gt;方法仅仅是删除元素，不会返回（获得）被删除的元素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;集合类型还有一个名为&lt;code&gt;isdisjoint&lt;/code&gt;的方法可以判断两个集合有没有相同的元素，如果没有相同元素，该方法返回&lt;code&gt;True&lt;/code&gt;，否则该方法返回&lt;code&gt;False&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set1 = {&apos;Java&apos;, &apos;Python&apos;, &apos;C++&apos;, &apos;Kotlin&apos;}
set2 = {&apos;Kotlin&apos;, &apos;Swift&apos;, &apos;Java&apos;, &apos;Dart&apos;}
set3 = {&apos;HTML&apos;, &apos;CSS&apos;, &apos;JavaScript&apos;}
print(set1.isdisjoint(set2))  # False
print(set1.isdisjoint(set3))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;不可变集合&lt;/h4&gt;
&lt;p&gt;Python 中还有一种不可变类型的集合，名字叫&lt;code&gt;frozenset&lt;/code&gt;。&lt;code&gt;set&lt;/code&gt;跟&lt;code&gt;frozenset&lt;/code&gt;的区别就如同&lt;code&gt;list&lt;/code&gt;跟&lt;code&gt;tuple&lt;/code&gt;的区别，&lt;code&gt;frozenset&lt;/code&gt;由于是不可变类型，能够计算出哈希码，因此它可以作为&lt;code&gt;set&lt;/code&gt;中的元素。除了不能添加和删除元素，&lt;code&gt;frozenset&lt;/code&gt;在其他方面跟&lt;code&gt;set&lt;/code&gt;是一样的，下面的代码简单的展示了&lt;code&gt;frozenset&lt;/code&gt;的用法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fset1 = frozenset({1, 3, 5, 7})
fset2 = frozenset(range(1, 6))
print(fset1)          # frozenset({1, 3, 5, 7})
print(fset2)          # frozenset({1, 2, 3, 4, 5})
print(fset1 &amp;amp; fset2)  # frozenset({1, 3, 5})
print(fset1 | fset2)  # frozenset({1, 2, 3, 4, 5, 7})
print(fset1 - fset2)  # frozenset({7})
print(fset1 &amp;lt; fset2)  # False
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 中的&lt;strong&gt;集合类型是一种无序容器&lt;/strong&gt;，&lt;strong&gt;不允许有重复运算&lt;/strong&gt;，由于底层使用了哈希存储，集合中的元素必须是&lt;code&gt;hashable&lt;/code&gt;类型。集合与列表最大的区别在于&lt;strong&gt;集合中的元素没有顺序&lt;/strong&gt;、所以&lt;strong&gt;不能够通过索引运算访问元素&lt;/strong&gt;、但是集合可以执行交集、并集、差集等二元运算，也可以通过关系运算符检查两个集合是否存在超集、子集等关系。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13 常用数据结构之字典&lt;/h2&gt;
&lt;p&gt;迄今为止，我们已经为大家介绍了 Python 中的三种容器型数据类型（列表、元组、集合），但是这些数据类型仍然不足以帮助我们解决所有的问题。例如，我们需要一个变量来保存一个人的多项信息，包括：姓名、年龄、身高、体重、家庭住址、本人手机号、紧急联系人手机号，此时你会发现，我们之前学过的列表、元组和集合类型都不够好使。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person1 = [&apos;王大锤&apos;, 55, 168, 60, &apos;成都市武侯区科华北路62号1栋101&apos;, &apos;13122334455&apos;, &apos;13800998877&apos;]
person2 = (&apos;王大锤&apos;, 55, 168, 60, &apos;成都市武侯区科华北路62号1栋101&apos;, &apos;13122334455&apos;, &apos;13800998877&apos;)
person3 = {&apos;王大锤&apos;, 55, 168, 60, &apos;成都市武侯区科华北路62号1栋101&apos;, &apos;13122334455&apos;, &apos;13800998877&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集合肯定是最不合适的，因为集合中不能有重复元素，如果一个人的年龄和体重刚好相同，那么集合中就会少一项信息；同理，如果这个人的手机号和紧急联系人手机号是相同的，那么集合中又会少一项信息。另一方面，虽然列表和元组可以把一个人的所有信息都保存下来，但是当你想要获取这个人的手机号或家庭住址时，你得先知道他的手机号是列表或元组中的第几个元素。总之，在遇到上述的场景时，列表、元组、集合都不是最合适的选择，此时我们需要字典（dictionary）类型，这种数据类型最适合把相关联的信息组装到一起，可以帮助我们解决 Python 程序中为真实事物建模的问题。&lt;/p&gt;
&lt;p&gt;说到字典这个词，大家一定不陌生，读小学的时候，每个人手头基本上都有一本《新华字典》，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/cd8ceaf2d0.webp&quot; style=&quot;zoom:80%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;Python 程序中的字典跟现实生活中的字典很像，它以键值对（键和值的组合）的方式把数据组织到一起，我们可以通过键找到与之对应的值并进行操作。就像《新华字典》中，每个字（键）都有与它对应的解释（值）一样，每个字和它的解释合在一起就是字典中的一个条目，而字典中通常包含了很多个这样的条目。&lt;/p&gt;
&lt;h4&gt;创建和使用字典&lt;/h4&gt;
&lt;p&gt;Python 中创建字典可以使用&lt;code&gt;{}&lt;/code&gt;字面量语法，这一点跟上一节课讲的集合是一样的。但是字典的&lt;code&gt;{}&lt;/code&gt;中的元素是以键值对的形式存在的，每个元素由&lt;code&gt;:&lt;/code&gt;分隔的两个值构成，&lt;code&gt;:&lt;/code&gt;前面是键，&lt;code&gt;:&lt;/code&gt;后面是值，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xinhua = {
    &apos;麓&apos;: &apos;山脚下&apos;,
    &apos;路&apos;: &apos;道，往来通行的地方；方面，地区：南～货，外～货；种类：他俩是一～人&apos;,
    &apos;蕗&apos;: &apos;甘草的别名&apos;,
    &apos;潞&apos;: &apos;潞水，水名，即今山西省的浊漳河；潞江，水名，即云南省的怒江&apos;
}
print(xinhua)
person = {
    &apos;name&apos;: &apos;王大锤&apos;,
    &apos;age&apos;: 55,
    &apos;height&apos;: 168,
    &apos;weight&apos;: 60,
    &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;, 
    &apos;tel&apos;: &apos;13122334455&apos;,
    &apos;emergence contact&apos;: &apos;13800998877&apos;
}
print(person)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码，相信大家已经看出来了，用字典来保存一个人的信息远远优于使用列表或元组，因为我们可以用&lt;code&gt;:&lt;/code&gt;前面的键来表示条目的含义，而&lt;code&gt;:&lt;/code&gt;后面就是这个条目所对应的值。&lt;/p&gt;
&lt;p&gt;当然，如果愿意，我们也可以使用内置函数&lt;code&gt;dict&lt;/code&gt;或者是字典的生成式语法来创建字典，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dict函数(构造器)中的每一组参数就是字典中的一组键值对
person = dict(name=&apos;王大锤&apos;, age=55, height=168, weight=60, addr=&apos;成都市武侯区科华北路62号1栋101&apos;)
print(person)  # {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 55, &apos;height&apos;: 168, &apos;weight&apos;: 60, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}

# 可以通过Python内置函数zip压缩两个序列并创建字典
items1 = dict(zip(&apos;ABCDE&apos;, &apos;12345&apos;))
print(items1)  # {&apos;A&apos;: &apos;1&apos;, &apos;B&apos;: &apos;2&apos;, &apos;C&apos;: &apos;3&apos;, &apos;D&apos;: &apos;4&apos;, &apos;E&apos;: &apos;5&apos;}
items2 = dict(zip(&apos;ABCDE&apos;, range(1, 10)))
print(items2)  # {&apos;A&apos;: 1, &apos;B&apos;: 2, &apos;C&apos;: 3, &apos;D&apos;: 4, &apos;E&apos;: 5}

# 用字典生成式语法创建字典
items3 = {x: x ** 3 for x in range(1, 6)}
print(items3)  # {1: 1, 2: 8, 3: 27, 4: 64, 5: 125}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想知道字典中一共有多少组键值对，仍然是使用&lt;code&gt;len&lt;/code&gt;函数；如果想对字典进行遍历，可以用&lt;code&gt;for&lt;/code&gt;循环，但是需要注意，&lt;code&gt;for&lt;/code&gt;循环只是对字典的键进行了遍历，不过没关系，在学习了字典的索引运算后，我们可以通过字典的键访问它对应的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {
    &apos;name&apos;: &apos;王大锤&apos;,
    &apos;age&apos;: 55,
    &apos;height&apos;: 168,
    &apos;weight&apos;: 60,
    &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;
}
print(len(person))  # 5
for key in person:
    print(key)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字典的运算&lt;/h4&gt;
&lt;p&gt;对于字典类型来说，成员运算和索引运算肯定是很重要的，前者可以判定指定的键在不在字典中，后者可以通过键访问对应的值或者向字典中添加新的键值对。值得注意的是，字典的索引不同于列表的索引，列表中的元素因为有属于自己有序号，所以列表的索引是一个整数；字典中因为保存的是键值对，所以字典需要用键去索引对应的值。需要&lt;strong&gt;特别提醒&lt;/strong&gt;大家注意的是，&lt;strong&gt;字典中的键必须是不可变类型&lt;/strong&gt;，例如整数（&lt;code&gt;int&lt;/code&gt;）、浮点数（&lt;code&gt;float&lt;/code&gt;）、字符串（&lt;code&gt;str&lt;/code&gt;）、元组（&lt;code&gt;tuple&lt;/code&gt;）等类型，这一点跟集合类型对元素的要求是一样的；很显然，之前我们讲的列表（&lt;code&gt;list&lt;/code&gt;）和集合（&lt;code&gt;set&lt;/code&gt;）不能作为字典中的键，字典类型本身也不能再作为字典中的键，因为字典也是可变类型，但是列表、集合、字典都可以作为字典中的值，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {
    &apos;name&apos;: &apos;王大锤&apos;,
    &apos;age&apos;: 55,
    &apos;height&apos;: 168,
    &apos;weight&apos;: 60,
    &apos;addr&apos;: [&apos;成都市武侯区科华北路62号1栋101&apos;, &apos;北京市西城区百万庄大街1号&apos;],
    &apos;car&apos;: {
        &apos;brand&apos;: &apos;BMW X7&apos;,
        &apos;maxSpeed&apos;: &apos;250&apos;,
        &apos;length&apos;: 5170,
        &apos;width&apos;: 2000,
        &apos;height&apos;: 1835,
        &apos;displacement&apos;: 3.0
    }
}
print(person)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可以看看下面的代码，了解一下字典的成员运算和索引运算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 55, &apos;height&apos;: 168, &apos;weight&apos;: 60, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}

# 成员运算
print(&apos;name&apos; in person)  # True
print(&apos;tel&apos; in person)   # False

# 索引运算
print(person[&apos;name&apos;])
print(person[&apos;addr&apos;])
person[&apos;age&apos;] = 25
person[&apos;height&apos;] = 178
person[&apos;tel&apos;] = &apos;13122334455&apos;
person[&apos;signature&apos;] = &apos;你的男朋友是一个盖世垃圾，他会踏着五彩祥云去迎娶你的闺蜜&apos;
print(person)

# 循环遍历
for key in person:
    print(f&apos;{key}:\t{person[key]}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，在通过索引运算获取字典中的值时，如指定的键没有在字典中，将会引发&lt;code&gt;KeyError&lt;/code&gt;异常。&lt;/p&gt;
&lt;h4&gt;字典的方法&lt;/h4&gt;
&lt;p&gt;字典类型的方法基本上都跟字典的键值对操作相关，其中&lt;code&gt;get&lt;/code&gt;方法可以通过键来获取对应的值。跟索引运算不同的是，&lt;code&gt;get&lt;/code&gt;方法在字典中没有指定的键时不会产生异常，而是返回&lt;code&gt;None&lt;/code&gt;或指定的默认值，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
print(person.get(&apos;name&apos;))       # 王大锤
print(person.get(&apos;sex&apos;))        # None
print(person.get(&apos;sex&apos;, True))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要获取字典中所有的键，可以使用&lt;code&gt;keys&lt;/code&gt;方法；如果需要获取字典中所有的值，可以使用&lt;code&gt;values&lt;/code&gt;方法。字典还有一个名为&lt;code&gt;items&lt;/code&gt;的方法，它会将键和值组装成二元组，通过该方法来遍历字典中的元素也是非常方便的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178}
print(person.keys())    # dict_keys([&apos;name&apos;, &apos;age&apos;, &apos;height&apos;])
print(person.values())  # dict_values([&apos;王大锤&apos;, 25, 178])
print(person.items())   # dict_items([(&apos;name&apos;, &apos;王大锤&apos;), (&apos;age&apos;, 25), (&apos;height&apos;, 178)])
for key, value in person.items():
    print(f&apos;{key}:\t{value}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字典的&lt;code&gt;update&lt;/code&gt;方法实现两个字典的合并操作。例如，有两个字典&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;，当执行&lt;code&gt;x.update(y)&lt;/code&gt;操作时，&lt;code&gt;x&lt;/code&gt;跟&lt;code&gt;y&lt;/code&gt;相同的键对应的值会被&lt;code&gt;y&lt;/code&gt;中的值更新，而&lt;code&gt;y&lt;/code&gt;中有但&lt;code&gt;x&lt;/code&gt;中没有的键值对会直接添加到&lt;code&gt;x&lt;/code&gt;中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person1 = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 55, &apos;height&apos;: 178}
person2 = {&apos;age&apos;: 25, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
person1.update(person2)
print(person1)  # {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 Python 3.9 及以上的版本，也可以使用&lt;code&gt;|&lt;/code&gt;运算符来完成同样的操作，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person1 = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 55, &apos;height&apos;: 178}
person2 = {&apos;age&apos;: 25, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
person1 |= person2
print(person1)  # {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过&lt;code&gt;pop&lt;/code&gt;或&lt;code&gt;popitem&lt;/code&gt;方法从字典中删除元素，前者会返回（获得）键对应的值，但是如果字典中不存在指定的键，会引发&lt;code&gt;KeyError&lt;/code&gt;错误；后者在删除元素时，会返回（获得）键和值组成的二元组。字典的&lt;code&gt;clear&lt;/code&gt;方法会清空字典中所有的键值对，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
print(person.pop(&apos;age&apos;))  # 25
print(person)             # {&apos;name&apos;: &apos;王大锤&apos;, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
print(person.popitem())   # (&apos;addr&apos;, &apos;成都市武侯区科华北路62号1栋101&apos;)
print(person)             # {&apos;name&apos;: &apos;王大锤&apos;, &apos;height&apos;: 178}
person.clear()
print(person)             # {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟列表一样，从字典中删除元素也可以使用&lt;code&gt;del&lt;/code&gt;关键字，在删除元素的时候如果指定的键索引不到对应的值，一样会引发&lt;code&gt;KeyError&lt;/code&gt;错误，具体的做法如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person = {&apos;name&apos;: &apos;王大锤&apos;, &apos;age&apos;: 25, &apos;height&apos;: 178, &apos;addr&apos;: &apos;成都市武侯区科华北路62号1栋101&apos;}
del person[&apos;age&apos;]
del person[&apos;addr&apos;]
print(person)  # {&apos;name&apos;: &apos;王大锤&apos;, &apos;height&apos;: 178}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字典的应用&lt;/h4&gt;
&lt;p&gt;我们通过几个简单的例子来看看如何使用字典类型解决一些实际的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例子1&lt;/strong&gt;：输入一段话，统计每个英文字母出现的次数，按出现次数从高到低输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentence = input(&apos;请输入一段话: &apos;)
counter = {}
for ch in sentence:
    if &apos;A&apos; &amp;lt;= ch &amp;lt;= &apos;Z&apos; or &apos;a&apos; &amp;lt;= ch &amp;lt;= &apos;z&apos;:
        counter[ch] = counter.get(ch, 0) + 1
sorted_keys = sorted(counter, key=counter.get, reverse=True)
for key in sorted_keys:
    print(f&apos;{key} 出现了 {counter[key]} 次.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;e 出现了 27 次.
n 出现了 21 次.
a 出现了 18 次.
i 出现了 18 次.
s 出现了 16 次.
t 出现了 16 次.
o 出现了 14 次.
h 出现了 13 次.
r 出现了 10 次.
d 出现了 9 次.
l 出现了 9 次.
g 出现了 6 次.
u 出现了 6 次.
f 出现了 6 次.
c 出现了 6 次.
y 出现了 5 次.
b 出现了 5 次.
m 出现了 4 次.
p 出现了 3 次.
w 出现了 2 次.
v 出现了 2 次.
M 出现了 1 次.
k 出现了 1 次.
x 出现了 1 次.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;例子2&lt;/strong&gt;：在一个字典中保存了股票的代码和价格，找出股价大于100元的股票并创建一个新的字典。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：可以用字典的生成式语法来创建这个新字典。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;stocks = {
    &apos;AAPL&apos;: 191.88,
    &apos;GOOG&apos;: 1186.96,
    &apos;IBM&apos;: 149.24,
    &apos;ORCL&apos;: 48.44,
    &apos;ACN&apos;: 166.89,
    &apos;FB&apos;: 208.09,
    &apos;SYMC&apos;: 21.29
}
stocks2 = {key: value for key, value in stocks.items() if value &amp;gt; 100}
print(stocks2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&apos;AAPL&apos;: 191.88, &apos;GOOG&apos;: 1186.96, &apos;IBM&apos;: 149.24, &apos;ACN&apos;: 166.89, &apos;FB&apos;: 208.09}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 程序中的字典跟现实生活中字典非常像，允许我们&lt;strong&gt;以键值对的形式保存数据&lt;/strong&gt;，再&lt;strong&gt;通过键访问对应的值&lt;/strong&gt;。字典是一种非常&lt;strong&gt;有利于数据检索&lt;/strong&gt;的数据类型，但是需要再次提醒大家，&lt;strong&gt;字典中的键必须是不可变类型&lt;/strong&gt;，列表、集合、字典等类型的数据都不能作为字典的键。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;14 函数和模块&lt;/h2&gt;
&lt;p&gt;在讲解本节课的内容之前，我们先来研究一道数学题，请说出下面的方程有多少组正整数解。&lt;/p&gt;
&lt;p&gt;$$
x_{1} + x_{2} + x_{3} + x_{4} = 8
$$&lt;/p&gt;
&lt;p&gt;你可能已经想到了，这个问题其实等同于将 8 个苹果分成四组且每组至少一个苹果有多少种方案，也等价于在分隔 8 个苹果的 7 个间隙之间放入三个隔断将苹果分成四组有多少种方案，所以答案是 $\small{C_{7}^{3} = 35}$ ，其中 $\small{C_{7}^{3}}$ 代表 7 选 3 的组合数，其计算公式如下所示。&lt;/p&gt;
&lt;p&gt;$$
C_m^n = \frac {m!} {n!(m-n)!}
$$&lt;/p&gt;
&lt;p&gt;根据之前学习的知识，我们可以用循环做累乘的方式分别计算出 $\small{m!}$ 、 $\small{n!}$ 和 $\small{(m-n)!}$ ，然后再通过除法运算得到组合数 $\small{C_{m}^{n}}$ ，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入m和n，计算组合数C(m,n)的值

Version: 1.0
Author: 骆昊
&quot;&quot;&quot;

m = int(input(&apos;m = &apos;))
n = int(input(&apos;n = &apos;))
# 计算m的阶乘
fm = 1
for num in range(1, m + 1):
    fm *= num
# 计算n的阶乘
fn = 1
for num in range(1, n + 1):
    fn *= num
# 计算m-n的阶乘
fk = 1
for num in range(1, m - n + 1):
    fk *= num
# 计算C(M,N)的值
print(fm // fn // fk)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m = 7
n = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;35
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知大家是否注意到，上面的代码中我们做了三次求阶乘的操作，虽然 $\small{m}$ 、 $\small{n}$ 、 $\small{m - n}$ 的值各不相同，但是三段代码并没有实质性的区别，属于重复代码。世界级的编程大师&lt;em&gt;Martin Fowler&lt;/em&gt;曾经说过：“&lt;strong&gt;代码有很多种坏味道，重复是最坏的一种！&lt;/strong&gt;”。要写出高质量的代码，首先就要解决重复代码的问题。对于上面的代码来说，我们可以将求阶乘的功能封装到一个称为“函数”的代码块中，在需要计算阶乘的地方，我们只需“调用函数”即可实现对求阶乘功能的复用。&lt;/p&gt;
&lt;h4&gt;定义函数&lt;/h4&gt;
&lt;p&gt;数学上的函数通常形如 $\small{y = f(x)}$ 或者 $\small{z = g(x, y)}$ 这样的形式，在 $\small{y = f(x)}$ 中， $\small{f}$ 是函数的名字， $\small{x}$ 是函数的自变量， $\small{y}$ 是函数的因变量；而在 $\small{z = g(x, y)}$ 中， $\small{g}$ 是函数名， $\small{x}$ 和 $\small{y}$ 是函数的自变量， $\small{z}$ 是函数的因变量。Python 中的函数跟这个结构是一致的，每个函数都有自己的名字、自变量和因变量。我们通常把 Python 函数的自变量称为函数的参数，而因变量称为函数的返回值。&lt;/p&gt;
&lt;p&gt;Python 中可以使用&lt;code&gt;def&lt;/code&gt;关键字来定义函数，和变量一样每个函数也应该有一个漂亮的名字，命名规则跟变量的命名规则是一样的（大家赶紧想想我们之前讲过的变量的命名规则）。在函数名后面的圆括号中可以设置函数的参数，也就是我们刚才说的函数的自变量，而函数执行完成后，我们会通过&lt;code&gt;return&lt;/code&gt;关键字来返回函数的执行结果，这就是我们刚才说的函数的因变量。如果函数中没有&lt;code&gt;return&lt;/code&gt;语句，那么函数会返回代表空值的&lt;code&gt;None&lt;/code&gt;。另外，函数也可以没有自变量（参数），但是函数名后面的圆括号是必须有的。一个函数要做的事情（要执行的代码），是通过代码缩进的方式放到函数定义行之后，跟之前分支和循环结构的代码块类似，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/b2c82fe802.webp&quot; style=&quot;zoom:45%;&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;下面，我们将之前代码中求阶乘的操作放到一个函数中，通过这种方式来重构上面的代码。&lt;strong&gt;所谓重构，是在不影响代码执行结果的前提下对代码的结构进行调整&lt;/strong&gt;，重构之后的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入m和n，计算组合数C(m,n)的值

Version: 1.1
Author: 骆昊
&quot;&quot;&quot;


# 通过关键字def定义求阶乘的函数
# 自变量（参数）num是一个非负整数
# 因变量（返回值）是num的阶乘
def fac(num):
    result = 1
    for n in range(2, num + 1):
        result *= n
    return result


m = int(input(&apos;m = &apos;))
n = int(input(&apos;n = &apos;))
# 计算阶乘的时候不需要写重复的代码而是直接调用函数
# 调用函数的语法是在函数名后面跟上圆括号并传入参数
print(fac(m) // fac(n) // fac(m - n))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大家可以感受下，上面的代码是不是比之前的版本更加简单优雅。更为重要的是，我们定义的求阶乘函数&lt;code&gt;fac&lt;/code&gt;还可以在其他需要求阶乘的代码中重复使用。所以，&lt;strong&gt;使用函数可以帮助我们将功能上相对独立且会被重复使用的代码封装起来&lt;/strong&gt;，当我们需要这些的代码，不是把重复的代码再编写一遍，而是&lt;strong&gt;通过调用函数实现对既有代码的复用&lt;/strong&gt;。事实上，Python 标准库的&lt;code&gt;math&lt;/code&gt;模块中，已经有一个名为&lt;code&gt;factorial&lt;/code&gt;的函数实现了求阶乘的功能，我们可以直接用&lt;code&gt;import math&lt;/code&gt;导入&lt;code&gt;math&lt;/code&gt;模块，然后使用&lt;code&gt;math.factorial&lt;/code&gt;来调用求阶乘的函数；我们也可以通过&lt;code&gt;from math import factorial&lt;/code&gt;直接导入&lt;code&gt;factorial&lt;/code&gt;函数来使用它，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入m和n，计算组合数C(m,n)的值

Version: 1.2
Author: 骆昊
&quot;&quot;&quot;
from math import factorial

m = int(input(&apos;m = &apos;))
n = int(input(&apos;n = &apos;))
print(factorial(m) // factorial(n) // factorial(m - n))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将来我们使用的函数，要么是自定义的函数，要么是 Python 标准库或者三方库中提供的函数，如果已经有现成的可用的函数，我们就没有必要自己去定义，“&lt;strong&gt;重复发明轮子&lt;/strong&gt;”是一件非常糟糕的事情。对于上面的代码，如果你觉得&lt;code&gt;factorial&lt;/code&gt;这个名字太长，书写代码的时候不是特别方便，我们在导入函数的时候还可以通过&lt;code&gt;as&lt;/code&gt;关键字为其别名。在调用函数的时候，我们可以用函数的别名，而不再使用它之前的名字，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
输入m和n，计算组合数C(m,n)的值

Version: 1.3
Author: 骆昊
&quot;&quot;&quot;
from math import factorial as f

m = int(input(&apos;m = &apos;))
n = int(input(&apos;n = &apos;))
print(f(m) // f(n) // f(m - n))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;函数的参数&lt;/h4&gt;
&lt;h5&gt;位置参数和关键字参数&lt;/h5&gt;
&lt;p&gt;我们再来写一个函数，根据给出的三条边的长度判断是否可以构成三角形，如果可以构成三角形则返回&lt;code&gt;True&lt;/code&gt;，否则返回&lt;code&gt;False&lt;/code&gt;，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def make_judgement(a, b, c):
    &quot;&quot;&quot;判断三条边的长度能否构成三角形&quot;&quot;&quot;
    return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面&lt;code&gt;make_judgement&lt;/code&gt;函数有三个参数，这种参数叫做位置参数，在调用函数时通常按照从左到右的顺序依次传入，而且传入参数的数量必须和定义函数时参数的数量相同，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(make_judgement(1, 2, 3))  # False
print(make_judgement(4, 5, 6))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不想按照从左到右的顺序依次给出&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt; 三个参数的值，也可以使用关键字参数，通过“参数名=参数值”的形式为函数传入参数，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(make_judgement(b=2, c=3, a=1))  # False
print(make_judgement(c=6, b=4, a=5))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义函数时，我们可以在参数列表中用&lt;code&gt;/&lt;/code&gt;设置&lt;strong&gt;强制位置参数&lt;/strong&gt;（&lt;em&gt;positional-only arguments&lt;/em&gt;），用&lt;code&gt;*&lt;/code&gt;设置&lt;strong&gt;命名关键字参数&lt;/strong&gt;。所谓强制位置参数，就是调用函数时只能按照参数位置来接收参数值的参数；而命名关键字参数只能通过“参数名=参数值”的方式来传递和接收参数，大家可以看看下面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /前面的参数是强制位置参数
def make_judgement(a, b, c, /):
    &quot;&quot;&quot;判断三条边的长度能否构成三角形&quot;&quot;&quot;
    return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b


# 下面的代码会产生TypeError错误，错误信息提示“强制位置参数是不允许给出参数名的”
# TypeError: make_judgement() got some positional-only arguments passed as keyword arguments
# print(make_judgement(b=2, c=3, a=1))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：强制位置参数是 Python 3.8 引入的新特性，在使用低版本的 Python 解释器时需要注意。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# *后面的参数是命名关键字参数
def make_judgement(*, a, b, c):
    &quot;&quot;&quot;判断三条边的长度能否构成三角形&quot;&quot;&quot;
    return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b


# 下面的代码会产生TypeError错误，错误信息提示“函数没有位置参数但却给了3个位置参数”
# TypeError: make_judgement() takes 0 positional arguments but 3 were given
# print(make_judgement(1, 2, 3))
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;参数的默认值&lt;/h5&gt;
&lt;p&gt;Python 中允许函数的参数拥有默认值，我们可以把之前讲过的一个例子“CRAPS赌博游戏”（《第07课：分支和循环结构的应用》）中摇色子获得点数的功能封装到函数中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from random import randrange


# 定义摇色子的函数
# 函数的自变量（参数）n表示色子的个数，默认值为2
# 函数的因变量（返回值）表示摇n颗色子得到的点数
def roll_dice(n=2):
    total = 0
    for _ in range(n):
        total += randrange(1, 7)
    return total


# 如果没有指定参数，那么n使用默认值2，表示摇两颗色子
print(roll_dice())
# 传入参数3，变量n被赋值为3，表示摇三颗色子获得点数
print(roll_dice(3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再来看一个更为简单的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add(a=0, b=0, c=0):
    &quot;&quot;&quot;三个数相加求和&quot;&quot;&quot;
    return a + b + c


# 调用add函数，没有传入参数，那么a、b、c都使用默认值0
print(add())         # 0
# 调用add函数，传入一个参数，该参数赋值给变量a, 变量b和c使用默认值0
print(add(1))        # 1
# 调用add函数，传入两个参数，分别赋值给变量a和b，变量c使用默认值0
print(add(1, 2))     # 3
# 调用add函数，传入三个参数，分别赋值给a、b、c三个变量
print(add(1, 2, 3))  # 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，&lt;strong&gt;带默认值的参数必须放在不带默认值的参数之后&lt;/strong&gt;，否则将产生&lt;code&gt;SyntaxError&lt;/code&gt;错误，错误消息是：&lt;code&gt;non-default argument follows default argument&lt;/code&gt;，翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。&lt;/p&gt;
&lt;h5&gt;可变参数&lt;/h5&gt;
&lt;p&gt;Python 语言中可以通过星号表达式语法让函数支持可变参数。所谓可变参数指的是在调用函数时，可以向函数传入&lt;code&gt;0&lt;/code&gt;个或任意多个参数。将来我们以团队协作的方式开发商业项目时，很有可能要设计函数给其他人使用，但有的时候我们并不知道函数的调用者会向该函数传入多少个参数，这个时候可变参数就能派上用场。&lt;/p&gt;
&lt;p&gt;下面的代码演示了如何使用可变位置参数实现对任意多个数求和的&lt;code&gt;add&lt;/code&gt;函数，调用函数时传入的参数会保存到一个元组，通过对该元组的遍历，可以获取传入函数的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 用星号表达式来表示args可以接收0个或任意多个参数
# 调用函数时传入的n个参数会组装成一个n元组赋给args
# 如果一个参数都没有传入，那么args会是一个空元组
def add(*args):
    total = 0
    # 对保存可变参数的元组进行循环遍历
    for val in args:
        # 对参数进行了类型检查（数值型的才能求和）
        if type(val) in (int, float):
            total += val
    return total


# 在调用add函数时可以传入0个或任意多个参数
print(add())         # 0
print(add(1))        # 1
print(add(1, 2, 3))  # 6
print(add(1, 2, &apos;hello&apos;, 3.45, 6))  # 12.45
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望通过“参数名=参数值”的形式传入若干个参数，具体有多少个参数也是不确定的，我们还可以给函数添加可变关键字参数，把传入的关键字参数组装到一个字典中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 参数列表中的**kwargs可以接收0个或任意多个关键字参数
# 调用函数时传入的关键字参数会组装成一个字典（参数名是字典中的键，参数值是字典中的值）
# 如果一个关键字参数都没有传入，那么kwargs会是一个空字典
def foo(*args, **kwargs):
    print(args)
    print(kwargs)


foo(3, 2.1, True, name=&apos;骆昊&apos;, age=43, gpa=4.95)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(3, 2.1, True)
{&apos;name&apos;: &apos;骆昊&apos;, &apos;age&apos;: 43, &apos;gpa&apos;: 4.95}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;用模块管理函数&lt;/h4&gt;
&lt;p&gt;不管用什么样的编程语言来写代码，给变量、函数起名字都是一个让人头疼的问题，因为我们会遇到&lt;strong&gt;命名冲突&lt;/strong&gt;这种尴尬的情况。最简单的场景就是在同一个&lt;code&gt;.py&lt;/code&gt;文件中定义了两个同名的函数，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def foo():
    print(&apos;hello, world!&apos;)


def foo():
    print(&apos;goodbye, world!&apos;)

    
foo()  # 大家猜猜调用foo函数会输出什么
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然上面的这种情况我们很容易就能避免，但是如果项目是团队协作多人开发的时候，团队中可能有多个程序员都定义了名为&lt;code&gt;foo&lt;/code&gt;的函数，这种情况下怎么解决命名冲突呢？答案其实很简单，Python 中每个文件就代表了一个模块（module），我们在不同的模块中可以有同名的函数，在使用函数的时候，我们通过&lt;code&gt;import&lt;/code&gt;关键字导入指定的模块再使用&lt;strong&gt;完全限定名&lt;/strong&gt;（&lt;code&gt;模块名.函数名&lt;/code&gt;）的调用方式，就可以区分到底要使用的是哪个模块中的&lt;code&gt;foo&lt;/code&gt;函数，代码如下所示。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;module1.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def foo():
    print(&apos;hello, world!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;module2.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def foo():
    print(&apos;goodbye, world!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;test.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import module1
import module2

# 用“模块名.函数名”的方式（完全限定名）调用函数，
module1.foo()  # hello, world!
module2.foo()  # goodbye, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在导入模块时，还可以使用&lt;code&gt;as&lt;/code&gt;关键字对模块进行别名，这样我们可以使用更为简短的完全限定名。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import module1 as m1
import module2 as m2

m1.foo()  # hello, world!
m2.foo()  # goodbye, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两段代码，我们导入的是定义函数的模块，我们也可以使用&lt;code&gt;from...import...&lt;/code&gt;语法从模块中直接导入需要使用的函数，代码如下所示。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from module1 import foo

foo()  # hello, world!

from module2 import foo

foo()  # goodbye, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，如果我们如果从两个不同的模块中导入了同名的函数，后面导入的函数会替换掉之前的导入，就像下面的代码，调用&lt;code&gt;foo&lt;/code&gt;会输出&lt;code&gt;goodbye, world!&lt;/code&gt;，因为我们先导入了&lt;code&gt;module1&lt;/code&gt;的&lt;code&gt;foo&lt;/code&gt;，后导入了&lt;code&gt;module2&lt;/code&gt;的&lt;code&gt;foo&lt;/code&gt; 。如果两个&lt;code&gt;from...import...&lt;/code&gt;反过来写，那就是另外一番光景了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from module1 import foo
from module2 import foo

foo()  # goodbye, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想在上面的代码中同时使用来自两个模块的&lt;code&gt;foo&lt;/code&gt;函数还是有办法的，大家可能已经猜到了，还是用&lt;code&gt;as&lt;/code&gt;关键字对导入的函数进行别名，代码如下所示。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test.py&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from module1 import foo as f1
from module2 import foo as f2

f1()  # hello, world!
f2()  # goodbye, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;标准库中的模块和函数&lt;/h4&gt;
&lt;p&gt;Python 标准库中提供了大量的模块和函数来简化我们的开发工作，我们之前用过的&lt;code&gt;random&lt;/code&gt;模块就为我们提供了生成随机数和进行随机抽样的函数；而&lt;code&gt;time&lt;/code&gt;模块则提供了和时间操作相关的函数；我们之前用到过的&lt;code&gt;math&lt;/code&gt;模块中还包括了计算正弦、余弦、指数、对数等一系列的数学函数。随着我们深入学习 Python 语言，我们还会用到更多的模块和函数。&lt;/p&gt;
&lt;p&gt;Python 标准库中还有一类函数是不需要&lt;code&gt;import&lt;/code&gt;就能够直接使用的，我们将其称之为&lt;strong&gt;内置函数&lt;/strong&gt;，这些内置函数不仅有用而且还很常用，下面的表格列出了一部分的内置函数。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回一个数的绝对值，例如：&lt;code&gt;abs(-1.3)&lt;/code&gt;会返回&lt;code&gt;1.3&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把一个整数转换成以&lt;code&gt;&apos;0b&apos;&lt;/code&gt;开头的二进制字符串，例如：&lt;code&gt;bin(123)&lt;/code&gt;会返回&lt;code&gt;&apos;0b1111011&apos;&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将Unicode编码转换成对应的字符，例如：&lt;code&gt;chr(8364)&lt;/code&gt;会返回&lt;code&gt;&apos;€&apos;&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将一个整数转换成以&lt;code&gt;&apos;0x&apos;&lt;/code&gt;开头的十六进制字符串，例如：&lt;code&gt;hex(123)&lt;/code&gt;会返回&lt;code&gt;&apos;0x7b&apos;&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从输入中读取一行，返回读到的字符串。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;len&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取字符串、列表等的长度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回多个参数或一个可迭代对象中的最大值，例如：&lt;code&gt;max(12, 95, 37)&lt;/code&gt;会返回&lt;code&gt;95&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回多个参数或一个可迭代对象中的最小值，例如：&lt;code&gt;min(12, 95, 37)&lt;/code&gt;会返回&lt;code&gt;12&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把一个整数转换成以&lt;code&gt;&apos;0o&apos;&lt;/code&gt;开头的八进制字符串，例如：&lt;code&gt;oct(123)&lt;/code&gt;会返回&lt;code&gt;&apos;0o173&apos;&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;打开一个文件并返回文件对象。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ord&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将字符转换成对应的Unicode编码，例如：&lt;code&gt;ord(&apos;€&apos;)&lt;/code&gt;会返回&lt;code&gt;8364&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求幂运算，例如：&lt;code&gt;pow(2, 3)&lt;/code&gt;会返回&lt;code&gt;8&lt;/code&gt;；&lt;code&gt;pow(2, 0.5)&lt;/code&gt;会返回&lt;code&gt;1.4142135623730951&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;print&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;打印输出。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;构造一个范围序列，例如：&lt;code&gt;range(100)&lt;/code&gt;会产生&lt;code&gt;0&lt;/code&gt;到&lt;code&gt;99&lt;/code&gt;的整数序列。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按照指定的精度对数值进行四舍五入，例如：&lt;code&gt;round(1.23456, 4)&lt;/code&gt;会返回&lt;code&gt;1.2346&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对一个序列中的项从左到右进行求和运算，例如：&lt;code&gt;sum(range(1, 101))&lt;/code&gt;会返回&lt;code&gt;5050&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回对象的类型，例如：&lt;code&gt;type(10)&lt;/code&gt;会返回&lt;code&gt;int&lt;/code&gt;；而&lt;code&gt; type(&apos;hello&apos;)&lt;/code&gt;会返回&lt;code&gt;str&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;函数是对功能相对独立且会重复使用的代码的封装&lt;/strong&gt;。学会使用定义和使用函数，就能够写出更为优质的代码。当然，Python 语言的标准库中已经为我们提供了大量的模块和常用的函数，用好这些模块和函数就能够用更少的代码做更多的事情；如果这些模块和函数不能满足我们的要求，可能就需要自定义函数，然后再通过模块的概念来管理这些自定义函数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;15 函数应用实战&lt;/h2&gt;
&lt;h4&gt;例子1：随机验证码&lt;/h4&gt;
&lt;p&gt;设计一个生成随机验证码的函数，验证码由数字和英文大小写字母构成，长度可以通过参数设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import string

ALL_CHARS = string.digits + string.ascii_letters


def generate_code(*, code_len=4):
    &quot;&quot;&quot;
    生成指定长度的验证码
    :param code_len: 验证码的长度(默认4个字符)
    :return: 由大小写英文字母和数字构成的随机验证码字符串
    &quot;&quot;&quot;
    return &apos;&apos;.join(random.choices(ALL_CHARS, k=code_len))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明1&lt;/strong&gt;：&lt;code&gt;string&lt;/code&gt;模块的&lt;code&gt;digits&lt;/code&gt;代表0到9的数字构成的字符串&lt;code&gt;&apos;0123456789&apos;&lt;/code&gt;，&lt;code&gt;string&lt;/code&gt;模块的&lt;code&gt;ascii_letters&lt;/code&gt;代表大小写英文字母构成的字符串&lt;code&gt;&apos;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明2&lt;/strong&gt;：&lt;code&gt;random&lt;/code&gt;模块的&lt;code&gt;sample&lt;/code&gt;和&lt;code&gt;choices&lt;/code&gt;函数都可以实现随机抽样，&lt;code&gt;sample&lt;/code&gt;实现无放回抽样，这意味着抽样取出的元素是不重复的；&lt;code&gt;choices&lt;/code&gt;实现有放回抽样，这意味着可能会重复选中某些元素。这两个函数的第一个参数代表抽样的总体，而参数&lt;code&gt;k&lt;/code&gt;代表样本容量，需要说明的是&lt;code&gt;choices&lt;/code&gt;函数的参数&lt;code&gt;k&lt;/code&gt;是一个命名关键字参数，在传参时必须指定参数名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以用下面的代码生成5组随机验证码来测试上面的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for _ in range(5):
    print(generate_code()) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;59tZ
QKU5
izq8
IBBb
jIfX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for _ in range(5):
    print(generate_code(code_len=6))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FxJucw
HS4H9G
0yyXfz
x7fohf
ReO22w
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：我们设计的&lt;code&gt;generate_code&lt;/code&gt;函数的参数是命名关键字参数，由于它有默认值，可以不给它传值，使用默认值4。如果需要给函数传入参数，必须指定参数名&lt;code&gt;code_len&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子2：判断素数&lt;/h4&gt;
&lt;p&gt;设计一个判断给定的大于1的正整数是不是质数的函数。质数是只能被1和自身整除的正整数（大于1），如果一个大于 1 的正整数 $\small{N}$ 是质数，那就意味着在 2 到 $\small{N-1}$ 之间都没有它的因子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_prime(num: int) -&amp;gt; bool:
    &quot;&quot;&quot;
    判断一个正整数是不是质数
    :param num: 大于1的正整数
    :return: 如果num是质数返回True，否则返回False
    &quot;&quot;&quot;
    for i in range(2, int(num ** 0.5) + 1):
        if num % i == 0:
            return False
    return True
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明1&lt;/strong&gt;：上面&lt;code&gt;is_prime&lt;/code&gt;函数的参数&lt;code&gt;num&lt;/code&gt;后面的&lt;code&gt;: int&lt;/code&gt;用来标注参数的类型，虽然它对代码的执行结果不产生任何影响，但是很好的增强了代码的可读性。同理，参数列表后面的&lt;code&gt;-&amp;gt; bool&lt;/code&gt;用来标注函数返回值的类型，它也不会对代码的执行结果产生影响，但是却让我们清楚的知道，调用函数会得到一个布尔值，要么是&lt;code&gt;True&lt;/code&gt;，要么是&lt;code&gt;False&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明2&lt;/strong&gt;：上面的循环并不需要从 2 循环到 $\small{N-1}$ ，因为如果循环进行到 $\small{\sqrt{N}}$ 时，还没有找到$\small{N}$的因子，那么 $\small{\sqrt{N}}$ 之后也不会出现 $\small{N}$ 的因子，大家可以自己想一想这是为什么。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子3：最大公约数和最小公倍数&lt;/h4&gt;
&lt;p&gt;设计计算两个正整数最大公约数和最小公倍数的函数。 $\small{x}$ 和 $\small{y}$ 的最大公约数是能够同时整除 $\small{x}$ 和 $\small{y}$ 的最大整数，如果 $\small{x}$ 和 $\small{y}$ 互质，那么它们的最大公约数为 1； $\small{x}$ 和 $\small{y}$ 的最小公倍数是能够同时被 $\small{x}$ 和 $\small{y}$ 整除的最小正整数，如果 $\small{x}$ 和 $\small{y}$ 互质，那么它们的最小公倍数为 $\small{x \times y}$ 。需要提醒大家注意的是，计算最大公约数和最小公倍数是两个不同的功能，应该设计成两个函数，而不是把两个功能放到同一个函数中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def lcm(x: int, y: int) -&amp;gt; int:
    &quot;&quot;&quot;求最小公倍数&quot;&quot;&quot;
    return x * y // gcd(x, y)


def gcd(x: int, y: int) -&amp;gt; int:
    &quot;&quot;&quot;求最大公约数&quot;&quot;&quot;
    while y % x != 0:
        x, y = y % x, x
    return x
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：函数之间可以相互调用，上面求最小公倍数的&lt;code&gt;lcm&lt;/code&gt;函数调用了求最大公约数的&lt;code&gt;gcd&lt;/code&gt;函数，通过 $\frac{x \times y}{ gcd(x, y)}$ 来计算最小公倍数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子4：数据统计&lt;/h4&gt;
&lt;p&gt;假设样本数据保存一个列表中，设计计算样本数据描述性统计信息的函数。描述性统计信息通常包括：算术平均值、中位数、极差（最大值和最小值的差）、方差、标准差、变异系数等，计算公式如下所示。&lt;/p&gt;
&lt;p&gt;样本均值（sample mean）：&lt;/p&gt;
&lt;p&gt;$$
\bar{x} = \frac{\sum_{i=1}^{n}x_{i}}{n} = \frac{x_{1}+x_{2}+\cdots +x_{n}}{n}
$$&lt;/p&gt;
&lt;p&gt;样本方差（sample variance）：&lt;/p&gt;
&lt;p&gt;$$
s^2 = \frac {\sum_{i=1}^{n}(x_i - \bar{x})^2} {n-1}
$$&lt;/p&gt;
&lt;p&gt;样本标准差（sample standard deviation）：&lt;/p&gt;
&lt;p&gt;$$
s = \sqrt{\frac{\sum_{i=1}^{n}(x_i - \bar{x})^2}{n-1}}
$$&lt;/p&gt;
&lt;p&gt;变异系数（coefficient of sample variation）：&lt;/p&gt;
&lt;p&gt;$$
CV = \frac{s}{\bar{x}}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def ptp(data):
    &quot;&quot;&quot;极差（全距）&quot;&quot;&quot;
    return max(data) - min(data)


def mean(data):
    &quot;&quot;&quot;算术平均&quot;&quot;&quot;
    return sum(data) / len(data)


def median(data):
    &quot;&quot;&quot;中位数&quot;&quot;&quot;
    temp, size = sorted(data), len(data)
    if size % 2 != 0:
        return temp[size // 2]
    else:
        return mean(temp[size // 2 - 1:size // 2 + 1])


def var(data, ddof=1):
    &quot;&quot;&quot;方差&quot;&quot;&quot;
    x_bar = mean(data)
    temp = [(num - x_bar) ** 2 for num in data]
    return sum(temp) / (len(temp) - ddof)


def std(data, ddof=1):
    &quot;&quot;&quot;标准差&quot;&quot;&quot;
    return var(data, ddof) ** 0.5


def cv(data, ddof=1):
    &quot;&quot;&quot;变异系数&quot;&quot;&quot;
    return std(data, ddof) / mean(data)


def describe(data):
    &quot;&quot;&quot;输出描述性统计信息&quot;&quot;&quot;
    print(f&apos;均值: {mean(data)}&apos;)
    print(f&apos;中位数: {median(data)}&apos;)
    print(f&apos;极差: {ptp(data)}&apos;)
    print(f&apos;方差: {var(data)}&apos;)
    print(f&apos;标准差: {std(data)}&apos;)
    print(f&apos;变异系数: {cv(data)}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明1&lt;/strong&gt;：中位数是将数据按照升序或降序排列后位于中间的数，它描述了数据的中等水平。中位数的计算分两种情况：当数据体量$n$为奇数时，中位数是位于 $\frac{n + 1}{2}$ 位置的元素；当数据体量 $\small{n}$ 为偶数时，中位数是位于 $\frac{n}{2}$ 和 $\frac{n}{2} + 1$ 两个位置元素的均值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明2&lt;/strong&gt;：计算方差和标准差的函数中有一个名为&lt;code&gt;ddof&lt;/code&gt;的参数，它代表了可以调整的自由度，默认值为 1。在计算样本方差和样本标准差时，需要进行自由度校正；如果要计算总体方差和总体标准差，可以将&lt;code&gt;ddof&lt;/code&gt;参数赋值为 0，即不需要进行自由度校正。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;说明3&lt;/strong&gt;：&lt;code&gt;describe&lt;/code&gt;函数将上面封装好的统计函数组装到一起，用于输出数据的描述性统计信息。事实上，Python 标准库中有一个名为&lt;code&gt;statistics&lt;/code&gt;的模块，它已经把获取描述性统计信息的函数封装好了，有兴趣的读者可以自行了解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子5：双色球随机选号&lt;/h4&gt;
&lt;p&gt;我们用函数重构之前讲过的双色球随机选号的例子（《第09课：常用数据结构之列表-2》），将生成随机号码和输出一组号码的功能分别封装到两个函数中，然后通过调用函数实现机选&lt;code&gt;N&lt;/code&gt;注号码的功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
双色球随机选号程序

Author: 骆昊
Version: 1.3
&quot;&quot;&quot;
import random

RED_BALLS = [i for i in range(1, 34)]
BLUE_BALLS = [i for i in range(1, 17)]


def choose():
    &quot;&quot;&quot;
    生成一组随机号码
    :return: 保存随机号码的列表
    &quot;&quot;&quot;
    selected_balls = random.sample(RED_BALLS, 6)
    selected_balls.sort()
    selected_balls.append(random.choice(BLUE_BALLS))
    return selected_balls


def display(balls):
    &quot;&quot;&quot;
    格式输出一组号码
    :param balls: 保存随机号码的列表
    &quot;&quot;&quot;
    for ball in balls[:-1]:
        print(f&apos;\033[031m{ball:0&amp;gt;2d}\033[0m&apos;, end=&apos; &apos;)
    print(f&apos;\033[034m{balls[-1]:0&amp;gt;2d}\033[0m&apos;)


n = int(input(&apos;生成几注号码: &apos;))
for _ in range(n):
    display(choose())
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：大家看看&lt;code&gt;display(choose())&lt;/code&gt;这行代码，这里我们先通过&lt;code&gt;choose&lt;/code&gt;函数获得一组随机号码，然后把&lt;code&gt;choose&lt;/code&gt;函数的返回值作为&lt;code&gt;display&lt;/code&gt;函数的参数，通过&lt;code&gt;display&lt;/code&gt;函数将选中的随机号码显示出来。重构之后的代码逻辑非常清晰，代码的可读性更强了。如果有人为你封装了这两个函数，你仅仅是函数的调用者，其实你根本不用关心&lt;code&gt;choose&lt;/code&gt;函数和&lt;code&gt;display&lt;/code&gt;函数的内部实现，你只需要知道调用&lt;code&gt;choose&lt;/code&gt;函数可以生成一组随机号码，而调用&lt;code&gt;display&lt;/code&gt;函数传入一个列表，就可以输出这组号码。将来我们使用各种各样的 Python 三方库时，我们也根本不关注它们的底层实现，我们需要知道的仅仅是调用哪个函数可以解决问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;在写代码尤其是开发商业项目的时候，一定要有意识的&lt;strong&gt;将相对独立且重复使用的功能封装成函数&lt;/strong&gt;，这样不管是自己还是团队的其他成员都可以通过调用函数的方式来使用这些功能，减少工作中那些重复且乏味的劳动。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;16 函数使用进阶&lt;/h2&gt;
&lt;p&gt;我们继续探索定义和使用函数的相关知识。通过前面的学习，我们知道了函数有自变量（参数）和因变量（返回值），自变量可以是任意的数据类型，因变量也可以是任意的数据类型，那么这里就有一个小问题，我们能不能用函数作为函数的参数，用函数作为函数的返回值？这里我们先说结论：&lt;strong&gt;Python 中的函数是“一等函数”&lt;/strong&gt;，所谓“一等函数”指的就是函数可以赋值给变量，函数可以作为函数的参数，函数也可以作为函数的返回值。把一个函数作为其他函数的参数或返回值的用法，我们通常称之为“高阶函数”。&lt;/p&gt;
&lt;h4&gt;高阶函数&lt;/h4&gt;
&lt;p&gt;我们回到之前讲过的一个例子，设计一个函数，传入任意多个参数，对其中&lt;code&gt;int&lt;/code&gt;类型或&lt;code&gt;float&lt;/code&gt;类型的元素实现求和操作。我们对之前的代码稍作调整，让整个代码更加紧凑一些，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def calc(*args, **kwargs):
    items = list(args) + list(kwargs.values())
    result = 0
    for item in items:
        if type(item) in (int, float):
            result += item
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望上面的&lt;code&gt;calc&lt;/code&gt;函数不仅仅可以做多个参数的求和，还可以实现更多的甚至是自定义的二元运算，我们该怎么做呢？上面的代码只能求和是因为函数中使用了&lt;code&gt;+=&lt;/code&gt;运算符，这使得函数跟加法运算形成了耦合关系，如果能解除这种耦合关系，函数的通用性和灵活性就会更好。解除耦合的办法就是将&lt;code&gt;+&lt;/code&gt;运算符变成函数调用，并将其设计为函数的参数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def calc(init_value, op_func, *args, **kwargs):
    items = list(args) + list(kwargs.values())
    result = init_value
    for item in items:
        if type(item) in (int, float):
            result = op_func(result, item)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，上面的函数增加了两个参数，其中&lt;code&gt;init_value&lt;/code&gt;代表运算的初始值，&lt;code&gt;op_func&lt;/code&gt;代表二元运算函数，为了调用修改后的函数，我们先定义做加法和乘法运算的函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add(x, y):
    return x + y


def mul(x, y):
    return x * y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要做求和的运算，我们可以按照下面的方式调用&lt;code&gt;calc&lt;/code&gt;函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(calc(0, add, 1, 2, 3, 4, 5))  # 15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要做求乘积运算，我们可以按照下面的方式调用&lt;code&gt;calc&lt;/code&gt;函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(calc(1, mul, 1, 2, 3, 4, 5))  # 120 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;calc&lt;/code&gt;函数通过将运算符变成函数的参数，实现了跟加法运算的解耦合，这是一种非常高明和实用的编程技巧，但对于最初学者来说可能会觉得难以理解，建议大家细品一下。需要注意上面的代码中，将函数作为参数传入其他函数和直接调用函数是有显著的区别的，调用函数需要在函数名后面跟上圆括号，而把函数作为参数时只需要函数名即可**。&lt;/p&gt;
&lt;p&gt;如果我们没有提前定义好&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;mul&lt;/code&gt;函数，也可以使用 Python 标准库中的&lt;code&gt;operator&lt;/code&gt;模块提供的&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;mul&lt;/code&gt;函数，它们分别代表了做加法和做乘法的二元运算，我们拿过来直接使用即可，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import operator

print(calc(0, operator.add, 1, 2, 3, 4, 5))  # 15
print(calc(1, operator.mul, 1, 2, 3, 4, 5))  # 120
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 内置函数中有不少高阶函数，我们前面提到过的&lt;code&gt;filter&lt;/code&gt;和&lt;code&gt;map&lt;/code&gt;函数就是高阶函数，前者可以实现对序列中元素的过滤，后者可以实现对序列中元素的映射，例如我们要去掉一个整数列表中的奇数，并对所有的偶数求平方得到一个新的列表，就可以直接使用这两个函数来做到，具体的做法是如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_even(num):
    &quot;&quot;&quot;判断num是不是偶数&quot;&quot;&quot;
    return num % 2 == 0


def square(num):
    &quot;&quot;&quot;求平方&quot;&quot;&quot;
    return num ** 2


old_nums = [35, 12, 8, 99, 60, 52]
new_nums = list(map(square, filter(is_even, old_nums)))
print(new_nums)  # [144, 64, 3600, 2704]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，要完成上面代码的功能，也可以使用列表生成式，列表生成式的做法更为简单优雅。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;old_nums = [35, 12, 8, 99, 60, 52]
new_nums = [num ** 2 for num in old_nums if num % 2 == 0]
print(new_nums)  # [144, 64, 3600, 2704]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再来讨论一个内置函数&lt;code&gt;sorted&lt;/code&gt;，它可以实现对容器型数据类型（如：列表、字典等）元素的排序。我们之前讲过&lt;code&gt;list&lt;/code&gt;类型的&lt;code&gt;sort&lt;/code&gt;方法，它实现了对列表元素的排序，&lt;code&gt;sorted&lt;/code&gt;函数从功能上来讲跟列表的&lt;code&gt;sort&lt;/code&gt;方法没有区别，但它会返回排序后的列表对象，而不是直接修改原来的列表，这一点我们称为&lt;strong&gt;函数的无副作用设计&lt;/strong&gt;，也就是说调用函数除了产生返回值以外，不会对程序的状态或外部环境产生任何其他的影响。使用&lt;code&gt;sorted&lt;/code&gt;函数排序时，可以通过高阶函数的形式自定义排序的规则，我们通过下面的例子加以说明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;old_strings = [&apos;in&apos;, &apos;apple&apos;, &apos;zoo&apos;, &apos;waxberry&apos;, &apos;pear&apos;]
new_strings = sorted(old_strings)
print(new_strings)  # [&apos;apple&apos;, &apos;in&apos;, &apos;pear&apos;, waxberry&apos;, &apos;zoo&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码对大家来说并不陌生，但是如果希望根据字符串的长度而不是字母表顺序对列表元素排序，我们可以向&lt;code&gt;sorted&lt;/code&gt;函数传入一个名为&lt;code&gt;key&lt;/code&gt;的参数，将&lt;code&gt;key&lt;/code&gt;参数赋值为获取字符串长度的函数&lt;code&gt;len&lt;/code&gt;，这个函数我们在之前的课程中讲到过，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;old_strings = [&apos;in&apos;, &apos;apple&apos;, &apos;zoo&apos;, &apos;waxberry&apos;, &apos;pear&apos;]
new_strings = sorted(old_strings, key=len)
print(new_strings)  # [&apos;in&apos;, &apos;zoo&apos;, &apos;pear&apos;, &apos;apple&apos;, &apos;waxberry&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：列表类型的&lt;code&gt;sort&lt;/code&gt;方法也有同样的&lt;code&gt;key&lt;/code&gt;参数，有兴趣的读者可以自行尝试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Lambda函数&lt;/h4&gt;
&lt;p&gt;在使用高阶函数的时候，如果作为参数或者返回值的函数本身非常简单，一行代码就能够完成，也不需要考虑对函数的复用，那么我们可以使用 lambda 函数。Python 中的 lambda 函数是没有的名字函数，所以很多人也把它叫做&lt;strong&gt;匿名函数&lt;/strong&gt;，lambda 函数只能有一行代码，代码中的表达式产生的运算结果就是这个匿名函数的返回值。之前的代码中，我们写的&lt;code&gt;is_even&lt;/code&gt;和&lt;code&gt;square&lt;/code&gt;函数都只有一行代码，我们可以考虑用 lambda 函数来替换掉它们，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;old_nums = [35, 12, 8, 99, 60, 52]
new_nums = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, old_nums)))
print(new_nums)  # [144, 64, 3600, 2704]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码可以看出，定义 lambda 函数的关键字是&lt;code&gt;lambda&lt;/code&gt;，后面跟函数的参数，如果有多个参数用逗号进行分隔；冒号后面的部分就是函数的执行体，通常是一个表达式，表达式的运算结果就是 lambda 函数的返回值，不需要写&lt;code&gt;return&lt;/code&gt; 关键字。&lt;/p&gt;
&lt;p&gt;前面我们说过，Python 中的函数是“一等函数”，函数是可以直接赋值给变量的。在学习了 lambda 函数之后，前面我们写过的一些函数就可以用一行代码来实现它们了，大家可以看看能否理解下面的求阶乘和判断素数的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import functools
import operator

# 用一行代码实现计算阶乘的函数
fac = lambda n: functools.reduce(operator.mul, range(2, n + 1), 1)

# 用一行代码实现判断素数的函数
is_prime = lambda x: all(map(lambda f: x % f, range(2, int(x ** 0.5) + 1)))

# 调用Lambda函数
print(fac(6))        # 720
print(is_prime(37))  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示1&lt;/strong&gt;：上面使用的&lt;code&gt;reduce&lt;/code&gt;函数是 Python 标准库&lt;code&gt;functools&lt;/code&gt;模块中的函数，它可以实现对一组数据的归约操作，类似于我们之前定义的&lt;code&gt;calc&lt;/code&gt;函数，第一个参数是代表运算的函数，第二个参数是运算的数据，第三个参数是运算的初始值。很显然，&lt;code&gt;reduce&lt;/code&gt;函数也是高阶函数，它和&lt;code&gt;filter&lt;/code&gt;函数、&lt;code&gt;map&lt;/code&gt;函数一起构成了处理数据中非常关键的三个动作：&lt;strong&gt;过滤&lt;/strong&gt;、&lt;strong&gt;映射&lt;/strong&gt;和&lt;strong&gt;归约&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示2&lt;/strong&gt;：上面判断素数的 lambda 函数通过&lt;code&gt;range&lt;/code&gt;函数构造了从 2 到 $\small{\sqrt{x}}$ 的范围，检查这个范围有没有&lt;code&gt;x&lt;/code&gt;的因子。&lt;code&gt;all&lt;/code&gt;函数也是 Python 内置函数，如果传入的序列中所有的布尔值都是&lt;code&gt;True&lt;/code&gt;，&lt;code&gt;all&lt;/code&gt;函数返回&lt;code&gt;True&lt;/code&gt;，否则&lt;code&gt;all&lt;/code&gt;函数返回&lt;code&gt;False&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;偏函数&lt;/h4&gt;
&lt;p&gt;偏函数是指固定函数的某些参数，生成一个新的函数，这样就无需在每次调用函数时都传递相同的参数。在 Python 语言中，我们可以使用&lt;code&gt;functools&lt;/code&gt;模块的&lt;code&gt;partial&lt;/code&gt;函数来创建偏函数。例如，&lt;code&gt;int&lt;/code&gt;函数在默认情况下可以将字符串视为十进制整数进行类型转换，如果我们修改它的&lt;code&gt;base&lt;/code&gt;参数，就可以定义出三个新函数，分别用于将二进制、八进制、十六进制字符串转换为整数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import functools

int2 = functools.partial(int, base=2)
int8 = functools.partial(int, base=8)
int16 = functools.partial(int, base=16)

print(int(&apos;1001&apos;))    # 1001

print(int2(&apos;1001&apos;))   # 9
print(int8(&apos;1001&apos;))   # 513
print(int16(&apos;1001&apos;))  # 4097
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知大家是否注意到，&lt;code&gt;partial&lt;/code&gt;函数的第一个参数和返回值都是函数，它将传入的函数处理成一个新的函数返回。通过构造偏函数，我们可以结合实际的使用场景将原函数变成使用起来更为便捷的新函数，不知道大家有没有觉得这波操作很有意思。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 中的函数是一等函数，可以赋值给变量，也可以作为函数的参数和返回值，这也就意味着我们可以在 Python 中使用高阶函数。高阶函数的概念对新手并不友好，但它却带来了函数设计上的灵活性。如果我们要定义的函数非常简单，只有一行代码，而且不需要函数名来复用它，我们可以使用 lambda 函数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;17 函数高级应用&lt;/h2&gt;
&lt;p&gt;在上一个章节中，我们探索了 Python 中的高阶函数，相信大家对函数的定义和应用有了更深刻的认知。本章我们继续为大家讲解函数相关的知识，一个是 Python 中的特色语法装饰器，一个是函数的递归调用。&lt;/p&gt;
&lt;h4&gt;装饰器&lt;/h4&gt;
&lt;p&gt;Python 语言中，装饰器是“&lt;strong&gt;用一个函数装饰另外一个函数并为其提供额外的能力&lt;/strong&gt;”的语法现象。装饰器本身是一个函数，它的参数是被装饰的函数，它的返回值是一个带有装饰功能的函数。通过前面的描述，相信大家已经听出来了，装饰器是一个高阶函数，它的参数和返回值都是函数。但是，装饰器的概念对编程语言的初学者来说，还是让人头疼的，下面我们先通过一个简单的例子来说明装饰器的作用。假设有名为&lt;code&gt;downlaod&lt;/code&gt;和&lt;code&gt;upload&lt;/code&gt;的两个函数，分别用于文件的下载和上传，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time


def download(filename):
    &quot;&quot;&quot;下载文件&quot;&quot;&quot;
    print(f&apos;开始下载{filename}.&apos;)
    time.sleep(random.random() * 6)
    print(f&apos;{filename}下载完成.&apos;)

    
def upload(filename):
    &quot;&quot;&quot;上传文件&quot;&quot;&quot;
    print(f&apos;开始上传{filename}.&apos;)
    time.sleep(random.random() * 8)
    print(f&apos;{filename}上传完成.&apos;)

    
download(&apos;MySQL从删库到跑路.avi&apos;)
upload(&apos;Python从入门到住院.pdf&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：上面的代码用休眠一段随机时间的方式模拟了下载和上传文件需要花费一定的时间，并没有真正的联网上传下载文件。用 Python 语言实现联网上传下载文件也非常简单，后面我们会讲到相关的知识。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在有一个新的需求，我们希望知道调用&lt;code&gt;download&lt;/code&gt;和&lt;code&gt;upload&lt;/code&gt;函数上传下载文件到底用了多少时间，这应该如何实现呢？相信很多小伙伴已经想到了，我们可以在函数开始执行的时候记录一个时间，在函数调用结束后记录一个时间，两个时间相减就可以计算出下载或上传的时间，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start = time.time()
download(&apos;MySQL从删库到跑路.avi&apos;)
end = time.time()
print(f&apos;花费时间: {end - start:.2f}秒&apos;)
start = time.time()
upload(&apos;Python从入门到住院.pdf&apos;)
end = time.time()
print(f&apos;花费时间: {end - start:.2f}秒&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码，我们可以在下载和上传文件时记录下耗费的时间，但不知道大家是否注意到，上面记录时间、计算和显示执行时间的代码都是重复代码。有编程经验的人都知道，&lt;strong&gt;重复的代码是万恶之源&lt;/strong&gt;，那么有没有办法在不写重复代码的前提下，用一种简单优雅的方式记录下函数的执行时间呢？在 Python 语言中，装饰器就是解决这类问题的最佳选择。通过装饰器语法，我们可以把跟原来的业务（上传和下载）没有关系计时功能的代码封装到一个函数中，如果&lt;code&gt;upload&lt;/code&gt;和&lt;code&gt;download&lt;/code&gt;函数需要记录时间，我们直接把装饰器作用到这两个函数上即可。既然上面提到了，装饰器是一个高阶函数，它的参数和返回值都是函数，我们将记录时间的装饰器姑且命名为&lt;code&gt;record_time&lt;/code&gt;，那么它的整体结构应该如下面的代码所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def record_time(func):
    
    def wrapper(*args, **kwargs):
        
        result = func(*args, **kwargs)
        
        return result
    
    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相信大家注意到了，&lt;code&gt;record_time&lt;/code&gt;函数的参数&lt;code&gt;func&lt;/code&gt;代表了一个被装饰的函数，函数里面定义的&lt;code&gt;wrapper&lt;/code&gt;函数是带有装饰功能的函数，它会执行被装饰的函数&lt;code&gt;func&lt;/code&gt;，它还需要返回在最后产生函数执行的返回值。不知大家是否留意到，上面的代码我在第4行和第6行留下了两个空行，这意味着我们可以这些地方添加代码来实现额外的功能。&lt;code&gt;record_time&lt;/code&gt;函数最终会返回这个带有装饰功能的函数&lt;code&gt;wrapper&lt;/code&gt;并通过它替代原函数&lt;code&gt;func&lt;/code&gt;，当原函数&lt;code&gt;func&lt;/code&gt;被&lt;code&gt;record_time&lt;/code&gt;函数装饰后，我们调用它时其实调用的是&lt;code&gt;wrapper&lt;/code&gt;函数，所以才获得了额外的能力。&lt;code&gt;wrapper&lt;/code&gt;函数的参数比较特殊，由于我们要用&lt;code&gt;wrapper&lt;/code&gt;替代原函数&lt;code&gt;func&lt;/code&gt;，但是我们又不清楚原函数&lt;code&gt;func&lt;/code&gt;会接受哪些参数，所以我们就通过可变参数和关键字参数照单全收，然后在调用&lt;code&gt;func&lt;/code&gt;的时候，原封不动的全部给它。这里还要强调一下，Python 语言支持函数的嵌套定义，就像上面，我们可以在&lt;code&gt;record_time&lt;/code&gt;函数中定义&lt;code&gt;wrapper&lt;/code&gt;函数，这个操作在很多编程语言中并不被支持。&lt;/p&gt;
&lt;p&gt;看懂这个结构后，我们就可以把记录时间的功能写到这个装饰器中，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time


def record_time(func):

    def wrapper(*args, **kwargs):
        # 在执行被装饰的函数之前记录开始时间
        start = time.time()
        # 执行被装饰的函数并获取返回值
        result = func(*args, **kwargs)
        # 在执行被装饰的函数之后记录结束时间
        end = time.time()
        # 计算和显示被装饰函数的执行时间
        print(f&apos;{func.__name__}执行时间: {end - start:.2f}秒&apos;)
        # 返回被装饰函数的返回值
        return result
    
    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写装饰器虽然颇费周折，但是这是个一劳永逸的骚操作，将来再有记录函数执行时间的需求时，我们只需要添加上面的装饰器即可。使用上面的装饰器函数有两种方式，第一种方式就是直接调用装饰器函数，传入被装饰的函数并获得返回值，我们可以用这个返回值直接替代原来的函数，那么在调用时就已经获得了装饰器提供的额外的能力（记录执行时间），大家试试下面的代码就明白了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;download = record_time(download)
upload = record_time(upload)
download(&apos;MySQL从删库到跑路.avi&apos;)
upload(&apos;Python从入门到住院.pdf&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Python 中，使用装饰器还有更为便捷的&lt;strong&gt;语法糖&lt;/strong&gt;（编程语言中添加的某种语法，这种语法对语言的功能没有影响，但是使用更加便捷，代码的可读性也更强，我们将其称之为“语法糖”或“糖衣语法”），可以用&lt;code&gt;@装饰器函数&lt;/code&gt;将装饰器函数直接放在被装饰的函数上，效果跟上面的代码相同。我们把完整的代码为大家罗列出来，大家可以再看看我们是如何定义和使用装饰器的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time


def record_time(func):

    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f&apos;{func.__name__}执行时间: {end - start:.2f}秒&apos;)
        return result

    return wrapper


@record_time
def download(filename):
    print(f&apos;开始下载{filename}.&apos;)
    time.sleep(random.random() * 6)
    print(f&apos;{filename}下载完成.&apos;)


@record_time
def upload(filename):
    print(f&apos;开始上传{filename}.&apos;)
    time.sleep(random.random() * 8)
    print(f&apos;{filename}上传完成.&apos;)


download(&apos;MySQL从删库到跑路.avi&apos;)
upload(&apos;Python从入门到住院.pdf&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码，我们通过装饰器语法糖为&lt;code&gt;download&lt;/code&gt;和&lt;code&gt;upload&lt;/code&gt;函数添加了装饰器，被装饰后的&lt;code&gt;download&lt;/code&gt;和&lt;code&gt;upload&lt;/code&gt;函数其实就是我们在装饰器中返回的&lt;code&gt;wrapper&lt;/code&gt;函数，调用它们其实就是在调用&lt;code&gt;wrapper&lt;/code&gt;函数，所以才有了记录函数执行时间的功能。&lt;/p&gt;
&lt;p&gt;如果在代码的某些地方，我们想去掉装饰器的作用执行原函数，那么在定义装饰器函数的时候，需要做一点点额外的工作。Python 标准库&lt;code&gt;functools&lt;/code&gt;模块的&lt;code&gt;wraps&lt;/code&gt;函数也是一个装饰器，我们将它放在&lt;code&gt;wrapper&lt;/code&gt;函数上，这个装饰器可以帮我们保留被装饰之前的函数，这样在需要取消装饰器时，可以通过被装饰函数的&lt;code&gt;__wrapped__&lt;/code&gt;属性获得被装饰之前的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import time

from functools import wraps


def record_time(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f&apos;{func.__name__}执行时间: {end - start:.2f}秒&apos;)
        return result

    return wrapper


@record_time
def download(filename):
    print(f&apos;开始下载{filename}.&apos;)
    time.sleep(random.random() * 6)
    print(f&apos;{filename}下载完成.&apos;)


@record_time
def upload(filename):
    print(f&apos;开始上传{filename}.&apos;)
    time.sleep(random.random() * 8)
    print(f&apos;{filename}上传完成.&apos;)


# 调用装饰后的函数会记录执行时间
download(&apos;MySQL从删库到跑路.avi&apos;)
upload(&apos;Python从入门到住院.pdf&apos;)
# 取消装饰器的作用不记录执行时间
download.__wrapped__(&apos;MySQL必知必会.pdf&apos;)
upload.__wrapped__(&apos;Python从新手到大师.pdf&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;装饰器函数本身也可以参数化&lt;/strong&gt;，简单的说就是装饰器也是可以通过调用者传入的参数来进行定制的，这个知识点我们在后面用到的时候再为大家讲解。&lt;/p&gt;
&lt;h4&gt;递归调用&lt;/h4&gt;
&lt;p&gt;Python 中允许函数嵌套定义，也允许函数之间相互调用，而且一个函数还可以直接或间接的调用自身。函数自己调用自己称为递归调用，那么递归调用有什么用处呢？现实中，有很多问题的定义本身就是一个递归定义，例如我们之前讲到的阶乘，非负整数&lt;code&gt;N&lt;/code&gt;的阶乘是&lt;code&gt;N&lt;/code&gt;乘以&lt;code&gt;N-1&lt;/code&gt;的阶乘，即 $\small{N! = N \times (N-1)!}$ ，定义的左边和右边都出现了阶乘的概念，所以这是一个递归定义。既然如此，我们可以使用递归调用的方式来写一个求阶乘的函数，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fac(num):
    if num in (0, 1):
        return 1
    return num * fac(num - 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，&lt;code&gt;fac&lt;/code&gt;函数中又调用了&lt;code&gt;fac&lt;/code&gt;函数，这就是所谓的递归调用。代码第2行的&lt;code&gt;if&lt;/code&gt;条件叫做递归的收敛条件，简单的说就是什么时候要结束函数的递归调用，在计算阶乘时，如果计算到&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;的阶乘，就停止递归调用，直接返回&lt;code&gt;1&lt;/code&gt;；代码第4行的&lt;code&gt;num * fac(num - 1)&lt;/code&gt;是递归公式，也就是阶乘的递归定义。下面，我们简单的分析下，如果用&lt;code&gt;fac(5)&lt;/code&gt;计算&lt;code&gt;5&lt;/code&gt;的阶乘，整个过程会是怎样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 递归调用函数入栈
# 5 * fac(4)
# 5 * (4 * fac(3))
# 5 * (4 * (3 * fac(2)))
# 5 * (4 * (3 * (2 * fac(1))))
# 停止递归函数出栈
# 5 * (4 * (3 * (2 * 1)))
# 5 * (4 * (3 * 2))
# 5 * (4 * 6)
# 5 * 24
# 120
print(fac(5))    # 120
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，函数调用会通过内存中称为“栈”（stack）的数据结构来保存当前代码的执行现场，函数调用结束后会通过这个栈结构恢复之前的执行现场。栈是一种先进后出的数据结构，这也就意味着最早入栈的函数最后才会返回，而最后入栈的函数会最先返回。例如调用一个名为&lt;code&gt;a&lt;/code&gt;的函数，函数&lt;code&gt;a&lt;/code&gt;的执行体中又调用了函数&lt;code&gt;b&lt;/code&gt;，函数&lt;code&gt;b&lt;/code&gt;的执行体中又调用了函数&lt;code&gt;c&lt;/code&gt;，那么最先入栈的函数是&lt;code&gt;a&lt;/code&gt;，最先出栈的函数是&lt;code&gt;c&lt;/code&gt;。每进入一个函数调用，栈就会增加一层栈帧（stack frame），栈帧就是我们刚才提到的保存当前代码执行现场的结构；每当函数调用结束后，栈就会减少一层栈帧。通常，内存中的栈空间很小，因此递归调用的次数如果太多，会导致栈溢出（stack overflow），所以&lt;strong&gt;递归调用一定要确保能够快速收敛&lt;/strong&gt;。我们可以尝试执行&lt;code&gt;fac(5000)&lt;/code&gt;，看看是不是会提示&lt;code&gt;RecursionError&lt;/code&gt;错误，错误消息为：&lt;code&gt;maximum recursion depth exceeded in comparison&lt;/code&gt;（超出最大递归深度），其实就是发生了栈溢出。&lt;/p&gt;
&lt;p&gt;如果我们使用官方的 Python 解释器（CPython），默认将函数调用的栈结构最大深度设置为&lt;code&gt;1000&lt;/code&gt;层。如果超出这个深度，就会发生上面说的&lt;code&gt;RecursionError&lt;/code&gt;。当然，我们可以使用&lt;code&gt;sys&lt;/code&gt;模块的&lt;code&gt;setrecursionlimit&lt;/code&gt;函数来改变递归调用的最大深度，但是我们不建议这样做，因为让递归快速收敛才是我们应该做的事情，否则就应该考虑使用循环递推而不是递归。&lt;/p&gt;
&lt;p&gt;再举一个之前讲过的生成斐波那契数列的例子，因为斐波那契数列前两个数都是&lt;code&gt;1&lt;/code&gt;，从第三个数开始，每个数是前两个数相加的和，可以记为&lt;code&gt;f(n) = f(n - 1) + f(n - 2)&lt;/code&gt;，很显然这又是一个递归的定义，所以我们可以用下面的递归调用函数来计算第​&lt;code&gt;n&lt;/code&gt;个斐波那契数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib1(n):
    if n in (1, 2):
        return 1
    return fib1(n - 1) + fib1(n - 2)


for i in range(1, 21):
    print(fib1(i))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要提醒大家，上面计算斐波那契数的代码虽然看起来非常简单明了，但执行性能是比较糟糕的。大家可以试一试，把上面代码&lt;code&gt;for&lt;/code&gt;循环中&lt;code&gt;range&lt;/code&gt;函数的第二个参数修改为&lt;code&gt;51&lt;/code&gt;，即输出前50个斐波那契数，看看需要多长时间，也欢迎大家在评论区留下你的代码执行时间。至于为什么这么慢，大家可以自己思考一下原因。很显然，直接使用循环递推的方式获得斐波那契数列是更好的选择，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def fib2(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此以外，我们还可以使用 Python 标准库中&lt;code&gt;functools&lt;/code&gt;模块的&lt;code&gt;lru_cache&lt;/code&gt;函数来优化上面的递归代码。&lt;code&gt;lru_cache&lt;/code&gt;函数是一个装饰器函数，我们将其置于上面的函数&lt;code&gt;fib1&lt;/code&gt;之上，它可以缓存该函数的执行结果从而避免在递归调用的过程中产生大量的重复运算，这样代码的执行性能就有“飞一般”的提升。大家可以尝试输出前50个斐波那契数，看看加上装饰器以后代码需要执行多长时间，评论区见！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import lru_cache


@lru_cache()
def fib1(n):
    if n in (1, 2):
        return 1
    return fib1(n - 1) + fib1(n - 2)


for i in range(1, 51):
    print(i, fib1(i))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：&lt;code&gt;lru_cache&lt;/code&gt;函数是一个带参数的装饰器，所以上面第4行代码使用装饰器语法糖时，&lt;code&gt;lru_cache&lt;/code&gt;后面要跟上圆括号。&lt;code&gt;lru_cache&lt;/code&gt;函数有一个非常重要的参数叫&lt;code&gt;maxsize&lt;/code&gt;，它可以用来定义缓存空间的大小，默认值是128。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;装饰器是 Python 语言中的特色语法，&lt;strong&gt;可以通过装饰器来增强现有的函数&lt;/strong&gt;，这是一种非常有用的编程技巧。另一方面，通过函数递归调用，可以在代码层面将一些复杂的问题简单化，但是&lt;strong&gt;递归调用一定要注意收敛条件和递归公式&lt;/strong&gt;，找到递归公式才有机会使用递归调用，而收敛条件则确保了递归调用能停下来。函数调用通过内存中的栈空间来保存现场和恢复现场，栈空间通常都很小，所以&lt;strong&gt;递归如果不能迅速收敛，很可能会引发栈溢出错误，从而导致程序的崩溃&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;18 面向对象编程入门&lt;/h2&gt;
&lt;p&gt;面向对象编程是一种非常流行的&lt;strong&gt;编程范式&lt;/strong&gt;（programming paradigm），所谓编程范式就是&lt;strong&gt;程序设计的方法论&lt;/strong&gt;，简单的说就是程序员对程序的认知和理解以及他们编写代码的方式。&lt;/p&gt;
&lt;p&gt;在前面的课程中，我们说过“&lt;strong&gt;程序是指令的集合&lt;/strong&gt;”，运行程序时，程序中的语句会变成一条或多条指令，然后由CPU（中央处理器）去执行。为了简化程序的设计，我们又讲到了函数，&lt;strong&gt;把相对独立且经常重复使用的代码放置到函数中&lt;/strong&gt;，在需要使用这些代码的时候调用函数即可。如果一个函数的功能过于复杂和臃肿，我们又可以进一步&lt;strong&gt;将函数进一步拆分为多个子函数&lt;/strong&gt;来降低系统的复杂性。&lt;/p&gt;
&lt;p&gt;不知大家是否发现，编程其实是写程序的人按照计算机的工作方式通过代码控制机器完成任务。但是，计算机的工作方式与人类正常的思维模式是不同的，如果编程就必须抛弃人类正常的思维方式去迎合计算机，编程的乐趣就少了很多。这里，我想说的并不是我们不能按照计算机的工作方式去编写代码，但是当我们需要开发一个复杂的系统时，这种方式会让代码过于复杂，从而导致开发和维护工作都变得举步维艰。&lt;/p&gt;
&lt;p&gt;随着软件复杂性的增加，编写正确可靠的代码会变成了一项极为艰巨的任务，这也是很多人都坚信“软件开发是人类改造世界所有活动中最为复杂的活动”的原因。如何用程序描述复杂系统和解决复杂问题，就成为了所有程序员必须要思考和直面的问题。诞生于上世纪70年代的 Smalltalk 语言让软件开发者看到了希望，因为它引入了一种新的编程范式叫面向对象编程。在面向对象编程的世界里，程序中的&lt;strong&gt;数据和操作数据的函数是一个逻辑上的整体&lt;/strong&gt;，我们称之为&lt;strong&gt;对象&lt;/strong&gt;，&lt;strong&gt;对象可以接收消息&lt;/strong&gt;，解决问题的方法就是&lt;strong&gt;创建对象并向对象发出各种各样的消息&lt;/strong&gt;；通过消息传递，程序中的多个对象可以协同工作，这样就能构造出复杂的系统并解决现实中的问题。当然，面向对象编程的雏形还可以向前追溯到更早期的Simula语言，但这不是我们要讨论的重点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 今天我们使用的很多高级程序设计语言都支持面向对象编程，但是面向对象编程也不是解决软件开发中所有问题的“银弹”，或者说在软件开发这个行业目前还没有所谓的“银弹”。关于这个问题，大家可以参考 IBM360 系统之父弗雷德里克·布鲁克斯所发表的论文《没有银弹：软件工程的本质性与附属性工作》或软件工程的经典著作《人月神话》一书。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;类和对象&lt;/h4&gt;
&lt;p&gt;如果要用一句话来概括面向对象编程，我认为下面的说法是相当精辟和准确的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面向对象编程&lt;/strong&gt;：把一组数据和处理数据的方法组成&lt;strong&gt;对象&lt;/strong&gt;，把行为相同的对象归纳为&lt;strong&gt;类&lt;/strong&gt;，通过&lt;strong&gt;封装&lt;/strong&gt;隐藏对象的内部细节，通过&lt;strong&gt;继承&lt;/strong&gt;实现类的特化和泛化，通过&lt;strong&gt;多态&lt;/strong&gt;实现基于对象类型的动态分派。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话对初学者来说可能不那么容易理解，但是我可以先为大家圈出几个关键词：&lt;strong&gt;对象&lt;/strong&gt;（object）、&lt;strong&gt;类&lt;/strong&gt;（class）、&lt;strong&gt;封装&lt;/strong&gt;（encapsulation）、&lt;strong&gt;继承&lt;/strong&gt;（inheritance）、&lt;strong&gt;多态&lt;/strong&gt;（polymorphism）。&lt;/p&gt;
&lt;p&gt;我们先说说类和对象这两个词。在面向对象编程中，&lt;strong&gt;类是一个抽象的概念，对象是一个具体的概念&lt;/strong&gt;。我们把同一类对象的共同特征抽取出来就是一个类，比如我们经常说的人类，这是一个抽象概念，而我们每个人就是人类的这个抽象概念下的实实在在的存在，也就是一个对象。简而言之，&lt;strong&gt;类是对象的蓝图和模板，对象是类的实例，是可以接受消息的实体&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在面向对象编程的世界中，&lt;strong&gt;一切皆为对象&lt;/strong&gt;，&lt;strong&gt;对象都有属性和行为&lt;/strong&gt;，&lt;strong&gt;每个对象都是独一无二的&lt;/strong&gt;，而且&lt;strong&gt;对象一定属于某个类&lt;/strong&gt;。对象的属性是对象的静态特征，对象的行为是对象的动态特征。按照上面的说法，如果我们把拥有共同特征的对象的属性和行为都抽取出来，就可以定义出一个类。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/c1c6f5cffe.webp&quot; width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;h4&gt;定义类&lt;/h4&gt;
&lt;p&gt;在 Python 语言中，我们可以使用&lt;code&gt;class&lt;/code&gt;关键字加上类名来定义类，通过缩进我们可以确定类的代码块，就如同定义函数那样。在类的代码块中，我们需要写一些函数，我们说过类是一个抽象概念，那么这些函数就是我们对一类对象共同的动态特征的提取。写在类里面的函数我们通常称之为&lt;strong&gt;方法&lt;/strong&gt;，方法就是对象的行为，也就是对象可以接收的消息。方法的第一个参数通常都是&lt;code&gt;self&lt;/code&gt;，它代表了接收这个消息的对象本身。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student:

    def study(self, course_name):
        print(f&apos;学生正在学习{course_name}.&apos;)

    def play(self):
        print(f&apos;学生正在玩游戏.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建和使用对象&lt;/h4&gt;
&lt;p&gt;在我们定义好一个类之后，可以使用构造器语法来创建对象，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stu1 = Student()
stu2 = Student()
print(stu1)    # &amp;lt;__main__.Student object at 0x10ad5ac50&amp;gt;
print(stu2)    # &amp;lt;__main__.Student object at 0x10ad5acd0&amp;gt; 
print(hex(id(stu1)), hex(id(stu2)))    # 0x10ad5ac50 0x10ad5acd0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在类的名字后跟上圆括号就是所谓的构造器语法，上面的代码创建了两个学生对象，一个赋值给变量&lt;code&gt;stu1&lt;/code&gt;，一个赋值给变量&lt;code&gt;stu2&lt;/code&gt;。当我们用&lt;code&gt;print&lt;/code&gt;函数打印&lt;code&gt;stu1&lt;/code&gt;和&lt;code&gt;stu2&lt;/code&gt;两个变量时，我们会看到输出了对象在内存中的地址（十六进制形式），跟我们用&lt;code&gt;id&lt;/code&gt;函数查看对象标识获得的值是相同的。现在我们可以告诉大家，我们定义的变量其实保存的是一个对象在内存中的逻辑地址（位置），通过这个逻辑地址，我们就可以在内存中找到这个对象。所以&lt;code&gt;stu3 = stu2&lt;/code&gt;这样的赋值语句并没有创建新的对象，只是用一个新的变量保存了已有对象的地址。&lt;/p&gt;
&lt;p&gt;接下来，我们尝试给对象发消息，即调用对象的方法。刚才的&lt;code&gt;Student&lt;/code&gt;类中我们定义了&lt;code&gt;study&lt;/code&gt;和&lt;code&gt;play&lt;/code&gt;两个方法，两个方法的第一个参数&lt;code&gt;self&lt;/code&gt;代表了接收消息的学生对象，&lt;code&gt;study&lt;/code&gt;方法的第二个参数是学习的课程名称。Python中，给对象发消息有两种方式，请看下面的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 通过“类.方法”调用方法
# 第一个参数是接收消息的对象
# 第二个参数是学习的课程名称
Student.study(stu1, &apos;Python程序设计&apos;)    # 学生正在学习Python程序设计.
# 通过“对象.方法”调用方法
# 点前面的对象就是接收消息的对象
# 只需要传入第二个参数课程名称
stu1.study(&apos;Python程序设计&apos;)             # 学生正在学习Python程序设计.

Student.play(stu2)                      # 学生正在玩游戏.
stu2.play()                             # 学生正在玩游戏. 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;初始化方法&lt;/h4&gt;
&lt;p&gt;大家可能已经注意到了，刚才我们创建的学生对象只有行为没有属性，如果要给学生对象定义属性，我们可以修改&lt;code&gt;Student&lt;/code&gt;类，为其添加一个名为&lt;code&gt;__init__&lt;/code&gt;的方法。在我们调用&lt;code&gt;Student&lt;/code&gt;类的构造器创建对象时，首先会在内存中获得保存学生对象所需的内存空间，然后通过自动执行&lt;code&gt;__init__&lt;/code&gt;方法，完成对内存的初始化操作，也就是把数据放到内存空间中。所以我们可以通过给&lt;code&gt;Student&lt;/code&gt;类添加&lt;code&gt;__init__&lt;/code&gt;方法的方式为学生对象指定属性，同时完成对属性赋初始值的操作，正因如此，&lt;code&gt;__init__&lt;/code&gt;方法通常也被称为初始化方法。&lt;/p&gt;
&lt;p&gt;我们对上面的&lt;code&gt;Student&lt;/code&gt;类稍作修改，给学生对象添加&lt;code&gt;name&lt;/code&gt;（姓名）和&lt;code&gt;age&lt;/code&gt;（年龄）两个属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student:
    &quot;&quot;&quot;学生&quot;&quot;&quot;

    def __init__(self, name, age):
        &quot;&quot;&quot;初始化方法&quot;&quot;&quot;
        self.name = name
        self.age = age

    def study(self, course_name):
        &quot;&quot;&quot;学习&quot;&quot;&quot;
        print(f&apos;{self.name}正在学习{course_name}.&apos;)

    def play(self):
        &quot;&quot;&quot;玩耍&quot;&quot;&quot;
        print(f&apos;{self.name}正在玩游戏.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改刚才创建对象和给对象发消息的代码，重新执行一次，看看程序的执行结果有什么变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 调用Student类的构造器创建对象并传入初始化参数
stu1 = Student(&apos;骆昊&apos;, 44)
stu2 = Student(&apos;王大锤&apos;, 25)
stu1.study(&apos;Python程序设计&apos;)    # 骆昊正在学习Python程序设计.
stu2.play()                    # 王大锤正在玩游戏.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;面向对象的支柱&lt;/h4&gt;
&lt;p&gt;面向对象编程有三大支柱，就是我们之前给大家划重点的时候圈出的三个词：&lt;strong&gt;封装&lt;/strong&gt;、&lt;strong&gt;继承&lt;/strong&gt;和&lt;strong&gt;多态&lt;/strong&gt;。后面两个概念在下一节课中会详细说明，这里我们先说一下什么是封装。我自己对封装的理解是：&lt;strong&gt;隐藏一切可以隐藏的实现细节，只向外界暴露简单的调用接口&lt;/strong&gt;。我们在类中定义的对象方法其实就是一种封装，这种封装可以让我们在创建对象之后，只需要给对象发送一个消息就可以执行方法中的代码，也就是说我们在只知道方法的名字和参数（方法的外部视图），不知道方法内部实现细节（方法的内部视图）的情况下就完成了对方法的使用。&lt;/p&gt;
&lt;p&gt;举一个例子，假如要控制一个机器人帮我倒杯水，如果不使用面向对象编程，不做任何的封装，那么就需要向这个机器人发出一系列的指令，如站起来、向左转、向前走5步、拿起面前的水杯、向后转、向前走10步、弯腰、放下水杯、按下出水按钮、等待10秒、松开出水按钮、拿起水杯、向右转、向前走5步、放下水杯等，才能完成这个简单的操作，想想都觉得麻烦。按照面向对象编程的思想，我们可以将倒水的操作封装到机器人的一个方法中，当需要机器人帮我们倒水的时候，只需要向机器人对象发出倒水的消息就可以了，这样做不是更好吗？&lt;/p&gt;
&lt;p&gt;在很多场景下，面向对象编程其实就是一个三步走的问题。第一步定义类，第二步创建对象，第三步给对象发消息。当然，有的时候我们是不需要第一步的，因为我们想用的类可能已经存在了。之前我们说过，Python内置的&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;dict&lt;/code&gt;其实都是类，如果要创建列表、集合、字典对象，我们就不用自定义类了。当然，有的类并不是 Python 标准库中直接提供的，它可能来自于第三方的代码，如何安装和使用三方代码在后续课程中会进行讨论。在某些特殊的场景中，我们会用到名为“内置对象”的对象，所谓“内置对象”就是说上面三步走的第一步和第二步都不需要了，因为类已经存在而且对象已然创建过了，直接向对象发消息就可以了，这也就是我们常说的“开箱即用”。&lt;/p&gt;
&lt;h4&gt;面向对象案例&lt;/h4&gt;
&lt;h5&gt;例子1：时钟&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要求&lt;/strong&gt;：定义一个类描述数字时钟，提供走字和显示时间的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import time


# 定义时钟类
class Clock:
    &quot;&quot;&quot;数字时钟&quot;&quot;&quot;

    def __init__(self, hour=0, minute=0, second=0):
        &quot;&quot;&quot;初始化方法
        :param hour: 时
        :param minute: 分
        :param second: 秒
        &quot;&quot;&quot;
        self.hour = hour
        self.min = minute
        self.sec = second

    def run(self):
        &quot;&quot;&quot;走字&quot;&quot;&quot;
        self.sec += 1
        if self.sec == 60:
            self.sec = 0
            self.min += 1
            if self.min == 60:
                self.min = 0
                self.hour += 1
                if self.hour == 24:
                    self.hour = 0

    def show(self):
        &quot;&quot;&quot;显示时间&quot;&quot;&quot;
        return f&apos;{self.hour:0&amp;gt;2d}:{self.min:0&amp;gt;2d}:{self.sec:0&amp;gt;2d}&apos;


# 创建时钟对象
clock = Clock(23, 59, 58)
while True:
    # 给时钟对象发消息读取时间
    print(clock.show())
    # 休眠1秒钟
    time.sleep(1)
    # 给时钟对象发消息使其走字
    clock.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;例子2：平面上的点&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要求&lt;/strong&gt;：定义一个类描述平面上的点，提供计算到另一个点距离的方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class Point:
    &quot;&quot;&quot;平面上的点&quot;&quot;&quot;

    def __init__(self, x=0, y=0):
        &quot;&quot;&quot;初始化方法
        :param x: 横坐标
        :param y: 纵坐标
        &quot;&quot;&quot;
        self.x, self.y = x, y

    def distance_to(self, other):
        &quot;&quot;&quot;计算与另一个点的距离
        :param other: 另一个点
        &quot;&quot;&quot;
        dx = self.x - other.x
        dy = self.y - other.y
        return (dx * dx + dy * dy) ** 0.5

    def __str__(self):
        return f&apos;({self.x}, {self.y})&apos;


p1 = Point(3, 5)
p2 = Point(6, 9)
print(p1)  # 调用对象的__str__魔法方法
print(p2)
print(p1.distance_to(p2))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;面向对象编程是一种非常流行的编程范式，除此之外还有&lt;strong&gt;指令式编程&lt;/strong&gt;、&lt;strong&gt;函数式编程&lt;/strong&gt;等编程范式。由于现实世界是由对象构成的，而对象是可以接收消息的实体，所以&lt;strong&gt;面向对象编程更符合人类正常的思维习惯&lt;/strong&gt;。类是抽象的，对象是具体的，有了类就能创建对象，有了对象就可以接收消息，这就是面向对象编程的基础。定义类的过程是一个抽象的过程，找到对象公共的属性属于数据抽象，找到对象公共的方法属于行为抽象。抽象的过程是一个仁者见仁智者见智的过程，对同一类对象进行抽象可能会得到不同的结果，如下图所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/python/day01-20/7ca9df2eee.webp&quot; width=&quot;75%&quot;&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 本节课的插图来自于 Grady Booc 等撰写的《面向对象分析与设计》一书，该书是讲解面向对象编程的经典著作，有兴趣的读者可以购买和阅读这本书来了解更多的面向对象的相关知识。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;19 面向对象编程进阶&lt;/h2&gt;
&lt;p&gt;前面我们讲解了 Python 面向对象编程的一些基础知识，本节我们继续讨论面向对象编程相关的内容。&lt;/p&gt;
&lt;h4&gt;可见性和属性装饰器&lt;/h4&gt;
&lt;p&gt;在很多面向对象编程语言中，对象的属性通常会被设置为私有（private）或受保护（protected）的成员，简单的说就是不允许直接访问这些属性；对象的方法通常都是公开的（public），因为公开的方法是对象能够接受的消息，也是对象暴露给外界的调用接口，这就是所谓的访问可见性。在 Python 中，可以通过给对象属性名添加前缀下划线的方式来说明属性的访问可见性，例如，可以用&lt;code&gt;__name&lt;/code&gt;表示一个私有属性，&lt;code&gt;_name&lt;/code&gt;表示一个受保护属性，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student:

    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def study(self, course_name):
        print(f&apos;{self.__name}正在学习{course_name}.&apos;)


stu = Student(&apos;王大锤&apos;, 20)
stu.study(&apos;Python程序设计&apos;)
print(stu.__name)  # AttributeError: &apos;Student&apos; object has no attribute &apos;__name&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码的最后一行会引发&lt;code&gt;AttributeError&lt;/code&gt;（属性错误）异常，异常消息为：&lt;code&gt;&apos;Student&apos; object has no attribute &apos;__name&apos;&lt;/code&gt;。由此可见，以&lt;code&gt;__&lt;/code&gt;开头的属性&lt;code&gt;__name&lt;/code&gt;相当于是私有的，在类的外面无法直接访问，但是类里面的&lt;code&gt;study&lt;/code&gt;方法中可以通过&lt;code&gt;self.__name&lt;/code&gt;访问该属性。需要说明的是，大多数使用 Python 语言的人在定义类时，通常不会选择让对象的属性私有或受保护，正如有一句名言说的：“&lt;strong&gt;We are all consenting adults here&lt;/strong&gt;”（大家都是成年人），成年人可以为自己的行为负责，而不需要通过 Python 语言本身来限制访问可见性。事实上，大多数的程序员都认为&lt;strong&gt;开放比封闭要好&lt;/strong&gt;，把对象的属性私有化并非必不可少的东西，所以 Python 语言并没有从语义上做出最严格的限定，也就是说上面的代码如果你愿意，用&lt;code&gt;stu._Student__name&lt;/code&gt;的方式仍然可以访问到私有属性&lt;code&gt;__name&lt;/code&gt;，有兴趣的读者可以自己试一试。&lt;/p&gt;
&lt;h4&gt;动态属性&lt;/h4&gt;
&lt;p&gt;Python 语言属于动态语言，维基百科对动态语言的解释是：“在运行时可以改变其结构的语言，例如新的函数、对象、甚至代码可以被引进，已有的函数可以被删除或是其他结构上的变化”。动态语言非常灵活，目前流行的 Python 和 JavaScript 都是动态语言，除此之外，诸如 PHP、Ruby 等也都属于动态语言，而 C、C++ 等语言则不属于动态语言。&lt;/p&gt;
&lt;p&gt;在 Python 中，我们可以动态为对象添加属性，这是 Python 作为动态类型语言的一项特权，代码如下所示。需要提醒大家的是，对象的方法其实本质上也是对象的属性，如果给对象发送一个无法接收的消息，引发的异常仍然是&lt;code&gt;AttributeError&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student:

    def __init__(self, name, age):
        self.name = name
        self.age = age


stu = Student(&apos;王大锤&apos;, 20)
stu.sex = &apos;男&apos;  # 给学生对象动态添加sex属性
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不希望在使用对象时动态的为对象添加属性，可以使用 Python 语言中的&lt;code&gt;__slots__&lt;/code&gt;魔法。对于&lt;code&gt;Student&lt;/code&gt;类来说，可以在类中指定&lt;code&gt;__slots__ = (&apos;name&apos;, &apos;age&apos;)&lt;/code&gt;，这样&lt;code&gt;Student&lt;/code&gt;类的对象只能有&lt;code&gt;name&lt;/code&gt;和&lt;code&gt;age&lt;/code&gt;属性，如果想动态添加其他属性将会引发异常，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student:
    __slots__ = (&apos;name&apos;, &apos;age&apos;)

    def __init__(self, name, age):
        self.name = name
        self.age = age


stu = Student(&apos;王大锤&apos;, 20)
# AttributeError: &apos;Student&apos; object has no attribute &apos;sex&apos;
stu.sex = &apos;男&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;静态方法和类方法&lt;/h4&gt;
&lt;p&gt;之前我们在类中定义的方法都是对象方法，换句话说这些方法都是对象可以接收的消息。除了对象方法之外，类中还可以有静态方法和类方法，这两类方法是发给类的消息，二者并没有实质性的区别。在面向对象的世界里，一切皆为对象，我们定义的每一个类其实也是一个对象，而静态方法和类方法就是发送给类对象的消息。那么，什么样的消息会直接发送给类对象呢？&lt;/p&gt;
&lt;p&gt;举一个例子，定义一个三角形类，通过传入三条边的长度来构造三角形，并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法，这一点毫无疑问。但是在创建三角形对象时，传入的三条边长未必能构造出三角形，为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形，这种方法很显然就不是对象方法，因为在调用这个方法时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法，也就是说这类方法不是发送给三角形对象的消息，而是发送给三角形类的消息，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Triangle(object):
    &quot;&quot;&quot;三角形&quot;&quot;&quot;

    def __init__(self, a, b, c):
        &quot;&quot;&quot;初始化方法&quot;&quot;&quot;
        self.a = a
        self.b = b
        self.c = c

    @staticmethod
    def is_valid(a, b, c):
        &quot;&quot;&quot;判断三条边长能否构成三角形(静态方法)&quot;&quot;&quot;
        return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b

    # @classmethod
    # def is_valid(cls, a, b, c):
    #     &quot;&quot;&quot;判断三条边长能否构成三角形(类方法)&quot;&quot;&quot;
    #     return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b

    def perimeter(self):
        &quot;&quot;&quot;计算周长&quot;&quot;&quot;
        return self.a + self.b + self.c

    def area(self):
        &quot;&quot;&quot;计算面积&quot;&quot;&quot;
        p = self.perimeter() / 2
        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5
      

if Triangle.is_valid(3, 4, 5):
    t = Triangle(3, 4, 5)
    print(f&apos;周长: {t.perimeter()}&apos;)
    print(f&apos;面积: {t.area()}&apos;)
else:
    print(&apos;无效的边长!!!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码使用&lt;code&gt;staticmethod&lt;/code&gt;装饰器声明了&lt;code&gt;is_valid&lt;/code&gt;方法是&lt;code&gt;Triangle&lt;/code&gt;类的静态方法，如果要声明类方法，可以使用&lt;code&gt;classmethod&lt;/code&gt;装饰器（如上面的代码15~18行所示）。可以直接使用&lt;code&gt;类名.方法名&lt;/code&gt;的方式来调用静态方法和类方法，二者的区别在于，类方法的第一个参数是类对象本身，而静态方法则没有这个参数。简单的总结一下，&lt;strong&gt;对象方法、类方法、静态方法都可以通过“类名.方法名”的方式来调用，区别在于方法的第一个参数到底是普通对象还是类对象，还是没有接受消息的对象&lt;/strong&gt;。静态方法通常也可以直接写成一个独立的函数，因为它并没有跟特定的对象绑定。&lt;/p&gt;
&lt;p&gt;这里做一个补充说明，我们可以给上面计算三角形周长和面积的方法添加一个&lt;code&gt;property&lt;/code&gt;装饰器（Python 内置类型），这样三角形类的&lt;code&gt;perimeter&lt;/code&gt;和&lt;code&gt;area&lt;/code&gt;就变成了两个属性，不再通过调用方法的方式来访问，而是用对象访问属性的方式直接获得，修改后的代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Triangle(object):
    &quot;&quot;&quot;三角形&quot;&quot;&quot;

    def __init__(self, a, b, c):
        &quot;&quot;&quot;初始化方法&quot;&quot;&quot;
        self.a = a
        self.b = b
        self.c = c

    @staticmethod
    def is_valid(a, b, c):
        &quot;&quot;&quot;判断三条边长能否构成三角形(静态方法)&quot;&quot;&quot;
        return a + b &amp;gt; c and b + c &amp;gt; a and a + c &amp;gt; b

    @property
    def perimeter(self):
        &quot;&quot;&quot;计算周长&quot;&quot;&quot;
        return self.a + self.b + self.c

    @property
    def area(self):
        &quot;&quot;&quot;计算面积&quot;&quot;&quot;
        p = self.perimeter / 2
        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5


if Triangle.is_valid(3, 4, 5):
    t = Triangle(3, 4, 5)
    print(f&apos;周长: {t.perimeter}&apos;)
    print(f&apos;面积: {t.area}&apos;)
else:
    print(&apos;无效的边长!!!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;继承和多态&lt;/h4&gt;
&lt;p&gt;面向对象的编程语言支持在已有类的基础上创建新类，从而减少重复代码的编写。提供继承信息的类叫做父类（超类、基类），得到继承信息的类叫做子类（派生类、衍生类）。例如，我们定义一个学生类和一个老师类，我们会发现他们有大量的重复代码，而这些重复代码都是老师和学生作为人的公共属性和行为，所以在这种情况下，我们应该先定义人类，再通过继承，从人类派生出老师类和学生类，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    &quot;&quot;&quot;人&quot;&quot;&quot;

    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def eat(self):
        print(f&apos;{self.name}正在吃饭.&apos;)
    
    def sleep(self):
        print(f&apos;{self.name}正在睡觉.&apos;)


class Student(Person):
    &quot;&quot;&quot;学生&quot;&quot;&quot;
    
    def __init__(self, name, age):
        super().__init__(name, age)
    
    def study(self, course_name):
        print(f&apos;{self.name}正在学习{course_name}.&apos;)


class Teacher(Person):
    &quot;&quot;&quot;老师&quot;&quot;&quot;

    def __init__(self, name, age, title):
        super().__init__(name, age)
        self.title = title
    
    def teach(self, course_name):
        print(f&apos;{self.name}{self.title}正在讲授{course_name}.&apos;)



stu1 = Student(&apos;白元芳&apos;, 21)
stu2 = Student(&apos;狄仁杰&apos;, 22)
tea1 = Teacher(&apos;武则天&apos;, 35, &apos;副教授&apos;)
stu1.eat()
stu2.sleep()
tea1.eat()
stu1.study(&apos;Python程序设计&apos;)
tea1.teach(&apos;Python程序设计&apos;)
stu2.study(&apos;数据科学导论&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承的语法是在定义类的时候，在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定它的父类是谁，那么默认的父类是&lt;code&gt;object&lt;/code&gt;类。&lt;code&gt;object&lt;/code&gt;类是 Python 中的顶级类，这也就意味着所有的类都是它的子类，要么直接继承它，要么间接继承它。Python 语言允许多重继承，也就是说一个类可以有一个或多个父类，关于多重继承的问题我们在后面会有更为详细的讨论。在子类的初始化方法中，我们可以通过&lt;code&gt;super().__init__()&lt;/code&gt;来调用父类初始化方法，&lt;code&gt;super&lt;/code&gt;函数是 Python 内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出，子类除了可以通过继承得到父类提供的属性和方法外，还可以定义自己特有的属性和方法，所以子类比父类拥有的更多的能力。在实际开发中，我们经常会用子类对象去替换掉一个父类对象，这是面向对象编程中一个常见的行为，也叫做“里氏替换原则”（Liskov Substitution Principle）。&lt;/p&gt;
&lt;p&gt;子类继承父类的方法后，还可以对方法进行重写（重新实现该方法），不同的子类可以对父类的同一个方法给出不同的实现版本，这样的方法在程序运行时就会表现出多态行为（调用相同的方法，做了不同的事情）。多态是面向对象编程中最精髓的部分，当然也是对初学者来说最难以理解和灵活运用的部分，我们会在下一个章节用专门的例子来讲解这个知识点。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Python 是动态类型语言，Python 中的对象可以动态的添加属性，对象的方法其实也是属性，只不过和该属性对应的是一个可以调用的函数。在面向对象的世界中，&lt;strong&gt;一切皆为对象&lt;/strong&gt;，我们定义的类也是对象，所以&lt;strong&gt;类也可以接收消息&lt;/strong&gt;，对应的方法是类方法或静态方法。通过继承，我们&lt;strong&gt;可以从已有的类创建新类&lt;/strong&gt;，实现对已有类代码的复用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;20 面向对象编程应用&lt;/h2&gt;
&lt;p&gt;面向对象编程对初学者来说不难理解但很难应用，虽然我们为大家总结过面向对象的三步走方法（定义类、创建对象、给对象发消息），但是说起来容易做起来难。&lt;strong&gt;大量的编程练习&lt;/strong&gt;和&lt;strong&gt;阅读优质的代码&lt;/strong&gt;可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识，同时也通过这些案例把我们之前学过的 Python 知识都串联起来。&lt;/p&gt;
&lt;h4&gt;例子1：扑克游戏。&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：简单起见，我们的扑克只有52张牌（没有大小王），游戏需要将 52 张牌发到 4 个玩家的手上，每个玩家手上有 13 张牌，按照黑桃、红心、草花、方块的顺序和点数从小到大排列，暂时不实现其他的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用面向对象编程方法，首先需要从问题的需求中找到对象并抽象出对应的类，此外还要找到对象的属性和行为。当然，这件事情并不是特别困难，我们可以从需求的描述中找出名词和动词，名词通常就是对象或者是对象的属性，而动词通常是对象的行为。扑克游戏中至少应该有三类对象，分别是牌、扑克和玩家，牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为 &lt;strong&gt;is-a关系（继承）&lt;/strong&gt;、**has-a关系（关联）**和 &lt;strong&gt;use-a关系（依赖）&lt;/strong&gt;。很显然扑克和牌是 has-a 关系，因为一副扑克有（has-a）52 张牌；玩家和牌之间不仅有关联关系还有依赖关系，因为玩家手上有（has-a）牌而且玩家使用了（use-a）牌。&lt;/p&gt;
&lt;p&gt;牌的属性显而易见，有花色和点数。我们可以用 0 到 3 的四个数字来代表四种不同的花色，但是这样的代码可读性会非常糟糕，因为我们并不知道黑桃、红心、草花、方块跟 0 到 3 的数字的对应关系。如果一个变量的取值只有有限多个选项，我们可以使用枚举。与 C、Java 等语言不同的是，Python 中没有声明枚举类型的关键字，但是可以通过继承&lt;code&gt;enum&lt;/code&gt;模块的&lt;code&gt;Enum&lt;/code&gt;类来创建枚举类型，代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from enum import Enum


class Suite(Enum):
    &quot;&quot;&quot;花色(枚举)&quot;&quot;&quot;
    SPADE, HEART, CLUB, DIAMOND = range(4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的代码可以看出，定义枚举类型其实就是定义符号常量，如&lt;code&gt;SPADE&lt;/code&gt;、&lt;code&gt;HEART&lt;/code&gt;等。每个符号常量都有与之对应的值，这样表示黑桃就可以不用数字 0，而是用&lt;code&gt;Suite.SPADE&lt;/code&gt;；同理，表示方块可以不用数字 3， 而是用&lt;code&gt;Suite.DIAMOND&lt;/code&gt;。注意，使用符号常量肯定是优于使用字面常量的，因为能够读懂英文就能理解符号常量的含义，代码的可读性会提升很多。Python 中的枚举类型是可迭代类型，简单的说就是可以将枚举类型放到&lt;code&gt;for-in&lt;/code&gt;循环中，依次取出每一个符号常量及其对应的值，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for suite in Suite:
    print(f&apos;{suite}: {suite.value}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们可以定义牌类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Card:
    &quot;&quot;&quot;牌&quot;&quot;&quot;

    def __init__(self, suite, face):
        self.suite = suite
        self.face = face

    def __repr__(self):
        suites = &apos;♠♥♣♦&apos;
        faces = [&apos;&apos;, &apos;A&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;, &apos;10&apos;, &apos;J&apos;, &apos;Q&apos;, &apos;K&apos;]
        return f&apos;{suites[self.suite.value]}{faces[self.face]}&apos;  # 返回牌的花色和点数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过下面的代码来测试下&lt;code&gt;Card&lt;/code&gt;类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1)  # ♠5 
print(card2)  # ♥K
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们定义扑克类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random


class Poker:
    &quot;&quot;&quot;扑克&quot;&quot;&quot;

    def __init__(self):
        self.cards = [Card(suite, face) 
                      for suite in Suite
                      for face in range(1, 14)]  # 52张牌构成的列表
        self.current = 0  # 记录发牌位置的属性

    def shuffle(self):
        &quot;&quot;&quot;洗牌&quot;&quot;&quot;
        self.current = 0
        random.shuffle(self.cards)  # 通过random模块的shuffle函数实现随机乱序

    def deal(self):
        &quot;&quot;&quot;发牌&quot;&quot;&quot;
        card = self.cards[self.current]
        self.current += 1
        return card

    @property
    def has_next(self):
        &quot;&quot;&quot;还有没有牌可以发&quot;&quot;&quot;
        return self.current &amp;lt; len(self.cards)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过下面的代码来测试下&lt;code&gt;Poker&lt;/code&gt;类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;poker = Poker()
print(poker.cards)  # 洗牌前的牌
poker.shuffle()
print(poker.cards)  # 洗牌后的牌
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义玩家类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Player:
    &quot;&quot;&quot;玩家&quot;&quot;&quot;

    def __init__(self, name):
        self.name = name
        self.cards = []  # 玩家手上的牌

    def get_one(self, card):
        &quot;&quot;&quot;摸牌&quot;&quot;&quot;
        self.cards.append(card)

    def arrange(self):
        &quot;&quot;&quot;整理手上的牌&quot;&quot;&quot;
        self.cards.sort()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建四个玩家并将牌发到玩家的手上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;poker = Poker()
poker.shuffle()
players = [Player(&apos;东邪&apos;), Player(&apos;西毒&apos;), Player(&apos;南帝&apos;), Player(&apos;北丐&apos;)]
# 将牌轮流发到每个玩家手上每人13张牌
for _ in range(13):
    for player in players:
        player.get_one(poker.deal())
# 玩家整理手上的牌输出名字和手牌
for player in players:
    player.arrange()
    print(f&apos;{player.name}: &apos;, end=&apos;&apos;)
    print(player.cards)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行上面的代码会在&lt;code&gt;player.arrange()&lt;/code&gt;那里出现异常，因为&lt;code&gt;Player&lt;/code&gt;的&lt;code&gt;arrange&lt;/code&gt;方法使用了列表的&lt;code&gt;sort&lt;/code&gt;对玩家手上的牌进行排序，排序需要比较两个&lt;code&gt;Card&lt;/code&gt;对象的大小，而&lt;code&gt;&amp;lt;&lt;/code&gt;运算符又不能直接作用于&lt;code&gt;Card&lt;/code&gt;类型，所以就出现了&lt;code&gt;TypeError&lt;/code&gt;异常，异常消息为：&lt;code&gt;&apos;&amp;lt;&apos; not supported between instances of &apos;Card&apos; and &apos;Card&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们可以对&lt;code&gt;Card&lt;/code&gt;类的代码稍作修改，使得两个&lt;code&gt;Card&lt;/code&gt;对象可以直接用&lt;code&gt;&amp;lt;&lt;/code&gt;进行大小的比较。这里用到技术叫&lt;strong&gt;运算符重载&lt;/strong&gt;，Python 中要实现对&lt;code&gt;&amp;lt;&lt;/code&gt;运算符的重载，需要在类中添加一个名为&lt;code&gt;__lt__&lt;/code&gt;的魔术方法。很显然，魔术方法&lt;code&gt;__lt__&lt;/code&gt;中的&lt;code&gt;lt&lt;/code&gt;是英文单词“less than”的缩写，以此类推，魔术方法&lt;code&gt;__gt__&lt;/code&gt;对应&lt;code&gt;&amp;gt;&lt;/code&gt;运算符，魔术方法&lt;code&gt;__le__&lt;/code&gt;对应&lt;code&gt;&amp;lt;=&lt;/code&gt;运算符，&lt;code&gt;__ge__&lt;/code&gt;对应&lt;code&gt;&amp;gt;=&lt;/code&gt;运算符，&lt;code&gt;__eq__&lt;/code&gt;对应&lt;code&gt;==&lt;/code&gt;运算符，&lt;code&gt;__ne__&lt;/code&gt;对应&lt;code&gt;!=&lt;/code&gt;运算符。&lt;/p&gt;
&lt;p&gt;修改后的&lt;code&gt;Card&lt;/code&gt;类代码如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Card:
    &quot;&quot;&quot;牌&quot;&quot;&quot;

    def __init__(self, suite, face):
        self.suite = suite
        self.face = face

    def __repr__(self):
        suites = &apos;♠♥♣♦&apos;
        faces = [&apos;&apos;, &apos;A&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;, &apos;10&apos;, &apos;J&apos;, &apos;Q&apos;, &apos;K&apos;]
        return f&apos;{suites[self.suite.value]}{faces[self.face]}&apos;
    
    def __lt__(self, other):
        if self.suite == other.suite:
            return self.face &amp;lt; other.face   # 花色相同比较点数的大小
        return self.suite.value &amp;lt; other.suite.value   # 花色不同比较花色对应的值
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 大家可以尝试在上面代码的基础上写一个简单的扑克游戏，如 21 点游戏（Black Jack），游戏的规则可以自己在网上找一找。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;例子2：工资结算系统。&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要求&lt;/strong&gt;：某公司有三种类型的员工，分别是部门经理、程序员和销售员。需要设计一个工资结算系统，根据提供的员工信息来计算员工的月薪。其中，部门经理的月薪是固定 15000 元；程序员按工作时间（以小时为单位）支付月薪，每小时 200 元；销售员的月薪由 1800 元底薪加上销售额 5% 的提成两部分构成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过对上述需求的分析，可以看出部门经理、程序员、销售员都是员工，有相同的属性和行为，那么我们可以先设计一个名为&lt;code&gt;Employee&lt;/code&gt;的父类，再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然，后续的代码不会创建&lt;code&gt;Employee&lt;/code&gt; 类的对象，因为我们需要的是具体的员工对象，所以这个类可以设计成专门用于继承的抽象类。Python 语言中没有定义抽象类的关键字，但是可以通过&lt;code&gt;abc&lt;/code&gt;模块中名为&lt;code&gt;ABCMeta&lt;/code&gt; 的元类来定义抽象类。关于元类的概念此处不展开讲解，当然大家不用纠结，照做即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABCMeta, abstractmethod


class Employee(metaclass=ABCMeta):
    &quot;&quot;&quot;员工&quot;&quot;&quot;

    def __init__(self, name):
        self.name = name

    @abstractmethod
    def get_salary(self):
        &quot;&quot;&quot;结算月薪&quot;&quot;&quot;
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的员工类中，有一个名为&lt;code&gt;get_salary&lt;/code&gt;的方法用于结算月薪，但是由于还没有确定是哪一类员工，所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法，我们可以使用&lt;code&gt;abstractmethod&lt;/code&gt;装饰器将其声明为抽象方法，所谓&lt;strong&gt;抽象方法就是只有声明没有实现的方法&lt;/strong&gt;，&lt;strong&gt;声明这个方法是为了让子类去重写这个方法&lt;/strong&gt;。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Manager(Employee):
    &quot;&quot;&quot;部门经理&quot;&quot;&quot;

    def get_salary(self):
        return 15000.0


class Programmer(Employee):
    &quot;&quot;&quot;程序员&quot;&quot;&quot;

    def __init__(self, name, working_hour=0):
        super().__init__(name)
        self.working_hour = working_hour

    def get_salary(self):
        return 200 * self.working_hour


class Salesman(Employee):
    &quot;&quot;&quot;销售员&quot;&quot;&quot;

    def __init__(self, name, sales=0):
        super().__init__(name)
        self.sales = sales

    def get_salary(self):
        return 1800 + self.sales * 0.05
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的&lt;code&gt;Manager&lt;/code&gt;、&lt;code&gt;Programmer&lt;/code&gt;、&lt;code&gt;Salesman&lt;/code&gt;三个类都继承自&lt;code&gt;Employee&lt;/code&gt;，三个类都分别重写了&lt;code&gt;get_salary&lt;/code&gt;方法。&lt;strong&gt;重写就是子类对父类已有的方法重新做出实现&lt;/strong&gt;。相信大家已经注意到了，三个子类中的&lt;code&gt;get_salary&lt;/code&gt;各不相同，所以这个方法在程序运行时会产生&lt;strong&gt;多态行为&lt;/strong&gt;，多态简单的说就是&lt;strong&gt;调用相同的方法&lt;/strong&gt;，&lt;strong&gt;不同的子类对象做不同的事情&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们通过下面的代码来完成这个工资结算系统，由于程序员和销售员需要分别录入本月的工作时间和销售额，所以在下面的代码中我们使用了 Python 内置的&lt;code&gt;isinstance&lt;/code&gt;函数来判断员工对象的类型。我们之前讲过的&lt;code&gt;type&lt;/code&gt;函数也能识别对象的类型，但是&lt;code&gt;isinstance&lt;/code&gt;函数更加强大，因为它可以判断出一个对象是不是某个继承结构下的子类型，你可以简单的理解为&lt;code&gt;type&lt;/code&gt;函数是对对象类型的精准匹配，而&lt;code&gt;isinstance&lt;/code&gt;函数是对对象类型的模糊匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emps = [Manager(&apos;刘备&apos;), Programmer(&apos;诸葛亮&apos;), Manager(&apos;曹操&apos;), Programmer(&apos;荀彧&apos;), Salesman(&apos;张辽&apos;)]
for emp in emps:
    if isinstance(emp, Programmer):
        emp.working_hour = int(input(f&apos;请输入{emp.name}本月工作时间: &apos;))
    elif isinstance(emp, Salesman):
        emp.sales = float(input(f&apos;请输入{emp.name}本月销售额: &apos;))
    print(f&apos;{emp.name}本月工资为: ￥{emp.get_salary():.2f}元&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;面向对象编程思想非常的好，也符合人类的正常思维习惯，但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀，这件事情无法一蹴而就，因为知识的积累本就是涓滴成河的过程。&lt;/p&gt;
</content:encoded></item></channel></rss>