本程序仅用作个人编程学习,严禁用于网站恶意攻击,所爬取数据严禁出售、传播或用于商业用途!!!

介绍

基于某政府招标网的数据采集类爬虫,可以获取招标工程信息。利用Pythonselenium模块操作浏览器自动化测试工具webdriver来运行。

可以获取相关信息:

  1. 招标工程名;
  2. 中标单位;
  3. 中标金额(百分率);
  4. 评审委员会名单;
  5. 项目地点;
  6. 详细信息链接。

运行程序后。爬取数据保存在程序同文件夹下的BiddingInfo.json中。

一些问题

数据准确性:

由于该网站的中标公示信息并不是采用统一的格式,所以获取中标详细信息可能会出现失败(例如:中标金额和中标单位),所以需要根据不同页面的不同格式来做出相应的处理。

程序效率:

由于基于浏览器自动化测试工具selenium,所以效率注定不会太高,但是优点在于可以实时观察数据爬取情况,出现意外时及时停止运行。

编程笔记

关于xpath获取元素

如图所示:使用xpath语法//tbody//td[2]获取的并不是整个tbody中的第二个td元素,而是tbody下一级中所有的所有的第二级的td元素。

截图示例
截图示例

selenium模块的使用中,不能直接使用xpath语法获取元素内文字,因为selenium语法要求寻找到的对象必须是html元素,不能是字符串。不能使用xpath语法直接获取标签内文字:

temp_dict['legal_person'] = self.driver.find_element_by_xpath("//tbody/tr[2]/td[2]/text()") # 项目法人
temp_dict['time'] = self.driver.find_element_by_xpath("//tbody/tr[5]/td[4]/text()")  # 时间

会报错:

Message: invalid selector: The result of the xpath expression "//tbody/tr[2]/td[2]/text()" is: [object Text]. It should be an element.

获取页面隐藏元素的text

之前遇到的疑问:

使用xpath定位时,最好先将浏览器窗口滚动到屏幕上,否则元素获取不准确,有时候还获取不到,不要以为只要元素只要在当前html文档中就能获取!!!

xpath获取元素里文本的两个必要条件:

  1. 元素在DOM中,如果页面存在Iframe框架则需要定位到框架后获取;
  2. 元素在当前窗口显示(人眼可以看到)。

解决该办法可以通过移动鼠标和执行滚动页面JS函数实现。

隐式等待没啥用,有时候浏览器界面已经可以看到元素,但是获取元素的text还是获取不到,需要使用time.sleep()强制等待。

为何出现这种情况?

html元素内,有些元素虽然在DOM文档中,但是该元素的css属性确实display: none;,而对这种元素直接使用element.txt是获取不到值的,因为由于webdriver spec的定义,Selenium WebDriver 只会与可见元素交互,所以获取隐藏元素的文本总是会返回空字符串(在使用scrapy框架的时候不会存在这个问题)。

在这些情况下,我们需要获取隐藏元素的文本。这些内容可以使用element.get_attribute('attributeName')方法来获取,通过textContent, innerText, innerHTML等属性获取值。

innerHTML 会返回元素的内部 HTML, 包含所有的HTML标签(例如,<div>Hello <p>World!</p></div>innerHTML会得到Hello <p>World!</p>)

textContentinnerText 只会得到文本内容,而不会包含 HTML 标签(textContent 是 W3C 兼容的文字内容属性,但是 IE 不支持;innerText 不是 W3C DOM 的指定内容,FireFox不支持)。

页面跳转后数据消失

在A页面保存了大量需要跳转页面的url,如果程序进行跳转,则之前保存的url会消失,需要在跳转之前使用一个变量(例如:数组)将所有链接保存进去。

# 定义一个数组暂时存放本页所有url
temp_url_list = []
# 跳转至项目详细信息页
for item in project_list:
    # print(item.get_attribute('href'), type(item.get_attribute('href')))
    temp_url_list.append(item.get_attribute('href'))

print(f"第{self.counter}页获取{len(temp_url_list)}条数据")

for url in temp_url_list:
    self.blank_in_detail(url)

# print(project_list[0].get_attribute('href')) # 可以正常打印
# for item in project_list:
#     self.blank_in_detail(item.get_attribute('href'))
#     print(project_list[0].get_attribute('href')) # 不能正常打印!页面跳转后数据消失!!!

一个正则的学习

import re

string = '''
S11芜湖至黄山高速公路通信管道工程施工中标公示公告

“S11芜湖至黄山高速公路通信管道工程施工(项目编号: 2020DFAGZ01853/GC20200417002-wxgd)”招标评标工作已经结束,中标人已经确定,现将中标公示公告如下:

中标单位名称:中徽建技术有限公司

中标金额: 人民币壹仟柒佰陆拾柒万捌仟圆整(¥17678000.0000)。
'''

print(re.search(r'中标金额(.*?)([\d+\.]+)(\)?)', string).group(2)) # 17678000.0000
  • 字符串前加r防止字符串转义;
  • *?表示非贪婪模式获取,通过在 *+? 限定符之后放置 ?,该表达式从"贪婪"表达式转换为"非贪婪"表达式或者最小匹配。

源代码

from selenium import webdriver
import time
import random
import re
import openpyxl
import json


# 创建一个爬虫类
class BiddingInfo(object):

    def __init__(self):
        '''
            对象初始化
        '''
        self.start_url = ''
        self.file = open('BiddingInfo.json', 'w',
                         encoding='utf-8')  # 设置打开格式,防止保存后乱码
        self.counter = 0  # 翻页计数器

    def run(self):
        '''
            定义对象入口函数
        '''
        self.get_web_gage()
        while True:

            return_mark = self.get_list()

            if return_mark:
                self.jump_into_iframe()  # 由于进入项目详细信息页,窗口跳转回来后需要重新进入框架

            self.click_next_page()

    def get_web_gage(self):
        '''
            构造浏览器监控并发送请求
        '''
        self.driver = webdriver.Chrome()
        self.driver.get(self.start_url)
        self.driver.implicitly_wait(10)  # 隐式等待,最长等20秒
        self.move_search_btn()  # 第一次打开窗口需要点击搜索按钮
        self.jump_into_iframe()

    def jump_into_iframe(self):
        '''
            将页面driver指向跳进frame框架
        '''
        frame = self.driver.find_element_by_id('infoframe')  # 根据id定位 frame元素
        self.driver.switch_to.frame(frame)  # 转向到该frame中

    def move_search_btn(self):
        '''
            将鼠标移动到该招标公告列表
        '''
        search_btn = self.driver.find_elements_by_class_name(
            "ewb-right-tab")[3]
        # 最大化当前页才能将鼠标移动到该元素
        self.driver.maximize_window()
        webdriver.common.action_chains.ActionChains(
            self.driver).move_to_element(search_btn).perform()
        time.sleep(3)  # 点击搜索后需要等待frame框架数据刷新

        # 页面滚动到最后,避免获取元素出现异常
        self.driver.execute_script('scrollTo(0,1000)')

    def get_list(self):
        '''
            逐条获取工程列表页的每条信息
        '''
        project_list = self.driver.find_elements_by_xpath(
            "//ul[contains(@class,'ewb-right-item')]//a")

        # 定义一个数组暂时存放本页所有url
        temp_url_list = []
        # 跳转至项目详细信息页
        for item in project_list:
            name = item.get_attribute("textContent")
            if (name.find("设计") > 0) and (name.find("路") > 0): # 根据项目名称进行过滤筛选
                temp_url_list.append(item.get_attribute('href'))

        print(f"第{self.counter+1}页获取{len(temp_url_list)}条有效数据.")

        if len(temp_url_list) > 0: # 只有当列表获取了有效数据才进行跳转
            for url in temp_url_list:
                self.blank_in_detail(url)
            return True # 本页获取了有效数据,就需要跳转至详情页面,那么再次跳转回来就需要重新进入Iframe框架
        else:
            return False

    def blank_in_detail(self, url):
        '''
            打开工程列表某项的详细新窗口
        '''
        # 通过执行js来新开一个标签页
        js = "window.open('"+url+"');"
        self.driver.execute_script(js)

        windows = self.driver.window_handles  # 1. 获取当前所有的窗口
        self.driver.switch_to.window(windows[1])  # 2. 根据窗口索引进行切换

        self.get_detail_info(url)
        self.driver.close()
        self.driver.switch_to.window(windows[0])  # 虽然窗口以关闭但是还要手动跳转

    def get_detail_info(self, url):
        '''
            获取工程详细页面的信息
        '''
        temp_dict = {}  # 整合该工程的字典
        temp_dict['url'] = url  # 该工程的网页url
        temp_dict['name'] = self.driver.find_element_by_id(
            'showtitle').text  # 工程名

        temp_dict['legal_person'] = self.driver.find_element_by_xpath(
            "//*[@id='container']//tbody/tr[2]/td[2]").get_attribute("innerHTML")  # 项目法人
        temp_dict['time'] = self.driver.find_element_by_xpath(
            "//tbody/tr[5]/td[4]").get_attribute("innerHTML")  # 时间

        print(temp_dict['legal_person'], temp_dict['time'])

        main_text = self.driver.find_elements_by_xpath(
            "//div[contains(@class, 'ewb-info-bd')]")[3]

        item_info = self.handle_string(
            main_text.get_attribute('textContent'))  # 获取招标信息

        # 完善项目信息
        temp_dict['bid_price'] = item_info[0]
        temp_dict['bidder'] = item_info[1]
        temp_dict['committee'] = item_info[2]
        temp_dict['abandon'] = item_info[3]

        print(temp_dict)

        # 写入数据
        json_data = json.dumps(temp_dict, ensure_ascii=False)+',\n'  # json格式化
        self.file.write(json_data)  # 写入json数据

    @staticmethod
    def handle_string(string):
        '''
            对项目详细页的中标信息进行数据筛选
        '''
        string = re.sub(' ', '', string)  # 去除所有空格(换行符不去掉,可以作为分隔符)
        string = re.sub(':', ':', string)  # 替换中文符号
        string = "".join(
            [s for s in string.splitlines(True) if s.strip()])  # 去除所有空白行

        # print(string)

        if (string.find('流标') > 0) or (string.find('暂无信息') > 0) or (string.find('放弃') > 0):
            # 项目流标或无信息
            return '', '', '', True
        else:
            bid_price = bidder = committee = ''
            try:
                # 项目有中标信息
                if re.search(r'(:?)([\d+\.]+)(\)?)元', string):  # 查找中标金额
                    bid_price = re.search(
                        r'(:?)([\d+\.]+)(\)?)元', string).group(2)
                elif re.search(r'中标金额(.*?)([\d+\.]+)(\)?)', string):  # 查找中标金额
                    bid_price = re.search(
                        r'中标金额(.*?)([\d+\.]+)(\)?)', string).group(2)
                elif re.search('中标单价:(.+)\n', string):
                    bid_price = re.search('中标单价:(.+)\n', string).group(1)
                elif re.search("中标费率:(.+)\n", string):
                    bid_price = re.search('中标单价:(.+)\n', string).group(1)
                else:
                    pass

                if re.search('单位名称:(.+)\n', string):  # 查找中标单位
                    bidder = re.search('单位名称:(.+)\n', string).group(1)

                if re.search('成员名单:(.+)\n', string):  # 查找中标单位
                    committee = re.search('成员名单:(.+)\n', string).group(1)
            except:
                print("获取项目信息失败!")
            finally:
                # 返回招标价格、中标人,评标委员会
                return bid_price, bidder, committee, False

    def click_next_page(self):
        '''
            点击进入下一页
        '''
        
        try:
            next_page_url = self.driver.find_elements_by_class_name(
                "wb-page-next")[1]
        except:
            print("获取下一页失败!(或者已经爬取全部数据.)")
        else:
            self.counter += 1
            print(f"第{self.counter}页数据爬取完毕,正在跳转至下一页...")
            next_page_url.click()
            time.sleep(3)  # 等待网页重构Iframe框架

    def __del__(self):
        '''
            对象销毁时保存文件(防止程序异常终止)
        '''
        self.file.close()  # 保存文件
        self.driver.quit()  # 退出浏览器


if __name__ == "__main__":
    # 程序入口
    my_spider = BiddingInfo()
    my_spider.run()