সরাসরি পর্বে যান
পর্ব ৪৭ · ৫৫-এর মধ্যে · মডিউল ৭

HTTP, API আর web scraping — দায়িত্বের সাথে ওয়েব থেকে ডেটা আনা

HTTP, APIs and responsible web scraping in Python

Taught in Bangla, this lesson shows how to fetch data from the web with Python: requests and responses, URLs, methods, headers and status codes, JSON APIs with pagination and rate limits, requests with timeouts, raise_for_status and retry with backoff, httpx and urllib, keeping API keys secret, and responsible scraping with robots.txt and BeautifulSoup.

এই পর্বে যা শিখবেন

  • একটি HTTP অনুরোধ শুরু থেকে শেষ পর্যন্ত কীভাবে যায় — DNS, সংযোগ, request, response
  • URL-এর অংশ, GET/POST/PUT/DELETE, গুরুত্বপূর্ণ header, আর প্রতিটি status code পেলে কী করবেন
  • JSON API-র ডকুমেন্টেশন পড়া, query parameter, pagination, rate limit আর API key গোপন রাখা
  • requests আর httpx দিয়ে অনুরোধ — timeout, session, retry ও backoff, আর standard library-র urllib
  • দায়িত্বশীল web scraping — robots.txt, ভদ্র গতি, আইনি-নৈতিক সীমা, আর BeautifulSoup দিয়ে HTML পার্স করে CSV/JSON-এ রাখা

১ · ওয়েব মানে প্রোগ্রামদের কথোপকথন — request আর response

ব্রাউজারে abcltech.com লিখে Enter চাপলে কী হয়? মনে হয় পাতাটি “খুলে গেল”. আসলে আপনার ব্রাউজার নামের একটি প্রোগ্রাম পৃথিবীর অন্য কোথাও রাখা একটি কম্পিউটারের প্রোগ্রামকে একটি লিখিত অনুরোধ পাঠিয়েছে: “অমুক পাতাটি দিন”. ওই প্রোগ্রামকে বলে server. server একটি লিখিত উত্তর ফেরত পাঠিয়েছে: “এই নিন, পাতাটি এরকম”. পুরো ওয়েব এই এক ধাঁচের ওপর দাঁড়িয়ে — একজন চায়, আরেকজন দেয়। যে চায় তাকে বলে client.

এই অনুরোধ আর উত্তর কোন ভাষায় লেখা হবে, তার নিয়মের নাম HTTP (HyperText Transfer Protocol). গোপনীয়তার জন্য আজকাল প্রায় সবসময় এর এনক্রিপ্ট করা রূপ HTTPS ব্যবহার হয়। মজার কথা হলো, HTTP জানে না অনুরোধটি কে পাঠাল — Chrome, একটি মোবাইল অ্যাপ, নাকি আপনার লেখা ১০ লাইনের Python স্ক্রিপ্ট। নিয়ম মানলেই উত্তর আসবে।

কমলাপুর রেলস্টেশনের টিকিট কাউন্টারের কথা ভাবুন। আপনি একটি ফরম পূরণ করে দেন: কোন ট্রেন, কোন তারিখ, কয়টি আসন (এটা request). কাউন্টারের কর্মী ফরমটি দেখে উত্তর দেন — হয় টিকিট (সফল), নয়তো “এই ট্রেন নেই” বা “আসন শেষ, পরে আসুন”. কর্মী আপনাকে চেনেন না; ফরম ঠিকমতো পূরণ হলেই কাজ হয়। আর লাইনে দাঁড়িয়ে কেউ যদি প্রতি সেকেন্ডে নতুন ফরম জমা দিতে থাকে, কর্মী তাকে সরিয়ে দেবেন — এটাই পরে দেখবেন rate limit হিসেবে।

Python প্রোগ্রামার হিসেবে ওয়েবের সাথে তিনভাবে কথা বলবেন:

🌐 ব্রাউজারের মতো পাতা চাওয়া

server ফেরত দেয় HTML — মানুষের চোখের জন্য সাজানো পাতা। শিরোনাম, ছবি, বোতাম, বিজ্ঞাপন সব মেশানো।

মানুষের জন্য

📦 API থেকে ডেটা চাওয়া

মুদ্রার হার, আবহাওয়া, ট্রেনের সময়সূচি — server ফেরত দেয় পরিষ্কার JSON, প্রোগ্রামের জন্যই বানানো। নিয়ম লেখা থাকে ডকুমেন্টেশনে।

প্রোগ্রামের জন্য

🧹 web scraping

API না থাকলে মানুষের জন্য বানানো HTML পাতা থেকে প্রোগ্রাম দিয়ে তথ্য বের করা। কাজ করে, কিন্তু ভঙ্গুর — আর অন্যের সার্ভারে চাপ দেয়, তাই সাবধানে আর ভদ্রভাবে।

শেষ উপায়

একটি অনুরোধ পাঠানো থেকে উত্তর পাওয়া পর্যন্ত আসলে কয়েকটি ধাপ পেরোতে হয়। ধাপে ধাপে দেখুন, যখন আপনার স্ক্রিপ্ট ঢাকার আবহাওয়া জানতে একটি API-কে ডাকে:

  1. URLভাঙাhost, path, query
  2. DNSনাম → IPঠিকানা খোঁজা
  3. সংযোগTCP + TLSনিরাপদ লাইন
  4. requestGET + headerকী চাই, আমি কে
  5. serverকাজযাচাই, ডেটা খোঁজা
  6. response200 + JSONstatus, header, body
URL-কে অংশে ভাগ করাrequests নিজেই করে
https://api.example.com/v1/forecast?city=dhaka
https             → scheme: কোন নিয়মে (এনক্রিপ্ট করা HTTP)
api.example.com   → host: কোন কম্পিউটার
/v1/forecast      → path: সেখানে কোন জিনিস
city=dhaka        → query: বাড়তি শর্ত
DNS lookupইন্টারনেটের ফোনবুক
প্রশ্ন:  api.example.com-এর IP কত?
উত্তর:  203.0.113.25   # উদাহরণ ঠিকানা
# কম্পিউটার নাম চেনে না, চেনে সংখ্যার ঠিকানা
# উত্তর কিছুক্ষণ cache থাকে, তাই পরেরবার দ্রুত
সংযোগ স্থাপনপোর্ট 443
TCP:  “শুনছেন?” → “শুনছি” → “শুরু করি”   # নির্ভরযোগ্য লাইন
TLS:  server সার্টিফিকেট দেখায় → আসল server কি না যাচাই
      দুই পক্ষ একটি গোপন চাবিতে একমত   # এখন থেকে সব এনক্রিপ্টেড
request — আসলে কয়েক লাইন লেখাrequest line + header
GET /v1/forecast?city=dhaka HTTP/1.1
Host: api.example.com
User-Agent: bd-weather-app/1.0
Accept: application/json
Authorization: Bearer <গোপন চাবি>
(ফাঁকা লাইন — GET-এ সাধারণত body নেই)
server-এর ভেতরেপর্ব ৪৮-এ নিজে বানাবেন
১. চাবি বৈধ?            → না হলে 401
২. এই মিনিটে কত অনুরোধ? → বেশি হলে 429
৩. path চেনা?           → না হলে 404
৪. database থেকে ঢাকার আবহাওয়া → JSON বানানো
# কোনো ধাপে কোড ভেঙে পড়লে → 500
responsestatus line + header + body
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Remaining: 59

{"city": "Dhaka", "temp_c": 31.5, "humidity": 74}
  1. URL ভাঙা। আপনার কোডে শুধু একটি লম্বা লেখা। লাইব্রেরি প্রথমে এটিকে অংশে ভাগ করে: কোন নিয়মে (https), কোন কম্পিউটারে (host), সেখানে কোন জিনিস (path), আর কোন শর্তে (query).
  2. DNS — নাম থেকে ঠিকানা। নেটওয়ার্ক চলে সংখ্যার IP address দিয়ে। DNS হলো ইন্টারনেটের ফোনবুক: “api.example.com” দিলে একটি IP ফেরত দেয়। ঠিকানা না মিললে Python-এ আসে connection error — ইন্টারনেট বন্ধ থাকলেও এখানেই আটকায়।
  3. সংযোগ — TCP আর TLS. TCP একটি নির্ভরযোগ্য লাইন খোলে, যাতে কোনো টুকরো হারালে আবার পাঠানো হয়। তারপর TLS: server তার সার্টিফিকেট দেখায়, আপনার কম্পিউটার যাচাই করে এটি আসল server কি না, আর দুই পক্ষ গোপন চাবিতে একমত হয়। এর পর রাস্তায় কেউ ডেটা পড়তে বা বদলাতে পারে না।
  4. request পাঠানো। HTTP/1.1-এ request আসলে কয়েক লাইন সাধারণ লেখা। প্রথম লাইনে method (GET), path আর version. তারপর header: নাম-মানের জোড়া, যা বলে আপনি কে, কী ধরনের উত্তর চান, আর আপনার চাবি কী। (HTTP/2 আর HTTP/3 একই তথ্য binary আকারে পাঠায়, কিন্তু অর্থ একই।)
  5. server কাজ করে। server যাচাই করে চাবি বৈধ কি না, আপনি সীমার বেশি অনুরোধ করছেন কি না, path চেনা কি না — তারপর ডেটা খুঁজে উত্তর বানায়। প্রতিটি সিদ্ধান্তের ফল একটি সংখ্যায় প্রকাশ পায়: status code. পর্ব ৪৮-এ FastAPI দিয়ে আপনি নিজেই এই পাশটা লিখবেন।
  6. response ফেরে। প্রথম লাইনে status (200 OK), তারপর header (যেমন Content-Type: application/json — body-টা JSON), তারপর একটি ফাঁকা লাইন, তারপর আসল ডেটা বা body. আপনার Python কোড এখন এই JSON-কে dict বানিয়ে ব্যবহার করবে — পর্ব ২৬-এর json.loads-এর কাজ।
মূল ধারণা

HTTP একটি প্রশ্ন-উত্তরের চুক্তি: প্রতিটি request একটি সম্পূর্ণ প্রশ্ন, প্রতিটি response একটি সম্পূর্ণ উত্তর, আর মাঝখানে server আপনাকে “মনে রাখে” না (একে বলে stateless). তাই প্রতিটি অনুরোধে দরকারি সব তথ্য — কে আপনি, কোন পাতা, কোন চাবি — নিজেই পাঠাতে হয়।

২ · URL-এর গঠন আর HTTP method — কোথায়, আর কী করতে চাই

একটি URL হলো ইন্টারনেটে একটি জিনিসের পূর্ণ ঠিকানা — চিঠির খামের ঠিকানার মতো, বড় থেকে ছোট দিকে। একটি বাস্তব উদাহরণ ভাঙা যাক:

https://api.example.com:443/v1/books/search?q=python&page=2#results
অংশউদাহরণেকাজ
schemehttpsকোন নিয়মে কথা হবে; https মানে এনক্রিপ্ট করা HTTP
hostapi.example.comকোন কম্পিউটার; DNS দিয়ে IP-তে বদলায়
port443সেই কম্পিউটারের কোন “দরজা”; https-এর ডিফল্ট 443, http-এর 80 — সাধারণত লেখা হয় না
path/v1/books/searchserver-এর ভেতরে কোন জিনিস; API-তে প্রতিটি path-কে বলে একটি endpoint
queryq=python&page=2?-এর পরে নাম=মান জোড়া, & দিয়ে আলাদা — খোঁজার শর্ত, পাতা নম্বর
fragment#resultsপাতার ভেতরের একটি জায়গা; ব্রাউজারেই থাকে, server-এ যায় না

query অংশকে বলে query string. এখানে একটি ফাঁদ আছে: URL-এ শুধু কিছু নির্দিষ্ট ASCII অক্ষর সরাসরি বসানো যায়। ফাঁকা জায়গা, &, ? বা বাংলা অক্ষর বসাতে হলে সেগুলো % আর hex সংখ্যায় লিখতে হয় — percent-encoding. “ঢাকা” হয়ে যায় %E0%A6%A2.... তাই কখনো f-string দিয়ে হাতে query জোড়া লাগাবেন না: কেউ খোঁজার ঘরে C&C++ লিখলেই & চিহ্নটি নতুন parameter শুরু করে দেবে। standard library-র urllib.parse.urlencode বা requests-এর params= এই কাজ ঠিকভাবে করে।

method — একই ঠিকানায় আলাদা কাজ

request-এর প্রথম শব্দটি HTTP method: আপনি জিনিসটির সাথে কী করতে চান। একই /books/42 ঠিকানায় GET মানে “বইটি দেখাও”, DELETE মানে “বইটি মুছে দাও”.

