想開發網頁爬蟲,發現被反爬了?想對 App 抓包,發現數據被加密了?不要擔憂,使用 Airtest 開發 App 爬蟲,只要人眼能看到,你就能抓到,最快只須要2分鐘,兼容 Unity3D、Cocos2dx-*、Android 原生 App、iOS App、Windows Mobile……。html
Airtest是網易開發的手機UI界面自動化測試工具,它本來的目的是經過所見即所得,截圖點擊等等功能,簡化手機App圖形界面測試代碼編寫工做。python
爬蟲開發本着天下工具爲我所用,能讓我獲取數據的工具都能用來開發爬蟲這一信念,決定使用Airtest來開發手機App爬蟲。android
因爲本文的目的是介紹如何使用Airtest來開發App爬蟲,那麼Airtest做爲測試開發工具的方法介紹將會一帶而過,僅僅說明如何安裝並進行基本的操做。正則表達式
從Airtest官網:airtest.netease.com下載Airtest,而後像安裝普通軟件同樣安裝便可。安裝過程沒有什麼須要特別說明的地方。Airtest已經幫你打包好了開發須要的所有環境,因此安裝完成Airtest之後就可以直接使用了。spring
Airtest運行之後的界面以下圖所示。網頁爬蟲
以Android手機爲例,因爲Airtest會經過adb命令安裝兩個輔助App到手機上,再用adb命令經過控制這兩個輔助App進而控制手機,所以首先須要確保手機的adb調試
功能是打開的,並容許經過adb命令安裝App到手機上。api
啓動Airtest之後,把Android手機鏈接到電腦上,點擊下圖方框中的refresh ADB
:bash
此時在Airtest界面右上角應該可以看到手機的信息,以下圖所示。微信
點擊connect
按鈕,此時能夠在界面上看到手機的界面,而且當你手動操做手機屏幕時,Airtest中的手機畫面實時更新。以下圖所示。編輯器
對於某些手機,例如小米,在第一次使用Airtest時,請注意手機上將會彈出提示,詢問你是否容許安裝App,此時須要點擊容許按鈕。
先經過一個簡單的例子,來看看如何快速上手Airtest,稍後再來詳解。
例如我如今想使用電腦控制手機,打開微信。
此時,點擊下圖中方框框住的touch
按鈕:
此時,把鼠標移動到Airtest右邊的手機屏幕區域,鼠標會變成十字型。在微信圖標的左上角按下鼠標左鍵不放,並拖到微信右下角鬆開鼠標。此時請注意中間代碼區域發生了什麼變化,以下圖所示。
好了。以上就是你須要使用電腦打開微信所要進行的所有操做。
點擊上方工具欄中的三角形圖標,運行代碼,以下圖所示。
代碼運行完成之後,微信被打開了。
在有了一個直觀的使用之後,咱們再來介紹一下Airtest的界面,將會更加有針對性。
Airtest的界面以下圖所示。
這裏,我把Airtest分紅了A-F6個區域,他們的功能以下:
A區是經常使用的基於圖像識別
的屏幕操做功能,例如:
touch
: 點擊屏幕元素swipe
: 滑動屏幕exists
: 判斷屏幕元素是否存在text
: 在輸入框中輸入文字snashot
: 截圖通常來講,是點擊A區裏面的某一個功能,而後在D區屏幕上進行框選操做,B區就會自動生成相應的操做代碼。
B區用來顯示和編寫Python代碼。在多數狀況下,不須要手動寫代碼,由於代碼會根據你在手機屏幕上面的操做自動生成。只有一些須要特別定製化的動做才須要修改代碼。
D區顯示了手機屏幕,當你操做手機真機時,這個屏幕會實時刷新。你也能夠直接在D區屏幕上使用鼠標操做手機,你的操做動做會被自動在真機上執行。
F區是一些經常使用工具,從左到右,依次爲:
其中1-5很好理解,那麼什麼是查看運行報告呢?
當你至少運行了一次之後,點擊這個功能,會自動給你打開一個網頁。網頁以下圖所示,這是你的代碼的運行報告,詳細到每一步操做了什麼元素。
經過截圖功能操做手機雖然方便,可是截圖涉及到分辨率的問題,代碼不能在不一樣的手機上通用。因此對於A區的功能,作點簡單操做便可,不用深刻了解。
更高級的功能,須要經過E區實現。
App的佈局信息就像網頁的HTML同樣,保存了App上面各個元素的相對位置和各個參數。對於一個App而言,在不一樣分辨率的手機上,可能相同的元素有着不一樣的座標點,可是這個元素的屬性參數通常是不會變的。所以,若是使用元素的屬性參數來尋找並控制這個元素,就能實如今不一樣分辨率手機上的精肯定位。
App的佈局信息的格式與App的開發環境有關。點擊F區的下拉菜單,能夠看到這裏可以指定不一樣的App開發環境。其中的Unity
、Cocos-*
等等通常是作遊戲用的,Android
是安卓原生App,iOS
是蘋果的App……以下圖所示。
以手機版知乎爲例,因爲它是Android原生的App,因此在F區下拉菜單選擇Android
,此時注意B區彈出提示,詢問你是否要插入poco初始代碼到當前輸入光標的位置,點擊Yes
,以下圖所示。
此時,B區自動插入了一段代碼,以下圖所示。
如今,點擊E區的鎖形圖標,以下圖所示。
鎖形圖標激活之後,你再操做D區的屏幕,點擊知乎
App下面的知乎
兩個字,會發現屏幕上被點擊的App並不會打開。但E區和C區卻發生了變化,以下圖所示。
其中E區顯示的樹狀結構就是當前屏幕的佈局信息,這與Chrome開發者工具裏面顯示的HTML結構一模一樣。C區顯示的是當前被我點中的元素的信息。
請注意在這些元素信息中,有一個text
屬性,它的值爲知乎
。那麼,這個屬性就能夠做爲一個定位元素,因而能夠在B區編寫代碼:
poco(text="知乎").click()
複製代碼
寫完代碼之後運行程序,能夠看到知乎App被打開了。以下圖所示。
注意,若是你發現手機真機顯示的界面與Airtest屏幕顯示的手機界面不一致,多是由於Airtest的屏幕被你鎖定了。在F區點一下鎖形圖標,取消鎖定,Airtest中的手機屏幕就會更新了。
打開知乎之後,我想使用知乎的搜索功能,那麼繼續,把鎖形圖標激活,而後點擊知乎頂部的搜索框,以下圖所示:
繼續看C區顯示的搜索框屬性,能夠看到這裏有一個name
屬性,它的值是com.zhihu.android:id/input
,還有一個text
屬性,它的值爲蔡徐坤任 NBA 新春賀歲大使
。能不能像前面打開知乎同樣,使用text
這個屬性呢?也行,也不行。說它行,是由於你這麼作確實如今能工做;說它不行,由於這是知乎的熱門搜索關鍵詞,隨時會改變。你今天使用這一句話成功了,明天熱門關鍵詞變化了,那麼你的代碼就沒法使用了。因此此時須要使用name
這個屬性。
常見的基本上不會變化的屬性包含但不限於:name
type
resourceId
package
。
另外還有一點,知乎首頁的這個搜索框,其實是不能輸入內容的,當你點擊之後,會跳轉到另外一個頁面,以下圖所示。
所以你須要先點擊一下這個輸入框,跳轉到真正的搜索界面:
poco(name="com.zhihu.android:id/input").click()
複製代碼
在真正的搜索界面以下圖所示。
能夠看到,name
屬性的值依然是com.zhihu.android:id/input
,此時就能夠輸入內容了。
輸入內容使用的方法爲set_text
,用法爲:
poco(name="com.zhihu.android:id/input").set_text('古劍奇譚三')
複製代碼
輸入了搜索關鍵詞之後,再來看看當前頁面,搜索出現了三個結果:
經過對比這三個結果的屬性信息,發現他們的name
屬性都是相同的,而text
不一樣。若是像下面這樣寫點擊動做:
poco(name='com.zhihu.android:id/magi_title').click()
複製代碼
那麼默認就會點擊第一個搜索結果。
若是我想點擊第二個搜索結果怎麼辦呢?能夠這樣寫代碼:
poco(name='com.zhihu.android:id/magi_title', text='古劍奇譚(電視劇)').click()
複製代碼
或者你也能夠像列表同樣使用索引定位:
poco(name='com.zhihu.android:id/magi_title')[1].click()
複製代碼
這兩種寫法的前提,都是咱們已經知道了每一個結果分別是什麼。假設如今我就想搜索古劍奇譚三
,但我不知道搜索結果是第幾項,又應該怎麼辦呢?此時還可使用正則表達式:
poco(name='com.zhihu.android:id/magi_title', textMatches='^古劍奇譚三.*$').click()
複製代碼
進入搜索結果之後,須要查看下面的各類問題,此時就須要不斷向上滑動屏幕。這裏有一點須要特別注意,Airtest只能獲取當前屏幕上的元素佈局信息,不在屏幕上的內容是沒法獲取的。這一點和Selenium是不同的。
滑動屏幕使用的命令爲swipe
,滑動屏幕須要使用座標信息。但這種座標和屏幕分辨率無關。這裏的座標
定義爲:(x, y),其中x爲橫座標,y爲縱座標。屏幕左上角爲(0, 0),屏幕右下角爲(1, 1),從左向右,橫座標從0逐漸增大到1,從上到下,縱座標從0逐漸增大到1。
如今我要把屏幕向上滑動,那麼在真機上面,我是先按住屏幕下方,而後把屏幕向上滑動,因此代碼能夠這樣寫:
# poco.swipe(起點座標,終點左邊)
poco.swipe([0.5, 0.8], [0.5, 0.2])
複製代碼
方向示意圖以下圖所示:
在通常狀況下:
在爬蟲開發中,涉及到的Airtest操做基本上已經介紹完畢。
在Airtest操做手機雖然方便,可是不可能在每一臺電腦上都安裝Airtest吧。因此須要想辦法把代碼從Airtest這個程序中分離出來。
Airtest基於Python的一個開源庫Poco開發,而在Airtest的B區寫的Python代碼,實際上就是Poco的代碼。因此只要安裝Poco庫,就能夠在Python中直接控制手機。
安裝Poco庫的命令爲:
pip install pocoui
複製代碼
這個庫依賴的東西有點多,安裝稍稍慢一些。安裝完成之後,咱們把代碼複製到PyCharm中,以下圖所示。
運行這段代碼,若是是Linux或者macOS的用戶,請注意看運行結果是否是有報錯,提示adb沒有運行權限。這是由於隨Poco安裝的adb沒有運行權限,須要給它添加權限,在終端執行命令:
# chmod +x 報錯信息中給出的adb地址
chmod +x /Users/kingname/.local/share/virtualenvs/ZhihuSpider/lib/python3.7/site-packages/airtest/core/android/static/adb/mac/adb(實際執行時請換成你的地址)
複製代碼
命令運行完成之後再次執行代碼,能夠看到代碼運行成功,手機被成功控制了,以下圖所示。
因爲Airtest的編輯器中的代碼運行後沒法正常打印出中文,所以後面的代碼都直接在PyCharm中執行。
既然要作爬蟲,就須要獲取手機上的文字內容。回到搜索頁面,我想知道「古劍奇譚」三這個關鍵字能搜索出多少條結果,每條結果有多少個討論,以下圖所示:
此時咱們須要作兩件事情:
E區的樹狀結構以下圖所示:
每個搜索結果的標題做爲text屬性的值,在name='com.zhihu.android:id/magi_title'
對應的元素中;每個搜索結果的討論數做爲text屬性的值,在name='com.zhihu.android:id/magi_count'
對應的元素中。
最直接的作法就是分別獲取三個標題和三個討論數,而後把它們合併在一塊兒:
title_obj_list = poco(name='com.zhihu.android:id/magi_title')
title_list = [title.get_text() for title in title_obj_list]
discuss_obj_list = poco(name='com.zhihu.android:id/magi_count')
discuss_list = [discuss.get_text() for discuss in discuss_obj_list]
for title, discuss in zip(title_list, discuss_list):
print(title, discuss)
複製代碼
運行效果以下圖所示:
可是這種作法其實是很危險的,假設會有某一個很生僻的搜索結果,只有標題沒有討論數,那麼這樣分開抓取再組合的作法,就會致使最後匹配錯位。因此合理的作法是先抓大再抓小。每一組標題和討論數,他們都有本身的父節點,以下圖箭頭所指向的三個android.widget.LinearLayout
:
那麼如今,使用先抓大再抓小的技巧,先把每一組結果的父節點抓下來,再到每個結果裏面分別獲取標題和討論數。
然而這個父節點又怎麼獲取呢?以下圖所示,這個父節點每個屬性值都沒有什麼特殊的,寫任何一個都有可能與別的節點撞上。
此時,最簡單的辦法,就是在E區,雙擊父節點。定位代碼就會自動添加,以下圖所示。
這個定位代碼看起來很是複雜,但實際上它的內在邏輯很是簡單,就是從頂層一層一層往下找而已。
自動生成的定位代碼以下:
poco("android.widget.LinearLayout").offspring("com.zhihu.android:id/action_bar_root").offspring("com.zhihu.android:id/parent_fragment_content_id").offspring("android.support.v7.widget.RecyclerView").child("android.widget.LinearLayout")[0]
複製代碼
在這個自動生成的定位代碼中,咱們看到了offspring
、child
這兩種方法。其中child
表明子節點,offspring
表明孫節點、孫節點的子節點、孫節點的孫節點……。簡言之,使用child
只會在子節點中搜索須要的內容,而使用offspring
會像文件夾遞歸同樣把裏面的全部節點都遍歷一次,直到找到符合條件的屬性爲止。顯然,offspring速度會比child慢。
實際上,咱們能夠對這個定位代碼作一些精簡:
poco("com.zhihu.android:id/parent_fragment_content_id").offspring("android.support.v7.widget.RecyclerView").child("android.widget.LinearLayout")[0]
複製代碼
這個精簡的方法,與從Chrome複製的XPath中進行精簡是同樣的邏輯,根本原則就是找到「獨一無二」的屬性值,而後用這個屬性值來進行定位。
因爲我點擊的是第一個搜索結果,因此定位代碼的最後有一個[0]
。如今因爲須要得到全部搜索結果的內容,因此應該去掉[0]
而使用for循環展開,而後獲取裏面的內容:
result_obj = poco("com.zhihu.android:id/parent_fragment_content_id").offspring("android.support.v7.widget.RecyclerView").child("android.widget.LinearLayout")
for result in result_obj:
title = result.child(name='com.zhihu.android:id/magi_title').get_text()
count = result.child(name='com.zhihu.android:id/magi_count').get_text()
print(title, count)
複製代碼
運行效果以下圖所示。
當咱們在電腦上插入多個Android手機時,執行命令:
adb devices -l
複製代碼
運行效果以下圖所示。
每一個手機都會被列出來。在最左邊的編號就是手機串號。使用這個串號能夠指定多個手機:
from airtest.core.api import auto_setup
from airtest.core.android import Android
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
auto_setup(__file__)
device_1 = Android('76efadf3a7ce4')
device_2 = Android('adfasdfasf23')
device_3 = Android('adifu39ernla')
poco_1 = AndroidUiautomationPoco(device_1, use_airtest_input=True, screenshot_each_action=False)
poco_2 = AndroidUiautomationPoco(device_2, use_airtest_input=True, screenshot_each_action=False)
poco_3 = AndroidUiautomationPoco(device_3, use_airtest_input=True, screenshot_each_action=False)
複製代碼
經過這種方式,在一臺電腦上使用USBHub,連上二三十臺手機是徹底沒有問題的。
Airtest支持無線模式,不須要USB,只要電腦和手機鏈接同一個WIFI就能控制:
若是你們對如何開啓無線模式有興趣,請留言,我就會繼續寫。
一臺電腦能夠鏈接三十臺手機,那麼若是有不少電腦和不少手機,就能夠實現手機爬蟲集羣,其運行效果以下圖所示。
關於如何搭建爬蟲集羣,已經超出本文的範圍了。若是你們有興趣,能夠閱讀個人書:Python爬蟲開發 從入門到實戰第十章對於如何搭建手機爬蟲集羣有詳細的說明和注意事項。
若是對個人書有興趣,請關注個人微信公衆號與我交流。