使用Python爬取考勤信息
最近公司的加班调休审批制度有一些调整,由于公司网站上没有清楚的标明各自有多少天可以调休,所以为了清楚的知道自己还有多少天可以调休,就想着使用爬虫爬一下考勤信息,把它放在一个Excel表中以方便查阅。最近项目不是很忙,也趁机学习学习Python爬虫。
一、环境准备
1.首先需要先安装Python,笔者使用的Python3.X。 2.然后使用pip安装工具安装爬虫所需要的非标准库 主要使用到以下一些非标准库:
selenium:为了模拟浏览器操作 win32:为了解密浏览器Cookies openpyxl:为了操作Excel
使用pip工具安装:
pip install selenium pip install pywin32 pip install openpyxl
3.由于笔者使用的是浏览器的模拟操作,所以还需要下载一个WebDriver,笔者使用的Chrome浏览器,所以下载与Chrome版本相对应的WebDriver放在Python的安装目录下。
二、编码
1.首先,我们把所有需要使用到的库加进来
1from win32 import win32crypt
2from os import getenv
3import sqlite3
4from selenium import webdriver
5from selenium.webdriver.common.by import By
6from selenium.webdriver.support import expected_conditions as EC
7from selenium.webdriver.support.wait import WebDriverWait
8import time
9import datetime
10import openpyxl
11from openpyxl.comments import Comment
12from openpyxl.styles import Font, colors, Alignment
13import math
14import re
2.使用浏览器模拟操作 使用浏览器模拟操作时,可以选择浏览器是否可见。
1opt = webdriver.ChromeOptions()
2 opt.headless = True # 浏览器是否可见
3 driver = webdriver.Chrome(
4 options=opt
5 )
创建好了WebDriver后,就可以登录网站了:
1driver.get('https://mis.XXXXX.cn')
由于网站是需要登录的,所以要模拟登录过程。使用浏览器模拟登录有两种方式,使用账号密码直接登录和使用之前登录过的Cookies登录。
- 直接登录 直接登录需要找到账号与密码输入框,在账号密码框中输入相应的账号与密码,再点登录。
1usr_name = driver.find_element_by_class_name("ant-input") # 找到账号输入框
2psw = driver.find_element_by_id("inputPassword") # 找到密码输入框
3submit_btn = driver.find_element_by_class_name("ant-btn") # 找到登录按钮
4usr_name.send_keys("abc") # 输入账号
5psw.send_keys("abc") # 输入密码
6submit_btn.click() # 点击登录按钮
BTW:由于各个网站使用的标签不一样,所以标签仅供参考。
- 使用Cookies登录 这里定义了一个函数专门获取Chrome浏览器下考勤网站的Cookie
1def get_cookie_from_chrome():
2 conn = sqlite3.connect(getenv("LOCALAPPDATA") + r"\Google\Chrome\User Data\Default\Cookies")
3 cursor = conn.cursor()
4 cursor.execute(
5 'select host_key, name, encrypted_value, path, is_httponly, is_secure from cookies where host_key like "%mis.XXXXX.cn%"')
6 cookies = []
7 for result in cursor.fetchall():
8 value = win32crypt.CryptUnprotectData(result[2], None, None, None, 0)[1]
9 if value:
10 is_http_only = False
11 secure = False
12 if result[4] != 0:
13 is_http_only = True
14
15 if result[5] != 0:
16 secure = True
17
18 cookie = {
19 'domain': result[0],
20 'httpOnly': is_http_only,
21 'name': result[1],
22 'path': result[3],
23 'secure': secure,
24 'value': value.decode('utf-8')
25 }
26 cookies.append(cookie)
27 cursor.close()
28 return cookies
获取到Cookies之后将之添加到WebDriver中:
1cookies = get_cookie_from_chrome()
2for cookie in cookies:
3 driver.add_cookie(cookie)
4driver.get('https://mis.XXXXX.cn')
由于网站使用了大量的Ajax以及Frame技术,所以需要等待一段时间,让数据加载完成。可以直接使用
1time.sleep
函数进行显示等待,也可以使用selenium的expected_conditions条件等待。 前者简单明了,但是如果在指定的时间内数据未加载完成,获取就会失败,出现异常;后者是设置一个最大等待时间,在此时间内根据设定的间隔时间不断检测是否出现指定的数据,如果出现则继续向后执行,否则等待直到最大等待时间超时。
这里网站使用了Frame技术,左边有一个树型结构,里面有一个“个人考勤”,点了“个人考勤”后,右边才会出现个人考勤的详细列表,如下图:
所以需要切换Frame,然后点击“个人考勤”
1wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'left')))
2work_day = wait.until(EC.element_to_be_clickable((By.ID, "sundefined5")))
3work_day.click()
这段话的意思是直到找到ID为‘left’的Frame,成功切换过去为止,然后直到找到ID为"sundefined5"的元素且可以被点击时,点击它。点击了“个人考勤”后,右边就会出现详细的考勤列表。 由于这个时候还在“left”Frame中,需要切换到新的个人考勤,就需要先切换到“left“的父Frame再切到”个人考勤“Fame
1driver.switch_to.parent_frame()
2wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'dz-kaoqin_iframe')))
切换到考勤详细列表后,开始找数据然后分析数据:
1wait.until(EC.presence_of_element_located((By.XPATH, "//tbody/tr")))
2date_list = wait.until(EC.presence_of_element_located((By.XPATH, "//tbody")))
3flag, lst = parse_work_time(driver, text) # 这里使用一个专门的函数来分析数据
考勤列表的日期项可以点开,然后弹出一个对话框,详细列出了打卡记录,但如果有没上班,则为空
所以需要模拟点击日期,再获取打卡记录,获取完后,再把“打卡信息”对话框关闭,继续获取下一天的信息
1item = wait.until(EC.element_to_be_clickable((By.LINK_TEXT, lst[0])))
2item.click() # 点击日期
3wait.until(EC.frame_to_be_available_and_switch_to_it((By.XPATH, '//iframe[@frameborder="0"]'))) # 切换到“打卡信息”对话框
4time.sleep(0.2) # 这里只能使用sleep,因为这里可能只有tbody而没有数据
5record_text = driver.find_element_by_xpath("//tbody").text # 获取所有打卡记录,保存在record_text中
6driver.switch_to.parent_frame() # 返回父Frame
7close_btn = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "layui-layer-ico"))) # 找到“打卡信息”对话框的关闭按钮
8close_btn.click() # 点击关闭按钮,关闭“打卡信息”对话框
如果有考勤异常或者请假单之类的,也会有一个链接,可以查看请假单记录:
1bill_list = driver.find_elements_by_class_name("link-billId")
2for bill in bill_list:
3 bill.click()
4 wait.until(EC.frame_to_be_available_and_switch_to_it((By.XPATH, '//iframe[@frameborder="0"]')))
5 time.sleep(0.2)
6 form = driver.find_element_by_xpath('//form')
7 work_time.bill_text = form.text
8 driver.switch_to.parent_frame()
9 close_btn = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "layui-layer-ico")))
10 close_btn.click()
11 return
把考勤信息爬取到后,就需要写入Excel表了。这里使用openpyxl库来写入,因为我试了xlwings以及xlwt两个库都不支持批注,只有openpyxl支持。
1def write_to_excel(work_time_list, file_path):
2 wb = openpyxl.Workbook() # 打开一个工作薄
3 wb.encoding = 'utf-8' # 使用UTF8编码
4 sh = wb.worksheets[0] # 获取第一个sheet
5 sh.title = "考勤" #将标题改为考勤
6 sheet = wb.create_sheet("原始文本记录") # 创建一个“原始文本记录”的表
7
8 # 设置“考勤”表的表头
9 row = 1
10 col = 1
11 sh.cell(row, col, '日期')
12 sh.cell(row, col + 1, '上班时间')
13 sh.cell(row, col + 2, '下班时间')
14 sh.cell(row, col + 3, '上班打卡时间')
15 sh.cell(row, col + 4, '下班打卡时间')
16 sh.cell(row, col + 5, '最晚到时间')
17 sh.cell(row, col + 6, '迟到分钟')
18 sh.cell(row, col + 7, '早退分钟')
19 sh.cell(row, col + 8, '考勤状态')
20 sh.cell(row, col + 9, '单据编号')
21 sh.cell(row, col + 10, '单据类型')
22 sh.cell(row, col + 11, '工作分钟')
23 sh.cell(row, col + 12, '工作小时')
24 sh.cell(row, col + 13, '是否加班')
25 sh.cell(row, col + 14, '剩余可调休加班小时')
26
27 # 设置“考勤”表的列宽
28 sh.column_dimensions['A'].width = 18
29 sh.column_dimensions['B'].width = 8
30 sh.column_dimensions['C'].width = 8
31 sh.column_dimensions['D'].width = 12
32 sh.column_dimensions['E'].width = 12
33 sh.column_dimensions['F'].width = 10
34 sh.column_dimensions['G'].width = 8
35 sh.column_dimensions['H'].width = 8
36 sh.column_dimensions['I'].width = 8
37 sh.column_dimensions['J'].width = 18
38 sh.column_dimensions['K'].width = 20
39 sh.column_dimensions['L'].width = 8
40 sh.column_dimensions['M'].width = 8
41 sh.column_dimensions['N'].width = 8
42 sh.column_dimensions['O'].width = 18
43
44 sheet.column_dimensions['A'].width = 100
45
46 blue_font = Font(name='宋体', size=11, italic=False, color=colors.BLUE, bold=False)
47 red_font = Font(name='宋体', size=11, italic=False, color=colors.RED, bold=False)
48
49 over_work_time_acc = 0
50 for item in work_time_list:
51 calc_work_time(item)
52 time_hour = parse_bill_info(item)
53 sheet.cell(row, 1).value = item.origin_text
54
55 row = row + 1
56 col = 1
57 dt = datetime.datetime.strptime(item.date, "%Y-%m-%d")
58 weekday = dt.weekday() + 1
59
60 cell = sh.cell(row, col, item.date + '(星期' + str(weekday) + ')')
61 has_comment = False
62 if item.record_text.__len__() > 0:
63 cell.comment = Comment(item.record_text, None, width=350) # 设置批注
64 has_comment = True
65
66 sh.cell(row, col + 1, item.start_time)
67 sh.cell(row, col + 2, item.end_time)
68 sh.cell(row, col + 3, item.real_start_time)
69 sh.cell(row, col + 4, item.real_end_time)
70 sh.cell(row, col + 5, item.late_start_time)
71 sh.cell(row, col + 6, item.late_time)
72 sh.cell(row, col + 7, item.before_time)
73 sh.cell(row, col + 8, item.status)
74 sh.cell(row, col + 9, item.handle_sn)
75 if item.bill_text.__len__() > 0:
76 sh.cell(row, col + 9).comment = Comment(item.bill_text, None, width=550)
77
78 sh.cell(row, col + 10, item.handle_type)
79 sh.cell(row, col + 11, item.valid_work_time)
80 valid_work_time = math.floor(item.valid_work_time / 60)
81 if valid_work_time > 8:
82 valid_work_time = 8
83 sh.cell(row, col + 12, valid_work_time)
84 if weekday == 6 or weekday == 7:
85 if has_comment and item.status == '休息':
86 sh.cell(row, col + 13, '加班')
87 over_work_time_acc += valid_work_time
88
89 over_work_time_acc -= time_hour
90 sh.cell(row, col + 14, over_work_time_acc)
91
92 if weekday == 6 or weekday == 7:
93 for col in range(1, sh.max_column + 1):
94 if has_comment:
95 sh.cell(row, col).font = red_font
96 else:
97 sh.cell(row, col).font = blue_font
98
99 wb.save(file_path) # 保存Excel表
100 wb.close() # 关闭
101 return
保存下来的内容如图所示:
祝好
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2018/2018-12-29-使用Python爬取考勤信息/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。