简体   繁体   English

从 sys.stdin 获取输入,非阻塞

[英]Taking input from sys.stdin, non-blocking

I'm working on a bot for a competition that receives its input through sys.stdin and uses Python's print() for output. I have the following:我正在为一个竞赛开发一个机器人,它通过sys.stdin接收输入并使用 Python 的print()来表示 output。我有以下内容:

import sys

def main():
    while True:
        line = sys.stdin.readline()
        parts = line.split()
        if len(parts) > 0:
            # do stuff

The problem is that the input comes in through a stream and using the above, blocks me from printing anything back until the stream is closed.问题是输入通过 stream 进入并使用上面的内容,阻止我打印任何东西,直到 stream 关闭。 What can I do to make this work?我该怎么做才能完成这项工作?

By turning blocking off you can only read a character at a time.通过关闭阻止,您一次只能读取一个字符。 So, there is no way to get readline() to work in a non-blocking context.所以,没有办法让readline()在非阻塞上下文中工作。 I assume you just want to read key presses to control the robot.我假设您只想阅读按键来控制机器人。

I have had no luck using select.select() on Linux and created a way with tweaking termios settings.我在 Linux 上使用select.select()没有运气,并创建了一种调整termios设置的方法。 So, this is Linux specific but works for me:所以,这是特定于 Linux 的,但对我有用:

import atexit, termios
import sys, os
import time


old_settings=None

def init_anykey():
   global old_settings
   old_settings = termios.tcgetattr(sys.stdin)
   new_settings = termios.tcgetattr(sys.stdin)
   new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags
   new_settings[6][termios.VMIN] = 0  # cc
   new_settings[6][termios.VTIME] = 0 # cc
   termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)

@atexit.register
def term_anykey():
   global old_settings
   if old_settings:
      termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

def anykey():
   ch_set = []
   ch = os.read(sys.stdin.fileno(), 1)
   while ch != None and len(ch) > 0:
      ch_set.append( ord(ch[0]) )
      ch = os.read(sys.stdin.fileno(), 1)
   return ch_set;

init_anykey()
while True:
   key = anykey()
   if key != None:
      print key
   else:
      time.sleep(0.1)

A better Windows or cross-platform answer is here: Non-blocking console input?更好的 Windows 或跨平台答案在这里: 非阻塞控制台输入?

You can use selectors for handle I/O multiplexing:您可以使用选择器来处理 I/O 多路复用:

https://docs.python.org/3/library/selectors.html https://docs.python.org/3/library/selectors.html

Try this out:试试这个:

#! /usr/bin/python3

import sys
import fcntl
import os
import selectors

# set sys.stdin non-blocking
orig_fl = fcntl.fcntl(sys.stdin, fcntl.F_GETFL)
fcntl.fcntl(sys.stdin, fcntl.F_SETFL, orig_fl | os.O_NONBLOCK)

# function to be called when enter is pressed
def got_keyboard_data(stdin):
    print('Keyboard input: {}'.format(stdin.read()))

# register event
m_selector = selectors.DefaultSelector()
m_selector.register(sys.stdin, selectors.EVENT_READ, got_keyboard_data)

while True:
    sys.stdout.write('Type something and hit enter: ')
    sys.stdout.flush()
    for k, mask in m_selector.select():
        callback = k.data
        callback(k.fileobj)

The above code will hold on the line上面的代码将保持在线

for k, mask in m_selector.select():

until a registered event occurs, returning a selector_key instance (k) and a mask of monitored events.直到一个注册的事件发生,返回一个 selector_key 实例 (k) 和一个被监控事件的掩码。

In the above example we registered only one event ( Enter key press ):在上面的例子中,我们只注册了一个事件(回车键按下):

m_selector.register(sys.stdin, selectors.EVENT_READ, got_keyboard_data)

The selector key instance is defined as follows:选择器键实例定义如下:

abstractmethod register(fileobj, events, data=None)

Therefore, the register method sets k.data as our callback function got_keyboard_data , and calls it when the Enter key is pressed :因此, register 方法将 k.data 设置为我们的回调函数got_keyboard_data ,并在按下Enter键时调用它:

    callback = k.data
    callback(k.fileobj)

A more complete example (and hopefully more useful) would be to multiplex stdin data from user with incomming connections from network:一个更完整的示例(希望更有用)是将来自用户的 stdin 数据与来自网络的传入连接复用:

import selectors
import socket
import sys
import os
import fcntl

m_selector = selectors.DefaultSelector()

# set sys.stdin non-blocking
def set_input_nonblocking():
    orig_fl = fcntl.fcntl(sys.stdin, fcntl.F_GETFL)
    fcntl.fcntl(sys.stdin, fcntl.F_SETFL, orig_fl | os.O_NONBLOCK)

def create_socket(port, max_conn):
    server_addr = ('localhost', port)
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.setblocking(False)
    server.bind(server_addr)
    server.listen(max_conn)
    return server

def read(conn, mask):
    global GO_ON
    client_address = conn.getpeername()
    data = conn.recv(1024)
    print('Got {} from {}'.format(data, client_address))
    if not data:
         GO_ON = False

def accept(sock, mask):
    new_conn, addr = sock.accept()
    new_conn.setblocking(False)
    print('Accepting connection from {}'.format(addr))
    m_selector.register(new_conn, selectors.EVENT_READ, read)

def quit():
    global GO_ON
    print('Exiting...')
    GO_ON = False


def from_keyboard(arg1, arg2):
    line = arg1.read()
    if line == 'quit\n':
        quit()
    else:
        print('User input: {}'.format(line))

GO_ON = True
set_input_nonblocking()

# listen to port 10000, at most 10 connections
server = create_socket(10000, 10)

m_selector.register(server, selectors.EVENT_READ, accept)
m_selector.register(sys.stdin, selectors.EVENT_READ, from_keyboard)

while GO_ON:
    sys.stdout.write('>>> ')
    sys.stdout.flush()
    for k, mask in m_selector.select():
        callback = k.data
        callback(k.fileobj, mask)


# unregister events
m_selector.unregister(sys.stdin)

# close connection
server.shutdown()
server.close()

#  close select
m_selector.close()

You can test using two terminals.您可以使用两个终端进行测试。 first terminal:第一个终端:

$ python3 test.py 
>>> bla

open another terminal and run:打开另一个终端并运行:

 $ nc localhost 10000
 hey!

back to the first回到第一个

>>> qwerqwer     

Result (seen on the main terminal):结果(在主终端上看到):

$ python3 test.py 
>>> bla
User input: bla

>>> Accepting connection from ('127.0.0.1', 39598)
>>> Got b'hey!\n' from ('127.0.0.1', 39598)
>>> qwerqwer     
User input: qwerqwer

>>> 
#-----------------------------------------------------------------------
# Get a character from the keyboard.  If Block is True wait for input,
# else return any available character or throw an exception if none is
# available.  Ctrl+C isn't handled and continues to generate the usual
# SIGINT signal, but special keys like the arrows return the expected 
# escape sequences.
#
# This requires:
#
#    import sys, select
#
# This was tested using python 2.7 on Mac OS X.  It will work on any
# Linux system, but will likely fail on Windows due to select/stdin
# limitations.
#-----------------------------------------------------------------------

def GetChar(Block=True):
  if Block or select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
    return sys.stdin.read(1)
  raise error('NoChar')

This is a posix solution, close to that of swdev .这是一个 posix 解决方案,接近swdev 的解决方案。

As he stated, you have to play with VMIN an VTIME to catch more than one char without requiring user to press [enter].正如他所说,您必须使用 VMIN 和 VTIME 来捕获多个字符,而无需用户按 [enter]。 Trying to only use raw mode will be a problem as special keys like arrows can mess next keypress.尝试仅使用原始模式将是一个问题,因为像箭头这样的特殊键可能会弄乱下一次按键。

