用PYTHON初次編寫(xiě)小工具心得
背景
有一個(gè)朋友拜托我開(kāi)發(fā)一個(gè)搶票類的工具迫悠,剛好最近有看python3的書(shū)籍鹏漆,順便練下手便答應(yīng)了她。題外話:是某公司CRM系統(tǒng)中的客戶預(yù)約功能创泄,購(gòu)買額度200萬(wàn)以下的金融產(chǎn)品很不容易預(yù)約上(而500萬(wàn)的產(chǎn)品不需要搶)艺玲,全國(guó)每個(gè)產(chǎn)品也就幾個(gè)名額。由于我朋友去到公司不久(新人)200萬(wàn)以下的產(chǎn)品是她收入和業(yè)績(jī)的主要來(lái)源了验烧。
寫(xiě)下本文的目的也僅僅是把涉及到各方面的要點(diǎn)記錄(踩過(guò)的坑)下來(lái)板驳,希望能幫助到初學(xué)者。
- selenium中switch_to()的使用
- requests中如何模擬登錄用戶
- selenium + requests 實(shí)現(xiàn)無(wú)所不能操作
- pyqt與qml文件通訊
- UI卡死與多線程
開(kāi)發(fā)迭代過(guò)程
第一版:龜速自動(dòng)操作——selenium
一開(kāi)始覺(jué)得不就是個(gè)拼手速的工具嘛碍拆,于是使用了selenium來(lái)模擬人的操作若治,工具很快寫(xiě)完,剛好100行代碼感混。遇到過(guò)的坑:
- 由于網(wǎng)頁(yè)中采用了frameset結(jié)構(gòu)端幼,采用switch_to()方法,需要注意相對(duì)位置弧满。
iframes = self.driver.driver.find_elements_by_tag_name('iframe')
iframe1 = iframes[1]
print('獲取預(yù)約頁(yè)面地址:' + iframe1.get_attribute('src'))
self.driver.driver.switch_to.frame(iframe1) # 切換到產(chǎn)品預(yù)約頁(yè)iframe
- 解決xss引起的chrome報(bào)錯(cuò)
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)
在實(shí)際搶的過(guò)程中婆跑,卻還是沒(méi)有搶到,雖然是比人快了不少庭呜,看來(lái)需要加速滑进,讓我想到了requests犀忱。
第二版:極速手動(dòng)操作——requests
不出所料,速度還是很快扶关,不過(guò)由于此CRM系統(tǒng)的阴汇,對(duì)其他行業(yè)的人來(lái)講根本操作不來(lái)(需通過(guò)審查元素獲取cookies、產(chǎn)品搜索與預(yù)約產(chǎn)品的url等)
# 準(zhǔn)備搜索
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('正在進(jìn)行第(%d)次搜索...' % search_count)
try:
results = product_json['results']
if results > 1:
print('錯(cuò)誤:查出多條產(chǎn)品节槐,請(qǐng)退出后重新輸入產(chǎn)品名稱')
break
elif results == 1: # 找到產(chǎn)品
proudct_id = product_json['records'][0]['id']
proudct_CPJC = product_json['records'][0]['CPJC']
print('>>>找到預(yù)約產(chǎn)品:id:'+proudct_id+'CPJC:'+proudct_CPJC)
break
elif results == 0:
# 循環(huán)讀取
time.sleep(0.001)
except json.decoder.JSONDecodeError as e:
print('參數(shù)不正確')
exit(0);
但由于此CRM系統(tǒng)的URL也是動(dòng)態(tài)的搀庶,含有操作碼oprateId(各個(gè)頁(yè)面還不同,且動(dòng)態(tài)改變铜异,沒(méi)找到規(guī)律)哥倔,只能從審查元素中去找到對(duì)應(yīng)的URL和模擬header等信息(很短時(shí)間才有效)。另外不可能每次我來(lái)幫她搶啊,于是就有了selenium+requests的想法
第三版:無(wú)所不能的組合——selenium+requests
在搜索產(chǎn)品和提交預(yù)約之前通過(guò)selenium獲取cookies和頁(yè)面地址上的operateId和token揍庄。
通過(guò)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)
通過(guò)selenium的switch_to()獲取頁(yè)面地址上的operateId和token
iframe = self.driver.driver.find_element_by_tag_name('iframe')
self.driver.driver.switch_to.frame(iframe) # 切換到主頁(yè)下半部iframe
self.driver.click_link_by_text("產(chǎn)品預(yù)約")
time.sleep(1)
self.driver.driver.switch_to.parent_frame()
iframes = self.driver.driver.find_elements_by_tag_name('iframe')
iframe1 = iframes[1]
# print('獲取預(yù)約頁(yè)面地址:' + iframe1.get_attribute('src'))
self.driver.driver.switch_to.frame(iframe1) # 切換到產(chǎn)品預(yù)約頁(yè)iframe
self.driver.click_link_by_id('ext-gen32') # 點(diǎn)開(kāi)搜索頁(yè)
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('獲取預(yù)約頁(yè)面地址:' + 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參數(shù)
token = dict_query['Token']
operateid = dict_query['OperateID']
self.Token = token
# self.SearchOperateID = operateid
self.YuyueOperateID = operateid
print('獲取Token:' + self.Token)
print('獲取預(yù)約頁(yè)面操作碼:' + self.YuyueOperateID)
此版很接近完美的實(shí)現(xiàn)了自動(dòng)化的登錄(驗(yàn)收碼還是需要手動(dòng)收入)咆蒿、自動(dòng)搜索產(chǎn)品,當(dāng)放出產(chǎn)品的時(shí)候自動(dòng)預(yù)約蚂子。經(jīng)測(cè)試4個(gè)產(chǎn)品全部預(yù)約到蜡秽。不過(guò)登錄用戶名、密碼缆镣、客戶手機(jī)、預(yù)約金額试浙、以及準(zhǔn)備預(yù)約的產(chǎn)品都寫(xiě)在python文件中的董瞻。讓她改幾個(gè)字(居然說(shuō)是讓她寫(xiě)代碼,我服了)田巴,本來(lái)想通過(guò)一個(gè)配置文件解決钠糊。但想到python的UI操作還沒(méi)試過(guò)(很久很久以前用過(guò)C語(yǔ)言+GTK),于是想試下pyqt+QT creator(模版只想可視化操作的)壹哺。
第四版:把程序裝進(jìn)殼里——PYQT+Qt Creator
Qt Creator 的設(shè)計(jì)目標(biāo)是使開(kāi)發(fā)人員能夠利用 Qt 這個(gè)應(yīng)用程序框架更加快速及輕易的完成開(kāi)發(fā)任務(wù)抄伍。Qt Creator 包括項(xiàng)目生成向?qū)А⒏呒?jí)的 C++ 代碼編輯器管宵、瀏覽文件及類的工具截珍、集成了 Qt Designer、Qt Assistant箩朴、Qt Linguist岗喉、圖形化的 GDB 調(diào)試前端,集成 qmake 構(gòu)建工具等炸庞。(百度百科)
Qt Creator 可以創(chuàng)建多種工程钱床,我選擇的是qml文件,很快做好了qml文件埠居,但是如何與python通訊呢查牌?百度了下都沒(méi)找到多少系統(tǒng)的文章事期,官方教程也沒(méi)講到(英文)https://doc.qt.io/qtforpython/tutorials/index.html
- 在界面觸發(fā)事件調(diào)用python, 需在python中用pyqtSlot()申明為槽函數(shù) ,并設(shè)置上下文關(guān)聯(lián)纸颜,這樣qml文件中就可以直接調(diào)用兽泣。
@pyqtSlot() # qml中可調(diào)用
def begin(self):
#代碼略
pass
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
qml = QQmlApplicationEngine('ui.qml')
rootObject = qml.rootObjects()[0]
instance = Reserve(qml.rootContext() ,rootObject) #預(yù)約實(shí)例
qml.rootContext().setContextProperty('con',instance ) #與qml文件建立關(guān)聯(lián)
sys.exit(app.exec())
qml文件中綁定事件,觸發(fā)調(diào)用python的槽函數(shù)
Connections {
target: button_start
onClicked: con.begin()
}
- 如何主動(dòng)更改ui界面中的值呢懂衩?例如之前用的print打印日志撞叨,現(xiàn)在需要全部顯示到界面上
在qml文件中自定義方法,類似javascript
function updatelog(log) {// 定義函數(shù)
textArea.append(log)
}
function clearlog() {
textArea.clear()
}
然后可直接
def __init__(self,context,parent=None):
super(Reserve,self).__init__(parent)
self.win = parent
self.ctx = context
#可直接調(diào)用qml中方法
self.win.showlog("測(cè)試日志")
好啦浊洞,界面也有了牵敷,與程序也封裝好了,開(kāi)始搶票吧法希,怎么回事枷餐?界面卡死了。之前學(xué)習(xí)時(shí)候知道需要將界面與程序使用線程分開(kāi)苫亦。
首先創(chuàng)建一個(gè)線程類
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()
#設(shè)置
def setup(self, instance):
self.yuyue = instance
#內(nèi)部/外部(線程)使用的輸出信息
def log(self,message):
self.signal.emit(message)
def run(self):
self.yuyue.config()
self.yuyue.login()
self.yuyue.start()
# 執(zhí)行完畢后發(fā)出信號(hào)
self.log("運(yùn)行完畢")
在主程序Init_方法中毛肋,啟動(dòng)線程,這里兩個(gè)信號(hào)(signal)屋剑,一個(gè)用于打印日志润匙,一個(gè)用于清空日志(日志達(dá)到某個(gè)閥值)
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報(bào)錯(cuò)。
self.driver_name = 'chrome'
self.driver = Browser(driver_name=self.driver_name,chrome_options=chrome_opt)
#啟動(dòng)一個(gè)線程唉匾,并設(shè)置連接通道
self.thread = WorkThread()
self.thread.signal.connect(self.callbacklog)
self.thread.clearsignal.connect(self.callbackclear)
#向通道發(fā)送信息
self.thread.log("初始化完成")
# 保存session
self.s = requests.session()
# 槽函數(shù)(通道末端)
def callbacklog(self, log):
self.win.updatelog(log) # 調(diào)用qml中的方法
pass
def callbackclear(self):
self.win.clearlog() # 調(diào)用qml中的方法
pass
這樣線程原來(lái)使用print()打印日志的方法全部替換成self.thread.log()即可孕讳。運(yùn)行界面一點(diǎn)都不卡了。
結(jié)束語(yǔ):對(duì)python新手來(lái)說(shuō)涉及到面還不少巍膘,雖然不少坑收獲還是不少厂财。我在身邊很多朋友眼中一直都是大神一樣的存在(其實(shí)我知道這些都是小把戲),我朋友在完成第二版的時(shí)候說(shuō)過(guò)一句話讓我很欣慰:
你把我的夢(mèng)境變成現(xiàn)實(shí)了峡懈,太厲害了璃饱。
當(dāng)然還有可以改進(jìn)的地方,比如驗(yàn)證碼完全可以不用手動(dòng)輸入肪康,可以利用機(jī)器學(xué)習(xí)荚恶,對(duì)驗(yàn)證碼進(jìn)行訓(xùn)練,然后自動(dòng)識(shí)別梅鹦。不過(guò)真的沒(méi)必要了裆甩。就她一個(gè)人用確實(shí)沒(méi)必要折騰了。