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 应用里那些证据阅读页要渲染的——于是页面上的论点,和它背后的原始素材,只隔着一个文件。