【问题标题】:Is it possible to do two consecutive, successful non-blocking reads of stdin in Python?是否可以在 Python 中对标准输入进行两次连续、成功的非阻塞读取?
【发布时间】:2023-12-01 22:38:01
【问题描述】:

为长代码帖子道歉,但我相信这是有用的上下文。

我正在尝试在原始 Python 中解析特殊键(没有诅咒),但似乎 select 进行非阻塞输入的技巧在这种情况下不起作用。特别是,在读取输入的第一个字符后,select 似乎返回 stdin 不可读,尽管还有更多的输入字符要读取。

重现问题的步骤:

  1. 运行下面的代码。
  2. 按左箭头键(或任何其他命名的特殊键)。
  3. 观察输出是ESC,后跟单独行中的转义序列的其余部分。预期行为:输出 ARROW_LEFT

是否可以正确读取特殊键的完整转义序列,同时仍能正确读取 ESC 本身?

#!/usr/bin/env python3

import sys
from enum import Enum
import tty
import termios
import select
import signal

# Takes a given single-character string and returns the string control version
# of it. For example, it takes 'c' and returns the string representation of
# Control-C.  This can be used to check for control-x keys in the output of
# readKey.
def controlKey(c):
  return chr(ord(c) & 0x1f)

def nonblock_read(stream, limit=1):
  if select.select([stream,],[],[],0.1)[0]:
    return stream.read(limit)
  return None

# Read a key of input as a string. For special keys, it returns a
# representative string. For control keys, it returns the raw string.
# This function assumes that the caller has already put the terminal in raw mode.
def readKey():
  c = nonblock_read(sys.stdin, 1)
  if not c: return None
  # Handle special keys represented by escape sequences
  if c == "\x1b":
    seq = [None] * 3
    seq[0] = nonblock_read(sys.stdin, 1)
    if not seq[0]: return "ESC"
    seq[1] = nonblock_read(sys.stdin, 1)
    if not seq[1]: return "ESC"

    if seq[0] == '[':
      if seq[1] >= '0' and seq[1] <= '9':
        seq[2] = nonblock_read(sys.stdin, 1)
        if not seq[2]: return "ESC"

        if seq[2] == '~':
          if seq[1] == '1': return "HOME_KEY"
          if seq[1] == '3': return "DEL_KEY"
          if seq[1] == '4': return "END_KEY"
          if seq[1] == '5': return "PAGE_UP"
          if seq[1] == '6': return "PAGE_DOWN"
          if seq[1] == '7': return "HOME_KEY"
          if seq[1] == '8': return "END_KEY"
      else:
        if seq[1] == 'A': return "ARROW_UP"
        if seq[1] == 'B': return "ARROW_DOWN"
        if seq[1] == 'C': return "ARROW_RIGHT"
        if seq[1] == 'D': return "ARROW_LEFT"
        if seq[1] == 'H': return "HOME_KEY"
        if seq[1] == 'F': return "END_KEY"
    elif seq[0] == 'O':
      if seq[1] == 'H': return "HOME_KEY"
      if seq[1] == 'F': return "END_KEY"
    return 'ESC'
  return c

def main():
  # Save terminal settings
  fd = sys.stdin.fileno()
  old_tty_settings = termios.tcgetattr(fd)
  # Enter raw mode
  tty.setraw(sys.stdin)
  ################################################################################  
  interrupt = controlKey("c")
  while True:
    s = readKey()
    if s:
      print(f"{s}", end="\r\n")
    if s == interrupt:
      break
  ################################################################################  
  # Exit raw mode
  fd = sys.stdin.fileno()
  termios.tcsetattr(fd, termios.TCSADRAIN, old_tty_settings)

if __name__ == "__main__":
  main()

【问题讨论】:

    标签: python python-3.x linux select stdin


    【解决方案1】:

    如果您使用低级 I/O,我认为它可以工作。 select.select 将接受数字文件描述符。我没有尝试将其与您的程序集成,但可以尝试一下。如果你按下例如,你应该得到一个字符序列。左箭头。原始文件似乎不适用于 sys.stdin,但 fd 0 可以。注意 os.read 从数字文件描述符中读取。

    import os
    import sys
    import select
    import tty
    import termios
    
    def read_all_available(fd):
        "do a single blocking read plus non-blocking reads while any more data exists"
        if not select.select([fd],[],[], None)[0]:
            return None
        val = os.read(fd, 1)
        while select.select([fd],[],[], 0)[0]:
            val += os.read(fd, 1)
        return val
    
    
    data = None
    while data != b'\x03':
        old_settings = termios.tcgetattr(0)
        tty.setraw(sys.stdin)
        data = read_all_available(0)
    
        # reset settings here just to allow tidier printing to screen
        termios.tcsetattr(0, termios.TCSADRAIN, old_settings)
        print(data, len(data))
    

    【讨论】:

    • 我猜select 只是因为对象sys.stdin 而坏掉了?
    • @merlin2011 不确定。显然存在某种缓冲。
    最近更新 更多