和大家之前说过,相关代码我会放出来,现在代码已经上传了去Github搜索 czw90130/AO3_DataAnalyze,我也和大家讲解一下实现方法,希望能激发大家学习编程兴趣。
这里先讲解爬虫是怎么实现的,至于文本分析器。下周有空再写吧,实在是肝不动了!
先说一下我的配置环境:
- 操作系统:Ubuntu 18.04
- Python环境:conda 4.8.2 Python 3.7.6
- 编辑器使用的是 Jupyter Notebook 代码也是ipynb格式。想学python或者数据分析的可以用这个,适合组织文章。
使用到的 Python 库有:
- sys os time 不解释了
- re 正则表达式解析
- tqdm 进度条
- selenuim 自动化测试/爬虫
- BeautifulSoup html 标签解析器
文章分析器使用的库也在这里说一下:
- jieba 结巴中文分词,就是吧文章拆分成一个个词语
- wordcloud 词云生成器
- matplotlib 图表绘制库
- numpy python数学运算库(这个其实就是做了个计数器~)
都是非常常用的库,对 Python 和数据分析有兴趣的朋友可以照着这个表看看。(操作系统不熟Windows也可以) 对AO3的爬取并不复杂,但还是有一些难度的。经过我一系列的测试发现,Request并不能有效的爬取AO3的信息,因此就选择了selenium。对于Selenium的介绍,我就过多不多描述了,有很多技术文介绍。大家可以自行搜索。我这里给大家一些关键词,方便大家搜索。
- selenium配置chromedriver
- selenium元素定位方法
首先加载要用到的库
import sys
import re
import os
import time
from tqdm import tqdm
from selenium import webdriver
from bs4 import BeautifulSoup
import random
爬取AO3的中文文章其实并不复杂:
1、我们进入AO3首页,点击 “Search” 然后点击 “Edit Your Search” 进入高级搜索模式
2、在Language里选择中文,点击“Search”
3、把滚动条拉倒最下面,点第二页
4、把浏览起里面的地址复制下来
我们仔细查看这个url请求,发现这个请求的参数还是非常清晰的,让我们来看看works/后面的参数:
search?commit=Search&page=后面跟着一个数字2,我们点击其第三页,这个数字也变成了3。所以可以断定这个参数指的是页码&work_search%5Blanguage_id%5D= 后面跟着zh字样,可以判这个参数是控制语言的。
同理&work_search%5Brating_ids%5D=控制的是分级
其他字段也是类似的,大家有兴趣可以自己试验,我就不再叙述了,值得一提的是,我在爬取时并没有用到分级标签功能,只是在搜索里面翻文章。通过多次搜索我发现AO3的搜索结果有一定的随机性,并没有主动干预搜索结果,这一点还是很良心的。
其他字段也是类似的,大家有兴趣可以自己试验,我就不再叙述了,值得一提的是,我在爬取时并没有用到分级标签功能,只是在搜索里面翻文章。通过多次搜索我发现AO3的搜索结果有一定的随机性,并没有主动干预搜索结果,这一点还是很良心的。
#获取搜索页面
def make_search_url(page=1, langu="zh", rating_key=""):
rating = {
"": "",
"Not_Rated": 9,
"General_Audiences": 10, #一般观众
"Teen_And_Up_Audiences": 11, #青少年及以上观众
"Mature": 12, #成熟
"Explicit": 13, #明确的
}
base_loc = '; #网站地址大家自己查这里我用 xxx 替代了
base_loc += "search?commit=Search&page="+str(page)+"&utf8=%E2%9C%93" #搜索页
base_loc += "&work_search%5Bbookmarks_count%5D="
base_loc += "&work_search%5Bcharacter_names%5D="
base_loc += "&work_search%5Bcomments_count%5D="
base_loc += "&work_search%5Bcomplete%5D="
base_loc += "&work_search%5Bcreators%5D="
base_loc += "&work_search%5Bcrossover%5D="
base_loc += "&work_search%5Bfandom_names%5D="
base_loc += "&work_search%5Bfreeform_names%5D="
base_loc += "&work_search%5Bhits%5D="
base_loc += "&work_search%5Bkudos_count%5D="
base_loc += "&work_search%5Blanguage_id%5D=" + langu #语言
base_loc += "&work_search%5Bquery%5D="
base_loc += "&work_search%5Brating_ids%5D=" + rating[rating_key] #分级
base_loc += "&work_search%5Brelationship_names%5D="
base_loc += "&work_search%5Brevised_at%5D="
base_loc += "&work_search%5Bsingle_chapter%5D=0"
base_loc += "&work_search%5Bsort_column%5D=_score"
base_loc += "&work_search%5Bsort_direction%5D=desc"
base_loc += "&work_search%5Btitle%5D="
base_loc += "&work_search%5Bword_count%5D="
return base_loc
下面我们看看搜索页面的html,在Chrome中可以按F12打开开发者工具。 Ctrl+Shift+C 使用元素选择工具点击一下文章标题,查看后发现所有的搜索结果都在 <ol class=work index group>标签下,并且在li标签的id中记录了文章的id。
我们点击进入一篇文章,查看文章的url发现文章url与上面的id是一一对应的。这样,我们就可以通过分析搜索页得到文章的地址。
这样,通过BeautifulSoup抓取相应标签获得li标签的id,就可以得到该搜索页下面所有的文章地址了。
#获取文章链接
def get_work_id_from_search(html):
old_list = []
soup = BeautifulSoup(html, ';)
ol = ('ol', attrs={'class': 'work index group'})
work_blurb_groups = ol.findAll('li', attrs={'class': 'work blurb group'})
for wbg in work_blurb_groups:
if wbg["id"] not in old_list:
old_li(wbg["id"])
return old_list
做好这些准备工作下面我们就开始正式爬取数据
save_path = "fulltext/" #存储文章的文件夹
#5000页中文内容,这里可以先取较小的数字做测试
start_p = 1
end_p = 5000
pbar = tqdm(range(start_p, end_p))
具体思路是这样的:
- 打开一个浏览器,需要注意的我这里使用了代理,否则无法浏览到AO3;
- 通过selenium find_element_by_id 功能找到相应按钮自动点击,同意网站条款;
- 进入循环,通过make_search_url函数组合出搜索页的链接,遍历页码;
- 将搜索页的html传入函数get_work_id_from_search提取出所有文章id;
- 遍历文章id通过文章id组合出文章地址并访问,最后保存文章页面的html。
这其中有两个注意事项:
- 当进入限制级文章时,网站会提示再次同意浏览条款,当检测到条款关键字时,使用find_element_by_link_text('Proceed').click()点击确认即可;
- 频繁访问后,网站会拒绝访问请求出现‘Retry later’页面,当检测到这种情况后,进行异常处理,关闭当前的浏览器,等待一分钟后重新访问。(这也是爬取文章速度比较慢的原因,有大神知道怎么解决的请赐教)
c_service = webdriver.c('/usr/bin/chromedriver')
c_()
c_()
chrome_options = webdriver.ChromeOptions()
c('--proxy-server=socks5://localhost:1080')
browser = webdriver.Chrome(chrome_options=chrome_options) # 调用Chrome浏览器
brow(";) # 网址用xxx替代
(3)
brow('tos_agree').click()
(1)
brow('accept_tos').click()
(1)
for page in pbar:
search_url = make_search_url(page) #生成搜寻页面链接
brow(search_url) # 请求页面,打开一个浏览器
html_text = brow # 获得页面代码
try:
work_list = get_work_id_from_search(html_text) #获得文章的id
for work in work_list:
work_path = os.(save_path, work+".html")
if os.(work_path):
continue
work_url = "; + work.split("_")[1] #创建文章URL 网址用xxx替代
brow(work_url)
html_text = brow #获得页面代码
if "If you accept cookies from our site and you choose \"Proceed\"" in html_text: #无法获取正文则点击Proceed
brow('Proceed').click()
(1)
brow(work_url)
html_text = brow
if "Retry later" in html_text:
raise AttributeError
if "<!--chapter content-->" in html_text:
("saving: " + work)
fh = open(work_path, 'w') #保存页面
(html_text) #写入内容
() #关闭
(floa(10,50))/10) #随机延时
except AttributeError as e:
print(e)
(3)
brow(";)
(3)
brow()
c_()
(60)
c_()
browser = webdriver.Chrome(chrome_options=chrome_options) #调用Chrome浏览器
brow(";) # 网址用xxx替代
(5)
brow('tos_agree').click()
(2)
brow('accept_tos').click()
(3)
(floa(10,50))/10) #随机延时
以上就是AO3爬虫的所有代码,爬虫会将文章都保存在 fulltext 文件夹下等待分析,这里的内容都在 AO3_ 中,都是开源的,大家可以自行 clone 拿去修改玩耍。
再次说明一下,因为是利用周末时间写的东西,语法格式也是比较飘逸,我自己也并不是爬虫和数据分析方面的专家。所以很多地方还是有些笨拙的。希望大神们也能帮忙指点。