【问题标题】: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 。或许这并不是那么简单。

标签: python input


【解决方案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:一般认为请求宽恕更好,见stackoverflow.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 控制代码的键(Backspace、Enter、Esc、Tab , Ctrl+字母)。在 GNU/Linux 上(可能取决于确切的终端?)您还可以得到 Insert、Delete、Pg Up、Pg Dn、Home、End 和 F n 键......但是,分离这些特殊的键存在问题Esc 中的键。

警告:与此处的大多数(全部?)答案一样,信号键如 Ctrl+C、Ctrl+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 代码)过于复杂。当仅仅一个函数就足够时,我看不到使用类的理由。下面是两个实现相同的功能但代码更易读的实现。

这两种实现:

  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

版本 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+C 或 Ctrl+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://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(),它会从键盘读取一个键,然后返回它。

如果你想要更多的东西,我创建了一个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()和事件是独立的,所以如果一个键被按下:

  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"

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

我也发布了这个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://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)
    

    参考资料:

    【讨论】:

      【解决方案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()
                      

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

                      【讨论】:

                      • 这没有回答问题。用户正在询问如何阅读单个字符。
                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多