Source code for klea_utils.biblio.regex
#!/usr/bin/env python3
"""
Regex-based bibliographic metadata extraction
File: klea_utils/biblio/regex.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import logging
import re
from typing import Any
logger = logging.getLogger(__name__)
#: Loose DOI pattern: a DOI begins with "10." followed by a 4-9 digit
#: registrant prefix and a slash.
DOI_RE = re.compile(r"\b10\.\d{4,9}/[^\s,;]+")
#: Loose URL pattern.
URL_RE = re.compile(r"https?://[^\s,;]+")
#: Labeled keyword list header (Keywords:, Key words:, ...).
_KEYWORDS_RE = re.compile(
r"(?im)^\s*(?:key\s*words?\s*(?:and\s*phrases?)?|key\s*terms?)\s*:\s*(.+)$"
)
#: Labeled author list header, without the bare "By:" variant.
_AUTHORS_LABELED_RE = re.compile(r"(?im)^\s*authors?\s*\(?s?\)?\s*:\s*(.{3,500})$")
#: Bare "By:" author line, only consulted when no labeled header matched.
_AUTHORS_BY_RE = re.compile(r"(?im)^\s*by\s*:\s*(.{3,500})$")
#: Labeled DOI line.
_DOI_LABELED_RE = re.compile(r"(?im)^\s*doi\s*:\s*(10\.\d{4,9}/[^\s,;]+)")
#: Labeled URL line (URL:, Website:, Homepage:, Webpage:).
_URL_LABELED_RE = re.compile(r"(?im)^\s*(?:url|website|homepage|webpage)\s*:\s*(\S+)")
#: Default number of leading characters scanned. Bibliographic headers
#: (authors, keywords, DOI, URL) live on the first page, so scanning the
#: whole document is unnecessary and would raise false-positive noise.
DEFAULT_SCAN_LIMIT = 3000
def _scan_keywords(text: str) -> list[str]:
"""Return the keyword list from a labeled keyword header, if any."""
match = _KEYWORDS_RE.search(text)
if not match:
return []
return _split_terms(match.group(1))
def _scan_authors(text: str) -> list[str]:
"""Return the author list from a labeled header, preferring
``Author(s):``/``Authors:`` over a bare ``By:`` line."""
match = _AUTHORS_LABELED_RE.search(text)
if not match:
match = _AUTHORS_BY_RE.search(text)
if not match:
return []
return _split_terms(match.group(1))
def _scan_doi(text: str) -> str | None:
"""Return a DOI from a labeled line, or from the loose pattern."""
match = _DOI_LABELED_RE.search(text)
if match:
return _rstrip_punct(match.group(1))
match = DOI_RE.search(text)
if match:
return _rstrip_punct(match.group(0))
return None
def _scan_url(text: str) -> str | None:
"""Return a URL from a labeled line (URL:, Website:, ...)."""
match = _URL_LABELED_RE.search(text)
if not match:
return None
candidate = _rstrip_punct(match.group(1))
url_match = URL_RE.search(candidate)
if url_match:
return _rstrip_punct(url_match.group(0))
if candidate.startswith("www."):
return candidate
return None
def _split_terms(value: str) -> list[str]:
"""Split a comma/semicolon/newline-separated term list."""
terms = re.split(r"[,;\n]", value)
return [term.strip().rstrip(".") for term in terms if term.strip()]
def _rstrip_punct(value: str) -> str:
"""Strip trailing punctuation that may follow a DOI/URL in a value."""
return value.rstrip(".,;:)]}")