爬虫概述
通俗的讲,爬虫就是模拟浏览器,向服务器发出请求,获取到服务器返回的内容,再挑出我们想要的内容保存下来。所以,写爬虫主要分为三步:
1.发出请求
2.解析页面
3.保存数据
一、发出请求
最基础的HTTP库有urllib,reuests
首先介绍urllib
1.1 urllib的使用
urllib主要有四个模块,request,error,parse,robotparser
- request : 最基本的HTTP请求模块,可用来模拟发出请。想在浏览器中输入网址,然后回车一样,只需要给库方法传递url以及额外的参数即可。
- error : 异常处理模块,如果出现请求错误,我们可以捕获这些异常
urlopen()
urlopen()参数为url,模拟打开浏览器,下面我们看下使用方法:
import urllib.request
response = urllib.request.urlopen("https://www.python.org")
print(response.read().decode('utf-8'))
返回的是Python 官网的网页源代码:
<div id="touchnav-wrapper">
<div id="nojs" class="do-not-print">
<p><strong>Notice:</strong> While Javascript is not essential for this website, your interaction with the content will be limited. Please turn Javascript on for the full experience. </p>
</div>
接下来,我们看下返回的结果是什么类型:
print(type(response))
可以看到返回的结果是HTTPResponse 对象:
<class 'http.client.HTTPResponse'>
所以,response就拥有read() , readinto() , getheader(name) , getheaders() 等方法以及 msg , status , reason , 的属性。
Request类
如果需要传入更多的参数,就需要使用Request类
看下Request类的构造:
class urllib.request.Request(url,data=None,headers={},origin_req_host=None, unverifiable=False , method= None)
- data : data参数如果要传,需要传入bytes(字节流),如果是字典,可以先用urllib.parse模块里的urlencode()编码
- headers : headers是一个字典,即请求头。
下面用一个例子来传入多个参数:
from urllib import request,parse
url = 'http://httpbin.org/post'
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
}
dict = {
'name':'Mike'
}
data = bytes(parse.urlencode(dict),encoding='utf-8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
response = request.urlopen(req)
print(response.read().decode('utf-8'))
结果为:
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "Mike"
},
"headers": {
"Accept-Encoding": "identity",
"Connection": "close",
"Content-Length": "9",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"
},
"json": null,
"origin": "218.94.83.134",
"url": "http://httpbin.org/post"
}
1.2 requests的使用
与urlopen 类似,request.get()方法,也是向浏览器发起请求。
import requests
response = requests.get('https://www.python.org')
print(type(response))
print(response)
返回结果为:
<class 'requests.models.Response'>
<Response [200]>
由此可见,response 是浏览器的Response,可用response.text或者response.content获取网站的内容。前者为str类型,后者为bytes类型。
同样我们可以传入headers:
import requests
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
}
response = requests.get('https://www.python.org',headers=headers)
如果请求是post方式,直接传入data即可:
from urllib import request,parse
url = 'http://httpbin.org/post'
data = {
'name':'Mike'
}
data = bytes(parse.urlencode(dict),encoding='utf-8')
response = requests.get(req)
print(response.text)
注意:两种发起请求方法都可以,两种方法获取信息不一样,urlopen()返回源码使用response.read()方法,requests.get()使用response.text。我一般常用requests,get()方法向浏览器发起请求。
二、解析页面
当我们获取到服务器发送回的响应后,我们便通过解析源码来获取到我们想要的内容。解析页面,我们常用的方法有:
- 正则表达式
- XPath
- Beautiful Soup
2.1 正则表达式
常用的正则表达式规则:
模式 | 描述 |
---|---|
\w | 匹配字母、数字、及下划线 |
\s | 匹配任意空白字符 |
\t | 匹配一个换行符 |
\d | 匹配任意数字,等价于[0-9] |
. | 匹配任意字符 |
* | 匹配0个或者多个表达式 |
? | 匹配0个或者1个前面的正则表达式定一的片段,非贪婪模式 |
a\b (中间是竖线) | 匹配a或者b |
() | 括号内的表达式,也表示一个组 |
常用的方法有:
- match() : 从字符串的开头开始匹配,(不常用)两个参数,第一个传入正则,第二个传入待匹配字符串,返回结果是SRE_Match对象,有两个属性,group()和span(),前者输出匹配到的内容,后者输出匹配的范围。
- search() : 匹配时,扫描整个字符串,返回匹配的第一个内容。
- findall() : 扫描全部字符串,返回所有内容。
- compile() : 将正则字符串编译成正则对象,方便在后面的匹配中复用。
注:一般在匹配字符串的时候,总会有换行,所以可以在调用函数的时候,加入re.S参数,可忽略换行。
下面将用正则表达式为例,爬取起点网玄幻小说的排行榜:
首先打开起点网,玄幻小说栏目(https://www.qidian.com/rank/click?style=1&page=)
我们将爬取排名,书本连接,书名,作者,还有时间。
打开谷歌开发者环境,找到我们要爬取的目标。
1、我们先用requests.get()方法,获取目标网页。
import requests
def get_one_page(url):
response = requests.get(url)
return response.text
函数返回为目标网页的源码。
2、解析源网页,每一本书都是包含在ul标签下的li标签里,里面包含了我们需要的几个内容。接下来,我们将写出正则表达式匹配到我们想要的内容。
import re
def parse_one_page(html):
pattern = re.compile('<li\sdata-rid.*?<span class="rank-tag no.*?">(.*?)<cite>.*?<h4><a\shref="(.*?)".*?>(.*?)</a></h4>',re.S)
items = re.findall(pattern,html)
return items
调用该函数,查看返回结果是什么形式的。
[('1', '//book.qidian.com/info/1010191960', '大王饶命'), ('2', '//book.qidian.com/info/1209977', '斗破苍穹'), ('3', '//book.qidian.com/info/1011705052', '明朝败家子'), ('4', '//book.qidian.com/info/1012486119', '十恶临城'), ('5', '//book.qidian.com/info/1002409852', '诡神冢'), ('6', '//book.qidian.com/info/1011483714', '怪物聊天群'), ('7', '//book.qidian.com/info/1010276884', '狼牙兵王'), ('8', '//book.qidian.com/info/1012237441', '全球高武'), ('9', '//book.qidian.com/info/1011816096', '全职武神'), ('10', '//book.qidian.com/info/1009704712', '牧神记'), ('11', '//book.qidian.com/info/1012749331', '重回80当大佬'), ('12', '//book.qidian.com/info/1010981643', '开天录'), ('13', '//book.qidian.com/info/1004608738', '圣墟'), ('14', '//book.qidian.com/info/3602691', '修真聊天群'), ('15', '//book.qidian.com/info/1011449952', '我在帝都建洞天'), ('16', '//book.qidian.com/info/1010730481', '神级大药师'), ('17', '//book.qidian.com/info/3393401', '极品全能学生'), ('18', '//book.qidian.com/info/1011468740', '我要大宝箱'), ('19', '//book.qidian.com/info/118447', '星辰变'), ('20', '//book.qidian.com/info/1010734492', '凡人修仙之仙界篇')]
可以看到返回结果是列表形式的,里面的元素元组,现在我们已经成功的获取到我们需要的数据了,下面处理并输出到文本里。
3.保存到文本文件中
def write_to_txt(content):
with open('起点小说排行榜.txt','a',encoding='utf-8') as f:
f.write(content+'\n')
这便是爬取一个页面并保存数据的三个核心步骤,而我们需要前XX名的,翻看第二页的链接,发现只需在后面加上page = i 页即可
所以可以把我们的url 成'https://www.qidian.com/rank/click?style=1&page='+str(page)即可。
下面附上完整的代码:
import requests
import re
import json
url = ''
def get_one_page(url):
response = requests.get(url)
return response.text
def parse_one_page(html):
pattern = re.compile('<li\sdata-rid.*?<span class="rank-tag no.*?">(.*?)<cite>.*?<h4><a\shref="(.*?)".*?>(.*?)</a></h4>',re.S)
items = re.findall(pattern,html)
return items
def main(page):
baseurl = 'https://www.qidian.com/rank/click?style=1&page='
url = baseurl + str(page)
html = get_one_page(url)
for item in parse_one_page(html):
with open('起点小说排行榜.txt','a',encoding='utf-8') as f:
num = int(item[0]) + page*20
f.write(str(num)+" "+"https:"+item[1]+" "+item[2]+'\n')
print('第'+str(page)+'页爬取成功')
if __name__ =='__main__':
for i in range(26):
main(i)
至此,我们便可以爬到起点网玄幻小说排名了。
Beautiful Soup
相比于正则,Beautiful Soup 选取标签的方法,会使解析页面更加的灵活。共有四种解析方法,我一般用BeautifulSoup(markup,'lxml')来解析。
基础用法:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>','lxml')
print(soup.p.string)
结果将输出:
Hello
Beautiful Soup有三种选择器:
- 节点选择器 : 如果返回的是单个节点,可以直接string,attrs输出文本和属性,如果是多个节点的生成器,可以转化为列表后取出某个元素,再调用string, attrs输出文本和属性。
- 方法选择器 : find_all()和fing()。
- CSS选择器 : 获取文本可以用string ,也可以用get_text()方法。
下面将用Beautiful Soup 方法解析起点网玄幻小说排行榜:
抓取页面与正则相同,主要是解析不同,Beautiful Soup的解析代码为:
def parse_one_page(html):
soup = BeautifulSoup(html,'lxml')
contents = []
for item in soup.select('.rank-view-list li'):
num = item.select('span')[0].get_text()
src ='https:' + item.select('a')[0].attrs['href']
book_name = item.select('h4 a')[0].get_text()
author = item.select('p a')[0].get_text()
date = item.select('.update span')[0].get_text()
content = num + " " + src + " " + book_name + " " + author + " " + date
contents.append(content)
return contents
将需要的内容存放再一个列表里,打印列表,结果为:
['1 https://book.qidian.com/info/1010191960 大王饶命 会说话的肘子 2018-10-26 20:54', '2 https://book.qidian.com/info/1209977 斗破苍穹 天蚕土豆
2018-09-19 09:59', '3 https://book.qidian.com/info/1011705052 明朝败家子 上山打老虎额 2018-10-26 19:00', '4 https://book.qidian.com/info/1012486119 十恶临城 言桄 2018-10-26 18:31', '5 https://book.qidian.com/info/1002409852 诡神冢 焚天孔雀 2018-10-26 18:31', '6 https://book.qidian.com/info/1011483714 怪物聊天群 泛舟填词 2018-10-26 19:02', '7 https://book.qidian.com/info/1010276884 狼牙兵王 蝼蚁望天 2018-10-26 21:41', '8 https://book.qidian.com/info/1012237441 全球高武 老鹰吃小鸡 2018-10-26 20:27', '9 https://book.qidian.com/info/1011816096 全职武神 流浪的蛤蟆 2018-10-26 10:00', '10 https://book.qidian.com/info/1009704712 牧神记 宅猪 2018-10-26 20:05', '11 https://book.qidian.com/info/1012749331 重回80当大佬 浙东匹夫 2018-10-26 07:49', '12 https://book.qidian.com/info/1010981643 开天录 血红 2018-10-26 12:00', '13 https://book.qidian.com/info/1004608738
圣墟 辰东 2018-10-25 23:43', '14 https://book.qidian.com/info/3602691 修真聊天群 圣骑士的传说 2018-10-26 00:00', '15 https://book.qidian.com/info/1011449952 我在帝都建洞天 万事皆虚 2018-10-26 18:11', '16 https://book.qidian.com/info/1010730481 神级大药师 微了个信 2018-10-26 21:32', '17
https://book.qidian.com/info/3393401 极品全能学生 花都大少 2018-10-26 17:00', '18 https://book.qidian.com/info/1011468740 我要大宝箱 风云指上
2018-10-26 20:00', '19 https://book.qidian.com/info/118447 星辰变 我吃西红柿 2017-09-21 10:23', '20 https://book.qidian.com/info/1010734492 凡人修仙之仙界篇 忘语 2018-10-26 12:30']
返回结果是列表形式的。
后面同样的保存到本地,再爬取其他页,完整的代码如下:
import requests
import pandas
from bs4 import BeautifulSoup
url = 'https://www.qidian.com/rank/click?style=1&page='
def get_one_page(url):
response = requests.get(url)
return response.text
def parse_one_page(html):
soup = BeautifulSoup(html,'lxml')
contents = []
for item in soup.select('.rank-view-list li'):
num = item.select('span')[0].get_text()
src ='https:' + item.select('a')[0].attrs['href']
book_name = item.select('h4 a')[0].get_text()
author = item.select('p a')[0].get_text()
date = item.select('.update span')[0].get_text()
content = num + " " + src + " " + book_name + " " + author + " " + date
contents.append(content)
return contents
def write_to_text(content):
with open('qidian.txt','a',encoding='utf-8') as w:
w.write(content + '\n')
def main(page):
url = 'https://www.qidian.com/rank/click?style=1&page=' + str(page)
html = get_one_page(url)
for content in parse_one_page(html):
write_to_text(content)
if __name__ == '__main__':
for page in range(10):
main(page)
总结
正则解析页面,通用性强,Beautiful Soup则更加简单,更容易理解。