@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,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import re
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
from jobkorea_talent_models import BASE_URL, Candidate
|
|
8
|
+
|
|
9
|
+
ACTION_CONTROL_RE = re.compile(
|
|
10
|
+
r"^(?:스크랩\s*\d*|저장하기|닫기|포지션\s*제안|메모하기|프로필\s*확인|이력서\s*확인|펼쳐보기|접기|이전|다음)$"
|
|
11
|
+
)
|
|
12
|
+
ACTION_CONTROL_INLINE_RE = re.compile(
|
|
13
|
+
r"(?:스크랩\s*\d+|저장하기|닫기|포지션\s*제안|메모하기|프로필\s*확인|이력서\s*확인|펼쳐보기|접기|이전|다음)"
|
|
14
|
+
)
|
|
15
|
+
RESUME_LINK_RE = re.compile(r'href="(?P<href>/corp/person/find/resume/view\?rNo=(?P<rno>\d+))"')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def clean_text(value: str) -> str:
|
|
19
|
+
value = html.unescape(value)
|
|
20
|
+
value = re.sub(r"<script[\s\S]*?</script>", " ", value, flags=re.I)
|
|
21
|
+
value = re.sub(r"<style[\s\S]*?</style>", " ", value, flags=re.I)
|
|
22
|
+
value = re.sub(r"<[^>]+>", " ", value)
|
|
23
|
+
value = re.sub(r"[ \t\r\f\v]+", " ", value)
|
|
24
|
+
value = re.sub(r"\n\s*\n+", "\n", value)
|
|
25
|
+
return value.strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_action_control_label(value: str) -> bool:
|
|
29
|
+
label = re.sub(r"\s+", " ", html.unescape(value)).strip()
|
|
30
|
+
return bool(label and ACTION_CONTROL_RE.match(label))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def filter_action_control_text(value: str) -> str:
|
|
34
|
+
lines = []
|
|
35
|
+
for line in value.splitlines():
|
|
36
|
+
label = line.strip()
|
|
37
|
+
if not label or is_action_control_label(label):
|
|
38
|
+
continue
|
|
39
|
+
label = ACTION_CONTROL_INLINE_RE.sub(" ", label)
|
|
40
|
+
label = re.sub(r"\s+", " ", label).strip()
|
|
41
|
+
if label:
|
|
42
|
+
lines.append(label)
|
|
43
|
+
return "\n".join(lines).strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def row_contains_other_resume(candidate_markup: str, rno: str) -> bool:
|
|
47
|
+
refs: list[str] = []
|
|
48
|
+
for href_rno, data_rno in re.findall(r"rNo=(\d+)|data-rno=[\"'](\d+)[\"']", candidate_markup):
|
|
49
|
+
refs.append(href_rno or data_rno)
|
|
50
|
+
return any(ref != rno for ref in refs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_regex_candidate_markup(markup: str, match: re.Match[str], rno: str) -> str:
|
|
54
|
+
row_start = markup.rfind("<tr", 0, match.start())
|
|
55
|
+
if row_start >= 0:
|
|
56
|
+
row_open_end = markup.find(">", row_start, match.start())
|
|
57
|
+
row_end = markup.find("</tr>", match.end())
|
|
58
|
+
row_open = markup[row_start : row_open_end + 1] if row_open_end >= 0 else ""
|
|
59
|
+
if row_end >= 0 and f'data-rno="{rno}"' in row_open:
|
|
60
|
+
return markup[row_start : row_end + len("</tr>")]
|
|
61
|
+
|
|
62
|
+
booth_start = markup.rfind('<div class="booth"', 0, match.start())
|
|
63
|
+
if booth_start >= 0:
|
|
64
|
+
next_booth = markup.find('<div class="booth"', match.end())
|
|
65
|
+
section_end = markup.find("</section>", match.end())
|
|
66
|
+
end_candidates = [pos for pos in (next_booth, section_end) if pos >= 0]
|
|
67
|
+
booth_end = min(end_candidates) if end_candidates else min(len(markup), match.end() + 2500)
|
|
68
|
+
booth = markup[booth_start:booth_end]
|
|
69
|
+
if not row_contains_other_resume(booth, rno):
|
|
70
|
+
return booth
|
|
71
|
+
|
|
72
|
+
start = max(0, match.start() - 300)
|
|
73
|
+
end = min(len(markup), match.end() + 1200)
|
|
74
|
+
return markup[start:end]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_with_bs4(markup: str, limit: int) -> list[Candidate] | None:
|
|
78
|
+
try:
|
|
79
|
+
from bs4 import BeautifulSoup
|
|
80
|
+
except ImportError:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
soup = BeautifulSoup(markup, "html.parser")
|
|
84
|
+
candidates: list[Candidate] = []
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
|
|
87
|
+
for link in soup.select('a[href*="/corp/person/find/resume/view?rNo="]'):
|
|
88
|
+
raw_href = link.get("href", "")
|
|
89
|
+
href = raw_href if isinstance(raw_href, str) else ""
|
|
90
|
+
matched_rno = re.search(r"rNo=(\d+)", href)
|
|
91
|
+
if not matched_rno:
|
|
92
|
+
continue
|
|
93
|
+
rno = matched_rno.group(1)
|
|
94
|
+
if rno in seen:
|
|
95
|
+
continue
|
|
96
|
+
seen.add(rno)
|
|
97
|
+
|
|
98
|
+
container = (
|
|
99
|
+
link.find_parent("tr", attrs={"data-rno": rno})
|
|
100
|
+
or link.find_parent(class_=re.compile(r"(^|\s)booth(\s|$)", re.I))
|
|
101
|
+
or link.parent
|
|
102
|
+
)
|
|
103
|
+
if container and row_contains_other_resume(str(container), rno):
|
|
104
|
+
container = link.parent
|
|
105
|
+
|
|
106
|
+
raw = clean_text(str(container)) if container else clean_text(str(link))
|
|
107
|
+
texts = []
|
|
108
|
+
for node in container.find_all(["dt", "dd", "p", "span", "li"]) if container else []:
|
|
109
|
+
label = node.get_text(" ", strip=True)
|
|
110
|
+
if label and not is_action_control_label(label):
|
|
111
|
+
texts.append(label)
|
|
112
|
+
for btn in container.select(".keywordSkill button, .keywordBox button") if container else []:
|
|
113
|
+
label = btn.get_text(" ", strip=True)
|
|
114
|
+
if label and not is_action_control_label(label):
|
|
115
|
+
texts.append(label)
|
|
116
|
+
text_join = " | ".join(dict.fromkeys(texts))
|
|
117
|
+
|
|
118
|
+
name_scope = container.select_one(".nameAge") if container else None
|
|
119
|
+
dt = (name_scope or container).find("dt") if container else None
|
|
120
|
+
name = dt.get_text(" ", strip=True) if dt else ""
|
|
121
|
+
dd = dt.find_next("dd") if dt else None
|
|
122
|
+
meta = dd.get_text(" ", strip=True) if dd else ""
|
|
123
|
+
if not name:
|
|
124
|
+
m_name = re.search(r"([가-힣A-Za-z]OO)\s*\(([^)]*)\)", raw)
|
|
125
|
+
if m_name:
|
|
126
|
+
name = m_name.group(1)
|
|
127
|
+
meta = "(" + m_name.group(2) + ")"
|
|
128
|
+
|
|
129
|
+
skills = []
|
|
130
|
+
for btn in container.select(".keywordSkill button, .keywordBox button") if container else []:
|
|
131
|
+
label = btn.get_text(" ", strip=True)
|
|
132
|
+
if label and not is_action_control_label(label):
|
|
133
|
+
skills.append(label)
|
|
134
|
+
|
|
135
|
+
career_node = container.select_one(".career") if container else None
|
|
136
|
+
candidates.append(
|
|
137
|
+
Candidate(
|
|
138
|
+
rno=rno,
|
|
139
|
+
url=urllib.parse.urljoin(BASE_URL, href),
|
|
140
|
+
name=name,
|
|
141
|
+
meta=meta,
|
|
142
|
+
career=career_node.get_text(" ", strip=True) if career_node else "",
|
|
143
|
+
skills=", ".join(skills[:25]),
|
|
144
|
+
raw_summary=filter_action_control_text(text_join[:1000] or raw[:1000]),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
if len(candidates) >= limit:
|
|
148
|
+
break
|
|
149
|
+
return candidates
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_with_regex(markup: str, limit: int) -> list[Candidate]:
|
|
153
|
+
candidates: list[Candidate] = []
|
|
154
|
+
seen: set[str] = set()
|
|
155
|
+
for match in RESUME_LINK_RE.finditer(markup):
|
|
156
|
+
rno = match.group("rno")
|
|
157
|
+
if rno in seen:
|
|
158
|
+
continue
|
|
159
|
+
seen.add(rno)
|
|
160
|
+
raw_markup = extract_regex_candidate_markup(markup, match, rno)
|
|
161
|
+
raw = clean_text(raw_markup)
|
|
162
|
+
name = ""
|
|
163
|
+
meta = ""
|
|
164
|
+
name_match = re.search(r"([가-힣A-Za-z]OO)\s*\(([^)]*)\)", raw)
|
|
165
|
+
if name_match:
|
|
166
|
+
name = name_match.group(1)
|
|
167
|
+
meta = "(" + name_match.group(2) + ")"
|
|
168
|
+
candidates.append(
|
|
169
|
+
Candidate(
|
|
170
|
+
rno=rno,
|
|
171
|
+
url=urllib.parse.urljoin(BASE_URL, match.group("href")),
|
|
172
|
+
name=name,
|
|
173
|
+
meta=meta,
|
|
174
|
+
raw_summary=filter_action_control_text(raw[:1000]),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
if len(candidates) >= limit:
|
|
178
|
+
break
|
|
179
|
+
return candidates
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_candidates(markup: str, limit: int) -> list[Candidate]:
|
|
183
|
+
parsed = parse_with_bs4(markup, limit)
|
|
184
|
+
if parsed is not None:
|
|
185
|
+
return parsed
|
|
186
|
+
return parse_with_regex(markup, limit)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search public JobKorea talent summaries.
|
|
3
|
+
|
|
4
|
+
This helper uses JobKorea's browser-visible corporate talent search page and its
|
|
5
|
+
same AJAX endpoint. It only reads public/obfuscated list summaries. Full resume
|
|
6
|
+
view, contact details, scraping at scale, scrap/bookmark, and position proposal
|
|
7
|
+
flows are intentionally out of scope because they require an employer account,
|
|
8
|
+
paid entitlements, or user confirmation.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.error
|
|
16
|
+
from dataclasses import asdict
|
|
17
|
+
|
|
18
|
+
from jobkorea_talent_models import Candidate
|
|
19
|
+
from jobkorea_talent_parse import clean_text, parse_candidates
|
|
20
|
+
from jobkorea_talent_search_condition import build_search_condition, post_search
|
|
21
|
+
|
|
22
|
+
__all__ = ["parse_candidates"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(description="Search public JobKorea talent summaries")
|
|
27
|
+
parser.add_argument("--keyword", "-k", action="append", default=[], help="통합검색 키워드. 여러 번 지정 가능")
|
|
28
|
+
parser.add_argument("--and-keyword", action="append", default=[], help="AND 키워드")
|
|
29
|
+
parser.add_argument("--or-keyword", action="append", default=[], help="OR 키워드")
|
|
30
|
+
parser.add_argument("--exclude-keyword", action="append", default=[], help="제외 키워드")
|
|
31
|
+
parser.add_argument("--job-category", action="append", default=[], help="직무 대분류명 예: AI·개발·데이터")
|
|
32
|
+
parser.add_argument("--work-area", action="append", default=[], help="희망 근무지역 예: 서울, 강남구, 경기")
|
|
33
|
+
parser.add_argument("--residential-area", action="append", default=[], help="거주지역 예: 서울, 성남시 분당구")
|
|
34
|
+
parser.add_argument("--career-min", type=int, help="최소 경력 연수")
|
|
35
|
+
parser.add_argument("--career-max", type=int, help="최대 경력 연수")
|
|
36
|
+
parser.add_argument("--page", type=int, default=1)
|
|
37
|
+
parser.add_argument("--limit", type=int, default=20, choices=[10, 20, 30, 50, 100])
|
|
38
|
+
parser.add_argument("--sort", default="0", help="잡코리아 sf 정렬 코드. 기본 0")
|
|
39
|
+
parser.add_argument("--json", action="store_true", help="JSON으로 출력")
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_markdown(candidates: list[Candidate], matched: dict[str, list[str]], args: argparse.Namespace) -> None:
|
|
44
|
+
print("# 잡코리아 인재검색 결과\n")
|
|
45
|
+
print(f"- 검색어: {', '.join(args.keyword + args.and_keyword + args.or_keyword) or '(없음)'}")
|
|
46
|
+
print(f"- 제외어: {', '.join(args.exclude_keyword) or '(없음)'}")
|
|
47
|
+
if any(matched.values()):
|
|
48
|
+
print(f"- 매칭된 필터: {json.dumps(matched, ensure_ascii=False)}")
|
|
49
|
+
print(f"- 결과 수: {len(candidates)}")
|
|
50
|
+
print("- 주의: 이름/회사명은 잡코리아 공개 화면 기준으로 마스킹되어 있으며, 상세 이력서 확인·포지션 제안은 기업회원 로그인/권한/사용자 확인이 필요합니다.\n")
|
|
51
|
+
for idx, candidate in enumerate(candidates, 1):
|
|
52
|
+
c = candidate
|
|
53
|
+
bits = [c.name, c.meta, c.career]
|
|
54
|
+
title = " ".join(x for x in bits if x).strip() or f"rNo={c.rno}"
|
|
55
|
+
print(f"## {idx}. {title}")
|
|
56
|
+
print(f"- URL: {c.url}")
|
|
57
|
+
if c.skills:
|
|
58
|
+
print(f"- 키워드/스킬: {c.skills}")
|
|
59
|
+
summary = c.raw_summary.replace("\n", " ")
|
|
60
|
+
if summary:
|
|
61
|
+
print(f"- 요약: {summary[:500]}")
|
|
62
|
+
print()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run(argv: list[str] | None = None) -> int:
|
|
66
|
+
parser = build_parser()
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
|
|
69
|
+
if not (args.keyword or args.and_keyword or args.or_keyword or args.job_category or args.work_area or args.residential_area):
|
|
70
|
+
parser.error("최소 하나 이상의 --keyword, --job-category, --work-area 등을 지정하세요")
|
|
71
|
+
|
|
72
|
+
sc, matched = build_search_condition(args)
|
|
73
|
+
markup = post_search(sc)
|
|
74
|
+
cleaned = clean_text(markup)
|
|
75
|
+
if "로그인" in cleaned[:500] and "인재" not in cleaned[:2000]:
|
|
76
|
+
raise RuntimeError("잡코리아가 로그인/차단 화면을 반환했습니다")
|
|
77
|
+
candidates = parse_candidates(markup, args.limit)
|
|
78
|
+
|
|
79
|
+
if args.json:
|
|
80
|
+
print(json.dumps({"matched_filters": matched, "candidates": [asdict(c) for c in candidates]}, ensure_ascii=False, indent=2))
|
|
81
|
+
else:
|
|
82
|
+
print_markdown(candidates, matched, args)
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
try:
|
|
88
|
+
raise SystemExit(run())
|
|
89
|
+
except urllib.error.HTTPError as exc:
|
|
90
|
+
print(f"HTTP error: {exc.code} {exc.reason}", file=sys.stderr)
|
|
91
|
+
raise SystemExit(2)
|
|
92
|
+
except (RuntimeError, urllib.error.URLError) as exc:
|
|
93
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
94
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from jobkorea_talent_models import AJAX_PATH, BASE_URL, DEFAULT_UA, FIND_PATH
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fetch(url: str, *, data: bytes | None = None, headers: dict[str, str] | None = None) -> str:
|
|
14
|
+
req_headers = {"User-Agent": DEFAULT_UA, "Referer": BASE_URL + FIND_PATH}
|
|
15
|
+
if headers:
|
|
16
|
+
req_headers.update(headers)
|
|
17
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method="POST" if data else "GET")
|
|
18
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
19
|
+
return resp.read().decode("utf-8", "ignore")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_json_object(source: str, marker: str) -> dict[str, Any]:
|
|
23
|
+
idx = source.find(marker)
|
|
24
|
+
if idx < 0:
|
|
25
|
+
raise RuntimeError(f"cannot find marker: {marker}")
|
|
26
|
+
start = source.find("{", idx)
|
|
27
|
+
if start < 0:
|
|
28
|
+
raise RuntimeError("cannot find JSON object start")
|
|
29
|
+
depth = 0
|
|
30
|
+
in_string = False
|
|
31
|
+
escape = False
|
|
32
|
+
for pos in range(start, len(source)):
|
|
33
|
+
ch = source[pos]
|
|
34
|
+
if in_string:
|
|
35
|
+
if escape:
|
|
36
|
+
escape = False
|
|
37
|
+
elif ch == "\\":
|
|
38
|
+
escape = True
|
|
39
|
+
elif ch == '"':
|
|
40
|
+
in_string = False
|
|
41
|
+
continue
|
|
42
|
+
if ch == '"':
|
|
43
|
+
in_string = True
|
|
44
|
+
elif ch == "{":
|
|
45
|
+
depth += 1
|
|
46
|
+
elif ch == "}":
|
|
47
|
+
depth -= 1
|
|
48
|
+
if depth == 0:
|
|
49
|
+
loaded = json.loads(source[start : pos + 1])
|
|
50
|
+
if not isinstance(loaded, dict):
|
|
51
|
+
raise RuntimeError("search condition was not a JSON object")
|
|
52
|
+
return loaded
|
|
53
|
+
raise RuntimeError("unterminated JSON object")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def iter_nodes(node: Any) -> Iterator[dict[str, Any]]:
|
|
57
|
+
if isinstance(node, dict):
|
|
58
|
+
yield node
|
|
59
|
+
for value in node.values():
|
|
60
|
+
yield from iter_nodes(value)
|
|
61
|
+
elif isinstance(node, list):
|
|
62
|
+
for item in node:
|
|
63
|
+
yield from iter_nodes(item)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def mark_matching_nodes(sc: dict[str, Any], top_key: str, labels: list[str]) -> list[str]:
|
|
67
|
+
if not labels:
|
|
68
|
+
return []
|
|
69
|
+
section = sc.get(top_key)
|
|
70
|
+
if section is None:
|
|
71
|
+
return []
|
|
72
|
+
wanted = [x.strip().lower() for x in labels if x.strip()]
|
|
73
|
+
matched: list[str] = []
|
|
74
|
+
for node in iter_nodes(section):
|
|
75
|
+
title = str(node.get("t", ""))
|
|
76
|
+
code = str(node.get("v", ""))
|
|
77
|
+
title_l = title.lower()
|
|
78
|
+
code_l = code.lower()
|
|
79
|
+
if any(w == title_l or w == code_l or w in title_l for w in wanted):
|
|
80
|
+
for key in ("s", "c", "use"):
|
|
81
|
+
if key in node:
|
|
82
|
+
node[key] = 1
|
|
83
|
+
matched.append(title or code)
|
|
84
|
+
return matched
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_search_condition(args: argparse.Namespace) -> tuple[dict[str, Any], dict[str, list[str]]]:
|
|
88
|
+
first = fetch(BASE_URL + FIND_PATH)
|
|
89
|
+
sc = extract_json_object(first, "var searchcondition =")
|
|
90
|
+
|
|
91
|
+
sc["p"] = args.page
|
|
92
|
+
sc["ps"] = args.limit
|
|
93
|
+
sc["saveno"] = 0
|
|
94
|
+
sc["ff"] = 0
|
|
95
|
+
sc["sf"] = args.sort
|
|
96
|
+
|
|
97
|
+
terms: list[dict[str, Any]] = []
|
|
98
|
+
for kw in args.keyword:
|
|
99
|
+
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 0})
|
|
100
|
+
for kw in args.and_keyword:
|
|
101
|
+
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 1})
|
|
102
|
+
for kw in args.or_keyword:
|
|
103
|
+
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 3})
|
|
104
|
+
for kw in args.exclude_keyword:
|
|
105
|
+
terms.append({"s": 1, "c": 1, "t": kw, "v": kw, "kwdtypecode": 1, "logictypecode": 2})
|
|
106
|
+
sc["totalkeywordlist"] = terms
|
|
107
|
+
|
|
108
|
+
if terms:
|
|
109
|
+
first_kw = terms[0]["t"]
|
|
110
|
+
sc.setdefault("pfr", {}).setdefault("ck", {})["Keyword"] = first_kw
|
|
111
|
+
sc["pfr"]["ck"]["KeywordType"] = 1
|
|
112
|
+
sc["pfr"]["n"] = 1
|
|
113
|
+
|
|
114
|
+
if args.career_min is not None:
|
|
115
|
+
sc.setdefault("career", {})["s"] = str(args.career_min)
|
|
116
|
+
if args.career_max is not None:
|
|
117
|
+
sc.setdefault("career", {})["e"] = str(args.career_max)
|
|
118
|
+
|
|
119
|
+
matched = {
|
|
120
|
+
"job_category": mark_matching_nodes(sc, "jobtype", args.job_category),
|
|
121
|
+
"work_area": mark_matching_nodes(sc, "workarea", args.work_area),
|
|
122
|
+
"residential_area": mark_matching_nodes(sc, "residentialarea", args.residential_area),
|
|
123
|
+
}
|
|
124
|
+
return sc, matched
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def post_search(sc: dict[str, Any]) -> str:
|
|
128
|
+
body = urllib.parse.urlencode({"searchCondition": json.dumps(sc, ensure_ascii=False)}).encode()
|
|
129
|
+
return fetch(
|
|
130
|
+
BASE_URL + AJAX_PATH,
|
|
131
|
+
data=body,
|
|
132
|
+
headers={
|
|
133
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
134
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
135
|
+
},
|
|
136
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fixture tests for JobKorea public fallback parsing."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SCRIPT = Path(__file__).with_name("jobkorea_talent_search.py")
|
|
11
|
+
sys.path.insert(0, str(SCRIPT.parent))
|
|
12
|
+
spec = importlib.util.spec_from_file_location("jobkorea_talent_search", SCRIPT)
|
|
13
|
+
assert spec is not None
|
|
14
|
+
helper = importlib.util.module_from_spec(spec)
|
|
15
|
+
sys.modules["jobkorea_talent_search"] = helper
|
|
16
|
+
assert spec.loader is not None
|
|
17
|
+
spec.loader.exec_module(helper)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
FALLBACK_FIXTURE = """
|
|
21
|
+
<section class="searchList">
|
|
22
|
+
<table class="tblSearchList">
|
|
23
|
+
<tbody>
|
|
24
|
+
<tr class="dvResumeTr" data-rno="111">
|
|
25
|
+
<td class="tdProfile">
|
|
26
|
+
<dl class="nameAge"><dt><a class="dvResumeLink" href="/corp/person/find/resume/view?rNo=111" data-rno="111">김OO</a></dt><dd>(여, 만 29세)</dd></dl>
|
|
27
|
+
<ul class="bullList"><li>25분전 공고 스크랩</li></ul>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="tdSummary">
|
|
30
|
+
<div class="userInfoBox">
|
|
31
|
+
<span class="career">경력 4년</span>
|
|
32
|
+
<p class="title"><a class="dvResumeLink" href="/corp/person/find/resume/view?rNo=111" data-rno="111">퍼포먼스 마케터</a></p>
|
|
33
|
+
<div class="keywordSkill keywordBox">
|
|
34
|
+
<button type="button" class="js-kwrdSearch">Google Analytics</button>
|
|
35
|
+
<button type="button" class="js-kwrdSearch">GA4</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="tdAction">
|
|
40
|
+
<button>스크랩 1</button><button>이력서 확인</button><button>포지션 제안</button><button>메모하기</button><button>저장하기</button><button>닫기</button>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<tr class="dvResumeTr" data-rno="222">
|
|
44
|
+
<td class="tdProfile">
|
|
45
|
+
<dl class="nameAge"><dt><a class="dvResumeLink" href="/corp/person/find/resume/view?rNo=222" data-rno="222">박OO</a></dt><dd>(남, 만 31세)</dd></dl>
|
|
46
|
+
</td>
|
|
47
|
+
<td class="tdSummary">
|
|
48
|
+
<span class="career">경력 6년</span>
|
|
49
|
+
<p class="title"><a class="dvResumeLink" href="/corp/person/find/resume/view?rNo=222" data-rno="222">브랜드 마케터</a></p>
|
|
50
|
+
<div class="keywordSkill keywordBox"><button type="button" class="js-kwrdSearch">브랜딩</button></div>
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</section>
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class JobKoreaFallbackParserTest(unittest.TestCase):
|
|
60
|
+
def test_parser_keeps_each_candidate_inside_its_own_row(self) -> None:
|
|
61
|
+
candidates = helper.parse_candidates(FALLBACK_FIXTURE, 10)
|
|
62
|
+
|
|
63
|
+
self.assertEqual([c.rno for c in candidates], ["111", "222"])
|
|
64
|
+
self.assertEqual(candidates[0].name, "김OO")
|
|
65
|
+
self.assertIn("Google Analytics", candidates[0].raw_summary)
|
|
66
|
+
self.assertIn("GA4", candidates[0].raw_summary)
|
|
67
|
+
self.assertNotIn("박OO", candidates[0].raw_summary)
|
|
68
|
+
self.assertNotIn("브랜딩", candidates[0].raw_summary)
|
|
69
|
+
self.assertNotIn("저장하기", candidates[0].raw_summary)
|
|
70
|
+
self.assertNotIn("닫기", candidates[0].raw_summary)
|
|
71
|
+
self.assertNotIn("포지션 제안", candidates[0].raw_summary)
|
|
72
|
+
self.assertNotIn("이력서 확인", candidates[0].raw_summary)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
unittest.main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jobkorea-talent-search",
|
|
3
|
+
"description": "잡코리아 기업회원 로그인 세션으로 유료 열람 전 마스킹된 인재 이력서를 검색·비교해 채용 검토용 shortlist를 만듭니다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"browser",
|
|
6
|
+
"action:communication"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: jobkorea-talent-search\ndescription: 잡코리아 기업회원 로그인 세션으로 유료 열람 전 마스킹된 인재 이력서를 검색·비교해 채용 검토용 shortlist를 만듭니다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: recruiting\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 조선왕조실록 검색
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
국사편찬위원회 조선왕조실록 사이트(`https://sillok.history.go.kr`)에서 **공식 검색 결과 HTML과 기사 상세 페이지를 직접 읽어** 조선왕조실록 기록을 찾는다.
|
|
6
|
+
|
|
7
|
+
v1 범위는 단순 스크래핑이다.
|
|
8
|
+
|
|
9
|
+
- 키워드 검색
|
|
10
|
+
- 선택적 왕별 필터(`--king`)
|
|
11
|
+
- 선택적 서기 연도 필터(`--year`)
|
|
12
|
+
- 검색 결과 제목/요약/원문 링크 정리
|
|
13
|
+
- 기사 상세 페이지에서 국역/원문 excerpt 추출
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- "조선왕조실록에서 훈민정음 찾아줘"
|
|
18
|
+
- "세종 때 실록에서 측우기 관련 기사 검색해줘"
|
|
19
|
+
- "1443년 조선왕조실록 기록 찾아줘"
|
|
20
|
+
- "정조실록에서 수원 관련 기록 몇 개 보여줘"
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- 인터넷 연결
|
|
25
|
+
- `python3`
|
|
26
|
+
- 별도 API 키 없음
|
|
27
|
+
- 설치된 skill payload 안에 `scripts/sillok_search.py` helper가 함께 들어 있다.
|
|
28
|
+
|
|
29
|
+
## Inputs
|
|
30
|
+
|
|
31
|
+
- 필수: 검색어
|
|
32
|
+
- 선택: 왕 이름 (`세종`, `정조`, `세종실록` 등)
|
|
33
|
+
- 선택: 서기 연도 (`1443` 같이 Gregorian year)
|
|
34
|
+
- 선택: 결과 수 (`--limit`)
|
|
35
|
+
- 선택: 검색 타입 (`--type k|w`)
|
|
36
|
+
- `k`: 국역 검색
|
|
37
|
+
- `w`: 원문 검색
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
1. `python3 scripts/sillok_search.py --query "..."` 로 공식 검색 endpoint를 호출한다.
|
|
42
|
+
2. 검색 결과 HTML에서 결과 수, 왕별 분류, 기사 링크, 요약을 파싱한다.
|
|
43
|
+
3. 필요하면 `--king`, `--year` 로 결과를 추가로 좁힌다.
|
|
44
|
+
4. 선택된 기사마다 `/id/<article_id>` 상세 페이지를 열어 국역/원문 excerpt를 가져온다.
|
|
45
|
+
5. 구조화된 JSON으로 반환한다.
|
|
46
|
+
|
|
47
|
+
## CLI examples
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python3 scripts/sillok_search.py --query "훈민정음"
|
|
51
|
+
python3 scripts/sillok_search.py --query "훈민정음" --king "세종" --year 1443 --limit 3
|
|
52
|
+
python3 scripts/sillok_search.py --query "측우기" --king "세종실록" --limit 5
|
|
53
|
+
python3 scripts/sillok_search.py --query "임진왜란" --type w --limit 5
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Response policy
|
|
57
|
+
|
|
58
|
+
- 결과는 공식 실록 사이트에서 확인한 **기사 제목 + 링크 + 요약 + 상세 excerpt** 중심으로 답한다.
|
|
59
|
+
- `--year` 는 서기 연도 기준으로 필터링한다.
|
|
60
|
+
- 입력한 왕 이름은 `세종`, `세종실록`처럼 조금 달라도 canonical 왕명으로 정규화한다.
|
|
61
|
+
- v1 에서는 semantic search, embedding, 대규모 색인 구축을 하지 않는다.
|
|
62
|
+
- 결과가 없으면 억지로 추정하지 말고 빈 결과를 그대로 알려준다.
|
|
63
|
+
|
|
64
|
+
## Done when
|
|
65
|
+
|
|
66
|
+
- 공식 사이트에서 실제 검색 결과가 1건 이상 조회되었다.
|
|
67
|
+
- 필요 시 왕/연도 필터가 적용되었다.
|
|
68
|
+
- 적어도 하나 이상의 기사 detail excerpt가 포함되었다.
|
|
69
|
+
- 링크가 `https://sillok.history.go.kr/id/...` 형태로 정리되었다.
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- 공식 메인: `https://sillok.history.go.kr`
|
|
74
|
+
- 검색 endpoint: `https://sillok.history.go.kr/search/searchResultList.do`
|
|
75
|
+
- 기사 상세: `https://sillok.history.go.kr/id/<article_id>`
|
|
76
|
+
- 이 저장소 v1 은 공개 HTML 표면만 사용한다.
|