NodeJS爬蟲——立馬理財

来源:https://www.cnblogs.com/tywei90/archive/2018/02/21/8456554.html
-Advertisement-
Play Games

其實在早之前,就做過 "立馬理財" 的銷售額統計,只不過是用前端js寫的,需要在首頁的console調試面板里粘貼一段代碼執行, "點擊這裡" 。主要是通過定時爬取 " " 非同步介面來獲取數據。然後通過一定的排重演算法來獲取最終的數據。但是這樣做有以下缺點: 0. 代碼只能在瀏覽器視窗下運行,關閉瀏覽 ...


其實在早之前,就做過立馬理財的銷售額統計,只不過是用前端js寫的,需要在首頁的console調試面板里粘貼一段代碼執行,點擊這裡。主要是通過定時爬取https://www.lmlc.com/s/web/home/user_buying非同步介面來獲取數據。然後通過一定的排重演算法來獲取最終的數據。但是這樣做有以下缺點:

  1. 代碼只能在瀏覽器視窗下運行,關閉瀏覽器或者電腦就失效了
  2. 只能爬取一個頁面的數據,不能整合其他頁面的數據
  3. 爬取的數據無法存儲到本地
  4. 上面的非同步介面數據會部分過濾,導致我們的排重演算法失效

由於最近學習了node爬蟲相關知識,我們可以在後臺自己模擬請求,爬取頁面數據。並且我開通了阿裡雲伺服器,可以把代碼放到雲端跑。這樣,1、2、3都可以解決。4是因為之前不知道這個ajax介面是每三分鐘更新一次,這樣我們可以根據這個來排重,確保數據不會重覆。說到爬蟲,大家想到的比較多的還是python,確實python有Scrapy等成熟的框架,可以實現很強大的爬取功能。但是node也有自身的優點,憑藉強大的非同步特性,可以很輕鬆的實現高效的非同步併發請求,節省cpu的開銷。其實node爬蟲還是比較簡單的,下麵我們就來分析整個爬蟲爬取的流程和最終如何展示數據的。

線上地址

一、爬蟲流程

我們最終的目標是實現爬取立馬理財每日的銷售額,並知道賣了哪些產品,每個產品又被哪些用戶在什麼時間點買的。首先,介紹下爬蟲爬取的主要步驟:

1. 結構分析

我們要爬取頁面的數據,第一步當然是要先分析清楚頁面結構,要爬哪些頁面,頁面的結構是怎樣的,需不需要登錄;有沒有ajax介面,返回什麼樣的數據等。

2. 數據抓取

分析清楚要爬取哪些頁面和ajax,就要去抓取數據了。如今的網頁的數據,大體分為同步頁面和ajax介面。同步頁面數據的抓取就需要我們先分析網頁的結構,python抓取數據一般是通過正則表達式匹配來獲取需要的數據;node有一個cheerio的工具,可以將獲取的頁面內容轉換成jquery對象,然後就可以用jquery強大的dom API來獲取節點相關數據, 其實大家看源碼,這些API本質也就是正則匹配。ajax介面數據一般都是json格式的,處理起來還是比較簡單的。

3. 數據存儲

抓取的數據後,會做簡單的篩選,然後將需要的數據先保存起來,以便後續的分析處理。當然我們可以用MySQL和Mongodb等資料庫存儲數據。這裡,我們為了方便,直接採用文件存儲。

4. 數據分析

因為我們最終是要展示數據的,所以我們要將原始的數據按照一定維度去處理分析,然後返回給客戶端。這個過程可以在存儲的時候去處理,也可以在展示的時候,前端發送請求,後臺取出存儲的數據再處理。這個看我們要怎麼展示數據了。

5. 結果展示

做了這麼多工作,一點展示輸出都沒有,怎麼甘心呢?這又回到了我們的老本行,前端展示頁面大家應該都很熟悉了。將數據展示出來才更直觀,方便我們分析統計。

二、爬蟲常用庫介紹

1. Superagent

Superagent是個輕量的的http方面的庫,是nodejs里一個非常方便的客戶端請求代理模塊,當我們需要進行get、post、head等網路請求時,嘗試下它吧。

2. Cheerio

Cheerio大家可以理解成一個 Node.js 版的 jquery,用來從網頁中以 css selector 取數據,使用方式跟 jquery 一模一樣。

3. Async

Async是一個流程式控制制工具包,提供了直接而強大的非同步功能mapLimit(arr, limit, iterator, callback),我們主要用到這個方法,大家可以去看看官網的API。

4. arr-del

arr-del是我自己寫的一個刪除數組元素方法的工具。可以通過傳入待刪除數組元素index組成的數組進行一次性刪除。

5. arr-sort

arr-sort是我自己寫的一個數組排序方法的工具。可以根據一個或者多個屬性進行排序,支持嵌套的屬性。而且可以再每個條件中指定排序的方向,並支持傳入比較函數。

三、頁面結構分析

先屢一下我們爬取的思路。立馬理財線上的產品主要是定期和立馬金庫(最新上線的光大銀行理財產品因為手續比較麻煩,而且起投金額高,基本沒人買,這裡不統計)。定期我們可以爬取理財頁的ajax介面:https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=0。(update: 定期近期沒貨,可能看不到數據)數據如下圖所示:

理財頁ajax介面數據

這裡包含了所有線上正在銷售的定期產品,ajax數據只有產品本身相關的信息,比如產品id、籌集金額、當前銷售額、年化收益率、投資天數等,並沒有產品被哪些用戶購買的信息。所以我們需要帶著id參數去它的產品詳情頁爬取,比如立馬聚財-12月期HLB01239511。詳情頁有一欄投資記錄,裡邊包含了我們需要的信息,如下圖所示:

詳情頁投資記錄

但是,詳情頁需要我們在登錄的狀態下才可以查看,這就需要我們帶著cookie去訪問,而且cookie是有有效期限制的,如何保持我們cookie一直在登錄態呢?請看後文。

其實立馬金庫也有類似的ajax介面:https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=1,但是裡邊的相關數據都是寫死的,沒有意義。而且金庫的詳情頁也沒有投資記錄信息。這就需要我們爬取一開始說的首頁的ajax介面:https://www.lmlc.com/s/web/home/user_buying。但是後來才發現這個介面是三分鐘更新一次,就是說後臺每隔三分鐘向伺服器請求一次數據。而一次是10條數據,所以如果在三分鐘內,購買產品的記錄數超過10條,數據就會有遺漏。這是沒有辦法的,所以立馬金庫的統計數據會比真實的偏少。

四、爬蟲代碼分析

1. 獲取登錄cookie

因為產品詳情頁需要登錄,所以我們要先拿到登錄的cookie才行。getCookie方法如下:

function getCookie() {
    superagent.post('https://www.lmlc.com/user/s/web/logon')
        .type('form')
        .send({
            phone: phone,
            password: password,
            productCode: "LMLC",
            origin: "PC"
        })
        .end(function(err, res) {
            if (err) {
                handleErr(err.message);
                return;
            }
            cookie = res.header['set-cookie']; //從response中得到cookie
            emitter.emit("setCookeie");
        })
}

phone和password參數是從命令行里傳進來的,就是立馬理財用手機號登錄的賬號和密碼。我們用superagent去模擬請求立馬理財登錄介面:https://www.lmlc.com/user/s/web/logon。傳入相應的參數,在回調中,我們拿到header的set-cookie信息,併發出一個setCookeie事件。因為我們設置了監聽事件:emitter.on("setCookie", requestData),所以一旦獲取cookie,我們就會去執行requestData方法。

2. 理財頁ajax的爬取

requestData方法的代碼如下:

function requestData() {
    superagent.get('https://www.lmlc.com/web/product/product_list?pageSize=100&pageNo=1&type=0')
    .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        // 在這裡清空數據,避免一個文件被同時寫入
        if(clearProd){
            fs.writeFileSync('data/prod.json', JSON.stringify([]));
            clearProd = false;
        }
        let addData = JSON.parse(pres.text).data;
        let formatedAddData = formatData(addData.result);
        let pageUrls = [];
        if(addData.totalPage > 1){
            handleErr('產品個數超過100個!');
            return;
        }
        for(let i=0,len=addData.result.length; i<len; i++){
            if(+new Date() < addData.result[i].buyStartTime){
                if(preIds.indexOf(addData.result[i].id) == -1){
                    preIds.push(addData.result[i].id);
                    setPreId(addData.result[i].buyStartTime, addData.result[i].id);
                }
            }else{
                pageUrls.push('https://www.lmlc.com/web/product/product_detail.html?id=' + addData.result[i].id);
            }
        }
        function setPreId(time, id){
            cache[id] = setInterval(function(){
                if(time - (+new Date()) < 1000){
                    // 預售產品開始搶購,直接修改爬取頻次為1s,防止丟失數據
                    clearInterval(cache[id]);
                    clearInterval(timer);
                    delay = 1000;
                    timer = setInterval(function(){
                        requestData();
                    }, delay);
                    // 同時刪除id記錄
                    let index = preIds.indexOf(id);
                    sort.delArrByIndex(preIds, [index]);
                }
            }, 1000)
        }
        // 處理售賣金額信息
        let oldData = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
        for(let i=0, len=formatedAddData.length; i<len; i++){
            let isNewProduct = true;
            for(let j=0, len2=oldData.length; j<len2; j++){
                if(formatedAddData[i].productId === oldData[j].productId){
                    isNewProduct = false;
                }
            }
            if(isNewProduct){
                oldData.push(formatedAddData[i]);
            }
        }
        fs.writeFileSync('data/prod.json', JSON.stringify(oldData));
        let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
        console.log((`理財列表ajax介面爬取完畢,時間:${time}`).warn);
        if(!pageUrls.length){
            delay = 32*1000;
            clearInterval(timer);
            timer = setInterval(function(){
                requestData();
            }, delay);
            return
        }
        getDetailData();
    });
}

代碼很長,getDetailData函數代碼後面分析。

請求的ajax介面是個分頁介面,因為一般在售的總產品數不會超過10條,我們這裡設置參數pageSize為100,這樣就可以一次性獲取所有產品。

clearProd是全局reset信號,每天0點整的時候,會清空prod(定期產品)和user(首頁用戶)數據。

因為有時候產品較少會採用搶購的方式,比如每天10點,這樣在每天10點的時候數據會更新很快,我們必須要增加爬取的頻次,以防丟失數據。所以針對預售產品即buyStartTime大於當前時間,我們要記錄下,並設定計時器,當開售時,調整爬取頻次為1次/秒,見setPreId方法。

如果沒有正在售賣的產品,即pageUrls為空,我們將爬取的頻次設置為最大32s。

requestData函數的這部分代碼主要記錄下是否有新產品,如果有的話,新建一個對象,記錄產品信息,push到prod數組裡。prod.json數據結構如下:

[{
  "productName": "立馬聚財-12月期HLB01230901",
  "financeTotalAmount": 1000000,
  "productId": "201801151830PD84123120",
  "yearReturnRate": 6.4,
  "investementDays": 364,
  "interestStartTime": "2018年01月23日",
  "interestEndTime": "2019年01月22日",
  "getDataTime": 1516118401299,
  "alreadyBuyAmount": 875000,
  "records": [
  {
    "username": "劉**",
    "buyTime": 1516117093472,
    "buyAmount": 30000,
    "uniqueId": "劉**151611709347230,000元"
  },
  {
    "username": "劉**",
    "buyTime": 1516116780799,
    "buyAmount": 50000,
    "uniqueId": "劉**151611678079950,000元"
  }]
}]

是一個對象數組,每個對象表示一個新產品,records屬性記錄著售賣信息。

3. 產品詳情頁的爬取

我們再看下getDetailData的代碼:

function getDetailData(){
    // 請求用戶信息介面,來判斷登錄是否還有效,在產品詳情頁判斷麻煩還要造成五次登錄請求
    superagent
        .post('https://www.lmlc.com/s/web/m/user_info')
        .set('Cookie', cookie)
        .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        let retcode = JSON.parse(pres.text).retcode;
        if(retcode === 410){
            handleErr('登陸cookie已失效,嘗試重新登陸...');
            getCookie();
            return;
        }
        var reptileLink = function(url,callback){
            // 如果爬取頁面有限制爬取次數,這裡可設置延遲
            console.log( '正在爬取產品詳情頁面:' + url);
            superagent
                .get(url)
                .set('Cookie', cookie)
                .end(function(err,pres){
                    // 常規的錯誤處理
                    if (err) {
                        handleErr(err.message);
                        return;
                    }
                    var $ = cheerio.load(pres.text);
                    var records = [];
                    var $table = $('.buy-records table');
                    if(!$table.length){
                        $table = $('.tabcontent table');
                    }
                    var $tr = $table.find('tr').slice(1);
                    $tr.each(function(){
                        records.push({
                            username: $('td', $(this)).eq(0).text(),
                            buyTime: parseInt($('td', $(this)).eq(1).attr('data-time').replace(/,/g, '')),
                            buyAmount: parseFloat($('td', $(this)).eq(2).text().replace(/,/g, '')),
                            uniqueId: $('td', $(this)).eq(0).text() + $('td', $(this)).eq(1).attr('data-time').replace(/,/g, '') + $('td', $(this)).eq(2).text()
                        })
                    });
                    callback(null, {
                        productId: url.split('?id=')[1],
                        records: records
                    });
                });
        };
        async.mapLimit(pageUrls, 10 ,function (url, callback) {
          reptileLink(url, callback);
        }, function (err,result) {
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log(`所有產品詳情頁爬取完畢,時間:${time}`.info);
            let oldRecord = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
            let counts = [];
            for(let i=0,len=result.length; i<len; i++){
                for(let j=0,len2=oldRecord.length; j<len2; j++){
                    if(result[i].productId === oldRecord[j].productId){
                        let count = 0;
                        let newRecords = [];
                        for(let k=0,len3=result[i].records.length; k<len3; k++){
                            let isNewRec = true;
                            for(let m=0,len4=oldRecord[j].records.length; m<len4; m++){
                                if(result[i].records[k].uniqueId === oldRecord[j].records[m].uniqueId){
                                    isNewRec = false;
                                }
                            }
                            if(isNewRec){
                                count++;
                                newRecords.push(result[i].records[k]);
                            }
                        }
                        oldRecord[j].records = oldRecord[j].records.concat(newRecords);
                        counts.push(count);
                    }
                }
            }
            let oldDelay = delay;
            delay = getNewDelay(delay, counts);
            function getNewDelay(delay, counts){
                let nowDate = (new Date()).toLocaleDateString();
                let time1 = Date.parse(nowDate + ' 00:00:00');
                let time2 = +new Date();
                // 根據這次更新情況,來動態設置爬取頻次
                let maxNum = Math.max(...counts);
                if(maxNum >=0 && maxNum <= 2){
                    delay = delay + 1000;
                }
                if(maxNum >=8 && maxNum <= 10){
                    delay = delay/2;
                }
                // 每天0點,prod數據清空,排除這個情況
                if(maxNum == 10 && (time2 - time1 >= 60*1000)){
                    handleErr('部分數據可能丟失!');
                }
                if(delay <= 1000){
                    delay = 1000;
                }
                if(delay >= 32*1000){
                    delay = 32*1000;
                }
                return delay
            }
            if(oldDelay != delay){
                clearInterval(timer);
                timer = setInterval(function(){
                    requestData();
                }, delay);
            }
            fs.writeFileSync('data/prod.json', JSON.stringify(oldRecord));
        })
    });
}

我們先去請求用戶信息介面,來判斷登錄是否還有效,因為在產品詳情頁判斷麻煩還要造成五次登錄請求。帶cookie請求很簡單,在post後面set下我們之前得到的cookie即可:.set('Cookie', cookie)。如果後臺返回的retcode為410表示登錄的cookie已失效,需要重新執行getCookie()。這樣就能保證爬蟲一直在登錄狀態。

async的mapLimit方法,會將pageUrls進行併發請求,一次併發量為10。對於每個pageUrl會執行reptileLink方法。等所有的非同步執行完畢後,再執行回調函數。回調函數的result參數是每個reptileLink函數返回數據組成的數組。

reptileLink函數是獲取產品詳情頁的投資記錄列表信息,uniqueId是由已知的username、buyTime、buyAmount參數組成的字元串,用來排重的。

async的回調主要是將最新的投資記錄信息寫入對應的產品對象里,同時生成了counts數組。counts數組是每個產品這次爬取新增的售賣記錄個數組成的數組,和delay一起傳入getNewDelay函數。getNewDelay動態調節爬取頻次,counts是調節delay的唯一依據。delay過大可能產生數據丟失,過小會增加伺服器負擔,可能會被管理員封ip。這裡設置delay最大值為32,最小值為1。

4. 首頁用戶ajax爬取

先上代碼:

function requestData1() {
    superagent.get(ajaxUrl1)
    .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        let newData = JSON.parse(pres.text).data;
        let formatNewData = formatData1(newData);
        // 在這裡清空數據,避免一個文件被同時寫入
        if(clearUser){
            fs.writeFileSync('data/user.json', '');
            clearUser = false;
        }
        let data = fs.readFileSync('data/user.json', 'utf-8');
        if(!data){
            fs.writeFileSync('data/user.json', JSON.stringify(formatNewData));
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log((`首頁用戶購買ajax爬取完畢,時間:${time}`).silly);
        }else{
            let oldData = JSON.parse(data);
            let addData = [];
            // 排重演算法,如果uniqueId不一樣那肯定是新生成的,否則看時間差如果是0(三分鐘內請求多次)或者三分鐘則是舊數據
            for(let i=0, len=formatNewData.length; i<len; i++){
                let matchArr = [];
                for(let len2=oldData.length, j=Math.max(0,len2 - 20); j<len2; j++){
                    if(formatNewData[i].uniqueId === oldData[j].uniqueId){
                        matchArr.push(j);
                    }
                }
                if(matchArr.length === 0){
                    addData.push(formatNewData[i]);
                }else{
                    let isNewBuy = true;
                    for(let k=0, len3=matchArr.length; k<len3; k++){
                        let delta = formatNewData[i].time - oldData[matchArr[k]].time;
                        if(delta == 0 || (Math.abs(delta - 3*60*1000) < 1000)){
                            isNewBuy = false;
                            // 更新時間,這樣下一次判斷還是三分鐘
                            oldData[matchArr[k]].time = formatNewData[i].time;
                        }
                    }
                    if(isNewBuy){
                        addData.push(formatNewData[i]);
                    }
                }
            }
            fs.writeFileSync('data/user.json', JSON.stringify(oldData.concat(addData)));
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log((`首頁用戶購買ajax爬取完畢,時間:${time}`).silly);
        }
    });
}

user.js的爬取和prod.js類似,這裡主要想說一下如何排重的。user.json數據格式如下:

[
{
  "payAmount": 5067.31,
  "productId": "jsfund",
  "productName": "立馬金庫",
  "productType": 6,
  "time": 1548489,
  "username": "鄭**",
  "buyTime": 1516118397758,
  "uniqueId": "5067.31jsfund鄭**"
}, {
  "payAmount": 30000,
  "productId": "201801151830PD84123120",
  "productName": "立馬聚財-12月期HLB01230901",
  "productType": 0,
  "time": 1306573,
  "username": "劉**",
  "buyTime": 1516117199684,
  "uniqueId": "30000201801151830PD84123120劉**"
}]

和產品詳情頁類似,我們也生成一個uniqueId參數用來排除,它是payAmount、productId、username參數的拼成的字元串。如果uniqueId不一樣,那肯定是一條新的記錄。如果相同那一定是一條新記錄嗎?答案是否定的。因為這個介面數據是三分鐘更新一次,而且給出的時間是相對時間,即數據更新時的時間減去購買的時間。所以每次更新後,即使是同一條記錄,時間也會不一樣。那如何排重呢?其實很簡單,如果uniqueId一樣,我們就判斷這個buyTime,如果buyTime的差正好接近180s,那麼幾乎可以肯定是舊數據。如果同一個人正好在三分鐘後購買同一個產品相同的金額那我也沒轍了,哈哈。

5. 零點整合數據

每天零點我們需要整理user.json和prod.json數據,生成最終的數據。代碼:

let globalTimer = setInterval(function(){
    let nowTime = +new Date();
    let nowStr = (new Date()).format("hh:mm:ss");
    let max = nowTime;
    let min = nowTime - 24*60*60*1000;
    // 每天00:00分的時候寫入當天的數據
    if(nowStr === "00:00:00"){
        // 先保存數據
        let prod = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
        let user = JSON.parse(fs.readFileSync('data/user.json', 'utf-8'));
        let lmlc = JSON.parse(JSON.stringify(prod));
        // 清空緩存數據
        clearProd = true;
        clearUser = true;
        // 不足一天的不統計
        // if(nowTime - initialTime < 24*60*60*1000) return
        // 篩選prod.records數據
        for(let i=0, len=prod.length; i<len; i++){
            let delArr1 = [];
            for(let j=0, len2=prod[i].records.length; j<len2; j++){
                if(prod[i].records[j].buyTime < min || prod[i].records[j].buyTime >= max){
                    delArr1.push(j);
                }
            }
            sort.delArrByIndex(lmlc[i].records, delArr1);
        }
        // 刪掉prod.records為空的數據
        let delArr2 = [];
        for(let i=0, len=lmlc.length; i<len; i++){
            if(!lmlc[i].records.length){
                delArr2.push(i);
            }
        }
        sort.delArrByIndex(lmlc, delArr2);

        // 初始化lmlc里的立馬金庫數據
        lmlc.unshift({
            "productName": "立馬金庫",
            "financeTotalAmount": 100000000,
            "productId": "jsfund",
            "yearReturnRate": 4.0,
            "investementDays": 1,
            "interestStartTime": (new Date(min)).format("yyyy年MM月dd日"),
            "interestEndTime": (new Date(max)).format("yyyy年MM月dd日"),
            "getDataTime": min,
            "alreadyBuyAmount": 0,
            "records": []
        });
        // 篩選user數據
        for(let i=0, len=user.length; i<len; i++){
            if(user[i].productId === "jsfund" && user[i].buyTime >= min && user[i].buyTime < max){
                lmlc[0].records.push({
                    "username": user[i].username,
                    "buyTime": user[i].buyTime,
                    "buyAmount": user[i].payAmount,
                });
            }
        }
        // 刪除無用屬性,按照時間排序
        lmlc[0].records.sort(function(a,b){return a.buyTime - b.buyTime});
        for(let i=1, len=lmlc.length; i<len; i++){
            lmlc[i].records.sort(function(a,b){return a.buyTime - b.buyTime});
            for(let j=0, len2=lmlc[i].records.length; j<len2; j++){
                delete lmlc[i].records[j].uniqueId
            }
        }
        // 爬取金庫收益,寫入前一天的數據,清空user.json和prod.json
        let dateStr = (new Date(nowTime - 10*60*1000)).format("yyyyMMdd");
        superagent
            .get('https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=1')
            .end(function(err,pres){
                // 常規的錯誤處理
                if (err) {
                    handleErr(err.message);
                    return;
                }
                var data = JSON.parse(pres.text).data;
                var rate = data.result[0].yearReturnRate||4.0;
                lmlc[0].yearReturnRate = rate

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 數組有三種寫法 1. var arr=new Array(); 2. var arr=[1,2,3] 3. var arr=new Array(); arr[0]=1; 下麵我解釋如下代碼: 關鍵詞for是用來迴圈的,用法很簡單,不過用點規律 for(等於什麼;到什麼值;怎麼到那個值),上面的代碼便 ...
  • 使用jQuery實現表單校驗:(單獨拿出來介紹表單校驗,是因為內容比較多,知識點較多); 1、註:這裡使用validation插件完成對錶單數據的校驗; validate:一款優秀的表單驗證插件——validation插件 (1)特點: 內置驗證規則:擁有必填、數字、email、url和信用卡號碼等 ...
  • 第一章 一些基本概念 HTML(超文本標記語言),構建網頁的靜態結構,由一系列的DOM組成; CSS(層疊樣式表),給網頁各部分結構添加樣式; JavaScript,通過獲取DOM給靜態結構加上動作,使用戶能夠與靜態網頁進行交互; DOM,一種API(應用程式介面),通過這個介面動態的訪問和修改結構 ...
  • 一、使用jQuery為標簽添加屬性或者樣式 1、$("#id名").css("css屬性名","屬性值");比如:$("tbody tr:even").css("background-color","yellow"); 2、使用addClass("class名"),然後在引入的css文件中寫樣式:. ...
  • 本文推薦兩款簡單的富文本編輯器【KindEditor,NicEdit】用於獲得所見即所得的編輯效果,本文僅供學習分享使用,如有不足之處,還請指正。 ...
  • pagemaker是一個前端頁面製作工具,方便產品,運營和視覺的同學迅速開發簡單的前端頁面,從而可以解放前端同學的工作量。此項目創意來自網易樂得內部項目 "nfop" 中的pagemaker項目。原來項目的前端是採用jquery和模板ejs做的,每次組件的更新都會重繪整個dom,性能不是很好。因為當 ...
  • arr sort " " " " " " " " " " 根據一個或者多個屬性對數組進行排序,支持嵌套的屬性。而且可以在每個條件中指定排序的方向,並支持傳入比較函數。 安裝 採用 "npm" 安裝: 採用 "yarn" 安裝: 用法 通過給定的對象屬性進行排序: 逆向排序 參數 : { Object ...
  • 研究了淘寶,天貓和網易彩票163的wap主頁樣式佈局,總結移動端佈局方案 註意:代碼運行是file協議,在chrome里不支持引用本地文件,會提示跨域錯誤,可以用firefox或者Safari打開 當時做的ppt下載: "2015年12月移動端佈局方案探究" 一、基本概念 1. 物理像素(physi ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...