Ngiêm cấm sử đụng PROXY và VPS vào mục đích trái pháp luật bạn sẽ phải chịu toàn bộ trách nhiệm trước pháp luật khi sử dụng dịch vụ của chúng tôi tks!
VCR.py cassette cho crawler test: ghi, lọc, phát lại

VCR.py cassette cho crawler test: ghi, lọc bí mật và phát lại

Chuyên viên Kỹ thuật ProxyVN 05-06-2026 Đang tính... phút đọc

VCR.py cassette cho crawler test là cách ghi HTTP thật một lần rồi phát lại khi kiểm parser, không cần chạm mạng nữa. Bài này viết cho người có crawler cần test ổn định. ProxyVN chạy vcrpy 9.4.2 với pytest trên origin cục bộ ngày 05/11/2026, thấy cassette thô lộ cả bốn loại bí mật, và mật khẩu proxy nằm trong đó dưới dạng base64 nên tìm chuỗi thường không ra.

VCR.py cassette cho crawler test: mục tiêu và phạm vi không lặp

Phần này chốt bài toán, đầu ra và điểm khác với bài kiểm dữ liệu crawl đã có.

Bài toán cần giải

Một crawler có parser phức tạp, nhưng test lại gọi website thật. Mỗi lần chạy test, mạng chậm thì thất bại, website đổi thì thất bại, và khóa API của nhóm đi qua máy CI. Có người gợi ý dùng VCR.py. Tệp cassette được commit vào kho mã, và một tuần sau có người phát hiện token nằm trong đó.

Đầu ra nghiệm thu

Đầu ra là một cassette fixture đã lọc token, cùng test replay chứng minh không tạo request mạng. Phép chứng minh phải đo được, không chỉ tin vào cấu hình. Bài có thêm một bước quét bí mật tự động trước khi cassette vào kho.

Ranh giới với Scrapy contracts

Bài Scrapy Contracts kiểm tra dữ liệu crawl kiểm callback bằng hợp đồng trong docstring, và contract vẫn tải URL thật. Bài này ghi lại phản hồi HTTP rồi phát lại. Hai cách bổ sung cho nhau: cassette giữ test ổn định, contract cảnh báo khi website thay đổi. Cassette không báo được website đã đổi, và đây là giới hạn chính của nó.

Không phải nguồn dữ liệu sản xuất

Cassette là fixture test. Không dùng cassette làm dữ liệu mới, và không phục vụ nó cho pipeline thật. Dữ liệu trong đó đông cứng tại thời điểm ghi và đã bị lọc. Crawler chạy thật phải đi qua đường mạng bình thường, còn VCR.py chỉ bật trong test.

Phạm vi lab

Lab dựng origin cục bộ cổng 9581 có đếm request, trả HTML sản phẩm, cookie, JSON có token. Lab chạy pytest với 8 test, tất cả đạt trong 3.33 giây. Một proxy cục bộ cổng 9599 dùng để kiểm chuyện lộ thông tin đăng nhập proxy. Không có website bên ngoài nào được gọi, nên mọi bí mật trong bài đều là chuỗi lab đặt.

Ai nên đọc

Bài hợp với kỹ sư viết crawler bằng requests hoặc httpx, người dựng CI cho dự án thu thập dữ liệu. Người dùng Scrapy cần lưu ý Scrapy không đi qua các thư viện HTTP mà VCR.py hỗ trợ theo cách thông thường, nên cần hướng khác. Lab không kiểm Scrapy.

Thuật ngữ dùng trong bài

Cassette là tệp YAML chứa các cặp request và response. Record mode quyết định khi nào ghi, khi nào phát lại. Replay là phát lại. Matcher là luật so request với bản ghi. Filter là bộ lọc thay giá trị nhạy cảm. Interaction là một cặp request, response trong cassette.

Ghi và phát lại HTTP cassette khi test parser, lọc credential khỏi bản ghi với VCR.py cassette cho crawler test

