| name | naver-blog-importer |
| description | Use this skill when the user wants to import, crawl, or bulk-convert Naver Blog posts (blog.naver.com or m.blog.naver.com) into local Markdown files. Triggers include any mention of "네이버 블로그 가져오기", "블로그 크롤링", "naver blog export", a blog.naver.com URL, or a blogId. Do NOT use for single-shot content reading — use web_fetch for that. This skill is for bulk export workflows where the user wants every post of a blog saved as .md files with images downloaded alongside. |
Naver Blog Importer
Bulk-import a Naver Blog's posts into local Markdown files with images downloaded to an assets/ folder alongside each post.
When to use
- User wants to export their own (or any) Naver Blog to Markdown
- User has a
blog.naver.com/{blogId} URL and wants all posts pulled down
- User mentions feeding a Naver Blog into a wiki / Obsidian vault / static site
When NOT to use
- Single post reading → use
web_fetch with the m.blog.naver.com URL directly
- Tistory, Medium, or other blog platforms → this skill is Naver-specific
Setup (first run only)
Before running the script for the first time, check that Python deps are installed:
python3 -c "import requests, bs4, markdownify" 2>/dev/null
If that exits non-zero, install them silently — do NOT ask the user, just install:
pip install -r "$(dirname "$0")/requirements.txt" \
|| python3 -m pip install --user requests beautifulsoup4 markdownify
(Use the requirements.txt shipped alongside this SKILL.md when possible. Fall back to --user install if pip lacks permissions.)
After install, re-check the import. If it still fails, surface the error to the user. Otherwise proceed silently — do not announce "installed deps", just continue with the actual task.
Invocation
When invoked via slash (/naver-blog-importer <args>), the args are the same as the CLI flags below — pass them straight to scripts/import_blog.py. If the user gives a full URL (e.g. blog.naver.com/myblog or https://blog.naver.com/myblog/22312345), extract the blogId before calling the script.
When invoked via natural language ("내 네이버 블로그 백업해줘"), ask for the blogId if not provided, then choose flags based on what they said:
- "처음 N개만" / "딱 N개" →
--limit N
- "목록만" / "어떤 글이 있는지만" →
--list-only
- "특정 카테고리" →
--category-no N (ask for N)
- "다시 받아" / "강제로" →
--force
After the run, report counts (ok / skipped / failed) and the output directory path.
Quick start
python scripts/import_blog.py <blogId> --list-only
python scripts/import_blog.py <blogId> --out ./naver-import
python scripts/import_blog.py <blogId> --category-no 7 --out ./naver-import
python scripts/import_blog.py <blogId> --start 0 --limit 50
blogId is the part after blog.naver.com/ — e.g. for blog.naver.com/myblog the id is myblog.
What it does
- Discover posts — hits
https://blog.naver.com/PostTitleListAsync.naver (the internal AJAX endpoint Naver itself uses for pagination) to enumerate every logNo on the blog. Paginates until exhausted.
- Fetch each post — uses the mobile URL
https://m.blog.naver.com/{blogId}/{logNo} which returns real HTML (the desktop URL wraps content in an iframe and returns an empty shell).
- Parse content — handles both SmartEditor 2.0 (
.se-main-container) and the legacy editor (#postViewArea / .post_ct). Extracts title + body only.
- Download images — pulls every image to
assets/{logNo}/img_NNN.{ext}, strips Naver's ?type=w80 resize params to get originals, rewrites markdown references to relative paths.
- Convert to Markdown — uses
markdownify for clean output. Preserves headings, bold/italic, lists, blockquotes, code blocks, links.
- Write file —
{yyyy-mm-dd}-{slugified-title}.md (date comes from the post metadata in the listing response).
Output layout
<out-dir>/
├── 2024-03-15-포스트제목.md
├── 2024-03-18-다른글.md
└── assets/
├── 223123456789/
│ ├── img_001.jpg
│ └── img_002.png
└── 223123456790/
└── img_001.jpg
Filenames use the original Korean title slugified (spaces → -, special chars stripped). If two posts collide, logNo is appended.
Resume and idempotency
The script is idempotent — if a .md file for a given logNo already exists in the output directory, it skips. To force re-import, delete the file or pass --force.
Progress is logged to stdout. On failure mid-run, re-running picks up where it stopped.
Rate limiting
Default is 1 request/second with random 0.3–0.8s jitter. Naver tolerates this fine for a personal blog (~hundreds of posts). For larger blogs, lower concurrency not higher — don't add threading, you'll get soft-banned.
Known limitations
- Embedded Naver maps / place cards / book cards → rendered as plain text with the card's visible label. Not worth parsing the full widget DOM.
- Video embeds (Naver TV, YouTube) → kept as links, not embedded
- Sticker/emoticon images → downloaded like regular images (you can delete the
assets/*/sticker_*.gif after the fact if you want)
- Private posts / neighbor-only posts → skipped with a warning (no auth support)
- Comments / 이웃커넥트 → not fetched (by design, per requirements)
Troubleshooting
- Empty body extracted — the post probably uses a very old editor or is a reblog. Check the raw HTML at
https://m.blog.naver.com/{blogId}/{logNo} manually.
- Listing stops early — Naver caps
PostTitleListAsync at ~30 pages sometimes. Use --category-no to iterate category-by-category instead.
- Images 403 — rare, happens on very old posts. The script logs and continues; image reference in MD will be broken but text is intact.
Files
scripts/import_blog.py — main entry point, CLI
scripts/fetch.py — URL normalization, listing pagination, post fetching
scripts/parse.py — SmartEditor / legacy editor parsing, markdown conversion
scripts/images.py — image URL normalization + download