ホームページ >ウェブフロントエンド >jsチュートリアル >NodeJSクローラーの詳しい説明
1. クローラープロセス
私たちの最終目標は、Lima Financial Management の毎日の売上をクロールし、どの製品が販売されたか、どのユーザーが各製品をいつ購入したかを知ることです。まず、クロールの主な手順を紹介します。
最初のステップは、もちろん、どのページをクロールするのか、ページ データを明確に分析することです。ページの構造、ログインする必要があるか、Ajax インターフェイスがあるか、返されるデータの種類など。
どのページと Ajax をクロールするかを明確に分析したら、データをクロールします。現在の Web ページのデータは、同期ページと Ajax インターフェイスに大別されます。ページ データの同期キャプチャでは、まず Web ページの構造を分析する必要があります。通常、Python は正規表現マッチングを通じてデータをキャプチャし、取得したページ コンテンツを jquery オブジェクトに変換できる Cherio ツールを備えています。実際、ソース コードを見ると、これらの API の本質は規則的なマッチングです。 Ajax インターフェイス データは通常、json 形式であり、処理が比較的簡単です。
データを取得した後、簡単なフィルタリングが行われ、その後の分析と処理のために必要なデータが保存されます。もちろん、MySQL や Mongodb などのデータベースを使用してデータを保存することもできます。ここでは便宜上、ファイル ストレージを直接使用します。
最終的にはデータを表示したいので、特定の次元に従って元のデータを処理および分析し、それをクライアントに返す必要があります。このプロセスは、保存中または表示中に処理でき、フロントエンドがリクエストを送信し、バックグラウンドが保存されたデータを取得して再度処理します。これは、データをどのように表示したいかによって異なります。
これだけの作業を行ったのに、まったく表示出力がありません。これは私たちの以前のビジネスに戻ります。フロントエンド表示ページについては誰もがよく知っているはずです。データの表示はより直感的であり、統計の分析が容易になります。
Superagent は、nodejs で get、post、head などのネットワークを実行する必要がある場合に非常に便利なクライアント リクエスト プロキシ モジュールです。リクエストがあれば試してみます。
Cheerio は、CSS セレクターを使用して Web ページからデータを取得するために使用される jquery の Node.js バージョンとして理解できます。使用方法は jquery とまったく同じです。
Async は、直接的で強力な非同期関数、mapLimit(arr、limit、iterator、callback) を提供するプロセス制御ツールキットです。API は公式 Web サイトで確認できます。
arr-delは、配列要素を削除するために私が独自に作成したツールです。削除する配列要素のインデックスで構成される配列を渡すことで、1 回限りの削除を実行できます。
arr-sort は私自身が書いた配列ソートツールです。並べ替えは 1 つ以上の属性に基づいて行うことができ、ネストされた属性がサポートされています。さらに、各条件でソート方向を指定したり、比較関数を渡すことができます。
まず、クローリングのアイデアを確認してみましょう。 Lima Financial Management Online の商品は主に定期商品と Lima Treasury です (光大銀行の最新の財務管理商品は処理が難しく、開始投資額が高いため、購入する人はほとんどいないため、ここには統計がありません)。定期的に、財務管理ページの ajax インターフェイスをクロールできます: https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=0
。 (追記:通常商品は近々在庫切れとなり、データがご覧いただけなくなる可能性があります) データは下図の通りです。
現在販売中の全ての通常商品が含まれております。オンラインで販売される ajax データには、製品 ID、調達額、現在の売上高、年率収益率、投資日数など、製品自体に関連する情報のみが含まれており、どのユーザーが製品を購入したかに関する情報はありません。 。そのため、製品詳細ページ (Jucai 即時発行 - 12 月号 HLB01239511 など) をクロールするには id パラメーターを取得する必要があります。詳細ページには、以下の図に示すように、必要な情報が含まれる投資記録の列があります:
ただし、詳細ページを表示するには、ログインする必要があります。 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
をクロールする必要があります。しかし、後でこのインターフェースが 3 分ごとに更新されることがわかりました。これは、バックグラウンドが 3 分ごとにサーバーにデータを要求することを意味します。データは一度に10件ありますので、3分以内に購入商品のレコード数が10件を超えた場合はデータが省略されます。これを回避する方法はないため、リマ財務省の統計は実際の統計よりも低くなります。 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条,数据就会有遗漏。这是没有办法的,所以立马金库的统计数据会比真实的偏少。
因为产品详情页需要登录,所以我们要先拿到登录的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方法。
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属性记录着售卖信息。
我们再看下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)
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); } }); }電話番号とパスワードのパラメーターはコマンド ラインから渡されます。これは、Financial Management の携帯電話番号でログインするために使用されるアカウント番号とパスワードです。スーパーエージェントを使用して、Immediate Financial Management ログイン インターフェイスのリクエストをシミュレートします:
https://www.lmlc.com/user/s/web/logon
。コールバックで対応するパラメーターを渡し、ヘッダーの set-cookie 情報を取得し、setCookeie イベントを送信します。リスニング イベント emitter.on("setCookie", requestData)
を設定しているため、Cookie を取得したら、requestData メソッドを実行します。 2. 財務管理ページの Ajax クローリング requestData メソッドのコードは次のとおりです: [ { "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刘**" }]コードは非常に長いので、getDetailData 関数のコードは後で分析します。 一般的に販売されている製品の総数は 10 を超えないため、要求された ajax インターフェイスはページング インターフェイスです。ここでは、すべての製品を一度に取得できるように、パラメーター pageSize を 100 に設定します。 🎜🎜clearProd は、毎日 0 時に、prod (通常の製品) と user (ホームページ ユーザー) のデータがクリアされるグローバル リセット信号です。 🎜🎜毎日10時に商品が売り切れる事が稀にあるため、毎日10時にデータが素早く更新されるため、データの損失を防ぐためにクロールの頻度を増やす必要があります。したがって、販売前の商品、つまり buyStartTime が現在時刻より大きい場合は、それを記録し、販売開始時にタイマーを設定する必要があります。setPreId メソッドを参照して、クロール頻度を 1 回/秒に調整します。 🎜🎜セール中の商品がない場合、つまり pageUrls が空の場合、クロールの頻度は最大 32 秒に設定されます。 🎜🎜 requestData 関数のコードのこの部分は、主に新しい製品があるかどうかを記録します。ある場合は、新しいオブジェクトを作成し、製品情報を記録し、prod 配列にプッシュします。 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; fs.writeFileSync(`data/${dateStr}.json`, JSON.stringify(lmlc)); }) } }, 1000);🎜 はオブジェクトの配列であり、各オブジェクトは新製品を表し、records 属性は販売情報を記録します。 🎜🎜3. 製品詳細ページをクロールします🎜🎜getDetailData:🎜
var gulp = require('gulp'); var uglify = require("gulp-uglify"); var less = require("gulp-less"); var minifyCss = require("gulp-minify-css"); var livereload = require('gulp-livereload'); var connect = require('gulp-connect'); var minimist = require('minimist'); var babel = require('gulp-babel'); var knownOptions = { string: 'env', default: { env: process.env.NODE_ENV || 'production' } }; var options = minimist(process.argv.slice(2), knownOptions); // js文件压缩 gulp.task('minify-js', function() { gulp.src('src/js/*.js') .pipe(babel({ presets: ['es2015'] })) .pipe(uglify()) .pipe(gulp.dest('dist/')); }); // js移动文件 gulp.task('move-js', function() { gulp.src('src/js/*.js') .pipe(babel({ presets: ['es2015'] })) .pipe(gulp.dest('dist/')) .pipe(connect.reload()); }); // less编译 gulp.task('compile-less', function() { gulp.src('src/css/*.less') .pipe(less()) .pipe(gulp.dest('dist/')) .pipe(connect.reload()); }); // less文件编译压缩 gulp.task('compile-minify-css', function() { gulp.src('src/css/*.less') .pipe(less()) .pipe(minifyCss()) .pipe(gulp.dest('dist/')); }); // html页面自动刷新 gulp.task('html', function () { gulp.src('views/*.html') .pipe(connect.reload()); }); // 页面自动刷新启动 gulp.task('connect', function() { connect.server({ livereload: true }); }); // 监测文件的改动 gulp.task('watch', function() { gulp.watch('src/css/*.less', ['compile-less']); gulp.watch('src/js/*.js', ['move-js']); gulp.watch('views/*.html', ['html']); }); // 激活浏览器livereload友好提示 gulp.task('tip', function() { console.log('\n<----- 请用chrome浏览器打开 http://localhost:5000 页面,并激活livereload插件 ----->\n'); }); if (options.env === 'development') { gulp.task('default', ['move-js', 'compile-less', 'connect', 'watch', 'tip']); }else{ gulp.task('default', ['minify-js', 'compile-minify-css']); }のコードを見てみましょう🎜最初に、ログインがまだ有効であるかどうかを判断するためにユーザー情報インターフェイスをリクエストします。これは、5回の判断に問題が発生するためです。製品詳細ページへのログイン要求。 Cookie を使用したリクエストは非常に簡単で、以前に取得した Cookie を投稿の後に設定するだけです:
.set('Cookie', cookie)
。バックグラウンドから返された retcode が 410 の場合、ログイン Cookie の有効期限が切れているため、getCookie() を再度実行する必要があることを意味します。これにより、クローラーが常にログインした状態になります。 🎜🎜 async の mapLimit メソッドは pageUrls に対して同時リクエストを行い、同時実行数は一度に 10 です。 reptileLink メソッドは pageUrl ごとに実行されます。すべての非同期実行が完了するまで待ってから、コールバック関数を実行します。コールバック関数の結果パラメータは、各 reptileLink 関数によって返されたデータで構成される配列です。 🎜🎜reptileLink 関数は、商品詳細ページの投資記録リスト情報を取得するために使用されます。 uniqueId は、重複を排除するために使用される、既知のユーザー名、buyTime、および buyAmount パラメーターで構成される文字列です。 🎜🎜 async のコールバックは主に、最新の投資記録情報を対応する製品オブジェクトに書き込み、同時にカウント配列を生成します。 counts 配列は、今回クロールされた各商品の新規販売レコード数からなる配列で、遅延とともに getNewDelay 関数に渡されます。 getNewDelay はクロール頻度を動的に調整します。遅延を調整するための唯一の基準はカウントです。遅延が大きすぎるとデータ損失が発生する可能性があり、小さすぎるとサーバーの負荷が増大し、管理者が IP アドレスをブロックする可能性があります。ここでは、遅延の最大値は 32、最小値は 1 に設定されています。 🎜🎜4. ホームページ ユーザーの ajax クローリング 🎜🎜 まず、コードに行きましょう: 🎜rrreee🎜 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,那么几乎可以肯定是旧数据。如果同一个人正好在三分钟后购买同一个产品相同的金额那我也没辙了,哈哈。
每天零点我们需要整理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; fs.writeFileSync(`data/${dateStr}.json`, JSON.stringify(lmlc)); }) } }, 1000);
globalTimer是个全局定时器,每隔1s执行一次,当时间为00:00:00
时,clearProd和clearUser全局参数为true
,这样在下次爬取过程时会清空user.json和prod.json文件。没有同步清空是因为防止多处同时修改同一文件报错。取出user.json里的所有金库记录,获取当天金库相关信息,生成一条立马金库的prod信息并unshift进prod.json里。删除一些无用属性,排序数组最终生成带有当天时间戳的json文件,如:20180101.json。
前端总共就两个页面,首页和详情页,首页主要展示实时销售额、某一时间段内的销售情况、具体某天的销售情况。详情页展示某天的具体某一产品销售情况。页面有两个入口,而且比较简单,这里我们采用gulp来打包压缩构建前端工程。后台用express搭建的,匹配到路由,从data文件夹里取到数据再分析处理再返回给前端。
Echarts
Echarts是一个绘图利器,百度公司不可多得的良心之作。能方便的绘制各种图形,官网已经更新到4.0了,功能更加强大。我们这里主要用到的是直方图。
DataTables
Datatables是一款jquery表格插件。它是一个高度灵活的工具,可以将任何HTML表格添加高级的交互功能。功能非常强大,有丰富的API,大家可以去官网学习。
Datepicker
Datepicker是一款基于jquery的日期选择器,需要的功能基本都有,主要样式比较好看,比jqueryUI官网的Datepicker好看太多。
gulp配置比较简单,代码如下:
var gulp = require('gulp'); var uglify = require("gulp-uglify"); var less = require("gulp-less"); var minifyCss = require("gulp-minify-css"); var livereload = require('gulp-livereload'); var connect = require('gulp-connect'); var minimist = require('minimist'); var babel = require('gulp-babel'); var knownOptions = { string: 'env', default: { env: process.env.NODE_ENV || 'production' } }; var options = minimist(process.argv.slice(2), knownOptions); // js文件压缩 gulp.task('minify-js', function() { gulp.src('src/js/*.js') .pipe(babel({ presets: ['es2015'] })) .pipe(uglify()) .pipe(gulp.dest('dist/')); }); // js移动文件 gulp.task('move-js', function() { gulp.src('src/js/*.js') .pipe(babel({ presets: ['es2015'] })) .pipe(gulp.dest('dist/')) .pipe(connect.reload()); }); // less编译 gulp.task('compile-less', function() { gulp.src('src/css/*.less') .pipe(less()) .pipe(gulp.dest('dist/')) .pipe(connect.reload()); }); // less文件编译压缩 gulp.task('compile-minify-css', function() { gulp.src('src/css/*.less') .pipe(less()) .pipe(minifyCss()) .pipe(gulp.dest('dist/')); }); // html页面自动刷新 gulp.task('html', function () { gulp.src('views/*.html') .pipe(connect.reload()); }); // 页面自动刷新启动 gulp.task('connect', function() { connect.server({ livereload: true }); }); // 监测文件的改动 gulp.task('watch', function() { gulp.watch('src/css/*.less', ['compile-less']); gulp.watch('src/js/*.js', ['move-js']); gulp.watch('views/*.html', ['html']); }); // 激活浏览器livereload友好提示 gulp.task('tip', function() { console.log('\n<----- 请用chrome浏览器打开 http://localhost:5000 页面,并激活livereload插件 ----->\n'); }); if (options.env === 'development') { gulp.task('default', ['move-js', 'compile-less', 'connect', 'watch', 'tip']); }else{ gulp.task('default', ['minify-js', 'compile-minify-css']); }
开发和生产环境都是将文件打包到dist目录。不同的是:开发环境只是编译es6和less文件;生产环境会再压缩混淆。支持livereload插件,在开发环境下,文件改动会自动刷新页面。
相关推荐:
以上がNodeJSクローラーの詳しい説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。