注册CSDN当好挺久了,但一直没有写过什么,主要是没啥可写的(本人太菜),另外也是因为CSDN写文章看起来挺麻烦的,被吓到了。我最近在看《python爬虫开发与项目实战》,把实战项目:基础爬虫这章的代码敲过并调试通过后,感觉好像可以写一写,顺便学习一下久仰大名的markdown编辑器。以下,权当对该章节的内容做个笔记,并针对代码做了些许改动以便于在python3.7中运行,并且能够输出满意的结果。
 基础爬虫项目功能简单,仅仅考虑功能实现,未涉及优化和稳健性的考虑。本实战项目的需求是:爬取100个百度百科网络爬虫词条以及相关词条的标题、摘要和链接等信息。

基础爬虫架构及运行流程

基础爬虫框架主要包括五大模块:

  1. 爬虫调度器,负责统筹调度其他模块
  2. URL管理器,负责管理URL链接
  3. HTML下载器,负责下载HTML网页
  4. HTML解析器,解析出新的URL以及有效数据
  5. 数据存储器,存储解析出来的数据

下面通过图1展示爬虫框架的动态运行流程

爬虫调度器URL管理器HTML下载器HTML解析器数据存储器1. 是否有待取的URL?2. Y or N3. 获取一个新URL4. 返回URL5. URL传递给下载器6. 返回HTML内容7.HTML内容交给解析器8. 解析出URL和数据9_1. 新URL交给管理器9_2. 存储有效数据loop[ 运行流程 ]爬虫调度器URL管理器HTML下载器HTML解析器数据存储器

URL管理器

  URL管理器主要包含两个变量:已爬取的URL集合,未爬取的URL集合。链接去重复在Python爬虫开发中是必备的功能,解决方案主要有三种:

  1. 内存去重
  2. 关系数据库去重
  3. 缓存数据库去重

  大型成熟的爬虫基本上采用缓存数据库的去重方案,尽可能避免内存大小的限制,又比关系型数据库去重性能高很多。由于本项目的爬取数量较小,所以使用python中set类型这种内存去重方式。
URL管理器除了具有两个URL集合,还需要提供以下接口:

  • 判断是否有待取的URL,has_new_url()
  • 添加新的URL到未爬取集合中,add_new_url(), add_new_urls()
  • 获取一个未爬取的URL,get_new_url()
  • 获取未爬取URL集合的大小,new_url_size()
  • 获取已经爬取的URL集合的大小,old_url_size()

URLManager.py代码如下:

class UrlManager(object):
    def __init__(self):
        self.new_urls = set()#未爬取URL集合
        self.old_urls = set()#已爬取URL集合
        
    def has_new_url(self):
        return self.new_url_size() != 0
    
    def get_new_url(self):
        new_url = self.new_urls.pop()
        self.old_urls.add(new_url)
        return new_url
    
    def add_new_url(self, url):
        if url is None:
            return
        if url not in self.new_urls and url not in self.old_urls:
            self.new_urls.add(url)
    
    def add_new_urls(self, urls):
        if urls is None or len(urls)==0:
            return
        for url in urls:
            self.add_new_url(url)
    
    def new_url_size(self):
        return len(self.new_urls)
    
    def old_url_size(self):
        return len(self.old_urls)

HTML管理器

  HTML管理器用来下载网页,需要注意网页的编码,保证下载的网页没有乱码,只需要实现一个接口: download(url)。程序HtmlDownloader.py代码如下:

import requests
import chardet

class HtmlDownloader(object):
    def download(self, url):
        if url is None:
            return None
        user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
        headers = {'User-Agent': user_agent}
        r = requests.get(url, headers=headers)
        
        if r.status_code == 200:
            r.encoding = chardet.detect(r.content)['encoding']
            return r.text
        return None

HTML解析器

1、解析内容分析

  使用BeautifulSoup进行HTML解析。需要解析的部分主要为在当前页面中提取相关词条的URL、前词条的标题和摘要信息。先在Chrome开发者工具中查看标题和摘要所在的 结构位置,如图所示。
基础爬虫笔记
  可以看到,标题的标记位于

<dd class="lemmaWgt-lemmaTitle-title">
<h1>网络爬虫</h1>

  摘要的标记位于

<div class="lemma-summary" label-module="lemmaSummary">
...
</div>

  需要抽取的URL的格式如图<a target="_blank" href="/item/%..." data-lemmaid='...'>所示,其中href中间的“%E8%9…”是因为中文出现在url中被转码了。

HTML解析器代码

  HTML解析器主要提供一个parser对外接口,输入参数为当前页面的URL和HTML下载其返回的网页内容。解析器HtmlParser.py程序的代码如下所示。其中,links = soup.find_all('a', href=re.compile(r'(/item/(%|\w)*/\d+)|(/item/[A-Za-z]+)'))中的正则表达式,是要考虑 ‘/item/万维网/215515’ 和 ‘/item/FOAF’,又需要排除 ‘/item/百度’ 这种。

import re
import urllib.parse
from bs4 import BeautifulSoup

class HtmlParser(object):
    
    def parser(self, page_url, html_cont):
        if page_url is None or html_cont is None:
            return
        soup = BeautifulSoup(html_cont, 'html.parser')
        new_urls = self.__get_new_urls(page_url, soup)
        new_data = self.__get_new_data(page_url, soup)
        return new_urls, new_data
    
    def __get_new_urls(self, page_url, soup):
        '在当前页面寻找新词条的URL'
        new_urls = set()
        links = soup.find_all('a', href=re.compile(r'(/item/(%|\w)*/\d+)|(/item/[A-Za-z]+)'))
        for link in links:
            new_url = link['href']
            new_url = urllib.parse.unquote(new_url)
            new_full_url = urllib.parse.urljoin(page_url, new_url)
            new_urls.add(new_full_url)
        return new_urls
    def __get_new_data(self, page_url, soup):
        '在当前页面抽取当前词条的数据,标题、摘要和当前页面的链接'
        data={}
        data['url']=page_url
        title = soup.find('dd', class_='lemmaWgt-lemmaTitle-title').find('h1')
        data ['title'] = title.get_text()
        summary = soup.find('div', class_='lemma-summary')
        data['summary'] = summary.get_text()
        return data

  在__get_new_urls()方法中,new_url = urllib.parse.unquote(new_url)保证最终输出文件中的url中文部分能够正常显示。

数据存储器

  数据存储器主要包括两个方法:

  • store_data(data),将解析出来的数据存储到内存中,
  • output_html(),将存储的数据输出为指定的文件格式

  DataOutput.py程序如下。其中,所有数据存储到内存,一次性写入文件容易使系统出现异常,造成数据丢失。更好的做法是将数据分批存储到文件,但是由于我们只需要100条数据,速度很快,这种方法尚且可行。如果数据很多,还是采取分批存储的办法。

import codecs

class DataOutput(object):
    
    def __init__(self):
        self.datas = []
    def store_data(self,data):
        if data is None:
            return
        self.datas.append(data)
    
    def output_html(self):
        fout = codecs.open('baike.html', 'w', encoding='utf-8')
        fout.write("<html>")
        fout.write("<head><meta charset='utf-8'/></head>")
        fout.write("<body>")
        fout.write("<table>")
        for data in self.datas:
            fout.write("<tr>")
            fout.write("<td>%s<td>"%data['url'])
            fout.write("<td>%s<td>"%data['title'])
            fout.write("<td>%s<td>"%data['summary'])
            fout.write("</tr>")
            self.datas.remove(data)
        fout.write("</table>")
        fout.write("</body>")
        fout.write("</html>")
        
        fout.close()

爬虫调度器

  爬虫调度器首先初始化各模块,然后通过crawl(root_url)方法传入入口URL,方法内部实现按照运行流程控制各个模块的工作。爬虫调度器SpiderMan.py的程序如下

from DataOutput import DataOutput
from HtmlDownloader import HtmlDownloader
from HtmlParser import HtmlParser
from URLManager import UrlManager

class SpiderMan(object):
    def __init__(self):
        self.manager = UrlManager()
        self.downloader = HtmlDownloader()
        self.parser = HtmlParser()
        self.output = DataOutput()
    def crawl(self, root_url):
        'root_url,爬虫的入口URL'
        self.manager.add_new_url(root_url)
        while(self.manager.has_new_url() and self.manager.old_url_size() < 100):
            try:
                new_url = self.manager.get_new_url()
                html = self.downloader.download(new_url)
                new_urls, data = self.parser.parser(new_url, html)
                self.manager.add_new_urls(new_urls)
                self.output.store_data(data)
                print("已经抓取%s个链接"%self.manager.old_url_size())
            except Exception:
                print("crawl failed")
        self.output.output_html()

if __name__ == "__main__":
    spider_man = SpiderMan()
    spider_man.crawl('https://baike.baidu.com/item/网络爬虫')

  上述各个文件均在同一个文件夹下。crawl方法中while的终止条件为: (1) 没有新的url了;(2) 尽管可能已经存下了非常多的url,但是只输出100个url。

问题及总结

  初次运行过程中,一直是爬取失败crawl failed,一个url都没有。一开始猜测原因是url的中文编码问题,后来,经过各种百度并最终实验,确定不是url的中文编码问题。以下为实验代码

import urllib, requests
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
headers = {'User-Agent': user_agent}
r1 = requests.get('https://baike.baidu.com/item/网络爬虫', headers=headers)
st = urllib.parse.quote('网络爬虫')
r2 = requests.get('https://baike.baidu.com/item/%s'%st, headers=headers)
print(r1.content==r2.content)

###Output
True

  最终通过在关键点处print,逐步定位到问题的出处在于HtmlParser.py中soup.find中的class_属性写错字母或者漏写、多写了字母!!!非常吐血,手残党伤不起呀
  BTW,可以在首行输入代码&#8195;(分号包含在内)实现段落首行缩进。

相关文章:

  • 2021-12-01
猜你喜欢
  • 2022-12-23
  • 2021-12-22
  • 2021-11-15
  • 2021-11-29
  • 2021-12-22
  • 2021-07-06
  • 2022-12-23
相关资源
相似解决方案