برنامه نویسی

جستجوی قلعه ها با استفاده از Go، MongoDB، Github Actions و Web Scraping

مدتی است!

از آخری مدتی می گذرد! TL;DR: کار، زندگی، مطالعه… می دانید 🙂

پروژه داده های باز با استفاده از Go

در مارس 2024، من یک پروژه کوچک برای آزمایش استفاده کردم رویدادهای ارسال شده توسط سرور (SSE) در یک وب سرور Go برای ارسال مداوم داده ها به یک سرویس گیرنده frontend. چیز خاصی نبود، اما هنوز خیلی باحال بود 🙂

این پروژه شامل سرور کوچکی بود که در Go نوشته شده بود که به یک کلاینت ظاهری مینیمالیستی که با HTML خام، جاوا اسکریپت وانیلی و Tailwind CSS ایجاد شده بود، خدمت می‌کرد. علاوه بر این، یک نقطه پایانی را فراهم می کند که در آن مشتری می تواند یک اتصال SSE را باز کند. هدف اصلی این بود که frontend دکمه‌ای داشته باشد که با فشار دادن آن، جستجوی سمت سرور برای جمع‌آوری داده‌های مربوط به قلعه‌ها آغاز شود. همانطور که قلعه‌ها پیدا می‌شدند، آنها از سرور به قسمت جلویی در زمان واقعی ارسال می‌شدند. من روی قلعه هایی از انگلستان و پرتغال تمرکز کردم و پروژه به خوبی کار کرد همانطور که در زیر می بینید:

نسخه مستقل Find castles در حال کار است

کد چنین پروژه مینیمالیستی را می توانید در اینجا پیدا کنید و می توانید دستورالعمل های README را دنبال کنید تا آن را بر روی ماشین محلی خود اجرا کنید.

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

چرا Go واقعا برای این پروژه می درخشد؟

برنامه ها و کانال ها! بزرگترین بخش کد این پروژه، پیمایش در وب سایت ها، جمع آوری و پردازش داده ها برای ذخیره آن در پایگاه داده خواهد بود. با استفاده از Go ما از سهولتی که زبان به ما ارائه می دهد برای اجرای این عملیات پیچیده با حفظ حداکثر مقدار ممکن مو استفاده می کنیم 🙂

چگونه کار می کند تا کنون؟

تاکنون جمع‌آوری‌کننده‌های داده را فقط برای ۳ کشور پیاده‌سازی کرده‌ام: ایرلند، پرتغال و بریتانیادلیل آن این بود که تلاش برای یافتن مرجع مناسب برای این کشورها چندان سخت نبود.

پیاده سازی فعلی اساساً دارای دو مرحله اصلی است: بازرسی وب سایت برای پیوندهای حاوی داده های قلعه و استخراج داده ها فی نفسه. این فرآیند برای همه کشورها یکسان است و به همین دلیل یک رابط برای ایجاد یک API پایدار برای غنی‌کننده‌های فعلی و آینده معرفی شد:

type Enricher interface {
  CollectCastlesToEnrich(ctx context.Context) ([]castle.Model, error)

  EnrichCastle(ctx context.Context, c castle.Model) (castle.Model, error)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر می خواهید اجرای حداقل یکی را ببینید، در اینجا می توانید غنی کننده ایرلند را پیدا کنید.

هنگامی که ما غنی‌کننده‌هایی داریم که قادر به جمع‌آوری و استخراج داده‌ها از منابع مناسب هستند، در واقع می‌توانیم داده‌ها را با استفاده از آن جمع‌آوری کنیم بسته مجری. این بسته، اجرای غنی‌کننده‌ها را با استفاده از گوروتین‌ها و کانال‌هایی که بار کار را بین CPUهای موجود توزیع می‌کنند، مدیریت می‌کند.

تعریف جریان مجری و سازنده تابع را می توان در زیر مشاهده کرد:

type EnchimentExecutor struct {
    enrichers map[castle.Country]enricher.Enricher
    cpus      int
}

func New(
    cpusToUse int,
    httpClient *http.Client,
    enrichers map[castle.Country]enricher.Enricher) *EnchimentExecutor {
    cpus := cpusToUse
    availableCPUs := runtime.NumCPU()
    if cpusToUse > availableCPUs {
        cpus = availableCPUs
    }
    return &EnchimentExecutor{
        cpus:      cpus,
        enrichers: enrichers,
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

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

func (ex *EnchimentExecutor) collectCastles(ctx context.Context) (<-chan castle.Model, <-chan error) {
    var collectingChan []<-chan castle.Model
    var errChan []<-chan error
    for _, enricher := range ex.enrichers {
        castlesChan, castlesErrChan := ex.toChanel(ctx, enricher)
        collectingChan = append(collectingChan, castlesChan)
        errChan = append(errChan, castlesErrChan)
    }
    return fanin.Merge(ctx, collectingChan...), fanin.Merge(ctx, errChan...)
}

func (ex *EnchimentExecutor) toChanel(ctx context.Context, e enricher.Enricher) (<-chan castle.Model, <-chan error) {
    castlesToEnrich := make(chan castle.Model)
    errChan := make(chan error)
    go func() {
        defer close(castlesToEnrich)
        defer close(errChan)

        englandCastles, err := e.CollectCastlesToEnrich(ctx)
        if err != nil {
            errChan <- err
        }
        for _, c := range englandCastles {
            castlesToEnrich <- c
        }
    }()
    return castlesToEnrich, errChan
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مرحله دوم گروهی از گوروتین ها را ایجاد می کند تا به کانال خروجی مرحله قبل گوش دهند، و با دریافت قلعه ها، داده ها را با خراش دادن صفحه HTML استخراج می کند. همانطور که استخراج داده ها به پایان می رسد، قلعه های غنی شده به کانال دیگری که حاوی قلعه های غنی شده است رانده می شوند.

func (ex *EnchimentExecutor) extractData(ctx context.Context, castlesToEnrich <-chan castle.Model) (chan castle.Model, chan error) {
    enrichedCastles := make(chan castle.Model)
    errChan := make(chan error)

    go func() {
        defer close(enrichedCastles)
        defer close(errChan)

        for {
            select {
            case <-ctx.Done():
                return
            case castleToEnrich, ok := <-castlesToEnrich:
                if ok {
                    enricher := ex.enrichers[castleToEnrich.Country]
                    enrichedCastle, err := enricher.EnrichCastle(ctx, castleToEnrich)
                    if err != nil {
                        errChan <- err
                    } else {
                        enrichedCastles <- enrichedCastle
                    }
                } else {
                    return
                }
            }
        }
    }()

    return enrichedCastles, errChan
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و عملکرد اصلی مجری که همه این کارها را انجام می دهد در زیر یک است:

func (ex *EnchimentExecutor) Enrich(ctx context.Context) (<-chan castle.Model, <-chan error) {
    castlesToEnrich, errChan := ex.collectCastles(ctx)
    enrichedCastlesBuf := []<-chan castle.Model{}
    castlesEnrichmentErr := []<-chan error{errChan}
    for i := 0; i < ex.cpus; i++ {
        receivedEnrichedCastlesChan, enrichErrs := ex.extractData(ctx, castlesToEnrich)
        enrichedCastlesBuf = append(enrichedCastlesBuf, receivedEnrichedCastlesChan)
        castlesEnrichmentErr = append(castlesEnrichmentErr, enrichErrs)
    }

    enrichedCastles := fanin.Merge(ctx, enrichedCastlesBuf...)
    enrichmentErrs := fanin.Merge(ctx, castlesEnrichmentErr...)

    return enrichedCastles, enrichmentErrs
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجرای کامل فعلی مجری را می توانید در اینجا بیابید.

آخرین فقط کانال را با قلعه های غنی شده مصرف می کند و آنها را به صورت انبوه در MongoDB ذخیره می کند:

castlesChan, errChan := castlesEnricher.Enrich(ctx)

var buffer []castle.Model

for {
  select {
  case castle, ok := <-castlesChan:
    if !ok {
      if len(buffer) > 0 {
        if err := db.SaveCastles(ctx, collection, buffer); err != nil {
          log.Fatal(err)
        }
      }
      return
    }
    buffer = append(buffer, castle)
    if len(buffer) >= bufferSize {
      if err := db.SaveCastles(ctx, collection, buffer); err != nil {
        log.Fatal(err)
      }
      buffer = buffer[:0]
    }
  case err := <-errChan:
    if err != nil {
      log.Printf("error enriching castles: %v", err)
    }
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

می توانید نسخه فعلی main.go را در اینجا بیابید. این فرآیند به صورت دوره ای با استفاده از یک کار برنامه ریزی شده ایجاد شده با استفاده از Github Actions اجرا می شود.

مراحل بعدی

این پروژه نقشه راه قابل توجهی در پیش دارد که در زیر می توانید مراحل بعدی را فهرست کنید.

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

2. پشتیبانی از چندین منبع وب سایت غنی سازی برای یک کشور: همچنین باید از چندین منبع وب سایت غنی سازی یک کشور پشتیبانی کند زیرا این چیزی است که من دیدم امکان پذیر است.

3. یک وب سایت رسمی ایجاد کنید: در این بین باید یک وب سایت رسمی برای این پروژه انجام شود تا داده های جمع آوری شده در دسترس باشد و مطمئناً پیشرفت را نشان دهد. چنین سایتی در حال انجام است و شما می توانید در اینجا از آن بازدید کنید. به دلیل عدم مهارت در طراحی سایت زشت است، اما با ما همراه باشید تا از آن عبور کنیم 🙂

4. یادگیری ماشینی را برای پر کردن شکاف های داده یکپارچه کنید: و مطمئناً چیزی که کمک زیادی خواهد کرد، به ویژه در تکمیل داده‌هایی که به سختی از طریق غنی‌کننده‌های معمولی یافت می‌شوند، یادگیری ماشینی خواهد بود، زیرا با تحریک این مدل‌ها برای درخواست داده‌های غیرقابل یافتن، می‌توانیم به طور موثر آن را پر کنیم. شکاف داده ها و غنی سازی مجموعه داده ها.

مشارکت ها خوش آمدید!
این پروژه متن باز است و همه همکاری بیش از حد استقبال می شود! چه به توسعه باطن، طراحی ظاهر یا هر جنبه دیگری از پروژه علاقه مند باشید، ورودی شما ارزشمند است.

اگر چیزی را پیدا کردید که می‌خواهید به آن کمک کنید – مخصوصاً با frontend 🙂 – فقط یک موضوع را در مخزن باز کنید و درخواست بررسی کد کنید.

این مقاله در اصل در سایت شخصی من ارسال شده است: https://www.buarki.com/blog/find-castles

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا