@nomadamas/k-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/k-skill.js +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
import ssl
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import asdict, dataclass
|
|
14
|
+
from http.cookiejar import CookieJar
|
|
15
|
+
from typing import Any, Iterable
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import requests
|
|
19
|
+
except ImportError: # pragma: no cover - optional runtime dependency
|
|
20
|
+
requests = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BASE_URL = "https://sillok.history.go.kr"
|
|
24
|
+
SEARCH_URL = f"{BASE_URL}/search/searchResultList.do"
|
|
25
|
+
DETAIL_URL_TEMPLATE = f"{BASE_URL}/id/{{article_id}}"
|
|
26
|
+
DEFAULT_TIMEOUT = 30
|
|
27
|
+
DEFAULT_LIMIT = 5
|
|
28
|
+
MAX_PAGES = 20
|
|
29
|
+
|
|
30
|
+
DEFAULT_HEADERS = {
|
|
31
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
32
|
+
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
33
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
34
|
+
"Origin": BASE_URL,
|
|
35
|
+
"Referer": f"{BASE_URL}/main/main.do",
|
|
36
|
+
"User-Agent": (
|
|
37
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
38
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.S)
|
|
43
|
+
BR_PATTERN = re.compile(r"<br\s*/?>", re.I)
|
|
44
|
+
TAG_PATTERN = re.compile(r"<[^>]+>")
|
|
45
|
+
WHITESPACE_PATTERN = re.compile(r"\s+")
|
|
46
|
+
DIGITS_PATTERN = re.compile(r"\d+")
|
|
47
|
+
DETAIL_FOOTER_PATTERN = re.compile(
|
|
48
|
+
r"\s*(?:[【〖](?:태백산사고본|국편영인본|분류)[】〗]|ⓒ\s*세종대왕기념사업회).*",
|
|
49
|
+
re.S,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
CATEGORY_PATTERN = re.compile(
|
|
53
|
+
r"<a\s+href=\"javascript:searchCategory\('([^']*)'\);\"\s+class=\"cate-item[^\"]*\">(.*?)</a>",
|
|
54
|
+
re.S,
|
|
55
|
+
)
|
|
56
|
+
RESULT_PATTERN = re.compile(
|
|
57
|
+
r"<div\s+class=\"result-box\">.*?"
|
|
58
|
+
r"<a\s+href=\"javascript:goView\('([^']+)',\s*\d+\);\"\s+class=\"subject\">(.*?)</a>\s*"
|
|
59
|
+
r"<p\s+class=\"text\">(.*?)</p>",
|
|
60
|
+
re.S,
|
|
61
|
+
)
|
|
62
|
+
TITLE_HEAD_PATTERN = re.compile(
|
|
63
|
+
r"<div\s+class=\"title\">\s*<p\s+class=\"date\">(.*?)</p>\s*<h3>(.*?)</h3>",
|
|
64
|
+
re.S,
|
|
65
|
+
)
|
|
66
|
+
LEFT_VIEW_PATTERN = re.compile(
|
|
67
|
+
r"<div\s+class=\"view-item\s+left\">.*?<div\s+class=\"view-text\">(.*?)</div>",
|
|
68
|
+
re.S,
|
|
69
|
+
)
|
|
70
|
+
RIGHT_VIEW_PATTERN = re.compile(
|
|
71
|
+
r"<div\s+class=\"view-item\s+right\">.*?<div\s+class=\"view-text\">(.*?)</div>",
|
|
72
|
+
re.S,
|
|
73
|
+
)
|
|
74
|
+
CLASSIFICATION_PATTERN = re.compile(r"<li\s+class=\"view_font02\">\s*[【〖]분류[】〗]\s*(.*?)</li>", re.S)
|
|
75
|
+
TOTAL_COUNT_PATTERN = re.compile(r"검색결과\s*<strong>(\d+)</strong>개")
|
|
76
|
+
HIDDEN_VALUE_TEMPLATE = '<input[^>]+id="{field}"[^>]+value="([^"]*)"'
|
|
77
|
+
|
|
78
|
+
KING_ACCESSION_YEARS = {
|
|
79
|
+
"태조": 1392,
|
|
80
|
+
"정종": 1399,
|
|
81
|
+
"태종": 1401,
|
|
82
|
+
"세종": 1418,
|
|
83
|
+
"문종": 1450,
|
|
84
|
+
"단종": 1452,
|
|
85
|
+
"세조": 1455,
|
|
86
|
+
"예종": 1468,
|
|
87
|
+
"성종": 1469,
|
|
88
|
+
"연산군": 1494,
|
|
89
|
+
"중종": 1506,
|
|
90
|
+
"인종": 1545,
|
|
91
|
+
"명종": 1545,
|
|
92
|
+
"선조": 1567,
|
|
93
|
+
"선조수정": 1567,
|
|
94
|
+
"광해군중초본": 1608,
|
|
95
|
+
"광해군정초본": 1608,
|
|
96
|
+
"인조": 1623,
|
|
97
|
+
"효종": 1649,
|
|
98
|
+
"현종": 1659,
|
|
99
|
+
"현종개수": 1659,
|
|
100
|
+
"숙종": 1674,
|
|
101
|
+
"숙종보궐정오": 1674,
|
|
102
|
+
"경종": 1720,
|
|
103
|
+
"경종수정": 1720,
|
|
104
|
+
"영조": 1724,
|
|
105
|
+
"정조": 1776,
|
|
106
|
+
"순조": 1800,
|
|
107
|
+
"헌종": 1834,
|
|
108
|
+
"철종": 1849,
|
|
109
|
+
"고종": 1863,
|
|
110
|
+
"순종": 1907,
|
|
111
|
+
"순종부록": 1910,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
KING_ALIASES = {
|
|
115
|
+
"태조실록": "태조",
|
|
116
|
+
"정종실록": "정종",
|
|
117
|
+
"태종실록": "태종",
|
|
118
|
+
"세종실록": "세종",
|
|
119
|
+
"문종실록": "문종",
|
|
120
|
+
"단종실록": "단종",
|
|
121
|
+
"세조실록": "세조",
|
|
122
|
+
"예종실록": "예종",
|
|
123
|
+
"성종실록": "성종",
|
|
124
|
+
"연산군일기": "연산군",
|
|
125
|
+
"중종실록": "중종",
|
|
126
|
+
"인종실록": "인종",
|
|
127
|
+
"명종실록": "명종",
|
|
128
|
+
"선조실록": "선조",
|
|
129
|
+
"선조수정실록": "선조수정",
|
|
130
|
+
"광해군중초본": "광해군중초본",
|
|
131
|
+
"광해군정초본": "광해군정초본",
|
|
132
|
+
"인조실록": "인조",
|
|
133
|
+
"효종실록": "효종",
|
|
134
|
+
"현종실록": "현종",
|
|
135
|
+
"현종개수실록": "현종개수",
|
|
136
|
+
"숙종실록": "숙종",
|
|
137
|
+
"숙종보궐정오": "숙종보궐정오",
|
|
138
|
+
"경종실록": "경종",
|
|
139
|
+
"경종수정실록": "경종수정",
|
|
140
|
+
"영조실록": "영조",
|
|
141
|
+
"정조실록": "정조",
|
|
142
|
+
"순조실록": "순조",
|
|
143
|
+
"헌종실록": "헌종",
|
|
144
|
+
"철종실록": "철종",
|
|
145
|
+
"고종실록": "고종",
|
|
146
|
+
"순종실록": "순종",
|
|
147
|
+
"순종실록부록": "순종부록",
|
|
148
|
+
"순종부록": "순종부록",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for canonical in KING_ACCESSION_YEARS:
|
|
152
|
+
KING_ALIASES.setdefault(canonical, canonical)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class SearchCategory:
|
|
157
|
+
label: str
|
|
158
|
+
count: int
|
|
159
|
+
token: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class ResultTitleMetadata:
|
|
164
|
+
king: str | None
|
|
165
|
+
regnal_year: int | None
|
|
166
|
+
gregorian_year: int | None
|
|
167
|
+
article_title: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class SearchResult:
|
|
172
|
+
article_id: str
|
|
173
|
+
url: str
|
|
174
|
+
title: str
|
|
175
|
+
article_title: str
|
|
176
|
+
summary: str
|
|
177
|
+
king: str | None
|
|
178
|
+
regnal_year: int | None
|
|
179
|
+
gregorian_year: int | None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class SearchReport:
|
|
184
|
+
query: str
|
|
185
|
+
search_type: str
|
|
186
|
+
total_results: int
|
|
187
|
+
type_count: int
|
|
188
|
+
categories: list[SearchCategory]
|
|
189
|
+
items: list[SearchResult]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True)
|
|
193
|
+
class ArticleDetail:
|
|
194
|
+
article_id: str
|
|
195
|
+
url: str
|
|
196
|
+
header: str
|
|
197
|
+
title: str
|
|
198
|
+
translated_text: str
|
|
199
|
+
original_text: str
|
|
200
|
+
classification: str | None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def clean_text(value: str | None) -> str:
|
|
204
|
+
text = COMMENT_PATTERN.sub(" ", value or "")
|
|
205
|
+
text = BR_PATTERN.sub("\n", text)
|
|
206
|
+
text = TAG_PATTERN.sub(" ", text)
|
|
207
|
+
text = html.unescape(text)
|
|
208
|
+
return WHITESPACE_PATTERN.sub(" ", text).strip()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def clean_article_text(value: str | None) -> str:
|
|
212
|
+
text = COMMENT_PATTERN.sub(" ", value or "")
|
|
213
|
+
text = BR_PATTERN.sub("\n", text)
|
|
214
|
+
text = TAG_PATTERN.sub(" ", text)
|
|
215
|
+
text = html.unescape(text)
|
|
216
|
+
lines = [WHITESPACE_PATTERN.sub(" ", line).strip() for line in text.splitlines()]
|
|
217
|
+
text = "\n".join(line for line in lines if line)
|
|
218
|
+
text = DETAIL_FOOTER_PATTERN.sub("", text).strip()
|
|
219
|
+
return WHITESPACE_PATTERN.sub(" ", text).strip()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def positive_int(raw_value: str) -> int:
|
|
223
|
+
value = int(raw_value)
|
|
224
|
+
if value <= 0:
|
|
225
|
+
raise argparse.ArgumentTypeError("must be a positive integer")
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def normalize_king_name(value: str | None) -> str | None:
|
|
230
|
+
if value is None:
|
|
231
|
+
return None
|
|
232
|
+
cleaned = clean_text(value).replace(" ", "")
|
|
233
|
+
if not cleaned:
|
|
234
|
+
return None
|
|
235
|
+
return KING_ALIASES.get(cleaned, cleaned)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_opener() -> urllib.request.OpenerDirector:
|
|
239
|
+
cookie_jar = CookieJar()
|
|
240
|
+
context = ssl.create_default_context()
|
|
241
|
+
return urllib.request.build_opener(
|
|
242
|
+
urllib.request.HTTPCookieProcessor(cookie_jar),
|
|
243
|
+
urllib.request.HTTPSHandler(context=context),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def build_http_client() -> Any:
|
|
248
|
+
return build_opener()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def should_fallback_to_opener(error: Exception) -> bool:
|
|
252
|
+
if requests is None:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
exceptions = getattr(requests, "exceptions", None)
|
|
256
|
+
http_error = getattr(exceptions, "HTTPError", None)
|
|
257
|
+
if isinstance(http_error, type) and isinstance(error, http_error):
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
request_exception = getattr(exceptions, "RequestException", None)
|
|
261
|
+
if isinstance(request_exception, type) and isinstance(error, request_exception):
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
return isinstance(error, OSError)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def fetch_text(
|
|
268
|
+
opener: Any,
|
|
269
|
+
url: str,
|
|
270
|
+
*,
|
|
271
|
+
data: dict[str, str] | None = None,
|
|
272
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
273
|
+
referer: str | None = None,
|
|
274
|
+
) -> str:
|
|
275
|
+
headers = dict(DEFAULT_HEADERS)
|
|
276
|
+
if referer is not None:
|
|
277
|
+
headers["Referer"] = referer
|
|
278
|
+
|
|
279
|
+
if requests is not None:
|
|
280
|
+
try:
|
|
281
|
+
if data is not None:
|
|
282
|
+
response = requests.post(url, data=data, timeout=timeout, headers=headers)
|
|
283
|
+
else:
|
|
284
|
+
response = requests.get(url, timeout=timeout, headers=headers)
|
|
285
|
+
response.raise_for_status()
|
|
286
|
+
return response.text
|
|
287
|
+
except Exception as error: # noqa: BLE001
|
|
288
|
+
if opener is None or not should_fallback_to_opener(error):
|
|
289
|
+
raise RuntimeError(f"Sillok request failed for {url}: {error}") from error
|
|
290
|
+
|
|
291
|
+
body = urllib.parse.urlencode(data).encode("utf-8") if data is not None else None
|
|
292
|
+
request = urllib.request.Request(url, data=body, headers=headers, method="POST" if body else "GET")
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
with opener.open(request, timeout=timeout) as response:
|
|
296
|
+
return response.read().decode("utf-8", "ignore")
|
|
297
|
+
except urllib.error.HTTPError as error: # type: ignore[attr-defined]
|
|
298
|
+
raise RuntimeError(f"Sillok request failed with HTTP {error.code} for {url}") from error
|
|
299
|
+
except urllib.error.URLError as error: # type: ignore[attr-defined]
|
|
300
|
+
raise RuntimeError(f"Sillok request failed for {url}: {error.reason}") from error
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def extract_hidden_int(html_text: str, field: str) -> int | None:
|
|
304
|
+
pattern = re.compile(HIDDEN_VALUE_TEMPLATE.format(field=re.escape(field)))
|
|
305
|
+
match = pattern.search(html_text)
|
|
306
|
+
if not match:
|
|
307
|
+
return None
|
|
308
|
+
digits = DIGITS_PATTERN.search(match.group(1))
|
|
309
|
+
return int(digits.group(0)) if digits else None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def parse_result_title_metadata(title: str) -> ResultTitleMetadata:
|
|
313
|
+
cleaned_title = clean_text(title)
|
|
314
|
+
article_title = cleaned_title.split("/", 1)[1].strip() if "/" in cleaned_title else cleaned_title
|
|
315
|
+
metadata_match = re.search(r",\s*([^,]+?)\s+(즉위년|\d+년)\b", cleaned_title)
|
|
316
|
+
|
|
317
|
+
if not metadata_match:
|
|
318
|
+
return ResultTitleMetadata(None, None, None, article_title)
|
|
319
|
+
|
|
320
|
+
king = normalize_king_name(metadata_match.group(1))
|
|
321
|
+
year_token = metadata_match.group(2)
|
|
322
|
+
regnal_year = 1 if year_token == "즉위년" else int(year_token.removesuffix("년"))
|
|
323
|
+
accession_year = KING_ACCESSION_YEARS.get(king or "")
|
|
324
|
+
if accession_year is None:
|
|
325
|
+
gregorian_year = None
|
|
326
|
+
elif year_token == "즉위년":
|
|
327
|
+
gregorian_year = accession_year
|
|
328
|
+
else:
|
|
329
|
+
gregorian_year = accession_year + regnal_year
|
|
330
|
+
|
|
331
|
+
return ResultTitleMetadata(king, regnal_year, gregorian_year, article_title)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def parse_search_results(html_text: str, *, query: str, search_type: str) -> SearchReport:
|
|
335
|
+
total_results = extract_hidden_int(html_text, "totalCount")
|
|
336
|
+
if total_results is None:
|
|
337
|
+
total_match = TOTAL_COUNT_PATTERN.search(html_text)
|
|
338
|
+
total_results = int(total_match.group(1)) if total_match else 0
|
|
339
|
+
|
|
340
|
+
count_field = {"k": "countK", "w": "countW", "m": "countM", "c": "countC"}.get(search_type, "")
|
|
341
|
+
type_count = extract_hidden_int(html_text, count_field) if count_field else None
|
|
342
|
+
if type_count is None:
|
|
343
|
+
type_count = total_results
|
|
344
|
+
|
|
345
|
+
categories: list[SearchCategory] = []
|
|
346
|
+
for token, label_html in CATEGORY_PATTERN.findall(html_text):
|
|
347
|
+
label = clean_text(label_html)
|
|
348
|
+
match = re.match(r"(.+?)\s*\((\d+)\)", label)
|
|
349
|
+
if not match:
|
|
350
|
+
continue
|
|
351
|
+
categories.append(SearchCategory(label=match.group(1), count=int(match.group(2)), token=token))
|
|
352
|
+
|
|
353
|
+
items: list[SearchResult] = []
|
|
354
|
+
for article_id, subject_html, summary_html in RESULT_PATTERN.findall(html_text):
|
|
355
|
+
title = clean_text(subject_html)
|
|
356
|
+
title = re.sub(r"^\d+\.\s*", "", title)
|
|
357
|
+
metadata = parse_result_title_metadata(title)
|
|
358
|
+
items.append(
|
|
359
|
+
SearchResult(
|
|
360
|
+
article_id=article_id,
|
|
361
|
+
url=DETAIL_URL_TEMPLATE.format(article_id=article_id),
|
|
362
|
+
title=title,
|
|
363
|
+
article_title=metadata.article_title,
|
|
364
|
+
summary=clean_text(summary_html),
|
|
365
|
+
king=metadata.king,
|
|
366
|
+
regnal_year=metadata.regnal_year,
|
|
367
|
+
gregorian_year=metadata.gregorian_year,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return SearchReport(
|
|
372
|
+
query=query,
|
|
373
|
+
search_type=search_type,
|
|
374
|
+
total_results=total_results,
|
|
375
|
+
type_count=type_count,
|
|
376
|
+
categories=categories,
|
|
377
|
+
items=items,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def filter_results(
|
|
382
|
+
items: Iterable[SearchResult],
|
|
383
|
+
*,
|
|
384
|
+
king: str | None = None,
|
|
385
|
+
year: int | None = None,
|
|
386
|
+
) -> list[SearchResult]:
|
|
387
|
+
normalized_king = normalize_king_name(king)
|
|
388
|
+
filtered: list[SearchResult] = []
|
|
389
|
+
|
|
390
|
+
for item in items:
|
|
391
|
+
if normalized_king is not None and item.king != normalized_king:
|
|
392
|
+
continue
|
|
393
|
+
if year is not None and item.gregorian_year != year:
|
|
394
|
+
continue
|
|
395
|
+
filtered.append(item)
|
|
396
|
+
|
|
397
|
+
return filtered
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def parse_detail_page(html_text: str, *, article_id: str) -> ArticleDetail:
|
|
401
|
+
title_head = TITLE_HEAD_PATTERN.search(html_text)
|
|
402
|
+
if not title_head:
|
|
403
|
+
raise ValueError("Unable to find the article header on the detail page.")
|
|
404
|
+
|
|
405
|
+
translated_match = LEFT_VIEW_PATTERN.search(html_text)
|
|
406
|
+
original_match = RIGHT_VIEW_PATTERN.search(html_text)
|
|
407
|
+
if not translated_match or not original_match:
|
|
408
|
+
raise ValueError("Unable to find translated/original article text on the detail page.")
|
|
409
|
+
|
|
410
|
+
classification_match = CLASSIFICATION_PATTERN.search(html_text)
|
|
411
|
+
|
|
412
|
+
return ArticleDetail(
|
|
413
|
+
article_id=article_id,
|
|
414
|
+
url=DETAIL_URL_TEMPLATE.format(article_id=article_id),
|
|
415
|
+
header=clean_text(title_head.group(1)),
|
|
416
|
+
title=clean_text(title_head.group(2)),
|
|
417
|
+
translated_text=clean_article_text(translated_match.group(1)),
|
|
418
|
+
original_text=clean_article_text(original_match.group(1)),
|
|
419
|
+
classification=clean_text(classification_match.group(1)) if classification_match else None,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def build_search_payload(*, query: str, search_type: str, page_index: int) -> dict[str, str]:
|
|
424
|
+
return {
|
|
425
|
+
"topSearchWord": query,
|
|
426
|
+
"pageIndex": str(page_index),
|
|
427
|
+
"initPageUnit": "0",
|
|
428
|
+
"type": search_type,
|
|
429
|
+
"sillokType": "S",
|
|
430
|
+
"topSearchWord_ime": f'<span class="newbatang">{html.escape(query)}</span>',
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def fetch_search_page(
|
|
435
|
+
opener: urllib.request.OpenerDirector,
|
|
436
|
+
*,
|
|
437
|
+
query: str,
|
|
438
|
+
search_type: str,
|
|
439
|
+
page_index: int,
|
|
440
|
+
timeout: int,
|
|
441
|
+
) -> SearchReport:
|
|
442
|
+
payload = build_search_payload(query=query, search_type=search_type, page_index=page_index)
|
|
443
|
+
html_text = fetch_text(opener, SEARCH_URL, data=payload, timeout=timeout, referer=f"{BASE_URL}/main/main.do")
|
|
444
|
+
return parse_search_results(html_text, query=query, search_type=search_type)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def fetch_detail_page(
|
|
448
|
+
opener: urllib.request.OpenerDirector,
|
|
449
|
+
*,
|
|
450
|
+
article_id: str,
|
|
451
|
+
timeout: int,
|
|
452
|
+
) -> ArticleDetail:
|
|
453
|
+
url = DETAIL_URL_TEMPLATE.format(article_id=article_id)
|
|
454
|
+
html_text = fetch_text(opener, url, timeout=timeout, referer=SEARCH_URL)
|
|
455
|
+
return parse_detail_page(html_text, article_id=article_id)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def search_sillok(
|
|
459
|
+
query: str,
|
|
460
|
+
*,
|
|
461
|
+
king: str | None = None,
|
|
462
|
+
year: int | None = None,
|
|
463
|
+
limit: int = DEFAULT_LIMIT,
|
|
464
|
+
search_type: str = "k",
|
|
465
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
466
|
+
) -> dict:
|
|
467
|
+
opener = build_http_client()
|
|
468
|
+
reports: list[SearchReport] = []
|
|
469
|
+
filtered_results: list[SearchResult] = []
|
|
470
|
+
|
|
471
|
+
page_index = 1
|
|
472
|
+
total_pages = 1
|
|
473
|
+
while page_index <= total_pages and page_index <= MAX_PAGES:
|
|
474
|
+
report = fetch_search_page(opener, query=query, search_type=search_type, page_index=page_index, timeout=timeout)
|
|
475
|
+
reports.append(report)
|
|
476
|
+
page_filtered = filter_results(report.items, king=king, year=year)
|
|
477
|
+
filtered_results.extend(page_filtered)
|
|
478
|
+
|
|
479
|
+
if page_index == 1:
|
|
480
|
+
page_size = len(report.items) or 1
|
|
481
|
+
total_pages = max(1, math.ceil((report.type_count or report.total_results or 0) / page_size))
|
|
482
|
+
if len(filtered_results) >= limit or not report.items:
|
|
483
|
+
break
|
|
484
|
+
page_index += 1
|
|
485
|
+
|
|
486
|
+
first_report = reports[0] if reports else SearchReport(query, search_type, 0, 0, [], [])
|
|
487
|
+
limited_results = filtered_results[:limit]
|
|
488
|
+
details = [fetch_detail_page(opener, article_id=item.article_id, timeout=timeout) for item in limited_results]
|
|
489
|
+
detail_map = {detail.article_id: detail for detail in details}
|
|
490
|
+
|
|
491
|
+
serialized_items = []
|
|
492
|
+
for item in limited_results:
|
|
493
|
+
detail = detail_map.get(item.article_id)
|
|
494
|
+
serialized_items.append(
|
|
495
|
+
{
|
|
496
|
+
**asdict(item),
|
|
497
|
+
"detail": asdict(detail) if detail else None,
|
|
498
|
+
"excerpt": detail.translated_text[:280] if detail and detail.translated_text else item.summary[:280],
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
"query": query,
|
|
504
|
+
"type": search_type,
|
|
505
|
+
"filters": {"king": normalize_king_name(king), "year": year, "limit": limit},
|
|
506
|
+
"total_results": first_report.total_results,
|
|
507
|
+
"type_count": first_report.type_count,
|
|
508
|
+
"returned_count": len(serialized_items),
|
|
509
|
+
"categories": [asdict(category) for category in first_report.categories],
|
|
510
|
+
"items": serialized_items,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
515
|
+
parser = argparse.ArgumentParser(description="Search Joseon Sillok records from sillok.history.go.kr")
|
|
516
|
+
parser.add_argument("--query", required=True, help="Search keyword to send to the Joseon Sillok site")
|
|
517
|
+
parser.add_argument("--king", help="Optional king filter, e.g. 세종 or 세종실록")
|
|
518
|
+
parser.add_argument("--year", type=positive_int, help="Optional Gregorian year filter, e.g. 1443")
|
|
519
|
+
parser.add_argument("--limit", type=positive_int, default=DEFAULT_LIMIT, help="Number of results to return")
|
|
520
|
+
parser.add_argument(
|
|
521
|
+
"--type",
|
|
522
|
+
dest="search_type",
|
|
523
|
+
choices=["k", "w"],
|
|
524
|
+
default="k",
|
|
525
|
+
help="Search translated text (k) or original text (w)",
|
|
526
|
+
)
|
|
527
|
+
parser.add_argument("--timeout", type=positive_int, default=DEFAULT_TIMEOUT, help="HTTP timeout in seconds")
|
|
528
|
+
return parser.parse_args(argv)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def main(argv: list[str] | None = None) -> int:
|
|
532
|
+
args = parse_args(argv)
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
report = search_sillok(
|
|
536
|
+
args.query,
|
|
537
|
+
king=args.king,
|
|
538
|
+
year=args.year,
|
|
539
|
+
limit=args.limit,
|
|
540
|
+
search_type=args.search_type,
|
|
541
|
+
timeout=args.timeout,
|
|
542
|
+
)
|
|
543
|
+
except Exception as error: # noqa: BLE001
|
|
544
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
545
|
+
return 1
|
|
546
|
+
|
|
547
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
548
|
+
return 0
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
if __name__ == "__main__":
|
|
552
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "joseon-sillok-search",
|
|
3
|
+
"description": "Search Joseon Dynasty Annals records from the official sillok.history.go.kr site with keyword search plus optional king/year narrowing.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"lookup"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: joseon-sillok-search\ndescription: Search Joseon Dynasty Annals records from the official sillok.history.go.kr site with keyword search plus optional king/year narrowing.\nlicense: MIT\nmetadata:\n category: history\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|