初めに
TypeScriptによるスクレピングの簡単な手法を紹介したいと思います。
記事のポイントはあくまでもTypeScriptの使用、高度なスクレピング技法の紹介ではありません。
前提条件
- ある程度Typescriptの文法が分かってること
- Node.jsの環境が整って、npmコマンド使えること
- グローバル環境にTypeScriptに入ってること
- 法に触れること、人に迷惑かけることをしないこと
プロジェクト初期化
mkdir [好きなディレクトリ] && cd [好きなディレクトリ]
package.jsonとtsconfig.jsonの初期化
npm init -y && tsc --init
プロジェクトのフォルダ内にsrcフォルダを作ります。
mkdir src
tscofig.jsonのrootDirをsrcフォルダに指定します。
...
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
...
srcフォルダ内にcrowllwe.tsファイルを作って、中身 console.log('test')
を追加します。
console.log('test');
現時点使用するライブラリをインストール
- npm install typescript -D
- npm install ts-node -D
package.jsonを修正します。
...
"scripts": {
"dev": "ts-node ./src/crowller.ts"
},
...
コマンドラインで npm run dev
を実行します。test
がもし正常に表示出来たらオーケーです。
$ npm run dev
> [好きなディレクトリ名]@1.0.0 dev [好きなディレクトリ名]
> ts-node ./src/crowller.ts
test
ここまで初期化は完了です。
ディレクトリ構成は以下の通りです。
好きなディレクトリ
|-node_modules
|-src
|- |- crowller.ts
|- package-lock.json
|- package.json
|- tsconfig.json
HTMLレスポンス取得
ターゲットサイトからHtmlレスポンスもらう必要がある為、リクエスト送れるライブラリsuperagent
を使用します。
npm install superagent --save
インストール終わったら、crowller.tsにimportします。
import superagent from 'superagent'
この場合、恐らくIDEに怒られます。vscode使用してコーティングする場合、以下のメッセージが表示されます。
'superagent' が宣言されていますが、その値が読み取られることはありません。ts(6133)
モジュール 'superagent' の宣言ファイルが見つかりませんでした。'/qiita-spider-ts/node_modules/superagent/lib/node/index.js' は暗黙的に 'any' 型になります。
Try `npm install @types/superagent` if it exists or add a new declaration (.d.ts) file containing `declare module 'superagent';`ts(
なぜなら、superagent
はjavascriptで書かれているライブラリ、Typescriptが直接認識することができません。
その場合、ライブラリの翻訳ファイルが必要になります。翻訳ファイルは.d.ts
の拡張子を持ってます。
翻訳ファイルをインストールします。
npm install @types/superagent -D
これでエラーが解決できるはずです、それでも消えない場合、一回IDEを再起動することお勧めします。
実際リクエスト送信して、HTMLリスポンス受けとってみましょう。
ターゲットサイトは任意で構いません。
import superagent from 'superagent'
class Crowller {
private url = "url"
constructor(){
this.getRawHtml();
}
async getRawHtml(){
const result = await superagent.get(this.url);
console.log(result.text)
}
}
const crowller = new Crowller()
npm run dev
で実行すると、レスポンスもらえたらオーケーです。
...
<span class='c-job_offer-detail__term-text'>給与</span>
</div>
</th>
<td class='c-job_offer-detail__description'>
<strong class='c-job_offer-detail__salary'>550万 〜 800万円</strong>
</td>
</tr>
<tr>
<th>
...
レスポンスから必要なデータを抜き取る
正規表現で抜き取ることもできますが、今回は多少便利になるcheerio
というライブラリを使用します。
ドキュメント
npm install cheerio --save
npm install @types/cheerio -D
cheerioを使用すれば、jQueryのような文法でHTMLをから内容を抜き取れます。
実際使ってみます、下記のDOM構造からテキスト内容を抜き取るためにcrowller.tsを修正します。
import superagent from 'superagent';
import cheerio from 'cheerio';
class Crowller {
private url = "url"
constructor(){
this.getRawHtml();
}
async getRawHtml(){
const result = await superagent.get(this.url);
this.getJobInfo(result.text);
}
getJobInfo(html:string){
const $ = cheerio.load(html)
const jobItems = $('.c-job_offer-recruiter__name');
jobItems.map((index, element)=>{
const companyName = $(element).find('a').text();
console.log(companyName)
})
}
}
const crowller = new Crowller()
実行してみます。
$ npm run dev
> qiita-spider-ts@1.0.0 dev 好きなディレクトリ名\qiita-spider-ts
> ts-node ./src/crowller.ts
xxx株式会社
株式会社xxx
xxx株式会社
...
データの保存
src
フォルダと同じ階層でデータ保存用のdata
フォルダを新規追加します。
|- node_modules
|- src
|- data
|- |- crowller.ts
|- package-lock.json
|- package.json
|- tsconfig.json
取得したデータをjson形式でdataフォルダに保存します。
その前にデータに含む要素を決めるためのインターフェースを定義します。
転職サイトをターゲットにしてるため、会社名とポジションと提示年収の三つをインターフェースの要素として追加します。
...
interface jobInfo {
companyName: string,
jobName: string,
salary: string
}
...
そして配列に継承させて、データを入れていきます。
...
getJobInfo(html:string){
const $ = cheerio.load(html)
const jobItems = $('.c-job_offer-box__body');
const jobInfos:jobInfo[] = [] //インターフェース継承
jobItems.map((index, element) => {
const companyName = $(element).find('.c-job_offer-recruiter__name a').text();
const jobName = $(element).find('.c-job_offer-detail__occupation').text();
const salary= $(element).find('.c-job_offer-detail__salary').text();
jobInfos.push({
companyName,
jobName,
salary
})
});
const result = {
time: (new Date()).getTime(),
data: jobInfos
};
console.log(result);
}
...
再度実行してみます。データが綺麗になってることが分かります。
$ npm run dev
> qiita-spider-ts@1.0.0 dev 好きなディレクトリ名\qiita-spider-ts
> ts-node ./src/crowller.ts
{ time: 1583160397866,
data:
[ { companyName: 'xx株式会社',
jobName: 'フロントエンドエンジニア',
salary: 'xxx万 〜 xxx万円' },
{ companyName: '株式会社xxxx',
...
保存用の関数を定義
generateJsonContent
というデータ保存用の関数を定義します。
...
async getRawHtml(){
const result = await superagent.get(this.url);
const jobResult = this.getJobInfo(result.text); //整形後のデータを受け取ります。
this.generateJsonContent(jobResult); //保存用の関数に渡します。
}
// 保存用の関数
generateJsonContent(){
}
...
getJobInfo(html:string){
...
const result = {
time: (new Date()).getTime(),
data: jobInfos
};
return result
}
でも、そのままデータを受け取れないので保存用のinterface
を定義します。
interface JobResult {
time: number,
data: JobInfo[]
}
それを保存用の関数の引数型として渡します。
...
generateJsonContent(jobResult:JobResult){
}
...
データをファイルに保存するために、node.jsのファイル操作関連のライブラリをimport
import fs from 'fs';
import path from 'path'
generateJsonContent関数の中身書いていきます。
...
generateJsonContent(jobResult:JobResult){
const filePath = path.resolve(__dirname, '../data/job.json')
let fileContent = {}
if(fs.existsSync(filePath)){
fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
fileContent[jobResult.time] = jobResult.data;
fs.writeFileSync(filePath, JSON.stringify(fileContent));
}
...
今の内容ですと、恐らく fileContent[jobResult.time]
がエラーになると思います。
エラーの内容は以下の通り。
(property) JobResult.time: number
Element implicitly has an 'any' type because expression of type 'number' can't be used to index type '{}'.
No index signature with a parameter of type 'number' was found on type '{}'.ts(7053)
これを解決するには fileContent
に型を振る必要があります。
そのまま let fileContent:any = {}
にしてもいいですが、
ちゃんとしたインターフェース定義した方がtypescriptらしいです。
...
interface Content {
[propName: number]: JobInfo[];
}
...
generateJsonContent(jobResult:JobResult){
...
let fileContent:Content = {}
...
}
最後に実行してみましょう。
npm run dev
data
フォルダの下にjob.json
ファイルが作られて、データも保存されてるはずです。
終わりに
最初計画として、Typescriptを使ってExpressでスクレピングコントロールできるAPIを作るまでやりたかったのですが、
流石に長すぎて良くないと思いましたので、また今度時間ある時に。
import fs from 'fs';
import path from 'path'
import superagent from 'superagent';
import cheerio from 'cheerio';
interface JobInfo {
companyName: string,
jobName: string,
salary: string
}
interface JobResult {
time: number,
data: JobInfo[]
}
interface Content {
[propName: number]: JobInfo[];
}
class Crowller {
private url = "url"
constructor(){
this.getRawHtml();
}
async getRawHtml(){
const result = await superagent.get(this.url);
const jobResult = this.getJobInfo(result.text);
this.generateJsonContent(jobResult)
}
generateJsonContent(jobResult:JobResult){
const filePath = path.resolve(__dirname, '../data/job.json')
let fileContent:Content = {}
if(fs.existsSync(filePath)){
fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
fileContent[jobResult.time] = jobResult.data;
fs.writeFileSync(filePath, JSON.stringify(fileContent));
}
getJobInfo(html:string){
const $ = cheerio.load(html)
const jobItems = $('.c-job_offer-box__body');
const jobInfos:JobInfo[] = []
jobItems.map((index, element)=>{
const companyName = $(element).find('.c-job_offer-recruiter__name a').text();
const jobName = $(element).find('.c-job_offer-detail__occupation').text();
const salary = $(element).find('.c-job_offer-detail__salary').text();
jobInfos.push({
companyName,
jobName,
salary
})
});
const result = {
time: (new Date()).getTime(),
data: jobInfos
};
return result
}
}
const crowller = new Crowller()