part two
接上一部分:
这部分主要讲PyQt5页面部分。主界面的功能就是收集用户输入的关键信息(搜索的关键字、百度AK、选择省份、excel数据本地存储路径、开启爬虫按钮,为了说清楚,首先一样化繁为简,化整为零分解。分为三部分:
① PyQt5界面布局
② 脚本开启scrapy 爬虫
③ 多线程开启通过队列获取scrapy爬取数据,在主界面日志窗口显示。
session one pyqt5 页面布局
pyqt5就不多说了,就是python一个画界面的包,商业化使用要收费。也没想着要干啥,就直接拿来用画界面吧。
2、一本书,pyqt5快速开发与实战,绿色的标题,上面一条绿色的变色龙(也有可能是蜥蜴),有兴趣可以看下,不是必要的,其实好多资料网上也是可以查到的。
②上图
④代码:
其实pyqt5画图的话,如果有前端经验的画,很好上手。布局,伸缩,自适应和浏览器一样。上代码
# -*- coding:utf-8 -*-
class SpiderForTanYangShi(QWidget):
containData_signal = pyqtSignal(list)
testsignal = pyqtSignal()
def __init__(self):
super().__init__()
self.initUI()
self.saveData = []
self.searchWord = ''
self.transeMessage = {}
self.Q = Manager().Queue(maxsize=0) # 用于取scrapy中日志的队列,用于主界面和scrapy通信
self.monitorQueue = Manager().Queue() # 用于开启数据监控中心的1.0版不需要
self.log_thread = LogThread(self.Q,self) # 读取日志的线程,用于取self.Q的队列中取日志
self.log_thread.sinOut.connect(self.getLogContent) # pyqt5信号,用于子线程和主页面通信
# 画页面组件
def initUI(self):
self.kindlyTest = QLabel(self)
self.kindlyTest.setText("友情测试:nidhogg,学好数理化")
self.kindlyTest.setFont(QFont("MicroSoft YaHei", 10, QFont.Normal))
self.lbl2 = QLabel(self)
self.lbl2.setText("请输入搜索关键词:")
self.lbl2.setFont(QFont("MicroSoft YaHei", 10, QFont.Normal))
self.qle = QLineEdit(self)
self.qle.setFixedWidth(252)
self.qle.textChanged.connect(self.setSearchWord)
self.lbl3 = QLabel(self)
self.lbl3.setText("请输入百度地图AK:")
self.lbl3.setFont(QFont("MicroSoft YaHei", 10, QFont.Normal))
self.baiduAk = QLineEdit(self)
self.baiduAk.setFixedWidth(157)
self.baiduAk.textChanged.connect(self.setBaiduAK)
self.defaultAk = QPushButton('使用默认ak', self)
self.defaultAk.setCheckable(True)
self.defaultAk.clicked[bool].connect(self.useDefaultAK)
self.redb = QPushButton('选择区域', self)
self.redb.setCheckable(True)
self.redb.clicked[bool].connect(self.chooseProvince)
self.lbl99 = QTextEdit(self)
self.lbl99.setReadOnly(True)
self.lbl99.setText("请选择省份:")
self.lbl99.setFont(QFont("MicroSoft YaHei", 10, QFont.Normal))
self.lbl99.setMaximumWidth(530)
self.lbl99.setMaximumHeight(100)
self.pathButton = QPushButton('选择存取路径', self)
self.pathButton.setCheckable(True)
self.pathButton.clicked.connect(self.selectPath)
self.filePath = QLabel(self)
self.filePath.setText(" ")
self.filePath.setFont(QFont("MicroSoft YaHei", 10, QFont.Normal))
self.startButton = QPushButton('开始爬取数据', self)
self.startButton.setCheckable(True)
self.startButton.clicked.connect(self.startSpider)
self.monitorStatus = QPushButton('数据监控中心') #1.0版先禁用
self.monitorStatus.setCheckable(True)
self.monitorStatus.clicked[bool].connect(self.openMonitorCenter)
self.monitorStatus.setDisabled(True)
self.logContent = QTextBrowser(self)
self.logContent.setFixedWidth(350)
self.logContent.setFixedHeight(280)
self.logContent.append('<div><div>使用说明:</div><p>1、本软件仅供技术交流,请勿用于商业及非法用途,本数据全部源于百度地图开放平台,数据所有权归百度所有。如产生法律纠纷与本人无关。</p><p>2、防止演变攻击被封,本软件已设置低速爬取</p><p>2、使用方法:<br /> 1)输入搜索的关键字<br> 2)输入百度地图开放平台ak,可自行申请<br> 3)选择要爬取的省份<br> 4)选择生成data.xlsx的excel保存路径<br> 5)点击开始爬取数据按钮开启爬虫<br> 6)观察本提示框会变持续输出日志,出现爬取%结束字样则爬取%数据结束<br> 7)到设置路径下找到data.xlsx为爬取的数据<br></p></div>')
leftBox = QVBoxLayout()
leftBox.setContentsMargins(50,40,0,0)
searchBox = QHBoxLayout()
searchBox.setContentsMargins(0,0,0,20)
searchBox.addWidget(self.lbl2)
searchBox.addSpacing(5)
searchBox.addWidget(self.qle)
searchBox.addStretch(1)
leftBox.addLayout(searchBox)
baiduAkLayout = QHBoxLayout()
baiduAkLayout.setContentsMargins(0,0,0,40)
baiduAkLayout.addWidget(self.lbl3)
baiduAkLayout.addSpacing(1)
baiduAkLayout.addWidget(self.baiduAk)
baiduAkLayout.addSpacing(15)
baiduAkLayout.addWidget(self.defaultAk)
baiduAkLayout.addStretch(1)
leftBox.addLayout(baiduAkLayout)
selectCity = QHBoxLayout()
selectCity.setContentsMargins(0,0,0,20)
selectCity.addWidget(self.redb,0,Qt.AlignTop)
selectCity.addSpacing(35)
selectCity.addWidget(self.lbl99)
selectCity.addStretch(1)
leftBox.addLayout(selectCity)
selectPath = QHBoxLayout()
selectPath.setContentsMargins(0,0,0,30)
selectPath.addWidget(self.pathButton)
selectPath.addSpacing(31)
selectPath.addWidget(self.filePath)
selectPath.addStretch(1)
leftBox.addLayout(selectPath)
startButtonLayout = QHBoxLayout()
startButtonLayout.addWidget(self.startButton)
startButtonLayout.addSpacing(30)
startButtonLayout.addWidget(self.monitorStatus)
startButtonLayout.addStretch(1)
leftBox.addLayout(startButtonLayout)
leftBox.addStretch(1)
rightBox = QVBoxLayout()
rightBox.setContentsMargins(0, 40, 50, 0)
rightBox.addWidget(self.kindlyTest)
rightBox.addSpacing(22)
rightBox.addWidget(self.logContent)
rightBox.addStretch(1)
globalWidget = QWidget(self)
globalLayout = QHBoxLayout(globalWidget) # 全局布局控件
globalLayout.addLayout(leftBox)
globalLayout.addLayout(rightBox)
self.resize(900, 450)
self.center()
self.setWindowTitle('爬虫For谭杨士1.0版 design by jing')
self.setLayout(globalLayout)
self.setWindowIcon(QIcon('F:\\visionSpider\\vision\\vision\\spiders\\a.png'))
self.show()
说明:画图主要是initUI中,进行画图,都是很基础的东西,如果你不懂,建议好好查查上面链接中的资料。
需要讲到的点:布局分绝对布局和布局类(水平、垂直、网格、表单)两种布局方式
A 、绝对布局和浏览器中的绝对定位很像,坐标写死的在页面中永远是那个位置。拉伸页面会导致页面变形。
B、 布局类布局有点像浏览器中的盒模型和flex伸缩布局。可以自适应,可以随页面大小而自适应。尽可能的保证页面布局。
其实就简单的页面来说哪种布局都无所谓,又不准备出Android版,和iOS版。。。本项目中用的是布局类,可以拉伸不变形的那种
示例:
self.pushButton = QPushButton('选择存取路径', self)
globalLayout = QHBoxLayout(globalWidget)
globalLayout.addLayout(self.pushButton)
⑥选择省份的弹出框副组件:
这块需要一个单独的弹出框组件choose_province.py
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ChooseProvince(QDialog):
Signal_OneParameter = pyqtSignal(list)
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
gridlayout = QGridLayout()
self.names = [
'全国', '黑龙江', '吉林', '辽宁', '北京', '天津', '山东', '河北', '山西', '河南', '陕西', '内蒙古',
'宁夏', '新疆', '甘肃', '青海', '浙江','江苏','安徽','上海', '湖南', '湖北', '江西', '福建', '广东',
'广西', '云南', '贵州', '四川', '重庆', '西藏','海南'
]
positions = [(i, j) for i in range(6) for j in range(6)]
self.list = []
self.localsName = locals()
for position, name in zip(positions,self.names):
cursor = self.names.index(name)
self.localsName['qcheck' + str(cursor)] = QCheckBox(name, self)
if (name == '全国'):
self.localsName['qcheck' + str(cursor)].stateChanged.connect(
lambda: self.allCountryClick(self.localsName['qcheck0']))
else:
self.localsName['qcheck' + str(cursor)].stateChanged.connect(self.partCheck)
self.list.append(self.localsName['qcheck' + str(cursor)])
gridlayout.addWidget(self.localsName['qcheck' + str(cursor)], *position)
vboxlayout = QVBoxLayout()
vboxlayout.addLayout(gridlayout)
self.buttonshure = QPushButton("确定", self)
self.buttonshure.clicked.connect(lambda: self.ensurebutton(self.buttonshure))
hboxlayout = QHBoxLayout()
hboxlayout.addStretch(1)
hboxlayout.addWidget(self.buttonshure)
vboxlayout.addLayout(hboxlayout)
self.setLayout(vboxlayout)
self.resize(400, 200)
self.center()
self.setWindowTitle('请选择省份')
self.setWindowIcon(QIcon('a.png'))
self.show()
def initData(self,data):
if '全国' in data:
data.remove('全国')
if(len(data) != 0):
for i in range(1,len(self.list)):
if self.list[i].text() in data:
self.list[i].setChecked(True)
else:
self.list[i].setChecked(False)
def ensurebutton(self, button):
namelist = []
for item in self.list:
if (item.isChecked()):
namelist.append(item.text())
self.selectvalue = ",".join(namelist)
self.Signal_OneParameter.emit(namelist)
self.close()
def allCountryClick(self, checkbox):
checkState = checkbox.checkState()
if(checkState == 0):
self.list[0].setTristate(False)
self.clearSelect()
if (checkState == 1):
pass
if (checkState == 2):
self.list[0].setTristate(False)
self.allSelect()
def clearSelect(self):
for i in range(1, len(self.list)):
self.list[i].setChecked(False)
def allSelect(self):
for i in range(1, len(self.list)):
self.list[i].setChecked(True)
def partCheck(self):
self.list[0].setCheckState(self.checkOhters()['status'])
def checkOhters(self):
checkdict = {}
# 判断是否全选
self.countNumber = 0
for i in range(1, len(self.list)):
if (self.list[i].isChecked() == True):
self.countNumber += 1
# 全选
if self.countNumber == len(self.list) - 1:
checkdict['status'] = Qt.Checked
return checkdict
# 部分全选
elif self.countNumber > 0 and self.countNumber < len(self.list) - 1:
checkdict['status'] = Qt.PartiallyChecked
self.list[0].setTristate(True)
return checkdict
# 全部选
elif self.countNumber == 0:
checkdict['status'] = Qt.Unchecked
return checkdict
# 默认不选
else:
checkdict['status'] = Qt.Unchecked
return checkdict
def center(self):
# 获得窗口
qr = self.frameGeometry()
# 获得屏幕中心点
cp = QDesktopWidget().availableGeometry().center()
# 显示到屏幕中心
qr.moveCenter(cp)
self.move(qr.topLeft())
说明:这个组件其实也是一个弹出框子组件,里面使用的是网格布局,代码在网上找到的抄的。。。
这块需要用到自定义信号传值,首先在子组件内定义一个信号:
Signal_OneParameter = pyqtSignal(list)
然后按钮时触发的事件,把要传出去的信息广播出去,emit('打雷下雨收衣服了~'),emit(namelist) namelist就是要广播的值
self.Signal_OneParameter.emit(namelist)
在主界面里直接拿到子组件的引用的信号成员变量连接主界面中的方法
self.selectProvince = ChooseProvince()
self.selectProvince.Signal_OneParameter.connect(self.getProvinceData)
主界面方法中getProvinceData参数中的list就是子组件广播的值:
def getProvinceData(self, list):
if '全国' in list:
list.remove('全国')
self.lbl99.setText(",".join(list))
session two 脚本启动scrapy 爬虫
其实scrapy 官方已经提供2种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
本项目中采用了第一种,为啥用第一种参考partone 致敬大神的代码
直接上启动脚本代码:
def startSpiderBegain(self):
self.p = Process(target=crawl, args=(self.transeMessage,self.Q,self.monitorQueue))
self.p.start()
注意:重要事情说一遍,
①crawl一定要定义成全局函数,否则程序有bug,多线程日志取不出来
②一定要 把settings = get_project_settings()扔给 process = CrawlerProcess(settings),这样scrapy才能加载默认配置,否则scrapy 只发请求爬数据,管道中间件都不起作用。
def crawl(message,queue,monitor):
# CrawlerProcess
settings = get_project_settings()
process = CrawlerProcess(settings)
process.crawl(VersionSpider, param=message, logrecord=queue,signal=monitor)
process.start()
crawl()方法中可以传递参数:
message 也就是self.transeMessage封装的是用户输入的(搜索信息,百度ak,excel存储路径,选择爬取数据的省份)
queue 也就是self.Q这是一个用于取scrapy中的数据作为日志展示在主界面上的队列。
signal 也就是self.monitorQueue 是用于打开可视化matplotlib数据图表的通信队列1.0版本中不涉及
这样就可以启动scrapy 了。
session three 通过多线程使用队列从爬虫中取日志
这块涉及python的核心技术,队列通信。队列有好几种,单向双向等,不往复杂了说。本项目中只用到最简单的单向队列,简单说比作漏斗一样,一头装数据一头取数据。scrapy 往队列里装数据,主界面从队列中取数据。队列是可以跨进程,跨线程的。
1、先生成一个漏斗
self.Q = Manager().Queue(maxsize=0) # 用于取scrapy中日志的队列,用于主界面和scrapy通信
2、倒油工:开启一个线程专门倒油
self.log_thread = LogThread(self.Q,self) # 读取日志的线程,用于取self.Q的队列中取日志
self.log_thread.sinOut.connect(self.getLogContent) # pyqt5信号,用于子线程和主页面通信
倒油工的工作职责:每隔5s中倒一次油,
from PyQt5.QtCore import QThread, pyqtSignal
import time
# 此线程用于将在spider中队列中存入的日志,每150ms循环一次,取出来传递给主页面日志渲染框
class LogThread(QThread):
sinOut = pyqtSignal(str)
def __init__(self,logque,tanyanshi):
super().__init__()
self.logQue = logque
self.fuckxiaotan = tanyanshi
def run(self):
time.sleep(5)
while True:
if not self.logQue.empty():
pieceLog = self.logQue.get_nowait()
self.sinOut.emit(pieceLog)
# 睡眠10毫秒,否则太快会导致闪退或者显示乱码
self.msleep(150)
1、先看看油桶里有没有油(if not self.logQue.empty()),如果有油就往出倒,没有就在再等5s看看有没有油,有油再倒。
2、init中的出始化的logque就是上面说的队列self.Q(漏斗),初始化传递过来的
3、同样线程中定义了信号,和上面讲的信号一样,子线程发出信号给pyqt5主界面,同时emit(pieceLog)一条数据给主界面,主界面拿到每一条scrapy塞过来的数据,并在界面右侧显示。而另一头scrapy如何往队列里装数据,part3中再说
主界面拿到数据:
def getLogContent(self,pieceLog):
self.logContent.append(pieceLog)
# 确保滑动条到底
cursor = self.logContent.textCursor()
pos = len(self.logContent.toPlainText())
cursor.setPosition(pos)
self.logContent.setTextCursor(cursor)
# self.progressBar.setValue(self.step)
if '爬取结束' in self.logContent.toPlainText():
self.startButton.setText('开始爬取数据')