PTT 爬虫

前言

良葛格过世的消息对我来说十分冲击,笔者从国中开始学 C 语言,就是一路看良哥的笔记长大,乃至于后来学的 Java, Python 以及很多软体设计的思维都受到良哥很大影响。人生短暂而脆弱,我们虽然无法带走任何东西,但良哥留下的着作却能够深深影响我们。他的文字流传于世,仿佛他仍然活着。有感于此,取之于网路,用之于网路,决定在这里展开笔者的技术分享之旅。由于笔者过去只有在担任课堂助教时才有编写教学讲义的经验,出社会后的历练也还不够深厚,如果在文章中有发现不好的地方,请大家多多包涵,并欢迎提出建议来讨论。

介绍

本文透过简单的 Python 爬虫程式,爬取 PTT 的文章标题。笔者其实一直想试试看製作一个文章分类的分类器,在训练分类器之前,需要先有个训练资料,而 PTT 论坛就是个爬虫友善的网站,讨论区的资讯量非常丰富,非常适合练习爬虫。本文以西洽板 (C_Chat) 为例,因为西洽板讨论量丰富且没有年龄限制,在实做上更为友善。

目标

希望可以製作一个资料集,获得类似以下格式的资料:

[    {"Category": "问题", "Title": "这是什么神奇宝贝"},    {"Category": "讨论", "Title": "有哪些太晶属性能近乎完美反制啊"},    {"Category": "闲聊", "Title": "SHIROBAKO 白箱圣地巡礼 第四弹"},    ...]

之后我们可以运用这样的资料集,来尝试训练 NLP 模型,例如使用 Title 来预测 Category 是什么。

检查网页元件

在设计爬虫程式之前,要先了解爬取的目标有哪些资讯。标记 (Tag)e.g. div, span, img, a, ..., etc.属性 (Attribute)e.g. id, class, href, ..., etc.要观察目标的资讯,可以简单的透过浏览器对该目标(例如连结、按钮)按右键,选择检查:
以 Chrome 来说,快捷键是 Ctrl + Shift + C,使用 Ctrl + Shift + I 也可以开启类似的介面,但不会追蹤游标指着的物件资讯。

环境

首先我们需要安装 requests, beautifulsoup4 两个套件:

pip install requests beautifulsoup4

爬取网页原始码

透过 requests 套件发送 HTTP Request 并取得网页原始码

import requestsurl = "https://www.ptt.cc/bbs/C_Chat/index.html"resp = requests.get(url)# 将结果存在 Source.html 里面with open("Source.html", "w", encoding="UTF-8") as f:    f.write(resp.text)

基于爬虫的道德伦理,可以只爬一次的东西,就不要爬两次。在练习爬虫的过程中难免会多次发送 Request,在迴圈里面使用爬虫,一个不慎可能会对目标伺服器造成很大的负担,所以我们最好养成把需要操作的资料先存在本机的习惯。

剖析网页原始码

Beautiful Soup 4 这个 Python 套件可以让开发者轻鬆分析 HTML, XML 这种标记语言格式的资料,在爬虫程式这种需要大量操作 HTML 的应用里面有相当大的用处。接下来用 Beautiful Soup 4 把刚刚存下来的 Source.html 进行分析,并尝试提取里面的标题:

from bs4 import BeautifulSoupwith open("Source.html", "r", encoding="UTF-8") as f:    bs = BeautifulSoup(f.read(), features="html.parser")links = bs.find_all("div", attrs={"class": "title"})for link in links:    print(link.text.strip())

以上程式码可能会印出类似以下的结果

[闲聊] 如果虚白是大奶正妹的话,一护会?[闲聊] 其实当五绝也不是那么得好。Re: [闲聊] 世界各国最受欢迎的初代宝可梦[公告] C_Chat板板规 v.16.8 暨好文补M区[公告] 看板活动公告彙整 & 置底推文闲聊区[名人] 批踢踢推广中心-看板知名人物题目募集中[公告] 4-11选举期间从严条款[公告] C_Chat 板 开始举办乐透!

标题字串处理

接下来我们需要把标题的分类与标题本身切开来,处理并且跳过已被删除的文章。这段算是相当单纯的字串,不熟 Python 的朋友不妨自己实做看看,以下是笔者的做法:

def SplitTitle(title: str):    if "本文已被删除" in title:        return    if "[" not in title:        return    if "]" not in title:        return    a = title.index("[") + 1    b = title.index("]")    category = title[a:b].strip()    title = title[b + 1 :].strip()    return {"Category": category, "Title": title}titles = [    "[闲聊] 如果虚白是大奶正妹的话,一护会?",    "[闲聊] 其实当五绝也不是那么得好。",    "Re: [闲聊] 世界各国最受欢迎的初代宝可梦",    "[公告] C_Chat板板规 v.16.8 暨好文补M区",    "[公告] 看板活动公告彙整 & 置底推文闲聊区",    "[名人] 批踢踢推广中心-看板知名人物题目募集中",    "[公告] 4-11选举期间从严条款",    "[公告] C_Chat 板 开始举办乐透!",]for t in titles:    print(SplitTitle(t))

会印出以下结果:

{'Category': '闲聊', 'Title': '如果虚白是大奶正妹的话,一护会?'}{'Category': '闲聊', 'Title': '其实当五绝也不是那么得好。'}{'Category': '闲聊', 'Title': '世界各国最受欢迎的初代宝可梦'}{'Category': '公告', 'Title': 'C_Chat板板规 v.16.8 暨好文补M区'}{'Category': '公告', 'Title': '看板活动公告彙整 & 置底推文闲聊区'}{'Category': '名人', 'Title': '批踢踢推广中心-看板知名人物题目募集中'}{'Category': '公告', 'Title': '4-11选举期间从严条款'}{'Category': '公告', 'Title': 'C_Chat 板 开始举办乐透!'}

造访下一页

只有一页讨论列表的标题,资料量显然是不够的。为了获得更多的资料,我们需要造访讨论列表的「上一页」

透过检查网页元件的方法,我们得知「上一页」是个 classbtn widea 元件

但很不幸的,这里至少有四个按钮都是 btn wide,类似这样的例子在网路爬虫里面其实很常见,需要多多观察。这个例子,我们只需要简单判断元件的文字即可:

from bs4 import BeautifulSoupwith open("Source.html", "r", encoding="UTF-8") as f:    bs = BeautifulSoup(f.read(), features="html.parser")def FindNextPage(bs):    links = bs.find_all("a", attrs={"class": "btn wide"})    for link in links:        if link.text == "‹ 上页":            return link.attrs["href"]print(FindNextPage(bs))

输出结果如下:

/bbs/C_Chat/index17572.html

这边抓到的连结只包含网址的后半而已,记得在前面加上 "https://www.ptt.cc"

完整程式码

整合以上步骤,完整的程式码如下:

import jsonimport requestsfrom bs4 import BeautifulSoup as BSdef Main():    base_url = "https://www.ptt.cc"    sub_url = f"/bbs/C_Chat/index.html"    data = list()    for _ in range(1000):        full_url = f"{base_url}{sub_url}"        bs = BS(requests.get(full_url).text, features="lxml")        titles = bs.find_all("div", attrs={"class": "title"})        for title in titles:            title = title.text.strip()            title = SplitTitle(title)            if title is None:                continue            data.append(title)        sub_url = FindNextPage(bs)    with open("Data.json", "w", encoding="UTF-8") as f:        json.dump(data, f, ensure_ascii=False, indent=4)def SplitTitle(title: str):    if "本文已被删除" in title:        return    if "[" not in title:        return    if "]" not in title:        return    a = title.index("[") + 1    b = title.index("]")    category = title[a:b].strip()    title = title[b + 1 :].strip()    return {"Category": category, "Title": title}def FindNextPage(bs):    links = bs.find_all("a", attrs={"class": "btn wide"})    for link in links:        if link.text == "‹ 上页":            return link.attrs["href"]if __name__ == "__main__":    Main()

结论

透过学习网路爬虫,可以了解基本的 HTML 架构与观察方法,结合简单的文字处理,可以让爬虫成为一个强大的资料工具。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章