簡単な本を大量にエクスポートするユーザーのすべての記事リストと記事のハイパーリンク


もっと読む
簡書の改版後、文章のタイトルによって文章を検索する機能がなくなりました.
 
簡単な本は大量に文章をダウンロードする機能を提供していますが、現地にダウンロードした文章は全部markdown形式です.文章のリンクは含まれていません.これは私の需要を満たしていません.
プログラマである以上、この機能がないなら自分で一つを実現します.
簡単な本のトップページを開けて、デフォルトでは8つの文章しか表示されていません.マウスを使って画面の底にスライドすると、怠け者のローディングイベントが発生し、バックグラウンドでより多くの記事のリストを読み込むので、文章の読み取りはサーバーの端で行われます.
 
Chrome開発者ツールを開けて、ネットワーク要求を観察して、urlの中で99 b 8712 e 8850が私の簡単な本のユーザーIDであることを要求します.
 
各ページの記事の内容はhtml形式で応答構造に含まれています.
 
私が関心を持っているのは文章のタイトルと文章のリンクだけで、上の図のように明るいフィールドに示されています.
最初にnodejsアプリケーションを書きました.コードは以下の通りです.
var request = require('request');
var jsdom = require("jsdom");
var JSDOM = jsdom.JSDOM;
const PREFIX = "https://www.jianshu.com";
const PAGE = "https://www.jianshu.com/u/99b8712e8850?order_by=shared_at&page=";
const MAX = 2;

var mArticleResult = new Map();
var pageNumber;
/* a given article: https://www.jianshu.com/p/963cd23fb092
  value got from API: /p/5c1d0319dc42
*/
var lastPageReached = false;
var url = "";

var aHandlers = [];

// use limited for loop to ease testing
for(var i = 0; i < MAX; i++){
  pageNumber = i + 1;
  var url = PAGE + pageNumber;
  // console.log("current page: " + url);
  var pageOptions = {
        url: url,
        method: "GET",
        headers: {
            "Accept": "text/html"
        }
  };
  aHandlers.push(getArticles(pageOptions, pageNumber));
  if( lastPageReached)
    break;
}

console.log("promise handler size: " + aHandlers.length);

Promise.all(aHandlers).then(function(){
  var articleIndex = 0;
  for (var [key, value] of mArticleResult) {
    console.log("Article[" + articleIndex++ + "]: " + key + " = " + value);
  }
  console.log("done");
}
  );

function getArticles(pageOptions, pageNumber) {
  return new Promise(function(resolve,reject){
      var requestC = request.defaults({jar: true});

      requestC(pageOptions,function(error,response,body){
        if( error){
          console.log("error: " + error);
          resolve(error);
        }
        var document = new JSDOM(body).window.document;
        var content = document.getElementsByTagName("li");

        for( var i =0; i < content.length; i++){
          var li = content[i];
          var children = li.childNodes;
          for( var j = 0; j < children.length; j++){
              var eachChild = children[j];
              if( eachChild.nodeName == "DIV"){
                var grandChild = eachChild.childNodes;
                for( var k = 0; k < grandChild.length; k++){
                  var grand = grandChild[k];
                  if( grand.nodeName == "A"){
                    var fragment = grand.getAttribute("href");
                    if( fragment.indexOf("/p") < 0)
                      continue;
                    console.log("title: " + grand.text);
                    var wholeURL = PREFIX + fragment;
                    console.log("url: " + wholeURL);
                    if( mArticleResult.has(grand.text)){
                      lastPageReached = true;
                      console.log("article size: " + mArticleResult.size);
                      resolve(pageNumber);
                    }
                    mArticleResult.set(grand.text, wholeURL);
                  }
                }
              }
          }
        }// end of outer loop
        resolve(pageNumber);
      }); 
     });
}
原理はnodejsのrequest moduleを使って、簡書のウェブサイトに同時に複数の要求を開始して、各要求は1ページの略書の文章を読みます.
その後、このような方法は、同時要求数が10以上の場合は動作しないことが分かり、簡易サイトは、このような要求を拒否し、HTTP 429の状態コードに戻る.
最後に簡単な同期要求を採用して実現しました.nodejsから提供されたsync-requestを使ってサイクル内で要求を開始しました.
var request = require("sync-request");
var jsdom = require("jsdom");
var JSDOM = jsdom.JSDOM;
var textEncoding = require('text-encoding'); 
var textDecoder = textEncoding.TextDecoder;

const PREFIX = "https://www.jianshu.com";
const PAGE = "https://www.jianshu.com/u/99b8712e8850?order_by=shared_at&page=";
const MAX = 100;

var mArticleResult = new Map();
var lastPageReached = false;
var pageNumber;
/* a given article: https://www.jianshu.com/p/963cd23fb092
  value got from API: /p/5c1d0319dc42
*/

try {
    // use limited for loop to ease testing
    for (var i = 0; i < MAX; i++) {
        if( lastPageReached)
          break;
        pageNumber = i + 1;
        var url = PAGE + pageNumber;
        console.log("current page: " + url);
        var response = request('GET', url);
        var html = new textDecoder("utf-8").decode(response.body);
        handleResponseHTML(html);
    }
} 
catch (e) {

}

var articleIndex = 0;
var resultHTML = "";

const fs = require('fs');

/*


eee

22

33

*/
var index = 1; for (var [key, value] of mArticleResult) { var article = "

+key+“\”>“+” index++「.」+value+「

"
+ "
"
; resultHTML = resultHTML + article; console.log("Article[" + articleIndex++ + "]: " + value + " = " + key); } resultHTML = resultHTML + ""; var pwd = process.cwd() + "/jianshu.html"; fs.appendFileSync(pwd, resultHTML); console.log("done"); function handleResponseHTML(html) { var document = new JSDOM(html).window.document; var content = document.getElementsByTagName("li"); for (var i = 0; i < content.length; i++) { var li = content[i]; var children = li.childNodes; for (var j = 0; j < children.length; j++) { var eachChild = children[j]; if (eachChild.nodeName == "DIV") { var grandChild = eachChild.childNodes; for (var k = 0; k < grandChild.length; k++) { var grand = grandChild[k]; if (grand.nodeName == "A") { var fragment = grand.getAttribute("href"); if (fragment.indexOf("/p") < 0) continue; // console.log("title: " + grand.text); var wholeURL = PREFIX + fragment; // console.log("url: " + wholeURL); if (mArticleResult.has(wholeURL)) { lastPageReached = true; console.log("article size: " + mArticleResult.size); return; } mArticleResult.set(wholeURL, grand.text); } } } } } }
このnodejsアプリケーションは、ローカルでhtmlファイルを生成し、記事ごとのタイトルとハイパーリンクを含みます.
 
もっとJerryのオリジナル記事を取得するには、公衆番号の「汪子煕」に注目してください.