Files
2026-07-13 12:40:28 +08:00

480 lines
13 KiB
Python

import argparse
import os
import pprint
from typing import Dict, Tuple, List
import re
import sys
import json
def extract_dataset_desc_links(desc:List[str]) -> List:
"""
Extract all the links from the description of datasets
:param desc: Lines of the description of the dataset
:return:
"""
out = []
md = "".join(desc)
md_links = re.findall("\\[.*\\]\\(.*\\)", md)
for md_link in md_links:
title, link = extract_title_and_link(md_link)
out.append({
"title": title,
"url": link,
})
return out
def sanitize_subdataset_name(name:str):
"""
Do some sanitization on automatically extracted subdataset name
:param name: raw subdataset name line
:return:
"""
name = name.replace("**", "")
if name.endswith(":"):
name = name[:-1]
return name.strip()
def extract_lines_before_tables(lines:List[str]):
"""
Extract the non-empty line before the table
:param lines: a list of lines
:return:
"""
out = []
before = None
in_table = False
for l in lines:
if l.startswith("|") and not in_table:
if before is not None:
out.append(before)
in_table = True
elif in_table and not l.startswith("|"):
in_table = False
before = None
if l.strip() != "":
before = l.strip()
elif l.strip() != "":
before = l.strip()
return out
def handle_multiple_sota_table_exceptions(section:List[str], sota_tables:List[List[str]]):
"""
Manually handle the edge cases with dataset partitions
These are not captured in a consistent format, so no unified approach is possible atm.
:param section: The lines in this section
:param sota_tables: The list of sota table lines
:return:
"""
section_full = "".join(section)
out = []
# Use the line before the table
subdatasets = extract_lines_before_tables(section)
subdatasets = [sanitize_subdataset_name(s) for s in subdatasets]
# exceptions:
if "hypernym discovery evaluation benchmark" in section_full:
subdatasets = subdatasets[1:]
if len(subdatasets) != len(sota_tables):
print("ERROR parsing the subdataset SOTA tables", file=sys.stderr)
print(sota_tables, file=sys.stderr)
else:
for i in range(len(subdatasets)):
out.append({
"subdataset": subdatasets[i],
"sota": extract_sota_table(sota_tables[i])
})
return out
def extract_title_and_link(md_link:str) -> Tuple:
"""
Extract the anchor text and URL from a markdown link
:param md_link: a string of ONLY the markdown link, e.g. "[google](http://google.com)"
:return: e.g. the tuple (google, http://google.com)
"""
title = re.findall("^\\[(.*)\\]", md_link)[0].strip()
link = re.findall("\\((.*)\\)$", md_link)[0].strip()
return title, link
def extract_model_name_and_author(md_name:str) -> Tuple:
"""
Extract the model name and author, if provided
:param md_name: a string with the model name from the sota table
:return: tuple (model_name, author_names)
"""
if ' (' in md_name and ')' in md_name:
model_name = md_name.split(' (')[0]
model_authors = md_name.split(' (')[1].split(')')[0]
elif '(' in md_name and ')' in md_name: # only has author name
model_name = None
model_authors = md_name
else:
model_name = md_name
model_authors = None
return model_name, model_authors
def extract_paper_title_and_link(paper_md:str) -> Tuple:
"""
Extract the title and link to the paper
:param paper_md: markdown for the paper link
:return: tuple (paper_title, paper_link)
"""
md_links = re.findall("\\[.*\\]\\(.*\\)", paper_md)
if len(md_links) > 1:
print("WARNING: Found multiple paper references: `%s`, using only the first..." % paper_md)
if len(md_links) == 0:
return None, None
md_link = md_links[0]
paper_title, paper_link = extract_title_and_link(md_link)
return paper_title, paper_link
def extract_code_links(code_md:str) -> List[Dict]:
"""
Extract the links to all code implementations
:param code_md:
:return:
"""
md_links = re.findall("\\[.*\\]\\(.*\\)", code_md)
links = []
for md_link in md_links:
t, l = extract_title_and_link(md_link)
links.append({
"title": t,
"url": l,
})
return links
def extract_sota_table(table_lines:List[str]) -> Dict:
"""
Parse a SOTA table out of lines in markdown
:param table_lines: lines in the SOTA table
:return:
"""
sota = {}
header = table_lines[0]
header_cols = [h.strip() for h in header.split("|") if h.strip()]
cols_sanitized = [h.lower() for h in header_cols]
cols_sanitized = [re.sub(" +", "", h).replace("**","") for h in cols_sanitized]
# find the model name column (usually the first one)
if "model" in cols_sanitized:
model_inx = cols_sanitized.index("model")
else:
print("ERROR: Model name not found in this SOTA table, skipping...\n", file=sys.stderr)
print("".join(table_lines), file=sys.stderr)
return {}
if "paper/source" in cols_sanitized:
paper_inx = cols_sanitized.index("paper/source")
elif "paper" in cols_sanitized:
paper_inx = cols_sanitized.index("paper")
else:
print("ERROR: Paper reference not found in this SOTA table, skipping...\n", file=sys.stderr)
print("".join(table_lines), file=sys.stderr)
return {}
if "code" in cols_sanitized:
code_inx = cols_sanitized.index("code")
else:
code_inx = None
metrics_inx = set(range(len(header_cols))) - set([model_inx, paper_inx, code_inx])
metrics_inx = sorted(list(metrics_inx))
metrics_names = [header_cols[i] for i in metrics_inx]
sota["metrics"] = metrics_names
sota["rows"] = []
min_cols = len(header_cols)
# now parse the table rows
rows = table_lines[2:]
for row in rows:
row_cols = [h.strip() for h in row.split("|")][1:]
if len(row_cols) < min_cols:
print("This row doesn't have enough columns, skipping: %s" % row, file=sys.stderr)
continue
# extract all the metrics
metrics = {}
for i in range(len(metrics_inx)):
metrics[metrics_names[i]] = row_cols[metrics_inx[i]]
# extract paper references
paper_title, paper_link = extract_paper_title_and_link(row_cols[paper_inx])
# extract model_name and author
model_name, model_author = extract_model_name_and_author(row_cols[model_inx])
sota_row = {
"model_name": model_name,
"metrics": metrics,
}
if paper_title is not None and paper_link is not None:
sota_row["paper_title"] = paper_title
sota_row["paper_url"] = paper_link
# and code links if they exist
if code_inx is not None:
sota_row["code_links"] = extract_code_links(row_cols[code_inx])
sota["rows"].append(sota_row)
return sota
def get_line_no(sections:List[str], section_index:int, section_line=0) -> int:
"""
Get the line number for a section heading
:param sections: A list of list of sections
:param section_index: Index of the current section
:param section_line: Index of the line within the section
:return:
"""
if section_index == 0:
return 1+section_line
lens = [len(s) for s in sections[:section_index]]
return sum(lens)+1+section_index
def extract_dataset_desc_and_sota_table(md_lines:List[str]) -> Tuple:
"""
Extract the lines that are the description and lines that are the sota table(s)
:param md_lines: a list of lines in this section
:return:
"""
# Main assumption is that the Sota table will minimally have a "Model" column
desc = []
tables = []
t = None
in_table = False
for l in md_lines:
if l.startswith("|") and "model" in l.lower() and not in_table:
t = [l]
in_table = True
elif in_table and l.startswith("|"):
t.append(l)
elif in_table and not l.startswith("|"):
if t is not None:
tables.append(t)
t = None
desc.append(l)
in_table = False
else:
desc.append(l)
if t is not None:
tables.append(t)
return desc, tables
def parse_markdown_file(md_file:str) -> List:
"""
Parse a single markdown file
:param md_file: path to the markdown file
:return:
"""
with open(md_file, "r") as f:
md_lines = f.readlines()
# Assumptions:
# 1) H1 are tasks
# 2) Everything until the next heading is the task description
# 3) H2 are subtasks, H3 are datasets, H4 are subdatasets
# Algorithm:
# 1) Split the document by headings
sections = []
cur = []
for line in md_lines:
if line.startswith("#"):
if cur:
sections.append(cur)
cur = [line]
else:
cur = [line]
else:
cur.append(line)
if cur:
sections.append(cur)
# 2) Parse each heading section one-by-one
parsed_out = [] # whole parsed output
t = {} # current task element being parsed
st = None # current subtask being parsed
ds = None # current dataset being parsed
for section_index in range(len(sections)):
section = sections[section_index]
header = section[0]
# Task definition
if header.startswith("#") and not header.startswith("##"):
if "task" in t:
parsed_out.append(t)
t = {}
t["task"] = header[1:].strip()
t["description"] = "".join(section[1:]).strip()
# reset subtasks and datasets
st = None
ds = None
## Subtask definition
if header.startswith("##") and not header.startswith("###"):
if "task" not in t:
print("ERROR: Unexpected subtask without a parent task at %s:#%d" %
(md_file, get_line_no(sections, section_index)), file=sys.stderr)
if "subtasks" not in t:
t["subtasks"] = []
# new substask
st = {}
t["subtasks"].append(st)
st["task"] = header[2:].strip()
st["description"] = "".join(section[1:]).strip()
st["source_link"] = {
"title": "NLP-progress",
"url": "https://github.com/sebastianruder/NLP-progress"
}
# reset the last dataset
ds = None
### Dataset definition
if header.startswith("###") and not header.startswith("####") and "Table of content" not in header:
if "task" not in t:
print("ERROR: Unexpected dataset without a parent task at %s:#%d" %
(md_file, get_line_no(sections, section_index)), file=sys.stderr)
if st is not None:
# we are in a subtask, add everything here
if "datasets" not in st:
st["datasets"] = []
# new dataset and add
ds = {}
st["datasets"].append(ds)
else:
# we are in a task, add here
if "datasets" not in t:
t["datasets"] = []
ds = {}
t["datasets"].append(ds)
ds["dataset"] = header[3:].strip()
# dataset description is everything that's not a table
desc, tables = extract_dataset_desc_and_sota_table(section[1:])
ds["description"] = "".join(desc).strip()
# see if there is an arxiv link in the first paragraph of the description
dataset_links = extract_dataset_desc_links(desc)
if dataset_links:
ds["dataset_links"] = dataset_links
if tables:
if len(tables) > 1:
ds["subdatasets"] = handle_multiple_sota_table_exceptions(section, tables)
else:
ds["sota"] = extract_sota_table(tables[0])
if t:
t["source_link"] = {
"title": "NLP-progress",
"url": "https://github.com/sebastianruder/NLP-progress"
}
parsed_out.append(t)
return parsed_out
def parse_markdown_directory(path:str):
"""
Parse all markdown files in a directory
:param path: Path to the directory
:return:
"""
all_files = os.listdir(path)
md_files = [f for f in all_files if f.endswith(".md")]
out = []
for md_file in md_files:
print("Processing `%s`..." % md_file)
out.extend(parse_markdown_file(os.path.join(path, md_file)))
return out
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="+", type=str, help="Files or directories to convert")
parser.add_argument("--output", default="structured.json", type=str, help="Output JSON file name")
args = parser.parse_args()
out = []
for path in args.paths:
if os.path.isdir(path):
out.extend(parse_markdown_directory(path))
else:
out.extend(parse_markdown_file(path))
with open(args.output, "w") as f:
f.write(json.dumps(out, indent=2))