用PYTHON初次編寫小工具心得

WhatsCode發表於2019-04-14

背景

有一個朋友拜託我開發一個搶票類的工具,剛好最近有看python3的書籍,順便練下手便答應了她。題外話:是某公司CRM系統中的客戶預約功能,購買額度200萬以下的金融產品很不容易預約上(而500萬的產品不需要搶),全國每個產品也就幾個名額。由於我朋友去到公司不久(新人)200萬以下的產品是她收入和業績的主要來源了。 寫下本文的目的也僅僅是把涉及到各方面的要點記錄(踩過的坑)下來,希望能幫助到初學者。

  1. selenium中switch_to()的使用
  2. requests中如何模擬登入使用者
  3. selenium + requests 實現無所不能操作
  4. pyqt與qml檔案通訊
  5. UI卡死與多執行緒

開發迭代過程

第一版:龜速自動操作——selenium

一開始覺得不就是個拼手速的工具嘛,於是使用了selenium來模擬人的操作,工具很快寫完,剛好100行程式碼。遇到過的坑:

  • 由於網頁中採用了frameset結構,採用switch_to()方法,需要注意相對位置。
 iframes = self.driver.driver.find_elements_by_tag_name('iframe')
 iframe1 = iframes[1]
 print('獲取預約頁面地址:' + iframe1.get_attribute('src'))
 self.driver.driver.switch_to.frame(iframe1)  # 切換到產品預約頁iframe
複製程式碼
  • 解決xss引起的chrome報錯
chrome_opt = Options()
chrome_opt.add_argument('--disable-xss-auditor')  
self.driver_name = 'chrome'
self.driver = Browser(driver_name=self.driver_name,chrome_options=chrome_opt)
複製程式碼

在實際搶的過程中,卻還是沒有搶到,雖然是比人快了不少,看來需要加速,讓我想到了requests。

第二版:極速手動操作——requests

不出所料,速度還是很快,不過由於此CRM系統的,對其他行業的人來講根本操作不來(需通過審查元素獲取cookies、產品搜尋與預約產品的url等)

# 準備搜尋
postdata = {
    'start': '0',
    'Search': '1',
    'Key': product_name,
    'loadStore': 'true',
    'extResponse':  'true',
}
headers={
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:65.0) Gecko/20100101 Firefox/65.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Cookie': header_cookies
}
search_count = 0
while True:
    rep = s.post(search_prod, data=postdata, headers=headers)

    product_json = json.loads(rep.text)
    search_count = search_count + 1
    if search_count % 10 == 1:
        print('正在進行第(%d)次搜尋...' % search_count)
    try:
        results = product_json['results']
        if results > 1:
            print('錯誤:查出多條產品,請退出後重新輸入產品名稱')
            break

        elif results == 1: # 找到產品
            proudct_id =  product_json['records'][0]['id']
            proudct_CPJC =  product_json['records'][0]['CPJC']
            print('>>>找到預約產品:id:'+proudct_id+'CPJC:'+proudct_CPJC)
            break
        elif results == 0:
            # 迴圈讀取
            time.sleep(0.001)
    except json.decoder.JSONDecodeError as e:
        print('引數不正確')
        exit(0);
複製程式碼

但由於此CRM系統的URL也是動態的,含有操作碼oprateId(各個頁面還不同,且動態改變,沒找到規律),只能從審查元素中去找到對應的URL和模擬header等資訊(很短時間才有效)。另外不可能每次我來幫她搶啊,於是就有了selenium+requests的想法

第三版:無所不能的組合——selenium+requests

在搜尋產品和提交預約之前通過selenium獲取cookies和頁面地址上的operateId和token。

通過selenium的get_cookies()獲取cookies

cookies = self.driver.driver.get_cookies()
for cookie in cookies:
    if cookie['name'] == 'JSESSIONID':
        self.jsessionid = cookie['value']
        break

print('cookie資訊:')
print('jsessionid:' + self.jsessionid)
        
複製程式碼

通過selenium的switch_to()獲取頁面地址上的operateId和token

iframe = self.driver.driver.find_element_by_tag_name('iframe')
self.driver.driver.switch_to.frame(iframe)  # 切換到主頁下半部iframe
self.driver.click_link_by_text("產品預約")
time.sleep(1)
self.driver.driver.switch_to.parent_frame()
iframes = self.driver.driver.find_elements_by_tag_name('iframe')
iframe1 = iframes[1]
# print('獲取預約頁面地址:' + iframe1.get_attribute('src'))
self.driver.driver.switch_to.frame(iframe1)  # 切換到產品預約頁iframe

self.driver.click_link_by_id('ext-gen32')  # 點開搜尋頁
time.sleep(1)
self.driver.driver.switch_to.parent_frame()
iframes = self.driver.driver.find_elements_by_tag_name('iframe')
iframe2 = iframes[2]
search_url = iframe2.get_attribute('src')
# print('獲取預約頁面地址:' + search_url)
parsed_search_url = urllib.parse.urlparse(search_url)
# print(parsed_search_url)
query_str = parsed_search_url.query
query_parms = query_str.split('&')

dict_query = self._parseQuery(query_parms)  # 處理url引數
token = dict_query['Token']
operateid = dict_query['OperateID']
self.Token = token
# self.SearchOperateID = operateid
self.YuyueOperateID = operateid
print('獲取Token:' + self.Token)
print('獲取預約頁面操作碼:' + self.YuyueOperateID)
複製程式碼

此版很接近完美的實現了自動化的登入(驗收碼還是需要手動收入)、自動搜尋產品,當放出產品的時候自動預約。經測試4個產品全部預約到。不過登入使用者名稱、密碼、客戶手機、預約金額、以及準備預約的產品都寫在python檔案中的。讓她改幾個字(居然說是讓她寫程式碼,我服了),本來想通過一個配置檔案解決。但想到python的UI操作還沒試過(很久很久以前用過C語言+GTK),於是想試下pyqt+QT creator(模版只想視覺化操作的)。

第四版:把程式裝進殼裡——PYQT+Qt Creator

Qt Creator 的設計目標是使開發人員能夠利用 Qt 這個應用程式框架更加快速及輕易的完成開發任務。Qt Creator 包括專案生成嚮導、高階的 C++ 程式碼編輯器、瀏覽檔案及類的工具、整合了 Qt Designer、Qt Assistant、Qt Linguist、圖形化的 GDB 除錯前端,整合 qmake 構建工具等。(百度百科) Qt Creator 可以建立多種工程,我選擇的是qml檔案,很快做好了qml檔案,但是如何與python通訊呢?百度了下都沒找到多少系統的文章,官方教程也沒講到(英文)doc.qt.io/qtforpython…

  1. 在介面觸發事件呼叫python, 需在python中用pyqtSlot()申明為槽函式 ,並設定上下文關聯,這樣qml檔案中就可以直接呼叫。
@pyqtSlot() # qml中可呼叫
def begin(self):
    #程式碼略
    pass

if __name__ == '__main__':

app = QGuiApplication(sys.argv)
qml = QQmlApplicationEngine('ui.qml')
rootObject = qml.rootObjects()[0]

instance = Reserve(qml.rootContext() ,rootObject)   #預約例項
qml.rootContext().setContextProperty('con',instance ) #與qml檔案建立關聯

sys.exit(app.exec())
複製程式碼

qml檔案中繫結事件,觸發呼叫python的槽函式

Connections {
    target: button_start
    onClicked: con.begin()
}
複製程式碼
  1. 如何主動更改ui介面中的值呢?例如之前用的print列印日誌,現在需要全部顯示到介面上 在qml檔案中自定義方法,類似javascript
function updatelog(log) {// 定義函式
    textArea.append(log)
}
function clearlog() {
    textArea.clear()
}
複製程式碼

然後可直接

 def __init__(self,context,parent=None):
    super(Reserve,self).__init__(parent)
    self.win = parent
    self.ctx = context
    
 #可直接呼叫qml中方法
self.win.showlog("測試日誌")
複製程式碼

好啦,介面也有了,與程式也封裝好了,開始搶票吧,怎麼回事?介面卡死了。之前學習時候知道需要將介面與程式使用執行緒分開。 首先建立一個執行緒類

class WorkThread(QThread):
    signal = pyqtSignal(type(""))
    clearsignal = pyqtSignal()
    message=""
    yuyue=""

    def __int__(self,parent=None):
        super(WorkThread,self).__init__(parent)

    def __del__(self):
        self.wait()
    #設定
    def setup(self, instance):
        self.yuyue = instance

    #內部/外部(執行緒)使用的輸出資訊
    def log(self,message):
        self.signal.emit(message)

    def run(self):
        self.yuyue.config()
        self.yuyue.login()
        self.yuyue.start()
        # 執行完畢後發出訊號
        self.log("執行完畢")
複製程式碼

在主程式_Init__方法中,啟動執行緒,這裡兩個訊號(signal),一個用於列印日誌,一個用於清空日誌(日誌達到某個閥值)

     def __init__(self,context,parent=None):
        super(Reserve,self).__init__(parent)
        self.win = parent
        self.ctx = context
        
        chrome_opt = Options()
        chrome_opt.add_argument('--disable-xss-auditor')  # 解決xss引起的chrome報錯。
        self.driver_name = 'chrome'
        self.driver = Browser(driver_name=self.driver_name,chrome_options=chrome_opt)

        #啟動一個執行緒,並設定連線通道
        self.thread = WorkThread()
        self.thread.signal.connect(self.callbacklog)
        self.thread.clearsignal.connect(self.callbackclear)
        #向通道傳送資訊
        self.thread.log("初始化完成")
        # 儲存session
        self.s = requests.session()

    # 槽函式(通道末端)
    def callbacklog(self, log):
        self.win.updatelog(log)  # 呼叫qml中的方法
        pass

    def callbackclear(self):
        self.win.clearlog()  # 呼叫qml中的方法
        pass
複製程式碼

這樣執行緒原來使用print()列印日誌的方法全部替換成self.thread.log()即可。執行介面一點都不卡了。

結束語:對python新手來說涉及到面還不少,雖然不少坑收穫還是不少。我在身邊很多朋友眼中一直都是大神一樣的存在(其實我知道這些都是小把戲),我朋友在完成第二版的時候說過一句話讓我很欣慰:

你把我的夢境變成現實了,太厲害了。

當然還有可以改進的地方,比如驗證碼完全可以不用手動輸入,可以利用機器學習,對驗證碼進行訓練,然後自動識別。不過真的沒必要了。就她一個人用確實沒必要折騰了。

相關文章