补环境即补充浏览器环境。那为什么要补充浏览器环境呢,因为大多数时候,我们执行js代码都是放在Node中执行。因为Node服务比浏览器更容易部署,所以更愿意用Node。

而Node环境和浏览器存在差异,所以我们需要补充浏览器环境。比如Node没有浏览器相关的对象如window, document等等。另外Node比浏览器多了一些require, module对象,这些也要去除。有同事说,Node没有浏览器中的dom树解析,没有渲染引擎等等,这也是对的。但是从js语言看,一切皆对象,Node是没有window, document等对象。为了让js代码能执行下去,我们只需要补充这些对象即可。例如window对象,有些校验不严格的情况下window = {}也是可以的。

对于Web反爬来说,大多数时候,都是想办法检测是不是Node环境,而这些检测手段,在js语言层面,大都是检测对象是否存在,检测原型链是否和浏览器一致,函数toString是否和浏览器一致。

Node比浏览器多了一些require, module对象,可以使用Node的vm2模块进行规避,vm2模块会构建沙盒环境,在沙盒里面少了很多Node特征。

Node比浏览器少的对象,可以使用proxy技术,缺啥补啥。目前市面上免费的课程,当属志远的补环境框架,讲的是真细。到这时候你会发现,js的语言基础很重要,原型链尤其重要。

有些时候,即便环境补好了,生成的token和浏览器还是不一样,这时需要看代码,对着浏览器进行比较,此时把混淆代码还原后更容易看懂。这就需要用到AST技术了,此时蔡老板的星球就派上用场了。

目前来看, 补环境技术遇到的一个难点是dom树操作,想在js里实现一个dom树操作真心不容易。而jsdom也算是补环境的一种,作为开源十多年的项目,jsdom实现了dom树操作,在jsdom上进行二次开发是个不错的选择。

联系作者

用zp_token完善补环境框架

从志远的补环境教程了解到还有补环境框架这个东西后,立刻打开了新思路,从简单的网站开始,逐渐完善自己的补环境框架。在完成了加速乐和hexin-v后,将新目标指向了zp_token.

去年的时候从卷木木的文章里知道了补环境这个东西,那时玩的就是zp_token, 但那时候的补环境还没有形成一个完整的体系,就是缺啥补啥的一种状态,代码写的不够通用;现在学习了补环境框架后,虽然也是缺啥补啥,但代码更通用,针对新的网站只要稍加修改就能跑起来。

相比于加速乐和hexin-v, zp_token的生成需要补充的环境增加不少,从混淆后的代码量也能看出难度。zp_token的混淆代码将近2万5千行,hexin-v就1200左右,加速乐不到1000行。但依然还是对着浏览器,缺啥补啥,只不过zp_token增加了更多的判断。处理完所有缺失的函数和属性后,发现生成的zp_token还是过不了网站的检测,这时就得分析代码。

将近2万5千的混淆代码,用AST去除控制流平坦化后,还有将近2万行,剩下还有好多字符串和花指令可以还原,调试也是能调试,但比较麻烦。手上也没有称手的AST插件来还原这种混淆,过去两年一直忙于业务开发,疲于奔命,在AST还原上,一直只是停留在控制流平坦化的还原,要搞定这种字符串和花指令还是需要花些时间。此时只好请大佬出山,大佬直接将代码还原到了6000行,调试起来方便无比。

有了新代码,对着浏览器进行调试,依然是缺啥补啥,很快就搞好了,生成的zp_token一测试,通过。经过了zp_token的测试,补环境框架完善了好多,下一个目标可以搞搞其它反爬产品。现在发现,做Web反爬是真的不容易,代码客户端都能看到,真的难做。

AST还是值得学习的,这里推荐蔡老板的星球AST入门与实践,另外渔歌的星球Python爬虫应用学习也非常值得学习,渔歌是真大佬,又强又卷,真是没得讲。

联系作者

最近试了下公司网站的反爬,发现用补环境技术半天就可以搞定,如果用jsdom这种伪浏览器,一分钟就可以搞定。于是写下自己这些年来对爬虫的理解,希望对公司的反爬有帮助。

对于一个接口的反爬,可以分两个阶段,一个是请求时做反爬,一个是请求后对请求结果做反爬。

对请求结果做反爬可以有请求结果加密,字体反爬,CSS反爬等等,其中字体反爬和CSS反爬对搜索引擎不友好。

