113 lines
3.3 KiB
TypeScript
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;
|
|
}
|