背景
北京時間晚上十一點,突然電腦右下角的QQ彈出了一條消息,"在?"
都9012年了還會有人單獨發個"在"然後人就失蹤了?有事情找就直接說事情嘛,你不說事情,我怎麼知道我應該"在"還是應該"不在"呢?
鼠標移動到右下角準備點擊"取消閃爍"時發現,是小美。
感覺空氣中突然瀰漫着一種說不明的東西,還是忍不住回覆了一句,"在,什麼事情?"
"你明天下午一點方便使用電腦嗎?"
唉,有什麼事情爲什麼不可以一口氣說完呢,爲什麼總要說半句呢,如果我在這個人面前我肯定一個大嘴巴子上去了。但,這是小美。"方便"。
"我明天選修課要搶課,你方便的話幫我一下唄,我和朋友約了出去玩了。"
果不其然,在我學會了如何修電腦、如何恢復數據、如何下載視頻等技能後,又有新的技能樹即將需要被點亮。不過區區搶選修課能難到我?我不僅要搶到,還要搶得漂亮,搶得安逸,搶得讓自己都佩服!
"No problem!搶課地址,賬號,密碼,課程名。"
這乾脆利落的語氣,完美。
需求分析
搶課只是一件小事,無非是打開瀏覽器,等時間到了瘋狂點擊。但想必大多數人都在大學時候有過慘痛的經歷,無外乎學校網絡太差,熱門課程搶的人數太多自己不一定能搶到。同樣,我也並不能保證自己在第二天下午一點的時候盯着屏幕不停地點,或者舒服一點用鼠標連點器不停地點,就能很大可能地搶到需要的課程。
學校的網絡沒法拯救,只能拯救自身的網絡;搶同一門課程的人數多,我們就需要開更多的窗口去搶!
很好,看來我們需要搞定一段自動搶課的代碼,然後在自己機器上部署、在舍友機器上部署、在雲環境部署、在網絡好的其他機器部署,想必這總能最大可能性搶到想要的課程了吧。
我彷彿看到了一個幕後黑手看着自己的屏幕嘿嘿嘿地笑着:
王小明 世界電子競技 搶課中...
吳小杰 芭蕾舞藝術 搶課成功
陳小龍 基礎烹飪知識 搶課成功
劉小鄉 莎士比亞戲劇選 搶課中...
...
搶課,我從來沒怕過誰。
方案設計
本文只着重介紹selenium相關使用,不會對整個方案進行完整的實現
此前在學習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" οnclick="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:截圖圖片存儲路勁
元素截圖
這一步實際上是基於上一個"頁面截圖"進一步對圖片進行處理,這主要用於驗證碼的獲取, 大多數網站的驗證碼並不是一個真實存在的圖片文件,而是一個實時生成的臨時圖片文件。因此我們需要通過截圖的方式來獲取這樣的驗證碼並且利用OCR進行進一步的識別。
#得到驗證碼在屏幕中的座標位置
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鏡像的最終測試,並臨時借用了朋友的高寬帶服務器部署了自己的服務。
北京時間下午一點零一分,成功搶到了課程。
就像花瓶碎了一樣,感覺一切頓時有點索然無味。
當然,以上都是假的。
但那些真的,也不知不覺就這樣過去了很多年,卻又像就發生在昨天。
而今天,我在達觀數據,它就像初生的太陽,你又在哪裏?
關於作者
景健:達觀數據後端開發工程師,負責達觀數據產品後端開發、產品落地、客戶定製化產品需求等設計。愛好數學,喜歡用代碼解決生活中的實際問題。