MongoDB upsert dữ liệu crawl: unique key và đọc matched, modified
MongoDB upsert dữ liệu crawl là cách ghi mỗi item theo khóa nghiệp vụ: chưa có thì thêm, có rồi thì cập nhật. Chạy lại lượt crawl sẽ không sinh bản trùng, miễn là khóa đúng và có unique index. ProxyVN chạy pymongo trên thư viện giả lập mongomock để đọc kết quả từng ca, và ghi rõ phần giả lập không thay được máy chủ MongoDB thật.
MongoDB upsert dữ liệu crawl: 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à giới hạn.
Bài toán cần giải
Một crawler chạy mỗi giờ, ghi sản phẩm vào MongoDB bằng insert. Sau một tuần, một sản phẩm có 168 bản ghi. Nhóm đổi sang upsert, nhưng lọc theo URL thô, nên cùng sản phẩm với tham số theo dõi khác nhau vẫn thành nhiều dòng. Họ cần một quy tắc khóa, một chỉ mục, và cách đọc kết quả để biết lượt crawl thực sự thay đổi gì.
Đầu ra nghiệm thu
Đầu ra gồm fixture bản ghi trùng và bản ghi cập nhật, một unique index, và bảng kết quả matched, modified cho từng ca. Kèm theo là cách xử lý DuplicateKeyError và cách giữ first_seen không bị ghi đè.
Ranh giới với bài khác
Bài không phải hướng dẫn NoSQL tổng quan và không so sánh MongoDB với cơ sở dữ liệu khác. Phần ETL có nhắc MongoDB ở bài khác chỉ dừng ở mức nêu tên công cụ. Bài này đi vào đúng một thao tác: updateOne với upsert theo unique key.
Tách danh tính item khỏi phiên proxy
Đây là lỗi thiết kế hay gặp. Khóa item phải sinh từ dữ liệu của website, không được dính tới proxy nào đã tải nó. Nếu khóa chứa IP, tên phiên hay thời điểm, cùng sản phẩm tải qua hai proxy sẽ thành hai bản ghi. Thông tin đường tải nên là trường phụ, không nằm trong khóa.
Giới hạn của lab
Nói rõ ngay. Lab không chạy máy chủ MongoDB. Bài dùng mongomock 4.3.0, thư viện giả lập chạy trong bộ nhớ, với pymongo 4.19.0. Mongomock mô phỏng khá nhiều thao tác, nhưng không có tranh chấp đồng thời thật, không có replica set, không có write concern, không có khóa ở tầng lưu trữ.
Mongomock khác máy chủ thật ở đâu
Bộ đếm matched, modified trong lab do mongomock tính. Mã lỗi 11000 cũng do nó đặt, bám theo mã của MongoDB. Lab có một lỗi thật của mongomock: bulk_write với pymongo 4.19 báo TypeError vì tham số sort lạ, nên phần ghi hàng loạt không chạy được. Bài ghi lại lỗi đó ở phần sau.
Vậy kết quả dùng được tới đâu
Quy tắc trong bài lấy từ tài liệu chính thức của MongoDB. Lab giúp anh em thấy cấu trúc mã và cách đọc kết quả. Hành vi đồng thời, hiệu năng và chi tiết thông báo lỗi phải kiểm trên máy chủ thật. Chỗ nào chỉ là giả lập, bài ghi rõ.
Ai nên đọc bài này
Người đang lưu kết quả crawl vào MongoDB. Người thấy số bản ghi tăng nhanh hơn số sản phẩm. Người muốn biết một lượt crawl đã đổi bao nhiêu dòng thay vì chỉ biết đã chạy xong.
Lưu snapshot crawl qua upsert và unique key với MongoDB upsert dữ liệu crawl
Phần này tóm tắt tài liệu, vì câu quan trọng nhất nằm ngay trong phần tham số.
updateOne và tùy chọn upsert
Trang db.collection.updateOne mô tả tùy chọn upsert: tạo tài liệu mới nếu không có tài liệu nào khớp filter. Khi tạo, tài liệu mới dựa trên điều kiện filter cộng với thao tác cập nhật.
db.items.updateOne(
{ source: "shop", item_id: "p1" },
{ $set: { price: 90, title: "Ao" } },
{ upsert: true }
)
Lệnh trên viết cho mongosh, là dạng ngắn nhất của bài toán. Filter chính là khóa, phần $set là dữ liệu.
Câu cảnh báo trong tài liệu
Tài liệu ghi muốn tránh nhiều lần upsert thì phải bảo đảm các trường trong filter được đánh unique index. Upsert một mình không chống trùng khi có nhiều worker. Hai worker cùng tìm không thấy, cùng chèn. Chỉ unique index mới chặn được.
Kết quả trả về

