@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,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only Danawa search/price comparison helper for Hermes.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python scripts/danawa_search.py search "에어팟 프로 2세대" --limit 8
|
|
6
|
+
python scripts/danawa_search.py offers 28208783 --limit 10
|
|
7
|
+
python scripts/danawa_search.py compare "에어팟 프로 2세대" --limit 5 --offers 5
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from html import unescape
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from bs4 import BeautifulSoup
|
|
23
|
+
except ImportError as exc: # pragma: no cover - environment guard
|
|
24
|
+
raise SystemExit("beautifulsoup4 is required: python -m pip install beautifulsoup4") from exc
|
|
25
|
+
|
|
26
|
+
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/121 Safari/537.36"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fetch(url: str, *, method: str = "GET", data: Optional[dict] = None, referer: Optional[str] = None) -> str:
|
|
30
|
+
headers = {
|
|
31
|
+
"User-Agent": UA,
|
|
32
|
+
"Accept-Language": "ko-KR,ko;q=0.9,en;q=0.8",
|
|
33
|
+
}
|
|
34
|
+
body = None
|
|
35
|
+
if data is not None:
|
|
36
|
+
body = urllib.parse.urlencode(data).encode("utf-8")
|
|
37
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
|
38
|
+
headers["X-Requested-With"] = "XMLHttpRequest"
|
|
39
|
+
if referer:
|
|
40
|
+
headers["Referer"] = referer
|
|
41
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
42
|
+
with urllib.request.urlopen(req, timeout=25) as resp:
|
|
43
|
+
return resp.read().decode("utf-8", "replace")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def soup_for(html: str) -> BeautifulSoup:
|
|
47
|
+
return BeautifulSoup(html, "html.parser")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clean_text(s: Optional[str]) -> Optional[str]:
|
|
51
|
+
if s is None:
|
|
52
|
+
return None
|
|
53
|
+
return " ".join(unescape(s).split())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_int(s: Optional[str]) -> Optional[int]:
|
|
57
|
+
if not s:
|
|
58
|
+
return None
|
|
59
|
+
digits = re.sub(r"\D", "", s)
|
|
60
|
+
return int(digits) if digits else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def abs_url(url: Optional[str]) -> Optional[str]:
|
|
64
|
+
if not url:
|
|
65
|
+
return None
|
|
66
|
+
if url.startswith("//"):
|
|
67
|
+
return "https:" + url
|
|
68
|
+
if url.startswith("/"):
|
|
69
|
+
return "https://prod.danawa.com" + url
|
|
70
|
+
return url
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def search(query: str, limit: int = 10) -> Dict[str, Any]:
|
|
74
|
+
url = "https://search.danawa.com/dsearch.php?query=" + urllib.parse.quote(query)
|
|
75
|
+
html = fetch(url)
|
|
76
|
+
soup = soup_for(html)
|
|
77
|
+
items: List[Dict[str, Any]] = []
|
|
78
|
+
for li in soup.select("li.prod_item"):
|
|
79
|
+
pid = (li.get("id") or "").replace("productItem", "") or None
|
|
80
|
+
name_el = li.select_one(".prod_name a") or li.select_one("p.prod_name a") or li.select_one('a[name="productName"]')
|
|
81
|
+
if not name_el:
|
|
82
|
+
continue
|
|
83
|
+
name = clean_text(name_el.get_text(" ", strip=True))
|
|
84
|
+
link = abs_url(name_el.get("href"))
|
|
85
|
+
min_input = li.select_one(f"#min_price_{pid}") if pid else None
|
|
86
|
+
price = parse_int(min_input.get("value") if min_input else None)
|
|
87
|
+
if price is None:
|
|
88
|
+
price_el = li.select_one(".price_sect strong") or li.select_one(".prod_pricelist strong")
|
|
89
|
+
price = parse_int(price_el.get_text() if price_el else None)
|
|
90
|
+
img = li.select_one(".thumb_image img")
|
|
91
|
+
image = abs_url((img.get("data-original") or img.get("src")) if img else None)
|
|
92
|
+
mall_el = li.select_one(".prod_pricelist .memory_sect") or li.select_one(".meta_item")
|
|
93
|
+
spec = " / ".join(clean_text(e.get_text(" ", strip=True)) or "" for e in li.select(".spec_list a, .spec_list span")[:10])
|
|
94
|
+
items.append(
|
|
95
|
+
{
|
|
96
|
+
"pcode": pid,
|
|
97
|
+
"title": name,
|
|
98
|
+
"price": price,
|
|
99
|
+
"price_text": f"{price:,}원" if price else None,
|
|
100
|
+
"mall_text": clean_text(mall_el.get_text(" ", strip=True)) if mall_el else None,
|
|
101
|
+
"url": link,
|
|
102
|
+
"image_url": image,
|
|
103
|
+
"spec": spec[:300] if spec else None,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
if len(items) >= limit:
|
|
107
|
+
break
|
|
108
|
+
return {"query": query, "source_url": url, "count": len(items), "items": items, "meta": {"extraction": "danawa-search-html", "ts": int(time.time())}}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def js_value(html: str, key: str) -> str:
|
|
112
|
+
patterns = [
|
|
113
|
+
rf"{re.escape(key)}\s*:\s*\"([^\"]*)\"",
|
|
114
|
+
rf"{re.escape(key)}\s*:\s*'([^']*)'",
|
|
115
|
+
rf"{re.escape(key)}\s*:\s*([0-9]+)",
|
|
116
|
+
]
|
|
117
|
+
for pat in patterns:
|
|
118
|
+
m = re.search(pat, html)
|
|
119
|
+
if m:
|
|
120
|
+
raw = m.group(1)
|
|
121
|
+
if "\\u" in raw or "\\/" in raw:
|
|
122
|
+
try:
|
|
123
|
+
return json.loads('"' + raw.replace('"', '\\"') + '"')
|
|
124
|
+
except Exception:
|
|
125
|
+
return raw.replace("\\/", "/")
|
|
126
|
+
return raw
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def product_meta(pcode: str) -> Dict[str, str]:
|
|
131
|
+
url = f"https://prod.danawa.com/info/?pcode={urllib.parse.quote(str(pcode))}"
|
|
132
|
+
html = fetch(url)
|
|
133
|
+
meta = {
|
|
134
|
+
"pcode": str(pcode),
|
|
135
|
+
"source_url": url,
|
|
136
|
+
"cate1": js_value(html, "nCategoryCode1"),
|
|
137
|
+
"cate2": js_value(html, "nCategoryCode2"),
|
|
138
|
+
"cate3": js_value(html, "nCategoryCode3"),
|
|
139
|
+
"cate4": js_value(html, "nCategoryCode4") or "0",
|
|
140
|
+
"UICategoryCode": js_value(html, "nCategoryCode"),
|
|
141
|
+
"powerLinkKeyword": js_value(html, "powerLinkKeyword"),
|
|
142
|
+
"minPrice": js_value(html, "nMinPrice"),
|
|
143
|
+
"keyword": js_value(html, "sKeyword"),
|
|
144
|
+
"NaPm": js_value(html, "sNaPm"),
|
|
145
|
+
"sProductFullName": js_value(html, "sProductName"),
|
|
146
|
+
"makerCode": js_value(html, "makerCode"),
|
|
147
|
+
"makerName": js_value(html, "makerName"),
|
|
148
|
+
}
|
|
149
|
+
title = soup_for(html).select_one(".prod_tit .title")
|
|
150
|
+
if title:
|
|
151
|
+
meta["sProductFullName"] = clean_text(title.get_text(" ", strip=True)) or meta["sProductFullName"]
|
|
152
|
+
return meta
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def offers(pcode: str, limit: int = 20, include_shipping: bool = False) -> Dict[str, Any]:
|
|
156
|
+
meta = product_meta(pcode)
|
|
157
|
+
post_price = "Y" if include_shipping else "N"
|
|
158
|
+
data = {
|
|
159
|
+
"pcode": meta["pcode"],
|
|
160
|
+
"cate1": meta.get("cate1", ""),
|
|
161
|
+
"cate2": meta.get("cate2", ""),
|
|
162
|
+
"cate3": meta.get("cate3", ""),
|
|
163
|
+
"cate4": meta.get("cate4", "0"),
|
|
164
|
+
"UICategoryCode": meta.get("UICategoryCode", "0"),
|
|
165
|
+
"powerLinkKeyword": meta.get("powerLinkKeyword", ""),
|
|
166
|
+
"minPrice": meta.get("minPrice", ""),
|
|
167
|
+
"keyword": meta.get("keyword", ""),
|
|
168
|
+
"NaPm": meta.get("NaPm", ""),
|
|
169
|
+
"bDeliveryLeftRightYN": "N",
|
|
170
|
+
"bQuickPostSortYN": "N",
|
|
171
|
+
"sSortType": "minPrice",
|
|
172
|
+
"sProductFullName": meta.get("sProductFullName", ""),
|
|
173
|
+
"bPostPriceYN": post_price,
|
|
174
|
+
"bBadgeDefaultYN": "N",
|
|
175
|
+
"bWarrantyDefaultYN": "N",
|
|
176
|
+
"nOpenMarketMoreCount": "30",
|
|
177
|
+
"nAffiliateMoreCount": "30",
|
|
178
|
+
"nOverseasShoppingMoreCount": "30",
|
|
179
|
+
"nGeneralAffiliateMoreCount": "3",
|
|
180
|
+
"sRelationMenuType": "",
|
|
181
|
+
"sRelationType": "",
|
|
182
|
+
"bCoupangSortYN": "N",
|
|
183
|
+
"makerCode": meta.get("makerCode", ""),
|
|
184
|
+
"makerName": meta.get("makerName", ""),
|
|
185
|
+
}
|
|
186
|
+
html = fetch("https://prod.danawa.com/info/ajax/getAllPriceCompareMallList.ajax.php", method="POST", data=data, referer=meta["source_url"])
|
|
187
|
+
soup = soup_for(html)
|
|
188
|
+
rows: List[Dict[str, Any]] = []
|
|
189
|
+
for div in soup.select(".diff_item"):
|
|
190
|
+
mall_img = div.select_one(".d_mall img")
|
|
191
|
+
mall = mall_img.get("alt") if mall_img else None
|
|
192
|
+
price_el = div.select_one("em.prc_c") or div.select_one("em.prc_t")
|
|
193
|
+
price = parse_int(price_el.get_text() if price_el else None)
|
|
194
|
+
if not mall or price is None:
|
|
195
|
+
continue
|
|
196
|
+
ship_el = div.select_one(".ship") or div.select_one(".stxt")
|
|
197
|
+
shipping = clean_text(ship_el.get_text(" ", strip=True)) if ship_el else None
|
|
198
|
+
shipping_fee = 0 if shipping and "무료" in shipping else parse_int(shipping)
|
|
199
|
+
card_line = div.select_one(".card_line")
|
|
200
|
+
card_price_el = card_line.select_one(".card_prc") if card_line else None
|
|
201
|
+
card_name_el = card_line.select_one(".txt") if card_line else None
|
|
202
|
+
card_price = parse_int(card_price_el.get_text() if card_price_el else None)
|
|
203
|
+
installment_el = div.select_one(".btn_foi .txt")
|
|
204
|
+
installment_detail_el = div.select_one(".foi_layer .ly_cont")
|
|
205
|
+
link = div.select_one("a.priceCompareBuyLink")
|
|
206
|
+
# 결제조건 ico만 캡처. 다른 ico(빠른배송, 안내, 상품리뷰 등)는 노이즈라 제외.
|
|
207
|
+
# 클래스만 있고 텍스트가 비어 있는 아이콘도 row 라벨이 누락되지 않도록
|
|
208
|
+
# 같은 정규화 테이블에서 표시 라벨/타입/boolean 필드를 모두 파생한다.
|
|
209
|
+
payment_condition_labels = {
|
|
210
|
+
"cash": "현금",
|
|
211
|
+
"point": "포인트",
|
|
212
|
+
"coupon": "쿠폰",
|
|
213
|
+
"card": "카드",
|
|
214
|
+
"discount": "할인",
|
|
215
|
+
"membership": "멤버십",
|
|
216
|
+
}
|
|
217
|
+
payment_condition_types: List[str] = []
|
|
218
|
+
payment_badges: List[str] = []
|
|
219
|
+
for el in div.select(".prc_line .ico, .d_dsc .ico"):
|
|
220
|
+
classes = set(el.get("class") or [])
|
|
221
|
+
text = clean_text(el.get_text(" ", strip=True)) or ""
|
|
222
|
+
matched_types = [
|
|
223
|
+
kind
|
|
224
|
+
for kind, label in payment_condition_labels.items()
|
|
225
|
+
if kind in classes or label in text
|
|
226
|
+
]
|
|
227
|
+
if not matched_types:
|
|
228
|
+
continue
|
|
229
|
+
for kind in matched_types:
|
|
230
|
+
if kind not in payment_condition_types:
|
|
231
|
+
payment_condition_types.append(kind)
|
|
232
|
+
label = payment_condition_labels[kind]
|
|
233
|
+
if label not in payment_badges:
|
|
234
|
+
payment_badges.append(label)
|
|
235
|
+
cash_only = "cash" in payment_condition_types
|
|
236
|
+
point_only = "point" in payment_condition_types
|
|
237
|
+
coupon_only = "coupon" in payment_condition_types
|
|
238
|
+
card_only_badge = "card" in payment_condition_types
|
|
239
|
+
discount_badge = "discount" in payment_condition_types
|
|
240
|
+
membership_badge = "membership" in payment_condition_types
|
|
241
|
+
payment_condition_label = ", ".join(payment_badges) or None
|
|
242
|
+
is_conditional_price = bool(payment_condition_types)
|
|
243
|
+
rows.append(
|
|
244
|
+
{
|
|
245
|
+
"mall": clean_text(mall),
|
|
246
|
+
"price": price,
|
|
247
|
+
"price_text": f"{price:,}원",
|
|
248
|
+
"shipping": shipping,
|
|
249
|
+
"is_free_shipping": bool(shipping and "무료" in shipping),
|
|
250
|
+
"shipping_fee": shipping_fee,
|
|
251
|
+
"total_price": price + (shipping_fee or 0),
|
|
252
|
+
"total_price_text": f"{price + (shipping_fee or 0):,}원",
|
|
253
|
+
"card_price": card_price,
|
|
254
|
+
"card_price_text": f"{card_price:,}원" if card_price else None,
|
|
255
|
+
"card_name": clean_text(card_name_el.get_text(" ", strip=True)) if card_name_el else None,
|
|
256
|
+
"card_discount": (price - card_price) if card_price else None,
|
|
257
|
+
"card_discount_text": f"{price - card_price:,}원" if card_price else None,
|
|
258
|
+
"installment": clean_text(installment_el.get_text(" ", strip=True)) if installment_el else None,
|
|
259
|
+
"installment_detail": clean_text(installment_detail_el.get_text(" ", strip=True)) if installment_detail_el else None,
|
|
260
|
+
"payment_badges": payment_badges,
|
|
261
|
+
"cash_only": cash_only,
|
|
262
|
+
"point_only": point_only,
|
|
263
|
+
"coupon_only": coupon_only,
|
|
264
|
+
"card_only_badge": card_only_badge,
|
|
265
|
+
"discount_badge": discount_badge,
|
|
266
|
+
"membership_badge": membership_badge,
|
|
267
|
+
"payment_condition_types": payment_condition_types,
|
|
268
|
+
"payment_condition_label": payment_condition_label,
|
|
269
|
+
"is_conditional_price": is_conditional_price,
|
|
270
|
+
"url": abs_url(link.get("href") if link else None),
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
# 정렬은 단순히 배송비 포함 실구매가 오름차순. 결제조건(현금/쿠폰/포인트/특정카드)은
|
|
274
|
+
# 분리 그룹으로 묶지 않고 row 단위로 payment_badges / payment_condition_types /
|
|
275
|
+
# payment_condition_label 및 세부 boolean 플래그로 노출한다. 호출자(또는 사용자)는 자기 결제수단에 맞춰 판단한다.
|
|
276
|
+
rows.sort(key=lambda row: (
|
|
277
|
+
row["total_price"] is None,
|
|
278
|
+
row["total_price"] or row["price"],
|
|
279
|
+
row["price"],
|
|
280
|
+
row["mall"] or "",
|
|
281
|
+
))
|
|
282
|
+
rows = rows[:limit]
|
|
283
|
+
return {
|
|
284
|
+
"pcode": str(pcode),
|
|
285
|
+
"title": meta.get("sProductFullName"),
|
|
286
|
+
"source_url": meta["source_url"],
|
|
287
|
+
"count": len(rows),
|
|
288
|
+
"normal_count": sum(1 for r in rows if not r.get("is_conditional_price")),
|
|
289
|
+
"conditional_count": sum(1 for r in rows if r.get("is_conditional_price")),
|
|
290
|
+
"offers": rows,
|
|
291
|
+
"meta": {
|
|
292
|
+
"extraction": "danawa-price-ajax",
|
|
293
|
+
"include_shipping": include_shipping,
|
|
294
|
+
"sort": "total_price",
|
|
295
|
+
"ts": int(time.time()),
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def compare(query: str, limit: int, offer_limit: int) -> Dict[str, Any]:
|
|
301
|
+
result = search(query, limit=limit)
|
|
302
|
+
enriched = []
|
|
303
|
+
for item in result["items"]:
|
|
304
|
+
row = dict(item)
|
|
305
|
+
if item.get("pcode"):
|
|
306
|
+
try:
|
|
307
|
+
off = offers(item["pcode"], limit=offer_limit)
|
|
308
|
+
row["offers"] = off.get("offers", [])
|
|
309
|
+
except Exception as exc: # keep search result usable if a detail call fails
|
|
310
|
+
row["offers_error"] = f"{type(exc).__name__}: {exc}"
|
|
311
|
+
enriched.append(row)
|
|
312
|
+
result["items"] = enriched
|
|
313
|
+
result["meta"]["detail_extraction"] = "best-effort"
|
|
314
|
+
return result
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def positive_int(raw: str) -> int:
|
|
318
|
+
value = int(raw)
|
|
319
|
+
if value < 1:
|
|
320
|
+
raise argparse.ArgumentTypeError("must be >= 1")
|
|
321
|
+
return value
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def main() -> int:
|
|
325
|
+
ap = argparse.ArgumentParser()
|
|
326
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
327
|
+
s = sub.add_parser("search")
|
|
328
|
+
s.add_argument("query")
|
|
329
|
+
s.add_argument("--limit", type=positive_int, default=10)
|
|
330
|
+
o = sub.add_parser("offers")
|
|
331
|
+
o.add_argument("pcode")
|
|
332
|
+
o.add_argument("--limit", type=positive_int, default=20)
|
|
333
|
+
o.add_argument("--include-shipping", action="store_true")
|
|
334
|
+
c = sub.add_parser("compare")
|
|
335
|
+
c.add_argument("query")
|
|
336
|
+
c.add_argument("--limit", type=positive_int, default=5)
|
|
337
|
+
c.add_argument("--offers", type=positive_int, default=5)
|
|
338
|
+
args = ap.parse_args()
|
|
339
|
+
try:
|
|
340
|
+
if args.cmd == "search":
|
|
341
|
+
out = search(args.query, args.limit)
|
|
342
|
+
elif args.cmd == "offers":
|
|
343
|
+
out = offers(args.pcode, args.limit, args.include_shipping)
|
|
344
|
+
else:
|
|
345
|
+
out = compare(args.query, args.limit, args.offers)
|
|
346
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
347
|
+
return 0
|
|
348
|
+
except Exception as exc:
|
|
349
|
+
print(json.dumps({"error": f"{type(exc).__name__}: {exc}"}, ensure_ascii=False), file=sys.stderr)
|
|
350
|
+
return 2
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "danawa-price-search",
|
|
3
|
+
"description": "다나와 공개 검색/가격비교 표면으로 상품 후보를 찾고, 쇼핑몰별 최저가·배송비 포함 실구매가·카드 할인가·무이자 할부 정보를 보수적으로 비교한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"action:commerce"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: danawa-price-search\ndescription: 다나와 공개 검색/가격비교 표면으로 상품 후보를 찾고, 쇼핑몰별 최저가·배송비 포함 실구매가·카드 할인가·무이자 할부 정보를 보수적으로 비교한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|