Phần này đi từ cài đặt tới bản ghi đầu tiên, rồi chỉ ra vì sao cấu hình mặc định là chưa đủ.

Nguồn tài liệu đã đọc

Lab đọc trang chủ tài liệu VCR.py, trang Usage, trang Configuration và trang Advanced. Brief ghi tài liệu bản 8.0.0, còn pip cài ra 9.4.2 ngày lab chạy. Thông báo lỗi của bản này khá chi tiết, sẽ thấy ở phần sau.

Cài đặt

Lab tạo venv riêng rồi cài vcrpy, pytest, requests, httpx, lxml. Thư viện chỉ cần cho test, nên đặt ở nhóm phụ thuộc dành cho phát triển, không vào phụ thuộc chạy thật.

import requests, vcr
 
B = "http://127.0.0.1:9581"
H = {"Authorization": "Bearer token-lab-bi-mat-123"}
RAW = vcr.VCR(cassette_library_dir="cassettes", record_mode="once")
 
with RAW.use_cassette("raw.yaml"):
    requests.get(B + "/catalog?page=1", headers=H)
    requests.get(B + "/login-echo?api_key=khoa-url-000")

Lần chạy đầu gọi thật tới origin và ghi hai interaction vào cassettes/raw.yaml. Nhìn đoạn code thì không có gì đáng lo. Vấn đề nằm trong tệp được sinh ra.

Cassette thô có gì

Lab tìm bốn chuỗi lab đặt trong raw.yaml: token trong header Authorization, cookie trong Set-Cookie của response, token trong thân JSON, khóa trong query string. Kết quả là cả bốn đều có. Bốn vị trí này là bốn cách lộ bí mật thường gặp khi commit cassette.

Bảng: Cassette thô có gì

Bí mật

Vị trí

Trong raw.yaml

Trong safe.yaml

token-lab-bi-mat-123

Header request

Có

Không

cookie-lab-bi-mat-456

Set-Cookie response

Có

Không

token-trong-body-789

Thân response JSON

Có

Không

khoa-url-000

Query string

Có

Không

Cấu hình lọc

Mỗi vị trí cần một cơ chế riêng. Filter_headers xử lý header request. Filter_query_parameters xử lý query. Thân và header của response phải qua hàm before_record_response. Chỉ khai filter_headers là cách nhiều bài hướng dẫn dừng lại, và đó là cách để sót.

import re
 
def scrub(resp):
    resp["headers"].pop("Set-Cookie", None)
    resp["headers"].pop("set-cookie", None)
    b = resp["body"].get("string", b"")
    if isinstance(b, bytes):
        resp["body"]["string"] = re.sub(rb'"access_token": "[^"]+"',
                                        b'"access_token": "LOC"', b)
    return resp
 
SAFE = vcr.VCR(
    cassette_library_dir="cassettes",
    filter_headers=[("authorization", "LOC"), ("cookie", None)],
    filter_query_parameters=[("api_key", "LOC")],
    before_record_response=scrub,
    decode_compressed_response=True,
    record_mode="once",
)

Với cấu hình này, safe.yaml không còn chuỗi nào trong bốn chuỗi bí mật. Giá trị thay thế LOC là tự đặt, còn None nghĩa là bỏ hẳn header.

Vì sao cần decode_compressed_response

Nếu response nén gzip, thân lưu trong cassette là byte nén, và regex lọc không thấy gì. Trang Advanced nhắc rằng bí mật trong thân nén sẽ không được tìm ra khi không bật decode_compressed_response=True. Origin của lab không nén nên lab không tái hiện ca này. Trang web thật hầu như đều nén, nên phải bật tùy chọn này. Liên quan có ở bài gzip và brotli khi web scraping.

Giá trị thay thế cố định

