GoodFirms, это B2B-каталог, связывающий покупателей с IT-поставщиками, разработчиками ПО и агентствами. Каждое публичное объявление содержит структурированные поля, необходимые для исследования конкурентов, оценки рынка и поиска партнёров: название компании, рейтинг, предоставляемые услуги, местоположение, ценовой диапазон почасовой ставки и ссылку на полный профиль. Для тех, кто картирует вертикаль или формирует шортлист поставщиков, публичные данные каталога являются исходным материалом, а их ручной сбор по десяткам агентств медленен и чреват ошибками.
В этом руководстве показано, как надёжно парсить GoodFirms с помощью Python. Вы создадите небольшой работающий скрипт, который получает отрендеренные страницы GoodFirms через Crawling API, собирает записи о компаниях из категорийного листинга, разбирает поля с помощью BeautifulSoup, обрабатывает разбивку на страницы и экспортирует чистые JSON и CSV. Всё руководство ограничено публичными данными бизнес-листинга, а раздел о законности в конце не является формальностью, поэтому прочитайте его перед реальным использованием.
Что вы создадите
Скрипт на Python, который принимает публичный URL категории GoodFirms, обходит разбитые на страницы списки поиска, извлекает структурированную запись на каждую компанию, затем переходит к отдельным профилям для получения более глубоких полей. В качестве примера используются агентства веб-разработки в Лондоне. Извлекаются следующие поля:
- Название компании указанное наименование бизнеса на карточке каталога.
- Рейтинг публичная оценка отзывов в листинге.
- Услуги категория услуг или слоган, под которым компания представлена в каталоге.
- Местоположение город и страна, указанные на карточке.
- Почасовая ставка ценовой диапазон со страницы профиля компании.
- URL профиля канонический линк на полную страницу профиля.
Почему обычный запрос не работает на GoodFirms
Запросите URL категории или профиля GoodFirms обычным HTTP-клиентом, и нередко получите статус 200 лишь с частью данных листинга в теле. Два фактора работают против вас. Во-первых, GoodFirms загружает большую часть сетки каталога и детали профиля в браузере через JavaScript, поэтому исходный HTML является тонкой оболочкой, заполняющейся только после выполнения скриптов страницы. При извлечении карточек компаний из этого первого ответа можно получить частичный набор или пропустить поля с поздним рендерингом. Во-вторых, активный B2B-каталог отслеживает автоматический трафик: IP датацентров и паттерны запросов, не характерные для браузера, ограничиваются, блокируются или получают проверку ещё до получения отрендеренного контента.
Поэтому работающий парсер GoodFirms требует двух вещей в одном запросе: браузера, который рендерит страницу, и IP-адреса, который платформа воспринимает как реального пользователя. Можно собрать это самостоятельно, используя headless-браузер и ротируемые резидентские прокси, но поддерживать всё это в рабочем состоянии и есть основная часть работы. Crawling API объединяет оба требования в одном вызове: отправьте URL с JavaScript-токеном, API рендерит страницу за надёжным IP и возвращает готовый HTML для разбора. Подробнее о том, почему клиентский рендеринг ломает наивные парсеры, см. в руководстве по обходу JavaScript-сайтов.
Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Поскольку GoodFirms заполняет части каталога и страниц профилей на стороне клиента, JS-токен является безопасным выбором по умолчанию: он возвращает готовую разметку, а не тонкую оболочку, которую вернул бы обычный запрос, поэтому у BeautifulSoup есть что разбирать.
Предварительные требования
Перед написанием кода необходимо подготовить несколько вещей. Это не займёт много времени.
Базовые знания Python. Вы должны уметь писать и запускать скрипты Python и устанавливать пакеты через pip. Если вы новичок в разборе данных, руководство по BeautifulSoup станет хорошим дополнением к этому туториалу, а более широкое руководство по парсингу сайтов с Python охватывает основы.
Python 3.8 или выше. Проверьте версию командой python --version. Если её нет, установите с python.org или через дистрибутив, такой как Anaconda, и убедитесь, что Python добавлен в PATH.
Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript (JS) токен со страницы документации аккаунта. Crawlbase предоставляет до 20 000 бесплатных запросов для начала, что вполне достаточно для освоения этого руководства. Обращайтесь с токеном как с паролем: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальную среду для изоляции зависимостей проекта, затем установите необходимые библиотеки.
python --version python -m venv goodfirms_env source goodfirms_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте среду командой goodfirms_env\Scripts\activate вместо строки с source. Две зависимости выполняют всю работу: crawlbase, официальный клиент для Crawling API, а beautifulsoup4 разбирает возвращаемый HTML, позволяя извлекать поля по CSS-селекторам. Модули json и csv входят в стандартную библиотеку, поэтому для экспорта ничего дополнительно устанавливать не нужно.
Шаг 1: получение отрендеренной страницы GoodFirms
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с JS-токеном и запросите URL категории GoodFirms. Передайте ajax_wait и page_wait, чтобы API ждал динамического контента до захвата страницы. Проверка Crawlbase-статуса cb_status (legacy pc_status) перед разбором позволяет явно обнаруживать ошибки.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } def crawl(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None if __name__ == "__main__": listing_url = "https://www.goodfirms.co/companies/web-development-agency/london" html = crawl(listing_url) print(html[:500] if html else "No HTML returned")
Два параметра ожидания важны для клиентски рендеримых целей. ajax_wait указывает API ждать асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы карточки с поздним рендерингом появились до захвата. Пять секунд, разумная отправная точка; увеличьте значение, если результаты возвращаются скудными. Запустите скрипт командой python goodfirms_scraper.py, вы должны увидеть реальную разметку каталога GoodFirms, а не оболочку от обычного запроса. Это подтверждает работу рендеринга перед написанием первого селектора.
GoodFirms требует отрендеренную страницу за надёжным IP в одном вызове, именно это настраивают параметры ajax_wait и page_wait выше. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует резидентские IP на стороне сервера и возвращает готовый HTML, избавляя вас от необходимости управлять парком headless-браузеров и пулом прокси. Начните с публичной страницы категории на бесплатном тарифе.
Шаг 2: определение селекторов листинга
Перед написанием парсера проверьте страницу категории в инструментах разработчика браузера (правый клик и выбор "Inspect", или нажатие Ctrl + Shift + I), чтобы найти элементы, обёртывающие каждую компанию. В листингах поиска GoodFirms каждая карточка компании находится внутри элемента списка, и поля соответствуют следующим селекторам:
-
Название компании находится в элементе
<h3>с классомfirm-name. -
Местоположение, это
<div>с классомfirm-location. -
Категория услуг, это
<div>, вложенный вfirm-content, с классомtagline. -
Рейтинг отображается в
<span>с классомrating-number. -
URL профиля, это атрибут
hrefэлемента<a>с классомvisit-profile, внутриfirm-urls.
Имея их, загрузите отрендеренный HTML в BeautifulSoup и извлеките каждое поле из карточки. Каждый поиск защищён, чтобы отсутствующее поле возвращало значение по умолчанию, а не прерывало выполнение.
from bs4 import BeautifulSoup def extract_company(card): name = card.select_one("h3.firm-name") location = card.select_one("div.firm-location") category = card.select_one("div.firm-content > div.tagline") rating = card.select_one("span.rating-number") link = card.select_one("div.firm-urls > a.visit-profile") return { "name": name.get_text(strip=True) if name else "", "location": location.get_text(strip=True) if location else "", "category": category.get_text(strip=True) if category else "", "rating": rating.get_text(strip=True) if rating else "No rating", "profile_url": link["href"] if link else "", } def parse_listings(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("ul.firm-directory-list > li.firm-wrapper") return [extract_company(card) for card in cards]
Контейнерный селектор ul.firm-directory-list > li.firm-wrapper переходит от списка каталога к каждой карточке компании, а extract_company считывает пять полей изнутри. Встроенные защиты не дают карточке без рейтинга прервать цикл: вместо этого возвращается значение "No rating".
Каталоговые сайты пересматривают разметку без предупреждения, и сгенерированные имена классов могут меняться между посещениями. Считайте приведённые здесь селекторы начальным шаблоном, а не контрактом. Если список возвращается пустым, проверьте живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обновление селекторов нормально для любого производственного парсера.
Шаг 3: обработка разбивки на страницы листинга
Одна страница категории, это срез набора результатов. GoodFirms разбивает на страницы с помощью параметра запроса page, поэтому обходятся каждая страница и собираются записи. Небольшая обёртка с повторными попытками вокруг запроса не даст одной медленной странице прервать выполнение.
import time def fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: print(f"Retrying ({attempt + 1}/{max_retries})...") time.sleep(1) print(f"Unable to fetch {page_url}") return None def scrape_all_pages(base_url, num_pages=5): all_companies = [] for page in range(1, num_pages + 1): url = f"{base_url}?page={page}" print(f"Scraping page {page}...") html = fetch_html(url) if html: all_companies.extend(parse_listings(html)) time.sleep(2) return all_companies
fetch_html повторяет неудачный запрос до двух раз с короткой паузой, возвращая HTML при успехе и None после исчерпания попыток. scrape_all_pages добавляет параметр page, разбирает карточки и ограничивает обход значением num_pages, чтобы большая категория не вышла из-под контроля. Пауза time.sleep(2) между страницами регулирует темп выполнения.
Шаг 4: сборка парсера листингов
Теперь объедините части в один работающий скрипт: обходите страницы, собирайте записи о компаниях и экспортируйте их в JSON и CSV.
import csv import json import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } def crawl(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None def fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: time.sleep(1) return None def extract_company(card): name = card.select_one("h3.firm-name") location = card.select_one("div.firm-location") category = card.select_one("div.firm-content > div.tagline") rating = card.select_one("span.rating-number") link = card.select_one("div.firm-urls > a.visit-profile") return { "name": name.get_text(strip=True) if name else "", "location": location.get_text(strip=True) if location else "", "category": category.get_text(strip=True) if category else "", "rating": rating.get_text(strip=True) if rating else "No rating", "profile_url": link["href"] if link else "", } def parse_listings(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("ul.firm-directory-list > li.firm-wrapper") return [extract_company(card) for card in cards] def scrape_all_pages(base_url, num_pages=5): all_companies = [] for page in range(1, num_pages + 1): url = f"{base_url}?page={page}" print(f"Scraping page {page}...") html = fetch_html(url) if html: all_companies.extend(parse_listings(html)) time.sleep(2) return all_companies def save_outputs(records): with open("goodfirms_companies.json", "w") as f: json.dump(records, f, indent=2) if not records: return with open("goodfirms_companies.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=records[0].keys()) writer.writeheader() writer.writerows(records) def main(): base_url = "https://www.goodfirms.co/companies/web-development-agency/london" companies = scrape_all_pages(base_url, num_pages=3) save_outputs(companies) print(f"Saved {len(companies)} companies") if __name__ == "__main__": main()
Скрипт обходит до трёх страниц категории, разбирает каждую в записи и регулирует цикл двухсекундной паузой. save_outputs записывает и JSON, и CSV, используя ключи первой записи как заголовок, так что данные получаются в нужном формате для последующих инструментов. Измените num_pages и URL категории под нужную вертикаль и город.
Как выглядит результат
Запустите полный скрипт командой python goodfirms_scraper.py и получите чистые структурированные записи на каждую компанию, готовые для анализа, базы данных или таблицы.
[ { "name": "Unified Infotech", "location": "London, United Kingdom", "category": "Driving Digital Transformation with Advanced Tech", "rating": "5.0", "profile_url": "https://www.goodfirms.co/company/unified-infotech" }, { "name": "instinctools", "location": "London, United Kingdom", "category": "Building Custom Software Solutions", "rating": "4.9", "profile_url": "https://www.goodfirms.co/company/instinctools" } ]
Соответствующий CSV содержит те же столбцы, по одной строке на компанию, что позволяет напрямую открыть в pandas или любой таблице для фильтрации по рейтингу, местоположению или категории услуг.
Шаг 5: парсинг страниц профилей компаний
Листинги дают широту охвата; страницы профилей дают глубину. Каждый URL профиля ведёт на страницу с полным описанием компании, ценовым диапазоном почасовой ставки, размером команды, годом основания и услугами. Проверьте страницу профиля аналогичным образом, и более глубокие поля соответствуют следующим селекторам:
-
Название компании, это
<h1>с атрибутомitemprop="name". -
Описание, это
<div>с классомprofile-summary-text. -
Почасовая ставка, это
<span>внутриdiv.profile-pricing. -
Количество сотрудников, это
<span>внутриdiv.profile-employees. -
Год основания, это
<span>внутриdiv.profile-founded. -
Услуги берутся из атрибута
data-nameкаждой<button>вul.services-chart-list.
import re import json import time from bs4 import BeautifulSoup def text_of(soup, selector, default="N/A"): el = soup.select_one(selector) return el.get_text(strip=True) if el else default def extract_profile(html, url): soup = BeautifulSoup(html, "html.parser") summary = soup.select_one("div.profile-summary-text") description = re.sub(r"\s+", " ", summary.get_text(strip=True)) if summary else "N/A" services = [b["data-name"] for b in soup.select("ul.services-chart-list button[data-name]")] return { "name": text_of(soup, 'h1[itemprop="name"]'), "profile_url": url, "description": description, "hourly_rate": text_of(soup, "div.profile-pricing > span"), "no_of_employees": text_of(soup, "div.profile-employees > span"), "year_founded": text_of(soup, "div.profile-founded > span"), "services": services, } def scrape_profiles(profile_urls): profiles = [] for url in profile_urls: print(f"Scraping profile: {url}") html = fetch_html(url) if html: profiles.append(extract_profile(html, url)) time.sleep(2) return profiles if __name__ == "__main__": profile_urls = [ "https://www.goodfirms.co/company/unified-infotech", "https://www.goodfirms.co/company/instinctools", ] data = scrape_profiles(profile_urls) with open("goodfirms_profiles.json", "w") as f: json.dump(data, f, indent=2) print(f"Saved {len(data)} profiles")
Код повторно использует обёртку fetch_html из парсера листингов, так что рендеринг, повторные попытки и JS-токен переносятся. Вызов re.sub(r"\s+", " ") сжимает лишние пробелы, которые нередко присутствуют в описаниях компаний, а список услуг считывает атрибут data-name каждой кнопки диаграммы. Типичная запись профиля выглядит так:
{ "name": "Unified Infotech", "profile_url": "https://www.goodfirms.co/company/unified-infotech", "description": "Unified Infotech is a digital transformation partner serving enterprises with custom web, mobile, and software solutions...", "hourly_rate": "$50 - $99/hr", "no_of_employees": "50 - 249", "year_founded": "2010", "services": [ "Web Development", "Software Development", "Web Designing (UI/UX)", "Mobile App Development", "E-commerce Development" ] }
Передайте значения profile_url из шага листингов в scrape_profiles, и в одном прогоне получите ценовой диапазон и услуги каждой компании, объединив широту охвата и глубину в одном наборе данных.
Как оставаться незаблокированным при масштабировании
Даже при наличии рендеринга активный каталог отслеживает трафик, характерный для парсеров. Несколько привычек помогут поддерживать более длительные прогоны на любой коммерческой цели.
- Соблюдайте темп запросов. Частые запросы к листингам в плотном цикле, самый быстрый способ попасть под ограничения или проверку. Двухсекундные паузы выше, это минимум, а не максимум; увеличивайте для крупных задач и варьируйте цели, а не обходите одну категорию на полной скорости.
- Используйте ротацию. Пул резидентских IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один из них не превысил лимит. Crawling API делает это за вас; если вы строите собственный стек, это ключевой компонент.
-
Следите за статус-кодами. Если прогон начинает возвращать значения
cb_statusне равные 200, это сигнал о том, что текущая частота запросов или IP-уровень уже недостаточны. Воспринимайте это как повод снизить активность, а не как шум.
Для более крупных обходов асинхронный Crawler ставит запросы в очередь и доставляет результаты на вебхук, что подходит для обхода многих страниц категорий без удержания открытых соединений. Общую стратегию см. в статье о том, как парсить сайты без блокировок. Аналогичный подход применим к парсингу Clutch, Superpages и другим локальным бизнес-каталогам.
Законно ли парсить GoodFirms?
Допустимость парсинга GoodFirms зависит от их Условий использования, вашей юрисдикции и того, что вы делаете с данными. GoodFirms ограничивает автоматический доступ и массовый сбор данных в своих условиях, поэтому парсинг может нарушать их независимо от тщательности вашего инструментария. Код в этом руководстве этого не меняет; он лишь делает техническую часть рабочей. Прочитайте Условия использования GoodFirms и его robots.txt перед началом, соблюдайте объявленные лимиты и директивы обхода, и поддерживайте объём запросов достаточно низким, чтобы не нагружать серверы.
Масштаб имеет значение не меньше, чем вежливость. Ограничивайтесь публичными данными бизнес-листинга: названием компании, рейтингом, категорией услуг, местоположением, ценовым диапазоном почасовой ставки и ссылкой на профиль, которые любой посетитель может видеть без аккаунта. В профиле компании могут также отображаться контактные данные бизнеса или конкретных людей, и как только вы собираете или храните что-либо, идентифицирующее человека, вступает в силу законодательство о защите данных. По GDPR вам нужно законное основание для обработки персональных данных, физические лица могут запросить удаление, а если вы используете собранные контактные данные для рассылки, это регулируется GDPR и US CAN-SPAM Act: нужно согласие или иное законное основание, достоверные данные отправителя и работающая отписка. Люди и компании могут отказаться от контакта, и вы обязаны уважать это. Избегайте парсинга чего-либо за логином и не распространяйте оригинальный редакционный контент или текст отзывов GoodFirms полностью, поскольку он защищён авторским правом.
Данное руководство намеренно ограничено публичными страницами листингов и профилей, поскольку это граница, делающая работу обоснованной. Оно не охватывает ничего за аккаунтом, массовый сбор личных контактных данных или попытки обхода аутентификации. Только публичные бизнес-данные. Если вашему проекту нужно больше, правильный путь, разрешённый: GoodFirms публикует данные через собственные каналы и партнёрские договорённости, поэтому проверьте, покрывает ли официальный API или лицензионная лента ваш вариант использования. Это правильный маршрут для коммерческого или массового применения, а не более изощрённый парсер.
Ключевые выводы
- GoodFirms рендерит части страниц на стороне клиента. Обычный запрос может вернуть тонкую оболочку, поэтому рендерите страницу с JS-токеном перед разбором.
-
Нужны рендеринг и надёжный IP вместе. Crawling API с JS-токеном делает оба в одном вызове;
ajax_waitиpage_waitуправляют временем ожидания контента. - Работайте в два слоя. Разбирайте категорийные листинги для получения названия, рейтинга, услуг, местоположения и ссылки на профиль, затем следуйте по URL профиля для получения ценового диапазона, размера команды и списка услуг.
-
Разбивка на страницы и экспорт. Обходите параметр запроса
pageдо предела, регулируйте прогон короткими паузами и записывайте данные в JSON и CSV. - Оставайтесь в рамках публичных бизнес-данных. Соблюдайте ToS и robots.txt GoodFirms, относитесь к любым личным контактным данным как к регулируемым по GDPR и CAN-SPAM с законным основанием и работающей отпиской, и никогда не трогайте логины или защищённый авторским правом редакционный контент.
Часто задаваемые вопросы
Почему обычный запрос возвращает только часть данных GoodFirms?
Потому что GoodFirms загружает части сетки каталога и детали профиля на стороне клиента с помощью JavaScript. Исходный HTML может быть оболочкой, заполняющейся только после выполнения скриптов, поэтому сырой запрос может вернуть статус 200 с отсутствующими карточками или полями профиля. Сначала отрендерите страницу для получения полного набора данных, этим занимается JS-токен Crawling API.
Нужен обычный токен или JS-токен для GoodFirms?
Используйте JS-токен. Обычный токен получает статический HTML, который может пропустить части GoodFirms, рендеримые в браузере. JS-токен сначала запускает страницу в реальном браузере, так что карточки компаний и поля профиля присутствуют при разборе BeautifulSoup.
Какие данные можно парсить с GoodFirms?
Публичные поля бизнес-листинга: название компании, рейтинг, категорию услуг или слоган, местоположение, ценовой диапазон почасовой ставки, размер команды, год основания и ссылку на профиль. Ограничивайтесь данными, видимыми любому посетителю без аккаунта, и относитесь к контактным данным конкретных людей как к персональным данным, выходящим за рамки охвата публичных листингов в этом руководстве.
Мои селекторы возвращают пустые результаты. Что изменилось?
Почти наверняка разметка GoodFirms. Имена классов, такие как firm-name, firm-location и rating-number, и контейнеры профиля, такие как profile-pricing и services-chart-list, могут меняться без предупреждения, поэтому работавшие ранее селекторы могут перестать работать. Проверьте живую страницу в инструментах разработчика браузера и обновите селекторы. Периодическое обновление селекторов нормально для любого производственного парсера.
Как обрабатывать разбивку на страницы в категории?
GoodFirms добавляет параметр запроса page к URL категории. Обходите страницы в цикле, разбирайте карточки компаний на каждой, ограничивайте обход значением num_pages, чтобы большая категория не вышла из-под контроля, и добавляйте короткую паузу между страницами. Функция scrape_all_pages выше демонстрирует полный цикл.
Можно ли использовать спарсенные данные GoodFirms для рассылки или коммерчески?
Воспринимайте это как юридический, а не технический вопрос. Любые собранные контактные данные являются персональными, поэтому рассылка регулируется GDPR и US CAN-SPAM Act: нужно законное основание или согласие, достоверные данные отправителя и работающая отписка, а люди могут отказаться от контакта. Условия использования GoodFirms также ограничивают повторное использование контента. Изучите условия, проверьте, покрывает ли официальный API или лицензионная лента ваш вариант использования, и обратитесь за юридической консультацией перед созданием продукта или списка рассылки на основе этих данных.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
