笔记:
标题
招标方:供应商
中标方:发布机构
成交时间 = 中标时间:发布日期
中标金额:成交金额
只要“结果公告、结果公示”
项目背景
政府的采购意向一向是许多中大型公司的主营业务之一,因此,实时动态的掌握政府的采购信息能够更有效的帮助企业盈利,这次我们的目标是商洛市政府网下面的招标与中标公告两个板块,主要通过中标公告所提供的信息,我们将会从中抽取相关的实体:招标方、中标方、中标时间、中标金额、成交时间等并将其保存在mysql数据库中。
网页分析
分析主页面,获取网页url,找到控制页数的参数:
控制台截图:
查询字符串截图:
发现确实是第二页:
基础网页分析到此结束,关于内容网页的分析会在“技术实现”中说明
技术流程图
技术实现
历史数据爬取
一开始的技术方案是使用串行的方式爬取所有的历史数据,但是发现速度慢如乌龟,于是决定使用并行的方式进行爬取,所使用的模块为
“concurrent.futures.ThreadPoolExecutor”,即使用多线程技术来进行爬取,中间有一段阻塞的时间,正好可以作为一种规避反爬虫的手段,因为在实际试验中发现,此网站的反爬手段非常单一,即封禁访问频繁的IP,那么在产生大量访问之前,根据试验发现,休息一下可以产生一定的迷惑效果,减少被封的次数,此外我也在其他方面做了改进以应对这种策略,这部分放在“技术优势”来讲。数据库采用了mysql,所有爬取到的数据经过实体抽取以后将以字符创的形式存入。
爬虫主体
def spider(url, headers):
success = False
while not success:
try:
res = requests.get(url, headers=headers)
success = True
except:
num = time.sleep(random.randint(5,20))
print("糟糕,你的爬虫被发现了!!但是别担心,{}秒后我们就会重启!嘿嘿".format(num))
# requests默认的编码是‘ISO-8859-1’,会出现乱码,这里重编码为utf-8
res.encoding = 'utf-8'
return res.text
spider函数负责处理请求与响应,将requests经过简单的封装以后,它拥有了一项新的功能,那就是不间断对某个链接尝试访问,如果出错,就间隔5到20s再发起请求。
获取网页url
在用并行处理这个问题的时候,受限于mapreduce框架,我只能获取到全部的url再进行请求处理,所以一共会有两段并发请求过程。
# 爬取历史数据
# 打开数据库连接
info = {
"host": "localhost",
"user": "root",
"password": "haizeiwang",
"db": "TESTDB",
"charset": "utf8" # 一定要加上负责中文无法显示
}
db = database(info)
# 创建数据表,如果存在则删除
db.create_table()
st = time.time()
# 定义原始页面url
page_url = "http://www.shangluo.gov.cn/zwgk/szfgkmlxxgk.jsp?ainfolist1501t=24&ainfolist1501p={}&ainfolist1501c=15&urltype=egovinfo.EgovInfoList&subtype=2&wbtreeid=1232&sccode=zccg_zhbgg&gilevel=2"
# 定义拼接字符串
content_url = "http://www.shangluo.gov.cn"
"""并行方案: 用两遍多线程,一遍获得所有的url,一遍过得所有的实体列表"""
# 生成所有页面url
urls = [page_url.format(i) for i in range(1, 25)]
"""开启页面线程池子"""
executer = ThreadPoolExecutor(max_workers=8)
# 生成map对象
concent_concurrent_url_list = executer.map(get_all_content_url, urls)
# 实例化map对象,获得所有内容url
_concent_concurrent_url_list_maped = list(concent_concurrent_url_list)
# 将列表展开
concent_concurrent_url_list_maped = []
for j in _concent_concurrent_url_list_maped:
for k in j:
concent_concurrent_url_list_maped.append(content_url + k)
# # ---阻塞---
print("页面线程已全部结束,进入10秒睡眠")
time.sleep(10)
print("睡眠结束,进入内容线程阶段")
# 生成map对象
obj_concurrent_list = executer.map(get_obj_list, concent_concurrent_url_list_maped)
# 实例化map对象,获得所有实体列表
obj_concurrent_list_maped = list(obj_concurrent_list)
# 将列表展开并保存
count = 0
for i in obj_concurrent_list_maped:
# save("./Current_content_list.txt", i)
# 将实体列表保存进数据库
db.insert_data(i)
print("第{}条数据插入完毕".format(count))
count += 1
et = time.time()
print("并行用时:", (et - st))
实体抽取
首先我们定义要获取实体列表,我们采用“元素选择+正则表达式”的方法来从网页中获取这些信息:
因为我所做的是简单的实体抽取,所以我的正则表达式有限,只针对以下几种案例进行抽取:
案例
下面是我所有使用来做实体抽取的的正则表达式
- 招标方,关键词:“采购单位:…\n”,“采购人信息\n名 称:…\n”,“采购人名称:…\n”
["(?s)<P>1.采购人信息</P>\r\n<P>名 称:(.*?)</P>", '采购单位:(.*)</p>', "采购人名称:(.*)</P>","(?s)\r\n第一中标候选人(.*?)<BR>"]
- 中标方,关键词:“中标单位:…\n”,“中标方:…\n”,“中标供应商:…\n”,“供应商名称:”
"中标单位:...\n","中标方:...\n","中标供应商:...\n","供应商名称:"
- 成交时间,正则匹配第一次的时间,之后转换为sql格式(xxxx-xx-xx)
"(\d{4}年\d{1,2}月\d{1,2}日)"
- 中标金额,关键词:“成交金额:…\n”,“中标金额:…\n”,“中标价:\n”
['成交金额:(.*)</P>', "中标金额:(.*)</P>", "中标价:(.*)<\P>"]
在这里的逻辑是:只匹配正则列表第一次出现的信息,因为许多文件会有重复的格式,造成爬取的困难与信息的紊乱。
代码实现:
# file_name:Regter.py
import regex as re
#获得招标方,关键词:"采购单位:...\n","采购人信息\n名 称:...\n","采购人名称:...\n"
def get_invitor(data):
pattern_list = ["(?s)<P>1.采购人信息</P>\r\n<P>名 称:(.*?)</P>", '采购单位:(.*)</p>', "采购人名称:(.*)</P>","(?s)\r\n第一中标候选人(.*?)<BR>"]
for i in pattern_list:
res = re.findall(i, data)
if res:
invator = res[0]
break # 只要第一个条件满足就行
else:
invator = ""
return invator
#获得中标方,关键词:"中标单位:...\n","中标方:...\n","中标供应商:...\n","供应商名称:"
def get_win(data):
pattern_list = ['供应商名称:(.*)</P>', "中标单位:(.*)</P>", "中标方:(.*)</P>"]
for i in pattern_list:
if re.findall(i, data):
win = re.findall(i, data)[0]
break # 只要第一个条件满足就行
else:
win = ""
return win
#获得成交金额,关键词:"成交金额:...\n","中标金额:...\n","中标价:\n"
def get_money(data):
pattern_list = ['成交金额:(.*)</P>', "中标金额:(.*)</P>", "中标价:(.*)<\P>"]
for i in pattern_list:
if re.findall(i, data):
money = re.findall(i, data)[0]
break # 只要第一个条件满足就行
else:
money = ""
return money
#获得成交时间,正则匹配第一次的时间,之后还要转换sql格式
def get_date(data):
time = re.findall("(\d{4}年\d{1,2}月\d{1,2}日)", data)[0]
time = time.replace("年", "-").replace("月", '-').replace("日", "")
return time
- 文件标题,借助beautifulsoup,根据观察发现,所有的标题所用的css样式一致,通过这一点就可以定位所有文件的标题:
# belongs Currentmain.py
content_soup = BeautifulSoup(content_text, 'html.parser')
# try:
title = content_soup.find_all("td",
style="FONT-WEIGHT: bold;FONT-SIZE: 14pt;COLOR: #d52b2b;LINE-HEIGHT: 250%;FONT-FAMILY: 宋体;TEXT-ALIGN: center")[
0].text.strip()
数据保存
在这里我使用了mysql作为保存手段,这里踩了一个坑,根据题目要求,将网页保存为字符串存入数据库中,但是由于网页拥有大量的“\”之类的符号,故需要进行转义再将其存入,可以使用 pymysql.escape_string()这一函数。
我将数据保存流程封装为了一个类,为了保证代码的复用性以及稳健性,我实现了以下功能
- “创建连接”
- “创建数据表,若已存在就删除”
- “插入数据,若发生错误就回滚”
import pymysql
class database:
def __init__(self, info=None):
if info != None:
self.info = info
else:
self.info = {
"host": "localhost",
"user": "root",
"password": "Haizeiwang_123",
"db": "TESTDB",
"charset": "utf8" # 一定要加上负责中文无法显示
}
def create_table(self):
# 打开数据库链接
db = pymysql.connect(**self.info)
# 创建游标对象
cursor = db.cursor()
# 使用 execute() 方法执行 SQL,如果表存在则删除
cursor.execute("DROP TABLE IF EXISTS 商洛市政府官网中标信息")
# 使用预处理语句创建表
sql = """CREATE TABLE 商洛市政府官网中标信息 (
id INT AUTO_INCREMENT,
文件类型 CHAR(50) NOT NULL,
招标方 CHAR(20) NULL,
中标方 CHAR(20) NULL,
成交时间 CHAR(20) NULL,
成交金额 CHAR(20) NULL,
文件标题 CHAR(50) NULL,
网页内容 LONGTEXT NULL,
PRIMARY KEY ( `id` )
)"""
cursor.execute(sql)
# 关闭数据库连接
db.close()
def insert_data(self, content=None):
# 链接数据库
db = pymysql.connect(**self.info)
# 创建游标对象
cursor = db.cursor()
# SQL 插入语句
# 在插入网页之前,需要先对其进行转义
content[-1] = pymysql.escape_string(content[-1])
sql = "INSERT INTO 商洛市政府官网中标信息(文件类型, \
招标方, 中标方, 成交时间, 成交金额, 文件标题, 网页内容) \
VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')" % \
tuple(content)
try:
# 执行sql语句
cursor.execute(sql)
# 提交到数据库执行
db.commit()
except:
# 如果发生错误则回滚
db.rollback()
# 关闭数据库连接
db.close()
if __name__ == '__main__':
info = {
"host": "localhost",
"user": "root",
"password": "haizeiwang",
"db": "TESTDB",
"charset": "utf8" # 一定要加上负责中文无法显示
}
db = database(info)
db.create_table()
with open("./content.html",'r',encoding='utf-8') as file:
f = file.read()
# f = f.replace("\r", "").replace('\n',"").replace("/","").replace("\\","")
# f = pymysql.escape_string(f)
print(type(f))
print(f)
db.insert_data(['a','c','d','f','x','d',f])
print("finish")
爬取更新数据
由于此网站更新不定时,且不定量,所以原先准备采用定时部署的方案被放弃,转而采用手动更新的方式,即用户判断需要爬取页数以及数据条数,或者只确定需要爬取的条数,程序就可以实现爬取,处理,存储一条龙。代码如下:
import requests
from bs4 import BeautifulSoup
import time
from SQL import database
from Regter import *
import sys
def spider(url, headers):
res = requests.get(url, headers=headers)
# requests默认的编码是‘ISO-8859-1’,会出现乱码,这里重编码为utf-8
res.encoding = 'utf-8'
return res.text
def pagemade(text): # 获取页数列表
soup = BeautifulSoup(text, 'html.parser')
reslis = soup.find_all('a', target="_blank")[3:-4:2] # 固定的序列分割模式
reslis = [i['href'] for i in reslis]
return reslis
def save(file_name, text):
with open(file_name, 'a',encoding='utf-8') as f:
f.write(str(text)+'\n')
def contentmade(content_text):
content_soup = BeautifulSoup(content_text, 'html.parser')
# try:
title = content_soup.find_all("td",
style="FONT-WEIGHT: bold;FONT-SIZE: 14pt;COLOR: #d52b2b;LINE-HEIGHT: 250%;FONT-FAMILY: 宋体;TEXT-ALIGN: center")[
0].text.strip()
# 对于文本的选择content_soup.find_all("p")[4:-4]
invitation = get_invitor(content_text)
win = get_win(content_text)
money = get_money(content_text)
win_time = get_date(content_text)
sql_lis = ["中标文件", invitation, win, win_time, money, title, content_text]
# except:
# sql_lis = ["中标文件", "", "", "", "", "", content_text]
return sql_lis
def main(m,n=None):
if n == None:
n = m // 15 + 1# 保证比总条数多一页
else:
pass
# 爬取更新书数据
# 链接数据库
db = database()
# 创建数据表
db.create_table()
# 定义原始页面url
page_url = "http://www.shangluo.gov.cn/zwgk/szfgkmlxxgk.jsp?ainfolist1501t=24&ainfolist1501p={}&ainfolist1501c=15&urltype=egovinfo.EgovInfoList&subtype=2&wbtreeid=1232&sccode=zccg_zhbgg&gilevel=2"
# 定义拼接字符串
content_url = "http://www.shangluo.gov.cn"
# 定义头部信息
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
# 页面循环
for i in range(1, n):
# 获得页面数据
pagetext = spider(page_url.format(i), headers)
# 获得页面链接列表
page_list = pagemade(pagetext)
# 内容循环
for j in page_list[:m]:
# 获得内容数据
content_text = spider(content_url + j, headers)
# 获得内容实体列表
content_list = contentmade(content_text)
# 将实体列表保存进数据库
db.insert_data(content_list)
# 将实体列表保存
# save("./update_content_list.txt",content_list)
print('第{}页爬取完毕'.format(i))
if __name__ == '__main__':
m = eval(input("请指定爬取条数:"))
try:
n = eval(input("请指定爬取页数(可以为空):"))
except:
n = None
main(m,n)
技术参数
名称:使用mysql数据库存储的进行了简单实体抽取的多线程爬虫
使用模块 | requests,BeautifulSoup,regter,time,random,pymysql,currently.future.ThreadPoolExecutor |
主要功能 | 1.历史数据并行爬取 2.指定页数或条数进行爬取3,简单实体抽取4,mysql存储 |
效率 | 并行时间:25s,串行效率 57s |
总共请求url数 | 360 |
线程池数量 | 8 |
并行时间:
串行时间:
技术优势
- 使用并行技术
- 遭到IP封禁随机睡眠机制,防止被封禁IP导致程序中断
- 数据库错误捕获机制,遇到错误自动回滚,减少操作量