公司动态
Python爬虫实战:批量下载漫画并搭建本地漫画库
简介这是一套基于Java开发的拷贝漫画CopyManga专项爬虫工具面向具备基础Java与HTTP协议知识的开发者用于合法合规地采集公开漫画元数据与章节信息适用于学习网络爬虫原理、反爬应对策略及Web数据提取实践。资源包共57个文件包含22个编译后class文件、20个核心Java源码涵盖请求调度、HTML解析、Cookie管理等模块、7个XML配置文件含Maven依赖与UI布局、以及README.md、.gitignore等工程支撑文件整体仅61KB轻量易读。已有410人学习下载代码结构清晰采用标准Maven项目组织内置robots.txt遵守机制与User-Agent模拟逻辑同时体现XPath解析、分页处理与基础异常重试等实战设计可直接运行调试并作为Java爬虫入门项目的参考范例。 我一直觉得把追更的漫画囤到本地是件特别有安全感的事。爬虫技术在这类需求上可以派上大用场我之前整理过一套“拷贝漫画爬虫工具.zip”目的就是批量抓取漫画章节、图片再按规则归档成本地文件夹。做这个工具不是头脑一热而是追的漫画多了之后在线看总有图挂、下架、APP更新改版的问题本地一份才是最稳的。这篇文章我会把整个实现过程、核心代码、踩过的坑原原本本写出来适合有一定Python基础、想系统了解爬虫项目全流程的朋友参考。1. 工具定位与整体设计思路1.1 先想清楚结果形态你要的不是图片是本地漫画库很多人一提到爬虫就觉得是“写个脚本把图拉下来”但真做起来你会发现如果只做到“把图下载到本地”后续根本没法用。漫画这种内容天然是三层结构一部漫画包含多个章节每个章节包含多张图片这三层关系必须清晰落地否则几十个G的图片堆在一起根本没法看。所以我在设计这个工具时第一件事就是定义输出目录结构而不是先写代码。结构定成这样comic_store/ ├── 漫画A/ │ ├── meta.json │ ├── 第001话/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ ├── 第002话/ │ └── ... └── 漫画B/ └── ...meta.json用来记录漫画名称、作者、简介、章节列表、抓取时间这些元信息目录名统一按“第xxx话”排序这样在任何阅读器里都能直接按顺序导入不需要额外解析。数据模型设计上每一层都要有独立的数据结构。漫画是顶层对象章节是中间层图片是叶子节点。Python里我用字典加列表就够了不需要上数据库但如果你要爬的漫画量非常大后续可以考虑SQLite。一个小技巧是在命名章节目录时统一补零1写成001这样文件系统排序时不会出现第2话排在第10话后面的问题。1.2 技术选型为什么不整Scrapy而是requests加线程池市面上的爬虫框架很多最出名的就是Scrapy。但我做这个工具的时候还是选择了requests parsel ThreadPoolExecutor这个组合原因很直接项目规模小。漫画站的结构相对固定要抓的接口也就几个没必要上一整套框架。Scrapy需要写spider、配置middleware、处理pipeline对一个个人工具来说过度设计。调试方便。requests是同步库配合print加try-except出问题一眼就能定位。Scrapy的日志体系虽然健全但写不对的时候排查链路更长。并发可控。图片下载是IO密集型操作线程池足够解决问题。Python的GIL对计算密集型任务限制很大但对网络请求这种阻塞型任务多线程完全够用。我用到的核心库就四个库名用途选它而不是...requestsHTTP请求httpx同步场景没明显优势parsel解析HTML/JSONBeautifulSoupparsel支持CSS和XPath语法更简洁ThreadPoolExecutor并发下载图片asyncio异步改造成本高线程池写起来最直观os / json目录创建与元数据存储无1.3 爬虫三种常见类型的灵活应对这个项目可以连带聊一下爬虫领域的通用分类因为不同需求要的爬虫形态完全不一样。批量型爬虫适合一次性把某部漫画所有章节拉下来对应我这个工具的主流使用方式增量型爬虫适合做追更——每天只检测新章节有更新就下载没有就跳过垂直型爬虫则是聚焦某一类特定站点比如只爬漫画站、只爬小说站做深度结构化采集。我这个工具实际上是批量为主增量为辅的混合模式。首次运行时全量下载后续再跑就可以通过meta.json里的章节记录做增量判断。这个设计花了我不少心思也是我最满意的地方。到了后期你甚至可以改成“自动检测更新加通知推送”那就是另一个话题了。2. 核心细节解析与实操要点2.1 看清目标站的数据流先找接口再写代码写任何爬虫的第一步绝不是写代码而是打开浏览器开发者工具老老实实看请求。打开目标漫画站的详情页按F12切到Network面板刷新页面你会发现大部分数据其实不是写在HTML里的而是通过异步接口返回的JSON。我在做这个项目时第一步是把详情页、章节页、图片加载页全部过了一遍记录每个页面对应的真实请求URL、请求方法、请求头和响应格式。目标站通常会把一个章节的图片列表做成一个专属接口返回的JSON里面包含了图片URL数组、章节ID、章节标题等信息。这个阶段最关键的产出是一张接口清单。我自己实际记录时是这么整理的数据目标接口路径方法关键参数返回格式漫画基本信息/api/comic/detailGETcomic_idJSON章节列表/api/comic/chaptersGETcomic_id, pageJSON章节图片列表/api/comic/chapter/imagesGETchapter_idJSON图片CDN地址接口返回的img_urls--字符串列表有了这张表后面写代码就是照着填参数而已。这里有一个经验之谈不要把网站的页面URL硬编码在代码里而是把接口路径抽成配置项写在项目根目录的config.py里。因为网站改版很频繁接口路径变更是最常见的问题抽成配置项之后真到那一天你只需要改一行配置不用翻遍代码去找。2.2 请求伪装与反爬应对把自己伪装成正常人爬虫写多了你就会发现反爬的核心逻辑就一句话让服务器觉得你不是机器人而是个正常人。这意味着你要在请求头、请求频率、访问行为上花功夫。首先是请求头。最基本的三个User-Agent、Referer、Host。User-Agent用来标识浏览器环境我习惯用Chrome最新版的UA。Referer特别重要很多图片CDN会校验这个字段如果请求图片时不带Referer或者Referer不对服务器直接返回403。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://目标站域名/, Accept: text/html,application/json,text/plain,*/*, Accept-Language: zh-CN,zh;q0.9, }其次是请求频率。我见过很多新人爬虫一上来就发几百个并发请求结果不到十秒就被封了IP。我自己的做法是在每个请求之间加一个随机的time.sleep(0.5, 1.5)同时限制线程池的最大并发数不超过4个。这样整体速度虽然会慢一些但胜在稳定实际上因为任务不断流一万张图用不了太久。第三是Cookie处理。如果只是爬公开的漫画章节大部分情况下不需要登录因此不需要Cookie。但如果你需要爬取付费内容或者需要登录才能看的章节就需要把浏览器里登录后的Cookie复制出来放进请求头里。这个操作在Session中维护即可。要注意Cookie有过期时间一旦失效需要重新从浏览器复制。2.3 数据校验与容错下载不全比爬不到更让人心态崩我第一版工具跑出来的结果最大的问题不是爬不到而是爬到一半图片下载失败导致章节里缺了几页。这个问题的根源在于我当时的代码只把请求结果写入了文件完全没有校验写入是否完整。后来我加了两层防护。第一层是下载后的完整性校验每次请求图片后检查response.status_code是否为200同时检查response.content的长度是否大于某个阈值比如10KB。第二层是重试机制下载失败的图片会进入一个failed_urls列表全部跑完之后自动重试这些失败的URL最多重试3次每次间隔5秒。还有一个小细节有些图片服务器会对单IP限速连续下载大图时偶尔会超时所以我在请求图片时设置了timeout(10, 30)——connect timeout设为10秒read timeout设为30秒。超时异常会被捕获并触发重试不会导致整个程序崩溃。3. 实操过程与核心环节实现3.1 环境准备Python版本和虚拟环境项目运行环境是Python 3.9推荐使用3.10或3.11版本太老或者太新都可能遇到依赖兼容问题。第一步是创建虚拟环境避免和系统Python环境冲突。python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install requests parsel需要说明的是我全程没有用Scrapy只装requests和parsel两个库就够了。如果你需要处理更复杂的JS渲染页面才会用到Selenium或Playwright我这里的目标站数据全是接口直出不需要。3.2 核心代码实现从章节列表到图片下载先写最核心的模块主要分三块获取章节列表、获取章节图片列表、下载图片。我建议你也按这个顺序来层层递进。第一步获取章节列表。这个函数负责接收一个comic_id请求章节列表接口解析返回的JSON过滤掉没有图片的章节返回一个有序列表。import requests import json 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://目标站域名/, Accept: application/json, text/plain, */*, } def get_chapter_list(comic_id): url https://目标站域名/api/comic/chapters params {comic_id: comic_id, page: 1} resp requests.get(url, headersHEADERS, paramsparams, timeout(10, 30)) resp.raise_for_status() data resp.json() chapters [] for item in data[data][list]: chapters.append({ chapter_id: item[id], title: item[title], order: item[ordering], }) # 按order排序 chapters.sort(keylambda x: x[order]) return chapters第二步获取章节图片列表。这个函数接收chapter_id返回该章节所有图片的URL列表。def get_chapter_images(chapter_id): url fhttps://目标站域名/api/comic/chapter/images?chapter_id{chapter_id} resp requests.get(url, headersHEADERS, timeout(10, 30)) resp.raise_for_status() data resp.json() return data[data][images]第三步下载图片。这是整个工具的主体力活我用ThreadPoolExecutor做并发同时保留信号量控制最大并发数。from concurrent.futures import ThreadPoolExecutor, as_completed import os import time import random MAX_WORKERS 4 def download_image(img_url, save_path, max_retry3): for attempt in range(1, max_retry 1): try: resp requests.get(img_url, headersHEADERS, timeout(10, 30)) if resp.status_code 200 and len(resp.content) 10240: with open(save_path, wb) as f: f.write(resp.content) return True else: print(f[尝试{attempt}] 图片状态异常或过小: {img_url}) except Exception as e: print(f[尝试{attempt}] 下载失败: {img_url}错误: {e}) time.sleep(5) return False def download_chapter(chapter, save_dir): os.makedirs(save_dir, exist_okTrue) img_urls get_chapter_images(chapter[chapter_id]) tasks [] with ThreadPoolExecutor(max_workersMAX_WORKERS) as executor: for idx, img_url in enumerate(img_urls, start1): ext os.path.splitext(img_url.split(?)[0])[1] or .jpg save_path os.path.join(save_dir, f{idx:03d}{ext}) tasks.append(executor.submit(download_image, img_url, save_path)) for future in as_completed(tasks): future.result() time.sleep(random.uniform(0.5, 1.5))这套代码运行起来很稳但最需要留意的还是线程安全。我在download_image里直接操作文件写入每个线程写入的是不同的文件不涉及共享状态所以不需要加锁。如果你还要记录下载进度到同一个文件或者数据库就需要考虑线程锁了。3.3 增量更新与断点续传的设计思路很多爬虫工具都号称支持“断点续传”真做起来其实不复杂。思路是在每次下载章节之前先检查目标目录是否已经存在以及目录内图片数量和预期是否一致。如果一致直接跳过不一致删除目录重新下载。我实际用的方法比这个更轻量就是在meta.json里维护一个已经完成下载的章节ID列表。每次运行时先读这个文件把已完成的章节过滤掉。这样即使程序中途崩溃下次运行也会跳过已经下载好的部分不用从头再来。def load_meta(comic_dir): meta_path os.path.join(comic_dir, meta.json) if os.path.exists(meta_path): with open(meta_path, r, encodingutf-8) as f: return json.load(f) return {finished_chapters: []}增量更新对追更场景非常重要。漫画站每周更新几话跑一次工具只下载新增的内容既不浪费时间也不给服务器造成压力。4. 常见问题与排查技巧实录4.1 请求被拒绝、返回403或者直接弹验证页这是爬虫最常见的问题没有之一。我在做这个项目的过程中遇到过几次排查路径基本是固定的第一步确认请求头是否完整。重点看User-Agent和Referer没有或者不对服务器会直接拒绝。第二步确认IP是否被封。频繁请求后突然所有请求都返回403大概率是IP被临时封禁。我自己的应对办法就是降低并发数、拉长间隔时间过几个小时再跑。第三步确认是否需要Cookie。有些站点的敏感接口必须带登录后的Cookie才能访问。这算是我的一个避坑总结清单如果你遇到问题建议按顺序排查不要一上来就换IP代理、换UA那样往往解决不了根本问题。4.2 图片下载成功但打不开或者文件大小不对这个问题我一开始也没意识到后来才明白是下载不完整导致的。原因通常是网络连接中断或者服务端返回了异常页面但状态码仍然是200。例如某些CDN在限流时会返回一个很小的HTML页面而不是图片此时程序只检查了状态码就会把HTML存成jpg文件。解决方法是加二层校验第一层判断响应长度第二层判断文件头。JPEG文件的前几个字节是FF D8 FFPNG是89 50 4E 47可以在写入之前检查一下。我在代码里只加了一个长度判断从实际效果看已经过滤掉了99%的问题。4.3 接口参数加密或返回密文怎么办有段时间目标站更新了接口原来的明文参数变成了加密字符串返回的数据也做了编码处理。遇到这种情况就要去做逆向分析。我分析请求列表时发现页面上多了一个JS文件里面有一部分逻辑是对参数进行签名。我用浏览器的开发者工具定位到这个函数把它的加密逻辑抽出来用Python重新实现了一遍问题就解决了。这是逆向爬虫的入门操作其实说到底就是“看懂前端在干什么然后在后端重新做一遍”。新手碰到这种不要慌多在Source面板里翻一翻找到加密函数断点调试看清楚了就复制逻辑。4.4 关于合法使用的几点提醒爬虫本身是技术工具没有原罪但使用场景必须讲分寸。我做的这个工具用途是把自己已经能访问的、公开的漫画章节存档到本地并没有去破解付费墙或者绕过任何权限控制。开发这类工具要注意不要爬取需要登录或付费才能访问的内容除非你有明确授权。不要高并发请求给自己用的工具不要影响目标站正常运营。做个人学习和备份用途不要拿爬取的内容去分发或商用。4.5 问题排查速查表现象最可能原因解决办法所有请求都403请求头缺失或IP被封补全Headers降低频率等待解封偶发403Referer不正确在Headers里添加Referer为站点首页图片下载后打不开下载不完整或存了错误响应增加响应体长度校验和文件头校验章节顺序错乱未按order字段排序拉取章节列表后显式排序目录显示排序不对文件名没有补零用f{idx:03d}.jpg而不是str(idx)接口返回乱码/密文数据加密或压缩问题逆向JS逻辑在Python中复现加密流程这些坑我都是实际踩过的写下来也是给自己留个备忘。爬虫技术的价值不在于能跑通一次而在于目标站改版之后你能多快跟上节奏这也是我坚持把项目结构做得足够清晰的原因。5. 工具完整使用流程5.1 初始化项目结构把工具解压后目录结构是这样的拷贝漫画爬虫工具/ ├── config.py ├── spider.py ├── requirements.txt └── comic_store/config.py放域名、Headers、路径等配置。spider.py是主程序。comic_store是下载目录可以提前建好也可以让程序自动创建。requirements.txt只有两行内容requests parsel你只需要进入目录后执行pip install -r requirements.txt就能完成依赖安装。5.2 配置漫画ID并启动下载打开config.py找到COMIC_ID 这一行填写你要下载的漫画ID。漫画ID怎么看在目标站的漫画详情页URL里会有一个数字ID把它填进来即可。然后运行python spider.py程序会先打印漫画基本信息然后逐章下载。下载过程中会显示当前章节、已完成图片数、总图片数遇到失败会自动重试。全部跑完后comic_store/漫画名/目录下就是一套完整的本地漫画库。5.3 增量追更的操作方法后续漫画更新了再次运行python spider.py。程序会读取meta.json自动跳过已经存在的章节只下载新增内容。整个过程中你不必关心哪些下过了哪些没有工具会自己判断。这套流程我用了很久稳定可靠。你现在看到的所有痛点都是我之前在真实环境里踩坑后得来的经验直接照抄就能用。本文还有配套的精品资源点击获取