بناء زاحف الويب باستخدام Go لاكتشاف العناوين المكررة

في هذه المقالة سأكتب زاحف ويب صغير. لم أكن متأكدًا مما إذا كان موقع الويب الخاص بي يحتوي على عناوين صفحات رائعة على مستوى الموقع ، وإذا كان لدي عناوين مكررة ، لذلك كتبت هذه الأداة الصغيرة لمعرفة ذلك.

سأبدأ بكتابة أمر يقبل صفحة بداية من سطر الأوامر ، ويتبع أي رابط يحتوي على عنوان url الأصلي كقاعدة.

سأضيف لاحقًا علامة اختيارية لاكتشاف ما إذا كان الموقع يحتوي على أم لاعناوين مكررة، وهو أمر قد يكون مفيدًا لأغراض تحسين محركات البحث.

مقدمةgolang.org/x/net/html

الgolang.org/xالحزم هي حزم يحتفظ بها فريق Go ، لكنها ليست جزءًا من المكتبة القياسية لأسباب مختلفة.

ربما تكون محددة للغاية ، ولن يستخدمها غالبية مطوري Go. ربما لا تزال قيد التطوير أو تجريبية ، لذلك لا يمكن تضمينها في stdlib ، والتي يجب أن تفي بوعد Go 1.0 بعدم وجود تغييرات غير متوافقة مع الوراء - عندما يتم إدخال شيء ما في stdlib ، يكون الأمر "نهائيًا".

واحدة من هذه الحزم هيgolang.org/x/net/html.

لتثبيته ، قم بتنفيذ

go get golang.org/x/net...

في هذه المقالة سأستخدم على وجه الخصوصhtml.Parse()وظيفة ، وhtml.Nodeهيكل:

package html

type Node struct { Type NodeType Data string Attr []Attribute FirstChild, NextSibling *node }

type NodeType int32

const ( ErrorNode NodeType = iota TextNode DocumentNode ElementNode CommentNode DoctypeNode )

type Attribute struct { Key, Val string }

func Parse(r io.Reader) (*Node, error)

يقبل البرنامج الأول أدناه عنوان URL ويحسب الروابط الفريدة التي يعثر عليها ، ويعطي ناتجًا مثل هذا:

http://localhost:1313/go-filesystem-structure/ -> Filesystem Structure of a Go project
http://localhost:1313/golang-measure-time/ -> Measuring execution time in a Go program
http://localhost:1313/go-tutorial-fortune/ -> Go CLI tutorial: fortune clone
http://localhost:1313/go-tutorial-lolcat/ -> Build a Command Line app with Go: lolcat

