3、HTML与网页结构解析:从标签到数据提取

做社交媒体数据抓取,说白了就是跟网页打交道。你想想看,微博、知乎、小红书这些平台,展示给你的内容本质上都是HTML。我刚开始做爬虫那会儿,总觉得HTML就是一堆乱七八糟的标签,后来才明白——不懂HTML结构,你连数据在哪儿都找不到

这一章,我带你把HTML解析这件事彻底搞明白。从基础标签到DOM树,再到用Python的BeautifulSoup库提取数据,一步到位。

3.1 HTML基础标签:网页的骨架

HTML不是什么高深的东西。它就像搭积木,每个标签就是一块积木。我习惯把HTML标签分成三类:结构标签、内容标签、属性标签

先看最常用的几个:

标签 作用 我在项目中踩过的坑
<div> 块级容器,最常用的布局元素 别滥用div,否则页面层级深到你怀疑人生
<span> 行内容器,用于包裹小段文本 它不会换行,很多人误以为它能当div用
<a> 超链接,href属性指定目标URL 注意href可能是相对路径,需要拼接
<img> 图片,src属性指定图片地址 有些网站用data-src做懒加载,直接拿src拿不到
<p> 段落文本 里面别再套块级元素,否则渲染会出问题
我的小习惯:抓取前先打开浏览器开发者工具(F12),看看目标数据的标签结构。这一步能省你后面80%的调试时间。

3.2 DOM树结构:网页的家族图谱

HTML文档解析后,浏览器会把它变成一棵树——DOM树。每个标签都是一个节点,节点之间有父子、兄弟关系。

举个例子,下面这段HTML:

<html>
  <body>
    <div id="content">
      <h1>标题</h1>
      <p class="desc">描述文本</p>
    </div>
  </body>
</html>

对应的DOM树长这样:

html body div#content h1 p.desc "标题" "描述文本"

为什么要理解DOM树?因为数据提取的本质就是在树上找节点。你想拿标题,就得从html→body→div→h1这条路径找下去。

我曾经犯过的错:直接拿正则表达式去匹配HTML内容。结果页面结构稍微一变,代码就崩了。后来我学乖了——解析HTML,永远用DOM解析器,别用正则

3.3 使用BeautifulSoup解析HTML

Python里解析HTML,我首选BeautifulSoup。它能把HTML字符串变成一棵可遍历的树,然后你用各种方法去摘果子(提取数据)。

先安装:

pip install beautifulsoup4 lxml

我习惯用lxml作为解析器,速度比Python内置的html.parser快不少。

基本用法:

from bs4 import BeautifulSoup

html = """
<div class="post">
  <h2>今天天气真好</h2>
  <p class="author">小明</p>
  <span class="likes">128</span>
</div>
"""

soup = BeautifulSoup(html, 'lxml')

# 提取标题
title = soup.find('h2').text
print(title)  # 输出:今天天气真好

# 提取作者
author = soup.find('p', class_='author').text
print(author)  # 输出:小明

# 提取点赞数
likes = soup.find('span', class_='likes').text
print(likes)  # 输出:128

常用的查找方法就这几个:

  • find() — 找第一个匹配的节点
  • find_all() — 找所有匹配的节点,返回列表
  • select() — 用CSS选择器查找,后面会讲
  • .text — 获取节点内的纯文本
  • ['属性名'] — 获取节点的属性值,比如a['href']
实战经验:抓取社交媒体数据时,我经常遇到嵌套很深的结构。这时候别一层层find,直接用find_all()配合标签名和class,一步到位。

3.4 CSS选择器入门:精准定位数据

CSS选择器本来是给前端做样式用的,但用在数据提取上,简直不要太爽。BeautifulSoup的select()方法支持CSS选择器,语法简洁,定位精准。

常用的CSS选择器:

选择器 含义 示例
标签名 选择所有该标签 soup.select('div')
.类名 选择所有该class的元素 soup.select('.post')
#id值 选择该id的元素 soup.select('#content')
父 > 子 直接子元素 soup.select('div > p')
祖先 后代 任意后代元素 soup.select('div p')
[属性=值] 按属性筛选 soup.select('[data-id="123"]')

看个实际例子。假设你要抓微博的帖子列表:

html = """
<div class="weibo-feed">
  <div class="card" data-id="1001">
    <p class="text">今天好开心</p>
    <span class="time">10分钟前</span>
  </div>
  <div class="card" data-id="1002">
    <p class="text">下雨了...</p>
    <span class="time">20分钟前</span>
  </div>
</div>
"""

soup = BeautifulSoup(html, 'lxml')

# 提取所有帖子的文本
texts = soup.select('.card .text')
for t in texts:
    print(t.text)

# 提取特定id的帖子
card = soup.select('[data-id="1002"]')
if card:
    print(card[0].select_one('.text').text)
我的建议:CSS选择器能组合使用。比如div.card > p.text:first-child,这种组合写法在复杂页面里特别管用。你可以在浏览器控制台先测试选择器,确认能选中目标再写代码。

3.5 实战:解析一条微博数据

把上面学的串起来,写一个完整的解析函数:

def parse_weibo(html):
    """
    解析单条微博的HTML,返回结构化数据
    """
    soup = BeautifulSoup(html, 'lxml')
    
    # 提取用户信息
    user = soup.select_one('.user-name')
    username = user.text.strip() if user else '未知'
    
    # 提取发布时间
    time = soup.select_one('.time')
    pub_time = time.text.strip() if time else ''
    
    # 提取正文
    content = soup.select_one('.content p')
    text = content.text.strip() if content else ''
    
    # 提取互动数据
    likes = soup.select_one('.likes span')
    like_count = likes.text.strip() if likes else '0'
    
    return {
        'username': username,
        'time': pub_time,
        'text': text,
        'likes': like_count
    }

嗯,这里要注意——实际抓取时,HTML结构可能比这复杂得多。我遇到过class名是动态生成的,也遇到过数据藏在JavaScript变量里的情况。但不管怎么变,解析的核心思路不变:找到DOM树中的目标节点,提取数据。

避坑指南:有些网站会故意混淆class名,比如把"content"写成"c0nt3nt"。这时候别硬匹配,试试用select('[class*="content"]')这种模糊匹配。

HTML解析这件事,说白了就是定位+提取两个步骤。定位靠CSS选择器或find方法,提取靠.text或属性访问。多练几次,你就能一眼看出数据在DOM树的哪个位置。

我刚开始做爬虫时,光解析一个页面就要折腾半天。现在熟练了,基本看一眼HTML结构,心里就有数了。你也一样,多写多练,很快就能上手。


专注资料整理