新增一个平台:策略 + 工厂模式
在 job_scrapers 里,每个招聘网站都是一个平台(platform),而每个平台都通过两块可插拔的部件来接入:一个负责遍历列表页的scraper,和一个把单条职位 HTML 变成结构化数据的parser。二者都在运行时按名字选取,所以新增一个网站的动作就是写两个类、然后注册它们——而不去改驱动抓取的那部分代码。
两个基类,各司其职
ScraperStrategy 是 scraper 的抽象基类。它持有一份 ScraperConfig(platform、baseUrl,以及可选的 rateLimit 和 timeout),并强制实现一个方法:scrape(browser),类型是一个 AsyncGenerator,逐条产出(yield)职位列表。这种生成器形态是刻意为之的——它让每个平台自己决定何时去取下一条职位,把内存占用压平,而不是把整个网站的结果先缓存起来。基类还提供一个用于限速的 sleep() 辅助方法,和一个 getPlatform() 取值方法。
ParserStrategy 是 parser 的抽象基类。它唯一的必需方法 parse(html, jobId, sourceUrl, existingJobDescription?, rawData?) 返回 Promise<ParsedJob>。类上描述的契约是三步流程:先从 DOM 里能抽多少抽多少,再调用 AI 补齐缺失字段,最后合并。可选的 existingJobDescription 让 parser 在职位描述没变时跳过重新解析;rawData 则留给那些返回 JSON 而非 HTML 的平台。共用的辅助方法都放在基类上:validateRequiredFields(坚持 jobTitle 与 jobDescription 都非空)、cleanText、normalizeText(用于比较描述),以及一个默认什么都不做的 enrichFromDescription 钩子。
两个工厂,两种风格
ScraperFactory 是注册表驱动的。一个 STRATEGIES 数组把每个平台列成 { id, name, factory },工厂据此构建一个以 id 为键的 Map。查找时会把传入的名字转小写,所以大小写是宽容的。在这张 map 之上,它提供 createScraper(平台未知时抛错)、normalizePlatformName(任意大小写/别名 → 规范名,或 null)、getSupportedPlatforms 和 isSupported。新增一个 scraper 就是数组里加一行。
ParserFactory 则是switch 驱动的:createParser 用一个 switch 匹配转小写后的平台名并 new 出对应的 parser,落到 default 分支时抛错。它带有几个 scraper 注册表里没有的 parser 分支(echinacities、schrole)——两边并不必须枚举出完全一致的集合。
已接入哪些平台
STRATEGIES 中注册了六个 scraper:DavesESL、TEAST、EChinaCareers、TeachAway、SearchAssociates、SeekTeachers。
注册一个新平台
- 写一个继承
ScraperStrategy并实现scrape()的 scraper 类。 - 写一个继承
ParserStrategy并实现parse()的 parser 类。 - 在
ScraperFactory.ts的STRATEGIES里加一条{ id, name, factory }记录。 - 在
ParserFactory.ts的switch里加一个case。
调用方无需改动:下游一切都已按名字解析 scraper 和 parser。