36c3a7fe7f
По личным телефонам фирмы обратным поиском в 2ГИС вскрываем связанные карточки
холдинга (проекты застройщика) — забираем их сайты и телефоны, цепочкой «до талого».
Общий номер (у >4 фирм) пропускаем; visited-set + потолок карточек; уже известную
главную карточку не дублируем. Защита от AutoMoney: реклама отсечена парсером,
общий номер обрублен до разворота, находки не клеятся молча.
За тумблером AUTOPODBOR_PHONE_REVERSE (по умолчанию ВЫКЛ). Юнит 259/259, feature
146/146. Решение и план — в docs/superpowers/{findings,plans}/2026-07-05-*.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
188 lines
7.3 KiB
PHP
188 lines
7.3 KiB
PHP
<?php
|
|
|
|
use App\Services\Autopodbor\Agent\Fetch\PageFetcher;
|
|
use App\Services\Autopodbor\Agent\Study\PhoneReverseExpander;
|
|
|
|
/** Fake-загрузчик: карта url → html; всё остальное — пустая строка (как реальный xfetch без ключа). */
|
|
function fakeFetcher(array $map): PageFetcher
|
|
{
|
|
return new class($map) implements PageFetcher
|
|
{
|
|
/** @param array<string,string> $map */
|
|
public function __construct(private array $map) {}
|
|
|
|
public function html(string $url): string
|
|
{
|
|
return $this->map[$url] ?? '';
|
|
}
|
|
};
|
|
}
|
|
|
|
it('по одному личному номеру находит карточку проекта: её сайт и телефон', function () {
|
|
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$firm = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
|
|
$fetch = fakeFetcher([
|
|
$search => '<a href="/krasnoyarsk/firm/111">Звезда, жилой комплекс</a>',
|
|
$firm => '{"url":"https://zvezda-dsk.ru","text":"сайт","type":"website"}'
|
|
.'{"type":"phone","value":"+73912002288"}',
|
|
]);
|
|
|
|
$exp = new PhoneReverseExpander($fetch);
|
|
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
$domains = array_column($res->sites, 'domain');
|
|
$phones = array_column($res->phones, 'digits');
|
|
|
|
expect($domains)->toContain('zvezda-dsk.ru')
|
|
->and($phones)->toContain('73912002288');
|
|
});
|
|
|
|
it('общий номер (фирм больше порога) пропускается — находок нет', function () {
|
|
$search = 'https://2gis.ru/krasnoyarsk/search/78001234567';
|
|
|
|
$links = '';
|
|
foreach (range(201, 205) as $id) {
|
|
$links .= '<a href="/krasnoyarsk/firm/'.$id.'">Фирма '.$id.'</a>';
|
|
}
|
|
$fetch = fakeFetcher([$search => $links]);
|
|
|
|
$exp = new PhoneReverseExpander($fetch, crowdedCap: 4);
|
|
$res = $exp->expand('krasnoyarsk', ['78001234567'], []);
|
|
|
|
expect($res->sites)->toBe([])
|
|
->and($res->phones)->toBe([]);
|
|
});
|
|
|
|
it('пустой слаг города — разворот невозможен, пустой результат', function () {
|
|
$exp = new PhoneReverseExpander(fakeFetcher([]));
|
|
$res = $exp->expand('', ['73912273531'], []);
|
|
|
|
expect($res->sites)->toBe([])->and($res->phones)->toBe([]);
|
|
});
|
|
|
|
it('копает до талого: телефон A → карточка с телефоном B → карточка B даёт сайт проекта', function () {
|
|
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
$searchB = 'https://2gis.ru/krasnoyarsk/search/73912002288';
|
|
$firm2 = 'https://2gis.ru/krasnoyarsk/firm/222';
|
|
|
|
$fetch = fakeFetcher([
|
|
$searchA => '<a href="/krasnoyarsk/firm/111">Управляющая компания</a>',
|
|
$firm1 => '{"type":"phone","value":"+73912002288"}',
|
|
$searchB => '<a href="/krasnoyarsk/firm/222">Аллея парк, жилой комплекс</a>',
|
|
$firm2 => '{"url":"https://alleya-park.ru","type":"website"}{"type":"phone","value":"+73912002288"}',
|
|
]);
|
|
|
|
$exp = new PhoneReverseExpander($fetch);
|
|
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
expect(array_column($res->sites, 'domain'))->toContain('alleya-park.ru');
|
|
});
|
|
|
|
it('одну и ту же карточку не открывает дважды (visited-set)', function () {
|
|
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
$searchB = 'https://2gis.ru/krasnoyarsk/search/73912002288';
|
|
|
|
$fetch = new class($searchA, $firm1, $searchB) implements PageFetcher
|
|
{
|
|
public int $opens = 0;
|
|
|
|
public function __construct(private string $sA, private string $f1, private string $sB) {}
|
|
|
|
public function html(string $url): string
|
|
{
|
|
if ($url === $this->sA || $url === $this->sB) {
|
|
return '<a href="/krasnoyarsk/firm/111">Фирма</a>';
|
|
}
|
|
if ($url === $this->f1) {
|
|
$this->opens++;
|
|
|
|
return '{"type":"phone","value":"+73912002288"}';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
};
|
|
|
|
$exp = new PhoneReverseExpander($fetch);
|
|
$exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
expect($fetch->opens)->toBe(1);
|
|
});
|
|
|
|
it('потолок карточек не превышается', function () {
|
|
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$fetch = new class($searchA) implements PageFetcher
|
|
{
|
|
public array $opens = [];
|
|
|
|
public function __construct(private string $sA) {}
|
|
|
|
public function html(string $url): string
|
|
{
|
|
if ($url === $this->sA) {
|
|
return '<a href="/krasnoyarsk/firm/111">A</a><a href="/krasnoyarsk/firm/222">B</a>';
|
|
}
|
|
$this->opens[] = $url;
|
|
|
|
return '{"type":"phone","value":"+7391200'.substr($url, -4).'"}';
|
|
}
|
|
};
|
|
|
|
$exp = new PhoneReverseExpander($fetch, maxFirms: 1);
|
|
$exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
expect(count($fetch->opens))->toBe(1);
|
|
});
|
|
|
|
it('уже известный домен фирмы в находки не повторяет (не дублирует главную карточку)', function () {
|
|
$searchA = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$firm1 = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
|
|
$fetch = fakeFetcher([
|
|
$searchA => '<a href="/krasnoyarsk/firm/111">ДСК</a>',
|
|
$firm1 => '{"url":"https://gk-dsk.ru","type":"website"}',
|
|
]);
|
|
|
|
$exp = new PhoneReverseExpander($fetch);
|
|
$res = $exp->expand('krasnoyarsk', ['73912273531'], ['gk-dsk.ru']);
|
|
|
|
expect(array_column($res->sites, 'domain'))->not->toContain('gk-dsk.ru');
|
|
});
|
|
|
|
it('сайт без схемы в карточке — добавляем https://', function () {
|
|
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$firm = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
$fetch = fakeFetcher([
|
|
$search => '<a href="/krasnoyarsk/firm/111">Проект</a>',
|
|
$firm => '{"url":"zvezda-dsk.ru","type":"website"}',
|
|
]);
|
|
$exp = new PhoneReverseExpander($fetch);
|
|
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
$site = array_values(array_filter($res->sites, fn ($s) => $s['domain'] === 'zvezda-dsk.ru'))[0] ?? null;
|
|
expect($site)->not->toBeNull()
|
|
->and($site['site_url'])->toBe('https://zvezda-dsk.ru');
|
|
});
|
|
|
|
it('ровно порог фирм — НЕ общая линия, находки берём', function () {
|
|
$search = 'https://2gis.ru/krasnoyarsk/search/73912273531';
|
|
$links = '';
|
|
foreach (range(301, 304) as $id) { // ровно 4 = crowdedCap → не общая линия
|
|
$links .= '<a href="/krasnoyarsk/firm/'.$id.'">Фирма '.$id.'</a>';
|
|
}
|
|
$map = [
|
|
$search => $links,
|
|
'https://2gis.ru/krasnoyarsk/firm/301' => '{"url":"https://proekt-301.ru","type":"website"}',
|
|
'https://2gis.ru/krasnoyarsk/firm/302' => '',
|
|
'https://2gis.ru/krasnoyarsk/firm/303' => '',
|
|
'https://2gis.ru/krasnoyarsk/firm/304' => '',
|
|
];
|
|
$exp = new PhoneReverseExpander(fakeFetcher($map), crowdedCap: 4);
|
|
$res = $exp->expand('krasnoyarsk', ['73912273531'], []);
|
|
|
|
expect(array_column($res->sites, 'domain'))->toContain('proekt-301.ru');
|
|
});
|