Theo dõi giá siêu thị bằng Playwright
(sakisv.net)- Vào tháng 12/2022, trong bối cảnh lạm phát cao, đã xây dựng pipeline pricewatcher.gr để theo dõi hằng ngày biến động giá tại 3 chuỗi siêu thị lớn nhất Hy Lạp
- Danh sách sản phẩm nằm sau phần render bằng JavaScript và infinite scroll, nên thay vì
curlhayrequests.get(), đã dùng Playwright để điều khiển trình duyệt và trích xuất dữ liệu từ DOM - Vì chỉ dùng laptop cũ thì chậm còn AWS EC2 thì đắt, nên chuyển sang cách chỉ tạo máy chủ Hetzner khi cần, chạy song song 3 scraper rồi tắt đi
- Với siêu thị mà Akamai chặn IP không phải dân dụng, đã dùng Tailscale exit node để lưu lượng đi qua IP nhà; đồng thời phát hiện lỗi bằng cảnh báo email, bước chuyển đổi nghiêm ngặt và heuristic về số lượng sản phẩm
- Đã rút ngắn thời gian chạy bằng máy chủ 8vCPU và chặn request ảnh; chi phí cho 31 lần chạy gần đây ở mức Hetzner €4.94, IPv4 €0.09 và lưu trữ Cloudflare R2 €0.00
Scraping các website siêu thị dựa trên JavaScript
- Trong bối cảnh lạm phát cao vào tháng 12/2022, đã xây dựng pricewatcher.gr để theo dõi biến động giá tại 3 chuỗi siêu thị lớn nhất Hy Lạp
- Cả ba cửa hàng e-shop của siêu thị đều dùng render bằng JavaScript, và một số khu vực còn tải thêm sản phẩm khi cuộn như infinite scroll trên mạng xã hội
- Không thể lấy dữ liệu sản phẩm bằng
curlhayrequests.get()đơn giản, nên đã dùng Playwright - Playwright điều khiển trình duyệt theo cách lập trình và xử lý các việc sau qua API
- mở tab mới và điều hướng tới URL
- kiểm tra DOM
- truy vấn chi tiết phần tử
- chặn và kiểm tra request
- Playwright hỗ trợ Chromium, Safari, Firefox và có thể dùng với Node, Java, .NET, Python
- Scraper tìm phần tử
load morecủa infinite scroll, tiếp tục cuộn, rồi loại trừ các sản phẩm có dấu hiệu hết hàng khỏi danh sáchli.product-item - Cuối cùng, từ mỗi sản phẩm sẽ parse tên sản phẩm, giá, ảnh, liên kết v.v., rồi lặp lại quy trình tương tự cho các danh mục sản phẩm tiếp theo
Chọn môi trường chạy hằng ngày
- Trên M1 MacBook Pro, xử lý toàn bộ một siêu thị mất từ 50 phút đến 2 giờ 30 phút, và ngay cả khi chạy song song 3 scraper cũng không tạo ra khác biệt đáng kể
- Laptop đủ dùng cho phát triển và kiểm thử, nhưng vẫn cần một môi trường thường trực để chạy mỗi ngày
-
Chạy riêng bằng laptop cũ
- Thử nghiệm đầu tiên là một chiếc laptop cũ từ năm 2013
- Cấu hình là CPU dòng M dual-core 2.20GHz và RAM 4GB, sau đó đã nâng RAM lên 12GB
- Nhưng ngay cả siêu thị “nhanh” nhất cũng mất hơn 2 giờ để xử lý, không đạt hiệu năng mong đợi
-
Chạy trên cloud
- Đã xem xét EC2 instance trên AWS ở mức 4 core và 8GB RAM, nhưng chi phí quá cao cho một side project
- Tại thời điểm viết bài,
c5a.xlargeởeu-north-1có giá $0.1640/giờ, tương đương $118.08/tháng hoặc $1,416.96/năm - Máy chủ tương đương
cpx31của Hetzner có giá $17.22 (€15.72)/tháng, tức $206.64/năm, rẻ hơn AWS khoảng 7 lần - Cuối cùng đã chọn Hetzner làm môi trường chạy
Pipeline hằng ngày được dựng bằng Concourse
- Chiếc laptop cũ không trực tiếp thực hiện scraping mà đóng vai trò máy chủ CI giao việc cho máy chủ Hetzner
- Công cụ CI được dùng là Concourse
- Concourse tự giới thiệu là “a continuous thing-doer”
- Công cụ này hướng tới build có thể tái lập nhờ mô hình pipeline khai báo và quản lý version của đầu vào
- Pipeline chạy mỗi đêm theo thứ tự sau
- tạo máy chủ scraping
- chạy song song tác vụ scraping của 3 siêu thị
- sau khi mọi tác vụ hoàn tất thì tắt máy chủ để tiết kiệm chi phí
- chuyển đầu ra thô của từng scraper sang bước chuyển đổi
- nạp dữ liệu đã chuyển đổi vào pricewatcher.gr
- nếu bất kỳ bước nào thất bại thì gửi cảnh báo email
Vượt giới hạn IP và Tailscale exit node
- Siêu thị dùng để thử nghiệm hoạt động bình thường, nhưng có một siêu thị khác nằm sau Akamai và bật rule tường lửa chặn request từ IP không phải dân dụng
- Cấu trúc cần thiết ở đây ngược với VPN thông thường: làm sao để request trông như được gửi đi từ IP nhà thật
- Với Tailscale, có thể gom nhiều thiết bị như thể chúng ở cùng một mạng
- Trong Tailscale, nếu chỉ định một thiết bị làm exit node, request của các thiết bị khác có thể được cấu hình để đi ra qua node đó
- Chiếc laptop cũ cũng kiêm luôn vai trò exit node cho lưu lượng scraping
- Một biến số khác là ISP đang dùng CGNAT, nên IP công khai không gắn riêng cho một cá nhân cụ thể mà được chia sẻ với khách hàng khác của ISP
Các kiểu lỗi và cách phát hiện
- Hệ thống này đã vận hành suốt 1 năm rưỡi và nhìn chung hoạt động đáng tin cậy
- Các dự án scraping khó tránh khỏi bị ảnh hưởng bởi thay đổi từ phía nhà phát triển website đích
- Lỗi được chia lớn thành hai loại
-
Thay đổi làm hỏng ngay
- Đây là trường hợp scraper thất bại ngay lập tức vì website thay đổi
- Ví dụ gồm
- thêm khảo sát khiến phải bấm nút thêm một lần nữa
- layout thay đổi hoàn toàn, buộc phải refactor lớn scraper
-
Thay đổi không làm hỏng ngay
- Trường hợp khó hơn là scraper vẫn chạy bình thường nhưng cách diễn giải dữ liệu bị lệch
- Ví dụ, nếu cách hiển thị giá thay đổi và phần thập phân bị tách thành ``, một gói khoai tây chiên giá €1.99 có thể bị parse thành €199
- Để bắt các thay đổi kiểu này, bước chuyển đổi được cấu hình để xác thực đầu vào nghiêm ngặt nhất có thể
- Vì chạy hằng ngày nên vẫn có thời gian kiểm tra vấn đề, nhưng nếu hỏng đúng lúc đang nghỉ phép thì vẫn là một nguồn gây bất an
Tối ưu thời gian chạy và độ ổn định
- Kiến trúc tổng thể gần như giữ nguyên từ đầu, nhưng nhiều phần đã được thay đổi để tăng độ tin cậy và giảm thao tác thủ công
- Các cải tiến đã áp dụng gồm
- cảnh báo email khi lỗi
- heuristic gửi cảnh báo khi số lượng sản phẩm của một siêu thị quá nhiều hoặc quá ít
- timeout
- retry mà không phải bắt đầu lại từ đầu
- Nút thắt lớn nhất là thời gian chạy scraping
- chạy càng lâu thì chi phí càng tăng
- khi lỗi rồi phải retry từ đầu thì mức độ bất tiện cũng lớn hơn
-
Dùng máy chủ lớn hơn
- Đã đổi máy chủ từ 4vCPU·16GB RAM sang 8vCPU·16GB RAM
- Thời gian chạy giảm khoảng 20%, đạt mức tương đương hiệu năng trên MBP
- Vì máy chủ scraping chỉ dùng khoảng 2 giờ nên chênh lệch giá gần như có thể bỏ qua
-
Tải ít hơn
- Dùng
page.routecủa Playwright để chặn request ảnh - Các request
.png,.jpgđược xử lý abort để không tải ảnh trong lúc nạp sản phẩm - Cách này giúp scraping nhanh hơn, đồng thời cũng giảm nhẹ băng thông và chi phí phía website đích
- Dùng
Chi phí cho 31 lần chạy
- Theo hóa đơn Hetzner gần đây, chi phí như sau
- 31 máy chủ đã tạo: €4.94
- 31 địa chỉ IPv4 được cấp cho các máy chủ đó: €0.09
- Dữ liệu scraping được lưu trong Cloudflare R2
- Hiện vẫn chưa vượt mức miễn phí 10GB của Cloudflare R2 nên chi phí lưu trữ là €0.00
- Toàn bộ pipeline vận hành như một cấu trúc theo dõi hằng ngày biến động giá siêu thị bằng cách kết hợp Playwright, Hetzner, Concourse, Tailscale và Cloudflare R2
1 bình luận
Các ý kiến trên Hacker News
Tôi đã làm một việc tương tự cho New Zealand từ đầu năm nay. Tôi dùng Playwright/Typescript để scrape dữ liệu rồi lưu vào cloud storage dưới dạng file Parquet, hiện vẫn chưa hiển thị lên giao diện
Phần lớn công việc nằm ở việc vượt qua các dịch vụ reverse proxy như Akamai, Cloudflare. Khi mới làm tôi cứ tưởng chưa ai làm, nhưng giờ tôi biết ở NZ có ít nhất 3 startup đang làm cùng việc này. Có vẻ lạm phát đã thúc đẩy khá nhiều đổi mới ở đây
Mẫu hình thì đúng như dự đoán. Các siêu thị dùng chiêu quen thuộc là làm giá cả phức tạp hết mức có thể, và dùng biến động giá kiểu “răng cưa” để phân tách người thiếu thời gian với người thiếu tiền. Họ cũng thường tách khách hàng trung thành với thương hiệu và khách hàng nhạy cảm về giá; nếu có 3 thương hiệu chocolate phổ biến thì mỗi tuần chỉ một trong số đó được bán ở mức giá hợp lý
Tôi đặc biệt tò mò đoạn “tách khách hàng trung thành với thương hiệu và khách hàng nhạy cảm về giá. Nếu có 3 thương hiệu chocolate phổ biến thì mỗi tuần chỉ một trong số đó được bán ở mức giá hợp lý”
Ở AU từng có nhiều scraper kiểu này xuất hiện rồi biến mất, nhưng thường bị các chuỗi siêu thị lớn chặn lại. Nó là một vòng lặp giữa sự hữu ích và câu hỏi “sao không có cái này nhỉ?”, trong khi thực ra nó đã tồn tại nhiều lần rồi
Việc xây crawler và hạ tầng hỗ trợ nó mất chưa đến 20 giờ
Hơn nữa, nó còn hữu ích hơn vì chỉ có một marketplace có toàn bộ giá của nhiều cửa hàng
Bài viết hay. Tôi cũng gặp vấn đề tương tự với website so sánh giá kính áp tròng mà tôi vận hành, https://lenspricer.com/, hiện hoạt động ở khoảng 30 quốc gia. Tôi đồng ý rằng việc các website thay đổi HTML thật sự là một cơn đau đầu
Một trong những trở ngại lớn nhất ban đầu là khớp cùng một sản phẩm trên hơn 100 website. Tưởng tên sản phẩm là duy nhất, nhưng ai cũng ghi hơi khác theo cách của mình. Phần lớn xử lý được bằng regex, nhưng khá nhiều trường hợp phải ánh xạ thủ công; một số trường hợp tôi dùng AI, nhưng tất cả đều được tôi tự kiểm chứng
Xây scraper và hạ tầng thì tương đối dễ. Khó là bảo trì tất cả scraper, và khi một sản phẩm biến mất khỏi site thì phải phân biệt đó là lỗi scraper, bị chặn, site thay đổi, hay tại thời điểm crawl thì site đang bảo trì
Đây là một dự án thú vị nhưng đôi khi khó, và có nhiều vấn đề rất khó chịu khi phải sửa
Giá thì mỗi nơi một kiểu, còn quyền lợi bảo hiểm thường được xử lý kiểu hoàn tiền qua thư khoảng 30%
Số lượng cửa hàng trông cũng rất ít, nên có khi tất cả liên kết đều là liên kết được tài trợ. Trên idealo.de tôi còn tìm được giá thấp hơn
Costco khá nổi tiếng về chuyện này; gần như mọi đồ điện tử bán trong cửa hàng và nhiều sản phẩm khác đều là SKU tùy chỉnh. Đôi khi cấu hình sản phẩm cũng hơi khác
Tôi đã làm một website tương tự và được khá nhiều người trong thành phố của tôi quan tâm. Tôi scrape cả dữ liệu từ app lẫn website, dùng một server Linode duy nhất có 2GB RAM, gắn 5 IPv4 và 1000 IPv6 miễn phí
Tất cả sản phẩm được thu thập tối đa mỗi 40 phút, trung bình khoảng 25 phút. Tôi dùng curl-impersonate và scrape JSON nhiều nhất có thể. 90% thị trường cung cấp giá qua các lệnh gọi Ajax, 10% còn lại thì dễ dàng parse HTML bằng regex
Có thể xem tại https://www.economizafloripa.com.br
https://www.economizafloripa.com.br/?q=parceria-comercial
Nhìn trang đó khiến dự án từ chỗ là “một công cụ hữu ích cho những người muốn giành lại quyền kiểm soát từ các công ty bán nhu yếu phẩm” trở thành “một nỗ lực kiếm tiền nữa”. Dĩ nhiên đó là quyền của họ, nhưng khi đọc trang chủ tôi đã kỳ vọng một động cơ mang tính đạo đức hơn
Bài viết hay
Mình nghĩ tốt nhất là tách scraping và parsing thành các tiến trình riêng. Nếu lưu JSON hoặc HTML gốc lại, bạn luôn có thể quay lại sửa parser rồi áp dụng lại
Mình đã làm một hệ thống và website tương tự cho Netherlands như một phần của dự án thạc sĩ: https://www.superprijsvergelijker.nl/
Phần lớn scraping trong dự án của mình là gửi các HTTP call đơn giản tới JSON API. Một số website dùng một instance Playwright để lấy session cookie hợp lệ và vượt qua cơ chế chống bot cũng như CAPTCHA. Phần còn lại gồm crawler/scraper, parser và API được viết bằng Haskell và chạy trên AWS ECS. Website là NextJS
Vấn đề chính mình vẫn đang cố giải là liên kết sản phẩm giữa các siêu thị khác nhau để liệt kê giá trên cùng một màn hình. Ví dụ ở đây: https://www.superprijsvergelijker.nl/supermarkt-aanbieding/6...
Nếu sản phẩm có ít nhất một mã vạch đúng được cung cấp thì phần lớn hoạt động tốt
Mình cho rằng hai chuỗi siêu thị lớn ở Australia chỉ cần cả hai bên vận hành thuật toán AI phân tích giá là đã có thể tạo ra một cấu trúc lưỡng quyền định giá phản cạnh tranh. Rốt cuộc các thuật toán có khả năng sẽ đi theo hướng hợp tác để tối đa hóa lợi nhuận
Chỉ với giá lấy công khai thì có thể làm hợp pháp, còn nếu chia sẻ chi phí cung ứng hoặc dữ liệu lợi nhuận theo từng sản phẩm thì cũng có thể làm bất hợp pháp. Kết quả có lẽ tương tự nhau
Hai AI đã được huấn luyện sẽ thực hiện hồi quy đa chiều, thậm chí có thể là siêu đa chiều, để tối đa hóa lợi nhuận theo những cách kỳ lạ, và người tiêu dùng sẽ phải gánh phần lợi nhuận đã được tối đa hóa của các doanh nghiệp bề ngoài là đối thủ cạnh tranh. Nếu có thể lấy dữ liệu giá theo cách này, thì không cần nhiều thứ để chạy hai triển khai machine learning tập trung vào lưỡng quyền
Lập luận là nếu mọi mức giá đều công khai, người tiêu dùng rốt cuộc lại phải trả giá cao hơn. Các siêu thị sẽ điều chỉnh giá về điểm mà tất cả đều đạt lợi nhuận tối đa
Các siêu thị ở đây đã thuê “thợ săn giá” trong nhiều năm, tức là những người đến các cửa hàng đối thủ để ghi lại giá của mọi mặt hàng
Ở Norway, bạn thường thấy nếu siêu thị A giảm giá một mặt hàng cụ thể trong một tuần, thì tuần sau hoặc tuần kế tiếp siêu thị B sẽ có mức giảm giá tương tự để kéo khách
Có lẽ có thể thêm kiểm tra tự động để bắt những thay đổi kiểu này. Ví dụ, nếu kiểm tra theo lẽ thường thất bại thì không đồng bộ thay đổi giá/sản phẩm
Có thể đặt tiêu chí như mỗi mức giá không được thay đổi quá 100%, và số sản phẩm đang hoạt động không được thay đổi quá 20%
Thay vào đó, mình dùng các heuristic kiểu này để kiểm tra việc scraping có thành công hay không. Ví dụ kiểm tra xem số sản phẩm scrape được hôm nay có nằm trong khoảng khoảng 10% so với trung bình 7 ngày gần nhất không
Khó không phải là bản thân việc scraping, mà là vượt qua các cơ chế chặn ngày càng tinh vi
Bạn phải liên tục xoay vòng proxy dân cư, dùng loại có điểm tín nhiệm cao, và tránh để lộ pattern scraping dữ liệu. Một số siêu thị không hiển thị network request trong tab Network, nên không thể chỉ lấy API response là xong
Dù có thử tấn công man-in-the-middle vào ứng dụng di động để xem network request và dữ liệu, nếu không ngụy trang đúng cách thì vẫn bị chặn
Mình đã thử, nhưng kết luận là không đáng vì chi phí và công sức phát triển liên tục. Thực tế, một số dịch vụ so sánh giá siêu thị chỉ đơn giản dùng nhân công lương thấp để scrape
Đây là dự án phụ, nên nếu đòi hỏi thời gian quá thường xuyên thì mình sẽ dừng lại và công khai toàn bộ code cùng dữ liệu
Nhưng mình tò mò làm sao mà network request lại không xuất hiện trong tab Network được
Phần khó nhất với mình là liên kết và so sánh sản phẩm giữa các siêu thị
Sẽ tốt nếu có minh bạch giá cả của hàng hóa. Có thể theo dõi quá trình này dễ hơn nhiều theo từng cửa hàng và khu vực
Ví dụ có thể so sánh giá oat milk, một sản phẩm thay thế sữa, theo mã bưu chính và theo từng cửa hàng tạp hóa. Cũng có thể theo dõi “shrinkflation”, tức là giá giữ nguyên nhưng dung lượng giảm
Theo nghĩa đó thì có vẻ bạn đang theo dõi giá, nhưng mình tò mò liệu bạn có kiểm tra cả chi phí theo gram hoặc ounce không. Nhà sản xuất hoặc cửa hàng có thể giữ nguyên giá nhưng đưa cho người tiêu dùng ít hàng hơn, và mình muốn biết công cụ có phát hiện được điều đó không
Dù vậy đây là tính năng hoàn toàn có thể thêm vào, và nếu có shrinkflation thì có thể hiển thị thời điểm nó xảy ra
Tại thị trường Thụy Điển, họ đã làm việc này hơn 8 năm. Có một website tên là https://www.matspar.se/, nơi khách hàng có thể xem tất cả sản phẩm ở các cửa hàng online lớn, so sánh giá rồi cho sản phẩm mong muốn vào giỏ hàng
Cuối cùng, họ có thể so sánh tổng giá trị giỏ hàng bao gồm cả phí giao hàng, rồi xuất giỏ hàng sang cửa hàng mong muốn để đặt mua
Tôi là đồng sáng lập kiêm CTO hiện tại, nên đã làm rất nhiều việc scraping và bảo trì trong suốt thời gian qua. Mỗi ngày chúng tôi scrape hơn 30 triệu mức giá
Theo dữ liệu công khai, doanh thu của công ty khoảng 400.000 USD và có 6 nhân viên: https://www.allabolag.se/5590076351/matspar-i-sverige-ab
Khi chuyển đến khu vực mới, tôi từng theo dõi giá, nhưng giờ tôi thấy mua sắm ở một khu chợ hoặc 2 siêu thị lớn luôn giữ giá thấp thì dễ hơn nhiều
Nếu ở châu Âu thì có lẽ là Aldi/Lidl
Nếu ở Mỹ thì có thể là Costco/Trader Joe's
Online thì có CamelCamelCamel/Amazon. Không phải thực phẩm, mà là mảng sức khỏe/làm đẹp/một số đồ điện tử
Nếu có thể mua trực tiếp từ nhà sản xuất thì đôi khi như vậy còn tốt hơn. Ví dụ, tôi mua số lượng lớn một thương hiệu xà phòng cụ thể mà mình thích từ website bán buôn thì giá chưa đến một nửa giá bán lẻ. Dầu gội cũng rẻ hơn nhiều so với bất kỳ cửa hàng bán lẻ nào khi mua trực tiếp theo can gallon
Trader Joe's có chất lượng cao hơn nhưng nhìn chung đắt hơn