從?#65279字符看dede模板頁面編碼問題
1541
2025-04-01
市面上的錄屏工具軟件有很多,基本都是窗口程序。畢竟,離開GUI的支持,設置參數、選擇錄像區域等操作都會變得非常困難。不過,窗口程序也并非無往不勝,即便是屏幕錄像這樣交互頻繁的應用,控制臺程序也同樣可以大顯身手,甚至比窗口程序的效率更高、操作更便捷。
今天,我帶給同學們的是一款命令行模式的錄屏軟件,可將屏幕指定區域的內容錄制成GIF動畫文件或MP4、AVI、WMV等格式的視頻文件,錄像區域、格式、幀率等參數,既可以由命令行傳入,也可以通過鼠標和熱鍵來調整。雖然只是實現了錄屏功能,卻涉及了以下諸多知識點:
使用pynput模塊的keyboard和mouse偵聽鍵盤和鼠標,實現熱鍵機制和鼠標選取
使用pywin32模塊的win32api、win32gui和win32con捕捉當前窗口句柄,實現窗口的隱藏和顯示
使用pillow模塊的ImageGrab實現屏幕截圖
使用imageio模塊生成GIF或MP4文件
使用Python標準模塊optparse構造linux風格的使用界面,遵循GNU/POSIX語法規則設置參數選項
使用批處理命令編寫批處理文件,最終生成桌面快捷方式
除了上述知識點外,這款屏幕錄像機還用到了定時器、線程、隊列等技術,以及生產者-消費者模式,幾乎就是一個Python技術博覽館。
1. 監聽鍵盤和鼠標
盡管pywin32也可以監聽鍵盤和鼠標,但我選擇是的pynput模塊,因為它實在是太好用了,還可以跨平臺。除了監聽,pynput模塊也可以用來操控鍵盤和鼠標。pynput模塊的安裝很簡單,直接使用pip安裝即可。
pip install pynput
pynput模塊提供了keyboard和mouse兩個類用于監聽鍵盤和鼠標,實例化時只需要提供相應的事件函數即可。通過下面的簡單例子,新手也很容易掌握pynput的使用要點。友情提示:不要在運行這段代碼的命令行窗口中測試鼠標左鍵,因為點擊左鍵會影響程序執行,導致反應遲滯。
from pynput import keyboard, mouse def on_click(x, y, button, pressed): """鼠標按鍵""" action = '按下' if pressed else '彈起' if button == mouse.Button.left: print('左鍵%s,(%d,%d)'%(action, x, y)) elif button == mouse.Button.right: print('右鍵%s,(%d,%d)'%(action, x, y)) def on_press(key): """鍵按下""" if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: print('Ctr鍵按下') def on_release(key): """鍵彈起""" if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: print('Ctr鍵彈起') elif key == keyboard.Key.esc: print('再見') return False monitor_m = mouse.Listener(on_click=on_click) monitor_m.start() monitor_k = keyboard.Listener(on_press=on_press, on_release=on_release) monitor_k.start() monitor_k.join()
2. 隱藏或顯示控制臺窗口
作為錄屏軟件,錄制時需要將自身窗口隱藏,而開始錄制前或錄制結束后又需要顯示自身窗口。雖然最小化、最大化窗口也可以滿足使用要求,但我選擇使用pywin32來隱藏或顯示控制臺窗口。pywin32模塊的安裝命令如下:
pip install pypiwin32
下面的3行語句分別實現獲取當前窗口句柄、隱藏和顯示窗口句柄指定的窗口。
import win32gui, win32api, win32con hwnd = win32gui.GetForegroundWindow() # 獲取最前窗口句柄 win32gui.ShowWindow(hwnd, win32con.SW_HIDE) # 隱藏hwnd指定的窗口 win32gui.ShowWindow(hwnd, win32con.SW_SHOW) # 顯示hwnd指定的窗口
3. 屏幕截圖
無所不能的pywin32也可以截屏,據說速度很快。為此我專門測試了全屏幕(1902x1080)截取,發現pywin32和pillow的ImageGrab在速度上堪堪打了個平手,但pywin32截圖需要從構建DC開始,大約十幾行代碼,而ImageGrab只需要一行代碼。既然代碼簡潔,比速度也不遜色,還有什么理由不選擇ImageGrab呢?
ImageGrab子模塊提供了一個截屏的函數grab,返回一個PIL圖像對象。grab函數接受一個四元組參數用以指定截圖區域的左上角和右下角在屏幕上的坐標,若省略參數,grab函數將截取整個屏幕。
from PIL import ImageGrab im = ImageGrab.grab((1200,600,1920,1080)) # 截取大小為720×480的屏幕區域 im.show() im = ImageGrab.grab() # 截取整個屏幕 im.show()
雖然代碼中看起來ImageGrab是從PIL模塊導入的,但實際上ImageGrab來自pillow模塊,這是由于版本歷史的原因造成的困惑。如果你還沒有安裝pillow模塊,請使用如下的命令安裝:
pip install pillow
4. 生成動畫或視頻文件
Python圖像庫有很多,imageio是后起之秀,也是其中的佼佼者,尤其在動畫和視頻領域,更是獨領風騷。在imageio誕生之前,生成GIF動畫需要幾百行代碼,而且因為依賴庫升級頻繁,幾乎每隔一段時間就需要重寫一次。現在有了imageio,一切都變得云淡風輕了。安裝imageio時,請一并安裝imageio-ffmpeg,這是imageio生成視頻文件的依賴庫。
pip install imageio
pip install imageio-ffmpeg
imageio提供了兩種生成動畫或視頻文件的方法:imageio.mimsave函數和imageio.get_writer函數。imageio.mimsave函數接收一個由PIL對象組成的列表作為參數,生成文件前需要將每一幀圖像轉成PIL對象并存入列表——這意味著生成文件必須在最后一幀圖像捕捉完成之后才能開始。
# imageio.mimsave(out_file, pil_list, format='GIF', fps=fps, loop=loop) # out_file - 輸出文件名 # pil_list - 列表,元素類型為PIL對象 # format - 輸出格式 # fps - 幀率(每秒播放的幀數) # loop - 循環次數,0表示無限循環(視頻格式不支持該參數)
imageio.get_writer函數類似于open函數,返回了一個文件對象,該對象提供append_data方法,可以將單幀的PIL對象對寫入輸出文件。寫入完成后,不要忘記使用close方法關閉文件對象。使用imageio.get_writer函數生成動畫或視頻文件,可以很好得支持生產者-消費者模式,捕捉一幀寫入一幀,停止錄屏后文件即告生成。
# writer = imageio.get_writer(out_file, fps=fps) # gif格式可增加loop參數 # writer.append_data(im_pil) # im_pil為PIL對象 # writer.close()
5. 定時器
錄屏依賴于精準的定時器,遺憾的是Python并沒有提供一個像樣的定時器,因此只能自己寫一個了。關于定時器的介紹,請參考我昨天寫的博文《無所不能的Python竟然沒有一個像樣的定時器?試試這個!》,這里就不再贅述了。
6. 完整代碼
源文件名為ScreenRecorder.py,全部代碼不足300行,代碼中用到的各個模塊和技術要點都已介紹過了,關鍵之處均有注釋。
# -*- coding:utf-8 -*- import os, time import optparse import threading import imageio import queue import numpy as np from PIL import Image, ImageGrab import win32gui, win32api, win32con from pynput import keyboard, mouse class PyTimer: """定時器類""" def __init__(self, func, *args, **kwargs): """構造函數""" self.func = func self.args = args self.kwargs = kwargs self.running = False def _run_func(self): """運行定時事件函數""" th = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs) th.setDaemon(True) th.start() def _start(self, interval, once): """啟動定時器的線程函數""" if interval < 0.010: interval = 0.010 if interval < 0.050: dt = interval/10 else: dt = 0.005 if once: deadline = time.time() + interval while time.time() < deadline: time.sleep(dt) # 定時時間到,調用定時事件函數 self._run_func() else: self.running = True deadline = time.time() + interval while self.running: while time.time() < deadline: time.sleep(dt) deadline += interval # 更新下一次定時時間 if self.running: # 定時時間到,調用定時事件函數 self._run_func() def start(self, interval, once=False): """啟動定時器 interval - 定時間隔,浮點型,以秒為單位,最高精度10毫秒 once - 是否僅啟動一次,默認是連續的 """ th = threading.Thread(target=self._start, args=(interval, once)) th.setDaemon(True) th.start() def stop(self): """停止定時器""" self.running = False class ScreenRecorder: """屏幕記錄器""" def __init__(self, out, fps=10, nfs=1000, loop=0): """構造函數""" self.format = ('.gif', '.mp4', '.avi', '.wmv') ext = os.path.splitext(out)[1].lower() if not ext in self.format: raise ValueError('不支持的文件格式:%s'%ext) self.out = out self.ext = ext self.fps = fps self.nfs = nfs self.loop = loop self.cw = win32api.GetSystemMetrics(win32con.SM_CXSCREEN) self.ch = win32api.GetSystemMetrics(win32con.SM_CYSCREEN) self.set_box((0, 0, self.cw, self.ch)) self.ctr_is_pressed = False self.hidding = False self.recording = False self.pos_click = (0,0) self.q = None self.hwnd = self._find_self() self.info = None self.help() self.status() def _find_self(self): """找到當前Python解釋器的窗口句柄""" return win32gui.GetForegroundWindow() # 獲取最前窗口句柄 def set_box(self, box): """設置記錄區域""" x0, y0, x1, y1 = box dx, dy = (x1-x0)%16, (y1-y0)%16 dx0, dx1 = dx//2, dx-dx//2 dy0, dy1 = dy//2, dy-dy//2 self.box = (x0+dx0, y0+dy0, x1-dx1, y1-dy1) def help(self): """熱鍵提示""" print('---------------------------------------------') print('Ctr + 回車鍵:隱藏/顯示窗口') print('Ctr + 鼠標左鍵或右鍵拖拽:設置記錄區域') print('Ctr + PageUp/PageDown:更改記錄格式') print('Ctr + Up/Down:調整幀率') print('Ctr + 空格鍵:開始/停止記錄') print('Esc:退出') print() def status(self): """當前狀態""" if self.info: print('\r%s'%(' '*len(self.info.encode('gbk')),), end='', flush=True) recording_text = '正在記錄' if self.recording else '準備就緒' if self.ext == 'gif': loop_str = '循環%d次'%self.loop if self.loop > 0 else '循環' else: loop_str = '不循環' self.info = '\r輸出文件:%s | 幀率:%d | 區域:%s'%(self.out, self.fps, str(self.box)) print(self.info, end='', flush=True) def start(self): """開始記錄""" self.q = queue.Queue(100) self.timer = PyTimer(self.capture) self.timer.start(1/self.fps) th = threading.Thread(target=self.produce) th.setDaemon(True) th.start() def stop(self): """停止記錄""" self.timer.stop() def capture(self): """截屏""" if not self.q.full(): im = ImageGrab.grab(self.box) self.q.put(im) def produce(self): """生成動畫或視頻文件""" if self.ext == '.gif': writer = imageio.get_writer(self.out, fps=self.fps, loop=self.loop) else: writer = imageio.get_writer(self.out, fps=self.fps) n = 0 while self.recording and n < self.nfs: if self.q.empty(): time.sleep(0.01) else: im = np.array(self.q.get()) writer.append_data(im) n += 1 writer.close() def on_press(self, key): """鍵按下""" if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: self.ctr_is_pressed = True def on_release(self, key): """鍵釋放""" if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: self.ctr_is_pressed = False elif key == keyboard.Key.space and self.ctr_is_pressed: if self.recording: # 停止記錄 self.stop() self.recording = False if self.hidding: win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW) # 顯示窗口 self.hidding = False else: # 開始記錄 self.start() self.recording = True if not self.hidding: win32gui.ShowWindow(self.hwnd, win32con.SW_HIDE) # 隱藏窗口 self.hidding = True elif key == keyboard.Key.enter and self.ctr_is_pressed: if self.hidding: # 顯示窗口 win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW) # 顯示窗口 self.hidding = False self.status() else: # 隱藏窗口 win32gui.ShowWindow(self.hwnd, win32con.SW_HIDE) # 隱藏窗口 self.hidding = True elif (key == keyboard.Key.page_down or key == keyboard.Key.page_up) and self.ctr_is_pressed: i = self.format.index(self.ext) if key == keyboard.Key.page_down: self.ext = self.format[(i+1)%len(self.format)] else: self.ext = self.format[(i-1)%len(self.format)] folder = os.path.split(self.out)[0] dt_str = time.strftime('%Y%m%d%H%M%S') self.out = os.path.join(folder, '%s%s'%(dt_str, self.ext)) self.status() elif key == keyboard.Key.left and self.ctr_is_pressed: if self.fps > 1: self.fps -= 1 self.status() elif key == keyboard.Key.right and self.ctr_is_pressed: if self.fps < 40: self.fps += 1 self.status() elif key == keyboard.Key.esc: print('\n程序已結束') return False def on_click(self, x, y, button, pressed): """鼠標按鍵""" if (button == mouse.Button.left or button == mouse.Button.right) and self.ctr_is_pressed: if pressed: self.pos_click = (x, y) elif self.pos_click != (x, y): x0, y0 = self.pos_click self.set_box((min(x0,x), min(y0,y), max(x0,x), max(y0,y))) self.status() def parse_args(): """獲取參數""" parser = optparse.OptionParser() parser.add_option('-o', '--out', action='store', type='string', dest='out', default='', help='輸出文件名') parser.add_option('-f', '--fps', action='store', type='int', dest='fps', default='10', help='幀率') parser.add_option('-n', '--nfs', action='store', type='int', dest='nfs', default='1000', help='最大幀數') parser.add_option('-l', '--loop', action='store', type='int', dest='loop', default=0, help='循環') return parser.parse_args() if __name__ == '__main__': options, args = parse_args() if options.out: out = options.out folder = os.path.split(out)[0] if folder and not os.path.isdir(folder): raise ValueError('路徑不存在:%s'%folder) else: dt_str = time.strftime('%Y%m%d%H%M%S') out = os.path.join(os.getcwd(), '%s.mp4'%(dt_str,)) sr = ScreenRecorder(out, fps=options.fps, nfs=options.nfs, loop=options.loop) monitor_m = mouse.Listener(on_click=sr.on_click) monitor_m.start() monitor_k = keyboard.Listener(on_press=sr.on_press, on_release=sr.on_release) monitor_k.start() monitor_k.join()
7. linux風格的使用界面
通過sys模塊的sys.argv接收命令行參數,是很多Python程序員的首選。不過,sys.argv無法處理默認參數、關鍵字參數,當參數較多時也極易發生張冠李戴的錯誤。熟悉linux的程序員,更喜歡使用GNU/POSIX語法設置參數選項。
ScreenRecorder.py借助于Python的標準模塊optparse,可以很容易地提供linux風格的使用界面。下圖中-h或者–help選項是optparse模塊自動生成的。
假如要錄屏到文件d:\demo.mp4,幀率為25,下面的兩種寫法是等價的。
python .\ScreenRecorder.py -o d:\demp.mp4 -f 25
python .\ScreenRecorder.py --out=d:\demp.mp4 --fps=25
錄屏程序啟動后,界面如下圖所示。現在可以使用熱鍵配合鼠標,盡情體驗這個新玩具了。
細心的同學很快就會發現,界面上的參數信息會實時更新,但屏幕卻沒有滾動。這是怎么實現的呢?有興趣的同學可以去讀一下代碼,或者在我的博客首頁搜索“必殺技”,就會找到答案。
8. 生成桌面快捷方式
先打開一個命令行窗口,再運行命令,還要輸入參數(其實不輸入參數也可以錄屏),有些同學就會覺得很麻煩。沒關系,下面我們再搞一個批處理,然后將批處理做成桌面快捷方式,就可以化繁為簡一鍵操作了。
隨便打開一個文本編輯器,生成如下的文件,并以ScreenRecorder.bat命名,保存在和ScreenRecorder.py同級的路徑下。如果需要改變錄屏的默認參數,也可以寫在這個文件中。
@echo off cd /d d:\XufiveGithub\ScreenRecorder python ScreenRecorder.py
有了bat文件之后,就可以在Windows桌面上生成該文件的快捷方式了。不知道如何生成快捷方式的同學,請自行搜索吧。最后附上一張我錄制的全球風場GIF動畫圖(局部)。
PowerShell Python 圖像處理 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。