为什么我的代码在到达终端末尾时会打印多行相同的行?

问题描述 投票:0回答:1
import time
import sys

def animate_string(input_string, delay_second=0.01):
    current_string = ""
    for target_char in input_string:
        if target_char == '\n':
            # Print a newline and reset current_string
            print()
            current_string = ""
            continue

        current_char = 'a'
        while current_char != target_char:
            a_code = ord(current_char)
            # previous string + current letter
            sys.stdout.write('\r' + current_string + current_char)
            sys.stdout.flush()
            time.sleep(delay_second)
            # Move to the next char (used ASCII)
            current_char = chr(ord(current_char) + 1)
            # Handle bound issue
            if current_char == '{':  # After 'z'
                current_char = 'A'
            elif current_char == '[':  # After 'Z'
                current_char = target_char
        # Add the target character to the current string
        current_string += target_char
        # Print the updated current string
        sys.stdout.write('\r' + current_string)
        sys.stdout.flush()
    # Print a newline at the end to finish the output cleanly
    print()

animate_string("fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2", 0.0002)

这是我的代码。我正在以一种很酷的方式打印字符串。它适用于少量字符,但当字符串变得足够长以到达终端末尾时,它开始一遍又一遍地打印同一行。为什么会发生这种情况以及如何解决这个问题?

python terminal printing sys
1个回答
0
投票

问题

正如评论之一所指出的,回车符 (

"\r"
) 将光标移动到当前行的开头。 当文本需要显示的终端列多于终端宽度时,文本将跨多行显示。 因此,当向终端写入“长”字符串时,光标将在与输出开始的行不同的行上结束。 任何后续的
"\r"
然后将光标移动到该 final 行的开头。

示例

假设终端宽度为 50 列,并且您尝试为 60 个字符的字符串设置动画

"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678"

target_char
包含第 41 个字符 (
O
) 时,这是第一个后续
sys.stdout.write
调用的结果(带行号):

1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNa
  ^                                        ^ cursor is here
  cursor will be returned here by "\r"

target_char
包含第 51 个字符 (
Y
) 时,这是第一个后续
sys.stdout.write
调用的结果(带行号):

1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
2 a
  ^^ cursor is here
  cursor will be returned here by "\r"

下一次调用

sys.stdout.write
(当
current_char
包含
b
时)会将光标移动到第二行的开头并仅覆盖
a
,结果是:

1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
2 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
3 b
  ^^ cursor is here
  cursor will be returned here by "\r"

从现在开始,每个

sys.stdout.write
调用都会在输出中添加一个新行。

解决方案

一种解决方案是在“循环”动画期间仅覆盖单个字符,而不是整行。 使用

"\b"
将光标向后移动一列最初似乎可以工作,但由于某种原因,不会跨行“向后换行”。 但是,使用 ANSI 控制序列来设置光标位置确实有效:

import os
import sys
import time

def animate_string(input_string, delay_second=0.001):
    stdout = sys.stdout
    if not stdout.isatty():
        # Standard output is not a terminal; just output the string
        stdout.write(f"{input_string}\n")
        return

    # Get the size of the terminal to control line breaks correctly
    cols, rows = os.get_terminal_size(stdout.fileno())
    # Assume we are at the start of a new line
    current_col = 1
    for target_char in input_string:
        if target_char == "\n":
            # Write a newline and reset `current_col`
            stdout.write(target_char)
            current_col = 1
            continue

        # Escape sequence that sets the horizontal position of the cursor to `current_col`
        # Setting this to "\b" does not work for long strings
        undo_seq = f"\x1b[{current_col}G"

        current_char = "a"
        time.sleep(delay_second)
        stdout.write(current_char)
        stdout.flush()
        while current_char != target_char:
            # Move to the next char (used ASCII)
            current_char = chr(ord(current_char) + 1)
            # Handle bound issue
            if current_char == "{":  # After 'z'
                current_char = "A"
            elif current_char == "[":  # After 'Z'
                current_char = target_char

            time.sleep(delay_second)
            # "Undo" sequence followed by current letter
            stdout.write(f"{undo_seq}{current_char}")
            stdout.flush()

        if current_col >= cols:
            # Detect line wrapping performed by the terminal
            current_col = 1
        else:
            current_col += 1

    # Print a newline at the end to finish the output cleanly
    stdout.write("\n")

我们使用

os.get_terminal_size
来获取终端宽度。 我们跟踪
current_col
中的当前列,并使用
CHA
ANSI 控制序列
在“循环”动画期间重置光标位置。

此外,我们使用

stdout.isatty
检查标准输出是否是终端,如果不是,我们只需将输入写出即可。

最后,我稍微移动了

time.sleep
调用,以使时间更加一致。

假设

这个解决方案并不完美,依赖于以下假设:

  • 调用此函数时光标位于行首。
  • 每个字符恰好占据一个终端列。
  • 终端支持所使用的 ANSI 转义序列(我已在 xterm、Alacritty 和 WezTerm 中对其进行了测试)。

对于稍微更高级的光标控制,这个问题及其答案,以及有关 ANSI 转义序列的维基百科条目,可能是一个很好的起点。 您可能也会对 Python 标准库中的

termios
tty
模块感兴趣,尽管它们并非随处可用。

© www.soinside.com 2019 - 2024. All rights reserved.