(1条消息)索引(从零开始)必须大于或等于零,且小于参数列表的大小。_数据库_pyy的博客-CSDN博客

mikel阅读(1425)

来源: (1条消息)索引(从零开始)必须大于或等于零,且小于参数列表的大小。_数据库_pyy的博客-CSDN博客

System.FormatException”类型的异常在 mscorlib.dll 中发生,但未在用户代码中进行处理

其他信息: 索引(从零开始)必须大于或等于零,且小于参数列表的大小。

去掉“,{2}”

return string.Format(“[{0},{1}]]”, strb.ToString(), strData);

修改如下:
————————————————
版权声明:本文为CSDN博主「u010082526」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010082526/article/details/78920563

因一纸设计稿,我把竞品APP扒得裤衩不剩(中)_移动开发_Coder-Pig的猪栏-CSDN博客

mikel阅读(969)

来源: 因一纸设计稿,我把竞品APP扒得裤衩不剩(中)_移动开发_Coder-Pig的猪栏-CSDN博客

严正声明:

1、相关破解技术仅限于技术研究使用,不得用于非法目的,否则后果自负。
2、笔者仅出于对技术的好奇,无恶意破坏APP,尊重原开发者的劳动成果,未用于商业用途。
0x1、无形之刃,最为致命 => 碎碎念
上一篇文章《因一纸设计稿,我把竞品APP扒得裤衩不剩(上)》是一篇比较简单的:

jsw => 技师文,呸,
jsw => 记述文,呸呸,
jsw => 技术文,呸呸呸,这什么垃圾输入法!
技术文,但是这评论区的风气,貌似有点不对???

冤枉啊,小弟真没去过这种地方,也没体验过这种“服务”,只是道听途说,可能:

我描述得「绘声绘色」,加之各位看官「浮想联翩」,才会觉得「煞有介事」。

em…那个,可以扶下我起来么,那个…跪久了…腿有点麻…

顺带恭喜下:FPX 3-0 G2,喜提S9总冠军,FPX牛逼!!!破音!!!

亚索的快乐你不懂~

哈哈,回到本文:

发现 => 很多童鞋对APP逆向很感兴趣;
但是 => 网上关于APP逆向文章的比较零散;
不知 => 如何入手,毕竟逆向的水可深了;
笔者 => 也只是个小白玩家,兴趣使然玩玩而已;
分享 => 目前会的一些「Android APP基础逆向姿势」;
还望 => 各位真丶逆向大佬轻喷;
如有 => 有更好的工具或者方法安利,欢迎评论区指出,谢谢~
顺带分享几个笔者常逛的逆向论坛:

看雪论坛:https://bbs.pediy.com/
吾爱破解:https://www.52pojie.cn/
蚁安网:https://bbs.mayidui.net/
逆向大佬姜维:http://www.520monkey.com/
贴心提醒:

此文内容较多,可能会有些枯燥,建议先点赞收藏,茶余饭后再慢慢品尝~

0x2、提莫队长,正在待命 => 硬件准备
在开始折腾Android APP逆向前,你需要:

1、一台「具有完整Root权限」的Android手机,注意是「完整Root」权限!!!

比如「魅族手机」在设置->安全->Root权限,中可以开启Root权限,但是却是「阉割的Root权限」,安装SuperSu重启后就一直卡气球。

2、怎么Root?根据自己的手机机型百度和逛各种搞机论坛吧(不要问我!)一般的常见的流程:

解BL锁(BootLoader) -> 刷第三方Recovery(如TWRP) -> 卡刷Maglisk 或 SuperSU(Android 8.0以前)

3、不要轻易尝试使用哪种「一键Root」的软件(大部分是毒瘤,如KingRoot),当然不是就不能用,可以过河拆桥,比如我的魅蓝E2的Root流程:

安装KingRoot(v5.0)授予Root权限,修复Root权限异常,此时就有完整Root权限了;
接着用「移除KingRoot」删掉KingRoot,此时还有完整Root权限;
安装SuperSu(v2.8.2),常规方式更新二进制文件,重启,Root完成。
4、推荐些能Root的手机?

Google Pixel亲儿子(真原生,香,就是性价比不高),小米,一加 等。

5、没钱买Android机或者已经有不能Root的手机了,可以试试「Android模拟器」

AS自带的AVD模拟器Root可以参见《搞机:AS自带模拟器AVD Root 和 Xposed安装》
也可以使用其他第三方的安卓模拟器,比如「夜游安卓模拟器、BlueStacks蓝叠」等。

0x3、一点寒芒先到,随后枪出如龙 => 概念与名词
在开始折腾APP逆向前,先来了解一些概念与名词~

① APK文件里都有什么?
获取APK的渠道:酷安、应用宝,豌豆荚等应用市场下载,有些还提供「应用历史版本」下载。
APK本质上是一个「压缩包」,把「.apk后缀」改为「.zip后缀」后解压,可以看到如下目录结构 (可能还有其他文件):

简单介绍下:

② 编译APK和反编译APK
所谓的「编译」,就是把「源码、资源文件等」按照一定的「规则」打包成APK,官网 提供了详细的编译构建过程图:

简述下大概流程:

Step 1:资源文件处理「AAPT」

assets会原封不动地打包在APK中;
res中每一个资源会赋予资源ID,以常量形式定义在R.java中,生成一个resource.arsc文件(资源索引表)。
Step 2:aidl文件「aidl」

将aidl后缀的文件转换为可用于进程通信的C/S端Java代码。
Step 3:Source Code「Java Compiler」

编译生成.class文件。
Step 4:代码混淆「ProGuard」(可选)

增加反编译难度,命名缩短为1-2个字母的名字,压缩(移除无效类、属性、方法等),优化bytecode移除没用的结构。
Step 5:转换为dex「dx.bat」

把所有claas文件转换为classes.dex文件,class -> Dalvik字节码,生成常量池,消除冗余数据等。(方法数超65535会生成多个dex文件)
Step 6:打包「ApkBuilder」

把resources.arsc、classes.dex、其他的资源一块打包生成未签名apk。
Step 7:签名「Jarsigner」

对未签名apk进行Debug或release签名。
Step 8:对齐优化「zipalign」

使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。
如果想了解更多编译构建流程可移步至:《10分钟了解Android项目构建流程》
而「反编译」则是反过来了,通过一些反编译工具,提取出源码,转换过程如下:

「APK ====> Dex ====> Jar(class文件)/Smali ==> Java源码」。

③ 加固和脱壳
APK可以说是每个Android开发仔的「心血结晶」,把各种自己觉得「牛逼哄哄的奇淫巧技」封装其中。但总有些「心怀叵测」想去搞你的APP,通过一些「反编译工具」获取你的源码,然后为所欲为:

加广告:你应用免费,给你加点广告,亦或者改成付费,然后下载量比你的多,气不气?
破解付费:你应用收费,Hook掉你的检测方法,发个破解包,还到处传播,气不气?
恶意攻击:逆向得出请求接口规律,批量短信验证注册,耗光你的短信池等,气不气?
这种「恶劣」的行径令人「气愤」像极了某类经典「动作电影」里的桥段:

男子上进,努力工作,妹子贤惠,料理家务;
妹子每天做好饭菜,等男子回来,一起吃饭,满怀憧憬,畅谈以后的二人世界;
酒饱饭后,温饱思XX,不可描述一番,却被「居心叵测」的邻居给盯上了;
和往常一样,男子出门上班,妹子在家做家务,晾衣服;
邻居 上线,用「谎言」诱骗妹子开门,接而挤门而入;
用「暴力和胁迫」,无视妹子的やめて和反抗,违背个人意愿;
粗暴地把衣服一件件褪去,仅剩下那「万恶的马赛克」;
守护着最后的一处「绝对领域」;
在几番不可描述后,把妹子占为己有,然后像玩物般戏耍。
看着妹子「因情绪过激而身体抽搐」,哭得「梨花带雨」,不禁让人「心生怜惜」,像我这种感性的蓝孩子:

总会忍不住抽上几张抽纸,“静静抹泪”,擦拭完,顿觉索然无味,一片空明,然后开始反思:

为什么那个邻居不是我?呸呸呸…

除了「同情女主」和「斥责坏人」外,应该如何避免这种事情的发生呢?

1、花点钱,请个「保镖」看门,坏人想进来要先过保镖这一关;
2、给妹子「加个锁」,让坏人无法不可描述,只能望而兴叹。
可以把例子中的「妹子」看做是我们编写的「APK」,而「请保镖」和「加锁的操作」则可以看做是「APK加固」,另外加固又称「加壳」,壳的定义:

一段专门负责「保护软件不被非法修改或反编译的程序」,一般先于程序运行,拿到控制权,然后完成它们保护软件的任务。

有「加壳」,自然也有「脱壳」,即去掉这层壳,拿到源码,也称为「砸壳」。

关于加固技术的发展,看雪上有篇:《一张表格看懂:市面上最为常见的 Android 安装包(APK)五代加固技术发展历程及优缺点比较》,不过图不怎么清晰,笔者重新排版了一下,有兴趣的读者可以看看:

④ 混淆与反混淆
「混淆」可以类比为上面「万恶的马赛克」,阻碍人类进步的绊脚石。而混淆则是增加了反编译的难度,同理,「反混淆」则对应「去除马赛克」,试图还原它原来的样子。

0x4、发动机已启动,随时可以出发 => 获得APP源码

加固虽然能在一定程度上「防止反编译和二次打包」,但加固后的APP可能会带来一些问题:

体积增大,启动速度变慢,兼容问题等

网上「免费加固」方案有很多,脱壳教程也是烂大街,而且有些恶心的第三方加固还会给你加点料(360加固锁屏广告),而使用「企业级的加固」,则需要支付不菲的费用,所以很多APP直接选择了「裸奔」。先来讲解一下未加固的怎么获取源码吧~

① 未加固(笔者使用的工具:apktool + jadx)
使用apktool:获取「素材资源,AndroidManifest.xml以及smail代码」
使用jadx:把「classes.dex」转换为「.java」代码
使用Jadx的注意事项:

使用jadx-gui可直接打开apk查看源码,但如果APK比较大(classes.dex有好几个),会直接卡死(比如微信),笔者的做法是命令行一个个dex文件去反编译,最后再把反编译的文件夹整合到同一个目录下。

这样的操作繁琐且重复,最适合批处理了,遂写了个反编译的批处理脚本(取需):

“””
自动解压apk,批量使用jadx进行反编译,结果代码汇总
“””
import os
import shutil
import zipfile
from datetime import datetime

apk_file_dict = {} # APK路径字典

# 遍历构造APK路径字典(构造文件路径列表,过滤apk,拼接)
def init_apk_dict(file_dir):
apk_path_list = list(filter(lambda fp: fp.endswith(“.apk”),
list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))))
index_list = [str(x) for x in range(1, len(apk_path_list) + 1)]
return dict(zip(index_list, apk_path_list))

# 移动文件夹
def move_dir(origin_dir, finally_dir):
shutil.move(origin_dir, finally_dir)

# 如果文件夹存在删除重建
def deal_dir_existed(path):
if os.path.exists(path):
print(“检测到文件夹【%s】已存在,执行删除…” % path)
shutil.rmtree(path)
os.makedirs(path)

# 判断目录是否存在,不存在则创建
def is_dir_existed(path, mkdir=True):
if mkdir:
if not os.path.exists(path):
os.makedirs(path)
else:
return os.path.exists(path)

# 获取目录下的所有文件路径
def fetch_all_file(file_dir):
return list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))

# 解压文件到特定路径中
def unzip_file(file_name, output_dir):
print(“开始解压文件…”)
f = zipfile.ZipFile(file_name, ‘r’)
for file in f.namelist():
f.extract(file, os.path.join(os.getcwd(), output_dir))
print(“文件解压完毕…”)

if __name__ == ‘__main__’:
print(“遍历当前目录下所有APK…”)
apk_file_dict = init_apk_dict(os.getcwd())
print(“遍历完毕…\n\n============ 当前目录下所有的APK ============\n”)
for (k, v) in apk_file_dict.items():
print(“%s.%s” % (k, v.split(os.sep)[-1]))
print(“\n%s” % (“=” * 45))
choice_pos = input(“%s” % “请输入需要反编译APK的数字编号:”)
print(“=” * 45, )
choice_apk = apk_file_dict.get(choice_pos)
apk_name = choice_apk.split(os.sep)[-1][:-4] # APK名字

# 创建相关文件夹
crack_dir = os.path.join(os.getcwd(), apk_name) # 工程根目录
deal_dir_existed(crack_dir)
crack_apktool_dir = os.path.join(crack_dir, “apktool” + os.sep) # APKTool反编译目录
deal_dir_existed(crack_apktool_dir)
crack_jadx_dir = os.path.join(crack_dir, “jadx” + os.sep) # JADX反编译目录
deal_dir_existed(crack_jadx_dir)
crack_temp_dir = os.path.join(crack_dir, “temp” + os.sep) # 解压后文件的临时存储路径
deal_dir_existed(crack_temp_dir)

# 利用APKTool提取资源文件
begin = datetime.now() # 计时
print(“APKTool提取资源文件…”)
os.system(“./apktool d %s -f -o %s” % (choice_apk, crack_apktool_dir))

# 复制一份AndroidManifest.xml、res、assets文件到外部
shutil.copy(os.path.join(crack_apktool_dir, “AndroidManifest.xml”), os.path.join(crack_dir, “AndroidManifest.xml”))
shutil.copytree(os.path.join(crack_apktool_dir, “res” + os.sep), os.path.join(crack_dir, “res” + os.sep))
shutil.copytree(os.path.join(crack_apktool_dir, “assets” + os.sep), os.path.join(crack_dir, “assets” + os.sep))
print(“资源文件提取完毕”)

# 利用jadx反编译源码
print(“JADX反编译提取源码…”)
choice_apk_zip = shutil.copy(choice_apk, choice_apk.replace(“.apk”, “.zip”))
unzip_file(choice_apk_zip, crack_temp_dir)
print(“开始批量反编译dex文件”)
for dex in list(filter(lambda fp: fp.endswith(“.dex”), fetch_all_file(crack_temp_dir))):
os.system(
“./jadx -d {0} {1}”.format(os.path.join(crack_jadx_dir, dex.split(os.sep)[-1][:-4]), dex))
print(“所有dex文件反编译完毕”)
# 将资源文件移入
shutil.move(os.path.join(crack_dir, “AndroidManifest.xml”), os.path.join(crack_jadx_dir, “AndroidManifest.xml”))
shutil.move(os.path.join(crack_dir, “res” + os.sep), os.path.join(crack_jadx_dir, “res” + os.sep))
shutil.move(os.path.join(crack_dir, “assets” + os.sep), os.path.join(crack_jadx_dir, “assets” + os.sep))
# 删除临时文件夹,压缩文件
shutil.rmtree(crack_temp_dir)
os.unlink(choice_apk_zip)
end = datetime.now()
print(“收尾操作~~~\n反编译完成,总耗时:%s秒” % (end – begin).seconds)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
执行前,你需要把apktool相关的东西,丢到jadx/build/jadx/bin目录下,如图所示:

接着终端键入:python3 auto_extract_apk.py,回车后输入对应编号,回车开始编译:

静待片刻后:

Tips:这里没有把多个classes文件夹整合到一起,是因为有些APP会出现合并冲突。

打开反编译后的目录,有如下两个文件夹:

按照自己的需要用Android Studio打开其中一个就好了:

apktool目录:apktool反编译后的内容,主要用于smail动态调试。
jadx目录:反编译成Java的内容。
② 反混淆(simplefy、Deguard)
代码是拿到了,但是打开代码,「一堆的abcd」,跟到眼花,可以试下「反混淆」,方案有两类,一种是通过「代码逆推」出名字,另一种是通过「统计逆推」出名字。

第一种方案的工具有很多(Jeb2,simplify等),前者付费需破解,Java版本有限制,Mac配置有点麻烦,故笔者用的是后者:「Simplefy」,Github仓库:https://github.com/CalebFenton/simplify,使用方法也很简单:

打开终端依次键入:

# 拉取仓库代码
git clone –recursive https://github.com/CalebFenton/simplify.git

# 来到目录下
cd simplify

# 编译
./gradlew fatjar
1
2
3
4
5
6
7
8
编译后完,执行下述指令即可反混淆APK:

# 反混淆APK(需要反混淆的APK,反混淆后的APK名)
./gradlew build && cp xxx.apk yyy.apk
1
2
静待反混淆完毕,接着反编译批处理脚本走一波,打开MapFragment比对下:

相比混淆前,多了一些变量名,当然也不是完全的,偶尔还是有abcd,但是可读性稍微提高了些,比如查找的时候不用在一个个adcd排除,但是,编译挺耗时的,而且我的电脑风扇呼呼呼地响。

第二种是通过统计的方法,利用统计推断出名字:DEGUARD:http://apk-deguard.com/,打开官网:

选择需要反混淆的APK后,Upload上传,接着等待处理完成,!!!别关页面!!!

一般需等待1-10分钟,处理完成后,点击output.apk,把APK下载到本地,同样执行批处理脚本反编译一波,和simplefy反混淆后的代码对比下:

大同小异,另外,反混淆并不能100%还原,而且还可能有些小错误,比如下面的代码:

虽说反编译后的可读性有所提高,但建议还是搭配着混淆的源码看。

③ 脱壳(FDex2,反射助手,dumpDex)
终于来到很多同学期待的脱壳环节,先说明下,笔者只是「工具党」水平,不会Native层的,so文件调试!如果本节的工具,你脱不出来,或者脱出来有问题,笔者也是爱莫能助。看雪有很多帮人脱壳的大佬,可以在上面发个帖子求助下~

1、判断是哪种加固

解压apk后在assets目录下看到so文件,比如360加固宝:libjagu.so和libjiagu_x86.so,百度搜下名字就知道是哪家的加固了,也可以直接用后面讲的「MT文件管理器2.0」直接查看。

2、FDex2脱壳(只适用于Android 7或以下版本,可以脱市面上大多数免费加固,成功率较高,推荐)

有ROOT:安装「XposedInstaller」和「FDex2」
没ROOT:安装「VirtualXposed」「FDex2」
比如:这里有个「360免费版加固的APK」,直接用jadx反编译后导入AS,但是反编译后的classes:

只有这么一丢丢点东西,把「待脱壳应用」安装到手机上,接着用FDex2来脱壳
已Root玩家:XposedInstall启用FDex2插件重启后,按如下步骤脱:

Step 1:FDex2选中待脱壳应用:

Step 2:打开待脱壳应用,接着来到上图的dex输出目录:

Step 3:把整个目录拉到电脑上,这里直接用adb命令拉取:
adb root
adb pull /data/user/0/包名 电脑文件夹
1
2

Step 4:「剔除加固相关的dex」,用jadx-gui依次打开,看到下面这种,直接把dex删掉

Step 5:使用jadx命令反编译dex,顺带改名,命名规则:按照文件大小降序,示例如下:
# 按照文件从大到小排序!!!
jadx aaa.dex -d classes
jadx bbb.dex -d classes1
jadx ccc.dex -d classes2
1
2
3
4
Step 6:删掉没脱壳前反编译项目里的classes,把这几个复制到其中:

行吧,脱壳成功,这里其实还可以还原APK的(二次打包),等下再讲~
未root玩家,安装打开VirtualXposed,添加应用:Fdex2和待脱壳应用

如果炮制,只是dex的路径有些不一样。

