Соскребая веб-страницу JavaScript с Python
Я пытаюсь разработать простой веб-скребок. Я хочу извлечь текст без кода HTML. На самом деле я достиг этой цели, но я видел, что на некоторых страницах, где загружен JavaScript, я не добился хороших результатов.
Например, если какой-то код JavaScript добавляет текст, я не вижу его, потому что когда я звоню
response = urllib2.urlopen(request)
Я получаю оригинальный текст без добавленного (потому что JavaScript выполняется в клиенте).
Итак, я ищу несколько идей для решения этой проблемы.
19 ответов
РЕДАКТИРОВАТЬ 30/Dec/2017: Этот ответ появляется в топ-результатах поиска Google, поэтому я решил обновить его. Старый ответ еще в конце.
Dryscape больше не поддерживается, и разработчики библиотеки Dryscape рекомендуют использовать только Python 2. Я обнаружил, что использование библиотеки Python Selenium с Phantom JS в качестве веб-драйвера достаточно быстрое и простое для выполнения работы.
После установки Phantom JS убедитесь, что phantomjs
двоичный файл доступен по текущему пути:
phantomjs --version
# result:
2.1.1
пример
Чтобы привести пример, я создал образец страницы со следующим HTML-кодом. ( ссылка):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Javascript scraping test</title>
</head>
<body>
<p id='intro-text'>No javascript support</p>
<script>
document.getElementById('intro-text').innerHTML = 'Yay! Supports javascript';
</script>
</body>
</html>
без JavaScript это говорит: No javascript support
и с помощью JavaScript: Yay! Supports javascript
Выскабливание без поддержки JS:
import requests
from bs4 import BeautifulSoup
response = requests.get(my_url)
soup = BeautifulSoup(response.text)
soup.find(id="intro-text")
# Result:
<p id="intro-text">No javascript support</p>
Соскоб с поддержкой JS:
from selenium import webdriver
driver = webdriver.PhantomJS()
driver.get(my_url)
p_element = driver.find_element_by_id(id_='intro-text')
print(p_element.text)
# result:
'Yay! Supports javascript'
Вы также можете использовать библиотеку Python dryscrape для очистки сайтов, управляемых JavaScript.
Соскоб с поддержкой JS:
import dryscrape
from bs4 import BeautifulSoup
session = dryscrape.Session()
session.visit(my_url)
response = session.body()
soup = BeautifulSoup(response)
soup.find(id="intro-text")
# Result:
<p id="intro-text">Yay! Supports javascript</p>
Мы не получаем правильных результатов, потому что любой контент, сгенерированный javascript, должен отображаться в DOM. Когда мы выбираем HTML-страницу, мы получаем исходную, неизмененную с помощью JavaScript, DOM.
Поэтому нам нужно визуализировать содержимое javascript перед сканированием страницы.
Поскольку селен уже упоминался в этой теме много раз (и иногда упоминалось также о его медленной скорости), я перечислю два других возможных решения.
Решение 1: Это очень хороший урок о том, как использовать Scrapy для сканирования контента, созданного в JavaScript, и мы собираемся следовать этому.
Что нам понадобится:
Докер установлен на нашей машине. До этого момента это преимущество перед другими решениями, поскольку оно использует независимую от ОС платформу.
Установите Splash, следуя инструкциям для нашей соответствующей ОС.
Цитирование из всплеск документации:Splash - это сервис рендеринга JavaScript. Это легкий веб-браузер с HTTP API, реализованный в Python 3 с использованием Twisted и QT5.
По сути, мы будем использовать Splash для визуализации сгенерированного Javascript контента.
Запустите сервер заставки:
sudo docker run -p 8050:8050 scrapinghub/splash
,Установите плагин scrapy-splash:
pip install scrapy-splash
Предполагая, что у нас уже есть проект Scrapy (если нет, давайте создадим его), мы будем следовать руководству и обновлять
settings.py
:Тогда иди в свой проект Scrapy
settings.py
и установите эти промежуточные программы:DOWNLOADER_MIDDLEWARES = { 'scrapy_splash.SplashCookiesMiddleware': 723, 'scrapy_splash.SplashMiddleware': 725, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810, }
URL сервера Splash (если вы используете Win или OSX, это должен быть URL-адрес док-машины: как получить IP-адрес контейнера Docker с хоста?):
SPLASH_URL = 'http://localhost:8050'
И, наконец, вам нужно также установить эти значения:
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter' HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
Наконец, мы можем использовать
SplashRequest
:У обычного паука у вас есть объекты Request, которые вы можете использовать для открытия URL. Если страница, которую вы хотите открыть, содержит данные, сгенерированные JS, вы должны использовать SplashRequest (или SplashFormRequest) для отображения страницы. Вот простой пример:
class MySpider(scrapy.Spider): name = "jsscraper" start_urls = ["http://quotes.toscrape.com/js/"] def start_requests(self): for url in self.start_urls: yield SplashRequest( url=url, callback=self.parse, endpoint='render.html' ) def parse(self, response): for q in response.css("div.quote"): quote = QuoteItem() quote["author"] = q.css(".author::text").extract_first() quote["quote"] = q.css(".text::text").extract_first() yield quote
SplashRequest отображает URL-адрес в виде HTML и возвращает ответ, который вы можете использовать в методе обратного вызова (синтаксический анализ).
Решение 2: Давайте назовем этот эксперимент в настоящий момент (май 2018)...
Это решение предназначено только для версии Python 3.6 (на данный момент).
Знаете ли вы модуль запросов (ну, как нет)?
Теперь у него в Интернете проскакивает маленький брат: запросы-HTML:
Эта библиотека предназначена для того, чтобы сделать анализ HTML (например, просмотр веб-страниц) максимально простым и интуитивно понятным.
Установить запросы-HTML:
pipenv install requests-html
Сделайте запрос на URL страницы:
from requests_html import HTMLSession session = HTMLSession() r = session.get(a_page_url)
Отобразите ответ, чтобы получить сгенерированные Javascript биты:
r.html.render()
Наконец, модуль, кажется, предлагает возможности очистки.
Кроме того, мы можем попробовать хорошо документированный способ использования BeautifulSoup с r.html
объект, который мы только что оказали.
Может быть, селен может сделать это.
from selenium import webdriver
import time
driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
Если вы когда-либо использовали Requests
Модуль для Python ранее, я недавно узнал, что разработчик создал новый модуль под названием Requests-HTML
который теперь также имеет возможность визуализации JavaScript.
Вы также можете посетить https://html.python-requests.org/ чтобы узнать больше об этом модуле, или, если вас интересует только рендеринг JavaScript, вы можете посетить https://html.python-requests.org/? непосредственного изучения того, как использовать модуль для рендеринга JavaScript с использованием Python.
По сути, как только вы правильно установите Requests-HTML
В следующем примере модуля, показанного по вышеуказанной ссылке, показано, как использовать этот модуль для очистки веб-сайта и рендеринга JavaScript, содержащегося на веб-сайте:
from requests_html import HTMLSession
session = HTMLSession()
r = session.get('http://python-requests.org/')
r.html.render()
r.html.search('Python 2 will retire in only {months} months!')['months']
'<time>25</time>' #This is the result.
Я недавно узнал об этом из видео на YouTube. Кликните сюда! смотреть видео на YouTube, которое демонстрирует, как работает модуль.
Похоже, к данным, которые вы действительно ищете, можно получить доступ через вторичный URL, который вызывается каким-то javascript на главной странице.
Хотя вы можете попробовать запустить javascript на сервере, чтобы справиться с этим, более простой подход может состоять в том, чтобы загрузить страницу с помощью Firefox и использовать такой инструмент, как Charles или Firebug, чтобы точно определить, что это за вторичный URL. Затем вы можете просто запросить этот URL непосредственно для данных, которые вас интересуют.
Это также кажется хорошим решением, взятым из отличного поста в блоге.
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
from lxml import html
#Take this class for granted.Just use result of rendering.
class Render(QWebPage):
def __init__(self, url):
self.app = QApplication(sys.argv)
QWebPage.__init__(self)
self.loadFinished.connect(self._loadFinished)
self.mainFrame().load(QUrl(url))
self.app.exec_()
def _loadFinished(self, result):
self.frame = self.mainFrame()
self.app.quit()
url = 'http://pycoders.com/archive/'
r = Render(url)
result = r.frame.toHtml()
# This step is important.Converting QString to Ascii for lxml to process
# The following returns an lxml element tree
archive_links = html.fromstring(str(result.toAscii()))
print archive_links
# The following returns an array containing the URLs
raw_links = archive_links.xpath('//div[@class="campaign"]/a/@href')
print raw_links
Селен лучше всего подходит для очистки содержимого JS и Ajax.
Проверьте эту статью https://likegeeks.com/python-web-scraping/
$ pip install selenium
Затем загрузите веб-драйвер Chrome.
from selenium import webdriver
browser = webdriver.Chrome()
browser.get("https://www.python.org/")
nav = browser.find_element_by_id("mainnav")
print(nav.text)
Легко, правда?
Вы также можете выполнить JavaScript с помощью веб-драйвера.
from selenium import webdriver
driver = webdriver.Firefox()
driver.get(url)
driver.execute_script('document.title')
или сохранить значение в переменной
result = driver.execute_script('var text = document.title ; return var')
Лично я предпочитаю использовать скрап и селен, а также докеризацию в отдельных контейнерах. Таким образом, вы можете установить как с минимальными хлопотами, так и сканировать современные веб-сайты, которые почти все содержат javascript в той или иной форме. Вот пример:
Использовать scrapy startproject
чтобы создать свой скребок и написать свой паук, скелет может быть таким простым:
import scrapy
class MySpider(scrapy.Spider):
name = 'my_spider'
start_urls = ['https://somewhere.com']
def start_requests(self):
yield scrapy.Request(url=self.start_urls[0])
def parse(self, response):
# do stuff with results, scrape items etc.
# now were just checking everything worked
print(response.body)
Настоящая магия происходит в middlewares.py. Перезаписать два метода в промежуточном программном обеспечении загрузчика, __init__
а также process_request
, следующим образом:
# import some additional modules that we need
import os
from copy import deepcopy
from time import sleep
from scrapy import signals
from scrapy.http import HtmlResponse
from selenium import webdriver
class SampleProjectDownloaderMiddleware(object):
def __init__(self):
SELENIUM_LOCATION = os.environ.get('SELENIUM_LOCATION', 'NOT_HERE')
SELENIUM_URL = f'http://{SELENIUM_LOCATION}:4444/wd/hub'
chrome_options = webdriver.ChromeOptions()
# chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
self.driver = webdriver.Remote(command_executor=SELENIUM_URL,
desired_capabilities=chrome_options.to_capabilities())
def process_request(self, request, spider):
self.driver.get(request.url)
# sleep a bit so the page has time to load
# or monitor items on page to continue as soon as page ready
sleep(4)
# if you need to manipulate the page content like clicking and scrolling, you do it here
# self.driver.find_element_by_css_selector('.my-class').click()
# you only need the now properly and completely rendered html from your page to get results
body = deepcopy(self.driver.page_source)
# copy the current url in case of redirects
url = deepcopy(self.driver.current_url)
return HtmlResponse(url, body=body, encoding='utf-8', request=request)
Не забудьте включить эту промежуточную программу, раскомментировав следующие строки в файле settings.py:
DOWNLOADER_MIDDLEWARES = {
'sample_project.middlewares.SampleProjectDownloaderMiddleware': 543,}
Далее для докеризации. Создайте свой Dockerfile
из облегченного образа (я использую Python Alpine здесь), скопируйте в него каталог вашего проекта, установите требования:
# Use an official Python runtime as a parent image
FROM python:3.6-alpine
# install some packages necessary to scrapy and then curl because it's handy for debugging
RUN apk --update add linux-headers libffi-dev openssl-dev build-base libxslt-dev libxml2-dev curl python-dev
WORKDIR /my_scraper
ADD requirements.txt /my_scraper/
RUN pip install -r requirements.txt
ADD . /scrapers
И, наконец, собрать все вместе в docker-compose.yaml
:
version: '2'
services:
selenium:
image: selenium/standalone-chrome
ports:
- "4444:4444"
shm_size: 1G
my_scraper:
build: .
depends_on:
- "selenium"
environment:
- SELENIUM_LOCATION=samplecrawler_selenium_1
volumes:
- .:/my_scraper
# use this command to keep the container running
command: tail -f /dev/null
Бежать docker-compose up -d
, Если вы делаете это в первый раз, потребуется некоторое время, чтобы он извлек последнюю версию селен / автономный хром, а также построил ваш скребковый образ.
Как только это будет сделано, вы можете проверить, что ваши контейнеры работают с docker ps
а также проверьте, совпадает ли имя контейнера селена с той переменной среды, которую мы передали нашему контейнеру скребка (здесь это было SELENIUM_LOCATION=samplecrawler_selenium_1
).
Введите свой скребок контейнер с docker exec -ti YOUR_CONTAINER_NAME sh
команда для меня была docker exec -ti samplecrawler_my_scraper_1 sh
, перейдите в правильный каталог и запустите свой скребок с scrapy crawl my_spider
,
Все это на моей странице GitHub, и вы можете получить его здесь
Сочетание BeautifulSoup и Selenium работает очень хорошо для меня.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup as bs
driver = webdriver.Firefox()
driver.get("http://somedomain/url_that_delays_loading")
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "myDynamicElement"))) #waits 10 seconds until element is located. Can have other wait conditions such as visibility_of_element_located or text_to_be_present_in_element
html = driver.page_source
soup = bs(html, "lxml")
dynamic_text = soup.find_all("p", {"class":"class_name"}) #or other attributes, optional
else:
print("Couldnt locate element")
PS Вы можете найти больше условий ожидания здесь
Использование PyQt5
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEnginePage
import sys
import bs4 as bs
import urllib.request
class Client(QWebEnginePage):
def __init__(self,url):
global app
self.app = QApplication(sys.argv)
QWebEnginePage.__init__(self)
self.html = ""
self.loadFinished.connect(self.on_load_finished)
self.load(QUrl(url))
self.app.exec_()
def on_load_finished(self):
self.html = self.toHtml(self.Callable)
print("Load Finished")
def Callable(self,data):
self.html = data
self.app.quit()
#url = ""
#client_response = Client(url)
#print(client_response.html)
Вы захотите использовать в своем скрипте веб-драйвер urllib, запросы, beautifulSoup и selenium для разных частей страницы (и многие другие).
Иногда вы получите то, что вам нужно, только с одним из этих модулей.
Иногда вам понадобится два, три или все эти модули.
Иногда вам нужно отключить JS в вашем браузере.
Иногда вам понадобится информация заголовка в вашем скрипте.
Ни один веб-сайт не может быть удален одним и тем же способом, и ни один веб-сайт не может быть удален одним и тем же способом навсегда без изменения вашего сканера, обычно через несколько месяцев. Но все они могут быть очищены! Там, где есть воля, есть способ наверняка.
Если в будущем вам понадобятся непрерывные данные, просто соберите все, что вам нужно, и сохраните их в файлах.dat с помощью pickle.
Просто продолжайте искать, как попробовать эти модули, копировать и вставлять свои ошибки в Google.
Драматург-Питон
Еще один вариантplaywright-python
, порт Microsoft Playwright (сама библиотека автоматизации браузера под влиянием Puppeteer) на Python.
Вот минимальный пример выбора элемента и получения его текста:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("http://whatsmyuseragent.org/")
ua = page.query_selector(".user-agent");
print(ua.text_content())
browser.close()
Pyppeteer
Вы можете рассмотреть Pyppeteer, порт Python для интерфейса Puppeteer для драйвера Chrome / Chromium .
Вот простой пример, показывающий, как вы можете использовать Pyppeteer для доступа к данным, которые были динамически введены на страницу:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch({"headless": True})
[page] = await browser.pages()
# normally, you go to a live site...
#await page.goto("http://www.example.com")
# but for this example, just set the HTML directly:
await page.setContent("""
<body>
<script>
// inject content dynamically with JS, not part of the static HTML!
document.body.innerHTML = `<p>hello world</p>`;
</script>
</body>
""")
print(await page.content()) # shows that the `<p>` was inserted
# evaluate a JS expression in browser context and scrape the data
expr = "document.querySelector('p').textContent"
print(await page.evaluate(expr, force_expr=True)) # => hello world
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Попробуйте получить доступ к API напрямую
Обычный сценарий, который вы увидите при парсинге, заключается в том, что данные запрашиваются асинхронно из конечной точки API веб-страницей. Минимальным примером этого может быть следующий сайт:
Не существует универсального способа получения этих данных. Основной метод - использовать BeautifulSoup для доступа к
<script>
текст тега, затем примените регулярное выражение или синтаксический анализ для извлечения структуры объекта, строки JSON или любого другого формата, в котором могут быть данные. Вот доказательство концепции на примере структуры, показанной выше:
import json
import re
from bs4 import BeautifulSoup
# pretend we've already used requests to retrieve the data,
# so we hardcode it for the purposes of this example
text = """
<body>
<script>
var someHardcodedData = {
userId: 1,
id: 1,
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
body: 'quia et suscipit\nsuscipit recusandae con sequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'
};
document.body.textContent = someHardcodedData.title;
</script>
</body>
"""
soup = BeautifulSoup(text, "lxml")
script_text = str(soup.select_one("script"))
pattern = r"title: '(.*?)'"
print(re.search(pattern, script_text, re.S).group(1))
Ознакомьтесь с этими ресурсами для анализа объектов JS, которые не совсем корректны JSON:
- Как преобразовать необработанный объект javascript в словарь Python?
- Как исправить ключевые значения JSON без двойных кавычек?
Вот несколько дополнительных тематических исследований / доказательств концепции, в которых парсинг был обойден с помощью API:
- Beautiful Soup не возвращает значение None для существующего элемента
- Извлечь данные из BeautifulSoup Python
- Очистка коллекций поклонников Bandcamp через POST (используется гибридный подход, при котором на веб-сайт был сделан первоначальный запрос для извлечения токена из разметки с помощью BeautifulSoup, который затем использовался во втором запросе к конечной точке JSON)
Если ничего не помогает, попробуйте одну из многих библиотек динамического парсинга, перечисленных в этом потоке.
Недавно я использовал библиотеку request_html для решения этой проблемы.
Их расширенная документация на readthedocs.io довольно хороша (пропустите аннотированную версию на pypi.org). Если ваш вариант использования является базовым, вы, вероятно, добьетесь успеха.
from requests_html import HTMLSession
session = HTMLSession()
response = session.request(method="get",url="www.google.com/")
response.html.render()
Если у вас возникли проблемы с рендерингом необходимых данных с помощью response.html.render(), вы можете передать некоторый javascript в функцию рендеринга для рендеринга нужного вам объекта js. Это скопировано из их документации, но это может быть именно то, что вам нужно:
Если указан сценарий, он выполнит предоставленный JavaScript во время выполнения. Пример:
script = """
() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio,
}
}
"""
Возвращает возвращаемое значение выполненного скрипта, если таковое предусмотрено:
>>> response.html.render(script=script)
{'width': 800, 'height': 600, 'deviceScaleFactor': 1}
В моем случае данные, которые мне нужны, были массивами, которые заполняли график javascript, но данные не отображались в виде текста где-либо в html. Иногда вообще не ясно, каковы имена объектов для данных, которые вы хотите, если данные заполняются динамически. Если вы не можете отследить объекты js непосредственно из источника просмотра или проверить, вы можете ввести «окно», а затем нажать ENTER в консоли отладчика в браузере (Chrome), чтобы вывести полный список объектов, отображаемых браузером. Если вы сделаете несколько обоснованных предположений о том, где хранятся данные, возможно, вам повезет найти их там. Мои данные графика находились под window.view.data в консоли, поэтому в переменной "script", переданной в метод .render(), указанный выше, я использовал:
return {
data: window.view.data
}
Как уже упоминалось, Selenium - хороший выбор для рендеринга результатов JavaScript:
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
options = Options()
options.headless = True
browser = Firefox(executable_path="/usr/local/bin/geckodriver", options=options)
url = "https://www.example.com"
browser.get(url)
А gazpacho - действительно простая библиотека для анализа визуализированного html:
from gazpacho import Soup
soup = Soup(browser.page_source)
soup.find("a").attrs['href']
Два дня пытаюсь найти ответ на этот вопрос. Многие ответы направляют вас к разным вопросам. Но Serpentr выше ответ Serpentr действительно по Serpentr. Это самое короткое и простое решение. Напоминаем, что последнее слово "var" представляет имя переменной, поэтому его следует использовать как:
result = driver.execute_script('var text = document.title ; return text')
Простое и быстрое решение:
У меня была такая же проблема. Я хочу очистить некоторые данные, созданные с помощью JavaScript. Если я соскребу только текст с этого сайта с помощью BeautifulSoup, то я закончу с тегами в тексте. Я хочу отобразить этот тег и хочу извлечь из него информацию. Кроме того, я не хочу использовать тяжелые фреймворки, такие как Scrapy и selenium.
Итак, я обнаружил, что модуль get метода запросов принимает URL-адреса и фактически отображает тег скрипта.
Пример:
import requests
custom_User_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
url = "https://www.abc.xyz/your/url"
response = requests.get(url, headers={"User-Agent": custom_User_agent})
html_text = response.text
Это отобразит загруженный сайт и отобразит теги.
Надеюсь, это поможет как быстрое и простое решение для рендеринга сайта, загруженного тегами скриптов.