python PyQt5 爬虫实现

搞一个图形化界面还是挺酷的,是吧

安装库什么的应该不用多说了吧。。

一般来说会让你把 designer.exe(编辑图形化界面的东西,跟vb差不多) 当作外部工具导入到 pycharm 界面里(这里不写了),其实就是打开方便点,不做也没什么关系,没有非要从pycharm打开,界面是这样的:

python PyQt5 爬虫实现_第1张图片

还要导入一个PyUIC 工具包,这个东西好像还是导入比较好,(写文件目录的时候可能由于安装的问题找不到那个文件,我刚开始也没找到,还不如直接在C盘搜索那个东西来的直接)不然挺麻烦的。UIC 是用来把你做的图形化界面直接生成代码的,手写的话要调位置大小颜色名字等等非常麻烦,而且不直观,所以这个东西非常强大。

做一个小实验。先随便编译一个界面
python PyQt5 爬虫实现_第2张图片
(其实左边的很多控件我也不知道是什么,不过就先知道那么几个必须的也差不多够了,要用的时候再去查就好了)

保存后生成 .ui 文件,保存在 .py 同一个目录下,然后用刚刚装的uic就可以把这个.ui文件转化为 .py文件,打开是这样的:

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

# Form implementation generated from reading ui file 'untitled.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!
import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.PushButton = QtWidgets.QPushButton(self.centralwidget)
        self.PushButton.setEnabled(True)
        self.PushButton.setGeometry(QtCore.QRect(270, 390, 241, 51))
        self.PushButton.setObjectName("PushButton")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(130, 350, 111, 18))
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(290, 350, 121, 18))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(430, 350, 111, 18))
        self.label_3.setObjectName("label_3")
        self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.textEdit.setGeometry(QtCore.QRect(330, 170, 107, 107))
        self.textEdit.setObjectName("textEdit")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 30))
        self.menubar.setObjectName("menubar")
        self.menuQt_designer = QtWidgets.QMenu(self.menubar)
        self.menuQt_designer.setObjectName("menuQt_designer")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.menubar.addAction(self.menuQt_designer.menuAction())

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.PushButton.setText(_translate("MainWindow", "一件三联  (按钮)"))
        self.label.setText(_translate("MainWindow", "点赞(Label)"))
        self.label_2.setText(_translate("MainWindow", "收藏(Label)"))
        self.label_3.setText(_translate("MainWindow", "关注(Label)"))
        self.menuQt_designer.setTitle(_translate("MainWindow", "Qt designer"))

你会发现它自动生成了一个类,如果想要显示这个界面,只需要在主函数里写几个语句:

    a = Ui_MainWindow()
app = QtWidgets.QApplication(sys.argv)  # 首先必须实例化QApplication类,作为GUI主程序入口
MainWindow = QtWidgets.QMainWindow()   # 实例化QtWidgets.QMainWindow类,创建自带menu的窗体类型QMainWindow
a.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())

就出现了这样的界面:
python PyQt5 爬虫实现_第3张图片

你可以在那个Text Edit 里输入字符,可以用来当作判断条件什么的,然后按钮可以写点击函数,作为人为的触发条件。

下面是爬虫

首先你要向网业发送请求,常用的模块有 urllib , urllib3, requests 等等。
例如如果想到得到百度首页的源码,可以:

import requests

response = requests.get('https://www.baidu.com')
print(response.content)

把得到的东西复制下来粘贴到记事本里,你会发现这是很明显的 html5 格式代码。
python PyQt5 爬虫实现_第4张图片

把格式转化为http,打开就可以搭建百度首页的界面了
python PyQt5 爬虫实现_第5张图片

超链接是可以点的(只要你联网),但是这是本地的,服务器里的东西都不会有(图片什么的),而且编码可能有些问题,这我也不懂啦。

请求headers 处理

很多时候网页设置了反爬虫,这时候你需要一个头部信息伪装一下(就像阿凡达一样),(我也不知道为什么很多地方都让你用火狐浏览器,不过我试了一下确实不错)。
python PyQt5 爬虫实现_第6张图片
把右下角那个蓝色的东西复制了,然后手动创建一个头部信息:

import requests

url = 'https://www.daidu.com'
headers = {
     'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0'}
response = requests.get(url, headers=headers)
print(response.content)

这样也可以得到一样的结果。(虽然百度不需要这样。)

代理服务

在爬取一个网页后,它可能会把你的IP屏蔽,所以下次就不能爬了。这时你可以用代理服务,租一个动态IP,就相当于打游戏号被封了再去买个号玩,一般是要钱的。

