Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| Tipitaka Query Helper | |
| ===================== | |
| Helper functions สำหรับ query ฐานข้อมูลพระไตรปิฎก | |
| Usage: | |
| from tipitaka_query import TipitakaDB | |
| db = TipitakaDB("tipitaka_mcu.db") | |
| # ค้นหาคำ | |
| results = db.search("อริยสัจ", limit=10) | |
| # ดึงหน้า | |
| page = db.get_page(volume=10, page=50) | |
| # ดึงตามเลขข้อ | |
| page = db.get_by_item(volume=10, item=123) | |
| # ดึงสารบัญ | |
| toc = db.get_toc(volume=10) | |
| """ | |
| import re | |
| import sqlite3 | |
| from pathlib import Path | |
| from typing import Optional, List, Dict, Any | |
| from dataclasses import dataclass | |
| def clean_text( | |
| text: str, | |
| remove_line_numbers: bool = True, | |
| remove_footnotes: bool = True, | |
| remove_item_numbers: bool = False, | |
| for_tts: bool = False | |
| ) -> str: | |
| """ | |
| ทำความสะอาดข้อความพระไตรปิฎก (รองรับ Clean Dataset) | |
| Args: | |
| text: ข้อความดิบ | |
| remove_line_numbers: ลบเลขบรรทัด (001, 002, ...) | |
| remove_footnotes: ลบเชิงอรรถ (บรรทัดที่ขึ้นต้นด้วย @ หรือ [เชิงอรรถ]) | |
| remove_item_numbers: ลบเลขข้อ [๑], [๒๓], ๑- เป็นต้น | |
| for_tts: เตรียมสำหรับ TTS (ลบทุกอย่างที่ไม่ควรอ่าน) | |
| Returns: | |
| ข้อความที่ clean แล้ว | |
| """ | |
| if for_tts: | |
| remove_line_numbers = True | |
| remove_footnotes = True | |
| remove_item_numbers = True | |
| lines = text.split('\n') | |
| cleaned = [] | |
| for line in lines: | |
| # 1. ลบเลขบรรทัด (ถ้ามีหลุดมา) | |
| if remove_line_numbers: | |
| line = re.sub(r'^\s*\d{3}\s+', '', line) | |
| # 2. ข้ามบรรทัดเชิงอรรถ | |
| # รองรับทั้ง @ (แบบเก่า) และ [เชิงอรรถ] (แบบคลีน) | |
| s_line = line.strip() | |
| if remove_footnotes: | |
| if s_line.startswith('@') or s_line.startswith('[เชิงอรรถ]'): | |
| continue | |
| # 3. ลบเลขข้อ และ footnote markers | |
| if remove_item_numbers: | |
| # ลบ [๑], [1] | |
| line = re.sub(r'\[[\u0E50-\u0E59]+\]', '', line) | |
| line = re.sub(r'\[\d+\]', '', line) | |
| # ลบ ๑-, ๑-๒, 1-, 1-2 | |
| line = re.sub(r'[\u0E50-\u0E59]+-[\u0E50-\u0E59]*', '', line) | |
| line = re.sub(r'\d+-\d*', '', line) | |
| else: | |
| # เก็บ [๑] ไว้ แต่ลบ ๑- (footnote reference) ทิ้ง | |
| # ต้องระวังไม่ให้ลบ ๑- ที่เป็นส่วนหนึ่งของเลขข้อ เช่น "๑-๕. เรื่อง..." | |
| # ปกติ footnote reference จะอยู่หลังคำ/ประโยคทันที โดยไม่มีช่องว่าง | |
| line = re.sub(r'(?<=[^\s])[\u0E50-\u0E59]+-', '', line) | |
| cleaned.append(line) | |
| # รวมบรรทัดและลบช่องว่างซ้ำ | |
| result = '\n'.join(cleaned) | |
| result = re.sub(r'\n{3,}', '\n\n', result) | |
| result = re.sub(r' +', ' ', result) | |
| return result.strip() | |
| class SearchResult: | |
| """ผลลัพธ์การค้นหา""" | |
| volume_number: int | |
| volume_title: str | |
| page_number: int | |
| snippet: str | |
| rank: float | |
| class PageContent: | |
| """เนื้อหาหน้า""" | |
| volume_number: int | |
| volume_title: str | |
| page_number: int | |
| content_text: str | |
| content_html: Optional[str] = None | |
| class TocItem: | |
| """รายการสารบัญ""" | |
| title: str | |
| page_number: int | |
| level: int | |
| class TipitakaDB: | |
| """Interface สำหรับ query ฐานข้อมูลพระไตรปิฎก""" | |
| def __init__(self, db_path: str | Path): | |
| self.db_path = Path(db_path) | |
| if not self.db_path.exists(): | |
| raise FileNotFoundError(f"Database not found: {db_path}") | |
| self._conn = None | |
| def conn(self) -> sqlite3.Connection: | |
| if self._conn is None: | |
| self._conn = sqlite3.connect(self.db_path) | |
| self._conn.row_factory = sqlite3.Row | |
| return self._conn | |
| def close(self): | |
| if self._conn: | |
| self._conn.close() | |
| self._conn = None | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| self.close() | |
| # ========== Search Functions ========== | |
| def search(self, query: str, limit: int = 20, volume: Optional[int] = None) -> List[SearchResult]: | |
| """ | |
| ค้นหาคำในพระไตรปิฎก (Full-Text Search) | |
| Args: | |
| query: คำที่ต้องการค้นหา | |
| limit: จำนวนผลลัพธ์สูงสุด | |
| volume: จำกัดการค้นหาในเล่มที่ระบุ (optional) | |
| Returns: | |
| List of SearchResult | |
| """ | |
| sql = """ | |
| SELECT | |
| v.volume_number, | |
| v.title as volume_title, | |
| p.page_number, | |
| snippet(pages_fts, 0, '>>>', '<<<', '...', 50) as snippet, | |
| rank | |
| FROM pages_fts | |
| JOIN pages p ON pages_fts.rowid = p.id | |
| JOIN volumes v ON p.volume_id = v.id | |
| WHERE pages_fts MATCH ? | |
| """ | |
| params = [query] | |
| if volume: | |
| sql += " AND v.volume_number = ?" | |
| params.append(volume) | |
| sql += " ORDER BY rank LIMIT ?" | |
| params.append(limit) | |
| cursor = self.conn.execute(sql, params) | |
| return [ | |
| SearchResult( | |
| volume_number=row['volume_number'], | |
| volume_title=row['volume_title'], | |
| page_number=row['page_number'], | |
| snippet=row['snippet'], | |
| rank=row['rank'] | |
| ) | |
| for row in cursor.fetchall() | |
| ] | |
| def search_simple(self, keyword: str, limit: int = 20) -> List[Dict[str, Any]]: | |
| """ | |
| ค้นหาแบบง่าย (LIKE) - ใช้เมื่อ FTS ไม่ทำงาน | |
| Returns: | |
| List of dicts with volume, page, excerpt | |
| """ | |
| sql = """ | |
| SELECT | |
| v.volume_number, | |
| v.title as volume_title, | |
| p.page_number, | |
| substr(p.content_text, max(1, instr(p.content_text, ?) - 50), 150) as excerpt | |
| FROM pages p | |
| JOIN volumes v ON p.volume_id = v.id | |
| WHERE p.content_text LIKE ? | |
| LIMIT ? | |
| """ | |
| cursor = self.conn.execute(sql, [keyword, f"%{keyword}%", limit]) | |
| return [dict(row) for row in cursor.fetchall()] | |
| # ========== Get Functions ========== | |
| def get_page(self, volume: int, page: int, include_html: bool = False) -> Optional[PageContent]: | |
| """ | |
| ดึงเนื้อหาหน้าที่ระบุ | |
| Args: | |
| volume: เลขเล่ม (1-45) | |
| page: เลขหน้า | |
| include_html: รวม HTML content ด้วยหรือไม่ | |
| """ | |
| sql = """ | |
| SELECT | |
| v.volume_number, | |
| v.title as volume_title, | |
| p.page_number, | |
| p.content_text, | |
| p.content_html | |
| FROM pages p | |
| JOIN volumes v ON p.volume_id = v.id | |
| WHERE v.volume_number = ? AND p.page_number = ? | |
| """ | |
| cursor = self.conn.execute(sql, [volume, page]) | |
| row = cursor.fetchone() | |
| if not row: | |
| return None | |
| return PageContent( | |
| volume_number=row['volume_number'], | |
| volume_title=row['volume_title'], | |
| page_number=row['page_number'], | |
| content_text=row['content_text'], | |
| content_html=row['content_html'] if include_html else None | |
| ) | |
| def get_pages(self, volume: int, start_page: int, end_page: int) -> List[PageContent]: | |
| """ดึงหลายหน้าพร้อมกัน""" | |
| sql = """ | |
| SELECT | |
| v.volume_number, | |
| v.title as volume_title, | |
| p.page_number, | |
| p.content_text | |
| FROM pages p | |
| JOIN volumes v ON p.volume_id = v.id | |
| WHERE v.volume_number = ? AND p.page_number BETWEEN ? AND ? | |
| ORDER BY p.page_number | |
| """ | |
| cursor = self.conn.execute(sql, [volume, start_page, end_page]) | |
| return [ | |
| PageContent( | |
| volume_number=row['volume_number'], | |
| volume_title=row['volume_title'], | |
| page_number=row['page_number'], | |
| content_text=row['content_text'] | |
| ) | |
| for row in cursor.fetchall() | |
| ] | |
| def get_section_text( | |
| self, | |
| volume: int, | |
| start_page: int, | |
| end_page: int, | |
| clean: bool = True, | |
| remove_line_numbers: bool = True, | |
| remove_footnotes: bool = True, | |
| remove_item_numbers: bool = False, | |
| for_tts: bool = False | |
| ) -> str: | |
| """ | |
| ดึงเนื้อหาหลายหน้าและรวมเป็น string เดียว | |
| Args: | |
| volume: เลขเล่ม (1-45) | |
| start_page: หน้าเริ่มต้น | |
| end_page: หน้าสิ้นสุด | |
| clean: ทำความสะอาดข้อความ | |
| remove_line_numbers: ลบเลขบรรทัด | |
| remove_footnotes: ลบเชิงอรรถ | |
| remove_item_numbers: ลบเลขข้อ [๑], [๒๓] | |
| for_tts: เตรียมสำหรับ TTS (ลบทุกอย่าง) | |
| Returns: | |
| เนื้อหาทั้งหมดรวมกันเป็น string | |
| """ | |
| pages = self.get_pages(volume, start_page, end_page) | |
| texts = [] | |
| for p in pages: | |
| text = p.content_text | |
| if clean: | |
| text = clean_text( | |
| text, | |
| remove_line_numbers, | |
| remove_footnotes, | |
| remove_item_numbers, | |
| for_tts | |
| ) | |
| texts.append(text) | |
| return "\n\n".join(texts) | |
| def get_by_item(self, volume: int, item_number: int) -> Optional[PageContent]: | |
| """ | |
| ดึงเนื้อหาตามเลขข้อ/เลขย่อหน้า | |
| Args: | |
| volume: เลขเล่ม (1-45) | |
| item_number: เลขข้อ | |
| """ | |
| sql = """ | |
| SELECT page_number | |
| FROM item_numbers i | |
| JOIN volumes v ON i.volume_id = v.id | |
| WHERE v.volume_number = ? AND i.item_number = ? | |
| """ | |
| cursor = self.conn.execute(sql, [volume, item_number]) | |
| row = cursor.fetchone() | |
| if not row: | |
| return None | |
| return self.get_page(volume, row['page_number']) | |
| # ========== Section Search ========== | |
| def find_section(self, title: str, volume: Optional[int] = None) -> List[Dict[str, Any]]: | |
| """ | |
| ค้นหา section/พระสูตรจากชื่อในสารบัญ | |
| Args: | |
| title: ชื่อที่ต้องการค้นหา (บางส่วนก็ได้) | |
| volume: จำกัดการค้นหาในเล่มที่ระบุ (optional) | |
| Returns: | |
| List of dict with volume, title, start_page, end_page | |
| """ | |
| sql = """ | |
| SELECT | |
| v.volume_number, | |
| v.title as volume_title, | |
| c.id, | |
| c.title, | |
| c.page_number, | |
| c.level | |
| FROM contents c | |
| JOIN volumes v ON c.volume_id = v.id | |
| WHERE c.title LIKE ? | |
| """ | |
| params = [f"%{title}%"] | |
| if volume: | |
| sql += " AND v.volume_number = ?" | |
| params.append(volume) | |
| sql += " ORDER BY v.volume_number, c.id" | |
| cursor = self.conn.execute(sql, params) | |
| matches = cursor.fetchall() | |
| results = [] | |
| for match in matches: | |
| vol = match['volume_number'] | |
| start_page = match['page_number'] | |
| section_id = match['id'] | |
| section_level = match['level'] | |
| # หา end_page โดยดู section ถัดไปที่มี level เท่ากันหรือน้อยกว่า | |
| sql_next = """ | |
| SELECT page_number | |
| FROM contents c | |
| JOIN volumes v ON c.volume_id = v.id | |
| WHERE v.volume_number = ? AND c.id > ? AND c.level <= ? | |
| ORDER BY c.id | |
| LIMIT 1 | |
| """ | |
| cursor = self.conn.execute(sql_next, [vol, section_id, section_level]) | |
| next_row = cursor.fetchone() | |
| if next_row: | |
| end_page = next_row['page_number'] - 1 | |
| else: | |
| # ถ้าไม่มี section ถัดไป ให้ใช้หน้าสุดท้ายของเล่ม | |
| sql_max = """ | |
| SELECT MAX(page_number) as max_page | |
| FROM pages p | |
| JOIN volumes v ON p.volume_id = v.id | |
| WHERE v.volume_number = ? | |
| """ | |
| cursor = self.conn.execute(sql_max, [vol]) | |
| end_page = cursor.fetchone()['max_page'] | |
| results.append({ | |
| 'volume': vol, | |
| 'volume_title': match['volume_title'], | |
| 'title': match['title'], | |
| 'start_page': start_page, | |
| 'end_page': end_page, | |
| 'level': section_level | |
| }) | |
| return results | |
| def get_section( | |
| self, | |
| title: str, | |
| volume: Optional[int] = None, | |
| for_tts: bool = False | |
| ) -> Optional[str]: | |
| """ | |
| ดึงเนื้อหาพระสูตร/section ทั้งหมดจากชื่อ | |
| Args: | |
| title: ชื่อพระสูตร (บางส่วนก็ได้) | |
| volume: เล่มที่ต้องการ (optional, ถ้าไม่ระบุจะใช้ผลลัพธ์แรก) | |
| for_tts: เตรียมสำหรับ TTS | |
| Returns: | |
| เนื้อหาทั้งหมด หรือ None ถ้าไม่พบ | |
| """ | |
| sections = self.find_section(title, volume) | |
| if not sections: | |
| return None | |
| # ใช้ผลลัพธ์แรก | |
| section = sections[0] | |
| return self.get_section_text( | |
| volume=section['volume'], | |
| start_page=section['start_page'], | |
| end_page=section['end_page'], | |
| for_tts=for_tts | |
| ) | |
| # ========== Table of Contents ========== | |
| def get_toc(self, volume: int) -> List[TocItem]: | |
| """ดึงสารบัญของเล่มที่ระบุ""" | |
| sql = """ | |
| SELECT c.title, c.page_number, c.level | |
| FROM contents c | |
| JOIN volumes v ON c.volume_id = v.id | |
| WHERE v.volume_number = ? | |
| ORDER BY c.id | |
| """ | |
| cursor = self.conn.execute(sql, [volume]) | |
| return [ | |
| TocItem( | |
| title=row['title'], | |
| page_number=row['page_number'], | |
| level=row['level'] | |
| ) | |
| for row in cursor.fetchall() | |
| ] | |
| # ========== Volume Info ========== | |
| def get_volumes(self) -> List[Dict[str, Any]]: | |
| """ดึงรายการเล่มทั้งหมด""" | |
| sql = """ | |
| SELECT volume_number, title, pitaka, total_pages | |
| FROM volumes | |
| ORDER BY volume_number | |
| """ | |
| cursor = self.conn.execute(sql) | |
| return [dict(row) for row in cursor.fetchall()] | |
| def get_volume_info(self, volume: int) -> Optional[Dict[str, Any]]: | |
| """ดึงข้อมูลเล่มที่ระบุ""" | |
| sql = """ | |
| SELECT volume_number, title, pitaka, total_pages | |
| FROM volumes | |
| WHERE volume_number = ? | |
| """ | |
| cursor = self.conn.execute(sql, [volume]) | |
| row = cursor.fetchone() | |
| return dict(row) if row else None | |
| # ========== Stats ========== | |
| def get_stats(self) -> Dict[str, Any]: | |
| """ดึงสถิติของฐานข้อมูล""" | |
| stats = {} | |
| # Volume count | |
| cursor = self.conn.execute("SELECT COUNT(*) FROM volumes") | |
| stats['total_volumes'] = cursor.fetchone()[0] | |
| # Page count | |
| cursor = self.conn.execute("SELECT COUNT(*) FROM pages") | |
| stats['total_pages'] = cursor.fetchone()[0] | |
| # Content items | |
| cursor = self.conn.execute("SELECT COUNT(*) FROM contents") | |
| stats['total_toc_items'] = cursor.fetchone()[0] | |
| # Item numbers | |
| cursor = self.conn.execute("SELECT COUNT(*) FROM item_numbers") | |
| stats['total_item_numbers'] = cursor.fetchone()[0] | |
| # By pitaka | |
| cursor = self.conn.execute(""" | |
| SELECT pitaka, COUNT(*) as count, SUM(total_pages) as pages | |
| FROM volumes | |
| GROUP BY pitaka | |
| """) | |
| stats['by_pitaka'] = {row['pitaka']: {'volumes': row['count'], 'pages': row['pages']} | |
| for row in cursor.fetchall()} | |
| return stats | |
| # ========== Convenience Functions ========== | |
| def search_text(keyword: str, db_path: str = "tipitaka_mcu.db", limit: int = 10) -> List[Dict]: | |
| """Quick search function""" | |
| with TipitakaDB(db_path) as db: | |
| results = db.search(keyword, limit=limit) | |
| return [ | |
| { | |
| 'volume': r.volume_number, | |
| 'title': r.volume_title, | |
| 'page': r.page_number, | |
| 'snippet': r.snippet | |
| } | |
| for r in results | |
| ] | |
| def get_page(volume: int, page: int, db_path: str = "tipitaka_mcu.db") -> Optional[str]: | |
| """Quick get page function""" | |
| with TipitakaDB(db_path) as db: | |
| result = db.get_page(volume, page) | |
| return result.content_text if result else None | |
| def get_toc(volume: int, db_path: str = "tipitaka_mcu.db") -> List[Dict]: | |
| """Quick get TOC function""" | |
| with TipitakaDB(db_path) as db: | |
| items = db.get_toc(volume) | |
| return [{'title': i.title, 'page': i.page_number, 'level': i.level} for i in items] | |
| # ========== CLI ========== | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Query Tipitaka Database") | |
| parser.add_argument("--db", default="tipitaka_mcu.db", help="Database path") | |
| subparsers = parser.add_subparsers(dest="command") | |
| # Search command | |
| search_parser = subparsers.add_parser("search", help="Search text") | |
| search_parser.add_argument("query", help="Search query") | |
| search_parser.add_argument("-n", "--limit", type=int, default=10, help="Max results") | |
| search_parser.add_argument("-v", "--volume", type=int, help="Limit to volume") | |
| # Get page command | |
| page_parser = subparsers.add_parser("page", help="Get page content") | |
| page_parser.add_argument("volume", type=int, help="Volume number") | |
| page_parser.add_argument("page", type=int, help="Page number") | |
| # TOC command | |
| toc_parser = subparsers.add_parser("toc", help="Get table of contents") | |
| toc_parser.add_argument("volume", type=int, help="Volume number") | |
| # Stats command | |
| subparsers.add_parser("stats", help="Show database statistics") | |
| args = parser.parse_args() | |
| if not args.command: | |
| parser.print_help() | |
| return | |
| db = TipitakaDB(args.db) | |
| if args.command == "search": | |
| results = db.search(args.query, limit=args.limit, volume=args.volume) | |
| for r in results: | |
| print(f"\n[เล่ม {r.volume_number} หน้า {r.page_number}] {r.volume_title}") | |
| print(f" {r.snippet}") | |
| elif args.command == "page": | |
| page = db.get_page(args.volume, args.page) | |
| if page: | |
| print(f"=== {page.volume_title} - หน้า {page.page_number} ===\n") | |
| print(page.content_text) | |
| else: | |
| print("ไม่พบหน้าที่ระบุ") | |
| elif args.command == "toc": | |
| toc = db.get_toc(args.volume) | |
| for item in toc: | |
| indent = " " * item.level | |
| print(f"{indent}{item.title} (หน้า {item.page_number})") | |
| elif args.command == "stats": | |
| stats = db.get_stats() | |
| print(f"Total volumes: {stats['total_volumes']}") | |
| print(f"Total pages: {stats['total_pages']}") | |
| print(f"Total TOC items: {stats['total_toc_items']}") | |
| print(f"Total item numbers: {stats['total_item_numbers']}") | |
| print("\nBy Pitaka:") | |
| for pitaka, data in stats['by_pitaka'].items(): | |
| print(f" {pitaka}: {data['volumes']} volumes, {data['pages']} pages") | |
| db.close() | |
| if __name__ == "__main__": | |
| main() | |