超贴心的,手把手教你写爬虫

目录

人生苦短我用Python,本文助你快速入门这篇文章中,学习了Python的语法知识。现在我们就拿Python做个爬虫玩玩,如果中途个别API忘了可以回头看看,别看我,我没忘!(逃

网络编程

​ 学习网络爬虫之前,有必要了解一下如何使用Python进行网络编程。既然说到网络编程,对于一些计算机网络的基础知识最好也有所了解。比如HTTP,在这里就不讲计算机基础了,贴出我之前的一篇博客。感兴趣的可以看看图解HTTP常见知识点总结

​ 网络编程是Python比较擅长的领域,内置了相关的库,第三方库也很丰富。下面主要介绍一下内置的urllib库和第三方的request库。

urllib库

​ urllib是Python内置的HTTP请求库,其使得HTTP请求变得非常方便。首先通过一个表格列出这个库的内置模块:

模块 作用
urllib.request HTTP请求模块,模拟浏览器发送HTTP请求
urllib.error 异常处理模块,捕获由于HTTP请求产生的异常,并进行处理
urllib.parse URL解析模块,提供了处理URL的工具函数
urllib.robotparser robots.txt解析模块,网站通过robots.txt文件设置爬虫可爬取的网页

​ 下面会演示一些常用的函数和功能,开始之前先import上面的几个模块。

urllib.request.urlopen函数

​ 这个函数的作用是向目标URL发送请求,其主要有三个参数:url目标地址、data请求数据、timeout超时时间。该函数会返回一个HTTPResponse对象,可以通过该对象获取响应内容,示例如下:

response = urllib.request.urlopen("https://www.baidu.com/")
print(response.read().decode("utf8")) # read()是读取响应内容。decode()是按指定方式解码

​ 可以看到我们使用这个函数只传入了一个URL,没传入data的话默认是None,表示是GET请求。接着再演示一下POST请求:

param_dict = {"key":"hello"} # 先创建请求数据
param_str = urllib.parse.urlencode(param_dict) # 将字典数据转换为字符串,如 key=hello
param_data=bytes(param_str,encoding="utf8") # 把字符串转换成字节对象(HTTP请求的data要求是bytes类型)
response = urllib.request.urlopen("http://httpbin.org/post",data=param_data) #这个网址专门测试HTTP请求的
print(response.read())

​ timeout就不再演示了,这个参数的单位是秒。怎么请求弄明白了,关键是要解析响应数据。比如响应状态码可以这么获取:response.status。获取整个响应头:response.getheaders(),也可以获取响应头里面某个字段的信息:response.getheader("Date"),这个是获取时间。

urllib.request.Request类

​ 虽然可以使用urlopen函数非常方便的发送简单的HTTP请求,但是对于一些复杂的请求操作,就无能为力了。这时候可以通过Request对象来构建更丰富的请求信息。这个类的构造方法有如下参数:

参数名词 是否必需 作用
url HTTP请求的目标URL
data 请求数据,数据类型是bytes
headers 头信息,可以用字典来构建
origin_req_host 发起请求的主机名或IP
unverifiable 请求是否为无法验证的,默认为False。
method 请求方式,如GET、POST等
url = "http://httpbin.org/get"
method = "GET"
# ...其他参数也可以自己构建
request_obj = urllib.request.Request(url=url,method=method) # 把参数传入Request的构造方法
response = urllib.request.urlopen(request_obj)
print(response.read())

urllib.error异常处理模块

​ 该模块中定义了两个常见的异常:URLEEror和HTTPError,后者是前者的子类。示例如下:

url = "https://afasdwad.com/" # 访问一个不存在的网站
try:
    request_obj = urllib.request.Request(url=url)
    response = urllib.request.urlopen(request_obj)
except urllib.error.URLError as e:
    print(e.reason) # reason属性记录着URLError的原因

​ 产生URLError的原因有两种:1.网络异常,失去网络连接。2.服务器连接失败。而产生HTTPError的原因是:返回的Response urlopen函数不能处理。可以通过HTTPError内置的属性了解异常原因,属性有:reason记录异常信息、code记录响应码、headers记录请求头信息。

requests库

​ requests库是基于urllib开发的HTTP相关的操作库,相比urllib更加简洁、易用。不过requests库是第三方库,需要单独安装才能使用,可以通过这个命令安装:pip3 install requests

​ 使用urllib中的urlopen时,我们传入data代表POST请求,不传入data代表GET请求。而在requests中有专门的函数对应GET还是POST。这些请求会返回一个requests.models.Response类型的响应数据,示例如下:

import requests
response = requests.get("http://www.baidu.com") 
print(type(response)) #输出 <class 'requests.models.Response'>
print(response.status_code) # 获取响应码
print(response.text) # 打印响应内容

​ 上面的例子调用的是get函数,通常可以传入两个参数,第一个是URL,第二个是请求参数params。GET请求的参数除了直接加在URL后面,还可以使用一个字典记录着,然后传给params。对于其他的请求方法,POST请求也有个post函数、PUT请求有put函数等等。

​ 返回的Response对象,除了可以获取响应码,它还有以下这些属性:

  • content:二进制数据,如图片视频等
  • url:请求的url
  • encoding:响应信息的编码格式
  • cookies:cookie信息
  • headers:响应头信息

​ 其他的函数就不一一演示,等需要用到的时候大家可以查文档,也可以直接看源码。比如post函数源码的参数列表是这样的:def post(url, data=None, json=None, **kwargs):。直接看源码就知道了它需要哪些参数,参数名是啥,一目了然。不过接触Python后,有个非常不好的体验:虽然写起来比其他传统面向对象语言方便很多,但是看别人的源码时不知道参数类型是啥。不过一般写的比较好的源码都会有注释,比如post函数开头就会说明data是字典类型的。

​ urllib库中可以用Request类的headers参数来构建头信息。那么最后我们再来说一下requests库中怎么构建headers头信息,这在爬虫中尤为重要,因为头信息可以把我们伪装成浏览器。

​ 我们直接使用字典把头信息里面对应的字段都填写完毕,再调用对应的get或post函数时,加上headers=dict就行了。**kwargs就是接收这些参数的。

​ 网络编程相关的API暂时就讲这些,下面就拿小说网站和京东为例,爬取上面的信息来练练手。

用爬虫下载小说

​ 在正式写程序之前有必要说说爬虫相关的基础知识。不知道有多少人和我一样,了解爬虫之前觉得它是个高大上、高度智能的程序。实际上,爬虫能做的我们人类也能做,只是效率非常低。其爬取信息的逻辑也很朴实无华:通过HTTP请求访问网站,然后利用正则表达式匹配我们需要的信息,最后对爬取的信息进行整理。虽然过程千差万别,但是大体的步骤就是这样。其中还涉及了各大网站反爬虫和爬虫高手们的反反爬虫。

​ 再者就是,具体网站具体分析,所以除了必要的后端知识,学习爬虫的基本前提就是起码看得懂HTML和会用浏览器的调试功能。不过这些就多说了,相信各位大手子都懂。

​ 第一个实战我们就挑选一个简单点的小说网站:https://www.kanunu8.com/book3/6879/。 先看一下页面:

超贴心的,手把手教你写爬虫

我们要做的就是把每个章节的内容都爬取下来,并以每个章节为一个文件,保存到本地文件夹中

​ 我们首先要获取每个章节的链接。按F12打开调式页面,我们通过HTML代码分析一下,如何才能获取这些章节目录?当然,如何找到章节目录没有严格限制,只要你写的正则表达式能满足这个要求即可。我这里就从正文这两个字入手,因为章节表格这个元素最开头的是这两字。我们来看一下源码:

超贴心的,手把手教你写爬虫

​ 我们要做的就是,写一个正则表达式,从正文二字开头,以</tbody>结尾,获取图中红色大括号括起来的这段HTML代码。获取到章节目录所在的代码后,我们再通过a标签获取每个章节的链接。注意:这个链接是相对路径,我们需要和网站URL进行拼接。

​ 有了大概的思路后,我们开始敲代码吧。代码并不复杂,我就全部贴出来,主要逻辑我就写在注释中,就不在正文中说明了。如果忘了正则表达式就去上一篇文章里回顾一下吧。

import requests
import re
import os

"""
传入网站的html字符串
利用正则表达式来解析出章节链接
"""
def get_toc(html,start_url):
    toc_url_list=[]
    # 获取目录(re.S代表把/n也当作一个普通的字符,而不是换行符。不然换行后有的内容会被分割,导致表达式匹配不上)
    toc_block=re.findall(r"正文(.*?)</tbody>",html,re.S)[0]
    # 获取章节链接
    # 啰嗦一句,Python中单引号和双引号都可以表示字符串,但是如果有多重引号时,建议区分开来,方便查看
    toc_url = re.findall(r'href="(.*?)"',toc_block,re.S)

    for url in toc_url:
        # 因为章节链接是相对路径,所以得和网址进行拼接
        toc_url_list.append(start_url+url)
    return toc_url_list


"""
获取每一章节的内容
"""
def get_article(toc_url_list):
    html_list=[]
    for url in toc_url_list:
        html_str = requests.get(url).content.decode("GBK")
        html_list.append(html_str)
    # 先创建个文件夹,文章就保存到这里面,exist_ok=True代表不存在就创建
    os.makedirs("动物庄园",exist_ok=True)
    for html in html_list:
        # 获取章节名称(只有章节名的size=4,我们根据这个特点匹配),group(1)表示返回第一个匹配到的子字符串
        chapter_name = re.search(r'size="4">(.*?)<',html,re.S).group(1)
        # 获取文章内容(全文被p标签包裹),并且把<br />给替换掉,注意/前有个空格
        text_block = re.search(r'<p>(.*?)</p>',html,re.S).group(1).replace("<br />","")
        save(chapter_name,text_block)

"""
保存文章
"""
def save(chapter_name,text_block):
    # 以写的方式打开指定文件
    with open(os.path.join("动物庄园",chapter_name+".txt"),"w",encoding="utf-8") as f:
        f.write(text_block)

# 开始
def main():
    try:
        start_url = "https://www.kanunu8.com/book3/6879/"
        # 获取小说主页的html(decode默认是utf8,但是这个网站的编码方式是GBK)
        html = requests.get(start_url).content.decode("GBK")
        # 获取每个章节的链接
        toc_url_list = get_toc(html,start_url)
        # 根据章节链接获取文章内容并保存
        get_article(toc_url_list)
    except Exception as e:
        print("发生异常:",e)

if __name__ == "__main__":
    main()

​ 最后看一下效果:

超贴心的,手把手教你写爬虫

拓展:一个简单的爬虫就写完了,但是还有很多可以拓展的地方。比如:改成多线程爬虫,提升效率,这个小项目很符合多线程爬虫的使用场景,典型的IO密集型任务。还可以优化一下入口,我们通过main方法传入书名,再去网站查找对应的书籍进行下载。

​ 我以多线程爬取为例,我们只需要稍微修改两个方法:

# 首先导入线程池
from concurrent.futures import ThreadPoolExecutor
# 我们把main方法修改一下
def main():
    try:
        start_url = "https://www.kanunu8.com/book3/6879/"
        html = requests.get(start_url).content.decode("GBK")
        toc_url_list = get_toc(html,start_url)
        os.makedirs("动物庄园",exist_ok=True)
        # 创建一个有4个线程的线程池
        with ThreadPoolExecutor(max_workers=4) as pool:
            pool.map(get_article,toc_url_list)
    except Exception as e:
        print("发生异常:",e)

map()方法中,第一个参数是待执行的方法名,不用加()。第二个参数是传入到get_article这个方法的参数,可以是列表、元组等。以本代码为例,map()方法的作用就是:会让线程池中的线程去执行get_article,并传入参数,这个参数就从toc_url_list依次获取。比如线程A拿了``toc_url_list`的第一个元素并传入,那么线程B就拿第二个元素并传入。

​ 既然我们知道了map()方法传入的是一个元素,而get_article原来接收的是一个列表,所以这个方法也需要稍微修改一下:

def get_article(url):
    html_str = requests.get(url).content.decode("GBK")
    chapter_name = re.search(r'size="4">(.*?)<',html_str,re.S).group(1)
    text_block = re.search(r'<p>(.*?)</p>',html_str,re.S).group(1).replace("<br />","")
    save(chapter_name,text_block)

​ 通过测试,在我的机器上,使用一个线程爬取这本小说花了24.9秒,使用4个线程花了4.6秒。当然我只测试了一次,应该有网络的原因,时间不是非常准确,但效果还是很明显的。

爬取京东商品信息

​ 有了第一个项目练手,是不是有点感觉呢?其实也没想象的那么复杂。下面我们再拿京东试一试,我想达到的目的是:收集京东上某个商品的信息,并保存到Excel表格中。这个项目中涉及了一些第三方库,不过大家可以先看我的注释,过后再去看它们的文档。

​ 具体问题具体分析,在贴爬虫代码之前我们先分析一下京东的网页源码,看看怎么设计爬虫的逻辑比较好。

​ 我们先在京东商城的搜索框里输入你想收集的商品,然后打开浏览器的调式功能,进入到Network,最后再点击搜索按钮。我们找一下搜索商品的接口链接是啥。

超贴心的,手把手教你写爬虫

​ 图中选中的网络请求就是搜索按钮对应的接口链接。拿到这个链接后我们就可以拼接URL,请求获取商品信息了。我们接着看商品搜索出来后,是怎么呈现的。

超贴心的,手把手教你写爬虫

​ 通过源码发现,每个商品对应一个li标签。一般商城网站都是由一些模板动态生成的,所以看上去很规整,这让我们的爬取难度也降低了。

​ 我们点进一个看看每个商品里又包含什么信息:

超贴心的,手把手教你写爬虫

​ 同样相当规整,最外层li的class叫gl-item,里面每个div对应一个商品信息。知道这些后,做起来就相当简单了,就用这些class的名称来爬取信息。我还是直接贴出全部代码,该说的都写在注释里。贴之前说说每个方法的作用。search_by_keyword:根据传入的商品关键词搜索商品。get_item_info:根据网页源码获取商品信息。skip_page:跳转到下一页并获取商品信息。save_excel:把获取的信息保存到Excel。

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from pyquery import PyQuery
from urllib.parse import quote
import re
from openpyxl import Workbook
from fake_useragent import UserAgent

# 设置请求头里的设备信息,不然会被京东拦截
dcap = dict(DesiredCapabilities.PHANTOMJS)
# 使用随机设备信息
dcap["phantomjs.page.settings.userAgent"] = (UserAgent().random)
# 构建浏览器对象
browser = webdriver.PhantomJS(desired_capabilities=dcap)

# 发送搜索商品的请求,并返回总页数
def search_by_keyword(keyword):
    print("正在搜索:{}".format(keyword))
    try:
        # 把关键词填入搜索链接
        url = "https://search.jd.com/Search?keyword=" + \
            quote(keyword)+"&enc=utf-8"
        # 通过浏览器对象发送GET请求
        browser.get(url)
        # 等待请求响应
        WebDriverWait(browser, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".gl-item"))
        )
        pages = WebDriverWait(browser, 10).until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > em:nth-child(1) > b"))
        )
        return int(pages.text)
    except TimeoutException as e:
        print("请求超时:"+e)

# 根据HTML获取对应的商品信息
def get_item_info(page):
    # 获取网页源代码
    html = browser.page_source
    # 使用 PyQuery解析网页源代码
    pq = PyQuery(html)
    # 获取商品的li标签
    items = pq(".gl-item").items()
    datas = []
    # Excel中的表头,如果当前是第一页信息就是添加表头
    if page==1:
        head = ["商品名称", "商品链接", "商品价格", "商品评价", "店铺名称", "商品标签"]
        datas.append(head)
    # 遍历当前页所有的商品信息
    for item in items:
        # 商品名称,使用正则表达式将商品名称中的换行符\n替换掉
        p_name = re.sub("\\n", "", item.find(".p-name em").text())
        href = item.find(".p-name a").attr("href")  # 商品链接
        p_price = item.find(".p-price").text()  # 商品价钱
        p_commit = item.find(".p-commit").text()  # 商品评价
        p_shop = item.find(".p-shop").text()  # 店铺名称
        p_icons = item.find(".p-icons").text()
        # info代表某个商品的信息
        info = []
        info.append(p_name)
        info.append(href)
        info.append(p_price)
        info.append(p_commit)
        info.append(p_shop)
        info.append(p_icons)
        print(info)
        # datas是当前页所有商品的信息
        datas.append(info)
    return datas

# 跳转到下一页并获取数据
def skip_page(page, ws):
    print("跳转到第{}页".format(page))
    try:
        # 获取跳转到第几页的输入框
        input_text = WebDriverWait(browser, 10).until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > input"))
        )
        # 获取跳转到第几页的确定按钮
        submit = WebDriverWait(browser, 10).until(
            EC.element_to_be_clickable(
                (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > a"))
        )
        input_text.clear()  # 清空输入框
        input_text.send_keys(page)  # 在输入框中填入要跳转的页码
        submit.click()  # 点击确定按钮

        # 等待网页加载完成,直到页面下方被选中并且高亮显示的页码,与页码输入框中的页码相等
        WebDriverWait(browser, 10).until(
            EC.text_to_be_present_in_element(
                (By.CSS_SELECTOR, "#J_bottomPage > span.p-num > a.curr"), str(page))
        )
        # 获取商品信息
        datas = get_item_info(page)
        # 如果有数据就保存到Excel中
        if len(datas) > 0:
            save_excel(datas, ws)
    except TimeoutException as e:
        print("请求超时:", e)
        skip_page(page, ws)  # 请求超时,重试
    except Exception as e:
        print("产生异常:", e)
        print("行数:", e.__traceback__.tb_lineno)

# 保存数据到Excel中
def save_excel(datas, ws):
    for data in datas:
        ws.append(data)


def main():
    try:
        keyword = "手机"  # 搜索关键词
        file_path = "./data.xlsx"  # 文件保存路径
        # 创建一个工作簿
        wb = Workbook()
        ws = wb.create_sheet("京东手机商品信息",0)
        pages = search_by_keyword(keyword)
        print("搜索结果共{}页".format(pages))
        # 按照顺序循环跳转到下一页(就不爬取所有的数据了,不然要等很久,如果需要爬取所有就把5改成pages+1)
        for page in range(1, 5):
            skip_page(page, ws)
        # 保存Excel表格
        wb.save(file_path)
    except Exception as err:
        print("产生异常:", err)
        wb.save(file_path)
    finally:
        browser.close()

if __name__ == '__main__':
    main()

超贴心的,手把手教你写爬虫
​ 从main方法开始,借助着注释,即使不知道这些库应该也能看懂了。下面是使用到的操作库的说明文档:

selenium:Selenium库是第三方Python库,是一个Web自动化测试工具,它能够驱动浏览器模拟输入、单击、下拉等浏览器操作。中文文档:https://selenium-python-zh.readthedocs.io/en/latest/index.html。部分内容还没翻译完,也可以看看这个:https://zhuanlan.zhihu.com/p/111859925。selenium建议安装低一点的版本,比如pip3 install selenium==2.48.0 ,默认安装的新版本不支持PhantomJS了。

PhantomJS:是一个可编程的无界面浏览器引擎,也可以使用谷歌或者火狐的。这个不属于Python的库,所以不能通过pip3直接安装,去找个网址http://phantomjs.org/download.html下载安装包,解压后,把所在路径添加到环境变量中(添加的路径要到bin目录中)。文档:https://phantomjs.org/quick-start.html

openpyxl:Excel操作库,可直接安装,文档:https://openpyxl.readthedocs.io/en/stable/。

pyquery:网页解析库,可直接安装,文档:https://pythonhosted.org/pyquery/

拓展:可以加上商品的选择条件,比如价格范围、销量排行。也可以进入到详情页面,爬取销量排行前几的评价等。

​ 今天就说到这里了,有问题感谢指出。如果有帮助可以点个赞、点个关注。接下来会学更多爬虫技巧以及其他的后端知识,到时候再分享给大家~

参考资料:《Python 3快速入门与实战》、《Python爬虫开发》、各种文档~

发表评论

评论已关闭。

相关文章