• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
heremei
博客园    首页    新随笔    联系   管理    订阅  订阅
【Heremei】Python爬虫个人学习笔记-2-lxml爬取Atcoder

五一回来以后,听我说假期学了爬虫,于是老师交代我可以尝试爬一下atcoder的题目

https://atcoder.jp/

先贴代码:

from lxml import etree
import requests as rq
import os

def pathCreate(path):
    if(os.path.exists(path)==False):
        os.makedirs(path)

class Subjects:
    def __init__(self,url,path):
        self.html=etree.HTML(rq.get(url).content)
        self.analSubject(path)
    def analPara(self,para,num):
        txt=''
        if(para.xpath('name()')=='h3'):
            return f'## {para.xpath("text()")[0]}\r\n'
        if(para.xpath('name()')=='p'):
            eles=para.xpath('*')
            if(len(eles)>0):
                ftxt=para.xpath('./*[1]/preceding-sibling::text()')
                if(len(ftxt)>0):
                    ftxt=ftxt[0]
                    ftxt=str(ftxt)
                    txt+=ftxt
            for ele in eles:
                txt+=self.analPara(ele,num)
                if(type(ele.tail)==str):
                    txt+=ele.tail
            txt+='\r\n'
            return txt
        if(para.xpath('name()')=='li'):
            eles=para.xpath('*')
            if(len(eles)>0):
                ftxt=para.xpath('./*[1]/preceding-sibling::text()')
                if(len(ftxt)>0):
                    ftxt=ftxt[0]
                    ftxt=str(ftxt)
                    txt+=ftxt
            for ele in eles:
                if(ele.xpath('name()')=='ul'):
                    txt+='\r\n'
                txt+=self.analPara(ele,num)
                if(type(ele.tail)==str):
                    txt+=ele.tail
            return txt
        if(para.xpath('name()')=='ul'):
            lis=para.xpath('*')
            for li in lis:
                for i in range(1,num-1+1):
                    txt+='  '
                txt+='- '
                txt+=self.analPara(li,num+1)
                txt+='\r\n'
            txt+='\r\n'
            return txt
        if(para.xpath('name()')=='pre'):
            txt+='> '
            eles=para.xpath('*')
            if(len(eles)>0):
                ftxt=para.xpath('./*[1]/preceding-sibling::text()')
                if(len(ftxt)>0):
                    ftxt=ftxt[0]
                    ftxt=str(ftxt)
                    txt+=ftxt
            else:
                ftxt=para.xpath('text()')
                if(len(ftxt)>0):
                    txt+=ftxt[0]
            for ele in eles:
                txt+=self.analPara(ele,num)
                txt+=' '
                if(type(ele.tail)==str):
                    txt+=ele.tail
            txt+='\r\n'
            return txt
        if(para.xpath('name()')=='br'):
            return ''
        if(para.xpath('name()')=='var'):
            return f'${para.xpath(".//text()")[0]}$'
        if(para.xpath('name()')=='em'):
            return f'*{para.xpath("text()")[0]}*'
        if(para.xpath('name()')=='code'):
            return f'`{para.xpath("text()")[0]}`'
        if(para.xpath('name()')=='blockquote'):
            txt+='> '
            eles=para.xpath('*')
            for ele in eles:
                txt+=self.analPara(ele,num)
            return txt
    def analSubject(self,path):
        title=self.html.xpath('/html/head/title/text()')[0]
        lim=self.html.xpath('//p[contains(text(),"Time Limit")]/text()')[0]
        print(self.html.xpath('name(//p[contains(text(),"Time Limit")])'))
        score=self.html.xpath('//p[contains(text(),"Score")]/var//text()')[0]
        md=f'# {title}\r\n{lim}  \r\nScore:**{score}** points\r\n'
        sections=self.html.xpath('//span[@class="lang-en"]//section')
        for section in sections:
            paras=section.xpath('*')
            for para in paras:
                md+=self.analPara(para,1)
        pathCreate(path)
        open(f'{path}\{title}.md','wb').write(md.encode('utf-8'))

