Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5
2835 字
14 分钟
【UPCPC2026】Domjudge 数据导入
2026-03-23
统计加载中...

我发现关于 domjudge 的搭建的教程挺多,但是导入数据的教程不太好找。我结合了一下已有的资料和官方文档自己折腾了一下,搞出来一套差不多能用的方案。

domserver 版本号是 8.3.2 运行环境是 ubuntu 24.04。运行脚本的电脑是 Macbook Pro M5。这篇博客主要是给自己干过的活留个档,如果版本号或者运行环境不一致可能需要微调。

同步设置#

jury interface -> Configuration Settings -> External Systems -> Data Source,改成 configuration data external,这样操作用的就是自己设的 external_id 了,可以避免很多强迫症的问题。

domserver 的前端的导入页在

interface

所有的通过图形化界面的导入方式都在这里。

用户导入#

找到 Teams & groups,在 Import JSON / YAML 下导入。

导入 Organizations#

对于校赛来说,组织可以设置成所有的学院。一共只有 id, icpc_id, name, formal_name, country 五个字段,我们不需要 icpc_id,我用了下面的脚本生成。

scripts/write_aff.py
import json
COLLEGE_MAP = {
"地球科学与技术学院": "c01",
"石油工程学院": "c02",
"化学化工学院": "c03",
"机电工程学院": "c04",
"储运与建筑工程学院": "c05",
"材料科学与工程学院": "c06",
"石大山能新能源学院": "c07",
"海洋与空间信息学院": "c08",
"控制科学与工程学院": "c09",
"青岛软件学院、计算机科学与技术学院": "c10",
"理学院": "c11",
"经济管理学院": "c12",
"外国语学院": "c13",
"文法学院": "c14",
"马克思主义学院": "c15",
"体育教学部": "c16",
}
organizations = []
for name, id in COLLEGE_MAP.items():
organizations.append({
"id": id,
"name": name,
"formal_name": name,
"country": "CHN"
})
with open("organizations.json", "w") as f:
json.dump(organizations, f, indent=2)

导入 teams 和 accounts#

teams 官方要求的格式是 json,accounts 官方要求的格式是 yaml. 我们的报名表的表头是这样的。

提交者(自动) 提交时间(自动) 学号(必填) 姓名(必填) 性别(必填) 学院(必填) 专业班级(必填) QQ(必填) 联系电话(必填)打星
scripts/write_teams.py
import json
import yaml
import pandas as pd
import secrets
import string
def generate_password(length=12, use_upper=True, use_digits=True, use_symbols=True):
# Gemini 写的
chars = string.ascii_lowercase
if use_upper:
chars += string.ascii_uppercase
if use_digits:
chars += string.digits
if use_symbols:
chars += string.punctuation
password = ''.join(secrets.choice(chars) for _ in range(length))
return password
DATA_PATH = "final.csv"
BEGIN_ID = 1001
COLLEGE_MAP = {
"地球科学与技术学院": "c01",
"石油工程学院": "c02",
"化学化工学院": "c03",
"机电工程学院": "c04",
"储运与建筑工程学院": "c05",
"材料科学与工程学院": "c06",
"石大山能新能源学院": "c07",
"海洋与空间信息学院": "c08",
"控制科学与工程学院": "c09",
"青岛软件学院、计算机科学与技术学院": "c10",
"理学院": "c11",
"经济管理学院": "c12",
"外国语学院": "c13",
"文法学院": "c14",
"马克思主义学院": "c15",
"体育教学部": "c16",
}
wl110 = pd.read_excel("文理楼110信息统计.xlsx", sheet_name="编号-ip") # 文理楼 110 的序号和 ip 的对应关系极其混乱,所以单独统计了一张键值对的表
df = pd.read_csv(DATA_PATH, dtype=str)
df["打星"] = df["打星"] == "True" # 转成一个 bool 型
user_table = []
teams = []
accounts = []
for idx, row in df.iterrows():
id = str(BEGIN_ID + idx)
if row["打星"] == True:
name = '*' + row["姓名(必填)"]
group_ids = ["observers"]
else:
name = row["姓名(必填)"]
if row["性别(必填)"] == "女":
group_ids = ["girl-participants"]
else:
group_ids = ["participants"]
if idx < 72: # 前 72 个人排到 102
pos = idx * 2 + 2
location = f"文理楼102-{pos}"
ip = f"172.20.6.{pos}"
else: # 后面 54 个人排到 110
location = f"文理楼110-{wl110.iloc[idx - 72]["编号"]}"
ip = f"172.20.10.{wl110.iloc[idx - 72]["ip"]}"
teams.append({
"id": id,
"group_ids": group_ids,
"name": row["学号(必填)"],
"display_name": name,
"organization_id": COLLEGE_MAP[row["学院(必填)"]],
"location": {"description": location}
})
# 生成了个密码备用,但是并没有派上用场,因为出问题的时候调座直接修改 ip 好像比数一遍密码快
password = generate_password(use_symbols=False)
accounts.append({
"id": id,
"username": row["学号(必填)"],
"password": password,
"type": "team",
"name": name,
"team_id": id,
"ip": ip,
})
user_table.append({
"username": row["学号(必填)"],
"passowrd": password,
"姓名": name,
"学院": row["学院(必填)"],
"班级": row["专业班级(必填)"],
"打星": row["打星"],
"位置": location
})
# domserver 导入(给 domserver 看的)
with open("output/teams.json", "w") as f:
json.dump(teams, f, indent=2, ensure_ascii=False)
with open("output/accounts.yaml", "w") as f:
yaml.dump(accounts, f, allow_unicode=True)
# 用户名单(给人看的)
pd.DataFrame(user_table).to_excel("user_table.xlsx")

