想開發網頁爬蟲,發現被反爬了?想對 App 抓包,發現資料被加密了?不要擔心,使用 Airtest 開發 App 爬蟲,只要人眼能看到,你就能抓到,最快只需要2分鐘,相容 Unity3D、Cocos2dx-*、Android 原生 App、iOS App、Windows Mobile……。
Airtest是網易開發的手機UI介面自動化測試工具,它原本的目的是通過所見即所得,截圖點選等等功能,簡化手機App圖形介面測試程式碼編寫工作。
爬蟲開發本著天下工具為我所用,能讓我獲取資料的工具都能用來開發爬蟲這一信念,決定使用Airtest來開發手機App爬蟲。
安裝和使用
由於本文的目的是介紹如何使用Airtest來開發App爬蟲,那麼Airtest作為測試開發工具的方法介紹將會一帶而過,僅僅說明如何安裝並進行基本的操作。
安裝Airtest
從Airtest官網:airtest.netease.com下載Airtest,然後像安裝普通軟體一樣安裝即可。安裝過程沒有什麼需要特別說明的地方。Airtest已經幫你打包好了開發需要的全部環境,所以安裝完成Airtest以後就能夠直接使用了。
Airtest執行以後的介面如下圖所示。
連線手機
以Android手機為例,由於Airtest會通過adb命令安裝兩個輔助App到手機上,再用adb命令通過控制這兩個輔助App進而控制手機,因此首先需要確保手機的adb除錯
功能是開啟的,並允許通過adb命令安裝App到手機上。
啟動Airtest以後,把Android手機連線到電腦上,點選下圖方框中的refresh ADB
:
此時在Airtest介面右上角應該能夠看到手機的資訊,如下圖所示。
點選connect
按鈕,此時可以在介面上看到手機的介面,並且當你手動操作手機螢幕時,Airtest中的手機畫面實時更新。如下圖所示。
對於某些手機,例如小米,在第一次使用Airtest時,請注意手機上將會彈出提示,詢問你是否允許安裝App,此時需要點選允許按鈕。
開啟微信
先通過一個簡單的例子,來看看如何快速上手Airtest,稍後再來詳解。
例如我現在想使用電腦控制手機,開啟微信。
此時,點選下圖中方框框住的touch
按鈕:
此時,把滑鼠移動到Airtest右邊的手機螢幕區域,滑鼠會變成十字型。在微信圖示的左上角按下滑鼠左鍵不放,並拖到微信右下角鬆開滑鼠。此時請注意中間程式碼區域發生了什麼變化,如下圖所示。
好了。以上就是你需要使用電腦開啟微信所要進行的全部操作。
點選上方工具欄中的三角形圖示,執行程式碼,如下圖所示。
程式碼執行完成以後,微信被開啟了。
介面介紹
在有了一個直觀的使用以後,我們再來介紹一下Airtest的介面,將會更加有針對性。
Airtest的介面如下圖所示。
這裡,我把Airtest分成了A-F6個區域,他們的功能如下:
- A區:常用操作功能區
- B區:Python程式碼編寫區
- C區:執行日誌區
- D區:手機螢幕區
- E區:App頁面佈局資訊檢視區
- F區:工具欄
A區是常用的基於影象識別
的螢幕操作功能,例如:
touch
: 點選螢幕元素swipe
: 滑動螢幕exists
: 判斷螢幕元素是否存在text
: 在輸入框中輸入文字snashot
: 截圖- ……
一般來說,是點選A區裡面的某一個功能,然後在D區螢幕上進行框選操作,B區就會自動生成相應的操作程式碼。
B區用來顯示和編寫Python程式碼。在多數情況下,不需要手動寫程式碼,因為程式碼會根據你在手機螢幕上面的操作自動生成。只有一些需要特別定製化的動作才需要修改程式碼。
D區顯示了手機螢幕,當你操作手機真機時,這個螢幕會實時重新整理。你也可以直接在D區螢幕上使用滑鼠操作手機,你的操作動作會被自動在真機上執行。
F區是一些常用工具,從左到右,依次為:
- 新建專案
- 開啟專案
- 儲存專案
- 執行程式碼
- 停止程式碼
- 檢視執行報告
其中1-5很好理解,那麼什麼是檢視執行報告呢?
當你至少執行了一次以後,點選這個功能,會自動給你開啟一個網頁。網頁如下圖所示,這是你的程式碼的執行報告,詳細到每一步操作了什麼元素。
通過截圖功能操作手機雖然方便,但是截圖涉及到解析度的問題,程式碼不能在不同的手機上通用。所以對於A區的功能,做點簡單操作即可,不用深入瞭解。
更高階的功能,需要通過E區實現。
基於App佈局資訊操作手機
初始化程式碼
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操作基本上已經介紹完畢。
單獨使用Python控制手機
在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爬蟲開發 從入門到實戰第十章對於如何搭建手機爬蟲叢集有詳細的說明和注意事項。
如果對我的書有興趣,請關注我的微信公眾號與我交流。