File size: 10,004 Bytes
3a8438d
 
 
 
 
 
 
 
4a37f72
 
cfeb7c9
8ca7b50
cfeb7c9
 
 
8ca7b50
4a37f72
8ca7b50
4a37f72
 
 
 
 
8ca7b50
3a8438d
 
 
 
 
8ca7b50
3a8438d
 
a088192
 
 
3a8438d
 
a088192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a8438d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfeb7c9
3a8438d
8ca7b50
 
 
 
 
 
 
 
 
 
 
 
 
3a8438d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ca7b50
3a8438d
 
 
cfeb7c9
3a8438d
a088192
8ca7b50
3a8438d
 
 
 
 
 
 
 
8ca7b50
3a8438d
 
a088192
8ca7b50
3a8438d
 
 
 
 
8ca7b50
 
 
 
 
 
3a8438d
cfeb7c9
3a8438d
8ca7b50
 
 
 
 
 
3a8438d
 
cfeb7c9
3a8438d
cfeb7c9
3a8438d
 
 
98c2c47
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import gradio as gr
import pandas as pd
import asyncio
import re
from playwright.async_api import async_playwright
import requests
from bs4 import BeautifulSoup
import tempfile
from spaces import GPU
import os, subprocess, sys

# ---------- Dummy GPU function (ZeroGPU requirement) ----------
@GPU
def dummy_gpu():
    return "GPU OK"
# -----------------------------------------------------------------

# ---------- Ensure Playwright Chromium is installed ----------
chromium_executable = "/home/user/.cache/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-linux64/chrome-headless-shell"
if not os.path.exists(chromium_executable):
    print("Downloading Playwright Chromium...")
    subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
    print("Chromium download complete.")
# -----------------------------------------------------------

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}

# ------------------ Scraping functions ------------------
async def search_google_maps(page, query, max_results):
    try:
        # Load the page and wait until the main content appears
        await page.goto("https://www.google.com/maps", wait_until="load", timeout=40000)
        # Wait a moment for dynamic content
        await page.wait_for_timeout(2000)

        # Accept cookies if the popup appears
        try:
            await page.click('button[aria-label="Accept all"]', timeout=3000)
        except:
            pass

        # Wait for the search box to be present (longer timeout)
        await page.wait_for_selector('input#searchboxinput', state="attached", timeout=30000)
        search_box = page.locator('input#searchboxinput')
        await search_box.fill(query)
        await search_box.press("Enter")

        # Wait for the results feed to appear
        await page.wait_for_selector('div[role="feed"]', timeout=20000)
        feed = page.locator('div[role="feed"]')
        prev = 0
        while True:
            items = await feed.locator('div[role="article"]').all()
            if len(items) >= max_results or len(items) == prev:
                break
            if items:
                await items[-1].scroll_into_view_if_needed()
            await page.wait_for_timeout(2000)
            prev = len(items)
        return (await feed.locator('div[role="article"]').all())[:max_results]
    except Exception as e:
        print(f"Error during search: {e}")
        return []

# (extract_details, scrape_website remain unchanged)
async def extract_details(page, article):
    await article.click()
    await page.wait_for_selector('div[aria-label*="Information for"]', timeout=10000)
    await page.wait_for_timeout(2000)
    details = {}
    try:
        el = page.locator('h1.fontHeadlineLarge')
        details['name'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['name'] = ''
    try:
        el = page.locator('button[data-item-id="address"]')
        details['address'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['address'] = ''
    try:
        el = page.locator('button[data-item-id*="phone"]')
        details['phone'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['phone'] = ''
    try:
        el = page.locator('a[data-item-id*="authority"]')
        details['website'] = await el.get_attribute('href') if await el.count() > 0 else ''
    except:
        details['website'] = ''
    try:
        el = page.locator('div.fontBodyMedium > span[aria-hidden="true"]')
        details['rating'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['rating'] = ''
    try:
        el = page.locator('button[aria-label*="reviews"]')
        txt = await el.inner_text() if await el.count() > 0 else ''
        details['reviews'] = txt.split('(')[-1].split(')')[0] if '(' in txt else ''
    except:
        details['reviews'] = ''
    try:
        el = page.locator('div[aria-label*="Opening hours"]')
        details['hours'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['hours'] = ''
    try:
        el = page.locator('button[jsaction*="pane.rating.category"]')
        details['category'] = await el.inner_text() if await el.count() > 0 else ''
    except:
        details['category'] = ''
    details['maps_url'] = page.url
    try:
        back_btn = page.locator('button[aria-label="Back"]')
        if await back_btn.count() > 0:
            await back_btn.click()
            await page.wait_for_timeout(1000)
    except:
        pass
    return details

def scrape_website(url):
    res = {"emails": [], "phones": [], "socials": []}
    if not url:
        return res
    try:
        r = requests.get(url, headers=HEADERS, timeout=10)
        if r.status_code != 200:
            return res
        soup = BeautifulSoup(r.text, "html.parser")
        text = soup.get_text()
        for a in soup.find_all("a", href=True):
            h = a["href"]
            if h.startswith("mailto:"):
                e = h.replace("mailto:", "").split("?")[0].strip()
                if e and e not in res["emails"]:
                    res["emails"].append(e)
        emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text)
        for e in emails:
            if e not in res["emails"]:
                res["emails"].append(e)
        phones = re.findall(r"(\+?\d{1,3}[-.\s]?)?\(?\d{1,4}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,9}", text)
        res["phones"] = list(set(phones))[:5]
        socials = []
        for a in soup.find_all("a", href=True):
            h = a["href"].lower()
            if any(s in h for s in ["facebook.com", "twitter.com", "linkedin.com", "instagram.com"]):
                socials.append(a["href"])
        res["socials"] = list(set(socials))[:5]
    except:
        pass
    return res

def run_scrape(query, country, city, max_results):
    full_query = query
    if city:
        full_query += f" in {city}"
    if country:
        full_query += f", {country}"

    async def _async():
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                headless=True,
                args=[
                    '--disable-blink-features=AutomationControlled',
                    '--no-sandbox',
                    '--disable-dev-shm-usage',
                ]
            )
            context = await browser.new_context(
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                viewport={"width": 1280, "height": 800}
            )
            page = await context.new_page()
            articles = await search_google_maps(page, full_query, max_results)
            results = []
            for art in articles:
                try:
                    d = await extract_details(page, art)
                    contacts = scrape_website(d.get('website', ''))
                    results.append({
                        "Name": d.get('name', ''),
                        "Address": d.get('address', ''),
                        "Phone (Maps)": d.get('phone', ''),
                        "Website": d.get('website', ''),
                        "Rating": d.get('rating', ''),
                        "Reviews": d.get('reviews', ''),
                        "Opening Hours": d.get('hours', ''),
                        "Category": d.get('category', ''),
                        "Maps URL": d.get('maps_url', ''),
                        "Emails (site)": ", ".join(contacts['emails']),
                        "Extra Phones (site)": ", ".join(contacts['phones']),
                        "Socials": ", ".join(contacts['socials'])
                    })
                except Exception:
                    pass
                await page.wait_for_timeout(1000)
            await context.close()
            await browser.close()
            return results

    data = asyncio.run(_async())
    if not data:
        return pd.DataFrame(), None, "No results found. Try a different query or fewer results."

    df = pd.DataFrame(data)
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
    with pd.ExcelWriter(tmp.name, engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="Results")
    excel_path = tmp.name
    tsv = df.to_csv(sep='\t', index=False)
    return df, excel_path, tsv

# ------------------ Gradio UI ------------------
with gr.Blocks(title="Google Maps Scraper") as demo:
    gr.Markdown("# 🗺️ Google Maps Business Scraper + Contact Finder")
    gr.Markdown("Enter a search query. Use small numbers (5‑10) to avoid timeouts.")

    with gr.Row():
        query = gr.Textbox(label="Search Query", value="parking businesses")
        country = gr.Textbox(label="Country (optional)", value="UK")
        city = gr.Textbox(label="City (optional)", value="London")
    max_results = gr.Slider(minimum=5, maximum=30, value=10, step=1, label="Max Results")

    scrape_btn = gr.Button("Start Scraping", variant="primary")

    dataframe = gr.Dataframe(label="Results", interactive=False)
    download_btn = gr.DownloadButton(label="📥 Download Excel", visible=False)
    tsv_output = gr.Textbox(label="📋 TSV (Copy manually)", visible=False, lines=10)

    def handle_scrape(query, country, city, max_results):
        df, excel_path, tsv = run_scrape(query, country, city, max_results)
        show = bool(excel_path)
        return (
            df,
            gr.update(value=excel_path, visible=show),
            gr.update(value=tsv, visible=show),
        )

    scrape_btn.click(
        fn=handle_scrape,
        inputs=[query, country, city, max_results],
        outputs=[dataframe, download_btn, tsv_output]
    )

if __name__ == "__main__":
    demo.launch(ssr_mode=False)