130 lines
3.5 KiB
JavaScript
130 lines
3.5 KiB
JavaScript
import puppeteer from "puppeteer";
|
|
import express from "express";
|
|
import "dotenv/config";
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// Symbol configuration
|
|
const symbols = {
|
|
BPIDEST2040: {
|
|
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040",
|
|
name: "BPI Destino PPR 2040",
|
|
},
|
|
BPIDEST2050: {
|
|
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050",
|
|
name: "BPI Destino PPR 2050",
|
|
},
|
|
};
|
|
|
|
// Function to scrape price for a given symbol
|
|
async function getLatestPrice(symbol) {
|
|
const config = symbols[symbol];
|
|
|
|
if (!config) {
|
|
throw new Error(`Invalid symbol: ${symbol}`);
|
|
}
|
|
|
|
// Launch the browser
|
|
const browser = await puppeteer.launch({
|
|
args: ["--no-sandbox"],
|
|
timeout: 10000,
|
|
});
|
|
|
|
try {
|
|
// Open a new tab
|
|
const page = await browser.newPage();
|
|
|
|
// Visit the page and wait until network connections are completed
|
|
await page.goto(config.url, { waitUntil: "networkidle2" });
|
|
|
|
// Interact with the DOM to retrieve the price and date
|
|
const [price, date] = await page.evaluate(() => {
|
|
const spanTags = document.getElementsByTagName("span");
|
|
const priceSearchText = "ÚLTIMA COTAÇÃO:";
|
|
const dateSearchText = "DATA COTAÇÃO:";
|
|
let priceElementFound;
|
|
let dateElementFound;
|
|
|
|
for (let i = 0; i < spanTags.length; i++) {
|
|
if (spanTags[i].textContent.trim() == priceSearchText) {
|
|
priceElementFound = spanTags[i];
|
|
if (priceElementFound && dateElementFound) break;
|
|
}
|
|
if (spanTags[i].textContent.trim() == dateSearchText) {
|
|
dateElementFound = spanTags[i];
|
|
if (priceElementFound && dateElementFound) break;
|
|
}
|
|
}
|
|
|
|
if (!priceElementFound || !dateElementFound) {
|
|
throw new Error("Could not find price or date elements");
|
|
}
|
|
|
|
return [
|
|
priceElementFound.nextSibling.innerHTML,
|
|
dateElementFound.nextSibling.innerHTML,
|
|
];
|
|
});
|
|
|
|
const marketPrice = parseFloat(
|
|
price.replace("€", "").trim().replace(",", "."),
|
|
);
|
|
|
|
return {
|
|
symbol,
|
|
name: config.name,
|
|
currency: "EUR",
|
|
price: marketPrice,
|
|
date: date.trim(),
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
} finally {
|
|
// Always close the browser
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
// API endpoint to get latest price for a symbol
|
|
app.get("/price/:symbol", async (req, res) => {
|
|
const { symbol } = req.params;
|
|
|
|
try {
|
|
console.log(`Fetching price for symbol: ${symbol}`);
|
|
const priceData = await getLatestPrice(symbol);
|
|
console.log(`Success: ${symbol} - €${priceData.price}`);
|
|
res.json(priceData);
|
|
} catch (error) {
|
|
console.error(`Error fetching price for ${symbol}:`, error.message);
|
|
res.status(400).json({
|
|
error: error.message,
|
|
symbol,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get("/health", (req, res) => {
|
|
res.json({ status: "ok", availableSymbols: Object.keys(symbols) });
|
|
});
|
|
|
|
// Root endpoint with usage instructions
|
|
app.get("/", (req, res) => {
|
|
res.json({
|
|
message: "BPI Stock Price Scraper API",
|
|
endpoints: {
|
|
health: "/health",
|
|
price: "/price/:symbol",
|
|
},
|
|
availableSymbols: Object.keys(symbols),
|
|
example: `/price/BPIDEST2040`,
|
|
});
|
|
});
|
|
|
|
// Start the server
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on http://localhost:${PORT}`);
|
|
console.log(`Available symbols: ${Object.keys(symbols).join(", ")}`);
|
|
console.log(`Example: http://localhost:${PORT}/price/BPIDEST2040`);
|
|
});
|