關於Python的源文件編譯看這一篇就夠了


前提概要

Python解釋器版本:3.6.8

操作系統:MacOS

編譯源文件的必要性

在實際的項目部署時,為了保護源代碼,我們通常會在部署項目之前將后端寫好的py文件編譯成pyc文件。

pyc文件是是一種二進制文件,是由py文件經過編譯后生成的文件,是一種byte code。py文件變成pyc文件后,加載的速度有所提高,而且pyc是一種跨平台的字節碼,是由python的虛擬機來執行的,這個是類似於JAVA或者.NET的虛擬機的概念。

pyc的內容,是跟python的版本相關的,不同版本編譯后的pyc文件是不同的,py2編譯的pyc文件,py3版本的python是無法執行的。

當然pyc文件也是可以反編譯的,不同版本編譯后的pyc文件是不同的,根據python源碼中提供的opcode,可以根據pyc文件反編譯出 py文件源碼,不過你可以自己修改python的源代碼中的opcode文件,重新編譯 python從而防止不法分子的破解。

總之,編譯源文件后再去部署項目可以十分有效的保護項目的源代碼不被外人所窺視。

測試的項目

測試的項目目錄結構如下:

本文使用這個項目的目錄結構作為測試的說明。

~/Desktop/pythonCompile                                                                                                                                       
.
├── compile_test.py
├── t3.py
├── test1
│   ├── t1.py
│   └── t2.py
└── test2
    ├── tt1.py
    └── tt2.py
也許這樣看的更清楚

python默認情況下做的編譯

其實Python在模塊導入的過程中會在當前腳本的項目的目錄下自動生成一個名為__pycache__的目錄,在這里面存放被導入模塊的編譯以后的pyc文件。

在上面的文件中我們做一下測試:

test1目錄下的py文件的內容如下:

t1.py:

# -*- coding:utf-8 -*-
def func1():
    print("調用func1...")

t2.py

# -*- coding:utf-8 -*-
from test1.t1 import func1


ret1 = func1()
print("在t2中調用t1的函數后執行...")

test2目錄下的py文件的內容如下:

tt1.py

# -*- coding:utf-8 -*-
def func_tt1():
    print("調用tt1.....")

tt2.py

# -*- coding:utf-8 -*-
from test2.tt1 import func_tt1


func_tt1()
print("調用tt1后執行...")

t3.py文件中的內容如下 —— 注意模塊導入的操作

# -*- coding:utf-8 -*-
import os
import sys


### 將當前目錄放在sys.path中才能正常導入模塊
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)

### 測試
# 導入tt2會執行tt2中的內容
from test2 import tt2

然后我們執行一下t3.py,可以看到在test2中生成了__pycache__這個目錄,讓我們再看看目錄中的內容:

也就是說,在默認情況下,其實Python就為我們做了優化:只要源文件不改變,生成默認的pyc文件后以后會執行編譯好的pyc文件中的內容,這樣大大提高了程序執行的效率!

手動生成pyc文件 ***

Python內置了兩個模塊來幫助我們編譯py源文件,一個是py_compile(編譯單個文件),另外一個是compileall(編譯一個目錄下所有的文件)。下面我們分別來介紹一下他們的使用。

編譯單個文件

默認時的編譯

注意操作前先把上面步驟中的兩個__pycache__目錄與目錄下的文件刪掉!

在根目錄下的compile_test.py文件中加入編譯單個文件的代碼:

import os
import py_compile

current_dir = os.path.dirname(os.path.abspath(__file__))
print(current_dir)

### 編譯單個文件
t2_path = os.path.join(current_dir,"test1","t2.py")
py_compile.compile(t2_path)

執行上面的代碼后我們可以看到:默認情況下會在被編譯文件的同目錄下生成一個名為__pycache__的目錄,里面存放着編譯后的文件:

我們上面的代碼編譯的是t2.py,所以會看到上面的結果。

指定目錄與文件名的編譯方法

當然,我們可以看到上面這種編譯方式不是很理想:一方面實際中我們想把編譯后的文件與源文件放在一起,另外源文件編譯后我們想把它刪掉,以后直接用編譯后的文件跑項目。

我們可以修改一下編譯的代碼:

import os
import py_compile

current_dir = os.path.dirname(__file__)
print(current_dir)

### 編譯單個文件
t2_path = os.path.join(current_dir,"test1","t2.py")
# 指定cfile可以與源文件在一個目錄而不是放在__pycache__目錄,並且指定編譯后文件的名字
py_compile.compile(t2_path,cfile="test1/t2.pyc")

在指定了cfle這個參數后,我們可以達到上面的需求:

py_compile的compile方法的源碼

這里把py_compile.compile的源碼給大家看看,幫助大家理解:

def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1):
    """Byte-compile one Python source file to Python bytecode.

    :param file: The source file name.
    :param cfile: The target byte compiled file name.  When not given, this
        defaults to the PEP 3147/PEP 488 location.
    :param dfile: Purported file name, i.e. the file name that shows up in
        error messages.  Defaults to the source file name.
    :param doraise: Flag indicating whether or not an exception should be
        raised when a compile error is found.  If an exception occurs and this
        flag is set to False, a string indicating the nature of the exception
        will be printed, and the function will return to the caller. If an
        exception occurs and this flag is set to True, a PyCompileError
        exception will be raised.
    :param optimize: The optimization level for the compiler.  Valid values
        are -1, 0, 1 and 2.  A value of -1 means to use the optimization
        level of the current interpreter, as given by -O command line options.

    :return: Path to the resulting byte compiled file.

    Note that it isn't necessary to byte-compile Python modules for
    execution efficiency -- Python itself byte-compiles a module when
    it is loaded, and if it can, writes out the bytecode to the
    corresponding .pyc file.

    However, if a Python installation is shared between users, it is a
    good idea to byte-compile all modules upon installation, since
    other users may not be able to write in the source directories,
    and thus they won't be able to write the .pyc file, and then
    they would be byte-compiling every module each time it is loaded.
    This can slow down program start-up considerably.

    See compileall.py for a script/module that uses this module to
    byte-compile all installed files (or all files in selected
    directories).

    Do note that FileExistsError is raised if cfile ends up pointing at a
    non-regular file or symlink. Because the compilation uses a file renaming,
    the resulting file would be regular and thus not the same type of file as
    it was previously.
    """
    if cfile is None:
        if optimize >= 0:
            optimization = optimize if optimize >= 1 else ''
            cfile = importlib.util.cache_from_source(file,
                                                     optimization=optimization)
        else:
            cfile = importlib.util.cache_from_source(file)
    if os.path.islink(cfile):
        msg = ('{} is a symlink and will be changed into a regular file if '
               'import writes a byte-compiled file to it')
        raise FileExistsError(msg.format(cfile))
    elif os.path.exists(cfile) and not os.path.isfile(cfile):
        msg = ('{} is a non-regular file and will be changed into a regular '
               'one if import writes a byte-compiled file to it')
        raise FileExistsError(msg.format(cfile))
    loader = importlib.machinery.SourceFileLoader('<py_compile>', file)
    source_bytes = loader.get_data(file)
    try:
        code = loader.source_to_code(source_bytes, dfile or file,
                                     _optimize=optimize)
    except Exception as err:
        py_exc = PyCompileError(err.__class__, err, dfile or file)
        if doraise:
            raise py_exc
        else:
            sys.stderr.write(py_exc.msg + '\n')
            return
    try:
        dirname = os.path.dirname(cfile)
        if dirname:
            os.makedirs(dirname)
    except FileExistsError:
        pass
    source_stats = loader.path_stats(file)
    bytecode = importlib._bootstrap_external._code_to_bytecode(
            code, source_stats['mtime'], source_stats['size'])
    mode = importlib._bootstrap_external._calc_mode(file)
    importlib._bootstrap_external._write_atomic(cfile, bytecode, mode)
    return cfile
compile方法的源碼

編譯多個文件

注意操作前先把前面編譯的文件刪掉!

默認時的編譯

默認不加參數的編譯方法如下:

import os
import compileall

current_dir = os.path.dirname(__file__)
print(current_dir)
compile_dir = os.path.join(current_dir,"test2")

### 編譯一個目錄下的所有文件
compileall.compile_dir(compile_dir)

這樣的話會在test2目錄下生成默認的__pycache__存放編譯文件的目錄:

指定目錄與文件名的編譯方法
import os
import compileall

current_dir = os.path.dirname(__file__)
print(current_dir)
compile_dir = os.path.join(current_dir,"test2")

### 編譯一個目錄下的所有文件
# 指定legacy為True后生成的pyc文件會與當前文件放在同一個目錄下並且后綴名字是原來的后綴名字加一個c
compileall.compile_dir(compile_dir,legacy=True)

指定了legacy為True后我們可以看到在同目錄下生成了對應名字的編譯文件:

編譯項目所有文件的方法

也很簡單,直接把項目的目錄放進去就好了:

import os
import compileall

current_dir = os.path.dirname(__file__)
print(current_dir)

### 編譯一個目錄下的所有文件
# 指定legacy為True后生成的pyc文件會與當前文件放在同一個目錄下並且后綴名字是原來的后綴名字加一個c
compileall.compile_dir(current_dir,legacy=True)

結果如下:

compileall.compile_dir的源碼

def compile_dir(dir, maxlevels=10, ddir=None, force=False, rx=None,
                quiet=0, legacy=False, optimize=-1, workers=1):
    """Byte-compile all modules in the given directory tree.

    Arguments (only dir is required):

    dir:       the directory to byte-compile
    maxlevels: maximum recursion level (default 10)
    ddir:      the directory that will be prepended to the path to the
               file as it is compiled into each byte-code file.
    force:     if True, force compilation, even if timestamps are up-to-date
    quiet:     full output with False or 0, errors only with 1,
               no output with 2
    legacy:    if True, produce legacy pyc paths instead of PEP 3147 paths
    optimize:  optimization level or -1 for level of the interpreter
    workers:   maximum number of parallel workers
    """
    ProcessPoolExecutor = None
    if workers is not None:
        if workers < 0:
            raise ValueError('workers must be greater or equal to 0')
        elif workers != 1:
            try:
                # Only import when needed, as low resource platforms may
                # fail to import it
                from concurrent.futures import ProcessPoolExecutor
            except ImportError:
                workers = 1
    files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels,
                      ddir=ddir)
    success = True
    if workers is not None and workers != 1 and ProcessPoolExecutor is not None:
        workers = workers or None
        with ProcessPoolExecutor(max_workers=workers) as executor:
            results = executor.map(partial(compile_file,
                                           ddir=ddir, force=force,
                                           rx=rx, quiet=quiet,
                                           legacy=legacy,
                                           optimize=optimize),
                                   files)
            success = min(results, default=True)
    else:
        for file in files:
            if not compile_file(file, ddir, force, rx, quiet,
                                legacy, optimize):
                success = False
    return success
View Code

腳本方式生成編譯文件 ***

上面的方式仍然有點繁瑣,我們可以使用腳本的方法,指定項目目錄來生成編譯文件:

默認情況

安裝好Python解釋器后直接運行即可:

python3 -m compileall "需要編譯的項目目錄"

比如上面這個目錄的編譯過程:

結果如下: 

指定目錄與文件名的方法

只需要加一個-b參數即可:

python3 -m compileall  -b  "需要編譯的項目目錄"

結果如下:

刪除原來的py文件或者__pycache__目錄:

在項目目錄下執行:

find ./ -name "*.py" |xargs rm -rf
find ./ -name “__pycache__” |xargs rm -rf

結果如下:

這樣,線上部署的代碼就不怕被偷窺了!

編譯后的文件與源文件同時存在時的說明與測試 ***

接下來我們測試一下編譯后的結果。

在項目目錄的下執行t3,可以看到還是能看到結果:

但是問題來了:如果我們把源py文件與編譯后的pyc文件放在一起的話,而且二者的內容不一樣會出現什么情況呢?

我們來測試一下,恢復test2目錄中的tt2.py源文件,並且修改tt2.py中的內容!

 並且修改tt2.py文件中的內容如下:

print("修改tt2中的內容!!!")

然后運行一下看看效果:

我們可以看到:如果源文件與編譯后的文件同時存在的話,在導入模塊時會優先執行源文件中的內容!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM