如何从用户那里读取单个字符?

How to read a single character from the user?

提问人:Evan Fosmark 提问时间:2/4/2009 最后编辑:martineauEvan Fosmark 更新时间:2/23/2023 访问量:365017

问:

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

python 输入

评论

2赞 A. Roy 9/9/2018
在Windows上,我遇到了与这个问题相同的问题。解决方案是将 替换为 ,如上所述。msvcrt.getchmsvcrt.getwch
0赞 Petr Mach 12/17/2019
解决方案是安装 getch 模块“pip install getch”。对于 Python2,请使用命令“pip2 install files.pythonhosted.org/packages/56/f7/...”。此解决方案也适用于 Termux (Android)。
0赞 ilon 10/28/2021
最简单的解决方案是使用 sshkeyboard。它比 getch 需要更少的编码,并且它是一个跨平台的解决方案。
1赞 user938883 1/16/2022
我简直不敢相信所有这些复杂的答案。在 Ruby 中:就是这样。input = STDIN.getch
0赞 Carl Smotricz 3/15/2022
@user93883 NameError:未定义全局名称“STDIN”。也许这毕竟不是那么简单。

答:

236赞 tehvan 2/4/2009 #1

以下是 ActiveState Recipes 网站的链接,其中说明了如何在 Windows、Linux 和 OSX 中读取单个字符:

    在 Windows 和 Unix 上从 stdin 读取类似 getch() 的无缓冲字符

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): return self.impl()


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

    def __call__(self):
        import sys, tty, 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()

评论

25赞 John Mulder 2/4/2009
代码似乎足够短,你可以把它包括在内,但+1可以这么快找到一个好的(跨平台)答案。
5赞 Phlya 3/30/2013
它是否能很好地处理非拉丁字母(例如西里尔字母)?我对此有疑问,无法弄清楚这是否是我的错误。
8赞 Seismoid 9/22/2014
我不喜欢像某种 if 语句那样使用异常的方式;为什么不调用 platform.system() 来检查操作系统?ImportError
11赞 dirkjot 4/21/2015
@Seismoid:请求原谅通常被认为更好,请参阅 stackoverflow.com/questions/12265451/...
4赞 Display Name 9/14/2015
在OS X上不起作用:“old_settings = termios.tcgetattr(fd)” “termios.error: (25, '设备不适用的ioctl')”
96赞 Yuval Adam 2/4/2009 #2
sys.stdin.read(1)

基本上会从 STDIN 读取 1 个字节。

如果您必须使用不等待的方法,您可以按照上一个答案中的建议使用此代码:\n

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): return self.impl()


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

    def __call__(self):
        import sys, tty, 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()

