☰
用MIT新闻练手:Python爬虫实战入门全攻略
2026/10/5 4:17:49 网站建设 项目流程

1. 为什么我推荐用“麻省理工新闻”练手——初级爬虫的第一个靶场

1.1 一个爬虫老手挑目标的三个标准

我带过不少朋友入坑爬虫,大家最爱问的问题不是“requests怎么用”,而是“我该先拿什么东西练手”。有人一上来就挑电商网站,结果被验证码和滑块折磨得怀疑人生;有人去爬社交平台,发现全是JS渲染的数据,半天取不到几行;还有人盯着政府公开数据接口,虽然能跑通,但总觉得没学到页面解析的东西。

按我的经验,给初级爬虫选目标网站,至少得满足三个条件:第一,页面结构要足够规矩,不搞那些花里胡哨的动态渲染,确保你用requests拿到HTML之后,真的能从中解析出东西;第二,数据规模要合适,不能太小让你觉得像在背答案,也不能大到把简单事情搞复杂;第三,站点要稳定,别爬两页就封你IP,或者直接给你返回一堆乱码。

麻省理工学院新闻(MIT News)就是这样一个很理想的起点。它看起来像个学术机构网站,但本质上是一个结构非常清晰的新闻门户:有列表页、有分页、有详情页、有多条新闻条目,每个条目里又包含标题、发布时间、摘要、链接、分类。这种结构几乎覆盖了爬虫入门阶段百分之八十的核心场景。

1.2 MIT新闻这个目标里藏着哪些入门点

先说这个网站的技术栈。打开麻省理工新闻的任意列表页,你按下F12看到的DOM结构是服务端直接渲染出来的,这意味着什么?意味着你不需要用Selenium,不需要等浏览器执行JavaScript,只需要一个requests.get()拿回HTML源码,就能在里面找到所有想要的数据。这一步直接省掉了初级爬虫最大的一个坑——处理动态加载。

再说它适合练习的理由:这个站点的XPath结构稳定。我试过在不同分类页、不同分页之间切换,文章卡片区域的class命名虽然有一定层次,但相对固定,一旦你写好了第一页的解析规则,后面几十页基本可以复用。你还可以顺手练一练XPath的text函数、contains函数、节点轴这些操作,而不是像某些网站那样,每条数据的XPath都长得完全不一样。

更重要的是,MIT新闻的数据量适中。新闻网站一般有持续更新的内容,分页数量可观又不至于多到离谱。你练习循环翻页、拼接URL、处理最后一页空列表这些问题时,都能在一个比较可控的范围内完成。如果你想练到分布式爬虫、消息队列、调度框架那一步,也可以拿它的全站文章做数据源。所以这个目标并不是练完就丢的玩具,而是一个从入门到进阶都能用的数据来源。

我个人的建议是,不要一上来就追求“爬到全站几十万条数据”,而是先把一个列表页、一个详情页的解析做到烂熟。MIT新闻刚好允许你用最小成本把这件事做扎实。

2. 动手前的准备:环境、工具和基本思路

2.1 需要的Python库只有三个

很多初学者容易被各种框架绕晕。今天听人说Scrapy好,明天听人说要用Playwright,后天又有人说必须上代理池——这些都没错,但都不是你第一天的任务。做初级爬虫,工具能少就少,先把基本功打牢。

这次实战我建议你只用三个库:

  • requests:负责发HTTP请求,拿回网页源码。它是Python生态里最常用的HTTP库,比urllib好用太多。
  • lxml:负责解析HTML,配合XPath提取节点。lxml底层是C语言写的libxml2,解析速度快,写XPath也方便。
  • csv / json:Python标准库,负责把结果落盘。数据量不大时根本不需要上数据库,CSV文件足够你后续做分析和练习。

有一些教程会推荐你用BeautifulSoup + requests的组合。我承认BeautifulSoup的API对新手更友好,但它在某些场景下的解析速度明显不如lxml,而且xpath的表达式写熟了之后,代码的可维护性会比find_all那一套更高。你可以两种都试,不过这次我以lxml为主,因为你想在网上搜“python xpath爬虫 text函数”这类问题时,大部分答案基本都是基于lxml写法的。

安装环境的时候,如果你还没装Python,建议直接用Python 3.10以上的版本。然后执行:

pip install requests lxml

就这么简单。不要在这步犹豫太久,真正的难点在后面的解析逻辑上。

2.2 解析方案怎么选:正则、XPath还是CSS选择器

新手最容易犯的一个错误,是想用正则表达式解析一切。正则确实强大,但用正则去匹配HTML结构非常脆弱,一旦网页的标签顺序有微调或者属性值里多了个空格,你的规则就可能全部失效。我在早期爬虫经验里最大的教训之一,就是妄图用一段正则搞定整页新闻内容,结果页面一改版就躺平。

比较合理的选择是XPath或CSS选择器。XPath的优势在于它操作的是文档树,你能明确表达“选取某个div下所有具有某class的a标签”这种层级关系,HTML的标签变化对它的影响远小于正则。CSS选择器在BeautifulSoup或者Selenium里用得多,写法更简洁,但在处理复杂的兄弟节点、父节点、文本筛选时,没有XPath那么灵活。

所以这次的代码统一用lxml.etree配合XPath来完成。如果你在搜索引擎里看到类似“python xpath爬虫 text函数”的内容,核心其实就是用text()去匹配节点的文本,用contains()去模糊匹配属性值。这些都会在当前这个项目里用到。

2.3 先摸清网页结构再写代码

写爬虫最容易犯的另一个错误,是一上来就开IDE敲代码,敲了半天才发现选择器压根不命中。正确顺序是先打开浏览器,按F12看一眼页面结构,再用代码去验证。

打开MIT News的首页,你会看到类似这样的结构逻辑:页面上方是新闻列表,每条新闻被包裹在一个列表项中;列表项里面有标题链接,标题下方有摘要文字,再往下是发布日期和分类标签。列表底部则是分页控件,包括首页、末页、上一页、下一页以及页数链接。

我在实际开发中还有一个习惯:在浏览器里右键某个标题,选择“检查”,然后复制它的XPath,先放到Python里跑一次看能不能取到。这样可以快速验证页面结构是否是你理解的那样。这个步骤虽然不起眼,但能省掉后面大量的调试时间。

3. 核心实战:用requests把整站文章标题拉下来

3.1 第一步:请求页面并验证响应

我们的第一个目标是:把MIT新闻的第一页文章标题全部提取出来,并打印在控制台上。这一步虽然简单,但它把请求、解析、提取三个核心环节完整地串了一遍。

先上基础代码:

import requests from lxml import etree url = "https://news.mit.edu/news2" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) print(resp.encoding)

为什么非要在请求头里带User-Agent?因为很多服务器会对没有UA的请求直接返回403或者识别为异常客户端。浏览器访问页面时会自动携带这一串信息,你的爬虫如果空着手过去,服务器当然怀疑你是脚本。加上这个常见的UA,至少能让请求看起来更像真人操作。

请求完之后,记得检查resp.status_code。如果是200,恭喜你,可以继续解析;如果是404,说明URL写错了;如果是403,大概率被封或者需要更多请求头;如果是5xx,那就是服务器自己的问题。这里我给初学者的建议是:出现问题先打印状态码和响应文本的前500个字符,看看返回的到底是什么,而不是盲目调代码。

3.2 第二步:用XPath定位文章标题

请求成功后,接下来就是关键的解析环节。用lxml解析HTML字符串,然后写XPath提取标题。

html = etree.HTML(resp.text) # 观察页面结构后,发现标题在 h3 下的 a 标签里 titles = html.xpath('//h3[@class="news3__card-title"]/a')

[^1] 注意,不同的时间点访问MIT News,页面结构有可能调整。比如我这次看到的class名是news3__card-title,你下次访问时可能已经改版。所以这里我要强调:XPath里最稳妥的写法是尽量结合标签语义和class特征,但也不要完全依赖class。下方是一个更通用的写法:

titles = html.xpath('//article//h3/a | //div[contains(@class,"title")]//a')

当你在网上搜索XPath的text函数时,常见的需求是“根据链接文本筛选节点”。比如只提取文本里包含“MIT”的标题:

targets = html.xpath('//h3[contains(a/text(), "MIT")]/a')

这里用contains和一个函数的组合,就是XPath里非常典型的文本筛选技巧。先不急着用太复杂的规则,你只需要记住:XPath里的方括号是条件过滤,text()代表当前节点的文本,contains()是模糊匹配的工具,这三个东西组合起来能解决绝大多数文本定位问题。

3.3 第三步:加上分页循环拿到全站列表

单页解析只是热身。爬虫真正的价值在于“批量”。MIT News的列表页URL有一定规律,通过观察分页控件的链接,你会发现它用类似“?page=2”这样的查询参数。那写起来就很简单了。