HTML解析之 BeautifulSoup

这是一个库,我在哪里看到有人叫他靓汤,名字挺有意思的。。。
靓汤3停止开发了,现在用4.但是已经被移植到bs4 库里了,所以导入时要 “ from bs4 import 靓汤 “ (首先安装bs4)
靓汤支持包含标准库里的解析器,也支持其他三方解析器。是用来解析html代码的(你肯定发现了上面的text文本很乱)。
我先试试xlml解析器:

import lxml
from bs4 import BeautifulSoup
soup = BeautifulSoup(open('baidu.html'), 'lxml')  #放在同一个目录下
print(soup.prettify())

然后输出的就是格式化后的html5代码了

python PyQt5 爬虫实现_第7张图片

然后跟着书上写一个查车票的代码,就当我学了hhh

1.首先获得请求地址

打开那个网站,输入目的地什么的,然后查询,然后马上打开网络监听器,刷新一下,找到类型为json的那条记录python PyQt5 爬虫实现_第8张图片
GET 后面的就是请求的地址。

2.下载站名文件

刷新一下,找到类型为js的然后文件名有 站名信息的请求。(station_name)那个
python PyQt5 爬虫实现_第9张图片

然后打开那个网址
python PyQt5 爬虫实现_第10张图片
是站名和缩写信息,我们需要将他进行相应转化(因为里面有很多无效的信息),得到一个显示站名(中文)——英文缩写的字典。

import re
import requests
import os


def getStation():
    # 发送请求获取所有车站名称,通过输入的站名称转化查询地址的参数
    url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9188'
    response = requests.get(url, verify=True)  # 请求并进行验证
    stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text)  # 获取需要的车站名称
    stations = dict((stations), indent=4)  # 转换为dic
    stations = str(stations)  # 转换为字符串类型否则无法写入文件
    write(stations)           #调用写入方法
    
    
def write(stations):
    file = open('stations.text', 'w', encoding='utf_8_sig')  # 以写模式打开文件
    file.write(stations)  # 写入数据
    file.close()

其中 stations = re.findall(u’([\u4e00-\u9fa5]+)|([A-Z]+)’, response.text) re后面的部分是正则表达式,用于匹配特定格式的字符串。前面\u开头的是表示中文的编码,然后匹配一个大写字母字符串。关于正则表达式请参考博文 https://zyc88.blog.csdn.net/article/details/98479629

然后打开写入的文本,可以得到一下内容:

python PyQt5 爬虫实现_第11张图片

车票信息的请求与显示

接下来我们需要获得网页上的车次信息。
车次信息我们可以通过查询json文件获得 (JSON是一种取代XML的数据结构,和xml相比,它更小巧但描述能力却不差,由于它的小巧所以网络传输数据将减少更多流量从而加快速度。
那么,JSON到底是什么?
JSON就是一串字符串 只不过元素会使用特定的符号标注。)
python PyQt5 爬虫实现_第12张图片

打开这个网页
python PyQt5 爬虫实现_第13张图片
你会发现 result目录下包含了时间地点等信息(地点是大写的英文缩写,所以我们之前得到了英文——中文的字典,所以就可以通过字典显示相应的中文信息)。所以我们只需要使用正则表达式匹配到需要的信息即可。

