Python爬虫实战:requests+BeautifulSoup+lxml构建采集器 简介这份资源面向具备一定编程基础、希望系统掌握网络数据采集的开发者与学习者围绕Python爬虫从基础概念到实战应用展开帮助读者理解自动化抓取工具的工作原理并落地到实际项目。内容涵盖发送请求、解析网页、数据存储三大核心环节重点讲解requests、BeautifulSoup、lxml与Scrapy等常用库的特点与适用场景并延伸至动态加载处理、反爬应对、效率优化以及法律法规与隐私保护等规范议题。资源包共1个docx文件约16KB以文档形式集中呈现知识要点与代码示例便于按章节查阅和对照练习。目前已有629人学习适合需要批量获取公开数据的研究人员、构建数据挖掘或信息监测系统的开发者以及希望将爬虫技术应用于个人小项目的学习者参考在合法合规前提下提升数据采集效率。1. 从一份爬虫笔记说起为什么我把它当项目脚手架用前阵子接手一个数据聚合的活儿需求很朴素从几个公开页面上把标题、链接、发布时间扒下来清洗后入库。我翻出自己压箱底的一份 Python 爬虫笔记本来只想抄两段requests的调用结果发现这份材料从 HTTP 请求、HTML 解析、数据落盘一路讲到动态渲染和并发抓取结构完整得可以直接当项目脚手架。它讲的不是某个具体网站怎么爬而是把「发请求 → 解析 → 存储 → 提速 → 合规」这条链路拆开揉碎每个环节都给了能跑的代码。适合谁刚学完 Python 语法、想动手抓点真实数据的新手以及需要快速搭一个采集原型、后面再往 Scrapy 或异步方案上迁的老手。我按它的骨架搭了个小采集器两小时跑通这篇就把我拆解和改造的过程写下来。2. 技术选型与目录结构requests BeautifulSoup lxml 的分工2.1 为什么是这套组合而不是一上来就 Scrapy这份材料的技术栈很克制请求层用requests解析层用BeautifulSoup和lxml存储层用pandas落 Excel规模化再上Scrapy。这个顺序是有讲究的。requests的 API 极简response.text一行拿到 HTML新手不用理解连接池、会话保持这些概念就能跑通第一个请求。BeautifulSoup容错性强遇到闭合标签乱七八糟的页面不会直接崩适合练手lxml基于 C 实现解析速度比纯 Python 的解析器快一个量级还支持 XPath等数据量上来再切过去。我一般会跟人说先用requests BeautifulSoup把单页抓取跑通确认目标数据结构稳定再考虑要不要上Scrapy。Scrapy的 Item、Spider、Pipeline 是一整套规则学习曲线陡如果只是抓几百条数据杀鸡用牛刀反而拖慢进度。这份材料把Scrapy放在「常用库」最后讲定位是「大规模数据」的解决方案这个取舍是对的。2.2 我按材料搭出来的目录结构材料本身是笔记体没有给工程目录。我按它的模块划分整理成下面这样每个文件对应一个职责改起来不会牵一发动全身spider_demo/ ├── config.py # URL、请求头、间隔时间等配置 ├── fetcher.py # 请求层封装 requests统一加 headers 和重试 ├── parser.py # 解析层BeautifulSoup / lxml 提取字段 ├── storage.py # 存储层写 Excel / CSV / 数据库 ├── pipeline.py # 串联抓取 → 解析 → 存储 └── requirements.txt # 依赖清单这个结构的好处是当你要换解析库时只动parser.py要换存储目标时只动storage.py请求层的重试和限速逻辑集中在fetcher.py。材料里把「发送请求、解析网页、数据存储」列为爬虫三大核心技术这个目录就是按这三块切的一一对应。2.3 依赖清单与版本注意requirements.txt里至少要锁这几个requests beautifulsoup4 lxml pandas openpyxlpandas写 Excel 依赖openpyxl这个容易漏漏了会在to_excel时报ModuleNotFoundError。lxml在部分环境需要编译工具链如果pip install lxml卡住常见做法是先用beautifulsoup4自带的html.parser顶着等环境配好再切。材料里没提这些依赖细节但实际搭的时候这几步绕不开。3. 核心链路拆解一个请求从发出到落盘的完整代码3.1 请求层headers 和超时是保命配置材料里给的requests.get(url)是最简形态直接拿去跑真实站点大概率吃闭门羹。我在fetcher.py里补了请求头和超时import requests import time HEADERS { # 模拟正常浏览器缺失 UA 是最常见的被拒原因 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch(url, retry3, interval1.5): 带重试和间隔的请求封装 for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 4xx/5xx 直接抛异常 resp.encoding resp.apparent_encoding # 修正中文乱码 return resp.text except requests.RequestException as e: print(f第 {i1} 次失败: {e}) time.sleep(interval * (i 1)) # 退避越失败等越久 return None逻辑说明timeout10防止某个请求卡死拖垮整个流程raise_for_status()把 HTTP 错误码转成异常交给except统一处理apparent_encoding是根据内容猜编码比默认的resp.encoding更靠谱中文页面乱码十有八九是这里没设。interval * (i 1)是简单的退避策略第一次失败等 1.5 秒第二次等 3 秒给目标站点喘息空间也降低被封的概率。参数上retry别设太大3 次足够再多说明目标站点已经明确拒绝你了硬刚没意义。3.2 解析层BeautifulSoup 和 lxml 各管一段材料里BeautifulSoup用find_all按标签抓lxml用 XPath 精确定位。我把两者放在parser.py里按页面复杂度选用from bs4 import BeautifulSoup from lxml import etree def parse_by_soup(html): 结构规整的页面用 BeautifulSoup容错好 soup BeautifulSoup(html, html.parser) items [] for h in soup.find_all(h1): items.append({title: h.get_text(stripTrue)}) return items def parse_by_xpath(html): 字段嵌套深、需要精确定位时用 lxml XPath tree etree.HTML(html) titles tree.xpath(//h1/text()) links tree.xpath(//a/href) return [{title: t, link: l} for t, l in zip(titles, links)]逻辑说明get_text(stripTrue)会去掉标签内多余的空白和换行直接拿到干净文本xpath(//a/href)里的href表示取属性值而不是文本这是 XPath 比find_all灵活的地方。参数上etree.HTML()会自动补全缺失的html、body标签所以哪怕拿到的是片段 HTML 也能解析。选哪个页面结构稳定、字段少BeautifulSoup写起来快字段藏在多层div里、需要按属性筛选lxml的 XPath 一行顶十行。3.3 存储层先落 Excel再考虑数据库材料里用pandas把数据写 Excel这个选择对原型阶段很务实import pandas as pd def save_to_excel(records, pathoutput.xlsx): records 是 list[dict]每个 dict 是一条记录 if not records: print(没有数据可写) return df pd.DataFrame(records) df.drop_duplicates(subset[title], inplaceTrue) # 按标题去重 df.to_excel(path, indexFalse) print(f已写入 {len(df)} 条到 {path})逻辑说明pd.DataFrame(records)直接把字典列表转成表格键自动变成列名drop_duplicates按标题去重爬虫重复抓同一页时这步能省掉很多脏数据indexFalse避免多出一列无意义的行号。参数上subset指定按哪几列判重如果标题可能重复但链接唯一就改成subset[link]。数据量超过几万条后 Excel 会变慢这时候再换 SQLite 或 MySQLpandas的to_sql接口能平滑迁移。3.4 串联pipeline 把三段接起来from fetcher import fetch from parser import parse_by_soup from storage import save_to_excel def run(urls): all_records [] for url in urls: html fetch(url) if html: all_records.extend(parse_by_soup(html)) save_to_excel(all_records) if __name__ __main__: run([https://www.example.com/page1, https://www.example.com/page2])这段是整个项目的入口逻辑就是「逐个 URL 抓取 → 解析 → 汇总 → 落盘」。if html这个判断不能省fetch失败返回None直接丢给解析器会报TypeError。要加新站点就在run里换解析函数要加并发就把for循环换成线程池下一章讲。4. 动态页面与并发提速Selenium 和线程池怎么接进来4.1 动态加载requests 拿不到的内容交给 Selenium材料里点出了一个关键问题AJAX 和 JavaScript 异步加载的内容requests拿到的 HTML 里根本没有。这时候得上Selenium模拟浏览器from selenium import webdriver from selenium.webdriver.chrome.options import Options def fetch_dynamic(url): opts Options() opts.add_argument(--headless) # 无头模式不弹窗口 opts.add_argument(--disable-gpu) driver webdriver.Chrome(optionsopts) try: driver.get(url) # 滚动到底部触发懒加载 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) import time; time.sleep(2) # 等异步请求返回 return driver.page_source finally: driver.quit() # 必须关否则进程残留逻辑说明--headless让浏览器在后台跑服务器上没图形界面也能用execute_script执行滚动很多页面要滚到底才加载后续内容time.sleep(2)是等 JS 执行完这个值要按页面实际速度调设短了拿到空数据设长了拖慢整体。finally里的driver.quit()是血泪经验不写的话每抓一次就留一个 Chrome 进程跑几十次机器就卡死了。参数上webdriver.Chrome()需要本机有对应版本的 ChromeDriver版本不匹配会直接报错这是新手最容易翻车的地方。4.2 并发抓取线程池比手写 Thread 省心材料里用threading.Thread手动管理线程能跑但代码啰嗦。我一般用concurrent.futures的线程池from concurrent.futures import ThreadPoolExecutor, as_completed from fetcher import fetch from parser import parse_by_soup def crawl_all(urls, max_workers5): results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {pool.submit(fetch, u): u for u in urls} for future in as_completed(future_map): url future_map[future] html future.result() if html: results.extend(parse_by_soup(html)) return results逻辑说明max_workers5表示同时最多 5 个请求在跑这个数别贪大设成 20、50 反而容易触发目标站点的频率限制5 到 10 是比较稳的区间。as_completed按完成顺序取结果谁先回来先处理不用等最慢的那个。future_map把 future 对象和 URL 对应起来出错时能定位是哪个 URL 挂了。参数上如果目标站点响应慢可以适当调大max_workers如果频繁收到 429 状态码说明太快了往下调。4.3 反爬应对间隔、请求头、代理三件套材料里列了设置请求间隔、模拟正常用户、使用代理 IP 三条。落到代码上间隔用time.sleep控制请求头在fetcher.py里统一加代理则在requests.get里传proxies参数。这里要提醒一句代理 IP 的获取和使用要遵守目标站点的规定别拿它去绕明确禁止抓取的站点。材料里还提到验证码处理这个在实际项目里通常是遇到才处理提前准备意义不大因为验证码形态千变万化。5. 避坑与排查那些让我重跑三遍的坑5.1 中文乱码现象是标题变成一串问号现象抓下来的中文全是????或者乱码方块。原因requests默认按 HTTP 响应头里的charset解码很多站点不写或者写错导致解码用错编码表。解决在fetch里加resp.encoding resp.apparent_encoding让requests根据实际字节内容猜编码。如果还乱就手动指定resp.encoding utf-8或gbk试。5.2 拿到空列表解析器返回空但请求是成功的现象fetch返回了 HTML但find_all(h1)结果是空列表。原因目标内容根本不在初始 HTML 里是 JS 渲染出来的requests拿到的只是骨架。解决先用浏览器开发者工具看「查看源代码」里有没有目标内容没有就说明要上Selenium。另一个常见原因是选择器写错比如类名里带空格find_all(div, class_a b)要写成列表[a, b]。5.3 请求被拒状态码 403 或 429现象跑着跑着开始返回 403 或 429。原因403 通常是请求头缺失或 UA 被识别为爬虫429 是请求频率超了。解决403 补全User-Agent、Accept-Language等头429 降低并发数、加大time.sleep间隔。如果补了头还是 403检查是不是漏了Referer有些站点会校验来源页。5.4 Selenium 报驱动版本不匹配现象SessionNotCreatedException提示 ChromeDriver 版本和 Chrome 对不上。原因Chrome 自动更新后本地的 ChromeDriver 还是旧版。解决用webdriver-manager自动管理驱动版本或者手动下载对应版本替换。这个坑几乎每个用 Selenium 的人都踩过属于玄学范畴版本号差一位就跑不起来。5.5 数据重复同一页被抓了多次现象Excel 里同一条数据出现好几遍。原因分页逻辑没去重或者重试机制把成功的请求又发了一遍。解决在存储层用drop_duplicates兜底同时在pipeline里维护一个已抓 URL 的集合抓过的直接跳过。重试逻辑里也要判断只有失败才重试别把成功的也重发。6. 二次开发把这份笔记改造成自己的采集器拿到这份材料后最省力的改造路径是「先跑通单页再扩到多页最后加调度」。第一步把config.py里的 URL 换成你真正要抓的页面跑一遍pipeline.py确认能拿到数据、能落盘。这一步别急着加并发单线程跑通最重要因为并发会把错误信息搅在一起新手很难定位。第二步扩多页。如果目标站点是page1、page2这种规律 URL直接在run里生成 URL 列表如果是「下一页」按钮就得先解析出下一页链接再递归抓。这里我一般会加一个max_pages参数防止不小心抓了几千页。第三步加调度和增量。最简单的做法是记录已抓 URL 到一个文本文件每次启动先读进来抓过的跳过。再进一步用 SQLite 存抓取记录字段包括 URL、抓取时间、状态这样断点续抓和去重都方便。第四步按需替换组件。解析规则变了就改parser.py要存数据库就把storage.py里的to_excel换成to_sql要上规模就把线程池换成Scrapy或asyncio。因为目录是按职责切的换哪块都不影响其他部分。验证改造是否成功我有个笨办法但很管用拿一个只有 3 条数据的测试页面跑人工核对抓下来的字段和页面上的内容是否一一对应。字段对上了再换真实页面。从那以后我每次改解析规则都强制先跑这个小测试页确认没把选择器改坏再上量。希望这份拆解能帮你少走点弯路。本文还有配套的精品资源点击获取