当前位置: 首页 > news >正文

Python实现视频播放器

Python实现视频播放器

Python实现视频播放器,在如下博文中介绍过

Python实现本地视频/音频播放器https://blog.csdn.net/cnds123/article/details/137874107

Python简单GUI程序示例 中 “四、视频播放器” https://blog.csdn.net/cnds123/article/details/122903311

现在再补充介绍一个。读者可以学习比较之。

预备知识

tkinter 是 Python 的标准库之一,用于创建图形用户界面(GUI)。它不需要单独安装,因为它是 Python 安装时自带的模块。

ttk 是 tkinter 的扩展模块,提供了更现代的 GUI 控件(如样式化的按钮、下拉菜单等)。

Style 是 ttk 模块的一部分,用于自定义 GUI 控件的样式。

time 是 Python 的标准库,用于处理时间相关的操作。

threading 是 Python 的标准库,用于实现多线程编程。

需要安装第三方库:opencv-python、pillow、ffpyplayer

cv2 是 OpenCV(Open Source Computer Vision Library)的 Python 接口,用于计算机视觉任务,如图像处理、视频处理等。

pip install opencv-python

Pillow 是 Python Imaging Library(PIL)的一个分支,用于处理图像。

pip install pillow

ffpyplayer 是一个基于 FFmpeg 的 Python 库,用于播放音频和视频。

pip install ffpyplayer

本文介绍了Python实现视频播放器及其改进版本

先看第一个效果图:

源码如下:

import cv2
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageSequence
from tkinter import ttk
from ffpyplayer.player import MediaPlayer
import time
import threading
from tkinter.ttk import Style