竞赛导入#

导入竞赛#

Contests 下导入,但是我感觉这个不如用图形界面创建。

题目导入#

最好保证先有一个竞赛。

题目格式#

首先过一遍题目的数据格式,每道题都是一个压缩包,以正式赛的 I 题为例,I.zip 里面的内容如下

I
# 这个目录下可以再放一个 problem.pdf 表示题面
# 也可以在 I/submissions/accepted/ 下面放标程,交上去之后会自动测一遍这个标程
├── data
│   ├── sample # 样例,用户可以打包下载这里的文件
│   │   ├── 1.ans
│   │   └── 1.in
│   └── secret # 测试数据,只需要 in 和 out 的 stem 对应即可
│   ├── 1.ans
│   ├── 1.in
│   ├── 2.ans
│   ├── 2.in
│   ├── 3.ans
│   ├── 3.in
│   ├── 4.ans
│   ├── 4.in
│   ├── 5.ans
│   └── 5.in
├── domjudge-problem.ini
├── output_validators # (可选)交互库 / spj
│   └── I_cmp
│   ├── interactor.cpp
│   └── testlib.h
└── problem.yaml
6 directories, 16 files
I/domjudge-problem.ini
timelimit='2' # 单位是 s
special_run='I_cmp' # (可选)如果不需要 spj 或者交互器可以无视,发现导出的里面有个这条,但是不加好像也没事
color='#ab812c' # (可选)气球颜色
I/problem.yaml
name: Liuxx怎么这么能跑啊
validation: 'custom interactive' # (可选)验证方法,交互题 custem interactive,一般 spj 用 custom
# (可选)浮点数误差 validator_flags: 'float_tolerance 1E-6'
limits:
memory: 256 # 单位是 MB

维护者的无奈#

不过一般题不是一个人出的,所以每个人都不好好看文档就会造成很大的混乱,比如

  • domjudge-problem.ini 或者 problem.yaml 缺失或者格式有问题
  • 测试数据里面的换行用 \r\n 而不是 \n,说几次都不听
  • 不传样例,只传测试数据

最后还得是搭建 domjudge 的人在擦屁股,我是真无语……如果你没有遇到这种恶习的情况可以直接看 打包 了。

于是我想到一个折中的办法,只要他们的测试数据,其他的都从题面里解析,所有测试数据都过滤一遍 \r\n,之后如果需要 spj,interactive 或者浮点数精度等调整自己手动在加一下。我们的题面统一用了 olymp.sty,参考了前前任部长的教程 ACM/ICPC/CCPC等算法竞赛规范题面撰写详细教程(全网最详细,看完包懂)

