← back to Professional Directory
agents/crawler-agent/crawlers/ucla-health.js
56 lines
/**
* UCLA Health provider directory crawler — Stage 5.
*
* Public, robots-aware. Polite rate-limit (0.5 rps) via lib/compliance.
* Discovers provider profile URLs from the main directory and extracts each.
*/
const { fetchCompliant, setHostRateLimit } = require('../../shared/compliance');
const { extractProviderCards, extractProviderProfile } = require('../framework/extractor');
const SOURCE_NAME = 'UCLA Health (uclahealth.org)';
const BASE = 'https://www.uclahealth.org';
const ENTRY = `${BASE}/providers`;
setHostRateLimit('www.uclahealth.org', 0.5);
async function fetchHtml(url) {
const res = await fetchCompliant(url, { accept: 'text/html' });
if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`);
return res.text();
}
async function* listProviderUrls() {
// The main directory is paginated; UCLA uses ?page= or ?p= depending on view.
for (let page = 1; page <= 200; page++) {
const url = `${ENTRY}?page=${page}`;
const html = await fetchHtml(url);
const cards = extractProviderCards(html, url);
if (cards.length === 0) return;
for (const c of cards) {
if (c.profile_url) yield c.profile_url;
}
}
}
async function crawl({ onProvider, maxPages = Infinity } = {}) {
let count = 0;
for await (const url of listProviderUrls()) {
if (count >= maxPages) break;
try {
const html = await fetchHtml(url);
const profile = extractProviderProfile(html, url);
profile.hospital_system = SOURCE_NAME;
await onProvider(profile);
} catch (e) {
if (e.code === 'CAPTCHA_DETECTED') {
console.error(`[ucla] CAPTCHA — aborting per compliance`); break;
}
console.error(`[ucla] error ${url}: ${e.message}`);
}
count++;
}
return count;
}
module.exports = { SOURCE_NAME, crawl };