爬虫常见两种情况:第一种情况,爬虫伪装成浏览器,向服务器要数据;第二种情况,在服务器往浏览器发送数据时,爬虫从中拦截,获取信息。
这两种情况,无论是暗号(参数)不对还是行为不对,都会被服务器识别。那么有没有什么办法可以做到几乎毫无痕迹地爬取数据呢?答案是有的。
目前,Android App主要有两种实现形式。第一种是Android原生App。这种App的全部或者大部分内容使用Android提供的各个接口来开发,例如Android版的微信就是一个Android原生的App。第二种是基于网页的App。这种App本质上就是一个浏览器,里面的所有内容实际上都是网页。例如,12306的App就是这样一种基于网页的App。
Android原生App爬虫(以下简称App爬虫)可以直接读取Android原生App上面的文本信息。
App爬虫可以实现自动滚动屏幕,自动单击进入详情页。凡是人可以对手机进行的操作,App爬虫都可以进行。
UiAutomator是Google官方提供的Android自动化图形接口测试框架。通过它可以实现对Android设备屏幕的各种操作,或者直接从屏幕上读取文字。大部分系统版本大于4.1的Android系统,都会内置UiAutomator。小米手机原装的MIUI系统除外,MIUI系统UiAutomator被移除了,需要刷开发版或者换其他系统才能使用。
一、环境搭建
1. 安装JRE
要使用UiAutomator操作Android手机,首先需要在计算机上安装Android的软件开发工具包(Software Development Kit, SDK)。要安装Android SDK,首先需要安装Java运行时环境(JavaRuntime Environment, JRE)。
对于Mac OS,使用Homebrew安装Java开发套件(Java SE Development Kit,JDK)。JRE包含在了JDK里面:
brew update
brew cask install java
对于Ubuntu,使用如下命令直接安装JRE:
sudo apt-get update
sudo apt-get install default-jre
对于Windows,可以访问http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html下载JRE 8。在这个页面,首先选择“Accept License Agreement”单选按钮,根据自己的系统下载对应的安装程序,32位系统下载并安装Windows x86;64位系统下载并安装Windows x64。安装过程不需要修改任何设置,全部单击“Next”按钮即可完成。
2. 安装Android SDK
安装完成JRE以后,再来安装Android SDK。请打开https://developer.android.google.cn/studio/index.html,这是Android开发者中国官网,在中国可以直接打开。打开网页,拖到最下方,在“仅获取命令行工具”中下载自己系统对应的SDK。
下载的是一个.zip格式的压缩包,将之解压可以得到一个名为tools的文件夹。
在Mac OS与Ubuntu的终端输入并执行如下命令:
cd ~/python/sdk
bin/sdkmanager "platform-tools"
Windows系统直接在tools文件夹中打开CMD窗口,并输入命令:
bin/sdkmanager.exe "platform-tools"
输入完成命令并按Enter键,可以看到在终端窗口弹出安装协议,输入y并按Enter键即可安装“platform-tools”。
需要注意的是,这里执行这个命令时,程序会从Google获取一些数据,此时可能会由于网络问题导致失败。如果遇到网络问题,那么就需要使用能访问Google的代理,并且命令也需要做一些修改,修改为:
bin/sdkmanager.exe "platform-tools" --proxy=http --proxy_host=代理IP --proxy-port=代理端口
安装完成以后,会在文件夹下出现一个“platform-tools”文件夹。现在需要将tools文件夹和platform-tools文件夹添加到系统的环境变量中。
3. 设置环境变量
对于使用Mac OS或Ubuntu系统并安装了zsh和Oh-my-zsh的小伙伴,请打开~/.zshrc并检查是否已经有export PATH开头的一句话,如果有,请修改为下面所示的代码这样,其中的tools和platform-tools文件夹的地址请改为实际地址:
export PATH="/Users/redhat/book/sdk/platform-tools:/Users/redhat/ book/sdk/tools:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
如果没有,请添加进去,然后保存,并在终端中执行:
source ~/.zshrc
上面这种方式可永久性添加环境变量,只需要做一次。
如果是Mac OS或者Ubuntu系统且没有安装zsh和Oh-my-zsh的小伙伴,或者想临时添加环境变量进行测试,可直接在终端执行下面代码,将tools和platform-tools的地址改为实际地址:
export PATH=$PATH:/Users/redhat/book/sdk/platform-tools:/Users/redhat/book/sdk/tools
需要注意的是,这种方法是临时添加环境变量,当前的终端窗口不能关闭。一旦关闭再重新打开,就需要再一次执行上面的代码。
对于Windows,打开任意一个文件夹,并在左侧导航窗口中右键单击“计算机”,选择“属性”命令,打开属性面板。在属性面板左侧选择“高级系统设置”选项,进入“高级”选项卡,最后单击右下角的“环境变量”按钮进入“环境变量”对话框,按照实际目录配置即可(个别Windows电脑,配置完环境变量需要重启生效)。
4. 开启开发者模式
要通过计算机控制手机,还需要打开Android手机的开发者模式。以小米手机的MIUI开发版和Android原生系统为例,如何开启开发者模式。
对于小米手机MIUI开发版,依次进入“系统设置”-“我的设备”-“全部参数”,快速连续单击“MIUI版本”这一栏5次,开发者模式就会打开。打开以后,回到“系统设置”主界面,依次进入“更多设置”-“开发者选项”。在“开发者选项”中分别打开“USB调试”、“USB安装”和“USB调试(安全模式)”这几个开关,系统会弹出警告,选择允许。
对于Andorid原生系统,以Google生产的Nexus为例。打开“系统设置”,进入最下方的“系统”,打开系统信息界面。拖动最下方,找到“关于手机”并点击,进入手机状态界面。
由于各大厂商对Android系统几乎都有自己的定制化,因此不能在这里一 一列举所有型号手机开启开发者模式的操作。对于没有提及的手机型号,请通过网络搜索。
设置好环境变量以后,在终端窗口输入“uiautomatorviewer”并按Enter键,如果可以弹出图所示的UI Automator Viewer窗口,表明环境设置成功。
将Android手机连接到计算机上,保持手机屏幕为亮起状态,单击UI Automator Viewer左上角文件夹右侧的手机图标,如果能够看到手机屏幕出现在窗口中,则表示一切顺利,环境搭建成功完成。如果在这个过程中手机弹出了任何警告窗口,都选择“运行”或者“确定”。
二、使用Python操纵手机
要使用Python来操作UI Automator从而控制手机,需要安装一个第三方库。这个库的名字就是uiautomator,使用pip进行安装
pip install uiautomator
安装完成以后运行Python并导入uiautomator:
from uiautomator import Device
device = Device()
print(device.dump())
由于是第一次运行,uiautomator会往手机中安装两个没有图标的程序。有一些手机系统可能会弹出窗口询问是否允许安装,单击“继续安装”按钮。安装完成以后,可以看到终端窗口出现了类似于XML的内容。这说明uiautomator这个第三方库成功安装。此时就可以使用Python控制手机了。
有一点需要特别说明,UI Automator Viewer与Python uiautomator不能同时使用。一旦Python的uiautomator运行过一次,它安装的两个文件就会在手机后台运行。这两个文件会占用Android内部的一个叫作UI hierarchy的东西,从而导致SDK自带的UI Automator Viewer一旦尝试获取手机屏幕就会报错,如图:
一旦出现这种情况,就需要手动结束Python uiautomator安装的两个文件的进程,或者重启手机才能继续使用UI Automator Viewer。这两个进程的名字分别为“uiautomator”和“com.github.uiautomator.test”。
与Selenium一样,要操作手机上面的元素,首先要找到被操作的东西。以打开微信为例,首先翻到有微信的那一页。
编写如下代码:
from uiautomator import Device
device = Device()
device(text='微信').click()
运行以后可以看到,微信自动被点击并打开。
如果计算机上面只连接了一台Android手机,那么初始化设备连接只需要使用device = Device()即可。那么如果计算机上连接了很多台手机,该怎么办呢?此时就需要指定手机的串号。要查看手机串号,需要在终端输入以下命令:
adb devices -l
从输出的内容可以看到手机的串号。只要不是人为去修改,在正常情况下,每一台手机的串号都是不一样的。因此可以认为串号与手机是一一对应的。将手机的串号(比如,'123456abccde')作为device的参数,就可以指定控制对应的手机:
device = Device('123456abccde')
在初始化了设备连接以后,所有的操作都是通过这个变量device的各个方法或者属性来完成的。device的参数text=“微信”称为“Selector”,也就是选择器。通过不同的选择器来选定不同的元素,并对元素进行不同的操作。
三、选择器
如何知道有哪些选择器可供使用呢?请执行以下代码:
from uiautomator import Device
device = Device()
print(device.dump())
此时终端会以XML输出当前手机屏幕显示的窗口布局信息。
这里的XML就相当于网页中的HTML,用来描述窗口上面各个部分的布局信息。XML的格式与HTML非常像,格式为:
<标签 属性1="属性值1" 属性2="属性值2">文本</标签>
其中的各个属性和属性值,就是选择器操作的对象。这里的不同属性包括但不限于“text”“class”“description”“resource-id”和“package”。
在uiautomator中,也有选择器和这些属性一一对应。其中,选择器“className”对应“class”标签;选择器“packageName”对应“package”标签;选择器“resourceId”对应“resource-id”标签。所以要选择一个元素,可能有如下的写法:
device(packageName='com.android.systemui')
device(className='android.widget.FrameLayout')
device(resourceId='com.android.systemui:id/clock')
device(text='短信')
device(index='3', resourceId='com.android.systemui:id/mobile_combo')
可以用一个标签来作为选择器,也可以同时使用多个标签来更精确地描述某一个元素。一般来说,操作一个有文字的元素,主要是使用text这个属性;如果是从屏幕上读文字,就使用其他的属性。
四、操作
在选择好元素以后,就需要对它进行操作。最常见的操作如下:
• 获得屏幕文字;
• 滚动屏幕;
• 滑动屏幕;
• 点击屏幕;
• 输入文字;
• 判断元素是否存在;
• 点亮关闭屏幕;
• 操作实体按键;
• watcher。
1. 获得屏幕文字
如果要从Android手机上读取当前屏幕上显示的文本内容,用到的是一个元素的“.text”属性。
2. 滚动屏幕
滚动屏幕对应的操作为“.scroll()”。它的操作对象是一个可以滚动的对象。如果手动操作可以把屏幕向上滚动,那么屏幕上应该至少有一个元素是可以滚动的,因此选择器可以写为:
device(scrollable=True)
那么向上滚动一屏可以写为:
device(scrollable=True).scroll.vert.forward()
如果想向下滚动一屏,则需要把forward()换为backward():
device(scrollable=True).scroll.vert.forward()
由于向上、向下是“垂直方向”,所以代码中使用了vert,这是英语单词vertical(垂直的)的简写。可能有些App会出现左右滚动的情况,这时候就需要使用horiz,这是英语单词horizontal(水平的)的简写。于是,向左及向右滚动就可以写为:
device(scrollable=True).scroll.horiz.forward() #向右滚动
device(scrollable=True).scroll.horiz.backward() #向左滚动
有一点需要注意,由于滚动一屏时,有可能前一屏最下面的元素滚动以后刚好到了后一屏的最上面,因此可能出现重复获取,在实际使用时要注意去重处理。
3. 滑动屏幕
在某些情况下,整个窗口布局的XML里面,所有元素的scrollable属性值全部都是False,例如小米手机的桌面。在这种情况下,没有办法使用“scrollable=True”来左右滚动桌面,于是就需要使用根据坐标来滑动桌面的“.swipe()”方法。
在Android系统的屏幕上,左上角为坐标原点(0, 0),越向下,y轴数字越大;越向右,x轴数字越大。
“.swipe()”这个方法操作的对象是整个手机屏幕,所以不需要为device设定选择器。“.swipe()”的用法为:
device.swipe(400, 600, 0, 600)
它接收4个参数,分别为起始点x坐标,起始点y坐标,终点x坐标,终点y坐标。对于左右滑动来说,只需要改变x坐标即可。如果要显示右边的一屏,那么起始点的x坐标要大于终点的x坐标。如果要显示左边的一屏,起始点的x坐标要小于终点的x坐标。
如果在实际写代码的时候搞不清x坐标哪个大,就亲自把手放在屏幕上滑动进行查看。如果要显示右边的一屏,那么会首先把手放在屏幕右侧,按住然后往左移动,所以此时起始点的x坐标要大一些。
4. 点击屏幕
选择一个元素以后,除了获取它上面的文字外,还可以点击。就如同前面通过点击打开微信一样。点击操作“.click()”和“.long_click()”分别对应短按点击和长按点击,它们可以直接应用于一个被选择出来的元素上。例如:
device(text='微信').click()
device(text='微信').long_click()
点击操作也可以直接应用于坐标位置。例如:
device.click(230, 567) #第1个参数为横坐标x轴,第2个参数为纵坐标y轴
device.long_click(230, 567) #第1个参数为横坐标x轴,第2个参数为纵坐标y轴
在某些App中,可能某一个元素并没有一个独特的标志来让选择器选择。这个时候就可以直接使用坐标来操作。
5. 输入文字
既然进入了搜索界面,那就需要搜索内容了。输入文本使用的操作是“.set_text()”。
因此可以用这个resource-id来作为选择器定位到搜索框,再输入文本。例如搜索名为“大秦重工”的博主,那么就使用以下代码:
device(resourceId='com.taptap:id/input_box').set_text('大秦重工')
6. 判断元素是否存在
由于手机上面的各个元素加载是需要一定时间的,如果在元素加载出来之前就对其进行操作,就会导致程序报错,在操作一个元素之前,先判断一下它是否存在是比较明智的做法。判断元素是否存在,使用“.exists”属性。如果存在,值为True,否则为False。其用法为:
input_box = device(resourceId='com.taptap:id/input_box')
if input_box.exists:
input_box.set_text('大秦重工')
else:
print('搜索框不存在')
如果元素只是因为没有来得及加载出来而不存在,并不是屏幕界面错误,那么还可以等待它加载出来以后再进行更多操作。使用到的方法是“.wait.exists()”,其中exists还可以设置等待超时时间。例如:
search_result = device(text='大秦重工')
if search_result.wait.exists(timeout=20000)
search_result.click()
else:
print('元素不存在')
等待文字为“大秦重工”的元素出现,如果在20s以内出现了这个元素,就点击它,否则就打印“元素不存在”的提示。需要注意的是,这里“.exists()”的超时时间单位为ms。所以要等待20s,就需要输入20000。
7. 点亮关闭屏幕
使用“.wakeup()”方法和“.sleep()”方法可以点亮或者关闭屏幕。当手机屏幕处于关闭状态时,使用“.wake()”方法可以让屏幕点亮;当手机屏幕处于点亮状态时,使用“.sleep()”方法可以将其关闭。其用法为:
device.wakeup() # 点亮屏幕
device.sleep() # 关闭屏幕
如果要检查当前手机屏幕是开启状态还是关闭状态,就需要使用“.screen”属性。它的值为“on”,表示当前手机屏幕亮起;值为“off”,表示当前手机屏幕关闭。其用法如下:
if device.screen == 'on':
print('当前手机屏幕为点亮状态')
elif device.screen == 'off':
print('当前手机屏幕为关闭状态')
8. 操作实体按键
Android一般自带不少实体按键,使用Python的uiautomator可以模拟这些按键被按下的状态。其使用方法为:
device.press.实体按键名称()
常见的实体按键名称和作用如表:
例如,现在的手机屏幕处于关闭状态,需要点亮屏幕,除了使用“.wakeup()”方法以外,还可以模拟按下电源键:
device.press.power()
又例如需要增大音量,那么就模拟按下音量增大键:
device.press.volume_up()
9. watcher
使用Android手机的时候,经常会遇到这样的情况:手机用着用着,突然弹出一个对话框,提示当前App有新版本可用。对于用户来说,遇到这种对话框,又不想升级,那么按一下返回键就可以暂时把这个对话框关闭。但是这种对话框对于使用程序来操作手机的场景来说,就是一个灾难。
device.watcher('In_Detail_to_Search').when(text='我的世界 Minecraft').when(text='预约').press.back()
这一段代码表示注册了一个名字为“In_Detail_to_Search”的watcher,这个watcher要执行的操作是按下实体按键的返回键。它被激活需要满足3个条件。
(1)代码出现了找不到元素的情况,即将报错。
(2)当前屏幕上某个元素的文本为“我的世界Minecraft”。
(3)当前屏幕上某个元素的文本为“预约”。
watcher是一种特殊的对象,它被注册以后就一直静静地等待,只有在程序里面的某一处代码出现了找不到元素的情况下才会被触发,并按照注册顺序依次进行检查。如果找到了符合条件的watcher,就会执行这个wather对应的操作。如果所有watcher都检查完依然没有找到符合条件的情况,那么就继续抛出找不到元素的异常。
watcher的第1个参数表示这个watcher的名字,可以是任何字符串。但是不同的watcher的名字不能相同。后面的所有“when()”表示满足这个watcher所需要的条件。每个when()之间是“与”的关系,只有所有的when里面的选择器同时满足,这个watcher才会被触发,并执行最后的语句。
10. 其他操作
Android的UI Automator框架可以实现所有图形界面的操作,只要是人能做的它都能做。Python的uiautomator库完整实现了这些操作。但是由于一些操作对于开发爬虫来说并没有什么作用,例如双指缩放屏幕、旋转屏幕等操作。感兴趣的小伙伴可以参阅uiautomator的官方文档。
--------------------------------------
没有自由的秩序和没有秩序的自由,同样具有破坏性。