基于某政府招标网的爬虫
[notice]
本程序仅用作个人编程学习,严禁用于网站恶意攻击,所爬取数据严禁出售、传播或用于商业用途!!!
[/notice]
介绍
基于某政府招标网的数据采集类爬虫,可以获取招标工程信息。利用Python
的selenium
模块操作浏览器自动化测试工具webdriver
来运行。
可以获取相关信息:
- 招标工程名;
- 中标单位;
- 中标金额(百分率);
- 评审委员会名单;
- 项目地点;
- 详细信息链接。
运行程序后。爬取数据保存在程序同文件夹下的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
获取元素里文本的两个必要条件:
- 元素在DOM中,如果页面存在Iframe框架则需要定位到框架后获取;
- 元素在当前窗口显示(人眼可以看到)。
解决该办法可以通过移动鼠标和执行滚动页面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>)
。
textContent
和 innerText
只会得到文本内容,而不会包含 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()