如何从用户那里读取单个字符?
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 控制代码的键(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另一种方法:
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+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://***.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()
在代码的末尾,它将保持屏幕
【讨论】:
这没有回答问题。用户正在询问如何阅读单个字符。以上是关于如何从用户那里读取单个字符?的主要内容,如果未能解决你的问题,请参考以下文章