1. 程式人生 > >中山大學羽毛球場館自動訂場(Python+selenium+百度aip)

中山大學羽毛球場館自動訂場(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以上的版本,否則不支援無頭模式)。

首先配置好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%以上。

但是我發現我們學校驗證碼上的噪音線都是黑色的,而字元通常都是彩色的,這可是個大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地址:

降噪部分的程式碼:

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 所 調 人 , 在 - 方 。
深 從 久 , 定 中 央
。 所 澈 伊 人 , 圭 水 淳
。 淇 渡 從 之 , 定 圭 北 中 阪 。
。 所 澈 伊人 , 圭 水 浩
從 丿 , 定 圭 水 中 瀝 。

這裡重點介紹一下百度文字識別介面的用法:

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

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

'appId': '11352243',

'apiKey': 'Nd5Z1NkGoLDvHwBsD2bFLpCE',

'secretKey': 'A9FsnnPj1Ys2Gofi0SNgYo23hKOIK8Os'

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

用百度的圖形識別介面識別驗證碼的程式碼:

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,後期可能會加上掛到伺服器上定時啟動或者是放到堅果雲上用手機啟動指令碼訂場的功能,但是我要暫停一下去寫高巨集作業啦,不然高巨集可能要涼,等過幾天再更新程式碼吧~

主要參考連結: