uiautomator2 操作手机
目录
[TOC]
一: 需求:
手机插入电脑端后,可以从电脑端控制手机进行微信公众号的信息爬取
二: 选择uiautomator2调研的原因?
uiautomator2底层是Google提供的用来做安卓自动化测试的UiAutomator。 UiAutomator功能很强:
可以对第三方App进行测试
获取屏幕上任意一个App的任意一个控件属性,并对其进行任意操作
测试逻辑能够用python编写:将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库
UiAutomator的功能可以满足操控手机,获取组件进而提取想要的信息;uiautomator2是一个python库。基于这些方面的考虑,选择了uiautomator2进行实际调研
三: 环境搭建
3.1 安装python库
uiautomator2(主要库)
weditor(直观获取组件层次)
jupyter(notebook笔记方便调试记录)
3.2 配置adb环境
下载adb
ADB和Fastboot for Windows: https://dl.google.com/android...
ADB和Fastboot for Mac: https://dl.google.com/android...
ADB和Fastboot for Linux: https://dl.google.com/android...
配置环境变量
windows:控制面板->系统与安全->系统->高级系统设置->环境变量,将adb路径添加到PATH中
linux: 编辑.bashrc, 添加export PATH=$PATH:adb路径, 然后执行source .bashrc
四: 使用
4.1 连接手机
- 检测手机是否正确配置
打开手机的开发者功能,并开启调试功能
使用usb线连接电脑时,usb用途选择“传输文件”
在命令行中使用adb devices查看是否成功检测到 - 获取手机连接后的识别码
import os
cmd = "adb devices"
r = os.popen(cmd)
text = r.read() # adb devices命令的执行结果
r.close()
row = text.split("\n")[1]
if not bool(row):
print("未检测到设备")
raise Exception("未检测到设备")
else:
con_str = row.split('\t')[0]
print(con_str) # 获取识别码
- 使用uiautomator2连接手机
import uiautomator2 as u2
d = u2.connect(con_str)
print(d.info)
4.2 操作微信搜索公众号
- 微信版本信息
d.app_info("com.tencent.mm") 打开微信
输入中文的时候需要使用fastinputIme
d.set_fastinput_ime(True)
先关掉微信重新启动
d.app_stop("com.tencent.mm")
d.app_start("com.tencent.mm") # 微信
- 获取状态栏大小(后续滑动的时候会用到)
status_bar_selector = d.xpath('//*[@resource-id="com.android.systemui:id/status_bar"]')
status_bar_x, status_bar_y, status_bar_width, status_bar_height = status_bar_selector.get().rect
print(status_bar_x, status_bar_y, status_bar_width, status_bar_height) 查找公众号
设定需要爬取的公众号变量
spider_name = "进击的Coder"
定位添加按钮"+"
selector_add_button = d.xpath('//com.tencent.mm.ui.mogic.WxViewPager//android.widget.ListView//android.widget.FrameLayout/android.widget.LinearLayout[1]/android.widget.RelativeLayout[2]/android.widget.ImageView')
selector_add_button.click()
定位"添加朋友"
d(text="添加朋友").click()
选择"公众号"
d(text="公众号").click()
输入要搜索的公众号名字,并搜索
d.set_fastinput_ime(True)
d.send_keys(spider_name, clear=True)
time.sleep(1)
在输入法中要打开OSError: FastInputIME started failed
try:
d.send_action("search")
except OSError:
raise Exception("请手动切换输入法")
从搜索结果中选择
time.sleep(2)点击对应名字的公众号进入
d.xpath('//*[@text="{}"]'.format(spider_name)).click()
判断是否关注
判断是否关注
need_subscribe=d.xpath('//*[@text="关注公众号"]').all()
if bool(need_subscribe):
print("没有关注该公众号")
# 关注后会自动进入该公众号
d.xpath('//*[@text="关注公众号"]').click()
# 等页面加载, 然后退回列表页面
time.sleep(2)
d.xpath('//android.support.v7.widget.LinearLayoutCompat').click()
else:
print("已经关注该公众号")
4.3 处理列表- 层次结构
ListView组件下有多个LinearLayout
LinearLayout下有0/1/多个ViewGroup
ViewGroup是一条消息所占据的组件
组件层次结构图示 - 思路
因为列表页面只加载屏幕能够显示的元素, 所以不能直接获取列表的全部, 需要依次滚动.
先处理当前显示的元素, 提取出ListView, 然后提取出ListView下的所有LinearLayout组件
每一个LinearView通过deal_linear_layout函数进行处理
提取出LinearLayout下的所有ViewGroup组件
每一个ViewGroup通过deal_view_group函数进行处理
在进行向下细分处理时, 传入当前组件是否是同类型最后最后一个的标识
如果LinearLayout是最后一个
如果没有ViewGroup, 返回该LinearLayout的rect属性
如果有ViewGroup:
如果该ViewGroup是最后一个, 返回rect属性
如果不是, 点击进入文章详细页面, 进行提取操作, 继续循环
根据deal_list函数的返回进行滑动操作
处理显示的列表页面
def deal_list():
""" 处理某时刻的列表"""
xpath_linear_layouts = '//*[@resource-id="android:id/list"]/android.widget.LinearLayout'
selector_linear_layouts = d.xpath(xpath_linear_layouts)
count_linear_layout = len(selector_linear_layouts.all())
result = None
for index in range(1, count_linear_layout+1):result = deal_linear_layout( '{}[{}]'.format(xpath_linear_layouts, index), index==count_linear_layout)
return result
处理单个LinearLayout组件
def deal_linear_layout(xpath_linear_layout, is_last_linear):
"""处理单个linear_layout"""
xpath_group_layouts = '{}/android.view.ViewGroup'.format(xpath_linear_layout)
selector_group_layouts = d.xpath(xpath_group_layouts)
count_group_layout = len(selector_group_layouts.all())
result = None
if count_group_layout == 0:print("没找到ViewGroup", is_last_linear) if is_last_linear: result = d.xpath(xpath_linear_layout).get().rect
else:
print("找到了ViewGroup, 开始处理组消息", count_group_layout) for index in range(1, count_group_layout+1): result = deal_view_group( '{}[{}]'.format(xpath_group_layouts, index), is_last_linear and index==count_group_layout)
return result
处理单个GroupView组件
def deal_view_group(xpath_group_layout, is_last_group):
"""处理单个group_layout"""
if is_last_group:# 返回最后一个groupview的rect selector_last_group_layout = d.xpath(xpath_group_layout) last_group_layout = selector_last_group_layout.get() return last_group_layout.rect
else:
selector_group_layout = d.xpath(xpath_group_layout) # 进入消息详情页面,提取消息具体信息 # selector_group_layout.click() return None
- 处理当前列表并滑动操作(重复执行)
last_layout_rect = deal_list()
sx=360
sy=last_layout_rect[1]
ex=360
ey=status_bar_height
d.drag(sx, sy, ex, ey)
- 结尾判断(未完善)
注: 列表是否进行到头的判断还没有添加
4.4 处理文章详情 标题
获取标题名
title_selector = d.xpath('//*[@resource-id="activity-name"]')
if len(title_selector.all()) != 0:
ele = title_selector.get()
print("获取到了标题:", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
else:
print("未找到标题")meta数据
版权logo
copyright_selector = d.xpath('//*[@resource-id="copyright_logo"]')
if len(copyright_selector.all()) != 0:
ele = copyright_selector.get()
print("获取到了版权", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
else:
print("未找到版权")
作者
author_selector = d.xpath('//*[@resource-id="js_name"]')
if len(author_selector.all()) != 0:
ele = author_selector.get()
print("获取到了作者", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
else:
print("未找到作者")
时间
time_selector = d.xpath('//*[@resource-id="publish_time"]')
if len(time_selector.all()) != 0:
ele = time_selector.get()
print("获取到了时间", ele.attrib.get("content-desc") if bool(ele.attrib.get("content-desc")) else ele.attrib.get("text"))
else:
print("未找到时间")
正文(未完善)
views_selector = d.xpath('//*[@resource-id="js_content"]/android.view.View')
if len(views_selector.all()) !=0:
for view in views_selector.all():view_string = view.attrib.get("content-desc") if bool(view.attrib.get("content-desc")) else view.attrib.get("text") if bool(view_string): print(view_string.strip())
else:
print("结尾了")
也需要同处理列表相同的操作,边滑动边处理- 返回列表页
back_selector = d.xpath('//*[@resource-id="com.tencent.mm:id/dn"]')
if len(back_selector.all()) == 0:
print("未找到返回键")
else:
print("返回")
back_selector.click()
五: 调研结果
5.1 碰到的问题
组件识别问题:存在在一些机型上有些界面无法分析组件关系,影响下一步的点击操作。
输入法切换问题:输入要搜索的内容后,需要回车确认。因为无法识别手机自带输入法的“搜索”按钮,所以需要切换uiautomator2库自带的输入法。部分型号手机无法使用命令切换。
5.2 手机型号&微信版本
手机型号 安卓型号 微信版本 切换输入法 识别组件
oppo A5 8.1.0 7.0.9 否 部分不能识别
搜索结果界面无法识别
oppo A5 8.1.0 7.0.10 否 可
红米6 9 7.0.12 可 可
红米6 8.1.0 7.0.6 可 部分不能识别
搜索结果界面无法识别
红米note5 9 7.0.10 可 可
5.3 目前结论
组件识别问题:需要微信版本7.0.10以上
输入法切换问题:红米系列手机可自动切换输入法; oppo A5不能自动切换输入法,需要提示用户手动切换