初めに
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します。
importsuperagentfrom'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リスポンス受けとってみましょう。
ターゲットサイトは任意で構いません。
importsuperagentfrom'superagent'classCrowller{privateurl="url"constructor(){this.getRawHtml();}asyncgetRawHtml(){constresult=awaitsuperagent.get(this.url);console.log(result.text)}}constcrowller=newCrowller()
npm run dev
で実行すると、レスポンスもらえたらオーケーです。
...
<spanclass='c-job_offer-detail__term-text'>給与</span></div></th><tdclass='c-job_offer-detail__description'><strongclass='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を修正します。
importsuperagentfrom'superagent';importcheeriofrom'cheerio';classCrowller{privateurl="url"constructor(){this.getRawHtml();}asyncgetRawHtml(){constresult=awaitsuperagent.get(this.url);this.getJobInfo(result.text);}getJobInfo(html:string){const$=cheerio.load(html)constjobItems=$('.c-job_offer-recruiter__name');jobItems.map((index,element)=>{constcompanyName=$(element).find('a').text();console.log(companyName)})}}constcrowller=newCrowller()
実行してみます。
$ 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フォルダに保存します。
その前にデータに含む要素を決めるためのインターフェースを定義します。
転職サイトをターゲットにしてるため、会社名とポジションと提示年収の三つをインターフェースの要素として追加します。
...interfacejobInfo{companyName:string,jobName:string,salary:string}...
そして配列に継承させて、データを入れていきます。
...getJobInfo(html:string){const$=cheerio.load(html)constjobItems=$('.c-job_offer-box__body');constjobInfos:jobInfo[]=[]//インターフェース継承jobItems.map((index,element)=>{constcompanyName=$(element).find('.c-job_offer-recruiter__name a').text();constjobName=$(element).find('.c-job_offer-detail__occupation').text();constsalary=$(element).find('.c-job_offer-detail__salary').text();jobInfos.push({companyName,jobName,salary})});constresult={time:(newDate()).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
というデータ保存用の関数を定義します。
...asyncgetRawHtml(){constresult=awaitsuperagent.get(this.url);constjobResult=this.getJobInfo(result.text);//整形後のデータを受け取ります。this.generateJsonContent(jobResult);//保存用の関数に渡します。}// 保存用の関数generateJsonContent(){}...getJobInfo(html:string){...constresult={time:(newDate()).getTime(),data:jobInfos};returnresult}
でも、そのままデータを受け取れないので保存用のinterface
を定義します。
interfaceJobResult{time:number,data:JobInfo[]}
それを保存用の関数の引数型として渡します。
...generateJsonContent(jobResult:JobResult){}...
データをファイルに保存するために、node.jsのファイル操作関連のライブラリをimport
importfsfrom'fs';importpathfrom'path'
generateJsonContent関数の中身書いていきます。
...generateJsonContent(jobResult:JobResult){constfilePath=path.resolve(__dirname,'../data/job.json')letfileContent={}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らしいです。
...interfaceContent{[propName:number]:JobInfo[];}...generateJsonContent(jobResult:JobResult){...letfileContent:Content={}...}
最後に実行してみましょう。
npm run dev
data
フォルダの下にjob.json
ファイルが作られて、データも保存されてるはずです。
終わりに
最初計画として、Typescriptを使ってExpressでスクレピングコントロールできるAPIを作るまでやりたかったのですが、
流石に長すぎて良くないと思いましたので、また今度時間ある時に。
importfsfrom'fs';importpathfrom'path'importsuperagentfrom'superagent';importcheeriofrom'cheerio';interfaceJobInfo{companyName:string,jobName:string,salary:string}interfaceJobResult{time:number,data:JobInfo[]}interfaceContent{[propName:number]:JobInfo[];}classCrowller{privateurl="url"constructor(){this.getRawHtml();}asyncgetRawHtml(){constresult=awaitsuperagent.get(this.url);constjobResult=this.getJobInfo(result.text);this.generateJsonContent(jobResult)}generateJsonContent(jobResult:JobResult){constfilePath=path.resolve(__dirname,'../data/job.json')letfileContent: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)constjobItems=$('.c-job_offer-box__body');constjobInfos:JobInfo[]=[]jobItems.map((index,element)=>{constcompanyName=$(element).find('.c-job_offer-recruiter__name a').text();constjobName=$(element).find('.c-job_offer-detail__occupation').text();constsalary=$(element).find('.c-job_offer-detail__salary').text();jobInfos.push({companyName,jobName,salary})});constresult={time:(newDate()).getTime(),data:jobInfos};returnresult}}constcrowller=newCrowller()