استخراج دیتا از صفحات وب در کتابخانه لاراولی
در مقاله قبلی ما یک پکیج ساده لاراولی رو ایجاد و امکان تست کردن رو در اون راهاندازی کردیم. در این مقاله و در ادامه قصد داریم کتابخانه خودمون رو گام به گام پیادهسازی کنیم. همانطور که پیشتر توضیح دادیم این کتابخانه قرار هست اطلاعات موجود درون یک فایل html رو بگیره و مقادیر اون رو برای ما استخراج کنه اصطلاحا parse کنه. خب بیایید برای شروع یک فایل html خیلی ساده رو تعریف کنیم، دراینجا من یک ساختار خیلی ساده برهمین اساس پیاده کردم که میخواهیم روی اون کار کنیم. این فایل رو در مسیر test/data/blog.html
ذخیزه کنید:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sample</title>
</head>
<body>
<h1>This is a title</h1>
<div>
<p>Blog post body here</p>
</div>
</body>
</html>
برای ادامه کار ما نیاز به الگوهای جستجو یا همون regexها داریم. اگه با این الگوها آشنا نیستید پیشنهاد میکنم از وبسایت زیر استفاده کنید چراکه علاوه بر محیط تعاملی و خروجیهایی که به زبانهای مختلف میده توضیحات کاملی هم برای هر الگو داره!
الگویی که من در اینجا نیاز دارم یک الگوی سادست که در اون تمام کاراکترهای درون تگ body رو استخراج میکنه!
/<body>([\w\W]*)<\/body>/
-------------------------
\w: برای انتخاب تمامی کلمات
\W: برای ا انتخاب کاراکترهای غیر از کلمات
[\w\W]: ترکیبی از هر نوع کارکتری
*: این الگوی برای تکرار صقر تابیشتر از یک استفاده میشه
(): این الگو هم نتیجه یک قسمت رو در یک آرایه جدا خروجی میده
همانطور که در تصویر زیر مشاهده میکنید خروجی این الگوی در یک گروه مجزا بدست اومده:


البته اگر در الگوی تعریف شده بجای body تگ h1 رو بزنیم اطلاعات درون h1 رو دریافت خواهیم کرد به همین سادگی!
بیایید به پروژه برگردیم و یک کلاس درون فولدر src به نام BlogPostParser ایجاد کنیم که محتوای فایل html رو بخونه و اطلاعاتی که میخواهیم رو استخراج کنه.
خب همانطور که در تصویر زیر مشاهده میکنید این کلاس یک سازنده داره که در اون آدرس فایل ذخیره خواهد شد و بعد از اون متدی فراخوانی میشه که وظیفه استخراج داده با کمک regex که نوشتیم رو داره! انتظار داریم این کلاس فایل html رو بخونه و دیتای که در عنوان وجود داره رو به ما برگردونه!
ضمنا برای اینکه محتوای درون فایل رو بخونیم از کلاس File که از کلاسهای درونی لاراول هست استفاده کردیم:


تابع getData هم در اینجا امکان دسترسی به خروجی data شده از بیرون کلاس رو به ما میده.
در قدم بعدی به کلاس تستمون برمیگردیم من کلاس قبلیو ویرایش کردم و یک تابع جدید بصورت زیر نوشتم:


توجه کنید که متغیر __DIR__
در اینجا به آدرسی که کلاس ParserTest اشاره میکنه! با اجرای تست مشاهده میکنید که عملیات با موفقیت انجام شده و دیتای درون تگ h1 از یک فایل html استخراج شده:


خب بیایید کمی مثالمون رو تغییر بدیم در اکثر صفحات وبسایتها تگهایی به نام متا وجود دارند که در صفحه نشون داده نمیشن ولی اطلاعات بیشتری در مورد محتوای صفحه دارند. بیایید مثالمون رو کمی تغییر بدیم و اطلاعات بیشتری به اون اضافه کنیم، بطور حتم در نمونههای واقعی تگها و اطلاعات بیش از اینهاست اما اجازه بدید در اینجا ساده در نظر بگیریم:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>This is a title</title>
<meta name="description" content="this is a description">
<meta property="og:image" content="https://percept.ir/um/5553/614263bd58b06.jpg">
</head>
<body>
<h1>This is a title</h1>
<div>
<p>Blog post body here</p>
</div>
</body>
</html>
خب حالا وقتش رسیده که تابع parseData رو کمی تغییر بدیم تا بتونیم اطلاعات بیشتری از فایل html بگیریم:


میتونید هر الگوی رو داخل سایت بررسی کنید و حالا اجازه بدید یکبار دیگه کد تست رو اجرا کنیم تا نحوه عملکرد این تابع رو ارزیابی کنیم:


خب ظاهرا همچی درست کار میکنه البته بهتر از یک تابع built-in درون php به نام trim استفاده کنید تا خروجی بهتر میشد، یعنی بهتر میبود خط آخر متد parseData بصورت زیر بنویسید:
// ...
$this->data['content'] = trim($matches[1]);
}
که در اینصورت خروجی خیلی بهتر خواهد شد:


اما کدی که زدیم خیلی بهینه نیست و خوب نوشته نشده ضمن اینکه در مساله واقعی باید امکان ویرایش کردن و افزودن فیلدهای بیشتر هم باشه. فراموش نکنید یکی از قابلیتهای هر بستهای قابلیت گسترشپذیری آن است!
برای اینکار ابتدا درون دایرکتوری src یک فولدر جدید به نام Fields ایجاد کنید. سپس یک کلاس جدید به نام Title تعریف کنید. دقت کنید که namespace رو حتما تعریف کنید این مورد همیشه باید در تمامی کلاسها تعریف بشه! من متدهارو بصورت static تعریف میکنم تا نیاز به ایجاد یه نمونه نداشته باشیم:


با اینکار متد parseData در کلاس BlogPostParser تنها برای قسمت title بصورت زیر خلاصه خواهد شد:


کمی بهتر شد اما بیایید کلاسهای فیلد رو با یک کلاس abstract کمی بهتر کنیم. برای این منظور یک کلاس abstract یه نام FieldContract ایجاد میکنیم و اون رو بصورت زیر تعریف میکنیم:


در این کلاس abstract شده (با اینکار امکان استفاده مستقیم رو گرفتیم) یک ویژگی بنام pattern داریم که باید در کلاسهای فرزند مقدار دهی بشه و یک تابع process که نیاز به تعریف مجدد در فرزندهارو کم میکنه البته این مورد رو هم میشه ویرایش کرد ولی در حالت کلی نیاز نیست!
حالا خیلی راحتتر میتونیم فیلدهای دیگه رو براساس این کلاس ایجاد کنیم تنها کافیه الگوی جستجو رو در هر کدوم تعریف بشه برای نمونه کلاس title رو مشاهده کنید:


من کلاسهای دیگه رو هم به همین شکل تعریف کردم و در نهایت متد praseData بصورت زیر درآمد:


قطعا این متد الان خوانایی بهتری داره ولی اینکه نام فیلد و کلاس رو کنار هم داریم استفاده میکنم داره اذیت میکنه و این روش کد زدن بعدا توسط کاربر قابل گسترش نیست! همانطور که گفتیم ممکنه کاربر بخواد فیلدهای دیگری هم داشته باشه در اینصورت این روش استاتیک خیلی جواب نیست، ضمن اینکه امکان ویرایش الگوهای جستجو هم میخواهیم!
خب تا همینجا کافیه میتونید تا مقاله بعدی در این رابطه جستجو کنید! و کامنت بدید یا اینکه صبر کنید تا در مقاله بعدی در مورد مشکلی که گفته شد راه حل رو ارائه بدیم 😉😉🙏
تمرین فراموش نشه ... تمرین ... تمرین ... تمرین