1-爬虫框架-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)
########