#!/usr/bin/python
# -*- coding: utf-8 -*-
import shutil
import requests
import re
import os
import commands
import multiprocessing as mup
from threading import Thread
import time
import threading
# 创建一个线程锁
threadLock = threading.Lock()
# 下载请求头,没有问题不要修改此内容
headers = {
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Mobile Safari/537.36',
'Referer': 'https://bestdori.com/',
'Host': 'bestdori.com'
}
# 立绘所在的网页根目录,默认为日服,如果下国服可以将下面url中的jp改成cn,其他服原理相同
url = 'https://bestdori.com/assets/jp/characters/resourceset/'
# 立绘id所属的标签信息,在全局变量的url所打开的网页的源代码中搜索id为res001001的立绘目录,
# 能找到所属标签为:<span data-v-4cb50f40="" class="m-l-xs">res001001</span>
# 那么此处应该填写data-v-4cb50f40
cgIDInfo = 'data-v-4cb50f40'
# 立绘id所在网页的源代码文件(html文件,可以只有包含立绘id的部分),程序使用正则表达式获取所有立绘的id
# 如果url中指定的区服有了改变,需要根据url的网页源代码重新提取相关内容填写到该文件中
# 不要改动该值,但需要改动该值对应的文件内容
htmlMsgFileName = 'htmlMsg.txt'
# 最大同时运行的线程数量
# 建议设置为2的n次方,如2,4,8,16,32,128,256
# 适当提高该值有助于提高下载速度,但不建议设置的太大,对网站的压力也许会很大
# 看不懂本说明可以不改此值
maxThread = 128
# 全局变量,计算当前共运行了几个线程
# 用户不可手动调整该值
nowthread = 0
# 立绘保存的根目录,默认为card,表示保存在程序所在目录的card文件夹下
# 用户可以自己设定此值,但如果想自定义子目录则需要修改源代码。
# 支持相对路径和绝对路径,相对路径如:card\\test,绝对路径如:D:\\card,路径最后都不要添加\\
saveRootDir = 'card'
# 特训前的立绘的图片的通用类型名
normal = 'card_normal.png'
# 特训前的立绘(只保留人物)的图片的通用类型名
trim_normal = 'trim_normal.png'
# 特训后的立绘的图片的通用类型名
special = 'card_after_training.png'
# 特训后的立绘(只保留人物)的图片的通用类型名
trim_special = 'trim_after_training.png'
# 需要下载哪些类型的立绘,默认下载全部类型,如果未来有其他类型也可在此添加
# 如:只需要下载特训后的立绘(不包括只留人物的),可以改为“downLoadType=[special]”,
# 如:需要下载特训前的所有立绘(包括只留人物的),可以改为“downLoadType=[normal,trim_normal]”
needDownLoadTypeNameList = [normal, special]
# 全局变量,写字符串到日志文件用,初始随便赋值
logFileObj = 0
# 错误日志文件名
errorLogFileName = 'error.txt'
# 下载失败列表,用于单独下载
downFailedList = []
def downloadOnePicture(cgId, pictureTypeName):
"""
从指定url中下载指定类型的立绘CG到本地目录,
url由固定前缀+立绘id+立绘所属图片类型名称三个要素决定。
:param cgId:立绘的id,如res0010001
:param pictureTypeName:某类图片的通用名称,如特训前卡片为'card_normal.png'
:return:void
"""
global nowthread, logFileObj
global saveRootDir, url
# 拼接完整的url,拼接规则来自网页下载任意一张图片后查看下载地址得到
fullUrl = url + cgId + '_rip/' + pictureTypeName
try:
print('now thread num:' + str(nowthread) + ' start get:' + fullUrl)
# 发起get请求,获得图片
data = requests.get(fullUrl, headers=headers)
# 获得图片存储路径
savePath = saveRootDir + '\\' + cgId[0:6] + '\\'
# 图片命名规则:该图片所属的目录名(也叫立绘ID)+图片所属的类型(如特训前,特训后等)
pictureFullName = cgId + '_' + pictureTypeName
# print 'need save path:' + savePath + pictureFullName
# print 'save Name:'+pictureFullName
# 如果获取到的图片大小小于100kb,则不保存到本地,否则将内存中的图片保存为文件
if len(data.content) > 100000:
# 调用公共方法open完成文件写入
# 'wb'参数:以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
with open(savePath + pictureFullName, 'wb') as file:
file.write(data.content)
file.close()
print('save end, path:' + savePath + pictureFullName)
else:
print('less 100kb,don\'t save:' + savePath + pictureFullName)
except Exception as e:
errorMsg = 'pass this pic:' + fullUrl + 'because of error:\n' + str(e)
print(errorMsg)
downFailedList.append(cgId)
logFileObj.write(errorMsg)
logFileObj.flush()
finally:
# 使用线程锁保证下面的代码一并运行
threadLock.acquire()
# 执行完成后需要减少线程计数
nowthread -= 1
# 打印日志时展示现在还在运行的线程数量
print('now thread num:' + str(nowthread) + ' end get:' + fullUrl)
# 释放线程锁
threadLock.release()
def startDownLoadOneTypeAllPictureByMutThread(cgIdList, pictureTypeName, maxThread):
"""
发起对某个指定类型的全部立绘图片的下载,
采取多线程下载的方式,减少每次下载时等待服务器响应时间较长的问题。
:param cgIdList:从网页上获取到的立绘存储目录名列表,例如res001001,res001002等
:param pictureTypeName:立绘所属类型的通用名称,如特训后的图片都叫'card_after_training.png'
:param maxThread:可以最多同时运行的线程数量
:return:void
"""
global nowthread, logFileObj
# 循环获取全部立绘的id
for id in cgIdList:
# 实时统计,在启动新线程前将已用线程数+1(相当于占坑)
nowthread += 1
try:
# 无限循环,检查当前正在运行的线程数量是否达到设定的上限
# 没有达到则启动新的线程,执行一张立绘图片的下载任务,启动新线程成功后跳出无限循环,否则持续循环下去,直到能够运行新线程位为止
while 1:
# 线程暂停0.01秒,防止执行过快导致的各类问题如崩溃
time.sleep(0.01)
# 一旦发现有空余则开启下载,否则无限循环等待
if 0 <= nowthread <= maxThread:
# 立刻启动新线程,调用真正的下载方法,传入图片所属id和图片类型,完整下载
Thread(target=downloadOnePicture, args=(id, pictureTypeName)).start()
# 能启动新线程则强制跳出无限循环
break;
# else:
# if threadLock.acquire(True): # 2、获取锁状态,一个线程有锁时,别的线程只能在外面等着
# print("it's have " + str(self._nowthread) + " thread in running")
# # 注意,上面的nowthread的输出值可能为if表达式成功时的值,这是因为线程运行的随机性,因为在判断if时的该值和在print时的该值可能因为线程运行缘故导致不同
# # 这里的nowthread值仅能提示在print这一瞬间有几个线程还在运行,没有结束
# # print需要上线程锁,否则可能会和线程函数的print产生冲突导致输出错乱()即一个print执行到一半,另一个print开始执行,结果是两个print的输出值混在一起
# threadLock.release() # 3、释放锁
except Exception as e:
errorMsg = 'start new thread to downLoad error:\n' + str(e)
print(errorMsg)
logFileObj.write(errorMsg)
logFileObj.flush()
# 发生exception则释放已经占用的线程数量
nowthread -= 1
# 所有立绘都加入下载队列后,主进程在此无限循环等待,直到所有线程都完成后主进程才继续,防止出现主进程已完成但某个线程未完成的情况
print('\nwaitting picture type:"' + pictureTypeName + '" all download jobs over......')
while 1:
time.sleep(0.1)
if nowthread <= 0:
break
print('\npicture type:"' + pictureTypeName + '" all download jobs end')
def autoCreateDir(cgIdList):
"""
在电脑上建立目录,用于存放立绘图片。
目录建立方式是一个根目录,一组子目录,一个队员的所有立绘会放在同一个子目录下,35个队员会有35个子目录,
如果两组立绘的id为res001001,res001002,那么本程序会认为他们属于同一个队员的立绘,会生成res001目录用于存放这两组立绘
:param cgIdList:从网页上获取到的立绘存储目录名列表,例如res001001,res001002等
:return:void
"""
global saveRootDir, logFileObj
# 只判断最后一级目录是否存在
if os.path.isdir(saveRootDir):
# 清除已经创建的目录
print('root dir exists,start recreate')
try:
shutil.rmtree(path=saveRootDir)
print('delete success.')
except Exception as e:
errorMsg1 = 'delete root dir "' + saveRootDir + '" error:' + str(e)
errorMsg2 = 'please delete "' + saveRootDir + '" by yourself and restart this program'
print(errorMsg1)
print(errorMsg2)
logFileObj.write(errorMsg1)
logFileObj.write(errorMsg2)
logFileObj.flush()
# 重建根目录
os.mkdir(saveRootDir)
# 重建立绘子目录,根据35个队员的id创建
for cgID in cgIdList:
# “cgID[0:6]”截取前段半部分的信息作为一个目录,比如一组立绘的CG id为res001001,那么截取后得到res001,
# 这样所有001号队员的立绘都能放在一个文件夹里,这样会生成35个子文件夹(如果之后不再增加队员数量)
cgSaveFullPath = saveRootDir + '\\' + cgID[0:6] + '\\'
if not os.path.isdir(cgSaveFullPath): ##不用加引号,如果是多级目录,只判断最后一级目录是否存在
print('dir not exists,start create:' + cgSaveFullPath)
# 只能创建单级目录,用这个命令创建级联的会报OSError错误
os.mkdir(cgSaveFullPath)
pass
def getCgIdList(htmlMsgFile):
"""
从本地文件中获取所有立绘的id,组成一个列表返回
:param htmlMsgFile:待读取的文件
:return:完整的立绘id列表
"""
# 一次性读取整个文件
html = open(htmlMsgFile).read()
# 使用正则表达式匹配指定标签内容,提取立绘id
# 使用浏览器访问全局变量的url,然后查看网页源代码,搜索任意一个立绘id,可得到标签模板,根据标签模板生成正则表达式
# findall表示搜索所有匹配的内容,返回的是一个列表
list = re.findall('<span ' + cgIDInfo + '="" class="m-l-xs">(res\d*?)</span>', html)
print(list)
return list
if __name__ == '__main__':
os.system('pause')
# 获得程序开始时的时间
start = time.time()
# 重建一个新文件
logFileObj = open(errorLogFileName, 'wb')
logFileObj.truncate()
logFileObj.close()
# 获得文件追加操作对象
logFileObj = open(errorLogFileName, 'a')
# 获得所有立绘的id
cdIdList = getCgIdList(htmlMsgFileName)
# 生成本地存储目录
autoCreateDir(cdIdList)
# cpu_Count=os.cpu_count()
# print("本机为", cpu_Count, "核 CPU")
# p = mup.Pool(4) # 根据自己电脑的核数选择最多开几个进程
# for picType in (normal,trim_normal,special,trim_special):
# # 多进程执行任务
# p.apply_async(getMutTypePicture, args=(li,picType,nowthread,maxThread))
# 循环下载所有指定类型的图片,如:想只下载特训后的图片,则修改循环范围变量的内容
for picTypeName in needDownLoadTypeNameList:
startDownLoadOneTypeAllPictureByMutThread(cdIdList, picTypeName, maxThread)
# p.close() # 禁止再创建新的子进程进程
# p.join() # 等待所有子进程结束后主进程才能继续
print('all end')
# 获得程序完成时间
end = time.time()
# 显示本程序运行耗时
print("down failed:", set(downFailedList))
print("this running use time: %f s" % (end - start))
print('press any key to continue...')
logFileObj.close()
os.system('pause')