ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python爬虫数据提取:BeautifulSoup解析HTML实战指南

Python爬虫数据提取:BeautifulSoup解析HTML实战指南 做爬虫的朋友都知道抓网页从来不是最难的部分真正让人头疼的是把那一大坨 HTML 字符串里想要的信息抠出来。BeautifulSoup 就是为这件事而生的——它是 Python 生态里最友好的 HTML 解析库之一能把网页里的标题、链接、价格、评论这些结构化字段用几行代码就干干净净地提取出来。这一节面向零基础读者我从 BeautifulSoup 的安装、核心对象讲起把 find、find_all、select 这些主力方法一个个拆开讲透最后用一个商品列表页的实战案例收尾。你不需要有任何 HTML 基础跟着敲一遍就能体会到从乱糟糟的网页源码里精准取数据的爽快感。1. 为什么爬虫离不开解析这一步1.1 爬虫完整流程里解析处于哪一环很多人学爬虫容易有一个误区以为用 requests 把网页源码抓下来就完事了。实际上requests 拿到手的只是一段很长的字符串里面混着标签、属性、样式、脚本真正对你有用的数据可能就藏在几十个div的深处。想把这些数据变成 Python 里能直接用的变量就必须经过解析这一步——把字符串变成有结构、有层次的对象再按需取值。一条完整的爬虫链路通常是这样的发送请求 → 获取响应内容 → 解析 HTML → 提取目标字段 → 清洗数据 → 存储或展示。BeautifulSoup 负责的就是解析 HTML和提取目标字段这两环。你可以把 BeautifulSoup 理解成一个网页解剖师它会先把 HTML 标签的嵌套关系梳理成一棵树然后你告诉它我要哪个枝条上的哪片叶子它就能精确地把那片叶子摘给你。这个树状结构在专业术语里叫文档树DOM Tree后面讲遍历时会反复用到。1.2 为什么正则在很多场景下不够用我知道肯定有人会问Python 里不是有 re 模块吗用正则匹配不也能提取数据能但很痛苦。正则本身是个好东西但 HTML 的嵌套结构极其复杂标签属性顺序可能变化同一个 class 可能有多个值甚至有动态生成的节点。用正则去匹配div classprice59.00/div这种简单结构还行一旦遇到多层嵌套、属性顺序不固定、标签里还套着标签的情况正则表达式会越写越长、越写越脆网站稍微改一下结构就直接崩。BeautifulSoup 的思路完全不同。它是基于选择器来定位元素的你只需要表达我要 class 为 price 的 span 标签而不用关心那个 span 里面还嵌套了什么、属性顺序怎么排。这就好比你要找一个住在小区里的人正则的方式是背下他的完整身份证号BeautifulSoup 的方式是告诉他穿蓝色外套戴眼镜的那个人。论灵活性和可维护性解析库完胜手写正则。1.3 解析库选型BeautifulSoup vs lxml vs 正则Python 里主流的 HTML 解析方案大概有三类我简单做了个对比方案学习成本性能适用场景正则 re中高很快只提取简单片段、或匹配非 HTML 文本BeautifulSoup低中等HTML 结构较乱、嵌套深适合新手和中小型爬虫lxml XPath中高很快大规模采集、对性能有要求结构相对规整的场景选型建议很直白零基础入门、或者要抓的页面比较杂乱直接用 BeautifulSoup等以后爬的数据量大了、对速度有要求了再学 lxml 的 XPath 也不迟。BeautifulSoup 底层也可以指定用 lxml 来做解析器属于鱼和熊掌可以兼得的方案后面会细说。至于正则我的建议是不要完全扔掉有些场景比如从一段 JS 代码里抠数据正则反而是最优解但它不应该成为你解析 HTML 的主力工具。2. BeautifulSoup 核心概念与安装准备2.1 三个核心对象Tag、NavigableString、BeautifulSoupBeautifulSoup 整个库其实就围绕三个东西转BeautifulSoup 对象、Tag 对象、NavigableString 对象。搞清楚它们后面所有代码你都能看懂了。BeautifulSoup 对象是你把整个 HTML 文档传进去之后得到的总容器它代表整棵文档树。Tag 对象是容器里的某个具体标签节点比如一个div、一个a它可以继续往下找自己的子标签。NavigableString 则是标签里面的纯文本内容比如span59.00/span里的59.00。这里有个很多人初次接触时容易懵的点Tag 是有名字、有属性的它有自己的标签结构而 NavigableString 就是纯文本没有属性可用。用 type() 去看它们的类型结果不一样操作方式也完全不同。举个例子soup.title 返回的是一个 Tag 对象soup.title.string 返回的才是字符串。如果你在 Tag 对象上直接调用字符串方法十有八九会报错反过来你要是在 NavigableString 上尝试.attrs也会一脸懵。记住这个边界排查问题的时候能省很多时间。2.2 解析器选择html.parser 与 lxml 的取舍BeautifulSoup 本身不解析 HTML它需要你指定一个解析器。内置的 html.parser 不需要额外安装任何东西开箱即用lxml 是第三方的高性能解析器解析速度快容错性强遇到残缺的 HTML 标签也能尽量补全。除此之外还有个 html5lib号称最接近浏览器行为但速度慢日常几乎用不上。我的建议是直接装 lxml把 BeautifulSoup 的解析器参数写成lxml。理由很简单很多真实网页的 HTML 是不规范的比如标签没闭合、属性值没加引号html.parser 碰到这种情况虽然不至于崩但解析结果可能跟你预期对不上。lxml 的容错能力更强实测下来对怪异 HTML 的还原度更高。安装命令很简单pip install beautifulsoup4 lxml注意包名是 beautifulsoup4不是 bs4。虽然导入库的时候写的是 from bs4 import BeautifulSoup但 pip 安装的名字已经统一成了 beautifulsoup4。我见过不少新手在终端敲 pip install bs4装上一个和 BeautifulSoup 完全无关的包然后一脸疑惑地来找我。装完之后可以跑一个最简单的验证from bs4 import BeautifulSoup soup BeautifulSoup(pHello, World/p, lxml) print(soup.p.string)能输出 Hello, World说明环境没问题咱们继续往下走。2.3 用一个小例子理解文档树为了让你对文档树有直觉感受我拿一个非常短的 HTML 来做演示!DOCTYPE html html head title我的书架/title /head body div classbook-list div classbook-item span classtitlePython编程从入门到实践/span span classprice59.00/span /div /div /body /html把这个字符串交给 BeautifulSoup 之后它内部会生成一棵树html 是根下面挂着 head 和 bodyhead 里有 titlebody 里有 divdiv 里又套着两个 span。你访问 soup.title它会沿着树一路找到 title 节点你访问 soup.div它会返回第一个 div。这种点一下名字就得到节点的语法看起来像在操作属性实际上是 BeautifulSoup 在背后遍历了整棵文档树。理解了这一点你就能明白为什么访问不存在的标签时它会返回 None 而不是报错——这棵树里找不到自然就是没有。3. 从 HTML 提取结构化字段核心操作手册3.1 find 与 find_all最常用的两个方法进入正题。日常爬虫里用得最多的就是 find 和 find_all。先看 find_allitems soup.find_all(div, class_book-item)这句话的意思是找出所有 class 为 book-item 的 div 标签返回一个列表。里面有两个细节要注意。第一个是参数名 class_ 后面有个下划线因为 class 是 Python 的关键字直接用会报语法错误这是 BeautifulSoup 特意改的。第二个是 find_all 永远返回列表哪怕只有一个结果也是[element]这种形式。find 则是只找第一个匹配的标签找不到就返回 Noneprice soup.find(span, class_price) if price is not None: print(price.text)为什么要有 find因为有些 class 全页只会出现一次比如页面标题、唯一的面包屑导航这时候用 find_all 再 [0] 就显得多余。但注意 find 返回的可能是 None所以拿到结果后先做个判空是最稳妥的写法否则下一步.text会直接抛 AttributeError我在后面排查部分会详细讲这个经典报错。这两个方法还支持传入多个过滤条件比如同时指定标签名和属性a_tag soup.find(a, class_detail-link, hrefTrue)hrefTrue 表示只需要带有 href 属性的 a 标签这在过滤无意义链接时非常实用。3.2 CSS 选择器select 的高效定位如果说 find_all 是按条件搜索那 select 就是按 CSS 路径定位。它支持所有常见的 CSS 选择器语法用起来更简洁更适合一次性写出精确路径titles soup.select(div.book-item span.title) first_price soup.select_one(.book-item .price)select 返回的是所有匹配元素的列表select_one 只返回第一个。.book-item .price表示 class 为 book-item 的元素内部的 class 为 price 的元素中间的空格表示后代关系表示直接子元素。这两种写法对应不同的嵌套层级实际使用时你可以根据 HTML 结构灵活选择。CSS 选择器还有一个巨大的优势它可以按属性值精确匹配比如找所有 title 属性包含某个关键词的链接links soup.select(a[href*detail])这行代码能找出所有 href 里包含 detail 的 a 标签类似正则里的模糊匹配。我用这个方法做过不少“只抓详情页链接”的需求比写多层 for 循环省事多了。可以的话把 select 常用语法过一遍#id选 id.class选 classa[href]选带 href 的 a 标签div p选直接子元素用熟了之后效率会高很多。3.3 提取文本与属性从元素到数据定位到元素之后接下来要做的就是把里面的数据拿出来。提取纯文本推荐用.get_text()它能把标签内部的文字内容全部拼出来title item.select_one(span.title).get_text(stripTrue)stripTrue 这个参数相当实用它会自动把文本首尾的空白字符清掉。很多 HTML 源码里标签和文本之间有大量换行和空格直接 .text 拿出来的字符串可能全是\n和空格肉眼看着正常存进数据库才发现脏得不行。所以我在所有提取文本的代码里都会习惯性地带上 stripTrue。提取属性值用.get(属性名)。比如要拿链接地址detail_url item.select_one(a).get(href)这里有个小坑如果标签里没有这个属性get 会返回 None。如果你直接[href]这种下标方式取不存在时会抛 KeyError。相比之下 get 更安全所以我建议统一用 get。另外虽然也可以用a_tag[href]这种形式但遇到属性缺失就崩新手比较容易在这一点上吃亏。3.4 实战抓取一个商品列表页光说不练假把式我们拼一个完整的实战案例。假设有一个图书列表页HTML 结构长这样div classbook-item span classtitlePython编程从入门到实践/span span classauthor埃里克·马瑟斯/span span classprice59.00/span a href/book/1001查看详情/a /div div classbook-item span classtitle流畅的Python/span span classauthor卢西亚诺·拉马略/span span classprice89.50/span a href/book/1002查看详情/a /div我们把它保存成一个本地 HTML 文件然后用 BeautifulSoup 把每本书的标题、作者、价格、详情链接都提取出来from bs4 import BeautifulSoup with open(books.html, encodingutf-8) as fp: soup BeautifulSoup(fp, lxml) books [] for item in soup.select(div.book-item): title item.select_one(span.title).get_text(stripTrue) author item.select_one(span.author).get_text(stripTrue) price item.select_one(span.price).get_text(stripTrue) detail item.select_one(a).get(href) books.append({ title: title, author: author, price: float(price), detail_url: detail }) for book in books: print(book)输出结果就是四个字典组成的列表每个字典都对应一本书的结构化数据。到这里你会发现前面讲的三个核心点全部串起来了select 定位节点get_text 抽文本get 取属性。整个过程没有任何正则代码读起来像散文一样清楚。这个案例里还有一个很值得学的思路先在内存里把数据组装成列表套字典的结构再去想存储的事。很多初学者一上来就想直接写数据库结果数据格式还没定就急着落库后面清洗逻辑一变库里全是脏数据。先把数据结构在内存里调顺再用 pandas 或 sqlalchemy 去存效率会高很多。4. 文档树遍历与数据清洗4.1 父子节点和兄弟节点怎么遍历find_all 和 select 是按条件找人但有时候你不知道目标长什么样只知道它在某个节点附近这时候就需要遍历文档树。BeautifulSoup 提供了一组非常直观的属性.parent找到父节点.children遍历直接子节点.next_sibling和.previous_sibling找相邻兄弟节点。举个例子如果某个商品的价格标签里没有 class但它的前一个兄弟标签是书名你可以这样定位title_tag soup.find(span, string流畅的Python) price_tag title_tag.find_next_sibling()find_next_sibling() 不带参数时返回下一个兄弟节点带参数时按条件过滤。这种通过上下文定位数据的技巧在处理那些 class 命名混乱的老旧网页时特别管用。我当年接手过一个内部系统前端给所有标签都起了c1、c2这种毫无意义的类名靠 select 根本没法写最后全靠父子兄弟关系一点点抠出来的。这里需要注意children 返回的是一个生成器不是列表。如果你要按索引取第几个子节点可以先 list() 转换或者直接用.contents属性它返回列表。不过说实话日常爬虫里遍历用得并不算多更常见的是用 select 精确定位只有在 HTML 结构奇怪、没法用选择器表达的时候遍历才会上场。你只要知道有这么几个属性遇到特殊情况能想起来用就行。4.2 编码乱码问题与解决方案爬虫十大经典老坑里编码绝对排前三。网页返回的 HTML 在 head 里通常有meta charsetutf-8但实际内容可能是 GBK 或其他编码。如果你不管三七二十一直接 requests.get 然后 .text大概率拿到一堆乱码。正确做法是先看响应头的编码或从 HTML 的 meta 标签里找 charset然后用正确的编码解码。BeautifulSoup 这里有个小帮忙如果你直接用BeautifulSoup(requests.get(url).content, lxml)在传 content 字节流的情况下它会自己去读 meta 标签判断编码。所以我在抓取时总是传 requests 返回的 content字节而不是 text已经解码过的字符串这样能避免相当一部分乱码问题。如果遇到 BeautifulSoup 也认不出来的情况就手动介入。requests 的响应对象有个.encoding属性你可以直接改resp requests.get(url) resp.encoding gbk soup BeautifulSoup(resp.text, lxml)改完再解析一般都能恢复正常。这里踩坑的教训是不要盲目相信网页声称的编码要以实际输出为准。乱码问题如果出现在存储阶段还牵扯到数据库连接串的 charset 配置那是另一条线这一节先不展开。4.3 清洗空白字符和脏数据的基本操作解析出来只是第一步清洗同样重要。我在 3.3 里提过 stripTrue 去首尾空白但它管不了中间的多余空格、全角半角混用、以及\u3000全角空格这种特殊字符。常见的清洗组合拳长这样def clean_text(raw): if not raw: return return .join(raw.split())raw.split()会按任意空白符包括换行、制表符、全角空格切开再重新拼起来这招能把字符串里所有多余空白一键收拢成单个空格。价格字段更特殊一点可能带着¥、、逗号千分位处理时可以先用正则把非数字和小数点去掉import re price_str re.sub(r[^\d.], , raw_price) price_value float(price_str)这套组合拳能解决 80% 的字段清洗需求。剩下 20% 比如日期格式统一、繁体转简体就需要结合具体站点去定制了。我的建议是清洗逻辑尽量写成一个纯函数单独放一个 utils 模块里不要跟解析代码混在一起。原因很简单同一个网站的同一个字段不同页面里的格式可能略有差异清洗函数独立出来后你可以集中测试、集中修补而不用在几十个解析函数里翻来翻去。5. 常见问题与排查技巧实录5.1 高频报错速查表我整理了一份新手最常遇到的报错对照照着排查能省不少时间报错信息成因解决方案AttributeError: NoneType object has no attribute textfind/select_one 没找到目标返回 None先判空再取属性检查标签名、class 是否写错AttributeError: ResultSet object has no attribute textfind_all 返回列表你却当成单个元素调用 .text遍历列表或者改用 find/select_oneKeyError: href用tag[href]时标签里没有 href 属性改用tag.get(href)SyntaxError: invalid keyword argument class把 class 直接当参数传了参数写成 class_乱码 / UnicodeDecodeError网页编码和解析编码不一致用 content 传给 BeautifulSoup或手动指定 resp.encoding这里面最经典的就是第一个。很多新手写完代码信心满满地跑结果被 NoneType 教育了一整天。归根到底就是因为网页结构和预期的不一样——可能 class 写错了可能元素在嵌套的 iframe 里可能内容是 JS 动态加载的。遇到这个报错不要慌先把 soup 打印出来看看或者把 select 的结果 length 打印一下确认元素到底有没有被定位到。5.2 定位不到元素的三个排查方向如果你确认目标数据确实在网页里但 BeautifulSoup 就是找不到我建议按顺序排查三个方向。第一检查你用的选择器是不是太严格。HTML 里 class 的顺序可能变化比如div classbook-item hot和div classhot book-item在浏览器里效果一样但如果你用 select(div.book-item.hot) 去匹配其中一种写法就匹配不到了。这时应该退一步用 find_all(div, class_book-item)或者干脆只按标签名找再在结果里进一步过滤。第二确认你要抓的数据是不是 JS 动态渲染的。现在的网页前端框架用得越来越多很多内容都是浏览器执行 JavaScript 之后才出现在 DOM 里的。requests 拿到的 HTML 只是最初的空壳BeautifulSoup 再厉害也解析不出一团根本不存在的节点。判断方法很简单右键查看网页源代码搜一下你要的数据在不在里面。如果不在那你需要的是渲染工具比如 Selenium 或 Playwright而不是解析库。第三看看内容有没有被嵌套在注释或特殊标签里。有些网站为了防止简单抓取会把数据塞在 HTML 注释里或者用 JSON 字符串的形式写在 script 标签内部。前者你肉眼能看到内容但 BeautifulSoup 默认不会把注释当普通节点处理后者则可以直接 json.loads 提取。遇到这种伪 HTML别死磕选择器换个思路从源码里找规律。5.3 关于爬虫使用边界的几句实在话每次讲爬虫我都会多说一句技术本身是中性的但使用方式要守住边界。你拿一个公开网页练手、做技术验证、爬取自己账号下的数据这些都算正常学习范畴。但不要去爬那些需要登录权限、明确在用户协议里禁止采集、或带着法律风险的数据接口。抓取频率也别太夸张给目标服务器留点喘息空间这是最基本的网络礼仪。另外现在很多网站已经有成熟的公开 API能用 API 解决的需求尽量不要用爬虫去硬啃 HTML。爬虫更适合做那些官方没提供接口、或者接口权限拿不到的场景。学习阶段建议先从自己搭的测试页面、或者允许爬虫的公开站点练手等理解了解析和清洗的逻辑再逐步接触复杂页面。爬虫是一个验证学习能力的好项目但长期来说把精力花在数据处理和业务理解上价值会更大。写在最后的一点体会BeautifulSoup 这套东西学下来真正让我觉得通了的时刻是在第一次把一个完全陌生的网站、从 requests 到清洗完成整套跑通的时候。你会发现解析 HTML 的技术点在整条爬虫链路里其实不算最难真正难的是对页面结构的观察力和对各种异常情况的耐心。建议你把 3.4 那个案例自己敲一遍再换一个本地 HTML 文件、换一套字段名练手对比一下 select 和 find_all 在不同结构下的表现差异很快就能形成肌肉记忆。等你熟练了可以再去接触 XPath、parsel 这些更偏工程化的解析方案但 BeautifulSoup 作为第一站绝对能把你的解析思维打得很扎实。
返回列表