(摘自 http://code.activestate.com/recipes/134892/)

评论

56赞 Evan Fosmark 2/4/2009
我觉得 sys.stdin.read(1) 等待 \n 很奇怪,哈哈。不过,感谢您的提交。
3赞 chryss 2/4/2009
一个字符还是一个字节?那是不一样的。
4赞 John La Rooy 10/13/2009
@Evan,这是因为 python 默认处于行缓冲模式
5赞 Tony Delroy 2/21/2014
@EvanFosmark: 不一定是 sys.stdin.read(1) 等待 \n,而是终端程序决定何时将其他字符发送到您的程序,直到它看到 '\n' 时才写入它们 - 否则您如何能够按退格键并更正您正在键入的内容?(严肃的答案是 - 教 Python 程序实现行控制、保留缓冲区、处理退格,但这是一个不同的世界,你可能不想在“读取字符”时买账,并且可能会使你的行处理与系统上的所有其他程序不同。
2赞 vaultah 9/27/2015
@Seismoid EAFP
22赞 Tyler 8/31/2011 #3

另一种方法:

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

来自这篇博文

评论

0赞 4/7/2017
似乎对我不起作用 - 调用后立即返回空字符串。在使用 Python 3.6 的 Linux 上。
2赞 Chris Gregg 2/5/2019
@Marein 如果要阻止它(等待输入),请删除 .否则,你可以把它放在一个循环中(最好在循环中睡一会儿,以防止旋转)。| os.O_NONBLOCK
1赞 Anonymous 1/2/2020
在 Python 中,最好使用 .while Truewhile 1
13赞 kiri 1/1/2014 #4

此代码基于此处,如果按下 + 或 +,则将正确引发 KeyboardInterrupt 和 EOFError。CtrlCCtrlD

应该可以在 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()

评论

0赞 Berry Tsakala 1/4/2022
它支持超越 ASCII 的 Unicode(这是其他一些解决方案所关注的)(它可以在 linux 上运行)
82赞 Louis 2/9/2014 #5

在两个答案中逐字引用的 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()

评论

0赞 user3342816 9/23/2019
好。但这也会读取 KeyboardInterrupt (Ctrl+C) 的第一个字符,并且代码有可能退出 .0
4赞 Davoud Taghawi-Nejad 6/23/2014 #6

这是非阻塞的,读取密钥并将其存储在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)

评论

1赞 martineau 8/31/2015
@ThorSummoner:这段代码有很多问题——所以,它不适用于命令行应用程序。
0赞 Davoud Taghawi-Nejad 9/1/2015
它为命令行应用程序运行,前提是 Windows 管理器正在运行。
0赞 Davoud Taghawi-Nejad 11/17/2015
不,它不能在无头操作系统中运行。但它确实在命令行窗口中运行。
86赞 Søren Løvborg 8/17/2014 #7

值得一试的是 readchar 库,它部分基于其他答案中提到的 ActiveState 配方(但此后已经走了很长一段路)。

安装:

python -m pip install readchar

用法:

import readchar
print('Reading a char:')
print(repr(readchar.readchar()))
print('Reading a key:')
print(repr(readchar.readkey()))

这是在 Windows 和 Linux 上使用 Python 3.9 进行测试的。它也应该在 PyCharm 终端中工作。

Windows 和 Linux 之间的密钥代码并不总是相同的,但该库提供了特定于平台的定义(如 readchar.key.F1)来帮助解决这个问题。

由于 Linux 将大多数特殊键报告为转义序列(以 开头),如果您点击实际的 Escape 键(由终端报告为单独的 ),就会感到困惑。不幸的是,这是一个常见的Unix问题,没有真正可靠的解决方案。\x1breadkey()\x1b

请注意,当在 + 上引发 (参见 readchar.config) 时,其他 Linux 信号键(例如 + 和 +)会被捕获并返回(分别作为 和 ),这可能是可取的,也可能是不可取的。readkeyKeyboardInterruptCtrlCCtrlDCtrlZ'\x04''\x1a'

对于类似于 Python 的类似输入提示的功能,请考虑此问题input()

评论

3赞 wrobell 6/6/2015
也适用于 Linux 上的 Python 3。比 getch 好得多,因为 readchar 允许在等待密钥时(通过线程或异步)打印到 stdout。
0赞 ipcjs 11/6/2016
在 Win10 + Python 3.5 上测试:ERROR:root:'in <string>' 需要字符串作为左操作数,而不是字节 回溯(最近一次调用最后):文件“..\main.py“,第 184 行,包装器结果 = func(*args, **kwargs) 文件”C:\GitHub\Python-Demo\demo\day_hello.py“,第 41 行,在 readch_eg print(readchar.readchar()) 文件”C:\Users\ipcjs\AppData\Local\Programs\Python\Python35\lib\site-packages\readchar\readchar_windows.py“,第 14 行,在 readchar 中,而 ch 在”\x00\xe0“中: TypeError: 'in <string>' 需要字符串作为左操作数, 不是字节
1赞 Melih Yıldız' 7/5/2017
@ipcjs请向维护者报告该错误
2赞 FistOfFury 12/26/2017
这是最好的答案。仅仅为了这个功能而向 VS C++ 库添加依赖项是疯狂的。
0赞 Nathan F. 4/20/2021
有关为 Control+ 返回哪些序列的完整列表...或者像退格键这样的东西,你可以看这里:github.com/magmax/python-readchar/blob/master/readchar/key.py
5赞 Alex Kleider 9/29/2014 #8

这可能是上下文管理器的一个用例。撇开 Windows 操作系统的余量不谈,我的建议如下:

#!/usr/bin/env python3
# file: 'readchar.py'
"""
Implementation of a way to get a single character of input
without waiting for the user to hit <Enter>.
(OS is Linux, Ubuntu 14.04)
"""

import tty, sys, termios

class ReadChar():
    def __enter__(self):
        self.fd = sys.stdin.fileno()
        self.old_settings = termios.tcgetattr(self.fd)
        tty.setraw(sys.stdin.fileno())
        return sys.stdin.read(1)
    def __exit__(self, type, value, traceback):
        termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)

def test():
    while True:
        with ReadChar() as rc:
            char = rc
        if ord(char) <= 32:
            print("You entered character with ordinal {}."\
                        .format(ord(char)))
        else:
            print("You entered character '{}'."\
                        .format(char))
        if char in "^C^D":
            sys.exit()

if __name__ == "__main__":
    test()

评论

0赞 L3viathan 4/12/2017
你也可以返回 in 并有一个返回的方法,然后你可以在一个上下文中读取多个字符。self __enter__readsys.stdin.read(1)
2赞 John Mark 12/30/2014 #9

python 中的包可用于进入“原始”模式,只需几条语句即可从终端输入字符。诅咒的主要用途是接管屏幕进行输出,这可能不是你想要的。此代码片段改用语句,这些语句是可用的,但您必须了解 curses 如何更改附加到输出的行尾。cursesprint()

#!/usr/bin/python3
# Demo of single char terminal input in raw mode with the curses package.
import sys, curses

def run_one_char(dummy):
    'Run until a carriage return is entered'
    char = ' '
    print('Welcome to curses', flush=True)
    while ord(char) != 13:
        char = one_char()

def one_char():
    'Read one character from the keyboard'
    print('\r? ', flush= True, end = '')

    ## A blocking single char read in raw mode. 
    char = sys.stdin.read(1)
    print('You entered %s\r' % char)
    return char

## Must init curses before calling any functions
curses.initscr()
## To make sure the terminal returns to its initial settings,
## and to set raw mode and guarantee cleanup on exit. 
curses.wrapper(run_one_char)
print('Curses be gone!')

评论

0赞 Big McLargeHuge 11/7/2022
既然你要导入诅咒,你还不如使用 docs.python.org/3/library/curses.html#curses.window.getch
3赞 PyGuy 2/21/2015 #10

试试pygame:

import pygame
pygame.init()             // eliminate error, pygame.error: video system not initialized
keys = pygame.key.get_pressed()

if keys[pygame.K_SPACE]:
    d = "space key"

print "You pressed the", d, "."

评论

0赞 dirkjot 4/21/2015
这是一个不错的主意,但它在命令行上不起作用:pygame.error: video system not initialized
8赞 jdev6 7/22/2015 #11

尝试使用这个: http://home.wlu.edu/~levys/software/kbhit.py 它是非阻塞的(这意味着您可以有一个 while 循环并在不停止它的情况下检测按键)和跨平台。

import os

# Windows
if os.name == 'nt':
    import msvcrt

# Posix (Linux, OS X)
else:
    import sys
    import termios
    import atexit
    from select import select


class KBHit:

    def __init__(self):
        '''Creates a KBHit object that you can call to do various keyboard things.'''

        if os.name == 'nt':
            pass

        else:

            # Save the terminal settings
            self.fd = sys.stdin.fileno()
            self.new_term = termios.tcgetattr(self.fd)
            self.old_term = termios.tcgetattr(self.fd)

            # New terminal setting unbuffered
            self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)

            # Support normal-terminal reset at exit
            atexit.register(self.set_normal_term)


    def set_normal_term(self):
        ''' Resets to normal terminal.  On Windows this is a no-op.
        '''

        if os.name == 'nt':
            pass

        else:
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)


    def getch(self):
        ''' Returns a keyboard character after kbhit() has been called.
            Should not be called in the same program as getarrow().
        '''

        s = ''

        if os.name == 'nt':
            return msvcrt.getch().decode('utf-8')

        else:
            return sys.stdin.read(1)


    def getarrow(self):
        ''' Returns an arrow-key code after kbhit() has been called. Codes are
        0 : up
        1 : right
        2 : down
        3 : left
        Should not be called in the same program as getch().
        '''

        if os.name == 'nt':
            msvcrt.getch() # skip 0xE0
            c = msvcrt.getch()
            vals = [72, 77, 80, 75]

        else:
            c = sys.stdin.read(3)[2]
            vals = [65, 67, 66, 68]

        return vals.index(ord(c.decode('utf-8')))


    def kbhit(self):
        ''' Returns True if keyboard character was hit, False otherwise.
        '''
        if os.name == 'nt':
            return msvcrt.kbhit()

        else:
            dr,dw,de = select([sys.stdin], [], [], 0)
            return dr != []

使用此示例:

import kbhit

kb = kbhit.KBHit()

while(True): 
    print("Key not pressed") #Do something
    if kb.kbhit(): #If a key is pressed:
        k_in = kb.getch() #Detect what key was pressed
        print("You pressed ", k_in, "!") #Do something
kb.set_normal_term()

或者你可以使用 PyPi 的 getch 模块。但这会阻塞 while 循环

评论

0赞 Dave 2/20/2021
尽管这在解决方案的啄食顺序中相当低,但它是唯一一个没有在 MacOS 上弄乱我的 stdout 的 - 谢谢!
8赞 Phylliida 7/31/2015 #12

这里的答案很有信息量,但是我也想要一种方法来异步按下键,并在单独的事件中触发按键,所有这些都是线程安全的、跨平台的方式。PyGame 对我来说也太臃肿了。所以我做了以下内容(在 Python 2.7 中,但我怀疑它很容易移植),我想我会在这里分享,以防它对其他人有用。我将其存储在名为 keyPress.py 的文件中。

class _Getch:
    """Gets a single character from standard input.  Does not echo to the
screen. From http://code.activestate.com/recipes/134892/"""
    def __init__(self):
        try:
            self.impl = _GetchWindows()
        except ImportError:
            try:
                self.impl = _GetchMacCarbon()
            except(AttributeError, ImportError):
                self.impl = _GetchUnix()

    def __call__(self): return self.impl()


class _GetchUnix:
    def __init__(self):
        import tty, sys, termios # import termios now or else you'll get the Unix version on the Mac

    def __call__(self):
        import sys, tty, 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()

class _GetchMacCarbon:
    """
    A function which returns the current ASCII key that is down;
    if no ASCII key is down, the null string is returned.  The
    page http://www.mactech.com/macintosh-c/chap02-1.html was
    very helpful in figuring out how to do this.
    """
    def __init__(self):
        import Carbon
        Carbon.Evt #see if it has this (in Unix, it doesn't)

    def __call__(self):
        import Carbon
        if Carbon.Evt.EventAvail(0x0008)[0]==0: # 0x0008 is the keyDownMask
            return ''
        else:
            #
            # The event contains the following info:
            # (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
            #
            # The message (msg) contains the ASCII char which is
            # extracted with the 0x000000FF charCodeMask; this
            # number is converted to an ASCII character with chr() and
            # returned
            #
            (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1]
            return chr(msg & 0x000000FF)

import threading
            
            
# From  https://stackoverflow.com/a/2022629/2924421
class Event(list):
    def __call__(self, *args, **kwargs):
        for f in self:
            f(*args, **kwargs)

    def __repr__(self):
        return "Event(%s)" % list.__repr__(self)            


def getKey():
    inkey = _Getch()
    import sys
    for i in xrange(sys.maxint):
        k=inkey()
        if k<>'':break
    return k

class KeyCallbackFunction():
    callbackParam = None
    actualFunction = None
    
    def __init__(self, actualFunction, callbackParam):
        self.actualFunction = actualFunction
        self.callbackParam = callbackParam

    def doCallback(self, inputKey):
        if not self.actualFunction is None:
            if self.callbackParam is None:
                callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,))
            else:
                callbackFunctionThread = threading.Thread(target=self.actualFunction, args=(inputKey,self.callbackParam))
            
            callbackFunctionThread.daemon = True
            callbackFunctionThread.start()
        
        

class KeyCapture():


    gotKeyLock = threading.Lock()
    gotKeys = []
    gotKeyEvent = threading.Event()

    keyBlockingSetKeyLock = threading.Lock()

    addingEventsLock = threading.Lock()
    keyReceiveEvents = Event()


    keysGotLock = threading.Lock()
    keysGot = []

    keyBlockingKeyLockLossy = threading.Lock()
    keyBlockingKeyLossy = None
    keyBlockingEventLossy = threading.Event()
    
    keysBlockingGotLock = threading.Lock()
    keysBlockingGot = []
    keyBlockingGotEvent = threading.Event()
    

    
    wantToStopLock = threading.Lock()
    wantToStop = False
    
    stoppedLock = threading.Lock()
    stopped = True
    
    isRunningEvent = False
    
    getKeyThread = None
    
    keyFunction = None
    keyArgs = None
    
    # Begin capturing keys. A seperate thread is launched that
    # captures key presses, and then these can be received via get,
    # getAsync, and adding an event via addEvent. Note that this
    # will prevent the system to accept keys as normal (say, if
    # you are in a python shell) because it overrides that key
    # capturing behavior.
    
    # If you start capture when it's already been started, a
    # InterruptedError("Keys are still being captured")
    # will be thrown
    
    # Note that get(), getAsync() and events are independent, so if a key is pressed:
    #
    # 1: Any calls to get() that are waiting, with lossy on, will return
    #    that key
    # 2: It will be stored in the queue of get keys, so that get() with lossy
    #    off will return the oldest key pressed not returned by get() yet.
    # 3: All events will be fired with that key as their input
    # 4: It will be stored in the list of getAsync() keys, where that list
    #    will be returned and set to empty list on the next call to getAsync().
    # get() call with it, aand add it to the getAsync() list.
    def startCapture(self, keyFunction=None, args=None):
        # Make sure we aren't already capturing keys
        self.stoppedLock.acquire()
        if not self.stopped:
            self.stoppedLock.release()
            raise InterruptedError("Keys are still being captured")
            return
        self.stopped = False
        self.stoppedLock.release()
        
        # If we have captured before, we need to allow the get() calls to actually
        # wait for key presses now by clearing the event
        if self.keyBlockingEventLossy.is_set():
            self.keyBlockingEventLossy.clear()
            
        # Have one function that we call every time a key is captured, intended for stopping capture
        # as desired
        self.keyFunction = keyFunction
        self.keyArgs = args
        
        # Begin capturing keys (in a seperate thread)
        self.getKeyThread = threading.Thread(target=self._threadProcessKeyPresses)
        self.getKeyThread.daemon = True
        self.getKeyThread.start()
        
        # Process key captures (in a seperate thread)
        self.getKeyThread = threading.Thread(target=self._threadStoreKeyPresses)
        self.getKeyThread.daemon = True
        self.getKeyThread.start()
    
    
    def capturing(self):
        self.stoppedLock.acquire()
        isCapturing = not self.stopped
        self.stoppedLock.release()
        return isCapturing
    # Stops the thread that is capturing keys on the first opporunity
    # has to do so. It usually can't stop immediately because getting a key
    # is a blocking process, so this will probably stop capturing after the
    # next key is pressed.
    #
    # However, Sometimes if you call stopCapture it will stop before starting capturing the
    # next key, due to multithreading race conditions. So if you want to stop capturing
    # reliably, call stopCapture in a function added via addEvent. Then you are
    # guaranteed that capturing will stop immediately after the rest of the callback
    # functions are called (before starting to capture the next key).
    def stopCapture(self):
        self.wantToStopLock.acquire()
        self.wantToStop = True 
        self.wantToStopLock.release()

    # Takes in a function that will be called every time a key is pressed (with that
    # key passed in as the first paramater in that function)
    def addEvent(self, keyPressEventFunction, args=None):   
        self.addingEventsLock.acquire()
        callbackHolder = KeyCallbackFunction(keyPressEventFunction, args)
        self.keyReceiveEvents.append(callbackHolder.doCallback)
        self.addingEventsLock.release()
    def clearEvents(self):
        self.addingEventsLock.acquire()
        self.keyReceiveEvents = Event()
        self.addingEventsLock.release()
    # Gets a key captured by this KeyCapture, blocking until a key is pressed.
    # There is an optional lossy paramater:
    # If True all keys before this call are ignored, and the next pressed key
    #   will be returned.
    # If False this will return the oldest key captured that hasn't
    #   been returned by get yet. False is the default.
    def get(self, lossy=False):
        if lossy:
            # Wait for the next key to be pressed
            self.keyBlockingEventLossy.wait()
            self.keyBlockingKeyLockLossy.acquire()
            keyReceived = self.keyBlockingKeyLossy
            self.keyBlockingKeyLockLossy.release()
            return keyReceived
        else:
            while True:
                # Wait until a key is pressed
                self.keyBlockingGotEvent.wait()
                
                # Get the key pressed
                readKey = None
                self.keysBlockingGotLock.acquire()
                # Get a key if it exists
                if len(self.keysBlockingGot) != 0:
                    readKey = self.keysBlockingGot.pop(0)
                # If we got the last one, tell us to wait
                if len(self.keysBlockingGot) == 0:
                    self.keyBlockingGotEvent.clear()
                self.keysBlockingGotLock.release()
                
                # Process the key (if it actually exists)
                if not readKey is None:
                    return readKey
                
                # Exit if we are stopping
                self.wantToStopLock.acquire()
                if self.wantToStop:
                    self.wantToStopLock.release()
                    return None
                self.wantToStopLock.release()
            
            
            
    
    def clearGetList(self):
        self.keysBlockingGotLock.acquire()
        self.keysBlockingGot = []
        self.keysBlockingGotLock.release()
    
    # Gets a list of all keys pressed since the last call to getAsync, in order
    # from first pressed, second pressed, .., most recent pressed
    def getAsync(self):
        self.keysGotLock.acquire();
        keysPressedList = list(self.keysGot)
        self.keysGot = []
        self.keysGotLock.release()
        return keysPressedList
    
    def clearAsyncList(self):
        self.keysGotLock.acquire();
        self.keysGot = []
        self.keysGotLock.release();

    def _processKey(self, readKey):
        # Append to list for GetKeyAsync
        self.keysGotLock.acquire()
        self.keysGot.append(readKey)
        self.keysGotLock.release()
        
        # Call lossy blocking key events
        self.keyBlockingKeyLockLossy.acquire()
        self.keyBlockingKeyLossy = readKey
        self.keyBlockingEventLossy.set()
        self.keyBlockingEventLossy.clear()
        self.keyBlockingKeyLockLossy.release()
        
        # Call non-lossy blocking key events
        self.keysBlockingGotLock.acquire()
        self.keysBlockingGot.append(readKey)
        if len(self.keysBlockingGot) == 1:
            self.keyBlockingGotEvent.set()
        self.keysBlockingGotLock.release()
        
        # Call events added by AddEvent
        self.addingEventsLock.acquire()
        self.keyReceiveEvents(readKey)
        self.addingEventsLock.release()

    def _threadProcessKeyPresses(self):
        while True:
            # Wait until a key is pressed
            self.gotKeyEvent.wait()
            
            # Get the key pressed
            readKey = None
            self.gotKeyLock.acquire()
            # Get a key if it exists
            if len(self.gotKeys) != 0:
                readKey = self.gotKeys.pop(0)
            # If we got the last one, tell us to wait
            if len(self.gotKeys) == 0:
                self.gotKeyEvent.clear()
            self.gotKeyLock.release()
            
            # Process the key (if it actually exists)
            if not readKey is None:
                self._processKey(readKey)
            
            # Exit if we are stopping
            self.wantToStopLock.acquire()
            if self.wantToStop:
                self.wantToStopLock.release()
                break
            self.wantToStopLock.release()
            
    def _threadStoreKeyPresses(self):
        while True:
            # Get a key
            readKey = getKey()
            
            # Run the potential shut down function
            if not self.keyFunction is None:
                self.keyFunction(readKey, self.keyArgs)
        
            # Add the key to the list of pressed keys
            self.gotKeyLock.acquire()
            self.gotKeys.append(readKey)
            if len(self.gotKeys) == 1:
                self.gotKeyEvent.set()
            self.gotKeyLock.release()
            
            # Exit if we are stopping
            self.wantToStopLock.acquire()
            if self.wantToStop:
                self.wantToStopLock.release()
                self.gotKeyEvent.set()
                break
            self.wantToStopLock.release()
    
        
        # If we have reached here we stopped capturing
        
        # All we need to do to clean up is ensure that
        # all the calls to .get() now return None.
        # To ensure no calls are stuck never returning,
        # we will leave the event set so any tasks waiting
        # for it immediately exit. This will be unset upon
        # starting key capturing again.
        
        self.stoppedLock.acquire()
        
        # We also need to set this to True so we can start up
        # capturing again.
        self.stopped = True
        self.stopped = True
        
        self.keyBlockingKeyLockLossy.acquire()
        self.keyBlockingKeyLossy = None
        self.keyBlockingEventLossy.set()
        self.keyBlockingKeyLockLossy.release()
        
        self.keysBlockingGotLock.acquire()
        self.keyBlockingGotEvent.set()
        self.keysBlockingGotLock.release()
        
        self.stoppedLock.release()

这个想法是,您可以简单地调用 ,这将从键盘上读取一个键,然后返回它。keyPress.getKey()

如果你想要更多的东西,我做了一个对象。您可以通过类似 .KeyCapturekeys = keyPress.KeyCapture()

然后,您可以做三件事:

addEvent(functionName)接受任何接受一个参数的函数。然后,每次按下一个键时,都会使用该键的字符串作为输入来调用此函数。这些都在一个单独的线程中运行,因此您可以阻止其中的所有内容,并且不会弄乱KeyCapturer的功能,也不会延迟其他事件。

get()以与以前相同的阻塞方式返回密钥。现在这里需要它,因为密钥现在正在通过对象捕获,因此会与该行为冲突,并且它们都会丢失一些密钥,因为一次只能捕获一个密钥。另外,假设用户按“a”,然后按“b”,你打电话,用户按“c”。该调用将立即返回“a”,然后如果您再次调用它,它将返回“b”,然后是“c”。如果您再次调用它,它将被阻止,直到按下另一个键。这样可以确保您不会错过任何键,如果需要,可以阻止。所以这样一来,就和以前有点不同了KeyCapturekeyPress.getKey()get()get()keyPress.getKey()

如果想要 back 的行为,就像 一样,只不过它只返回调用按下的键。因此,在上面的示例中,将阻止直到用户按下“c”,然后如果您再次调用它,它将阻止直到按下另一个键。getKey()get(lossy=True)get()get()get()

getAsync()有点不同。它是为进行大量处理而设计的,然后偶尔会回来检查按下了哪些键。因此,返回自上次调用以来按下的所有键的列表,按从最旧的键按下到最近按下的键的顺序排列。它也不会阻塞,这意味着如果自上次调用以来没有按下任何键,将返回一个空键。getAsync()getAsync()getAsync()[]

要真正开始捕获密钥,您需要使用上面制作的对象进行调用。 是非阻塞的,只需启动一个仅记录按键的线程,以及另一个处理这些按键的线程。有两个线程可以确保记录按键的线程不会遗漏任何键。keys.startCapture()keysstartCapture

如果要停止捕获密钥,可以调用,它将停止捕获密钥。但是,由于捕获密钥是阻塞操作,因此线程捕获密钥可能会在调用 后再捕获一个密钥。keys.stopCapture()stopCapture()

为了防止这种情况,您可以将一个可选参数传入一个函数,该函数只是执行诸如检查键是否等于“c”然后退出之类的操作。重要的是,这个功能之前很少做,例如,在这里睡觉会导致我们错过按键。startCapture(functionName, args)

但是,如果在此函数中调用,则将立即停止密钥捕获,不再尝试捕获,并且将立即返回所有调用,如果尚未按下任何键,则返回 None。stopCapture()get()

此外,由于并存储所有以前按下的键(直到您检索它们),您可以调用和忘记之前按下的键。get()getAsync()clearGetList()clearAsyncList()

请注意,和 事件是独立的,因此如果按下某个键:get()getAsync()

  1. 一个正在等待的调用,在有损开启的情况下,将返回 那把钥匙。其他等待呼叫(如果有)将继续等待。get()
  2. 该密钥将存储在获取密钥的队列中,因此在有损关闭的情况下将返回尚未返回的最早的密钥。get()get()
  3. 所有事件都将使用该键作为其输入来触发
  4. 该密钥将存储在密钥列表中,该密钥将在下次调用时返回并设置为空列表getAsync()getAsync()

如果所有这些都太多了,下面是一个示例用例:

import keyPress
import time
import threading

def KeyPressed(k, printLock):
    printLock.acquire()
    print "Event: " + k
    printLock.release()
    time.sleep(4)
    printLock.acquire()
    print "Event after delay: " + k
    printLock.release()

def GetKeyBlocking(keys, printLock):    
    while keys.capturing():
        keyReceived = keys.get()
        time.sleep(1)
        printLock.acquire()
        if not keyReceived is None:
            print "Block " + keyReceived
        else:
            print "Block None"
        printLock.release()

def GetKeyBlockingLossy(keys, printLock):   
    while keys.capturing():
        keyReceived = keys.get(lossy=True)
        time.sleep(1)
        printLock.acquire()
        if not keyReceived is None:
            print "Lossy: " + keyReceived
        else:
            print "Lossy: None"
        printLock.release()

def CheckToClose(k, (keys, printLock)):
    printLock.acquire()
    print "Close: " + k
    printLock.release()
    if k == "c":
        keys.stopCapture()
        
printLock = threading.Lock()

print "Press a key:"
print "You pressed: " + keyPress.getKey()
print ""

keys = keyPress.KeyCapture()

keys.addEvent(KeyPressed, printLock)



print "Starting capture"
            
keys.startCapture(CheckToClose, (keys, printLock))
            
getKeyBlockingThread = threading.Thread(target=GetKeyBlocking, args=(keys, printLock))
getKeyBlockingThread.daemon = True
getKeyBlockingThread.start()

            
getKeyBlockingThreadLossy = threading.Thread(target=GetKeyBlockingLossy, args=(keys, printLock))
getKeyBlockingThreadLossy.daemon = True
getKeyBlockingThreadLossy.start()

while keys.capturing():
    keysPressed = keys.getAsync()
    printLock.acquire()
    if keysPressed != []:
        print "Async: " + str(keysPressed)
    printLock.release()
    time.sleep(1)

print "done capturing"

从我所做的简单测试来看,它对我来说效果很好,但是如果我错过了什么,我也会很乐意接受其他人的反馈。

我也在这里发布了这个。

-4赞 Mabooka 12/16/2015 #13

内置raw_input应该会有所帮助。

for i in range(3):
    print ("So much work to do!")
k = raw_input("Press any key to continue...")
print ("Ok, back to work.")

评论

8赞 vac 12/17/2015
raw_input正在等待回车键
0赞 xro 1/28/2016 #14

我的 python3 解决方案,不依赖于任何 pip 包。

# precondition: import tty, sys
def query_yes_no(question, default=True):
    """
    Ask the user a yes/no question.
    Returns immediately upon reading one-char answer.
    Accepts multiple language characters for yes/no.
    """
    if not sys.stdin.isatty():
        return default
    if default:
        prompt = "[Y/n]?"
        other_answers = "n"
    else:
        prompt = "[y/N]?"
        other_answers = "yjosiá"

    print(question,prompt,flush= True,end=" ")
    oldttysettings = tty.tcgetattr(sys.stdin.fileno())
    try:
        tty.setraw(sys.stdin.fileno())
        return not sys.stdin.read(1).lower() in other_answers
    except:
        return default
    finally:
        tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN , oldttysettings)
        sys.stdout.write("\r\n")
        tty.tcdrain(sys.stdin.fileno())
16赞 Matthew Strax-Haber 5/2/2016 #15

(当前)排名靠前的答案(带有 ActiveState 代码)过于复杂。当仅仅一个函数就足够了时,我看不出有什么理由使用类。下面是两个实现,它们完成相同的操作,但代码更具可读性。

这两种实现方式:

  1. 在 Python 2 或 Python 3 中工作得很好
  2. 适用于 Windows、OSX 和 Linux
  3. 只读取一个字节(即,它们不等待换行符)
  4. 不依赖于任何外部库
  5. 是自包含的(函数定义之外没有代码)

版本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

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

[编辑]我错过了 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))

评论

2赞 TheDavidFactor 5/21/2016
我在打印消息时遇到了 tty.setraw() 问题,同时等待密钥(多线程)。长话短说,我发现使用 tty.setcbreak() 可以让你在不破坏所有其他正常内容的情况下获得一个字符。这个答案说来话长
0赞 iggy12345 2/2/2021
我在使用 PyCharm 执行 shell 模拟器时遇到了问题,并出现以下错误:... old_settings = termios.tcgetattr(fd) ... termios.error: (25, 'Inappropriate ioctl for device')
0赞 jpmc26 5/10/2021
这个答案早于你的答案,是一个更好的方法。
0赞 Joao-3 9/29/2022
如果有人试图告诉你,就会输出一个类型字节。要将其转换为普通字符串,请使用msvcrt.getch()str(msvcrt.getch())
3赞 Noah 7/23/2017 #16

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

另一个重要的细节是,如果你想读取一个字符而不是一个字节,你应该从输入流中读取 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)
1赞 theAlse 11/29/2017 #17

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

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!")
5赞 ibic 1/7/2018 #18

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

while(True):
    print(getch())

我永远无法终止脚本,我必须杀死我的终端才能逃脱。Ctrl-C

我相信下面这句话是原因,也太残酷了:

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

除此之外,包并不是真正需要的,足以处理它。ttytermios

以下是对我有用的改进代码(将中断),并在您键入时回显字符的额外函数:Ctrl-Cgetche

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)

引用:

5赞 qel 9/23/2018 #19

如果我在做一些复杂的事情,我会使用诅咒来读取密钥。但很多时候我只想要一个简单的 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

评论

1赞 Mr PizzaGuy 11/5/2020
这正是我想要的,简单,并使用默认的 python 库,谢谢!!
0赞 Radzor 3/24/2023
不可移植到 Windows
0赞 Ben Ogorek 7/22/2019 #20

接受的答案对我来说表现不佳(我会按住一个键,什么都不会发生,然后我按另一个键,它就会起作用)。

在了解了诅咒模块之后,这似乎真的是正确的方法。它现在可以通过 windows-cursors 用于 Windows(通过 pip 提供),因此您可以以与平台无关的方式进行编程。下面是一个受 YouTube 上这个精彩教程启发的示例:

import curses                                                                                                                                       
def getkey(stdscr):
    curses.curs_set(0)
    while True:
        key = stdscr.getch()
        if key != -1:
            break
    return key

if __name__ == "__main__":
    print(curses.wrapper(getkey))

使用扩展名保存它,或以交互模式运行。.pycurses.wrapper(getkey)

0赞 Meir Gabay 9/1/2019 #21

在这里回答: 在python中raw_input,不按回车键

使用此代码-

from tkinter import Tk, Frame


def __set_key(e, root):
    """
    e - event with attribute 'char', the released key
    """
    global key_pressed
    if e.char:
        key_pressed = e.char
        root.destroy()


def get_key(msg="Press any key ...", time_to_sleep=3):
    """
    msg - set to empty string if you don't want to print anything
    time_to_sleep - default 3 seconds
    """
    global key_pressed
    if msg:
        print(msg)
    key_pressed = None
    root = Tk()
    root.overrideredirect(True)
    frame = Frame(root, width=0, height=0)
    frame.bind("<KeyRelease>", lambda f: __set_key(f, root))
    frame.pack()
    root.focus_set()
    frame.focus_set()
    frame.focus_force()  # doesn't work in a while loop without it
    root.after(time_to_sleep * 1000, func=root.destroy)
    root.mainloop()
    root = None  # just in case
    return key_pressed


def __main():
        c = None
        while not c:
                c = get_key("Choose your weapon ... ", 2)
        print(c)

if __name__ == "__main__":
    __main()

编号: https://github.com/unfor19/mg-tools/blob/master/mgtools/get_key_pressed.py

0赞 Vinay Verma 10/7/2019 #22

如果只想注册一个键,即使用户按下该键不止一次或按住该键的时间更长,也要按。 为避免获得多个按下的输入,请使用 while 循环并传递它。

import keyboard

while(True):
  if(keyboard.is_pressed('w')):
      s+=1
      while(keyboard.is_pressed('w')):
        pass
  if(keyboard.is_pressed('s')):
      s-=1
      while(keyboard.is_pressed('s')):
        pass
  print(s)

评论

0赞 flaschbier 11/27/2021
模块应该来自哪个包?是什么让你写作?这不是 C :)keyboardif():
8赞 Yash Rathi 8/18/2021 #23

您可以使用点击。它经过充分测试,适用于 Linux、Mac 和 Windows。

import click

print('Continue? [yn] ')
c = click.getchar()   # Gets a single character


if c == 'y':
    print('We will go on')
elif c == 'n':
    print('Abort!')
else:
    print('Invalid input :(')
1赞 ilon 10/28/2021 #24

最简单的跨平台解决方案是 sshkeyboard。使用 安装 ,pip install sshkeyboard

然后编写脚本,例如:

from sshkeyboard import listen_keyboard

def press(key):
    print(f"'{key}' pressed")

def release(key):
    print(f"'{key}' released")

listen_keyboard(
    on_press=press,
    on_release=release,
)

它将打印:

'a' pressed
'a' released

按下键时。 默认情况下,键结束侦听。AESC

它需要的编码比 curses、tkinter 和 getch 等更少。

3赞 Markus Hirsimäki 4/28/2022 #25

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 上产生长 ANSI 转义序列,在 Windows 上产生其他东西。它将捕获<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