ساخت یک وب اسکرپر ساده با جاوا

2019-07-08
30 دقیقه مطالعه

این روزا خراش وب یا وب اسکرپینگ خیلی پرطرفداره و اتفاقا خیلی کار جالب و در نوع خودش لذت بخشی هم هست. توی این مطلب می‌خوایم توی چند دقیقه یه وب اسکرپر ساده با زبان جاوا بنویسیم که بتونه عناوین صفحه‌ی اول وبلاگ من یا جایی مثل انجمن فارسی آرچ لینوکس رو به همراه لینک هر عنوان جستجو کنه.

چیزهایی که برای نوشتن همچین برنامه‌ای ازشون استفاده خواهیم کرد:

از OpenJDK 8 به عنوان کیت توسعه‌مون استفاده می‌کنیم که یه پیاده سازی اوپن سورس از پلتفرم جاوا رو برامون فراهم می‌کنه.

از Maven به عنوان مدیر پروژه استفاده خواهیم کرد که بهمون کمک می‌کنه به سادگی کتابخونه‌ی مورد نظرمون رو به برنامه اضافه کنیم.

از Emacs هم به عنوان محیط توسعه‌مون استفاده می‌کنیم که با چندتا پلاگین مناسب برنامه نویسی با جاوا و maven میشه. . و اما برای کار اصلی‌مون یعنی خراش وب هم از کتابخونه‌ی jsoup استفاده می‌کنیم.

قبل از هر چیز با استفاده از maven یه پروژه‌ی جدید درست می‌کنیم:

mvn archetype:generate -DgroupId=ir.mehranzr.simplescrapper -DartifactId=simple-web-scrapper -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

این روند چند ثانیه‌ای طول می‌کشه.

وارد دایرکتوری پروژه میشیم و تو فایل pom.xml، بین دو تگ <dependencies> (در صورت عدم وجود خودتون بسازیدش) خط‌های زیر رو قرار می‌دیم:

<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.11.3</version>
</dependency>

بیاید الگوریتم‌مون رو بررسی کنیم. روند کار خیلی ساده‌ست،

در مرحله‌ی اول ما قصد داریم محتویات یه صفحه‌ی وب رو دریافت کنیم و اون رو توی یه متغیر ذخیره کنیم. کافیه فقط یه درخواست به سایت مورد نظر بزنیم تا پاسخ وب سرور به ما برگردونده بشه و اون پاسخ رو می‌تونیم داخل یه متغیر ذخیره کنیم. (اختیاری: می‌تونیم قبلش هم بررسی کنیم که آیا پاسخ درخواست ما درست برگشته یا نه؟)

توی مرحله‌ی بعد میایم و با استفاده از CSS Selectorها عناصر مدنظرمون از محتویات صفحه رو جدا می‌کنیم و داخل یه متغیر دیگه ذخیره می‌کنیم.

و اما در مرحله‌ی آخر یه حلقه‌ی For می‌نویسیم که با استفاده ازش بتونیم دونه دونه عناصر رو از متغیری که عناصر رو داخلش ذخیره کردیم بیرون بکشیم.

منطقا توی هر دور حلقه یکی از عناصر رو خواهیم داشت و در نتیجه داخل حلقه باید عنوان و لینک اون عنصر خاص رو استخراج کنیم و نمایش بدیم (یا داخل یه فایل ذخیره کنیم.)

بسیار خب ! حالا که الگوریتم‌مون هم مشخص شد می‌تونیم بریم برنامه‌مون رو بنویسیم.

اگه به مسیر src/main/java/ir/mehranzr/simplescrapper برین (اسم پکیج شما ممکنه فرق کنه مثلا اگه اسم پکیج‌تون com.example.scrapper بود باید به همچین مسیری می‌رفتین: src/main/java/com/example/scrapper ) یه فایل app.java می‌بینید که اگه بازش کنید می‌بینید همچین محتوایی داره:

package ir.mehranzr.simplescrapper;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

مثل یه فایل معمول جاوا کلاس و متد اصلی رو می‌تونین مشاهده کنین که داخلش هم یه فرمان چاپ با پارامتر Hello World وجود داره. این فرمان چاپ صرفا جهت تست maven هست و هیچ کاربرد دیگه‌ای نداره و می‌تونید پاکش کنید.

داخل کلاس و خارج از متد اصلی یه متغیر ثابت به نام URL درست می‌کنیم و مقداری که بهش می‌دیم آدرس سایت موردنظرمونه (که در اینجا صفحه‌ی اصلی همین وبلاگ هست):

public class App {
    protected static final String URL = "https://mehranzr.ir/";
    public static void main( String[] args ) {
    }
}

همچنین می‌تونین جای تعریف مقدار ثابت توی متغیر از اسکنر استفاده کنین تا ورودی کاربر رو بخونه یا به صورت آرگیومنت موقع اجرا ورودی رو از کاربر بگیرید. این دیگه به خودتون بستگی داره و اینجا چون قراره یه مثال ساده از وب اسکرپینگ بزنیم وارد اون موضوعات نمی‌شیم.

حالا طبق الگوریتمی که داریم یه متغیر درست می‌کنیم که محتویات وب‌سایت رو داخلش ذخیره کنیم. این متغیر از نوع Document (ایمپورت‌ها رو توی ایمکس می‌تونید به javaimp بسپارید) خواهد بود و مقدارش پاسخ وب‌سرور سایت به درخواست ما خواهد بود (خروجی html):

Document siteContains = Jsoup.connect(URL).get();

در اینجا وقتی از متد get استفاده می‌کنیم نیاز به هندل کردن یه exception داریم که به سادگی با یه ایمپورت و تغییر متد اصلی به شکل زیر قابل انجامه:

import java.io.IOException;
public static void main(String[] args) throws IOException {
    ....
}

خب مرحله‌ی بعد نوبت می‌رسه به پیدا کردن CSS Selector مورد نیازمون که از مهم‌ترین مراحل به حساب میاد. اولین کاری که لازمه اینه که بریم به صفحه‌ای که قصد داریم اطلاعاتش رو استخراج کنیم. برای مثال اینجا من صفحه‌ی اصلی وبلاگ خودم رو مثال می‌زنم، با یه مرورگر واردش می‌شیم و روی یکی از عناوین راست کلیک می‌کنیم و Inspect Element رو انتخاب می‌کنیم:

find css selector 1

اگر کمی دقت کنید می‌بینید که عنوان ما داخل یک تگ <span> با کلاس list-line قرار گرفته و خود عنوان به شکل لینک هست. (تگ <a>) پس CSS Selectorای که برای استخراج عنوان نیاز داریم باید به لینکی اشاره کنه که داخل یک تگ با کلاس list-line قرار گرفته. پس سلکتورمون می‌شه این:

.list-line a

حالا که CSS Selectorمون رو پیدا کردیم می‌ریم تا ادامه‌ی برنامه رو بنویسیم. تا اینجا ما محتویات سایت رو داخل siteContains ذخیره کردیم حالا باید بیایم و از داخل محتویات سایت مقادیری که با CSS Selectorمون بهش اشاره می‌کنیم رو پیدا کنیم و داخل یک متغیر ذخیره کنیم:

    Elements titleElements = siteContains.select(".list-line a");

بسیار خب، به همین سادگی الان تمام عناصری که سی اس اس سلکتورمون بهش اشاره می‌کرد داخل متغیر titleEelemnts ذخیره شده. حالا تنها چیزی که نیازه اینه که بیایم و این المنت‌ها رو یکی یکی فراخوانی کنیم (و در اینجا فقط چاپ‌شون می‌کنیم). برای این کار از یه حلقه‌ی for استفاده می‌کنیم:

    for (Element titleElement : titleElements){
        String title = titleElement.text();
        String link = titleElement.attr("href");
        System.out.println(title + "\n" + URL + link);
    }

توی هر دور از حلقه‌ی بالا یک آیتم از متغیر titleElements رو به عنوان titleElement قرار می‌دیم و با استفاده از متد text() مقدار متنی تگ رو داخل متغیر title و با استفاده از متد attr() و آرگومان "href" میایم و مقدار اتریبیوت href تگ مورد نظرمون (که یک تگ <a> هست) رو داخل متغیر link ذخیره می‌کنیم. در نهایت فقط کافیه مقادیر رو چاپ کنیم یا هر جای دیگه‌ای که می‌خوایم ازش استفاده کنیم.

حالا می‌تونیم برنامه رو اجرا کنیم؛ برای کامپایل و اجرای برنامه از طریق maven می‌تونید از همچین دستوری استفاده کنید (حواستون به اسم پکیج باشه):