methodকাজbody থাকে?আবার পাঠানো নিরাপদ?
GETপড়া — ডেটা আনানাহ্যাঁ — কিছু বদলায় না
POSTনতুন কিছু তৈরি, বা কোনো কাজ চালানো (অর্ডার, মেসেজ)হ্যাঁনা — দুবার পাঠালে দুটি অর্ডার হতে পারে
PUTপুরোটা বদলে নতুন রূপ বসানোহ্যাঁহ্যাঁ — একই রূপ বারবার বসালে ফল একই
PATCHকিছু ঘর বদলানোহ্যাঁনির্ভর করে
DELETEমুছে ফেলাসাধারণত নাহ্যাঁ — মোছা জিনিস আবার মুছলে অবস্থা একই

শেষ কলামটি খুব গুরুত্বপূর্ণ, পরে retry-এর সময় লাগবে। যে কাজ একবার আর দশবার করলে ফল একই, তাকে বলে idempotent. GET, PUT, DELETE নিয়ম অনুযায়ী idempotent; POST নয়। bKash-এ “Send Money” চেপে নেটওয়ার্ক আটকে গেলে অ্যাপ নিজে থেকে আবার পাঠায় না — কারণ প্রথম অনুরোধটি হয়তো পৌঁছে গেছে, আবার পাঠালে টাকা দুবার কাটবে। আবহাওয়া পড়ার GET অনুরোধ নিশ্চিন্তে আবার পাঠানো যায়।

৩ · header আর status code — “আমি কে” আর “কী হলো”

header হলো request বা response-এর সাথে পাঠানো ছোট ছোট নাম-মানের জোড়া — খামের ওপরের লেবেলের মতো। শত শত header আছে, কিন্তু শুরুতে কয়েকটি চিনলেই যথেষ্ট:

headerকে পাঠায়মানে
User-Agentclient“আমি কোন প্রোগ্রাম”. requests ডিফল্টে পাঠায় python-requests/2.x; ভদ্র বট এখানে নিজের নাম আর যোগাযোগের ঠিকানা দেয়
Acceptclientকোন ধরনের উত্তর চাই, যেমন application/json
Content-Typeদুই পক্ষইbody-টা কোন ফরম্যাটে: application/json, text/html; charset=utf-8
Authorizationclientপরিচয়ের প্রমাণ, প্রায়ই Bearer <token>. এটি পাসওয়ার্ডের মতো গোপন
Retry-Afterserverকত সেকেন্ড পরে আবার চেষ্টা করবেন (429 বা 503-এর সাথে)
Locationserverredirect হলে নতুন ঠিকানা

status code — তিন অঙ্কের একটি সংখ্যায় পুরো গল্প

প্রতিটি response-এর শুরুতে একটি তিন অঙ্কের status code থাকে। প্রথম অঙ্কটিই আসল শ্রেণি বলে দেয়: 2 মানে সফল, 3 মানে অন্য কোথাও যান, 4 মানে আপনার (client-এর) ভুল, 5 মানে server-এর সমস্যা। এই পার্থক্য সিদ্ধান্তের জন্য জরুরি: 4xx পেলে হুবহু একই অনুরোধ আবার পাঠিয়ে লাভ নেই, অনুরোধ ঠিক করতে হবে; 5xx পেলে একটু পরে আবার চেষ্টা করা যুক্তিসঙ্গত।

codeনামমানেআপনার কোড কী করবে
200OKসফল, body-তে উত্তরbody পড়ুন
201CreatedPOST দিয়ে নতুন জিনিস তৈরি হয়েছেনতুন জিনিসের id বা ঠিকানা নিন
301 / 302Moved Permanently / Foundজিনিসটি অন্য ঠিকানায় (স্থায়ী / সাময়িক)requests নিজেই Location-এ যায়; 301 হলে কোডের URL হালনাগাদ করুন
400Bad Requestঅনুরোধের গঠন ভুল (ভুল parameter, ভাঙা JSON)retry নয় — অনুরোধ ঠিক করুন
401Unauthorizedপরিচয় নেই বা চাবি ভুল/মেয়াদোত্তীর্ণচাবি দেখুন; retry নয়
403Forbiddenপরিচয় জানা, কিন্তু অনুমতি নেইথামুন; জোর করে ঢোকার চেষ্টা নয়
404Not Foundএই ঠিকানায় কিছু নেইব্যবহারকারীকে জানান; retry নয়
429Too Many Requestsসীমার বেশি অনুরোধRetry-After যতক্ষণ বলে অপেক্ষা, তারপর ধীরে আবার
500Internal Server Errorserver-এর কোড ভেঙেছেবিরতি দিয়ে কয়েকবার retry, তারপর হাল ছাড়ুন
502 / 503 / 504Bad Gateway / Unavailable / Timeoutserver ব্যস্ত বা সাময়িক বন্ধ500-এর মতোই — ধীরে retry

এই টেবিলটিকে একটি সিদ্ধান্তের পথ হিসেবে ধাপে ধাপে দেখুন — প্রতিটি response এলে আপনার প্রোগ্রাম কোন রাস্তায় যাবে:

  1. responsestatus?প্রথম অঙ্ক দেখুন
  2. 2xx.json()পড়ে যাচাই
  3. 404 / 4xxজানানretry নয়
  4. 429Retry-Afterঅপেক্ষা, আবার
  5. 5xxbackoffপরে আবার, সীমিতবার
r = requests.get(url, timeout=10)আগে status, পরে body
r.status_code   → ?
r.ok            → True যদি status < 400
# সবচেয়ে প্রচলিত ভুল: status না দেখেই r.json()
# 500-এর body প্রায়ই HTML error পাতা → JSONDecodeError
200 OKসফল — কিন্তু ডেটা যাচাই এখনও বাকি
data = r.json()
rate = data.get("rates", {}).get("BDT")
if rate is None:
    raise ValueError("উত্তরে BDT নেই")   # API বদলাতে পারে
404 Not Foundআপনার দিকের সমস্যা
GET /v1/city/dhakaa   → 404
# বানান ভুল — একশবার পাঠালেও একই উত্তর
print("শহরটি পাওয়া যায়নি, বানান দেখুন")
# 400, 401, 403-ও একই শ্রেণি: অনুরোধ বা চাবি ঠিক করুন
429 Too Many Requestsserver বলছে “ধীরে”
HTTP/1.1 429 Too Many Requests
Retry-After: 30
# → ৩০ সেকেন্ড ঘুমান, তারপর আবার
# header না থাকলে নিজে বিরতি দিন: ১, ২, ৪, ৮ সেকেন্ড…
500 / 502 / 503 / 504server-এর দিকের সমস্যা
চেষ্টা ১ → 503   ঘুম ১ সেকেন্ড
চেষ্টা ২ → 503   ঘুম ২ সেকেন্ড
চেষ্টা ৩ → 503   ঘুম ৪ সেকেন্ড
চেষ্টা ৪ → হাল ছাড়ুন, log লিখুন, ব্যবহারকারীকে জানান
  1. আগে status, পরে body. response এলেই সরাসরি r.json() নয়। server ভেঙে পড়লে body-তে JSON-এর বদলে একটি HTML error পাতা আসে, আর .json() একটি বিভ্রান্তিকর JSONDecodeError দেয় — আসল সমস্যা (500) চাপা পড়ে যায়। তাই প্রথম কাজ: status দেখা।
  2. 2xx — সফল, কিন্তু অন্ধ বিশ্বাস নয়। JSON পড়ুন, তারপর যাচাই করুন যে দরকারি চাবিগুলো আছে। API বদলায়, কোনো ঘর মাঝে মাঝে null আসে। পর্ব ১৬-এর .get() আর পর্ব ২৬-এর শিক্ষা এখানে কাজে লাগে।
  3. 4xx — নিজের অনুরোধ ঠিক করুন। 404 মানে ঠিকানায় কিছু নেই; একই অনুরোধ আবার পাঠালে একই উত্তর আসবে। ব্যবহারকারীকে স্পষ্ট বার্তা দিন। 401 মানে চাবি ভুল বা মেয়াদ শেষ; 403 মানে অনুমতি নেই — দুটিতেই retry অর্থহীন, আর 403-কে “ফাঁকি দিয়ে” পেরোনোর চেষ্টা করবেন না।
  4. 429 — একমাত্র 4xx যেখানে অপেক্ষা করে আবার চেষ্টা করা ঠিক। server স্পষ্ট বলছে আপনি বেশি দ্রুত। সাথে প্রায়ই Retry-After header থাকে — সেই কয়েক সেকেন্ড মেনে চলুন। না মানলে অনেক API সাময়িকভাবে আপনার চাবি বা IP আটকে দেয়।
  5. 5xx — সাময়িক সমস্যা, ধৈর্য ধরে কিন্তু সীমিতবার। server ব্যস্ত বা ভেঙেছে। বিরতি দিয়ে আবার চেষ্টা যুক্তিসঙ্গত, তবে প্রতিবার বিরতি দ্বিগুণ করুন (exponential backoff) আর একটা সর্বোচ্চ সংখ্যা রাখুন। হাজার client একসাথে না থেমে retry করলে অসুস্থ server আর উঠে দাঁড়াতে পারে না। আর মনে রাখুন: শুধু idempotent অনুরোধই অন্ধভাবে retry করা যায়।

এবার এই ধারণাগুলো standard library দিয়েই হাতে-কলমে দেখুন — কোনো নেটওয়ার্ক ছাড়া। কোডটি query-সহ একটি সার্চ URL বানায়, সেটিকে আবার অংশে ভাগ করে, আর পর্ব ১১-এর match দিয়ে status code থেকে করণীয় ঠিক করে:

from urllib.parse import urlencode, urlsplit, parse_qs
from http import HTTPStatus
base = "https://api.example.com/v1/search"
url = f"{base}?{urlencode({'q': 'ঢাকা', 'page': 2, 'lang': 'bn'})}"
print(url)
parts = urlsplit(url)
print(parts.scheme, parts.netloc, parts.path, parse_qs(parts.query))

def next_step(code: int) -> str:
    match code:
        case 200 | 201: return "ডেটা পড়ুন"
        case 301 | 302: return "নতুন ঠিকানায় যান"
        case 401 | 403: return "চাবি বা অনুমতি দেখুন"
        case 404: return "ঠিকানা ভুল — জানান"
        case 429: return "অপেক্ষা করে আবার চেষ্টা"
        case c if 500 <= c < 600: return "পরে আবার চেষ্টা"
        case _: return "অনুরোধ ঠিক করুন"

for code in (200, 404, 429, 503):
    print(code, HTTPStatus(code).phrase, "→", next_step(code))

প্রথম লাইনে পুরো URL: ...search?q=%E0%A6%A2%E0%A6%BE%E0%A6%95%E0%A6%BE&page=2&lang=bn — “ঢাকা” percent-encoding হয়ে গেছে, আর int 2 নিজেই লেখায় বদলেছে। দ্বিতীয় লাইনে urlsplit URL-কে অংশে ভেঙেছে: https api.example.com /v1/search, আর parse_qs query-কে আবার পড়ার মতো বানিয়েছে: {'q': ['ঢাকা'], 'page': ['2'], 'lang': ['bn']}. লক্ষ করুন, প্রতিটি মান একটি list (একই নাম একাধিকবার আসতে পারে বলে), আর '2' এখন str — URL-এ সবই লেখা। তারপর চারটি লাইন: 200 OK → ডেটা পড়ুন, 404 Not Found → ঠিকানা ভুল — জানান, 429 Too Many Requests → অপেক্ষা করে আবার চেষ্টা, 503 Service Unavailable → পরে আবার চেষ্টা. http.HTTPStatus হলো standard library-র একটি enum, যাতে প্রতিটি code-এর প্রমিত নাম আছে; শেষের case c if ... একটি guard দিয়ে পুরো 5xx শ্রেণি একসাথে ধরে।

৪ · JSON API — ডকুমেন্টেশন, pagination, rate limit আর গোপন চাবি

একটি ওয়েব API হলো এমন একগুচ্ছ endpoint, যা মানুষের জন্য নয়, প্রোগ্রামের জন্য বানানো। প্রতিটি ভালো API-র সাথে থাকে ডকুমেন্টেশন — আর কোড লেখার আগে সেটি পড়াই আসল কাজ। AI-কে জিজ্ঞেস করলে সে প্রায়ই পুরোনো বা বানানো endpoint দেয়; সত্যের উৎস ডকুমেন্টেশন। পড়ার সময় এই প্রশ্নগুলোর উত্তর খুঁজুন:

  • base URL আর endpoint: যেমন /v1/forecast. path-এ v1 মানে version — API বদলালে v2 আসে, পুরোনোটা কিছুদিন চলে।
  • parameter: কোনগুলো বাধ্যতামূলক, কোনগুলো ঐচ্ছিক, মান কোন ধাঁচে (তারিখ 2026-10-18? অক্ষাংশ দশমিকে?)।
  • উত্তরের নমুনা: কোন চাবির ভেতরে কী থাকে, কোনটি null হতে পারে, ভুল হলে উত্তর কেমন দেখায়।
  • চাবি আর সীমা: API key লাগে কি না, কোথায় পাঠাতে হয় (header নাকি query), আর মিনিটে বা দিনে কয়টি অনুরোধ চলবে।
  • শর্তাবলি: বিনা মূল্যে কি না, বাণিজ্যিক কাজে চলবে কি না, উৎসের নাম (attribution) দেখাতে হবে কি না।

pagination — হাজার রেকর্ড একবারে আসে না

একটি বইয়ের দোকানের API-তে ৫০,০০০ বই থাকলে server একবারে সব পাঠায় না — এতে server-এর মেমরি, নেটওয়ার্ক আর আপনার ধৈর্য তিনটিই শেষ হবে। তাই উত্তর আসে পাতায় পাতায়, প্রতি পাতায় ধরুন ২০টি করে। একে বলে pagination. API সাধারণত তিন ধাঁচের একটি ব্যবহার করে: পাতা নম্বর (?page=3), কতটি বাদ দিয়ে শুরু (?offset=40&limit=20), বা একটি cursor — উত্তরের ভেতরে থাকা একটি চিহ্ন বা next লিংক, যা পরের পাতার ঠিকানা বলে। আপনার কোডের কাজ: একটি লুপে পাতা আনা, যতক্ষণ “পরের পাতা” আছে। ধাপে ধাপে দেখুন:

1params, items = {"page": 1}, []2while True:3    data = s.get(URL, params=params, timeout=10).json()4    items.extend(data["results"])5    if data["next"] is None: break6    params["page"] += 1; time.sleep(1)7print(len(items))
API-তে মোট ৪৫টি বই, প্রতি পাতায় ২০টি
page
এই পাতায়
len(items)
data["next"]
  1. প্রথম পাতা। page=1 দিয়ে অনুরোধ, উত্তরে ২০টি বই আর একটি next লিংক। বইগুলো items-এ যোগ হলো, পরের পাতা আছে, তাই page বাড়ল — আর বাড়ানোর পর এক সেকেন্ড বিরতি, যাতে server-এর ওপর চাপ না পড়ে।
  2. দ্বিতীয় পাতা। একই কাজ, এখন মোট ৪০টি। লক্ষ করুন, লুপটি জানে না মোট কত পাতা — সে শুধু উত্তরের next দেখে সিদ্ধান্ত নেয়। এটাই নিরাপদ পথ; “৫০ পাতা আছে” ধরে নিয়ে range(50) লিখলে ডেটা বাড়লে বা কমলে কোড ভুল হবে।
  3. শেষ পাতা। মাত্র ৫টি বই, আর next এখন None (JSON-এর null). বইগুলো যোগ করে লুপ break করে।
  4. ফল। ৪৫টি বই, তিনটি অনুরোধে। বাস্তব কোডে আরও দুটি সুরক্ষা যোগ করুন: পাতার একটি সর্বোচ্চ সংখ্যা (API-র বাগে next কখনো None না হলে যেন লুপ চিরকাল না ঘোরে), আর প্রতিটি উত্তরের status যাচাই — পরের অংশে দেখবেন।

rate limit — কেন server “ধীরে” বলে

প্রায় প্রতিটি পাবলিক API একটি rate limit রাখে: যেমন মিনিটে ৬০টি বা দিনে ১,০০০টি অনুরোধ। কারণ সহজ — server-এর ক্ষমতা সীমিত, আর একজন অসাবধান ব্যবহারকারী যেন বাকি সবার সেবা আটকে না দেয়। অনেক API উত্তরের header-এ জানিয়ে দেয় কতটি বাকি আছে (যেমন X-RateLimit-Remaining — নামটি প্রচলিত হলেও সব API-তে এক নয়), আর সীমা পেরোলে 429 পাঠায়। ভদ্র client নিজেই গতি নিয়ন্ত্রণ করে: অনুরোধের মাঝে বিরতি, একই ডেটা বারবার না চেয়ে cache করা, আর বাকি সংখ্যা কমে এলে ধীর হওয়া।

API key — পাসওয়ার্ডের মতোই গোপন

অনেক API ব্যবহার করতে নিবন্ধন করে একটি API key নিতে হয় — একটি লম্বা এলোমেলো লেখা, যা দিয়ে server চেনে কে অনুরোধ করছে, কতটি করছে, আর কোনো পেইড প্ল্যান হলে কার বিল হবে। অর্থাৎ কেউ আপনার চাবি পেলে আপনার নামে অনুরোধ করতে পারবে, আপনার সীমা শেষ করতে পারবে, এমনকি আপনার কার্ডে খরচ তুলতে পারবে। সবচেয়ে প্রচলিত ভুল:

API_KEY = "sk_live_9f8a7b6c5d4e3f2a1b0c"      # ভুল! কোডের সাথে git-এ চলে যাবে
r = requests.get(URL, params={"key": API_KEY}, timeout=10)

কোডটি GitHub-এ তুললেই চাবি সারা পৃথিবীর কাছে উন্মুক্ত। পাবলিক repository-তে ফাঁস হওয়া চাবি খুঁজে বেড়ানো স্বয়ংক্রিয় বট আছে, আর পরে commit মুছে দিলেও git-এর ইতিহাসে চাবি থেকে যায়। একবার ফাঁস হলে একমাত্র সমাধান: চাবিটি বাতিল করে নতুন নেওয়া। সঠিক পথ হলো চাবিকে কোডের বাইরে রাখা — environment variable-এ (পর্ব ২৭-এ os.environ দেখেছেন):

import os

API_KEY = os.environ.get("WEATHER_API_KEY")
if not API_KEY:
    raise SystemExit("WEATHER_API_KEY সেট করা নেই — README দেখুন")
headers = {"Authorization": f"Bearer {API_KEY}"}   # ডকুমেন্টেশন যেভাবে বলে

terminal-এ চাবি সেট করা হয় চালানোর আগে (Windows PowerShell-এ $env:WEATHER_API_KEY="...", Linux/Mac-এ export WEATHER_API_KEY=...). প্রজেক্টে অনেক চাবি থাকলে সেগুলো একটি .env ফাইলে রেখে python-dotenv প্যাকেজ দিয়ে পড়া প্রচলিত — কিন্তু .env-কে অবশ্যই .gitignore-এ যোগ করুন, আর অন্যদের জন্য চাবি ছাড়া একটি .env.example রাখুন। পর্ব ৫০-এ secret আর নিরাপত্তা নিয়ে আরও বিস্তারিত আলোচনা আছে।

চাবি কখনো ব্রাউজারের কোডে বা মোবাইল অ্যাপে নয়। JavaScript বা অ্যাপের ভেতরে রাখা চাবি যে কেউ বের করে নিতে পারে। চাবি থাকে শুধু server-এ; ব্রাউজার আপনার server-কে ডাকে, আপনার server চাবিসহ API-কে ডাকে। আর চাবি log-এ বা error বার্তায় ছাপবেন না — URL-এর query-তে চাবি থাকলে পুরো URL log করাও বিপজ্জনক।

৫ · requests — “HTTP for Humans”

Python-এর standard library-তে HTTP-র জন্য urllib.request আছে, কিন্তু সেটি ব্যবহার করা একটু কষ্টকর। তাই বাস্তবে সবচেয়ে প্রচলিত হলো PyPI-র requests লাইব্রেরি — Python-এর সবচেয়ে বেশি নামানো প্যাকেজগুলোর একটি। এটি বাইরের প্যাকেজ, তাই পর্ব ২৩-এর মতো একটি virtual environment-এ install করুন:

python -m pip install requests

এই পাতার “চালান” বোতাম requests, httpx বা BeautifulSoup চালাতে পারে না। পাতার runner-এ শুধু Python-এর standard library আছে, আর বাইরের ওয়েবসাইটে অনুরোধ পাঠানোর সুযোগও নেই। তাই এখন থেকে বাইরের লাইব্রেরির কোডগুলো সাধারণ কোড ব্লক হিসেবে দেখানো হয়েছে। নিজে চালাতে উপরের মতো install করে নিজের কম্পিউটারে চালান, অথবা Google Colab-এ (সেখানে requests আগে থেকেই বসানো)। মুদ্রার হার আর আবহাওয়ার আউটপুটগুলো শুধু একটি নির্দিষ্ট সময়ের উদাহরণ; আপনার সংখ্যা আলাদা হবে।

প্রথম অনুরোধ — মুদ্রার হার

ধরুন একটি ছোট টুল বানাচ্ছেন, যা ডলারের বিপরীতে টাকার হার দেখায় — বিদেশ থেকে রেমিট্যান্স বা অনলাইন কেনাকাটার হিসাবে কাজে লাগে। বিনা চাবিতে ব্যবহারযোগ্য এমন একটি খোলা API হলো ExchangeRate-API-র open endpoint (ব্যবহারের আগে এর শর্তাবলি দেখে নিন — যেমন উৎসের নাম দেখানো):

import requests

def usd_to_bdt() -> float:
    r = requests.get("https://open.er-api.com/v6/latest/USD", timeout=10)
    r.raise_for_status()                      # 4xx/5xx হলে HTTPError
    data = r.json()                           # JSON → dict
    if data.get("result") != "success":
        raise RuntimeError(f"API error: {data.get('error-type')}")
    return data["rates"]["BDT"]

rate = usd_to_bdt()
print(f"১ ডলার = {rate:.2f} টাকা; ১৫০ ডলার = {150 * rate:,.2f} টাকা")
১ ডলার = 123.04 টাকা; ১৫০ ডলার = 18,456.22 টাকা     ← হার প্রতিদিন বদলায়, তাই আপনার সংখ্যা আলাদা হবে

ছোট কোডে চারটি ভালো অভ্যাস আছে, প্রতিটি আলাদাভাবে চিনে নিন:

  1. timeout=10 — সবচেয়ে গুরুত্বপূর্ণ। requests-এ কোনো ডিফল্ট timeout নেই: server উত্তর না দিলে আপনার প্রোগ্রাম অনির্দিষ্টকাল অপেক্ষা করবে, কোনো error ছাড়াই। রাতে চলা একটি স্ক্রিপ্ট সকালে দেখবেন একই জায়গায় আটকে আছে। requests-এর নিজের ডকুমেন্টেশনও বলে, প্রায় সব production কোডে timeout দেওয়া উচিত। timeout=(3.05, 10) লিখলে প্রথম সংখ্যা সংযোগ স্থাপনের সীমা, দ্বিতীয়টি উত্তরের প্রতিটি অংশের জন্য অপেক্ষার সীমা — পুরো ডাউনলোডের মোট সময় নয়।
  2. raise_for_status() — status 400 বা তার বেশি হলে requests.HTTPError তোলে, ঠিক থাকলে কিছুই করে না। requests নিজে থেকে 404 বা 500-কে error মনে করে না; আপনি না দেখলে চুপচাপ ভুল body নিয়ে এগোবেন।
  3. r.json() — body-কে JSON হিসেবে পড়ে dict/list দেয় (ভেতরে পর্ব ২৬-এর json.loads). body JSON না হলে error. লেখা চাইলে r.text, কাঁচা byte চাইলে r.content.
  4. উত্তরের গঠন যাচাই — এই API সফল হলেও "result" চাবিতে জানায়; অনেক API 200 দিয়েও ভেতরে error পাঠায়। ডকুমেন্টেশন দেখে যাচাই যোগ করুন।

params, headers আর ঢাকার আবহাওয়া

query হাতে না লিখে dict হিসেবে params=-এ দিন — requests নিজেই percent-encoding করে URL বানায়। header দিন headers=-এ। Open-Meteo নামের একটি খোলা আবহাওয়া API অক্ষাংশ-দ্রাঘিমাংশ দিয়ে বর্তমান তাপমাত্রা দেয় (অবাণিজ্যিক ব্যবহারে চাবি লাগে না; শর্তাবলি দেখে নিন):

import requests

params = {"latitude": 23.81, "longitude": 90.41,
          "current": "temperature_2m,relative_humidity_2m"}
headers = {"User-Agent": "bd-weather-learner/0.1 (you@example.com)"}
r = requests.get("https://api.open-meteo.com/v1/forecast",
                 params=params, headers=headers, timeout=10)
r.raise_for_status()
print(r.url)
now = r.json()["current"]
print(f"ঢাকা: {now['temperature_2m']}°C, আর্দ্রতা {now['relative_humidity_2m']}%")
https://api.open-meteo.com/v1/forecast?latitude=23.81&longitude=90.41&current=temperature_2m%2Crelative_humidity_2m
ঢাকা: 27.5°C, আর্দ্রতা 68%          ← আবহাওয়া বদলায়, তাই সংখ্যা আলাদা হবে

r.url দেখাচ্ছে requests কী URL বানিয়েছে — কমা হয়ে গেছে %2C. ডিবাগিংয়ে এটি খুব কাজের: API অদ্ভুত উত্তর দিলে প্রথমে দেখুন আসলে কী পাঠানো হয়েছিল।

POST আর json=

