0x1、引言

上节我们学习了AccessibilityService无障碍的基础知识,并写了一个简单的微信自动登录的小案例。相信大家都意犹未尽,所以本节安排一波实战 —— 微信僵尸好友检测

啥是 僵尸好友

在微信里,对方把你删除/拉黑了,并不会从你的好友列表消失,只有你给他/她发消息,看到红色感叹号才知道。而他/她如果把 加好友验证选项关闭,你发消息不会有红色感叹号,而对方却能看到你发的消息:

他/她顺手点这个把你加回来,你这边是不会有任何提醒的,所以被删这件事你可能永远都不知道~

对于我这种有强迫症的人来说,既然对方删了我,那我也要删了他/她。直接粗暴地给每个好友 群发消息看会不会出现红色感叹号 的方案显然不太行,浪费自己时间不说,还打扰了别人,万一发给了一些不得不加,但平时无天可聊的人,就尴尬了。

随手搜了一下,看到一个 拉群 的方案:拉群时被删的好友会提示不是好友关系

但每次只能检测50人 (好像是这个值),超过50需要对方同意才可以入群,就是会 收到邀请通知。而少于50的话,只要不往群里发消息,被拉的人是不知道群存在的。当然,要是被别人知道了的话,就是社死了~

又随手搜了一下:自动清理微信僵尸好友

九块九解君愁?也有些免费帮你清理的公号 (哪有那么多天上掉馅饼的好事),看了下演示视频,需要扫码登录,猜测用的是PC端协议。登录后,他们可以用你的账号随便发消息,这就存在 风险 了,万一他们:以生病、出车祸、急用钱等各种理由向你的朋友 群发诈骗信息,又或者 群发色情信息 给你造成不良影响,导致封号呢?

所以,但凡涉及到 要你登录 的,都请不要尝试,以免造成不必要的损失。那,有没有 免费安全又好用 的工具呢?还真有,网上很多文章都提到了它李跳跳的 真实好友,界面长这样:

用法简单

打开无障碍权限,点击开始检测,晾一边等它自动检测完,最后会输出正常/异常好友到列表。点击可以复制微信号,打开微信自行搜索,按需删除关系异常的好友即可。

笔者简单体验了一下,很赞,虽然没开源,但APP没申请任何权限(不联网),所以你不需要担心隐私泄露啥的。如果懒得折腾,完全可以放心使用,当然,建议到官方公号「大小姐李跳跳」下载。毕竟破解APP后加点广告、引流信息等恶意内容很常见,比如我就见过跳过广告的APP反而被加入了开屏广告,23333~

em… 好像扯得有点远了,本文的目的不是教会大家使用这款软件,而是 借(chao)鉴(xi) 它, 利用上节所学的AccessibilityService基础,自己实现一个检测微信僵尸好友的工具!本节某些工具代码get√了,也可以为你开发其它无障碍服务脚本提供一些助力哦~ 话不多说,赶紧开始!!!


0x2、如何判断被删除/拉黑?—— 假转账法

上面说了 群发消息拉群 验证好友关系都不太靠谱,所以这里采用真实好友用的—— 假转账法,无感,不打扰对方,也不会产生真实的转账行为。它的判定流程如下:

  • 进入好友转账页,呢称后面出现真实姓名,说明是 正常好友关系
  • 呢称后没有真实姓名,进行 “假转账” 进一步确认关系,可能会出现四种情况:
  • ① 提示:你不是收款方好友,对方添加你为好友后才能发起转账 → 说明被删除了;
  • ② 提示:请确认你和他(她)的好友关系是否正常 → 说明被拉黑了;
  • ③ 提示:对方微信号已被限制登录,为保障你的资金安全,暂时无法完成交易 → 对方账号异常;
  • ④ 弹出:输入支付密码 界面,说明是正常好友关系

核心难点解决了,记者就是编写脚本来实现自动化了~


0x3、实战环节

① 界面设计

设置页 直接复用上节的熊猫头,添加一个 去清理的Button,点击跳转到 清理僵尸好友页,基本UI样式如下:

一个重新检测的Button + 一个显示结果的RecyclerView,非常简洁(lou)~


② 跳转微信

跳转外部APP的方式有两种:Intent指定启动APP包名和Activity名URL Scheme请求,直接给出工具代码:

