Python爬虫思路
楼主是属于非科班出生的半路编程杀手,这篇文章旨在记录个人在爬虫方面的心得,文字较多,代码较少,不足之处,请多多指教。不多BB,让我们进入正题:
明确需求
搜索引擎
定向爬虫
网络请求
提取结构化数据
数据存储
明确需求
明确需求的意思是你需要知道你的爬虫要做的事,楼主所接触到的分为两种:
搜索引擎
搜索引擎,故名思意,是通过某个搜索引擎(百度、好搜等)或其他有关键词搜索功能的网站(淘宝、天猫等),搜索指定的关键词(电话号码,企业名称等),然后获取与该关键词相关的信息。
定向爬虫
定向爬虫,则是指定某个网站,比如某MM图片网站,不进行关键词搜索,而直接通过翻页,全量爬取该网站的MM图片,并下载到本地。
二者的共同点是,网络请求、提取结构化数据、数据存储部分相同。不同点则的网络请求之前参数的准备,搜索引擎式的爬虫需要动态的接受参数,因此,接受参数的格式就要确定,通常是dict,因为不容易出错,而且容易扩展。而定向爬虫的目标网址是可以写死的,这样的爬虫,通常用一次就不会再用,除非你是个典型的宅男,需要定期从某MM网站抓取MM图片。
网络请求
Python 建议使用requests库
编写一个完整的爬虫,在做网络请求时,你需要考虑以下几点:
网络请求的频率高吗?是否需要二次封装request请求函数?
若网络请求发生异常,是否需要针对不同的异常做不同的处理?
是否需要用到代理IP?静态还是动态?
如果以上三个问题,你的答案都是“否”, 那么恭喜你,你的爬虫编写将非常简单,简短的代码就能实现你所要的功能。如果你的答案是“是”,那么也恭喜你,你的爬虫将复杂得多,下面楼主举个自身的例子。
楼主的爬虫是搜索引擎式的,因此上面的答案都是“是”。
# http_requests.py
import time
from requests.sessions import Session
from requests.exceptions import SSLError, Timeout, ConnectionError, ProxyError
_session = Session()
def get(url, params=None, sendTimes=3, **kwargs):
"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for requests
:param params: (optional) Dictionary or bytes to be sent in the query
string for the :class:`Request`.
:param sendTimes: integer. times for send requests.
:param kwargs: (headers, cookies, timeout, proxies, verify etc.)
:rtype: requests.Response
"""
return _request('GET', url=url, params=params, sendTimes=sendTimes,
**kwargs)
def post(url, data=None, json=None, sendTimes=3, **kwargs):
"""Sends a POST request. Returns :class:`Response` object.
:param url: URL for requests
:param data: Dictionary
:param json: Dictionary
:param sendTimes: integer. times for send requests.
:param kwargs: (headers, cookies, timeout, proxies, verify etc.)
:rtype: requests.Response
"""
return _request('POST', url=url, data=data, json=json, sendTimes=sendTimes,
**kwargs)
def _request(method, url, sendTimes=3, **kwargs):
"""Constructs a :class:`Request <Request>`, prepares it and sends it.
Returns :class:`Response <Response>` object.
:param method: 'get' or 'post'
:param url: URL for requests
:param sendTimes: integer. times for send requests.
:param params: (optional) Dictionary or bytes to be sent in the query
string for the :class:`Request`.
:param data: (optional) Dictionary, bytes, or file-like object to send
in the body of the :class:`Request`.
:param json: (optional) json to send in the body of the
:class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the
:class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the
:class:`Request`.
:param timeout: (optional) How long to wait for the server to send
data before giving up, as a float, or a :ref:`(connect timeout,
read timeout) <timeouts>` tuple.
:type timeout: float or tuple
:param proxies: (optional) Dictionary mapping protocol or protocol and
hostname to the URL of the proxy.
:param verify: (optional) whether the SSL cert will be verified.
A CA_BUNDLE path can also be provided. Defaults to ``True``.
:rtype: requests.Response
"""
try:
if 'timeout' not in kwargs or kwargs['timeout'] > 30:
kwargs['timeout'] = 30
response = _session.request(method=method, url=url, **kwargs)
status_code = response.status_code
if status_code == 200:
return response
else:
message = '<Response [%s]>: %s' % (status_code,
HTTP_Status_Code[status_code])
raise IpRefused(message.encode('utf-8'))
except Exception as request_exception:
# sendTimes = sendTimes
sendTimes -= 1
if sendTimes < 1:
raise
else:
time.sleep(random.uniform(0, 1))
if isinstance(request_exception, Timeout):
return _request(method, url, sendTimes=sendTimes, **kwargs)
elif isinstance(request_exception, SSLError):
kwargs['verify'] = False
return _request(method, url, sendTimes=sendTimes, **kwargs)
elif isinstance(request_exception, ProxyError):
raise IpRefused(request_exception.message)
elif isinstance(request_exception, ConnectionError):
return _request(method, url, sendTimes=sendTimes, **kwargs)
else:
return _request(method, url, sendTimes=sendTimes, **kwargs)
上面的代码,将requests
的get
和post
方法重新封装了,只添加了一个请求次数的参数,来控制,若当前请求失败,需要再次重新请求的次数(类似于刷新)。同时,还能解决第二个问题,针对不同的异常,做不同的处理。细心的童鞋可能注意到了有个IpRefused
,这是楼主自定义的异常类,如果想知道如何自定义异常类,请搜索其他博客,这里不作介绍。
上面的封装提供了以下方便:
1、首次请求失败,可多次发起请求。
2、针对不同的异常作相应的处理。
3、可抛出自定义异常类
由于网络请求属于爬虫的主要部分,若想做到可拓展性、稳定性,建议单独创建1-2个.py脚本。
提取结构化数据
建议独立.py脚本
根据自己的具体需求,去提取数据,并组合成结构化数据,最好是dict
或pandas.DataFrame
。楼主的思路如下,但总都觉得不是很好,可也没有想到其他优化方案。
# parse_html.py
class Parse_html(Object):
def __init__(self, *args, **kwargs):
"""需要初始化的参数"""
self.data = None
def get_variable1(self):
"""解析出字段1"""
def get_variable2(self):
"""解析出字段2"""
使用时如下:
P = Parse_html(html)
funcs = ['get_variable1', 'get_variable2']
for each_func in funcs:
getattr(P, each_func)('each_func的参数')
# 这个方法需要所有的each_func接收同样的参数
我知道这个方案不是最好的,如果有更优化方案的童鞋看到,欢迎交流。
数据存储
建议独立.py脚本,这里不多说文字,直接上楼主的思路:
# database.py
from sqlalchemy import create_engine
class Connect:
def __init__(self, db):
"""
:param db: 'oracle' or 'mysql'
"""
if db.upper() == 'ORACLE':
self.engine = create_engine('oracle://username:password@host:port/oradb1')
elif db.upper() == 'MYSQL':
self.engine = create_engine("mysql+pymysql://username:password@host:port/database",echo=True)
# 数据库类型+数据库驱动://用户名:密码@主机:端口/数据库名称,其他参数
def read_sql(self, sql):
"""执行sql语句读取数据"""
def write_sql(self, *args):
"""根据具体情况,选择用pandas.DataFrame.to_sql插入或者执行sql语句"""
这个脚本如果写的好,是可以作为自用的自定义库,之后的每个爬虫,都可以通过调用该脚本来实现数据存储。
后语
作者是第一次写这样的博客,对markdown语法不是很熟,排版或许很丑,但至少是个开始,对自己也是一种鼓励。后续或许会有跟多的学习笔记。不求自己成为大牛,只求学过的知识不再忘记。希望大家可以一起交流。