vivino-spider/index.ts

113 lines
3.3 KiB
TypeScript

import { Page, chromium } from "playwright";
import sheet from "./sheet.js";
import { ListWineResponseData, WineListItem } from "./model/WineList.js";
import { PageResponse } from "./model/util.js";
import { StaticWineDetailResponseData } from "./model/StaticWineDetail.js";
import { UserWineDetailResponseData } from "./model/UserWineDetail.js";
import path from "path";
const wsEndpoint = "ws://localhost:12224/d73f2ab43afd2d5beb5bf5a7e626b5d1";
const browser = await chromium.connect(wsEndpoint, {
slowMo: 1000,
});
const MIAN_PAGE = "https://www.vivino.cc/";
const requestMap = {
list: "/app-api/appapi/wine/web/vintage/nodeFind",
staticDetail: "/app-api/appapi/wine/web/findWineStaticDetailsWeb",
userSpecialDetail: "/app-api/appapi/wine/web/findWineUserSpecialWeb",
};
const page = await browser.newPage({
baseURL: MIAN_PAGE,
});
page.setViewportSize({
width: 2560,
height: 1440,
});
let times = 0;
const MAX_PAGE = 2;
await Promise.all([page.goto("/search"), startSpide()]);
await browser.close();
async function startSpide() {
while (times < MAX_PAGE) {
await spideWineList(page);
const locator = page.locator(".btn-next");
if (await locator.isDisabled()) {
break;
}
await locator.click();
times++;
}
await sheet?.workbook.xlsx.writeFile(
path.resolve(process.cwd(), "./store/红酒-" + Date.now() + ".xlsx")
);
}
async function spideWineList(page: Page) {
const wineList = await waitForListRequestResponse(page);
for (let index = 0; index < wineList.length; index++) {
const wine = wineList[index];
const { uuid, vintageName } = wine;
const url = new URL("https://www.vivino.cc/detail");
url.searchParams.set("wineUUID", uuid);
url.searchParams.set("name", vintageName);
const newPage = await browser.newPage();
const [_, row] = await Promise.all([
newPage.goto(url.href),
spideWineDetail(newPage, wine),
]);
sheet?.addRow(row);
}
}
async function spideWineDetail(page: Page, wine: WineListItem) {
const staticDetailResponse = await page.waitForResponse((respone) =>
respone.url().includes(requestMap.staticDetail)
);
const userDetailResponse = await page.waitForResponse((respone) =>
respone.url().includes(requestMap.userSpecialDetail)
);
const { data: wineStaticDetail } = await staticDetailResponse.json();
const { data: wineUserDetail } = await userDetailResponse.json();
const { vintageRate, price, numOfVintageRates, country, imgUrl } = wine;
const {
wineName,
alcohol,
wineryCountryName,
wineryRegionName,
wineryName,
flavor,
typeName,
style,
grapes,
} = wineStaticDetail as StaticWineDetailResponseData;
const {} = wineUserDetail as UserWineDetailResponseData;
await page.close();
return [
wineName,
price,
vintageRate,
numOfVintageRates,
country,
imgUrl,
`${wineryCountryName}·${wineryRegionName}·${wineryName}`,
`${wineryCountryName}·${wineryRegionName}`,
flavor,
typeName,
grapes.map((grape) => grape.grapesName).join("、"),
style,
alcohol,
];
}
async function waitForListRequestResponse(page: Page) {
const response = await page.waitForResponse((response) =>
response.url().includes(requestMap.list)
);
if (response) {
const res: PageResponse<ListWineResponseData> = await response.json();
return res.data.records;
}
return response;
}