feat(papers): B-3 PR6 — OpenAlex ISSN 소스 시드 (KR/JP 안전 저널 직접 커버)

plan safety-library-b3-1 PR6 (revised). 라이브 정찰: KoreaScience=깨끗한 API 없음(OAI 404)·
J-STAGE=ToS bulk 금지, 그리고 Phase-1 메타는 OpenAlex 가 이미 전수 색인(한국안전학회지 1766건 실측)
→ 전용 스크래퍼 대신 검증된 OpenAlex 수집기에 도메인 저널 ISSN 시드 추가(전용 무료 전문 PDF=Phase-2 park).
- _JOURNAL_ISSNS(OpenAlex sources 실측): 한국안전학회지 1738-3803·한국가스학회지 1226-8402·
  KSME A/B 1226-4873·1226-4881·KSME Intl 1226-4865·JP 고압 0917-639X.
- _seeds() = ISSN 시드(cap 우선) + 키워드. build_issn_filter(primary_location.source.issn:).
  run() 루프 통합(종류별 필터, 워터마크 시드별). 적재/parser/cap/signal-only = PR3 재사용.

단위 8 passed(+ISSN 시드). 라이브 PASS: 키주입 run → 한국안전학회지 5건 적재(ISSN 우선 확인),
running fastapi 무접촉. KoreaScience/J-STAGE 전용 fulltext 수집기 = Phase-2 강등(park).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-06-13 22:42:30 +00:00
parent 1fbb341e28
commit fdabca2a2f
2 changed files with 47 additions and 6 deletions
+33 -6
View File
@@ -52,6 +52,18 @@ _KEYWORDS = (
"fatigue life assessment", "fatigue life assessment",
) )
# 도메인 직결 저널 ISSN 시드(OpenAlex sources 실측 확인) — 키워드 매칭 누락분까지 전수 커버.
# KR 안전/가스/기계 + JP 고압. KR/JP 관심 = OpenAlex 깨끗한 API 로 직접(KoreaScience/J-STAGE 전용
# 스크래퍼 불요 — Phase-1 메타는 OpenAlex 와 중복, 전용 수집기의 유니크 가치=무료 전문 PDF=Phase-2).
_JOURNAL_ISSNS = (
("한국안전학회지", "1738-3803"),
("한국가스학회지", "1226-8402"),
("대한기계학회논문집 A", "1226-4873"),
("대한기계학회논문집 B", "1226-4881"),
("KSME International J.", "1226-4865"),
("Review of High Pressure Sci&Tech (JP)", "0917-639X"),
)
_RUN_CAP = 60 # 1회 run 신규 적재 상한(임베드 큐 보호). bulk 시 해제. _RUN_CAP = 60 # 1회 run 신규 적재 상한(임베드 큐 보호). bulk 시 해제.
_PER_PAGE = 50 _PER_PAGE = 50
_MAX_PAGES_PER_KW = 4 # 키워드당 최대 페이지(증분이라 보통 1페이지에 워터마크 도달) _MAX_PAGES_PER_KW = 4 # 키워드당 최대 페이지(증분이라 보통 1페이지에 워터마크 도달)
@@ -145,6 +157,20 @@ def build_filter(keyword: str, from_date: str | None = None) -> str:
return f return f
def build_issn_filter(issn: str, from_date: str | None = None) -> str:
f = f"primary_location.source.issn:{issn}"
if from_date:
f += f",from_publication_date:{from_date}"
return f
def _seeds() -> list[tuple[str, str, str]]:
"""수집 시드 = (라벨, 워터마크키, 종류). 도메인 저널 ISSN 우선(cap 우선권) → 키워드."""
s: list[tuple[str, str, str]] = [(label, issn, "issn") for label, issn in _JOURNAL_ISSNS]
s += [(kw, kw, "kw") for kw in _KEYWORDS]
return s
# ───────────────────────── 적재 (DB — PR3 라이브 검증) ───────────────────────── # ───────────────────────── 적재 (DB — PR3 라이브 검증) ─────────────────────────
def _build_paper_meta(source: NewsSource, w: OpenAlexWork) -> dict: def _build_paper_meta(source: NewsSource, w: OpenAlexWork) -> dict:
@@ -296,13 +322,14 @@ async def run(bulk: bool = False, limit: int = 0) -> None:
async with httpx.AsyncClient( async with httpx.AsyncClient(
timeout=30.0, headers={"User-Agent": CRAWL_UA}, follow_redirects=True timeout=30.0, headers={"User-Agent": CRAWL_UA}, follow_redirects=True
) as client: ) as client:
for keyword in _KEYWORDS: for label, wm_key, kind in _seeds():
if inserted >= run_cap: if inserted >= run_cap:
break break
async with async_session() as session: async with async_session() as session:
src = await session.get(NewsSource, source_id) src = await session.get(NewsSource, source_id)
watermark = None if bulk else _watermark(src, keyword) watermark = None if bulk else _watermark(src, wm_key)
filter_str = build_filter(keyword, watermark) filter_str = (build_issn_filter(wm_key, watermark) if kind == "issn"
else build_filter(wm_key, watermark))
newest: str | None = None newest: str | None = None
cursor = "*" cursor = "*"
max_pages = (10**6 if bulk else _MAX_PAGES_PER_KW) max_pages = (10**6 if bulk else _MAX_PAGES_PER_KW)
@@ -334,10 +361,10 @@ async def run(bulk: bool = False, limit: int = 0) -> None:
if newest: if newest:
async with async_session() as session: async with async_session() as session:
src = await session.get(NewsSource, source_id) src = await session.get(NewsSource, source_id)
_set_watermark(src, keyword, newest) _set_watermark(src, wm_key, newest)
await session.commit() await session.commit()
except (httpx.HTTPError, FeedError, ValueError) as e: except (httpx.HTTPError, FeedError, ValueError) as e:
msg = f"[{keyword}] {e or repr(e)}" msg = f"[{label}] {e or repr(e)}"
logger.error(f"[openalex] {msg}") logger.error(f"[openalex] {msg}")
failures.append(msg) failures.append(msg)
@@ -351,7 +378,7 @@ async def run(bulk: bool = False, limit: int = 0) -> None:
deferred = "" if inserted < run_cap else f" (cap {run_cap} 도달 — 잔여 다음 run 이월)" deferred = "" if inserted < run_cap else f" (cap {run_cap} 도달 — 잔여 다음 run 이월)"
logger.info( logger.info(
f"[openalex] {len(_KEYWORDS)}개 키워드 스캔 {seen}건 → 신규 {inserted}{deferred}" f"[openalex] {len(_seeds())}시드(ISSN+키워드) 스캔 {seen}건 → 신규 {inserted}{deferred}"
+ (f" / 실패 {len(failures)}" if failures else "") + (f" / 실패 {len(failures)}" if failures else "")
) )
+14
View File
@@ -11,7 +11,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "app"))
from workers.openalex_collector import ( # noqa: E402 from workers.openalex_collector import ( # noqa: E402
_reconstruct_abstract, _reconstruct_abstract,
_seeds,
build_filter, build_filter,
build_issn_filter,
license_meta, license_meta,
parse_openalex_works, parse_openalex_works,
) )
@@ -90,3 +92,15 @@ def test_build_filter():
assert build_filter("process safety") == "title_and_abstract.search:process safety" assert build_filter("process safety") == "title_and_abstract.search:process safety"
assert build_filter("process safety", "2026-06-01") == \ assert build_filter("process safety", "2026-06-01") == \
"title_and_abstract.search:process safety,from_publication_date:2026-06-01" "title_and_abstract.search:process safety,from_publication_date:2026-06-01"
# ─── PR6: ISSN 소스 시드 (KR/JP 안전 저널 직접 커버) ───
def test_build_issn_filter_and_seeds():
assert build_issn_filter("1738-3803") == "primary_location.source.issn:1738-3803"
assert build_issn_filter("1738-3803", "2026-01-01") == \
"primary_location.source.issn:1738-3803,from_publication_date:2026-01-01"
seeds = _seeds()
kinds = [k for _, _, k in seeds]
assert kinds[0] == "issn" # ISSN 시드가 키워드보다 먼저(cap 우선권)
assert any(v == "1738-3803" and k == "issn" for _, v, k in seeds) # 한국안전학회지 포함