Skip to content

Commit f203287

Browse files
Revert "fix(osvita): послідовний + браузероподібний парс, щоб не тригерити anti-bot"
This reverts commit d38fba6.
1 parent d38fba6 commit f203287

2 files changed

Lines changed: 13 additions & 41 deletions

File tree

internal/parser/osvita/osvita.go

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,9 @@ const (
3535
programHost = "vstup.osvita.ua"
3636
defaultAPIURL = "https://vstup.osvita.ua/api/"
3737
defaultPageSize = 500
38-
// defaultWorkers is 1 — sequential pagination, matching the browser and the
39-
// Python original. osvita's app rate-limits per IP and answers "Error 300 /
40-
// Перезавантажте" to bursty traffic; the previous 8-worker fan-out tripped
41-
// that on the very first program and returned 0 applicants. A single lane
42-
// (0, 500, 1000, …) stays under the radar; big programs are only a handful
43-
// of pages, so the latency cost is small.
44-
defaultWorkers = 1
45-
defaultTimeout = 60 * time.Second
46-
defaultRetries = 3
47-
48-
// browserUA makes our requests look like a real browser. osvita gates the
49-
// applicant API behind a Cloudflare managed challenge that flags obvious
50-
// bots (the Go default "Go-http-client/1.1" UA among them).
51-
browserUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
38+
defaultWorkers = 8
39+
defaultTimeout = 60 * time.Second
40+
defaultRetries = 3
5241

5342
// maxRequests caps how many applicant rows a single Parse will
5443
// accumulate. Real programs top out at a few thousand; this is ~100x
@@ -107,8 +96,8 @@ func New(opts ...Option) *Parser {
10796
// fails fast (short-circuiting the retry loop) once the host starts
10897
// returning 429/5xx. Applied last so it wraps any custom transport.
10998
p.client.Transport = httpx.NewGate(p.client.Transport, httpx.Limits{
110-
RPS: 4, // gentle — osvita rate-flags bursty traffic with "Error 300"
111-
Burst: 2,
99+
RPS: 12,
100+
Burst: defaultWorkers,
112101
FailThreshold: 8,
113102
OpenFor: 15 * time.Second,
114103
})
@@ -133,7 +122,7 @@ func (p *Parser) Parse(ctx context.Context, programURL string) (*abit.Program, e
133122
return nil, fmt.Errorf("osvita: static page: %w", err)
134123
}
135124

136-
if err := p.fanOut(ctx, prog, programURL, sid, uid, year); err != nil {
125+
if err := p.fanOut(ctx, prog, sid, uid, year); err != nil {
137126
return nil, fmt.Errorf("osvita: requests: %w", err)
138127
}
139128
return prog, nil
@@ -169,7 +158,7 @@ func parseProgramURL(rawURL string) (sid, uid, year string, err error) {
169158
//
170159
// On the first hard error a worker records it and cancels the shared
171160
// context, so sibling lanes stop promptly instead of hammering osvita.
172-
func (p *Parser) fanOut(ctx context.Context, prog *abit.Program, referer, sid, uid, year string) error {
161+
func (p *Parser) fanOut(ctx context.Context, prog *abit.Program, sid, uid, year string) error {
173162
ctx, cancel := context.WithCancel(ctx)
174163
defer cancel()
175164

@@ -183,7 +172,7 @@ func (p *Parser) fanOut(ctx context.Context, prog *abit.Program, referer, sid, u
183172

184173
// Warm-up: osvita.ua frequently returns an empty body to a "cold"
185174
// session, so prime the cookie jar with one throwaway request.
186-
_, _ = p.fetchJSONURL(ctx, formValues(year, sid, uid, 0), referer)
175+
_, _ = p.fetchJSONURL(ctx, formValues(year, sid, uid, 0))
187176

188177
for w := 0; w < p.workers; w++ {
189178
wg.Add(1)
@@ -194,7 +183,7 @@ func (p *Parser) fanOut(ctx context.Context, prog *abit.Program, referer, sid, u
194183
if ctx.Err() != nil {
195184
return
196185
}
197-
chunk, err := p.fetchChunk(ctx, referer, sid, uid, year, offset)
186+
chunk, err := p.fetchChunk(ctx, sid, uid, year, offset)
198187
if err != nil {
199188
// A cancellation triggered by a sibling's error isn't a
200189
// new failure — don't overwrite the real firstErr with it.
@@ -280,12 +269,12 @@ var errEmptyURL = errors.New("osvita: empty url response")
280269

281270
// fetchChunk runs the two-step API dance: POST to get a signed JSON URL,
282271
// then GET that URL.
283-
func (p *Parser) fetchChunk(ctx context.Context, referer, sid, uid, year string, last int) (*rawChunk, error) {
272+
func (p *Parser) fetchChunk(ctx context.Context, sid, uid, year string, last int) (*rawChunk, error) {
284273
form := formValues(year, sid, uid, last)
285274

286275
var jsonURL string
287276
err := p.retry(ctx, func() error {
288-
u, err := p.fetchJSONURL(ctx, form, referer)
277+
u, err := p.fetchJSONURL(ctx, form)
289278
if err != nil {
290279
return err
291280
}
@@ -317,20 +306,12 @@ func (p *Parser) fetchChunk(ctx context.Context, referer, sid, uid, year string,
317306
return chunk, nil
318307
}
319308

320-
func (p *Parser) fetchJSONURL(ctx context.Context, form url.Values, referer string) (string, error) {
309+
func (p *Parser) fetchJSONURL(ctx context.Context, form url.Values) (string, error) {
321310
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.apiURL, strings.NewReader(form.Encode()))
322311
if err != nil {
323312
return "", err
324313
}
325314
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
326-
// Look like the page's own AJAX call so osvita's anti-bot lets us through.
327-
setBrowserHeaders(req)
328-
req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
329-
req.Header.Set("X-Requested-With", "XMLHttpRequest")
330-
if referer != "" {
331-
req.Header.Set("Referer", referer)
332-
req.Header.Set("Origin", "https://"+programHost)
333-
}
334315

335316
resp, err := p.client.Do(req)
336317
if err != nil {
@@ -373,18 +354,11 @@ func firstNonEmpty(a, b string) string {
373354
return b
374355
}
375356

376-
// setBrowserHeaders makes a request look like it came from a real browser.
377-
func setBrowserHeaders(req *http.Request) {
378-
req.Header.Set("User-Agent", browserUA)
379-
req.Header.Set("Accept-Language", "uk-UA,uk;q=0.9,en;q=0.8")
380-
}
381-
382357
func (p *Parser) fetchPayload(ctx context.Context, jsonURL string) (*rawChunk, error) {
383358
req, err := http.NewRequestWithContext(ctx, http.MethodGet, jsonURL, nil)
384359
if err != nil {
385360
return nil, err
386361
}
387-
setBrowserHeaders(req)
388362
resp, err := p.client.Do(req)
389363
if err != nil {
390364
return nil, err
@@ -451,8 +425,6 @@ func (p *Parser) fetchStatic(ctx context.Context, programURL string) (*abit.Prog
451425
if err != nil {
452426
return nil, err
453427
}
454-
setBrowserHeaders(req)
455-
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
456428
resp, err := p.client.Do(req)
457429
if err != nil {
458430
return nil, err

internal/parser/osvita/pagination_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func TestFanOut_RetriesFlakyEmptyURL(t *testing.T) {
5656

5757
p := New(WithAPIURL(srv.URL), WithWorkers(2), WithPageSize(pg), WithMaxRetries(3))
5858
prog := &abit.Program{}
59-
if err := p.fanOut(context.Background(), prog, srv.URL, "sid", "uid", "2025"); err != nil {
59+
if err := p.fanOut(context.Background(), prog, "sid", "uid", "2025"); err != nil {
6060
t.Fatalf("fanOut: %v", err)
6161
}
6262
if len(prog.Requests) != total {

0 commit comments

Comments
 (0)