Web Scraping với TypeScript: Trích xuất kiểu với Cheerio và Node
Web Data Collection Specialist
Tóm tắt:
- Node 22 chạy TypeScript trực tiếp với
node --experimental-strip-types, vì vậy một scraper không cần bước xây dựng và không cần trình đóng gói. fetchđược tích hợp trong runtime, điều này khiếncheeriotrở thành phụ thuộc duy nhất cho việc phân tích HTML và chọn CSS.- Gõ kiểu cho bản ghi bạn trích xuất là điều làm cho scraper dễ bảo trì: trình biên dịch sẽ báo lỗi trường được đổi tên tại thời điểm nó được tiêu thụ thay vì sau khi dữ liệu đã chuyển downstream.
- Các kiểu mô tả hình dạng mà bạn mong đợi, không phải trang mà bạn nhận được — một trang được render phía khách hàng vẫn trả về một phản hồi hợp lệ mà phân tích ra không có bản ghi nào.
- Scrapeless Universal Scraping API sẽ render trang đầu tiên, và các bộ chọn
cheeriokhông thay đổi sẽ trả về tất cả 10 bản ghi. - Bắt đầu với thẻ miễn phí của Scrapeless và chỉ định ví dụ pivot vào mục tiêu của riêng bạn.
TypeScript xứng đáng có chỗ trong một scraper vì một lý do: dữ liệu bạn trích xuất có hình dạng, và hình dạng đó có thể thay đổi. Một trang web đổi tên một trường, một bộ chọn bắt đầu trả về chuỗi rỗng, và một scraper JavaScript đơn giản sẽ mang lại thiệt hại một cách âm thầm vào bất cứ gì tiêu thụ nó. Một bản ghi kiểu đưa điều đó trở thành một lỗi biên dịch.
Điều thay đổi gần đây là chi phí thiết lập. Node 22 loại bỏ các kiểu định danh một cách tự nhiên, vì vậy không có bước tsc, không có trình đóng gói, và không có ts-node trong cây phụ thuộc.
Những gì bạn cần
Việc scraping web bằng TypeScript vào năm 2026 cần Node 22 trở lên và một phụ thuộc duy nhất. Các phiên bản bên dưới là những gì các ví dụ đã chạy:
| Thành phần | Phiên bản | Công việc |
|---|---|---|
| Node.js | 22.22.3 | Runtime, fetch tự nhiên, cắt bỏ kiểu tự nhiên |
cheerio |
1.2.0 | Phân tích HTML và bộ chọn CSS |
fetch được tích hợp trong runtime, vì vậy không cần import và không cần thư viện HTTP. cheerio cung cấp một API hình dạng jQuery trên một tài liệu đã phân tích, đây là điều gần nhất với một tiêu chuẩn cho truy vấn HTML phía máy chủ trong hệ sinh thái Node. Nó phân tích theo tiêu chuẩn phân tích HTML thay vì coi đánh dấu như văn bản để khớp với các mẫu.
Cài đặt
Tạo dự án và thêm một phụ thuộc:
bash
mkdir ts-scraper && cd ts-scraper
npm init -y
npm pkg set type=module
npm install cheerio@1.2.0
Cài đặt type=module là điều quan trọng: các ví dụ bên dưới sử dụng await ở cấp độ cao nhất, điều này yêu cầu cú pháp mô-đun ES.
Trích xuất một Bản ghi có Kiểu
Khai báo hình dạng trước, sau đó làm cho việc trích xuất tạo ra nó. Trình biên dịch sẽ giữ bạn ở đó:
typescript
import * as cheerio from "cheerio";
interface Quote {
text: string;
author: string;
tags: string[];
}
const res = await fetch("https://quotes.toscrape.com/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());
const quotes: Quote[] = $("div.quote")
.map((_, el) => ({
text: $(el).find("span.text").text(),
author: $(el).find("small.author").text(),
tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
}))
.get();
console.log(`quotes parsed: ${quotes.length}`);
console.log(JSON.stringify(quotes[0], null, 2));
Chạy nó mà không cần bước xây dựng:
bash
node --experimental-strip-types static.ts
text
quotes parsed: 10
{
"text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
"author": "Albert Einstein",
"tags": [
"change",
"deep-thoughts",
"thinking",
"world"
]
}
Ba điều đang thực hiện công việc thực sự ở đó.
Chú thích Quote trên quotes là điều làm cho callback .map() được kiểm tra kiểu. Nếu bạn trả về một đối tượng thiếu tags, hoặc viết sai là tag, lỗi sẽ xảy ra trên dòng đó thay vì nổi lên sau như là undefined trong bất cứ gì tiêu thụ mảng.
res.ok là kiểm tra mà mọi người bỏ qua. fetch không ném ra lỗi trên một 404 hoặc một 403 — nó giải quyết bình thường với ok được đặt thành false, và trang lỗi phân tích ra không có kết quả giống như một kết quả rỗng. Các lớp trạng thái mà hành xử theo cách này được định nghĩa trong tiêu chuẩn ngữ nghĩa HTTP.
Chức năng lồng ghép .map(...).get() là cách mà cheerio biến một chọn lựa thành một mảng thực sự. Lời gọi bên trong thu thập các chuỗi thẻ, vì vậy tags đến dưới dạng string[] hơn là một đối tượng cheerio.
Nơi Các Kiểu Ngừng Giúp Đỡ
Một kiểu mô tả bản ghi bạn mong đợi, không phải trang bạn nhận được. Cả fetch và cheerio đều không chạy JavaScript, vì vậy trên một trang xây dựng nội dung của nó trong trình duyệt, các bộ chọn không khớp với gì cả và các kiểu được xác nhận bằng một mảng rỗng.
Trang web trên công bố một bản sao được chỉnh sửa bởi khách hàng của cùng một dữ liệu tại /js/. Cùng một mã phân tích, chỉ định vào nó:
typescript
import * as cheerio from "cheerio";
const res = await fetch("https://quotes.toscrape.com/js/");
Nếu (!res.ok) throw new Error(HTTP ${res.status});
const html = await res.text();
const $ = cheerio.load(html);
console.log(byte HTML: ${html.length});
console.log(trích dẫn đã phân tích: ${$("div.quote").length});
Kết quả truy vấn thành công, res.ok là đúng, và 5.806 ký tự HTML hợp lệ đã được phân tích mà không có vấn đề gì. Quote[] là một mảng trống được kiểu hóa hoàn hảo. Đây là chế độ thất bại mà chúng ta cần thiết kế xung quanh, vì không có gì trong hệ thống kiểu hoặc lớp HTTP báo cáo điều đó - phần đánh dấu trích dẫn được ghi vào DOM sau khi một script chạy.
Render Đầu Tiên, Sau Đó Phân Tích
API Lấy Dữ Liệu Bất Động Sản Toàn Cầu Scrapeless lấp đầy khoảng cách đó bằng cách hiển thị trang trong một trình duyệt đám mây và trả về HTML kết quả, vì vậy phần TypeScript vẫn giữ nguyên gọi fetch đã được kiểu hóa.
Đặt khóa của bạn:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
Chỉ lớp truy vấn thay đổi - interface Quote và mã chọn lọc giống hệt như ví dụ đầu tiên:
typescript
import * as cheerio from "cheerio";
interface Quote {
text: string;
author: string;
tags: string[];
}
const res = await fetch("https://api.scrapeless.com/api/v2/unlocker/request", {
method: "POST",
headers: {
"x-api-token": process.env.SCRAPELESS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
actor: "unlocker.webunlocker",
input: {
url: "https://quotes.toscrape.com/js/",
js_render: true,
headless: true,
},
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const envelope: { data: string } = await res.json();
const $ = cheerio.load(envelope.data);
const quotes: Quote[] = $("div.quote")
.map((_, el) => ({
text: $(el).find("span.text").text(),
author: $(el).find("small.author").text(),
tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
}))
.get();
console.log(`byte HTML: ${envelope.data.length}`);
console.log(`trích dẫn đã phân tích: ${quotes.length}`);
console.log(`tác giả đầu tiên: ${quotes[0]?.author}`);
text
byte HTML: 8940
trích dẫn đã phân tích: 10
tác giả đầu tiên: Albert Einstein
Cùng một trang, cùng các selectors, cùng một interface. Số lượng bản ghi chuyển từ 0 sang 10 và nội dung tải về tăng từ 5806 lên 8940 ký tự, và sự khác biệt duy nhất là lớp nào đã lấy HTML.
Hai chi tiết TypeScript trong cuộc gọi đó đáng để sao chép. Ghi chú envelope là { data: string } ngăn await res.json() làm rò rỉ any qua phần còn lại của tệp, nơi sự an toàn kiểu thường bị rò rỉ ra khỏi trình thu thập dữ liệu. Và quotes[0]?.author tôn trọng thực tế rằng một chỉ mục mảng có thể là undefined - với noUncheckedIndexedAccess được bật, trình biên dịch yêu cầu điều đó.
Trường data giữ tài liệu đã được hiển thị dưới dạng một chuỗi, đó là lý do tại sao nó đi thẳng vào cheerio.load. Các tùy chọn hiển thị được đề cập trong tài liệu Scrapeless, và hành vi js_render tương tự được khám phá thêm trong hướng dẫn hiển thị JS.
Khắc Phục Sự Cố
ERR_UNKNOWN_FILE_EXTENSION trên tệp .ts. Cờ --experimental-strip-types bị thiếu, hoặc Node cũ hơn phiên bản 22. Việc loại bỏ kiểu tại thời điểm tải sẽ xóa các chú thích; nó không kiểm tra kiểu, vì vậy hãy chạy tsc --noEmit một cách riêng biệt khi bạn muốn ý kiến của trình biên dịch.
Không thể sử dụng câu lệnh import bên ngoài một mô-đun. Gói thiếu "type": "module". await ở cấp độ cao nhất cần các mô-đun ES.
Loại bỏ kiểu từ chối một enum hoặc một thuộc tính tham số. Những cấu trúc đó tạo ra mã thực thi tại thời gian thực thay vì có thể bị xóa, vì vậy việc loại bỏ không thể xử lý chúng. Sử dụng một tập hợp các chuỗi văn bản thay vì một enum, và gán các trường tạo thành một cách rõ ràng.
Selectors khớp trong trình duyệt nhưng không khớp trong script. So sánh với nguồn xem thay vì trình kiểm tra. Trình kiểm tra hiển thị DOM sau khi các script đã chạy, điều này không phải là những gì fetch đã nhận - in độ dài phản hồi trước tiên, như ví dụ trên làm.
Trước khi chỉ ra điều này vào một mục tiêu trực tiếp, hãy kiểm tra điều khoản của trang web và các chỉ thị /robots.txt của nó, tuân theo tiêu chuẩn Giao thức Loại trừ Robots, và chỉ giữ việc thu thập dữ liệu đối với các dữ liệu công cộng với khối lượng mà trang có thể phục vụ một cách thoải mái.
Kết Luận
TypeScript cung cấp cho một trình thu thập dữ liệu một hợp đồng: khai báo bản ghi, và trình biên dịch sẽ thông báo cho bạn khi việc trích xuất không còn thoả mãn nó. Với Node 22 loại bỏ kiểu một cách tự nhiên và fetch được tích hợp, hợp đồng đó chỉ tốn một phụ thuộc và không cần bước xây dựng nào.
Các loại không thể cho bạn biết liệu trang mà bạn lấy có chứa dữ liệu hay không. Kiểm tra đó phải được thực hiện một cách rõ ràng — một mảng rỗng được gõ tốt là cái mà một trang được kết xuất trên client trả về, và nó trông hoàn toàn giống như một trang không có kết quả. Đo lường sự khác biệt là thói quen đáng giữ: 10 bản ghi được kết xuất trên máy chủ, 0 trên bản sao JavaScript, lại 10 khi một cái gì đó kết xuất trang trước khi cheerio nhìn thấy nó.
Bắt đầu với gói miễn phí Scrapeless để thực hiện bước kết xuất với các mục tiêu của riêng bạn, và kiểm tra giá hiện tại của Scrapeless khi bạn định kích thước một công việc.
Câu hỏi thường gặp
Q: Tôi có cần biên dịch TypeScript để sử dụng nó cho việc thu thập dữ liệu không?
Không. Node 22 và các phiên bản sau chạy các tệp .ts trực tiếp với node --experimental-strip-types, cái sẽ loại bỏ chú thích kiểu khi tải. Điều đó có nghĩa là không có bước biên dịch và không có công cụ đóng gói cho một trình thu thập. Việc loại bỏ kiểu không kiểm tra kiểu, vì vậy hãy chạy tsc --noEmit trong CI khi bạn muốn trình biên dịch thực sự xác minh chúng.
Q: Thư viện phân tích HTML nào tôi nên sử dụng với TypeScript?
cheerio bao phủ hầu hết công việc — nó phân tích với một trình phân tích tuân thủ chuẩn HTML và cung cấp một API chọn lọc kiểu jQuery với định nghĩa TypeScript được bao gồm. Chỉ chọn một trình duyệt không đầu hoặc một API kết xuất khi nội dung được viết vào DOM bởi các script, mà không có trình phân tích nào có thể khôi phục một mình.
Q: fetch có gây ra lỗi trên 404 trong Node không?
Không, và điều này làm cho mọi người bất ngờ. fetch giải quyết bình thường cho bất kỳ phản hồi HTTP nào và chỉ từ chối khi xảy ra lỗi ở cấp độ mạng, vì vậy bạn phải tự kiểm tra res.ok. Nếu không có kiểm tra đó, một trang lỗi phân tích thành không có kết quả và không thể phân biệt với một trang mà thực sự không có kết quả.
Q: Làm thế nào tôi giữ cho các kiểu dữ liệu trung thực khi phản hồi là JSON?
Chú thích tại ranh giới. await res.json() trả về any, vì vậy gán nó cho một biến gõ kiểu như const envelope: { data: string } là cái ngăn chặn any khỏi lan tỏa qua phần còn lại của tệp. Đối với dữ liệu từ nguồn không đáng tin cậy, hãy xác thực tại thời điểm chạy với một thư viện sơ đồ thay vì chỉ dựa vào chú thích.
Q: TypeScript có thể thu thập dữ liệu từ một trang được kết xuất trong trình duyệt không?
Không tự nó. Ngôn ngữ không có sự liên quan đến việc JavaScript có thực thi hay không — fetch trả về các byte mà máy chủ gửi, và ví dụ trên cho thấy các byte đó phân tích thành không có bản ghi nào trên một trang được kết xuất trên client. Việc kết xuất phải xảy ra ở nơi khác, hoặc trong một trình duyệt không đầu mà bạn điều khiển hoặc thông qua một API trả về DOM đã được kết xuất.
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.



