查看原文
其他

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

连享会 连享会 2023-10-24

👇 连享会 · 推文导航 | www.lianxh.cn

连享会 · 文本分析 | 爬虫 | 机器学习

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

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


目录

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

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

    • 2.1 Xpath  常用表达式

    • 2.2 lxml 模块使用

  • 3. 获取详情页数据

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

  • 4. 写在最后

  • 5. 附录:完整 Python 代码

  • 7. 相关推文



温馨提示: 文中链接在微信中无法生效。请点击底部「阅读原文」。或直接长按/扫描如下二维码,直达原文:

本文将以中国自愿减排交易信息平台 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(044):
    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)  快速定位特定的元素及获取的节点信息,数据解析效率飞起。

关联阅读

  • lxml 中文文档 @w3cschool
  • XPath 教程 @w3cschool

2.1 Xpath  常用表达式

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

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

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 等。为了尽可能解释清楚,文中也引申了不少无聊的概念,很高兴你能看到最后 。

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

  • 豆瓣电影 Top 250
  • 古诗文网
  • WallHere 电脑桌面壁纸
  • 百度新闻:海量中文资讯平台
  • 51 job:前程无忧
  • 当当网图书

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

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(044):
    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!-------------")

理论 + 实证:从「读懂模型」到「折腾模型」
🎦 理论模型构建专题
📅 2022 年 4 月 23-24 日 (周六-周日)
🔑 郭凯明副教授 (中山大学)
🍓 课程主页https://gitee.com/lianxh/emodel

7. 相关推文

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

  • 专题:专题课程
    • ⏩ 专题课:文本分析-爬虫-机器学习-2022年4月
    • ⚽助教招聘:文本分析-爬虫-机器学习
    • 助教入选结果 - 连享会 文本分析与爬虫直播课
  • 专题:文本分析-爬虫
    • Stata爬虫:爬取地区宏观数据
    • Stata爬虫:爬取A股公司基本信息
    • Python:爬取东方财富股吧评论进行情感分析
    • Stata爬虫-正则表达式:爬取必胜客
    • Python爬虫: 《经济研究》研究热点和主题分析
  • 专题:Python-R-Matlab
    • Python爬虫:爬取华尔街日报的全部历史文章并翻译
    • Python爬虫:从SEC-EDGAR爬取股东治理数据-Shareholder-Activism
    • Python:爬取巨潮网公告
    • Python:爬取上市公司公告-Wind-CSMAR
    • Python: 6 小时爬完上交所和深交所的年报问询函
    • Python 调用 API 爬取百度 POI 数据小贴士——坐标转换、数据清洗与 ArcGIS 可视化
    • Python 调用 API 爬取百度 POI 数据
    • Python: 批量爬取下载中国知网(CNKI) PDF论文

New! Stata 搜索神器:lianxhsongbl  GIF 动图介绍
搜: 推文、数据分享、期刊论文、重现代码 ……
👉 安装:
. ssc install lianxh
. ssc install songbl
👉  使用:
. lianxh DID 倍分法
. songbl all

🍏 关于我们

  • 连享会 ( www.lianxh.cn,推文列表) 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 直通车: 👉【**百度一下:**连享会】即可直达连享会主页。亦可进一步添加 「知乎」,「b 站」,「面板数据」,「公开课」 等关键词细化搜索。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存