尧图精选

Python爬虫实战:requests+BeautifulSoup全站抓取名人名言与作者数据

🕒 发布时间:2026/10/1 4:32:12 📁 来源:尧图网络
爬虫项目写了不少从电商商品到新闻文章从豆瓣书评到微博热搜但给我印象最深的反而是这个看起来最简单的名人名言站。为什么因为这类网站往往结构规整、分页规律、单个页面复杂度低看起来半小时就能搞定可真正从头到尾跑一遍你才发现翻页调度、数据去重、编码乱码、请求频率控制每一个环节都能给你整出新花样。这篇文章我打算把这次“全站名人名言与作者数据”抓取的完整过程拆开揉碎讲清楚包括技术选型为什么这么定、每个步骤背后的设计理由、以及我踩过的坑和最后的排查方案。项目工具链固定为 Python 的 requests 加 BeautifulSoup没有任何花活适合刚学完 Python 基础、想找个真实项目练手的同学也适合需要批量名言语料做文本分析、做聊天机器人素材、做 App 数据源的朋友直接抄作业。1. 项目发起先想清楚要爬什么再动手写代码做爬虫这几年我最大的体会是真正耽误时间的从来不是写代码而是没想清楚就开干。面对一个名人名言站第一步不是打开编辑器而是先明确两件事——这个站适不适合作为目标以及我到底要拿到什么粒度的数据。1.1 为什么拿名人名言站练手名人名言网站是爬虫入门极好的样本数据源原因有三。第一结构规整。绝大多数名言站的列表页都是“名言文本 作者 出处 分类标签”这种固定结构一页二十条左右每条数据都躺在同一个 HTML 模块里。这意味着你只要写对一个提取规则就能机械地复用到整个网站不需要针对不同板块做差异化处理。第二分页机制简单。最常见的是 URL 带页码参数比如 ?page2或者路径带序号比如 /list_2.html几乎不会有复杂的翻页交互。这是练习“全站遍历”逻辑的最佳场景一个循环、一个页码范围、一个终止条件就够了。第三数据维度丰富但量级适中。名人名言通常包含作者、分类励志、爱情、成功等、朝代或国籍、出处作品等字段方便你后续按维度筛选。而这类网站总量通常几千到几万条既不是一页能拿完的“小打小闹”也不至于像电商全站那样动辄千万级需要分布式方案非常适合单机爬虫在合理时间内完成。我在第一次接触这个需求时的判断是这是一个典型的“静态页面 规律分页”场景单机可以轻松吃掉完全不需要上框架、不需要搞队列和调度别把问题复杂化。1.2 目标数据到底长什么样动手之前我习惯先把目标数据结构画出来。名人名言数据的核心字段可以这样定义字段说明类型示例id唯一标识int/strq0001_hashcontent名言正文str“生活就像海洋只有意志坚强的人才能到达彼岸。”author作者名str马克思author_info作者延伸信息可选str德国思想家、哲学家tag分类标签list励志, 坚持source出处/作品str《资本论》url原文页面地址strhttps://example.com/quotes/...注意这里我特意把author_info和source标成了“可选”。原因很实际名言站的数据质量参差不齐有的页面会在作者后面附一行简介有的干脆只有“佚名”两个字有的名言明确标注出自某本书有的根本没有出处。抓取之前就要定好“拿不到时怎么办”的策略是空字符串占位、还是跳过这条记录、还是从别的字段推断我的选择是空字符串占位宁可保留一条部分缺失的记录也不要因为某个字段缺失就丢弃整条数据——后续清洗时再决定怎么补。在实际抓取前我还要做一件事人工点开几个栏目页确认翻页规律是否全区一致。有些名言站的分类页翻页规律和“全部名言”页不一样有的站从第 5 页开始 URL 结构突变有的站列表页有数据但详情页需要单独解析。这些都要先用浏览器当测试工具把 URL 变化记录下来再写循环。这个“先观察、再编码”的习惯能帮你省下至少一半调试时间。2. 技术选型轻装上阵还是全家桶项目目标明确了接下来才进入选型环节。很多人学爬虫时一上来就搜 Scrapy 教程、分布式爬虫方案结果光配置环境就劝退了。我的建议是根据目标站点的复杂度选择刚好够用的技术栈而不是把重型武器都搬出来吓自己。2.1 技术栈requests BeautifulSoup 就够了这个项目我选的是 Python、requests、BeautifulSoup4 lxml、pandas或内置 csv 模块外加 time 模块控制频率。全套纯标准库加三个常用的第三方库环境搭建时间不超过五分钟。每个组件承担什么职责我说清楚requests负责发 HTTP 请求、带请求头、拿响应文本。它是替代 Python 内置 urllib 的主力库接口简洁能自动处理 cookie、重定向、编码探测是爬虫开发者的标配。BeautifulSoup4 lxml负责把 HTML 字符串解析成可查询的树结构。BeautifulSoup 的select方法支持 CSS 选择器提取数据时写起来非常直观lxml 是它的解析引擎解析速度快底层是 C 实现。pandas严格来说项目不用 pandas 也能跑用内置 csv 模块写 CSV 完全可行。但如果你后续想做数据统计、筛选、导出 Excelpandas 的 DataFrame 操作会顺手很多。time控制请求频率后面我会细说为什么这一步是保命的关键。为什么强调“够用就好”因为你每引入一个框架就多一层学习和排错成本。Scrapy 这类框架确实强大但它有自己的数据管道、中间件、Item 模型、Selector 语法这些概念对新手而言是额外的认知负担。写单机爬虫用reponse requests.get(url)这种直白的调用数据流是线性的、透明的出问题你能立刻知道卡在哪一行这是学习阶段最需要的。2.2 为什么没选 Scrapy 和 Selenium这个项目我明确否掉了两个工具。第一个是Scrapy。不是它不好而是因为目标站的体量不需要。Scrapy 的优势在于大规模爬取、并发调度、去重、异常重试、分布式扩展这些能力解决的是“效率和工程化”问题。而名言站只有几万条数据单线程循环加 1 秒延迟跑完也才十个小时左右并且这个时间是可以接受的。如果引入 Scrapy你还要遵守它的项目目录结构、熟悉它的yield和Request回调机制学习曲线一下子陡峭起来。我的结论是新手阶段别让框架的抽象概念掩盖了爬虫的本质——无非就是“请求 → 解析 → 提取 → 存储”四个动作的循环。第二个是Selenium。Selenium 是浏览器自动化工具用来对付那些通过 JavaScript 动态渲染页面、普通 requests 拿不到数据的网站。我一开始也担心名言站的数据是不是动态加载的于是先把首页 HTML 下载下来搜了几个关键词——发现名言内容直接埋在 HTML 源码里。这就意味着这个站是纯静态渲染Selenium 完全没有必要。教训是判断是否需要浏览器渲染工具不要靠猜先把源码打开看一眼。2.3 数据结构和存储格式怎么定存储方案我选了 CSV 作为主格式同时导出 JSON 作为备份。原因有三个CSV 可以用 Excel 直接打开检查、字段增删方便、文件体积小JSON 则保留更完整的嵌套结构比如 tag 可以是列表方便程序二次读取。这里有一个容易忽视的编码细节写 CSV 时必须使用utf-8-sig编码而不是普通的utf-8。原因是 Excel 打开 UTF-8 无 BOM 的文件时中文会全部变成乱码而utf-8-sig会在文件开头加一个 BOM 标记让 Excel 正确识别编码。这个坑我第一次写爬虫时就踩过当时导出的数据在程序里看完全正常发给同事用 Excel 打开直接一团乱码差点让我怀疑人生。import pandas as pd df pd.DataFrame(data_list) df.to_csv(quotes.csv, indexFalse, encodingutf-8-sig) df.to_json(quotes.json, orientrecords, force_asciiFalse, indent2)注意 JSON 导出有两个参数不能漏force_asciiFalse保证中文不被转成 Unicode 转义序列indent2让文件格式可读。2.4 代码模块怎么拆我把整个项目拆成四个函数fetch_page、parse_page、save_data、main。这个分工不是写起来顺手而是为了单点排错——哪一步出问题你只需要检查对应函数。def fetch_page(url, headersNone, retries3): 发送请求并返回 HTML 文本 ... def parse_page(html): 解析 HTML提取名言数据列表 ... def save_data(rows, filenamequotes.csv): 把数据追加写入 CSV ... def main(): 总控循环遍历所有分页并调度上面三个函数 ...fetch_page只负责网络层出错就重试parse_page只关心解析拿不到数据就返回空列表save_data只管持久化。这样设计的好处是当某个页面结构异常导致解析不到数据时你很快能定位是提取规则的问题还是页面本身的问题而不是五个逻辑混在一坨几百行的代码里反复猜。3. 实操攻坚从单页验证到全站收割技术方案定了接下来就是写代码、调试、跑全站。这一节我从第一行代码到完整全站抓取逐步拆解每一个关键点我都会解释“为什么这么做”而不只是给出代码。3.1 第一步伪装请求头爬虫的请求头本质上是向服务器表明“我是一个正常的浏览器用户”。服务器反爬的一个重要手段就是检测请求头里的 User-Agent简称 UA字段。默认的python-requests/2.x.x这种 UA 会直接暴露爬虫身份很多服务器看到就返回 403 拒绝访问。所以第一件事就是伪装 UA。我通常还会把 Accept、Accept-Language、Referer 一起带上这样请求看起来更像真实的浏览器访问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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://example.com/, } response requests.get(url, headersheaders, timeout10)这里有个细节Referer 头的值应该和你请求的页面来源保持一致如果是第一页就没有 Referer这时不填也比乱填好。还有timeout10必须配置否则遇到网络异常时 requests 会一直傻等整个爬虫就像死了一样。我在实际测试中先修改 UA 再请求目标首页状态码从 403 变成了 200说明目标站确实做了 UA 校验。这个排查过程也再次印证了一个经验遇到访问失败第一反应永远是先检查响应状态码和响应头而不是去改解析逻辑。3.2 第二步解析第一条名言拿到 HTML 文本后先别急着写解析代码我建议先打印一段 HTML或者搜索名言内容在源码中出现的位置找到它所在的标签结构。这个操作相当于“解剖麻雀”把目标元素的结构看清楚。以典型的列表页为例一句名言通常嵌套在一个类似div classquotes的容器里里面再有span classtext放名言、span classauthor放作者。用 BeautifulSoup 的 CSS 选择器直接提取from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) for item in soup.select(div.quotes): content item.select_one(.text).get_text(stripTrue) author item.select_one(.author).get_text(stripTrue) # tag 和 source 按需提取如果没有就返回 None这里有两个值得说的习惯。第一选择器要尽量精确到模块而不是全局检索所有.author。页面上可能还有“热门作者”侧边栏里面也有.author元素如果你直接soup.select(.author)把整页所有匹配节点全捞出来数据就串了。必须先定位到div.quotes这个名言模块再在模块内部找子节点。第二get_text(stripTrue)里这个stripTrue一定要加。不加的话从 HTML 里提取出来的文本会带着大量空格和换行符比如“ 生活就像海洋 ”这样的脏数据。加上之后自动去掉首尾空白清洗工作能少一半。我在解析时给每个item打印过前几条结果确认内容、作者、标签都提取正确后才进入下一步。永远先验证单条数据再批量跑全站这句话值得写在你的显示器贴纸上。3.3 第三步翻页逻辑单页解析通了接下来就是“全站”的关键——翻页调度。先说结论我见过的大多数名言站翻页无非两种形式一种是 URL 参数式https://example.com/quotes?page2另一种是路径式https://example.com/quotes/2。我的实现是写一个循环从第 1 页遍历到某个上限每页解析完成后追加保存。但这里有一个重要的工程细节循环的终止条件不能只看“页码范围”。页面总量你可能知道比如 500 页也可能不知道网站一直有新增内容。而且页码存在风险——某些站不存在的页码会返回 200 和空数据而不是 404这就导致“访问了 500 页但实际只有 300 页”时循环继续空转甚至重复写入。我采用的方案是把“本页是否有数据”作为真正的终止条件。如果连续 3 页解析结果为空就认为到达了最后一页主动跳出循环。这样即使页码范围估计错了也不会死循环或者跑进死胡同。page 1 empty_count 0 while True: url fhttps://example.com/quotes?page{page} html fetch_page(url, headers) if html is None: empty_count 1 if empty_count 3: break page 1 time.sleep(1) continue rows parse_page(html) if not rows: empty_count 1 if empty_count 3: break else: empty_count 0 save_data(rows) page 1 time.sleep(random.uniform(0.5, 1.5))顺带说一句为什么time.sleep要用随机间隔而不是固定 1 秒因为固定间隔会被服务器识别为“机器节奏”而随机间隔更接近人类行为。这是反爬识别里最基础的一课也是礼貌爬虫的基本素养——不要给目标服务器造成压力。3.4 第四步清洗、去重、落盘解析出来的原始数据往往带着各种杂质HTML 实体nbsp;、多余换行、全角半角空格、重复的名言。清洗这一步看似琐碎却直接决定你最终拿到的是可用的数据集还是一堆需要再次整理的半成品。我的清洗流程分三层文本层用正则把所有空白字符压缩为一个空格把 HTML 实体替换为正常符号去掉不可见字符。语义层把全角标点统一为半角或反过来看你的下游需求去除因为页面截断产生的半句比如末尾的省略号。结构层打掉重复项。名言类数据天然容易重复同一个作者同一句话可能因为标点不同、繁简不同、空格不同在页面里出现多次。去重我用了一个经典技巧用名言正文的哈希值做唯一键。先把正文做归一化清洗去掉标点、统一大小写、去掉所有空格再计算哈希存进一个 set。每次新增数据前先查 set命中就跳过。import hashlib seen set() def normalize(text): # 去掉标点和空格统一小写用于去重 return re.sub(r[\s\p{P}\p{S}], , text, flagsre.U).lower() def unique_key(content): return hashlib.md5(normalize(content).encode(utf-8)).hexdigest() def is_duplicate(content): key unique_key(content) if key in seen: return True seen.add(key) return False去重逻辑放在“保存前”而不是“解析时”这样即使程序中途崩溃已保存的历史数据也能通过一次加载被重新识别。这一点在后面“断点续爬”里还会用上。3.5 断点续爬让爬虫具备重启能力全站爬取通常要跑数小时如果中途因为网络波动、内存涨满、服务器断连而挂掉重头再来一遍是很崩溃的。所以我在项目里一开始就设计了断点续爬。实现方法不复杂把已经成功抓取的页面页码记录到一个本地文件中。每次启动时读取这个文件从断点页码继续而不是从第 1 页开始。更进一步我把“已处理的名言哈希集合”也序列化保存这样即使断点位置不对、或某个页面重复处理了去重逻辑也能兜住。import json def load_checkpoint(pathcheckpoint.json): try: with open(path, r, encodingutf-8) as f: data json.load(f) return data.get(last_page, 0), set(data.get(seen, [])) except FileNotFoundError: return 0, set() def save_checkpoint(path, last_page, seen): with open(path, w, encodingutf-8) as f: json.dump({last_page: last_page, seen: list(seen)}, f, ensure_asciiFalse)为什么这个设计重要我实际跑测试时在快抓完第 1000 页时因为本地网络抖动连续 5 个请求超时程序直接崩溃。如果没有断点续爬前面几小时的成果就全部报废。有了这个机制重启后从第 1000 页继续半小时内补齐剩余数据。这已经不是优化项而是爬虫程序的及格线。4. 避坑实录那些年我踩过的坑理论部分讲完下面这部分可能是对你最有价值的——我在这个项目里真实遇到的问题、排查思路、以及最终的解决方案。我把它整理成手册你将来写任何爬虫大概率也会遇到类似的坑。4.1 中文乱码的真相用 requests 拿中文网页最可能遇到的坑就是乱码。很多新手第一反应是“网页是 GBK 编码吧改成 gbk 试试”但乱码的根因往往不是你想的那样。requests 在获取响应时会优先根据响应头里的charset字段解码文本如果响应头没写、或者写错了就会用ISO-8859-1去解码中文自然全乱。解决办法是拿到响应后先检查response.encoding和response.apparent_encoding。# 如果乱码优先用 apparent_encoding 重新解码 if response.status_code 200: html response.content.decode(response.apparent_encoding, errorsignore)response.content是原始字节apparent_encoding是根据内容检测出的编码通常基于 chardet 库这两个配合起来基本能覆盖所有中文站的情况。这里我建议直接用字节内容解码而不是依赖response.text因为response.text是基于response.encoding解码的结果如果 encoding 是错的text 就已经是乱码了后续再折腾都没用。还有一个被忽略的乱码来源是lxml 解析器的编码推断。BeautifulSoup 解析字节而不是字符串时会自动尝试编码推断但如果你传给它的是已经按错误编码解码后的字符串那就无力回天了。所以正确姿势是优先用 response.content字节喂给 BeautifulSoup让它自己判断。4.2 被封 IP 前的征兆我在测试过程中体验过一把“被目标站温柔地拒绝”一开始一切正常跑到第 200 多页时响应速度肉眼可见地变慢然后突然一次请求返回了 503 状态码再访问网页版首页直接弹出一个验证码页面。这时我才意识到频率太高触发了对方的反爬策略。总结下来封禁前的常见征兆有这么几类征兆说明应对响应码突变为 403/503/429服务器拒绝了请求停止高频率请求降低频率或换 IP返回 HTML 里出现验证码关键词被判定为机器访问放慢频率重启 Cookie 会话连续多次请求超时IP 可能已被临时限制终止爬取休息一段时间再续跑页面数据量骤减或全是空被降级响应了检查返回 HTML 内容是否被替换我的核心应对策略其实只有一条把请求频率控制放在项目设计阶段而不是等被识别后再补救。具体做法是每页请求间隔控制在 0.8 到 2 秒随机浮动同时始终带完整的浏览器请求头尤其是 Cookie。很多站即使没有登录要求也会给带了 Cookie 的访问者更宽松的限制所以我会先用浏览器打开目标站抓取初始 Cookie然后把它带进爬虫请求。4.3 页面里没有你要的数据名言站的列表页如果只有名言摘要完整正文可能在详情页里这时候你需要做的就是“二次请求”先解析列表页拿到详情页 URL再逐个请求详情页提取完整数据。我在测试中发现目标站的作者简介就在详情页里列表页只有作者名字于是实现了一个二级调度逻辑列表页解析出detail_url塞进队列详情页再提取author_info。def parse_detail(detail_url): html fetch_page(detail_url, headers) soup BeautifulSoup(html, lxml) author_info soup.select_one(.author-intro) return author_info.get_text(stripTrue) if author_info else 不过我也强调不要把所有数据都试图从详情页抓能列表页一步拿到就别多请求一次。每多一次请求就多一分被封风险、多一分耗时。我会先在浏览器里确认列表页源码是否包含所需字段确定缺失后才引入详情页抓取逻辑。4.4 异常处理与日志系统爬虫跑全站最怕两种情况一种是程序静默地失败——请求异常被 catch 后什么都不做继续循环最后积累了半份坏数据另一种是一遇异常就崩——第 20 页挂了整个项目停在那里等你手动重启。成熟的爬虫应该把异常当成“正常情况”对待因为网络环境本身就不可靠。我的做法是三层防御第一层fetch_page内部用try-except包围请求捕获超时、连接错误、响应状态码异常并实现指数退避重试。第二层如果重试 3 次还失败就把这个 URL 记录下来打印警告继续下一项不阻塞整个任务。第三层所有关键动作写日志。不仅写错误日志也写进度日志每 10 页打印一次当前页码和累计条数。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(crawler.log, encodingutf-8), logging.StreamHandler() ] ) for retry in range(3): try: resp requests.get(url, headersheaders, timeout(5, 10)) if resp.status_code 200: return resp.content except requests.RequestException as e: logging.warning(请求失败(第%d次): %s, %s, retry 1, url, e) time.sleep(2 ** retry) # 指数退避2 ** retry这个细节值得说一下重试间隔依次为 1 秒、2 秒、4 秒指数增长既给网络恢复留下时间又不会在服务器已经压力很大时加重负担。4.5 常见问题速查表把这次踩过的坑整理成一张速查表方便你以后直接对照问题可能原因解决方案中文乱码编码识别错误用response.content.decode(apparent_encoding)或把字节直接交给 BeautifulSoup请求被 403 拒绝UA 被识别带完整浏览器头和 Cookie翻页到后面全是空页码不存在或已到终点连续空 3 页终止循环数据重复页面内容重复或分页重叠正文哈希归一化去重程序跑一半崩溃网络异常无容错增加重试、断点续传、日志页面源码里看不到数据数据由 JS 动态渲染用浏览器开发者工具找 XHR 接口或引入 Selenium关于表格里最后一行“JS 动态渲染”我这次没遇到但我在排查时特意留了一个心眼如果在源码里搜不到名言内容我会先检查网络请求里有没有返回 JSON 数据的接口只有确认没有接口才考虑上 Selenium。因为接口方案远比 Selenium 轻量、稳定、快速——很多人一上来就无脑上浏览器自动化反而把简单问题复杂化了。5. 最后分享一点个人经验爬完这几千条名言我的最终成果是一份干净的 CSV、一份 JSON、一个日志文件外加一个正儿八经派上用场的“防崩溃”爬虫脚本。回看整个过程我想把最重要的经验浓缩成三句话。第一先观察再编码。打开浏览器、查看源码、确认分页规律、确认字段位置这些手动的活儿永远比写代码更有价值。我在没有确认页面结构之前就从不肯写提取规则因为我知道结构一变前面的代码全废。第二断点续爬是底线。任何超过十分钟的爬取任务都要在一开始就把断点和日志做进去。因为网络环境是不可控的程序崩溃是必然的而“崩溃后能从哪里接着跑”才是区分专业和业余的分水岭。我自己就靠这一条避免了好几小时的重复劳动。第三控制频率是读书人的本分。爬虫技术本身没有善恶但它实实在在占用着目标站的服务器带宽。把请求间隔调大一点、随机一点带上礼貌的请求头既是为了不被封禁更是因为你没有理由去拖垮一个和你无冤无仇的网站。下次如果你也想抓一个“看起来很简单”的静态网站我希望你能用上这篇文章里的思路先拆需求再选工具写完单页验证再批量最后别忘了日志和断点。这套流程我用了很久至少在国内各种静态内容站上从没翻过车。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →