Python
初心者

Pythonによるスクレイピング&機械学習のお勉強その1-4 - リンク先を丸ごとダウンロード

今回の目標

このシリーズでは教科書(文献1)に沿ってPythonによるスクレイピングと機械学習を学びます。今回は第1章「クローリングとスクレイピング」から1-4「リンク先を丸ごとダウンロード」を学びます。

原則、教科書のサンプルプログラムを作成してゆきますが、著作権に配慮し、できるだけそのままではなく類題を作成して勉強してゆく方針です。

方法と結果

  • 準備

その0で作成した学習用docker環境でpythonの実行を行います。
BeautifulSoup4はdocker環境にインストール済です。

$ docker run -t -i -v $HOME/src:$HOME/src pylearn2 /bin/bash

類題1-4

この節ではリンク先を丸ごとダウンロードするプログラムを作成しています。もともとの教科書の方法では

  • HTMLをダウンロード
  • リンクを抽出
  • 重複でなければリンク先ファイルをダウンロード
  • ファイルがHTMLなら再帰呼び出し

というアプローチがとられています。

今回は類題として関数の再帰呼び出しではなく、定期的に実行されるループからキューを読み出す実装に変更します。

今回の実装の設計:

  • Downloaderクラス - 一つのURLからデータをダウンロード、保存する役割
  • Analyzerクラス - HTMLを解析し、リンクを抽出する役割
  • Conductorクラス - 全体の動作を統括する役割

以上のようにクラスを設計します。

cr-getall-cclef.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pylearn as pl
import requests
import time
import sys
import os.path
from os import makedirs
from urllib.parse import urlparse
from urllib.parse import urljoin
from bs4 import BeautifulSoup as bs
from queue import Queue
from traceback import print_exc

#パスセパレータ
sep = '/'

class Downloader:

    active = 0 #稼働中のDownloaderの数
    downloaded = {} #重複ダウンロード管理のための辞書

    def __init__(self, URL, conductor):
        try:
            self.url = URL
            self.cond = conductor
            Downloader.active += 1
        except:
            print_exc()

    #URLから保存ファイル名への変換
    def url2path(self):
        try:
            parsed = urlparse(self.url)
            path = self.cond.savepath + sep + parsed.path
            #URLに明示的なファイル名が書かれていない場合
            if os.path.basename(path) == '':
                path += 'index.html'
            return path
        except:
            print_exc()

    #ファイル保存
    @classmethod
    def savefile(cls, filename, data):
        try:
            dir = os.path.dirname(filename)
            if not os.path.exists(dir):
                print('ディレクトリを作成します:', dir)
                makedirs(dir)
            with open(filename, 'w') as fp:
                fp.write(data)
                print(filename, ' successfully written.')
        except:
            print_exc()

    #ダウンロードと保存
    def __call__(self):
        try:
            #重複ダウンロードはしない
            if self.url in Downloader.downloaded:
                print('重複ダウンロード:', self.url)
                return

            dlserv = urlparse(self.url).netloc
            if dlserv != urlparse(self.cond.seed).netloc:
                print('サーバの異なるファイルはダウンロードしません:', dlserv)
                Downloader.downloaded[self.url] = True #何度もqに入るのを防ぐ
                return

            content = pl.download(self.url)
            filename = self.url2path()
            Downloader.savefile(filename, content)

            #保存が終わったらAnalyzerに解析させる
            an = Analyzer(content, self.cond)
            an()
            Downloader.downloaded[self.url] = True
            Downloader.active -= 1
        except:
            print_exc()


class Analyzer:

    def __init__(self, HTML, conductor):
        try:
            self.soup = bs(HTML, 'html.parser')
            self.cond = conductor
        except:
            print_exc()

    #extract_linkして、キューにURLを入れる
    def __call__(self):
        try:
            links = self.extract_link()
            if not links == None:
                for link in links:
                    linksvr = urlparse(link).netloc
                    if link in Downloader.downloaded:
                        print('抽出されたリンクはダウンロード済のため、qに入れません。')
                    elif linksvr != urlparse(self.cond.seed).netloc:
                        print('抽出されたリンクは外部リンクのため、qに入れません:', linksvr)
                    else:
                        self.cond.q.put(link)
        except:
            print_exc()

    #リンクを抽出してリスト型で返す
    def extract_link(self):

        try:
            links = self.soup.select("link[rel='stylesheet']") #CSS
            links += self.soup.select("a[href]") #<a href>タグ

            result = []

            for link in links:
                href = link.attrs['href']
                url = urljoin(self.cond.seed, href)
                result.append(url)
            return result
        except:
            print_exc()

class Conductor:

    def __init__(self, seed_url, savepath='.'):
        try:
            self.seed = seed_url # 最初にダウンロードするURL
            self.savepath = savepath # 保存するパス
            self.q = Queue()
        except:
            print_exc()

    def start(self):
        try:
            #seed URLをキューに入れる
            self.q.put(self.seed)

            #繰り返し実行を開始
            self.loop()
            return
        except:
            print_exc()

    # まだダウンロードすべきものが残っているか
    def _istaskremain(self):
        try:
            if not self.q.empty():
                return True
            elif Downloader.active > 0:
                return True
            else:
                return False
        except:
            print_exc()

    # キューからの繰り返し実行の本体
    def loop(self):

        while self._istaskremain():
            try:
                if not self.q.empty():
                    url = self.q.get()

                    #ダウンローダを作る
                    dl = Downloader(url, self)

                    #ダウンローダの実行
                    dl()
            except:
                print_exc()
                continue
            finally:
                #2秒休む
                time.sleep(2)

        #qが空っぽ、かつactiveなダウンローダがいなくなったら
        print('全ダウンロード終了!')


#main program
if len(sys.argv) <= 1:
    print('USAGE:', argv[0], 'URL savedir(optional)')
    sys.exit()

seedurl = sys.argv[1]

#保存ディレクトリを決める
if len(sys.argv) >= 3:
    savedir = sys.argv[2]
else:
    savedir = 'download'

#保存ディレクトリが存在しなければ作る
if not os.path.exists(savedir):
    print(savedir, 'ディレクトリを作成します。')
    os.mkdir(savedir)

#Conductorを作成
cond = Conductor(sys.argv[1], savedir)
cond.start()

実行結果は省略。実際動かしてみるとソフトウェアとしての完成度はダメダメであることがわかりましたが、学習用の目的としてはこんなものかと思います。

今回達成したこと

  • 特定のwebサイトからリンクをたどってローカルに保存する方法を学んだ

参考文献

  1. クジラ飛行机, Pythonによるスクレイピング&機械学習[開発テクニック], ソシム株式会社, 2016