从零到接单 09:BeautifulSoup 静态网页爬取——从 HTML 中提取一切

pythontutorial爬虫自动化接单BeautifulSoupHTML

系列目录:本文是「从零到接单:Python 自动化与爬虫实战」系列的第 9 篇。上一篇我们用 requests 拿到了网页的 HTML 源码——但那是一大坨看不懂的字符串。这篇我们学 BeautifulSoup,把 HTML 解析成结构化的数据,精确提取你想要的信息。


requests 负责"把网页搬回来",BeautifulSoup 负责"拆开网页、挑出有用的部分"。两者配合就是一套完整的静态爬虫。

一、安装并快速上手

pip install beautifulsoup4 lxml
  • beautifulsoup4:HTML 解析库
  • lxml:高性能解析器(比内置的 html.parser 更快更好)
import requests
from bs4 import BeautifulSoup

# 获取网页
resp = requests.get("https://httpbin.org/html")
resp.encoding = "utf-8"

# 解析 HTML
soup = BeautifulSoup(resp.text, "lxml")

# 美化输出(看看结构)
# print(soup.prettify())

# 获取标题
print(soup.title)            # <title>...</title>
print(soup.title.string)     # 标题文本内容
print(soup.title.name)       # 标签名:title

二、使用本地 HTML 练习

为了避免频繁请求网站,先用一段本地 HTML 练习:

html_doc = """
<html>
<head><title>测试页面</title></head>
<body>
    <div class="article-list">
        <div class="article">
            <h2><a href="/post/1">Python 爬虫入门</a></h2>
            <p class="summary">从零开始学习 Python 爬虫技术...</p>
            <span class="date">2026-07-01</span>
            <span class="author">张三</span>
            <span class="views">1280 阅读</span>
        </div>
        <div class="article">
            <h2><a href="/post/2">自动化脚本实战</a></h2>
            <p class="summary">用 Python 自动处理 Excel 报表...</p>
            <span class="date">2026-07-05</span>
            <span class="author">李四</span>
            <span class="views">2560 阅读</span>
        </div>
        <div class="article">
            <h2><a href="/post/3">RPA 入门指南</a></h2>
            <p class="summary">RPA 是什么?如何用 Python 实现...</p>
            <span class="date">2026-07-10</span>
            <span class="author">张三</span>
            <span class="views">960 阅读</span>
        </div>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html_doc, "lxml")

三、核心查找方法

find():找第一个匹配

# 按标签名找
article = soup.find("div", class_="article")
print(article.h2.a.string)  # Python 爬虫入门

# 按属性找
link = soup.find("a", href="/post/2")
print(link.string)  # 自动化脚本实战

# 多条件
el = soup.find("span", class_="date", string="2026-07-05")
print(el)  # <span class="date">2026-07-05</span>

⚠️ 注意是 class_ 不是 class(因为 class 是 Python 关键字)。

find_all():找所有匹配(最常用!)

# 找所有文章
articles = soup.find_all("div", class_="article")
print(f"找到 {len(articles)} 篇文章")

for article in articles:
    title = article.h2.a.string
    author = article.find("span", class_="author").string
    views = article.find("span", class_="views").string
    print(f"{title} | {author} | {views}")

# 使用 limit 限制数量
first_two = soup.find_all("div", class_="article", limit=2)

select():CSS 选择器(推荐!)

# 标签选择器
titles = soup.select("h2")
print([t.a.string for t in titles])

# 类选择器
articles = soup.select(".article")
summaries = soup.select(".summary")

# 属性选择器
link = soup.select('a[href="/post/1"]')

# 层级选择器
links_in_article = soup.select(".article h2 a")

# 伪类:第 n 个
first_article = soup.select(".article:nth-of-type(1)")

# 找所有 class 包含 views 的 span
all_views = soup.select("span.views")
total_views = sum(int(v.string.replace(" 阅读", "")) for v in all_views)
print(f"总阅读量:{total_views}")  # 总阅读量:4800

四、提取数据

提取文本

tag = soup.find("h2")

# .string:直接子文本
print(tag.string)          # None(因为 h2 里还有 a 标签)

# .get_text():所有文本(推荐)
print(tag.get_text())      # Python 爬虫入门
print(tag.get_text(strip=True))  # 去掉首尾空白

# .text:属性方式
print(tag.text)            # Python 爬虫入门

提取属性

link = soup.find("a")
print(link["href"])       # /post/1
print(link.get("href"))   # /post/1(推荐,不存在返回 None)
print(link.attrs)         # {'href': '/post/1'}

导航查找

article = soup.find("div", class_="article")

# 向下找
article.h2            # 第一个 h2 子标签
article.h2.a          # h2 里的 a 标签

# 找父节点
article.parent        # 父标签

# 找兄弟节点
h2 = article.h2
h2.find_next_sibling()       # 下一个兄弟
h2.find_previous_sibling()   # 上一个兄弟
h2.find_next_siblings()      # 后面所有兄弟

五、实际案例:爬取一个博客列表

假设我们要爬取一个博客网站的文章列表:

import requests
from bs4 import BeautifulSoup
import csv
import time

class BlogCrawler:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
        })
    
    def crawl_list_page(self, url):
        """爬取列表页"""
        try:
            resp = self.session.get(url, timeout=10)
            resp.raise_for_status()
            resp.encoding = resp.apparent_encoding
            soup = BeautifulSoup(resp.text, "lxml")
            return soup
        except Exception as e:
            print(f"请求失败:{e}")
            return None
    
    def parse_articles(self, soup):
        """从 soup 中解析文章列表"""
        articles = []
        
        # 找到所有文章卡片(根据实际网站结构调整选择器)
        for card in soup.select(".article"):
            title = card.select_one("h2 a")
            summary = card.select_one(".summary")
            date = card.select_one(".date")
            author = card.select_one(".author")
            
            articles.append({
                "title": title.get_text(strip=True) if title else "",
                "link": title["href"] if title else "",
                "summary": summary.get_text(strip=True) if summary else "",
                "date": date.get_text(strip=True) if date else "",
                "author": author.get_text(strip=True) if author else "",
            })
        
        return articles
    
    def save_to_csv(self, articles, filename="articles.csv"):
        """保存到 CSV"""
        if not articles:
            print("没有数据可保存")
            return
        
        with open(filename, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=articles[0].keys())
            writer.writeheader()
            writer.writerows(articles)
        
        print(f"已保存 {len(articles)} 篇文章到 {filename}")

# 使用
if __name__ == "__main__":
    crawler = BlogCrawler()
    
    # 这里用 httpbin.org 演示结构,实际使用时替换为真实网站
    # soup = crawler.crawl_list_page("https://example-blog.com")
    
    # 用本地数据演示
    soup = BeautifulSoup(html_doc, "lxml")  # html_doc 是上面定义的测试 HTML
    articles = crawler.parse_articles(soup)
    
    for a in articles:
        print(f"• {a['title']} ({a['date']}) - {a['author']}")
    
    crawler.save_to_csv(articles)

六、常见坑与解决

1. 元素找不到返回 None

title = soup.select_one(".nonexistent")
# title 是 None!直接调用 .string 会报错!

# ✅ 安全写法
if title:
    print(title.string)
else:
    print("标题未找到")

# 或者用 get
print(title.get_text() if title else "未找到")

2. 编码问题(中文乱码)

resp = requests.get(url)
resp.encoding = resp.apparent_encoding  # 自动检测编码
# 或者手动指定
resp.encoding = "utf-8"

3. 动态加载的内容拿不到

# BeautifulSoup 只能解析 HTML 中的静态内容
# 如果网站用 JavaScript 加载数据,BS4 看不到!
# → 需要下一篇讲的 Selenium/Playwright

4. 请求频率太高被 Ban

import time
import random

for page in range(1, 11):
    url = f"https://example.com/list?page={page}"
    # ... 爬取 ...
    time.sleep(random.uniform(1, 3))  # 随机延时 1-3 秒

七、快速参考卡

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "lxml")

# ── 查找 ──
soup.find("div")                  # 找第一个 div
soup.find("div", class_="box")    # 带 class
soup.find_all("a")                # 找所有 a
soup.select(".article")           # CSS 选择器
soup.select_one("#main h1")       # 选一个

# ── 提取 ──
tag.get_text(strip=True)          # 文本
tag["href"]                       # 属性
tag.get("class", [])              # 属性(带默认值)

# ── 导航 ──
tag.parent                        # 父节点
tag.children                      # 所有子节点
tag.next_sibling                  # 下一个兄弟
tag.previous_sibling              # 上一个兄弟

总结

BeautifulSoup 让 HTML 解析变得简单优雅。核心三点:

  1. soup.find() / soup.select_one():找一个元素
  2. soup.find_all() / soup.select():找所有元素
  3. .get_text() / ["attr"]:提取文本或属性

requests + BeautifulSoup 的组合能处理 80% 的静态网页爬取需求。但有些网页的数据是 JavaScript 动态加载的——下一篇我们学习 Selenium/Playwright,搞定动态网页。


练习:找一个你感兴趣的博客或新闻网站,用 requests + BeautifulSoup 爬取它的文章列表页,提取每篇文章的标题、发布日期和链接,保存为 CSV 文件。记住设置合理的请求间隔!

Comments

Sign in to leave a comment.