前言
良葛格过世的消息对我来说十分冲击,笔者从国中开始学 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.要观察目标的资讯,可以简单的透过浏览器对该目标(例如连结、按钮)按右键,选择检查:
环境
首先我们需要安装 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 板 开始举办乐透!'}
造访下一页
只有一页讨论列表的标题,资料量显然是不够的。为了获得更多的资料,我们需要造访讨论列表的「上一页」
透过检查网页元件的方法,我们得知「上一页」是个 class
为 btn wide
的 a
元件
但很不幸的,这里至少有四个按钮都是 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 架构与观察方法,结合简单的文字处理,可以让爬虫成为一个强大的资料工具。