PyQt5和Scrapy整合解决方案

PyQt5和Scrapy整合解决方案

实现爬虫功能

完成界面布局

整合PyQt5和Scrapy代码

打包


整体思路:在PyQt5窗口类中开一个进程运行Scrapy代码,并使用Queue进行通信。

实现爬虫功能

首先是Scrapy,笔者使用以下命令创建项目:

scrapy startproject books
cd books
scrapy genspider book books.toscrape.com

该项目对books.toscrape.com网站上的书籍信息作了简单的爬取,以下是爬虫文件book.py的内容:

# -*- coding: utf-8 -*-
import scrapy
from books.items import BooksItem


class BookSpider(scrapy.Spider):
    name = 'book'
    allowed_domains = ['books.toscrape.com']
    # start_urls = ['http://books.toscrape.com/']

    def start_requests(self):
        print('开始爬取')
        for page_num in range(1, 51):
            url = 'http://books.toscrape.com/catalogue/page-%d.html' % page_num
            yield scrapy.Request(url)

    def parse(self, response):
        for book in response.xpath('//article[@class="product_pod"]'):
            # 初始化Item
            items = BooksItem()

            # 书本标题
            items['title'] = book.xpath('./h3/a/@title').extract_first()

            # 书本价格
            items['price'] = book.xpath('./div/p[@class="price_color"]/text()').extract_first()

            # 书本评级
            review = book.xpath('./p[1]/@class').extract_first()
            items['review'] = review.split(' ')[-1]

            print(f"{items['title']}\n{items['price']}\n{items['review']}\n")
            yield items

    def close(spider, reason):
        print('爬取结束')

以下是items.py内容:

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class BooksItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    price = scrapy.Field()
    review = scrapy.Field()

为使讲解尽可能简洁,其余文件保持不变。

完成界面布局

接着我们在books项目文件夹中新建一个gui.py,并用PyQt5来编写一个GUI界面:

代码如下:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, \
                            QTextBrowser, QComboBox, QHBoxLayout, QVBoxLayout


class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.setWindowTitle('PyQt5与Scrapy')

        self.ua_line = QLineEdit(self)                   # 输入USER_AGENT
        self.obey_combo = QComboBox(self)                # 选择ROBOTSTXT_OBEY
        self.obey_combo.addItems(['是', '否'])
        self.log_browser = QTextBrowser(self)            # 日志输出框
        self.crawl_btn = QPushButton('开始爬取', self)    # 开始爬取按钮

        # 布局
        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()
        self.h_layout.addWidget(QLabel('输入User-Agent'))
        self.h_layout.addWidget(self.ua_line)
        self.h_layout.addWidget(QLabel('是否遵循Robot协议'))
        self.h_layout.addWidget(self.obey_combo)
        self.v_layout.addLayout(self.h_layout)
        self.v_layout.addWidget(QLabel('日志输出框'))
        self.v_layout.addWidget(self.log_browser)
        self.v_layout.addWidget(self.crawl_btn)
        self.setLayout(self.v_layout)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

界面显示如下:

用户可以输入User-Agent以及选择是否遵循爬虫协议,这些都会应用到BookSpider上。而日志输出框则会显示BookSpider中打印的内容。

整合PyQt5和Scrapy代码

我们先完成“开始爬取”按钮的功能,当用户按钮该按钮后,爬虫启动。首先把信号和槽进行连接:

...
self.crawl_btn = QPushButton('开始爬取', self)    # 开始爬取按钮
self.crawl_btn.clicked.connect(self.crawl_slot)
...

槽函数crawl_slot实现如下:

def crawl_slot(self):
    if self.crawl_btn.text() == '开始爬取':
        self.log_browser.clear()
        self.crawl_btn.setText('停止爬取')
        ua = self.ua_line.text().strip()
        is_obey = True if self.obey_combo.currentText() == '是' else False
        self.p = Process(target=..., args=(...))
        self.p.start()
    else:
        self.crawl_btn.setText('开始爬取')
        self.p.terminate()

在槽函数中我们通过判断按钮文本来启动或停止进程。如果按钮文本为“开始爬取”,则清空日志输出框、改变按钮文本、获取用户输入的User-Agent以及选择项,接着创建一个进程并启动。如果文本为“停止爬取”,则直接终止进程。注意这里要在文件开头导入相应的类:

from multiprocessing import Process

现在我们要考虑的是进程实例化时的target参数和args参数。我们知道启动Scrapy爬虫的方式有两种:一种是通过命令行,另一种是脚本。笔者建议使用后者。

而在脚本启动方面,Scrapy提供了两种方式,一种是使用CrawlerProcess,另一种是CrawlerRunner。可以查看Scrapy文档

# CrawlerProcess
import scrapy
from scrapy.crawler import CrawlerProcess

class MySpider(scrapy.Spider):
    # Your spider definition
    ...

process = CrawlerProcess(settings={
    'FEED_FORMAT': 'json',
    'FEED_URI': 'items.json'
})

process.crawl(MySpider)
process.start() # the script will block here until the crawling is finished
# CrawlerRunner
from twisted.internet import reactor
import scrapy
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging

class MySpider(scrapy.Spider):
    # Your spider definition
    ...

# configure_logging可有可无,读者可自己运行代码了解该函数用处。
# 在CrawlerProcess中,该函数默认使用。
configure_logging({'LOG_FORMAT': '%(levelname)s: %(message)s'})    
runner = CrawlerRunner()

d = runner.crawl(MySpider)
d.addBoth(lambda _: reactor.stop())
reactor.run() # the script will block here until the crawling is finished

经笔者测试,两种方法都能用于整合。

我们接下来要做的就是在gui.py中写一个crawl函数(注意不能作为任何PyQt5相关类的成员函数,否则运行闪退):

def crawl(Q, ua, is_obey):
    # CrawlerProcess
    process = CrawlerProcess(settings={
        'USER_AGENT': ua,
        'ROBOTSTXT_OBEY': is_obey
    })

    process.crawl(BookSpider, Q=Q)
    process.start()
    
    # CrawlerRunner
    """runner = CrawlerRunner(settings={
        'USER_AGENT': ua,
        'ROBOTSTXT_OBEY': is_obey
    })

    d = runner.crawl(BookSpider, Q=Q)
    d.addBoth(lambda _: reactor.stop())
    reactor.run()"""

在crawl函数中,笔者选择CrawlerProcess来启动Scrapy代码。我们看到该函数一共有三个参数:Q,ua和is_obey。

ua和is_obey这里就不讲了。Q用于进程间通信,我们会通过runner.crawl函数将其传给BookSpider,此时需要对BookSpider类作两处修改:

1. 增加一个类变量Q:

class BookSpider(scrapy.Spider):
    name = 'book'
    allowed_domains = ['books.toscrape.com']
    # start_urls = ['http://books.toscrape.com/']
    Q = None

    ...

2. 将print改为Q.put():

self.Q.put('开始爬取')
self.Q.put(f"{items['title']}\n{items['price']}\n{items['review']}\n")
spider.Q.put('爬取结束')

现在我们来完善Demo窗口类。首先实例化一个Q成员变量:

self.Q = Manager().Queue()

记得导入Manager类:

from multiprocessing import Process, Manager

然后完成进程实例化代码:

self.p = Process(target=crawl, args=(self.Q, ua, is_obey))

此时我们运行代码,点击按钮,发现BookSpider正常运行:

但是发现一个问题,日志输出框中并没有显示Q队列中的消息。此时想到我们应该用一个进程或者线程来持续读取Q队列中的消息并将其显示到日志输出框上。笔者这里创建了一个LogThread线程类:

class LogThread(QThread):
    def __init__(self, gui):
        super(LogThread, self).__init__()
        self.gui = gui

    def run(self):
        while True:
            if not self.gui.Q.empty():
                self.gui.log_browser.append(self.gui.Q.get())

                # 确保滑动条到底
                cursor = self.gui.log_browser.textCursor()
                pos = len(self.gui.log_browser.toPlainText())
                cursor.setPosition(pos)
                self.gui.log_browser.setTextCursor(cursor)

                if '爬取结束' in self.gui.log_browser.toPlainText():
                    self.gui.crawl_btn.setText('开始爬取')
                    break

                # 睡眠10毫秒,否则太快会导致闪退或者显示乱码
                self.msleep(10)

可以看到在run函数中有一个while循环,在循环中我们首先判断队列是否为空,如果不是,则取出队列中的一条消息并显示到日志框上。最后如果日志框上出现“爬取结束”字样,则修改按钮文本并退出循环。注意这里一定要进行睡眠,否则QTextBrowser无法很好的显示队列内容,而且常常会一下子显示一大段队列消息从而导致闪退。

