资讯动态

Python爬虫实战:从零抓取网页数据并保存CSV完整指南

发布时间:2026/9/10 13:42:24 来源:尧图企业网站定制
前阵子帮朋友整理一个书单网站上的书籍信息一开始想着手动复制到表格里结果才复制了不到一百条就觉得不对劲——这活要是有一千条我今晚就别睡了。于是花了两小时用Python写了个小爬虫把整站的书名、价格、评级全部扒下来存成CSV朋友拿到数据的时候整个人都是懵的。这就是Python爬虫最典型的打开方式不复杂但能实实在在把人从重复劳动里解放出来。这篇文章不搞那些花里胡哨的框架和分布式架构就讲清楚一件事用Python写一个能跑、能抓数据、能存下来的简单爬虫到底需要经历哪些步骤。适合刚学完Python基础语法、想动手写点真东西的读者也适合已经在用requests但经常被解析和编码问题卡住的人。我会把从分析网页结构、发出HTTP请求、解析HTML到翻页爬取、保存数据的完整链路都过一遍还会把我自己踩过的坑同步说出来。看完之后你对爬虫的认知就不再是“魔法”而是一条清晰的生产流水线。1. 动手之前先想明白网络爬虫到底在爬什么很多新手一上来就问“爬虫怎么写”但我建议先搞清楚一件事情爬虫拿到的到底是什么。不理解这一点后面所有的代码都可能是在瞎写。1.1 你看到的页面和爬虫拿到的HTML是两套东西你在浏览器里打开一个网站看到的是排版精美、图文并茂的页面但爬虫用requests请求同一个URL拿到的是一大段HTML源代码。这两者之间有什么关系我用一个例子来解释你在餐厅点餐时看到的是菜单上的成品图片那是厨师做好之后的样子而爬虫做的事情更像是直接进后厨翻原材料清单——它拿到的是菜品配料表看不到成品的摆盘和滤镜。浏览器访问一个网页的过程大致是这样输入网址后浏览器向服务器发起HTTP请求服务器返回一段HTML文档浏览器再把这段HTML解析渲染成你看到的页面。爬虫在这个过程中只做一个动作发HTTP请求拿HTML文档。至于拿到之后怎么从HTML里提取出你需要的信息那是解析的工作。这个理解有什么用很关键。因为很多网站的内容是JavaScript动态加载的也就是说服务器返回的HTML文档其实是一个空壳浏览器里的内容是通过后续的JS脚本请求其他接口再渲染上去的。这种情况下你用requests直接抓主页URL拿到的HTML里根本没有你要的数据这就是为什么很多新手爬虫跑出来是空的翻遍代码也找不到逻辑错误。1.2 快速判断一个页面是静态还是动态渲染在写爬虫之前先花三十秒做一次判断能帮你省下不少时间。操作方法很简单用浏览器打开目标页面。在页面空白处点右键选择“查看网页源代码”。按CtrlF输入你准备爬取的某个关键内容比如你看到的某个标题文本。重点来了如果在源代码里能找到这段内容说明数据是直接写在HTML里的静态渲染requests直接能抓。如果找不到说明内容是JS动态加载的你得另想办法。以我实战用的练习站点books.toscrape.com为例我打开页面后按CtrlF搜索一本书的标题能在源代码里直接找到这就意味着爬虫可以走最省力的路子直接请求URL、解析HTML。如果是动态渲染的页面通常有两条路一条是去浏览器的开发者工具F12里的Network面板找XHR请求看看数据是不是来自某个JSON接口找到了就直接请求那个接口反而比解析HTML更简单另一条是用Playwright或Selenium这类无头浏览器工具模拟真实用户操作但这已经超出“简单爬虫”的范畴入门阶段先不展开。“简单爬虫”的第一课是先学会挑简单的目标下手。2. 从零搭一个可用的爬虫环境环境这东西很多人觉得不重要随便装个Python就开始写。等真的开始做项目才发现依赖管理一塌糊涂装个第三方库把系统Python搞坏了又得重来。我建议一开始就养成好习惯后面会省很多事。2.1 虚拟环境是第一个该养成的习惯虚拟环境的作用一句话解释给每个项目单独隔一个房间每个房间里的依赖互不干扰。比如项目A用的是requests 2.28项目B要用requests 2.31如果全装在同一个全局环境里分分钟冲突。用虚拟环境各装各的互不打扰。创建和使用虚拟环境的命令也简单# 创建虚拟环境venv这个名字可以自己改 python -m venv venv # Windows系统激活虚拟环境 venv\Scripts\activate # macOS / Linux系统激活虚拟环境 source venv/bin/activate激活之后你的命令行前面会多出一个小括号提示里面写着venv或者你自己起的名字。看到它就说明当前在虚拟环境里了这时候再装依赖都是装到这个房间里不会污染全局环境。还有一个小提醒如果是新装的电脑第一次在命令行敲python没反应十有八九是安装Python时没勾选“Add Python to PATH”这个选项。重新运行安装程序把那个勾打上就好。这个是很多人卡在第一步的常见原因。2.2 三个核心库的分工写一个简单爬虫依赖其实就三个多一个都不需要库名作用安装命令requests发起HTTP请求拿到网页源代码pip install requestsbeautifulsoup4解析HTML提取需要的数据pip install beautifulsoup4lxml解析器的底层加速引擎pip install lxml安装的时候可以一次性装pip install requests beautifulsoup4 lxml有些教程会顺便让你装pandas说用来存数据。我的建议是入门阶段先别pandas的数据结构对新手来说反而增加负担CSV标准库就够用。等爬虫跑通了再研究数据清洗不迟。关于Python版本我用的是3.10理论上3.8以上都没问题。如果机器上已经装了Anaconda直接用conda的环境也完全可以核心逻辑一样只是环境管理工具不同而已。3. 把目标网页的URL结构看懂比写代码更重要老实说我第一次写爬虫的时候也犯过这个毛病打开编辑器就开始敲代码根本没仔细看URL长什么样。结果就是翻页的逻辑完全拎不清还在代码里用各种奇怪的循环去试。后来才意识到URL里藏着整个网站的数据组织逻辑看懂它爬虫就成功了一半。3.1 URL的组成一条地址就是一个数据接口我们拆解一下https://books.toscrape.com/catalogue/page-1.html这个URLhttps是协议告诉服务器用什么方式通信books.toscrape.com是域名定位到具体的服务器/catalogue/是路径指向服务器上的某个目录page-1.html是具体文件重点在这个page-1。这个URL结构透露了一个非常关键的信息翻页是靠page-后面的数字来实现的。想要爬第二页、第三页不需要模拟点击按钮直接改URL里的数字就行。这就是为什么我说看懂URL比写代码更重要——你花五分钟观察URL规律就能省下两小时研究怎么处理页面交互的时间。还有一类网站用的是查询参数比如常见的?page2或者?keywordpython这种形式原理一样都是通过修改URL的参数来获取不同的数据内容。理解了这个翻页循环的代码就直接写出来了for page in range(1, 51): url fhttps://books.toscrape.com/catalogue/page-{page}.html # 后续的请求和解析逻辑3.2 用开发者工具找数据的真实来源光看URL还不够有些页面的核心数据藏在子链接或者接口里这时候就要打开浏览器的开发者工具来看了。按F12打开开发者工具切到Network网络面板刷新页面你会看到浏览器发出的所有网络请求。在筛选框里选Doc能过滤出主文档请求这个就是页面本身的HTML。如果你在主文档请求的响应里找不到目标数据就去XHR或Fetch分类里翻这些通常就是动态加载数据的接口。点开一个接口按Preview预览或Response响应标签如果能看到JSON格式的数据恭喜你最简单的爬虫路径找到了——直接请求这个接口然后用Python自带的json库解析就行连BeautifulSoup都用不上。这里有句经验之谈能在源码里找到的不要翻接口能在接口里拿到JSON的不要解析HTML。按这个优先级来代码会简单得多。4. requests库的完整请求闭环requests库是Python里最优雅的HTTP客户端库没有之一。它的API设计得非常直观几乎就是你用浏览器访问网页时的思考过程。下面这段代码是我平时写爬虫的固定模板直接用就行。4.1 一个只要复制就能跑的请求模板import requests url https://books.toscrape.com/catalogue/page-1.html headers { 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 } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 状态码不是200时抛出异常 print(resp.status_code) # 200 print(resp.text[:500]) # 打印HTML前500个字符这里有几个点值得单独说一说。第一是headers就是请求头。服务器在收到请求时会先看请求头里的User-Agent——它就像是你的身份介绍信。如果不加这个参数requests默认的UA是python-requests/xxx很多服务器一看就知道是爬虫直接拒绝响应返回403。我习惯直接复制浏览器里最常见的Chrome UA伪装成一个真实访问者。第二是timeout参数这是我最强调的一个点。网络请求是一定会失败的只是时间问题。不加timeout的话一个卡死的连接可能会让程序挂在那里好几十分钟看起来像是死机了。设成10秒十秒内没响应就直接放弃程序继续往下走。第三是raise_for_status()请求状态码见2.2。4.2 状态码与异常处理的判断逻辑服务器对一次HTTP请求的响应状态码就是它给你的回话。几个最常见的状态码含义处理方式200请求成功返回你想要的资源正常解析404页面不存在要么URL写错了要么翻页到头了停止爬取403服务器拒绝访问通常是被反爬虫拦截了检查请求头降低频率500服务器内部错误不是你的问题重试几次再说requests.get()有一个很容易踩的坑即使服务器返回404或者500requests也不会报错它会默默创建一个响应对象返回给你。所以如果你不检查状态码代码就会拿着一个404的错误页面当成正常页面去解析结果啥都找不到你还在那纠结是不是选择器写错了。raise_for_status()就是主动“引爆”错误的开关如果状态码不是200它就会抛出HTTPError异常。配合try/except结构就可以把请求失败和解析逻辑分开处理try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() except requests.exceptions.Timeout: print(f请求超时跳过{url}) except requests.exceptions.RequestException as e: print(f请求出错{e}) # 记录日志继续下一个URL还有一个常用技巧用requests.Session()替代requests.get()。Session对象会在多次请求之间自动保持cookies这对那些需要保持登录状态的网站非常有用。它的用法就是把requests.get(url, ...)换成session.get(url, ...)其他逻辑完全一样。5. 解析HTML把网页从“一篇文档”变成“一张表格”请求拿到的是HTML抓取后就要开始解析了。HTML本身是一堆标签嵌套的文本里面混着大量像div、a、span这样的标记但真正有价值的信息往往藏在元素属性或标签文本里。解析的任务就是把这堆文档里的有用信息提取出来整理成结构化的数据。5.1 为什么优先选BeautifulSoup而不是正则很多刚接触爬虫的人会说“HTML不就是文本吗我直接用正则匹配就行了。”确实在极小规模的场景下正则足够顶用。但正则有一个天生的缺陷它不理解HTML的层级结构。你写了一个正则去匹配标题标签如果页面里其他地方也出现了类似结构的文本就会匹配到一堆脏数据。更惨的是HTML写得不规范时正则会把标签截断数据直接变成乱码。BeautifulSoup的做法是先把HTML解析成一棵文档树然后你通过标签名、类名、ID、属性这些特征去定位元素就像在文件夹里逐层打开目录一样精确而直观。用一个通俗的类比正则像是在一堆砂子里用手淘金BeautifulSoup则是先给你一份矿藏分布图然后告诉你哪一格里有金子。5.2 CSS选择器语法速查BeautifulSoup的.select()方法支持CSS选择器也就是你在写前端样式时使用的那套语法。如果你的爬虫练到后面要接触Scrapy框架这套语法依然通用所以很值得花点时间掌握。最常用的几个选择器选择器写法含义示例tag按标签名选择soup.select(h3).class按类名选择soup.select(.product_pod)#id按ID选择soup.select(#content)tag.class同时限定标签和类名soup.select(p.price_color)parent child选择父元素下的直接子元素soup.select(div a)[attrvalue]按属性筛选soup.select([title...])实际解析的代码大概长这样from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) # 找到所有书籍卡片每本书是一个article标签且class为product_pod books soup.select(article.product_pod) for book in books: # 书名在h3标签下的a标签的title属性里 title book.h3.a[title] # 价格在class为price_color的p标签里 price book.select_one(p.price_color).get_text() # 星级在class为star-rating的p标签的class列表里 rating book.select_one(p.star-rating)[class][1] print(title, price, rating)select_one和select的区别是前者只返回第一个匹配的元素后者返回所有匹配的列表。拿到元素之后用[属性名]取HTML属性用.get_text()取元素内的纯文本内容。这套组合拳能覆盖绝大多数解析场景。5.3 lxml和html.parser选哪个在写BeautifulSoup(html, lxml)时第二个参数指定了解析器。lxml底层是C语言库libxml2的Python绑定解析速度非常快。html.parser是Python标准库自带的解析器纯Python实现速度稍微慢一点但胜在不需要额外装依赖、通用性好。我的建议是默认用lxml遇到安装问题比如某些老平台上编译失败或者诡异的解析异常时直接改成html.parser试试不需要改其他代码。因为BeautifulSoup的API对解析器是透明的统一都叫soup底层换引擎不影响你的调用方式。6. 实战完整抓取一个列表页并保存到CSV前面讲了半天的理论现在来一个完整的实战项目。目标是爬取https://books.toscrape.com这个专门给爬虫学习者练习用的虚拟书店网站抓取全站50页的书籍列表保存书名、价格、评级三个字段到CSV文件。6.1 为什么选这个网站做练习因为它天生就是为了解决“新手练爬虫找不到合法目标”这个痛点而存在的。整站内容完全公开没有登录墙没有复杂反爬数据量适中大概一千本书页面结构规整而且它自带robots协议允许爬虫访问。用它练习你学到的技术是通用的同时又不至于惹上法律麻烦。6.2 第一步先人工观察页面结构拿到任何目标网站第一件事都不是写代码而是用浏览器打开它看看页面上有哪些数据值得爬。打开books.toscrape.com你会发现每本书是一个卡片上面有书名、价格、星级。按F12键对着任意一本书点右键→“检查”就能看到这本书对应的HTML结构article classproduct_pod h3a href... titleA Light in the AtticA Light in the Attic/a/h3 p classprice_color£51.77/p p classstar-rating Threei classicon-star/i/p /article观察完毕我们需要的关键信息就藏在书名在h3 a的title属性里价格在p.price_color的文本里星级在p.star-rating的class列表里比如Three代表三星。有了这个地图解析代码就很好写了。6.3 第二步写完整的爬虫代码import time import csv import requests from bs4 import BeautifulSoup headers { 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 } def fetch_page(page_num): url fhttps://books.toscrape.com/catalogue/page-{page_num}.html resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return BeautifulSoup(resp.text, lxml) def parse_books(soup): books [] for book in soup.select(article.product_pod): title book.h3.a[title] price book.select_one(p.price_color).get_text().replace(£, ) rating book.select_one(p.star-rating)[class][1] books.append([title, price, rating]) return books all_books [] for page in range(1, 51): try: soup fetch_page(page) page_books parse_books(soup) all_books.extend(page_books) print(f第{page}页抓到{len(page_books)}本书) except Exception as e: print(f第{page}页出错: {e}) time.sleep(1) # 控制请求频率做一个礼貌的爬虫 with open(books.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([书名, 价格, 评级]) writer.writerows(all_books) print(f完成共抓取 {len(all_books)} 本书)这段代码里有个细节值得说明price替换掉了£符号因为在CSV里存纯数字后续做数据分析比如算平均价格会方便很多。这是一个很小但非常实用的数据处理习惯在源头就清理格式比事后清洗舒服得多。6.4 第三步运行和确认结果跑完这段代码后同级目录下会生成一个books.csv文件。用Excel打开你会看到表头加1000条数据书名、价格、评级整整齐齐排列。程序最后打印的总数量如果接近1000说明50页全部抓完了。我提醒一个在Windows上特别常见的坑如果CSV里的中文显示乱码问题出在编码上。utf-8编码的文件在Excel里默认按ANSI解码中文就变成一堆乱码了。解决办法就是我代码里用的encodingutf-8-sig——它会在文件开头加一个BOM标记Excel识别到这个标记就知道按UTF-8解码了。这个坑我当年踩过折腾了大半小时。7. 容易被忽略但早晚会踩的坑编码、请求头、字段缺失爬虫新手最容易翻车的往往不是解析逻辑而是那些看起来不起眼的细节。我在前面已经提到了一部分这里集中把最容易踩的三个坑展开讲讲。7.1 乱码问题的根因和解决套路你在用requests拿到resp.text时requests会自己猜测网页用的什么编码。它优先看响应头里的charset字段如果没有就默认按ISO-8859-1拉丁字符集解码。问题是很多中文网站根本不在响应头里写charset或者写的是charsetgbk就这样一种情况按错误编码解码屏幕上就会出现成片的火星文。解决套路很简单两行代码resp.encoding resp.apparent_encodingapparent_encoding是requests用chardet库从页面内容本身推断出来的编码基本可靠。如果推断出来还是乱的那就得手动指定比如直接resp.encoding gbk或者utf-8再打印文本看对不对。我的经验顺序是默认UTF-8 → 乱码就用apparent_encoding→ 还乱就逐个尝试常见编码。7.2 字段缺失会让程序直接崩溃网页数据并不总是规整的。一本书可能恰好没有价格标签某篇文章恰好没有作者信息这时候你用book.select_one(p.price_color).get_text()去提取select_one返回的是None然后对None调用get_text()程序就直接抛AttributeError崩了整个爬虫中断。稳妥的做法是写一个安全提取函数def safe_extract(parent, selector, attrNone, default): element parent.select_one(selector) if element is None: return default if attr: return element.get(attr, default) return element.get_text(stripTrue)这样即使缺了某个字段程序也会返回一个空字符串而不是崩溃。数据完整性的另一个好习惯是爬完之后用pandas或者Excel看一眼是否有空值缺的字段回去补而不是让程序白跑一趟。7.3 控制抓取速度别把对方服务器当自家后院很多网站的服务器并没有多强劲频繁的并发请求会让它响应变慢严重的会被当成攻击行为直接封IP。一个礼貌的爬虫应该控制请求频率最简单的方式就是循环里加一行time.sleep(1) # 每秒一个请求遇到重要页面再写个简单的重试机制for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() break except requests.exceptions.RequestException: if attempt 2: raise # 第三次还失败放弃 time.sleep(2 * (attempt 1)) # 指数退避这行代码在实战里真的非常有用——因为网络请求是一个天然不稳定的操作服务器抖动、网络波动都可能让某一两次请求失败重试一下就好了这个逻辑在处理长列表、多页码的数据时能有效减少中断次数。8. 爬虫的边界哪些能爬哪些不能碰技术本身是中性的但怎么使用技术是有边界的。我接触过一些想学爬虫的同学上来就问怎么爬抖音、怎么爬电商平台的商品数据。我的统一回复是先学会爬公开网页再谈商业数据采集。商业站点几乎都有严密的反爬机制和明确的服务条款限制硬来不仅技术上打不过法律上也会把自己埋进去。8.1 robots.txt网站给你的爬虫许可单每个正儿八经的网站一般都会在域名根目录放一个robots.txt文件它用专门的语法告诉爬虫哪些路径可以访问、哪些路径禁止访问。你可以在浏览器地址栏直接输入站点域名/robots.txt查看。比如https://books.toscrape.com/robots.txt打开后你会看到类似这样的内容User-agent: * Allow: / Disallow: /catalogue/User-agent: *表示规则适用于所有爬虫Allow: /表示允许访问根目录Disallow: /catalogue/表示禁止访问catalogue目录下的一切内容。如果你的爬虫是商业用途或者数据量很大就必须严格遵守这些规则。这里我要多说一句遵守robots.txt不是技术问题是应不应该做的问题。对个人学习用途偶尔抓取一些公开数据只要频率低、不碰敏感数据、不给服务器添麻烦一般问题不大。但如果要大规模爬取尤其是用于商业变现就必须提前研究清楚目标站点的服务条款和相关法律规定。个人信息保护法实施后抓取个人身份信息、联系方式这类数据面临的法律风险比过去高得多绝对不能碰。8.2 爬虫技术的正途在哪里说完了不能碰的再说说能做什么。爬虫技术在很多场景下是完全正当且高效的搜索引擎聚合公开网页内容、监控公开市场行情和竞品公开报价、定时抓取天气或公共数据接口、整理学术论文公开摘要、自动同步博客文章到自己的站点。books.toscrape.com本身就是爬虫学习社区专门搭的练习站点用这类数据源练手又安全又能学到真东西。想往深处发展的读者下一步可以这样走把爬下来的数据存进SQLite或MySQL学习基本的数据管理研究一下Scrapy框架比手写requests循环更工程化支持并发、中间件、管道学点pandas把爬下来的数据做清洗和分析让数据产生真正价值遇到动态加载的页面研究一下Playwright这类工具它们能做浏览器自动化但不要拿来做批量采集。技术可以让你看到更大的数据世界但前提是守规矩、知分寸。爬虫解决的是公开资源的收集问题真正的难点永远在于能不能把数据转化为洞察。我到现在还保留着一个习惯每爬完一个网站先检查一遍robots.txt再检查一遍自己的请求频率这跟技术高低无关纯粹是不想给别人添麻烦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价