青南

青南 查看完整檔案

上海編輯電子科技大學  |  電子信息工程 編輯高級研發工程師  |  NewsBreak 編輯 www.kingname.info 編輯
編輯

微軟最有價值專家(MVP)。已出版圖書《Python?爬蟲開發,從入門到實戰》、《左手?MongoDB,右手?Redis——從入門到商業實戰》。獨立開發維護開源項目?GNE(獲得近2000?Star)。

個人動態

青南 發布了文章 · 2月18日

小問題大隱患:如何正確設置 Python 項目的入口文件?

今天在公眾號粉絲群里面,有一位同學提到了 Python 找不到模塊的問題:

問題涉及到的代碼結構和代碼截圖如下:

這個問題的解決方法非常簡單,就是把start.py文件從bin文件夾移出來就好了。

但如果對這個問題進一步分析,可以看到更多問題。

在我以前的文章:為什么Python代碼能運行但是PyCharm給我畫紅線?中,我講到了工作區(Workdir)對代碼的影響。PyCharm、VSCode 識別的工作區,可能并不等于你直接在終端窗口運行.py文件時候的工作區。

今天這個問題本質上也是工作區導致的問題。 這個同學的項目根目錄是MY_API,所以他使用的編輯器VSCode 就會默認把MY_API當做工作區。所以,當他在start.py文件中寫上from lib.interface import server時,VScode 并不會給他標記紅色波浪線。因為從 VSCode 的視角看,lib文件夾確實就是在工作區下面的。

但是,當他在 VSCode 里面運行這個start.py文件時,Python 是從bin文件夾下面運行的。此時,Python 會把bin文件夾當做工作區。在工作區里面就只有這一個start.py文件,所以當然找不到lib文件夾。

如果僅僅從技術上來說,你非要導入 bin 文件夾的父文件夾下面的其他模塊,也并不困難,我在一日一技:導入父文件夾中的模塊并讀取當前文件夾內的資源一文中講到了具體的做法。

但問題在于,你不應該這樣做。你不應該把項目的入口文件,放到項目內部很深的文件夾中。

所謂入口文件,就是要首先經過它,才能到達其他的文件。當你拿到一個 Python 項目,你只需要首先從入口文件開始閱讀代碼,根據入口文件調用的模塊,一路看下去,你就能讀到它的所有實現邏輯。

但如果大家經常逛 Github,就會發現,有些人可能是被其他垃圾語言污染了思想,他的 Python 項目,根目錄有五六個文件夾和七八個.py文件。你拿到這個項目的時候,你甚至不知道,當你想運行這個代碼的時候,python3 xxx.py應該運行哪個文件。你多方打聽,或者看了半天文檔,才知道,哦,原來入口文件在com/xx/yy/zz/script/run.py。

當你打開這個run.py文件,你發現它的頂部,文件導入的代碼寫的是from ../../../../aaa import bbb。

簡直是神經病寫法。我知道有些垃圾語言流行這樣寫。但現在你用的是 Python,學聰明一點,別那樣寫。

對于一個 Python 項目來說,入口文件應該始終在最外層。例如:

當你要啟動這個項目的時候,直接在最外層python3 main.py,就能把它啟動起來。在main.py里面,你可以導入其他模塊,然后調用其他模塊里面的類或者函數。

這樣做的好處是什么?這樣做,你是在項目的根目錄啟動的這個項目,所以你的工作區就是項目的根目錄。那么你在任何一個.py文件里面都可以很容易地基于工作區導入任意其他文件。例如,你現在在models/mongo-util/mongob_helper.py文件中,你想導入utils/abc.py中的time_format()函數,那么,你只需要這樣寫就可以了。

from utils.abc import time_format

你根本不可能出現需要導入父文件夾中的某個模塊的情況。

只有工具腳本,才需要單獨使用一個文件夾來存放,然后調用父文件夾中的其他文件。例如,我現在有一個工具腳本,它每天晚上0點會讀寫 MongoDB,清理無效數據,那么此時,我可以在根目錄單獨創建一個scripttools或者bin文件夾,然后把工具腳本放進去,例如:

在這個工具腳本里面,你可能會調用models/mongo-util/mongob_helper.py文件中的某個函數。這種情況下,你調用父文件夾中的內容是可以接受的。但這畢竟只是工具腳本。

可能還有同學要問,那如果我的項目是一個 Python 的包,它本身沒有入口文件怎么辦呢?這個時候,你可以把這個包的__init__.py當做它的入口文件。大家可以參考我在 GitHub - kingname/GeneralNewsExtractor: 新聞網頁正文通用抽取器 Beta 版.的代碼組織結構。在項目根目錄留下一個example.py文件,用來演示如何調用這個包。而這個包本身的代碼,是在一個叫做gne的文件夾中的。這個gne文件夾是一個包,它的入口文件在__init__.py中。

各位,當你寫代碼的時候,你先想一想,如果別人拿到了你的代碼,想要梳理一下這個項目的邏輯,在不詢問你的情況下,怎么讓他知道應該從哪個文件開始讀?應該按什么順序讀?他能不能輕易地看到數據在你的代碼中是怎么運轉的?

查看原文

贊 0 收藏 0 評論 0

青南 發布了文章 · 2月16日

在 Linux 服務器中創建假桌面運行模擬瀏覽器有頭模式

經常使用 Selenium 或者 Puppeteer 的同學都知道,他們啟動的 Chrome 瀏覽器分為有頭模式和無頭模式。在自己電腦上操作時,如果是有頭模式,會彈出一個 Chrome 瀏覽器窗口,然后你能看到這個瀏覽器里面在自動操作。而無頭模式則不會彈出任何窗口,只有進程。

別去送死了。Selenium 與 Puppeteer 能被網站探測的幾十個特征這篇文章中,我們介紹了一個探測模擬瀏覽器特征的網站。通過他我們可以發現,在不做任何設置的情況下,Selenium 或者 Puppeteer 啟動的瀏覽器有幾十個特征能夠被目標網站識別為爬蟲。并且,無頭模式的特征比有頭模式的特征多得多。

