DEV Community

Session zero
Session zero

Posted on

Korean Fashion Data at Scale: Scraping Musinsa Rankings and Product Details with Python

1. Why Musinsa? The K-Fashion Intelligence Hub

The Platform in Numbers

Musinsa (무신사) isn't just Korea's largest fashion platform — it's the authority for Korean streetwear and contemporary fashion:

  • 22M+ MAU (monthly active users) as of 2025
  • 7,000+ brands listed, from global giants to independent Korean designers
  • Real-time ranking updated hourly across 100+ categories
  • #1 fashion destination for Korean consumers aged 15–35
  • $3B+ GMV annually, growing 30%+ year-over-year

Unlike luxury platforms, Musinsa's sweet spot is accessible fashion — the streetwear, sneakers, and Korean contemporary styles that define what young Koreans are actually wearing. This makes its data uniquely valuable for trend analysis.

Why Global Brands Are Watching

K-fashion has gone global. International buyers at trade shows in Paris and New York now track Musinsa rankings to identify emerging Korean brands before they go mainstream. Brands like Ader Error, Wooyoungmi, and Juun.J — now stocked in Dover Street Market and Selfridges — built their initial following on Musinsa.

For researchers, the platform represents:

  • A leading indicator of global streetwear trends (what's hot in Seoul today reaches Tokyo in 3 months, Los Angeles in 6)
  • Price benchmarking data across categories with consistent structure
  • Brand performance tracking — which Korean labels are gaining traction vs. declining
  • Demand signals for wholesale and investment decisions

2. What Data is Available on Musinsa

Musinsa structures its data across four main data types, each accessible via its public web interface:

2.1 Ranking Data

The crown jewel of Musinsa for trend researchers. Available at:
https://www.musinsa.com/main/musinsa/ranking

Each ranking entry contains:

- Rank position (current + previous)
- Brand name (Korean + romanized)
- Product name
- Current price (KRW)
- Original price (before discount)
- Discount percentage
- Thumbnail image URL
- Product page URL
- Category tag
- Gender target (men/women/unisex)
Enter fullscreen mode Exit fullscreen mode

Category depth: Rankings are available for 100+ sub-categories including:

  • Top, Pants, Outerwear, Shoes, Accessories
  • Brand-specific rankings (Top 100 by brand)
  • Age/gender filtered rankings

2.2 Product Detail Pages

Individual product pages at https://www.musinsa.com/app/goods/{product_id} contain:

- Full product description
- All price variants (size/color options)
- Stock availability per variant
- Accumulated sales count ("판매량")
- Review count + average rating
- Brand information + brand store link
- Related products
- Size guide
- Material composition
- Origin (Korean domestic vs. import)
Enter fullscreen mode Exit fullscreen mode

2.3 Brand Information

Each brand has a dedicated store page at https://www.musinsa.com/store/{brand_id}:

- Brand name + logo
- Brand description + founding year
- Total product count
- Follower count (brand popularity signal)
- Brand ranking position
- Representative product images
- Price range (min-max)
Enter fullscreen mode Exit fullscreen mode

2.4 Search Results

Musinsa's search endpoint supports rich filtering:

- Keyword search
- Category filter
- Price range filter
- Brand filter  
- Gender filter
- Sort by: popularity, newest, price (asc/desc), review count
Enter fullscreen mode Exit fullscreen mode

3. Technical Approach: Why Musinsa is Scraper-Friendly

Unlike many e-commerce platforms that use heavy client-side JavaScript rendering and aggressive bot detection, Musinsa takes a different approach.

Server-Side Rendering (SSR)

Musinsa's product and ranking pages are primarily server-side rendered (SSR). This means:

  1. Product data is embedded in the initial HTML response — no need to execute JavaScript
  2. Standard HTTP requests with appropriate headers return complete page content
  3. Playwright or Selenium are optional, not required for most use cases
  4. Response times are fast (< 1 second per page)

Verification (tested 2026-03-04):

GET https://www.musinsa.com/main/musinsa/ranking
→ HTTP 200 OK (SSR HTML, ranking data in page source)
Enter fullscreen mode Exit fullscreen mode

Low Bot Detection

Compared to platforms like Coupang (which uses Akamai Bot Manager and returns 403 for basic requests), Musinsa's bot detection is relatively permissive:

  • No CAPTCHA challenges for standard browsing patterns
  • Rate limiting is loose (reasonable request intervals are sufficient)
  • Standard requests library with realistic headers works for most pages
  • Proxy rotation recommended for large-scale extraction but not required for testing

Recommended Stack

# For ranking + product pages (SSR)
requests + BeautifulSoup (Cheerio equivalent in Python)

# For search with AJAX pagination (if needed)
Playwright (optional fallback)

# For large scale
Apify SDK + Apify Proxy (automatic IP rotation)
Enter fullscreen mode Exit fullscreen mode

4. Quick Start with the Apify Actor

The fastest way to get Musinsa data is the Musinsa Scraper Apify actor — no code required.

→ Try it now: https://apify.com/oxygenated_quagmire/musinsa-scraper

(Actor #13 — link will be active once development is complete)

What the Actor Does

The actor handles all the complexity for you:

  • Pagination through ranking pages (1,000+ items across all categories)
  • Product detail enrichment (click-through to individual pages)
  • Consistent JSON output structure
  • Automatic retry on transient failures
  • IP rotation via Apify Proxy

Input Schema

{
  "mode": "RANKING",
  "category": "all",
  "gender": "all",
  "maxItems": 100
}
Enter fullscreen mode Exit fullscreen mode

Mode options:

  • RANKING — Extract current ranking by category
  • PRODUCT — Scrape specific product pages
  • BRAND — Extract brand information + product list
  • SEARCH — Keyword search with filters

Example Output (Ranking Mode)

[
  {
    "rank": 1,
    "previousRank": 3,
    "rankChange": 2,
    "productId": "1234567",
    "productName": "오버핏 워싱 데님 재킷",
    "productNameEn": "Oversized Washed Denim Jacket",
    "brandName": "커버낫",
    "brandNameEn": "Covernat",
    "currentPrice": 129000,
    "originalPrice": 179000,
    "discountRate": 27,
    "currency": "KRW",
    "category": "아우터",
    "gender": "unisex",
    "imageUrl": "https://image.musinsa.com/...",
    "productUrl": "https://www.musinsa.com/app/goods/1234567",
    "scrapedAt": "2026-03-04T15:00:00+09:00"
  }
]
Enter fullscreen mode Exit fullscreen mode

Pricing

  • $0.50 per 1,000 items (Pay-per-result)
  • Free tier: $5/month credit = 10,000 items free
  • Typical ranking run (TOP 100 all categories): ~$0.05

5. Full Python Pipeline

For developers who want full control, here's a complete pipeline that extracts Musinsa rankings and enriches them with product details.

Part 1: Ranking Extractor

import requests
from bs4 import BeautifulSoup
import json
import time
from datetime import datetime

class MusinsaRankingScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'Referer': 'https://www.musinsa.com/',
        })
        self.base_url = "https://www.musinsa.com/main/musinsa/ranking"

    def get_ranking(self, category="all", gender="all", page=1):
        """Extract ranking page for given category and gender."""
        params = {
            'storeCode': 'musinsa',
            'categoryCode': category,
            'gf': gender,
            'page': page
        }

        try:
            response = self.session.get(self.base_url, params=params, timeout=10)
            response.raise_for_status()
            return self._parse_ranking_page(response.text)
        except requests.RequestException as e:
            print(f"Request failed: {e}")
            return []

    def _parse_ranking_page(self, html):
        """Parse ranking HTML and extract product data."""
        soup = BeautifulSoup(html, 'html.parser')
        products = []

        # Find all ranking list items
        items = soup.select('li.list-item')

        for item in items:
            try:
                rank_elem = item.select_one('.rank-num')
                product_elem = item.select_one('.list-item__name')
                brand_elem = item.select_one('.list-item__brand')
                price_elem = item.select_one('.price')
                original_price_elem = item.select_one('.price-original')
                discount_elem = item.select_one('.discount-rate')
                link_elem = item.select_one('a[href*="/app/goods/"]')
                img_elem = item.select_one('img.lazy')

                if not all([rank_elem, product_elem, brand_elem]):
                    continue

                # Extract product ID from URL
                product_url = link_elem['href'] if link_elem else ''
                product_id = product_url.split('/')[-1].split('?')[0] if product_url else ''

                # Parse prices
                current_price = self._parse_price(price_elem.text if price_elem else '0')
                original_price = self._parse_price(original_price_elem.text if original_price_elem else '0')
                discount_rate = int(discount_elem.text.replace('%', '').strip()) if discount_elem else 0

                product = {
                    'rank': int(rank_elem.text.strip()),
                    'productId': product_id,
                    'productName': product_elem.text.strip(),
                    'brandName': brand_elem.text.strip(),
                    'currentPrice': current_price,
                    'originalPrice': original_price or current_price,
                    'discountRate': discount_rate,
                    'currency': 'KRW',
                    'productUrl': f"https://www.musinsa.com{product_url}" if product_url.startswith('/') else product_url,
                    'imageUrl': img_elem.get('data-original', '') if img_elem else '',
                    'scrapedAt': datetime.now().isoformat()
                }
                products.append(product)

            except (AttributeError, ValueError) as e:
                print(f"Parse error for item: {e}")
                continue

        return products

    def _parse_price(self, price_str):
        """Convert price string like '129,000원' to integer."""
        return int(''.join(filter(str.isdigit, price_str))) if price_str else 0

    def get_full_ranking(self, category="all", gender="all", max_pages=5):
        """Extract multiple pages of rankings."""
        all_products = []

        for page in range(1, max_pages + 1):
            products = self.get_ranking(category=category, gender=gender, page=page)

            if not products:
                break

            all_products.extend(products)
            print(f"  Page {page}: {len(products)} products extracted")
            time.sleep(0.5)  # Polite delay

        return all_products


# Usage
scraper = MusinsaRankingScraper()
ranking = scraper.get_full_ranking(category="all", gender="all", max_pages=3)
print(f"Total: {len(ranking)} products extracted")
Enter fullscreen mode Exit fullscreen mode

Part 2: Product Detail Enricher

class MusinsaProductScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'ko-KR,ko;q=0.9',
        })

    def get_product_details(self, product_id):
        """Extract detailed information for a specific product."""
        url = f"https://www.musinsa.com/app/goods/{product_id}"

        try:
            response = self.session.get(url, timeout=10)
            response.raise_for_status()
            return self._parse_product_page(response.text, product_id)
        except requests.RequestException as e:
            print(f"Failed to fetch product {product_id}: {e}")
            return None

    def _parse_product_page(self, html, product_id):
        """Parse product detail page."""
        soup = BeautifulSoup(html, 'html.parser')

        # Extract JSON-LD if available (structured data)
        json_ld = soup.find('script', type='application/ld+json')
        structured_data = {}
        if json_ld:
            try:
                structured_data = json.loads(json_ld.string)
            except json.JSONDecodeError:
                pass

        # Extract key product info
        title_elem = soup.select_one('.product_title')
        brand_elem = soup.select_one('.product_title .brand')
        review_count_elem = soup.select_one('.review-count')
        rating_elem = soup.select_one('.star-rating-text')
        sales_elem = soup.select_one('.sales-count')
        description_elem = soup.select_one('#product_info')

        return {
            'productId': product_id,
            'fullTitle': title_elem.text.strip() if title_elem else '',
            'brandName': brand_elem.text.strip() if brand_elem else '',
            'reviewCount': self._extract_number(review_count_elem.text if review_count_elem else '0'),
            'averageRating': float(rating_elem.text.strip()) if rating_elem else 0.0,
            'salesCount': self._extract_number(sales_elem.text if sales_elem else '0'),
            'description': description_elem.text.strip()[:500] if description_elem else '',
            'structuredData': structured_data,
            'detailScrapedAt': datetime.now().isoformat()
        }

    def _extract_number(self, text):
        """Extract integer from text like '1,234개'."""
        digits = ''.join(filter(str.isdigit, text))
        return int(digits) if digits else 0

    def enrich_rankings(self, ranking_products, max_products=50, delay=0.3):
        """Enrich ranking data with product details."""
        product_scraper = MusinsaProductScraper()
        enriched = []

        for i, product in enumerate(ranking_products[:max_products]):
            product_id = product.get('productId')
            if not product_id:
                enriched.append(product)
                continue

            print(f"  [{i+1}/{min(len(ranking_products), max_products)}] Enriching product {product_id}...")
            details = product_scraper.get_product_details(product_id)

            if details:
                merged = {**product, **details}
                enriched.append(merged)
            else:
                enriched.append(product)

            time.sleep(delay)

        return enriched


# Full pipeline
ranking_scraper = MusinsaRankingScraper()
product_scraper = MusinsaProductScraper()

# Step 1: Get rankings
print("Extracting Musinsa TOP 100 rankings...")
ranking = ranking_scraper.get_full_ranking(max_pages=2)

# Step 2: Enrich top 20 with details
print(f"\nEnriching top 20 products with details...")
enriched = product_scraper.enrich_rankings(ranking, max_products=20)

# Step 3: Save to file
with open('musinsa_ranking_enriched.json', 'w', encoding='utf-8') as f:
    json.dump(enriched, f, ensure_ascii=False, indent=2)

print(f"\n✅ Complete! {len(enriched)} products saved to musinsa_ranking_enriched.json")
Enter fullscreen mode Exit fullscreen mode

Part 3: Scheduled Price Monitor

import schedule
import time
import csv
from pathlib import Path

def monitor_price_changes(product_ids, output_file='musinsa_price_history.csv'):
    """Track price changes over time for specific products."""

    scraper = MusinsaProductScraper()
    fieldnames = ['timestamp', 'productId', 'productName', 'brandName', 
                  'currentPrice', 'originalPrice', 'discountRate']

    # Initialize CSV if not exists
    if not Path(output_file).exists():
        with open(output_file, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()

    print(f"Checking prices for {len(product_ids)} products...")
    rows = []

    for product_id in product_ids:
        details = scraper.get_product_details(product_id)
        if details:
            rows.append({
                'timestamp': datetime.now().isoformat(),
                'productId': product_id,
                'productName': details.get('fullTitle', ''),
                'brandName': details.get('brandName', ''),
                'currentPrice': details.get('currentPrice', 0),
                'originalPrice': details.get('originalPrice', 0),
                'discountRate': details.get('discountRate', 0),
            })
        time.sleep(0.3)

    with open(output_file, 'a', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writerows(rows)

    print(f"{len(rows)} records saved at {datetime.now().strftime('%Y-%m-%d %H:%M')}")


# Watch list — top Korean streetwear items
WATCH_LIST = ['1234567', '2345678', '3456789']  # Replace with actual product IDs

# Run every 6 hours
schedule.every(6).hours.do(monitor_price_changes, product_ids=WATCH_LIST)

# Run once immediately
monitor_price_changes(WATCH_LIST)

# Keep running
while True:
    schedule.run_pending()
    time.sleep(60)
Enter fullscreen mode Exit fullscreen mode

6. Real Use Cases

Use Case 1: Fashion Trend Intelligence for Brands

The Problem: A European fashion brand wants to understand which silhouettes, colors, and styles are trending in Korea before deciding on their next collection direction.

The Approach:

  1. Extract Musinsa TOP 200 rankings weekly across all categories
  2. Parse product names and descriptions to identify recurring style keywords
  3. Track which brands/products are entering vs. leaving the top 100
  4. Correlate ranking movements with seasonal events and Korean fashion weeks

Sample Analysis:

import pandas as pd
from collections import Counter

# Load weekly ranking snapshots
df = pd.read_json('musinsa_weekly_rankings.json')

# Keyword frequency in product names
all_words = ' '.join(df['productName'].tolist()).split()
word_freq = Counter(all_words)

# Trending brands (appeared in TOP 100 for first time this week)
new_entries = df[df['previousRank'] > 100]
print("New entrants to TOP 100:", new_entries['brandName'].value_counts().head(10))

# Average discount rate by category
print(df.groupby('category')['discountRate'].mean().sort_values(ascending=False))
Enter fullscreen mode Exit fullscreen mode

Business Value: Validated $15K+ trend research reports replaced by automated weekly pipeline costing ~$2/week.


Use Case 2: Price Tracking and Discount Alert System

The Problem: A Korean fashion reseller wants to buy Musinsa items during sales and resell them internationally at a profit.

The Approach:

  1. Define a watchlist of 200 products across categories
  2. Monitor prices every 6 hours
  3. Alert via Telegram/Slack when discount rate exceeds threshold (e.g., > 40%)
  4. Track price history to identify seasonal patterns (biggest sales: 11.11, Chuseok, end-of-season)

Discount Alert Bot:

import httpx
import asyncio

async def check_discounts(watchlist, threshold=40):
    """Alert on deep discounts."""
    scraper = MusinsaProductScraper()
    alerts = []

    for product_id in watchlist:
        product = scraper.get_product_details(product_id)
        if product and product.get('discountRate', 0) >= threshold:
            alerts.append({
                'product': product['fullTitle'],
                'brand': product['brandName'],
                'original': product['originalPrice'],
                'sale': product['currentPrice'],
                'discount': product['discountRate']
            })

    if alerts:
        message = "🔥 Musinsa Deep Discount Alert!\n\n"
        for a in alerts:
            message += f"{a['brand']}{a['product']}\n"
            message += f"{a['original']:,} → ₩{a['sale']:,} ({a['discount']}% off)\n\n"
        # Send to Telegram, Slack, etc.
        print(message)

    return alerts


asyncio.run(check_discounts(WATCH_LIST, threshold=40))
Enter fullscreen mode Exit fullscreen mode

Business Value: Automated deal detection replacing 2-3 hours of daily manual monitoring.


Use Case 3: Brand Benchmarking for K-Fashion Labels

The Problem: An emerging Korean fashion brand (50K Instagram followers) wants to understand how they rank against competitors and where their pricing sits in the market.

The Approach:

  1. Extract all products for 10 competitor brands via brand scraper
  2. Compare price distributions, discount frequencies, and product category mix
  3. Track ranking trajectories over 30, 60, 90 days
  4. Identify which product types drive the highest rankings for competitors

Competitive Dashboard Data:

# Brand comparison matrix
brands = ['covernat', 'apair', 'nnine', 'solew']  # Musinsa brand IDs
brand_data = {}

for brand_id in brands:
    products = scraper.get_brand_products(brand_id)
    brand_data[brand_id] = {
        'product_count': len(products),
        'avg_price': sum(p['currentPrice'] for p in products) / len(products),
        'top_ranked': min(p['rank'] for p in products if p.get('rank')),
        'categories': Counter(p['category'] for p in products).most_common(3)
    }

df = pd.DataFrame(brand_data).T
print(df.to_markdown())
Enter fullscreen mode Exit fullscreen mode

Business Value: Replaces $5K/month brand intelligence subscription with automated scraper.


Use Case 4: Inventory and Restocking Monitor

The Problem: A Musinsa reseller needs to know when out-of-stock items (especially limited edition drops) come back in stock.

The Approach:

  1. Check product pages for stock status every 15 minutes during anticipated restocking windows
  2. Monitor new arrivals for specific brands during Musinsa's "New Season" drops
  3. Automate purchase triggers (with care for ToS compliance)

Restocking Notifier:

def check_restock(product_id, target_sizes=['M', 'L', 'XL']):
    """Check if a sold-out product is back in stock."""
    product = scraper.get_product_details(product_id)

    if not product:
        return False

    available_sizes = product.get('availableSizes', [])
    restocked = [s for s in target_sizes if s in available_sizes]

    if restocked:
        print(f"🚨 RESTOCKED: {product['fullTitle']}")
        print(f"   Available in: {', '.join(restocked)}")
        return True
    return False


# Check every 10 minutes for limited items
schedule.every(10).minutes.do(check_restock, 
    product_id='LIMITED_EDITION_ID', 
    target_sizes=['M', 'L'])
Enter fullscreen mode Exit fullscreen mode

Business Value: Automated monitoring replacing manual F5-refreshing during limited drops.


7. What's Next: The Korean E-Commerce Data Ecosystem

Musinsa is the fashion layer of a rich Korean e-commerce data ecosystem. Once you've built your fashion intelligence pipeline, the natural extensions are:

Connect to Price Comparison Platforms

Track the same products across Musinsa, 29CM, and W Concept to identify price differentials and arbitrage opportunities. Our naver-place-search scraper can help you find offline retail locations carrying the same brands.

Layer in Consumer Sentiment

Combine Musinsa ranking data with our Naver Blog Reviews scraper to understand why products rank highly — not just that they do. Korean fashion bloggers write extensively about Musinsa purchases.

Track Cultural Context with Naver News

Fashion trends don't happen in a vacuum. Use the Naver News scraper to correlate Musinsa ranking movements with fashion coverage, influencer mentions, and K-drama costume placements.

Build the K-Commerce Intelligence Stack

For full market coverage:

Data Need Apify Actor
Fashion rankings & products Musinsa Scraper ← This guide
Fashion & lifestyle reviews Naver Place Reviews
Consumer opinion & blog posts Naver Blog Reviews
News & brand mentions Naver News Scraper
K-pop merch demand Melon Chart Scraper
C2C fashion resale market Bunjang Market Scraper
Book/entertainment tie-ins YES24 Book Scraper

The full stack gives you a 360° view of Korean consumer behavior — from what they're listening to (Melon) to what they're reading about (Naver News) to what they're buying (Musinsa, Bunjang).


Wrapping Up

Musinsa's combination of SSR rendering, rich structured data, and relatively low bot detection makes it one of the most accessible major Korean e-commerce platforms for data extraction. With a ~$0.50/1K items price point via the Apify actor, you can run a full weekly ranking extraction for less than $0.10.

Whether you're a trend researcher, price intelligence analyst, or brand strategist, Musinsa data gives you a direct window into the pulse of Korean fashion — in real time.

Get started:


All code examples are for educational purposes. Always review a platform's Terms of Service and robots.txt before scraping. Use reasonable request delays and avoid overloading servers.

Part of the "Korean Web Data" series — scraping guides for Korea's unique digital ecosystem.

Top comments (0)