Что будет соскабливаться
Подготовка
Во-первых, нам нужно создать проект Node.js* и добавить npm
пакеты puppeteer
, puppeteer-extra
и puppeteer-extra-plugin-stealth
для управления Chromium (или Chrome, или Firefox, но сейчас мы работаем только с Chromium, который используется по умолчанию) через протокол DevTools в режиме headless или non-headless.
Для этого в директории с нашим проектом откройте командную строку и введите npm init -y
, а затем npm i puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
.
*Если у вас не установлен Node.js, вы можете загрузить его с сайта nodejs.org и следовать документации по установке.
📌Примечание: также вы можете использовать puppeteer
без каких-либо расширений, но я настоятельно рекомендую использовать его с puppeteer-extra
с puppeteer-extra-plugin-stealth
для предотвращения обнаружения веб-сайтом того, что вы используете безголовый Chromium или что вы используете веб-драйвер. Вы можете проверить это на сайте тестов безголового Chrome. На скриншоте ниже показана разница.
Процесс
Расширение SelectorGadget Chrome использовалось для захвата селекторов CSS при нажатии на нужный элемент в браузере. Если у вас возникли трудности с пониманием этого, у нас есть специальная статья в блоге SerpApi, посвященная веб-скрапингу с CSS-селекторами.
Приведенный ниже рисунок иллюстрирует подход к выбору различных частей результатов.
Полный код
📌Примечания:
- Чтобы сделать наш поиск более релевантным, нам нужно добавить параметр GPS координат. Он должен быть построен в следующей последовательности:
@
+latitude
+,
+longitude
+,
+zoom
. Это сформирует строку, которая будет выглядеть следующим образом: например,@47.6040174,-122.1854488,11z
. Параметр масштаба необязателен, но рекомендуется для более высокой точности (он варьируется от3z
, карта полностью уменьшена — до21z
, карта полностью увеличена). На нашем канале YouTube есть специальный видеоролик, объясняющий, что и почему нужно делать с GPS-координатами Google Maps. - Иногда Google отображает результаты из локальных мест с помощью пагинации, а иногда загружает больше результатов по мере прокрутки. Этот код работает в обоих случаях. Если в вашем случае отображается пагинация, вам необходимо откомментировать цикл
while
и внутренние строки в функцииgetLocalPlacesInfo
.
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
puppeteer.use(StealthPlugin());
const requestParams = {
baseURL: `http://google.com`,
query: "starbucks", // what we want to search
coordinates: "@47.6040174,-122.1854488,11z", // parameter defines GPS coordinates of location where you want your query to be applied
hl: "en", // parameter defines the language to use for the Google maps search
};
async function scrollPage(page, scrollContainer) {
let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
while (true) {
await page.evaluate(`document.querySelector("${scrollContainer}").scrollTo(0, document.querySelector("${scrollContainer}").scrollHeight)`);
await page.waitForTimeout(2000);
let newHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
if (newHeight === lastHeight) {
break;
}
lastHeight = newHeight;
}
}
async function fillDataFromPage(page) {
const dataFromPage = await page.evaluate(() => {
return Array.from(document.querySelectorAll(".bfdHYd")).map((el) => {
const placeUrl = el.parentElement.querySelector(".hfpxzc")?.getAttribute("href");
const urlPattern = /!1s(?<id>[^!]+).+!3d(?<latitude>[^!]+)!4d(?<longitude>[^!]+)/gm; // https://regex101.com/r/KFE09c/1
const dataId = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.id)[0];
const latitude = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.latitude)[0];
const longitude = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.longitude)[0];
return {
title: el.querySelector(".qBF1Pd")?.textContent.trim(),
rating: el.querySelector(".MW4etd")?.textContent.trim(),
reviews: el.querySelector(".UY7F9")?.textContent.replace("(", "").replace(")", "").trim(),
type: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(1) > span:first-child")?.textContent.replaceAll("·", "").trim(),
address: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(1) > span:last-child")?.textContent.replaceAll("·", "").trim(),
openState: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(3) > span:first-child")?.textContent.replaceAll("·", "").trim(),
phone: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(3) > span:last-child")?.textContent.replaceAll("·", "").trim(),
website: el.querySelector("a[data-value]")?.getAttribute("href"),
description: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(2)")?.textContent.replace("·", "").trim(),
serviceOptions: el.querySelector(".qty3Ue")?.textContent.replaceAll("·", "").replaceAll(" ", " ").trim(),
gpsCoordinates: {
latitude,
longitude,
},
placeUrl,
dataId,
};
});
});
return dataFromPage;
}
async function getLocalPlacesInfo() {
const browser = await puppeteer.launch({
headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const URL = `${requestParams.baseURL}/maps/search/${requestParams.query}/${requestParams.coordinates}?hl=${requestParams.hl}`;
await page.setDefaultNavigationTimeout(60000);
await page.goto(URL);
await page.waitForNavigation();
const scrollContainer = ".m6QErb[aria-label]";
const localPlacesInfo = [];
// while (true) {
await page.waitForTimeout(2000);
// const nextPageBtn = await page.$("#eY4Fjd:not([disabled])");
// if (!nextPageBtn) break;
await scrollPage(page, scrollContainer);
localPlacesInfo.push(...(await fillDataFromPage(page)));
// await page.click("#eY4Fjd");
// }
await browser.close();
return localPlacesInfo;
}
getLocalPlacesInfo().then(console.log);
Объяснение кода
Объявите константы из необходимых библиотек:
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
Код | Пояснение |
---|---|
puppeteer |
Библиотека управления Chromium |
StealthPlugin |
библиотека для предотвращения обнаружения веб-сайтом того, что вы используете веб-драйвер |
Далее мы «скажем» puppeteer
использовать StealthPlugin
:
puppeteer.use(StealthPlugin());
Далее пишем, что мы хотим искать, и необходимые параметры для запроса:
const requestParams = {
baseURL: `http://google.com`,
query: "starbucks",
coordinates: "@47.6040174,-122.1854488,11z",
hl: "en",
};
Код | Пояснение |
---|---|
query |
поисковый запрос |
coordinates |
параметр определяет GPS-координаты места, где вы хотите применить ваш запрос. Подробнее см. в справке Google Maps |
hl |
параметр определяет язык, который будет использоваться для поиска в Google Maps. |
Далее запишем функцию для прокрутки контейнера мест на странице:
async function scrollPage(page, scrollContainer) {
let lastHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
while (true) {
await page.evaluate(`document.querySelector("${scrollContainer}").scrollTo(0, document.querySelector("${scrollContainer}").scrollHeight)`);
await page.waitForTimeout(2000);
let newHeight = await page.evaluate(`document.querySelector("${scrollContainer}").scrollHeight`);
if (newHeight === lastHeight) {
break;
}
lastHeight = newHeight;
}
}
Код | Объяснение |
---|---|
lastHeight |
текущая высота прокрутки контейнера |
page.evaluate('document.querySelector... |
выполняет код из скобок в консоли браузера и возвращает результат |
page.waitForTimeout(2000) |
ожидание 2000 мс перед продолжением |
newHeight |
высота прокрутки контейнера после прокрутки |
Далее мы запишем функцию для получения информации о местах на странице:
async function fillDataFromPage(page) {
const dataFromPage = await page.evaluate(() => {
return Array.from(document.querySelectorAll(".bfdHYd")).map((el) => {
const placeUrl = el.parentElement.querySelector(".hfpxzc")?.getAttribute("href");
const urlPattern = /!1s(?<id>[^!]+).+!3d(?<latitude>[^!]+)!4d(?<longitude>[^!]+)/gm; // https://regex101.com/r/KFE09c/1
const dataId = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.id)[0];
const latitude = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.latitude)[0];
const longitude = [...placeUrl.matchAll(urlPattern)].map(({ groups }) => groups.longitude)[0];
return {
title: el.querySelector(".qBF1Pd")?.textContent.trim(),
rating: el.querySelector(".MW4etd")?.textContent.trim(),
reviews: el.querySelector(".UY7F9")?.textContent.replace("(", "").replace(")", "").trim(),
type: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(1) > span:first-child")?.textContent.replaceAll("·", "").trim(),
address: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(1) > span:last-child")?.textContent.replaceAll("·", "").trim(),
openState: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(3) > span:first-child")?.textContent.replaceAll("·", "").trim(),
phone: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(3) > span:last-child")?.textContent.replaceAll("·", "").trim(),
website: el.querySelector("a[data-value]")?.getAttribute("href"),
description: el.querySelector(".W4Efsd:last-child > .W4Efsd:nth-of-type(2)")?.textContent.replace("·", "").trim(),
serviceOptions: el.querySelector(".qty3Ue")?.textContent.replaceAll("·", "").replaceAll(" ", " ").trim(),
gpsCoordinates: {
latitude,
longitude,
},
placeUrl,
dataId,
};
});
});
return dataFromPage;
}
Код | Пояснение |
---|---|
document.querySelectorAll(".bfdHYd") |
возвращает статический NodeList, представляющий список элементов документа, которые соответствуют селекторам css с именем класса bfdHYd |
el.querySelector(".qBF1Pd") |
возвращает первый html элемент с селектором .qBF1Pd , который является любым дочерним элементом html элемента el |
.getAttribute("href") |
получает значение атрибута href элемента html |
urlPattern |
шаблон RegEx для поиска и определения id, широты и долготы. Посмотрите, что он позволяет найти |
[...placeUrl.matchAll(urlPattern)] |
в этом коде мы используем синтаксис spread для создания массива из итератора, который был возвращен из метода matchAll (в данном случае эта запись равна Array.from(placeUrl.matchAll(urlPattern)) ) |
.textContent |
получает необработанный текст html-элемента |
.trim() |
удаляет пробельные символы с обоих концов строки |
И, наконец, функция для управления браузером и получения информации:
async function getLocalPlacesInfo() {
const browser = await puppeteer.launch({
headless: false,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const URL = `${requestParams.baseURL}/maps/search/${requestParams.query}/${requestParams.coordinates}?hl=${requestParams.hl}`;
await page.setDefaultNavigationTimeout(60000);
await page.goto(URL);
await page.waitForNavigation();
const scrollContainer = ".m6QErb[aria-label]";
const localPlacesInfo = [];
// while (true) {
await page.waitForTimeout(2000);
// const nextPageBtn = await page.$("#eY4Fjd:not([disabled])");
// if (!nextPageBtn) break;
await scrollPage(page, scrollContainer);
localPlacesInfo.push(...(await fillDataFromPage(page)));
// await page.click("#eY4Fjd");
// }
await browser.close();
return localPlacesInfo;
}
getLocalPlacesInfo().then(console.log);
Код | Пояснение |
---|---|
puppeteer.launch({options}) |
этот метод запускает новый экземпляр браузера Chromium с текущими опциями |
headless |
определяет, какой режим использовать: безголовый (по умолчанию) или безголовый |
args |
массив с аргументами, который используется с Chromium |
["--no-sandbox", "--disable-setuid-sandbox"] |
эти аргументы мы используем для разрешения запуска процесса браузера в онлайн IDE |
browser.newPage() |
этот метод запускает новую страницу |
page.setDefaultNavigationTimeout(60000) |
изменение времени ожидания селекторов по умолчанию (30 сек) на 60000 мс (1 мин) для медленного интернет-соединения |
page.goto(URL) |
навигация по URL , который определен выше |
page.$("#eY4Fjd:not([disabled])") |
этот метод находит html элемент с селектором #eY4Fjd:not([disabled]) и возвращает его |
localPlacesInfo.push(...(await fillDataFromPage(page))) |
в этом коде мы используем синтаксис spread, чтобы разделить массив, возвращаемый функцией fillDataFromPage , на элементы и добавить их в конец массива localPlacesInfo . |
page.click("#eY4Fjd") |
этот метод эмулирует щелчок мыши на html-элементе с селектором #eY4Fjd . |
browser.close() |
После этого мы закрываем экземпляр браузера. |
Теперь мы можем запустить наш парсер. Для этого введите node YOUR_FILE_NAME
в командную строку. Где YOUR_FILE_NAME
— это имя вашего файла .js
.
Выход
📌Примечание: если вы видите в консоли что-то вроде [Object]
, вы можете использовать console.dir(result, { depth: null })
вместо console.log()
. Смотрите документацию Node.js для получения дополнительной информации.
[
[
{
"title":"Starbucks",
"rating":"4.2",
"reviews":"233",
"type":"Coffee shop",
"address":"545 Bellevue Square",
"openState":"Closed ⋅ Opens 7AM",
"phone":"+1 425-452-5534",
"website":"https://www.starbucks.com/store-locator/store/18615/",
"description":"Iconic Seattle-based coffeehouse chain",
"serviceOptions":"Dine-in Takeaway No delivery",
"gpsCoordinates":{
"latitude":"47.617077",
"longitude":"-122.2019599"
},
"placeUrl":"https://www.google.com/maps/place/Starbucks/data=!4m7!3m6!1s0x54906c8f50e36025:0x5175a46aeadfbc0f!8m2!3d47.617077!4d-122.2019599!16s%2Fg%2F1thw6fd9!19sChIJJWDjUI9skFQRD7zf6mqkdVE?authuser=0&hl=en&rclk=1",
"dataId":"0x54906c8f50e36025:0x5175a46aeadfbc0f"
},
{
"title":"Starbucks",
"reviews":"379",
"type":"Coffee shop",
"address":"1785 NE 44th St",
"openState":"Closed ⋅ Opens 4:30AM",
"phone":"+1 425-226-7007",
"website":"https://www.starbucks.com/store-locator/store/10581/",
"description":"Iconic Seattle-based coffeehouse chain",
"serviceOptions":"Dine-in Drive-through Delivery",
"gpsCoordinates":{
"latitude":"47.5319688",
"longitude":"-122.1942498"
},
"placeUrl":"https://www.google.com/maps/place/Starbucks/data=!4m7!3m6!1s0x549069a98254bd17:0xb2f64f75b3edf4c3!8m2!3d47.5319688!4d-122.1942498!16s%2Fg%2F1tdfmzpb!19sChIJF71UgqlpkFQRw_Tts3VP9rI?authuser=0&hl=en&rclk=1",
"dataId":"0x549069a98254bd17:0xb2f64f75b3edf4c3"
},
...and other results
]
API локальных результатов Google Maps
В качестве альтернативы вы можете использовать Google Maps Local Results API от SerpApi. SerpApi — это бесплатный API со 100 поисками в месяц. Если вам нужно больше поисковых запросов, существуют платные тарифные планы.
Разница в том, что вам не придется писать код с нуля и поддерживать его. Вы также можете столкнуться с блокировкой от Google и изменением селекторов, что приведет к поломке парсера. Вместо этого вам просто нужно итерировать структурированный JSON и получать нужные данные. Проверьте игровую площадку.
Во-первых, нам нужно установить google-search-results-nodejs
. Для этого в консоли нужно ввести: npm i google-search-results-nodejs
.
📌Примечание: Чтобы сделать наш поиск более релевантным, нам нужно добавить параметр GPS координат. Он должен быть построен в следующей последовательности: @
+ latitude
+ ,
+ longitude
+ ,
+ zoom
. Это сформирует строку, которая будет выглядеть следующим образом: например, @47.6040174,-122.1854488,11z
. Параметр масштаба необязателен, но рекомендуется для более высокой точности (он варьируется от 3z
, карта полностью уменьшена — до 21z
, карта полностью увеличена). На нашем канале YouTube есть специальный видеоролик, объясняющий, что и почему нужно делать с GPS-координатами Google Maps.
const SerpApi = require("google-search-results-nodejs");
const search = new SerpApi.GoogleSearch(process.env.API_KEY); //your API key from serpapi.com
const searchString = "starbucks"; // what we want to search
const params = {
engine: "google_maps", // search engine
q: searchString, // search query
hl: "en", // parameter defines the language to use for the Google search
ll: "@47.6040174,-122.1854488,11z", // parameter defines GPS coordinates of location where you want your query to be applied
type: "search", // parameter defines the type of search you want to make
};
const getJson = () => {
return new Promise((resolve) => {
search.json(params, resolve);
});
};
const getResults = async () => {
const allPlaces = [];
while (true) {
const json = await getJson();
if (json.local_results) {
allPlaces.push(...json.local_results)
} else break;
if (json.serpapi_pagination?.next) {
!params.start ? (params.start = 20) : (params.start += 20);
} else break;
}
return allPlaces;
};
getResults.then((result) => console.dir(result, { depth: null }));
Объяснение кода
Объявите константы из необходимых библиотек:
const SerpApi = require("google-search-results-nodejs");
const search = new SerpApi.GoogleSearch(API_KEY);
Код | Объяснение |
---|---|
SerpApi |
Библиотека SerpApi Node.js |
search |
новый экземпляр класса GoogleSearch |
API_KEY |
ваш API-ключ от SerpApi |
Далее мы напишем, что мы хотим искать, и необходимые параметры для выполнения запроса:
const searchString = "starbucks";
const params = {
engine: "google_maps",
q: searchString,
hl: "en",
ll: "@47.6040174,-122.1854488,11z", // parameter defines GPS coordinates of location where you want your query to be applied
type: "search", // parameter defines the type of search you want to make
};
Код | Пояснение |
---|---|
searchString |
что мы хотим искать |
engine |
поисковая система |
q |
поисковый запрос |
hl |
параметр определяет язык, который будет использоваться для поиска в Google Scholar |
ll |
параметр определяет GPS-координаты места, где будет применяться запрос |
type |
параметр определяет тип поиска, который вы хотите осуществить. |
Далее мы обернем метод поиска из библиотеки SerpApi в обещание для дальнейшей работы с результатами поиска:
const getJson = () => {
return new Promise((resolve) => {
search.json(params, resolve);
})
}
И, наконец, мы объявляем и запускаем функцию getResult
, которая получает информацию о местах на всех страницах и возвращает ее:
const getResults = async () => {
const allPlaces = [];
while (true) {
const json = await getJson();
if (json.local_results) {
allPlaces.push(...json.local_results)
} else break;
if (json.serpapi_pagination?.next) {
!params.start ? (params.start = 20) : (params.start += 20);
} else break;
}
return allPlaces;
};
getResults().then((result) => console.dir(result, { depth: null }))
Код | Пояснение |
---|---|
allPlaces |
массив со всей информацией о ссылках со всех страниц |
allPlaces.push(...json.local_results) |
в этом коде мы используем синтаксис spread, чтобы разделить массив local_results из результата, который был возвращен из функции getJson на элементы и добавить их в конец массива allPlaces |
console.dir(result, { depth: null }) |
консольный метод dir позволяет использовать объект с необходимыми параметрами для изменения параметров вывода по умолчанию. Смотрите документацию Node.js для получения дополнительной информации |
Вывод
[
{
"position":1,
"title":"Starbucks",
"place_id":"ChIJrxaZdhlBkFQRk-hWRsy4sWA",
"data_id":"0x54904119769916af:0x60b1b8cc4656e893",
"data_cid":"6967553286011807891",
"reviews_link":"https://serpapi.com/search.json?data_id=0x54904119769916af%3A0x60b1b8cc4656e893&engine=google_maps_reviews&hl=en",
"photos_link":"https://serpapi.com/search.json?data_id=0x54904119769916af%3A0x60b1b8cc4656e893&engine=google_maps_photos&hl=en",
"gps_coordinates":{
"latitude":47.544705,
"longitude":-122.38743199999999
},
"place_id_search":"https://serpapi.com/search.json?data=%214m5%213m4%211s0x54904119769916af%3A0x60b1b8cc4656e893%218m2%213d47.544705%214d-122.38743199999999&engine=google_maps&google_domain=google.com&hl=en&start=80&type=place",
"rating":4.2,
"reviews":310,
"price":"$$",
"type":"Coffee shop",
"address":"6501 California Ave SW, Seattle, WA 98136, United States",
"open_state":"Closed ⋅ Opens 5AM",
"hours":"Closed ⋅ Opens 5AM",
"operating_hours":{
"wednesday":"5am–5:30pm",
"thursday":"5am–5:30pm",
"friday":"5am–5:30pm",
"saturday":"5am–5:30pm",
"sunday":"5am–5:30pm",
"monday":"5am–5:30pm",
"tuesday":"5am–5:30pm"
},
"phone":"+1 206-938-6371",
"website":"https://www.starbucks.com/store-locator/store/18390/",
"description":"Iconic Seattle-based coffeehouse chain. Seattle-based coffeehouse chain known for its signature roasts, light bites and WiFi availability.",
"service_options":{
"dine_in":true,
"drive_through":true,
"delivery":true
},
"thumbnail":"https://lh5.googleusercontent.com/p/AF1QipOSvSFJ7cD_s3pemaRs_TjEQe2_aVAy_NhUZVgN=w80-h106-k-no"
},
...and other results
]
Ссылки
- Код в онлайн IDE
- Google Maps Local Results API
Если вы хотите увидеть некоторые проекты, сделанные с помощью SerpApi, пожалуйста, напишите мне сообщение.
Присоединяйтесь к нам на Twitter | YouTube
Add a Feature Request💫 or a Bug🐞