This commit is contained in:
cwx committed 2024-10-28 02:41:52 +08:00
1 parent ca805fe252
commit 5bd0292131
8 files changed
+20 -20

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import os.path
from scrapy.exporters import PythonItemExporter
from scrapy.exporters import JsonItemExporter
class CommonExporter():
def getPath(self, file , sufix=None):
sufix = "."+sufix
dirname = os.path.dirname(file)
if not os.path.exists(dirname):
os.makedirs(dirname)
if sufix != None and sufix not in file:
file = file + sufix
return file
class ItemExporter(PythonItemExporter):
def convert(self, data):
if isinstance(data, bytes): return data.decode("utf-8")
if isinstance(data, dict): return dict(map(self.convert, data.items()))
if isinstance(data, tuple): return map(self.convert, data)
if isinstance(data, list): return [self.convert(i) for i in data]
return data
def export_obj(self, obj_item):
self.start_exporting()
obj_item = self.convert(self.export_item(obj_item))
self.finish_exporting()
return obj_item
class JsonExport(JsonItemExporter):
def __init__(self, file, **kwargs):
file = CommonExporter().getPath(file=file, sufix= "json")
self.file = open(file, "wb")
super(JsonExport, self).__init__(self.file, **kwargs)
def export_json(self, json_object, if_return=False):
self.start_exporting()
self.export_item(json_object)
self.finish_exporting()
self.file.close()
if if_return:
return ItemExporter().export_obj(json_object)
+106
View File
@@ -0,0 +1,106 @@
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.org/en/latest/topics/items.html
import os,Comics.settings as settings,logging
from scrapy.item import Item, Field
from Comics._utils.utils import ComicPath
from Comics._utils.utils import imageUtils,fileUtils
from itemloaders.processors import TakeFirst
# 繁体中文转为简体中文
def serialize_to_chinese(value): return ComicPath.chinese_convert(value)
# 将路径字符串转为合法路径
def serialize_to_fix_file(value): return ComicPath.fix_file_name(value)
def serialize_name_to_chinese(value): return ComicPath.fix_file_name(ComicPath.chinese_convert(value))
# 图片数据解析(私有方法)
def _serialize_to_images(value, result_type=None):
(count, images_item, image_urls) = [1,[],[]]
for image in value:
try:
(image_src, scramble) = [image.get("src"), image.get("scramble")]
except:
(image_src, scramble) = [image, False]
count_image = settings.IMAGES_NAME_FORMAT.format(count)
# suffix = "."+str(image_src).split(".")[-1]
suffix = ".jpg"
image_name = count_image + suffix
#if scramble:
if scramble == "True":
de_str = str(image_src).split("/")[-1].replace(suffix, "==")
blocks_num = imageUtils.encodeImage(de_str)
image_name = ComicPath.getFileScrambleImageName(count=count_image, block=blocks_num, suffix=suffix)
if str(image_src).startswith('http'):
images_item.append(image_name)
else:
images_item.append(image_src)
image_urls.append(image_src)
count += 1
logging.debug(f"images_len: {len(images_item)}")
if result_type == "image_urls": return image_urls
else: return images_item
# 图像处理方法
def serialize_to_images(value): return _serialize_to_images(value)
# 图像链接处理方法
def serialize_to_image_urls(value): return _serialize_to_images(value, result_type="image_urls")
# ComicItem
class ComicItem(Item):
# 工程
current_project = Field()
# link
link = Field()
# 全部章节
chapters = Field()
# 编号
index = Field(output_processor=TakeFirst())
# 漫画名
name = Field(serializer=serialize_name_to_chinese, output_processor=TakeFirst())
# 源漫画名
s_name = Field(serializer=serialize_to_fix_file, output_processor=TakeFirst())
# 章节名
chapter = Field(serializer=serialize_name_to_chinese, output_processor=TakeFirst())
# 源章节名
s_chapter = Field(serializer=serialize_to_fix_file, output_processor=TakeFirst())
# 图片链接
list_img = Field(serializer=serialize_to_images)
# 作者
author = Field(serialize_to_chinese=serialize_to_chinese, output_processor=TakeFirst())
# 封面链接
icon = Field(output_processor=TakeFirst())
# 标签
tags = Field(serializer=serialize_to_chinese, output_processor=TakeFirst())
# 概述
dep = Field(serializer=serialize_to_chinese, output_processor=TakeFirst())
# 时间
date = Field(output_processor=TakeFirst())
# 流派
genre = Field(output_processor=TakeFirst())
# 年龄分级
age_rating = Field(output_processor=TakeFirst())
# 合计
count = Field()
# 旧图片
images_old = Field(serializer=serialize_to_images)
# 章节图像合集
images = Field(serializer=serialize_to_images)
# 图像链接
image_urls = Field(serializer=serialize_to_image_urls)
# 图像名
images_name = Field()
domain = Field()
#章节链接
chapter_href = Field()
#章节API
chapter_api = Field()
class BooksItem(Item):
current_project = Field()
names = Field()
urls = Field()
+205
View File
@@ -0,0 +1,205 @@
import json,logging,os
from scrapy.loader import ItemLoader
from Comics.settings import PROJECT_KEY,IMAGES_STORE
class BaseLoader(ItemLoader):
def parseExec(self,data,exec):
if data !=None and exec != None:
dots = str(exec).split(".")
if not isinstance(data,dict): data = json.loads(data)
for dot in dots:
if data != None: data = data.get(dot)
logging.debug(f"data= {data} dot={dot}")
return data
def add_xpath(self, field_name, xpath, *processors, index=None, exec=None, re=None, is_null=None, **kw):
"""
Similar to :meth:`ItemLoader.add_value` but receives an XPath instead of a
value, which is used to extract a list of strings from the
selector associated with this :class:`ItemLoader`.
See :meth:`get_xpath` for ``kwargs``.
:param xpath: the XPath to extract data from
:type xpath: str
Examples::
# HTML snippet: <p class="product-name">Color TV</p>
loader.add_xpath('name', '//p[@class="product-name"]')
# HTML snippet: <p id="price">the price is $1200</p>
loader.add_xpath('price', '//p[@id="price"]', re='the price is (.*)')
"""
values = self._get_xpathvalues(xpath, **kw)
if exec is not None:
values = self.parseExec(values, exec)
if index is not None:
if len(values) - 1 >= index:
try:
values = values[index]
except Exception as e:
logging.error(f"values [{values}] error index [{index}]")
logging.error(e)
else:
if is_null is not None: values = is_null
else: values=""
self.add_value(field_name, values, *processors, re=re, **kw)
def add_exec(self, field_name, value, str_exec=None, *processors, re=None, **kw):
if str_exec is not None:
value = self.parseExec(value, str_exec)
self.add_value(field_name, value, *processors, re=re, **kw)
def get_exec(self, value, str_exec):
return self.parseExec(value, str_exec)
def add_value(self, field_name, value, *processors, re=None, **kw):
if self.auto_replace_value(field_name, value):
return super().add_value(field_name, value, *processors, re=re, **kw)
def auto_replace_value(self, field_name, value):
if self.get_output_value(field_name) != None:
self._replace_value(field_name, value)
return False
else: return True
# 设置漫画属性
def set_properties(self, name, value=None, xpath=None, index=None, sexec=None):
if value != None:
self.add_value(field_name=name, value=value)
if xpath != None:
self.add_xpath(field_name=name, xpath=xpath, index=index)
if sexec != None:
self.add_exec(field_name=name, value=value, str_exec=sexec)
def get_output_value(self, field_name, skip_field=["chapter"]):
value = super().get_output_value(field_name)
try:
if isinstance(value, list) and len(value) == 1:
if field_name not in skip_field: value = value[0]
else: value = "".join(value)
except:
print(f"get_output_value value={value} type={type(value)}")
return value
# 工程名
def project_name(self, project_name): self.add_value(PROJECT_KEY, project_name)
# 工程名
def get_project_name(self): return self.get_output_value(PROJECT_KEY)
class ComicLoader(BaseLoader):
# 漫画名
def name(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('name', value, xpath, index, sexec)
# 漫画封面链接
def icon(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('icon', value, xpath, index, sexec)
# 作者
def author(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('author', value, xpath, index, sexec)
# 标签
def tags(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('tags', value, xpath, index, sexec)
# 概述
def dep(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('dep', value, xpath, index, sexec)
# 时间
def date(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('date', value, xpath, index, sexec)
# 流派
def genre(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('genre', value, xpath, index, sexec)
# 年龄分级
def age_rating(self, value=None, xpath=None, index=None, sexec=None):
self.set_properties('age_rating', value, xpath, index, sexec)
# 全部章节
def chapters(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('chapters', value, xpath, index, sexec)
# 单一章节
def chapter(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('chapter', value, xpath, index, sexec)
# 图像名称
def images(self, value=None, xpath=None, index=None, sexec=None): self.set_properties('images', value, xpath, index, sexec)
# 图像链接
def image_urls(self, value=None, xpath=None, index=None, sexec=None):
self.set_properties('images', value, xpath, index, sexec)
self.set_properties('image_urls', value, xpath, index, sexec)
def get_output_value(self, field_name, skip_field=["chapter"]):
value = super().get_output_value(field_name)
try:
if isinstance(value, list) and len(value) == 1:
if field_name not in skip_field: value = value[0]
else: value = "".join(value)
except:
print(f"get_output_value value={value} type={type(value)}")
return value
# 漫画名称
def get_name(self): return self.get_output_value("name")
def get_sname(self): return self.get_output_value("s_name")
# 漫画章节
def get_chapter(self): return self.get_output_value("chapter")
def get_schapter(self): return self.get_output_value("s_chapter")
# 漫画封面
def get_icon(self): return self.get_output_value("icon")
# 章节链接
def get_chapter_href(self): return self.get_output_value("chapter_href")
# 全部章节
def get_chapters(self): return self.get_output_value("chapters")
def get_chapter_api(self): return self.get_output_value("chapter_api")
def get_images(self): return self.get_output_value("images")
def get_image_urls(self): return self.get_output_value("image_urls")
### ComicEntity
def set_count(self, value): self.set_properties('count', value=value)
def set_index(self, value): self.set_properties('index', value=value)
def set_sname(self, value): self.set_properties('s_name', value=value)
def set_chapter(self, value): self.set_properties('chapter', value=value)
def set_schapter(self, value): self.set_properties('s_chapter', value=value)
def set_domain(self, value): self.set_properties('domain', value=value)
def get_domain(self): return self.get_output_value("domain")
# 章节页码
def count(self):
len_images = len(self.get_images())
if len_images != 0: return self.set_count(len_images)
# 章节编号
def index(self):
chapters, chapter = [ self.get_chapters(), self.get_chapter() ]
if chapter != None and len(chapters) > 0: return self.set_index(chapters.index(chapter) + 1)
def save_sname_schapter(self):
chapter = self.get_chapter()
name = self.get_name()
if chapter != None : self.set_schapter(chapter)
if name != None : self.set_sname(name)
def load_item(self, chapter=None):
self.count()
self.index()
if not self.get_icon().startswith("http"): self.icon(self.get_domain()+ self.get_icon())
if chapter != None: self.set_chapter(chapter)
self.save_sname_schapter()
return super().load_item()
def setImageItem(self, url, scramble): return { "src" : url, "scramble": scramble}
def set_image_item(self, image_url, image_path, image_name, image_scramble="False", image_type="Image"):
return { "image_url" : image_url, "image_path" : image_path, "image_name" : image_name, "image_scramble" : image_scramble , "image_type" : image_type}
# 图像链接处理
def parse_images(self):
images_item = []
icon_path = os.path.join(self.get_project_name(), "icons", self.get_name(), self.get_name()+".jpg")
images_item.append(self.set_image_item(image_url= self.get_icon() , image_path = icon_path , image_name=self.get_name()+".jpg", image_scramble="False", image_type="Icon"))
for url, name in zip(self.get_image_urls(), self.get_images()):
image_path = os.path.join(self.get_project_name(), "images", self.get_name(), self.get_chapter(), name)
images_item.append(self.set_image_item(image_url= url , image_path= image_path, image_name=name))
return images_item
class BooksLoader(BaseLoader):
def get_names(self): return self.get_output_value("names")
def get_urls(self): return self.get_output_value("urls")
+149
View File
@@ -0,0 +1,149 @@
# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from scrapy import signals
import random,logging
from pathlib import Path
from Comics.settings import PROXY_LIST
# File cache settings begin
import pickle,time
from scrapy.http.request import Request
from scrapy.spiders import Spider
from scrapy.utils.python import to_bytes, to_unicode
from w3lib.http import headers_dict_to_raw, headers_raw_to_dict
from scrapy.extensions.httpcache import FilesystemCacheStorage
from Comics.settings import HTTPCACHE_ALLOW_PREFIXS, HTTPCACHE_PROXY_DOMAINS
# File cache settings end
# useful for handling different item types with a single interface
logger = logging.getLogger(__name__)
class ProxyMiddleware(object):
def process_request(self, request, spider):
url = request.url
logging.debug(f"proxy url=== {url} {str(url).split('.')[-1]}")
domain = str(url).replace("https://", "").replace("http://", "").split("/")[0]
if str(url).split('.')[-1] not in HTTPCACHE_ALLOW_PREFIXS or domain in HTTPCACHE_PROXY_DOMAINS:
if len(PROXY_LIST) != 0:
request.meta["proxy"] = random.choice(PROXY_LIST)
else:
logging.debug(f"skip proxy... {url}")
################################################################
# HTTP/HTTPS文件缓存中间件
################################################################
class MyFilesystemCacheStorage(FilesystemCacheStorage):
def verify_next(self, url):
logging.debug(f"cache url=== {url} {str(url).split('.')[-1]}")
if str(url).split('.')[-1] not in HTTPCACHE_ALLOW_PREFIXS:
logging.debug(f"取消缓存 === {url}")
return False
else:
logging.debug(f"开始缓存 === {url}")
return True
def retrieve_response(self, spider: Spider, request: Request):
if self.verify_next(request._get_url()):
super().retrieve_response(spider, request)
def store_response(self, spider: Spider, request: Request, response):
if self.verify_next(request._get_url()):
super().store_response(spider, request, response)
class ComicsSpiderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
def process_start_requests(self, start_requests, spider):
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
class ComicsDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
+972
View File
@@ -0,0 +1,972 @@
import base64,hashlib,os,shutil,os.path,pathlib
import math,time,json,datetime,logging
import re,requests,time,xmlschema
from datetime import date
from Comics import settings
from opencc import OpenCC
from PIL import Image
from pathlib import Path
from zipfile import ZipFile
from Comics.settings import COMIC_INFO_XML_FILE,COMIC_INFO_XSD_FILE,OUTPUT_DIR,PROJECT_KEY
import yaml
from Comics._utils.loader import BaseLoader
from Comics._utils.loader import ComicLoader
from Comics._utils.loader import BooksLoader
from tinydb import TinyDB, Query
# 配置类
class Conf():
"""
# 读取yml文件配置
# @project 根据工程名读取配置 project.yml
# @key 读取key内的字典的数据(默认为空)
"""
#def init(self, project, key=None):
# data = None
# if project == None: project = "config"
# with open(os.path.join("Comics","spiders", project)+".yml") as f:
# data = yaml.load(f, Loader=yaml.FullLoader)
# if key != None and data != None:
# return data[key]
def get_config_value(self, project, key=None):
# 使用Path类来处理文件路径
config_path = Path(os.path.join("Comics","spiders", project)+".yml")
#Path("Comics") / "spiders" / (project + ".yml")
# 检查项目是否存在
if not config_path.is_file():
return None
# 打开文件并加载配置数据
try:
with config_path.open('r') as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
print(f"Error loading YAML file: {e}")
return None
# 检查key是否存在
if key is not None and key in data:
return data[key]
else:
return None
# 根据读取的配置数据导入到ComicLoader中
def base_data(self, project, item: BaseLoader, child_data='data', val=None):
item.project_name(project)
data = self.get_config_value(project, child_data)
for key, xpath_data in data.items():
if isinstance(xpath_data, str): xpath_data = {'xpath': xpath_data}
xpath = xpath_data.get('xpath', None)
index = xpath_data.get('index', None)
value = xpath_data.get('value', None) if val is None else val
sexec = xpath_data.get('sexec', None)
item.set_properties(name=key, value=value, xpath=xpath, index=index, sexec=sexec)
return item
def books(self, project, item: BooksLoader, child_data='books', val=None):
return self.base_data(project, item, child_data, val)
def comic(self, project, item: ComicLoader, child_data='data', val=None):
return self.base_data(project, item, child_data, val)
def parse_chapter(self,item: ComicLoader, value):
return self.comic(item.get_project_name(), item, "parse_chapter", value)
def parse_chapter_api(self, item: ComicLoader, value):
return self.comic(item.get_project_name(), item, "parse_chapter_api", value)
# 文件操作类
class fileUtils:
# 文件是否存在
@classmethod
def exists(cls, path): return os.path.exists(path)
# 文件路径拼接
@classmethod
def join(cls, path, *paths): return os.path.join(path, *paths);
# 文件夹名
@classmethod
def dirname(cls, path): return os.path.dirname(path);
# 文件名
@classmethod
def basename(cls, path): return os.path.basename(path);
# 保存文件
@classmethod
def write(cls,path,data,type='file'):
root_dir = os.path.dirname(path)
if not os.path.exists(root_dir): os.makedirs(root_dir)
with open(path,'w',encoding='utf-8') as fs:
if type == 'file':
fs.write(data)
elif type == 'json':
fs.write(json.dumps(data,ensure_ascii=False,indent=4))
elif type == 'yaml':
fs.write(yaml.dump(data, allow_unicode=True, default_flow_style=False, indent=4))
# 返回校验后的文件路径
@classmethod
def path(cls, file):
base_dir = os.path.dirname(file)
if not os.path.exists(base_dir): os.makedirs(base_dir)
return file
# 比较文件大小
@classmethod
def compare_size(cls, dst, file):
if cls.exists(dst) and cls.exists(file):
return os.stat(dst).st_size == os.stat(file).st_size
else:
return None
# 读取文件
@classmethod
def read(cls, file, type='file'):
if os.path.exists(file):
with open(file, "r", encoding="utf-8") as fs:
if type == "json":
return json.loads(fs.read())
elif type == 'yaml':
return yaml.safe_load(fs.read())
elif type == 'file':
return fs.read()
else:
return []
@classmethod
def get_file_md5(cls, file_path):
md5 = hashlib.md5()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
md5.update(chunk)
return md5.hexdigest()
@classmethod
def write_json(cls, path, data): cls.write(path, data , type="json")
@classmethod
def read_json(cls, file): return cls.read(file, type="json")
"""
图像编号 image-1.jpg
如:存在image.png 返回 image-1.png 反之 image.png
"""
@classmethod
def file_check(cls, file, result="file", count=0):
# 初始化临时文件名 文件信息 文件名列表
temp_file_name, files_data, files_name = [file, {}, []]
# 默认文件名不存在
if not cls.exists(temp_file_name) and temp_file_name == file: count = 1
while count or count == 0:
# 获取格式化文件名
temp_file_name = ComicPath().images_icon(file=file, count=count)
# 格式化文件名存在
if cls.exists(temp_file_name):
# 保存存在的文件名
files_name.append(temp_file_name)
file_size = os.path.getsize(temp_file_name)
# 保存文件名和大小数据
files_data[file_size] = {"name": temp_file_name, "size": file_size}
# 格式化文件名
# temp_file_name = ComicPath().images_icon(file=file, count=count)
count += 1
else:
# 检测是否有重复数据
# 提取重复并需删除的文件名
diff_names = {value["name"] for value in files_data.values()}
# 不存在则返回原文件名
if len(diff_names) == 0:
# 默认为空
if result == "size": return diff_names
# 原文件名
else: return file
for file_name in files_name:
if file_name not in diff_names:
logging.info(f"删除文件:{file_name}")
os.remove(file_name)
# 判断是否存在初始文件和多个文件名
if len(diff_names) == 1 and file not in diff_names:
for diff_file in diff_names:
logging.info(f"移动文件{diff_file}到 {file}")
shutil.move(diff_file, file)
if file in diff_names and len(diff_names) > 1:
move_file = ComicPath().images_icon(file=file, count=count)
logging.info(f"移动文件{file}到 {move_file}")
shutil.move(file, move_file)
cls.file_check(file=file,result=result,count=0)
# 去重后文件名数与存在的文件名数不存在则证明文件存在重复,重新运行本方法
if len(set(diff_names)) != len(set(files_name)): cls.file_check(file, result=result,count=0)
if result == "size":
return {value["size"] for value in files_data.values()}
else:
return temp_file_name
# 判断文件是否更新
@classmethod
def file_update(cls, old_file, new_file):
is_update = False
if os.path.exists(old_file): is_update = os.path.getsize(old_file) not in cls.file_check(new_file, result="size")
return is_update
# 判断是否需要更新封面
@classmethod
def update_icon(cls, image_path, save_path):
# 不存在则更新
if cls.file_update(image_path, save_path):
save_dir = os.path.dirname(save_path)
if not os.path.exists(save_dir): os.makedirs(save_dir)
logging.info(f"update icon ... {image_path} ===> {cls.file_check(save_path)}")
shutil.copyfile(image_path, cls.file_check(save_path))
# 公共工具类
class CommonUtils:
@classmethod
def parseExec(cls,data,exec):
if data !=None and exec != None:
dots = str(exec).split(".")
if not isinstance(data,dict): data = json.loads(data)
for dot in dots:
data = data.get(dot)
return data
@classmethod
def _validate_xml(cls,xml_file, xsd_file):
# 读取XSD文件
xsd = xmlschema.XMLSchema(xsd_file)
# 验证XML
is_valid = xsd.is_valid(xml_file)
if is_valid:
print("XML文件通过XSD验证成功!")
else:
print("XML文件未通过XSD验证。以下是验证错误信息:")
validation_errors = xsd.to_errors(xml_file)
for error in validation_errors:
print(error)
@classmethod
def validate_comicinfo_xml(cls, xml_file):
cls._validate_xml(xml_file, COMIC_INFO_XSD_FILE)
# 图片处理类
class imageUtils:
@classmethod
def descramble_images_by_dir(cls, chapter_dir):
if os.path.isfile(chapter_dir):
chapter_dir = os.path.dirname(chapter_dir)
scramble_count = 0
if os.path.exists(chapter_dir): #获取章节图片路径
while ComicPath.PREFIX_SCRAMBLE in os.listdir(chapter_dir):
for img in os.listdir(chapter_dir):
if img.startswith(ComicPath.PREFIX_SCRAMBLE):
imageUtils.encode_scramble_image(os.path.join(chapter_dir, img))
scramble_count += 1
logging.debug(f"{ComicPath.PREFIX_SCRAMBLE} {scramble_count}")
return scramble_count
@classmethod
def deScrambleImagesByPath(cls, img_path, img_save=None):
if os.path.basename(img_path).\
startswith(ComicPath.PREFIX_SCRAMBLE) and os.path.exists(img_path):
img_path = imageUtils.encode_scramble_image(img_path, img_save)
return img_path
@classmethod
def encodeImage(cls,str_en):
#print("en",str_en)
enc = base64.b64decode(str_en)
#print("解密:",enc)
m = hashlib.md5()
m.update(enc)
md5 = m.digest()
d = md5[-1]
#print(md5)
try:
blocks = d % 10 + 5
except:
blocks = 0 %10 + 5
#print("blocks=",blocks)
return blocks
@classmethod
def scrambleImage(cls,file_path):
#检测到未下载完的图像 直接返回None
if str(file_path).endswith(".downloads"):
os.remove(file_path)
return None
file_str = str(file_path).split("=")
#10_29.jpg
base_dir = file_str[0].replace("scramble","")
base_name = file_str[-1]
base_fn = base_name.split("_")
save_name = base_fn[1]
save_name_delesu = save_name.split(".")[0]
blocks = int(base_fn[0])
save_file_path = os.path.join(base_dir,save_name)
print("sva",save_file_path)
if os.path.exists(save_file_path):
print("图片已解密,已跳过:", save_file_path)
return None
image_su = str(file_path).split(".")[-1]
try:
img = Image.open(file_path)
except:
print(f"error Image: {file_path}")
width = img.width
height = img.height
#blocks = cls.encodeImage(enStr)
print("blocks=",blocks)
block_height = int(height / blocks)
block_width = int(width / blocks)
print("blockHeight=",block_height)
suffix = str(file_path).split(".")[-1]
split_path = os.path.join(base_dir,save_name_delesu+"split")
if image_su == "downloads":
return None
is_split = cls.splitimage(file_path,blocks,1,split_path)
if is_split != None:
cls.image_compose(split_path,blocks,1,save_file_path,block_height,width)
else:
if os.path.exists(split_path):
shutil.rmtree(split_path)
if os.path.exists(file_path):
shutil.move(file_path, save_file_path)
#完成后清空
return file_path
@classmethod
def splitimage(cls,src,rownum,colnum,dstpath):
img=Image.open(src)
w,h=img.size
if rownum<= h and colnum<=w:
s=os.path.split(src)
if dstpath=='':
dstpath = s[0]
if not os.path.exists(dstpath):
os.makedirs(dstpath)
fn=s[1].split('.')
basename=fn[0]
ext=fn[-1]
num=0
rowheight=h//rownum
colwidth=w//colnum
for r in range(rownum):
for c in range(colnum):
box=(c*colwidth,r*rowheight,(c+1)*colwidth,(r+1)*rowheight)
count_image = "{:0>3d}".format(num)
file_path = os.path.join(dstpath,str(count_image)+'.'+ext)
print("file_path=",file_path)
img.crop(box).save(file_path)
num=num+1
return "成功"
else:
print('不数!')
return None
@classmethod
def image_compose(cls,src,row,column,save_path,image_height,image_width):
image_size = image_height
#image_height = 376
#image_width = 720
images_format = ['.png','.jpg']
#image_names = [name for name in os.listdir(src) for item in images_format if
# os.path.splitext(name)[1] == item][::-1]
img_list=os.listdir(src)
img_list.sort()
img_list.sort(key=lambda x: int(x[:-4]))
##文件名按数字排序
img_nums=len(img_list)
image_names = []
for i in range(img_nums):
img_name=os.path.join(src,img_list[i])
image_names.append(img_name)
#使用倒序
image_names = image_names[::-1]
# 简单的对于参数的设定和实际图片集的大小进行数量判断
if len(image_names) < row * column:
raise ValueError("合成图片的参数和要求的数量不能匹配!")
to_image = Image.new('RGB', (column * image_width, row * image_height)) #创建一个新图
# 循环遍历,把每张图片按顺序粘贴到对应位置上
for y in range(1, row + 1):
for x in range(1, column + 1):
#1 * (row=1 -1) col=1 -1
image_path = image_names[column * (y - 1) + x - 1]
print("split_image=",image_path)
from_image = Image.open(image_path)
#保持原图片大小
#.resize(
# (image_size, image_size),Image.ANTIALIAS)
to_image.paste(from_image, ((x - 1) * image_size, (y - 1) * image_size))
from_image.close()
to_image.save(save_path)
print("图片合并完成:", save_path)
shutil.rmtree(src)
# 保存新图
@classmethod
def getScrambleImage(cls,path):
scramble_file_cache = cls.scrambleImage(path)
if scramble_file_cache != None and os.path.exists(scramble_file_cache): os.remove(scramble_file_cache)
@classmethod
def encode_scramble_image(cls, img_path, img_save=None):
if not os.path.exists(img_path):
return
image = Image.open(img_path)
w, h = image.size
#image.show()
file_str = str(img_path).split("=")
#10_29.jpg
base_fn = file_str[-1].split("_")
blocks = int(base_fn[0])
if img_save == None:
save_path = os.path.join(os.path.dirname(img_path),ComicPath.getFileScrambleImageSave(img_path))
else: save_path = img_save
# print(type(aid),type(img_name))
if blocks:
s = blocks # 随机值
# print(s)
l = h % s # 切割最后多余的值
box_list = []
hz = 0
for i in range(s):
c = math.floor(h / s)
g = i * c
hz += c
h2 = h - c * (i + 1) - l
if i == 0:
c += l;hz += l
else:
g += l
box_list.append((0, h2, w, h - g))
# print(box_list,len(box_list))
item_width = w
# box_list.reverse() #还原切图可以倒序列表
# print(box_list, len(box_list))
newh = 0
image_list = [image.crop(box) for box in box_list]
# print(box_list)
newimage = Image.new("RGB", (w, h))
for image in image_list:
# image.show()
b_w, b_h = image.size
newimage.paste(image, (0, newh))
newh += b_h
newimage.save(save_path)
logging.info(f"解密成功 {save_path}")
if os.path.exists(img_path):
os.remove(img_path)
logging.debug(f"remove {img_path}")
return save_path
# 压缩工具类
class CBZUtils:
@classmethod
def readDirsOrFiles(cls, dir, type):
data = []
files = os.listdir(dir)
for file in files:
path = os.path.join(dir, file)
if type == "files" and os.path.isfile(path):
data.append(path)
if type == "dirs" and os.path.isdir(path):
data.append(path)
return data
@classmethod
def zip_compression(cls, source_dir=None, target_file=None, remove=True):
target_dir = os.path.dirname(target_file)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
if not os.path.exists(target_file) and source_dir is not None:
with ZipFile(target_file, mode='w') as zf:
for path, dir_names, filenames in os.walk(source_dir):
path = Path(path)
arc_dir = path.relative_to(source_dir)
y = 0
for filename in filenames:
y = y + 1
print("打包中:" + str(y) + "/" + str(len(filenames)), os.path.join(source_dir, filename))
zf.write(path.joinpath(filename), arc_dir.joinpath(filename))
zf.close()
logging.info(f"打包完成:{target_file}")
@classmethod
def packComicChapterCBZ(cls, src_dir, dts_path, comic_info_images, remove=True):
if os.path.exists(src_dir):
dirs = os.listdir(src_dir)
for file in dirs:
if file.startswith(ComicPath.PREFIX_SCRAMBLE):
try:
imageUtils.deScrambleImagesByPath(os.path.join(src_dir,file))
except Exception as e:
print(f"删除 {file} 发生错误 {e},已跳过")
return False
cls.zip_compression(src_dir, dts_path)
time.sleep(0.1)
if remove: shutil.rmtree(src_dir)
# validation
return cls.cbz_validate(dts_path, comic_info_images)
@classmethod
def replaceZip(cls, filepath, unpack_dir=None):
if not cls.compareFileDate(filepath): return None
if unpack_dir == None:
unpack_dir = str(filepath).split(".")[0]
fz = ZipFile(filepath, 'r')
for file in fz.namelist():
if file.endswith(".jpg"):
data = fz.read(file)
if len(data) < 500 and os.path.exists(filepath):
os.remove(filepath)
print(f"数据不完整,已删除:{filepath}")
if cls.compareFileDate(filepath):
os.utime(filepath)
print(f"已更新文件时间 {filepath}")
if os.path.exists(unpack_dir):
shutil.rmtree(unpack_dir)
# 删除删除main.ftl文件
# delete_filename = ''
# if os.path.exists(delete_filename):
# os.remove(delete_filename)
# time.sleep(60)
# shutil.copy(文件的路径,另一个目录);拷贝main.ftl到准备压缩的目录下
# cls.zip_compression()
# 小于则运行
@classmethod
def compareFileDate(cls, filepath):
if os.path.exists(filepath):
ctime = os.path.getmtime(filepath)
str_ctime = datetime.fromtimestamp(int(ctime))
file_ctime = str(str_ctime.year) + "{:0>2d}".format(str_ctime.month) + "{:0>2d}".format(
str_ctime.day) + "{:0>2d}".format(str_ctime.hour)
c_ctime = 2023011603
else:
return False
if int(file_ctime) < c_ctime:
return True
return False
@classmethod
def zip_info(cls, path, filter=True):
result = None
try:
with ZipFile(path, "r") as zip_file:
result = zip_file.namelist()
if filter:
result.remove(COMIC_INFO_XML_FILE)
except Exception as e:
print(e)
return result
@classmethod
def cbz_validate(cls, zip_path, comic_info_images):
cbz_info = cls.zip_info(zip_path)
if len(cbz_info) == len(comic_info_images):
# logging.info(f"validating successfully === {zip_path}")
# ntfy.sendMsg(f"validating successfully === {zip_path}", alert=True)
ntfy.gotify(title="漫画下载完成", message=f"validating successfully === {zip_path}")
return True
else:
os.remove(zip_path)
ntfy.gotify(title="漫画校验失败", message=f"validating fail === {zip_path}, cbz_info={cbz_info},zip_info={comic_info_images}")
#ntfy.sendMsg(f"validating fail === {zip_path}, cbz_info={cbz_info},zip_info={comic_info_images}", alert=True)
return False
# 检测工具类
class checkUtils:
def read(self, item):
file = os.path.join(OUTPUT_DIR, ComicLoader(item=item).get_project_name(), "error_comics.json")
return fileUtils.read(file)
#
# 检测某一章节是否连续错误
def export_error(self, item):
if not self.is_error(item):
file = os.path.join(OUTPUT_DIR, ComicLoader(item=item).get_project_name(), "error_comics.json")
try:
error_comic = eval(self.read(item))
except:
error_comic = []
error_comic.append({ "name" : ComicPath.new_file_name(item['name']),
"chapter" : ComicPath.new_file_name(item['chapter']),
"date" : ComicPath().getYearMonthDay()})
fileUtils().save_file(file, json.dumps(error_comic))
def is_error(self, item):
try:
for error_c in eval(self.read(item)):
(name, chatper, date) = [error_c['name'], error_c['chapter'], error_c['date']]
if ComicPath.new_file_name(item['name']) == ComicPath.new_file_name(name) and ComicPath.new_file_name(item['chapter']) == ComicPath.new_file_name(chatper):
return True
else:
return False
except:
return False
# Comic路径类
class ComicPath:
ci, project, name, chapter = [ None, "", "", ""]
def __init__(self, item = None):
if item == None: return
else:
self.ci : ComicLoader = ComicLoader(item=item)
if self.ci.get_project_name() != None: self.project = self.fix_file_name(self.ci.get_project_name())
if self.ci.get_name() != None: self.name = self.fix_file_name(self.ci.get_name())
if self.ci.get_chapter() != None: self.chapter = self.fix_file_name(self.ci.get_chapter())
PREFIX_SCRAMBLE = "scramble="
MAPPING_IMAGE = "image"
MAPPING_COMIC_INFO = "comic_info"
MAPPING_CBZ_ICON = "cbz_icon"
MAPPING_DOWN_ICON = "down_icon"
MAPPING_DOWN_CACHE_ICON = "down_cache_icon"
MAPPING_ICON = "icon"
MAPPING_ICON_CACHE = "icon_cache"
MAPPING_CBZ = "cbz"
MAPPING_CBZ_DIR = "cbz_dir"
MAPPING_OLD_CBZ_DIR = "old_cbz_dir"
MAPPING_IMAGES_DIR = "images_dir"
MAPPING_COMIC_JSON = "comic_json"
def PATH_MAPPING(self):
if self.project == None or self.name == None or self.chapter == None: return None
return {
self.MAPPING_IMAGE: os.path.join(self.project, "images", self.name, self.chapter),
self.MAPPING_COMIC_INFO: os.path.join(self.project, "images", self.name, self.chapter),
self.MAPPING_CBZ_ICON: os.path.join(settings.CBZ_EXPORT_PATH, self.project, self.name, self.chapter+".jpg"),
self.MAPPING_DOWN_ICON: os.path.join(settings.IMAGES_STORE, self.project, "icons", self.name, self.name+".jpg"),
self.MAPPING_DOWN_CACHE_ICON: os.path.join(settings.IMAGES_STORE, self.project, "icons", ".cache", self.name+".jpg"),
self.MAPPING_ICON: os.path.join(self.project, "icons", self.name, self.name+".jpg"),
self.MAPPING_ICON_CACHE: os.path.join(self.project, "icons", ".cache", self.name+".jpg"),
self.MAPPING_CBZ: os.path.join(settings.CBZ_EXPORT_PATH, self.project, self.name, self.chapter+".CBZ"),
self.MAPPING_IMAGES_DIR: os.path.join(settings.IMAGES_STORE, self.project, "images", self.name, self.chapter),
self.MAPPING_COMIC_JSON: os.path.join(settings.IMAGES_STORE, self.project, "json", self.name, self.chapter+".json"),
self.MAPPING_CBZ_DIR: os.path.join(settings.CBZ_EXPORT_PATH, self.project, self.name),
self.MAPPING_OLD_CBZ_DIR: os.path.join(settings.OLD_CBZ_EXPORT_PATH, self.project, self.name)
}
def PATH_CBZ(self, result_type=MAPPING_CBZ): return self.file_path(result_type=result_type)
def getDirJosnComicChapter(self, result_type=MAPPING_COMIC_JSON): return self.file_path(result_type=result_type)
def file_path(self, result_type=MAPPING_IMAGE, file=None, convert=True, chapter=None):
if chapter != None: self.chapter = chapter
path = self.PATH_MAPPING().get(result_type, None)
if result_type == self.MAPPING_IMAGE and os.path.sep not in file:
return os.path.join(path, file)
if convert:
path = self.chinese_convert(path)
return path
@classmethod
def getYearMonthDay(cls):
today = date.today()
# 格式化为年-月-日
return today.strftime("%Y%m%d")
#@classmethod
#def getDirComicChapter(cls, result_type=): return cls.file_path(result_type=result_type)
@classmethod
def getFileScrambleImageName(cls,count,block,suffix=".jpg"): return cls.PREFIX_SCRAMBLE+str(block)+"_"+str(count)+suffix
@classmethod
def getFileScrambleImageSave(cls,file,relative=False, is_prefix=True):
file_name = str(file).split("_")[-1]
if relative:
file_name = os.path.basename(file_name)
if relative == "fullpath":
file_name = os.path.join(os.path.dirname(file), file_name)
if not is_prefix:
return file_name.split(".")[0]
else:
return file_name
#繁体中文转简体中文
@classmethod
def chinese_convert(cls, text,convert='t2s'): return OpenCC(convert).convert(str(text))
#处理成符合规定的文件名
@classmethod
def fix_file_name(cls, filename, replace=None):
if not isinstance(filename, str):
return filename
in_tab = r'[?*/\|.:><]'
str_replace = ""
if replace is not None:
str_replace = replace
filename = re.sub(in_tab, str_replace, filename)
count = 1
while True:
str_file = filename[0-count]
if str_file == " ":
count += 1
else:
filename = filename[0:len(filename)+1-count]
break
return filename
@classmethod
def new_file_name(cls, name): return cls.fix_file_name(cls.chinese_convert(name))
@classmethod
def images_icon(cls, file, count):
if count == 0: return file
name, suffix = os.path.splitext(file)
return name+"-"+str(count)+suffix
# 通知类
class ntfy:
@classmethod
def sendMsg(cls, msg,alert=False,sleep=None,error=None):
try:
print(f"#ntfy: {msg}")
if alert:
requests.post("https://ntfy.caiwenxiu.cn/PyComic",
data=msg.encode(encoding='utf-8'))
except:
print(f"#ntfy error: {msg}")
if sleep != None:
logging.info(f'等待{sleep}秒后进入下一阶段')
time.sleep(int(sleep))
if error != None:
print(f"#ntfy Error: {error}")
return False
else:
return True
@classmethod
def gotify(cls,title, message):
# 替换为您的 Gotify 服务器 URL
GOTIFY_URL = 'https://gotify.caiwenxiu.cn'
# 替换为您的 Gotify 应用程序令牌
GOTIFY_TOKEN = 'Aq3balsIgPjwD6D'
# 通知的标题和消息
#title = "Test Notification"
#message = "This is a test message from Python script."
# 要发送的通知数据
payload = {
'title': title,
'message': message,
'priority': 5
}
# 发送 POST 请求到 Gotify 服务器
response = requests.post(
f'{GOTIFY_URL}/message',
headers={'X-Gotify-Key': GOTIFY_TOKEN},
json=payload
)
# 检查响应状态码
if response.status_code == 200:
print("Notification sent successfully!")
else:
print(f"Failed to send notification. Status code: {response.status_code}")
class logger:
def log_image_download(self, image_path):
if image_path == "":
logging.info(f"icon file exists: IMAGE_STORE {image_path}")
else:
logging.info(f"file exists: IMAGE_STORE {image_path}")
def log_image_not_downloaded(self, image_path):
logging.info(f"file does not exist: IMAGE_STORE {image_path}")
class ItemIconUtils:
def __init__(self, path, icon_item=None):
if icon_item != None: self.icon_item = icon_item
else: self.icon_item = []
self.path = path
if path != None:
self.read_icon_json()
def add(self, name, path, size, md5, url=None):
self.icon_item.append({ 'name': name, 'path': path, 'size': size, 'md5':md5, 'url': url })
def toString(self):
return self.icon_item
def _get_values(self, key): return [ item.get(key) for item in self.icon_item ]
def get_names(self): return self._get_values("name")
def get_file_count_by_md5(self, md5):
md5_list = self._get_values("md5")
md5_count = md5_list.count(md5)
if md5_count > 1:
logging.info(f" {md5_list} == 存在md5重复文件")
return md5_count
def get_file_by_key_value(self, key, value):
list_item = []
for item in self.icon_item:
if item.get(key) == value: return list_item.append(item)
return list_item
def get_file_by_md5(self, md5):
list_item = []
for item in self.icon_item:
if item.get("md5") == md5 and self.get_file_count_by_md5(md5) == 1: list_item.append(item)
return list_item
def get_file_by_url(self, url): return self.get_file_by_key_value('url', url)
def update_file_by_md5_to_url(self, md5, url):
file_item = self.get_file_by_md5(md5)
if len(file_item) > 1 :
logging.info(f" {file_item} == 存在多个结果")
if len(file_item) == 1:
file_item = file_item[0]
self.icon_item.remove(file_item)
file_item['url'] = url
self.icon_item.append(file_item)
return self.write_icon_json()
def get_files_info(self, directory, filter_suffix=None):
for root, dirs, files in os.walk(directory):
for file in files:
name, suffix = os.path.splitext(file)
if suffix != filter_suffix:
file_path = os.path.join(root, file)
file_size = os.path.getsize(file_path)
self.add(name= name, path=file_path, size=file_size, md5=fileUtils.get_file_md5(file_path))
return self.toString()
def write_icon_json(self):
if self.path != None:
self.get_files_info(self.path, filter_suffix='.json')
fileUtils.write_json(os.path.join(self.path,"icons.json"), self.toString())
def read_icon_json(self):
if self.path != None: self.icon_item = fileUtils.read_json(os.path.join(self.path, "icons.json"))
return self.icon_item
class DBUtils:
def __init__(self, db_name, suffix="json"):
self.db = TinyDB(ComicPath().getDirConfDefault(db_name,suffix=suffix))
@classmethod
def init_db(cls,db_name):
return TinyDB(ComicPath.getDirConfDefault(db_name,suffix="json"))
@classmethod
def set(cls,name,progress,db_name):
if name == None or progress == None or db_name == None:
print("dbUtils set 数据为空")
return False
db = cls.init_db(db_name)
comic = Query()
if len(db.search(comic.name == name)) == 0: db.insert({"name":name,"progress":progress})
else: db.update({"progress":progress},comic.name== name)
msg = "失败"
if cls.query(name,progress,db_name): msg = "成功"
logger.debug(f"设置{msg}, name={name} value={progress} db={db_name}")
@classmethod
def query(cls,name,progress=None,db_name=None):
result = False
db = cls.init_db(db_name)
if db == None: return None
data = db.search(Query().name == name)
logger.debug(f"result query= {data}")
if progress != None:
try:
if len(db.search((Query().name == name) & (Query().progress == progress))) != 0: result = True
except Exception as e:
print(e)
return result
@classmethod
def remove(cls,name,db_name=None):
db = cls.init_db(db_name)
db.remove(Query().name == name)
class oldUtils:
def new_files(self, files, folder, suffix="CBZ", result_type="new"):
result_files = self.old_files(files=files, folder=folder, suffix=suffix, result_type=result_type)
new_files = []
if result_files == None:
if isinstance(files, str): new_files.append(ComicPath.chinese_convert(ComicPath.fix_file_name(files)))
else:
for file in files: new_files.append(ComicPath.chinese_convert(ComicPath.fix_file_name(file)))
return new_files
else: return result_files
def old_images(self,folder, suffix="jpg", result_type="old"):
return self.old_files(files=[], folder=folder, suffix=suffix, result_type=result_type)
def old_files(self, files, folder, suffix="CBZ", result_type="old"):
result = None
# 方法三:使用pathlib模块的iterdir方法获取文件夹下的所有文件和文件夹
# 如果只需要文件名而不是文件的绝对路径,可以使用name属性获取文件名
if os.path.exists(folder):
file_names = [f.name for f in pathlib.Path(folder).iterdir() if f.is_file()]
else:
return None
old_item = []
for file_name in file_names:
file_split = file_name.split(".")
file_suffix = file_split[-1]
file_prefix = file_split[0]
if file_suffix == suffix:
old_item.append(file_prefix)
new_item = []
if isinstance(files, str): new_item.append(ComicPath.chinese_convert(ComicPath.fix_file_name(files)))
else:
for file in files: new_item.append(ComicPath.chinese_convert(ComicPath.fix_file_name(file)))
only_in_new_item = [item for item in new_item if item not in old_item]
only_in_old_item = [item for item in old_item if item not in new_item]
in_new_item_and_old_item = [item for item in new_item if item in old_item]
logging.debug(f"只在new_item中: {only_in_new_item}")
logging.debug(f"只在old_item中: {only_in_old_item}")
logging.debug(f"在new_item和old_item中都有: {in_new_item_and_old_item}")
if result_type == "old": result = only_in_old_item
if result_type == "new": result = only_in_new_item
return result
def clean_old_files(self, files, folder, move_folder, suffix="CBZ"):
# 方法三:使用pathlib模块的iterdir方法获取文件夹下的所有文件和文件夹
# 如果只需要文件名而不是文件的绝对路径,可以使用name属性获取文件名
only_in_old_item = self.old_files(files=files, folder=folder, suffix=suffix)
def move_file():
"""移动文件
"""
if not os.path.exists(move_folder): os.makedirs(move_folder)
for old_file in only_in_old_item:
try:
suffixs = [ suffix, "jpg" ]
for suf in suffixs:
new_move_file = os.path.join(folder, old_file)+"."+suf
old_move_file = os.path.join(move_folder, old_file)+"."+suf
if os.path.exists(new_move_file):
shutil.move(new_move_file, old_move_file)
print(f"move old_file={new_move_file} --> {old_move_file}")
except:
print(f"Error: move old_file={new_move_file} --> {old_move_file}")
if only_in_old_item != None: move_file()