最后我们再在Demo窗口类中实例化好线程类,并修改下按钮槽函数即可:

    ...
    self.log_thread = LogThread(self)

def crawl_slot(self):
    if self.crawl_btn.text() == '开始爬取':
        ...
        self.log_thread.start()
    else:
        ...
        self.log_thread.terminate()

现在运行发现日志输出框有内容了:

gui.py全部代码如下:

import sys
from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, \
                            QTextBrowser, QComboBox, QHBoxLayout, QVBoxLayout

from books.spiders.book import BookSpider
from twisted.internet import reactor
from scrapy.crawler import CrawlerRunner
from multiprocessing import Process, Manager
from scrapy.crawler import CrawlerProcess


def crawl(Q, ua, is_obey):
    # CrawlerProcess
    process = CrawlerProcess(settings={
        'USER_AGENT': ua,
        'ROBOTSTXT_OBEY': is_obey
    })

    process.crawl(BookSpider, Q=Q)
    process.start()

    # CrawlerRunner
    """runner = CrawlerRunner(settings={
        'USER_AGENT': ua,
        'ROBOTSTXT_OBEY': is_obey
    })

    d = runner.crawl(BookSpider, Q=Q)
    d.addBoth(lambda _: reactor.stop())
    reactor.run()"""


class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.setWindowTitle('PyQt5与Scrapy')

        self.ua_line = QLineEdit(self)                   # 输入USER_AGENT
        self.obey_combo = QComboBox(self)                # 选择ROBOTSTXT_OBEY
        self.obey_combo.addItems(['是', '否'])
        self.log_browser = QTextBrowser(self)            # 日志输出框
        self.crawl_btn = QPushButton('开始爬取', self)    # 开始爬取按钮
        self.crawl_btn.clicked.connect(self.crawl_slot)

        # 布局
        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()
        self.h_layout.addWidget(QLabel('输入User-Agent'))
        self.h_layout.addWidget(self.ua_line)
        self.h_layout.addWidget(QLabel('是否遵循Robot协议'))
        self.h_layout.addWidget(self.obey_combo)
        self.v_layout.addLayout(self.h_layout)
        self.v_layout.addWidget(QLabel('日志输出框'))
        self.v_layout.addWidget(self.log_browser)
        self.v_layout.addWidget(self.crawl_btn)
        self.setLayout(self.v_layout)

        self.Q = Manager().Queue()
        self.log_thread = LogThread(self)

    def crawl_slot(self):
        if self.crawl_btn.text() == '开始爬取':
            self.log_browser.clear()
            self.crawl_btn.setText('停止爬取')
            ua = self.ua_line.text().strip()
            is_obey = True if self.obey_combo.currentText() == '是' else False
            self.p = Process(target=crawl, args=(self.Q, ua, is_obey))
            self.p.start()
            self.log_thread.start()
        else:
            self.crawl_btn.setText('开始爬取')
            self.p.terminate()
            self.log_thread.terminate()

    def closeEvent(self, event):
        self.p.terminate()
        self.log_thread.terminate()


class LogThread(QThread):
    def __init__(self, gui):
        super(LogThread, self).__init__()
        self.gui = gui

    def run(self):
        while True:
            if not self.gui.Q.empty():
                self.gui.log_browser.append(self.gui.Q.get())

                # 确保滑动条到底
                cursor = self.gui.log_browser.textCursor()
                pos = len(self.gui.log_browser.toPlainText())
                cursor.setPosition(pos)
                self.gui.log_browser.setTextCursor(cursor)

                if '爬取结束' in self.gui.log_browser.toPlainText():
                    self.gui.crawl_btn.setText('开始爬取')
                    break

                # 睡眠10毫秒,否则太快会导致闪退或者显示乱码
                self.msleep(10)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

项目文件下载地址:

链接:https://pan.baidu.com/s/17DWWyXQiJ7chRE4R40x1Hg 
提取码:leay 

打包

请看笔者的《PyInstaller打包实战指南》:https://blog.csdn.net/La_vie_est_belle/article/details/96321995

欢迎关注我的微信公众号,发现更多有趣内容:

发布了83 篇原创文章 · 获赞 157 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/La_vie_est_belle/article/details/102539029
今日推荐