基于Python+SQLite編寫一個本機程序啟動器
前言
最近做了一個小工具:程序啟動器。功能聽起來很樸素——掃描本機安裝過的程序,做成一個可以搜索、雙擊運行的列表,跟 Windows 自帶的開始菜單搜索有點像。但真正把它做"能用、能打包、能長期維護"的過程中,踩了幾個很典型的坑:
- 程序打包成 exe 之后,配置文件和數(shù)據(jù)庫到底應(yīng)該存哪里?
- 掃描"已安裝程序"這件事,到底該掃哪些數(shù)據(jù)源才算全?
- GUI 程序里做耗時操作(掃描),怎么才能不卡界面?
- SQLite 怎么設(shè)計才能做到"重復(fù)掃描不產(chǎn)生臟數(shù)據(jù)"?
這篇文章就把這個項目的源碼從頭到尾拆一遍,重點講清楚"為什么這么寫",而不只是"寫了什么"。完整代碼大約 770 行,技術(shù)棧是 wxPython(GUI)+ SQLite(數(shù)據(jù))+ JSON(配置)+ threading(后臺任務(wù)),最終用 PyInstaller 打包成單文件 exe。

一、需求拆解與整體架構(gòu)
把需求拆成四塊,正好對應(yīng)代碼里的四個模塊:
| 需求 | 對應(yīng)模塊 |
|---|---|
| 找到本機裝了哪些程序 | scan_start_menu / scan_registry / scan_command_line_tools |
| 存起來,支持搜索 | DBManager(SQLite) |
| 記住用戶的操作習慣 | ConfigManager(JSON) |
| 界面 + 交互 | MainFrame(wxPython) |
數(shù)據(jù)流是一條很直白的單向管道:
系統(tǒng)數(shù)據(jù)源(開始菜單/注冊表/PATH)
│ scan_*()
▼
[ (name, path), ... ] ← 內(nèi)存中的元組列表
│ db.bulk_insert_scanned()
▼
SQLite (apps.db)
│ db.search(keyword)
▼
wx.ListCtrl 界面渲染
理解了這條鏈路,后面看代碼就不會散。
二、最容易被忽視的坑:exe 打包后到底該往哪寫文件
這是我在需求里被特別強調(diào)的一點:不管是直接跑 .py,還是打包成 .exe,配置文件和數(shù)據(jù)庫都要落在"程序自身所在的文件夾"。這個需求聽起來簡單,但背后有個經(jīng)典陷阱。
很多人的第一反應(yīng)是用 os.getcwd()(當前工作目錄),但這是錯的——如果用戶從別的目錄、或者創(chuàng)建了桌面快捷方式來啟動這個 exe,工作目錄很可能根本不是 exe 所在的文件夾,配置文件就會莫名其妙地散落到別處。
第二個常見錯誤是在 PyInstaller 單文件模式(-F)下使用 sys._MEIPASS。_MEIPASS 指向的是程序啟動時解壓出來的臨時目錄,每次運行都會變、程序退出后還會被清理,用它來存數(shù)據(jù)庫等于"每次都是一個新數(shù)據(jù)庫"。
正確的做法是區(qū)分"是否被凍結(jié)(frozen)":
def get_base_path() -> str:
"""返回程序自身所在目錄(用于存放配置文件和數(shù)據(jù)庫文件)"""
if getattr(sys, "frozen", False):
# PyInstaller 打包后運行(無論 -F 單文件還是 -D 目錄模式)
# sys.executable 始終指向"最終生成的 exe 文件"本身的位置,
# 而不是 -F 模式下的臨時解壓目錄(sys._MEIPASS 才是臨時目錄,這里不能用它)。
base = os.path.dirname(os.path.abspath(sys.executable))
else:
# 直接以 .py 方式運行
base = os.path.dirname(os.path.abspath(__file__))
return base
BASE_DIR = get_base_path()
DB_PATH = os.path.join(BASE_DIR, DB_FILENAME)
CONFIG_PATH = os.path.join(BASE_DIR, CONFIG_FILENAME)
關(guān)鍵點在 sys.frozen 這個標志:PyInstaller、cx_Freeze 等打包工具在運行時都會給 sys 對象加上這個屬性。只要判斷出"我是被打包過的",就用 sys.executable(exe 自身的真實路徑,而不是臨時解壓路徑)來定位目錄;否則退回到 __file__(腳本自身路徑)。兩個分支殊途同歸——最終都拿到"程序所在的文件夾",DB_PATH 和 CONFIG_PATH 在模塊加載時就被確定為全局常量,后面所有讀寫都基于這兩個絕對路徑,不會再受工作目錄影響。
三、數(shù)據(jù)從哪來:三路掃描的設(shè)計
這是整個項目里最有意思的部分。一開始我只做了兩路掃描:
- 開始菜單快捷方式(
.lnk文件) - 注冊表卸載列表(
...\Uninstall鍵)
跑起來之后發(fā)現(xiàn)一個問題:像 Claude Code、Codex 這類通過 npm install -g 裝的命令行工具,兩路都掃不到。原因也很直接——這類工具的安裝方式跟傳統(tǒng) GUI 軟件完全不同:npm 只是往全局目錄里丟一個可執(zhí)行文件(Windows 下是 .cmd shim),再把這個目錄塞進 PATH 環(huán)境變量,既不建快捷方式,也不寫注冊表。這倒逼出了第三路掃描。三路掃描各自的實現(xiàn)思路如下。
3.1 開始菜單掃描:最簡單也最可靠
def scan_start_menu():
results = []
candidates = []
program_data = os.environ.get("ProgramData")
app_data = os.environ.get("APPDATA")
if program_data:
candidates.append(os.path.join(program_data, "Microsoft", "Windows", "Start Menu", "Programs"))
if app_data:
candidates.append(os.path.join(app_data, "Microsoft", "Windows", "Start Menu", "Programs"))
for base in candidates:
if not os.path.isdir(base):
continue
for root, _dirs, files in os.walk(base):
for f in files:
if f.lower().endswith(".lnk"):
full_path = os.path.join(root, f)
name = os.path.splitext(f)[0]
results.append((name, full_path))
return results
這里有個討巧的地方:不需要解析 .lnk 文件指向的真實目標(那需要額外依賴 pywin32 去調(diào)用 COM 接口 WScript.Shell)。因為 Windows Shell 本身就認識 .lnk 文件,直接用 os.startfile(lnk_path) 就能像雙擊圖標一樣正確啟動,所以掃描階段只需要把 .lnk 的路徑原樣存下來。這個決定省掉了一個第三方依賴,也是"少即是多"的一個例子。
ProgramData 目錄下是所有用戶共享的開始菜單項,APPDATA 下是當前用戶私有的,兩個都要掃,用 os.walk 遞歸是因為開始菜單里經(jīng)常有分類子文件夾(比如"Microsoft Office")。
3.2 注冊表掃描:拿到"官方認證"的安裝列表
def scan_registry():
if not IS_WINDOWS:
return []
results = []
roots = [
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
]
for hive, path in roots:
try:
key = winreg.OpenKey(hive, path)
except FileNotFoundError:
continue
...
這里有幾個細節(jié)值得展開:
- 為什么要查三個根鍵,而不是一個?
SOFTWARE\...\Uninstall是 64 位程序注冊的地方;32 位程序在 64 位系統(tǒng)上會被重定向到WOW6432Node子鍵下;還有些程序(尤其是"僅當前用戶安裝"的)寫在HKEY_CURRENT_USER而不是HKEY_LOCAL_MACHINE。漏掉任何一個都會讓掃描結(jié)果不完整。 SystemComponent過濾:注冊表里除了真正的應(yīng)用程序,還有大量.NET 運行庫、驅(qū)動補丁之類的"系統(tǒng)組件",它們也會出現(xiàn)在同一個 Uninstall 列表里。微軟的約定是給這類條目打上SystemComponent=1,代碼里直接把它們過濾掉,避免列表被無意義的補丁項淹沒。- 可執(zhí)行文件路徑的兩級兜底:優(yōu)先從
DisplayIcon字段拿(它通常直接指向 exe,但可能帶,0這種圖標索引后綴,需要split(",")[0]切掉);如果這個字段沒有或者指向的文件不存在,再退而求其次,去InstallLocation目錄下找第一個.exe文件。這種"多級兜底"的寫法在處理系統(tǒng) API/注冊表這種非結(jié)構(gòu)化數(shù)據(jù)源時很常見——你沒法假設(shè)每個字段都一定存在,只能層層降級。
3.3 PATH 掃描:為了 CLI 工具專門加的一路
def scan_command_line_tools():
if not IS_WINDOWS:
return []
exclude_keywords = ("system32", "syswow64", "windowspowershell", "\\windows\\", "wbem")
exe_exts = (".exe", ".cmd", ".bat", ".ps1")
path_env = os.environ.get("PATH", "")
dirs = [d.strip('"') for d in path_env.split(os.pathsep) if d.strip()]
seen_dirs = set()
results = {}
for d in dirs:
norm = os.path.normcase(os.path.normpath(d))
if norm in seen_dirs:
continue
seen_dirs.add(norm)
if not os.path.isdir(d):
continue
if any(kw in norm for kw in exclude_keywords):
continue
try:
entries = os.listdir(d)
except OSError:
continue
for f in entries:
ext = os.path.splitext(f)[1].lower()
if ext not in exe_exts:
continue
full_path = os.path.join(d, f)
if not os.path.isfile(full_path):
continue
name = os.path.splitext(f)[0]
key = name.lower()
if key not in results:
results[key] = (name, full_path)
return list(results.values())
這段代碼最核心的設(shè)計取舍是排除關(guān)鍵詞表 exclude_keywords。如果不做排除,直接遍歷 PATH 里的每一條目錄(System32 通常也在 PATH 里),會把幾百個 Windows 系統(tǒng)自帶命令(notepad.exe、ping.exe……)全部收進來,列表可用性直接歸零。所以這里用一個很簡單但有效的策略:只要目錄路徑里包含 system32、syswow64、windowspowershell、windows\ 等關(guān)鍵詞就直接跳過,只保留用戶自己裝的那些工具目錄(比如 npm 全局目錄、Python Scripts 目錄等)。
另一個細節(jié)是去重兩次:第一次是 seen_dirs 去重目錄本身(PATH 里經(jīng)常有重復(fù)路徑),第二次是 results 字典按文件名(小寫)去重——如果同一個命令名在多個目錄里都有可執(zhí)行文件(比如同時裝了 .cmd 和 .exe),只保留 PATH 中排在前面的那個,這跟命令行實際調(diào)用時"誰在前面生效"的行為是一致的,語義上更準確。
3.4 三路數(shù)據(jù)怎么合并
前兩路(開始菜單 + 注冊表)在 scan_all_programs() 里合并去重,第三路單獨跑:
def scan_all_programs():
merged = {}
for name, path in scan_registry(): # 注冊表結(jié)果優(yōu)先,能拿到真實 exe 路徑
key = name.lower()
if key not in merged:
merged[key] = (name, path)
for name, path in scan_start_menu(): # 開始菜單作為補充
key = name.lower()
if key not in merged:
merged[key] = (name, path)
return list(merged.values())
之所以沒有把 CLI 工具也塞進這同一個字典,是因為它們在數(shù)據(jù)庫里需要保留不同的"來源標簽"(app vs cli),方便在界面上分類展示,這個設(shè)計留到下一節(jié)數(shù)據(jù)庫部分細說。
四、數(shù)據(jù)層:SQLite 的表結(jié)構(gòu)與"冪等掃描"設(shè)計
表結(jié)構(gòu)很簡單:
CREATE TABLE IF NOT EXISTS programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
source TEXT DEFAULT 'scan',
use_count INTEGER DEFAULT 0,
last_used TEXT,
added_time TEXT,
UNIQUE(path)
)有兩個設(shè)計點值得說一下。
第一,UNIQUE(path) + INSERT OR IGNORE 組合,實現(xiàn)"冪等掃描"。 每次點"重新掃描",理論上會重復(fù)插入之前已經(jīng)存在的程序。如果沒有唯一約束,每掃一次數(shù)據(jù)庫就多一倍臟數(shù)據(jù)。這里用路徑做唯一鍵(同一個路徑只能存一條記錄),插入語句用 INSERT OR IGNORE——沖突了就靜默跳過,不報錯、不覆蓋:
def bulk_insert_scanned(self, items, source="app"):
conn = self._connect()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conn.executemany(
"INSERT OR IGNORE INTO programs "
"(name, path, source, use_count, last_used, added_time) "
"VALUES (?, ?, ?, 0, NULL, ?)",
[(name, path, source, now) for name, path in items],
)
conn.commit()
conn.close()
配合"重新掃描"前先 clear_scanned()(刪除所有 source != 'custom' 的記錄)再重新插入,就實現(xiàn)了一個干凈的刷新邏輯:自動掃描出來的數(shù)據(jù)每次都是全新快照,用戶手動添加的自定義程序永遠不受影響。
第二,source 字段承擔了"數(shù)據(jù)血緣"的角色。 一條記錄是從開始菜單/注冊表掃來的(app)、從 PATH 掃來的(cli),還是用戶手動添加的(custom),全靠這一個字段區(qū)分。界面上這三種來源會顯示成"已安裝 / 命令行工具 / 自定義"三種不同標簽,用戶一眼就能看出這條記錄是自動發(fā)現(xiàn)的還是自己加的,也方便后續(xù)做分類過濾(雖然當前版本還沒做分類篩選,但字段已經(jīng)預(yù)留好了)。
搜索本身就是一條 LIKE 模糊查詢,按使用次數(shù)降序、名稱升序排列,讓用得越多的程序排得越靠前,是一個很輕量但很實用的"權(quán)重排序":
cur.execute(
"SELECT id, name, path, source, use_count, last_used FROM programs "
"WHERE name LIKE ? ORDER BY use_count DESC, name COLLATE NOCASE",
(f"%{keyword}%",),
)
COLLATE NOCASE 保證排序?qū)Υ笮懖幻舾?,中英文混排的場景下會更符合直覺。
五、配置層:一個容錯的 JSON 深合并
配置管理的需求是"記住用戶的窗口大小/位置、上次搜索內(nèi)容,下次啟動自動恢復(fù)"。實現(xiàn)上沒有用什么框架,就是一個手寫的深合并:
DEFAULT_CONFIG = {
"window": {"width": 920, "height": 600, "x": -1, "y": -1, "maximized": False},
"last_search": "",
"last_scan_time": "",
}
class ConfigManager:
def _load(self) -> dict:
merged = json.loads(json.dumps(DEFAULT_CONFIG)) # 深拷貝默認值
if os.path.isfile(self.path):
try:
with open(self.path, "r", encoding="utf-8") as f:
saved = json.load(f)
for key, value in saved.items():
if key == "window" and isinstance(value, dict):
merged["window"].update(value)
else:
merged[key] = value
except Exception:
pass # 配置文件損壞時使用默認值,不影響程序啟動
return merged
兩個小細節(jié):
json.loads(json.dumps(DEFAULT_CONFIG))是一種"窮人版深拷貝",比copy.deepcopy更直白(反正配置本身就是可以 JSON 序列化的簡單結(jié)構(gòu)),避免多個ConfigManager實例共享同一份默認字典對象導(dǎo)致互相污染。- 讀取配置文件用了一個大大的
except Exception: pass。這是有意為之:配置文件本質(zhì)上是"錦上添花"的數(shù)據(jù),哪怕它被用戶手動改壞了、或者磁盤寫入過程中意外損壞成半截 JSON,也絕不能因為這個讓整個程序崩潰啟動不了——靜默回退到默認配置,用戶頂多是"窗口位置沒記住",而不是"程序打不開"。這種"優(yōu)雅降級"的思路在處理任何用戶可寫的配置文件時都值得借鑒。
六、界面與交互:不卡 UI 的關(guān)鍵是"掃描放到后臺線程"
wxPython 跟大多數(shù) GUI 框架一樣,是單線程事件循環(huán)模型——所有界面刷新都必須發(fā)生在主線程。如果直接在按鈕點擊回調(diào)里跑 scan_all_programs(),注冊表 + 開始菜單 + PATH 三路掃描少說也要幾百毫秒到幾秒,界面會明顯卡死、無響應(yīng)。
解決方式是標準的"子線程干活,wx.CallAfter 回主線程更新界面":
def start_scan(self, auto: bool = False):
self.btn_refresh.Disable()
self.status_bar.SetStatusText("正在掃描已安裝的程序,請稍候…")
thread = threading.Thread(target=self._scan_worker, daemon=True)
thread.start()
def _scan_worker(self):
app_items, cli_items = [], []
try:
app_items = scan_all_programs()
except Exception as e:
wx.CallAfter(wx.MessageBox, f"掃描已安裝程序時發(fā)生錯誤:\n{e}", "掃描失敗", wx.OK | wx.ICON_ERROR)
try:
cli_items = scan_command_line_tools()
except Exception as e:
wx.CallAfter(wx.MessageBox, f"掃描命令行工具時發(fā)生錯誤:\n{e}", "掃描失敗", wx.OK | wx.ICON_ERROR)
def finish():
self.db.clear_scanned()
self.db.bulk_insert_scanned(app_items, source="app")
self.db.bulk_insert_scanned(cli_items, source="cli")
self.refresh_list(self.search_ctrl.GetValue())
self.btn_refresh.Enable()
wx.CallAfter(finish)
threading.Thread(..., daemon=True) 里跑的是純 I/O 操作(讀注冊表、讀文件系統(tǒng)),不涉及任何 wx 控件的直接操作——子線程里絕對不能直接調(diào)用 self.list_ctrl.xxx() 這類 UI API,這是 wxPython(以及幾乎所有 GUI 框架)的鐵律。所有真正觸碰界面的代碼,都包在 finish() 函數(shù)里,通過 wx.CallAfter(finish) 扔回主線程的事件隊列去執(zhí)行,由框架保證它在合適的時機、在主線程里被調(diào)用。這個模式簡單但很容易被新手忽略,忽略了輕則界面繪制錯亂,重則直接崩潰。
其余的交互設(shè)計相對常規(guī),簡單提一下用到的幾個 wx 組件:
wx.SearchCtrl:自帶搜索圖標和清除按鈕的輸入框,比自己拼一個TextCtrl加按鈕要省事得多,也更符合原生系統(tǒng)的視覺習慣,EVT_TEXT綁定實時過濾、EVT_SEARCHCTRL_CANCEL_BTN處理一鍵清空。wx.ListCtrl+ListCtrlAutoWidthMixin:報表模式的列表控件,混入AutoWidthMixin之后最后一列會自動撐滿剩余寬度,不用手動處理窗口縮放時的列寬重新計算。EVT_LIST_ITEM_ACTIVATED:這是"雙擊運行"需求對應(yīng)的事件,wx 里雙擊列表項、或者選中后按回車都會觸發(fā)這個事件,天然貼合"雙擊運行"的交互預(yù)期。- 右鍵菜單
PopupMenu:動態(tài)創(chuàng)建wx.Menu,把"運行 / 打開所在文件夾 / 刪除"三個高頻操作掛上去,用完即銷毀(menu.Destroy()),避免每次右鍵都往內(nèi)存里堆對象。
七、啟動流程小結(jié)
把前面幾節(jié)串起來,MainFrame.__init__ 里的這幾行其實是整個程序的"主線":
self._build_ui()
self._restore_window_state(win_cfg)
self._bind_events()
self.Show()
if self.db.count() == 0:
self.start_scan(auto=True) # 數(shù)據(jù)庫為空 → 首次運行,自動全量掃描
else:
last_search = self.config.get("last_search", "")
self.search_ctrl.SetValue(last_search)
self.refresh_list(last_search) # 數(shù)據(jù)庫有數(shù)據(jù) → 直接展示,并恢復(fù)上次搜索
db.count() == 0 這個判斷很關(guān)鍵:它把"首次安裝自動初始化數(shù)據(jù)"和"日常啟動秒開"兩種場景優(yōu)雅地統(tǒng)一到了一套代碼里,用戶完全不需要感知"我要不要點一下掃描按鈕"這種細節(jié)。
八、打包分發(fā):驗證路徑方案是否真的生效
pip install pyinstaller pyinstaller -F -w -n 程序啟動器 app_launcher.py
-F 打單文件、-w 不帶控制臺窗口(純 GUI 程序)。打包完成后,把生成的 程序啟動器.exe 復(fù)制到任意目錄下雙擊運行,正確的表現(xiàn)應(yīng)該是:exe 所在目錄下自動生成 config.json 和 apps.db 這兩個文件,而不是出現(xiàn)在系統(tǒng)臨時目錄或者 %TEMP% 下的某個隨機文件夾里。這也是驗證第二節(jié) get_base_path() 方案是否真正生效的最直接辦法——如果打包后配置文件跑丟了,基本可以斷定是用錯了 sys._MEIPASS 或者依賴了 os.getcwd()。
九、幾個可以繼續(xù)優(yōu)化的方向
代碼目前是一個能跑、邏輯清晰的版本,但還有一些可以打磨的空間,留在這里做個記錄:
- 圖標提取:目前列表是純文本,如果用
SHGetFileInfo或wx.Icon.FromFile把每個程序的圖標讀出來顯示在列表前面,視覺上會更接近真正的開始菜單體驗。 - PATH 掃描的誤報:
scan_command_line_tools目前只按擴展名 + 目錄關(guān)鍵詞過濾,如果用戶的PATH里有一些不常用的第三方目錄,仍可能掃進一些"技術(shù)上是可執(zhí)行文件,但用戶其實不關(guān)心"的條目,未來可以考慮加一個"從列表隱藏"的操作,而不是只有物理刪除。 - 數(shù)據(jù)庫連接方式:現(xiàn)在每次操作都是"開連接 → 執(zhí)行 → 關(guān)連接",對這種單機小工具夠用,但如果以后要支持更高頻的操作(比如輸入時就實時查庫),可以考慮維護一個常駐連接 + 加鎖,減少反復(fù)建連的開銷。
- 跨平臺 完整度:
launch_program已經(jīng)對 macOS(open)、Linux(xdg-open)做了兼容,但掃描邏輯(開始菜單 / 注冊表 / PATH 排除規(guī)則)目前只針對 Windows,非 Windows 平臺只能靠手動"添加程序",這是當前版本一個明確的能力邊界。
十、總結(jié)
這個項目體量不大,但完整走了一遍"桌面小工具"從設(shè)計到能分發(fā)的典型鏈路:路徑定位、多數(shù)據(jù)源采集與合并、SQLite 冪等寫入、JSON 容錯配置、GUI 線程模型、打包驗證——每一個環(huán)節(jié)單獨拎出來都不復(fù)雜,但組合在一起、并且要經(jīng)得起"打包成 exe 到處運行"的檢驗,才是這類工具真正的門檻所在。如果你也在做類似的本機小工具,希望這篇拆解能幫你少踩幾個坑。
以上就是基于Python+SQLite編寫一個本機程序啟動器的詳細內(nèi)容,更多關(guān)于Python SQLite本機程序啟動的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用PyInstaller輕松實現(xiàn)將Python腳本打包成獨立的.exe可執(zhí)行文件
PyInstaller本質(zhì)上是一個打包工具,它會把Python程序連同它需要的所有依賴全部封裝成一個獨立的,可以直接運行的文件,下面小編就和大家系統(tǒng)性地介紹一下PyInstaller的完整使用方法,希望對大家有所幫助2026-04-04
pymongo如何通過oplog獲取數(shù)據(jù)(mongodb)
使用MongoDB的oplog(操作日志)進行數(shù)據(jù)同步是高級的用法,主要用于復(fù)制和故障恢復(fù),這篇文章主要介紹了pymongo通過oplog獲取數(shù)據(jù)(mongodb),需要的朋友可以參考下2023-09-09
Python讀取Ansible?playbooks返回信息示例解析
這篇文章主要為大家介紹了Python讀取Ansible?playbooks返回信息示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12
python實現(xiàn)微信每日一句自動發(fā)送給喜歡的人
這篇文章主要為大家詳細介紹了python實現(xiàn)微信每日一句自動發(fā)送給喜歡的人,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04
解決Django部署設(shè)置Debug=False時xadmin后臺管理系統(tǒng)樣式丟失
這篇文章主要介紹了解決Django部署設(shè)置Debug=False時xadmin后臺管理系統(tǒng)樣式丟失的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04
python 命令行傳入?yún)?shù)實現(xiàn)解析
這篇文章主要介紹了python 命令行傳入?yún)?shù)實現(xiàn)解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-08-08