3、反射大师(和FDex类似,下载地址:https://www.lanzous.com/b04xxlujg)

注意,同样只支持Android 7.0及以下,adb安装后,xposed启用插件,重启手机,接着打开反射大师:

Step 1:选中待脱壳APP,弹出对话框选择打开

Step 2:点击中间的六芒星,弹出如下对话框,长按「写出DEX」

Step 3:等待写出完毕,可以在/storage/emulated/0中找到导出的dex:

Step 4:pull到电脑上用jadx-gui打开看看:

行吧,脱壳成功,就是我们想要的dex了,另一个classes2.dex则是相关的~:

4、dumpDex脱壳(Github:https://github.com/WrBug/dumpDex/releases)

官方仓库的README.md中有一句:

可以的话建议自己编译,流程也很简单:

# 1、拉取项目代码到本地
git clone https://github.com/WrBug/dumpDex.git

# 2、AS中Open项目,等待编译完成

# 3、删掉build.gradle里签名相关的代码

# 4、点击顶部菜单栏Build -> Build APK,或者直接在终端./gradlew clean build

# 5、adb命令直接把编译生成的apk安装到手机上

# 6、接着来到如下左图路径,把对应的so,通过adb push到目录下:
adb push lib/armeabi-v7a/libnativeDump.so /data/local/tmp
adb push lib/arm64-v8a/libnativeDump.so /data/local/tmp/libnativeDump64.so
# 修改权限
adb shell
su
chmod 777 /data/local/tmp/libnativeDump.so
chmod 777 /data/local/tmp/libnativeDump64.so
# 临时关闭SELinux(重启后会失效,可调用getenforce查询)
setenfore 0

# 7、打开XposedInstaller看已经启用DumpDex插件,是的话重启手机

# 8、开机后,打开想脱壳的应用,不用理闪退,接着打开data/data/包名查看是否有Dump目录

# 9、进入如果出现下图所示的多个dex,说明脱壳成功,否则可能是脱壳失败
# (看是否有报错信息),或者不支持(比如360加固免费版只支持新版,不支持旧版)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

另外,脱出来的dex不一定就可用,比如某个用了「腾讯御安全」的应用:

用jadx-gui打开这的dex,一堆这样的错误:

出现这个的原因是「指令集被抽取」,打开smail文件你就知道了:

方法指令都被nop(零)替换了,工具党到这里就可以放弃了,要调试so文件。

Tips:本节用到的东西,都有给出比较官方的下载链接!!!你也可以到公号
「抠腚男孩」输入000,回复对应序号下载,谢谢~

参考文献:

Android打包流程
————————————————
版权声明:本文为CSDN博主「coder-pig」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/coder_pig/article/details/103615488

10分钟了解Android项目构建流程 - 掘金

mikel阅读(1085)

来源: 10分钟了解Android项目构建流程 – 掘金

前言

上两篇博客中提到了构建过程的问题,之前毕业在准备面试的过程中,对这个部分有过较为认真的学习,也进行了博客记录,但是实际工作过程中,如果是在写业务逻辑上,那么这方面的问题接触的就会比较少了。逐渐的淡忘了,其次,之前所写的文章条理性也不是很强,同时,最近准备进行Gradle插件的一系列博客的产出,其中将会涉及到很多与项目构建相关的内容。所以此文也将成为后续文章的一个铺垫。

构建过程

项目的构建: 当我们打开一个项目,我们可以看到的是我们写的Java Code文件or Other JVM Code,资源文件,Build配置文件,但是通过run the project,我们就可以得到一个在我们的Andoid设备上可以运行的Apk,上线应用市场,还需要我们对其进行签名处理,来确保我们App的唯一性和安全性。整个过程就是所谓的项目构建。

如何实现整个构建的过程,对于每一个构建的步骤,都需要相应的功能模块来进行,比如Java Code编译,如何打成dex包等等,而这Android则为我们提供了相应的工具,在Android Studio命令行窗口中,我们可以通过相应的命令行来进行控制,但是,整个构建过程涉及到很多的步骤,很多的工具的使用,如果都通过命令行来进行控制,势必会相当麻烦,因此Androd Studio等IDE则对整个过程进行了一个打包,当我们在Run project的时候,底层的打包工具就会被调用,打包流程都会自动执行。然后我们只需要对构建文件按照自己的需求进行相应的配置,就可以构建出自己所需要的项目。

那么,整个Andoid项目的构建过程中,都执行了那些构建的任务呢?

首先看一下,Google官方为我们提供的详细的构建过程图

构建过程概述

如果你接触Android开发已经有一段时间了,我想当你看到这张图的时候,就会觉得很清晰。但是更多的可能会一头雾水,如果之前没有阅读相关的资料的话,那么,接下来,将针对上述的构建过程,先给出一个概述,这样你将会整个构建流程在心中有一个框架,然后针对其中具体的细节,进行进一步详细的讲解。

图中绿色标注为其中用到的相应工具,蓝色代表的是中间生成的各类文件类型。

  • 首先aapt工具会将资源文件进行转化,生成对应资源ID的R文件和资源文件。
  • adil工具会将其中的aidl接口转化成Java的接口
  • 至此,Java Compiler开始进行Java文件向class文件的转化,将R文件,Java源代码,由aidl转化来的Java接口,统一转化成.class文件。
  • 通过dx工具将class文件转化为dex文件。
  • 此时我们得到了经过处理后的资源文件和一个dex文件,当然,还会存在一些其它的资源文件,这个时候,就是将其打包成一个类似apk的文件。但还并不是直接可以安装在Android系统上的APK文件。
  • 通过签名工具对其进行签名。
  • 通过Zipalign进行优化,提升运行速度(原理后文会提及)。
  • 最终,一个可以安装在我们手机上的APK了。

通过上述讲解,我想对于Android项目的整个构建过程,应该有了一个很清晰的框架了,下面将针对其中的具体的细节,和前面挖的一些坑,来进行更细致的分析,下图是一个Android项目构建过程的详细步骤图。

” alt=”详细构建过程” data-src=”https://user-gold-cdn.xitu.io/2018/1/25/1612d1948620cb96?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”734″ data-height=”800″ />

接下来的分析,我们还是按照上述构建过程概述的顺序和流程,进行具体的分析。

第1步:aapt打包资源文件,生成R.java和编译后的资源(二进制文件)

讲到资源文件的处理,我们先来看一下Android中的资源文件有那些呢?Android应用程序资源可以分为两大类,分别是assets和res:    1. assets类资源放在工程根目录的assets子目录下,它里面保存的是一些原始的文件,可以以任何方式来进行组织。这些文件最终会被原装不动地打包在apk文件中。如果我们要在程序中访问这些文件,那么就需要指定文件名来访问。例如,假设在assets目录下有一个名称为filename的文件,那么就可以使用以下代码来访问它:

AssetManager am= getAssets();    
InputStream is = assset.open("filename");  
复制代码

2. res类资源放在工程根目录的res子目录下,它里面保存的文件大多数都会被编译,并且都会被赋予资源ID。这样我们就可以在程序中通过ID来访问res类的资源。res类资源按照不同的用途可以进一步划分为以下10种子类型: layout(布局文件),drawable,xml,value,menu,raw,color,anim,animator,mipmap。 为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android应用程序资源的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI上,从而提高用户体验。由于Android应用程序资源的组织方式可以达到18个维度,因此就要求Android资源管理框架能够快速定位最匹配设备当前配置信息的资源来展现在UI上,否则的话,就会影响用户体验。为了支持Android资源管理框架快速定位最匹配资源,Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:

  • 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中。
  • 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。包含了所有的id值的数据集合。在该文件中,如果某个id对应的是string,那么该文件会直接包含该值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的路径。

为什么要转化为二进制文件?

  • 二进制格式的XML文件占用空间更小。这是由于所有XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
  • 二进制格式的XML文件解析速度更快。这是由于二进制格式的XML元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。 有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。

对于具体的一些操作流程,可以参考本人之前的一篇文章APK打包安装过程或者更偏向于源码层级的老罗的文章。(文后参考文献链接)

第2步:aidl

aidl,全名Android Interface Definition Language,即Android接口定义语言。是我们在编写进程间通信的代码的时候,定义的接口。 输入:aidl后缀的文件。输出:可用于进程通信的C/S端java代码,位于build/generated/source/aidl。

第3步:Java源码编译

我们有了R.java和aidl生成的Java文件,再加上工程的源代码,现在可以使用javac进行正常的java编译生成class文件了。

输入:java source的文件夹(另外还包括了build/generated下的:R.java, aidl生成的java文件,以及BuildConfig.java)。输出:对于gradle编译,可以在build/intermediates/classes里,看到输出的class文件。

第4步:代码混淆(proguard)

源码编译之后,我们可能还会对其进行代码的混淆,混淆的作用是增加反编译的难度,同时也将一些代码的命名进行了缩短,减少代码占用的空间。混淆完成之后,会生成一个混淆前后的映射表,这个是用来在反应我们的应用执行的时候的一些堆栈信息,可以将混淆后的信息转化为我们混淆前实际代码中的内容。 而这个过程使用的工具就是ProGuard,是一个开源的Java代码混淆器(obfuscation)。ADT r8开始它被默认集成到了Android SDK中。 其具备三个主要功能。

  • 压缩 – 移除无效的类、属性、方法等
  • 优化 – 优化bytecode移除没用的结构
  • 混淆 – 把类名、属性名、方法名替换为晦涩难懂的1到2个字母的名字 当然它也只能混淆Java代码,Android工程中Native代码,资源文件(图片、xml),它是无法混淆的。而且对于Java的常量值也是无法混淆的,所以不要使用常量定义平文的密码等重要信息。同时对于混淆,我们可以通过代码制定去混淆那些,不去混淆那些。
-keep public class com.rensanning.example.Test
复制代码

第5步:转化为dex

调用dx.bat将所有的class文件转化为classes.dex文件,dx会将class转换为Dalvik字节码,生成常量池,消除冗余数据等。由于dalvik是一种针对嵌入式设备而特殊设计的java虚拟机,所以dex文件与标准的class文件在结构设计上有着本质的区别,当java程序编译成class后,使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右。class文件结构和dex文件结构比对。

Dex和Class比对

第6步:apkbuilder

打包生成APK文件。旧的apkbuilder脚本已经废弃,现在都已经通过sdklib.jar的ApkBuilder类进行打包了。输入为我们之前生成的包含resources.arcs的.ap_文件,上一步生成的dex文件,以及其他资源如jni、.so文件。 大致步骤为 以包含resources.arcs的.ap_文件为基础,new一个ApkBuilder,设置DebugMode

apkBuilder.addZipFile(f);
apkBuilder.addSourceFolder(f);
apkBuilder.addResourcesFromJar(f);
apkBuilder.addNativeLibraries(nativeFileList);
apkBuilder.sealApk(); // 关闭apk文件
generateDependencyFile(depFile, inputPaths, outputFile.getAbsolutePath());

复制代码

第7步:对APK签名

对APK文件进行签名。Android系统在安装APK的时候,首先会检验APK的签名,如果发现签名文件不存在或者校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名。签名信息中包含有开发者信息,在一定程度上可以防止应用被伪造。对一个APK文件签名之后,APK文件根目录下会增加META-INF目录,该目录下增加三个文件:

  • MANIFEST.MF
  • [CERT].RSA
  • [CERT]

Android系统就是根据这三个文件的内容对APK文件进行签名检验的。签名过程主要利用apksign.jar或者jarsinger.jar两个工具。将根据我们提供的Debug和Release两个版本的Keystore进行相应的签名。

MANIFEST.MF中包含对apk中除了/META-INF文件夹外所有文件的签名值,签名方法是先SHA1()(或其他hash方法)在base64()。存储形式是:Name加[SHA1]-Digest。

[CERT].SF是对MANIFEST.MF文件整体签名以及其中各个条目的签名。一般地,如果是使用工具签名,还多包括一项。就是对MANIFEST.MF头部信息的签名。

[CERT].RSA包含用私钥对[CERT].SF的签名以及包含公钥信息的数字证书。

第8步:zipalign优化

Zipalign是一个Android平台上整理APK文件的工具,它首次被引入是在Android 1.6版本的SDK软件开发工具包中。它能够对打包的Android应用程序进行优化, 以使Android操作系统与应用程序之间的交互作用更有效率,这能够让应用程序和整个系统运行得更快。用Zipalign处理过的应用程序执行时间达到最低限度,当设备运行APK应用程序时占更少的RAM。

  • Zipalign如何进行优化的呢?

调用buildtoolszipalign,对签名后的APK文件进行对齐处理,使APK中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问APK文件时会更快。同时也减少了在设备上运行时的内存消耗。如果对于为何提速不理解,那么可以看下内存对齐的规则以及作用该篇文章,对于内存对齐的好处有比较生动详细的解释。最终这样我们的APK就生成完毕了。

典型的APK中内容

  • AndroidManifest.xml 程序全局配置文件
  • classes.dex Dalvik字节码
  • resources.arsc 资源索引表
  • META-INF该目录下存放的是签名信息
  • res 该目录存放资源文件
  • assets该目录可以存放一些配置或资源文件

总结

至此,对于Andoid项目构建过程的分析已经完成,当然,并没与深入到源码层级的分析,本文的旨在对于构建过程流程上的了解和其中一些优化的原因所在,为后续通过Gradle插件hook构建过程来做一定的操作,做一个铺垫。

参考文章

Android APK 签名原理及方法

改善android性能工具篇【zipalign】

Android应用程序资源的编译和打包过程分析

Android资源管理框架(AssetManager)简要介绍和学习计划

Android代码混淆之ProGuard

作者:Jensen95
链接:https://juejin.im/post/5a69c0ccf265da3e2a0dc9aa
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

小猪的Python学习之旅 —— 3.正则表达式 - 掘金

mikel阅读(945)

来源: 小猪的Python学习之旅 —— 3.正则表达式 – 掘金

引言

上一节学习了一波urllib库和BeautifulSoup的使用,爬取很多小网站 基本是得心应手的了,而一般我们想爬取的数据基本都是字符串,图片url, 或者是段落文字等,掌握字符串的处理显得尤为重要,说到字符串处理, 除了了解字符串相关的处理函数外,还需要 正则表达式 这枚字符串处理神器! 对于正则表达式,很多人开发者貌似都很抗拒,老说学来干嘛,要什么 正则表达式上网一搜就是啦,对此我只能说2333,爬取网页数据的时候, 你搜下给我看,不同的场景匹配字符串的正则表达式都是不一样的,掌握 正则表达式的编写就显得尤为重要了。本节通过一些有趣的例子帮你 快速上手正则表达式,其实真没想象中那么难!


re模块

Python中通过**re模块**使用正则表达式,该模块提供的几个常用方法:

1.匹配

re.match(pattern, string, flags=0)

  • 参数匹配的正则表达式要匹配的字符串标志位(匹配方法)
  • 尝试从字符串的开头进行匹配,匹配成功会返回一个匹配的对象, 类型是:<class '_sre.SRE_Match'> group与groups

re.search(pattern, string, flags=0)

  • 参数:同上
  • 扫描整个字符串,返回第一个匹配的对象,否则返回None

注意match方法和search的最大区别:match如果开头就不和正则表达式匹配, 直接返回None,而search则是匹配整个字符串!!

2.检索与替换

re.findall(pattern, string, flags=0)

  • 参数:同上
  • 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回

re.finditer(pattern, string, flags=0)

  • 参数:同上
  • 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回

re.sub(pattern, repl, string, count=0, flags=0)

  • 参数:repl替换为什么字符串,可以是函数,把匹配到的结果做一些转换; count替换的最大次数,默认0代表替换所有的匹配。
  • 找到所有匹配的子字符串,并替换为新的内容

re.split(pattern, string, maxsplit=0, flags=0)

  • 参数:maxsplit设置分割的数量,默认0代表所有满足匹配的都分割
  • 在正则表达式匹配的地方进行分割,并返回一个列表

3.编译成Pattern对象

对于会多次用到的正则表达式,我们可以调用re的compile()方法编译成 Pattern对象,调用的时候直接Pattern对象.xxx即可,从而提高运行效率。

附:flags(可选标志位)表

多个标志可通过按位OR(|)进行连接,比如:re.I|re.M

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

2.正则规则详解


1.加在正则字符串前的’r’

为了告诉编译器这个string是个raw string(原字符串),不要转义反斜杠! 比如在raw string里\n是两个字符,”和’n’,不是换行!

2.字符

字符 作用
. 匹配任意一个字符(除了\n)
[] 匹配[]中列举的字符
[^...] 匹配不在[]中列举的字符
\d 匹配数字,0到9
\D 匹配非数字
\s 匹配空白,就是空格和tab
\S 匹配非空白
\w 匹配字母数字或下划线字符,a-z,A-Z,0-9,_
\W 匹配非字母数字或下划线字符
- 匹配范围,比如[a-f]

3.数量

字符 作用(前面三个做了优化,速度会更快,尽量优先用前三个)
* 前面的字符出现了0次或无限次,即可有可无
+ 前面的字符出现了1次或无限次,即最少一次
? 前面的字符出现了0次或者1次,要么不出现,要么只出现一次
{m} 前一个字符出现m次
{m,} 前一个字符至少出现m次
{m,n} 前一个字符出现m到n次

4.边界

字符 作用
^ 字符串开头
$ 字符串结尾
\b 单词边界,即单词和空格间的位置,比如’er\b’
可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’
\B 非单词边界,和上面的\b相反
\A 匹配字符串的开始位置
\Z 匹配字符串的结束位置

5.分组

用**()表示的就是要提取的分组**,一般用于提取子串, 比如:^(\d{3})-(\d{3,8})$:从匹配的字符串中提取出区号和本地号码

字符 作用
匹配左右任意一个表达式
(re) 匹配括号内的表达式,也表示一个组
(?:re) 同上,但是不表示一个组
(?P<name>) 分组起别名,group可以根据别名取出,比如(?P<first>\d)
match后的结果调m.group(‘first’)可以拿到第一个分组中匹配的记过
(?=re) 前向肯定断言,如果当前包含的正则表达式在当前位置成功匹配,
则代表成功,否则失败。一旦该部分正则表达式被匹配引擎尝试过,
就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。
(?!re) 前向否定断言,作用与上面的相反
(?<=re) 后向肯定断言,作用和(?=re)相同,只是方向相反
(?<!re) 后向否定断言,作用于(?!re)相同,只是方向想法

附:group()方法与其他方法详解

不引入括号,增个表达式作为一个组,是group(0)

不引入**()的话,代表整个表达式作为一个组,group = group(0) 如果引入()**的话,会把表达式分为多个分组,比如下面的例子:

输出结果

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe1baa50f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”95″ data-height=”62″ />

除了group方法外还有三个常用的方法:

  • groups(): 从group(1)开始往后的所有的值,返回一个元组
  • start():返回匹配的开始位置
  • end():返回匹配的结束位置
  • span():返回一个元组组,表示匹配位置(开始,结束)

贪婪与非贪婪

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。 比如:ret = re.match(r'^(\d+)(0*)$','12345000').groups()ß 我们的原意是想得到**(‘12345′,’000’)这样的结果,但是输出 ret我们看到的却是:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe20739ed?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”120″ data-height=”21″ />

,由于贪婪,直接把后面的 0全给匹配了,结果0*只能匹配空字符串了,如果想尽可能少的 匹配,可以在\d+后加上一个?问号*采用非贪婪匹配,改成: r’^(\d+?)(0)$’,输出结果就变成了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe3edaedf?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”118″ data-height=”20″ />

3.正则练习

例子1:简单验证手机号码格式

流程分析:

  • 1.开头可能是带0(长途),86(天朝国际区号),17951(国际电话)中的一个或者一个也没有:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e583fe1a2a757?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”357″ data-height=”24″ />
  • 2.接着1xx,有13x,14x,15x,17x,18x,然后这个x也是取值范围也是不一样的: 13x:0123456789 14x:579 15x:012356789 17x:01678 18x:0123456789 然后修改下正则表达式,可以随便输个字符串验证下:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584013db9939?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”534″ data-height=”29″ />
  • 3.最后就是剩下部分的8个数字了,很简单:[0-9]{8} 加上:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584018e1df00?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”588″ data-height=”26″ />
^(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}$
复制代码

例子2:验证身份证

流程分析:

身份证号码分为一代和二代,一代由15位号码组成,而二代则是由18个号码组成: 十五位:xxxxxx    yy mm dd   pp s 十八位:xxxxxx yyyy mm dd ppp s

为了方便了解,把这两种情况分开,先是十八位的:

  • 1.前6位地址编码(省市县),第一位从1开始,其他五位0-9
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58401791c7be?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”306″ data-height=”23″ />
  • 2.第7到10(接着的两位或者四位有):,范围是1800到2099:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584016d503e5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”300″ data-height=”23″ />
  • 3.第11到12,1-9月需要补0,10,11,12
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840146cfba4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”344″ data-height=”24″ />
  • 4.第13到14,首位可能是012,第二位为0-9,还要补上10,20,30,31
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58401af0fef3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”513″ data-height=”25″ />
  • 5.第15到17顺序码,这里就是三个数字,对同年、同月、同日出生的人 编定的顺序号,奇数分给男的,偶数分给女的:
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403a7b7578?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”550″ data-height=”27″ />
  • 6.第18位校验码,0到9或者x和X
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403b7fb295?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”673″ data-height=”22″ />

能推算出18的,那么推算出15的也不难了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403db90de6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”525″ data-height=”19″ />

最后用|组合下:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58403dc1e7e6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”645″ data-height=”37″ />
^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{3}[0-9Xx]|[1-9]\d{5}\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{2}[0-9Xx]$
复制代码

另外,这里的正则匹配出的身份证不一定是合法的,判断身份是否 合法还需要通过程序进行校验,校验最后的校验码是否正确

扩展阅读:身份证的最后一位是怎么算出来的? 更多可见:第二代身份证号码编排规则

首先有个加权因子的表:(没弄懂怎么算出来的..) [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]

然后位和值想乘,结果相加,最后除11求余,比如我随便网上找的 一串身份证:411381199312150167,我们来验证下最后的7是对的吗?

sum = 47 + 19 + 110 + 35 +88 + 1 4 … + 6 * 2 = 282 sum % 11 = 7,所以这个是一个合法的身份证号。


例子3:验证ip是否正确

流程分析

ip由4段组成,xxx.xxx.xxx.xxx,访问从0到255,因为要考虑上中间的. 所以我们把第一段和后面三段分开,然后分析下ip的结构,可能是这几种情况: 一位数[1-9] 两位数[1-9][0-9] 三位数(100-199):1[0-9][0-9] 三位数(200-249):2[0-4][0-9] 三位数(250-255): 25[0-5] 理清了第一段的正则怎么写就一清二楚了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584040c72108?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”474″ data-height=”25″ />

然后后面三段,需要在前面加上一个一个**.**,然后这玩意是元字符, 需要加上一个反斜杠\,让他失去作用,后面三段的正则就是:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840455566b2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”445″ data-height=”19″ />

把两段拼接下即可得出完整的验证ip的正则表达式了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58405fc830ff?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”520″ data-height=”37″ />
^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$
复制代码

例子4:匹配各种乱七八糟的

  • 匹配中文[\u4e00-\u9fa5]
  • 匹配双字节字符[^\x00-\xff]
  • 匹配数字并输出示例
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584060a28da0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”428″ data-height=”48″ />

    输出结果

    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584060c2964e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”46″ data-height=”19″ />
  • 匹配开头结尾示例
    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584065d7cfb2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”477″ data-height=”96″ />

    输出结果

    ” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e58406ebc611c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”94″ data-height=”67″ />

4.正则实战

实战:抓一波城市编码列表

本来想着就抓抓中国气象局的天气就好了,然后呢,比如深圳天气的网页是: www.weather.com.cn/weather1dn/… 然后这个101280601是城市编码,然后网上搜了下城市编码列表,发现要么 很多是错的,要么就缺失很多,或者链接失效,想想自己想办法写一个采集 的,先搞一份城市编码的列表,不过我去哪里找数据来源呢?中国气象局 肯定是会有的,只是应该不会直接全部暴露出来,想想能不能通过一些间接 操作来实现。对着中国气象局的网站瞎点,结果不负有心人,我在这里: www.weather.com.cn/forecast/ 发现了这个:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584082bc4aca?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”251″ data-height=”166″ />

点进去后:www.weather.com.cn/textFC/hb.s… 然后,我觉得这可能是入手点:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840824188ba?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”1025″ data-height=”369″ />

F12打开开发者工具,不出所料:

这里有个超链接,难不成是北京所有的地区的列表,点击下进去看看: www.weather.com.cn/textFC/beij…

卧槽,果然是北京所有的地区,然后每个地区的名字貌似都有一个超链接, F12看下指向哪里?

到这里就豁(huo)然开朗了,我们来捋一捋实现的流程:

  • 1.先拿到第一层的城市列表链接用列表存起来
  • 2.接着遍历列表去访问不同的城市列表链接,截取不同城市的城市名,城市编码存起来

流程看上去很简单,接着来实操一波。

先是拿城市列表url

这个很容易拿,就直接贴代码了:

拿到需要的城市列表url:

接着随便点开一个,比如beijing.shtml,页面结构是这样的: 想要的内容是这里的超链接:

F12看下页面结构,层次有点多,不过没关系,这样更能够锻炼我们

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840b9222896?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”431″ data-height=”515″ />

入手点一般都是离我们想要数据最近地方下手,我看上了:conMidtab3 全局搜了一下,也就八个:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840bdedec87?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”516″ data-height=”49″ />

第一个直接就可以排除了:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840bc5a0412?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”159″ data-height=”202″ />

接着其余的七个,然后发现都他么是一样的…,那就直接抓到第一个吧:

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e5840d2e37a95?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”552″ data-height=”24″ />

输出下:

是我们想要的内容,接着里面的tr是我们需要内容,找一波:

输出下:

继续细扒,我们要的只是a这个东西:

输出下:

重复出现了一堆详情,很明显是我们不想要的,我们可以在循环的时候 执行一波判断,重复的不加入到列表中:

然后我们想拿到城市编码和城市名称这两个东西:

城市的话还好,直接调用tag对象的string直接就能拿到, 而城市编码的话,按照以前的套路,我们需要先[‘href’]拿到 再做字符串裁剪,挺繁琐的,既然本节学习了正则,为何不用 正则来一步到位,不难写出这样的正则:

匹配拿到**group(1)**就是我们要的城市编码:

输出内容:

卧槽,就是我们想要的结果,美滋滋,接着把之前拿到所有 的城市列表都跑一波,存字典里返回,最后赛到一个大字典 里,然后写入到文件中,完成。


========= BUG的分割线 =========

最后把数据打印出来发现只有428条数据,后面才发现conMidtab3那里处理有些 问题,漏掉了一些,限于篇幅,就不重新解释了,直接贴上修正完后的代码把…

import urllib.request
from urllib import error
from bs4 import BeautifulSoup
import os.path
import re
import operator

# 通过中国气象局抓取到所有的城市编码

# 中国气象网基地址
weather_base_url = "http://www.weather.com.cn"
# 华北天气预报url
weather_hb_url = "http://www.weather.com.cn/textFC/hb.shtml#"


# 获得城市列表链接
def get_city_list_url():
    city_list_url = []
    weather_hb_resp = urllib.request.urlopen(weather_hb_url)
    weather_hb_html = weather_hb_resp.read().decode('utf-8')
    weather_hb_soup = BeautifulSoup(weather_hb_html, 'html.parser')
    weather_box = weather_hb_soup.find(attrs={'class': 'lqcontentBoxheader'})
    weather_a_list = weather_box.findAll('a')
    for i in weather_a_list:
        city_list_url.append(weather_base_url + i['href'])
    return city_list_url


# 根据传入的城市列表url获取对应城市编码
def get_city_code(city_list_url):
    city_code_dict = {}  # 创建一个空字典
    city_pattern = re.compile(r'^<a.*?weather/(.*?).s.*</a>$')  # 获取城市编码的正则

    weather_hb_resp = urllib.request.urlopen(city_list_url)
    weather_hb_html = weather_hb_resp.read().decode('utf-8')
    weather_hb_soup = BeautifulSoup(weather_hb_html, 'html.parser')
    # 需要过滤一波无效的
    div_conMidtab = weather_hb_soup.find_all(attrs={'class': 'conMidtab', 'style': ''})

    for mid in div_conMidtab:
        tab3 = mid.find_all(attrs={'class': 'conMidtab3'})
        for tab in tab3:
            trs = tab.findAll('tr')
            for tr in trs:
                a_list = tr.findAll('a')
                for a in a_list:
                    if a.get_text() != "详情":
                        # 正则拿到城市编码
                        city_code = city_pattern.match(str(a)).group(1)
                        city_name = a.string
                        city_code_dict[city_code] = city_name
        return city_code_dict


# 写入文件中
def write_to_file(city_code_list):
    try:
        with open('city_code.txt', "w+") as f:
            for city in city_code_list:
                f.write(city[0] + ":" + city[1] + "\n")
    except OSError as reason:
        print(str(reason))
    else:
        print("文件写入完毕!")


if __name__ == '__main__':
    city_result = {}  # 创建一个空字典,用来存所有的字典
    city_list = get_city_list_url()

    # get_city_code("http://www.weather.com.cn/textFC/guangdong.shtml")

    for i in city_list:
        print("开始查询:" + i)
        city_result.update(get_city_code(i))

    # 根据编码从升序排列一波
    sort_list = sorted(city_result.items(), key=operator.itemgetter(0))

    # 保存到文件中
    write_to_file(sort_list)

复制代码

运行结果

” data-src=”https://user-gold-cdn.xitu.io/2018/1/11/160e584121f34f1c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1″ data-width=”405″ data-height=”116″ />

5.小结和几个API

本节对Python中了正则表达式进行了一波学习,练手,发现和Java里的正则 多了一些规则,正则在字符串匹配的时候是挺爽的,但是正则并不是全能 的,比如闰年二月份有多少天的那个问题,还需要程序另外去做判断! 正则还需要多练手啊,限于篇幅,就没有另外去抓各种天气信息了, 而且不是刚需,顺道提供两个免费可用三个和能拿到天气数据的API吧:

还有个中国气象局提供的根据经纬度获取天气的: e.weather.com.cn/d/town/inde…

人生苦短,我用Python,爬虫真好玩!期待下节爬虫框架scrapy学习~


来啊,Py交易啊

想加群一起学习Py的可以加下,智障机器人小Pig,验证信息里包含: PythonpythonpyPy加群交易屁眼 中的一个关键词即可通过;

验证通过后回复 加群 即可获得加群链接(不要把机器人玩坏了!!!)~~~ 欢迎各种像我一样的Py初学者,Py大神加入,一起愉快地交流学♂习,van♂转py。

作者:coder-pig
链接:https://juejin.im/post/5a576d976fb9a01c9b65dee6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Android无障碍服务 x itchat 打造微信半自动机器人_移动开发_Coder-Pig的猪栏-CSDN博客

mikel阅读(1168)

来源: Android无障碍服务 x itchat 打造微信半自动机器人_移动开发_Coder-Pig的猪栏-CSDN博客

em…是我,那个『敲最屌的码,输最多的钱』的傻雕开发仔,故事的最后:没有暴富,没有嫩模,也没有穴深妹…

再次奉劝各位一句:远离投机倒把,保持身心健康!(当然,后面如果学到机器学习的东西,可能会有续集 ~(╯▽╰ )真香~~)

好的,碎碎念的那么多,说回本节,写这一篇原因是,我的Py交易群里,童鞋问的最多的问题都是和机器人有关,基本都是下面这类问题:

1.群主,你的机器人怎么实现的?
2.群主,你的apk怎么运行了没反应?
3.群主,支持自动添加手机号为好友吗?
…等等
一开始我还是比较热衷帮忙解决问题的,但是耐心这种东西呢,是最容易被消磨殆尽的。而且学习Python之后,我变得越来越懒,一些繁琐重复的操作,我都会想办法自动化…当然,如果你是给我打钱或者是妹子,我也是很乐意的。

其实Android无障碍服务和itchat都是一些老生常谈的东西了,我也写过好几篇文章了:

妙用AccessibilityService黑科技实现微信自动加好友拉人进群聊
自动抢红包,点赞朋友圈,AccessibilityService解放你的双手
利用itchat搭建微信机器人详解(附三个实用示例)
小猪的Python学习之旅 —— 18.Python微信转发小宇宙早报
小猪的Python学习之旅 —— 19.Python微信自动好友验证,自动回复,发送群聊链接
东西就那些,套路也是那些,大部分时候,你只需要一点**『灵性』**!

就好像七巧板一样,只有七块,但是却能拼接出许多图形。要有灵性!本文先说下无障碍服务和itchat的一些核心要点,然后以读者编写的机器人脚本为例,教你实现一波,希望可以给你带来一些提示,然后去扩展自己想要的功能。

开源的微信个人号接口库——itchat
itchat库基于微信网页版,出了好几年的了,微信正在慢慢收窄网页端的功能(因为滥用的微信机器人),很多以前能用的接口慢慢都不能用了,比如:拉人进群,添加好友等,反正网页端没有的,现在都不能用。丢几个链接:

微信网页端登录:https://wx2.qq.com/
itchat的仓库地址:https://github.com/littlecodersh/ItChat
itchat官方文档:https://itchat.readthedocs.io/zh/latest/
itchat现在能做的:监听加好友的信息,监听聊天信息(包括群聊),发送信息。
基本上常用的而且可用的就这三个。当然如果你愿意掏钱的话,你不需要折腾那么多,网上有其他付费的途径:微友助手,王二狗机器人,还有xposed插件等。基于其他协议且可用的免费开源库目前还没见过,有知道的欢迎在评论区告知下!

使用要点提炼

如果可以,我希望你,尽可能的学会使用『正则表达式处理字符串』(基本功),丢个以前写过的文章:
小猪的Python学习之旅 —— 3.正则表达式,正则对于字符串匹配,提取非常实用!

1.监听并通过加好友的请求
@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
# 自动将新好友的消息录入,不需要重载通讯录
itchat.add_friend(**msg[‘Text’])
1
2
3
4
通过上面的add_friend函数,就可以完成添加好友的操作了。但是,有时我们可能需要做一些过滤,
不然乱七八糟的人都能加你了,是吧,比如对添加内容进行过滤,包含某些字眼才通过验证,或者
获取加你的人的相关信息,比如姓名,验证信息,个性签名,性别等。我们直接把上面的msg打印出来,
内容如下:

{‘MsgId’: ‘6930655840618917667’, ‘FromUserName’: ‘fmessage’, ‘ToUserName’: ‘@64fc6691440834f2dfba5489d652e5dbae06da4d57d550757403094424f7ec9c’, ‘MsgType’: 37, ‘Content’: ‘<msg fromusername=”wxid_gvr9a3le939h22″ encryptusername=”v1_6a48a18b8d6164b69dfdd3949311cd4bc58d96dfe00636d5e9814ff36b8d107164e768ccd9b2fb911ea9b87ccac42978@stranger” fromnickname=”Robot Pig” content=”我是Robot Pig” shortpy=”ROBOTPIG” imagestatus=”3″ scene=”30″ country=”AD” province=”” city=”” sign=”(´v`o)♡” percard=”1″ sex=”2″ alias=”” weibo=”” albumflag=”0″ albumstyle=”0″ albumbgimgid=”” snsflag=”1″ snsbgimgid=”http://mmsns.qpic.cn/mmsns/icDH6NcE3zNVBleeQZUzzlnhWk16tIfPKyvsmqWIpUwAxHkvricuNCL2RvGPjS3pVq7miaZQoju8TU/0″ snsbgobjectid=”12478275406675193980″ mhash=”197adbfd7de1668f30895d20dfb09b67″ mfullhash=”197adbfd7de1668f30895d20dfb09b67″ bigheadimgurl=”http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/0″ smallheadimgurl=”http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/96″ ticket=”v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger” opcode=”2″ googlecontact=”” qrticket=”” chatroomusername=”” sourceusername=”” sourcenickname=””><brandlist count=”0″ ver=”688441058″></brandlist></msg>’, ‘Status’: 3, ‘ImgStatus’: 1, ‘CreateTime’: 1541558757, ‘VoiceLength’: 0, ‘PlayLength’: 0, ‘FileName’: ”, ‘FileSize’: ”, ‘MediaId’: ”, ‘Url’: ”, ‘AppMsgType’: 0, ‘StatusNotifyCode’: 0, ‘StatusNotifyUserName’: ”, ‘RecommendInfo’: {‘UserName’: ‘@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3’, ‘NickName’: ‘Robot Pig’, ‘QQNum’: 0, ‘Province’: ”, ‘City’: ”, ‘Content’: ‘我是Robot Pig’, ‘Signature’: ‘(´v`o)♡’, ‘Alias’: ”, ‘Scene’: 30, ‘VerifyFlag’: 0, ‘AttrStatus’: 50467109, ‘Sex’: 2, ‘Ticket’: ‘v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger’, ‘OpCode’: 2}, ‘ForwardFlag’: 0, ‘AppInfo’: {‘AppID’: ”, ‘Type’: 0}, ‘HasProductId’: 0, ‘Ticket’: ”, ‘ImgHeight’: 0, ‘ImgWidth’: 0, ‘SubMsgType’: 0, ‘NewMsgId’: 6930655840618917667, ‘OriContent’: ”, ‘EncryFileName’: ”, ‘User’: <User: {‘UserName’: ‘@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3’, ‘MemberList’: <ContactList: []>}>, ‘Type’: ‘Friends’, ‘Text’: {‘status’: 3, ‘userName’: ‘@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3’, ‘verifyContent’: ”, ‘autoUpdate’: {‘UserName’: ‘@156957cc475e1113c0a8fa340eae41ac1d92a12a523302a8eb06208616d656b3’, ‘NickName’: ‘Robot Pig’, ‘QQNum’: 0, ‘Province’: ”, ‘City’: ”, ‘Content’: ‘我是Robot Pig’, ‘Signature’: ‘(´v`o)♡’, ‘Alias’: ”, ‘Scene’: 30, ‘VerifyFlag’: 0, ‘AttrStatus’: 50467109, ‘Sex’: 2, ‘Ticket’: ‘v2_4ed4b6a5ac5ccf04c68c7bd64e4b543ba5babde23ce2985d4317bfc4bb62dcdfd78bb701551e1410fe64836f9bd147199de3e4493a031ea2daf52187816d9207@stranger’, ‘OpCode’: 2}}}
1
看上去和Json有点类似是吧,但是不是Json,你用Json格式化工具试试就知道了,而是一种类似于字典的东东
(跟下源码就知道了,msg的类:itchat.storage.messagequeue.Message)一大串有点乱,用PyCharm新建一个json文件,
复制粘贴格式化下:

因为类似于字典,你可以通过键的形式获取所需的值,比如打印下msg[‘Content’]:

<msg fromusername=”wxid_gvr9a3le939h22″ encryptusername=”v1_6a48a18b8d6164b69dfdd3949311cd4bc58d96dfe00636d5e9814ff36b8d107164e768ccd9b2fb911ea9b87ccac42978@stranger” fromnickname=”Robot Pig” content=”我是Robot Pig” shortpy=”ROBOTPIG” imagestatus=”3″ scene=”30″ country=”AD” province=”” city=”” sign=”(´v`o)♡” percard=”1″ sex=”2″ alias=”” weibo=”” albumflag=”0″ albumstyle=”0″ albumbgimgid=”” snsflag=”1″ snsbgimgid=”http://mmsns.qpic.cn/mmsns/icDH6NcE3zNVBleeQZUzzlnhWk16tIfPKyvsmqWIpUwAxHkvricuNCL2RvGPjS3pVq7miaZQoju8TU/0″ snsbgobjectid=”12478275406675193980″ mhash=”197adbfd7de1668f30895d20dfb09b67″ mfullhash=”197adbfd7de1668f30895d20dfb09b67″ bigheadimgurl=”http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/0″ smallheadimgurl=”http://wx.qlogo.cn/mmhead/ver_1/jYcpsrqBVaxDuMbFgPZwH5F3mvcgKMNVrQXlmmbGWx5p9iaFrYALPyia1b7Izn4YCUYDRXz1jnfKw0eUXvEeEH5NouILblzLDjog40fqOjIjc/96″ ticket=”v2_4ed4b6a5ac5ccf04c68c7bd64e4b543b5c95f344b73e23e05f71c527683d8693969fcef8a893316431a660ac382032b022ec8af4d8ae5372e680931064da1ce3@stranger” opcode=”2″ googlecontact=”” qrticket=”” chatroomusername=”” sourceusername=”” sourcenickname=””><brandlist count=”0″ ver=”688441086″></brandlist></msg>
1
同样新建一个xml文件,复制粘贴格式化下:

里面有我们想要的信息,接着我们用正则来提取这些想要的数据:

msg_pattern = re.compile(‘<msg fromusername=”(.*?)”.*?fromnickname=”(.*?)” content=”(.*?)”.*?sign=”(.*?)”.*?sex=”(\d)”.*?bigheadimgurl=”(.*?)”‘,re.S)
1
接着修改一波代码:

@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
result = msg_pattern.search(msg[‘Content’])
if result is not None:
print(‘添加人微信id:’, result.group(1))
print(‘添加人用户名’, result.group(2))
print(‘验证内容’, result.group(3))
print(‘添加人个性签名’, result.group(4))
print(‘添加人性别’, result.group(5))
print(‘添加人头像大图’, result.group(6))
1
2
3
4
5
6
7
8
9
10
添加试试,打印结果如下:

行吧,什么加你人的信息都拿到了,你想干嘛就干嘛!

2.监听聊天信息
这个也很简单,你可以监听多种类型的信息,如下表所示:

信息类型 解释
itchat.content.TEXT 文本内容
itchat.content.MAP 位置文本
itchat.content.Card 名片
itchat.content.Note 通知文本
itchat.content.Sharing 分享名称
itchat.content.RECORDING 录音
itchat.PICTURE 图片/表情
itchat.content.VOICE 录音
itchat.content.ATTACHMENT 附件
itchat.content.VIDEO 短视频
itchat.content.FRIENDS 好友邀请
itchat.content.SYSTEM 系统信息
可注册多个信息监听,后注册的信息优先级高于先注册信息,带参数信息高于不带参数信息。
核心代码示例如下:

@itchat.msg_register(itchat.content.TEXT)
def reply_msg(msg):
if msg[‘Content’] == u’你好’:
itchat.send_msg(msg[‘User’][‘NickName’] + “你好啊!”, msg[‘FromUserName’])
1
2
3
4
和上面的监听并通过加好友的请求玩法一样,根据键拿值,或者正则提取需要的数据。这里提供几个常用的键:

msg[‘Content’] # 获取用户发送的内容,后面的匹配值建议加上u,代表Unicode编码
msg[‘User’][‘NickName’] # 发送信息的用户名
msg[‘FromUserName’] # 接收信息的用户名,这个不是直接的用户昵称或微信号!!!
1
2
3
3.发送信息
itchat支持下述几种类型的信息(不支持语音):

函数名 作用
send_msg() 发送文字信息
send_file() 发送文件
send_video() 发送视频
send_image() 发送图片
核心代码示例如下:

user_info = itchat.search_friends(name=’一朵死去的花’)
if len(user_info) > 0:
# 拿到用户名
user_name = user_info[0][‘UserName’]
# 发送文字信息
itchat.send_msg(‘培杰你好啊!’, user_name)
# 发送图片
time.sleep(10)
itchat.send_image(‘cat.jpg’, user_name)
# 发送文件
time.sleep(10)
itchat.send_file(’19_2.py’, user_name)
# 发送视频
time.sleep(10)
itchat.send_video(‘sport.mp4’, user_name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
建议加入延时,避免信息发送过于频繁,导致账号被封!

4.获得群聊成员列表
核心代码示例如下:

@itchat.msg_register(itchat.content.TEXT, isGroupChat=True)
def reply_msg(msg):
print(“收到一条群信息:”, msg[‘ActualNickName’], msg[‘Content’])
1
2
3
玩法和之前的一样,另外,还可以调用**msg.isAt**判断是否有人@自己。

5.监控加群信息
核心代码示例如下:

@itchat.msg_register([NOTE], isGroupChat=True)
def revoke_msg(msg):
if ‘邀请’ in str(msg[‘Text’]):
# 进行相关操作
1
2
3
4
就是判断提示信息里是否有加群字眼,好吧,常用的大概就这些,其他的自行查阅文档。

Android无障碍服务——AccessibilityService
其实就是一个自动点点点的东西,没什么技术含量,真的!!!在开始讲解AccessibilityService之前,先要明确一点:

什么是自动化?

下面是我个人的理解:

把本该人做的,重复性高,单调,机械化的操作,交给程序去完成。

举个例子,小猪每天都要用微信拉人进群,所需的操作步骤如下所示:

是的,你拉一个人,需要20多秒,每次拉人的操作都是机械重复的。如果每天有30个人
进群,你需要花费:600s,10分钟我都够开一把王者荣耀的了,别人在上分,我还在拉人???

多捞哦,用AccessibilityService写个自动点点点的工具就可以把我从中解放出来。

1.自定义Service继承AccessibilityService
自定义一个AccessibilityService类,重写两个主要方法:onInterrupt( ):辅助功能中断的回调,基本不用理,
核心还是: onAccessibilityEvent(AccessibilityEvent event) 。

当界面发生改变,比如顶部Notification,界面更新,内容变化等,就会触发**onAccessibilityEvent方法。
点开AccessibilityEvent**类可以看到一堆的事件类型:

上面一大堆,其实并没有什么用,我一般是习惯直接把event.toString()给打印出来,然后自行判断:

代码示例如下:

这里做的事情就是,当无障碍相关的Event触发时,去判断Event类型以及触发事件的类名,再去执行相关操作:点击,滚动,填充文本等。

2.获取结点的几个方法
可以通过resource-id,text来定位到结点,如果可以,建议使用后者,因为一般APP更新后,这个id都会发生变化,(所以微信更新后都需要做适配,就是更新这个id)

1)通过UI Automator来查看布局层次

旧版的Android Studio,Ctrl + alt + A,输入 monitor 可以找到,新版的
Android Studio是找不到的,你需要来到**android-sdk/tools**目录下:

连接手机后,点击顶部的:

接着可以看到当前页面的层次结构图:

有一点务必注意: resource-id不一定是唯一!!!

getRootInActiveWindow( ):获取当前整个活动窗口的根节点,返回的是一个AccessibilityNodeInfo类,代表View的状态信息, 提供了下述几个非常实用的方法:

findAccessibilityNodeInfosByViewId:通过视图id查找节点元素。
findAccessibilityNodeInfosByText:通过字符串查找节点元素。
getParent:获取父节点。
getChild:获取子节点。
另外,找结点要注意判空,找不到对应结点直接调用其他方法是会空指针异常的!!!
找到结点然后就是一些动作了,常用的点击,长按,滚动和输入文字。代码示例如下:

/* 点击 */
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)

/* 长按 */
node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)

/* 滚动 */
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) //向上滚动
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) //向下滚动