mvn install # just first time
mvn compile
mvn exec:java -Dexec.mainClass="ir.mehranzr.simplescrapper.App"

بعد از اجرا به خروجی مورد نظرمون می‌رسیم:

java web scrapper output 2

و کل سورس برنامه‌مون هم به این شکل شد:

package ir.mehranzr.simplescrapper;

import java.io.IOException;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class App {
    protected static final String URL = "https://mehranzr.ir/";

    public static void main( String[] args ) throws IOException {
    	Document siteContains = Jsoup.connect(URL).get();
    	Elements titleElements = siteContains.select(".list-line a");
    
    	for (Element titleElement : titleElements){
    	    String title = titleElement.text();
    	    String link = titleElement.attr("href");
    	    System.out.println(title + "\n" + URL + link);
    	}
    	
    }
}


برای اینکه قضیه روشن‌تر بشه یه مثال دیگه هم می‌زنیم و این بار می‌ریم سراغ انجمن فارسی آرچ لینوکس.

وارد انجمن فارسی آرچ لینوکس بشین و روی یکی از عناوین صفحه راست کلیک کنین و Inspect Element رو انتخاب کنید:

find css selector 2

می‌بینید که عنوان مورد نظر داخل یک تگ <div> با کلاس tclcon، یه <div> دیگه، یک تگ <h3> و در نهایت یک لینک (<a>) قرار گرفته. تنها چیزی که برای نوشتن این CSS Selector نیاز داریم دونستن اسم کلاس <div> و تگی که داخل اون <div> دنبالشیم هست (که اینجا دنبال یک لینک هستیم)، پس:

.tclcon a

حالا برمی‌گردیم به فایل برنامه‌مون و خطوط مربوط به آدرس و CSS Selector رو تغییر می‌دیم:

protected static final String URL = "https://bbs.archusers.ir/";
Elements titleElements = siteContains.select(".tclcon a");

الان اگر برنامه رو اجرا کنیم، خروجی همونطوری که انتظار داشتیم خواهد بود:

java web scrapper output 1

در انتها چندتا نکته باقی می‌مونه که لازمه بگم:

1- بعضی از سایت‌ها وقتی بفهمن شما با مرورگر واقعی وارد سایت نشدین و یک Scrapper هستین ممکنه به کلاینت شما اجازه‌ی ورود ندن! برای حل موضوع باید عامل کاربر رو به یک مرورگر معمول تغییر بدین. خیلی راحت می‌تونین با مراجعه به این سایت و در قسمت User Agent String explained، عامل کاربر خودتون رو دریافت کنین:

بعد از اینکه قسمت User Agent String explained مربوط به مرورگرتون رو کپی کردین اون رو داخل یه متغیر ذخیره کنین، مثلا:

protected  static  final String USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0";

و بعد خطی که به سایت وصل می‌شیم و محتویاتش رو دریافت می‌کنیم رو به شکل زیر تغییر بدین:

Document siteContains = Jsoup.connect(URL).userAgent(USER_AGENT).get();

2 - منطقا این روند برای همه سایت‌ها و همه‌ی اهداف نیست و برای سایت‌های مختلف باید CSS Selectorهای مربوط به هدفتون رو پیدا کنین و اینکه با دیتای استخراج شده می‌خواین چیکار کنین به عهده‌ی خودتونه ما اینجا فقط پرینتش کردیم.

3- جهت اطلاع: کتابخونه‌ای که ازش برای این برنامه استفاده کردیم برای parse کردن اچ تی ام ال بود و بیشتر به درد وب اسکرپینگ و استخراج داده از “صفحات وب” می‌خوره و اگه بخواین از این کتابخونه جهت خوندن api ها استفاده کنید اصلا گزینه‌ی مناسبی نیست.

در نهایت هم بگم این رو نمی‌شه یه آموزش مبتدی به حساب آورد چرا که باید تا حد معقولی جاوا بلد باشین و به پیدا کردن CSS Selectorها تسلط کافی داشته باشین و بتونین با maven هم کار کنین. در نتیجه اگه مبتدی هستین و چیزی متوجه نشدین نگران نباشین و به جاش پیش‌نیازها رو یاد بگیرین که با این کار می‌تونین Scrapper های خیلی بهتر هم بنویسین :)


سورس این پست رو می‌تونین از اینجا ببینین :)