lefengyang
2026-08-20 d01a28cb030b2cb0ff1d42273c877a0f728519a6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import {IPage, ISearchItem, ISearchParams, ISearchProvider, Order, Sort, StationId} from "../type";
import {HTMLElement, parse} from "node-html-parser";
import {beginToPage, wrappedFetch} from "../lib";
 
 
/**
 * 雅虎拍卖的搜索服务提供者。
 */
export class Provider4YahooAuction implements ISearchProvider {
 
    readonly stationId = StationId.YahooAuction;
 
    async search(params: ISearchParams): Promise<IPage> {
        const warn: string[] = [];
        const {keyword, begin = 1, maxNum = 50, category, minPrice, maxPrice, shipping, sort, order} = params;
        const url = new URL("https://auctions.yahoo.co.jp/search/search");
        url.searchParams.set("p", keyword);
        url.searchParams.set("dest_pref_code", "13");
        url.searchParams.set("b", String(begin));
        url.searchParams.set("n", String(maxNum));
        if (category) {
            url.searchParams.set("auccat", category);
        }
        if (minPrice) {
            url.searchParams.set("min", String(minPrice));
        }
        if (maxPrice) {
            url.searchParams.set("max", String(maxPrice));
        }
        if (minPrice || maxPrice) {
            url.searchParams.set("price_type", 'currentprice');
        }
        if (shipping === 'free') {
            url.searchParams.set("shipping", '1');
        }
        if (sort) {
            if (sort === Sort.Latest) {
                url.searchParams.set("s1", 'new');
                url.searchParams.set("o1", 'a');
            } else if (sort === Sort.BidCount) {
                url.searchParams.set("s1", 'bids');
                url.searchParams.set("o1", 'a');
                if (order === Order.Desc) {
                    warn.push(`sort=${sort} unsupported order=${order}`);
                }
            } else {
                if (sort === Sort.Price) {
                    url.searchParams.set("s1", 'tbids');
                } else if (sort === Sort.BuyItNow) {
                    url.searchParams.set("s1", 'tbidorbuy');
                } else if (sort === Sort.RemainTime) {
                    url.searchParams.set("s1", 'end');
                }
                if (sort !== Sort.Default) {
                    if (order === Order.Desc) {
                        url.searchParams.set("o1", 'd');
                    } else {
                        url.searchParams.set("o1", 'a');
                    }
                }
            }
        }
 
 
        const resp = await wrappedFetch(url);
 
        const pageNum = beginToPage(begin, maxNum);
 
        // noinspection TypeScriptUnresolvedReference
        const html = await resp.text();
 
        if (!resp.ok) {
            if (resp.status === 404 && resp.headers.get('content-type')?.includes('html')) {
                const errorMessage = parseProductsError(html);
                return {...params, dataList: [], error: errorMessage};
            }
            return {...params, dataList: [], error: `HTTP error! status: ${resp.status}`};
        }
 
        const {total, dataList} = parseProducts(html);
 
        return {
            warning: warn.join(', '),
            srcUrl: url.toString(),
            begin,
            maxNum,
            pageNum,
            total,
            ...params,
            dataList,
        };
    }
}
 
 
function parseProducts(docHtml: string): Omit<IPage, keyof ISearchParams> {
    const map = new Map<string, ISearchItem>();
    const doc = parse(docHtml);
    doc.querySelectorAll('.Products__items .Product')
        .map(parseItem)
        .forEach(item => item.itemId && map.set(item.itemId, item));
 
    const totalText = doc.querySelector('.Tab__item--current .Tab__subText')?.textContent;
    const total = totalText ? parseInt(totalText.replace(/\D+/g, '')) : 0;
 
    return {
        total: Math.max(total || 0, map.size),
        dataList: Array.from(map.values())
    };
}
 
 
function parseItem(el: HTMLElement): ISearchItem {
    const priceValueNodes = el.querySelectorAll('.Product__priceInfo .Product__priceValue');
    const priceText = priceValueNodes[0]?.textContent?.replace(/\D+/g, '');
    const buyItNowPriceText = priceValueNodes[1]?.textContent?.replace(/\D+/g, '');
    const shippingFeeText = el.querySelector('.Product__priceInfo .Product__postage')?.textContent?.replace(/\D+/g, '');
 
    return {
        itemId: el.querySelector('a.Product__imageLink')?.getAttribute('data-auction-id'),
        url: el.querySelector('a.Product__imageLink')?.getAttribute('href'),
        image: {
            imageUrl: el.querySelector('img.Product__imageData')?.getAttribute('src'),
            alt: el.querySelector('img.Product__imageData')?.getAttribute('alt'),
        },
        name: el.querySelector('.Product__title')?.textContent?.trim(),
        bid: el.querySelector('.Product__bid')?.textContent?.trim(),
        time: el.querySelector('.Product__time')?.textContent?.trim(),
        price: priceText ? parseInt(priceText) : undefined,
        buyItNowPrice: buyItNowPriceText ? parseInt(buyItNowPriceText) : undefined,
        shippingFee: shippingFeeText ? parseInt(shippingFeeText) : 0,
    };
}
 
/**
 * 从错误页面HTML中解析出错误信息。
 * @param docHtml 错误页面的HTML文档字符串。
 * @returns 返回错误信息字符串。
 */
function parseProductsError(docHtml: string): string {
    const doc = parse(docHtml);
    let msg = doc.querySelector('.SearchMode+div')?.text?.trim();
    if (!msg) {
        msg = doc.querySelector('.msg')?.text?.trim();
    }
    if (!msg) {
        msg = doc.querySelector('#contents')?.text?.trim();
    }
    if (!msg) {
        msg = doc.querySelector('body')?.text?.trim();
    }
    return msg || "Unknown error from Yahoo Auction";
}