开发环境

基础语法那章的内容我是在Docker容器中玩的,但是真正做项目的时候,没有IDE的强大辅助功能来协助的话是很累人的一件事。因此本文中,我选择使用JetbrainPycharm这个IDE来开发、调试代码。IDE的好处多多,比如:

  • 强大的智能提示
  • 强大的断点调试
  • 性能追踪
  • 方便好用的各种插件
  • 各种自定义配置

需求

为了实践Python,最先想到的就是要完成一个爬虫程序,大概需求如下:

实施

可配置化我本身是计划通过DI(Dependency Injection)这个技术来完成,不过查了下资料,由于Python和其他语言不太一样,Python是可以多父类继承,并且遵循Duck Typing原则,因此DI在Python中并不实用(Python也是没有Interface概念的)。但可以通过如下方式实现类似的逻辑:

# 假设a-class-name这个类包含在xxx.py文件中,首先引入这个文件中的内容
from xxx import *
# 然后执行以下这行代码,这将初始化一个a-class-name类的实例
(lambda x: globals()[x])('a-class-name')

入口程序文件main.py

main.py主要有几个功能:

  • 通过交互让用户输入:项目名称、网站首页、线程数三个初始化变量
  • 初始化数据库访问对象
  • 初始化爬虫对象
  • 初始化线程池
  • 执行程序

核心代码如下:

from db_queue import *
... def execute():
... (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)(home_page, project_name + '_pages')
Spider(project_name, home_page, DomainHelpers.get_domain_name(home_page), project_settings.HTML_RESOLVER_NAME) worker = Worker(thread_count, project_name)
worker.create_threads()
worker.crawl() execute()

逻辑解释:

  1. (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)(home_page, project_name + '_pages'),本例中DB_CLASS_NAME = 'MongoDbQueue',因此Python将在当前页面的应用中查找名为MongoDbQueue的类来执行初始化并传入构造函数的参数:home_pageproject_name + '_pages'
  2. 初始化Spider类,以便在线程中执行爬取页面
  3. 初始化指定数量的现成作为线程池以备后续使用,main.py执行完毕,线程将被自动回收
  4. 开始执行爬虫程序

线程创建类worker.py文件

from db_queue import *

class Worker:
...
def __init__(self, thread_count, project_name):
Worker.DB = (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)
... def create_threads(self):
for _ in range(self.thread_count):
t = threading.Thread(target=self.__run_thread)
t.daemon = True
t.start() def __run_thread(self):
while True:
url = self.queue.get()
Spider.crawl_page(threading.current_thread().name, url)
self.queue.task_done() def __create_jobs(self):
for link in Worker.DB.get_pending_queue():
self.queue.put(link)
self.queue.join()
self.crawl() def crawl(self):
urls = Worker.DB.get_pending_queue()
if len(urls) > 0:
self.__create_jobs()

逻辑解释:

  1. __init__中将数据库连接类保存到全局变量DB中
  2. create_threads将初始化指定数量的线程数,设置为datmon=true以便线程被创建之后一直存在,随时可以被调用
  3. crawl将获取待爬列表之后,将其放入Spider所需的待爬队列中
  4. self.queue.join()是用来阻塞队列,这样队列中的每一项都将只被调用一次
  5. __run_thread__create_jobs这两个方法是Worker内部调用的方法,不需要公开给其他人,因此加上前缀__(两个下划线)

数据库操作基础类

由于需要将数据库操作做成可替换,因此必须实现数据库操作的接口,而Python没有Interface,但是可以使用abc(Abstract Based Class)来实现类似于Interface所需的功能。

代码如下:

from abc import ABCMeta, abstractmethod

class DbBase(metaclass=ABCMeta):
@abstractmethod
def __init__(self, file_name):
pass @staticmethod
@abstractmethod
def get_pending_queue():
pass @staticmethod
@abstractmethod
def is_page_in_queue():
pass @staticmethod
@abstractmethod
def save_pending_queue():
pass @staticmethod
@abstractmethod
def set_page_crawled():
pass

逻辑解释:

  1. class DbBase(metaclass=ABCMeta)表示DbBase类的元类为ABCMeta
  2. @abstractmethod则表明该方法在继承了DbBase的类中必须被实现,如果没有被实现,执行时将会报错:TypeError: Can't instantiate abstract class XXXX with abstract methods xxxx

数据库存储操作db_queue.py文件

from pymongo import *
from abc_base.db_base import DbBase
... class MongoDbQueue(DbBase): def __init__(self, home_page, tbl_name='pages'):
... MongoDbQueue.db = MongoClient(project_settings.DB_CONNECTION_STRING)[project_settings.DB_REPOSITORY_NAME] ... # create unique index
MongoDbQueue.db[MongoDbQueue.tbl_name].create_index('url', unique=True) @staticmethod
def get_pending_queue():
... @staticmethod
def is_page_in_queue(url):
... @staticmethod
def save_pending_queue(urls):
... @staticmethod
def set_page_crawled(url):
...

逻辑解释:

  1. class MongoDbQueue(DbBase):表示该类继承了DbBase,因此必须实现DbBase中定义的几个方法__init__get_pending_queueis_page_in_queuesave_pending_queueset_page_crawled
  2. 为了确保相同的url绝对不会重复,在数据库层也增加一个Unique Index以便从数据库层面也做好验证
  3. get_pending_queue将所有未被爬过的页面列表返回
  4. is_page_in_queue判断是否页面在待爬列表中
  5. save_pending_queue,这个方法是在爬取某个页面,抓取了该页面上所有新的代码链接之后,将数据库中不存在的连接保存为待爬页面
  6. set_page_crawled,这个方法将数据库中已存在,且状态为未爬过的页面,设置为已爬,该方法将在爬虫爬好某个页面之后被调用

爬虫文件spider.py文件

...
class Spider:
... def __init__(self, base_url, domain_name, html_resolver):
...
Spider.crawl_page('First spider', Spider.BASE_URL) @staticmethod
def crawl_page(thread_name, page_url):
if Spider.DB.is_page_in_queue(page_url):
...
urls = Spider.add_links_to_queue(Spider.gather_links(page_url))
Spider.DB.save_pending_queue(urls)
Spider.DB.set_page_crawled(page_url) @staticmethod
def gather_links(page_url):
html_string = ''
...
# to make self-signed ssl works, pass variable 'context' to function 'urlopen'
context = ssl._create_unverified_context()
response = urlopen(page_url, context=context)
...
finder = (lambda x: globals()[x])(Spider.HTML_RESOLVER)(Spider.BASE_URL, page_url)
return finder.page_links() @staticmethod
def add_links_to_queue(urls):
...
for url in urls:
if Spider.DOMAIN_NAME != DomainHelpers.get_domain_name(url):
continue
...

逻辑解释:

  1. Spider.DB = (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)这一行依然是动态初始化数据库操作类
  2. context = ssl._create_unverified_context(),有时候有些自签名ssl证书,执行urlopen方法时会报错,需要创建这个context变量来避免这个错误产生
  3. finder = (lambda x: globals()[x])(Spider.HTML_RESOLVER)(Spider.BASE_URL, page_url)这行也是通过动态初始化的方式,按照配置文件中指定的解析类来解析html内容,如果想自定义解析内容,只要重新实现一个解析类即可
  4. add_links_to_queue 这个方法是确保只会将当前域名相关的页面保存起来以便后续继续爬,如果不加这个判断,一旦页面上有一个www.weibo.com这样的链接的话,那爬虫估计会把整个互联网上的内容都爬一遍。。。

html解析html_resolver.py文件

class HtmlResolver(HTMLParser):
...
def handle_starttag(self, tag, attrs):
if tag == 'a':
for (attribute, value) in attrs:
if attribute == 'href':
url = parse.urljoin(self.base_url, value)
self.links.add(url) ...

这个类决定了我们爬取页面的逻辑,这里我们只抓去链接(也就是a标签)中的href属性中的内容。

执行过程动图

附录

本Demo完整代码已经放到Github上: https://github.com/fisherdan/crawler


本文在博客园和我的个人博客www.fujiabin.com上同步发布。转载请注明来源。

Python3学习笔记2:简易Web爬虫的更多相关文章

  1. python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例

    python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例 新浪爱彩双色球开奖数据URL:http://zst.aicai.com/ssq/openInfo/ 最终输出结果格 ...

  2. ASP.NET MVC Web API 学习笔记---第一个Web API程序

    http://www.cnblogs.com/qingyuan/archive/2012/10/12/2720824.html GetListAll /api/Contact GetListBySex ...

  3. Python3学习笔记(urllib模块的使用)转http://www.cnblogs.com/Lands-ljk/p/5447127.html

    Python3学习笔记(urllib模块的使用)   1.基本方法 urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None,  ...

  4. Python3学习笔记 - 准备环境

    前言 最近乘着项目不忙想赶一波时髦学习一下Python3.由于正好学习了Docker,并深深迷上了Docker,所以必须趁热打铁的用它来创建我们的Python3的开发测试环境.Python3的中文教程 ...

  5. python3学习笔记(7)_listComprehensions-列表生成式

    #python3 学习笔记17/07/11 # !/usr/bin/env python3 # -*- conding:utf-8 -*- #通过列表生成式可以生成格式各样的list,这种list 一 ...

  6. python3学习笔记(6)_iteration

    #python3 学习笔记17/07/10 # !/usr/bin/env python3 # -*- coding:utf-8 -*- #类似 其他语言的for循环,但是比for抽象程度更高 # f ...

  7. python3学习笔记(5)_slice

    #python3 学习笔记17/07/10 # !/usr/bin/env python3 # -*- coding:utf-8 -*- #切片slice 大大简化 对于指定索引的操作 fruits ...

  8. Spring实战第八章学习笔记————使用Spring Web Flow

    Spring实战第八章学习笔记----使用Spring Web Flow Spring Web Flow是一个Web框架,它适用于元素按规定流程运行的程序. 其实我们可以使用任何WEB框架写流程化的应 ...

  9. Spring实战第五章学习笔记————构建Spring Web应用程序

    Spring实战第五章学习笔记----构建Spring Web应用程序 Spring MVC基于模型-视图-控制器(Model-View-Controller)模式实现,它能够构建像Spring框架那 ...

  10. 第3次作业-MOOC学习笔记:Python网络爬虫与信息提取

    1.注册中国大学MOOC 2.选择北京理工大学嵩天老师的<Python网络爬虫与信息提取>MOOC课程 3.学习完成第0周至第4周的课程内容,并完成各周作业 4.提供图片或网站显示的学习进 ...

随机推荐

  1. 【Vue】利用父子组件间通信实现一个场景

    组件间通信是组件开发的,我们既希望组件的独立性,数据能互不干扰,又不可避免组件间会有联系和交互. 在vue中,父子组件的关系可以总结为props down,events up: 在vue2.0中废弃了 ...

  2. zookeeper启动后的注意事项

    在各节点中运行zkServer.sh start后 1.首先看进程是否存在 QuorumPeerMain. 2.查看zookeeper的运行状态 zkServer.sh status,会出现一个lea ...

  3. 关于Makefile,Makefile.in,Makefile.am,Configure功能及相互关系的问题

    makefile写法 在 Unix 上写程式的人大概都碰过 Makefile,尤其是用 C 来开发程式的人.用 make来开发和编译程式的确很方便,可是要写出一个 Makefile就不简单了.偏偏介绍 ...

  4. nginx服务部署 说明

    第1章 常用的软件 1.1 常用来提供静态服务的软件   Apache :这是中小型Web服务的主流,Web服务器中的老大哥,   Nginx :大型网站Web服务的主流,曾经Web服务器中的初生牛犊 ...

  5. vue初级学习--组件的使用(自定义组件)

    一.导语 突然冒出四个字,分即是合,嗯,优点道理....................... 二.正文 在搞的仿淘宝demo,之前加入购物车是与商品详情一块的,今天把它单独拆出来,复用性高点,那这样 ...

  6. Android打开/data/目录以及导出文件

    打开logcat和和file Explorer Tools-->Android-->Android Device Monitor 如果右侧没有出现,Windows-->Show Vi ...

  7. AJAX 笔记

    一.什么是 AJAX ? AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML). AJAX 是一种用于创建快速动态网页的技术. 通过 ...

  8. 粗略整理的java面试题

    1.垃圾回收  是回收的空闲堆空间 只有在cpu空闲并且堆空间不足的情况下才回收 2.threadlocal  就是为线程的变量都提供了一个副本,每个线程运行都只是在更新这个副本. Threadloc ...

  9. linux tar命令 压缩、打包、解压 详解

    linux tar命令 压缩.打包.解压 详解 1.常用压缩命令 tar –czvf 压缩后的文件.tar.gz 要压缩的文件 2.常用解压命令 tar –xzvf 解压后的文件.tar.gz [要解 ...

  10. postgis常用操作手册

    查询所有函数: SELECT * FROM pg_proc; 更新坐标系st_setsrid,查看坐标系:st_srid 创建空间索引: CREATE INDEX [indexname] ON [ta ...