前端客户机100%CPU高内存占用镇楼

最近无聊写了一个爬图+看图的软件,遇到了很多问题,也获得了不少收获。为预防老年痴呆遗忘,提前记录一些心得。

后端使用python flask框架开发。

一、身份验证

身份验证首先要满足基本功能,能够验证传入后端的用户名密码是否正确,然后放行。最简单的方式,直接明文保存用户密码并写死在代码里,粗暴拼接前端返回的用户名密码,直接比对是否相等,返回true或false。

USER_KEY = "user&123"

def if_input_correct(username,password):
    user_input_key = username + "&" + password
    if user_input_key == USER_KEY:
        return True
    return False

def if_user_login(user_auth):
    if user_auth != USER_KEY:
        return False
    return True

这种方法的问题在于,明文保存密码安全性无法保证,容易泄露。即使不泄露,由于未设置SameSite属性或 CSRF 令牌,攻击者可诱导用户在已登录状态下执行恶意操作。另外密码未经过哈希 + 加盐处理,也无法抵御彩虹表攻击。

所以改进方式应该是:

1、数据库存储用户敏感信息,而非明文保存用户名密码;

2、用户名密码不直接保存,应加盐后通过计算哈希等方式获取计算值,只保存计算值和盐;

3、配置samesite属性,防CSRF

# 注册新用户(生成盐和哈希)
def register_user(username, password):
    salt = os.urandom(16)  # 生成随机盐
    password_hash = hashlib.pbkdf2_hmac(
        'sha256', 
        password.encode('utf-8'), 
        salt, 
        100000
    )
    db.save_user(username, password_hash, salt)

# 登录验证(使用存储的盐计算哈希)
def login(username, password):
    stored_data = db.get_user(username)
    if not stored_data:
        return False
    
    stored_hash, salt = stored_data
    computed_hash = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        100000
    )
    
    return stored_hash == computed_hash

# 登录接口(设置samesite=strict的cookie)
@app.route('/login', methods=['POST'])
def login_api():
    username = request.form.get('username')
    password = request.form.get('password')
    
    if login(username, password):
        resp = make_response("Login success")
        resp.set_cookie('session_id', 'user_session_data', samesite='Strict')
        return resp
    return "Login failed", 401

二、下载图片

下载图片基本上没有什么需要赘述的内容,由于目标网站提供了足够好用的api接口,只需根据返回的json内容合理保存图片和缩略图即可。

三、图片保存和图片管理

这是本文比较重点内容,主要在于图片的管理方式。

最开始的方式是保存后,通过glob.glob 遍历所有文件并返回json的方式,让前端读取图片展示出来。但当图片多了之后,不管是对前端和对后端都会造成不小的负担。

于后端而言:每次都要遍历全部文件,而且根据缩略图来获取原图要多次调用get_original_format ,会进一步加重I/O负载。

于前端而言:由于后端没有分页机制,函数一次性返回所有匹配的图片信息,当图片很多时, json数据量可能会极为庞大,客户端接收效率降低,前端渲染图片时的压力也会大大增加。

@app.route('/api/images')
def get_images():
    """提取文件"""
    # 初始化一个空列表 images,用于存储符合条件的图片信息
    images = []
    try:
        # 判断是否存在缩略图地址
        pic_dir = app.config['THUMBNAIL_FOLDER']
        if os.path.exists(pic_dir):
            # 遍历文件夹下所有文件
            pic_files = glob.glob(os.path.join(pic_dir,'*'))
            for f in pic_files:
                # 根据缩略图名称,获取其原始文件的实际格式
                original_format = get_original_format(f)
                # 如果找到了,则添加图片信息到images列表中
                if original_format:
                    images.append({
                        'name': os.path.basename(f),
                        'original_format': original_format
                    })
        # 返回images列表
        return jsonify(images)

由于前端渲染压力大,我首先想到的是解决前端压力过大的问题,建立分页机制。但是依赖glob.glob 遍历方式的后端,如果引入分页机制,对后端又是一个难点。

1、遍历文件无法保证文件顺序:glob.glob 的返回顺序是不确定且不可靠的,的返回顺序是不确定且不可靠的;

2、遍历所有文件再分页输出可能会加剧后端的内存占用;

3、同一个文件夹内文件过多可能导致性能问题,如果分开存放,传统遍历方式有可能陷入深分页问题。

所以答案呼之欲出了…只能依靠索引解决问题,有必要引入数据库了。

简单来说就是建立目录,然后只通过目录来查询而不实际访问文件。这样做的好处是这样做的好处是:

首先,能显著提升查询效率,就像在图书馆通过书目检索找到书籍位置,大大减少了数据处理时间,对高并发请求的响应速度也会大幅提升。

其次,有效降低后端内存压力,无需一次性读取所有文件信息进行分页处理,仅按需从数据库获取目录相关数据,避免了因大量文件遍历导致的内存占用过高问题,使后端服务更加稳定可靠。

再者,保证了数据查询的一致性和准确性,数据库的索引机制可以严格按照设定的规则存储和检索数据,避免了因文件遍历顺序不确定带来的结果不一致问题,让目录与实际查询结果始终保持准确对应。

当然也存在一定坏处,例如增加了系统架构的复杂性,同时还有数据一致性方面的挑战 。但是总的来说,利大于弊,且是必须的。

四、关于数据库设计和tags

以下是设计的数据库表结构

-- 文件表
CREATE TABLE IF NOT EXISTS file (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    source TEXT NOT NULL CHECK (source IN ('pixiv',  'ai', 'import')),
    source_id TEXT UNIQUE,
    format TEXT NOT NULL CHECK (format IN ('.jpg', '.jpeg', '.png', '.bmp', '.webp', '.gif')),
    width INTEGER,
    height INTEGER,
    size INTEGER,
    rating TEXT CHECK (rating IN ('e', 'q', 's')),
    original INTEGER NOT NULL CHECK (original IN (0, 1)),
    deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
    delete_time INTEGER,
    like INTEGER NOT NULL DEFAULT 0 CHECK (like IN (0, 1)),
    thumbnail_path TEXT,
    file_path TEXT NOT NULL UNIQUE,
    upload_time INTEGER NOT NULL,
    hash TEXT UNIQUE
);

-- 标签表
CREATE TABLE IF NOT EXISTS tag (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE COLLATE NOCASE
);

-- 文件-标签关联表
CREATE TABLE IF NOT EXISTS file_tag (
    file_id INTEGER NOT NULL,
    tag_id INTEGER NOT NULL,
    PRIMARY KEY (file_id, tag_id),
    FOREIGN KEY (file_id) REFERENCES file(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE
);

-- 索引定义
CREATE INDEX IF NOT EXISTS idx_file_format ON file(format);
CREATE INDEX IF NOT EXISTS idx_file_rating ON file(rating);
CREATE INDEX IF NOT EXISTS idx_file_deleted ON file(deleted);
CREATE INDEX IF NOT EXISTS idx_file_like ON file(like);
CREATE INDEX IF NOT EXISTS idx_file_upload_time ON file(upload_time);
CREATE INDEX IF NOT EXISTS idx_file_tag_file_id ON file_tag(file_id);
CREATE INDEX IF NOT EXISTS idx_file_tag_tag_id ON file_tag(tag_id);

由于标签和文件是多对多的关系,一个文件可能存在多个标签,一个标签也可能对应多个文件,所以标签和文件分别建立表,然后通过关联表中的联合主键来建立之间的联系。

五、效果展示

登录页面效果

图片下载功能

前端浏览图片功能

通过标签搜索功能