也就是說,即使你不使用任何隱藏特征的技術,僅僅使用有頭模式,你都會安全很多。如果網站不是非常嚴格的反爬蟲,很多情況下,使用無頭模式更容易被發現,但使用有頭模式,更難被發現。

下圖為有頭模式,不使用任何隱藏特征的技術訪問檢測網站:

下圖為無頭模式,不使用任何隱藏特征的技術訪問檢測網站:

萬里河山一片紅

所以,一般情況下,你應該多使用有頭模式。

但問題在于,當我們要在 Linux 服務器上面使用 Selenium 或者 Puppeteer 運行爬蟲的時候,就會發現有頭模式始終會報錯。這是因為,有頭模式需要系統提供圖形界面支持,才能繪制瀏覽器窗口,但是 Linux 服務器一般來說是沒有圖形界面的,所以有頭模式一定會失敗。

在這種情況下,為了能夠使用模擬瀏覽器的有頭模式,我們需要搞一個假的圖形界面出來,從而欺騙瀏覽器,讓它的有頭模式能夠正常使用。

為了達到這個目的,我們可以使用一個叫做 Xvfb的東西。這個東西在維基百科上面的介紹如下:

Xvfb or X virtual framebuffer is a display server implementing the X11 display server protocol. In contrast to other display servers, Xvfb performs all graphical operations in virtual memory without showing any screen output.

Xvfb 在一個沒有圖像設備的機器上實現了 X11顯示服務的協議。它實現了其他圖形界面都有的各種接口,但并沒有真正的圖形界面。所以當一個程序在 Xvfb 中調用圖形界面相關的操作時,這些操作都會在虛擬內存里面運行,只不過你什么都看不到而已。

使用 Xvfb,我們就可以欺騙 Selenium 或者 Puppeteer,讓它以為自己運行在一個有圖形界面的系統里面,這樣一來就能夠正常使用有頭模式了。

要安裝 Xvfb 非常簡單,在 Ubuntu 中,只需要執行下面兩行命令就可以了:

sudo apt-get update
sudo apt-get install xvfb

現在,我們來寫一段非常簡單的 Selenium 操作 Chrome 的代碼:

import time
from selenium.webdriver import Chrome
driver = Chrome('./chromedriver')
driver.get('https://bot.sannysoft.com/')
time.sleep(5)
driver.save_screenshot('screenshot.png')
driver.close()
print('運行完成')

如果直接在服務器上運行,效果如下圖所示:

因為沒有圖形界面,所以程序必定報錯。

現在,我們只需要在運行這段代碼的命令前面加上xvfb-run,再來看看運行效果:

代碼成功運行,沒有報錯?,F在我們從服務器上把這個生成的screenshot.png文件拉下來,打開以后可以看到內容如下:

可以看到,雖然窗口比較小,但確實是有頭模式下面的檢測結果。當然,我們也可以調整一下窗口大小,增加參數:xvfb-run python3 test.py -s -screen 0 1920x1080x16就能假裝在一個分辨率為1920x1280的顯示器上運行程序了。然后修改 Selenium 的代碼,設置瀏覽器窗口的大?。?/p>

運行效果如下圖所示:

本文演示使用的是 Python操作 Selenium,你也可以試一試使用 Puppeteer,只需要把啟動命令改為xvfb-run node index.js就可以了。

有了本文以后,再結合我之前的兩篇文章:

相信你的模擬瀏覽器能夠繞過更多的網站。

查看原文

贊 11 收藏 6 評論 0

青南 分享了頭條 · 2020-01-01

目前最好用的新聞網站通用抽取起,經過數百個網站測試,準確率接近100%

贊 0 收藏 0 評論 0

青南 發布了文章 · 2019-12-14

為什么每一個爬蟲工程師都應該學習 Kafka

這篇文章不會涉及到Kafka 的具體操作,而是告訴你 Kafka 是什么,以及它能在爬蟲開發中扮演什么重要角色。

一個簡單的需求

假設我們需要寫一個微博爬蟲,老板給的需求如下:

開發爬蟲對你來說非常簡單,于是三下五除二你就把爬蟲開發好了:

接下來開始做報警功能,邏輯也非常簡單:

再來看看統計關鍵詞的功能,這個功能背后有一個網頁,會實時顯示抓取數據量的變化情況,可以顯示每分鐘、每小時的某個關鍵詞的抓取量。

這個功能對你來說也挺簡單,于是你實現了如下邏輯:

最后一個需求,對微博數據進行情感分析。情感分析的模塊有別的部門同事開發,你要做的就是每個小時拉取一批數據,發送到接口,獲取返回,然后存入后端需要的數據庫:

任務完成,于是你高興地回家睡覺了。

困難接踵而至

爬蟲變慢了

隨著老板逐漸增加新的關鍵詞,你發現每一次完整抓取的時間越來越長,一開始是2分鐘抓取一輪,后來變成10分鐘一輪,然后變成30分鐘一輪,接下來變成1小時才能抓取一輪。隨著延遲越來越高,你的報警越來越不準確,微博都發出來一小時了,你的報警還沒有發出來,因為那一條微博還沒有來得及入庫。

你的爬蟲技術非常好,能繞過所有反爬蟲機制,你有無限個代理 IP,于是你輕輕松松就把爬蟲提高到了每秒一百萬并發?,F在只需要1分鐘你就能完成全部數據的抓取。這下沒問題了吧。

可是報警還是沒有發出來。這是怎么回事?

數據庫撐不住了

經過排查,你發現了問題。數據抓取量上來了,但是 MongoDB 卻無法同時接收那么多的數據寫入。數據寫入速度遠遠小于爬取數據,大量的數據堆積在內存中。于是你的服務器爆炸了。

