经常有初学者跑来问我拿到一个网页源码接下来该怎么办requests 明明已经把 HTML 全抓回来了可看着满屏的标签和属性就是不知道怎么把想要的书名、价格、链接一个个摘出来。这其实就是爬虫路上的第一道真正的坎——解析。而 BeautifulSoup就是帮你把这团乱麻理顺的那把梳子。这个系列之前几章咱们把 HTTP 请求、requests 库的基本用法都过了一遍算是拿到了“下载网页”的能力。但下载只是手段拿到结构化字段才是目的。第四章“解析与清洗”要解决的就是“从原始 HTML 里精准提取信息”这件事。这一节先从最基础、也最好上手的 BeautifulSoup 讲起目标很明确让你拿到任意一个静态 HTML 页面能写出代码把里面指定的标题、链接、表格字段提取出来存成干净的 Python 数据结构。这篇内容适合刚学完 requests、还没接触过任何解析库的零基础读者也适合那些用过正则表达式提取但被各种转义和边界条件折磨到崩溃的“半新手”。1. 内容整体设计与思路拆解1.1 为什么爬虫非得有个“解析”步骤第一次写爬虫的人会有个错觉网页就是给我看的文字在哪我直接找不就行了吗可一旦你打开开发者工具看到的是满屏的div、span、classprice、idproduct-name这种东西会发现人眼“看”和程序“读”之间隔着一道墙。HTML 本质是“带标记的纯文本”程序没法天生知道h1里的内容比footer里的内容更重要这需要一层解析逻辑来告诉它哪个标签承载着我要的数据。用正则表达式确实也能提取而且很多老手早期都是正则党。但正则的问题是HTML 不是正则友好的文本格式标签可以嵌套属性顺序可以变化引号可单可双同一份数据在不同页面里长得还不一样。你用正则匹配标题可能一个页面能匹配上换个页面就漏了因为中间多了个空格或注释。更别提 HTML 本身的结构化信息——父子节点、兄弟节点、属性层级——正则一概视而不见它只认“字符串模式”。所以这个系列走到解析这一步我推荐的路径是结构化解析优先正则作为兜底和辅助。BeautifulSoup 恰好就是把 HTML 字符串转成“可查询的树”这个环节里最经典的工具。它不要求你懂多少前端知识也不依赖浏览器环境装个库、传一段 HTML 进去它就能帮你把标签树建好。剩下的工作就是在这棵树上做“查找”而已。1.2 BeautifulSoup 在整个爬虫流程里的位置画一条完整的数据链路URL 列表 → requests 请求 → 拿到 HTML → BeautifulSoup 解析 → 提取字段 → 清洗数据 → 存储CSV / Excel / 数据库。你可以看到解析处在“获取”和“存储”的中间地带承上启下。前面的 requests 如果只给你一个response.text那解析环节就该把它变成结构清晰的soup对象后面的存储环节需要的是一行行干净的记录解析环节就得保证提取出来的字段没有多余的空白、没有乱码、没有一堆None。BeautifulSoup 在这条链路里负责两件事第一把 HTML 文本解析为可操作的标签树第二提供便捷的查找方法让你用选择器、属性、文本内容等方式定位目标节点。它不像浏览器渲染引擎那样把页面“画”出来也不需要执行 JavaScript所以对静态页面来说又轻又快。它的定位更像是“手术刀”——直接在 DOM 树结构上做精准切割。2. 核心细节解析与实操要点2.1 环境准备与安装这关别翻车先说环境。BeautifulSoup 在 PyPI 上的包名叫beautifulsoup4注意是 4不是 3。早期版本遗留代码里偶尔能看到BeautifulSoup3 的写法新项目一律不要碰。安装命令超简单pip install beautifulsoup4但实际解析 HTML 时BeautifulSoup 只是个外壳它真正干活要靠底层解析器。默认情况下如果你没安装lxml或html5lib它会用 Python 标准库里的html.parser。这个默认值能跑但有几个毛病容错能力一般、速度一般、对畸形 HTML 的处理不够智能。所以我的建议是装完beautifulsoup4之后顺手把lxml也装上pip install lxml然后在创建 BeautifulSoup 对象时显式指定解析器from bs4 import BeautifulSoup soup BeautifulSoup(html_text, lxml)有人会问lxml和html.parser到底差多少说个我实际测试过的结果面对一份包含几百个标签、且有不少属性缺失的页面html.parser解析耗时大约 0.8 秒lxml大概 0.3 秒而且lxml对嵌套错误的标签修正更接近浏览器行为。爬虫一旦跑批量任务几千个页面下来这差距就很明显了。另一个选项html5lib是容错最强、也最慢的除非你面对的页面烂到极致否则没必要选它。安装后最好做个快速验证确保环境没问题from bs4 import BeautifulSoup html p classintroHello, BeautifulSoup/p soup BeautifulSoup(html, lxml) print(soup.p.text)能输出Hello, BeautifulSoup说明环境 OK可以继续往下走。2.2 BeautifulSoup 的四种对象模型新手容易忽略一个概念BeautifulSoup 解析完 HTML 之后生成的soup对象是由四种基本对象组成的树。搞懂这四种对象后面所有查找、遍历操作都会清晰很多。Tag对应 HTML 里的一个标签节点比如div classcontent.../div。它是我们最常操作的对象可以访问.name、.attrs、.text、.string。NavigableString标签内部的文本内容比如p你好/p里的“你好”。这个对象支持字符串操作可以直接用str()转成普通字符串。BeautifulSoup文档根对象代表整个解析后的文档。通常我们用soup这个名字实例化它它本身也可以看作一个特殊的 Tag。CommentHTML 注释内容在遍历时会被识别为特殊类型的 NavigableString避免把注释文本当正文提取出来。实际写代码时90% 的时间你都在操作Tag对象。但有一个坑值得提前说Tag.string只在标签内只有一个直接子文本时才有值如果标签内部还有子标签比如div价格 span100/span/div.string返回的是None这时候得用.text或.get_text()才能拿到完整文本。很多刚上手的人在这里栽跟头以为提取逻辑写错了其实是 API 语义没吃透。2.3 查找方法的家族图谱find 与 find_all 够用BeautifulSoup 提供了两套 API一套是直接通过标签名和属性访问的快捷方式比如soup.p、soup.title另一套是功能完整的查找方法find()和find_all()。快捷方式只适合快速看一眼真正写爬虫我建议你忘掉soup.p这种写法老老实实用find系列。核心方法的调用格式就下面这几种# 按标签名查找 soup.find(h1) soup.find_all(a) # 按属性查找 soup.find_all(div, class_price) soup.find_all(a, idnav-link) soup.find_all(img, attrs{data-src: xxx})注意两个细节第一因为class是 Python 关键字所以用 BeautifulSoup 时得写成class_带下划线第二attrs参数可以传字典适合那些属性名本身带 Python 关键字的场景比如>section soup.find(section, idgoods-list) price section.find(span, class_price)还有一种情况很常见页面里有一堆相同的标签但你只想要第一个。find()返回单个对象find_all()返回列表如果你确认目标只有一个或只想取第一个直接用find()不用写find_all()[0]。3. 实操过程与核心环节实现3.1 一个真实可跑的案例抓取静态列表页的图书信息为了让你看完就能照做我这里用一个典型的静态列表页场景来演示一个图书展示页面里面每本书有书名、作者、价格和详情页链接。真实世界中这种页面多得是你可以随手找一个不反爬的静态站点来练手也可以本地用 HTML 文件来模拟。这里给出可直接运行的完整示例。先准备一段 HTML 样例你可以直接保存成demo.html!DOCTYPE html html langzh-cn head meta charsetutf-8 title热门技术书单/title /head body div classbook-list div classbook-item h2 classtitlePython编程从入门到实践第3版/h2 span classauthorEric Matthes/span span classprice89.80/span a href/book/python-crash-course查看详情/a /div div classbook-item h2 classtitle流畅的Python第2版/h2 span classauthorLuciano Ramalho/span span classprice134.30/span a href/book/fluent-python查看详情/a /div div classbook-item h2 classtitle爬虫开发实战/h2 span classauthorTruman/span span classprice62.90/span a href/book/spider-dev查看详情/a /div /div /body /html接下来写解析代码。整个流程分四步读文件或请求网页、创建 soup 对象、定位所有book-item节点、在每个节点里提取子字段。from bs4 import BeautifulSoup with open(demo.html, encodingutf-8) as f: html f.read() soup BeautifulSoup(html, lxml) book_items soup.find_all(div, class_book-item) books [] for item in book_items: title item.find(h2, class_title).text.strip() author item.find(span, class_author).text.strip() price item.find(span, class_price).text.strip() link item.find(a)[href] books.append({ title: title, author: author, price: float(price), link: link, }) for book in books: print(book)这段代码输出三个字典里面就是你想要的结构化字段。有一个细节值得注意我拿到title后立刻调用了.strip()因为 HTML 源码里标签和文本之间经常存在换行和缩进尤其当 HTML 是格式化过的情况下.text返回的内容可能带着前后空白。清洗字段的习惯最好从第一天就养成不要等存进数据库再处理。3.2 用 CSS 选择器扩展查找能力如果你写过前端或者只是用过 jQuery你会觉得find_all这种“按条件找”的方式写长了有点啰嗦。BeautifulSoup 还内置了一套更接近前端习惯的查询语法select()和select_one()它们是按 CSS 选择器规则来工作的。# 等价于 soup.find_all(div, class_book-item) items soup.select(div.book-item) # 等价于 soup.find(span, class_price) price soup.select_one(span.price) # 更复杂的选择器 title soup.select_one(.book-item .title) link soup.select_one(a[href^/book/])[href]select_one对应返回单个节点select返回列表。CSS 选择器写起来紧凑层级关系一目了然尤其适合那种“目标节点嵌在多层 div 里”的页面。比如页面结构是article div.content ul li.item a用find_all链式写法得写好几层嵌套用select一行就搞定links soup.select(article div.content li.item a)日常写爬虫我推荐find家族和select家族混着用简单属性查找用find复杂层级定位用select。不用纠结谁更好工具互补才是常态。3.3 提取字段后的清洗动作解析只是第一步提取到的文本十有八九是“脏”的。常见的情况包括字符串首尾有空白、价格带货币符号和单位、日期格式不统一、链接是相对路径需要拼接成绝对 URL。这些小问题如果不处理存进 Excel 或数据库后会发现数据根本没法分析。清洗动作我习惯直接在解析循环里做而不是单独开一个后处理阶段。举个例子价格字段有时候是¥89.80就不能直接转float得先过滤掉非数字字符import re price_text item.find(span, class_price).text.strip() price_clean float(re.sub(r[^\d.], , price_text))再比如链接字段如果 HTML 里是/book/python-crash-course这样的相对路径你得跟站点主域名拼一下才能拿到完整 URLfrom urllib.parse import urljoin base_url https://books.example.com full_url urljoin(base_url, link)这些动作看起来细碎但正是“清”这个环节的价值所在。爬虫项目做到后面你会发现真正耗时耗力的根本不是请求和解析而是这些清洗规则在无限堆叠——每个数据源都有自己的小脾气你得为它们逐个写适配器。4. 常见问题与排查技巧实录4.1 解析结果为空先查这五个地方爬虫写好了跑起来却拿到一堆空列表这是新手遇到最多的情况。我总结了一个排查顺序按这个顺序查90% 的问题能在两分钟内定位第一确认你拿到的 HTML 确实是渲染后的内容。很多页面用 JavaScript 动态加载数据requests 能拿到的只是空壳。这种问题用 BeautifulSoup 解决不了得切换到 Selenium 或 Playwright或者去找页面背后的 JSON 数据接口。第二确认标签选择器和页面实际结构一致。用浏览器开发者工具查看元素时你看到的是浏览器修正后的 DOM可能和原始 HTML 源码不同。比如原始代码里没有闭合的标签浏览器会补全但 BeautifulSoup 在某些解析器下不一定补成一样的结构。第三检查class_是否写成了class。这个低级错误特别常见会直接抛语法错误但有时候错误被 try 吞了就变成返回空结果。第四看你的选择器是否只匹配到一个限定范围内的节点。如果你用soup.find(div, class_item)但页面里 class 为item hidden的元素不会被选中因为匹配是精准的不是包含关系。此时应该用select(div[class~item])或用class_传列表。第五别忘了 HTML 有可能不是 UTF-8 编码。requests 的response.text会尝试猜测编码但猜错的情况很常见乱码的 HTML 会导致标签匹配不上。手动指定编码一般能解决用response.content.decode(gbk, errorsignore)。排查点判断方法常用解法动态内容未渲染打印 HTML 看目标字段是否存在改用无头浏览器或找接口结构不匹配对比浏览器 DOM 和源码 DOM调整选择器class写成关键词冲突看是否报错换成class_class 多值匹配用开发者工具看真实 class用 CSS 选择器代替编码错误出现乱码字符手动指定编码4.2 定位不准你匹配到了太多无关节点比匹配不到更烦人的是“好像匹配到了但混进来一堆别的”。比如页面上有一个图书列表和你想要的列表结构一样class 命名也类似find_all就把两个列表的都找出来了。这个问题的本质是你的选择器“太宽”了。解法就一个思路让选择器更贴近目标节点的上下文。最简单有效的办法是“从父到子逐层定位”。先把包含目标数据的大容器找出来再在容器内精确查找content soup.find(div, idmain-column) items content.find_all(div, class_item)如果你用的select则可以利用层级选择器限制路径# 只匹配 main-column 下的 item items soup.select(#main-column .item)还有一种更稳健的定位思路利用兄弟节点或属性组合。比如某个span的 class 是price但它前面没有任何标志性父标签这时候可以用find_next_sibling()或find_previous_sibling()来根据相邻节点定位price_span title_tag.find_next_sibling(span, class_price)4.3 大规模抓取时的性能与内存提示BeautifulSoup 本身不是高性能解析库它的设计目标就是易用性而不是每秒处理几万个页面的吞吐量。当你面对几十万级别的页面需要认真考虑两件事第一lxml解析器一定要装性能差距很现实第二解析完就释放对象别把soup对象往列表里塞否则内存占用会肉眼可见地涨。如果发现单线程跑得太慢建议先别急着上多线程而是先优化请求和解析的衔接方式用requests.Session()复用连接批量解析时按批处理及时释放内存数据及时写入文件或数据库而不是攒在内存里。等这些优化做完还不够再考虑用concurrent.futures.ThreadPoolExecutor做多线程并发。但并发一开你就得面对限流、封 IP、请求频率控制的问题这属于爬虫进阶的范畴了第四章节先不展开。4.4 编码与乱码问题速查前面提到编码问题这里给出一组实际的判断方法。页面返回的 HTML 里通常写着meta charsetgbk或meta charsetutf-8但这不是 100% 可靠因为有些网站声明跟实际内容不一致。最稳妥的做法是用requests的response.encoding属性设置你怀疑的编码然后用response.text来解析或者直接操作字节import requests from bs4 import BeautifulSoup resp requests.get(https://example.com) # 先尝试从 headers 里拿编码拿不到就默认 utf-8 encoding resp.apparent_encoding or utf-8 html resp.content.decode(encoding, errorsignore) soup BeautifulSoup(html, lxml)apparent_encoding是 requests 基于内容猜测的编码它不是百分百准确但能覆盖大多数情况。如果你遇到反爬严重的网站resp.content可能已经是加密或压缩过的内容这时候先解决压缩解压和加密流程而不是在解析层纠结。5. 解析后的数据保存与下一步规划5.1 先存 CSV最简单也最通用解析完成、清洗干净的数据第一站往往是落盘。对初学者来说CSV 是最友好的格式Excel 直接打开pandas 直接读取编码用 UTF-8 加utf-8-sig避免中文乱码。保存代码很简单import csv with open(books.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, author, price, link]) writer.writeheader() writer.writerows(books)一个容易踩的坑在 Windows 下不加newline会导致每行后面多一个空行写入的 CSV 打开后行距巨大。这是我早期犯过的错现在每次写 CSV 都习惯性带上这个参数。5.2 数据量大了再考虑数据库如果你要抓的数据量超过几千条CSV 就不太够用了——更新记录、去重、条件查询都很麻烦。这时候可以引入 SQLite它是 Python 标准库自带的不需要装任何外部服务。建表、插入数据、去重查询的代码不复杂import sqlite3 conn sqlite3.connect(books.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS books ( title TEXT PRIMARY KEY, author TEXT, price REAL, link TEXT ) ) for book in books: cursor.execute( INSERT OR IGNORE INTO books (title, author, price, link) VALUES (?, ?, ?, ?), (book[title], book[author], book[price], book[link]), ) conn.commit() conn.close()这里用title作为主键INSERT OR IGNORE实现最简单的去重。实际项目中你会遇到主键冲突、更新已有数据的问题策略会比这个复杂但基础框架就是这一套。等 SQLite 不够用了再考虑 MySQL 或 PostgreSQL那是后话。5.3 定位自己在整个爬虫体系中的进度到这里你已经走完了“下载 HTML → 解析 HTML → 提取字段 → 清洗保存”这一整条最小闭环。这个闭环的意义很大它意味着你具备了独立完成“静态页面数据采集”的能力。很多真实场景比如抓取政府公开目录、抓取企业信息公示页、抓取静态展示的商品列表都属于这个范畴。这个系列后续的章节会接着讲更复杂的场景需要登录认证的页面怎么办、数据通过 Ajax 异步加载怎么办、页面上有反爬验证怎么绕过、如何用 Scrapy 框架来规模化采集。但不管那些内容多花哨底层都离不开 BeautifulSoup 构建的那套“解析树 查找节点 提取属性”的基本功。把这一节吃透后面你会省很多力气。6. 实操心得与避坑备忘6.1 我最想让你记住的几个原则写解析代码容易写出“不崩”的解析代码难。我觉得最有用的几条经验说给你听第一永远假设页面会变。今天能匹配到.price不代表明天还能匹配到。好的爬虫代码应该把选择器集中定义在一个地方方便改同时加好容错判断——if item.find(span, class_price) is None就跳过这条数据而不是让整个程序崩掉。第二复杂页面先手工分析再写代码。遇到一个结构很深的页面别急着敲代码先用浏览器开发者工具把目标节点“扒”清楚它的父节点是谁、class 有什么特征、有没有唯一的 id。分析清楚再写你写的选择器会稳定得多。第三尊重目标网站。爬虫写的再漂亮也得考虑对方的服务器和 robots 协议。控制请求频率设置合理的 User-Agent抓下来的数据不要用于非法用途。这不是说教是每个从业者都必须建立的底线意识。爬虫本身是工具工具无害关键是你怎么用它。6.2 一个推荐的学习路径学完这一节的 BeautifulSoup 基础接下去怎么练我给你一个“由浅入深”的实操路径先爬一个纯静态页面比如某个经典书籍的目录页提取书名和章节链接然后找一个分页列表页把第 1 到第 3 页的数据全部抓下来练习 URL 拼接和循环请求接着找一个属性匹配更复杂的页面试试select的层级写法最后把你抓到的数据分别存成 CSV 和 SQLite完整走一遍流程。这个过程走完你会发现自己已经能写“不再只是演示”的爬虫脚本了。我自己带过不少新人凡是老老实实按这个路径练过一遍的后面的进阶内容都学得很快。解析和清洗的基本功就是爬虫这条路上最值得花时间的地方。
