مقدمه
در وب اسکریپینگ، انتخاب یک HTML parser مناسب میتواند تفاوت بزرگی در سرعت، مصرف حافظه و سهولت توسعه ایجاد کند. این مقاله بر اساس مقایسهٔ پنج کتابخانهٔ محبوب Node.js — Cheerio، jsdom، parse5، htmlparser2 و xml2js — نوشته شده و برای توسعهدهندگان پایتون سطح متوسط طراحی شده تا با مفاهیم، معادلهای پایتونی و نکات عملی آشنا شوند. در پایان خواهید دانست هر کتابخانه برای چه سناریویی مناسب است، چگونه نمونهٔ پایهای را پیادهسازی کنید و چه نکات عملکردی و امنیتی را رعایت کنید.
مروری کلی و معیارهای انتخاب
وقتی parser را انتخاب میکنیم، چند معیار کلیدی را در نظر بگیرید:
- پشتیبانی از اجرای JavaScript (برای صفحات داینامیک)
- سرعت و مصرف حافظه (برای صفحات بزرگ یا حجم بالا)
- سهولت استفاده و API (CSS selector vs event-based)
- پشتیبانی از استریمینگ و پردازش بخشبهبخش
- سازگاری با استانداردهای HTML/W3C
در ادامه هر کتابخانه را توضیح میدهیم، مثالهای Node.js را بازنویسی میکنیم و برای هر مورد معادل یا روشهای معمول در پایتون را نشان میدهیم.
Cheerio
ایدهٔ کلی: Cheerio یک کتابخانهٔ سبک و سریع است که سینتکس آن شبیه jQuery است؛ مناسب برای استخراج سریع دادهها از HTML ایستا (بدون نیاز به اجرای JavaScript).
نمونهٔ Node.js (پاکشده و ساده):
const rp = require('request-promise');
const cheerio = require('cheerio');
rp('https://quotes.toscrape.com/')
.then(html => {
const $ = cheerio.load(html);
$('.quote .text').each((i, el) => {
const quote = $(el).text().trim();
console.log(`Quote ${i+1}: ${quote}`);
});
})
.catch(err => console.error(err));توضیح:
- ورودی: URL (یا HTML خام)
- خروجی: لیست متن عناصر انتخابشده
- نقش هر بخش: request-promise HTML را میگیرد؛ cheerio.load آن را پارس میکند؛ انتخابگر CSS عناصر هدف را برمیگرداند.
- خطبهخط: درخواست HTTP → بارگذاری در Cheerio → پیدا کردن المانها با انتخابگر → خواندن متن و trim → چاپ.
معادل پایتون (requests + BeautifulSoup):
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://quotes.toscrape.com/')
soup = BeautifulSoup(resp.text, 'html.parser')
for i, el in enumerate(soup.select('.quote .text')):
print(f"Quote {i+1}: {el.get_text(strip=True)}")نکات عملی و محدودیتها:
- مزایا: سبک، سریع برای HTML ایستا، API ساده بر پایهٔ CSS selector.
- معایب: اجرای JavaScript را ندارد؛ مناسب نیست برای محتوای داینامیک که توسط JS ساخته میشود.
- Best practice: قبل از پارس، HTML را با ابزارهای حذفِ whitespace و نرمالایز کردن کاراکترها آماده کنید تا نتایج selector پایدار باشند.
JSDOM
ایدهٔ کلی: jsdom محیطی شبیه مرورگر ایجاد میکند که DOM کامل و APIهای مرورگر را فراهم میکند و میتواند برای سناریوهایی که نیاز به اجرای JavaScript یا تعامل DOM پیچیده دارند، مفید باشد.
نمونهٔ Node.js:
const { JSDOM } = require('jsdom');
const rp = require('request-promise');
rp('https://quotes.toscrape.com/')
.then(html => {
const dom = new JSDOM(html);
const document = dom.window.document;
const quotes = document.querySelectorAll('.quote .text');
quotes.forEach((q, i) => console.log(`Quote ${i+1}: ${q.textContent.trim()}`));
})
.catch(err => console.error(err));توضیح:
- ورودی: HTML یا URL
- خروجی: دسترسی DOM (document) با متدهای استاندارد مانند querySelectorAll
- مزیت اصلی: شبیهسازی رفتار مرورگر در سطح DOM، مناسب برای تست، SSR یا صفحات با نیاز به تعامل DOM.
معادل پایتون برای محتوای داینامیک: معمولاً از ابزارهایی مانند Playwright یا Selenium استفاده میکنیم تا صفحه را رندر و سپس HTML را خوانده و با BeautifulSoup/ lxml پارس کنیم. مثال کوتاه با Playwright (پایتون):
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://quotes.toscrape.com/')
html = page.content()
soup = BeautifulSoup(html, 'html.parser')
for i, el in enumerate(soup.select('.quote .text')):
print(f"Quote {i+1}: {el.get_text(strip=True)}")
browser.close()نکات عملی و محدودیتها:
- مزایا: امکان اجرای JS و دسترسی به APIهای window/document.
- معایب: مصرف منابع بیشتر و پیچیدگی نصب/پیکربندی، کندتر از پارسرهای سبک.
- رفتن سمت jsdom وقتی منطقی است که تعامل با DOM یا اجرای اسکریپتها ضروری باشد؛ در غیر این صورت Cheerio یا Parse5 سریعتر و بهینهترند.
Parse5
ایدهٔ کلی: parse5 یک پارسر سطح پایین و W3C-compliant است که برای کارایی و حافظه بهینه طراحی شده و برای پردازشهای بزرگ یا نیاز به تطابق با استانداردها مناسب است.
نمونهٔ Node.js (استراتژی: parse + traverse):
const rp = require('request-promise');
const parse5 = require('parse5');
rp('https://quotes.toscrape.com/')
.then(html => {
const document = parse5.parse(html);
const quotes = [];
function walk(node) {
if (node.tagName === 'span' && node.attrs && node.attrs.find(a => a.name === 'class' && a.value === 'text')) {
const textNode = node.childNodes && node.childNodes[0];
if (textNode && textNode.value) quotes.push(textNode.value.trim());
}
if (node.childNodes) node.childNodes.forEach(walk);
}
walk(document);
quotes.forEach((q, i) => console.log(`Quote ${i+1}: ${q}`));
})
.catch(err => console.error(err));توضیح:
- Parse5 مستقیماً درختی تولید میکند که باید با آن پیمایش (traverse) شود؛ لذا API پایینتری نسبت به Cheerio دارد.
- مناسب برای ابزارهایی که نیاز به اعتبارسنجی/تطابق با استاندارد دارند یا پردازش حجم بالای HTML.
معادل پایتون: برای پیروی از استاندارد HTML5 میتوان از html5lib همراه با lxml یا BeautifulSoup استفاده کرد؛ برای پردازشهای سطح پایینتر از lxml.etree استفاده میشود. مثال با html5lib+lxml:
from lxml import html
import requests
resp = requests.get('https://quotes.toscrape.com/')
doc = html.fromstring(resp.content) # lxml سازگار با استانداردها و سریع
quotes = doc.cssselect('.quote .text')
for i, el in enumerate(quotes):
print(f"Quote {i+1}: {el.text_content().strip()}")نکات عملی:
- Parse5 برای زمانی مناسب است که میخواهید دقیقاً رفتار پارسینگ مرورگر را تقلید کنید و مصرف حافظه برایتان مهم است.
- وقتی نیاز به selector ساده دارید، استفاده از Cheerio یا سطح بالاتری راحتتر است.
htmlparser2
ایدهٔ کلی: htmlparser2 یک پارسر مبتنی بر SAX/event است که برای پردازش استریمها و فایلهای بزرگ مناسب است؛ به جای ساختن کل درخت DOM، رویدادها را هنگام خواندن HTML منتشر میکند.
نمونهٔ Node.js (event-driven):
const rp = require('request-promise');
const { Parser } = require('htmlparser2');
rp('https://quotes.toscrape.com/')
.then(html => {
const quotes = [];
const parser = new Parser({
onopentag(name, attrs) {
if (name === 'span' && attrs.class === 'text') this._collect = '';
},
ontext(text) {
if (this._collect !== undefined) this._collect += text.trim();
},
onclosetag(name) {
if (name === 'span' && this._collect !== undefined) {
quotes.push(this._collect);
this._collect = undefined;
}
}
});
parser.write(html);
parser.end();
quotes.forEach((q, i) => console.log(`Quote ${i+1}: ${q}`));
})
.catch(err => console.error(err));توضیح:
- استفاده از callbackها برای پردازش شروع/متن/پایان تگها
- مزیت اصلی: کمحافظه بودن و مناسب برای استریمینگ و پردازش تدریجی
معادل پایتون برای استریمینگ و پردازش تدریجی:
import requests
from lxml import etree
# مثال: استفاده از iterparse برای فایل HTML بزرگ (باید HTML به XML تبدیل یا پاکسازی شود)
resp = requests.get('https://example.com/large.html', stream=True)
context = etree.iterparse(resp.raw, html=True, events=('end',), tag='span')
for event, elem in context:
if 'text' in (elem.get('class') or ''):
print(elem.text_content().strip())
elem.clear() # آزاد کردن حافظهنکات عملی:
- htmlparser2 برای استخراجِ خطبهخط یا لاگمانند مفید است؛ در پایتون iterparse یا SAX-based parsers همان نقش را دارند.
- اگر نیاز به پردازش هزاران صفحه یا اسناد بسیار بزرگ دارید، از مدل استریمینگ استفاده کنید تا مصرف حافظه محدود بماند.
xml2js
ایدهٔ کلی: xml2js کتابخانهای برای تبدیل XML به اشیاء جاوااسکریپت است؛ برای HTML معمولی توصیه نمیشود مگر اینکه HTML ورودی دقیقاً XML-شکل (XHTML) باشد یا با APIهای XML کار میکنید.
نمونهٔ Node.js:
const rp = require('request-promise');
const { parseString } = require('xml2js');
rp('https://quotes-api.example.com/api/quotes')
.then(xml => {
parseString(xml, (err, result) => {
if (err) return console.error(err);
const quotes = result.quotes.quote || [];
quotes.forEach((q, i) => console.log(`Quote ${i+1}: ${q}`));
});
})
.catch(err => console.error(err));معادل پایتون: از xml.etree.ElementTree یا xmltodict استفاده میشود:
import requests
import xmltodict
resp = requests.get('https://quotes-api.example.com/api/quotes')
data = xmltodict.parse(resp.text)
quotes = data.get('quotes', {}).get('quote', [])
for i, q in enumerate(quotes):
print(f"Quote {i+1}: {q}")نکات عملی:
- xml2js برای APIهای مبتنی بر XML عالی است؛ برای HTML معمولی احتمالاً ابزارهای HTML-specific بهتر عمل میکنند.
- اگر منبع شما XHTML یا XML-styled است، xml2js و xmltodict پردازش را بسیار ساده میکنند.
مقایسهٔ جامع و موارد تصمیمگیری
خلاصهٔ نقاط قوت هر ابزار:
- Cheerio: سریع، کمحجم، API شبیه jQuery — مناسب استخراج سریع از HTML ایستا.
- JSDOM: محیط مرورگر-مانند، اجرای JS — مناسب صفحات داینامیک و تست.
- Parse5: پارسینگ W3C-compliant، کارایی بالا — مناسب نیازهای استاندارد محور و حجم بالا.
- htmlparser2: مدل event-based و استریمینگ — مناسب پردازش اسناد بزرگ یا جریانهای HTML.
- xml2js: تبدیل XML به شیء — مناسب APIهای XML/XHTML.
راهنمای تصمیمگیری سریع:
- صفحه ایستا و نیاز به سرعت: Cheerio (یا در پایتون BeautifulSoup/lxml)
- صفحه داینامیک که JS تولید محتوا میکند: JSDOM یا ابزار headless (Playwright/Selenium) و سپس پارس
- پردازش صفحات بسیار بزرگ یا استریم: htmlparser2 یا رویکردهای SAX / iterparse در پایتون
- لزوم تطابق با استاندارد HTML5: Parse5 یا html5lib در پایتون
- منابع XML: xml2js یا xmltodict/xml.etree
بهینهسازی، امنیت و پایداری
نکات مهم هنگام اسکریپ کردن (وب اسکریپینگ):
- مدیریت خطا: همیشه پاسخهای غیر 200، timeout و محتوای ناقص را مدیریت کنید. در Node.js از try/catch یا .catch و در پایتون از استثناءها استفاده کنید.
- retry و backoff: برای درخواستها از مکانیزم retry با exponential backoff استفاده کنید تا از بلاک شدن یا spike جلوگیری شود.
- استفاده از جلسات (sessions): در پایتون از requests.Session و در Node.js از pool های HTTP برای نگه داشتن کانکشنها استفاده کنید تا overhead کاهش یابد.
- همزمانی/موازیسازی: برای مقیاسپذیری از concurrency کنترلشده استفاده کنید (asyncio/aiohttp در پایتون یا Promise.all محدودشده در Node.js). جلوگیری از ارسال تعداد زیاد درخواست همزمان که منجر به блок شدن میشود.
- پروکسی و ریتلیمیت: در صورت نیاز از پروکسی و محدودیت سرعت (rate limiting) استفاده کنید و headerهای معقول ارسال کنید (User-Agent). رعایت قوانین سایت و نرخ مجاز درخواستها.
- امنیت: محتویات HTML ممکن است شامل اسکریپت یا payload مخرب باشد؛ هر دادهٔ ورودی را قبل از پردازش یا ذخیره نهایی validate و sanitize کنید.
نمونهٔ الگوی کار (پایتون) با retry و async برای استخراج مقیاسپذیر
import asyncio
import aiohttp
from lxml import html
from tenacity import AsyncRetrying, stop_after_attempt, wait_exponential
async def fetch(session, url):
async for attempt in AsyncRetrying(stop=stop_after_attempt(3), wait=wait_exponential()):
with attempt:
async with session.get(url, timeout=10) as resp:
resp.raise_for_status()
return await resp.text()
async def parse_quotes(html_text):
doc = html.fromstring(html_text)
return [el.text_content().strip() for el in doc.cssselect('.quote .text')]
async def worker(url):
async with aiohttp.ClientSession() as session:
html_text = await fetch(session, url)
quotes = await asyncio.get_event_loop().run_in_executor(None, parse_quotes, html_text)
for q in quotes:
print(q)
asyncio.run(worker('https://quotes.toscrape.com/'))این الگو نشان میدهد چطور از async برای concurrency کنترلشده، از tenacity برای retry و از lxml برای پردازش سریع HTML استفاده کنید.
جمعبندی
برای انتخاب parser مناسب ابتدا سناریوی خود را مشخص کنید: آیا صفحه داینامیک است؟ حجم صفحات چقدر است؟ آیا نیاز به استریم دارید؟ در عمل:
- اگر به سرعت و سادگی نیاز دارید و محتوا ایستا است، Cheerio (Node.js) یا BeautifulSoup/lxml (پایتون) را انتخاب کنید.
- برای صفحات داینامیک یا نیاز به اجرای JS از JSDOM یا ابزارهای headless مانند Playwright/Selenium استفاده کنید.
- برای پردازش اسناد بزرگ یا استریمینگ سمت htmlparser2 یا رویکردهای SAX/iterparse در پایتون بروید.
در نهایت، ترکیبی از ابزارها معمولاً بهترین نتیجه را میدهد: رندر با headless در صورت نیاز، سپس پارس و استخراج با ابزار سبکتر برای افزایش کارایی. همیشه نکات مربوط به retry، محدودسازی نرخ، و مدیریت حافظه را در پیادهسازی تولیدی رعایت کنید.