解析 latex 文件需要 texsoup 库

pip install texsoup

这里模式也很固定,的很多代码也都是先 ai 在微调的。

scripts/tex_info.py
"""
解析 tex 用
"""
from TexSoup import TexSoup
from TexSoup.data import TexNode, TexArgs
def tex_info(content: str):
# 解析LaTeX内容
soup = TexSoup(content)
# 初始化返回值
title = ""
timelimit = ""
memlimit = ""
samples = []
# 1. 提取problem环境的参数(时间限制、内存限制)
problem_env = soup.find("problem")
if problem_env and hasattr(problem_env, 'args') and len(problem_env.args) >= 5:
# problem环境参数:{Title}{standard input}{standard output}{2 seconds}{256 megabytes}
title = str(problem_env.args[0]).strip("{}")
time_arg = str(problem_env.args[3]).strip("{}") # 第4个参数是时间限制
mem_arg = str(problem_env.args[4]).strip("{}") # 第5个参数是内存限制
timelimit = int(_get_arg_text(time_arg).split()[0])
memlimit = int(_get_arg_text(mem_arg).split()[0])
# 2. 提取example环境中的样例输入输出
example_envs = soup.find_all("example")
for example in example_envs:
# 找到exmp命令(样例输入输出的容器)
exmp_cmd = example.find("exmp")
if exmp_cmd and hasattr(exmp_cmd, 'args') and len(exmp_cmd.args) >= 2:
# exmp的第一个参数是输入,第二个是输出
in_arg = exmp_cmd.args[0]
ans_arg = exmp_cmd.args[1]
# 提取输入输出文本并清理格式
in_text = _clean_sample_text(_get_arg_text(in_arg))
ans_text = _clean_sample_text(_get_arg_text(ans_arg))
if in_text or ans_text:
samples.append({
"in": in_text,
"ans": ans_text
})
return title, timelimit, memlimit, samples
def _get_arg_text(arg) -> str:
"""辅助函数:提取TexArgs/TexNode的文本内容"""
if isinstance(arg, (TexArgs, TexNode)):
# 直接取text属性,若没有则转为字符串
return getattr(arg, 'text', str(arg))
return str(arg)
def _clean_sample_text(text: str) -> str:
"""辅助函数:清理样例文本中的多余符号和空白"""
return text.strip("{}%\n").replace("\r\n", "\n")
scripts/convert.py
from pathlib import Path
from shutil import rmtree
from tex_info import tex_info
basedir = Path("/Users/wangyafei/Documents/学校事务/社团管理/2025社团管理/2026校赛/contests/正式赛/problems")
tex_path = basedir / "LaTeX" # tex 的目录,每道题是一个单独的 tex,文件名如 A.tex
std_path = basedir / "std" # 标程的目录,文件名如 std-a.cpp
input_path = basedir / '数据' # 测试数据
output_path = basedir / 'norm_output'
output_path.mkdir(exist_ok=True) # 创建目录
for dir in input_path.iterdir():
if dir.is_dir():
label = dir.stem.upper()
print(f"Processing Problem {label}")
out_dir = output_path / label
rmtree(out_dir, ignore_errors=True)
out_dir.mkdir(exist_ok=True)
content = (tex_path / f"{label}.tex").read_text()
title, timelimit, memlimit, samples = tex_info(content)
# domjudge-problem.ini
print("Writing domjudge-problem.ini")
(out_dir / 'domjudge-problem.ini').write_text(f"timelimit = {timelimit}")
# problem.yaml
print("Writing problem.yaml")
(out_dir / 'problem.yaml').write_text(f"name: \"{title}\"\nlimits:\n memory: {memlimit}")
# submissions/accepted
try:
print("Writing submissions/correct/solve.cpp")
std_dir = out_dir / 'submissions' / 'accepted'
std_dir.mkdir(parents=True, exist_ok=True)
(std_dir / 'solve.cpp').write_text((std_path / f"std-{label.lower()}.cpp").read_text())
except Exception as e:
print(f'error: {e}')
# data/sample
print("Writing samples")
sample_dir = out_dir / 'data' / 'sample'
sample_dir.mkdir(parents=True)
for i, data in enumerate(samples):
(sample_dir / f"{i}.in").write_text(data.get("in"))
(sample_dir / f"{i}.ans").write_text(data.get("ans"))
# data/secret
print("Copying testcases")
data_dir = out_dir / 'data' / 'secret'
data_dir.mkdir(parents=True)
for file in (dir / 'data' / 'secret').iterdir():
filename = file.name if file.suffix == ".in" else file.stem + ".ans" # 没错这都有人不看,用了 .out,所以不是 in 都认为是输出了
out_file_path = data_dir / filename
out_file_path.write_text(file.read_text().replace('\r\n', '\n'))

