最近公司的加班调休审批制度有一些调整,由于公司网站上没有清楚的标明各自有多少天可以调休,所以为了清楚的知道自己还有多少天可以调休,就想着使用爬虫爬一下考勤信息,把它放在一个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

保存下来的内容如图所示:

祝好