/* 输入文字 */
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,”xxx”)
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)

/* 通过粘贴板输入文字 */
public static void sendTextForEditText(Context context, AccessibilityNodeInfo editNode, String text) {
if (editNode != null) {
ClipboardManager clipboard = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(“text”, text);
clipboard.setPrimaryClip(clip);
//获得焦点
editNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
//粘贴内容
edittext.performAction(AccessibilityNodeInfo.ACTION_PASTE);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
除此之外,还有AccessibilityService本身特有的方法,如模拟回退键,Home键等。

performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) //回退
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) //Home键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) //点击notification
1
2
3
大概的玩法就这些,除了通过UI Automator获得id外,还可以通过其他几种方式来获取id。

2)开发者助手

如果你手机root了的话,可以安装一个**『开发者助手』**,点击当前界面分析,点击想查看的节点即可,如图所示。

3)通过adb命令

依次键入:

adb shell uiautomator dump /mnt/sdcard/window_dump.xml
adb pull /mnt/sdcard/window_dump.xml
1
2
运行结果如图所示:

接着可以把这个xml文件丢到as里,格式化下,折叠下一层层拆开,然后去找对应的结点:

这种方法是不怎么推荐的,除非这个结点很明显,比如文本啊,之类的,层级很多的时候,可能会找死你…

3.AccessibilityService注意事项
在使用AccessibilityService服务时,有几点要注意:

首先需要手动开启无障碍服务!!!程序转了跑,没反应,多半是因为没有开启无障碍服务!
无障碍服务一般在:辅助功能->无障碍,(不同的手机可能不同)找到自己的点点点APK,开启,如图所示:

另外,有一点要注意,有时可能因为异常导致程序意外终止了,你需要到无障碍中关掉对应的服务,然后重启。
还有一点最重要的无障碍服务的适用范围:

原生的Android APP!!! 是的原生!!!现在很多应用都是混合应用,对于H5的页面,无障碍服务是无能为力的!因为此时的控件点击事件不是通过onClick来产生的,而是直接判断TouchEvent。而Android的无障碍服务没有提供发送down,move,up事件的api。而替代方案只能使用root后的手机,向系统发送全局点击命令。

一般是拿到结点,然后获得结点所在的区域,然后执行相关的命令,比如点击,常用代码示例如下:

/* 执行Shell命令 */
public static void execShellCmd(String cmd) {
try {
// 申请获取root权限,这一步很重要,不然会没有作用
Process process = Runtime.getRuntime().exec(“su”);
// 获取输出流
OutputStream outputStream = process.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeBytes(cmd);
dataOutputStream.flush();
dataOutputStream.close();
outputStream.close();
} catch (Throwable t) {
t.printStackTrace();
}
}

/* 点击某个结点 */
public static void perforGlobalClick(AccessibilityNodeInfo info) {
Rect rect = new Rect();
info.getBoundsInScreen(rect);
perforGlobalClick(rect.centerX(), rect.centerY());
}

/* 点击某个坐标点 */
public static void perforGlobalClick(int x, int y) {
execShellCmd(“input tap ” + x + ” ” + y);
}

/* 全局滑动 */
public static void perforGlobalSwipe(int x0, int y0, int x1, int y1) {
execShellCmd(“input swipe ” + x0 + ” ” + y0 + ” ” + x1 + ” ” + y1);
}

/* 全局返回 */
public static void perforGlobalHome(long delay) {
execShellCmd(“input keyevent ” + KeyEvent.KEYCODE_HOME);
}

/* 全局Home键 */
public static void perforGlobalHome(long delay) {
execShellCmd(“input keyevent ” + KeyEvent.KEYCODE_BACK);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
行吧,关于AccessibilityService的玩法大概就这些了,接着教大家撸一个我的半自动微信机器人。

3.动手撸一个自己的微信机器人
先罗列下我的需求:

1.自动通过别人加好友的验证,发送欢迎图和欢迎信息;
2.监听用户发送的信息,响应对应的信息
菜单:返回菜单回复词
1:加入「Python学习交流群」
2:加入「Android学习交流群」
3:加入「闲聊扯淡群」
4:加入「抠腚男孩的妙妙屋」
5:关注公众号「抠腚男孩」
6:小猪的「个人博客」
7:小猪的「Github」
8:给小猪「打赏」
9:小猪的「微信」(不闲聊!)
其他,默认回复黑人问号图。
好的,问题来了,itchat现在不支持拉人进群,怎么办?一个折中的方法,就是利用Android AccessibilityService,
完成自动拉人进群,我们可以采集发送进群的人的用户名,然后定时(比如两小时)发送一次到文件传输助手,然后
复制粘贴下用户名到我们编写的无障碍脚本里,完成自动拉人的操作,因为还要人去复制粘贴,所以只能算半自动!

接着第二个问题,数据的传输格式,如果只是一个群的话,最简单的,用户名拼接,回车作为分隔:

小A
小B
小C
1
2
3
这里的话,因为我有多个群,读者可以选择加入自己想加入的群,回车换行或者添加分隔符的方式显得有点low,
而且不方便扩展,这里,我决定使用Json字符串,存储加每个群的人的用户名,最后拼接成一个Json,示例如下:

{
“Python”: [],
“Python2”: [
“朱伟”,
“程命沆(hàng)”,
“双枪老汉”
]
“Speak”: [
“程命沆(hàng)”
],
“Android”: [
“朱伟”,
“程命沆(hàng)”
],
“Guy”: []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS:这里有Python2的原因是,一群满了,所以要做下判断,如果人数达到495的,把加群的人添加到二群。
所以还需要监控群群人员变化的信息,有新的人进群,获取一下一群当前的,加入到二群中。还有,要对
加群的人做下判断,如果已经在群里了,就不要添加到列表中。逻辑都弄清楚了,那就直接上代码吧:

# -*- coding:utf-8 -*-
# 微信小宇宙助手
import datetime
import re
import time
import random
import json
import itchat
from itchat.content import *
from apscheduler.schedulers.blocking import BlockingScheduler

# 群聊人员列表
member_python_list = []
member_python_list_2 = []
member_android_list = []
member_speak_list = []

# 加群人员的列表
group_python_list = [] # Python
group_python_list_2 = [] # Python 2群
group_android_list = [] # Android
group_speak_list = [] # 闲聊

# 获取群聊人员的列表的正则
nickname_compile = re.compile(r”\<ChatroomMember:.*?’NickName’: ‘(.*?)'”, re.S)

# 获取群聊名称的正则
group_name_compile = re.compile(“‘NickName’: ‘(.{1,40})’, ‘HeadImgUrl’:”, re.S)

# 添加好友通过欢迎词
welcome_words = ‘(˶ᵔᵕᵔ˶)嘤嘤嘤,???\n我是智障机器人小Pig,发送关键字:「菜单」 \n 查看更多小Pig的更多功能!’

# 菜单回复词
menu_answer = ‘(˶ᵔᵕᵔ˶)锵锵锵~???,\n’ \
‘可用关键词如下(输入对应数字,比如1):\n’ \
‘ ? 1.加入「Python学习交流群」\n’ \
‘ ? 2.加入「Android学习交流群」\n’ \
‘ ? 3.加入「闲聊扯淡群」\n’ \
‘ ? 4.关注公众号「抠腚男孩」\n’ \
‘ ? 5.小猪的「个人博客」\n’ \
‘ ? 6.小猪的「GitHub」\n’ \
‘ ? 7.给小猪「打赏」\n’ \
‘ ? 8.小猪「微信」(不闲聊哦~)\n’ \
‘注:请不要回复过于频繁,智障机器人不会聊天哦!?’

# 加群统一回复词
add_group_answer = ‘???FBI Warning!???\n(`・ω・´)ゞ非常抱歉的通知您:\n\n微信粑粑把拉人接口禁掉了,你的加群请求已收到,小猪童鞋会尽快把你拉到群中。\n\nヾノ≧∀≦)o 麻烦耐心等候哦!’

# 重复加群回复词
add_repeat_answer = ‘<(`^´)>哼,敲生气,你都在群里了,加什么群鸭!???’

# 捐献回复词
donate_answer = ‘(˶ᵔᵕᵔ˶)您的打赏,会让小猪更有动力肝♂出更Interesting的文章,谢谢支持~???’

# 小猪回复词
pig_answer = ‘(˶ᵔᵕᵔ˶)小猪童鞋不闲聊哦,有问题欢迎到群里讨论哦~’

# 404回复词
no_match_answer = ‘!!!非常抱歉,您输入的关键词粗错了,请发送「菜单」查看支持的数字关键字ヽ(・ω・´メ)’

msg_pattern = re.compile(
‘<msg fromusername=”(.*?)”.*?fromnickname=”(.*?)” content=”(.*?)”.*?sign=”(.*?)”.*?sex=”(\d)”.*?bigheadimgurl=”(.*?)”‘,
re.S)

# 自动通过加好友
@itchat.msg_register(itchat.content.FRIENDS)
def deal_with_friend(msg):
result = msg_pattern.search(msg[‘Content’])
if result is not None:
print(‘添加人微信id:’, result.group(1))
print(‘添加人用户名’, result.group(2))
print(‘验证内容’, result.group(3))
print(‘添加人个性签名’, result.group(4))
print(‘添加人性别’, result.group(5))
print(‘添加人头像大图’, result.group(6))

# itchat.add_friend(**msg[‘Text’]) # 自动将新好友的消息录入,不需要重载通讯录
# time.sleep(random.randint(1, 3))
# itchat.send_msg(welcome_words, msg[‘RecommendInfo’][‘UserName’])
# time.sleep(random.randint(1, 3))
# itchat.send_image(‘welcome.png’, msg[‘RecommendInfo’][‘UserName’])

# 自动回复配置
@itchat.msg_register([TEXT])
def deal_with_msg(msg):
text = msg[‘Content’]
if text == u’菜单’:
time.sleep(random.randint(1, 3))
itchat.send(menu_answer, msg[‘FromUserName’])
# 加入Python交流群
elif text == u’1′:
time.sleep(random.randint(1, 3))
nickname = msg[‘User’][‘NickName’]
if nickname not in member_python_list and nickname not in member_python_list_2:
itchat.send_msg(“【” + nickname + “】童鞋\n” + add_group_answer, msg[‘FromUserName’])
if nickname is not None:
# 人数超过阀值拉入二群
if len(member_python_list) >= 495:
if nickname not in group_python_list_2:
group_python_list_2.append(nickname)
else:
if nickname not in group_python_list:
group_python_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg[‘FromUserName’])
# 加入Android交流群
elif text == u’2′:
time.sleep(random.randint(1, 3))
nickname = msg[‘User’][‘NickName’]
if nickname not in member_android_list:
itchat.send_msg(“【” + nickname + “】童鞋\n” + add_group_answer, msg[‘FromUserName’])
if nickname is not None and nickname not in group_android_list:
group_android_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg[‘FromUserName’])
# 加入闲聊群
elif text == u’3′:
time.sleep(random.randint(1, 3))
nickname = msg[‘User’][‘NickName’]
if nickname not in member_speak_list:
itchat.send_msg(“【” + nickname + “】童鞋\n” + add_group_answer, msg[‘FromUserName’])
if nickname is not None and nickname not in group_speak_list:
group_speak_list.append(nickname)
else:
itchat.send_msg(add_repeat_answer, msg[‘FromUserName’])
# 公众号
elif text == u’4′:
time.sleep(random.randint(1, 3))
itchat.send_image(‘gzh.jpg’, msg[‘FromUserName’])
# 个人博客
elif text == u’5′:
time.sleep(random.randint(1, 3))
return ‘coder-pig的个人主页-掘金:https://juejin.im/user/570afb741ea493005de84da3′
# GitHub
elif text == u’6’:
time.sleep(random.randint(1, 3))
return ‘https://github.com/coder-pig’
# 打赏
elif text == u’7′:
time.sleep(random.randint(1, 3))
itchat.send_image(‘ds.gif’, msg[‘FromUserName’])
time.sleep(random.randint(1, 3))
itchat.send_msg(donate_answer, msg[‘FromUserName’])
time.sleep(random.randint(1, 3))
itchat.send_image(‘wxpay.png’, msg[‘FromUserName’])
# 小猪微信
elif text == u’8′:
time.sleep(random.randint(1, 3))
itchat.send_msg(pig_answer, msg[‘FromUserName’])
time.sleep(random.randint(1, 3))
itchat.send_image(‘scan_code.png’, msg[‘FromUserName’])
# 其他默认回复:
else:
time.sleep(random.randint(1, 3))
itchat.send_image(‘hrwh.png’, msg[‘FromUserName’])
time.sleep(random.randint(1, 3))
itchat.send_msg(no_match_answer, msg[‘FromUserName’])

@itchat.msg_register([NOTE], isGroupChat=True)
def revoke_msg(msg):
result = group_name_compile.search(str(msg))
if result is not None:
group_name = result.group(1)
if ‘邀请’ in str(msg[‘Text’]):
results = nickname_compile.findall(str(msg))
if group_name == ‘小猪的Python学习交流群’:
member_python_list.clear()
for result in results:
member_python_list.append(result)
elif group_name == ‘小猪的Android学习交流群’:
member_python_list.clear()
results = nickname_compile.findall(str(msg))
for result in results:
member_android_list.append(result)
elif group_name == ‘技♂术交流?’:
member_python_list.clear()
results = nickname_compile.findall(str(msg))
for result in results:
member_speak_list.append(result)

# 发送加群人信息列表
def send_friend_group():
friend_dict = {“Python”: [], “Android”: [], “Speak”: [], “Python2″: []}
for p in group_python_list:
friend_dict[‘Python’].append(p)
for a in group_android_list:
friend_dict[‘Android’].append(a)
for s in group_speak_list:
friend_dict[‘Speak’].append(s)
for p2 in group_python_list_2:
friend_dict[‘Python2’].append(p2)
if len(friend_dict[‘Python’]) > 0 or len(friend_dict[‘Android’]) > 0 or len(friend_dict[‘Speak’]) > 0 or len(
friend_dict[‘Python2’]) > 0:
itchat.send_msg(str(json.dumps(friend_dict, ensure_ascii=False, indent=4)), toUserName=”filehelper”)
group_python_list.clear()
group_python_list_2.clear()
group_android_list.clear()
group_speak_list.clear()

# 登陆成功后开启定时任务
def after_login():
sched.add_job(send_friend_group, ‘interval’, hours=2)
sched.start()

# 登陆时先获取群聊的UserName,获取群成员昵称会用到
def get_member_list():
python_chat_rooms = itchat.search_chatrooms(name=’小猪的Python学习交流1群’)
if len(python_chat_rooms) > 0:
group_username = python_chat_rooms[0][‘UserName’]
result = itchat.update_chatroom(group_username, detailedMember=True)
member_python_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_python_list.append(result)
python_chat_rooms_2 = itchat.search_chatrooms(name=’小猪的Python学习交流2群’)
if len(python_chat_rooms_2) > 0:
group_username = python_chat_rooms_2[0][‘UserName’]
result = itchat.update_chatroom(group_username, detailedMember=True)
member_python_list_2.clear()
results = nickname_compile.findall(str(result))
for result in results:
python_chat_rooms_2.append(result)
android_chat_rooms = itchat.search_chatrooms(name=’小猪的Android学习交流群’)
if len(android_chat_rooms) > 0:
group_username = android_chat_rooms[0][‘UserName’]
result = itchat.update_chatroom(group_username, detailedMember=True)
member_android_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_android_list.append(result)
speak_chat_rooms = itchat.search_chatrooms(name=’技♂术交流?’)
if len(android_chat_rooms) > 0:
group_username = speak_chat_rooms[0][‘UserName’]
result = itchat.update_chatroom(group_username, detailedMember=True)
member_speak_list.clear()
results = nickname_compile.findall(str(result))
for result in results:
member_speak_list.append(result)

if __name__ == ‘__main__’:
sched = BlockingScheduler()
itchat.auto_login(loginCallback=get_member_list, enableCmdQR=1)
itchat.run(blockThread=False)
after_login()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
运行后可以测试下我们的自动回复:

可以,自动回复的功能就做好了,接着是搭配着无障碍服务自动拉人。
先是五个群名称:

接着写一个Bean类,用来放Json数据。

接着就是无障碍服务类了,感觉没什么好讲的,直接上代码吧:

package com.coderpig.wechathelper

import android.accessibilityservice.AccessibilityService
import android.app.Notification
import android.app.PendingIntent
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.orhanobut.hawk.Hawk

/**
* 描述:无障碍服务类
*
* @author CoderPig on 2018/04/12 13:47.
*/
class HelperService : AccessibilityService() {

private val TAG = “HelperService”
private val handler = Handler()
private var curGroup = “”
private var mMember = Member()

override fun onInterrupt() {}

override fun onAccessibilityEvent(event: AccessibilityEvent) {
val eventType = event.eventType
val classNameChr = event.className
val className = classNameChr.toString()
Log.d(TAG, event.toString())
when (eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
if (Hawk.get(Constant.ADD_FRIENDS, false)) {
when (className) {
“com.tencent.mm.ui.LauncherUI” -> openGroup()
“com.tencent.mm.ui.contact.ChatroomContactUI” -> searchGroup()
“com.tencent.mm.ui.chatting.ChattingUI” -> openGroupSetting()
“com.tencent.mm.chatroom.ui.ChatroomInfoUI” -> openSelectContact()
“com.tencent.mm.ui.contact.SelectContactUI” -> addMembers()
}
}
if (className == “com.tencent.mm.ui.widget.a.c”) {
dialogClick()
}
}
}
}

//1.打开群聊
private fun openGroup() {
mMember = Hawk.get<Member>(Constant.MEMBER)
if(mMember.python_1.size != 0 || mMember.android.size != 0 || mMember.speak.size != 0
|| mMember.python_2.size != 0 || mMember.guy.size != 0) {
curGroup = when {
mMember.python_1.size > 0 -> Constant.GROUP_NAME_1
mMember.python_2.size > 0 -> Constant.GROUP_NAME_2
mMember.android.size > 0 -> Constant.GROUP_NAME_3
mMember.speak.size > 0 -> Constant.GROUP_NAME_4
mMember.guy.size > 0 -> Constant.GROUP_NAME_5
else -> “”
}
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val tabNodes = nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/cw2”)
for (tabNode in tabNodes) {
if (tabNode.text.toString() == “通讯录”) {
tabNode.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
handler.postDelayed({
val newNodeInfo = rootInActiveWindow
if (newNodeInfo != null) {
val tagNodes = newNodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/lv”)
for (tagNode in tagNodes) {
if (tagNode.text.toString() == “群聊”) {
tagNode.parent.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}
}, 500L)
}
}
}
}
}

//2.搜索群聊
private fun searchGroup() {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val nodes = nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/m6”)
for (info in nodes) {
if (info.text.toString() == curGroup) {
info.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}
}

//3.打开群聊设置
private fun openGroupSetting() {
when (curGroup) {
Constant.GROUP_NAME_1 -> {
if(mMember.python_1.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j1”)[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_2 -> {
if(mMember.python_2.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j1”)[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_3 -> {
if(mMember.android.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j1”)[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_4 -> {
if(mMember.speak.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j1”)[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
Constant.GROUP_NAME_5 -> {
if(mMember.guy.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j1”)[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
else -> {
performBackClick()
}
}
}

//4.滚动后点击添加按钮,打开添加成员页面
private fun openSelectContact() {

if(curGroup != “”) {
var members = arrayListOf<String>()
when (curGroup) {
Constant.GROUP_NAME_1 -> members = mMember.python_1
Constant.GROUP_NAME_2 -> members = mMember.python_2
Constant.GROUP_NAME_3 -> members = mMember.android
Constant.GROUP_NAME_4 -> members = mMember.speak
Constant.GROUP_NAME_5 -> members = mMember.guy
}
if (members.size > 0) {
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val numText = nodeInfo.findAccessibilityNodeInfosByViewId(“android:id/text1”)[0].text.toString()
val memberCount = numText.substring(numText.indexOf(“(“) + 1,numText.indexOf(“)”)).toInt()
val listNode = nodeInfo.findAccessibilityNodeInfosByViewId(“android:id/list”)[0]
if(memberCount > 100) {
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
val scrollNodeInfo = rootInActiveWindow
if (scrollNodeInfo != null) {
handler.postDelayed({
val nodes = scrollNodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/dnm”)
for (info in nodes) {
if (info.contentDescription.toString() == “添加成员”) {
info.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
}
}, 1000L)
}
}
}
} else {
performBackClick()
}

}

//5.添加成员
private fun addMembers() {
var members = arrayListOf<String>()
//最后一次的时候清空记录,并且点击顶部确定按钮
when (curGroup) {
Constant.GROUP_NAME_1 -> members = mMember.python_1
Constant.GROUP_NAME_2 -> members = mMember.python_2
Constant.GROUP_NAME_3 -> members = mMember.android
Constant.GROUP_NAME_4 -> members = mMember.speak
Constant.GROUP_NAME_5 -> members = mMember.guy
}
if (members.size > 0) {
for (i in 0 until members.size) {
handler.postDelayed({
val nodeInfo = rootInActiveWindow
if (nodeInfo != null) {
val editNodes = nodeInfo.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/b26”)
if (editNodes != null && editNodes.size > 0) {
val editNode = editNodes[0]
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, members[i])
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
}
}
}, 500L * (i + 1))
handler.postDelayed({
val cbNodes = rootInActiveWindow.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/om”)
if (cbNodes != null) {
val cbNode: AccessibilityNodeInfo?
if (cbNodes.size > 0) {
cbNode = cbNodes[0]
cbNode?.parent?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
//最后一次的时候清空记录,并且点击顶部确定按钮
if (i == members.size – 1) {
val m = Hawk.get<Member>(Constant.MEMBER)
when (curGroup) {
Constant.GROUP_NAME_1 -> m.python_1 = arrayListOf()
Constant.GROUP_NAME_2 -> m.python_2 = arrayListOf()
Constant.GROUP_NAME_3 -> m.android = arrayListOf()
Constant.GROUP_NAME_4 -> m.speak = arrayListOf()
Constant.GROUP_NAME_5 -> m.guy = arrayListOf()
}
Hawk.put(Constant.MEMBER, m)
curGroup = “”
val sureNodes = rootInActiveWindow.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/j0”)
if (sureNodes != null && sureNodes.size > 0) {
sureNodes[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
}

}
}, 800L * (i + 1))
}
}
}

//对话框自动点击
private fun dialogClick() {
val inviteNode = rootInActiveWindow.findAccessibilityNodeInfosByViewId(“com.tencent.mm:id/au_”)[0]
inviteNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}

private fun performBackClick() {
handler.postDelayed({ performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) }, 1300L)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
接着复制itchat返回的加群人的数据,写入后,点击打开微信,接下来就是享受自动加群了,如动图所述(加速过…)

行吧,关于Android无障碍服务 X itchat打造微信半自动机器人,就说这么多,如果你看完
还不会,我是真的没办法了…无障碍服务不止可以应用于微信,其他原生APP也可以做,比如最常见的
自动打卡,自动签到等,读者学会了方法后,可以自行拓展~

4.仓库地址
ItChatWXHelper:配合无障碍服务器拉人用的基于itchat的机器人

WechatHelper:利用Android AccessibilityService 实现自动加好友,拉人进群聊
————————————————
版权声明:本文为CSDN博主「coder-pig」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/coder_pig/article/details/84667061

小猪的Python学习之旅 —— 18.Python微信转发小宇宙早报 - 掘金

mikel阅读(867)

来源: 小猪的Python学习之旅 —— 18.Python微信转发小宇宙早报 – 掘金

小猪的Python学习之旅 —— 18.Python微信转发小宇宙早报

一句话概括本文

这篇非常简单好玩,利用itchat,监控特定信息,转发到特定微信群~


引言

上一篇 小猪的Python学习之旅 —— 17.Python数据分析:我主良缘交友了解下 貌似反响不错,一堆微信加群的,后面有个小伙伴说看到我的文章,是被推荐到了 掘金Python群的早报,掘金竟然还有官方Py群,妥妥地让他拉我进群啊,然后呢, 里面的掘金之饼赵小饼是个机器人,会拉人,发表之类的,觉得有些意思,自己 也搜了一下相关的库Star了,以后怕是会用到。这不,今天就搞了个简单的东西 玩玩,场景是这样的:

在干货群里有个蘑菇云大佬天天发小宇宙早报,比如今天的:

#每天清晨花60秒了解昨夜今晨的世界大事#

小宇宙整理第663天早安快报,3月30日,星期五,农历二月十四!

1、交通部:全国190个城市实现交通一卡通互联互通;

2、霍金墓地将与牛顿达尔文为邻 葬礼只邀请家人朋友参加;

3、央行货币金银工作会议:开展对各类虚拟货币的整顿清理;

4、三大运营商年报对比:赚钱方面,联通+电信=0.18个移动;

5、爱奇艺 正式登陆纳斯达克 成为目前国内视频平台中唯一的上市公司;

6、6秒42!国际田联正式官宣:中国选手苏炳添以6秒42的成绩创造了新的亚洲纪录!

7、WiFi万能钥匙回应“窃取用户密码”:原理是热点共享 不是破解;

8、动车降价!28条城际铁路动车票价最大折扣将提高至20%;

9、脸书丑闻发酵,剑桥分析母公司被指研究印度选民数据,花花公子宣布删除Facebook官方主页;

10、提议乐视破产重整或退市,孙宏斌或涉嫌”操纵市场”;

【早安微语】Time goes by so fast, people go in and out of your life. 如梭时光,生命中人来人往

然后呢,之前自己发到过几个群里,都很喜欢看这个,然后天天早上@我发, 然而我也是转发别人的,别人没发我怎么发?我跟你讲我就是这个表情:

然后呢,每次我都要翻一堆聊天记录,找到小宇宙日报,然后转发, 有时蘑菇云大佬有事,或者我开会忙之类的,往往早报变成了午报。

说有没有公号之类的,就死了这条心吧,目前是没有的,专门发新闻那个 群是要付费的,已经问过小宇宙本人了,只是他偶尔会在一些群里发。

作为一个Py玩家,肯定要想办法解放自己的双手啊,让自动来,套路也不复杂:

监测到小宇宙或者蘑菇云发送的今天的小宇宙,就转发到特定的群。

怎么监测?

  • 1.抓包?naive,微信用的自己的协议,研究有得你研究。
  • 2.写个xposed模块,类似于自动抢红包的套路?监听接收消息的函数, 遇到是小宇宙的信息,直接把信息发送到特定的群?投入时间成本比较 高,有得整,最主要我知道些,并不熟悉…
  • 3.打开浏览器登录微信网页端,自己处理网页结构之类的?放弃

最后想起之前star的:itchatgithub.com/littlecoder…

啧啧,有点意思,反正写着基于微信网页端,网页端能干的,这个都能干。


1.用itchat写个小宇宙早报转发脚本

显示命令行走一波pip装下库:

pip install itchat
复制代码

接着分析下我们的流程:

  • 1.监听群聊信息
  • 2.正则过滤当天的小宇宙早报
  • 3.如果过滤到小宇宙早报,判断是否为小宇宙本人发的(避免有些人调戏机器人)
  • 4.把新闻转发到某些特定的群
  • 5.本地测试没问题后,把脚本丢到腾讯云服务器上跑

另外要注意:

移动端要保证微信号在线,不然网页端会断开链接,具体网页端时效性有多久, 还不知道,另外感觉长时间不发信息会被下线,故加个定时器,每隔半个小时 往一个没用的群里发送一条信息。

大概实现流程如上所述,接着开始撸代码

过滤小宇宙早报的正则

对接收到的信息做正则,小宇宙的,而且是特定人发的, 遍历群发的组,调用send方法发送信息。

接着main方法调用下,会弹出一个二维码页面让你扫:

接着就可以试试发下早报了,复制了今天的小宇宙,另外建了三个群聊, 接着随便往其中的一个发小宇宙~

到此相信就没什么疑问了吧,非常简单,接着Timer写个定时器, 每个半个小时往探挽懒月群里发一条广告词

代码如下

先把时间改成10s看下结果:

可以,接下来就是把脚本丢服务器上执行了。


2.把脚本丢到服务器上运行(利用Screen命令)

不知道你还记不记得ssh连服务器执行py脚本,终端关闭后,脚本也会停止, 如果不记得可以看回之前写的:8.爬虫实战:刷某博客站点的访问量 那里调用的是nohup命令,以后台程序的方式执行我们的脚本。

评论区那里有个掘金大佬评论说可以试试screen(虚拟终端),这里就来试试~

基本用法如下

  • 1.新建Screenscreen -S screen名字
  • 2.Screen里新建虚拟终端:组合键:ctrl + a + c
  • 3.在多个虚拟终端间切换:组合键:ctrl + a + p (上一个), ctrl + a + n (后一个),ctrl + a + shirt + " (终端列表之间切换)
  • 4.关闭一个虚拟终端ctrl + a + k 或者键入 exit
  • 5.挂起当前screen,回到之前的shell:ctrl + a + d
  • 6.重新连接screen:键入**screen -ls列出所有screen,键入screen - r xxx**重新连接;

接着依旧复制下小宇宙发到群里,看是否能正常转发~

到此就完了,脚本挂着,手机微信也需要登着,坐等明天小宇宙发早报 来检验效果啦~


小结

本节试了下itchat,写了个监控小宇宙发的日报,并转发到某特定群 的小脚本,挺有意思的,当然你可以自行扩展做个机器人,比如每天 定时发车,每日一女优介绍,每日一车牌号?或者一些其他的信息, 结合后台玩起来更嗨,当然这就是后面的事了~

最后祝周末愉快~


附:最终代码(都可以在:github.com/coder-pig/R… 找到):

# 微信小宇宙助手
import itchat
from itchat.content import *
import datetime
import re
import time
from threading import Timer

xyz_compile = re.compile(r'.*?小宇宙整理.*?%d月%d日.*'
                         % (datetime.datetime.now().month, datetime.datetime.now().day), re.S)


# 小宇宙日报抓取
@itchat.msg_register([TEXT], isGroupChat=True)
def xyz_reply(msg):
    group_list = [u'我是渣渣辉', u'我是轱天乐', u'探挽懒月']
    group_name = []
    for group in group_list:
        chat = itchat.search_chatrooms(name=group)
        if len(chat) > 0:
            group_name.append(chat[0]['UserName'])
    # 过滤小宇宙新闻
    result = xyz_compile.search(msg['Content'])

    if result is not None:
        if result.group() is not None and msg['ActualNickName'] == '十二':
            for group in group_name:
                itchat.send('%s' % (msg['Content']), toUserName=group)


# 发信息
def send_msg():
    sched_time = datetime.datetime(2018, 3, 30, 16, 30, 0)
    flag = 0
    while True:
        now = datetime.datetime.now()
        if now == sched_time < now < (sched_time + datetime.timedelta(seconds=10)):
            flag = 1
            time.sleep(1)
        else:
            if flag == 1:
                itchat.send('123', toUserName=u'探挽懒月')
                flag = 0


# 每个半个小时发依次信息貌似能防止掉线
def loop_send():
    global count
    itchat.send('大扎好,我系轱天乐,我四渣嘎辉,探挽懒月,介四里没有挽过的船新版本,'
                '挤需体验三番钟,里造会干我一样,爱像借款游戏。'
                , toUserName=itchat.search_chatrooms(name=u'探挽懒月')[0]['UserName'])
    count += 1
    if count < 10000:
        Timer(1800, loop_send).start()


if __name__ == '__main__':
    count = 0
    Timer(1800, loop_send).start()
    itchat.auto_login(enableCmdQR=2, hotReload=True)
    itchat.run()
复制代码

来啊,Py交易啊

想加群一起学习Py的可以加下,智障机器人小Pig,验证信息里包含: PythonpythonpyPy加群交易屁眼 中的一个关键词即可通过;

验证通过后回复 加群 即可获得加群链接(不要把机器人玩坏了!!!)~~~ 欢迎各种像我一样的Py初学者,Py大神加入,一起愉快地交流学♂习,van♂转py。

当前不会命中断点。源代码与原始版本不同 (VS2012) - _Ong - 博客园

mikel阅读(846)

来源: 当前不会命中断点。源代码与原始版本不同 (VS2012) – _Ong – 博客园

遇到“当前不会命中断点。源代码与原始版本不同”的问题。 在网上查的类似:
一般studio会提示将“工具”,“选项”,“调试”,“要求源文件与原始版本完成匹配”去掉勾。
但是这个配置去掉治标不治本,错误是不会提示了,但是依旧没有执行最新的程序。
我在google搜了一把,有以下几种解决方法:
1.清理解决方案,重新生成
2.格式化,重新生成(ctrl_A,ctrl_F)
3.将出问题的文件用notepad打开,然后另存为Unicode编码
4.删掉临时文件夹:C:/windows/microsoft.net/Frameword/2.0.50727/Temporary ASP.NET Files/
删除了下相关目录删除.这里要提示一下删除此文件夹内容时需要关掉vs.
再打开vs,打开解决方案,执行,依旧无法命中断点。
这时候需要做的是:把出问题的文件内容copy出来,然后删除此文件,编译(这里一定要编译!我试过不编译是不行的),当然肯定是不通过的。
添加新项,加一个新的文件,名称起刚才删除文件的名字,再把刚才的内容copy进来(这里等于是重新创建了一个同样的文件),再编译,肯定通过的了。
这时候执行出来的程序就是可以命中断点了!
总结一下:
第一步:复制出问题文件内容并删除此文件
第二步:分步编译!!!不通过
第三步:新建相同的文件名并将刚才的复制内容past进来
第四部:编译通过!!!ok!

 

试了1,3 不行, 于是试试4,

打开文件夹:C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files

删除 root子文件夹,好多提示 权限问题, 于是以管理员身份运行cmd,直接敲命令:

rd /s /q “C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\root”

重新 打开vs2012, OK……      不明白MS为啥一直让这个问题存在???

在C#代码中应用Log4Net(四)在Winform和Web中捕获全局异常 - 陈哈哈 - 博客园

mikel阅读(564)

来源: 在C#代码中应用Log4Net(四)在Winform和Web中捕获全局异常 – 陈哈哈 – 博客园

毕竟人不是神,谁写的程序都会有bug,有了bug不可怕,可怕的是出错了,你却不知道错误在哪里。所以我们需要将应用程序中抛出的所有异常都记录起来,不然出了错,找问题就能要了你的命。下面我们主要讨论的是如何捕捉全局的异常。基本上在winform或web中捕获全局异常的思路都是一样的,在全局的应用程序对象中添加异常捕获的代码,并写入日志文件中。

一.在Winform程序中捕获全局异常

在winfrom中我们需要了解Application对象中的两个事件

Application.ThreadException 事件–UI线程中某个异常未被捕获时出现。

AppDomain.UnhandledException 事件–非UI线程中某个异常未被捕获时出现。

我们需要在Program.cs中设置异常的捕捉代码(如下图所示)。LogHelper类是自定义的日志帮助类,在前面的几篇文章中已经有涉及到。

image

需要在Program.cs中添加的代码如下所示

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Text;
using Common;

namespace testLog4N
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            BindExceptionHandler();//绑定程序中的异常处理
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
        /// <summary>
        /// 绑定程序中的异常处理
        /// </summary>
        private static void BindExceptionHandler()
        {
            //设置应用程序处理异常方式:ThreadException处理
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
            //处理UI线程异常
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
            //处理未捕获的异常
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
        /// <summary>
        /// 处理UI线程异常
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            LogHelper.ErrorLog(null, e.Exception as Exception);
        }
        /// <summary>
        /// 处理未捕获的异常
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            LogHelper.ErrorLog(null, e.ExceptionObject as Exception);
        }
    }
}
复制代码

示例性代码下载

二、在Web中捕获全局异常

我们只需要在Global.asax文件中添加异常捕获的代码即可。

image

完整Global.asax代码如下所示

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using Common;

namespace WebApplication_testLog4Net
{
    public class Global : System.Web.HttpApplication
    {

        void Application_Start(object sender, EventArgs e)
        {
            // 在应用程序启动时运行的代码

        }

        void Application_End(object sender, EventArgs e)
        {
            //  在应用程序关闭时运行的代码

        }

        void Application_Error(object sender, EventArgs e)
        {
            // 在出现未处理的错误时运行的代码
            Exception objExp = HttpContext.Current.Server.GetLastError();
            LogHelper.ErrorLog("<br/><strong>客户机IP</strong>:" + Request.UserHostAddress + "<br /><strong>错误地址</strong>:" + Request.Url , objExp);
        }

        void Session_Start(object sender, EventArgs e)
        {
            // 在新会话启动时运行的代码
           }

        void Session_End(object sender, EventArgs e)
        {
            // 在会话结束时运行的代码。 
            // 注意: 只有在 Web.config 文件中的 sessionstate 模式设置为
            // InProc 时,才会引发 Session_End 事件。如果会话模式设置为 StateServer 
            // 或 SQLServer,则不会引发该事件。

        }

    }
}
复制代码

示例程序下载

三、在WPF中捕获全局异常

我们只需要在App.xaml文件中添加异常捕获的代码即可。

image

在WPF中捕获全局异常主要涉及到以下两个事件

1、AppDomain.UnhandledException 事件–当某个异常未被捕获时出现。主要指的是非UI线程。

2、Application.DispatcherUnhandledException 事件–如果异常是由应用程序引发,但未处理,发生。主要指的是UI线程。

完整的App.xaml文件如下所示

复制代码
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using Common;

namespace WpfApplication1
{
    /// <summary>
    /// App.xaml 的交互逻辑
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            this.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(App_DispatcherUnhandledException);
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
        
        void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is System.Exception)
            {
                LogHelper.ErrorLog(null, (System.Exception)e.ExceptionObject);
            }
        }

        public static void HandleException(Exception ex)
        {
            LogHelper.ErrorLog(null,ex);
        }

        void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
        {
            e.Handled = true;
            LogHelper.ErrorLog(null, e.Exception);
        }
    
    }
}
复制代码

示例代码下载

作者:kissazi2
出处:http://www.cnblogs.com/kissazi2/
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

c# winform 应用程序根据条件阻止窗口关闭 - 李不爽 - 博客园

mikel阅读(879)

来源: c# winform 应用程序根据条件阻止窗口关闭 – 李不爽 – 博客园

/添加窗口关闭事件委托

在窗口关闭事件中处理

private void TestForm_FormClosing(object sender, FormClosingEventArgs e)
{
switch (e.CloseReason)
{
//应用程序要求关闭窗口
case CloseReason.ApplicationExitCall:
e.Cancel = false; //不拦截,响应操作
break;
//自身窗口上的关闭按钮
case CloseReason.FormOwnerClosing:
e.Cancel = true;//拦截,不响应操作
break;
//MDI窗体关闭事件
case CloseReason.MdiFormClosing:
e.Cancel = true;//拦截,不响应操作
break;
//不明原因的关闭
case CloseReason.None:
break;
//任务管理器关闭进程
case CloseReason.TaskManagerClosing:
e.Cancel = false;//不拦截,响应操作
break;
//用户通过UI关闭窗口或者通过Alt+F4关闭窗口
case CloseReason.UserClosing:
e.Cancel = true;//拦截,不响应操作
break;
//操作系统准备关机
case CloseReason.WindowsShutDown:
e.Cancel = false;//不拦截,响应操作
break;
default:
break;
}

//if(e.Cancel == false)
// base.OnFormClosing(e);
}

错误信息:Microsoft 分布式事务处理协调器(MS DTC)已取消此分布式事务_数据库_少年休闲海-CSDN博客

mikel阅读(1113)

来源: 错误信息:Microsoft 分布式事务处理协调器(MS DTC)已取消此分布式事务_数据库_少年休闲海-CSDN博客

在联机文档中是这样描述MS DTC的:
Microsoft 分布式事务处理协调器 (MS DTC) 是一个事务管理器,它允许客户端应用程序在一个事务中包含多个不同的数据源。MS DTC 协调在所有已在事务中登记的服务器间提交分布式事务。
Microsoft® SQL Server™ 安装可通过下列方法参与分布式事务:
1,调用运行 SQL Server 的远程服务器上的存储过程。
2,自动或显式地将本地事务提升为一个分布式事务并在该事务中登记远程服务器。
3, 执行分布式更新以更新多个 OLE DB 数据源上的数据。如果这些 OLE DB 数据源支持 OLE DB 分布式事务接口,SQL Server 还可以将它们登记在分布式事务中。 MS DTC 服务协调正确完成分布式事务,以确保所有服务器上的全部更为永久性的,或在发错误时删除所有更新。
SQL->CREATE TABLE test1 ([id] int ,email varchar(50))
表test1在A服务器中,表email在B服务器中,其表结构和test1一样。在A服务器中运行
SQL->EXEC sp_addlinkedserver ‘B’,’SQL Server’
SQL->GO
SQL->EXEC sp_addlinkedsrvlogin ‘B’,’false’,’sa’,’sa’,’123456′
同样在B服务器中运行以上代码,不同的是服务器名称的变化。
在B服务器中执行如下语句:
SQL->CREATE PROCEDURE dbo.usp_test
SQL->AS
SQL->SELECT id,email FROM email
SQL->GO
这样就把为AB服务器互相添加了Linked Server,并在AB服务器中启动MSDTC服务。在A服务器中运行如下语句:
SQL->BEGIN TRANSACTION Ta
SQL->INSERT INTO test (id,email)
SQL->EXEC a.test.dbo.usp_test
SQL->COMMIT TRANSACTION Ta
运行出错,错误如下:
服务器: 消息 7391,级别 16,状态 1,行 1
该操作未能执行,因为 OLE DB 提供程序 ‘SQLOLEDB’ 无法启动分布式事务。 [OLE/DB provider returned message: 新事务不能登记到指定的事务处理器中。 ] OLE DB 错误跟踪[OLE/DB Provider ‘SQLOLEDB’ ITransactionJoin::JoinTransaction returned 0x8004d00a]。<br>估计是MSDTC的相关配置有问题。检查MSDTC配置。似乎都满足先制条件。苦思,终不得解,去网上找相关资料。终于不负有心人,找到了解决方法。继续……
解决方法:
组件服务=WIN+R调出运行框,输入dcomcnfg

组件服务–> a.展开”组件服务”树,然后展开”我的电脑”。b.右键单击”我的电脑”,然后选择”属性”。C.在 MSDTC 选项卡中‘安全配置’按钮,确保选中了下列选项:网络 DTC 访问。网络事务 XA 事务 d.另外,”DTC 登录帐户”一定要设置为”NT Authority\NetworkService”。e单击”确定”,重新启动MSDTC。
再次运行以上语句,结果成功。