Ba bộ đếm
Tài liệu liệt kê các trường: matchedCount là số tài liệu khớp, modifiedCount là số tài liệu đã đổi, upsertedId là _id của tài liệu vừa tạo, upsertedCount là số tài liệu chèn. Trong pymongo, các trường tương ứng là matched_count, modified_count và upserted_id.
mongosh |
pymongo |
Ý nghĩa với crawl |
|---|---|---|
matchedCount |
matched_count |
Item đã tồn tại |
modifiedCount |
modified_count |
Dữ liệu thực sự đổi |
upsertedId |
upserted_id |
Item mới, lần đầu thấy |
upsertedCount |
không có trường riêng |
Số item mới trong lệnh |
Unique index
Trang Unique Indexes nêu unique index bảo đảm trường được đánh chỉ mục không chứa giá trị trùng. Với chỉ mục ghép, tổ hợp giá trị phải là duy nhất. Tài liệu cũng nêu MongoDB không tạo được unique index nếu dữ liệu hiện có đã vi phạm.
db.items.createIndex({ source: 1, item_id: 1 }, { unique: true })
Với crawl nhiều nguồn, chỉ mục ghép theo nguồn và mã item là lựa chọn tự nhiên. Mã sản phẩm của hai website có thể trùng nhau, nên chỉ dùng item_id là chưa đủ.
Trường thiếu trong unique index
Tài liệu nêu nếu tài liệu thiếu trường trong unique index một trường, chỉ mục lưu giá trị null, và vì tính duy nhất nên chỉ một tài liệu được phép như vậy. Lab thử chèn hai tài liệu thiếu item_id: lần hai ném DuplicateKeyError. Parser lỗi trả về item không có mã sẽ đụng đúng ca này.
Upsert trong giao dịch và sharding
Tài liệu nêu từ MongoDB 8.0, upsert trên collection sharded không cần đủ shard key trong filter, và có mục riêng cho upsert trong giao dịch. Lab không có các môi trường này. Nếu kho của anh em là cụm sharded, hãy đọc kỹ mục đó trước khi chọn khóa.
Ví dụ về lỗi khi upsert
Tài liệu có ví dụ thú vị: upsert đặt _id trong $set trên tài liệu đã có _id khác sẽ báo lỗi và được bọc trong try catch. Bài học cho crawl: đừng đổi _id trong $set. Hãy để MongoDB sinh _id, hoặc đặt _id một lần trong filter.
Đặt unique index theo business key item crawl trong MongoDB upsert dữ liệu crawl
Phần này chọn khóa, quyết định quan trọng nhất của cả thiết kế.
Business key là gì
Business key là thứ định danh một item theo nghĩa nghiệp vụ, không đổi giữa các lượt crawl. Với sản phẩm là nguồn cộng mã sản phẩm trên website. Với bài báo có thể là URL chuẩn. Khóa tốt không đổi khi giá đổi, tiêu đề đổi, hay proxy đổi.
Lựa chọn khóa |
Ví dụ |
Vấn đề |
|---|---|---|
URL thô |
/p1?utm=a |
Tham số theo dõi tách một item thành nhiều dòng |
URL chuẩn hóa |
/p1 |
Dùng được nếu website có URL ổn định |
Mã trên website |
shop, p1 |
Tốt nhất khi trích được |
Băm toàn bộ nội dung |
sha256 của HTML |
Đổi giá là thành item mới |
Khóa kèm proxy hoặc giờ |
p1, 10.0.0.5 |
Mỗi lượt một bản, mất ý nghĩa |
Ca khóa sai trong lab

Khóa sai
Lab upsert ba URL của cùng một sản phẩm, lọc theo URL thô. Kết quả có ba bản ghi cho một sản phẩm. Cấu hình không sai, mã không lỗi, chỉ khóa sai.
for u in ["http://s/p1?utm=a", "http://s/p1?utm=b", "http://s/p1"]:
y.update_one({"url": u}, {"$set": {"price": 100}}, upsert=True)
print(y.count_documents({})) # 3
Output là 3. Không có lỗi nào báo cho anh em biết, vì về mặt kỹ thuật ba URL đó khác nhau. Chỉ đếm số item mới thấy.
Chuẩn hóa trước khi đặt khóa
Quy tắc chuẩn hóa phải chạy trong parser, trước khi tới bước ghi. Bỏ tham số theo dõi, thống nhất chữ thường ở host, thống nhất dấu gạch chéo cuối. Nếu trang có thẻ canonical và anh em tin được nó, dùng làm cơ sở. Viết quy tắc này một chỗ và test riêng.
Tạo index trước khi crawl
Tạo chỉ mục khi collection còn trống là dễ nhất. Với pymongo, lệnh create_index gọi lúc khởi động là an toàn vì nó không làm gì nếu chỉ mục đã có.
c.create_index([("source", 1), ("item_id", 1)], unique=True, name="uq_item")
Lab trả về tên uq_item. Đặt tên rõ ràng giúp đọc thông báo lỗi dễ hơn.
Thêm unique index vào dữ liệu đã trùng
Lab chèn hai bản ghi trùng rồi tạo unique index, nhận DuplicateKeyError. Khớp với tài liệu: dữ liệu vi phạm thì không tạo được. Vậy nên phải dọn trùng trước. Pipeline aggregate gom theo khóa, giữ bản mới nhất, xóa phần còn lại. Việc này nên chạy trên bản sao trước.
db.items.aggregate([
{ $group: { _id: { source: "$source", item_id: "$item_id" },
n: { $sum: 1 }, ids: { $push: "$_id" } } },
{ $match: { n: { $gt: 1 } } }
])
Câu trên chỉ liệt kê nhóm trùng, ProxyVN chưa chạy nó. Đừng xóa trước khi xem kết quả.
Khóa ghép và thứ tự trường
Filter trong updateOne nên dùng đúng các trường của chỉ mục. Trường hay lọc riêng lẻ đặt trước. Với source rồi item_id, câu hỏi tất cả item của một nguồn cũng dùng được chỉ mục này. Thêm trường thừa vào filter có thể làm upsert không khớp và sinh lỗi trùng khóa.
Không đưa trường hay đổi vào filter
Đây là lỗi hay gặp: đưa price vào filter. Giá đổi thì filter không khớp, upsert cố chèn tài liệu mới, unique index chặn lại. Filter chỉ chứa khóa, mọi thứ còn lại nằm trong $set. Hàm tách khóa ở phần sau giải quyết chuyện này.
Giữ _id mặc định hay dùng làm khóa
Có thể đặt _id bằng chuỗi nguồn cộng mã item. Cách đó tiết kiệm một chỉ mục, nhưng _id không đổi được. Nếu quy tắc khóa có thể đổi, chỉ mục riêng linh hoạt hơn. ProxyVN nghiêng về chỉ mục riêng cho crawler còn đang thay đổi.
Dùng updateOne với filter và upsert rõ ràng trong MongoDB upsert dữ liệu crawl
Phần này là mã ghi, chạy thật với pymongo trên mongomock.
Hàm upsert cơ bản
Hàm tách item thành khóa và thân, băm thân để so nội dung, dùng $setOnInsert cho trường chỉ ghi một lần.
import hashlib
def H(x):
return hashlib.sha256(repr(sorted(x.items())).encode()).hexdigest()[:12]
def split(it):
f = {"source": it["source"], "item_id": it["item_id"]}
return f, {k: v for k, v in it.items() if k not in f}
def up(c, it, seen):
f, b = split(it)
return c.update_one(
f,
{"$set": {**b, "hash": H(b)}, "$setOnInsert": {"first_seen": seen}},
upsert=True,
)
Trường first_seen nằm trong $setOnInsert nên chỉ được ghi khi tạo mới. Đây là cách giữ thời điểm lần đầu thấy item.
$set và $setOnInsert
$set chạy mọi lần. $setOnInsert chỉ chạy khi upsert dẫn tới chèn. Một trường không được xuất hiện ở cả hai, MongoDB sẽ báo xung đột. Lab kiểm tra bản ghi sau hai lần ghi: first_seen vẫn là 8 giờ, giá đã là 90.
{'source': 'shop', 'item_id': 'p1', 'price': 90, 'title': 'Ao',
'hash': '17492f9f4d69', 'first_seen': datetime.datetime(2026, 11, 4, 8, 0)}
Bản ghi trên là output thật của lab.
Dùng $set chứ đừng thay cả tài liệu
replace_one với upsert sẽ xóa các trường không có trong tài liệu mới, gồm cả first_seen. Hai nguồn dữ liệu ghi vào cùng tài liệu cũng sẽ xóa của nhau. Với crawl, $set từng trường an toàn hơn. Trường bị mất trên website sẽ không tự xóa khỏi kho, cần xử lý riêng nếu muốn.
Trường bị mất trên website
Trang không còn hiển thị giá thì parser trả None hoặc thiếu trường. Thiếu trường thì $set giữ giá cũ, dữ liệu sai âm thầm. Nên quy định rõ: parser luôn trả đủ trường, giá trị không có là None. Khi đó $set ghi null và lượt sau thấy được.
Thời gian lưu ở múi giờ nào
Lab dùng datetime không có múi giờ cho gọn. Thực tế dùng UTC có múi giờ. pymongo lưu kiểu date theo UTC, nên giờ địa phương không có múi giờ sẽ gây lệch khi đọc lại. Đây là lỗi rất khó thấy trong báo cáo theo ngày.
Kiểm matchedCount, modifiedCount và upsertedId của MongoDB upsert dữ liệu crawl
Phần này đọc kết quả, thứ mà nhiều crawler bỏ qua.
Ba ca cơ bản

Ba lần ghi
Lab ghi cùng một item ba lần theo ba tình huống. Kết quả dưới đây là output thật của mongomock, khớp với mô tả trong tài liệu.
Ca |
matched |
modified |
upserted |
Ý nghĩa |
|---|---|---|---|---|
Item mới |
0 |
0 |
Có |
Lần đầu thấy |
Giống hệt lần trước |
1 |
0 |
Không |
Không có gì đổi |
Giá đổi 100 thành 90 |
1 |
1 |
Không |
Dữ liệu cập nhật |
Đọc modified bằng 0
Giá trị modified bằng 0 với matched bằng 1 là thông tin quý. Nó nghĩa là item vẫn trên website và không đổi. Tổng hợp qua một lượt, anh em có ngay ba con số: mới, đổi, không đổi. Trên máy chủ thật, MongoDB không ghi khi giá trị mới giống giá trị cũ, nên modifiedCount cũng là 0.
Bẫy last_seen
Hầu hết crawler muốn lưu last_seen. Đặt trường này trong $set thì mọi lượt đều có giá trị mới, modified luôn bằng 1. Lab ghi hai lần cùng dữ liệu, lần hai báo modified 1 dù nội dung không đổi.
last_seen lan1 matched 0 modified 0 upserted True
last_seen lan2 matched 1 modified 1 upserted False
Kết quả này làm mất tín hiệu đổi nội dung. Mỗi lượt crawl báo cáo 100 phần trăm đã đổi, không ai tin con số nữa.
Cách giữ cả hai
Chia làm hai bước. Bước một cập nhật có điều kiện hash khác. Không khớp thì bước hai upsert last_seen. Tín hiệu đổi nội dung nằm ở bước một.
def up3(c, it, seen):
f, b = split(it)
h = H(b)
r = c.update_one({**f, "hash": {"$ne": h}},
{"$set": {**b, "hash": h, "changed_at": seen}})
if r.matched_count == 0:
r2 = c.update_one(f,
{"$set": {"last_seen": seen},
"$setOnInsert": {**b, "hash": h, "changed_at": seen}},
upsert=True)
return ("khong doi" if r2.upserted_id is None else "moi"), r2
return "doi", r
Lab chạy ba lần: lần đầu trả moi, lần hai khong doi, lần ba khi giá đổi trả doi.
Lần |
Kết quả |
matched |
modified |
|---|---|---|---|
1 |
moi |
0 |
0 |
2 |
khong doi |
1 |
0 |
3 |
doi |
1 |
1 |
Ở lần hai, modified bằng 0 trong output vì mongomock chưa thấy trường last_seen thay đổi so với lần ghi trước, do cùng một mốc giờ. Với mốc giờ khác, con số này sẽ là 1, nên hàm trả về nhãn thay vì để phía trên tự đọc bộ đếm.
Chi phí của hai bước
Cách trên tốn hai lần gọi với item không đổi. Với lô lớn, anh em có thể đọc trước hash của cả lô bằng một câu find, so ở phía client, rồi ghi phần thực sự đổi. Lab không đo chi phí này. Chọn cách nào tùy tỷ lệ item đổi giữa hai lượt.
Báo cáo mỗi lượt crawl
Sau mỗi lượt nên ghi một dòng tổng hợp vào collection riêng: số mới, số đổi, số không đổi, số lỗi. Lượt nào số mới bằng 0 và số không đổi tụt mạnh thường là website đã đổi cấu trúc, chứ không phải hết hàng.
{"run": "2026-11-04T09", "source": "shop", "new": 12, "changed": 40, "same": 930, "errors": 3}
Dòng trên là mẫu định dạng, các con số là ví dụ chứ không phải dữ liệu đo.
Giữ lịch sử giá
Giá cả là ví dụ dễ thấy nhất, nhưng tên, tồn kho, ảnh cũng có thể đổi theo lượt. Phần này áp dụng cho mọi trường anh em muốn theo dõi.
Upsert chỉ giữ trạng thái mới nhất. Muốn biết giá theo thời gian, ghi thêm một tài liệu vào collection lịch sử khi nhãn là doi. Collection chính nhỏ và đọc nhanh, collection lịch sử chỉ nối thêm. Không nên đẩy mảng lịch sử vào tài liệu chính, vì nó phình không giới hạn.
Item biến mất khỏi website
Upsert chỉ xử lý thứ crawler nhìn thấy. Sản phẩm bị gỡ thì không có lệnh ghi nào chạm tới nó, tài liệu cứ nằm đó như còn bán. Với last_seen, anh em tìm được các item có last_seen cũ hơn hai lượt rồi đánh dấu inactive. Đừng xóa ngay, vì lượt crawl lỗi có thể làm hàng loạt item trông như biến mất.
cutoff = run_started - timedelta(hours=3)
r = c.update_many(
{"source": "shop", "last_seen": {"$lt": cutoff}, "active": {"$ne": False}},
{"$set": {"active": False, "inactive_at": run_started}},
)
print(r.modified_count)
Đoạn trên là ví dụ triển khai, chưa chạy trong lab. Chỉ chạy nó sau một lượt crawl đã hoàn tất tốt, nếu không sẽ đánh dấu nhầm cả nguồn.
Kiểu dữ liệu ổn định
Giá lúc là chuỗi, lúc là số thì hash đổi liên tục, modified bằng 1 mãi mà giá chẳng đổi. Parser nên chuẩn hóa về một kiểu: số cho giá, chuỗi đã bỏ khoảng trắng cho tên. Hàm băm trong lab dùng repr, nên chỉ cần lệch kiểu là ra hash khác. Đây là lỗi hay gặp khi nhiều parser cùng ghi một collection.
Phiên bản schema
Mỗi tài liệu nên có trường schema_version. Khi parser đổi cách tách dữ liệu, số này tăng, và bước đối chiếu biết bản ghi cũ tạo theo quy tắc nào. Không có nó, lượt đầu sau khi sửa parser sẽ làm toàn bộ item báo đổi, và không ai phân biệt được với thay đổi thật trên website.
Tái lập duplicate key khi cập nhật cùng item trong MongoDB upsert dữ liệu crawl
Phần này gom các ca lỗi.
Insert trùng khóa
Lab gọi insert_one với khóa đã có. pymongo ném DuplicateKeyError với mã 11000. Crawler dùng upsert hiếm khi gặp ca này, nhưng các script nạp dữ liệu một lần thì hay gặp.
from pymongo.errors import DuplicateKeyError
try:
c.insert_one({"source": "shop", "item_id": "p1"})
except DuplicateKeyError as e:
print(type(e).__name__, e.code) # DuplicateKeyError 11000
Mã 11000 là mã cần bắt, đừng so chuỗi thông báo.
Đổi khóa thành khóa đã có
Lab cập nhật item p2 sang item_id p1, nhận DuplicateKeyError. Ca này xuất hiện khi website đổi mã sản phẩm rồi có quy trình gộp. Unique index làm đúng việc của nó là chặn lại, và người vận hành phải quyết định gộp thế nào.
Hai worker cùng upsert một item
Trên máy chủ thật, hai worker cùng không thấy item và cùng chèn. Một bên thành công, bên kia nhận lỗi trùng khóa. Tài liệu ghi lời khuyên unique index cho đúng tình huống này. Mongomock chạy trong một tiến trình nên không tái lập được, đây là ca quan trọng nhất mà lab không thử được.
Xử lý lỗi trùng khóa khi upsert
Khi đã có unique index, lỗi trùng khóa trong upsert có nghĩa là bên kia vừa chèn xong. Cách xử lý đơn giản là thử lại một lần, lần này sẽ khớp và thành cập nhật.
def safe_up(c, it, seen, tries=3):
for i in range(tries):
try:
return up(c, it, seen)
except DuplicateKeyError:
if i == tries - 1:
raise
Đoạn này là ví dụ triển khai. Vì mongomock không tạo được tranh chấp, nhánh except chưa từng chạy trong lab.
Không có unique index thì sao
Lab chèn thẳng hai bản ghi trùng vào collection không có chỉ mục, thành hai bản. Upsert đồng thời trên máy chủ thật cũng có thể ra kết quả đó, theo cảnh báo trong tài liệu. Sau khi đã trùng thì không tạo được chỉ mục, nên anh em phải làm chỉ mục từ đầu.
Ghi hàng loạt và lỗi của mongomock
Crawler thật nên gom lô bằng bulk_write với UpdateOne. Lab thử chạy và nhận lỗi thật của thư viện giả lập.
bulk_write mongomock loi: BulkOperationBuilder.add_update() got an unexpected keyword argument 'sort'
Pymongo 4.19 truyền thêm tham số sort, mongomock 4.3.0 chưa biết. Lỗi này nằm ở mongomock, không phải MongoDB. Lab thay bằng update_one tuần tự, nên kết quả bulk trong bài chưa được kiểm chứng.
Mã bulk nên viết thế nào
Với máy chủ thật, mã có dạng dưới đây. ordered=False cho phép các thao tác khác tiếp tục khi một thao tác lỗi. Lỗi sẽ gom trong BulkWriteError, anh em đọc danh sách writeErrors.
from pymongo import UpdateOne
from pymongo.errors import BulkWriteError
ops = [UpdateOne(key(it), {"$set": body(it)}, upsert=True) for it in batch]
try:
r = c.bulk_write(ops, ordered=False)
print(r.matched_count, r.modified_count, r.upserted_count)
except BulkWriteError as e:
for w in e.details["writeErrors"]:
print(w["index"], w["code"])
Đoạn này chưa chạy được trong lab. Các tên hàm key và body là của anh em.
Trùng ngay trong một lô
Một lô có thể chứa cùng item hai lần. Lab chạy sáu thao tác trên ba khóa theo kiểu tuần tự, ra ba tài liệu. Với bulk không thứ tự, hai thao tác cùng khóa có thể chạy theo thứ tự bất kỳ. Khử trùng lô ở phía client trước khi gửi, giữ bản mới nhất.
Kích thước lô
Lô quá lớn chiếm bộ nhớ và làm lỗi khó khoanh vùng. Lô quá nhỏ tốn nhiều lượt mạng. Lab không đo, nên bài không đưa con số. Anh em bắt đầu ở vài trăm, đo thời gian mỗi lô rồi chỉnh.
Đầu ra nghiệm thu của MongoDB upsert dữ liệu crawl
Phần này gom tiêu chí và phần chưa thử.
Danh sách kiểm tra
Thiết kế đạt khi mọi dòng dưới đây đúng.
- Khóa nghiệp vụ không chứa proxy, IP, giờ hay trường hay đổi.
- Có unique index ghép theo nguồn và mã item, tạo trước khi crawl.
- Filter chỉ chứa khóa, dữ liệu nằm trong $set.
- first_seen nằm trong $setOnInsert.
- Có băm nội dung để phân biệt đổi và không đổi.
- Bắt DuplicateKeyError theo mã 11000 và thử lại.
- Có báo cáo số mới, đổi, không đổi mỗi lượt.
Fixture nên giữ trong kho mã

