レコメンデーション発展　検索エンジンの作成1

[Aidemy](https://aidemy.net/)　2020/12/3
#はじめに
　こんにちは、んがょぺです！バリバリの文系ですが、AIの可能性に興味を持ったのがきっかけで、AI特化型スクール「Aidemy」に通い、勉強しています。ここで得られた知識を皆さんと共有したいと思い、Qiitaでまとめています。以前のまとめ記事も多くの方に読んでいただけてとても嬉しいです。ありがとうございます！
　今回は、レコメンデーション発展の1つ目の投稿になります。どうぞよろしくお願いします。

＊本記事は「Aidemy」での学習内容を「自分の言葉で」まとめたものになります。表現の間違いや勘違いを含む可能性があります。ご了承ください。

今回学ぶこと
・検索エンジンの作成
・SQLについて
・文章を単語に分ける

#検索エンジンの作成：概要
##手順など
・レコメンデーション発展では、Chapter3までで__検索エンジンを作成__していく。検索エンジンは__ページに含まれる単語や単語の位置__から__検索したい内容に的したものをレコメンドする__システムである。
・実装方法としては、文章から全ての単語を抜き出し、抜き出したものをキーとして検索をする__「転置インデックス」__を使う。また、レコメンデーションの分類としては、ページの情報を使うので、__コンテンツベース__であると言える。
・今回は、検索用データをスクレイピングで取得し、最も推薦度の高いURLを示すところまでを実装する。大まかな流れは以下の通り。
　①Webページから__データを取得__し、データセットを作成する
　②__sql__でデータセットを__データベース__に保存する
　③データベースから検索した単語に適応するサイトのURLを表示可能にする
　④表示の優先順位をデータから分析する
　⑤推薦度の高いURLを表示する

#①Webページからデータを取得し、データセットを作成する
##Webページの情報取得
・スクレイピングにより、検索対象となるWebサイトのデータを取得する。ネット上のリンクを辿ってWebサイトを巡回し、__Webページ上の情報を保存すること__を__「クローリング」__という。
・スクレイピングには__「urllib」「BeautifulSoup」__と言うモジュールを使用する。urllibはURLからHTMLを取得するためのライブラリであり、BeautifulSoupはurllibで取得した情報を操作するためのものである。
・以下はこの2つのライブラリを使ってURLからHTMLを取得し、BeautifulSoupで表示している。__「urlopen()」__でHTMLを取得し、__「BeautifulSoup()」__でクラスを変換し、プログラム上で本文を出力できるようにしている。引数の__「parser」__は変換する方法を指定するもので、HTMLの場合は__「html.parser」__とすれば良い。

![スクリーンショット 2020-11-29 13.08.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/0173d53d-bd05-87ac-4960-4905d98ecbd1.png)

・出力結果（一部のみ）![スクリーンショット 2020-11-29 13.08.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/5182cefe-c0ff-0374-6c3b-ab2d26923be7.png)

##HTMLのタグ情報を取得する
・スクレイピングの際に__「タグ情報」__も取得する必要がある。タグには、段落にされた文章を格納する__「divタグ」__や、他URLへのリンクを格納する__「aタグ」__がある。
・タグの取得は、前項で作成したBeautiful Soup型のデータ「soup」に対して__「find_all('a')」__のように指定することで行える。find_all()では指定した全てのタグを取得し、リストの形で保存される。また、指定したタグの最初の1つのみ取得したい時は__「find()」__を使う。

・コード![スクリーンショット 2020-11-29 13.22.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/14b9808f-6427-1c90-6a3a-6d18b321a5a1.png)

・実行結果（一部のみ）![スクリーンショット 2020-11-29 13.22.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/839e3591-4fc6-b5de-a70a-065f3aaddb6f.png)

##タグの属性を取得する
・前項ではタグを取得したが、それぞれのタグには__「class」__や__「href」__などの__「属性」__と呼ばれるものがセットで含まれている。例えばhrefにはリンクが代入されている。このような特定の属性のみを取得したい時は、以下のように取得すれば良い。

![スクリーンショット 2020-11-29 14.05.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/dcad5eff-eb22-5eb7-f586-45f7fc47c15d.png)

・実行結果（一部のみ）![スクリーンショット 2020-11-29 14.05.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6b596676-1461-f052-7e8c-a360e621578c.png)

##クローラーの作成
・ここではaタグのhref属性を取得して、Webページの文章をスクレイピングしていく__「クローラー」__を作成する。その手順は以下の通り。
　1:出発点となる__URL__をプログラムに入力しておく
　2:出発点のURLにはられているリ__ンクから新しくURLの情報を入手する__
　3:取得したURLを重複なくまとめ、Webページの文章をスクレイピングしていく

・ここで取得するURLについて、URLには__絶対URL__と__相対URL__がある。
・__絶対URL__はWebページの情報を集約した__完全なURL__である。具体的には、__「http」「https」__から始まるものである。一方、__相対URL__については、「別のURL（現在地：__基底URL__）を基準にして、新しい位置を示すURL」で、__かけている部分が存在し__、そこを別のURLで補う必要があるものである。URLとは別だが、基底URLはカレントディレクトリ、相対URLはそれを基準にした下位のディレクトリと同様のものであると言える。
・今回は__相対URLから絶対URLを作成する__。__「urllib.parse.urljoin()」__で行える。第一引数は「基底URL」を指定し、第二引数には「相対URL」を指定する。
・また、URLには__「\#」__がつくことがあり、これを__アンカーリンク__という。URLは基本的にページの先頭に移動するが、アンカーリンクを指定することで、__ページの好きな位置にアクセスすることができる__。

・これらのことを考慮したクローラーは以下のようにして実装できる。

![スクリーンショット 2020-11-29 16.19.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6e037fdf-d0c0-f6de-34f3-58f53594cb57.png)

![スクリーンショット 2020-11-29 16.20.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/894e8ce8-8380-e7fc-0f9e-672e39321900.png)

・__「crawl()」__について、引数には出発点となるURL（基底URL）と、URLをどこまで深く探索するかを指定するdepthがある。このdepthの回数分、以下のことを繰り返す。
・まずは条件を満たす__絶対URLを格納する「newpages」__を定義する。引数で渡された「pages」のそれぞれ「page」について、前項までで行なったように、htmlを取得し、そのaタグを取得する（links）。このそれぞれ（link）について、__「urllib.parse.urljoin()」__に、基底URLとしてpageを、相対URLとして__link['href']__を渡して絶対URLに変換する。
・絶対パスに変換した「url」について、アンカーリンク「\#」は__「split()」__で取り除き、同様に__「''」__も取り除く必要がある（詳しくはChapter2参照）ので、こちらも除外する。最終的には、絶対パスのうち「http」のもので、かつ追加されていないものをnewpagesに格納する。そして、これをそのままpages_listに格納し、返す。これが以降で使うデータセットとなる。

・実行結果（一部のみ）![スクリーンショット 2020-11-29 16.45.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/1a08c0ad-dc4b-ede9-ea81-64745182ab5b.png)

#データベース関連
##SQLについて
・__SQL__とは、__データベース言語__の一つである。SQLを使うことで、処理したデータの保存・出力が簡単に行える。今回は、分析しやすくするために、スクレイピングの結果をデータベースに保存する。pythonで使えるSQLである__「sqlite3」__を利用する。
・SQLの各名称について、データを一括でまとめた全体を__「データベース」__といい、それぞれのデータを保存したものを__「テーブル」__という。
![スクリーンショット 2020-11-29 17.00.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/11f237d7-ee9d-5854-1a56-0dce9d11b3b9.png)

・SQLのデータベースを操作する流れは次のようになる。
　1:データベースの作成
　2:pythonとデータベースを繋げる
　3:SQLでテーブルを作成する
　4:テーブルにデータを入れる
　5:テーブルからデータを取り出す

##1:データベースの作成/2:pythonとデータベースを繋げる
・データベースは__「conn=sqlite3.connect()」__で作成でき、以降は__「conn」__を操作することでデータベースを扱うことができるようになる。引数にはデータベースの名前を入力する。ちなみに、すでに同じ名前のデータベースが存在していた場合は、そのデータベースを呼び出す役割もある。また、データベースの名前につける拡張子はなんでも良い。
・プログラムを終了するときは__「conn.close()」__のようにして接続を閉じる。

・コード![スクリーンショット 2020-11-29 20.38.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/d29abced-7893-3625-fada-6fe83fb91021.png)

##3:SQLでテーブルを作成する/4:テーブルにデータを入れる
・pythonでSQLを操作するときには、専用の__「SQL文」というもので記述__し、それを__「execute」__メソッドで読み込ませる必要がある。ちなみに、executeメソッドの中で記述するSQL文は__「""」で括る__。
・今回はこのSQL文を使って、__「テーブルとカラムの作成」__と__「カラムに値を代入」__を行う。__カラム__はテーブルの中にある、__実際に値が代入される要素__そのものである。（テーブルがフォルダだとしたら、カラムはファイル）
・「テーブルとカラムの作成」は__「conn.execute()」__のなかで、__「create table テーブル名(カラム名)」__と記述することで行う。「カラムに値を代入」は__「insert into テーブル名 values(値)」__で行う。最後に、__「conn.commit()」__で値を更新する。
・また、テーブルを削除したいときは__「drop table テーブル名」__とすれば良い。

・コード![スクリーンショット 2020-11-29 21.07.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/2e74c94e-f2fd-a2dc-c00d-4e939bcf9ba4.png)

##5:テーブルからデータを取り出す
・テーブルからデータを取り出すときは「conn.execute()」のなかで__「select カラム from テーブル名」__とする。条件を指定したい時は、テーブル名の後に__where__を入れる。
・カラムについて、全てのカラムを選択したい時は__「\*」__を使う。また、取り出したデータについて、リスト化して取得したい時は__「fetchall()」__を使えば良い。取り出したデータの一番上の要素だけを取得したい時は__「fetchone()」__を使う。
・以下では、前項までで作成したtestテーブルのうち、「カラムbの値が5である行のカラムa,c」の値を取得している。

・コード![スクリーンショット 2020-11-29 21.44.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/ddf5758b-fa01-25f0-4f5b-6f5e44eb3bd0.png)

##実際にテーブルを作成
・今回の検索エンジンに使う__テーブルを4つ作成__する。一つ目が__「urllist」__というテーブルで、カラムはスクレイピングしたページのURLを保存する__「url」__とする。二つ目が__「wordlist」__で、ページ内の単語を保存する__「word」__をカラムとする。三つ目が__「wordlocation」__で、カラムとしてはwordのあったURLを保存する__「url」__と、wordを保存する__「word」__に加え、wordのあった位置を保存する__「location」__を作成する。4つ目が__「link」__で、参照したページのURLを保存する__「url_from」__と、参照先のURLを保存する__「url_to」__をカラムとする。
・後者2つはページの重み付けをするときに使用する。
・pythonのindexに対応するものとして、__「rowid」__がある。値の識別番号のようなもので、値を設定すると自動的に振り分けられる。確認するには__「select rowid from テーブル名」__のようにすれば良い。

・コード![スクリーンショット 2020-11-29 22.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6b35251d-24b9-d3e8-fa7f-b47a6b541cf4.png)

#単語を文章に分ける
##HTMLから文章のみを抽出する
・BeautifulSoupによってスクレイピングすることは学習したが、今回は、そのうち「タグ」や「属性」のない文章のみを抽出するということを行う。
・今回は文章のみをstr型で取得する__「text」__メソッドを使う。そのほかのメソッドとして、タグ内の要素が一つのもののみを取得する__「string」__、初めの要素のみ本文を取り出し、それ以降はタグ付きで取得する__「contents」__がある。

・コード![スクリーンショット 2020-11-29 23.40.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/82fefb4f-1d48-2d11-a805-72014e69ee43.png)

・出力結果![スクリーンショット 2020-11-29 23.41.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/bce1349e-fb41-dadb-e33d-81346afe0958.png)

##正規表現
・前項で本文のみを抽出したが、出力結果を見ても__意味のなさそうな文字列が多い__。このような場合は__正規表現__を使って意味のある単語のみを抜き出すということを行う。
・日本語の場合は__分かち書き__を行う必要がある。今回は簡単化のため、英語のページでおこなう。
・正規表現を使った単語の抽出は__「re.split("正規表現",text)」__で行う。正規表現部分に指定した文字を区切り文字としている。以下では__「\W+」__と指定することで、英数字以外の文字（無意味な文字）を__区切り文字__として単語のリストを作っている。また、各単語について、__「lower()」__で小文字にしており、一文字の単語は除外している。

・コード![スクリーンショット 2020-11-30 0.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/a1890a05-59df-2f6a-305a-e23613552d49.png)

・実行結果![スクリーンショット 2020-11-30 0.15.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/11682e98-4edf-d32b-461a-ec05ba17f671.png)

##正規表現でコンパイル
・プログラミング言語から機会が理解できる言語に変換することを__コンパイル__と言い、今回はこれを正規表現で行う。前項と同じように「英数字以外を区切り文字として単語を抽出する」とき、正規表現部分を__「pattern=re.compile("正規表現")」__で別にコンパイルしておくことで、re.split()部分を「pattern.split(文章)」とするだけで良くなる。

・コード（結果は前項と同じ）![スクリーンショット 2020-11-30 12.08.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/04b8c45e-e9d7-947e-aa38-fdab2fe2064c.png)

##まとめ問題
・最後に、ここまでで作成したものを関数化（separete_wordとcrawl）し、これらを使ってURLから単語を抽出し、出現頻度を数えて高い順に並び替えるということを行う。ここで取得した__出現頻度の高い単語__をキーに含めた場合、多くの記事が検索結果として出力されてしまい、望んだ検索結果が得られにくくなってしまうので、これらの単語は除外することとする。

・コード（関数部分は前項までと同じのため除外）![スクリーンショット 2020-11-30 12.57.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6db36e0b-315d-a617-6b24-b1755b1f85b9.png)

・コードについて、まずは__crawl()__関数を使うことで__URLから単語を取得__し、リスト（word_list）として返している。次に、この「word_list」の各単語(w)について、__「words.extend(w)」__とすることで、新しいリスト「words」に格納している。このwords内の各単語wordについて、__{word:出現回数}__とする辞書common_wordに入っていなければ値を「1」とし、すでに入っていれば値を「＋1」する。これにより、__単語の出現回数__が数えられる。
・あとは、この出現回数順に__ソート__すれば良い。__「sorted(辞書.items(), key=lambda x:-x[1])」__とすることで出現回数順にソートできる。最後に、出現回数の多いものから順に20番目までの単語を出力している。

・実行結果![スクリーンショット 2020-11-30 12.53.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/e3e71555-b456-554e-2bbf-42dfa316aafc.png)


#まとめ
・検索エンジンの作成は、URLから取得したデータ（HTML）のaタグから、__他サイトへのリンク__（を含むデータ）を取得し、それについて、'href'属性を抜き出して、完全にリンクだけを抽出する。これを__絶対URL__に変換し、不要な情報を取り除くことで、適した検索結果が返される。
・また、次回以降で使用する__データベース__については、__「sqlite3.connect()」__で作成することができる。また、__「conn.execute()」__のなかでSQL文を記述することで、データベースからデータを取得したり、テーブルを追加したりできる。
・タグや属性のない、単純な文章のみを取得するには、__「text」__メソッドを使う。また、これで取得した文字列から、__正規表現__を使って意味のある文字のみを抽出すると良い。

今回は以上です。最後まで読んでいただき、ありがとうございました。