class VideoPlayTk:
    def __init__(self):
        self.__win = tk.Tk()
        self.__canvas = None     # 画布变量
        self.__videoWH = [1920, 1080]  # 窗口大小
        self.__num1 = 0.6
        # 窗口缩小系数,默认为0.6,最大最好1
        self.__data = list()
        self.__uploadImgSize = [150, 150]  # 一个加载视频的gif图片尺寸
        self.__canvasWH = []     # 画布的宽度 和 高度
        self.__canvasImg = None  # 画布图片
        self.__canvasXY = []     # 图片插入到canvas的具体左上角坐标x,y
        self.__progressBar = None  # 进度条
        self.__Llabel = None   # 左边显示时间的标签
        self.__Rlabel = None   # 右边显示时间的标签
        self.__player = None   # 音频播放 变量
        self.__scale = None   # 调节音量的控件
        self.__stopBtn = None   # 暂停按钮
        self.__stopState = ''  # 一个控制相关线程的变量
        self.__running = True  # 控制线程运行的标志

    def sectionOne(self):
        width = int(self.__videoWH[0] * self.__num1)
        height = int(self.__videoWH[1] * self.__num1)
        self.__canvasWH = [width, height]
        self.__win.title('视频播放器')
        self.__win.geometry(f'{width}x{height + 90}')
        self.__win.resizable(height=0, width=0)
        # 窗口不可放大
        # 菜单
        self.__win.iconbitmap('./example.ico')

        mainMenu = tk.Menu(self.__win)
        fileMenu = tk.Menu(mainMenu, tearoff=False)
        # 不添加分割线
        fileMenu.add_command(label='打开', command=self.__openDir)

        fileMenu.add_separator()
        # 设置分割线
        fileMenu.add_command(label='退出', command=self.__on_close)
        mainMenu.add_cascade(label='文件', menu=fileMenu)
        # 在主目录菜单上新增“文件”选项,并通过menu参数与下拉菜单绑定
        self.__win.config(menu=mainMenu)
        # 将主菜单设置在窗口上
        # 画布
        self.__canvas = tk.Canvas(self.__win, bg='black', width=width, height=height)
        self.__canvas.grid(row=0, column=0, columnspan=3)

        self.__Llabel = tk.Label(self.__win, text='00:00:00', font=('华文宋体', 12))
        self.__Llabel.grid(row=1, column=0)

        # 进度条
        style = Style()
        style.theme_use('clam')
        style.configure("my0.Horizontal.TProgressbar", troughcolor='white', background='red')
        # 设置进度条的颜色
        progressBar = ttk.Progressbar(self.__win, orient=tk.HORIZONTAL,style='my0.Horizontal.TProgressbar')
        progressBar.grid(row=1, column=1)
        progressBar['length'] = width - 200
        progressBar['value'] = 0

        self.__progressBar = progressBar

        self.__Rlabel = tk.Label(self.__win, text='00:00:00', font=('华文宋体', 12))
        self.__Rlabel.grid(row=1, column=2)

        self.__stopBtn = tk.Button(self.__win,text='暂停',font=('华文宋体', 12),command=self.__fun5)
        self.__stopBtn.grid(row=2,column=0,sticky=tk.E)
        # 音量
        self.__scale = tk.Scale(self.__win, from_=0, to=100, resolution=1, length=120, orient=tk.HORIZONTAL,
                                font=('华文宋体', 12),command=self.__fun2)
        self.__scale.grid(row=2, column=1, sticky=tk.E)
        self.__scale.set(100)

        self.__win.protocol("WM_DELETE_WINDOW", self.__on_close)
        self.__win.mainloop()

    def __on_close(self):
        self.__running = False  # 停止线程
        if self.__player is not None:
            self.__player.close_player()
        self.__win.quit()
        self.__win.destroy()  # 确保窗口被销毁

    # 暂停播放的方法
    def __fun5(self):
        if self.__player is not None:
            self.__player.set_pause(True)
            # 点击暂停按钮,视频播放暂停
            self.__stopState = 'stop'
            # 设置暂停播放状态

    # 调节音量的方法
    def __fun2(self, value):
        if self.__player is not None:
            self.__player.set_volume(int(value) / 100)
            # value 值的范围为0~100,set_volume方法参数取值为0.0 ~ 1.0

    # 将总的时间(单位秒)进行相关格式化  如 119 为 00:01:59
    def __getTime(self,totleTime):
        hour = 0
        minute = totleTime//60
        if minute > 60:
            hour = minute // 60
        second = int(totleTime%60)

        return '{:02d}:{:02d}:{:02d}'.format(hour,minute,second)

    def __openDir(self):
        fileName = filedialog.askopenfilename()
        # 视频文件的绝对路径
        if fileName != '':
            cap = cv2.VideoCapture(filename=fileName)
            self.__data = list()
            w = cap.get(propId=cv2.CAP_PROP_FRAME_WIDTH)
            h = cap.get(propId=cv2.CAP_PROP_FRAME_HEIGHT)
            fps = cap.get(propId=cv2.CAP_PROP_FPS)
            count = cap.get(propId=cv2.CAP_PROP_FRAME_COUNT)
            print('时长:',int(count*(1/fps)))
            self.__data = [int(w), int(h), int(fps), int(count),int(count*(1/fps))]
            print(self.__data)
            # 使用cv2获取视频的尺寸,比如1920*1080,帧率(1秒以内变换图片的张数),图片的总数,总时长
            canvas_x = 0
            canvas_y = 0
            if w / self.__canvasWH[0] == h / self.__canvasWH[1]:
                print('图片大小1920*1080')
                bi = w / self.__canvasWH[0]
            else:
                if w / self.__canvasWH[0] > h / self.__canvasWH[1]:
                    bi = w / self.__canvasWH[0]
                    canvas_y = int((self.__canvasWH[1] - h / bi) // 2)
                else:
                    bi = h / self.__canvasWH[1]
                    canvas_x = int((self.__canvasWH[0] - w / bi) // 2)

            self.__canvasXY = [canvas_x, canvas_y]
            # 根据视频的尺度用于确定画布 图片左上角坐标

            # 下面这一段代码可以注释,只是实现播放视频时实现视频加载图标
            w = self.__canvasWH[0]
            h = self.__canvasWH[1]
            x = (w - self.__uploadImgSize[0]) // 2
            y = (h - self.__uploadImgSize[1]) // 2
            self.__canvas.delete(tk.ALL)

            img_i = 0
            while img_i < 5:
                uploadImg = Image.open('upload.gif')
                iter = ImageSequence.Iterator(uploadImg)
                for frame in iter:
                    pic = ImageTk.PhotoImage(frame)
                    self.__canvas.create_image(x, y, image=pic, anchor=tk.W)
                    time.sleep(0.01)
                    self.__canvas.update()
                img_i += 1

            size = (int(self.__data[0] / bi), int(self.__data[1] / bi))
            self.__player = MediaPlayer(filename=fileName)
          
            thread1 = threading.Thread(target=self.__fun3)
            # 时间加载的线程
            thread2 = threading.Thread(target=self.__fun4)
            # 时间进度条的线程
            thread1.start()
            thread2.start()
            print('执行主程序!')
            self.__fun1(size)
            self.__player.close_player()
            cap.release()

    # 时间进度条
    def __fun4(self):
        progressBar = self.__progressBar
        progressBar['maximum'] = self.__data[4]
        progressBar['value'] = 0
        for i in range(self.__data[4]+1):
            if not self.__running:
                break
            if self.__stopState == 'stop':
                break
            progressBar['value'] += 1
            time.sleep(1)

    # 时间展示
    def __fun3(self):
        print(self.__getTime(self.__data[4]))
        self.__Rlabel['text'] = self.__getTime(self.__data[4])
        second1 = 0
        minute1 = 0
        hour1 = 0
        for i in range(self.__data[4]+1):
            if not self.__running:
                break
            if second1 != 0 and second1 == 60:
                minute1 += 1
                second1 = 0

            if minute1 == 59 and second1 == 60:
                hour1 += 1
                minute1 = 0
                second1 = 0

            if self.__stopState == 'stop':
                break

            self.__Llabel['text'] = '{:02d}:{:02d}:{:02d}'.format(hour1,minute1,second1)
            time.sleep(1)
            second1 += 1

    # 播放图片
    def __fun1(self, size):
        i = 0
        self.__canvas.delete(tk.ALL)
        self.__stopState = ''
        while i < self.__data[3]:
            if not self.__running:
                break
            frame, val = self.__player.get_frame()
            if self.__stopState == 'stop':
                break
            if val == 'eof':
                self.__player.set_pause(True) # 需要!不信你试试
                break
            elif frame is None:
                if i != 0:
                    self.__player.set_pause(True)
                    # self.__player.close_player()
                    break
                else:
                    time.sleep(0.01)
            else:
                image, pts = frame
                bytes_1 = bytes(image.to_bytearray()[0])
                img2 = Image.frombytes("RGB", image.get_size(), bytes_1)
                img2 = img2.resize(size=size,resample=Image.ANTIALIAS)
                self.__canvasImg = ImageTk.PhotoImage(image=img2)
                self.__canvas.create_image(self.__canvasXY[0], self.__canvasXY[1], image=self.__canvasImg, anchor=tk.NW)
                self.__canvas.update()
                time.sleep(1/self.__data[2])
                i += 1

if __name__ == '__main__':
    a = VideoPlayTk()
    a.sectionOne()

改进,添加“继续”播放按钮

先看效果图:

源码如下:

import cv2
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageSequence
from tkinter import ttk
from ffpyplayer.player import MediaPlayer
import time
import threading
from tkinter.ttk import Style

class VideoPlayTk:
    def __init__(self):
        self.__win = tk.Tk()
        self.__canvas = None     # 画布变量
        self.__videoWH = [1920, 1080]  # 窗口大小
        self.__num1 = 0.6
        # 窗口缩小系数,默认为0.6,最大最好1
        self.__data = list()
        self.__uploadImgSize = [150, 150]  # 一个加载视频的gif图片尺寸
        self.__canvasWH = []     # 画布的宽度 和 高度
        self.__canvasImg = None  # 画布图片
        self.__canvasXY = []     # 图片插入到canvas的具体左上角坐标x,y
        self.__progressBar = None  # 进度条
        self.__Llabel = None   # 左边显示时间的标签
        self.__Rlabel = None   # 右边显示时间的标签
        self.__player = None   # 音频播放 变量
        self.__scale = None   # 调节音量的控件
        self.__stopBtn = None   # 暂停按钮
        self.__resumeBtn = None  # 继续按钮
        self.__stopState = ''  # 一个控制相关线程的变量
        self.__running = True  # 控制线程运行的标志
        self.__current_frame = 0  # 当前播放的帧数
        self.__video_thread = None  # 视频播放线程
        self.__progress_thread = None  # 进度条更新线程
        self.__progress_value = 0  # 保存进度条的当前值
        self.__time_value = 0  # 保存时间标签的当前值
        self.__video_finished = False  # 视频播放结束标志

    def sectionOne(self):
        width = int(self.__videoWH[0] * self.__num1)
        height = int(self.__videoWH[1] * self.__num1)
        self.__canvasWH = [width, height]
        self.__win.title('视频播放器')
        self.__win.geometry(f'{width}x{height + 120}')  # 增加高度以容纳更多按钮
        self.__win.resizable(height=0, width=0)
        # 窗口不可放大
        # 菜单
        self.__win.iconbitmap('./example.ico')

        mainMenu = tk.Menu(self.__win)
        fileMenu = tk.Menu(mainMenu, tearoff=False)
        # 不添加分割线
        fileMenu.add_command(label='打开', command=self.__openDir)

        fileMenu.add_separator()
        # 设置分割线
        fileMenu.add_command(label='退出', command=self.__on_close)
        mainMenu.add_cascade(label='文件', menu=fileMenu)
        # 在主目录菜单上新增“文件”选项,并通过menu参数与下拉菜单绑定
        self.__win.config(menu=mainMenu)  
        # 将主菜单设置在窗口上
        # 画布
        self.__canvas = tk.Canvas(self.__win, bg='black', width=width, height=height)
        self.__canvas.grid(row=0, column=0, columnspan=3)

        self.__Llabel = tk.Label(self.__win, text='00:00:00', font=('华文宋体', 12))
        self.__Llabel.grid(row=1, column=0)

        # 进度条
        style = Style()
        style.theme_use('clam')
        style.configure("my0.Horizontal.TProgressbar", troughcolor='white', background='red')
        # 设置进度条的颜色
        progressBar = ttk.Progressbar(self.__win, orient=tk.HORIZONTAL, style='my0.Horizontal.TProgressbar')
        progressBar.grid(row=1, column=1)
        progressBar['length'] = width - 200
        progressBar['value'] = 0

        self.__progressBar = progressBar

        self.__Rlabel = tk.Label(self.__win, text='00:00:00', font=('华文宋体', 12))
        self.__Rlabel.grid(row=1, column=2)

        # 暂停按钮
        self.__stopBtn = tk.Button(self.__win, text='暂停', font=('华文宋体', 12), command=self.__pause_video)
        self.__stopBtn.grid(row=2, column=0, sticky=tk.E)

        # 继续按钮
        self.__resumeBtn = tk.Button(self.__win, text='继续', font=('华文宋体', 12), command=self.__resume_video)
        self.__resumeBtn.grid(row=2, column=1)  # , sticky=tk.E

        # 音量
        self.__scale = tk.Scale(self.__win, from_=0, to=100, resolution=1, length=120, orient=tk.HORIZONTAL,
                                font=('华文宋体', 12), command=self.__adjust_volume)
        self.__scale.grid(row=2, column=2) # , sticky=tk.E
        self.__scale.set(100)

        self.__win.protocol("WM_DELETE_WINDOW", self.__on_close)
        self.__win.mainloop()

    def __on_close(self):
        self.__running = False  # 停止线程
        if self.__player is not None:
            self.__player.close_player()
        self.__win.quit()
        self.__win.destroy()  # 确保窗口被销毁

    # 暂停视频
    def __pause_video(self):
        if self.__player is not None:
            self.__player.set_pause(True)
            self.__stopState = 'stop'  # 设置暂停状态
            self.__progress_value = self.__progressBar['value']  # 保存进度条的当前值
            self.__time_value = self.__get_seconds_from_time(self.__Llabel['text'])  # 保存时间标签的当前值

    # 继续视频
    def __resume_video(self):
        if self.__player is not None:
            self.__player.set_pause(False)
            self.__stopState = ''  # 清除暂停状态
            # 如果视频播放线程已经结束,重新启动它
            if self.__video_thread is None or not self.__video_thread.is_alive():
                size = (int(self.__data[0] / (self.__data[0] / self.__canvasWH[0])), int(self.__data[1] / (self.__data[1] / self.__canvasWH[1])))
                self.__video_thread = threading.Thread(target=self.__play_video, args=(size,))
                self.__video_thread.start()
            # 如果进度条线程已经结束,重新启动它
            if self.__progress_thread is None or not self.__progress_thread.is_alive():
                self.__progress_thread = threading.Thread(target=self.__update_progress)
                self.__progress_thread.start()
            # 如果时间标签线程已经结束,重新启动它
            if not hasattr(self, '__time_thread') or not self.__time_thread.is_alive():
                self.__time_thread = threading.Thread(target=self.__update_time)
                self.__time_thread.start()

    # 调节音量的方法
    def __adjust_volume(self, value):
        if self.__player is not None:
            self.__player.set_volume(int(value) / 100)
            # value 值的范围为0~100,set_volume方法参数取值为0.0 ~ 1.0

    # 将总的时间(单位秒)进行相关格式化  如 119 为 00:01:59
    def __getTime(self, totleTime):
        hour = 0
        minute = totleTime // 60
        if minute > 60:
            hour = minute // 60
        second = int(totleTime % 60)

        return '{:02d}:{:02d}:{:02d}'.format(hour, minute, second)

    # 将时间字符串转换为秒数
    def __get_seconds_from_time(self, time_str):
        h, m, s = map(int, time_str.split(':'))
        return h * 3600 + m * 60 + s

    def __openDir(self):
        fileName = filedialog.askopenfilename()
        # 视频文件的绝对路径
        if fileName != '':
            # 停止当前视频播放
            self.__running = False
            if self.__player is not None:
                self.__player.close_player()
            if self.__video_thread is not None and self.__video_thread.is_alive():
                self.__video_thread.join()
            if self.__progress_thread is not None and self.__progress_thread.is_alive():
                self.__progress_thread.join()
            if hasattr(self, '__time_thread') and self.__time_thread.is_alive():
                self.__time_thread.join()

            # 重置状态变量
            self.__running = True
            self.__stopState = ''
            self.__progress_value = 0
            self.__time_value = 0
            self.__video_finished = False

            # 重置时间轴和进度条
            self.__Llabel['text'] = '00:00:00'
            self.__Rlabel['text'] = '00:00:00'
            self.__progressBar['value'] = 0

            cap = cv2.VideoCapture(filename=fileName)
            self.__data = list()
            w = cap.get(propId=cv2.CAP_PROP_FRAME_WIDTH)
            h = cap.get(propId=cv2.CAP_PROP_FRAME_HEIGHT)
            fps = cap.get(propId=cv2.CAP_PROP_FPS)
            count = cap.get(propId=cv2.CAP_PROP_FRAME_COUNT)
            print('时长:', int(count * (1 / fps)))
            self.__data = [int(w), int(h), int(fps), int(count), int(count * (1 / fps))]
            print(self.__data)
            # 使用cv2获取视频的尺寸,比如1920*1080,帧率(1秒以内变换图片的张数),图片的总数,总时长
            canvas_x = 0
            canvas_y = 0
            if w / self.__canvasWH[0] == h / self.__canvasWH[1]:
                print('图片大小1920*1080')
                bi = w / self.__canvasWH[0]
            else:
                if w / self.__canvasWH[0] > h / self.__canvasWH[1]:
                    bi = w / self.__canvasWH[0]
                    canvas_y = int((self.__canvasWH[1] - h / bi) // 2)
                else:
                    bi = h / self.__canvasWH[1]
                    canvas_x = int((self.__canvasWH[0] - w / bi) // 2)

            self.__canvasXY = [canvas_x, canvas_y]
            # 根据视频的尺度用于确定画布 图片左上角坐标

            # 下面这一段代码可以注释,只是实现播放视频时实现视频加载图标
            w = self.__canvasWH[0]
            h = self.__canvasWH[1]
            x = (w - self.__uploadImgSize[0]) // 2
            y = (h - self.__uploadImgSize[1]) // 2
            self.__canvas.delete(tk.ALL)

            img_i = 0
            while img_i < 5:
                uploadImg = Image.open('upload.gif')
                iter = ImageSequence.Iterator(uploadImg)
                for frame in iter:
                    pic = ImageTk.PhotoImage(frame)
                    self.__canvas.create_image(x, y, image=pic, anchor=tk.W)
                    time.sleep(0.01)
                    self.__canvas.update()
                img_i += 1

            size = (int(self.__data[0] / bi), int(self.__data[1] / bi))
            self.__player = MediaPlayer(filename=fileName)

            self.__video_thread = threading.Thread(target=self.__play_video, args=(size,))
            self.__video_thread.start()

            self.__progress_thread = threading.Thread(target=self.__update_progress)
            self.__progress_thread.start()

            self.__time_thread = threading.Thread(target=self.__update_time)
            self.__time_thread.start()

            print('执行主程序!')
            cap.release()

    # 时间进度条
    def __update_progress(self):
        progressBar = self.__progressBar
        progressBar['maximum'] = self.__data[4]
        progressBar['value'] = self.__progress_value  # 从保存的位置恢复进度条
        for i in range(self.__progress_value, self.__data[4] + 1):
            if not self.__running or self.__video_finished:
                break
            if self.__stopState == 'stop':
                break
            progressBar['value'] = i
            time.sleep(1)

    # 时间展示
    def __update_time(self):
        self.__Rlabel['text'] = self.__getTime(self.__data[4])
        second1 = self.__time_value % 60
        minute1 = (self.__time_value // 60) % 60
        hour1 = self.__time_value // 3600
        for i in range(self.__time_value, self.__data[4] + 1):
            if not self.__running or self.__video_finished:
                break
            if second1 != 0 and second1 == 60:
                minute1 += 1
                second1 = 0

            if minute1 == 59 and second1 == 60:
                hour1 += 1
                minute1 = 0
                second1 = 0

            if self.__stopState == 'stop':
                break

            self.__Llabel['text'] = '{:02d}:{:02d}:{:02d}'.format(hour1, minute1, second1)
            time.sleep(1)
            second1 += 1
            self.__time_value += 1

    # 播放视频
    def __play_video(self, size):
        i = 0
        self.__canvas.delete(tk.ALL)
        self.__stopState = ''
        while i < self.__data[3]:
            if not self.__running:
                break
            frame, val = self.__player.get_frame()
            if self.__stopState == 'stop':
                break
            if val == 'eof':
                self.__video_finished = True  # 设置视频播放结束标志
                self.__player.set_pause(True) # 需要!不信你试试
                break
            elif frame is None:
                if i != 0:
                    self.__player.set_pause(True)
                    break
                else:
                    time.sleep(0.01)
            else:
                image, pts = frame
                bytes_1 = bytes(image.to_bytearray()[0])
                img2 = Image.frombytes("RGB", image.get_size(), bytes_1)
                img2 = img2.resize(size=size, resample=Image.ANTIALIAS)
                self.__canvasImg = ImageTk.PhotoImage(image=img2)
                self.__canvas.create_image(self.__canvasXY[0], self.__canvasXY[1], image=self.__canvasImg, anchor=tk.NW)
                self.__canvas.update()
                time.sleep(1 / self.__data[2])
                i += 1

if __name__ == '__main__':
    a = VideoPlayTk()
    a.sectionOne()

OK!

相关文章:

  • 数据结构:二叉树的链式结构及相关算法详解
  • 通过百度构建一个智能体
  • 【Django自学】Django入门:如何使用django开发一个web项目(非常详细)
  • 使用tkinter有UI方式来拷贝Excel文件
  • 1629 按键持续时间最长的键
  • leetcode:2164. 对奇偶下标分别排序(python3解法)
  • 使用PDFMiner.six解析PDF数据
  • python-leetcode-删除并获得点数
  • AI Angent=智能体?
  • 统计有序矩阵中的负数
  • 新建菜单项的创建之CmpGetValueListFromCache函数分析
  • nuxt常用组件库html-validator应用解析
  • 使用 Selenium 和 Requests 自动化获取动态 Referer 和 Sign 的完整指南
  • 商淘云B2B2C系统 一款支持商家也能分销的多用户商城
  • 神经网络AI原理回顾
  • (YOLOv11)基于Vue Flask YOLOv11的水稻病害检测系统【含有数据大屏展示】
  • MapReduce编程模型
  • 【 实战案例篇三】【某金融信息系统项目管理案例分析】
  • 2.5 运算符2
  • Leetcode2414:最长的字母序连续子字符串的长度
  • 网站制作前需要进行规划设计/百度推广电话销售话术
  • php网站开发员工资/重庆seo整站优化报价
  • 海宁公司做网站/网站建设排名优化
  • 网站改成自适应/百度seo综合查询
  • 巩义专业网站建设价格/教育培训机构报名
  • 如何增加网站收录/小红书怎么做关键词排名优化