checkpoint: pipeline + 27k/h baseline before sitemap discovery
This commit is contained in:
@@ -13,6 +13,10 @@ RUN pip install --no-cache-dir uv \
|
|||||||
&& pip install --no-cache-dir -r /tmp/requirements.txt \
|
&& pip install --no-cache-dir -r /tmp/requirements.txt \
|
||||||
&& pip install --no-cache-dir playwright-stealth
|
&& pip install --no-cache-dir playwright-stealth
|
||||||
|
|
||||||
|
# Speed-up libs: orjson (fast JSON parse), brotli (HTTP br decompress).
|
||||||
|
# Pinned outside uv.lock to avoid lock-regen churn.
|
||||||
|
RUN pip install --no-cache-dir "orjson>=3.10.0" "brotli>=1.1.0"
|
||||||
|
|
||||||
# Install both Chromium and Firefox. Chromium is the default engine in Docker
|
# Install both Chromium and Firefox. Chromium is the default engine in Docker
|
||||||
# because it works more reliably with the current IAAI listing page.
|
# because it works more reliably with the current IAAI listing page.
|
||||||
RUN python -m playwright install chromium firefox
|
RUN python -m playwright install chromium firefox
|
||||||
|
|||||||
11
_check.sh
Normal file
11
_check.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "== LOCK =="
|
||||||
|
docker exec iaai-redis redis-cli get iaai:locks:sync_listing
|
||||||
|
docker exec iaai-redis redis-cli ttl iaai:locks:sync_listing
|
||||||
|
echo "== CHECKPOINT =="
|
||||||
|
docker exec iaai-redis redis-cli get iaai:state:sync_listing_checkpoint
|
||||||
|
docker exec iaai-redis redis-cli get iaai:state:sync_full_scan_done
|
||||||
|
echo "== DB =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select count(*) as total, count(*) filter (where last_seen_at > now() - interval '3 minutes') as recent from cars;"
|
||||||
|
echo "== WORKER TAIL =="
|
||||||
|
docker logs iaai-worker --tail 25 2>&1 | tail -25
|
||||||
26
_check2.sh
Normal file
26
_check2.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== ТЕКУЩАЯ АКТИВНОСТЬ SYNC (последние логи) =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker logs iaai-worker --tail 30 2>&1 | grep -E "Segment|HTTP-mode page|upserted|finished|ERROR" | tail -20
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== DB: всего машин и апсерты за 5 минут =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_5min=' || count(*) filter (where last_seen_at > now() - interval '5 minutes'), 'last_1min=' || count(*) filter (where last_seen_at > now() - interval '1 minute') from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== Макс timestamp (когда была последняя запись) =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'max_last_seen=' || max(last_seen_at), 'now=' || now() from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== Топ марок в БД =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select make, count(*) from cars group by make order by count(*) desc limit 10;"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== Очередь Celery =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker exec iaai-redis redis-cli llen scraping
|
||||||
|
echo "lock TTL: $(docker exec iaai-redis redis-cli ttl iaai:locks:sync_listing)s"
|
||||||
15
_check3.sh
Normal file
15
_check3.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "== ERRORS/EXCEPTIONS за последние 5 минут worker =="
|
||||||
|
docker logs iaai-worker --since 5m 2>&1 | grep -iE "error|traceback|exception|failed|warning" | grep -v "Segment.*done" | tail -20
|
||||||
|
echo ""
|
||||||
|
echo "== Последние события (batch/upsert/save) =="
|
||||||
|
docker logs iaai-worker --tail 500 2>&1 | grep -iE "batch|upsert|save|Processing|finished|collected" | tail -15
|
||||||
|
echo ""
|
||||||
|
echo "== DB: cars schema =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -c "\d cars" 2>&1 | head -40
|
||||||
|
echo ""
|
||||||
|
echo "== DB: real counts =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_5min=' || count(*) filter (where last_seen_at > now() - interval '5 minutes') from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "== Текущая активность sync (tail) =="
|
||||||
|
docker logs iaai-worker --tail 5 2>&1
|
||||||
15
_check4.sh
Normal file
15
_check4.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "== DB: счётчики =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_10min=' || count(*) filter (where last_seen_at > now() - interval '10 minutes'), 'last_2min=' || count(*) filter (where last_seen_at > now() - interval '2 minutes'), 'max_last_seen=' || max(last_seen_at)::text from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "== Топ brand =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select brand, count(*) from cars group by brand order by count(*) desc limit 10;"
|
||||||
|
echo ""
|
||||||
|
echo "== Текущая фаза sync (tail) =="
|
||||||
|
docker logs iaai-worker --tail 5 2>&1
|
||||||
|
echo ""
|
||||||
|
echo "== Последние upserted/batch =="
|
||||||
|
docker logs iaai-worker --tail 500 2>&1 | grep -iE "upserted|Processing batch|Sync run.*finished|Segment.*done" | tail -10
|
||||||
|
echo ""
|
||||||
|
echo "== ERRORS свежие =="
|
||||||
|
docker logs iaai-worker --since 10m 2>&1 | grep -iE "ERROR|Traceback|Exception" | tail -10
|
||||||
26
_db_now.sh
Normal file
26
_db_now.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
|
||||||
|
echo "== DB now =="
|
||||||
|
docker compose exec -T postgres psql -U iaai -d iaai_scraper -c "
|
||||||
|
SELECT
|
||||||
|
count(*) AS total,
|
||||||
|
count(*) FILTER (WHERE last_seen_at > now() - interval '1 minute') AS last_1min,
|
||||||
|
count(*) FILTER (WHERE last_seen_at > now() - interval '5 minutes') AS last_5min,
|
||||||
|
count(*) FILTER (WHERE last_seen_at > now() - interval '15 minutes') AS last_15min,
|
||||||
|
max(last_seen_at) AS max_ts,
|
||||||
|
now() AS now_ts
|
||||||
|
FROM cars;"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== last 10 upsert / segment-done events =="
|
||||||
|
docker compose logs --tail=1000 worker 2>&1 | grep -E 'upserted=|Segment .* done|HTTP-mode batch' | tail -n 15
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== last 5 page-progress =="
|
||||||
|
docker compose logs --tail=200 worker 2>&1 | grep -E 'HTTP-mode page' | tail -n 5
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== recent 500s (if any) =="
|
||||||
|
docker compose logs --tail=500 worker 2>&1 | grep -E 'status=500|status=403' | tail -n 5 || true
|
||||||
19
_db_now2.sh
Normal file
19
_db_now2.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
|
||||||
|
echo "== DB =="
|
||||||
|
docker compose exec -T postgres psql -U iaai -d iaai_scraper -c "
|
||||||
|
SELECT
|
||||||
|
count(*) AS total,
|
||||||
|
count(*) FILTER (WHERE last_seen_at > now() - interval '1 minute') AS l1,
|
||||||
|
count(*) FILTER (WHERE last_seen_at > now() - interval '5 minutes') AS l5,
|
||||||
|
count(*) FILTER (WHERE created_at > now() - interval '15 minutes') AS created_15m,
|
||||||
|
max(last_seen_at) AS max_seen,
|
||||||
|
max(created_at) AS max_created,
|
||||||
|
now() AS now_ts
|
||||||
|
FROM cars;"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== last 50 lines of worker log (everything) =="
|
||||||
|
docker compose logs --tail=50 worker 2>&1 | tail -n 50
|
||||||
BIN
_deploy_patch.tar.gz
Normal file
BIN
_deploy_patch.tar.gz
Normal file
Binary file not shown.
42
_diag_vps.sh
Normal file
42
_diag_vps.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 1. PROXY/HTTP CONFIG в .env =="
|
||||||
|
echo "=========================================="
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
grep -iE "PROXY|HTTP_MODE|USER_AGENT|IAAI_BASE|IAAI_.*URL|BRIGHT" .env | grep -v "^#"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 2. ВНЕШНИЙ IP VPS =="
|
||||||
|
echo "=========================================="
|
||||||
|
curl -s --max-time 10 https://api.ipify.org; echo ""
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 3. ПРЯМОЙ ЗАПРОС IAAI (без прокси) =="
|
||||||
|
echo "=========================================="
|
||||||
|
curl -s -o /dev/null -w "HTTP=%{http_code} time=%{time_total}s size=%{size_download}\n" --max-time 20 "https://www.iaai.com/"
|
||||||
|
curl -s -o /tmp/iaai_root.html -w "HTTP=%{http_code} size=%{size_download}\n" --max-time 20 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "https://www.iaai.com/"
|
||||||
|
echo "--- первые 400 байт ответа ---"
|
||||||
|
head -c 400 /tmp/iaai_root.html; echo ""
|
||||||
|
echo "--- признак Incapsula/bot-check ---"
|
||||||
|
grep -oiE "incapsula|_Incapsula_|captcha|access denied|bot|cloudflare" /tmp/iaai_root.html | sort -u
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 4. POST /Search (как это делает парсер) =="
|
||||||
|
echo "=========================================="
|
||||||
|
curl -s -o /tmp/iaai_search.html -w "HTTP=%{http_code} size=%{size_download}\n" --max-time 20 \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-H "Accept: application/json, text/html, */*" \
|
||||||
|
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
|
||||||
|
-X POST "https://www.iaai.com/Search" \
|
||||||
|
--data "Make=TOYOTA&YearMin=1900&YearMax=2012&Page=1"
|
||||||
|
head -c 400 /tmp/iaai_search.html; echo ""
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 5. PROXY env внутри worker-контейнера =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker exec iaai-worker env | grep -iE "PROXY|IAAI|HTTP_" | sort
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "== 6. Последние ERROR-логи worker =="
|
||||||
|
echo "=========================================="
|
||||||
|
docker logs iaai-worker --tail 200 2>&1 | grep -iE "proxy|bright|incapsula|403|500|429|blocked" | tail -15
|
||||||
26
_dispatch_check.sh
Normal file
26
_dispatch_check.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
|
||||||
|
echo "== dispatch =="
|
||||||
|
TID=$(docker compose exec -T worker python -c "from iaai_scraper.worker.tasks import sync_listing_task; print(sync_listing_task.delay().id)")
|
||||||
|
echo "dispatched: $TID"
|
||||||
|
|
||||||
|
echo "== wait 120s =="
|
||||||
|
sleep 120
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== DB =="
|
||||||
|
docker compose exec -T postgres psql -U iaai -d iaai_scraper -c "SELECT count(*) AS total, count(*) FILTER (WHERE last_seen_at > now() - interval '5 minutes') AS last_5min, max(last_seen_at) AS max FROM cars;"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== upsert / batch / segment-done events (last 40) =="
|
||||||
|
docker compose logs --tail=800 worker 2>&1 | grep -E 'HTTP-mode batch|upserted=|HTTP-mode listing done|Segment .* done|HTTP fast-path using proxy|Sync run .* finished' | tail -n 40
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== page-progress tail =="
|
||||||
|
docker compose logs --tail=400 worker 2>&1 | grep -E 'HTTP-mode page' | tail -n 15
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== errors if any =="
|
||||||
|
docker compose logs --tail=400 worker 2>&1 | grep -Ei 'error|exception|403|500|incapsula|pardon' | tail -n 15 || true
|
||||||
55
_enable_proxy.sh
Normal file
55
_enable_proxy.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
|
||||||
|
echo "== patch .env =="
|
||||||
|
# удалить старые SOCKS5_*/IAAI_PROXY_* строки если есть
|
||||||
|
sed -i '/^SOCKS5_PROXY_/d; /^IAAI_PROXY_/d' .env
|
||||||
|
|
||||||
|
cat >> .env <<'EOF'
|
||||||
|
|
||||||
|
# --- SOCKS5 US residential proxy (BulletProof) ---
|
||||||
|
SOCKS5_PROXY_HOST=residential.bpproxy.at
|
||||||
|
SOCKS5_PROXY_PORT=1002
|
||||||
|
SOCKS5_PROXY_USER=bpuser-S7o3VlC0
|
||||||
|
SOCKS5_PROXY_PASS=YKmRzvjvnnvyQdEUQ8hM_country-US_session-q3TGNZZO_lifetime-30
|
||||||
|
EOF
|
||||||
|
|
||||||
|
grep -E '^(SOCKS5_|IAAI_PROXY_)' .env
|
||||||
|
|
||||||
|
echo "== restart worker =="
|
||||||
|
docker compose stop worker
|
||||||
|
docker compose up -d worker
|
||||||
|
|
||||||
|
echo "== wait 8s for worker + bridge =="
|
||||||
|
sleep 8
|
||||||
|
|
||||||
|
echo "== verify proxy bridge + external IP =="
|
||||||
|
docker compose exec -T worker sh -c 'curl -s --max-time 20 --proxy http://127.0.0.1:8899 https://api.ipify.org' || echo "proxy check FAILED"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== direct check (no proxy) =="
|
||||||
|
docker compose exec -T worker sh -c 'curl -s --max-time 10 https://api.ipify.org' || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== clear locks/state =="
|
||||||
|
docker compose exec -T redis redis-cli DEL iaai:locks:sync_listing iaai:state:startup_sync_dispatched iaai:state:sync_listing_followup_pending
|
||||||
|
|
||||||
|
echo "== dispatch fresh task =="
|
||||||
|
TID=$(docker compose exec -T worker python -c "from iaai_scraper.worker.tasks import sync_listing; print(sync_listing.delay().id)")
|
||||||
|
echo "dispatched: $TID"
|
||||||
|
|
||||||
|
echo "== wait 90s =="
|
||||||
|
sleep 90
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== DB =="
|
||||||
|
docker compose exec -T postgres psql -U iaai -d iaai_scraper -c "SELECT count(*) AS total, count(*) FILTER (WHERE last_seen_at > now() - interval '5 minutes') AS last_5min, max(last_seen_at) AS max FROM cars;"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== last 30 log lines (batch / upserted / HTTP-mode page) =="
|
||||||
|
docker compose logs --tail=500 worker 2>&1 | grep -E 'HTTP-mode batch|upserted=|HTTP-mode listing done|Segment .* done|HTTP fast-path using proxy|proxy_bridge' | tail -n 30
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== sample of recent pagination lines =="
|
||||||
|
docker compose logs --tail=300 worker 2>&1 | grep -E 'HTTP-mode page' | tail -n 10
|
||||||
13
_kick.sh
Normal file
13
_kick.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
echo "== Cleaning stale Redis state =="
|
||||||
|
docker exec iaai-redis redis-cli del iaai:locks:sync_listing iaai:state:startup_sync_dispatched iaai:state:sync_listing_followup_pending iaai:state:sync_listing_bootstrap_failure_streak
|
||||||
|
echo "== Dispatching fresh sync_listing_task =="
|
||||||
|
docker exec iaai-worker python -c "from iaai_scraper.worker.celery_app import celery_app; r=celery_app.send_task('iaai_scraper.worker.tasks.sync_listing_task', kwargs={'only_new': False}, queue='scraping'); print('dispatched:', r.id)"
|
||||||
|
sleep 30
|
||||||
|
echo "== Worker logs =="
|
||||||
|
docker logs iaai-worker --tail 30 2>&1 | grep -E "Segment|sync_listing_task|progress|upserted|ERROR|clamp" | tail -20
|
||||||
|
echo "== Lock TTL now =="
|
||||||
|
docker exec iaai-redis redis-cli ttl iaai:locks:sync_listing
|
||||||
|
echo "== DB recent =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select count(*) as total, count(*) filter (where last_seen_at > now() - interval '2 minutes') as recent from cars;"
|
||||||
29
_reduce50.sh
Normal file
29
_reduce50.sh
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
echo "== update .env =="
|
||||||
|
sed -i 's/^IAAI_MAX_PAGES_PER_RUN=.*/IAAI_MAX_PAGES_PER_RUN=50/' .env
|
||||||
|
grep MAX_PAGES .env
|
||||||
|
echo ""
|
||||||
|
echo "== restart worker (wait for finish) =="
|
||||||
|
docker compose stop worker
|
||||||
|
docker compose up -d worker
|
||||||
|
sleep 10
|
||||||
|
echo ""
|
||||||
|
echo "== clear lock =="
|
||||||
|
docker exec iaai-redis redis-cli del iaai:locks:sync_listing iaai:state:startup_sync_dispatched iaai:state:sync_listing_followup_pending
|
||||||
|
echo ""
|
||||||
|
echo "== dispatch =="
|
||||||
|
docker exec iaai-worker python -c "from iaai_scraper.worker.celery_app import celery_app; r=celery_app.send_task('iaai_scraper.worker.tasks.sync_listing_task', kwargs={'only_new': False}, queue='scraping'); print('dispatched:', r.id)"
|
||||||
|
echo ""
|
||||||
|
echo "== wait 90s =="
|
||||||
|
sleep 90
|
||||||
|
echo ""
|
||||||
|
echo "== DB =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_5min=' || count(*) filter (where last_seen_at > now() - interval '5 minutes'), 'max=' || max(last_seen_at)::text from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "== BATCH + UPSERT EVENTS =="
|
||||||
|
docker logs iaai-worker --tail 5000 2>&1 | grep -E "HTTP-mode batch|HTTP-mode listing done|Segment.*done|upserted" | tail -20
|
||||||
|
echo ""
|
||||||
|
echo "== TAIL =="
|
||||||
|
docker logs iaai-worker --tail 8
|
||||||
24
_rollback_limits.sh
Normal file
24
_rollback_limits.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
cd /root/iaai_scraper_project
|
||||||
|
echo "== 1. Verify .env =="
|
||||||
|
grep -E "MAX_PAGES|MAX_VEHICLES" .env
|
||||||
|
echo ""
|
||||||
|
echo "== 2. Restart worker + beat (не трогаем redis/postgres) =="
|
||||||
|
docker compose restart worker beat
|
||||||
|
sleep 5
|
||||||
|
echo ""
|
||||||
|
echo "== 3. Clear Redis lock =="
|
||||||
|
docker exec iaai-redis redis-cli del iaai:locks:sync_listing iaai:state:startup_sync_dispatched iaai:state:sync_listing_followup_pending
|
||||||
|
echo ""
|
||||||
|
echo "== 4. Dispatch fresh task =="
|
||||||
|
docker exec iaai-worker python -c "from iaai_scraper.worker.celery_app import celery_app; r=celery_app.send_task('iaai_scraper.worker.tasks.sync_listing_task', kwargs={'only_new': False}, queue='scraping'); print('dispatched:', r.id)"
|
||||||
|
echo ""
|
||||||
|
echo "== 5. Wait 45s, then check =="
|
||||||
|
sleep 45
|
||||||
|
echo ""
|
||||||
|
echo "== LOGS =="
|
||||||
|
docker logs iaai-worker --tail 250 2>&1 | grep -E "Segment|HTTP-mode page [0-9]+0:|HTTP-mode batch|HTTP-mode listing done|Sync run.*finished|upserted=" | tail -25
|
||||||
|
echo ""
|
||||||
|
echo "== DB =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_2min=' || count(*) filter (where last_seen_at > now() - interval '2 minutes'), 'max=' || max(last_seen_at)::text from cars;"
|
||||||
10
_wait_check.sh
Normal file
10
_wait_check.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
sleep 60
|
||||||
|
echo "== DB =="
|
||||||
|
docker exec iaai-postgres psql -U iaai -d iaai_scraper -t -c "select 'total=' || count(*), 'last_3min=' || count(*) filter (where last_seen_at > now() - interval '3 minutes'), 'max=' || max(last_seen_at)::text from cars;"
|
||||||
|
echo ""
|
||||||
|
echo "== BATCH + SEGMENT DONE EVENTS =="
|
||||||
|
docker logs iaai-worker --tail 3000 2>&1 | grep -E "HTTP-mode batch|HTTP-mode listing done: [1-9]|Segment.*done.*upserted=[1-9]|Sync run.*finished: [1-9]" | tail -15
|
||||||
|
echo ""
|
||||||
|
echo "== последние 10 строк =="
|
||||||
|
docker logs iaai-worker --tail 10
|
||||||
12
burst_test.ps1
Normal file
12
burst_test.ps1
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
$ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||||
|
$sw=[Diagnostics.Stopwatch]::StartNew()
|
||||||
|
$results=@()
|
||||||
|
1..15 | ForEach-Object {
|
||||||
|
$id = 45184893 + $_
|
||||||
|
$code = curl.exe -s -o NUL -A $ua -w "%{http_code}" "https://www.iaai.com/VehicleDetail/$id~US"
|
||||||
|
$results += [pscustomobject]@{Idx=$_; Id=$id; Code=$code}
|
||||||
|
}
|
||||||
|
$sw.Stop()
|
||||||
|
$results | Format-Table -AutoSize | Out-String | Set-Content burst_results.txt
|
||||||
|
("elapsed_total_s={0}" -f [math]::Round($sw.Elapsed.TotalSeconds,2)) | Add-Content burst_results.txt
|
||||||
|
("avg_per_page_s={0}" -f [math]::Round($sw.Elapsed.TotalSeconds/15,2)) | Add-Content burst_results.txt
|
||||||
@@ -5,13 +5,13 @@ x-app-env: &app-env
|
|||||||
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
||||||
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/0}
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/0}
|
||||||
IAAI_DATABASE_POOL_RECYCLE_SECONDS: ${IAAI_DATABASE_POOL_RECYCLE_SECONDS:-1800}
|
IAAI_DATABASE_POOL_RECYCLE_SECONDS: ${IAAI_DATABASE_POOL_RECYCLE_SECONDS:-1800}
|
||||||
CELERY_TASK_SOFT_TIME_LIMIT: ${CELERY_TASK_SOFT_TIME_LIMIT:-3300}
|
CELERY_TASK_SOFT_TIME_LIMIT: ${CELERY_TASK_SOFT_TIME_LIMIT:-900}
|
||||||
CELERY_TASK_TIME_LIMIT: ${CELERY_TASK_TIME_LIMIT:-3600}
|
CELERY_TASK_TIME_LIMIT: ${CELERY_TASK_TIME_LIMIT:-1200}
|
||||||
CELERY_BROKER_VISIBILITY_TIMEOUT: ${CELERY_BROKER_VISIBILITY_TIMEOUT:-7200}
|
CELERY_BROKER_VISIBILITY_TIMEOUT: ${CELERY_BROKER_VISIBILITY_TIMEOUT:-2400}
|
||||||
# 0 => без лимита (в коде интерпретируется как None).
|
# 0 => без лимита (в коде интерпретируется как None).
|
||||||
# После первичного полного прохода hourly-режим должен успевать за всеми новыми авто.
|
# После первичного полного прохода hourly-режим должен успевать за всеми новыми авто.
|
||||||
CELERY_BEAT_SYNC_LIMIT: ${CELERY_BEAT_SYNC_LIMIT:-0}
|
CELERY_BEAT_SYNC_LIMIT: ${CELERY_BEAT_SYNC_LIMIT:-0}
|
||||||
CELERY_WORKER_MAX_TASKS_PER_CHILD: ${CELERY_WORKER_MAX_TASKS_PER_CHILD:-5}
|
CELERY_WORKER_MAX_TASKS_PER_CHILD: ${CELERY_WORKER_MAX_TASKS_PER_CHILD:-3}
|
||||||
CELERY_BATCH_SIZE: ${CELERY_BATCH_SIZE:-200}
|
CELERY_BATCH_SIZE: ${CELERY_BATCH_SIZE:-200}
|
||||||
IAAI_PARALLEL_TABS: ${IAAI_PARALLEL_TABS:-40}
|
IAAI_PARALLEL_TABS: ${IAAI_PARALLEL_TABS:-40}
|
||||||
IAAI_BLOCK_RESOURCES: ${IAAI_BLOCK_RESOURCES:-true}
|
IAAI_BLOCK_RESOURCES: ${IAAI_BLOCK_RESOURCES:-true}
|
||||||
@@ -134,6 +134,8 @@ services:
|
|||||||
container_name: iaai-worker
|
container_name: iaai-worker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
|
mem_limit: ${IAAI_WORKER_MEM_LIMIT:-2g}
|
||||||
|
memswap_limit: ${IAAI_WORKER_MEM_LIMIT:-2g}
|
||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
@@ -142,9 +144,9 @@ services:
|
|||||||
stop_grace_period: 60s
|
stop_grace_period: 60s
|
||||||
command: >
|
command: >
|
||||||
celery -A iaai_scraper.worker.celery_app worker
|
celery -A iaai_scraper.worker.celery_app worker
|
||||||
--loglevel=info --concurrency=${CELERY_WORKER_CONCURRENCY:-4} --pool=prefork
|
--loglevel=info --concurrency=${CELERY_WORKER_CONCURRENCY:-2} --pool=prefork
|
||||||
--pidfile=/tmp/celery-worker.pid
|
--pidfile=/tmp/celery-worker.pid
|
||||||
-Q scraping --max-tasks-per-child=${CELERY_WORKER_MAX_TASKS_PER_CHILD:-5}
|
-Q scraping --max-tasks-per-child=${CELERY_WORKER_MAX_TASKS_PER_CHILD:-3}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "test -f /tmp/celery-worker.pid && kill -0 $(cat /tmp/celery-worker.pid)"]
|
test: ["CMD-SHELL", "test -f /tmp/celery-worker.pid && kill -0 $(cat /tmp/celery-worker.pid)"]
|
||||||
interval: 60s
|
interval: 60s
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ def first_non_empty(values: Iterable[Any]) -> Any | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Регулярные выражения для VIN, lot и price.
|
# Регулярные выражения для lot и price.
|
||||||
VIN_RE = re.compile(r"\b([A-HJ-NPR-Z0-9]{17})\b", re.IGNORECASE)
|
|
||||||
LOT_RE = re.compile(r"\b(\d{7,10})\b")
|
LOT_RE = re.compile(r"\b(\d{7,10})\b")
|
||||||
PRICE_RE = re.compile(r"\$\s?([\d,]+(?:\.\d{1,2})?)")
|
PRICE_RE = re.compile(r"\$\s?([\d,]+(?:\.\d{1,2})?)")
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import html as html_module
|
import html as html_module
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
@@ -15,6 +16,13 @@ from urllib.request import Request, urlopen
|
|||||||
|
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
try:
|
||||||
|
import orjson as _orjson # ~3-5× быстрее stdlib json на парсинге
|
||||||
|
_HAS_ORJSON = True
|
||||||
|
except ImportError:
|
||||||
|
_orjson = None
|
||||||
|
_HAS_ORJSON = False
|
||||||
|
|
||||||
from playwright.sync_api import Error as PlaywrightError
|
from playwright.sync_api import Error as PlaywrightError
|
||||||
from playwright.sync_api import BrowserContext, Page, sync_playwright
|
from playwright.sync_api import BrowserContext, Page, sync_playwright
|
||||||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||||
@@ -37,6 +45,69 @@ VEHICLE_ID_RE = re.compile(r"/VehicleDetail/(\d+)(?:~[A-Z]{2})?", re.IGNORECASE)
|
|||||||
HTML_TAG_RE = re.compile(r"<[^>]+>")
|
HTML_TAG_RE = re.compile(r"<[^>]+>")
|
||||||
SCRIPT_STYLE_RE = re.compile(r"<(script|style)[^>]*>.*?</\1>", re.IGNORECASE | re.DOTALL)
|
SCRIPT_STYLE_RE = re.compile(r"<(script|style)[^>]*>.*?</\1>", re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
# ── Anti-stall watchdog ──
|
||||||
|
# Жёсткие верхние границы на per-page операции браузера. Если Playwright "тихо" завис
|
||||||
|
# (частая реакция IAAI на затяжной скрапинг — page hangs без raise), SIGALRM вернёт
|
||||||
|
# управление и существующая recover-логика (_reopen_listing_and_resume) стартует
|
||||||
|
# новый context с нуля.
|
||||||
|
_PAGE_COLLECT_TIMEOUT_S = 90
|
||||||
|
_PAGE_NEXT_TIMEOUT_S = 60
|
||||||
|
# Проактивная ротация контекста отключена: reactive-восстановление
|
||||||
|
# (retry при пустой странице + _reopen_listing_and_resume) уже закрывает
|
||||||
|
# anti-bot соскоки, а проактивная ротация требовала долистывать кликами
|
||||||
|
# после reopen и конфликтовала с лимитом max_nav_pages в recovery-пути.
|
||||||
|
# Включить можно переменной окружения IAAI_CONTEXT_ROTATE_EVERY_PAGES.
|
||||||
|
_CONTEXT_ROTATE_EVERY_PAGES = int(os.environ.get("IAAI_CONTEXT_ROTATE_EVERY_PAGES", "0") or 0)
|
||||||
|
|
||||||
|
|
||||||
|
class PageOperationTimeoutError(Exception):
|
||||||
|
"""Browser page operation exceeded per-op watchdog timeout."""
|
||||||
|
|
||||||
|
|
||||||
|
class _PageOpWatchdog:
|
||||||
|
"""SIGALRM-based watchdog.
|
||||||
|
|
||||||
|
Работает только в главном потоке процесса (Celery prefork child — это ок).
|
||||||
|
В других контекстах — no-op.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, seconds: int, label: str) -> None:
|
||||||
|
self.seconds = max(1, int(seconds))
|
||||||
|
self.label = label
|
||||||
|
self._old_handler = None
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
def _handler(self, signum, frame): # noqa: ARG002
|
||||||
|
raise PageOperationTimeoutError(
|
||||||
|
f"Page operation '{self.label}' exceeded {self.seconds}s watchdog"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
import threading as _threading
|
||||||
|
if _threading.current_thread() is not _threading.main_thread():
|
||||||
|
return self
|
||||||
|
if not hasattr(signal, "SIGALRM"):
|
||||||
|
return self
|
||||||
|
try:
|
||||||
|
self._old_handler = signal.signal(signal.SIGALRM, self._handler)
|
||||||
|
signal.alarm(self.seconds)
|
||||||
|
self._active = True
|
||||||
|
except (ValueError, OSError):
|
||||||
|
# signal можно выставлять только из main thread; в остальном пропускаем.
|
||||||
|
self._active = False
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
if not self._active:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
signal.alarm(0)
|
||||||
|
if self._old_handler is not None:
|
||||||
|
signal.signal(signal.SIGALRM, self._old_handler)
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class IAAIScraper:
|
class IAAIScraper:
|
||||||
|
|
||||||
@@ -120,13 +191,24 @@ class IAAIScraper:
|
|||||||
self.car_mapper = CarMapper()
|
self.car_mapper = CarMapper()
|
||||||
self.persistence = PersistenceService(self.settings)
|
self.persistence = PersistenceService(self.settings)
|
||||||
self._shutdown_requested = False
|
self._shutdown_requested = False
|
||||||
|
# HTTP pool: при parallel_tabs=24 и concurrency=4 пик ≈ 96 конкурентных сокетов;
|
||||||
|
# даём запас до 256, чтобы не упираться в PoolError под всплесками PX/retry.
|
||||||
|
# TCP_NODELAY выключает Nagle: для коротких HTTPS-запросов с keep-alive
|
||||||
|
# это снижает tail-latency на десятки ms.
|
||||||
|
import socket as _socket
|
||||||
|
_socket_options = [
|
||||||
|
(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1),
|
||||||
|
(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1),
|
||||||
|
]
|
||||||
proxy_url = self.settings.proxy.server
|
proxy_url = self.settings.proxy.server
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
_proxy_kwargs: dict = {
|
_proxy_kwargs: dict = {
|
||||||
"num_pools": 4,
|
"num_pools": 8,
|
||||||
"maxsize": 64,
|
"maxsize": 256,
|
||||||
|
"block": False,
|
||||||
"retries": False,
|
"retries": False,
|
||||||
"timeout": urllib3.Timeout(connect=5, read=10),
|
"timeout": urllib3.Timeout(connect=5, read=20),
|
||||||
|
"socket_options": _socket_options,
|
||||||
}
|
}
|
||||||
if self.settings.proxy.username:
|
if self.settings.proxy.username:
|
||||||
_proxy_kwargs["proxy_headers"] = urllib3.make_headers(
|
_proxy_kwargs["proxy_headers"] = urllib3.make_headers(
|
||||||
@@ -136,10 +218,12 @@ class IAAIScraper:
|
|||||||
logger.info("HTTP fast-path using proxy: %s", proxy_url)
|
logger.info("HTTP fast-path using proxy: %s", proxy_url)
|
||||||
else:
|
else:
|
||||||
self._http_pool = urllib3.PoolManager(
|
self._http_pool = urllib3.PoolManager(
|
||||||
num_pools=4,
|
num_pools=8,
|
||||||
maxsize=64,
|
maxsize=256,
|
||||||
|
block=False,
|
||||||
retries=False,
|
retries=False,
|
||||||
timeout=urllib3.Timeout(connect=5, read=10),
|
timeout=urllib3.Timeout(connect=5, read=20),
|
||||||
|
socket_options=_socket_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _new_trace_id(self, prefix: str) -> str:
|
def _new_trace_id(self, prefix: str) -> str:
|
||||||
@@ -515,32 +599,44 @@ class IAAIScraper:
|
|||||||
"all_listing_origin_urls": all_listing_origin_urls,
|
"all_listing_origin_urls": all_listing_origin_urls,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _process_pending_batch(batch_urls: list[str], batch_start: int) -> bool:
|
# ── Pipeline-режим: батчи (HTTP + DB upsert) выполняются в фоне,
|
||||||
nonlocal cars_upserted, cars_failed, images_upserted, failures
|
# пока главный поток продолжает собирать listing-страницы через
|
||||||
if not batch_urls:
|
# Playwright. Это убирает простой Playwright во время HTTP-фазы
|
||||||
return True
|
# (~50-65 сек на 400 машин) и даёт ~25-30% к throughput.
|
||||||
|
# max_workers=1: батчи исполняются последовательно, но ВНЕ
|
||||||
|
# главного потока, чтобы не блокировать сбор listing-а.
|
||||||
|
# Browser fallback откладывается (skip_browser_fallback=True),
|
||||||
|
# потому что Playwright не thread-safe — обработаем после loop'а.
|
||||||
|
batch_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="batch-pipeline")
|
||||||
|
pending_future: Future | None = None
|
||||||
|
pending_future_meta: tuple[int, int] | None = None # (batch_start, batch_size)
|
||||||
|
deferred_fallbacks: list[tuple[str, int]] = []
|
||||||
|
|
||||||
logger.info(
|
def _drain_pending_future() -> bool:
|
||||||
"Processing batch %d-%d of %d",
|
"""Waits for in-flight batch and accumulates its result.
|
||||||
batch_start + 1,
|
|
||||||
batch_start + len(batch_urls),
|
Returns False if a non-recoverable error occurred and remaining
|
||||||
total,
|
batches should be aborted (mirrors the legacy contract).
|
||||||
)
|
"""
|
||||||
try:
|
nonlocal pending_future, pending_future_meta
|
||||||
batch_result = self.sync_batch(batch_urls, lane=lane)
|
nonlocal cars_upserted, cars_failed, images_upserted, failures
|
||||||
cars_upserted += batch_result.get("cars_upserted", 0)
|
if pending_future is None:
|
||||||
cars_failed += batch_result.get("cars_failed", 0)
|
|
||||||
images_upserted += batch_result.get("images_upserted", 0)
|
|
||||||
failures.extend(batch_result.get("failures", []))
|
|
||||||
return True
|
return True
|
||||||
|
future = pending_future
|
||||||
|
meta = pending_future_meta or (0, 0)
|
||||||
|
pending_future = None
|
||||||
|
pending_future_meta = None
|
||||||
|
batch_start, batch_size_actual = meta
|
||||||
|
try:
|
||||||
|
batch_result = future.result()
|
||||||
except PlaywrightError as pw_exc:
|
except PlaywrightError as pw_exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Batch %d-%d browser crashed: %s. Reinitializing browser context.",
|
"Batch %d-%d browser crashed: %s. Reinitializing browser context.",
|
||||||
batch_start + 1,
|
batch_start + 1,
|
||||||
batch_start + len(batch_urls),
|
batch_start + batch_size_actual,
|
||||||
pw_exc,
|
pw_exc,
|
||||||
)
|
)
|
||||||
cars_failed += len(batch_urls)
|
cars_failed += batch_size_actual
|
||||||
failures.append({"vehicle_url": f"batch_{batch_start}", "error": str(pw_exc)})
|
failures.append({"vehicle_url": f"batch_{batch_start}", "error": str(pw_exc)})
|
||||||
try:
|
try:
|
||||||
self._new_context()
|
self._new_context()
|
||||||
@@ -553,12 +649,51 @@ class IAAIScraper:
|
|||||||
logger.error(
|
logger.error(
|
||||||
"Batch %d-%d failed: %s. Continuing with next batch.",
|
"Batch %d-%d failed: %s. Continuing with next batch.",
|
||||||
batch_start + 1,
|
batch_start + 1,
|
||||||
batch_start + len(batch_urls),
|
batch_start + batch_size_actual,
|
||||||
batch_exc,
|
batch_exc,
|
||||||
)
|
)
|
||||||
cars_failed += len(batch_urls)
|
cars_failed += batch_size_actual
|
||||||
failures.append({"vehicle_url": f"batch_{batch_start}", "error": str(batch_exc)})
|
failures.append({"vehicle_url": f"batch_{batch_start}", "error": str(batch_exc)})
|
||||||
return True
|
return True
|
||||||
|
cars_upserted += batch_result.get("cars_upserted", 0)
|
||||||
|
cars_failed += batch_result.get("cars_failed", 0)
|
||||||
|
images_upserted += batch_result.get("images_upserted", 0)
|
||||||
|
failures.extend(batch_result.get("failures", []))
|
||||||
|
deferred_fallbacks.extend(batch_result.get("deferred_fallback_urls", []))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _process_pending_batch(batch_urls: list[str], batch_start: int) -> bool:
|
||||||
|
nonlocal pending_future, pending_future_meta
|
||||||
|
# Drain previous batch first (if still running).
|
||||||
|
if not _drain_pending_future():
|
||||||
|
return False
|
||||||
|
if not batch_urls:
|
||||||
|
return True
|
||||||
|
logger.info(
|
||||||
|
"Processing batch %d-%d of %d (pipelined)",
|
||||||
|
batch_start + 1,
|
||||||
|
batch_start + len(batch_urls),
|
||||||
|
total,
|
||||||
|
)
|
||||||
|
# Cookie header обязательно вычисляем в главном потоке — Playwright
|
||||||
|
# context.cookies() использует greenlet, который нельзя дёргать из background thread'а.
|
||||||
|
cookie_header = self._cookie_header_for_context()
|
||||||
|
pending_future = batch_executor.submit(
|
||||||
|
self.sync_batch,
|
||||||
|
batch_urls,
|
||||||
|
lane=lane,
|
||||||
|
skip_browser_fallback=True,
|
||||||
|
cookie_header_override=cookie_headerkground thread'а.
|
||||||
|
cookie_header = self._cookie_header_for_context()
|
||||||
|
pending_future = batch_executor.submit(
|
||||||
|
self.sync_batch,
|
||||||
|
batch_urls,
|
||||||
|
lane=lane,
|
||||||
|
skip_browser_fallback=True,
|
||||||
|
cookie_header_override=cookie_header,
|
||||||
|
)
|
||||||
|
pending_future_meta = (batch_start, len(batch_urls))
|
||||||
|
return True
|
||||||
|
|
||||||
page = None
|
page = None
|
||||||
try:
|
try:
|
||||||
@@ -570,12 +705,77 @@ class IAAIScraper:
|
|||||||
year_max=year_max,
|
year_max=year_max,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pages_since_rotation = 0
|
||||||
for page_number in range(1, self.settings.listing.max_pages_per_run + 1):
|
for page_number in range(1, self.settings.listing.max_pages_per_run + 1):
|
||||||
|
# Проактивная ротация контекста (опционально, по умолчанию выключена).
|
||||||
|
# Если включена — требует долистывания до page_number после reopen,
|
||||||
|
# поэтому max_nav_pages поднят под реальную глубину.
|
||||||
|
if _CONTEXT_ROTATE_EVERY_PAGES > 0 and pages_since_rotation >= _CONTEXT_ROTATE_EVERY_PAGES:
|
||||||
|
logger.warning(
|
||||||
|
"Rotating browser context at page %d (every %d pages) to reset anti-bot state",
|
||||||
|
page_number, _CONTEXT_ROTATE_EVERY_PAGES,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
page = None
|
||||||
|
try:
|
||||||
|
page, _ = self._reopen_listing_and_resume(
|
||||||
|
target_page_number=page_number,
|
||||||
|
make=make,
|
||||||
|
model=model,
|
||||||
|
listing_url=listing_url,
|
||||||
|
year_min=year_min,
|
||||||
|
year_max=year_max,
|
||||||
|
max_nav_pages=max(page_number, 500),
|
||||||
|
)
|
||||||
|
pages_since_rotation = 0
|
||||||
|
except ListingResumeError as resume_exc:
|
||||||
|
logger.warning(
|
||||||
|
"Context rotation could not resume at page %d (%s) — stopping segment",
|
||||||
|
page_number, resume_exc,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
with _PageOpWatchdog(_PAGE_COLLECT_TIMEOUT_S, f"collect page {page_number}"):
|
||||||
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
||||||
|
except (PageOperationTimeoutError, PlaywrightError) as op_exc:
|
||||||
|
logger.warning(
|
||||||
|
"Page %d collect stalled/failed (%s) — reopening listing and resuming",
|
||||||
|
page_number, op_exc,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
page = None
|
||||||
|
try:
|
||||||
|
page, _ = self._reopen_listing_and_resume(
|
||||||
|
target_page_number=page_number,
|
||||||
|
make=make,
|
||||||
|
model=model,
|
||||||
|
listing_url=listing_url,
|
||||||
|
year_min=year_min,
|
||||||
|
year_max=year_max,
|
||||||
|
max_nav_pages=max(page_number, 500),
|
||||||
|
)
|
||||||
|
pages_since_rotation = 0
|
||||||
|
with _PageOpWatchdog(_PAGE_COLLECT_TIMEOUT_S, f"collect page {page_number} (after reopen)"):
|
||||||
|
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
||||||
|
except (ListingResumeError, PageOperationTimeoutError, PlaywrightError) as resume_exc:
|
||||||
|
logger.warning(
|
||||||
|
"Cannot recover at page %d (%s) — stopping pagination for this segment",
|
||||||
|
page_number, resume_exc,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
pages_info.append({
|
pages_info.append({
|
||||||
"page_number": page_result.page_number,
|
"page_number": page_result.page_number,
|
||||||
"links_found": len(page_result.vehicle_links),
|
"links_found": len(page_result.vehicle_links),
|
||||||
})
|
})
|
||||||
|
pages_since_rotation += 1
|
||||||
|
|
||||||
page_urls = self._extract_page_urls(
|
page_urls = self._extract_page_urls(
|
||||||
page_result,
|
page_result,
|
||||||
@@ -604,6 +804,7 @@ class IAAIScraper:
|
|||||||
listing_url=listing_url,
|
listing_url=listing_url,
|
||||||
year_min=year_min,
|
year_min=year_min,
|
||||||
year_max=year_max,
|
year_max=year_max,
|
||||||
|
max_nav_pages=max(page_number, 500),
|
||||||
)
|
)
|
||||||
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
||||||
page_urls = self._extract_page_urls(
|
page_urls = self._extract_page_urls(
|
||||||
@@ -694,9 +895,21 @@ class IAAIScraper:
|
|||||||
or not page_result.next_page_detected
|
or not page_result.next_page_detected
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
if not self.listing_collector.go_to_next_page(page, expected_page_number=page_number + 1):
|
try:
|
||||||
|
with _PageOpWatchdog(_PAGE_NEXT_TIMEOUT_S, f"next page {page_number + 1}"):
|
||||||
|
next_ok = self.listing_collector.go_to_next_page(page, expected_page_number=page_number + 1)
|
||||||
|
except (PageOperationTimeoutError, PlaywrightError) as nav_exc:
|
||||||
|
logger.warning(
|
||||||
|
"go_to_next_page stalled/failed at page %d (%s) — will reopen",
|
||||||
|
page_number + 1, nav_exc,
|
||||||
|
)
|
||||||
|
next_ok = False
|
||||||
|
if not next_ok:
|
||||||
logger.warning("Failed to navigate to page %d — reopening listing and resuming", page_number + 1)
|
logger.warning("Failed to navigate to page %d — reopening listing and resuming", page_number + 1)
|
||||||
|
try:
|
||||||
page.close()
|
page.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
page = None
|
page = None
|
||||||
try:
|
try:
|
||||||
page, _ = self._reopen_listing_and_resume(
|
page, _ = self._reopen_listing_and_resume(
|
||||||
@@ -706,7 +919,9 @@ class IAAIScraper:
|
|||||||
listing_url=listing_url,
|
listing_url=listing_url,
|
||||||
year_min=year_min,
|
year_min=year_min,
|
||||||
year_max=year_max,
|
year_max=year_max,
|
||||||
|
max_nav_pages=max(page_number + 1, 500),
|
||||||
)
|
)
|
||||||
|
pages_since_rotation = 0
|
||||||
except ListingResumeError as resume_exc:
|
except ListingResumeError as resume_exc:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cannot resume at page %d (%s) — treating pagination as exhausted",
|
"Cannot resume at page %d (%s) — treating pagination as exhausted",
|
||||||
@@ -717,6 +932,35 @@ class IAAIScraper:
|
|||||||
if pending_urls:
|
if pending_urls:
|
||||||
_process_pending_batch(pending_urls, cars_upserted + cars_failed)
|
_process_pending_batch(pending_urls, cars_upserted + cars_failed)
|
||||||
|
|
||||||
|
# Финальный drain последнего in-flight батча.
|
||||||
|
_drain_pending_future()
|
||||||
|
|
||||||
|
# Обрабатываем отложенные browser fallback'и (Playwright теперь свободен).
|
||||||
|
if deferred_fallbacks:
|
||||||
|
fb_pages = min(len(deferred_fallbacks), self._FALLBACK_PARALLEL_PAGES)
|
||||||
|
logger.info(
|
||||||
|
"Processing %d deferred browser fallbacks (%d parallel pages)",
|
||||||
|
len(deferred_fallbacks), fb_pages,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fb_results = self._browser_fallback_parallel(
|
||||||
|
deferred_fallbacks, len(deferred_fallbacks), fb_pages,
|
||||||
|
)
|
||||||
|
fb_records = fb_results.get("records", [])
|
||||||
|
if fb_records:
|
||||||
|
try:
|
||||||
|
upsert_result = self.persistence.upsert_cars_batch(fb_records)
|
||||||
|
cars_upserted += upsert_result["inserted"] + upsert_result["updated"]
|
||||||
|
images_upserted += upsert_result["images_upserted"]
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Deferred fallback upsert failed: %s", exc)
|
||||||
|
cars_failed += len(fb_records)
|
||||||
|
cars_failed += fb_results.get("cars_failed", 0)
|
||||||
|
failures.extend(fb_results.get("failures", []))
|
||||||
|
except Exception as fb_exc:
|
||||||
|
logger.error("Deferred fallback batch failed: %s", fb_exc)
|
||||||
|
cars_failed += len(deferred_fallbacks)
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"sync_listing final progress: pages=%d, discovered=%d, upserted=%d, failed=%d",
|
"sync_listing final progress: pages=%d, discovered=%d, upserted=%d, failed=%d",
|
||||||
len(pages_info),
|
len(pages_info),
|
||||||
@@ -725,6 +969,12 @@ class IAAIScraper:
|
|||||||
cars_failed,
|
cars_failed,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
# Ensure no future is left dangling on exception path.
|
||||||
|
try:
|
||||||
|
_drain_pending_future()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
batch_executor.shutdown(wait=True)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
page.close()
|
page.close()
|
||||||
|
|
||||||
@@ -979,7 +1229,8 @@ class IAAIScraper:
|
|||||||
"""Извлекает IAAI inventoryView.attributes из HTML без браузера.
|
"""Извлекает IAAI inventoryView.attributes из HTML без браузера.
|
||||||
|
|
||||||
Использует json.JSONDecoder.raw_decode для быстрого поиска JSON
|
Использует json.JSONDecoder.raw_decode для быстрого поиска JSON
|
||||||
вместо посимвольного сканирования скобок.
|
вместо посимвольного сканирования скобок. Если получившийся срез
|
||||||
|
целиком отдаётся декодеру — пробуем orjson (3-5× быстрее stdlib).
|
||||||
"""
|
"""
|
||||||
search = "inventoryView"
|
search = "inventoryView"
|
||||||
pos = html.find(search)
|
pos = html.find(search)
|
||||||
@@ -996,6 +1247,19 @@ class IAAIScraper:
|
|||||||
if content_start < 0:
|
if content_start < 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Быстрый путь: вырезаем всё до </script>, пытаемся orjson; иначе stdlib.
|
||||||
|
obj: dict | None = None
|
||||||
|
if _HAS_ORJSON:
|
||||||
|
script_end = html.find("</script>", content_start)
|
||||||
|
if script_end > content_start:
|
||||||
|
# Подрезаем хвост до последней закрывающей скобки JSON.
|
||||||
|
snippet = html[content_start:script_end].rstrip().rstrip(";")
|
||||||
|
try:
|
||||||
|
obj = _orjson.loads(snippet)
|
||||||
|
except Exception:
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
decoder = json.JSONDecoder()
|
decoder = json.JSONDecoder()
|
||||||
try:
|
try:
|
||||||
obj, _ = decoder.raw_decode(html, content_start)
|
obj, _ = decoder.raw_decode(html, content_start)
|
||||||
@@ -1118,11 +1382,19 @@ class IAAIScraper:
|
|||||||
cookie_header: str,
|
cookie_header: str,
|
||||||
user_agent: str,
|
user_agent: str,
|
||||||
) -> CarRecord:
|
) -> CarRecord:
|
||||||
"""Быстрый HTTP-запрос через urllib3 (connection pooling / keep-alive)."""
|
"""Быстрый HTTP-запрос через urllib3 (connection pooling / keep-alive).
|
||||||
|
|
||||||
|
Заголовок Accept-Encoding включает gzip/deflate/br — IAAI отдаёт HTML
|
||||||
|
со сжатием 5-10×, что снижает байты по сети и время декода.
|
||||||
|
urllib3 сам прозрачно распаковывает gzip и deflate; для br нужен
|
||||||
|
пакет `brotli` (он в зависимостях). Если brotli не установлен — он
|
||||||
|
просто не появится в Accept-Encoding и сервер ответит gzip.
|
||||||
|
"""
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": user_agent,
|
"User-Agent": user_agent,
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": self._ACCEPT_ENCODING,
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Pragma": "no-cache",
|
"Pragma": "no-cache",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
@@ -1131,7 +1403,10 @@ class IAAIScraper:
|
|||||||
if cookie_header:
|
if cookie_header:
|
||||||
headers["Cookie"] = cookie_header
|
headers["Cookie"] = cookie_header
|
||||||
|
|
||||||
response = self._http_pool.request("GET", vehicle_url, headers=headers)
|
# decode_content=True — urllib3 сам распакует gzip/deflate/br.
|
||||||
|
response = self._http_pool.request(
|
||||||
|
"GET", vehicle_url, headers=headers, decode_content=True,
|
||||||
|
)
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
raise RuntimeError(f"HTTP fetch failed for {vehicle_url}: {response.status}")
|
raise RuntimeError(f"HTTP fetch failed for {vehicle_url}: {response.status}")
|
||||||
html = response.data.decode("utf-8", errors="ignore")
|
html = response.data.decode("utf-8", errors="ignore")
|
||||||
@@ -1205,6 +1480,10 @@ class IAAIScraper:
|
|||||||
_HTTP_RETRY_DELAYS = (0.4, 1.0) # Задержки между повторами
|
_HTTP_RETRY_DELAYS = (0.4, 1.0) # Задержки между повторами
|
||||||
_FALLBACK_PARALLEL_PAGES = 4 # Параллельных вкладок для fallback
|
_FALLBACK_PARALLEL_PAGES = 4 # Параллельных вкладок для fallback
|
||||||
_FALLBACK_NAV_TIMEOUT_MS = 8000 # Таймаут навигации fallback
|
_FALLBACK_NAV_TIMEOUT_MS = 8000 # Таймаут навигации fallback
|
||||||
|
# gzip+deflate: нативно в urllib3 (zlib, ~0 CPU). Brotli отключён намеренно —
|
||||||
|
# python-brotli декодирует на CPU и под 32 параллельными потоками становится
|
||||||
|
# узким местом (хуже, чем гонять чуть больше байт по сети).
|
||||||
|
_ACCEPT_ENCODING = "gzip, deflate"
|
||||||
|
|
||||||
def _browser_fallback_parallel(
|
def _browser_fallback_parallel(
|
||||||
self,
|
self,
|
||||||
@@ -1359,11 +1638,15 @@ class IAAIScraper:
|
|||||||
"protection_events": protection_events,
|
"protection_events": protection_events,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cookie_header_override: str | None = None,
|
||||||
def sync_batch(
|
def sync_batch(
|
||||||
self,
|
self,
|
||||||
vehicle_urls: list[str],
|
vehicle_urls: list[str],
|
||||||
lane: str = "iaai_cars",
|
lane: str = "iaai_cars",
|
||||||
parallel_tabs: int | None = None,
|
parallel_tabs: int | None = None,
|
||||||
|
*,
|
||||||
|
skip_browser_fallback: bool = False,
|
||||||
|
cookie_header_override: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
trace_id = self._new_trace_id("sync-batch")
|
trace_id = self._new_trace_id("sync-batch")
|
||||||
started_at = time.perf_counter()
|
started_at = time.perf_counter()
|
||||||
@@ -1374,7 +1657,13 @@ class IAAIScraper:
|
|||||||
cars_failed = 0
|
cars_failed = 0
|
||||||
images_upserted = 0
|
images_upserted = 0
|
||||||
records: list[CarRecord] = []
|
records: list[CarRecord] = []
|
||||||
failures: list[dict[str, str]] = []
|
# cookie_header_override передаётся из главного потока при pipeline-вызове —
|
||||||
|
# вычисление cookie_header из background thread'а ломает Playwright sync greenlet.
|
||||||
|
cookie_header = (
|
||||||
|
cookie_header_override
|
||||||
|
if cookie_header_override is not None
|
||||||
|
else self._cookie_header_for_context()
|
||||||
|
|
||||||
http_successes = 0
|
http_successes = 0
|
||||||
http_fallbacks = 0
|
http_fallbacks = 0
|
||||||
protection_events = 0
|
protection_events = 0
|
||||||
@@ -1382,17 +1671,25 @@ class IAAIScraper:
|
|||||||
total = len(vehicle_urls)
|
total = len(vehicle_urls)
|
||||||
logger.info("sync_batch: %d vehicles, %d parallel workers", total, num_workers)
|
logger.info("sync_batch: %d vehicles, %d parallel workers", total, num_workers)
|
||||||
|
|
||||||
cookie_header = self._cookie_header_for_context()
|
# cookie_header_override передаётся из главного потока при pipeline-вызове —
|
||||||
user_agent = (
|
# вычисление cookie_header из background thread'а ломает Playwright sync greenlet.
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) "
|
cookie_header = (
|
||||||
"Gecko/20100101 Firefox/128.0"
|
cookie_header_override
|
||||||
|
if cookie_header_override is not None
|
||||||
|
else self._cookie_header_for_context()
|
||||||
)
|
)
|
||||||
|
# UA должен совпадать с UA Playwright-контекста, иначе Imperva видит
|
||||||
|
# «cookies от Chrome, ходим под Firefox» — типичный сигнал бота.
|
||||||
|
user_agent = self.settings.fingerprint.user_agent
|
||||||
|
|
||||||
# ── Фаза 1: HTTP fast-path ──
|
# ── Фаза 1: HTTP fast-path ──
|
||||||
fallback_urls: list[tuple[str, int]] = []
|
fallback_urls: list[tuple[str, int]] = []
|
||||||
|
|
||||||
def _fetch_one_with_retry(idx_url: tuple[int, str]) -> tuple[int, CarRecord | Exception]:
|
def _fetch_one_with_retry(idx_url: tuple[int, str]) -> tuple[int, CarRecord | Exception]:
|
||||||
idx, url = idx_url
|
idx, url = idx_url
|
||||||
|
# Небольшой анти-бёрст джиттер: разносим старт запросов в пуле,
|
||||||
|
# чтобы N воркеров не били в одну TLS-/PX-волну.
|
||||||
|
time.sleep(random.uniform(0.0, 0.15))
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
for attempt in range(1 + self._HTTP_MAX_RETRIES):
|
for attempt in range(1 + self._HTTP_MAX_RETRIES):
|
||||||
try:
|
try:
|
||||||
@@ -1431,12 +1728,24 @@ class IAAIScraper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ── Фаза 2: браузерный fallback ──
|
# ── Фаза 2: браузерный fallback ──
|
||||||
|
# При skip_browser_fallback=True (pipeline-режим) возвращаем fallback_urls
|
||||||
|
# вызывающему — Playwright занят сбором листинга в главном потоке,
|
||||||
|
# его нельзя трогать из фонового executor'а.
|
||||||
|
deferred_fallback_urls: list[tuple[str, int]] = []
|
||||||
if fallback_urls:
|
if fallback_urls:
|
||||||
|
if skip_browser_fallback:
|
||||||
|
deferred_fallback_urls = fallback_urls
|
||||||
logger.info(
|
logger.info(
|
||||||
"Fallback browser mode: %d/%d URLs, single page",
|
"Deferring browser fallback: %d/%d URLs (pipeline mode)",
|
||||||
len(fallback_urls), total,
|
len(fallback_urls), total,
|
||||||
)
|
)
|
||||||
fb_results = self._browser_fallback_parallel(fallback_urls, total, 1)
|
else:
|
||||||
|
fb_pages = min(len(fallback_urls), self._FALLBACK_PARALLEL_PAGES)
|
||||||
|
logger.info(
|
||||||
|
"Fallback browser mode: %d/%d URLs, %d parallel pages",
|
||||||
|
len(fallback_urls), total, fb_pages,
|
||||||
|
)
|
||||||
|
fb_results = self._browser_fallback_parallel(fallback_urls, total, fb_pages)
|
||||||
records.extend(fb_results["records"])
|
records.extend(fb_results["records"])
|
||||||
cars_failed += fb_results["cars_failed"]
|
cars_failed += fb_results["cars_failed"]
|
||||||
protection_events += fb_results["protection_events"]
|
protection_events += fb_results["protection_events"]
|
||||||
@@ -1475,6 +1784,7 @@ class IAAIScraper:
|
|||||||
"protection_events": protection_events,
|
"protection_events": protection_events,
|
||||||
"elapsed_seconds": round(time.perf_counter() - started_at, 3),
|
"elapsed_seconds": round(time.perf_counter() - started_at, 3),
|
||||||
"failures": failures,
|
"failures": failures,
|
||||||
|
"deferred_fallback_urls": deferred_fallback_urls,
|
||||||
}
|
}
|
||||||
|
|
||||||
def sync_listing(
|
def sync_listing(
|
||||||
|
|||||||
@@ -608,7 +608,10 @@ def sync_listing_task(
|
|||||||
)
|
)
|
||||||
if force_bootstrap_full_scan:
|
if force_bootstrap_full_scan:
|
||||||
_set_full_scan_done(redis_client, False)
|
_set_full_scan_done(redis_client, False)
|
||||||
_enqueue_bootstrap_followup("soft_time_limit_exceeded", count_as_failure=False)
|
# SoftTimeLimit считаем failure для circuit breaker:
|
||||||
|
# если сегмент систематически не укладывается в time limit,
|
||||||
|
# нельзя крутить followup бесконечно.
|
||||||
|
_enqueue_bootstrap_followup("soft_time_limit_exceeded", count_as_failure=True)
|
||||||
# Partial progress уже записан в БД через finish_sync_run.
|
# Partial progress уже записан в БД через finish_sync_run.
|
||||||
# Не retry — следующий запуск продолжит обработку по расписанию.
|
# Не retry — следующий запуск продолжит обработку по расписанию.
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ dependencies = [
|
|||||||
"sqlalchemy>=2.0.32",
|
"sqlalchemy>=2.0.32",
|
||||||
"uvicorn>=0.34.0",
|
"uvicorn>=0.34.0",
|
||||||
"urllib3>=2.0.0",
|
"urllib3>=2.0.0",
|
||||||
|
"orjson>=3.10.0",
|
||||||
|
"brotli>=1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
1
sitemap1.xml
Normal file
1
sitemap1.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemap2.xml
Normal file
1
sitemap2.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemap3.xml
Normal file
1
sitemap3.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemapMake1.xml
Normal file
1
sitemapMake1.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemapMakeModelYear1.xml
Normal file
1
sitemapMakeModelYear1.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemap_test.xml
Normal file
1
sitemap_test.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--List of sitemaps created--><sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemap1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemap2.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemap3.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapbranches1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapauctions1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapMake1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapMakeModel1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapMakeModelYear1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapVehicleTypeMake1.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap><sitemap><loc>https://www.iaai.com/Xj9rDOVMEi0hc38S/sitemapVehicleType.xml</loc><lastmod>2026-04-18T04:03:01-05:00</lastmod></sitemap></sitemapindex>
|
||||||
1
sitemapauctions1.xml
Normal file
1
sitemapauctions1.xml
Normal file
File diff suppressed because one or more lines are too long
1
sitemapbranches1.xml
Normal file
1
sitemapbranches1.xml
Normal file
File diff suppressed because one or more lines are too long
4281
vd_test.html
Normal file
4281
vd_test.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user