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

platform-strategy-factory-pattern

新增一个平台:策略 + 工厂模式

job_scrapers 里,每个招聘网站都是一个平台(platform),而每个平台都通过两块可插拔的部件来接入:一个负责遍历列表页的scraper,和一个把单条职位 HTML 变成结构化数据的parser。二者都在运行时按名字选取,所以新增一个网站的动作就是写两个类、然后注册它们——而不去改驱动抓取的那部分代码。

两个基类,各司其职

ScraperStrategy 是 scraper 的抽象基类。它持有一份 ScraperConfigplatformbaseUrl,以及可选的 rateLimittimeout),并强制实现一个方法:scrape(browser),类型是一个 AsyncGenerator逐条产出(yield)职位列表。这种生成器形态是刻意为之的——它让每个平台自己决定何时去取下一条职位,把内存占用压平,而不是把整个网站的结果先缓存起来。基类还提供一个用于限速的 sleep() 辅助方法,和一个 getPlatform() 取值方法。

ParserStrategy 是 parser 的抽象基类。它唯一的必需方法 parse(html, jobId, sourceUrl, existingJobDescription?, rawData?) 返回 Promise<ParsedJob>。类上描述的契约是三步流程:先从 DOM 里能抽多少抽多少,再调用 AI 补齐缺失字段,最后合并。可选的 existingJobDescription 让 parser 在职位描述没变时跳过重新解析;rawData 则留给那些返回 JSON 而非 HTML 的平台。共用的辅助方法都放在基类上:validateRequiredFields(坚持 jobTitlejobDescription 都非空)、cleanTextnormalizeText(用于比较描述),以及一个默认什么都不做的 enrichFromDescription 钩子。

两个工厂,两种风格

ScraperFactory注册表驱动的。一个 STRATEGIES 数组把每个平台列成 { id, name, factory },工厂据此构建一个以 id 为键的 Map。查找时会把传入的名字转小写,所以大小写是宽容的。在这张 map 之上,它提供 createScraper(平台未知时抛错)、normalizePlatformName(任意大小写/别名 → 规范名,或 null)、getSupportedPlatformsisSupported。新增一个 scraper 就是数组里加一行。

ParserFactory 则是switch 驱动的:createParser 用一个 switch 匹配转小写后的平台名并 new 出对应的 parser,落到 default 分支时抛错。它带有几个 scraper 注册表里没有的 parser 分支(echinacitiesschrole)——两边并不必须枚举出完全一致的集合。

已接入哪些平台

STRATEGIES 中注册了六个 scraper:DavesESL、TEAST、EChinaCareers、TeachAway、SearchAssociates、SeekTeachers

注册一个新平台

  1. 写一个继承 ScraperStrategy 并实现 scrape() 的 scraper 类。
  2. 写一个继承 ParserStrategy 并实现 parse() 的 parser 类。
  3. ScraperFactory.tsSTRATEGIES 里加一条 { id, name, factory } 记录。
  4. ParserFactory.tsswitch 里加一个 case

调用方无需改动:下游一切都已按名字解析 scraper 和 parser。

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 →