对请求时做反爬,主要是添加请求token, 对请求的token做校验。请求token的生成过程要手机客户端信息,对客户端环境做校验。

对客户端环境做校验主要分为两种,一种是浏览器环境,一种是Node环境。

对于浏览器环境的检测,有Canvas指纹,Webgl指纹,Audio指纹,字体指纹,还有WebRTC指纹等等。WebRTC可以获取到客户端的实际内外网IP,即便浏览器加了代理。对Canvas指纹,Webgl指纹,Audio指纹,字体指纹做校验是为了防止爬虫用程序批量抓取,因为这种批量抓取,指纹大都是一样的。当然对于有经验的爬虫,这些都防不住,指纹可以修改,WebRTC可以禁用。

对于Node环境,主要是检测与浏览器环境的差异,如require, module等关键字是不是存在;还可以检测对象的原型链,如document的原型链是不是和浏览器一致等等;还可以检测函数实现,检测代码是否被格式化,检测异常堆栈,这些都有待反爬工程师去挖掘。

反爬代码写好之后,我们还需对它进行混淆,增加破解难度。目前推荐vmp混淆,但由于vmp性能有问题,只能用在关键算法的加密上,不能用于全部代码。当然自己实现一个vmp有一点难,v_jstools的作者有vmp混淆工具,可以去找找,开源的有Obfuscator混淆,但蔡老板可以一键还原,所以做混淆有点难,目前就没有蔡老板搞不定的混淆(除了vmp)。反爬代码的破解难度不取决于混淆,而取决于反爬代码对环境的检测,检测的越多,越难破解。要知道补环境这种爬虫技术是可以无视混淆的,在补环境过不了反爬时,才需要看混淆代码。

除了环境检测外,还可以增加一些行为事件检测,如鼠标滑动事件,鼠标点击事件,这些都可以增加爬虫的破解难度。

对于token,最好只能请求一次就失效,而不是可以一直请求;对于反爬代码,能做到动态变化最好,这些都会增加爬虫的破解难度。

另外,风控也是反爬的一种,这里就不班门弄斧了。

总体来说,反爬防不住所有人,再难的反爬,也有人能破解,但只要增加了破解反爬的成本,让爬虫放弃抓取,反爬就成功了。

联系作者

20年7月份,做爬虫逆向不到3个月的时候,遇到了一段js反爬, 后来才知道它叫加速乐, 用的混淆是开源的Obsfuscator混淆。那时虽然接手了同事的瑞数项目,但对js逆向还没有深入的研究,还不懂AST, 不懂补环境,甚至还不懂本地调试。

从同事那里知道本地调试,把代码下载下来后,用正则进行了处理,就开始进行调试,打断点,知道了会对浏览器的一些环境进行检测,会使代码进入死循环。和同事折腾了一个周末的js,准确率到95%, 可以用到生产环境中。

两年过去,学习了AST还原代码,蔡老板也开源了他写的Obsfuscator解混淆工具,工具一跑,900多行的混淆代码只剩300行不到,调试起来轻轻松松。

最近学习了补环境这个新技术,于是用加速乐来测试下。因为补环境是缺啥补啥,而加速乐对环境检测其实挺少的,花了一个小时的补出来的环境就能够生成cookie, 测试了一下,能跑通,真是省心又省力,生产力大大提升。

补环境真不错,无视加密代码和混淆,就是缺啥补啥,现在想来使用jsdom也算是补环境的一种手段。

联系作者

js代码在Node环境中可以执行,但因为与浏览器环境不一致,生成的参数会与浏览器不同,这时就需要用到补环境,补充Node环境相对于浏览器缺少的环境,去除Node环境中相对于浏览器多余的环境。

例如Node环境中,函数toString的时候大都是function () { [native code] } ,而浏览器中会返回整个函数定义

Node环境中有module, Buffer等等,而浏览器中没有.

还有一些会检测原型链,例如浏览器中document.bod有值,但是document.hasOwnProperty(‘body’)却是false.

补环境的时候,proxy是一个非常有用的工具。固定本地代码和浏览器代码,固定随机参数,慢慢对比。

补环境教程推荐志远的补环境视频,那是真的强。

联系作者

在逆向一些复杂的前端代码(如每次请求返回的代码都不一样)时, 能在本地调试就显得非常重要。本地调试,也就是很多人说的脱机。

