Skip to content

Chi tiết quy trình xử lý Scraping

Quy trình tổng thể

Tham số CLI

Tham sốKiểuMặc địnhMô tả
--type_keywordstrallĐối tượng xử lý: all / normal / reverse
--offsetint0Vị trí bắt đầu lấy từ khóa
--limitint1000Số lượng từ khóa tối đa
--debugint01 để chụp screenshot khi lỗi
--ranking_threshold_for_screenshotint3Ngưỡng rank để upload S3
--retry_failedstr""Đối tượng retry: all / timeout / recaptcha / error (phân cách bằng dấu phẩy)

SearchRankingProcessAWS.handle (Xử lý process)

Lấy dữ liệu từ khóa (get_users)

Thực thi tìm kiếm Playwright (fetch_search_rankings_aws)

Tìm kiếm & phán đoán xếp hạng 1 từ khóa (fetch_search_ranking)

Thứ tự ưu tiên phán đoán xếp hạng

Ưu tiênĐối tượng kiểm traSelectorGiá trị rank
1Knowledge Panel*[data-attrid="title"]-1 (cùng cửa hàng) / -2 (cửa hàng khác)
2Local Pack (thông thường)Phần tử tiêu đề kết quả bản đồ-11 / -12 / -13 (vị trí 1~3)
2'Local Pack (khách sạn)Phần tử trong image-carousel-11 / -12 / -13
3Trang bản đồ (thông thường)Danh sách sau click「すべて表示」Vị trí 1~N (tối đa 3 trang)
3'Trang bản đồ (dịch vụ)Sau click「その他のお店やサービス」Vị trí 1~N
3''Trang bản đồ (khách sạn)Danh sách bản đồ khách sạnVị trí 1~N
4Bản đồ đơn locationKết quả đơn trong bản đồ-11
5Trang bản đồ tĩnhDanh sách không JavaScriptVị trí 1~N

Chuyển đổi giá trị xếp hạng

Xử lý lưu kết quả (process_rankings_data_success)

Biện pháp chống phát hiện

Biện phápChi tiết
UserAgentChọn ngẫu nhiên từ 15 loại (Chrome/Firefox/Safari/Edge/Opera, Windows/Mac/Linux)
Gõ phímGõ từng ký tự với độ trễ 50-200ms + chờ 100-300ms giữa các ký tự
Di chuyển chuộtDi chuyển đến tọa độ ngẫu nhiên trước/sau tìm kiếm
Chờ ngẫu nhiênChờ ngẫu nhiên 1-3 giây giữa các thao tác
Chặn tài nguyênChặn hình ảnh/CSS/media/font để tăng tốc
GeolocationCài đặt tọa độ cửa hàng vào trình duyệt
Vô hiệu hóa cờ tự động--disable-blink-features=AutomationControlled
Xử lý đồng ý CookieTự động xử lý modal đồng ý của Google

Thông báo Slack

Thông báo báo cáo (khi hoàn thành)

Scraper Report
・システム: scrappy
・モード: Normal / Reverse
・バッチの実行時間: 2026-02-11 09:00:00 - 2026-02-11 09:15:00
・全実行件数: 500件
・タイムアウトエラー: 3件
・reCAPTCHAエラー: 1件
・例外エラー: 0件

Thông báo cảnh báo lỗi (khi vượt ngưỡng)

Scraper Alert
・System: scrappy
・Unexpected Errors Total: 25 (5%)
・Out of Range Total (圏外): 100 (20%)
・Keywords Total: 500
・Unexpected Error Threshold: 20%
・Out of Range Threshold: 20%

Luồng sau khi Refactor (Phương thức Worker song song)

Tổng quan

Refactor xử lý tuần tự hiện tại thành xử lý song song dựa trên worker. Queue hóa theo đơn vị từ khóa, nhiều worker xử lý đồng thời không xung đột.

Thay đổi

MụcHiện tạiSau refactor
Đơn vị xử lýTuần tự theo người dùngQueue hóa theo từ khóa
Song songThread (normal/reverse)--workers N tùy chỉnh
Loại trừ lẫn nhauKhông (phân chia bằng offset/limit)search_status = 6 (PROCESSING) loại trừ bằng DB
Phục hồi sự cốXử lý thủ côngreset_stale_processing phục hồi tự động
Retry--retry_failed chạy lại toàn bộChỉ đưa từ khóa thất bại vào queue

