ð èªåæçš¿ã3æ¬éè€ããæ¥ïŒæåå€å®ã®åœé°æ§ãè§£åãã
æ10äžã®å€§åŠçãæãæã¡ã§60äžã«ä¹ããäŒç€Ÿéœåã§è§£éãããŠãŒãã«ãªããå幎ãã㊠Claude Code ã®èªåŸç°å¢ãçµã¿äžããŠæå120äžãŸã§æ»ããŸããããã®éçšã§åŠãã ããšã¯ãèªååã®æåäœéšãã§ã¯ãªããèªååãå£ãããšãã«äœãèµ·ãããã®ç©ã¿äžãã§ãã
2026幎8æ28æ¥ãInstagram ã®åãã«ã«ãŒã»ã«ã3æ¬äžŠã³ãŸãããã·ã§ãŒãã³ãŒã DckjvpPoFRtã»DckN-yYkIBsJã»DclaPYooPjBââ13:58ã17:55ã21:54 JSTããžã§ãã¯3åãšããæçš¿ããããã£ãŒã«ã«èŠã€ãããªãããšèšé²ããŠããŸããããInstagram API ãçŽæ¥ç¢ºèªãããš3åãšãæçš¿ã¯æç«ããŠããŸããã
æåããŠããã®ã«ã倱æããšèª€å€å®ããââåœé°æ§ïŒfalse negativeïŒ ãå¯äœçšã3åç¹°ãè¿ããäºæ ã§ãã
ãªããã®ä»çµã¿ãå¹ãã®ã
ãç°å¢ãäœãããšããã®ã¯ãèªåãæäœããåæ°ããŒãã«è¿ã¥ããããšã§ãã
Instagram èªåå㯠launchd ãå®å»ã«ã¹ã¯ãªãããåŒã³åºãããã£ãã·ã§ã³çæããæçš¿ã»çå°ç¢ºèªã»å°åž³æžã蟌ã¿ãŸã§ã人éã®é¢äžãªãã«å®èµ°ããŸããå
šäœã§çŽ171æ¬ã®ãžã§ãã䞊走ããŠãããInstagram ãžã®ã«ã«ãŒã»ã«æçš¿ã¯ãã®1ã¬ãŒã³ã§ããã仿¥æçš¿ã§ããããããããã確èªãã«ããå¿
èŠã¯ãªããå°åž³ã® ok:true ãš Discord ã®éç¥ãããã代ããã«äŒããŸãã
ããããäœæ¥ãã§ãªããç°å¢ãã§ããçç±ã¯ããªãã©ã€ãç°å¢ã倿ããããã§ãã
çå°ç¢ºèªïŒ_verify_landed()ïŒã ok:false ãè¿ãã°ã次ã®å®æå®è¡ã§åããžã§ããèµ°ããŸãã人éãããã倱æããŠããã ããããããäžåºŠããšæåã§ãããããã«ãã¹ã±ãžã¥ãŒã©ãèªåã§åå®è¡ãããæéãåŽåããŒãã§ãââå€å®ãæ£ãããã°ã
å€å®ãééã£ãŠãããšãããã®ä»çµã¿ã¯éæ¹åã«åããŸããæçš¿ã¯æåããŠããã®ã« ok:false ãåºç¶ãããšãã¹ã±ãžã¥ãŒã©ã¯ããŸã æçš¿ã§ããŠããªãããšè§£éããŠåãå
容ãå床éä¿¡ããŸããå¯äœçšïŒæçš¿ïŒãæžãã§ããã«ãããããããæ€èšŒå±€ã ãããæªå®æœããšèšã匵ãããã®ç¶æ
ã§ãªãã©ã€ãèµ°ãã°ãåãæçš¿ãäœæ¬ã§ãç©ã¿äžãããŸãã
ãæ¢ãŸãããããå¢ãããã»ããæªã
åœéœæ§ïŒfalse positiveïŒââãå®ã¯å€±æããŠããã®ã«æåãšèª€å€å®ãããââã¯åºè·ã©ã€ã³ãæ¢ããŸããããã¯èŠãã倱æã§ãã誰ããæ°ã¥ããŠèª¿ã¹ãŸãã
åœé°æ§ïŒfalse negativeïŒââãå®ã¯æåããŠããã®ã«å€±æãšèª€å€å®ãããââã¯åºè·ãå¢ãããŸããããã¯èŠããªã倱æã§ããå°åž³ã«ã¯ ok:false ã䞊ã³ã衚åãã¯ãæçš¿ãé
ããŠãããç¶æ
ã«èŠããŸããå®éã«ã¯åãæçš¿ãéç£ãããŠããŠããã¹ã¯ãªããã¯æ·¡ã
ãšæ¬¡ã®ãªãã©ã€ãäºçŽãç¶ããŸãã
å¯äœçšã®ããæäœã«å¯Ÿããåœé°æ§ã¯ããæ€èšŒã®å€±æãã§ã¯ãªããå¯äœçšã®éè€ãã被害ã§ãã ééãããã³ã«çŸå®äžçã«çè·¡ãå¢ããŸããInstagram ã®å Žåããããã£ãŒã«ã«åäžã«ã«ãŒã»ã«ã3æ¬äžŠã¶ãšãã圢ã§ã
åŠç¿ããŒãã«ã¯ããèšé²ãããŠããŸããã[[false-positive-stops-the-line]] ã¯ã誀æ€ç¥ãåºè·ãæ¢ãããåãã£ããããã£ã¡ã¯èª€æ€ç¥ãåºè·ãå¢ãããæ¢ãŸãããæªããã
ç°å¢ãå¹ãæ¡ä»¶
171æ¬ã®ãžã§ããå¹çããåãã®ã¯ãåãžã§ãããèªåã®ä»äºãçµãã£ããã©ããããæ£ç¢ºã«å€å®ã§ããŠããããã§ããå€å®ãæ£ç¢ºãªããæåãããžã§ãã¯1åã§æžã¿ã倱æãããžã§ãã ããåå®è¡ãåãåããŸãã
å€å®ãå£ãããšããã®åæã厩ããŸããæåãããžã§ããã倱æãæ±ãã§åå®è¡ãåãåããŸããå¯äœçšãåªçïŒäœåºŠãã£ãŠãåãçµæïŒãªæäœãªãåé¡ãããŸããââããSNS æçš¿ã¯åªçã§ã¯ãããŸããã1åéãã°1æ¬å ¬éããã3åéãã°3æ¬å ¬éãããŸãã
ããªãã©ã€åæ°ãäœåã«ããããããå
ã«ãããã®æäœã¯åªçãããåããªããšãããªãã åªçã§ãªãæäœã«3åã®ãªãã©ã€ãä»ããããšã¯ãå¯äœçšãæå€§3å蚱容ãã宣èšãšåãã§ãããªãã©ã€èšèšã®æ£å
žïŒretry-and-giveup-design.mdïŒããåã倱æã3åæããŠãçµæã¯å€ãããªãããšèšãã®ãåãæèã§ãââåé¡ããå€å®ã®ææãã®ãã®ãã«ããå Žåãåæ°ãå¢ãããŠãäœãå€ãããŸããã
å šäœã®æµã
äºæ åœæ¥ã®åããã·ãŒã±ã³ã¹ã§è¿œããŸãã
[launchd: 13:58 JST]
â
âŒ
ig_autopost.py
â
ââ (1) ãã£ãã·ã§ã³çæã»æçš¿å®è¡
â IG ã«ã«ãŒã»ã«éä¿¡ âââââââââââââ⺠æå â (DckjvpPoFRt)
â
ââ (2) çå°ç¢ºèª: _verify_landed()
â
ââ ãããã£ãŒã« DOM ååŸ
â ãã£ãã·ã§ã³å
é 12æå: "ðŸ å人éçºã®éç£..."
â IG ã¬ã³ããªã³ã°: <img alt="ðŸ"> â inner_text ã«åºãªã
â ç
§åæåå(DOMåŽ): " å人éçºã®éç£ v" â ãºã¬ã
â
ââ ç
§åNG â ok: false â å°åž³: ãæçš¿ãããŠããªãã
â
âŒ
[launchd: 17:55 JST] â ã¹ã±ãžã¥ãŒã©ããŸã æªæçš¿ããšå€æã»åå®è¡
â
ââ åããã£ãã·ã§ã³ã§æçš¿ ââââââââââââââ⺠éè€1æ¬ç® (DckN-yYkIBsJ)
ââ _verify_landed() â ok: false â ãŸãåœé°æ§
â
âŒ
[launchd: 21:54 JST] â ããã«åå®è¡
ââ æçš¿ âââââââââââââââââââââââââââââââ⺠éè€2æ¬ç® (DclaPYooPjB)
ââ _verify_landed() â ok: true â ããããæåå€å®
8æéã§3æ¬ããã¹ãŠåäžå 容ã®ã«ã«ãŒã»ã«ãã¹ã¯ãªããã¯3åãšããæ£ããåããããšããèªèã®ãŸãŸçµäºããŠããŸããIG API ã§ç¢ºèªããŠåããŠéè€ã倿ããŸããã
çå ïŒç §åã®ææãå£ããŠãã
_verify_landed() ã¯ãããã£ãŒã«ããŒãžã® DOM ãååŸããæ¬æå
é 12æåããã£ãã·ã§ã³ã®å
é 12æåãšçªãåãããŠããŸããããã®èšèšã®åæã¯ãDOM ã® inner_text ã«ãã£ãã·ã§ã³ãäžžããšåºããã§ãã
ãšããã Instagram ã¯çµµæåã <img> ã¿ã°ã«ã¬ã³ããªã³ã°ããŸãããã£ãã·ã§ã³åé ã ðŸ ã®ãšããDOM äžã§ã¯ <img alt="ðŸ"> ã«ãªããinner_text ã«ã¯ãã®æåãåºãŸããã
ãã£ãã·ã§ã³æåå: "ðŸ å人éçºã®éç£ vol..."
ç
§åããããšããæåå(å
é 12æå): "ðŸ å人éçºã®éç£ vo"
DOM inner_text(å
é 12æå): " å人éçºã®éç£ vol." â ðŸ ã img ã«åããŠæ¶ãã
â æ°žä¹
ã«äžäžèŽ
ãæŒãããïŒæçš¿ã§ãããïŒãã§ãªãããäœãç §åããããã§å£ããã®ããã®çš®ã®åœé°æ§ã®æ¬è³ªã§ããç §åã®ä»çµã¿ã¯åããŠããŸãããã ããç §åã®ææãç ŽæããŠããŸãã
ä¿®æ£ã®3æ¬æ±
1. ãã£ãã·ã§ã³æ£èŠåïŒçµµæåã»å šè§é€å»ïŒ
ç §åããŒã DOM ã®ã¬ã³ããªã³ã°ã«äŸåããªã圢ã«å€æããŸãããã£ãã·ã§ã³æååãš DOM ããåã£ãæ¬æã®äž¡åŽã«åã颿°ãéãããšã§ãã©ã¡ããæ¬ ããŠãæ¯èŒãæç«ããªããªããŸãã
def normalize_caption_key(text: str) -> str:
import unicodedata
# å
šè§ã»åè§ç©ºçœãé€å»
text = "".join(c for c in text if not c.isspace())
# BMPå€ïŒçµµæå ord > 0xFFFFïŒãš Unicode ã«ããŽãª So/Sk ãé€å»
text = "".join(
c for c in text
if ord(c) <= 0xFFFF
and unicodedata.category(c) not in ("So", "Sk")
)
# å
é 24æåïŒæ®ãã24æåæªæºãªããã£ãã·ã§ã³å
šäœïŒ
return text[:24] if len(text) >= 24 else text
ç §åå¹ ã 12 æåãã 24 æåã«å»¶ã³ãŠããã®ã¯ãæ£èŠååŸã«çµµæåãéåŒãããåã®ããŒãžã³ã§ããããšã®12æåã«ãŒã«ã¯æªæ£èŠåã®æååã«å¯ŸããŠèšèšãããŠãããããæ£èŠååŸã®æ®ããåãéã®æ å ±ãæã€ã«ã¯é·ãããå¿ èŠããããŸãã
ããã£ãã·ã§ã³ãšããŒãžæ¬æã®äž¡åŽã«åãæ£èŠåãããããââãããèã§ããçåŽã ãæ£èŠåããŠãã倿åŸã®æååé·ãåããå¥ã®ãºã¬ãçã¿ãŸãã
2. API å èªã¿ã«ãã DOM ãã©ãŒã«ããã¯æé€
DOM ã® inner_text ã¯ãIG ãã©ãã¬ã³ããªã³ã°ãããã«äŸåããŸããIG ã¯ãã€ã§ã仿§ãå€ããããããã®äžç¢ºå®æ§ããŒãã«ããã®ã API å
èªã¿ã§ãã
ãã°ã€ã³æžã¿ããŒãžã® ds_user_id cookie ã䜿ããå
éš API ã«çŽæ¥åãåãããŸããAPI ã JSON ãè¿ããïŒïŒæ£åžžã«å¿çããïŒã«ããããããè©²åœæçš¿ãèŠã€ãããªããã°ãDOM ã«éããã倱æãšããŠç¢ºå®ãããŸãã8åã®åãåããããã¹ãŠ JSON ã§ãªãïŒAPI ãå£ããŠããçïŒãšãã ããDOM ã1åã ããã©ãŒã«ããã¯ãšããŠäœ¿ããŸãã
API åãåãã (æå€§8å)
â
ââ JSON è¿çãã + æçš¿ãã â ok: true
ââ JSON è¿çãã + æçš¿ãªã â ok: falseïŒDOM ã«éãããªãïŒ
ââ 8åãšã JSON ä»¥å€ â DOM ã1åã ãç¢ºèª â ok / false
ã確èªäžèœãããæªæçš¿ããšèª€èš³ããªãããã®èšèšã§ããAPI ãå£ããŠãããšãã« DOM ã«éããã°ãçµå± inner_text ã®ç
§åã«æ»ããŸããããã§ã¯åœé°æ§ãåçºãããå¯èœæ§ãæ®ããŸãã
3. fail-closed èšèš
API 確èªãäžèœãªãšããããã©ã«ãããæçš¿æžã¿ããšããŠæ±ããŸããçç±ã¯é察称æ§ã§ãã
- ãæªæçš¿ãšèª€å€å® â åå®è¡ â éè€ãïŒä»åå®éã«èµ·ãã被害ïŒ3æ¬éè€ã»åŸããæ¶ãæéïŒ
- ãæçš¿æžã¿ãšèª€å€å® â ã¹ããã â 1ååé å»¶ãïŒæ¬¡ã®å®æå®è¡ã§èªç¶ã«ååããã
éè€ã¯åŸããæ¶ããŸãããå ¬éãããŠããæéãšåé€äœæ¥ãçºçããŸããé å»¶ã¯æ¬¡ã®ãžã§ããè£ããŸããé察称ãªè¢«å®³ã«ã¯ã軜ãã»ããããã©ã«ãã«ãã fail-closed ãé©åã§ãã
ããããŠããã§ã«å
¬éæžã¿ã®æçš¿ãã倱æããšããŠå°åž³ã«æ®ããªããã already-live å€å®ãå°å
¥ããŸããã
# æ¢åæçš¿ãšããŠæ€åºããå Žå
return {"ok": True, "note": "already-live"}
ok:false ã§å°åž³ã«ç©ãŸãããšã次ã®ãªãã©ã€å€å®ã§ãæªæçš¿ãæ±ãã«ãªããŸããok:true ã§èšé²ããããšã§ãéè€ã¬ãŒããæ£ããæ©èœããŸãã
ãã®3ç¹ã®ä¿®æ£ã¯ IG ãžäžåæ¥ç¶ãããéçãªå®è£
倿ŽãšçŽé¢æ°ãã¹ãã ãã§çå°ãããŸããïŒpytest 12 passedïŒãæçš¿åã®éè€ãã§ãã¯ïŒhas_existing_caption()ïŒãã«ã«ãŒã»ã«ãšãªãŒã«ã®äž¡ã¬ãŒã³ã«å
±é颿°ãšããŠé
ç·ããAPI 確èªäžèœæã¯åæ§ã« fail-closed ã§æ¢ãŸããŸãã
å®è£ ã®è©³çް
normalize_caption_key â ãªããã®å®è£
ã«ãªã£ãã
ååã§ç€ºããæ£èŠå颿°ããããäžæ®µæ·±ãåè§£ããŸãã
def normalize_caption_key(text: str) -> str:
import unicodedata
text = "".join(c for c in text if not c.isspace())
text = "".join(
c for c in text
if ord(c) <= 0xFFFF
and unicodedata.category(c) not in ("So", "Sk")
)
return text[:24] if len(text) >= 24 else text
ord(c) <= 0xFFFF ãšããæ¡ä»¶ã®æå³ã¯ãBMPïŒBasic Multilingual PlaneïŒã«åãŸãæåã ããæ®ããšããããšã§ããçµµæåã®ã»ãšãã©ã¯ U+1F000 以äžãã€ãŸã ord ã 65535 ãè¶
ããŸããðŸ 㯠ord ã 128126 ãªã®ã§å³é€å€ããããUnicodeã«ããŽãª SoïŒSymbol, OtherïŒãš SkïŒSymbol, ModifierïŒãé€å»ããã®ã¯ãBMPå
ã«ãè£
食çãªèšå·ãæ··åšããŠããããã§ããããšãã° â
ïŒU+2605, ã«ããŽãª SoïŒã¯BMPã«åãŸã£ãŠããŸãã inner_text ã«å®å®ããŠåºããšã¯èšããªãã
ã24æåããšããç
§åå¹
ã¯å®æž¬ããéç®ããŠããŸããããšã®12æåã¯æªæ£èŠåã®æååã§èšèšãããŠããŸãããæ£èŠååŸã¯çµµæåãæããåã ãæååãçããªããŸãããã£ãã·ã§ã³åé ã ðŸ å人éçºã®éç£ vol.XX ââ ã®ãããªå ŽåãðŸ ãè±èœããåŸã®æåã®12æå㯠ã å人éçºã®éç£ vã ãšããªãæ±çšçãªæååã«ãªããŸãã24æåãŸã§äŒžã°ããš ã å人éçºã®éç£ vol.18 ââã ãŸã§èªããå¥ã®æçš¿ãšè¡çªãã«ãããªããŸãã
èã¯é¢æ°ã®é©çšå¯Ÿè±¡ã§ããããã£ãã·ã§ã³æååããšãDOMããåã£ãããŒãžæ¬æãã®äž¡åŽã«åã normalize_caption_key ãéããŸããçåŽã ãæ£èŠåããŠãã倿åŸã®æåæ°ããºã¬ãŠæ°ãããã¹ããããçã¿ãŸããpytest ã§ç¢ºèªããã®ããäž¡åŽã«éããçµæãäžèŽãããããšããã·ããªãªã§ãã
# ãã¹ãã®éªšæ Œ
def test_normalize_both_sides():
caption = "ðŸ å人éçºã®éç£ vol.18 ââ "
dom_text = " å人éçºã®éç£ vol.18 ââ " # IG ã img ã«å€æããåŸ
assert normalize_caption_key(caption) == normalize_caption_key(dom_text)
çŽé¢æ°ãªã®ã§IGã«äžåæ¥ç¶ããæ€èšŒã§ããŸããããããIGæªæ¥ç¶ã§ pytest 12 passedãã®å®äœã§ãã
API å èªã¿ â ã確èªäžèœããšãæªæçš¿ããåé¢ããå®è£
ååã§ç€ºãããããŒãå®è£ ã«èœãšããšãå€å®ã®åå²ã¯æ¬¡ã®ãããªåœ¢ã«ãªããŸãã
def _verify_via_api(shortcode: str, caption: str) -> dict:
for attempt in range(8):
resp = _fetch_profile_api(user_id=DS_USER_ID, cursor=...)
if resp is None: # JSON以å€ãè¿ã£ãŠãã
continue
# JSONãè¿ã£ã = APIæ£åžž
for post in resp.get("edges", []):
node_text = post["node"].get("edge_media_to_caption", {})
key = normalize_caption_key(node_text.get("edges", [{}])[0].get("node", {}).get("text", ""))
if key and key == normalize_caption_key(caption):
return {"ok": True, "source": "api"}
# JSONãè¿ã£ãã該åœãªã â DOMãžéãããªã
return {"ok": False, "source": "api", "reason": "not_found"}
# 8åãšãJSONã§ãªãã£ã â DOMã1åã ã
return _verify_via_dom(caption)
æ žå¿ã¯ resp is None ãš return {"ok": False} ã®åå²ã®é察称æ§ã§ãããAPIãåããŠããŠJSONãè¿ã£ãŠãããã®ã«ãè©²åœæçš¿ãèŠã€ãããªãããªããããã¯æçš¿ãããŠããªããšããç¢ºå®æ
å ±ã§ããDOMãžéããçç±ã¯ãããŸãããã8åãšãJSONãè¿ããªãã£ãããšãã ããDOM確èªã«ãã©ãŒã«ããã¯ããŸãã
ãªã8åããšãããšãå éšAPIãžã®ãªã¯ãšã¹ãã¯Instagramã®ã¬ãŒãå¶éã«åŒã£ãããããšãããã1ã2å倱æããããšã«æ£åžžã«è¿ãããšãããããã§ãããã ã8åãšãJSONã§ãªããªããAPIãå£ããŠãããã»ãã·ã§ã³ãåããŠãããã®ã©ã¡ããã§ããã以äžåãç¶ããŠãçµæã¯å€ãããŸããã
ds_user_id cookie ã䜿ãçç±ã¯ããã°ã€ã³æžã¿ã®å
éšAPIã¯ãã©ããŒå€ã®ãããã£ãŒã«ã«å¯ŸããŠãæçš¿äžèЧãJSON圢åŒã§è¿ãããã§ããããŒãžããŒã·ã§ã³ãå¿
èŠãªå Žå㯠after ã«ãŒãœã«ã䜿ããŸãããçå°ç¢ºèªã§ã¯çŽè¿æçš¿ã ãã確èªããã®ã§1ããŒãžã§è¶³ããŸãã
fail-closed ãš already-live ã®å°åž³èšèš
# API確èªã8åãšãäžèœ â DOMãžè¡ã£ããçµæãäžæ â fail-closed
def _verify_landed(shortcode: str, caption: str) -> dict:
result = _verify_via_api(shortcode, caption)
if result.get("source") == "api":
return result
# DOMãã©ãŒã«ããã¯ã®çµæ
dom_result = result
if dom_result.get("ok") is None: # 確èªäžèœ
return {"ok": True, "note": "unverified-assumed-live"}
return dom_result
ok: True, note: "already-live" ã®èšé²åœ¢åŒã«ããŠããã®ã¯ãå°åž³ã®èªã¿åãåŽã ok:true ãã¹ãããå€å®ã«äœ¿ã£ãŠããããã§ããok:false ã§èšé²ãããšã次ã®ãªãã©ã€å®è¡æã«ãæªæçš¿ããšããŠååŠçãããŸããnote ãã£ãŒã«ãã¯äººéãå°åž³ãèŠããšãã«ããªãæåæ±ãã«ãããããæ®ãããã®ãã®ã§ãã¹ã¯ãªããã®ããžãã¯ã«ã¯äœ¿ããŸããããã®åºå¥ãå
¥ããŠãããªããšã3ã¶æåŸã«å°åž³ãèªãã ãšãããªããããtrue?ããšããçåã«çããããªããªããŸãã
æçš¿æ°ãã«ã¿ã«ããäºé確èªãåãããŠå ¥ããŠããŸããæçš¿ååŸã®ãããã£ãŒã«æçš¿æ°ãååŸããå·®åã +1 ãªããã£ãã·ã§ã³ç §åãšç¬ç«ããŠæåå€å®ããŸããããã¯ãã£ãã·ã§ã³æ£èŠåãäœããã®çç±ã§äžèŽããªãã£ãå Žåã®ã»ãŒããã£ãããã§ãã2ã€ã®ç¢ºèªãåãå éšAPIã䜿ããªãããèŠãŠãããææããå¥ïŒãã£ãã·ã§ã³ vs ä»¶æ°ïŒã«ãªã£ãŠããã®ã§ãçæ¹ãå£ããŠãå€å®ãå®å šã«ã¯æ»ã«ãŸããã
ç§ãè©°ãŸã£ã話
ä¿®æ£ã®ç¿æ¥ãä»åºŠã¯ç©ºã®æçš¿ã16æ¬äžŠãã
8æ28æ¥ã®ä¿®æ£ïŒæ£èŠåïŒAPIå èªã¿ïŒfail-closedïŒãå ¥ããç¿ã æ¥ã®8æ30æ¥ãä»åºŠã¯ æ¬æã®ãªãã«ã«ãŒã»ã«ã16æ¬å ¬éãããŸãããåã2ãã¿ãŒã³ã8ã©ãŠã³ããã€ã§ãã
çç¶ã¯ããŸãéè€ãåºããã§ãããä»åºŠã®åå ã¯ç
§åã§ã¯ãªãæçš¿åŽã§ãããig_autopost.py ã® keyboard.insert_text ããã£ãã·ã§ã³æ¬ã«å
¥ã£ãŠããããã¹ã¯ãªãŒã³ã·ã§ããã«ã¯ 0/2200 ã®ãŸãŸãã·ã§ã¢ããæŒããèšé²ãæ®ã£ãŠããŸããã
ãã®äºæ ãååãããã¡ãæªãã£ãã®ã¯ãåŠç¿ããŒãã«æžããéãã§ãã
ðŽ æçš¿åã®éè€ã¬ãŒãïŒ
has_existing_captionïŒãšæçš¿åŸã®çå°ç §åïŒ_verify_landedïŒã¯ãåãå éšAPIãšåã24åããŒïŒnormalize_caption_keyïŒãå ±æããŠããã æ¬æã空ã ãšäž¡æ¹ãšãäžèŽããªã â éè€ã¬ãŒããããŸã åºãŠããªãããšèšããçå°ç §åããåºãŠããªãããšèšã â å€±ææ±ãã§åãIDãåæçš¿ã
8æ28æ¥ã«ãæçš¿åã®éè€ãã§ãã¯ãå ±é颿°ã«åãåºãããããšã¯æ£ãã倿ã§ããããã ãå ±éåããçµæãå£ãæ¹ãŸã§å ±éã«ãªã£ãã颿°ã1ã€ã§ããããšã¯åå©çšæ§ã®èгç¹ã§ã¯æ£è§£ã§ããã£ãã·ã§ã³ãæ£åžžã«å ¥ã£ãŠããéãåé¡ãªãããã ããã£ãã·ã§ã³ã空ã«ãªããšããçµè·¯ã«å¯ŸããŠãå ±æããŠããå šãŠã®ã¬ãŒããåæã«ç¡ååãããŸãã
çŽãã¯2段éã§ããã
ç¬¬äžæ®µïŒæçš¿åã«ãã£ãã·ã§ã³ãå ¥ã£ãããšã確èªããã
def insert_caption_with_verify(page, caption: str) -> None:
# ææ®µ1: keyboard.insert_text
# ææ®µ2: clipboardçµç±ã§paste
# ææ®µ3: JSã§valueçŽæ¥ã»ãã
for method in [_keyboard_insert, _clipboard_paste, _js_set_value]:
method(page, caption)
actual = page.locator('[data-lexical-editor]').inner_text()
if len(actual) >= len(caption) - 5: # 5æåããŒãžã³
return
raise RuntimeError(f"ãã£ãã·ã§ã³æå
¥å€±æ: æåŸ
{len(caption)}æå/宿ž¬{len(actual)}æå")
ãå
¥ãããã®å€å®ã keyboard.insert_text ã®æ»ãå€ã§ãªãããã£ãŒã«ãã®å®éã®æåæ°ã§è¡ããŸããAPIããå
¥ããããè¿ããŠããDOMã®ã¬ã³ããªã³ã°ã远ãã€ããŠããªãå ŽåããããŸããå®èµ°ç¢ºèªã§ã¯ ãã£ãã·ã§ã³æå
¥ 1257æå / æåŸ
1257æå ãšåºãŠããçå°ç¢ºèª13ç§ã§æåããŸããã
ç¬¬äºæ®µïŒç §åããŒã空ã«ãªãçµè·¯ãå ¥å£ã§æ¢ããã
def normalize_caption_key(text: str) -> str:
# ...ïŒåè¿°ã®æ£èŠåïŒ
result = text[:24] if len(text) >= 24 else text
if not result:
raise ValueError("ç
§åããŒã空: æçš¿åã«äžæ¢ããŠãã ãã")
return result
空ããŒã¯ãäžèŽããªããã§ã¯ãªããç
§åãæç«ããŠããªãããšãã第3ã®ç¶æ
ã§ãããæç«ããŠããªãç
§åãã®çµæã ok:false ãšããŠå°åž³ã«ç©ããšããªãã©ã€ãèµ°ã£ãŠå¯äœçšãå¢ããŸããå
¥å£ã§æ¢ããŠæç€ºçã«ãšã©ãŒã«ããããšã§ããç
§åäžèœ â åæçš¿ããšããé£éãæã¡ãŸãã
TikTok ã15æ¥é£ç¶ã§ãšã©ãŒãåããŠããããæçš¿ã¯å šéšã§ããŠãã
9æ8æ¥ã«çºèŠããã®ã¯ãpost_reel_tiktok.py ã9æ24æ¥ä»¥éãã£ãš RuntimeError ã§çµäºããŠãããšãã話ã§ãã15æ¥éãå°åž³ã«ã¯æ¯æ¥ ok:false ãç©ãŸããŠããŸããããšãããTikTokã®ããã·ã¥ããŒãã確èªãããšãæçš¿ã¯å
šéšæåããŠããŸããã
åå ã¯ãæåå€å®ããã³ããŒã®åºå®æèšã«äŸåããŠããããšã§ããã¹ã¯ãªããã¯æçš¿ãã¿ã³æŒäžåŸãããŒãžbodyã«5çš®é¡ã®åºå®æèšãåºããŸã§120ç§ããŒãªã³ã°ããŠããŸããã
SUCCESS_TEXTS = [
"åç»ãæçš¿ãããŸãã",
"æçš¿ãäœæããŸãã",
"ã³ã³ãã³ããå
¬éãããŸãã",
# ...ä»2çš®
]
TikTokã¯æçš¿åŸã«æçš¿ç®¡çããŒãžïŒURLã« /tiktokstudio/content ãå«ãïŒãžé·ç§»ããŠããŸãããããã®ç»é¢ã®ãµã€ãããŒã®èªã¯ã管çãããã§ã¯ãªã**ã管çã**ã«å€ãã£ãŠããŸããããããã® SUCCESS_TEXTS ã«ãäžèŽããªãã®ã§ãæ¯å RuntimeError ãæããŠçµãã£ãŠããã
15æ¥éã«æ°ã¥ããªãã£ãçç±ã2ã€ãããŸãã第äžã«ãå°åž³ã® skip å€å®ã ok:true ã®ãšã³ããªããèŠãŠããªãã£ãããšãok:false ãšããŠèšé²ãããåç»IDã¯ã次åå®è¡æã«ããŸã æçš¿ããŠããªãããšããŠååŠçã®åè£ã«ãªããŸãããã ãå®éã«ã¯æçš¿æžã¿ãªã®ã§ãä»®ã«åå®è¡ãèµ°ãã°åãåç»ã2æ¬å
¬éãããŠããŸãããããŸããŸéè€ãåºãªãã£ãã®ã¯ãå°åž³ã« ok:false ãç©ãŸããåç»IDããã¥ãŒã®æåŸã«æ¥ãŠããããæ°èŠã®åç»åªå
ã§åŠçãåã£ãŠããããã§ãã
第äºã«ã倱ææã®äŸå€ã«ããŒãžã®URLãbodyå
é ãèŒããŠããªãã£ãããšãRuntimeError: ã¿ã€ã ã¢ãŠã: æåæèšãèŠã€ãããŸãã ãšãã1è¡ã ãã§ã¯ããTikTokãæèšãå€ããããšãã蚺æãã§ããŸããããšã©ãŒãåºããšãçŸå Žã«èšŒæ ãæ®ããªããšã埩æ§ã«èŠããæéãèšãã¿ãŸãã15æ¥ããã£ãã®ã¯ãã®èšé²ã®æ¬ åŠãåå ã§ãã
çŽãã¯å€å®ã®å€éåã§ãã
def _is_posted_successfully(page) -> bool:
# å€å®1: åŸæ¥ã®æåæèšïŒTikTokãå€ããããïŒ
if any(t in page.inner_text("body") for t in SUCCESS_TEXTS):
return True
# å€å®2: URLãuploadãé¢ããŠcontentã«çãã
if "/tiktokstudio/content" in page.url:
return True
# å€å®3: èªåãæãããã£ãã·ã§ã³ã®å
é 12æåãæ¬æã«ãã
if normalize_caption_key(caption)[:12] in page.inner_text("body"):
return True
return False
å€å®3ã®ãèªåãæãããã£ãã·ã§ã³ã§ç¢ºèªããããšããã¢ãããŒããäžçªå£ãã«ããã§ãããã³ããŒãUIãå€ããŠããæçš¿ããã®ãèªåãªãèªåã®ããã¹ãã¯ç»é¢ã«åºãŸãããã³ããŒã®èªåœã«äŸåããå€å®1ãš2ã¯ãã©ãŒã«ããã¯æ±ãã«ãªããŸãã
åŠç¿ããŒãã«ã¯ããèšé²ããŸããã
ãã³ããŒæèšã«äŸåããå€å®ã¯ãèœã¡ããšãã®çŸç©ãæ®ããªããšçŽããªã
ãšã©ãŒã®èšé²èšèšã¯ããªãã©ã€èšèšãšåããããéèŠã§ããpage.url ãš body å
é 200æåãå¿
ãäŸå€ã«èŒããããã«ããã®ã¯ãã®äºæ
ããã§ãã
XïŒæ§TwitterïŒã®åŒçšãã¹ãã5æ¥éãã£ãšã0ä»¶ãã«èŠãã
9æ10æ¥ã«æ°ã¥ããã®ã¯ã9æ5æ¥ä»¥éã®åŒçšãã¹ãããã¹ãŠ quoted=0 ã§èšäžãããŠããããšã§ããå®éã«ã¯æçš¿ãããŠããŸããããçå°ç
§åã¹ã¯ãªãããå
šä»¶ unverified ãè¿ããŠä»¶æ°ãç©ãŸãªãã£ãã
åå ã¯Playwrightã®strict modeãšDOMæ§é ã®å€åã§ããã
// 倿Žå
const tweetText = await article.locator('[data-testid="tweetText"]').innerText()
åŒçšãã¹ãã®articleèŠçŽ ã«ã¯ tweetText ã2åå
¥ã£ãŠããŸããèªåã®æ¬æãšãåã蟌ãŸããå
ãã¹ãã§ããPlaywrightã® innerText() ã¯strict modeã§ã¯èŠçŽ ãè€æ°ãããšãšã©ãŒãæããŸãããã®ãšã©ãŒãcatchãããŠç©ºæåã«ãªããç
§åã unverified ã«åããquoted=0 ã§èšäžãããŠããŸããã
çŽã㯠.first() ã§å
é èŠçŽ ã ããèªãããšã§ãã
const tweetText = await article.locator('[data-testid="tweetText"]').first().innerText()
1è¡ã®å€æŽã§ããã5æ¥åã®åœã¢ã©ãŒããæ¢ãŸããŸãããhealth.mjs ããæ¬æ¥0ä»¶ãã®éç¥ããã¹ã¯ãããã«åºãç¶ããŠããã®ã§ã5æ¥éã¯ãåŒçšãã¹ããå
šéšã³ã±ãŠãããåå äžæããšããç¶æ
ã§ãããå®éã«ã¯äœãå£ããŠããããæçš¿ã¯å
šéšæåããŠããã
ãã®äºæ ã¯TikTokã®15æ¥é£ç¶exit 1ãšå®å šã«åãæ§é ã§ããããã³ããŒåŽDOMã1èŠçŽ âè€æ°èŠçŽ ã«å€ãããšãstrictãªç §åã倱æã«åããŠåœé°æ§ã«ãªããââæ§é ã¯åãã§ããçºçããã®ã¯å¥ã®ãã©ãããã©ãŒã ã§2é±éåŸã®ããšã§ãã1ç®æã§åŠãã ãã¿ãŒã³ã暪æããŠãã§ãã¯ã§ããŠããã°ã5æ¥ã§ã¯ãªã0æ¥ã§å¡ããŸããã
ãããæãçãã£ãè©°ãŸãã®è©±ã§ãããåãå£ãæ¹ãç¥ã£ãŠãããã«ãããããããå¥ãã©ãããã©ãŒã ã§ã®åçºãé²ããªãã£ããä¿®æ£ãå
¥ããããšãååã®ç
§åã䜿ã£ãŠããå
šã¬ãŒã³ã® reply.mjs ã«ãåã .first() ãäºé²çã«è¶³ããŸããããããã¯äºæ
åŸã®åŠçœ®ã§ãã
3ã€ã®å€±æã«å ±éããŠãããã®
8æ30æ¥ã®ç©ºãã£ãã·ã§ã³16æ¬ãTikTokã®15æ¥é£ç¶exit1ãXåŒçšãã¹ãã®5æ¥é0ä»¶ââ3ã€ã«å ±éããŠããã®ã¯ãç §ååšãå£ããŠããããšããç §ååšèªèº«ãæããããªãããšããæ§é ã§ãã
ã¹ã¯ãªãã㯠ok:false ãè¿ãç¶ããŸããå°åž³ã«ã¯å€±æãç©ãŸããŸããã¹ã±ãžã¥ãŒã©ã¯åå®è¡ãäºçŽããŸãããã®é£éã®ã©ãã«ããå€å®ã®ææãå£ããŠããããšããä¿¡å·ã¯åºãŸãããã¹ã¯ãªããã¯æ£ããåããŠããŸãââãã ããééã£ãææã䜿ã£ãŠã
ãªãã©ã€èšèšã®æ£å
žïŒretry-and-giveup-design.mdïŒã«ã¯æ¬¡ã®äžæããããŸãã
åã倱æã3åæããŠãçµæã¯å€ãããªããåæ°ã§ãªãæž¡ããŠããå 容ãçãã
ä»åã®3ã€ã¯ãã¹ãŠãã®ã±ãŒã¹ã§ãããªãã©ã€åæ°ã2åã«æžãããŠãã3åã®ãŸãŸã§ããç §åã®ææãå£ããŠããéãçµæã¯å€ãããŸãããå€å®ã3åãšãåãæ¹åã«åãç¶ããå Žåââåœé°æ§ãé£ç¶ããå ŽåââããŸãçãã¹ãã¯ãäœãç §åææã«ããŠããããã§ãã
inner_text ã®ã¬ã³ããªã³ã°äŸåãåºå®æèšã®ãã³ããŒå€æŽãstrict modeã®èŠçŽ æ°å€åââ3ã€ãšããèªåãæžããç
§åããžãã¯ã¯æ£ããããå€åŽã®ç°å¢ãç
§åãç¡å¹åããããšããåã§ãããã®åã«å¯Ÿãããªãã©ã€åæ°ã®å¢æžã¯ç¡åã§ããææãå€ããããç
§åã®ä»æ¹ãå€ããããã©ã¡ãããããããŸããã
ä»ã®ç°å¢ã§ã¯ãçå°ç¢ºèªã倱æããå Žåã«å³ãªãã©ã€ããã®ã§ã¯ãªããããªã倱æãããããå¥çµè·¯ã§åãã¹ããããæãã§ããŸãã倱æã®ãšãã«APIãçŽæ¥å©ããŠãæçš¿ãååšããããã確èªããååšããã° already-live ãšããŠèšé²ããŸããååšããªããã°åããŠãæ¬åœã«æªæçš¿ããšç¢ºå®ããŸãããã®ã確èªããŠãã倿ãããäžæéããå¯äœçšã®éè€ãé²ãæåŸã®å£ã§ãã
ã€ãŸãããã€ã³ã
èªåãå®éã«èžãã å°é·ãç¶²çŸ ããŸããã©ãããã¹ã¯ãªããã¯æ£åžžã«çµäºããŠããã®ã«çŸå®ã®äžçã ããå£ããããšããåã§ãã
-
çµµæå以å€ã®èšå·ãinner_textããæ¶ããã
âïŒU+2605ã»Unicodeã«ããŽãªSoïŒã¯BMPå ã®æåã§ãããIG ã®ã¬ã³ããªã³ã°ã§inner_textã«å®å®ããŠåºããšã¯èšããŸãããord(c) <= 0xFFFFã§è¶³ãããšæã£ãŠããŸããããã«ããŽãªSoïŒSymbol, OtherïŒãšSkïŒSymbol, ModifierïŒã远å é€å»ããçç±ã¯ããã«ãããŸããæ£èŠåã®æ¡ä»¶ããBMPå ãã©ãããã ãã§æ±ºãããšæ¬¡ã®äºæ ãæ¥ãŸãã -
ç §åããŒãçåŽããæ£èŠåããªãããã£ãã·ã§ã³æååã ãæ£èŠåããŠDOMååŸåŽãçã®ãŸãŸæ¯èŒãããšãçµµæåãæ¶ããåã ãæåæ°ããããæ°ãããã¹ããããçãŸããŸãã
normalize_caption_keyãå¿ ãäž¡åŽã«éãââããã¯å®è£ ã®äžã§æãå¿ãããããã€ã³ãã§ãã -
ç §åããŒã空ã«ãªãçµè·¯ã朰ããŠããªãã8æ30æ¥ã«æ¬æã®ãªãã«ã«ãŒã»ã«ã16æ¬å ¬éãããæ ¹æ¬åå ãããã§ãã
ig_autopost.pyã®keyboard.insert_textããã£ãã·ã§ã³æ¬ã«å ¥ããã0/2200ã®ãŸãŸãã·ã§ã¢ããæŒããŸãããæçš¿åéè€ã¬ãŒãïŒhas_existing_captionïŒãšæçš¿åŸçå°ç §åïŒ_verify_landedïŒã¯åã24åããŒãå ±æããŠããã®ã§ã空ããŒã«ãªã£ãç¬éã«äž¡æ¹ãåæã«ç¡ååãããŸããçµæãéè€ã¬ãŒããããŸã åºãŠããªãããšèšããçå°ç §åããåºãŠããªãããšèšããåãå 容ããªãã©ã€ãç¶ããŸããã -
ã鲿¢ããšãæ€ç¥ããåãç §ååšãå ±æããŠãããã³ãŒãã®åå©çšæ§ãšãã芳ç¹ã§ã¯æ£ããèšèšã§ãããã®é¢æ°ãå£ããç¬éã«é²æ¢ãšæ€ç¥ãåæã«æ»ã«ãŸããåäžé害ç¹ãé ããŠããŸããå°ãªããšãäžæ¹ã¯å¥ã®ææïŒæçš¿æ°ã®ãã«ã¿ã»APIä»¶æ°ïŒã§ç¢ºèªããå¿ èŠããããŸãã
-
ãã³ããŒã®åºå®æèšã«äŸåããæåå€å®ãTikTokãæçš¿åŸã«ããŒãžé·ç§»ãããµã€ãããŒã®èªãã管çãããããã管çãã«å€ãã£ãã ãã§ãç»é²ããŠãã5çš®é¡ã®åºå®æèšãã¹ãŠã«äžèŽããªããªããŸããã
post_reel_tiktok.pyã¯æçš¿ãããã³ã«RuntimeErrorãæãã15æ¥éok:falseãç©ã¿ç¶ããŸãããTikTokã®ããã·ã¥ããŒããçŽæ¥éããŠåããŠãå šéšæçš¿ã§ããŠããããšããããŸããã -
倱æäŸå€ã«ããŒãžURLãbodyå é ãæ®ããŠããªãã
RuntimeError: ã¿ã€ã ã¢ãŠã: æåæèšãèŠã€ãããŸããã®1è¡ã ãã§ã¯ãTikTokãæèšãå€ããããšãã蚺æãã§ããŸãããpage.urlãšbodyå é 200æåãäŸå€ã«å ¥ã£ãŠããªãã£ãããã§ãåãåãã«15æ¥ããããŸããã -
Playwright strict modeã§è€æ°èŠçŽ ãthrowãããXïŒæ§TwitterïŒã®åŒçšãã¹ãã®articleèŠçŽ ã«ã¯
[data-testid="tweetText"]ã2åãããŸãââèªåã®æ¬æãšãåã蟌ãŸããå ãã¹ãã®æ¬æã§ããinnerText()ãstrict modeã§throwãã空æåãè¿ããç §åãunverifiedã«åãã9æ5æ¥ä»¥éã®åŒçšãã¹ãããã¹ãŠquoted=0ã§èšäžãããŸããã.first()ã1è¡è¿œå ããã ãã§è§£æ±ºããŸãããã5æ¥éæ°ã¥ããŸããã§ããã -
ã確èªäžèœãããæªæçš¿ããšèª€èš³ããèšèšãAPIãå£ããŠãããšãã«DOMãžãã©ãŒã«ããã¯ãããšãçµå±
inner_textã®ç §åã«æ»ããŸãããAPIãJSONãè¿ããã®ã«æçš¿ãèŠã€ãããªããã¯ç¢ºå®æ å ±ïŒæªæçš¿ïŒã§ãããã8åãšãJSONãè¿ããªãã£ããã¯åã«ç¢ºèªã§ããªãã£ãã ãã§ãããã®2ã€ãåºå¥ããã«DOMãžéãããšãåœé°æ§ã®åçºçµè·¯ãæ®ããŸãã -
åã倱æãåæ°ãå¢ãããŠ3åæãããnote-autolikeã®rewriteã3æ¥éãŒãä»¶ã ã£ãã®ã¯ãããªããŒã¿ã58åè¶ ã匟ãã®ã«çæããã³ããã«åæ°å¶éãäžèšãæžãããŠããªãã£ãããã§ãã
askClaudeã¯å€±æããŠãåäžããã³ããã3åæãçŽãã ãã§ã3åãšãåã63åã¿ã€ãã«ãè¿ããŸãããå¶çŽã¯ããªããŒã¿ãšçæããã³ããã®äž¡æ¹ã«æžããªããã°æ©èœããŸããã -
ã¹ãããååŸå€±æã§å³exit 0ããã
browser-slot.shãã°ããŒãã«æ ïŒ3æ¬ïŒãååŸã§ããªãã£ãç¬éã«SKIP: global limit reachedãåºããŠå³çµäºããŠããããã12æå°12ä»¶ã»09æå°9ä»¶ã»11æå°8ä»¶ãèµ·åããããæ¶ããŸããã600ç§åŸ æ©ãªãã·ã§ã³ã远å ããå詊è¡ééã15ã45ç§ã®ã©ã³ãã ã«ããŠãã空æ¯ãã¯ã»ãŒãŒãã«ãªããŸãããåºå®ééã«ãããšå šãžã§ããåãç¬éã«æ®ºå°ããïŒthundering herdïŒã®ã§ãã©ã³ãã å¹ ã¯å¿ é ã§ãã -
ãã©ãŒã«ããã¯ã®éãå ãåãå£ã«åœããã
gen_note_thumbs.pyã§å®Chromeâ忢±chromiumâenv ã®3段éãå ã远å ããŸãããã3段ç®ãŸã§åã90ç§rc=-9ã§çµãããŸãããçç¶ãåãã§ãåå ã¯åããšã¯éããŸãããéãå ã远å ããåã«ããã®éãå ã¯å ãšå¥ã®å£ã«åœããããã1å宿ž¬ããŠããé ç·ãã¹ãã§ããã -
IG delete APIã200ãè¿ããŠãåé€ãããŠããªããã»ãã·ã§ã³ïŒ
sessionidïŒãåããç¶æ ã§deleteAPIãå©ããšã200ãè¿ããªããHTMLãæ»ããŸãã8æ28æ¥ã®éè€3æ¬ãåé€ããããšããŠ1ä»¶ãæ¶ããŠããŸããã§ããããAPIã200ãè¿ããããæåã®å®çŸ©ã«ããªãââãã®æèšã¯æçš¿ç¢ºèªãšå šãåãã§ãã -
best-effortçŽ éãã§äžæµã§ããæ°ã¥ããªããIGãªãŒã«ã®ã¯ãããæ¯çéžæã§ã
query_selectorãNoneãè¿ããŠãé»ã£ãŠçŽ éãããåå²ããããŸãããã©ãã«ãããªãªãžãã«ããOriginalãã9:16ããå ã®åçããå ã®æ¯çããšæ¥ã«ãã£ãŠæºããäžæ¹ãã³ãŒãåŽã®åè£ãªã¹ãã¯åºå®ã§ããã9:16ãéžã°ãã1:1ã®ãŸãŸã次ãžãã«é²ã¿ãã°ãªããã§å·Šå³ãåããåç»ãå ¬éãããŸãããéžæã»åæ¿ã®æäœã¯ãéžæã§ããããšã宿ž¬ã§ç¢ºèªããŠããæ¬¡ã®ã¹ããããžé²ãå¿ èŠããããŸãã -
note CTAã®è²Œä»çµæãä¿¡çšããã9æ2æ¥ã«note.comãåŒçšãããã¯ãèœãšã仿§å€æŽãããŸãããã¹ã¯ãªããã¯æ«å°Ÿã®çœ²åæååã®äžèŽã§æ€èšŒããŠããã®ã§ãèœãšãããèšäºããæªåæ ããšå€å®ããŠè²ŒãçŽãç¶ããéè€CTA 59æ¬ïŒçœ²åæ¬ èœ31æ¬ãçºçããŸããããšãã£ã¿ã®ãå ¥ããããšããèšé²ã§ãªããå ¬éããŒãžãå®éã«èªã¿æ»ããŠæ€èšŒããã¹ãã§ããã
-
ååã®ãã°ãå¥ãã©ãããã©ãŒã ã§åçºãããããDOM1èŠçŽ âè€æ°èŠçŽ ã«å€ãããš strict ãªç §åã倱æã«åããŠåœé°æ§ã«ãªãããšããæ§é ã¯ãTikTokã§åŠãã§ããŸãããã«ãããããã2é±éåŸã«Xã§åãæ§é ãèžã¿ãŸãããä¿®æ£æã«å šã¬ãŒã³ã®ååç®æãäºé²çã«çŽãç¿æ £ããªãã£ãçµæã§ãã1ç®æã§åŠãã ãã¿ãŒã³ã暪æããŠãã§ãã¯ã§ããŠããã°ã5æ¥ã§ã¯ãªã0æ¥ã§å¡ããŸããã
ãã¹ããã©ã¯ãã£ã¹
äžã®è©°ãŸããçµãŠä»ã®ç°å¢ã§å®éã«é©çšããŠãããã¿ãŒã³ãæŽçããŸãã
1. ç §åããŒã¯å¿ ãäž¡åŽã«åãæ£èŠå颿°ãéã
def normalize_caption_key(text: str) -> str:
import unicodedata
text = "".join(c for c in text if not c.isspace())
text = "".join(
c for c in text
if ord(c) <= 0xFFFF
and unicodedata.category(c) not in ("So", "Sk")
)
return text[:24] if len(text) >= 24 else text
ãã£ãã·ã§ã³æååãšDOMååŸããã¹ãã®äž¡åŽã«åã颿°ãéããŸããçåŽã ãã§ã¯å€æåŸã®æåæ°ããããæ°ãããã¹ããããçãŸããŸãã
2. ç §åããŒã空ã«ãªãçµè·¯ãå ¥å£ã§ãšã©ãŒã«ãã
result = text[:24] if len(text) >= 24 else text
if not result:
raise ValueError("ç
§åããŒã空: æçš¿åã«äžæ¢ããŠãã ãã")
return result
空ããŒã¯ãäžèŽããªãïŒok:falseïŒãã§ã¯ãªããç
§åãæç«ããŠããªãã第3ã®ç¶æ
ã§ããok:false ãšããŠå°åž³ã«ç©ããšæ¬¡ã®ãªãã©ã€ãèµ°ã£ãŠå¯äœçšãå¢ããŸãã
3. å¯äœçšã®ããæäœã®ç¢ºèªã¯ãçžæåŽã®å®äœãã§è¡ã
def _verify_via_api(shortcode: str, caption: str) -> dict:
for attempt in range(8):
resp = _fetch_profile_api(user_id=DS_USER_ID, cursor=...)
if resp is None:
continue
for post in resp.get("edges", []):
key = normalize_caption_key(...)
if key and key == normalize_caption_key(caption):
return {"ok": True, "source": "api"}
return {"ok": False, "source": "api", "reason": "not_found"}
return _verify_via_dom(caption)
ãAPIãJSONãè¿ããã®ã«æçš¿ãèŠã€ãããªããã¯ç¢ºå®æ å ±ã§ãDOMãžéãããŸããã8åãšãJSONãè¿ããªããšãã ãDOMã1åã ã䜿ããŸããã確èªäžèœããšãæªæçš¿ãã¯å¥ã®ç¶æ ã§ãã
4. fail-closedèšèšã§ç¢ºèªäžèœæã¯ãæçš¿æžã¿ãæ±ãã«ãã
éè€ïŒåŸããæ¶ãæéã»å
¬éãããŠããæéïŒãšé
å»¶ïŒæ¬¡ã®ãžã§ããè£ãïŒã®è¢«å®³ã¯é察称ã§ãã確èªã§ããªããšãã¯è»œãã»ããéžã³ã{"ok": True, "note": "unverified-assumed-live"} ãšããŠå°åž³ã«ç©ã¿ãŸããok:true ã§èšé²ããããšã§ã次ã®ãªãã©ã€ãèµ°ããŸããã
5. ã鲿¢ããšãæ€ç¥ãã«å¥ã®ææã䜿ã
æçš¿åéè€ã¬ãŒããšæçš¿åŸçå°ç §åãåã24åããŒãå ±æãããšãäžæ¹ãå£ããã°äž¡æ¹ãæ»ã«ãŸããæçš¿æ°ãã«ã¿ïŒæçš¿ååŸã®ãããã£ãŒã«æçš¿æ°ã®å·®åïŒãç¬ç«ããç¢ºèªææãšããŠäžŠèµ°ãããŸãããã£ãã·ã§ã³ãšä»¶æ°ãšãã2ã€ã®ææãç¬ç«ããŠåããŠãããããçæ¹ãå£ããŠãå€å®ãå®å šã«ã¯æ»ã«ãŸããã
6. ãã£ãã·ã§ã³æå ¥ã¯ããã£ãŒã«ãã®å®éã®æåæ°ãã§å€å®ãã
def insert_caption_with_verify(page, caption: str) -> None:
for method in [_keyboard_insert, _clipboard_paste, _js_set_value]:
method(page, caption)
actual = page.locator('[data-lexical-editor]').inner_text()
if len(actual) >= len(caption) - 5:
return
raise RuntimeError(f"ãã£ãã·ã§ã³æå
¥å€±æ: æåŸ
{len(caption)}æå/宿ž¬{len(actual)}æå")
ãå
¥ãããã®å€å®ã¯APIã®æ»ãå€ã§ãªãããã£ãŒã«ãã®å®éã®æåæ°ã§ãã3ææ®µè©ŠããŠèªã¿æ»ãã§æ€èšŒããââå®èµ°ç¢ºèªã§ã¯ãã£ãã·ã§ã³æå
¥ 1257æå / æåŸ
1257æåãšåºãŠããçå°ç¢ºèª13ç§ã§æåããŸããã
7. æåå€å®ã¯ãã³ããŒæèšã§ãªããèªåãæããããã¹ããã§è¡ã
def _is_posted_successfully(page, caption) -> bool:
if any(t in page.inner_text("body") for t in SUCCESS_TEXTS):
return True
if "/tiktokstudio/content" in page.url:
return True
if normalize_caption_key(caption)[:12] in page.inner_text("body"):
return True
return False
å€å®3ã®ãèªåãæãããã£ãã·ã§ã³ã§ç¢ºèªããããæãå£ãã«ããã§ãããã³ããŒãUIãå€ããŠããèªåãæçš¿ããããã¹ãã¯ç»é¢ã«åºãŸããåºå®æèšã¯ãã©ãŒã«ããã¯æ±ãã§ãã
8. 倱æäŸå€ã«ã¯å¿
ã page.url ãšbodyå
é 200æåãèŒãã
ãã³ããŒæèšã«äŸåããå€å®ãå£ãããšããçŸå Žã«èšŒæ ããªããã°èšºæã«15æ¥ããããŸããäŸå€ã®1è¡ã« page.url ãš page.inner_text("body")[:200] ãå¿
ãèŒããŸãã
9. Playwright strict mode㯠.first() ãæç€ºããå
šã¬ãŒã³ã«äºé²çã«å
¥ãã
// 倿Žå: strict modeã§throw
const tweetText = await article.locator('[data-testid="tweetText"]').innerText()
// 倿ŽåŸ
const tweetText = await article.locator('[data-testid="tweetText"]').first().innerText()
ä¿®æ£ããæç¹ã§ãååã®ç §åã䜿ã£ãŠããå šã¬ãŒã³ã確èªããŠåãä¿®æ£ãå ¥ããŸããã1ç®æã§åŠãã ãå¥ãã©ãããã©ãŒã ã§åçºããé²ãããã®ç¿æ £ã§ãã
10. åã倱æã3åç¶ããããåæ°ã§ãªãæž¡ããŠããå 容ãçã
retry-and-giveup-design.md ã«æžããéãã§ãââãåã倱æã3åæããŠãçµæã¯å€ãããªããåæ°ã§ãªãæž¡ããŠããå
容ãçããã æ±ºå®è«çãªå€±æïŒããªããŒã·ã§ã³éåã»ãã°ã€ã³åãã»ãã³ããŒæèšã®å€åïŒã¯ãªãã©ã€åã«å€±æã®çš®é¡ãåé¡ããªããã°ãåæ°ãå¢ããããšã¯è³æºãçŒãã ãã§ãã
11. ãªãã©ã€åæ°ã¯ãå¯äœçšãäœåèµ·ãããŠããããã§æ±ºãã
åªçã§ãªãæäœã«3åãªãã©ã€ãä»ããããšã¯ãå¯äœçšãæå€§3å蚱容ãã宣èšãšåãã§ããSNSæçš¿ã®ãªãã©ã€ãèšèšããåã«ããã®æäœã¯åªçãããåããŸããåªçã§ãªããã°ãªãã©ã€åã«å¿ ãçŸåšã®ç¶æ ã確èªããæé ãæã¿ãŸãã
12. ã¹ãããåŸ ã¡ã¯å³SKIPã§ãªãæå€§600ç§åŸ æ©ãã
BROWSER_SLOT_WAIT_SEC=600 # 0ã«ããã°åŸæ¥ã®å³skip
# å詊è¡ééã¯ã©ã³ãã ïŒthundering herd 鲿¢ïŒ
sleep $((RANDOM % 30 + 15))
BROWSER_SLOT_WAIT_SEC=0 ã§åŸæ¥ã®å³skipãšå®å
šäžèŽãããããæ¢åã®30æ¬ä»¥äžã®launchdãžã§ããšã®åŸæ¹äºæãä¿ãŠãŸãã
13. ãåŸ ã£ã/諊ãã/ã¹ãããããããå°åž³ã®ãã°ã§åºå¥ãã
waited=600s ã§è«Šããèšé²ãšãexit 0 ã§äœãããçµãã£ãèšé²ãåãè¡ã«èŠãããšãå¹çãæªãããšãããããèµ°ã£ãŠããªããã®åºå¥ãã€ããŸãããåœé°æ§ã®é£ç¶ããç
§åãå£ããŠããããªã®ããæ¬åœã«èµ°ã£ãŠããªãããªã®ããå°åž³ããå€å®ããããã«ããã°ã«çç±ãå¿
ãæ®ããŸãã
14. ãªãã©ã€ã远å ãããããã©ã«ãæ³šå ¥ãåãã³ãããã«å ¥ãã
# æ¬çªã§ã¯ç¡å¹ã泚å
¥æã ãæå¹
WA_API_FAIL_ONCE=2 node index.js
# â "api retry attempt=1/3" â "attempt=2/3" â exit 0 ã§å®èµ°
# æªèšå®ãªã retry ãã°ã1è¡ãåºãªã
ãã©ã«ã泚å
¥ãªãã§ãªãã©ã€ãã远å ããããšèšã£ãŠãã鿬 é害ãåçŸã§ããªãããå®éã«çµè·¯ãéã£ãããšã瀺ããŸãããgit diff ãèŠããŠçµããã«ããªãã
15. å ¬éããŒãžãèªã¿æ»ããŠãããåæ ãããããšèšé²ãã
ãšãã£ã¿ã®ãå
¥ããããšããæ»ãå€ãä¿¡çšããŸãããnoteã®CTAéè€90æ¬ã¯ãã®ç¢ºèªã¹ãããããªãã£ãããšã§çºçããŸãããreader-state-not-self-record ã®ååââèªåã®éä¿¡ãã°ã§ãªãçžæåŽã®å®äœã§å€å®ããââã¯æçš¿ç¢ºèªãšè²Œãä»ã確èªã®äž¡æ¹ã«é©çšããŸãã
ãŸãšã
8æ28æ¥ããçŽ2é±éã§3ã€ã®ãã©ãããã©ãŒã ãé£ç¶ããŠåãåã®å£ãæ¹ãããŸãããIG ã®3æ¬éè€ã»TikTok ã®15æ¥é£ç¶exit 1ã»XåŒçšãã¹ãã®5æ¥éãŒãä»¶ââã©ãããç §ååšãæ£åžžã«åããŠããã®ã«ç §åã®ææãå£ããŠããããšããæ§é ã§ãã
ã¹ã¯ãªããã¯ãšã©ãŒãæããŸãããå°åž³ã«ã¯ ok:false ãç©ãŸãç¶ããã¹ã±ãžã¥ãŒã©ã¯æ·¡ã
ãšåå®è¡ãäºçŽããŸããããžã§ããå£ããŠããããšããä¿¡å·ã¯ã©ãã«ãåºãŸãããæ°ã¥ãã®ã¯å€ããçŸå®ãçŽæ¥èŠããšãã ãã§ãââãããã£ãŒã«ã«3æ¬äžŠãã ã«ã«ãŒã»ã«ãããã·ã¥ããŒãã§ç¢ºèªããæçš¿äžèЧã1é±éåã®ãŒãã«ãŠã³ãéç¥ã®åŸã§ããããéããå°åž³ã
retry-and-giveup-design.md ã®äžæããã®æ§é ã端çã«èšãåã£ãŠããŸãã
åã倱æã3åæããŠãçµæã¯å€ãããªããåæ°ã§ãªãæž¡ããŠããå 容ãçãã
ä»ã®ç°å¢ã§ã¯çå°ç¢ºèªã倱æãããšããå³ãªãã©ã€ã§ã¯ãªãããªã倱æãããããå¥çµè·¯ã§åãã¹ããããæã¿ãŸããAPIãçŽæ¥å©ããŠæçš¿ã®ååšã確èªããååšããã° already-live ãšã㊠ok:true ã§èšé²ããŸããååšããªããã°åããŠãæ¬åœã«æªæçš¿ããšç¢ºå®ããŸãã
ãã®ã確èªããŠãã倿ãããäžæéãã171æ¬ã®ãžã§ããèªåŸãããåææ¡ä»¶ã§ããç°å¢ãäœããšã¯ãèªåãæäœããåæ°ããŒãã«è¿ã¥ããããšã§ãããã®ãŒããä¿ã€ã«ã¯ãå€å®ã®ææãäœã§ããããåãç¶ããããšãå¿ èŠã§ããç §ååšã¯å£ããŸãããå£ããã®ã¯ãç §ååšãèŠãŠããææã§ãã
ä»çµã¿ã®å šäœåã»æ120äžã®å èš³ã»30æ¥æé ã¯æænoteã«ãŸãšããŠããŸãã ð Claude CodeèªåŸç°å¢ã§ãå®éã©ã皌ãã â ä»çµã¿ã»å®äŸã»å§ãæ¹ã»ãµããŒã
LilyïŒ@bokuwalilyïŒâ å人éçºè ãClaude Code ã§èªåååºç€ãçµã¿ãªãããiOSã¢ããªãWebãµãŒãã¹ãéç£ããŠããŸã
- AIã§ãå¯ãŠãŠãåãä»çµã¿ããäœã£ãŠæ120äžã«ãã話㯠noteã®ææèšäº ã«ð°
- OSS: github.com/bokuwalily ð
- ææ°æ å ±ã»ãåãåãã㯠X @bokuwalily ãžð
- AIå°å ¥ã»èªååã®çžè«ãšå®è£ ãã³ãã¬7æ¬ã®é åžã¯ å ¬åŒLINE ããð¬
çããã® â€ïž ãã·ã§ã¢ãå±ã¿ã«ãªããŸãïŒ