Đôi khi cần phải chạy tập lệnh trình bao Python/Ruby/PHP từ Node.js. Bài đăng này xem xét các phương pháp hay nhất xung quanh việc tận dụng child_ process.spawn để đóng gói lệnh gọi này trong Node.js/JavaScript.
Mục tiêu ở đây là có một lớp khả năng tương tác giữa Node.js và lớp vỏ bên ngoài. Đây là một giải pháp thay thế nhanh nếu một số phần khác trong hệ thống của bạn không được phát triển bằng JavaScript.
chúng tôi sẽ sử dụng spawn
trên exec
bởi vì chúng ta đang nói về việc truyền dữ liệu và lượng lớn dữ liệu tiềm năng. Để hiểu sự khác biệt giữa child_process.spawn
và child_process.exec
xem “Sự khác biệt giữa spawn và exec của Node.js child_ process”.
Dài và ngắn của nó là sử dụng exec
đối với lượng dữ liệu nhỏ (dưới 200k) bằng giao diện Bộ đệm và spawn
cho số lượng lớn hơn bằng cách sử dụng giao diện luồng.
spawn
có cú pháp dài dòng hơn cho một số trường hợp sử dụng mà chúng ta sẽ xem xét. Việc tích hợp với Ruby/Python/PHP sẽ thuận tiện hơn vì chúng tôi có thể nhận được nhiều dữ liệu hơn một vài dòng văn bản.
Ví dụ đầy đủ github.com/HugoDF/node-run-python.
Các ví dụ sau có 2 phần:
- Phần thực sự chạy lệnh shell, thường là một chức năng được gọi là
run
và - một IIFE (“biểu thức hàm được gọi ngay lập tức”) thực sự gọi nó,
(async () => { await run() }
)(). IIFE này là một mẫu đẹp được kích hoạt bởi async/await (xem Async JS: history, patterns and gotchas) nhưng nó chỉ ở đó cho mục đích minh họa vì nó đại diện cho lời gọi đến góied sp
cuộc gọi awn từ một phần khác của ứng dụng của bạn.
Gọi một lệnh shell và đăng nhập nó
sử dụng spawn
là quá mức cần thiết trong tình huống này vì echo sẽ chỉ trả lại những gì được truyền cho nó.
Ví dụ này khá dễ hiểu và chỉ ra cách sử dụng child_process.spawn
để “bóc vỏ” và đọc lại dữ liệu đó.
spawn
lấy tệp thực thi để gọi làm tham số đầu tiên và tùy chọn một mảng các tùy chọn/tham số cho tệp thực thi làm tham số thứ hai.
const { spawn } = require('child_process');
function run() {
const process = spawn('echo', ['foo']);
process.stdout.on(
'data',
(data) => console.log(data.toString())
);
}
(() => {
try {
run()
// process.exit(0)
} catch (e) {
console.error(e.stack);
process.exit(1);
}
})();
Gọi Python cho phiên bản của nó
Chúng tôi sẽ di chuyển khá nhanh để giới thiệu cách chúng tôi sẽ làm điều gì đó tương tự như trên với Python. Lưu ý lại cách làm --version
được truyền vào bên trong một mảng.
Chúng tôi cũng tạo một trình ghi nhật ký đẹp mắt để phân biệt giữa thiết bị xuất chuẩn và thiết bị xuất chuẩn và liên kết với chúng. Vì spawn trả về một thể hiện có stdout
và stderr
trình phát sự kiện, chúng ta có thể liên kết logOutput
chức năng để 'data'
sự kiện sử dụng .on('data', () => { /* our callback function */
}).
Một mẩu tin thú vị khác là python
--version
xuất phiên bản thành stderr
. Sự không nhất quán xung quanh việc liệu các tệp thực thi *NIX có sử dụng mã thoát, thiết bị xuất chuẩn và thiết bị xuất chuẩn khi thành công/lỗi hay không là một vấn đề khó hiểu mà chúng ta sẽ phải lưu ý khi tích hợp Python/Ruby/khác với Node.js.
const { spawn } = require('child_process')
const logOutput = (name) => (data) => console.log(`[${name}] ${data.toString()}`)
function run() {
const process = spawn('python', ['--version']);
process.stdout.on(
'data',
logOutput('stdout')
);
process.stderr.on(
'data',
logOutput('stderr')
);
}
(() => {
try {
run()
process.exit(0)
} catch (e) {
console.error(e.stack);
process.exit(1);
}
})();
Đầu ra:
$ node run.js
[stderr] Python 2.7.13
Gọi tập lệnh Python từ Node
Bây giờ chúng ta sẽ chạy một tập lệnh Python chính thức (mặc dù nó cũng có thể là Ruby, PHP, shell, v.v.) từ Node.js.
Đây là script.py
nó chỉ đăng xuất argv
(“vectơ đối số”, tức là. ['path/to/executable', /* command line arguments ]
)
import sys
print(sys.argv)
Giống như trong ví dụ trước, chúng ta sẽ chỉ gọi spawn với python
với đường dẫn đến tập lệnh Python (./script.py
) trong tham số thứ hai.
Đây là một vấn đề khác về việc tích hợp các tập lệnh theo cách này. Trong ví dụ này, đường dẫn đến tập lệnh dựa trên thư mục làm việc mà từ đó node
được gọi là.
Tất nhiên, có một cách giải quyết khác là sử dụng path
mô-đun và __dirname
ví dụ như có thể giải quyết một other-script.py
cùng vị trí với lệnh gọi tệp JavaScript/mô-đun Node spawn
sử dụng: require('path').resolve(__dirname, './other-script.py')
.
const { spawn } = require('child_process')
const logOutput = (name) => (data) => console.log(`[${name}] ${data.toString()}`)
function run() {
const process = spawn('python', ['./script.py']);
process.stdout.on(
'data',
logOutput('stdout')
);
process.stderr.on(
'data',
logOutput('stderr')
);
}
(() => {
try {
run()
// process.exit(0)
} catch (e) {
console.error(e.stack);
process.exit(1);
}
})();
Đầu ra:
node run.js
\[stdout\] ['./script.py']
Truyền đối số cho tập lệnh Python từ Node.js bằng cách sử dụng child_ process.spawn
Bước tiếp theo của quá trình tích hợp là có thể chuyển dữ liệu từ mã Node/JavaScript sang tập lệnh Python.
Để làm được điều này, chúng ta sẽ chuyển thêm các đối số shell bằng cách sử dụng mảng đối số (tham số thứ hai cho spawn
).
const { spawn } = require('child_process')
const logOutput = (name) => (data) => console.log(`[${name}] ${data.toString()}`)
function run() {
const process = spawn('python', ['./script.py', 'my', 'args']);
process.stdout.on(
'data',
logOutput('stdout')
);
process.stderr.on(
'data',
logOutput('stderr')
);
}
(() => {
try {
run()
// process.exit(0)
} catch (e) {
console.error(e.stack);
process.exit(1);
}
})();
Của chúng ta script.py
cũng sẽ chỉ đăng xuất argv
ngoại trừ phần tử đầu tiên (là đường dẫn đến tập lệnh).
import sys
print(sys.argv)[1:]
Đây là đầu ra:
node run.js
\[stdout\] ['my', 'args']
Đọc đầu ra child_ process.spawn từ Node.js
Thật tuyệt khi có thể chuyển dữ liệu xuống tập lệnh Python. Chúng tôi vẫn không thể lấy lại dữ liệu từ tập lệnh Python ở định dạng mà chúng tôi có thể tận dụng trong ứng dụng Node.js/JavaScript của mình.
Giải pháp cho vấn đề này là bọc toàn bộ spawn
-calling chức năng thành một Promise. Điều này cho phép chúng tôi quyết định khi nào chúng tôi muốn resolve
hoặc reject
.
Để theo dõi (các) luồng đầu ra của tập lệnh Python, chúng tôi đệm đầu ra theo cách thủ công bằng cách sử dụng các mảng (một cho stdout
và một cái khác cho stderr
).
Chúng tôi cũng thêm một người nghe cho 'exit'
sử dụng spawn().on('exit', (code, signal) => { /* probably call resolve() */
}). Đây là nơi chúng ta sẽ có xu hướng to reso
tôive/rej
ect (các) giá trị của Lời hứa từ Python/Ruby/tập lệnh khác.
const { spawn } = require('child_process')
const logOutput = (name) => (data) => console.log(`[${name}] ${data}`)
function run() {
return new Promise((resolve, reject) => {
const process = spawn('python', ['./script.py', 'my', 'args']);
const out = []
process.stdout.on(
'data',
(data) => {
out.push(data.toString());
logOutput('stdout')(data);
}
);
const err = []
process.stderr.on(
'data',
(data) => {
err.push(data.toString());
logOutput('stderr')(data);
}
);
process.on('exit', (code, signal) => {
logOutput('exit')(`${code} (${signal})`)
resolve(out);
});
});
}
(async () => {
try {
const output = await run()
logOutput('main')(output)
process.exit(0)
} catch (e) {
console.error(e.stack);
process.exit(1);
}
})();
Đầu ra:
node run.js
\[stdout\] ['my', 'args']
\[main\] ['my', 'args']
Xử lý lỗi từ child_ process.spawn
Tiếp theo, chúng ta cần xử lý các lỗi từ tập lệnh Python/Ruby/shell ở cấp độ Node.js/JavaScript.
Cách chính mà tệp thực thi *NIX báo hiệu rằng nó bị lỗi là sử dụng 1
mã thoát. Đó là lý do tại sao .on('exit'
trình xử lý bây giờ thực hiện kiểm tra đối với code === 0
trước khi quyết định giải quyết hay từ chối với (các) giá trị.
const { spawn } = require('child_process')
const logOutput = (name) => (data) => console.log(`[${name}] ${data}`)
function run() {
return new Promise((resolve, reject) => {
const process = spawn('python', ['./script.py', 'my', 'args']);
const out = []
process.stdout.on(
'data',
(data) => {
out.push(data.toString());
logOutput('stdout')(data);
}
);
const err = []
process.stderr.on(
'data',
(data) => {
err.push(data.toString());
logOutput('stderr')(data);
}
);
process.on('exit', (code, signal) => {
logOutput('exit')(`${code} (${signal})`)
if (code === 0) {
resolve(out);
} else {
reject(new Error(err.join('\n')))
}
});
});
}
(async () => {
try {
const output = await run()
logOutput('main')(output)
process.exit(0)
} catch (e) {
console.error('Error during script execution ', e.stack);
process.exit(1);
}
})();
Đầu ra:
node run.js
[stderr] Traceback (most recent call last):
File "./script.py", line 3, in <module>
print(sy.argv)[1:]
NameError: name 'sy' is not defined
Error during script execution Error: Traceback (most recent call last):
File "./script.py", line 3, in <module>
print(sy.argv)[1:]
NameError: name 'sy' is not defined
at ChildProcess.process.on (/app/run.js:33:16)
at ChildProcess.emit (events.js:182:13)
at Process.ChildProcess._handle.onexit (internal/child_process.js:240:12)
Truyền dữ liệu có cấu trúc từ Python/Ruby sang Node.js/JavaScript
Bước cuối cùng để tích hợp đầy đủ giữa các tập lệnh Ruby/Python/PHP/shell và lớp ứng dụng Node.js/JavaScript của chúng tôi là có thể chuyển dữ liệu có cấu trúc trở lại từ tập lệnh cho đến Node.js/JavaScript.
Định dạng dữ liệu có cấu trúc đơn giản nhất có xu hướng khả dụng trong cả Python/Ruby/PHP và Node.js/JavaScript là JSON.
Trong tập lệnh Python, chúng tôi in json.dumps()
đầu ra của một từ điển, xem script.py
:
import sys
import json
send_message_back = {
'arguments': sys.argv[1:],
'message': """Hello,
This is my message.
To the world"""
}
print(json.dumps(send_message_back))
Trong Node, chúng tôi thêm một số logic phân tích cú pháp JSON (sử dụng JSON.parse
) bên trong 'exit'
người xử lý.
Một vấn đề ở điểm này là nếu, ví dụ JSON.parse()
không thành công do JSON được định dạng sai, chúng tôi cần phải khắc phục lỗi đó. Do đó, thử/bắt nơi catch
mệnh đề reject
-s lỗi tiềm ẩn: try { resolve(JSON.parse(out[0])) } catch(e) { reject(e) }
.
const { spawn } = require('child_process')
const logOutput = (name) => (message) => console.log(`[${name}] ${message}`)
function run() {
return new Promise((resolve, reject) => {
const process = spawn('python', ['./script.py', 'my', 'args']);
const out = []
process.stdout.on(
'data',
(data) => {
out.push(data.toString());
logOutput('stdout')(data);
}
);
const err = []
process.stderr.on(
'data',
(data) => {
err.push(data.toString());
logOutput('stderr')(data);
}
);
process.on('exit', (code, signal) => {
logOutput('exit')(`${code} (${signal})`)
if (code !== 0) {
reject(new Error(err.join('\n')))
return
}
try {
resolve(JSON.parse(out[0]));
} catch(e) {
reject(e);
}
});
});
}
(async () => {
try {
const output = await run()
logOutput('main')(output.message)
process.exit(0)
} catch (e) {
console.error('Error during script execution ', e.stack);
process.exit(1);
}
})();
Đầu ra:
node run.js
[stdout] {"message": "Hello,\nThis is my message.\n\nTo the world", "arguments": ["my", "args"]}
[main] Hello,
This is my message.
To the world
Đó là nó! Cảm ơn vì đã đọc 🙂
Tôi đã mở các điểm cố vấn tại https://mentorcruise.com/mentor/HugoDiFrancesco/. Làm điều đó nếu bạn muốn Node.js/JavaScript/cố vấn nghề nghiệp hoặc cảm thấy tự do để tweet cho tôi @hugo__df
Và đọc thêm các bài viết của tôi trên codewithhugo.com