Dùng cùng một giá trị thay thế cho mọi cassette, để diff trong kho mã không đổi vô cớ. Không dùng chuỗi ngẫu nhiên. Với request được phát lại, header đã bị thay trong cassette, nên test không cần token thật. Máy CI chạy không cần biết bí mật nào.

Thứ tự lọc so với matcher

Bộ lọc chạy trên request trước khi so và ghi. Phép thử sau của lab cho thấy một request mang token khác vẫn phát lại được, vì header không thuộc matcher mặc định. Khi thêm headers vào match_on, phép so vẫn khớp, vì cả hai bên đều đã bị thay bằng LOC. Điều này tiện, nhưng cũng nghĩa là test không kiểm token thật có được gửi đi hay không.

Chọn record_mode và tách cassette test khỏi dữ liệu thật trong VCR.py cassette cho crawler test

Record mode là công tắc quyết định test có gọi mạng hay không. Chọn sai là mất hết lợi ích.

Bốn mode theo tài liệu

Bốn mode ghi

Bốn mode ghi

Tài liệu mô tả once là mặc định: ghi nếu chưa có cassette, phát lại nếu đã có, và báo lỗi với request mới. New_episodes ghi thêm request mới. None không bao giờ ghi. All luôn ghi lại. Lab chạy lần lượt bốn mode trên một cassette, có một request mới ở ba lượt sau.

Bảng: Bốn mode theo tài liệu

Mode

Origin nhận thêm

Bản ghi trong lúc chạy

Lượt phát lại

once, chưa có tệp

1

1

0

new_episodes

1

2

1

all

2

4

0

none

0

2

2

Đọc bảng

Lượt once gọi một lần rồi ghi. Lượt new_episodes phát lại request cũ và gọi thật request mới. Lượt all gọi thật cả hai. Lượt none không chạm origin, phát lại hai bản ghi. Mode all giữ 4 bản ghi trong bộ nhớ lúc chạy nhưng lượt none sau đó chỉ thấy 2, tức tệp ghi lại có 2 interaction. Phát hiện này đúng với lần chạy của lab.

Mode nào cho CI

Trên CI phải dùng none. Với once, nếu ai đó quên commit cassette thì CI âm thầm gọi mạng và ghi tệp mới. Với none, test thất bại rõ ràng. Lab khuyên đặt mode qua biến môi trường, mặc định none, và chỉ người ghi mới đặt once.

import os
 
MODE = os.environ.get("VCR_RECORD_MODE", "none")
SAFE = vcr.VCR(cassette_library_dir="tests/cassettes",
               filter_headers=[("authorization", "LOC"), ("cookie", None)],
               filter_query_parameters=[("api_key", "LOC")],
               before_record_response=scrub,
               decode_compressed_response=True,
               record_mode=MODE)

Cấu hình này gom vào một module test dùng chung. Mọi test import từ đây, nên không ai quên bộ lọc.

Mỗi test một cassette

Một cassette cho một test giúp diff nhỏ, dễ xem lại. Tên cassette trùng tên test. Với parser, mỗi loại trang nên có một cassette: danh mục, chi tiết, trang rỗng, trang lỗi. Ghép mọi thứ vào một tệp lớn khiến việc cập nhật rất đau đầu.

Tách khỏi dữ liệu thật

Cassette nằm dưới thư mục tests, không chung với thư mục dữ liệu. Trong code sản xuất không import vcr. Một phép kiểm trên CI có thể tìm import này trong mã chính. Ranh giới rõ ràng ngăn việc ai đó tái dùng cassette thay cho nguồn thật.

Nội dung trang có bản quyền

Cassette chứa HTML thật của trang được ghi. Commit vào kho công khai là phân phối lại nội dung của người khác. Với kho công khai, anh em cắt HTML về đoạn tối thiểu cho test, hoặc dùng trang của chính mình. Dữ liệu cá nhân trong trang cũng phải được loại trước khi commit.

Kích thước cassette

Cassette của lab chỉ khoảng 1 KB vì trang rất nhỏ. Trang thật có thể vài trăm KB mỗi tệp, và kho mã phình lên. Before_record_response có thể cắt phần không dùng. Cẩn thận rằng parser phải vẫn chạy trên phần còn lại, và test nên chạy lại sau khi cắt.

Ngày trong header

Cassette lưu Date và các header thay đổi theo lần ghi. Lần ghi lại sẽ làm diff to hơn cần thiết. Lọc các header không dùng đến giúp lịch sử sạch. Lab chưa lọc Date, X-Request-Id vẫn nằm trong cassette, và phần replay vẫn trả req-12 của lần ghi.

Lọc token, cookie và proxy credentials trước khi ghi với VCR.py cassette cho crawler test

Phần này nói về phát hiện đáng giá nhất: thông tin đăng nhập proxy.

Crawler đi qua proxy có gì khác

Mật khẩu ẩn trong base64

Mật khẩu ẩn trong base64

Crawler thật thường cấu hình proxy dạng http://user:pass@host:port. Với URL HTTP, requests đưa thông tin đó vào header Proxy-Authorization ở dạng Basic base64. Lab dựng proxy cục bộ, gửi request tới http://shop.lab.test/catalog?page=5 và proxy ghi nhận header Basic dS1sYWI6cC1sYWItOTk5.

Cassette thô chứa gì

Lab tìm chuỗi mật khẩu p-lab-999 trong px_raw.yaml và không thấy. Người soát lướt qua sẽ yên tâm. Nhưng tìm chuỗi base64 thì có: header Proxy-Authorization nằm nguyên trong cassette. Giải mã base64 chỉ là một dòng lệnh. Tìm mật khẩu dạng chữ không ra không có nghĩa là an toàn.

import base64
 
print(base64.b64decode("dS1sYWI6cC1sYWItOTk5").decode())
# u-lab:p-lab-999

Output là tài khoản lab đặt ra. Với proxy thật, đây là thông tin đăng nhập trả phí, lộ ra thì người khác dùng được.

Cách lọc

Thêm proxy-authorization vào filter_headers. Lab chạy lại với px_safe.yaml và không còn header nào. Đây là dòng anh em phải có trong mọi cấu hình VCR cho crawler dùng proxy. Tên header không phân biệt hoa thường theo cách lab chạy.

S = vcr.VCR(cassette_library_dir="cassettes",
            filter_headers=["proxy-authorization"])
with S.use_cassette("px_safe.yaml"):
    requests.get("http://shop.lab.test/catalog?page=5", proxies=PX, timeout=5)

Biến PX chứa chuỗi proxy cục bộ. Cassette vẫn ghi URL đích, không ghi địa chỉ proxy, vì lab tìm 9599 trong tệp và không thấy. Host proxy không lộ trong ca này.

Proxy HTTPS khác HTTP

Với URL HTTPS, requests mở đường hầm CONNECT và header Proxy-Authorization đi ở bước đó, không phải trong request bên trong. Lab chỉ kiểm URL HTTP. Với HTTPS, hành vi ghi có thể khác, anh em nên quét cassette thật thay vì suy đoán. Cách đặt chuỗi proxy có ở bài định dạng proxy.

Replay không cần proxy

Lab phát lại px_raw.yaml hai lần: một lần vẫn truyền proxies, một lần không. Cả hai đều trả 200, và proxy không nhận thêm request nào. Vậy CI không cần proxy, không cần tài khoản proxy. Đây là lợi ích thực tế: test parser tách khỏi hạ tầng mạng.

Mật khẩu trong thân POST

Lab gửi form đăng nhập với password. Cassette post.yaml chứa nguyên mật khẩu. Thêm filter_post_data_parameters với cặp password, LOC thì post_safe.yaml chỉ còn password=LOC. Bộ lọc này là vị trí thứ năm cần nhớ, sau header, query, cookie và thân response.

Máy quét cuối cùng

