Python秘籍:轻松修改第三方库源码的终极黑科技

需求不符合

很多时候,我们下载的 第三方库 是不会有需求不满足的情况,但也有极少的情况,第三方库 没有兼顾到需求,导致开发者无法实现相关功能。

如何通过一些操作将 第三方库 源码进行修改,是我们将要遇到的一个难点。接下来,本文将介绍几个修改源码的操作,看看你有实现过几个?

本文可操作的是 有源码的第三方库,非源码的不在本文讨论范围内。

模拟示例 

# -*- coding: utf-8 -*-
import threading
import time


class Proxy:
    def __init__(self):
        # 这个线程是为了模拟网络代理抓包后的发送任务,是测试用的
        self.simulate_thread = threading.Thread(target=self.run, args=())
        self.simulate_thread.start()

        self.lock = threading.Lock()

        self.target = None

    def run(self):
        while True:
            time.sleep(1)
            with self.lock:
                if self.target is not None:
                    self.target(self.parse(None))

    def parse(self, data):
        '''
        模拟 解析二进制数据并转为字典
        :param data:
        :return:
        '''

        result = {
            'host': '127.0.0.1',
            'content_type': 'text/html',
            'body': '<html></html>'
        }
        return result['body']

    def hook(self, target):
        '''
        模拟挂载方法
        :param target:
        :return:
        '''
        with self.lock:
            self.target = target

上面代码将模拟一个网络代理,我们将其取名为 Proxy 库,这个网络代理可以捕获 接口二进制数据 ,并返回一个 内容 给开发者。

该网络代理的作者虽然得到了一个比较全的数据,但只返回了 body 给使用者,而现在我们需要获取 host 的内容,所以要进行修改源码来获取。

下面是我们调用的代码:

def get_hook_data(data):
    print(data)


p = Proxy()
p.hook(target=get_hook_data)

结果返回:

<html></html>
<html></html>

1. 修改源文件

这个方法应该是绝大部分开发者能想到的办法,由于 python 的第三方库绝大部分都是通过 pip 来安装的,我们可以通过找到 安装路径 的第三方库源码来修改。

例如我们假设上面的 Proxy 的源码安装在了 D:\Env\Project\Lib\site-packages\Proxy ,找到了源码文件 Proxy.py

源码路径
源码路径

将源码的 parse() 方法直接进行修改:

def parse(self, data):
    '''
    模拟 解析二进制数据并转为字典
    :param data:
    :return:
    '''

    result = {
        'host': '127.0.0.1',
        'content_type': 'text/html',
        'body': '<html></html>'
    }

    return {
        'body': result['body'],
        'host': result['host']
    }

现在我们来看看返回结果:

{'body': '<html></html>', 'host': '127.0.0.1'}
{'body': '<html></html>', 'host': '127.0.0.1'}
{'body': '<html></html>', 'host': '127.0.0.1'}
{'body': '<html></html>', 'host': '127.0.0.1'}
  • 优点 :简洁明了,非常直接

  • 缺点 :当我们环境发生改变时,每次都需要修改源码,非常麻烦

  • 2. 继承修改

    继承修改 的方法比较适合大神,为什么这么说呢?假如我们的这个 二进制数据 解析方法非常非常麻烦,没有一定的了解很难解析,那么这个方法将会非常痛苦。

    class MyProxy(Proxy):
        def parse(self, data):
            # 这里需要我们自己重新实现第三方库的逻辑
    
            result = {
                'host': '127.0.0.1',
                'content_type': 'text/html',
                'body': '<html></html>'
            }
            return {
                'body': result['body'],
                'host': result['host']
            }

    我们继承了原来 第三方库 的  ,然后通过继承覆写来修改方法的返回值,现在我们可以通过调用 继承 类来实现需求:

    def get_hook_data(data):
        print(data)
    
    
    p = MyProxy()
    p.hook(target=get_hook_data)

    返回结果:

    {'body': '<html></html>', 'host': '127.0.0.1'}
    {'body': '<html></html>', 'host': '127.0.0.1'}
  • 优点 :不需要修改源码文件

  • 缺点 :当源码逻辑非常复杂时,重新去实现逻辑比较困难;如果源码中存在大量调用其他模块的,需要一模一样 import 过来,工作量比较大

  • 额外提供一个方法来减少 继承 实现难度:我们可以通过复制 源码 文件原有逻辑来进行继承,这样会减少很多工作量。

    3. 猴子补丁

    猴子补丁可以在运行时修改类,通过它我们也可以改写方法,但和继承类似,通过它进行修改也免不了重新实现源码逻辑:

    def my_parse(self, data):
        # 这里需要我们自己重新实现第三方库的逻辑
    
        result = {
            'host': '127.0.0.1',
            'content_type': 'text/html',
            'body': '<html></html>'
        }
        return {
            'body': result['body'],
            'host': result['host']
        }
    
    
    Proxy.parse = my_parse

    正常调用:

    p = Proxy()
    p.hook(target=get_hook_data)

    返回结果:

    {'body': '<html></html>', 'host': '127.0.0.1'}
    {'body': '<html></html>', 'host': '127.0.0.1'}
  • 优点 :不需要修改源码文件

  • 缺点 :缺点和 继承修改 类似

  • 4. 追踪局部变量

    接下来,我们将需要一点 黑魔法 来实现。

    众所周知在 PyCharm 进行断点运行时,可以在断点处来获取 局部和全局变量,那么我们是否可以用代码来做到这一点呢?

    答案是可以,请看代码:

    import sys
    
    
    class VariableTracer:
        def__init__(self):
            # 用来保存局部变量
            self.vars = None
    
        def trace(self, func, *args, **kwargs):
            old_profile = sys.getprofile()
            # 设置新的 profiling 函数为我们自定义函数
            sys.setprofile(self.profiling)
            # 调用需要监听的函数
            func(*args, **kwargs)
            # 将以前的 profiling 函数 更换回去
            sys.setprofile(old_profile)
            returnself.vars
    
        def profiling(self, frame, event, arg):
            # 当方法调用 return 之前的局部变量
            if event == 'return':
                vars: dict = frame.f_locals
                # 保存下来进行返回
                self.vars = {key: value for key, value invars.items()}
    
    
    class MyProxy(Proxy):
        def parse(self, data):
            vars = VariableTracer().trace(super(MyProxy, self).parse, data)
            result = vars['result']
            return {
                'host': result['host'],
                'body': result['body']
            }

    我们通过 sys.setprofile() 来设置一个自定义的 profiling函数,这个函数在以下事件发生时都会被解释器调用:

    1. 函数调用(call):当一个函数被调用时。

    2. 函数返回(return):当一个函数返回时。

    3. 异常抛出(exception):当一个异常被抛出时。

    4. C 函数调用(c_call):当一个 C 函数被调用时(仅适用于某些情况)。

    我们通过被调用的时机去获取局部变量,这样就可以更换返回值结果。

    我们使用自定义类正常调用:

    def get_hook_data(data):
        print(f'hook {data}')
    
    
    p = MyProxy()
    p.hook(target=get_hook_data)

    返回结果:

    {'host': '127.0.0.1', 'body': '<html></html>'}
    {'host': '127.0.0.1', 'body': '<html></html>'}
  • 优点 :不需要修改源码文件和重复实现源码逻辑

  • 缺点 :如果源码耗时复杂,可能会有性能问题

  • 结尾

    修改源码文件逻辑的事情可能发生的频率不是很高,但真正遇到时那就非常糟心,本文使用了四种方式,如果你还有更好的方式请留言告诉我吧。

    如果这篇文章对你有帮助,点个赞让我知道哦!

    作者:Python卡皮巴拉

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python秘籍:轻松修改第三方库源码的终极黑科技

    发表回复