不用一個一個上傳、不用手動翻到眼花。用「先量化、再抽樣人工校正」的流程,把速度和公平性都顧到。
目錄
1. 引起動機:為什麼不要一個一個傳 sb3 2. 現況說明:常見卡關與三種做法比較 3. 功能介紹:批次掃描 sb3 其實在做什麼 4. 應用實例:用 CSV 先排出高/中/低,抽樣校正更準 5. 操作教學:從資料夾到 report.csv 的完整步驟 6. Q&A:缺點、風險、以及怎麼補救1. 引起動機:為什麼不要一個一個傳 sb3
這篇文章你會學到什麼
如果你手上有上百個 Scratch 作業(.sb3),最痛的通常不是「打分」本身,而是:
- 開檔、等載入、切角色、找積木、聽配音,時間像被吸走。
- 學生作品類型差很大:有人做劇情動畫、有人做互動遊戲,拿同一標準容易吵架。
- 只靠印象給分會不安心:你會希望有「客觀依據」撐住分數。
所以本文的核心做法是:
核心流程(老師實戰版)
- 本機批次掃描 sb3 → 產出 report.csv(每個作品一列)
- 用 CSV 先把全班排序、分類(劇情/互動/混合)
- 抽樣 10~20 個作品人工校正 → 把分數調到更接近你「真正想給的」
2. 現況說明:常見卡關與三種做法比較
先講現實:你不可能把每一份都「完整看完再評」。所以我們通常會在以下三種做法裡選一個平衡點:
| 做法 | 你要做的事 | 優點 | 缺點 | 適合誰 |
|---|---|---|---|---|
| A. 批次掃描→CSV(最推薦) | 在自己電腦跑腳本,產 report.csv,再抽樣少量 sb3 讓我/你人工校正 | 最快、最省上傳、最能做全班統計 | CSV 只看得到客觀指標,看不到「好不好看」 | 上百份、想要又快又有依據 |
| B. 分批壓縮上傳 | 把 sb3 拆成多個 zip 分次上傳 | 你不用寫程式 | 上傳仍然麻煩、檔案管理費工、我端仍要逐個讀 | 份數中等、或你沒辦法跑 Python |
| C. 抽樣 20 份先定尺 | 只傳高/中/低各幾份,先做評分規準 | 最省事、最快建立「班級尺度」 | 不能直接得到每一份的完整分數 | 你只需要評分標準,不一定要我逐份打 |
老師小提醒
如果你想「全班每份都有分數」又不想上傳到爆,通常就是選 A:用 CSV 先把全體拉起來,再針對代表作校正。
3. 功能介紹:批次掃描 sb3 其實在做什麼
Scratch 的 sb3 本質上是一個壓縮檔,裡面有 project.json。我們做的「批次掃描」,不是在幫你評美術,而是把可量化的結構拉出來,例如:
- 角色數、造型數、音效數(作品完成度的客觀線索)
- 積木總數、等待積木比例(劇情型常見 wait 很多)
- 是否使用:廣播訊息、變數、清單、迴圈、判斷、自訂積木、分身
- 互動線索:點擊角色、按鍵、碰撞偵測等
重點:這不是「最終分數」,而是「初評指標」
有用廣播不代表用得好;wait 多也不一定壞(劇情動畫本來就可能要配音對齊)。所以我們會用「抽樣校正」把誤差修回來。
4. 應用實例:用 CSV 先排出高/中/低,抽樣校正更準
你可以把 report.csv 想成「全班作品體檢表」。最常見的用法是:
- 先分類:劇情動畫、互動遊戲、混合型
- 先排序:找出可能最高分、最低分、以及中間段的代表作
- 抽樣校正:各類型抽 3~5 件,人工看一遍,把權重調到你認同的尺度
- 批次套用:剩下 80~90% 的作品,用校正後的規則快速落點
一個很穩的抽樣法(共 10 件)
- 自動分最高 3 件(看是不是「真的強」)
- 自動分最低 3 件(看是不是「真的需要加強」)
- 中位數附近 4 件(看「主流平均」落在哪)
抽樣看完,你就能把「技術分」和「表現分」調成你心中合理的比例。
5. 操作教學:從資料夾到 report.csv 的完整步驟
你需要準備
- 一個資料夾:裡面放所有
.sb3 - Python 3(Mac 通常有;Windows 若沒有再裝)
步驟 1:把所有 sb3 放到同一個資料夾
例如:
/Users/你的名字/Desktop/scratch_hw/
步驟 2:建立批次掃描腳本(免套件)
把下面這段存成 sb3_batch_report.py:
# sb3_batch_report.py
# 目的:批次讀取 .sb3,抽出可量化指標,產出 report.csv 與 report.sample.txt
# 用法:
# python3 sb3_batch_report.py "/path/to/sb3_folder" report.csv
import csv, json, zipfile, sys
from pathlib import Path
from collections import Counter
FEATURE_OPS = {
"broadcast": {"event_broadcast", "event_broadcastandwait", "event_whenbroadcastreceived"},
"vars": {"data_setvariableto", "data_changevariableby", "data_showvariable", "data_hidevariable"},
"lists": {"data_addtolist", "data_deleteoflist", "data_deletealloflist", "data_insertatlist",
"data_replaceitemoflist", "data_itemoflist", "data_lengthoflist", "data_listcontainsitem"},
"loops": {"control_repeat", "control_forever", "control_repeat_until"},
"ifs": {"control_if", "control_if_else", "control_wait_until"},
"custom": {"procedures_definition", "procedures_call"},
"clone": {"control_create_clone_of", "control_start_as_clone", "control_delete_this_clone"},
}
INTERACTIVE_HINTS = {
"event_whenthisspriteclicked", "event_whenkeypressed",
"sensing_mousedown", "sensing_keypressed", "sensing_touchingobject",
"sensing_touchingcolor", "sensing_coloristouchingcolor"
}
def analyze_sb3(sb3_path: Path):
with zipfile.ZipFile(sb3_path, "r") as z:
try:
project = json.load(z.open("project.json"))
except KeyError:
return None
targets = project.get("targets", [])
extensions = project.get("extensions", [])
sprites = [t for t in targets if not t.get("isStage")]
sprite_count = len(sprites)
costumes = sum(len(t.get("costumes", [])) for t in targets)
sounds = sum(len(t.get("sounds", [])) for t in targets)
var_defs = sum(len(t.get("variables", {})) for t in targets)
list_defs = sum(len(t.get("lists", {})) for t in targets)
broadcast_defs = sum(len(t.get("broadcasts", {})) for t in targets)
opcodes = Counter()
for t in targets:
blocks = t.get("blocks", {}) or {}
for _, b in blocks.items():
if isinstance(b, dict) and b.get("opcode"):
opcodes[b["opcode"]] += 1
total_blocks = sum(opcodes.values())
wait_blocks = opcodes.get("control_wait", 0) + opcodes.get("control_wait_until", 0)
wait_ratio = (wait_blocks / total_blocks) if total_blocks else 0.0
def has_any(ops):
return sum(opcodes.get(o, 0) for o in ops) > 0
feats = {k: has_any(v) for k, v in FEATURE_OPS.items()}
interactive = any(opcodes.get(o, 0) > 0 for o in INTERACTIVE_HINTS)
# 用來排序用的初步分數(不是最終分數)
tech = 0.0
tech += min(10.0, (total_blocks / 200.0) * 10.0)
tech += 8.0 if feats["broadcast"] else 0.0
tech += 8.0 if feats["vars"] else 0.0
tech += 6.0 if feats["lists"] else 0.0
tech += 6.0 if feats["loops"] else 0.0
tech += 6.0 if feats["ifs"] else 0.0
tech += 6.0 if feats["custom"] else 0.0
tech += 4.0 if feats["clone"] else 0.0
tech += 2.0 if len(extensions) > 0 else 0.0
tech -= min(10.0, wait_ratio * 20.0) # wait 太多先扣一些,避免全靠 wait 撐分
assets = 0.0
assets += min(10.0, (costumes / 30.0) * 10.0)
assets += min(6.0, (sounds / 20.0) * 6.0)
assets += min(4.0, (sprite_count / 10.0) * 4.0)
auto_score = max(0, min(100, round(tech * 0.65 + assets * 0.35)))
return {
"file": sb3_path.name,
"size_mb": round(sb3_path.stat().st_size / (1024 * 1024), 2),
"sprites": sprite_count,
"costumes": costumes,
"sounds": sounds,
"blocks": total_blocks,
"wait_ratio": round(wait_ratio, 3),
"var_defs": var_defs,
"list_defs": list_defs,
"broadcast_defs": broadcast_defs,
"extensions": ",".join(extensions) if extensions else "",
"uses_broadcast": int(feats["broadcast"]),
"uses_vars": int(feats["vars"]),
"uses_lists": int(feats["lists"]),
"uses_loops": int(feats["loops"]),
"uses_if": int(feats["ifs"]),
"uses_custom": int(feats["custom"]),
"uses_clone": int(feats["clone"]),
"interactive_hint": int(interactive),
"auto_score_for_sorting": auto_score,
}
def main():
if len(sys.argv) < 3:
print("Usage: python3 sb3_batch_report.py /path/to/folder report.csv")
sys.exit(1)
folder = Path(sys.argv[1]).expanduser()
out_csv = Path(sys.argv[2]).expanduser()
sb3_files = sorted(folder.glob("*.sb3"))
rows = []
for f in sb3_files:
r = analyze_sb3(f)
if r:
rows.append(r)
if not rows:
print("No valid .sb3 found.")
sys.exit(1)
fieldnames = list(rows[0].keys())
with out_csv.open("w", newline="", encoding="utf-8") as fp:
w = csv.DictWriter(fp, fieldnames=fieldnames)
w.writeheader()
w.writerows(rows)
# 抽樣清單:最高3 + 最低3 + 中位附近4(共10個)
rows_sorted = sorted(rows, key=lambda x: x["auto_score_for_sorting"])
picks = []
picks += rows_sorted[:3]
mid = len(rows_sorted)//2
picks += rows_sorted[max(0, mid-2):min(len(rows_sorted), mid+2)]
picks += rows_sorted[-3:]
pick_names = [p["file"] for p in picks]
sample_txt = out_csv.with_suffix(".sample.txt")
sample_txt.write_text("\n".join(pick_names), encoding="utf-8")
print(f"Done. Wrote: {out_csv}")
print(f"Sample list: {sample_txt}")
if __name__ == "__main__":
main()
步驟 3:執行腳本,產出 report.csv
打開終端機(Mac)或 PowerShell(Windows),執行:
python3 sb3_batch_report.py "/你的/sb3資料夾" report.csv
成功後,你會得到兩個檔:
report.csv:全班作品指標報表report.sample.txt:建議你抽樣檢查的 10 份作品檔名
步驟 4:把「CSV + 抽樣 sb3」交給我(或你自己)做校正評分
- 上傳
report.csv - 照
report.sample.txt的清單,上傳那 10 個 sb3
我就能用同一套規則把全班都打出 100 分制,並附「每份作品最關鍵的 3 條改進建議」。
我想先看缺點與補救隱私提醒
如果作品包含學生姓名、聲音或個資,建議先用檔名匿名(例如 501_01.sb3)再整理與分享,並遵守學校的資料規範。
6. Q&A:缺點、風險、以及怎麼補救
Q1:用 CSV 批次掃描的缺點是什麼?會不會不準?會有誤差,因為 CSV 看的多是「客觀指標」,看不到劇情張力、節奏、配音自然度、美術美感等。但它很適合做初評與排序,再用 10~20 份抽樣人工校正,把分數拉回你真正認同的尺度。
Q2:學生如果「堆積木、堆素材」會不會刷分?有可能,所以我建議一定要做抽樣校正:看「最高分的 3 件」是不是實至名歸。若出現刷分型作品,就把權重調整成更重視互動、結構或可讀性(例如廣播、變數、判斷),而不是純數量。
Q3:劇情動畫 wait 很多,會不會被扣太多分?如果你班上多是劇情型作品,確實要調整規則。wait 多不一定壞,關鍵是「有沒有用廣播訊息讓流程更穩」。做法:把 wait 扣分調輕,並提高 broadcast 的加分,這樣劇情類會更公平。
Q4:我不能跑 Python,還有替代方案嗎?可以。你可以用「分批 zip 上傳」或「抽樣 20 份先定評分規準」。如果只是要建立全班尺度,抽樣 20 份會最快;如果一定要全班逐份評,就用分批 zip(每包 20~30 件)比較好管。
Q5:分批 zip 會遇到檔案太大,怎麼分割?建議用 7-Zip 分卷(例如每卷 90MB),或直接把 sb3 分批放入不同資料夾再各自壓縮。分卷檔案命名會像 batch.zip.001、batch.zip.002,依序上傳即可。
老師版結論
如果你要兼顧「快、穩、公平」:用 CSV 做全量初評 + 抽樣人工校正是最漂亮的解。
本文提供的流程與程式碼屬於教學用途,實際評分仍應以任課老師的評量規準、課堂教學目標與學校相關規定為準。若作品含個資(姓名、聲音、照片等),請先完成匿名化與必要的授權/告知後再分享或上傳。
