如果你已经用
requests拿到了网页源码,却不知道如何从中"捞"出想要的数据——这篇文章就是为你量身定制的。今天,我们用 BeautifulSoup 的 7种核心武器,带你从"看到源码"到"精准提取",彻底打通数据抓取的最后一公里。
一、环境准备:工欲善其事,必先利其器
pip install requests beautifulsoup4 lxml💡 小贴士:
lxml解析器速度更快,建议安装。如果报错,用html.parser也完全够用。
import requestsfrom bs4 import BeautifulSoupimport re# 基础请求模板(后续代码复用)headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
二、七大武器:从网页中精准提取数据的完整攻略
武器①:find() —— 精准定位"第一个"
find() 返回匹配条件的第一个元素。当你明确知道目标唯一,或只关心第一个结果时,用它最顺手。
# 示例:获取页面第一个 h1 标题soup = BeautifulSoup(html, 'html.parser')first_title = soup.find('h1')print(first_title.text) # 输出标题文本# 按 class 查找第一个电影条目first_movie = soup.find('div', class_='item')# 按 id 查找(id 在页面中通常唯一)nav_menu = soup.find('ul', id='main-nav')# 按属性字典查找quote = soup.find('span', attrs={'itemprop': 'text'})
适用场景:获取页面标题、导航栏、第一个商品卡片等唯一或首个元素。
武器②:find_all() —— 批量收割"全部"
find_all() 返回所有匹配元素的列表,是爬虫中最常用的"收割机"。
# 获取所有电影条目movies = soup.find_all('div', class_='item')print(f"共找到 {len(movies)} 部电影")# 获取所有评分ratings = soup.find_all('span', class_='rating_num')for r in ratings:print(r.text) # 9.7, 9.6, 9.5...# 限制返回数量(只取前5个)top5 = soup.find_all('div', class_='item', limit=5)# 多重条件筛选:class 为 quote 且 itemprop 为 textquotes = soup.find_all('span', attrs={'class': 'quote', 'itemprop': 'text'})
适用场景:批量提取列表数据、表格行、商品卡片、评论等重复结构。
武器③:select() —— CSS选择器的优雅之道
如果你熟悉前端 CSS,select() 会让你的代码简洁得像写诗。它支持完整的 CSS 选择器语法。
# 基础选择器items = soup.select('div.item') # 所有 class="item" 的 divitems = soup.select('.item') # 同上,更简洁items = soup.select('#main') # id="main" 的元素# 层级选择器links = soup.select('div.item a') # item 内的所有 a 标签link = soup.select_one('div.item > a') # item 的直接子 a 标签# 属性选择器authors = soup.select("small[class='author'][itemprop='author']")# 伪类选择器(部分支持)first_item = soup.select_one('div.item:first-child')odd_items = soup.select('div.item:nth-of-type(odd)')
find_all() vs select() 对比:
class_='name' | .name | |
建议:简单查找用 find_all(),复杂层级或属性组合用 select()。
武器④:文本与属性获取 —— 拿到"内容"和"链接"
找到元素只是第一步,提取数据才是目的。
movie = soup.find('div', class_='item')# 获取文本内容(自动去除标签)title = movie.find('span', class_='title').texttitle = movie.find('span', class_='title').get_text() # 同上title = movie.find('span', class_='title').string # 仅直接子文本# 获取属性值link = movie.find('a')['href'] # 链接地址img_url = movie.find('img')['src'] # 图片地址alt_text = movie.find('img').get('alt') # 安全获取(不存在不报错)# 获取多个 classelement = soup.find('div')class_list = element.get('class') # 返回列表:['item', 'active']# 获取所有属性attrs = movie.attrs # 返回字典:{'class': ['item'], 'id': 'movie-1'}
text vs string vs get_text() 的区别:
.text | ||
.string | ||
.get_text() | strip=True 等参数 |
# 推荐用法:自动清理空白clean_text = element.get_text(strip=True)
武器⑤:文档树遍历 —— 父子兄弟齐上阵
有时标签没有明显的 class/id,但你知道它在文档树中的位置关系。
element = soup.find('div', class_='item')# 向上遍历parent = element.parent # 父节点grandparent = element.parent.parent # 祖父节点ancestors = element.parents # 所有祖先(生成器)# 向下遍历children = element.children # 直接子节点(生成器)descendants = element.descendants # 所有子孙(生成器)# 横向遍历next_sibling = element.next_sibling # 下一个兄弟prev_sibling = element.previous_sibling # 上一个兄弟next_siblings = element.next_siblings # 后面所有兄弟# 实用技巧:找到评论数(位于 star div 的最后一个 span)star_div = movie.find('div', class_='star')people = star_div.find_all('span')[-1].text # "2500000人评价"
适用场景:处理结构不规则的 HTML、表格单元格定位、相邻元素关联提取。
武器⑥:正则匹配 —— 模糊搜索的终极利器
当目标文本包含变体,或 class 名称动态变化时,正则表达式就是你的救星。
import re# 查找包含 "Einstein" 的所有文本(忽略大小写)pattern = re.compile(r'Einstein', re.IGNORECASE)matches = soup.find_all(string=pattern)print(f"找到 {len(matches)} 处匹配")# 查找 class 以 "col-" 开头的所有元素col_elements = soup.find_all(class_=re.compile('^col-'))# 查找 href 包含 "detail" 的链接detail_links = soup.find_all('a', href=re.compile('detail'))# 提取文本中的数字people_text = "2500000人评价"number = re.search(r'\d+', people_text).group() # "2500000"
适用场景:动态 class 名、模糊文本匹配、数据格式提取(如从"9.7分"中提取数字)。
武器⑦:prettify() —— 格式化与调试神器
prettify() 不是提取工具,而是调试和输出的利器。它能将混乱的 HTML 格式化缩进,让你一眼看清结构。
# 格式化输出整个文档print(soup.prettify())# 格式化输出单个元素movie = soup.find('div', class_='item')print(movie.prettify())# 保存到文件查看结构with open('debug.html', 'w', encoding='utf-8') as f:f.write(soup.prettify())
适用场景:调试时查看 HTML 结构、保存清洗后的 HTML、生成可读性强的报告。
三、实战案例:requests + bs4 抓取豆瓣电影Top250
下面是一个完整、可直接运行的实战案例,综合运用上述武器。
目标分析
URL 规律: https://movie.douban.com/top250?start={0,25,50,...,225}每页 25 条,共 10 页 提取字段:排名、标题、评分、评价人数、引言、链接
完整代码
import requestsfrom bs4 import BeautifulSoupimport pandas as pdimport timeimport reheaders = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}def get_page_movies(url):# 获取单页电影数据try:response = requests.get(url, headers=headers, timeout=10)response.raise_for_status()soup = BeautifulSoup(response.text, 'html.parser')movies = []for item in soup.find_all('div', class_='item'):# 排名rank = item.find('em').text# 标题(可能有两个,取第一个)title = item.find('span', class_='title').text# 评分rating = item.find('span', class_='rating_num').text# 评价人数(正则提取数字)people_text = item.find('div', class_='star').find_all('span')[-1].textpeople = re.search(r'\d+', people_text).group()# 引言(可能不存在,做安全处理)quote_tag = item.find('span', class_='inq')quote = quote_tag.text if quote_tag else ''# 链接link = item.find('a')['href']movies.append({'排名': int(rank),'标题': title,'评分': float(rating),'评价人数': int(people),'引言': quote,'链接': link})return moviesexcept Exception as e:print(f"请求失败: {e}")return []def get_all_movies():# 获取全部250部电影all_movies = []base_url = 'https://movie.douban.com/top250?start={}'for start in range(0, 250, 25):url = base_url.format(start)print(f"正在爬取: {url}")movies = get_page_movies(url)all_movies.extend(movies)# 礼貌爬虫:每页间隔 2-3 秒time.sleep(2)return all_movies# 执行爬取if __name__ == '__main__':movies = get_all_movies()# 保存为 CSVdf = pd.DataFrame(movies)df.to_csv('douban_top250.csv', index=False, encoding='utf-8-sig')# 简单分析print(f"共爬取 {len(movies)} 部电影")print(f"平均评分: {df['评分'].mean():.2f}")print(f"最高分: {df.loc[df['评分'].idxmax()]['标题']} ({df['评分'].max()}分)")print(f"评价最多: {df.loc[df['评价人数'].idxmax()]['标题']} ({df['评价人数'].max()}人)")
代码亮点解析
find_all() | ||
find() | ||
star.find_all('span')[-1] | ||
re.search(r'\d+', people_text) | ||
quote_tag.text if quote_tag else '' |
四、进阶技巧:从"能爬"到"爬得好"
技巧1:动态页面处理(JavaScript渲染)
BeautifulSoup 只能解析静态 HTML。如果数据由 JavaScript 动态加载,需要借助其他工具:
# 方案A:Selenium(模拟浏览器)from selenium import webdriverfrom bs4 import BeautifulSoupdriver = webdriver.Chrome()driver.get('https://example.com/dynamic-page')# 等待 JS 加载完成...time.sleep(3)soup = BeautifulSoup(driver.page_source, 'html.parser')# 后续用 bs4 解析...driver.quit()# 方案B:Playwright(更现代,推荐)from playwright.sync_api import sync_playwrightwith sync_playwright() as p:browser = p.chromium.launch()page = browser.new_page()page.goto('https://example.com')page.wait_for_selector('.loaded-content') # 等待元素出现html = page.content()browser.close()soup = BeautifulSoup(html, 'html.parser')
技巧2:表格数据提取( 标签)
# 提取网页表格为 DataFrameimport pandas as pdtable = soup.find('table', class_='data-table')# 方法1:手动提取rows = []for tr in table.find_all('tr')[1:]: # 跳过表头 cells = [td.get_text(strip=True) for td in tr.find_all(['td', 'th'])] rows.append(cells)df = pd.DataFrame(rows[1:], columns=rows[0])# 方法2:直接用 pandas(更简洁)df = pd.read_html(str(table))[0]
# 提取网页表格为 DataFrameimport pandas as pdtable = soup.find('table', class_='data-table')# 方法1:手动提取rows = []for tr in table.find_all('tr')[1:]: # 跳过表头cells = [td.get_text(strip=True) for td in tr.find_all(['td', 'th'])]rows.append(cells)df = pd.DataFrame(rows[1:], columns=rows[0])# 方法2:直接用 pandas(更简洁)df = pd.read_html(str(table))[0]
技巧3:修改 HTML 内容(数据清洗与重构)
# 修改标签内容tag = soup.find('h1')tag.string = '新标题'# 修改属性tag['class'] = 'new-class'tag['id'] = 'main-title'# 添加新属性tag['data-custom'] = 'value'# 删除属性del tag['class']# 插入新标签new_tag = soup.new_tag('p')new_tag.string = '这是新段落'tag.append(new_tag)# 删除标签tag.decompose()# 替换标签new_tag = soup.new_tag('div')tag.replace_with(new_tag)# 输出修改后的 HTMLprint(soup.prettify())
五、常见问题:6个踩坑实录与解决方案
问题1:返回 403 Forbidden / 418 I'm a teapot
原因:网站识别出你是爬虫,拒绝访问。
解决方案:
# ① 完善请求头headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Referer': 'https://movie.douban.com/top250','Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',}# ② 降低请求频率import time, randomtime.sleep(random.uniform(2, 5)) # 随机延迟 2-5 秒# ③ 使用代理 IP(高阶)proxies = {'http': 'http://your-proxy:port','https': 'https://your-proxy:port'}response = requests.get(url, headers=headers, proxies=proxies)
问题2:中文乱码
原因:编码识别错误。
解决方案:
# 方案1:手动指定编码response.encoding = 'utf-8'# 方案2:根据内容自动推断response.encoding = response.apparent_encoding# 方案3:保存时指定编码df.to_csv('data.csv', encoding='utf-8-sig') # -sig 解决 Excel 打开乱码
问题3:find/find_all 返回 None 或空列表
原因:选择器写错、class 名动态变化、或数据在 JS 中。
解决方案:
# ① 检查 HTML 结构print(soup.prettify()) # 先看源码里到底有没有# ② 使用更稳健的选择器# 避免:soup.find('div', class_='exact-class-name')# 推荐:soup.find('div', class_=re.compile('partial'))# ③ 安全取值 + 调试result = soup.find('div', class_='item')if result:print(result.text)else:print("未找到元素,检查选择器或页面结构")
问题4:提取的文本包含大量空白和换行
原因:HTML 中的空白字符被保留。
解决方案:
# 使用 strip 参数清理text = element.get_text(strip=True)# 或使用正则进一步清理import retext = re.sub(r'\s+', ' ', element.get_text()).strip()
问题5:只能爬到第一页,翻页失败
原因:URL 构造错误、未处理分页参数、或被反爬。
解决方案:
# 检查分页规律def make_urls(base, total_pages, step=25):return [f"{base}?start={i*step}" for i in range(total_pages)]# 豆瓣示例:start=0,25,50...urls = [f'https://movie.douban.com/top250?start={i}' for i in range(0, 250, 25)]# 加异常处理,单页失败不影响整体for url in urls:try:data = get_page_data(url)except Exception as e:print(f"跳过失败页面: {url}, 错误: {e}")continue
问题6:class 属性包含多个值,匹配失败
原因:class_='item' 只匹配 class 恰好等于 "item" 的元素。如果 class="item active",则匹配失败。
解决方案:
# 错误 ❌soup.find('div', class_='item') # 匹配不到 class="item active"# 正确 ✅ 方法1:传入列表(匹配包含任意一个)soup.find('div', class_=['item', 'active'])# 正确 ✅ 方法2:使用 CSS 选择器soup.select('div.item') # 匹配包含 item 类的所有 div# 正确 ✅ 方法3:正则模糊匹配soup.find('div', class_=re.compile('item'))
六、总结:一张图记住7种武器
+-----------------------------------------------------+| BeautifulSoup 七大武器速查表 |+-----------------------------------------------------+| ① find() -> 精准定位第一个元素 || ② find_all() -> 批量获取所有匹配元素 || ③ select() -> CSS选择器,灵活优雅 || ④ 文本/属性获取 -> .text / ['href'] / .get() || ⑤ 文档树遍历 -> .parent / .children / .siblings || ⑥ 正则匹配 -> 模糊搜索,动态class也不怕 || ⑦ prettify() -> 格式化调试,结构一目了然 |+-----------------------------------------------------+
写在最后
BeautifulSoup 就像一把瑞士军刀——find() 和 find_all() 解决 80% 的问题,select() 和正则匹配解决剩下的 15%,文档树遍历和 prettify() 是那关键的 5%。掌握这 7 种武器,再配合 requests 的请求能力和 pandas 的数据处理能力,你已经具备了独立完成 90% 爬虫项目的能力。
下一步建议:
动手跑一遍豆瓣 Top250 的代码 尝试爬取自己感兴趣的网站(记得先看 robots.txt) 学习 Scrapy 框架,应对大规模爬取需求
如果这篇文章对你有帮助,欢迎点赞、关注、转发三连! 有任何问题,欢迎在评论区留言,我会一一解答。
(本文仅供学习交流,请遵守各网站的 robots 协议,合理控制请求频率,做有礼貌的爬虫开发者。)
夜雨聆风