لنبدأ منmain()، حيث يعرض نظرة عامة عالية المستوى على ما يفعله البرنامج.

  1. يحصل علىurlمن صفحات CLI باستخدام `os.Args [1]
  2. ينشأvisited، وهي خريطة تحتوي على سلاسل رئيسية وسلسلة قيمة ، حيث سنخزن عنوان URL وعنوان صفحات الموقع
  3. المكالماتanalyze().urlيتم تمريره مرتين ، لأن الوظيفة تكرارية وتعمل المعلمة الثانية كعنوان URL الأساسي للمكالمات العودية
  4. يتكرر علىvisitedالخريطة التي تم تمريرها بالرجوع إلىanalyze()والآن تم ملء جميع القيم ، حتى نتمكن من طباعتها

    package main
    

    import ( “fmt” “net/http” “os” “strings”

    <span style="color:#e6db74">"golang.org/x/net/html"</span>
    

    )

    func main() { url := os.Args[1] if url == “” { fmt.Println(“Usage: webcrawler &lt;url&gt;) os.Exit(1) } visited := map[string]string{} analyze(url, url, &visited) for k, v := range visited { fmt.Printf(”%s -> %s\n”, k, v) } }

بسيطا بما فيه الكفاية؟ دعنا ندخلanalyze(). أول شيء ، إنه يدعوparse()، والذي يعطي سلسلة تشير إلى عنوان URL سيؤدي إلى جلبه وتحليله بإرجاع مؤشر html.Node وخطأ.

تحليل func (سلسلة عنوان url) (* html.Node ، خطأ)

بعد التحقق من النجاح ،analyze()يجلب عنوان الصفحة باستخدامpageTitle()، التي تعطي إشارة إلى html.Node ، تفحصها حتى تعثر على علامة العنوان ، ثم ترجع قيمتها.

func pageTitle (n * html.Node) سلسلة

بمجرد حصولنا على عنوان الصفحة ، يمكننا إضافته إلى ملفvisitedخريطة.

بعد ذلك ، نحصل على جميع روابط الصفحات عن طريق الاتصالpageLinks()، والتي بالنظر إلى عقدة صفحة البداية ، ستفحص بشكل متكرر جميع عقد الصفحة وستعيد قائمة الروابط الفريدة التي تم العثور عليها (لا توجد تكرارات).

func pageLinks (ارتباطات [] سلسلة ، n * html.Node) [] سلسلة

بمجرد أن نحصل على شريحة الروابط ، نكررها ، ونقوم بفحص بسيط: إذاvisitedلا تحتوي على الصفحة بعد ، فهذا يعني أننا لم نقم بزيارتها بعد ، ويجب أن يحتوي الرابطbaseurlكبادئة. إذا تم تأكيد هذين التأكيدات ، فيمكننا الاتصالanalyze()مع رابط الرابط.

// analyze given a url and a basurl, recoursively scans the page
// following all the links and fills the `visited` map
func analyze(url, baseurl string, visited *map[string]string) {
	page, err := parse(url)
	if err != nil {
		fmt.Printf("Error getting page %s %s\n", url, err)
		return
	}
	title := pageTitle(page)
	(*visited)[url] = title
<span style="color:#75715e">//recursively find links

links := pageLinks(nil, page) for _, link := range links { if (*visited)[link] == “” && strings.HasPrefix(link, baseurl) { analyze(link, baseurl, visited) } } }

pageTitle()يستخدمgolang.org/x/net/htmlواجهات برمجة التطبيقات التي قدمناها أعلاه. في التكرار الأول ،nهل<html>العقدة. نحن نبحث عن علامة العنوان. لا يرضي التكرار الأول هذا أبدًا ، لذلك نذهب ونقوم بعمل حلقة فوق الطفل الأول لـ<html>أولا ، وإخوته فيما بعد ، ونحن ندعوpageTitle()بشكل متكرر تمرير العقدة الجديدة.

في النهاية سنصل إلى<title>الوسم: أhtml.Nodeعلى سبيل المثال معTypeيساويhtml.ElementNode(انظر أعلاه) وDataيساويtitle، ونعيد محتواه من خلال الوصول إلىFirstChild.Dataمنشأه

// pageTitle given a reference to a html.Node, scans it until it
// finds the title tag, and returns its value
func pageTitle(n *html.Node) string {
	var title string
	if n.Type == html.ElementNode && n.Data == "title" {
		return n.FirstChild.Data
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		title = pageTitle(c)
		if title != "" {
			break
		}
	}
	return title
}

pageLinks()لا يختلف كثيرا عنpageTitle()، باستثناء أنه لا يتوقف عند العثور على العنصر الأول ، ولكنه يبحث عن كل ارتباط ، لذلك يجب أن نجتازlinksشريحة كمعامل لهذه الوظيفة العودية. يتم اكتشاف الروابط عن طريق التحقق منhtml.Nodeلديهاhtml.ElementNode TypeوDataيجب أنaوأيضا يجب أن يكون لديهمAttrمعKey hrefوإلا فإنه يمكن أن يكون مرساة.

// pageLinks will recursively scan a `html.Node` and will return
// a list of links found, with no duplicates
func pageLinks(links []string, n *html.Node) []string {
	if n.Type == html.ElementNode && n.Data == "a" {
		for _, a := range n.Attr {
			if a.Key == "href" {
				if !sliceContains(links, a.Val) {
					links = append(links, a.Val)
				}
			}
		}
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		links = pageLinks(links, c)
	}
	return links
}

sliceContains()هي وظيفة المنفعة التي دعاهاpageLinks()للتحقق من التفرد في الشريحة.

// sliceContains returns true if `slice` contains `value`
func sliceContains(slice []string, value string) bool {
	for _, v := range slice {
		if v == value {
			return true
		}
	}
	return false
}

الوظيفة الأخيرة هيparse(). يستخدمhttpوظيفة stdlib للحصول على محتويات URL (http.Get()) ثم يستخدم ملفgolang.org/x/net/html html.Parse()API لتحليل نص الاستجابة من طلب HTTP ، وإرجاع ملفhtml.Nodeالمرجعي.

// parse given a string pointing to a URL will fetch and parse it
// returning an html.Node pointer
func parse(url string) (*html.Node, error) {
	r, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("Cannot get page")
	}
	b, err := html.Parse(r.Body)
	if err != nil {
		return nil, fmt.Errorf("Cannot parse page")
	}
	return b, err
}

كشف العناوين المكررة

منذ أن أردتاستخدم علم سطر الأوامرللتحقق من التكرارات ، سأقوم بتغيير طفيف في كيفية تمرير عنوان URL إلى البرنامج: بدلاً من استخدامos.Args، سأقوم بتمرير عنوان URL باستخدام علامة أيضًا.

هذا هو المعدلmain()الوظيفة ، مع تحليل الأعلام قبل القيام بالأعمال المعتادة لإعدادanalyze()تنفيذ وطباعة القيم. بالإضافة إلى ذلك ، يوجد في النهاية فحص لـdupعلم منطقي ، وإذا كان صحيحًا يتم تشغيلهcheckDuplicates().

import (
	"flag"
//...
)

func main() { var url string var dup bool flag.StringVar(&url, “url”, “”, “the url to parse”) flag.BoolVar(&dup, “dup”, false, “if set, check for duplicates”) flag.Parse()

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">""</span> {
	<span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">PrintDefaults</span>()
	<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
}

<span style="color:#a6e22e">visited</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{}
<span style="color:#a6e22e">analyze</span>(<span style="color:#a6e22e">url</span>, <span style="color:#a6e22e">url</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">visited</span>)
<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">visited</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">"%s -&gt; %s\n"</span>, <span style="color:#a6e22e">link</span>, <span style="color:#a6e22e">title</span>)
}

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">dup</span> {
	<span style="color:#a6e22e">checkDuplicates</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">visited</span>)
}

}

checkDuplicatesتأخذ خريطة url -> العناوين وتكرر عليها لبناء موقعها الخاصuniquesالخريطة ، هذه المرة تحتوي على عنوان الصفحة كمفتاح ، حتى نتمكن من التحقق منuniques[title] == ""لتحديد ما إذا كان العنوان موجودًا بالفعل ، ويمكننا الوصول إلى الصفحة الأولى التي تم إدخالها بهذا العنوان عن طريق الطباعةuniques[title].

// checkDuplicates scans the visited map for pages with duplicate titles
// and writes a report
func checkDuplicates(visited *map[string]string) {
	found := false
	uniques := map[string]string{}
	fmt.Printf("\nChecking duplicates..\n")
	for link, title := range *visited {
		if uniques[title] == "" {
			uniques[title] = link
		} else {
			found = true
			fmt.Printf("Duplicate title \"%s\" in %s but already found in %s\n", title, link, uniques[title])
		}
	}
<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">found</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"No duplicates were found 😇"</span>)
}

}

الاعتمادات

لغة البرمجة Goكتاب دونوفان وكيرنيغان يستخدم زاحف الويب كمثال في جميع أنحاء الكتاب ، وتغييره في فصول مختلفة لتقديم مفاهيم جديدة. الرمز الوارد في هذه المقالة مستوحى من الكتاب.


المزيد من دروس Go: