背景
北京時間晚上十一點,突然電腦右下角的QQ彈出了一條訊息,"在?"
都9012年了還會有人單獨發個"在"然後人就失蹤了?有事情找就直接說事情嘛,你不說事情,我怎麼知道我應該"在"還是應該"不在"呢?
滑鼠移動到右下角準備點選"取消閃爍"時發現,是小美。
感覺空氣中突然瀰漫著一種說不明的東西,還是忍不住回覆了一句,"在,什麼事情?"
"你明天下午一點方便使用電腦嗎?"
唉,有什麼事情為什麼不可以一口氣說完呢,為什麼總要說半句呢,如果我在這個人面前我肯定一個大嘴巴子上去了。但,這是小美。"方便"。
"我明天選修課要搶課,你方便的話幫我一下唄,我和朋友約了出去玩了。"
果不其然,在我學會了如何修電腦、如何恢復資料、如何下載影片等技能後,又有新的技能樹即將需要被點亮。不過區區搶選修課能難到我?我不僅要搶到,還要搶得漂亮,搶得安逸,搶得讓自己都佩服!
"No problem!搶課地址,賬號,密碼,課程名。"
這乾脆利落的語氣,完美。
需求分析
搶課只是一件小事,無非是開啟瀏覽器,等時間到了瘋狂點選。但想必大多數人都在大學時候有過慘痛的經歷,無外乎學校網路太差,熱門課程搶的人數太多自己不一定能搶到。同樣,我也並不能保證自己在第二天下午一點的時候盯著螢幕不停地點,或者舒服一點用滑鼠連點器不停地點,就能很大可能地搶到需要的課程。
學校的網路沒法拯救,只能拯救自身的網路;搶同一門課程的人數多,我們就需要開更多的視窗去搶!
很好,看來我們需要搞定一段自動搶課的程式碼,然後在自己機器上部署、在舍友機器上部署、在雲環境部署、在網路好的其他機器部署,想必這總能最大可能性搶到想要的課程了吧。
我彷彿看到了一個幕後黑手看著自己的螢幕嘿嘿嘿地笑著:
王小明 世界電子競技 搶課中...
吳小杰 芭蕾舞藝術 搶課成功
陳小龍 基礎烹飪知識 搶課成功
劉小鄉 莎士比亞戲劇選 搶課中...
...
搶課,我從來沒怕過誰。
方案設計
此前在學習python爬蟲的時候接觸過selenium的知識,完全可以適配這樣瀏覽器操作場景。
可以先用selenium寫一個操作瀏覽器搶課的指令碼,再用flask來接收外部的請求命令執行對應的搶課指令碼,用docker打包成映象,再到能暫時操作的所有電腦,雲伺服器部署一套。
等快到搶課時間了,安安心心躺在椅子上,執行一下批次傳送請求的指令碼,就可以靜待搶課成功的好訊息了。
我只想發出三個字的聲音:還!有!誰!
selenium簡介
官網介紹
Selenium is a suite of tools to automate web browsers across many platforms.
runs in many browsers and operating systems
can be controlled by many programming languages and testing frameworks.
Selenium 官網:http://seleniumhq.org/
Selenium Github 主頁:https://github.com/SeleniumHQ/selenium
百度百科
Selenium是一個用於Web應用程式測試的工具。
Selenium測試直接執行在瀏覽器中,就像真正的使用者在操作一樣。
支援的瀏覽器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。
這個工具的主要功能包括:測試與瀏覽器的相容性——測試你的應用程式看是否能夠很好得工作在不同瀏覽器和作業系統之上。
測試系統功能——建立迴歸測試檢驗軟體功能和使用者需求。
支援自動錄製動作和自動生成 .Net、Java、Perl等不同語言的測試指令碼。
功能
框架底層使用JavaScript模擬真實使用者對瀏覽器進行操作。
測試指令碼執行時,瀏覽器自動按照指令碼程式碼做出點選,輸入,開啟,驗證等操作,就像真實使用者所做的一樣,從終端使用者的角度測試應用程式。
使瀏覽器相容性測試自動化成為可能,儘管在不同的瀏覽器上依然有細微的差別。
使用簡單,可使用Java,Python等多種語言編寫用例指令碼。
簡單來說,起初selenium是一套用於web自動化測試的工具,其本身具備了對多種瀏覽器/作業系統的相容支援,且能透過多種語言進行操作控制。但其強大的功能不僅僅適用於web自動化測試領域,同樣也被廣泛地使用在爬蟲以及rpa(Robotic Process Automation)相關的業務場景上。而我們現在需要實現的可以說就是一個簡單的rpa功能。
開發及測試環境介紹
筆者開發環境:Mac OS、PyCharm、python3.6、chrome
筆者提供的測試網址:www.uncleyiba.com
該網址可自行註冊,或者使用測試賬號nvshen/nvshen
因為是沿用的很久之前剛接觸tornado時候的程式碼,所以對tornado的使用有一些誤解。[1]
因此可能導致如果多人使用線上測試環境,會有一些問題出現。
所以建議可以本地啟一套測試環境。
測試網站及selenium指令碼原始碼:selenium_example(https://github.com/uncleYiba/selenium_example)
使用方式見readme.md
selenium使用瞭解
關注點
對於selenium而言,需要實現我們的需求,著重要關注兩點:
1.頁面元素定位 是否能準確獲取到我們需要進行操作的元素 2.頁面元素操作
點選、填入、清楚內容、獲取資料、雙擊、按住、鬆開、拖動等
開發者工具使用
既然是操作瀏覽器進行需求實現,那我們必然要對前端有一些瞭解。也就是說html和js相關知識需要有一些涉獵,另外需要會操作chrome的開發者工具,即Mac的option+command+i,或者是windows的F12。
首先點選紅色按鈕,再選取需要定位的具體元素資訊,即可獲取到該元素在html頁面中的所有資訊。
以登陸按鈕為例,我們發現其並沒有設定id屬性,但是其onclick觸發的方法直接表現了出來,我們便可以透過兩種方式觸發對應的登陸事件。可以根據class_name,tag_name等來獲取元素並執行點選事件,或者可以直接執行login()的js從而觸發登陸事件。
<button class="button" onclick="login()" style="margin-left:18%">登陸</button>
selenium元素定位
selenium提供了一系列的方法透過元素的屬性定位頁面中的具體元素,其屬性包括並不僅限於id、name、class_name、tag_name、xpath、css_elector等,透過WebDriver物件我們可以呼叫對應的find_element_by方法。例如以下html程式碼:
<input id="login" type="button">登陸</input>
我們獲取其元素的方法就是find_element_by_id,其他具體的可用方法列表如下:
如果方法名中只是"element"則獲取到的是單個元素物件,而如果是"elements"的話則獲取到的是該種類物件的一個list集合。
對於單個元素物件而言,其所有可執行方法或屬性如下:
從列表中我們可以清晰地看見單個元素物件也具有find_element_by的一系列方法,意味著我們可以定位一個父元素,再透過父元素定位其子元素,一層一層定位準確。
selenium元素操作
其中browser為瀏覽器物件
常見的html元素包括:輸入框input,按鈕button,核取方塊checkbox,單選框radio,下拉選擇框select,時間選擇框date,富文字框textarea,檔案選擇file以及一些用於顯示文字的標籤包括不僅限於div、span、p等。
輸入框input:
<input type="text" id="last_name" name="last_name">
需要實現對其輸入的功能,可使用如下程式碼:
browser.find_element_by_name("last_name").send_keys("測試文字")
按鈕button:
<button id="submit" onclick="alertDiv('提交成功!')">提交</button>
需要實現對其進行點選的功能,可使用如下程式碼:
browser.find_element_by_id("submit")[1].click()
核取方塊checkbox:
<input type="checkbox" name="q1" value="0">一
<input type="checkbox" name="q2" value="1">二
<input type="checkbox" name="q3" value="2">三
<input type="checkbox" name="q4" value="3">四
需要實現對其value=0和value=1核取方塊的選中功能,可使用如下程式碼:
browser.find_element_by_name("q1").click()
browser.find_element_by_name("q2").click()
單選框radio:
<input type="radio" name="team" value="0">是 <input type="radio" name="team" value="1">否
需要實現對其value=0,顯示為"是"的單選框的選擇功能,可使用如下程式碼:
browser.find_elements_by_name("team")[0].click()
下拉選擇框select:
<select name="gender">
<option value="0">男</option>
<option value="1">女</option>
<option value="2">其他</option>
</select>
需要實現對其下拉選項"男"的選擇功能,可使用如下程式碼:
browser.find_element_by_name("gender").find_elements_by_tag_name("option")[0].click()
時間選擇框date:
<input type="date" name="birthday">
需要實現對其時間的輸入,可使用如下程式碼:
js_input = '''$("input[name={0}]").val("{1}")'''.format("birthday", "2000-01-01") browser.execute_script(js_input)
PS:鑑於各種日期控制元件比較多,個人使用看來直接使用js對其賦值是一種比較方便的方式
富文字框textarea:
<textarea name="textarea"></textarea>
需要實現對其輸入的功能,可使用如下程式碼:
browser.find_element_by_name("textarea").send_keys("測試文字")
檔案選擇file:
<input type="file" name="file">
需要實現檔案的選擇功能,可使用如下程式碼:
browser.find_element_by_name("file").send_keys(file_path)
至於其他一些用於顯示文字的標籤,例如:
<span id="name">嬴政</span>
需要實現對其文字內容的獲取,可使用如下程式碼:
text = browser.find_element_by_id("name").text
至於元素的點住,鬆開,拖動等操作將結合在實際案例的程式碼中。
搶課測試流程及對應程式碼分析
測試網站可見上面開發及測試環境介紹章節介紹
1.開啟對應的測試網站www.uncleyiba.com
option = webdriver.ChromeOptions()
option.add_argument('disable-infobars')
browser = webdriver.Chrome(chrome_options=option)
browser.set_window_size(1500, 1000)
browser.get("http://www.uncleyiba.com")
其中第二行程式碼加或者不加的區別僅僅是開啟的瀏覽器會不會顯示如圖所示的資訊:
透過這段程式碼我們操作成功開啟一個1500*1000大小的chrome瀏覽器並開啟了http://www.uncleyiba.com這個網站
2.輸入使用者名稱和密碼,並點選登陸按鈕
# 填入使用者名稱和密碼
utils.write_into_input_by_id(browser, "username", "nvshen")
utils.write_into_input_by_id(browser, "password", "nvshen")
# 執行登陸js
js_login = "login()"
browser.execute_script(js_login)
這裡我們輸入了對應的使用者名稱和密碼,並且透過執行登陸按鈕的js程式碼實現登入,而並不是透過點選登陸按鈕的方式
其中write_into_input_by_id方法具體程式碼如下:
def write_into_input_by_id(browser, ele_id, content):
ele = browser.find_element_by_id(ele_id)
ele.send_keys(content)
找到對應的id的input元素並向其中輸入指定的文字
3.等待三秒鐘網頁跳轉,並確保成功跳轉
# 首先我們要根據彈窗結果讀取狀態,如果是我們想要的狀態則繼續下一步操作,否則報錯
login_message_show_return_param = dict()
if not utils.while_else_sleep(page_check.login_message_show, {"browser": browser},
login_message_show_return_param):
raise Exception(login_message_show_return_param["message"])
time.sleep(3)
# 三秒之後會重新整理頁面,要確保頁面是否已經重新整理成功,否則報錯
login_success_return_param = dict()
if not utils.while_else_sleep(page_check.login_success, {"browser": browser},login_success_return_param):
raise Exception(login_message_show_return_param["message"])
在執行頁面跳轉的時候總會因為網路或其他原因,導致其中的延時時間具有不確定性, 可能你一個請求發過去是毫秒級響應,也有可能過十幾二十秒都沒有反應, 這時候我們需要一個延時等待的機制來儘可能規避這種情況, 這裡是透過寫了一個簡單的函式while_else_sleep來監控頁面的狀態, 從而實現對頁面的載入等待。
def while_else_sleep(func, param, return_param, false_func=nothing_happened_func, times_begin=0, times_max=20, sleep_time=1): ''' :param func:重複執行的方法 :param param:func方法攜帶的引數 :param return_param:func方法返回的引數 :param false_func:如果func返回結果為false,需要執行的函式 :param times_begin:開始的次數 :param times_max:最大重複執行次數 :param sleep_time:每次等待時間 :return:boolean ''' times = times_begin while True: #print("{0}:{1}[max:{2}]".format(func, times, times_max)) times += 1 if func(param, return_param): break else: time.sleep(sleep_time) false_func(param, return_param) if times >= times_max: return False if return_param.get("status", "") == "over": return False return True
我們透過判定函式func來判定頁面是否已經到達了我們預料的狀態。如果沒有,則進行對應的延時等待,並進行重試補救操作false_func。當重試超過一定的次數times_max,則判定此次操作是超時的,沒有繼續重試的必要了。當然在確定頁面狀態的過程中可能會出現一些不再需要重試的情況,比如"密碼輸入錯誤", 這種情況你再怎麼重試密碼也依舊是錯誤的,所以我們會在return_param中加入status判定, 如果碰到類似於這種情況則直接執行跳出操作,並判定執行失敗。
理解了while_else_sleep函式再來看這個部分的整體函式,就很容易理解其含義:
確保彈窗是登陸成功狀態,如果是則等待三秒,確認已經登陸成功並跳轉到了預期的頁面。
4.點選"定時搶課頁面"
# 找到對應的 定時搶課頁面 的a標籤
select_class_a = browser.find_elements_by_tag_name("li")[0].find_element_by_tag_name("a")
select_class_a.click()
# 判定子頁面是否重新整理成功
select_class_page_show_return_param = dict()
if not utils.while_else_sleep(page_check.select_class_page_show, {"browser": browser},
select_class_page_show_return_param):
raise Exception(select_class_page_show_return_param["message"])
這裡觸發了一個點選a標籤的事件,實現iframe的跳轉,並且透過一個while_else_sleep函式對iframe的頁面跳轉狀態進行了判定
5.切換到對應的iframe進行搶課
# 切換iframe
browser.switch_to.frame(browser.find_element_by_id("child_frame"))
這裡僅僅是一個切換frame的操作,除了同標籤頁面內的frame切換,selenium還可以在同一個瀏覽器視窗的不同標籤之間進行切換
6.根據當前時間設定課程開搶時間,並將這個時間傳給對應的時間控制元件
# 設定好開搶時間 如果秒數小於40 那就當前時間,如果秒數大於40,則推遲一分鐘
second = int(time.strftime('%S', time.localtime(time.time())))
time_str = time.strftime('%Y-%m-%dT%H:%M', time.localtime(time.time() + 60))
if second >= 40:
time_str = time.strftime('%Y-%m-%dT%H:%M', time.localtime(time.time() + 120))
# 填入日期
utils.write_into_date_by_id(browser, "date", time_str)
這裡我們首先獲取了當前時間,並且在當前時間的基礎上對課程開搶時間進行了相應的延長, 這裡有一個注意點是我們設定的時間傳參格式是%Y-%m-%dT%H:%M
,原因如圖:
我們在獲取date控制元件的樣例時間格式的時候獲取到的就是這樣的格式, 因此我們再反過來進行賦值的時候要以同樣的格式構造對應的時間值。不以特定的格式進行賦值則會引發js的錯誤。
7.點選"生成搶課列表"
# 點選 生成搶課列表
js_create_class_list = "create_class_list()"
browser.execute_script(js_create_class_list)
# 判定一下是否出現彈窗表示時間選擇有誤
time_error_div_show_return_param = dict()
if utils.while_else_sleep(page_check.time_error_div_show, {"browser": browser},
time_error_div_show_return_param, times_max=4):
raise Exception(time_error_div_show_return_param["message"])
# 判定table是否已經展示出來
class_table_show_return_param = dict()
if not utils.while_else_sleep(page_check.class_table_show, {"browser": browser}, class_table_show_return_param):
raise Exception(class_table_show_return_param["message"])
這裡的點選操作我依舊是採用的執行js的方式,我們可以檢視對應的button的程式碼:
當然可以透過xpath或tag_name的方式獲取元素再進行點選,這個因個人的習慣不同選擇的方式也不一樣。
在進行js執行/按鈕點選之後,我增加了一步判定,以免上一步時間設定出錯導致搶課列表不能成功生成。
而在確定時間沒有出錯之後生成搶課列表也是需要一定時間的(當然我這裡是js直接生成的寫死的列表, 實際必然是實時向伺服器傳送請求獲取到對應的列表,所以會有一定的請求時間),所以我們進行了延時等待的判定。
8.不停地點選某個課程的"搶課"按鈕,直到搶課成功
# 選擇我們需要的課程 例如 計算機課
trs = browser.find_element_by_id("class_list").find_element_by_tag_name("tbody").find_elements_by_tag_name("tr")
find_flag = False
for each_tr in trs:
tds = each_tr.find_elements_by_tag_name("td")
if tds[1].text == "計算機課":
# time.sleep(1)
left_time_str = tds[3].text
pat_num = "\d+"
result_pat = re.findall(pat_num, left_time_str)
if len(result_pat) > 0:
left_time = int(result_pat[0])
click_button_param = {"browser": browser, "button": tds[2].find_element_by_tag_name("button")}
click_button_return_param = dict()
if not utils.while_else_sleep(page_check.click_button, click_button_param,
click_button_return_param, times_max=(left_time+3)*10,
sleep_time=0.1):
raise Exception(click_button_return_param["message"])
# 判定搶課是否成功(有無彈窗,彈窗內容)
select_fail_message_show_return_param = dict()
if not utils.while_else_sleep(page_check.select_fail_div_show, {"browser": browser},
select_fail_message_show_return_param, times_max=4):
raise Exception(browser.find_element_by_id("class_list").find_element_by_tag_name("tbody").
find_elements_by_tag_name("tr")[0].find_elements_by_tag_name("td")[3].text)
else:
raise Exception(tds[3].text)
find_flag = True
break
這裡我們獲取了所有課程的資訊,並透過對td內容的判定找到了我們需要的計算機課,進一步找到其搶課按鈕。
透過獲取了剩餘時間,計算出假設我們每秒點選十次的話需要點選多少次 (超時過多之後的點選可以有但是沒必要,反正也搶不到了)
全部次數試完之後再判定一下是否搶課成功,即識別是否有彈窗以及彈窗的內容是什麼
表單提交樣例
除了搶課案例外筆者還提供了另一個可供測試的表單提交樣例, 其中包括了更多的元素操作:input填寫,select選擇,date填寫,radio選擇,checkout選擇,file選取, textarea填寫,元素拖拽,按鈕點選這些事件。
表單提交頁面的進入和選課類似,這裡不做重複介紹。大多數元素的操作在之前的章節中也有介紹,方法大同小異參考一下原始碼即可理解。
這裡重點提出來的則是元素拖拽的演示。
在表單提交這個案例中筆者增加了一個類似於驗證的機制,需要將黑色小方塊移動至幾乎與紅色小方塊重合的地步,才可以進行最終的提交。
其中小方塊這一段的html程式碼是這樣的:
<td colspan="2" id="td1">
<div id="div2" style="left: 44.6836px; top: 24.9741px;"></div>
<div id="div1" style="left: 342px; top: 593px;"></div>
</td>
css:
#div1{
width: 30px;
height: 30px;
background-color: black;
position: absolute;
}
#div2{
width: 30px;
height: 30px;
background-color: red;
position: relative;
}
兩個小方塊都是30*30大小,其中紅色方塊是可不操作的,其位置在這個td內部隨機。黑色方塊有初始位置,可以進行拖拽移動。
其實現程式碼如下:
# 拖動驗證
# 1.分別得到兩個div的left和top
div1 = browser.find_element_by_id("div1")
div2 = browser.find_element_by_id("div2")
left_div1 = div1.location.get("x")
top_div1 = div1.location.get("y")
left_div2 = div2.location.get("x")
top_div2 = div2.location.get("y")
# 2.設定好ActionChains物件用於進行鍵鼠操作
actions = ActionChains(browser)
actions.click_and_hold(div1) # a.按住div1
actions.move_by_offset(left_div2 - left_div1, top_div2 - top_div1) # b.橫縱座標移動(相對座標)
actions.release() # c.釋放滑鼠
actions.perform() # d.執行動作流
ActionChains類可以實現對一組"動作"的執行,它有如下的"動作"可以被執行:
包括不僅限於單機,雙擊,按下,鬆開,移動等。
這裡我們透過計算了兩個方塊的相對位置,點選並按住小黑方塊(div1), 並將其移動相應的相對距離,再釋放滑鼠這樣的操作,來實現"讓小黑方塊覆蓋小紅方塊"的驗證操作。
其他可能進行的操作
其中browser為瀏覽器物件
頁面截圖
在伺服器執行selenium指令碼的時候我們無法直觀地看到當時瀏覽器執行的情況, 因此需要對程式碼執行異常的地方進行捕獲,透過截圖的方式來人為分析可能出現的錯誤。
browser.get_screenshot_as_file(screenshot_path)
screenshot_path:截圖圖片儲存路勁
元素截圖
#得到驗證碼在螢幕中的座標位置
left, top, right, bottom = get_elementid_location(browser, "checkCodeImage")
# 瀏覽器頁面截圖並儲存
screenshot_path = os.path.join(conf.data_path, picuniqid + "_screenshot" + ".png")
browser.get_screenshot_as_file(screenshot_path)
# 儲存驗證碼圖
captcha_path = os.path.join(conf.data_path, picuniqid + "_captcha" + ".png")
im = Image.open(screenshot_path)
im = im.crop((left, top, right, bottom))
im.save(captcha_path)
其中get_elementid_location方法是根據元素的location和size方法獲取其在螢幕中的座標
這其中有一個值得關注的問題是retina螢幕的問題
所謂“Retina”是一種顯示標準,是把更多的畫素點壓縮至一塊螢幕裡,從而達到更高的解析度並提高螢幕顯示的細膩程度。
由摩托羅拉公司研發。最初該技術是用於Moto Aura上。這種解析度在正常觀看距離下足以使人肉眼無法分辨其中的單獨畫素。也被稱為視網膜螢幕。
以MacBook Pro with Retina Display為例,工作時顯示卡渲染出2880x1800個畫素,其中每四個畫素一組,輸出原來螢幕的一個畫素顯示的大小區域內的影像。
這樣一來,使用者所看到的圖示與文字的大小與原來的1440x900解析度螢幕相同,但精細度是原來的4倍,但對於特殊元素,如影片與影像,則以一個圖片畫素對應一個螢幕畫素的方式顯示。
故不會產生Windows中解析度提升使螢幕文字與影像變小,造成閱讀困難的問題。這樣在設計軟體時只需將所有的UI元素的精細度都提高到原來的4倍就可以既保持了觀看舒適度,又提高了顯示效果。關於iOS裝置,也由四個畫素代替原來一個畫素,透過下圖對比就可以較明顯地觀察到這種關係。
劃重點每四個畫素一組
所以如果selenium是在mac的主屏或者說是其他retina螢幕上工作的時候我們需要將其獲得的元素座標乘上2才是其真實的座標:
if is_retina_display(browser):
left = int(captcha_location['x'] )*2
top = int(captcha_location['y'] )*2
right = int(captcha_location['x'] +captcha_size['width'] )*2
bottom = int(captcha_location['y'] + captcha_size['height'] )*2
else:
left = int(captcha_location['x'])
top = int(captcha_location['y'])
right = int(captcha_location['x'] + captcha_size['width'])
bottom = int(captcha_location['y'] + captcha_size['height'])
得到了螢幕截圖和元素位置,透過Image類的操作即可以準確獲得想要截圖的元素的位置
螢幕滾動
這個也是配合截圖使用的,因為截圖僅僅是擷取當前的螢幕,如果頁面可以向下滾動或者向上滾動,則被隱藏的部分無法被截圖獲得
js_scroll='''$(document).scrollTop({0})'''.format(scroll_num)
browser.execute_script(js_scroll)
其中scroll_num
即捲軸的位置,0則代表是在最上方
屬性方法介紹
1.獲取元素是否顯示、是否可被操作、是否可進行選擇
2.獲取元素的標籤屬性,例如獲取其name屬性
name = ele.get_attribute("name")
ele為元素物件
3.獲取元素的css屬性,例如獲取其color屬性
color = ele.value_of_css_property("color") ele為元素物件
4.清除元素的值
ele.clear()
ele為元素物件
背景後續
北京時間凌晨五點,我完成了對docker映象的最終測試,並臨時借用了朋友的高寬頻伺服器部署了自己的服務。
北京時間下午一點零一分,成功搶到了課程。
就像花瓶碎了一樣,感覺一切頓時有點索然無味。
當然,以上都是假的。
但那些真的,也不知不覺就這樣過去了很多年,卻又像就發生在昨天。
而今天,我在達觀資料,它就像初生的太陽,你又在哪裡?
關於作者
景健:達觀資料後端開發工程師,負責達觀資料產品後端開發、產品落地、客戶定製化產品需求等設計。愛好數學,喜歡用程式碼解決生活中的實際問題。