first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright:v1.44.0-jammy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
RUN npx playwright install chromium
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Link Scraper
|
||||||
|
|
||||||
|
A lightweight API built with Express and Playwright to scrape and extract links from web pages.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "link-scraper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "ts-node-dev --respawn src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"playwright": "^1.44.0",
|
||||||
|
"cors": "^2.8.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"ts-node-dev": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import { scrapeRoute } from "./routes/scrape";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get("/health", (_req, res) => res.json({ status: "ok" }));
|
||||||
|
app.use("/scrape", scrapeRoute);
|
||||||
|
|
||||||
|
app.listen(PORT, () => console.log(`Link scraper running on :${PORT}`));
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { extractNavLinks } from "../services/scraper";
|
||||||
|
|
||||||
|
export const scrapeRoute = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /scrape
|
||||||
|
* Body: { "url": "https://firma.com" }
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* {
|
||||||
|
* "source": "https://firma.com",
|
||||||
|
* "count": 6,
|
||||||
|
* "links": [
|
||||||
|
* { "label": "Hakkımızda", "url": "https://firma.com/hakkimizda" },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
scrapeRoute.post("/", async (req: Request, res: Response) => {
|
||||||
|
const { url } = req.body as { url?: string };
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({ error: "'url' alanı zorunlu" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basit URL validasyonu
|
||||||
|
try { new URL(url); } catch {
|
||||||
|
return res.status(400).json({ error: "Geçersiz URL formatı" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const links = await extractNavLinks(url);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
source: url,
|
||||||
|
count: links.length,
|
||||||
|
links,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Link çekme başarısız",
|
||||||
|
detail: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { chromium, Browser } from "playwright";
|
||||||
|
|
||||||
|
let browser: Browser | null = null;
|
||||||
|
|
||||||
|
async function getBrowser(): Promise<Browser> {
|
||||||
|
if (!browser || !browser.isConnected()) {
|
||||||
|
browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar'da atlanacak anahtar kelimeler
|
||||||
|
const SKIP_KEYWORDS = [
|
||||||
|
"kariyer", "/ik", "kvkk", "gizlilik", "cerez",
|
||||||
|
"blog", "haber", "saglikbulteni", "saglik-rehberi",
|
||||||
|
"rezervasyon", "randevu", "e-sonuc", "checkup",
|
||||||
|
"wp-admin", "wp-content", "feed", "sitemap",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SKIP_SCHEMES = ["mailto:", "tel:", "javascript:", "#"];
|
||||||
|
|
||||||
|
export interface NavLink {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractNavLinks(targetUrl: string): Promise<NavLink[]> {
|
||||||
|
const base = new URL(targetUrl);
|
||||||
|
const b = await getBrowser();
|
||||||
|
const page = await b.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||||
|
await page.waitForTimeout(1200); // dropdown'ların render edilmesi için
|
||||||
|
|
||||||
|
// Navbar içindeki tüm <a> etiketlerini çek
|
||||||
|
const rawLinks = await page.evaluate(() => {
|
||||||
|
const selectors = [
|
||||||
|
"nav a", "header a",
|
||||||
|
"[class*='menu'] a", "[class*='navbar'] a",
|
||||||
|
"[class*='nav-'] a", "[id*='menu'] a", "[id*='nav'] a",
|
||||||
|
];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const links: { href: string; label: string }[] = [];
|
||||||
|
|
||||||
|
for (const sel of selectors) {
|
||||||
|
document.querySelectorAll<HTMLAnchorElement>(sel).forEach((el) => {
|
||||||
|
const href = el.href || el.getAttribute("href") || "";
|
||||||
|
const label = el.textContent?.trim().replace(/\s+/g, " ") || "";
|
||||||
|
if (href && !seen.has(href)) {
|
||||||
|
seen.add(href);
|
||||||
|
links.push({ href, label });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtrele ve temizle
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const result: NavLink[] = [];
|
||||||
|
|
||||||
|
for (const { href, label } of rawLinks) {
|
||||||
|
if (!href) continue;
|
||||||
|
if (SKIP_SCHEMES.some((s) => href.startsWith(s))) continue;
|
||||||
|
|
||||||
|
let parsed: URL;
|
||||||
|
try { parsed = new URL(href, targetUrl); } catch { continue; }
|
||||||
|
|
||||||
|
// Sadece aynı domain
|
||||||
|
if (parsed.hostname !== base.hostname) continue;
|
||||||
|
|
||||||
|
const path = parsed.pathname.toLowerCase();
|
||||||
|
|
||||||
|
// Atlanacak sayfalar
|
||||||
|
if (SKIP_KEYWORDS.some((kw) => path.includes(kw))) continue;
|
||||||
|
|
||||||
|
// Fragment ve trailing slash temizle
|
||||||
|
parsed.hash = "";
|
||||||
|
const normalized = parsed.toString().replace(/\/$/, "");
|
||||||
|
|
||||||
|
if (seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
|
||||||
|
result.push({ label: label || pathToLabel(parsed.pathname), url: normalized });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToLabel(pathname: string): string {
|
||||||
|
const last = pathname.split("/").filter(Boolean).pop() || "anasayfa";
|
||||||
|
return last.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user