← back to Handbag Authentication
api/yahoo-data-extractor.js
167 lines
const puppeteer = require('puppeteer');
class YahooDataExtractor {
constructor() {
this.browser = null;
}
async init() {
if (!this.browser) {
this.browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
}
}
async extractFullDetails(yahooUrl) {
await this.init();
const page = await this.browser.newPage();
try {
await page.goto(yahooUrl, { waitUntil: 'networkidle2', timeout: 30000 });
const details = await page.evaluate(() => {
const getText = (selector) => {
const el = document.querySelector(selector);
return el ? el.textContent.trim() : null;
};
const getAllText = (selector) => {
const els = document.querySelectorAll(selector);
return Array.from(els).map(el => el.textContent.trim());
};
// Extract title
const title = getText('h1.ProductTitle__text') ||
getText('.ProductTitle') ||
getText('h1');
// Extract description
const description = getText('.ProductExplanation__body') ||
getText('.ProductExplanation') ||
getText('[class*="explanation"]') ||
getText('[class*="description"]');
// Extract product details table
const detailRows = {};
document.querySelectorAll('.ProductDetail__item, .Product__detail tr').forEach(row => {
const label = row.querySelector('dt, th, .label');
const value = row.querySelector('dd, td:last-child, .value');
if (label && value) {
detailRows[label.textContent.trim()] = value.textContent.trim();
}
});
// Extract images
const images = [];
document.querySelectorAll('.ProductImage__image img, .Product__image img, [class*="ProductImage"] img').forEach(img => {
if (img.src && !img.src.includes('noimage')) {
images.push(img.src.replace(/\.(jpg|jpeg|png).*$/, '.$1')); // Clean URL params
}
});
// Extract condition
const condition = getText('.Product__condition') ||
getText('[class*="condition"]') ||
detailRows['状態'] ||
detailRows['商品の状態'] ||
'';
// Extract brand from various places
const brand = detailRows['ブランド'] ||
detailRows['メーカー'] ||
getText('.Product__brand') ||
'';
// Extract model
const model = detailRows['モデル'] ||
detailRows['型番'] ||
detailRows['品番'] ||
'';
// Extract size
const size = detailRows['サイズ'] ||
detailRows['Size'] ||
'';
// Extract color
const color = detailRows['カラー'] ||
detailRows['色'] ||
detailRows['Color'] ||
'';
// Extract material
const material = detailRows['素材'] ||
detailRows['Material'] ||
'';
// Extract serial number
const serial = detailRows['シリアルナンバー'] ||
detailRows['製造番号'] ||
detailRows['Serial'] ||
'';
// Extract all condition notes
const conditionNotes = getAllText('.Product__condition p, [class*="condition"] p, [class*="note"]');
return {
title,
description,
brand,
model,
size,
color,
material,
serial,
condition,
conditionNotes: conditionNotes.join(' '),
images,
detailTable: detailRows,
extractedAt: new Date().toISOString()
};
});
await page.close();
return details;
} catch (error) {
console.error('Yahoo extraction error:', error);
await page.close();
return null;
}
}
async translateCondition(japaneseCondition) {
const conditionMap = {
'未使用': 'New with tags',
'新品': 'New',
'新品同様': 'Like new',
'極美品': 'Excellent',
'美品': 'Very good',
'AB': 'Good',
'BC': 'Fair',
'CD': 'Poor',
'中古': 'Used',
'ジャンク': 'For parts'
};
for (const [jp, en] of Object.entries(conditionMap)) {
if (japaneseCondition.includes(jp)) {
return en;
}
}
return 'Used';
}
async close() {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
module.exports = YahooDataExtractor;