用python寫一個自動打卡系統

Kaminyou
10 min readJan 3, 2023

--

任何能用自己電腦上網打卡的系統,必定能照著以下思路寫出自動打卡器。

想必大多數的人,不管是在學校、實習、工作期間,免不了被要求上下班打卡,但大部分的打卡系統應該都是要求大家登入後按下上下班打卡鍵即可,不禁讓人產生是否可以寫個python script定時在上下班時間自動打卡,省去忘記打卡的窘境。因此,今天便會以概念上講述如何開發這個系統,以及自己利用公司的打卡系統開發出的程式作為範例。

目前自己實作出來的系統已經開源在GitHub repo(請點擊之)中,應該對於使用xxxxx打卡系統的所有企業都可以直接clone下來使用,若是使用其他打卡系統的公司也可以比照此法進行開發。

觀察API運作

首先,請先打開chrome的開發者模式查看network,去觀察登入是打哪個API、打卡是打哪個API,以及在登入時有什麼cookie是被設置的、打卡時什麼cookie或是header是必要傳遞的、什麼data是要跟著傳過去的。

以下的內容便是以這樣的思維編排:

A. 登入是打哪個API

可以觀察到主要好像是前面的login以及下一個main?from=/…做了一些事情,因此點進去查看打的api以及封包:

發現只有login 這個以POST打了/domain/Account/login的API並且同時傳了帳號密碼過去,另一個main?from=/… 只是上一個動作後的重新導向。

接著觀察一下自己的瀏覽器,看一下有沒有什麼cookie被設定了。

看起來有三個cookie被設定了,但setTopMenu看了一下內容是空的,所以合理推測可能只有上面兩個有用途。

B. 打卡是打哪個API

接著來觀察打卡的API以及哪些cookie以及什麼data是需要被傳遞的。一樣以開發者模式觀察按下下班的打卡鍵後network中的狀況。

以POST打了兩個API: /users/clock_listing/users/att_status_listing 。其中看起來只有/users/clock_listing的payload帶有有用訊息:

另外測試了上班的打卡鍵,發現也是打/users/clock_listing但只有payload中data[ClockRecord][clock_type]的值會變換成S。而不管是上下班卡data[ClockRecord][user_id]data[AttRecord][user_id] 都是同樣的值,推測這代表著要打誰的卡,但一般來說自己的帳號只能打自己的卡,為了測試這個行為,在下一個section中送POST時有試圖改user_id,發現確實能打到別人的卡,因此這個server實作上只會確認登入者是合法的,但不會檢查打卡跟登入者是否為同一人。

以上的觀察都沒有在payload中找到任何關於時間的蹤跡,故推測打卡的時間紀錄就是戳到該API瞬間的時間為主。

C. 開始進行測試

把整個POST request以curl的格式複製出來:

雖然也可以直接用curl打,但我習慣把它轉成python requests的格式,可以使用這個網站來做:

直接用python執行這份script,發現打卡成功了!而下班時間也被更新到發出這個requests的時間點!而若把data[ClockRecord][clock_type]的值變換成S ,上班打卡原本已經打的時間並不會更新,因此推測:

  1. 上下班卡都可以在同一天打無數次
  2. 上班卡,會紀錄最早那次的打卡時間
  3. 下班卡,會紀錄最晚那次的打卡時間

所以若從早上9點到晚上7點,每個整點時間都同時打上下班卡,則最後的上班卡時間會是9點,下班卡時間是晚上7點。

D. 什麼cookie或是header是必要傳遞的

接著都以打下班卡的API來做實驗,因為只有下班卡才能確認API有打成功。首先,先確認什麼cookie是重要的,setTopMenu 必然沒用途直接忽略,剩下兩個cookie進行總共四種的所有組合的測試(全無、全有、有A無B、有B無A),發現lifeTim… 也是沒必要的,所以只有一個cookie是必要送出的,下一節再討論取得cookie的問題。

而header中也有許多冗余的項目,例如sec-ch-ua就不像是會需要的東西,經過一番嘗試,最後簡化的版本如GitHub repo中request裡送出的header所示

E. 如何取得cookie

在network中搜尋Set-Cookie,發現cookie在進入登入頁面時便會被設置,接著POST /domain/Account/login 登入時,這個cookie同時會被送出去,而這個cookie也會在接下來打卡時一直被使用。總而言之,cookie在GET /accounts/login即可拿到,且登入後也不會被換掉。

為了確保接下來實作正確性,特別POST/domain/Account/login 登入但不帶上cookie,發現server端也會再提供另一組cookie,不過基本上代表用一開始GET /accounts/login 拿到的cookie即可。

F. 如何得知user_id

總不可能在這個系統實作出來後,使用者必須用以上這些方法才能得知自己的user_id,所以就在登入後的頁面切到Elements中搜尋看看,果然找到了一些地方擁有這樣的資訊,例如這個name=data[EboardBrowser][user_id]的element中:

實作

A. 重整邏輯

  1. GET /accounts/login 頁面拿到cookie
  2. POST /Accounts/login 登入(payload置入帳號密碼,此時就要帶上cookie)
  3. 拿到上述的response後,找name=data[EboardBrowser][user_id] element拿到user_id
  4. POST /users/clock_listing (payload置入user_id與要打上班或下班卡的flag,此時也要帶上cookie)
  5. 似乎不需要logout,那就不做了

B. 以python實作

因為要處理cookie的問題,實作上以requests.Session搭配context manager會比較方便,請見repo。整體實作起來的感覺如下:

import requests

class Puncher:

def __init__(self, account: str, password: str, subdomain: str):
self.account = account
self.password = password
self.subdomain = subdomain

def _api(self, api_name: str) -> str:
return f"https://xxx_domain.com" + api_name

def __enter__(self):
self.session = requests.Session()
r = self.session.get(self._api("/accounts/login"))

headers = {
...
}

data = {
'data[Account][username]': self.account,
'data[Account][passwd]': self.password,
'data[remember]': '0',
}

r = self.session.post(
self._api("/Accounts/login"),
headers=headers,
data=data,
)
soup = BeautifulSoup(r.text, "html.parser")
self.user_id = soup.find(
"input",
{"name": "data[EboardBrowser][user_id]"},
).attrs["value"]

return self

def __exit__(self, type, value, traceback):
self.session.close()

def punch_in(self, user_id: t.Union[int, str]):
headers = {
...
}

data = {
'_method': 'POST',
'data[ClockRecord][user_id]': str(user_id),
'data[AttRecord][user_id]': str(user_id),
'data[ClockRecord][shift_id]': '10',
'data[ClockRecord][period]': '1',
'data[ClockRecord][clock_type]': 'S', # might use a flag to control
'data[ClockRecord][latitude]': '',
'data[ClockRecord][longitude]': '',
}

r = self.session.post(
self._api("/users/clock_listing"),
headers=headers,
data=data,
)

if r.status_code != HTTPStatus.OK:
raise requests.exceptions.HTTPError(f"status {r.status_code}")

C. 定時任務

有了上述的Puncher class,接著就是讓它能定時在每天上下班時間自動打卡了,這邊選擇使用python celery library提供的Periodic Tasks作法,請見repo

D. Dockerize

最後,如果能在隨手撿到的一台server都能部署這樣的服務幫忙自動打卡,那麼就真的方便極了!因此我把整個system寫成了docker-compose能一鍵部署的形式,另外celery需要用到的redis也能同時一併起起來!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Kaminyou
Kaminyou

Written by Kaminyou

Computer Vision Researcher

No responses yet

Write a response