Lavorare con file di grandi dimensioni
Che cosa cambiare quando i PDF diventano grandi: streaming, timeout, nuovi tentativi, limiti di dimensione e quando conviene dividere prima.
I PDF di grandi dimensioni danno luogo alla stessa richiesta di quelli piccoli (un file in input, un file in output), ma l’approccio ingenuo di leggere l’intero documento in memoria e affidarsi a un timeout predefinito crolla man mano che i file crescono. Eseguire in streaming l’upload e il download, lasciare alla richiesta il tempo di concludersi e ritentare in caso di errori transitori. Poiché l’API è stateless, una richiesta fallita non lascia nulla dietro di sé, quindi i nuovi tentativi sono sicuri.
Usare lo streaming, non il buffer
Leggere l’input dal disco a blocchi mentre lo si invia e scrivere la risposta sul
disco a blocchi man mano che arriva. Non tenere mai in memoria l’intero
documento. Ogni esempio qui sotto usa lo streaming in entrambe le direzioni e
appone una filigrana su big.pdf, che sta al posto di qualsiasi azione a output
singolo.
# curl streams the upload from disk and the response to disk by default.
curl https://api.pdfblocks.com/v1/add_text_watermark \
-H 'X-API-Key: your_api_key' \
-F file=@big.pdf \
-F line_1='CONFIDENTIAL' \
--max-time 600 \
--retry 3 --retry-delay 2 --retry-all-errors \
-o watermarked.pdf# pip install requests requests-toolbelt
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
with open('big.pdf', 'rb') as file:
form = MultipartEncoder(fields={
'file': ('big.pdf', file, 'application/pdf'), # streamed from disk
'line_1': 'CONFIDENTIAL',
})
with requests.post(
'https://api.pdfblocks.com/v1/add_text_watermark',
headers={'X-API-Key': 'your_api_key', 'Content-Type': form.content_type},
data=form,
stream=True,
timeout=(10, 600), # 10s to connect, 600s to read
) as response:
response.raise_for_status()
with open('watermarked.pdf', 'wb') as output:
for chunk in response.iter_content(chunk_size=65536):
output.write(chunk)// npm install form-data
import { createReadStream, createWriteStream } from 'node:fs';
import { request } from 'node:https';
import FormData from 'form-data';
const form = new FormData();
form.append('file', createReadStream('big.pdf')); // streamed from disk
form.append('line_1', 'CONFIDENTIAL');
const req = request(
'https://api.pdfblocks.com/v1/add_text_watermark',
{
method: 'POST',
headers: { 'X-API-Key': 'your_api_key', ...form.getHeaders() },
timeout: 600000,
},
(response) => {
if (response.statusCode !== 200) {
throw new Error(`Request failed: ${response.statusCode}`);
}
response.pipe(createWriteStream('watermarked.pdf')); // streamed to disk
},
);
form.pipe(req); // stream the multipart bodypackage main
import (
"io"
"mime/multipart"
"net/http"
"os"
"time"
)
func main() {
// Generate the multipart body on the fly with io.Pipe: the file is never
// held in memory in full.
pr, pw := io.Pipe()
form := multipart.NewWriter(pw)
go func() {
defer pw.Close()
defer form.Close()
file, _ := os.Open("big.pdf")
defer file.Close()
part, _ := form.CreateFormFile("file", "big.pdf")
io.Copy(part, file) // streamed, not buffered
form.WriteField("line_1", "CONFIDENTIAL")
}()
req, _ := http.NewRequest("POST",
"https://api.pdfblocks.com/v1/add_text_watermark", pr)
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("X-API-Key", "your_api_key")
client := &http.Client{Timeout: 10 * time.Minute}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := os.Create("watermarked.pdf")
defer out.Close()
io.Copy(out, res.Body) // streamed to disk
}using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");
using var fileStream = File.OpenRead("big.pdf");
using var form = new MultipartFormDataContent
{
{ new StreamContent(fileStream), "file", "big.pdf" }, // streamed from disk
{ new StringContent("CONFIDENTIAL"), "line_1" },
};
// ResponseHeadersRead returns as soon as the headers arrive, so the body isn't
// buffered before you read it.
using var response = await client.PostAsync(
"https://api.pdfblocks.com/v1/add_text_watermark",
form,
HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
await using var output = File.Create("watermarked.pdf");
await response.Content.CopyToAsync(output); // streamed to diskImpostare timeout generosi
Un documento di grandi dimensioni richiede più tempo per l’upload e per l’elaborazione, quindi un timeout predefinito del client, spesso di 30 secondi o meno, interrompe una richiesta che sarebbe altrimenti andata a buon fine. Conviene dividere il budget:
- Un breve timeout di connessione (pochi secondi) fallisce subito quando l’host è irraggiungibile.
- Un lungo timeout di lettura dà all’elaborazione il tempo di concludersi. Gli esempi qui sopra usano dieci minuti: dimensionarlo sul documento più grande che ci si aspetta.
Non impostare un timeout illimitato: serve comunque che una connessione bloccata prima o poi si arrenda, in modo che un nuovo tentativo possa subentrare.
Ritentare in caso di errori transitori
I reset di rete, i timeout di connessione e (una volta in vigore) le risposte
429 o 5xx sono transitori: la stessa richiesta può benissimo riuscire un
istante dopo. Ritentare con un backoff esponenziale e un po’ di jitter. Qui
ritentare è sicuro proprio perché l’API è stateless: ogni chiamata è una
funzione pura dei suoi input, senza scritture parziali da annullare.
import random
import time
import requests
def send_with_retries(build_request, attempts=4):
for attempt in range(attempts):
try:
response = build_request()
# Retry only on server-side / throttling responses.
if response.status_code < 500 and response.status_code != 429:
return response
except requests.RequestException:
if attempt == attempts - 1:
raise
# Back off: 1s, 2s, 4s, … plus jitter.
time.sleep(2 ** attempt + random.uniform(0, 1))
return responseNon ritentare un 400: un errore di validazione non si corregge da solo e il
corpo application/problem+json indica quale campo correggere. Vedere
Errori per il catalogo. Dalla riga di comando cURL ha già
questo meccanismo integrato: --retry 3 --retry-delay 2 --retry-all-errors.
Limiti di dimensione e rate limit
I limiti di dimensione delle richieste, i rate limit e i relativi codici di
stato 413, 429 e affini sono una parte riservata del contratto dell’API e
non sono ancora applicati. Non è pubblicata alcuna soglia fissa.
Progettare per il contratto anziché per un numero preciso.
Per restare robusti quando i limiti entreranno in vigore, mantenere le singole
richieste a una dimensione ragionevole, trattare 413 e 429 come casi da
ritentare con backoff (oppure, per 413, come il segnale che occorre dividere
l’input) e rispettare l’intestazione Retry-After quando è presente. Vedere
Rate limit e utilizzo per sapere come viene
misurato l’utilizzo.
Dividere prima i file molto grandi
Se un singolo documento è troppo grande o troppo lento da elaborare in un’unica chiamata, dividerlo in parti più piccole, elaborare ciascuna in modo indipendente e (se serve riavere un solo file) unire i risultati. Questo limita la memoria e il tempo richiesti da ogni singola richiesta e permette di elaborare le parti in parallelo.
Usare Dividere per dimensione del file o Dividere per numero di pagine per produrre blocchi gestibili. Vedere Dividere un PDF per sapere come spacchettare l’output.
Eseguire la propria azione su ogni parte, in parallelo se si vuole, dato che le chiamate sono indipendenti.
Ricombinare le parti elaborate con Unire documenti PDF.
Vedi anche
Come viene misurato l’utilizzo e il contratto riservato sui limiti.
I codici di stato, la forma problem+json e che cosa ritentare.
Dividere prima un documento enorme in parti più piccole.
Eseguire in streaming una pipeline in più passaggi senza file temporanei.