LitReviewAI / src /metadata_extractor.py
bano1's picture
Update src/metadata_extractor.py
f63e910 verified
Raw
History Blame Contribute Delete
7.48 kB
import re
from src.pdf_parser import extract_text_from_pdf
from src.grobid import extract_metadata_grobid
def extract_title(text):
"""Extracts paper title (before Abstract), avoiding citations and irrelevant headers."""
lines = [l.strip() for l in text.split("\n") if l.strip()]
# Find index of "Abstract" (case-insensitive) or common translations
abs_index = None
for i, line in enumerate(lines):
low = line.lower()
if low.startswith("abstract") or any(k in low for k in ["résumé", "resumen", "summary", "overview", "abstract—"]):
abs_index = i
break
# If Abstract found, check lines before abstract; else check top area
if abs_index is not None:
candidate_lines = lines[:abs_index]
else:
candidate_lines = lines[:100]
skip_words = ["journal", "doi", "copyright", "arxiv", "volume",
"methods", "open access", "citation", "editor", "published"]
candidates = []
for line in candidate_lines:
low = line.lower()
# Skip unwanted lines
if any(w in low for w in skip_words):
continue
# Skip author-like lists (many commas) or emails
if "@" in line or re.search(r"\bjournal\b", low):
continue
# Skip short/very long lines
if 5 <= len(line.split()) <= 30:
# avoid lines that look like "Editor: Name" etc
if re.match(r"^(editor|edited by|edited).+", low):
continue
candidates.append(line)
# Prefer line with colon (typical title: subtitle)
for line in candidates:
if ":" in line:
return line
# fallback: longest candidate
if candidates:
return max(candidates, key=len)
return "Untitled"
def extract_authors(text, max_scan_lines=80):
"""
Flexible author extraction:
- Finds a likely title, then scans the following lines looking for names.
- Handles comma-separated authors on one line or names on separate lines.
"""
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
authors = []
# 1) find a likely title index (first reasonably long line)
title_idx = None
for i, line in enumerate(lines[:40]):
if len(line.split()) >= 4 and not any(k in line.lower() for k in ["journal", "doi", "copyright", "arxiv"]):
title_idx = i
break
start = title_idx + 1 if title_idx is not None else 0
# 2) scan lines after title until we hit abstract/keywords or a limit
abstract_markers = ["abstract", "résumé", "resumen", "summary", "overview", "introduction", "keywords"]
for line in lines[start:start + max_scan_lines]:
low = line.lower()
if any(m in low for m in abstract_markers):
break
# If line contains commas or " and ", likely multiple authors
if "," in line or " and " in low or ";" in line:
parts = re.split(r",|;|\sand\s", line)
for part in parts:
part = part.strip()
if not part:
continue
# match typical name patterns: "First Last", "First M. Last", "Last, First"
# handle "Last, First" by swapping
if re.match(r"^[A-Z][a-z]+,\s*[A-Z][a-z]+", part):
# swap "Last, First" -> "First Last"
t = re.split(r",\s*", part)
name = t[1] + " " + t[0]
else:
name = part
# require at least two capitalized tokens to be considered a name
cap_tokens = [t for t in name.split() if re.match(r"^[A-Z][a-z]+\.?$", t) or re.match(r"^[A-Z]\.$", t)]
if len(cap_tokens) >= 2 and not looks_like_affiliation(name):
authors.append(name)
if authors:
continue
else:
# attempt single-line name extraction
matches = re.findall(r'\b[A-Z][a-z]+(?:\s[A-Z]\.?\s?[A-Z][a-z]+){0,2}\b', line)
for m in matches:
if len(m.split()) >= 2 and not looks_like_affiliation(m):
authors.append(m)
# 3) fallback: check metadata-like "By X Y" lines
if not authors:
for line in lines[:40]:
m = re.match(r"^(By|BY|by)\s+(.+)$", line)
if m:
parts = re.split(r",|;|\sand\s", m.group(2))
for p in parts:
p = p.strip()
if p and not looks_like_affiliation(p):
authors.append(p)
if authors:
break
return authors if authors else ["Unknown"]
AFFIL_KEYWORDS = [
"university", "institute", "department", "school", "hospital",
"clinic", "center", "centre", "laboratory", "lab", "college",
"medicine", "research", "faculty", "division", "program", "department of"
]
def looks_like_affiliation(line):
low = line.lower()
if any(k in low for k in AFFIL_KEYWORDS):
return True
if "@" in line or "http" in low or "www." in low:
return True
# if line has few capitalized tokens relative to total tokens, it's likely not a name
toks = [t for t in line.split() if t.strip()]
if len(toks) == 0:
return True
cap = sum(1 for t in toks if re.match(r"^[A-Z][a-z]+$", t))
if cap / len(toks) < 0.4:
return True
return False
def extract_abstract(text):
"""Extract abstract block (multiple formats supported). Gathers until next main heading."""
lines = text.split("\n")
abstract_lines = []
capture = False
abstract_markers = ["abstract", "résumé", "resumen", "summary", "overview"]
stop_markers = ["introduction", "keywords", "1.", "methods", "materials", "results", "conclusion", "references"]
for line in lines:
l = line.lower().strip()
if any(m in l for m in abstract_markers):
capture = True
continue
if capture:
# stop when a common section heading appears
if any(re.match(rf"^{sm}\b", l) for sm in stop_markers):
break
abstract_lines.append(line.strip())
abstract = " ".join(abstract_lines).strip()
return abstract
def extract_metadata(pdf_file):
"""
Extract metadata using GROBID first.
If GROBID fails, use the regex-based fallback.
"""
# Read full paper text once
text = extract_text_from_pdf(pdf_file)
# Reset pointer because extract_text_from_pdf() consumed it
pdf_file.seek(0)
# -------------------------------
# Try GROBID
# -------------------------------
try:
grobid_meta = extract_metadata_grobid(pdf_file)
if grobid_meta:
title = grobid_meta.get("title", "").strip()
authors = grobid_meta.get("authors", [])
abstract = grobid_meta.get("abstract", "").strip()
return {
"title": title if title else extract_title(text),
"authors": authors if authors else extract_authors(text),
"abstract": abstract if abstract else extract_abstract(text)
}
except Exception:
pass
# -------------------------------
# Fallback (Regex)
# -------------------------------
return {
"title": extract_title(text),
"authors": extract_authors(text),
"abstract": extract_abstract(text)
}