为什么要做
还在做汉化组的扒图的时候,一直觉得comic walker这个站点的漫画拔起来非常麻烦,因为它自带一个阅读器界面,异步加载图片,而且无法右键,甚至在网页元素中,也无法直接保存图片(这应该是漫画网站的常见配置了),只能一个个在控制台中拖动图片至新的标签页才能加载图片并保存,十分麻烦,正好学习python中,就写了这个小工具。

四个选项皆无法保存图片
找到真实图片的加载方式
先搞清楚漫画的图片是如何加载的,才能进行写步骤代码的操作,先进入任意漫画阅读器界面中,进入F12控制台中并使用监视器查看详细的加载步骤



可以很容易就找到图像位置并看到具体的请求url,第一步首先尝试此url是否为图片的真实url,复制url,新建标签页,输入后回车得到404,可见请求url并非真实的图片url

得到的只是404
可以说,绝大多数常见的漫画网站基本都是把链接放在json内再各种手段加密,从而得到一个经过前端处理的图片,具体图片的请求url也是经过js随机产生的,这就是连控制台内右键都无法保存图片的原因。
再返回到控制台,可以看到三个图片下面有三个显示为奇怪的乱码的文件,且每次进行翻页的操作,这个迷之文件也和图片一起,一一对应地加载,很明显这个文件似乎跟最终图片有关,更进一步也印证了这个观点,每次刷新下,图片url都会改变,但迷之文件的url是固定的。

此时从json文件中下手,仔细寻找一番,可以再KDCW_MF03201766010001_68和frames这两个文件中发现储存的漫画数据

其中最重要的便是frames,里面有每个迷之乱码文件的url和一段对应的drm_hash码,很明显是用这一串hash对乱码文件进行了某种计算后才能得到原始图片
当然到了这一地步,应该是查看具体的js代码,查看具体是做了什么样的计算,但很可惜本人没有学过js,看不懂js脚本,所以试了各种方法和手段都无法得到图片
于是具体的解密方法就要请出万能的github了,经过一番搜索,我找到了一个由@vtbk 写的开源爬虫项目,其中就有对迷之文件解密的操作
具体来说,就是经过url得到的乱码文件转换为二进制文件,把hash的前16位也转化为二进制文件,再对这两个数据进行集合计算,得到两个文件中不同时存在的数据,结果的二进制文件,就是漫画原图。
开始写代码
由于本代码是给仍在comic walker进行扒图操作的汉化组组员使用的,所以需要考虑到便捷性,于是有了个大致的思路

import os
import requests
import json
from itertools import cycle
from PIL import Image
from io import BytesIO
from bs4 import BeautifulSoup
#得到漫画url
def get_comic_url():
url = input('请输入漫画所在网址\n例:https://comic-walker.com/contents/detail/KDCW_KS01200137010000_68/\n')
return url
#传入漫画url,返回由章节标题和cid组成的字典,以及漫画标题
def get_chpter_cid_title(comic_url):
response = request(comic_url).content
html_text = BeautifulSoup(response,'html.parser',from_encoding='utf-8')
souce_chapter = html_text.find(class_="ac-content-inner").find_all('a')
comic_title = html_text.find(class_="comicIndex-box").find('h1').get_text()
cid = []
chpter_title = []
for chapter in souce_chapter:
cid.append(chapter.attrs["href"].split('=')[-1])
chpter_title.append(chapter.attrs["title"].replace(comic_title,'').replace(' ',''))
return dict(zip(chpter_title,cid)),comic_title
#传入漫画标题以及由章节标题和cid组成的字典,返回经过筛选的路径列表和cid字典
def check_path(comic_title:str,chpter_cid:dict):
main_path = get_comic_path()
title_path = os.path.join(main_path,comic_title)
if os.path.exists(title_path):#漫画文件夹存在
creat_chapter_path_list = []
need_del_key = []
for key in chpter_cid.keys():#筛选需要更新的章节
chapter_path = os.path.join(title_path,key)
if os.path.exists(chapter_path):
need_del_key.append(key)
else:
creat_chapter_path_list.append(chapter_path)
for key in need_del_key:
del chpter_cid[key]
if chpter_cid == {}:
print('此漫画无可更新章节')
#需要下载的字典为空,无可用更新,退出
os.system('pause')
print('开始在 %s 下创建以下文件夹:'%title_path)
for creat_chapter_path in creat_chapter_path_list:#开始创建文件夹
os.mkdir(creat_chapter_path)
print('%s创建成功'%creat_chapter_path)
return creat_chapter_path_list,chpter_cid
else:#漫画文件夹不存在
os.mkdir(title_path)
print('开始在 %s 下创建以下文件夹:'%title_path)
chapter_path_list = []
for key in chpter_cid.keys():
chapter_path = os.path.join(title_path,key)
chapter_path_list.append(chapter_path)
os.mkdir(chapter_path)
print('%s创建成功'%chapter_path)
return chapter_path_list,chpter_cid
#读取本地储存好的路径
def path_open_read():
with open('./path.txt','r') as f:
path = f.read()
return path
#写入路径并保存为txt
def path_open_write(path):
with open('./path.txt','w') as f:
f.write(path)
return None
#检查漫画储存路径
def get_comic_path():
if os.path.exists('./path.txt'):
return path_open_read()
else:
while 1:
path = input('请输入存储漫画的文件夹路径:\n')
if os.path.exists(path):
path_open_write(path)
return path
else:
print('路径不存在,请检查并重新输入')
#请求
def request(url: str):
#headers = {'User-Agent': 'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'}
return requests.get(url)
#传入json形式的frames,得到未解密的原图
def fetch_images_as_bytes(frames):
print('开始查找图片:')
images = []
i=1
for frame in frames:
url = frame['meta']['source_url']
image_as_bytes = request(url).content
images.append(image_as_bytes)
print('查找第%d张图片'%i)
i += 1
print('此章节查找结束')
return images
#传入json形式的frames,得到drm_hash码
def get_drm_hashes_from_frames(frames):
return [frame['meta']['drm_hash'] for frame in frames]
#传入未解密的原图和hash码,返回解密后的二进制列表
def solve(sources, drm_hashes):
images = []
for source, drm_hash in zip(sources, drm_hashes):
decoded_image_bytes = bytearray()
for a, b in zip(source, cycle(bytes.fromhex(drm_hash[0:16]))):
decoded_image_bytes.append(a ^ b)
decoded_image = Image.open(BytesIO(decoded_image_bytes))
images.append(decoded_image)
return images
#按文件名和路径依次保存解密好的图片
def save(images,chapter_path):
print('开始保存图片:')
for i, image in enumerate(images):
filename = str(i+1) + '.jpg'
filename = filename.rjust(7, '0')
filepath = os.path.join(chapter_path,filename)
image.save(filepath)
print('保存第%s张图片'%i)
print('此章节保存结束')
#传入cid,得到章节frames的请求url
def get_url_frames(cid):
url_frames = 'https://manga-api.nicoseiga.jp/api/v1/comicwalker/episodes/{}/frames'.format(cid)
return url_frames
#运行
def run():
print('使用时请打开代理全局模式,卡死时请直接退出并检查代理设置')
comic_url = get_comic_url()
chpter_cid,comic_title = get_chpter_cid_title(comic_url)
chapter_path_list,chpter_cid_checked = check_path(comic_title,chpter_cid)
#cid_list = [valus for key,valus in chpter_cid_checked.items()]
i = 0
chapter_num = len(chpter_cid_checked)
for cid in chpter_cid_checked.values():
url_frames = get_url_frames(cid)
chapter_frames = json.loads(request(url_frames).content)['data']['result']
images = solve(fetch_images_as_bytes(chapter_frames), get_drm_hashes_from_frames(chapter_frames))
save(images,chapter_path_list[i])
i += 1
if i == chapter_num:
print('漫画保存完毕')
else:
print('剩余{}个章节'.format(chapter_num-i))
run()
os.system('pause')
由于环境和IDE的问题,把所有功能都放在了一个文件下,并打好包后传给了组员,进过几次尝试之后,下载成功。
存在的问题
- 检测到本地path.txt文件后没有验证里面的路径是否存在
- 对网络环境要求苛刻,需要全局代理
- 下载与保存是按每一话而不是每一张图,导致错误后维护性差
目前来说,作为一个小汉化组自用的小工具是够用了,后续或许会更新其他站点的爬取规则