Android 平台的Python——第三方库移植

Android 平台的Python——基础篇(一)
Android 平台的Python——JNI方案(二)
Android 平台的Python——CLE方案实现(三)
Android 平台的Python——第三方库移植
Android 平台的Python——编译Python解释器

之前一段时间一直比较忙,导致很多想研究想写的博客没写,现在有时间正好补充一篇。前面写了三篇关于将Python3嵌入Android项目的博客,后来一直有人留言问怎么移植Python的第三方库,包括说调用标准库报错等等问题,我之前一直以为这些是Python的基本常识,没想到很多人都不知道。之前的三篇博客,大概是写给会一点Python的Android工程师看的,并不是完全定位写给小白看,这样一来我会默认看客已经知道哪些知识,很多细节没有详细解释,另一方面,我写博客的初衷是对自己知识的一个整理,等于是写给自己看的笔记。但是从另一个角度讲,最好的学习方法是把自己学会的知识立刻教授他人,这就是所谓的学习金字塔原理,有兴趣的人可以去了解学习金字塔。那么以后写博客,我会尽可能关注到一些细节,提升博客的水准,这也是对自己的一个升华提高。

Python 的模块加载路径

在讲库移植之前,先提下Python的基础知识,当我们自己编写了一个好用的库时,我们希望每次创建Python工程时都可以引入这个库,那么最简单的办法就是把它拷贝到Python标准库的路径下,但是不建议这样做,弄脏了标准库的目录,更好的做法是放到Python第三方库路径下,作为一个第三方库引入工程。这个路径是
...\python\Lib\site-packages
路径中的python就是你安装的Python的根目录。说到这里,其实就引出了另一个话题,Python的模块加载路径,我们可以在解释器中输出这个路径

import sys
print(sys.path)

以上代码输出的是一个列表,列表里面就是Python解释器去搜索模块的路径,一个一个去找,最后还是找不到,就会报错,说没有这个模块。知道了这一点,我们只需要修改这个列表,把我们自己写的模块的路径加到这个搜索列表,这样就可以成功引入我们自己编写的模块了。这种方法解决了一个问题,就是我想引入某个模块,这个模块或者是我们自己写的或者是从网上下载的某个库源码,但我不想把这些源码拷贝到Python解释器的库路径下面,而是存放在某个目录,更方便管理,比如是个git库目录,这个时候我们就可以在脚本的开始加上以下代码,动态将源码路径添加到Python模块搜索路径列表中

import sys
sys.path.append("my code path")

CLE框架下导入Python第三方库

前面的博客已经讲过了怎么使用CLE框架,在Android项目中嵌入Python解释器,但是有一些细节,很多人还不清楚

  • Android 工程的ibs目录中放的libpython3.4m.so是什么?
    要回答这个问题,首先要了解Python解释器的源码结构。Python解释器源码可以从官网下载,也可以从Github上下载 Python解释器源码
    Android 平台的Python——第三方库移植_第1张图片
    划去一些无关的内容,具体说一下结构
    • Include:包含Python提供的所有头文件,如果需要自己使用C/C++编写自定义模块扩展Python,就需要用到这写头文件
    • Lib: 由Python语言编写的所有标准库
    • Modules:包含了标准库中所有使用C语言编写的模块
    • Parser:对Python代码进行词法分析和语法分析的部分
    • Objects:所有Python的内建对象
    • Python:Python运行的核心。解释器中的Compiler和执行引擎部分
      如果对源码学习感兴趣,推荐一本书《Python源码剖析》,看过之后会受益匪浅,特别是对想自己改写Python解释器的人

那么回到我们的话题,libpython3.4m.so实际上只是Parser、Objects、Python以及一小部分Modules编译出来的动态库,只是提供了Python解释器的核心功能。如有时间,在以后的博客,我会详细讲解,如何手动用NDK,使用Android.mk文件以及Makefile文件,分别在Windows系统和Ubuntu系统上交叉编译出完整的在Android上运行的Python so库。

  • 为什么使用CLE框架集成Python解释器后,有些标准库报找不到错误?
    看博客不细心的人,可能没有注意到一张图,这里面放的so是什么?
    Android 平台的Python——第三方库移植_第2张图片
    我们打开下载的CLE文件starcore_for_android.2.6.0,进入里面的python.files目录下面,一路下去找到一个叫lib-dynload的目录
    Android 平台的Python——第三方库移植_第3张图片
    可以看到,里面放了一堆so库,这个就是上文讲的Modules里面编译出来的Python标准库,这些Python的标准库都是用C语言写的,CLE框架中,将Modules里面的标准库模块都编译成了一个个小的so,这样做的好处就是可以按需集成,我们知道Android的Apk文件都是要尽可能小的,你没有用到的模块,可以不用集成到apk中,否则全部打包到libpython.so中,无端增加了apk大小。

好了,到此铺垫都讲完了。现在具体谈一下装载Python库的思路

  1. 将需要的Python库打包到工程的assets目录下,在适当的时候,将assets目录下的文件都拷贝到手机存储中,这个存储可以是sdcard,也可以是内部的/data/data/package-name下
  2. 在需要引入这些库的时候,使用我们一开始讲的方法,将路径添加到Python的模块搜索列表——sys.path列表,让解释器能搜索到它们。

下面我们以一个实例来具体说明,这次我们需要移植的是爬虫需要用到的两个库,requests和BeautifulSoup,有了这两个库,我们瞬间就能将废旧的Android手机制作成Python爬虫机,老机器焕发新生命,怎么样激不激动?

1.打包库

如果我们本地Python环境中已经安装了这些库,可以直接去Python的库目录打包,因为这些库基本是纯Python代码,是跨平台的,当然,别把Python2.x和Python3.x搞混。如果没有安装,去相关的官网下载它们的源码打包。
Android 平台的Python——第三方库移植_第4张图片
在这里插入图片描述

很多人可能不知道,Python本身就是支持导入zip格式的包的,不信的同学可以自己在本地实验一下,将自己写的库压缩为zip,然后依然可以愉快的import。以上包中,python3.5.zip是纯Python代码的标准库,在CLE里面已经提供了,唯一需要说明的地方,是certifi库为什么没有打包,而是以目录形式提供呢?这里也正是我采坑的地方,之前试验一直报错没有成功移植requests库,就是因为打包了certifi,后来反复测试定位到该包,发现里面有一个cacert.pem文件,不是.py文件,在压缩包中无法被读取,因此只能以文件夹形式提供了。
另外还有一个小点说一下,相信绝大部分人不会犯错,但总有粗心大意的。打包的时候,要打包这个库的源码父目录,就像certifi一样,是打包这个certifi目录,而不是进到certifi里面,选中所有文件压缩。之前一个同事就是犯这种错误,一直和我说不成功。

2.递归拷贝

在工程的assets目录创建python文件夹,将所有包复制进该目录,在app启动的适当时候,调用以下代码拷贝assets中的所有文件到手机存储

		// Extract python files from assets
        AssetExtractor assetExtractor = new AssetExtractor(this);
        assetExtractor.removeAssets("python");
        assetExtractor.copyAssets("python");

        // Get the extracted assets directory
        String pyPath = assetExtractor.getAssetsDataDir() + "python";

AssetExtractor类在Android 平台的Python——JNI方案(二)一文已经提过了,这里再次给出开源库中的
链接 这里只有一点需要特别说明,在刚开始的时候我准备剪裁lib-dynload文件夹提供的C语言部分的Python标准库,结果试验性的放了几个so,一直报各种找不到错误,最后不想浪费时间试错,直接将lib-dynload中的所有so拷贝到了assets/python文件夹,有时间的朋友可以精心剪裁出真正需要的so,减小apk体积。

3.添加库到搜索路径中

还没看过CLE使用的那篇博客,请先浏览CLE的使用一文Android 平台的Python——CLE方案实现(三)
另外需要注意的是在Android清单文件中,网络权限别忘了

protected void init() {
        final String appLib = getApplicationInfo().nativeLibraryDir;
        AsyncTask.execute(new Runnable() {

            @Override
            public void run() {
                loadPy(appLib);
            }
        });
    }

    void loadPy(String appLib){
        // Extract python files from assets
        AssetExtractor assetExtractor = new AssetExtractor(this);
        assetExtractor.removeAssets("python");
        assetExtractor.copyAssets("python");

        // Get the extracted assets directory
        String pyPath = assetExtractor.getAssetsDataDir() + "python";

        try {
            // 加载Python解释器
            System.load(appLib + File.separator + "libpython3.5m.so");
        } catch (Exception e) {
            e.printStackTrace();
        }

        /*----init starcore----*/
        StarCoreFactoryPath.StarCoreCoreLibraryPath = appLib;
        StarCoreFactoryPath.StarCoreShareLibraryPath = appLib;
        StarCoreFactoryPath.StarCoreOperationPath = pyPath;

        StarCoreFactory starcore = StarCoreFactory.GetFactory();
        //用户名、密码 test , 123
        StarServiceClass service = starcore._InitSimple("test", "123", 0, 0);
        mSrvGroup = (StarSrvGroupClass) service._Get("_ServiceGroup");
        service._CheckPassword(false);

        /*----run python code----*/
        mSrvGroup._InitRaw("python35", service);
        StarObjectClass python = service._ImportRawContext("python", "", false, "");
        /* 设置Python模块加载路径 即sys.path.insert() */
        python._Call("import", "sys");
        StarObjectClass pythonSys = python._GetObject("sys");
        StarObjectClass pythonPath = (StarObjectClass) pythonSys._Get("path");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"python3.5.zip");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"requests.zip");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"idna.zip");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"certifi");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"chardet.zip");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"urllib3.zip");
        pythonPath._Call("insert", 0, pyPath+ File.separator +"bs4.zip");
        pythonPath._Call("insert", 0, appLib);
        pythonPath._Call("insert", 0, pyPath);

        python._Set("JavaClass", Log.class);
        service._DoFile("python", pyPath + "/test.py", "");
        Log.d("callpython", "python end");
    }

