2026-09-23·by Sijie Wang#node#project#youteacher#analyze

esl-pain-point-scraper-suite

ESL 痛点抓取脚本集

analyze 是 YouTeacher 里做策略与商业计划的那块——是文档,不是运行时代码。但一份声称「老师正在被骗」「学校每月发这么多职位」的商业计划,得把这些说法坐实。这套脚本就是把它们坐实的方式:youteacher_analyze/scrapers/ 下的一小把一次性 Python 脚本,跑到 ESL 老师真正吐槽和真正招人的地方去,把原始素材拉成磁盘上的 JSON,交给计划里的证据页面渲染。它不是 job_scrapers 那个生产服务——这些是用完即弃的研究脚本,唯一产物是一份市场快照,为了支撑一个论点而抓这一次。

整套脚本被两个问题驱动,脚本也就顺着这两个问题干净地分成两半:痛点是真的吗?(定性——论坛帖子与 Reddit 讨论)和市场有多大?(定量——招聘站的发帖量)。

两种采集方式,对应两类来源

Reddit 有 API,所以 Reddit 这条路就用它:reddit_crawler.py 驱动 PRAW,启动时从 .env 文件读入一组 Reddit 凭据(经由 python-dotenv)来认证——绝不硬编码。这个脚本其余部分完全不需要浏览器。

论坛和招聘站没有可用的 API,所以其余每个脚本都用 Playwright 驱动一个 headless Chromium。每个脚本都开一个带真实桌面 user-agent(多数还设了桌面视口)的浏览器上下文,访问站点自己的页面,等它渲染完,再读 DOM——就是真人访客看到的那一层。之所以要用浏览器,原因正在于此:这些站点由 JavaScript 渲染、对爬虫戒备(Dave's ESL Cafe 挂在 Cloudflare 后面),一个裸 HTTP 客户端几乎拿不到东西。

(一句该写在页面上、而不只是留在代码里的告诫:这些是各站点服务条款约束下的第三方 UGC。脚本会自我限速以示礼貌,但真要重跑,应当遵守 robots.txt 和各站 ToS;抓下来的 JSON 里含有论坛用户的账号名和帖子——那是需要妥善处理、而非拿去转载的个人数据。)

定性这一半:挖吐槽

reddit_crawler.py 是这里最丰富的一个。它的核心是一份手工搭的查询设计:一个字典,把大约十个 subreddit——TEFL/ESL/teach-abroad 这些教学社区,加上在华外籍社区——各自映射到一份专属的搜索词表,按那个社区的用语来调(ESL 版给「recruiter / agency / scam / visa」;中国版给「Z 签 / 被遣返 / 被捕」)。它按全时段搜索、每条查询设上限、对被多条查询命中的帖子去重,并为每帖保留标题、正文、分数、评论数、永久链接、时间戳、作者,以及最靠前的几条评论——因为真正的痛点常常就藏在评论里。

两个设计选择让它是探索式而非印证式。其一,它会把每帖按一套痛点分类法打标签——骗局、薪酬、签证、流失、资质核验、招聘、体验、已读不回、歧视、沟通——但打标签从不用来过滤搜索;一帖不管命不命中都会保留。其二,有一个显式的 unknown 兜底类:不匹配任何已知类别的帖子不被丢弃,而是被归到一起、在输出摘要里标出,作为有待人工审阅的潜在新痛点。这个脚本想发现的是它此前叫不出名字的痛点,而不只是数它预期之内的那些。

三个论坛抓取脚本——daves_esl_scraper.py(Dave's ESL Cafe,一个 phpBB 论坛)、tefl_net_scraper.py(TEFL.net 的亚洲版)、chinese_forums_scraper.py(Chinese-Forums.com 的「在中国教英语」版块)——共用同一形状:先翻几页把帖子列出来,再逐帖进去抽取其中的发言(作者、正文,截断——Dave's 还会额外抓每帖的日期)。它们的差别只在匹配各论坛 HTML 的 CSS 选择器,以及一处过滤:TEFL 和 Chinese-Forums 这两个脚本会先按标题里的痛点关键词(scam、unpaid、visa、fired、nightmare……)收窄,再花时间去抓正文;而 Dave's 那个——对准的是一个纯职位版块——则全部保留,只把命中关键词的标出来。

定量这一半:给市场量尺寸

serious_teachers_scraper.py 从 SeriousTeachers.com 采集中国区职位——每条抓标题、学校、地点、日期、URL。job_volume_analyzer.py 铺得更宽:它同时抓 Dave's ESL Cafe 和 SeriousTeachers 两家的招聘板,来度量发了多少职位、由谁发、多久发一次。它按板、按学校、按月计数;算出月均和年化预估量;并把发帖最多的学校排名。

analyze_existing_jobs.py 收口,且完全不做抓取——它从磁盘读入一份已采集的 Dave's ESL 数据集,把原始职位帖变成一份计划真正会用的数字:有多少不重复的学校在招人、每月的发帖节奏、每家学校发帖频次的分布、以及每校平均发帖数。再由此推出每月在招的不重复学校数的估计——一个完全建立在观测到的发帖之上、而非拍脑袋假设的、自下而上的市场规模校验。具体数字落在商业计划文档里;本节讲的是方法,不是数字。

靠试错换来的健壮,以及底下的设计

这些浏览器脚本里有个反复出现的迹象:抽取都写成逐个试一串选择器直到命中article.job-listing[class*="job"]……),一个都不中时就退回一个合理默认值,或者把页面 HTML 存下来供调试。这正是抓取你控制不了的站点时诚实的签名——标记会变,所以代码选择兜底,而不假设有唯一正确的选择器。

底下,这套脚本表达的是一个立场:计划里的说法,都该能追溯到某个陌生人也能重新采集一遍的东西。 痛点脚本把零散的轶事变成一份分了类、可链接的语料(还给尚未被命名的痛点留了个出口);量化脚本把「市场很大」变成一份数过、带日期、按学校统计的账。两者都把纯 JSON 落到磁盘,而这份 JSON 正是 analyze 应用里那些证据阅读页要渲染的——于是页面上的论点,和它背后的原始素材,只隔着一个文件。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →