其实这篇不涉及什么高大上的反爬,但是实在不知道要把这篇文章归类到哪里,就直接先扔这里吧。
先吐槽一句:萌娘百科绝对是我再也不想爬第二次的网站。
第二句:(真理)把网站弄得越乱越让人摸不着头脑,就是最好的反爬手段,比什么加密都管用。
先看需求:爬取萌娘百科中所有游戏角色介绍
正常对于一个百科类网站而言,一拿到这个需求,第一反应肯定是先弄一个游戏角色名的list,然后挨个进行search,抓取返回的页面内容。然而当我想进一步索要游戏角色名的list时,得到的回复是:没有现成的游戏角色名,把萌娘上有的游戏角色爬了就行。
歪日???
一开始的思路还是先自己整一个游戏角色名的list,这样我面临一个很大的问题就是,量上不去。即便在度娘的帮助下,我能找到的游戏角色也就那么几个,再去除一些萌娘没有收录的词条,就所剩无几了。
没辙,死磕网站吧。
然后我找到了这个:https://mzh.moegirl.org.cn/Special:特殊页面
我一度以为自己发现了宝藏,直到我在让人眼花撩乱的页面中怎么也找不到我要的游戏角色分类…这分类都是啥啊,太乱了吧。
后来勉强又找到了两个看上去能爬的、勉强贴近我的需求的页面:https://mzh.moegirl.org.cn/Category:分类、https://mzh.moegirl.org.cn/Category:电子游戏角色列表
可算是有点东西了。虽然这肯定不是所有的链接,但好歹能缓解我的燃眉之急。
这也算是给了一个新的思路:爬这种百科类的网站的时候,可以尝试去定位网站的根目录,然后从根目录里沿着网站的分类树一级一级地往下搜索,把需要的链接抓全。
这是爬萌娘遇到的第一个问题,勉强算解决了吧。
爬的过程中发现,萌娘的每一个词条的最底端,会附上一大堆的相关或者参考链接,有很多游戏角色都会给总给在一个类似下边的大表中:
这张大表除了有游戏角色,还有一堆乱七八糟的分类,并且,每个页面下的大表可能都不一样,当然也有可能一样。
那我只要里边的角色名和链接,我该怎么操作?
观察页面源代码发现,表格的第一列(即带有xx角色字样的标题列)跟其右边的内容是一一对应的,同在一个tr标签下,也就是说,他们是兄弟节点。
XPath的following-sibling
轴可以选择当前节点之后的兄弟节点:
from lxml import etree # 使用XPath选择包含"角色"文字的td标签,并获取其下一个兄弟节点的内容 selected_elements = tree.xpath('//td[contains(text(), "角色")]/following-sibling::td[1]')
在XPath表达式中,//td[contains(text(), "角色")]/following-sibling::td[1]
表示选择包含"角色"文字的td标签,然后获取其下一个兄弟节点的内容。
最终的代码如下:
def get_table_links(url): headers = { 'User-Agent': random.choice(USER_AGENTS_LIST), 'Referer': url } resp = requests.get(url, headers=headers) text = resp.text html = etree.HTML(text) # 所有罗列了游戏角色的表格定位 hrefs = html.xpath('(//table[@class="navbox"])[1]//td[contains(text(), "角色")]/following-sibling::td[1]//a[not(@class="new") and not(starts-with(@href, "http"))]/@href') return hrefs
其中,//a[not(@class="new") and not(starts-with(@href, "http"))]
意味获取所有class不为“new”的a标签下的链接,之所以要排除掉这个class,是因为萌娘中,class="new"意味着该页面不存在。
最后感慨,我确实、十分、非常佩服萌娘网站的设计者,顶礼膜拜的那种。
补充,抓网页内容的时候,我需要抓取网页中的div下的所有文本,但是总抓到一些乱七八糟的css代码、js代码之类的东西,一查看,是萌娘中的一些字体采用了加粗、颜色等样式修饰,有些还设置了不同的显示模式(比如鼠标停留在抹黑的区域才给显示文字这种,挺有意思的)。反应岛页面源代码中,就是p标签下的文字后边跟着一个style标签或者script标签,类似下边这种:
<p> 在这个世界中,「崩坏」是一种 <span>周期性 <style>.vue3-marquee{display:flex!important;position:relative}.vue3-marquee.horizontal{overflow-x:hidden!important;flex-direction:row!important;</style> </span> 灾难,它伴随文明而生,以毁灭文明为最终目标。 </p>
直接用xpath抓取p标签下的所有文本tree.xpath('//div[@class="mw-parser-output"]//text()
,会把style或者script标签下乱七八糟的那一堆东西也抓到(一开始用这个跑了几个页面发现总是能抓到我不想要的那一堆东西),查了下网上的资料,根据xpath语法在后边加一个not把不想要的标签过滤掉就可以:
selected_elements = html.xpath('//div[@class="mw-parser-output"]//text()[not(ancestor::style) and not(ancestor::script)]')
这里总结一下,following-sibling
找的是兄弟节点,一般把这个写在xpath的路径下,用于路径的定位;ancestor
找的是祖先节点(直译hhhh),可以放在xpath表达式最后,用于对不相关标签的一个清洗。另外,xpath中的not
、and
语句可以跟@class
、@href
结合使用,根据(比如a
)标签内的属性对标签进行过滤和筛选。
最后再补充个题外话,所有的数据都是为后边的算法服务的,所以数据存储的时候必须考虑到后续代码读取数据是否方便,是否可以写一段通用的代码读取所有的数据。所以在存数据的时候,如果暂时不写入数据库的话,最好能按行存成txt
或者jsonl
文件格式,具体哪个看数据的复杂程度,简单存个链接或者关键字啥的用txt
就可以了,字段比较复杂的存成jsonl
。特别是对于爬虫数据来说,不要轻易存json
文件!因为当内容太多写入的文件太大的时候(上限是啥我也没试过,盲猜取决于你的电脑运行内存),json
文件无法一次性被loads进python直接进行读取,程序会报错的,导致最后还是得按行读取json
文件,然后就是被各种奇奇怪怪的换行符和左右括号支配的恐惧,最后还得重新爬(如果你有别的解决方案欢迎踢我)。
以萌娘为例,记录一下我们讨论数据存储格式和要保留的字段的大致经过:
第一版:
{ "title":页面总标题, "目录1":内容1, "目录2":内容2, ... }
否了,萌娘的每个页面的目录又不是完全一样,这样后边没法读取。
第二版:
{ "title":页面总标题, "content":{ "目录1":内容1, "目录2":内容2, ... }, }
这个倒还能看得过去(事实上最后敲定的就是这一版),但是我总觉得还是差点啥。
自己再改的第三版:
{ "title":页面总标题, "content":{ "category":{ 1:"目录1", 2:"目录2", ... } "目录1":内容1, "目录2":内容2, ... }, }
这个可以通过"category"
先定位到各条数据的目录,然后进一步定位内容。但是即使不加"category"
,for循环也可以直接遍历key先找到各个目录然后进一步定位的,多不了几行代码。把目录单独再存一下,可能就是拿空间换时间吧。
最后,为了方便数据追溯,再补充一些其他字段:
{ "title":页面总标题, "content":{ "category":{ 1:"目录1", 2:"目录2", ... } "目录1":内容1, "目录2":内容2 ... }, "url":当前链接, "referer_title":上一级标题, "referer":上一级链接, }