PyPtt - 如何利用 NAWS 動態調整視窗高度提升資料獲取效率

在使用 PyPtt 進行自動化開發時,許多開發者可能會好奇:為什麼某些操作(如抓取看板文章)在不同環境下,速度差異如此巨大?除了網路品質外,核心差異往往在於是否有效利用了 Telnet 的 NAWS 協定(Negotiate About Window Size)

今天我們將深入剖析 NAWS 的運作原理,並對照 PyPtt 的真實原始碼,了解這個機制如何被完整實作。

挑戰:為什麼標準 BBS 資料獲取很慢?

PTT 是基於 Telnet 協定的 BBS。傳統的 Telnet 客戶端預設視窗高度通常為 24 行。這表示,當程式需要獲取一百篇文章時,它必須進行多次「分頁(Paging)」互動:

  1. 請求一頁(24 行)
  2. 等待網路傳輸
  3. 解析終端機內容
  4. 發送「翻頁」指令
  5. 重複以上步驟

頻繁的網路往返(Round-trips)與解析開銷,是 BBS 資料獲取效能的主要瓶頸。而解開這個瓶頸的鑰匙,正是 NAWS

NAWS 協定深度解析

什麼是 NAWS?

NAWS(Negotiate About Window Size)定義於 RFC 1073,是 Telnet 協定的標準擴充選項(Option Code = 31 / 0x1F)。它允許 Telnet 客戶端主動告知伺服器目前終端機的視窗尺寸(寬度與高度),讓伺服器能依據這個資訊調整輸出的分頁行為。

在沒有 NAWS 的情況下,PTT 伺服器只能預設以 24 行為一頁,不論客戶端實際上能顯示多少內容。

協商交握流程(Telnet Option Negotiation)

NAWS 的啟用分為兩個階段:能力協商尺寸通報

Phase 1 - 能力協商

客戶端發送 WILL NAWS,表示「我支援 NAWS,我會通報我的視窗大小」:

1
2
IAC  WILL  NAWS
0xFF 0xFB 0x1F

伺服器若支援,回應 DO NAWS,表示「好,請告訴我你的視窗大小」:

1
2
IAC  DO   NAWS
0xFF 0xFD 0x1F

Phase 2 - 尺寸通報(Subnegotiation)

協商完成後,客戶端透過 Subnegotiation(SB)封包傳送實際的視窗尺寸:

1
2
IAC  SB   NAWS  [寬度 高位] [寬度 低位] [高度 高位] [高度 低位] IAC  SE
0xFF 0xFA 0x1F 0x00 0x50 0x00 0x64 0xFF 0xF0

以上範例通報的尺寸為:

  • 寬度:0x0050 = 80 欄
  • 高度:0x0064 = 100 行

每當視窗尺寸改變,客戶端可以重新發送 SB 封包更新伺服器端的認知。PTT 伺服器在收到這個封包後,會立即以新的行數作為每頁的輸出量。

PyPtt 的 NAWS 實作

connect_core.py 負責底層的 WebSocket/Telnet 連線。在連線建立後,PyPtt 立即發出兩段連在一起的 NAWS 封包:第一段是 IAC WILL NAWS 能力宣告,第二段是 IAC SB 尺寸通報。

1
2
3
4
5
6
7
8
9
10
11
12
# connect_core.py L217–L227
# Respond to PTT's IAC DO NAWS (ff fd 1f) and announce terminal size.
# PTT honours any height >= 24; larger values reduce get_content iterations.
# PTT's hard cap is 100 rows regardless of what we send.
# h is validated to 24–254; 255 is excluded because 0xFF inside a
# Telnet SB payload must be doubled (RFC 854) — simpler to cap at 254.
h = self.config.screen_height
naws = (
b'\xff\xfb\x1f' # IAC WILL NAWS
+ b'\xff\xfa\x1f\x00\x50' + bytes([0, h]) + b'\xff\xf0' # IAC SB NAWS 80×h IAC SE
)
loop.run_until_complete(self._core.send(naws))

注意:RFC 854 規定 Telnet SB payload 中的 0xFF 必須被跳脫為 0xFF 0xFF,因此高度上限刻意設為 254 而非 255,以避免手動處理轉義邏輯。

