如何使用 Python 抓取包含动态内容(由 JavaScript 创建)的页面?
- 2024-11-18 08:41:00
- admin 原创
- 20
问题描述:
我正在尝试开发一个简单的网页抓取工具。我想提取没有 HTML 标记的纯文本。我的代码适用于纯(静态)HTML,但当内容由页面中嵌入的 JavaScript 生成时则不适用。
特别是,当我使用它urllib2.urlopen(request)
读取页面内容时,它不会显示任何由 JavaScript 代码添加的内容,因为该代码不会在任何地方执行。通常它会由 Web 浏览器运行,但这不是我的程序的一部分。
我如何从我的 Python 代码中访问此动态内容?
另请参阅scrapy 是否可以用于从使用 AJAX 的网站抓取动态内容?以获取有关 Scrapy 的特定答案。
另请参阅如何使用 python 中的 selenium webdriver 滚动网页?以通过 Selenium 处理特定类型的动态内容。
解决方案 1:
编辑于 2021 年 9 月:phantomjs
也不再维护
编辑于 2017 年 12 月 30 日:这个答案出现在 Google 搜索的顶部结果中,所以我决定更新它。旧答案仍然在最后。
dryscape 不再维护,并且 dryscape 开发人员推荐的库仅适用于 Python 2。我发现使用 Selenium 的 Python 库和 Phantom JS 作为 Web 驱动程序速度足够快,并且可以轻松完成工作。
安装Phantom JS后,请确保phantomjs
二进制文件在当前路径中可用:
phantomjs --version
# result:
2.1.1
Example 为了举例,我创建了一个示例页面,其中包含以下 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 支持的 #Scraping:
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>
解决方案 2:
我们没有得到正确的结果,因为任何 javascript 生成的内容都需要在 DOM 上呈现。当我们获取 HTML 页面时,我们会获取初始的、未经 javascript 修改的 DOM。
因此我们需要在抓取页面之前呈现 javascript 内容。
由于这个线程中已经多次提到了硒(并且还提到了它有时会变得多么慢),所以我将列出另外两种可能的解决方案。
解决方案 1:这是一个关于如何使用 Scrapy 抓取 javascript 生成的内容的非常好的教程,我们将遵循该教程。
我们需要:
我们的机器上安装了Docker。到目前为止,这比其他解决方案更具优势,因为它采用了独立于操作系统的平台。
按照我们对应操作系统的说明安装 Splash
。引用自 splash 文档:
Splash 是一种 JavaScript 渲染服务。它是一款具有 HTTP API 的轻量级 Web 浏览器,使用 Twisted 和 QT5 在 Python 3 中实现。
本质上我们将使用 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, }
Splash 服务器的 URL(如果您使用的是 Win 或 OSX,这应该是 docker 机器的 URL:如何从主机获取 Docker 容器的 IP 地址?):
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 年 5 月)我们将其称为实验性的...
此解决方案仅适用于 Python 版本 3.6(目前)。
你知道请求模块吗(谁不知道呢)?
现在它有一个网页爬取模块:requests-HTML:
这个库旨在使解析 HTML(例如抓取网页)尽可能简单和直观。
安装requests-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
解决方案 3:
也许硒可以做到这一点。
from selenium import webdriver
import time
driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
解决方案 4:
如果您以前曾经使用过Requests
Python 模块,我最近发现开发人员创建了一个名为 的新模块Requests-HTML
,该模块现在还具有呈现 JavaScript 的能力。
您还可以访问https://html.python-requests.org/了解有关此模块的更多信息,或者如果您只对渲染 JavaScript 感兴趣,那么您可以访问https://html.python-requests.org/?#javascript-support直接了解如何使用该模块使用 Python 渲染 JavaScript。
本质上,一旦你正确安装了模块,上面的链接中显示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 视频,其中演示了该模块的工作原理。
解决方案 5:
听起来您真正寻找的数据可以通过主页上某些 JavaScript 调用的辅助 URL 来访问。
虽然您可以尝试在服务器上运行 JavaScript 来处理此问题,但更简单的方法可能是使用 Firefox 加载页面,并使用Charles或Firebug等工具来准确识别辅助 URL。然后,您可以直接查询该 URL 以获取您感兴趣的数据。
解决方案 6:
这似乎也是一个很好的解决方案,取自一篇很棒的博客文章
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
解决方案 7:
Selenium 最适合抓取 JS 和 Ajax 内容。
查看本文了解如何使用 Python 从网络提取数据
$ pip install selenium
然后下载 Chrome webdriver。
from selenium import webdriver
browser = webdriver.Chrome()
browser.get("https://www.python.org/")
nav = browser.find_element_by_id("mainnav")
print(nav.text)
很简单,对吧?
解决方案 8:
您还可以使用 webdriver 执行 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 text')
解决方案 9:
我个人更喜欢使用 scrapy 和 selenium,并将两者放在不同的容器中。这样,您可以轻松安装两者,并抓取几乎所有以某种形式包含 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,}
接下来进行 docker 化。从轻量级镜像创建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
。如果您是第一次执行此操作,则需要一段时间才能获取最新的 selenium/standalone-chrome 并构建您的抓取工具映像。
完成后,您可以检查您的容器是否正在运行docker ps
,还可以检查 selenium 容器的名称是否与我们传递给 scraper 容器的环境变量的名称相匹配(这里是SELENIUM_LOCATION=samplecrawler_selenium_1
)。
使用 进入你的 scraper 容器docker exec -ti YOUR_CONTAINER_NAME sh
,对我来说命令是docker exec -ti samplecrawler_my_scraper_1 sh
,cd 进入正确的目录并使用 运行你的 scraper scrapy crawl my_spider
。
整个内容都在我的 github 页面上,你可以从这里获取
解决方案 10:
对于我来说,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您可以在这里找到更多等待条件
解决方案 11:
使用 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)
解决方案 12:
剧作家-Python
还有一个选择是playwright-python
,将微软的 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()
解决方案 13:
尝试直接访问 API
在抓取数据时,您会看到一种常见的情况,即网页从 API 端点异步请求数据。以下网站就是一个最简单的示例:
<body>
<script>
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => {
if (!res.ok) throw Error(res.status);
return res.json();
})
.then(data => {
// inject data dynamically via JS after page load
document.body.innerText = data.title;
})
.catch(err => console.error(err))
;
</script>
</body>
运行代码片段Hide results展开片段
一般程序是使用浏览器的开发人员工具的网络选项卡搜索页面发出的请求,以查找要抓取的数据的关键字/子字符串。通常,您会看到一个不受保护的 API 请求端点,其中包含 JSON 有效负载,您可以使用urllib
或requests
模块直接访问它。上面的可运行代码片段就是这种情况,您可以使用它来练习。单击“运行代码片段”后,以下是我在网络选项卡中找到端点的方式:
此示例是人为设计的;从静态标记来看,端点 URL 可能不明显,因为它可能被动态组装、缩小并隐藏在数十个其他请求和端点之下。网络请求还将显示您可能需要的任何相关请求负载详细信息,例如访问令牌。
获取端点 URL 和相关详细信息后,使用标准 HTTP 库在 Python 中构建请求并请求数据:
>>> import requests
>>> res = requests.get("https://jsonplaceholder.typicode.com/posts/1")
>>> data = res.json()
>>> data["title"]
'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'
当你可以摆脱它时,这往往比使用 Selenium、Playwright-Python、Scrapy 或你阅读这篇文章时的任何流行的抓取库来抓取页面更容易、更快、更可靠。
在许多情况下,即使端点是安全的,将令牌或 cookie 从浏览器请求复制到您的 Python 请求也会让您获得访问权限。
如果您运气不好,数据没有通过以良好格式返回数据的 API 请求到达,则它可能是标签中原始浏览器有效负载的一部分<script>
,可以是 JSON 字符串或(更可能)JS 对象。例如:
<body>
<script>
var someHardcodedData = {
userId: 1,
id: 1,
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
body: 'quia et suscipit
suscipit recusandae con sequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto'
};
document.body.textContent = someHardcodedData.title;
</script>
</body>
运行代码片段Hide results展开片段
没有一种万能的方法可以获取这些数据。基本方法是使用 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
suscipit recusandae con sequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum 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))
查看以下用于解析不太有效的 JSON 的 JS 对象的资源:
如何将原始 javascript 对象转换为 python 字典?
如何修复没有双引号的 JSON 键值?
以下是一些使用 API 绕过抓取的额外案例研究/概念验证:
如何使用 Python beautifulsoup 将 yelp 评论和星级评定抓取到 CSV 中
Beautiful Soup 对现有元素返回 None
从 BeautifulSoup Python 中提取数据
通过 POST 抓取 Bandcamp 粉丝收藏(使用混合方法,首先向网站发出初始请求,使用 BeautifulSoup 从标记中提取令牌,然后将其用于对 JSON 端点的第二次请求)
使用 puppeteer 抓取 nba.com 页面(使用 JS,但该技术也适用于 Python)
如果其他方法都失败了,请尝试此线程中列出的众多动态抓取库之一。
解决方案 14:
截至 2022 年底,Pyppeteer不再维护;请考虑使用playwright-python作为替代方案。
Pyppeteer
您可能会考虑Pyppeteer,这是 Chrome/Chromium 驱动程序前端Puppeteer的 Python 端口。
这是一个简单的例子,展示如何使用 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.run(main())
查看Pyppeteer 的参考文档。
解决方案 15:
您将需要在脚本中针对页面的不同部分使用 urllib、requests、beautifulSoup 和 selenium web driver(仅举几例)。
有时,仅使用其中一个模块即可获得所需的内容。
有时,您需要两个、三个或所有模块。
有时,您需要关闭浏览器上的 js。
有时,您需要脚本中的标头信息。
没有网站可以以相同的方式进行抓取,也没有网站可以永远以相同的方式进行抓取,而无需修改爬虫程序,通常是几个月后。但它们都可以被抓取!有志者事竟成。如果
您将来需要持续抓取数据,只需抓取所需的所有内容并将其存储在带有 pickle 的 .dat 文件中。
只需继续搜索如何使用这些模块,然后将错误复制并粘贴到 Google 中。
解决方案 16:
我最近使用 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 对象,您可以在浏览器 (Chrome) 中的调试器控制台中输入“window”,然后按 ENTER,以调出浏览器呈现的对象的完整列表。如果您对数据存储位置进行一些有根据的猜测,您可能会在那里找到它。我的图形数据在控制台中的 window.view.data 下,因此在传递给上面引用的 .render() 方法的“script”变量中,我使用了:
return {
data: window.view.data
}
解决方案 17:
如上所述,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']
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件