注册CSDN当好挺久了,但一直没有写过什么,主要是没啥可写的(本人太菜),另外也是因为CSDN写文章看起来挺麻烦的,被吓到了。我最近在看《python爬虫开发与项目实战》,把实战项目:基础爬虫这章的代码敲过并调试通过后,感觉好像可以写一写,顺便学习一下久仰大名的markdown编辑器。以下,权当对该章节的内容做个笔记,并针对代码做了些许改动以便于在python3.7中运行,并且能够输出满意的结果。
基础爬虫项目功能简单,仅仅考虑功能实现,未涉及优化和稳健性的考虑。本实战项目的需求是:爬取100个百度百科网络爬虫词条以及相关词条的标题、摘要和链接等信息。
基础爬虫架构及运行流程
基础爬虫框架主要包括五大模块:
- 爬虫调度器,负责统筹调度其他模块
- URL管理器,负责管理URL链接
- HTML下载器,负责下载HTML网页
- HTML解析器,解析出新的URL以及有效数据
- 数据存储器,存储解析出来的数据
下面通过图1展示爬虫框架的动态运行流程
URL管理器
URL管理器主要包含两个变量:已爬取的URL集合,未爬取的URL集合。链接去重复在Python爬虫开发中是必备的功能,解决方案主要有三种:
- 内存去重
- 关系数据库去重
- 缓存数据库去重
大型成熟的爬虫基本上采用缓存数据库的去重方案,尽可能避免内存大小的限制,又比关系型数据库去重性能高很多。由于本项目的爬取数量较小,所以使用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,可以在首行输入代码 (分号包含在内)实现段落首行缩进。