OPEN SOURCE
software/scripts/fetch_pericope_data.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1#!/usr/bin/env python32"""3Fetch and extract pericope/paragraph data from SBLGNT.45This script downloads the SBLGNT OSIS XML and extracts paragraph6boundaries to augment the manually curated pericopes.json.78Usage:9uv run python scripts/fetch_pericope_data.py [--book BOOK]1011Data Sources:12- SBLGNT OSIS XML: https://sblgnt.com/download/sblgnt.osis.zip13- KJV with paragraph markers: https://github.com/farskipper/kjv14"""1516from __future__ import annotations1718import argparse19import json20import re21import tempfile22import zipfile23from pathlib import Path24from urllib.request import urlretrieve25from xml.etree import ElementTree as ET2627# OSIS XML namespace28OSIS_NS = {"osis": "http://www.bibletechnologies.net/2003/OSIS/namespace"}2930# Book name mappings (OSIS ID -> our format)31BOOK_MAP = {32"Matt": "matthew",33"Mark": "mark",34"Luke": "luke",35"John": "john",36"Acts": "acts",37"Rom": "romans",38"1Cor": "1corinthians",39"2Cor": "2corinthians",40"Gal": "galatians",41"Eph": "ephesians",42"Phil": "philippians",43"Col": "colossians",44"1Thess": "1thessalonians",45"2Thess": "2thessalonians",46"1Tim": "1timothy",47"2Tim": "2timothy",48"Titus": "titus",49"Phlm": "philemon",50"Heb": "hebrews",51"Jas": "james",52"1Pet": "1peter",53"2Pet": "2peter",54"1John": "1john",55"2John": "2john",56"3John": "3john",57"Jude": "jude",58"Rev": "revelation",59}606162def download_sblgnt_osis(cache_dir: Path) -> Path:63"""Download SBLGNT OSIS XML if not cached."""64zip_path = cache_dir / "sblgnt.osis.zip"65xml_path = cache_dir / "sblgnt.osis.xml"6667if xml_path.exists():68print(f"Using cached: {xml_path}")69return xml_path7071print("Downloading SBLGNT OSIS XML...")72url = "https://sblgnt.com/download/sblgnt.osis.zip"7374cache_dir.mkdir(parents=True, exist_ok=True)75urlretrieve(url, zip_path)7677print("Extracting...")78with zipfile.ZipFile(zip_path, "r") as zf:79# Find the XML file in the zip80xml_files = [n for n in zf.namelist() if n.endswith(".xml")]81if xml_files:82zf.extract(xml_files[0], cache_dir)83extracted = cache_dir / xml_files[0]84extracted.rename(xml_path)8586return xml_path878889def extract_paragraphs_from_osis(xml_path: Path, book_filter: str | None = None) -> dict:90"""91Extract paragraph boundaries from SBLGNT OSIS XML.9293The SBLGNT uses <p> elements to mark paragraph breaks.94Verse references are in <verse> elements with osisID attributes.9596Returns:97Dictionary of book -> list of paragraph boundaries98"""99print(f"Parsing {xml_path}...")100tree = ET.parse(xml_path)101root = tree.getroot()102103paragraphs = {}104105# Find all div elements (books)106for div in root.findall(".//osis:div[@type='book']", OSIS_NS):107book_id = div.get("osisID", "")108book_name = BOOK_MAP.get(book_id, book_id.lower())109110if book_filter and book_name != book_filter:111continue112113print(f"Processing {book_name}...")114book_paragraphs = []115current_para_verses = []116current_para_start = None117118# Iterate through all elements in order119for elem in div.iter():120tag = elem.tag.replace(f"{{{OSIS_NS['osis']}}}", "")121122if tag == "p":123# New paragraph - save previous if exists124if current_para_verses:125book_paragraphs.append({126"start": current_para_start,127"end": current_para_verses[-1],128"verses": current_para_verses.copy(),129})130current_para_verses = []131current_para_start = None132133elif tag == "verse":134osis_id = elem.get("osisID", "")135if osis_id and "." in osis_id:136# Parse "John.1.1" format137parts = osis_id.split(".")138if len(parts) >= 3:139chapter = int(parts[1])140verse = int(parts[2])141verse_ref = f"{chapter}:{verse}"142143if current_para_start is None:144current_para_start = verse_ref145current_para_verses.append(verse_ref)146147# Don't forget the last paragraph148if current_para_verses:149book_paragraphs.append({150"start": current_para_start,151"end": current_para_verses[-1],152"verses": current_para_verses,153})154155if book_paragraphs:156paragraphs[book_name] = book_paragraphs157158return paragraphs159160161def merge_with_pericopes(162paragraphs: dict,163pericopes_path: Path,164) -> dict:165"""166Merge extracted paragraph data with existing pericopes.json.167168Adds paragraph boundaries as a reference while preserving169manually curated pericope divisions.170"""171if pericopes_path.exists():172with pericopes_path.open("r", encoding="utf-8") as f:173pericopes = json.load(f)174else:175pericopes = {176"version": "1.0",177"source": "SBLGNT paragraph extraction",178"books": {},179}180181# Add paragraph data to each book182for book_name, book_paras in paragraphs.items():183if book_name not in pericopes["books"]:184pericopes["books"][book_name] = {185"name": book_name.title(),186"pericopes": [],187}188189# Store raw paragraph data separately190pericopes["books"][book_name]["sblgnt_paragraphs"] = book_paras191192return pericopes193194195def generate_pericopes_from_paragraphs(paragraphs: list[dict], book: str) -> list[dict]:196"""197Generate pericope entries from paragraph data.198199Combines small paragraphs into larger units (target: 5-15 verses).200"""201pericopes = []202current_verses = []203pericope_num = 1204205for para in paragraphs:206current_verses.extend(para["verses"])207208# If we have enough verses, create a pericope209if len(current_verses) >= 8 or para == paragraphs[-1]:210if current_verses:211start = current_verses[0]212end = current_verses[-1]213start_ch, start_v = map(int, start.split(":"))214end_ch, end_v = map(int, end.split(":"))215216pericopes.append({217"id": f"{book}-{start_ch}-{start_v}-auto",218"title": None,219"title_ja": None,220"start_chapter": start_ch,221"start_verse": start_v,222"end_chapter": end_ch,223"end_verse": end_v,224"verses": current_verses.copy(),225"theme": None,226"auto_generated": True,227})228current_verses = []229pericope_num += 1230231return pericopes232233234def generate_paragraphs_from_sources(book: str, sources_dir: Path) -> list[dict]:235"""Fallback paragraph generation from local source texts.236237The SBLGNT OSIS download is the textual apparatus (commentary238annotations), not the running text — it contains no verse elements, so239paragraph extraction yields nothing. And OT books are not in SBLGNT at240all. This fallback windows the verses of each chapter (from the local241greek/hebrew source JSON) into fixed-size pseudo-paragraphs so that242chunk/pericope translation is available for every book.243"""244for lang in ("greek", "hebrew"):245path = sources_dir / lang / f"{book}.json"246if path.exists():247with path.open("r", encoding="utf-8") as f:248data = json.load(f)249break250else:251return []252253by_chapter: dict[int, list[int]] = {}254for key in data:255parts = key.split(":")256if len(parts) == 2:257by_chapter.setdefault(int(parts[0]), []).append(int(parts[1]))258259paragraphs = []260window = 6261for chapter in sorted(by_chapter):262verses = sorted(by_chapter[chapter])263for i in range(0, len(verses), window):264chunk = verses[i : i + window]265refs = [f"{chapter}:{v}" for v in chunk]266paragraphs.append({"start": refs[0], "end": refs[-1], "verses": refs})267268return paragraphs269270271def main():272parser = argparse.ArgumentParser(273description="Fetch and extract pericope data from SBLGNT"274)275parser.add_argument(276"--book",277help="Only process specific book (e.g., 'john')",278)279parser.add_argument(280"--output",281default="data/metadata/pericopes.json",282help="Output file path",283)284parser.add_argument(285"--cache-dir",286default=".cache/sblgnt",287help="Cache directory for downloaded files",288)289parser.add_argument(290"--auto-generate",291action="store_true",292help="Auto-generate pericopes from paragraphs",293)294args = parser.parse_args()295296# Get project root297script_dir = Path(__file__).parent298project_root = script_dir.parent299cache_dir = project_root / args.cache_dir300output_path = project_root / args.output301302# Download SBLGNT303xml_path = download_sblgnt_osis(cache_dir)304305# Extract paragraphs306paragraphs = extract_paragraphs_from_osis(xml_path, args.book)307308print(f"\nExtracted paragraphs from {len(paragraphs)} books:")309for book, paras in paragraphs.items():310print(f" {book}: {len(paras)} paragraphs")311312# Fallback: OSIS extraction found nothing for the requested book313# (apparatus-only download, or an OT book not in SBLGNT).314if args.book and args.book not in paragraphs:315fallback = generate_paragraphs_from_sources(316args.book, project_root / "data" / "sources"317)318if fallback:319print(320f"OSIS had no paragraphs for {args.book}; generated "321f"{len(fallback)} windowed pseudo-paragraphs from local sources"322)323paragraphs[args.book] = fallback324325# Merge with existing pericopes326result = merge_with_pericopes(paragraphs, output_path)327328# Optionally auto-generate pericopes329if args.auto_generate:330for book_name, book_paras in paragraphs.items():331if book_name in result["books"]:332existing = result["books"][book_name].get("pericopes", [])333if not existing:334print(f"Auto-generating pericopes for {book_name}...")335result["books"][book_name]["pericopes"] = (336generate_pericopes_from_paragraphs(book_paras, book_name)337)338339# Save340output_path.parent.mkdir(parents=True, exist_ok=True)341with output_path.open("w", encoding="utf-8") as f:342json.dump(result, f, ensure_ascii=False, indent=2)343344print(f"\nSaved to: {output_path}")345346347if __name__ == "__main__":348main()349