Crawl dữ liệu với golang
Lấy 200$ cloud miễn phí để tha hồ deploy project và nghịch phá server ở đây nhé mọi người.
👇️👇️👇️
Mở đầu
Vấn đề crawl dữ liệu thì cũng có khá nhiều công cụ và thư viện hỗ trợ làm việc này. Nhưng hôm nay mình xin mạn phép hướng dẫn crawl dữ liệu bằng một ngôn ngữ lập trình khá nổi trong thời gian gần đây - Golang. Vì hiệu năng của nó cực tốt, và càng phù hợp hơn khi chúng ta cần crawl một lượng dữ liệu lớn và gửi đi một lượng request liên tục thì golang - với khả năng tính toán song song của mình xem ra là rất phù hợp...
Một số package cần sử dụng
-
Gocolly - là một package viết bằng ngôn ngữ lập trình Golang, hỗ trợ crawl dữ liệu tương đối nổi tiếng (dựa trên lõi một package nổi tiếng khác là Goquery). Nhìn chung mình chọn nó vì nó đơn giản dễ sử dụng, lại vừa đủ dùng. Bạn có thể xem qua một chút trên trang github của package này
-
Go MySQL Driver - Package để chúng ta làm việc với Mysql, bởi lẽ mình cần crawl dữ liệu về và lưu vào database để dùng mà. Bạn có thể cần xem qua package này tại đây
-
Flag - Flag thì đã quá nổi tiếng rồi, pack này hỗ trợ chúng ta trong việc xây dựng cái CLI tool với các tham số truyền vào, ví dụ như
crawl --target=medium.com
chẳng hạn. Thì flag hỗ trợ chúng ta tạo và paser các tham số--target=medium.com
kiểu này. Và khi gõ--help
thì nó show luôn thông tin tham số xem nó có kiểu dữ liệu gì. Tham số đó để làm gì... blala tương tự như những gì chúng ta thấy khi gõzip --hepl
haycomposer --help
..v.v ấy -
encoding/xml Package này dùng để đọc file xml. Mình gặp may một chút, cái site mình nhắm đến để crawl dữ liệu có sẵn file sitemap.xml (file này tương đối cần thiết cho việc google bot crawl và đánh index site của bạn, nhiều site chu đáo, họ có sẵn file này) nên mình đỡ được bao nhiêu công đoạn. Lấy được file
sitemap.xml
này về là tóm hầu hết url hiện có của site. Mình chỉ cần đọc file này ra, đưa vào mảng và quét qua hết url trong file để crawl dữ liệu từng trang là được. KakaNếu không thì cũng đầy công cụ hỗ trợ
sinh file sitemap
kiểu này. Ví dụ như https://www.xml-sitemaps.com/, https://xmlsitemapgenerator.org/... -
Ngoài một số package chính trên. Mình còn cần đến vài package khác như
crypto/md5
,encoding/hex
,regexp
vànet/http
...
Xử lý file xml
Như đã nói ở trên. Do trang mục tiêu mình nhắm đến có sẵn file sitemap.xml
, nên mình sẽ triển khai theo hướng khai thác file này. Một file sitemap.xml
thường có định dạng như một ví dụ dưới đây
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- Generated by Web-Site-Map.com -->
<url><loc>https://www.dkn.tv/the-gioi/ty-le-nguoi-my-ung-ho-xay-tuong-bien-gioi-cao-nhat-moi-thoi-dai.html</loc><priority>1.00</priority></url>
<url><loc>https://www.dkn.tv/the-gioi/bao-nuoc-ngoai-vi-sao-du-khach-han-quoc-den-viet-nam-tang-manh.html</loc><priority>0.85</priority></url>
<url><loc>https://www.dkn.tv/the-gioi/dong-ho-go-lon-nhat-the-gioi-sau-15-nam-sang-tao-da-tim-duoc-noi-trung-bay.html</loc><priority>0.85</priority></url>
<url><loc>https://www.dkn.tv/the-gioi/nguoi-goc-viet-lam-thien-nguyen-de-tra-on-nuoc-my.html</loc><priority>0.85</priority></url>
<url><loc>https://www.dkn.tv/tin-giai-tri/vang-duy-manh-doi-tuyen-viet-nam-dung-hang-thu-nao-dau-yemen.html/loc><priority>0.85</priority></url>
<url><loc>https://www.dkn.tv/trong-nuoc/nhau-dem-o-phong-tro-4-thanh-nien-bi-nhom-nguoi-la-chem-guc.html</loc><priority>0.85</priority></url>
</urlset>
Và đây là phần code xử lý đọc file sitemap.xml
trên và trả về một struct Urlset
trong đó field Urls
là một slice gồm các chuỗi url đã được bóc ra để tiện lặp qua slice này và quét các url trong đó
package processXml
import (
"encoding/xml"
"io/ioutil"
"os"
"fmt"
)
type Urlset struct {
XMLUrlSet xml.Name `xml:"urlset"`
Urls []Url `xml:"url"`
}
type Url struct {
Url xml.Name `xml:"url"`
Loc string `xml:"loc"`
}
func ReadSiteMap(sitemap string) (urlSet Urlset){
xmlFile, err := os.Open(sitemap)
if err != nil {
fmt.Println(err)
}
fmt.Println("Successfully Opened users.xml")
defer xmlFile.Close()
byteValue, _ := ioutil.ReadAll(xmlFile)
xml.Unmarshal(byteValue, &urlSet)
return
}
Connect Mysql
Theo thói quen mình thường tách phần connect db ra như dưới đây
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func DBConn() (db *sql.DB) {
dbDriver := "mysql"
dbUser := "root"
dbPass := "secret"
dbName := "viblo-crawler-example"
db, err := sql.Open(dbDriver, dbUser + ":" + dbPass + "@tcp(localhost:3306)/" + dbName)
if err != nil {
panic(err.Error())
}
return db
}
Bóc element
Bạn có thể xem qua rất nhiều ví dụ về việc sử dụng gocolly ở đây . Việc quan trọng là cần xác định được selector để chọn đúng đến phần tử html mà chúng ta cần lấy content trong đó - Cái này thì chỉ cần inspect element xong chuột phải rồi chọn copy selector
là được, chỉ cần để ý lại chút xíu select cho chính xác nếu cần. Dưới đây là một đoạn code ví dụ kết hợp với file xml trên:
package main
import (
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"regexp"
"./database"
"./processXml"
"github.com/gocolly/colly"
"github.com/gosimple/slug"
)
func visitLink(urlSet processXml.Urlset, db *sql.DB) {
for i := 0; i < len(urlSet.Urls); i++ {
c := colly.NewCollector()
c.OnHTML("#the-post", func(e *colly.HTMLElement) {
// Lấy tiêu đề bài viết
title := e.ChildText("#the-post > div.post-head > h1")
// Lấy thuộc tích src của ảnh đại diện bài viết dùng để download ảnh
avataUrl := e.ChildAttr("#the-post > div.post-head > figure > div > img", "src")
// Tải ảnh avata
avata := downloadImage(avataUrl, title)
// Lấy content bài viết
content := ""
e.ForEach("#the-post-content > p", func(_ int, m *colly.HTMLElement) {
contentOrigin := regexp.MustCompile(`\n`)
contentConverted := contentOrigin.ReplaceAllString(m.Text, "<br/>")
content += "<p>" + contentConverted + "</p>"
})
// Tạo slug name dựa trên tiêu đề bài viết
slugName := slug.Make(title)
// Lưu dữ liệu bài viết vào DB
insPost, err := db.Prepare("INSERT INTO dkn.posts (title, avata, content, slug) VALUES(?, ?, ?, ?)")
handleError(err)
insPost.Exec(title, "img/"+ avata + ".jpg", content, slugName)
fmt.Printf("Inserted: %q\n", content)
})
c.OnRequest(func(r *colly.Request) {
fmt.Println("Crawling... ", r.URL)
})
c.Visit(urlSet.Urls[i].Loc)
}
}
func main() {
db := database.DBConn()
defer db.Close()
links := processXml.ReadSiteMap("sitemaptest.xml")
visitLink(links, db)
}
func handleError(e error) {
if e != nil {
panic(e)
}
}
// Tải luôn hình đại diện bài viết
func downloadImage(src, title string) (image string){
// Tạo hasname cho ảnh tránh bị trùng lặp
md5HashInBytes := md5.Sum([]byte(title))
image = hex.EncodeToString(md5HashInBytes[:])
img, _ := os.Create("img/" + image + ".jpg")
defer img.Close()
resp, _ := http.Get(src)
defer resp.Body.Close()
b, _ := io.Copy(img, resp.Body)
fmt.Println("Saved image ! Size: ", b)
return
}
Và kết quả thu được như dưới đây
Kết luận
Qua ví dụ nhỏ trên đây, chỉ với vài đoạn code nhỏ chúng ta đã có thể dễ dàng thu thập dữ liệu cần thiết về. Tất nhiên nếu dữ liệu trên trang mục tiêu có cấu trúc html phức tạp hơn, chúng ta cần nhiều phần dữ liệu hơn thì đòi hỏi logic trong code phải phức tạp hơn nữa. Trên đây chỉ là ví dụ ở mức đơn giản nhất thôi ^^
Mặc dù hiệu năng phụ thuộc nhiều vào tốcmạng của chúng ta nữa. Nhưng sử dụng go mang đến tốc độ rất tốt về khả năng xử lý song song cùng lúc các url. Một lợi thế nữa là có thể dễ dàng build đoạn code trên thanh một tool CLI rồi chuyển qua chạy ở các môi trường khác nhau mà không đòi hỏi người dùng cài cắm gì cả...
Bài viết hơi nhiều code, mong bạn thông cảm nhé (bow) Cảm ơn bạn đã đọc bài viết của mình ^^
All rights reserved