Here we use tty.setcbreak() or tty.setraw() as a shortcut, but they have short internals .这里我们使用 tty.setcbreak() 或 tty.setraw() 作为快捷方式,但它们的内部结构很短

import termios
import tty
import sys
import select

def get_enter_key():
    fd = sys.stdin.fileno()
    orig_fl = termios.tcgetattr(fd)
    try:
        tty.setcbreak(fd)  # use tty.setraw() instead to catch ^C also
        mode = termios.tcgetattr(fd)
        CC = 6
        mode[CC][termios.VMIN] = 0
        mode[CC][termios.VTIME] = 0
        termios.tcsetattr(fd, termios.TCSAFLUSH, mode)
        keypress, _, _ = select.select([fd], [], [])
        if keypress:
            return sys.stdin.read(4095)
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, orig_fl)

try:
    while True:
        print(get_enter_key())
except KeyboardInterrupt:
    print('exiting')
    sys.exit()

note that there are two potential timeouts you could add here:请注意,您可以在此处添加两个潜在的超时:

Might I suggest nobreak ?我可以建议nobreak吗? If'n you are willing to use curses.如果你愿意使用诅咒。

https://docs.python.org/3/library/curses.html#curses.window.nodelay https://docs.python.org/3/library/curses.html#curses.window.nodelay

You should be able to get read of a stream with either您应该能够通过以下任一方法读取 stream

sys.stdin.read(1)

to read utf-8 decoded chars or:读取 utf-8 解码字符或:

sys.stdin.buffer.read(1)

to read raw chars.读取原始字符。

I would do this if I wanted to get raw data from the stdin and do something with it in a timely manner, without reading a newline or filling up the internal buffer first.如果我想从 stdin 获取原始数据并及时对其进行处理,而不先读取换行符或填充内部缓冲区,我会这样做。 This is suitable for running programs remotely via ssh where tty is not available, see:这适用于在 tty 不可用的情况下通过 ssh 远程运行程序,请参阅:

ssh me@host '/usr/bin/python -c "import sys; print(sys.stdin.isatty())"'

There are some other things to think about to make programs work as expected in this scenario.要使程序在这种情况下按预期工作,还需要考虑其他一些事情。 You need to flush the output when you're done to avoid buffering delays, and it could be easy to assume a program hasn't read the input, when you've simply not flushed the output.完成后需要刷新 output 以避免缓冲延迟,并且当您根本没有刷新 output 时,很容易假设程序没有读取输入。

stdout.write("my data")
stdout.flush()

But usually it's not the input reading that's the problem but that the terminal (or program) supplying the input stream is not handing it over when you expect, or perhaps it's not reading your output when you expect.但通常问题不是输入读数,而是提供输入 stream 的终端(或程序)没有在您期望的时候移交它,或者它可能没有在您期望的时候读取您的 output。 If you have a tty to start with (see ssh check above) you can put it into raw mode with the tty module.如果您有一个 tty 开始(请参阅上面的 ssh 检查),您可以使用 tty 模块将其置于原始模式。

import sys
import termios
import tty

old = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin)
c = None
try:
    c = sys.stdin.read(1)[0]
finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
print(c)

... if using Mac/Linux. ... 如果使用 Mac/Linux。 If using Windows you could use msvcrt.getch().如果使用 Windows,您可以使用 msvcrt.getch()。

Use a generator - thankfully sys.stdin is already a generator!使用生成器 - 幸好sys.stdin已经是一个生成器!

A generator enables you to work on an infinite stream.生成器使您能够处理无限流。 Always when you call it, it returns the next element.当你调用它时,它总是返回下一个元素。 In order to build a generator you need the yield keyword.为了构建生成器,您需要使用yield关键字。

for line in sys.stdin:
    print line

    if a_certain_situation_happens:
        break        

Do not forget to place a break statement into the loop if a certain, wished situation happens.如果某种特定的情况发生,不要忘记在循环中放置一个break语句。

You can find more information about generators on:您可以在以下位置找到有关生成器的更多信息:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM