具有超时的键盘输入?

103
你如何在N秒后超时提示用户输入内容?
谷歌指向一个邮件线程(http://mail.python.org/pipermail/python-list/2006-January/533215.html),但它似乎不起作用。无论是sys.input.readline还是timer.sleep()发生超时的语句,我总是得到:
<type 'exceptions.TypeError'>: [raw_]input expected at most 1 arguments, got 2

某种情况下,异常未被捕获。


相关:Python 3定时输入 - jfs
可能是Python函数调用超时的重复问题。 - n611x007
相关:raw_input和timeout /3471461 - n611x007
如何在输入上设置时间限制 /2933399 - n611x007
跨平台的stdlib解决方案,可用于多个输入直到超时:链接 - Darkonaut
29个回答

124
使用select调用更短,而且应该更具可移植性。
import sys, select

print "You have ten seconds to answer!"

i, o, e = select.select( [sys.stdin], [], [], 10 )

if (i):
  print "You said", sys.stdin.readline().strip()
else:
  print "You said nothing!"

54
我刚刚测试了一下,这在 Windows 上不起作用。虽然“select”是可用的,但是在 Windows 上,它只能接受套接字作为参数——sys.stdin 和文件描述符是 Unix 的东西。下次我会确保先进行测试。 - Great Turtle
21
可恶,不过,有哪个自重的程序员会使用Windows呢? ;) 对于简单的用户输入,我猜可以用一个围绕着“kbhit”的循环来完成,它可以检测键盘按键,并使用“getch”和“time.sleep”以在超时后退出。但这会很丑陋。 - Pontus
3
如果您打算在此调用之后再次从标准输入读取数据,那么当读取超时时,最好执行termios.tcflush(sys.stdin, termios.TCIFLUSH)。否则,如果用户输入了字符但没有按Enter键,终端仿真器可能会允许用户按退格键并擦除程序的后续输出(直到用户输入的字符数)。 - iafisher

51

你提供的示例是错误的,实际上异常发生在调用闹钟处理程序时而不是阻塞读取时。建议使用以下代码:

import signal
TIMEOUT = 5 # number of seconds your want for timeout

def interrupted(signum, frame):
    "called when read times out"
    print 'interrupted!'
signal.signal(signal.SIGALRM, interrupted)

def input():
    try:
            print 'You have 5 seconds to type in your stuff...'
            foo = raw_input()
            return foo
    except:
            # timeout
            return

# set alarm
signal.alarm(TIMEOUT)
s = input()
# disable the alarm after success
signal.alarm(0)
print 'You typed', s

今天我一直在努力实现带有超时的键盘输入。我只是想要一种停止从硬盘中复制图像的方法,以便我可以通过按键来停止它,因此我想要一个小的超时时间(33毫秒)。我只想指出,在stackoverflow上找到的一些解决方案在IDLE上不起作用!(我不知道为什么)。您必须在终端上执行它们。而且,我在互联网上找到的最有用的代码是这个:http://home.wlu.edu/~levys/software/kbhit.py。祝你好运! - jespestana
7
我尝试了这个解决方案,但在Python3中并没有奏效。你需要在被中断的函数中引发一个错误,以便在定义的输入函数中捕获该异常 - 这将使其在Python3中奏效 :) - rnbguy
10
这对我没用。它只会在5秒后打印出“interrupted”,但它实际上并没有停止input的执行。它仍然等待用户按下Enter键,即使在“Interrupted”消息出现后输入任何文本,它也会将其打印出来。在Linux上使用Python 2和3进行了测试。 - tobias_k
1
一个指向库文档的链接对于调试如果某人无法正常工作将非常有用。 - Btc Sources
1
你需要为此定义一个处理程序。例如, “def handler(signum, frame): raise IOError”,然后“signal.signal(signal.SIGALRM, handler)”。 - Phillip1982

20

如果您不关心它是如何工作的,只需执行
pip install inputimeout
即可。

from inputimeout import inputimeout, TimeoutOccurred

if __name__ == "__main__":
    try:
        c = inputimeout(prompt='hello\n', timeout=3)
    except TimeoutOccurred:
        c = 'timeout'
    print(c)

非常简单
https://pypi.org/project/inputimeout/


2
FYI:PyPi的链接有一个错别字,目前有打开的PR(#6 /#9)来修复它。源代码在这里:https://github.com/johejo/inputimeout - KyleKing
适用于Windows! - FloPinguin

17

虽然不是Python解决方案,但...

我在运行CentOS (Linux)下的脚本时遇到了这个问题,对于我的情况,只需在子进程中运行Bash "read -t"命令即可解决。这是一种残酷、恶心的hack方式,但它的工作效果让我感到内疚,我希望与大家分享。

import subprocess
subprocess.call('read -t 30', shell=True)

我需要的是等待30秒,除非按下回车键。 这个方案非常有效。


13

这里有一个在Windows上运行的示例

我无法让这些示例在Windows上运行,所以我合并了一些不同的StackOverflow答案得到了以下内容:


import threading, msvcrt
import sys

def readInput(caption, default, timeout = 5):
    class KeyboardThread(threading.Thread):
        def run(self):
            self.timedout = False
            self.input = ''
            while True:
                if msvcrt.kbhit():
                    chr = msvcrt.getche()
                    if ord(chr) == 13:
                        break
                    elif ord(chr) >= 32:
                        self.input += chr
                if len(self.input) == 0 and self.timedout:
                    break    


    sys.stdout.write('%s(%s):'%(caption, default));
    result = default
    it = KeyboardThread()
    it.start()
    it.join(timeout)
    it.timedout = True
    if len(it.input) > 0:
        # wait for rest of input
        it.join()
        result = it.input
    print ''  # needed to move to next line
    return result

# and some examples of usage
ans = readInput('Please type a name', 'john') 
print 'The name is %s' % ans
ans = readInput('Please enter a number', 10 ) 
print 'The number is %s' % ans 

2
我刚意识到我不需要使用线程。请查看相同的代码,但没有线程,网址为https://dev59.com/cHA75IYBdhLWcg3wGlEa#3911560。 - Paul
这似乎在Windows上无法工作。我正在运行您的代码,除了将Print更改为py3语法并添加stdout.flush()之外,其他都一样。Windows7,python3.6 - some bits flipped
2
在Python 3中,用print(prompt, end='', flush=True)替换sys.stdout.write来打印提示信息。 - Anakhand

11

保罗的答案并没有完全起作用。 下面是修改后的代码,这在我的以下设置中可以运行:

  • windows 7 x64

  • 普通CMD shell(例如,不是 git-bash或其他非M $ shell)

    -- 在git-bash中似乎没有任何msvcrt可用。

  • python 3.6

(我发布了一个新答案,因为直接编辑Paul的答案会将其从python 2.x更改为3.x,这似乎对编辑来说太过于繁琐(py2仍在使用)

import sys, time, msvcrt

def readInput( caption, default, timeout = 5):

    start_time = time.time()
    sys.stdout.write('%s(%s):'%(caption, default))
    sys.stdout.flush()
    input = ''
    while True:
        if msvcrt.kbhit():
            byte_arr = msvcrt.getche()
            if ord(byte_arr) == 13: # enter_key
                break
            elif ord(byte_arr) >= 32: #space_char
                input += "".join(map(chr,byte_arr))
        if len(input) == 0 and (time.time() - start_time) > timeout:
            print("timing out, using default value.")
            break

    print('')  # needed to move to next line
    if len(input) > 0:
        return input
    else:
        return default

# and some examples of usage
ans = readInput('Please type a name', 'john') 
print( 'The name is %s' % ans)
ans = readInput('Please enter a number', 10 ) 
print( 'The number is %s' % ans) 

这里的格式不像我期望的那样工作。我被难住了,在Meta上提问了:http://meta.stackexchange.com/q/290162/208995 - some bits flipped
当我尝试这样做时,我无法输入任何内容(它等待5秒钟而不让我输入任何内容)。 - Varun Vejalla

5

我花了大约20分钟的时间,所以我认为把这个分享出来是值得一试的。这是直接基于user137673的回答建立的。我发现做类似这样的事情非常有用:

#! /usr/bin/env python

import signal

timeout = None

def main():
    inp = stdinWait("You have 5 seconds to type text and press <Enter>... ", "[no text]", 5, "Aw man! You ran out of time!!")
    if not timeout:
        print "You entered", inp
    else:
        print "You didn't enter anything because I'm on a tight schedule!"

def stdinWait(text, default, time, timeoutDisplay = None, **kwargs):
    signal.signal(signal.SIGALRM, interrupt)
    signal.alarm(time) # sets timeout
    global timeout
    try:
        inp = raw_input(text)
        signal.alarm(0)
        timeout = False
    except (KeyboardInterrupt):
        printInterrupt = kwargs.get("printInterrupt", True)
        if printInterrupt:
            print "Keyboard interrupt"
        timeout = True # Do this so you don't mistakenly get input when there is none
        inp = default
    except:
        timeout = True
        if not timeoutDisplay is None:
            print timeoutDisplay
        signal.alarm(0)
        inp = default
    return inp

def interrupt(signum, frame):
    raise Exception("")

if __name__ == "__main__":
    main()

很棒的解决方案。在Python3中运行非常好。无法给它点赞。 - Regis May

5

以下代码对我有用。

我使用了两个线程,一个用于获取原始输入(raw_Input),另一个等待特定的时间。 如果其中任何一个线程退出,则两个线程都将被终止并返回。

def _input(msg, q):
    ra = raw_input(msg)
    if ra:
        q.put(ra)
    else:
        q.put("None")
    return

def _slp(tm, q):
    time.sleep(tm)
    q.put("Timeout")
    return

def wait_for_input(msg="Press Enter to continue", time=10):
    q = Queue.Queue()
    th = threading.Thread(target=_input, args=(msg, q,))
    tt = threading.Thread(target=_slp, args=(time, q,))

    th.start()
    tt.start()
    ret = None
    while True:
        ret = q.get()
        if ret:
            th._Thread__stop()
            tt._Thread__stop()
            return ret
    return ret

print time.ctime()    
t= wait_for_input()
print "\nResponse :",t 
print time.ctime()

4

对于Linux系统,我更喜欢由@Pontus提供的select版本。在此,只需一个Python3函数即可像shell中的read一样工作:

import sys, select

def timeout_input(prompt, timeout=3, default=""):
    print(prompt, end=': ', flush=True)
    inputs, outputs, errors = select.select([sys.stdin], [], [], timeout)
    print()
    return (0, sys.stdin.readline().strip()) if inputs else (-1, default)

运行

In [29]: timeout_input("Continue? (Y/n)", 3, "y")                                                                                                                                                                  
Continue? (Y/n): 
Out[29]: (-1, 'y')

In [30]: timeout_input("Continue? (Y/n)", 3, "y")                                                                                                                                                                  
Continue? (Y/n): n

Out[30]: (0, 'n')

还有一个yes_or_no函数

In [33]: yes_or_no_3 = lambda prompt: 'n' not in timeout_input(prompt + "? (Y/n)", 3, default="y")[1].lower()                                                                                                      

In [34]: yes_or_no_3("Continue")                                                                                                                                                                                   
Continue? (Y/n): 
Out[34]: True

In [35]: yes_or_no_3("Continue")                                                                                                                                                                                   
Continue? (Y/n): no

Out[35]: False

对于那些想知道的人 - 这在Ubuntu 18.04 / 20.04和Debian 10(Buster)上的Python 3.7和3.8上完美运行。简短,简单,而且非常有效! - Someguy123

4
这里是一个便携且简单的Python 3解决方案,使用线程。这是唯一一个在跨平台时对我有效的解决方案。
其他我尝试过的方法都有问题: - 使用signal.SIGALRM:在Windows上无法工作 - 使用select调用:在Windows上无法工作 - 使用强制终止进程(而不是线程):无法在新进程中使用stdin(stdin会自动关闭) - 将stdin重定向到StringIO并直接写入stdin:如果已经调用了input(),仍然会写入先前的stdin(参见https://dev59.com/FmUp5IYBdhLWcg3wx5tu#15055639
    from threading import Thread
    class myClass:
        _input = None

        def __init__(self):
            get_input_thread = Thread(target=self.get_input)
            get_input_thread.daemon = True  # Otherwise the thread won't be terminated when the main program terminates.
            get_input_thread.start()
            get_input_thread.join(timeout=20)

            if myClass._input is None:
                print("No input was given within 20 seconds")
            else:
                print("Input given was: {}".format(myClass._input))


        @classmethod
        def get_input(cls):
            cls._input = input("")
            return

3
这种方法可行,但会在超时后使线程继续运行。 - bgusach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接