Skip to content

Commit

Permalink
v2.4.3: 实现最新的禁漫APP接口加解密算法(1.6.3),优化代码结构,更新README (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
hect0x7 authored Nov 22, 2023
1 parent 52298d0 commit f35cb12
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 146 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# Python API For JMComic (禁漫天堂)

封装了一套可用于爬取JM的Python API.
本项目封装了一套可用于爬取JM的Python API.

简单来说,就是可以通过简单的几行Python代码,实现下载JM上的本子到本地,并且是处理好的图片.
你可以通过简单的几行Python代码,实现下载JM上的本子到本地,并且是处理好的图片

**友情提示:珍爱JM,为了减轻JM的服务器压力,请不要一次性爬取太多本子,西门🙏🙏🙏**.

## 项目介绍

本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。

除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹等等,按需实现。

目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。


## 安装教程

* 通过pip官方源安装(推荐,并且更新也是这个命令)
Expand Down Expand Up @@ -38,6 +47,7 @@ $ jmcomic 422866
## 项目特点

- **绕过Cloudflare的反爬虫**
- **实现禁漫APP接口最新的加解密算法 (1.6.3)**
- 用法多样:

- GitHub Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md))
Expand Down
2 changes: 1 addition & 1 deletion src/jmcomic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# 被依赖方 <--- 使用方
# config <--- entity <--- toolkit <--- client <--- option <--- downloader

__version__ = '2.4.2'
__version__ = '2.4.3'

from .api import *
from .jm_plugin import *
Expand Down
91 changes: 72 additions & 19 deletions src/jmcomic/jm_client_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ def album_comment(self,
)

resp = self.post('/ajax/album_comment',
headers=JmModuleConfig.album_comment_headers,
headers=self.album_comment_headers,
data=data,
)

Expand Down Expand Up @@ -467,6 +467,26 @@ def check_special_http_code(cls, resp):
+ (f'URL=[{url}]' if url is not None else '')
)

album_comment_headers = {
'authority': '18comic.vip',
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'origin': 'https://18comic.vip',
'pragma': 'no-cache',
'referer': 'https://18comic.vip/album/248965/',
'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/114.0.0.0 Safari/537.36',
'x-requested-with': 'XMLHttpRequest',
}


# 基于禁漫移动端(APP)实现的JmClient
class JmApiClient(AbstractJmClient):
Expand Down Expand Up @@ -556,7 +576,7 @@ def fetch_detail_entity(self, apid, clazz):
url,
params={
'id': apid,
}
},
)

self.require_resp_success(resp, url)
Expand All @@ -571,11 +591,13 @@ def fetch_scramble_id(self, photo_id):
resp = self.req_api(
self.API_SCRAMBLE,
params={
"id": photo_id,
"mode": "vertical",
"page": "0",
"app_img_shunt": "1",
}
'id': photo_id,
'mode': 'vertical',
'page': '0',
'app_img_shunt': '1',
'express': 'off',
'v': time_stamp(),
},
)

scramble_id = PatternTool.match_or_default(resp.text,
Expand Down Expand Up @@ -713,21 +735,41 @@ def favorite_folder(self,
return JmPageTool.parse_api_to_favorite_page(resp.model_data)

def req_api(self, url, get=True, **kwargs) -> JmApiResp:
# set headers
headers, key_ts = self.headers_key_ts
kwargs['headers'] = headers
ts = self.decide_headers_and_ts(kwargs, url)

if get:
resp = self.get(url, **kwargs)
else:
resp = self.post(url, **kwargs)

return JmApiResp.wrap(resp, key_ts)
return JmApiResp(resp, ts)

# noinspection PyMethodMayBeStatic
def decide_headers_and_ts(self, kwargs, url):
# 获取时间戳
if url == self.API_SCRAMBLE:
# /chapter_view_template
# 这个接口很特殊,用的密钥 18comicAPPContent 而不是 18comicAPP
# 如果用后者,则会返回403信息
ts = time_stamp()
token, tokenparam = JmCryptoTool.token_and_tokenparam(ts, secret=JmMagicConstants.APP_TOKEN_SECRET_2)

elif JmModuleConfig.use_fix_timestamp:
ts, token, tokenparam = JmModuleConfig.get_fix_ts_token_tokenparam()

else:
ts = time_stamp()
token, tokenparam = JmCryptoTool.token_and_tokenparam(ts)

# 计算token,tokenparam
headers = kwargs.get('headers', JmMagicConstants.APP_HEADERS_TEMPLATE.copy())
headers.update({
'token': token,
'tokenparam': tokenparam,
})
kwargs['headers'] = headers

@property
def headers_key_ts(self):
key_ts = time_stamp()
return JmModuleConfig.new_api_headers(key_ts), key_ts
return ts

@classmethod
def require_resp_success(cls, resp: JmApiResp, orig_req_url: str):
Expand All @@ -743,11 +785,22 @@ def require_resp_success(cls, resp: JmApiResp, orig_req_url: str):
# 暂无

def after_init(self):
# cookies = self.__class__.fetch_init_cookies(self)
# self.get_root_postman().get_meta_data()['cookies'] = cookies
# 保证拥有cookies,因为移动端要求必须携带cookies,否则会直接跳转同一本子【禁漫娘】
if JmModuleConfig.api_client_require_cookies:
self.ensure_have_cookies()

self.get_root_postman().get_meta_data()['cookies'] = JmModuleConfig.get_cookies(self)
pass
from threading import Lock
client_init_cookies_lock = Lock()

def ensure_have_cookies(self):
if self.get_meta_data('cookies'):
return

with self.client_init_cookies_lock:
if self.get_meta_data('cookies'):
return

self['cookies'] = JmModuleConfig.get_cookies(self)


class FutureClientProxy(JmcomicClient):
Expand Down
30 changes: 3 additions & 27 deletions src/jmcomic/jm_client_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,45 +68,21 @@ def transfer_to(self,

class JmApiResp(JmResp):

@classmethod
def wrap(cls, resp, key_ts):
def __init__(self, resp, ts: str):
ExceptionTool.require_true(not isinstance(resp, JmApiResp), f'重复包装: {resp}')

return cls(resp, key_ts)

def __init__(self, resp, key_ts):
super().__init__(resp)
self.key_ts = key_ts
self.ts = ts
self.cache_decode_data = None

@property
def is_success(self) -> bool:
return super().is_success and self.json()['code'] == 200

@staticmethod
def parse_data(text, time) -> str:
# 1. base64解码
import base64
data = base64.b64decode(text)

# 2. AES-ECB解密
# key = 时间戳拼接 '18comicAPPContent' 的md5
import hashlib
key = hashlib.md5(f"{time}18comicAPPContent".encode("utf-8")).hexdigest().encode("utf-8")
from Crypto.Cipher import AES
data = AES.new(key, AES.MODE_ECB).decrypt(data)

# 3. 移除末尾的一些特殊字符
data = data[:-data[-1]]

# 4. 解码为字符串 (json)
res = data.decode('utf-8')
return res

@property
@field_cache('__cache_decoded_data__')
def decoded_data(self) -> str:
return self.parse_data(self.encoded_data, self.key_ts)
return JmCryptoTool.decode_resp_data(self.encoded_data, self.ts)

@property
def encoded_data(self) -> str:
Expand Down
Loading

0 comments on commit f35cb12

Please sign in to comment.