Tham số CLI mới

Tham sốKiểuMặc địnhMô tả
--workersint1Số worker song song

Chuyển đổi trạng thái search_status

Trạng tháiGiá trịMô tả
INIT1Chưa xử lý (có thể đưa vào queue)
SUCCESS2Tìm kiếm thành công
TIMEOUT3Lỗi timeout
RECAPTCHA_ERROR4Phát hiện reCAPTCHA
UNEXPECTED_ERROR5Lỗi exception
PROCESSING6Thêm mới — Worker đang xử lý

Luồng xử lý Worker

reset_stale_processing (Phục hồi sự cố)

Khi worker bị dừng giữa chừng, tự động phục hồi từ khóa còn ở trạng thái PROCESSING (6).

Luồng retry_failed

Hạng mục test

#Nội dung testCách xác nhận
1Thực thi worker đơn với --workers 1Tất cả từ khóa được xử lý tuần tự
2Thực thi song song với --workers 3Từ khóa không bị xử lý trùng lặp
3Dừng worker giữa chừngreset_stale_processing đặt PROCESSING về INIT
4--retry_failed allChỉ từ khóa thất bại được đưa vào queue
5Kiểm tra trạng thái sau hoàn thànhKhông còn search_status = 6 trong DB

Quy trình Rollback

  • Hoàn nguyên code
  • UPDATE mappy_keywords SET search_status = 1 WHERE search_status = 6 để phục hồi
  • Không cần DB migration (chỉ dùng giá trị mới trên cột int hiện có)

Liên kết DB ứng dụng

DB scraping và DB ứng dụng (phía Laravel) được tách biệt hoàn toàn. Đồng bộ dữ liệu qua 2 hàm AWS Lambda sau.

Hệ thống mục tiêu

system_idTên DBHệ thống
1gmacMAPPY
2gmac2GCOR
3pipitPIPIT

sync_database (Lambda)

Hướng: DB ứng dụng → DB scraping

Chạy hàng ngày qua EventBridge schedule. Đồng bộ các bảng sau từ 3 DB ứng dụng (gmac/gcor/pipit) vào DB scraping.

Bảng đồng bộNội dung
mappy_usersMaster người dùng (tenant)
mappy_user_settingsCài đặt người dùng
mappy_gbp_locationsThông tin GBP location
mappy_gbp_locations_store_codesStore code
mappy_integrated_gbp_locationsLiên kết người dùng × location
mappy_keywordsTừ khóa tìm kiếm

Backup bảng hiện có trước khi đồng bộ, sau khi hoàn thành sẽ trigger xử lý scraping qua EventBridge event.

syncResultFetchSearch (Lambda)

Hướng: DB scraping → DB ứng dụng

Chạy sau khi scraping hoàn thành. Lọc kết quả theo system_id và phân phối về từng DB ứng dụng.

Bảng trả vềNội dung
mappy_search_ranking_exportsKết quả xếp hạng (mảng JSON)
mappy_temp_search_rankingsKết quả tìm kiếm theo từ khóa
mappy_search_ranking_processesMetadata xử lý
mappy_search_ranking_update_logsLog cập nhật

Upload log S3

Sau khi hoàn thành, upload debug log lên S3:

s3://{SCRAPER_SCRAPING_BUCKET}/log/debug_aws/{year}/{month}/{day}/{datetime}-{suffix}.log

Ví dụ suffix: normal_offset_0_limit_500, retry_timeout_reverse_offset_500_limit_500

Cải thiện fetch_search_ranking

Vấn đề hiện tại

#Vấn đềẢnh hưởng
1Phán đoán khách sạn phụ thuộc DOM (image-carousel)Cùng location nhưng kết quả có thể khác nhau tùy từ khóa
28 hàm phán đoán lồng nhau sâu (tối đa 6 tầng)Khó đọc, khó bảo trì
3Xử lý phân trang bản đồ lặp lại ở 3 nơicheck_search_map_page / check_search_map_page_service / check_search_map_page_static
4Kiểm tra reCAPTCHA lặp lại ở tất cả hàm (8 nơi)Code copy-paste giống nhau
5Tuple trả về không thống nhất (4 phần tử vs 5 phần tử)Chỉ check_search_map_page_static thêm error_count

Phán đoán ngành nghề khách sạn trước

Tổng quan

Thay đổi phán đoán DOM hiện tại (page.locator("image-carousel").count() > 0) thành phán đoán trước dựa trên primary_category của GBP. Bảng mappy_gbp_locations đã có sẵn primary_category (định dạng gcid) và primary_category_display_name (tên hiển thị).

Danh sách category cơ sở lưu trú

Tất cả category hiển thị layout khách sạn (image-carousel) trên kết quả tìm kiếm Google:

gcidTên tiếng AnhTên hiển thị tiếng Nhật
gcid:hotelHotelホテル
gcid:motelMotelモーテル
gcid:hostelHostelホステル
gcid:innInn旅館 / イン
gcid:resort_hotelResort hotelリゾートホテル
gcid:lodgeLodgeロッジ
gcid:bed_and_breakfastBed & breakfastペンション / 民宿
gcid:guest_houseGuest houseゲストハウス
gcid:capsule_hotelCapsule hotelカプセルホテル
gcid:love_hotelLove hotelラブホテル
gcid:hot_spring_hotelHot spring hotel温泉ホテル
gcid:extended_stay_hotelExtended stay hotel長期滞在ホテル
gcid:wellness_hotelWellness hotelウェルネスホテル
gcid:cottageCottageコテージ
gcid:chaletChaletシャレー
gcid:holiday_homeHoliday home貸別荘
gcid:camping_cabinCamping cabinキャンピングキャビン
gcid:farmstayFarmstayファームステイ
gcid:pensionPensionペンション
gcid:boarding_houseBoarding house下宿
gcid:youth_hostelYouth hostelユースホステル
gcid:lodgingLodging宿泊施設
gcid:campgroundCampgroundキャンプ場
gcid:rv_parkRV parkRVパーク
gcid:self_catering_accommodationSelf-catering accommodationセルフケータリング

Logic phán đoán

python
# configs/hotel_categories.py — Tách ra file cấu hình
HOTEL_CATEGORY_IDS = {
    "gcid:hotel", "gcid:motel", "gcid:hostel", "gcid:inn",
    "gcid:resort_hotel", "gcid:lodge", "gcid:bed_and_breakfast",
    "gcid:guest_house", "gcid:capsule_hotel", "gcid:love_hotel",
    "gcid:hot_spring_hotel", "gcid:extended_stay_hotel",
    "gcid:wellness_hotel", "gcid:cottage", "gcid:chalet",
    "gcid:holiday_home", "gcid:camping_cabin", "gcid:farmstay",
    "gcid:pension", "gcid:boarding_house", "gcid:youth_hostel",
    "gcid:lodging", "gcid:campground", "gcid:rv_park",
    "gcid:self_catering_accommodation",
}

HOTEL_DISPLAY_KEYWORDS = [
    "ホテル", "モーテル", "ホステル", "旅館", "リゾート", "ロッジ",
    "ペンション", "民宿", "ゲストハウス", "カプセル", "ラブホテル",
    "温泉", "コテージ", "シャレー", "貸別荘", "キャビン",
    "ファームステイ", "下宿", "ユースホステル", "宿泊", "ウェルネス",
    "キャンプ", "RVパーク", "セルフケータリング", "イン",
    # Fallback tiếng Anh
    "Hotel", "Motel", "Hostel", "Inn", "Resort", "Lodge",
    "B&B", "Bed & breakfast", "Guest house", "Capsule",
    "Cottage", "Chalet", "Cabin", "Farmstay", "Pension",
    "Boarding house", "Youth hostel", "Lodging", "Campground",
]

def is_hotel_business(location) -> bool:
    """Phán đoán trước ngành nghề khách sạn từ primary_category"""
    if location.primary_category:
        cat = location.primary_category.strip().lower()
        if cat in {c.lower() for c in HOTEL_CATEGORY_IDS}:
            return True
    if location.primary_category_display_name:
        name = location.primary_category_display_name
        return any(kw in name for kw in HOTEL_DISPLAY_KEYWORDS)
    return False

Luồng dữ liệu

  1. Gọi is_hotel_business(location) khi lấy thông tin location trong get_users()
  2. Lưu kết quả vào user_data["is_hotel"]
  3. Truyền is_hotel qua fetch_search_rankings_aws()fetch_search_ranking()
  4. Các hàm phán đoán chuyển đổi selector dựa trên is_hotel

Đối phó khi thay đổi category

Google thêm/thay đổi/hợp nhất category không định kỳ (trước đây đã xóa Boutique Hotel, Budget Hotel, Beach Resort...).

Đối phóMô tả
Tách ra file cấu hìnhTách HOTEL_CATEGORY_IDSHOTEL_DISPLAY_KEYWORDS vào configs/hotel_categories.py. Khi thêm category chỉ cần sửa file này
Giữ DOM fallbackKhi is_hotel=False nhưng DOM phát hiện image-carousel, vẫn xử lý như layout khách sạn (safety net cho category mới)
Log phát hiện fallbackKhi DOM fallback kích hoạt, ghi primary_category vào WARN log để nhắc thêm vào file cấu hình
sync_databaseprimary_category đã được đồng bộ từ DB ứng dụng sang DB scraping, không cần thêm xử lý
[WARN] Hotel layout detected by DOM but not by category.
       primary_category=gcid:new_category_name location_id=123
       → Hãy xem xét thêm vào configs/hotel_categories.py

Cải thiện luồng phán đoán

Luồng sau cải thiện

Hợp nhất hàm

Hiện tại (8 hàm)Sau cải thiện (4 hàm)Nội dung thay đổi
check_local_pack()check_local_pack(is_hotel)Chuyển selector bằng is_hotel
check_local_pack_hotel()(hợp nhất)
check_search_map_page()check_map_page(is_hotel)Tự động phát hiện nút「さらに表示」, phân trang chung
check_search_map_page_hotel()(hợp nhất)
check_search_map_page_service()(hợp nhất)
check_map_single_location()Hợp nhất vào check_map_pageKiểm tra single location trước khi chuyển trang bản đồ
check_search_map_page_static()check_map_page_static()Fallback cuối cùng (thay đổi nhẹ)
scan_map_listings()Mới: Xử lý phân trang chung

Xử lý phân trang chung (scan_map_listings)

Hợp nhất xử lý scan trang bản đồ lặp lại ở 3 nơi thành 1:

python
def scan_map_listings(page, location_title, heading_selector, max_pages=3):
    """Xử lý chung scan danh sách trang bản đồ và trả về ranking"""
    location_index = 0
    for current_page in range(1, max_pages + 1):
        # Loại trừ sponsor
        sponsors_count = page.locator("span").filter(has_text="スポンサー").count()
        headings = page.locator(heading_selector).all()[sponsors_count:]
        for heading in headings:
            location_index += 1
            if heading.count() > 0 and location_title == heading.inner_text():
                return True, location_index
        # Phân trang
        if not click_next_page(page):
            break
    return False, None


def click_next_page(page):
    """Xử lý chung chuyển trang tiếp theo"""
    next_selectors = [
        ("button span", "次へ"),           # Cho khách sạn
        ("a span", "次へ"),                 # Cho dịch vụ
        ("#pnnext", None),                  # Cho bản đồ thường
        ('a[role="button"]', "もっと見る"),  # Cho bản đồ thường (pattern khác)
    ]
    for selector, text in next_selectors:
        locator = page.locator(selector)
        if text:
            locator = locator.filter(has_text=text)
        if locator.count() > 0:
            locator.first.click()
            page.wait_for_load_state("domcontentloaded", timeout=6000)
            return True
    return False

Thống nhất giá trị trả về

Thống nhất giá trị trả về của tất cả hàm phán đoán thành SearchResult dataclass:

python
@dataclass
class SearchResult:
    has_ranking: bool = False
    ranking_value: int | None = None
    timeout_count: int = 0
    recaptcha_count: int = 0
    error_count: int = 0

Tập trung kiểm tra reCAPTCHA

Hiện tại kiểm tra reCAPTCHA ở tất cả 8 hàm, tập trung lại như sau:

Thời điểmVị trí kiểm tra
Ngay sau tìm kiếm1 lần trong fetch_search_ranking → phát hiện thì return ngay
Sau chuyển trang1 lần trong check_map_page / check_map_page_static sau khi chuyển trang

Xóa kiểm tra reCAPTCHA khỏi các hàm phán đoán riêng lẻ (check_knowledge_panel / check_local_pack).