爬虫下载社保数据

背景

有个妹子计算工资,需要到社保网上下载当月应扣缴明细,公司 10 多个社保账户,每个登录下载保存完需要花费 30 分钟左右。假期最后一天写个爬虫来批量下载。
运行环境:windows10 x64,python3.7

收获

  • 完成批量下载功能,节省下载时间。
  • 了解 requests_html 库 session 机制,登陆信息保存在 session,后续 request 复用。
  • 了解使用 pytesseract\Pillow\tesseract-ocr 识别简单图片验证码方法。
  • 学会使用 chrome 浏览器开发工具,分析网站登陆过程。

过程

1 分析网站登陆

登陆页面为 http://rlsbj.cq.gov.cn/ggfw/index2.jsp ,在登陆页面输入错误的登陆信息,正确的验证码。F12 开启开发者模式,并勾选“preserve log”,找到验证码生成 url、验证码验证 url 和账号密码验证 url。
验证码验证 url
image.png
验证码验证 post 信息
image.png
账号密码验证 url
image.png
账号密码验证 post 信息
image.png
查看网页源码,找到验证码生成 url,time 为毫秒时间戳。
image.png
整个登陆过程为 1)浏览器请求验证码生成 url,生成验证码,2)带上一个 session,post 验证码、账号、密码信息到账号密码验证 url。3)账号密码验证 url 会自动将验证码 post 到验证码验证 url,返回成功后,继续 post 账号、密码信息到登陆验证 url。

2 程序半自动登陆

使用 requests 库半自动登陆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
import time
t = time.time()
mtime = (str(round(t * 1000)))
code_img_url = 'http://rlsbj.cq.gov.cn/ggfw/validateCodeBLH_image.do?time='+ mtime
print(code_img_url)
valcode = requests.get(code_img_url)
print('====cookies 1======')
print(requests.utils.dict_from_cookiejar(valcode.cookies))
f = open('valcode.png','wb')
f.write(valcode.content)
f.close()
code = input('请输入验证码:')
code_data = {}
code_data["validateCode"] = str(code)
HEADERS = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
}

login_url = 'http://rlsbj.cq.gov.cn/ggfw/LoginBLH_unitLogin.do'
data = {"dwbh":'20134675',"password":'realpasswd-base64',"validateCode":code_data["validateCode"]}
print(data)
login_check = requests.post(login_url, headers = HEADERS, cookies = requests.utils.dict_from_cookiejar(valcode.cookies), data=data)
print(login_check.text) # 返回登陆结果信息,确定是否登陆成功。

check_url_1 = 'http://rlsbj.cq.gov.cn/ggfw/unit/QueryBLH_unitMain.do?code=1112'
check_url_1_r = requests.get(check_url_1, headers = HEADERS, cookies = requests.utils.dict_from_cookiejar(login_check.cookies))
print(check_url_1_r.text) # 返回登陆后首页信息

3 自动识别验证码

自动识别验证码主要参考别人的方法,踩坑是安装 tesseract-ocr。该函数能去除干扰线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import os
import pytesseract
from PIL import Image
from collections import defaultdict

# tesseract.exe所在的文件路径
# pytesseract.pytesseract.tesseract_cmd = 'C://Program Files (x86)/Tesseract-OCR/tesseract.exe'

# 获取图片中像素点数量最多的像素
def get_threshold(image):
pixel_dict = defaultdict(int)

# 像素及该像素出现次数的字典
rows, cols = image.size
for i in range(rows):
for j in range(cols):
pixel = image.getpixel((i, j))
pixel_dict[pixel] += 1

count_max = max(pixel_dict.values()) # 获取像素出现出多的次数
pixel_dict_reverse = {v:k for k,v in pixel_dict.items()}
threshold = pixel_dict_reverse[count_max] # 获取出现次数最多的像素点

return threshold

# 按照阈值进行二值化处理
# threshold: 像素阈值
def get_bin_table(threshold):
# 获取灰度转二值的映射table
table = []
for i in range(256):
rate = 0.1 # 在threshold的适当范围内进行处理
if threshold*(1-rate)<= i <= threshold*(1+rate):
table.append(1)
else:
table.append(0)
return table

# 去掉二值化处理后的图片中的噪声点
def cut_noise(image):

rows, cols = image.size # 图片的宽度和高度
change_pos = [] # 记录噪声点位置

# 遍历图片中的每个点,除掉边缘
for i in range(1, rows-1):
for j in range(1, cols-1):
# pixel_set用来记录该店附近的黑色像素的数量
pixel_set = []
# 取该点的邻域为以该点为中心的九宫格
for m in range(i-1, i+2):
for n in range(j-1, j+2):
if image.getpixel((m, n)) != 1: # 1为白色,0位黑色
pixel_set.append(image.getpixel((m, n)))

# 如果该位置的九宫内的黑色数量小于等于4,则判断为噪声
if len(pixel_set) <= 4:
change_pos.append((i,j))

# 对相应位置进行像素修改,将噪声处的像素置为1(白色)
for pos in change_pos:
image.putpixel(pos, 1)

return image # 返回修改后的图片

# 识别图片中的数字加字母
# 传入参数为图片路径,返回结果为:识别结果
def OCR_lmj(img_path):

image = Image.open(img_path) # 打开图片文件
imgry = image.convert('L') # 转化为灰度图

# 获取图片中的出现次数最多的像素,即为该图片的背景
max_pixel = get_threshold(imgry)

# 将图片进行二值化处理
table = get_bin_table(threshold=max_pixel)
out = imgry.point(table, '1')

# 去掉图片中的噪声(孤立点)
out = cut_noise(out)

#保存图片
# out.save('E://figures/img_gray.jpg')

# 仅识别图片中的数字
text = pytesseract.image_to_string(out, config='digits')
# 识别图片中的数字和字母
# text = pytesseract.image_to_string(out)

# 去掉识别结果中的特殊字符
exclude_char_list = ' .:\\|\'\"?![],()~@#$%^&*_+-={};<>/¥'
text = ''.join([x for x in text if x not in exclude_char_list])
#print(text)

return text

def main():

# 识别指定文件目录下的图片
# 图片存放目录figures
dir = 'E://figures'

correct_count = 0 # 图片总数
total_count = 0 # 识别正确的图片数量

# 遍历figures下的png,jpg文件
for file in os.listdir(dir):
if file.endswith('.png') or file.endswith('.jpg'):
# print(file)
image_path = '%s/%s'%(dir,file) # 图片路径

answer = file.split('.')[0] # 图片名称,即图片中的正确文字
recognizition = OCR_lmj(image_path) # 图片识别的文字结果

print((answer, recognizition))
if recognizition == answer: # 如果识别结果正确,则total_count加1
correct_count += 1

total_count += 1

print('Total count: %d, correct: %d.'%(total_count, correct_count))
'''
# 单张图片识别
image_path = 'E://figures/code (1).jpg'
OCR_lmj(image_path)
'''

# main()
# print(OCR_lmj('valcode2.png'))

4 整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import requests
from requests_html import HTMLSession,HTML
from vcode import OCR_lmj
import time
import json
act_list= [
{
"name": '店一',
"user": 'account',
"passwd":'passwd'
},
{
"name": '店二',
"user": 'account',
"passwd":'passwd'
}
]


def save_to_file(r,file_name):
f = open(file_name, 'wb')
f.write(r.content)
f.close()


def create_sessions(act_list):
sessions_list = []
t = time.time()
mtime = (str(round(t * 1000)))
code_img_url = 'http://rlsbj.cq.gov.cn/ggfw/validateCodeBLH_image.do?time='+ mtime
login_url = 'http://rlsbj.cq.gov.cn/ggfw/LoginBLH_unitLogin.do'
for act in act_list:
success_code = '0'
while success_code == '0':
t = time.time()
mtime = (str(round(t * 1000)))
code_img_url = 'http://rlsbj.cq.gov.cn/ggfw/validateCodeBLH_image.do?time='+ mtime
session = HTMLSession()
r = session.get(code_img_url)
save_to_file(r,'vcode.jpg')
code = OCR_lmj('vcode.jpg')
data = {"dwbh":act['user'],"password":act['passwd'],"validateCode":str(code)}
login_result = json.loads(session.post(login_url,data=data).text)
success_code = login_result['code']
# print(login_result)
sessions_list.append(session)
time.sleep(2)
print("登陆成功,共计%d个账户" % len(sessions_list))
return sessions_list


def get_url_list():
base_url = 'http://rlsbj.cq.gov.cn/ggfw/unit/ExportBLH_exportExcelAll.do?code='
export_item = ['1114','1119','1120','1127','1123','1124']
item_name = ['01医保应缴明细','02养老明细','03工伤明细','04失业汇总','05失业明细非补','05失业明细个人补']
export_month = '201910'
url_list2 = []
for item in export_item:
url_dict = {}
url_dict['name'] = item_name[export_item.index(item)]
url_dict["url"] = base_url + str(item)+'&afkssq='+export_month+'&r=0.9082537130128169'
url_list2.append(url_dict)
return url_list2


# create_sessions(act_list)

def main():
sessions_list = create_sessions(act_list)
url_list = get_url_list()
count = 0
for session in sessions_list:
for url in url_list:
file_name = act_list[sessions_list.index(session)]['name']+'-'+url['name']+'.xls'
# print(url['url'])
exp_r = session.get(url['url'])
save_to_file(exp_r,file_name)
time.sleep(2)
count = count +1
print("导出成功,共生成文件%d个" % count)
# main()
if __name__ == '__main__':
main()

运行结果,并在程序目录生成文件。
image.png

改善

~~整合函数使用改为 requests 库,兼容 python2 ~~ 继续使用 requests_html 库,功能更多,使用更方便。
生成带日期的文件
运行方式优化,有界面输入账号密码,导出月份等信息。

参考

https://segmentfault.com/a/1190000015240294 验证码识别
https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-v5.0.0-alpha.20190708.exe tesseract-ocr 下载,需要将程序路径加入到环境变量并重启 powershell。
https://github.com/cncert/requests-html-doc-cn