Web Scraping bằng Scala: Lấy dữ liệu, Phân tích và Mở khóa Các Trang Bảo vệ
Expert in Web Scraping Technologies
TL;DR:
- Scala thu hoạch từ web với hai phần: một client HTTP trên JVM để lấy dữ liệu, và một parser dựa trên jsoup để trích xuất. requests-scala thực hiện yêu cầu; scala-scraper chuyển đổi HTML thành các nút có thể chọn bằng CSS.
- Toàn bộ dự án là ba phụ thuộc sbt. requests-scala 0.9.0 cho HTTP, scala-scraper 3.2.0 cho việc phân tích, và ujson 4.1.0 cho một bao JSON mà bạn giải mã sau — không có framework nào để học.
- Phân trang là một vòng lặp qua liên kết "tiếp theo". scala-scraper đọc href của trang tiếp theo với một bộ chọn tùy chọn, vì vậy việc đi qua danh mục là một hàm đệ quy đuôi, không phải là một hàng đợi.
- Lấy dữ liệu tĩnh có một giới hạn cứng: nó không thể chạy JavaScript hoặc vượt qua thử thách bot. Một lệnh
requests.getthông thường trả về vỏ rỗng của một trang đã được client render và nhận được một trang thử thách trên các trang bảo vệ. - API Scraping Universal Không Mảnh sẽ thu hẹp khoảng cách này với một POST HTTP đơn giản.
js_render: truethực hiện trang ở phía máy chủ và trả về DOM hoàn chỉnh; client requests-scala mà bạn nói chuyện với một trang có thể nói chuyện với API. - Cuộc gọi mở khóa đã được chạy trực tiếp chống lại điểm cuối: HTTP 200, 51.275 bytes HTML đã được render, 20 tiêu đề sản phẩm. Hình dạng yêu cầu và phản hồi trong hướng dẫn này đến từ cuộc chạy trực tiếp đó.
- Miễn phí để bắt đầu. Tài khoản Scrapeless mới bao gồm thời gian chạy miễn phí — đăng ký tại app.scrapeless.com.
Giới thiệu: nơi Scala phù hợp trong scraping
Scala chạy trên JVM, điều này có nghĩa là một scraper viết bằng nó thừa hưởng jsoup, Akka, và một hệ sinh thái HTTP trưởng thành miễn phí. Ngôn ngữ này rất phù hợp khi việc thu hoạch dành cho thứ gì đó đã có trên JVM — một công việc Spark, một nhà sản xuất Kafka, một dịch vụ dữ liệu — và bạn muốn việc trích xuất trong cùng một mã nguồn, với cùng các kiểu, như pipeline tiêu thụ nó.
Nửa công việc fetch và parse đó thì ngắn gọn. Một vài dòng kéo một trang và đọc giá trị từ nó bằng các bộ chọn CSS. Sự cọ xát bắt đầu từ nơi mọi scraper đều gặp phải: một phần ngày càng tăng của web xây dựng nội dung của nó bằng JavaScript mà phải thực sự chạy trước khi dữ liệu tồn tại, và các trang được bảo vệ thì yêu cầu quyền truy cập thông qua phân tích dấu vân tay TLS và các trang thử thách mà một client HTTP thô không bao giờ vượt qua.
Hướng dẫn này xây dựng scraper tĩnh trước tiên — dự án sbt, một fetch HTTP, trích xuất dựa trên bộ chọn, phân trang — sau đó vẽ ra ranh giới trung thực nơi cách tiếp cận đó dừng lại và chuyển giao các trang khó cho API Scraping Universal Không Mảnh. Cuộc gọi API ở cuối đã được chạy trực tiếp; các số liệu của nó là một bản ghi thực tế.
Những gì bạn có thể làm với stack này
- Lấy và phân tích trên JVM — requests-scala cho yêu cầu, scala-scraper (một wrapper jsoup) cho trích xuất bằng CSS-selector.
- Giữ việc trích xuất trong mã nguồn dữ liệu của bạn — đọc giá trị vào các kiểu Scala ngay bên cạnh công việc Spark hoặc Kafka sử dụng chúng.
- Đi bộ trong các danh sách phân trang — theo dõi liên kết trang tiếp theo trong một vòng lặp đệ quy đuôi cho đến khi nó không còn nữa.
- Đạt được các trang đã được render JavaScript và bảo vệ — POST chúng đến API Scraping Universal và phân tích HTML đã được render theo cách tương tự.
- Bỏ qua stack chống bot — phân tích dấu vân tay TLS, IP dân cư, và giải quyết thử thách sống bên trong API, không phải mã Scala của bạn.
Tại sao API Scraping Universal Không Mảnh
API Scraping Universal Không Mảnh nhận một URL mục tiêu và trả về HTML đã được render, không bị chặn. Đối với một client Scala cụ thể, nó mang lại:
- Render JavaScript phía máy chủ —
js_render: truetrả về DOM hoàn chỉnh, vì vậy scala-scraper thấy nội dung thực thay vì một vỏ rỗng. - Proxy dân cư ở hơn 195 quốc gia — việc lấy dữ liệu được thực hiện từ các IP tin cậy; bạn không bao giờ xây dựng hoặc thay đổi một hồ bơi trong Scala.
- Xử lý chống bot — phân tích dấu vân tay TLS và giải quyết thử thách xảy ra phía API, không phải quá trình JVM của bạn.
- Một POST HTTPS đơn giản — không thêm SDK vào
build.sbt; client requests-scala mà bạn đã có là đủ. - Một bao nhỏ —
{"code":200,"data":"<html>…"}, giải mã với cùng một ujson mà bạn sử dụng ở chỗ khác.
Nhận khóa API của bạn trên kế hoạch miễn phí tại app.scrapeless.com.
Các yêu cầu
- Một JDK (11 hoặc mới hơn) và sbt đã được cài đặt
- Scala 2.13 (các phiên bản phụ thuộc dưới đây là các build 2.13)
- Một tài khoản Scrapeless và khóa API — đăng ký tại app.scrapeless.com
- Kiến thức cơ bản về terminal
Lưu ý: Mã Scala trong phần build và bước bên dưới là một khe hở trong việc xác minh hướng dẫn này — không có thời gian chạy JVM/sbt nào có sẵn trên máy xác minh, vì vậy những khối đó đã được soạn thảo và kiểm tra đối chiếu với các API và phiên bản Maven Central hiện tại của thư viện thay vì được thực thi. Cuộc gọi mở khóa mang tính chất tải trọng của Scrapeless đã được chạy trực tiếp chống lại điểm cuối; yêu cầu và phản hồi của nó là một bản ghi thực tế.
Cài đặt
Tạo một thư mục dự án với hai tệp. build.sbt xác định ngôn ngữ và ba phụ thuộc:
scala
ThisBuild / scalaVersion := "2.13.16"
lazy val scraper = (project in file("."))
.settings(
name := "scala-scraper-demo",
```vi
libraryDependencies ++= Seq(
"com.lihaoyi" %% "requests" % "0.9.0",
"net.ruippeixotog" %% "scala-scraper" % "3.2.0",
"com.lihaoyi" %% "ujson" % "4.1.0"
)
)
tệp tin project/build.properties cố định sbt chính nó:
text
sbt.version=1.12.13
scala-scraper kéo theo jsoup một cách gián tiếp, vì vậy bạn phân tích bằng công cụ jsoup thông qua DSL Scala có kiểu mà không cần phụ thuộc vào jsoup trực tiếp. Chạy sbt update một lần để giải quyết mọi thứ, sau đó sbt console cho REPL hoặc sbt run cho main.
Bước 1 — Lấy một trang
requests-scala là một khách hàng HTTP đồng bộ nhẹ nhàng. Một cuộc gọi nhận được thân trang dưới dạng chuỗi:
scala
val res = requests.get(
"https://books.toscrape.com/",
headers = Map("User-Agent" -> "Mozilla/5.0 (compatible; scala-scraper-demo)")
)
println(res.statusCode) // 200
val html: String = res.text()
res.text() là HTML thô. Đối với một trang được render bởi máy chủ như trang này, chuỗi đó đã chứa dữ liệu; đối với một trang được render bởi máy khách, nó sẽ giữ một shell trống, đó là giới hạn mà Bước 4 đề cập đến.
Bước 2 — Phân tích với scala-scraper
scala-scraper phân tích chuỗi thành một tài liệu và chọn các nút với các bộ chọn CSS thông qua DSL của nó. Toán tử >> trích xuất; elementList, attr, và texts định hình kết quả thành các giá trị Scala:
scala
import net.ruippeixotog.scalascraper.browser.JsoupBrowser
import net.ruippeixotog.scalascraper.dsl.DSL._
import net.ruippeixotog.scalascraper.dsl.DSL.Extract._
val doc = JsoupBrowser().parseString(html)
val titles: List[String] = doc >> elementList("article.product_pod h3 a") >> attr("title")
val prices: List[String] = doc >> texts("p.price_color")
val books = titles.zip(prices)
books.foreach { case (t, p) => println(s"$p $t") }
article.product_pod h3 a là bộ chọn bền vững ở đây — lớp thẻ sản phẩm cộng với liên kết bên trong tiêu đề của nó — và title chứa tên đầy đủ ngay cả khi văn bản hiển thị bị cắt ngắn. Lấy giá trị từ một thuộc tính thay vì văn bản đã render là cách đọc ổn định hơn bất cứ khi nào trang web cung cấp nó.
Bước 3 — Theo dõi phân trang
Danh mục tiếp tục qua các trang, mỗi trang liên kết đến trang tiếp theo thông qua một phần tử li.next a. Bộ chọn tùy chọn >?> của scala-scraper trả về None khi liên kết đó vắng mặt, đây chính là điều kiện dừng của vòng lặp:
scala
import net.ruippeixotog.scalascraper.model.Document
def nextUrl(doc: Document, base: String): Option[String] =
(doc >?> element("li.next a")).map(a => base + a.attr("href"))
@annotation.tailrec
def crawl(url: String, base: String, acc: List[String]): List[String] = {
val doc = JsoupBrowser().parseString(requests.get(url).text())
val names = doc >> elementList("article.product_pod h3 a") >> attr("title")
nextUrl(doc, base) match {
case Some(next) => crawl(next, base, acc ++ names)
case None => acc ++ names
}
}
val all = crawl("https://books.toscrape.com/catalogue/page-1.html",
"https://books.toscrape.com/catalogue/", Nil)
println(all.size)
Giữ cho vòng lặp lịch sự — một máy chủ tại một thời điểm, một độ trễ nhỏ giữa các trang — và đối xử với các trường vắng mặt như Option, không bao giờ như một giá trị mà bạn cho là có mặt.
Đặt API key của bạn trên gói miễn phí: app.scrapeless.com
Nơi mà việc lấy tĩnh dừng lại
requests.get làm một việc: nó trả về các byte mà máy chủ gửi đến một khách hàng ẩn danh. Điều đó đủ cho một danh mục được render bởi máy chủ và không hơn. Hai trường hợp làm điều đó hỏng, và cả hai đều khá phổ biến:
- Các trang được render bởi máy khách. Khi một trang web xây dựng nội dung của nó bằng JavaScript, HTML mà bạn lấy là một shell trống với dữ liệu vẫn bị khóa trong các script. scala-scraper không có gì để chọn vì nội dung chưa bao giờ ở trong các byte.
- Các trang được bảo vệ. Các trang web có các biện pháp chống bot tích cực trả lời yêu cầu ẩn danh bằng một thử thách gián đoạn, không phải là trang. Một khách hàng HTTP thông thường không có cách nào để xóa nó.
Tái hiện sửa chữa bằng Scala — một trình duyệt không có đầu để chạy JavaScript, một hồ bơi proxy dân cư, một giải pháp thử thách — là một dự án lớn hơn nhiều so với việc scrape chính nó. Hành động thực tiễn là ngừng khiến Scala làm phần đó và chuyển những URL đó cho một API render.
Cái xoắn cloud: render phía máy chủ, phân tích trong Scala
API Scrapeless Universal Scraping nhận một URL mục tiêu, chạy nó phía máy chủ thông qua một trình duyệt thực và điểm truy cập dân cư, và trả về HTML hoàn chỉnh. Từ Scala, đó chỉ là một POST với cùng một khách hàng requests-scala, và ujson giải mã phản hồi:
scala
val apiKey = sys.env("SCRAPELESS_API_KEY")
val payload = ujson.Obj(
"actor" -> "unlocker.webunlocker",
"input" -> ujson.Obj(
"url" -> "https://books.toscrape.com/",
"method" -> "GET",
"redirect" -> true,
"js_render" -> true
)
)
val res = requests.post(
"https://api.scrapeless.com/api/v1/unlocker/request",
vi
headers = Map("Content-Type" -> "application/json", "x-api-token" -> apiKey),
data = ujson.write(payload),
readTimeout = 120000
)
val env = ujson.read(res.text())
val html = env("data").str // cây DOM đã được render dưới dạng chuỗi
js_render: true là cờ chịu tải: nó cho API biết để chạy JavaScript của trang và trả về DOM đã hoàn thiện, vì vậy một trang có nội dung được tạo ra từ phía khách sẽ trở lại như mã thực. Từ đây, html đi thẳng vào cùng một JsoupBrowser().parseString(html) và cùng các bộ chọn từ Bước 2 - nửa phân tích cú pháp của công cụ thu thập dữ liệu của bạn không thay đổi, chỉ có việc lấy dữ liệu.
Những gì bạn nhận được
Phản hồi từ API là một phong bì nhỏ, có thể dự đoán được:
json
{
"code": 200,
"data": "<html>...DOM đã được render...</html>"
}
// mẫu minh họa: sơ đồ là hình dạng thực tế từ một cuộc gọi trực tiếp; chuỗi "data" đã bị cắt ngắn ở đây. Trong lần chạy đã xác minh, "data" chứa 51,275 byte của HTML đã được(render) JSON-escaped.
Một cuộc gọi trực tiếp đến điểm cuối cho trang danh mục ở trên đã trả về HTTP 200 với 51,275 byte của HTML đã được render; việc chạy các bộ chọn từ Bước 2 trên HTML đó cho ra 20 tiêu đề sản phẩm, tiêu đề đầu tiên là "A Light in the Attic" với giá £51.77. Một vài ghi chú từ lần chạy:
js_render: truetốn thời gian nhưng mang lại nội dung. Tắt nó cho các trang tĩnh để nhanh hơn; bật nó lên khi trang trống không có nó.ujsonđọc trường mà bạn cần.env("data").strlà toàn bộ mã giải; phần còn lại của phong bì chỉ là trạng tháicode.- Các bộ chọn giữ nguyên trong Scala. API trả lại HTML, vì vậy logic trích xuất, kiểu dữ liệu và kiểm tra tồn tại trong mã nguồn của bạn, không phải ở sau một sơ đồ được quản lý.
- Xem các trường thiếu như
Option. Một bộ chọn nullable với>?>là cách đọc chính xác bất cứ khi nào một thẻ có thể bỏ qua giá hoặc tiêu đề.
Kết luận: Scala cho việc phân tích cú pháp, một API cho việc lấy dữ liệu khó khăn
Một bộ thu thập dữ liệu Scala ngắn gọn nơi JVM mạnh — requests-scala cho yêu cầu, scala-scraper cho trích xuất bộ chọn CSS, một vòng đi đuôi đệ quy qua liên kết trang tiếp theo. Nó gặp phải bức tường mà mọi bộ thu thập tĩnh đều gặp phải: các trang được tạo từ phía khách và các biện pháp chống bot chủ động mà một khách hàng HTTP thông thường không thể vượt qua. Định hướng các URL đó thông qua Universal Scraping API giữ việc sửa chữa chỉ ở một POST duy nhất và để việc phân tích của bạn không bị ảnh hưởng. Để tách biệt tương tự giữa việc lấy và phân tích trong ngôn ngữ khác, xem hướng dẫn thu thập thông tin JavaScript và Node.js; tài liệu bao quát toàn bộ API và các tham số của nó. Gán js_render cho những gì trang cần, giữ bộ chọn trong Scala, và xem mỗi trường như tùy chọn.
Sẵn sàng xây dựng đường ống dữ liệu AI của bạn?
Tham gia cộng đồng của chúng tôi để yêu cầu một kế hoạch miễn phí và kết nối với các nhà phát triển xây dựng bộ thu thập dữ liệu JVM: Discord · Telegram.
Đăng ký tại app.scrapeless.com để nhận thời gian chạy miễn phí và điều chỉnh chương trình trên cho các trang và bộ chọn mà đường ống Scala của bạn cần. Xem giá cả để biết quy mô.
Câu hỏi thường gặp
Q: Việc thu thập dữ liệu bằng Scala có hợp pháp không?
Việc thu thập dữ liệu công khai thường được phép, nhưng quy tắc có thể khác nhau tùy theo khu vực pháp lý và trang web. Xem lại điều khoản dịch vụ của mục tiêu, tôn trọng chỉ thị robots, tránh dữ liệu cá nhân hoặc bị hạn chế, và tham khảo ý kiến luật sư cho bất kỳ điều gì thương mại.
Q: Tôi có cần proxy không?
Đối với việc thu thập dữ liệu nhẹ một trang được tạo ra từ máy chủ, thì không. Đối với các trang được bảo vệ hoặc tạo từ phía khách, yêu cầu gửi qua proxy dân cư của Universal Scraping API ở hơn 195 quốc gia, vì vậy bạn không cần xây dựng một nhóm trong Scala.
Q: Thách thức bot trông như thế nào, và làm thế nào tôi có thể nhận được một bản render sạch?
Thay vì trang, một yêu cầu ẩn danh sẽ nhận được một thông báo thách thức. Định tuyến URL đó qua Universal Scraping API với js_render: true; nó chạy trang từ phía máy chủ thông qua một IP dân cư đáng tin cậy và trả về HTML đã hoàn thiện.
Q: Tại sao dùng scala-scraper mà không gọi jsoup trực tiếp?
scala-scraper bọc jsoup trong một DSL Scala có kiểu, vì vậy các bộ chọn trả về List[String] hoặc Option[Element] thay vì các bộ sưu tập Java. Bạn có được bộ phân tích cú pháp của jsoup với các kết quả phù hợp với việc phân tích cú pháp mẫu Scala.
Q: Các bộ chọn của tôi bị hỏng sau khi trang web thay đổi. Bây giờ làm gì?
Thẻ HTML thay đổi. Kiểm tra lại trang và siết chặt bộ chọn — ưu tiên một lớp container ổn định cộng với một thuộc tính đọc (article.product_pod h3 a → title) hơn là một lớp CSS mã hóa thay đổi trong lần thiết kế tiếp theo.
Q: Tôi có thể chạy nhiều trang song song không?
Có, nhưng hãy giữ khoảng ba công nhân mỗi máy chủ để bạn giữ lịch sự và tránh giới hạn tần suất. Một lần duy nhất đi bộ theo đuôi đệ quy với một độ trễ nhỏ là mặc định an toàn.
Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.



