ktty1220 / cheerio-httpcli

iconvによる文字コード変換とcheerioによるHTMLパースを組み込んだNode.js用HTTPクライアントモジュール
MIT License
262 stars 28 forks source link

fetch()でPOSTを指定したい #32

Closed corgimkii closed 5 years ago

corgimkii commented 5 years ago

いつもWebスクレイピングの用途でcheerio-httpcliを利用させていただいております。 ありがとうございます。

fetch()がGETにて決め打ちになっているため、POSTでアクセスする必要があるページが対象のときは下記のように追記して利用しているのですが、今後、機能追加の予定などはありますでしょうか? もしくは、下記のような形にてプルリクを上げてもよろしいでしょうか?

cheerio-httpcli/index.d.ts

function fetchPost(url: string, param: {[ name: string ]: any}, encode: string, callback: FetchCallback): void;

cheerio-httpcli/lib/core.js

  /**
   * POSTによる非同期httpリクエストを実行
   *
   * @param url      リクエスト先のURL
   * @param param    リクエストパラメータ
   * @param encode   取得先のHTMLのエンコーディング(default: 自動判定)
   * @param callback リクエスト完了時のコールバック関数(err, cheerio, response, body)
   */
  CheerioHttpCli.prototype.fetchPost = function (url, param, encode, callback) {
    return client.run('POST', url, param, encode, callback);
  };
ktty1220 commented 5 years ago

cheerio-httpcliはブラウザと同じような使い勝手(エンコーディングとかリクエストヘッダは勝手に良い感じにしてくれるので気にしないでいい)でWEBページをスクレイピングできるように、というコンセプトで作られています。

ブラウザのアドレスバーにURLを入力してページにアクセスする際にいきなりPOSTリクエストを飛ばせないのと同じように、cheeroi-httpcliでいきなりPOSTリクエストという機能は今のところは実装していません。

POSTリクエストを送信したい場合は、ブラウザでの操作と同じように、まずPOSTするフォームが設置されているページにfetch()でアクセスして、そこから$('form').submit()$('form input[type=submit]').click()のような感じでフォーム送信をエミュレートする、という方法を推奨しています。

POSTリクエストを直接送信する方法だと、ある時から送信元のフォームの部品が増えたり減ったり、一部inputvalueの内容が変わったりしていた場合、サーバー側が想定していないパラメータを送信してしまう(ありえないパラメータが送信されてきた => ロボットによるアクセスと断定される => 遮断)可能性がありますが、上記の推奨方法ならそういった心配は減ります。

直接POSTリクエストを送信したいケースとして考えられるのは、

  1. 何かしらのAPIを直接呼び出す
  2. POSTしたいフォームが設置されているページが動的にフォーム部品を用意するような形になっていて、静的なページをベースに処理するcheerio-httpcliだと対応できない
  3. とあるページからのフォームを使用してパラメータを変えて何度もフォーム送信をしたいが、送信先のページからいちいちフォームのページに戻るのは面倒なので、あらかじめ分かっているパラメータを配列にして直接POSTでループして処理したい

などがありますが、

1に関しては取得する情報がWEBページではなくJSONやXMLになると思うので、むしろcheerio-httpcliを使用しないでrequestモジュールやfetchモジュールなど内部で余計な加工を行わないシンプルな通信モジュールを使用した方が良いです(cheerio-httpcliだと内部でエンコーディング変換など行うのでAPIレスポンス内容が壊れる可能性がある)。

2については、以下のように送信時に動的に作成されるフォーム部品を追加すれば対応可能です。

// 動的にフォーム部品が作成されるページにfetchでアクセス
client.fetch('https://hogehoge.foo/bar.html')
.then(({ $ }) => {
  $('form').submit({
    query: 'hoge',                       // 元々存在する項目
    new_input1: '12345',                 // 元々存在しない項目を指定(文字列)
    new_input2: [ 'aaa', 'bbb', 'ccc' ]  // 元々存在しない項目を指定(配列)
  });
});

3については以下のような感じで対応できます。おそらくいきなりPOST送信するループと同じような形になると思います。

(async () => {
  // スリープ関数
  const sleep = (n) => new Promise((resolve) => setTimeout(resolve, n));

  // フォームが設置されているページにfetchでアクセス
  const formPage = await client.fetch('http://hogehoge.foo/bar.html');

  // 以下の配列内の文字列を1つずつ指定して同じフォームから送信
  const fruits = [ 'apple', 'banana', 'cherry' ];

  for (let i = 0; i < fruits.length; i++) {
    // result = フォーム送信先ページの情報
    const resultPage = await formPage.$('form').submit({
      query: fruits[i]
    });

    // 検索結果を取得
    console.log(resultPage.$('h1').text());

    // 1秒待機
    await sleep(1000);
  }
})();

どいった感じで、今のところ直接POSTはなくても良いんじゃないかと思っていますが、POSTリクエストを直接送信したいというシーンを教えてもらっても良いですか。

「なくてもいい」というだけで「あってはならない」というわけではないので、事情を聞いてみて「ああ、それなら直接POSTは必要だなー」ということになれば実装を検討すると思います。

※「なくてもいい」ということは「あってもいい」ということにもなりますが、基本コンセプトが冒頭で述べた通りで、POST送信するにはまず送信したいフォームのページにアクセスするというのが正規の手順だと思いますので、抜け道的な機能は不要であればそれに越したことはない的な感じで考えています。

corgimkii commented 5 years ago

コンセプトを十分理解せずにissueを上げてしまいすみません。。 やろうとしていることは翻訳サイトにおいて、submit後の翻訳結果画面のスクレイピングです。 (その翻訳サイトでしか提供していない独自の付加機能が必要で、またそのサイトはAPI提供をしていません)

上に挙げて頂いた1.〜3.の中ですと、3.に該当するかと思います。 フォームが設置されている入力画面(GET)は共通ですし、ブラウザのフォーム送信エミュレートと、記載いただいたコード例にて対応可能です! おっしゃるとおり、ブラウザの直接POSTを機能追加するまでもない案件ですね。失礼いたしました。

丁寧にご教示くださり、ありがとうございます。

ktty1220 commented 5 years ago

コンセプトについてはどこかに明記してたわけではないのでお気になさらず。 :wink: 今回は機能追加無しで対応できそうということなので、こちらは閉じさせてもらいます。