class Contests:
    def __init__(self,url,oripath):
        self.html=etree.HTML(rq.get(url+'/tasks').content)
        toppage=etree.HTML(rq.get(url).content)
        name=toppage.xpath('//h1/text()')[0]
        self.path=rf'{oripath}\{name}'
        pathCreate(self.path)
        self.getIndex()
    def getIndex(self):
        tabIndex=self.html.xpath('//tbody/tr')
        for sub in tabIndex:
            Subjects('https://atcoder.jp'+sub.xpath("./td[2]/a/@href")[0],self.path)

if __name__=='__main__':
    url='https://atcoder.jp/contests/arc118'
    Contests(url,r'D:\Spider_AtCoder')
    

任务目标

爬下atcoder的题目到本地存为markdown

lxml

为什么不用selenium了

selenium需要打开浏览器,加载网页,一方面比较慢,不稳定。另一方面更重要的原因是:当网页中出现父标签下子标签和文本交替出现时,selenium很难正确爬下来文本(一般子标签是插入的数学公式之类,我也需要其中的内容)


lxml是什么

这是一个专门解析xml文档的库,支持xpath,而本次任务中的主要工具就是xpath的定位

用html.xpath('')双引号之间写xpath路径,就可以找到你所需要的节点

XML,Xpath都是什么

先贴上w3school的链接:
XML:https://www.w3school.com.cn/xml/index.asp
XPath:https://www.w3school.com.cn/xpath/index.asp
基础内容不予详述

简单来说,XML和HTML极为类似,以至于高版本的HTML其实就是一种XML文档。Xpath就是在XML文档里寻找内容的语言

开始

准备工作

需要安装两个包:lxml和requests

后者用于将网页文档下载下来,前者用来解析下下来的网页

在cmd里照常安装:pip install lxml和pip install requests

前者只需要加载etree一个类,后者直接全import进来就好

from lxml import etree
import requests as rq

由于刚开始用requests时看的那位博主时把requests简写成rq,我也这样习惯了(并没有)

解析任务

下载文档

分两步:下载字符串的文档;把文档转化为xpath用的形式

html=etree.HTML(rq.get(url).content)

rq.get(url).content返回url页面的文档etree.HTML()则把文档转换格式

进行解析

我们先来解析单个题目:以https://atcoder.jp/contests/arc118/tasks/arc118_a为例

若是处理一整个比赛,就先从比赛页面获取目录一个个搞就好,不做详述,都在代码里

首先有三个单独的信息:标题,时空限制和分数

标题

我们发现标题在<head>/<title>里,直接找文本就好

html.xpath('/html/head/title/text()')[0]

这里重点介绍一些大坑!

使用xpath返回的大部分东西都是一个列表,只有name()才会返回一个值
所以我们都得加一个[0]才能获取某一个元素,且还需要在很多地方先判断列表非空,要不然会提示out of range


text()只会提取当前节点内遇到的第一部分字符

这样举例:我有这样的xml文档:

<a>"Part 1"<b>"Son's text"</b>"Part 2"</a>

那么a.xpath('text()')的返回值将会是 "Part 1"

<a><b>"Son's text"</b>"Part 2"</a>

这样a.xpath('text()')的返回值将会是 "Part 2"

这后面会用到现在介绍的这些大坑

时空限制和分数

对于这些本身很有的信息,我们直接使用xpath中的contains()函数解决

contains(a,b)即a中包含b则返回True否则返回False

我们观察到这些信息都是在<p>标签里的,所以找文本信息里有"Time Limit"和"Score"字样的<p>标签

注意Score中数字信息是被放在var里面的里面被嵌套着,但由于var里面只有这一个文本,直接var//text()获取就好

lim=html.xpath(//p[contains(text(),"Time Limit")]/text())[0]
score=html.xpath(//p[contains(text(),"Score")]/var//text())[0]

程序结构

定义类:Subject 用来处理题目
我们观察到题目是由一个个section组成的
用sections=self.html.xpath('//span[@class="lang-en"]//section')直接获取section组成的列表,要注意网站是由en和jp两种语言的,我们需要找到语言是从哪里分的并进行筛选

接着for section in sections一个一个处理

每个section都是由若干个元素组成,section内部没有单独的文本
paras=section.xpath('*')获取所有子元素(没有更远的孙子辈之类的后代,*只会获取子代)

同样for para in paras一个个处理

我们处理每一个段落都用一个analPara()方法来解决,在里面写一个个if(name()=="")来处理不同名字的元素。这样做的好处是,大部分时候我们的标签底下还会继续嵌套,我们直接调用analPara()递归会更方便。

几个重点标签的处理:文本夹杂子标签

对于<p> <li> <pre>这种的,都是我们之前讲的:父标签下既有文本也有子标签,子标签里还有文本,父标签下的文本还被子标签隔开了,我们采用如下方法:

eles=para.xpath('*')
if(len(eles)>0):
    ftxt=para.xpath('./*[1]/preceding-sibling::text()')
    if(len(ftxt)>0):
        ftxt=ftxt[0]
        ftxt=str(ftxt)
        txt+=ftxt
else:
    ftxt=para.xpath('text()')
    if(len(ftxt)>0):
        txt+=ftxt[0]
for ele in eles:
    txt+=self.analPara(ele,num)
    txt+=' '
    if(type(ele.tail)==str):
        txt+=ele.tail

etree有这样的成员变量:tail,他是该标签(不是标签里面)后面紧跟的文本,直到下一个兄弟标签或父标签结束为止。

以此为例:

<a>Part 1<b>Son's text</b>Part 2<c></c></a>

b.tail返回"Part 2"

这样的话,我们的思路就是:1 先判断确实有子标签 2 获取第一个子标签之前的文本 3 获取父标签下面所有子标签,循环获取子标签内 的文本,再获取子标签后的文本

步骤1只需要先获取所有子标签,然后判断列表长度就好。步骤3一个个获取文本,再调用tail

步骤2采取以下代码:

ftxt=para.xpath('./*[1]/preceding-sibling::text()')

意为para下第一个子元素前面的同辈的文本信息

要注意xpath的所有索引都是从1开始的,不是从0开始!

始终不要忘记判断列表是否为空

看w3school教程时时刻注意节点和元素的不同

总的代码如下:

if(len(eles)>0):
    ftxt=para.xpath('./*[1]/preceding-sibling::text()')
    if(len(ftxt)>0):
        ftxt=ftxt[0]
        ftxt=str(ftxt) #注意这里要转一下类型,没法直接加
        txt+=ftxt
else:
    ftxt=para.xpath('text()')
    if(len(ftxt)>0):
        txt+=ftxt[0]
for ele in eles:
    txt+=self.analPara(ele,num)
    txt+=' '
    if(type(ele.tail)==str):
        txt+=ele.tail

几个重点标签的处理:ul的嵌套

不像其他的格式,在markdown里,如下的格式是没法直接嵌套的:

  • a
  • b
  • c
    • c1
    • c2
    • c3

书写方式是:第一层-加空格,第二层两个空格加-加空格,第四层四个空格加-加空格

所以我们给analPara增加一个参数:num。每次多嵌套一次就++,输入每一行li的时候加2*(num-1)个空格就好

后记

本文只讲了一些笔者认为重要的地方,细节部分请自己看代码

由于atcoder有很多格式,所以说每种格式都需要写一个单独的if,有很多我目前都没遇到,就先不改了,你是需要自己去写每种标签的转换的。

对于name():name()很奇怪的只会返回一个列表,且不能/step1/step2/step3/name()这样搞,只能name()或者name(/step1/step2/step3)才不会报错……反正就是很特殊

内容可以为空
posted on 2021-05-13 18:51  heremei  阅读(423)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3