Renew Projects

This commit is contained in:
e2hang
2025-12-31 13:22:56 +08:00
parent 4b60ced553
commit d143bbc65c
1753 changed files with 841 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
repo_url: https://github.com/hect0x7/JMComic-Crawler-Python
repo_name: hect0x7/JMComic-Crawler-Python
site_name: jmcomic
theme:
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: 切换到浅色主题
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: 切换到深色主题
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/brightness-4
name: 主题跟随系统
name: material
features:
- navigation.sections
- content.code.copy
- content.code.select
- search.suggest
- search.highlight
- search.share
- navigation.tabs
- navigation.top
plugins:
- search
- mkdocstrings:
handlers:
# See: https://mkdocstrings.github.io/python/usage/
python:
paths: [ '../../src/' ]
options:
summary:
functions: true
preload_modules:
- common
docstring_style: sphinx
markdown_extensions:
- attr_list
- toc:
permalink: true
- pymdownx.highlight
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
docs_dir: sources

View File

@@ -0,0 +1,5 @@
mkdocs
mkdocstrings[python]
markdown-include
mkdocs-material
commonX

View File

@@ -0,0 +1,11 @@
# 版本更新计划
| 版本范围 | 更新计划 |
|:--------:|:--------------------------------------:|
| v2.5.* | 引入新插件`jm-server`,实现基于浏览器观看本地本子。 |
| v2.4.* | 项目实现基本稳定,进入维护期,按需增加功能。 |
| v2.3.* | 实现移动端API的基础功能统一HTML和API的实现。 |
| v2.2.* | 新的插件体系,新的命令行调用,完善搜索功能。 |
| v2.1.* | 拆分Downloader抽象调度优化可扩展性、代码复用性、模块级别自定义。 |
| v2.0.* | 重新设计合理的抽象层次实现请求重试切换域名机制新的option配置设计。 |
| v1.\*.\* | 基于HTML实现基础功能。 |

View File

@@ -0,0 +1,7 @@
# client
::: jmcomic.jm_client_impl
options:
members:
- JmHtmlClient
- JmApiClient

View File

@@ -0,0 +1,6 @@
# command-line
::: jmcomic.cl
options:
members:
- JmcomicUI

View File

@@ -0,0 +1,9 @@
# config
::: jmcomic.jm_config
options:
members:
- JmMagicConstants
- JmModuleConfig
- default_jm_logging

View File

@@ -0,0 +1,11 @@
# download
::: jmcomic.api
options:
members:
- download_album
- download_photo
- create_option
- create_option_by_env
- create_option_by_file
- create_option_by_str

View File

@@ -0,0 +1,11 @@
# entity
::: jmcomic.jm_entity
options:
inherited_members: true
members:
- JmAlbumDetail
- JmPhotoDetail
- JmImageDetail
- JmPageContent
- JmSearchPage

View File

@@ -0,0 +1,7 @@
# option
::: jmcomic.jm_option
options:
members:
- DirRule
- JmOption

View File

@@ -0,0 +1,6 @@
# plugin
::: jmcomic.jm_plugin
options:
filters:
- Plugin$

View File

@@ -0,0 +1,11 @@
# toolkit
::: jmcomic.jm_toolkit
options:
inherited_members: true
members:
- JmcomicText
- PatternTool
- JmPageTool
- JmImageTool
- JmCryptoTool

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -0,0 +1,40 @@
首页
=====================================
> jmcomic库封装了一套可用于爬取禁漫的Python API.
>
> 你可以通过简单的几行Python代码访问禁漫的接口以及下载禁漫的本子。
>
> [查看项目更新计划](TODO.md)
## 入门
- [快速上手(GitHub README)](https://github.com/hect0x7/JMComic-Crawler-Python/tree/master?tab=readme-ov-file#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B)
- [常用类和方法演示](tutorial/0_common_usage.md)
- [option配置以及插件写法](./option_file_syntax.md)
## 特殊用法教程
- [使用GitHub Actions下载本子](./tutorial/1_github_actions.md)
- [使用GitHub Actions导出收藏夹](tutorial/10_export_favorites.md)
- [命令行用法教程](tutorial/2_command_line.md)
- [测试你的ip能访问哪些禁漫域名](tutorial/8_pick_domain.md)
## 核心机制
- [下载过滤器机制](tutorial/5_filter.md)
- [插件机制](tutorial/6_plugin.md)
## 自定义
- [下载文件夹名](tutorial/9_custom_download_dir_name.md)
- [日志](tutorial/11_log_custom.md)
- [模块](tutorial/4_module_custom.md)

View File

@@ -0,0 +1,269 @@
# 配置文件指南
## 1. 配置前需知
* option有`默认值`,你配置文件中的配置项会覆盖`默认值`。因此你只需要添加感兴趣的配置项即可。
* 你也可以使用下面的代码来得到option的默认值。你可以删除其中的大部分配置项只保留你要覆盖的配置项。
```python
from jmcomic import JmOption
JmOption.default().to_file('./option.yml') # 创建默认option导出为option.yml文件
```
## 2. option常规配置项
```yaml
# 开启jmcomic的日志输出默认为true
# 对日志有需求的可进一步参考文档 → https://jmcomic.readthedocs.io/en/latest/tutorial/11_log_custom/
log: true
# 配置客户端相关
client:
# impl: 客户端实现类不配置默认会使用JmModuleConfig.DEFAULT_CLIENT_IMPL
# 可配置:
# html - 表示网页端
# api - 表示APP端
# APP端不限ip兼容性好网页端限制ip地区但效率高
impl: html
# domain: 域名配置,默认是 [],表示运行时自动获取域名。
# 可配置特定域名,如下:
# 程序会先用第一个域名如果第一个域名重试n次失败则换下一个域名重试以此类推。
domain:
- jm-comic.org
- jm-comic2.cc
- 18comic.vip
- 18comic.org
# retry_times: 请求失败重试次数默认为5
retry_times: 5
# postman: 请求配置
postman:
meta_data:
# proxies: 代理配置,默认是 system表示使用系统代理。
# 以下的写法都可以:
# proxies: null # 不使用代理
# proxies: clash
# proxies: v2ray
# proxies: 127.0.0.1:7890
# proxies:
# http: 127.0.0.1:7890
# https: 127.0.0.1:7890
proxies: system
# cookies: 帐号配置,默认是 null表示未登录状态访问JM。
# 禁漫的大部分本子,下载是不需要登录的;少部分敏感题材需要登录才能看。
# 如果你希望以登录状态下载本子最简单的方式是配置一下浏览器的cookies
# 不用全部cookies只要那个叫 AVS 就行。
# 特别注意!!!(https://github.com/hect0x7/JMComic-Crawler-Python/issues/104)
# cookies是区分域名的
# 假如你要访问的是 `18comic.vip`那么你配置的cookies也要来自于 `18comic.vip`,不能配置来自于 `jm-comic.club` 的cookies。
# 如果你发现配置了cookies还是没有效果大概率就是你配置的cookies和代码访问的域名不一致。
cookies:
AVS: qkwehjjasdowqeq # 这个值是乱打的,不能用
# 下载配置
download:
cache: true # 如果要下载的文件在磁盘上已存在不用再下一遍了吧默认为true
image:
decode: true # JM的原图是混淆过的要不要还原默认为true
suffix: .jpg # 把图片都转为.jpg格式默认为null表示不转换。
threading:
# image: 同时下载的图片数默认是30张图
# 数值大,下得快,配置要求高,对禁漫压力大
# 数值小,下得慢,配置要求低,对禁漫压力小
# PS: 禁漫网页一次最多请求50张图
image: 30
# photo: 同时下载的章节数不配置默认是cpu的线程数。例如8核16线程的cpu → 16.
photo: 16
# 文件夹规则配置,决定图片文件存放在你的电脑上的哪个文件夹
dir_rule:
# base_dir: 根目录。
# 此配置也支持引用环境变量,例如
# base_dir: ${JM_DIR}/下载文件夹/
base_dir: D:/a/b/c/
# rule: 规则dsl。
# 本项只建议了解编程的朋友定制,实现在这个类: jmcomic.jm_option.DirRule
# 写法:
# 1. 以'Bd'开头,表示根目录
# 2. 文件夹每增加一层,使用 '_' 或者 '/' 区隔
# 3. 用Pxxx或者Ayyy指代文件夹名意思是 JmPhotoDetail.xxx / JmAlbumDetail的.yyy。xxx和yyy可以写什么需要看源码。
#
# 下面演示如果要使用禁漫网站的默认下载方式,该怎么写:
# 规则: 根目录 / 本子id / 章节序号 / 图片文件
# rule: 'Bd / Aid / Pindex'
# rule: 'Bd_Aid_Pindex'
# 默认规则是: 根目录 / 章节标题 / 图片文件
rule: Bd_Ptitle
```
## 3. option插件配置项
* **插件配置中的kwargs参数支持引用环境变量语法为 ${环境变量名}**
```yaml
# 插件的配置示例
plugins:
after_init:
- plugin: usage_log # 实时打印硬件占用率的插件
kwargs:
interval: 0.5 # 间隔时间
enable_warning: true # 占用过大时发出预警
- plugin: login # 登录插件
kwargs:
username: un # 用户名
password: pw # 密码
- plugin: find_update # 只下载新章插件
kwargs:
145504: 290266 # 下载本子145504的章节290266以后的新章
- plugin: image_suffix_filter # 图片后缀过滤器插件,可以控制只下载哪些后缀的图片
kwargs:
allowed_orig_suffix: # 后缀列表,表示只想下载以.gif结尾的图片
- .gif
- plugin: replace_path_string # 字符串替换插件,直接对下载文件夹的路径进行文本替换
kwargs:
replace:
# {左边写你要替换的原文}: {右边写替换成什么文本}
aaa: bbb
kyockcho: きょくちょ
- plugin: client_proxy # 客户端实现类代理插件,不建议非开发人员使用
kwargs:
proxy_client_key: photo_concurrent_fetcher_proxy # 代理类的client_key
whitelist: [ api, ] # 白名单当client.impl匹配白名单时才代理
- plugin: auto_set_browser_cookies # 自动获取浏览器cookies详见插件类代码→AutoSetBrowserCookiesPlugin
kwargs:
browser: chrome
domain: 18comic.vip
# v2.5.0 引入的插件
# 可以启动一个服务器,可以在浏览器上查看本子
# 基于flask框架需要安装额外库: [pip install plugin_jm_server]
# 源码https://github.com/hect0x7/plugin-jm-server
- plugin: jm_server
kwargs:
password: '3333' # 服务器访问密码
base_dir: D:/a/b/c/ # 根目录默认使用dir_rule.base_dir
# 下面是高级配置,不配置也可以
# run下的参数是flask框架的app对象的run方法参数详见flask文档
run:
host: 0.0.0.0 # 默认接收所有ip的请求
port: 80 # 服务器端口默认为80
debug: false # 是否开启debug模式默认为false
# 支持重写背景图片,可以使用你喜欢的背景图片作为背景
img_overwrite:
bg.jpg: D:/浏览器的背景图
m_bg.jpeg: D:/移动设备浏览器的背景图
- plugin: subscribe_album_update # 自动订阅本子并下载、发送邮件通知的插件
kwargs:
download_if_has_update: true
email_notify: # 参数说明见下【发送qq邮件插件】
msg_from: aaa@qq.com
msg_to: aaa@qq.com
password: dkjlakdjlkas
title: album update !!!
content: album update !!!
album_photo_dict:
324930: 424507
after_album:
- plugin: zip # 压缩文件插件
kwargs:
level: photo # 按照章节,一个章节一个压缩文件
# level 也可以配成 album表示一个本子对应一个压缩文件该压缩文件会包含这个本子的所有章节
filename_rule: Ptitle # 压缩文件的命名规则
# 请注意⚠ [https://github.com/hect0x7/JMComic-Crawler-Python/issues/223#issuecomment-2045227527]
# filename_rule和level有对应关系
# 如果level=[photo], filename_rule只能写Pxxx
# 如果level=[album], filename_rule只能写Axxx
zip_dir: D:/jmcomic/zip/ # 压缩文件存放的文件夹
delete_original_file: true # 压缩成功后,删除所有原文件和文件夹
# 删除重复文件插件
# 参考 → [https://github.com/hect0x7/JMComic-Crawler-Python/issues/244]
- plugin: delete_duplicated_files
kwargs:
# limit: 必填表示对md5出现次数的限制
limit: 3
# 如果文件的md5的出现次数 >= limit是否要删除
# 如果delete_original_file不配置此插件只会打印信息不会执行其他操作
# 如果limit=1, delete_original_file=true 效果会是删除所有文件
delete_original_file: true
- plugin: send_qq_email # 发送qq邮件插件
kwargs:
msg_from: ${EMAIL} # 发件人
msg_to: aaa@qq.com # 收件人
password: dkjlakdjlkas # 发件人的授权码
title: jmcomic # 标题
content: jmcomic finished !!! # 内容
main:
- plugin: favorite_folder_export # 导出收藏夹插件
log: false
kwargs:
zip_enable: true # 对导出文件进行压缩
zip_filepath: ${JM_DOWNLOAD_DIR}/export.zip # 压缩文件路径
zip_password: ${ZIP_PASSWORD} # 压缩密码
before_photo:
- plugin: skip_photo_with_few_images # 跳过下载章节图片数量过少的章节。一些韩漫的章节是公告,没有实际内容,就可以用该插件来跳过下载这些章节。
kwargs:
at_least_image_count: 3 # 至少要有多少张图,才下载此章节
after_photo:
# 把章节的所有图片合并为一个pdf的插件
# 使用前需要安装依赖库: [pip install img2pdf]
- plugin: img2pdf
kwargs:
pdf_dir: D:/pdf/ # pdf存放文件夹
filename_rule: Pid # pdf命名规则P代表photo, id代表使用photo.id也就是章节id
# img2pdf也支持合并整个本子把上方的after_photo改为after_album即可。
# https://github.com/hect0x7/JMComic-Crawler-Python/discussions/258
# 配置到after_album时需要修改filename_rule参数不能写Pxx只能写Axx示例如下
- plugin: img2pdf
kwargs:
pdf_dir: D:/pdf/ # pdf存放文件夹
filename_rule: Aname # pdf命名规则A代表album, name代表使用album.name也就是本子名称
# 插件来源https://github.com/hect0x7/JMComic-Crawler-Python/pull/294
# long_img插件是把所有图片合并为一个png长图效果和img2pdf类似
- plugin: long_img
kwargs:
img_dir: D:/pdf/ # 长图存放文件夹
filename_rule: Aname # 长图命名规则,同上
# 请注意⚠
# 下方的j2p插件的功能不如img2pdf插件不建议使用。
# 如有图片转pdf的需求直接使用img2pdf即可下面的内容请忽略。
- plugin: j2p # 图片合并插件可以将下载下来的jpg图片合成为一个pdf插件
# 请注意⚠ 该插件的使用前提是下载下来的图片是jpg图片
# 因此,使用该插件前,需要有如下配置:下载图片格式转为jpg上文有解释过此配置
# download:
# image:
# suffix: .jpg
kwargs:
pdf_dir: D:/pdf/ # pdf存放文件夹
filename_rule: Pid # pdf命名规则
quality: 100 # pdf质量0 - 100
```

View File

@@ -0,0 +1,298 @@
# 常用类和方法
## 下载本子/章节
```python
from jmcomic import *
# 下载id为438696的本子 (https://18comic.vip/album/438696)
download_album(438696)
# 下载章节 (https://18comic.vip/photo/438696)
download_photo(438696)
# 同时下载多个本子
download_album([123, 456, 789])
```
## 使用option定制化下载本子
如果你在下载本子时有一些定制化需求,
例如指定禁漫域名,使用代理,登录禁漫,图片格式转换等等,
那么你可以试试看jmcomic提供的option机制
```python
from jmcomic import *
# 1. 在调用下载api前通过创建和使用option对象可以定制化下载行为。
# 推荐使用配置文件的方式来创建option对象
# 你可以配置很多东西比如代理、cookies、下载规则等等。
# 配置文件的语法参考: https://jmcomic.readthedocs.io/en/latest/option_file_syntax/
option = create_option_by_file('op.yml') # 通过配置文件来创建option对象
# 2. 调用下载api把option作为参数传递
download_album(123, option)
# 也可以使用下面这种面向对象的方式,是一样的
option.download_album(123)
```
## 获取本子/章节/图片的实体类,下载图片
```python
from jmcomic import *
# 客户端
client = JmOption.default().new_jm_client()
# 本子实体类
album: JmAlbumDetail = client.get_album_detail('427413')
def fetch(photo: JmPhotoDetail):
# 章节实体类
photo = client.get_photo_detail(photo.photo_id, False)
print(f'章节id: {photo.photo_id}')
# 图片实体类
image: JmImageDetail
for image in photo:
print(f'图片url: {image.img_url}')
# 下载单个图片
client.download_by_image_detail(image, './a.jpg')
# 如果是已知未混淆的图片也可以直接使用url来下载
random_image_domain = JmModuleConfig.DOMAIN_IMAGE_LIST
client.download_image(f'https://{random_image_domain}/media/albums/416130.jpg', './a.jpg')
# 多线程发起请求
multi_thread_launcher(
iter_objs=album,
apply_each_obj_func=fetch
)
```
## jmcomic异常处理示例
```python
from jmcomic import *
# 客户端
client = JmOption.default().new_jm_client()
# 捕获jmcomic可能出现的异常
try:
# 请求本子实体类
album: JmAlbumDetail = client.get_album_detail('427413')
except MissingAlbumPhotoException as e:
print(f'id={e.error_jmid}的本子不存在')
except JsonResolveFailException as e:
print(f'解析json失败')
# 响应对象
resp = e.resp
print(f'resp.text: {resp.text}, resp.status_code: {resp.status_code}')
except RequestRetryAllFailException as e:
print(f'请求失败,重试次数耗尽')
except JmcomicException as e:
# 捕获所有异常,用作兜底
print(f'jmcomic遇到异常: {e}')
```
## 搜索本子
```python
from jmcomic import *
client = JmOption.default().new_jm_client()
# 分页查询search_site就是禁漫网页上的【站内搜索】
page: JmSearchPage = client.search_site(search_query='+MANA +无修正', page=1)
# page默认的迭代方式是page.iter_id_title(),每次迭代返回 albun_id, title
for album_id, title in page:
print(f'[{album_id}]: {title}')
# 直接搜索禁漫车号
page = client.search_site(search_query='427413')
album: JmAlbumDetail = page.single_album
print(album.tags)
```
## 搜索并下载本子
```python
from jmcomic import *
option = JmOption.default()
client = option.new_jm_client()
tag = '無修正'
# 搜索标签可以使用search_tag。
# 搜索第一页。
page: JmSearchPage = client.search_tag(tag, page=1)
aid_list = []
for aid, atitle, tag_list in page.iter_id_title_tag(): # 使用page的iter_id_title_tag迭代器
if tag in tag_list:
print(f'[标签/{tag}] 发现目标: [{aid}]: [{atitle}]')
aid_list.append(aid)
download_album(aid_list, option)
```
## 获取收藏夹
可参考discussions: https://github.com/hect0x7/JMComic-Crawler-Python/discussions/235
```python
from jmcomic import *
option = JmOption.default()
client = option.new_jm_client()
client.login('用户名', '密码') # 也可以使用login插件/配置cookies
# 遍历全部收藏的所有页
for page in cl.favorite_folder_gen(): # 如果你只想获取特定收藏夹需要添加folder_id参数
# 遍历每页结果
for aid, atitle in page.iter_id_title():
# aid: 本子的album_id
# atitle: 本子的名称
print(aid)
# 打印当前帐号的所有收藏夹信息
for folder_id, folder_name in page.iter_folder_id_name():
print(f'收藏夹id: {folder_id}, 收藏夹名称: {folder_name}')
# 获取特定收藏夹的单页使用favorite_folder方法
page = client.favorite_folder(page=1,
order_by=JmMagicConstants.ORDER_BY_LATEST,
folder_id='0' # 收藏夹id
)
```
## 分类 / 排行榜
禁漫的分类是一个和搜索有些类似的功能。
搜索是按某一条件进行过滤。
分类没有过滤就是把某一类别category下的本子全都调出来。
禁漫的排行榜就是分类的一种形式
下面演示调用分类api
```python
from jmcomic import *
# 创建客户端
op = JmOption.default()
cl = op.new_jm_client()
# 调用分类接口
# 根据下面的参数,这个调用的意义就是:
# 在全部分类下,选择所有时间范围,按观看数排序后,获取第一页的本子
page: JmCategoryPage = cl.categories_filter(
page=1,
time=JmMagicConstants.TIME_ALL, # 时间选择全部具体可以写什么请见JmMagicConstants
category=JmMagicConstants.CATEGORY_ALL, # 分类选择全部具体可以写什么请见JmMagicConstants
order_by=JmMagicConstants.ORDER_BY_LATEST, # 按照观看数排序具体可以写什么请见JmMagicConstants
)
# 月排行底层实现也是调的categories_filter
page: JmCategoryPage = cl.month_ranking(1)
# 周排行
page: JmCategoryPage = cl.week_ranking(1)
# 循环获取分页,使用 cl.categories_filter_gen
for page in cl.categories_filter_gen(page=1, # 起始页码
# 下面是分类参数
time=JmMagicConstants.TIME_WEEK,
category=JmMagicConstants.CATEGORY_ALL,
order_by=JmMagicConstants.ORDER_BY_VIEW,
):
for aid, atitle in page:
print(aid, atitle)
```
## 高级搜索(分类/副分类)
禁漫网页端的搜索除了常规条件,还支持【分类】和【副分类】的搜索。
在任一搜索页面,你会看到本子图的右上方有两个标签。左边的是【分类】,右边的是【副分类】。
下面演示代码如何编写。
* **注意!!禁漫移动端没有提供如下功能,以下代码仅对网页端生效。**
```python
# 在编写代码前,建议先熟悉禁漫网页的搜本功能,下面的代码都是对照网页编写的。
# 网页搜索示例https://18comic.vip/search/photos/doujin/sub/CG?main_tag=0&search_query=mana&page=1&o=mr&t=a
from jmcomic import *
op = create_option_by_file('op.yml')
# 创建网页端client
html_cl = op.new_jm_client(impl='html')
# 使用站内搜索,指定【分类】和【副分类】
# 分类 = JmMagicConstants.CATEGORY_DOUJIN = 同人本
# 副分类 = JmMagicConstants.SUB_DOUJIN_CG = CG本
# 实际URLhttps://18comic.vip/search/photos/doujin/sub/CG?main_tag=0&search_query=mana&page=1&o=mr&t=a
page = html_cl.search_site(search_query='mana',
category=JmMagicConstants.CATEGORY_DOUJIN,
sub_category=JmMagicConstants.SUB_DOUJIN_CG,
page=1,
)
# 打印page内容
for aid, atitle in page.iter_id_title():
print(aid, atitle)
# 循环获取分页
for page in html_cl.search_gen(search_query='mana',
category=JmMagicConstants.CATEGORY_DOUJIN,
sub_category=JmMagicConstants.SUB_DOUJIN_CG,
page=1, # 起始页码
):
# 打印page内容
for aid, atitle in page.iter_id_title():
print(aid, atitle)
```
## 手动创建Client
```python
# 默认的使用方式是先创建optionoption封装了所有配置然后由option.new_jm_client() 创建客户端client使用client可以访问禁漫接口
# 下面演示直接构造client的方式
from jmcomic import *
"""
创建JM客户端
:param postman: 负责实现HTTP请求的对象持有cookies、headers、proxies等信息
:param domain_list: 禁漫域名
:param retry_times: 重试次数
"""
# 网页端
cl = JmHtmlClient(
postman=JmModuleConfig.new_postman(),
domain_list=['18comic.vip'],
retry_times=1
)
# API端APP
cl = JmApiClient(
postman=JmModuleConfig.new_postman(),
domain_list=JmModuleConfig.DOMAIN_API_LIST,
retry_times=1
)
```

View File

@@ -0,0 +1,75 @@
# 导出并下载你的禁漫收藏夹数据
一共需要三步:
1. fork一份我的代码仓库。
2. 配置你的禁漫帐号密码。
3. 运行工作流,下载结果。
下面截图解析这三步的详细过程。
## 1. fork一份我的代码仓库
访问下面这个网址:
`https://github.com/hect0x7/JMComic-Crawler-Python/fork`
直接拉到页面最底部,如下所示:
最新提示下图的1可以不做即直接点绿色的Create fork按钮
![1](../images/1.png)
## 2. 配置你的禁漫帐号密码
在开始下面的步骤之前你需要先启用你的repo的Actions开启方式如下
![6](../images/6.png)
**注意事项:
最好使用secrets保证不会泄漏信息。
虽然也支持通过工作流dispatch输入帐号密码但是那种方式会让账号密码明文显示在Actions日志中。
如果你打算采用后者,最好下载完就及时删除工作流。**
然后访问下面这个网址:
`https://github.com/你的用户名/JMComic-Crawler-Python/settings/secrets/actions`
按下图步骤进行操作:
![7](../images/7.png)
然后来到填写页面
![8](../images/8.png)
你需要填入下面的内容并点【Add secret】保存
`JM_USERNAME`: 你的禁漫帐号用户名
同样的方式再配置如下内容:
`JM_PASSWORD`: 你的禁漫帐号密码
`ZIP_PASSWORD`: 压缩文件密码,防止别人下载使用你的文件
## 3. 运行工作流,下载结果。
访问如下网址:
`https://github.com/你的用户名/JMComic-Crawler-Python/actions/workflows/export_favorites.yml`
按照下图步骤点击,即可运行工作流。
* 如果你在步骤2已经配置了secrets无需在意输入框。
* 如果你没有配置secrets你可以在输入框中输入账号 密码 压缩密码,但要记得及时删除工作流来避免信息泄漏。
![9](../images/9.png)
待工作流变绿,表示已完成,点击去拉到页面底部,即可下载文件。
![10](../images/10.png)

View File

@@ -0,0 +1,55 @@
# 日志自定义
本文档缘起于 GitHub Discussions: [discussions/195](https://github.com/hect0x7/JMComic-Crawler-Python/discussions/195)
下面是这个问题的解决方法:
## 1. 日志完全开启/关闭
使用代码:
```
from jmcomic import disable_jm_log
disable_jm_log()
```
使用配置:
```yaml
log: false
```
## 2. 日志过滤只保留特定topic
使用插件配置
```yaml
log: true
plugins:
after_init:
- plugin: log_topic_filter # 日志topic过滤插件
kwargs:
whitelist: [ # 只保留api和html这两个是Client发请求时会打的日志topic
'api',
'html',
]
```
## 3. 屏蔽插件的日志
给插件配置加上一个`log`配置项即可
```yaml
plugins:
after_init:
- plugin: client_proxy # 提高移动端的请求效率的插件
log: false # 插件自身不打印日志
kwargs:
proxy_client_key: photo_concurrent_fetcher_proxy
whitelist: [ api, ]
```
## 4. 完全自定义 jmcomic 日志
你可以自定义jmcomic的模块日志打印函数参考文档[模块自定义](./4_module_custom.md#自定义log)

View File

@@ -0,0 +1,74 @@
# GitHub Actions使用教程
一共需要三步:
1. fork一份我的代码仓库。
2. 填写你需要下载的本子id。
3. 等待GitHub Actions下载完成下载成品zip文件。
下面截图解析这三步的详细过程。
## 1. fork一份我的代码仓库
访问下面这个网址:
`https://github.com/hect0x7/JMComic-Crawler-Python/fork`
直接拉到页面最底部,如下所示:
最新提示下图的1可以不做即直接点绿色的Create fork按钮
![1](../images/1.png)
## 2. 填写你需要下载的本子id
在开始下面的步骤之前你需要先启用你的repo的Actions开启方式如下
![6](../images/6.png)
### 2.1. 方式一(最新、简单、推荐)
访问下面这个网址:
`https://github.com/你的用户名/JMComic-Crawler-Python/actions/workflows/download_dispatch.yml`
按下图步骤进行操作:
![5](../images/5.png)
### 2.2. 方式二
访问下面这个网址:
`https://github.com/你的用户名/JMComic-Crawler-Python/edit/master/usage/workflow_download.py`
其实就是编辑 `usage/workflow_download.py`文件,任意分支皆可。
按下图步骤进行操作:
![2](../images/2.png)
看到上面的绿色框里的代码了吗?(位置可能有些变化)
上面有注释把你要下载的本子的id填入一行一个id前面可以带`JM`
填完点提交会自动触发GitHub Actions下载这些本子。
## 3. 等待GitHub Actions下载完成下载成品zip文件
来到Actions页面选择最新的一次记录等待它完成。
![3](../images/3.png)
完成以后,在页面最底部,点击下载你的成品即可。
![4](../images/4.png)
如果你发现GitHub Actions显示❌表明出现了问题运行失败。
下面是问题的排查步骤:
1. 检查你在步骤2中填写是否有误。
2. 点进build查看运行流程找到`运行下载脚本`这个步骤,查看报错的具体信息
3. 来到我的项目仓库点进Issue先根据报错信息搜索Issue如果能搜到类似问题自行参考解决。
4. 如果还是不知道如何解决提一个Issue详细描述下你的问题。

View File

@@ -0,0 +1,25 @@
# 命令行教程
## 1. 基本用法
```
# 下载album 123 456下载photo 333。彼此之间使用空格间隔
jmcomic 123 456 p333
```
## 2. 自定义option
### 2.1. 通过命令行
使用 --option 参数指定option配置文件路径
```
jmcomic 123 --option="D:/a.yml"
```
### 2.2. 使用环境变量
配置环境变量 `JM_OPTION_PATH` 为option配置文件路径
```
set JM_OPTION_PATH="D:/a.yml"
jmcomic 123
```

View File

@@ -0,0 +1,194 @@
# 模块自定义
下方所有函数都省略了如下的导包和准备代码
```python
from jmcomic import *
option = JmOption.default()
client: JmcomicClient = option.build_jm_client()
```
## 自定义下载前后的回调函数
```python
def custom_download_callback():
"""
该函数演示自定义下载时的回调函数
"""
# jmcomic的下载功能由 JmModuleConfig.CLASS_DOWNLOADER 这个类来负责执行
# 这个类默认是 JmDownloader继承了DownloadCallback
# 你可以写一个自定义类继承JmDownloader覆盖属于DownloadCallback的方法来实现自定义回调
class MyDownloader(JmDownloader):
# 覆盖 album 下载完成后的回调
def after_album(self, album: JmAlbumDetail):
print(f'album下载完毕: {album}')
pass
# 最后,让你的自定义类生效
JmModuleConfig.CLASS_DOWNLOADER = MyDownloader
```
## 自定义option类
```python
def custom_option_class():
"""
该函数演示自定义option类
"""
# jmcomic模块支持自定义Option类
# 你可以写一个自己的类继承JmOption然后覆盖其中的一些方法。
class MyOption(JmOption):
def __init__(self, *args, **kwargs):
print('MyOption 初始化开始')
super().__init__(*args, **kwargs)
@classmethod
def default(cls):
print('调用了MyOption.default()')
return super().default()
# 最后替换默认Option类即可
JmModuleConfig.CLASS_OPTION = MyOption
```
## 自定义client类
```python
def custom_client_class():
"""
该文件演示自定义client类
"""
# 默认情况下JmOption使用client类是根据配置项 `client.impl` 决定的
# JmOption会根据`client.impl`到 JmModuleConfig.CLASS_CLIENT_IMPL 中查找
# 自定义client的步骤如下
# 1. 自定义Client类
class MyClient(JmHtmlClient):
client_key = 'myclient'
pass
# 2. 让MyClient生效
JmModuleConfig.register_client(MyClient)
# 3. 在配置文件中使用你定义的client.impl后续使用这个option即可
"""
client:
impl: myclient
"""
```
## 自定义实体类(本子/章节/图片)
```python
def custom_album_photo_image_detail_class():
"""
该函数演示自定义实体类(本子/章节/图片)
在使用路径规则 DirRule 时,可能会遇到需要自定义实体类属性的情况,例如:
dir_rule:
base_dir: ${workspace}
rule: Bd_Acustom_Pcustom
上面的AcustomPcustom都是自定义字段
如果你想要使用这种自定义字段,你就需要替换默认的实体类,方式如下
"""
# 自定义本子实体类
class MyAlbum(JmAlbumDetail):
# 自定义 custom 属性
@property
def custom(self):
return f'custom_{self.title}'
# 自定义章节实体类
class MyPhoto(JmPhotoDetail):
# 自定义 custom 属性
@property
def custom(self):
return f'custom_{self.title}'
"""
v2.3.3: 支持更灵活的自定义方式,可以使用函数,效果同上,示例见下
"""
class MyAlbum2(JmAlbumDetail):
def get_dirname(self, ref: str) -> str:
if ref == 'custom':
return f'custom_{self.name}'
return super().get_dirname(ref)
# 最后,替换默认实体类来让你的自定义类生效
JmModuleConfig.CLASS_ALBUM = MyAlbum
JmModuleConfig.CLASS_PHOTO = MyPhoto
```
## 自定义log
```python
def custom_jm_log():
"""
该函数演示自定义log
"""
# jmcomic模块在运行过程中会使用 jm_log() 这个函数进行打印信息
# jm_log() 这个函数 最后会调用 JmModuleConfig.log_executor 函数
# 你可以写一个自己的函数,替换 JmModuleConfig.log_executor实现自定义log
# 1. 自定义log函数
def my_log(topic: str, msg: str):
"""
这个log函数的参数列表必须包含两个参数topic和msg
@param topic: log主题例如 'album.before', 'req.error', 'plugin.error'
@param msg: 具体log的信息
"""
pass
# 2. 让my_log生效
JmModuleConfig.log_executor = my_log
```
## 自定义异常监听器/回调
```python
def custom_exception_listener():
"""
该函数演示jmcomic的异常监听器机制
"""
# 1. 选一个可能会发生的、你感兴趣的异常
etype = ResponseUnexpectedException
def listener(e):
"""
你的监听器方法
该方法无需返回值
:param e: 异常实例
"""
print(f'my exception listener invoke !!! exception happened: {e}')
# 注册监听器/回调
# 这个异常类或者这个异常的子类的实例将要被raise前你的listener方法会被调用
JmModuleConfig.register_exception_listener(etype, listener)
```

View File

@@ -0,0 +1,78 @@
# Filter - 下载过滤器
filter(过滤器)是v2.1.12新引入的机制,
利用filter你可以实现下载时过滤本子/章节/图片,完全控制你要下载的内容。
使用filter的步骤如下
```
1. 自定义class继承JmDownloader重写do_filter方法即:
class MyDownloader(JmDownloader):
def do_filter(self, detail):
# 如何重写参考JmDownloader.do_filter和下面的示例
...
2. 让你的class生效使用如下代码
JmModuleConfig.CLASS_DOWNLOADER = MyDownloader
3. 照常使用下载api:
download_album(xxx, option)
```
* 下面的示例只演示步骤1
## 示例1只下载章节的前三张图
```python
from jmcomic import *
class First3ImageDownloader(JmDownloader):
def do_filter(self, detail):
if detail.is_photo():
photo: JmPhotoDetail = detail
# 支持[start,end,step]
return photo[:3]
return detail
```
## 示例2只下载本子的特定章节以后的章节
```python
from jmcomic import *
# 参考https://github.com/hect0x7/JMComic-Crawler-Python/issues/95
class FindUpdateDownloader(JmDownloader):
album_after_photo = {
'xxx': 'yyy'
}
def do_filter(self, detail):
if not detail.is_album():
return detail
return self.find_update(detail)
# 带入漫画id, 章节id(第x章)寻找该漫画下第x章节後的所有章节Id
def find_update(self, album: JmAlbumDetail):
if album.album_id not in self.album_after_photo:
return album
photo_ls = []
photo_begin = self.album_after_photo[album.album_id]
is_new_photo = False
for photo in album:
if is_new_photo:
photo_ls.append(photo)
if photo.photo_id == photo_begin:
is_new_photo = True
return photo_ls
```

View File

@@ -0,0 +1,121 @@
# Plugin - 插件
plugin(扩展/插件)是v2.2.0新引入的机制,使用插件可以实现灵活无感知的功能增强。
目前jmcomic已经内置了一些插件源码位于 src/jmcomic/jm_plugin.py。
你可以在这里查看这些插件的配置→ [option_file_syntax](../option_file_syntax.md#3-option插件配置项)
## 1. 插件机制介绍
你可以在option配置文件中配置插件插件会特定的<u>`事件`</u>发生时自动执行。
jmcomic里的内置事件如下
- `after_init`: option对象创建后
- `before_image`: 下载图片前
- `before_album`: 下载本子前
- `before_photo`: 下载章节前
- `after_image`: 下载图片后
- `after_album`: 下载本子后
- `after_photo`: 下载章节后
举例你可以配置一个login插件在option对象创建后`after_init`)时,调用`login`插件,执行登录禁漫的功能。
这样你后续使用option下载本子时都会处于已登录状态。已登录状态可以让你访问所有禁漫本子。
## 2. 示例在after_init时调用login插件
`login`插件功能登录JM获取并保存cookies到option内。后续option创建的Client都会携带cookies即都是登录状态。
配置方式将以下配置写入option文件
```yaml
plugins: # 插件配置项
after_init: # 在after_init事件时自动执行插件
- plugin: login # 插件的key
kwargs: # 下面是给插件的参数 (kwargs),由插件类自定义
username: un # 禁漫帐号
password: pw # 密码
```
有了上述option配置当你调用下面的代码时login插件就会执行。
```python
import jmcomic
option = jmcomic.create_option_by_file('xxx.yml') # 创建option对象
# 程序走到这里login插件已经调用完毕了
# 后续下载本子就都是已登录状态里了
option.download_album(123)
```
## 3. 怎么手动调用插件
你可以使用下面的代码触发某个事件
```python
# 假设你已经创建了option对象
from jmcomic import JmOption
option: JmOption
# 手动调用after_init事件下的插件
option.call_all_plugin('after_init')
# 手动调用一个特定事件的插件(如果你没有配置这个事件的插件,那么无事发生。)
option.call_all_plugin('my_event')
```
## 4. 示例:自定义插件
* 如果你有好的plugin想法也欢迎向我提PR将你的plugin内置到jmcomic模块中
下面演示自定义插件分为3步:
1. 自定义plugin类
2. 让plugin类生效
3. 使用plugin的key
```python
# 1. 自定义plugin类
from jmcomic import JmOptionPlugin, JmModuleConfig
# 自定义一个类继承JmOptionPlugin
class MyPlugin(JmOptionPlugin):
# 指定你的插件的key
plugin_key = 'myplugin'
# 实现invoke方法
# 方法的参数可以自定义,这里假设方法只有一个参数 word
def invoke(self, word) -> None:
print(word)
# 2. 让plugin类生效
JmModuleConfig.register_plugin(MyPlugin)
```
接下来在option中配置如下内容来使用你的插件
```yaml
# 3. 在配置文件中使用plugin
plugins:
after_init: # 事件
- plugin: myplugin # 你自定义的插件key
kwargs:
word: hello jmcomic # 你自定义的插件的参数
```
完成上述步骤后每当你使用下面的代码你的plugin会被调用控制台就会打印出 `hello jmcomic`
```python
from jmcomic import create_option
option = create_option('xxx')
```

View File

@@ -0,0 +1,54 @@
# 综合使用实例
* 基于v2.2.2+内置插件
## 功能需求
1. 下载id为145504的本子
2. 只下载章节在290266之后的新章
3. 最后要把图片转为jpg格式
4. 要使用登录状态去下载;
5. 压缩文件,按照章节压缩,一个章节一个压缩文件,压缩文件的命名: 章节标题.zip
6. 自动把下载的文件全部删除,最后只要保留压缩文件。
## 实现方案
#### 1. 写2行python代码
```python
from jmcomic import create_option
create_option('myoption.yml')
```
#### 2. 配置option调用1的脚本即可
```yaml
dir_rule: # 下载路径规则
rule: Bd_Aid
base_dir: D:/jmcomic
download:
image:
suffix: .jpg # 转为jpg格式的图片
client:
domain:
- 18comic.vip # 指定域名
plugins:
after_init:
- plugin: login # 登录插件
kwargs:
username: un
password: pw
- plugin: find_update # 只下载新章插件
kwargs:
145504: 290266 # 下载本子145504的章节290266以后的新章
after_album:
- plugin: zip # 压缩文件插件
kwargs:
level: photo # 按照章节,一个章节一个压缩文件
filename_rule: Ptitle # 压缩文件的命名规则
zip_dir: D:/jmcomic # 压缩文件存放的文件夹
delete_original_file: true # 压缩成功后,删除所有原文件和文件夹
```

View File

@@ -0,0 +1,81 @@
# 趣味用法测试你的ip可以访问哪些禁漫域名
```python
"""
该脚本的作用测试当前ip可以访问哪些禁漫域名
"""
from jmcomic import *
option = JmOption.default()
meta_data = {
# 'proxies': ProxyBuilder.clash_proxy()
}
disable_jm_log()
def get_all_domain():
template = 'https://jmcmomic.github.io/go/{}.html'
url_ls = [
template.format(i)
for i in range(300, 309)
]
domain_set: Set[str] = set()
def fetch_domain(url):
from curl_cffi import requests as postman
text = postman.get(url, allow_redirects=False, **meta_data).text
for domain in JmcomicText.analyse_jm_pub_html(text):
if domain.startswith('jm365.work'):
continue
domain_set.add(domain)
multi_thread_launcher(
iter_objs=url_ls,
apply_each_obj_func=fetch_domain,
)
return domain_set
domain_set = get_all_domain()
print(f'获取到{len(domain_set)}个域名,开始测试')
domain_status_dict = {}
def test_domain(domain: str):
client = option.new_jm_client(impl='html', domain_list=[domain], **meta_data)
status = 'ok'
try:
client.get_album_detail('123456')
except Exception as e:
status = str(e.args)
pass
domain_status_dict[domain] = status
multi_thread_launcher(
iter_objs=domain_set,
apply_each_obj_func=test_domain,
)
for domain, status in domain_status_dict.items():
print(f'{domain}: {status}')
```
# 程序输出示例
```text
获取到7个域名开始测试
18comic.vip: ok
18comic.org: ok
18comic-palworld.vip: ok
18comic-c.art: ok
jmcomic1.me: ok
jmcomic.me: ok
18comic-palworld.club: ok
```

View File

@@ -0,0 +1,144 @@
# 自定义下载文件夹名
## 0. 最简单直接粗暴有效的方式
使用插件`replace_path_string`
这个插件可以直接替换下载文件夹路径配置示例如下把如下配置放入option配置文件即可
```yml
plugins:
after_init:
- plugin: replace_path_string
kwargs:
replace:
# {左边写你要替换的原文}: {右边写替换成什么文本}
kyockcho: きょくちょ
```
该示例会把文件夹路径中所有`kyockcho`都变为`きょくちょ`,例如:
`D:/a/[kyockcho]本子名称 - kyockcho/` 改为↓
`D:/a/[きょくちょ]本子名称 - きょくちょ/`
---------------
**_如果上述简单的文本替换无法满足你或者你需要更多上下文写逻辑代码那么下面的内容正适合你阅读。_**
## 1. DirRule机制简介
当你使用download_album下载本子时本子会以一定的路径规则DirRule下载到你的磁盘上。
你可以使用配置文件定制DirRule例如下面的例子
```yaml
dir_rule:
# 设定根目录 base_dir
base_dir: D:/a/b/c/
rule: Bd / Ptitle # P表示章节title表示使用章节的title字段
# 这个规则的含义是,把图片下载到路径 {base_dir}/{Ptitle}/ 下
# 即:根目录 / 章节标题 / 图片文件
```
例如假设一个章节的名称Ptitle是ddd则最后的下载文件夹结构为 `D:/a/b/c/ddd/`
```
D:/a/b/c/ddd/00001.webp
D:/a/b/c/ddd/00002.webp
D:/a/b/c/ddd/00003.webp
...
```
上述的PtitleP表示章节title表示使用章节的title字段。
除了title你还可以写什么其实Ptitle表示的是jmcomic里的章节实体类 JmPhotoDetail 的属性。
最终能写什么取决于JmPhotoDetail有哪些属性建议使用IDE来获知这些属性不过这需要你懂一些python基础。
除了Pxxx你还可以写Axxx表示这个章节所在的本子的属性xxx详见本子实体类 JmAlbumDetail。
## 2. 自定义字段名
上述例子使用了title字段如果你想自定义一个字段然后在DirRule中使用自定义字段该怎么做
基于v2.4.6,你可以使用如下方式
1. 给你的自定义字段取个名
```yaml
dir_rule: # 忽略base_dir配置项
rule: Bd_Amyname # A表示本子myname表示本子的一个自定义字段
```
2. 在代码中,加入你自定义字段的处理函数
```python
from jmcomic import JmModuleConfig
# 你需要写一个函数把字段名作为key函数作为value加到JmModuleConfig.AFIELD_ADVICE这个字典中
JmModuleConfig.AFIELD_ADVICE['myname'] = lambda album: f'[{album.id}] {album.title}'
```
这样一来Amyname这个规则就会交由你的函数进行处理你便可以返回一个自定义的文件夹名
## 3. 更多的使用例子
### 完全使用自己的文件夹名
```python
from jmcomic import JmModuleConfig
dic = {
'248965': '社团学姐(爆赞韩漫)'
}
# Amyname
JmModuleConfig.AFIELD_ADVICE['myname'] = lambda album: dic[album.id]
download_album(248965)
```
### 文件夹名=作者+标题
```python
from jmcomic import JmModuleConfig
# Amyname
JmModuleConfig.AFIELD_ADVICE['myname'] = lambda album: f'【{album.author}{album.title}'
# album有一个内置字段 authoroname效果类似
```
### 文件夹名=禁漫车号+标题
```python
from jmcomic import JmModuleConfig
# Pmyname
JmModuleConfig.PFIELD_ADVICE['myname'] = lambda photo: f'【{photo.id}{photo.title}'
```
### 文件夹名=第x话+标题
```yaml
# 直接使用内置字段 indextitle 即可
dir_rule:
rule: Bd_Pindextitle
```