开发环境

基础语法那章的内容我是在Docker容器中玩的,但是真正做项目的时候,没有IDE的强大辅助功能来协助的话是很累人的一件事。因此本文中,我选择使用Jetbrain的Pycharm这个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_page和project_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_queue、is_page_in_queue、save_pending_queue及set_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. 在webapi中为Action使用dynamic参数实现Post方式调用

    1.在webapi中使用controller/action/id的路径配置,打开文件[App_Start] -[WebApiConfig] config.Routes.MapHttpRoute( na ...

  2. 2_ROS学习

    2_VNC远程连接树莓派 在上一次,我们成功的给树莓派安装了Ubuntu mate的操作系统. 树莓派是嵌入式计算机,一般是没有显示屏来显示的,我们通过远程连接来访问树莓派.网上推荐了ssh连接,xr ...

  3. Maven 设置Maven源/镜像

    在Eclipse/ InteliJ Idea使用Maven时,企业提供的本地仓库经常有些Jar包下载不下来,所以.配置Maven设置国内镜像对开发人员来说是必不可少的.今天写一篇自己配制的Maven镜 ...

  4. struts2(五)之struts2拦截器与自定义拦截器

    前言 前面介绍了struts2的输入验证,如果让我自己选的话,肯定是选择xml配置校验的方法,因为,能使用struts2中的一些校验规则,就无需自己编写了, 不过到后面应该都有其他更方便的校验方法,而 ...

  5. Javaweb项目开发的前后端解耦的必要性

    JavaWeb项目为何我们要放弃jsp?为何要前后端解耦?为何要动静分离? 使用jsp的痛点: 1.jsp上动态资源和静态资源全部耦合在一起,服务器压力大,因为服务器会收到各种静态资源的http请求, ...

  6. Windows 编程,程序编译使用的命令行工具。

    Windows 编程,程序编译使用的命令行工具. 1.cl.exe文件是Visual C\C++的编译器,它将程序源代码文件编译为obj文件. 2.rc.exe文件是资源编译器.工程项目中的.rc文件 ...

  7. ES6中数组的新方法

    数组的扩展 1.1扩展运算符 1.1.1:... 扩展运算符(spread)是三个点(...).它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列. <body> < ...

  8. window.onload,document.ready

    执行时间 window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行. $(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕. 编写个数不同 wind ...

  9. VUE父子组件传值问题

    一.父组件向子组件传递数据 组件实例的作用域是孤立的.这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据.要让子组件使用父组件的数据,我们需要通过子组件的props选项. 1.静态props ...

  10. 面试题:判断连个字符串是否互为回环变位(Circular Rotaion)

    题干:   如果字符串 s 中的字符循环移动任意位置之后能够得到另一个字符串 t,那么 s 就被称为 t 的回环变位(circular rotation).   例如,ACTGACG 就是 TGACG ...