Windows 下 Node 执行 exec 返回值乱码的解决方案

本文介绍了在 Windows 下,如何使用 Node 得到正确的 exec 输出编码而无需手动指定输出字符集。

前言

在 node child_process模块中,允许使用 exec 来开启一个子进程,执行特定的命令,在 Windows 上,如果命令输出有中文, 常常会出现执行命令后的 stdout 为乱码,其原因是简体中文的 Windows 系统默认使用的编码方式是 CP936 ,而 Nodejs 尝试直接使用 UTF8 来进行输出。

其他解决方法

目前网上有很多解决办法,比较常见的有两种,一个是执行命令时提前指定好编码,使用 chcp 65001 使 shell 切换到 UTF8 字符集,但是这个命令并不是同步执行的,很多情况下难以兼容,插入到命令中使用起来也不方便。
第二种方法是做后置处理,在exec 时传递 {encoding:"buffer"} 使用 buffer 输出,然后在回调函数中使用 iconv decode 解码,但是这个方法需要提前知道当前的编码方式。

有一些地方提到可以使用 JsChardet 这个包来做检查,这个包在字符量较大的情况下比较准确,但是还是偶尔会出现准确率不高的情况,主要的 bad case 出现在小部分中文 + 大部分英文的情况,会识别成其他语种。

一种比较易于实现的解决方案

在本例中,我尝试使用 powershell 读取当前 QQ音乐 的标题,用户的设备环境比较单一,基本可以确认是在 windows 下,所以可能使用的字符集编码也比较有限。因此可以通过预先埋设探测字符的方式来做检查。

在命令行中输出一串固定的中文字符,然后遍历常见的中文字符集表,尝试解码,直到某个字符集能够正确解码出目标字符即可。

通过这种方法能够极大的提高检查和识别速度,由于环境也比较单一,因此不可能出现把中文解码为泰语的情况,识别率也能得到保证。

这是上面一种埋值+探测结合的方法,比起 chcp 65001 ,只是埋一个字符串对于执行命令来说成本比较低,不需要关注 chcp的执行状态,而相对于 JsChardet ,能够缩小字符集的范围,减小 bad case 的出现几率。

总结

该方案只是对于 乱码问题的解决方法之一,比起上面两个方法,都会显得并不雅观,只是减小了命令编写和识别的难度,而且在执行前后都需要做很多的处理,难以作为一个通用的解决方案来使用。因此该方案仅在开发中文 Windows 下的程序时,可以作为一个相对简单的处理方案。

About

代码开源协议 :MIT

本文使用 powershell 检查 QQ 音乐的代码

exec(`powershell.exe  -ExecutionPolicy Bypass "Get-Process QQMusic | Select-Object MainWindowTitle  | Format-Wide | Out-String"`)

本文相关代码

import {exec as execute} from 'child_process'
import iconv from 'iconv-lite'

/**
 * 使用特定的标记字符串来辅助解码,辨别exec的输出编码
 */
const chineseMark = '$中文标记$'
/**
 * @description 执行原生命令
 * @param {string} command 命令
 * @param {object} options exec 的选项
 * @param {number} timeout 超时时间
 */
export function exec (command, options = null, timeout = 300) {
  options = {
    encoding: 'buffer',
    windowsHide: true, // 默认设置为隐藏子窗口
    ...options
  }
  let childProcess = null
  let exitFlag = false
  return Promise.race([
    new Promise((resolve, reject) => {
      try {
        childProcess = execute(command, options, (error, stdout, stderr) => {
          if (error) {
            reject(error)
          } else {
            // Decode Buffer
            if (Buffer.isBuffer(stdout)) {
              let charset = ['cp936', 'utf8'].find((charset) => {
                return ~iconv.decode(stdout, charset).indexOf(chineseMark)
              })
              if (charset) {
                [stdout, stderr] = [stdout, stderr].map(v => iconv.decode(v, charset))
                stdout = stdout.replace(chineseMark, '') // 移除标记
              } else {
                [stdout, stderr] = [stdout, stderr].map(v => v.toString('utf8'))
              }
              // 尝试检查编码
            }
            resolve(stdout, stderr)
          }
        })
      } catch (e) {
        reject(e)
      } finally {
        exitFlag = true
      }
    }),
    new Promise((resolve, reject) => {
      setTimeout(() => {
        if (!exitFlag) {
          // 子进程未退出,手动结束
          childProcess && childProcess.kill()
          reject(new Error('EXEC TIMED OUT'))
        } else {
          resolve()
        }
      }, Math.max(timeout, 1000))
    })
  ])
}
/**
 * @description 获取窗口标题
 */
export function getWindows () {
  // return exec('echo hello你好啊' + chineseMark)
  return exec(`powershell.exe  -ExecutionPolicy Bypass "Get-Process QQMusic | Select-Object MainWindowTitle  | Format-Wide | Out-String | &{$input+'${chineseMark}'}"`)
}