استخراج دیتا از صفحات وب در کتابخانه لاراولی

در مقاله قبلی ما یک پکیج ساده لاراولی رو ایجاد و امکان تست کردن رو در اون راه‌اندازی کردیم. در این مقاله و در ادامه قصد داریم کتابخانه‌ خودمون رو گام به گام پیاده‌سازی کنیم. همانطور که پیش‌تر توضیح دادیم این کتابخانه قرار هست اطلاعات موجود درون یک فایل 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]:‌ ترکیبی از هر نوع کارکتری
*:‌ این الگوی برای تکرار صقر تابیشتر از یک استفاده میشه
(): این الگو هم نتیجه یک قسمت رو در یک آرایه جدا خروجی میده

همانطور که در تصویر زیر مشاهده می‌کنید خروجی این الگوی در یک گروه مجزا بدست اومده:

Image for post
Image for post

البته اگر در الگوی تعریف شده بجای body تگ h1 رو بزنیم اطلاعات درون h1 رو دریافت خواهیم کرد به همین سادگی!

بیایید به پروژه برگردیم و یک کلاس درون فولدر ‍src به نام BlogPostParser ایجاد کنیم که محتوای فایل html رو بخونه و اطلاعاتی که می‌خواهیم رو استخراج کنه.

خب همانطور که در تصویر زیر مشاهده می‌کنید این کلاس یک سازنده داره که در اون آدرس فایل ذخیره خواهد شد و بعد از اون متدی فراخوانی میشه که وظیفه استخراج داده با کمک regex که نوشتیم رو داره! انتظار داریم این کلاس فایل html رو بخونه و دیتای که در عنوان وجود داره رو به ما برگردونه!

ضمنا برای اینکه محتوای درون فایل رو بخونیم از کلاس File که از کلاس‌های درونی لاراول هست استفاده کردیم:

Image for post
Image for post

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

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

Image for post
Image for post

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

Image for post
Image for post

 

خب بیایید کمی مثالمون رو تغییر بدیم در اکثر صفحات وب‌سایت‌ها تگ‌هایی به نام متا وجود دارند که در صفحه نشون داده نمیشن ولی اطلاعات بیشتری در مورد محتوای صفحه دارند. بیایید مثالمون رو کمی تغییر بدیم و اطلاعات بیشتری به اون اضافه کنیم،‌ بطور حتم در نمونه‌های واقعی تگ‌ها و اطلاعات بیش از اینهاست اما اجازه بدید در اینجا ساده در نظر بگیریم:

<!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 بگیریم:

Image for post
Image for post

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

Image for post
Image for post

خب ظاهرا همچی درست کار می‌کنه البته بهتر از یک تابع built-in درون php به نام trim استفاده کنید تا خروجی بهتر میشد،‌ یعنی بهتر می‌بود خط آخر متد parseData بصورت زیر بنویسید:

   // ...
   
   $this->data['content'] = trim($matches[1]);
}        

که در اینصورت خروجی خیلی بهتر خواهد شد:

Image for post
Image for post

اما کدی که زدیم خیلی بهینه نیست و خوب نوشته نشده ضمن اینکه در مساله واقعی باید امکان ویرایش کردن و افزودن فیلد‌های بیشتر هم باشه. فراموش نکنید یکی از قابلیت‌های هر بسته‌ای قابلیت گسترش‌پذیری آن است!

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

Image for post
Image for post

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

Image for post
Image for post

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

Image for post
Image for post

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

حالا خیلی راحت‌تر می‌تونیم فیلدهای دیگه رو براساس این کلاس ایجاد کنیم تنها کافیه الگوی جستجو رو در هر کدوم تعریف بشه برای نمونه کلاس title رو مشاهده کنید:

Image for post
Image for post

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

Image for post
Image for post

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


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

تمرین فراموش نشه ... تمرین ... تمرین ... تمرین