内容是对《Python爬虫开发:从入门到实战》的摘录、理解、代码实践和遇到的问题。
全自动爬虫
上一个阶段案例中,实现了半自动爬虫开发,要实现“全自动“的爬虫,还需要把其中“手动复制粘贴网页源代码”的部分自动化。可以通过requests库实现这一点。
requests是Python的一个第三方HTTP(Hypertext Transfer Protocol 超文本传输协议)库,它比Python自带的网络库urlib更简单、方便和人性化。使用requests可以让Python实现访问网页并获取源码的功能。
requests是第三方库,需要手动安装,手动安装过程省略。
使用requests获取网页源代码
网页有很多种打开方式,最常见的是GET方式和POST方式。在浏览器中可以通过直接输入网址访问的页面就是使用了GET方式;只能通过从另一个页面单击某个链接或某个按钮以后转跳,而不能通过输入网址访问的网页就是使用了POST方式。
有一些网页,使用GET和POST方式访问相同的网址得到的结果不一样;还有一些网页只能使用POST方式访问,使用GET方式访问会直接返回错误信息。
GET方式
对于使用GET方式的网页,在Python里可以使用requests的get()方法获取网页的源代码。
import requests
html = requests.get(' https://tieba.baidu.com/p/8466216461')
# 此时直接打印html得到的是“<Response [200]>”,无法得到网页源码
html_bytes = html.content # 使用.content属性来显示bytes型的网页源码
html_str = html_bytes.decode() # 将bytes型的网页源代码解码为字符串型的源代码,因为在bytes型的数据类型下,中文是无法显示的
以上可以缩写为:
import requests
html = requests.get('https://tieba.baidu.com/p/8466216461').content.decode()
.decode()的参数可以省略,省略时默认使用UTF-8编码格式来把bytes型解码为字符串型的源码。对于编码格式本身不是UTF-8的页面,就需要在括号里写目标编码格式的名字,如:.decode(‘GBK’)等。
编码格式有几十种,但最常见的是‘UTF-8’,’GBK’,’GB2312’和’GB18030’。不确定编码格式的时候可以每种都尝试一下,打印源代码后中文正常显示就行。
POST方式
使用requests的post()方法
import requests
data = {
'key1': 'value1',
'key2': 'value2'
}
html_formdata = requests.post('网址', data=data).content.decode()
# 使用formdata提交数据
其中,data这个字典的内容和项数需要根据实际情况修改,Key和Value在不同的网站是不一样的,构造这个字典是做爬虫的任务之一。
还有一些网站,提交的内容需要时JSON格式的,因此post()方法的参数需要进行一些修改
html_formdata = requests.post('网址', json=data).content.decode()
# 使用json提交数据
这样requests就可以自动将字典转换为JSON字符串。
(关于data字典的构造方式还不太清楚,所以这个部分没有获取实际网页源码的例子)
结合requests对“半自动爬虫”改进
import json
import re
import csv
import requests
'''
“半自动爬虫”的源码获取方法
# 读入保存在code.txt中的网页源码
with open('code.txt', encoding='UTF-8') as f:
source = f.read()
'''
# 使用requests获取源码
source = requests.get('https://tieba.baidu.com/p/8537125946').content.decode()
# 根据源码与网页内容的比对,找出每一层回复开始、结尾的规律,获得按层划分的大文本块
every_reply = re.findall(
'<li class="d_nameplate">(.*?)<ul class="p_props_tail props_appraise_wrap">',
source, re.S)
# 将分层写入txt文件,便于观察改进
with open('separated.txt', 'w', encoding='UTF-8') as f:
for eachRow in every_reply:
f.write(eachRow)
for i in range(7):
f.write('\n')
# 从每一个大文本块里提取出该层回复的回复者、回复内容和回复时间,保存在result_list中
result_list = []
for each in every_reply:
result = {}
result['userName'] = re.findall('&fr=pb" target="_blank">(.*?)</a>', each, re.S)
result['content'] = re.findall('class="d_post_content j_d_post_content " style="display:;">(.*?)</div><br>', each,
re.S)
result['reply_time'] = re.findall('<span class="tail-info">(2023.*?)</span><div', each, re.S)
result_list.append(result)
# 将result_list写入csv文件
with open('file.csv', 'w', encoding='UTF-8-sig', newline="") as f:
writer = csv.DictWriter(f, fieldnames=['userName', 'content', 'reply_time'])
writer.writeheader()
writer.writerows(result_list)
多线程爬虫
上面实现的爬虫只有一个进程、一个线程,因此称为单线程爬虫。单线程爬虫每次只访问一个页面,而一个页面最多也就几百KB,不能充分利用计算机的网络带宽,多出来的网速和从发起请求到得到源码中间的时间都被浪费了。如果可以同时让爬虫访问10个页面,就相当于爬取速度提高了10倍。为了达到这个目的,就需要使用多线程技术。
一点补充:
Python这门语言在设计的时候,有一个全局解释器锁(Global Interpreter Lock, GIL),这导致Python的多线程都是伪多线程,即本质上还是一个线程,但这个线程每件事只做几毫秒,几毫秒后就保存现场,换做其他事情…微观上的单线程,在宏观上就像同时在做几件事。这种机制在I/O密集型的操作上影响不大,但在计算密集型的操作上会对性能产生非常大的影响,所以涉及计算密集型的程序,需要使用多进程,Python的多进程不受GIL的影响。
爬虫属于I/O密集型的程序,所以使用多线程可以大大提高爬取效率。
多进程库multiprocessing
multiprocessing是Python的多进程库,用来处理与多进程相关的操作。但是由于进程与进程之间不能直接共享内存和堆栈资源,而且启动新的进程开销也比线程大得多,因此使用多线程来爬取比使用多进程有更多的优势。multiprocessing下面有一个dummy模块,它可以让Python的线程使用multiprocessing的各种方法。
dummy下有一个Pool类,它用来实现线程池。这个线程池有一个map方法,可以让线程池里面所有线程都“同时”执行一个函数。
开发多线程爬虫
多线程操作与异步操作
本小节讲到的是多线程操作,后面的章节会讲到异步操作的爬虫框架。在需要操作的动作数量不大时,这两种方式的性能没什么区别,但当动作的数量大量增大,多线程的效率提升就会下降,甚至比单线程还差,这种情况下就需要采用异步操作。
比较单线程爬虫和多线程爬虫爬取百度首页的性能差异
使用单线程循环访问百度首页100次并计算时间:
import json
import re
import csv
import time
import requests
stat = time.time()
for i in range(100):
requests.get('https://www.baidu.com/')
end = time.time()
print(f'单线程循环访问100次百度首页,耗时为:{end-stat}')
单线程循环访问100次百度首页,耗时为:32.5662727355957
使用五个线程访问百度首页100次并计算时间:
import json
import re
import csv
import time
import multiprocessing
import requests
def query(url):
requests.get(url)
if __name__ == '__main__':
stat = time.time()
url_list = []
for i in range(100):
url_list.append('https://www.baidu.com/')
pool = multiprocessing.Pool(5)
pool.map(query, url_list)
# 线程池的map()方法接收两个参数,第一个参数是所有线程都要执行的函数的函数名(仅仅是函数名字,不带括号),第二个参数是一个列表,是将要用于前面函数的参数们
end = time.time()
print(f'使用5个线程访问100次百度首页,耗时:{end - stat}')
使用5个线程访问100次百度首页,耗时:10.967380285263062
另外:
需要注意的是,在Windows上要想使用进程模块,就必须把有关进程的代码写在if __name__ == ‘__main__’ 内,否则在Windows下使用进程模块会产生异常。Unix/Linux下则不需要。
python 进程池multiprocessing.Pool(44) - 知乎 (zhihu.com)
可以看出5个线程同时运行确实比单线程效率高。
但需要注意,并非线程池设置得越大越好。从上面的结果也可以看出,5个线程的运行时间实际是大于单线程运行时间五分之一的,多出来的其实就是线程切换的时间,这从侧面反映了Python的多线程在微观上还是串行的。因此,如果线程池设置得过大,线程切换导致的开销可能会抵消多线程带来的性能提升。
爬虫常见的搜索算法
深度优先搜索和广度优先搜索。
深度优先搜索:把一个大类的所有信息都爬取完后再爬取下一个大类的所有信息。
广度优先搜索:先爬取完所有大类,再进一步爬取大类下的信息。