Vì có nhiều vị trí, lab viết hàm quét cả thư mục cassette theo danh sách mẫu. Chạy trên 8 tệp: raw.yaml lộ bốn loại, px_raw.yaml và post_safe.yaml còn chuỗi base64, post.yaml lộ hai loại, các tệp khác sạch. Post_safe.yaml là bài học nhỏ: nó lọc mật khẩu form nhưng quên Proxy-Authorization.

import pathlib
 
def scan(root, pats):
    bad = {}
    for p in sorted(pathlib.Path(root).glob("*.yaml")):
        x = p.read_text(encoding="utf-8")
        h = [k for k in pats if k in x]
        if h:
            bad[p.name] = h
    return bad
 
def test_no_secret_in_cassette():
    assert scan("tests/cassettes", ["Proxy-Authorization", "Set-Cookie", "Bearer "]) == {}

Test này chạy trên CI. Mẫu tìm theo tên trường, không theo giá trị bí mật, nên không cần đưa bí mật thật vào test. Anh em bổ sung mẫu theo dự án, ví dụ tiền tố token.

Khi đã lỡ commit

Xóa tệp khỏi nhánh hiện tại là chưa đủ, vì bí mật còn trong lịch sử. Việc đầu tiên là thu hồi và đổi token, mật khẩu proxy. Sau đó mới dọn lịch sử nếu quy trình cho phép. Đổi bí mật quan trọng hơn dọn lịch sử.

Hook trước khi commit

Đặt máy quét vào pre-commit để chặn ngay trên máy lập trình viên. Các công cụ quét bí mật có sẵn cũng giúp, nhưng chuỗi base64 trong header không phải lúc nào cũng được nhận ra. Một luật riêng cho Proxy-Authorization rất rẻ và đáng tin hơn.

Replay cassette với mạng tắt trong test parser của VCR.py cassette cho crawler test

Không tin vào cấu hình, lab đo trực tiếp.

Cách chứng minh không gọi mạng

Replay không chạm mạng

Replay không chạm mạng

Lab dùng hai bằng chứng độc lập. Bằng chứng một: bọc socket.socket.connect để đếm số lần kết nối. Bằng chứng hai: origin có bộ đếm, đọc trước và sau. Nếu cả hai cùng bằng 0 thì test thật sự không ra mạng.

import socket
 
def test_replay_khong_cham_mang():
    h0 = hits()
    real = socket.socket.connect
    n = {"c": 0}
 
    def spy(self, a):
        n["c"] += 1
        return real(self, a)
 
    socket.socket.connect = spy
    try:
        with SAFE.use_cassette("safe.yaml", record_mode="none") as c:
            r = requests.get(B + "/catalog?page=1", headers=H)
            print(r.status_code, parse(r.text), c.play_count, c.all_played)
    finally:
        socket.socket.connect = real
    print(n["c"], hits() - h0)

Output thật: 200, ['A11', 'A12'], play_count 1, all_played False, rồi 0 kết nối và origin tăng 0. All_played là False vì cassette có hai interaction mà test mới phát một.

Parser chạy trên dữ liệu phát lại

Hàm parse dùng lxml đọc data-sku của các thẻ li. Dữ liệu từ cassette đi qua đúng đường code đó. Test kiểm parser ở mức thật: từ HTML tới danh sách sku. Đó chính là thứ cần ổn định, và cũng là phạm vi giá trị của cassette.

from lxml import html
 
def parse(t):
    return [x.get("data-sku") for x in html.fromstring(t).xpath('//li[@class="p"]')]

Khi website đổi cấu trúc, test cassette vẫn xanh, vì HTML đã đông cứng. Đó không phải lỗi của VCR.py, nhưng nhóm phải có một kênh khác để phát hiện thay đổi.

Cookie sau replay

Sau khi lọc, response phát lại không có Set-Cookie, lab đọc ra None. Nếu code test phụ thuộc cookie để đi tiếp, bước sau sẽ khác thực tế. Cách xử lý là thay cookie bằng giá trị giả thay vì xóa, để code vẫn nhận được cookie. Đây là sự đánh đổi cần quyết định theo từng test.

Kiểm mọi interaction được dùng

Thuộc tính all_played cho biết còn bản ghi thừa không. Cassette dài ra theo thời gian có thể chứa request không còn dùng. Một assert c.all_played trong test giúp cassette đúng với code hiện tại. Lab để lại bản ghi thừa có chủ ý nên giá trị là False.

Httpx được hỗ trợ

Lab ghi và phát lại với httpx.get: response 200, parse được ['A41', 'A42'], lúc phát lại origin tăng 0, cassette không lộ token hay cookie. Vậy cấu hình lọc dùng chung cho requests và httpx. Client bất đồng bộ lab chưa kiểm.

Tắt mạng thật trên CI

Có thể tăng độ tin cậy của phép đo bằng cách chạy test trong môi trường không có mạng. Plugin pytest như pytest-socket chặn socket ở mức tiến trình, lab chưa dùng. Bọc socket như trên đủ làm bằng chứng cho một bài kiểm. Môi trường CI không mạng là phép thử mạnh hơn.

Tốc độ

Tám test của lab chạy 3.33 giây, trong đó có lần ghi thật và lần tạo process. Test parser qua cassette thường rất nhanh so với gọi website thật. Khác biệt lớn nhất nằm ở độ ổn định, vì không còn chờ mạng hay lỗi 429 ngẫu nhiên.

Kiểm mismatch request và cách cập nhật cassette có chủ đích với VCR.py cassette cho crawler test

Lỗi không khớp là tín hiệu có ích, miễn là anh em đọc nó đúng.

Thông báo khi không khớp

Lab gọi page=2 với cassette chỉ có page=1 và mode none. VCR.py ném CannotOverwriteExistingCassetteException. Thông báo của bản 9.4.2 nói rõ không có interaction nào khớp, bản gần nhất khác ở query, liệt kê match_on, số interaction đã ghi, đã phát, rồi gợi ý cách sửa.

from vcr.errors import CannotOverwriteExistingCassetteException
import pytest
 
def test_khong_khop():
    with SAFE.use_cassette("safe.yaml", record_mode="none"):
        with pytest.raises(CannotOverwriteExistingCassetteException):
            requests.get(B + "/catalog?page=2", headers=H)

Test này đạt, tức hành vi đúng như mong muốn: request lạ không lọt ra mạng. Đây là lưới an toàn cho crawler có URL sinh động.

Matcher mặc định

Thông báo liệt kê match_on: method, scheme, host, port, path, query. Header và body không nằm trong danh sách. Vì vậy request với token khác vẫn khớp, như lab đã thấy. Với API POST, cần thêm body để phân biệt các request khác thân.

Đọc cùng URL hai lần

Lab gọi cùng URL hai lần với mode none. Lần hai báo lỗi: request khớp một interaction nhưng nó đã được phát rồi. Crawler có retry hay gọi lại sẽ gặp đúng lỗi này. Tham số allow_playback_repeats=True cho phép phát lại nhiều lần. Bật tùy chọn này có thể che một lỗi gọi lặp, nên chỉ bật khi hành vi đó là chủ ý.

Tham số thay đổi mỗi lần

URL có timestamp hay nonce sẽ không bao giờ khớp. Thông báo gợi ý nới match_on hoặc dùng before_record_request để chuẩn hóa. Lab không có ca này. Với crawler thêm tham số chống cache, anh em chuẩn hóa tham số đó trong before_record_request.

Cập nhật cassette có chủ đích

Khi website đổi, quy trình là: xóa cassette liên quan, đặt VCR_RECORD_MODE=once, chạy đúng test đó, chạy máy quét bí mật, xem diff, commit. Thông báo của 9.4.2 còn nhắc record_mode all kèm drop_unused_requests=True để ghi lại từ đầu. Không chạy mode all cho cả bộ test, vì khó soát mọi thứ thay đổi.