有很多工具可以复制网站请求,浏览器也可以用来做这件事。右击,然后存储为,就可以把大多数请求都保存下来,然后缺啥补啥。

如果需要保持域名和线上一致,还需要用到Nginx, 修改hosts, 把域名指向本地。

如果需要保持请求的url和线上一致,还需要用到Nginx里的rewrite, 例如把.do的转成.html。

这样一套完成后,就可以开心的在本地调试了。

联系作者

第一次遇到JJEncode, 代码与如下类似,一堆的奇奇怪怪的字符,特征是结尾有两个括号

1
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"(\\\"\\"+$.__$+$.__$+$.___+$.$$$_+(![]+"")[$._$_]+(![]+"")[$._$_]+$._$+",\\"+$.$__+$.___+"\\"+$.__$+$.__$+$._$_+$.$_$_+"\\"+$.__$+$.$$_+$.$$_+$.$_$_+"\\"+$.__$+$._$_+$._$$+$.$$__+"\\"+$.__$+$.$$_+$._$_+"\\"+$.__$+$.$_$+$.__$+"\\"+$.__$+$.$$_+$.___+$.__+"\\\"\\"+$.$__+$.___+");\\"+$.__$+$._$_+"\\"+$.__$+$.__$+"\\"+$.__$+$.__$+"\\"+$.__$+$.__$+"\"")())();

结果我想的复杂了,想着用AST去解决,实际上非常简单,只需要把代码放在浏览器console里,把最后的括号去掉,就能还原代码,上述代码可以得到如下代码

1
2
3
4
5
(function anonymous(
) {
alert("Hello, JavaScript" );

})

与JJEncode类似的还有AAEncode, JSFuck,这里就不一一列举了

联系作者

有段时间,对APP逆向热情高涨,尝试了多个APP的逆向,对APP逆向有了大致的了解。和Web逆向一样,首先得抓包,不同的是,APP抓包难度更大一些。有一些APP抓包容易,有一些APP抓包难。

对于简单的APP,用一些抓包软件,设置下HTTPS代理即可。可以直接在手机上进行抓包,也可以在电脑上进行抓包。

在手机端抓包比较容易,直接安装抓包APP就行,如iOS系统中的Stream, Android系统中HttpCanary都是很好的手机端抓包软件。

在电脑端抓包需要设置的更多一些,但设置好之后,更容易找到对应的接口,因为你在APP中操作的时候,请求包也同时发出,同时电脑端的抓包软件功能也更强大一些,便于查找。电脑端可以使用Fiddler,Charles等抓包软件,需要将手机和电脑在同一个网络中,并在手机上设置代理,将请求代理到电脑上,并在手机上安装抓包软件的证书。设置好之后即可进行抓包。

有一些复杂的APP,用上面的抓包软件抓不到包,有一些是因为APP里忽略代理,有一些是因为APP设置了证书双向认证,要解决这些问题,要使用其它的方法,目前在学习中。

联系作者

想在Node环境中实现浏览器的操作,寻寻觅觅,最终找到了jsdom。

试用了jsdom后,发现是真的方便,实现了绝大多数的浏览器操作,几乎可以当作浏览器来使用。这就是刚来搞逆向时,那时还停留在扣代码的阶段,就想要一个Node库来帮助我模拟出浏览器环境,如浏览器指纹,鼠标键盘事件和滑动轨迹等等。虽然现在还不知道用jsdom怎么添加鼠标键盘事件,但对于大多数网站已经足够用。

熟悉使用后,可以用于大多数网站,省去了很多时间。对于不会自己补环境的逆向工程师,真是省心又省力,值得一学。

联系作者

前些天在看David Beazley的Python3 metaprogramming视频,觉得是时候总结下视频中学到的内容, 这篇文章主要是这个视频的笔记,以及关于metaclass的一些思考.

装饰器

首先看一个需求,就是想在函数被调用时,记录一下,可以简单的加个print

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def add(x, y):
print('add')
return x + y

def sub(x, y):
print('sub')
return x - y

def mul(x, y):
print('mul')
return x * y

def div(x, y):
print('div')
return x / y

print(add(3, 5))
print(sub(5, 2))

输出
add
8
sub
3

但是这样的话, print语句就重复了, 每个函数里都得加print, 于是我们可以使用装饰器

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
# 一个简单的装饰器
def debug(func):
def wrapper(*args, **kwargs):
print(func.__name__)
return func(*args, **kwargs)
return wrapper

@debug
def add(x, y):
return x + y

@debug
def sub(x, y):
return x - y

@debug
def mul(x, y):
return x * y

@debug
def div(x, y):
return x / y

print(add(3, 5))
print(sub(5, 2))

输出
add
8
sub
3

使用functools

但是这个简单的装饰器是存在问题的,它会忽略被装饰的函数

1
2
3
4
print(add)

输出
<function debug.<locals>.wrapper at 0x1037a6bf8>

这里add函数经过debug装饰器装饰后,函数名都被忽略了,这个时候functools模块就派上用场了, 里面的wraps装饰器就是用来解决这个问题

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
from functools import wraps

def debug(func):
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper

@debug
def add(x, y):
return x + y

@debug
def sub(x, y):
return x - y

@debug
def mul(x, y):
return x * y

@debug
def div(x, y):
return x / y

print(add(3, 5))
print(sub(5, 2))

输出
add
8
sub
3

print(add)
输出
<function add at 0x1037afa60>

带参数的装饰器

有时候装饰器里想传入一些参数, 这时就可以写带参数的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# decorators with args
from functools import wraps

def debug(prefix=''):
def decroate(func):
msg = prefix + func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
return decroate

@debug('###')
def add(x, y):
return x + y

print(add(3, 2))
输出
###add
5

这种带参数的装饰器有一个头疼的问题是, 使用装饰器时, 如果不想传参数, 也得加上括号, 不然会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@debug
def sub(x, y):
return x - y

sub(5, 3)

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-40-9a93be17056c> in <module>
3 return x - y
4
----> 5 sub(5, 3)

TypeError: decroate() takes 1 positional argument but 2 were given

加上括号后, 就不报错, 这很丑陋

1
2
3
4
5
@debug()
def sub(x, y):
return x - y

print(sub(5, 3))

这里有一个小技巧, 实现如下

1
2
3
4
5
6
7
8
9
10
11
from functools import wraps, partial

def debug(func=None, prefix=''):
if func is None:
return partial(debug, prefix=prefix)
msg = prefix + func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper

如此, 当使用默认参数时, 即便不带括号时,也不会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@debug(prefix='###')
def add(x, y):
return x + y


@debug
def sub(x, y):
return x - y

print(add(3, 2))
print(sub(5, 3))
输出
###add
5
sub
2

类装饰器

以下我们定义一个Spam类,

1
2
3
4
5
6
7
8
9
10
11
class Spam:
def a(self):
pass
def b(self):
pass
@classmethod
def c(cls):
pass
@staticmethod
def d():
pass

然后我们想在类的方法被调用时, 能够记录下, 想上面的函数被调用时一样, 这时我们就会可以编写一个类装饰器

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
def debugmethods(cls):
# cls is class
for key, val in vars(cls).items():
if callable(val):
setattr(cls, key, debug(val))
return cls

@debugmethods
class Spam:
def a(self):
pass
def b(self):
pass
@classmethod
def c(cls):
pass
@staticmethod
def d():
pass

spam = Spam()
spam.a()
spam.b()
spam.c()
spam.d()
输出
Spam.a
Spam.b

这里只打印了a和b, 没有打印c和d, 这什么原因呢? 这是因为classmethod和staticmethod都是descriptor, 也就是描述器, 它们没有实现__call__方法,也就不是callable的

我们也可以编写一个类装饰器, 当获取一个属性时, 打印日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# debug access
def debugattr(cls):
orig_getattribute = cls.__getattribute__
def __getattribute__(self, name):
print('Get:', name)
return orig_getattribute(self, name)
cls.__getattribute__ = __getattribute__
return cls

@debugattr
class Spam:
def __init__(self, x, y):
self.x = x
self.y = y

spam = Spam(2, 3)
print(spam.x)
输出
Get: x
2

metaclass

现在我们想对所有的类都能打印日志,一个解决的办法是在所有的类前面都加上类装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@debugmethods
class Base:
def a(self):
pass
def b(self):
pass

@debugmethods
class Spam(Base):
def a(self):
pass
b = Base()
b.a()
s = Spam()
s.a()
输出
Base.a
Spam.a

但这样很麻烦,于是metaclass派上用场了, metaclass最强的地方是可以控制类的创建

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
# a metaclass
class debugmeta(type):
def __new__(cls, clsname, bases, clsdict):
clsobj = super().__new__(cls, clsname, bases, clsdict)
# print("here", cls, clsname, type(clsobj), clsobj)
clsobj = debugmethods(clsobj)
# print(vars(clsobj))
return clsobj

class Base(metaclass=debugmeta):
def a(self):
pass
def b(self):
pass


class Spam(Base):
def __init__(self, name):
self.name = name

def a(self):
pass

b = Base()
b.a()
s = Spam('name')
s.a()

输出
Base.a
Spam.__init__
Spam.a

从上面的例子中我们看到,有一个类有metaclass, 它的所有子类都有metaclass, 这说明metaclass是会被继承的。结合蔡元楠的《metaclass, 是潘多拉魔盒还是阿拉丁神灯》, 可以知道,这里debugmeta其实不只一种写法,在__init__函数里实现也是可以的。重载__init__的实现如下

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
# a metaclass
class debugmeta(type):
def __init__(cls, name, bases, kwds):
super(debugmeta, cls).__init__(name, bases, kwds)
cls = debugmethods(cls)

class Base(metaclass=debugmeta):
def a(self):
pass
def b(self):
pass


class Spam(Base):
def __init__(self, name):
self.name = name

def a(self):
pass
b = Base()
b.a()
s = Spam('lala')
s.a()
输出
Base.a
Spam.__init__
Spam.a

这是因为, 所有的类都是type的实例, 都是对type的__call__方法进行重载, 而type的__call__方法会调用type.__new__(typeclass, classname, superclasses, attributedict)type.__init__(class, classname, superclasses, attributedict), 所以上面重写__new__和重写__init__都是可以的。

学到这里,我脑海里冒出了一个想法,就是为啥这里一定要用metaclass呢? 用继承的方式难道不行吗?于是自己尝试写了个继承的方式, 发现也是跑得通的。

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
# a baseclass
class debugmeta:
def __new__(cls, *args, **kwargs):
cls = debugmethods(cls)
clsobj = object.__new__(cls)
# clsobj = debugmethods(clsobj)
# print('aaa', type(clsobj), clsobj)
# print('aaa', clsobj, type(clsobj), vars(clsobj), dir(clsobj), type(clsobj.a), callable(clsobj.a))
return clsobj

class Base(debugmeta):
def a(self):
pass


class Spam(Base):
def __init__(self, name):
self.name = name
def a(self):
pass


b = Base()
b.a()
s = Spam('name')
s.a()
输出
Base.a
Spam.__init__
Spam.a

但实际上,这样做法是有问题的,后面等到后面我们来纠正这个问题。

既然如此,那么蔡元楠在《metaclass, 是潘多拉魔盒还是阿拉丁神灯》介绍的yaml的动态序列化和逆序列化的能力又为何要用metaclass实现呢?用继承难道不行吗?于是也写了一个继承的版本, 代码如下。

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
import yaml


class MyYAMLObjectBaseclass(object):
"""
The metaclass for YAMLObject.
"""

def __new__(cls, *args, **kwargs):
if cls.yaml_tag:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
cls.yaml_dumper.add_representer(cls, cls.to_yaml)
return object.__new__(cls)

class MyYAMLObject(MyYAMLObjectBaseclass):
"""
An object that can dump itself to a YAML stream
and load itself from a YAML stream.
"""


__slots__ = () # no direct instantiation, so allow immutable subclasses

yaml_loader = yaml.Loader
yaml_dumper = yaml.Dumper

yaml_tag = None
yaml_flow_style = None

@classmethod
def from_yaml(cls, loader, node):
"""
Convert a representation node to a Python object.
"""

return loader.construct_yaml_object(node, cls)

@classmethod
def to_yaml(cls, dumper, data):
"""
Convert a Python object to a representation node.
"""

return dumper.represent_yaml_object(cls.yaml_tag, data, cls,
flow_style=cls.yaml_flow_style)


class Monster(MyYAMLObject):
yaml_tag = '!Monster'

def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks)


class Dragon(Monster):
yaml_tag = '!Dragon'

def __init__(self, name, hp, ac, attacks, energy):
super(Dragon, self).__init__(name, hp, ac, attacks)
self.energy = energy

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}, energy={})".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks, self.energy)

m = Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
ms = yaml.dump(m)
# print(ms)
d = Dragon(name='Cave spider', hp=[2, 6], ac=17, attacks=['BITE', 'HURT'], energy=5000)
ds = yaml.dump(d)
# print(ds)

print(yaml.load(ms, Loader=yaml.Loader))
print(yaml.load(ds, Loader=yaml.Loader))
输出
Monster(name=Cave spider, hp=[2, 6], ac=16, attacks=['BITE', 'HURT']
Dragon(name=Cave spider, hp=[2, 6], ac=17, attacks=['BITE', 'HURT'], energy=5000)

所以其实到这里我还是没有明白为啥yaml要用metaclass来实现这个功能, 直到把子类的yaml_tag去掉, 才发现问题。

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
import yaml


class Monster(MyYAMLObject):
yaml_tag = '!Monster'

def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks)


class Dragon(Monster):

def __init__(self, name, hp, ac, attacks, energy):
super(Dragon, self).__init__(name, hp, ac, attacks)
self.energy = energy

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}, energy={})".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks, self.energy)

m = Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
ms = yaml.dump(m)
# print(ms)
d = Dragon(name='Cave spider', hp=[2, 6], ac=17, attacks=['BITE', 'HURT'], energy=5000)
ds = yaml.dump(d)
# print(ds)

print(yaml.load(ms, Loader=yaml.Loader))
print(yaml.load(ds, Loader=yaml.Loader))
输出
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-58-1e3c4c44fbaf> in <module>
33 # print(ds)
34
---> 35 print(yaml.load(ms, Loader=yaml.Loader))
36 print(yaml.load(ds, Loader=yaml.Loader))

<ipython-input-58-1e3c4c44fbaf> in __repr__(self)
24 def __repr__(self):
25 return "{}(name={}, hp={}, ac={}, attacks={}, energy={})".format(self.__class__.__name__, self.name, self.hp,
---> 26 self.ac, self.attacks, self.energy)
27
28 m = Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])

AttributeError: 'Dragon' object has no attribute 'energy'

这是因为Dragon这里没有yaml_tag的时候, 会继承父类Monster的yaml_tag, 这时Dragon.yaml_tag就是非空, 然后就会将!Monster这个标记与Dragon绑定到一起了, 覆盖了前面!Monster与Monster的绑定, 这时再去加载Monster类dump出来的内容, 就会报没有energy. 而使用metaclass就不存在这个问题, 因为在创建Dragon类时, 传入的属性字典里不会带有yaml_tag, 也就不会将!Monster这个标记与Dragon绑定到一起. 写代码测试如下

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
import yaml


class MyYAMLObjectMetaclass(type):
"""
The metaclass for YAMLObject.
"""

