2022年1月7日 星期五

Python 學習筆記 : 在 Python 中執行 Javascript 的方法 (一)

昨天去母校圖書館拿 NLP 預約書時在架上找到下面這本書 : 



Source : 博客來


此書作者精通 Python, Javascript, 以及 Excel VBA 這三種語言, 並交叉用於網路爬蟲專案中, 是我讀過的爬蟲書籍裡技術含量最高的一本書 (所以這是進階的書, 初學者看起來會有點霧煞煞). 但我只對 Javascript 與 Python 有興趣, 因為我一直對 VB 與 VBA 語法很感冒. 書中第 4-2 節談到如何在 Python 程式調用 Javascript 很是新奇, 所以就作了如下的測試.

書中說在 Python 中處理 Javascript 程式碼有四種方式, 但我認為其中 Node.js 不算, Node.js 使用 Chrome V8 引擎直接將 Javascript 程式碼轉成機器碼, 是獨立於瀏覽器之外的 Javascript 執行環境, 如果有安裝 Node.js 的話, 通常是 Pyexecjs 等套件所使用的執行環境. 

資將其餘三種方式摘要整理如下 :
  • Pyexecjs :
    使用本機 Javascript 執行環境執行 Javascript 程式碼, 不須安裝 Javascript 套件, 是最常用的方法, 但因為要啟動執行環境, 故執行效能較差. 
  • PyV8 :
    此套件將 Chrome V8 引擎用 Python 封裝, 故不需要啟動 Javascript 執行環境, 執行效能佳, 但目前僅支援 Python 2.6 與 3.3 版.  
  • Js2Py :
    此為純 Python 直譯器, 可將 Javascript 程式碼轉換為 Python 程式碼執行, 不需要啟動 Javascript 執行環境, 但對於易混淆的 Javascript 程式碼, 轉換時容易發生錯誤. 
此書只介紹了 Pyexecjs 用法, 而下面這篇則三種都有 :



一. 使用 Pyexecjs 套件 : 

Pyexecjs 是第三方套件, 使用前須用 pip 指令安裝 : 

pip install PyExecJS

C:\Users\User>pip install PyExecJS    
Collecting PyExecJS
  Downloading PyExecJS-1.5.1.tar.gz (13 kB)
  Preparing metadata (setup.py) ... done
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from PyExecJS) (1.12.0)
Building wheels for collected packages: PyExecJS
  Building wheel for PyExecJS (setup.py) ... done
  Created wheel for PyExecJS: filename=PyExecJS-1.5.1-py3-none-any.whl size=14588 sha256=f9b0c30a65095f22b2a69d58812f397d25548e2dcc971cbc76c566b6c16605ea
  Stored in directory: c:\users\user\appdata\local\pip\cache\wheels\9a\ee\03\da5c0b4a8c13362beeb844eb913bbe58a89bde1de2b9157007
Successfully built PyExecJS
Installing collected packages: PyExecJS
Successfully installed PyExecJS-1.5.1

可見 Pyexecjs 套件並不大, 只有 13KB 左右, 相依套件也只有 six 而已. 

安裝好後即可匯入使用了, 注意, 要匯入的模組名稱為 execjs, 不是套件名稱 pyexecjs :

>>> import execjs    
>>> type(execjs)    
<class 'module'>   

用 dir() 檢視模組內容 : 

>>> dir(execjs)    
['AbstractRuntime', 'Error', 'ExternalRuntime', 'ProgramError', 'RuntimeError', 'RuntimeUnavailableError', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_abstract_runtime', '_abstract_runtime_context', '_exceptions', '_external_runtime', '_json2', '_misc', '_pyv8runtime', '_runner_sources', '_runtimes', 'compile', 'division', 'eval', 'exec_', 'execjs', 'get', 'get_from_environment', 'register', 'runtime_names', 'runtimes', 'unicode_literals', 'with_statement']

可用 eval() 過濾掉底線開頭的成員, 這樣更能清楚看出成員是函式或類別 : 

>>> members=dir(execjs)   
>>> for mbr in members:            # 走訪 execjs 模組成員
    obj=eval('execjs.' + mbr)         # 用 eval() 求值取得 execjs 成員之參考
    if not mbr.startswith('_'):        # 走訪所有不是 "_" 開頭的成員
        print(mbr, type(obj))  
        
AbstractRuntime <class 'abc.ABCMeta'>
Error <class 'type'>
ExternalRuntime <class 'abc.ABCMeta'>
ProgramError <class 'type'>
RuntimeError <class 'type'>
RuntimeUnavailableError <class 'type'>
compile <class 'function'> 
division <class '__future__._Feature'>
eval <class 'function'>
exec_ <class 'function'>
execjs <class 'module'>
get <class 'function'>
get_from_environment <class 'function'>
register <class 'function'>
runtime_names <class 'module'>
runtimes <class 'function'>
unicode_literals <class '__future__._Feature'>
with_statement <class '__future__._Feature'>

其中 get() 函式則用來取得外部 Javascrript 執行環境 ExternalRuntime 物件, 例如 : 

>>> ext_runtime=execjs.get()    
>>> type(ext_runtime)    
<class 'execjs._external_runtime.ExternalRuntime'>   
>>> ext_runtime.name   
'Node.js (V8)'

因為我的筆電之前就已安裝了 Node.js, 所以 execjs 模組就以 Node.js 裡面的 V8 引擎來執行 Javascript 程式碼. 

execjs 模組中最主要的是用來將 Javascript 程式碼編譯成 Python 程式碼的 compile() 函式, 只要將 Javascript 程式碼字串傳入 compile(), 它便會傳回一個編譯過後的 Context 物件, 例如 : 

>>> jscode="""
function hello(msg='World') {         # Javascript 函式
  return 'Hello ' + msg;                     # 傳回字串
  } 
function sum(a, b) {                          # Javascript 函式
  return a + b                                     # 傳回數值
  }  
var a=1, b=2;                                      # 定義全域變數 a, b
"""  
>>> ctx=execjs.compile(jscode)      
>>> type(ctx)    
<class 'execjs._external_runtime.ExternalRuntime.Context'>   

同樣用內建函式 dir() 與 eval() 來檢視 Context 物件成員 : 

>>> members=dir(ctx)     
>>> for mbr in members:              # 走訪 Context 物件成員
    obj=eval('ctx.' + mbr)                # 用 eval() 求值取得 Context 成員之參考
    if not mbr.startswith('_'):          # 走訪所有不是 "_" 開頭的成員
        print(mbr, type(obj))  
        
call <class 'method'>   
eval <class 'method'>   
exec_ <class 'method'>
is_available <class 'method'>

會用到的就是 call() 與 eval() 這兩個方法, 其中 call() 用來呼叫 Javascript 函式, 其語法如下 :

ctx.call(函式名稱, 參數1, 參數2, ...)    

第一參數為要呼叫的 Javascript 函式名稱字串, 後面跟著要傳入的參數列. eval() 方法用來取得 Javascript 程式碼中的全域變數, 其語法如下 :

ctx.eval(變數名稱)    

其傳入參數為 Javascript 程式碼中的變數名稱字串.

例如 : 

>>> ctx.call('hello')                   # 無參數呼叫 hello() 函式
'Hello World'
>>> ctx.call('hello', 'Tony')      # 帶參數呼叫 hello() 函式
'Hello Tony'   
>>> a=ctx.eval('a')                    # 取得 Javascript 全域變數 a 之值
>>> print(a)    
1
>>> b=ctx.eval('b')                   # 取得 Javascript 全域變數 b 之值
>>> print(b)    
2
>>> ctx.call('sum', a, b)           # 呼叫 sum() 函式
3

此處分別測試不帶參數與有帶參數呼叫 Javascript 中的 hello() 函式, 不帶參數時會以預設值 'World' 代替. 另外用 eval() 方法分別取得 Javascript 程式碼中的全域變數 a, b, 並用它們來呼叫 sum() 函式, 可見都能傳回預期的結果. 

沒有留言 :