parent
a085592b53
commit
92a1eb558a
|
@ -158,5 +158,5 @@ cython_debug/
|
|||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
.idea/
|
||||
|
||||
|
|
|
@ -0,0 +1,981 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import requests as req
|
||||
from lxml import etree
|
||||
from tkinter import Tk, filedialog
|
||||
from ebooklib import epub
|
||||
from tqdm import tqdm
|
||||
from bs4 import BeautifulSoup
|
||||
import json, time, random, os, platform, shutil
|
||||
import concurrent.futures
|
||||
|
||||
CODE = [[58344, 58715], [58345, 58716]]
|
||||
charset = json.loads(
|
||||
'[["D","在","主","特","家","军","然","表","场","4","要","只","v","和","?","6","别","还","g","现","儿","岁","?","?","此","象","月","3","出","战","工","相","o","男","直","失","世","F","都","平","文","什","V","O","将","真","T","那","当","?","会","立","些","u","是","十","张","学","气","大","爱","两","命","全","后","东","性","通","被","1","它","乐","接","而","感","车","山","公","了","常","以","何","可","话","先","p","i","叫","轻","M","士","w","着","变","尔","快","l","个","说","少","色","里","安","花","远","7","难","师","放","t","报","认","面","道","S","?","克","地","度","I","好","机","U","民","写","把","万","同","水","新","没","书","电","吃","像","斯","5","为","y","白","几","日","教","看","但","第","加","候","作","上","拉","住","有","法","r","事","应","位","利","你","声","身","国","问","马","女","他","Y","比","父","x","A","H","N","s","X","边","美","对","所","金","活","回","意","到","z","从","j","知","又","内","因","点","Q","三","定","8","R","b","正","或","夫","向","德","听","更","?","得","告","并","本","q","过","记","L","让","打","f","人","就","者","去","原","满","体","做","经","K","走","如","孩","c","G","给","使","物","?","最","笑","部","?","员","等","受","k","行","一","条","果","动","光","门","头","见","往","自","解","成","处","天","能","于","名","其","发","总","母","的","死","手","入","路","进","心","来","h","时","力","多","开","已","许","d","至","由","很","界","n","小","与","Z","想","代","么","分","生","口","再","妈","望","次","西","风","种","带","J","?","实","情","才","这","?","E","我","神","格","长","觉","间","年","眼","无","不","亲","关","结","0","友","信","下","却","重","己","老","2","音","字","m","呢","明","之","前","高","P","B","目","太","e","9","起","稜","她","也","W","用","方","子","英","每","理","便","四","数","期","中","C","外","样","a","海","们","任"],["s","?","作","口","在","他","能","并","B","士","4","U","克","才","正","们","字","声","高","全","尔","活","者","动","其","主","报","多","望","放","h","w","次","年","?","中","3","特","于","十","入","要","男","同","G","面","分","方","K","什","再","教","本","己","结","1","等","世","N","?","说","g","u","期","Z","外","美","M","行","给","9","文","将","两","许","张","友","0","英","应","向","像","此","白","安","少","何","打","气","常","定","间","花","见","孩","它","直","风","数","使","道","第","水","已","女","山","解","d","P","的","通","关","性","叫","儿","L","妈","问","回","神","来","S","","四","望","前","国","些","O","v","l","A","心","平","自","无","军","光","代","是","好","却","c","得","种","就","意","先","立","z","子","过","Y","j","表","","么","所","接","了","名","金","受","J","满","眼","没","部","那","m","每","车","度","可","R","斯","经","现","门","明","V","如","走","命","y","6","E","战","很","上","f","月","西","7","长","夫","想","话","变","海","机","x","到","W","一","成","生","信","笑","但","父","开","内","东","马","日","小","而","后","带","以","三","几","为","认","X","死","员","目","位","之","学","远","人","音","呢","我","q","乐","象","重","对","个","被","别","F","也","书","稜","D","写","还","因","家","发","时","i","或","住","德","当","o","l","比","觉","然","吃","去","公","a","老","亲","情","体","太","b","万","C","电","理","?","失","力","更","拉","物","着","原","她","工","实","色","感","记","看","出","相","路","大","你","候","2","和","?","与","p","样","新","只","便","最","不","进","T","r","做","格","母","总","爱","身","师","轻","知","往","加","从","?","天","e","H","?","听","场","由","快","边","让","把","任","8","条","头","事","至","起","点","真","手","这","难","都","界","用","法","n","处","下","又","Q","告","地","5","k","t","岁","有","会","果","利","民"]]')
|
||||
headers_lib = [
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36'},
|
||||
{'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0'},
|
||||
{
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47'}
|
||||
]
|
||||
|
||||
headers = headers_lib[random.randint(0, len(headers_lib) - 1)]
|
||||
|
||||
|
||||
def get_cookie(zj, t=0):
|
||||
global cookie
|
||||
bas = 1000000000000000000
|
||||
if t == 0:
|
||||
for i in range(random.randint(bas * 6, bas * 8), bas * 9):
|
||||
time.sleep(random.randint(50, 150) / 1000)
|
||||
cookie = 'novel_web_id=' + str(i)
|
||||
if len(down_text(zj, 2)) > 200:
|
||||
with open(cookie_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(cookie, f)
|
||||
return 's'
|
||||
else:
|
||||
cookie = t
|
||||
if len(down_text(zj, 2)) > 200:
|
||||
return 's'
|
||||
else:
|
||||
return 'err'
|
||||
|
||||
|
||||
def down_zj(it):
|
||||
an = {}
|
||||
ele = etree.HTML(req.get('https://fanqienovel.com/page/' + str(it), headers=headers).text)
|
||||
a = ele.xpath('//div[@class="chapter"]/div/a')
|
||||
for i in range(len(a)):
|
||||
an[a[i].text] = a[i].xpath('@href')[0].split('/')[-1]
|
||||
if ele.xpath('//h1/text()') == []:
|
||||
return ['err', 0, 0]
|
||||
return [ele.xpath('//h1/text()')[0], an, ele.xpath('//span[@class="info-label-yellow"]/text()')]
|
||||
|
||||
|
||||
def interpreter(uni, mode):
|
||||
bias = uni - CODE[mode][0]
|
||||
if bias < 0 or bias >= len(charset[mode]) or charset[mode][bias] == '?':
|
||||
return chr(uni)
|
||||
return charset[mode][bias]
|
||||
|
||||
|
||||
def str_interpreter(n, mode):
|
||||
s = ''
|
||||
for i in range(len(n)):
|
||||
uni = ord(n[i])
|
||||
if CODE[mode][0] <= uni <= CODE[mode][1]:
|
||||
s += interpreter(uni, mode)
|
||||
else:
|
||||
s += n[i]
|
||||
return s
|
||||
|
||||
|
||||
def down_text(it, mod=1):
|
||||
global cookie
|
||||
headers2 = headers
|
||||
headers2['cookie'] = cookie
|
||||
f = False
|
||||
while True:
|
||||
try:
|
||||
res = req.get('https://fanqienovel.com/reader/' + str(it), headers=headers2)
|
||||
n = '\n'.join(etree.HTML(res.text).xpath('//div[@class="muye-reader-content noselect"]//p/text()'))
|
||||
break
|
||||
except:
|
||||
if mod == 2:
|
||||
return ('err')
|
||||
f = True
|
||||
time.sleep(0.4)
|
||||
if mod == 1:
|
||||
s = str_interpreter(n, 0)
|
||||
else:
|
||||
s = n
|
||||
try:
|
||||
if mod == 1:
|
||||
return s, f
|
||||
else:
|
||||
return s
|
||||
except:
|
||||
s = s[6:]
|
||||
tmp = 1
|
||||
a = ''
|
||||
for i in s:
|
||||
if i == '<':
|
||||
tmp += 1
|
||||
elif i == '>':
|
||||
tmp -= 1
|
||||
elif tmp == 0:
|
||||
a += i
|
||||
elif tmp == 1 and i == 'p':
|
||||
a = (a + '\n').replace('\n\n', '\n')
|
||||
return a, f
|
||||
|
||||
|
||||
def down_text_old(it, mod=1):
|
||||
global cookie
|
||||
headers2 = headers
|
||||
headers2['cookie'] = cookie
|
||||
f = False
|
||||
while True:
|
||||
try:
|
||||
res = req.get('https://fanqienovel.com/api/reader/full?itemId=' + str(it), headers=headers2)
|
||||
n = json.loads(res.text)['data']['chapterData']['content']
|
||||
break
|
||||
except:
|
||||
if mod == 2:
|
||||
return ('err')
|
||||
f = True
|
||||
time.sleep(0.4)
|
||||
if mod == 1:
|
||||
s = str_interpreter(n, 0)
|
||||
else:
|
||||
s = n
|
||||
try:
|
||||
if mod == 1:
|
||||
return '\n'.join(etree.HTML(s).xpath('//p/text()')), f
|
||||
else:
|
||||
return '\n'.join(etree.HTML(s).xpath('//p/text()'))
|
||||
except:
|
||||
s = s[6:]
|
||||
tmp = 1
|
||||
a = ''
|
||||
for i in s:
|
||||
if i == '<':
|
||||
tmp += 1
|
||||
elif i == '>':
|
||||
tmp -= 1
|
||||
elif tmp == 0:
|
||||
a += i
|
||||
elif tmp == 1 and i == 'p':
|
||||
a = (a + '\n').replace('\n\n', '\n')
|
||||
return a, f
|
||||
|
||||
|
||||
def sanitize_filename(filename):
|
||||
illegal_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
|
||||
illegal_chars_rep = ['<', '>', ':', '"', '/', '\', '|', '?', '*']
|
||||
for i in range(len(illegal_chars)):
|
||||
filename = filename.replace(illegal_chars[i], illegal_chars_rep[i])
|
||||
return filename
|
||||
|
||||
|
||||
def download_chapter(chapter_title, chapter_id, ozj):
|
||||
global zj, cs, book_json_path
|
||||
f = False
|
||||
if chapter_title in ozj:
|
||||
try:
|
||||
int(ozj[chapter_title])
|
||||
f = True
|
||||
except:
|
||||
zj[chapter_title] = ozj[chapter_title]
|
||||
else:
|
||||
f = True
|
||||
if f:
|
||||
tqdm.write(f'下载 {chapter_title}')
|
||||
zj[chapter_title], st = down_text(chapter_id)
|
||||
time.sleep(random.randint(config['delay'][0], config['delay'][1]) / 1000)
|
||||
if st:
|
||||
tcs += 1
|
||||
if tcs > 7:
|
||||
tcs = 0
|
||||
get_cookie(tzj)
|
||||
cs += 1
|
||||
if cs >= 5:
|
||||
cs = 0
|
||||
with open(book_json_path, 'w', encoding='UTF-8') as json_file:
|
||||
json.dump(zj, json_file, ensure_ascii=False)
|
||||
return chapter_title
|
||||
|
||||
|
||||
def down_book(it):
|
||||
global zj, cs, book_json_path
|
||||
name, zj, zt = down_zj(it)
|
||||
if name == 'err':
|
||||
return 'err'
|
||||
zt = zt[0]
|
||||
|
||||
safe_name = sanitize_filename(name)
|
||||
book_dir = os.path.join(script_dir, safe_name)
|
||||
print('\n开始下载《%s》,状态‘%s’' % (name, zt))
|
||||
book_json_path = os.path.join(bookstore_dir, safe_name + '.json')
|
||||
|
||||
if os.path.exists(book_json_path):
|
||||
with open(book_json_path, 'r', encoding='UTF-8') as json_file:
|
||||
ozj = json.load(json_file)
|
||||
else:
|
||||
ozj = {}
|
||||
|
||||
cs = 0
|
||||
tcs = 0
|
||||
tasks = []
|
||||
# 使用配置的线程数创建线程池
|
||||
if 'xc' in config:
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=config['xc'])
|
||||
else:
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
pbar = tqdm(total=len(zj))
|
||||
for chapter_title, chapter_id in zj.items():
|
||||
tasks.append(executor.submit(download_chapter, chapter_title, chapter_id, ozj))
|
||||
for future in concurrent.futures.as_completed(tasks):
|
||||
chapter_title = future.result()
|
||||
pbar.update(1)
|
||||
|
||||
with open(book_json_path, 'w', encoding='UTF-8') as json_file:
|
||||
json.dump(zj, json_file, ensure_ascii=False)
|
||||
|
||||
fg = '\n' + config['kgf'] * config['kg']
|
||||
if config['save_mode'] == 1:
|
||||
text_file_path = os.path.join(config['save_path'], safe_name + '.txt')
|
||||
with open(text_file_path, 'w', encoding='UTF-8') as text_file:
|
||||
for chapter_title in zj:
|
||||
text_file.write('\n' + chapter_title + fg)
|
||||
if config['kg'] == 0:
|
||||
text_file.write(zj[chapter_title] + '\n')
|
||||
else:
|
||||
text_file.write(zj[chapter_title].replace('\n', fg) + '\n')
|
||||
elif config['save_mode'] == 2:
|
||||
text_dir_path = os.path.join(config['save_path'], safe_name)
|
||||
if not os.path.exists(text_dir_path):
|
||||
os.makedirs(text_dir_path)
|
||||
for chapter_title in zj:
|
||||
text_file_path = os.path.join(text_dir_path, sanitize_filename(chapter_title) + '.txt')
|
||||
with open(text_file_path, 'w', encoding='UTF-8') as text_file:
|
||||
text_file.write(fg)
|
||||
if config['kg'] == 0:
|
||||
text_file.write(zj[chapter_title] + '\n')
|
||||
else:
|
||||
text_file.write(zj[chapter_title].replace('\n', fg) + '\n')
|
||||
else:
|
||||
print('保存模式出错!')
|
||||
|
||||
return zt
|
||||
|
||||
|
||||
def download_chapter_epub(chapter_title, chapter_id, ozj):
|
||||
global zj, cs, book_json_path
|
||||
f = False
|
||||
if chapter_title in ozj:
|
||||
try:
|
||||
int(ozj[chapter_title])
|
||||
f = True
|
||||
except:
|
||||
zj[chapter_title] = ozj[chapter_title]
|
||||
else:
|
||||
f = True
|
||||
if f:
|
||||
tqdm.write(f'下载 {chapter_title}')
|
||||
zj[chapter_title], st = down_text(chapter_id)
|
||||
time.sleep(random.randint(config['delay'][0], config['delay'][1]) / 1000)
|
||||
if st:
|
||||
tcs += 1
|
||||
if tcs > 7:
|
||||
tcs = 0
|
||||
get_cookie(tzj)
|
||||
cs += 1
|
||||
if cs >= 5:
|
||||
cs = 0
|
||||
with open(book_json_path, 'w', encoding='UTF-8') as json_file:
|
||||
json.dump(zj, json_file, ensure_ascii=False)
|
||||
return chapter_title, zj[chapter_title]
|
||||
|
||||
|
||||
def down_book_epub(it):
|
||||
global zj, cs, book_json_path
|
||||
name, zj, zt = down_zj(it)
|
||||
if name == 'err':
|
||||
return 'err'
|
||||
zt = zt[0]
|
||||
|
||||
safe_name = sanitize_filename(name)
|
||||
book_dir = os.path.join(script_dir, safe_name)
|
||||
|
||||
print('\n开始下载《%s》,状态‘%s’' % (name, zt))
|
||||
book_json_path = os.path.join(bookstore_dir, safe_name + '.json')
|
||||
|
||||
existing_json_content = {}
|
||||
if os.path.exists(book_json_path):
|
||||
with open(book_json_path, 'r', encoding='UTF-8') as json_file:
|
||||
existing_json_content = json.load(json_file)
|
||||
|
||||
# 获取作者信息
|
||||
url = f'https://fanqienovel.com/page/{it}'
|
||||
response = req.get(url)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
script_tag = soup.find('script', type="application/ld+json")
|
||||
author_name = None
|
||||
if script_tag:
|
||||
json_data = script_tag.string
|
||||
import json
|
||||
data = json.loads(json_data)
|
||||
if 'author' in data:
|
||||
author_name = data['author'][0]['name']
|
||||
|
||||
book = epub.EpubBook()
|
||||
|
||||
book.set_title(name)
|
||||
if author_name:
|
||||
book.add_author(author_name)
|
||||
book.set_language('zh')
|
||||
|
||||
# 查找小说封面图片
|
||||
url = f'https://fanqienovel.com/page/{it}'
|
||||
response = req.get(url)
|
||||
if response.status_code == 200:
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
json_ld_script = soup.find('script', {'type': 'application/ld+json'})
|
||||
if json_ld_script:
|
||||
json_content = json_ld_script.string
|
||||
data = json.loads(json_content)
|
||||
if 'image' in data:
|
||||
img_url = data['image'][0]
|
||||
img_response = req.get(img_url)
|
||||
if img_response.status_code == 200:
|
||||
# 将图片添加到 EPUB 书的封面
|
||||
book.set_cover('image.jpg', img_response.content)
|
||||
|
||||
# 创建一个包含图片的页面并添加到书的开头,设置图片占满页面
|
||||
image_content = f'<div style="width:100%;height:100%;display:flex;justify-content:center;align-items:center;"><img src="image.jpg" style="width:100%;height:100%;object-fit:cover;" /></div>'
|
||||
image_page = epub.EpubHtml(title='封面图片', file_name='cover_image.xhtml', content=image_content)
|
||||
book.add_item(image_page)
|
||||
book.spine.insert(0, image_page)
|
||||
|
||||
# 创建目录列表
|
||||
toc = []
|
||||
|
||||
cs = 0
|
||||
tcs = 0
|
||||
tasks = []
|
||||
# 使用配置的线程数创建线程池
|
||||
if 'xc' in config:
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=config['xc'])
|
||||
else:
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
pbar = tqdm(total=len(zj))
|
||||
for chapter_title, chapter_id in zj.items():
|
||||
tasks.append(executor.submit(download_chapter_epub, chapter_title, chapter_id, existing_json_content))
|
||||
chapters = []
|
||||
for future in concurrent.futures.as_completed(tasks):
|
||||
chapter_title, chapter_content = future.result()
|
||||
pbar.update(1)
|
||||
chapters.append((chapter_title, chapter_content))
|
||||
|
||||
# 按章节标题排序
|
||||
chapters.sort(key=lambda x: list(zj.keys()).index(x[0]))
|
||||
|
||||
for chapter_title, chapter_content in chapters:
|
||||
# 创建章节,确保内容换行并添加段首空格符
|
||||
chapter_content = chapter_content.replace('\n', f'\n{config["kgf"] * config["kg"]}')
|
||||
chapter_content = f'{config["kgf"] * config["kg"]}' + chapter_content # 添加首段的段首空格符
|
||||
chapter_content = chapter_content.replace('\n', '<br/>')
|
||||
chapter = epub.EpubHtml(title=chapter_title, file_name=f'{chapter_title}.xhtml',
|
||||
content=f'<h1>{chapter_title}</h1><p>{chapter_content}</p>')
|
||||
book.add_item(chapter)
|
||||
toc.append(chapter)
|
||||
book.spine.append(chapter)
|
||||
|
||||
# 设置目录
|
||||
book.toc = toc
|
||||
# 添加目录文件
|
||||
book.add_item(epub.EpubNcx())
|
||||
# 编写 EPUB 文件
|
||||
epub.write_epub(os.path.join(config['save_path'], f'{safe_name}.epub'), book, {})
|
||||
|
||||
return 's'
|
||||
|
||||
|
||||
def down_book_html(it):
|
||||
global zj, cs, book_json_path, book_dir
|
||||
name, zj, zt = down_zj(it)
|
||||
if name == 'err':
|
||||
return 'err'
|
||||
zt = zt[0]
|
||||
|
||||
safe_name = sanitize_filename(name)
|
||||
book_dir = os.path.join(script_dir, f"{safe_name}(html)")
|
||||
if not os.path.exists(book_dir):
|
||||
os.makedirs(book_dir)
|
||||
|
||||
print('\n开始下载《%s》,状态‘%s’' % (name, zt))
|
||||
book_json_path = os.path.join(bookstore_dir, safe_name + '.json')
|
||||
|
||||
existing_json_content = {}
|
||||
if os.path.exists(book_json_path):
|
||||
with open(book_json_path, 'r', encoding='UTF-8') as json_file:
|
||||
existing_json_content = json.load(json_file)
|
||||
|
||||
# 生成目录 HTML 文件内容,添加 CSS 样式和响应式设计的 meta 标签
|
||||
toc_content = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>目录</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>目录</h1>
|
||||
<ul>
|
||||
"""
|
||||
for chapter_title in zj:
|
||||
toc_content += f"<li><a href='{chapter_title}.html'>{chapter_title}</a></li>"
|
||||
toc_content += "</ul></body></html>"
|
||||
|
||||
# 将目录内容写入文件
|
||||
with open(os.path.join(book_dir, "index.html"), "w", encoding='UTF-8') as toc_file:
|
||||
toc_file.write(toc_content)
|
||||
|
||||
cs = 0
|
||||
tcs = 0
|
||||
tasks = []
|
||||
# 使用配置的线程数创建线程池
|
||||
if 'xc' in config:
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=config['xc'])
|
||||
else:
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
pbar = tqdm(total=len(zj))
|
||||
for chapter_title, chapter_id in zj.items():
|
||||
tasks.append(executor.submit(download_chapter_html, chapter_title, chapter_id, existing_json_content))
|
||||
for future in concurrent.futures.as_completed(tasks):
|
||||
chapter_title = future.result()
|
||||
pbar.update(1)
|
||||
|
||||
return 's'
|
||||
|
||||
|
||||
def download_chapter_html(chapter_title, chapter_id, existing_json_content):
|
||||
global zj, cs, book_json_path, book_dir
|
||||
f = False
|
||||
if chapter_title in existing_json_content:
|
||||
try:
|
||||
int(existing_json_content[chapter_title])
|
||||
f = True
|
||||
except:
|
||||
zj[chapter_title] = existing_json_content[chapter_title]
|
||||
else:
|
||||
f = True
|
||||
if f:
|
||||
tqdm.write(f'下载 {chapter_title}')
|
||||
chapter_content, _ = down_text(chapter_id)
|
||||
time.sleep(random.randint(config['delay'][0], config['delay'][1]) / 1000)
|
||||
cs += 1
|
||||
|
||||
# 每章都保存 JSON 文件
|
||||
existing_json_content[chapter_title] = chapter_content
|
||||
with open(book_json_path, 'w', encoding='UTF-8') as json_file:
|
||||
json.dump(existing_json_content, json_file, ensure_ascii=False)
|
||||
|
||||
# 生成章节 HTML 文件内容,添加 CSS 样式、返回顶部按钮和装饰元素,同时保留换行符
|
||||
formatted_content = chapter_content.replace('\n', '<br/>')
|
||||
next_chapter_button = ""
|
||||
if len(zj) > list(zj.keys()).index(chapter_title) + 1:
|
||||
next_chapter_key = list(zj.keys())[list(zj.keys()).index(chapter_title) + 1]
|
||||
next_chapter_button = f"<button onclick=\"location.href='{next_chapter_key}.html'\">下一章</button>"
|
||||
|
||||
chapter_html_content = f"""
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{chapter_title}</title>
|
||||
<style>
|
||||
body {{
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}}
|
||||
.left-side {{
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
}}
|
||||
.content {{
|
||||
flex: 3;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
}}
|
||||
.right-side {{
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
}}
|
||||
button {{
|
||||
background-color: #d3d3d3;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
#toggle-mode {{
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
body {{
|
||||
background-color: #333;
|
||||
}}
|
||||
.left-side,.right-side {{
|
||||
background-color: #444;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #222;
|
||||
color: white;
|
||||
}}
|
||||
button {{
|
||||
background-color: #555;
|
||||
color: white;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
let isDarkMode = false;
|
||||
document.getElementById('toggle-mode').addEventListener('click', function() {{
|
||||
isDarkMode =!isDarkMode;
|
||||
if (isDarkMode) {{
|
||||
document.body.classList.add('dark-mode');
|
||||
localStorage.setItem('mode', 'dark');
|
||||
}} else {{
|
||||
document.body.classList.remove('dark-mode');
|
||||
localStorage.setItem('mode', 'light');
|
||||
}}
|
||||
}});
|
||||
|
||||
// 检查本地存储以确定初始模式
|
||||
const savedMode = localStorage.getItem('mode');
|
||||
if (savedMode === 'dark') {{
|
||||
document.body.classList.add('dark-mode');
|
||||
isDarkMode = true;
|
||||
}}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="left-side"></div>
|
||||
<div class="content">
|
||||
<h1>{chapter_title}</h1>
|
||||
<p>{formatted_content}</p>
|
||||
<a href="#" id="back-to-top">返回顶部</a>
|
||||
</div>
|
||||
<div class="right-side"></div>
|
||||
<div style="text-align: center; position: fixed; bottom: 20px; width: 100%;">
|
||||
<button onclick="location.href='index.html'">目录</button>
|
||||
{next_chapter_button}
|
||||
<button onclick="backToTop()">返回顶部</button>
|
||||
<button id="toggle-mode">切换模式</button>
|
||||
</div>
|
||||
<script>
|
||||
// 当用户滚动页面时显示/隐藏返回顶部按钮
|
||||
window.onscroll = function() {{
|
||||
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {{
|
||||
document.getElementById("back-to-top").style.display = "block";
|
||||
}} else {{
|
||||
document.getElementById("back-to-top").style.display = "none";
|
||||
}}
|
||||
}};
|
||||
|
||||
// 当用户点击返回顶部按钮时,滚动页面到顶部
|
||||
function backToTop() {{
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 将章节内容写入文件
|
||||
with open(os.path.join(book_dir, f"{chapter_title}.html"), "w", encoding='UTF-8') as chapter_file:
|
||||
chapter_file.write(chapter_html_content)
|
||||
return chapter_title
|
||||
|
||||
|
||||
def down_book_latex(it):
|
||||
global zj, cs, book_json_path, book_dir
|
||||
name, zj, zt = down_zj(it)
|
||||
if name == 'err':
|
||||
return 'err'
|
||||
zt = zt[0]
|
||||
|
||||
safe_name = sanitize_filename(name)
|
||||
|
||||
print('\n开始下载《%s》,状态‘%s’' % (name, zt))
|
||||
book_json_path = os.path.join(bookstore_dir, safe_name + '.json')
|
||||
|
||||
existing_json_content = {}
|
||||
if os.path.exists(book_json_path):
|
||||
with open(book_json_path, 'r', encoding='UTF-8') as json_file:
|
||||
existing_json_content = json.load(json_file)
|
||||
|
||||
latex_content = ""
|
||||
cs = 0
|
||||
tcs = 0
|
||||
tasks = []
|
||||
# 使用配置的线程数创建线程池
|
||||
if 'xc' in config:
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=config['xc'])
|
||||
else:
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
pbar = tqdm(total=len(zj))
|
||||
for chapter_title, chapter_id in zj.items():
|
||||
tasks.append(executor.submit(download_chapter_latex, chapter_title, chapter_id, existing_json_content))
|
||||
for future in concurrent.futures.as_completed(tasks):
|
||||
chapter_title = future.result()
|
||||
pbar.update(1)
|
||||
|
||||
# 在脚本所在目录下输出 LaTeX 文件
|
||||
latex_file_path = os.path.join(script_dir, f'{safe_name}.tex')
|
||||
with open(latex_file_path, 'w', encoding='UTF-8') as latex_file:
|
||||
latex_file.write(latex_content)
|
||||
|
||||
return 's'
|
||||
|
||||
|
||||
def download_chapter_latex(chapter_title, chapter_id, existing_json_content):
|
||||
global zj, cs, book_json_path, book_dir
|
||||
f = False
|
||||
if chapter_title in existing_json_content:
|
||||
try:
|
||||
int(existing_json_content[chapter_title])
|
||||
f = True
|
||||
except:
|
||||
zj[chapter_title] = existing_json_content[chapter_title]
|
||||
else:
|
||||
f = True
|
||||
if f:
|
||||
tqdm.write(f'下载 {chapter_title}')
|
||||
chapter_content, _ = down_text(chapter_id)
|
||||
time.sleep(random.randint(config['delay'][0], config['delay'][1]) / 1000)
|
||||
|
||||
# 每章都保存 JSON 文件
|
||||
existing_json_content[chapter_title] = chapter_content
|
||||
with open(book_json_path, 'w', encoding='UTF-8') as json_file:
|
||||
json.dump(existing_json_content, json_file, ensure_ascii=False)
|
||||
|
||||
# 将章节内容转换为 LaTeX 格式
|
||||
formatted_content = chapter_content.replace('\n', '\\newline ')
|
||||
return f"\\chapter{{{chapter_title}}}\n{formatted_content}\n"
|
||||
return None
|
||||
|
||||
|
||||
def select_save_directory():
|
||||
root = Tk()
|
||||
root.withdraw() # 隐藏主窗口
|
||||
return filedialog.askdirectory(title='请选择保存小说的文件夹')
|
||||
|
||||
|
||||
def search():
|
||||
while True:
|
||||
key = input("请输入搜索关键词(直接Enter返回):")
|
||||
if key == '':
|
||||
return 'b'
|
||||
# 使用新的API进行搜索
|
||||
url = f"https://api5-normal-lf.fqnovel.com/reading/bookapi/search/page/v/?query={key}&aid=1967&channel=0&os_version=0&device_type=0&device_platform=0&iid=466614321180296&passback={{(page-1)*10}}&version_code=999"
|
||||
response = req.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data['code'] == 0:
|
||||
books = data['data']
|
||||
if not books:
|
||||
print("没有找到相关书籍。")
|
||||
break
|
||||
for i, book in enumerate(books):
|
||||
print(
|
||||
f"{i + 1}. 名称:{book['book_data'][0]['book_name']} 作者:{book['book_data'][0]['author']} ID:{book['book_data'][0]['book_id']} 字数:{book['book_data'][0]['word_number']}")
|
||||
while True:
|
||||
choice_ = input("请选择一个结果, 输入 r 以重新搜索:")
|
||||
if choice_ == "r":
|
||||
break
|
||||
elif choice_.isdigit() and 1 <= int(choice_) <= len(books):
|
||||
chosen_book = books[int(choice_) - 1]
|
||||
return chosen_book['book_data'][0]['book_id']
|
||||
else:
|
||||
print("输入无效,请重新输入。")
|
||||
else:
|
||||
print("搜索出错,错误码:", data['code'])
|
||||
break
|
||||
else:
|
||||
print("请求失败,状态码:", response.status_code)
|
||||
break
|
||||
|
||||
|
||||
def book2down(inp):
|
||||
if str(inp)[:4] == 'http':
|
||||
inp = inp.split('?')[0].split('/')[-1]
|
||||
try:
|
||||
book_id = int(inp)
|
||||
with open(record_path, 'r', encoding='UTF-8') as f:
|
||||
records = json.load(f)
|
||||
if book_id not in records:
|
||||
records.append(book_id)
|
||||
with open(record_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(records, f)
|
||||
if config['save_mode'] == 3:
|
||||
status = down_book_epub(book_id)
|
||||
elif config['save_mode'] == 4:
|
||||
status = down_book_html(book_id)
|
||||
elif config['save_mode'] == 5: # 新增的 LaTeX 保存模式
|
||||
status = down_book_latex(book_id)
|
||||
else:
|
||||
status = down_book(book_id)
|
||||
if status == 'err':
|
||||
print('找不到此书')
|
||||
return 'err'
|
||||
else:
|
||||
return 's'
|
||||
except ValueError:
|
||||
return 'err'
|
||||
|
||||
|
||||
# script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
script_dir = ''
|
||||
|
||||
# 设置data文件夹的路径
|
||||
data_dir = os.path.join(script_dir, 'data')
|
||||
|
||||
# 检查data文件夹是否存在,如果不存在则创建
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
# 设置bookstore文件夹的路径
|
||||
bookstore_dir = os.path.join(data_dir, 'bookstore')
|
||||
|
||||
# 检查bookstore文件夹是否存在,如果不存在则创建
|
||||
if not os.path.exists(bookstore_dir):
|
||||
os.makedirs(bookstore_dir)
|
||||
|
||||
# 更新record.json和config.json的文件路径
|
||||
record_path = os.path.join(data_dir, 'record.json')
|
||||
config_path = os.path.join(data_dir, 'config.json')
|
||||
|
||||
# 打印程序信息
|
||||
print('本程序完全免费。\nGithub: https://github.com/ying-ck/fanqienovel-downloader\n作者:Yck & qxqycb')
|
||||
|
||||
# 检查并创建配置文件config.json
|
||||
config_path = os.path.join(data_dir, 'config.json')
|
||||
reset = {'kg': 0, 'kgf': ' ', 'delay': [50, 150], 'save_path': '', 'save_mode': 1, 'space_mode': 'halfwidth', 'xc': 1}
|
||||
if not os.path.exists(config_path):
|
||||
if os.path.exists('config.json'):
|
||||
os.replace('config.json', config_path)
|
||||
else:
|
||||
config = reset
|
||||
with open(config_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(reset, f)
|
||||
else:
|
||||
with open(config_path, 'r', encoding='UTF-8') as f:
|
||||
config = json.load(f)
|
||||
for i in reset:
|
||||
if not i in config:
|
||||
config[i] = reset[i]
|
||||
|
||||
# 检查并创建记录文件record.json
|
||||
record_path = os.path.join(data_dir, 'record.json')
|
||||
if not os.path.exists(record_path):
|
||||
if os.path.exists('record.json'):
|
||||
os.replace('record.json', record_path)
|
||||
else:
|
||||
with open(record_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump([], f)
|
||||
|
||||
print('正在获取cookie')
|
||||
cookie_path = os.path.join(data_dir, 'cookie.json')
|
||||
tzj = int(random.choice(list(down_zj(7143038691944959011)[1].values())[21:]))
|
||||
tmod = 0
|
||||
if os.path.exists(cookie_path):
|
||||
with open(cookie_path, 'r', encoding='UTF-8') as f:
|
||||
cookie = json.load(f)
|
||||
tmod = 1
|
||||
if tmod == 0 or get_cookie(tzj, cookie) == 'err':
|
||||
get_cookie(tzj)
|
||||
print('成功')
|
||||
|
||||
backup_folder_path = 'C:\\Users\\Administrator\\fanqie_down_backup'
|
||||
|
||||
if os.path.exists(backup_folder_path):
|
||||
choice = input("检测到备份文件夹,是否使用备份数据?1.使用备份 2.跳过:")
|
||||
if choice == '1':
|
||||
if os.path.isdir(backup_folder_path):
|
||||
source_folder_path = os.path.dirname(os.path.abspath(__file__))
|
||||
for item in os.listdir(backup_folder_path):
|
||||
source_item_path = os.path.join(backup_folder_path, item)
|
||||
target_item_path = os.path.join(source_folder_path, item)
|
||||
if os.path.isfile(source_item_path):
|
||||
if os.path.exists(target_item_path):
|
||||
os.remove(target_item_path)
|
||||
shutil.copy2(source_item_path, target_item_path)
|
||||
elif os.path.isdir(source_item_path):
|
||||
if os.path.exists(target_item_path):
|
||||
shutil.rmtree(target_item_path)
|
||||
shutil.copytree(source_item_path, target_item_path)
|
||||
else:
|
||||
print("备份文件夹不存在,无法使用备份数据。")
|
||||
elif choice != '2':
|
||||
print("输入无效,请重新运行程序并正确输入。")
|
||||
else:
|
||||
print("程序还未备份")
|
||||
|
||||
|
||||
def perform_backup():
|
||||
# 如果备份文件夹存在,先删除旧备份内容
|
||||
if os.path.isdir(backup_folder_path):
|
||||
for item in os.listdir(backup_folder_path):
|
||||
item_path = os.path.join(backup_folder_path, item)
|
||||
if os.path.isfile(item_path):
|
||||
os.remove(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
else:
|
||||
os.makedirs(backup_folder_path)
|
||||
source_folder_path = os.path.dirname(os.path.abspath(__file__))
|
||||
for item in os.listdir(source_folder_path):
|
||||
source_item_path = os.path.join(source_folder_path, item)
|
||||
target_item_path = os.path.join(backup_folder_path, item)
|
||||
if os.path.isfile(source_item_path) and os.path.basename(__file__) != item:
|
||||
shutil.copy2(source_item_path, target_item_path)
|
||||
elif os.path.isdir(source_item_path) and os.path.basename(__file__) != item and item != 'backup':
|
||||
shutil.copytree(source_item_path, target_item_path)
|
||||
|
||||
|
||||
# 主循环
|
||||
while True:
|
||||
print('\n输入书的id直接下载\n输入下面的数字进入其他功能:')
|
||||
print('''
|
||||
1. 更新小说
|
||||
2. 搜索
|
||||
3. 批量下载
|
||||
4. 设置
|
||||
5. 备份
|
||||
6. 退出
|
||||
''')
|
||||
|
||||
inp = input()
|
||||
|
||||
if inp == '1':
|
||||
# 更新操作
|
||||
with open(record_path, 'r', encoding='UTF-8') as f:
|
||||
records = json.load(f)
|
||||
for book_id in tqdm(records):
|
||||
status = book2down(book_id)
|
||||
if status == 'err' or status == '已完结':
|
||||
records.remove(book_id)
|
||||
with open(record_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(records, f)
|
||||
print('更新完成')
|
||||
|
||||
elif inp == '4':
|
||||
print('请选择项目:1.正文段首占位符 2.章节下载间隔延迟 3.小说保存路径 4.小说保存方式 5.设置下载线程数')
|
||||
inp2 = input()
|
||||
if inp2 == '1':
|
||||
tmp = input('请输入正文段首占位符(当前为"%s")(直接Enter不更改):' % config['kgf'])
|
||||
if tmp != '':
|
||||
config['kgf'] = tmp
|
||||
config['kg'] = int(input('请输入正文段首占位符数(当前为%d):' % config['kg']))
|
||||
elif inp2 == '2':
|
||||
print('由于延迟过小造成的后果请自行负责。\n请输入下载间隔随机延迟的')
|
||||
config['delay'][0] = int(input('下限(当前为%d)(毫秒):' % config['delay'][0]))
|
||||
config['delay'][1] = int(input('上限(当前为%d)(毫秒):' % config['delay'][1]))
|
||||
elif inp2 == '3':
|
||||
print('tip:设置为当前目录点取消')
|
||||
time.sleep(1)
|
||||
config['save_path'] = select_save_directory()
|
||||
elif inp2 == '4':
|
||||
print('请选择:1.保存为单个 txt 2.分章保存 3.保存为 epub 4.保存为 HTML 网页格式 5.保存为 LaTeX')
|
||||
inp3 = input()
|
||||
if inp3 == '1':
|
||||
config['save_mode'] = 1
|
||||
elif inp3 == '2':
|
||||
config['save_mode'] = 2
|
||||
elif inp3 == '3':
|
||||
config['save_mode'] = 3
|
||||
elif inp3 == '4':
|
||||
config['save_mode'] = 4
|
||||
elif inp3 == '5':
|
||||
config['save_mode'] = 5
|
||||
else:
|
||||
print('请正确输入!')
|
||||
continue
|
||||
elif inp2 == '5':
|
||||
config['xc'] = int(input('请输入下载线程数:'))
|
||||
else:
|
||||
print('请正确输入!')
|
||||
continue
|
||||
with open(config_path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(config, f)
|
||||
print('设置完成')
|
||||
|
||||
elif inp == '2':
|
||||
|
||||
tmp = search()
|
||||
if tmp == 'b':
|
||||
continue
|
||||
if book2down(tmp) == 'err':
|
||||
print('下载失败')
|
||||
|
||||
elif inp == '3':
|
||||
urls_path = 'urls.txt' # 定义文件名
|
||||
if not os.path.exists(urls_path):
|
||||
print(f"未找到'{urls_path}',将为您创建一个新的文件。")
|
||||
with open(urls_path, 'w', encoding='UTF-8') as file:
|
||||
file.write("# 请输入小说链接,一行一个\n")
|
||||
print(f"'{urls_path}' 已存在。请在文件中输入小说链接,一行一个。")
|
||||
|
||||
# 使用默认文本编辑器打开urls.txt文件供用户编辑
|
||||
root = Tk()
|
||||
root.withdraw() # 隐藏主窗口
|
||||
root.update() # 更新Tkinter的事件循环,确保窗口被隐藏
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# Windows系统使用os.startfile
|
||||
os.startfile(urls_path)
|
||||
elif platform.system() == "Darwin":
|
||||
# macOS系统使用open命令
|
||||
os.system(f"open -a TextEdit {urls_path}")
|
||||
else:
|
||||
# 其他系统使用默认文本编辑器
|
||||
os.system(f"xdg-open {urls_path}")
|
||||
|
||||
print("输入完成后请保存并关闭文件,然后按Enter键继续...")
|
||||
input()
|
||||
|
||||
# 读取urls.txt文件中的链接
|
||||
with open(urls_path, 'r', encoding='UTF-8') as file:
|
||||
content = file.read()
|
||||
urls = content.replace(' ', '').split('\n')
|
||||
|
||||
# 开始批量下载
|
||||
for url in urls:
|
||||
if url[0] != '#':
|
||||
print(f'开始下载链接: {url}')
|
||||
status = book2down(url) # 修改这里,传递保存方式
|
||||
if status == 'err':
|
||||
print(f'链接: {url} 下载失败。')
|
||||
else:
|
||||
print(f'链接: {url} 下载完成。')
|
||||
|
||||
elif inp == '5':
|
||||
perform_backup()
|
||||
print('备份完成')
|
||||
|
||||
elif inp == '6':
|
||||
break
|
||||
|
||||
else:
|
||||
# 下载新书或更新现有书籍
|
||||
if book2down(inp) == 'err':
|
||||
print('请输入有效的选项或书籍ID。')
|
|
@ -0,0 +1,96 @@
|
|||
import json
|
||||
import requests
|
||||
import parsel
|
||||
|
||||
|
||||
class Novel:
|
||||
def __init__(self, nid):
|
||||
self.novel_id = nid
|
||||
self.book_name = None
|
||||
self.book_tags = []
|
||||
self.chapters = []
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
'cookie': 's_v_web_id=verify_m24u3hpa_LjedVsj2_0wHE_4YHz_AQbA_XubUuy7zBEfB; novel_web_id=7424527740918957579; Hm_lvt_2667d29c8e792e6fa9182c20a3013175=1728657590,1728729482; HMACCOUNT=C11EDE0200C117A2; csrf_session_id=9d05fbb2d55d14bcef609e07b15ee962; serial_uuid=7424527740918957579; serial_webid=7424527740918957579; passport_csrf_token=4ca9597fb853eee76008f7aa64d8e2b2; passport_csrf_token_default=4ca9597fb853eee76008f7aa64d8e2b2; passport_mfa_token=Cjd8rmE00d6EfflBq%2FcSKtg4tdE1wjK3k4AlVK9xhhfSX9yES1syXBMr7T5SqdgNhHiMb3ZcENizGkoKPBwhI5O6cVkE8SiUJ4MM3ZWbezvJEknWED5U%2FreyeQvJnVRfBsQqHY2RtF6nrPYyGuweLAfpGWn5dA2N%2FxD4xt4NGPax0WwgAiIBA8AKpQg%3D; d_ticket=6537bdb09e3bc966a73555b625f4a20805d80; odin_tt=8b14d78bda8d96ee252a0b04bde6df67e417de819ae2aff5e84dde678f187f509b2372655b99325ad4ce341c6c80bd5d947a24cb10a10aaf4dec4d9b69267a08; n_mh=LcM1cOw8HFMjuUsAxgY98un18tJ2aS13XDoKUMJmgAw; passport_auth_status=d05d05a79a1b91086a256e14792c088f%2C; passport_auth_status_ss=d05d05a79a1b91086a256e14792c088f%2C; sid_guard=fc6b04b9b74cd000904ad05a7d97eecc%7C1728730882%7C5184000%7CWed%2C+11-Dec-2024+11%3A01%3A22+GMT; uid_tt=081a78b4f45cf6f7248d345f6f6b2a2f; uid_tt_ss=081a78b4f45cf6f7248d345f6f6b2a2f; sid_tt=fc6b04b9b74cd000904ad05a7d97eecc; sessionid=fc6b04b9b74cd000904ad05a7d97eecc; sessionid_ss=fc6b04b9b74cd000904ad05a7d97eecc; is_staff_user=false; sid_ucp_v1=1.0.0-KGMxMDEzMDA5MTNjN2MwNzI1M2NkZTg3NWE2MDJiODFlMzc3MzJiZmYKHwiY37C54MyAAhCCrqm4BhjHEyAMMLvkgaMGOAJA8QcaAmhsIiBmYzZiMDRiOWI3NGNkMDAwOTA0YWQwNWE3ZDk3ZWVjYw; ssid_ucp_v1=1.0.0-KGMxMDEzMDA5MTNjN2MwNzI1M2NkZTg3NWE2MDJiODFlMzc3MzJiZmYKHwiY37C54MyAAhCCrqm4BhjHEyAMMLvkgaMGOAJA8QcaAmhsIiBmYzZiMDRiOWI3NGNkMDAwOTA0YWQwNWE3ZDk3ZWVjYw; store-region=cn-gd; store-region-src=uid; Hm_lpvt_2667d29c8e792e6fa9182c20a3013175=1728733437; ttwid=1%7CpG5C6EqM_9UeWXYJGM3B4K4x0Nyc97iCcZj51uLAaqI%7C1728733438%7C9f427b4c00ce6215497cb23aee88d69836d2eecfc5fab5ef250da93cdaf6b745',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0'
|
||||
}
|
||||
|
||||
def fetch_novel_content(self):
|
||||
# 请求链接
|
||||
url = 'https://fanqienovel.com/page/' + self.novel_id
|
||||
web_page = requests.get(url, headers=self.headers).content.decode('utf-8')
|
||||
web_selector = parsel.Selector(web_page)
|
||||
|
||||
# 书名
|
||||
self.book_name = web_selector.css('.info-name h1::text').get()
|
||||
# 小说标签
|
||||
self.book_tags = web_selector.css('.info-label span::text').getall()
|
||||
# 章节ID
|
||||
chapter_urls = web_selector.css('.chapter-item .chapter-item-title::attr(href)').getall()
|
||||
|
||||
# 逐章获取章节内容
|
||||
for index, id_link in enumerate(chapter_urls):
|
||||
chapter_data = self.get_chapter_content(id_link)
|
||||
self.chapters.append(chapter_data) # 保存章节数据
|
||||
print(f"获取章节 {chapter_data['chapterName']} 成功")
|
||||
|
||||
# 每5章保存一次
|
||||
if (index + 1) % 5 == 0:
|
||||
self.save_content_to_file() # 每5章保存内容
|
||||
|
||||
# 如果还有未保存的章节,最后再保存一次
|
||||
self.save_content_to_file()
|
||||
|
||||
def get_chapter_content(self, id_link):
|
||||
chapter_url = 'https://fanqienovel.com' + id_link
|
||||
chapter_data = requests.get(chapter_url, headers=self.headers).content.decode('utf-8')
|
||||
chapter_selector = parsel.Selector(chapter_data)
|
||||
|
||||
# 章节名
|
||||
chapter_name = chapter_selector.css('.muye-reader-title::text').get()
|
||||
# 章节字数
|
||||
chapter_words = chapter_selector.css('.desc-item:nth-child(1)::text').get()
|
||||
# 章节更新时间
|
||||
chapter_update_time = chapter_selector.css('.desc-item:nth-child(2)::text').get()
|
||||
# 章节内容
|
||||
chapter_contents = chapter_selector.css('.muye-reader-content-16 p::text').getall()
|
||||
chapter_content = self.decrypt_chapter_content('\n\n'.join(chapter_contents))
|
||||
|
||||
return {
|
||||
"chapterName": chapter_name,
|
||||
"wordCount": chapter_words,
|
||||
"updateTime": chapter_update_time,
|
||||
"content": chapter_content,
|
||||
}
|
||||
|
||||
def decrypt_chapter_content(self, content):
|
||||
with open('woff2.json', 'r', encoding='utf-8') as f:
|
||||
woff2_dict = json.load(f)
|
||||
converted_content = ""
|
||||
for index in content:
|
||||
try:
|
||||
converted_content += woff2_dict[str(ord(index))]
|
||||
except:
|
||||
converted_content += index
|
||||
return converted_content
|
||||
|
||||
def save_content_to_file(self):
|
||||
novel_data = {
|
||||
"novel": {
|
||||
"title": self.book_name,
|
||||
"tags": self.book_tags,
|
||||
"chapters": self.chapters
|
||||
}
|
||||
}
|
||||
with open(f'{novel_data["novel"]["title"]}.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(novel_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == '__main__':
|
||||
novel_id = '7412526577163979800'
|
||||
novel = Novel(novel_id)
|
||||
novel.fetch_novel_content() # 获取小说内容
|
||||
novel.save_content_to_file() # 保存内容到json
|
||||
print(novel.book_name) # 打印书名
|
|
@ -0,0 +1,6 @@
|
|||
requests~=2.32.3
|
||||
lxml~=5.3.0
|
||||
EbookLib~=0.18
|
||||
tqdm~=4.66.5
|
||||
beautifulsoup4~=4.12.3
|
||||
parsel~=1.9.1
|
|
@ -0,0 +1,364 @@
|
|||
{
|
||||
"58670": "0",
|
||||
"58413": "1",
|
||||
"58678": "2",
|
||||
"58371": "3",
|
||||
"58353": "4",
|
||||
"58480": "5",
|
||||
"58359": "6",
|
||||
"58449": "7",
|
||||
"58540": "8",
|
||||
"58692": "9",
|
||||
"58712": "a",
|
||||
"58542": "b",
|
||||
"58575": "c",
|
||||
"58626": "d",
|
||||
"58691": "e",
|
||||
"58561": "f",
|
||||
"58362": "g",
|
||||
"58619": "h",
|
||||
"58430": "i",
|
||||
"58531": "j",
|
||||
"58588": "k",
|
||||
"58440": "l",
|
||||
"58681": "m",
|
||||
"58631": "n",
|
||||
"58376": "o",
|
||||
"58429": "p",
|
||||
"58555": "q",
|
||||
"58498": "r",
|
||||
"58518": "s",
|
||||
"58453": "t",
|
||||
"58397": "u",
|
||||
"58356": "v",
|
||||
"58435": "w",
|
||||
"58514": "x",
|
||||
"58482": "y",
|
||||
"58529": "z",
|
||||
"58515": "A",
|
||||
"58688": "B",
|
||||
"58709": "C",
|
||||
"58344": "D",
|
||||
"58656": "E",
|
||||
"58381": "F",
|
||||
"58576": "G",
|
||||
"58516": "H",
|
||||
"58463": "I",
|
||||
"58649": "J",
|
||||
"58571": "K",
|
||||
"58558": "L",
|
||||
"58433": "M",
|
||||
"58517": "N",
|
||||
"58387": "O",
|
||||
"58687": "P",
|
||||
"58537": "Q",
|
||||
"58541": "R",
|
||||
"58458": "S",
|
||||
"58390": "T",
|
||||
"58466": "U",
|
||||
"58386": "V",
|
||||
"58697": "W",
|
||||
"58519": "X",
|
||||
"58511": "Y",
|
||||
"58634": "Z",
|
||||
"58611": "的",
|
||||
"58590": "一",
|
||||
"58398": "是",
|
||||
"58422": "了",
|
||||
"58657": "我",
|
||||
"58666": "不",
|
||||
"58562": "人",
|
||||
"58345": "在",
|
||||
"58510": "他",
|
||||
"58496": "有",
|
||||
"58654": "这",
|
||||
"58441": "个",
|
||||
"58493": "上",
|
||||
"58714": "们",
|
||||
"58618": "来",
|
||||
"58528": "到",
|
||||
"58620": "时",
|
||||
"58403": "大",
|
||||
"58461": "地",
|
||||
"58481": "为",
|
||||
"58700": "子",
|
||||
"58708": "中",
|
||||
"58503": "你",
|
||||
"58442": "说",
|
||||
"58639": "生",
|
||||
"58506": "国",
|
||||
"58663": "年",
|
||||
"58436": "着",
|
||||
"58563": "就",
|
||||
"58391": "那",
|
||||
"58357": "和",
|
||||
"58354": "要",
|
||||
"58695": "她",
|
||||
"58372": "出",
|
||||
"58696": "也",
|
||||
"58551": "得",
|
||||
"58445": "里",
|
||||
"58408": "后",
|
||||
"58599": "自",
|
||||
"58424": "以",
|
||||
"58394": "会",
|
||||
"58348": "家",
|
||||
"58426": "可",
|
||||
"58673": "下",
|
||||
"58417": "而",
|
||||
"58556": "过",
|
||||
"58603": "天",
|
||||
"58565": "去",
|
||||
"58604": "能",
|
||||
"58522": "对",
|
||||
"58632": "小",
|
||||
"58622": "多",
|
||||
"58350": "然",
|
||||
"58605": "于",
|
||||
"58617": "心",
|
||||
"58401": "学",
|
||||
"58637": "么",
|
||||
"58684": "之",
|
||||
"58382": "都",
|
||||
"58464": "好",
|
||||
"58487": "看",
|
||||
"58693": "起",
|
||||
"58608": "发",
|
||||
"58392": "当",
|
||||
"58474": "没",
|
||||
"58601": "成",
|
||||
"58355": "只",
|
||||
"58573": "如",
|
||||
"58499": "事",
|
||||
"58469": "把",
|
||||
"58361": "还",
|
||||
"58698": "用",
|
||||
"58489": "第",
|
||||
"58711": "样",
|
||||
"58457": "道",
|
||||
"58635": "想",
|
||||
"58492": "作",
|
||||
"58647": "种",
|
||||
"58623": "开",
|
||||
"58521": "美",
|
||||
"58609": "总",
|
||||
"58530": "从",
|
||||
"58665": "无",
|
||||
"58652": "情",
|
||||
"58676": "己",
|
||||
"58456": "面",
|
||||
"58581": "最",
|
||||
"58509": "女",
|
||||
"58488": "但",
|
||||
"58363": "现",
|
||||
"58685": "前",
|
||||
"58396": "些",
|
||||
"58523": "所",
|
||||
"58471": "同",
|
||||
"58485": "日",
|
||||
"58613": "手",
|
||||
"58533": "又",
|
||||
"58589": "行",
|
||||
"58527": "意",
|
||||
"58593": "动",
|
||||
"58699": "方",
|
||||
"58707": "期",
|
||||
"58414": "它",
|
||||
"58596": "头",
|
||||
"58570": "经",
|
||||
"58660": "长",
|
||||
"58364": "儿",
|
||||
"58526": "回",
|
||||
"58501": "位",
|
||||
"58638": "分",
|
||||
"58404": "爱",
|
||||
"58677": "老",
|
||||
"58535": "因",
|
||||
"58629": "很",
|
||||
"58577": "给",
|
||||
"58606": "名",
|
||||
"58497": "法",
|
||||
"58662": "间",
|
||||
"58479": "斯",
|
||||
"58532": "知",
|
||||
"58380": "世",
|
||||
"58385": "什",
|
||||
"58405": "两",
|
||||
"58644": "次",
|
||||
"58578": "使",
|
||||
"58505": "身",
|
||||
"58564": "者",
|
||||
"58412": "被",
|
||||
"58686": "高",
|
||||
"58624": "已",
|
||||
"58667": "亲",
|
||||
"58607": "其",
|
||||
"58616": "进",
|
||||
"58368": "此",
|
||||
"58427": "话",
|
||||
"58423": "常",
|
||||
"58633": "与",
|
||||
"58525": "活",
|
||||
"58543": "正",
|
||||
"58418": "感",
|
||||
"58597": "见",
|
||||
"58683": "明",
|
||||
"58507": "问",
|
||||
"58621": "力",
|
||||
"58703": "理",
|
||||
"58438": "尔",
|
||||
"58536": "点",
|
||||
"58384": "文",
|
||||
"58484": "几",
|
||||
"58539": "定",
|
||||
"58554": "本",
|
||||
"58421": "公",
|
||||
"58347": "特",
|
||||
"58569": "做",
|
||||
"58710": "外",
|
||||
"58574": "孩",
|
||||
"58375": "相",
|
||||
"58645": "西",
|
||||
"58592": "果",
|
||||
"58572": "走",
|
||||
"58388": "将",
|
||||
"58370": "月",
|
||||
"58399": "十",
|
||||
"58651": "实",
|
||||
"58546": "向",
|
||||
"58504": "声",
|
||||
"58419": "车",
|
||||
"58407": "全",
|
||||
"58672": "信",
|
||||
"58675": "重",
|
||||
"58538": "三",
|
||||
"58465": "机",
|
||||
"58374": "工",
|
||||
"58579": "物",
|
||||
"58402": "气",
|
||||
"58702": "每",
|
||||
"58553": "并",
|
||||
"58360": "别",
|
||||
"58389": "真",
|
||||
"58560": "打",
|
||||
"58690": "太",
|
||||
"58473": "新",
|
||||
"58512": "比",
|
||||
"58653": "才",
|
||||
"58704": "便",
|
||||
"58545": "夫",
|
||||
"58641": "再",
|
||||
"58475": "书",
|
||||
"58583": "部",
|
||||
"58472": "水",
|
||||
"58478": "像",
|
||||
"58664": "眼",
|
||||
"58586": "等",
|
||||
"58568": "体",
|
||||
"58674": "却",
|
||||
"58490": "加",
|
||||
"58476": "电",
|
||||
"58346": "主",
|
||||
"58630": "界",
|
||||
"58595": "门",
|
||||
"58502": "利",
|
||||
"58713": "海",
|
||||
"58587": "受",
|
||||
"58548": "听",
|
||||
"58351": "表",
|
||||
"58547": "德",
|
||||
"58443": "少",
|
||||
"58460": "克",
|
||||
"58636": "代",
|
||||
"58585": "员",
|
||||
"58625": "许",
|
||||
"58694": "稜",
|
||||
"58428": "先",
|
||||
"58640": "口",
|
||||
"58628": "由",
|
||||
"58612": "死",
|
||||
"58446": "安",
|
||||
"58468": "写",
|
||||
"58410": "性",
|
||||
"58508": "马",
|
||||
"58594": "光",
|
||||
"58483": "白",
|
||||
"58544": "或",
|
||||
"58495": "住",
|
||||
"58450": "难",
|
||||
"58643": "望",
|
||||
"58486": "教",
|
||||
"58406": "命",
|
||||
"58447": "花",
|
||||
"58669": "结",
|
||||
"58415": "乐",
|
||||
"58444": "色",
|
||||
"58549": "更",
|
||||
"58494": "拉",
|
||||
"58409": "东",
|
||||
"58658": "神",
|
||||
"58557": "记",
|
||||
"58602": "处",
|
||||
"58559": "让",
|
||||
"58610": "母",
|
||||
"58513": "父",
|
||||
"58500": "应",
|
||||
"58378": "直",
|
||||
"58680": "字",
|
||||
"58352": "场",
|
||||
"58383": "平",
|
||||
"58454": "报",
|
||||
"58671": "友",
|
||||
"58668": "关",
|
||||
"58452": "放",
|
||||
"58627": "至",
|
||||
"58400": "张",
|
||||
"58455": "认",
|
||||
"58416": "接",
|
||||
"58552": "告",
|
||||
"58614": "入",
|
||||
"58582": "笑",
|
||||
"58534": "内",
|
||||
"58701": "英",
|
||||
"58349": "军",
|
||||
"58491": "候",
|
||||
"58467": "民",
|
||||
"58365": "岁",
|
||||
"58598": "往",
|
||||
"58425": "何",
|
||||
"58462": "度",
|
||||
"58420": "山",
|
||||
"58661": "觉",
|
||||
"58615": "路",
|
||||
"58648": "带",
|
||||
"58470": "万",
|
||||
"58377": "男",
|
||||
"58520": "边",
|
||||
"58646": "风",
|
||||
"58600": "解",
|
||||
"58431": "叫",
|
||||
"58715": "任",
|
||||
"58524": "金",
|
||||
"58439": "快",
|
||||
"58566": "原",
|
||||
"58477": "吃",
|
||||
"58642": "妈",
|
||||
"58437": "变",
|
||||
"58411": "通",
|
||||
"58451": "师",
|
||||
"58395": "立",
|
||||
"58369": "象",
|
||||
"58706": "数",
|
||||
"58705": "四",
|
||||
"58379": "失",
|
||||
"58567": "满",
|
||||
"58373": "战",
|
||||
"58448": "远",
|
||||
"58659": "格",
|
||||
"58434": "士",
|
||||
"58679": "音",
|
||||
"58432": "轻",
|
||||
"58689": "目",
|
||||
"58591": "条",
|
||||
"58682": "呢"
|
||||
}
|
Loading…
Reference in New Issue