首页 > 解决方案 > PyQt5 QWebEnginePage - 可以编辑 HTML 以打开下拉列表吗?

问题描述

我正在尝试开发一个可持续的网络抓取脚本,以从网站获取所有产品的列表。产品类别链接位于网页上的下拉(或可展开)元素中。在提取 html 并使用 Beautiful Soup 将其转换为文本之前,我使用 PyQt5 模拟客户端。

例如,如果您在浏览器上访问该网站,则必须单击页面左上角附近的按钮才能打开从屏幕左侧弹出的类别列表(我将参考这个作为“侧边栏”)。在这些类别中的每一个中,当单击时,会有一个更具体的类别列表,每个类别都有一个我试图用我的代码获取的链接(我将这些称为“子类别”)。

即使侧栏被隐藏,初始类别列表元素也会出现在我的 Beautiful Soup 中,但子类别元素保持隐藏,除非子类别标题展开(因此,它们不会显示在我的汤中)。我已经通过手动检查 Chrome 浏览器中的元素来确认这一点。以下是网页 HTML 的片段,附有我自己的评论以帮助解释:

<div aria-label="Fruits &amp; Vegetables" data-automation-id="taxonomy-toggle-Fruits &amp; Vegetables">
  <button aria-disabled="false" aria-expanded="false" class="NavSection__sectionBtn___1_cAs" data- 
   automation-id="nav-section-toggle" tabindex="-1"> #Initial category that contains sub-categories
  </button>
  <div>
  </div> #Contains the links I need, but doesn't populate HTML text unless sub-category element is expanded
</div>

以下是子类别元素已展开时的外观:

<div aria-label="Fruits &amp; Vegetables" data-automation-id="taxonomy-toggle-Fruits &amp; Vegetables">
      <button aria-disabled="true" aria-expanded="true" class="NavSection__sectionBtn___1_cAs" data- 
       automation-id="nav-section-toggle" tabindex="-1"> #Initial category that contains sub-categories
      </button>
      <div>
         <ul class>
           <li class = "NavSection__sectionLink__rbr40> </li>
           <li class = "NavSection__sectionLink__rbr40> </li> #can open each li element up to acquire href link
           <li class = "NavSection__sectionLink__rbr40> </li>
         </ul>
      </div>
</div>

这是我的代码:

import bs4 as bs
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEnginePage

#act as a client via Qt5 to acquire javascript elements from webpage
class Page(QWebEnginePage):

    def __init__(self, url):
        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, html_str):   
        self.html = html_str
        self.app.quit()

page = Page("https://grocery.walmart.com")
soup = bs.BeautifulSoup(page.html, 'lxml')
print(soup.prettify())

我知道如果元素的aria-expandedaria-disabled属性<button>从“False”更改为“True”,子类别<li>元素将出现在 HTML 中。我通过 Chrome 浏览器中的手动检查确认了这一点。

我的问题是是否有可能href<li>元素中获取?我的假设是我必须编辑 HTML 以aria在初始解析后将属性从“False”更改为“True”,然后使用这些更改重新解析 HTML。如果没有,除了 Selenium 之外,还有其他方法可以从网页中获取这些元素吗?我正在尝试使用更精简的方法(不打开浏览器窗口等)。

我可以提供实际的网站 URL 和网页截图来帮助澄清,不确定这是否被认为是良好的做法或在 Stack Overflow 上是否允许(我是新来的!)。

有关我尝试使用的方法的更多背景信息,请参阅以下内容:

Sentdex 的 PyQt4 动态抓取视频

PyQt4 到 PyQt5 库的变化

标签: pythonhtmlweb-scrapingbeautifulsouppyqt5

解决方案


如果您从页面下载 HTML,您将看到几乎整个页面都是使用 javascript 创建的,因此 Beautiful Soup 不是正确的工具,因为它仅用于分析 HTML。在这种情况下,解决方案是通过 javascript 使用以下runJavaScript()方法实现逻辑QWebEnginePage

from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets


class WalmartGroceryPage(QtWebEngineWidgets.QWebEnginePage):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._results = None
        self.loadFinished.connect(self._on_load_finished)
        self.setUrl(QtCore.QUrl("https://grocery.walmart.com"))

    @QtCore.pyqtSlot(bool)
    def _on_load_finished(self, ok):
        if ok:
            self.runJavaScript(
                """
                function scraper_script(){
                    var results = []
                    self.document.getElementById("mobileNavigationBtn").click();
                    var elements = document.getElementsByClassName("NavSection__sectionBtn___1_cAs");
                    for (const element of elements) {
                        element.click();
                        var items = [];
                        var sub_elements = document.getElementsByClassName("MobileNavigation__navLink___2-m6_");
                        for (const e of sub_elements) {
                            var d = {"name": e.innerText, "url": e.href};
                            items.push(d);
                        }
                        var data = {"name": element.innerText, "items": items};
                        results.push(data);
                    }
                    return results;
                }
                scraper_script();
                """,
                self.results_callback,
            )

    def results_callback(self, value):
        self._results = value
        QtCore.QCoreApplication.quit()

    @property
    def results(self):
        return self._results


if __name__ == "__main__":
    import sys
    import json

    # sys.argv.append("--remote-debugging-port=8000")
    app = QtWidgets.QApplication(sys.argv)

    page = WalmartGroceryPage()
    ret = app.exec_()
    results = page.results

    print(json.dumps(results, indent=4))

输出:

[
    {
        "items": [
            {
                "name": "Fall Flavors Shop",
                "url": "https://grocery.walmart.com/cp/Flavors%20of%20Fall/9576778812"
            },
            {
                "name": "Baking Center",
                "url": "https://grocery.walmart.com/browse?shelfId=3433056320"
            },
            {
                "name": "Peak Season Produce",
                "url": "https://grocery.walmart.com/browse?shelfId=4881154845"
            },
# ...

推荐阅读