写在前面
最近在学习爬虫,发现 requests 这个库在爬取小规模网页的时候,非常好用。用 requests 库爬取了几个芬兰的网站,然后突然发现,可以用一套代码结构来爬取这种网页,于是就来整理了一下。
为了便于理解,我找了其中一个相对简单的网址 赫尔辛基时间 。下面就通过它,来介绍一下 requests 爬虫的通用结构。
Requests 爬虫的基本步骤
利用 requests 库写的爬虫,无外乎下面这几个步骤:
- 解析当前的 url,并找出这个 url 页面内所有需要爬取的子链接。
- 获取每个超链接页面的详细信息
- 保存数据
当然,如果 url 内部还有需要的 url,则需要进行迭代。凡是获取超链接的任务,都算是第一步要做的事,第二步负责提取网页内容,第三步保存数据。
下面看一下爬取赫尔辛基文化 (http://www.helsinkitimes.fi/culture.html) 的代码结构。
1. 获取超链接信息
首先,需要根据这个给出的 url 获取所有需要爬取的 url。因为超链接太多了,我们要筛选出有用的,就必须要找到它们的共同规律,这样才能把它们挑选出来。
这里我们查看网站的源代码,会发现所有文章的 url 链接都是以 “http://www.helsinkitimes.fi/culture/” 开始的,后面是一个四位数的数字,最后以 .html
结尾。
找到规律就很好办了,只要是符合这样结构的超链接,就是我们需要爬取的文章链接。这样我们就可以利用正则表达式,把它们从网站中一一挑出来了。 代码如下:
# 获取页面超链接信息
def get_html(url) :
html = requests.get(url)
html.encoding='utf-8'
soup = BeautifulSoup(html.text, 'lxml')
for i in soup.find_all('a', href=re.compile(r"/culture/\d{4}[^/]*\.html$")) :
yield "http://www.helsinkitimes.fi" + i.get('href')
这里我们就可以以 url = http://www.helsinkitimes.fi/culture.html 作为 url 参数。
注意:
yield 是返回每一个满足的超链接,效果上,与返回一个包含所有超链接的 List 是一样的。但利用 yield 就代表该函数是一个生成器,非常节省内存空间,这里不多做解释,感兴趣的可以自行百度查找原因,我们继续爬取网站内容。
2. 获取每个超链接页面详细信息
接下来就是分析每个页面的源码了。一般情况下,每篇文章的 html 结构都是相同的,我们只需要找到相同的结构,把它们分离开来,挑出我们需要的存储起来,就可以了。
思路就这么简单,下面来看代码吧。
因为我们需要将内容变为 json 格式,因此需要先创建一个字典,然后给每个字段赋予相对应的值,我们的字典一共有 6 个键值,定义如下:
data = {'title': '', 'url': '','review': '', 'content': '', 'time': '', 'type': ''}
然后定义两个数组,一个用来存储爬取出来的所有文章信息,另一个用来检测 url 是否重复,避免重复爬取:
dataList = []
url_list = [] # 存储所有访问过的 url, 避免重复访问
然后对每个 url,请求网址,并用 BeautifulSoup 类进行解析:
html_link = requests.get(i)
html_link.encoding='utf-8'
soup = BeautifulSoup(html_link.text, 'lxml')
然后,根据网页源码,找到 6 个键值对应的信息:
url = i
review = ""
content_list = soup.select(".article-content-main")[0].select("p")
content = ""
for j in content_list :
content = content + str(j)
# 处理内容,替换字符串中的字符,去掉正文中的图片信息
content = contentDeal.deal_content(content)
# 通过标签名查找 时间
time = soup.select("time")[0]
time = re.search(r"<time datetime=\"(.*)\">", str(time)).group(1)
type = "culture"
这里的 contentDeal 类是我自己定义的一个过滤类,将获取到的信息里面的一些无用信息过滤掉。
最后给字典赋值,添加到数组中,最后返回数组,就可以了:
这个功能是最主要的部分,稍微有点复杂,代码也稍长,但结构还是比较清晰的,整个函数的代码如下:
# 获取超链接页面详细信息
def get_html_link(link_list) :
# 创建字典
data = {'title': '', 'url': '','review': '', 'content': '', 'time': '', 'type': ''}
dataList = []
url_list = [] # 存储所有访问过的 url, 避免重复访问
# 遍历所有子链接
for i in link_list :
# 判断是否遍历过
flag = 0
for url in url_list :
if url == str(i) :
flag = 1
break
if flag == 1 :
continue
url_list.append(i)
# 正常请求
html_link = requests.get(i)
html_link.encoding='utf-8'
soup = BeautifulSoup(html_link.text, 'lxml')
title = soup.title.string
url = i
review = ""
content_list = soup.select(".article-content-main")[0].select("p")
content = ""
for j in content_list :
content = content + str(j)
# 处理内容,替换字符串中的字符,去掉正文中的图片信息
content = contentDeal.deal_content(content)
# 通过标签名查找 时间
time = soup.select("time")[0]
time = re.search(r"<time datetime=\"(.*)\">", str(time)).group(1)
type = "culture"
# 给字典赋值
data['title'] = title
data['url'] = url
data['review'] = review
data['content'] = content
data['time'] = time
data['type'] = type
# 加入 List
dataList.append(data)
# 更改字典地址
data = copy.copy(data)
return dataList
contentDeal 类:
import re
# 处理字符串文本内容
def deal_content(content) :
# 将 h1 标签转成 p 标签
content = re.sub(r"h1>", "p>", content)
# 将 figcaption 标签转成 p 标签
content = re.sub(r"figcaption>", "p>", content)
# 将 p 标签规格化
content = content.replace("</p>", "<p>")
content = re.sub(r"<p.*?>", "<p>", content)
# 将 br 标签变为空格
content = re.sub(r"<br.*?>", " ", content)
content = content.replace("</br>", " ")
# 去除 a 开头的标签
content = re.sub(r"<a.*?>", "", content)
content = re.sub(r"</a.*?>", "", content)
# 去除 b 开头的标签
content = re.sub(r"<b.*?>", "", content)
content = re.sub(r"</b.*?>", "", content)
# 去除 e 开头的标签
content = re.sub(r"<e.*?>", "", content)
content = re.sub(r"</e.*?>", "", content)
# 去除 f 开头的标签
content = re.sub(r"<f.*?>", "", content)
content = re.sub(r"</f.*?>", "", content)
# 去除 i 开头的标签
content = re.sub(r"<i.*?>", "", content)
content = re.sub(r"</i.*?>", "", content)
# 去除 l 开头的标签
content = re.sub(r"<l.*?>", "", content)
content = re.sub(r"</l.*?>", "", content)
# 去除 s 开头的标签
content = re.sub(r"<s.*?>", "", content)
content = re.sub(r"</s.*?>", "", content)
# 去除 t 开头的标签
content = re.sub(r"<t.*?>", "", content)`在这里插入代码片`
content = re.sub(r"</t.*?>", "", content)
# 去除 u 开头的标签
content = re.sub(r"<u.*?>", "", content)
content = re.sub(r"</u.*?>", "", content)
# 去除多余的空格
content = re.sub(r"\s+", " ", content)
return content
由于没有评论信息,因此这里的 review 一直是空的,可以忽略这个值。
3. 写回文件
写回文件就简单多了,直接利用最基本的文件操作就可以了,只有一点需要注意的,那就是我们需要保存在 .json 文件中。每一行是一个 json 格式的信息,代码如下:
# 保存数据
def save_data(content_list) :
with open('../www.helsinkitimes.fi/culture.json', 'a', encoding='utf-8') as f:
for json_data in content_list :
f.write(json.dumps(json_data, ensure_ascii=False)+'\n')
f.flush()
到这里就算基本写完了,最后把它们串在一起就好了。值得庆幸的是,我们这里的网页不是动态进行加载的,而且网页内所有需要的内容,都能在 html 中找到,这样,爬取就比较方便。最难的部分就是获取超链接内容了,但利用 BeautifulSoup 和 正则表达式,也是很容易的。
完整代码
主程序:
import requests, re, json, copy
from bs4 import BeautifulSoup
import contentDeal
# 获取页面超链接信息
def get_html(url) :
html = requests.get(url)
html.encoding='utf-8'
soup = BeautifulSoup(html.text, 'lxml')
for i in soup.find_all('a', href=re.compile(r"/culture/\d{4}[^/]*\.html$")) :
yield "http://www.helsinkitimes.fi" + i.get('href')
# 获取超链接页面详细信息
def get_html_link(link_list) :
# 创建字典
data = {'title': '', 'url': '','review': '', 'content': '', 'time': '', 'type': ''}
dataList = []
url_list = [] # 存储所有访问过的 url, 避免重复访问
# 遍历所有子链接
for i in link_list :
# 判断是否遍历过
flag = 0
for url in url_list :
if url == str(i) :
flag = 1
break
if flag == 1 :
continue
url_list.append(i)
# 正常请求
html_link = requests.get(i)
html_link.encoding='utf-8'
soup = BeautifulSoup(html_link.text, 'lxml')
title = soup.title.string
url = i
review = ""
content_list = soup.select(".article-content-main")[0].select("p")
content = ""
for j in content_list :
content = content + str(j)
# 处理内容,替换字符串中的字符,去掉正文中的图片信息
content = contentDeal.deal_content(content)
# 通过标签名查找 时间
time = soup.select("time")[0]
time = re.search(r"<time datetime=\"(.*)\">", str(time)).group(1)
type = "culture"
# 给字典赋值
data['title'] = title
data['url'] = url
data['review'] = review
data['content'] = content
data['time'] = time
data['type'] = type
# 加入 List
dataList.append(data)
# 更改字典地址
data = copy.copy(data)
return dataList
# 保存数据
def save_data(content_list) :
with open('../www.helsinkitimes.fi/culture.json', 'a', encoding='utf-8') as f:
for json_data in content_list :
f.write(json.dumps(json_data, ensure_ascii=False)+'\n')
f.flush()
# 函数回调
def fun_call(url) :
link_list = get_html(url) # 返回一个生成器
content_list = get_html_link(link_list)
save_data(content_list)
# 主函数
def main() :
url = 'http://www.helsinkitimes.fi/culture.html'
fun_call(url)
if __name__=='__main__':
main()
contentDeal 类:
import re
# 处理字符串文本内容
def deal_content(content) :
# 将 h1 标签转成 p 标签
content = re.sub(r"h1>", "p>", content)
# 将 figcaption 标签转成 p 标签
content = re.sub(r"figcaption>", "p>", content)
# 将 p 标签规格化
content = content.replace("</p>", "<p>")
content = re.sub(r"<p.*?>", "<p>", content)
# 将 br 标签变为空格
content = re.sub(r"<br.*?>", " ", content)
content = content.replace("</br>", " ")
# 去除 a 开头的标签
content = re.sub(r"<a.*?>", "", content)
content = re.sub(r"</a.*?>", "", content)
# 去除 b 开头的标签
content = re.sub(r"<b.*?>", "", content)
content = re.sub(r"</b.*?>", "", content)
# 去除 e 开头的标签
content = re.sub(r"<e.*?>", "", content)
content = re.sub(r"</e.*?>", "", content)
# 去除 f 开头的标签
content = re.sub(r"<f.*?>", "", content)
content = re.sub(r"</f.*?>", "", content)
# 去除 i 开头的标签
content = re.sub(r"<i.*?>", "", content)
content = re.sub(r"</i.*?>", "", content)
# 去除 l 开头的标签
content = re.sub(r"<l.*?>", "", content)
content = re.sub(r"</l.*?>", "", content)
# 去除 s 开头的标签
content = re.sub(r"<s.*?>", "", content)
content = re.sub(r"</s.*?>", "", content)
# 去除 t 开头的标签
content = re.sub(r"<t.*?>", "", content)`在这里插入代码片`
content = re.sub(r"</t.*?>", "", content)
# 去除 u 开头的标签
content = re.sub(r"<u.*?>", "", content)
content = re.sub(r"</u.*?>", "", content)
# 去除多余的空格
content = re.sub(r"\s+", " ", content)
return content
最后,附上项目地址:github地址——爬取 www.helsinkitimes.fi
项目上还有其他几个芬兰网站的爬取代码,由于是刚开始学习,接触的知识比较少,如有任何不对的地方,欢迎指出,谢谢!