中山大學羽毛球場館自動訂場(Python+selenium+百度aip)

雙鴨山南校人太多,小夥伴們日常約球搶不到室內的場館,只好去室外打。所以趁考完試有時間寫了一個自動搶羽毛球場的腳本,網好的時候20秒訂場無壓力。下面來分享一波這個腳本的一些技術細節(重點講一下圖像降噪和百度雲的文字識別接口),也算是對我這兩天學習的一個總結。

其實在大概在一年之前在自動操作瀏覽器上selenium+phantomjs還是標配,但是隨着17年8月firefox和chrome相繼推出了無頭模式的瀏覽器完美地取代了phantomjs,所以selenium 3.8.1以上的版本都不再支持phantomjs。這個腳本用的是headless firefox,與以往Selenium不同的是需要額外加載selenium firefox 的官方Webdriver:Geckodriver,不然會報錯。另外一個要注意的是如果想用headless firefox需要電腦本身已經安裝了firefox(注意至少是56.0以上的版本,否則不支持無頭模式)。

Gecokdriver下載地址:https://github.com/mozilla/geckodriver/releases

首先配置好driver並獲取鴨大NetID登錄頁面信息:

from selenium import webdriver

driver = webdriver.Firefox(executable_path='.../geckodriver')#根據geckodriver所在路徑配置driver
driver.get("http://gym.sysu.edu.cn/product/show.html?id=61")#獲取頁面信息
driver.maximize_window()#最大化窗口
driver.find_element_by_xpath("//a[contains(text(),'登錄')]").click()#跳轉到登錄頁面

配置好driver後,我們就可以用python在後臺操作網頁啦,一些常用的操作:

driver.get("http://gym.sysu.edu.cn/index.html")#打開網頁
driver.find_element_by_xpath("//a[@href='/product/index.html']").click()#用click()點擊用xpath定位到的網頁元素
driver.find_element_by_xpath("//input[@id='txt_name']").send_keys('英東羽毛球場')#用send_keys()向輸入框中填入數據
driver.execute_script("window.scrollTo(0,500)")#滑動網頁到指定位置
driver.save_screenshot(screenshotadd)#保存當前頁面截圖
driver.page_source()#獲取當前頁面源代碼
driver.current_url#當前頁面的url

我們需要對登錄頁面截圖,根據驗證碼的xpath定位驗證碼在整張截圖的位置並截取保存驗證碼到本地:

def Convertimg():
    imglocation = ("//img[@name='captchaImg']")#驗證碼的xpath地址
    driver.save_screenshot(screenshotadd)
    im = Image.open(screenshotadd)
    left = driver.find_element_by_xpath(imglocation).location['x']
    top = driver.find_element_by_xpath(imglocation).location['y']
    right = driver.find_element_by_xpath(imglocation).location['x'] + driver.find_element_by_xpath(imglocation).size['width']
    bottom = driver.find_element_by_xpath(imglocation).location['y'] + driver.find_element_by_xpath(imglocation).size['height']
    im = im.crop((left, top, right, bottom))
    im.save(codeadd)

原始的驗證碼是這樣的:

雖然現在百度谷歌阿里都有各自的將圖像識別成文字的python接口,但是這些接口在圖像有噪音時普遍很差。比如這裏的驗證碼有多條直線穿過,如果直接用像pytesseract這樣的圖像識別接口來識別的話基本上成功率只有百分之幾,也就是說可能要試100次才能成功登陸幾次,那我這卡着12點搶場地的腳本肯定要涼。所以必須對驗證碼進行降噪再進行圖像識別才行。事實上經過降噪處理的驗證碼識別一次成功率達到了80%以上。

現在比較流行的比較標準的驗證碼識別套路是(圖片來自https://blog.csdn.net/eddy_zheng/article/details/48895085):

但是我發現我們學校驗證碼上的噪音線都是黑色的,而字符通常都是彩色的,這可是個大bug啊哈哈哈。所以我的降噪流程是:

1.將黑色以及接近黑色的噪音線(R,G,B值都接近0)替換爲白色(R,G,B值都是255)

2.將第一步得到的圖像進行二值處理變成黑白圖像,灰度閾值設爲160都可,也就是低於這個閾值的點都填白色,高於這個閾值的點都填黑色

3.提取邊緣,使得字母和數字更清晰(雖然在這張圖不太明顯,但是經過幾十張驗證碼的測試,這一步還是有必要的):


4.把圖片適當變大一點,提高文字識別的精度

到這裏降噪就完成啦,順便介紹一下python圖片處理界的三個大佬:PIL、opencv和skimage。PIL應該是最老的一位大佬,有些年久失修而且目前只支持python 2.x版本,於是有人就把它改造升級成了python 3.x下的pillow,功能也變強了(但仍然是偏基礎的庫)。opencv可以說是高端玩家了,不僅高端還資格老,不僅資格老還在不停地更新。圖像處理對opencv只是小菜,它還能處理視頻,也有深度神經網絡和機器學習功能,這意味着opencv自己就可以完成對文字識別的訓練和深度學習提高識別精度,可以說是圖像處理界的戰鬥機了。skimage看名字就知道它和大名鼎鼎的scikit-learn有點兒關係,功能上比較接近opencv,也是個強大的包。我覺得看documentation應該是學習一個包最快的方式,這裏放上三個包的documentation地址。但是也放上一個寫得不錯的圖像處理的總結比較貼供參考:https://blog.csdn.net/IAMoldpan/article/details/78628460

Documentation地址:

skimage : http://scikit-image.org/docs/stable/

opencv:https://docs.opencv.org/master/d9/df8/tutorial_root.html

PIL:http://pillow.readthedocs.io/en/5.2.x/

降噪部分的代碼:

def clearimage(originadd):
    img = Image.open(originadd)#讀取系統的內照片
    #將黑色干擾線替換爲白色
    width = img.size[0]#長度
    height = img.size[1]#寬度
    for i in range(0,width):#遍歷所有長度的點
        for j in range(0,height):#遍歷所有寬度的點
            data = (img.getpixel((i,j)))#打印該圖片的所有點
            if (data[0]<=25 and data[1]<=25 and data[2]<=25):#RGBA的r,g,b均小於25
                img.putpixel((i,j),(255,255,255,255))#則這些像素點的顏色改成白色
    img = img.convert("RGB")#把圖片強制轉成RGB
    img.save(repadd)#保存修改像素點後的圖片
    #灰度化
    Grayimg = cv2.cvtColor(cv2.imread(repadd), cv2.COLOR_BGR2GRAY)
    ret, thresh = cv2.threshold(Grayimg, 160, 255,cv2.THRESH_BINARY)
    cv2.imwrite(greyadd, thresh)
    #提取邊緣
    edimg = Image.open(greyadd)
    conF = edimg.filter(ImageFilter.CONTOUR)
    conF.save(edadd)
#改變圖片尺寸
def ResizeImage(filein, fileout, width, height, type):
  img = Image.open(filein)
  out = img.resize((width, height),Image.ANTIALIAS) 
  out.save(fileout, type)

其實按照圖像識別的套路,在正式識別之前是要把圖片的字符切成一個一個的然後分別識別的,但是我降噪做得比較乾淨不切片也能識別的很好加上學校網站允許填入的驗證碼之間有空格所以就沒有切片後分別識別,這樣運行速度也快一點。

在圖像降噪上也有一些流行的算法,但是我們對我鴨大的驗證碼效果都不太好,降完後識別率依然很低所以我就棄用了。但是這裏也把這些算法放上來給大家參考:

#去除多餘的點和多餘的干擾線
im = Image.open(rgbadd)
data = im.getdata()
w, h = im.size
try:
    for x in range(1, w - 1):
        if x > 1 and x != w - 2:
            # 獲取目標像素點左右位置
            left = x - 1
            right = x + 1
        for y in range(1, h - 1):
            # 獲取目標像素點上下位置
            up = y - 1
            down = y + 1
            if x <= 2 or x >= (w - 2):
                data.putpixel((x, y), 255)
            elif y <= 2 or y >= (h - 2):
                data.putpixel((x, y), 255)
            elif data.getpixel((x, y)) == 0:
                if y > 1 and y != h - 1:
                    # 以目標像素點爲中心點,獲取周圍像素點顏色
                    # 0爲黑色,255爲白色
                    up_color = data.getpixel((x, up))
                    down_color = data.getpixel((x, down))
                    left_color = data.getpixel((left, y))
                    left_down_color = data.getpixel((left, down))
                    right_color = data.getpixel((right, y))
                    right_up_color = data.getpixel((right, up))
                    right_down_color = data.getpixel((right, down))
                    # 去除豎線干擾線
                    if down_color == 0:
                        if left_color == 255 and left_down_color == 255 and \
                                right_color == 255 and right_down_color == 255:
                            data.putpixel((x, y), 255)
                    # 去除橫線干擾線
                    elif right_color == 0:
                        if down_color == 255 and right_down_color == 255 and \
                                up_color == 255 and right_up_color == 255:
                            data.putpixel((x, y), 255)
                # 去除斜線干擾線
                if left_color == 255 and right_color == 255 \
                        and up_color == 255 and down_color == 255:
                    data.putpixel((x, y), 255)
            else:
                pass
except:
    pass

另一種降噪方法:

# 根據一個點A的RGB值,與周圍的8個點的RBG值比較,設定一個值N(0 <N <8),當A的RGB值與周圍8個點的RGB相等數小於N時,此點爲噪點
# G: Integer 圖像二值化閥值
# N: Integer 降噪率 0 <N <8
# Z: Integer 降噪次數
# 輸出
#  0:降噪成功
#  1:降噪失敗
def clearNoise(image, N, Z):
    for i in range(0, Z):
        t2val[(0, 0)] = 1
        t2val[(image.size[0] - 1, image.size[1] - 1)] = 1
        for x in range(1, image.size[0] - 1):
            for y in range(1, image.size[1] - 1):
                nearDots = 0
                L = t2val[(x, y)]
                if L == t2val[(x - 1, y - 1)]:
                    nearDots += 1
                if L == t2val[(x - 1, y)]:
                    nearDots += 1
                if L == t2val[(x - 1, y + 1)]:
                    nearDots += 1
                if L == t2val[(x, y - 1)]:
                    nearDots += 1
                if L == t2val[(x, y + 1)]:
                    nearDots += 1
                if L == t2val[(x + 1, y - 1)]:
                    nearDots += 1
                if L == t2val[(x + 1, y)]:
                    nearDots += 1
                if L == t2val[(x + 1, y + 1)]:
                    nearDots += 1
                if nearDots < N:
                    t2val[(x, y)] = 1

下面一步就是對降噪完成的圖片進行文字識別得到驗證碼啦,這裏一定要吹一波百度的文字識別接口baidu-aip,私以爲做得相當不錯,在中文的圖像識別上有碾壓谷歌的pytesseract的趨勢。來張無噪聲的圖感受一下:

百度baidu-aip接口識別結果:

蒹葭
先秦:佚名
蒹葭蒼蒼,白露爲霜。所謂伊人,在水一方。
溯洄從之,道阻且長。溯游從之,宛在水中央。
蒹葭萋萋,白露未晞。所謂伊人,在水之湄。
溯洄從之,道陽且躋。溯游從之,宛在水中坻。
蒹葭采采,白露未已。所謂伊人,在水之涘。
溯洄從之,道阻且右。溯游從之,宛在水中沚。

谷歌pytesseract識別結果:
8 所 調 人 , 在 - 方 。
深 從 久 , 定 中 央
。 所 澈 伊 人 , 圭 水 淳
。 淇 渡 從 之 , 定 圭 北 中 阪 。
。 所 澈 伊人 , 圭 水 浩
從 丿 , 定 圭 水 中 瀝 。

這裏重點介紹一下百度文字識別接口的用法:

1.首先註冊一個百度雲賬號https://cloud.baidu.com/

2.在導航欄裏面點文字識別,百度雲提供了很多免費的人工智能的接口,感覺可以說是很棒了,對自然語言處理、自動駕駛、人臉識別感興趣的小夥伴都可以來圍觀一波,一定會有意想不到的驚喜。

點創建應用,勾選文字識別下面的接口,就可以得到百度爸爸給我們的接口密匙啦,密匙包括:

'appId': '11352243',

'apiKey': 'Nd5Z1NkGoLDvHwBsD2bFLpCE',

'secretKey': 'A9FsnnPj1Ys2Gofi0SNgYo23hKOIK8Os'

(舉個栗子大概是這種形式,但是爲了隱私隨便改了幾個字母)

3.安裝baidu-aip包,下載地址:https://pypi.org/project/baidu-aip/。私以爲直接用pip或者brew更方便一點。

用百度的圖形識別接口識別驗證碼的代碼:

config = {
    'appId': '11352243',
    'apiKey': 'Nd5Z1NkGoLDvHwBsD2bFLpCE',
    'secretKey': 'A9FsnnPj1Ys2Gofi0SNgYo23hKOIK8Os'
}

client = AipOcr(**config)
image = open(image_path,'rb').read()
result = client.basicGeneral(image)
with open("/Users/mengjiexu/Documents/badminton/result.txt","a") as f:
    for line in result["words_result"]:
        f.write(line["words"]+"\n")

另外一個在寫程序中遇到的小麻煩是明明可以從源代碼中定位到訂球場頁面的點擊位置,但是運行時總是報錯"could not be scrolled into view",可能是這個元素在瀏覽器中實際上是不能被直接點擊的,所以與一般直接click()的方式不同,我不得不換用了另一種方法去點擊它,於是成功了。

target = driver.find_element_by_xpath("//a[@href='show.html?id=61']")
driver.execute_script("arguments[0].click();", target)

另外分享一些用python和網站進行交互的tips:

1.如果你網速不夠快,就需要在跳轉頁面時加個兩三秒的等待時間或者直接指定等某個元素完全渲染出來再進行下一步,不然即使你的代碼沒錯,也有可能因爲暫時沒有渲染出你下一步要用的元素報錯。舉個栗子(不是這次寫鴨大的程序時候用到的):

import selenium.webdriver.support.ui as ui

wait = ui.WebDriverWait(driver, 10)#等指定元素加載出來再進行下一步
wait.until(lambda driver: driver.find_element_by_xpath("//a[contains(text(),'人才管理')]"))
driver.find_element_by_xpath("//a[contains(text(),'人才管理')]").click()
time.sleep(2)#加兩秒的等待時間

2.有些網頁會有iframe,一般iframe的形式都是跳出一個邊欄把頁面分成兩部分(具體有沒有iframe需要看網頁的源代碼才能完全確定),而iframe裏的元素是不能直接訪問的,需要從主頁面switch過去,舉個栗子:

cvframe = driver.find_element_by_xpath("//div/iframe")#根據xpath定位iframe的位置
driver.switch_to_frame(cvframe)#從主界面切換到iframe以訪問iframe中的元素
html = etree.HTML(driver.page_source)
cvcontent = html.xpath("//html/body/descendant::text()")
with open("{}{}{}".format('/Users/mengjiexu/Documents/parser/', id, '.txt'), 'w') as f:
    for line in cvcontent:
        f.write(line.strip())
f.close()
driver.switch_to_default_content()#從iframe切回主界面

現在只是完成了一個有基礎的訂場功能的demo,後期可能會加上掛到服務器上定時啓動或者是放到堅果雲上用手機啓動腳本訂場的功能,但是我要暫停一下去寫高宏作業啦,不然高宏可能要涼,等過幾天再更新代碼吧~

應廣大熱愛打球的吃瓜小夥伴們的要求,這裏先放上目前的demo:https://github.com/xumj9/sysubadminton

程序實現效果請看:http://t.cn/RewK11A?m=4265757913116630&u=3564852345

 

主要參考鏈接:

https://segmentfault.com/a/1190000012861561

https://blog.csdn.net/u013010889/article/details/54347089

https://www.cnblogs.com/monxue/p/get_random_code.html

https://www.cnblogs.com/qqandfqr/p/7866650.html

https://blog.csdn.net/xinghun_4/article/details/47864949

https://blog.csdn.net/eddy_zheng/article/details/48895085

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章