如何在Python中爬取包含动态内容(由JavaScript创建)的页面?

278
我正在尝试开发一个简单的网络爬虫。我想提取没有HTML标记的纯文本。我的代码可以处理纯静态的HTML,但是当内容是由嵌入在页面中的JavaScript生成时,就无法正常工作。
特别是,当我使用urllib2.urlopen(request)来读取页面内容时,它不会显示由JavaScript代码添加的任何内容,因为该代码在任何地方都没有被执行。通常它会被网络浏览器运行,但这不是我的程序的一部分。
我该如何在我的Python代码中访问这个动态内容呢?
另请参阅Can scrapy be used to scrape dynamic content from websites that are using AJAX?以获取与Scrapy相关的具体答案。

3
听起来你可能需要更重的工具,试试 Selenium 或 Watir。 - wim
3
我已经在Java中成功完成了这个(我使用了Cobra工具包http://lobobrowser.org/cobra.jsp)。由于你想用Python来进行编程(这总是一个不错的选择),我建议以下两个选项:- http://www.packtpub.com/article/web-scraping-with-python-part-2 - http://blog.databigbang.com/web-scraping-ajax-and-javascript-sites/ - bpgergo
16
请注意,最佳答案 的最后更新时间为2017年,截至2021年,PhantomJS和dryscrape已经被弃用,因此已经过时。在尝试该建议中的任何技术之前,我建议您先阅读整个帖子。 - ggorlen
18个回答

7

使用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)

1
+1,谢谢!这是对我有效的解决方案,因为Selenium对于如此简单的任务有点过度,而requests-html仅适用于Python 3.6。我会推荐这个解决方案胜过其他任何解决方案。 - WhiteWood
1
以上代码对我有效,但仅在单独安装了QtWebEngineWidgets后才有效。按照以下顺序安装:pip install PyQt5,然后再安装pip install QtWebEngineWidgets - NeuroMorphing
这个网站上能否执行JS? - MaxFrost
是的,runJavaScript函数应该在页面加载后运行。 - Ash Ishh

3

Playwright-Python

还有一个选择是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()

嘿,谢谢你的报告。希望你很高兴听到我要删除我的账户了(只是在等待删除批准),我的编辑不会再打扰你或其他人了,你可以保留你们完美的历史记录,包括各种库的所有旧版本等等。不过,你可以随意查看这篇博客文章:https://www.joelonsoftware.com/2008/12/28/stack-overflow-is-a-wiki/ - user3064538
@user14 我并不反对编辑,也不希望你删除你的账户。我欣赏你的理念,我也不反对像Joel建议的那样通过编辑来改进答案,就像维基百科一样。这个帖子肯定存在问题——旧的、长期废弃的库占据了主导地位,而Playwright,可能是可预见未来最好的答案,需要很长时间才能崭露头角。但是我们现在有了趋势排序的答案,取消置顶的原始帖子选择,还有评论、投票和编辑免责声明和警示的能力。彻底改变可能对某些人有价值的旧帖子就太过分了。 - undefined

3
您的脚本中需要使用urllib、requests、beautifulSoup和selenium web driver等模块来处理页面的不同部分(仅列举部分)。有时您只需使用其中一个模块即可获取所需内容。有时您需要两个、三个或所有这些模块。有时您需要在浏览器上关闭js。有时您需要在脚本中添加头信息。没有网站能够以相同的方式进行爬取,也没有网站可以在没有修改爬虫的情况下永久以同样的方式进行爬取,通常几个月后就需要进行修改。但是它们都可以被抓取!有志者事竟成。如果您未来需要连续获取抓取数据,请将所有需要的内容抓取并存储在.dat文件中,使用pickle进行管理。请不断搜索如何使用这些模块,并将错误复制粘贴到Google中以寻求帮助。

3

尝试直接访问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>

在许多情况下,API将受到CORS或访问令牌的保护,或者受到限制性速率限制,但在其他情况下,它是公开可访问的,您可以完全绕过网站。对于CORS问题,您可以尝试使用cors-anywhere

一般的步骤是使用浏览器的开发人员工具网络选项卡搜索页面发出的请求,以获取您想要抓取的数据的关键字/子字符串。通常,您会看到一个没有保护的API请求端点,其中包含一个JSON有效负载,您可以直接使用urllibrequests模块访问。这就是上面可运行片段的情况,您可以用它来练习。点击“运行片段”后,这是我在网络选项卡中找到端点的方法:

example network tab showing remote URL endpoint found with a search

这个例子是人为构造的;从静态标记中看,端点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或任何流行的网络爬虫库更容易、更快速、更可靠,以便获取页面数据。如果你不幸的话,数据没有通过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\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>

获得这些数据没有一种万能的方法。基本技术是使用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))

查看以下资源,以解析不完全符合JSON格式的JS对象:

这里有一些额外的案例研究/概念证明,展示了如何使用API绕过网络爬虫的限制:

如果所有其他方法都失败了,请尝试此主题中列出的许多动态爬取库之一。


现代页面有大量的异步请求,管理起来非常困难。只有在较小的页面上,并且你知道要查找什么时才有效。 - anishtain4
@anishtain4 如果你使用开发工具中的搜索工具来过滤特定数据,那么请不要担心请求的数量,就像这篇文章中所展示的一样。我已经成功地在许多现代网页上使用了这种技术,其中一些在案例研究链接中展示。试试吧——这是一个被大量忽视的技巧,可以节省大量的爬虫代码编写,尤其是当API没有保护时。即使你正在使用动态爬虫,通常你也想绕过不稳定的DOM,而是使用请求/响应,因为你有正确的凭据和来源。 - ggorlen
这是一个有趣的技巧,我会记在心里。不幸的是,我试图爬取的网站一直让我无法进入。 - anishtain4
是的,它并不是一个通用解决方案,只是一种在工作时很好用且易于检查的选项,而且在确定如何获取所需数据时也很容易。页面上的 JS 通常从 <script> 块或 API 中提取数据,因此要检查的第一件事是是否可以从页面 JS 使用的相同来源获取该数据。 - ggorlen

3

截至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的参考文档


0
如前所述,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']

0
我最近使用了 requests_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对象。这是从他们的文档中复制的,但这可能正是您所需要的:
如果指定了script,它将在运行时执行提供的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
}

1
似乎 requests_html 不再得到积极维护(最后更新于2020年5月)。它使用 pyppeteer 进行渲染,而 pyppeteer 似乎正在积极维护;它在底层使用 Chromium 进行渲染。 - VirtualScooter

-4

简单快速的解决方案:

我也遇到了同样的问题。我想要爬取一些由JavaScript构建的数据。如果我仅使用BeautifulSoup从此网站爬取文本,那么在文本中会包含<script>标签。

我想要渲染这个<script>标签,并从中获取信息。

另外,我不想使用Scrapy和selenium这样的重型框架。

因此,我发现requests模块的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

这将渲染加载站点并呈现标签。

希望这能作为快速简便的解决方案,用于呈现包含脚本标签的站点。


请在您的答案中包含提供脚本标记渲染的示例网站,可以吗? - VirtualScooter
1
这显然无法解决 OP 所提出的任何问题。 - tym
1
查看<script>标签的文本和实际执行其中的JS之间存在差异。这只是前者,而不是后者。 - ggorlen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接