Lịch ghi lại

Cassette lâu năm trở thành bản mô tả website trong quá khứ. Nhóm nên có job định kỳ ghi lại trên nhánh riêng và xem diff, hoặc một test gọi mạng thật chạy theo lịch. Scrapy contracts ở đầu bài là một ví dụ của lớp kiểm thật này. Hai lớp cùng tồn tại thì không bên nào phải làm việc của bên kia.

Khi nào không nên dùng cassette

Với logic retry, timeout, mất kết nối, cassette không mô phỏng được thời gian trễ hay socket đứt giữa chừng. Bài WireMock trong cùng cụm xử lý nhóm đó. Cassette hợp nhất cho parser và luồng request xác định. Dùng sai công cụ thì test xanh mà không kiểm được gì.

Vai trò proxy trong test

Test không cần proxy thật. Proxy chỉ tham gia lúc ghi, nếu trang cần IP hoặc vùng cụ thể để trả nội dung đúng. Bài Web Scraping proxy là gì nói về vai trò đó.

Nếu lúc ghi anh em cần IP Việt Nam ổn định, có thể mua proxy tại trang chủ ProxyVN, và nhớ lọc Proxy-Authorization.

Quy trình ghi cassette đầu tiên

Với một trang mới, anh em làm theo thứ tự cố định. Bước một viết test parser, chạy mode once với tài khoản thử, không dùng tài khoản thật. Bước hai đọc tệp YAML bằng mắt một lần, tìm mọi trường lạ như mã phiên hay email. Bước ba chạy test quét. Bước bốn đổi về mode none rồi chạy lại để xác nhận phát lại đạt. Bước năm mới commit.

Dùng tài khoản thử khi ghi

Nếu trang cần đăng nhập, dùng tài khoản riêng cho test, quyền thấp, có thể thu hồi. Như vậy dù bộ lọc sót một chỗ, thiệt hại vẫn nhỏ. Tài khoản thật của nhân sự không bao giờ nên đi qua lần ghi. Nguyên tắc này rẻ hơn mọi bộ lọc.

Dữ liệu cá nhân trong response

Bộ lọc header không bắt được email hay tên người dùng trong HTML. Trang tài khoản, trang đơn hàng hay trang bình luận đều có thể chứa thông tin đó. Với nhóm trang này, anh em viết thêm luật thay thế trong before_record_response, hoặc chỉ giữ fixture tự dựng bằng tay. Dữ liệu người khác trong kho mã là rủi ro thật, không chỉ là chuyện gọn gàng.

Phân tầng test cho crawler

Một bộ test crawler hợp lý có ba tầng. Tầng parser chạy trên cassette, nhanh, chạy mọi commit. Tầng lỗi mạng chạy trên origin giả. Tầng kiểm thật gọi website theo lịch, số ít và đúng nhịp. Mỗi tầng trả lời một câu hỏi khác, và khi một tầng đỏ, anh em biết ngay nên tìm lỗi ở đâu.

Đọc diff cassette

Khi ghi lại, diff cho thấy trang đã đổi gì. Đây là thông tin có ích, nhất là với các trang đổi class CSS. Người review nên đọc diff này như đọc code, không bấm chấp nhận cho nhanh. Một dòng token lọt vào cũng sẽ hiện ngay ở đây, nên bước đọc diff là lớp kiểm cuối cùng trước khi merge vào nhánh chính của nhóm. Đừng bỏ qua bước này chỉ vì cassette quá dài hoặc đang vội.

Khóa phiên bản

Lab chạy Python 3.12.10, vcrpy 9.4.2, requests 2.34.2, httpx 0.28.1, pytest 9.2.0. Định dạng cassette có thể đổi giữa các bản lớn, nên ghim phiên bản trong tệp phụ thuộc dev. Nâng bản thì chạy toàn bộ test trước khi merge.