def __new__(cls, name, bases, kwds):
print(cls, name, bases, kwds.get('yaml_tag', 'hahaha'))
clsobj = super().__new__(cls, name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
clsobj.yaml_loader.add_constructor(clsobj.yaml_tag, clsobj.from_yaml)
clsobj.yaml_dumper.add_representer(clsobj, clsobj.to_yaml)
return clsobj

class ThisYAMLObjectMetaclass(type):
"""
The metaclass for YAMLObject.
"""

def __init__(cls, name, bases, kwds):
print(cls, name, bases, kwds.get('yaml_tag', 'hahaha'))
super(ThisYAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
cls.yaml_dumper.add_representer(cls, cls.to_yaml)


class ThisYAMLObjectBaseclass(object):
"""
The metaclass for YAMLObject.
"""

def __new__(cls, *args, **kwargs):
print('base __new__', cls, args, kwargs)
if cls.yaml_tag:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
cls.yaml_dumper.add_representer(cls, cls.to_yaml)
return object.__new__(cls)


class MyYAMLObject(metaclass=ThisYAMLObjectMetaclass):
# class MyYAMLObject(ThisYAMLObjectBaseclass):
# class MyYAMLObject(metaclass=MyYAMLObjectMetaclass):
"""
An object that can dump itself to a YAML stream
and load itself from a YAML stream.
"""


__slots__ = () # no direct instantiation, so allow immutable subclasses

yaml_loader = yaml.Loader
yaml_dumper = yaml.Dumper

yaml_tag = None
yaml_flow_style = None

@classmethod
def from_yaml(cls, loader, node):
"""
Convert a representation node to a Python object.
"""

return loader.construct_yaml_object(node, cls)

@classmethod
def to_yaml(cls, dumper, data):
"""
Convert a Python object to a representation node.
"""

return dumper.represent_yaml_object(cls.yaml_tag, data, cls,
flow_style=cls.yaml_flow_style)


class Monster(MyYAMLObject):
yaml_tag = '!Monster'

def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks)


class Dragon(Monster):
# yaml_tag = '!Dragon'

def __init__(self, name, hp, ac, attacks, energy):
super(Dragon, self).__init__(name, hp, ac, attacks)
self.energy = energy

def __repr__(self):
return "{}(name={}, hp={}, ac={}, attacks={}, energy={})".format(self.__class__.__name__, self.name, self.hp,
self.ac, self.attacks, self.energy)

m = Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
ms = yaml.dump(m)
# print(ms)
d = Dragon(name='Cave spider', hp=[2, 6], ac=17, attacks=['BITE', 'HURT'], energy=5000)
ds = yaml.dump(d)
# print(ds)

print(yaml.load(ms, Loader=yaml.Loader))
print(yaml.load(ds, Loader=yaml.Loader))
输出
<class '__main__.MyYAMLObject'> MyYAMLObject () {'__module__': '__main__', '__qualname__': 'MyYAMLObject', '__doc__': '\n An object that can dump itself to a YAML stream\n and load itself from a YAML stream.\n ', '__slots__': (), 'yaml_loader': <class 'yaml.loader.Loader'>, 'yaml_dumper': <class 'yaml.dumper.Dumper'>, 'yaml_tag': None, 'yaml_flow_style': None, 'from_yaml': <classmethod object at 0x1038e9320>, 'to_yaml': <classmethod object at 0x1038e9390>}
<class '__main__.Monster'> Monster (<class '__main__.MyYAMLObject'>,) {'__module__': '__main__', '__qualname__': 'Monster', 'yaml_tag': '!Monster', '__init__': <function Monster.__init__ at 0x1038d2f28>, '__repr__': <function Monster.__repr__ at 0x1038ce048>}

<class '__main__.Dragon'> Dragon (<class '__main__.Monster'>,) {'__module__': '__main__', '__qualname__': 'Dragon', '__init__': <function Dragon.__init__ at 0x1038ce0d0>, '__repr__': <function Dragon.__repr__ at 0x1038ce158>, '__classcell__': <cell at 0x103837af8: ThisYAMLObjectMetaclass object at 0x7fc8fb130de8>}
Monster(name=Cave spider, hp=[2, 6], ac=16, attacks=['BITE', 'HURT']

Dragon(name=Cave spider, hp=[2, 6], ac=17, attacks=['BITE', 'HURT'], energy=5000)

从上面的结果里可以看到, yaml_tag是没有在Dragon类的属性字典里的,即便是Dragon类会从Monster那里继承yaml_tag.

回到前面的用基类来实现对所有类使用debugmethods进行装饰的例子。这里因为每次创建对象的时候都会调用__new__方法, 会导致多次调用debugmethods装饰器, 这样会导致创建多少个对象, 调用一次类的方法就会输出多次, 测试如下

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
# a baseclass
class debugmeta:
def __new__(cls, *args, **kwargs):
cls = debugmethods(cls)
clsobj = object.__new__(cls)
# clsobj = debugmethods(clsobj)
# print('aaa', type(clsobj), clsobj)
# print('aaa', clsobj, type(clsobj), vars(clsobj), dir(clsobj), type(clsobj.a), callable(clsobj.a))
return clsobj

class Base(debugmeta):
def a(self):
pass


class Spam(Base):
def __init__(self, name):
self.name = name
def a(self):
pass


b = Base()
b.a()
s = Spam('name')
s.a()
print('-------')
s = Spam('lblb')
s.a()
输出如下
here <class '__main__.Base'>
Base.a
here <class '__main__.Spam'>
Spam.__init__
Spam.a
-------
here <class '__main__.Spam'>
Spam.__init__
Spam.__init__
Spam.a
Spam.a

这里Spam类创建了两个对象, 就调用了两次debugmethods, 所以会有多次输出。

到了这里, 我终于明白为什么要用metaclass来解决对于所有类使用debugmethods来装饰的问题,因为在metaclass里实现,则只会调用一次,因为一个类的创建只需要一次。也明白为什么yaml要使用metaclass, 而不是继承了。

总的来说, metaclass并不是什么奇淫巧技,简单来说就是一种改变类创建过程的能力。当然, 绝大多数情况下都不需要用到它。

联系作者