ডেটা পাঠাতে (নতুন কিছু তৈরি করতে) POST, আর body দিন json=-এ: requests dict-কে JSON বানায় আর নিজেই Content-Type: application/json header বসায়। (data= দিলে যায় HTML ফর্মের ধাঁচে — দুটি গুলিয়ে ফেলা একটি প্রচলিত ভুল।) httpbin.org নামের একটি পরীক্ষার সাইট যা পাঠাবেন তা-ই ফেরত দেখায় — শেখার জন্য আদর্শ:

r = requests.post("https://httpbin.org/post", json={"name": "রিয়া"}, timeout=10)
print(r.status_code, r.json()["json"], r.request.headers["Content-Type"])
# 200 {'name': 'রিয়া'} application/json

Session — একই server-এ বারবার

একই API-তে অনেক অনুরোধ পাঠালে প্রতিবার নতুন সংযোগ (DNS, TCP, TLS — stepper-এর ধাপ ২-৩) খোলা অপচয়। requests.Session একটি সংযোগ খোলা রেখে পুনর্ব্যবহার করে (connection pooling), আর সাধারণ header ও cookie একবার বসালে সব অনুরোধে যায়। pagination বা scraping-এ এটি লক্ষণীয়ভাবে দ্রুত:

with requests.Session() as s:
    s.headers.update({"User-Agent": "bd-books-app/0.1", "Accept": "application/json"})
    for page in range(1, 4):
        r = s.get("https://api.example.com/v1/books", params={"page": page}, timeout=10)
        r.raise_for_status()

৬ · নেটওয়ার্ক ভাঙবেই — timeout, connection error, retry আর backoff

নিজের কম্পিউটারের ফাইল পড়া প্রায় সবসময় সফল হয়। নেটওয়ার্ক তেমন নয়: load-shedding-এ রাউটার বন্ধ, মোবাইল ডেটা দুর্বল, server ব্যস্ত, DNS সাড়া দিচ্ছে না। নেটওয়ার্ক কোডে ব্যর্থতা ব্যতিক্রম নয়, স্বাভাবিক ঘটনা — তাই পর্ব ২৪-এর exception handling এখানে বাধ্যতামূলক। requests-এর সব exception একটি মূল class requests.RequestException-এর নিচে:

exceptionকখনretry করবেন?
requests.ConnectionErrorDNS ব্যর্থ, সংযোগ প্রত্যাখ্যাত, ইন্টারনেট নেইহ্যাঁ, বিরতি দিয়ে
requests.Timeoutনির্ধারিত সময়ে সংযোগ বা উত্তর আসেনিহ্যাঁ, বিরতি দিয়ে (idempotent হলে)
requests.HTTPErrorraise_for_status() 4xx/5xx পেয়েছেশুধু 429 আর 5xx
requests.JSONDecodeErrorbody বৈধ JSON নয়সাধারণত না — উত্তরটা দেখুন

retry — কিন্তু ধীরে ধীরে বাড়ানো বিরতিতে

সাময়িক সমস্যায় আবার চেষ্টা করা যুক্তিসঙ্গত, কিন্তু কীভাবে? ব্যর্থ হওয়ামাত্র সাথে সাথে আবার পাঠালে অসুস্থ server আরও চাপে পড়ে। প্রচলিত কৌশল exponential backoff: প্রথমবার ১ সেকেন্ড, তারপর ২, ৪, ৮ — প্রতিবার দ্বিগুণ, আর একটি সর্বোচ্চ চেষ্টার সংখ্যা। সাথে একটু এলোমেলো বাড়তি সময় (jitter) যোগ করা হয়, যাতে হাজার client একই মুহূর্তে আবার আঘাত না করে। পর্ব ৩৫-এ শেখা decorator এই কাজের জন্য চমৎকার — retry-র যুক্তি একবার লিখে যেকোনো function-এ লাগানো যায়:

import functools, random, time
import requests

RETRY_STATUS = {429, 500, 502, 503, 504}

def retry(times=4, base=1.0):
    def deco(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except (requests.ConnectionError, requests.Timeout) as e:
                    wait, err = base * 2 ** attempt, e
                except requests.HTTPError as e:
                    if e.response.status_code not in RETRY_STATUS:
                        raise                               # 400/401/404: retry অর্থহীন
                    after = e.response.headers.get("Retry-After", "")
                    wait = int(after) if after.isdigit() else base * 2 ** attempt
                    err = e
                if attempt == times - 1:
                    raise err
                time.sleep(wait + random.uniform(0, 0.5))   # backoff + jitter
        return wrapper
    return deco

@retry(times=4)
def get_json(url, **params):
    r = requests.get(url, params=params, timeout=10)
    r.raise_for_status()
    return r.json()

লক্ষ করুন সিদ্ধান্তগুলো: 404 বা 401-এ সাথে সাথে exception উপরে যায়; 429-এ server-এর Retry-After মানা হয় (header-টি সেকেন্ডের বদলে তারিখও হতে পারে — তখন এই কোড নিজের backoff-এ ফিরে যায়); আর শেষ চেষ্টাতেও ব্যর্থ হলে আসল error-টি উপরে যায়, চুপচাপ None ফেরত নয়। আর এটি শুধু GET-এর মতো idempotent অনুরোধে লাগান — POST-এ অন্ধ retry একটি অর্ডার দুবার করতে পারে।

নিজে লেখা শিক্ষণীয়, কিন্তু বাস্তবে requests-এর ভেতরের লাইব্রেরি urllib3-এর তৈরি Retry ব্যবহার করাই প্রচলিত — এটি backoff, status তালিকা, এমনকি Retry-After নিজেই সামলায়:

from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(total=4, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504],
              allowed_methods=["GET"])                 # শুধু idempotent method
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))
r = s.get(url, timeout=10)   # সব চেষ্টা ব্যর্থ হলে requests.exceptions.RetryError

tenacity বা backoff-এর মতো বাইরের লাইব্রেরিও decorator দিয়ে retry দেয়, আরও নমনীয় নিয়মসহ। যেটাই ব্যবহার করুন, তিনটি প্রশ্নের উত্তর কোডে স্পষ্ট থাকতে হবে: কোন ভুলে retry, কতবার, আর কত বিরতিতে.

৭ · httpx আর urllib.request — আধুনিক বিকল্প আর standard library

requests দারুণ, কিন্তু এর দুটি সীমা আছে: এটি async নয়, আর HTTP/2 বোঝে না। একসাথে ৬৪ জেলার আবহাওয়া আনতে চাইলে requests একটির পর একটি অনুরোধ পাঠাবে, আর প্রতিটির উত্তরের জন্য বসে থাকবে। পর্ব ৪২-এ দেখেছেন, এমন I/O-bound কাজে asyncio অপেক্ষার সময়গুলো একসাথে কাটাতে পারে। httpx হলো requests-এর মতো চেহারার একটি আধুনিক লাইব্রেরি, যার sync আর async দুই রূপই আছে, আর ঐচ্ছিকভাবে HTTP/2-ও। একটি সুন্দর পার্থক্য: httpx-এ ডিফল্ট timeout ৫ সেকেন্ড — ভুলে গেলেও প্রোগ্রাম চিরকাল আটকে থাকে না।

import asyncio
import httpx

CITIES = {"ঢাকা": (23.81, 90.41), "চট্টগ্রাম": (22.36, 91.78), "রাজশাহী": (24.37, 88.60)}
URL = "https://api.open-meteo.com/v1/forecast"

async def main():
    async with httpx.AsyncClient(timeout=10) as client:
        tasks = [client.get(URL, params={"latitude": la, "longitude": lo,
                                         "current": "temperature_2m"})
                 for la, lo in CITIES.values()]
        responses = await asyncio.gather(*tasks)
    for city, r in zip(CITIES, responses):
        r.raise_for_status()
        print(city, r.json()["current"]["temperature_2m"], "°C")

asyncio.run(main())
ঢাকা 27.5 °C
চট্টগ্রাম 26.7 °C
রাজশাহী 25.0 °C          ← আবহাওয়া অনুযায়ী সংখ্যা বদলাবে

তিনটি অনুরোধ প্রায় একসাথে যায়, তাই মোট সময় মোটামুটি একটি অনুরোধের সমান। কিন্তু সাবধান: async দিয়ে একসাথে শত শত অনুরোধ পাঠানো খুব সহজ — আর সেটাই rate limit ভাঙার সবচেয়ে দ্রুত উপায়। পর্ব ৪২-এর asyncio.Semaphore দিয়ে একসাথে চলা অনুরোধের সংখ্যা সীমিত রাখুন।

কিছু install করা যাবে না? — urllib.request

কখনো এমন জায়গায় কোড চালাতে হয় যেখানে বাইরের প্যাকেজ install করার সুযোগ নেই — একটি সীমাবদ্ধ server, একটি ছোট একক-ফাইলের স্ক্রিপ্ট। তখন standard library-র urllib যথেষ্ট:

import json
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

req = Request("https://httpbin.org/get?q=x", headers={"User-Agent": "bd-learner/0.1"})
try:
    with urlopen(req, timeout=10) as resp:
        print(resp.status, json.load(resp)["args"])     # 200 {'q': 'x'}
except HTTPError as e:            # 4xx/5xx — urllib নিজেই exception তোলে
    print("HTTP", e.code, e.reason)
except URLError as e:             # DNS, সংযোগ, timeout
    print("নেটওয়ার্ক সমস্যা:", e.reason)
বিষয়urllib.requestrequestshttpx
installলাগে নাpippip
ডিফল্ট timeoutনেইনেই৫ সেকেন্ড
4xx/5xx-এexception তোলেতোলে না — raise_for_status()তোলে না — raise_for_status()
asyncনানাহ্যাঁ
HTTP/2নানাঐচ্ছিক
কখনছোট স্ক্রিপ্ট, প্যাকেজ নিষেধসাধারণ sync কাজ — সবচেয়ে প্রচলিতasync অ্যাপ, FastAPI-র সাথে, নতুন প্রজেক্ট
C/C++ জানলে

C বা C++-এর standard library-তে কোনো HTTP client নেই। C-তে সাধারণত libcurl ব্যবহার হয়, নয়তো socket খুলে নিজের হাতে GET / HTTP/1.1 লাইন লিখে পাঠাতে হয় আর উত্তরের byte নিজে পার্স করতে হয়। Python-এর requests ভেতরে ঠিক সেই socket-ই ব্যবহার করে — শুধু DNS, TLS, header, redirect আর encoding-এর কাজ আপনার হয়ে করে দেয়। socket-এর নিচের স্তর দেখতে চাইলে পড়ুন C কোর্সের TCP socket পর্ব।

৮ · web scraping — দায়িত্ব আগে, কোড পরে

সব তথ্যের API থাকে না। একটি বইয়ের দোকানের ওয়েবসাইটে দাম আছে, একটি প্রতিষ্ঠানের সাইটে নোটিশের তালিকা আছে, কিন্তু প্রোগ্রামের জন্য কোনো JSON নেই। তখন HTML পাতা নামিয়ে ভেতর থেকে তথ্য বের করা যায় — একে বলে web scraping, আর যে প্রোগ্রাম লিংক ধরে ধরে অনেক পাতা ঘোরে তাকে বলে crawler. প্রযুক্তিগতভাবে এটি সহজ; কঠিন অংশটি হলো দায়িত্ব। আপনার স্ক্রিপ্ট অন্য কারও server-এ চলে, তাদের বিদ্যুৎ আর ব্যান্ডউইথ খরচ করে, আর এমন তথ্য পড়ে যা মানুষের জন্য প্রকাশিত হয়েছিল, প্রোগ্রামের জন্য নয়।

পাড়ার লাইব্রেরিতে গিয়ে বই পড়া আর নোট নেওয়া স্বাভাবিক। কিন্তু কেউ যদি প্রতিদিন শত শত বই একসাথে নামিয়ে ফটোকপি করতে থাকে, অন্য পাঠকরা বসতে পারে না, আর লাইব্রেরিয়ান নিয়ম জানতে চাইবেন। লাইব্রেরির দরজায় নোটিশ থাকে — কোন তাক সবার জন্য, কোনটি শুধু কর্মীদের। scraping-এ সেই নোটিশটি হলো robots.txt আর সাইটের শর্তাবলি।

ভদ্র scraper-এর নিয়ম

  1. আগে API খুঁজুন। অনেক সাইটের official API, ডেটা ডাউনলোডের পাতা বা RSS feed থাকে। সরকারি আর আন্তর্জাতিক অনেক প্রতিষ্ঠান খোলা ডেটা দেয়। API স্থিতিশীল, পরিষ্কার, আর অনুমোদিত — scraping এর কোনোটিই নয়।
  2. robots.txt পড়ুন। সাইটের মূলে (https://site.com/robots.txt) একটি ছোট লেখার ফাইল, যাতে সাইটের মালিক বলে দেন কোন বট কোন path-এ যেতে পারবে না। এটি আইন নয়, একটি স্বেচ্ছা-মানা নিয়ম (Robots Exclusion Protocol) — কিন্তু ভদ্র বট সবসময় মানে।
  3. শর্তাবলি (Terms of Service) পড়ুন। অনেক সাইট স্পষ্ট লিখে দেয় স্বয়ংক্রিয়ভাবে তথ্য সংগ্রহ নিষেধ। লগইন করে ঢুকতে হয় এমন জায়গা scraping প্রায় সবসময় শর্ত ভাঙে।
  4. নিজের পরিচয় দিন। User-Agent-এ বটের নাম আর যোগাযোগের ঠিকানা লিখুন, যাতে সমস্যা হলে সাইটের মালিক আপনাকে জানাতে পারেন। ব্রাউজারের ছদ্মবেশ নেওয়া বা IP বদলে বাধা পেরোনো — মালিকের স্পষ্ট “না”-কে উপেক্ষা করা।
  5. ধীরে চলুন। একবারে একটি অনুরোধ, মাঝে কয়েক সেকেন্ড বিরতি; robots.txt-এ Crawl-delay থাকলে মানুন। 429 বা 503 পেলে থামুন। সম্ভব হলে সাইটের ব্যস্ত সময় এড়ান।
  6. cache করুন, একবারই নামান। পাতা একবার নামিয়ে ডিস্কে রাখুন; কোড ঠিক করতে করতে একই পাতা পঞ্চাশবার নামাবেন না। যতটুকু দরকার শুধু ততটুকু।
  7. ব্যক্তিগত তথ্য নয়। মানুষের নাম, ফোন নম্বর, ঠিকানা, ছবি, প্রোফাইল — প্রকাশ্যে দেখা গেলেও সংগ্রহ করে জমানো গোপনীয়তার লঙ্ঘন হতে পারে, আর অনেক দেশে ব্যক্তিগত তথ্য সুরক্ষার আইন এটি নিয়ন্ত্রণ করে।
  8. কপিরাইট মানুন। তথ্য (একটি বইয়ের দাম) আর সৃজনশীল লেখা (একটি পুরো সংবাদ প্রতিবেদন) আলাদা জিনিস। অন্যের লেখা, ছবি বা ডেটাবেস নিজের সাইটে হুবহু আবার প্রকাশ করা সাধারণত অনুমোদিত নয়।

আইনি দিক নিয়ে একটি সাধারণ কথা। scraping কতটা বৈধ, তা দেশ, সাইটের শর্তাবলি, ডেটার ধরন আর আপনি ডেটা দিয়ে কী করছেন — সবকিছুর ওপর নির্ভর করে, আর বিভিন্ন দেশে বিভিন্ন মামলায় বিভিন্ন রায় হয়েছে। এই পর্ব আইনি পরামর্শ নয়। নিজের শেখার জন্য অনুশীলন-সাইটে scraping এক জিনিস; ব্যবসা বা প্রকাশের জন্য অন্যের ডেটা সংগ্রহ করতে চাইলে আগে অনুমতি নিন বা আইনজ্ঞের পরামর্শ নিন।

শেখার জন্য বিশেষভাবে বানানো কিছু “sandbox” সাইট আছে, যেমন books.toscrape.com — একটি কাল্পনিক বইয়ের দোকান, যার একমাত্র উদ্দেশ্য scraping অনুশীলন। পুরো ভদ্র প্রক্রিয়াটি ধাপে ধাপে দেখুন:

  1. API আছে?আগে খুঁজুনথাকলে scraping নয়
  2. robots.txtcan_fetch?শর্তাবলিও
  3. fetchUA + timeoutমাঝে বিরতি
  4. parseBeautifulSoupselector দিয়ে
  5. saveCSV / JSONএকবারই নামানো
প্রথম প্রশ্নডেটা অন্য পথে পাওয়া যায়?
☐ সাইটে “API” বা “Developers” পাতা?
☐ CSV/JSON ডাউনলোড বা RSS feed?
☐ ব্রাউজারের DevTools → Network-এ কোনো JSON অনুরোধ?
একটিও পেলে → সেটি ব্যবহার করুন
https://shop.example/robots.txtমালিকের নির্দেশ
User-agent: *
Disallow: /account/
Disallow: /cart/
Crawl-delay: 2
# /catalogue/ → অনুমতি আছে; /account/ → নয়
একটি পাতা নামানোপরিচয় দিয়ে, ধীরে
User-Agent: bd-learner-bot/0.1 (you@example.com)
GET /catalogue/page-1.html  → 200
time.sleep(2)
GET /catalogue/page-2.html  → 200
HTML থেকে তথ্যCSS selector
<article class="product_pod">
  <h3><a title="A Light in the Attic">A Light in the ...</a></h3>
  <p class="price_color">£51.77</p>
→ select("article.product_pod"), a["title"], get_text()
books.csvপর্ব ২৬-এর DictWriter
title,price,rating
A Light in the Attic,£51.77,Three
Tipping the Velvet,£53.74,One
# কাঁচা HTML-ও ডিস্কে রাখুন — আবার নামাতে হবে না
  1. আগে বিকল্প খুঁজুন। সাইটে API, ডাউনলোডের পাতা বা feed আছে কি না দেখুন। আরেকটি কৌশল: ব্রাউজারের DevTools খুলে Network ট্যাবে দেখুন পাতাটি নিজেই কোনো JSON API থেকে ডেটা আনছে কি না। থাকলে HTML পার্স করার চেয়ে সেটি পড়া অনেক সহজ ও স্থিতিশীল (তবে শর্তাবলি সেখানেও প্রযোজ্য)।
  2. robots.txt আর শর্তাবলি। standard library-র urllib.robotparser ফাইলটি পড়ে বলে দেয় একটি URL-এ যাওয়ার অনুমতি আছে কি না। ফাইলটি না থাকলে (404) প্রচলিত নিয়মে সব অনুমোদিত — লেখার সময় books.toscrape.com-এ এমনই ছিল। Crawl-delay আনুষ্ঠানিক মানদণ্ডের (RFC 9309) অংশ নয়, কিন্তু মালিক লিখে থাকলে মানা ভদ্রতা।
  3. ধীরে, পরিচয় দিয়ে নামান। একটি Session, নিজের নাম ও যোগাযোগসহ User-Agent, প্রতিটি অনুরোধে timeout, প্রতিটি উত্তরে status যাচাই, আর দুটি পাতার মাঝে বিরতি। একজন মানুষ যত দ্রুত পাতা পড়তে পারে, তার চেয়ে দ্রুত যাওয়ার দরকার নেই।
  4. পার্স করুন। HTML-কে একটি গাছ হিসেবে পড়ুন (পরের অংশে BeautifulSoup), আর CSS selector দিয়ে ঠিক যে ঘরগুলো দরকার সেগুলো তুলুন। লক্ষ করুন: চোখে দেখা শিরোনাম কাটা (“A Light in the ...”), পূর্ণ নাম আছে title attribute-এ — HTML খুলে দেখে নেওয়ার এটাই কারণ।
  5. সংরক্ষণ করুন। ফল CSV বা JSON-এ রাখুন (পর্ব ২৬), আর সম্ভব হলে কাঁচা HTML-ও — তাহলে পার্সিং কোডে ভুল পেলে আবার server-এ না গিয়েই ঠিক করতে পারবেন। কোন তারিখে, কোন উৎস থেকে নেওয়া, তা-ও লিখে রাখুন।

এবার robots.txt যাচাই আর HTML পার্সিং — দুটিই শুধু standard library দিয়ে, নেটওয়ার্ক ছাড়া। robots.txt-এর লাইনগুলো সরাসরি দেওয়া হয়েছে, আর HTML একটি string; html.parser module-এর HTMLParser প্রতিটি ট্যাগ খুললে আর প্রতিটি লেখার টুকরোয় আপনার method ডাকে:

from urllib.robotparser import RobotFileParser
from html.parser import HTMLParser
robots = RobotFileParser()
robots.parse(["User-agent: *", "Disallow: /account/", "Crawl-delay: 2"])
print(robots.can_fetch("mybot", "/books/p1.html"), robots.can_fetch("mybot", "/account/login"), robots.crawl_delay("mybot"))
page = ('<h3><a title="আমার বাংলা বই">আমার বাংলা...</a></h3><p class="price">৳ 250</p>'
        '<h3><a title="Python শেখা">Python শেখা</a></h3><p class="price">৳ 480</p>')
class BookParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.books, self.in_price = [], False
    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if tag == "a" and "title" in attrs:
            self.books.append({"title": attrs["title"]})
        self.in_price = attrs.get("class") == "price"
    def handle_data(self, data):
        if self.in_price:
            self.books[-1]["price"] = int(data.split()[-1])
parser = BookParser()
parser.feed(page)
print(parser.books)

প্রথম লাইন True False 2: /books/p1.html-এ যাওয়া যাবে, /account/login-এ নয় (কারণ Disallow: /account/), আর মালিক অনুরোধের মাঝে ২ সেকেন্ড বিরতি চান। বাস্তবে robots.set_url(".../robots.txt") আর robots.read() দিয়ে ফাইলটি নেটওয়ার্ক থেকে পড়া হয়। দ্বিতীয় লাইন [{'title': 'আমার বাংলা বই', 'price': 250}, {'title': 'Python শেখা', 'price': 480}]. parser প্রতিটি <a> ট্যাগের title attribute থেকে পূর্ণ নাম নিয়েছে (চোখে দেখা লেখা “আমার বাংলা...” কাটা ছিল), আর class="price" ট্যাগের পরের লেখা থেকে ৳ চিহ্ন বাদ দিয়ে সংখ্যা বানিয়েছে। কাজ করে, কিন্তু লক্ষ করুন কতটা হাতের কাজ — নিজে অবস্থা (in_price) মনে রাখতে হচ্ছে। পরের অংশে BeautifulSoup একই কাজ দুই লাইনে করবে।

৯ · BeautifulSoup দিয়ে HTML পড়া — আর regex কেন নয়

HTML দেখতে লেখা, কিন্তু আসলে একটি গাছ: <html>-এর ভেতরে <body>, তার ভেতরে <article>, তার ভেতরে <h3>… ব্রাউজার পাতাটি পড়ে এই গাছ বানায় — একে বলে DOM. BeautifulSoup (প্যাকেজের নাম beautifulsoup4, import করতে হয় bs4) Python-এ একই কাজ করে, আর গাছে খোঁজার সহজ উপায় দেয়। এটি ভাঙা, অসম্পূর্ণ HTML-ও সহনশীলভাবে পড়ে — বাস্তব ওয়েবে যা খুব দরকারি।

python -m pip install requests beautifulsoup4
লিখুনফল
soup.find("h3")প্রথম <h3>, না পেলে None
soup.find_all("a", class_="next")সব মিলে যাওয়া ট্যাগের list (class Python-এর keyword, তাই class_)
soup.select("article.product_pod h3 a")CSS selector দিয়ে সব — ওয়েব ডিজাইনের সেই একই ভাষা
soup.select_one("p.price_color")selector-এর প্রথমটি, না পেলে None
tag.get_text(strip=True)ট্যাগের ভেতরের সব লেখা, দুই পাশের ফাঁকা জায়গা বাদে
tag["href"] / tag.get("href")attribute-এর মান; না থাকলে প্রথমটি KeyError, দ্বিতীয়টি None

এবার পুরো scraper — books.toscrape.com থেকে বইয়ের নাম, দাম আর রেটিং, পাতায় পাতায় next লিংক ধরে, ভদ্র বিরতিসহ, ফল CSV-তে। (এটি আসল নেটওয়ার্ক ব্যবহার করে, তাই এখানে চালানো যাবে না; নিজের কম্পিউটারে চালিয়ে দেখুন।)

import csv, time
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup

START = "https://books.toscrape.com/catalogue/page-1.html"
HEADERS = {"User-Agent": "bd-learner-bot/0.1 (learning project; you@example.com)"}

def scrape(max_pages=3, delay=2.0):
    books, url = [], START
    with requests.Session() as s:
        s.headers.update(HEADERS)
        for _ in range(max_pages):                       # সর্বোচ্চ সীমা
            r = s.get(url, timeout=10)
            r.raise_for_status()
            soup = BeautifulSoup(r.content, "html.parser")   # text নয়, content
            for card in soup.select("article.product_pod"):
                link = card.select_one("h3 a")
                books.append({
                    "title": link["title"],
                    "price": card.select_one("p.price_color").get_text(strip=True),
                    "rating": card.select_one("p.star-rating")["class"][1],
                })
            nxt = soup.select_one("li.next a")
            if nxt is None:
                break
            url = urljoin(url, nxt["href"])              # আপেক্ষিক লিংক → পূর্ণ URL
            time.sleep(delay)                            # ভদ্র বিরতি
    return books

books = scrape(max_pages=2)
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=["title", "price", "rating"])
    w.writeheader()
    w.writerows(books)
print(len(books), books[0])
40 {'title': 'A Light in the Attic', 'price': '£51.77', 'rating': 'Three'}

দুটি খুঁটিনাটি, দুটিই বাস্তব পরীক্ষায় ধরা পড়েছে। প্রথমত, r.content (byte) দেওয়া হয়েছে, r.text নয়। এই সাইটের server header-এ encoding বলে না, তাই requests নিয়ম অনুযায়ী পুরোনো ISO-8859-1 ধরে নেয়, আর r.text-এ দাম হয়ে যায় £51.77 — পর্ব ২৫-এর mojibake. byte দিলে BeautifulSoup পাতার ভেতরের <meta charset> দেখে নিজেই UTF-8 বোঝে। বাংলা সাইটে এই ভুল হলে পুরো লেখা অর্থহীন চিহ্নে ভরে যায়। দ্বিতীয়ত, রেটিং লেখা হিসেবে নেই; আছে class নামে (star-rating Three) — তাই HTML নিজের চোখে খুলে দেখা ছাড়া scraper লেখা যায় না।

HTML-এ regex কেন নয়

পর্ব ২৮-এ regular expression শিখেছেন, আর প্রথম চিন্তা আসতে পারে: re.findall(r'<a title="(.*?)"', html). ছোট নমুনায় এটি কাজ করবে, তারপর বাস্তবে ভাঙবে। attribute-এর ক্রম বদলালে (<a href=... title=...>), একক উদ্ধৃতি এলে, ট্যাগ দুই লাইনে ভাঙলে, মন্তব্যের ভেতরে একই লেখা থাকলে, &amp;-এর মতো entity এলে — প্রতিটির জন্য regex আরও জটিল হয়। মূল কারণ তাত্ত্বিক: HTML-এ ট্যাগের ভেতরে ট্যাগ যত গভীরে খুশি বসানো যায়, আর সাধারণ regular expression এমন অসীম গভীরতার গঠন গুনে মেলাতে পারে না। parser ঠিক এই কাজের জন্যই বানানো। regex-এর জায়গা হলো parser দিয়ে লেখা বের করার পরে — যেমন "৳ 1,250" থেকে সংখ্যাটি তোলা।

JavaScript দিয়ে বানানো পাতা

অনেক আধুনিক সাইটে প্রথম HTML প্রায় ফাঁকা; ব্রাউজারে JavaScript চলে তারপর ডেটা এনে পাতা ভরায়। requests JavaScript চালায় না, তাই আপনি পাবেন সেই ফাঁকা কাঠামো। দুটি পথ আছে। প্রথম ও ভালো পথ: DevTools-এর Network ট্যাবে খুঁজুন পাতাটি কোন JSON endpoint থেকে ডেটা আনছে — অনেক সময় সেটিই সবচেয়ে পরিষ্কার উৎস (শর্তাবলি মেনে)। দ্বিতীয় পথ: Playwright বা Selenium-এর মতো টুল দিয়ে একটি আসল ব্রাউজারকে প্রোগ্রাম থেকে চালানো (headless browser) — কিন্তু এটি ধীর, ভারী, আর server-এর ওপর চাপ বেশি। তাই নিয়মটি আবারও: আগে API.

পরীক্ষা ও ইন্টারভিউতে
  • GET আর POST-এর পার্থক্য? — GET ডেটা পড়ে, body থাকে না, আবার পাঠানো নিরাপদ (idempotent); POST নতুন কিছু তৈরি করে বা কাজ চালায়, ডেটা যায় body-তে, আর দুবার পাঠালে দুবার কাজ হতে পারে।
  • 401 আর 403-এর পার্থক্য? — 401: পরিচয় নেই বা চাবি ভুল; 403: পরিচয় জানা, কিন্তু অনুমতি নেই।
  • requests-এ timeout না দিলে কী হয়? — কোনো ডিফল্ট সীমা নেই; server উত্তর না দিলে প্রোগ্রাম অনির্দিষ্টকাল আটকে থাকতে পারে।
  • 4xx আর 5xx-এ retry-এর নিয়ম? — 4xx client-এর ভুল, retry অর্থহীন (ব্যতিক্রম 429 — অপেক্ষা করে আবার); 5xx সাময়িক server সমস্যা, exponential backoff দিয়ে সীমিতবার retry.
  • robots.txt কী? — সাইটের মূলে রাখা একটি লেখার ফাইল, যেখানে মালিক বলে দেন কোন বট কোন path-এ যাবে না; আইনি বাধ্যবাধকতা নয়, তবে ভদ্র crawler মেনে চলে।

১০ · AI-এর যুগে — AI-এর লেখা নেটওয়ার্ক কোড যাচাই

“এই API থেকে ডেটা আনো” বা “এই সাইট থেকে সব দাম scrape করো” — AI এমন কোড মুহূর্তে লেখে, আর প্রথমবার চালালে প্রায়ই কাজও করে। সমস্যা বেরোয় পরে: রাতে চলতে চলতে আটকে যাওয়া স্ক্রিপ্ট, GitHub-এ ফাঁস হওয়া চাবি, বা সাইটের মালিকের কাছ থেকে IP ব্লক। নিচের ভুলগুলো বারবার দেখা যায়:

AI প্রায়ই যা লেখেসমস্যাভালো রূপ
requests.get(url)timeout নেই — চিরকাল আটকে থাকতে পারেপ্রতিটি অনুরোধে timeout=10
API_KEY = "sk_..." কোডের ভেতরেgit-এ গেলেই ফাঁসos.environ, .env + .gitignore
সরাসরি r.json()404/500-এ বিভ্রান্তিকর error বা ভুল ডেটাআগে raise_for_status(), তারপর গঠন যাচাই
while True: retry, কোনো বিরতি নেইserver-এ আঘাত, অসীম লুপসীমিত চেষ্টা, backoff + jitter, 429-এ Retry-After
হাজার অনুরোধ একসাথে, ব্রাউজারের ভুয়া User-Agentrate limit ভাঙা, মালিকের “না” উপেক্ষাসৎ User-Agent, বিরতি, Semaphore
robots.txt বা শর্তাবলির কোনো উল্লেখ নেইনিষিদ্ধ জায়গায় scrapingআগে API, তারপর robotparser আর শর্তাবলি
re.findall দিয়ে HTML পার্সছোট পরিবর্তনেই চুপচাপ ভাঙেBeautifulSoup / html.parser
f-string দিয়ে query জোড়াবাংলা বা &-এ ভুল URLparams= বা urlencode

AI-কে API বা scraping কোড লিখতে বলার সময় শর্তগুলো আগেই বলে দিন:

আমি Python 3.12 বা নতুন version আর requests লাইব্রেরি ব্যবহার করি। একটি খোলা আবহাওয়া API থেকে বাংলাদেশের ৮টি বিভাগীয় শহরের বর্তমান তাপমাত্রা এনে weather.csv-তে লেখো (শহর, তাপমাত্রা, সময়)। API-র ডকুমেন্টেশনের লিংক: [এখানে দেব]. ডকুমেন্টেশনে যা নেই এমন কোনো endpoint বা parameter বানাবে না। শর্ত: ১. প্রতিটি অনুরোধে timeout দেবে, একটি requests.Session ব্যবহার করবে, আর User-Agent-এ প্রজেক্টের নাম দেবে। ২. API key থাকলে শুধু environment variable থেকে পড়বে; কোডে বা log-এ চাবি থাকবে না। ৩. প্রতিটি উত্তরে raise_for_status() আর JSON-এর দরকারি চাবিগুলো যাচাই করবে। ৪. ConnectionError, Timeout, 429 আর 5xx-এ সর্বোচ্চ ৪ বার retry করবে exponential backoff দিয়ে; 429-এ Retry-After মানবে; 4xx-এ retry করবে না। ৫. অনুরোধের মাঝে অন্তত ১ সেকেন্ড বিরতি দেবে। কোনো শহর ব্যর্থ হলে বাকিগুলো চালিয়ে যাবে আর শেষে কোনগুলো ব্যর্থ তা জানাবে। ৬. কোডের নিচে লিখে দাও কোন পরিস্থিতিতে কী ঘটবে।

AI-এর লেখা HTTP/scraping কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১HTTP শুরু হয়েছিল এক লাইনের একটি সরল প্রোটোকল হিসেবে। HTTP/1.1, HTTP/2 আর HTTP/3 — প্রতিটি নতুন version কোন সমস্যার সমাধান করতে এসেছে?

১৯৮৯ সালের মার্চে CERN-এ Tim Berners-Lee একটি প্রস্তাব লেখেন: গবেষকদের নথিগুলো যেন একে অপরের সাথে লিংক দিয়ে যুক্ত থাকে, যেকোনো কম্পিউটার থেকে পড়া যায়। ১৯৯০ সালের শেষ দিকে তিনি প্রথম web server, প্রথম ব্রাউজার, HTML আর HTTP — চারটিই চালু করেন, আর ১৯৯১ সালে প্রকল্পটি CERN-এর বাইরে উন্মুক্ত হয়। প্রথম HTTP (পরে নাম হয় HTTP/0.9) ছিল হাস্যকর রকমের সরল: client একটি লাইন পাঠাত — GET /page.html — আর server HTML পাঠিয়ে সংযোগ বন্ধ করত। কোনো header নেই, status code নেই, ছবি বা অন্য ফরম্যাট নেই।

ওয়েব দ্রুত বাড়ল, আর ব্রাউজার ও server নির্মাতারা নিজেরাই header, status code আর Content-Type যোগ করলেন। ১৯৯৬ সালে RFC 1945 সেই প্রচলিত চর্চাকে HTTP/1.0 হিসেবে লিখে রাখে। কিন্তু একটি বড় অপচয় ছিল: প্রতিটি ছবি বা ফাইলের জন্য নতুন TCP সংযোগ। HTTP/1.1 (প্রথমে RFC 2068, ১৯৯৭; পরে RFC 2616, ১৯৯৯) সংযোগ খোলা রাখার ব্যবস্থা (persistent connection) বাধ্যতামূলক করল, Host header দিয়ে একটি IP-তে অনেক ওয়েবসাইট রাখা সম্ভব করল, আর cache ও chunked transfer-এর নিয়ম দিল। এই version প্রায় দুই দশক ওয়েবকে চালিয়েছে। পরে স্পেসিফিকেশনটি কয়েক দফায় নতুন করে লেখা হয়; ২০২২ সালে RFC 9110 HTTP-র “অর্থ” (method, status, header) সব version-এর জন্য এক জায়গায় সাজায়, আর RFC 9112 শুধু HTTP/1.1-এর লেখার ধরন।

HTTP/1.1-এর একটি মৌলিক সমস্যা ছিল head-of-line blocking: একটি সংযোগে একটির পর একটি উত্তর, তাই একটি ধীর ফাইল পেছনের সবাইকে আটকে রাখে। ব্রাউজাররা তাই একসাথে ছয়টি পর্যন্ত সংযোগ খুলত। Google পরীক্ষামূলকভাবে SPDY নামের একটি প্রোটোকল বানায়, আর তার ভিত্তিতে ২০১৫ সালে আসে HTTP/2 (RFC 7540): লেখার বদলে binary frame, একটি সংযোগে অনেক অনুরোধ একসাথে (multiplexing), আর header সংকোচন। কিন্তু নিচে TCP থাকায় একটি প্যাকেট হারালে সব stream আবার আটকে যেত। তাই HTTP/3 (RFC 9114, ২০২২) TCP ছেড়ে QUIC-এর ওপর চলে — UDP-ভিত্তিক একটি নতুন পরিবহন প্রোটোকল (RFC 9000, ২০২১), যাতে TLS ভেতরেই গাঁথা আর প্রতিটি stream স্বাধীন। দুর্বল মোবাইল নেটওয়ার্কে এর সুবিধা সবচেয়ে বেশি।

লক্ষ করার মতো ব্যাপার: এত পরিবর্তনের পরও GET, POST, 404, Content-Type — অর্থ একই আছে। তাই ১৯৯৯ সালের ধারণা দিয়ে লেখা requests কোড আজকের server-এর সাথেও কথা বলে; পরিবহনের স্তর বদলেছে, ভাষার অর্থ নয়।

মূল উপলব্ধি: ভালো প্রোটোকল নকশা অর্থ আর পরিবহনকে আলাদা রাখে। HTTP-র method আর status-এর শব্দভান্ডার তিন দশক টিকে আছে, আর নিচের স্তর বারবার বদলে গতি বাড়ানো গেছে — আপনার কোডকে না ভেঙেই।

প্র ০২প্রায় সব API নিজেকে “REST API” বলে। REST শব্দটি আসলে কোথা থেকে এল, এর মূল নীতি কী, আর আজকের বেশিরভাগ JSON API সত্যিই কতটা REST?