/*** 跳转其它APP* @param packageName 跳转APP包名* @param activityName 跳转APP的Activity名* @param errorTips 跳转页面不存在时的提示* */
fun Context.startApp(packageName: String, activityName: String, errorTips: String) {try {startActivity(Intent(Intent.ACTION_VIEW).apply {component = ComponentName(packageName, activityName)flags = Intent.FLAG_ACTIVITY_NEW_TASK})} catch (e: ActivityNotFoundException) {shortToast(errorTips)} catch (e: Exception) {e.message?.let { logD(it) }}
}/*** 跳转其它APP* @param urlScheme URL Scheme请求字符串* @param errorTips 跳转页面不存在时的提示* */
fun Context.startApp(urlScheme: String, errorTips: String) {try {startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(urlScheme)))} catch (e: ActivityNotFoundException) {shortToast(errorTips)} catch (e: Exception) {e.message?.let { logD(it) }}
}

读者可能对这里的捕获 ActivityNotFoundException 感到奇怪,为啥不通过 getPackageManager().getInstalledPackages(0) 读已安装应用列表,然后再遍历判断?

答:因为这样不仅需要权限,还涉及到了隐私,为了简化处理,直接捕获这个异常,然后给出未安装的提示。因为如果设备安装了,一般只要你不写错包名啥的,是不会触发这个异常的!调用示例如下:

startApp("com.tencent.mm", "com.tencent.mm.ui.LauncherUI", "未安装微信")
startApp("weixin://", "未安装微信")

另外,URL Scheme对于一些内嵌浏览器页面的APP跳转有奇效,比如之前某东双11活动页的跳转的scheme如下:

"openApp.jdMobile://virtual?params={"category":"jump","action":"to","des":"m","sourceValue":"JSHOP_SOURCE_VALUE","sourceType":"JSHOP_SOURCE_TYPE","url":"https://u.jd.com/kIrrQ3H","M_sourceFrom":"mxz","msf_type":"auto"}'})"

执行后会跳转到下述页面:(活动已过期,正常情况下你是进不了这个页面的~)


③ 搞清Event的触发链条

按照上节所说,可以先把无障碍配置文件里的 android:accessibilityEventTypes 设置为 typeAllMask,监听所有类型的Event。在 onAccessibilityEvent() 里把日志打印出来,然后筛选自己关注的Event类型,最后再把 android:accessibilityFeedbackType 设置为这些类型。