data = []  # 用于保存整理好的车次信息
type_data = []  # 保存车次分类后最后的数据
data.clear()  # 清空数据
# 查询请求地址
url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2021-04-15&leftTicketDTO.from_station=VAP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT'
headers = {
     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0'}
response = requests.get(url, headers=headers)
print(response.content)
result = response.json()

虽然加了伪装头,但是还是一直报错,我当时也不知道是为什么。
python PyQt5 爬虫实现_第14张图片
我也去试了下其他网站的json文件,发现有些是可以成功的,有些则不行。后来才知道是这个伪装没有到位,需要在header里再加上cookie信息。

url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2021-04-15&leftTicketDTO.from_station=VAP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT'
    headers = {
     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0',
               'Cookie': '_uab_collina=161832647069317045785167; JSESSIONID=30A5B88583AAA6A020412480317F10BB; RAIL_EXPIRATION=1618615370989; RAIL_DEVICEID=SoJe9TAXhJzwgdY2FSkrcHYfSq5FNXt6-FqIyW10ebz3XpTw8V4j8ZyUTYPoaXPpCwIKObW74sC_iSvN3aWjkS4Zyctl0mG3yoC1xN1VahajKcWDkQ3BJpaOXr8-fMvusS3YQE1whI9qzvcnaZGu0yU4p9uCr8nM; _jc_save_fromStation=%u5317%u4EAC%u5317%2CVAP; _jc_save_toStation=%u4E0A%u6D77%2CSHH; _jc_save_fromDate=2021-04-15; _jc_save_toDate=2021-04-15; _jc_save_wfdc_flag=dc; BIGipServerpool_passport=115606026.50215.0000; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1173357066.38945.0000'}
    # 发送查询请求
    response = requests.get(url, headers=headers)

这样就成功了。(上上上图中往下翻就能找到cookie信息)

然后对应上面json文件的数据,可以发现它的信息是通过==|==分割的,然后我们需要的地点与时间等信息的位置都是固定的,所以我们可以数出位置然后通过位置信息得到我们需要的东西

            for i in result:
                # # 分割数据并添加到列表中
                tmp_list = i.split('|')
                # 因为查询结果中出发站和到达站为站名的缩写字母,所以需要在车站库中找到对应的车站名称
                from_station = list(stations.keys())[list(stations.values()).index(tmp_list[6])]
                to_station = list(stations.keys())[list(stations.values()).index(tmp_list[7])]
                # 创建座位数组,由于返回的座位数据中含有空既“”,所以将空改成--这样好识别
                seat = [tmp_list[3], from_station, to_station, tmp_list[8], tmp_list[9], tmp_list[10]
                    , tmp_list[32], tmp_list[31], tmp_list[30], tmp_list[21]
                    , tmp_list[23], tmp_list[33], tmp_list[28], tmp_list[24], tmp_list[29], tmp_list[26]]
                newSeat = []
                # 循环将座位信息中的空既“”,改成--这样好识别
                for s in seat:
                    if s == "":
                        s = "--"
                    else:
                        s = s
                    newSeat.append(s)  # 保存新的座位信息
                data.append(newSeat)
          return data  # 返回整理好的车次信息

然后还有一些特定车型的选项,就在上面得到的基础上再做就可以:

# 获取高铁信息的方法
def g_vehicle():
    if len(data) != 0:
        for g in data:  # 循环所有火车数据
            i = g[0].startswith('G')  # 判断车次首字母是不是高铁
            if i:  # 如果是将该条信息添加到高铁数据中
                type_data.append(g)

#移除高铁信息的方法
def r_g_vehicle():
    if len(data) != 0:
        for g in data:
            i = g[0].startswith('G')
            if i:   #移除高铁信息
                type_data.remove(g)

其他的选项同理

准备工作都完成,接下来只需要编写查询按钮的函数了。
我们需要得到使用者在输入框里的地点的时间信息并检查输入的是否符合规范:

    def on_click(self):
        get_from = self.textEdit.toPlainText()  # 获取出发地
        get_to = self.textEdit_2.toPlainText()  # 获取到达地
        get_date = self.textEdit_3.toPlainText()  # 获取出发时间
        # 判断车站文件是否存在
        if isStations() == True:
            stations = eval(read())  # 读取所有车站并转换为dic类型
            # 判断所有参数是否为空,出发地、目的地、出发日期
            if get_from != "" and get_to != "" and get_date != "":
                # 判断输入的车站名称是否存在,以及时间格式是否正确
                if get_from in stations and get_to in stations and is_valid_date(get_date):
                    # 获取输入的日期是当前年初到现在一共过了多少天
                    inputYearDay = time.strptime(get_date, "%Y-%m-%d").tm_yday
                    # 获取系统当前日期是当前年初到现在一共过了多少天
                    yearToday = time.localtime(time.time()).tm_yday
                    # 计算时间差,也就是输入的日期减掉系统当前的日期
                    timeDifference = inputYearDay - yearToday
                    # 判断时间差为0时证明是查询当前的查票,
                    # 以及29天以后的车票。12306官方要求只能查询30天以内的车票
                    if timeDifference >= 0 and timeDifference <= 28:
                        from_station = stations[get_from]  # 在所有车站文件中找到对应的参数,出发地
                        to_station = stations[get_to]  # 目的地
                        print(from_station)
                        print(to_station)
                        data = query(get_date, from_station, to_station)  # 发送查询请求,并获取返回的信息
                        print(data)
                        if len(data) != 0:  # 判断返回的数据是否为空
                            # 如果不是空的数据就将车票信息显示在表格中
                            self.displayTable(len(data), 16, data)
                        else:
                            self.messageDialog('警告', '没有返回的网络数据!')
                    else:
                        self.messageDialog('警告', '超出查询日期的范围内,'
                                                 '不可查询昨天的车票信息,以及29天以后的车票信息!')
                else:
                    self.messageDialog('警告', '输入的站名不存在,或日期格式不正确!')
            else:
                self.messageDialog('警告', '请填写车站名称!')
        else:
            self.messageDialog('警告', '未下载车站查询文件!')

    # 高铁复选框事件处理
    def change_G(self, state):
        # 选中将高铁信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取高铁信息
            g_vehicle()
            # 通过表格显示该车型数据
            self.displayTable(len(type_data), 16, type_data)
        else:
            # 取消选中状态将移除该数据
            r_g_vehicle()
            self.displayTable(len(type_data), 16, type_data)

    # 动车复选框事件处理
    def change_D(self, state):
        # 选中将动车信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取动车信息
            d_vehicle()
            # 通过表格显示该车型数据
            self.displayTable(len(type_data), 16, type_data)

        else:
            # 取消选中状态将移除该数据
            r_d_vehicle()
            self.displayTable(len(type_data), 16, type_data)

    # 直达复选框事件处理
    def change_Z(self, state):
        # 选中将直达车信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取直达车信息
            z_vehicle()
            self.displayTable(len(type_data), 16, type_data)
        else:
            # 取消选中状态将移除该数据
            r_z_vehicle()
            self.displayTable(len(type_data), 16, type_data)

    # 特快复选框事件处理
    def change_T(self, state):
        # 选中将特快车信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取特快车信息
            t_vehicle()
            self.displayTable(len(type_data), 16, type_data)
        else:
            # 取消选中状态将移除该数据
            r_t_vehicle()
            self.displayTable(len(type_data), 16, type_data)

    # 快速复选框事件处理
    def change_K(self, state):
        # 选中将快车信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取快速车信息
            k_vehicle()
            self.displayTable(len(type_data), 16, type_data)

        else:
            # 取消选中状态将移除该数据
            r_k_vehicle()
            self.displayTable(len(type_data), 16, type_data)

    # 显示消息提示框,参数title为提示框标题文字,message为提示信息
    def messageDialog(self, title, message):
        msg_box = QMessageBox(QMessageBox.Warning, title, message)
        msg_box.exec_()

    # 显示车次信息的表格
    # train参数为共有多少趟列车,该参数作为表格的行。
    # info参数为每趟列车的具体信息,例如有座、无座卧铺等。该参数作为表格的列
    def displayTable(self, train, info, data):
        self.model.clear()
        for row in range(train):
            for column in range(info):
                # 添加表格内容
                item = QStandardItem(data[row][column])
                # 向表格存储模式中添加表格具体信息
                self.model.setItem(row, column, item)
        # 设置表格存储数据的模式
        self.tableView.setModel(self.model)

然后还要处理选择车型的问题:

  def change_G(self, state):
        # 选中将高铁信息添加到最后要显示的数据当中
        if state == QtCore.Qt.Checked:
            # 获取高铁信息
            g_vehicle()
            # 通过表格显示该车型数据
            self.displayTable(len(type_data), 16, type_data)
        else:
            # 取消选中状态将移除该数据
            r_g_vehicle()
            self.displayTable(len(type_data), 16, type_data)
#同样的操作,省略

因为json文件的请求地址上是有时间和地址信息的,然后我们需要根据用户输入的时间地址信息进行相应的查询,所以我们需要用format格式化url地址。

data = query(get_date, from_station, to_station)
#这两个语句不在一起
def query(date, from_station, to_station):
	url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date={
     }&leftTicketDTO.from_station={
     }&leftTicketDTO.to_station={
     }&purpose_codes=ADULT.format(data, from_station, to_station)
	#省略

接下来是显示的问题:显示函数:

    def displayTable(self, train, info, data):
        self.model.clear()
        for row in range(train):
            for column in range(info):
                # 添加表格内容
                item = QStandardItem(data[row][column])
                # 向表格存储模式中添加表格具体信息
                self.model.setItem(row, column, item)
        # 设置表格存储数据的模式
        self.tableView.setModel(self.model)

然后加上主函数就可以运行一下了。

运行一下看看效果吧:
python PyQt5 爬虫实现_第15张图片
阿这。。。。因为现在是晚上11点半了,现在去网站上看也确实没有今天的票了,明天再去试试吧。。。(7点的时候查是有票的)

#申明:省略了一些小函数和很多东西,只是大致把这个程序的过程理了一遍然后学习了相关的知识,让我自己写我现在恐怕也写不出来,这个程序确实有点难。。。可能 一定是我太菜了)

你可能感兴趣的:(爬虫,python,qt,爬虫)