@@ -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-
382357func (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
0 commit comments