资讯详情 Python爬CSDN博客文章列表:requests+BeautifulSoup入门实战
📅 2026/10/2 14:36:56
说实话拿爬CSDN博客文章列表当练习项目是我带过不少新人之后觉得最“舒服”的一个入门case。它不像爬电商那样页面花里胡哨散落各处也不像微博那种需要搞定登录授权才能看数据CSDN博客列表页是实打实的服务端渲染HTML结构规律、分页方式固定正好把requests、BeautifulSoup、解析、循环、存储这条链路完整走一遍。而且做完之后你手里会多一个“输入用户名 → 输出文章表格”的小工具成就感很直接后面想扩展成爬正文、做词频统计都有现成的底子。这篇文章我就按一个刚接触Python爬虫的新手视角把整个项目的从零实现讲清楚。我会从目标拆解、环境准备、请求发送、HTML解析、分页、数据存储到403报错和页面结构变化这些坑全部过一遍。文中给的代码是我实测跑通过的版本你可以直接抄但我更建议你跟着思路自己敲一遍因为你敲的过程中踩的每一个报错都会变成你未来调试其它爬虫的经验。1. 动手前先想清楚这个项目到底要爬什么1.1 目标拆解很多人一上来就开始写代码结果写到一半发现“我要爬什么信息、爬到哪一页结束、数据存成什么样”都没想明白。这个项目虽然小但五脏俱全先把目标拆明白再做能省掉大量返工。我们要做的事情很简单给定一个CSDN博客作者的主页地址比如https://blog.csdn.net/你的用户名自动抓取该作者的全部文章列表拿到每篇文章的标题、链接、发布时间、阅读数最后汇总保存到一个CSV文件里。就这么一件事拆开看却包含四个核心子任务构造正确的请求拿到包含文章列表的HTML页面从HTML里定位文章列表区域提取出单篇文章的信息翻页把全部分页的文章列表都抓下来清洗数据保存成本地文件。这四步对应了爬虫最经典的“请求 → 解析 → 遍历 → 存储”流程。可以说只要你把这个小项目彻底吃透市面上90%静态网页爬虫你都知道怎么下手了差别只是选择器和页面结构不同而已。为什么要选CSDN因为CSDN博客主页的文章列表是服务端直接渲染在HTML里的你甚至不用开启浏览器就能看到完整内容。这意味着我们可以用最简单的requests库拿到HTML源码再用BeautifulSoup去解析不需要处理复杂的前端渲染、接口签名、加密参数这些东西。对新手来说少了这些干扰项才能把心思集中在爬虫的核心思路上。1.2 爬虫的规矩学技术前先学会边界在写任何一行代码之前我想先聊点“不那么酷”但很重要的事情。爬虫技术本身是中性的但怎么用是有边界的。CSDN在robots.txt里明确了哪些路径允许抓取、哪些不允许你写爬虫之前应该先看一眼。我们可以抓公开博客文章列表用于学习、分析和归档但不要用爬虫去刷访问量、恶意采集后倒卖数据、抓取用户未公开的个人信息或者对服务器造成明显压力。具体操作上我给自己定了几条习惯也建议你保留每次请求之间至少间隔1到2秒只抓取我们需要的公开页面不去触碰登录后才能看到的非公开内容遇到403、验证码就停下来检查而不是换着法儿去绕过。这不是教你缩手缩脚而是让你明白爬虫最稀缺的是稳定和长期不是瞬时的高并发。很多爬虫被对方服务器封掉就是因为访问频率太像“攻击”而不是“人”。2. 环境准备与工具选型2.1 开发环境搭建在动手之前先确认电脑上的Python环境。我建议使用Python 3.10及以上版本不仅语法更友好而且很多库的新版本已经放弃了对旧版本的支持。如果电脑还没装Python直接去官网下载安装包安装时记得勾选“Add Python to PATH”这一步能让后面的命令行操作省去很多麻烦。然后找个干净的目录创建虚拟环境。很多新手不习惯用虚拟环境总觉得“我直接全局装不就行了”。但你以后项目多了就会发现A项目需要requests 2.20B项目需要requests 3.0两个版本冲突起来会让你很痛苦。所以从第一个项目开始就养成好习惯python -m venv csdn_envWindows环境下激活csdn_env\Scripts\activatemacOS/Linux环境下激活source csdn_env/bin/activate激活之后命令行前面会出现(csdn_env)前缀说明你已经进入虚拟环境。接下来只需要安装三个库pip install requests beautifulsoup4 lxml为什么还要多装一个lxml因为beautifulsoup4本身是纯Python实现的解析器速度一般而lxml是C语言实现的解析器速度更快。BeautifulSoup在指定lxml作为解析器之后解析速度会有明显提升。对于小页面可能感知不大但等你要批量跑很多个作者的时候差距就出来了。2.2 工具为什么是 requests BeautifulSoup 而不是 Scrapy / Selenium我见过很多新人一开口就要上Scrapy觉得Scrapy才是“正规军”。但说实话Scrapy是一个完整的爬虫框架它有下载中间件、Spider管理、Pipeline、ItemModel这些概念学习曲线比这个项目本身的难度还要高。用Scrapy写这个十行核心代码的小爬虫就像用企业级服务器去跑一个计算器程序不是不行但没必要。requests和BeautifulSoup这个组合的优点在于它们都是单一职责的库每个环节你都能看到具体发生了什么出了问题也好排查。requests负责把对方服务器的HTML拿回来BeautifulSoup负责把HTML里的人肉内容筛出来。没有魔法没有隐藏逻辑特别适合用来建立正确的爬虫心智模型。那Selenium呢Selenium确实可以用来抓取动态网页它直接驱动浏览器可以等待JavaScript渲染完成后再抓取数据。但代价是资源占用高、速度慢、而且很容易被对方通过webdriver特征检测到。CSDN博客文章列表根本不需要JavaScript渲染用Selenium属于高射炮打蚊子。更聪明的做法是先用requests直接请求页面如果发现关键数据不在HTML里再根据XHR请求去找背后的API。这个判断能力也是做爬虫很重要的经验。3. 爬虫实现一步一步拿到文章列表3.1 确定目标URL结构先做一次手工侦察。打开CSDN博客访问任意一个作者主页你会发现文章列表的URL是有固定规则的。比如某位博主的首页地址是https://blog.csdn.net/hello_world那他的文章列表分页地址就是https://blog.csdn.net/hello_world/article/list/1 https://blog.csdn.net/hello_world/article/list/2 https://blog.csdn.net/hello_world/article/list/3这个规律一旦搞清楚分页就好办了。我们只要把最后面的页码数字替换掉就能循环请求每一页数据。同时在页面底部的分页区域会有一段文字类似于“共76页”我们可以动态获取总页数避免拍脑袋写死。开发的时候我先手动打开这个URL复制HTML到编辑器里搜索“article-list”大致确定文章卡片所在的位置然后再写代码。这种先观察、再编码的顺序能让你少走很多弯路。3.2 发起HTTP请求接下来写第一个版本的请求代码。直接用requests.get去访问目标URL一般会碰到一个很典型的问题服务器返回403。因为requests默认的User-Agent是python-requests/...服务器一看到这种“非浏览器”的标识大概率会直接拒绝。所以我们必须伪装成浏览器的请求头import requests 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, Referer: https://blog.csdn.net/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch_page(url, timeout10): session requests.Session() session.headers.update(HEADERS) try: response session.get(url, timeouttimeout) if response.status_code 200: return response.text else: print(f[警告] 请求失败状态码{response.status_code}URL{url}) return None except requests.RequestException as e: print(f[错误] 请求异常{e}) return None这里我特意封装了一个fetch_page函数而不是直接把requests.get裸露在循环里。别小看这个封装它把“网络请求”这个变化最频繁的部分隔离出来以后你想加代理、加重试、加日志都只需要改这一个函数。还要注意一个细节我用了requests.Session()而不是每次调用requests.get。Session能自动保持TCP连接复用连续请求时性能会好一些同时也能维持Cookie上下文。不过对于CSDN这种页面Cookie的影响很小所以Session的最大价值还是连接复用。3.3 用BeautifulSoup解析文章列表拿到HTML字符串之后下一步就是把它交给BeautifulSoup解析。这里的关键是找到“每篇文章卡片”的CSS选择器。以当前CSDN博客列表页的结构为例每个文章卡片的根节点是div.article-list里面有一个h4h4里面是文章标题的a标签。我刚学的时候踩过一个坑直接用find_all(a)去筛结果把侧边栏、footer里的链接全都混进来了。后面才明白一定要先定位到文章列表的容器再在容器内部去提取。代码如下from bs4 import BeautifulSoup def parse_article_list(html): if not html: return [] soup BeautifulSoup(html, lxml) articles [] items soup.select(div.article-list article) # 以实际页面为准 # 如果上面选不到元素就退化为更宽松的匹配 if not items: items soup.select(div.article-item) for item in items: title_tag item.select_one(h4 a) if not title_tag: continue title title_tag.get_text(stripTrue) link title_tag[href] articles.append({title: title, link: link}) return articles为什么我要用select而不是find_all因为select支持CSS选择器语法写起来更简洁而且直观程度高。select_one(h4 a)的意思是“在item内部找第一个h4下面的a标签”。这里注意我只提取了标题和链接因为这两个是最稳定的字段不会因为页面改版而轻易丢失。需要特别提醒的是CSDN的HTML结构会随页面改版而变化。所以你在实战中可能遇到select结果为空的情况。遇到这种情况不用慌先打印一段HTML源码看看观察真实结构。项目是死的人是活的只要理解了“定位容器 → 提取字段”的思路换一个选择器不代表重写整个爬虫。3.4 提取发布时间、阅读数等附加字段光有标题和链接还不够作为练习我们可以把发布时间和阅读数也抓下来。在CSDN列表页中每篇文章的行信息部分通常有这样的节点div classinfo span classdate2024-01-15 10:20/span span classread-num1234阅读/span /div对应的提取代码可以这样写import re def parse_article_list_with_info(html): if not html: return [] soup BeautifulSoup(html, lxml) articles [] items soup.select(div.article-list article) for item in items: title_tag item.select_one(h4 a) if not title_tag: continue title title_tag.get_text(stripTrue) link title_tag[href] date_tag item.select_one(.date, .date-time, time) publish_date if date_tag: publish_date date_tag.get_text(stripTrue) read_tag item.select_one(.read-num, .read-count) read_count 0 if read_tag: match re.search(r(\d), read_tag.get_text()) if match: read_count int(match.group(1)) articles.append({ title: title, link: link, publish_date: publish_date, read_count: read_count, }) return articles阅读数这里我用正则(\d)去数字串里提取数字。为什么不直接拿文本因为页面上可能是“1234阅读”也可能是“1.2万阅读”直接拿文本存进CSV后面会很难做排序和统计。用正则先把纯数字摘出来虽然“1.2万”会被截成“1”但至少数据结构是干净的。如果你需要处理“万”这个单位可以自己加逻辑判断我在后面会补充。3.5 分页循环从第一页抓到最后一页前面搞定了单页解析现在来处理循环。首先要解决一个问题到底有多少页最稳妥的办法是从页面里读取“共X页”这样的信息。在CSDN的分页区域通常会有类似span classpage_nums共10页/span的节点。如果找不到我们可以在后台循环里逐页请求直到某一页返回的文章数为0就自动停止。第二种方式更通用因为很多网站的分页提示格式不一样而“最后一页没有数据”在逻辑上是恒定的。我用的混合策略是优先尝试解析总页数解析不到或解析出错时自动切换成“空列表终止”模式。import time def crawl_author(username, max_pages50, delay1.5): all_articles [] base_url fhttps://blog.csdn.net/{username}/article/list/ for page in range(1, max_pages 1): url base_url str(page) print(f[信息] 正在抓取第 {page} 页{url}) html fetch_page(url) articles parse_article_list_with_info(html) if not articles: print([信息] 此页无数据终止分页。) break all_articles.extend(articles) time.sleep(delay) return all_articles有几个点需要说一下。delay这个参数就是我们的“礼貌间隔”我习惯设置为1~2秒既不会让对方服务器压力太大也不至于慢到让人失去耐心。max_pages是一个保险丝防止代码写错导致无限循环。如果某天目标网站的分页规则变了max_pages能保证程序不会失控。另外我在每次请求前打印页码信息这种做法在调试时太有用了你能直观看到程序跑到了哪一步。4. 数据处理与保存4.1 清洗和去重爬虫拿到的原始数据直接落盘是不行的里面有很多脏东西。比如标题里可能包含多余的换行、空格因为页面结构抖动同一篇文章可能在相邻两页重复出现还有阅读数字段可能混入单位字符。所以写一个清洗函数是必要的。def clean_articles(articles): seen set() cleaned [] for item in articles: title .join(item[title].split()) if not title or title in seen: continue seen.add(title) cleaned.append({ title: title, link: item[link], publish_date: item[publish_date], read_count: item[read_count], }) return cleaned这里我用 .join(item[title].split())把连续的多个空格、换行替换成单个空格然后用seen集合做标题去重。为什么不按链接去重因为链接往往带一些跟踪参数不如标题干净。但标题偶尔也有被修改的情况更稳妥的做法是同时维护一个链接集合双维度去重。对于这个小项目标题去重已经足够。4.2 保存为CSV和JSON数据清洗完就可以输出了。我建议同时支持CSV和JSON两种格式。CSV的好处是能用Excel打开给人看JSON的好处是机器易读后续写程序处理更方便。import csv import json def save_to_csv(articles, filenamearticles.csv): with open(filename, modew, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, link, publish_date, read_count]) writer.writeheader() writer.writerows(articles) def save_to_json(articles, filenamearticles.json): with open(filename, modew, encodingutf-8) as f: json.dump(articles, f, ensure_asciiFalse, indent2)编码这里有两个容易踩坑的点。encodingutf-8-sig会在文件开头插入BOM头这是为了兼容Excel打开CSV时中文不乱码。如果你用encodingutf-8记事本打开可能正常但Excel打开会出现“锟斤拷”乱码。JSON则用ensure_asciiFalse让中文以原样保存否则文件中会是一堆\uXXXX转义序列没办法直接阅读。4.3 异常重试与日志记录网络请求不像本地计算它充满了不确定性。对方服务器可能临时抽风你的网络可能抖动一下或者你写了错误的选择器导致解析到一半报错。所以我在主线代码里增加了一个简单的重试机制from functools import wraps import time def retry_on_failure(max_retries3, base_delay2): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f[警告] 第 {attempt 1} 次尝试失败{e}) if attempt max_retries - 1: raise time.sleep(base_delay * (attempt 1)) return wrapper return decorator然后给fetch_page挂上这个装饰器。需要注意的是我们只建议对网络请求做重试不要对“解析失败”这种逻辑错误做盲目重试因为重试多少次结果都一样只是浪费时间。重试的核心是应对瞬时故障而不是掩盖程序bug。5. 常见问题与反爬避坑5.1 403 / 500 错误403大概是你遇到的第一个“拦路虎”。原因基本就三个User-Agent不是浏览器请求频率太高请求头里缺少Referer等浏览器会带上但代码里没带的字段。解决方案也很直接把请求头补齐降低抓取频率。如果你的程序已经正常抓了几页突然开始连续返回403那大概率是对服务器做出了“异常访问”的判断这时候最好的办法就是停下来休息几分钟把delay调大而不是立刻换个IP继续猛冲。500错误通常是服务器内部问题也可能是你的请求触发了某个服务端的异常逻辑。遇到500先查自己的请求头和参数再用浏览器打开同样的URL看是否能正常访问。如果浏览器也打不开那就是对方网站的问题不用管。5.2 页面结构变化导致解析结果为空CSDN前端会不定期改版。你今天写好的div.article-list可能下个月就变成了div.blog-list-box。这是爬虫维护成本的主要来源。我的经验是在解析函数里加一个简单的断言一旦结果为0就输出HTML片段items soup.select(div.article-list article) if not items: print([调试] 未找到文章列表HTML片段如下) print(soup.prettify()[:3000])这样在页面改版的第一时间你就能看到真实结构而不是对着簿一弘的空气发呆。永远不要把选择器写死得过于精确尽量选择那些“语义稳定”的标签。比如h4、a这类标签名称通常不会变但class很容易变所以我会优先考虑用标签加相对位置来定位。5.3 分页只抓到了第一页如果你发现程序只跑了第一页就停下来了先确认是不是第二页返回的文章列表本身就是空的。有可能是因为第二页及之后的页面URL规则不是简单的数字递增也可能是你在页面上看到的数字页码需要先登录才能访问甚至可能是你的session没有保持Cookie。另外注意一个很常见的逻辑错误如果你在循环里提前break了一定要注意max_pages与真实页数的关系。我习惯打印“当前页 / 总共发现文章数”这样一眼就能看出分页是否提前终止。5.4 关于登录态和Cookie的说明CSDN部分页面需要登录比如点赞、收藏这些操作但公开博客文章列表是不需要登录的。这个项目里不要碰登录一方面是因为涉及个人账号行为和隐私边界另一方面是技术稳定性的问题Cookie会过期一旦过期你的爬虫就变成了一堆401。如果你未来确实要爬登录后的内容我建议你研究一下CSDN开放API或者用官方提供的导出能力而不是去模拟登录这条路维护成本极高。5.5 数据准确性的讨论关于阅读数里的“万”单位我在这里多写几句。如果你要统计超过1万阅读的文章正则(\d)只取到整数部分结果就完全错了。一个更健壮的解析函数应该这样写def parse_read_count(text): text text.replace(阅读, ).strip() if 万 in text: return int(float(text.replace(万, )) * 10000) return int(text)注意1.2万要先转成float再乘10000最后转换成int。如果直接int(1.2)会直接报错。这种小细节只有在真实数据集里才会遇到处理好了你的爬虫才算真正达到了“可用”的级别。6. 后续可以怎么玩6.1 扩展功能正文内容提取和词频统计文章列表爬下来只是第一步。你可以顺着每篇文章的link字段再去抓取正文内容。CSDN博文正文所在的节点一般是#article_content或者是div.article_content和列表页一样抓到HTML后用Soup提取正文过滤掉代码块再做词频统计看看这位博主常年都在写什么主题。这其实就是一个非常有意思的博主画像项目。但请注意全量抓取一个人所有文章的正文请求量会成倍增加。我的经验是把延时进一步加大到3秒左右并且分段跑完每抓200篇文章停下来休息几分钟。别贪多一次抓几千篇很容易把请求IP推上风口浪尖。6.2 定时任务每天自动更新作者文章列表如果你想追踪一位博主的新文章可以把爬虫脚本变成定时任务。Windows上可以直接用“任务计划程序”macOS/Linux上则用cron比如每天早上9点跑一次脚本把新文章追加到CSV后面。代码本身不需要特殊改动但建议你在脚本里增加一个“更新模式”只抓第一页然后把新标题与已有数据比对避免重复。6.3 面对更复杂的页面时该怎么办等你玩转了requestsBeautifulSoup可以慢慢接触一些更复杂的场景。比如有的网站列表是异步加载的你需要打开开发者工具在Network面板里找到XHR请求分析它的返回JSON有的网站内容经过接口签名加密那就需要逆向JS逻辑还有的网站强烈依赖Cookie登录态那就需要考虑自动化浏览器。学爬虫的过程就像技能树一开始的静态HTML只是最低处的果实越往上爬越复杂思路也从“解析HTML”渐渐变成“模拟数据接口调用”。我个人在实际操作中的体会是这个项目里最值钱的不是那几十行代码而是你建立的“先观察、后编码、勤记录、重礼貌”的工作习惯。做爬虫最怕的就是闷头写一堆代码然后对着报错干瞪眼。你先打开浏览器、按F12、刷新页面、看Network里到底发了多少个请求哪个请求返回了你需要的数据这个过程做完其实整个爬虫的轮廓就浮出水面了。最后再分享一个小技巧遇到任何解析问题先别急着搜代码把网页源码完整保存到一个.html文件里然后用编辑器CtrlF搜你想抓的那个字段名。你能用肉眼在源码里定位到它代码就一定能定位到它你在源码里都找不到它那大概率这个页面就不是纯HTML渲染的该去研究XHR请求了。这个习惯让我少走了无数弯路今天也一并交给你。