REST (Representational State Transfer) শব্দটি এসেছে Roy Fielding-এর ২০০০ সালের PhD গবেষণাপত্র থেকে, University of California, Irvine-এ। Fielding শুধু একজন গবেষক ছিলেন না — তিনি HTTP/1.1 স্পেসিফিকেশনের অন্যতম লেখক আর Apache web server প্রকল্পের সহ-প্রতিষ্ঠাতা। গবেষণাপত্রে তিনি ব্যাখ্যা করেন, ওয়েব কেন এত বড় হয়েও টিকে আছে — কোন স্থাপত্যগত নিয়ম এটিকে বিশাল আকারে কাজ করতে দেয় — আর সেই নিয়মগুলোর সমষ্টির নাম দেন REST.

মূল নিয়মগুলো: client আর server আলাদা; প্রতিটি অনুরোধ stateless — server আগের অনুরোধ মনে রাখে না, সব তথ্য অনুরোধেই থাকে; উত্তর cache করা যায় কি না তা স্পষ্ট বলা থাকে; মাঝখানে proxy বা cache-এর মতো স্তর থাকতে পারে, client না জেনেই; আর সবচেয়ে গুরুত্বপূর্ণ, একটি uniform interface — প্রতিটি জিনিস (resource) একটি URL দিয়ে চেনা যায়, আর সবার সাথে একই সীমিত method (GET, PUT, DELETE…) দিয়ে কাজ হয়। এর একটি অংশ, যা প্রায়ই ভুলে যাওয়া হয়, হলো hypermedia: উত্তরের ভেতরেই পরের সম্ভাব্য কাজের লিংক থাকবে, ঠিক যেমন HTML পাতায় লিংক থাকে, যাতে client-কে আগে থেকে সব URL মুখস্থ রাখতে না হয়।

কেন এই নিয়মগুলো কাজের? statelessness-এর জন্য যেকোনো অনুরোধ যেকোনো server যন্ত্রে পাঠানো যায় — হাজার যন্ত্রে ভাগ করা সহজ। cache-যোগ্যতার জন্য একই উত্তর বারবার বানাতে হয় না। uniform interface-এর জন্য requests-এর মতো একটি সাধারণ লাইব্রেরি পৃথিবীর যেকোনো API-র সাথে কথা বলতে পারে।

বাস্তবে আজকের বেশিরভাগ “REST API” হলো “HTTP-র ওপর JSON, URL-এ resource-এর নাম, আর মোটামুটি ঠিক method”. hypermedia প্রায় কেউ রাখে না; client ডকুমেন্টেশন পড়ে URL কোডে লিখে রাখে। Fielding নিজে ২০০৮ সালে একটি ব্লগ লেখায় অভিযোগ করেন যে hypermedia ছাড়া কোনো API-কে REST বলা যায় না। শিল্পজগৎ তবু শব্দটি ঢিলেঢালা অর্থেই ব্যবহার করে চলেছে। বিকল্পও এসেছে: Facebook-এর GraphQL (ভেতরে ২০১২ থেকে, উন্মুক্ত ২০১৫), যেখানে client একটি অনুরোধে ঠিক যে ঘরগুলো দরকার তা চায়; আর gRPC, যা binary ফরম্যাটে দ্রুত সার্ভিস-থেকে-সার্ভিস কথার জন্য।

মূল উপলব্ধি: REST কোনো লাইব্রেরি বা ফরম্যাট নয়, ওয়েবের সাফল্য থেকে বের করা কিছু স্থাপত্য-নিয়ম। আপনি পর্ব ৪৮-এ নিজের API বানানোর সময় সবচেয়ে কাজের অংশগুলো — স্পষ্ট resource URL, সঠিক method আর status code, stateless অনুরোধ — মেনে চললেই ব্যবহারকারীরা আপনার API সহজে বুঝবে।

প্র ০৩requests-এর স্লোগান “HTTP for Humans”. standard library-তে urllib থাকা সত্ত্বেও একটি বাইরের লাইব্রেরি কেন প্রায় সবার প্রথম পছন্দ হলো — আর তবু এটি standard library-তে ঢোকেনি কেন?

২০১০-এর দিকে Python 2-এ HTTP অনুরোধ পাঠানোর জন্য ছিল urllib আর urllib2 — দুটি আলাদা module, কাজ ভাগাভাগি করা অদ্ভুতভাবে। একটি সাধারণ Basic Authentication-সহ GET-এর জন্য password manager, handler, opener বানাতে হতো; query encode করতে আরেক module; JSON পড়তে আরেকটি। Kenneth Reitz এই অভিজ্ঞতায় বিরক্ত হয়ে ২০১১ সালের ফেব্রুয়ারিতে requests প্রকাশ করেন। নকশার দর্শন ছিল: প্রোগ্রামার যা ভাবে, কোড যেন ঠিক তেমন দেখায় — requests.get(url, params=..., auth=...), আর উত্তরে r.status_code, r.json(). ভেতরের জটিল কাজ — connection pooling, TLS যাচাই, redirect, encoding অনুমান — হয় urllib3 নামের আরেকটি লাইব্রেরির ওপর।

লাইব্রেরিটি দ্রুত Python জগতের মানদণ্ড হয়ে ওঠে; আজও এটি PyPI-র সবচেয়ে বেশি নামানো প্যাকেজগুলোর একটি, আর pip নিজের ভেতরে এর একটি কপি রাখে। পরে প্রজেক্টটির রক্ষণাবেক্ষণ Python Software Foundation-এর GitHub সংস্থার অধীনে যায়। এর API নকশা এতটাই প্রভাবশালী যে httpx ইচ্ছে করে প্রায় একই চেহারা নিয়েছে, যাতে requests জানা মানুষ সহজে সরে যেতে পারে।

তাহলে standard library-তে কেন নয়? standard library-র একটি বড় শর্ত হলো স্থিতিশীলতা: একটি module ঢুকলে তার API বছরের পর বছর না ভেঙে রাখতে হয়, আর নতুন বৈশিষ্ট্য আসে শুধু Python-এর নতুন version-এর সাথে, বছরে একবার। HTTP-র জগৎ দ্রুত বদলায় — নতুন TLS নিয়ম, নিরাপত্তা সমস্যার জরুরি সমাধান, সার্টিফিকেট তালিকার হালনাগাদ। একটি বাইরের প্যাকেজ এক সপ্তাহে নতুন version দিতে পারে; standard library-র module পারে না, আর পুরোনো Python ব্যবহারকারীরা হালনাগাদ পানই না। requests-এর রক্ষণাবেক্ষণকারী আর Python-এর মূল ডেভেলপাররা এই কারণে একমত হন যে এটি বাইরে থাকাই ভালো। এর একটি দামও আছে: requests-এর কিছু ডিফল্ট, যেমন timeout না থাকা, পেছনের দিকে সামঞ্জস্য রাখার জন্য বদলানো কঠিন — httpx নতুন শুরু করেছিল বলে ৫ সেকেন্ডের ডিফল্ট দিতে পেরেছে।

মূল উপলব্ধি: API নকশা একটি লাইব্রেরির জনপ্রিয়তার সবচেয়ে বড় কারণ হতে পারে — একই কাজ, কিন্তু মানুষের চিন্তার ধাঁচে সাজানো। আর “standard library-তে নেই” মানেই খারাপ নয়; দ্রুত বদলানো ক্ষেত্রে বাইরে থাকাই অনেক সময় নিরাপদ।

প্র ০৪robots.txt কোনো প্রযুক্তিগত বাধা নয় — যেকোনো বট চাইলে উপেক্ষা করতে পারে। তবু প্রায় ত্রিশ বছর ধরে এটি কাজ করছে কেন? আর AI-এর যুগে এটি কোন নতুন প্রশ্নের মুখে?

১৯৯৩-৯৪ সালে ওয়েব তখনও ছোট, আর প্রথম দিকের crawler-গুলো প্রায়ই অসাবধান ছিল — কোনো বিরতি ছাড়া একটি server-এর সব পাতা একসাথে চাইত, আর তখনকার দুর্বল server বসে যেত। ১৯৯৪ সালে Martijn Koster, যিনি নিজেও ওয়েবের প্রথম দিকের একটি সার্চ ইঞ্জিন (ALIWEB) বানিয়েছিলেন, ওয়েব-ডেভেলপারদের একটি mailing list-এ একটি সহজ প্রস্তাব দেন: প্রতিটি সাইটের মূলে /robots.txt নামে একটি ফাইল থাকবে, যেখানে মালিক লিখবেন কোন বট কোন path-এ যাবে না। কোনো সংস্থা এটি অনুমোদন করেনি; এটি ছিল ভদ্রলোকের চুক্তি। তবু তখনকার crawler নির্মাতারা দ্রুত মেনে নেন, আর পরে বড় সার্চ ইঞ্জিনগুলোও।

এটি কাজ করল কেন? কারণ সবার স্বার্থ মিলেছিল। সাইটের মালিক চান সার্চ ইঞ্জিনে আসতে, কিন্তু server বাঁচাতে আর কিছু জায়গা (লগইন, কার্ট, অসীম ক্যালেন্ডার পাতা) বাদ রাখতে। সার্চ ইঞ্জিন চায় মালিকদের আস্থা, যাতে তারা IP ব্লক না করে। নিয়মটি এত সহজ যে যে কেউ এক বিকেলে মেনে চলতে পারে। আর যে বট মানে না, তার পরিচয় প্রকাশ পেলে খ্যাতির ক্ষতি হয়, আর সাইট তাকে প্রযুক্তিগতভাবে আটকানোর ব্যবস্থা নেয়।

প্রায় ২৫ বছর এটি অলিখিত মানদণ্ড ছিল, আর প্রতিটি crawler একটু আলাদাভাবে ব্যাখ্যা করত — wildcard চলবে কি না, ফাইল কত বড় হতে পারে, ফাইল না পেলে কী। ২০১৯ সালে Google, Koster-সহ কয়েকজনকে নিয়ে IETF-এ একটি খসড়া জমা দেয়, আর ২০২২ সালের সেপ্টেম্বরে এটি RFC 9309 হিসেবে প্রকাশিত হয়। মজার বিষয়, Crawl-delay এর অংশ নয় — কিছু crawler মানে, কিছু মানে না।

AI-এর যুগে প্রশ্ন বদলেছে। আগে বিনিময়টি পরিষ্কার ছিল: সার্চ ইঞ্জিন পাতা পড়ে, বদলে পাঠক পাঠায়। বড় ভাষা-মডেল প্রশিক্ষণের জন্য পাতা সংগ্রহ করলে সেই বিনিময় আর স্পষ্ট নয় — মডেল উত্তর দিয়ে দেয়, পাঠক হয়তো মূল সাইটে যায়ই না। ২০২৩ সাল থেকে বিভিন্ন AI প্রতিষ্ঠান নিজেদের crawler-এর আলাদা User-Agent নাম প্রকাশ করেছে, আর বহু সংবাদ ও প্রকাশনা সাইট robots.txt-এ সেগুলো আটকে দিয়েছে। কিছু crawler নিয়ম না মানার অভিযোগও উঠেছে। ফলে আলোচনা চলছে — শুধু robots.txt যথেষ্ট কি না, নাকি অনুমতি, লাইসেন্স বা আইনের নতুন কাঠামো লাগবে।

মূল উপলব্ধি: ইন্টারনেটের অনেক কিছু প্রযুক্তি দিয়ে নয়, পারস্পরিক আস্থা দিয়ে চলে। robots.txt মানা আপনার scraper-কে ধীর করে না — এটি আপনাকে সেই সম্প্রদায়ের ভদ্র সদস্য রাখে, যাদের আস্থার ওপর খোলা ওয়েব টিকে আছে।

প্র ০৫প্রায় প্রতিটি API চাবি চায় আর rate limit চাপায়। শুধু “অপব্যবহার ঠেকানো” ছাড়া এর পেছনে আর কী কী প্রকৌশল ও অর্থনৈতিক যুক্তি আছে, আর server আসলে সীমা গোনে কীভাবে?

প্রথম যুক্তি ক্ষমতা আর ন্যায্যতা। একটি server একসাথে সীমিত অনুরোধ সামলাতে পারে। সীমা না থাকলে একজনের ভুল লুপ — while True: requests.get(...) — হাজার ব্যবহারকারীর সেবা ধীর করে দেয়। সীমা প্রত্যেককে একটি ন্যায্য ভাগ দেয়, অনেকটা সরু রাস্তায় ট্রাফিক সিগন্যালের মতো।

দ্বিতীয় যুক্তি পরিচয় আর হিসাব। চাবি দিয়ে server জানে কে কতটুকু ব্যবহার করছে। এতে বিনা মূল্যের স্তরে দিনে ১,০০০ অনুরোধ আর পেইড স্তরে বেশি — এমন ব্যবসায়িক মডেল সম্ভব হয়। সমস্যা হলে নির্দিষ্ট চাবি বন্ধ করা যায়, সবাইকে নয়। আর ডেভেলপারের সাথে যোগাযোগের পথ থাকে: API বদলালে আগে থেকে জানানো। মনে রাখবেন, চাবি শক্ত নিরাপত্তা নয় — এটি পরিচয়ের টোকেন; ব্যবহারকারীর ব্যক্তিগত ডেটায় প্রবেশের মতো কাজে OAuth-এর মতো আরও শক্ত পদ্ধতি লাগে, যেখানে ব্যবহারকারী নিজে অনুমতি দেন আর token-এর মেয়াদ সীমিত।