import time all_titles = [] for page in range(1, 11): url = f"https://news.mit.edu/news2?page={page}" resp = requests.get(url, headers=headers, timeout=10) if resp.status_code != 200: break html = etree.HTML(resp.text) titles = html.xpath('//h3[contains(@class,"title")]/a/text()') if not titles: break all_titles.extend(titles) print(f"第{page}页,抓到{len(titles)}条标题") time.sleep(1)

这段代码里有几个细节值得注意。第一,每次请求之间加了time.sleep(1),这是给服务器喘息的空间,也是爬虫的基本素养。第二,如果在某一页抓不到标题,就主动break,避免因为页面结构变化导致无限空转。第三,用extend而不是append,把列表平铺而不是嵌套进一个大列表。

你可能想问,为什么非要睡眠?实际上,如果你用全速去抓取一个新闻网站,几百个请求瞬间打过去,很容易触发服务器的限流策略。我在实际操作中发现,即使是相对友好的新闻站,也不喜欢被密集请求刷。初级爬虫阶段养成加延迟的习惯,比以后被封锁再来补课划算得多。

3.4 保存到CSV并加上基本异常处理

数据抓下来不保存,等于白干。用Python内置csv库把这些标题写入文件,第一步实战就算完整了。

import csv with open("mit_titles.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["标题"]) for title in all_titles: writer.writerow([title])

加上异常处理以后,代码会更健壮:

try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() except requests.exceptions.RequestException as e: print("请求失败:", e) continue

这里用resp.raise_for_status()代替手动判断,只要状态码不是2xx,就会主动抛出异常,省得你每页都写一个if判断。你可能会在实际运行中遇到连接超时、DNS解析失败、SSL证书校验失败等各种问题,requests的RequestException就是这些错误的总父类,统一捕获能防止程序因为一次网络抖动就整个崩溃。

到这一步,你已经完成了一个最基础的爬虫闭环:发送请求、解析HTML、提取数据、批量翻页、落盘保存。这五个环节就是爬虫技术的核心骨架,后面所有复杂的框架和中间件都是在这个骨架上做增强。

4. 进阶细节:提取发布时间、摘要与正文链接

4.1 解析列表页里能挖到的所有信息

光有标题不够。新闻网站的数据维度至少还包括发布时间、摘要、分类、作者、原文链接。如果你只抓标题,后面做内容分析或者训练语料时会发现信息量根本不够用。

MIT News列表页的结构比较友好,每条新闻卡片里同时包含标题、摘要、发布时间和链接。对应的XPath可以逐个拆解。以下是我在解析时实际用过的写法:

cards = html.xpath('//div[contains(@class,"view-content")]//article') for card in cards: title = card.xpath('.//h3/a/text()')[0].strip() link = card.xpath('.//h3/a/@href')[0] summary = card.xpath('.//div[contains(@class,"description")]/text()') date = card.xpath('.//time/@datetime')

这里的一个关键技巧是“先定位卡片,再在卡片内做二次定位”。也就是说,不要一开始就写绝对路径,把整条XPath从html/body一路写到文章卡片,那样改版一次就废掉。用.//开头,意思是“从当前节点的后代里查找”,代码的容错性会明显提高。

有些新闻网站的发布时间不是显示成

date_text = card.xpath('.//span[contains(@class,"date")]/text()')

如果多个节点拼在一起,你可以用string-join或者直接用Python侧做replace清洗。记住一个原则:XPath负责定位,Python负责清洗。别试图用一条巨大无比的正则或XPath把所有脏东西都处理完,拆开来做,每个环节都好调试。

4.2 追进详情页拿正文和图片

列表页信息毕竟有限,正文和图片需要进入详情页抓取。这一步会引入一个新的问题:请求量从一页几十个变成几十页几百个。你需要控制节奏,也需要注意异常处理。

详情页的解析思路和列表页一致,先定位正文容器,再从中提取段落文本。

detail_resp = requests.get(link, headers=headers, timeout=10) detail_html = etree.HTML(detail_resp.text) paragraphs = detail_html.xpath('//div[contains(@class,"article-body")]//p/text()') full_text = "\n".join(p.strip() for p in paragraphs)

图片同样可以提取:

images = detail_html.xpath('//figure//img/@src')

但这里需要注意,MIT News的图片地址有时候是相对路径,你得自己拼上域名前缀。还有,图片可能是srcset或者data-src这种懒加载属性,直接取src可能取不到。初级爬虫阶段遇到这种情况,你可以先不做图片下载,只记录图片URL,后续需要再研究懒加载的规律。

追进详情页时,我强烈建议你打印一下进度,比如当前抓到哪个链接、标题是什么、正文有几千字。别小看这种日志,它会帮你在程序跑挂的时候迅速定位问题出在哪条URL上。

4.3 优雅限制请求频率:给自己的爬虫装个“刹车”

既然已经进入了详情页抓取阶段,请求量就上来了。这时我建议你把请求封装成一个函数,在函数内部统一管理请求间隔和异常重试。

def fetch(url, retry=3, delay=1): for i in range(retry): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() return resp except requests.exceptions.RequestException as e: print(f"第{i+1}次请求失败: {e}") time.sleep(delay) return None

这个函数的好处是,你所有的请求逻辑都集中在一个地方,后续想加上代理、想修改重试策略、想统一设置请求头,只需要改这一个函数。我自己的项目里,几乎所有基于requests写的爬虫最后都会长成这样。

另外关于延迟,有一个小经验:固定延迟1秒看起来简单,但机器味很重。如果想更自然,可以加一点随机浮动:

import random time.sleep(0.5 + random.random())

这样做不是为了让服务器“看不出来”,而是为了平滑请求流量,避免瞬时脉冲。

5. 常见问题与排查技巧实录

5.1 返回值404/403怎么处理

我见过太多新手在爬虫报错时不知所措。先说404,它通常是URL拼接错了,尤其是分页参数。你可以在代码里打印当前请求的完整URL,然后复制到浏览器里打开看看。如果浏览器能正常打开但requests返回404,那就是URL里带了什么特殊字符或者参数被错误编码了。

403比404更麻烦,意味着服务器知道你是爬虫,或者至少不愿意响应这次请求。处理思路一般有三步:

  • 检查User-Agent是否伪装成浏览器,还缺少什么常见的请求头,可以加上Accept、Accept-Language这些。
  • 放慢速度,加长sleep时间,观察是否恢复正常。
  • 如果必须继续访问,考虑使用代理IP。

但麻省理工新闻这类学术新闻站,风格相对温和,基本只需要一个正常的UA和适度的请求间隔,不太会走到代理那一步。这也是我推荐它做靶场的原因之一——你不需要把精力花在对抗反爬虫上,而是专注在爬虫本身的技术学习。

5.2 XPath匹配为空怎么定位

XPath匹配为空是这个项目里最高频的问题。先别急着改代码,按照下面的顺序排查:

  1. 打印resp.text的前2000个字符,确认返回的是不是你预期的HTML。
  2. 用浏览器开发者工具,查看目标元素的真实XPath。
  3. 检查有没有iframe嵌套,内容可能在iframe里,常规XPath根本触碰不到。
  4. 检查text()和@href的提取位置,是不是把文本和属性搞混了。

我在实际调试中喜欢写一个临时小函数,专门用来测试XPath:

def test_xpath(html_text, expr): tree = etree.HTML(html_text) result = tree.xpath(expr) print(len(result)) for item in result[:3]: print(item)

每次调整XPath后,直接调用这个函数看能不能命中。命中后再放进完整的爬虫流程里,能极大减少“写了半天才发现选择器错了”的情况。

5.3 虚假的“动态加载”:如何识别真正的JS渲染页面

MIT News是服务端渲染,所以requests能直接拿到内容。但你会看到很多其他网站,requests拿回来的HTML里根本没有你要的数据,而是在一个空的div里等着JavaScript去填空。

判断方法很简单:在浏览器开发者工具里查看页面源代码,也就是Ctrl+U那个原始HTML,搜一下你的目标文本。如果原始HTML里能搜到,那是服务端渲染,requests可以直接拿;如果搜不到,那大概率是客户端渲染,需要Selenium或者Playwright这类工具驱动浏览器。

很多初级爬虫的问题在于分不清这两者。看到某些教程说“不需要selenium,requests+AJA抓动态数据”,就以为自己也能行。实际上,那种做法需要去翻XHR接口、解析JSON,属于另一套技术栈。MIT News的好处就是它没有这个问题,你先练好一套技术,再接触另一套,才不会混。

5.4 编码问题:新闻页里的UTF-8和BOM陷阱

爬虫第二常见的问题是乱码。MIT News的页面是UTF-8编码,requests通常能正确解码。但你在保存CSV时,如果直接open一个文件然后往里写中文,Windows默认编码可能是gbk,导致文件打开乱码。

所以写CSV时一定要显式指定encoding="utf-8"或encoding="utf-8-sig"。这里有个细节:如果文件要用Excel打开,建议用utf-8-sig,它会自动写入BOM头,Excel才不会把它当成乱码。如果你的CSV需要被程序再读取,那普通utf-8就够了。

还有,如果你发现页面返回的内容解析出来有奇怪的字符,可以手动检查resp.encoding是什么,必要时用resp.encoding = 'utf-8'强制指定。requests有时候会根据headers里的charset猜编码,猜错就会乱,手动指定是最可靠的。

6. 从“能跑”到“好用”的一些经验心得

6.1 爬虫速度:适度并发带来的收益

当你把单线程爬虫跑通之后,会开始嫌弃它慢。MIT News全站可能有几万条新闻,每条详情页请求一次,算下来要好几个小时。这时你可以尝试用concurrent.futures里的ThreadPoolExecutor做简单并发。

我提醒一句:并发不是越多越好。很多新手一上来就开50个线程,结果没跑多久就被服务器限制,甚至把自己的IP拉黑。以MIT News的速度,我个人建议从5个线程开始测试,观察请求的成功率和响应时间,再慢慢往上加。

from concurrent.futures import ThreadPoolExecutor def crawl_detail(link): resp = fetch(link) # 解析逻辑 return data with ThreadPoolExecutor(max_workers=5) as executor: results = list(executor.map(crawl_detail, links))

这个并发版本的代码并不复杂,但带来的收益很明显。同样是抓几千条详情页,单线程可能需要一个小时,5线程并发可能缩减到十五分钟。不过你要记住,并发场景下的异常处理比单线程更麻烦,连接超时的概率会上升,日志必须打印线程编号或者URL,方便定位问题。

6.2 数据清洗与去重

爬虫抓回来的数据不能直接用。你会发现标题里有换行、多空格、HTML实体符号,摘要里可能有“Read More”之类的尾巴。清洗的基本原则是:先把文本strip去掉首尾空白,再把不必要的换行替换成空格,最后把HTML实体(比如 )替换成正常字符。这一步用Python的replace配合html.unescape就够。

去重也很重要,尤其是新闻网站首页和列表页可能有重复推荐位。去重最简单的方式是维护一个已见链接的集合:

seen = set() if link not in seen: seen.add(link) # 处理这条数据

这个集合在内存里就够,数据量大了再考虑用Redis或者布隆过滤器,但你暂时用不上。

6.3 它对后续技术栈的延展价值

我认为麻省理工新闻这个爬虫项目最大的价值,是它的数据结构和你的爬虫能力能平滑扩展。你把列表页解析和详情页解析做完之后,很自然地就会想:标题能不能用来做关键词提取?摘要能不能做文本分类?正文能不能做主题建模?这些下游需求会反过来推动你优化数据格式,而不会像某些网站数据那样,千辛万苦抓出来却根本没法用。

如果你还想往分布式爬虫方向发展,MIT News的数据量也算合适的试验田。Scrapy内置的并发调度、去重队列、增量爬取,都可以用这个网站当靶子。换句话说,这个项目不是一条死胡同,而是一个能不断扩展的地基。

6.4 合规意识:千万别碰哪几类目标

最后说点实在的。爬虫技术本身没有任何问题,但在实际使用中,你必须对目标网站有基本的判断。

第一类,需要登录后才能访问的数据,不要去硬撞。登录墙本质上是网站设定的权限边界,你绕过它就是绕过访问控制。第二类,明确在robots.txt里禁止爬取的部分,至少要有合理频率和完整标识,不要踩线。第三类,带有个人隐私信息的数据,比如手机号、住址、证件号,这种不管技术上容易还是难,都不该碰。

对MIT News这类新闻网站,适度抓取公开的新闻标题和摘要,用于学习或数据分析,风险相对可控。但我也建议你在代码里写明当前请求的目的,设置可辨识的User-Agent,比如加入你的联系方式或项目名。这既是礼貌,也是一层自我保护。

爬虫最宝贵的能力不是“能把网页抓下来”,而是“知道自己能抓什么、该抓什么”。这段经验在我参与的项目里反复被验证,希望你看完这篇实战记录后,也能先想清楚目标,再动手写第一行代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询