Парсинг веб-страниц на Python из Google Cloud Functions

Я хотел бы использовать библиотеку Playwright для очистки веб-страниц внутри Google Cloud Functions.

В целом я новичок в GCP и Python, поэтому на самом деле я не просто ищу решение, а больше изучаю лучшие практики.

Предположим, что мы используем самый простой сценарий использования Playwright для перехода на https://www.google.com, делаем снимок экрана и возвращаем его в браузер с помощью запроса на получение...

До сих пор я пробовал (безуспешно) - ниже:

main.py

import asyncio
from playwright.async_api import async_playwright
from google.cloud import functions_framework
from flask import send_file
import io

@functions_framework.http
def capture_screenshot(request):
    screenshot = asyncio.run(take_screenshot())
    return send_file(
        io.BytesIO(screenshot),
        mimetype='image/png',
        as_attachment=False,
        attachment_filename='screenshot.png'
    )

async def take_screenshot():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto('https://www.google.com')
        screenshot = await page.screenshot(full_page=True)
        await browser.close()
        return screenshot

требования.txt

playwright==1.34.0
Flask==2.3.3
google-cloud-functions==1.0.0

Код развертывается, но когда он запускается, в браузер возвращается следующая ошибка:

500 Внутренняя ошибка сервера: на сервере произошла внутренняя ошибка. и не смог выполнить ваш запрос. Либо сервер перегружено или в приложении возникла ошибка.

Наконец, просматривая логи, я получаю следующее:

[2024-08-11 03:41:08,376] ERROR in app: Exception on / [GET]
Traceback (most recent call last):
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/flask/app.py", line 2190, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/flask/app.py", line 1486, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/flask/app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/flask/app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/functions_framework/__init__.py", line 99, in view_func
    return function(request._get_current_object())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/functions_framework/__init__.py", line 80, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/main.py", line 9, in capture_screenshot
    screenshot = asyncio.run(take_screenshot())
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.runtime/python/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/layers/google.python.runtime/python/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.runtime/python/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/workspace/main.py", line 19, in take_screenshot
    browser = await p.chromium.launch()
              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/playwright/async_api/_generated.py", line 14655, in launch
    await self._impl_obj.launch(
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/playwright/_impl/_browser_type.py", line 95, in launch
    Browser, from_channel(await self._channel.send("launch", params))
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/playwright/_impl/_connection.py", line 61, in send
    return await self._connection.wrap_api_call(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/playwright/_impl/_connection.py", line 482, in wrap_api_call
    return await cb()
           ^^^^^^^^^^
  File "/layers/google.python.pip/pip/lib/python3.11/site-packages/playwright/_impl/_connection.py", line 97, in inner_send
    result = next(iter(done)).result()
             ^^^^^^^^^^^^^^^^^^^^^^^^^
playwright._impl._api_types.Error: Executable doesn't exist at /www-data-home/.cache/ms-playwright/chromium-1064/chrome-linux/chrome
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated.       ║
║ Please run the following command to download new browsers: ║
║                                                            ║
║     playwright install                                     ║
║                                                            ║
║ <3 Playwright Team                                         ║
╚════════════════════════════════════════════════════════════╝

Читая другие сообщения SO и другие фрагменты, которые я обнаружил, проблема, похоже, связана с драйвером Chromium, и было предложено создать Docker непосредственно в Google Run - но не уверен, как это работает - любые предложения или ресурсы будут очень ценю

🤔 А знаете ли вы, что...
Python поддерживает многозадачность и многопоточность.


50
1

Ответ:

Решено

Вероятно, это связано с тем, что драматург ожидает конкретную версию исполняемого файла Chrome, которая может не существовать в вашей среде. Я могу придумать один простой обходной путь: добавить вызов подпроцесса для установки хрома через CLI драматурга при вызове функции.

import subprocess
subprocess.run(["playwright", "install", "chromium"])

поэтому код будет

import asyncio
from playwright.async_api import async_playwright
from google.cloud import functions_framework
from flask import send_file
import io
import subprocess

@functions_framework.http
def capture_screenshot(request):
    subprocess.run(["playwright", "install", "chromium"])
    screenshot = asyncio.run(take_screenshot())
    return send_file(
        io.BytesIO(screenshot),
        mimetype='image/png',
        as_attachment=False,
        attachment_filename='screenshot.png'
    )

async def take_screenshot():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto('https://www.google.com')
        screenshot = await page.screenshot(full_page=True)
        await browser.close()
        return screenshot

В качестве альтернативы, как вы упомянули, вы можете не использовать Google Cloud Functions и вместо этого преобразовать его в веб-приложение Dockerized Python, которое вы можете собрать и развернуть в Cloud Run. Причина использования этого подхода заключается в том, что в Dockerfile веб-приложения вы можете добавить дополнительные шаги настройки и установки в CLI, что невозможно с помощью Google Cloud Functions. В этом случае вы можете добавить строку

RUN pip install -r requirements.txt
RUN playwright install chromium

в файл docker, чтобы среда могла установить все зависимости Python вместе с версией Chrome, совместимой с драматургом, при создании образа для веб-приложения Python.