← back to Handbag Authentication
crawler/komehyo.js
143 lines
const puppeteer = require('puppeteer');
const axios = require('axios');
class KomehyoScraper {
constructor() {
this.baseUrl = 'https://komehyo.jp/search/';
this.brands = ['gucci', 'louis vuitton', 'chanel', 'hermes'];
this.maxPages = 3;
}
async scrapeSearch(brand, page = 1) {
const searchUrl = `${this.baseUrl}?q=${encodeURIComponent(brand + ' bag')}&wovn=en&sortKey=03&page=${page}`;
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');
console.log(`Scraping Komehyo: ${searchUrl}`);
await browserPage.goto(searchUrl, { waitUntil: 'networkidle2', timeout: 30000 });
// Wait for products to load
await browserPage.waitForSelector('.product-item, .item, .search-result-item', { timeout: 10000 }).catch(() => {});
const listings = await browserPage.evaluate(() => {
const items = [];
// Multiple selectors to handle different page layouts
const productSelectors = [
'.product-item',
'.item',
'.search-result-item',
'[data-product-id]',
'article.product'
];
let productElements = [];
for (const selector of productSelectors) {
productElements = document.querySelectorAll(selector);
if (productElements.length > 0) break;
}
productElements.forEach((item) => {
try {
// Extract image
const imgEl = item.querySelector('img');
const image = imgEl?.src || imgEl?.dataset?.src || '';
// Extract title
const titleEl = item.querySelector('.product-title, .item-name, h3, .title');
const title = titleEl?.textContent?.trim() || '';
// Extract price
const priceEl = item.querySelector('.price, .product-price, [class*="price"]');
const priceText = priceEl?.textContent?.trim() || '';
const priceMatch = priceText.match(/[\d,]+/);
const price = priceMatch ? parseInt(priceMatch[0].replace(/,/g, '')) : 0;
// Extract URL
const linkEl = item.querySelector('a');
const url = linkEl?.href || '';
// Extract ID from URL or data attribute
const idMatch = url.match(/\/(\d+)/);
const id = item.dataset?.productId || idMatch?.[1] || url;
// Extract condition
const conditionEl = item.querySelector('.condition, .item-condition, [class*="condition"]');
const condition = conditionEl?.textContent?.trim() || 'Used';
if (title && price > 0) {
items.push({
external_id: id,
title,
price_jpy: price,
image_url: image,
thumbnail_url: image,
product_url: url.startsWith('http') ? url : `https://komehyo.jp${url}`,
condition,
listing_type: 'buy'
});
}
} catch (e) {
console.error('Error parsing item:', e);
}
});
return items;
});
await browser.close();
return listings;
} catch (error) {
console.error(`Error scraping Komehyo page ${page}:`, error.message);
return [];
}
}
async scrapeAllBrands() {
const allListings = [];
for (const brand of this.brands) {
console.log(`\nScraping Komehyo for ${brand}...`);
for (let page = 1; page <= this.maxPages; page++) {
const listings = await this.scrapeSearch(brand, page);
if (listings.length === 0) {
console.log(`No more results for ${brand} at page ${page}`);
break;
}
// Add brand and source info
listings.forEach(listing => {
listing.brand = brand;
listing.source = 'komehyo';
});
allListings.push(...listings);
console.log(`Found ${listings.length} items on page ${page}`);
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 1000));
}
}
return allListings;
}
// Convert JPY to USD (approximate)
convertToUSD(jpy) {
const exchangeRate = 150; // Update with real-time rate
return Math.round((jpy / exchangeRate) * 100) / 100;
}
}
module.exports = KomehyoScraper;