简介这是一套基于Java开发的漫画网站数据采集工具面向有一定Java基础的开发者或数据采集初学者用于学习网页爬虫原理与实战开发。资源聚焦拷贝漫画copymanga站点涵盖URL调度、HTTP请求、HTML解析、数据持久化等完整爬虫流程实现可辅助理解反爬应对、robots协议遵守等工程实践要点。压缩包共57个文件含22个编译后class文件、20个核心Java源码如SpiderMain、PageParser等、7个XML配置文件含Maven依赖与Spring Bean定义以及README说明、IDE配置和构建相关文件整体仅61KB轻量易读。目前已有410人学习下载代码结构清晰模块职责分明附带完整项目配置与可运行入口适合直接导入IDE调试学习是理解Java Web爬虫从设计到落地的典型教学级案例。 花了一晚上写了个漫画爬虫工具把整部漫画批量下载打包成zip压缩包。趁热把整个实现思路和踩坑过程整理出来给需要的人参考。1. 为什么漫画站的爬虫比普通网站难写先说结论漫画站的爬虫难度不在“爬”本身而在“还原阅读体验”。普通新闻爬虫抓到正文文字就算完事漫画爬虫要处理的是图片流。一部漫画动辄几百话每话几十张图你要把这一层一层的数据结构理顺——站点下面是漫画漫画下面是章节章节下面是图片图片还得按顺序拼接才能看。我最早接触这个需求是被一个朋友问的他说想把自己追的一部小众漫画存到本地方便通勤路上离线看。我一开始以为这事很简单requests请求一下HTML正则抽几个图片链接就完事。真正动手才发现现在正规一点的漫画平台基本都不做纯服务端渲染了页面框架是JS动态加载的直接拿requests拿不到任何章节数据。如果你也是第一次写漫画爬虫建议先建立一个认知漫画平台的数据层级通常是四层结构任何一层没打通后面全白搭。漫画站首页 - 漫画详情页 - 章节列表 - 章节内图片序列后面所有的代码逻辑本质上都是围绕这四层展开的。你的爬虫能不能稳定跑完一整部漫画取决于每一层的数据解析是否可靠。另外一个容易被忽视的问题是存储组织。图片下载下来不是终点你要让用户能离线看必须打包成zip或者pdf。zip格式通用性强手机电脑都能直接解压看所以我最终选了zip。2. 动手前先摸清目标站的请求协议写爬虫最忌讳上来就写代码。先花半小时搞清楚目标站的前端是怎么请求数据的后面能省半天调试时间。2.1 用开发者工具看请求而不是看页面源码打开目标漫画站的某个漫画详情页按F12进入开发者工具切到Network面板刷新页面过滤XHR请求。这个时候你会看到页面在加载过程中发起的所有Ajax请求。核心要关注的是三类接口详情页接口返回漫画名称、封面、简介等元信息章节列表接口返回该漫画的所有章节ID和章节名称图片列表接口返回某一章节内所有图片的URL列表如果你在XHR请求里找不到明显的图片列表接口大概率是用了JS加密或混淆这时候就需要分析JS逻辑了。但绝大多数站点图片列表接口都是明文的只是接口路径有一定的规则。2.2 requests模拟请求的三个关键头拿到接口后先用requests试着请求一下。只带一个普通User-Agent就请求往往会被拦下来。我实际测试下来有三个请求头基本是必须的请求头作用缺失的后果User-Agent标识客户端类型部分站点直接返回403Referer标识请求来源页面返回403或空数据尤其是图片请求Cookie维持登录态/访问令牌访问付费章节或需要登录的漫画失败有些平台的接口会校验Referer是否来自本站域名请求头里没有Referer接口直接返回{code: -1}之类的错误。这个坑非常隐蔽因为单独看文档根本不会有人提醒你。Cookie的问题更麻烦。如果你要爬的漫画需要登录才能看那第一步就得先用浏览器登录一次然后把浏览器Cookie复制到爬虫的请求头里。Cookie获取方式浏览器开发者工具 - Network - 随便点一个请求 - Request Headers - 复制Cookie整段字符串。2.3 接口返回的数据结构要留个心眼我遇到的情况是章节列表接口返回的是一个JSON数组每个元素包含chapter_id、chapter_title、chapter_order三个字段。图片列表接口返回的是一个JSON对象里面是一个URL数组。注意一个细节很多平台的JSON返回是经过一层封装的比如{code:0, data: {...}}这种格式。要解析数据必须先定位到data字段不要直接去顶层取数据否则会拿到一堆空值。还要留意接口返回的图片URL有时是相对路径需要拼接域名有时是webp格式需要转换成jpg这些都会影响最终打包效果。实际写代码的时候建议加一个统一的URL标准化处理逻辑。3. 核心实现一个可复用的漫画爬虫骨架搞清楚请求协议之后写代码就顺理成章了。我把核心逻辑分成三块目录解析、图片下载、打包输出。下面用Python requests BeautifulSoup实现一套完整的骨架你可以根据自己的目标站替换选择器。3.1 目录解析与章节遍历第一步是从漫画详情页拿到所有章节信息。这里有一个经验不要在前端页面里找章节列表而是去找章节列表接口。页面HTML里渲染的章节列表通常被截断尤其是几百话的长篇漫画接口返回才是全量。import requests from bs4 import BeautifulSoup import json HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/ } def get_chapters(comic_url): resp requests.get(comic_url, headersHEADERS, timeout10) resp.raise_for_status() # 假设章节列表在接口 /api/chapters 中把 comic_id 拼上去 comic_id extract_comic_id(comic_url) api_url fhttps://api.example.com/api/chapters?comic_id{comic_id} data requests.get(api_url, headersHEADERS, timeout10).json() chapters [] for item in data[data][list]: chapters.append({ id: item[chapter_id], title: item[chapter_title], order: item[chapter_order] }) # 按章节顺序排序 chapters.sort(keylambda x: x[order]) return chapters注意我加了timeout10这是必须的。网络请求不可控不设置超时会导致爬虫卡死在一个请求上整个任务都要重启。3.2 图片链接提取与并发下载拿到章节ID后请求图片列表接口得到该章节所有图片的URL。下载图片时建议用线程池做并发但并发数别太大。我实测ThreadPoolExecutor(max_workers4)最稳妥再高会被站点限流。from concurrent.futures import ThreadPoolExecutor def download_chapter_images(chapter, save_dir): api_url fhttps://api.example.com/api/images?chapter_id{chapter[id]} data requests.get(api_url, headersHEADERS, timeout10).json() img_urls data[data][images] img_objs [] for idx, url in enumerate(img_urls): img_objs.append({ url: normalize_url(url), path: f{idx1:03d}.jpg }) def fetch_one(img_obj): path f{save_dir}/{img_obj[path]} for retry in range(3): try: r requests.get(img_obj[url], headersHEADERS, timeout15) if r.status_code 200: with open(path, wb) as f: f.write(r.content) return True except Exception: continue return False with ThreadPoolExecutor(max_workers4) as executor: results list(executor.map(fetch_one, img_objs)) failed sum(1 for r in results if not r) return failed文件名用001.jpg、002.jpg这种零填充的格式是为了保证解压后按文件名排序就是正确的阅读顺序。这是很多新手容易忽略的细节如果文件名是1.jpg、2.jpg、10.jpg排序结果会是1、10、2阅读顺序直接乱掉。重试机制也要做。图片下载过程中网络抖动很正常一次失败就放弃会导致缺图。我写了3次重试每次重试间隔1秒实测能把失败率降到几乎为零。3.3 汇总为zip包的组织逻辑单章下载完成后把目录下所有图片压缩成zip格式。Python自带的zipfile模块就能搞定不用装第三方库。import zipfile import os def pack_chapter_to_zip(chapter_dir, zip_path): with zipfile.ZipFile(zip_path, w, zipfile.ZIP_STORED) as zf: for root, _, files in os.walk(chapter_dir): for file in sorted(files): if file.endswith(.jpg): file_path os.path.join(root, file) arcname f{chapter_title}/{file} zf.write(file_path, arcname)压缩格式建议用ZIP_STORED而不是ZIP_DEFLATED。原因很实在图片本身就是压缩过的格式再用DEFLATE压缩一遍体积基本不会变小反而会拖慢压缩速度。用STORED模式是直接存储速度快文件也不会变大。4. 打包和下载环节最容易翻车的几个坑工具写完不是结束能跑通才是开始。我在实际使用过程中遇到过好几个让人抓狂的坑。这里单独列一节希望你别再踩一遍。4.1 下载了一半zip却说“file is not a zip file”第一次完整跑完工具后我满心欢喜地解压看效果。结果Windows资源管理器直接弹窗file is not a zip file。排查了半天最后发现问题出在下载过程被中断过。下载脚本跑到一半断网了我直接重新跑了一遍但脚本的逻辑是“覆盖写文件”之前的下载记录全丢了。中途断掉的任务最终产出了一个残缺的zip包文件头还在但文件尾不完整解压工具自然不认。解决思路有两个断点续传每张图片下载后记录状态下次启动时跳过已下载的文件完整性校验打包前检查图片数量和字节大小是否与预期一致简单实现断点续传其实不难就是下载前先看一眼本地文件是否存在且大小不为0def fetch_one_with_skip(img_obj): path f{save_dir}/{img_obj[path]} if os.path.exists(path) and os.path.getsize(path) 0: return True for retry in range(3): try: r requests.get(img_obj[url], headersHEADERS, timeout15) if r.status_code 200: with open(path, wb) as f: f.write(r.content) return True except Exception: time.sleep(1) return False这样即使中途断了重启脚本也会从失败的地方继续而不是从头再来。4.2 zip -ff 是修复命令但治标不治本Linux用户可能听说过zip -FF damaged.zip --out repaired.zip可以用来修复zip文件。这个命令在某些场景下确实能救回部分数据但它不是银弹。-ff的完整逻辑是“通过文件头信息重建目录结构”如果文件尾部损坏不严重还有救如果文件头部本身就损坏了基本无解。我在踩了file is not a zip file的坑之后试过用zip -ff修复效果一般还是重写代码做断点续传更靠谱。建议把精力花在防止文件损坏上而不是事后修复。4.3 中文文件名乱码与编码问题漫画章节名是中文打包成zip后在Windows上解压可能会出现乱码。原因是zipfile模块默认用UTF-8写文件名但老版本Windows解压工具默认按GBK解码。解决方法是设置压缩包标志位强制使用UTF-8def pack_chapter_to_zip(chapter_dir, zip_path, chapter_title): with zipfile.ZipFile(zip_path, w, zipfile.ZIP_STORED) as zf: for root, _, files in os.walk(chapter_dir): for file in sorted(files): if file.endswith(.jpg): file_path os.path.join(root, file) arcname f{chapter_title}/{file} # 强制UTF-8编码文件名 zf.write(file_path, arcname.encode(utf-8).decode(utf-8))但注意某些国产压缩软件对UTF-8标志位支持不完善遇到这类报错可以换用7-Zip重新压缩或者直接用Python脚本解压Python对编码处理更规范。4.4 分卷压缩文件z01的处理如果你下载的zip资源是分卷压缩的比如.zip文件旁边还有.z01、.z02文件别直接用解压工具去解压.zip文件会报“需要下一卷”的错误。正确的操作方式是把所有分卷文件放到同一个目录然后用7-Zip打开.zip的最后一个卷一般是最后一个数字结尾的文件或者直接双击.zip文件7-Zip会自动识别相邻分卷。我一开始不知道傻乎乎地把.zip复制到别的目录去解压结果一直报错。后来才反应过来分卷压缩的所有切片必须放在一起完整了才能解压。4.5 Cookie过期导致越爬越少漫画爬虫如果带了Cookie运行时间一长就会遇到Cookie过期的问题。表现是前面几十个章节正常下载突然开始大面积失败。这是因为站点登录态失效后接口不再返回付费章节数据。处理方案有三种按推荐度排序定期刷新Cookie手动从浏览器复制新的Cookie更新到配置里用Session保持会话requests.Session可以自动维持会话配合登录接口做自动续期抓取前做一次可用性检查请求第一个API后检查返回码发现失效就立即中止避免浪费时间我最终用了第三种方案简单可靠成本最低。5. 请求频率控制与封IP应对写爬虫第一课就是“别把对方服务器搞挂了”。漫画平台对请求频率的敏感度比普通网站高因为图片请求非常占用带宽。5.1 压测出来的合理请求间隔我刚开始跑工具的时候没做任何限速4线程并发跑全速下载跑了两百多话之后突然所有请求都开始返回403。一开始以为是代码写错了后来发现是IP被临时封禁了。解封等了一个小时才恢复。后面我把策略调整为每请求10次主动sleep 1-2秒图片并发数降到2。这个频率跑下来整部漫画爬完也没有再触发封禁。建议你在自己写工具的时候先在目标站用小量请求测试出发封阈值。一般漫画站的防守策略是滑动窗口计数比如1分钟内超过60次请求就封IP那你的请求间隔至少得压到1秒以上。5.2 设置随机延时比固定延时更有效固定延时毕竟有规律容易被识别。我在实际代码里加了个随机延时函数import random import time def random_delay(): time.sleep(random.uniform(0.5, 2.0))每个请求之间随机延迟0.5-2秒模拟真实用户的访问节奏。这样既不会触发封IP也不会因为延时太长导致整体效率太低。5.3 真正要做好的是数据校验很多新手犯的错是只关心请求是否成功忽略了数据正确性。比如图片接口返回的成功码是200但下载下来的图片实际是一张403错误页或者下载的图片只有几KB大小明显不是正常图片。我在代码里加了两个校验点Content-Length校验响应头声明的长度与实际下载字节数是否一致文件头校验检查图片文件的前几个字节是否是JPEG或PNG魔数def is_valid_image(file_path): try: with open(file_path, rb) as f: header f.read(3) if header b\xff\xd8\xff: # JPEG return True if header b\x89PN: # PNG return True return False except Exception: return False这两道校验几乎能拦截掉90%的下载异常防止你花了几个小时跑出来的结果不能用。6. 实战中关于合法使用的几点提醒爬虫工具写出来不难但用在哪里、怎么用边界要想清楚。这里不是念教条是几个很现实的问题。6.1 这个工具适合谁不适合谁漫画爬虫工具适合的是自己购买/有权限阅读的漫画做本地备份。比如你买了某个平台的会员平台不提供离线下载功能你写脚本把自己买的内容存到本地完全没有问题。不适合的是把爬虫工具用于大规模抓取、二次分发、商业售卖。这类行为既违反平台服务条款也可能触及法律红线。特别是某些漫画平台有独家版权内容批量抓取后传播风险极高。我的建议是自己在本地用、自己看绝不要公开分享抓取的内容。6.2 robots.txt 和站点访问协议动手写爬虫前花一分钟看一眼目标站的robots.txt路径一般是https://域名/robots.txt。这个文件声明了哪些路径允许抓取、哪些路径禁止抓取。虽然robots.txt没有强制执行力但尊重它既是职业道德也能帮你规避很多不必要的麻烦。大部分平台的robots.txt都会注明Disallow: /api/说明接口数据不欢迎爬虫建议只抓取前端页面展示的数据不要触碰后端接口。6.3 站点改版后的维护成本很多自用爬虫工具都有一个“通病”站方一改版工具就废。漫画平台的页面结构、接口参数、图片URL规则都会定期调整。今天跑得好好的脚本可能下个月就完全不可用了。真正能长期运行的工具设计时就要注意把“易变部分”独立出来。我在代码里把接口URL、请求头、解析逻辑分成独立模块改版时只需要替换对应模块不用动主流程。这个结构后面维护起来很省心。7. 完整工具的可视化思路与后续扩展上面这套骨架能跑通基本流程但离“好用”还有距离。下面聊几个可以继续扩展的方向。7.1 增量更新机制漫画会持续更新每次跑全量爬虫太浪费时间。可以做一个增量更新记录上次爬到的章节ID下次只抓取大于该ID的章节。实现上只需要在章节列表里加个判断def filter_new_chapters(chapters, last_chapter_id): return [c for c in chapters if c[id] last_chapter_id]增量更新最大的好处是省流量、省时间也减少了对目标站的请求压力。7.2 多漫画批量管理如果一个脚本只能爬一部漫画管理成本太高。更好的设计是做成“任务队列”一个配置文件里定义多个任务每个任务包含漫画URL和输出目录脚本按顺序执行。CONFIG [ {comic_url: https://example.com/comic/1, save_dir: ./downloads/comic1}, {comic_url: https://example.com/comic/2, save_dir: ./downloads/comic2}, ]这就是批量型爬虫的典型应用场景。对于漫画这类数据量大的站点批量型爬虫比增量型爬虫更适合用在首次全量抓取增量型适合后续日常更新。还有一个容易忽略的点垂直型爬虫。针对漫画这个垂直领域你可以把爬虫做得更“专业”一些比如自动解析章节名、自动生成目录索引、自动生成mht或pdf电子书。垂直型爬虫的精髓在于深度优化某个特定领域的数据处理流程而不是贪多求全。7.3 生成PDF还是ZIPZIP适合手机相册直接看图PDF适合电脑/平板阅读器。各有优劣格式优点缺点ZIP保存原始画质通用性强需要解压后查看部分阅读器体验一般PDF阅读体验好适合电子书大体积PDF生成慢画质有损我个人更倾向于ZIP因为保留原始图片画质很重要。漫画的画面细节多压缩成PDF后网点、线条细节都会有一定损伤。如果真想要PDF可以用img2pdf库直接封装图片不做二次压缩画质损失小。7.4 给工具加日志和监控自用的爬虫工具最怕静默失败。跑了两小时结果全是403你不看日志根本发现不了。所以我给工具加了一个简单的日志模块import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(crawler.log, encodingutf-8), logging.StreamHandler() ] )每次请求成功或失败都输出一条日志。这样即使工具跑到一半挂掉也能通过日志定位是哪一步出的问题。最后再分享一个实用的小技巧写这类下载工具调试阶段最怕的就是反复下载同一部漫画来测试代码。我的做法是先做一个--test参数只下载第一章图片快速验证整个流程是否跑通。测试通过后再全量跑效率高很多。逻辑很简单就是在入口加个判定import sys if --test in sys.argv: chapters chapters[:1]一个很小的改动能省下大量调试时间。另外如果你在解压zip时遇到“file is not a zip file”或者could not find EOCD这种报错先别急着用修复工具。用你的Python环境直接测试一下zip包是否完整import zipfile with zipfile.ZipFile(output.zip, r) as zf: bad_file zf.testzip() if bad_file: print(f损坏文件: {bad_file}) else: print(zip完整)如果提示损坏优先重新下载对应章节而不是死磕修复工具。这个工具我目前已经稳定跑了好几部漫画单部两三百章的体量配合限速和断点续传大概两三个小时跑完中途基本不需要人工干预。把它分享出来的价值在于漫画爬虫的完整链路——从接口分析、请求伪装、并发下载、zip打包到异常处理——每一个环节都有值得注意的细节。希望这篇内容能帮你少走点弯路。本文还有配套的精品资源点击获取