Files
flaskProject/app/services/book_service.py
T
YanLongChangAn ade1b6c9b0 feat(project): 初始化项目结构和配置
- 添加.gitignore和.flaskenv环境配置文件
- 创建Flask应用基础架构,包括models、services、utils模块
- 配置数据库模型User和Setting,集成SQLAlchemy和Alembic迁移
- 添加前端Vue项目结构,包含Element Plus组件库
- 配置前后端API路由和蓝prints模块
- 实现书籍下载服务BookService功能模块
- 添加代码规范配置.editorconfig、.oxfmtrc.json、.oxlintrc.json
- 配置VSCode推荐插件和前端构建工具链
- 实现文件服务FileService和相关业务逻辑
2026-04-19 15:43:45 +08:00

164 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import requests
from bs4 import BeautifulSoup
from app.extensions import db
from tenacity import retry, stop_after_attempt, RetryError
from app.utils import datetime_to_timestamp, timestamp_to_datetime, get_today_timestamp
from app.models import Setting
class BookService:
def __init__(self):
self.setting = Setting.query.filter_by(name="book_download").first().to_dict()
self.setting["targetDate"] = timestamp_to_datetime(datetime_to_timestamp(self.setting["targetDate"]) - 24 * 60 * 60)
app_dir = os.path.dirname(os.path.dirname(__file__))
self.download_path = os.path.join(os.path.dirname(app_dir), "download") # 下载目录
self.today_timestamp = get_today_timestamp()
self.one_date_loading = False
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"cookie": "cPNj_2132_saltkey=CeJLs8Ed; "
"cPNj_2132_auth=44bdzFfx4TYjBMhuKf4AZdi2JM%2BXZ0Okxd0JEl1Hmwbz%2Fr3WiX0sLmATmolWkLlRgorFz%2BULizz5o6G%2FJ3YGWYPF0xw; "
}
def legitimate_naming(self, name): # 合法命名
name = name.replace(":", "")
name = name.replace("<", "《")
name = name.replace(">", "》")
name = name.replace("/", " ")
name = name.replace("\\", " ")
name = name.replace("?", "")
return name
def create_folder(self, target_dir, name): # 创建文件夹
real_dir = os.path.join(target_dir, name)
folder = os.path.exists(real_dir)
if not folder:
os.makedirs(real_dir)
else:
print(f"文件夹:{name} 已存在")
def isDownloadTarget(self, soup): # 是否是可以下载的文件
return soup.headers.get('Content-Type') in ['"application/octet-stream"', "application/octet-stream"]
def post_page(self, name, url): # 帖子页面
response = requests.get(f"{self.setting["baseUrl"]}{url}", headers=self.headers)
print(f"详情页面状态码:{str(response.status_code)}")
soup = BeautifulSoup(response.text, 'lxml')
resource_boxs = soup.select('ignore_js_op')
for i in resource_boxs:
download_dir_path = os.path.join(self.download_path, self.setting["targetDate"])
self.create_folder(download_dir_path, name)
try:
for string in i.select_one(".tip.tip_4").stripped_strings:
if "阅读权限: " in repr(string):
privilege_level = int(
repr(string).strip("'").strip('阅读权限: '))
if privilege_level > self.setting["privilegeLevel"]:
print("下载失败,权限等级不够")
return
yield from self.download_file(f"{self.setting["baseUrl"]}{i.select_one(
'a')['href']}", name, i.select_one('a').string)
except RetryError as e:
print('重试三次也不成功')
@retry(stop=stop_after_attempt(3))
def download_file(self, file_url, dir_name, file_name): # 下载文件
file_res = requests.get(file_url, headers=self.headers, stream=True, allow_redirects=False)
if file_res.status_code in (301, 302, 307, 308): # 非200状态码,重新请求
location = file_res.headers["location"]
print(f'跳转页面了:{location}')
file_res = requests.get(location, headers={"referer": location}, stream=True)
print(f"下载状态码:{file_res.status_code} {file_url}")
print(dir_name + "/" + file_name)
yield f"data: {self.setting["targetDate"]} {file_name}\n\n"
if self.isDownloadTarget(file_res):
with open(f"download/{self.setting["targetDate"]}/{dir_name}/{file_name}", 'wb') as file2:
for chunk in file_res.iter_content(chunk_size=1024):
if chunk:
file2.write(chunk)
def section_page(self, page): # 版块页面
print(f"当前页面 {page}")
target_date_timestamp = datetime_to_timestamp(self.setting["targetDate"]) # 目标时间戳
if target_date_timestamp >= self.today_timestamp: # 判断时间(是否大于当前时间戳)
# print("不允许下载当天的")
yield f"data: 不允许下载当天的\n\n"
return None
# yield f"data: 当前页面 {page}\n\n"
url = f"{self.setting["baseUrl"]}forum.php?mod={self.setting["mod"]}&fid={self.setting["fid"]}&page={page}" # 版块页面地址
response = requests.get(url, headers=self.headers) # 请求
soup = BeautifulSoup(response.text, 'lxml') # 解析
total_page_element = soup.select_one("a.bm_h") # 获取总页数元素
if total_page_element is not None: # 总页数元素存在 则更新总页数
new_total_page = int(total_page_element.get("totalpage")) # 获取总页数
if self.setting["totalPage"] != new_total_page: # 总页数有更新
self.setting["totalPage"] = new_total_page # 更新总页数
Setting.query.filter_by(name="book_download").update({"totalPage": self.setting["totalPage"]})
db.session.commit()
yield from self.section_page(new_total_page - self.setting["targetCountdownPage"]) # 重新跳转指点版块页面
return None
posts = soup.select("table#threadlisttableid tbody[id^=normalthread]") # 获取帖子列表
posts.reverse() # 帖子列表倒序
posts_min_timestamp = datetime_to_timestamp(self.get_post_date(posts[0])) # 帖子列表最小时间戳
posts_max_timestamp = datetime_to_timestamp(self.get_post_date(posts[-1])) # 帖子列表最大时间戳
# 本页最早的帖子时间戳小于等于目标时间戳 且 倒计时页数大于 0 则跳转到上一页继续查找
if posts_min_timestamp >= target_date_timestamp and not self.one_date_loading and self.setting["targetCountdownPage"] > 0:
print("跳转到上一页")
# yield f"data: 跳转到上一页\n\n"
self.setting["targetCountdownPage"] = max(self.setting["targetCountdownPage"] - 1, 0)
Setting.query.filter_by(name="book_download").update({"targetCountdownPage": self.setting["targetCountdownPage"]})
db.session.commit()
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
return None
else:
print("不需要跳转上一页")
self.one_date_loading = True
# yield f"data: 不需要跳转上一页\n\n"
for post in posts:
post_element = post.select_one(".s.xst")
post_date = self.get_post_date(post) # 获取帖子时间
post_date_timestamp = datetime_to_timestamp(post_date) # 帖子时间戳
if post_date_timestamp == target_date_timestamp: # 判断是否是目标时间
print(f"目标时间是{post_date}")
# yield f"data: 目标时间是{post_date}\n\n"
yield from self.post_page(self.legitimate_naming(post_element.string), post_element['href']) # 跳转帖子页面
if post_date_timestamp > target_date_timestamp: # 帖子时间大于目标时间并且没有下一天时间
self.setting["targetDate"] = post_date
Setting.query.filter_by(name="book_download").update({"targetDate": self.setting["targetDate"]})
db.session.commit()
print(f"下一天是 {post_date}")
# yield f"data: 下一天是 {post_date}\n\n"
self.one_date_loading = False
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
return None
# 判断是否需要跳转到下一页
if posts_max_timestamp <= target_date_timestamp and self.setting["targetCountdownPage"] < self.setting[
"totalPage"] - 1:
print("跳转到下一页")
# yield f"data: 跳转到下一页\n\n"
self.setting["targetCountdownPage"] = min(self.setting["targetCountdownPage"] + 1,
self.setting["totalPage"] - 1)
Setting.query.filter_by(name="book_download").update(
{"targetCountdownPage": self.setting["targetCountdownPage"]})
db.session.commit()
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
return None
def get_post_date(self, element):
if element.select_one(".by em span span"):
if element.select_one(".by em span span").get("title"):
return element.select_one(".by em span span").get("title")
else:
return element.select_one(".by em span span").string
else:
return element.select_one(".by em span").string
def book_download(self):
self.create_folder(self.download_path, self.setting["targetDate"]) # 创建文件夹
target_page = self.setting["totalPage"] - self.setting["targetCountdownPage"] # 目标页
yield from self.section_page(target_page) # 前往目标页
yield f"event: close\ndata: 已完成\n\n"