点击重新检测,跳转微信,输出日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0DsrafjB-1670470679475)(https://upload-images.jianshu.io/upload_images/27208505-e6c697b0fe9f3f5e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.LauncherUI
  • 说明:进入微信首页

此时点击底部的 通讯录,输出日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ol36sBKq-1670470679476)(https://upload-images.jianshu.io/upload_images/27208505-140d259469828fb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

当:EventTypeTYPE_VIEW_CLICKEDText通讯录 时说明点击了通讯录。接着随意 点击一个联系人,输出日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ElCkRuNY-1670470679477)(https://upload-images.jianshu.io/upload_images/27208505-cbe27418e7f23b37.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.plugin.profile.ui.ContactInfoUI
  • 说明:进入联系人信息页

接着 点击发消息,输出日志如下:

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.chatting.ChattingUI
  • 说明:进入聊天页

接着 点击加号更多按钮,输出日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XtrtDh3V-1670470679479)(https://upload-images.jianshu.io/upload_images/27208505-5d13b37ab4ce697d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

当:EventTypeTYPE_VIEW_CLICKEDText更多功能按钮,已折叠 说明:点击了更多按钮,接着 点击转账按钮,输出日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pZ3meFip-1670470679480)(https://upload-images.jianshu.io/upload_images/27208505-863e6db1332b68e7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNameom.tencent.mm.plugin.remittance.ui.RemittanceUI
  • 说明:进入转账页,输入0.01,接着 点击转账按钮,输出日志如下:

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.widget.dialog.f
  • 说明:出现异常弹窗,好友关系不正常,比如这里的Text就显示:你不是收款方好友,对方添加你为好友后才能发起转账, 我知道了

接着再试试正常转账,需要输入支付密码的情况,输入日志如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LFd2uilL-1670470679482)(https://upload-images.jianshu.io/upload_images/27208505-563c19711b484a2e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNameandroid.widget.LinearLayout - 说明:出现输入支付密码的页面

因为LinearLayout不是特别的Activity或者类,所以等下还得特别处理一下。当然,这只是大概的Event触发流程,实际开发过程可能出现某些Event不触发的情形,随机应变咯。接着就是在适当的Event,获取相应节点,执行对应的交互,如点击、滑动等。不过再次之前,还得先改动下我们的 无障碍配置文件~。


④ 修改无障碍配置文件

笔者突然有点好奇 真实好友 的配置,那就开扒,定位到它的配置文件:

咦,跟我的配置不一样,没设置 android:packageNames,上节说过不设置这个属性的话,是监听所有App的,检测僵尸好友,不是只应该监听 com.tencent.mm 微信的吗?还有监听的事件类型只监听 typeWindowsChanged,关于这种类型,官方文档中这样介绍到:

API 21新增,系统窗口事件改变会触发,难不成这中event类型更高效?写一个顶几个?于是我Copy了它的配置,并加上 android:packageNames=“com.tencent.mm”,运行后却发现没有日志输出。

接着把它删掉再试,此时有日志信息输出:

但packageName和className都是null,这样能区分哪个APP?哪个页面?真是好友是咋做的?

简单脱下壳导出dex,丢电脑里用jadx反编译成java,直接定位到它的无障碍服务类 → MyAccessibilityService,搜 setServiceInfo(),我感觉它是不是在代码里又进行了动态配置,结果没找着,接着搜 onAccessibilityEvent():

往线程池里丢了线程实例t,跟下t的代码实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wzQBg5C-1670470679487)(https://upload-images.jianshu.io/upload_images/27208505-6200ed7d16dc0a18.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

第一段不难看出大概得逻辑:

getRootInActiveWindow() 获得节点树,然后判断packageName是否为com.tencent.mm,是执行微信相关校验逻辑

第二段稍微难猜一点,应该是用来 判断用户是否退出微信,只在 onServiceConnected() 调用一次,断点了一下for循环:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FS8uTK2E-1670470679488)(https://upload-images.jianshu.io/upload_images/27208505-b24d7fd54cedbce4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

这里拿到了Launcher(桌面启动器) 和 设置的包名信息,尔后的com.android.incallui一般是拨号软件的包名,再加上真实好友的包名。如果包名和这四个匹配,说明用户退出微信页面,中断任务执行。

另外,在阅读源码时还发现了作者不同版本的兼容方式,需要通过id定位节点的,把每个版本节点对应id存一个数组中,遍历查找:

虽然没细看完整代码,不过大概能猜到作者的意图,这样处理的好处,不用区分Event类型,根据页面特征点进行匹配,当前处于哪一步,执行对应的处理逻辑,实属牛啤!

但我们这里不这样做,毕竟练手,2333,还是特意区分event来玩耍,后面再改进亦可,给出我们的无障碍服务配置如下:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"android:description="@string/accessibility_desc"android:accessibilityEventTypes="typeWindowStateChanged|typeViewClicked"android:accessibilityFeedbackType="feedbackSpoken"android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews"android:canRetrieveWindowContent="true"android:notificationTimeout="100"android:canPerformGestures="true"android:packageNames="com.tencent.mm"android:settingsActivity="cn.coderpig.clearcorpse.SettingActivity" />

另外,真实好友配置文件中的 android:accessibilityFlags=“flagRetrieveInteractiveWindows” 这个是用来搭配 TYPE_WINDOWS_CHANGE 事件类型使用的:


⑤ 点击通讯录Tab

跳转微信后,定位到通讯录节点,触发点击,运行打印节点树的python脚本,输出结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3ZYR08j-1670470679491)(https://upload-images.jianshu.io/upload_images/27208505-03e29063902f80a5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

可以看到id未f2s得节点有多个,故这里通过文本匹配,而它的clickable为false,说明是不可点击的,得调用 parent() 获得他的父节点才能点击。

这种情况很常见,获取到的节点不支持点击,有时得连续调用好几个 parent() 才能拿到可点击的节点,跟连体蜈蚣一样。所以这里封装下点击的方法,递归获取能点击的父节点,具体代码如下:

// 点击
fun AccessibilityNodeInfo?.click() {if (this == null) returnif (this.isClickable) {this.performAction(AccessibilityNodeInfo.ACTION_CLICK)return} else {this.parent.click()}
}// 长按
fun AccessibilityNodeInfo?.longClick() {if (this == null) returnif (this.isClickable) {this.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)return} else {this.parent.longClick()}
}

补全下点击代码:

class ClearCorpseAccessibilityService : AccessibilityService() {companion object {const val LAUNCHER_UI = "com.tencent.mm.ui.LauncherUI"  // 首页}override fun onAccessibilityEvent(event: AccessibilityEvent) {if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {when (event.className.toString()) {LAUNCHER_UI -> {event.source?.let { source ->source.getNodeByText("通讯录").click()}}}}}override fun onInterrupt() { }
}

可以,运行后自动点击通讯录了。

关于结点查找的两个方法: findAccessibilityNodeInfosByViewId()findAccessibilityNodeInfosByText() 的返回类型都是 List,而我们大部分时候只要第一个结点,每次得写一堆判空然后取第一个的重复代码显得不太美观,同样封装下,顺带加上轮询,因为有时页面可能还没load完,此时拿不到节点,过一会儿就能拿到了,封装后的代码如下:

/*** 根据id查找单个节点* @param id 控件id* @return 对应id的节点* */
fun AccessibilityNodeInfo.getNodeById(id: String): AccessibilityNodeInfo? {var count = 0while (count < 10) {findAccessibilityNodeInfosByViewId(id).let {if (!it.isNullOrEmpty()) return it[0]}sleep(100)count++}return null
}/*** 根据id查找多个节点* @param id 控件id* @return 对应id的节点列表* */
fun AccessibilityNodeInfo.getNodesById(id: String): List<AccessibilityNodeInfo>? {var count = 0while (count < 10) {findAccessibilityNodeInfosByViewId(id).let {if (!it.isNullOrEmpty()) return it}sleep(100)count++}return null
}/*** 根据文本查找单个节点* @param text 匹配文本* @param allMatch 是否全匹配,默认false,contains()方式的匹配* @return 匹配文本的节点* */
fun AccessibilityNodeInfo.getNodeByText(text: String,allMatch: Boolean = false
): AccessibilityNodeInfo? {var count = 0while (count < 10) {findAccessibilityNodeInfosByText(text).let {if (!it.isNullOrEmpty()) {if (allMatch) {it.forEach { node -> if (node.text == text) return node }} else {return it[0]}}sleep(100)count++}}return null
}/*** 根据文本查找多个节点* @param text 匹配文本* @param allMatch 是否全匹配,默认false,contains()方式的匹配* @return 匹配文本的节点列表* */
fun AccessibilityNodeInfo.getNodesByText(text: String,allMatch: Boolean = false
): List<AccessibilityNodeInfo>? {var count = 0while (count < 10) {findAccessibilityNodeInfosByText(text).let {if (!it.isNullOrEmpty()) {return if (allMatch) {val tempList = arrayListOf<AccessibilityNodeInfo>()it.forEach { node -> if (node.text == text) tempList.add(node) }if (tempList.isEmpty()) null else tempList} else {it}}sleep(100)count++}}return null
}/*** 获取结点的文本* */
fun AccessibilityNodeInfo?.text(): String {return this?.text?.toString() ?: ""
}

封装好的代码等下直接调,美滋滋~


⑥ 好友列表点击

来到好友列表,还是运行打印节点树的python脚本:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eE1kBzKi-1670470679492)(https://upload-images.jianshu.io/upload_images/27208505-1dafb51d52b2366f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

直接就定位到了列表节点,但我突然有点厌烦这种获取方式了,每次找节点都得运行一次脚本,得想办法简化下~

突然心生一计,我直接写个递归遍历结点的方法,把要用到的信息打印出来不就好了?说干就干:

/*** 遍历打印结点* */
fun AccessibilityNodeInfo?.fullPrintNode(tag: String,spaceCount: Int = 0
) {if (this == null) returnval spaceSb = StringBuilder().apply { repeat(spaceCount) { append("  ") } }logD("$tag: $spaceSb$text | $viewIdResourceName | $className | Clickable: $isClickable")if (childCount == 0) returnfor (i in 0 until childCount) getChild(i).fullPrintNode(tag, spaceCount + 1)
}// 调用下
source.fullPrintNode("首页")

运行后,输出日志信息如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WEuke8YG-1670470679493)(https://upload-images.jianshu.io/upload_images/27208505-e0afb4fbb6c71c49.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

舒服了啊,列表项的id也get√了,继续完善下代码:

const val CONTACT_LIST_ID = "js"
const val CONTACT_ITEM_ID = "hg4"override fun onAccessibilityEvent(event: AccessibilityEvent) {when (event.eventType) {TYPE_WINDOW_STATE_CHANGED -> {when (event.className.toString()) {LAUNCHER_UI -> {event.source?.let { source -> source.getNodeByText("通讯录").click() }}}}TYPE_VIEW_CLICKED -> {if (event.text[0] == "通讯录") {// 这里不能用event的getSource(),只能获取到发生改变的节点// 需要调用getRootInActiveWindow()获得所有结点rootInActiveWindow?.let { source ->val contactList = source.getNodeById(wxNodeId(CONTACT_LIST_ID))if (contactList != null) {contactList.getNodeById(wxNodeId(CONTACT_ITEM_ID)).click()} else {logD("未能获取好友列表")}}}}else -> logD("$event")}}

杠杠滴!有了上面的工具代码,后续的开发也变得简单了许多~


⑦ 联系人信息页点击发消息

来到联系人信息页,获取联系人微信号,然后点击发消息,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XSbs0NHs-1670470679494)(https://upload-images.jianshu.io/upload_images/27208505-f285461f0921cc2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

运行后正常点击,控制台看到联系人微信号也打印出来了~


⑧ 聊天页点击加号+转账

来到聊天页,点击更多按钮,底部弹出窗口点击转账。但这里有些奇怪,并没有走到上面的ChattingUI,所以这里换成监听点击了发消息,然后再执行这些操作。

理论上是这样,但实际上并没有点击转账,打断点发现,点击的确实是clickable的父节点。应该是微信做了什么防护,拦截了节点的点击行为。这种情况得变通下了,用手势的方式来实现模拟点击,同样给出直接就能用的工具代码:

/*** 利用手势模拟点击* @param node: 需要点击的节点* */
fun AccessibilityService.gestureClick(node: AccessibilityNodeInfo?) {if (node == null) returnval tempRect = Rect()node.getBoundsInScreen(tempRect)val x = ((tempRect.left + tempRect.right) / 2).toFloat()val y = ((tempRect.top + tempRect.bottom) / 2).toFloat()dispatchGesture(GestureDescription.Builder().apply {addStroke(GestureDescription.StrokeDescription(Path().apply { moveTo(x, y) }, 0L, 200L))}.build(),object : AccessibilityService.GestureResultCallback() {override fun onCompleted(gestureDescription: GestureDescription?) {super.onCompleted(gestureDescription)logD("手势点击完成: 【$x - $y】")}},null)
}

修改下调用处:

可以,手势模拟点击正常~


⑨ 转账页处理逻辑

来到转账页,判断昵称后面是否有真实姓名,是说明好友关系正常。没有的话,转账0.01,出现异常状态弹窗(被删、拉黑、对方账号异常),出现输入密码的弹窗说明关系正常。逻辑非常清楚,就直接给出代码吧:

日志输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pWDG1UZ-1670470679498)(https://upload-images.jianshu.io/upload_images/27208505-c9645bb87e744a83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rni6EW5d-1670470679499)(https://upload-images.jianshu.io/upload_images/27208505-33c85325dbf22d84.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

可以,到此整条检测链条的基本流程就实现啦~


0x4、小结

不知不觉又到文尾,限于篇幅,并没有实现完整功能,目前还差:遍历所有好友执行上述逻辑和检测结果保存了,当然可能还有一些bug,后续会完善下更新到Github上:ClearCorpse,感兴趣的可以先Star,也可以自己续着写,师傅领进门,修行靠自身,多练多总结才是真,感谢,我们下节再见~

作者:coder_pig
链接:https://juejin.cn/post/7170340157185327118

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

【Android自动化】AccessibilityService实战-微信僵尸好友检测相关推荐

  1. 用 Python + Appium 的方式自动化清理微信僵尸好友

    文 | 某某白米饭 来源:Python 技术「ID: pythonall」 随着微信的使用时间越长,微信好友也越来越多,有些好友将你删除了你也不知道.当我们发消息的时候会出现下面扎心的一幕,然后默默将 ...

  2. 强大!Python 自动化清理微信僵尸好友

    文 | 某某白米饭 来源 | Python 技术 自从 微信网页版 被限制登陆之后wxpy库就基本没啥用了,但是还是有很多同学想学微信自动化,其实有些功能是可以用其他自动化库代替的,今天就给大家介绍一 ...

  3. 使用Android辅助功能AccessibilityService实现微信自动聊天【外挂插件】

    本文是使用Android辅助功能AccessibilityService实现微信自动聊天demo: 只是为了跟深入的了解Android辅助功能, 提高自身的动手能力. 请勿用于商用,或非法用途. 动手 ...

  4. python-app自动化查找微信僵尸好友

    通过微信转账的方式来判断,这里没有做删除操作,可以自己去写,对于已经僵尸好友和转账异常的用户会存储到对应文件中,方便查看自己手动删除或重新添加 转账页面不同的提示如下几种,所以要分类型操作:      ...

  5. Python+Appium实现删除微信“僵尸好友”

    话不多说,本文旨在功能使用,不讲解原理!!! 本文所需环境配置: win10 JDK1.8 Python 3.6.8 Appium 1.15.1 android-sdk 文件获取:https://pa ...

  6. python开发程序知道微信好友是否已读信息吗_基于Python+adb实现微信是否好友检测...

    本文介绍的基于Python+adb实现的微信好友检测,是通过adb操控手机,模拟人的点击.截屏.然后调用OCR接口实现好友检测,对应用无侵入,无需扫描登录即可实现好友检测.网上看到一些文章类似功能的实 ...

  7. python检测微信好友是否删除_基于Python+adb实现微信是否好友检测

    本文介绍的基于Python+adb实现的微信好友检测,是通过adb操控手机,模拟人的点击.截屏操作,对应用无侵入,无需扫描登录即可实现好友检测. 网上看到一些文章类似功能的实现,总结起来千篇一律的引入 ...

  8. python批量删除微信好友_用 Python + Appium 的方式自动化清理微信僵尸好友

    随着微信的使用时间越长,微信好友也越来越多,有些好友将你删除了你也不知道.当我们发消息的时候会出现下面扎心的一幕,然后默默将他删除 使用 Appium 基础的 appium 使用在公众号文章 < ...

  9. Android 通过AccessibilityService实现微信聊天记录导出

    接上Android 微信聊天记录.联系人备份并导出为表格继续讲 不太了解AccessibilityService可以看看这篇文章 基本原理: 首先打开 DDMS 捕捉界面元素 拿到resourceid ...

最新文章

  1. pandas drop 删除行和列的方法
  2. 孕妇可以使用计算机,【电脑对孕妇有影响吗】电脑对孕妇的危害,孕妇能玩电脑吗 - 妈妈网百科...
  3. Vue001_模板语法
  4. java的四种引用类型_你知道Java的四种引用类型吗?
  5. Hibernate bean 对象配制文件
  6. 回归(regression)——统计学习方法
  7. 常见的几种 RuntimeException
  8. base64 convert to file
  9. 源码安装MySQL5.5.20
  10. WhatsApp拟取消服务订阅年费
  11. STM32F107VCTx I2C通信
  12. 想去掉抖音短视频里的水印,有没有一键去水印的方法?
  13. duble 和 float 小数的位数
  14. 快速查看本机公网IP地址
  15. flash player 11.2 64位 linux,Adobe Flash Player 11.2.202 Beta 1支持 64位操作系统
  16. 笔记本java怎么启动独立显卡_笔记本双显卡怎么切换,告诉你笔记本双显卡怎么切换到独立显卡...
  17. 本科学经济还是学数学和计算机,如果考研想往经管类方向,学习数学类,还是数学与应用数学, 这两个专业有什么不同...
  18. php http请求 返回数据包太大 499,http错误码原理及复现 - 499,500,502,504
  19. Matlab 可见光波段植被指数
  20. 从零开始写Python爬虫 --- 1.4 爬取生活大爆炸百度贴吧内容

热门文章

  1. 微型计算机k80,微型计算机原理与接口技术课程设计报告智能交通灯控制系统设计(15页)-原创力文档...
  2. 学Python好还是学Java好?学哪个更好就业?
  3. 跟杨春娟学Spring笔记:AOP之SpringAOP引介通知
  4. WMS系统解决方案,多系统无缝集成,解决信息孤岛
  5. [磁盘清理] Windows Server 2003 系统盘清理
  6. Halcon2019软件安装教程
  7. 装完黑苹果怎么装windows_手把手教你轻松安装 Win10/ 黑苹果macOS10.14.1双系统
  8. 【组合导航】imu中的低通滤波器
  9. PHP Web应用开发 -用PHP实现简单的个人博客网站
  10. java数组 如何动态增加、查询、删除元素