2025, Dec 14 03:02

Как корректно извлекать URL из встроенного JS: регулярные выражения, BeautifulSoup и Python

Разбираем ошибку с шаблоном /images/* и показываем, как извлечь URL из скрипта с помощью регулярных выражений, Python и BeautifulSoup на сайте ESA Hubble.

Извлекать структурированные данные из встроенных скриптов оказывается коварно, если первым делом попробовать «накинуть» на них быстрый регэксп. В нашей задаче всё просто: достать строковые пути, которые идут после url в блоке скрипта на странице архива галактик, и затем использовать их для запросов изображений в более высоком разрешении. Однако из‑за тонкой ошибки в регулярном выражении первоначальный подход возвращает лишь буквальные вхождения слова images. Разберёмся, что пошло не так и как аккуратно это исправить.

Воспроизводим проблему

Страницу получаем через requests и парсим с помощью BeautifulSoup. Берём первый тег script и пытаемся вытащить пути с помощью регулярки. Но на выходе — массив из обрывков совпадений, где нет нужных URL.

import re
import requests
from bs4 import BeautifulSoup
page_addr = 'https://esahubble.org/images/archive/category/galaxies/page/1/'
resp = requests.get(page_addr)
dom = BeautifulSoup(resp.text, 'html.parser')
js_tag = dom.find('script')
paths = re.findall(r"/images/*", str(js_tag))
print(paths)

Что именно не так и почему

Шаблон /images/* не означает «/images/ и дальше — что угодно». Звёздочка применяется только к символу непосредственно перед ней. В данном случае это «/images/ и затем ноль или больше прямых слэшей», поэтому совпадения сжимаются до буквального фрагмента вокруг images и не захватывают остальную часть пути.

В этом блоке скрипта есть два удобных ориентира. Можно либо захватить значение, идущее после url: '...' (это вернёт полный относительный путь), либо ограничиться только первым сегментом после /images/, если этого достаточно. Оба варианта корректны — всё зависит от того, как вы собираетесь формировать последующие запросы.

Решение: явно сопоставлять значение url

Самый прямой путь — сопоставить поле url и взять значение в кавычках. В результате получится массив строк вроде /images/heic2018b/, которые можно склеить с базовым путём сайта или использовать как есть.

import re
import requests
from bs4 import BeautifulSoup
page_addr = 'https://esahubble.org/images/archive/category/galaxies/page/1/'
resp = requests.get(page_addr)
dom = BeautifulSoup(resp.text, 'html.parser')
js_tag = dom.find('script')
pattern = r"url\s*:\s*'([^']+)'"
collected = re.findall(pattern, str(js_tag))
print(collected)

Если нужен только первый фрагмент после /images/ — без завершающего слэша и более глубоких сегментов — поменяйте шаблон, чтобы он брал именно этот отрезок: /images/[^/]+. Он якорится на /images/ и останавливается у следующего слэша, возвращая значения вроде /images/heic2018b.

import re
import requests
from bs4 import BeautifulSoup
page_addr = 'https://esahubble.org/images/archive/category/galaxies/page/1/'
resp = requests.get(page_addr)
dom = BeautifulSoup(resp.text, 'html.parser')
js_tag = dom.find('script')
subset = re.findall(r"/images/[^/]+", str(js_tag))
print(subset)

Опционально: сохраняем результаты в файл

Если удобнее сохранить извлечённые пути на диск для последующей обработки, можно записать их построчно. Это повторяет подход с сохранением всех найденных значений url.

import re
js_blob = r"""
{
    id: 'heic2018b',
    title: 'Galaxy NGC 2525',
    width: 3657,
    height: 3920,
    src: 'https://cdn.esahubble.org/archives/images/thumb300y/heic2018b.jpg',
    url: '/images/heic2018b/',
    potw: ''
},
{
    id: 'potw1345a',
    title: 'Antennae Galaxies reloaded',
    width: 4240,
    height: 4211,
    src: 'https://cdn.esahubble.org/archives/images/thumb300y/potw1345a.jpg',
    url: '/images/potw1345a/',
    potw: '11 November 2013'
},
{
    id: 'heic0817a',
    title: 'Magnetic monster NGC 1275',
    width: 4633,
    height: 3590,
    src: 'https://cdn.esahubble.org/archives/images/thumb300y/heic0817a.jpg',
    url: '/images/heic0817a/',
    potw: ''
},
"""
selector = r"url\s*:\s*'([^']+)'"
found = re.findall(selector, js_blob)
with open('after_urls.txt', 'w') as fh:
    for item in found:
        fh.write(item + '\n')
print(f"Wrote {len(found)} after URLs to after_urls.txt")

Почему это важно

Небольшие промахи с регулярками часто дают странные, обрывочные результаты — особенно при парсинге полуструктурированного JavaScript внутри HTML. Важно помнить: звёздочка повторяет предшествующий токен и не означает «что угодно». Это понимание помогает избежать скрытых багов и лишних запросов. Привязка шаблонов к устойчивым ключам вроде url и захват значений в кавычках обычно надёжнее и проще в сопровождении для таких задач.

Подводим итоги

При разборе встроенных блоков данных ориентируйтесь на стабильные «якоря» в тексте. На этой странице надёжнее всего вытаскивать значение url — оно даёт ровно те относительные пути, которые нужны. Если вашей задаче достаточно первого сегмента после /images/, сузьте класс символов, чтобы остановиться у ближайшего слэша. Держите конвейер простым: получить страницу, найти скрипт, применить точное регулярное выражение и переиспользовать получившийся массив для последующих запросов.