小白python入门 - 37. 网页抓取与解析

📅 2026/7/26 6:20:05
小白python入门 - 37. 网页抓取与解析
1. 本课定位是什么、为何重要 前几课我们一直在和「程序可读的接口」打交道HTTP 五件套、requests、并发与协程。可是世界上仍有大量信息以网页 HTML 的形式公开——文档站、公告列表、课程目录。若数据就在页面源码里你还不会「把标签变成表格」就只能手工复制。 这一课定位静态页抓取下载 HTML、按结构抽出标题和链接、清洗后写入 SQLite。它和第 34 课调 JSON 是两条腿字段在 dict 里用键取字段在标签里用选择器取。学完你能为「页面上有、接口没有」的简单站点做入库并为下一课「页面上看得到但源码是空壳」埋下对比。 前面会调 API、会并发。很多资料仍以 **网页 HTML** 形式公开。 **静态页抓取** 下载 HTML 文本 → 按标签抽出字段 → 清洗 → 入库接 SQLite。 | 概念 | 一句话 | |------|--------| | **静态页** | 服务器直接返回较完整的 HTML主要信息不依赖浏览器再跑复杂 JS | | **解析** | 从 HTML 字符串里定位节点取出标题、链接等 | | **BeautifulSoup** | 常用解析库支持 CSS 选择器 | | **选择器** | 描述「树里走哪条路径」的短语法 | **为何重要** 文档站、列表页、公告页仍常见也是理解「页面结构」的基础。动态站点优先走接口第 38 课。 **对比已学** | Day33/34 调 JSON API | 本课解析 HTML | |----------------------|---------------| | 字段在 dict 键里 | 字段在标签文本/属性里 | | 结构相对稳定 | 改版选择器易碎 | | 直接 r.json() | BeautifulSoup select | | 参数绑定防注入 | 入库时同样要参数绑定 | bash pip install beautifulsoup4 lxml --- ### 2. HTML 像一棵树 要解析页面先换一种看法别把 HTML 当成乱七八糟的长字符串而当成一棵嵌套的树——大盒子套小盒子。浏览器渲染靠这棵树解析库找数据也靠这棵树。 这一节用一小段列表 HTML对应标签、属性、文本、父子关系并给出常用 CSS 选择器直觉。只有树的画面立住了后面的 select 才不是魔法咒语。 浏览器和解析库都把 HTML 看成**嵌套的盒子节点** html body h1Course Notes/h1 ul classposts li a classtitle href/p/1HTTP 入门/a /li /ul /body | 说法 | 对应 | |------|------| | 标签 | a、ul、li | | 属性 | classtitle、href/p/1 | | 文本 | HTTP 入门 | | 父子 | ul 包含 lili 包含 a | **选择器直觉** | 选择器 | 含义 | |--------|------| | ul.posts | class 为 posts 的 ul | | a.title | class 为 title 的 a | | ul.posts a.title | posts 列表里的标题链接 | | #main | id 为 main 的节点示例 | --- ### 3. 本质、约束与坑 解析的本质就是在树上按路径找到节点把文本和属性变成 Python 里的字符串和元组。它默认只读内存中的 HTML不会偷偷改你磁盘文件。 但抓取不是纯技术题还有合规、频率、编码、相对链接、以及「页面其实是空壳」等坑。这一节把约束和坑表摊开避免你一上来就对公网站点狂轰滥炸或在 JS 渲染站上死磕选择器。 **本质** 解析 在树上按路径找节点把文本/属性变成 Python 数据。 **解析前** 一整段字符串。 **解析后** 列表/元组/字典**默认不改磁盘上的 HTML**除非你自己写回。 **约束与合规必读** | 要点 | 说明 | |------|------| | robots.txt / 服务条款 | 是否允许抓 | | 频率 | 不要轰炸加 timeout 与间隔 | | 数据用途 | 隐私与版权 | | User-Agent | 部分站点会拒绝空白 UA | | 教学 | 优先 **本地 HTML** 或明确允许的源 | **常见坑** | 坑 | 后果 | 做法 | |----|------|------| | 页面靠 JS 填充 | HTML 是空壳select 为空 | 第 38 课找 XHR/API | | 选择器写死易变 class | 改版全挂 | 选更稳定结构写测试 | | 相对链接未补全 | 存了 /p/1 拼不出绝对 URL | urllib.parse.urljoin | | 编码不对 | 中文乱码 | 看 charsetr.encoding | | 把整页当正则硬拆 | 极脆、难维护 | 用解析器 | | 入库拼接 SQL | 注入风险 | ? 占位 | --- ### 4. 解析能力分组 和学 requests 时一样我们把 BeautifulSoup 的用法按能力分组如何加载、如何选多个/一个节点、如何取文本和属性、如何做点清洗。 下面用最小例子带预期输出让你从「一个链接」练到「多个链接」并知道选不中时返回 None。先会这些综合脚本里的循环才读得懂。 | 类别 | 常用 | 作用 | |------|------|------| | 加载 | BeautifulSoup(html, lxml) | 建树 | | 查找多节点 | select(...) | 返回列表 | | 查找单节点 | select_one(...) | 返回一个或 None | | 旧式查找 | find / find_all | 也能用 | | 取值 | get_text、get(href) | 文本与属性 | | 清洗 | strip、过滤空 | 落地前整理 | #### 4.1 最小解析 python from bs4 import BeautifulSoup html ul classpostslia classtitle href/p/1HTTP 入门/a/li/ul soup BeautifulSoup(html, lxml) a soup.select_one(ul.posts a.title) print(a.get_text(stripTrue)) print(a.get(href)) **预期输出** text HTTP 入门 /p/1 #### 4.2 多个节点 python from bs4 import BeautifulSoup html ul classposts lia classtitle href/p/1HTTP 入门/a/li lia classtitle href/p/2requests 实战/a/li /ul soup BeautifulSoup(html, lxml) for a in soup.select(ul.posts a.title): print(a.get_text(stripTrue), a.get(href)) **预期输出** text HTTP 入门 /p/1 requests 实战 /p/2 #### 4.3 找不到时 python a soup.select_one(a.not-exist) print(a) # None 生产代码要判断 if a is None避免空指针。 #### 4.4 相对链接补全了解 python from urllib.parse import urljoin base https://example.com/docs/ print(urljoin(base, /p/1)) print(urljoin(base, p/1)) **预期形态** 得到可访问的绝对 URL 字符串。 --- ### 5. 从「字符串」到「表」清洗清单 选中节点只是第一步。真实页面常有多余空白、空标题、相对路径直接入库会很难看也很难用。 这一节给出清洗清单去空白、过滤空项、补全链接、去重、类型处理。清洗前是脏列表清洗后才适合 INSERT——这也是和第 39 课流水线「parse」阶段的同一思维。 | 步骤 | 做什么 | 例子 | |------|--------|------| | 去空白 | strip() | 标题 → 标题 | | 去空项 | 过滤空标题 | 广告空节点 | | 统一链接 | urljoin | 相对 → 绝对 | | 去重 | set / SQL UNIQUE | 同一链接抓两次 | | 类型 | 日期字符串再解析 | 进阶 | **清洗前** 标题可能含换行、链接可能是相对路径。 **清洗后** 字段干净适合 INSERT。 --- ### 6. 落地场景 技术要落到场景才记得住课程笔记列表、公告栏、文档目录、简易导航站分别抽什么、存哪。 本课实践用本地生成的 sample.html是为了稳定可复现真实站点则用 requests 取 r.text 再交给 Soup并遵守合规。场景表帮你想象「毕业后可能用在哪」。 | 场景 | 抽取什么 | 存哪 | |------|----------|------| | 课程笔记列表 | 标题 链接 | articles | | 公告栏 | 标题 日期 | 业务表 | | 文档目录 | 章节名 anchor | 本地索引 | | 简易导航站 | 站名 href | 书签库 | 本课用**本地生成 sample.html**不依赖外站改版。 真实站点时 python import requests from bs4 import BeautifulSoup r requests.get(url, timeout20, headers{User-Agent: python-lab-day37/1.0}) r.raise_for_status() soup BeautifulSoup(r.text, lxml) 并遵守合规与频率。 --- ### 7. 与数据库衔接回顾 解析结果若只 print关掉程序就没了。数据持久化阶段你已学过参数化入库——这里把 executemany 接回来。 对照错误与正确写法f-string 拼 SQL 危险标题里的引号都可能炸语句参数绑定更安全。解析失败时也不要强行 insert 空数据。 python conn.executemany( INSERT INTO articles (title, url) VALUES (?, ?), items, # [(title, url), ...] ) | 错误 | 正确 | |------|------| | f-string 拼 SQL | ? 占位 | | 标题含 导致语句炸 | 参数绑定自动处理 | | 解析失败仍强行 insert | 先校验 items 非空 | --- ### 8. 综合实践 一键生成 sample、解析、建表、写入、查询走通「字符串 → 列表 → SQLite」全链路。 请真实运行并看 COUNT 是否为 3。这是本课的验收证据也是后面流水线项目里「解析 存储」的迷你版。 bash mkdir -p ~/python-lab/src/day37 cd ~/python-lab/src/day37 pip install beautifulsoup4 lxml Windows 推荐 Cygwin 或 WSL。 bash cat parse_demo.py EOF # Day37: static HTML parse sqlite import sqlite3 from pathlib import Path from bs4 import BeautifulSoup DIR Path(__file__).resolve().parent HTML DIR / sample.html DB DIR / articles.db SAMPLE !DOCTYPE html html headmeta charsetutf-8titleDemo Board/title/head body h1Course Notes/h1 ul classposts lia classtitle href/p/1HTTP 入门/a/li lia classtitle href/p/2requests 实战/a/li lia classtitle href/p/3asyncio 笔记/a/li /ul /body /html def main(): HTML.write_text(SAMPLE, encodingutf-8) html HTML.read_text(encodingutf-8) soup BeautifulSoup(html, lxml) items [] for a in soup.select(ul.posts a.title): title a.get_text(stripTrue) href a.get(href, ) items.append((title, href)) print(--- parsed ---) for row in items: print(row) if DB.exists(): DB.unlink() with sqlite3.connect(DB) as conn: conn.execute( CREATE TABLE articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, url TEXT NOT NULL ) ) conn.executemany( INSERT INTO articles (title, url) VALUES (?, ?), items, ) conn.commit() print(--- db count ---) print(conn.execute(SELECT COUNT(*) FROM articles).fetchone()[0]) print(--- db rows ---) for row in conn.execute(SELECT id, title, url FROM articles ORDER BY id): print(row) if __name__ __main__: main() EOF python3 parse_demo.py **实测输出** text --- parsed --- (HTTP 入门, /p/1) (requests 实战, /p/2) (asyncio 笔记, /p/3) --- db count --- 3 --- db rows --- (1, HTTP 入门, /p/1) (2, requests 实战, /p/2) (3, asyncio 笔记, /p/3) **流水线直觉** text HTML 字符串 → select 列表 → [(title,url)...] → SQLite articles --- ### 9. 解析器选择了解 BeautifulSoup 后面可以接不同解析器lxml 快、html.parser 免安装、html5lib 更宽容但慢。 本课默认 lxml若环境装不上知道能改 html.parser 即可不必纠结细节。 | parser | 说明 | |--------|------| | lxml | 快、常用需安装 lxml | | html.parser | 标准库自带免额外依赖 | | html5lib | 更宽容更慢 | 本课用 lxml。若环境装不上可改 html.parser。 --- ### 10. 常见问答 为什么不用正则硬扒 HTMLselect 和 find 怎么选数据能商用吗Vue/React 页面怎么办 集中回答这些问题避免你走「万能正则」弯路也把「空壳页去第 38 课」的路径再次强调。 **Q为什么不用正则扒 HTML** A嵌套、属性顺序、换行都会让正则脆弱解析器更合适。 **Qselect 和 find_all 哪个好** A都会用即可CSS 选择器对「前端熟悉的同学」更直观。 **Q抓到的数据能直接商用吗** A不一定。看版权、协议与法律本课只教学。 **Q页面是 Vue/React 的怎么办** A先看 Network 有没有 JSON不要只死磕首屏 HTML第 38 课。 --- ### 11. 调试技巧 选择器写不对时最崩溃。这一节给可操作的调试步骤存文件打开看结构、控制选中数量、先打印前几条。 调试能力比背更多 API 更重要——真实页面一改版你要靠这些步骤快速定位。 1. 先把 HTML 存成文件用浏览器打开看结构。 2. 在开发者工具里对节点「Copy selector」作参考再手工简化。 3. 打印 len(soup.select(...))先确认选中了几个。 4. 只对前 3 条 print避免刷屏。 python nodes soup.select(ul.posts a.title) print(count, len(nodes)) for a in nodes[:3]: print(a.get_text(stripTrue)) --- ### 12. 错误示例 vs 正确示例 用脆弱正则和解析器两种写法对照让你看见「为什么课堂坚持用 Soup」。 属性顺序一变正则就挂而 select 走的是结构关系。把正确气味记下来。 **脆弱正则不推荐** python import re # 属性顺序一变就挂 re.findall(ra classtitle href([^])([^])/a, html) **解析器推荐** python for a in soup.select(a.title): href, title a.get(href), a.get_text(stripTrue) --- ### 13. 选择器再练三组 再给一小段 HTML 和几组选择器训练「改变 class 后选中集合如何变」。 自己改一改再 print len(select(...))比只看表格印象深。 html div idbox p classitemA/p p classitem highlightB/p span classitemC/span /div | 选择器 | 大致选中 | |--------|----------| | #box .item | A、B、C | | p.item | A、B | | p.highlight | B | | span.item | C | 自己在本地 HTML 里改 class观察 select 数量变化——这是调试基本功。 --- ### 14. 入库后查询衔接 SQL 数据进库后就可以用 SQL 过滤例如标题模糊匹配。这展示了「为何不满足于 Python 列表」。 和数据持久化课程衔接解析是取数SQL 是用数。 python for row in conn.execute( SELECT title FROM articles WHERE title LIKE ?, (%HTTP%,), ): print(row[0]) **修改前** 数据只在 Python 列表。 **修改后** 可用 SQL 过滤、排序、分页——这也是「解析完要入库」的理由之一。 --- ### 15. 端到端伪流程真实站点时 把合规、请求、解析、入库、自检排成 17 步清单作为真实站点时的作战流程。 任一步失败都不要假装成功写脏数据——这和 Day34 的 raise_for_status 精神一致。 text 1. 确认合规 2. requests.get(list_url, timeout..., headersUA) 3. raise_for_status 4. BeautifulSoup(r.text, lxml) 5. select → 清洗 → 列表 6. executemany 入库 7. SELECT COUNT 自检 任一步失败都不要假装成功写脏数据。 --- ### 16. 自我检查清单 打勾确认树结构、select/取值、入库、空壳页意识、合规与参数绑定。 过关后再进第 38 课对比「有接口时为什么不该死磕 HTML」。 - [ ] 能画 HTML 父子关系 - [ ] 会 select / get_text / get(href) - [ ] 会把结果写入 SQLite - [ ] 知道空壳页该去找 API - [ ] 知道合规与参数绑定 --- ## 总结 静态抓取四步下载、解析、清洗、入库选择器定位本地样例优先空壳页改找接口SQL 要参数绑定。 下一课专门处理「页面看得见、源码没有」的动态内容主线是接口优先。 - 静态抓取 下载 解析 清洗 入库。 - CSS 选择器定位教学优先本地样例。 - 空壳页 / JS 渲染 → 宜优先找 JSON 接口。 - 入库坚持参数绑定合规与频率不能省。 --- ## 小练笔 自测题含概念与可选改 SAMPLE 的实践。先做后对答案。 可选实践请真改 HTML 重跑确认 COUNT 变化。 ### 题 1 a.get_text(stripTrue) 取的是什么 ### 题 2 判断任何网站都可以无限制高频爬取。 ### 题 3 本课数据进了哪张表 ### 题 4 HTML 是空壳、数据在 XHR 里时本课方法常见失败点是什么 ### 题 5 判断BeautifulSoup 解析一定会修改磁盘上的 HTML 文件。 ### 题 6 选择器 ul.posts a.title 在选什么 ### 题 7 相对链接 /p/1 入库前通常要做什么 ### 题 8 判断用 f-string 把标题拼进 SQL 是推荐做法。 ### 题 9可选实践 在 SAMPLE 里再加一条链接重跑后确认 COUNT(*)4。 ### 题 10 select 与 select_one 的差别 --- ## 小练笔参考答案 先独立完成。简答意思对即可。 与「空壳页/合规/参数绑定」冲突的理解需要修正。 ### 题 1 链接可见文字标题。 ### 题 2 **错** ### 题 3 articles ### 题 4 下载到的 HTML 没有目标节点选择器结果为空。 ### 题 5 **错** ### 题 6 ul.posts 下的 a.title 节点。 ### 题 7 用 urljoin 等补成绝对 URL若需要可访问链接。 ### 题 8 **错**应用参数绑定 ### 题 9 以你改完后的输出为准。 ### 题 10 select 返回列表select_one 返回单个节点或 None。