ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实战:Lofter内容批量爬取与自动分类归档方案

Python实战:Lofter内容批量爬取与自动分类归档方案 简介本资源是一套基于Python实现的Lofter平台内容批量爬取与智能分类保存实战项目面向爬虫初学者与数据采集爱好者解决社交媒体轻博客内容自动化采集、结构化存储与多维度归档的实际需求。压缩包共76个文件含12个核心Python脚本如作者图文提取、标签解析、登录模拟等模块、53张示例截图与4张实测图片辅以README说明、小白教程文档、更新计划及工具类辅助脚本整体大小为6.41MB目录组织清晰便于按功能模块快速定位与调试。已有108人学习下载提供完整可运行代码链路、常见反爬应对策略如User-Agent轮换、请求间隔控制、标准化数据保存逻辑按作者/标签/日期三级目录归档并附带requirements依赖清单与典型运行日志参考助力读者从零掌握动态网页抓取、HTML解析与工程化数据管理全流程。 《Python 基于爬虫技术的 Lofter 内容批量爬取与分类保存.zip》这个项目标题一看就知道是干嘛的用 Python 写一套爬虫把 Lofter 上某个标签页、某个用户主页或者某个合集下的内容批量抓下来再按作者、标签、文章类型或者发布时间自动归档到本地。Lofter 这个平台在国内同人创作圈子里用得非常多文章、图片、短篇小说、绘画作品散落在各个用户主页里如果你只是想备份自己喜欢的作者或者想对某一类题材做点文本分析和素材收集手动一页页翻简直要命。这套程序解决的就是这个重复劳动跑一遍能省下大半天的功夫。我先说下适合谁看你至少会用 Python 写脚本知道 requests 和 BeautifulSoup 的基本用法但对网页结构分析、分页处理、反爬应对这些还没形成体系。如果你想找一个完整的练手项目把“爬虫设计思路 请求数据解密 文件归档”一次性串起来这篇就是给你准备的。1. 项目整体设计与实现思路1.1 这个项目到底要解决什么问题Lofter 的页面结构有点特殊跟传统博客不一样。它不是一个页面里把所有内容都渲染好等你来看而是先给你一个空壳 HTML数据通过底层的 API 接口以 JSON 格式异步加载。这就导致很多刚入门爬虫的朋友第一眼看到网页源码时一脸懵——明明浏览器里显示得有图有文可 requests 抓回来的 HTML 里啥都没有。这就是为什么这个项目不能照搬“普通博客爬虫”的套路需要针对 Lofter 的数据加载机制做专门设计。从功能需求上看这个爬虫要解决的几个核心点包括顺着标签页或用户主页把所有文章的链接地址都拿到手进入每一篇文章的详情页把标题、正文、发布时间、标签、热度数据都抓下来正文里的插图要能批量下载到本地不能漏最后按“作者/合集/文章类型/月份”这种层级自动建目录把 Markdown 文件和图片文件分开放。这四件事听起来不难但真正做起来每一步都有小坑等着你。我后面会逐一拆开讲。1.2 技术选型为什么是 requests BeautifulSoup 额外接口解析先明确一个观点爬虫项目的技术选型不是越花哨越好而是越贴合目标网站的数据结构越好。网上有大量 Python 爬虫教程一上来就教 Scrapy 框架但 Lofter 这个场景用 Scrapy 有点杀鸡用牛刀而且排错成本高。这个项目我用的技术栈是这样的requests负责所有 HTTP 请求处理会话和请求头BeautifulSoup4解析 HTML 结构提取文章链接和页面信息json解析 API 返回的数据因为 Lofter 的正文内容很多是 JSON 里的字段传过来的re做正文清洗和标签提取处理字符串里的干扰内容pathlib或os负责生成归档目录、处理文件路径。整套下来没有任何一个冷门库只要你的 Python 环境是 3.7 以上版本pip 装一下就能跑不存在环境配置把人劝退的情况。为什么要解析接口而不直接解析 HTML这就是 Lofter 和普通网站最大的区别。你在浏览器里打开一篇 Lofter 文章正文是有的但很多列表页实际上只返回了一个空壳加一段 JavaScript 加密数据真正的文章资源是通过页面里的一段隐藏 JSON 字段传递的。你要是不懂这个逻辑直接在列表页找文章内容怎么找都是空。1.3 功能模块划分与工作流程我一开始设计这个爬虫的时候没有把它写成一个又长又乱的单文件脚本而是按功能拆成了四个模块后面排错和加功能都方便得多lofter_spider/ ├── config.py # 配置目标地址、请求头、保存路径 ├── fetcher.py # 负责请求页面和解析链接 ├── parser.py # 解析文章详情、提取正文和图片 ├── saver.py # 负责目录创建和文件保存 └── main.py # 主入口串起整个流程整个工作流程是这样的从配置里读取起始页标签页、用户主页或者合集页发送请求解析出当前页所有文章链接判断是否存在下一页循环直到所有文章链接都拿到对每一个文章链接发送详情请求拿到 JSON 数据包从 JSON 里解析出标题、正文、图片、标签、时间等信息根据配置的归档规则自动生成目录结构把正文保存为 Markdown 文件图片下载到指定文件夹输出本次抓取的统计信息比如成功多少篇、失败多少篇、耗时多久。这个流程看起来平平无奇但每个环节都有可以优化的细节尤其是第 4 步和第 5 步我踩过的坑比想象中多得多。→ 继续阅读核心细节解析2. 核心细节解析与实操要点2.1 Lofter 网页结构深度剖析数据到底藏在哪里要写明白爬虫就得先说清楚 Lofter 网页的数据加载机制。我拿一个典型的 Lofter 标签页来举例比如你在浏览器里打开某个标签页面正常显示 20 篇文章的卡片列表。这时候你按下 F12 看网络请求会发现页面加载过程发了一堆请求其中最重要的几个是这样的第一个请求是页面本身返回一个 HTML 文档。这个 HTML 里只有页面的框架和 CSS/JS 的引用地址文章列表的 DOM 结构是后面加载出来的紧接着浏览器会执行 JavaScript 脚本去请求一个内部接口接口地址通常长这样https://www.lofter.com/tag/{标签名}?page1接口返回的数据是 JSON 格式里面有个字段叫data这个字段下有一个post列表列表的每一项就是一张文章卡片的完整数据。这里有一个关键点post列表里的每一项虽然包含了文章的部分信息标题、摘要、作者、链接但正文内容并不在这个接口里。正文是在另一个详情接口里返回的需要你点进文章页面才能拿到。所以整个爬虫的设计思路就很清晰了第一步把列表接口的所有文章链接和基本信息抓下来第二步再逐一请求详情接口获取完整的正文内容。2.2 解密 JSON 数据包正文和图片的获取逻辑我第一次写完列表页爬虫后信心满满觉得文章链接都拿到了往下就是水到渠成的事。结果一请求详情页又给我上了一课。Lofter 的文章详情页是这样的你请求https://username.lofter.com/post/xxxxx返回的 HTML 里确实有正文但正文不在body标签的内容区而是放在了一段script标签里的 JavaScript 变量中。这个变量的名字通常是window.__INITIAL_STATE__或者window._posts里面是一大段 JSON 文本。你需要做的就是从这一段 JSON 里找到文章正文和图片信息。详细点说这个 JSON 的结构大致是{ post: { postId: 123456789, title: 文章标题, content: p正文HTML内容/p, tagList: [标签1, 标签2], time: 1710000000000, photoUrl: [], videoUrl: [] } }其中content字段是 HTML 格式的正文。图片不是单独存一个列表的而是穿插在正文 HTML 的img标签里需要你用正则表达式把src字段提取出来。这就是为什么我在技术选型里提到了re处理这类半结构化内容正则虽然不是唯一的办法但一定是最快的。2.3 请求头伪装与异常处理的关键参数关于请求头这个问题我见过太多人要么完全不设置要么只设置一个 User-Agent然后就抱怨被拒。Lofter 的服务器对爬虫并不算特别严格但你至少要表现得像一个正常的浏览器。我用的请求头模板是这样的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: 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://www.lofter.com/ }注意Referer这个字段很多爬虫新手容易忽略。Lofter 会校验请求来源如果你请求一个文章详情页却没有带Referer服务器可能返回 403。另外我强烈建议在代码里加上一个简单的重试机制。网络请求不可能 100% 成功不能因为一次超时就整个程序崩掉。我在fetcher.py里封装了一个带重试的请求函数def fetch_url(url, headers, retries3): for i in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text except requests.RequestException as e: print(f请求失败第 {i1} 次重试{e}) time.sleep(2) return None这个看似简单的封装在实际批量爬取的时候能帮大忙。毕竟几百个网页里总有那么几个会超时如果你不做重试一篇文章丢了你还不知道是在哪一步丢的。2.4 翻页逻辑与文章链接去重Lofter 的翻页逻辑用的是传统的页码方式URL 上加一个?pageN的参数就行。和那些需要拼接加密参数的网站比Lofter 在翻页这一块算是友好的。但是有个坑标签页和用户主页的翻页接口返回的数据结构不一样。标签页的接口返回的是一个叫data.post的列表字段而用户主页的接口返回的字段叫做data.posts多了个 s。我第一次没注意这个细节程序直接抛KeyError排查了半天才找到原因。在这里我建议大家在写解析函数时先手动打印一次返回的 JSON 数据结构确认字段名再写代码。别怕浪费时间这一步能帮你省下后面十倍的排错时间。另外为了防止同一个文章链接被重复抓取建议在内存里维护一个链接集合每拿到一个新链接就先判断是否已经在集合里了。这个操作很简单但很重要因为内容瀑布流页面很容易因为接口重复返回而出现同一篇文章被抓两次的问题。3. 实操过程与核心环节实现3.1 环境准备与基础配置开始写代码之前先把环境准备好。你如果已经装好了 Python 3.8 及以上版本这一步跳过前面的安装部分就行。需要安装的第三方库只有两个requests和beautifulsoup4。pip install requests beautifulsoup4 lxml我额外装了lxml是因为 BeautifulSoup 用它做解析器速度更快HTML 解析也更能容忍一些不规范标签。接下来在config.py里面写几个核心配置项# config.py TARGET_URL https://www.lofter.com/tag/你的标签名 # 改成你的目标页 SAVE_DIR ./lofter_data HEADERS { User-Agent: Mozilla/5.0 ..., Accept-Language: zh-CN,zh;q0.9, Referer: https://www.lofter.com/ } MAX_PAGES 5 # 最多翻多少页防止失控 DELAY_SECONDS 2 # 每两次请求之间的间隔两个参数需要特别解释MAX_PAGES是防止你填入一个超热门标签后爬虫无限翻页把服务器压力拉满也将你本地磁盘塞爆。一般抓个几页就够做项目了没必要把上万篇内容全部扒下来。DELAY_SECONDS是礼貌间隔我习惯设置为 2 秒既不会让服务器反感也不至于慢得让人失去耐心。3.2 列表页解析拿到所有文章链接列表页解析是这个项目的起手式。上一步我们已经确认了列表接口返回 JSON 数据所以这一步我们直接在fetcher.py里写接口请求和解析的代码。import requests import json import time from config import HEADERS, MAX_PAGES, DELAY_SECONDS def fetch_post_links(tag_url, max_pagesMAX_PAGES): links set() for page in range(1, max_pages 1): # 拼接分页 URL page_url f{tag_url}?page{page} resp requests.get(page_url, headersHEADERS, timeout10) if resp.status_code ! 200: print(f第 {page} 页请求失败) continue data resp.json() # 注意标签页的字段是 post用户主页可能是 posts这里要灵活处理 posts data.get(data, {}).get(post, []) if not posts: print(f第 {page} 页没有数据提前结束) break for post in posts: post_url post.get(postUrl) if post_url: links.add(post_url) print(f第 {page} 页累计获取 {len(links)} 条链接) time.sleep(DELAY_SECONDS) return list(links)这里的postUrl是文章的唯一链接地址格式通常是https://username.lofter.com/post/xxxxx。我选择用集合来存储因为集合天然去重。需要注意的地方data.get(data, {})这个写法是防御性的。如果接口返回的不是预期结构调用.get不会抛异常而是返回一个空字典后续迭代空列表也不会报错。这种写法在爬虫里很实用你不用对网页结构做太多假设容错率会高很多。3.3 详情页解析提取正文、标题和图片拿到了文章链接列表之后第二步就是逐一请求详情页。这一步我放在parser.py里。import re import json import requests from bs4 import BeautifulSoup from config import HEADERS def parse_post_detail(post_url): resp requests.get(post_url, headersHEADERS, timeout10) if resp.status_code ! 200: return None soup BeautifulSoup(resp.text, lxml) # 查找 window.__INITIAL_STATE__ 数据 script_tag None for script in soup.find_all(script): if script.string and __INITIAL_STATE__ in script.string: script_tag script.string break if not script_tag: return None # 提取 JSON 字符串 json_text re.search(rwindow\.__INITIAL_STATE__\s*\s*(\{.*?\});, script_tag, re.S) if not json_text: return None data json.loads(json_text.group(1)) post data.get(post, {}) title post.get(title, ) content_html post.get(content, ) # 提取正文文本去掉 HTML 标签 content_soup BeautifulSoup(content_html, lxml) text content_soup.get_text(separator\n).strip() # 提取所有图片链接 img_urls re.findall(rimg[^]src[\](.*?)[\], content_html) return { title: title, text: text, images: img_urls, tags: post.get(tagList, []), time: post.get(time, ) }这段代码的意图拆解一下第一步用 BeautifulSoup 遍历所有script标签找到包含window.__INITIAL_STATE__的那一段。不要试图用正则直接匹配全文字符串因为正文里面可能也包含类似的关键词锁定 script 标签能避免误匹配。第二步用正则把 JSON 字符串抠出来然后json.loads转成字典。这里正则里的.*?用了非贪婪模式配合re.S标志可以跨行匹配。第三步从字典里取post字段再取content字段。这个字段是 HTML 格式需要先转成 BeautifulSoup 对象再用.get_text(separator\n)提取纯文本。separator\n这个参数很关键不加的话所有段落会揉成一团阅读体验极差。第四步从content_html里提取图片链接。因为图片就是穿插在正文里的img标签正则提取是最好的办法。3.4 分类保存方案目录结构设计与文件写入到这一步前面所有解析的成果都汇聚到“保存”这个环节。我设计的保存方案是按照“作者 / 合集 / 日期 / 文章标题”这样的层级来组织目录结构。import os import re from pathlib import Path def sanitize_filename(name): # 去掉 Windows 文件名中不允许的字符 return re.sub(r[\\/:*?|], _, name) def save_post(post_data, authorunknown, root./lofter_data): title sanitize_filename(post_data[title]) if not title: title untitled # 生成文章目录根目录/作者/合集/文章标题 post_dir Path(root) / author / post_data.get(collection, default) / title post_dir.mkdir(parentsTrue, exist_okTrue) # 保存正文 Markdown md_content f# {post_data[title]}\n\n md_content f 标签{, .join(post_data[tags])}\n\n md_content f 时间{post_data[time]}\n\n md_content post_data[text] md_file post_dir / article.md md_file.write_text(md_content, encodingutf-8) # 保存图片 for idx, img_url in enumerate(post_data[images]): try: img_resp requests.get(img_url, headersHEADERS, timeout10) if img_resp.status_code 200: img_ext Path(img_url.split(?)[0]).suffix or .jpg img_file post_dir / fimage_{idx1}{img_ext} img_file.write_bytes(img_resp.content) except Exception as e: print(f图片下载失败{img_url}原因{e}) print(f已保存{post_data[title]})有几个细节值得展开说。目录名里的sanitize_filename函数非常必要。Windows 文件系统不允许文件名里出现\ / : * ? |这几个字符而文章标题里出现冒号和问号是家常便饭。如果你不提前清洗mkdir的时候就会直接抛异常。Path(root) / author / collection / title这种写法是顺序判断的。如果作者是“张三”合集是“现代AU”文章标题是“第5章”生成的目录就是./lofter_data/张三/现代AU/第5章/。这个层级的好处是你后续想按作者整理所有文章或者想按合集统一处理复制目录一步就搞定了。图片保存的时候我用了Path(img_url.split(?)[0]).suffix来提取文件后缀。这是因为 Lofter 的图片 CDN 地址后面经常带一堆参数比如?imageViewthumbnail...如果不把参数切掉提取到的后缀是错的导致图片文件没有扩展名。3.5 主流程串联跑通整个项目有了上面几个模块最后在主入口把它们串起来# main.py from fetcher import fetch_post_links from parser import parse_post_detail from saver import save_post from config import TARGET_URL def main(): print(开始获取文章链接...) links fetch_post_links(TARGET_URL) print(f共获取 {len(links)} 篇文章) success_count 0 fail_count 0 for link in links: detail parse_post_detail(link) if detail: # 从链接里提取作者名比如 https://abc.lofter.com/post/xxx author link.split(//)[1].split(.)[0] save_post(detail, authorauthor) success_count 1 else: print(f解析失败{link}) fail_count 1 print(f抓取完成成功 {success_count} 篇失败 {fail_count} 篇) if __name__ __main__: main()主流程的思路很直白就是“拿链接 → 解析详情 → 保存”三步循环。把作者名从链接里提取出来的逻辑是Lofter 的用户主页 URL 是https://用户名.lofter.com而文章链接是https://用户名.lofter.com/post/xxx所以从链接里拆分出二级域名即可。这里有个实战经验你最好在main.py里加一个断点续爬的机制。做法很简单每成功保存一篇文章把它的链接追加写入一个done.txt文件。下次启动时先读取done.txt里的链接把已经爬过的文章跳过。这样即使程序中途因为各种原因崩了也不需要从头来过。实际运行下来正常网络环境下抓取 100 篇文章含图片下载大概需要五到八分钟。这个速度跟网速和 Lofter 服务器的响应时间有关但是整体在可接受范围内。4. 常见问题与排查技巧实录4.1 跑着跑着被服务器拦截返回 403这个问题几乎每个做过 Lofter 爬虫的人都会遇到。明明最开始还能正常抓取跑了百来个请求之后突然服务器开始返回 403 Forbidden或者是一个极简的 HTML 页面提示“访问频繁”。这可能是因为你的请求频率太高触发了服务器的限流机制。解决办法通常是两个方向一个是把DELAY_SECONDS从 2 秒提到 5 秒甚至更长另一个是给请求头加上一些能降低机器识别概率的字段比如Accept-Encoding、Accept-Language等尽量模拟完整浏览器的请求特征。如果还是被限流我建议先停半小时再跑不要跟服务器硬碰硬。4.2 正文内容提取出来是一堆乱码或空白这个问题基本可以断定是编码问题。requests在解析响应内容时如果不指定编码会去猜Content-Type头里的 charset如果没猜中返回的resp.text就是乱码。解决办法是手动指定编码在请求 Lafter 页面时加上resp.encoding utf-8然后重新解析 JSON 或 HTML。这个方法可以解决 90% 以上的乱码问题。4.3 图片下载后打不开文件只有几 KB这个问题通常不是代码逻辑的错而是图片的防盗链机制在作怪。Lofter 的图片 CDN 会检查Referer头如果发现请求的 Referer 不是 Lofter 页面就会返回一个占位图或者错误图。解决办法是在下载图片时把Referer设置为文章详情页的地址img_headers HEADERS.copy() img_headers[Referer] post_url img_resp requests.get(img_url, headersimg_headers, timeout10)就是这么小的一个细节能帮你省掉无数次排查的时间。4.4 标签管理好分类保存的前提是分类信息完整很多人在做的“分类保存”只停留在目录层级分类但其实 Lofter 每篇文章自带标签如果你的爬虫把标签解析丢了后续把文本内容放进知识库做检索时会发现少了一整个维度。在parse_post_detail里标签是从tagList字段取到的。这个字段是一个列表保存的时候我建议用逗号分隔拼到 Markdown 开头。现在你可能觉得这是个可有可无的信息等你后续要基于标签做内容筛选时就会发现这一步有多值钱。→ 继续阅读常见问题排查5. 性能优化、接口分析与经验总结5.1 多线程还是单线程如何平衡速度与风险爬虫跑得慢是很多新手纠结的问题。觉得一百篇文章要五分钟太久了。于是有人一上来就上多线程ThreadPoolExecutor加 20 个线程并发抓取。结果呢快了是真快了但更容易触发服务器限流甚至把整个 IP 封了得不偿失。我个人的建议是单线程 合理延时是 Lofter 爬虫最稳妥的方案。原因很简单这个项目我们做的是“内容备份”和“归档整理”不是爬取百万级数据的商业项目没有必要追求极限速度。真觉得慢可以稍微优化一下把“解析详情”和“下载图片”拆成两个阶段先用单线程把文章正文解析完然后再批量下载图片。这样你在等待图片下载时如果被限流了正文数据已经安全落在本地了损失降到最低。5.2 请求频率与机器人检测的关系一封来自经验的提醒Lofter 的机器人检测机制并不算特别复杂但它的核心逻辑是通过同一个 IP 单位时间内对接口的请求次数来判断。这个机制有一个特点如果你长时间连续请求不休息即使每个请求间隔三秒也会被判定为高频访问。所以我在配置里加入了一个“自动休息”的设置每抓满 50 篇文章程序暂停 60 秒。这个参数写在config.py里你可以根据实际情况调整。这个“休息”不是随意加的它模拟了真实用户阅读与停留的节奏能有效降低被检测概率。5.3 断点续爬让你的程序不再白跑前面提到过断点续爬的思路这里详细把代码补充一下。断点续爬的本质是把进度记录到磁盘存下来的链接就是“已完成”的标记。import os DONE_FILE done.txt def load_done_set(): if not os.path.exists(DONE_FILE): return set() with open(DONE_FILE, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def mark_done(url): with open(DONE_FILE, a, encodingutf-8) as f: f.write(url \n)然后在主循环里加上判断done load_done_set() for link in links: if link in done: continue detail parse_post_detail(link) if detail: save_post(detail, authorauthor) mark_done(link)这个方案的优点是即使程序崩溃你只需要重新运行main.py它会自动跳过已经处理完的链接继续处理剩下的。这比从头开始跑一遍省时间得多。5.4 把爬下来的内容用起来从本地归档到内容分析爬虫写完之后如果你只是把文章保存在本地文件夹里吃灰那这套项目的价值就大打折扣了。我在实际使用中通常会继续做这几种处理对保存下来的 Markdown 文件做全文检索。用 Python 的os.walk遍历lofter_data目录把所有.md文件拼接成一个纯文本文件然后用正则或分词工具按关键词过滤。比如我想看某个作者写过的所有“校园AU”相关文章只需要在标题或标签里搜索“校园”就行。如果抓取量足够大还可以把文本数据导入到支持全文检索的工具里做一些简单的词频统计、情感分析或者人物关系提取。Lofter 上的同人文通常有非常明显的标签体系这些都为你后续分析提供了很好的素材。5.5 关于版权与数据使用的一点提醒最后想聊一个很多爬虫教程不会提、但实际操作时躲不开的话题版权与数据使用。Lofter 是创作者分享作品的社区作者们付出了时间和心力才写出那些文章、画出那些图。我们写爬虫做备份最多是为了自己收藏方便或者做技术学习完全没有必要把抓取的内容再分发出去。我在项目里做分类保存的时候特意在程序输出的最后一行加了一句“仅个人学习与备份使用”提醒自己不要越界。另外爬虫请求频率一定要控制好不要对你的目标服务器造成过大压力。写爬虫本身是技术练习但尊重他人权益、合理使用数据同样是一名合格开发者必须具备的素质。这也是我为什么在代码里一定要设MAX_PAGES和DELAY_SECONDS的原因——不给自己留一个“失控”的入口。回到这个项目本身。用 Python 爬取 Lofter 并分类保存技术难点不在“爬”这个动作上而在“如何精准提取你真正想要的数据”和“如何把数据整理成可用的结构”。这个项目跑通一遍你不仅掌握了针对异步加载网站的分析技巧还积累了处理 JSON 嵌套数据、做文件归档、应对反爬机制的实战经验。这套方法论迁移到其他 UGC 社区类网站同样能打。本文还有配套的精品资源点击获取
返回列表