ARTICLE DETAIL

资讯详情

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

Python爬虫实战:requests+bs4批量下载漫画并合成PDF

Python爬虫实战:requests+bs4批量下载漫画并合成PDF 这篇漫画下载教程我拖了挺久才写因为市面上的同类文章要么讲得太浅改了网站结构就废掉要么一上来就堆 Scrapy、Splash、验证码识别这些重型武器新手根本用不上。其实日常看漫画、做个人离线阅读库远没那么复杂。你只需要 Python 的 requests 解析页面结构bs4 提取图片地址再用 img2pdf 把本地图片按顺序合成为一个 PDF 文件整套流程走通也就一晚上时间。这篇文章以“无加密”站点为目标就是那种页面纯静态、图片地址直接写在 HTML 里、没有复杂 JS 渲染的漫画网站。这类站是爬虫新手最好的练手对象结构透明、出错直观而你学会的技能点——URL 拼接、请求头伪装、流式下载、批量文件处理——可以平移到绝大多数常规网站包括新闻站、图库站、文档站。不需要你有爬虫基础但至少得会一点 Python 语法能看懂函数和循环。我会把每一步的“为什么这么做”也讲清楚不然换了站点你照样抓瞎。1. 项目思路与整体设计1.1 为什么选“无加密”站点做实战先说个大实话网上流传的“爬取XX漫画网站”的教程十个里有九个拿付费站点或加了加密参数的站点做案例然后教你对着一段看不懂的 JS 分析半天最后得出需要 Selenium 模拟浏览器。这种教程看完你只会复制粘贴因为核心逻辑全是死记硬背站点一改立刻报废。无加密站点不一样。它的真实图片地址就躺在 HTML 里你右键查看源代码就能看到。整个爬虫的逻辑退回到最朴素的模型发一个 HTTP 请求拿 HTML 字符串用字符串解析工具踢出 标签拼成完整的图片 URL再发请求把图片二进制数据写到本地文件。没有 token、没有动态签名、没有接口加密每一步失败你都能从返回结果里直接判断原因。选这种站点做实战真正的价值在于训练你“拆解流程”的能力。拿到任何一个网站你能先分清哪部分是静态 HTML、哪部分是异步加载、哪部分是加密接口这比会调任何框架都重要。等你把无加密流程跑熟了再去看 Selenium、Pyppeteer、mitmproxy 这些工具你会知道它们分别在解决哪个环节的什么问题而不是乱学一气。1.2 技术选型与工具链我的选择很保守全栈依赖不超过五个库每一个都经住了大量生产环境的考验工具/库用途选型理由Python 3.10开发语言语法友好处理字符串和文件非常顺手requests发送 HTTP 请求API 简洁自动处理 Cookies实战首选BeautifulSoup4解析 HTML容错能力强比正则表达式可靠得多Pillow校验图片完整性不依赖它生成 PDF只是用来做图片损坏检测img2pdf图片合并 PDF无损压缩速度快不引入重量级 PDF 引擎很多人问我为什么不用 Scrapy。Scrapy 是优秀的框架但对于这种几十个页面、图片直链的下载任务它的中间件、管道、选择器配置反而是负担。requests 配合循环写下来代码量更短逻辑一目了然出了问题你也能快速定位到具体某一行。工具是用来解决问题的不是用来炫耀的能用简单方案解决绝不上重武器。我用 img2pdf 而不是直接用 Pillow 来保存 PDF是因为 Pillow 在遇到 CMYK 模式的 JPEG 时经常闹脾气合并出来的 PDF 要么偏色要么直接报错。img2pdf 直接读取图片的原始字节流封装进 PDF 容器不做任何重编码既快又稳这个选择帮我少掉了好几天头发。1.3 完整流程预览整个项目可以拆成五个串联的环节请求漫画目录页解析出所有章节的名称和 URL。依次请求每个章节的详情页解析该章节下每一张漫画图片的 URL。为每一张图片发送下载请求以流式方式写入本地磁盘。按章节对本地图片排序用 img2pdf 合并成单文件 PDF。清理过程中的临时图片文件保留最终 PDF。这套流程有一个非常优雅的地方中途断了可以随时重跑。图片下载阶段做了断点续传重复运行不会重复下载已有文件PDF 合并阶段有完整度检查不会把缺页的文件当作成功产物。对于漫画这种动辄几百张图片的任务来说能不能断点续传直接决定脚本是“能用”还是“好用”。2. 目标解析看懂漫画站的页面结构2.1 从目录页到图片页的请求链路动工之前必须把网站的信息架构摸清楚。绝大多数漫画站的 URL 结构长这样书籍目录页是一个 URL每个章节对应目录页里的一个链接点进章节后图片又分页展示每页对应一个子 URL。我用一个通用例子来演示假设目标站点结构如下书籍主页https://manga.example.com/book/12345/章节目录就在书籍主页里用ul列表呈现每个a标签指向章节地址章节页面https://manga.example.com/book/12345/chapter/3/漫画图片章节页面中的img标签src属性指向图片地址这个结构清晰得简直是专门为教学准备的。实际遇到复杂站点也别慌Flex 布局、懒加载、CSS 背景图替换这几种花样后面排查章节我会细讲。眼下先把这个最基础的结构吃透。判断一个章节有多少页你可以直接观察 URL 规律。如果章节地址末尾的数字是页码那就好办但更多站点是在章节页内放一个“下一张”按钮一个页面只有一张大图。第二种结构更常见所以我们解析的目标就是“每一页对应一张大图”。2.2 用开发者工具定位真实图片地址不要一上来就写代码先手动把流程走通一次。打开浏览器进入一个漫画章节页按 F12 打开开发者工具切到 Network网络面板刷新页面然后筛选出图片类型的资源请求。这里有个小技巧很多懒加载站点把真实图片地址放在>HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 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, Referer: https://manga.example.com/, }Referer 字段尤其重要。大部分图片 CDN 只允许图片被页面正常加载时访问检查 Referer 必须匹配白名单域名一旦发现来源不对直接拒绝。这招对小白爬虫几乎一招致命但只要你在下载图片时把 Referer 改成章节页的地址就能轻松绕开。下载图片和解析 HTML 用的请求头最好分开维护因为图片请求需要 Referer而 HTML 请求不需要。3. 代码实现从章节抓取到PDF合并3.1 环境准备与依赖安装老规矩先建虚拟环境。我不推荐把依赖直接装到全局因为 requests、bs4 这些库升级频繁你很难保证不同项目之间的依赖不互相打架。python -m venv manga-downloader source manga-downloader/bin/activate # Windows 用 manga-downloader\Scripts\activate pip install requests beautifulsoup4 pillow img2pdfrequests 负责网络层BeautifulSoup4 负责解析 HTMLPillow 用在后面做图片损坏检查img2pdf 做 PDF 合成。四个库加起来不到几十兆装完就能开工。全部装好后新建一个manga_crawler.py文件接下来所有代码都写在这里。3.2 抓取章节列表第一步是拿到书籍主页解析出所有章节链接。我用一个书旗漫画风格的模拟站点做示范但选择器和 URL 结构完全按照通用逻辑编写你们用任何同类站点都能对照着改。import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse BOOK_URL https://manga.example.com/book/12345/ def get_chapter_list(book_url): resp requests.get(book_url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) chapter_list [] # 选择章节列表的选择器不同站点需要调整 for a in soup.select(ul.chapter-list li a): title a.get_text(stripTrue) href a.get(href) if title and href: full_url urljoin(book_url, href) chapter_list.append((title, full_url)) return chapter_list chapters get_chapter_list(BOOK_URL) print(f共发现 {len(chapters)} 个章节) for title, url in chapters[:5]: print(title, url)urljoin是这个环节的核心函数。HTML 里的链接分三种完整地址、根路径相对地址/book/xxx/、当前路径相对地址chapter/3/。urljoin能把它们全部规范化成完整 URL省去你手写拼接逻辑少踩坑。我见过太多人自己写字符串拼接遇到目录层级不同就拼错了。章节标题里经常混入换行符、空格和特殊字符这些后面要用来做文件名。Windows 文件名不允许包含\/:*?|这九个字符所以提前清洗比最后报错再改要舒服得多。我通常用一行正则处理import re def safe_filename(name): return re.sub(r[\\/*?:|], , name).strip()3.3 提取每页图片地址拿到章节 URL 后请求章节页并提取图片地址。不同的漫画站结构差异不小但无外乎是img标签藏在哪个容器里def get_page_image_url(chapter_url): resp requests.get(chapter_url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) img soup.select_one(div.reader-content img) if not img: return None # 优先取>import os from pathlib import Path def download_image(img_url, save_path, referer): if Path(save_path).exists() and Path(save_path).stat().st_size 1024: print(f跳过已存在文件{save_path}) return True headers {**HEADERS, Referer: referer} try: with requests.get(img_url, headersheaders, streamTrue, timeout30) as resp: resp.raise_for_status() total int(resp.headers.get(content-length, 0)) downloaded 0 with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) downloaded len(chunk) # 大小校验如果服务端给了 content-length下载完对比一下 if total and downloaded ! total: print(f下载不完整{save_path}预期 {total}实际 {downloaded}) return False return True except requests.RequestException as e: print(f下载失败{img_url}原因{e}) return False注意我把streamTrue放在最外层请求里了这样响应对象不会立即把所有内容读入内存。写入时固定用 8KB 的块大小这个值对大多数网站和本地磁盘都是最优解太小浪费 CPU太大容易在弱网条件下卡住。下载顺序我按章节页的自然顺序来也就是从前往后下载然后保存为001.jpg、002.jpg这种带零填充的文件名。零填充非常重要因为字符串排序时10.jpg会排在2.jpg前面如果没有零填充后续合并 PDF 的顺序就乱了。这是新手最容易忽略又最容易翻车的地方。def download_chapter(title, chapter_url): chapter_dir Path(downloads) / safe_filename(title) chapter_dir.mkdir(parentsTrue, exist_okTrue) page_url chapter_url page_num 1 failed 0 while page_url: img_url get_page_image_url(page_url) if not img_url: break filename f{page_num:03d}.jpg save_path chapter_dir / filename ok download_image(img_url, save_path, refererchapter_url) if not ok: failed 1 if failed 3: # 连续失败多次可能是被限流了歇一会儿 time.sleep(10) else: failed 0 next_link get_next_page_link(page_url) page_url urljoin(chapter_url, next_link) if next_link else None page_num 1 time.sleep(0.5) print(f章节 {title} 下载完成共 {page_num - 1} 页)这个get_next_page_link函数就是去解析当前页面里“下一张”按钮的 href 属性。这是漫画站最通用的分页方式比猜测页码靠谱得多。我在分页判断时加了容错机制连续失败三次就休眠 10 秒这是应对隐式限流的常用策略。3.5 按章节合并PDF下载完成只是做完一半合并 PDF 是另一半。合并前我先用 Pillow 做一次完整性检查目的是排除那些下到一半的损坏图片避免生成的 PDF 打不开或者缺页from PIL import Image import img2pdf def images_to_pdf(image_dir, output_pdf): image_files sorted( [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(.jpg)] ) valid_files [] for img_path in image_files: try: with Image.open(img_path) as im: im.verify() # 验证图片完整性 valid_files.append(img_path) except Exception: print(f发现损坏图片已移除{img_path}) os.remove(img_path) if not valid_files: print(没有有效的图片跳过 PDF 生成) return with open(output_pdf, wb) as f: f.write(img2pdf.convert(valid_files)) print(fPDF 已生成{output_pdf})img2pdf.convert接收一个图片文件列表输出格式是 PDF 字节流。它会自动处理图片的分辨率和尺寸不需要你手工调整。之前提过不用 Pillow 转 PDF 的原因——色彩模式问题——在这段代码里暴露得明明白白Pillow 只用来读取并验证图片真正封装 PDF 的活全部交给 img2pdf。另外我帮你想好了一个清理策略合并 PDF 成功后保留一个章节的原始图片还是直接清理我的建议是临时图片保留到所有章节合并完毕后再统一清理万一中间章节需要重新合并就不用重新下载。磁盘吃紧的话合并完先删除该章节图片最后再整体检查一遍 PDF 大小是否合理。4. 常见问题与排查技巧实录4.1 请求被拒或返回403这是新手上路遇到最多的状况。403 的常见原因有四个没带 UA、没带 Referer、请求频率太高、IP 被封锁。逐个排查的顺序也很讲究。先看请求头。你可以在代码里临时打印一下响应状态码和响应头如果返回 403 但手动浏览器访问正常那就是请求头问题。把浏览器里 F12 Copy as cURL 出来的请求头完整搬过来一般就能解决。如果请求头完整但还是 403就要考虑频率了。我见过有人把time.sleep(0.5)改成直接不睡结果 200 张图片冲到第 30 张就被封 IP整个脚本白跑。频率是你的保护伞尤其是目标站没有加密的情况下你只有低调一点才不会触发它的防护阈值。每张图片之间至少间隔 0.3~0.5 秒这个节奏既不会让人等太久也不会触发限流。真要遇到 IP 被临时封锁别想什么代理池。个人娱乐级爬虫用不上那套复杂基建你只需要耐心等待几分钟到几小时换个网络环境接着跑就行。真正需要分布式和代理池的场景是那些以爬虫为主业的商业项目咱们的场景用不上。4.2 图片下载损坏或不完整这种问题最坑因为图片文件名字、数量都在打开才发现图片是坏的一半灰白。原因通常是下载过程中连接被断开或者服务器返回了错误页面但你误当图片保存了。我的策略是双重校验文件大小校验加上 Pillow 完整性校验。文件大小校验在下载函数里已经做过Pillow 校验在合并 PDF 前做。两层筛完基本能保证进入 PDF 的每一张图都是能正常打开的。还有一种情况是下载了一张 HTML 错误页面但文件被保存成了.jpg。判断方法很简单用文本方式打开文件开头如果出现!DOCTYPE html或者一串 JSON 字符串那就是服务器返回了拦截页面。这种文件在 Pillow 完整性检查阶段会被筛出来但最稳妥的做法是下载时检查resp.headers.get(content-type)如果它返回的不是image/jpeg、image/png之类直接放弃这张图片。4.3 合并PDF顺序错乱PDF 页序错乱最常见的原因就是文件名排序问题。如果你已经用了零填充格式基本不会踩这个坑。但如果某个章节图片超过一千页999.jpg之后就会出现1000.jpg字符串排序时它反而排到998.jpg前面。处理办法有两个一是把文件名扩展为四位甚至五位零填充够你用到九千页二是在合并时不要依赖文件名排序而是把页码信息存在一个独立的文件列表里按页码字段排序。我个人的习惯是双保险——既零填充又在合并前用sort(keylambda x: int(re.search(r(\d), x).group(1)))做一次数字排序万无一失。4.4 关于多线程下载的取舍很多人拿到下载脚本后的第一个想法是“太慢了我要加多线程”。先别急我要泼一盆冷水。多线程能提速的前提是服务器愿意让你提速。如果一个漫画站没有加密、没有反爬机制它的带宽本来就是最大的瓶颈。你用单线程 0.5 秒一张图一章 50 页也就半分钟用 20 个线程狂下不仅不一定会快还可能因为触发限流被强制断开最后反而要花更多时间处理失败重试。如果你确定要提速最稳妥的做法是用ThreadPoolExecutor控制并发数最大不超过 5from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers5) as executor: futures [ executor.submit(download_image, img_url, save_path, referer) for img_url, save_path, referer in download_tasks ] for future in as_completed(futures): print(完成一项, future.result())多线程只用于下载阶段解析和合并保持单线程。合并 PDF 必须保证顺序多线程处理顺序逻辑只会让你的代码一团乱麻得不偿失。5. 扩展思路与合规边界5.1 增量更新与章节监控漫画是连载制今天下载完不等于永远结束。很多朋友用这个脚本只下载完当下内容就丢到一边下一周更新了再手动重跑全量脚本浪费时间浪费流量。增量更新很简单在项目目录里建一个downloaded.json文件每成功处理一个章节就把章节标题和 URL 写进这个 JSON。下次跑脚本时先读取 JSON发现某个章节已经处理过直接跳过。这么做还能在脚本异常退出后帮你快速续跑而不是从头再来。配合一个定时任务比如 Windows 的计划任务或者 Linux 的 cron每周日下午自动跑一次增量检查你就能拥有一个完全自动化的追更系统。我个人很喜欢这种“设置一次就再也不操心”的方案这也是爬虫给我生活带来的最大便利之一。5.2 遇到JS渲染站点怎么办你可能已经注意到了我教你的这套方案只适用于图片地址直接出现在 HTML 源码里的站点。有些漫画站会用 JavaScript 动态拼接图片地址或者用懒加载插件让图片地址在初始 HTML 中不可见。遇到这种站怎么办两个备选方案。第一用浏览器开发者工具手动查数据接口。很多 JS 渲染站点并不是没有接口只是接口在页面加载后才会请求。你打开 Network 面板筛选 XHR 或 Fetch 请求往往能看到一个api.php?actionchapteridxxx之类的接口返回 JSON 数据里面直接就是所有图片地址的数组。这种情况你根本不用 Selenium直接模拟请求这个接口就行。第二个方案是用 Selenium 或 Playwright 渲染页面等图片加载完成后再提取 URL。这种方案速度慢、耗资源但确实是通用兜底方案。我平时尽量走接口方案因为它更快更飘但 Selenium 作为最后手段也有存在价值。这里不展开讲太多因为那是另一篇教程的量。知道有这条路将来真遇到了不会觉得无路可走。5.3 爬虫练习的边界与建议技术本身是工具用得好不好全在拿它做什么。写这个教程的核心目的是帮大家掌握网络请求、HTML 解析、文件处理和自动化这批基本功并不是教大家批量扒图盗版传播。在动手爬任何站点之前我的建议是主动阅读目标站的robots.txt和用户协议尊重站点声明。个人学习、离线阅读、存档备份这类场景通常问题不太大但如果你要把下载内容打包传播、商用盈利性质就完全变了。传播盗版漫画涉及版权问题这个红线一定不要碰。现在漫画平台很多都有官方 App 和会员服务你花钱买下的阅读权益比任何爬虫都稳定。爬虫更适合的场景是抓取已进入公有领域的内容、你拥有的内容或者用于纯粹的技术学习。我每写完一个爬虫都会考虑如果我是网站方我会不会介意这种行为——这是判断边界最简单直接的方法。6. 最后再分享几个实操细节如果你的脚本已经能正常运行了后面这几条细节能帮你的产物体感更上一层楼。第一下载下来的图片建议顺手压缩一下再合并。很多漫画站的原图是 1500px 宽以上的高清图一张就要 1MB 左右一章 50 页就是 50MB整个 PDF 体积非常夸张。如果你只是自己在手机或平板上看可以用 Pillow 把图片等比缩放到宽 1280px、质量 85 再合并体积能减少 60%观感几乎没差别。第二PDF 的元数据值得设置。标题、作者、创建日期这些信息做好之后你的离线书库在阅读器里会显得极其工整翻起来也舒服。img2pdf 本身不提供元数据设置接口你可以用 PyPDF2 在生成 PDF 后再写一遍元数据或者干脆就用 PDF 阅读器自带的功能批量编辑。这步不是必选项但做完之后你的“个人漫画图书馆”会很有成就感。第三这个项目的代码结构可以复用目录页解析、详情页解析、文件下载、文件合并这四个模块几乎就是大多数爬虫项目的标准流程。你把漫画站换成图片素材站、壁纸站、文档站逻辑几乎不用动改改选择器和输出格式就能跑。学会这种模块化拆解比你照着教程敲一百遍代码都有用。我在实际写这个项目时第一个版本跑起来全是毛病忘记加 Referer 被 CDN 拒了、文件名没清洗导致 Windows 报错、章节排序错乱导致 PDF 倒着翻。这些坑我全都踩了一遍最后沉淀成这篇文章里的各种防御措施。你现在照着这份教程来基本可以一路顺畅到底。如果中途还是遇到没见过的报错先用好你的开发者工具看看服务器到底返回了什么这个问题你独立解决过一次你的爬虫功力就真正上了一个台阶。
返回列表