记录一次学习爬虫的过程,从0到爬取今日头条,微信公众号。文章将详细的记录遇到的写爬虫过程中遇到的所有问题,以及尝试的各种解决问题的方案。
开发需求
某日收到运营同学提出的开发需求邮件,大概内容就是,获取部分友商网站的,今日头条头条号的,微信公众号的文章(统称第三方文章),包括文章标题,描述,文章连接,作者,发布时间。第三方内容更新时,需要及时获取。
这是让我写爬虫呀,爬虫是个好东西,但是我不会呀!学呗!
基础知识准备
在公司一直用python开发,那爬虫也用python写呗,python写爬虫,早有耳闻requests和BeautifulSoup包的大名,数据库就用mysql。这样获取数据,解析数据,存储数据的都有了。
那么
先看一波requests, BeautifulSoup的文档,-.-!!!
requests 文档
BeautifulSoup 文档
送分题
发现友商的网站都是这样古老的风格,据说都是某框架一键搭建的,基本没有反爬措施。这不就是传说中的送分题,开撸!
image.png
先在chrome中观察一下页面,很容易发现,页面都是这个get请求返回的,并且数据的结构很简单,整体是一个table,每一行是tbody id 为normalthread_*
image.png
image.png
那开始撸代码喽
html = requests.get(url).text
# 最好先判断一下请求返回值是否为200
soup = BeautifulSoup(html, 'html.parser', from_encoding='utf-8')
contents = soup.find_all('tbody', attrs={"id": re.compile(r"normalthread_*")})
for content in contents:
try:
title_link = content.find('a', class_='xst')
title = title_link.string
post_url = title_link.get('href')
author = content.find('td', class_='by').find('a').string
post_time = content.find('td', class_='by').find('em').find('span').string
......
except Exception as e:
logger.info(e)
其他友商的网站也都是类似的简单结构,就不一一赘述了,打完收工!
今日头条头条号
失败的尝试
用爬取友商网站的方式爬取今日头条,结果很僵硬,服务器只会返回{"message": "error", "has_more": false, "data": []},显然,今日头条是有反爬的,这可难倒了我这个新手,抓包分析今日头条的请求。
发现我们要的数据都是在这个https://www.toutiao.com/c/user/article/?page_type=1&user_id=6609164111&max_behot_time=0&count=20&as=A1158AB24157B44&cp=5A21379BC444AE1&_signature=WJGs4hASArGHJ2lKzejNLViRrP的请求中以json格式返回,这个请求的前面的参数都能明白是什么意思,后面三个 as,cp,_signature 参数估计就是一种校验。多请求几次不难发现这三个参数每次都不一样,显然和时间有关。
思路:这三个参数肯定是某段js代码生成,我们只需要找到这段代码,翻译成python,每次请求带上这三个参数,就可以获得我们想要的数据的json格式,在解析json就简单了。撸
发现页面中引入了这个js文件,找到这个代码的文件格式化。搜索_signature关键字
image.png
image.png
幸运,我们要的三个参数都在这里,as,cp是ascp.getHoney()方法的返回值。_signature,是TAC.sign(userInfo.id + "" + c.params.max_behot_time).
userInfo.id ,c.params.max_behot_time 大概都是我们url中的参数。
继续查找getHoney方法,发现还是在个文件中有这个方法
i.getHoney = function() {
var t = Math.floor((new Date).getTime() / 1e3)
, i = t.toString(16).toUpperCase()
, e = md5(t).toString().toUpperCase();
if (8 != i.length)
return {
as: "479BB4B7254C150",
cp: "7E0AC8874BB0985"
};
for (var s = e.slice(0, 5), o = e.slice(-5), n = "", a = 0; 5 > a; a++)
n += s[a] + i[a];
for (var l = "", r = 0; 5 > r; r++)
l += i[r + 3] + o[r];
return {
as: "A1" + n + i.slice(-3),
cp: i.slice(0, 3) + l + "E1"
}
}
翻译成python 代码
def getASCP(time=None):
time = int(math.floor(time.time()))
e = hex(time).upper()[2:]
md5 = hashlib.md5()
md5.update(str(time).encode(encoding='utf-8'))
i = md5.hexdigest().upper()
if len(e) != 8:
AS = '479BB4B7254C150'
CP = '7E0AC8874BB0985'
return AS, CP
n = i[0:5]
a = i[-5:]
s = ''
r = ''
for o in range(5):
s += n[o] + e[o]
r += e[o + 3] + a[o]
AS = 'A1' + s + e[-3:]
CP = e[0:3] + r + 'E1'
return AS, CP
那就还差_signature参数,也就是要找到TAC.sign方法。继续查找,依旧在这个文件中发现一段代码
Function(function(t) {
return '�e(e,a,r){�(b[e]||(b[e]=t("x,y","�x "+e+" y"�)(r,a)}�a(e,a,r){�(k[r]||(k[r]=t("x,y","�new x[y]("+Array(r+1).join(",x[�y]")�(1)+")"�)(e,a)}�r(e,a,r){�n,t,s={},b=s.d=r?r.d+1:0;for(s["$"+b]=s,t=0;t<b;t�)s[n="$"+t]=r[n];for(t=0,b=s�=a�;t<b;t�)s[t]=a[t];�c(e,0,s)}�c(t,b,k){�u(e){v[x�]=e}�f�{�g=�,t�ing(b�g)}�l�{try{y=c(t,b,k)}catch(e){h=e,y=l}}for(�h,y,d,g,v=[],x=0;;)switch(g=�){case 1:u(!�)�4:�f��5:u(�(e){�a=0,r=e�;���{�c=a<r;�c&&u(e[a�]),c}}(���6:y=�,u(�(y��8:if(g=�,l��g,g=�,y===c)b+=g;else if(y!==l)�y�9:�c�10:u(s(���11:y=�,u(�+y)�12:for(y=f�,d=[],g=0;g<y�;g�)d[g]=y.charCodeAt(g)^g+y�;u(String.fromCharCode.apply(null,d��13:y=�,h=delete �[y]�14:���59:u((g=�)?(y=x,v.slice(x-=g,y�:[])�61:u(�[�])�62:g=�,k[0]=65599*k[0]+k[1].charCodeAt(g)>>>0�65:h=�,y=�,�[y]=h�66:u(e(t[b�],�,���67:y=�,d=�,u((g=�).x===c?r(g.y,y,k):g.apply(d,y��68:u(e((g=t[b�])<"<"?(b--,f�):g+g,�,���70:u(!1)�71:�n�72:�+f��73:u(parseInt(f�,36��75:if(�){b��case 74:g=�<<16>>16�g�76:u(k[�])�77:y=�,u(�[y])�78:g=�,u(a(v,x-=g+1,g��79:g=�,u(k["$"+g])�81:h=�,�[f�]=h�82:u(�[f�])�83:h=�,k[�]=h�84:�!0�85:�void 0�86:u(v[x-1])�88:h=�,y=�,�h,�y�89:u(��{�e�{�r(e.y,arguments,k)}�e.y=f�,e.x=c,e}�)�90:�null�91:�h�93:h=��0:��;default:u((g<<16>>16)-16)}}�n=this,t=n.Function,s=Object.keys||�(e){�a={},r=0;for(�c in e)a[r�]=c;�a�=r,a},b={},k={};�r'.replace(/[�-�]/g, function(i) {
return t[15 & i.charCodeAt(0)]
})
}("v[x++]=�v[--x]�t.charCodeAt(b++)-32�function �return �))�++�.substr�var �.length�()�,b+=�;break;case �;break}".split("�")))()('gr$Daten Иb/s!l y͒yĹg,(lfi~ah`{mv,-n|jqewVxp{rvmmx,&eff�kx[!cs"l".Pq%widthl"@q&heightl"vr*getContextx$"2d[!cs#l#,*;?|u.|uc{uq$fontl#vr(fillTextx$$龘ฑภ경2<[#c}l#2q*shadowBlurl#1q-shadowOffsetXl#$$limeq+shadowColorl#vr#arcx88802[%c}l#vr&strokex[ c}l"v,)}eOmyoZB]mx[ cs!0s$l$Pb<k7l l!r&lengthb%^l$1+s$j�l s#i$1ek1s$gr#tack4)zgr#tac$! +0o![#cj?o ]!l$b%s"o ]!l"l$b*b^0d#>>>s!0s%yA0s"l"l!r&lengthb<k+l"^l"1+s"j�l s&l&z0l!$ +["cs\'(0l#i\'1ps9wxb&s() &{s)/s(gr&Stringr,fromCharCodes)0s*yWl ._b&s o!])l l Jb<k$.aj;l .Tb<k$.gj/l .^b<k&i"-4j!�+& s+yPo!]+s!l!l Hd>&l!l Bd>&+l!l <d>&+l!l 6d>&+l!l &+ s,y=o!o!]/q"13o!l q"10o!],l 2d>& s.{s-yMo!o!]0q"13o!]*Ld<l 4d#>>>b|s!o!l q"10o!],l!& s/yIo!o!].q"13o!],o!]*Jd<l 6d#>>>b|&o!]+l &+ s0l-l!&l-l!i\'1z141z4b/@d<l"b|&+l-l(l!b^&+l-l&zl\'g,)gk}ejo{�cm,)|yn~Lij~em["cl$b%@d<l&zl\'l $ +["cl$b%b|&+l-l%8d<@b|l!b^&+ q$sign ', [TAC = {}]);
脸一黑,这是什么鬼,混淆过?尚且不说看不懂,就算看懂了翻译成python也有难度,经过几次挣扎,彷徨,做了个艰难的决定,放弃这个方案,毕竟世上无难事,只要肯放弃。
另辟蹊径
之前的想法是行不通了,只有另辟蹊径呗,继续查找一下解决方案。遇见
selenium
( 那天下午阳光很好,你穿了件连衣裙 ^.^ )
了解selenium 文档(非官方)
selenium能获得ajax后的html文件,这样我们的问题就解决了,不用管访问目标网站时到底执行了,只需要把最后生成目标网页抓出来。解析即可。
开始撸代码
driver = webdriver.Chrome()
# driver = webdriver.PhantomJS(executable_path="/Users/wxf/Downloads/phantomjs-2.1.1-macosx/bin/phantomjs")
for url in urls:
logger.info("start jrtt url {}".format(url))
# 使用浏览器请求页面
driver.get(url)
# 等待ajax加载,这里一定要等下,刚开始就没加载出来,还误以为方案不可行
driver.refresh() # 刷新一下,会打开浏览器,浏览器有一个被测试工具控制的提示,堵塞页面加载。
WebDriverWait(driver, timeout=10).until(
lambda x: x.find_element_by_class_name('left'))
#selenium也能解析html标签,前面已经学习了BeautifulSoup,这里就还是用BeautifulSoup
soup = BeautifulSoup(driver.page_source, 'html.parser', from_encoding='utf-8')
contents = soup.find('div', class_='left').find_all('div', class_='rbox-inner')
for content in contents:
try:
# print(content)
title_link = content.find('a', class_='link title')
title = title_link.string
post_url = JRTT_URL + title_link.get('href')
......
except Exception as e:
logger.info(e)
这样头条号内容的抓取都不在话下了。
微信公众号文章
打开运营同学给的微信链接,发现已过期,微信的链接有时效性,再让给个新的,打开,发现提示只能在微信客户端打开。问,你们平时怎么在pc端浏览器里打开微信公众号的链接,得到结论
http://weixin.sogou.com/
思路,请求http://weixin.sogou.com/
我们想要爬取的公众号,然后模拟浏览器点击事件。就能获得这个公众号最近10篇文章。
driver = webdriver.Chrome()
# 使用浏览器请求页面
for wx_gzh_name in wx_gzh_names:
url = "http://weixin.sogou.com/weixin?type=1&s_from=input&query={}&ie=utf8&_sug_=n&_sug_type_=".format(
wx_gzh_name)
driver.get(url)
driver.find_element_by_class_name('img-box').click()
all_handle = driver.window_handles
#点击公众号列表某行,会新开一个公众号详情tab页,我们切换到新开的tab页
driver.switch_to.window(all_handle[1])
try:
WebDriverWait(driver, timeout=10).until(
lambda x: x.find_element_by_class_name('weui_media_title'))
soup = BeautifulSoup(driver.page_source, 'html.parser', from_encoding='utf-8')
wx_profile_nickname = soup.find('strong', class_='profile_nickname').text
contents = soup.find_all('div', class_='weui_media_bd')
for content in contents:
title_link = content.find('h4', class_='weui_media_title')
title = title_link.text
except Exception as e:
print(e)
finally:
driver.close()
time.sleep(random.randint(5, 10)) # 防止太请求太频繁
driver.switch_to.window(all_handle[0])
微信公众号的文章就到手了
剩下的问题:IP被禁,部署
正在沾沾自喜时,发现微信公众号爬取不了了,让其他同事访问搜狗微信搜索,发现每次搜索都要填一个简单的验证码。公司公用同一个公网ip,那说明我们的ip被封了呗。
解决方案思路:
1.识别验证码
2.ip池
程序写完了,一直在本地跑,是不行的,怎么部署到服务器,代码执行依赖浏览器,服务器上是没有浏览器的。
IP被禁,部署还有其他问题将在下篇在记载!
网友评论