server সীমা গোনে কয়েকটি প্রচলিত অ্যালগরিদমে। সবচেয়ে সহজ fixed window: প্রতি মিনিটে একটি গণনা, মিনিট শুরু হলে শূন্য — কিন্তু মিনিটের শেষ সেকেন্ড আর পরের মিনিটের প্রথম সেকেন্ডে দ্বিগুণ অনুরোধ পাঠানো যায়। sliding window এই ফাঁক কমায়। সবচেয়ে জনপ্রিয় token bucket: একটি বালতিতে নির্দিষ্ট হারে token জমে (ধরুন সেকেন্ডে ১টি, সর্বোচ্চ ৬০টি), প্রতিটি অনুরোধে একটি token খরচ; বালতি খালি হলে 429. এতে মাঝে মাঝে হঠাৎ কিছু অনুরোধ (burst) চলে, কিন্তু গড় হার সীমিত থাকে। একাধিক server যন্ত্র থাকলে গণনা রাখা হয় Redis-এর মতো একটি দ্রুত কেন্দ্রীয় ভান্ডারে।

429 status code-টি নিজেই তুলনামূলক নতুন — ২০১২ সালের RFC 6585 এটি যোগ করে, আগে API-গুলো 403 বা 503 দিয়ে কাজ চালাত। সাথে Retry-After header client-কে ঠিক কতক্ষণ অপেক্ষা করতে হবে বলে দেয়। ভালো client এই তথ্য মেনে চলে, আর সবচেয়ে ভালো client সীমায় পৌঁছানোর আগেই নিজের গতি কমায় — উত্তরের header-এ বাকি সংখ্যা দেখে বা নিজে বিরতি দিয়ে। পর্ব ৪৮-এ নিজের API বানালে আপনিও এই প্রশ্নের অন্য পাশে দাঁড়াবেন: আপনার server-কে কে কতটুকু ব্যবহার করতে পারবে?

মূল উপলব্ধি: চাবি আর rate limit হলো একটি ভাগ করা সম্পদকে টেকসই রাখার ব্যবস্থা — প্রকৌশলের দিক থেকে ক্ষমতা রক্ষা, অর্থনীতির দিক থেকে বিনা মূল্যের সেবা টিকিয়ে রাখা। সীমা মানা client সেই সম্পদ সবার জন্য খোলা রাখতে সাহায্য করে।

অনুশীলন

এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. URL ভাঙুন: https://shop.example.com/api/v2/products?category=rice&sort=price&page=3#top — scheme, host, path, query-র প্রতিটি parameter আর fragment আলাদা করে লিখুন। কোন অংশটি server-এ যায় না? আর category-র মান যদি চাল & ডাল হতো, হাতে লেখা URL-এ কী সমস্যা হতো?

    scheme https; host shop.example.com; path /api/v2/products; query: category=rice, sort=price, page=3; fragment top. fragment ব্রাউজারেই থাকে, server-এ যায় না। চাল & ডাল হাতে বসালে ফাঁকা জায়গা আর বাংলা অক্ষর অবৈধ, আর & চিহ্নটি query-কে ভেঙে দেয় — server দেখবে category=চাল আর ডাল নামের একটি অদ্ভুত parameter. params= বা urlencode একে %E0...%26... রূপে ঠিকভাবে encode করে।

  2. সিদ্ধান্ত নিন: আপনার স্ক্রিপ্ট প্রতিটি পরিস্থিতিতে কী করবে — retry, অপেক্ষা করে retry, নাকি থেমে জানানো? (ক) GET-এ 503; (খ) GET-এ 404; (গ) GET-এ 429, সাথে Retry-After: 20; (ঘ) POST দিয়ে অর্ডার পাঠাতে গিয়ে Timeout; (ঙ) 401.

    (ক) backoff দিয়ে সীমিতবার retry — সাময়িক server সমস্যা। (খ) থেমে জানান — ঠিকানা ভুল, retry অর্থহীন। (গ) ২০ সেকেন্ড অপেক্ষা করে আবার, আর পরে নিজের গতি কমান। (ঘ) অন্ধ retry নয়! অর্ডারটি হয়তো server-এ পৌঁছেছে, শুধু উত্তর আসেনি; আবার পাঠালে দুটি অর্ডার হতে পারে। আগে অর্ডারের অবস্থা দেখুন, বা API যদি idempotency key সমর্থন করে তবে সেটি দিয়ে পাঠান। (ঙ) থামুন — চাবি ভুল বা মেয়াদোত্তীর্ণ; চাবি ঠিক না করে retry করলে একই উত্তর।

  3. বাগ খুঁজুন: একটি AI এই কোড দিয়েছে একটি সংবাদ সাইট থেকে শিরোনাম আনতে: KEY = "abc123", তারপর একটি for লুপে ৫০০টি পাতার জন্য html = requests.get(url + str(i), headers={"User-Agent": "Mozilla/5.0 Chrome"}).text, তারপর re.findall("<h2>(.*?)</h2>", html). অন্তত পাঁচটি সমস্যা খুঁজুন।

    (১) চাবি কোডের ভেতরে — environment variable-এ নিন। (২) timeout নেই — একটি পাতা আটকালে পুরো স্ক্রিপ্ট আটকে যাবে। (৩) status যাচাই নেই — 404 বা 429-এর error পাতাও “শিরোনাম” হিসেবে পার্স হবে। (৪) বিরতি নেই, ৫০০টি অনুরোধ একটানা — সাইটের ওপর চাপ, প্রায় নিশ্চিত ব্লক; Session-ও নেই। (৫) ব্রাউজারের ছদ্মবেশী User-Agent — সৎ পরিচয় দিন। (৬) regex দিয়ে HTML — <h2 class="..."> হলেই মিলবে না; BeautifulSoup ব্যবহার করুন। (৭) .text-এ encoding ভুল হলে বাংলা শিরোনাম নষ্ট হতে পারে। আর মূল প্রশ্ন: সাইটের API বা RSS feed আছে কি না, robots.txt আর শর্তাবলি অনুমতি দেয় কি না, আর সংবাদের পূর্ণ লেখা কপিরাইটযুক্ত।

  4. ছোট কোড লিখুন: একটি function fetch_json(url, params=None, tries=3) লিখুন, যা requests দিয়ে GET পাঠায়, timeout দেয়, ConnectionError/Timeout আর 5xx-এ ১, ২, ৪ সেকেন্ড বিরতিতে retry করে, 4xx-এ সাথে সাথে exception তোলে, আর সফল হলে dict ফেরত দেয়।
    import time
    import requests
    
    def fetch_json(url, params=None, tries=3):
        for attempt in range(tries):
            try:
                r = requests.get(url, params=params, timeout=10)
                if r.status_code < 500:
                    r.raise_for_status()          # 4xx → সাথে সাথে HTTPError
                    return r.json()
                problem = f"server {r.status_code}"
            except (requests.ConnectionError, requests.Timeout) as e:
                problem = type(e).__name__
            if attempt < tries - 1:
                time.sleep(2 ** attempt)         # 1, 2, 4 ...
        raise RuntimeError(f"{tries} বার চেষ্টা ব্যর্থ: {problem}")

    লক্ষ করুন, 4xx-এর HTTPError except-এ ধরা পড়ে না, তাই সরাসরি উপরে যায়। শেষ চেষ্টার পরে আর ঘুমানো হয় না। উন্নতি হিসেবে 429 আলাদা করে Retry-After পড়তে পারেন, আর বিরতিতে একটু jitter যোগ করতে পারেন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি ওয়েবসাইট থেকে দায়িত্বের সাথে ডেটা সংগ্রহের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔎 official API, ডাউনলোড বা feed আছে কি না খোঁজা
  2. 🤖 robots.txt আর শর্তাবলি পড়া
  3. 🐢 সৎ User-Agent, timeout আর বিরতি দিয়ে পাতা নামানো
  4. 🚦 প্রতিটি উত্তরের status code যাচাই
  5. 🧩 BeautifulSoup দিয়ে HTML পার্স করা
  6. 💾 ফল CSV বা JSON-এ সংরক্ষণ

একটি API থেকে HTTP status code 429 এলে client-এর সবচেয়ে উপযুক্ত আচরণ কোনটি?

  • সাথে সাথে একই অনুরোধ আবার পাঠানো
  • ধরে নেওয়া যে API চিরতরে বন্ধ, প্রোগ্রাম থামানো
  • Retry-After header-এর সময় অপেক্ষা করে আবার চেষ্টা
  • নতুন API key বা অন্য IP দিয়ে সাথে সাথে পাঠানো

Python-এর requests লাইব্রেরিতে requests.get(url) লিখলে, timeout না দিলে কী ঘটে?

  • ৫ সেকেন্ড পরে নিজে থেকে Timeout হয়
  • কোনো সীমা থাকে না — server সাড়া না দিলে অনির্দিষ্টকাল অপেক্ষা করতে পারে
  • কোড চালানোর আগেই error দেয়
  • নিজে থেকে তিনবার retry করে তারপর থামে

একটি REST API-তে নতুন একটি অর্ডার তৈরি করতে সাধারণত কোন HTTP method ব্যবহার হয়, আর সফল হলে কোন status code আসা প্রচলিত?

  • GET, 200
  • DELETE, 204
  • PUT, 404
  • POST, 201

একটি Python প্রোগ্রামে ওয়েব API-র গোপন চাবি রাখার সবচেয়ে নিরাপদ সাধারণ উপায় কোনটি?

  • environment variable-এ রেখে os.environ দিয়ে পড়া
  • কোডের শুরুতে একটি constant হিসেবে লেখা
  • কোডের comment-এ লিখে রাখা, যাতে ভুলে না যাই
  • ওয়েবসাইটের JavaScript ফাইলে রাখা, যাতে ব্রাউজার সরাসরি API ডাকে

একটি ওয়েবসাইটের /robots.txt ফাইল সম্পর্কে কোন কথাটি সঠিক?

  • এটি নিষিদ্ধ পাতাগুলো পাসওয়ার্ড দিয়ে সুরক্ষিত করে
  • এটি ব্রাউজারকে বলে কোন পাতা মানুষকে দেখানো যাবে না
  • এতে সাইটের মালিক বটদের জানান কোন path-এ যাওয়া যাবে না, আর ভদ্র crawler তা মেনে চলে
  • এতে অনুমতি থাকলে যেকোনো ডেটা যেকোনো কাজে ব্যবহার আইনত বৈধ

HTML পাতা থেকে তথ্য বের করতে regular expression-এর বদলে BeautifulSoup-এর মতো parser ব্যবহারের মূল কারণ কী?

  • regex সবসময় অনেক ধীর
  • HTML একটি nested গাছের গঠন, যা regex নির্ভরযোগ্যভাবে সামলাতে পারে না
  • Python-এর re module HTML string পড়তে পারে না
  • parser ব্যবহার করলে robots.txt মানার দরকার হয় না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ওয়েব = request আর response. URL বলে কোথায়, method বলে কী করতে চাই, header বলে আমি কে, আর status code বলে কী হলো — 2xx সফল, 4xx আপনার ভুল, 5xx server-এর।
  2. requests-এ প্রতিটি অনুরোধে timeout=, তারপর raise_for_status(), তারপর .json() আর উত্তরের গঠন যাচাই। query দিন params=-এ, JSON body json=-এ, বারবার অনুরোধে Session.
  3. নেটওয়ার্ক ভাঙবেই: ConnectionError, Timeout, 429 আর 5xx-এ সীমিত retry, exponential backoff আর jitter দিয়ে; 429-এ Retry-After মানুন; 4xx-এ আর POST-এ অন্ধ retry নয়।
  4. API key পাসওয়ার্ডের মতো — environment variable-এ, কখনো কোডে বা git-এ নয়। ডকুমেন্টেশন পড়ুন, pagination-এ next ধরে চলুন, rate limit মানুন। async দরকার হলে httpx, কিছু install না করা গেলে urllib.
  5. scraping শেষ উপায়: আগে API, তারপর robots.txt ও শর্তাবলি, সৎ User-Agent, ধীর গতি, cache, ব্যক্তিগত তথ্য নয়। HTML পড়ুন BeautifulSoup দিয়ে (regex নয়), r.content দিয়ে encoding ঠিক রাখুন, ফল রাখুন CSV/JSON-এ।

পরের পর্বে: এতক্ষণ আপনি ছিলেন client — অন্যের server-এর কাছে চাইছিলেন। পরের পর্বে দাঁড়াবেন অন্য পাশে: FastAPI দিয়ে নিজের API বানাবেন, যেখানে আপনি ঠিক করবেন কোন URL কী ফেরত দেবে, কখন 404 আর কখন 201 — আর আজকের requests আর httpx দিয়েই সেটি পরীক্ষা করবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান