
很多人刚入门Python爬虫的时候特别容易一上来就选那些“高并发、强反爬、登录验证”的大站练手结果请求发过去就是403、验证码、封IP挫败感拉满。我自己也走过这段弯路后来发现像MIT News麻省理工学院新闻这种高校新闻发布站才是初级爬虫非常理想的练手素材结构稳定、内容更新规律、没有复杂的登录逻辑还能顺便练到requests请求、XPath解析、text()文本提取、数据清洗、CSV落盘这一整套爬虫核心流程。这篇文章就用它做例子完整走一遍实际开发过程包括我踩过的坑和排查思路适合刚学完Python基础、想找第一个真实项目练手的朋友。1. 项目全貌搞清楚爬虫到底在解决什么问题1.1 为什么选MIT News而不是国内新闻站练手爬虫最怕的不是网站反爬强而是目标页面结构三天两头变或者内容全是图片、视频压根没有文本信息。MIT News是纯粹的文本内容站点首页和列表页都有清晰的标题、摘要、日期、分类HTML结构规整非常适合做第一个“真实数据”项目。更重要的一点是它给初学者的容错空间非常大。哪怕你的XPath写得不够严谨也能提取到大部分内容就算请求频率稍微快了一点它也通常不会直接封禁你。这种“友好型”站点能让你把注意力集中在爬虫本身的原理上而不是和反爬机制斗智斗勇。等你在这种干净站点上把流程跑通、把代码写扎实了再去接触那些有反爬策略的站点才不至于手足无措。1.2 一次标准爬虫请求的完整链路网络爬虫原理其实不复杂本质上就是“模拟浏览器去访问服务器拿到响应再从响应里挖出需要的数据”。放到MIT News这个场景里完整链路可以拆成四步构造请求用requests库向目标URL发HTTP GET请求带上浏览器相关的请求头。获取响应服务器返回HTML文档可能是完整的静态页面也可能是被JS动态渲染的骨架。解析内容拿到HTML之后用XPath或者CSS选择器定位新闻标题、链接、日期、摘要所在的位置。清洗与存储把提取出来的字符串去掉空白符、拼接完整URL最后存成CSV、JSON或直接写进数据库。这个流程你说它是“原理”也好“套路”也罢总之绝大多数爬虫项目都是这四个环节的不同组合。MIT News这种静态页面恰好每一步都能用最经典的方式实现没有花里胡哨的加密参数也不用加载浏览器内核。1.3 用到的技术栈和选型理由这个项目我用的核心工具是requests、lxml、csv外加time模块做限速。requestsPython里最顺手的HTTP库相比标准库urllib它的会话管理、超时设置、异常处理都更人性化。当然也有httpx、aiohttp这些更现代的选择但初级项目里requests的生态和资料最多遇到问题最好查。lxmlXPath解析的利器底层是C语言实现的解析速度快而且语法比正则表达式直观太多。它是这个项目里负责“定位”和“提取”的绝对主力。csvPython标准库不需要装额外依赖用来把格式化数据写成本地文件非常省事。time.sleep()别小看它礼貌爬虫和暴力爬虫之间往往就差一个time.sleep()。我不推荐一上来就用Scrapy框架或者Selenium。Scrapy虽然强大但它的信号、中间件、Item Pipeline这些概念容易让新手眼花缭乱Selenium更是杀鸡用牛刀MIT News的列表页是静态返回的老老实实用requests就够了。先把裸代码写明白后面再上框架你会更容易理解框架到底帮你解决了什么问题。2. 上手之前先观察目标网站再动手写代码2.1 静态页面还是动态渲染先按F12看一眼源码很多新手拿到一个网站急着复制URL就开始写requests代码结果打印出来发现啥都没有因为页面内容是JavaScript异步加载的。所以第一步一定是在浏览器里按F12打开开发者工具找到Elements面板看看新闻标题是不是直接出现在HTML源码里。MIT News的列表页就属于典型的服务端渲染你把页面另存为HTML文件用记事本打开能直接看到完整的新闻标题和正文摘要。这意味着requests返回的HTML里就已经带了所有数据不需要额外分析XHR请求。如果未来你遇到动态站HTML里只有一堆空的div那就别硬用requests解析整页了正确做法是切到Network面板找到真正返回JSON数据的接口直接模拟那个接口请求。不过这和本文主题无关先不展开。2.2 请求阶段必须注意的三个细节Headers、超时、回退请求头是服务器判断你是不是“真人浏览器”的重要依据。最基础的一个字段是User-Agent如果你不加这个字段很多服务器会直接返回403或者一个空白页面。我习惯在请求头里至少带上这两项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-Language: en-US,en;q0.9 }Accept-Language这个字段容易被人忽略但对英文站点来说它能让服务器返回你期望的语言版本也能增加请求的“正常感”。然后是超时设置。requests.get()里的timeout参数必须写我一般设为10秒。如果不写遇到网络波动时脚本可能一直卡在那里像死机一样。再配合try...except遇到超时或者连接错误就重试一次不要无限重试三次不行就跳过这对初级脚本来说已经够用了。2.3 页面结构拆解明确你要提取的字段在写XPath之前先在开发者工具里手动定位一下新闻列表的每个字段。我在这次实战中模拟的结构是每篇新闻的标题在h3标签内class属性包含article-title标题文字在a标签里。详情链接是a标签的href属性且是相对路径比如/2024/01/15/some-story/。摘要是一个p标签class里包含description。发布日期在time标签的datetime属性里。这个结构不保证和线上完全一致因为网站改版是常态。所以我在代码里会先用开发者工具确认实际标签再决定XPath怎么写。这里的关键不是记住MIT News的具体结构而是养成“先看HTML再写定位表达式”的好习惯。一个能少踩很多坑的经验把页面结构观察这一步做成笔记包括你确认过的标签名、属性名、层级关系。不用写得多工整自己看得懂就行。因为爬虫代码最大的敌人就是网站改版一旦结构变了你有笔记在手排查定位问题的速度会快很多。3. 核心编码requests请求与XPath提取实战3.1 环境准备安装依赖这个项目只需要两个第三方库。在命令行里执行pip install requests lxmllxml在Windows上一般都有预编译的wheel包安装过程不太会出问题。如果你用的是Anaconda那这两个库大概率已经内置了。装完之后在Python交互环境里执行import requests, lxml不报错就算准备完成。3.2 写一个稳定的请求函数不要把请求逻辑散落在主流程里我习惯抽成一个函数统一处理headers、超时、异常。下面是这个项目里实际用到的版本import requests import time 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-Language: en-US,en;q0.9 } def fetch_page(url, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f[错误] 请求失败: {e}, 第{attempt 1}次尝试) time.sleep(2) return Noneresp.raise_for_status()会在服务器返回4xx、5xx时主动抛出异常这比你自己判断resp.status_code 200更简洁。如果三次重试后还是失败就直接返回None让主流程跳过这一页。这个函数在后面的跨页面爬取中也能复用。3.3 XPath定位与text()的配合拿到HTML文本后先用lxml.html.fromstring()把它转成可查询的树形结构然后就可以用XPath表达式定位节点了。这里重点说text()的作用它提取的是某个节点的直接文本内容而不是HTML标签。from lxml import html def parse_news(page): tree html.fromstring(page) articles tree.xpath(//h3[contains(class, article-title)]) results [] for article in articles: title_node article.xpath(./a) if not title_node: continue title title_node[0].xpath(string(./text())) link title_node[0].xpath(./href)[0] ...这里要解释一下为什么标题提取用string()包一层text()。因为如果某个标题里还嵌套了span、em这类子标签直接取./a/text()会得到多个字符串片段而且文本会被子标签切开。string()会把整个a内部的文本一次性拼接出来输出更干净。XPath里contains(class, article-title)比classarticle-title更实用因为很多HTML元素的class是多个类名叠加的比如classarticle-title news-card用等号匹配就会漏掉。contains()是模糊包含能应对这类情况。3.4 相对URL拼接与日期清洗热门搜索词“python xpath爬虫 text函数”在这个项目里的体现就是上面这段代码。接下来还有两个容易卡壳的点链接补全和日期格式化。MIT News列表页里的href通常是相对路径不补全的话我们拿到的是一堆/2024/01/15/xxx/根本没法直接访问。补全逻辑很简单from urllib.parse import urljoin BASE_URL https://news.mit.edu/ full_url urljoin(BASE_URL, link)urljoin会自动处理https://news.mit.edu/和/2024/01/15/xxx/之间的拼接不会出现双斜杠或者缺斜杠的问题。日期字段在time标签里一般是datetime2024-01-15T09:00:00-05:00这种格式我们要的只是2024-01-15这部分的日期可以用字符串截取或者split(T)[0]。摘要字段提取后通常带大量换行和空格建议统一做一次清洗summary .join(summary_node).strip() summary .join(summary.split())先用strip()去掉首尾空白再用split()把连续换行和空格压缩成单个空格得到的就是一段干净的通稿文本。4. 数据落盘把新闻保存成CSV并整合完整脚本4.1 CSV编码的坑为什么中文会乱码虽然MIT News是英文站但数据落盘这个环节对中文读者来说还是要多说一句。Python的csv模块写入文件时如果你用open(news.csv, w, encodingutf-8)生成的文件用Excel打开就会乱码。正确做法是使用encodingutf-8-sig它会在文件开头写入一个BOM头Excel才能正确识别UTF-8编码。with open(mit_news.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, link, date, summary]) writer.writeheader() writer.writerows(results)newline这个参数也别省略否则在Windows上写CSV会在每行之间多出一个空行这是历史遗留问题手动加\r\n都救不回来。4.2 整合后的完整爬虫脚本把上面的代码整合起来就是一个能直接跑的脚本。我给的版本是“单页版”逻辑清晰适合新手理解import requests import time import csv from lxml import html from urllib.parse import urljoin BASE_URL https://news.mit.edu/ 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-Language: en-US,en;q0.9 } def fetch_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text def parse_page(text): tree html.fromstring(text) articles tree.xpath(//h3[contains(class, article-title)]) result [] for article in articles: a_node article.xpath(./a) if not a_node: continue title a_node[0].xpath(string(./text())).strip() link a_node[0].xpath(./href)[0] full_link urljoin(BASE_URL, link) date_node article.xpath(.//time/datetime) date date_node[0].split(T)[0] if date_node else summary_node article.xpath(.//p[contains(class, description)]//text()) summary .join(.join(summary_node).split()) result.append({ title: title, link: full_link, date: date, summary: summary }) return result def main(): page fetch_page(BASE_URL) news_list parse_page(page) with open(mit_news.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, link, date, summary]) writer.writeheader() writer.writerows(news_list) print(f抓取完成共保存 {len(news_list)} 条新闻) if __name__ __main__: main()这个脚本本身没有做频率控制因为只请求了一页速度很快。但如果你要扩展到首页、分页、专题栏目就必须在每个请求之间加time.sleep(1)或者time.sleep(random.uniform(1, 3))把请求节奏降下来。4.3 增量爬取用集合做URL去重一次抓完100条当然很开心但是过几天你再跑一次脚本就会得到重复数据。初级项目里最简单的去重方案是维护一个“已见URL集合”先读一遍已有的CSV把里面的link字段放进一个set新抓到的链接如果已经在集合里就直接跳过。seen_links set() try: with open(mit_news.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: seen_links.add(row[link]) except FileNotFoundError: pass这个策略有内存占用方面的局限但对MIT News这种体量的站点完全够用。等以后数据量大了再考虑改用数据库的唯一索引或者布隆过滤器那是后话。5. 实战复盘高频问题与排查技巧5.1 403与429请求被拒绝的第一反应新手最常见的报错就是403 Forbidden。遇到这个状态码第一反应是检查请求头尤其是User-Agent。如果你用的是python-requests/2.31.0这种默认UA服务器一眼就能识别出这是脚本请求。还有一个容易被忽略的点某些站点会校验Referer字段它表示“你从哪个页面跳转过来的”。如果你直接请求列表页有时需要在headers里加上页面本身的URL作为Referer。MIT News一般不需要但养成补全请求头的习惯没坏处。如果状态码是429 Too Many Requests说明请求频率太快了触发限流。解决办法是降低频率把time.sleep调大比如改为2到3秒。不要想着绕过限流礼貌爬取才是长期之道。5.2 XPath匹配不到内容先用手动方式验证XPath写完之后提取到的列表是空的这类问题在爬虫调试里占比最高。我的排查思路是先打印page[:500]确认拿到的HTML里有目标数据。在浏览器开发者工具里用$x(//h3[contains(class, article-title)])手动验证XPath看能不能选中节点。检查HTML是不是嵌套在iframe里或者表格里有tbody自动包裹这些都会影响XPath的路径。如果表达式本身没问题但还是为空很可能是页面结构里混入了注释代码或者模板语法打印一下节点数量做个判断。text()提取不到文本时也要确认一下目标标签下面是不是有子标签把文本拆开了。比如aspanTitle/span/a直接取./a/text()就是空列表而string(./a)能拿到完整文本。这个差异非常典型建议牢牢记住。5.3 下载内容乱码与空白符过多MIT News是英文内容基本不会碰到编码问题但如果遇到乱码通用排查方式是看响应头里的Content-Type声明了什么编码然后用resp.encoding或resp.apparent_encoding手动设置resp.encoding resp.apparent_encoding对于空白符我的处理策略是“先合并再split”。摘要这种多行文本往往夹杂\n、\t和多个连续空格直接用strip()是去不干净的。用 .join(summary.split())可以把任意多空白符统一压缩成单个空格文本立刻变得干净整洁。5.4 网站改版的结构变化把XPath集中管理这是爬虫项目的终极问题。你今天爬得好好的下个月网站改版所有XPath全部失效。我的做法是把XPath集中放在脚本顶部的常量区而不是散落在代码各处。这样改版后只需要修改一处不用满屏找表达式。XPATH_ARTICLE //h3[contains(class, article-title)] XPATH_TITLE ./a XPATH_DATE .//time/datetime XPATH_SUMMARY .//p[contains(class, description)]//text()另外爬取任何网站之前最好看一眼该站点的robots.txt比如访问https://news.mit.edu/robots.txt。它虽然不是一个有强制力的法律文件但写它的人明确表达了网站对爬虫的态度。尊重这个文件既是合规意识也是在保护自己的爬虫不被封禁。6. 经验沉淀与下一步扩展方向写完这个项目我自己最大的体会是初级爬虫的核心不在“爬”这个动作而在“处理不确定性”。你在浏览器里看到一个页面觉得数据伸手可得但真正写代码时各种意外层出不穷网络超时、标签结构变化、字段为空、编码异常……能把这些问题逐个解决掉你对Python的理解、对HTTP协议的理解、对HTML结构的理解都会上一个大台阶。如果你想把方向再往前推进有几个自然延伸的思路分页抓取MIT News的列表有分页参数循环请求多页配合time.sleep做限速练到“批量采集”这层。详情页抓取列表页拿摘要详情页拿正文。用爬下来的列表链接二次请求详情页再提取正文内容这比我上面给的单页脚本多一层解析逻辑但核心方法完全一样。数据可视化把抓下来的新闻按日期、分类做统计分析画成图表。这方面的资料很多正好把“爬虫→数据清洗→可视化”完整链路打通。分布式爬虫如果你未来要爬的站点量级巨大一台机器跑不过来了再研究Scrapy搭配Redis实现的分布式方案。但说实话MIT News这个量级完全用不上分布式先把单机版做大做强再谈别的。最后分享一个我实际工作中一直保留的习惯每写完一个爬虫都把这个页面的HTML快照保存一份到本地。这样就算对方网站改了版我手里的历史数据和分析文档也不会彻底报废。爬虫的维护成本往往比开发成本高给自己留点后路总比到时候对着404页面发呆强。