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

Posted

技术标签:

【中文标题】如何从用户那里读取单个字符?【英文标题】:How to read a single character from the user? 【发布时间】:2010-10-05 08:27:09 【问题描述】:

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

【问题讨论】:

在 Windows 上,我遇到了与 question 相同的问题。解决方案是按照那里的建议将msvcrt.getch 替换为msvcrt.getwch 解决方案是安装 getch 模块“pip install getch”。对于 Python2 使用命令“pip2 install files.pythonhosted.org/packages/56/f7/…”。此解决方案也适用于 Termux (android)。 最简单的解决方案是使用sshkeyboard。它比 getch 需要更少的编码,而且它是一个跨平台的解决方案。 我无法相信所有这些复杂的答案。在 Ruby 中:input = STDIN.getch 就是这样。 @user93883 NameError: global name 'STDIN' is not defined 。或许这并不是那么简单。 【参考方案1】:

这是 ActiveState 食谱网站的链接,该网站说明了如何在 Windows、Linux 和 OSX 中读取单个字符:

    getch()-like unbuffered character reading from stdin on both Windows and Unix

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()

【讨论】:

代码似乎足够短,您可以将其包含在内,但是 +1 可以快速找到一个好的(跨平台)答案。 它是否能很好地处理非拉丁文(例如西里尔文)字母?我对此有疑问,不知道是不是我的错。 我不喜欢 ImportError 异常被用作某种 if 语句;为什么不调用 platform.system() 来检查操作系统? @Seismoid:一般认为请求宽恕更好,见***.com/questions/12265451/… 在 OS X 上不起作用:"old_settings = termios.tcgetattr(fd)" "termios.error: (25, 'Inappropriate ioctl for device')"【参考方案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/

【讨论】:

我觉得 sys.stdin.read(1) 等待 \n 很奇怪,哈哈。不过,感谢您的提交。 一个字符还是一个字节?那不一样。 @Evan,那是因为python默认处于行缓冲模式 @EvanFosmark:sys.stdin.read(1) 不一定要等待 \n,而是终端程序决定何时将其他字符发送到您的程序时才会写入它们,直到它看到'\n' - 你怎么能按退格键并更正你正在输入的内容? (对此的严肃回答是 - 教 python 程序实现行控制,保留缓冲区,处理退格,但这是一个不同的世界,当你只是“读取一个字符”时,你可能不想购买,并且可以让你的行处理方式与系统上的所有其他程序不同。) @Seismoid EAFP【参考方案3】:

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

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()

【讨论】:

不错。但这也会读取 KeyboardInterrupt (Ctrl+C) 的第一个字符,并且代码有可能以0 退出。【参考方案4】:

readchar 库也值得一试,它部分基于其他答案中提到的 ActiveState 配方。

安装:

pip install readchar

用法:

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

使用 Python 2.7 在 Windows 和 Linux 上测试。

在 Windows 上,仅支持映射到字母或 ASCII 控制代码的键(BackspaceEnterEscTab , Ctrl+字母)。在 GNU/Linux 上(可能取决于确切的终端?)您还可以得到 InsertDeletePg UpPg Dn、HomeEndF n 键......但是,分离这些特殊的键存在问题Esc 中的键。

警告:与此处的大多数(全部?)答案一样,信号键如 Ctrl+CCtrl+D Ctrl+Z 被捕获并返回(分别为'\x03''\x04''\x1a');您的程序可能很难中止。

【讨论】:

也适用于 Linux 上的 Python 3。比 getch 好得多,因为 readchar 允许在等待密钥(通过线程或异步)时打印到标准输出。 在 Win10 + Python 3.5 上测试: ERROR:root:'in ' 需要字符串作为左操作数,而不是字节 Traceback(最近一次调用最后):文件“..\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 in '\x00\xe0': TypeError: 'in ' 需要字符串作为左操作数,而不是字节 @ipcjs 请将该错误报告给维护者 这是最好的答案。为这个功能添加对 VS C++ 库的依赖是疯狂的。 对于 Control+... 或退格等返回的序列的完整列表,您可以查看此处:github.com/magmax/python-readchar/blob/master/readchar/key.py【参考方案5】:

另一种方法:

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

来自this blog post。

【讨论】:

似乎对我不起作用 - 调用时立即返回空字符串。在 Linux 上使用 Python 3.6。 @Marein 如果您希望它阻止(等待输入),请删除 | os.O_NONBLOCK。否则,你可以把它放在一个循环中(最好在循环中睡一会儿以防止旋转)。 在 Python 中,最好使用while True 然后使用while 1【参考方案6】:

(当前)排名靠前的答案(使用 ActiveState 代码)过于复杂。当仅仅一个函数就足够时,我看不到使用类的理由。下面是两个实现相同的功能但代码更易读的实现。

这两种实现:

    在 Python 2 或 Python 3 中运行良好 在 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))

【讨论】:

我在打印消息同时等待键(多线程)时遇到了 tty.setraw() 的问题。长话短说,我发现使用 tty.setcbreak() 可以让你在不破坏所有其他正常内容的情况下获得单个角色。长篇大论answer 我在使用 PyCharm 执行 shell 模拟器时遇到了以下错误:... old_settings = termios.tcgetattr(fd) ... termios.error: (25, 'Inappropriate ioctl for device') This answer 早于您的方法,是一种更好的方法。【参考方案7】:

此代码基于here,如果 Ctrl+CCtrl+D 将正确引发 KeyboardInterrupt 和 EOFError 被按下。

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

【讨论】:

它支持 ASCII 以外的 Unicode(这是其他一些解决方案所关心的问题)(它适用于 linux)【参考方案8】:

试试这个: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()

或者您可以使用getch module from PyPi。但这会阻塞while循环

【讨论】:

尽管这在解决方案的优先顺序上相当低,但它是唯一没有与我在 MacOS 上的标准输出混淆的解决方案 - 谢谢!【参考方案9】:

here 的答案提供了丰富的信息,但是我还想要一种方法来异步获取按键并在单独的事件中触发按键,所有这些都以线程安全、跨平台的方式进行。 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://***.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(),它会从键盘读取一个键,然后返回它。

如果你想要更多的东西,我创建了一个KeyCapture 对象。您可以通过keys = keyPress.KeyCapture() 之类的方式创建一个。

那么你可以做三件事:

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

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

如果您想要返回 getKey() 的行为,get(lossy=True) 类似于 get(),除了它只返回在调用 get() 之后按下的键。所以在上面的例子中,get() 会阻塞直到用户按下'c',然后如果你再次调用它,它会阻塞直到按下另一个键。

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

要真正开始捕获密钥,您需要使用上面创建的 keys 对象调用 keys.startCapture()startCapture 是非阻塞的,只需启动一个只记录按键的线程,然后启动另一个线程来处理这些按键。有两个线程来保证记录按键的线程不会漏掉任何键。

如果你想停止抓取key,你可以拨打keys.stopCapture(),它会停止抓取key。但是,由于捕获键是一种阻塞操作,因此捕获键的线程可能会在调用 stopCapture() 之后再捕获一个键。

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

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

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

注意get()getAsync()和事件是独立的,所以如果一个键被按下:

    get() 的呼叫正在等待,有损开启,将返回 那把钥匙。其他正在等待的呼叫(如果有)将继续等待。 该键将存储在获取键的队列中,因此关闭有损的get() 将返回get() 尚未返回的最早按下的键。 所有事件都会以该键作为输入触发 该密钥将存储在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"

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

我也发布了这个here。

【讨论】:

【参考方案10】:

这可能是上下文管理器的一个用例。抛开 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()

【讨论】:

你也可以在__enter__中返回self ,并有一个read方法返回sys.stdin.read(1),那么你可以在一个上下文中读取多个字符。【参考方案11】:

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

【讨论】:

这正是我要找的,简单,使用默认的python库,谢谢!!!【参考方案12】:

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

【讨论】:

@ThorSummoner:这段代码有很多问题——所以,它不适用于命令行应用程序。 它为命令行应用程序运行,因为 Windows 管理器正在运行。 不,它不能在无头操作系统中运行。但它确实在命令行窗口中运行。【参考方案13】:

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://***.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

【讨论】:

【参考方案14】:

您可以使用click。它经过充分测试,可在 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 :(')

【讨论】:

【参考方案15】:

用 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, "."

【讨论】:

这是个好主意,但在命令行上不起作用:pygame.error: video system not initialized【参考方案16】:

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

另一个重要的细节是,如果您要读取一个 character 而不是一个 byte,您应该从输入流中读取 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)

【讨论】:

【参考方案17】:

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

#!/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!')

【讨论】:

【参考方案18】:

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

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!")

【讨论】:

【参考方案19】:

最简单的跨平台解决方案是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

A 键被按下时。 ESC 键默认结束监听。

与curses、tkinter 和getch 相比,它需要的编码更少。

【讨论】:

【参考方案20】:

我的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())

【讨论】:

【参考方案21】:

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

在了解了 curses 模块之后,这似乎是正确的方法。现在可以通过windows-cursors(通过 pip 获得)在 Windows 上使用它,因此您可以以与平台无关的方式进行编程。这是一个受 YouTube 上 nice tutorial 启发的示例:

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))

使用.py 扩展保存它,或在交互模式下运行curses.wrapper(getkey)

【讨论】:

【参考方案22】:

在这里回答:raw_input in python without pressing enter

使用此代码-

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

【讨论】:

【参考方案23】:

如果您只想注册一个按键,即使用户按下了不止一次或按住按键的时间更长。 为了避免获得多个按下的输入,请使用 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)

【讨论】:

keyboard 模块应该来自哪个包?是什么让你写if():?这不是 C :)【参考方案24】:

内置 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.")

【讨论】:

raw_input 正在等待输入键【参考方案25】:

如果您只想按住屏幕以便在终端上看到结果,只需编写

input()

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

【讨论】:

这没有回答问题。用户正在询问如何阅读单个字符。

以上是关于如何从用户那里读取单个字符?的主要内容,如果未能解决你的问题,请参考以下文章

C ++从文件中读取多行到单个字符串

如何在 Python 中从十六进制(字节字符串)中读取单个字符?

Javascript没有从输入字段中读取单个字符

如何限制用户仅在 C++ 中输入单个字符

如何使用套接字从服务器读取数据?

如何在 C++ 中将文件中的段落作为单个字符串读取