首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
139 阅读
2
nvim番外之将配置的插件管理器更新为lazy
98 阅读
3
2018总结与2019规划
69 阅读
4
从零开始配置 vim(15)——状态栏配置
58 阅读
5
PDF标准详解(五)——图形状态
46 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
elisp
文本编辑器
Java
读书笔记
反汇编
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
332
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
332
篇与
的结果
2018-06-02
Facebook爬虫
初次接触到scrapy是公司要求编写一个能够解析JavaScript的爬虫爬取链接的时候听过过,当时我当时觉得它并不适合这个项目所以放弃这个方案,时隔一年多公司有了爬取Facebook用户信息的需求,这样才让我正式接触并使用到scrapy需求首先从文件或者数据库导入第一批用户做为顶层用户,并爬取顶层用户好友的发帖信息包括其中的图片将第一步中爬取到的用户好友作为第二层用户并爬取它们的发帖信息和好友信息将第二层用户中爬到的好友作为第三层用户并爬取它们的好友信息也就是说不断爬取用户的好友和它的发帖信息直到第三层为止根据这个需求首先来确定相关方案爬虫框架使用scrapy + splash:Facebook中大量采用异步加载,如果简单收发包必定很多内容是解析不到的,因此这里需要一个JavaScript渲染引擎,这个引擎可以使用selenium + chrome(handless) 这套,但是根据网上一位老哥的博客我知道了splash这种东西,在做相关比较之后我选择了使用splash,主要的理由有以下几点:a. 与selenium比较起来,它的官方文档更为全面b. 支持异步的方式,这个可以与scrapy的异步回调方式完美结合并充分发挥性能c. 它提供了一套与scrapy结合的封装库,可以像scrapy直接yield request对象即可,使用方式与scrapy类似降低了学习成本d. 它提供了lua脚本的方式可以很方便的操作浏览器对象e. 跨平台。相比于使用chrome作为渲染工具,它可以直接执行在Linux平台在scrapy中使用splash时可以安装对应的封装库scrapy_splash,这个库的安装配置以及使用网上基本都有详细的讲解内容,这里就不再提了当然它也有缺点,但是这并不在讨论之中,至于具体如何选择就是一个见仁见智的问题了开发语言: python3 ,python在开发爬虫方面有独特的优势,这个就不用我多说了,弄过爬虫的朋友都知道开发工具 pycharm, JB的pycharm几乎是Python IDE的首选设计与实现这里可能涉及到商业秘密,毕竟是签过保密协议的,所以在这部分我不会放出完整的代码,只会提供一个思路然后给出部分关键代码以供参考。这里我想根据我遇到的问题,以问题的方式来讲述这个项目,毕竟对于爬虫、框架这些东西大家都很熟再来讲这些也没有多大意思了用户登录在浏览器中操作的时候发现,如果是游客(也就是未登陆状态)的时候,当我们浏览相关用户的时间线时会得到下面这个界面在未登录的情况下查看用户信息的时候会弹出一个界面需要登录或者注册。因此从这里来看爬虫的第一个任务就应该是登录登录的时候scrapy提供了一个form_response的方法可以很方便的填写表单并提交,但是我发现用这种方式只能在返回的response对象中的request.headers里面找到cookie的字符串,而由于splash需要我们传入cookie的字典形式,这里我没有找到什么很好的办法,只能是采用splash 提供的方法。Facebook中登录页面为https://www.facebook/login。因此我重载爬虫的start_requests方法,提交一个针对这个登录页面url的请求。这个页面不涉及到渲染问题自然就使用Requests对象def start_requests(self): #开启爬取之前先登录 yield Request( url= self.login_url, # https://www.facebook.com/login callback= self.login, )当它请求的页面返回时触发login方法,在这个方法中我们提供了一个lua脚本自动填写用户名密码,然后提交请求,并最终返回成功的cookieyield SplashFormRequest.from_response( response, url = self.login_url, formdata={ "email":user, "pass": password }, endpoint="execute", args={ "wait": 30, "lua_source": lua_script, #这个参数是一个lua脚本的字符串 "user_name" : user, #user和password将会作为参数传入到lua脚本中 "user_passwd" : password, }, callback = self.after_login, errback = self.error_parse, )这里我们使用splash来发送请求包,这里我们主要向lua脚本中传入用户名和密码,下面是lua脚本的相关内容function main(splash, args) local ok, reason = splash:go(args.url) user_name = args.user_name user_passwd = args.user_passwd user_text = splash:select("#email") pass_text = splash:select("#pass") login_btn = splash:select("#loginbutton") if (user_text and pass_text and login_btn) then user_text:send_text(user_name) pass_text:send_text(user_passwd) login_btn:mouse_click({}) end splash:wait(math.random(5, 10)) return { url = splash:url(), cookies = splash:get_cookies(), headers = splash.args.headers, } end根据相关资料,SplashRequest 函数中的参数将会以lua table的形式被传入到splash形参中,而函数的args参数中的内容以 table的形式被传入到形参args中,所以这里要获取到用户名和密码只需要从args里面取即可上述lua代码首先请求对应的登录界面(我觉得这里应该不用请求,而直接使用response,但是这是我在写这篇文章的时候想到的还没有验证),然后通过css选择器找到填写用户名,密码的输入框和提交按钮。然后填写相关内容,最后点击按钮进行登录,然后等待一定时间,这里一定要等待以便Facebook服务器验证并跳转到对应的链接,最后我们是通过链接来判断是否登录成功。最后返回当前页面的url,cookie和对应的头信息在浏览器中执行登录操作的时候发现如果是新用户(没有填写相关信息用户)会跳转到www.facebook.com/?sk=welcome这个页面要求用户填入一定的信息,而老用户则会跳转到www.facebook.com 这个url,这个页面会显示用户关注的好友动态。因此在程序中我也根据跳转的新页面是否是这两个页面来进行判断是否登录成功的.登录成功后将脚本返回的cookie保存,脚本返回的信息在scrapy的response.data中作为字典的形式保存代理由于众所周知的原因,Facebook对于国内来说是一个404的站点,国内用户想要访问必须提供一个代理。在scrapy中代理可以设置在对应的下载中间件中,在下载中间件的process_request函数中设置request.meta["proxy"] = proxy但是这种方式针对splash时就不管用了,我找了很多资料发现可以在lua脚本中设置,每次在执行之前都需要相同的代码来设置代理,因此我们可以采用下面的模板function main(splash, args) splash:on_request(function(request) request:set_proxy{ host = '0.0.0.0', --代理服务器的IP port = 0, --代理服务器的端口 username = '', --登录的用户名和密码 password = '', type = "http", -- 代理的协议,根据官网的说法目前只支持http和ss5 ,目前就这个项目来说http就够了 } end) --do something end每次执行含有这段代码的脚本时首先执行on_request函数设置代理的相关信息,然后执行splash:go函数时就可以使用上面的配置访问对应站点了使爬虫保持登录状态根据splash的官方文档的说明,splash其实可以看做一个干净的浏览器,就好像我们在使用浏览器每次请求一个新页面的时候同时清理了里面的缓存一样,它不会保存之前的任何状态,所以这里的cookie只能每次在发包的同时给它设置上,好在splash给了相应的方法来设置和获取它,下面是关于cookie的模板local cookies = splash:get_cookies() -- ... do something ... splash:init_cookies(cookies) -- restore cookies至此我们的lua脚本的模板就变成了这样function main(splash, args) splash:init_cookies(splash.cookies) -- 这个cookie是通过SplashRequest函数的cookies参数传入的 splash:on_request(function(request) request:set_proxy{ host = '0.0.0.0', --代理服务器的IP port = 0, --代理服务器的端口 username = '', --登录的用户名和密码 password = '', type = "http", -- 代理的协议,根据官网的说法目前只支持http和ss5 ,目前就这个项目来说http就够了 } end) --do something return { cookie = splash:get_cookies() } end获取用户主页面我们在Facebook随便点击一个用户进入它的主页面,查看url如下可以看到针对用户名为英文的情况,它简单的将英文名作为二级目录,只不过将空格换成了点,而针对不为英文的用户,它以profile作为二级目录,并且后面带上一个参数id,这个ID就是用户的ID。其实根据后面我自己的实验不管上面的哪种用户都可以通过这个ID访问到,我们可以组成一个url:https://www.facebook.com/[id] 来访问用户首页,因此项目中要求提供的外部导入用户名必须是英文或者是ID,以便能直接通过url拼接的方式来获取用户首页除了这个区别之外,还有一种称之为公共主页的页面,比如下面是特朗普的公共主页对于公共主页来说它没有好友信息,没有时间线,因此针对这种页面的信息的解析可能需要别的方法。而光从url、id、和页面内容来看很难区分,而我在查找获取Facebook用户ID的相关内容的时候碰巧找到了它的区分方法,公共主页的HTML代码中只有一个page_id和profile_id,而个人的只有profile_id 其中用户ID就是这个profile_id比如下面分别是一个个人主页和公共主页搜索page_id的结果从上面的结果来看个人用户中page_id 只会出现在注释中,这是用浏览器请求的结果,其实在实际使用爬虫爬取到的结果中是搜不到这个id的,我们可以根据这个特性来区分,并且获取这两种主页的IDdef _get_user_info(self, html, url): key = "page_id=(\d+)" # 使用正则表达式获取page_id后面的id值 # page_id 只会出现在公共主页上,所以根据page_id来判断页面类型 pattern = re.compile(key) it = pattern.finditer(html) user_type = 0 try: it = next(it) #如果未找到这个地方会报StopIteration异常 user_type = TopUser.PUBLIC_PAGE # 公共主页 except StopIteration: # 未找到page_id 此时视为个人主页 key = "profile_id=(\d+)" #个人主页的id是profile_id的值 pattern = re.compile(key) it = pattern.finditer(html) try: it = next(it) user_type = TopUser.PRIVATE_PAGE except StopIteration: # 两个都没找到,此时视为页面错误,返回错误,爬虫停止 pass #TODO:解析对应的用户信息,这里主要解析用户id和页面类型获取时间线信息Facebook的用户时间线是通过异步加载的方式来进行的,我使用Chrome分析过它发送的异步请求,发现它里面是经过了加密的,因此不能通过解析它的响应包来获取相关信息,但是我们有splash这一大杀器,它就是一个浏览器,一般在加载更多信息的时候都会执行下来操作,所以说这里我们只要模拟这个下拉的操作就可以了,要操作这个浏览器当然是使用lua脚本了,下面是对应的lua脚本function main(splash, args) --前面是设置cookie和代理的操作 local ok, reason = splash:go(args.url) splash:wait(math.random(5, 10)) html = splash:html() old_html = "" flush_times = args.flush_times --这里是下拉次数,就好像操作浏览器一样每次下拉就会加载新的内容 i = 0 while(html ~= old_html) -- 当下拉得到的新页面与原来的相同,就认为它已经没有新的内容了,此时就返回 do old_html = html splash:runjs([[window.scrollTo(0, document.body.scrollHeight)]]) -- 执行js下拉页面 splash:wait(math.random(1,2)) -- 这里一定要等待,否则可能会来不及加载,根据我的实验只要大于1s就可以得到下拉加载的新内容,可能具体值需要根据不同的网络环境 if (flush_times ~= 0 and i == flush_times) then -- 当达到设置下拉上限并且不为0时推出,这里下拉次数为0表示一直下拉直到没有新内容 print("即将退出循环") break end html = splash:html() i = i + 1 end return { html = splash:html(), cookies = splash:get_cookies(), }上面的代码中,首先请求时间线的界面,然后获取相关的设置,主要是下拉次数。<br/>注意这里的下拉次数要根据超时值来设置,根据splash的官方文档,每个请求都有一个超时值,大于这个超时值会直接返回504 的错误这个时候就什么都得不到了,所以这里理想情况下是可以一直下拉的,但是由于有超时值的存在,必须给定一个下拉次数。我们可以在启动splash 的时候通过参数--wait-timeout给定。然后根据这个参数的设置一直下拉,直到没有更新或者达到最大下来次数。<br/>这个也有问题,如果网络不好或者其他情况导致没有加载出来,就认为它已经没有新内容了,这样会导致爬取内容不全这样我们就获取到了用户的时间线信息,具体的内容的解析就不再多说了,需要提醒一点的是,用户发帖中包含图片时分为三种情况,单个图片,多个图片(多个图片一般就被叫做albums——相册或者图集),简单的更新头像;这三种情况下页面的对应结构不同所以需要分情况,而且当时间线中包含视频的时候情况又不同获取公共主页的发帖信息公共主页中没有时间线,所以它的解析与个人主页的不同,好在Facebook提供了一种叫做图谱API的东西可以很方便的就可以获取到发帖信息。既然有这种API,为什么不用它获取个人用户信息呢?其实我也想用,就是要针对个人使用API就必须获取用户本人的确认,也就是要用户登录你的爬虫,然后授权给你,这自然是不可能的,所以针对个人用户只能简单的通过模拟浏览器的方式来解析HTML页面要使用Facebook的 API首先要获取一个access_token. 常规思路是先去去开发者平台注册一个开发者账号并建立一个应用。然后获取应用的token。但是我发现一般的应用Token 在获取公共主页的时候也存在一个授权的问题,好在Facebook提供了一个api的测试平台,而平台中提供了一个graph explore token,这个token可以不用授权,但是它只有一个小时的有效期,所以要使用API,首先就是从这个测试平台获取到这token。Facebook并没有提供任何有效方法来获取这个token,这个时候自然又要使用传统的方式,通过splash请求这个url,然后解析HTML获取对应token。针对这个问题,我处理的步骤如下:根据上一步获取到的页面类型,如果是公共页面,则先请求https://developers.facebook.com/tools/explorer/这个url,在登录状态下(前提是你的对应账号是Facebook的开发者账号),它会自动生成一个测试用的access_token就像下面这样输入框中就是token从该页面中获取到对应的token, 并调用对应的API获取公共主页的发帖信息,这里主要调用posts 并获取它的链接、ID、具体信息、图片、创建时间和编辑者 这些信息,具体的API文档参考Facebook官方文档,这里就不再介绍他们了def get_access_token(self, response): sel = Selector(response=response) access_token = sel.xpath('//label[@class="_2toh _36wp _55r1 _58ak"]/input[@class="_58al"]//@value').extract_first() #获取到token的值 #拼接API api = urljoin("https://graph.facebook.com/v3.0", response.meta["user_id"]) api = api + "/posts" + "?access_token=" + access_token + "&fields=link,id,message,full_picture,parent_id,created_time" yield Request( url=api, callback=self._get_public_posts, errback=self.error_parse )API返回的信息是以json格式返回的,下面是使用posts返回的一个例子,这里只是作为一个例子,请求返回的内容与项目中可能并不一样,但是并不影响针对它的分析{ "data": [ { "created_time": "2018-06-01T22:30:00+0000", "message": "Congratulations to our contest winners, Joseph and Dana! It’s always a pleasure to meet the people who make our MOVEMENT possible. Thank you!", "id": "153080620724_10161074525735725" }, ], "paging": { "cursors": { "before": "Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RBeU9UTTROekExTURBek9UVTBNakkwTnc4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TnpRMU1qVTNNelUzTWpVUEJIUnBiV1VHV3hISTZABRT0ZD", "after": "Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RFMk9UZA3pOemN4TkRrMU1EWXpNVFEwTlE4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TkRZANE1UazBNekEzTWpVUEJIUnBiV1VHV3dyV0FRRT0ZD" }, "next": "https://graph.facebook.com/v3.0/153080620724/posts?access_token=EAACEdEose0cBAMo5MUmbhGNfJlCGcrZArh7RBiCeSbwpqUq84tyEOs3HvO5KWoOtkWERRgkZBFjvOCb1DQ3go7PywUrA43SdJTqjeyjs5p2w3UanCFm0jxiE4Dt91A4qcGQYo9iobrLVrtCL0bdoNdkicQb6izFzxZBx0sPVauofQMLZCEFNLNcFqfkAinPWtSVeUQRxjQZDZD&pretty=0&limit=25&after=Q2c4U1pXNTBYM0YxWlhKNVgzTjBiM0o1WDJsa0R5QXhOVE13T0RBMk1qQTNNalE2T0RFMk9UZA3pOemN4TkRrMU1EWXpNVFEwTlE4TVlYQnBYM04wYjNKNVgybGtEeDR4TlRNd09EQTJNakEzTWpSZAk1UQXhOakV3TkRZANE1UazBNekEzTWpVUEJIUnBiV1VHV3dyV0FRRT0ZD" } }这个json主要分为两个部分一个是data,包含的是具体发帖的相关信息,另一个是paging,这个值里面包含了几个游标,其中next表示下一页的请求地址,我们只要判断出json中存在这个next就循环向这个next对应的url发包,当返回的json中不存在这个next时就标明已经到了最后一页。此时就解析了所有的发帖下面是具体的代码def _get_public_posts(self, response): rep_json = json.loads(response.text) data = rep_json["data"] post_user = response.meta["user"] for post in data: try: item = FbspiderItem() item["post_id"] = post["id"] item["post_user"] = post_user item["post_message"] = post["message"] item["post_time"] = post["created_time"] item["post_link"] = post["link"] yield item #存储图片的信息 item = FBPostImgItem() item["post_id"] = post["id"] item["img_url"] = post["full_picture"] yield item except KeyError: # 暂时不处理未找到帖子内容的情况 continue if "paging" not in rep_json: return paging = rep_json["paging"] if "next" in paging: api = paging["next"] yield Request( url = api, callback= self._get_public_posts, meta={"user" : post_user}, )获取好友信息获取好友的信息也需要采用模拟浏览器的方式,首先在用户页面上查找是否有好友的链接可以供点击,如果没有说明没有开放权限,当存在这个链接并点进去之后,可能并没有好友项,比如下面这样或者另外的情况,所以这里判断是否有好友信息需要两步,第一步是上面部分有好友这一栏,第二步是点进去之后在下面一栏中有全部好友这项内容。同样即使有好友,它也不会一次加载完毕,这里也用到下拉的相关操作。部分代码如下:function main(splash, args) -- 设置cookie和代理 local ok, reason = splash:go(args.url) splash:wait(math.random(5, 10)) friend_btn = splash:select("a[data-tab-key= 'friends']") --查找最上面那栏中是否有好友这个链接 if (friend_btn) then friend_btn:mouse_click({}) --点击进入好友页面 splash:wait(math.random(5, 10)) else return { hasFriend = false, cookie = splash:get_cookies(), } end return { hasFriend = true, html = splash:html(), cookie = splash:get_cookies(), url = splash:url(), } end执行完上述代码后,再分析是否有对应的好友信息,有的话就下拉刷新页面获取更多好友信息#当上面的代码执行完后进入这个函数 def _get_friends_page(self, response): hasFriend = response.data["hasFriend"] if not hasFriend: print("用户[%s]未开放好友查询权限" % response.meta["name"]) return html = response.data["html"] sel = Selector(response = response) friends = sel.xpath("//a[@name='全部好友']") if friends == []: print("用户[%s]未开放好友查询权限" % response.meta["name"]) return #获取代理,拼接对应的lua脚本 yield SplashRequest( url = response.data["url"], callback= self.parse_friends, endpoint="execute", cookies= random.choice(self.cookie), meta= {"level" : response.meta["level"], "name" : response.meta["name"]}, args={ "wait" : 30, "lua_source" : lua_script, } )当下拉结束之后就是解析页面获取页面中的好友信息了,在解析的时候发现,当点击某个好友进入它的主页面时,页面的链接为 https://www.facebook.com/profile.php?id=100011359746168&fref=pb&hc_location=friends_tab这个时候就会产生一种想法,这个id是不是就是用户id呢?我用这个id来直接访问用户主页行不行呢?经过我的实验,这个想法是对的,是可行的,因此对于好友这层用户我们可以直接拿到它的ID,不用向前面一样根据名称来拼接,下面是解析好友信息的部分代码def parse_friends(self, response): sel = Selector(response = response) friends = sel.xpath("//li[@class='_698']//div[@class='fsl fwb fcb']//a") #拼接lua脚本 for friend in friends: url = friend.xpath(".//@href").extract_first() name = friend.xpath(".//text()").extract_first() #记录当前好友关系 friend_item = FBUserItem() friend_item["user_name"] = response.meta["name"] friend_item["friend_name"] = name print("提取到好友信息%s : %s" % (friend_item["user_name"], friend_item["friend_name"])) yield friend_item #这里再次提交请求主要是为了获取好友的好友,以及获取好友发的帖子 #其实也可以在这个请求执行完成之后解析用户主页面得到用户的ID等信息 yield SplashRequest( url = url, endpoint="execute", callback= self.parse_main_page, meta={"name" : name, "level" : level}, cookies = random.choice(self.cookie), # 从cookie池中随机取出一个cookie args={ "wait": 30, "lua_source": lua_script, } )反爬虫的相关操作针对爬虫程序来说最头疼的就是有的站点在反爬虫这块做的太好了,Facebook就是这样的一个站点,我的测试账号在执行程序的时候被封过无数次。为了防止被封我主要采取了这样几个措施减少并发数,设置发包的延时这些内容主要在scrapy的配置文件中控制DOWNLOAD_DELAY = 5 # 发包延时 CONCURRENT_REQUESTS = 16 # 最大并发数设置代理池代理池的设置通过下载中间件的process_request函数来设置,设置的相关代码如下:def process_request(self, request, spider): if self.proxies != []: proxy = random.choice(self.proxies) # self.proxies 是一个含有多个代理的列表,从中随机取一个 print("启用代理:%s" % proxy) if "splash" in request.meta: #判断是否是一个splash请求 request.meta['splash']['args']['proxy'] = proxy# 设置splash代理 else: request.meta["proxy"] = proxy #设置scrapy的代理这里虽然判断了是否为一个splash,但是针对splash的请求,这种设置方式无效,需要采用之前介绍的方式在LUA脚本中设置,而随机设置的方法就是在一组代理中随机选择一个传入对应的LUA脚本中设置UA在下载中间件的process_request函数中来设置,设置的方法与设置代理的方法类似class RotateUserAgentMiddleware(UserAgentMiddleware): def __init__(self, user_agent=''): self.user_agent = user_agent def process_request(self, request, spider): ua = random.choice(self.user_agent_list) if ua: print("启用UA :%s" % ua) request.headers.setdefault('User-Agent', ua) user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1" "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ] 设置多用户登录这里我设置了多个登录用户,通过从用户的登录cookie池中随机选取一个作为请求的cookie,在爬虫开始位置导入多个用户的用户名和密码信息,依次登录,登录成功后保存用户cookie到列表,后面每次发包前随机选取一个cookie设置SplashReuqests函数的等待时间就像前面代码中每个SplashRequest函数的args参数中总会带有 一个wait的键值,这个表示每次接到请求后等待的时长,加上这个是为了减慢爬虫运行速度防止由于发包过快导致账号被封至此,我已将之前涉及到的所有问题基本上都提到了,很多地方我自认为处理的不是很完美,但是写出来的内容勉强够用。这个爬虫项目我最大的收获就是知道了splash这个好用的东西,可惜的是它并没有中文的文档,所以像我这样刚过四级的人读起来还是有点吃力的。所以为了方便他人学习,以及提高我的英文水平我决定乘着这段时间我有空闲我想翻译它的官方文档。目前项目刚刚开始,地址为:splash中文文档PS: 不知道这个项目取这个名字会不会涉及到虚假宣传或者版权什么的,如果涉及到这些我会立马改名最后的最后列举出项目中参考的文档,在整个项目中参考的文档实在太多,不会一一列举,这里只列举我印象最深的一些回归爬虫,拥抱scrapy&splash。抓facebook public post like、comment、shareSplash官方文档Scrapy文档scrapy_splash项目文档最后再说两句没想到这个博客有这么多人来看并且评论,最近很长时间没有登录CSDN的博客,今天再登录上来才发现有许多新的留言,这些留言我没有及时回复,也不知道那些有问题的朋友,你们的问题解决了没有。在此我对自己的失误表示抱歉。另外我想额外说几点:之前有朋友通过qq联系到我,咨询相关问题,我基本上把我知道的都说出来了,我在这篇博客后面也贴出了splash相关的文档,有英文也有我自己翻译的中文的,各位朋友可以自己根据自己的情况结合着来看。在提问之前请仔细阅读相关文档,我想很多在文档上有的内容就不要来问我了。毕竟我翻译这个文档已经有些日子了,很多细节可能我自己都不太记得了。如果文档哪些地方翻译的不太清楚的,欢迎大家批评指正。另外有很多朋友私信我,问我想不想接一些私活什么的,在这里感谢这些朋友的信任。在这里我想说一下,暂时我没有接这方面的私活的准备。第一:我自己最近比较忙,在我的新年计划中提到,我目前正在进行角色转换,不仅有自己的事,也要盯着手下的人完成任务,加班比较多,没有什么时间来做私活。第二,不同的网站差距还是很大的,从用户权限验证到反爬虫机制都不一样,而且不同的私活需求也不一样,需要花大量的时间来分析目标网站。我自己并不想以后专门钻研爬虫,所以不太想花太多的时间在这种项目上。第三,我也有自己的职业规划,我希望自己能在多余时间内提升自己的职业技能。第四我自己其实没有多少信心来完成新接的项目,因为这个项目是延期的,大概延期了一周左右,而且很多地方没有我当初设想的那么简单,而且多多少少有一些问题最后无法解决,但是它并不是一个纯商业项目,只是一个简单的玩具而已。所以说实话我可能并没有独立开发一整套商用爬虫的能力。第五,大家都是在网上的陌生人,我也怕被骗,而且爬虫从某种程度上可能有法律上的风险。基于以上几点我想暂时不接受任何形式的私活,最后谢谢各位的信任
2018年06月02日
6 阅读
0 评论
0 点赞
2018-05-27
ATL模板库中的OLEDB与ADO
上次将OLEDB的所有内容基本上都说完了,从之前的示例上来看OLEDB中有许多变量的定义,什么结果集对象、session对象、命令对象,还有各种缓冲等等,总体上来说直接使用OLEDB写程序很麻烦,用很大的代码量带来的仅仅只是简单的功能。还要考虑各种缓冲的释放,各种对象的关闭,程序员的大量精力都浪费在无用的事情上,针对这些情况微软在OLEDB上提供了两种封装方式,一种是将其封装在ATL模板库中,一种是使用ActiveX控件来进行封装称之为ADO,这次主要写的是这两种方式ATL 模板中的OLEDB由于ATL模板是开源的,这种方式封装简洁,调试简易(毕竟源代码都给你了),各个模块相对独立,但是它的缺点很明显就是使用门槛相对较高,只有对C++中的模板十分熟悉的开发人员才能使用的得心应手。ATL中的OLEDB主要有两大模块,提供者模块和消费者模块,顾名思义,提供者模块是数据库的开发人员使用的,它主要使用这个模块实现OLEDB中的接口,对外提供相应的数据库服务;消费者模块就是使用OLEDB在程序中操作数据库。这里主要说的是消费者模块ATL主要封装的类ATL针对OLEDB封装的主要有这么几个重要的类:数据库对象CDataConnection 数据源连接类主要实现的是数据库的连接相关的功能,根据这个可以猜测出来它实际上封装的是OLEDB中的数据源对象和会话对象CDataSource:数据源对象CEnumerator: 架构结果集对象,主要用来查询数据库的相关信息,比如数据库中的表结构等信息CSession: 会话对象访问器对象:CAccessor: 常规的访问器对象CAccessorBase: 访问器对象的基类CDynamicAccessor:动态绑定的访问器CDynamicParamterAccessor:参数绑定的访问器,从之前博文的内容来看它应该是进行参数化查询等操作时使用的对象CDynamicStringAccessor:这个一般是要将查询结果显示为字符串时使用,它负责将数据库中的数据转化为字符串ALT中针对OLEDB的封装在头文件atldbcli.h中,在项目中只要包含它就行了模板的使用静态绑定针对静态绑定,VS提供了很好的向导程序帮助我们生成对应的类,方便了开发,使用的基本步骤如下:在项目上右键,选择添加类在类选择框中点击ATL并选择其中的ATL OLEDB使用者选择对应的数据源、数据库表和需要对数据库进行的操作注意如果要对数据库表进行增删改查等操作,一定要选这里的表选项点击数据源配置数据源连接的相关属性,最后点击完成。最终会在项目中生成对应的头文件这是最终生成的完整代码class Caa26Accessor { public: //value LONG m_aac031; TCHAR m_aaa146[51]; LONG m_aaa147; LONG m_aaa148; //status DBSTATUS m_dwaac031Status; DBSTATUS m_dwaaa146Status; DBSTATUS m_dwaaa147Status; DBSTATUS m_dwaaa148Status; //lenth DBLENGTH m_dwaac031Length; DBLENGTH m_dwaaa146Length; DBLENGTH m_dwaaa147Length; DBLENGTH m_dwaaa148Length; void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_IRowsetChange, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); } HRESULT OpenDataSource() { CDataSource _db; HRESULT hr; hr = _db.OpenFromInitializationString(L"Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa; Password=XXXXXX;Initial Catalog=study;Data Source=XXXX;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LIU-PC;Use Encryption for Data=False;Tag with column collation when possible=False"); if (FAILED(hr)) { #ifdef _DEBUG AtlTraceErrorRecords(hr); #endif return hr; } return m_session.Open(_db); } void CloseDataSource() { m_session.Close(); } operator const CSession&() { return m_session; } CSession m_session; DEFINE_COMMAND_EX(Caa26Accessor, L" \ SELECT \ aac031, \ aaa146, \ aaa147, \ aaa148 \ FROM dbo.aa26") BEGIN_COLUMN_MAP(Caa26Accessor) COLUMN_ENTRY_LENGTH_STATUS(1, m_aac031, m_dwaac031Length, m_dwaac031Status) COLUMN_ENTRY_LENGTH_STATUS(2, m_aaa146, m_dwaaa146Length, m_dwaaa146Status) COLUMN_ENTRY_LENGTH_STATUS(3, m_aaa147, m_dwaaa147Length, m_dwaaa147Status) COLUMN_ENTRY_LENGTH_STATUS(4, m_aaa148, m_dwaaa148Length, m_dwaaa148Status) END_COLUMN_MAP() }; class Caa26 : public CCommand<CAccessor<Caa26Accessor> > { public: HRESULT OpenAll() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; __if_exists(GetRowsetProperties) { CDBPropSet propset(DBPROPSET_ROWSET); __if_exists(HasBookmark) { if( HasBookmark() ) propset.AddProperty(DBPROP_IRowsetLocate, true); } GetRowsetProperties(&propset); return OpenRowset(&propset); } __if_not_exists(GetRowsetProperties) { __if_exists(HasBookmark) { if( HasBookmark() ) { CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetLocate, true); return OpenRowset(&propset); } } } return OpenRowset(); } HRESULT OpenRowset(DBPROPSET *pPropSet = NULL) { HRESULT hr = Open(m_session, NULL, pPropSet); #ifdef _DEBUG if(FAILED(hr)) AtlTraceErrorRecords(hr); #endif return hr; } void CloseAll() { Close(); ReleaseCommand(); CloseDataSource(); } };从名字上来看Caa26Accessor主要是作为一个访问器,其实它的功能也是与访问器相关的,比如创建访问器和数据绑定都在最后这个映射中。而后面的Caa26类主要是用来执行sql语句并根据上面的访问器类来解析数据,其实我们使用上主要使用后面这个类,这些代码都很简单,有之前的OLEDB基础很容易就能理解它们,这里就不再在这块进行展开了int _tmain(int argc, TCHAR *argv) { CoInitialize(NULL); Caa26 aa26; HRESULT hRes = aa26.OpenDataSource(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.OpenRowset(); if (FAILED(hRes)) { aa26.CloseAll(); CoUninitialize(); return -1; } hRes = aa26.MoveNext(); COM_USEPRINTF(); do { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), aa26.m_aac031, aa26.m_aaa146, aa26.m_aaa147, aa26.m_aaa148); hRes = aa26.MoveNext(); } while (S_OK == hRes); aa26.CloseAll(); CoUninitialize(); return 0; }动态绑定动态绑定主要是使用模板对相关的类进行拼装,所以这里需要关于模板的相关知识,只有掌握了这些才能正确的拼接出合适的类。一般需要拼接的是这样几个类结果集类,在结果集类的模板中填入对应的访问器类,表示该结果集将使用对应的访问器进行解析。访问器类可以系统预定义的,也向静态绑定那样自定义。Command类,在命令对象类的模板位置填入与命令相关的类,也就是执行命令生成的结果集、以及解析结果集所用的访问器,之后就主要使用Command类来进行数据库的相关操作了下面是一个使用的示例typedef CCommand<CDynamicAccessor, CRowset, CMultipleResults> CComRowset; typedef CTable<CDynamicAccessor, CRowset> CComTable; //将所有绑定的数据类型转化为字符串 typedef CTable<CDynamicStringAccessor, CRowset> CComTableString; int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); COM_USEPRINTF(); //连接数据库,创建session对象 CDataSource db; db.Open(); CSession session; session.Open(db); //打开数据库表 CComTableString table; table.Open(session, OLESTR("T_DecimalDemo")); HRESULT hRes = table.MoveFirst(); if (FAILED(hRes)) { COM_PRINTF(_T("表中没有数据,退出程序\n")); goto __CLEAN_UP; } do { //这里传入的参数是列的序号,注意一下,由于有第0列的行需要存在,所以真正的数据是从第一列开始的 COM_PRINTF(_T("|%-10s|%-20s|%-20s|%-20s|\n"), table.GetString(1), table.GetString(2), table.GetString(3), table.GetString(4)); hRes = table.MoveNext(); } while (S_OK == hRes); __CLEAN_UP: CoUninitialize(); return 0; }在上面的代码中我们定义了两个模板类,Ctable和CCommand类,没有发现需要的访问器类,查看CTable类可以发现它是继承于CAccessorRowset,而CAccessorRowset继承于TAccessor和 TRowset,也就是说它提供了访问器的相关功能而且它还可以使用OpenRowset方法不执行SQL直接打开数据表,因此在这里我们选择使用它在CTable的模板中填入CDynamicStringAccessor表示将会把得到的结果集中的数据转化为字符串。在使用上先使用CDataSource类的Open方法打开数据库连接,然后调用CTable的Open打开数据表,接着调用CTable的MoveFirst的方法将行句柄移动到首行。接着在循环中调用table的GetString方法得到各个字段的字符串值,并调用MoveNext方法移动到下一行其实在代码中并没有使用CCommand类,这是由于这里只是简单的使用直接打开数据表的方式,而并没有执行SQL语句,因此不需要它,在这里定义它只是简单的展示一下ADOATL针对OLEDB封装的确是方便了不少,但是对于像我这种将C++简单的作为带对象的C来看的人来说,它使用模板实在是太不友好了,说实话现在我现在对模板的认识实在太少,在代码中我也尽量避免使用模板。所以在我看来使用ATL还不如自己根据项目封装一套。好在微软实在太为开发者着想了,又提供了ADO这种针对ActiveX的封装方式。要使用ADO组件需要先导入,导入的语句如下:#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile")这个路径一般是不会变化的,而EOF在C++中一般是用在文件中的,所以这里将它rename一下ADO中的主要对象和接口有:Connect :数据库的连接对象,类似于OLEDB中的数据源对象和session对象Command:命令对象,用来执行sql语句,类似于OLEDB中的Command对象Recordset: 记录集对象,执行SQL语句返回的结果,类似于OLEDB中的结果集对象Record: 数据记录对象,一般都是从Recordset中取得,就好像OLEDB中从结果集对象通过访问器获取到具体的数据一样Field:记录中的一个字段,可以简单的看做就是一个表字段的值,一般一个记录集中有多条记录,而一条记录中有个Field对象Parameter:参数对象,一般用于参数化查询或者调用存储过程Property:属性,与之前OLEDB中的属性对应-在ADO中大量使用智能指针,所谓的智能指针是它的生命周期结束后会自动析构它所指向的对象,同时也封装了一些常见指针操作,虽然它是这个对象但是它的使用上与普通的指针基本上相同。ADO中的智能指针对象一般是在类名后加上Ptr。比如Connect对象的智能指针对象是_ConnectPtr智能指针有利也有弊,有利的地方在于它能够自动管理内存,不需要程序员进行额外的释放操作,而且它在使用上就像普通的指针,相比于使用类的普通指针更为方便,不利的地方在于为了方便它的使用一般都经过了大量的重载,因此很多地方表面上看是一个普通的寻址操作,而实际上却是一个函数调用,这样就降低了性能。所以在特别强调性能的场合要避免使用智能指针。在使用上,一般经过这样几个步骤:定义数据库连接的Connect对象调用Connect对象的Open方法连接数据库,这里使用的连接字串的方式创建Command对象并调用对象Execute方法执行SQL,并获取对应的记录集。这里执行SQL语句也可以使用Recordset对象的Open方法。循环调用Recordse对象的MoveNext不断取出对应行的行记录下面是一个使用的简单例子#import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename("EOF", "EndOfFile") int _tmain(int argc, TCHAR *argv[]) { CoInitialize(NULL); _ConnectionPtr conn; _RecordsetPtr rowset; _bstr_t bstrConn = _T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;"); conn.CreateInstance(_T("ADODB.Connection")); conn->Open(bstrConn, _T("sa"), _T("123456"), adModeUnknown); if (conn->State) { COM_PRINTF(_T("连接到数据源成功\n")); }else { COM_PRINTF(_T("连接到数据源失败\n")); return 0; } rowset.CreateInstance(__uuidof(Recordset)); rowset->Open(_T("select * from aa26;"), conn.GetInterfacePtr(), adOpenStatic, adLockOptimistic, adCmdText); while (!rowset->EndOfFile) { COM_PRINTF(_T("|%-30u|%-30s|%-30u|%-30u|\n"), rowset->Fields->GetItem(_T("aac031"))->Value.intVal, rowset->Fields->GetItem(_T("aaa146"))->Value.bstrVal, rowset->Fields->GetItem(_T("aaa147"))->Value.llVal, rowset->Fields->GetItem(_T("aaa148"))->Value.llVal ); rowset->MoveNext(); } CoUninitialize(); return 0; }ADO与OLEDB混合编程ADO相比较OLEDB来说确实方便了不少,但是它也有它的问题,比如它是封装的ActiveX控件,从效率上肯定比不上OLEDB,而且ADO中记录集是一次性将结果中的所有数据加载到内存中,如果数据表比教大时这种方式很吃内存。而OLEDB是每次调用GetNextRow时加载一条记录到内存(其实根据之前的代码可以知道它加载的时机,加载的大小是可以控制的),它相对来说比教灵活。其实上述问题使用二者的混合编程就可以很好的解决,在处理结果集时使用OLEDB,而在其他操作时使用ADO这样既保留了ADO的简洁性也使用了OLEDB灵活管理结果集内存的能力。在ADO中,可以通过_Recordset查询出ADORecordsetConstruction接口,这个接口提供了将记录集转化为OLEDB中结果集,以及将结果集转化为Recordset对象的能力下面是一个简单的例子CoInitialize(NULL); try { _bstr_t bsCnct(_T("Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Password = 123456;Initial Catalog=Study;Data Source=LIU-PC\\SQLEXPRESS;")); _RecordsetPtr Rowset(__uuidof(Recordset)); Rowset->Open(_T("select * from aa26;") ,bsCnct,adOpenStatic,adLockOptimistic,adCmdText); //获取IRowset接口指针 ADORecordsetConstruction* padoRecordsetConstruct = NULL; Rowset->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); IRowset * pIRowset = NULL; padoRecordsetConstruct->get_Rowset((IUnknown **)&pIRowset); padoRecordsetConstruct->Release(); DisplayRowSet(pIRowset); pIRowset->Release(); GRS_PRINTF(_T("\n\n显示第二个结果集:\n")); //使用OLEDB方法打开一个结果集 IOpenRowset* pIOpenRowset = NULL; TCHAR* pszTableName = _T("T_State"); DBID TableID = {}; CreateDBSession(pIOpenRowset); TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = (LPOLESTR)pszTableName; HRESULT hr = pIOpenRowset->OpenRowset(NULL,&TableID,NULL,IID_IRowset,0,NULL,(IUnknown**)&pIRowset); if(FAILED(hr)) { _com_raise_error(hr); } //创建一个新的ADO记录集对象 _RecordsetPtr Rowset2(__uuidof(Recordset)); Rowset2->QueryInterface(__uuidof(ADORecordsetConstruction), (void **) &padoRecordsetConstruct); //将OLEDB的结果集放置到ADO记录集对象中 padoRecordsetConstruct->put_Rowset(pIRowset); ULONG ulRow = 0; while(!Rowset2->EndOfFile) { COM_PRINTF(_T("|%10u|%10s|%-40s|\n") ,++ulRow ,Rowset2->Fields->GetItem("K_StateCode")->Value.bstrVal ,Rowset2->Fields->GetItem("F_StateName")->Value.bstrVal); Rowset2->MoveNext(); } } catch(_com_error & e) { COM_PRINTF(_T("发生错误:\n Source : %s \n Description : %s \n") ,(LPCTSTR)e.Source(),(LPCTSTR)e.Description()); } _tsystem(_T("PAUSE")); CoUninitialize(); return 0;这次就不再放上整体例子的链接了,有之前的基础应该很容易看懂这些,而且这次代码比较短,基本上将所有代码全粘贴了过来。
2018年05月27日
6 阅读
0 评论
0 点赞
2018-05-19
OLEDB事务
学过数据的人一般都知道事务的重要性,事务是一种对数据源的一系列更新进行分组或者批处理以便当所有更新都成功时同时提交更新,或者任意一个更新失败时进行回滚将数据库中的数据回滚到执行批处理中的所有操作之前的一种方法。使用事务保证了数据的完整性。这里不展开详细的说事务,只是谈谈OLEDB在事务上的支持ITransactionLocal接口OLEDB中支持事务的接口是ITransactionLocal接口,该接口是一个可选接口,OLEDB并不强制要求所有数据库都支持该接口,所以在使用之前需要先判断是否支持,好在现在常见的几种数据库都支持。该接口属于回话对象,因此要得到该接口只需要根据一个回话对象调用QueryInterface即可调用接口的StartTransaction方法开始一个事务该函数的原型如下HRESULT StartTransaction ( ISOLEVEL isoLevel, ULONG isoFlags, ITransactionOptions *pOtherOptions, ULONG *pulTransactionLevel); 第一个参数是事务并发的隔离级别,一般最常用的是ISOLATIONLEVEL_CURSORSTABILITY,表示只有最终提交之后才能查询对应数据库表的数据第二个参数是一个标志,目前它的值必须为0第3个参数是一个指针,它可以为空,或者是调用ITransactionLocal::GetOptionsObject函数返回的一个指针第4个参数是调用该函数创建一个事务后,该事务的并发隔离级别隔离级别是针对不同的线程或者进程的,比如有多个客户端同时在操作数据库时,如果我们设置为ISOLATIONLEVEL_CURSORSTABILITY,那么在同一事务中只有当其中一个客户端提交了事务更新后,另外一个客户端才能正常的进行查询等操作,可以简单的将这个标识视为它在数据库中上了锁,只有当它完成事务后其他客户端才可以正常使用数据库开始一个事务后正常的进行相关的数据库操作当所有步骤都正常完成后调用ITransaction::Commit方法提交事务所做的所有修改或者当其中有一步或者几步失败时调用ITransaction::Abort方法回滚所有的操作演示例子cppcpp//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据//注意使用ISOLATIONLEVEL_CURSORSTABILITY表示最终Commint以后,才能读取这两个表的数据hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); hr = pITransaction->StartTransaction(ISOLATIONLEVEL_CURSORSTABILITY,0,NULL,NULL); //获取主表主键的最大值//获取主表主键的最大值 pRetData = pRetData = RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary"));RunSqlGetValue(pIOpenRowset,_T("Select Max(PID) As PMax From T_Primary")); if(NULLif(NULL == pRetData)== pRetData) {{ goto CLEAR_UP;goto CLEAR_UP; }} iPID = iPID = *(int*)((BYTE*)pRetData +*(int*)((BYTE*)pRetData + sizeof(DBSTATUS)sizeof(DBSTATUS) ++ sizeof(ULONG));sizeof(ULONG)); //最大值总是加1,这样即使取得的是空值,起始值也是正常的1//最大值总是加1,这样即使取得的是空值,起始值也是正常的1 ++iPID;++iPID; TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszPrimaryTable;(LPOLESTR)pszPrimaryTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszPrimaryTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_PID//设置第一个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = iPID;= iPID; //设置第二个字段 F_MValue//设置第二个字段 F_MValue *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) ,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据"));,pChangeBindings[1].cbMaxLen/sizeof(WCHAR),_T("主表数据")); //插入新数据//插入新数据 hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); COM_SAFEFREE(pChangeBindings);COM_SAFEFREE(pChangeBindings); COM_SAFEFREE(pRetData);COM_SAFEFREE(pRetData); COM_SAFEFREE(pbNewData);COM_SAFEFREE(pbNewData); if(NULLif(NULL != hChangeAccessor &&!= hChangeAccessor && NULLNULL != pIAccessor)!= pIAccessor) {{ pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); pIAccessor->ReleaseAccessor(hChangeAccessor,NULL); hChangeAccessor = hChangeAccessor = NULL;NULL; }} COM_SAFERELEASE(pIAccessor);COM_SAFERELEASE(pIAccessor); COM_SAFERELEASE(pIRowsetChange);COM_SAFERELEASE(pIRowsetChange); COM_SAFERELEASE(pIRowsetUpdate);COM_SAFERELEASE(pIRowsetUpdate); //插入第二个也就是从表的数据//插入第二个也就是从表的数据 TableID.eKind = DBKIND_NAME; TableID.eKind = DBKIND_NAME; TableID.uName.pwszName = TableID.uName.pwszName = (LPOLESTR)pszMinorTable;(LPOLESTR)pszMinorTable; hr = pIOpenRowset->OpenRowset(NULL,&TableID hr = pIOpenRowset->OpenRowset(NULL,&TableID ,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange);,NULL,IID_IRowsetChange,1,PropSet,(IUnknown**)&pIRowsetChange); COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr);COM_COM_CHECK(hr,_T("打开表对象'%s'失败,错误码:0x%08X\n"),pszMinorTable,hr); ulChangeOffset = ulChangeOffset = CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols);CreateAccessor(pIRowsetChange,pIAccessor,hChangeAccessor,pChangeBindings,ulRealCols); if(0if(0 == ulChangeOffset== ulChangeOffset |||| NULLNULL == hChangeAccessor== hChangeAccessor |||| NULLNULL == pIAccessor== pIAccessor |||| NULLNULL == pChangeBindings== pChangeBindings |||| 00 == ulRealCols)== ulRealCols) {{ goto CLEAR_UP;goto CLEAR_UP; }} //分配一个新行数据 设置数据后 插入//分配一个新行数据 设置数据后 插入 pbNewData = pbNewData = (BYTE*)COM_CALLOC(ulChangeOffset);(BYTE*)COM_CALLOC(ulChangeOffset); //设置第一个字段 K_MID//设置第一个字段 K_MID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[0].obLength) == sizeof(int);sizeof(int); //设置第二个字段 K_PID//设置第二个字段 K_PID *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[1].obLength) == sizeof(int);sizeof(int); *(int*)*(int*) (pbNewData + pChangeBindings[1].obValue)(pbNewData + pChangeBindings[1].obValue) = iPID;= iPID; //设置第二个字段//设置第二个字段 *(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength)*(DBLENGTH *)((BYTE *)pbNewData + pChangeBindings[2].obLength) == 8;8; StringCchCopy((WCHAR*)StringCchCopy((WCHAR*) (pbNewData + pChangeBindings[2].obValue)(pbNewData + pChangeBindings[2].obValue) ,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据"));,pChangeBindings[2].cbMaxLen/sizeof(WCHAR),_T("从表数据")); for(int i = iMIDS; i <= iMIDMax; i++)for(int i = iMIDS; i <= iMIDMax; i++) {//循环插入新数据{//循环插入新数据 //设置第一个字段 K_MID//设置第一个字段 K_MID *(int*)*(int*) (pbNewData + pChangeBindings[0].obValue)(pbNewData + pChangeBindings[0].obValue) = i;= i; hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); hr = pIRowsetChange->InsertRow(NULL,hChangeAccessor,pbNewData,NULL); COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用InsertRow插入新行失败,错误码:0x%08X\n"),hr); }} hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); hr = pIRowsetChange->QueryInterface(IID_IRowsetUpdate,(void**)&pIRowsetUpdate); COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("获取IRowsetUpdate接口失败,错误码:0x%08X\n"),hr); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); hr = pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL); COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("调用Update提交更新失败,错误码:0x%08X\n"),hr); //所有操作都成功了,提交事务释放资源//所有操作都成功了,提交事务释放资源 hr = pITransaction->Commit(FALSE, XACTTC_SYNC, hr = pITransaction->Commit(FALSE, XACTTC_SYNC, 0);0); COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr);COM_COM_CHECK(hr,_T("事务提交失败,错误码:0x%08X\n"),hr); CLEAR_UP:CLEAR_UP://操作失败,回滚事务先,然后释放资源//操作失败,回滚事务先,然后释放资源 hr = pITransaction->Abort(NULL, FALSE, FALSE); hr = pITransaction->Abort(NULL, FALSE, FALSE);在上述代码中首先创建一个事务对象,然后在进行相关的数据库操作,这里主要是在更新和插入新数据,当所有操作成功后调用commit函数提交,当其中有错误时会跳转到CLEAR_UP标签下,调用Abort进行回滚 最后实例的完整代码: [Trancation](https://gitee.com/masimaro/codes/tcesnrul0g2yi76bam5dj19#Trancation) <!-- more -->
2018年05月19日
1 阅读
0 评论
0 点赞
2018-05-12
OLEDB 简单数据查找定位和错误处理
在数据库查询中,我们主要使用的SQL语句,但是之前也说过,SQL语句需要经历解释执行的步骤,这样就会拖慢程序的运行速度,针对一些具体的简单查询,比如根据用户ID从用户表中查询用户具体信息,像这样的简单查询OLEDB提供了专门的查询接口。使用该接口可以很大程度上提升程序性能。另外在之前的代码中,只是简单的通过HRESULT这个返回值来判断是否成功,针对错误没有具体的处理,但是OLEDB提供了自己的处理机制,这篇博文主要来介绍这两种情况下的处理方式简单数据查询和定位它的使用方法与之前的简单读取结果集类似,主要经历如下几部绑定需要在查询中做条件的几列(绑定的方式与之前的相同)分配一段内存,给定对应的条件值循环调用IRowsetFind接口的FindNextRow方法,传入对应的结果集、条件、条件值的缓冲,接收函数返回的新的结果集指针使用常规方法访问结果集FindNextRow函数的定义如下:HRESULT FindNextRow ( HCHAPTER hChapter, HACCESSOR hAccessor, //绑定查询条件的访问器,用于OLEDB组件访问用户传进来的条件 void *pFindValue, //之前的内存缓冲 DBCOMPAREOP CompareOp, // 查询条件,主要有:DBCOMPAREOPS_EQ、DBCOMPAREOPS_NE、DBCOMPAREOPS_LT等等,具体的请参看MSDN DBBKMARK cbBookmark, const BYTE *pBookmark, DBROWOFFSET lRowsOffset, DBROWCOUNT cRows, //一次返回的行数 DBCOUNTITEM *pcRowsObtained, //真实返回的行 HROW **prghRows); //返回的行访问器的句柄数组下面是一个具体的例子:HRESULT hRes = pIRowset->QueryInterface(IID_IRowsetFind, (void**)&pIRowsetFind); COM_SUCCESS(hRes, _T("查询接口IRowsetFind失败,错误码为:%08x\n"), hRes); hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口IColumnInfo失败,错误码为:%08x\n"), hRes); hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询接口IAccessor失败,错误码为:%08x\n"), hRes); hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &rgColumnsInfo, &lpColumnsName); COM_SUCCESS(hRes, _T("查询列信息失败,错误码为:%08x\n"), hRes); rgQueryBinding[0].bPrecision = rgColumnsInfo[1].bPrecision; rgQueryBinding[0].bScale = rgColumnsInfo[1].bScale; rgQueryBinding[0].cbMaxLen = sizeof(ULONG); rgQueryBinding[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgQueryBinding[0].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; rgQueryBinding[0].eParamIO = DBPARAMIO_NOTPARAM; rgQueryBinding[0].iOrdinal = 4; //绑定第4列,也就是表中的所属行政区编号列 rgQueryBinding[0].obStatus = 0; rgQueryBinding[0].obLength = sizeof(DBSTATUS); rgQueryBinding[0].obValue = sizeof(DBSTATUS) + sizeof(ULONG); rgQueryBinding[0].wType = DBTYPE_I4; hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1, rgQueryBinding, 0, &hQueryAccessor, NULL); COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes); pQueryBuff = COM_ALLOC(void, rgQueryBinding[0].obValue + sizeof(ULONG)); *(DBSTATUS*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obValue) = DBSTATUS_S_OK; *(ULONG*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obLength) = sizeof(ULONG); *(ULONG*)((LPBYTE)pQueryBuff + rgQueryBinding[0].obValue) = uId; hRes = pIRowsetFind->FindNextRow(DB_NULL_HCHAPTER, hQueryAccessor, pQueryBuff, DBCOMPAREOPS_EQ, 0, NULL, 0, 10, &cRowsObtained, &rgShowRows); COM_SUCCESS(hRes, _T("查询结果集失败,错误码为:%08x\n"), hRes); bRet = ReadRows(cColumns, rgColumnsInfo, cRowsObtained, rgShowRows, pIRowset);这段代码首先获取到对应列的列信息,然后根据这个列信息进行动态绑定,在这里我们绑定第4列,也就是之前行政区表的所属行政区编号列,接着针对这个绑定创建访问器,并分配缓冲存储对应的条件值,最后调用FindNextRow返回查询到的新的结果集,并调用对应的函数读取返回的结果集上面的代码并不复杂,从FindNextRow的第4个参数的值来看,它只能支持简单的大于小于等于等等操作,像sql语句中的模糊查询,多表查询,联合查询等等它是不能胜任的,因此说它只是一个简单查询,它在某些简单场合下可以节省性能,但是对于复杂的业务逻辑中SQL语句仍然是不二的选择错误处理在windows中定义了丰富的错误处理代码和错误处理方式,几乎每种类型的程序都有自己的一套处理方式,比如Win32 API中的GetLastError,WinSock中的WSAGetLastError, 其实在OLEDB中有它自己的处理方式。COM中可以使用GetErrorInfo函数得到一个错误的信息的接口,IErrorInfo,进一步可以根据该接口的对应函数可以得到具体的错误信息。IErrorInfo接口IErrorInfo 有时候自身包含一些出错信息,可以直接读取。IErrorInfo有时候只有一条错误信息,有时候是一个树形结构的错误信息通过调用QueryInterface函数查询错误对象的IErrorRecords接口来判定错误信息是否还有详细的子记录。如果有子记录。如果能得到IErrorRecords接口,就调用IErrorRecords::GetRecordCount获得错误信息记录个数,接着循环调用IErrorRecords::GetErrorInfo又取得子记录的IErrorInfo接口,并获取错误信息若没有子错误记录,那么直接调用IErrorInfo::GetDescription得到错误描述信息,调用IErrorInfo::GetSource得到错误来源信息以上所述IErrorInfo接口是COM定义的标准接口,IErrorRecords是OLEDB专门定义的错误信息记录接口。IErrorRecords接口其实IErrorRecords接口除了能获取子记录的IErrorInfo接口外还有一个重要的功能。调用接口的GetBasicErrorInfo方法可以得到一个指定索引错误记录的基本错误信息结构体ERRORINFO。该结构的定义如下:typedef struct tagERRORINFO { HRESULT hrError; DWORD dwMinor; CLSID clsid; IID iid; DISPID dispid; } ERRORINFO;根据这个结构可以得到指定错误的具体信息,有点类似于FormatMessage格式化错误码,得到错误码对应的错误提示信息。另外可以调用接口的GetCustomErrorObject给定一个错误码,得到一个具体的错误对象,一般在OLEDB中这个对象是ISQLErrorInfo接口这两个函数的第一个参数是一个编号,这个编号一般是第几个IErrorRecords中错误信息的编号。下面是一个具体的例子LPOLESTR lpSQL = OLESTR("select * where aa26;select * from aa26 where, aac031;"); HRESULT hRes =pIOpenRowset->QueryInterface(IID_IDBCreateCommand, (void**)&pIDBCreateCommand); COM_SUCCESS(hRes, _T("查询接口IDBCreateCommand失败,错误码为:%08x\n"), hRes); LPOLESTR lpErrorInfo = (LPOLESTR)CoTaskMemAlloc(1024 * sizeof(WCHAR)); hRes = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**)&pICommandText); COM_SUCCESS(hRes, _T("创建接口ICommandText失败,错误码为:%08x\n"), hRes); pICommandText->SetCommandText(DBGUID_DEFAULT, lpSQL); hRes = pICommandText->Execute(NULL, IID_IRowset, NULL, NULL, (IUnknown**)&pIRowset); if (FAILED(hRes)) { GetErrorInfo(0, &pIErrorInfo); HRESULT hr = pIErrorInfo->QueryInterface(IID_IErrorRecords, (void**)&pIErrorRecords); if (SUCCEEDED(hr)) { ULONG uRecordes = 0; hr = pIErrorRecords->GetRecordCount(&uRecordes); COM_SUCCESS(hr, _T("获取错误集个数失败,错误码为:%08x\n"), hr); for (int i = 0; i < uRecordes; i++) { ReadErrorRecords(lpErrorInfo, 1024 * sizeof(WCHAR), hRes, i, pIErrorRecords, lpSQL); COM_PRINTF(_T("%s"), lpErrorInfo); } }else { ReadErrorInfo(lpErrorInfo, 1024 * sizeof(WCHAR), hRes, pIErrorInfo); COM_PRINTF(_T("%s"), lpErrorInfo); } }在上述例子中,我们故意传入一个错误的SQL语句,让其出错,然后通过GetErrorInfo函数获取一个错误的IErrorInfo接口,尝试查询IErrorRecords,如果有那么在循环中遍历它的子集,并且得到每个子集的详细错误信息。否则直接调用函数ReadErrorInfo获取错误的具体信息BOOL ReadErrorRecords(LPOLESTR &lpErrorInfo, DWORD dwSize, HRESULT hErrRes, ULONG uRecordIndex, IErrorRecords *pIErrorRecords, LPWSTR lpSql) { ERRORINFO ErrorInfo = {0}; static LCID lcid = GetUserDefaultLCID(); HRESULT hRes = pIErrorRecords->GetBasicErrorInfo(uRecordIndex, &ErrorInfo); COM_CHECK_HR(hRes); hRes = pIErrorRecords->GetErrorInfo(uRecordIndex, lcid, &pIErrorInfo); COM_CHECK_HR(hRes); hRes = pIErrorInfo->GetDescription(&pstrDescription); COM_CHECK_HR(hRes); hRes = pIErrorInfo->GetSource(&pstrSource); COM_CHECK_HR(hRes); if (ReadSQLError(&bstrSQLErrorInfo, pIErrorRecords, uRecordIndex)) { StringCchPrintf(lpErrorInfo, dwSize, _T("\n(%s)\n错误信息: HRESULT=0x%08X\n描述: %s\nSQL错误信息: %s\n来源: %s"), lpSql, ErrorInfo.hrError, pstrDescription, bstrSQLErrorInfo, pstrSource); }else { StringCchPrintf(lpErrorInfo, dwSize, _T("\n(%s)\n错误信息: HRESULT=0x%08X\n描述: %s\n来源: %s"), lpSql, ErrorInfo.hrError, pstrDescription, pstrSource); } }该函数用于显示错误子集的信息,在函数中首先调用IErrorRecords接口的GetBasicErrorInfo函数传入子集的编号,获取子集的基本信息,然后再调用IErrorRecords接口的GetErrorInfo方法获取子集的IErrorInfo接口,接着调用IErrorInfo接口的相应函数获取错误的详细信息,在这个里面我们调用了另外一个自定义函数ReadSQLError,尝试获取在执行SQL语句时的错误,然后进行相关的输出。函数的部分代码如下:BOOL ReadSQLError(BSTR *pbstrSQL, IErrorRecords *pIErrorRecords, DWORD dwRecordIndex) { HRESULT hRes = pIErrorRecords->GetCustomErrorObject(dwRecordIndex, IID_ISQLErrorInfo, (IUnknown**)&pISQLErrorInfo); hRes = pISQLErrorInfo->GetSQLInfo(pbstrSQL, &lNativeErrror); }这个函数就简单的调用了IErrorRecords接口的GetCustomErrorObject方法传入子集的编号,获取到ISQLErrorInfo接口,最后调用ISQLErrorInfo接口的GetSQLInfo方法获取执行SQL语句时的错误。至于函数ReadErrorInfo,它的代码十分简单,就只是ReadErrorRecords函数中关于IErrorInfo处理的部分代码而已,在这就不在说明
2018年05月12日
6 阅读
0 评论
0 点赞
2018-05-04
OLEDB 数据变更通知
除了之前介绍的接口,OLEDB还定义了其他一些支持回调的接口,可以异步操作OLEDB对象或者得到一些重要的事件通知,从而使应用程序有机会进行一些必要的处理。其中较有用的就是结果集对象的变更通知接口。通过这个接口可以及时得到结果集被增删改数据变化的情况,并有机会进行必要的数据合法性审核。除了之前介绍的接口,OLEDB还定义了其他一些支持回调的接口,可以异步操作OLEDB对象或者得到一些重要的事件通知,从而使应用程序有机会进行一些必要的处理。其中较有用的就是结果集对象的变更通知接口。通过这个接口可以及时得到结果集被增删改数据变化的情况,并有机会进行必要的数据合法性审核。数据变更通知的接口是IRowsetNotify,数据源对象要求的异步通知事件接口是IDBAsynchNotify。标准COM的回调方式为了更好的理解OLEDB的回调,先回忆一下标准COM的回调方式。COM组件除了提供函数供应用程序主动调用这种方式外,还提供了回调这种方式,这种方式由用户实现相应的接口,然后由COM组件来调用,这样我们就可以知道COM组件的运行状态,同时能针对一些情况进行处理,比如处理内存耗尽,获取用户输入等等。要支持事件回调的COM组件必须提供IConnectionPointContainer接口,调用者调用IConnectionPointContainer接口的FindConnectPoint接口,通过回调事件的IID找到特定的事件挂载点,然后调用接口的Advise方法将挂载点与对应的回调函数关联起来(一个事件可以对应多个回调函数)这样当事件发生时就可以调用对应的回调函数。这个机制有点类似于QT中的信号和槽函数机制,QT中的事件是实现定义好的,可以直接使用而这里是需要通过事件ID找到具体事件,拥有事件后,与QT步骤类似,都是需要将事件与对应的回调函数绑定。IRowsetNotify接口对于OLEDB结果集来说,最重要的事件接口是IRowsetNotify,该接口提供三个重要的通知函数:OnFieldChange:列数据发生变更OnRowChange: 行发生变化,尤其是删除或者插入行OnRowsetChange:修改数据被提交通过这些事件函数具体实现时设置不同的返回值可以控制结果集对象对修改做出的响应,比如:返回S_OK表示接受这个修改,返回S_FALSE明确拒绝接受这个修改。这样就给了一个最终反悔的机制。这些函数有两个重要的参数:DBREASON: 发生变化的原因DBEVENTPHASE:事件被触发的阶段通过对这两个参数组合的判定,可以准确的判断出结果集中数据变化的动态追踪及情况DBREASON 参数的相关值DBREASON_ROW_ASYNCHINSERT:异步插入DBREASON_ROWSET_FETCHPOSITIONCHANGE:结果集的行指针发生变化,当调用类似 IRowset::GetNextRows or IRowset::RestartPosition时触发DBREASON_ROWSET_RELEASE:当结果集被释放的时候触发DBREASON_ROWSET_CHANGED:数据库中某些元数据发生变化时触发,这里是指描述数据库表字段的一些信息发生变化,比如表字段的大小,类型这些数据,要修改这些数据需要用户具有一定的权限,一般情况下不会触发这个原因DBREASON_COLUMN_SET:当行数据被设置时触发(这里只是已存在的行数据被设置,不包括新增行),一般调用SetData时会触发DBREASON_COLUMN_RECALCULATED:当列的值发生变更时触发,一般是调用SetDataDBREASON_ROW_ACTIVATE:当用户修改行指针导致行的状态由未激活变为激活时触发DBREASON_ROW_RELEASE:当调用ReleaseRows释放某些行句柄的时候触发DBREASON_ROW_DELETE:当行被删除时触发DBREASON_ROW_FIRSTCHANGE:当某些行的某列被设置新值后又改变了当前行指针的指向时,它会被第一时间触发,并且它的触发会早于DBREASON_COLUMN_SET,这个事件只会在使用延迟更新的时候才会产生。DBREASON_ROW_INSERT:在插入新行的时候触发DBREASON_ROW_UNDOCHANGE:当调用Undo放弃修改的时候触发DBREASON_ROW_UNDOINSERT:当调用Undo放弃插入新行的时候触发DBREASON_ROW_UNDODELETE:当调用Undo放弃删除的时候触发DBREASON_ROW_UPDATE:当调用Update进行更新的时候触发DBEVENTPHASE这个参数表示当前执行的状态,一般操作数据结果集有5个状态,分别对应这样的5个值:DBEVENTPHASE_OKTODO:准备好了去做,当应用程序需要操作结果集的时候会发送一个DBEVENTPHASE_OKTODO到监听程序(在这暂时就理解为OLEDB的数据源),监听程序收到后不会立马去执行该动作,而是会返回S_OK表示它知道了这个请求,或者返回S_FALSE拒绝这个请求DBEVENTPHASE_ABOUTTODO:当数据源针对 DBEVENTPHASE_OKTODO返回S_OK时,应用程序会给一个信号,告知数据源可以进行执行动作之前最后的准备工作,这部完成之后,数据源会异步的执行相关请求操作DBEVENTPHASE_DIDEVENT:当数据源执行完这次的请求之后会到这个状态,此时数据库表的数据已经更新DBEVENTPHASE_FAILEDTODO:当之前的某一步发生错误时会进入这个状态,此时会产生回滚,将数据还原到最开始的状态。下面是数据状态迁移图,这个图很形象的展示了在某个操作执行过程中的各种状态变化结果集对象事件通知接口的使用方法定义一个派生自IRowsetNotify接口的类,并实现其接口中的所有方法设置结果集对象属性集DBPROPSET_ROWSET中的DBPROP_IConnectionPointContainer属性为VARIANT_TRUE获得结果集对象调用IRowset::QueryInterface方法得到IConnectionPointContainer接口指针调用IConnectionPointContainer::FindConnectionPoint方法得到IRowsetNotify接口对应的IConnectionPoint接口指针实例化一个第一步中创建的类调用IConnectionPoint::Advise并传递该对象指针对结果集对象进行操作,此时如果事件条件成立,结果集对象会调用该对象的相应方法通知调用者触发了什么事件详细的内容可以参考MSDN IRowsetNotify例子最后来看使用的具体例子class CCOMRowsetNotify: public IRowsetNotify { public: CCOMRowsetNotify(void); virtual ~CCOMRowsetNotify(void); protected: virtual HRESULT FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp); public: virtual HRESULT Addvise(IUnknown *pIUnknown, REFIID rrid); virtual HRESULT UnAddvise(IUnknown *pIUnknown, REFIID rrid); public: virtual STDMETHODIMP_(ULONG) AddRef(void); virtual STDMETHODIMP_(ULONG) Release(void); virtual STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject); virtual STDMETHODIMP OnFieldChange (IRowset *pRowset, HROW hRow, DBORDINAL cColumns, DBORDINAL rgColumns[], DBREASON eReason, DBEVENTPHASE ePhase,BOOL fCantDeny); virtual STDMETHODIMP OnRowChange (IRowset *pRowset, DBCOUNTITEM cRows,const HROW rghRows[], DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny); virtual STDMETHODIMP OnRowsetChange (IRowset *pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny); protected: ULONG m_uRef; DWORD m_dwCookie; };使用时首先定义一个派生自IRowsetNotify的类,并实现所有的接口方法if (!OpenTable(pIOpenRowset, pIRowsetChange)) { COM_PRINTF(_T("打开表失败\n")); goto __CLEAN_UP; } RowsetNotify.Addvise(pIRowsetChange, IID_IRowsetNotify); HRESULT CCOMRowsetNotify::FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp) { IConnectionPointContainer* pICpc = NULL; HRESULT hr = pIUnknown->QueryInterface(IID_IConnectionPointContainer,(void**)&pICpc); if(FAILED(hr)) { COM_PRINTF(_T("通过IRowset接口获取IConnectionPointContainer接口失败,错误码:0x%08X\n"),hr); return hr; } hr = pICpc->FindConnectionPoint(rrid,&pIcp); if(FAILED(hr)) { COM_PRINTF(_T("获取IConnectionPoint接口失败,错误码:0x%08X\n"),hr); COM_SAFE_RELEASE(pIcp); return hr; } return hr; } HRESULT CCOMRowsetNotify::Addvise(IUnknown *pIUnknown, REFIID rrid) { IConnectionPoint *pIcp = NULL; HRESULT hRes = FindConnectionPointContainer(pIUnknown, rrid, pIcp); if (S_OK != hRes) { return hRes; } hRes = pIcp->Advise(dynamic_cast<IRowsetNotify*>(this), &m_dwCookie); COM_SAFE_RELEASE(pIcp); return hRes; } 上述代码先打开数据结果集,然后调用类对象的Addvise方法传入IID_IRowsetNotify接口指针,在方法Addvise中做的主要操作是首先使用传入的接口指针查找到接口IConnectionPointContainer,然后利用IConnectionPointContainer接口的FindConnectionPoint方法找到对应的挂载点,最后调用IConnectionPointContainer的Advise方法将对应的类对象挂载到挂载点上,这样在后面操作结果集时就会调用对应的On函数,完成对应事件的处理
2018年05月04日
7 阅读
0 评论
0 点赞
2018-04-27
OLEDB 静态绑定和数据转化接口
title: OLEDB 静态绑定和数据转化接口tags: [OLEDB, 数据库编程, VC++, 数据库, 静态绑定, 数据转化对象接口]date: 2018-04-27 20:13:54categories: windows 数据库编程keywords: OLEDB, 数据库编程, VC++, 数据库, 静态绑定, 数据类型转化OLEDB 提供了静态绑定和动态绑定两种方式,相比动态绑定来说,静态绑定在使用上更加简单,而在灵活性上不如动态绑定,动态绑定在前面已经介绍过了,本文主要介绍OLEDB中的静态,以及常用的数据类型转化接口。静态绑定之前的例子都是根据返回的COLUMNINFO结构来知晓数据表中各项的具体信息,然后进行绑定操作,这个操作由于可以动态的针对不同的数据类型绑定为不同的类型,因此称之为动态绑定。动态绑定是建立在我们对数据库中表结构一无所知,而又需要对数据库进行编程,但是一般在实际的项目中开发人员都是知道数据库的具体结构的,而且一旦数据库设计好了后续更改的可能性也不太大,因此可以采取静态绑定的方式来减少编程的复杂度。在进行静态绑定时,一般针对每个数据库表结构定义一个结构体用来描述表的各项数据,然后利用结构体的偏移来绑定到数据库中。数据关系对应表一般静态绑定需要将数据库表的各项数据与结构体中的成员一一对应,这个时候就涉及到数据库数据类型到C/C++中数据类型的转化,下表列举了常见的数据库类型到C/C++数据类型的转化关系数据库类型OLEDB 类型C/C++类型binaryDBTYPE_BYTES/DBTYPE_IUNKNOWNBYTE[length]/BLOBvarbinaryDBTYPE_BYTES/DBTYPE_IUNKNOWNBLOBbitDBTYPE_BOOLVARIANT_BOOLcharDBTYPE_STRchar[length]varcharDBTYPE_STRchar[length]nvarcharDBTYPE_WSTRwchar_t[length]ncharDBTYPE_WSTRwchar_t[length]textDBTYPE_STR/DBTYPE_IUNKNOWNBLOBimageDBTYPE_BYTES/DBTYPE_IUNKNOWNBLOBntextDBTYPE_WSTR/DBTYPE_IUNKNOWNBLOBtinyintDBTYPE_UI1BYTEsmallintDBTYPE_I2SHORTintDBTYPE_I4LONGbigintDBTYPE_I8LARGE_INTEGERrealDBTYPE_R4floatfloatDBTYPE_R8doublemoneyDBTYPE_CYLARGE_INTEGERnumericDBTYPE_NUMERICtypedef struct tagDB_NUMERIC {<br/> BYTE precision;<br/> BYTE scale;<br/> BYTE sign;<br/> BYTE val[16];} DB_NUMERIC;decimalDBTYPE_NUMERICtypedef struct tagDB_NUMERIC {<br/> BYTE precision;<br/> BYTE scale;<br/> BYTE sign;<br/> BYTE val[16];<br/>} DB_NUMERIC;sysnameDBTYPE_WSTRwchar_t[length]datetimeDBTYPE_DBTIMESTAMPtypedef struct tagDBTIMESTAMP {<br/> SHORT year;<br/> USHORT month;<br/> USHORT day;<br/> USHORT hour;<br/> USHORT minute;<br/> USHORT second;<br/> ULONG fraction;<br/>}DBTIMESTAMP;timestampDBTYPE_BYTESBYTE[length]uniqueidentifierDBTYPE_GUIDGUID实例下面是一个静态绑定的例子//静态绑定的结构 typedef struct _tag_DBSTRUCT { DBSTATUS dbCodeStatus; ULONG uCodeLength; int nCode; DBSTATUS dbNameStatus; ULONG uNameLength; WCHAR szName[NAME_LENGTH]; }DBSTRUCT, *LPDBSTRUCT; dbBindings[0].bPrecision = 0; dbBindings[0].bScale = 0; dbBindings[0].cbMaxLen = sizeof(int); dbBindings[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED; dbBindings[0].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; dbBindings[0].iOrdinal = pdbColumnInfo[0].iOrdinal; dbBindings[0].obStatus = offsetof(DBSTRUCT, dbCodeStatus); dbBindings[0].obLength = offsetof(DBSTRUCT, uCodeLength); dbBindings[0].obValue = offsetof(DBSTRUCT, nCode); dbBindings[0].wType = DBTYPE_I4; dbBindings[1].bPrecision = 0; dbBindings[1].bScale = 0; dbBindings[1].cbMaxLen = sizeof(WCHAR) * NAME_LENGTH; dbBindings[1].dwMemOwner = DBMEMOWNER_CLIENTOWNED; dbBindings[1].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE; dbBindings[1].iOrdinal = pdbColumnInfo[1].iOrdinal; dbBindings[1].obStatus = offsetof(DBSTRUCT, dbNameStatus); dbBindings[1].obLength = offsetof(DBSTRUCT, uNameLength); dbBindings[1].obValue = offsetof(DBSTRUCT, szName); dbBindings[1].wType = DBTYPE_WSTR; hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 2, dbBindings, 0, &hAccessor, NULL); pdbStruct = (DBSTRUCT*)COM_ALLOC(sizeof(DBSTRUCT) * cRows); while (TRUE) { hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &cRowsObtained, &prghRows); if (hRes != S_OK && cRowsObtained == 0) { break; } ZeroMemory(pdbStruct, sizeof(DBSTRUCT) * cRows); for (int i = 0; i < cRowsObtained; i++) { hRes = pIRowset->GetData(prghRows[i], hAccessor, &pdbStruct[i]); if (!FAILED(hRes)) { COM_PRINTF(_T("%012d\t%s\n"), pdbStruct[i].nCode, pdbStruct[i].szName); } } pIRowset->ReleaseRows(cRowsObtained, prghRows, NULL, NULL, NULL); }我们针对之前的行政区表来进行演示,在这个表中我们只查询其中的两列数据,与之前的例子相似,针对每列定义3项数据,分别是状态,长度和真实的数据,在绑定的时候就不需要计算总体需要内存的大小,行内存大小就是结构体的大小,在绑定的时候我们结构体成员在结构体中的偏移作为返回数据时各项在缓冲中的偏移。而在访问数据时就需要自己计算偏移,直接使用结构体中的成员即可。从上面的例子,我总结了静态绑定和动态绑定之间的差别:其实从本质上将动态绑定和静态绑定没有区别,都是分配一段缓冲作为行集的缓冲,然后在使用的时候进行偏移的计算静态绑定是利用我们提前知道数据库的结构,实现通过结构体来安排各项在缓冲中的偏移所占内存的大小。动态绑定中所有成员的分配和所占内存大小都是根据COLUMNINFO结构事后动态分配的,需要自己计算偏移。相比于动态绑定来说,静态绑定不需要获取数据库的各项的属性信息,不需要自己计算各项的偏移,相对比较简单,适用于事先知道数据库的表结构,使用相对固定,一旦数据库结构改变就需要改变代码动态绑定可以适用于几乎任何情形,可扩展性强,几乎不需要考虑数据库表结构变更问题。代码灵活,但是需要自己计算偏移,自己分配管理内存,相对来说对程序员的功力要求更高一些。数据类型转化数据库中数据类型繁多,而对应到具体的编程语言上有不同的展示方式,具体的语言中对同一种数据库类型有不同的数据类型对应,甚至有的可能并没有什么类型可以直接对应,这就涉及到一个从数据库数据类型到具体编程语言数据类型之间进行转换的问题,针对这一问题OLEDB提供了一个接口——IDataConvert一般情况下任何数据类型都可以转化为相应格式的字符串,而对应的字符串又可以反过来转化为数据库中相应的数据类型。当然一些特殊转换也是允许的,比如:整型数据间的转换,浮点数间的转换等。这也是使用这个数据转化接口的主要原则。数据转换接口的使用使用COM标准的方式创建IDataConver接口(调用CreateInstance函数传入CLSID_OLEDB_CONVERSIONLIBRARY创建一个IID_IDataConvert接口)接着调用该接口的DataConvert方法可以进行数据转化调用接口的CanConvert可以知道两种数据类型之间能否进行转化。调用GetConversionSize可以知道源数据类型转化为指定类型时需要的缓冲大小。实例这个例子相对比较简单,就简单的在之前打印数据库数据中加了一句转化为字符串的操作,然后打印,就不在文中展示了,具体的例子见我放到码云中的代码片段例子代码:静态绑定数据类型转化
2018年04月27日
7 阅读
0 评论
0 点赞
2018-04-27
Python处理正则表达式超时的办法
title: Python处理正则表达式超时的办法tags: [python3, 正则表达式超时, re模块]date: 2018-04-27 21:40:21categories: Pythonkeywords: python3, 正则表达式, re模块, linux信号最近在项目中遇到一个问题,就是需要采用正则匹配一些疑似暗链和挂马的HTML代码,而公司的老大给的正则表达式有的地方写的不够严谨,导致在匹配的时候发生卡死的现象,而后面的逻辑自然无法执行了。虽然用正则表达式来判断暗链和挂马可能不那么准确或者行业内很少有人那么做,但是本文不讨论如何使用正确的姿势判断暗链挂马,只关注与正则超时的处理。在使用正则表达式的时候,如果正则写的太糟糕,所消耗的时间是惊人的,并且有可能会一直回溯,而产生卡死的现象,所以一般的大型公司都会有专门的人来对正则进行优化,从而提高程序效率。一般来说如果可能的话不要让用户来输入正则进行匹配。但是现在既没有专门的人进行正则的优化,本人也对正则了解的不够,所以只能从另外的角度来考虑处理超时的问题。首先我想到的方法是另外开启一个线程来进行匹配,而在主线程中进行等待,如果发现子线程在规定的时间内没有返回就kill掉子线程。这也是一个方案,但是我现在要介绍另外一种方案,该方案来自我在网上看到的一篇博客.博客地址该博客给出了另外一种办法,就是采用信号的方式,在正则匹配之前定义一个信号,并规定触犯时间和处理的函数,如果在规定时间内程序没有结束那么触发一个TimeoutError的异常,而主线程收到这个异常时就会中断执行,并处理这个异常,这样就从正则匹配中解脱出来,达到了我们要的结果。这个方法有两个不足之处:信号这个东西是Linux独有的,在Windows下不适用信号只能在主线程中使用,而如果在子线程中进行正则匹配,那么这个方法就不适用我的项目正在运行在Linux系统上,所以针对第一个不足来说可以接受,但是我的正则匹配都是在子线程中,所以乍看之下这个方案也不太靠谱,但是好在我在后面的评论中发现博主给出了针对第二种不足的解决方案——开辟一个子进程,将正则匹配放到子线程中,这样一来可以充分利用多核(毕竟Python中的多线程是个伪多线程),二来可以分方便的使用该方案解决问题,下面是实际的代码import re import multiprocessing import signal def time_out(b, c): raise TimeoutError def search_with_timeout(pipe, word, value): signal.signal(signal.SIGALRM, time_out) signal.alarm(1) r = re.compile(word) try: ret = r.search(value, re.I) b_ret = True if ret != None else False pipe.send(b_ret) except TimeoutError: pipe.send(False)在上面的代码中先的定义了一个信号,给定1s中以后触发,触发的函数为time_out然后执行正则表达式,如果在这1s中内无法完成,那么处理函数会被调用,会跑出一个异常,此时主线程终止当前任务的执行,进入到异常处理流程,这样就可以终止正则匹配,从而正常的返回。由于这个部分是一个新进程自然就涉及到不同进程之间的通信,在这个例子中我使用了管道进行通信。由于Python在创建子进程的时候可以进行参数的传入所以我只需要一个管道将数据从子进程中写入,再从朱金城中读取就好了。下面是调用该子进程的代码:pipe = multiprocessing.Pipe() p = multiprocessing.Process(target = search_with_timeout, args = (pipe[0], word, left_value)) p.start() p.join() #等待进程的结束 ret = pipe[1].recv() #获取管道中的数据
2018年04月27日
7 阅读
0 评论
0 点赞
2018-04-20
OLEDB存取BLOB型数据
现代数据库系统除了支持一些标准的通用数据类型以外,大多数还支持一种称之为BLOB型的数据。BLOB全称为big large object bytes, 大二进制对象类型,这种类型的数据通常用于存储文档、图片、音频等文件,这些文件一般体积较大,保存这些文件可以很方便的管理和检索这类信息。在MS SQLSERVER中常见的BLOB数据类型有text、ntext(n表示unicode)、image、nvarchar、varchar、varbinary等。其中image基本可以用来保存一切二进制文件,比如word、Excel、音频、视频等等类型。针对BLOB型数据,OLEDB也提供了对它的支持使用BLOB型数据的利弊一般数据库对BLOB型数据有特殊的处理方式,比如压缩等等,在数据库中存储BLOB数据可以方便的进行检索,展示,备份等操作。但是由于BLOB型数据本身比较大,存储量太大时数据量太大容易拖慢数据库性能,所以一般的说法都是尽量不要在数据库中存储这类信息。特别是图片,音视频。针对这类文件一般的做法是将其保存在系统的某个路径钟中,而在数据库中存储对应的路径操作BLOB型数据的一般方法一般针对BLOB不能像普通数据那样操作,而需要一些特殊的操作,在OLEDB中通过设置绑定结构中的一些特殊值最终指定获取BLOB型数据的一个ISequentialStream接口指针,最终会通过这个接口来进行BLOB型数据的读写操作判断一个列是否是BLOB型数据判断某个列是否是BLOB型数据一般通过如下两个条件:pColumnInfo[i].wType == DBTYPE_IUNKNOW : 包含当列信息的DBCOLUMNSINFO 结构体对象的wType值为DBTYPE_IUNKNOW,该列的类型为DBTYPE_IUNKNOW,该条件也被称为列类型判定pColumnInfo[i].dwFlags & DBCOLUMNFLAGS_ISLONG :当列信息中的dwFlag值为DBCOLUMNFLAGS_ISLONG,也就是说该列的标识中包含DBCOLUMNFLAGS_ISLONG属性,该判定条件也被称之为列标识判定当这两个条件之一成立之时,我们就可以断定这列为BLOB型数据BLOG型数据的绑定在进行BLOB型数据的绑定也有特殊要求,主要体现在下面几点:绑定结构的cbMaxLength 需要设置为0绑定结构的wType设置为DBTYPE_IUNKNOW为结构的pObject指针分配内存,大小等于DBOBJECT结构的大小指定pObject的成员 pObject->iid = IID_ISequentialStream pObject->dwFlags = STGM_READ为行缓冲长度加上一个IStream指针的长度,此时数据源不再提供查询到的数据而提供一个接口指针,后续对BLOB数据的操作都使用该指针进行最后使用完后记得释放pObject所指向的内存空间读取BLOB数据根据前面所说的创建绑定结构,并为绑定结构赋值,最终可以从结果集中获取到一个ISequentialStream接口指针。调用接口的Read方法可以读取到BLOB列中的数据,而BLOB数据的长度存储在绑定时指定的数据长度内存偏移处,这与普通列的长度存放返回方式是一样的,一般BLOB数据都比较长,这个时候就需要分段读取。在使用ISequentialStream接口操作BLOB型数据时需要注意的一个问题是,有的数据库不支持在一个访问器中访问多个BLOB数据列。一般BLOB数据列及其的消耗资源,并且数据库鼓励我们在设计数据库表结构的时候做到一行只有一列BLOB数据,因此很多数据库并不支持在一个访问器中读取多个BLOB数据。要判断数据库是否支持在一个访问器中读取多个BLOB数据,可以获取DBPROP_MULTIPLESTORAGEOBJECTS属性,该属性属于属性集DBPROPSET_ROWSET,它是一个只读属性,如果该属性的值为TRUE表示支持,为FALSE表示不支持。下面是一个读取BLOB型数据的例子,数据库中的表结构为:id(int)、text(image)、png(image)、jpg(image)void ReadBLOB(IRowset *pIRowset) { COM_DECLARE_INTERFACE(IColumnsInfo); COM_DECLARE_INTERFACE(IAccessor); DBORDINAL cColumns = 0; DBCOLUMNINFO* rgColumnsInfo = NULL; LPOLESTR lpszColumnsName = NULL; DBBINDING* rgBindings = NULL; DBBINDING** ppBindings = NULL; //绑定结构数组 DWORD *puDataLen = NULL; //当前访问器所需内存大小 DWORD *pulColCnt = NULL; //当前访问器中包含的项 ULONG ulBindCnt = 0; //访问器的数量 ULONG uBlob = 0; //当前有多少blob数据 HACCESSOR* phAccessor = NULL; HROW* hRow = NULL; DBCOUNTITEM ulGetRows = 0; ULONG uCols = 0; PVOID pData1 = NULL; //第1个访问器中数据的缓冲 PVOID pData2 = NULL; //第2个访问器中数据的缓冲 PVOID pData3 = NULL; //第3个访问器中数据的缓冲 HRESULT hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo); COM_SUCCESS(hRes, _T("查询接口pIColumnsInfo失败,错误码为:%08x\n"), hRes); hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &rgColumnsInfo, &lpszColumnsName); COM_SUCCESS(hRes, _T("获取结果集列信息失败,错误码为:%08x\n"), hRes); ppBindings = (DBBINDING**)COM_ALLOC(sizeof(DBBINDING*)); rgBindings = (DBBINDING*)COM_ALLOC(sizeof(DBBINDING) * cColumns); pulColCnt = (DWORD*)COM_ALLOC(sizeof(DWORD)); puDataLen = (DWORD*)COM_ALLOC(sizeof(DWORD)); for (int i = 0; i < cColumns; i++) { //如果当前访问器对应的绑定结构的数组的首地址为空,将当前绑定结构指针作为绑定结构数组的首地址 if (NULL == ppBindings[ulBindCnt]) { ppBindings[ulBindCnt] = &rgBindings[i]; } ++pulColCnt[ulBindCnt]; rgBindings[i].bPrecision = rgColumnsInfo[i].bPrecision; rgBindings[i].bScale = rgBindings[i].bScale; rgBindings[i].cbMaxLen = 10 * sizeof(WCHAR); rgBindings[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgBindings[i].dwPart = DBPART_LENGTH | DBPART_STATUS | DBPART_VALUE; rgBindings[i].eParamIO = DBPARAMIO_NOTPARAM; rgBindings[i].iOrdinal = rgColumnsInfo[i].iOrdinal; rgBindings[i].obStatus = puDataLen[ulBindCnt]; rgBindings[i].obLength = puDataLen[ulBindCnt] + sizeof(DBSTATUS); rgBindings[i].obValue = rgBindings[i].obLength + sizeof(ULONG); rgBindings[i].wType = DBTYPE_WSTR; if (rgColumnsInfo[i].wType == DBTYPE_IUNKNOWN || rgColumnsInfo[i].dwFlags & DBCOLUMNFLAGS_ISLONG) { rgBindings[i].cbMaxLen = 0; rgBindings[i].wType = DBTYPE_IUNKNOWN; rgBindings[i].pObject = (DBOBJECT*)COM_ALLOC(sizeof(DBOBJECT)); rgBindings[i].pObject->iid = IID_ISequentialStream; rgBindings[i].pObject->dwFlags = STGM_READ; uBlob++; } //记录下每个访问器所需内存的大小 puDataLen[ulBindCnt] = rgBindings[i].obValue + rgBindings[i].cbMaxLen; if (rgBindings[i].wType == DBTYPE_IUNKNOWN) { puDataLen[ulBindCnt] = rgBindings[i].obValue + sizeof(ISequentialStream*); } puDataLen[ulBindCnt] = UPGROUND(puDataLen[ulBindCnt]); //判断当前是否需要创建单独的访问器 if ((uBlob || rgBindings[i].iOrdinal == 0)) { ulBindCnt++; ppBindings = (DBBINDING**)COM_REALLOC(ppBindings, sizeof(DBBINDING*) * (ulBindCnt + 1)); puDataLen = (DWORD*)COM_REALLOC(puDataLen, sizeof(DWORD) * (ulBindCnt + 1)); pulColCnt = (DWORD*)COM_REALLOC(pulColCnt, sizeof(DWORD) * (ulBindCnt + 1)); } } //创建访问器 phAccessor = (HACCESSOR*)COM_ALLOC( (ulBindCnt + 1) * sizeof(HACCESSOR)); hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); COM_SUCCESS(hRes, _T("查询IAccessor接口失败,错误码为:%08x\n"), hRes); for (int i = 0; i < ulBindCnt; i++) { hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, pulColCnt[i], ppBindings[i], 0, &phAccessor[i], NULL); COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes); } //读取其中的一行数据 hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, 1, &ulGetRows, &hRow); COM_SUCCESS(hRes, _T("读取行数据失败,错误码为:%08x\n"), hRes); //读取第一个绑定结构中的信息 pData1 = COM_ALLOC(puDataLen[0]); hRes = pIRowset->GetData(hRow[0], phAccessor[0], pData1); for(int i = 0; i < pulColCnt[0]; i++) { if (ppBindings[0][i].wType == DBTYPE_IUNKNOWN) { DBSTATUS dbStatus = *(DBSTATUS*)((BYTE*)pData1 + ppBindings[0][i].obStatus); if (dbStatus == DBSTATUS_S_OK) { ULONG uFileLen = *(ULONG*)((BYTE*)pData1 + ppBindings[0][i].obLength); if (uFileLen > 0) { DWORD dwReaded = 0; PVOID pFileData = COM_ALLOC(uFileLen); ZeroMemory(pFileData, uFileLen); ISequentialStream *pSeqStream = *(ISequentialStream**)((BYTE*)pData1 + ppBindings[0][i].obValue); pSeqStream->Read(pFileData, uFileLen, &dwReaded); WriteFileData(_T("1.txt"), pFileData, dwReaded); } } } } //后续的部分就不再写出来了,写法与上面的代码类似 pIRowset->ReleaseRows(1, hRow, NULL, NULL, NULL); __CLEAR_UP: //后面是清理的代码由于我们事先知道数据表的结构,它有3个BLOB型数据,所以这里直接定义了3个缓冲用来接收3个BLOB型数据。为了方便检测,我们另外写了一个的函数,将读取出来的BLOB数据写入到文件中,事后以文件显示是否正确来测试这段代码首先还是与以前一样,获取数据表的结构,然后进行绑定,注意这里由于使用的是SQL Server,它不支持一个访问器中访问多个BLOB,所以这里没有判断直接绑定不同的访问器。在绑定的时候使用ulBindCnt作为当前访问器的数量,在循环里面有一个判断当(uBlob || rgBindings[i].iOrdinal == 0) && (ulBindCnt != cColumns - 1)条件成立时将访问器的数量加1,该条件表示之前已经有blob型数据(之前SQL不支持一个访问器访问多个BLOB,如果之前已经有BLOB数据了,就需要另外创建访问器)或者当前是第0行(因为第0行只允许读,所以将其作为与BLOB型数据一样处理),当这些条件成立时会新增一个访问器,而随着访问器的增加,需要改变ppBindings数组中的元素,该数组存储的是访问器对应的绑定结构开始的指针。数组puDataLen表示的是当前访问器所需内存的大小,pulColCnt表示当前访问器中共有多少列,针对这个表最终这些结构的内容大致如下图:绑定完成之后,后面就是根据数组中的内容创建对应的访问器,然后绑定、读取数据,针对BLOB数据,我们还是一样从对应缓冲的obValue偏移处得到接口指针,然后调用接口的Read方法读取,最后写入文件BLOB数据的写入:要写入BLOB型数据也需要使用ISequentialStream接口,但是它不像之前可以直接使用接口的Write方法,写入的对象必须要自己从ISequentialStream接口派生,并指定一段内存作为缓冲,以便供OLEDB组件调用写方法时作为数据缓冲。这段缓冲必须要保证分配在COM堆上,也就是要使用CoTaskMemory分配内存。这里涉及到的对象主要有IStream、ISequentialStream、IStorage、ILockBytes,同样,并不是所有数据源都支持这4类对象,具体支持哪些可以查询DBPROPSET_DATASOURCEINFO属性集中的DBPROP_STRUCTUREDSTORAGE属性来判定,目前SQL Server中支持ISequentialStream接口。虽然我们可以使用这种方式来实现读写BLOB,但是每种数据源支持的程度不同,而且有的数据源甚至不支持这种方式,为了查询对读写BLOB数据支持到何种程度,可以查询DBPROPSET_DATASOURCEINFO属性集合的DBPROP_OLEOBJECTS属性来判定通常有以下几种支持方式(DBPROP_OLEOBJECTS属性的值,按位设置):DBPROPVAL_OO_BLOB: 就是之前介绍的接口方式,使用接口的方式来读写BLOB数据DBPROPVAL_OO_DIRECTBIND: 可以直接绑定在行中,通过行访问器像普通列一样访问,也就是说它不需要获取专门的指针来操作,他可以就像操作普通数据那样,分配对应内存就可以访问,但是要注意分配内存的大小,每行中对应列中BLOB的数据长度差别可能会很明显,比如有的可能是一部长达2小时的电影文件,而有的可能是一部短视频,它们之间的差距可能会达到上G,而按照最小的来可能会发生截断,按最大的分配可能会发生多达好几个G的内存浪费DBPROPVAL_OO_IPERSIST:通过IPersistStream, IPersistStreamInit, or IPersistStorage三个接口的Persist对象访问DBPROPVAL_OO_ROWOBJECT: 支持整行作为一个对象来访问,通过结果集对象的IGetRow接口来获得行对象,但是这种模式会破坏第三范式,所以一般数据库都不支持DBPROPVAL_OO_SCOPED: 通过IScopedOperations接口来暴露行对象,通过这个接口可以暴露一个树形的结果集对象DBPROPVAL_OO_SINGLETON: 直接通过ICommand::Execute和IOpenRowset::OpenRowset来打开行对象下面是插入BLOB数据的一个实例//自定义一个 class CSeqStream : public ISequentialStream { public: // Constructors CSeqStream(); virtual ~CSeqStream(); public: virtual BOOL Seek(ULONG iPos); //将当前内存指针偏移到指定位置 virtual BOOL CompareData(void* pBuffer); //比较两段内存中的值 virtual ULONG Length() { return m_cBufSize; }; virtual operator void* const() { return m_pBuffer; }; public: STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppv); //读写内存的操作,这些是必须实现的函数 STDMETHODIMP Read( /* [out] */ void __RPC_FAR *pv, /* [in] */ ULONG cb, /* [out] */ ULONG __RPC_FAR *pcbRead); STDMETHODIMP Write( /* [in] */ const void __RPC_FAR *pv, /* [in] */ ULONG cb, /* [out]*/ ULONG __RPC_FAR *pcbWritten); private: ULONG m_cRef; // reference count void* m_pBuffer; // buffer ULONG m_cBufSize; // buffer size ULONG m_iPos; // current index position in the buffer };//插入数据第一列BLOB数据 //这里由于已经事先知道每列的数据结构,因此采用偷懒的方法,一行行的插入 pData1 = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pdwDataLen[nCol]); for(int i = 0; i < pulColCnt[nCol]; i++) { if (DBTYPE_IUNKNOWN == ppBindings[nCol][i].wType) { *(DBSTATUS*)((BYTE*)pData1 + ppBindings[nCol][i].obStatus) = DBSTATUS_S_OK; CSeqStream *pSeqStream = new CSeqStream(); GetFileData(_T("test.txt"), dwFileLen, pFileData); pSeqStream->Write(pFileData, dwFileLen, &dwWritten); pSeqStream->Seek(0); //写这个操作将缓存的指针偏移到了最后,需要调整一下,以便OLEDB组件在插入BLOB数据时从缓存中读取 HeapFree(GetProcessHeap(), 0, pFileData); *(ULONG*)((BYTE*)pData1 + ppBindings[nCol][i].obLength) = dwFileLen; *(ISequentialStream**)((BYTE*)pData1 + ppBindings[nCol][i].obValue) = pSeqStream; //此处不用release pSeqStream,COM组件会自动释放 }else { //根据数据库定义,此处应该为ID *(ULONG*)((BYTE*)pData1 + ppBindings[nCol][i].obLength) = 10; if (DBTYPE_WSTR == ppBindings[nCol][i].wType) { StringCchCopy((LPOLESTR)((BYTE*)pData1 + ppBindings[nCol][i].obValue), 10, SysAllocString(OLESTR("1"))); } } } hRes = pIRowsetChange->InsertRow(DB_NULL_HCHAPTER, phAccessor[nCol], pData1, &hNewRow); COM_SUCCESS(hRes, _T("插入第1列BLOB数据失败,错误码为:%08x\n"), hRes);在上面的代码中首先定义一个派生类,用来进行BLOB数据的读写,然后在后面的代码中演示了如何使用它在后面的一段代码中,基本步骤和之前一样,经过连接数据源、创建回话对象,打开表,然后绑定,获取行访问器,这里由于代码基本不变,为了节约篇幅所以省略它们,只贴出最重要的部分。在插入的代码中,首先查找访问器中的各个列的属性,如果是BLOB数据就采用BLOB数据的插入办法,否则用一般数据的插入办法。插入BLOB数据时,首先创建一个派生类的对象,注意此处由于后续要交给OLEDB组件调用,所以不能用栈内存。我们先调用类的Write方法将内存写入对应的缓冲中,然后调用Seek函数将内存指针偏移到缓冲的首地址,这个指针的作用就相当于文件的文件指针,COM组件在调用对应函数将它插入数据库时会采用这个内存的指针,所以必须将其置到首地址处。让后将对象的指针放入到对应的obvalues偏移中,设置对应的数据大小为BLOB数据的大小,最后只要像普通数据类型那样调用对应的更新方法即可实现BLOB数据的插入最后贴上两个例子的详细代码地址示例1:BLOB数据的读取示例2:BLOB数据的插入
2018年04月20日
6 阅读
0 评论
0 点赞
2018-04-14
OLEDB不使用SQL语句直接打开数据表
一般来说获取数据库表的方法是采用类似select * from table_name这样的sql语句。SQL语句必然伴随着数据库的解释执行,一般来说效率比较低下,而且使用SQL语句时需要数据库支持ICommandText对象,但是在OLEDB中它是一个可选接口,也就是有的数据库可能不支持,这个时候OLEDB给我们提供了一种方法让我们能够在不使用SQL的情况下操作数据库表对象。直接打开表对象需要使用IOpenRowset接口。该接口属于Session对象。打开数据库表的一般步骤声明一个DBID结构对象为结构对象的ekind(对象种类)字段赋值DBKIND_NAME值为结构对象的uName.pwszName字段赋值为表名调用IOpenRowset接口的OpenRowset方法,将DBID结构的指针传入,并让函数返回结果集对象IOpenRowset接口属于Session,可以在使用CreateSession时让其直接打开这个接口,而且该接口是必须实现的接口,因此不用担心获取不到的情况,得到这个接口后就可以直接使用接口的OpenRowset方法。OpenRowset函数原型如下:HRESULT OpenRowset( IUnknown *pUnkOuter, DBID *pTableID, //打开表时使用该结构 DBID *pIndexID, //打开索引时使用这个参数 REFIID riid, //返回对象的GUID ULONG cPropertySets, //给对应返回对象设置的属性集的个数 DBPROPSET rgPropertySets[], //给对应对象设置的属性集 IUnknown **ppRowset); // 返回的接口从函数定义上来,这种方式还可以用来打开索引使用实例BOOL OpenTable(IOpenRowset *pIOpenRowset, IRowset* &pIRowset) { DBID dbId = {0}; dbId.eKind = DBKIND_NAME; dbId.uName.pwszName = OLESTR("aa26"); DBPROP dbRowsetProp[4] = {0}; DBPROPSET dbRowsetPropset[1] = {0}; //运行直接使用对应接口函数对数据库进行增删改操作 dbRowsetProp[0].colid = DB_NULLID; dbRowsetProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[0].dwPropertyID = DBPROP_UPDATABILITY; dbRowsetProp[0].vValue.vt = VT_I4; dbRowsetProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_DELETE; //运行在删改的同时插入数据 dbRowsetProp[1].colid = DB_NULLID; dbRowsetProp[1].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[1].dwPropertyID = DBPROP_CANHOLDROWS; dbRowsetProp[1].vValue.vt = VT_BOOL; dbRowsetProp[1].vValue.boolVal = VARIANT_TRUE; //打开IRowsetUpdate接口,实现延迟更新 dbRowsetProp[2].colid = DB_NULLID; dbRowsetProp[2].dwOptions = DBPROPOPTIONS_REQUIRED; dbRowsetProp[2].dwPropertyID = DBPROP_IRowsetUpdate; dbRowsetProp[2].vValue.vt = VT_BOOL; dbRowsetProp[2].vValue.boolVal = VARIANT_TRUE; dbRowsetPropset[0].cProperties = 3; dbRowsetPropset[0].guidPropertySet = DBPROPSET_ROWSET; dbRowsetPropset[0].rgProperties = dbRowsetProp; HRESULT hRes = pIOpenRowset->OpenRowset(NULL, &dbId, NULL, IID_IRowset, 1, dbRowsetPropset, (IUnknown**)&pIRowset); return SUCCEEDED(hRes); }详细的代码请参考: 完整代码
2018年04月14日
11 阅读
0 评论
0 点赞
2018-04-08
OLEDB 枚举数据源
在之前的程序中,可以看到有这样一个功能,弹出一个对话框让用户选择需要连接的数据源,并输入用户名和密码,最后连接;而且在一些数据库管理软件中也提供这种功能——能够自己枚举出系统中存在的数据源,同时还可以枚举出能够连接的SQL Server数据库的实例。其实这个功能是OLEDB提供的高级功能之一。枚举对象用于搜寻可用的数据源和其它的枚举对象(层次式),枚举出来的对象是一个树形结构。在程序中提供一个枚举对象就可以枚举里面的所有数据源,如果没有指定所使用的的上层枚举对象,则可以使用顶层枚举对象来枚举可用的OLEDB提供程序,其实我们使用枚举对象枚举数据源时它也是在注册表的对应位置进行搜索,所以我们可以直接利用操作注册表的方式来获取数据源对象,但是注册表中的信息过于复杂,而且系统对注册表的依赖比较严重,所以并不推荐使用这种方式。枚举对象的原型如下:CoType TEnumerator { [mandatory] IParseDisplayName; [mandatory] ISourcesRowset; [optional] IDBInitialize; [optional] IDBProperties; [optional] ISupportErrorInfo; }顶层枚举对象的获取和遍历要利用数据源枚举功能,第一个要获取的枚举对象就是顶层枚举对象。或者称之为根枚举器,根枚举器对象的CLSID是CLSID_OLEDB_ENUMNRATOR,顶层枚举对象可以使用标准的COM对象创建方式来创建,之后可以使用ISourceRowset对象的GetSourcesRowset,得到数据源组合成的结果集。接着可以根据行集中的行类型来判断是否是一个子枚举对象或者数据源对象。如果是子枚举对象,可以利用名字对象的方法创建一个新的子枚举对象,然后根据这个枚举对象来枚举其中的数据源对象。一般来说这颗数结构只有两层。OLEDB提供者结果集在上面我们说可以根据结果集中的行类型来判断是否是一个子枚举对象或者数据源对象,那么怎么获取这个行类型呢?这里需要了解返回的行集的结构。字段名称类型最大长度含义描述SOURCES_NAMEDBTYPE_WSTR128枚举对象或数据源名称SOURCES_PARSENAMEDBTYPE_WSTR128可以传递给IParseDisplayName接口并得到一个moniker对象的字符串(枚举对象或数据源的moniker)SOURCES_DESCRIPTIONDBTYPE_WSTR128枚举对象或数据源的描述SOURCES_TYPEDBTYPE_UI22(单位字节)枚举对象或实例的类型,有下列值:DBSOURCETYPE_BINDER (=4)- URLDBSOURCETYPE_DATASOURCE_MDP (=3) - OLAP提供者DBSOURCETYPE_DATASOURCE_TDP (=1) - 关系型或表格型数据源DBSOURCETYPE_ENUMERATOR (=2) - 子枚举对象SOURCES_ISPARENTDBTYPE_BOOL2(单位字节)是否是父枚举器在枚举时根据SOURCES_TYPE字段来判断是否是子枚举对象,如果是则使用第二列的数据获取子枚举器的对象。如果根据名称创建子枚举器这里需要使用IMoniker接口。名字对象(moniker)的创建方法,是一种标准的以名字解析方法创建一个COM对象及接口的方法。相比于直接使用CoCreateInstance来说是一种更加高级的方法。这是标准的COM 对象的创建方式,其原理就是通过一个全局唯一的名称在注册表中搜索得到对应的CLSID,然后根据ID调用CoCreateInstance来创建对象。具体搜索过程可以参考COM基础系列在数据源枚举的应用中,先从ISourcesRowset对象中Query出IParseDisplayName接口,再调用该接口的ParseDisplayName方法传入上述表格中SOURCES_PARSENAME的值,得到IMoniker接口,最后调用全局函数BindMinker传递IMoniker接口指针并指定需要创建的接口ID。具体例子最后是一个具体的例子这个例子中创建了一个MFC应用程序,最后效果类似于前面几个例子中的OLEDB的数据源选择对话框。在例子中最主要的代码有两段:IDBSourceDlg对话框的EnumDataSource方法,和IDBConnectDlg方法Initialize。这两个分别用来枚举系统中存在的数据源对象和数据源对象中对应的数据库实例。当用户根据界面的提示选择了对应的选项后点击测试连接按钮来尝试连接。这里展示的代码主要是3段,枚举数据源,枚举数据源中对应的数据库实例,以及根据选择的实例生成对应的数据源对象接口并测试连接。void IDBSourceDlg::EnumDataSource(ISourcesRowset *pISourceRowset) { COM_DECLARE_INTERFACE(IRowset); COM_DECLARE_INTERFACE(IAccessor); COM_DECLARE_INTERFACE(IMoniker); COM_DECLARE_INTERFACE(IParseDisplayName); HROW *rgRows = NULL; HACCESSOR hAccessor = NULL; ULONG cRows = 10; DWORD dwOffset = 0; PVOID pData = NULL; PVOID pCurrentData = NULL; DBCOUNTITEM cRowsObtained = 0; LPOLESTR lpParamName = OLESTR(""); //利用顶层枚举对象来枚举系统中存在的数据源 HRESULT hRes = pISourceRowset->GetSourcesRowset(NULL, IID_IRowset, 0, NULL, (IUnknown**)&pIRowset); ISourcesRowset *pSubSourceRowset = NULL; ULONG ulEaten = 0; if (FAILED(hRes)) { ComMessageBox(NULL, _T("OLEDB 错误"), MB_OK, __T("创建接口ISourcesRowset失败,错误码:%08x\n"), hRes); goto __CLEAR_UP; } DBBINDING rgBinding[3] = {0}; //这里只关心我们需要的列,不需要获取所有的列 for (int i = 0; i < 3; i++) { rgBinding[i].bPrecision = 0; rgBinding[i].bScale = 0; rgBinding[i].cbMaxLen = 128 * sizeof(WCHAR); rgBinding[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgBinding[i].dwPart = DBPART_VALUE; rgBinding[i].eParamIO = DBPARAMIO_NOTPARAM; rgBinding[i].wType = DBTYPE_WSTR; rgBinding[i].obLength = 0; rgBinding[i].obStatus = 0; rgBinding[i].obValue = dwOffset; dwOffset += rgBinding[0].cbMaxLen; } rgBinding[0].iOrdinal = 3; //第三项是数据源或者枚举器的描述信息,用于显示 rgBinding[1].wType = DBTYPE_UI2; rgBinding[1].iOrdinal = 4; //第四列是枚举出来的类型信息,用于判断是否需要递归 rgBinding[2].iOrdinal = 1; //第一列是枚举出来的类型信息,用于获取子枚举器 hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); if (FAILED(hRes)) { ComMessageBox(NULL, _T("OLEDB 错误"), MB_OK, __T("查询接口pIAccessor失败,错误码:%08x\n"), hRes); goto __CLEAR_UP; } hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 3, rgBinding, 0, &hAccessor, NULL); if (FAILED(hRes)) { ComMessageBox(NULL, _T("OLEDB 错误"), MB_OK, __T("创建访问器失败,错误码:%08x\n"), hRes); goto __CLEAR_UP; } pData = MALLOC(dwOffset * cRows); while (TRUE) { hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &cRowsObtained, &rgRows); if(S_OK != hRes && cRowsObtained == 0) { break; } ZeroMemory(pData, dwOffset * cRows); for (int i = 0; i < cRowsObtained; i++) { pCurrentData = (BYTE*)pData + dwOffset * i; pIRowset->GetData(rgRows[i], hAccessor, pCurrentData); DATASOURCE_ENUM_INFO dbei = {0}; //将枚举到的相关信息存储到对应的结构中 dbei.csSourceName = (LPCTSTR)((BYTE*)pCurrentData + rgBinding[2].obValue); dbei.csDisplayName = (LPCTSTR)((BYTE*)pCurrentData + rgBinding[0].obValue); dbei.dbTypeEnum = *(DBTYPEENUM*)((BYTE*)pCurrentData + rgBinding[1].obValue); m_DataSourceList.AddString(dbei.csDisplayName); //显示数据源信息 g_DataSources.push_back(dbei); } pIRowset->ReleaseRows(cRowsObtained, rgRows, NULL, NULL, NULL); } pIAccessor->ReleaseAccessor(hAccessor, NULL); __CLEAR_UP: FREE(pData); CoTaskMemFree(rgRows); COM_SAFE_RELEASE(pIRowset); COM_SAFE_RELEASE(pIAccessor); }void IDBConnectDlg::Initialize(const CStringW& csSelected) { BSTR lpOleName = NULL; ULONG uEaten = 0; for (vector<DATASOURCE_ENUM_INFO>::iterator it = g_DataSources.begin(); it != g_DataSources.end(); it++) { if (it->csDisplayName == csSelected) { lpOleName = it->csSourceName.AllocSysString(); } } COM_DECLARE_INTERFACE(ISourcesRowset); COM_DECLARE_INTERFACE(IParseDisplayName); COM_DECLARE_INTERFACE(IMoniker); CoCreateInstance(CLSID_OLEDB_ENUMERATOR, NULL, CLSCTX_INPROC_SERVER, IID_ISourcesRowset, (void**)&pISourcesRowset); pISourcesRowset->QueryInterface(IID_IParseDisplayName, (void**)&pIParseDisplayName); pIParseDisplayName->ParseDisplayName(NULL, lpOleName, &uEaten, &pIMoniker); if (lpOleName != NULL) { SysFreeString(lpOleName); } HRESULT hRes = BindMoniker(pIMoniker, 0, IID_ISourcesRowset, (void**)&m_pConnSourceRowset); COM_SAFE_RELEASE(pIMoniker); COM_SAFE_RELEASE(pIParseDisplayName); if (FAILED(hRes)) { COM_SAFE_RELEASE(m_pConnSourceRowset); return; } COM_DECLARE_INTERFACE(IRowset) hRes = m_pConnSourceRowset->GetSourcesRowset(NULL, IID_IRowset, 0, NULL, (IUnknown**)&pIRowset); if (FAILED(hRes)) { return; } DBBINDING rgBind[1] = {0}; rgBind[0].bPrecision = 0; rgBind[0].bScale = 0; rgBind[0].cbMaxLen = 128 * sizeof(WCHAR); rgBind[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED; rgBind[0].dwPart = DBPART_VALUE; rgBind[0].eParamIO = DBPARAMIO_NOTPARAM; rgBind[0].iOrdinal = 2; //绑定第二项,用于展示数据源 rgBind[0].obLength = 0; rgBind[0].obStatus = 0; rgBind[0].obValue = 0; rgBind[0].wType = DBTYPE_WSTR; HACCESSOR hAccessor = NULL; HROW *rghRows = NULL; PVOID pData = NULL; PVOID pCurrData = NULL; ULONG cRows = 10; COM_DECLARE_INTERFACE(IAccessor); hRes = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor); if (FAILED(hRes)) { COM_SAFE_RELEASE(pIRowset); return; } hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1, rgBind, 0, &hAccessor, NULL); DBCOUNTITEM cRowsObtained; if (FAILED(hRes)) { COM_SAFE_RELEASE(pIRowset); COM_SAFE_RELEASE(pIAccessor); return; } pData = MALLOC(rgBind[0].cbMaxLen * cRows); while (TRUE) { hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &cRowsObtained, &rghRows); if (S_OK != hRes && cRowsObtained == 0) { break; } for (int i = 0; i < cRowsObtained; i++) { pCurrData = (BYTE*)pData + rgBind[0].cbMaxLen * i; pIRowset->GetData(rghRows[i], hAccessor, pCurrData); m_ComboDataSource.AddString((LPOLESTR)pCurrData); } pIRowset->ReleaseRows(cRowsObtained, rghRows, NULL, NULL, NULL); } FREE(pData); pIAccessor->ReleaseAccessor(hAccessor, NULL); }void IDBConnectDlg::OnBnClickedBtnConnectTest() { // TODO: 在此添加控件通知处理程序代码 CStringW csSelected = _T(""); ULONG chEaten = 0; m_ComboDataSource.GetWindowText(csSelected); COM_DECLARE_INTERFACE(IParseDisplayName); COM_DECLARE_INTERFACE(IMoniker); if (m_pConnSourceRowset == NULL) { MessageBox(_T("连接失败")); return; } HRESULT hRes = m_pConnSourceRowset->QueryInterface(IID_IParseDisplayName, (void**)&pIParseDisplayName); if (FAILED(hRes)) { return; } hRes = pIParseDisplayName->ParseDisplayName(NULL, csSelected.AllocSysString(), &chEaten, &pIMoniker); COM_SAFE_RELEASE(pIParseDisplayName); if (FAILED(hRes)) { MessageBox(_T("连接失败")); return; } COM_DECLARE_INTERFACE(IDBProperties); hRes = BindMoniker(pIMoniker, 0, IID_IDBProperties, (void**)&pIDBProperties); COM_SAFE_RELEASE(pIMoniker); if (FAILED(hRes)) { MessageBox(_T("连接失败")); return; } //获取用户输入 CStringW csDB = _T(""); CStringW csUser = _T(""); CStringW csPasswd = _T(""); GetDlgItemText(IDC_EDIT_USERNAME, csUser); GetDlgItemText(IDC_EDIT_PASSWORD, csPasswd); GetDlgItemText(IDC_EDIT_DATABASE, csDB); //设置链接属性 DBPROP connProp[5] = {0}; DBPROPSET connPropset[1] = {0}; connProp[0].colid = DB_NULLID; connProp[0].dwOptions = DBPROPOPTIONS_REQUIRED; connProp[0].dwPropertyID = DBPROP_INIT_DATASOURCE; connProp[0].vValue.vt = VT_BSTR; connProp[0].vValue.bstrVal = csSelected.AllocSysString(); connProp[1].colid = DB_NULLID; connProp[1].dwOptions = DBPROPOPTIONS_REQUIRED; connProp[1].dwPropertyID = DBPROP_INIT_CATALOG; connProp[1].vValue.vt = VT_BSTR; connProp[1].vValue.bstrVal = csDB.AllocSysString(); connProp[2].colid = DB_NULLID; connProp[2].dwOptions = DBPROPOPTIONS_REQUIRED; connProp[2].dwPropertyID = DBPROP_AUTH_USERID; connProp[2].vValue.vt = VT_BSTR; connProp[2].vValue.bstrVal = csUser.AllocSysString(); connProp[3].colid = DB_NULLID; connProp[3].dwOptions = DBPROPOPTIONS_REQUIRED; connProp[3].dwPropertyID = DBPROP_AUTH_PASSWORD; connProp[3].vValue.vt = VT_BSTR; connProp[3].vValue.bstrVal = csPasswd.AllocSysString(); connPropset[0].cProperties = 4; connPropset[0].guidPropertySet = DBPROPSET_DBINIT; connPropset[0].rgProperties = connProp; hRes = pIDBProperties->SetProperties(1, connPropset); if (FAILED(hRes)) { COM_SAFE_RELEASE(pIDBProperties); return; } COM_DECLARE_INTERFACE(IDBInitialize); hRes = pIDBProperties ->QueryInterface(IID_IDBInitialize, (void**)&pIDBInitialize); COM_SAFE_RELEASE(pIDBProperties); if (FAILED(hRes)) { return; } hRes = pIDBInitialize->Initialize(); if (FAILED(hRes)) { MessageBox(_T("连接失败")); }else { MessageBox(_T("连接成功")); } pIDBInitialize->Uninitialize(); COM_SAFE_RELEASE(pIDBInitialize); }最后,这次由于是一个MFC的程序,涉及到的代码文件比较多,因此就不像之前那样以代码片段的方式方上来了,这次我将其以项目的方式放到GitHub上供大家参考。项目地址
2018年04月08日
6 阅读
0 评论
0 点赞
1
...
21
22
23
...
34