وب اِسکرِپینگ، تکنیکی است که در آن، از رباتها به منظور استخراج محتوا و دادههای وبسایتی دلخواه، استفاده میشود. این کار به صورت دستی نیز قابل انجام است، اما خودکار کردن این فرایند، علاوه بر سرعت بیشتر و بهینگی، خطای کمتری را به دنبال دارد. عملکرد وب اِسکرِپینگ به گونهای است که کدهای HTML نهفته در صفحات وب را بیرون میکِشد و دادههای موجود در این صفحات، که ممکن است ساختاری ضعیف و غیرجدولی داشته باشند را بهصورت قالبی ساختاریافته در میآورد که برای کاربردهای گوناگون، مناسبتر خواهد بود. وب اسکرپینگ با سی شارپ فرایندی فراتر از جمعآوری دادهها است و میتواند در بایگانی دادهها و ردیابی آنلاین تغییرات آنها نیز به ما کمک کند. در این مطلب، به بیانی ساده و بهصورت گام به گام، آموزش وب اسکرپینگ با سی شارپ را با استفاده از کتابخانههایی، نظیر «سلنیوم» (Selenium) و Html Agility Pack، خواهیم داشت و با ایجاد یک خزندهوب، دادههای مورد نظر را از یک وب سایت نمونه استخراج میکنیم. همچنین مفاهیم پیشرفتهتری مانند اسکرپینگ موازی، اسکرپینگ وبسایتهای پویا و راهکارهایی به منظور پرهیز از بلاک شدن را نیز بررسی خواهیم کرد.
آشنایی با وب اسکرپینگ
وب اسکرپینگ، فرایند استخراج داده از وبسایتها با استفاده از ابزارها و نرمافزارهای خودکار است. در دنیای پُرشتاب و دادهمحور امروز، وب اِسکرِپینگ به ابزاری ضروری برای کسب و کارها، محققان و تحلیلگران تبدیل شده است تا دیدگاههای ارزشمندی را از دادههای حجیم موجود در اینترنت، بیرون بِکشند.
تحلیلهای صورت گرفته با استفاده از دادههای Scrape شده، در مقاصد مختلفی مانند شناسایی مشتریان بالقوه، اطلاع از روند قیمتگذاری در بازار، تحلیل احساسات مشتری و دهها مورد دیگر قابل استفاده هستند. Web Scraping به طور روز افزون توسط محققان، به منظور ایجاد «مجموعه دادهها» (Data Sets)، برای پروژههایی مانند «متنکاوی» (Text Mining) و غیره استفاده میشود. این دادهها، ممکن است مجموعهای از مقالات ژورنالی یا متون دیجیتالی باشند.
سی شارپ (و به طور کلی دات نت)، تمامیابزارها و کتابخانههای لازم برای پیادهسازی وب اِسکرِپینگ را فراهم و به شما کمک میکند تا به سرعت پروژه Scraper خود را پیادهسازی و دادههای مورد نظر را جمعآوری کنید.
یکی از موضوعاتی که در این نوشتار به آن پرداخته شده است، تکنیکهای مختلف برای پیشگیری از «انسداد» (Block شدن) و همینطور محدود شدن توسط سرور است. این قضیه، به عنوان یکی از موانع اصلی در مبحث وب اِسکرِپینگ، بهشمار میرود.
امروزه، شرکتهای زیادی هستند که از مزایای دادههای جمعآوری شده از وب، بهره میگیرند. سی شارپ، یکی از مناسبترین زبانهای برنامهنویسی برای پیاده سازی Web Scraping است.
کتابخانه Html Agility Pack، محبوبترین کتابخانه Scraper برای سی شارپ است. با استفاده از این کتابخانه، میتوانید صفحات وب مورد نظر را دانلود و محتوای HTML آنها را تجزیه کنید، به طوریکه، پس از انتخاب عناصر HTML، بتوان دادههای مورد نیاز را از آنها استخراج کرد.
Selenium WebDriver، کتابخانهی دیگری است که از چندین زبان برنامهنویسی پشتیبانی میکند و به شما امکان میدهد تستهای خودکار برای وب اپلیکیشن بنویسید. همچنین میتوانید از آن برای اهداف وب اسکرپینگ نیز استفاده کنید.
پیش نیازهای Web Scraping با C#
در این بخش به معرفی و شرح پیشنیازهایی پرداخته شده است که برای وب اسکریپینگ با سی شارپ مورد نیاز هستند. این موارد در زیربخشهایی جداگانه شامل دانلود و راهاندازی محیط و ابزارهای اولیه و همچنین ایجاد پروژه در سی شارپ، ارائه شده است.
تنظیم و راهاندازی محیط برنامه نویسی وب اسکرپینگ با سی شارپ
به منظور پیادهسازی وب اِسکرِپینگ با سی شارپ، لازم است تا پیش نیازهای زیر را فراهم کنیم:
- دانت نت نسخه ۷ (یا بالاتر): توصیه میشود که از جدیدترین نسخه دانت نت استفاده شود.
- محیط IDE برای کد نویسی C#: ویژوال استدیو کامیونیتی ۲۰۲۲، میتواند گزینه مناسبی برای Web Scraping با سی شارپ باشد. همچنین اگر گزینه کمحجمتری را ترجیح میدهید، میتوانید محیط ویژوال استدیو کد، به همراه افزونه C# را انتخاب کنید.
برای صرفه جویی در زمان، بهتر است بسته دات نت Coding Pack را دانلود و نصب کنید. با نصب این بسته، «ویژوال استودیو کد» (VSCode) به همراه افزونههای ضروری دات نت SDK را خواهید داشت. در غیر این صورت، میتوانید ابزارهای مورد نیاز را به صورت جداگانه، دانلود و نصب کنید.
اکنون، همه چیز برای Web Scraping با سی شارپ آماده است ولی برای اطمینان از اینکه دات نت را به درستی نصب کردهایم، میبایست پنجرهی پاورشل را باز کرده و دستورات زیر را در آن اجرا کنیم.
dotnet --list-sdks
در صورتی که مشکلی وجود نداشته باشد، شماره نسخه دات نت SDK که روی سیستم نصب شده است، در خروجی چاپ میشود (ما در اینجا نسخه ۷٫۰٫۱۰۱ از دات نت SDK را برای وب اسکرپینگ با سی شارپ نصب کردیم).
در صورتی که بجای دستور بالا، پیغامیبا محتوای زیر نمایش داده شد،
'dotnet' is not recognized as an internal or external command error
نیاز است که سیستم را ریستارت و مراحل فوق را دوباره امتحان کنیم. در صورت مشاهده مجدد این خطا، بهتر است که دات نت از روی سیستم حذف و دوباره نصب شود.
راه اندازی پروژه سی شارپ
یک «اپلیکیشن کنسول» (Console Application) را در ویژوال استدیو ایجاد میکنیم. به این صورت که ابتدا، یک پوشه خالی به نام SimpleWebScraper
، برای پروژهی Web Scraping با سی شارپ میسازیم.
mkdir SimpleWebScraper
حالا، VSCode را باز میکنیم و پس از کلیک روی منوی File
، گزینه Open Folder
را انتخاب میکنیم (معادل کلید ترکیبی Ctrl+K Ctrl O).
در پنجره باز شده، پوشه SimpleWebScraper
را انتخاب و صبر میکنیم تا ویژوال استدیو آن را باز کند. سپس با انتخاب گزینه Terminal
از منوی View
که در نوار منوی اصلی وجود دارد، پنجره Terminal را نیز به ویژوال استدیو اضافه میکنیم.
در پنجره Terminal
، دستور زیر را اجرا میکنیم:
dotnet new console --framework net7.0
دستور بالا، یک پروژه کنسول .NET 7.0 را راهاندازی میکند. به بیان دقیقتر، یک فایل پروژه با فرمت .csproj
و یک فایل C# به نام Program.cs
ایجاد میشود. حالا، کد زیر را با محتویات موجود در Program.cs
، جایگزین میکنیم.
namespace SimpleWebScraper
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
// scraping logic...
}
}
}
این کد، نمونهای از یک اسکریپت ساده کنسول در سی شارپ است. توجه داشته باشیم که تابع Main()
، شامل منطق وب اِسکرِپینگ با سی شارپ خواهد بود. با دستور زیر ، میتوانیم اسکریپت را اجرا کنیم:
dotnet run
که باید خروجی زیر را چاپ کند:
"Hello, World!"
بسیار عالی، اسکریپت اولیه C#، همانطوری که انتظار میرفت بدون مشکل کار میکند و ما با خیال راحت میتوانیم مقدمات یادگیری Web Scraping با سی شارپ را ادامه دهیم.
چگونه اطلاعات یک وبسایت را جمع آوری کنیم؟
در این قسمت، وب اسکرپینگ با سی شارپ را در قالب ساخت برنامهای برای استخراج دادهها از وبسایت ScrapeMe یاد میگیریم. وبسایت ScrapeMe، فهرستی از عناصر الهام گرفته از شخصت کارتونی Pokemon را در قالب چندین صفحه به نمایش میگذارد. اسکریپت C#، به طور خودکار، داده مربوط به هر محصول را بازدید و استخراج میکند.
در تصویر زیر، نمایی از وبسایت ScrapeMe، نشان داده شده است.
حالا وقت آن رسیده که برخی از وابستگیها را نصب و استخراج داده از وب را شروع کنیم.
گام اول: نصب کتابخانه Html Agility Pack و افزونه CSS Selector مربوط به آن
Html Agility Pack (که به اختصار HAP هم نامیده میشود)، یکی از کتابخانههای قدرتمند و منبع باز .NET برای تجزیه اسناد HTML است. این کتابخانه که API منعطفی برای وب اِسکرِپینگ محسوب میشود، به ما اجازه میدهد تا صفحه HTML مورد نظر را دانلود و تجزیه کنیم. همچنین میتوانیم عناصر HTML را انتخاب کرده و دادهها را از آنها بیرون بکشیم. کتابخانه Html Agility Pack را از طریق برنامه مدیر بسته NuGet، با استفاده از دستور زیر نصب میکنیم.
dotnet add package HtmlAgilityPack
با اینکه Html Agility Pack به طور ذاتی از XPath و XSLT پشتیبانی میکند، اما اینها روشهای محبوبی برای انتخاب عناصر HTML از DOM محسوب نمیشوند. خوشبختانه، افزونهای به نام CSS Selector وجود دارد که محبوبیت بیشتری دارد. این افزونه از طریق کتابخانه HtmlAgilityPack.CssSelectors
از NuGet قابل نصب است.
dotnet add package HtmlAgilityPack.CssSelectors
با نصب این افزونه، HAP قادر خواهد بود تا سلکتورهای CSS را با روشهای توسعهیافتهای درک کند. سلکتورهای CSS، اولین بخش از یک دستور CSS هستند و به مرورگر میگویند که کدام عناصر از HTML باید انتخاب شوند تا مقادیر ویژگی CSS که در داخل دستور تعیین کردیم، روی آنها اعمال شوند.
حالا میبایست، کتابخانه Html Agility Pack را با افزودن خط زیر در ابتدای فایل Program.cs پروژه، به خزنده وب C#، وارد کنیم.
using HtmlAgilityPack;
اگر تا اینجای کار، ویژوال استدیو خطایی را گزارش نکرد، یعنی کار به درستی پیش رفته است.
گام دوم: بارگیری صفحه وب هدف
منظور از «صفحهوب هدف» (Target Web Page)، صفحهای است که قصد اِسکرِپینگ آن را داریم. با ایجاد شیای از Html Agility Pack شروع میکنیم.
var web = new HtmlWeb();
HtmlWeb
به ما امکان دسترسی به قابلیتهای وب اِسکرِپینگ ارائه شده توسط HAP را میدهد. سپس از متد Load()
مربوط به HtmlWeb
، برای دریافت HTML از URL استفاده میکنیم.
// loading the target web page
var document = web.Load("https://scrapeme.live/shop/");
در پشت صحنه، HAP یک درخواست HTTP از نوع GET
را به منظور دانلود صفحه وب و تجزیه محتوای HTML آن ارسال میکند. در صورت بروز خطا، یک HtmlAgilityPack.HtmlWebException
و در صورتی که همه چیز همانطور که انتظار میرود کار کند، یک شی HtmlDocument
HAP را ارائه میدهد. حالا آماده هستیم تا از HtmlDocument
برای استخراج داده از عناصر HTML استفاده کنیم. اما پیش از آن، اجازه بدهید که کد صفحه هدف را به منظور تعریف یک راهبرد موثر برای انتخاب عناصر HTML بررسی کنیم.
گام سوم: بررسی صفحه هدف
صفحه مورد نظر را برررسی میکنیم تا ببینیم ساختار آن چگونه است. این کار را با تگهای HTML هدف (گرههای مورد نظر) که عناصر محصول را تشکیل میدهند، شروع میکنیم. روی یکی از این تگها راست کلیک و با انتخاب گزینه Inspect به DevTools مرورگر، دسترسی پیدا میکنیم.
لازم به ذکر است که DevTools، بخشی از مرورگر است که ابزارها و امکاناتی را برای برنامه نویسان فراهم میکند.
در اینجا، یک تگ li.product
HTML را میبینید که متشکل از چهار مولفه زیر است:
- URL محصول در تگ a
- تصویر محصول در تگ img
- نام محصول در تگ h2
- قیمت محصول در تگ span
با کلاس .price
در صورت بررسی سایر محصولات، خواهید دید که آنها نیز از ساختار یکسانی پیروی میکنند. در حقیقت چیزی که تغییر میکند، مقادیر ذخیره شده در پشت عناصر HTML هستند و این یعنی میتوانیم همه آنها را با برنامهنویسی، Scrape کنیم. در ادامه آموزشِ Web Scraping با سی شارپ، چگونگی اِسکرِپ دادهها از این عناصر HTML محصول را با استفاده از ابزار HAP در C#، یاد میگیریم.
گام چهارم: استخراج داده از عناصر HTML
ما باید یک کلاس سفارشی C# را برای ذخیره دادههای Scrape شده تعریف کنیم. برای این منظور، یک کلاس PokemonProduct
را به شرح زیر، در داخل Program
برنامه خود مینویسیم:
public class PokemonProduct
{
public string? Url { get; set; }
public string? Image { get; set; }
public string? Name { get; set; }
public string? Price { get; set; }
}
کلاس فوق شامل فیلدهای Url
، Image
، Name
و Price
است. این فیلدها، مواردی هستند که قصد Scrape کردن آنها را از هر محصول داریم. حالا، فهرستی از PokemonProduct
را در تابع Main()
برنامه، مانند خط زیر مقداردهی میکنیم.
var pokemonProducts = new List();
عبارت فوق شامل، داده Scrape و ذخیره شده در «وهلههای» (Instances) PokemonProduct
است. حالا وقت آن فرا رسیده است تا با استفاده از HAP، فهرستی از همه عناصر HTML li.product
را از DOM بیرون بِکشیم، شبیه به کد زیر:
// selecting all HTML product elements from the current page
var productHTMLElements = document.DocumentNode.QuerySelectorAll("li.product");
متد QuerySelectorAll()
، این امکان را برایمان فراهم میکند تا گرههای HTML را از DOM با استفاده از سلکتور CSS، بازیابی کنیم. در متد فوق، استراتژی سلکتور CSS li.product
، برای به دست آوردن تمام عناصر محصول اعمال شده است. به طور خاص، QuerySelectorAll()
فهرستی از اشیای HtmlNode
HAP را بر میگرداند.
این نکته حائز اهمیت است که متد QuerySelectorAll()
به افزونه CSS Selector مربوط به HAP تعلق دارد. بنابراین نمیتوانیم آن را روی رابط اصلی Html Agility Pack پیدا کنیم. از یک حلقه foreach
، برای پیمایش روی لیست HTML و داده Scrape شده هر محصول، استفاده میکنیم.
// iterating over the list of product elements
foreach (var productHTMLElement in productHTMLElements)
{
// scraping the interesting data from the current HTML element
var url = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("a").Attributes["href"].Value);
var image = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("img").Attributes["src"].Value);
var name = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("h2").InnerText);
var price = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector(".price").InnerText);
// instancing a new PokemonProduct object
var pokemonProduct = new PokemonProduct() { Url = url, Image = image, Name = name, Price = price };
// adding the object containing the scraped data to the list
pokemonProducts.Add(pokemonProduct);
}
تا اینجای کار توانستیم منطق وب اسکرپینگ با سی شارپ را پیادهسازی کنیم. متد QuerySelector()
، سلکتور CSS را در گرههای فرزند HtmlNode
، بهمنظور دریافت یکی از آنها، اعمال میکند.
پس از آن، ما ویژگی HTML را از Attributes
انتخاب میکنیم و داده آن را با استفاده از Value
بیرون میکشیم. هر مقدار را نیز با استفاده از HtmlEntity.DeEntitize()
برای جایگزینی با موجودیتهای شناخته شده HTML، پوشش میدهیم. بهتر است دوباره یادآوری شود که QuerySelector()
، متعلق به افزونه CSS Selector است و در حالت عادی نمیتوانیم آن را در HAP بیابیم. اکنون زمان آن رسیده است که یاد بگیریم که چگونه دادههای اِسکرِپ شده را در قالبی خوانا، مانند CSV ذخیره کنیم.
گام پنجم: صدور دادههای اِسکرِپ شده در قالب CSV
ما میتوانیم دادههای اِسکرِپ شده را به وسیله توابع داخلی C# به قالب CSV تبدیل کنیم اما کتابخانه CsvHelper، انتخاب مناسبتری برای این کار است.
خوب است که بدانیم، قالب CSV، به ما اجازه میدهد تا دادهها را در قالبی جدول مانند، ذخیره کنیم. CsvHelper کتابخانهای سریع، منعطف و قابل اطمینان از دات نت است که برای خواندن و نوشتن فایلهای CSV مورد استفاده قرار میگیرد. برای نصب این کتابخانه، کافی است بسته ناگت CsvHelper
را با دستور زیر به وابستگیهای پروژه خود اضافه کنیم:
dotnet add package CsvHelper
با افزودن خط زیر به ابتدای فایل Program.cs
، این کتابخانه را به پروژه خود اضافه میکنیم:
using CsvHelper;
همانطور که در زیر مشاهده میشود، دادههای به دست آمده را به کمک CSVHelper، به یک فایل خروجی CSV تبدیل میکنیم:
// initializing the CSV output file
using (var writer = new StreamWriter("pokemon-products.csv"))
// initializing the CSV writer
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
// populating the CSV file
csv.WriteRecords(pokemonProducts);
}
در ابتدا، فایل pokemon-products.csv
ایجاد میشود. پس از آن، متد WriteRecords()
، تمام رکوردهای محصول را در فایل CSV مینویسد. به لطف ویژگی using
در سی شارپ، این اسکریپت به طور خوکار منابع مربوط به اشیا نوشتن را، آزاد میکند. توجه داشته باشید که «سازنده» (Constructor) به یک پارامتر CultureInfo
نیاز دارد. این پارامتر، مشخصات قالب بندی، کاراکترهای جداکننده و پایانِ خط را تعیین میکند. InvariantCulture
تضمین میکند که همه نرمافزارها میتوانند CSV تولید شده را بدون در نظر گرفتن تنظیمات محلی کاربر، تجزیه کنند.
برای استفاده از مقادیر CultureInfo
، نیاز است که دستور زیر را نیز اضافه کنیم.
using System.Globalization;
بسیار عالی. تنها چیزی که باقی میماند، اجرای وب اِسکرِپری است که با سی شارپ نوشتیم.
گام ششم: راهاندازی اِسکرِپر
محتوای فایل Program.cs
، در وب اِسکرِپری که تا کنون نوشتیم به صورت زیر است:
using HtmlAgilityPack;
using CsvHelper;
using System.Globalization;
namespace SimpleWebScraper
{
public class Program
{
// defining a custom class to store the scraped data
public class PokemonProduct
{
public string? Url { get; set; }
public string? Image { get; set; }
public string? Name { get; set; }
public string? Price { get; set; }
}
public static void Main()
{
// creating the list that will keep the scraped data
var pokemonProducts = new List();
// creating the HAP object
var web = new HtmlWeb();
// visiting the target web page
var document = web.Load("https://scrapeme.live/shop/");
// getting the list of HTML product nodes
var productHTMLElements = document.DocumentNode.QuerySelectorAll("li.product");
// iterating over the list of product HTML elements
foreach (var productHTMLElement in productHTMLElements)
{
// scraping logic
var url = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("a").Attributes["href"].Value);
var image = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("img").Attributes["src"].Value);
var name = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("h2").InnerText);
var price = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector(".price").InnerText);
var pokemonProduct = new PokemonProduct() { Url = url, Image = image, Name = name, Price = price };
pokemonProducts.Add(pokemonProduct);
}
// crating the CSV output file
using (var writer = new StreamWriter("pokemon-products.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
// populating the CSV file
csv.WriteRecords(pokemonProducts);
}
}
}
}
با دستور زیر، اسکریپت را اجرا میکنیم:
dotnet run
بسته به زمان پاسخگویی سرور صفحه مورد نظر، ممکن است کمیطول بکشد. پس از پایان این فرایند، میتوانیم فایل pokemon-products.csv
را در پوشه اصلی پروژه C# خود، پیدا کنیم. برای بررسی دادهها، آن را باز کنید.
همانطور که دیدیم، با نوشتن حدود 50 خط کُد توانستیم پروژه وب اسکرپینگ با سی شارپ را به طور کاملا کاربردی ایجاد کنیم.
وب اسکرپینگ پیشرفته در سی شارپ
Web Scraping با سی شارپ، فراتر از اصولی است که تاکنون دیدیم. در ادامه، تکنیکهای پیشرفتهتری را یاد خواهیم گرفت تا به ما برای تبدیل شدن به یک متخصص وب اسکرپینگ با سی شارپ، کمک کند.
Web Crawling با دات نت
فراموش نکنیم که وبسایت ScrapeMe، لیستی «صفحهبندی شده» (Pagination) از محصولات را نشان میدهد، به این معنی که وبسایت هدف، از چندین صفحه وب تشکیل شده است. برای اِسکرِپینگ همه محصولات، باید کل وبسایت بازدید شود، این همان چیزی است که «خزیدن در وب» (Web Crawling) به آن اشاره دارد.
برای انجام Web Crawling در سی شارپ، باید تمام لینکهای صفحهبندی را دنبال کنیم. عنصر HTML مربوط به صفحهبندی را بررسی میکنیم با این هدف که بفهمیم چگونه URL صفحات باید استخراج شوند. با راست کلیک روی شماره صفحه، گزینه Inspect را انتخاب میکنیم.
صفحهای مشابه تصویر زیر در DevTools مرورگر، مشاهده میشود.
در اینجا، باید توجه داشت که تمام عناصر HTML مربوط به صفحهبندی، کلاسِ CSS page-numbers
(شماره صفحه) را به اشتراک میگذارند. به طور دقیق تر، فقط گرههای HTML شامل URL هستند، در حالی که عناصر span
، «نگهدارنده مکان» (Palceholder) محسوب میشوند. بنابراین، میتوانیم تمام عناصر صفحه بندی را با a.page-numbers
سلکتور CSS، انتخاب کنیم.
مفاهیم مقدماتی CSS — آموزش CSS (بخش اول)
برای جلوگیری از اِسکرِپینگ مجدد یک صفحه، به چند ساختار داده اضافی نیاز داریم:
- pagesDiscovered
: لیستی که URLهای پیدا شده به وسیله Crawler را نگه میدارد.
- pagesToScrape
: صفی، شامل لیستی از صفحات، که برنامه Crawler به زودی قرار است آنها اِسکرِپ کند.
همچنین به یک متغیر limit
، که مانع از خزیدن همیشگی برنامه Crawler در صفحات میشود، نیاز داریم.
// the URL of the first pagination web page
var firstPageToScrape = "https://scrapeme.live/shop/page/1/";
// the list of pages discovered during the crawling task
var pagesDiscovered = new List { firstPageToScrape };
// the list of pages that remains to be scraped
var pagesToScrape = new Queue();
// initializing the list with firstPageToScrape
pagesToScrape.Enqueue(firstPageToScrape);
// current crawling iteration
int i = 1;
// the maximum number of pages to scrape before stopping
int limit = 5;
// until there are no pages to scrape or limit is hit
while (pagesToScrape.Count != 0 && i
کارهایی که «خزنده» (Crawler) فوق، انجام میدهد، به شرح زیر است:
- از اولین صفحه در لیست صفحهبندی شروع میکند.
- در صفحه جاری، به دنبال URL-های صفحهبندی جدید میگردد.
- URLهای کشف شده را به صف اِسکرِپینگ اضافه میکند.
- دادهها را از صفحه جاری اِسکرِپ میکند.
- چهار مرحله قبلی را برای هر صفحه موجود در صف، تکرار میکند. این تکرار تا زمانی ادامه مییابد که، یا عناصر وجود در صف تمام شوند (صف خالی شود) یا اینکه به اندازه محدودیت تعیین شده در limit
، از صفحات بازدید کرده باشد.
از آنجایی که وبسایت ScrapeMe از ۴۸ صفحه تشکیل شده است، برای اِسکرِپینگ دادهها از همه محصولات، limit
را روی ۴۸ تنظیم میکنیم. در این صورت، pokemon-product.csv
، به ازای هر یک از ۷۵۵ محصول موجود در وبسایت، رکوردی جداگانه خواهد داشت. تا اینجای کار یک برنامه وب اسکرپینگ با سی شارپ ساختهایم که میتواند یک وبسایت را بطور کامل Scrape کند.
پرهیز از بلاک شدن
ممکن است برنامهای که برای Web Scraping با سی شارپ نوشتیم، از کار بیفتد. این اتفاق به دلیل وجود چندین مکانیسم ضد اِسکرِپینگی است که ممکن است وبسایتها اتخاذ کنند. تکنیکهای ضد Scraping زیادی وجود دارند که اسکریپت ما برای مقابله با آنها باید آماده باشد.
ابتداییترین تکنیک، این است که درخواستهای HTTP را بر اساس مقدار «سرآمد» (Headers) آنها مسدود کنیم. این معمولاً زمانی اتفاق میافتد که درخواستها از مقدار User-Agent
نامعتبر استفاده میکنند.
سرآمد User-Agent
، حاوی اطلاعاتی است که منشأ درخواست را مشخص میکنند. به طور معمول، موارد قابل قبول و معتبر، به مرورگرها و سیستم عاملهای رایج اشاره میکنند. کتابخانههای اِسکرِپینگ تمایل دارند تا از placeholder-هایی از User-Agent
استفاده کنند که میتوانند به راحتی برنامه Scraper ما را آشکار کنند. میتوانیم یک User-Agent
معتبر را در Html Agility Pack با خط زیر به صورت سراسری ایجاد کنیم.
// setting a global User-Agent header in HAP
web.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36";
حالا، اینطور به نظر میرسد که تمام درخواستهای HTTP، که به وسیله HAP انجام شده است، از کروم ۱۰۹ میآیند.
وب اسکرپینگ موازی با سی شارپ
کارایی Web Scraping با سی شارپ، به سرعت وب سرورِ مورد نظر (وب سرور صفحات هدفی که قصد اسکرپ آن را داریم) بستگی دارد. با ایجاد درخواستهای موازی و اِسکرِپینگ صفحات به طور همزمان، با این مشکل مقابله میکنیم. همچنین ضمن پرهیز از اتلاف وقت، سرعت Scraper خود را به سطح بالاتری بهبود میدهیم. این همان مفهومی است که موازیسازی وب اسکرپینگ با سی شارپ، بیان میکند. برای این کار، لیست تمام صفحاتی که Crawler سی شارپی ما باید از آن بازدید کند را در یک ConcurrentBag:
ذخیره میکنیم:
var pagesToScrape = new ConcurrentBag {
"https://scrapeme.live/shop/page/1/",
"https://scrapeme.live/shop/page/2/",
"https://scrapeme.live/shop/page/3/",
// ...
"https://scrapeme.live/shop/page/47/",
"https://scrapeme.live/shop/page/48/"
};
List
در سی شارپ، Thread-Safe نیست و زمانی که صحبت از کارهای موازی میشود، نباید از آن استفاده کنیم. در این مواقع، List
را با ConcurrentBag
، که جایگزینی Thread-Safe و «نامرتب» (non-Order) برای آن است، تعویض میکنیم. Thread-Safe بودن قطعه کد، به این معنی است که نتایج حاصل، به دلیل تعامل همزمان چندین «رشته» (Thread) با کدها، اثرات نامطلوب (دادههای ناسازگار، استثنا و غیره) ایجاد نمیکنند. به همین دلیل، pokemonProducts
را به ConcurrentBag
تبدیل میکنیم:
var pokemonProducts = new ConcurrentBag();
بیایید Web Scraping موازی را پیادهسازی کنیم. از Parallel.forEach()
برای اجرای یک حلقه foreach
به صورت موازی در سی شارپ و همینطور Scraping چندین صفحه به طور همزمان، استفاده میکنیم:
// the import statement required to use Parallel.forEach()
using System.Collections.Concurrent;
// ...
Parallel.ForEach(
pagesToScrape,
// limiting the parallelization level to 4 pages at a time
new ParallelOptions { MaxDegreeOfParallelism = 4 },
currentPage => {
// visiting the current page of the loop
var currentDocument = web.Load(currentPage);
// complete scrapping logic
var productHTMLElements = currentDocument.DocumentNode.QuerySelectorAll("li.product");
foreach (var productHTMLElement in productHTMLElements)
{
var url = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("a").Attributes["href"].Value);
var image = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("img").Attributes["src"].Value);
var name = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("h2").InnerText);
var price = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector(".price").InnerText);
var pokemonProduct = new PokemonProduct() { Url = url, Image = image, Name = name, Price = price };
// storing the scraped product data in parallel
pokemonProducts.Add(pokemonProduct);
}
}
);
اکنون، برنامهای که برای وب اِسکرِپینگ با سی شارپ نوشتیم، بسیار سریعتر شده است. اما فراموش نکنیم که سطح موازیسازی را برای پرهیز از فشار زیاد به سرور، محدود کنیم. زیرا هدف ما استخراج دادهها از وبسایت و نه انجام یک حمله DoS است. کدهای بالا به عنوان مثالی برای درک چگونگی دستیابی به «Crawling موازی» در سی شارپ بودند. تمام کدهای برنامه Scraping موازی که با سی شارپ نوشتیم را در زیر مشاهده میکنید.
using HtmlAgilityPack;
using CsvHelper;
using System.Globalization;
using System.Collections.Concurrent;
namespace SimpleWebScraper
{
public class Program
{
public class PokemonProduct
{
public string? Url { get; set; }
public string? Image { get; set; }
public string? Name { get; set; }
public string? Price { get; set; }
}
public static void Main()
{
// initializing HAP
var web = new HtmlWeb();
// this can't be a List because it's not thread-safe
var pokemonProducts = new ConcurrentBag();
// the complete list of pages to scrape
var pagesToScrape = new ConcurrentBag {
"https://scrapeme.live/shop/page/1/",
"https://scrapeme.live/shop/page/2/",
// ...
"https://scrapeme.live/shop/page/48/"
};
// performing parallel web scraping
Parallel.ForEach(
pagesToScrape,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
currentPage =>
{
var currentDocument = web.Load(currentPage);
var productHTMLElements = currentDocument.DocumentNode.QuerySelectorAll("li.product");
foreach (var productHTMLElement in productHTMLElements)
{
var url = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("a").Attributes["href"].Value);
var image = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("img").Attributes["src"].Value);
var name = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector("h2").InnerText);
var price = HtmlEntity.DeEntitize(productHTMLElement.QuerySelector(".price").InnerText);
var pokemonProduct = new PokemonProduct() { Url = url, Image = image, Name = name, Price = price };
pokemonProducts.Add(pokemonProduct);
}
}
);
// exporting to CSV
using (var writer = new StreamWriter("pokemon-products.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(pokemonProducts);
}
}
}
}
Scraping وبسایت داینامیک با استفاده از مرورگر هِدلِس در سی شارپ
سایتهایی که محتوای استاتیک دارند، تمام محتوای خود را در صفحاتی از HTML جای دادهاند که توسط سرور بازگردانده شدهاست. این موضوع باعث میشود که سایتهای مذکور، به یک هدفِ آسان برای اِسکرِیپینگ، بهوسیله هر کتابخانه تجزیه HTML، تبدیل شوند.
وبسایتهای پویا، از زبان جاوا اسکریپت برای رِندر یا بازیابی دادهها استفاده میکنند. دلیل آن این است که این نوع وبسایتها برای بازیابی داینامیکِ تمام یا بخشی از محتوا به جاوا اسکریپت متکی هستند. اِسکرِپینگ چنین وبسایتهایی نیازمند ابزاری است که بتواند جاوا اسکریپت را مانند یک «مرورگر هِدلِس یا بیسر» (Headless Browser) اجرا کند.
اگر با این اصطلاح آشنا نیستید، باید بگوییم که مرورگر هِدلِس، مرورگری قابل برنامهنویسی و بدون «رابط کاربری گرافیکی» (GUI) است. پُر استفادهترین کتابخانه برای مرورگر هِدلِس در سی شارپ، با بیش از 65 میلیون دانلود، «سلنیوم» (Selenium) نام دارد. برای استفاده از آن، بسته ناگت Selenium.WebDriver را طبق دستور زیر نصب میکنیم:
dotnet add package Selenium.WebDriver
از سلنیوم در حالت هِدلِس برای اِسکرِپینگ دادهها از وبسایت ScrapeMe، با منطق زیر استفاده میکنیم:
using CsvHelper;
using System.Globalization;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
namespace SimpleWebScraper
{
public class Program
{
public class PokemonProduct
{
public string? Url { get; set; }
public string? Image { get; set; }
public string? Name { get; set; }
public string? Price { get; set; }
}
public static void Main()
{
var pokemonProducts = new List();
// to open Chrome in headless mode
var chromeOptions = new ChromeOptions();
chromeOptions.AddArguments("headless");
// starting a Selenium instance
using (var driver = new ChromeDriver(chromeOptions))
{
// navigating to the target page in the browser
driver.Navigate().GoToUrl("https://scrapeme.live/shop/");
// getting the HTML product elements
var productHTMLElements = driver.FindElements(By.CssSelector("li.product"));
// iterating over them to scrape the data of interest
foreach (var productHTMLElement in productHTMLElements)
{
// scraping logic
var url = productHTMLElement.FindElement(By.CssSelector("a")).GetAttribute("href");
var image = productHTMLElement.FindElement(By.CssSelector("img")).GetAttribute("src");
var name = productHTMLElement.FindElement(By.CssSelector("h2")).Text;
var price = productHTMLElement.FindElement(By.CssSelector(".price")).Text;
var pokemonProduct = new PokemonProduct() { Url = url, Image = image, Name = name, Price = price };
pokemonProducts.Add(pokemonProduct);
}
}
// export logic
using (var writer = new StreamWriter("pokemon-products.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(pokemonProducts);
}
}
}
}
تابع FindElements()
سلنیوم، به مرورگر، این امکان را میدهد تا گرههای HTML را جستجو کند. به لطف وجود این تابع، میتوانیم عناصر HTML محصول را از طریق پرس و جوی سلکتورِ CSS، انتخاب و پس از آن، عملیات پیمایش را روی آنها با یک حلقه foreach
انجام دهیم. سپس متد GetAttribute()
را اِعمال و از Text
برای استخراج دادههای مورد نظر استفاده میکنیم.
Web Scraping با سی شارپ، چه از طریق HAP انجام و چه با سلنیوم پیادهسازی شود، کدنویسی تقریباً یکسانی دارد و تفاوت آن، در نحوه اجرای منطق اِسکرِپینگ است. HAP، صفحات HTML را به منظور استخراج دادهها از آن تجزیه میکند و سلنیوم دستورات اِسکرِپینگ را در یک مرورگر هِدلِس اجرا میکند.
به لطف وجود سلنیوم، میتوانیم وبسایتهایی با محتوای پویا را Crawl کنیم و مانند یک کاربر واقعی در مرورگر، با صفحات وب، تعامل داشته باشیم. همچنین کُد ما احتمالاً به عنوان یک ربات شناسایی میشود. در نتیجه، با استفاده از سلنیوم، Scraping صفحه وب با پرهیز از انسداد، آسانتر میشود.
بسته Html Agility Pack، تمامیقابلیتهای مرورگر را به همراه ندارد، بنابراین، از HAP فقط میتوانیم به منظور اِسکرِیپینگِ وبسایتهای ایستا استفاده کنیم. همچنین مانند سلنیوم، سربار اضافی منابع، برای اجرای یک مرورگر معمولی را به همراه ندارد.
کد نهایی Web Scraping با سی شارپ
در این بخش، کد کامل Web Scraping با سی شارپ، به همراه Crawling و منطق ضدبلاک اولیه، که با استفاده از بسته Html Agility Pack ساختیم، ارائه شده است.
using HtmlAgilityPack;
using System.Globalization;
using CsvHelper;
using System.Collections.Concurrent;
namespace SimpleWebScraper
{
public class Program
{
// defining a custom class to store
// the scraped data
public class PokemonProduct
{
public string? Url { get; set; }
public string? Image { get; set; }
public string? Name { get; set; }
public string? Price { get; set; }
}
public static void Main()
{
// initializing HAP
var web = new HtmlWeb();
// setting a global User-Agent header
web.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36";
// creating the list that will keep the scraped data
var pokemonProducts = new List();
// the URL of the first pagination web page
var firstPageToScrape = "https://scrapeme.live/shop/page/1/";
// the list of pages discovered during the crawling task
var pagesDiscovered = new List { firstPageToScrape };
// the list of pages that remains to be scraped
var pagesToScrape = new Queue();
// initializing the list with firstPageToScrape
pagesToScrape.Enqueue(firstPageToScrape);
// current crawling iteration
int i = 1;
// the maximum number of pages to scrape before stopping
int limit = 5;
// until there is a page to scrape or limit is hit
while (pagesToScrape.Count != 0 && i
در نتیجه، با نوشتن کمتر از 100 خط کد توانستیم، برنامه Web Scraping با سی شارپ را پیادهسازی کنیم
سایر کتابخانه های وب اسکرپینگ با سی شارپ کدامند؟
در این بخش، ابزارهای دیگری که در Web Scraping با سی شارپ میتوانند مورد استفاده قرار بگیرند، به شرح زیر معرفی شده است:
- ZenRows: یک API با امکانات فراوان و قابلیت استفاده آسان برای تسهیل فرایند استخراج دادهها از صفحات وب است. ZenRows میانبُری خودکار برای هر سیستم ضد ربات یا ضد اِسکرِپینگ فراهم میکند. علاوه بر این، به همراه پراکسی، مرورگر هِدلِس کاربردی و تضمین آپتایم ۹۹ درصدی ارائه میشود.
- Puppeteer Sharp: کتابخانه الهام گرفته شده دات نت از Puppeteer در Node.js است. با استفاده از آن، میتوانیم یک مرورگر کرومیوم هِدلِس را به منظور تست و اِسکرِپینگ هدایت کنیم.
- AngleSharp: کتابخانه اپن سورسی از دات نت، با هدف تجزیه و دستکاری اسناد XML و HTML است. این کتابخانه به ما امکان میدهد تا دادهها را از یک وبسایت استخراج و عناصر HTML را از طریق سلکتورهای CSS، انتخاب کنیم.
- HttpClient: محبوبترین کلاینت HTTP سی شارپ است و در زمینه وب اسکرپینگ نیز کاربرد دارد. زیرا به شما امکان میدهد درخواستهای HTTP را به راحتی و به صورت «ناهمزمان» (Asynchronously) انجام دهید.
جمعبندی
در این نوشتار، اطلاعات مربوط به یادگیری وب اسکرپینگ با سی شارپ تا حد زیادی پوشش و آموزش داده شد. ابتدا اصول و مفاهیم اولیه Web Scraping را یاد گرفتیم، سپس کار را با پیادهسازی مفاهیم پیشرفته Web Scraping با سی شارپ ادامه دادیم.
به طور خلاصه، اکنون میدانیم:
- چگونه با استفاده از بسته Html Agility Pack، اِسکرِپینگ مقدماتی وب را در سی شارپ انجام دهیم.
- چگونه کل یک وبسایت را از طریق خزیدن در وب Scrape کنیم.
- در مواقع مورد نیاز، از راهحلی که مرورگر هِدلِس سی شارپ ارائه میدهد استفاده کنیم.
- چگونه دادههای وبسایت پویا را با استفاده از سلنیوم استخراج کنیم.
Web Scraping با سی شارپ، چالشی نسبتاً بزرگ به حساب میآید. زیرا در حال حاضر، وبسایتها از فناوریهای ضد Scraping زیادی استفاده میکنند. دور زدن همه آنها کار آسانی نیست و ما همیشه باید به دنبال راه حلی برای این چالش باشیم. در این مسیر میتوانیم از APIهای موجود نیز، استفاده کنیم.
سوالات متداول Web Scraping با سی شارپ
در این بخش به تعدادی از پرسشهای رایج پیرامون وب اسکریپینگ در سی شارپ پاسخ داده شده است.
آیا زبان سی شارپ برای وب اسکرپینگ گزینه مناسبی است؟
بله، سی شارپ یک زبان برنامهنویسی همه منظوره است که شما را قادر میسازد تا فرایند وب اسکرپینگ را در حد خیلی خوبی انجام دهید. سی شارپ دارای یک کامیونیتی بزرگ و فعال است که کتابخانههای زیادی را برای کمک به شما، در دستیابی به هدفی که از وب اسکرپینگ دارید، توسعه داده است.
وب اسکرپینگ با سی شارپ چگونه انجام می شود؟
وب اسکرپینگ در سی شارپ، مانند سایر زبانهای برنامه نویسی انجام میشود. با استفاده از یک کتابخانه سی شارپ مربوط به وب اسکرپینگ، میتوانید به وبسایت مورد نظر متصل شوید، عناصر HTML را از DOM آن انتخاب و دادههای آن را بازیابی کنید.
بهترین روش برای وب اسکرپینگ با سی شارپ چیست؟
استفاده از کتابخانههای موجود در NuGet برای Scraping در سی شارپ، همه چیز را آسانتر میکند. Selenium ،ScrapySharp و Html Agility Pack، برخی از محبوبترین کتابخانههای سی شارپ برای استفاده در پروژههای اسکرپینگ دادهها محسوب میشوند.
با دادههای Scrape شده چه باید کرد؟
پس از جمعآوری دادهها، با توجه به هدفی که در ذهن داریم، لازم است آنها را در جایی نگهداری کنیم. در ادامه برخی از این موارد فهرست شدهاند.
- آنها را میتوان در پایگاهداده ذخیره کرد تا هر جایی که نیاز شد، با پرس و جو بازیابی شوند.
- آنها را به قالب JSON تبدیل کنیم و از آن برای فراخوانی برخی از APIها کمک بگیریم.
- تبدیل دادههای Scrape شده به فرمتهای قابل خواندن برای افراد دیگر، در قالبهایی مانند CSV، که با مایکروسافت اکسل نیز قابل نمایش است.
این روشها، فقط چندین نمونه رایج هستند. اما نکته مهم، این است که وقتی دادههای Scrape شده را در کدهایتان دارید، میتوانید آنها را به هر نحوی که مایل بودید، استفاده کنید. به طور معمول، دادههای Scrape شده به قالب مفیدتری برای بازاریابی، تجزیه و تحلیل دادهها یا تیم فروش شما تبدیل میشوند.
حریم خصوصی در Web Scraping با سی شارپ چگونه حفظ میشود؟
اگر میخواهید از مواردی همچون، افشای آدرس IP، مسدود شدن و افشای هویت خودتان جلوگیری کنید، پروکسیهای وب اسکرپینگ، این کار را برای شما انجام میدهند. پروکسی سرور، به عنوان دروازهای بین برنامه شما و سرور وبسایت مورد نظر عمل میکند و در نتیجه IP شما پنهان میشود. به این ترتیب، این سرویس به شما کمک میکند تا بر مشکل انسداد IP غلبه کنید و همچنین، دادهها را به صورت ناشناس Scrape و محتوای مورد نظر را در همه کشورها باز کنید. پروکسیها انواع مختلفی دارند و در مقاصد متنوعی استفاده میشوند. بنابراین باید مطمئن شوید که پروکسی سرور مناسبی را انتخاب میکنید.