Python爬虫实战:Lofter批量抓取与分类归档全解析
简介本资源是一套面向Python初学者与数据采集实践者的Lofter平台内容批量爬取与分类保存实战方案聚焦社交媒体轻博客数据的合规采集、结构化解析与本地归档。资源包共76个文件含12个核心Python脚本如作者图文提取、标签解析、登录模拟等模块、53张示例截图与4张实测图片辅以README.md、小白教程.md、更新记录计划等说明文档以及txt_list、img_list等辅助清单文件整体压缩后仅6.41MB轻量易部署。目前已有108人学习下载适合希望掌握requestsBeautifulSoup实战、应对反爬策略如User-Agent轮换、访问间隔控制、构建多级目录分类体系的学习者。读者可直接复用完整爬虫框架理解Lofter网页结构分析逻辑获得从登录、列表页解析、详情页提取到按作者/标签/日期自动归档的全流程代码实现与工程组织方式。1. 项目起源我被自己的收藏夹逼成了爬虫选手先说说这个项目是怎么来的。我在Lofter上关注了不少创作者有画手、写手还有几个做摄影日志的博主。时间一长收藏夹里攒了上千条内容。Lofter本身的搜索和分组功能说不上难用但当我想把某个作者的全部文章、某类标签下的所有图片拉到本地做备份时手动一条一条复制根本不现实。那段时间我正好在系统学Pythonrequests库刚玩熟。于是顺手写了一个脚本目标很明确输入作者主页地址或标签地址脚本自动遍历所有文章抓取正文、图片、发布时间和标签然后按照“作者/日期/标签”的规则分类保存到本地。一开始只想解决自己的归档需求后来不断踩坑、重构慢慢变成一个结构还算完整的爬虫小项目。这个项目适合什么人参考第一刚学完Python基础、想拿真实网站练手的爬虫初学者第二有Lofter内容备份需求、但不想用第三方工具的用户第三想了解“批量下载任务如何做分类存储”这类工程组织问题的开发者。我会从请求、解析、存储、稳定性四个层面完整拆一遍代码部分可以直接抄。有一点先声明这个项目只抓取线上公开可见的内容不涉及任何私密信息、非公开接口破解也不做高并发抓取。写爬虫之前先想清楚“这个数据是不是公开的、抓下来会不会给别人造成困扰”是基本的职业素养。2. 从页面结构反推数据入口Lofter的URL规律与请求参数2.1 三类主要页面作者主页、标签页、搜索页Lofter页面的结构比较规整数据类型主要来自三类入口作者主页https://用户名.lofter.com或者https://用户名.lofter.com/view展示该用户发布的所有文章列表。标签页https://www.lofter.com/tag/关键词展示该标签下的公开内容。搜索页https://www.lofter.com/search?q关键词按关键词全文搜索。这里最关键的一个规律是Lofter的列表页是懒加载的滚动到页面底部才会加载更多内容。如果直接请求列表页的HTML只能拿到第一屏的几篇文章拿不到完整数据。解决办法有两种思路思路一模拟浏览器滚动。用Selenium或Playwright控制浏览器自动滚动到底部、等待内容加载。好处是符合用户操作逻辑坏处是速度慢、资源占用大而且对爬虫学习项目来说引入浏览器驱动有点重。思路二抓XHR接口。打开浏览器开发者工具切到Network面板在列表页不断往下滚观察发出的XHR请求。Lofter的列表加载接口通常会返回一段JSON或者DWR格式的文本里面包含文章的基本信息标题、链接、发布时间、摘要、缩略图地址等。这种方式比解析HTML高效得多数据也更结构化。我最终采用的是思路二。具体做法是用Chrome开发者工具分析网络请求找到列表加载接口的URL规律和请求参数然后把它们直接用在requests请求里。这样脚本不依赖浏览器跑起来非常轻快。2.2 请求头里容易忽略但必须带上的参数爬Lofter的请求头配置我踩过不少坑。最基本的User-Agent必须改掉requests默认的python-requests/2.x会被识别为脚本访问早期版本甚至直接拒绝服务。这里建议用一个完整的浏览器UA。还有一个容易忽略的点是Referer。如果你的爬虫只抓正文页还好但如果你想抓列表接口Lofter的接口会校验Referer来源。我实际测试中不带Referer的请求偶尔能返回数据但带上的成功率更高所以干脆在Session级统一设置。Cookie分两种情况。抓公开内容时Lofter大部分页面不强制登录也能浏览。但如果抓取频率稍微高一点或者翻页翻得特别深服务端可能要求验证身份。我的做法是先手动在浏览器里登录一次Lofter把Cookie复制到代码配置文件里。这个Cookie能显著降低被要求验证的概率。不过要注意Cookie有有效期过期了重新复制就行。import requests from fake_useragent import UserAgent def build_session(): session requests.Session() session.headers.update({ User-Agent: UserAgent().random, Referer: https://www.lofter.com/, Accept: application/json, text/javascript, */*; q0.01, Accept-Language: zh-CN,zh;q0.9,en;q0.8, X-Requested-With: XMLHttpRequest }) # 从配置文件读取Cookie这里用字典形式 session.cookies.update(load_cookies_from_config()) return session2.3 列表接口的翻页参数与数据流Lofter的列表接口返回格式在历史上变过几次。早期是DWR协议文本后来又改过JSON。我的脚本里同时兼容两种解析方式逻辑是如果响应以//开头或者包含dwr字样就走DWR解析如果是标准JSON就直接response.json()。翻页参数的核心是一个时间戳相关的标识。接口通过time参数控制返回某一时刻之前的文章列表下一页的time取值通常是当前返回列表里最后一篇文章的发布时间戳。这套逻辑和微博、贴吧这类时间线产品类似。换句话说你首先要从响应体里解析出“下一页游标”下一次请求再带上。如果你现在打开开发者工具发现参数名变了也没关系核心逻辑不变观察翻页时哪个参数变化了哪个参数决定了返回内容的偏移量把它记录下来。我强烈建议写爬虫之前花半小时做这个“人工分析接口”的动作比盲目翻HTML靠谱得多。3. 抓取与解析正文、图片、标签的提取策略3.1 列表页解析和详情页解析分层处理我设计了两层抓取第一层是“列表层”。从列表接口拿到一篇文章的标题、链接、发布时间、标签、封面缩略图。这一层的数据已经可以支撑做索引或目录了。但正文内容、图片原图、完整标签列表还在详情页里所以需要第二层。第二层是“详情层”。对列表里的每个文章链接发起请求用BeautifulSoup解析详情页HTML。为什么分开两层而不是直接抓详情页因为列表接口一次能拿到几十篇文章的信息批量获取效率高。如果直接遍历页面里的链接去抓详情等于把列表页当成唯一数据源还得额外写一套HTML列表解析逻辑。分层的好处是职责清晰列表层只需要负责“找出有哪些文章”详情层只负责“抓某一篇文章的完整内容”。3.2 BeautifulSoup提取正文和标签的细节详情页HTML结构里正文通常在一个div或article容器内图片则是img标签。我用CSS选择器定位主内容区然后提取文本和图片地址。from bs4 import BeautifulSoup def parse_post_detail(html, post_url): soup BeautifulSoup(html, html.parser) # 标题优先从og:title meta标签拿备选是h1 title_meta soup.select_one(meta[propertyog:title]) title title_meta.get(content) if title_meta else soup.select_one(h1).get_text(stripTrue) # 发布时间 time_meta soup.select_one(meta[propertyarticle:published_time]) publish_time time_meta.get(content) if time_meta else # 标签 tags [a.get_text(stripTrue) for a in soup.select(a.ctag)] # 正文区域 content_selector soup.select_one(div.post-cont) text_content content_selector.get_text(\n, stripTrue) if content_selector else # 图片原始地址 images [] if content_selector: for img in content_selector.select(img): src img.get(data-origin) or img.get(src) if src and src not in images: images.append(src) return { title: title, publish_time: publish_time, tags: tags, text: text_content, images: images, url: post_url, }这里有个细节值得说抓图片时优先取>import os import uuid def download_image(session, url, save_dir): os.makedirs(save_dir, exist_okTrue) ext os.path.splitext(url.split(?)[0])[1] if ext.lower() not in (.jpg, .jpeg, .png, .gif, .webp): ext .jpg filename f{uuid.uuid4().hex}{ext} filepath os.path.join(save_dir, filename) try: with session.get(url, streamTrue, timeout30) as r: r.raise_for_status() with open(filepath, wb) as f: for chunk in r.iter_content(chunk_size8192): f.write(chunk) return filepath except Exception as e: print(f[图片下载失败] {url} - {e}) return None图片文件名用UUID随机生成而不是用文章标题。原因在于同一篇文章里可能有几十张图标题相同如果只用标题命名会互相覆盖。虽然可以加序号但还要处理特殊字符问题。UUID方案最简单不会重名图片内容本身可以在文章正文里按顺序体现。4. 分类保存的核心目录结构设计与文件名安全4.1 为什么“分类保存”是项目的灵魂标题里“批量爬取”和“分类保存”是并列关系但实际做下来真正决定这个脚本好不好用的是分类保存的部分。抓到一万个文件堆在一个文件夹里那不叫归档叫垃圾场。分类的意义在于让未来的自己能在几秒内找到特定内容。我的目录结构设计成三层output/ └── 作者名称/ └── 2025-06/ ├── 2025-06-15_文章标题.md └── images/ ├── a1b2c3d4....jpg └── e5f6a7b8....png第一层是作者。因为Lofter的内容天然按用户组织按作者分类能最快定位内容来源。第二层是年月方便按时间线回溯。第三层是单篇文章正文存成Markdown文件图片放在该文章目录下的images/子目录。如果爬的是标签页就把第一层换成标签名。如果爬的是搜索页第一层换成搜索关键词。这样无论数据来自哪个入口都能在磁盘上形成可读性很强的树状结构。4.2 文件名清洗防止非法字符和路径过长Windows文件系统对文件名有特殊要求\ / : * ? |这些字符不允许出现在文件名里。而文章标题里很容易出现冒号、引号、问号。如果不做清洗保存文件的时候直接报错。import re def sanitize_filename(name, max_length80): name re.sub(r[\\/:*?|], _, name) name name.strip().strip(.) if len(name) max_length: name name[:max_length] return name or untitled还有路径长度问题。如果作者的Lofter用户名很长加上日期和标题再嵌套图片目录很容易超过Windows默认的260字符路径限制。我处理的办法是控制标题截断长度最多保留80个字符。另外在保存之前先os.makedirs(dir_path, exist_okTrue)确保每一级目录都存在。4.3 边抓边存还是抓完再存这里我采用的策略是“边抓边存”。每拿到一篇文章的详情数据立刻调用save_post()写到磁盘然后才去抓下一篇。好处很明显如果中途网络中断或程序崩溃已经保存的数据不会丢下次跑脚本时跳过已存在的文件即可。如果选择“全部抓完再统一保存”一旦程序在中途异常退出前面所有请求都白费了。尤其抓几百篇文章时请求时间很长中途失败的概率不低。边抓边存配合日志记录是最稳妥的方案。def save_post(post, root_dir): author_dir os.path.join(root_dir, sanitize_filename(post[author])) month_dir os.path.join(author_dir, post[publish_time][:7]) images_dir os.path.join(month_dir, images) os.makedirs(images_dir, exist_okTrue) filename f{post[publish_time][:10]}_{sanitize_filename(post[title])}.md filepath os.path.join(month_dir, filename) if os.path.exists(filepath): return filepath # 下载图片 local_images [] for url in post[images]: local_path download_image(session, url, images_dir) if local_path: local_images.append(os.path.basename(local_path)) # 写Markdown with open(filepath, w, encodingutf-8) as f: f.write(f# {post[title]}\n\n) f.write(f- 作者{post[author]}\n) f.write(f- 时间{post[publish_time]}\n) f.write(f- 链接{post[url]}\n) f.write(f- 标签{, .join(post[tags])}\n\n) f.write(---\n\n) f.write(post[text]) f.write(\n\n---\n\n) for name in local_images: f.write(f![{name}](images/{name})\n\n) return filepath这里有一个取舍正文里如果包含图片写Markdown时用相对路径引用这样整个目录拷走之后图片还能正常显示。如果用绝对路径换一台电脑就全废了。5. 稳定性与反爬请求频率、重试策略与异常处理5.1 请求频率用随机延时替代固定延时Lofter对短时间高频请求的检测很明确。我第一次写的时候大意了直接用for循环跑每篇文章间隔0.2秒结果跑到第40篇左右开始出现验证页面然后IP被限流了大概几小时。从那之后我就把限速当成整个项目最重要的一环。正确做法是每次请求之间加随机延时让请求间隔像人的操作习惯一样不规则。import time import random def polite_sleep(min_seconds1.5, max_seconds3.5): time.sleep(random.uniform(min_seconds, max_seconds))这个区间的选择要平衡效率和安全。抓一篇文章平均需要2到3秒如果抓500篇总耗时大概20到25分钟。对归档任务来说完全能接受。如果你觉得慢可以适当压缩到1秒到2秒但不建议低于1秒尤其是连续翻页的时候。还有一个技巧把延时逻辑封装进统一的fetch()函数而不是在每次调用requests.get()的地方单独写。这样后续想调整频率只需要改一个地方。5.2 指数退避重试应对偶发超时和限流响应网络请求不可能每次都成功。超时、503、429都是常见状态。我的重试策略是“指数退避”第一次失败等2秒重试第二次等4秒第三次等8秒最多重试5次。超过5次就放弃把URL记录到失败日志里。import time import requests def fetch_with_retry(session, url, max_retries5, **kwargs): for attempt in range(max_retries): try: response session.get(url, timeout30, **kwargs) if response.status_code 200: return response elif response.status_code in (403, 429, 503): wait 2 ** attempt random.random() time.sleep(wait) else: print(f[HTTP {response.status_code}] {url}) return None except requests.RequestException as e: wait 2 ** attempt random.random() time.sleep(wait) print(f[重试耗尽] {url}) return None为什么用指数退避而不是固定间隔重试因为如果是限流导致的失败固定间隔可能刚好卡在对方的限流窗口期里每次都触发限制。指数退避等于让服务端有充分时间恢复这是比较通用的处理方式。5.3 验证码与封禁信号遇到就停别硬刚必须说清楚爬虫遇到验证码最理智的应对不是“破解验证码”而是“停止抓取降低频率过一段时间再试”。试图绕过验证码不仅技术复杂而且很容易触碰到合规红线。我自己的判断规则是信号处理返回页面中包含“验证”等字样立即暂停整个任务等待30分钟连续出现多次403说明UA或频率有问题检查配置停止抓取返回空数据但状态码200说明接口参数或Cookie可能失效重新分析请求如果你跑了一个小时之后突然遇到验证最稳妥的做法是让程序停手第二天继续。断点续爬的机制保证停手不会导致数据丢失这一点后面会讲到。5.4 用Session复用连接减少不必要的开销requests的Session对象会自动保存Cookie并且默认启用连接池复用这样多个请求之间可以复用TCP连接速度更快对服务端的压力也小一些。我全程使用同一个Session实例而不是每次requests.get()新建。session build_session() for post in post_list: resp fetch_with_retry(session, post[detail_url]) # 处理... polite_sleep()Session复用还有一个隐藏好处如果登录Cookie在第一个请求里设置过后续请求会带上相同Cookie行为更像一个真实浏览器。6. 断点续爬与增量更新让脚本可以反复运行6.1 已抓取进度记录任务中断不白跑上面说到了边抓边存这已经解决了“中断丢数据”的问题。但“跳过已保存文件”的逻辑还需要再强调一下。save_post()里检查os.path.exists(filepath)如果文件已存在直接返回不重复下载图片。这就是最简单实用的断点续爬机制。不过单纯靠文件系统判断有一个盲区如果文章本身被更新了比如作者编辑了正文文件已存在的话就不会重新抓。对归档场景来说这其实不是什么大问题。如果你确实需要追踪更新就改用“按最后修改时间判断是否重新抓取”或者维护一个记录已抓取URL和抓取时间的SQLite数据库。我一开始用文件判断后来为了做增量更新在项目里加了一个progress.json来记录状态。{ last_crawl_time: 2025-06-15 22:13:00, crawled_urls: [ https://xxx.lofter.com/post/123, https://xxx.lofter.com/post/456 ] }每次成功抓完一篇文章就把URL追加进去。下次运行时加载这个文件遇到重复URL直接跳过。6.2 增量更新只抓新增和变化的内容增量爬取的核心思路是记录“增量游标”。对于作者主页的时间线数据来说下一页游标就是时间戳。每次抓完一轮记录当前抓到的最后时间下一次从该时间点之前的接口继续请求直到没有新数据为止。我做的增量更新方案是这样的加载上次记录的“最新一篇文章的发布时间”。第一次请求时不指定time参数获取最新内容。对比返回结果里是否有新文章如果有抓取详情。更新last_crawl_time保存进度。这样平时维护作者主页每天跑一次增量更新大约只需要几秒钟就能完成对服务器几乎零压力。6.3 扩展方向定时任务、日志与通知爬虫跑起来之后稳定运行的最后一公里在于运维。我的项目在后期加了三个功能日志模块用标准库logging输出到文件每次抓完记录成功数量、失败URL、运行耗时。排查问题时不用靠猜。定时执行用系统自带的cronmacOS/Linux或计划任务Windows每天凌晨跑一次增量更新。定时任务的频率不宜太高一天一次足够。失败通知如果某次任务失败UR过多通过微信或邮件发一条提醒。我用的Server酱和SMTP都可以。不做通知也行日志已经足够但对持续性维护来说通知能让你第一时间发现问题。import logging logging.basicConfig( filenamecrawler.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def log_summary(total, success, failed): logging.info(f任务完成共 {total} 篇成功 {success} 篇失败 {failed} 篇)提一句定时跑爬虫的前提是你确定自己有权限持续访问这个站点。如果对方明确在robots协议或者用户协议里禁止抓取就别定时了遵守规则比技术实现更重要。7. 最后的几点实战体会如果你也准备写这个项目我在实际使用过程中有几个体会写在这里当参考。第一先手动抓一个页面彻底解析好再写循环。不要一上来就写完整脚本你会发现排错极其痛苦。正确路径是先用requests拿一个详情页HTML存成sample.html在Jupyter里反复解析确认标题、正文、图片、标签都能取到然后再整合成完整脚本。第二Cookie不要硬编码在代码里。写在代码里每次过期都要改代码容易漏改。用一个config.json或者环境变量存Cookie脚本运行时读取。这样换Cookie只需要改配置文件不动代码。第三图片下载失败不能导致整个文章保存失败。一篇文章里有图挂了正文仍然值得保存。我的做法是在save_post里对每个图片单独try/except下载失败的图片在Markdown里保留原始URL后续可以手动补抓。第四不要同时爬多个作者、多个标签。单个线程、串行请求、限速运行虽然慢但稳定。多线程并发看着快实际踩坑成本很高。这个项目追求的是可靠归档不是速度赛跑。这个爬虫我用了大半年中间也维护过几次。Lofter页面改版之后解析代码会失效但只要理解了“列表接口分页”和“详情页字段提取”这两个核心逻辑重新适配只需要改选择器和参数名大框架完全不用动。这也是我推荐用Python写这类项目的原因库生态成熟requests加BeautifulSoup组合已经能覆盖绝大多数场景遇到改版也好调整。本文还有配套的精品资源点击获取

相关新闻