test.py文件

import imp  #test load path
import requests
from bs4 import BeautifulSoup

def log(content):
    JavaClass.d("formPython",content)


def testGet():
    log('Hello,World from python')
    r = requests.get("https://www.baidu.com/")
    r.encoding ='utf-8'
    bsObj = BeautifulSoup(r.text,"html.parser")
    for node in bsObj.findAll("a"):
        log("---**--- "+node.text)

testGet()

日志:
Android 平台的Python——第三方库移植_第5张图片

在Crytax-NDK的Python中集成第三方库

有了CLE,我为什么仍然执着于Crytax-NDK中的Python解释器了?说实话,我并不是特别喜欢CLE,因为它封装了太多细节,且源码并未开源,具体实现代码不知,性能就无法掌控,特别是无用代码,导入一些非必要的so和jar,增加了apk体积,因为这个框架并不是专门针对python的,还可以集成其他的很多脚本语言到Android中,为了通用性,往往就需要很多对我们来说无用的代码,性能也会有牺牲。但是它的优点也很明确,那就是使用简单,不需要你会Ndk开发,技术成本低。

使用Crytax-NDK实现,具体思路和上面讲的是一样的,直接参看Android 平台的Python——JNI方案(二)一文,然后将需要的第三方库源码打包安装到手机存储,需要注意的地方就是在调用的Python脚本的开始处,加上以下代码,也可以使用其他更优雅的方式,完成这个搜索路径添加,这里只是一个简单的demo代码演示。

import sys
sys.path.append("你拷贝到手机上的路径/assets/python/urllib3.zip")
sys.path.append("你拷贝到手机上的路径/assets/python/chardet.zip")
sys.path.append("你拷贝到手机上的路径/assets/python/certifi")
sys.path.append("你拷贝到手机上的路径/assets/python/idna.zip")

但是,但是……

HTTPSConnectionPool(host='www.baidu.com', port=443): Max retries exceeded with url: / (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.",))

这里有一个极其操蛋的问题,使用requests访问https的地址时,会报错,只能访问http地址。因为Crytax-NDK库的Python解释器编译得有问题,没有支持openssl,真不知道Crytax-NDK的作者怎么想的,由于Crytax-NDK是开源的,我好不容易找到了其源码,查看了他们编译Python解释器的脚本,真让人无语,不能访问https的Python有什么用?
Android 平台的Python——第三方库移植_第6张图片
可以看到,在编译ssl模块时,加了一个OPENSSL_HOME属性控制,即有ssl源码时,就编译这部分,否则跳过,然而Crytax-NDK里面openssl的目录是空的,所以最后生成的Python一系列so中,唯独没有ssl的so。尝试从其他地方拷贝一个ssl的so是不可行的,因为他们的Python解释器里,ssl的属性是没有enable的,你拷贝了解释器也并不会去链接,然并卵,看来只能手动重新编译这个Python解释器了,但是手上没有搭建环境,光环境搭建就得折腾一番,下次博客在写吧,下次的博客我主要讨论一下,自己手工编译解释器,然后运用cython模块编译pyjnius库,实现纯手动在Android搭建一个python.so+ pyjnius.so的环境,实现简便的Java与Python的互操作,有了它,CLE基本可以扔掉了。如果不知道pyjnius,请谷歌。

最后,如果您觉得我的博客对您有用,看过之后,麻烦点个赞,毕竟顶一下又不会怀孕,因为很多人看过之后,也没有一点表示,不管怎么说,写博客既花时间,也耗费一点精力,毕竟也是在分享知识啊,在这个知识付费的时代,免费分享也不易,点个赞,只是鼠标一抖的事而已,谢谢!

关注个人公众号:编程之路从0到1

编程之路从0到1

你可能感兴趣的:(Python3新天地,Android技术)