打包#

注意:压缩包根路径应该是 data, domjudge-problem.ini, problem.yaml …,而不是一个目录 I!

这个任务比较好描述,所以我丢给 ai 直接就得到了一个能用的脚本,把 output 目录下的所有题目的目录打包输出到 archives。

scripts/compress.py
import os
import zipfile
from pathlib import Path
def zip_folders_in_output(output_dir: str = "output", archives_dir: str = "archives"):
"""
遍历output路径下的所有文件夹,将文件夹内容打包为zip包(散列内容),输出到archives路径
Args:
output_dir (str): 源文件夹路径,默认"output"
archives_dir (str): 压缩包输出路径,默认"archives"
"""
# 转换为Path对象,方便路径操作
output_path = Path(output_dir)
archives_path = Path(archives_dir)
# 检查output目录是否存在
if not output_path.exists():
print(f"错误:{output_dir} 目录不存在!")
return
# 创建archives目录(若不存在)
archives_path.mkdir(exist_ok=True)
# 遍历output下的所有一级子目录
for item in output_path.iterdir():
if item.is_dir():
folder_name = item.name # 文件夹名称(作为zip包名)
zip_file_path = archives_path / f"{folder_name}.zip" # zip包路径
# 创建zip文件(压缩模式为DEFLATED,即有压缩效果)
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 遍历文件夹内的所有文件和子目录
for root, dirs, files in os.walk(item):
# 计算相对路径(去掉外层文件夹,实现散列打包)
relative_root = os.path.relpath(root, item)
# 先添加子目录(确保空目录也被打包)
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
relative_dir_path = os.path.join(relative_root, dir_name)
# 写入目录到zip包
zipf.write(dir_path, relative_dir_path)
# 再添加文件
for file_name in files:
file_path = os.path.join(root, file_name)
relative_file_path = os.path.join(relative_root, file_name)
# 写入文件到zip包(使用相对路径,避免外层文件夹)
zipf.write(file_path, relative_file_path)
print(f"成功打包:{zip_file_path}")
if __name__ == "__main__":
# 调用函数,可自定义output和archives路径
zip_folders_in_output(output_dir="output", archives_dir="archives")

导入题目#

Problems 下,但是一般题目很多,一个一个手动导入很麻烦,另外我用图形界面导入有时候还一直报错,所以我选择用 api 导入。

scripts/send.py
import requests
API_URL = "<替换成domjudge的ip或域名>/api/v4"
USERNAME = "admin"
PASSWORD = "<替换成管理员密码>"
CONTEST_ID = "upcpc2026"
def upload_problem(zip_path):
endpoint = f"{API_URL}/contests/{CONTEST_ID}/problems"
with open(zip_path, 'rb') as f:
files = {'zip': f}
response = requests.post(
endpoint,
auth=(USERNAME, PASSWORD),
files=files
)
if response.status_code == 200:
print("Successfully uploaded!")
print("Response:", response.json())
else:
print(f"Failed! Status code: {response.status_code}")
print("Error:", response.text)
for i in range(0, 13):
upload_problem(f'./{chr(i + ord('A'))}.zip') # 如果这样写就必须要 cd 到压缩包那个目录下跑,也可以写死目录或者写好相对路径
【UPCPC2026】Domjudge 数据导入
https://starlab.top/posts/upcpc2026-import/
作者
Star
发布于
2026-03-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时