Python+PySide6構(gòu)建一個(gè)響應(yīng)式視頻剪輯工具
從長(zhǎng)視頻中批量剪輯出精彩片段。傳統(tǒng)的做法是打開(kāi)笨重的剪輯軟件手動(dòng)操作,效率低下。作為一個(gè)開(kāi)發(fā)者,我們自然會(huì)想:能不能寫個(gè)腳本來(lái)自動(dòng)化這個(gè)過(guò)程?
當(dāng)然可以。但如果想讓這個(gè)工具更易用,一個(gè)圖形用戶界面是必不可少的。而這恰恰引入了 GUI 編程中最經(jīng)典的挑戰(zhàn):如何在執(zhí)行耗時(shí)任務(wù)(如視頻處理)的同時(shí),保持界面的流暢響應(yīng)?
這篇文章將分享我寫的一個(gè)app.py單文件小工具,它能根據(jù)字幕文件批量剪輯視頻。并聊聊如何使用 PySide6框架,通過(guò) QThreadPool 和信號(hào)/槽機(jī)制,構(gòu)建一個(gè)高性能、不卡頓的桌面應(yīng)用。

UI線程的“生命不可承受之重”
任何 GUI 框架,無(wú)論是前端的瀏覽器、Android 的 View 系統(tǒng)還是 Qt,都有一個(gè)主線程,也叫 UI 線程。它負(fù)責(zé)處理用戶輸入、繪制界面、響應(yīng)事件。如果在這個(gè)線程里執(zhí)行一個(gè)耗時(shí)任務(wù),比如調(diào)用 ffmpeg 命令來(lái)剪輯一個(gè) 10 秒的視頻,那么在這 10 秒內(nèi),整個(gè)應(yīng)用界面將完全凍結(jié)——無(wú)法點(diǎn)擊按鈕,無(wú)法拖動(dòng)窗口,在用戶看來(lái)就是“程序卡死了”。
這就是我們首先要解決的問(wèn)題。解決方案很明確:將所有耗時(shí)操作都放到后臺(tái)工作線程中去執(zhí)行。
架構(gòu)設(shè)計(jì):QThreadPool與QRunnable
在 Qt 中,處理并發(fā)任務(wù)的首選方式之一就是 QThreadPool 和 QRunnable。
QThreadPool: 一個(gè)托管的線程池,負(fù)責(zé)管理和復(fù)用工作線程,避免了手動(dòng)創(chuàng)建和銷毀線程的開(kāi)銷。QRunnable: 一個(gè)輕量級(jí)的工作任務(wù)抽象類。我們只需要繼承它并實(shí)現(xiàn)run()方法,就可以將具體的業(yè)務(wù)邏輯(如調(diào)用 ffmpeg)封裝起來(lái)。
在我的代碼中,定義了兩個(gè)這樣的任務(wù)類:
LoadSubtitlesTask: 負(fù)責(zé)讀取和解析字幕文件。雖然這通常很快,但對(duì)于非常大的字幕文件,也可能造成瞬間卡頓,因此也將其放入后臺(tái)。ClipTask: 核心的剪輯任務(wù),封裝了對(duì)subprocess.run()的調(diào)用來(lái)執(zhí)行ffmpeg命令。
class ClipTask(QRunnable):
def __init__(self, video_path, sub, line_num, ...):
super().__init__()
# ... 初始化
def run(self):
# ... 構(gòu)建 ffmpeg 命令
cmd = ["ffmpeg", "-ss", start_time, "-i", video_path, ...]
try:
# 這是耗時(shí)操作
subprocess.run(cmd, check=True, capture_output=True)
# 任務(wù)成功了
except Exception as e:
# 任務(wù)失敗了
pass
現(xiàn)在問(wèn)題來(lái)了:任務(wù)在后臺(tái)線程執(zhí)行,但執(zhí)行的結(jié)果(成功、失敗、進(jìn)度)需要更新到主線程的 UI 控件上。直接在工作線程中操作 UI 控件是絕對(duì)禁止的,這會(huì)導(dǎo)致線程不安全,輕則界面錯(cuò)亂,重則程序崩潰。
這就是 Qt 的精髓——信號(hào)與槽(Signal/Slot)機(jī)制——登場(chǎng)的時(shí)候了。
用信號(hào)與槽實(shí)現(xiàn)線程安全通信
信號(hào)與槽是 Qt 框架的核心特性,它是一種聽(tīng)起來(lái)有些復(fù)雜但實(shí)際還算簡(jiǎn)單的觀察者模式實(shí)現(xiàn),完美地解決了跨線程通信的難題。
- Signal(信號(hào)): 當(dāng)某個(gè)特定事件發(fā)生時(shí),一個(gè)對(duì)象可以“發(fā)射(emit)”一個(gè)信號(hào)。信號(hào)可以攜帶參數(shù)。
- Slot(槽): 一個(gè)可以接收并處理信號(hào)的函數(shù)或方法。
當(dāng)一個(gè)對(duì)象的信號(hào)連接(connect)到另一個(gè)對(duì)象的槽時(shí),一旦信號(hào)被發(fā)射,與之關(guān)聯(lián)的槽函數(shù)就會(huì)被自動(dòng)調(diào)用。最關(guān)鍵的是,如果信號(hào)是從工作線程發(fā)射,而接收槽位于主線程的對(duì)象上,Qt 會(huì)自動(dòng)、安全地將這個(gè)調(diào)用安排到主線程的事件循環(huán)中執(zhí)行。
在我的代碼中,創(chuàng)建了一個(gè)專門的 Signals 類來(lái)統(tǒng)一定義所有需要用到的信號(hào):
# 必須繼承自 QObject 才能定義信號(hào)
class Signals(QObject):
# 進(jìn)度更新信號(hào),攜帶一個(gè)字符串參數(shù)
progress = Signal(str)
# 所有任務(wù)完成的信號(hào),不帶參數(shù)
finished = Signal()
# 字幕加載完成的信號(hào),攜帶解析后的數(shù)據(jù)和文件名
subtitles_loaded = Signal(list, str)
# 加載出錯(cuò)的信號(hào)
load_error = Signal(str)
工作流程:
初始化與連接:在主窗口 MainWindow 的 __init__ 方法中,實(shí)例化 Signals 類,并將信號(hào)連接到對(duì)應(yīng)的槽函數(shù)。
class MainWindow(QWidget):
def __init__(self):
# ...
self.signals = Signals()
# 將 progress 信號(hào)連接到 update_progress 槽函數(shù)
self.signals.progress.connect(self.update_progress)
self.signals.finished.connect(self.clipping_finished)
# ...
任務(wù)分發(fā)與信號(hào)發(fā)射:當(dāng)用戶點(diǎn)擊“開(kāi)始剪輯”按鈕時(shí),start_clipping 方法會(huì)為每個(gè)選中的字幕行創(chuàng)建一個(gè) ClipTask 實(shí)例,并將我們的 signals 對(duì)象傳遞給它。
# 在 start_clipping 中
for line_num in self.selected_lines:
sub = self.subtitles[line_num - 1]
# 將 signals 對(duì)象傳入任務(wù)
task = ClipTask(..., self.signals)
# 提交到線程池執(zhí)行
self.thread_pool.start(task)
后臺(tái)發(fā)射信號(hào):ClipTask 在其 run 方法中執(zhí)行完 ffmpeg 命令后,根據(jù)結(jié)果發(fā)射不同的信號(hào)。
# 在 ClipTask.run 中
class ClipTask(QRunnable):
def run(self):
try:
subprocess.run(...)
# 任務(wù)成功,從工作線程發(fā)射 progress 信號(hào)
self.signals.progress.emit(f"Completed clip {self.line_num}")
except subprocess.CalledProcessError as e:
# 任務(wù)失敗,同樣發(fā)射 progress 信號(hào),但內(nèi)容是錯(cuò)誤信息
self.signals.progress.emit(f"Failed clip {self.line_num}: {e}")
主線程安全接收并更新 UI:由于我們?cè)诘谝徊街薪⒘诉B接,主線程的 update_progress 方法會(huì)被自動(dòng)調(diào)用。我們可以在這個(gè)方法里安全地更新 QLabel 等 UI 控件。
# 在 MainWindow 中定義的槽函數(shù)
def update_progress(self, message: str):
if "Completed" in message:
self.completed_clips += 1
elif "Failed" in message:
self.failed_clips.append(message)
self.progress_label.setText(
f"共: {self.total_clips}, 完成: {self.completed_clips}, 失敗: {len(self.failed_clips)}"
)
# 檢查是否所有任務(wù)都結(jié)束了
if self.completed_clips + len(self.failed_clips) >= self.total_clips:
self.signals.finished.emit()
通過(guò)這個(gè)機(jī)制,我們成功地將耗時(shí)的業(yè)務(wù)邏輯與 UI 更新解耦,實(shí)現(xiàn)了應(yīng)用的流暢響應(yīng)。即使用戶一次性剪輯上百個(gè)片段,界面也依然能自如操作。
用uv實(shí)現(xiàn)零依賴運(yùn)行
對(duì)于 Python 開(kāi)發(fā)者來(lái)說(shuō),環(huán)境和依賴管理常常是個(gè)頭疼的問(wèn)題。為了讓這個(gè)工具對(duì)技術(shù)小白也足夠友好,我采用了最近大火的 uv 工具。
通過(guò)在腳本文件頭部添加特定的注釋,uv 能夠讀取這些元數(shù)據(jù),自動(dòng)創(chuàng)建一個(gè)虛擬環(huán)境并安裝所需的依賴,然后執(zhí)行腳本。
# /// script # requires-python = ">=3.10,<=3.12" # dependencies = [ # "pyside6", # "pysubs2" # ] # ///
這意味著用戶只需要下載 uv.exe 和 app.py,無(wú)需手動(dòng) pip install 任何東西,只需在命令行運(yùn)行 uv run app.py 即可啟動(dòng)程序。這極大地降低了使用門檻,也為代碼分發(fā)提供了一種輕量級(jí)的解決方案。
完整代碼
下面是完整的項(xiàng)目代碼,涵蓋了從 UI 布局、事件處理到并發(fā)編程和線程安全通信的各個(gè)方面。
# /// script
# requires-python = ">=3.10,<=3.12"
# dependencies = [
# "pyside6",
# "pysubs2"
# ]
#
# [[tool.uv.index]]
# url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
#
# ///
import sys
import os,shutil,platform
from pathlib import Path
import subprocess
from PySide6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QFileDialog, QListWidget, QListWidgetItem, QCheckBox,
QComboBox
)
from PySide6.QtCore import Qt, QThreadPool, QRunnable, Signal, QObject, QUrl, Slot,QSize
from PySide6.QtGui import QDesktopServices
from datetime import timedelta
import pysubs2
# 全局輸出文件夾
ROOT_DIR=Path(os.getcwd()).as_posix()
output_folder = f"{ROOT_DIR}/output"
class Signals(QObject):
progress = Signal(str)
finished = Signal()
subtitles_loaded = Signal(list, str) # list of (i, text), subtitle_name
load_error = Signal(str)
class ClipTask(QRunnable):
def __init__(self, video_path, sub, line_num, subtitle_name, signals, mode):
super().__init__()
self.video_path = video_path
self.sub = sub
self.line_num = line_num
self.subtitle_name = subtitle_name
self.signals = signals
self.mode = mode
def run(self):
try:
start_time = self.sub.start / 1000.0
duration = (self.sub.end - self.sub.start) / 1000.0
output_dir = f'{output_folder}/{self.subtitle_name}'
os.makedirs(output_dir, exist_ok=True)
if self.mode == 0: # 默認(rèn)
output_path = os.path.join(output_dir, f"{self.line_num}.mp4")
cmd = [
"ffmpeg","-y", "-ss", str(start_time), "-t", str(duration),
"-i", self.video_path, "-c:v", "copy", "-c:a", "copy",
output_path
]
subprocess.run(cmd, check=True, capture_output=True)
elif self.mode == 1: # 僅視頻
output_path = os.path.join(output_dir, f"{self.line_num}.mp4")
cmd = [
"ffmpeg","-y", "-ss", str(start_time), "-t", str(duration),
"-i", self.video_path, "-c:v", "copy", "-an",
output_path
]
subprocess.run(cmd, check=True, capture_output=True)
elif self.mode == 2: # 僅音頻
output_path = os.path.join(output_dir, f"{self.line_num}.wav")
cmd = [
"ffmpeg", "-y","-ss", str(start_time), "-t", str(duration),
"-i", self.video_path, "-vn", "-c:a", "pcm_s16le",
output_path
]
subprocess.run(cmd, check=True, capture_output=True)
elif self.mode == 3: # 分離
# 無(wú)聲視頻
video_path_out = os.path.join(output_dir, f"{self.line_num}.mp4")
cmd_video = [
"ffmpeg","-y", "-ss", str(start_time), "-t", str(duration),
"-i", self.video_path, "-c:v", "copy", "-an",
video_path_out
]
subprocess.run(cmd_video, check=True, capture_output=True)
# 音頻
audio_path_out = os.path.join(output_dir, f"{self.line_num}.wav")
cmd_audio = [
"ffmpeg","-y", "-ss", str(start_time), "-t", str(duration),
"-i", self.video_path, "-vn", "-c:a", "pcm_s16le",
audio_path_out
]
subprocess.run(cmd_audio, check=True, capture_output=True)
# 注意:Completed 和 Failed 不可修改,UI線程據(jù)此判斷成功與失敗,丑陋但簡(jiǎn)單
self.signals.progress.emit(f"Completed clip {self.line_num}")
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
self.signals.progress.emit(f"Failed clip {self.line_num}: {error_msg}")
except Exception as e:
self.signals.progress.emit(f"Failed clip {self.line_num}: {str(e)}")
class LoadSubtitlesTask(QRunnable):
def __init__(self, subtitle_path, signals):
super().__init__()
self.subtitle_path = subtitle_path
self.signals = signals
def _format_time(self,ms):
hours = ms // (1000 * 3600)
minutes = (ms // (1000 * 60)) % 60
seconds = (ms // 1000) % 60
milliseconds = ms % 1000
return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"
def run(self):
try:
subtitles = pysubs2.load(self.subtitle_path)
subtitle_name = os.path.splitext(os.path.basename(self.subtitle_path))[0]
data = []
for i, sub in enumerate(subtitles, 1):
start = self._format_time(sub.start)
end = self._format_time(sub.end)
duration = (sub.end - sub.start) / 1000.0
text = f"{start}->{end}({duration:.2f}s) {sub.text}"
data.append((i, text))
self.signals.subtitles_loaded.emit(data, subtitle_name)
except Exception as e:
self.signals.load_error.emit(str(e))
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("按字幕剪輯視頻")
self.resize(1000, 600)
self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint)
self.video_path = None
self.subtitle_path = None
self.subtitles = None
self.subtitle_name = None
self.selected_lines = []
self.thread_pool = QThreadPool()
self.is_clipping = False
self.signals = Signals()
self.signals.progress.connect(self.update_progress)
self.signals.finished.connect(self.clipping_finished)
self.signals.subtitles_loaded.connect(self.on_subtitles_loaded)
self.signals.load_error.connect(self.on_load_error)
self.total_clips = 0
self.completed_clips = 0
self.failed_clips = []
self.open_button = None
self.active_tasks = 0
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout()
# 文件選擇
file_layout = QHBoxLayout()
self.video_label = QLabel("未選視頻")
video_btn = QPushButton("選擇視頻")
video_btn.clicked.connect(self.select_video)
video_btn.setMinimumSize(QSize(200, 35))
video_btn.setCursor(Qt.PointingHandCursor)
file_layout.addWidget(video_btn)
file_layout.addWidget(self.video_label)
self.subtitle_label = QLabel("未選字幕")
subtitle_btn = QPushButton("選擇字幕")
subtitle_btn.setMinimumSize(QSize(200, 35))
subtitle_btn.clicked.connect(self.select_subtitle)
subtitle_btn.setCursor(Qt.PointingHandCursor)
file_layout.addWidget(subtitle_btn)
file_layout.addWidget(self.subtitle_label)
# 輸出模式下拉列表
self.output_mode = QComboBox()
self.output_mode.addItems([
"默認(rèn)(mp4片段有聲)",
"僅保留視頻(mp4片段無(wú)聲)",
"僅保留音頻(wav音頻片段)",
"聲畫分離(mp4片段無(wú)聲和wav音頻片段)"
])
file_layout.addWidget(self.output_mode)
file_layout.addStretch()
layout.addLayout(file_layout)
# 批量選擇按鈕
batch_layout = QHBoxLayout()
select_all_btn = QPushButton("全選")
select_all_btn.clicked.connect(self.select_all)
select_all_btn.setCursor(Qt.PointingHandCursor)
batch_layout.addWidget(select_all_btn)
deselect_all_btn = QPushButton("全不選")
deselect_all_btn.clicked.connect(self.deselect_all)
deselect_all_btn.setCursor(Qt.PointingHandCursor)
batch_layout.addWidget(deselect_all_btn)
invert_btn = QPushButton("反選")
invert_btn.clicked.connect(self.invert_selection)
invert_btn.setCursor(Qt.PointingHandCursor)
batch_layout.addWidget(invert_btn)
batch_layout.addStretch()
layout.addLayout(batch_layout)
# 字幕列表
self.subtitle_list = QListWidget()
layout.addWidget(self.subtitle_list)
# 底部按鈕
btn_layout = QHBoxLayout()
self.clip_btn = QPushButton("開(kāi)始剪輯")
self.clip_btn.setCursor(Qt.PointingHandCursor)
self.clip_btn.setMinimumSize(QSize(200, 35))
self.clip_btn.clicked.connect(self.start_clipping)
btn_layout.addWidget(self.clip_btn)
self.clear_btn = QPushButton("清除已選")
self.clear_btn.setCursor(Qt.PointingHandCursor)
self.clear_btn.setMaximumWidth(150)
self.clear_btn.clicked.connect(self.clear_all)
btn_layout.addWidget(self.clear_btn)
self.open_button = QPushButton("打開(kāi)輸出目錄")
self.open_button.setMaximumWidth(200)
self.open_button.setCursor(Qt.PointingHandCursor)
self.open_button.clicked.connect(self.open_output_folder)
self.open_button.hide()
btn_layout.addWidget(self.open_button)
layout.addLayout(btn_layout)
self.progress_label = QLabel("")
self.progress_label.setStyleSheet('color:#2196f3;font-size:14px')
layout.addWidget(self.progress_label)
self.setLayout(layout)
def select_video(self):
path, _ = QFileDialog.getOpenFileName(self, "選擇視頻", "", "Video Files (*.mp4 *.avi *.mkv)")
if path:
self.video_path = path
self.video_label.setText(os.path.basename(path))
def select_subtitle(self):
global output_folder
path, _ = QFileDialog.getOpenFileName(self, "選擇對(duì)應(yīng)字幕", "", "Subtitle Files (*.srt *.ass *.vtt)")
if path:
output_folder=Path(path).parent.as_posix()
self.subtitle_path = path
self.subtitle_label.setText(os.path.basename(path))
self.subtitle_list.clear()
self.progress_label.setText("正在渲染字幕...")
task = LoadSubtitlesTask(self.subtitle_path, self.signals)
self.thread_pool.start(task)
@Slot(list, str)
def on_subtitles_loaded(self, data, subtitle_name):
self.subtitles = pysubs2.load(self.subtitle_path) # Reload if needed
self.subtitle_name = subtitle_name
self.subtitle_list.clear()
for i, text in data:
item = QListWidgetItem()
check = QCheckBox(f"{i}行: {text}")
self.subtitle_list.addItem(item)
self.subtitle_list.setItemWidget(item, check)
self.progress_label.setText(f"字幕渲染完成.剪輯后輸出到:{output_folder}/{subtitle_name}")
@Slot(str)
def on_load_error(self, error):
self.progress_label.setText(f"字幕渲染出錯(cuò): {error}")
def select_all(self):
for i in range(self.subtitle_list.count()):
item = self.subtitle_list.item(i)
check = self.subtitle_list.itemWidget(item)
check.setChecked(True)
def deselect_all(self):
for i in range(self.subtitle_list.count()):
item = self.subtitle_list.item(i)
check = self.subtitle_list.itemWidget(item)
check.setChecked(False)
def invert_selection(self):
for i in range(self.subtitle_list.count()):
item = self.subtitle_list.item(i)
check = self.subtitle_list.itemWidget(item)
check.setChecked(not check.isChecked())
def clear_all(self):
self.video_path = None
self.subtitle_path = None
self.subtitles = None
self.subtitle_name = None
self.selected_lines = []
self.video_label.setText("未選視頻")
self.subtitle_label.setText("未選字幕")
self.subtitle_list.clear()
self.progress_label.setText("")
self.clip_btn.setText("開(kāi)始剪輯")
self.is_clipping = False
self.total_clips = 0
self.completed_clips = 0
self.failed_clips = []
self.output_mode.setCurrentIndex(0)
self.active_tasks = 0
self.open_button.hide()
def start_clipping(self):
if self.is_clipping:
self.stop_clipping()
return
if not self.video_path or not self.subtitle_name:
self.progress_label.setText("請(qǐng)選擇待剪輯視頻及對(duì)應(yīng)字幕文件.")
return
self.selected_lines = []
for i in range(self.subtitle_list.count()):
item = self.subtitle_list.item(i)
check = self.subtitle_list.itemWidget(item)
if check.isChecked():
self.selected_lines.append(i + 1) # 1-based
if not self.selected_lines:
self.progress_label.setText("至少請(qǐng)選中一行字幕.")
return
mode = self.output_mode.currentIndex()
self.is_clipping = True
self.clip_btn.setText("立即停止")
self.total_clips = len(self.selected_lines)
self.completed_clips = 0
self.failed_clips = []
self.open_button.show()
self.active_tasks = self.total_clips
self.progress_label.setText(f"共: {self.total_clips}, 完成: 0, 失敗: 0")
for line_num in self.selected_lines:
sub = self.subtitles[line_num - 1]
task = ClipTask(self.video_path, sub, line_num, self.subtitle_name, self.signals, mode)
self.thread_pool.start(task)
def stop_clipping(self):
self.thread_pool.clear()
self.is_clipping = False
self.clip_btn.setText("開(kāi)始剪輯")
self.progress_label.setText("立即停止.")
self.active_tasks = 0
def update_progress(self, message):
if "Completed" in message:
self.completed_clips += 1
elif "Failed" in message:
self.failed_clips.append(message)
self.active_tasks -= 1
self.progress_label.setText(
f"共: {self.total_clips}, 完成: {self.completed_clips}, "
f"失敗: {len(self.failed_clips)}\n" + "\n".join(self.failed_clips)
)
if self.active_tasks <= 0 and self.is_clipping:
self.signals.finished.emit()
def clipping_finished(self):
self.is_clipping = False
self.clip_btn.setText("開(kāi)始剪輯")
self.active_tasks = 0
def open_output_folder(self):
output_dir = f'{output_folder}/{self.subtitle_name}'
QDesktopServices.openUrl(QUrl.fromLocalFile(output_dir))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
希望這個(gè)實(shí)例能為你下次構(gòu)建桌面應(yīng)用時(shí),在處理并發(fā)和線程通信方面提供一些有用的參考。
到此這篇關(guān)于Python+PySide6構(gòu)建一個(gè)響應(yīng)式視頻剪輯工具的文章就介紹到這了,更多相關(guān)Python視頻剪輯內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python?Django源碼運(yùn)行過(guò)程解析
這篇文章主要介紹了Python?Django源碼運(yùn)行過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08
Python實(shí)現(xiàn)的選擇排序算法原理與用法實(shí)例分析
這篇文章主要介紹了Python實(shí)現(xiàn)的選擇排序算法,簡(jiǎn)單描述了選擇排序的原理,并結(jié)合實(shí)例形式分析了Python實(shí)現(xiàn)與應(yīng)用選擇排序的具體操作技巧,需要的朋友可以參考下2017-11-11
python中isoweekday和weekday的區(qū)別及說(shuō)明
這篇文章主要介紹了python中isoweekday和weekday的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
Python開(kāi)發(fā)的實(shí)用計(jì)算器完整實(shí)例
這篇文章主要介紹了Python開(kāi)發(fā)的實(shí)用計(jì)算器,結(jié)合完整實(shí)例形式分析了Python實(shí)現(xiàn)計(jì)算器四則運(yùn)算、開(kāi)方、取余等相關(guān)操作技巧,需要的朋友可以參考下2017-05-05
python網(wǎng)絡(luò)編程學(xué)習(xí)筆記(三):socket網(wǎng)絡(luò)服務(wù)器
服務(wù)器和客戶端程序很類似,上節(jié)學(xué)習(xí)了客戶端程序,這一節(jié)將仔細(xì)學(xué)習(xí)一下利用socket建立TCP服務(wù)器和UDP服務(wù)器2014-06-06
django 開(kāi)發(fā)忘記密碼通過(guò)郵箱找回功能示例
這篇文章主要介紹了django 開(kāi)發(fā)忘記密碼通過(guò)郵箱找回功能示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04
Python結(jié)合PyQt5實(shí)現(xiàn)MD(Markdown)轉(zhuǎn)DOCX工具
這篇文章主要為大家詳細(xì)介紹了Python如何結(jié)合PyQt5實(shí)現(xiàn)一個(gè)MD(Markdown)轉(zhuǎn)DOCX工具,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-07-07

