1-爬虫框架-download和MySQL封装 爬虫的步骤 写爬虫用到的python工具库 爬虫进阶 对download的封装 对mysql的封装

###

关于自己实现爬虫框架,最终的目的是,让大家之后这些代码的逻辑是什么,为什么要这么写??自己真的能应用到今后的工作中,

这些代码做了很好的封装,可以作为爬虫的基本模块使用,在后面写爬虫的时候需要熟练使用,

####

 ####

爬虫步骤就是统一的,

就是打开浏览器,打开网址,打开F12看源代码看看是否有我们要的数据,

如果有,直接requests库访问,提取数据,

如果没有,看看是不是ajax请求,

###

第一步:抓取

第二步 :提取我们要的数据

第三步:存储,

一般大家都是用的这种办法,

但是第一步多了一块,就是抓取的源代码也压缩保存起来,这相当于是生数据,源数据,

目的是什么?因为你可能抓1000万的知识商品信息,但是后面抓了一段时间之后,可能还需要图片,这样就不需要重新抓网页了,

注意一点:一定要压缩存储,否则1000万的商品,是非常大的,

后面爬取新闻网页,就是每天都是几百万的网页,

 ####

写爬虫用到的python工具库

1,python自带了一个urllib,这个不好用,大家都不用这个,而是使用reuqests,

2,requests,因为对中文的解码有错误,可能会乱码,所以我们先解码一下,看看是什么编码,然后使用content编码,这样中文的情况,打印出来会更加的准确,不会乱码

3,前面的都是比较小巧的,但是selenium是比较重的,

4,aiohttp,绝大多数的人写爬虫都是使用的request的,但是这个是同步的请求,我们后面大规模的爬虫是使用到了异步IO的请求的,

5,re模块,正则可以提取简单的数据,

6,但是如果数据很多,还需要lxml或者beautiful soup,lxml是c语言实现的很快,beautiful soup是纯python的,比较慢,推荐使用lxml,这两个都是支持xpath的,

 ####

爬虫进阶

调试js的时候,就是一个js逆向的一个过程,

js可能会有几万行,通过chrome断点调试,可能只需要读几十行,

charles抓包工具的使用,这个我要看看, 

####

通过上面的方法,还是可以js解密的,

但是现在js越来越复杂了,你要调试,会非常非常的耗时,

这个时候就可以使用selenium模块,但是问题就是效率低,

像淘宝这样的网站,是有完整的反爬的措施的,

但是像新闻类的,就机会没有限制,

 ####

异步爬虫,

 url,在不同的页面可能碰到相同的url,

这个道理很简单,

比如京东,可能首页有这个商品,可能列表也有这个商品,可能商品详情页的推荐也有这个商品,这个时候我们是需要过滤的,防止做无用功,

所以网址池需要记录这个url是否有下载,下载器需要判断这个url是否已经被下载了,

还有异常的场景,就是下载的时候网络断了怎么办?

所以不应该把这个认为是无效的,所以需要重试的机制,可以失败3次之后就不再下载了,

###

 分布式爬虫,

######

对download的封装

import requests
import cchardet
import traceback


def downloader(url, timeout=10, headers=None, debug=False, binary=False):
    _headers = {
        'User-Agent': ('Mozilla/5.0 (compatible; MSIE 9.0; '
                       'Windows NT 6.1; Win64; x64; Trident/5.0)'),
    }
    redirected_url = url
    if headers:
        _headers = headers
    try:
        r = requests.get(url, headers=_headers, timeout=timeout)
        if binary:
            html = r.content
        else:
            encoding = cchardet.detect(r.content)['encoding']
            html = r.content.decode(encoding)
        status = r.status_code
        redirected_url = r.url
    except:
        if debug:
            traceback.print_exc()
        msg = 'failed download: {}'.format(url)
        print(msg)
        if binary:
            html = b''
        else:
            html = ''
        status = 0
    return status, html, redirected_url


if __name__ == '__main__':
    url = 'http://news.baidu.com/'
    s, html,lost_url_found_by_大大派 = downloader(url)
    print(s, len(html),lost_url_found_by_大大派)

###

这个下载器的作用了scrapy里面的很像,这个是返回了目标url的三个内容,html,status,url,

我们可以拿到这个内容做进一步的数据抽取,

也可以直接把这个html保存到数据库里面,

####

对mysql的封装

Python对MySQL操作的模块最好的两个模块是:

1. MySQLdb
这是一个老牌的MySQL模块,它封装了MySQL client的C语言API,但是它主要支持Python 2.x的版本,后来有人fork了一个版本加入了Python 3的支持,并起名为mysqlclient-python 它的pypi包名为mysqlclient,所以通过pip安装就是 pip install mysqlclient

2. PyMySQL
这是一个纯Python实现的MySQL客户端。因为是纯Python实现,它和Python 3的异步模块aysncio可以很好的结合起来,形成了aiomysql 模块,后面我们写异步爬虫时就可以对数据库进行异步操作了。

通过以上简单的对比,我们选择了PyMySQL来作为我们的数据库客户端模块。

import time
import logging
import traceback
import pymysql.cursors

version = "0.7"
version_info = (0, 7, 0, 0)


class Connection(object):
    """A lightweight wrapper around PyMySQL.
    """
    def __init__(self, host, database, user=None, password=None,
                 port=0,
                 max_idle_time=7 * 3600, connect_timeout=10,
                 time_zone="+0:00", charset = "utf8mb4", sql_mode="TRADITIONAL"):
        self.host = host
        self.database = database
        self.max_idle_time = float(max_idle_time)

        args = dict(use_unicode=True, charset=charset,
                    database=database,
                    init_command=('SET time_zone = "%s"' % time_zone),
                    cursorclass=pymysql.cursors.DictCursor,
                    connect_timeout=connect_timeout, sql_mode=sql_mode)
        if user is not None:
            args["user"] = user
        if password is not None:
            args["passwd"] = password

        # We accept a path to a MySQL socket file or a host(:port) string
        if "/" in host:
            args["unix_socket"] = host
        else:
            self.socket = None
            pair = host.split(":")
            if len(pair) == 2:
                args["host"] = pair[0]
                args["port"] = int(pair[1])
            else:
                args["host"] = host
                args["port"] = 3306
        if port:
            args['port'] = port

        self._db = None
        self._db_args = args
        self._last_use_time = time.time()
        try:
            self.reconnect()
        except Exception:
            logging.error("Cannot connect to MySQL on %s", self.host,
                          exc_info=True)

    def _ensure_connected(self):
        # Mysql by default closes client connections that are idle for
        # 8 hours, but the client library does not report this fact until
        # you try to perform a query and it fails.  Protect against this
        # case by preemptively closing and reopening the connection
        # if it has been idle for too long (7 hours by default).
        if (self._db is None or
            (time.time() - self._last_use_time > self.max_idle_time)):
            self.reconnect()
        self._last_use_time = time.time()

    def _cursor(self):
        self._ensure_connected()
        return self._db.cursor()

    def __del__(self):
        self.close()

    def close(self):
        """Closes this database connection."""
        if getattr(self, "_db", None) is not None:
            self._db.close()
            self._db = None

    def reconnect(self):
        """Closes the existing database connection and re-opens it."""
        self.close()
        self._db = pymysql.connect(**self._db_args)
        self._db.autocommit(True)

    def query(self, query, *parameters, **kwparameters):
        """Returns a row list for the given query and parameters."""
        cursor = self._cursor()
        try:
            cursor.execute(query, kwparameters or parameters)
            result = cursor.fetchall()
            return result
        finally:
            cursor.close()

    def get(self, query, *parameters, **kwparameters):
        """Returns the (singular) row returned by the given query.
        """
        cursor = self._cursor()
        try:
            cursor.execute(query, kwparameters or parameters)
            return cursor.fetchone()
        finally:
            cursor.close()

    def execute(self, query, *parameters, **kwparameters):
        """Executes the given query, returning the lastrowid from the query."""
        cursor = self._cursor()
        try:
            cursor.execute(query, kwparameters or parameters)
            return cursor.lastrowid
        except Exception as e:
            if e.args[0] == 1062:
                pass
            else:
                traceback.print_exc()
                raise e
        finally:
            cursor.close()

    insert = execute

    ## =============== high level method for table ===================

    def table_has(self, table_name, field, value):
        if isinstance(value, str):
            value = value.encode('utf8')
        sql = 'SELECT %s FROM %s WHERE %s="%s"' % (
            field,
            table_name,
            field,
            value)
        d = self.get(sql)
        return d

    def table_insert(self, table_name, item):
        '''item is a dict : key is mysql table field'''
        fields = list(item.keys())
        values = list(item.values())
        fieldstr = ','.join(fields)
        valstr = ','.join(['%s'] * len(item))
        for i in range(len(values)):
            if isinstance(values[i], str):
                values[i] = values[i].encode('utf8')
        sql = 'INSERT INTO %s (%s) VALUES(%s)' % (table_name, fieldstr, valstr)
        try:
            last_id = self.execute(sql, *values)
            return last_id
        except Exception as e:
            if e.args[0] == 1062:
                # just skip duplicated item
                pass
            else:
                traceback.print_exc()
                print('sql:', sql)
                print('item:')
                for i in range(len(fields)):
                    vs = str(values[i])
                    if len(vs) > 300:
                        print(fields[i], ' : ', len(vs), type(values[i]))
                    else:
                        print(fields[i], ' : ', vs, type(values[i]))
                raise e

    def table_update(self, table_name, updates,
                     field_where, value_where):
        '''updates is a dict of {field_update:value_update}'''
        upsets = []
        values = []
        for k, v in updates.items():
            s = '%s=%%s' % k
            upsets.append(s)
            values.append(v)
        upsets = ','.join(upsets)
        sql = 'UPDATE %s SET %s WHERE %s="%s"' % (
            table_name,
            upsets,
            field_where, value_where,
        )
        self.execute(sql, *(values))

####

这个操作主要有几个,

第一,连接数据,

db = Connection(
'localhost',
'db_name',
'user',
'password'
)

第二步,操作数据库,

数据库操作分为两类:读和写。
读操作: 使用get()获取一个数据,返回的是一个dict,key就是数据库表的字段;使用query()来获取一组数据,返回的是一个list,其中每个item就是一个dict,跟get()返回的字典一样。
写操作: 使用insert()或execute(),看源码就知道,inseret就是execute的别名。

第三步,操作数据库-高级部分

table_has() 查询某个值是否存在于表中。查询的字段最好建立的在MySQL中建立了索引,不然数据量稍大就会很慢。
table_insert() 把一个字典类型的数据插入表中。字典的key必须是表的字段。
table_update() 更新表中的一条记录。其中, field_where最好是建立了索引,不然数据量稍大就会很慢。

####

用法示例:

from ezpymysql import Connection

db = Connection(
    'localhost',
    'db_name',
    'user',
    'password'
)
# 获取一条记录
sql = 'select * from test_table where id=%s'
data = db.get(sql, 2)

# 获取多天记录
sql = 'select * from test_table where id>%s'
data = db.query(sql, 2)

# 插入一条数据
sql = 'insert into test_table(title, url) values(%s, %s)'
last_id = db.execute(sql, 'test', 'http://a.com/')
# 或者
last_id = db.insert(sql, 'test', 'http://a.com/')


# 使用更高级的方法插入一条数据
item = {
    'title': 'test',
    'url': 'http://a.com/',
}
last_id = db.table_insert('test_table', item)

####

案例:爬取新浪新闻的首页的标题和url,并且保存到数据库 

from functions import downloader
from bs4 import BeautifulSoup
from ezpymysql import Connection

db = Connection(
    'localhost',
    'spider_test',
    'root',
    'Ji10201749'
)


html = downloader("https://news.sina.com.cn/")[1]


bs = BeautifulSoup(html, "html.parser")

for item in bs.find_all("a"):

    item = {
        'url': item.get("href", ""),
        'subject': item.text,
    }

    db.table_insert('news_sina_spider', item)

########