从零开始学Python爬虫:用MIT新闻实战带你打通数据采集全流程
发布时间:2026/10/6 13:40:44 锦皓数字建站

聊到爬虫入门十个教程里有八个让你拿豆瓣或者某个电商站点练手结果一上来就被登录墙、字体反爬和验证码轮番劝退。我带了这么多年新人最常用的建议反而不是教他写复杂的调度代码而是先找一个“耐爬”的目标。MIT News麻省理工学院新闻就是这样一个被低估的练习场页面是服务端直出的标准HTML不需要登录内容全部公开结构上又是最典型的“列表页到详情页”链路。这篇实战就围绕它展开用requests拿页面、BeautifulSoup解析、lxml当底层解析器把新闻列表页里的详情链接抽出来再进到每篇文章里抽取标题、发布日期、摘要和正文最后落成JSON和CSV文件。刚会写Python、想找个真实项目动手的新手可以直接照着做想系统梳理一遍“发送请求—解析HTML—抽取字段—保存数据”全流程的人这篇也能帮你把知识串起来。1. 为什么拿MIT News练手一个“耐爬”的站点比炫技更重要1.1 爬虫新手真正的门槛不是代码而是目标站点的复杂程度先说结论初级爬虫最常见的失败不是不会写requests.get()而是选错了对象。很多常见练习目标对新手来说都偏重了豆瓣的列表页会挡登录部分电商站点做过字体反爬不少资讯类应用是JS动态渲染的。你发一个请求过去返回的HTML里要么看不到数据要么直接给你跳去登录页要么是一堆混淆后的乱码字符。这其实不叫爬虫难这叫“你选的对手超出了你当前的等级”。好爬的站点通常有几个通用特征第一HTML是服务端渲染的内容直接写在响应体里不需要执行JavaScript才能拿到第二核心数据不需要登录就能访问第三页面结构语义化程度高标题就是h1列表就是一组带链接的卡片第四站点本身流量没那么大不会对普通爬虫做特别激进的防护。MIT News几乎全中。我拿它做教学案例很多次亲测requests返回的HTML里就能看到完整新闻列表抽取逻辑干净非常适合把精力花在“怎么解析”而不是“怎么绕过去”上。新手常遇到的几个卡点和MIT News的实际情况可以简单对一下常见卡点典型表现MIT News的情况登录墙请求返回登录页公开新闻无需登录JS渲染HTML里找不到数据服务端直出响应即内容字体反爬文本乱码或错位普通HTML文本无混淆数据藏在接口里需逆向分析XHR静态HTML里自带全量内容反爬风控频繁出现验证码正常频率访问即可稳定抓取这不是说MIT News完全不会做任何防护而是说它作为教学对象不会让新手一上来就撞上一堆和“爬虫基础逻辑”无关的难题。先把最核心的链路练会后面再碰那些重防护站点至少你知道问题出在哪一层。1.2 新闻站的标配结构列表页给线索详情页给全量内容网站再多变新闻类站点的信息架构基本是同一个模子。列表页展示一组文章卡片每张卡片上有标题、摘要、发布日期和一个详情链接点进详情页之后才能看到完整正文、作者、分类这些字段。所以爬新闻站的通用拆解只有两步第一步从列表页把每篇文章的详情URL收集出来第二步逐个请求详情页按字段抽取内容。MIT News正是这个模子很标准的样本。它的详情页URL带有年份和文章slug一眼就能识别形如news.mit.edu/2025/xxx这样的结构。这让初学者可以用一条简单的路径规则过滤掉导航链接而不需要先深入理解复杂的选择器。等你把这条流程跑通再换任何新闻站只需要换“列表页怎么选、详情页怎么取字段”这两处骨架完全不用动。1.3 明确边界这次实操到底做到哪一步动手之前先把目标缩小。这篇文章不会去抓图片、PDF、历史全量新闻也不会扯分布式。你要完成的就三件事打开MIT News首页拿到当前页面上的新闻条目对每一条请求详情页抽取标题、日期、摘要、正文把结果保存为文件。这个范围对练手刚刚好既能覆盖爬虫最核心的完整链路又不会被各种边缘情况拖垮。另外建议你用“写一次就当成一个可复用脚本”的心态来做。不要写死在某个URL上把请求函数、解析函数拆开以后换成别的新闻站复用成本最低。真实工程里爬虫改版是常态网站不会永远不变你今天写好的class选择器可能过段时间就失效。所以从第一天起就要养成“按功能拆函数、按真实HTML去调”的习惯这比多背几个API有用得多。2. 环境准备三个库加一个DevTools就能起飞2.1 装着三个库就够了依赖越少初学越不容易被环境劝退。我建议Python 3.9以上版本然后装这三个pip install requests beautifulsoup4 lxml三个库各管一件事requests负责发HTTP请求拿到HTML字符串beautifulsoup4负责把HTML字符串变成可以查询的对象lxml是底层解析器beautifulsoup4在解析时会调用它。初学者最容易忽略的是lxml不装也能跑但bs4会退回默认的Python内置解析器遇到不规范HTML时你拿到的解析结果会差不少。新闻站的HTML通常是机器生成的标签嵌套偶有不规范lxml的容错性明显更好所以哪怕多花几秒安装也值。2.2 解析器决定了你后面能拿到什么这里多说一句原理。BeautifulSoup本身并不解析HTML它只是把解析工作交给背后的引擎。lxml基于C语言实现解析速度比纯Python实现快一个量级更重要的是它会自动处理不闭合的标签、修正错位的嵌套关系让混乱的HTML“结构化”。这对初级爬虫是巨大的便利——你不需要把HTML规范里的每条细节都背下来只要知道“我要找的那个元素大概长什么样”就行。代码里的固定写法就一行from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml)这个soup对象之后可以调用select()、find()、find_all()这些方法。我会在后面的代码里逐步用到。顺便提醒一句Windows上如果安装lxml报编译错误先升级pip或者换回官方发行版Python基本都能解决。2.3 动手前先在DevTools里“验货”写爬虫前我强烈建议你先用浏览器打开目标页面按F12进入DevTools的Elements面板亲手翻一翻HTML结构。新手常见的问题是不先看真实结构直接凭感觉写选择器结果定位不到元素就怀疑是库的问题。其实你只需要多花两分钟在页面上找到你想抓的那篇文章标题右键检查就能看到它在HTML里的完整位置。操作方法不复杂在Elements面板里按CtrlF输入页面上某个新闻标题的文字浏览器会直接帮你定位到对应的HTML节点。这时候看它外层包着什么标签、有没有class、旁边的日期是什么结构、详情链接的href长什么样心里就有底了。我把这叫“验货”好比去菜市场之前先搞清楚菜是怎么摆的再写采购清单。不然你看不见货架就只能在代码里瞎猜选择器。比如你看到某篇文章标题的结构是a href/2025/xxx标题/a那你下一步就清楚了详情页请求https://news.mit.edu/2025/xxx就行列表页解析要做的就是把这些带年份路径的链接捞出来。这就是“先看目标再动手”的价值之后所有解析代码都是在这个基础上生长出来的。2.4 XPath与CSS选择器先精通一种就够熟悉爬虫话题的人应该经常看到python xpath爬虫 text函数这类热搜词。XPath确实是很常用的技术它和BeautifulSoup里的CSS选择器一样都是为了解决同一个问题把HTML里的某个字段映射成一条可执行的查询表达式。比如找标题节点CSS写法是h1XPath写法是//h1。我的建议是初学者没必要同时啃两套语法容易混。先把select()和find()这一套用熟遇到复杂节点再补XPath。这篇实战教程统一使用BeautifulSoup的CSS选择器你跑通之后换成XPath也只是替换查询表达式的问题核心逻辑完全一样。3. 列表页解析从HTML堆里找出每一篇新闻的链接3.1 写一个带headers的请求函数先写最基础的请求层。这个函数要做三件事带一个合理的User-Agent、设置超时时间、对异常状态码快速报错。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Accept-Language: en-US,en;q0.9,zh-CN;q0.8, } def fetch(url: str) - str: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text为什么不裸调requests.get()因为不少服务器会默认拒绝没有User-Agent的客户端或者返回不同的内容版本。你可以简单理解成不带UA去敲门服务器不知道来的是谁可能直接不回应带一个正常浏览器的UA至少让请求看起来像一次普通访问。raise_for_status()同样关键一旦状态码不是200它会立刻抛异常这样你不会拿着一个404页面继续解析半天。timeout也很重要没有它某个慢响应可能让你的脚本一直挂在原地。3.2 用“先找容器再抽链接”的方式定位文章卡片拿到首页HTML后先实例化souphtml fetch(https://news.mit.edu/) soup BeautifulSoup(html, lxml)接下来是列表页解析的关键思路不要一上来就soup.find_all(a)把全站链接都收集起来那会把导航、页脚、相关推荐全卷进来。更可靠的方式是先找“文章卡片”的容器。大部分新闻站里每篇新闻都会放在一个article标签或一个带特定class的div里。你可以先用一个很朴素的方式articles soup.find_all(article) raw_links [] for article in articles: a_tag article.find(a, hrefTrue) if a_tag: raw_links.append(a_tag[href]) print(从article容器里拿到的链接数:, len(raw_links))我先选article是因为它的语意就是“文章”在语义化良好的新闻站里命中率很高。如果你发现拿到链接数明显少于页面上看到的文章数就去DevTools里看一下卡片实际用的什么标签可能是div带某个class把find_all里的参数换成真实结构就行。这一步的目的是先把范围圈住避免后面数据里混入大量无关URL。3.3 去重与补全URL两个必须养成的习惯列表页里同一篇文章常常会出现多个入口标题一个、摘要一个、“阅读全文”按钮又一个。如果不去重后面会重复请求详情页既浪费服务器带宽也污染自己的数据。去重很简单用set临时存一下再转回list。同时HTML里的href常常是相对路径比如/2025/xxx直接拿它请求会失败需要用urllib.parse.urljoin补全成绝对地址。我常用的写法是先用正则过滤出形如“年份/slug”的路径再去重和补全from urllib.parse import urljoin import re BASE https://news.mit.edu pattern re.compile(r/\d{4}/[a-z0-9-]) all_hrefs [a.get(href) for a in soup.select(a[href]) if a.get(href)] news_urls [] seen set() for href in all_hrefs: if pattern.search(href): full_url urljoin(BASE, href) if full_url not in seen: seen.add(full_url) news_urls.append(full_url) print(去重后的新闻链接数量:, len(news_urls)) for u in news_urls[:5]: print(u)urljoin会聪明地处理/2025/xxx和2025/xxx这类差异始终以域名根路径为基准拼接。这个小函数几乎每个手写爬虫项目都会用到值得背下来。正则里的[a-z0-9-]对应文章slug也就是标题的URL化写法。如果某些站点的slug里有大写字母就补一个A-Za-z进去这都属于随时可调的细节。到这里列表页的任务就完成了我们手里有了一个不重复的、绝对地址的新闻详情链接列表。接下来进入详情页抓全量字段。4. 详情页字段抽取标题、时间、摘要、正文的定位逻辑4.1 抽字段的原则先找单一节点再给兜底进入详情页后核心是从HTML里抽取四个字段标题、发布日期、摘要、正文。先写一个解析函数def parse_detail(html: str, url: str) - dict: soup BeautifulSoup(html, lxml) # 标题全站唯一的主要标题通常就是 h1 title_tag soup.find(h1) title title_tag.get_text(stripTrue) if title_tag else url # 日期time 标签通常带 datetime 属性比从文本里猜更可靠 time_tag soup.find(time) date time_tag.get(datetime, ) if time_tag else # 摘要优先取正文区里的摘要段落其次用 meta 描述兜底 summary summary_node soup.select_one(div.dek, p.dek, p.summary, .article-subheadline) if summary_node: summary summary_node.get_text(stripTrue) else: meta soup.find(meta, attrs{name: description}) if meta and meta.get(content): summary meta[content].strip() # 正文在正文容器内选取所有段落并过滤过短内容 body_node soup.select_one(div.article-body, div.story-text, div.field--name-body) paragraphs [] if body_node: paragraphs [p.get_text(stripTrue) for p in body_node.select(p)] paragraphs [p for p in paragraphs if len(p) 5] return { url: url, title: title, date: date, summary: summary, body: \n.join(paragraphs), }代码里有个理念每一个字段都要尽量做到“主用一套选择器备用一套选择器”。页面结构改版或者某篇文章没有摘要时兜底逻辑能保证程序不直接崩溃。比如标题万一没拿到我就用URL顶上摘要定位不到就退回读meta description。虽然meta里的描述通常是SEO用的和页面展示不一定完全相同但总比空着强。4.2 日期为什么要优先读time标签的datetime属性很多新手喜欢从页面文本里把“March 1, 2025”这种自然语言日期挖出来再用正则转成标准格式。这是给自己加活。新闻站为了语义化和SEO几乎都会在time标签里提供机器可读的datetime属性比如2025-03-01T09:00:00-05:00。直接读属性得到的就是标准的ISO 8601格式后续排序、入库都不用再做转换。这里我不建议你用第三方库去解析自然语言日期。初级项目没必要引入那么重的依赖把datetime属性原样存成字符串就好。等你真正需要“北京时间几点发的”这类需求再单独做格式化那是展示层的活和爬虫本身无关。4.3 正文抽取的一个常见失误拿到的不只是正文正文抽取是很多人栽跟头的地方。最粗暴的写法是soup.get_text()这会把整个页面的文字全取出来结果里混进导航栏、相关阅读、页脚版权等一堆噪音。正确的姿势是先用选择器圈出一个“正文容器”比如div.article-body再去这个容器里面找p标签。你可以把正文容器想象成厨房p标签是洗好的菜只有在这个范围内取菜才不会把旁边垃圾桶的东西也端上桌。过滤空行和过短段落也很必要。页面里会有一些p标签只用来撑间距或者被脚本注入了一串空字符。我设了len(p) 5的阈值少于5个字符的段落直接丢弃这在大多数新闻页都能明显清理噪音。合并正文我用换行符\n而不是空格因为段落语义对后期做文本分析、摘要生成更有价值。4.4 网络层兜底加一个带重试的请求函数详情页数量一多网络抖动就会冒出来。单个请求失败时最合理的办法不是当场放弃而是过一会儿重试。一个简单的重试版请求函数可以这样写import time def fetch_with_retry(url: str, tries: int 3, base_delay: float 2.0) - str: for i in range(tries): try: return fetch(url) except requests.RequestException: if i tries - 1: raise time.sleep(base_delay * (i 1))这算最朴素的指数退避第一次失败等2秒第二次等4秒第三次才把异常抛出去。它能挡掉大部分临时性的连接拒绝和超时。初学阶段你完全可以把所有请求都走这个函数而不是只在出错的时候才想起来要重试。4.5 组装主循环把前面的零件拼起来主循环其实很短all_results [] for i, url in enumerate(news_urls, 1): try: html fetch_with_retry(url) item parse_detail(html, url) all_results.append(item) print(f[{i}/{len(news_urls)}] {item[title]}) except Exception as e: print(f[{i}] 失败: {url} - {e}) time.sleep(2)循环里一定要用try包住单篇处理逻辑别让一篇失败卡死整个任务。print进度不是可有可无尤其跑上百个链接的时候你知道它到底是在干活还是卡住了心里有底。每轮2秒的sleep就是礼貌限速后面我还会专门讲先记住一个原则详情页连抓一定要睡列表页单次请求不紧张但睡了也没坏处。5. 数据落地把抓到的新闻存成JSON和CSV5.1 为什么先落JSON再导CSV新闻正文是长文本里面不可避免会有逗号、换行、双引号。直接写进CSV转义规则会变得很麻烦你得处理引号转义、换行被误判成分隔符这类问题。JSON天然支持多行文本和嵌套结构程序读写也都省心。所以我的建议是主体数据先用JSON保存需要给非技术同事导表格的时候再从JSON临时转成CSV。写JSON就几行import json def save_json(results: list, filename: str) - None: with open(filename, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)ensure_asciiFalse不能漏否则中文会被转义成\uXXXX肉眼没法看很多程序也不方便直接处理。indent2纯粹为了可读性调试期会舒服很多。5.2 CSV导出的两个小细节再看CSV写法import csv def save_csv(results: list, filename: str) - None: with open(filename, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[url, title, date, summary, body]) writer.writeheader() writer.writerows(results)我第一次给别人演示时用普通的utf-8编码导CSV结果Excel打开全是乱码。后来才明白Windows下的Excel默认会用ANSI去解读没有BOM标记的UTF-8文件。解决办法就是utf-8-sig它会往文件开头写入BOM标记Excel看到这个标记就知道该用UTF-8来读了。newline也是小细节csv模块在Windows上默认会在每行末尾追加换行导致文件里多出空行。加了这个参数就不会有莫名其妙的空行。5.3 边抓边存最简单的断点续爬启蒙一次性save_json有个风险如果你要爬200篇跑到第150篇时脚本崩了前面149篇全白抓。入门阶段就可以养成边抓边存的好习惯做法是用JSONL格式每行一条JSON对象def append_json(item: dict, filename: str) - None: with open(filename, a, encodingutf-8) as f: f.write(json.dumps(item, ensure_asciiFalse) \n)主循环里每抓完一篇就append_json(item, mit_news.jsonl)。这样脚本中途崩掉已抓的数据已经躺在文件里了。想要完整数组之后再聚合转换一次就行。更进一步你可以在程序启动时先读一遍已有的JSONL文件把抓过的URL存进一个set循环里遇到已经抓过的就跳过。这就是最朴素的断点续爬代码不多但能帮你避开“扒了一下午手一抖全没了”的惨剧。6. 做得久比做得快更重要限速、robots与内容边界6.1 先花两分钟读一下robots.txt很多新手写完爬虫跑通后第一反应是“这个站我能爬了”下一步可能就想把全部分页都抓下来。每次动手前我建议你先读目标站的robots.txt。用requests就能看resp requests.get(https://news.mit.edu/robots.txt, timeout10) print(resp.text)robots.txt是网站对所有网络爬虫发出的“可访问范围”声明它会告诉你哪些路径允许抓取、哪些不允许。读它不是为了背条文而是培养一个职业习惯先确认边界再动手。像MIT News这类高校站robots里限制的通常是后台、会话这类路径公开新闻列表和详情页一般允许普通爬虫访问。即便这样也建议把它当规范来尊重。6.2 限速不是对服务器的施舍而是让自己更省心爬虫圈有句老话降低频率就少一半烦心事。你站在服务器视角想一下一个未知客户端每秒发来5个请求和每2秒来1个风险完全不一样。前者触发429请求过多和503服务暂不可用的概率会直线增加。限速对你自己的收益是实打实的——少遇到封禁、少报错、脚本跑得更顺。这次实战里我建议详情页请求统一加time.sleep(2)。如果某次运行发现服务器响应变慢就把间隔加长到5秒。具体数字没有绝对标准核心是“让请求频率看起来像一个人在正常浏览”。顺带说一句不要一上来就研究代理IP池初级项目完全用不上那是另一套复杂度等真有规模化需求再学也不迟。6.3 爬下来的内容能做什么、不能做什么边界意识把MIT新闻抓下来之后能拿来做什么个人学习、字段分析、做成自己的新闻阅读数据集、练文本挖掘这些场景问题不大。但不能做的事是批量打包转卖、把原文当成自己原创去发布。新闻内容有自己的版权归属MIT News很多内容采用开放许可但“能不能用”和“怎么用”要看具体页面的版权说明。对爬虫学习者来说更核心的态度是学习爬虫的初衷是自动获取公开信息而不是与某个网站的安全防线对抗。现在经常能看到关于前端防爬、后端接口防护的讨论问“能不能绕过去”。作为写爬虫的一方我的看法是网站设置防护是正当的而你作为学习者更应该选择合法、公开、允许访问的数据源来练习。把技术用在“MIT News今天更新了几条新闻”这种公开数据上比钻某个站的漏洞能学到更多也睡得安稳。做初级爬虫真要总结只有一句耐得住性子看HTML守得住频率访问服务器记得把数据及时落盘。这套流程跑下来MIT News对你不再是个神秘的站点它会变成几十条结构规整的JSON记录。下次你想爬任何新闻站甚至可以只改两个地方——列表页的选择器、详情页的字段选择器整个脚本照跑。如果中途遇到定位不到、字段缺失的情况先回DevTools里“验货”再对着正文结构调选择器90%的问题当场就能解决。剩下的10%多半是网络抖动或站点结构变更换一个重试策略就过去了。我当初写完这个脚本顺手把摘要部分接了一个关键词统计做了个简单的栏目热点词云算是这个练手项目的第一个小扩展。你也一样跑通它再往上加功能会发现爬虫这条路越走越宽。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。