← back to Handbag Authentication
crawler/hermes-specialist.js
186 lines
const puppeteer = require('puppeteer');
class HermesSpecialistScraper {
constructor() {
this.baseUrl = 'https://auctions.yahoo.co.jp/search/search';
// Specific Hermès bag models with Japanese names
this.hermesModels = [
'エルメス バーキン', // Birkin
'エルメス ケリー', // Kelly
'エルメス コンスタンス', // Constance
'エルメス ピコタン', // Picotin
'エルメス エヴリン', // Evelyne
'エルメス ガーデンパーティ', // Garden Party
'エルメス ボリード', // Bolide
'エルメス リンディ', // Lindy
'エルメス ジプシエール', // Jypsiere
'エルメス エールバッグ', // Herbag
'エルメス トリム', // Trim
'エルメス フールトゥ', // Fourre Tout
'Hermes Birkin',
'Hermes Kelly',
'Hermes Constance'
];
this.maxPages = 15; // More pages for high-value Hermès bags
}
async scrapeSearch(model, page = 1) {
try {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const browserPage = await browser.newPage();
await browserPage.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
const searchUrl = `${this.baseUrl}?p=${encodeURIComponent(model)}&b=${(page - 1) * 50 + 1}`;
console.log(`Scraping Hermès specialist: ${searchUrl}`);
await browserPage.goto(searchUrl, { waitUntil: 'networkidle2', timeout: 30000 });
await browserPage.waitForSelector('.Product, .Product__item', { timeout: 10000 }).catch(() => {});
const listings = await browserPage.evaluate(() => {
const items = [];
const productElements = document.querySelectorAll('.Product, .Product__item');
productElements.forEach((item) => {
try {
const titleEl = item.querySelector('.Product__title, a');
const title = titleEl?.textContent?.trim() || '';
// Better price extraction - try multiple selectors
let priceText = '';
const priceSelectors = [
'.Product__price',
'.u-tax',
'.Price',
'.Product__priceValue',
'[class*="price"]',
'[class*="Price"]'
];
for (const selector of priceSelectors) {
const priceEl = item.querySelector(selector);
if (priceEl && priceEl.textContent.match(/[\d,]+/)) {
priceText = priceEl.textContent.trim();
break;
}
}
// Also try getting price from anywhere in the item
if (!priceText) {
const allText = item.textContent || '';
const pricePattern = /(\d{1,3}(?:,\d{3})+)\s*円|¥\s*(\d{1,3}(?:,\d{3})+)/;
const match = allText.match(pricePattern);
if (match) {
priceText = match[1] || match[2];
}
}
const priceMatch = priceText.match(/[\d,]+/);
const price = priceMatch ? parseInt(priceMatch[0].replace(/,/g, '')) : 0;
const imgEl = item.querySelector('img');
const image = imgEl?.src || '';
const linkEl = item.querySelector('a');
const url = linkEl?.href || '';
const idMatch = url.match(/\/([a-z0-9]+)$/);
const id = idMatch?.[1] || url;
const timeEl = item.querySelector('.Product__time');
const endTime = timeEl?.textContent?.trim() || '';
// Try to extract size info from title
const sizeMatch = title.match(/(\d+)\s*cm|サイズ\s*(\d+)/i);
const size = sizeMatch ? (sizeMatch[1] || sizeMatch[2]) + 'cm' : '';
// Try to extract color
const colorMatch = title.match(/(ブラック|ホワイト|レッド|ブルー|グリーン|オレンジ|ピンク|ベージュ|グレー|Black|White|Red|Blue|Green|Orange|Pink|Beige|Grey|Etoupe|Gold|Rose)/i);
const color = colorMatch ? colorMatch[1] : '';
// Try to extract material
const materialMatch = title.match(/(トゴ|エプソン|クレマンス|トリヨンクレマンス|スイフト|ヴォーエプソン|Togo|Epsom|Clemence|Swift|Box|Crocodile|Alligator|Lizard)/i);
const material = materialMatch ? materialMatch[1] : '';
// Detect condition
let condition = 'Used';
if (title.match(/未使用|新品|UNUSED|NEW/i)) {
condition = 'New/Unused';
} else if (title.match(/美品|極美品|EXCELLENT/i)) {
condition = 'Excellent';
} else if (title.match(/良品|GOOD/i)) {
condition = 'Good';
} else if (title.match(/難あり|ジャンク|JUNK|DAMAGED/i)) {
condition = 'Fair/Damaged';
}
if (title && price > 0) {
items.push({
external_id: id,
title,
price_jpy: price,
image_url: image,
thumbnail_url: image,
product_url: url,
listing_type: 'auction',
auction_end_date: endTime,
condition,
size,
color,
material
});
}
} catch (e) {
console.error('Error parsing Hermès item:', e);
}
});
return items;
});
await browser.close();
return listings;
} catch (error) {
console.error(`Error scraping Hermès specialist:`, error.message);
return [];
}
}
async scrapeAllModels() {
const allListings = [];
for (const model of this.hermesModels) {
console.log(`\nScraping Hermès specialist for ${model}...`);
for (let page = 1; page <= this.maxPages; page++) {
const listings = await this.scrapeSearch(model, page);
if (listings.length === 0) break;
listings.forEach(listing => {
listing.brand = 'Hermes';
listing.source = 'yahoo_hermes_specialist';
listing.model = model;
});
allListings.push(...listings);
console.log(`Found ${listings.length} Hermès items on page ${page}`);
// Longer delay for high-value scraping to avoid rate limits
await new Promise(resolve => setTimeout(resolve, 4000));
}
}
console.log(`\n✨ Hermès specialist crawl complete: ${allListings.length} total items`);
return allListings;
}
}
module.exports = HermesSpecialistScraper;