Skip to content

Commit 6a1c3c0

Browse files
committed
README.md
1 parent fcb23b4 commit 6a1c3c0

1 file changed

Lines changed: 358 additions & 0 deletions

File tree

README.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# 2GIS Scraper PHP
2+
3+
A PHP client library for scraping data from [2GIS](https://2gis.ru) — the largest business directory and map service across Russia, Central Asia, and the Middle East. Extract places, reviews, real estate listings, and job vacancies with typed DTOs.
4+
5+
Powered by [Apify](https://apify.com/) actors under the hood.
6+
7+
## Installation
8+
9+
```bash
10+
composer require scraper-apis/2gis-parser
11+
```
12+
13+
## Quick Start
14+
15+
```php
16+
use TwoGisParser\Client;
17+
18+
$client = new Client('your_apify_api_token');
19+
20+
// Search for restaurants in Moscow
21+
$places = $client->scrapePlaces(
22+
query: ['restaurant'],
23+
location: 'Moscow',
24+
maxResults: 50,
25+
);
26+
27+
foreach ($places as $place) {
28+
echo "{$place->name} — {$place->address}\n";
29+
echo "Rating: {$place->rating} ({$place->reviewCount} reviews)\n";
30+
31+
if ($place->hasContactInfo()) {
32+
echo "Phone: {$place->getFirstPhone()}\n";
33+
echo "Email: {$place->getFirstEmail()}\n";
34+
}
35+
36+
if ($place->hasWebsite()) {
37+
echo "Website: {$place->website}\n";
38+
}
39+
}
40+
```
41+
42+
## Available Methods
43+
44+
The client wraps 4 specialized Apify actors, each optimized for a specific data type.
45+
46+
### 1. Places / Businesses
47+
48+
Search the 2GIS catalog by keyword and city. Returns rich records with 60+ fields including contacts, ratings, schedule, photos, and more. Covers 207 cities across 20 countries.
49+
50+
```php
51+
use TwoGisParser\Language;
52+
use TwoGisParser\Country;
53+
54+
$places = $client->scrapePlaces(
55+
query: ['dentist', 'dental clinic'],
56+
location: 'Saint Petersburg',
57+
maxResults: 200,
58+
language: Language::Russian,
59+
country: Country::Russia,
60+
options: [
61+
'categoryFilter' => ['Dentistry'],
62+
'filterRating' => 'excellent', // 4.5+ rating
63+
'skipClosedPlaces' => true,
64+
'filterCardPayment' => true,
65+
'maxReviews' => 10, // fetch up to 10 reviews per place
66+
'maxPhotos' => 5, // fetch up to 5 photos per place
67+
],
68+
);
69+
```
70+
71+
**Available options for `scrapePlaces()`:**
72+
73+
| Option | Type | Description |
74+
|--------|------|-------------|
75+
| `categoryFilter` | `string[]` | Filter by business categories (fuzzy-matched against 731 categories) |
76+
| `filterRating` | `string` | Min rating: `perfect` (4.9+), `excellent` (4.5+), `pretty_good` (4.0+), `nice` (3.5+), `not_bad` (3.0+) |
77+
| `sortBy` | `string` | Sort by: `rating`, `opened_time`, `name` |
78+
| `filterWebsite` | `string` | `all`, `withWebsite`, `withoutWebsite` |
79+
| `skipClosedPlaces` | `bool` | Skip permanently closed places |
80+
| `searchMatching` | `string` | `all`, `only_includes`, `only_exact` |
81+
| `filterHasPhotos` | `bool` | Only places with photos |
82+
| `filter24h` | `bool` | Only 24/7 places |
83+
| `filterNewPlaces` | `bool` | Only recently opened places |
84+
| `filterDelivery` | `bool` | Has delivery |
85+
| `filterTakeaway` | `bool` | Has takeaway |
86+
| `filterCardPayment` | `bool` | Accepts card payment |
87+
| `filterWifi` | `bool` | WiFi available |
88+
| `filterHasGoods` | `bool` | Has price list/goods on 2GIS |
89+
| `filterAvgPriceMin` | `int` | Min average check (local currency) |
90+
| `filterAvgPriceMax` | `int` | Max average check (local currency) |
91+
| `maxReviews` | `int` | Max reviews per place (0 = disabled, 99999 = all) |
92+
| `reviewsRating` | `string` | `all`, `positive` (4-5), `negative` (1-2) |
93+
| `reviewsWithAnswer` | `bool` | Only reviews with business reply |
94+
| `reviewsStartDate` | `string` | Only reviews after YYYY-MM-DD |
95+
| `reviewsTopic` | `string` | Server-side topic filter |
96+
| `reviewsFilterKeyword` | `string` | Client-side text search within reviews |
97+
| `reviewsSource` | `string` | `all`, `2gis`, `flamp`, `booking` |
98+
| `maxPhotos` | `int` | Max photos per place (0 = disabled, 99999 = all) |
99+
| `photoCategories` | `string[]` | Photo categories: `food_and_drinks`, `interior`, `outside`, `price_list_image`, etc. |
100+
| `scrapeOrgBranches` | `bool` | Scrape all branches of found organizations |
101+
102+
**Place DTO helpers:**
103+
104+
```php
105+
$place->hasContactInfo(); // true if phones or emails exist
106+
$place->getFirstPhone(); // first phone number or null
107+
$place->getFirstEmail(); // first email or null
108+
$place->hasWebsite(); // true if website is set
109+
$place->getCoordinates(); // ['lat' => float, 'lng' => float] or null
110+
```
111+
112+
### 2. Reviews
113+
114+
Extract reviews from specific 2GIS places. Accepts place URLs, search URLs, or branch IDs. Returns flat one-row-per-review records, ideal for CSV/Excel export. Aggregates reviews from 2GIS, Flamp, and Booking.
115+
116+
```php
117+
use TwoGisParser\ReviewsRating;
118+
use TwoGisParser\ReviewsSource;
119+
120+
$reviews = $client->scrapeReviews(
121+
startUrls: [
122+
'https://2gis.ru/moscow/firm/70000001057394703',
123+
'70000001057394704', // plain branch ID also works
124+
],
125+
maxReviews: 500,
126+
reviewsRating: ReviewsRating::Negative,
127+
reviewsSource: ReviewsSource::TwoGis,
128+
options: [
129+
'reviewsStartDate' => '2025-01-01',
130+
'reviewsWithAnswer' => true,
131+
],
132+
);
133+
134+
foreach ($reviews as $review) {
135+
echo "{$review->authorName}: {$review->rating}/5\n";
136+
echo "{$review->text}\n";
137+
138+
if ($review->hasOfficialAnswer()) {
139+
echo "Reply: {$review->getOfficialAnswerText()}\n";
140+
}
141+
}
142+
```
143+
144+
**Available options for `scrapeReviews()`:**
145+
146+
| Option | Type | Description |
147+
|--------|------|-------------|
148+
| `reviewsWithAnswer` | `bool` | Only reviews with business reply |
149+
| `reviewsStartDate` | `string` | Only reviews after YYYY-MM-DD |
150+
| `reviewsTopic` | `string` | Server-side topic filter |
151+
| `reviewsFilterKeyword` | `string` | Client-side keyword filter |
152+
153+
**Review DTO helpers:**
154+
155+
```php
156+
$review->hasOfficialAnswer(); // true if business replied
157+
$review->getOfficialAnswerText(); // reply text or null
158+
$review->hasPhotos(); // true if review has photos
159+
$review->isPositive(); // true if rating >= 4
160+
$review->isNegative(); // true if rating <= 2
161+
```
162+
163+
### 3. Real Estate / Property
164+
165+
Scrape property listings from 2GIS across Russia, Kazakhstan, and Kyrgyzstan. Supports sale/rent for residential and commercial properties, plus daily rentals.
166+
167+
```php
168+
use TwoGisParser\PropertyCategory;
169+
use TwoGisParser\PropertySort;
170+
171+
$properties = $client->scrapeProperties(
172+
location: 'Kazan',
173+
maxResults: 500,
174+
category: PropertyCategory::SaleResidential,
175+
sort: PropertySort::PriceAsc,
176+
options: [
177+
'rooms' => ['2', '3'],
178+
'priceMax' => 15000000,
179+
'areaMin' => 50,
180+
'notFirstFloor' => true,
181+
'notLastFloor' => true,
182+
],
183+
);
184+
185+
foreach ($properties as $property) {
186+
echo "{$property->name}\n";
187+
echo "Price: {$property->getPriceFormatted()}\n";
188+
echo "Area: {$property->area} m², floor {$property->floor}\n";
189+
echo "Address: {$property->address}\n";
190+
}
191+
```
192+
193+
**Available options for `scrapeProperties()`:**
194+
195+
| Option | Type | Description |
196+
|--------|------|-------------|
197+
| `rooms` | `string[]` | Room count: `studio`, `1`, `2`, `3`, `4`, `5` |
198+
| `propertyType` | `string[]` | `flat`, `house`, `land`, `cottage`, `room`, `townhouse`, `share`, `part_house` |
199+
| `newBuilding` | `string[]` | `secondary` (resale), `new` (new building) |
200+
| `priceMin` | `int` | Minimum price |
201+
| `priceMax` | `int` | Maximum price |
202+
| `pricePerMeterMin` | `float` | Min price per m2 |
203+
| `pricePerMeterMax` | `float` | Max price per m2 |
204+
| `areaMin` | `float` | Min area in m2 |
205+
| `areaMax` | `float` | Max area in m2 |
206+
| `floorMin` | `int` | Min floor number |
207+
| `floorMax` | `int` | Max floor number |
208+
| `notFirstFloor` | `bool` | Exclude first floor |
209+
| `notLastFloor` | `bool` | Exclude last floor |
210+
| `floorsInBuildingMin` | `int` | Min building floors |
211+
| `floorsInBuildingMax` | `int` | Max building floors |
212+
| `metroTime` | `string` | Metro proximity: `5`, `10`, `20` (minutes walking) |
213+
| `provider` | `string[]` | Listing providers: `cian`, `domclick` |
214+
215+
**Property DTO helpers:**
216+
217+
```php
218+
$property->hasImages(); // true if images array is not empty
219+
$property->getCoordinates(); // ['lat' => float, 'lng' => float] or null
220+
$property->getPriceFormatted(); // "1,500,000 RUB" or null
221+
```
222+
223+
### 4. Jobs / Vacancies
224+
225+
Scrape job vacancies from 2GIS by city. Supports filtering by 224 job categories and salary range.
226+
227+
```php
228+
$jobs = $client->scrapeJobs(
229+
location: 'Novosibirsk',
230+
maxResults: 300,
231+
salaryMin: 80000,
232+
categoryId: '200', // Developer
233+
);
234+
235+
foreach ($jobs as $job) {
236+
echo "{$job->name} at {$job->orgName}\n";
237+
echo "Salary: {$job->salaryLabel}\n";
238+
echo "Address: {$job->address}\n";
239+
240+
if ($job->hasApplyUrl()) {
241+
echo "Apply: {$job->applyUrl}\n";
242+
}
243+
}
244+
```
245+
246+
**Job DTO helpers:**
247+
248+
```php
249+
$job->hasApplyUrl(); // true if apply URL is set
250+
$job->getCoordinates(); // ['lat' => float, 'lng' => float] or null
251+
```
252+
253+
## Enums Reference
254+
255+
### Language
256+
257+
Supported across all 4 actors. 14 languages covering the 2GIS service area.
258+
259+
| Case | Value | Language |
260+
|------|-------|----------|
261+
| `Auto` | `auto` | Auto-detect |
262+
| `Russian` | `ru` | Russian |
263+
| `English` | `en` | English |
264+
| `Arabic` | `ar` | Arabic |
265+
| `Kazakh` | `kk` | Kazakh |
266+
| `Uzbek` | `uz` | Uzbek |
267+
| `Kyrgyz` | `ky` | Kyrgyz |
268+
| `Armenian` | `hy` | Armenian |
269+
| `Georgian` | `ka` | Georgian |
270+
| `Azerbaijani` | `az` | Azerbaijani |
271+
| `Tajik` | `tg` | Tajik |
272+
| `Czech` | `cs` | Czech |
273+
| `Spanish` | `es` | Spanish |
274+
| `Italian` | `it` | Italian |
275+
276+
### Country
277+
278+
21 countries where 2GIS operates.
279+
280+
| Case | Value |
281+
|------|-------|
282+
| `Auto` | *(auto-detect)* |
283+
| `Russia` | `ru` |
284+
| `Kazakhstan` | `kz` |
285+
| `UAE` | `ae` |
286+
| `Uzbekistan` | `uz` |
287+
| `Kyrgyzstan` | `kg` |
288+
| `Armenia` | `am` |
289+
| `Georgia` | `ge` |
290+
| `Azerbaijan` | `az` |
291+
| `Belarus` | `by` |
292+
| `Tajikistan` | `tj` |
293+
| `SaudiArabia` | `sa` |
294+
| `Bahrain` | `bh` |
295+
| `Kuwait` | `kw` |
296+
| `Qatar` | `qa` |
297+
| `Oman` | `om` |
298+
| `Iraq` | `iq` |
299+
| `Chile` | `cl` |
300+
| `Czechia` | `cz` |
301+
| `Italy` | `it` |
302+
| `Cyprus` | `cy` |
303+
304+
### Other Enums
305+
306+
| Enum | Values |
307+
|------|--------|
308+
| `RatingFilter` | `None`, `Perfect` (4.9+), `Excellent` (4.5+), `PrettyGood` (4.0+), `Nice` (3.5+), `NotBad` (3.0+) |
309+
| `ReviewsRating` | `All`, `Positive` (4-5 stars), `Negative` (1-2 stars) |
310+
| `ReviewsSource` | `All`, `TwoGis`, `Flamp`, `Booking` |
311+
| `PropertyCategory` | `SaleResidential`, `SaleCommercial`, `RentResidential`, `RentCommercial`, `DailyRent` |
312+
| `PropertySort` | `Default`, `PriceAsc`, `PriceDesc`, `AreaAsc`, `AreaDesc` |
313+
314+
## Configuration
315+
316+
```php
317+
use TwoGisParser\Client;
318+
use TwoGisParser\Config;
319+
320+
// Simple — just pass the API token
321+
$client = new Client('your_apify_api_token');
322+
323+
// Advanced — override actor IDs, timeout, or base URL
324+
$client = new Client('token', new Config(
325+
apiToken: 'token',
326+
placesActorId: 'zen-studio/2gis-places-scraper-api',
327+
reviewsActorId: 'zen-studio/2gis-reviews-scraper',
328+
propertyActorId: 'zen-studio/2gis-property-scraper',
329+
jobsActorId: 'zen-studio/2gis-jobs-scraper',
330+
baseUrl: 'https://api.apify.com/v2',
331+
timeout: 900,
332+
));
333+
```
334+
335+
## Error Handling
336+
337+
```php
338+
use TwoGisParser\Exception\ApiException;
339+
use TwoGisParser\Exception\RateLimitException;
340+
341+
try {
342+
$places = $client->scrapePlaces(query: ['cafe'], location: 'Almaty');
343+
} catch (RateLimitException $e) {
344+
// Retry after the suggested delay
345+
sleep($e->retryAfter);
346+
} catch (ApiException $e) {
347+
echo "API error: {$e->getMessage()}\n";
348+
}
349+
```
350+
351+
## Requirements
352+
353+
- PHP 8.3+
354+
- [Apify API token](https://console.apify.com/account/integrations)
355+
356+
## License
357+
358+
MIT

0 commit comments

Comments
 (0)