你緊急搭建了100個數據庫并編號0-99,對于抓取到的微博,先把每一條微博的 ID對100求余數,然后把數據存入余數對應的 MongoDB 中。每一臺 MongoDB 的壓力下降到了原來的1%。數據終于可以即時存進數據庫里面了。

可是報警還是沒有發出來,不僅如此,現在實時抓取量統計功能也不能用了,還有什么問題?

查詢來不及了

現在報警程序要遍歷100個數據庫最近5分鐘里面的每一條數據,確認是否有需要報警的內容。但是這個遍歷過程就遠遠超過5分鐘。

時間錯開了

由于微博的綜合搜索功能不是按照時間排序的,那么就會出現這樣一種情況,早上10:01發的微博,你在12:02的時候才抓到。

不論你是在報警的時候篩選數據,還是篩選數據推送給 NLP 分析接口,如果你是以微博的發布時間來搜索,那么這一條都會被你直接漏掉——當你在10:05的時候檢索10:00-10:05這5分鐘發表的微博,由于這一條微博沒有抓到,你自然搜索不到。

當你12:05開始檢索12:00-12:05的數據時,你搜索的是發布時間為12:00-12:05的數據,于是10:01這條數據雖然是在12:02抓到的,但你也無法篩選出來。

那么是不是可以用抓取時間來搜索呢?例如10:05開始檢索在10:00-10:05抓取到的數據,無論它的發布時間是多少,都檢索出來。

這樣做確實可以保證不漏掉數據,但這樣做的代價是你必需保存、檢索非常非常多的數據。例如每次抓取,只要發布時間是最近10小時的,都要保存下來。于是報警程序在檢索數據時,就需要檢索這5分鐘入庫的,實際上發布時間在10小時內的全部數據。

什么,你說每次保存之前檢查一下這條微博是否已經存在,如果存在就不保存?別忘了批量寫入時間都不夠了,你還準備分一些時間去查詢?

臟數據來了

老板突然來跟你說,關鍵詞“籃球”里面有大量的關于 蔡徐坤的內容,所以要你把所有包含蔡徐坤的數據全部刪掉。

那么,這個過濾邏輯放在哪里?放在爬蟲的 pipelines.py 里面嗎?那你要重新部署所有爬蟲。今天是過濾蔡徐坤,明天是過濾范層層,后天是過濾王一博,每天增加關鍵詞,你每天都得重新部署爬蟲?

那你把關鍵詞放在 Redis 或者 MongoDB 里面,每次插入數據前,讀取所有關鍵詞,看微博里面不包含再存。

還是那個問題,插入時間本來就不夠了,你還要查數據庫?

好,關鍵詞過濾不放在爬蟲里面了。你寫了一個腳本,每分鐘檢查一次MongoDB新增的數據,如果發現包含 不需要的關鍵詞,就把他刪除。

現在問題來了,刪除數據的程序每分鐘檢查一次,報警程序每5分鐘檢查一次。中間必定存在某些數據,還沒有來得及刪除,報警程序就報警了,老板收到報警來看數據,而你的刪除程序又在這時把這個臟數據刪了。

這下好了,天天報假警,狼來了的故事重演了。

5個問題1個救星

如果你在爬蟲開發的過程中遇到過上面的諸多問題,那么,你就應該試一試使用 Kafka。一次性解決上面的所有問題。

把 Kafka 加入到你的爬蟲流程中,那么你的爬蟲架構變成了下面這樣:

這看起來似乎和數據直接寫進 MongoDB 里面,然后各個程序讀取 MongoDB 沒什么區別???那 Kafka 能解決什么問題?

我們來看看,在這個爬蟲架構里面,我們將會用到的 Kafka 的特性:

與其說 Kafka 在這個爬蟲架構中像 MongoDB,不如說更像 Redis 的列表。

現在來簡化一下我們的模型,如果現在爬蟲只有一個需求,就是搜索,然后報警。那么我們可以這樣設計:

爬蟲爬下來的數據,直接塞進 Redis 的列表右側。報警程序從 Redis 列表左側一條一條讀取。讀取一條檢視一條,如果包含報警關鍵詞,就報警。然后讀取下一條。

這樣做有什么好處?

因為報警程序直接從 Redis 里面一條一條讀取,不存在按時間搜索數據的過程,所以不會有數據延遲的問題。由于 Redis 是單線程數據庫,所以可以同時啟動很多個報警程序。由于 lpop 讀取一條就刪除一條,如果報警程序因為某種原因崩潰了,再把它啟動起來即可,它會接著工作,不會重復報警。

但使用 Redis 列表的優勢也是劣勢:列表中的信息只能消費1次,被彈出了就沒有了。

所以如果既需要報警,還需要把數據存入 MongoDB 備份,那么只有一個辦法,就是報警程序檢查完數據以后,把數據存入 MongoDB。

可我只是一個哨兵,為什么要讓我做后勤兵的工作?

一個報警程序,讓它做報警的事情就好了,它不應該做儲存數據的事情。

而使用 Kafka,它有 Redis 列表的這些好處,但又沒有 Redis 列表的弊端!

我們完全可以分別實現4個程序,不同程序之間消費數據的快慢互不影響。但同一個程序,無論是關閉再打開,還是同時運行多次,都不會重復消費。

程序1:報警

從 Kafka 中一條一條讀取數據,做報警相關的工作。程序1可以同時啟動多個。關了再重新打開也不會重復消費。

程序2:儲存原始數據

這個程序從 Kafka 中一條一條讀取數據,每湊夠1000條就批量寫入到 MongoDB 中。這個程序不要求實時儲存數據,有延遲也沒關系。 存入MongoDB中也只是原始數據存檔。一般情況下不會再從 MongoDB 里面讀取出來。

程序3:統計

從 Kafka 中讀取數據,記錄關鍵詞、發布時間。按小時和分鐘分別對每個關鍵詞的微博計數。最后把計數結果保存下來。

