Spaces:
Sleeping
Sleeping
Download src/detect_artifacts.py from PabloLE/Busqueda-Arxiv: direct link, hf CLI and curl.
- Browser
- Download file 7.14 kB
-
https://huggingface.co/spaces/PabloLE/Busqueda-Arxiv/resolve/main/src/detect_artifacts.py
- Command line
-
hf download hf://spaces/PabloLE/Busqueda-Arxiv/src/detect_artifacts.py
-
curl -L -o detect_artifacts.py https://huggingface.co/spaces/PabloLE/Busqueda-Arxiv/resolve/main/src/detect_artifacts.py
7.14 kB
| """ | |
| Detector de codigo/datos basado en patrones regex sobre: | |
| - abstract (arXiv) | |
| - openalex_abstract (a veces la version del journal, mas pulida) | |
| - comment (campo "comments" de arXiv: ahi suelen ir links a github) | |
| Estrategia: | |
| - URL patterns: hosts conocidos. Si aparece -> has_code/has_data = True | |
| + se guarda la URL deduplicada. | |
| - Promise patterns: frases que prometen disponibilidad ("code is available", | |
| "we release the dataset", etc.). Si aparece pero no hay URL -> flag True, | |
| URL list vacia. (Util porque a veces el link esta en el PDF, no en metadata.) | |
| Limitaciones conocidas: | |
| - No parsea el PDF (eso queda para v2). Cobertura limitada al ~5-15% en q-fin. | |
| - Falsos positivos posibles: "github.com" mencionado en passing sin ser | |
| realmente el repo del paper. Costo aceptable para un MVP. | |
| Uso: | |
| from src.detect_artifacts import detect, detect_for_db | |
| result = detect("Code: https://github.com/foo/bar. Dataset on Zenodo.") | |
| # -> {"has_code": True, "has_data": True, "code_urls": [...], "data_urls": [...]} | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| import sqlite3 | |
| from dataclasses import dataclass, field | |
| from typing import Iterable | |
| log = logging.getLogger(__name__) | |
| # ------------------------------------------------------------ | |
| # Patrones de URL (hosts conocidos) | |
| # ------------------------------------------------------------ | |
| # Permite paths con / . _ - y digitos. Corta en whitespace o paren/coma/comilla. | |
| _URL_TAIL = r"[A-Za-z0-9_./\-]+" | |
| CODE_URL_PATTERNS = [ | |
| re.compile(rf"https?://(?:www\.)?github\.com/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?gitlab\.com/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?bitbucket\.org/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?codeocean\.com/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?huggingface\.co/{_URL_TAIL}", re.IGNORECASE), | |
| # github sin esquema explicito (muy comun en comments) | |
| re.compile(rf"(?<![\w/])github\.com/{_URL_TAIL}", re.IGNORECASE), | |
| ] | |
| DATA_URL_PATTERNS = [ | |
| re.compile(rf"https?://(?:www\.)?zenodo\.org/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?figshare\.com/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?osf\.io/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://dataverse\.[A-Za-z0-9.\-]+/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?kaggle\.com/datasets/{_URL_TAIL}", re.IGNORECASE), | |
| re.compile(rf"https?://(?:www\.)?data\.mendeley\.com/{_URL_TAIL}", re.IGNORECASE), | |
| ] | |
| # Datasets en HuggingFace tienen URL distinta -> los tratamos como data | |
| HF_DATASET_RE = re.compile( | |
| rf"https?://(?:www\.)?huggingface\.co/datasets/{_URL_TAIL}", re.IGNORECASE | |
| ) | |
| # ------------------------------------------------------------ | |
| # Patrones de promesa (sin URL explicita) | |
| # ------------------------------------------------------------ | |
| # Compilamos con \b para evitar matches dentro de palabras. | |
| CODE_PROMISE_PATTERNS = [ | |
| re.compile(r"\bcode\s+is\s+(publicly\s+|freely\s+)?available", re.IGNORECASE), | |
| re.compile(r"\b(source\s+)?code\s+(can\s+be|is|will\s+be)\s+(found|accessed|downloaded|made\s+available)", re.IGNORECASE), | |
| re.compile(r"\bwe\s+(make|release|provide|publish)\s+(the\s+|our\s+)?(source\s+)?code", re.IGNORECASE), | |
| re.compile(r"\breplication\s+(package|code|kit|materials)\b", re.IGNORECASE), | |
| re.compile(r"\bopen[\s-]?sourced?\b", re.IGNORECASE), | |
| re.compile(r"\bimplementation\s+is\s+(publicly\s+)?available", re.IGNORECASE), | |
| ] | |
| DATA_PROMISE_PATTERNS = [ | |
| re.compile(r"\b(data|dataset|datasets)\s+(is|are)\s+(publicly\s+|freely\s+)?available", re.IGNORECASE), | |
| re.compile(r"\bwe\s+(release|provide|publish|share)\s+(the\s+|our\s+|a\s+new\s+)?(data|dataset|datasets)", re.IGNORECASE), | |
| re.compile(r"\b(public|open)\s+dataset\b", re.IGNORECASE), | |
| re.compile(r"\bsupplementary\s+(materials?|data)\b", re.IGNORECASE), | |
| ] | |
| # ------------------------------------------------------------ | |
| # Detection | |
| # ------------------------------------------------------------ | |
| class Detection: | |
| has_code: bool = False | |
| has_data: bool = False | |
| code_urls: list[str] = field(default_factory=list) | |
| data_urls: list[str] = field(default_factory=list) | |
| def _clean_url(u: str) -> str: | |
| """Quita puntuacion terminal tipica de prosa: . , ; ) etc.""" | |
| return u.rstrip(".,;)>'\"`") | |
| def detect(*texts: str | None) -> Detection: | |
| """Aplica todos los patrones sobre la union de los textos provistos.""" | |
| blob = " \n ".join(t for t in texts if t) | |
| if not blob: | |
| return Detection() | |
| code_urls: set[str] = set() | |
| data_urls: set[str] = set() | |
| # HF datasets ANTES que HF general, para clasificarlo como data, no code. | |
| for m in HF_DATASET_RE.finditer(blob): | |
| data_urls.add(_clean_url(m.group(0))) | |
| for pat in CODE_URL_PATTERNS: | |
| for m in pat.finditer(blob): | |
| url = _clean_url(m.group(0)) | |
| # Excluir HF datasets que ya cayeron en data | |
| if "/datasets/" in url.lower(): | |
| continue | |
| code_urls.add(url) | |
| for pat in DATA_URL_PATTERNS: | |
| for m in pat.finditer(blob): | |
| data_urls.add(_clean_url(m.group(0))) | |
| has_code = bool(code_urls) or any(p.search(blob) for p in CODE_PROMISE_PATTERNS) | |
| has_data = bool(data_urls) or any(p.search(blob) for p in DATA_PROMISE_PATTERNS) | |
| return Detection( | |
| has_code=has_code, | |
| has_data=has_data, | |
| code_urls=sorted(code_urls), | |
| data_urls=sorted(data_urls), | |
| ) | |
| # ------------------------------------------------------------ | |
| # DB sweep | |
| # ------------------------------------------------------------ | |
| class DetectStats: | |
| scanned: int = 0 | |
| with_code: int = 0 | |
| with_data: int = 0 | |
| with_code_url: int = 0 | |
| with_data_url: int = 0 | |
| def detect_for_db( | |
| conn: sqlite3.Connection, | |
| only_missing: bool = True, | |
| ) -> DetectStats: | |
| """ | |
| Corre el detector sobre los papers de la DB. | |
| only_missing=True salta los papers que ya tienen has_code/has_data setteado | |
| (utiles para corridas incrementales). | |
| """ | |
| from src import db | |
| where = "WHERE has_code IS NULL AND has_data IS NULL" if only_missing else "" | |
| rows = conn.execute( | |
| f"SELECT arxiv_id, abstract, openalex_abstract, comment " | |
| f"FROM papers {where}" | |
| ).fetchall() | |
| stats = DetectStats(scanned=len(rows)) | |
| log.info("Escaneando %d papers...", len(rows)) | |
| for r in rows: | |
| det = detect(r["abstract"], r["openalex_abstract"], r["comment"]) | |
| db.update_artifacts( | |
| conn, | |
| arxiv_id=r["arxiv_id"], | |
| has_code=det.has_code, | |
| has_data=det.has_data, | |
| code_urls=det.code_urls, | |
| data_urls=det.data_urls, | |
| ) | |
| if det.has_code: | |
| stats.with_code += 1 | |
| if det.has_data: | |
| stats.with_data += 1 | |
| if det.code_urls: | |
| stats.with_code_url += 1 | |
| if det.data_urls: | |
| stats.with_data_url += 1 | |
| conn.commit() | |
| return stats | |