1 urlpare模块
urlparse模块主要用于处理URL字符串,它的核心功能是将URL拆分为多个组成部分,并允许你通过名字属性或索引来访问这些部分。通过调用urlparse模块的相关函数,你可以轻松解析URL,获取其不同组件的信息,如协议(scheme)、网络位置(netloc)、路径(path)、参数(parameters)、查询字符串(query)和片段(fragment)。
1.1 链接组成
新建urlpaser_test.py
#!/usr/bin/env python from urllib.parse import urlparse,urlunparse,urlsplit,urlunsplit,urljoin,parse_qs,parse_qsl,quote,unquote url = 'http://www.baidu.com/index.html;user?id=0#comment' # result = urlparse(url=url) # # print(type(result),result) # print(result.scheme) # print(result.netloc) # print(result.path) # print(result.params) # print(result.query) # print(result.fragment) # result = urlparse(url=url,scheme='https',allow_fragments=False) # print(result.scheme) # print(result.fragment) # data = ['http','www.baidu.com','index.html','user','id=0','comment'] # url = urlunparse(data) # print(url)
效果
1.2 链接解析
当然可以,以下是对您提供的代码添加中文注释的版本: python #!/usr/bin/env python # 导入urlparse模块中的多个函数 from urllib.parse import urlparse, urlunparse, urlsplit, urlunsplit, urljoin, parse_qs, parse_qsl, quote, unquote # 假设有一个URL字符串 # url = 'http://www.baidu.com/index.html;user?id=0#comment' # 使用urlparse函数解析URL # result = urlparse(url=url) # # 打印解析结果及其类型 # # print(type(result), result) # # 打印URL的各个组成部分 # print(result.scheme) # 协议部分 # print(result.netloc) # 网络位置部分 # print(result.path) # 路径部分 # print(result.params) # 参数部分 # print(result.query) # 查询字符串部分 # print(result.fragment) # 片段(锚点)部分 # 使用urlparse函数解析URL,并指定协议和不允许片段 # result = urlparse(url=url, scheme='https', allow_fragments=False) # 打印指定协议后的结果和片段部分(如果allow_fragments为False,则fragment为None) # print(result.scheme) # print(result.fragment) # 使用urlunparse函数将URL的各个组成部分组合成一个完整的URL # data = ['http', 'www.baidu.com', 'index.html', 'user', 'id=0', 'comment'] # url = urlunparse(data) # print(url) # 使用urlsplit函数解析URL,它与urlparse类似,但不解析参数部分 # url = 'http://www.baidu.com/index.html;user?id=0#comment' # result = urlsplit(url) # 打印urlsplit的解析结果 # print(result) # 打印URL的各个组成部分 # print(result.scheme) # print(result[0]) # 与result.scheme相同 # print(result.netloc) # print(result.path) # print(result.query) # print(result.fragment) # 使用urlunsplit函数将URL的各个组成部分(不包括参数部分)组合成一个完整的URL # data = ('http', 'www.baidu.com', 'index.html', 'id=0', 'comment') # print(urlunsplit(data)) # 使用urljoin函数合并基础URL和相对URL # base_url = 'https://www.baidu.com' # relative_url = '/path/to/xxxx' # url = urljoin(base_url, relative_url) # print(url) # 测试urljoin函数在不同情况下的表现 # print(urljoin('https://www.baidu.com', '/FAQ.html')) # 当相对URL是绝对URL时,它会覆盖基础URL的协议和位置 # print(urljoin('https://www.baidu.com', 'http://www.mashibing/FAQ.html')) # 当相对URL包含协议时,它会覆盖基础URL的协议 # print(urljoin('https://www.baidu.com/admin.html', 'http://www.mashibing/FAQ.html')) # 当相对URL是查询字符串或片段时,它会被添加到基础URL的末尾 # print(urljoin('https://www.baidu.com?wd=aaa', '?user=1#comment')) # print(urljoin('https://www.baidu.com#comment', '?user=1')) # 使用parse_qs函数解析查询字符串,返回一个字典,字典的键是参数名,值是参数值的列表 query = 'name=handsomewuyue&age=18' print(parse_qs(query)) # 使用parse_qsl函数解析查询字符串,返回一个元组的列表,每个元组包含一个参数名和对应的参数值 # data = parse_qs(query) print(parse_qsl(query)) # 使用quote函数对字符串进行URL编码 keyword = "美女" url = 'https://www.baidu.com/s?wd=' + quote(keyword) print(url) # 使用unquote函数对URL编码的字符串进行解码 url_1 = unquote(url) print(url_1)
2 robot协议
robots协议,也称为爬虫协议、爬虫规则或机器人协议,其全称是“网络爬虫排除标准”(Robots Exclusion Protocol)。这是网站国际互联网界通行的道德规范,旨在保护网站数据和敏感信息,确保用户个人信息和隐私不被侵犯。通过robots协议,网站可以告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。搜索引擎则会通过读取robots.txt文件来识别页面是否允许被抓取。
2.1 robotparser模块
robotparser
是Python urllib
库中的一个模块,主要用于识别网站的robots.txt
文件。robots.txt
文件是网站管理员为搜索引擎爬虫(如Googlebot)或其他网络爬虫提供的一个访问控制文件,它指明了哪些爬虫可以访问网站的哪些部分,哪些不能访问。
robotparser
模块提供了RobotFileParser
类,这个类能够读取、解析URL上的robots.txt
文件,并根据文件中的规则来判断特定的爬虫(通过其User-Agent
标识)是否允许访问某个URL。
以下是robotparser
模块的一些主要方法和功能:
-
set_url(url)
: 设置指向robots.txt
的文件URL。 -
read()
: 读取robots.txt
的URL并将其输入到解析器中。 -
parse(lines)
: 解析robots.txt
文件,传入的参数是robots.txt
文件中的某些行内容。 -
can_fetch(user-agent, url)
: 判断特定的爬虫(通过其User-Agent
)是否可以访问某个URL。
2.2 案例
新建robot_test.py
#!/usr/bin/env python from urllib.robotparser import RobotFileParser #创建一个RobotFileParser对象用于解析robots.txt robot_parser = RobotFileParser() robot_parser.set_url('https://www.zhihu.com/robots.txt') #读取并且解析robots.txt robot_parser.read() #检查是否可以爬取特定的url user_agent = "BaiduSpider" check_url = 'https://www.zhihu.com/' #can_ferch if robot_parser.can_fetch(user_agent,check_url): print('可以爬取到这个url') else: print('不可以爬取到这个url')
效果
3 request复习
3.1 案例1
新建requests1.py
#!/usr/bin/env python import requests # 导入requests库,用于发起HTTP请求 # 发送GET请求示例 # r = requests.get('http://httpbin.org/get') # 发送GET请求到http://httpbin.org/get # print(r.text) # 打印请求返回的文本内容 # 发送带参数的GET请求示例 # data = { # 'name':'handsomewuyue', # 构造请求参数,name字段 # 'age':'18' # 构造请求参数,age字段 # } # r = requests.get('http://httpbin.org/get', params=data) # 发送带参数的GET请求 # print(r.text) # 打印请求返回的文本内容 # 发送POST请求示例 # data = { # 'name':'handsomewuyue', # 构造POST请求的数据,name字段 # 'age':'18' # 构造POST请求的数据,age字段 # } # r = requests.post('http://httpbin.org/post', data=data) # 发送POST请求 # print(r.text) # 打印请求返回的文本内容 # 其他HTTP方法示例(未实际执行) # r = requests.put('http://httpbin.org/put') # 发送PUT请求,需要相应URL支持 # r = requests.delete('http://httpbin.org/delete') # 发送DELETE请求,需要相应URL支持 # r = requests.head('http://httpbin.org/head') # 发送HEAD请求,只获取响应头 # r = requests.options('http://httpbin.org/options') # 发送OPTIONS请求,获取服务器支持的请求方法 # 自定义请求头发送GET请求示例 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' # 自定义User-Agent头 } r = requests.get('https://www.zhihu.com', headers=headers) # 发送带自定义headers的GET请求到知乎首页 print(r.text) # 打印请求返回的文本内容
效果
3.2 案例2
新建requests2.py
python import requests # 导入requests库,用于发起HTTP请求 # 定义目标URL,这里使用httpbin.org提供的POST请求测试接口 url = 'http://httpbin.org/post' # 创建一个字典,用于存放要上传的文件 # 'file'是上传文件时使用的字段名,open('favicon.ico','rb')打开名为'favicon.ico'的文件,并以二进制模式读取 files = {'file': open('favicon.ico', 'rb')} # 使用requests库的post方法发起POST请求,并上传文件 # files参数用于指定上传的文件 r = requests.post(url, files=files) # 打印请求返回的文本内容 print(r.text)
效果
3.3 案例3
新建requests3.py
import requests url = 'https://www.zhihu.com' # r = requests.get(url=url) # print(r.cookies) # for key,value in r.cookies.items(): # print(key + '=' + value) headers = { 'Cookie':'_zap=65985310-a068-4c12-ae0a-56251c2c5c12; d_c0=AbCTugmbSBePTrEOSOMV93HuhQPipyoMkyM=|1692789471; YD00517437729195%3AWM_TID=emVDLGILJsBERAVQVAeAkpebh4G4Aa0t; __snaker__id=zptBm6fqrykPGJWm; YD00517437729195%3AWM_NI=SQ6wtpN50b7JklREIGLJWfA7KXvU0ElY78UhdWVg5RLCcXFSmIutvYWSTxuU4CpLkUPWI2Y4mwYdcmBA086%2BVej4kBzqutG6WzywEoor7JT9in6Xt7l1sOIXt%2ByjYphdTVg%3D; YD00517437729195%3AWM_NIKE=9ca17ae2e6ffcda170e2e6ee83aa66a1ecfd96eb4b8ea88aa7c44e838b8a87c56297abf990f15cf68e9887d02af0fea7c3b92a918ffa98f77c8eeb0083b642a9b49ea6cc4d90969987c43d8988bab6b34fb087f9d9d864a8899a88b85a909dadb9c559af97bb95f553958d9c99f85ff89dc0adea5f948987d3dc40afa99785e267829f00d9b52196bba6d5ea4abaafbb95c63e95a6b798c865adaef9abd733abb2fab9f73db0efbdb0cc54b8f5b78bdc7cacbc9cb7e637e2a3; _xsrf=jWERx0Ik6tjUozZKGWIpPBRGFOJ6QYhq; l_cap_id="NWI2N2QwMDM0MjUzNDU4OWExMmE3NmI0Y2I5ZTc3MmQ=|1693290667|022d1fb6013e570378e4fa421ac1e25e984e3165"; r_cap_id="YjY0YWI2YjcyM2QxNGI1Mjg5MTM5ZmRjMGEwYzEwYTg=|1693290667|1fcd27c525bc660ecf654c711ac76b67d452689c"; cap_id="YjU5YjE5MzFjN2ZlNDhlZDhlYWQ0NTE5OTRkZTRjZjU=|1693290667|a118f84fa5d20957be92227edacf0e66d30eeec0"; captcha_session_v2=2|1:0|10:1693292926|18:captcha_session_v2|88:TWFFU1NCVHlldDVJaTR5cTlwM2h4SmhGeTJTZ010WFV5Rm8xVzE3WWpOUjFyL0xkcVI3TjZGeHNxY3FGVmxCVQ==|2ce870f0a811b7f7d08e168a66921c06fe61a12bda683b178a4b602e4752f7a0; gdxidpyhxdE=QGggrpC%2BxZPIYJYBt8R4ujh6KD%5CTpC9W20pZ6YAcDsSM81tdUWJuNon2TBMMS3NC%2BjjbAMgsmIoioR%2BnaJbPnN3TJma2%2B%5C%5CVBm7gS4%5CsviBdQK6%2BolIZbAtHwcEfBz0VRYv6eQ%2FjmU4SrTrwXqesKfBaqvIRyDvr69M1gSgHinN%2FUV1s%3A1693293827600; q_c1=c4dd7565996f498d8689e31708ec9ef1|1693292955000|1693292955000; z_c0=2|1:0|10:1693292957|4:z_c0|92:Mi4xVWdrZUJnQUFBQUFCc0pPNkNadElGeVlBQUFCZ0FsVk5tdWZhWlFEa0pEVnZ5MGRVbmNuQzhDM3ljT0VyVnFUUUtB|924bb27a52a11d16167abbc8c01824e09e82e2df7f3724ceea2b675a569137ad; Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49=1693288624,1693290816,1693292927,1693315711; SESSIONID=7NnIui3CdpkQ2CVVCuillDLNphd1UQs3BxB7AVZiwUy; JOID=VlwQC0nBLspKFqRRHM0YnbfzUXEK9mGpCWXNY1KjXqAWLuEHS0DQICIQpV0bHMfWHsi4i_WxPEPuNtHxDESP0qY=; osd=UF0dC0LHL8dKHaJQEc0Tm7b-UXoM92ypAmPMblKoWKEbLuoBSk3QKyQRqF0QGsbbHsO-ivixN0XvO9H6CkWC0q0=; Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49=1693316313; tst=r; KLBRSID=f48cb29c5180c5b0d91ded2e70103232|1693316989|1693315625', 'Usrt-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' } r = requests.get(url=url,headers=headers) with open('1.html','wb') as f: f.write(r.text.encode('utf-8'))
3.4 request(证书忽略,超时。证书)
新建requests4.py
import requests # 导入requests库,用于发起HTTP请求 from requests.auth import HTTPBasicAuth # 从requests库中导入HTTPBasicAuth类,用于HTTP基础认证 # r = requests.get('https://ssr2.scrape.center/') # 发送GET请求到https://ssr2.scrape.center/, # print(r.status_code) # 打印请求返回的HTTP状态码, # r = requests.get('https://ssr2.scrape.center/', verify=False, timeout=0.1) # 发送GET请求到https://ssr2.scrape.center/, 不验证SSL证书,并设置请求超时时间为0.1秒 # print(r.status_code) # 打印请求返回的HTTP状态码,但这行也被注释掉了 # r = requests.get('https://ssr3.scrape.center/', verify=False, auth=HTTPBasicAuth('admin', 'admin')) # 发送GET请求到https://ssr3.scrape.center/,不验证SSL证书,并使用HTTP基础认证,用户名和密码分别是'admin' # print(r.text) # 打印请求返回的文本内容
效果
3.5 request高级特性
新建requestcookiejar.py
import requests.cookies Cookie='_zap=65985310-a068-4c12-ae0a-56251c2c5c12; d_c0=AbCTugmbSBePTrEOSOMV93HuhQPipyoMkyM=|1692789471; YD00517437729195%3AWM_TID=emVDLGILJsBERAVQVAeAkpebh4G4Aa0t; __snaker__id=zptBm6fqrykPGJWm; YD00517437729195%3AWM_NI=SQ6wtpN50b7JklREIGLJWfA7KXvU0ElY78UhdWVg5RLCcXFSmIutvYWSTxuU4CpLkUPWI2Y4mwYdcmBA086%2BVej4kBzqutG6WzywEoor7JT9in6Xt7l1sOIXt%2ByjYphdTVg%3D; YD00517437729195%3AWM_NIKE=9ca17ae2e6ffcda170e2e6ee83aa66a1ecfd96eb4b8ea88aa7c44e838b8a87c56297abf990f15cf68e9887d02af0fea7c3b92a918ffa98f77c8eeb0083b642a9b49ea6cc4d90969987c43d8988bab6b34fb087f9d9d864a8899a88b85a909dadb9c559af97bb95f553958d9c99f85ff89dc0adea5f948987d3dc40afa99785e267829f00d9b52196bba6d5ea4abaafbb95c63e95a6b798c865adaef9abd733abb2fab9f73db0efbdb0cc54b8f5b78bdc7cacbc9cb7e637e2a3; _xsrf=jWERx0Ik6tjUozZKGWIpPBRGFOJ6QYhq; l_cap_id="NWI2N2QwMDM0MjUzNDU4OWExMmE3NmI0Y2I5ZTc3MmQ=|1693290667|022d1fb6013e570378e4fa421ac1e25e984e3165"; r_cap_id="YjY0YWI2YjcyM2QxNGI1Mjg5MTM5ZmRjMGEwYzEwYTg=|1693290667|1fcd27c525bc660ecf654c711ac76b67d452689c"; cap_id="YjU5YjE5MzFjN2ZlNDhlZDhlYWQ0NTE5OTRkZTRjZjU=|1693290667|a118f84fa5d20957be92227edacf0e66d30eeec0"; captcha_session_v2=2|1:0|10:1693292926|18:captcha_session_v2|88:TWFFU1NCVHlldDVJaTR5cTlwM2h4SmhGeTJTZ010WFV5Rm8xVzE3WWpOUjFyL0xkcVI3TjZGeHNxY3FGVmxCVQ==|2ce870f0a811b7f7d08e168a66921c06fe61a12bda683b178a4b602e4752f7a0; gdxidpyhxdE=QGggrpC%2BxZPIYJYBt8R4ujh6KD%5CTpC9W20pZ6YAcDsSM81tdUWJuNon2TBMMS3NC%2BjjbAMgsmIoioR%2BnaJbPnN3TJma2%2B%5C%5CVBm7gS4%5CsviBdQK6%2BolIZbAtHwcEfBz0VRYv6eQ%2FjmU4SrTrwXqesKfBaqvIRyDvr69M1gSgHinN%2FUV1s%3A1693293827600; q_c1=c4dd7565996f498d8689e31708ec9ef1|1693292955000|1693292955000; z_c0=2|1:0|10:1693292957|4:z_c0|92:Mi4xVWdrZUJnQUFBQUFCc0pPNkNadElGeVlBQUFCZ0FsVk5tdWZhWlFEa0pEVnZ5MGRVbmNuQzhDM3ljT0VyVnFUUUtB|924bb27a52a11d16167abbc8c01824e09e82e2df7f3724ceea2b675a569137ad; Hm_lvt_98beee57fd2ef70ccdd5ca52b9740c49=1693288624,1693290816,1693292927,1693315711; SESSIONID=7NnIui3CdpkQ2CVVCuillDLNphd1UQs3BxB7AVZiwUy; JOID=VlwQC0nBLspKFqRRHM0YnbfzUXEK9mGpCWXNY1KjXqAWLuEHS0DQICIQpV0bHMfWHsi4i_WxPEPuNtHxDESP0qY=; osd=UF0dC0LHL8dKHaJQEc0Tm7b-UXoM92ypAmPMblKoWKEbLuoBSk3QKyQRqF0QGsbbHsO-ivixN0XvO9H6CkWC0q0=; Hm_lpvt_98beee57fd2ef70ccdd5ca52b9740c49=1693316313; tst=r; KLBRSID=f48cb29c5180c5b0d91ded2e70103232|1693316989|1693315625' #创建一个空的RequestsCookieJar来保存cookies jar = requests.cookies.RequestsCookieJar() headers = { 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' } #遍历分割cookies字符串,将每一个cookie的建值对添加到 cookiejar中 for cookie in Cookie.split(';'): key,value = cookie.split('=',1) # print(key) jar.set(key,value) #发送请求并且附带cookies和header r = requests.get('http://www.zhihu.com',cookies=jar,headers=headers) with open('2.html','wb') as f: f.write(r.text.encode('utf-8'))
3.6 reqiest_session
python import requests # 导入requests库,用于发起HTTP请求 # #1. 创建一个session对象 # session对象可以跨请求保持某些参数,比如cookies,请求头等 s = requests.Session() # #2. 使用session对象向网站发送GET请求并设置cookie # 发送GET请求到http://httpbin.org/cookies/set/number/123456,设置cookie的键为'number',值为'123456' # 注意:这里设置cookie的操作是由httpbin网站内部处理的,响应中不会直接显示设置的cookie s.get('http://httpbin.org/cookies/set/number/123456') # #3. 使用同一个session对象再次发送GET请求,获取当前会话里的cookie # 发送GET请求到http://httpbin.org/cookies,获取当前会话中的所有cookies # 由于使用了同一个session对象,它会自动携带之前设置的cookie进行请求 r = s.get('http://httpbin.org/cookies') # 打印响应的文本内容,其中包含当前会话的cookies信息 print(r.text)
效果
4 httpx
4.1 httpx模块介绍
httpx模块是Python 3的一个全功能HTTP客户端,它提供了同步和异步API,并支持HTTP/1.1和HTTP/2。该模块建立在requests库的完善可用性之上,不仅继承了requests的大部分功能,而且比后者更加强大和灵活
模块安装
pip install httpx 支持http2.0还需要按照2.0的支持 pip install "httpx[http2]"
新建httpx_test.py
#!/usr/bin/env python import requests import httpx url = 'https://spa16.scrape.center/' headers = { "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.76" } data={ "name":"handsome scj" } client = httpx.Client(http2=True) response = client.post('https://www.httpbin.org/post',headers=headers,data=data) print(response.text)
效果
5 抓取电影信息
5.1 项目梳理
第一步:遍历所有的页码,拼接URL 第二步:拿到详情页页面的url 第三步:再详情页面用正则匹配出我们要的内容 第四步:保存数据
5.2 logging模块:
用于记录日志,方便调试 日志级别: DEBUG: 用于详细的调试信息,通常用于开发和故障排查。 INFO: 提供一般信息,表明应用程序正在正常运行。 WARNING: 表示潜在的问题或异常情况,但不会中断应用程序。 ERROR: 指示错误发生,可能会导致应用程序部分失败。 CRITICAL: 指示严重错误,可能会导致应用程序完全失败。
5.2.1 logging案例
新建logging_demo.py
#!/usr/bin/env python import logging #配置日志记录器 #第一种方式 #level指定记录日志的级别 # logging.basicConfig(level=logging.DEBUG,filename="app.log", # format='%(asctime)s - %(levelname)s - %(message)s') # loger = logging.getLogger("myapp") # loger.debug("这是一条debug信息") # loger.info("这是一条info信息") # loger.warning("这是一条warning信息") # loger.error("这是一条error信息") # loger.critical("这是一条CRITICAL信息") #创建 loger = logging.getLogger("myapp") loger.setLevel(logging.INFO) #显示在终端上 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) #显示到文件里 file_handler = logging.FileHandler("myapp.log") file_handler.setLevel(logging.ERROR) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) loger.addHandler(file_handler) loger.addHandler(console_handler) loger.info("这是一条info信息") loger.error("这是一条error信息")
效果
5.3 项目实现1
#!/usr/bin/env python import logging import requests import re from urllib.parse import urljoin import pymongo logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') BASE_URL = 'https://ssr1.scrape.center' TOTAL_PAGE = 10 #抓取某一页面的内容 def scrape_index(page): index_url = f'{BASE_URL}/page/{page}' return scrape_page(index_url) #定义一个函数抓取网页的内容 def scrape_page(url): logging.info("正在抓取 %s.....",url) #发起get请求 try: response = requests.get(url) if response.status_code == 200: return response.text else: logging.error("抓取 %s 时返回无效的状态码 %s",url,response.status_code) except requests.RequestException : #如果发生异常,就报错 logging.error("抓取%s时发生异常",url,exc_info=True) #解析内容,并提取出详情页面的url def parse_index(html): #用正则把连接给提取出来 # print(type(html)) pattern = re.compile('<a.*href="(.*?)".*?class="name">') items = re.findall(pattern,html) # print(items) if not items: return [] for item in items: #把相对链接转为绝对链接 detail_url = urljoin(BASE_URL,item) # print(detail_url) logging.info('找到详情页面了,链接%s',detail_url) yield detail_url def main(): for page in range(1,TOTAL_PAGE+1): index_html = scrape_index(page) detail_urls = parse_index(index_html) # print(list(detail_urls)) logging.info('详细页面链接 %s', list(detail_urls)) if __name__ == '__main__': main()