@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.
Files changed (350) hide show
  1. package/bin/k-skill.js +60 -0
  2. package/package.json +37 -0
  3. package/skills/assembly-bill-vote-search/instruction.md +111 -0
  4. package/skills/assembly-bill-vote-search/skill.json +10 -0
  5. package/skills/biz-health-check/instruction.md +69 -0
  6. package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
  7. package/skills/biz-health-check/skill.json +10 -0
  8. package/skills/bok-ecos-stats/instruction.md +95 -0
  9. package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
  10. package/skills/bok-ecos-stats/skill.json +10 -0
  11. package/skills/building-register-search/instruction.md +69 -0
  12. package/skills/building-register-search/scripts/building_register.py +309 -0
  13. package/skills/building-register-search/scripts/building_register_xml.py +32 -0
  14. package/skills/building-register-search/skill.json +9 -0
  15. package/skills/bunjang-search/instruction.md +154 -0
  16. package/skills/bunjang-search/skill.json +10 -0
  17. package/skills/catchtable-sniper/instruction.md +270 -0
  18. package/skills/catchtable-sniper/skill.json +9 -0
  19. package/skills/cheap-gas-nearby/instruction.md +107 -0
  20. package/skills/cheap-gas-nearby/skill.json +10 -0
  21. package/skills/corporate-registration-consulting/instruction.md +117 -0
  22. package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
  23. package/skills/corporate-registration-consulting/skill.json +8 -0
  24. package/skills/coupang-product-search/instruction.md +219 -0
  25. package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
  26. package/skills/coupang-product-search/skill.json +10 -0
  27. package/skills/court-auction-notice-search/instruction.md +201 -0
  28. package/skills/court-auction-notice-search/skill.json +9 -0
  29. package/skills/court-payment-order-assistant/instruction.md +120 -0
  30. package/skills/court-payment-order-assistant/skill.json +10 -0
  31. package/skills/d2b-notice-search/instruction.md +117 -0
  32. package/skills/d2b-notice-search/skill.json +10 -0
  33. package/skills/daangn-cars-search/instruction.md +91 -0
  34. package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
  35. package/skills/daangn-cars-search/skill.json +9 -0
  36. package/skills/daangn-jobs-search/instruction.md +90 -0
  37. package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
  38. package/skills/daangn-jobs-search/skill.json +9 -0
  39. package/skills/daangn-realty-search/instruction.md +114 -0
  40. package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
  41. package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
  42. package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
  43. package/skills/daangn-realty-search/skill.json +8 -0
  44. package/skills/daangn-used-goods-search/instruction.md +90 -0
  45. package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
  46. package/skills/daangn-used-goods-search/skill.json +9 -0
  47. package/skills/daishin-report-search/instruction.md +138 -0
  48. package/skills/daishin-report-search/skill.json +10 -0
  49. package/skills/daiso-product-search/instruction.md +172 -0
  50. package/skills/daiso-product-search/skill.json +8 -0
  51. package/skills/danawa-price-search/instruction.md +183 -0
  52. package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
  53. package/skills/danawa-price-search/skill.json +8 -0
  54. package/skills/delivery-tracking/instruction.md +358 -0
  55. package/skills/delivery-tracking/skill.json +8 -0
  56. package/skills/donation-place-search/instruction.md +129 -0
  57. package/skills/donation-place-search/skill.json +9 -0
  58. package/skills/emergency-room-beds/instruction.md +82 -0
  59. package/skills/emergency-room-beds/skill.json +8 -0
  60. package/skills/ev-charger-nearby/instruction.md +80 -0
  61. package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
  62. package/skills/ev-charger-nearby/skill.json +10 -0
  63. package/skills/ev-subsidy-status/instruction.md +165 -0
  64. package/skills/ev-subsidy-status/skill.json +10 -0
  65. package/skills/express-bus-booking/instruction.md +207 -0
  66. package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
  67. package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
  68. package/skills/express-bus-booking/skill.json +8 -0
  69. package/skills/fine-dust-location/instruction.md +89 -0
  70. package/skills/fine-dust-location/skill.json +9 -0
  71. package/skills/flight-ticket-search/instruction.md +237 -0
  72. package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
  73. package/skills/flight-ticket-search/skill.json +9 -0
  74. package/skills/foresttrip-vacancy/instruction.md +167 -0
  75. package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
  76. package/skills/foresttrip-vacancy/skill.json +10 -0
  77. package/skills/fsc-corporate-info/instruction.md +57 -0
  78. package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
  79. package/skills/fsc-corporate-info/skill.json +10 -0
  80. package/skills/g2b-order-plan-search/instruction.md +131 -0
  81. package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
  82. package/skills/g2b-order-plan-search/skill.json +10 -0
  83. package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
  84. package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
  85. package/skills/g2b-sanctioned-supplier/skill.json +10 -0
  86. package/skills/gangnamunni-clinic-search/instruction.md +113 -0
  87. package/skills/gangnamunni-clinic-search/skill.json +9 -0
  88. package/skills/geeknews-search/instruction.md +69 -0
  89. package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
  90. package/skills/geeknews-search/skill.json +8 -0
  91. package/skills/gongsijiga-search/instruction.md +128 -0
  92. package/skills/gongsijiga-search/skill.json +9 -0
  93. package/skills/gov-overseas-trip-report/instruction.md +488 -0
  94. package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
  95. package/skills/gov-overseas-trip-report/skill.json +9 -0
  96. package/skills/han-river-water-level/instruction.md +83 -0
  97. package/skills/han-river-water-level/skill.json +10 -0
  98. package/skills/highway-traffic-status/instruction.md +88 -0
  99. package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
  100. package/skills/highway-traffic-status/skill.json +11 -0
  101. package/skills/hipass-receipt/instruction.md +97 -0
  102. package/skills/hipass-receipt/skill.json +10 -0
  103. package/skills/hola-poke-yeoksam/instruction.md +247 -0
  104. package/skills/hola-poke-yeoksam/skill.json +8 -0
  105. package/skills/household-waste-info/instruction.md +117 -0
  106. package/skills/household-waste-info/skill.json +10 -0
  107. package/skills/housing-official-price/instruction.md +177 -0
  108. package/skills/housing-official-price/skill.json +10 -0
  109. package/skills/hwp/instruction.md +206 -0
  110. package/skills/hwp/skill.json +8 -0
  111. package/skills/intercity-bus-booking/instruction.md +189 -0
  112. package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
  113. package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
  114. package/skills/intercity-bus-booking/skill.json +8 -0
  115. package/skills/iros-registry-automation/instruction.md +229 -0
  116. package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
  117. package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
  118. package/skills/iros-registry-automation/skill.json +9 -0
  119. package/skills/job-posting-match/instruction.md +130 -0
  120. package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
  121. package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
  122. package/skills/job-posting-match/skill.json +8 -0
  123. package/skills/jobkorea-talent-search/instruction.md +118 -0
  124. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
  125. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
  126. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
  127. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
  128. package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
  129. package/skills/jobkorea-talent-search/skill.json +9 -0
  130. package/skills/joseon-sillok-search/instruction.md +76 -0
  131. package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
  132. package/skills/joseon-sillok-search/skill.json +8 -0
  133. package/skills/k-dart/instruction.md +406 -0
  134. package/skills/k-dart/skill.json +8 -0
  135. package/skills/k-schoollunch-menu/instruction.md +109 -0
  136. package/skills/k-schoollunch-menu/skill.json +9 -0
  137. package/skills/k-skill-cleaner/instruction.md +80 -0
  138. package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
  139. package/skills/k-skill-cleaner/skill.json +8 -0
  140. package/skills/k-skill-setup/instruction.md +253 -0
  141. package/skills/k-skill-setup/skill.json +11 -0
  142. package/skills/kakao-bar-nearby/instruction.md +76 -0
  143. package/skills/kakao-bar-nearby/skill.json +8 -0
  144. package/skills/kakao-map/instruction.md +176 -0
  145. package/skills/kakao-map/skill.json +10 -0
  146. package/skills/kakaotalk-mac/instruction.md +189 -0
  147. package/skills/kakaotalk-mac/skill.json +8 -0
  148. package/skills/kbl-results/instruction.md +89 -0
  149. package/skills/kbl-results/skill.json +9 -0
  150. package/skills/kbo-results/instruction.md +82 -0
  151. package/skills/kbo-results/skill.json +8 -0
  152. package/skills/keris-academic-search/instruction.md +81 -0
  153. package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
  154. package/skills/keris-academic-search/skill.json +10 -0
  155. package/skills/kleague-results/instruction.md +92 -0
  156. package/skills/kleague-results/skill.json +8 -0
  157. package/skills/kopis-performance-search/instruction.md +109 -0
  158. package/skills/kopis-performance-search/skill.json +10 -0
  159. package/skills/korea-weather/instruction.md +93 -0
  160. package/skills/korea-weather/skill.json +10 -0
  161. package/skills/korean-character-count/instruction.md +87 -0
  162. package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
  163. package/skills/korean-character-count/skill.json +8 -0
  164. package/skills/korean-cinema-search/instruction.md +177 -0
  165. package/skills/korean-cinema-search/skill.json +8 -0
  166. package/skills/korean-heritage-search/instruction.md +106 -0
  167. package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
  168. package/skills/korean-heritage-search/skill.json +8 -0
  169. package/skills/korean-holiday-calendar/instruction.md +97 -0
  170. package/skills/korean-holiday-calendar/skill.json +9 -0
  171. package/skills/korean-humanizer/instruction.md +389 -0
  172. package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
  173. package/skills/korean-humanizer/skill.json +8 -0
  174. package/skills/korean-jangbu-for/instruction.md +133 -0
  175. package/skills/korean-jangbu-for/scripts/install.sh +237 -0
  176. package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
  177. package/skills/korean-jangbu-for/skill.json +8 -0
  178. package/skills/korean-law-search/instruction.md +126 -0
  179. package/skills/korean-law-search/skill.json +10 -0
  180. package/skills/korean-marathon-schedule/instruction.md +111 -0
  181. package/skills/korean-marathon-schedule/skill.json +9 -0
  182. package/skills/korean-middle-korean/instruction.md +79 -0
  183. package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
  184. package/skills/korean-middle-korean/skill.json +8 -0
  185. package/skills/korean-patent-search/instruction.md +79 -0
  186. package/skills/korean-patent-search/scripts/patent_search.py +409 -0
  187. package/skills/korean-patent-search/skill.json +9 -0
  188. package/skills/korean-privacy-terms/instruction.md +128 -0
  189. package/skills/korean-privacy-terms/scripts/install.sh +108 -0
  190. package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
  191. package/skills/korean-privacy-terms/skill.json +8 -0
  192. package/skills/korean-scholarship-search/instruction.md +317 -0
  193. package/skills/korean-scholarship-search/references/report-format.md +40 -0
  194. package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
  195. package/skills/korean-scholarship-search/references/search-clues.md +58 -0
  196. package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
  197. package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
  198. package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
  199. package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
  200. package/skills/korean-scholarship-search/skill.json +8 -0
  201. package/skills/korean-slang-writing/instruction.md +181 -0
  202. package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
  203. package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
  204. package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
  205. package/skills/korean-slang-writing/skill.json +8 -0
  206. package/skills/korean-spell-check/instruction.md +105 -0
  207. package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
  208. package/skills/korean-spell-check/skill.json +9 -0
  209. package/skills/korean-stock-search/instruction.md +194 -0
  210. package/skills/korean-stock-search/skill.json +10 -0
  211. package/skills/korean-transit-route/instruction.md +113 -0
  212. package/skills/korean-transit-route/skill.json +10 -0
  213. package/skills/kosis-stats/instruction.md +232 -0
  214. package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
  215. package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
  216. package/skills/kosis-stats/skill.json +9 -0
  217. package/skills/kr-whois-lookup/instruction.md +107 -0
  218. package/skills/kr-whois-lookup/skill.json +10 -0
  219. package/skills/kstartup-search/instruction.md +186 -0
  220. package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
  221. package/skills/kstartup-search/skill.json +11 -0
  222. package/skills/ktx-booking/instruction.md +244 -0
  223. package/skills/ktx-booking/skill.json +10 -0
  224. package/skills/lck-analytics/instruction.md +192 -0
  225. package/skills/lck-analytics/scripts/_lib.js +103 -0
  226. package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
  227. package/skills/lck-analytics/scripts/build-match-report.js +44 -0
  228. package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
  229. package/skills/lck-analytics/skill.json +9 -0
  230. package/skills/lh-notice-search/instruction.md +206 -0
  231. package/skills/lh-notice-search/skill.json +10 -0
  232. package/skills/library-book-search/instruction.md +139 -0
  233. package/skills/library-book-search/skill.json +9 -0
  234. package/skills/local-election-candidate-search/instruction.md +77 -0
  235. package/skills/local-election-candidate-search/skill.json +8 -0
  236. package/skills/localdata-business-status/instruction.md +64 -0
  237. package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
  238. package/skills/localdata-business-status/skill.json +8 -0
  239. package/skills/lotto-results/instruction.md +80 -0
  240. package/skills/lotto-results/skill.json +8 -0
  241. package/skills/lovebug-report/instruction.md +185 -0
  242. package/skills/lovebug-report/skill.json +9 -0
  243. package/skills/market-kurly-search/instruction.md +125 -0
  244. package/skills/market-kurly-search/skill.json +8 -0
  245. package/skills/mfds-drug-safety/instruction.md +86 -0
  246. package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
  247. package/skills/mfds-drug-safety/skill.json +10 -0
  248. package/skills/mfds-food-safety/instruction.md +126 -0
  249. package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
  250. package/skills/mfds-food-safety/skill.json +10 -0
  251. package/skills/myrealtrip-search/instruction.md +239 -0
  252. package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
  253. package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
  254. package/skills/myrealtrip-search/skill.json +9 -0
  255. package/skills/naming-house/instruction.md +146 -0
  256. package/skills/naming-house/skill.json +8 -0
  257. package/skills/national-pension-workplace/instruction.md +64 -0
  258. package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
  259. package/skills/national-pension-workplace/skill.json +10 -0
  260. package/skills/naver-ad-performance/instruction.md +108 -0
  261. package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
  262. package/skills/naver-ad-performance/skill.json +9 -0
  263. package/skills/naver-blog-research/instruction.md +128 -0
  264. package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
  265. package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
  266. package/skills/naver-blog-research/scripts/naver_read.py +256 -0
  267. package/skills/naver-blog-research/scripts/naver_search.py +192 -0
  268. package/skills/naver-blog-research/skill.json +8 -0
  269. package/skills/naver-news-search/instruction.md +103 -0
  270. package/skills/naver-news-search/skill.json +10 -0
  271. package/skills/naver-shopping-search/instruction.md +94 -0
  272. package/skills/naver-shopping-search/skill.json +11 -0
  273. package/skills/nhis-care-checkup-search/instruction.md +116 -0
  274. package/skills/nhis-care-checkup-search/skill.json +10 -0
  275. package/skills/nts-business-registration/instruction.md +115 -0
  276. package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
  277. package/skills/nts-business-registration/skill.json +10 -0
  278. package/skills/nts-tax-delinquency/instruction.md +54 -0
  279. package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
  280. package/skills/nts-tax-delinquency/skill.json +8 -0
  281. package/skills/ohou-today-deal/instruction.md +182 -0
  282. package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
  283. package/skills/ohou-today-deal/skill.json +9 -0
  284. package/skills/olive-young-search/instruction.md +154 -0
  285. package/skills/olive-young-search/skill.json +8 -0
  286. package/skills/parking-lot-search/instruction.md +96 -0
  287. package/skills/parking-lot-search/skill.json +9 -0
  288. package/skills/popbill/instruction.md +146 -0
  289. package/skills/popbill/scripts/popbill_cli.py +284 -0
  290. package/skills/popbill/scripts/popbill_registry.py +54 -0
  291. package/skills/popbill/scripts/popbill_safety.py +37 -0
  292. package/skills/popbill/scripts/popbill_templates.py +71 -0
  293. package/skills/popbill/skill.json +9 -0
  294. package/skills/public-restroom-nearby/instruction.md +89 -0
  295. package/skills/public-restroom-nearby/skill.json +9 -0
  296. package/skills/real-estate-search/instruction.md +172 -0
  297. package/skills/real-estate-search/skill.json +10 -0
  298. package/skills/rhwp-advanced/instruction.md +145 -0
  299. package/skills/rhwp-advanced/skill.json +8 -0
  300. package/skills/rhwp-edit/instruction.md +153 -0
  301. package/skills/rhwp-edit/skill.json +8 -0
  302. package/skills/s2b-notice-search/instruction.md +66 -0
  303. package/skills/s2b-notice-search/skill.json +9 -0
  304. package/skills/saju-fortune/instruction.md +177 -0
  305. package/skills/saju-fortune/skill.json +8 -0
  306. package/skills/saramin-talent-search/instruction.md +119 -0
  307. package/skills/saramin-talent-search/skill.json +9 -0
  308. package/skills/seoul-bike/instruction.md +83 -0
  309. package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
  310. package/skills/seoul-bike/skill.json +10 -0
  311. package/skills/seoul-density/instruction.md +109 -0
  312. package/skills/seoul-density/scripts/seoul_density.py +271 -0
  313. package/skills/seoul-density/skill.json +10 -0
  314. package/skills/seoul-subway-arrival/instruction.md +85 -0
  315. package/skills/seoul-subway-arrival/skill.json +9 -0
  316. package/skills/sh-notice-search/instruction.md +150 -0
  317. package/skills/sh-notice-search/skill.json +9 -0
  318. package/skills/srt-booking/instruction.md +181 -0
  319. package/skills/srt-booking/scripts/srt_booking.py +272 -0
  320. package/skills/srt-booking/scripts/srt_seats.py +156 -0
  321. package/skills/srt-booking/skill.json +20 -0
  322. package/skills/subway-lost-property/instruction.md +93 -0
  323. package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
  324. package/skills/subway-lost-property/skill.json +8 -0
  325. package/skills/ticket-availability/instruction.md +175 -0
  326. package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
  327. package/skills/ticket-availability/skill.json +9 -0
  328. package/skills/toss-securities/instruction.md +116 -0
  329. package/skills/toss-securities/skill.json +9 -0
  330. package/skills/used-car-price-search/instruction.md +109 -0
  331. package/skills/used-car-price-search/skill.json +8 -0
  332. package/skills/yebigun-training/instruction.md +177 -0
  333. package/skills/yebigun-training/skill.json +9 -0
  334. package/skills/zipcode-search/instruction.md +159 -0
  335. package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
  336. package/skills/zipcode-search/skill.json +8 -0
  337. package/src/assemble.js +134 -0
  338. package/src/detect.js +18 -0
  339. package/templates/action-account.md +4 -0
  340. package/templates/action-booking.md +5 -0
  341. package/templates/action-commerce.md +5 -0
  342. package/templates/action-communication.md +5 -0
  343. package/templates/action-submission.md +5 -0
  344. package/templates/browser.md +4 -0
  345. package/templates/core.md +5 -0
  346. package/templates/hard-boundary.md +3 -0
  347. package/templates/local.md +2 -0
  348. package/templates/lookup.md +2 -0
  349. package/templates/proxy.md +2 -0
  350. package/templates/vault.md +4 -0
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.error
8
+ import urllib.request
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+ from _naver_http import is_naver_url, urlopen
13
+
14
+ DEFAULT_OUTPUT_DIR = "./naver-images"
15
+ DEFAULT_MAX = 10
16
+ DEFAULT_TIMEOUT = 15
17
+
18
+ DEFAULT_HEADERS = {
19
+ "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
20
+ "Accept-Language": "ko,en-US;q=0.9,en;q=0.8",
21
+ "Referer": "https://m.blog.naver.com/",
22
+ "User-Agent": (
23
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
24
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
25
+ ),
26
+ }
27
+
28
+ CONTENT_TYPE_TO_EXT = {
29
+ "image/jpeg": ".jpg",
30
+ "image/png": ".png",
31
+ "image/gif": ".gif",
32
+ "image/webp": ".webp",
33
+ "image/bmp": ".bmp",
34
+ "image/svg+xml": ".svg",
35
+ }
36
+
37
+
38
+ _MAGIC_BYTES = (
39
+ (b"\x89PNG\r\n\x1a\n", ".png"),
40
+ (b"GIF87a", ".gif"),
41
+ (b"GIF89a", ".gif"),
42
+ (b"RIFF", ".webp"), # WebP: RIFF....WEBP (check first 4 bytes)
43
+ (b"BM", ".bmp"),
44
+ )
45
+
46
+
47
+ def guess_extension(url: str, content_type: str | None = None, data: bytes | None = None) -> str:
48
+ if content_type:
49
+ ct = content_type.split(";")[0].strip().lower()
50
+ if ct in CONTENT_TYPE_TO_EXT:
51
+ return CONTENT_TYPE_TO_EXT[ct]
52
+
53
+ lower_url = url.lower().split("?")[0]
54
+ for ext in (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"):
55
+ if lower_url.endswith(ext):
56
+ return ".jpg" if ext == ".jpeg" else ext
57
+
58
+ if data:
59
+ for magic, ext in _MAGIC_BYTES:
60
+ if data[:len(magic)] == magic:
61
+ if ext == ".webp" and data[8:12] != b"WEBP":
62
+ continue
63
+ return ext
64
+ if data[:2] in (b"\xff\xd8",):
65
+ return ".jpg"
66
+
67
+ return ".jpg"
68
+
69
+
70
+ def download_image(url: str, output_path: str, output_dir: str, timeout: int = DEFAULT_TIMEOUT, *, insecure: bool = False) -> dict:
71
+ """Download a single image from a Naver CDN URL.
72
+
73
+ *output_dir* is used solely for path-traversal protection: the resolved
74
+ *output_path* must reside inside *output_dir*.
75
+ """
76
+ if not is_naver_url(url):
77
+ return {"url": url, "error": "Not a Naver CDN URL. Skipped."}
78
+
79
+ real_dir = os.path.realpath(output_dir)
80
+ if not os.path.realpath(output_path).startswith(real_dir + os.sep):
81
+ return {"url": url, "error": "Output path escapes target directory. Skipped."}
82
+
83
+ request = urllib.request.Request(url, headers=DEFAULT_HEADERS)
84
+
85
+ try:
86
+ with urlopen(request, timeout, insecure=insecure) as response:
87
+ data = response.read()
88
+ content_type = response.headers.get("Content-Type", "")
89
+ except (urllib.error.HTTPError, urllib.error.URLError, OSError) as error:
90
+ return {"url": url, "error": str(error)}
91
+
92
+ ext = guess_extension(url, content_type, data)
93
+ if not os.path.splitext(output_path)[1]:
94
+ output_path += ext
95
+
96
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
97
+
98
+ with open(output_path, "wb") as f:
99
+ f.write(data)
100
+
101
+ size_kb = round(len(data) / 1024, 1)
102
+ return {"url": url, "path": output_path, "size_kb": size_kb}
103
+
104
+
105
+ def download_images(
106
+ urls: list[str],
107
+ output_dir: str = DEFAULT_OUTPUT_DIR,
108
+ max_count: int = DEFAULT_MAX,
109
+ timeout: int = DEFAULT_TIMEOUT,
110
+ *,
111
+ insecure: bool = False,
112
+ ) -> dict:
113
+ os.makedirs(output_dir, exist_ok=True)
114
+
115
+ max_count = max(1, max_count)
116
+ targets = urls[:max_count]
117
+ downloaded: list[dict] = []
118
+ failed: list[dict] = []
119
+
120
+ # index → result 순서를 보장하기 위해 dict로 매핑
121
+ results_by_index: dict[int, dict] = {}
122
+
123
+ with ThreadPoolExecutor(max_workers=min(4, max(1, len(targets)))) as executor:
124
+ future_to_index = {}
125
+ for i, url in enumerate(targets, start=1):
126
+ filename = f"{i:03d}"
127
+ output_path = os.path.join(output_dir, filename)
128
+ future = executor.submit(download_image, url, output_path, output_dir, timeout, insecure=insecure)
129
+ future_to_index[future] = i
130
+
131
+ for future in as_completed(future_to_index):
132
+ idx = future_to_index[future]
133
+ try:
134
+ results_by_index[idx] = future.result()
135
+ except Exception as exc:
136
+ results_by_index[idx] = {"url": targets[idx - 1], "error": str(exc)}
137
+
138
+ # 원래 순서대로 정렬
139
+ for idx in sorted(results_by_index):
140
+ result = results_by_index[idx]
141
+ if "error" in result:
142
+ failed.append(result)
143
+ else:
144
+ downloaded.append(result)
145
+
146
+ return {
147
+ "downloaded": len(downloaded),
148
+ "files": downloaded,
149
+ "failed": failed,
150
+ }
151
+
152
+
153
+ def parse_args(argv: list[str]) -> argparse.Namespace:
154
+ parser = argparse.ArgumentParser(
155
+ description="Download images from Naver blog CDN URLs."
156
+ )
157
+ parser.add_argument(
158
+ "--urls", type=str, default="",
159
+ help="Comma-separated image URLs.",
160
+ )
161
+ parser.add_argument(
162
+ "--output", type=str, default=DEFAULT_OUTPUT_DIR,
163
+ help=f"Output directory. Default: {DEFAULT_OUTPUT_DIR}",
164
+ )
165
+ parser.add_argument(
166
+ "--max", type=int, default=DEFAULT_MAX,
167
+ help=f"Maximum number of images to download. Default: {DEFAULT_MAX}",
168
+ )
169
+ parser.add_argument(
170
+ "--timeout", type=int, default=DEFAULT_TIMEOUT,
171
+ help=f"HTTP request timeout in seconds. Default: {DEFAULT_TIMEOUT}",
172
+ )
173
+ parser.add_argument(
174
+ "--insecure", action="store_true",
175
+ help="Skip SSL certificate verification (use only when certificate errors occur).",
176
+ )
177
+ return parser.parse_args(argv)
178
+
179
+
180
+ def read_urls_from_stdin() -> list[str]:
181
+ try:
182
+ data = json.load(sys.stdin)
183
+ if isinstance(data, dict) and "images" in data:
184
+ return [img["url"] for img in data["images"] if isinstance(img, dict) and img.get("url")]
185
+ if isinstance(data, list):
186
+ return [
187
+ u for item in data
188
+ if (u := (item if isinstance(item, str) else item.get("url", "")))
189
+ ]
190
+ if isinstance(data, dict):
191
+ print(
192
+ "[warn] stdin JSON에 'images' 키가 없습니다. "
193
+ "naver_read.py 실행 시 --no-images 플래그를 사용하지 않았는지 확인하세요.",
194
+ file=sys.stderr,
195
+ )
196
+ except (json.JSONDecodeError, KeyError, TypeError) as exc:
197
+ print(f"[warn] stdin JSON 파싱 실패: {exc}", file=sys.stderr)
198
+ return []
199
+ return []
200
+
201
+
202
+ def main(argv: list[str] | None = None) -> int:
203
+ args = parse_args(argv or sys.argv[1:])
204
+
205
+ urls: list[str] = []
206
+
207
+ if args.urls:
208
+ urls = [u.strip() for u in args.urls.split(",") if u.strip()]
209
+
210
+ if not urls and not sys.stdin.isatty():
211
+ urls = read_urls_from_stdin()
212
+
213
+ if not urls:
214
+ print(
215
+ json.dumps({"error": "No image URLs provided. Use --urls or pipe naver_read.py output via stdin."}, ensure_ascii=False),
216
+ file=sys.stderr,
217
+ )
218
+ return 1
219
+
220
+ result = download_images(
221
+ urls,
222
+ output_dir=args.output,
223
+ max_count=args.max,
224
+ timeout=args.timeout,
225
+ insecure=args.insecure,
226
+ )
227
+
228
+ print(json.dumps(result, ensure_ascii=False, indent=2))
229
+ return 0
230
+
231
+
232
+ if __name__ == "__main__":
233
+ raise SystemExit(main())
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from html import unescape
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
13
+ from _naver_http import TAG_RE, is_naver_url, urlopen
14
+
15
+ MOBILE_UA = (
16
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
17
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
18
+ )
19
+
20
+ DEFAULT_HEADERS = {
21
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
22
+ "Accept-Language": "ko,en-US;q=0.9,en;q=0.8",
23
+ "User-Agent": MOBILE_UA,
24
+ }
25
+
26
+ BR_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
27
+ BLOCK_END_RE = re.compile(r"</(p|div|li)>", re.IGNORECASE)
28
+ WHITESPACE_RE = re.compile(r"[ \t]+")
29
+ BLANK_LINES_RE = re.compile(r"\n{3,}")
30
+
31
+ _IMG_CDN_HOSTS = r"(?:blogfiles\.naver\.net|postfiles\.pstatic\.net|mblogthumb-phinf\.pstatic\.net)"
32
+
33
+ IMAGE_LAZY_PATTERN = re.compile(
34
+ rf'data-lazy-src="(https?://{_IMG_CDN_HOSTS}[^"]+)"'
35
+ )
36
+ IMAGE_SRC_PATTERN = re.compile(
37
+ rf'src="(https?://{_IMG_CDN_HOSTS}[^"]+)"'
38
+ )
39
+ IMAGE_ALT_PATTERN = re.compile(
40
+ r'alt="([^"]*)"'
41
+ )
42
+
43
+ TITLE_PATTERN = re.compile(
44
+ r'<title[^>]*>(.*?)</title>', re.DOTALL | re.IGNORECASE
45
+ )
46
+
47
+ SCRIPT_STYLE_RE = re.compile(r"<(script|style|noscript)[^>]*>.*?</\1>", re.DOTALL | re.IGNORECASE)
48
+
49
+ PC_BLOG_RE = re.compile(r"^https?://blog\.naver\.com/")
50
+ BLOG_ID_RE = re.compile(r"blog\.naver\.com/([a-zA-Z0-9_]+)/(\d+)")
51
+
52
+
53
+ def to_mobile_url(url: str) -> str:
54
+ url = url.strip()
55
+ url = PC_BLOG_RE.sub("https://m.blog.naver.com/", url)
56
+ if not url.startswith("https://m.blog.naver.com/"):
57
+ match = BLOG_ID_RE.search(url)
58
+ if match:
59
+ url = f"https://m.blog.naver.com/{match.group(1)}/{match.group(2)}"
60
+ return url
61
+
62
+
63
+ def fetch_blog_page(url: str, timeout: int = 20, *, insecure: bool = False) -> str:
64
+ mobile_url = to_mobile_url(url)
65
+ if not is_naver_url(mobile_url):
66
+ raise ValueError(f"Not a Naver blog URL: {url}")
67
+ request = urllib.request.Request(mobile_url, headers=DEFAULT_HEADERS)
68
+
69
+ try:
70
+ with urlopen(request, timeout, insecure=insecure) as response:
71
+ return response.read().decode("utf-8", "ignore")
72
+ except urllib.error.HTTPError as error:
73
+ raise RuntimeError(
74
+ f"Naver blog returned HTTP {error.code} for {mobile_url}. "
75
+ "The post may not exist or access may be restricted."
76
+ ) from error
77
+
78
+
79
+ def extract_title(html: str) -> str:
80
+ match = TITLE_PATTERN.search(html)
81
+ if not match:
82
+ return ""
83
+ title = unescape(TAG_RE.sub("", match.group(1))).strip()
84
+ title = re.sub(r"\s*[-:|]?\s*네이버\s*블로그$", "", title).strip()
85
+ return title
86
+
87
+
88
+ def _extract_div_block(html: str, start_pos: int) -> str:
89
+ tag_start = html.rfind("<div", 0, start_pos)
90
+ if tag_start < 0:
91
+ tag_start = start_pos
92
+
93
+ depth = 0
94
+ pos = tag_start
95
+ started = False
96
+ length = len(html)
97
+ while pos < length:
98
+ # HTML 주석 건너뛰기
99
+ if html[pos : pos + 4] == "<!--":
100
+ end = html.find("-->", pos + 4)
101
+ pos = end + 3 if end >= 0 else length
102
+ continue
103
+ if html[pos : pos + 4] == "<div" and (pos + 4 >= length or html[pos + 4] in (" ", ">", "\t", "\n", "/")):
104
+ depth += 1
105
+ started = True
106
+ elif html[pos : pos + 6] == "</div>":
107
+ depth -= 1
108
+ if started and depth == 0:
109
+ return html[tag_start : pos + 6]
110
+ pos += 1
111
+
112
+ return html[tag_start:]
113
+
114
+
115
+ def extract_content_area(html: str) -> str:
116
+ cleaned = SCRIPT_STYLE_RE.sub("", html)
117
+
118
+ match = re.search(r'class="[^"]*\bse-main-container\b[^"]*"', cleaned)
119
+ if match:
120
+ return _extract_div_block(cleaned, match.start())
121
+
122
+ for class_name in ("post_ct", "postViewArea", "post-view"):
123
+ match = re.search(rf'class="[^"]*\b{re.escape(class_name)}\b[^"]*"', cleaned)
124
+ if match:
125
+ return _extract_div_block(cleaned, match.start())
126
+
127
+ marker = cleaned.find('id="viewTypeSelector"')
128
+ if marker >= 0:
129
+ return _extract_div_block(cleaned, marker)
130
+
131
+ return ""
132
+
133
+
134
+ def extract_text(html_fragment: str) -> str:
135
+ text = BR_RE.sub("\n", html_fragment)
136
+ text = BLOCK_END_RE.sub("\n", text)
137
+ text = TAG_RE.sub("", text)
138
+ text = unescape(text)
139
+
140
+ lines = []
141
+ for line in text.split("\n"):
142
+ stripped = WHITESPACE_RE.sub(" ", line).strip()
143
+ if stripped:
144
+ lines.append(stripped)
145
+
146
+ result = "\n".join(lines)
147
+ result = BLANK_LINES_RE.sub("\n\n", result)
148
+ return result.strip()
149
+
150
+
151
+ def extract_images(html_fragment: str) -> list[dict]:
152
+ images: list[dict] = []
153
+ seen_base: set[str] = set()
154
+
155
+ img_tags = re.finditer(r"<img\s[^>]+>", html_fragment, re.IGNORECASE)
156
+ for img_match in img_tags:
157
+ img_tag = img_match.group(0)
158
+
159
+ lazy_match = IMAGE_LAZY_PATTERN.search(img_tag)
160
+ src_match = IMAGE_SRC_PATTERN.search(img_tag)
161
+ url_match = lazy_match or src_match
162
+ if not url_match:
163
+ continue
164
+
165
+ url = url_match.group(1)
166
+
167
+ base_url = re.sub(r"\?type=.*$", "", url)
168
+ if base_url in seen_base:
169
+ continue
170
+ seen_base.add(base_url)
171
+
172
+ if "?type=" not in url:
173
+ url = base_url
174
+ elif "_blur" in url:
175
+ url = re.sub(r"\?type=w\d+_blur", "?type=w800", url)
176
+
177
+ alt_match = IMAGE_ALT_PATTERN.search(img_tag)
178
+ alt = unescape(alt_match.group(1)).strip() if alt_match else ""
179
+
180
+ images.append({"url": url, "alt": alt})
181
+
182
+ return images
183
+
184
+
185
+ def read_blog(url: str, include_images: bool = True, max_length: int = 0, timeout: int = 20, *, insecure: bool = False) -> dict:
186
+ html = fetch_blog_page(url, timeout=timeout, insecure=insecure)
187
+ mobile_url = to_mobile_url(url)
188
+
189
+ title = extract_title(html)
190
+ content_area = extract_content_area(html)
191
+ content = extract_text(content_area)
192
+
193
+ if max_length > 0 and len(content) > max_length:
194
+ content = content[:max_length] + "..."
195
+
196
+ result: dict = {
197
+ "url": mobile_url,
198
+ "title": title,
199
+ "content": content,
200
+ "char_count": len(content),
201
+ }
202
+
203
+ if not content:
204
+ result["warning"] = "본문 영역을 찾지 못했습니다. 네이버 HTML 구조가 변경되었을 수 있습니다."
205
+
206
+ if include_images:
207
+ result["images"] = extract_images(content_area)
208
+
209
+ return result
210
+
211
+
212
+ def parse_args(argv: list[str]) -> argparse.Namespace:
213
+ parser = argparse.ArgumentParser(
214
+ description="Read a Naver blog post and extract text content and images."
215
+ )
216
+ parser.add_argument("url", help="Naver blog post URL (PC or mobile).")
217
+ parser.add_argument(
218
+ "--no-images", action="store_true",
219
+ help="Exclude image URLs from output.",
220
+ )
221
+ parser.add_argument(
222
+ "--max-length", type=int, default=0,
223
+ help="Maximum content length in characters (0 = unlimited). Default: 0.",
224
+ )
225
+ parser.add_argument(
226
+ "--timeout", type=int, default=20,
227
+ help="HTTP request timeout in seconds. Default: 20.",
228
+ )
229
+ parser.add_argument(
230
+ "--insecure", action="store_true",
231
+ help="Skip SSL certificate verification (use only when certificate errors occur).",
232
+ )
233
+ return parser.parse_args(argv)
234
+
235
+
236
+ def main(argv: list[str] | None = None) -> int:
237
+ args = parse_args(argv or sys.argv[1:])
238
+
239
+ try:
240
+ result = read_blog(
241
+ args.url,
242
+ include_images=not args.no_images,
243
+ max_length=args.max_length,
244
+ timeout=args.timeout,
245
+ insecure=args.insecure,
246
+ )
247
+ except (RuntimeError, ValueError) as error:
248
+ print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
249
+ return 1
250
+
251
+ print(json.dumps(result, ensure_ascii=False, indent=2))
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ raise SystemExit(main())
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import time
9
+ import urllib.parse
10
+ import urllib.request
11
+ from html import unescape
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+ from _naver_http import TAG_RE, urlopen
15
+
16
+ SEARCH_URL = "https://search.naver.com/search.naver"
17
+ DEFAULT_COUNT = 10
18
+ MAX_COUNT = 30
19
+ FIRST_PAGE_START = 1
20
+ RESULTS_PER_PAGE = 15
21
+
22
+ DEFAULT_HEADERS = {
23
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
24
+ "Accept-Language": "ko,en-US;q=0.9,en;q=0.8",
25
+ "User-Agent": (
26
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
27
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
28
+ ),
29
+ }
30
+
31
+ BLOG_ANCHOR_PATTERN = re.compile(
32
+ r'<a[^>]*href="(https?://blog\.naver\.com/([a-zA-Z0-9_]+)/(\d+))"[^>]*>(.*?)</a>',
33
+ re.DOTALL,
34
+ )
35
+
36
+
37
+ def strip_html(text: str) -> str:
38
+ return unescape(TAG_RE.sub("", text)).strip()
39
+
40
+
41
+ def build_search_params(query: str, start: int = FIRST_PAGE_START, sort: str = "sim") -> dict[str, str]:
42
+ return {
43
+ "query": query,
44
+ "ssc": "tab.blog.all",
45
+ "sm": "tab_jum" if start <= FIRST_PAGE_START else "tab_pge",
46
+ "start": str(start),
47
+ "nso": {"sim": "so:r,p:all,a:all", "date": "so:dd,p:all,a:all"}.get(sort, "so:r,p:all,a:all"),
48
+ }
49
+
50
+
51
+ def fetch_search_page(query: str, start: int = 1, sort: str = "sim", timeout: int = 15, *, insecure: bool = False) -> str:
52
+ params = build_search_params(query, start=start, sort=sort)
53
+ url = f"{SEARCH_URL}?{urllib.parse.urlencode(params)}"
54
+ request = urllib.request.Request(url, headers=DEFAULT_HEADERS)
55
+
56
+ try:
57
+ with urlopen(request, timeout, insecure=insecure) as response:
58
+ return response.read().decode("utf-8", "ignore")
59
+ except urllib.error.HTTPError as error:
60
+ raise RuntimeError(
61
+ f"Naver search returned HTTP {error.code}. "
62
+ "The request may have been blocked. Retry later or reduce request volume."
63
+ ) from error
64
+
65
+
66
+ def parse_search_results(html: str) -> list[dict]:
67
+ results: list[dict] = []
68
+ anchors = BLOG_ANCHOR_PATTERN.findall(html)
69
+
70
+ pending: dict[str, dict] = {}
71
+
72
+ for full_url, user_id, post_id, inner_html in anchors:
73
+ if full_url not in pending:
74
+ pending[full_url] = {
75
+ "url": full_url,
76
+ "mobile_url": f"https://m.blog.naver.com/{user_id}/{post_id}",
77
+ "author": user_id,
78
+ "title": "",
79
+ "snippet": "",
80
+ }
81
+
82
+ text = strip_html(inner_html)
83
+ if not text:
84
+ continue
85
+
86
+ entry = pending[full_url]
87
+
88
+ if "headline1" in inner_html or "text-type-headline" in inner_html:
89
+ if not entry["title"]:
90
+ entry["title"] = text
91
+ elif "body1" in inner_html or "text-type-body" in inner_html:
92
+ if not entry["snippet"]:
93
+ entry["snippet"] = text
94
+ else:
95
+ if not entry["title"]:
96
+ entry["title"] = text
97
+
98
+ for entry in pending.values():
99
+ results.append(entry)
100
+
101
+ return results
102
+
103
+
104
+ def search(query: str, count: int = DEFAULT_COUNT, sort: str = "sim", timeout: int = 15, *, insecure: bool = False) -> dict:
105
+ count = max(1, min(count, MAX_COUNT))
106
+ all_results: list[dict] = []
107
+ seen_urls: set[str] = set()
108
+ start = FIRST_PAGE_START
109
+ # 네이버 검색이 페이지당 정확히 RESULTS_PER_PAGE개를 반환하지 않을 수 있으므로 여유 페이지 확보
110
+ max_pages = (count // RESULTS_PER_PAGE) + 3
111
+
112
+ for page_num in range(max_pages):
113
+ if len(all_results) >= count:
114
+ break
115
+
116
+ if page_num > 0:
117
+ time.sleep(0.5)
118
+
119
+ html = fetch_search_page(query, start=start, sort=sort, timeout=timeout, insecure=insecure)
120
+ page_results = parse_search_results(html)[:RESULTS_PER_PAGE]
121
+
122
+ if not page_results:
123
+ if start == 1:
124
+ print("[warn] 검색 결과 파싱 실패. 네이버 HTML 구조가 변경되었을 수 있습니다.", file=sys.stderr)
125
+ break
126
+
127
+ new_count = 0
128
+ for result in page_results:
129
+ if result["url"] not in seen_urls:
130
+ seen_urls.add(result["url"])
131
+ all_results.append(result)
132
+ new_count += 1
133
+ if len(all_results) >= count:
134
+ break
135
+
136
+ if new_count == 0:
137
+ break
138
+
139
+ start += RESULTS_PER_PAGE
140
+
141
+ return {
142
+ "query": query,
143
+ "total_results": len(all_results),
144
+ "results": all_results,
145
+ }
146
+
147
+
148
+ def parse_args(argv: list[str]) -> argparse.Namespace:
149
+ parser = argparse.ArgumentParser(
150
+ description="Search Naver blogs and return structured JSON results."
151
+ )
152
+ parser.add_argument("query", help="Search query string.")
153
+ parser.add_argument(
154
+ "--count", type=int, default=DEFAULT_COUNT,
155
+ help=f"Number of results to return (max {MAX_COUNT}, default {DEFAULT_COUNT}).",
156
+ )
157
+ parser.add_argument(
158
+ "--sort", choices=["sim", "date"], default="sim",
159
+ help="Sort order: sim (relevance) or date (newest first). Default: sim.",
160
+ )
161
+ parser.add_argument(
162
+ "--timeout", type=int, default=15,
163
+ help="HTTP request timeout in seconds. Default: 15.",
164
+ )
165
+ parser.add_argument(
166
+ "--insecure", action="store_true",
167
+ help="Skip SSL certificate verification (use only when certificate errors occur).",
168
+ )
169
+ return parser.parse_args(argv)
170
+
171
+
172
+ def main(argv: list[str] | None = None) -> int:
173
+ args = parse_args(argv or sys.argv[1:])
174
+
175
+ try:
176
+ result = search(
177
+ args.query,
178
+ count=args.count,
179
+ sort=args.sort,
180
+ timeout=args.timeout,
181
+ insecure=args.insecure,
182
+ )
183
+ except RuntimeError as error:
184
+ print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
185
+ return 1
186
+
187
+ print(json.dumps(result, ensure_ascii=False, indent=2))
188
+ return 0
189
+
190
+
191
+ if __name__ == "__main__":
192
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "naver-blog-research",
3
+ "description": "Search Naver blogs, read full post content, and download images using only python3 stdlib — no API key required.",
4
+ "profiles": [
5
+ "lookup"
6
+ ],
7
+ "frontmatter": "name: naver-blog-research\ndescription: Search Naver blogs, read full post content, and download images using only python3 stdlib — no API key required.\nlicense: MIT\nmetadata:\n category: research\n locale: ko-KR\n phase: v1"
8
+ }