最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Python+SQLite編寫一個本機程序啟動器

 更新時間:2026年07月03日 08:25:41   作者:winfredzhang  
本文介紹了基于Python+SQLite編寫一個Windows程序啟動器工具的相關(guān)知識,文章特別強調(diào)了打包后配置文件路徑的正確處理方法,以及針對不同程序安裝方式的多源數(shù)據(jù)采集策略,最終實現(xiàn)了一個可維護、可擴展的輕量級程序啟動

前言

最近做了一個小工具:程序啟動器。功能聽起來很樸素——掃描本機安裝過的程序,做成一個可以搜索、雙擊運行的列表,跟 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.executableexe 自身的真實路徑,而不是臨時解壓路徑)來定位目錄;否則退回到 __file__(腳本自身路徑)。兩個分支殊途同歸——最終都拿到"程序所在的文件夾",DB_PATHCONFIG_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……)全部收進來,列表可用性直接歸零。所以這里用一個很簡單但有效的策略:只要目錄路徑里包含 system32syswow64、windowspowershellwindows\ 等關(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.jsonapps.db 這兩個文件,而不是出現(xiàn)在系統(tǒng)臨時目錄或者 %TEMP% 下的某個隨機文件夾里。這也是驗證第二節(jié) get_base_path() 方案是否真正生效的最直接辦法——如果打包后配置文件跑丟了,基本可以斷定是用錯了 sys._MEIPASS 或者依賴了 os.getcwd()。

九、幾個可以繼續(xù)優(yōu)化的方向

代碼目前是一個能跑、邏輯清晰的版本,但還有一些可以打磨的空間,留在這里做個記錄:

  1. 圖標提取:目前列表是純文本,如果用 SHGetFileInfowx.Icon.FromFile 把每個程序的圖標讀出來顯示在列表前面,視覺上會更接近真正的開始菜單體驗。
  2. PATH 掃描的誤報scan_command_line_tools 目前只按擴展名 + 目錄關(guān)鍵詞過濾,如果用戶的 PATH 里有一些不常用的第三方目錄,仍可能掃進一些"技術(shù)上是可執(zhí)行文件,但用戶其實不關(guān)心"的條目,未來可以考慮加一個"從列表隱藏"的操作,而不是只有物理刪除。
  3. 數(shù)據(jù)庫連接方式:現(xiàn)在每次操作都是"開連接 → 執(zhí)行 → 關(guān)連接",對這種單機小工具夠用,但如果以后要支持更高頻的操作(比如輸入時就實時查庫),可以考慮維護一個常駐連接 + 加鎖,減少反復(fù)建連的開銷。
  4. 跨平臺 完整度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)文章

最新評論

军事| 大城县| 满城县| 涿鹿县| 旅游| 南汇区| 荣昌县| 屯昌县| 容城县| 丹凤县| 垫江县| 福建省| 定南县| 永定县| 绥滨县| 秦皇岛市| 玉溪市| 平陆县| 贺州市| 宁陵县| 义乌市| 鄂伦春自治旗| 沁水县| 安仁县| 蛟河市| 沙雅县| 嵊州市| 上栗县| 元氏县| 临朐县| 舒城县| 凌云县| 浙江省| 卢龙县| 云霄县| 水城县| 巩留县| 长子县| 原阳县| 元江| 海兴县|