From 730a93613749690ffe05040e9188b76788dc2d65 Mon Sep 17 00:00:00 2001 From: Hoelee Date: Sun, 13 Sep 2026 23:30:21 +0800 Subject: [PATCH] Add scraping bot-walled marketplace warm-browser-session deep-dive (en + zh + OG) --- ...alled-marketplace-warm-browser-session.png | Bin 0 -> 47270 bytes scripts/og-gen/generate.mjs | 21 ++- ...walled-marketplace-warm-browser-session.md | 165 ++++++++++++++++++ ...walled-marketplace-warm-browser-session.md | 165 ++++++++++++++++++ 4 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 public/og/scraping-bot-walled-marketplace-warm-browser-session.png create mode 100644 src/content/posts/scraping-bot-walled-marketplace-warm-browser-session.md create mode 100644 src/content/posts/zh/scraping-bot-walled-marketplace-warm-browser-session.md diff --git a/public/og/scraping-bot-walled-marketplace-warm-browser-session.png b/public/og/scraping-bot-walled-marketplace-warm-browser-session.png new file mode 100644 index 0000000000000000000000000000000000000000..002aac621ae38e453b89b8c092e884161e5c924a GIT binary patch literal 47270 zcmeFYRZyH!vo1_RAVPrP?iSqLf?I&#F2UUfw;=?AyL*De;O_1W?jGC*Cb%<)kZ=F{ zuXDAp&efixYF54Uvghqy-RtR|kS_{S$nWsq!NI{H%SekW!@<3(fP;JK`sU@+lMZqS z($m)qfU=Y*T-hk$J{%k=oQ$}Ls(U(Q#Y(#Sp1?KC>u(T)16@OuYtdkolp;dkOMwKzv9GF2D-~t(W8%qto z(coFu@0)qvi?5Si0avc=M-Yg>pJrEA*GNy#{FXed zxeLQHGFGH$q32ZiHtlnccgGq0ZV9$TwnLY*%|vbqM5Xmh$IDjMjWdf*^*OB%`cH#f z_dj@vV|IEdA4)z=TLtUZtIo-62Eu$vA`zbl?(o#o`-J<1;%9t`oz({~;T6l+dXDA$ zyc2U%KwQ<$=nB7N+3@Zkz4V4AA>hR^~Nr?S&@fe7< zVa5~Wc6aJ{#hQ%wAFciK6TjredX&72DeUeTSif~ulqAnb1j$&@lT4;!|F^yG6Tgw( zEb$#<&pp)Zt`=C5K_G<7LE(UH+`Xb;zYots#D?lPAKvvxPMj50wO-_&3&X67pi2sn zQG*T!At*`ScnF$qz9mG-?-lDX8Y(-6%!~fs7=HJd65?m^r)(7-}AZtzM6wUqI3!dhW|gktl>C zV?7kaTym@bS!0pN7l~QN^39&B0_YmIKx+We0zdzI7Z>8e%}7@gHoM$Iies_X8p-b{ zuCB*oKT-riw7caT+?V%pL@8tU-g+WAp8hTIt4C(``n%Yp(D7>q)&j}eld5!yR)Kf$ z*ro%w$-w8SDp0=v{P9Op?%_$#_GJw1cel1Whz=m@xr~=n2kT#t6)sW4o$oGfZz5n3 zXP1@L)$A(1UJW6lT_0P2-a%cy8C65!p9lI`k0^rt)m9~=EDAD^Id;VwrpzeFNKmh)$7wfe<1lmdbyzgB|4n|z?cMR>d1$PU7-7A%4 z#C&@)gNm;$VN_IZ$IGlzW?J6x*sd^P&(z>K)aHNK?onBzzDYQ;ji|4E&ot!XL~zWe z?lb*`0(lqZp|(J}_1j)FnHW}RKEQnJoVVHva|Rs&EoNM}5)fk1r1?plGKt%o8hYTZ znOJ>Xj*yW7$1E4c%w-%Y4wy5|yvYaMttH$Rw}2TZ$Qk#u>xqSFn$13H*=xMlyw(?) z*&Y>&o*T+FUzI!M9zJiq^w1W9eKw|9JuP-!-|SOWUd&a>y%27cZDW^IeS49}@^ zb0*EEpAlZRvDgFnHgigwgg{Odok+&*$4>no`LZ??b8CbsjbWv z9{Xk3;RTw~K06__vE#I70e8xk4))3Cwz9l|GRXx5Nihsp(%IjV#|yE1c1U=V+O0>$ zMKm_`nG`>;n2!y%#7nai(L4i|+_m^R!BUw{Qx0-OxYA$@`J65~-nr@vu2>hz@!=Fc zu&~#7D9FDJ3K9^wjn+a8sfm0kAoCjsOEx92hIJq*SXwDb1lAz%d}eWda_8@ znf+n?5g-2ULq8{XAKs|!mX{93!*X!2wl#oj#2{MXZ1%{5pEg<1?!{YLsp7yfB1-aX z+U7YbgtL&~R9R&Wh85l8cDuE*TtlI^T?T9Q5X0-{CRfAd(6Cwih?6b!m?|c(qv11D zrAc!V+lbRGCm)QumZZcx?Qdlx1Y?~mSM_7Bkyg@)ijH;{hpPMO%VF8{jBYmK7Mi~c z*CSEHiRuZ;(_!WlR~J{)=`T``h%k&-Z*uvo@=NT;y^g})gZkbmXtisNT7Z*jVNe=h_)R=c>V30} z>cyDAyT>C6V_l-)Mc+hBGc$dcH_2=?M)zX6udUhvr99B<6#Y@Ezd;Y&L-(Tf8~fT- zGe$>8r|N0|i!Ty|t}Y31o~DcclQoC5cs+1`Ik4oiUuY{{USD}mTzrd{bv5WCt7Z=w zd0q{j8qBJynqbyUt|;FS=-HIwRAsclz)ViAoS56kAYD#c%+tbqKW4uD)DN4uUPmpA zo10b}O!oBuGSv~JWyx{zR=P5`X9I(EDPtG-WBV@7dyrz{n?hdz-vjR2}L z6&I&`uf;m{Ug`ktQgkmFMeAh0>nxUEtc4n8H&|1-sDgs(!RxBJ5Ia>*u5IG4GB_UcLr)*3H-fp12!^ui%_p-Zmz(B8fPlWT})T7Frq!SxYi+h+bB))@dQNX~aP+;u8pT?`ohu@9)xpZjAeRP6 zlnecaYH#to5;jgQW@#{iyE-2iMc$g znY@&K7<6rJk*Kws{D+kc4yJ%dfEeH=L{aFrsV4WfIqoK_UE`y*n+Jzc)lYVj0Tb@q z8>8dheX6wt>337rk(h^RH@Qm@Fv1&w)~)8)gGmal0ucXz3)o#PGcc~(J7cLFi`Y}R zy`u2;uBAK*BBV6CC17PWKv8VpzXF^!5#m&P@Nv6L@|aoM_3_!tDK|N~m#JJK%WL}e zZNj&^C=W8#EyYIP&Rlf=6icbp+IG`O3D9z|y-0IYEgU~l*j`iHrwZKHO4(*SDqF?y z2_D#w35KgJm3dkm%EO&@^WpWM;2=+~^*vIUo7wKMGV>}aK9fV^y`}o4%;1#-PSvCS z%;(XUBr9mNf2~o|ul99-c=&wO;lLC=a_BO`R0}oukFQN%L%*5(og!AK1chWX4_;>Q zMp9zp5~E=0JNtWRQ%aS*GF@yTIn%GTK55|>XiDEH+kc$@NWah+ORYd1R9bywztb+i zJ8x_A@G2t%oxrMH)gZHUPnH&!Kwf;%xEa4HbThTNFVoU4{4kwAW_SuaIeLQL)%-j6 z(a|x7MqQxGZ1joLZHmKhzK=|gcm2mWZ*23D%4((@Vl;D%#$-4gF_pwDGqulp4T{64 z+y>OHgB9KUt$S&_&^fjfWR{T4elNO7xHZEX$Xi9W_;jEA;~PwrU;If(#WSn+_qw`^ zpEwRGKhcmoAzczFIOo)qRMN-^W5?G!n}%5CUQeKjpAKzg4qqIjn)_N%<79ONh0Atu zdA20MbL!KnTjwn&$6ZWBmXuos^Q=Zie6@{OlpSWGw+x=wII3zOV?5%!pCve@&cR=jZ03=>rxBBZ>e8n$;%7V zCkSFFEZa2jj_RDof!B*eLAypJN~6z1PL8YCPLb* z53892C*ND$f=6qZQIb=C_z0Vv`_X~itXvp}8%0T_n{#JR$Z0`wi~C1ERN-gC;I*z} zbFTx6^Tjv;*L2|CqRURw61BAjbrhL5PIhzmjdwP`ZCRI;|Fp8hQt+}zDr>x-aQa@| zS`8+>mUR_`_*q_w-{`b-l64d>HJ@@9tGooc4#hYk~=iC;=nSPuI4NC#_zB-L4#5 z(^stB2a~xj^xwTh1YQ+uV`{mi;?>)g&^7u$offBmQbndQ)Fs?BW(Ug6FqBqj5Xp&) z=!gr-xO`vtmCk2G>;Qn7n6>T9QYNvNDBW0I>K&ai!MXvRzfu|bRwPe@lK}}ld>?xKUWKC3o0({JmuOiHt0g>`OK@xWU#UA=9sMwjFivr6L*WA1;?R?D_ck zG$Z699P{$EeK&yza>q+)Eoi0jBgo>Ej78R=(4zP8a$}dJnI�>I1WiNTlQnpXTw= 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
H^pXCfFPX*P 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 F`d_->s%8KH literal 0 HcmV?d00001 diff --git a/scripts/og-gen/generate.mjs b/scripts/og-gen/generate.mjs index c7fc581..7852a43 100644 --- a/scripts/og-gen/generate.mjs +++ b/scripts/og-gen/generate.mjs @@ -110,15 +110,20 @@ const TERMINALS = {
$enhance_prompt:false · same-origin paths→ 1 room out ✓
`, 'how-i-made-my-own-songs-with-suno-ai': ` -
$suno "indie folk · playful whistles · sprite-girl vocals"
-
 lyrics + metatags → 7 songs shipped ✓
-
$v6: section edits · single-line swaps · voices
`, +
$suno "indie folk · playful whistles · sprite-girl vocals"
+
 lyrics + metatags → 7 songs shipped ✓
+
$v6: section edits · single-line swaps · voices
`, - 'passbolt-hang-three-failure-modes': ` -
$curl -I https://pass.hoelee.com
-
 504 · ERR_TOO_MANY_REDIRECTS · fingerprint null
-
$mount passbolt.php · full fingerprint→ fixed ✓
`, - }; + 'passbolt-hang-three-failure-modes': ` +
$curl -I https://pass.hoelee.com
+
 504 · ERR_TOO_MANY_REDIRECTS · fingerprint null
+
$mount passbolt.php · full fingerprint→ fixed ✓
`, + + 'scraping-bot-walled-marketplace-warm-browser-session': ` +
$CDP → shopee search "used ssd"
+
 /verify captcha · empty product cards
+
$warm session · 7s pacing · sweep.py→ 20+ listings ✓
`, +}; const DEFAULT_TERMINAL = `
$engineering · devops · self-hosting
diff --git a/src/content/posts/scraping-bot-walled-marketplace-warm-browser-session.md b/src/content/posts/scraping-bot-walled-marketplace-warm-browser-session.md new file mode 100644 index 0000000..5b7d987 --- /dev/null +++ b/src/content/posts/scraping-bot-walled-marketplace-warm-browser-session.md @@ -0,0 +1,165 @@ +--- +title: "Scraping a Bot-Walled Marketplace With a Warm Browser Session" +description: "How I scraped Shopee's bot-walled listings with a warm headless-Chrome session over CDP — fail-fast health checks, split-wait rendering, and why I refused to parallelize it." +pubDate: 2026-09-13 +category: engineering +tags: ["scraping", "cdp", "headless-chrome", "anti-bot", "shopee", "python"] +ogImage: "/og/scraping-bot-walled-marketplace-warm-browser-session.png" +draft: false +--- + +I set out to do something that sounds simple: list all the second-hand M.2 NVMe SSDs on a marketplace, sorted by price, so I could buy the cheapest one. + +The result was nothing like a clean `requests` + BeautifulSoup script. It was a week-long fight against an anti-bot wall, a warm browser session held together with duct tape, and — the part I didn't expect — a hard lesson about why you cannot just "parallelize" a scraper by throwing more agents at it. + +This is that story, told in the order I lived it: the problem, everything I tried that failed, the thing that finally worked, and what I'd do differently. + +## The problem, phrased the way someone would actually search it + +> "How do I scrape product listings from a site that blocks headless browsers?" + +Or, more honestly: **how do you scrape a marketplace that has already made scraping its sole job description?** Shopee doesn't politely serve HTML to `curl`. It detects automation, throws a `/verify` captcha, and silently serves you a page that looks fine but contains nothing. + +My requirement list was small: + +- Search for "used SSD", filter to "used condition only", filter to M.2 NVMe (not SATA/mSATA), filter to ≥128 GB. +- Extract every listing's capacity variants and prices. +- Sort by price ascending. +- Do it without a `chromedriver` Farm of a hundred headless instances, because I don't own a hundred residential IPs. + +Everything after this point is the debugging story. + +## What I tried, and why it failed + +### Attempt 1: cold headless Chrome via CDP — "just point Chrome's remote debugger at it" + +I launched Chrome with `--headless --remote-debugging-port=9222`, connected with the Chrome DevTools Protocol, and navigated to a search URL. It felt clever for about ten seconds. + +**Failure:** the page loaded, but every product card was empty. The DOM was a shell. Shopee had fingerprinted the headless browser (no real GPU, no real user-agent fingerprint consistency, `navigator.webdriver` flag) and redirected me into a `/verify` captcha loop I never got past. Cold headless profiles hit the wall immediately. + +The lesson is boring but important: **anti-bot systems don't care how good your selectors are if the browser itself is lying about what it is.** + +### Attempt 2: parallel subagents — "just spawn N workers" + +This was the one I really wanted to work. I have a multi-agent setup. The natural instinct is: spawn five agents, each scraping a different search query, gather the results, merge. + +**Failure:** all of them attach to the *same* browser session. They don't get their own browser — they get their own *tab* in one shared Chrome instance, or worse, they all fight over the same single tab. Every `Page.navigate` un-does the previous agent's context. And critically: + +1. **The warm session is a shared resource.** There is one Canary profile that isn't flagged. Five agents navigated the same tab in an interleaved order, which is indistinguishable — to Shopee — from one browser doing rapid-fire navigation. That's exactly the behavior that *triggers* the wall. +2. **Anti-bot pacing is cumulative.** My working setup depends on ~7-second gaps between navigations to look human. Five agents navigating concurrently means five navigations hitting the same session with effectively zero gap. + +So the "horizontal scaling" instinct — more workers — is actively hostile to the one thing keeping me alive: looking human. The concurrency ceiling here isn't CPU or memory. It's **one warm browser, one tab at a time.** + +### Attempt 3: trusting the "Buy With Voucher" button for the price + +Once I had listings rendering, I assumed the buy button's label contained the price. On most marketplaces it does. On this render, the button said `Add To Cart` / `Buy Now`. My regex for "Buy With Voucher … RM…" matched nothing, and I went around in circles for a while before actually reading the DOM. + +**Failure:** the price on Shopee product pages isn't in the button. It's rendered as **split spans** — `RM` in one element, `84` in another, `00` in a third — so a naive leaf-text match returns fragments, or a **range** (`RM125.00 - RM155.00`) when the listing has multiple variants. + +### Attempt 4: clicking variant options to get an exact price + +For a single-axis variant selector (e.g. one dropdown of capacities — 128GB / 256GB / 512GB), clicking one option collapses the price to a single value, and my extraction worked. + +**Failure:** for a **two-axis** selector (brand × capacity), clicking the first axis (a brand) locks a price, but adding the second axis (a capacity) *un-collapses* it back to the product-wide range. The panel renders the range of all purchasable combinations, not the exact combination I selected, and the "selected" state is marked by a class that also matches every other option box. There is no reliable way to know which combination I actually locked. + +I spent real time on this before accepting the honest answer: **for genuinely two-axis listings, the correct data is the price range, not a guessed per-combination number.** Shipping a fragile override that "sometimes works" would be worse than reporting an honest range. + +## The fix — what actually worked + +Three pieces, all small, all learned the hard way. + +### 1. A warm, human-paced browser session (the foundation) + +Don't fight the wall. Use a browser profile that has already logged in and browsed normally — a "warm" profile. Navigate with real 7-second gaps. It is slower and it is the only thing that reliably survives. + +```python +# not real code — the *shape* of it. Navigate, then wait like a human. +goto_url(target) +time.sleep(7) # human pacing, non-negotiable +wait_for_selector(".shopee-search-item-result__item") +``` + +### 2. Fail fast, don't time out + +Before any scrape, hit the CDP HTTP endpoint and check the session state. If Canary is down or the tab is sitting on `/verify`, abort in ~1 second instead of wasting 40 seconds on a "did not render" timeout. + +```python +import json, urllib.request + +def session_healthy(): + try: + tabs = json.load(urllib.request.urlopen("http://127.0.0.1:9222/json")) + except Exception: + return False + for t in tabs: + if t.get("type") == "page" and "/verify" in t.get("url", ""): + return False # captcha wall — re-warm the session, don't scrape + return True +``` + +This one change turned "silent 40-second dead-ends" into "instant, actionable failure." + +### 3. The right price selector: deepest pure-RM node + +The buy button is a lie. The price is the **deepest element whose text is a pure RM value** — a range (`RM125.00 - RM155.00`) before a variant is selected, a single value (`RM84.00`) after. Walk the DOM and take the deepest element whose text matches: + +```python +import re + +RM = re.compile(r"^RM\s?[\d,.]+(\s?[-–]\s?RM\s?[\d,.]+)?$") + +def find_price(root): + best, depth = None, -1 + for el in root.querySelectorAll("*"): + txt = (el.textContent or "").strip() + if txt and RM.match(txt) and " " not in txt.split("RM")[0]: + d = depth_of(el) + if d > depth: + best, depth = el, d + return best.textContent.strip() if best else None +``` + +That's the whole trick. No brittle class names. No guessing. A node whose entire text *is* a price is the price. + +## The result — one quantified outcome + +From a single warm session, in one serial pass, I extracted **20+ used-NVMe listings with per-variant prices**, filtered to M.2 NVMe ≥128GB, sorted by price. The cheapest real target — a 256GB M.2 NVMe — surfaced at **RM84**, alongside several 256GB options in the RM125–175 range. One command (`sweep.py ...`) now re-checks the whole shortlist and dumps JSONLines. + +But the number I'm most glad about is this: **zero captchas.** The warm-session + human-pacing + fail-fast combo survived the entire sweep without tripping `/verify` once. + +## What I'd do differently + +1. **I'd write the fail-fast health check first**, before a single scraping line. It's thirty seconds of work that saves forty seconds per future failure. +2. **I'd resist the parallelization instinct day one.** The answer to "the scraper is slow" is "bundle more work into one serial pass" (batch search, `sweep.py` over many URLs), not "more workers." Shared browser state makes concurrency worse, not better. +3. **I'd accept the two-axis limitation immediately** and report ranges, instead of burning an afternoon on a selector that can't be made reliable. +4. **I'd stop guessing DOM structure and probe it.** Almost every mistaken assumption (`/i.` vs `-i.` in the URL path, `Buy With Voucher` as the price anchor, "the filter is a real `` checkbox") came from *not reading the actual DOM first*. + +## The reusable pieces + +All of this became a small, standalone toolkit — each script is one self-contained `python x.py ...` call: + +| Script | What it does | +|---|---| +| `search.py` | Coarse search → listing cards | +| `used_search.py` | Clicks the "Used" condition checkbox, verifies `USED_ITEM` in the URL, extracts | +| `detail.py` | Pulls per-variant prices from a product page (deepest-RM-node method + split-wait) | +| `sweep.py` | Batch detail over N URLs in one session, dedupes by item-id, writes JSONLines, prints a table | +| `cdp_common.py` | Shared health check + navigation helpers | + +The genuinely reusable lesson isn't the Shopee-specific selectors — it's the *posture*: **assume the page is lying about its DOM, fail fast, and scale by batching, not by parallel workers.** + +--- + +*This is a real debugging session, not a tutorial written after the fact. The scraping toolkit and every one of these failures happened while actually hunting for a cheap SSD — I kept the parts that generalize and cut the parts that only matter to one Malaysian marketplace at one point in time.* + +--- + +## Want this for your business? + +I build custom scraping and data-extraction pipelines — warm-session CDP scrapers, bot-wall-aware crawlers, and batch price-monitoring sweeps — as well as full-stack web apps (PHP/CodeIgniter, Java/Spring, React/TypeScript) and self-hosted automation (n8n, Telegram bots, Docker on NAS). + +If you need product or price data off a site that fights back, tap through and tell me what you're trying to extract: + +- 💬 [WhatsApp](https://wa.me/60127972969) — message me directly +- ✉️ [Email](mailto:me@hoelee.com?subject=Custom%20scraping%20pipeline) — tell me the site + the data +- 🌐 [hoelee.com](https://hoelee.com) — see what else I build \ No newline at end of file diff --git a/src/content/posts/zh/scraping-bot-walled-marketplace-warm-browser-session.md b/src/content/posts/zh/scraping-bot-walled-marketplace-warm-browser-session.md new file mode 100644 index 0000000..6c48808 --- /dev/null +++ b/src/content/posts/zh/scraping-bot-walled-marketplace-warm-browser-session.md @@ -0,0 +1,165 @@ +--- +title: "用「热浏览器会话」爬取有反爬墙的电商网站" +description: "我如何用热 headless-Chrome 会话通过 CDP 爬取 Shopee 的反爬商品列表——失败即止的健康检查、分段等待渲染,以及我为什么拒绝并行化处理。" +pubDate: 2026-09-13 +category: engineering +tags: ["scraping", "cdp", "headless-chrome", "anti-bot", "shopee", "python"] +ogImage: "/og/scraping-bot-walled-marketplace-warm-browser-session.png" +draft: false +--- + +我一开始想做一件听起来很简单的事:把一个电商网站上所有的二手 M.2 NVMe SSD 都列出来,按价格排序,好让我买到最便宜的那一个。 + +结果完全不是一段干净的 `requests` + BeautifulSoup 脚本。它变成了一场长达一周的、跟反爬墙的拉锯战,一个靠胶带勉强粘在一起的热浏览器会话,以及——最让我意外的部分——一个关于「你不能靠堆更多 agent 来『并行化』爬虫」的惨痛教训。 + +这就是那个故事,按照我经历的顺序来讲:问题、我试过却失败的每一件事、最终奏效的办法,以及下次我会怎么做。 + +## 问题,用别人真正会搜索的方式表述 + +> "如何从一个拦截无头浏览器的网站爬取商品列表?" + +或者更诚实地说:**当一个电商网站已经把「防爬」当成它唯一的本职工作,你要怎么爬它?** Shopee 不会礼貌地把 HTML 喂给 `curl`。它会检测自动化、抛出一个 `/verify` 验证码,然后静默地给你一个「看起来正常但里面什么都没有」的页面。 + +我的需求清单很短: + +- 搜索「二手 SSD」,只筛选「二手」,只筛选 M.2 NVMe(排除 SATA/mSATA),只筛选 ≥128 GB。 +- 提取每个商品的所有容量规格和价格。 +- 按价格升序排序。 +- 不搞一百个 headless 实例的 `chromedriver` 农场,因为我没有一百个住宅 IP。 + +从这里开始,全部都是调试故事。 + +## 我试过的,以及为什么失败 + +### 尝试 1:通过 CDP 用冷的无头 Chrome——「直接把 Chrome 远程调试开起来」 + +我用 `--headless --remote-debugging-port=9222` 启动了 Chrome,用 Chrome DevTools Protocol 连上去,然后导航到搜索 URL。大约十秒钟里我觉得自己很聪明。 + +**失败:** 页面加载了,但每个商品卡片都是空的。DOM 是个空壳。Shopee 指纹识别了这个无头浏览器(没有真正的 GPU、user-agent 指纹不一致、`navigator.webdriver` 标志),把我重定向进了一个我永远过不去的 `/verify` 验证码循环。冷的无头 profile 一上来就撞墙。 + +教训很无聊但很重要:**如果你的浏览器本身在「身份」上撒谎,选择器写得再好也没用。** + +### 尝试 2:并行子 agent——「直接开 N 个 worker」 + +这是我最想让它成功的方案。我有一套多 agent 的架构。本能反应就是:开五个 agent,每个爬一个不同的搜索词,收集结果,合并。 + +**失败:** 它们全都连到**同一个**浏览器会话上。它们不会各自拿到一个浏览器——只会各自拿到共享 Chrome 实例里的一个标签页,或者更糟:全都挤在同一个标签页上。每一次 `Page.navigate` 都会把上一个 agent 的上下文抹掉。而且关键在于: + +1. **热会话是共享资源。** 只有一个没被标记的 Canary profile。五个 agent 交错访问同一个标签页,在 Shopee 眼里,跟一个浏览器疯狂快速导航没有区别。而这恰恰是**触发**反爬墙的行为。 +2. **反爬的「节奏」是累积的。** 我的可用方案依赖导航之间约 7 秒的间隔来显得像真人。五个 agent 并发导航,就等于五个导航以零间隔打向同一个会话。 + +所以「横向扩展」的本能——加更多 worker——对我赖以存活的那件事(看起来像真人)是有害的。这里的并发上限不是 CPU 也不是内存,而是**一个热浏览器,一次一个标签页。** + +### 尝试 3:靠「Buy With Voucher」按钮来拿价格 + +一旦列表能渲染出来了,我假设购买按钮的文案里带着价格。大多数电商网站确实如此。但这个渲染出来的按钮写的是 `Add To Cart` / `Buy Now`。我用来匹配「Buy With Voucher … RM…」的正则什么都匹配不到,我在原地打转了好一会儿,才真正去读了 DOM。 + +**失败:** Shopee 商品页的价格不在按钮里。它被渲染成**拆分的 span**——`RM` 在一个元素里、`84` 在另一个里、`00` 又在另一个里——所以朴素的叶子文本匹配只会抓到碎片,或者当商品有多个规格时,抓到一个**区间**(`RM125.00 - RM155.00`)。 + +### 尝试 4:点击规格选项来拿精确价格 + +对于单轴规格选择器(比如一个容量下拉:128GB / 256GB / 512GB),点击一个选项会把价格收敛成单个值,我的提取方法能正常工作。 + +**失败:** 对于**双轴**选择器(品牌 × 容量),点击第一个轴(某个品牌)会锁定一个价格,但加上第二个轴(某个容量)又把它**解散**回商品全区间。这个面板渲染的是「所有可购买组合」的区间,而不是我选中的那个具体组合;而且「已选中」状态是用一个「同时匹配其他所有选项框」的 class 来标记的。我没法可靠地知道我到底锁定了哪个组合。 + +我在这个上面花了实打实的时间,最后才接受这个诚实的答案:**对于真正的双轴商品,正确的数据是价格区间,而不是一个瞎猜的「每个组合」数字。** 发布一个「有时候能用」的脆弱覆盖方案,比老实报告一个区间更糟糕。 + +## 修复——真正奏效的东西 + +三块,都很小,都是吃了亏才学会的。 + +### 1. 热的人性化节奏的浏览器会话(地基) + +别跟墙硬刚。用一个已经登录过、正常浏览过的浏览器 profile——一个「热」profile。用真实的 7 秒间隔来导航。它更慢,但它是唯一能稳定活下来的方式。 + +```python +# 不是真正的代码——是它的「形状」。先导航,然后像真人一样等待。 +goto_url(target) +time.sleep(7) # 真人节奏,不可妥协 +wait_for_selector(".shopee-search-item-result__item") +``` + +### 2. 失败即止,别傻等超时 + +在任何爬取之前,先打 CDP 的 HTTP 端点,检查会话状态。如果 Canary 挂掉了、或标签页停在 `/verify` 上,就在约 1 秒内中止,而不是在「怎么还不渲染」的超时里浪费 40 秒。 + +```python +import json, urllib.request + +def session_healthy(): + try: + tabs = json.load(urllib.request.urlopen("http://127.0.0.1:9222/json")) + except Exception: + return False + for t in tabs: + if t.get("type") == "page" and "/verify" in t.get("url", ""): + return False # 撞到验证码墙——重新热会话,别爬了 + return True +``` + +就这么一个改动,把「静默的 40 秒死胡同」变成了「即时、可执行的失败」。 + +### 3. 正确的价格选择器:最深的纯 RM 节点 + +购买按钮是个谎言。价格是**那个文本内容纯粹是一个 RM 值的、最深的元素**——在选中规格之前是一个区间(`RM125.00 - RM155.00`),之后是单个值(`RM84.00`)。遍历 DOM,取文本能匹配上的最深元素: + +```python +import re + +RM = re.compile(r"^RM\s?[\d,.]+(\s?[-–]\s?RM\s?[\d,.]+)?$") + +def find_price(root): + best, depth = None, -1 + for el in root.querySelectorAll("*"): + txt = (el.textContent or "").strip() + if txt and RM.match(txt) and " " not in txt.split("RM")[0]: + d = depth_of(el) + if d > depth: + best, depth = el, d + return best.textContent.strip() if best else None +``` + +这就是全部的诀窍。没有脆弱的 class 名。没有瞎猜。一个「整个文本就是一个价格」的节点,就是价格本身。 + +## 结果——一个可量化的成果 + +从单个热会话、一次串行遍历中,我提取了 **20+ 个二手 NVMe 商品及其各规格价格**,筛选到 M.2 NVMe ≥128GB,按价格升序排序。最便宜的真实目标——一块 256GB M.2 NVMe——以 **RM84** 的价格浮出水面,另外还有几块 256GB 的选项在 RM125–175 区间。现在一条命令(`sweep.py ...`)就能重新检查整份候选清单,并输出 JSONLines。 + +但我最庆幸的数字是这个:**零验证码。** 「热会话 + 人性化节奏 + 失败即止」这套组合,在整次遍历中一次都没触发过 `/verify`。 + +## 下次我会怎么做 + +1. **我会一开始就写「失败即止」的健康检查**,在任何爬取代码之前。三十秒的活,换来未来每次失败省四十秒。 +2. **我会从第一天就抵抗并行化的冲动。** 「爬虫太慢」的答案是把更多工作打包进一次串行遍历(批量搜索、对多个 URL 跑 `sweep.py`),而不是「再加 worker」。共享的浏览器状态让并发变得更糟,而不是更好。 +3. **我会立刻接受双轴的限制**,老实报告区间,而不是在一个根本没法可靠化的选择器上烧一个下午。 +4. **我会别再猜 DOM 结构,先去探测它。** 几乎每一个错误假设(URL 路径里 `/i.` vs `-i.`、把 `Buy With Voucher` 当价格锚点、「过滤器是个真正的 `` 复选框」)都来自**没有先读真实的 DOM**。 + +## 可复用的部分 + +所有这一切被做成了一个小的独立工具集——每个脚本都是一次自包含的 `python x.py ...` 调用: + +| 脚本 | 作用 | +|---|---| +| `search.py` | 粗粒度搜索 → 商品卡片 | +| `used_search.py` | 点击「二手」条件复选框,校验 URL 里的 `USED_ITEM`,再提取 | +| `detail.py` | 从商品页提取各规格价格(最深 RM 节点法 + 分段等待) | +| `sweep.py` | 一次会话内对 N 个 URL 做批量详情提取,按 item-id 去重,写 JSONLines,打印表格 | +| `cdp_common.py` | 共享的健康检查 + 导航辅助 | + +真正可复用的教训不是那些 Shopee 专属的选择器——而是**姿态**:**假设页面在 DOM 上撒谎,失败即止,用批量化而不是并行 worker 来扩展。** + +--- + +*这是一次真实的调试过程,不是事后补写的教程。这个爬虫工具集、以及这里的每一次失败,都发生在我真的为了买一块便宜 SSD 而爬取的时候——我保留了能泛化的部分,砍掉了那些只对某个马来西亚电商网站、某个时间点有效的部分。* + +--- + +## 想为你的业务做这个吗? + +我做定制爬虫和数据提取管道——热会话 CDP 爬虫、反爬墙感知的采集器、批量价格监控扫描——也做全栈 Web 应用(PHP/CodeIgniter、Java/Spring、React/TypeScript)和自托管自动化(n8n、Telegram 机器人、NAS 上的 Docker)。 + +如果你需要从某个「会反抗」的网站拿商品或价格数据,直接点进来,告诉我你想提取什么: + +- 💬 [WhatsApp](https://wa.me/60127972969) — 直接私信我 +- ✉️ [Email](mailto:me@hoelee.com?subject=Custom%20scraping%20pipeline) — 告诉我网站和数据 +- 🌐 [hoelee.com](https://hoelee.com) — 看看我还做了些什么 \ No newline at end of file