五一回来以后,听我说假期学了爬虫,于是老师交代我可以尝试爬一下atcoder的题目
先贴代码:
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)才不会报错……反正就是很特殊
浙公网安备 33010602011771号