程序4:情感分析

從 Kafka 中讀取每一條數據,湊夠一批發送給 NLP 分析接口。拿到結果存入后端數據庫中。

如果要清洗數據怎么辦

4個需求都解決了,那么如果還是需要你首先移除臟數據,再分析怎么辦呢?實際上非常簡單,你加一個 Kafka(Topic) 就好了!

大批量通用爬蟲

除了上面的微博例子以外,我們再來看看在開發通用爬蟲的時候,如何應用 Kafka。

在任何時候,無論是 XPath 提取數據還是解析網站返回的 JSON,都不是爬蟲開發的主要工作。爬蟲開發的主要工作一直是爬蟲的調度和反爬蟲的開發。

我們現在寫 Scrapy 的時候,處理反爬蟲的邏輯和提取數據的邏輯都是寫在一個爬蟲項目中的,那么在開發的時候實際上很難實現多人協作。

現在我們把網站內容的爬蟲和數據提取分開,實現下面這樣一個爬蟲架構:

爬蟲開發技術好的同學,負責實現繞過反爬蟲,獲取網站的內容,無論是 HTML 源代碼還是接口返回的JSON。拿到以后,直接塞進 Kafka。

爬蟲技術相對一般的同學、實習生,需要做的只是從 Kafka 里面獲取數據,不需要關心這個數據是來自于 Scrapy 還是 Selenium。他們要做的只是把這些HTML 或者JSON 按照產品要求解析成格式化的數據,然后塞進 Kafka,供后續數據分析的同學繼續讀取并使用。

如此一來,一個數據小組的工作就分開了,每個人做各自負責的事情,約定好格式,同步開發,互不影響。

為什么是 Kafka 而不是其他

上面描述的功能,實際上有不少 MQ 都能實現。但為什么是 Kafka 而不是其他呢?因為Kafka 集群的性能非常高,在垃圾電腦上搭建的集群能抗住每秒10萬并發的數據寫入量。而如果選擇性能好一些的服務器,每秒100萬的數據寫入也能輕松應對。

總結

這篇文章通過兩個例子介紹了 Kafka 在爬蟲開發中的作用。作為一個爬蟲工程師,作為我的讀者。請一定要掌握 Kafka。

下一篇文章,我們來講講如何使用 Kafka。比你在網上看到的教程會更簡單,更容易懂。

關注本公眾號,回復“爬蟲與Kafka”獲取本文對應的思維導圖原圖。

查看原文

贊 15 收藏 10 評論 1

青南 發布了文章 · 2019-09-09

新聞類網頁正文通用抽取器

項目起源

開發這個項目,源自于我在知網發現了一篇關于自動化抽取新聞類網站正文的算法論文——《基于文本及符號密度的網頁正文提取方法》

這篇論文中描述的算法看起來簡潔清晰,并且符合邏輯。但由于論文中只講了算法原理,并沒有具體的語言實現,所以我使用 Python 根據論文實現了這個抽取器。并分別使用今日頭條、網易新聞、游民星空、觀察者網、鳳凰網、騰訊新聞、ReadHub、新浪新聞做了測試,發現提取效果非常出色,幾乎能夠達到100%的準確率。

項目現狀

在論文中描述的正文提取基礎上,我增加了標題、發布時間和文章作者的自動化探測與提取功能。

最后的輸出效果如下圖所示:

目前這個項目是一個非常非常早期的 Demo,發布出來是希望能夠盡快得到大家的使用反饋,從而能夠更好地有針對性地進行開發。

本項目取名為抽取器,而不是爬蟲,是為了規避不必要的風險,因此,本項目的輸入是 HTML,輸出是一個字典。請自行使用恰當的方法獲取目標網站的 HTML。

本項目現在不會,將來也不會提供主動請求網站 HTML 的功能。

如何使用

項目代碼中的GeneralNewsCrawler.py提供了本項目的基本使用示例。

  • 本項目的測試代碼在test文件夾中
  • 本項目的輸入 HTML 為經過 JavaScript 渲染以后的 HTML,而不是普通的網頁源代碼。所以無論是后端渲染、Ajax 異步加載都適用于本項目。
  • 如果你要手動測試新的目標網站或者目標新聞,那么你可以在 Chrome 瀏覽器中打開對應頁面,然后開啟開發者工具,如下圖所示:

Elements標簽頁定位到<html>標簽,并右鍵,選擇Copy-Copy OuterHTML,如下圖所示

  • 當然,你可以使用 Puppeteer/Pyppeteer、Selenium 或者其他任何方式獲取目標頁面的JavaScript渲染后的源代碼。
  • 獲取到源代碼以后,通過如下代碼提取信息:
from GeneralNewsCrawler import GeneralNewsExtractor

extractor = GeneralNewsExtractor()
html = '你的目標網頁正文'
result = extractor.extract(html)
print(result)

對大多數新聞頁面而言,以上的寫法就能夠解決問題了。

但某些新聞網頁下面會有評論,評論里面可能存在長篇大論,它們會看起來比真正的新聞正文更像是正文,因此extractor.extract()方法還有一個默認參數noise_mode_list,用于在網頁預處理時提前把評論區域整個移除。

noise_mode_list的值是一個列表,列表里面的每一個元素都是 XPath,對應了你需要提前移除的,可能會導致干擾的目標標簽。

例如,觀察者網下面的評論區域對應的Xpath 為//div[@class="comment-list"]。所以在提取觀察者網時,為了防止評論干擾,就可以加上這個參數:

result = extractor.extract(html, noise_node_list=['//div[@class="comment-list"]'])

test文件夾中的網頁的提取結果,請查看result.txt。

已知問題

  1. 目前本項目只適用于新聞頁的信息提取。如果目標網站不是新聞頁,或者是今日頭條中的相冊型文章,那么抽取結果可能不符合預期。
  2. 可能會有一些新聞頁面出現抽取結果中的作者為空字符串的情況,這可能是由于文章本身沒有作者,或者使用了已有正則表達式沒有覆蓋到的情況。

Todo

  • 使用一個配置文件來存放常量數據,而不是直接 Hard Code 寫在代碼中。
  • 允許自定義時間、作者的提取Pattern
  • 自動識別新聞列表頁
  • 優化內容提取速度
  • 測試更多新聞網站
  • ……

交流溝通

  • 項目地址:https://github.com/kingname/G...
  • 如果您覺得GNE對您的日常開發或公司有幫助,請加作者微信 mxqiuchen(或掃描下方二維碼) 并注明"GNE",作者會將你拉入群。

驗證消息:GNE

查看原文

贊 6 收藏 5 評論 11

青南 評論了文章 · 2019-04-01

為什么你需要少看垃圾博客以及如何在Python里精確地四舍五入

今天又有一個Python初學者被中文技術博客中的垃圾文章給誤導了。

這位初學者的問題是:

在Python中,如何精確地進行浮點數的四舍五入,保留兩位小數?

如果你在Google或者百度上搜索,你會發現大量的來自CSDN或者簡書上面的文章講到這一點,但是他們的說法無外乎下面幾種:

連例子都不舉的垃圾文章

如下圖所示,懶得吐槽。

使用round函數

他們舉的例子為:

>>> round(1.234, 2)
1.23

這種文章,他只演示了四舍,但是卻沒有演示五入。所以如果你代碼稍作修改,就會發現有問題:

>>> round(11.245, 2)
11.24

先放大再縮小

這種文章稍微好一點,知道多舉幾個例子:

然而這種文章也是漏洞百出,只要你多嘗試幾個數字就會發現問題,在Python 2和Python 3下面,效果是不一樣的。先來看看Python 2下面的運行效果:

在Python 2里面,直接使用round,1.125精確到兩位小數后為1.13,而1.115精確到兩位小數后是1.11。

再來看看Python 3下面的效果:

在Python 3下面,1.125在精確到兩位小數以后是1.12。

他舉的例子,在Python 3中先放大再縮小,也并不總是正確。

裝逼貨

還有一種裝逼貨,文章和先放大再縮小差不多,但是他還知道decimal這個模塊。

不過他的使用方法,大家看他吧

具體原因不詳 ????

不推薦使用這個方法???

這種人要先裝個逼,表示自己知道有這樣一個庫,但是用起來發現有問題,而且不知道原因,所以不建議大家使用。

decimal是專門為高精度計算用的模塊,他竟然說不建議大家使用???

round到底出了什么問題?

罵完了,我們來說說,在Python 3里面,round這個內置的函數到底有什么問題。

網上有人說,因為在計算機里面,小數是不精確的,例如1.115在計算機中實際上是1.1149999999999999911182,所以當你對這個小數精確到小數點后兩位的時候,實際上小數點后第三位是4,所以四舍五入,因此結果為1.11。

這種說法,對了一半。

因為并不是所有的小數在計算機中都是不精確的。例如0.125這個小數在計算機中就是精確的,它就是0.125,沒有省略后面的值,沒有近似,它確確實實就是0.125。

但是如果我們在Python中把0.125精確到小數點后兩位,那么它的就會變成0.12

>>> round(0.125, 2)
0.12

為什么在這里四舍了?

還有更奇怪的,另一個在計算機里面能夠精確表示的小數0.375,我們來看看精確到小數點后兩位是多少:

>>> round(0.375, 2)
0.38

為什么這里又五入了?

因為在Python 3里面,round對小數的精確度采用了四舍六入五成雙的方式。

如果你寫過大學物理的實驗報告,那么你應該會記得老師講過,直接使用四舍五入,最后的結果可能會偏高。所以需要使用奇進偶舍的處理方法。

例如對于一個小數a.bcd,需要精確到小數點后兩位,那么就要看小數點后第三位:

  1. 如果d小于5,直接舍去
  2. 如果d大于5,直接進位
  3. 如果d等于5:

    1. d后面沒有數據,且c為偶數,那么不進位,保留c
    2. d后面沒有數據,且c為奇數,那么進位,c變成(c + 1)
    3. 如果d后面還有非0數字,例如實際上小數為a.bcdef,此時一定要進位,c變成(c + 1)

關于奇進偶舍,有興趣的同學可以在維基百科搜索這兩個詞條:數值修約奇進偶舍。

所以,round給出的結果如果與你設想的不一樣,那么你需要考慮兩個原因:

  1. 你的這個小數在計算機中能不能被精確儲存?如果不能,那么它可能并沒有達到四舍五入的標準,例如1.115,它的小數點后第三位實際上是4,當然會被舍去。
  2. 如果你的這個小數在計算機中能被精確表示,那么,round采用的進位機制是奇進偶舍,所以這取決于你要保留的那一位,它是奇數還是偶數,以及它的下一位后面還有沒有數據。

如何正確進行四舍五入

如果要實現我們數學上的四舍五入,那么就需要使用decimal模塊。

如何正確使用decimal模塊呢?

看官方文檔,不要看中文垃圾博客?。?!

看官方文檔,不要看中文垃圾博客?。?!

看官方文檔,不要看中文垃圾博客?。?!

不要擔心看不懂英文,Python已經推出了官方中文文檔(有些函數的使用方法還沒有翻譯完成)。

我們來看一下:https://docs.python.org/zh-cn...

官方文檔給出了具體的寫法:

>>>Decimal('1.41421356').quantize(Decimal('1.000'))
Decimal('1.414')

那么我們來測試一下,0.1250.375分別保留兩位小數是多少:

>>> from decimal import Decimal
>>> Decimal('0.125').quantize(Decimal('0.00'))
Decimal('0.12')
>>> Decimal('0.375').quantize(Decimal('0.00'))
Decimal('0.38')

怎么結果和round一樣?我們來看看文檔中quantize的函數原型和文檔說明:

這里提到了可以通過指定rounding參數來確定進位方式。如果沒有指定rounding參數,那么默認使用上下文提供的進位方式。

現在我們來查看一下默認上下文中的進位方式是什么:

>>> from decimal import getcontext
>>> getcontext().rounding
'ROUND_HALF_EVEN'

如下圖所示:

ROUND_HALF_EVEN實際上就是奇進偶舍!如果要指定真正的四舍五入,那么我們需要在quantize中指定進位方式為ROUND_HALF_UP

>>> from decimal import Decimal, ROUND_HALF_UP
>>> Decimal('0.375').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')

現在看起來一切都正常了。

那么會不會有人進一步追問一下,如果Decimal接收的參數不是字符串,而是浮點數會怎么樣呢?

來實驗一下:


>>> Decimal(0.375).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal(0.125).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')

那是不是說明,在Decimal的第一個參數,可以直接傳浮點數呢?

我們換一個數來測試一下:

>>> Decimal(11.245).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.24')
>>> Decimal('11.245').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.25')

為什么浮點數11.245和字符串'11.245',傳進去以后,結果不一樣?

我們繼續在文檔在尋找答案。

官方文檔已經很清楚地說明了,如果你傳入的參數為浮點數,并且這個浮點值在計算機里面不能被精確存儲,那么它會先被轉換為一個不精確的二進制值,然后再把這個不精確的二進制值轉換為等效的十進制值。

對于不能精確表示的小數,當你傳入的時候,Python在拿到這個數前,這個數就已經被轉成了一個不精確的數了。所以你雖然參數傳入的是11.245,但是Python拿到的實際上是11.244999999999...。

但是如果你傳入的是字符串'11.245',那么Python拿到它的時候,就能知道這是11.245,不會提前被轉換為一個不精確的值,所以,建議給Decimal的第一個參數傳入字符串型的浮點數,而不是直接寫浮點數。

總結,如果想實現精確的四舍五入,代碼應該這樣寫:

from decimal import Decimal, ROUND_HALF_UP

origin_num = Decimal('11.245')
answer_num = origin_num.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
print(answer_num)

運行效果如下圖所示:

特別注意,一旦要做精確計算,那么就不應該再單獨使用浮點數,而是應該總是使用Decimal('浮點數')。否則,當你賦值的時候,精度已經被丟失了,建議全程使用Decimal舉例:

a = Decimal('0.1')
b = Decimal('0.2')
c = a + b
print(c)

最后,如果有同學想知道為什么0.125和0.375能被精確的儲存,而1.115、11.245不能被精確儲存,請在這篇文章下面留言,如果想知道的同學多,我就寫一篇文章來說明。

如果這篇文章對你有幫助,請考慮關注我的微信公眾號 未聞Code:

查看原文

青南 發布了文章 · 2019-03-31

為什么你需要少看垃圾博客以及如何在Python里精確地四舍五入

今天又有一個Python初學者被中文技術博客中的垃圾文章給誤導了。

這位初學者的問題是:

在Python中,如何精確地進行浮點數的四舍五入,保留兩位小數?

如果你在Google或者百度上搜索,你會發現大量的來自CSDN或者簡書上面的文章講到這一點,但是他們的說法無外乎下面幾種:

連例子都不舉的垃圾文章

如下圖所示,懶得吐槽。

使用round函數

他們舉的例子為:

>>> round(1.234, 2)
1.23

這種文章,他只演示了四舍,但是卻沒有演示五入。所以如果你代碼稍作修改,就會發現有問題:

>>> round(11.245, 2)
11.24

先放大再縮小

這種文章稍微好一點,知道多舉幾個例子:

然而這種文章也是漏洞百出,只要你多嘗試幾個數字就會發現問題,在Python 2和Python 3下面,效果是不一樣的。先來看看Python 2下面的運行效果:

在Python 2里面,直接使用round,1.125精確到兩位小數后為1.13,而1.115精確到兩位小數后是1.11。

再來看看Python 3下面的效果:

在Python 3下面,1.125在精確到兩位小數以后是1.12。

他舉的例子,在Python 3中先放大再縮小,也并不總是正確。

裝逼貨

還有一種裝逼貨,文章和先放大再縮小差不多,但是他還知道decimal這個模塊。

不過他的使用方法,大家看他吧

具體原因不詳 ????

不推薦使用這個方法???

這種人要先裝個逼,表示自己知道有這樣一個庫,但是用起來發現有問題,而且不知道原因,所以不建議大家使用。

decimal是專門為高精度計算用的模塊,他竟然說不建議大家使用???

round到底出了什么問題?

罵完了,我們來說說,在Python 3里面,round這個內置的函數到底有什么問題。

網上有人說,因為在計算機里面,小數是不精確的,例如1.115在計算機中實際上是1.1149999999999999911182,所以當你對這個小數精確到小數點后兩位的時候,實際上小數點后第三位是4,所以四舍五入,因此結果為1.11。

這種說法,對了一半。

因為并不是所有的小數在計算機中都是不精確的。例如0.125這個小數在計算機中就是精確的,它就是0.125,沒有省略后面的值,沒有近似,它確確實實就是0.125。

但是如果我們在Python中把0.125精確到小數點后兩位,那么它的就會變成0.12

>>> round(0.125, 2)
0.12

為什么在這里四舍了?

還有更奇怪的,另一個在計算機里面能夠精確表示的小數0.375,我們來看看精確到小數點后兩位是多少:

>>> round(0.375, 2)
0.38

為什么這里又五入了?

因為在Python 3里面,round對小數的精確度采用了四舍六入五成雙的方式。

如果你寫過大學物理的實驗報告,那么你應該會記得老師講過,直接使用四舍五入,最后的結果可能會偏高。所以需要使用奇進偶舍的處理方法。

例如對于一個小數a.bcd,需要精確到小數點后兩位,那么就要看小數點后第三位:

  1. 如果d小于5,直接舍去
  2. 如果d大于5,直接進位
  3. 如果d等于5:

    1. d后面沒有數據,且c為偶數,那么不進位,保留c
    2. d后面沒有數據,且c為奇數,那么進位,c變成(c + 1)
    3. 如果d后面還有非0數字,例如實際上小數為a.bcdef,此時一定要進位,c變成(c + 1)

關于奇進偶舍,有興趣的同學可以在維基百科搜索這兩個詞條:數值修約奇進偶舍。

所以,round給出的結果如果與你設想的不一樣,那么你需要考慮兩個原因:

  1. 你的這個小數在計算機中能不能被精確儲存?如果不能,那么它可能并沒有達到四舍五入的標準,例如1.115,它的小數點后第三位實際上是4,當然會被舍去。
  2. 如果你的這個小數在計算機中能被精確表示,那么,round采用的進位機制是奇進偶舍,所以這取決于你要保留的那一位,它是奇數還是偶數,以及它的下一位后面還有沒有數據。

如何正確進行四舍五入

如果要實現我們數學上的四舍五入,那么就需要使用decimal模塊。

如何正確使用decimal模塊呢?

看官方文檔,不要看中文垃圾博客?。?!

看官方文檔,不要看中文垃圾博客?。?!

看官方文檔,不要看中文垃圾博客?。?!

不要擔心看不懂英文,Python已經推出了官方中文文檔(有些函數的使用方法還沒有翻譯完成)。

我們來看一下:https://docs.python.org/zh-cn...

官方文檔給出了具體的寫法:

>>>Decimal('1.41421356').quantize(Decimal('1.000'))
Decimal('1.414')

那么我們來測試一下,0.1250.375分別保留兩位小數是多少:

>>> from decimal import Decimal
>>> Decimal('0.125').quantize(Decimal('0.00'))
Decimal('0.12')
>>> Decimal('0.375').quantize(Decimal('0.00'))
Decimal('0.38')

怎么結果和round一樣?我們來看看文檔中quantize的函數原型和文檔說明:

這里提到了可以通過指定rounding參數來確定進位方式。如果沒有指定rounding參數,那么默認使用上下文提供的進位方式。

現在我們來查看一下默認上下文中的進位方式是什么:

>>> from decimal import getcontext
>>> getcontext().rounding
'ROUND_HALF_EVEN'

如下圖所示:

ROUND_HALF_EVEN實際上就是奇進偶舍!如果要指定真正的四舍五入,那么我們需要在quantize中指定進位方式為ROUND_HALF_UP

>>> from decimal import Decimal, ROUND_HALF_UP
>>> Decimal('0.375').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')

現在看起來一切都正常了。

那么會不會有人進一步追問一下,如果Decimal接收的參數不是字符串,而是浮點數會怎么樣呢?

來實驗一下:


>>> Decimal(0.375).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.38')
>>> Decimal(0.125).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('0.13')

那是不是說明,在Decimal的第一個參數,可以直接傳浮點數呢?

我們換一個數來測試一下:

>>> Decimal(11.245).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.24')
>>> Decimal('11.245').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
Decimal('11.25')

為什么浮點數11.245和字符串'11.245',傳進去以后,結果不一樣?

我們繼續在文檔在尋找答案。

官方文檔已經很清楚地說明了,如果你傳入的參數為浮點數,并且這個浮點值在計算機里面不能被精確存儲,那么它會先被轉換為一個不精確的二進制值,然后再把這個不精確的二進制值轉換為等效的十進制值。

對于不能精確表示的小數,當你傳入的時候,Python在拿到這個數前,這個數就已經被轉成了一個不精確的數了。所以你雖然參數傳入的是11.245,但是Python拿到的實際上是11.244999999999...。

但是如果你傳入的是字符串'11.245',那么Python拿到它的時候,就能知道這是11.245,不會提前被轉換為一個不精確的值,所以,建議給Decimal的第一個參數傳入字符串型的浮點數,而不是直接寫浮點數。

總結,如果想實現精確的四舍五入,代碼應該這樣寫:

from decimal import Decimal, ROUND_HALF_UP

origin_num = Decimal('11.245')
answer_num = origin_num.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
print(answer_num)

運行效果如下圖所示:

特別注意,一旦要做精確計算,那么就不應該再單獨使用浮點數,而是應該總是使用Decimal('浮點數')。否則,當你賦值的時候,精度已經被丟失了,建議全程使用Decimal舉例:

a = Decimal('0.1')
b = Decimal('0.2')
c = a + b
print(c)

最后,如果有同學想知道為什么0.125和0.375能被精確的儲存,而1.115、11.245不能被精確儲存,請在這篇文章下面留言,如果想知道的同學多,我就寫一篇文章來說明。

如果這篇文章對你有幫助,請考慮關注我的微信公眾號 未聞Code:

查看原文

贊 16 收藏 7 評論 4

青南 評論了文章 · 2019-03-29

全面超越Appium,使用Airtest超快速開發App爬蟲

想開發網頁爬蟲,發現被反爬了?想對 App 抓包,發現數據被加密了?不要擔心,使用 Airtest 開發 App 爬蟲,只要人眼能看到,你就能抓到,最快只需要2分鐘,兼容 Unity3D、Cocos2dx-*、Android 原生 App、iOS App、Windows Mobile……。

Airtest是網易開發的手機UI界面自動化測試工具,它原本的目的是通過所見即所得,截圖點擊等等功能,簡化手機App圖形界面測試代碼編寫工作。

爬蟲開發本著天下工具為我所用,能讓我獲取數據的工具都能用來開發爬蟲這一信念,決定使用Airtest來開發手機App爬蟲。

安裝和使用

由于本文的目的是介紹如何使用Airtest來開發App爬蟲,那么Airtest作為測試開發工具的方法介紹將會一帶而過,僅僅說明如何安裝并進行基本的操作。

安裝Airtest