Đầu ra nghiệm thu cassette fixture đã lọc token và test replay không tạo request mạng của VCR.py cassette cho crawler test

Mục này chốt tài sản bàn giao và danh sách kiểm.

Kết quả lab

Nghiệm thu cassette

Nghiệm thu cassette

Bảng: Kết quả lab

Test

Kết quả thật

Ghi thô

Lộ 4 trên 4 bí mật

Ghi đã lọc

Lộ 0 trên 4

Replay

200, sku đúng, 0 connect, origin +0

Sai query

Ném CannotOverwriteExistingCassetteException

Đọc hai lần

Lỗi, trừ khi allow_playback_repeats

httpx

Ghi và phát lại đạt

Proxy thô

Lộ Proxy-Authorization base64

Proxy đã lọc

Không còn header

Danh sách kiểm

  • CI: record_mode none.
  • Header: lọc authorization, cookie, proxy-authorization.
  • Query và body: lọc tham số khóa, mật khẩu form.
  • Response: gỡ Set-Cookie, token trong thân, bật giải nén.
  • Quét: test quét cassette theo tên trường trên CI.
  • Chứng minh: có phép đếm kết nối hoặc chạy không mạng.

Câu kết luận được phép viết

Từ lab có thể viết rằng cassette mặc định lộ bí mật ở nhiều vị trí. Mật khẩu proxy nằm dạng base64 nên tìm chuỗi thường không thấy. Replay đo được 0 kết nối. Không nên viết rằng cấu hình này đủ cho mọi website, vì lab chỉ dùng origin cục bộ, không có nén, không có HTTPS.

Những gì lab chưa chạy

Lab chưa thử HTTPS qua CONNECT, chưa thử thân nén thật, chưa thử client bất đồng bộ của httpx, chưa thử aiohttp, chưa thử Scrapy, pytest-recording, pytest-socket. Lab chưa thử before_record_request cho tham số thay đổi. Danh sách trên là việc nên chạy trên dự án thật.

Cassette là fixture đông cứng, không phải nguồn dữ liệu. Tìm mật khẩu không ra chưa có nghĩa là an toàn, hãy tìm tên header.

VCR.py cassette cho crawler test: câu hỏi thường gặp

Các câu dưới đây gom những điểm hay bị hỏi.

Có commit cassette vào kho mã không?

Có, sau khi lọc và quét. Với kho công khai, cần cân nhắc thêm bản quyền nội dung trang.

Record mode nào cho CI?

None, để request lạ làm test thất bại thay vì âm thầm ra mạng.

Vì sao tìm mật khẩu proxy trong cassette không thấy?

Vì nó nằm trong header Proxy-Authorization dạng base64. Hãy tìm tên header.

Cassette có phát hiện website đổi không?

Không. Cần test gọi thật định kỳ hoặc ghi lại theo lịch.

Có dùng cassette làm dữ liệu cho pipeline được không?

Không nên. Đó là fixture đông cứng và đã bị lọc.

Crawler có retry thì sao?

Mặc định lần phát thứ hai báo lỗi. Ghi đủ các lần gọi hoặc bật allow_playback_repeats có chủ ý.

Lỡ commit token thì làm gì?

Thu hồi và đổi token trước, rồi mới xử lý lịch sử kho mã.

Có cần proxy khi chạy test không?

Không. Lab phát lại đạt dù không truyền proxy.

VCR.py cassette cho crawler test và những điều cần nhớ

VCR.py cassette cho crawler test giữ test parser ổn định khi anh em lọc đủ năm vị trí bí mật, quét cassette bằng test riêng, đặt record_mode none trên CI và đo được không có kết nối ra ngoài. Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam nhắc anh em lọc Proxy-Authorization khi dùng VCR.py cassette cho crawler test.

TIN TỨC LIÊN QUAN