Python爬虫2:小白系列之requests和lxml

发布时间:2022-03-15 阅读 1730

Stata连享会   主页 || 视频 || 推文 || 知乎 || Bilibili 站

温馨提示: 定期 清理浏览器缓存,可以获得最佳浏览体验。

New! lianxh 命令发布了:
随时搜索推文、Stata 资源。安装:
. ssc install lianxh
详情参见帮助文件 (有惊喜):
. help lianxh
连享会新命令:cnssc, ihelp, rdbalance, gitee, installpkg

课程详情 https://gitee.com/lianxh/Course

课程主页 https://gitee.com/lianxh/Course

⛳ Stata 系列推文:

PDF下载 - 推文合集

作者:初虹
E-mail:chuhong@mail.sdufe.edu.cn
个人公众号:虹鹄山庄

提示:本文是系列文章的第二篇,建议你先看完上一篇,再来继续。


目录


本文将以中国自愿减排交易信息平台 China Certified Emission Reduction Exchange Info-Platform 为例,详细介绍常见爬虫的第二种类型~

网站页面
网站页面

先上图,一共 44 页,共 861 条数据~ 乍一看,比上篇文章的案例稍微复杂了一点,因为要根据项目 URL 进入详情页爬取具体信息。不过别怕,同样按照爬虫四步来分析。

  1. 准备 URL 列表 (Uniform Resource Locator,统一资源定位符,即我们通常说的网址。)
  2. 遍历 URL,发送请求*(Request),获取响应(Response)*
  3. 提取数据
  4. 数据存储

1. 获取所有页面的 URL:url_page

我们的最终目标是进入详情页获取到每个项目详细的备案信息,所以在进入详情页之前,我们需要得到每个项目的 URL(url_li),才能根据该 URL 发起对详情页的请求。而获取项目的 URL 需要处理好「翻页」动作,也就是先把这 44 页的页面 URL (url_page)拿到手。

一边点击不同的页码,一边观察浏览器地址栏的变化,是不是可以很容易找到规律。

http://cdm.ccchina.org.cn/zyblist.aspx?clmId=164&page=0  #第1页
http://cdm.ccchina.org.cn/zyblist.aspx?clmId=164&page=3  #第2页 
http://cdm.ccchina.org.cn/zyblist.aspx?clmId=164&page=43 # 第44页

一个 for 循环搭配字符串格式化输出函数 format(),就能很容易解决「翻页」问题。不信你也试试运行下面三行命令,是不是与预期完全一致~

for page_num in range(0, 44):
    url_page = 'http://cdm.ccchina.org.cn/zyblist.aspx?clmId=164&page={}'.format(page_num)
    print(url_page)

关联阅读Python format 格式化函数 | 菜鸟教程

2. 获取所有项目的 URL:`url_li

接下来就得获取项目 URL 了(url_li)。点开几个项目看看浏览器地址栏的变化,是不是和 url_page 基本相同,除了尾部的数字在动态变化,其余都没变~

http://cdm.ccchina.org.cn/zybDetail.aspx?Id=905 #第一个项目
http://cdm.ccchina.org.cn/zybDetail.aspx?Id=906 #第二个项目
http://cdm.ccchina.org.cn/zybDetail.aspx?Id=907 #第三个项目

但是这次可以 for 循环和 format() 的方式解决嘛?恐怕不行。因为我们无法确定 Id 后面的数值是规律可循的、还是随机的。不管怎样,接下来搞定它。

抓包
抓包

拿出我们的侦查工具——抓包,看他究竟藏在哪里。应该不费力就能确定url_page 对应的Request MethodGET,所以直接 requests.get() 便可轻松获取页面对应 HTML。

import requests
from fake_useragent import UserAgent

# 伪装 UA
ua = UserAgent(verify_ssl=False)
for i in range(1,500):
    headers = {
        "User-Agent":ua.random,
    }

# 发起请求、获取响应
resp = requests.get(url=url_page, headers=headers, verify=False)
html = resp.content.decode()

那如何从 HTML 页面中提取数据呢?回答这个问题之前,我们先来回顾一下上篇文章返回的 Response 类型是哪种嘛?JSON 字符串是吧,然后用的 json 模块和 jsonpath 模块提取数据。

而本文返回 HTML 格式,需要通过正则表达式 re 库 、 Beautiful Soup 库或lxml 等模块获取数据。lxml 模块解析速度最快,语法也很简洁,本文重点介绍这个。

lxml 模块作为一款高性能的 Python HTML 、XML 解析器,在解析数据方面离不开 XPath 的加持。 lxml可以利用 XPath(XML Path Language) 快速定位特定的元素及获取的节点信息,数据解析效率飞起。

关联阅读

2.1 Xpath 常用表达式

表达式 描述
nodename 选中该节点的所有子节点
/ 从根节点选取、或者是元素和元素间的过渡
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性
text() 选取文本

除了写 xpath 语法外,还可以直接使用节点选择的工具 XPath Helper 在浏览器中进行语法测试,这是一个浏览器插件,各大浏览器扩展商店均不难下载,若网络不畅,可移步 浏览器技巧:我的常用扩展 寻找谷歌应用商店镜像网站下载插件。

XPath 浏览器插件
XPath 浏览器插件

注意: 该工具是用来学习 xpath 语法的,他们都是从 elements 中匹配数据,elements 中的数据和 Network里 URL 地址对应的 Response 不相同,所以代码中,不建议使用这些工具进行数据的提取。

2.2 lxml 模块使用

  • 导入 lxml 模块的 etree
from lxml import etree
  • etree.HTML 实例化 Element 对象,Element 对象具有 xpath 的方法,返回列表类型,能够接受 bytes 类型的数据和 str 类型的数据。
html = etree.HTML(text) 
ret_list = html.xpath("xpath 字符串")

解析过程中有个小技巧,建议你也形成这个习惯:先分组后提取。如果我们取到的是一个节点,返回的是 element 对象,可以继续使用 xpath 方法,对此可在后面的数据提取过程中先根据某个标签进行分组,分组之后再进行数据提取。

html = etree.HTML(text)
# 先提取一个整组
li_list = html.xpath("//li[@class='item-1']")

#再在每一组中继续进行数据的提取
for li in li_list:
    item = {}
    item["href"] = li.xpath("./a/@href")[0]
    item["title"] = li.xpath("./a/text()")[0]
    # 如果部分数据缺失使得提取出来的数据可能存在对应错误的情况,可使用 if else 三元运算符来解决
    # 三元运算符:exp1 if condition else exp2
    # item["href"] = li.xpath("./a/@href")[0] if len(li.xpath("./a/@href"))>0 else None
    # item["title"] = li.xpath("./a/text()")[0] if len(li.xpath("./a/text()"))>0 else None
    # print(item)

好了,回到我们的案例中来。我们想要在 HTML 源码中获取项目 URL,通过 Ctrl+F 搜索项目名称快速定位到页面对应位置,通过获取 a 标签的 href 属性实现对项目 URL (url_li)的补齐。

抓包分析
抓包分析

抓包分析(上图)可以看出项目信息都存储在 class="li" 里,我们要找补全的 url_li 动态部分,就在 a 标签中。于是,循着「先分组后提取」的思路,通过 xpath 定位到该页面的所有项目并存储为 li_list,再遍历 li_list,将 titlehrefpagenum 存储为字典(dict)类型,然后通过 pandas 存储为 .csv 格式。

html1 = etree.HTML(html_first)  
content_list = []
# 先分组
li_list = html1.xpath("//body//li[@class='li']")

# 再提取
for li in li_list:
    item = {}
    item["href"] = "http://cdm.ccchina.org.cn/"+li.xpath(".//a/@href")[0]
    item["title"] = li.xpath(".//a/@title")[0]
    item["pagenum"] = page_num+1
    content_list.append(item)
    # 持久化存储
    dataframe = pd.DataFrame(content_list)
    dataframe.to_csv("./ccer/ccer_url.csv", encoding='gbk')
time.sleep(random.randint(1 ,2))
print("第"+str(page_num+1)+"页---")

这样就把所有项目的 URL (url_li)爬好并储存在 ccer_url.csv 中了,接下来遍历上图的变量 href,就能爬取详情页数据了。

3. 获取详情页数据

首先,我们需要读取 ccer_url.csvhref 列,作为详情页的 URL。

# 遍历存储好的项目 URL 
df = pd.read_csv('./ccer/ccer_url.csv',encoding='gbk')
for url_li in df['href'].to_list():
    print(url_li)

接下来,发送 get 请求,获取响应。

# 发送请求,获取响应
response = requests.get(url_li, headers=headers, verify=False)
html_second = response.content.decode()
html2 = etree.HTML(html_second)

最后,思路基本同上,按照「先分组后提取」的原则遍历得到每个项目具体信息。

# 数据提取:先分组再提取
td_list = html2.xpath("//div[@class='text_main']/div/table/tbody")
for td in td_list:
	item = {}
	# html.xpath("normalize-space(xpath语法)"") 可以去除\r \n \t
	item["beian_num"]   = td.xpath("normalize-space(./tr[1]/td[2])")
	item["name"]        = td.xpath("normalize-space(./tr[2]/td[2])")
	item["owner"]       = td.xpath("normalize-space(./tr[3]/td[2])")
	item["category"]    = td.xpath("normalize-space(./tr[4]/td[2])")
	item["type"]        = td.xpath("normalize-space(./tr[5]/td[2])")
	item["method"]      = td.xpath("normalize-space(./tr[6]/td[2])")
	item["quantity"]    = td.xpath("normalize-space(./tr[7]/td[2])")
	item["period"]      = td.xpath("normalize-space(./tr[8]/td[2])")
	item["institution"] = td.xpath("normalize-space(./tr[9]/td[2])")
	item["report"]      = td.xpath("normalize-space(./tr[10]/td[2]//span/b/span/a/text())")
	item["time1"]       = td.xpath("normalize-space(./tr[11]/td[2])")
	item["other"]       = td.xpath("normalize-space(./tr[12]/td[2])")

	item_list.append(item)
	df1 = pd.DataFrame(item_list)
	df1.to_csv("./ccer/ccer_item.csv", encoding="gb18030")

于是,所有任务就大功告成啦~

4. 矛与盾:反爬与反反爬

在结束本文之前,还是想要聊聊反爬与反反爬这事儿。爬虫,短期内快速爬取数据的方式会给网站服务器造成不小压力,所以很多网站都有比较严格的反爬措施,比如,常见反爬手段可粗略分为五大类:

  • headers 字段:User-Agentreferercookie
  • IP 地址
  • jsjs 实现跳转、js 生成请求参数或数据加密
  • 验证码
  • 其他:自定义字体(比如:猫眼电影)、CSS像素偏移(比如:去哪儿网)

而对于用户来说,既然你有「盾」护,那就只能以锋「矛」应对了。反反爬的主要思路是尽可能地模拟浏览器,浏览器如何操作,代码中就如何实现。所以反爬与反反爬其实就处于「动态博弈」之中。

下面提了几点反反爬的主要措施,毕竟只有知己知彼了,才能对症下药。

严控 IP 和 headers 是最常见的反爬手段。如果爬取过快,同一个 IP 大量请求了对方服务器,那有可能会被识别为爬虫,也就是我们常听到的「封 IP」。对应的措施可以通过购买高质量的代理 IP 搞定。对于 headers 字段的反爬问题,我们的原则就是「缺啥补啥」,Response 返回的 headers 字段很多,一开始我们不清楚哪些有用、哪些没用,只能一次次尝试,在盲目尝试之前,也可以参考别人的思路。

比如,伪装 UA 需要在请求前添加 User-Agent 字典,当然更好的方式是构建 User-Agent 池随机生成 UA。还有些网站(比如 豆瓣电视剧)必须得加入 referer 字段来反反爬,抑或某些网站需要登录(比如 新浪微博)才能获取全部数据,那我们就需要对应加上referercookie 字段。

关联阅读: UA 池的构建Cookies 池的搭建

关于 js 生成请求参数或数据加密,通过 Selenium 很容易解决;还有现在网上随处可见的验证码也属于反爬手段之一,对于简单的爬虫,我们手动填上就好,但如果大批量爬取就需要通过打码平台或者机器学习的方法来识别,当然打码平台廉价易用没有其他学习成本,更值得推荐。

使用网络爬虫做数据采集也应该有所不为。爬取过快会给对方的网站服务器造成很大压力,恶意消耗网站服务器资源,在道德层面上应该自我节制,练手的项目爬几页即可,重点是思路的理顺和代码的学习。**总之,爬虫需谨慎,慢点就慢点吧 : ) **

4. 写在最后

这两篇文章运用的爬虫知识都十分基础,但提到的两种类型的爬虫却十分常见。进阶操作基本均未涉及,比如易于维护的面向对象编程、提高爬取速度的多线程和多进程、万能爬虫法 Selenium、大型爬虫框架 Scrapy 等。为了尽可能解释清楚,文中也引申了不少无聊的概念,很高兴你能看到最后 。

当然,如果文章能激起你一点点儿想要动手试试的好奇心,实乃荣幸。作为回馈,我也找了几个小项目,适合上手,你可以练一练~

万事难开头,「入门」后便是另一番天地。

5. 附录:完整 Python 代码

# Author:@初虹
# Date:2021-11-14
# mail: chuhong@mail.sdufe.edu.cn
# 个人公众号:虹鹄山庄

# 导入模块
import requests
from lxml import etree
import time
import random
from fake_useragent import UserAgent
from pandas.core.frame import DataFrame
import pandas as pd

# 伪装 UA
ua = UserAgent(verify_ssl=False)
for i in range(1,500):
    headers = {
        "User-Agent":ua.random,
    }

# 爬取项目URL,并储存到文件中
content_list = []
for page_num in range(0, 44):
    url_page = 'http://cdm.ccchina.org.cn/zyblist.aspx?clmId=164&page={}'.format(page_num)
    resp = requests.get(url=url_page, headers=headers, verify=False)
    html_first = resp.content.decode()
    html1 = etree.HTML(html_first)    
    
    li_list = html1.xpath("//body//li[@class='li']")
    
    for li in li_list:
        item = {}
        item["href"] = "http://cdm.ccchina.org.cn/"+li.xpath(".//a/@href")[0]
        item["title"] = li.xpath(".//a/@title")[0]
        item["pagenum"] = page_num+1
        content_list.append(item)
        dataframe = pd.DataFrame(content_list)
        dataframe.to_csv("./ccer/ccer_url.csv", encoding='gbk')
    time.sleep(random.randint(1 ,2))
    print("第"+str(page_num+1)+"页---")
print("Finished!--------------")

# 定义空列表
beian_num = []
name = []
owner = []
category = []
type = []
method = []
quantity = []
period = []
institution = []
report = []
time1 = []
other = []
item_list = []

# 读取文件中的项目 URL 并遍历
df = pd.read_csv('./ccer/ccer_url.csv',encoding='gbk')
for url_li in df['href'].to_list():
    # 发送请求、获取响应 
    response = requests.get(url_li, headers=headers, verify=False)
    html_second = response.content.decode()
    html2 = etree.HTML(html_second)
    
    # 数据解析(先分组,后提取)
    td_list = html2.xpath("//div[@class='text_main']/div/table/tbody")
    for td in td_list:
        item = {}
        # html.xpath("normalize-space(xpath语法)"") 可以去除\r \n \t
        item["beian_num"]   = td.xpath("normalize-space(./tr[1]/td[2])")
        item["name"]        = td.xpath("normalize-space(./tr[2]/td[2])")
        item["owner"]       = td.xpath("normalize-space(./tr[3]/td[2])")
        item["category"]    = td.xpath("normalize-space(./tr[4]/td[2])")
        item["type"]        = td.xpath("normalize-space(./tr[5]/td[2])")
        item["method"]      = td.xpath("normalize-space(./tr[6]/td[2])")
        item["quantity"]    = td.xpath("normalize-space(./tr[7]/td[2])")
        item["period"]      = td.xpath("normalize-space(./tr[8]/td[2])")
        item["institution"] = td.xpath("normalize-space(./tr[9]/td[2])")
        item["report"]      = td.xpath("normalize-space(./tr[10]/td[2]//span/b/span/a/text())")
        item["time1"]       = td.xpath("normalize-space(./tr[11]/td[2])")
        item["other"]       = td.xpath("normalize-space(./tr[12]/td[2])")
        item_list.append(item)
	 
	 # 持久化存储
        df1 = pd.DataFrame(item_list)
        df1.to_csv("./ccer/ccer_item1.csv", encoding="gb18030")
    # time.sleep(random.randint(1, 3))
    print("第"+str(df['href'].to_list().index(url_li)+1)+"个----")
print("Finished!-------------")

7. 相关推文

Note:产生如下推文列表的 Stata 命令为:
lianxh 爬
安装最新版 lianxh 命令:
ssc install lianxh, replace

相关课程

免费公开课

最新课程-直播课

专题 嘉宾 直播/回看视频
最新专题 文本分析、机器学习、效率专题、生存分析等
研究设计 连玉君 我的特斯拉-实证研究设计-幻灯片-
面板模型 连玉君 动态面板模型-幻灯片-
面板模型 连玉君 直击面板数据模型 [免费公开课,2小时]
  • Note: 部分课程的资料,PPT 等可以前往 连享会-直播课 主页查看,下载。

课程主页

课程主页

关于我们

  • Stata连享会 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 连享会-主页知乎专栏,700+ 推文,实证分析不再抓狂。直播间 有很多视频课程,可以随时观看。
  • 公众号关键词搜索/回复 功能已经上线。大家可以在公众号左下角点击键盘图标,输入简要关键词,以便快速呈现历史推文,获取工具软件和数据下载。常见关键词:课程, 直播, 视频, 客服, 模型设定, 研究设计, stata, plus, 绘图, 编程, 面板, 论文重现, 可视化, RDD, DID, PSM, 合成控制法

连享会小程序:扫一扫,看推文,看视频……

扫码加入连享会微信群,提问交流更方便

✏ 连享会-常见问题解答:
https://gitee.com/lianxh/Course/wikis

New! lianxh 命令发布了:
随时搜索连享会推文、Stata 资源,安装命令如下:
. ssc install lianxh
使用详情参见帮助文件 (有惊喜):
. help lianxh