從Airtest官網:https://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. 新建項目
  2. 打開項目
  3. 保存項目
  4. 運行代碼
  5. 停止代碼
  6. 查看運行報告

其中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這個屬性。

常見的基本上不會變化的屬性包含但不限于:nametyperesourceIdpackage。

另外還有一點,知乎首頁的這個搜索框,實際上是不能輸入內容的,當你點擊以后,會跳轉到另一個頁面,如下圖所示。

因此你需要先點擊一下這個輸入框,跳轉到真正的搜索界面:

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中執行。

既然要做爬蟲,就需要獲取手機上的文字內容?;氐剿阉黜撁?,我想知道“古劍奇譚”三這個關鍵字能搜索出多少條結果,每條結果有多少個討論,如下圖所示:

此時我們需要做兩件事情:

  1. 分別查看每一個搜索結果
  2. 獲取屏幕上的文字

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]?,F在由于需要獲得所有搜索結果的內容,所以應該去掉[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爬蟲開發 從入門到實戰第十章對于如何搭建手機爬蟲集群有詳細的說明和注意事項。

如果對我的書有興趣,請關注我的微信公眾號與我交流。

查看原文

青南 評論了文章 · 2019-03-27

全面超越Appium,使用Airtest超快速開發App爬蟲

想開發網頁爬蟲,發現被反爬了?想對 App 抓包,發現數據被加密了?不要擔心,使用 Airtest 開發 App 爬蟲,只要人眼能看到,你就能抓到,最快只需要2分鐘,兼容 Unity3D、Cocos2dx-*、Android 原生 App、iOS App、Windows Mobile……。

Airtest是網易開發的手機UI界面自動化測試工具,它原本的目的是通過所見即所得,截圖點擊等等功能,簡化手機App圖形界面測試代碼編寫工作。

爬蟲開發本著天下工具為我所用,能讓我獲取數據的工具都能用來開發爬蟲這一信念,決定使用Airtest來開發手機App爬蟲。

安裝和使用

由于本文的目的是介紹如何使用Airtest來開發App爬蟲,那么Airtest作為測試開發工具的方法介紹將會一帶而過,僅僅說明如何安裝并進行基本的操作。

安裝Airtest

從Airtest官網:https://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. 新建項目
  2. 打開項目
  3. 保存項目
  4. 運行代碼
  5. 停止代碼
  6. 查看運行報告

其中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這個屬性。

常見的基本上不會變化的屬性包含但不限于:nametyperesourceIdpackage。

另外還有一點,知乎首頁的這個搜索框,實際上是不能輸入內容的,當你點擊以后,會跳轉到另一個頁面,如下圖所示。

因此你需要先點擊一下這個輸入框,跳轉到真正的搜索界面:

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中執行。

既然要做爬蟲,就需要獲取手機上的文字內容?;氐剿阉黜撁?,我想知道“古劍奇譚”三這個關鍵字能搜索出多少條結果,每條結果有多少個討論,如下圖所示:

此時我們需要做兩件事情:

  1. 分別查看每一個搜索結果
  2. 獲取屏幕上的文字

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]?,F在由于需要獲得所有搜索結果的內容,所以應該去掉[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爬蟲開發 從入門到實戰第十章對于如何搭建手機爬蟲集群有詳細的說明和注意事項。

如果對我的書有興趣,請關注我的微信公眾號與我交流。

查看原文

青南 評論了文章 · 2019-03-27

全面超越Appium,使用Airtest超快速開發App爬蟲

想開發網頁爬蟲,發現被反爬了?想對 App 抓包,發現數據被加密了?不要擔心,使用 Airtest 開發 App 爬蟲,只要人眼能看到,你就能抓到,最快只需要2分鐘,兼容 Unity3D、Cocos2dx-*、Android 原生 App、iOS App、Windows Mobile……。

Airtest是網易開發的手機UI界面自動化測試工具,它原本的目的是通過所見即所得,截圖點擊等等功能,簡化手機App圖形界面測試代碼編寫工作。

爬蟲開發本著天下工具為我所用,能讓我獲取數據的工具都能用來開發爬蟲這一信念,決定使用Airtest來開發手機App爬蟲。

安裝和使用

由于本文的目的是介紹如何使用Airtest來開發App爬蟲,那么Airtest作為測試開發工具的方法介紹將會一帶而過,僅僅說明如何安裝并進行基本的操作。

安裝Airtest

從Airtest官網:https://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. 新建項目
  2. 打開項目
  3. 保存項目
  4. 運行代碼
  5. 停止代碼
  6. 查看運行報告

其中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這個屬性。

常見的基本上不會變化的屬性包含但不限于:nametyperesourceIdpackage。

另外還有一點,知乎首頁的這個搜索框,實際上是不能輸入內容的,當你點擊以后,會跳轉到另一個頁面,如下圖所示。

因此你需要先點擊一下這個輸入框,跳轉到真正的搜索界面:

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中執行。

既然要做爬蟲,就需要獲取手機上的文字內容?;氐剿阉黜撁?,我想知道“古劍奇譚”三這個關鍵字能搜索出多少條結果,每條結果有多少個討論,如下圖所示:

此時我們需要做兩件事情:

  1. 分別查看每一個搜索結果
  2. 獲取屏幕上的文字

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]?,F在由于需要獲得所有搜索結果的內容,所以應該去掉[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爬蟲開發 從入門到實戰第十章對于如何搭建手機爬蟲集群有詳細的說明和注意事項。

如果對我的書有興趣,請關注我的微信公眾號與我交流。

查看原文

認證與成就

  • 獲得 138 次點贊
  • 獲得 2 枚徽章 獲得 0 枚金徽章, 獲得 0 枚銀徽章, 獲得 2 枚銅徽章

擅長技能
編輯

開源項目 & 著作
編輯

  • GNE

    從新聞網頁自動抽取正文。

注冊于 2015-09-21
個人主頁被 2.2k 人瀏覽

一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕 <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>