Bộ fixture
Bộ fixture nhỏ gồm năm dòng là đủ: item mới, item giống hệt, item đổi giá, cùng item với URL khác tham số, item thiếu mã. Test chạy mỗi khi sửa parser. Các ca này chạy được trên mongomock, không cần máy chủ, nên đưa vào test tự động rất tiện.
Kiểm lại trên máy chủ thật
Bốn thứ cần chạy lại trên MongoDB thật: nhiều tiến trình upsert cùng khóa, bulk_write có lỗi một phần, modifiedCount khi giá trị không đổi, thời gian ghi với chỉ mục khi dữ liệu lớn. Mongomock không cho biết gì về bốn thứ này.
Đọc lại dữ liệu sau khi ghi
Sau một lượt, lấy ngẫu nhiên vài chục item và so với trang gốc đã lưu. Bộ đếm chỉ nói có ghi hay không, nó không nói giá trị có đúng hay không. Parser tách nhầm giá khuyến mãi với giá gốc vẫn cho modified đẹp đẽ. Một lần so tay mỗi tuần bắt được lỗi mà không số liệu nào báo trước.
Những gì lab chưa chạy
Chưa chạy máy chủ MongoDB, chưa thử tranh chấp đồng thời, chưa chạy bulk_write, chưa có replica set, write concern, sharding, giao dịch, chưa đo hiệu năng. Câu aggregate tìm trùng và các hàm safe_up, mã bulk là ví dụ chưa chạy.
Write concern cho dữ liệu crawl
Mặc định đủ dùng với phần lớn crawl, vì mất một item có thể lấy lại ở lượt sau. Tài liệu updateOne có tham số writeConcern cho trường hợp cần chắc hơn. Lab không có replica set nên không thử. Dữ liệu tính tiền thì khác, nhưng đó không phải bài toán của bài này.
Xóa dữ liệu cũ theo thời gian
Collection lịch sử phình đều theo thời gian. MongoDB có TTL index để tự xóa tài liệu quá hạn, phù hợp với nhật ký lượt crawl. Lab không thử vì mongomock không chạy tiến trình nền đó. Collection chính thì không nên dùng TTL, vì item không đổi vẫn còn giá trị.
Proxy ở đâu trong luồng này
Proxy chỉ xuất hiện ở bước tải, trước parser. Khi đã có item, MongoDB không cần biết nó đi qua đường nào. Với website được phép thu thập, có lúc cần IP tại Việt Nam để nhận đúng nội dung trong nước. Khi đó anh em có thể mua proxy giá rẻ tại trang chủ ProxyVN. Proxy không chống được bản ghi trùng. Vai trò của nó có ở bài Web Scraping proxy là gì, cách ghi chuỗi cấu hình ở bài định dạng proxy.
Lưu thông tin proxy để điều tra
Nếu muốn biết item tải qua đường nào, ghi vào trường phụ fetched_via trong collection nhật ký, không phải trong khóa, cũng không phải trong tài liệu chính. Đặt vào tài liệu chính sẽ làm modified luôn bằng 1, y hệt bẫy last_seen. Không ghi tên đăng nhập hay mật khẩu proxy vào đây.
MongoDB upsert dữ liệu crawl: câu hỏi thường gặp
Các câu dưới đây gom những điểm anh em hay hỏi.
Upsert có đủ để chống trùng không?
Không đủ khi có nhiều worker. Tài liệu yêu cầu các trường trong filter có unique index. Upsert một mình vẫn có thể tạo hai bản khi hai tiến trình cùng chèn.
Vì sao modified_count bằng 0 dù vừa ghi?
Vì dữ liệu ghi giống dữ liệu cũ. Đó là tín hiệu tốt: item không đổi. Lab ghi hai lần cùng nội dung, lần hai báo matched 1, modified 0.
Lưu last_seen thế nào mà không mất tín hiệu đổi?
Tách hai bước. Cập nhật nội dung với điều kiện hash khác, rồi mới cập nhật last_seen. Đặt last_seen chung $set thì modified luôn bằng 1, lab đã thấy.
Có nên dùng URL làm khóa không?
Chỉ khi URL đã chuẩn hóa và ổn định. Lab cho thấy ba URL khác tham số thành ba bản ghi cho cùng một sản phẩm.
Mongomock có thay được MongoDB khi test không?
Dùng tốt cho logic đơn giản, không dùng được cho đồng thời và hiệu năng. Lab còn gặp lỗi bulk_write với pymongo 4.19, nên đừng tin nó cho mọi thao tác.
Proxy có ảnh hưởng tới khóa không?
Không được ảnh hưởng. Khóa sinh từ dữ liệu website. Nếu khóa dính với proxy, mỗi đường tải sẽ tạo thêm bản ghi.
MongoDB upsert dữ liệu crawl và những điều cần nhớ
MongoDB upsert dữ liệu crawl đúng khi filter chỉ chứa khóa nghiệp vụ và khóa đó có unique index. Đọc matched với modified cho biết lượt crawl mới, đổi hay giữ nguyên, nên đừng để last_seen làm nhiễu tín hiệu. Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam khuyên anh em kiểm lại MongoDB upsert dữ liệu crawl trên máy chủ thật.