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


当前回答

TL;DR:这是你的无依赖跨平台最大密度复制面糊

我知道我一直在找☝️。你从谷歌来到这里,想要一些不需要pip安装这个和那个就能工作的东西?我相当肯定这个解决方案将继续工作很长一段时间。

示例使用

>>> getch_but_it_actually_works() # just normal key like a
'a'

>>> getch_but_it_actually_works() # a but its shift or capslock
'A'

>>> getch_but_it_actually_works() # just bare enter
'\r'

>>> getch_but_it_actually_works() # literal ESC key
'\x1b'

>>> getch_but_it_actually_works() # one of the arrow keys on linux
'\x1b[A'

>>> getch_but_it_actually_works() # one of the arrow keys on windows
'àK'

>>> getch_but_it_actually_works() # some really obscure key-combo. still works.
'\x1b[19;6~'

跨平台解决方案,无外部依赖

滚动到更详细的答案在结束理智的缩进和评论。这是最大密度预览,便于复制粘贴。只需调用getch_but_it_actually_works()

import os
def _read_one_wide_char_win(): return msvcrt.getwch()
def _char_can_be_escape_win(char): return True if char in ("\x00", "à") else False
def _dump_keyboard_buff_win():
    try: msvcrt.ungetwch("a")
    except OSError: return msvcrt.getwch()
    else: _ = msvcrt.getwch(); return ""
def _read_one_wide_char_nix():
    old_settings = termios.tcgetattr(sys.stdin.fileno()); tty.setraw(sys.stdin.fileno())
    wchar = sys.stdin.read(1)
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings); return wchar
def _char_can_be_escape_nix(char): return True if char == "\x1b" else False
def _dump_keyboard_buff_nix():
    old_settings = termios.tcgetattr(sys.stdin.fileno())
    tty.setraw(sys.stdin.fileno()); os.set_blocking(sys.stdin.fileno(), False)
    buffer_dump = ""
    while char := sys.stdin.read(1): buffer_dump += char
    os.set_blocking(sys.stdin.fileno(), True); termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
    if buffer_dump: return buffer_dump
    else: return ""
if os.name == "nt":
    import msvcrt
    read_one_wdchar, char_can_escape, dump_key_buffer = _read_one_wide_char_win, _char_can_be_escape_win, _dump_keyboard_buff_win
if os.name == "posix":
    import termios, tty, sys
    read_one_wdchar, char_can_escape, dump_key_buffer = _read_one_wide_char_nix, _char_can_be_escape_nix, _dump_keyboard_buff_nix
def getch_but_it_actually_works():
    wchar = read_one_wdchar()
    if char_can_escape(wchar): dump = dump_key_buffer(); return wchar + dump
    else: return wchar


答案很长,代码带有注释和合理的缩进

这里是所有评论的长答案。仍然没有依赖关系。

这很可能在linux和windows上工作很长一段时间。没有外部依赖,只有内置。

它还将处理边缘情况,如敲击方向键或一些模糊的东西,如<ctrl + shift + f12>,这将在linux和windows上产生很长的ANSI转义序列。它将捕获诸如<ctrl+x>或<ctrl+z>或tab或F1-12作为单个输入

这些年来,我已经回到这个帖子上几十次了,所以现在是时候把两分钱和利息还给我了。下面是完整的注释代码。

这个例子有点长,但您可以跳过阅读大部分内容。相关的位在最后,你可以复制粘贴整个东西。


import os

def _read_one_wide_char_win():
    """Wait keyhit return chr. Get only 1st chr if multipart key like arrow"""
    return msvcrt.getwch()

def _char_can_be_escape_win(char):
    """Return true if char could start a multipart key code (e.g.: arrows)"""
    return True if char in ("\x00", "à") else False # \x00 is null character

def _dump_keyboard_buff_win():
    """If piece of multipart keycode in buffer, return it. Else return None"""
    try:                       # msvcrt.kbhit wont work with msvcrt.getwch
        msvcrt.ungetwch("a")   # check buffer status by ungetching wchr
    except OSError:            # ungetch fails > something in buffer so >
        return msvcrt.getwch() # return the buffer note: win multipart keys
    else:                      # are always 2 parts. if ungetwch does not fail
        _ = msvcrt.getwch()    # clean up and return empty string
        return ""

def _read_one_wide_char_nix():
    """Wait keyhit return chr. Get only 1st chr if multipart key like arrow"""
    old_settings = termios.tcgetattr(sys.stdin.fileno()) # save settings
    tty.setraw(sys.stdin.fileno()) # set raw mode to catch raw key w/o enter
    wchar = sys.stdin.read(1)
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
    return wchar

def _char_can_be_escape_nix(char):
    """Return true if char could start a multipart key code (e.g.: arrows)"""
    return True if char == "\x1b" else False # "\x1b" is literal esc-key

def _dump_keyboard_buff_nix():
    """If parts of multipart keycode in buffer, return them. Otherwise None"""
    old_settings = termios.tcgetattr(sys.stdin.fileno()) # save settings
    tty.setraw(sys.stdin.fileno()) # raw to read single key w/o enter
    os.set_blocking(sys.stdin.fileno(), False) # dont block for empty buffer
    buffer_dump = ""
    while char := sys.stdin.read(1):
        buffer_dump += char
    os.set_blocking(sys.stdin.fileno(), True) # restore normal settings
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old_settings)
    if buffer_dump:
        return buffer_dump
    else:
        return ""

if os.name == "nt":
    import msvcrt
    read_one_wdchar = _read_one_wide_char_win
    char_can_escape = _char_can_be_escape_win
    dump_key_buffer = _dump_keyboard_buff_win
if os.name == "posix":
    import termios
    import tty
    import sys
    read_one_wdchar = _read_one_wide_char_nix
    char_can_escape = _char_can_be_escape_nix
    dump_key_buffer = _dump_keyboard_buff_nix


def getch_but_it_actually_works():
    """Returns a printable character or a keycode corresponding to special key
    like arrow or insert. Compatible with windows and linux, no external libs
    except for builtins. Uses different builtins for windows and linux.

    This function is more accurately called:
    "get_wide_character_or_keycode_if_the_key_was_nonprintable()"

    e.g.:
        * returns "e" if e was pressed
        * returns "E" if shift or capslock was on
        * returns "x1b[19;6~'" for ctrl + shift + F8 on unix

    You can use string.isprintable() if you need to sometimes print the output
    and sometimes use it for menu control and such. Printing raw ansi escape
    codes can cause your terminal to do things like move cursor three rows up.

    Enter will return "\ r" on all platforms (without the space seen here)
    as the enter key will produce carriage return, but windows and linux
    interpret it differently in different contexts on higher level
    """
    wchar = read_one_wdchar()    # get first char from key press or key combo
    if char_can_escape(wchar):   # if char is escapecode, more may be waiting
        dump = dump_key_buffer() # dump buffer to check if more were waiting.
        return wchar + dump      # return escape+buffer. buff could be just ""
    else:                        # if buffer was empty then we return a single
        return wchar             # key like "e" or "\x1b" for the ESC button

其他回答

(目前)排名第一的答案(带有ActiveState代码)过于复杂。当仅仅一个函数就足够了时,我不认为有理由使用类。下面是两个实现,它们实现了相同的功能,但代码可读性更强。

这两个实现:

可以在python2或python3中正常工作 可以在Windows, OSX和Linux上工作 只读取一个字节(即,它们不等待换行符) 不要依赖任何外部库 是自包含的(没有函数定义之外的代码)

版本1:易读且简单

def getChar():
    try:
        # for Windows-based systems
        import msvcrt # If successful, we are on Windows
        return msvcrt.getch()

    except ImportError:
        # for POSIX-based systems (with termios & tty support)
        import tty, sys, termios  # raises ImportError if unsupported

        fd = sys.stdin.fileno()
        oldSettings = termios.tcgetattr(fd)

        try:
            tty.setcbreak(fd)
            answer = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)

        return answer

版本2:避免重复导入和异常处理:

[编辑]我错过了ActiveState代码的一个优点。如果您计划多次读取字符,该代码可以避免在类unix系统上重复Windows导入和ImportError异常处理的成本(可以忽略不计)。虽然你可能应该更关心代码的可读性,而不是可以忽略的优化,这里有一个替代方案(它类似于Louis的答案,但getChar()是自包含的),它的功能与ActiveState代码相同,更可读:

def getChar():
    # figure out which function to use once, and store it in _func
    if "_func" not in getChar.__dict__:
        try:
            # for Windows-based systems
            import msvcrt # If successful, we are on Windows
            getChar._func=msvcrt.getch

        except ImportError:
            # for POSIX-based systems (with termios & tty support)
            import tty, sys, termios # raises ImportError if unsupported

            def _ttyRead():
                fd = sys.stdin.fileno()
                oldSettings = termios.tcgetattr(fd)

                try:
                    tty.setcbreak(fd)
                    answer = sys.stdin.read(1)
                finally:
                    termios.tcsetattr(fd, termios.TCSADRAIN, oldSettings)

                return answer

            getChar._func=_ttyRead

    return getChar._func()

使用上述getChar()版本的示例代码:

from __future__ import print_function # put at top of file if using Python 2

# Example of a prompt for one character of input
promptStr   = "Please give me a character:"
responseStr = "Thank you for giving me a '{}'."
print(promptStr, end="\n> ")
answer = getChar()
print("\n")
print(responseStr.format(answer))

两个答案中逐字引用的ActiveState配方是过度设计的。它可以归结为:

def _find_getch():
    try:
        import termios
    except ImportError:
        # Non-POSIX. Return msvcrt's (Windows') getch.
        import msvcrt
        return msvcrt.getch

    # POSIX system. Create and return a getch that manipulates the tty.
    import sys, tty
    def _getch():
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

    return _getch

getch = _find_getch()

这是NON-BLOCKING,读取一个键并将其存储在keypress.key中。

import Tkinter as tk


class Keypress:
    def __init__(self):
        self.root = tk.Tk()
        self.root.geometry('300x200')
        self.root.bind('<KeyPress>', self.onKeyPress)

    def onKeyPress(self, event):
        self.key = event.char

    def __eq__(self, other):
        return self.key == other

    def __str__(self):
        return self.key

在你的程序中

keypress = Keypress()

while something:
   do something
   if keypress == 'c':
        break
   elif keypress == 'i': 
       print('info')
   else:
       print("i dont understand %s" % keypress)

另一个回答中的一个评论提到了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)

这段代码,基于这里,将正确地引发KeyboardInterrupt和EOFError如果按下Ctrl+C或Ctrl+D。

应该在Windows和Linux上工作。OS X版本可从原始源代码获得。

class _Getch:
    """Gets a single character from standard input.  Does not echo to the screen."""
    def __init__(self):
        try:
            self.impl = _GetchWindows()
        except ImportError:
            self.impl = _GetchUnix()

    def __call__(self): 
        char = self.impl()
        if char == '\x03':
            raise KeyboardInterrupt
        elif char == '\x04':
            raise EOFError
        return char

class _GetchUnix:
    def __init__(self):
        import tty
        import sys

    def __call__(self):
        import sys
        import tty
        import termios
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch


class _GetchWindows:
    def __init__(self):
        import msvcrt

    def __call__(self):
        import msvcrt
        return msvcrt.getch()


getch = _Getch()