除了連線時的初始化,connect_core.py 也實作了 set_screen_height() 方法,允許在 session 進行中隨時重新發送 NAWS 封包來調整視窗高度:

1
2
3
4
5
6
7
8
9
10
# connect_core.py L452–L460
def set_screen_height(self, height: int) -> None:
"""Send a mid-session NAWS to resize the terminal, then update config."""
naws = (
b'\xff\xfb\x1f'
+ b'\xff\xfa\x1f\x00\x50' + bytes([0, height]) + b'\xff\xf0'
)
loop = self._get_event_loop()
loop.run_until_complete(self._core.send(naws))
self.config.screen_height = height

這個設計讓 PyPtt 能夠在需要大量獲取資料的操作前臨時擴展視窗,完成後再還原,而不影響其他一般操作的預設行為。

VT100Parser 的狀態對齊

僅僅發送 NAWS 封包是不夠的。PyPtt 內部維護了一個 VT100Parser 來模擬終端機畫面。如果 Parser 不知道視窗已經擴大,它將無法正確處理超出 24 行的資料——後面的行會被靜默丟棄。

connect_core.py 可以看到,VT100Parser 在每次解析畫面時,都會直接從 config.screen_height 取得當前視窗高度:

1
2
# connect_core.py L254
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding, self.config.screen_height)

這意味著只要 set_screen_height() 同步更新了 config.screen_height,下一次解析就會自動用新的高度建立 Parser,不需要額外的同步步驟。

NAWS 封包決定伺服器「輸出多少」;VT100Parser 的高度參數決定客戶端「接收多少」。set_screen_height() 同時更新兩者,確保兩端始終一致。

expanded_screen:封裝成上下文管理器

_api_util.pyset_screen_height() 進一步封裝成 expanded_screen 上下文管理器,讓呼叫端不需要手動管理還原邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# _api_util.py L18–L36
_MAX_GET_CONTENT_HEIGHT = 100 # PTT server hard cap

@contextmanager
def expanded_screen(api):
"""Temporarily resize the PTT terminal to its maximum (100 rows) and restore on exit."""
original_height = api.config.screen_height
if original_height < _MAX_GET_CONTENT_HEIGHT:
api.connect_core.set_screen_height(_MAX_GET_CONTENT_HEIGHT)
try:
yield
finally:
if original_height < _MAX_GET_CONTENT_HEIGHT:
api.connect_core.set_screen_height(original_height)

_MAX_GET_CONTENT_HEIGHT = 100 是 PTT 伺服器的硬性上限;不論你在 NAWS 封包裡填入多大的數字,PTT 最多只會輸出 100 行。

在 PyPtt 內部,get_content()(負責讀取文章全文)就是直接以 expanded_screen 包裹整個獲取迴圈:

1
2
3
4
# _api_util.py L55
with expanded_screen(api):
# 翻頁迴圈,每次最多讀取 100 行而非 24 行
...

實作指南:如何在您的程式中使用?

若您自己的邏輯也需要大量分頁操作,只需同樣包裹在 expanded_screen 中即可:

1
2
3
4
5
6
7
8
from PyPtt import API, _api_util

ptt = API()
ptt.login('your_id', 'your_password')

# 透過 NAWS 將視窗擴展至 100 行,減少翻頁次數
with _api_util.expanded_screen(ptt):
posts = ptt.get_post_list(board='Gossiping', index=1000)

離開 with 區塊後,expanded_screen 會自動透過 set_screen_height() 還原原始高度,並重新發送 NAWS 封包通知伺服器。

總結

NAWS(RFC 1073)是一個常被忽略但極具威力的 Telnet 擴充協定。PyPtt 的實作路徑清晰完整:

  1. 連線時在 connect_core.py 發送初始 NAWS 封包(IAC WILL NAWS + IAC SB
  2. 需要擴展時,透過 set_screen_height() 發送新的 NAWS 封包,同時同步 VT100Parser 的解析高度
  3. expanded_screen 上下文管理器自動管理擴展與還原,確保冪等性

透過將視窗高度從 24 行提升至 100 行,分頁互動次數可降低至原來的 1/4 到 1/5,在大量獲取資料的場景下效果尤為顯著。

參考

  • PyPtt 原始碼:connect_core.py_api_util.py
  • NAWS 規範: RFC 1073

也許你也會想看看