是否有办法从用户输入中读取单个字符?例如,他们在终端上按下一个键,它就会返回(有点像getch())。我知道Windows中有这个功能,但我想要跨平台的东西。


当前回答

另一种方法:

import os
import sys    
import termios
import fcntl

def getch():
  fd = sys.stdin.fileno()

  oldterm = termios.tcgetattr(fd)
  newattr = termios.tcgetattr(fd)
  newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
  termios.tcsetattr(fd, termios.TCSANOW, newattr)

  oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
  fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)

  try:        
    while 1:            
      try:
        c = sys.stdin.read(1)
        break
      except IOError: pass
  finally:
    termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
  return c

摘自这篇博文。

其他回答

如果我在做一些复杂的事情,我会使用诅咒来读取键。但很多时候,我只是想要一个简单的Python 3脚本,使用标准库,可以读取方向键,所以我这样做:

import sys, termios, tty

key_Enter = 13
key_Esc = 27
key_Up = '\033[A'
key_Dn = '\033[B'
key_Rt = '\033[C'
key_Lt = '\033[D'

fdInput = sys.stdin.fileno()
termAttr = termios.tcgetattr(0)

def getch():
    tty.setraw(fdInput)
    ch = sys.stdin.buffer.raw.read(4).decode(sys.stdin.encoding)
    if len(ch) == 1:
        if ord(ch) < 32 or ord(ch) > 126:
            ch = ord(ch)
    elif ord(ch[0]) == 27:
        ch = '\033' + ch[1:]
    termios.tcsetattr(fdInput, termios.TCSADRAIN, termAttr)
    return ch

ActiveState的配方似乎包含一个小错误,“posix”系统,防止Ctrl-C中断(我使用Mac)。如果我把下面的代码放在我的脚本:

while(True):
    print(getch())

我将永远无法用Ctrl-C终止脚本,我必须杀死我的终端才能退出。

我认为下面这句话是原因,而且它也太残酷了:

tty.setraw(sys.stdin.fileno())

除此之外,tty包实际上并不需要,termios就足以处理它了。

下面是改进的代码,为我工作(Ctrl-C将中断),与额外的getche函数,当你输入的字符回显:

if sys.platform == 'win32':
    import msvcrt
    getch = msvcrt.getch
    getche = msvcrt.getche
else:
    import sys
    import termios
    def __gen_ch_getter(echo):
        def __fun():
            fd = sys.stdin.fileno()
            oldattr = termios.tcgetattr(fd)
            newattr = oldattr[:]
            try:
                if echo:
                    # disable ctrl character printing, otherwise, backspace will be printed as "^?"
                    lflag = ~(termios.ICANON | termios.ECHOCTL)
                else:
                    lflag = ~(termios.ICANON | termios.ECHO)
                newattr[3] &= lflag
                termios.tcsetattr(fd, termios.TCSADRAIN, newattr)
                ch = sys.stdin.read(1)
                if echo and ord(ch) == 127: # backspace
                    # emulate backspace erasing
                    # https://stackoverflow.com/a/47962872/404271
                    sys.stdout.write('\b \b')
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, oldattr)
            return ch
        return __fun
    getch = __gen_ch_getter(False)
    getche = __gen_ch_getter(True)

引用:

https://pypi.python.org/pypi/getch

如果你只是想按住屏幕,这样你就可以在终端上看到结果

input()

在代码的末尾,它将保存屏幕

我相信这是一个最优雅的解决方案。

import os

if os.name == 'nt':
    import msvcrt
    def getch():
        return msvcrt.getch().decode()
else:
    import sys, tty, termios
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    def getch():
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

然后在代码中使用它:

if getch() == chr(ESC_ASCII_VALUE):
    print("ESC!")

另一个回答中的一个评论提到了cbreak模式,这对Unix实现很重要,因为您通常不希望^C (KeyboardError)被getchar使用(当您将终端设置为原始模式时,它会被使用,就像大多数其他回答所做的那样)。

另一个重要的细节是,如果您希望读取一个字符而不是一个字节,您应该从输入流中读取4个字节,因为这是UTF-8 (Python 3+)中单个字符所包含的最大字节数。对于键盘箭头等多字节字符,只读取一个字节将产生意想不到的结果。

下面是我修改后的Unix实现:

import contextlib
import os
import sys
import termios
import tty


_MAX_CHARACTER_BYTE_LENGTH = 4


@contextlib.contextmanager
def _tty_reset(file_descriptor):
    """
    A context manager that saves the tty flags of a file descriptor upon
    entering and restores them upon exiting.
    """
    old_settings = termios.tcgetattr(file_descriptor)
    try:
        yield
    finally:
        termios.tcsetattr(file_descriptor, termios.TCSADRAIN, old_settings)


def get_character(file=sys.stdin):
    """
    Read a single character from the given input stream (defaults to sys.stdin).
    """
    file_descriptor = file.fileno()
    with _tty_reset(file_descriptor):
        tty.setcbreak(file_descriptor)
        return os.read(file_descriptor, _MAX_CHARACTER_BYTE_LENGTH)