ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

百度贴吧爬虫实战:从requests到scrapy的数据采集全攻略

百度贴吧爬虫实战:从requests到scrapy的数据采集全攻略 贴吧大概是国内最古老也最长寿的论坛形态之一从“帝吧出征”到各种兴趣吧沉淀下来的长尾内容至今还在源源不断地产生。做过几年数据采集的人都知道贴吧是一个很典型的半静态站点——列表页是服务端渲染详情页又有异步加载反爬不算凶但小坑不断。我之前给一个舆情项目做数据源时花了两个晚上把百度贴吧爬虫完整趟了一遍从requests直接怼到scrapy重新封装都试过。这篇文章把整个实现思路、踩坑记录和可以直接抄作业的代码都梳理出来给准备做社区类数据采集的同学一个参考。1. 项目整体思路与目标拆解1.1 为什么选贴吧作为爬虫练习场景很多新手第一次写爬虫不是爬豆瓣就是爬贴吧这个选择其实有道理。贴吧的HTML结构相对规整不像现在很多网站恨不得把整个页面拆成十几个接口也不像短视频平台那样需要逆向加密参数。贴吧的核心数据——帖子标题、回复数、最后回复时间、楼主ID、楼层内容——基本都是直接嵌在HTML或者简单的JSON接口里用requests加BeautifulSoup就能搞定。我从实际的爬虫项目角度来说它也是一个很好的练手样本既有列表页的翻页逻辑又有详情页的异步楼层加载还涉及图片抓取、编码处理、请求频率控制这几个经典问题。做爬虫前一定要先想清楚目标。贴吧相关的内容采集通常有三个层次列表页采集抓取某个吧的主题帖列表拿到帖子标题、链接、回复数、作者等索引数据。这类数据适合做趋势分析、热门话题监控。详情页采集进入具体帖子抓取每一楼的正文内容和发帖人。这是做文本分析、舆情分析的主要数据来源。多媒体资源采集把帖子里附带的图片、视频等其他资源下载到本地。适合做素材库或者数据备份。我这次的目标是前两个层次顺手把图片下载一起做了。如果你的目标是定向监控某个吧的新帖那还需要加定时调度和增量抓取这个后面会专门讲。1.2 技术选型requests方案还是scrapy框架技术选型是很多人纠结的第一关。我在这个项目里用了requests加BeautifulSoup的组合配合fake_useragent和sqlite3做持久化。坦白说如果你只是跑一个吧的数据或者想快速验证思路requests方案绰绰有余。它的好处是逻辑直观每一行代码都在掌控之中出问题了好排查。我在实际工作中跑过一批覆盖三十多个吧的采集任务单机四五个线程一天下来数据量大概在几十万条requests方案完全没有瓶颈。但如果你要抓的是几十上百个吧、需要反复增量更新、还要跑分布式那我建议直接用scrapy。scrapy的并发模型比手写threading优雅得多内置了请求去重、失败重试、pipelines存储等一系列机制后期维护成本低很多。我自己的经验是先用手写requests把全流程跑通一次把URL规律、页面结构、反爬策略摸清楚再决定要不要迁移到scrapy。直接上手scrapy反而容易被框架的复杂度劝退因为你得同时理解框架本身的机制和目标站点的逻辑。这个项目的核心链路是构造请求头 → 请求列表页 → 解析帖子索引 → 请求详情页 → 解析楼层内容 → 提取数据 → 入库。每个环节都有几个容易踩的坑下面详细拆开讲。2. 环境准备与核心模块设计2.1 环境依赖与工程目录结构整个项目只需要Python 3.8以上环境依赖库也非常少。建议用虚拟环境管理避免把系统Python搞乱pip install requests beautifulsoup4 lxml fake-useragent这四个库的分工很清晰requests负责发HTTP请求beautifulsoup4负责解析HTMLlxml是beautifulsoup的解析引擎比默认的html.parser快不少fake-useragent用来随机生成User-Agent。如果你要处理异步加载的内容还需要额外装一个requests-html或者直接上playwright这个后面会说。工程目录我习惯这样组织tieba_spider/ ├── config.py # 配置项目标吧名、请求间隔、存储路径 ├── crawler.py # 核心爬虫逻辑列表页和详情页抓取 ├── parser.py # 页面解析解析函数独立封装 ├── storage.py # 数据存储sqlite写入 ├── main.py # 入口参数解析和任务调度 └── data/ # 数据输出目录模块划分没必要太细但解析和抓取一定要分开。原因很简单网页改版太频繁了贴吧平均每隔一段时间就会微调一下HTML结构到时候你只需要改parser.py而不用动抓取逻辑。我见过不少项目把解析代码和请求代码揉在一起改一次结构就要从头捋一遍非常痛苦。2.2 请求头伪装与常见反爬机制贴吧的反爬不算强但不代表没有。实际测试下来主要就三样请求频率限制、User-Agent检测、以及部分敏感内容的Cookie校验。先说User-Agent。fake-useragent库可以随机生成UA但用的时候有个小坑——它默认会从远程服务器拉取UA列表如果网络不稳定或者被墙了就会报错。解决办法是把它改成从本地文件读取或者直接手动维护一个UA列表。我在项目里用手动维护的方式稳定且不依赖外部服务USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0, ]除了UA还有两个请求头值得带上Accept-Language和Referer。贴吧对Referer不是特别严格但带上总没错。完整的headers构造headers { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, Referer: https://tieba.baidu.com/ }再说Cookie。正常情况下不用带Cookie也能访问贴吧的列表页和详情页但如果某些帖子涉及敏感话题或者需要登录才能查看的内容服务端会校验Cookie。我处理的方式是先用浏览器登录一次贴吧从开发者工具里复制Cookie到配置文件中请求时自动带上。注意Cookie会过期跑长任务时最好设计一个过期自动退出的机制方便及时发现。请求频率方面贴吧的限流策略比较隐晦不会直接封IP更多是返回验证码或者空数据。实测下来单IP每秒不要超过2个请求连续跑30分钟最好休息几分钟。我通常的做法是每个请求间隔随机0.3到1秒这样既不会明显拖慢速度也不会触发限流。2.3 Cookie与登录态处理贴吧详情页的楼层加载如果帖子楼层数量超过一定阈值我记得是几百楼后面的楼层不是直接渲染在HTML里而是通过一个单独的JSON接口异步加载。这个接口需要带一个tbs参数这个参数是从cookie或者页面源码里取的。第一次跑的时候我没注意结果发现只能爬到前几百楼后面的楼层死活出不来。tbs参数的获取其实很简单随便打开一个贴吧详情页在HTML源码里搜一下tbs就能找到。它是一个很短的时间戳字符串。可以用正则直接提取或者用BeautifulSoup从script标签里解析。提取到之后存到全局变量里后续的异步楼层请求都带上这个参数。还有一种更省事的方案就是直接用tieba.baidu.com/p/帖子ID?pn页码这个带分页参数的形式。贴吧的楼层分页有几种URL风格老版本是?pn2这种新版本还支持?pn2see_lz1只看楼主。只要理解了URL参数的含义翻页逻辑就很简单了。3. 数据抓取完整实操从列表页到详情页3.1 列表页解析翻页规律与字段提取列表页的URL格式非常固定https://tieba.baidu.com/f?kw吧名ieutf-8pn页码。注意ieutf-8是编码参数带上可以用UTF-8编码解析否则默认可能是GBK解析中文会出问题。pn的值是翻页的关键——第1页是0第2页是50第3页是100也就是每页50个帖子页码等于(页数-1)*50。用requests拿到页面HTML后先用BeautifulSoup定位到div.threadlist_title或者a.j_th_tit这类元素。不同时期的贴吧模板类名会变我跑的时候用的是a.j_th_tit提取标题和链接这个类名在很长一段时间内都比较稳定。每个帖子的数据结构大概是标题a.j_th_tit的文本链接a.j_th_tit的href属性后面拼接帖子ID作者span.frs-author-name的文本回复数span.threadlist_rep_num或类似元素的文本最后回复时间span.threadlist_reply_date或span.threadlist_time的文本写列表页解析函数时要注意先确认页面是否为空。贴吧有一些吧是完全没人发帖的列表为空还有一些吧可能因为内容原因被隐藏了这时候解析结果为空数组需要判断一下避免空跑浪费流量和请求次数。def parse_list_page(html): soup BeautifulSoup(html, lxml) threads [] for item in soup.select(li.j_thread_list): link item.select_one(a.j_th_tit) if not link: continue title link.get_text(stripTrue) href link.get(href, ) author_el item.select_one(span.frs-author-name) reply_el item.select_one(span.threadlist_rep_num) threads.append({ title: title, url: fhttps://tieba.baidu.com{href} if href.startswith(/p/) else fhttps://tieba.baidu.com/p/{href.split(?)[0].split(/)[-1]}, author: author_el.get_text(stripTrue) if author_el else , reply_num: int(reply_el.get_text(stripTrue)) if reply_el else 0, }) return threads3.2 详情页解析楼层结构与楼层分页进入帖子详情页URL格式是https://tieba.baidu.com/p/帖子ID。页面主体由一个个楼层组成每个楼层对应div.l_post容器。楼层里最核心的字段是楼层数div.post-tail-wrap里的内容或者楼层回复按钮的>def parse_detail_page(html, post_id): soup BeautifulSoup(html, lxml) floors [] for post in soup.select(div.l_post): content_el post.select_one(div.d_post_content) author_el post.select_one(a.p_author_name) time_el post.select_one(span.post-tail) if not content_el: continue floors.append({ post_id: post_id, floor: post.get(data-floor, ), author: author_el.get_text(stripTrue) if author_el else , time: time_el.get_text(stripTrue) if time_el else , content: content_el.get_text(separator\n, stripTrue), }) return floors3.3 图片与附件资源下载方案帖子正文里的图片很多都是放在百度自己的CDN上URL格式一般是https://imgsa.baidu.com/...或者https://tiebapic.baidu.com/...。图片下载本身不难用requests的streamTrue模式流式写入本地文件。需要注意的坑有两个一是防盗链。有些图片的CDN会检查Referer如果Referer不是贴吧域名可能返回403。解决办法就是请求图片时把Referer设置成具体的帖子详情页URL。二是图片重复。同一个图片可能在不同楼层被引用多次下载前先检查本地文件是否存在用图片URL的MD5作为文件名天然去重。我在跑项目的时候发现有些热门帖子里图片量很大几千张图如果不去重很快就撑爆硬盘。简单封装的图片下载函数def download_image(img_url, save_path, referer): headers { User-Agent: random.choice(USER_AGENTS), Referer: referer, } resp requests.get(img_url, headersheaders, streamTrue, timeout10) if resp.status_code 200: with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size1024): f.write(chunk)3.4 数据存储SQLite轻量方案与字段设计存储层我用的是sqlite3标准库零依赖、单文件、够用。表结构设计上帖子、楼层、作者这几个实体分开存方便后续做关联查询。核心表大概长这样CREATE TABLE threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id TEXT UNIQUE, title TEXT, author TEXT, reply_num INTEGER, create_time TEXT, last_reply_time TEXT, url TEXT, crawled_at TEXT ); CREATE TABLE floors ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id TEXT, floor INTEGER, author TEXT, time TEXT, content TEXT, UNIQUE(post_id, floor) ); CREATE TABLE images ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id TEXT, floor INTEGER, img_url TEXT, local_path TEXT, UNIQUE(img_url) );唯一约束是必须的。爬虫重跑是常态没有唯一约束重复数据会把你烦死。写入时用INSERT OR IGNORE天然跳过已存在的数据。sqlite的并发写入不太行但单线程、单进程、间隔写入的情况下完全没问题。如果你是多线程写库记得把check_same_threadFalse加在连接参数里或者干脆用一个独立队列来执行写操作。4. 并发调度与反爬策略实测4.1 单线程、多线程与异步方案怎么选这个项目的抓取量如果不大几百个帖子单线程顺序跑完全够用。优点是不会触发任何限流代码也最好写。缺点是慢——一个帖子详情页平均要1秒算上列表页抓1000个帖子可能要20分钟以上。如果目标量级是几万个帖子单线程就不太行了。这时需要多线程。Python的多线程因为GIL的存在对requests这种IO密集型的任务反而很友好——大部分时间都花在等待网络响应上GIL不会成为瓶颈。我用的是concurrent.futures.ThreadPoolExecutor简单高效from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers4) as executor: future_map {executor.submit(crawl_post, post_id): post_id for post_id in post_ids} for future in as_completed(future_map): try: result future.result(timeout30) save_to_db(result) except Exception as e: logger.error(f任务失败: {future_map[future]}, 错误: {e})线程数不是越大越好。我实测过贴吧场景下4到6个线程是比较平衡的再往上走限流概率直线上升平均抓取速度反而下降。这里建议做一层请求频率总闸用token bucket或者简单的time.sleep随机等待来控制整体QPS上限。如果你的团队有分布式调度需求scrapy加scrapy-redis是成熟方案。不过对贴吧这种量级分布式多少有点杀鸡用牛刀。真正需要分布式的是那种全网级别的采集单机几个吧的数据完全没必要。4.2 User-Agent轮换与IP代理池的取舍IP代理池可能是很多爬虫教程里最喜欢渲染的内容了但贴吧这个场景真的不需要。如果你只是跑几个吧、几千个帖子单个IP加正常的访问频率完全够用。我之前用requests方案连续跑了三天没有触发过一次验证码。不过有一点要注意代理质量不行比不代理更糟。网上免费的代理池大部分是失效代理或者高匿度不够的代理用起来反而导致请求超时、返回异常数据。如果真的需要代理我建议用付费的、验证过稳定性的代理商API或者干脆用住宅代理IP。贴吧的服务端会检测数据中心IP就是机房IP普通机房的IP反而容易被重点关注。还有一点经验分享尽量不要用携程、去哪儿这些大厂的代理接口给贴吧用那些代理池多半是爬虫用户共用早已被各大网站标记命中率很低。真做项目的话直接买那种按流量计费、自动轮换的代理服务省心。4.3 验证码应对与频率过限的降级方案贴吧的验证码不是经常出现但一旦出现就是硬骨头。我遇到过一次是连续快速抓取后返回了一个要求输入验证码的页面。后来我总结出一套降级策略检测机制在解析函数里首先检查页面是否包含验证码特征。贴吧的验证码页面通常包含verify关键词或者是window.location.href跳转到verify.tieba.baidu.com。熔断机制连续检测到验证码3次以上立刻停止当前任务等待10到30分钟后再重试。不要硬怼硬怼只会触发更长的封禁周期。限速重试降低线程数把请求间隔从0.5秒拉到3秒以上重新跑。验证码自动识别这块虽然有打码平台可以用但为贴吧专门接打码平台有点大材小用。我的建议是能通过降低频率避开的就别花钱用打码平台。爬虫的本质是模拟人类访问人类看一个帖子不可能每秒点十次下一页。5. 常见问题与排查技巧实录5.1 乱码问题页面编码判断与处理贴吧历史上长期使用GBK编码后来页面主体切到了UTF-8但某些遗留页面、某些接口、某些CDN资源仍然可能返回GBK编码。如果你直接用resp.text去拿内容requests会先根据响应头里的charset参数猜测编码猜错的话就会出现中文乱码。我的处理办法是在拿到响应后手动检查编码。如果页面里包含charsetgbk或者charsetgb2312就用resp.encoding gbk重新解析如果是utf-8就用resp.encoding utf-8。一个更稳妥的方式是读原始的字节内容然后交给chardet库去判断import chardet def decode_response(resp): raw_data resp.content if resp.encoding: try: return raw_data.decode(resp.encoding) except UnicodeDecodeError: pass detected chardet.detect(raw_data) return raw_data.decode(detected.get(encoding, utf-8), errorsreplace)关键点一定不要直接信任响应头里的charset很多服务器返回的Content-Type头并不准确。用chardet检测虽然慢一点点但稳。5.2 翻页失败URL参数与for循环的坑翻页失败是新手遇到最多的问题。常见原因有这么几个一是pn参数计算错误。贴吧的页码从0开始第1页pn0第2页pn50第3页pn100。如果你用(page - 1) * 50第二页就是50没毛病。但如果你用(page - 1)那就永远停在第一页。这个错我见过不止一次。二是列表页和详情页搞混。列表页的URL参数是f?kwxxxpnyyy详情页的URL参数是p/帖子ID?pn页码。详情页的pn参数从1开始每页大约20到30楼但具体多少楼取决于页面渲染。详情页翻页时需要注意判断当前页面是否存在如果请求的页码超过了总页数页面会重定向回第一页。这时候你的代码如果不判断URL是否变化就会陷入死循环。三是处理完第一页后没有把for循环推进下去。很多人写完一个for page in range(1, max_page 1)循环后发现只跑了第一页调试了半天发现是max_page计算错了。贴吧的列表页总页数通常会显示在页面底部用BeautifulSoup解析ul.pagination或者span.page_count可以拿到。但保险起见我建议用“抓取到当前页没有内容就停止”的兜底逻辑防止服务端返回的空页面导致死循环。5.3 异步加载内容抓不到楼中楼与楼主发言的补充接口详情页的楼层主体内容在HTML里能抓到但楼中楼楼层内的回复和楼主发言只看楼主模式是单独接口。如果你的数据需求包含楼中楼仅靠解析HTML是拿不全的。楼中楼的接口是https://tieba.baidu.com/p/comment?tid帖子IDpid楼层IDpn页码返回JSON字段结构跟总评论接口类似包含回复者、回复内容、回复时间。调用这个接口时需要带上Cookie否则可能返回空数据。另一个异步场景是分享视频。现在很多贴吧帖子会直接嵌入B站或者小程序视频这类内容在HTML里只有一个iframe或者video标签实际的内容源在第三方服务器。如果你想抓视频本身就得顺着iframe的src去解析第三方页面复杂度会高不少。我在项目里直接跳过了这部分只抓纯图片资源。5.4 高频请求被封后的恢复策略即使前面做了各种限制长期跑还是可能被封。贴吧封IP的表现不是直接拒绝访问而是所有返回页面变成安全验证页。判断是否被封很简单检查响应URL是否跳转到了verify.tieba.baidu.com或者页面HTML里是否包含请输入验证码字样。恢复策略我按严重程度排个序轻度限制返回页面偶尔出现验证码。解决方式是降低频率暂停5到10分钟再继续。中度限制连续多次返回验证码或者API接口返回空数据。解决方式是停止任务至少30分钟同时将并发数降到2以下请求间隔拉长到5秒以上。重度限制IP被拉黑所有页面都跳转验证。这种只能换IP了。如果是家庭宽带重启路由器大概率能换IP如果跑在云服务器上就得换一台机器或者买新的代理。在代码层面我封装了一个简单的状态检测函数每次请求后都检查是否进入验证页。一旦发现异常就把当前进度保存到本地进度文件比如把已经完成的任务ID列表存成json下次启动时从断点续跑。断点续跑对长任务太重要了没有这个机制一次封禁就可能浪费之前几小时跑到的所有进度。6. 成果展示与后续扩展方向6.1 数据可视化与简单分析示例数据抓下来后如果只是躺在sqlite里就太可惜了。我用抓到的数据做过两件有意思的事一是统计某个吧的每日发帖量变化趋势看热度的波动规律二是用jieba分词对帖子正文做词频统计看大家在讨论什么。每日发帖量统计其实很简单按日期字段做group by就行SELECT substr(create_time, 1, 10) as date, count(*) as cnt FROM floors GROUP BY date ORDER BY date;词频统计用jieba加collections.Counter几十行代码就能跑出结果。如果你抓的是特定领域贴吧比如某个游戏的攻略吧词频统计可以快速帮你定位到这个阶段大家都在关注什么装备、什么副本、什么角色。可视化的话我用的matplotlib直接把折线图、柱状图画出来够用了。如果你想做交互式仪表盘可以考虑streamlit或者flask加echartsPython后端加JavaScript前端网上模板很多搭起来也不费劲。6.2 从requests迁移到scrapy的注意事项前面提到过如果你觉得数据量会持续增长可以迁移到scrapy。迁移时最需要注意的点不是代码重写而是URL构造和解析逻辑的复用。scrapy的Rules和LinkExtractor虽然方便但贴吧这种URL规律性很强的站点直接在parse方法里用yield scrapy.Request更直观。scrapy里设置请求间隔非常方便DOWNLOAD_DELAY 1.0表示每个请求之间至少间隔1秒。配合CONCURRENT_REQUESTS_PER_DOMAIN 4可以精确控制对贴吧的访问频率比手写线程池的方式优雅很多。代理中间件在scrapy里也有成熟插件scrapy-proxy-middleware如果你之前用requests爬的时候使用了代理迁移成本主要在把原有的请求头构造逻辑改造成中间件形式。6.3 合规提醒与长期运行建议最后说一点合规的事情。爬虫本身不违法但爬什么、怎么爬、爬了干什么这些环节都可能触碰法律红线。贴吧的用户协议里明确写了未经许可不得抓取数据虽然实际上百度的态度是“睁一只眼闭一只眼”但如果你把抓取的数据用于商业变现或者大规模公开传播风险会急剧上升。我的建议是**只抓公开数据不碰任何需要登录才能看的私密内容。控制抓取频率不要对目标服务器造成压力。数据仅用于个人学习和研究不用于商业用途。如果做舆情监控类项目尽量使用官方提供的API或者数据服务。长期运行还有一个容易被忽视的问题存储空间和日志管理。sqlite单文件超过几个GB后写入性能会下降建议定期做数据归档或者分库。日志方面爬虫这种无人值守的任务日志一定要带上时间戳和请求URL否则排查问题时会怀疑人生。我习惯把日志同时输出到控制台和文件按天滚动保留最近三十天。这个习惯在跑长任务时帮了我大忙。贴吧这个项目做完后我把同一套思路迁移到了几个其他中文社区站点跑通后发现核心逻辑大同小异URL规律拼出来、解析模块独立、请求频率控制好、断点续跑兜底。爬虫的价值从来不在代码本身而在你对目标站点底层规则的理解和对数据链条的整体把控。希望这篇实战记录能帮你少走几步弯路。
返回列表