← back to Cypress Awards
backend/src/scraper/main.js
258 lines
const CyPresFinder = require('./cypresFinder');
const Organization = require('../models/Organization');
const { query } = require('../models/database');
const natural = require('natural');
require('dotenv').config();
class CyPresScraper {
constructor() {
this.finder = new CyPresFinder({
delay: parseInt(process.env.SCRAPER_DELAY_MS) || 2000,
userAgent: process.env.SCRAPER_USER_AGENT
});
this.classifier = new natural.BayesClassifier();
this.loadClassifier();
}
// Load or train text classifier for categorization
loadClassifier() {
// Training data for automatic categorization
const trainingData = [
{ text: 'legal aid services lawyer attorney court', categories: ['Legal Aid'] },
{ text: 'education school student learning university', categories: ['Education & Research'] },
{ text: 'health medical healthcare clinic hospital', categories: ['Health Services'] },
{ text: 'environment conservation climate nature wildlife', categories: ['Environmental Conservation'] },
{ text: 'community development neighborhood local residents', categories: ['Community Development'] },
{ text: 'arts culture music theater performance', categories: ['Arts & Culture'] },
{ text: 'children youth kids students teens', categories: ['Youth & Children'] },
{ text: 'veterans military service armed forces', categories: ['Veterans Services'] },
{ text: 'disability accessible wheelchair special needs', categories: ['Disability Rights'] },
{ text: 'seniors elderly aging retirement', categories: ['Senior Services'] },
{ text: 'advocacy policy reform legislation government', categories: ['Advocacy & Policy'] },
{ text: 'technology digital internet innovation software', categories: ['Technology & Innovation'] },
{ text: 'animal pets rescue shelter wildlife', categories: ['Animal Welfare'] },
{ text: 'international global humanitarian relief development', categories: ['International Development'] },
{ text: 'social services welfare assistance support', categories: ['Social Services'] }
];
// Train classifier
trainingData.forEach(item => {
item.categories.forEach(category => {
this.classifier.addDocument(item.text, category);
});
});
this.classifier.train();
}
// Categorize organization based on mission and content
categorizeOrganization(text) {
const categories = [];
const classification = this.classifier.getClassifications(text.toLowerCase());
// Take top 3 categories with confidence > 0.1
classification
.filter(c => c.value > 0.1)
.slice(0, 3)
.forEach(c => categories.push(c.label));
return categories;
}
// Identify legal topics from text
identifyLegalTopics(text) {
const lowerText = text.toLowerCase();
const topics = [];
const topicKeywords = {
'Consumer Protection': ['consumer', 'fraud', 'advertising', 'ftc', 'unfair practices'],
'Civil Rights': ['civil rights', 'discrimination', 'equality', 'voting', 'ada', 'aclu'],
'Environmental Law': ['environment', 'pollution', 'climate', 'epa', 'conservation'],
'Labor & Employment': ['labor', 'employment', 'wage', 'workplace', 'union', 'workers'],
'Privacy & Data Security': ['privacy', 'data breach', 'security', 'gdpr', 'personal information'],
'Healthcare': ['healthcare', 'medical', 'health insurance', 'medicare', 'medicaid'],
'Education': ['education', 'school', 'student rights', 'title ix', 'university'],
'Housing': ['housing', 'tenant', 'homeless', 'eviction', 'fair housing'],
'Immigration': ['immigration', 'asylum', 'deportation', 'refugee', 'visa', 'immigrant'],
'Criminal Justice': ['criminal justice', 'prison', 'conviction', 'sentencing', 'incarceration'],
'Technology & Internet': ['technology', 'internet', 'digital rights', 'net neutrality', 'tech'],
'Financial Services': ['financial', 'banking', 'securities', 'predatory lending', 'credit']
};
for (const [topic, keywords] of Object.entries(topicKeywords)) {
let matchCount = 0;
for (const keyword of keywords) {
if (lowerText.includes(keyword)) {
matchCount++;
}
}
if (matchCount >= 2) {
topics.push(topic);
}
}
return topics;
}
// Scrape a single non-profit
async scrapeNonProfit(websiteUrl) {
console.log(`\nScraping: ${websiteUrl}`);
try {
// Check robots.txt
const allowed = await this.finder.checkRobotsTxt(websiteUrl);
if (!allowed) {
console.log(`Robots.txt disallows scraping: ${websiteUrl}`);
await this.logScraping(websiteUrl, 'blocked', 'Blocked by robots.txt');
return null;
}
// Find cy pres page
const cyPresPage = await this.finder.findCyPresPage(websiteUrl);
if (!cyPresPage) {
console.log(`No cy pres page found: ${websiteUrl}`);
await this.logScraping(websiteUrl, 'no_cypres_page', 'No cy pres page found');
return null;
}
// Extract organization details
const orgDetails = await this.finder.extractOrgDetails(cyPresPage.html, websiteUrl);
const cyPresStatement = this.finder.extractCyPresStatement(cyPresPage.html);
// Categorize and identify topics
const combinedText = `${orgDetails.mission} ${cyPresStatement.statement_text}`;
const categories = this.categorizeOrganization(combinedText);
const legalTopics = this.identifyLegalTopics(combinedText);
// Save to database
const org = await Organization.create({
name: orgDetails.name || new URL(websiteUrl).hostname,
website_url: websiteUrl,
cypres_page_url: cyPresPage.url,
mission_statement: orgDetails.mission,
logo_url: orgDetails.logo,
location_country: 'USA'
});
// Add categories and legal topics
if (categories.length > 0) {
await Organization.addCategories(org.id, categories);
}
if (legalTopics.length > 0) {
await Organization.addLegalTopics(org.id, legalTopics);
}
// Save cy pres statement
await Organization.saveCyPresStatement(org.id, cyPresStatement);
await this.logScraping(websiteUrl, 'success', 'Successfully scraped');
console.log(`✓ Successfully scraped: ${orgDetails.name}`);
console.log(` Categories: ${categories.join(', ')}`);
console.log(` Legal Topics: ${legalTopics.join(', ')}`);
return org;
} catch (error) {
console.error(`Error scraping ${websiteUrl}:`, error.message);
await this.logScraping(websiteUrl, 'failed', error.message);
return null;
}
}
// Log scraping activity
async logScraping(url, status, message) {
const sql = `
INSERT INTO scraping_logs (url, status, error_message)
VALUES ($1, $2, $3)
`;
await query(sql, [url, status, message]);
}
// Scrape multiple non-profits from a list
async scrapeList(urls, maxConcurrent = 5) {
console.log(`Starting scraping of ${urls.length} organizations...`);
console.log(`Max concurrent requests: ${maxConcurrent}\n`);
const results = [];
for (let i = 0; i < urls.length; i += maxConcurrent) {
const batch = urls.slice(i, i + maxConcurrent);
const batchPromises = batch.map(url => this.scrapeNonProfit(url));
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach(result => {
if (result.status === 'fulfilled') {
results.push(result.value);
}
});
console.log(`\nProgress: ${Math.min(i + maxConcurrent, urls.length)}/${urls.length}`);
}
const successful = results.filter(r => r !== null).length;
console.log(`\n=== Scraping Complete ===`);
console.log(`Total: ${urls.length}`);
console.log(`Successful: ${successful}`);
console.log(`Failed: ${urls.length - successful}`);
return results;
}
}
// Main execution
if (require.main === module) {
const scraper = new CyPresScraper();
const args = process.argv.slice(2);
const isTest = args.includes('--test');
const isLarge = args.includes('--large');
const isMega = args.includes('--mega');
// Choose URL list based on arguments
let nonprofitUrls;
if (isMega) {
nonprofitUrls = require('../utils/nonprofitUrls_mega');
} else if (isLarge) {
nonprofitUrls = require('../utils/nonprofitUrls_large');
} else {
nonprofitUrls = require('../utils/nonprofitUrls');
}
if (isTest) {
console.log('Running in TEST mode with 3 sample URLs...\n');
const testUrls = nonprofitUrls.slice(0, 3);
scraper.scrapeList(testUrls, 1)
.then(() => {
console.log('\nTest complete. Exiting...');
process.exit(0);
})
.catch(error => {
console.error('Error:', error);
process.exit(1);
});
} else {
// Production mode - scrape all URLs
console.log(`Running in PRODUCTION mode with ${nonprofitUrls.length} URLs...\n`);
const estimatedHours = Math.ceil((nonprofitUrls.length * 2.5) / 3600); // ~2.5 seconds per URL average
console.log(`Estimated time: ${estimatedHours} hours\n`);
const maxConcurrent = parseInt(process.env.SCRAPER_MAX_CONCURRENT) || 5;
scraper.scrapeList(nonprofitUrls, maxConcurrent)
.then(() => {
console.log('\nProduction scraping complete. Exiting...');
process.exit(0);
})
.catch(error => {
console.error('Error:', error);
process.exit(1);
});
}
}
module.exports = CyPresScraper;