Korean e-commerce is one of the richest, least-scraped data sources on the web. K-beauty and K-fashion drive billions in global demand, but almost every tutorial you find scrapes Amazon or Shopify — because the Korean platforms sit behind a double wall: the language, and some genuinely annoying anti-bot setups.
I spent a while getting reliable, structured data out of the three that matter most — Olive Young (K-beauty), Musinsa (K-fashion), and Naver Place (local businesses + reviews). Here's what actually worked, the parts that fought back, and the rules I set for myself.
Rule #1: never parse the HTML if the site talks JSON to itself
Every one of these is a JavaScript SPA. If you GET the page and run Cheerio over it, you get a shell. The trick is the same everywhere: open the network tab, reload, and watch what the frontend calls to fill itself in. That call is almost always a clean JSON endpoint — the same data the page renders, minus the parsing pain and minus 90% of the breakage when they reskin the site.
Musinsa was the friendliest. One endpoint powers both category rankings and keyword search:
GET https://api.musinsa.com/api2/dp/v1/plp/goods
?gf=A&sortCode=POPULAR&caller=CATEGORY&category=001&page=1&size=60
Swap caller=CATEGORY&category=001 for caller=SEARCH&keyword=<term> and you've got search. The response hands you goodsNo, brandName, normalPrice, finalPrice, saleRate, reviewCount, reviewScore — everything, already structured. No auth. (Their reviewScore is 0–100, so divide by 20 if you want a 0–5 rating.)
Olive Young was two services once I switched to their global storefront (global.oliveyoung.com) — which is actually better for most people, because it returns English product names and USD prices instead of Korean + KRW:
GET product-ranking-service.oliveyoung.com/v1/pages/ranking/sales/products # best sellers
POST cbe-external-api.oliveyoung.com/display/v1/search/products/unified-search # search
The domestic site (www.oliveyoung.co.kr) 403'd my datacenter IP immediately; the global one didn't. Worth checking both when a Korean site blocks you — the international storefront is often more permissive.
Naver Place was the stubborn one. The clean data lives in the Apollo cache embedded in the search results HTML:
GET https://search.naver.com/search.naver?where=nexearch&query=강남역 카페
Pull the __APOLLO_STATE__ blob out of the page, and you get PlaceListBusinessesItem nodes with name, category, address, phone, rating, review count — plus a few review snippets linked from each place. The deeper per-place review API (pcmap-api.place.naver.com/graphql) exists, but it CAPTCHA-walls datacenter IPs with an HTTP 405. More on that below.
The parts that fought back
Datacenter IPs get filtered. All three tolerate a light touch from a normal IP but clamp down on cloud ranges fast. The fix is boring: residential proxies (Korea-geo for Naver especially). What matters in code is that a hung proxy tunnel shouldn't take down the whole run — wrap the fetch so a proxy timeout falls back to a direct connection instead of throwing. That one change turned a lot of silent zero-result runs into successful ones.
"Succeeded with 0 results" is a lie you tell yourself. My first Naver cloud run reported success and returned nothing, with no error in the logs — because the code swallowed a proxy timeout and exited cleanly. If a run collects nothing, throw. A paid data tool that silently returns empty is worse than one that fails loudly.
Pagination isn't always real pagination. Naver's search page repeats results after ~15 businesses; true depth requires the GraphQL route (and a residential IP). Know where your easy path ends so you don't ship something that looks paginated but isn't.
Rule #2: public, non-personal data only
This is the line I don't cross, and I'd encourage anyone scraping reviews to hold it too: I never collect reviewer identity. Review objects carry text, rating, date, and keywords — never nicknames, profile URLs, or user IDs. Business phone numbers and addresses are fine (they're public business info); a person's name attached to a review is not. It keeps the data useful for sentiment/market analysis without turning into a privacy problem.
The output that makes it usable
Whatever the source shape, everything normalizes to one flat, snake_case contract:
{
"source": "oliveyoung",
"product_id": "GA230518746",
"name": "SKIN1004 Madagascar Centella Hyalu-Cica Water-Fit Sun Serum",
"brand": "SKIN1004",
"price_usd": 28.0,
"sale_price_usd": 22.4,
"rating": 4.9,
"review_count": 9958,
"rank": 1,
"url": "https://global.oliveyoung.com/product/detail?prdtNo=GA230518746",
"scraped_at": "2026-07-07T09:35:33+09:00"
}
Consistent field names and KST timestamps across all three sources mean you can dump them into the same table and diff prices over time without writing per-site glue.
If you'd rather not maintain any of this
I packaged all three as ready-to-run scrapers on the Apify Store — they handle the proxying, the fallbacks, and the JSON normalization, and they're priced per result so you only pay for what you pull:
- Olive Young (K-beauty, USD prices): https://apify.com/kdatafactory/oliveyoung-scraper
- Musinsa (K-fashion): https://apify.com/kdatafactory/musinsa-scraper
- Naver Place (businesses + reviews): https://apify.com/kdatafactory/naver-place-scraper
But honestly, the endpoints above are the whole trick — if you just need a one-off pull, the network tab will get you most of the way. Happy to answer questions on any of the three in the comments.
Top comments (0)