小爬最近的一個需求是:將windows系統下的打印任務批量有序給到網絡打印機。
用戶先從公司的OA(B/S模式)系統下 打印指定內容的表單以及表單中的附件內容。這個問題可以這樣分解:
1、抓包,得到OA對應的任務接口,然后利用python requests模擬post請求,獲取所有的表單的URL並進行必要的去重處理;
2、打印OA表單的過程,需要瀏覽器在前台,這個時候可以結合selenium的driver.get(url)方法,打開每一個表單,同時解析網頁內容,拿到所有附件的相關信息(名稱、后綴、下載地址),利用requests再度保存這些附件至本地;
3、打開表單后,利用 win32api.keybd_event,模擬鍵盤快捷鍵“Ctrl + Shift + P”調出系統的打印窗口;
4、選中“PDF打印機”,需要電腦中有“Microsoft Print to Pdf”或者“Foxit Reader PDF Printer”等;
5、利用pywin32中的相關方法,驅動打印過程,將每個OA表單(網頁)打印成PDF文件並格式化命名&存儲,與前面的附件內容存儲到同一個文件夾;
6、附件文件和OA生成的PDF文件均格式化存儲,用OA單號作為文件名的一部分,將兩者關聯起來;
7、將本地對應文件夾的所有內容有序推送給打印機,指定打印機為某一台網絡打印機。同時要確保打印過程中,不亂序;
針對步驟3,可以自定義函數來實現:
#鍵盤按下 def key_down(keyname): win32api.keybd_event(vk_code[keyname],0,0,0) #鍵盤抬起 def key_up(key_name): win32api.keybd_event(vk_code[key_name],0,win32con.KEYEVENTF_KEYUP,0) #按鍵組合操作 def simulate_three_key(firstkey,sencondkey,lastkey): key_down(firstkey) key_down(sencondkey) key_down(lastkey) key_up(lastkey) key_up(sencondkey) key_up(firstkey) #按鍵組合操作 def simulate_two_key(firstkey,sencondkey): key_down(firstkey) key_down(sencondkey) key_up(sencondkey) key_up(firstkey)
然后利用 simulate_three_key('ctrl',"shift",'p') 即可呼出系統的默認打印窗口:
那么步驟4,也就是上圖的打印窗口,如何選中某一個打印機呢?直接利用win32gui.SendMessage
來選中某個打印機是非常困難的。一種可行的方法是,利用pywin32下的win32print模塊,也就是本文的重點。
比如,用下面的代碼可以遍歷並獲取到當前計算機的所有打印機信息:
for it in win32print.EnumPrinters(6): print(it[1])
我們甚至可以知道某台打印機的當前狀態,假定某台打印機名為printerName,則可以這樣獲取打印機狀態:
hPrinter = win32print.OpenPrinter (printerName) dic = hex(win32print.GetPrinter(hPrinter,2)['Status']) if dic[-2]=="8": print("The printer is offline.") if dic[-5]=="4": print("The printer is out of toner.") elif dic[-5]=="2": print("The printer is low on toner.")
Printer status name/value |
Description |
---|---|
PRINTER_STATUS_BUSY 0x00000200 |
The printer is busy. |
PRINTER_STATUS_DOOR_OPEN 0x00400000 |
The printer door is open. |
PRINTER_STATUS_ERROR 0x00000002 |
The printer is in an error state. |
PRINTER_STATUS_INITIALIZING 0x00008000 |
The printer is initializing. |
PRINTER_STATUS_IO_ACTIVE 0x00000100 |
The printer is in an active input or output state. |
PRINTER_STATUS_MANUAL_FEED 0x00000020 |
The printer is in a manual feed state. |
PRINTER_STATUS_NOT_AVAILABLE 0x00001000 |
The printer is not available for printing. |
PRINTER_STATUS_NO_TONER 0x00040000 |
The printer is out of toner. |
PRINTER_STATUS_OFFLINE 0x00000080 |
The printer is offline. |
PRINTER_STATUS_OUTPUT_BIN_FULL 0x00000800 |
The printer's output bin is full. |
PRINTER_STATUS_OUT_OF_MEMORY 0x00200000 |
The printer has run out of memory. |
PRINTER_STATUS_PAGE_PUNT 0x00080000 |
The printer cannot print the current page. |
PRINTER_STATUS_PAPER_JAM 0x00000008 |
Paper is stuck in the printer. |
PRINTER_STATUS_PAPER_OUT 0x00000010 |
The printer is out of paper. |
PRINTER_STATUS_PAPER_PROBLEM 0x00000040 |
The printer has an unspecified paper problem. |
PRINTER_STATUS_PAUSED 0x00000001 |
The printer is paused. |
PRINTER_STATUS_PENDING_DELETION 0x00000004 |
The printer is being deleted as a result of a client's call to RpcDeletePrinter. No new jobs can be submitted on existing printer objects for that printer. |
PRINTER_STATUS_POWER_SAVE 0x01000000 |
The printer is in power-save mode.<182> |
PRINTER_STATUS_PRINTING 0x00000400 |
The printer is printing. |
PRINTER_STATUS_PROCESSING 0x00004000 |
The printer is processing a print job. |
PRINTER_STATUS_SERVER_OFFLINE 0x02000000 |
The printer is offline.<183> |
PRINTER_STATUS_SERVER_UNKNOWN 0x00800000 |
The printer status is unknown.<184> |
PRINTER_STATUS_TONER_LOW 0x00020000 |
The printer is low on toner. |
PRINTER_STATUS_USER_INTERVENTION 0x00100000 |
The printer has an error that requires the user to do something. |
PRINTER_STATUS_WAITING 0x00002000 |
The printer is waiting. |
PRINTER_STATUS_WARMING_UP 0x00010000 |
The printer is warming up. |
更多的打印機接口信息,可查詢微軟的開發文檔:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/1625e9d9-29e4-48f4-b83d-3bd0fdaea787?redirectedfrom=MSDN
我們也可以得到當前默認的打印機,設置默認打印機:
currentPrinter=win32print.GetDefaultPrinterW()
win32print.SetDefaultPrinterW(printer)
我們利用上面兩個函數,可以先得到系統當前的打印機,用變量存儲后,再設置默認打印機至 PDF打印機,待執行完所有任務后,再設置默認打印機為用戶一開始的默認打印機,整個過程用戶不需要更多的干預;
重點說下步驟7:我們需要以OA表單+附件的形式,逐一給打印機分配任務,且不能亂序:
如果附件是圖片性質,我們可以結合Pillow庫來處理,示例代碼如下:
import win32print import win32ui from PIL import Image, ImageWin # Constants for GetDeviceCaps # # # HORZRES / VERTRES = printable area # HORZRES = 8 VERTRES = 10 # # LOGPIXELS = dots per inch # LOGPIXELSX = 88 LOGPIXELSY = 90 # # PHYSICALWIDTH/HEIGHT = total area # PHYSICALWIDTH = 110 PHYSICALHEIGHT = 111 # # PHYSICALOFFSETX/Y = left / top margin # PHYSICALOFFSETX = 112 PHYSICALOFFSETY = 113 def print_image(file_name): printer_name = win32print.GetDefaultPrinterW() # 獲得默認打印機 # # You can only write a Device-independent bitmap # directly to a Windows device context; therefore # we need (for ease) to use the Python Imaging # Library to manipulate the image. # # Create a device context from a named printer # and assess the printable size of the paper. # hDC = win32ui.CreateDC () hDC.CreatePrinterDC (printer_name) printable_area = hDC.GetDeviceCaps (HORZRES), hDC.GetDeviceCaps (VERTRES) printer_size = hDC.GetDeviceCaps (PHYSICALWIDTH), hDC.GetDeviceCaps (PHYSICALHEIGHT) printer_margins = hDC.GetDeviceCaps (PHYSICALOFFSETX), hDC.GetDeviceCaps (PHYSICALOFFSETY) # # Open the image, rotate it if it's wider than # it is high, and work out how much to multiply # each pixel by to get it as big as possible on # the page without distorting. # bmp = Image.open (file_name) # bmp = bmp.rotate (90) # bmp.save("test1.png") if bmp.size[0] > bmp.size[1]: # bmp = bmp.rotate (90) bmp=bmp.transpose(Image.ROTATE_90) ratios = [1.0 * printable_area[0] / bmp.size[0], 1.0 * printable_area[1] / bmp.size[1]] scale = min (ratios)*0.85 #這個0.85的系數是不希望圖片被打印太大,缺少margin,不方便文檔的裝訂 file_name=file_name.split("\\")[-1] #這一步是為了提取fullpath中的filename部分 # # Start the print job, and draw the bitmap to # the printer device at the scaled size. # hDC.StartDoc (file_name) hDC.StartPage () dib = ImageWin.Dib (bmp) scaled_width, scaled_height = [int (scale * i) for i in bmp.size] x1 = int ((printer_size[0] - scaled_width) / 2) y1 = int ((printer_size[1] - scaled_height) / 2) x2 = x1 + scaled_width y2 = y1 + scaled_height dib.draw (hDC.GetHandleOutput (), (x1, y1, x2, y2)) hDC.EndPage () hDC.EndDoc () hDC.DeleteDC ()
需要強調的是,如果我們對圖片進行后台旋轉90度時,一定要用transpose(Image.ROTATE_90),不要使用 rotate (90),否則打印的圖片很有可能顯示不完整,且有黑邊;
具體的transpose用法見Pillow官網文檔:
如果我們要打印的任務是PDF或者其他office類型的文檔,可以利用win32api.ShellExecute方法,示例如下:
def printer_loading(filename): # open (filename, "r") currentPrinter=win32print.GetDefaultPrinterW() win32api.ShellExecute (0,"print",filename,'/d:"%s"' % currentPrinter,".",0)
該方法有一個缺陷,win32api.ShellExecute 會在指令發出后,立即返回值,而不是等打印任務真正傳輸到打印機后再返回。這就意味着,附件中的圖片用win32ui的方法走后台已經傳輸給打印機,而PDF等其他文件可能還沒及時發送給打印機,造成打印任務亂序。
可行的解決方法是,利用win32print.EnumJobs,定時獲取打印機當前的任務隊列,確保隊列中出現剛推送的任務后,再來推送下一個打印任務。示例如下:
由於打印任務是動態增減的,每次得到的tasks可能都不同,且由於打印機可能有很多人共同使用,不能保證某個用戶的某次打印任務一定會出現在打印隊列的最上方。所以要盡可能拿到所有的任務;
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
至此,這個項目中的難點都逐一有了解決方案,希望小爬以上的思路,對喜歡自動化的你,能有所借鑒~~