fix(scraper): 微信客户抓取三连修(wechatId过滤+完整body+total-aware翻页,6563→6846全量)
- 真机抓包确认过滤字段是 wechatId(原 userId 被无视,每员工拉回部门前1000行) - 补 ymdStart/ymdEnd/friendTypeYunke=1/isDel=0 完整 body(与云客页面一致) - total-aware 翻页:端点分页中途掉短页,翻到声明数满或连续空页(重复行幂等) - 验证:csf 员工声明1999→distinct1846全量到手;云客total含跨页重复水分(1999声明vs1846真实) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -256,34 +256,84 @@ class PageScraper:
|
|||||||
|
|
||||||
|
|
||||||
async def scrape_friends(cookie_str: str, max_employees: int = 100):
|
async def scrape_friends(cookie_str: str, max_employees: int = 100):
|
||||||
"""微信客户:浏览器直发 friend_list/es 部门级全量翻页(2026-08-20 起,
|
"""微信客户:浏览器直发 friend_list/es,**逐员工**(userId 过滤)翻页。
|
||||||
旧导航+拦截被登录墙杀死 0 捕获)。记录自带 owner(wxId),无需逐员工遍历;
|
|
||||||
``max_employees`` 参数保留兼容 CLI,已不使用。"""
|
部门级全量分页排序不稳定(实测:云客声明 8691 vs 到手 distinct 6563,
|
||||||
|
8682 行里 2119 行跨页重复挤占页容量漏抓真客户,2026-08-20);逐员工范围
|
||||||
|
小漂移可忽略,且与云客后台「按员工看客户列表」口径一致。员工清单来自
|
||||||
|
本地 wechat_account(user_id 列)。
|
||||||
|
"""
|
||||||
|
from app.db.base import async_engine
|
||||||
|
from sqlalchemy import text as sa_text
|
||||||
from app.scraper.wechat import _normalize
|
from app.scraper.wechat import _normalize
|
||||||
from app.storage.repos import upsert_customer
|
from app.storage.repos import upsert_customer
|
||||||
|
|
||||||
dept = await _root_dept_id()
|
dept = await _root_dept_id()
|
||||||
base_body = {"departmentId": dept} if dept else {}
|
async with async_engine.connect() as conn:
|
||||||
|
result = await conn.execute(sa_text(
|
||||||
|
"SELECT wx_id FROM wechat_account WHERE wx_id IS NOT NULL AND wx_id != ''"
|
||||||
|
))
|
||||||
|
employees = [row[0] for row in result.fetchall()][:max_employees]
|
||||||
|
|
||||||
|
import datetime as _dt
|
||||||
|
today = _dt.date.today().isoformat()
|
||||||
|
|
||||||
|
total_fetched = 0
|
||||||
|
total_upserted = 0
|
||||||
async with PageScraper(cookie_str) as scraper:
|
async with PageScraper(cookie_str) as scraper:
|
||||||
page = await _open_fetch_page(scraper)
|
page = await _open_fetch_page(scraper)
|
||||||
rows = await _fetch_pages(
|
for i, wx_id in enumerate(employees):
|
||||||
page, INTERCEPT_PATHS["friend_list"],
|
# 真机抓包(2026-08-20 云客客户列表页)确认的过滤参数:wechatId(非
|
||||||
base_body=base_body, parse_rows=_rows_basic_result,
|
# userId!)+ymd 窗口+friendTypeYunke=1+isDel=0
|
||||||
)
|
base_body = {
|
||||||
|
"ymdStart": "0000-00-00",
|
||||||
|
"ymdEnd": today,
|
||||||
|
"wechatId": wx_id,
|
||||||
|
"salesname": "",
|
||||||
|
"friendTypeYunke": "1",
|
||||||
|
"isDel": 0,
|
||||||
|
}
|
||||||
|
# total-aware 翻页:该端点分页不稳(中途偶发短页),按声明 total
|
||||||
|
# 翻满或连续空页才停;跨页重复行 upsert 幂等无害。
|
||||||
|
rows: list[dict] = []
|
||||||
|
declared: int | None = None
|
||||||
|
empty_streak = 0
|
||||||
|
page_num = 1
|
||||||
|
while page_num <= 400:
|
||||||
|
resp = await _browser_fetch(
|
||||||
|
page, INTERCEPT_PATHS["friend_list"],
|
||||||
|
json_body={**base_body, "pageNum": page_num, "pageSize": 50},
|
||||||
|
)
|
||||||
|
basic = ((resp.get("data") or {}).get("basicResult") or {})
|
||||||
|
declared = basic.get("total") or declared
|
||||||
|
page_rows = basic.get("data") or []
|
||||||
|
if not page_rows:
|
||||||
|
empty_streak += 1
|
||||||
|
if empty_streak >= 2 or (declared and len(rows) >= declared):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
empty_streak = 0
|
||||||
|
rows.extend(page_rows)
|
||||||
|
if declared and len(rows) >= declared:
|
||||||
|
break
|
||||||
|
page_num += 1
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
rec = _normalize(row)
|
rec = _normalize(row)
|
||||||
if rec.get("friend_id"):
|
if rec.get("friend_id"):
|
||||||
records.append(rec)
|
records.append(rec)
|
||||||
|
total_fetched += len(records)
|
||||||
|
if records:
|
||||||
|
await _upsert_all(upsert_customer, records)
|
||||||
|
total_upserted += len(records)
|
||||||
|
logger.info(
|
||||||
|
f" [{i+1}/{len(employees)}] {wx_id[:16]}..: declared={declared} "
|
||||||
|
f"rows={len(rows)} (total fetched {total_fetched})"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2 + (i % 3))
|
||||||
|
|
||||||
total_upserted = 0
|
|
||||||
for i in range(0, len(records), 500):
|
|
||||||
await _upsert_all(upsert_customer, records[i:i + 500])
|
|
||||||
total_upserted += len(records[i:i + 500])
|
|
||||||
|
|
||||||
total_fetched = len(records)
|
|
||||||
logger.info(f"DONE: friends fetched={total_fetched}, upserted={total_upserted}")
|
logger.info(f"DONE: friends fetched={total_fetched}, upserted={total_upserted}")
|
||||||
return total_fetched, total_upserted
|
return total_fetched, total_upserted
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user